C++:继承

1. 继承的概念及其定义

1.1 概念

继承(inheritance)是面向对象编程的三大核心特性之一,核心价值是代码复用建立类的层次关系,是面向对象程序设计使代码可以复用的最重要的手段。

简单来说,继承允许一个类去获取另一个类已有的成员变量和成员函数,无需重复编写这些代码;继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的是函数层次的复用,而继承是类设计层次的复用。

简单来说继承就好比子女继承父母的 "资产",同时还能拥有自己的 "专属东西"

这里的 "父母" 就是「基类 / 父类」,他们的 "资产"(比如房子、存款、生活技能)就对应类里的成员变量(数据)和成员函数(方法)。

这里的 "子女" 就是「派生类 / 子类」,子女不需要重新创造这些 "资产",天生就能拥有并使用父母的这些东西(对应代码复用,不用重复编写相同逻辑)。

子女除了继承来的东西,还能有自己的专属物品(比如自己的手机、职业技能),对应派生类可以新增自己独有的成员变量和成员函数;同时子女也可以对继承来的技能进行优化(比如父母只会做家常菜,子女在这个基础上学会了做精致西餐),对应派生类重写基类的方法,定制化实现功能。

所以说,继承就是 "拿来主义 + 个性化改造",少写重复代码,还能实现功能升级

1.2 定义

1.2.1 继承的格式

我们来举一个例子,比如现在我需要统计全校师生的信息,那么不管是学生还是老师,都会有年龄、姓名这样的共有信息,但是对于学生来说,学生还有学号这一独有信息,对于老师来说,还有教学科目这一独有信息。那么此时我会创建一个类用来存储个人信息,这个类叫:Person。对于老师和学生分别用一个类来存储各自的独有信息,那就有Student和Teacher这两个类,那么对于继承的格式就是这样的:

这里的Student就是派生类,Person就是基类,public就代表继承的方式,由此可见在继承当中一定存在这三个信息。同时对于继承与被继承的两个类之间的关系,我们称之为:派生类和基类。或者是:子类和父类。我们一般用这两组称呼,不要将称呼混搭,比如说成派生类和父类,这种说法就比较奇怪。

在这里我们让Student和Teacher都继承Person,在主函数当中,Student这个类中并没有identify 这个函数,但是实例化出来的对象 s 却可以使用,这就是继承的原因。

1.2.2 基类成员访问方式的变化

大家注意看,在上述的Person类中,出现了protected访问限定符,可是我们之前一直使用的都是private访问限定符去修饰私有成员,为什么到继承这里就变了呢?

我在最初讲解类和对象的时候就提到过:"目前阶段protected和private是一样的,以后继承章节才能体现出他们的区别。" 这是我的文章链接,大家可以去阅读一下:https://blog.csdn.net/2502_91842264/article/details/155107272?fromshare=blogdetail&sharetype=blogdetail&sharerId=155107272&sharerefer=PC&sharesource=2502_91842264&sharefrom=from_link

首先我们要先说明一下继承方式的概念,在C++中,有三种继承方式:private继承、protected继承、public继承。同时也有三种访问限定符:private、protected、public。

用 private 修饰的成员(变量 / 函数),只有当前类自己的成员函数能访问,其他任何外部代码(包括子类、普通函数、主函数)都碰不到。

用 protected 修饰的成员,当前类自己能访问,它的子类(不管是公有 / 私有 / 保护继承)也能访问,但外部代码(比如主函数、普通函数)还是碰不到。

当派生类从基类继承的时候,就有一下这些关系:

由此可见: 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

对于上面这个表格大家可以这样去总结:,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式==Min(成员在基类的访问限定符,继承方式),public >protected> private

表格中所说的,比如:"派生类的public成员",意思是指:派生类从基类当中通过public的继承方式继承的基类中的public成员,对于派生类来说是派生类的public成员。另外,所说的:"在派生类中不可见"指的是不可以在派生类当中直接使用,但是能间接使用:

比如在这段代码当中,Studen和Teacher都是通过public的方式继承的Person,但是Studen和Teacher实例化出来的对象在调用identify函数的时候,却通过该函数调用到了Person的私有成员变量。

这里需要注意的点是:使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public。这也就意味着继承方式是可以不写的,比如这样:

那Student就是默认以private的方式继承Person。不过在写代码或者工作的时候,最好显示的写出继承方式

并且对于继承来说:在实际运用中⼀般使用都是public继承,几乎很少使用 protetced / private 继承,也不提倡使用 protetced / private 继承,因为 protetced / private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

1.3 继承类模板

继承类模板的格式其实跟普通的继承可是差不太多:

在这里我定义了一个类模板Stack,然后包含了标准命名空间里面的vector容器的头文件。此时我让Stack以public的方式继承vector,就可以使用vector中的函数。但需要注意的是:基类是类模板时,需要指定⼀下类域,就是图中的 " vector<T> : : ",否则编译报错 :error C3861: "push_back": 找不到标识符。 因为 stack 实例化时,也实例化 vector 了,但是模版是按需实例化, push_back 等成员函数未实例化,所以找不到。

2. 基类和派生类之间的转换

通常情况下我们把一个类型的对象赋值给另一个类型的指针或者引用时,存在类型转换,中间会产生临时对象,所以需要加 const,如:int a = 1; const double& d = a;

在 public 继承中 ,就是一个特殊处理的例外,派生类对象可以赋值给基类的指针 / 基类的引用,而不需要加 const,这里的指针和引用绑定是:派生类对象中的基类部分。也就意味着一个基类的指针或者引用,可能指向基类对象,也可能指向派生类对象

我们用一段代码来演示这段话:

现在有这样一段代码:一个基类person和一个以public继承方式继承的派生类student。然后进行下列操作:

首先实例化了一个Student类的对象sobj,再实例化了一个Person类的指针pp,用来接收sobj的地址,大家会发现,此时在左侧没有加const,编译依然正常通过。包括下面用sobj直接赋值给 rp 也是。

并且用派生类对象给基类对象进行赋值时也没有问题,派生类对象赋值给基类对象是通过基类的拷贝构造函数或者赋值重载函数完成的,这两个函数的细节我们后面再讲,这个过程就像派生类自己定义部分成员切掉了一样,所以也被叫做切割或者切片:

但如果用基类对象给派生类对象赋值就会引发报错,因为就目前阶段而言,基类对象不能赋值给派生类对象,等后续我们会讲,在一种特殊情况下,基类对象也可以赋值给派生类对象。

此外,基类的指针或者引用,可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI (Run-TimeType Information) 的 dynamic_cast 来进行识别后进行安全转换。

3. 继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域。

  2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。但如果在派生类成员函数中,仍想要访问基类成员,可以使用 基类::基类成员 显示访问。

  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏

  4. 注意在实际中在继承体系里面最好不要定义同名的成员。

我们用一段代码进行举例:

派生类是Student,基类是Person,继承方式是public,我们可以看到基类和派生类当中有一个同名的成员变量:_num。先实例化出来一个对象s1,调用Student中的Print函数,当我想要使用Person类中的_num的时候,需要用到域作用限定符来指定类域,但如果不指定类域的话,就代表使用的就是派生类自身的_num。

这边给大家来一道易错题:

这道题最容易错误的点在于,这两个函数看上去非常像函数重载,导致我们会误以为:在主函数当中通过传不同的参数,就能找到对应的函数,从而选择C.正常运行。但实际上,函数重载这个情况一定是在同一作用域下才能构成,但是我们前面提到:在继承体系中基类和派生类都有独立的作用域。所以这里构成的是隐藏。并且为什么不能通过不同的参数类型从而找到对应的函数呢?还是因为隐藏,因为如果是成员函数的隐藏,只需要函数名相同就构成隐藏。所以对于b.fun(10)这个代码是可以正常运行的,因为B类型当中确实有fun(int x)这个函数,但是b.fun( )这个函数是在A类型当中的,在此情况下构成隐藏,所以调用不到A类型中的fun函数,除非使用域限定作用符。

4. 派生类的默认成员函数

4.1 4个常见默认成员函数

派生类的默认成员函数一共有6个,默认的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

  3. 派生类的 operator = 必须要调用基类的 operator = 完成基类的复制。需要注意的是派生类的 operator = 隐藏了基类的 operator=,所以显示调用基类的 operator=,需要指定基类作用域

  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

  5. 派生类对象初始化先调用基类构造再调派生类构造。

  6. 派生类对象析构清理先调用派生类析构再调基类的析构。

  7. 因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同 (这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成 destructor (),所以基类析构函数不加 virtual 的情况下,派生类析构函数和基类析构函数构成隐藏关系。

我们用一段代码来举例:

cpp 复制代码
class Person
{
public:
    Person(const char* name = "peter")
        : _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;
    }
protected:
    string _name; 
};

class Student : public Person
{
protected:
    int _num;
    string _address;
};

这里给出了派生类和基类,在基类的成员函数当中都加了一个对应特征的打印语句,这是为了便利我们在接下来的测试中,更直观的能看到都调用了哪些语句。

我们首先要知道,因为派生类是以public的方式继承的基类,所以在派生类当中的成员函数可以分为两类:1. 基类成员;2. 派生类自身成员。而且对于不同类的成员,派生类有不同的处理模式,对于基类成员,在使用时调用的是基类的默认构造函数,而对于自身成员调用的就是自身的构造函数。

在这张图中大家可以看到,我先实例化了一个Student类型的对象s1,然后通过监视窗口观察,会发现对于基类的成员,确实是调用了基类中的默认构造函数。

但是当我把基类中的默认构造函数删除时,就会引发下列报错:

这就进一步的说明了:派生类中的基类成员,是调用了基类中的默认构造函数。

那么我们能不能用Student中的默认构造函数去初始化基类成员呢?比如这样:

大家会发现这样还是会报错,并且报错的内容是:类"Person"不存在默认构造函数。这是怎么回事呢?这其实是因为,在派生类当中,对于基类的默认成员,必须将其当作一个整体,统一使用基类的默认构造函数,不能单独初始化。所以只能写成这样:

大家要记住这个用法。

再来看一个例子:比如派生类需要拷贝基类成员的时候,也会调用基类的拷贝构造函数:

就像这样,张三这个名字属于基类中的成员变量_name,所以再进行拷贝的时候,会调用基类中的拷贝构造函数。那 18 这个数字属于派生类中的变量 _num ,就会调用编译器自动生成的拷贝构造函数。地球村这个地址也是属于派生类中的变量 _address ,但因为类型是string,所以直接调用string的拷贝构造函数。

那我们能不能写一个Student的拷贝构造函数呢?就像这样:

大家要注意的是Person(s)这一行代码,这里其实调用的就是基类person中的拷贝构造函数:

在这里基类拷贝构造函数中的引用,实际上就是引用的派生类成员中的基类的部分。这里之所以能这样写,用到的就是我们在前面提到的知识:在 public 继承中 ,就是一个特殊处理的例外,派生类对象可以赋值给基类的指针 / 基类的引用,而不需要加 const,这里的指针和引用绑定是派生类对象中的基类部分。

接下来再看派生类的赋值重载,这其实主要应对的是深拷贝的场景,否则编译器默认生成的赋值重载函数就够用了:

我们需要注意的是赋值重载函数中的Person :: operator=(s)这一行代码,实际上调用的是基类person中的赋值重载函数:

另外还有一个析构函数,我们直接展示它的写法:

这里为什么要这样写呢?还要加上一个类域,难道不是多此一举吗,~Person()这个函数多么有辨识度,怎么可能找不到呢?实际上,析构函数的名字会因为构成"重写",导致被处理成destructor,"重写"这个概念我们后续多态章节会继续讲到。所以这样的话,不管是~Person还是~Student,都会被处理成destructor,从而进一步导致隐藏的问题,所以这里需要指定类域。

并且需要注意的是:派生类的析构调用后,会自动调用基类的析构函数,所以派生类自身并不需要显式调用析构函数。这是C++的一个对于派生类和基类中析构函数的调用机制。如果你显式调用,就会引发多次析构的问题:

比如在这里,明明只有两个对象但是却调用了四次析构函数,按理来说只需要调用两次。这就是因为当显式又写了Person::~Person( )时,会先进行了一次主动~Student()函数,调用~Person()析构。然后再默认走一次基类中的~Person()。这是因为在构造初始化的时候,是依照先父后子的顺序进行构造,那么在清理资源的时候,就要按照先子后父的顺序。

4.2 实现一个不能被继承的类

我们之前提到过,派生类不能直接单的初始化基类的成员,派生类必须显式调用基类的构造函数,那么如果此时我把基类的构造函数私有化,导致派生类无法调用基类的构造函数,那派生类就无法完成初始化这个操作,如果连初始化都完成不了,那派生类也就没有用处了。

所以第一种方法是用了两个互斥的语法,达到了这个目的。第一个语法就是:私有类成员在派生类中不可见。第二个语法就是:派生类不能单独初始化基类成员变量,必须调用基类的构造函数。接下来我们用一个样例:

在此处我们把Base这个基类的构造函数用private限定,那在Derive中就是不可见的,所以当我在主函数中实例化对象的时候,就无法达到这个目的,并且会报一个无法引用"Derive"的默认构造函数的错误:

通过这个办法,就能让Base这个类变成一个不能被继承的类。

第二个方法:在C++11当中,新增了一个final关键字,final修改基类,这样基类就会被修饰成"最终类",此时派生类就不能继承了。

这样的方法就更加直观。

5. 继承和友元

友元关系不能继承,也就是说基类的友元不能访问派生类私有和保护成员 。我们用一个例子来更直观的理解:

基类 想象成你爸妈的家,派生类是你自己的家。

1.基类的友元就像是你爸妈的好朋友,他们可以自由进出你爸妈的家(访问基类的私有 / 保护成员)。

  1. 但你爸妈的朋友,并不是你家的朋友。他们没有你家的钥匙,不能随便进你的房间(访问派生类的私有 / 保护成员)。

  2. 反过来,你作为子女(派生类)可以继承爸妈家的东西,但你不能继承爸妈的朋友关系 ------ 你爸妈的朋友不会因为你是他们的孩子,就自动成为你的朋友。

所以:

1. 友元关系不能被继承:基类的友元只对基类有访问权限,不能自动访问派生类的私有 / 保护成员。

2. 友元的权限仅限基类:基类的友元可以访问基类的成员,但无法访问派生类新增的成员,哪怕派生类继承了基类。

**3. 派生类想给友元权限,得自己重新声明:**在派生类中单独声明友元。

我们用一段代码来更深入理解:

我们一点一点来进行解析:首先为什么要在开头写一个:class Student?这是因为在下面Person这个类中,友元函数Display里面有一个参数类型是Student。因为我们说编译器在查找某个东西的时候,遵循的是向上查找。所以如果不在前面加一个前置声明,那友元函数Display就不认识Student是什么。

然后,因为Display是Person的友元,所以Display函数中可以访问到Person的被保护的成员变量,但是却不能访问Student中被保护的成员变量,这就说明了:友元关系不能被继承。

如果我也想让Display函数能访问到Student里面的被保护的成员变量,就要手动的将Display设置为Student的友元:

6. 继承与静态成员

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

我们用一段代码进行举例:

在Person这个类当中有一个静态成员变量_count,然后用Student以public的方式继承Person。然后实例化两个类型的对象。这里的运行结果可以看到,非静态成员 _name 的地址是不一样的,说明派生类继承下来了,基类和派生类对象各有一份。但是Person类中的静态成员变量_count和Student继承到Person的静态成员变量_count是相同的地址。 并且当我们打印这两个类中的_count的值的时候,结果也是一样的。 说明派生类和基类共用同一份静态成员,并且公有的情况下,基类、派生类指定类域都可以访问静态成员。

7. 多继承及其菱形继承问题

7.1 继承模型

1. 单继承:一个派生类只有一个直接基类时,称这个继承关系为单继承。

大家最开始会有很多误区,首先就是会把这种认为是多继承:

但实际上这只是多个派生类继承了同一个基类而已,所以它还是单继承关系。

2. 多继承:一个派生类有两个或以上直接基类时,称这个继承关系为多继承。多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员放在最后面。

3. 菱形继承菱形继承是多继承的一种特殊情况,也被称为钻石继承。菱形继承的问题,从对象成员模型构造可以看出,它存在数据冗余和二义性的问题,例如在 Assistant 对象中,Person 成员会存在两份。支持多继承就一定会出现菱形继承的可能性,像 Java 就直接不支持多继承,以此规避这类问题,因此在实践中,我们也不建议设计出菱形继承这样的模型。

所谓的数据冗余是指会出现这样的情况:

比如像上图中,Student和Teacher类都继承了Person类,而Person类中有一个成员变量_name,这就会导致Student和Teacher类中各自都有一个继承于Person的_name,当Assistant再继承Student和Teacher类的时候,就会导致Assistant类中有两份string _name,但实际上只需要一个string _name就可以存储Student和Teacher中所需要的名字信息。

那所谓的二义性是指:

因为刚刚已经有了数据冗余的问题,导致Teacher和Student中都有_name 变量,那么当我实例化出来一个Assistant的一个对象 a ,想要去修改 a 中的_name 变量,此时编译器就不知道我们要修改的是哪个_name ,这就是_name有两个含义,一个是Teacher的_name,一个是Student的_name。

对于二义性的问题的第一个办法,就是去指定类域:

像这样,直接告诉编译器我们要修改的是哪个类中的_name。

第二种方法就是直接去解决数据冗余的问题,这是因为,我们从刚刚的例子中可以得知,二义性的问题之所以存在,是因为数据冗余的存在,所以如果想要彻底规避二义性,最好的办法就是避免数据冗余。那么为了解决数据冗余的问题,C++在杜撰的时候就衍生了一个虚继承的概念。

7.2 虚继承

虚继承(Virtual Inheritance)是 C++ 中专门用于解决菱形继承(钻石继承)带来的数据冗余访问二义性问题的一种继承方式。语法形式:在派生类继承基类时,在继承方式前添加 virtual 关键字,格式为:class 派生类名 : virtual 继承方式 基类名。

首先需要注意的是,是虚继承仅需在菱形继承的 "中间派生类" 继承公共基类时使用,最终派生类无需额外添加 virtual。也就是在这里添加:

并且我们看,在添加virtual关键字之前,创建出来的对象a,只有Student和Teacher类中的内容:

当添加关键字virtual之后,会发现Person类被单独提了出来,相当于让其变成一个独立的成员:

因此可以这样理解:虚继承会让所有虚继承该公共基类的中间派生类,共享同一份公共基类的实例,该实例被称为 "虚基类子对象",最终派生类中仅保留一份公共基类的成员副本。

因此数据冗余的问题就解决了,从而二义性的问题也解决了:

现在大家要看这一个问题:

像这样的继承关系是不是菱形继承呢?答案:是的。

判断是否为虚继承的特征,是要注意到底有没有数据冗余和二义性这两个问题。我们可以从图中看到B和C都从A继承,那么必然会涉及到数据冗余和二义性的问题。尽管它的继承关系不是一个标准的菱形,但依然是一个菱形继承,并且我们的虚继承关键字virtual要添加在B和C当中。

然后我们还需要看一个问题,我们需要用到下面这段代码:

cpp 复制代码
class Person
{
public:
    Person(const char* name)
        :_name(name)
    {
    }
    string _name; //姓名
};
class Student : virtual public Person
{
public:
    Student(const char* name, int num)
        :Person(name)
        , _num(num)
    {
    }
protected:
    int _num; //学号
};

class Teacher : virtual public Person
{
public:
    Teacher(const char* name, int id)
        :Person(name)
        , _id(id)
    {
    }
protected:
    int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
public:
    Assistant(const char* name1, const char* name2, const char* name3)
        :Person(name3)
        , Student(name1, 1)
        , Teacher(name2, 2)
    {}
protected:
    string _majorCourse; //主修课程 
};

int main()
{
    Assistant a("张三", "李四", "王五");
    return 0;
}

问题是:这里 a 对象中 _name 是 " 张三 ", " 李四 ", " 王五 " 中的哪一个?

答案是:王五。

有些同学很有可能会误会,觉得首先因为这是继承关系,从初始化列表的角度来说,先声明的变量先初始化,那么按照代码中的顺序,初始化顺序应该是Person>Student>Teacher,而因为在Student和Teacher中对于_name的初始化,都是调用的基类Person中的构造函数,所以最后一次修改是传给Teacher的哪个值,就应该选:李四。

但是这里有一个机制 ,正因为是:Student和Teacher中对于_name的初始化,都是调用的基类Person中的构造函数,所以在Assistant中,在初始化列表里面调用Person(name3)的时候,,虚基类,也就是Person,的构造函数由最终派生类,Assistant,直接调用,中间派生类Student和Teacher,对虚基类构造函数的调用会被编译器忽略。也就意味着Student(name1,1)和Teacher(name2,2)这两行代码中传的name1和name2都是不起效果的。

那既然如此,Student和Teacher中构造函数里的Person(_name)这一行代码是不是就不需要写了呢?也并不是,必须要写。因为对 "虚基类构造函数的调用会被编译器忽略" 这个机制只是出现在Student和Teacher同时为中间派生类的情况下。而Student和Teacher 作为独立的类,其构造函数必须满足 "基类初始化规则"------ 无论是否被用作虚继承,只要 Person 没有默认构造函数,Student和Teacher 的构造函数就必须在初始化列表显式调用。

7.3 IO库中的菱形虚拟继承

虽然我们说要避免使用菱形继承,但是在实际环境当中,菱形继承在特定情况下也是需要的,就比如上面的IO库中的iostream,它和ios、istream、ostream就构成了菱形继承。

8. 多继承中指针偏移问题

首先我们要知道:多继承的派生类对象在内存中是按「继承顺序」依次存放各个基类的成员,再存放自身成员的。比如 classC : public A , public B ,C 对象的内存布局是:[A的成员] -->[B的成员] -->[C的成员]。

而且我们在前面的基类和派生类之间的转换中提到过: " 在 public 继承中 ,就是一个特殊处理的例外,派生类对象可以赋值给基类的指针 / 基类的引用,而不需要加 const,这里的指针和引用绑定是:派生类对象中的基类部分。"

那也就导致了: 多继承中,不同基类的指针指向同一个派生类对象,地址可能不同,编译器会自动调整指针地址(偏移)以匹配对应基类的成员布局

用图片和代码来解释就像是这样:

现在有三个类,Base1、Base2、Derive。它们的继承关系是这样的:

那么它们在内存中的排布大概就是这样的:

现在有一串代码:

首先:Base1类型的指针p1、Base2类型的指针p2、Derive类型的指针p3,这三个指针同时都指向实例化出来的对象d的地址。那么首先对于Derive类型的指针p3来说,它肯定是指向对象d的首地址。对于Base1类型的指针p1来说,它指向的是Derive类型的对象d当中的Base1的部分,因为Base1在Derive类中是第一位,所以p1也是指向d的首地址,但是对于Base2类型的指针p2来说,它指向的是Derive类型的对象d当中的Base2的部分,因此就会做出指针偏移,用图片解释就是这样:

所以当我们遇到这道问题的时候:

我们的答案就是选:C。

9. 继承和组合

我们用一段代码来初步理解继承和组合:

首先:public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象。组合是一种 has-a 的关系。假设 B 组合了 A,每个 B 对象中都有一个 A 对象。就如上图所示。但不管是继承还是组合,本质上都是类成员的复用。

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用 (white-box reuse)。术语 "白箱" 是相对可视性而言:在继承方式中,基类的内部细节(如 public/protected 成员)对派生类可见且可直接访问,基类的封装性因此被一定程度破坏。基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用 (black-box reuse),因为对象的内部细节是不可见的。对象只以 "黑箱" 的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承 (is-a) 那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承 (is-a) 也适合组合 (has-a),就用组合。

本文到此结束,感谢各位读者的阅读,如果有讲解的不到位或者错误的地方,欢迎各位读者的批评或指正。

相关推荐
txinyu的博客2 小时前
解析muduo源码之 atomic.h
服务器·c++
GSDjisidi2 小时前
正社員・個人事業主歓迎|GSD東京本社で働こう|業界トップクラスの福利厚生完備
开发语言·面试·职场和发展
xiaoye-duck2 小时前
C++ string 类使用超全攻略(下):修改、查找、获取及常见实用接口深度解析
开发语言·c++·stl
程序员老舅2 小时前
【无标题】
c++·嵌入式·八股文·c++八股文·八股文面试题·c++面经·c++面试题
Tao____2 小时前
可以本地部署的物联网平台
java·开发语言·物联网·mqtt·低代码
码界奇点2 小时前
基于DDD与CQRS的Java企业级应用框架设计与实现
java·开发语言·c++·毕业设计·源代码管理
柏林以东_2 小时前
线程安全的数据集合
java·开发语言·安全
Frank_refuel2 小时前
C++STL之set和map的接口使用介绍
数据库·c++·算法
喵喵喵小鱼2 小时前
arcgis JavaScript api实现同时展示多个撒点气泡
开发语言·javascript·arcgis