3.1继承:
被继承的类称为基类;
新产生的类称为派生类;
继承时会有基类的属性;
派生类无法访问基类的私有成员;
不论继承权限如何,派生类总能访问隐藏基类对象的public与protected成员;
如在派生类内创建基类对象,则只可访问其public成员
public继承反应了现实中 “is a” 的关系
class fish
{
public:
string name;
}
class GoldFish:public fish //此处的public为继承权限 缺省时为private继承
{
public:
string color;
}
int main()
{
GoldFish gfish;
gfish.name ="jinyu";
gfisn.color = "gold";
}
/ | 自身类 | 子类 | 外界 |
---|---|---|---|
public | 1 | 1 | 1 |
protected | 1 | 1 | 0 |
private | 1 | 0 | 0 |
派生类对象构建时先构建其基类-产生对象时,派生类含有隐藏基类对象
继承权限:
class GoldFish:public fish中的public为继承权限
子类继承的父类成员在自身类内不能高于“继承权限”
子类的构造函数优先构造父类再构造子类
执行子类的析构函数时优先析构子类再析构父类
父类的构造需要传参时,则必须写于初始化列表中
同名问题
属性同名
同名隐藏-若派生类对象某一属性与基类重名,则会隐藏 基类的同名属性;要对其访问需要使用作用域解析符::
class A{
//若声明派生类为基类的友元friend class B;则派生类可访问基类所有成员
protected:
int ax;
public:
A() :ax(0){}
};
class B : public A
{
private:
int ax;
public:
B():ax(10){}
void fun ()
{
ax = 100 ;
//可以typedef A Base;
//从而Base::ax = 200;
A::ax = 200;
}
};
int main()
{
B b;
b.fun() ;
}
方法同名
同样对其使用作用域解析符::
class A
{
protected:
int ax;
public:
A() :ax(0){}
void fun ()
{
ax = 100 ;
A::ax = 200;
}
};
class B : public A
{
private:
int ax;
public:
B():ax(10){}
void fun ()
{
ax = 100 ;
A::ax = 200;
}
};
int main()
{
B b;
b.fun() ;
b.A::fun()
}
注意,仅可在public继承下可实现;private或protected继承下的隐藏基类在外部函数(main)中无法被访问
赋值兼容规则
在任何需要基类对象的地方都可以用公有派生类的对象来代替,这条规则称赋值兼容规则。它包括以下情况:C++面向对象编程中一条重要的规则是:公有继承意味着“是一个”。一定要牢牢记住这条规则。
1.派生类的对象可以赋值给基类的对象,这时是把派生类对象中从对应基类中继承来的隐藏对象赋值给基类对象。反过来不行,因为派生类的新成员无值可赋。
2.可以将一个派生类的对象的地址赋给其基类的指针变量,但只能通过这个指针访问派生类中由基类继承来的隐藏对象,不能访问派生类中的新成员。同样也不能反过来做。
3.派生类对象可以初始化基类的引用。引用是别名,但这个别名只能包含派生类对象中的由基类继承来的隐藏对象。
3.2 多态
多态分为静多态与动多态
相对于发生在编译期的静多态,动多态发生在运行期;这也是两者的主要区别
时期 | 例子 | |
---|---|---|
静多态 | 编译期 | 摸板,函数重载 |
动多态 | 运行期 | 虚函数 |
动多态
动多态的产生条件:
使用指针或是引用调用虚函数 ,且对象需是一个完整的对象
完整的对象是指 构造函数执行完毕,析构函数未执行的实例
即动多态是通过继承(public继承)、虚函数(virtual)、指针(->& virtual)来实现。(缺一不可)
动多态的调用过程:
使用指针或者引用调用虚函数
在对象中找到vfptr
根据vfptr找到vftable
在vftable中找到要调用的函数
调用
虚函数
virtual
虚函数具有传递性
class object
{
public:
virtual object * fun()
}
class Base :public Object
{
public:
virtual Base * fun()
}
;
vftable(虚表)
· vftable什么时候产生?于何处存储?
编译期;rodata段(只读数据段)
class中有虚函数就会创建虚表;
虚表存储各虚函数的函数指针;
编译时,编译器若发现派生类对象有基类的同名函数,则会发生同名覆盖,虚表中的函数指针被替换;
class Base
{
private:
int value;
public:
Base(int x = 0) : value(x){}
virtual void add() {}
virtual void fun() {}
virtual void print() const {}
}
class derive : public Base
{
private: int sum;
public:
derive (int x = 0) : base(x+10) , sum(x){}
virtual void add() {}
virtual void fun() {}
virtual void print() const {};
int main()
{
derive derive (10);//大小为12个字节,8(sum,value)+ 4 (虚表指针vfptr)
}
为了调用虚表中的函数指针,每个有虚函数的类都会额外开辟4个字节来存储一个虚指针_vfptr,用其来索引向自己类的虚表;
虚表指针_vfptr
_vfptr在构造时候写入对象的存储空间,其用来指向该类的vftable
一个类的虚表只有一份
虚表指针存在于派生类对象的隐藏基类中
如在上例中,创建derive对象时,首先调用base的构造函数创建隐藏base类;因其有虚函数,编译器同时创建虚表指针_vfptr,使其指向base的虚表;之后构建derive,对所有vftable中的同名函数进行同名覆盖,同时,该派生类中的虚表指针改为指向derive的虚表;
即-父类中的虚函数会被子类中相同的函数覆盖;该过程发生于子类在构建时的虚函数表中
什么情况下析构函数需写成需虚函数?
当存在父类指针指向堆上的子类对象时,则需把父类的虚构函数写成虚函数**
构造函数能不能写成虚函数
不能,构造函数是虚表创建的前提,而virtual函数的构造需要用到虚表
静态函数能不能写成虚函数
不能;静态函数不依赖于对象,从而无法产生动多态
析构函数能不能写成虚函数
能,当基类析构函数声明为虚函数时,其派生类析构函数自动带有virtual声明
析构函数会reset虚表,当derive的析构函数执行完成后,_vfptr会重置指向base的虚表,从而执行base的析构函数;
虚函数能否写为内敛函数
不能,虚函数在编译期需要将函数指针放入vftable;内敛函数在编译期展开;在release版本中没有地址
类的编译顺序
先编译类名
再编译成员名
再编译成员函数
RTTI
vftable = RTTI + 函数指针
int main()
{
//dynamic_cast<type_name> 父类指针强转子类指针的专用类型指针,
//于vftable的RTTI中寻找type_name类型的
//1.必须有RTTI,2.父类指针指向的对象中的RTTI确实是子类
Derive *pd = dynamic_cast<Derive*>(p);
return 0;
}
菱形继承与虚继承
菱形继承
——该继承会导致造成公共基类在派生类对象中存在多个实例
使用虚继承来解决菱形继承问题
class Object
{
int value;
public:
Object(int x = 0) : value(x){}
};
class derive: virtual public Object//虚继承
{
int num;
public:
Base(int x = 0) : num(x),Object(x + 10){}
};
class Test :virtual public Object
{
int sum;
public:
Test(int x = 0): sum(x),Object(x + 10){}
};
class Det : public base,public Test
{
private:
int total;
public:
Det(int x=0):total(x),base(x+10),Test(x+20),Object(x+100){}
};
int main()
{
det d(0);
return 0;
}
d1内存分配图如下
object首先被创建
被虚继承的类称为虚基类;
虚基类在派生类对象中存放于vbtable中;
虚基类在派生类中被构造时,在原本存储该基类对象的位置上创建一个指针,来指向虚基类实例的位置;
从而保证虚基类在派生类中只会有一个实例存在
虚基类在派生类构造时会被直接当作父类继承
纯虚函数与抽象类
virtual void add() = 0;
base的纯虚函数实现依靠derive;
纯虚函数是为给派生类提供接口;
有纯虚函数的类叫做抽象类
抽象类不能用来实例化对象,出于该目的,也常将抽象类的构造函数声明为protect权限;
要求限制子类必须覆盖某个接口
class A
{
public:
virtual void fun() = 0; //纯虚函数
}
int main
{
A a;
}