Effective2 c++ 学习笔记(Item5)
item 5: know what functions c++ silently writes and calls
编译器可能会自动创建的函数
如果你只是定义一个空类,编译器在编译得时候会根据需要帮你添加默认构造函数,赋值函数,拷贝构造函数,和析构函数。编译器帮你添加的这部分隐藏代码都是public并且inline的,需要注意的是,在类定义里面直接编写的函数即使没有inline关键字也是内联函数。编译器只有判断你需要使用这些隐藏函数的时候才会帮你创建。如果你自己没有定义拷贝构造函数,全文也没用到拷贝构造函数,那么编译器是不会生
成这部分隐藏代码的。还有一些情况,编译器因为无法自动生成隐藏代码,而直接报错。
编译器编译构造函数,拷贝构造函数,拷贝赋值函数,析构函数的递归特殊性
- 编译器在编译某个类的构造函数时(包括构造函数和拷贝构造函数),会先递归编译其父类的构造函数。不论这个类的构造函数是你明确自己写的还是编译器默认产生的默认构造函数。
- 编译器在编译某个类的拷贝赋值函数时,如果是你明确自己写的拷贝赋值函数,那么编译器不会递归调用父类的拷贝赋值函数去进行父类成分的赋值。如果编译器自动为你生成的,那么编译器会递归去调用父类的拷贝赋值函数。
- 编译器在编译析构函数的时候,无论析构函数是否是自己明确写的,都会有个递归调用,先调用自己的析构函数,然后析构父类。
情况1,编译器不帮你创建默认构造函数
当你明确的写下了自己的构造函数,不管这个构造函数是否带参数,系统都不再会帮你创建不带参数的默认构造函数。
构造函数是个很特殊的函数,当程序运行他的时候,可不是简单的只运行构造函数里面代码,相反他还需要递归地去调用父类的默认构造函数。需要特别注意的是递归调用父类的构造函数是默认的构造函数就是不带任何参数的构造函数。如果父类没有这样的默认构造函数,编译器也没有自动创建,那么这个类是没有办法被继承的。
所以会有下面有趣的情况发生, 下面的代码是无法编译通过的,原因是在main函数里面,调用Dog myDog
这个语句会去调用Dog的默认构造函数,这个构造函数是编译器自动帮忙创建了,因为你并没有在Dog类定义里明确写自己的构造函数。编译的Dog的构造函数时,先递归去找父类Animal的默认构造函数。注意只找父类Animal的默认构造函数。此时编译器找不到Animal的默认构造函数,因为在Animal的类定义里面你已经明确写下了一种构造函数,编译器不会生成Animal的默认构造函数,于是编译失败。如果你明确写下的正好是默认构造函数,那么编译将不受影响。
class Animal
{
public:
Animal(string notes){}
};
class Dog: public Animal
{
};
int main()
{
Dog myDog;
}
情况2: 编译器自动创建拷贝赋值函数失败
比如下面的代码中,因为类里面有两个不能赋值的对象string& schoolName; const int memberLimit;
所以编译器在发现你没有创建拷贝赋值函数=
,但是代码中又用到的情况下,编译器尝试创建拷贝赋值函数,但是创建的时候发现这两个不能赋值对象,所以编译报错。
同样的你没有自己写拷贝构造函数,但是代码中用到了,编译器也会尝试帮你创建。拷贝构造相当于一次初始化。classRoom bRoom(aRoom)
的含义是给bRoom分配空间,然后把初始值一次性初始化到这段空间上。引用 和 const对象允许初始化。所以这里编译器能够成功创建拷贝构造函数的。
Class classRoom
{
public:
classRoom(string sN,int limit):schoolName(sN),memberLimit(limit)
{
}
string& schoolName;
const int memberLimit;
}
int main()
{
string school = "XX";
classRoom aRoom(school, 50);
classRoom bRoom(aRoom);
bRoom = aRoom;
return 0;
}
c++规定引用是不能修改的,引用相当于别名,在定义时就关联好了, 在关联的时候就固定好映射关系了。如果想把这个别名给其他人用,是不允许的。const变量是无法在定义外赋值的。此时只能自己手动创建拷贝构造函数,绕过这两个变量的赋值。
还有一种情况,编译器会拒绝自动帮你生成而导致编译失败。如果目标类的基类的拷贝赋值函数是private,那么编译器尝试创建目标类的默认拷贝赋值函数时, 会尝试调用基类的拷贝赋值函数先完成基类成分的拷贝赋值。但此时编译器会发现对方是private,目标类没有权限去调用,此时编译器会拒绝编译而编译失败。
可以用下面的代码简单实验验证
Class Room
{
public:
int size;
private:
Room& operator=(const Room& rhs)
{
if(this ==&rhs)
return (*this);
size = rhs.size;
return (*this); }
};
class ClassRoom : public Room
{
public:int studentCnt;
}
int main()
{
ClassRoom oneClass;
oneClass.studentCnt = 10;
oneClass.size = 100;
ClassRoom anotherClass;
anotherClass = oneClass;
}
这个代码编译不过,因为在编译anotherClass = oneClass
语句时,要先调用父类的赋值函数完成父类成分的赋值,然后才是继承类。 编译器发现父类的赋值函数是private的,那么就会编译失败。
但是如果自己写目标类的拷贝赋值函数,就可以规避编译的问题,如果用户自己写了明确的拷贝构造函数,那么编译器只会调用用户定义的拷贝构造函数,并且不会递归调用父类的拷贝构造函数。注意这里和构造函数不一样,对于构造函数,递归调用父类的构造函数是在任何时候都进行的,就是在你明确定义了自己的构造函数的情况下,编译器也会自动递归调用父类的构造函数。
Class Room
{ public: int size;
private: Room& operator=(const Room& rhs)
{
if(this ==&rhs)
return (*this);
size = rhs.size;
return (*this);
}
};
class ClassRoom : public Room
{
public:
int studentCnt;
ClassRoom& operator=(const ClassRoom& rhs)
{
this->size = rhs.size;
this->studentCnt = rhs.studentCnt;
return (*this);
}
int main()
{
ClassRoom oneClass;
oneClass.studentCnt = 10;
oneClass.size = 100;
ClassRoom anotherClass;
anotherClass = oneClass;
}
网友评论