读书笔记_Effective_C++_条款五:了解C++默默编写并调用哪些函数

 

一个常见的面试题就是问“一个空类占几个字节”,想当然的是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,则编译器会拒绝生成默认的构造函数和赋值运算符重载函数。

posted @ 2013-03-17 12:43  Jerry19880126  阅读(1034)  评论(0编辑  收藏  举报