C++引入类机制的目的:
-
从语法上将数据和操作捆绑在一起;
-
从语法上消除变量和函数的名字冲突;
-
从语法上允许服务端设计者控制数据和函数的访问权限;
-
从工程上支持数据封装、信息隐藏、将责任推向服务端、减小信息共享、独立问题域,减少信息的交换量,减少程序员之间的协调;
C++和C定义结构的区别:
-
C++中struct和class除了前者的缺省访问限制符为public,后者为private,继承关系中前者缺省为public,后者缺省为private之外,其他都相同;
-
C中的struct不能定义方法,表示一个纯数据结构,不能有嵌套函数,只能定义变量,但可以定义函数指针变量,从而具有模拟C++类方法的能力;
C++中常见类型的声明:(注意:[]的优先级大于*)
-
整型数:int a;
-
指向整型数的指针:int *a;
-
指向一个指向整形数的指针的指针:int **a;
-
数组元素为10个整型数的数组:int a[10];
-
数组元素为10个指向整形数的指针的数组:int *a[10];
-
指向一个由10个整型数组成的数组的指针:int (*a)[10];
-
指向一个有一个整型数参数,返回值为一个整型数的函数的指针:
int (*a)(int );
-
数组元素为10个上述函数指针类型的数组:int (*a[10])(int );
C++空类(没有定义任何变量和函数)中,默认产生的六种类成员函数:
1 class Empty 2 { 3 public: 4 Empty(); // 无参构造函数 5 ~Empty(); // 析构函数 6 Empty( const Empty& ); // 拷贝构造函数 7 Empty& operator=( const Empty& ); // 赋值运算符 8 Empty* operator&(); // 取址运算符 9 const Empty* operator&() const; // 常量取址运算符const 10 };
-
这样的对象在内存中有一个字节(1byte)的占位符,如果有虚拟指针,则另外增加4个字节的内存耗用,一共5个字节;
-
注意到有两个缺省的取值运算符,这样的设计时为了应对常量对象的声明;所以类定义中如果定义int getX();则应该定义对应的int getX() const重载函数(缺省类参数为const *this),以防止当申明常量对象时调用int getX()出现错误;
缺省构造函数(Default Constructor):
-
如果类定义中没有显示定义任何构造函数,则编译器会自动为我们生成一个缺省构造函数,也就是无参构造函数;但是,一旦我们定义任何类型的构造函数后,编译器不会自动生成缺省构造函数;
Cylinder(){……}
Cylinder c; Cylinder c[100]; Matrix {Cylinder c; ……};
-
如果我们在某类定义中定义了非无参构造函数,当此类型变量作为另一个类d的类成员,创建d类型的时候需要首先初始化其类成员,此时c需要一个无参构造函 数,但此时编译器不再自动生成,所以会出现错误;另外一种错误情况就是定义Cylinder c[100];的时候;所以好的类设计总会提供一个无参构造函数;
-
Cylinder cl;会隐式调用缺省(无参)构造函数;但是调用malloc不会隐式调用任何构造函数;仅仅是在堆内存申请一块内存,并且也是C++中唯一可以不调用构造函数而创建对象的方法;
Cylinder *c2=(Cylinder*)malloc(sizeof(Cylinder));
良好的构造函数设计:
class Point {…… Point(int a=0, int b=0):size(a),height(b) {……}};
-
上述类定义中的构造函数可实现四个功能:
如果创建对象时用户不传任何参数Point p;则可作为缺省无参构造函数;如果创建对象时用户仅提供一个参数Point p=20;Point p(10);则作为转换构造函数;
如果创建对象时用户提供两个参数Point p(10,28);则作为一般构造函数;
如果成员变量为对象,则成员初始化列表可以让成员仅初始化一次,提升程序性能;
拷贝构造函数(Copy Constructor):
-
无论用户是否定义了其他的构造函数,编译器都会自动生成一个拷贝构造函数,除非用户显示定义自己的拷贝构造函数;
-
但系统提供的拷贝构造函数在初始化拷贝时仅仅是将数据成员进行值复制(也就是浅复制),如果实例对象中有索引动态内存的指针变量,则拷贝前后会共享同一动态内存,此时需要特别注意动态内存的关系(这种情况下一般都需要重新定义拷贝构造函数)
Cylinder(const Cylinder & copy) {……}
Cylinder c1(1,3); Cylinder c2=c1; Cylinder c3(c1);
-
如果定义为按值传递,则会造成调用结构上的无限递归;函数调用过程中编译器会使用实际参数值初始化形式参数,也就是调用拷贝构造函数,所以最终导致无限递归调用;
-
另外注意区分拷贝构造函数和赋值操作符重载函数,前者在对象初始化的时候调用,后者在对象初始化之后调用,分别调用不同的函数;
Cylinder c1(1,3); Cylinder c2=c1; Cylinder c3; c3=c2;
转换构造函数(Conversion Constructor):
-
需要用户显式定义,编译器不会自动提供;此类构造函数有且只能有一个参数(特别像一般的构造函数),并且为非本类类型,则在调用函数的时候可写作,Object o(30);也可写作:Object c=30;
Cylinder(int n) {……};
首先调用转换构造函数利用30生成一个临时的匿名Object对象,然后使用拷贝构造函数或者赋值操作符重载函数进行与c的赋值操作;
Cylinder c1=20; Cylinder c2; c2=10;
所以使用内置基本类型对用户自定义类型进行初始化或者赋值时,编译器会首先检查这个自定义类型是否具有转换构造函数,然后再判断是否是语法错误;
-
如果某个函数的形式参数为Object类型,但仅提供一个int类型的实际参数,此时如果Object类定义中定义了针对int类型的转换构造函数,则转换过程如下:
1). 编译器先用转换构造函数将int类型的值生成一个临时的匿名Object对象;2). 如果是其他数值类型(如double),编译器还会首先进行位提升或者类型强制转化(double->int,就是针对其他类型和转换构造函数的参数类型),之后再调用转换构造函数生成一个匿名的临时Object对象;
3). 最后将这个临时的Object对象作为真正的参数传入函数;
Cylinder(int n) {……};
void Copy(Cylinder *to, Cylinder &from) {……}
Cylinder c1; Copy(&c1,70.8);
注意参数不能是指针类型,因为转换构造函数创建的是一个变量,而不是指针;
析构函数(Destruction Constructor):
-
用于释放实例类在运行过程中申请的系统资源,比如堆内存;析构函数没有参数,没有返回值,有且仅有一个;如果没有显示定义则编译器会提供一个什么也不做的 缺省析构函数;析构函数调用的顺序需要和构造函数调用的顺序相反,比如类成员中的对象需要优先调用自己的析构函数,继承关系中也需要让基类对象优先调用析 构函数;
-
调用析构函数的情景,其作用是在对象的内存空间被撤销之前做一些清理工作;
1). 对于extern和static变量,在main函数结束之后,程序结束之前;2). 对于auto变量,在作用域结束之前;3). 对于heap变量,new/delete类型的话在delete的时候;malloc/free类型的话在free函数调用之前需要手动调用析构函数;
虚函数设计中需注意的问题:
-
虚拟函数利用多态机制基于同一调用接口实现不同的功能;
-
如果子类定义一个与父类中虚函数仅仅名字相同的函数(参数不同,返回类型不同),则不能产生多态机制,而仅仅是函数覆盖;(也就是说虚函数必须要求函数名和参数都相同,并且基类的函数标注virtual);
-
虚函数实现依赖vtable,而vtable与实例对象绑定(this指针),所以仅有类成员函数才可声明为虚函数;全局函数,构造函数,static和inline修饰的函数则不能:
static函数独立于实例对象;
inline函数在编译时期就将函数声明替换为方法体;
constructor函数调用的时候并不存在完整的对象;当调用构造函数的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定,不能是虚函数;而析构函数可以是虚函数;
-
如果不能保证某个类不会有子类,一般需要将destructor函数定义为虚函数;
-
vtable在编译期就已经确定并与类定义绑定,实例对象则含有指向VT的指针;
Const关键字:
-
修饰变量:此变量的值在定义时必须初始化,之后不可再赋值;甚至不可将其地址赋值给其他指针变量,仅当指针变量和引用变量前使用const时才可,此指针和引用变量不可用作左值;
-
用于一般变量申明:表示常量变量,仅能在声明的时候赋值;
-
用于参数申明:表示参数作为输入值,否则参数作为输出值;
-
用于函数的返回值:表示返回值不能用于左值(不能改变);
-
修饰函数:在函数名之后,{}之前添加const表示函数内部不会修改参数的值;如果此函数为类成员函数则说明不会修改所有成员变量的值;
-
用于一般类成员函数申明(添加于函数末尾):表示此函数不修改此类的成员变量(const this的指针)
class Cylinder {…… void show() const {……}};
Point* colsestOne(const Point &p) {return …… this :
const_cast<Point*>(&p);}
使用const_cast<Type>(ConstType)可以将同类型的由const修饰的变量转换成没有const属性的变量,但仅去除常量属性仍旧不能修改变量的值;
各种程序设计机制存在的用意:
-
用static修饰的类成员函数:此函数与类实例对象相互独立,所以完全可为全局函数,但为了从语法层面表明与某个类别相关,则在类定义内部实现,方便传达程序员的意图;可以直接使用类名调用,也可以使用对象调用;
Point::samePoint(p1, p2);
-
用const修饰的类成员函数:此函数不改变类对象成员的成员变量(全局函数则不改变参数变量),传入const *this指针;使用const修饰传入参数,则表示此参数为输入值,不可改变,否则为输出值,可以改变;
void show() const {……}
-
用Rational(long n=0, long d=1);定义构造函数:这个构造函数可以作为一般构造函数,转换构造函数,缺省构造函数,减少代码量;转换构造函数的调用通常为隐式行为,如果我们要求 类型转换需要显式执行,则在这个构造函数函数前添加explicit,则仅当使用强制转换操作时才能调用此函数,不能隐式转换;
-
用friend Rational operator+(const Rational &x, const Rational &y)定义友元函数:具有和类成员函数一样的访问权限,但编译器不为其缺省加入this指针,一般为全局函数或者另一个类定义的函数。用于解决重 载运算符函数中,左操作数为数值类型,右操作数为对象类型的情况(一般的类的重载运算符函数中左操作数必须为对象本身,也就是缺省的this),当左操作 数为数值类型时,首先调用类型转换函数将其变成类类型,然后调用对应的类重载操作符函数;
-
使用成员初始化列表:在实现构造函数时在构造函数头和函数体之间加入定义,有4个方面的优势:使用成员初始化列表:在实现构造函数时在构造函数头和函数体之间加入定义,有4个方面的优势:
可以避免类成员变量为对象变量时进行的不必要的缺省构造函数的调用(初始化列表并没有规定变量的初始化顺序,变量在类定义中的顺序决定其初始化顺序),提升性能;
可以避免因为类变量定义中没有定义缺省构造函数而出现语法错误;
可以避免const变量的错误初始化,类成员中的const变量由于只能在定义时初始化,所以需要使用初始化列表进行初始化;
可以避免引用类型变量的错误初始化,类成员中的引用类型变量也只能在定义时初始化,所以需要使用初始化列表进行初始化;
-
类定义中总是提供缺省构造函数:防止类型以无参数方式创建时发生错误(数组元素或者类成员变量);
-
将拷贝构造函数声明为private:禁止按值传递此类对象;也可将其他对象的函数声明为private,达到对应的限制;