0%

C++中的继承

继承概念

类级别的代码复用

  1. 继承方式:public、protected、private
  2. protectde访问权限/private访问权限:
    • protected —-> 在当前类和子类中可见,在其他地方不可见
    • private —-> 在当前类中可见,在其他地方不可见
  3. 父类成员在子类中的访问权限:min { 成员在父类中的原始访问权限,继承方式 }
  4. 一般都是public继承,protected/private继承很少使用/几乎不用
  5. 默认继承方式:
    • class定义的类默认继承方式是private
    • struct定义的类默认继承方式public
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class/struct 类名 : 需要继承的类
子类/派生类 父类/基类

class Person
{
public:
void Print()
{
cout << "nema:" << _name << endl;
cout << "age:" << _age << endl;
}
protected://类外不可见,内部和子类可见
string _name = "abc";
int _age = 10;
};

class Student : public Person
{
protected:
int _num = 2020;
};

子类继承方式为protected/private,从父类继承下来的所用成员的最低访问权限为protected/private,所以在子类外不可见

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 派生类不可见 在派生类不可见 派生类不可见

切片

子类对象,指针,引用可以直接赋值给父类对象,指针,引用,此处不是隐式类型转换

父类对象不能赋值给子类对象

父类指针、引用不能直接赋值给子类指针、引用。可以通过强制类型转换进行赋值,但是强制转换类型不安全,可能会导致越界,一般不使用强制类型转换,而是动态类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Person p;
Student st;

//切片
p = st;
Person& rs = st;
Person* ptrrs = &st;


//不可行
st = p;
Student& rp = p;
Student* ptrp = &p;

//不安全,可能会访问越界
Student& rp = (Student&)p;
Student* ptrp = (Student*)&p;

子类对应类型赋值给父类对应类型 —-> 切片:安全,不是隐式类型转换

父类(指针/引用)赋值给子类(指针/引用) —-> 强制类型转换:不安全,存在访问越界的风险

同名隐藏

父类和子类中有同名的成员,子类只能直接看到自己的成员,如果需要访问父类同名的成员,需要加父类作用域,不同的作用域下,含有同名成员,当前作用域下的成员就会隐藏其他作用域下的成员,不是继承体系独有的

  • 成员变量隐藏:成员变量的名称相同
  • 函数隐藏:函数名字相同,就会构成函数隐藏,与参数无关 —-> 这种情况是发生在父类和子类汇总,不在同一个作用域;函数重载:在同一个作用域,函数名相同,参数不同
1
2
3
4
5
6
7
8
9
10
11
12
13
class Student : public Person
{
public:
void setNum(int _num)
{ _num = _num; }//就近原则,此时为局部域,局部变量
protected:
int _num = 2020;
};

Student st;
cout << st._num << endl;
st.setNum(1999);
cout << st._num << endl;//2020

同名隐藏:父类中的同名成员被子类中的同名成员隐藏

父类、子类的作用域都是独立的,不同的作用域中可以有同名的成员,只要函数名相同,就会构成同名隐藏,不是函数重载

派生类的默认成员函数

编译器自动生成的默认构造自动调用父类的默认构造

显示定义的构造函数也自动调用父类的默认构造,在初始化列表中调用父类构造

父类成员必须要由父类的构造函数完成初始化

子类构造函数:

  1. 一定会调用父类的构造函数
    1. 如果不显示调用,自动调用父类的默认构造
    2. 如果显示调用,则调用显示指定的父类构造
  2. 继承自父类的成员变量,一定要通过父类的构造函数完成初始化,在子类的初始化列表中只能显示的初始化子类新增的成员变量
  3. 初始化顺序:一定是首先初始化父类成员,再去初始化子类成员
  4. 创建子类对象时,首先调用子类的构造函数,在子类构造函数的初始化列表中调用父类的构造函数,先执行父类的构造逻辑,然后再执行子类本身的构造逻辑

编译器自动生成的拷贝构造自动调用父类的拷贝构造

显示定义的拷贝构造,自动调用父类的默认构造,不是父类的拷贝构造

调用父类拷贝构造会有有切片操作

子类的拷贝构造:

  1. 默认行为(没有显示定义子类的拷贝构造):父类的拷贝构造
  2. 显示定义子类拷贝构造默认行为(没有显示调用父类的拷贝构造):父类的默认构造
  3. 在子类的拷贝构造中可以指定调用哪一个父类的构造函数,不一定是拷贝构造

编译器自动生成的赋值运算符重载函数自动调用父类的赋值运算符重载函数

子类赋值运算符和父类赋值运算符构成同名隐藏

子类的赋值运算符:

  1. 默认行为:调用父类的赋值运算符
  2. 显示定义:和父类的赋值运算符构成同名隐藏,如果需要调用父类的赋值运算符,需要指定父类的作用域
  3. 建议调用父类的赋值运算符:达到代码复用的目的

父类析构不需要显示调用,可能会导致资源二次释放的问题

子类析构函数:

  1. 编译器自动生成的析构函数自动调用父类的析构函数
  2. 显示定义的子类析构函数也会自动调用父类的析构函数
  3. 无论子类析构是否显示调用父类析构,编译器都会自动调用一次父类析构
  4. 子类析构和父类析构底层函数名相同,构成函数隐藏

继承与友元、静态成员

友元关系不能继承,基类的友元不能访问子类的私有和保护成员

基类定义了static静态成员,则继承体系中只有一个这样的成员,无论派生出多少个子类,都只有一个static成员实例

复杂继承

单继承:一个子类只有一个直接父类

1
2
3
4
5
6
class A
{};
class B : public A
{};
class C : public B
{};

多继承:一个子类有两个或以上直接父类

1
2
3
4
5
6
class A
{};
class B
{};
class c : public A, public B
{};

菱形继承:多继承的一种特殊情况

1
2
3
4
5
6
7
8
class A
{};
class B : public A
{};
class C : public A
{};
class D : public B, public C
{};

菱形继承存在数据冗余和二义性的问题

菱形虚拟继承

1
2
3
4
5
6
7
8
class A
{};
class B : virtual public A
{};
class C : virtual public A
{};
class D : public B, public C
{};

通过虚基表指针和虚基表,虚基表存放公共部分的偏移量,虚基表指针指向虚基表

虚基表

  1. 相对于当前位置的偏移量
  2. 相对于公共部分偏移量

菱形虚拟继承可以解决数据冗余和二义性

  1. 通过虚基表指针和虚基表实现
  2. 如果需要访问公共成员:首先通过虚基表指针找到虚基表,读取偏移量,当前位置偏移指定的偏移量,找到公共部分成员,切片操作的过程
  3. 通过指针大小,换取重复成员的大小
  4. 时间换空间的语法
-------------本文结束感谢您的阅读-------------