一. 编译器何时为类生成合适的特殊默认函数
当声明如下一个空类时:
class CA {};
一般认为C++编译会在背后默默帮你生成5个函数:默认构造函数,拷贝构造函数,析构函数,赋值运算符重载函数,取地址运算符重载函数,结果类被扩展为如下形式:
class CA()
{
public:
CA() {...}
CA(const CA& other) {...}
~CA() {...}
CA& operator=(const CA& other) {...}
CA* operator&() {...}
}
但实际情况并非如此,编译器只有认为需要的时候才生成相应函数,这体现了C++的效率至上理念,对于如上空类根本就不需要生成任何函数,因为这些默认函数没有任何有意义的事可做,当你在类里显式声明了任何类型的构造函数时(包括拷贝构造函数),编译器便不会为你生成默认构造函数,同样其他四个函数当在类里有显式声明时,编译器都不会产生默认的,这里要特别强调一下operator&()函数,可能很多人不知道它的存在,如果你不去显式申明,确实没有存在的必要,就是获取对象的地址,但你在类里显式定义后,也许会改变它的默认意义,可能会造成使用者的困惑, 例如:
class CA { CA* operator&() {...}};
CA a;
CA* p = &a; //这里&a等价与a.operator&()调用,其意义完全取决于设计者的实现,而未必就是取对象a的地址。
那么除去operator&(),在没有显式申明其他函数的前提下,编译器何时会为你生成这些特殊函数呢,以下列举几种常见情况(更多信息可参考《深度探索C++对象模型》):
1. 类里声明了虚函数
2. 基类体系某一个里申明了虚函数
3. 基类体系里某一个里显式定义了相应函数
4. 含有类成员,类成员出现了以上情况之一
二. 拷贝构造/赋值操作注意事项
当一个类里申明了一个引用成员或const成员,必须显式定义operator=(...)才能处理对象赋值问题,否则编译无法通过,如下所示:
class CB
{
public:
CB() : a(1) , b(a) {}
int a;
int& b;
};
CB b , a;
a = b; //无法通过编译
当一个派生类显式定义了拷贝构造函数或operator=(...),他们并不会默认的去调基类的相应函数,这可能与你的预期不一致,导致对象拷贝赋值的不完全,如下所示:
class Base
{
public:
Base() {...}
Base(const Base& other) : a(other.a) {}
Base& operator=(const Base& other) { a = other.a; }
int a;
};
class Derive : public Base
{
public:
Derive() {...}
Derive(const Derive& other) : b(other.b) {...} //不是调用Base的拷贝构造函数,而是调用无参构造函数Base(),导致Base部分没有拷贝
Derive& operator=(const Derive& other) { b = other.b; } //没有调用基类operator=(...)函数
int b;
}
要想实现完全拷贝,需要自己显式申明:
class Derive : public Base
{
public:
Derive() {...}
Derive(const Derive& other) : Base(other), b(other.b) {...}
Derive& operator=((const Derive& other) { Base::operator=(other); b = other.b; }
int b;
}
三. 虚拟析构函数
在C++里,构造函数是没办法被显式调用的,只能由编译器调用,但析构函数是可以被显式调用的。一个类是否都应该有一个显式的虚拟析构函数呢?不一定,只有当该类需要当作基类,而且需要delete一个该基类指针,而该基类指针实际指向其派生类时,才需要,目的是防止内存泄漏,当不存在如此情况时,若不需要在析构函数里做什么工作,可完全不声明,由编译器去决定是否需要生成一个默认的析构函数。析构函数有时会声明为纯虚的,这一般出现在定义一个不能实例化的类,但该类除了析构函数没有其他的虚函数,此时可将析构函数声明为纯虚的。
四. 构造/析构函数里原则上不应该有的操作
在构造/析构函数里一般不要调用虚函数,其行为一般非你所预期,这依赖于实现,目前的实现来讲,不会发生多态行为,而是像调用一个普通成员函数一样直接产生编译期绑定,如下例:
class Base
{
public:
Base() { test(); }
virtual void test() {cout << "Base::test" << endl; }
};
class Derive : public Base
{
public:
Derive() { test(); }
virtual void test() { cout << "Derive::test" << endl; }
};
Derive d;
输出:Base::test
Derive::test
另不要在构造/析构完成过复杂容易引起异常的操作。
五. 限制对象创建与复制
当想让一个对象只能在栈上分配,而禁止在堆上分配时,可以通过将opertor new(size_t size)申明为类的私有函数方式实现,反过来可通过申明类的构造函数为私有的,同时定义一个静态工厂函数,实现里通过new的方式返回对象指针即可,这种方式也能控制对象创建的数量,当然还有其他的方法,对对象创建的控制无论是创建方式还是数量都基本可通过显式定义operator new(...)/operator delete(...), 控制构造/析构函数的访问权限,另外可能辅助一些静态工厂函数来实现。当想禁止一个对象的复制时,可通过将拷贝构造函数及operator=(...)显式申明为私有的。
六. 成员初始化列表机制
一个类里成员的初始化有两种方式,常见的是在构造函数里初始化,但这并非真正的初始化,因为成员在进入构造函数体之前已完成了默认的初始化工作,在构造函数内都只能算赋值动作,如果想真正显式执行特定的类成员初始化动作,可采用第二种初始化列表机制,如下类:
class CA
{
public:
CA() {...}
CA(int a) {...}
};
class CB
{
public:
CB(int a) : ca(a) {...} //通过初始化列表机制显式指定相应ca的构造方法
CA ca;
}
需要注意的是初始化是按照成员在类里申明顺序初始化的,这有时可能会导致一些隐晦的错误,例如如下定义:
class CA
{
public:
CA(): j(1),i(j) {}
private:
int i;
int j;
};
这里虽然j的初始化放在i之前,但由于声明时i在j之前,所以i会先初始化,而此时j处于不确定状态。
另外需要注意的是有些类成员只能通过成员初始化列表机制初始化,有如下几种常见情况:
1. 类成员没有默认构造函数或可不带参数调用的构造函数(注:这里没有说无参构造函数是考虑到默认参数的存在);
2. 类成员为引用类型;
3. 类成员为const修饰型;
七. 类对象的构造过程
为了下面叙述方便,先提出一个扩展构造函数的概念,由编译器在你编写的实际构造函数前插入必要代码构成,当你定义一个类对象时,实际上初始化是通过调用扩展构造函数完成,在进入你所编写的构造函数之前,这段必要代码会完成基类扩展构造函数,类类型成员扩展构造函数调用,虚表指针初始化等关键工作,那么一个最具普遍意义的类对象的构造过程如下:
1. 调用类的扩展构造函数,当有继承存在时进行步骤2,否则到步骤3;
2. 当不存在虚拟继承时,调用基类的扩展构造函数,当存在多个基类时,按照申明顺序依次调用,基类的构造行为同样,这样就保证了按照继承树从跟往下构造的顺序;
存在虚拟继承的情况比较复杂一点,平时很少用到,这里不细说了,具体细节可参看《深度探索C++对象模型》,这里指出一点技巧,可以利用虚拟继承的相关特性设计一个不能被继承使用的类。
3. 当存在自定义类型成员时,按照其在类里的申明顺序依次调用相应的扩展构造函数;
4. 进入你编写的实际构造函数完成整个构造过程;
下面举例说明:
class Base1
{
public:
Base1() { cout << "Base1 Constructor..." << endl; }
};
class Base2
{
public:
Base2() { cout << "Base2 Constructor..." << endl; }
};
class Member
{
public:
Member() { cout << "Member Constructor..." << endl; }
};
class Derive : public Base1 , pubic Base2
{
public:
Derive() { cout << "Derive Construcotor..." << endl; }
Member member;
};
Derive d;
输出:
Base1 Constructor...
Base2 Constructor...
Member Constructor...
Derive Constructor...