一、首先我们先来学习下:《Effect C++》学习------条款05:了解C++默默编写并调用哪些函数。
如果你在类声明时没有声明拷贝构造函数、拷贝类型操作符、析构函数、构造函数。编译器会自动为你提供一份声明,惟有这些函数在被需要(调用时),他们才会被编译器创造出来。
默认构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用基类和非静态成员变量的构造函数和析构函数;至于拷贝构造函数和拷贝赋值操作符,编译器创建的版本只是单纯地将来源对象的每一个非静态成员变量拷贝到目标对象。
- 空类,声明时编译器不会生成任何成员函数: 对于空类,编译器不会生成任何的成员函数,只会生成1个字节的占位符。
有时可能会以为编译器会为空类生成默认构造函数等,事实上是不会的,编译器只会在需要的时候生成6个成员函数:一个缺省的构造函数、一个拷贝构造函数、一个析构函数、一个赋值运算符、一对取址运算符和一个this指针。
class A
{
};
class B
{
/// 声明一个虚函数
virtual bool compare(int a, int b) = 0;
};
class C :public A, public B
{
};
class D :public A, public B
{
/// 声明一个虚函数
virtual bool compare(int a, int b) = 0;
};
class E :virtual A, virtual B
{
};
class F :virtual A, virtual B
{
virtual bool compare(int a, int b) = 0;
};
int main()
{
cout << "A zize:" << sizeof(A) << endl;//1
cout << "B zize:" << sizeof(B) << endl;//4
cout << "C zize:" << sizeof(C) << endl;//4
cout << "D zize:" << sizeof(D) << endl;//4
cout << "E zize:" << sizeof(E) << endl;//8
cout << "F zize:" << sizeof(F) << endl;//8
system("pause");
return 0;
}
分析:
类A是空类,但空类同样可以被实例化,而每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址,所以sizeof(A)的大小为1。
类B里面因有一个纯虚函数,故有一个指向虚函数的指针(vptr),32位系统分配给指针的大小为4个字节,所以sizeof(B)的大小为4。类C继承于A和B,编译器取消A的占位符,保留一虚函数表,故大小为4。类D继承于A和B,派生类基类共享一个虚表,故大小为4。类E虚继承A和B,含有一个指向基类的指针(vftr)和一个指向虚函数的指针。类F虚继承A和B,含有一个指向基类的指针(vftr)和一个指向虚函数的指针。
2、空类,定义时会生成6个成员函数
class Empty
{
};
等价于
class Empty
{
public:
Empty(); // 缺省构造函数
Empty(const Empty &rhs); // 拷贝构造函数
~Empty(); // 析构函数
Empty& operator=(const Empty &rhs); //赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&()const; //取址运算符(const版本)
};
使用时的情况
Empty *e = new Empty();// 缺省构造函数;
delete e; // 析构函数
Empty e1; // 缺省构造函数;
Empty e2(e1); // 拷贝构造函数
Empty e3 = e2; // 赋值运算符
Empty *pe1 = &e1; // 取址运算符
const Empty *pe2 = &e2; //取址运算符(const)
编译器对这些函数的实现如下:
inline Empty::Empty() //缺省构造函数
{
}
inline Empty::~Empty() //析构函数
{
}
inline Empty *Empty::operator&() //取址运算符(非const)
{
return this;
}
inline const Empty *Empty::operator&() const //取址运算符(const)
{
return this;
}
inline Empty::Empty(const Empty &rhs) //拷贝构造函数
{
//对类的非静态数据成员进行以"成员为单位"逐一拷贝构造
//固定类型的对象拷贝构造是从源对象到目标对象的"逐位"拷贝
}
inline Empty& Empty::operator=(const Empty &rhs) //赋值运算符
{
//对类的非静态数据成员进行以"成员为单位"逐一赋值 //固定类型的对象赋值是从源对象到目标对象的"逐位"赋值。
}
二、类大小计算
1、空类大小:我们都知道空类大小为1,为什么呢?C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址。这是由于:new需要分配不同的内存地址,不能分配内存大小为0的空间避免除以 sizeof(T)时得到除以0错误,故使用一个字节来区分空类。
有两种情况需要我们注意下:
A、第一种情况,涉及到空类的继承。 当派生类继承空类后,派生类如果有自己的数据成员,而空基类的一个字节并不会加到派生类中去。如下:sizeof(DerEmpty)为4。
class Empty{};
class DerEmpty:public Empty
{
public:
int a;
}
B、第二中情况,一个类包含一个空类对象数据成员。
class Empty {};
class HoldsAnInt {
int x;
Empty e;
};
sizeof(HoldsAnInt)为8。 因为在这种情况下,空类的1字节是会被计算进去的。而又由于字节对齐的原则,所以结果为4+4=8。继承空类的派生类,如果派生类也为空类,大小也都为1。
2、含有虚函数成员:
在上文中,我们也简要分析了含有虚函数时类的大小,这里举例子详细说明。
首先,要介绍一下虚函数的工作原理:虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。编译器必须要保证虚函数表的指针存放于对象实例中最前面的位置(这是为了确保正确取到虚函数的偏移量)。
每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个VTABLE的作用就是保存自己类中所有虚函数的地址,可以把VTABLE形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。在每个带有虚函数的类 中,编译器秘密地置入一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的V TA B L E。 当构造该派生类对象时,其成员VPTR被初始化指向该派生类的VTABLE。所以可以认为VTABLE是该类的所有对象共有的,在定义该类时被初始化;而VPTR则是每个类对象都有独立一份的,且在该类对象被构造时被初始化。
假设我们有这样的一个类:
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
当我们定义一个这个类的实例,Base b时,其b中成员的存放如下:
指向虚函数表的指针在对象b的最前面。虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符”\0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在vs下,这个值是NULL。而在linux下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。 因为对象b中多了一个指向虚函数表的指针,而指针的sizeof是4,因此含有虚函数的类或实例最后的sizeof是实际的数据成员的sizeof加4。sizeof(Base)为4。
下面将讨论针对基类含有虚函数的继承讨论:
(1)在派生类中不对基类的虚函数进行覆盖,同时派生类中还拥有自己的虚函数,比如有如下的派生类:
class Derived: public Base
{
public:
virtual void f1() { cout << "Derived::f1" << endl; }
virtual void g1() { cout << "Derived::g1" << endl; }
virtual void h1() { cout << "Derived::h1" << endl; }
};
基类和派生类的关系如下:
当定义一个Derived的对象d后,其成员的存放如下:
可以发现:
1)虚函数按照其声明顺序放于表中。
2)基类的虚函数在派生类的虚函数前面。
此时基类和派生类的sizeof都是数据成员的大小+指针的大小4。
(2)在派生类中对基类的虚函数进行覆盖,假设有如下的派生类:
class Derived: public Base
{
public:
virtual void f() { cout << "Derived::f" << endl; }
virtual void g1() { cout << "Derived::g1" << endl; }
virtual void h1() { cout << "Derived::h1" << endl; }
};
基类和派生类之间的关系:其中基类的虚函数f在派生类中被覆盖了
当我们定义一个派生类对象d后,其d的成员存放为:
可以发现:
1)覆盖的f()函数被放到了虚表中原来基类虚函数的位置。
2)没有被覆盖的函数依旧。_
派生类的大小仍是基类和派生类的非静态数据成员的大小+一个vptr指针的大小,这样,我们就可以看到对于下面这样的程序:
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
(3)多继承:无虚函数覆盖
假设基类和派生类之间有如下关系:
对于派生类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个基类都有自己的虚表。
2) 派生类的成员函数被放到了第一个基类的表中。(所谓的第一个基类是按照声明顺序来判断的)
由于每个基类都需要一个指针来指向其虚函数表,因此d的sizeof等于d的数据成员加上三个指针的大小。
(4)多重继承,含虚函数覆盖
假设,基类和派生类又如下关系:派生类中覆盖了基类的虚函数f
下面是对于派生类实例中的虚函数表的图:
我们可以看见,三个基类虚函数表中的f()的位置被替换成了派生类的函数指针。这样,我们就可以任一静态类型的基类类来指向派生类,并调用派生类的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
此情况派生类的大小也是类的所有非静态数据成员的大小+三个指针的大小。
class A
{
};
class B
{
char ch;
virtual void func0() { }
};
class C
{
char ch1;
char ch2;
virtual void func() { }
virtual void func1() { }
};
class D: public A, public C
{
int d;
virtual void func() { }
virtual void func1() { }
};
class E: public B, public C
{
int e;
virtual void func0() { }
virtual void func1() { }
};
int main(void)
{
cout << "A=" << sizeof(A) << endl; //result=1
cout << "B=" << sizeof(B) << endl; //result=8
cout << "C=" << sizeof(C) << endl; //result=8
cout << "D=" << sizeof(D) << endl; //result=12
cout << "E=" << sizeof(E) << endl; //result=20
return 0;
}
结果分析:
1.A为空类,所以大小为1
2.B的大小为char数据成员大小+vptr指针大小。由于字节对齐,大小为4+4=8
三、虚函数表的打印
class A1
{
public:
A1(int _a1 = 1) : a1(_a1) { }
virtual void f() { cout << "A1::f" << endl; }
virtual void g() { cout << "A1::g" << endl; }
virtual void h() { cout << "A1::h" << endl; }
~A1() {}
private:
int a1;
};
class C : public A1
{
public:
C(int _a1 = 1, int _c = 4) :A1(_a1), c(_c) { }
virtual void f() { cout << "C::f" << endl; }
virtual void g() { cout << "C::g" << endl; }
virtual void h() { cout << "C::h" << endl; }
private:
int c;
};
如果类C中重写了A类中的函数,那么就会覆盖A类的虚函数,重写一部分就会覆盖一部分,重写全部就会覆盖全部。如果C中重新写了一些别的虚函数,那么这些虚函数将排在父类的后面,这里编译器无法显示,可以通过打印虚表来进行。打印的过程比较简单,通过访问类C的前8字节(64位编译器)找到虚函数表,再一次遍历虚函数表即可。虚函数表最后一项用的是0,代表虚函数表结束。
class Base
{
public:
int base_data;
Base() { base_data = 1; }
virtual void func1() { cout << "base_func1" << endl; }
virtual void func2() { cout << "base_func2" << endl; }
virtual void func3() { cout << "base_func3" << endl; }
};
class Derive : public Base
{
public:
int derive_data;
Derive() { derive_data = 2; }
virtual void func1() { cout << "derive_func1" << endl; }
virtual void func2() { cout << "derive_func2" << endl; }
};
typedef void(*func)();
int test()
{
Base base;
cout << "&base: " << &base << endl;
cout << "&base.base_data: " << &base.base_data << endl;
cout << "----------------------------------------" << endl;
Derive derive;
cout << "&derive: " << &derive << endl;
cout << "&derive.base_data: " << &derive.base_data << endl;
cout << "&derive.derive_data: " << &derive.derive_data << endl;
cout << "----------------------------------------" << endl;
for (int i = 0; i<3; i++)
{
// &base : base首地址
// (unsigned long*)&base : base的首地址,vptr的地址
// (*(unsigned long*)&base) : vptr的内容,即vtable的地址,指向第一个虚函数的slot的地址
// (unsigned long*)(*(unsigned long*)&base) : vtable的地址,指向第一个虚函数的slot的地址
// vtbl : 指向虚函数slot的地址
// *vtbl : 虚函数的地址
unsigned long* vtbl = (unsigned long*)(*(unsigned long*)&base) + i;
cout << "slot address: " << vtbl << endl;
cout << "func address: " << *vtbl << endl;
func pfunc = (func)*(vtbl);
pfunc();
}
cout << "----------------------------------------" << endl;
for(int i=0; i<3; i++)
{
unsigned long* vtbl = (unsigned long*)(*(unsigned long*)&derive) + i;
cout << "slot address: " << vtbl << endl;
cout << "func address: " << *vtbl << endl;
func pfunc = (func)*(vtbl); pfunc();
}
cout << "----------------------------------------" << endl;
return 1;
}
四、类继承与C++多态
1、类继承:C++是一种面向对象的语言,最重要的一个目的就是——提供可重用的代码,而类继承就是C++提供来扩展和修改类的方法。类继承就是从已有的类中派生出新的类,派生类继承了基类的特性,同时可以添加自己的特性。实际上,类与类之间的关系分为三种:代理、组合和继承。
以下是三种关系的图解:(为了更好的理解)
基类可以派生出派生类,基类也叫做“父类”,派生类也称为“子类”。 那么,派生类从基类中继承了哪些东西呢?分为两个方面:
1). 变量——派生类继承了基类中所有的成员变量,并从基类中继承了基类作用域,即使子类中的变量和父类中的同名,有了作用域,两者也不冲突。
2).方法——派生类继承了基类中除去构造函数、析构函数以外的所有方法。
2、继承方式和访问限定符
继承方式有三种——public、protected和private,不同的继承方式对继承到派生类中的基类成员有什么影响?见下图:
总的来说,父类成员的访问限定符通过继承派生到子类中之后,访问限定符的权限小于、等于原权限。其中,父类中的private成员只有父类本身及其友元可以访问,通过其他方式都不能进行访问,当然就包括继承。protected多用于继承当中,如果对父类成员的要求是——子类可访问而外部不可访问,则可以选择protected继承方式。
3、派生类对象的构造方式
前面也提到,派生类将基类中除去构造函数和析构函数的其他方法继承了过来,那么对于派生类对象中自己的成员变量和来自基类的成员变量,它们的构造方式是怎样的呢?
答案是:
1).先调用基类构造函数,构造基类部分成员变量,再调用派生类构造函数构造派生类部分的成员变量。
2).基类部分成员的初始化方式在派生类构造函数的初始化列表中指定。
3).若基类中还有成员对象,则先调用成员对象的构造函数,再调用基类构造函数,最后是派生类构造函数。析构顺序和构造顺序相反。见下:
class Test
{
public: Test()
{
cout<<"Test::Test()"<<endl;
}
private:
int mc;
};
class Base
{
public:
Base(int a)
{
ma = a; cout<<"Base::base()"<<endl;
}
~Base()
{
cout<<"Base::~base()"<<endl;
}
private:
int ma;
Test t;
};
class Derive : public Base
{
public:
Derive(int b) :Base(b)
{
mb = b;
cout << "Derive::derive()" << endl;
}
~Derive()
{
cout << "Derive::~derive()" << endl;
}
private:
int mb;
};
结果如下:
4、基类和派生类中同名成员的关系
派生类从基类中继承过来的成员(函数、变量)可能和派生类部分成员(函数、变量)重名。
1).前面提到,派生类从基类中继承了基类作用域,所以同成员名变量可以靠作用域区分开(隐藏)。
2).同名成员函数则有三种关系:重载、隐藏和覆盖。
(1)重载overload
函数重载有三个条件,一函数名相同,二形参类型、个数、顺序不同,三相同作用域。根据第三个条件,可知函数重载只可能发生在一个类中,见下:
终于进入正题,我们先来看下C++官方对于多态的描述:多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定。多态是面向对象编程的核心思想之一,因此我们有必要深入探索一下它的实现原理。理解了原理才能更好的使用。
C++按照实现的时机分为编译时多态和运行时多态
1.编译时多态也成为静态连编,是指程序在编译的时候就确定了多态性,通过重载机制实现
2运行时多态又称为动态联编,是指必须在运行中才能确定的多态性,通过继承和虚函数实现