继承(inheritance)机制时面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类的特性基础上进行拓展,增加功能,这样产生新的类,称派生类
继承体现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程
继承后父类的Person成员(成员函数和成员变量)都会变成子类的一部分。这里体现出了Student复用了Person的成员
通过监视窗口我们可以看到继承后父类的成员变量的确是被子类对象复用了
通过函数调用我们可以看到父类成员函数在子类对象中的复用
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的 private成员 |
基类的protected成员 | 派生类的proteted成员 | 派生类的protected成员 | 派生类的 private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结
private
成员在派生类中不论是以什么方式继承都是不可见的(这里的不可见指的是基类的私有成员虽然还是被继承到了派生类对象中,但是在语法上限制了派生类对象不管在类外面还是类里面都不能去访问基类的private
对象)protected
public
>protected
>private
class
的默认继承方式是private
继承,struct
默认的继承方式是public
继承(不过最好还是显式写出)class Person
{
protected:
string _mame = "zhangsan";
string _sex = "man";
int _age = 22;
};
class Stuedent :public Person
{
protected:
int _stuid = 2024;
};
int main()
{
Stuedent stu;
Person per = stu;//将派生类对象赋值给子类对象
return 0;
}
注意
将派生类对象赋值给子类对象也可以称为”赋值兼容转换“
赋值兼容转换与普通赋值转换的区别就是,在一般赋值的时候,若两边对象/操作数 类型不一样的时候,会产生具有常属性的临时变量,这里我在之前博客中也有提到。赋值兼容转换则是当赋值两边对象不同时,中间并不会产生临时变量
这里有个形象的说法叫做切片或者切割,寓意是将派生类中父类那部分切来赋值过去
int main()
{
Stuedent s;
Person p = s;
Person& r = s;
Person* ptr = &s;
return 0;
}
在语句Person* ptr = &s;
中,prt
指向的并不是临时对象也不是单单是对象s
,而是子类对象s中从父类Person
切出来的那一部分
同理,在语句Person& r = s;
中,r
引用的是子类对象s中从父类Person
继承下来的那一部分(由于赋值兼容转换,中间没有产生具有常属性的中间变量,所以引用r
的前面也不需要加上const修饰)
关于继承中的作用域,有以下几条性质
class Person
{
protected:
string _name = "zhangsan";
int _num = 12;
};
class Student :public Person
{
public:
void print()
{
cout << "姓名:" << _name << endl;
cout << "学号:" << _num << endl;//访问Student自己的_num成员
cout << "年龄:" << Person::_num << endl;//访问从Person继承下来的_num成员
}
private:
int _num = 99;
};
int main()
{
Student stu;
stu.print();
return 0;
}
单继承:一个子类只有一个直接父类
多继承:一个子类有两个或者两个以上的直接父类
class Person
{
public:
string _name;
};
class Student :public Person
{
protected:
int _id;
};
class Teacher :public Person
{
protected:
int _num;
};
class Assistant :public Student, public Teacher
{
protected:
string _majorCource;
};
从上面对象成员模型构造,可以看出菱形继承存在数据冗余和二义性的问题,即在Assistant的对象中从Person继承下来的成员会有两份name,但是我们有时根本就不需要两个_name成员变量,存在空间浪费。
虚拟继承可以解决菱形继承的二义性和数据冗余问题。就比如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,就可以解决问题
class Person
{
public:
string _name;
};
//class Student :public Person
class Student :virtual public Person
{
protected:
int _id;
};
//class Teacher :public Person
class Teacher :virtual public Person
{
protected:
int _num;
};
class Assistant :public Student, public Teacher
{
protected:
string _majorCource;
};
为了更好的解释虚拟继承的原理,这里给出了一个简化的虚拟继承体系
class A
{
public:
int _a;
};
class B:virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
class D :public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
采用虚拟继承
以d对象中B部分为例,与普通继承不一样的是,d._b地址前面的位置存的不再是d.A::_a的数据,而是一串地址,根据这串地址我们找过去,发现数据为全0,该地址的下一位地址存的是28(十六进制),其实,这个值就是所谓的偏移量,编译器就是根据这个去找到从B中继承下来的有关A的成员变量
同理,d._c前面存的也是一串地址。
因为在虚拟继承中,D类型对象将A类型的成员放到了对象组成的最下面,这个成员同时属于B和C,当B和C要去寻找公共的A时,就会通过B和C两个指针,指向一张表,这两个指针叫做虚基表指针,两个表就叫做虚基表。虚机表中存的偏移量,通过拿到偏移量就可以找到A成员的位置
所以,此后我们在碰到这样的场景时
D dd;
B*pb=&dd
pb->a++;
当pb要去访问_a时,就先要利用存的指针找到偏移量,再根据偏移量加上自己的地址就可以找到_a