两个类间的关系:组合与继承
继承:可以使用现有类的所有功能,并在无需编写原来的类的情况下对这些功能进行扩展。
通过继承创建的新类称为“子类”或“派生类”。 被继承的类称为“基类”,“父类”或“超类”。
继承的作用:代码的复用
访问权限
派生类对象怎么构造
在派生类构造函数的初始化列表中,指定基类成员的构造方式
class Base
{
public:
Base(int data) :ma(data)
{
cout << "Base()" << endl;
}
~Base(){ cout << "~Base()" << endl; }
protected:
int ma;
};
class Derive : public Base
{
public:
// “Base”: 没有合适的默认构造函数可用
Derive(int data) :Base(data), mb(data)
{
cout << "Derive()" << endl;
}
~Derive(){ cout << "~Derive()" << endl; }
private:
int mb;
};
同名成员的访问方式(基类和派生类的同名成员方法的关系)
重载:在基类或者派生类同一个类作用域当中的。
函数名相同,参数列表不同
隐藏:方法分布在基类和派生类中
方法名字相同,就说派生类的同名方法把基类的同名方法给隐藏了
覆盖/重写:方法分布在基类和派生类中
基类的方法是虚函数,派生类的方法和基类的虚函数 返回值相同,函数名相同,参数列表也相同,那么此时派生类的这个函数自动处理成虚函数,和基类的函数成为覆盖关系, 在虚函数表中进行覆盖!
不可以的
派生类对象 = 基类对象
派生类指针(同引用) =》 基类对象可以
基类对象 = 派生类对象
基类指针(同引用)=》 派生类对象
判断是否为动态绑定
没有virtual , 一定是静态绑定
有virtual,而且用指针&引用 =》 动态绑定 call eax => 运行时多态
有virtual,但是用对象调用 =》 静态绑定 call 0x9009876多态:基类指针(引用)指向派生类对象
实现虚函数需要的的两个条件
1.取地址 2.依赖对象
对象的生命周期【构造函数完 == 析构函数开始】
构造函数和析构函数内部都不能发生动态绑定,多态!(调用虚函数不发生动态绑定)
虚函数在构造函数开始需要做的事
1. 栈帧开辟 2.栈帧初始化 3.vftable 存入 vfptr里面
class Base
{
public:
// 构造函数和析构函数内部都不能发生动态绑定,多态!
// 对象 【构造函数完,析构函数开始】
Base(int data) :ma(data)
{
// 1. 栈帧开辟 2.栈帧初始化 3.vftable=》vfptr里面
cout << "Base()" << endl;
//clear();
this->show();
}
virtual ~Base()
{
this->show();
cout << "~Base()" << endl;
}
void clear()
{ memset(this, 0, sizeof(*this)); }
virtual void show(int i=10)
{
cout << "Base::show" << endl;
}
private:
int ma;
};
编译时期(静态指针指向) 方法的访问权限。基类方法的访问限定
运行时期 RTTI 运行时的类型信息方法的访问权限问题 编译阶段确定 基类方法的访问限定
方法参数的压栈,是编译时候确定。具体调用哪个方法,动态绑定,是运行时期确定
虚函数表:
继承结构中,构造函数刚开始的时候,每一层构造函数会把自己类型的虚函数表的地址填入vfptr里面
纯虚函数------含有纯虚函数的类是抽象类
class Man // 拥有纯虚函数的类 =》 抽象类
{
public:
Man(int id, string name)
:_id(id), _name(name){}
// 基类提供的这个虚函数,就是为所有派生类提供统一的虚函数接口,
// 让派生类自己去重写的
virtual void show() = 0; // 虚函数 纯虚函数
protected:
int _id;
string _name;
};
基类没有函数可以写成纯虚函数的情况下,析构函数可以写成纯虚函数。在类外面需要重写析构函数(标明作用域)
class Base
{
public:
Base(int data) :ma(data){ cout << "Base()" << endl; }
virtual ~Base() = 0;
virtual void show(int i=10)
{
cout << "Base::show" << endl;
}
private:
int ma;
};
Base::~Base()
{
cout << "~Base()" << endl;
}
虚继承
多重继承/菱形继承 ,产生的问题 :派生类有多份基类的数据。所以用虚继承来解决该问题
/**
普通继承(没有使用虚基类)
*/
// 基类A
class A
{
public:
int dataA;
};
class B : public A
{
public:
int dataB;
};
class C : public A
{
public:
int dataC;
};
class D : public B, public C
{
public:
int dataD;
};
上面是一个简单的多继承例子,我们启动Visual Studio命令提示功能,切换到NormalInheritance.cpp文件所在目录,输入一下命令:c1 NormalInheritance.cpp /d1reportSingleClassLayoutD
我们可以看到class D的内存布局如下:
从类D的内存布局可以看到A派生出B和C,B和C中分别包含A的成员。再由B和C派生出D,此时D包含了B和C的成员。这样D中就总共出现了2个A成员。大家注意到左边的几个数字,这几个数字表明了D中各成员在D中排列的起始地址,D中的五个成员变量(B::dataA、dataB、C::dataA、dataC、dataD)各占用4个字节,sizeof(D) = 20。
为了跟后文加以比较,我们再来看看B和C的内存布局:
class A
{
public:
A(int data) :ma(data){ cout << "A()" << endl; }
~A(){ cout << "~A()" << endl; }
protected:
int ma;
};
class B : virtual public A
{
public:
B(int data) :A(data), mb(data){ cout << "B()" << endl; }
~B(){ cout << "~B()" << endl; }
protected:
int mb;
};
class C : virtual public A
{
public:
C(int data) :A(data), mc(data){ cout << "C()" << endl; }
~C(){ cout << "~C()" << endl; }
protected:
int mc;
};
class D : public B, public C
{
public:
D(int data) :md(data),A(data), B(data), C(data)
{
cout << "D()" << endl;
}
~D(){ cout << "~D()" << endl; }
void show(){ cout << ma << endl; }
protected:
int md;
};
int main()
{
cout << sizeof(D) << endl;
D d(10);
d.show();
/*
通过D类的vbptr,在vbtable寻找A类vbptr的偏移量,找到A类的vbptr,修改A类的成员的值
*/
int *p = (int*)&d;
int *q = (int*)*p;
q += 1;
int offset = *q;
char *p1 = (char*)&d;
p1 += offset;
*(int*)p1 = 30;
d.show();
return 0;
}
VirtualInheritance.cpp和NormalInheritance.cpp的不同点在与B和C继承A时使用了virtual关键字,也就是虚继承。同样,我们看看B、C、D类的内存布局情况:
我们可以看到,菱形继承体系中的子类在内存布局上和普通多继承体系中的子类类有很大的不一样。对于类B和C,sizeof的值变成了12,除了包含类A的成员变量dataA外还多了一个指针vbptr,类D除了继承B、C各自的成员变量dataB、dataA和自己的成员变量外,还有两个分别属于B、C的指针。
由上图,我们可以发现。虚继承之后,虚基类(A)的数据,搬到派生类对象的后面,原来的地方放一个vbptr(虚基类指针)
上图为D类的内存布局,虚函数表保存的是B和C各自vbptr到A::ma的字节数,也就是偏移量。而正是因为这里保存的便宜量,我们才可以在D类里通过指针访问A类的成员,并修改。