一个常见的面试题就是问“一个空类占几个字节”,想当然的是0个字节,但事实上类要区分不同的对象,比如:
1 EmptyClass obj1; 2 EmptyClass obj2;
即便是空类,也要能识别obj1和obj2的不同,所以空类仍然要占字节数,一般占一个字节。
还有一个针对空类的问题是“一个空类里面有什么”,就是想问编译器为这个空类自动生成了哪些成员函数。
很容易想到的是生成了默认的构造函数和析构函数,事实上还有拷贝构造函数和赋值运算符,所以,总共生成了四个成员函数。具体地说,就是你表面上写了
1 Class EmptyClass 2 { 3 };
但实际编译器为你加了四个成员函数,所以看起来像这样:
1 Class EmtpyClass 2 { 3 public: 4 5 // 构造函数 6 EmtpyClass(){} 7 8 9 10 // 析构函数 11 ~EmptyClass(){} 12 13 14 15 // 拷贝构造函数 16 EmptyClass(const EmptyClass& obj) 17 { 18 … 19 } 20 21 22 23 // 赋值运算符重载 24 EmptyClass& operator= (const EmptyClass& obj) 25 { 26 … 27 } 28 29 }; 30 31
拷贝构造函数和赋值运算符的函数体内容由成员变量决定,假设有成员变量var1和var2,那么拷贝构造函数和赋值运算法的函数体像这样:
1 EmptyClass(const EmptyClass& obj):var1(obj.var1), var2(obj.var2){} 2 3 4 5 EmptyClass& operator= (const EmptyClass& obj) 6 { 7 var1 = obj.var1; 8 var2 = obj.var2; 9 return *this; 10 }
赋值运算符要返回自身*this,是因为考虑到可以出现连等的情况,比如obj1 = obj2 = obj3,另外,这里都使用了自身类的引用,即EmptyClass&,这里的引用是必须要加的,这是因为:
(1) 引用修饰形参时,可以避免实参对形参的拷贝,一方面可以节省空间和时间资源,更为重要的是若实参对形参拷贝了,又会调用一次拷贝构造函数,这样拷贝构造函数就会一遍又一遍的被调用,造成无穷递归。
(2) 引用修饰返回值时,可以使返回的对象原地修改。比如(a=b) ++,这样返回的a对象还可以进行自增操作,如果不加引用,则因为生成的是原对象的拷贝,所以这样的自增操作并不使a本体自增。
对初学者而言,还要注意区分什么时候调用的是赋值运算符,什么时候调用的是拷贝构造函数。比如:
EmptyClass a(b); // 调用的是拷贝构造函数
EmptyClass a = b; // 调用的是拷贝构造函数
1 EmptyClass a; 2 a = b; // 调用的是赋值运算符
这里注意一下第二个和第三个例子,同样是等号,但却调用了不同的成员函数,重要的区别就要看是不是在这句话中新产生一个对象,第二个例子新产生一个对象,所以调用的是拷贝构造,第三个例子a在“=”前已经诞生了,所以调用的是赋值运算符。
本书中还讲到了一个特殊的情况,就是成员变量是const的,或者是引用,比如:
1 class SampleClass 2 { 3 private: 4 const int var1; 5 double& var2; 6 };
这时候编译器会报错,告诉你无法提供合适的构造函数,因为对于const变量以及reference,需要在声明的时候初始化,而编译器提供的默认构造函数显然无法做到这点。可以改成下面这样:
1 class SampleClass 2 { 3 private: 4 const int var1; 5 double& var2; 6 7 public: 8 SampleClass(const int a = 0, double b = 0):var1(a), var2(b){} 9 10 };
编译器不会报错了,但是如果像这样:
1 SampleClass obj1; 2 SampleClass obj2; 3 obj2 = obj1;
编译器会提示“operator =”函数在“SampleClass”中不可用,这说明编译器同样没有为SampleClass生成赋值运算符,因为var1和var2在初始化后,值就不能再改变了。但:
1 SampleClass obj1; 2 SampleClass obj2(obj1);
却是可以编译通过的,这是因为编译器可以生成默认的拷贝构造函数:
SampleClass(const SampleClass& s): var1(s.var1), var2(s.var2){}
这种生成方式并不会破坏const和reference的特性。
综上,编译器总是尽量地去生成这四个成员函数,但如果成员变量出现了const和reference,则编译器会拒绝生成默认的构造函数和赋值运算符重载函数。