C++语言基础(笔记全文)

来源:互联网 发布:linux开发工程师待遇 编辑:程序博客网 时间:2024/05/22 14:35
C++语言基础
一、C++文件的构成
1、头文件
在创建MFC应用时,类向导会为每个头文件添加宏定义,例如:
    #if !defined(AFX_ODBCVIEW_H__B82AC4A2_3DBE_4A29_A549_F9939BE498E3__INCLUDED_)
    #define AFX_ODBCVIEW_H__B82AC4A2_3DBE_4A29_A549_F9939BE498E3__INCLUDED_
    宏定义的目的是防止头文件被重复引用,当编译器编译头文件时,判断当前宏是否被定义,若没定义则进行定义,并编译头文件,否则略过当前头文件
a、引用头文件
使用<>格式引用系统的头文件,例如#include <iostream.h>
使用""格式引用自定义头文件,例如#include "student.h"
b、头文件中只提供声明信息
C++中允许使用内联函数,将函数的声明和实现放在一起,但是,这样做不容易形成一套风格,建议在头文件中只提供声明信息,在源文件中提供实现信息,使程序的逻辑结构更加清晰
2、源文件
以.cpp为扩展名,源文件中通常包含3部分内容,即源文件版权,版本的声明,对头文件的引用,以及系统功能的实现代码
二、C++的基本要素
1、标识符
在C++语言中,变量、常量、函数、标签和用户定义的各种对象,被称之为标识符
标识符由一个或多个字符组成,标识符可以是字母、数字或下划线,但是标识符的首字母必须是字母或下划线,而不能是数字
标识符是区分大小写的,且不能与C++中的关键字同名
标识符的长度是任意的,但前1024个字符是有意义的
a、不同类型的对象使用类型作为标识符的前缀
    例如:我们定义了一个整型变量,为了表示变量的类型,使用小写字符n做为前缀
    int nAge;//年龄
    int nKindCout;//种类数量
b、成员变量使用m_作为前缀
    在定义类的成员变量时,成员变量以m_作为前缀,例如:
    m_nAge;
    m_nSize;
c、类名以C作为前缀
    例如:class CStudent;
    class CPerson;
d、全局对象以g_作为前缀
    例如:g_Connection;
    g_Recordset;
e、接口以I为前缀
    例如:IUnknown;
    IInterface;
2、关键字
    关键字是C++编译器内置的有特殊意义的标识符,用户不能定义与关键字相同的标识符,在VC中,关键字默认都是以蓝色显示的
3、常量
    常量,顾名思义,其值在运行时是不能改变的,但是在定义常量时可以设置初始值,在C++中可以使用const来定义一个常量,例如:const int nMaxValue=1100;
对于常量,编译器会将其放置了一个只读的内存区域,其值不能被修改,但是可以应用在各种表达式中,如果用户试图修改常量,编译器将提示错误
4、变量
    其值可以改变的量叫变量,变量提供了一个具有名称(变量名)的存储区域,使得开发人员可以听过名称来对存储区域进行读写。每一个变量,都具有两个属性,也就通常所说的左值和右值,所谓左值,是指变量的地址值,即存储变量值的内存地址,右值是指变量的数据值,即内存地址中存储的数据。
例如:int a,b,c;
    变量初始化,即为其设置初始值
    例如:int a=10;
5、变量的存储类型
a、extern存储类型
    在此之前,先搞明白,变量的声明和定义
    变量的声明:是告知编译器变量的名称和数据类型
    变量的定义:将为变量分配存储区域
    使用extern关键字定义的变量:表示该变量只声明而不定义。
        例如:extern int var;
    好处:在一个应用程序包含多个文件时,如果在一个文件中定义一个全局变量,若其他文件中要访问该全局变量,则在另一个文件中通过使用关键字extern声明该全局变量,那么在该文件中就可以访问该全局变量了。
    例如:一个文件中定义一个整型的全局变量var; int var=0;
    而在另一个文件中通过extern关键字声明全局变量var,在该文件中就可以访问全局变量var了。extern int var;
b、static存储类型
    在此之前,先明白,局部变量和全局变量
    局部变量:是指在函数内定义的变量,在函数调用结束后,局部变量将消失
    全局变量:是指在函数体外定义的变量,它的声明周期随着程序的结束而结束
    static存储类型主要是针对局部变量的,如果定义一个静态的局部变量,它的声明周期在函数调用结束后依然存在,下次调用函数时,会依然保存上一次函数调用之后的值,并且,对于静态变量只执行一次初始化
    static存储类型表示变量在函数或文件内是“持久性”变量,通常也被称为静态变量。
    全局静态变量:使用static在函数外部定义的变量,它的作用域仅限于当前定义的文件,不能够被其他文件使用extern关键字访问,可以认为静态全局变量为半个全局变量(不能够被其他文件共享)
    局部静态变量:使用static在函数内部定义的变量,当函数调用结束时,变量并不被释放,依然保留其值,当下一次调用函数时,将应用之前的变量值,它的作用域时当前函数,它不能被外部函数或文件访问。
c、register存储类型
    使用register关键字定义的变量,表示变量将被放置在CPU的寄存器中。访问的时候比普通的变量快,register变量只能用于局部变量或作为函数的形式参数、而不能够定义全局的register,例如:
    register int slocal=10;
    对于寄存器变量,程序中不能获得变量的地址,也不能够称为全局变量和静态变量。但可以作为函数的形式参数
d、auto存储类型
    变量的存储方式主要有两种,静态存储和动态存储。
    全局变量、静态变量均属于静态存储,而普通的局部变量属于动态存储
    auto关键字表示变量被动态存储,默认情况下的局部变量均属于auto变量(也称为自动变量),定义一个全局的auto变量是非法的,因为全局变量属于静态存储,与auto变量相互冲突
注意:在定义变量时,不能同时使用register、extern、static和auto4个关键字中的任意两个关键字。
三、数据类型
1、简单的数据类型

基本数据类型

字符型

    单字符型 char

    宽字符型 wchar_t

整型 int

实型

    单精度 float

    双精度 double

逻辑型 bool

空值型 void

构 造 类 型

派生类型

    指针 type *

    引用 type &

复合类型

    数组 type[]

    结构体 struct

    共用体 union

    枚 举 enum

    类    class

2、布尔类型
在逻辑判断中只存在两个值,真和假,布尔型可以按整型对的,1表示真,0表示假。在程序中可以将布尔型对象赋给整型对象。例如:
    bool bRet=true;//定义布尔类型的对象
    int nData= bRet;//定义整型变量,将其初始化为bRet,即1;
    反之,也可以将整数类型对象赋值给布尔类型的对象。
3、数组类型
    数组描述的是一组相同数据类型元素的集合。在内存里,数组是连续存储的,数组按维数划分:可以分为一维、二维、多维数组。
a、一维数组的定义格式: int nArray[5];
    程序中可以通过元素在数组中的位置来访问,这被称之为索引访问或下标访问,例如:要访问上面数组中的第二个元素 nArray[1]=10;
    C++中数组的下标是从0开始的,以数组nArray为例,其下标范围是0~4
    在定义数组时,数组维数指定了数组中包含的元素数量,也就是说数组长度,数组维数必须大于等于1,并且必须输一个常量表达式,即在编译时就能够计算出它的值。
    例如:const int nSize=5;//定义一个整型常量
    int nArray[nSize];//合法的数组定义
    如果去掉“const”则是非法的,因为维数是常量表达式,而变量nSize只能在程序执行到其定义时才能确定大小
    数组初始化,例如: int nArray[5]={4,6,6,7,5};//数组定义并初始化
    可以对部分元素进行初始化,例如:int nArray[5]={4,6,6,7};没有被初始化的部分默认为0
    如果需要将数组中所有元素初始化为0,可以简写如下:int nArray[5]={0};
b、二维数组定义格式:数组类型 数组名[常量表达式][常量表达式];
    在二维数组中,第一维称为行维,第二维称为列维
    二维数组初始化,例如:
    int nArray[2][3]={{2,3,5},{5,6,7}};
    在定义二维数组时,如果需要提供全部元素的初始值,可以省略第一维的长度,但不能省掉第二维的长度,例如:int nArray[2][3]={5,6,7,4,5,3};
4、枚举类型
    在开发应用程序时,经常需要使用一组标记来描述对象的状态,例如一个记录集对象可以有打开状态、编辑状态和关闭状态,在程序中为了描述记录集的状态,通常定义一组常量,例如:
    const int RS_OPEN=1;
    const int RS_EDIT=2;
    const int RS_CLOSE=3;
如果需要编写一个函数根据参数描述的状态执行相关操作,可以编写一个类似下面的函数
void OperateRecordeset(int nSize)
{
    if(nSize==RS_OPEN){//执行打开操作}
    else if(nSize==RS_EDIT){//执行编辑操作}
    else if(nSize==RS_CLOSE){//执行关闭操作}
}
枚举类型能够很好的解决“编译器并不能限制用户提供的没有意义的参数”,它能够将一组枚举常量与一个枚举类型名称关联,如果将函数的参数定义为某一枚举类型,则只允许该枚举常量作为函数的实际参数,在C++中使用关键字enum定义一个枚举类型,例如:
    enum RecordsetState {RS_OPEN,RS_EDIT,RS_CLOSE};//定义枚举类型
    在定义枚举类型时。可以为各个枚举常量提供一个整数值,如果没有提供整数值,默认第一个常量为0,第二个常量为1,以此类推。
    例如:枚举类型作为函数参数
#include "stdafx.h"
#include <iostream.h>
//RS_EDIT的值默认是RS_OPEN+1,即4
enum RecordsetState{RS_OPEN=3,RS_EDIT,RS_CLOSE=6};
void OperateRecordset(RecordsetState nState)
{
if(nState==RS_OPEN)
{
cout<<"打开记录集"<<endl;
}
else if(nState==RS_EDIT)
{
cout<<"编辑记录集"<<endl;
}
else if(nState==RS_CLOSE)
{
cout<<"关闭记录集"<<endl;
}
}
int main(int argc,char* argv[])
{
OperateRecordset(RS_OPEN);//调用函数OperateRecordset
OperateRecordset(RS_EDIT);//调用函数OperateRecordset
OperateRecordset(RS_CLOSE);//调用函数OperateRecordset
return 0;
}
5、结构体类型
    结构体是一组变量的集合,它能够将不同的数据类型的对象组合成一个整体,以描述一个新的对象,例如:
const int CHAR_LEN=128;
struct Student
{
    char szName[CHAR_LEN];
    int nAge;
    char szSex[CHAR_LEN];
    char szAddress[CHAR_LEN];
} ;
其中:struct是关键字,Student是结构体类型名称,它可以是任意合法的标识符,在花括号内定义的变量是结构体Student的成员,有时也被称之为字段(Field)或元素(Element)。在结构体定义的结尾处是一个分号,且分号不可以省略。
在C++中定义结构体变量的格式与定义普通变量的格式相同,例如:Student stu; 
在定义结构体变量后,若要访问结构体中的各个成员,可以使用“.”符号来访问结构体中的成员,例如:stu.nAge=22;
6、公用体类型
    公用体类型提供了一种机制,使得多个变量(公用体中的成员)可以共享同一个内存地址
    例如:下面的代码定义了一种公用体类型 unType
    union unType
    {
        char cdate;
        int ndata;
    };
定义共用体与定义结构体类似,只是关键字不同,公用体使用union关键字,在公用体unType中,成员cdata与ndata的起始位置相同。
由于共用体成员公用内存空间,因此如果试图改变了一个共用体成员的值,其他成员的值也会发生改变,但对于共用体来说,通常一次只需要使用一个成员,当第一一个共用体变量时,编译器会根据共用体成员中占用最多的内存空间的变量分配空间。这样使得共用体中所有成员都能获得足够的空间。
注意:共用体变量同样需要声明后才能用,而且不能直接引用共用体变量,只能引用共用体变量中的成员
7、指针类型
在应用程序中,指针非常强大,它能够在调用函数中修改函数参数,能够进行动态分配,同时也是非常危险的。
所谓的指针,实际上是一个变量,它能够包含某一个内存地址。指针能够存储变量的地址,那么通过指针自然可以访问到变量地址空间中的数据,即变量的值。
为了防止歧义,我们将指针值描述为指针指向的地址,将指针指向地址中的数据(它指向的变量值)
称之为指针指向的数据
a、定义指针
格式:数据类型 *指针变量名;
例如:int *pLen;
在定义了一个指针之后,如果指针没有进行初始化,还不能够使用指针,在使用指针之前,必须为指针赋值,因为使用一个未初始化或未赋值的指针是非常危险的。由于指针的值要求的是一个变量的地址,因此需要将一个变量的地址赋给指针对象。可以使用取地址运算符“&”获取变量的地址。下面的语句将一个整型变量的地址赋值给指针变量。
int nLen=8;    int *pLen;    nLen=pLen;
与定义普通变量类似,在定义指针变量时也可以直接进行初始化。例如:
int nLen=8;    int *pLen=&pLen;
在指针被赋值或初始化之后,就可以利用指针来访问或修改其他变量了。为了访问指针指向的数据,需要在指针前使用“*”符号。例如:
    int nLen=8;    int *pLen=&pLen;    cout<<"pLen指向的数据"<<*pLen<<endl;
b、指针变量的赋值
    两个整型变量之间可以进行赋值,两个指针变量之间也可以进行赋值
    int nLen=10;
    int nCount=100;
    int *pLen=&nLen;//定义一个指针,初始化为nLen的地址
    int *pCount=&Count;//定义一个指针,初始化为nCount的地址
    pLen=pCount;//将pCount赋给pLen,此时pLen的值和pCount的值是相同的,即变量nCount的地址,那么pLen所指向的数据(*pLen)为100;即nCount的值
c、指针与数组
    使用指针不仅可以指向变量,还可以指向数据对象,对于数组来说,数组名表示的是数组的首地址,即数组中第一个元素的地址,因此将数组直接赋给指针对象是完全合法的。例如:
    int nArray[5]={1,2,3,4,5};//定义一个包含5个元素的整型数据,并进行初始化
    int *pIndex=nArray;//定义一个整型指针,将其初始化为一个数组对象
    这样,指针pIndex就指向了数组的首地址,即数组中的第一个元素的地址。
对于指针变量来说,进行“++”运算并不是简单的对指针值自加或者对指针指向的数据自加1,而是对指针的值自加“sizeof(指针类型)”
例如:使用指针变量遍历数组
    int nArray[5]={1,2,3,4,5};//定义一个包含5个元素的整型数据,并进行初始化
    int *pIndex=nArray;//定义一个整型指针,将其初始化为一个数组对象
    for(int i=0;i<5;i++)
    {
        cout<<*pIndex;//读取数组元素值
        pIndex++;//使指针指向下一个数组元素
    }
结果是:1    2    3    4    5
可以将花括号里的两句合并成一句
cout<<*pIndex++<<endl;
其作用是相同的。
d、指针数组
数组时相同数据类型元素的集合,那么数组中的元素可以是指针类型。此时的数组称之为指针数组。例如:int *pArray[3];//定义一个指针数组,数组中包含3个元素
数组中的每一个元素均是指针,即pArray[0],pArray[1],pArray[2]均指一个指针
为指针数组赋值,例如:
int *pArray[3];//定义一个指针数组,数组中包含3个元素
int nArray[3]={1,2,3};//定义一个数组,包含3个元素
for(int i=0;i<3;i++)
{
    pArray[i]=*nArray[i];//为指针数组元素赋值
    cout<<*pArray[i]<<endl;//读取数组元素值
}
C++中定义一个指针的指针,可以采用如下形式:数据类型 ** 变量名;    例如:int **pIndex;
这样int *pArray[3];    int **pIndex=pArray;就是合法的。
为了访问pArray数组中的元素,可以使用“*”运算符,即*pIndex,但是pArray数组中的元素时指针类型。为了访问数组元素 指向的数据,需要使用**pIndex来获取。
e、常量指针和指针常量
    在定义指针是,也可以使用关键字,例如:
    int nVar =10;//定义一个整型变量
    const int * pVar=&nVar;//定义一个整型常量的指针,并进行初始化
对于指针pVar来说,用户不能修改pVar指向的值,但是可以修改pVar指向的指针。例如:
    int nVar=10;
    const int *pVar=&nVar;
    //*pVar=20;//错误代码,不能修改指针常量的数据
    int nNum=20;
    pVar=&nNum;//修改指针常量指向的地址
在定义常量指针时,也可以将const关键字放置在数据类型之后,*符号之前的位置
    int const *pVar=&nVar;
在使用const关键字定义指针时,也可以将const关键字放置在指针变量的前面。但是指针变量的性质发生了改变。如下代码:
    int nVar=10;
    int * const pVar=&nVar;
    *pVar=20;//修改指针指向的数据
    int nNum=5;
    //pVar=&nNum;//代码错误,不能修改常量指针指向的地址
上面的代码,定义了一个常量指针pVar,使得用户不能够修改pVar指向的地址,但是可以修改pVar指向的数据。
在定义指针的时候也可以同时使用两个const关键字,例如:
int nVar=10;
const int *const pVar=*nVar;//定义一个常量指针常量
在上面的代码中,用户即不能修改常量指针pVar,也不能修改pVar指向的地址。
8、引用类型
引用时一个已存在对象的别名,一旦使用一个对象初始化引用对象以后,那么引用对象就成了目标对象的代名词,对引用对象操作,实际就是对目标对象进行操作。即使对引用对象使用取地址运算符“&”进行取地址运算,它将获取的目标对象的地址
格式:数据类型 &引用别名=目标对象;
例如:下面定义一个整型变量,然后定义一个引用对象,将其初始化为整型变量
int nKinds=100;
int &nRefKinds=nKinds;//定义一个引用对象,将其初始化为nKinds
注意:引用对象只能在初始化时设置目标对象,其后对引用对象的操作实际上都是对目标对象的操作。
可以将常量引用初始化为字面常量或面临值。
const int & nRefLen=3;//合法的语句
int const & nRefLen=3;//等同于const int & nRefLen=3;
这样,在程序中,就不能修改引用对象nRefLen的值了,因为目标对象是个常量
由引用的性质可以推断,引用一个常量是没有意义的,但是合法的
9、自定义类型
用户可以使用typedef关键字自定义数据类型,并不是真的创建新的数据类型,而是在已存在的数据类型定义一个新的名称
格式:typedef 数据类型 新的名称;
例如:typedef unsigned int UIMT;
当定义完新的数据类型后,就可以像定义普通的数据变量一样定义新数据类型变量
例如,下面的代码定义了一个UINT类型的变量
UNIT ivar=10;
注意:在程序中使用在定义类型的好处:是能够提高程序的移植性。同一种数据类型,在不同的操作系统上,其长度或性质可能是不同的,如果程序中统一使用了自定义类型,在修改程序时,只需要修改自定义类型的基本类型就可以了、代码的其他地方不需要改动。
10、数据类型转换
a、数值类型转换
    在数值计算(加减乘除等操作)过程中一些常用的数值转换规则
    ·如果两个操作数有一个操作的类型是long double,则另一个操作数无论何种类型,均被转换成long double
    ·如果两个操作数有一个操作类型是double,而一个操作数不是long double类型,则另一个操作数被转换为double类型
    ·如果两个操作数的类型都不是double(long double)类型。而一个操作数的类型是float,则另一个操作数被转换成float型
    ·如果两个操作数都是整数,编译器将所有小于int型的操作提升为int类型,然后进行进行比较
    强制类型装换
    格式:数据类型 (表达式)     或者    (表达式)数据类型
b、静态转换
    静态转换:使用static_cast关键字来实现强制类型装换。
    例如:使用static_cast将整型转换成double型
    double dbRate=static_cast<double>(10)/3;
c、动态转换
    动态转换通常用于一个类对象指针转换为另一个类对象的指针。如果源指针类型与目标指针类型不兼容,则转换的结果为NULL。程序中可以通过检测结果是否为NULL了来判断强制类型转换是否成功。使用关键字dynamic_cast进行动态转换
    动态转换只能对void*(无类型指针)类型或者类对象指针进行转换,并且类中必须包含虚方法,而不能对普通的数据类型进行转换
d、常量转换
常量转换用于将const对象转换为非const对象。分析下面的语句:
    const int MAX_LEN=100;
    int *pLen=const_cast<int *>&MAX_LEN;//常量转换
e、重解释装换
    它能够将任何指针类型转换为其他的指针类型,不安全,尽量少使用。
    转换时使用的关键字是:reinterpret_cast
f、数值类型与字符串间的转换
1)、字符串转换为整数
语法:int atoi(const char* string);
其中:string表示转换后的字符串;返回值:如果成功,返回值为字符串转换后的整数。若不成功,返回0;如果参数string转换后数值溢出,返回值未知。
例如:
    char *pHeight="100";
    char *pWidth="20";
    int nHight=atoi(pHeight);//将字符串转换为整数
    int nWidth=atoi(pWidth);
    int nArea=nHight+nWidth;
注意:在程序中使用atoi、atof等类型转换函数前,需要引用stdlib.h头文件。
2)、字符串转换为实数
语法:double atof(const char *string);
其中:string表示转换后的字符串;返回值:如果成功,返回值为字符串转换后的实数。若不成功,返回0.0;如果参数string转换后数值溢出,返回值未知。
    char *pHeight="14.45";
    char *pWidth="152.53";
    double dbHight=atof(pHeight);//将字符串转换为实数
    double  dbWidth=atof(pWidth);
    double  dbArea=dbHight+dbWidth;
3)、整数转换为字符串
语法:char *itoa(int value,char *str,int radix);
其中:
value:表示待转换的整数
str表示一个字符指针,用于存储函数转换后的字符串
radix:表示一个基数,返回2...36;通常为10,即采用十进制转换
返回值:函数返回一个指向str参数的字符指针
例如:
    int nDays=365;
    char pText[128]={0};//定义一个字符数组,用于存储字符串
    itoa(nDays,pText,10);//将整数转换为字符串
    cout<<pText<<endl;
4)、实数转换为字符串
语法:char *fcvt(double value,int count,int *dec,int sign);
其中:
value:表示待转换的实数
count:表示小数点后的位数,即精度
dec:表示整型指针,记录小数点位置
sign:是一个整型指针,表示数字的符合,0为正数,1为负数
返回值:成功,返回一个字符指针,否则为NULL
例如:
    double dbPI=3.1415926;
    int nDec;
    int nSign;
    char *pText=fcvt(dbPI,2,&nDec,&nSign);//将dbPI转换成字符串

四、运算符
1、赋值运算符
赋值与初始化的区别
对象的初始化只能进行一次,即在定义对象时为其提供一个初始值;在使用赋值运算符时,赋值运算符左右两端的操作数数据类型必须相同且兼容。
赋值运算符有:=、+=、-=、*=、/=、%=
2、算数运算符
主要包括+、-、*、/、%
注意:在使用除法运算符时,两个整型数相除的结果也是整数,例如5/2的结果是2
3、关系运算符和逻辑运算符
关系运算符主要有:>、<、==、>=、<=、!=
逻辑运算符主要有:&&(都真才真)、!!、||(都假才假)
4、自增自减运算符
即++、--
++、--所放的位置不同,表示的意义不同,例如:
int a;
a++;(先计算再自增)
++a;(先自增再计算)
5、位运算符
位运算符主要有:&(与)、|(或)、^(异或)、~(按位取反)、<<(左移)、>>(右移)
6、sizeof运算符
sizeof以字节数为单位确定某个数据类型或对象的大小写,例如:
int nLen=100;
int nSizeObj=sizeof(nLen);//获取变量nSizeObj的大小,为4,即4个字节
int nSizeType=sizeof(int);//获取int数据类型的大小,为4,即4个字节
seziof可以作用于某个对象或数据类型上,如果作用于某个对象上,可以写为“sizeof(对象)”或者“sizeof 对象”
sizeof不仅可以测试简单对象的大小,还可以测试数组的大小、指针的长度
例如:测数组
    char pText[5]="khjsk";//pText的长度是5
例如:测指针
    int *pLen;
    char* pText;
    double *pRate;
    int nIntPtr=sizeof(pLen);//获取指针的长度4
    int nCharPtr=sizeof(pText);//获取指针的长度4
    int nDoublePtr=sizeof(pRate);//获取指针的长度4
注意:在使用sizeof运算符测试指针长度时,无论指针的类型和指针所指向的数据是什么。只要在32为系统上,指针的长度都为4.因为指针存储的是对象的地址,
7、new和delete运算符
数据存储方式有两种
栈存储:存储空间小,生命周期短,局部变量和函数参数
堆存储:存储空间大,生命周期长,静态变量和全局变量
用户可以使用new运算符在堆中开辟一个空间,使变量存储在堆中,例如:
int *pData=new int;//定义一个指针,使用new在堆中开辟空间
*pData=100;//为指针在堆中的数据赋值
delete pData;//释放pSata在堆中占用的空间
运算符delete用于释放new运算符在堆中开辟的空间
new不仅可以对简单的对象分配空间,还可以为数组对象在堆中开辟空间,例如
int *pData=new int[3];//在堆中分配空间
pData[0]=2;
delete []pData;//释放堆中分配的数组空间
注意:当前指针指向了堆空间的地址,如果将其再指向其他地址,将导致无法释放堆空间
8、结合性和优先级
结合性:指表达式的整体计算方向,即从左至右或从右至左
优先级:表示的是运算符的优先执行顺序

五、表达式
1、逗号表达式
    是指使用逗号运算符将两个表达式连接起来,例如:
    10*5,8*9;
    其运算过程是,先计算表达式1,即“10*5”;再计算表达式2,即“8*9”
2、三目表达式
    即条件表达式,它由条件运算符和变量构成的,例如:
    int a=4,b=5,c=3;
    (a+b)>(b*c)?a:b;
若关系表达式的值为真,则整个条件表达式的结果是“:”符号前的操作,反之,是“:”符号后的操作


语句
一、语句的构成
语句由表达式和一个分号构成,例如:一个简单的语句
int nNum=200;
在编写语句时,如果只写一个分号,没有表达式的出现,这样的语句也是合法的,例如:
;
此时的语句被称为空语句
二、复合语句
复合语句也被称为块语句,是由一对大括号和一组简单语句构成,例如:
{
     int a=4,b=5,c=3;
    (a+b)>(b*c)?a:b;
}
注意:复合语句的结尾处是没有分号的
三、分支语句
1、if语句
if语句用于表达式的值有选择的执行语句,
格式:if(表达式){//语句}else{//语句}
注意:else字句在使用时采用就近原则,即else字句与其之前最近的if语句对应
2、switch语句
格式:
switch(表达式)
{
case 常量1:
    语句;
    break;
case 常量2:
    语句;
    break;
...
default:
    语句;
}
其中,表达式必须有序类型,不能是实数或字符串类型,表达式逐一与case语句的常量匹配,如果发现有常量与表达式相互匹配,则执行当前case部分的语句,直到遇到break语句位置。或者到达switch语句的末尾。当表达式没有与之匹配的常量,将执行default部分的代码,default语句是可选的。若没有default语句,switch语句将不执行任何动作
例如:
int a;
switch(a)
{
case 1:
    cout<<"1======="<<endl;
    break;
case 2:
    cout<<"2======="<<endl;
    break;
case 3:
    cout<<"3======="<<endl;
    break;
case 4:
    cout<<"4======="<<endl;
    break;
default:
    cout<<"0======="<<endl;
}
四、循环语句
1、while循环
格式:
while(表达式)
{
    //语句
}
例如:
int main(int argc,char* argv[])
{
    int nSum=0;
    int i=1;
    while(i<101)
    {
        nSum+=i;
        i++;
    }
    cout<<"结果为:"<<nSum<<endl;
    return 0;
}
注意:在循环体中i需要递减,否则循环条件永远为true,循环会无休止的进行,成为了死循环
2、do while语句
语法:
do
{
    //循环体
}while(表达式);
例如:
int main(int argc,char* argv[])
{
    int nSum=0;
    int i=1;
    do
    {
        nSum+=i;
        i++;
    }while(i<101);
    cout<<"结果为:"<<nSum<<endl;
    return 0;
}
3、for循环
语法:
for(变量初始赋值,循环条件,变量递增或递减)
{
    //循环体
}
例如:
int main(int argc,char* argv[])
{
    int nSum=0;
    for(int i=1; i<101;i++)
    {
        nSum+=i;
    }
    cout<<"结果为:"<<nSum<<endl;
    return 0;
}
4、嵌套循环语句
指的是:循环语句中还包含循环语句
例如:9*9乘法表的打印
注意:在使用嵌套循环时,要注意变量的作用域。
五、跳转语句
1、goto语句
goto语句能够在函数内部实现无条件跳转
格式:goto 标号;
例如:
int main(int argc,char* argv[])
{
    int nSum=0;
    int i=1;
label:
        nSum+=i;
        i++;
    if(i<100)
    {
        goto label;//转向标签
    }
    cout<<"结果为:"<<nSum<<endl;
    return 0;
}
其中:标号是用户自定义的一个标识符,以冒号结束,利用goto可以实现循环功能
注意:使用goto语句,应该注意标签的定义,其后不能紧接着出现“}”符号;goto语句不能越过复合语句外的变量定义的语句。
2、return语句
return语句用于退出当前调用的函数,当程序在一个函数中执行时,遇到return 语句将退出当前函数,返回调用该函数的地方开始执行下一条语句,
bool getPorint()
{
    return false;
}
在函数内使用return语句时,注意return语句返回的类型与函数的返回值类型相同。return语句不可以返回一个常量值,但可以返回一个变量、表达式。其中,变量或表达式的结果作为函数的返回值。
在使用return语句提前结束函数时,需要注意如果代码之前在堆中分配了内存,则在return语句之前应释放内存,防止产生内存泄露、
3、exit语句
exit语句用于终止当前调用的进程,通常用于结束当前的应用程序,实际上,exit是一个退出当前调用进程的函数,它包含一个整型参数,标识进程退出代码。与return不同,return只是退出当前调用的函数,除非当前函数是应用程序的主函数,return语句才结束当前调用的进程,而exit语句直接结束当前调用进程,无论当前函数是否是应用程序主函数。
void ExitDemo(const int ret)
{
    if(ret==0)
        exit(ret);
    else
        return;
}
当调用ExitDemo函数传递一个0值时将结束当前应用程序,传递一个非0值,则ExitDemo不会进行任何操作。
注意:在使用前需要引用头文件“iomanip.h”


面向对象程序设计
一、类与对象
1、类的定义
格式:
class 类名
{
    //数据和方法的定义
};
类的定义包括两个部分,类头和类体,类头由class关键字和类名构成,类体由一组大括号{}和一个分号“;”构成,类体中通常定义类的数据和方法。数据:描述的是类的特性,也称为属性或数据成员;方法:实际上是类中定义的函数,也称方法,描述的是累的行为。
例如:定义一个CUser类
class CUser
{
private:
    char m_Username[128];
    char m_Password[128];
public:
    bool Login()
    {
        if(strcmp(m_Username,"admi")==0 && strcmp(m_Password,"password")==0)
        {
            cout<<"登陆成功!"<<endl;
            return true;
        }
        else
        {
            cout<<"登陆失败!"<<endl;
            return false;
        }
    }    
};
当方法的定义放置在类体外时,方法的实现部分首先是方法的返回值,然后是方法的名称和参数列表,最后是方法体。
注意:当方法的定义放置在类体外时,方法名称前需要使用类名和域限定符”::“来标记方法属于哪个类
在使用类的成员变量时,不能对成员变量进行初始化,这一点与定义普通的变量不同
2、类的成员访问
类成员主要是指类中的数据成员和方法(方法也被称之为成员函数
访问修饰符
public:public成员也被称为公有成员,public成员可以在程序的任何地方进行访问
private:private成员也被称为私有成员,该成员只能在该类中访问,派生类以及程序的其他地方均不能访问私有成员。如果在定义类时,没有指定访问限定符,默认为private
protect:protect成员也被称为保护成员,该成员只能在该类和该类的派生类(子类)中访问,除此之外,程序的其他地方不能访问保护成员。
注意:
a、类成员是具有访问权限的,如果类的外部访问私有或受保护的成员将出现访问错误
b、在定义类时,也可以将类对象声明为一个指针,CUser *pUser;程序中可以使用new运算符来为指针分配内存。例如:CUser *pUser=new CUser;或者 :CUser *pUser=new CUser();
c、如果类对象被定义为指针,需要使用”->“运算符来访问类的成员,而不能使用”.“运算符来访问
d、如果将类定义为常量指针的话,则对象只允许调用const方法。
3、构造函数和析构函数
每一个类都有构造函数和析构函数,其中,构造函数在定义对象时被调用,析构函数在对象释放时被调用,如果用户没有提供构造函数,系统将提供默认的构造函数和析构函数
a、构造函数
构造函数是一个与类同名的方法,可以没有参数,有一个参数或多个参数,但是构造函数没有返回值。如果够咱函数没有参数,该函数被称为类的默认构造函数。
如果用户为类定义了构造函数,无论是默认构造函数还是非默认构造函数,系统均不会提供默认的构造函数。
一个类可以包含多个构造函数,各个构造函数之间通过参数列表进行区分
如果想要定义一个CUser对象的指针,并调用非默认构造函数进行初始化,可以采用如下的格式:CUser *pUser=new CUser("admin","password");//调用非默认构造函数为指针分配空间
类的构造函数通过使用冒号”:“运算符提供了初始化成员的方法,如:CUser():m_Username("admin"),m_Password("password"){}
复制构造函数
复制构造函数与其他的构造函数类似,以类名作为函数的名称,但是参数只有一个,即该类飞常量引用类型。因为复制构造函数的目的是为函数复制时间参数,没有必要在复制构造函数中修改参数,因此参数定义为常量类型
#include <iostream.h>
#include <string.h>

class CBook
{
public:
char m_BookName[128];
const unsigned int m_Price;
int m_ChapterNum;
CBook()//默认构造函数
:m_Price(68),m_ChapterNum(56)//初始化数据
{
strcpy(m_BookName,"C++实训");
cout<<"构造函数被调用"<<endl;
}
CBook(const CBook &book)//定义复制构造函数
:m_Price(book.m_Price)//初始化数据成员
{
m_ChapterNum=book.m_ChapterNum;
strcpy(m_BookName,book.m_BookName);
cout<<"复制构造函数被调用"<<endl;
}
};

void OutputBookInfo(CBook book)
{
cout<<"BookName is "<<book.m_BookName<<endl;
}

void main()
{
CBook book;
OutputBookInfo(book);
}
如果将
void OutputBookInfo(CBook book)函数中的参数改为
void OutputBookInfo(CBook &book)则赋值构造函数不会被调用,这是因为
OutputBookInfo函数是以引用类型作为参数,函数参数引用的方式传递,直接将实际参数的地址传递给函数,不涉及复制参数,因此没有调用复制构造函数
总结:编写函数时,尽量采用按引用的方式传递参数,这样可以避免调用赋值够咱函数,能够极大的提高程序的执行效率
b、析构函数
析构函数在对象超出作用范围或使用delete运算符释放对象时被调用,用于释放对象占用的空间。析构函数没有返回值,甚至void类型也不可以,析构函数也没有参数,因此析构函数是不能重载的
c、内联成员函数
在定义函数时,可以使用inline关键字将函数定义为内联函数,在定义类的成员函数时,也可以使用inline关键字将成员函数定义为内联函数。其实,对于成员函数来说,如果其定义在类体中,即使没有使用inline关键字,该成员函数被认为是内联成员函数
#include <iostream.h>
class CUser
{
private:
char m_Username[128];
char m_Password[128];
public:
inline char* GetUsername()const;
};
char *CUser::GetUsername()const//内联成员函数
{
return (char*)m_Username;
}
5、静态类成员
如果将类成员定义为静态类成员,则允许使用类名直接访问,使用static关键字修饰
例如:
class CBook
{
public:
    static unsigned int m_Price;
}
在定义静态数据成员时,通常需要在类体外部对静态数据成员进行初始化,例如:unsigned int CBook::m_Price=10;//初始化静态数据成员
对于静态数据成员来说,不仅可以通过对象访问,还可以直接使用类名访问。
例如:
CBook::m_Price;//使用类名直接访问静态成员
book.m_Price;//通过对象访问静态成员
在一个类中,静态数据成员是被所有的类对象所共享的,这就意味着无论定义多少个类对象,类的静态数据成员
只有一份,同时,如果某一个对象修改了静态数据成员,其他对象的静态数据成员(实际上是同一个静态数据成员)也将改变
注意:
a、静态数据成员可以是当前类的类型,而其他数据成员只能是当前类的指针或引用类型。
在定义类成员时,对于静态数据成员,其类型可以是当前类的类型,而非静态数据成员则不可以,除非数据成员的类型为当前类的指针或引用类型
例如:静态数据成员可以是当前类的类型
class CBook
{
public:
    static unsigned int m_Price;
    CBook m_Book;//错误,不允许在该类中定义所属类的对象
    static CBook m_VBook;//正确,静态数据成员允许定义类的所属类对象
    CBook *m_pBook;//正确,允许定义类的所属类型的指针类型对象
};
b、静态数据成员可以作为成员函数的默认参数
    在定义类的成员函数时,可以为成员函数指定默认参数,其参数的默认值也可以是累的静态数据成员,但普通的数据成员则不能作为成员函数的默认参数
例如:静态数据成员可以作为成员函数的默认参数
class CBook
{
public:
    static unsigned int m_Price;
    int m_Pages;
    void OutputInfo(int data=m_Price)//定义一个函数,以静态数据成员作为默认参数
    {
        cout<<data<<endl;
    }
    void OutputPage(int page=m_Pages)//错误定义,类的普通数据成员不能作为默认参数
    {
        cout<<page<<endl;
    }
};
静态成员函数
定义静态成员函数与定义普通的成员函数类似,只是在成员函数前加static关键字。
例如:static void OutputInfo();//定义静态成员函数
例如:
class CBook
{
public:
    static unsigned int m_Price;
    int m_Pages;
    static void OutputInfo()
    {
        cout<<m_Price<<endl;//正确的访问
        cout<<m_Pages<<endl;//非法的访问,不能访问非静态数据成员
    }
}
对于静态成员函数不能定义为const成员函数,即静态成员函数末尾不能使用const关键字。
在定义静态数据成员函数时,如果函数的实现代码处于类体之外,则在函数的实现部分不能再标识static关键字。
6、隐藏的this指针
对于非静态成员,每一个对象都有自己的一份拷贝,即每个对象都有自己的数据成员和成员函数
每个类的成员函数(非静态成员函数)中都隐含着一个this指针,指向被调用的对象的指针,其类型为当前类型的指针类型,在const方法中,为当前类类型的const指针类型
例如:
#include <iostream.h>
class CBook
{
public:
int m_Pages;
void OutputPages()
{
cout<<m_Pages<<endl;
}
};
int main(int argc,char* argv[])
{
CBook vbook,cbook;
vbook.m_Pages=521;
cbook.m_Pages=563;
vbook.OutputPages();
cbook.OutputPages();
return 0;
}
当bbook对象调用OutputPages时,this指针指向vbook对象。在OutputPages成员函数中,用户可以显示的使用this指针访问数据成员。
例如:
void OutputPages()
{
cout<<this->m_Pages<<endl;
}
实际上,编译器为了实现this指针,在成员函数中添加了this指针对数据成员或方法。类似于上面的OutputPages方法
说明:
为了将this指针指向当前调用对象,并在成员函数中能够使用,在每一个成员函数中都隐含着一个this指针作为指针参数,并在函数调用时将对象自身的地址隐含作为实际参数传递
例如:
void OutputPages(CBook *this)//隐含添加this指针
{
cout<<this->m_Pages<<endl;
}
7、运算符重载
如果我们在类中使用”+“重载运算符,则可以实现两个同类型对象的加法运算
运算符重载
#include <iostream.h>
#include <string.h>
class CUser
{
public:
char m_Username[128];
char m_Password[128];
int m_nLevel;//操作员级别
public:
CUser()//默认构造函数
{
m_nLevel=1;
strcpy(m_Username,"admin");
strcpy(m_Password,"password");
cout<<"构造函数被调用!"<<endl;
}
CUser operator+(CUser &refUser)//重载“+”运算符
{
CUser user;
user.m_nLevel=m_nLevel+refUser.m_nLevel;
return user;
}
};
上述代码中,为CUser类实现了+运算符的重载,运算符重载需要使用operator关键字,其后是需要重载的运算符,参数及返回值根据实际需要来设置,下面定义了3个CUser对象。将两个相加,赋值给第三个对象
int main(int argc,char* argv)
{
CUser User,comUser;
CUser defUser=User+comUser;
cout<<"m_nLever="<<defUser.m_nLevel<<endl;
return 0;
}
如果用户想要实现CUser对象与一个整数相加,可以通过修改重载运算符的参数来实现。例如:
CUser operator + (int nDate)//实现CUser对象与整数的加法运算
{
    CUser user;
    user.m_nLevel=m_nLevel+nData;
    return user
}
实际上CUser operator + (int nDate)与CUser operator + (CUser &refUser)函数是可以同时存在的,即表示重载函数。这样在CUser类中即可以实现两个CUser类对象的加法运算。也可以实现CUser对象与整数的加法运算。
我们知道整数的加法运算符合交换律,即A+B等于B+A,但对于重载的+运算符来说,”User+10“不可以写为”10+User“,为了使语句”CUser defUser=10+User;“能够通过编译,可以定义一个全局的运算符重载函数。
例如:
CUser operator+(int nData,CUser &refUser)//全局运算符重载
{
    CUser user;
    user.m_nLevel=refUser.m_nLevel+nData;
    return User;
}
这样语句”CUser defUser=10+User;“就成了合法的语句。在上述函数中,注意参数的顺序,整型要放置在前面,CUser类型放置在后面,不能够进行倒置。否则语句仍然不能通过编译
前置运算:在默认情况下,如果重载运算符没有参数,则表示前置运算,例如:
void operator++()
{
    ++m_nLevel;
}
如果重载运算符使用了整数作为参数,则表示后置运算,此时的参数值可以被忽略,它只是一个标识。例如:
void operator++(int)
{
    ++m_nLevel;
}
我们可以通过重载=运算符将一个整数赋给一个对象,要实现将对象赋值给整数,可以使用C++提供的转换运算符,例如:装换运算符
#include <iostream.h>
#include <string.h>

class CUser
{
public:
char m_Username[128];
char m_Password[128];
int nLevel;
public:
CUser()
{
m_nLevel=1;
strcpy(m_Username,"admin");
strcpy(m_Password,"password");
cout<<"构造函数被调用"<<endl;
}
operator int()//转换运算符
{
return m_nLevel;
}
};
转换运算符由关键字operator开始,其后是转换为的数据类型。在定义转换运算符时,之一operator关键字前没有数据类型,虽然转换符实际上返回了一个转换后的值,但是不能指定返回值的数据类型
下面在main函数中定义一个CUser对象,将该对象赋值给一个整型变量
int main(int argc,char* argv)
{
CUser user;
int nData=user;//将CUser对象赋值给提个整型变量
cout<<"m_nLevel="<<nData<<endl;
return 0;
}
注意:
a、并不是所有的C++运算符都可以被重载
    大多数是可以的,但是”::“、”?“、”:“、”.“运算符不能被重载
b、运算符重载的一些限制
    不能构建新的运算符
    不能改变原有运算符操作数的个数
    不能改变原有运算符的优先级
    不能改变原有运算符的结合性
    不能改变原有运算符的语法结构
c、运算符重载的基本准则
    一元操作数可以是不带参数的成员函数,或是带一个参数的非成员函数
    二元操作数可以是不带一个参数的成员函数,或者是带2个参数的非成员函数
    ”=“、”[]“、”->“和”()“运算符只能定义为成员函数
    ”->“运算符的返回值必须是指针类型或者能够使用”->“运算符类型的对象
    重载”++“和”--“运算符时,带一个int型参数,表示后置运算,不带参数表示前置运算
8、友元类和友元方法
    在程序开发时,如果两个类的耦合度比较紧密,能够在一个类中访问另一个类的私有成员会带来很大方便。C++提供友元类和友元方法来实现访问其他类的私有成员,当用户希望另一个类能够访问当前类的私有成员时,可以在当前类中将另一个类作为自己的友元类,这样在另一个类中就可以访问当前类的私有成员了
例如:定义友元类
#include <iostream.h>
#include <string.h>

class CItem
{
private:
char m_Name[128];
void OutputName()
{
cout<<"m_Name="<<m_Name<<endl;
}
public:
friend class CList;//为CList类作为自己的友元类
void SetItemName(const char *pchData)//定义共有成员函数,设置m_Name成员
{
if(pchData != NULL)
{
strcpy(m_Name,pchData);
}
}
CItem()//构造函数,初始化数据成员m_Name
{
memset(m_Name,0,128);
}
};
class CList
{
private:
CItem m_Item;
public:
void OutputItem();//定义共有成员函数
};
void CList::OutputItem()//OutputItem函数的实现代码
{
m_Item.SetItemName("beijing");//调用CItem类的共有方法
m_Item.OutputName();//调用CItem类的私有方法
}
上述代码中,在定义CItem类时,使用friend关键字将CList类定义为CItem类的友元,这样CList类中所有的方法都可以访问CItem类中的私有成员了。
假设需要实现只允许CList类的某个成员访问CItem类的私有成员,而不允许其他成员函数访问CItem类的私有数据。这可以通过友元函数来实现、
例如:
friend void CList::OutputItem(){}
对于友元函数来说,不仅可以是类的成员函数,还可以是一个全局函数,例如:
在类体内:friend void OutputItem(CItem *pItem);
9、类的继承
继承是面向对象的主要特征之一,它使得一个类可以从现有的类中派生,而不必重新定义一个新类。例如:
#include <iostream.h>
#include <string.h>

#define MAXLEN 128
class CEmployee
{
public:
int m_ID;
char m_Name[MAXLEN];
char m_Depart[MAXLEN];
CEmployee()
{
memset(m_Name,0,MAXLEN);
memset(m_Depart,0,MAXLEN);
cout<<"员工类构造函数被调用"<<endl;
}
void OutputName()
{
cout<<"员工姓名:"<<m_Name<<endl;
}
};

class COperator:public CEmployee
{
public:
char m_Password[MAXLEN];
bool Login()
{
if(strcmp(m_Name,"admin")==0 && strcmp(m_Password,"password")==0)
{
cout<<"登陆成功"<<endl;
return true;
}
else
{
cout<<"登陆失败"<<endl;
return false;
}
}
};
上述代码在定义COperator类时使用了“:”运算符,表示该类派生于一个基类,public关键字表示派生类的类型为公有型,其后的CEmployee表示COperator类的基类,也就是父类。
当一个类从另一个类继承时,可以有3种派生类型,分别为公有型(public)、私有型(private)和保护型(protect)。
public型表示对于基类中的public数据成员和方法,在派生类中仍然是public。
private型表示对于基类中的 private 数据成员和方法,在派生类中仍然是 private 
protect型表示对于基类中的 protect 数据成员和方法,在派生类中仍然是 protect 
void main()
{
COperator opera;
strcpy(opera.m_Name,"admin");
strcpy(opera.m_Password,"password");
opera.Login();
opera.OutputName();
}
用户在父类中派生子类时,可能存在一种情况,即在子类中定义了一个与父类同名的方法。此时,称为子类隐藏了父类的方法。
如果子类中隐藏了父类的方法,则父类中所有同名的方法(重载方法)均被隐藏
如果用户想要访问被隐藏的父类的方法,依然需要指定父类的名称。例如:
COperator opera;
opera.CEmployee::OutputName("admin");//调用基类中被隐藏的方法、
虚函数
在定义函数时,在函数前面使用关键字virtual关键字,使用虚方法可以实现类的动态绑定,即根据对象运行时的类型来确定调用哪个类的方法。而不是根据对象定义时的类型来确定调用哪个类的方法。
#include <iostream.h>
#include <string.h>

#define MAXLEN 128
class CEmployee
{
public:
int m_ID;
char m_Name[MAXLEN];
char m_Depart[MAXLEN];
CEmployee()
{
memset(m_Name,0,MAXLEN);
memset(m_Depart,0,MAXLEN);
cout<<"员工类构造函数被调用"<<endl;
}
virtual void OutputName()
{
cout<<"员工姓名:"<<m_Name<<endl;
}
};
class COperator:public CEmployee
{
public:
char m_Password[MAXLEN];
void OutputName()
{
cout<<"操作员姓名:"<<m_Name<<endl;
}
};
上述代码中,CEpmloyee类中定义了一个虚方法OutputName,在子类COperator类中改写了OutputName方法,其中,COperator中的OutputName方法仍为虚方法,即使没有使用virtual关键字。
void main()
{
CEmployee *pWork=new COperator();
strcpy(pWork->m_Name,"admin");
pWork->OutputName();
delete pWork;
}
此时,“ pWork->OutputName(); ”语句调用的是Operator类的OutputName方法、
在C++语言中,除了能够定义虚方法之外,还可以定义纯虚方法,也就是抽象方法。一个包含纯虚方法的类被称为抽象类。抽象类是不能够被实例化的,通常用于实现接口的定义。
例如:
#define MAXLEN 128;
class CEmployee
{
public:
    int m_ID;
    char m_Name[MAXLEN];
    virtual    void OutputName()=0;
};
抽象方法的定义时在虚方法的基础上在末尾添加”=0“;对于包含纯虚方法的类来说,是不能呢个够被实例化的
抽象类通常用于作为其他类的父类,从抽象类派生的子类如果是抽象类,则子类必须实现父类中所有的抽象方法
当从父类派生一个子类后,定义一个子类的对象时,它将依次调用父类的构造函数,当前类的构造函数来创建对象,在释放子类对象时,先调用的是当前类的析构函数,然后是父类的析构函数
定义一个基类类型的指针,调用子类的构造函数为其构建对象,当对象释放时,如果析构函数是虚函数,则先调用父类的析构函数,然后再调用子类的析构函数。如果析构函数不是虚函数,则只调用父类的析构函数。
C++语言允许子类从多个父类继承公有的和受保护的成员。
多继承
#include <iostream.h>
#include <string.h>

class CBird
{
public:
void FlyInSky()
{
cout<<"鸟能够在天空飞翔!"<<endl;
}
void Breath()
{
cout<<"鸟能够呼吸"<<endl;
}
};
class CFish
{
public:
void Swimming()
{
cout<<"鱼可以在水里游"<<endl;
}
void Breath()
{
cout<<"鱼能够呼吸"<<endl;
}
};
class CWater:public CBird,public CFish
{
public:
void Action()
{
cout<<"水鸟即能飞又能游"<<endl;
}
};
int main(int argc,char *argv[])
{
CWater water;
water.FlyInSky();
water.Swimming();
return 0;
}
上述代码中,CBird类与CFish类都有Breath方法,如果CWater类对象要调用Breath方法,此时,编译器将产生歧义,不知道要具体调用哪个Breath方法。为了让CWater类对象能够访问Breath方法,需要在Breath方法前具体制定类名。例如:
water.CFish::Breath();
water.CBird::Breath();
假如CBird类和CFish类均派生于同一个父类,例如CAnimal类
C++的虚继承机制
例如:
#include <iostream.h>
#include <string.h>

class CAnimal
{
public:
CAnimal()
{
cout<<"动物类被构造"<<endl;
}
void Move()
{
cout<<"动物能够移动"<<endl;
}
};

class CBird:virtual public CAnimal
{
public:
CBird()
{
cout<<"鸟类被构造"<<endl;
}
void FlyInSky()
{
cout<<"鸟能够在天空飞翔!"<<endl;
}
void Breath()
{
cout<<"鸟能够呼吸"<<endl;
}
};
class CFish:virtual public CAnimal
{
public:
CFish()
{
cout<<"鱼类被构造"<<endl;
}
void Swimming()
{
cout<<"鱼可以在水里游"<<endl;
}
void Breath()
{
cout<<"鱼能够呼吸"<<endl;
}
};
class CWater:public CBird,public CFish
{
public:
CWater()
{
cout<<"鱼鸟被构造"<<endl;
}
void Action()
{
cout<<"水鸟即能飞又能游"<<endl;
}
};
int main(int argc,char *argv[])
{
CWater water;
/*water.FlyInSky();
water.Swimming();
water.Action();
water.CFish::Breath();
water.CBird::Breath();*/
return 0;
}
通常,在定义一个对象时,先一次调用基类的构造函数,最后才是自身的构造函数,但对于虚继承来说,情况有些不同,在定义CWater类对象时,先调用基类CAnimal的构造函数,再调用CBird的构造函数,人啊后是CFish,最后是CWater构造函数。
10、类域
所谓的类域是指类的作用域。我们在定义一个类时,类体就是类的作用域,类的所有成员均处于类中。当程序中使用点运算符”.“和箭头运算符”->“访问类的成员时,编译器会根据运算符前面的类名来确定其类域,查找类成员。如果使用域运算符(::)访问类成员,编译器将根据运算符前面的类名来确定其类域。查找类成员
在定义类时,类成员的声明顺序也是很重要的,先声明的成员不能使用后声明的成员。
在类的定义中,通常对象在使用前都需要先声明,这个规则有2个例外情况,第一种情况是内联函数,对于内联函数来说,函数的声明被放置在函数定义处处理,因此,类中的所有成员对内联函数都是可见的
例如:
class CUser
{
private:
    char m_Username[128];
public:
    void SetLevel(int nLevel)//内联函数
    {
        m_nLevel=nLevel;
    }
    int m_nLevel;
}
上述代码即使m_nLevel成员的定义出现在SetLevel函数定义之后,但是由于SetLevel是内联函数,即在函数定义处访问类的数据成员,自然是合法的;
第二种情况出现在成员函数的默认参数,在定义类的成员函数时,可以为成员函数指定默认参数,这个默认参数不仅可以是一个常量值,还可以是类的数据成员,准确的说是类的静态数据成员,类的非静态数据成员不能成为成员函数的默认参数。
注意:如火在类中自定义了一个类型,在类域内该类型都被用来解析成员函数参数的类型名。
例如:在类的声明时定义了一个自定义类型,那么允许在方法定义时使用该类型作为参数类型。类内部的自定义类型作为成员函数参数类型。
public:
    typedef unsigned int UINT;//自定义类型UINT
void CUser::SetLevel(UINT nLevel){}//在类的声明之外使用UINT类型
注意:在对静态变量进行初始化时,初始化的语句也被认为在类域内
例如:
public:
    static int m_nLevel;//定义静态数据成员
    static int GetLevel()//定义静态成员函数
    {
        return 1;
    }
int CUser::m_nLevel=GetLevel();//利用静态成员函数进行初始化
11、局部类
类的定义也可以放置在函数中,这样的类被称为局部类
例如:
void LocalClass()
{
    class CBook
    {
    private:
        int m_Pages;
    public:
        void SetPages(int nPage)
        {
            if(m_Pages!=nPage)
                m_Pages=npage;
        }
        void GetPages()
        {
            return m_Pages;
        }
    };
    CBook book;
    book.SetPages(300);
    cout<<book.GetPages();
}
12、嵌套类
在C++中,允许在一个内的内部再定义一个类,这样的类被称之为嵌套类
对于内部的嵌套类来说,只允许其在外围的类域中使用,在其它类域或者作用域中是不可见的。但是可以通过使用外围的类域作为限定符来定义CNode对象。如下的定义时合法的
void main()
{
    CList::CNode node;
}
单数这样做通常是不合理的,也是有限制条件的。因为既然定义了嵌套类,通常都不允许在外界访问,这违背了使用嵌套类的原则,为了防止在外界直接使用嵌套类,可以将嵌套类定义为私有成员。
二、类模板
1、类模板的定义及应用
链表:链表的功能是向末尾节点添加数据,遍历链表中的节点,在链表结束时释放所有节点
单向链表
#include <iostream.h>

class CNode//定义一个节点类
{
public:
CNode *m_pNext;//定义一个节点指针,指向下一个节点
int m_nData;//定义节点的数据
CNode()//定义节点类的构造函数
{
m_pNext=NULL;
}
};
class CList//定义链表类
{
private:
CNode *m_pHeader;//定义头节点
int m_nNodeSum;//定义节点数量
public:
CList()//定义链表的构造函数
{
m_pHeader=NULL;
m_nNodeSum=0;
}
CNode *MoveTrail()//移动到尾节点
{
CNode* pTmp=m_pHeader;//定义一个临时节点,将其指向头节点
for(int i=1;i<m_nNodeSum;i++)
{
pTmp=pTmp->m_pNext;//获取下一个节点
}
return pTmp;
}
void AddNode(CNode *pNode)
{
if(m_nNodeSum==0)
{
m_pHeader=pNode;//将节点添加到头节点中
}
else
{
CNode* pTrail=MoveTrail();//搜索尾节点
pTrail->m_pNext=pNode;//在尾节点处添加节点
}
m_nNodeSum++;//使链表节点数量加1
}
void IterateList()
{
if(m_nNodeSum>0)//判断链表是否为空
{
CNode* pTmp=m_pHeader;//定义一个临时节点,将其指向头节点
cout<<pTmp->m_nData<<endl;
for(int i=1;i<m_nNodeSum;i++)
{
pTmp=pTmp->m_pNext;//获取下一个节点
cout<<pTmp->m_nData<<endl;
}
}
}
~CList()
{
if(m_nNodeSum>0)
{
CNode *pDelete=m_pHeader;//定义一个临时节点,指向头节点
CNode *pTmp=NULL;
for(int i=1;i<m_nNodeSum;i++)
{
pTmp=pDelete->m_pNext;//获取下一个节点
delete pDelete;
pDelete=pTmp;
}
m_nNodeSum=0;
pDelete=NULL;
pTmp=NULL;
}
m_pHeader=NULL;
}
};
void main()
{
CList list;
for(int i=0;i<5;i++)
{
CNode *pNode=new CNode();
pNode->m_nData=i;
list.AddNode(pNode);
}
list.IterateList();
}
上述代码在定义链表类CList时存在最大缺陷就是链表不够灵活,其节点只能是CNode类型,为了让CList能够适应各种类型的节点。一个最简单的办法就是使用类模板,类模板的定义与函数模板类似,以关键字template开始,其后是由尖括号构成的模板参数
模板链表
#include <iostream.h>

class CNode//定义一个节点类
{
public:
CNode *m_pNext;//定义一个节点指针,指向下一个节点
int m_nData;//定义节点的数据
CNode()//定义节点类的构造函数
{
m_pNext=NULL;
}
};


template <class Type>
class CList//定义链表类
{
private:
Type *m_pHeader;//定义头节点
int m_nNodeSum;//定义节点数量
public:
CList()//定义链表的构造函数
{
m_pHeader=NULL;
m_nNodeSum=0;
}
Type *MoveTrail()//移动到尾节点
{
Type* pTmp=m_pHeader;//定义一个临时节点,将其指向头节点
for(int i=1;i<m_nNodeSum;i++)
{
pTmp=pTmp->m_pNext;//获取下一个节点
}
return pTmp;
}
void AddNode(Type *pNode)
{
if(m_nNodeSum==0)
{
m_pHeader=pNode;//将节点添加到头节点中
}
else
{
Type* pTrail=MoveTrail();//搜索尾节点
pTrail->m_pNext=pNode;//在尾节点处添加节点
}
m_nNodeSum++;//使链表节点数量加1
}
void IterateList()
{
if(m_nNodeSum>0)//判断链表是否为空
{
Type* pTmp=m_pHeader;//定义一个临时节点,将其指向头节点
cout<<pTmp->m_nData<<endl;
for(int i=1;i<m_nNodeSum;i++)
{
pTmp=pTmp->m_pNext;//获取下一个节点
cout<<pTmp->m_nData<<endl;
}
}
}
~CList()
{
if(m_nNodeSum>0)
{
Type *pDelete=m_pHeader;//定义一个临时节点,指向头节点
Type *pTmp=NULL;
for(int i=1;i<m_nNodeSum;i++)
{
pTmp=pDelete->m_pNext;//获取下一个节点
delete pDelete;
pDelete=pTmp;
}
m_nNodeSum=0;
pDelete=NULL;
pTmp=NULL;
}
m_pHeader=NULL;
}
};

class CNet
{
public:
CNet* m_pNext;
int m_nData;
CNet()
{
m_pNext=NULL;
}
};


void main()
{
CList <CNode> nodelist;
for(int i=0;i<5;i++)
{
CNode *pNode=new CNode();
pNode->m_nData=i;
nodelist.AddNode(pNode);
}
nodelist.IterateList();
CList <CNet> netlist;
for(int j=0;j<5;j++)
{
CNet *pNode=new CNet();
pNode->m_nData=j;
netlist.AddNode(pNode);
}
netlist.IterateList();
}
模板由template关键字开始,其后是模板参数列表,由尖括号表示,模板参数列表不能为空,它可以是一个类型参数,即由关键字class或typename和一个标示符构成,也可以是非类型参数,由一个常量表达式表示,一个类模板可以有多个类型参数。例如:
template <class Type1,class Type2,typename Type3>
class CList
{//.....};
注意:每一模板参数必须由class或typename标识,不能够利用class或typename关键字定义多个模板参数
模板非类型参数由一个普通的参数声明构成,并且模板类型参数和非类型参数可以混合在一起使用,例如:
template <class Type,int nSize>
class CList
{//.....};
注意:
a、模板参数与全局对象重名
模板参数的名字在模板声明后直到模板介绍都可以在模板中使用们如果在全局区域定义了与模板参数相同的对象,则在模板中全局对象被隐藏
b、模板参数名不能与模板中自定义类型或类的名字相同
2、定义类模板的静态数据
在类模板中用户也可以定义静态的数据成员,只是类模板中的每个实例都有自己的静态数据成员,而不是所有的类模板实例共享静态数据成员。例如:
public:
    static int m_ListValue;//定义静态数据成员
类体外
int CList<Type>::m_ListValue=100;
main函数
CList<CNode> nodelist;//实例话类模板
nodelist.m_ListValue=200;//设置静态数据成员
CList<CNet> netlist;//实例化类模板
netlist.m_ListValue=300;//设置静态数据成员
cout<<"nodelist实例:"<<nodelist.m_ListValue<<endl;//输出静态数据成员
cout<<"netList实例:"<<netList.m_ListValue<<endl;//输出静态数据成员
结果:
nodelist实例:200
netlist实例:300
从上面的例子可以发现。模板实例nodelist和netlist均有各自的静态数据成员,但是,对于同一类型的模板实例,其静态数据成员是共享的。例如:同一类型的模板实现共享静态数据成员
void main()
{
    CList<CNode> nodelist;//实例话类模板
    nodelist.m_ListValue=200;//设置静态数据成员
    CList<CNode> netlist;//实例化类模板
    netlist.m_ListValue=300;//设置静态数据成员
    cout<<"nodelist实例:"<<nodelist.m_ListValue<<endl;//输出静态数据成员
    cout<<"netList实例:"<<netList.m_ListValue<<endl;//输出静态数据成员
}
结果:
nodelist实例:300
netlist实例:300
从上例可以发现。模板实例nodelist和netlist共享静态数据成员,因为他们的模板均为CNode
三、异常处理
1、异常捕获语句
为了防止程序由于意外的异常导致中断,可以使用异常捕捉语句来提前捕捉并处理异常。使得程序在产生异常时能够正常运行,在C++语言中,为了处理异常,提供了try和catch语句,try语句和catch语句实际上是一个语句块,try语句包含的是可能产生异常的代码,catch语句块包含的是处理异常的代码。例如:
捕捉异常:
void main()
{
    try
    {
        int nDiv=100;
        int nDivisor=0;
        int nRet=nDiv/nDivisor;
        cout<<nRet<<endl;
    }
    catch(...)
    {
        cout<<"产生异常!"<<endl;
    }
}
结果:常数异常!
从结果上看,当程序代码产生异常时,程序并没有中断,因为在程序代码(catch语句部分)中处理了该异常。
在catch语句中出现了...符号,表示处理所有异常,如果try语句中没有产生异常,则不会执行catch语句块中的代码,实际上一个try语句可以对应多个catch语句。每一个catch语句可以关联一个异常类,当try语句块中产生的异常与catch语句关联的异常匹配时,将执行该catch语句块中的代码。
注意:
try部分的语句是可能出现错误的语句,该语句必须由大括号包含,即使只有一条语句,catch部分的语句是处理异常的语句,该部分的语句也必须由大括号包含,在try程序段后,必须紧跟着一个活多个catch程序段,每个catch程序段用于捕捉一种类型的异常,并列的catch程序之间不允许插入其他语句。
2、抛出异常
异常不仅可以由系统自动触发(由执行的程序代码所致),也是由用户自己触发异常,自己触发异常的好处是用户可以定制自己的逻辑规则,例如:当用户的某一项操作不符合业务规则时,我们可以自定义一个异常,在该情况发生时触发异常,交由异常处理语句进行相应处理。
C++中提供了throw关键字用于触发异常,下例中,如果除数为0,则使用throw关键字抛出异常
void main()
{
    try
    {
        int nDiv=100;
        int nDivisor=0;
        if(nDivisor<=0)
        {
            throw"除数必须大于0";//抛出异常
        }
        int nRet=nDiv/nDivisor;
        cout<<nRet<<endl;
    }
    catch(...)
    {
        cout<<"运算失败!"<<endl;
    }
}
运行结果:运算失败!
自定义异常类
首先定义两个异常类
#include <iostream.h>
#include <string.h>

//除零异常类
class CDivZeroException
{
public:
char ExceptionMsg[128];
CDivZeroException()
{
strcpy(ExceptionMsg,"除零错误!");
}
};
//除数为负数异常类
class CNegException
{
public:
char ExceptionMsg[128];
CNegException()
{
strcpy(ExceptionMsg,"除数为负数错误");
}
};
//定义一个除法函数
bool Div(int nDiv,int nDivisor,int &nRet)
{
try
{
if(nDivisor==0)
{
throw CDivZeroException();
}
else if(nDivisor<0)
{
throw CNegException();
}
else
{
nRet=nDiv/nDivisor;
}
}
catch(CDivZeroException e)
{
cout<<e.ExceptionMsg<<endl;
return false;
}
catch(CNegException e)
{
cout<<e.ExceptionMsg<<endl;
return false;
}
return true;
}

int main(int argc,char *argv[])
{
int nRet;
bool bRet=Div(100,4,nRet);
cout<<nRet<<endl;
return 0;
}

























































原创粉丝点击