1. 构造函数和析构函数的执行顺序
结论:有继承关系时,类的构造函数的调用顺序:先父类,后子类;对于每个类,都是先初始化成员变量,再执行构造函数。析构函数的调用顺序,先子类,后父类;对于每个类,先执行该类的析构函数,再执行成员的析构函数。类中的成员按照声明的先后顺序执行构造函数,析构时顺序相反。
形象化的理解就是拿盖房子类比:先盖基础,再盖上层; 拆的时候先拆上层,后拆下面的地基。盖每层时,先将内部和四周的墙壁造好,再去封顶,拆时顺序相反。
例子代码:
#include <iostream>
class A0{
public:
A0(){
std::cout << "A0\n";
}
~A0(){
std::cout << "Delete A0\n";
}
};
class A1{
public:
A1(){
std::cout << "A1\n";
}
~A1(){
std::cout << "Delete A1\n";
}
};
class B{
public:
B(){
std::cout << "B\n";
}
~B(){
std::cout << "Delete B\n";
}
private:
A0 a;
};
class B1:public B{
public:
B1(){
std::cout << "B1\n";
}
~B1(){
std::cout << "Delete B1\n";
}
private:
A1 a1;
A0 a0;
};
int main()
{
B1 *b = new B1();
delete b;
}
A0
B
A1
A0
B1
Delete B1
Delete A0
Delete A1
Delete B
Delete A0
2. 有无virtual 关键字对执行函数的影响
结论:当使用派生类的指针访问父类的对象的函数的时候,如果该函数使用了virtual关键字修饰,那么执行的就是派生类的函数,否则执行的是基类的函数。
原因:使用virtual关键字修饰,执行的是派生类的函数很好理解,因为是到虚表里面去找该函数,虚表里面同名的函数只可能有一个;没有使用virtual关键字修饰的情况执行的是基类的函数,表示c++在找一个函数的时候是根据参数的类型来找的,这个是在编译时就确定的。
例子代码:
#include<iostream>
using namespace std;
class Base {
public:
void f1(void){ cout << "Base::f1\n";}
virtual void f2(void){ cout << "Base::f2\n";}
};
class Derived : public Base{
public:
void f1(void){ cout << "Derived::f1\n";}
virtual void f2(void){ cout << "Derived::f2\n";}
};
int main()
{
Derived *pd = new Derived();
Base *pb = pd;
pb->f1();
pb->f2();
pd->f1();
pd->f2();
return 0;
}
执行结果:
Base::f1
Derived::f2
Derived::f1
Derived::f2
3. 默认的构造函数,拷贝构造函数和赋值函数
3.1 c++默认提供的三个函数是public和inline修饰的;
3.2 当类中具有引用成员或者是const成员时,必须自己提供构造函数和赋值函数,否则会导致编译错误;但是默认提供的拷贝构造函数能够正确的运行;
3.3 存在继承关系时,派生类的构造函数和析构函数会默认调用基类的构造和析构函数,因此若基类的构造或者析构函数是private成员,则派生类无法使用;
3.4 当作为基类的类是用于多态性时,需要将析构函数定位virtual,这样可以保证delete执行的是正确的析构函数;另一条准则是,只要类中有一个virtual函数,其析构函数都要设为virtual;当定义析构函数是纯虚函数时,可以给出析构函数的定义,否则派生类无法使用。
3.5 不要在构造函数和析构函数中执行任何virtual函数,因为此时类的结构不完整,在基类中使用的virtual函数一定是基类的函数,而不可能是派生类的。
3.6 赋值函数的声明形式如下:
MyClass& operator=(const MyClass& rhs)
其他有=号的操作符都要返回一个reference to *this;
存在继承关系时的形式,一定要显示执行父类的赋值操作:
DerivedClass& DerivedClass::operator=(const DerivedClass& rhs)
{
BaseClass::operator=(rhs);
....;
}
3.7 拷贝构造函数的声明形式如下:
MyClass(const MyClass& rhs);
存在继承关系时的定义:
DerivedClass::DerivedClass(const DerivedClass& rhs):
BaseClass(rhs)
{
....;//可使用rhs的私有成员,貌似所有函数都可以使用
}
现有MyClass a;
拷贝构造函数在两种情况下会执行:
MyClass b(a);
MyClass b = a;
需要注意的是下面这种用法,从形式上看应该是执行一次构造函数和一次赋值函数,其实只是执行了一遍拷贝构造函数,提高了效率。
4. 指针资源管理
对系统的资源进行管理需要保证使用完后,交还给系统使用,例如存储器资源。对资源进行管理的第一条准则就是“以对象管理资源”,因为类的对象不存在时会自动执行析构函数,帮助我们释放资源,防止用户的疏忽导致的资源泄露。
对指针来说:使用std::shared_ptr这个模板类来帮助管理指针,使得该指针可以在多处使用,当各处都不使用时,自动销毁该指针。
例如,Mesh类是很多Entity对象共享的资源:
typedef std;:shared_ptr<Mesh> MeshPtr;
Entity类本来的构造函数是: Entity(Mesh *), 且析构函数中还要对Mesh进行析构,而Mesh又是多个Entity共享的,所以不能擅自delete,除非使用Primer中介绍的引用计数法或者伙伴类法,但是简单的实现又没有多线程安全性,所以比较难以实现。
现在使用智能指针后,变为Entity(MeshPtr), 其实MeshPtr就是Mesh*的一个伙伴类,但是处理了很多我们看不到的细节,而且具有多线程可冲入性,因此非常适合这种情况。
注意点:使用new分配内存的时候,new和delete对于数组资源的情况,一定要配对。例如str::string *pString = new std::string[100]; 那么销毁就应该使用delete []pString。
这个和malloc与free的情况不同。malloc的内存,前面一定有大小信息。但是new一个对象 的时候,申请的内存前面没有大小信息;new一个对象的数组,会有大小信息。c++为了兼容c导致了new的两种语义,所以必须要通过语义上的形式来帮助编译器进行区分。
同样,对系统关键性的其他资源如文件描述符,互斥锁,图形界面中的字型和画刷,数据库连接以及网络socket等,都要对其进行一定的封装后再使用来保证这些资源使用过后能够归还给操作系统。
5. 实现:条款26至条款31
5.1 尽量延迟变量定义的时间,一则可以减少使用赋值操作,直接使用拷贝构造函数进行构造或者使用参数进行构造; 二则可以减少后面代码出现异常对变量的析构造。
在一个循环体中要不停的使用一个变量,有两种方式:一,每次循环使用一个构造函数和析构函数; 二,每次只使用赋值函数。这个就要比较两者的代价,在我看来使用后者较为合适,因为内存的申请和释放非常消耗资源。
5.2 尽量少做转型动作
c++中兼容c中的强制类型转换;同时加入了自己的四种转换:
const_cast: 用于去除变量的常量性质,这是唯一具有此能力的一个类型转换,在c中无法实现;
dynamic_cast: 用于在继承体系中的“安全向下转型”,这个在单继承中体现的不明显,在多继承中使用。用于将pointer-to-base转换为pointer-to-derived,在单继承中两个指针值相互转换没有偏移,但是在多继承中就不能简单的强制转换了,必须通过这种方式才能实现。同时这个操作也是四种转型操作中代价最为昂贵的操作;
reinterpret_cast: 用于非常规的类型转换,称为“低级转换”,可以在没有联系的类型之间进行转换。使用较少。
static_cast: 就是c中使用的强制类型转换,在c++中主要用于非多态类型的转换,在“向上转型”中是安全的,即pointer-to-derived变为pointer-to-base是安全的;但是“向下转型”由于没有动态类型检查,则不安全。
c++中的转型操作并不是简单的告诉编译器,对象的类型发生变化; 转型操作在编译后会生成可执行代码的,例如从int转化为float,特别使用pointer-to-base转换为pointer-to-derived,会有多次比较的。所以,尽量减少使用转型,取而带之,应该使用虚函数来实现一些继承关系中都有的函数,这样就可以直接通过基类的指针或引用来调用派生类的函数。
#include <iostream>
using namespace std;
class Base1{
public:
int base1;
virtual void f(){}
};
class Base2{
public:
int base2;
virtual void f(){}
};
class Derived:public Base1, public Base2{
public:
int deride;
};
int main()
{
Base2 *pBase2 = new Derived();
Derived *pDerived1 = static_cast<Derived*>(pBase2);
Derived *pDerived2 = dynamic_cast<Derived*>(pBase2);
Base1 *pBase1_1 = static_cast<Base1*>(pDerived2);
Base1 *pBase1_2 = dynamic_cast<Base1*>(pDerived2);
cout << "Base2:" << pBase2 << endl;
cout << "Derived(static_cast): " << pDerived1 << endl;
cout << "Derived(dynamic_cast): " << pDerived2 << endl;
cout << "Base1_1(static_cast):" << pBase1_1 <<endl;
cout << "Base1_2(dynamic_cast):" << pBase1_2 << endl;
return 0;
}
Base2:0x9ddc010
Derived(static_cast): 0x9ddc008
Derived(dynamic_cast): 0x9ddc008
Base1_1(static_cast):0x9ddc008
Base1_2(dynamic_cast):0x9ddc008