编译器可暗自为类创建default构造函数、copy构造函数、copy 赋值操作符和析构函数
当你写一个空类
class Empty{};
如果你自己没有声明,则编译器会声明一个copy构造函数、一个copy 赋值操作符和一个析构函数。此外,如果你没有声明任何构造函数,编译器也会为你声明一个default构造函数。所有这些函数都是public且inline
class Empty{
public:
Empty(){...} //default构造函数
Empty(const Empty& rhs){...} //copy构造函数
~Empty(){} //析构函数
Empty& operator=(const Empty& rhs){...} //copy赋值操作符
};
这些函数被调用
Empty e1; //default构造函数+析构函数
Empty e2(e1); //copy构造函数
e2 = e1; //copy赋值操作符
编译器提供的:
- default构造函数和析构函数像是调用基类和non-static成员变量的构造函数和析构函数。编译器产生的析构函数是一个non-virtual,除非这个类的基类自身声明有virtual析构函数
- copy构造函数和copy赋值操作符,编译器创建的版本只是单纯地将来源对象的每一个non-static成员拷贝到目标对象。
- 当类中包含reference和const成员时,编译器会拒绝生成copy赋值操作符,必须自己定义copy赋值操作符,因为C++不允许让reference改指向不同对象,更改const成员也是不合法的。
- 如果某个基类将copy赋值操作符声明为private,编译器将拒绝为其子类生成一个copy赋值操作符。
为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现
- “将成员函数声明为private且故意不实现它们”这一伎俩是如此被大家接受,因而被用在C++ iostream程序库阻止copy赋值操作符和copy构造函数行为。例如ios_base,basic_ios和sentry。
- 一般而言这个做法并不绝对安全,因为成员函数和友元函数还是可以调用的private函数,如果调用了会获得连接器的错误报告。
- 要拒绝编译器对A类的copy赋值操作符和copy构造函数行为,则需要将A类的基类的copy赋值操作符和copy构造函数行为声明为private。
关于虚函数
1、为多态基类声明virtual析构函数
C++中,当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,实际执行时通常发生的是对象的derived成分没被销毁,然而其base class成分被销毁,derived class的析构函数也未能执行起来。造成“局部销毁”对象。解决方法就是给base class一个virtual析构函数。
如果一个类不企图被当做base class,令其析构函数为virtual是一个馊主意。
polymorphic(带多态性质的)base class应该声明一个virtual析构函数。
只有当class内含至少一个virtual函数,才为其声明virtual析构函数。任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。
2、虚函数之静态绑定&动态绑定
针对对象指针的情况,对于引用(reference)的情况同样适用
相关概念:
- 对象的静态类型:对象在声明时采用的类型。是在编译期确定的。
- 对象的动态类型:目前所指对象的类型。是在运行期决定的。
- 静态绑定:在编译时,根据变量的静态类型来决定调用哪个函数。
- 动态绑定:在运行时,根据变量实际指向的对象类型来决定调用哪个函数。
静态类型&动态类型
对象的动态类型可以更改,但是静态类型无法更改。
class Base{}; class deri_1 : public Base{}; class deri_2 : public Base{}; deri_2* pderi_2 = new deri_2(); //pderi_2的静态类型是它声明的类型deri_2*,动态类型也是deri_2* Base* pBase = pderi_2; //pBase的静态类型是它声明的类型pBase*,动态类型是pBase所指向的对象pderi_2的类型pderi_2*; deri_1* pderi_1 = new deri_1(); pBase = pderi_1; //pBase的动态类型是可以更改的,现在它的动态类型是pderi_1*
关于静态绑定&动态绑定:
class B{ void DoSomething(); virtual void vfun(); }; class D : public B { void DoSomething(); virtual void vfun(); }; D* pD = new D();//pD的静态类型是D,动态类型是D B* pB = pD;//pB的静态类型是B,动态类型是D
此时,若调用DoSomething():
DoSomething()是一个no-virtual函数,它是静态绑定的,编译器会在编译期根据对象的静态类型来选择函数。
- pD->DoSomething()://调用的是D::DoSomething();
- pB->DoSomething()://调用的是B::DoSomething();
此时,若调用vfun():
vfun是一个虚函数,它动态绑定的,也就是说它绑定的是对象的动态类型。
pD->vfun()://调用D::vfun()
pB->vfun()://调用D::vfun()
缺省参数和虚函数
虚函数是动态绑定的,但是为了执行效率,缺省参数是静态绑定的。
class B{ virtual void vfun(int i = 10); }; class D : public B { virtual void vfun(int i = 20); }; D* pD = new D(); B* pB = pD;
pD->vfun(); //i=20; pB->vfun(); //i=10;
3、虚函数之虚函数表
欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。这份信息通常是有一个vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table)。
说明:
- 每一个带有virtual函数的class都有一个相应的vtbl。
- 虚函数按照其声明顺序放在 VTable 中
- 基类的虚函数在派生类虚函数前面
- 编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置
举例:
派生类没有覆盖任何基类函数
函数 f() 在派生类中重写
派生类中重写的函数f() 覆盖了基类的函数 f() ,并放在虚函数表中原来基类中 f() 的位置;没有覆盖的函数位置不变。多重继承(无虚函数覆盖)
多重继承(虚函数覆盖)
4、谨慎对待虚函数
如果class内含virtual函数,其对象的体积会增加。
例如:
class Point{ public: Point(int xcoord,int ycoord); ~Point(); private: int x,y; };//Point类是一个2D空间点
如果Point内含virtual函数,在32位计算机体系结构中将占用64bits(两个int成员变量)至96bits(两个int加一个虚函数表的指针,32位系统中的指针占4个字节(32比特));在64位计算机体系结构中将占用64bits至128bits(32位系统中的指针占8个字节(64比特))。
因为其他语言中不一定存在虚函数表或者虚函数表指针,因此虚函数的存在也会影响移植性。
构造函数不能为虚函数
4、纯虚函数(抽象类)
- 虚函数和纯虚函数的比较
虚函数 | 纯虚函数 |
---|---|
声明 | virtual ReturnType FunctionName(Parameter); |
说明 | 在基类中是有定义的,即便定义是空,所以子类中可以重写也可以不写基类中的虚函数! |
定义一个函数为纯虚函数,一般表示该函数没有被实现。但是,这不代表纯虚函数不能被实现。纯虚函数也是可以定义的。
通常的纯虚函数不需要定义,是因为我们一般不会调用抽象类的这个函数,只会调用派生类的对应函数。但析构函数、构造函数和其他内部函数不一样,在调用时,编译器需要产生一个调用链。因此,纯虚析构函数必须要有定义。
例:如果我们想拥有一个抽象类(有个纯虚函数)作为基类(有个virtual函数),则可以定义纯虚析构函数,
class Base { public: Base(){} virtual ~Base()= 0; //纯虚函数的声明 };
此时注意:必须为纯虚析构函数构建定义:
Base::~Base(){ } //纯虚析构函数的定义
析构函数绝不要吐出异常
如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数吐出异常就是危险,总会带来“过早结束程序”或“发生不明确行为”的风险。
如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。传播异常就是允许它离开这个析构函数。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
不在析构和构造过程中调用virtual函数
举例(以构造过程为例,也适用于析构过程):
class Transaction{
public:
Transaction();
virtual void logTransaction() const = 0 ;//做日志记录,纯虚函数
...
};
Transaction::Transaction(){
...
logTransaction();
}
class BuyTransaction:public Transaction{
public:
virtual void logTransaction() const;
...
};
class SellTransaction:public Transaction{
public:
virtual void logTransaction() const;
...
};
执行:
BuyTransaction b;
- BuyTransaction构造函数被调用,但首先Transaction构造函数一定会更早地被调用。
derived class对象内的base class成分会在derived class自身成分被构造之前先构造妥当。也就是,当base class构造函数执行时derived class的成员变量尚未初始化。正是因为这些成员变量处于未定义状态,所以base class构造期间virtual函数绝不会下降到derived classes阶层。
在derived class对象的base class构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析至base class,若使用运行期类型信息(例如,dynamic_cast和typeid)也会把对象视为base class类型。Transaction构造函数调用的logTransaction是Transaction内的版本,不是BuyTransaction的版本。 - 在derived class对象的base class构造期间:
- 如果调用了纯虚函数,大多执行系统会中止程序;
- 如果调用了非纯虚函数,即在base class有该函数的实现代码,则实现base class中该函数的代码,由此导致在建立derived class对象时调用错误版本的函数。
解决方法:将调用的函数设置为非虚函数,在构造期间,令derived classes将必要的构造信息向上传递至base class构造函数。
class Transaction{
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const;//非纯虚函数
...
};
Transaction::Transaction(const std::string& logInfo){
...
logTransaction(logInfo);
}
class BuyTransaction:public Transaction{
public:
BuyTransaction(parameters):Transaction(createlogstring(parameters)){...}
...
private:
static std::string createlogstring(parameters);
//令该函数为static,使得不可能意外指向“初期未成熟之BuyTransaction对象内尚未初始化的成员变量”
};
不小心在构造函数或析构函数中间接调用虚函数是C++中常见的错误,所以最好在编码规范中严格限制构造函数和析构函数可以做的事情。把复杂的对象初始化过程搬到一个独立的初始化函数当中,并由使用者显式调用。在构造函数和析构函数中只做简单而意义明确的对资源引用和解引用的工作即可。
如果class有多个构造函数,每个都需要执行某些相同工作,那么避免代码重复的一个优秀做法是把共同的初始化代码放进一个初始化函数如init内。