【C++】面向对象三大特性之继承
- 继承的概念
- 继承基类成员访问方式的变化
- 子类到父类对象之间赋值兼容转换
- 继承中的作用域
- 子类的默认成员函数
- 继承和友元、静态成员的关系
- 菱形继承和菱形的虚拟继承
- 虚拟继承解决二义性和数据冗余
继承的概念
继承:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类 。而以前我们接触的复用都是函数复用,继承是类设计层次的复用。
继承基类成员访问方式的变化
继承后的成员权限:
- 基类private成员在派生类中无论以什么方式继承都是不可见的
- 基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private。
class A {
public:
int a = 10;
protected:
int b = 20;
private:
int c = 30;
};
class B :private A {
void print()
{
cout << a << endl;
cout << b << endl;
//cout << c << endl;不可访问
}
};
子类到父类对象之间赋值兼容转换
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割
- 基类对象不能赋值给派生类对象
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的
- 切片仅限公有继承。如:父类为公有,子类保护或私有继承后,成员变为保护和私有,子类再切片给父类,那么被继承的成员权限与父类不一样了,所以切片仅限公有继承
class A
{
public:
int _a;
};
class B : private A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
int main()
{
B b;
C c;
A a(b);//出错
A aa(c);
}
继承中的作用域
- 在继承中父类和子类都有自己独立的类域。
- 当子类和父类中存在同名成员时,子类将会屏蔽继承于父类的同名成员,这种情况被称为隐藏或重定义。(子类内部优先使用自己类域的同名成员,子类中使用 基类::基类成员 显示访问)
- 子类和父类中的同名成员函数并不构成函数重载,因为它们所处于不同的类域,子类会隐藏父类同名函数。
- 父类和子类尽量不要使用同名成员。
子类的默认成员函数
父、子类中成员处理方式
- 子类中有两部分成员,一类是子类原生的成员,另一类是继承于父类的成员。
- 对于原生成员,按照普通类调用默认成员函数的规则进行处理;对于继承自父类的成员,将会调用父类中的默认成员函数进行处理。(各玩各的)
需要自己写默认成员函数的情况
-
对于默认构造函数,如果父类没有默认构造函数,就需要我们自己去写,显式调用构造
-
对于拷贝构造和赋值重载,如果子类存在深拷贝的问题,这个时候就需要我们自己实现拷贝构造和赋值重载来实现深拷贝
-
对于析构函数,根据具体实际的情况,如果子类有资源需要释放,那我们就需要自己手动实现析构。
以下是显示写默认成员函数的例子
构造:父类中没有默认构造函数,需要在子类中显示调用
拷贝构造函数:必须调用基类的拷贝构造完成基类的拷贝初始化
派生类的赋值重载必须要调用基类的赋值重载完成基类的复制
class Person
{
public:
Person(const char* name)
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
delete[] p;
}
protected:
string _name; // 姓名
int* p = new int[10];
};
派生类中
1、构造函数,父类成员初始化必须调用父类的构造函数
class Student : public Person
{
public:
Student(const char* name, int num)
:Person(name)//调用父类的构造函数
,_num(num)
, _address("南京")
{}
Student(const Student& s)
:Person(s)//发生切片 子类对象赋值给父类对象
, _num(s._num)
, _address(s._address)
{}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);//切片
_num = s._num;
}
return *this;
}
//析构函数
//子析构函数和父析构函数构成隐藏关系(由于多态关系需求,所有析构函数都会处理成destructor函数名)
//调用析构函数的时候,先析构子类,在析构父类;子类不需要显示调用父类析构,子类析构后会自动调用父类析构
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _num; //学号
string _address;
};
我们并不需要显式调用父类的析构函数,因为出了子类析构函数的作用域,编译器会自动调用父类的析构。手动调用父类析构将会造成重复析构。
继承和友元、静态成员的关系
- 友元关系不能被继承
- 父类中的静态成员也会被继承,但是整个继承关系中共用这个静态成员
菱形继承和菱形的虚拟继承
- 什么是菱形继承?菱形继承的问题是什么?
菱形继承是多继承的一种特殊情况,父类对象二义性,数据冗余
class A
{
public:
int _a;
};
class B : public A
//class B : virtual public A
{
public:
int _b;
};
class C : public A
//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中会有两份A成员,调用时存在二义性和数据冗余
虚拟继承解决二义性和数据冗余
未使用虚拟继承
通过调用内存,会发现对象d中存在两份的_a,存在二义性和数据冗余
在上述代码中,class B : virtual public A;class C : virtual public A
当使用了虚拟继承,通过调用内存,发现对象d中仅有一份_a,但是继承于B类和C类的_b和_c上方多了一串地址,查找这个地址,发现这个地址之后的位置存放一个数字0x14(十进制20),这个数字就是b中_a距离公共_a的偏移量,如【0x0115FE5C+0x00000014=0x0115FE70】这样就解决了菱形继承成员冗余的问题
这里的A类叫做虚基类,在对象d中,将虚基类的成员放到一个公共的位置,继承的B、C类需要找到A的成员,通过虚基表【存储偏移量的表】中的偏移量进行计算。可以看到虚基表中第一行还空置了4个字节,这块空间存放的也是一个偏移量,它是指向虚基表的指针或者偏移量表格指针