C++知识点总结:4.继承和派生(自用)


引用:
[1]C语言中文网


1. C++的继承方式

继承方式包括 public、private和 protected。如果不写,那么默认为private。

protected和private一样,对象不可访问。但是存在继承关系时,基类中的protected成员可以在派生类中使用,而基类中的private成员,则不可以在派生类中使用。

注意,我们这里说的是基类的 private 成员不能在派生类中使用,并没有说基类的 private 成员不能被继承。实际上,基类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。


访问权限:public > protected > private。

在继承方式中,基类和派生类的成员权限关系如下:

继承方式权限关系
public继承基类成员属性为public,则在派生类中为public属性;
基类成员属性为protected,则在派生类中为protected属性;
基类成员属性为private,则在派生类中不可访问;
protected继承基类成员属性为public,则在派生类中为protected属性;
基类成员属性为protected,则在派生类中为protected属性;
基类成员属性为private,则在派生类中不可访问;
private继承基类成员属性为public,则在派生类中为private属性;
基类成员属性为protected,则在派生类中为private属性;
基类成员属性为private,则在派生类中不可访问;

从上表中可以看到,从基类继承的成员属性权限,都不会超过继承方式指定的权限。例如:基类中成员为public属性,如果通过protected继承,则该成员在派生类中为protected属性(不得超过protected权限)。

同样,上表也指明无论什么继承方式,基类中的private属性成员都不能使用。

2. 访问基类中的private成员

派生类访问基类中的private属性成员变量的唯一方法,就是借用基类中非private属性成员函数来访问基类中的private属性成员变量。如果基类中没有非private属性成员函数,则基类中的private属性成员变量在派生类中无法访问。

3. 改变访问权限

使用 using 关键字可以改变基类成员派生类中的访问权限,例如将public改为 private、将protected改为public。

using只能改变基类成员在派生类中public和protected成员的访问权限,不能改变 private 成员的访问权限。

调用方法:
在这里插入图片描述

4. 名字遮挡

如果派生类中定义的成员变量和成员函数和基类中的成员变量和成员函数同名,则默认为使用派生类中定义的成员,基类中同名的成员被遮挡。但是,基类中同名的成员函数仍然可以调用,只不过要加上域解析符"::"。

重点:遮挡并不是重载。
只要派生类中成员的名字和基类中的成员名字相同,就构成遮挡。并不需要参数不同。如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数

只有一个作用域内的同名函数才具有重载关系,不同作用域内的同名函数是会造成遮蔽

5. 类作用域的继承嵌套

类内本身也是一种作用域。当发生类继承时,派生类的作用域就会嵌套到基类作用域之中。如下图所示:
在这里插入图片描述
基类的作用域可以被称为外层作用域,派生类的作用域为内层作用域。当编译器在内层作用域中无法找到成员后,编译器就会到外层作用域去找。 如果多层嵌套,那编译器还会继续往外层找。这个过程叫做名字查找(name lookup)

再强调下:只有一个作用域内的同名函数才具有重载关系,不同作用域内的同名函数是会造成遮蔽

6. 对象的内存模型

前提回顾:
类中的成员变量和成员函数在实例化后分别存储,对象的内存模型中仅存储成员变量,而成员函数存储在代码区,由所有对象共享。

当发生类继承后,对象内存模型的存储方式见下面这个例子(例子来自C语言中文网):

假如基类中有成员变量m_a, m_b。则基类对象obj_a的内存模型为:
在这里插入图片描述
当派生类B继承基类A,且新添加成员变量m_c,则派生类对象obj_b的内存模型为:
在这里插入图片描述
此时,新的派生类C继承基类B,且新添加成员变量m_d,m_b,m_c(派生类C中遮蔽了B类中的m_c和A类中的m_b)。此时派生类对象obj_c的内存模型为:
在这里插入图片描述

可以总结出的规律:

  1. 在派生类对象中,会把基类成员变量放前面,派生类的成员变量放后面
  2. 如果出现名字遮挡,对象的内存模型中仍然会保存基类的同名成员变量。
  3. 在派生类的对象模型中,会包含所有基类的成员变量。这种设计方案的优点是访问效率高,能够在派生类对象中直接访问基类变量,无需经过好几层间接计算。

7. 基类和派生类的构造函数

  1. 基类的构造函数不能被继承,需要通过派生类进行调用。派生类创建对象时必须要调用基类的构造函数。如果不指明,就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。
  2. 对继承过来的基类成员变量的初始化工作由派生类的构造函数完成。

但是基类中的private类型的成员变量在派生类中无法访问,没法直接通过派生类的构造函数进行初始化,此时解决方法是在派生类构造函数中调用基类构造函数

在派生类构造函数中对基类private类型的成员变量初始化的方法,见下例:

class People{
private:
    char* m_name;
    int m_age;
public:
    People(char* name, int age):m_name(name), m_age(age){}
    void show();
};

void People::show() {cout<< "People name is "<<m_name <<" age is "<< m_age<<endl;}

class Student: public People{
public:
    int m_score;
public:
    // 通过派生类构造函数中调用基类构造函数来给private修饰的成员赋值。
    Student(char* name, int age, int score):People(name, age), m_score(score){}
    void show();
};

void Student::show() {cout<< "Student class show "<<endl;}

int main(){
    Student stu("liyi", 18, 100);
    stu.show();
    stu.People::show();
    return 0;
}

在这里插入图片描述

注意
在派生类构造函数初始化中,当我们把基类构造函数放在后面,也是先调用基类构造函数。(初始化列表中的顺序不受影响)

Student(char* name, int age, int score):m_score(score), People(name, age){}

8. 构造函数的调用顺序

多层继承时,构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。
例如:
A -> B -> C。
A是B的基类,B是C的基类。A是C的间接基类。
构造函数的调用顺序为:A构造函数 -> B构造函数 -> C构造函数。

注意:派生类的构造函数不能调用间接基类,只能调用上级基类。即,C不能调用A的构造函数。

C语言中文网中的解释:
在这里插入图片描述

9. 基类和派生类的析构函数

  1. 基类的析构函数不能被继承
  2. 与构造函数不同,不用在派生类显式的调用基类析构函数,因为每个类只有一个析构函数,程序知道怎么选择。

调用顺序和构造函数相反

实例化对象:基类构造函数——>派生类构造函数
销毁对象:派生类析构函数——>基类析构函数

10. 多继承

派生类如果只有一个基类,则称为单继承
派生类如果有多个基类,则称为多继承

类似:

// 以公有方式继承A,私有方式继承B,保护方式继承C
class D: public A, private B, protected C{
     //构造函数的调用同单继承。同时初始化列表的顺序和调用顺序无关,和声明顺序有关,即先A后B再C。
     D():B(),A(),C()
     {
     }
}

多继承的命名冲突,如果多个基类出现同名成员(变量和函数),则需要域解析符::,来消除二义性。

11. 虚继承和虚基类

前提:
多继承会导致命名冲突。尤其是菱形继承。以下图片来自C语言中文网
在这里插入图片描述

//间接基类A
class A{
protected:
    int m_a;
};
//直接基类B
class B: public A{
protected:
    int m_b;
};
//直接基类C
class C: public A{
protected:
    int m_c;
};
//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //命名冲突
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};
int main(){
    D d;
    return 0;
}

在这里插入图片描述
为了解决上述问题,就提出了虚继承,使得在派生类中只保留一份间接基成员。(回顾一下,直接继承的是直接基成员,间接继承的是间接基成员。例如上例中,B和C是直接基类,A是间接基类)

定义:虚继承的目的是为了让某个类声明,愿意共享它的基类。这个被共享的基类就称为虚基类。(在下例中,是让B声明,愿意共享它的基类A,A被称为虚基类)

只需要在上述代码中,让B和C虚继承A。

class B: virtual public A{
protected:
    int m_b;
};
//直接基类C
class C: virtual public A{
protected:
    int m_c;
};

在这里插入图片描述

12. 虚继承的构造函数

  1. 正常单继承,最终的派生类只需要调用派生类构造函数来初始化它的直接基类,而间接基类,则是由直接基类来调用直接基类构造函数初始化。例如:一个A—>B—>C的继承,对于最终派生类C而言,B是直接基类,A是间接基类。A的初始化由B进行,B的初始化由A进行。
  2. 对于虚继承而言,虚基类的初始化则是由最终的派生来类的构造函数来进行。还是以上面菱形继承为例,对于最终派生类D而言,B和C是直接基类,A是B和C的虚基类。A的初始化需要由D的构造函数来实现。如果任由B和C分别通过自己的构造函数来初始化A,则会出现两份A的内存,从而出现二义性。
    在这里插入图片描述
  3. 之前的单继承就讲过,在基类的初始化顺序和初始化列表中的顺序无关,和继承时的声明有关。但是对于虚继承而言,不论声明是何顺序,虚基类总是最先进行初始化,之后再按照声明出现的顺序调用其他的构造函数进行初始化。

13. 虚继承的内存模型

正常继承的内存模型:
举例:A—>B—>C—>D(D继承C,C继承B,B继承A)
在这里插入图片描述
虚继承:
假设A 是 B 的虚基类,B 又是 C 的虚基类,D继承自B和A
在这里插入图片描述
从上面的两张图中可以发现,虚继承时的派生类对象被分成了两部分

  • 不带阴影的一部分偏移量固定,不会随着继承层次的增加而改变,称为固定部分;
  • 带有阴影的一部分是虚基类的子对象,偏移量会随着继承层次的增加而改变,称为共享部分。

(虚继承的放后面,普通继承的放前面)

14. C++将派生类赋值给基类(向上转型)

在数据类型转化中,int可以转化为float,float也可以转化为int。那么,类作为一种数据类型,也是可以发生数据类型转化的。但是前提是必须在基类和派生类之间才有意义(其他类别之间没有相同的成员,转化毫无意义)。当把派生类赋值给基类的数据类型转化被称为向上转型。把基类赋值给派生类的数据类型转化被称为向下转型

在这里插入图片描述

14.1 派生类对象直接赋值给基类对象

这种方式的数据类型转化,也叫向上转型,其方法就是讲派生类中的同名成员变量数值直接赋值给基类中的同名成员变量,基类中不同名的成员变量舍弃。具体见下图(图来自C语言中文网
在这里插入图片描述

至于成员函数不存在赋值问题,所以不受影响,基类仍然使用基类的成员函数,派生类使用派生类的成员函数。

注意:这种转换关系是不可逆的,只能用派生类对象给基类对象赋值,而不能用基类对象给派生类对象赋值。

14.2 派生类对象指针直接赋值给基类对象指针

与对象直接赋值的方式不同,对象指针的直接赋值,会直接改变基类对象指针的方向。即让基类指针指向派生类对象。此时基类对象指针就可以访问派生类对象的成员变量。

(此时注意,仍然是只能访问同名的成员变量)

但是值得注意的是:基类对象指针虽然指向派生类对象的内存地址,但是仍然调用基类对象的成员函数。具体原理为编译器根据指针的地址访问成员变量,指针指向谁就访问谁的成员变量,但是编译器会根据指针的类型访问对应类型对象的成员函数,指针类型是哪个类就访问哪个类的成员函数

class A{
public:
    int m_a;
    void display(){cout<<"class A: m_a="<<m_a<<endl;}
    A(int a);
};
A::A(int a):m_a(a){}

class B: public A{
public:
    int m_b;
    int m_c;
    void display(){cout<<"class B: m_a="<<m_a<<", m_b="<<m_b<<endl;}
    B(int a, int b, int c);
};
B::B(int a, int b, int c):A(a), m_b(b), m_c(c) {}

int main(){
    A* pa = new A(10);
    B* pb = new B(1, 2, 3);
    cout << "对象指针转换前,pa的地址为:"<< pa <<endl;
    cout << "对象指针转换前,pb的地址为:"<< pb <<endl;
    cout << "对象指针转换前 pa->m_a:"<< pa->m_a <<endl;
    pa->display();
    pb->display();
    pa = pb; //对象指针向上转型
    cout << "对象指针转换后,pa的地址为:"<< pa <<endl;
    cout << "对象指针转换后,pb的地址为:"<< pb <<endl;
    cout << "对象指针转换后 pa->m_a:"<< pa->m_a <<endl;
    pa->display();
    pb->display();
    delete pa, pb;
    return 0}

在这里插入图片描述

14.3 派生类对象引用直接赋值给基类对象引用

引用其实就是通过指针实现的,是对指针的封装,因此对象引用赋值的方法同对象指针的赋值方法。

int main(){
    A a = A(10);
    B b = B(1, 2, 3);
    A& ra = a;
    B& rb = b;
    cout << "对象指针转换前,ra的地址为:"<< &ra <<endl;
    cout << "对象指针转换前,rb的地址为:"<< &rb <<endl;
    cout << "对象指针转换前 ra->m_a:"<< ra.m_a <<endl;
    ra.display();
    rb.display();
    ra = rb; //对象指针向上转型
    cout << "对象指针转换后,pa的地址为:"<< &ra <<endl;
    cout << "对象指针转换后,pb的地址为:"<< &rb <<endl;
    cout << "对象指针转换后 pa->m_a:"<< ra.m_a <<endl;
    ra.display();
    rb.display();
    return 0;
}

但是,和指针相区别的是,引用没有改变地址。
在这里插入图片描述

补充:15 派生类对象指针直接赋值为什么会出现有的地址相同,有的地址不同。

原因见下例:

继承关系:(在多继承中会出现该问题)
在这里插入图片描述

class A{
public:
    int m_a;
    void display(){cout<<"class A: m_a="<<m_a<<endl;}
    A(int a);
};
A::A(int a):m_a(a){}

class B: public A{
public:
    int m_b;
    void display(){cout<<"class B: m_a="<<m_a<<", m_b="<<m_b<<endl;}
    B(int a, int b);
};
B::B(int a, int b):A(a), m_b(b){}

class C{
public:
    int m_c;
    void display(){cout<<"class C: m_c="<<m_c<<endl;}
    C(int c);
};
C::C(int c):m_c(c){}

class D:public B, public C{
public:
    int m_d;
    void display(){cout<<"class C: m_c="<<m_c<<endl;}
    D(int a, int b, int c, int d);
};
D::D(int a, int b, int c, int d):B(a, b), C(c), m_d(d){}

int main() {
    A *pa = new A(10);
    B *pb = new B(1, 2);
    C *pc = new C(20);
    D *pd = new D(40, 50, 60, 70);
    cout << "对象指针转换前,pa的地址为:" << pa << endl;
    cout << "对象指针转换前,pb的地址为:" << pb << endl;
    cout << "对象指针转换前,pc的地址为:" << pc << endl;
    cout << "对象指针转换前,pd的地址为:" << pd << endl;
    pa = pd;
    pb = pd;
    pc = pd;
    cout <<"--------------------------------"<<endl;
    cout << "对象指针转换后,pa的地址为:" << pa << endl;
    cout << "对象指针转换后,pb的地址为:" << pb << endl;
    cout << "对象指针转换后,pc的地址为:" << pc << endl;
    cout << "对象指针转换后,pd的地址为:" << pd << endl;

    return 0;
}

输出:
在这里插入图片描述
原因是:

D类由于多继承的存在,导致的不同类中成员变量的起始指针地址不同。如下图所示,在D的内存模型中的类A的成员变量起始地址和类B、类D的地址是一样(因为它们可以看作是单继承A->B->D),类C的成员变量地址则不一样(相对D而言,C是多继承。B->D, C->D)。因此,在pd赋值给pc时,需要先把pd的地址偏移到类C的成员变量所在地址位置,然后再赋值给pc。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

玉堃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值