前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >今天你学C++了吗?——C++中的继承

今天你学C++了吗?——C++中的继承

作者头像
用户11352420
发布于 2025-03-13 00:42:13
发布于 2025-03-13 00:42:13
10100
代码可运行
举报
文章被收录于专栏:编程学习编程学习
运行总次数:0
代码可运行

继承的概念

继承?什么是继承?在生活中,我们可以听到继承人这样的专有名词,那么C++中的继承是什么呢?我们来看看继承的概念~

继承是一种机制,它允许一个类(称为子类派生类)获取另一个类(称为基类父类)的属性和方法。这就像是在现实生活中,孩子会继承父母的某些特征和行为。

想象一下,我们想要描述一个包含多种动物的世界。在这个世界中,有狗、猫、鸟等多种动物。这些动物之间有一些共同的特征和行为,比如它们都有名字和年龄,它们都会吃和睡觉。但是,它们也有一些独特的特征和行为,比如狗会叫,鸟会飞~

为了简化描述并避免重复,我们可以引入一个概念叫做“继承”。

基类:动物(Animal)

首先,我们定义一个基类,叫做“动物”(Animal)。这个基类代表了所有动物共有的特征和行为。它包含两个基本的属性:名字(name)和年龄(age)。同时,它还定义了两个基本的行为:吃(eat)和睡觉(sleep)。

子类:狗(Dog)、鸟(Bird)

接着,我们定义几个子类,分别叫做“狗”(Dog)、“鸟”(Bird)。这些子类都是基类“动物”(Animal)的特例,也就是说,它们都是动物的一种。

  • (Dog):狗继承了动物的所有特征和行为,比如它有名字和年龄,它可以吃和睡觉。但是,狗还有一些独特的特征,比如一个较为独特的行为——叫(bark)。
  • (Bird):鸟也继承了动物的特征和行为。它有名字、年龄,可以吃和睡觉。但是,鸟有一个非常独特的行为——飞(fly)。当然,并不是所有的鸟都会飞,但在这个简化的例子中,我们假设鸟会飞。

通过继承,我们可以很容易地描述不同种类的动物,同时避免重复描述它们共有的特征和行为。这样,我们的描述更加简洁、清晰,也更容易理解和维护。

接下来我们使用代码进行实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include<iostream>
using namespace std;
//基类Animal
class Animal
{
public:
    string _name;
    int _age;

    void eat() {
        cout << _name << " is eating." << endl;
    }

    void sleep() {
        cout << _name << " is sleeping." << endl;
    }
};

//子类
class Dog :public Animal
{
public:
    void bark() 
    {
        cout << _name << " is barking." << endl;
    }
};

class Bird : public Animal {
public:
    void fly()
    {
        cout << _name << " is flying." << endl;
    }
};

在这个程序中,通过继承,可以很容易地扩展类体系,添加新的动物类型,而不需要重复编写已经存在的代码。继承使得代码更加模块化和易于维护,同时也提高了代码的可复用性。继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段~

继承的定义

在上面的代码,可能大家对继承的定义还不太清楚,我们接下来就来掀开它的神秘面纱~

上面的代码定义了一个简单的类继承体系,其中包含一个基类(或者叫父类)Animal和两个派生类(或者叫子类)Dog(狗)和Bird(鸟),每个类都有其特定的属性和方法~

继承方式和访问限定符都有三种:

不同的继承方式和不同的访问限定符组合决定了派生类(子类)如何访问基类(父类)的成员(属性和方法),我们来看看下面这张图~

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的“不可见”是指,虽然基类的私有成员被继承到了派生类对象中,但语法上限制了派生类对象(无论是在类内部还是类外部)都不能直接访问它。
  2. 基类private成员在派⽣类中是不能被访问的。如果基类成员不希望被类外部直接访问,但需要在派生类中能够访问,那么应该将其定义为protected。保护成员限定符(protected)的出现正是为了解决这类继承中的访问控制问题。(总结:基类私有成员不能直接访问不是没有被继承,而是权限问题)
  3. 通过分析访问权限表格,我们可以总结出:基类的私有成员在派生类中都是不可见的。而基类的其他成员在派生类中的访问方式取决于成员在基类中的访问限定符和继承方式的最小值(Min),其中public的访问权限最高,其次是protected,最后是private~(public>protected>private)
  4. 当使用关键字class进行继承时,默认的继承方式是private;而当使用struct进行继承时,默认的继承方式是public。为了避免混淆和潜在的错误,最好明确指定继承方式。
  5. 在实际应用中,通常使用的是public继承,而protected/private继承的使用相对较少,且一般不提倡使用。因为通过protected/private继承的成员只能在派生类的内部使用,这可能会降低代码的扩展性和维护性。

接下来我们来进行简单的使用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//基类Animal
class Animal
{
public:
    string _name;
    int _age;

    void eat() {
        cout << _name << " is eating." << endl;
    }

    void sleep() {
        cout << _name << " is sleeping." << endl;
    }
};

//子类
class Dog : Animal//class关键字,继承方式为private
{
public:
    void bark()
    {
        cout << _name << " is barking." << endl;
    }
};

struct Bird : Animal //struct关键字,继承方式为public
{
public:
    void fly()
    {
        cout << _name << " is flying." << endl;
    }
};
int main()
{
    Dog d;
    d._name = "DD";//err
    d._age = 6;//err
    d.eat();//err 
    //class 关键字,不显示写继承方式默认为private
    //除基类的私有成员外,其他成员在派生类中的访问方式
    //取决于成员在基类中的访问限定符和继承方式的最小值(Min),这里是private
    d.bark();
    Bird b;
    b._name = "BB";
    b._age = 3;
    b.eat();
    b.fly();
    return 0;
}

基类和派生类间的转换

在面向对象编程中,关于继承与多态性,我们常遇到这样的情境:

一个由public继承而来的派生类对象,能够轻松地赋值给一个基类类型的指针或引用。这一操作,形象地被称为“切片”或“切割”。其寓意在于,当我们从派生类中“切割”出基类部分时,基类指针或引用便指向了这块被“切割”出来的基类“片段”

然而,事情并非总是双向的:

我们无法将一个基类对象直接赋值给一个派生类对象,这在逻辑上是不允许的。

但转换思维,我们或许可以尝试另一种方式:

• 通过强制类型转换,基类指针或引用可以被赋予派生类指针或引用的“外衣”。但这样的操作隐藏着风险,唯有当基类指针确实指向一个派生类对象时,这样的转换才是安全的。幸运的是,当基类具备多态性时,我们可以借助RTTI(Run-Time Type Information,即运行时类型信息)中的dynamic_cast工具,进行类型识别,从而确保转换的安全性。(小贴士:关于dynamic_cast的深入讲解,我们将留待后续的类型转换章节博客中详细展开,此处仅作简要提及)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//基类Animal
class Animal
{
public:
    string _name;
    int _age;

    void eat() {
        cout << _name << " is eating." << endl;
    }

    void sleep() {
        cout << _name << " is sleeping." << endl;
    }
};

//子类
class Dog : public Animal
{
public:
    void bark()
    {
        cout << _name << " is barking." << endl;
    }
};

struct Bird : public Animal 
{
public:
    void fly()
    {
        cout << _name << " is flying." << endl;
    }
};
int main()
{
    Dog d;
    d._name = "DD";
    d._age = 6;
    d.eat();
    d.bark();
    Bird b;
    b._name = "BB";
    b._age = 3;
    b.eat();
    b.fly();

    //1.派生类对象可以赋值给基类
    Animal a1;
    a1 = d;
    a1.eat();
   // a1.bark();//err 父类没有这个成员函数
    Animal a2;
    a2 = b;
    a2.eat();
   // a2.fly();//err 父类没有这个成员函数
    //2.派生类对象可以赋值给基类的指针/引用
    Animal* aa1 = &d;
    Animal& aa2 = d;
    Dog* dp = &d;
    cout << aa1 << endl;
    cout << dp << endl;//这也就是同一个地址

    //3.基类对象不可以赋值给派生类
   // d = a1;//这里编译报错
    return 0;
}

继承中的作用域

我们首先来看看隐藏规则

1、在继承体系中,基类和派生类各自拥有独立的作用域。 2、当派生类与基类中存在同名成员时,派生类的成员会屏蔽基类中对同名成员的直接访问。这种情况被称为隐藏。 (在派生类的成员函数中,可以通过使用“基类::基类成员”的方式来显式访问被隐藏的基类成员。) 3、需要特别注意的是,如果发生的是成员函数的隐藏,那么只要函数名相同(也就是说参数即使不同),就构成了隐藏。 4、在实际应用中,为了避免潜在的混淆和错误,建议在继承体系中尽量避免定义同名的成员。 5、与我们的函数重载进行区分,一个类中函数名相同,但是函数参数类型或者个数不同就构成函数重载,而函数隐藏只要函数名相同就构成~

接下来,看看实际的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//基类Animal
class Animal
{
public:
    string _name;
    int _age;

    void eat()
    {
        cout << " Animal " << _name << " is eating." << endl;
    }

    void sleep() 
    {
        cout << " Animal " << _name << " is sleeping." << endl;
    }
};

//子类
class Dog : public Animal
{
public:
    void bark()
    {
        cout << " Dog " << _name << " is barking." << endl;
    }

    void bark(int a)
    {
        cout << a << " Dog " << _name << " is barking." << endl;
    }
    //一个类中函数名相同,但是函数参数类型或者个数不同就构成函数重载

    //隐藏只要函数名相同就构成
    void eat()
    {
        cout << " Dog " << _name << " is eating." << endl;
    }
};

struct Bird : public Animal 
{
public:
    void fly()
    {
        cout << " Bird " << _name << " is flying." << endl;
    }
};

int main()
{
    Dog d;
    d._name = "DD";
    d._age = 6;
    d.bark();
    d.bark(3);//函数重载

    d.eat();//函数隐藏——调用派生类,而不是基类
    d.Animal::eat();//使用“基类::基类成员”的方式来显式访问被隐藏的基类成员
    d.sleep();//调用基类的
    return 0;
}

接下来,我们来看看两道有趣的题目:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class A
{
public:
void func()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void func(int i)
{
cout << "func(int i)" <<i<<endl;
}
};
int main()
{
B b;
b.func(10);
b.func();
return 0;
};

1》上面A和B类中的两个func构成什么关系()

A. 重载 B. 隐藏 C.没关系

2》 下面程序的编译运行结果是什么()

A. 编译报错 B. 运行报错 C. 正常运行

答案揭晓:

正确答案:B A 解析:事实上,func函数名相同就构成了隐藏,所以第一题选择B 而构成隐藏就只能直接访问子类的成员函数func(int a),但是子类的成员函数func(int a)必须有参数才可以正常调用,所以这里b.func()编译就会出问题,第二题答案选择A

派生类的默认成员函数

在C++中,若我们未显式定义,编译器会自动为类生成以下六个默认成员函数。那么,在派生类中,这些成员函数是如何生成和工作的呢(这里主要讲解四个常用默认成员函数)?

  1. 构造函数:派生类的构造函数在创建对象时,必须调用基类的构造函数来初始化基类成员。若基类没有提供默认构造函数(即无参构造函数),则派生类构造函数需在初始化列表中显式调用基类的某个构造函数。
  2. 拷贝构造函数:派生类的拷贝构造函数在复制对象时,会隐式调用基类的拷贝构造函数来完成基类成员的复制初始化。这是为了确保基类部分被正确复制。
  3. 赋值运算符(operator=:派生类的赋值运算符在对象赋值时,必须调用基类的赋值运算符来完成基类成员的更新。值得注意的是,派生类的赋值运算符会隐藏基类的赋值运算符,因此调用基类赋值运算符时,需显式指定基类作用域(例如,BaseClass::operator=(other);)。
  4. 析构函数:派生类的析构函数在对象销毁时,会自动调用基类的析构函数来清理基类成员。这是为了确保派生类对象在析构时,先清理派生类成员,再清理基类成员,从而保持正确的资源释放顺序。
  5. 对象初始化顺序:在创建派生类对象时,首先会调用基类的构造函数来初始化基类部分,随后才会调用派生类的构造函数来初始化派生类部分(总结:先初始化基类,再初始化派生类
  6. 对象析构顺序:在销毁派生类对象时,首先会调用派生类的析构函数来清理派生类部分,然后才会调用基类的析构函数来清理基类部分(总结:先析构派生类,再析构基类
  7. 析构函数与多态:在多态场景中,若基类的析构函数未声明为虚函数,则派生类的析构函数与基类的析构函数之间会构成隐藏关系,而非重写关系。(这可能导致通过基类指针删除派生类对象时,仅调用基类的析构函数,从而引发资源泄露或未定义行为。)因此,在多态场景中,通常建议将基类的析构函数声明为虚函数。(后面使用到会具体讲解)

我们首先来看看构造和析构:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//基类Animal
class Animal
{
public:
    string _name;
    int _age;
    //基类构造函数
    Animal(string name, int age):_name(name),_age(age)
    {
        cout << "基类Animal构造" << endl;
    }
    void eat()
    {
        cout << " Animal " << _name << " is eating." << endl;
    }

    void sleep() 
    {
        cout << " Animal " << _name << " is sleeping." << endl;
    }
    //基类析构函数
    ~Animal()
    {
        cout << "~Animal" << endl;
    }
};

//派生类
class Dog : public Animal
{
public:
    int _id;
    //派生类构造函数
    //Dog(string name,int age,int id):_name(name),_age(age),_id(id)//err 这种写法是错误的
   //基类没有提供默认构造函数(即无参构造函数),则派生类构造函数需在初始化列表中显式调用基类的某个构造函数
    //正确写法
    Dog(string name, int age, int id) :Animal(name, age), _id(id)
    {
        cout << "派生类Dog构造" << endl;
    }
    void bark()
    {
        cout << " Dog " << _name << " is barking." << endl;
    }
    //派生类析构函数
    ~Dog()
    {
        cout << "~Dog()" << endl;
    }
};

class Bird : public Animal 
{
public:
    void fly()
    {
        cout << " Bird " << _name << " is flying." << endl;
    }
};

int main()
{
    Dog d("DD", 6, 1);
    //先初始化基类,再初始化派生类
    //先析构派生类,再析构基类
    return 0;
}

接下来,看看拷贝构造和赋值运算符重载:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include<iostream>
using namespace std;
//基类Animal
class Animal
{
public:
    string _name;
    int _age;
    //基类构造函数
    Animal(string name, int age):_name(name),_age(age)
    {
        cout << "基类Animal构造" << endl;
    }
    //基类拷贝构造
    Animal(const Animal& a):_name(a._name),_age(a._age)
    {
        cout << "Animal拷贝构造" << endl;
    }
    //基类赋值运算符重载
    Animal& operator=(const Animal& a)
    {
       
        //不是本身才进行赋值
        if (this != &a)
        {
            _name = a._name;
            _age = a._age;
        }
        cout << "Animal 赋值运算符重载" << endl;
        return *this;
    }
    void eat()
    {
        cout << " Animal " << _name << " is eating." << endl;
    }

    void sleep() 
    {
        cout << " Animal " << _name << " is sleeping." << endl;
    }
    //基类析构函数
    ~Animal()
    {
        cout << "~Animal" << endl;
    }
};

//派生类
class Dog : public Animal
{
public:
    int _id;
    //派生类构造函数
    //Dog(string name,int age,int id):_name(name),_age(age),_id(id)//err 这种写法是错误的
   //基类没有提供默认构造函数(即无参构造函数),则派生类构造函数需在初始化列表中显式调用基类的某个构造函数
    //正确写法
    Dog(string name, int age, int id) :Animal(name, age), _id(id)
    {
        cout << "派生类Dog构造" << endl;
    }

    //派生类拷贝构造
    //这里派生类和基类之间的转换就发生了大作用
    Dog(const Dog& d) :Animal(d), _id(d._id)
    {
        cout << "Dog 拷贝构造" << endl;
    }
    //派生类赋值运算符重载
    Dog& operator=(const Dog& d)
    {
        //派生类的赋值运算符在对象赋值时,必须调用基类的赋值运算符来完成基类成员的更新
       // 派生类的赋值运算符会隐藏基类的赋值运算符
       // 因此调用基类赋值运算符时,需显式指定基类作用域
        if (this != &d)
        {
            Animal::operator=(d);//显式指定基类作用域
            _id = d._id;
        }
        cout << "Dog 赋值运算符重载" << endl;
        return *this;
    }
    void bark()
    {
        cout << " Dog " << _name << " is barking." << endl;
    }
    //派生类析构函数
    ~Dog()
    {
        cout << "~Dog()" << endl;
    }
};



int main()
{
    Dog d1("DD", 6, 1);
    //先初始化基类,再初始化派生类
    Dog d2 = d1;//调用拷贝构造
    Dog d3("DD3", 8, 2);
    d3 = d1;
    //先析构派生类,再析构基类
    return 0;
}

实现一个不能被继承的类

在C++里,有时候我们不想让别人基于我们的类创建新的子类,这就像我们不希望别人随意改动我们设计好的玩具一样。为了实现这个目的,我们可以把类设置为“最终类”,也就是不能被继承的类。

怎么做呢?

方法1:将基类的构造函数设为私有,本意是防止外部创建对象,但并非有效阻止继承的手段。因为派生类在构造时仍需调用基类构造函数,私有构造函数会导致编译错误,且这种错误发生在尝试实例化派生类时,而非继承时。此外,该方法未能明确表达“不可继承”的意图。(这个方法事实上卡了语法的Bug) 方法2:C++11引入的final关键字,是专为阻止类被继承而设计的声明为final的类无法被其他类继承,编译器将直接报错。此方法既清晰又有效,直接解决了类不应被继承的问题,且没有引入不必要的复杂性或潜在错误。因此,推荐使用final关键字来实现不可继承的类。

为什么要这么做呢?

主要是为了保护我们的设计,防止别人不小心或者故意地破坏它。这样,我们就可以确保我们的类按照我们预期的方式工作,不会出现意外的行为或者错误。

使用举例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//实现一个不能被继承的类
// C++11的方法——声明为final的类无法被其他类继承,编译器将直接报错
class Base final
{
public:
	void func1() { cout << "Base::func1" << endl; }
protected:
	int a = 1;
private:
	// C++98的方法——构造函数私有
	/*Base()
	{}*/
};
class Derive :public Base
{
	void func2() { cout << "Derive::func2" << endl; }
protected:
	int b = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

继承与友元

友元关系在C++中不遵循继承规则,即基类的友元不能自动访问派生类的私有和保护成员。即使基类与某类建立了友元关系,该关系也不会传递给基类的派生类。因此,派生类的成员访问权限对基类的友元类是受限的,如果我们想解决这个问题,可以让友元关系在派生类也是存在的~

例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include<iostream>
using namespace std;
class B;//前向声明B类,说明有这个类
class A
{
public:
	friend void show(const A& a, const B& b);
	A(int a) :_a(a)
	{

	}
protected:
	int _a;
};
class B :public A
{
public:
	friend void show(const A& a, const B& b);
	//让友元关系在派生类也存在
	B(int a, int b) :A(a), _b(b)
	{

	}
protected:
	int _b;
};
void show(const A& a, const B& b) 
{
	cout << "show()" << endl;
	cout << a._a << endl;
	cout << b._b << endl;//基类的友元不能自动访问派生类的私有和保护成员
	//解决方法:让友元关系在派生类也是存在的
}

int main()
{

	B b(1, 2);
	show(b, b);
	return 0;
}

继承与静态成员

在面向对象编程中,若基类定义了一个静态成员,那么在整个继承体系中,这个静态成员将只有一个唯一的实例存在,无论从这个基类派生出多少个子类,这些子类都将共享这个唯一的静态成员实例

例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Base 
{
public:
    int _a;
    static int _count;//基类有一个静态成员变量
};

int Base::_count = 0; // 静态成员变量的定义和初始化

class Derived1 : public Base 
{

};
class Derived2 : public Base 
{

};

int main() {
    Base b1, b2;
    Derived1 d1;
    Derived2 d2;

    b1._count++; // 访问并修改静态成员
    d1._count++; // 同样访问的是Base::count,因为静态成员在继承体系中是共享的

    std::cout << "Base count: " << b1._count << std::endl; // 输出2
    std::cout << "Base count: " << b2._count << std::endl; // 输出2
    std::cout << "Derived1 count: " << d1._count << std::endl; // 输出2,因为访问的是同一个静态成员
    std::cout << "Derived2 count: " << d1._count << std::endl; // 输出2,因为访问的是同一个静态成员
    //证明:打印地址是一样的
    //静态成员在继承体系中是共享的
    cout << &b1._count << endl;
    cout << &b2._count << endl;
    cout << &d1._count << endl;
    cout << &d2._count << endl << endl;
    //非静态成员在继承体系中不共享同一份,地址不一样
    cout << &b1._a << endl;
    cout << &d1._a << endl;
    return 0;
}

多继承及其菱形继承问题

继承模型

单继承

定义:当一个派生类只有一个直接基类时,这种继承关系被称为单继承。

特点

  • 继承关系简单明了。
  • 派生类可以访问基类的公有和保护成员(取决于访问权限)。
  • 易于理解和实现。

例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Base 
{
public:
    int _a;
};

class Derived1 : public Base //只有一个基类
{

};
多继承

定义:当一个派生类有两个或更多直接基类时,这种继承关系被称为多继承。

特点

  • 派生类可以继承多个基类的功能。
  • 内存中的对象模型通常是按照基类继承的顺序排列的,即先继承的基类在前,后继承的基类在后,派生类成员放在最后。
  • 可能存在二义性问题,如果多个基类中有同名的成员。
  • 复杂度高,可能导致代码难以维护和理解。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Base1
{
public:
    int _a;
};

class Base2
{
public:
    int _b;
};

class Derived1 : public Base1,public Base2//一个派生类有两个或更多直接基类——多继承
{

};
class Derived2 : public Base1//单继承
{

};

内存中的模型在多继承中,对象的内存布局通常按照基类继承的顺序来排列。这意味着,如果派生类从A和B两个基类继承,且A在B之前被继承,那么对象在内存中的布局将首先是A基类的成员,然后是B基类的成员,最后是派生类自己的成员。

多继承中指针偏移问题?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}

结合上面的代码,下面说法正确的是( )

A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

正确答案:C

解析: 这个问题就与我们提到的“切割”有关系,同时我们需要考虑内存模型。Base1和Base2虽然都是Derive父类,但在子类内存模型中,其位置不同,所以p1和p2所指子类的位置也不相同,因此p1!=p2。 由于Base1对象是第一个被继承的父类类型,所有其地址与子类对象的地址Derive所指位置都为子类对象的起始位置,因此p1==p3,所以C正确 画图理解:

举一反三:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include<iostream>
#include<vector>
using namespace std;
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base2, public Base1 { public: int _d; };
int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	cout << p1 << endl;
	cout << p2 << endl;
	cout << p3 << endl;
	return 0;
}

结合上面的代码,下面说法正确的是( )

A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 == p3

正确答案:D 这一段代码由于Base2对象是第一个被继承的父类类型,所有其地址与子类对象的地址Derive所指位置都为子类对象的起始位置,因此p2==p3,所以C正确 所以我们需要注意子类继承父类的顺序

菱形继承(也称为钻石继承)

定义:菱形继承是多继承的一种特殊情况,其中两个基类都从同一个公共基类继承,而这两个基类又共同被一个派生类继承。

特点

  • 存在数据冗余问题,因为公共基类的成员在派生类中会有两份拷贝(一份来自每个直接基类)。
  • 存在二义性问题,因为派生类可能通过不同的路径访问到公共基类的同名成员。
  • 增加了代码的复杂性和维护难度。

问题: 菱形继承的问题主要体现在数据冗余和二义性上。数据冗余是因为公共基类的成员在派生类中会有多份拷贝,这可能导致不必要的内存开销和潜在的数据不一致问题。二义性则是因为派生类可能通过不同的基类路径访问到公共基类的同名成员,这可能导致编译错误或运行时错误。

比如我们来看看下面的代码就是一个菱形继承,存在二义性和数据冗余的问题!我们可以解决二义性问题,但是数据冗余解决不了

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//菱形继承
class A
{
public:
	int _a;
};
class B :public A
{
public:
	int _b;
};
class C :public A
{
public:
	int _c;
};
class D :public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	//d._a = 2;//err D::_a不明确 —— 存在二义性
	//解决方法——显示指定访问哪一个基类,但是数据冗余问题无法解决
	d.B::_a = 2;
	d.C::_a = 3;
	//在两个类分别拷贝了一次,依然存在数据冗余
	cout << d.B::_a << endl;
	cout << d.C::_a << endl;
	return 0;
}

上面的代码,类A派生出类B和类C,类D继承自类B和类C。此时,类A中的成员变量和成员函数在类D中就会存在两份拷贝,一份来自A→B→D路径,另一份来自A→C→D路径。

画图理解:

解决方案: 为了避免菱形继承的问题,一些编程语言(如Java)直接不支持多继承。在其他支持多继承的语言中(如C++),可以通过虚继承(virtual inheritance)来解决菱形继承带来的二义性问题。虚继承会确保公共基类在派生类中只有一份拷贝,从而避免了数据冗余和二义性

实践建议: 尽管多继承在某些情况下可能提供方便,但由于其复杂性和潜在的问题(特别是菱形继承),在实践中通常建议避免使用多继承。相反,可以通过组合(composition)或接口(interface)来实现类似的功能,这些技术通常更加灵活且易于维护。

虚继承

定义:虚继承是C++中一种特殊的继承方式,用于解决多重继承中的菱形继承问题。

问题背景:在菱形继承结构中,一个基类通过多个路径被同一个派生类继承,可能导致基类成员在派生类中存在多个副本,引发数据冗余和二义性。

解决方案:通过虚继承,无论通过多少条路径,确保派生类只继承基类的一个副本。

语法:在继承声明中使用virtual关键字。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//虚继承
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._a = 1;//解决了二义性和数据冗余问题
	d.B::_a = 2;
	d.C::_a = 3;
	//通过虚继承,无论通过多少条路径,确保派生类只继承基类的一个副本
	cout << d._a << endl;
	cout << d.C::_a << endl;
	return 0;
}

优点

  • 解决菱形继承问题。
  • 避免基类成员的多个副本。

缺点

  • 可能增加运行时开销(由于vtable和vptr的使用)。
  • 语法相对复杂。

总之,C++中的虚继承是一种用于处理多重继承中菱形继承问题的有效机制,它通过确保基类只有一个副本来避免数据冗余和二义性。

接下来思考,如果继承关系像下面这个样子,这是不是菱形继承呢?如果是,我们怎么使用虚继承解决这个问题呢?

答案是这也是菱形继承,因为B和C中分别拷贝了一份A类,也就造成了二义性和数据冗余,我们只需要将B和C使用关键字设置为虚继承就可以很好的解决这个问题~

继承和组合

  • public继承:你可以把public继承想象成“就是”(is-a)的关系。这意味着,每个派生类的对象其实就是一个基类对象的特殊版本。比如说,如果有一个“动物”基类,而“狗”是“动物”的一个派生类,那么每条狗都可以被认为是一个动物。
  • 组合:组合更像是“有”(has-a)的关系。如果B类组合了A类,你可以理解为每个B类对象里都装着一个A类对象。比如,你有一个“汽车”类,里面装了一个“发动机”对象,这就是组合。
  • 继承:通过继承,你可以利用基类的代码来实现派生类。这种方式就像是直接复用基类的内部实现,所以叫做“白箱复用”。但这样做也有个缺点,就是基类的内部细节对派生类来说是可见的,这可能会破坏基类的封装性,如果基类有什么变动,派生类很可能会受到影响。
  • 对象组合:除了继承,你还可以通过组合对象来实现新的功能。这种方式更像是把几个已有的对象组合起来,形成一个新的对象。这种方式叫做“黑箱复用”,因为被组合的对象的内部细节是不可见的,你只关心它的接口。这样,组合类之间的依赖关系就不那么紧密了,维护起来也更容易。
  • 优先使用组合:在编程时,我们通常建议优先使用组合而不是继承。因为组合的耦合度低,代码更容易维护。当然,这并不是绝对的。如果类之间的关系真的符合“就是”的关系,或者你需要实现多态,那么继承还是必要的。但如果类之间的关系既可以用继承也可以用组合,那么通常建议使用组合。

组合例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 Tire(轮胎)和Car()更符合has-a的关系
class Tire
{
protected:
	string _brand = "MM"; // 品牌
	size_t _size = 17; // 尺⼨
};
class Car 
{
protected:
	string _colour = "黑色"; //颜色
	Tire _t1; // 轮胎
	Tire _t2; // 轮胎
	Tire _t3; // 轮胎
	Tire _t4; // 轮胎
};

继承类模板

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include<iostream>
#include<vector>
using namespace std;
namespace xiaodu
{
	//template<class T>
	//class vector
	//{ };
	// stack和vector的关系,既符合is-a,也符合has-a
	template<class T>
	class stack : public std::vector<T>//继承类模板
	{
	public:
		void push(const T& x)
		{
			// 基类是类模板时,需要指定⼀下类域
			// 否则编译报错:error C3861: “push_back”: 找不到标识符
			// 因为stack<int>实例化时,也实例化vector<int>了——继承关系
// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
			vector<T>::push_back(x);//指定类域
			//push_back(x);
		}
		void pop()
		{
			vector<T>::pop_back();
		}
		const T& top()
		{
			return vector<T>::back();
		}
		bool empty()
		{
			return vector<T>::empty();
		}
	};
}
int main()
{
	xiaodu::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	return 0;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 继承的概念
  • 继承的定义
  • 基类和派生类间的转换
  • 继承中的作用域
  • 派生类的默认成员函数
  • 实现一个不能被继承的类
  • 继承与友元
  • 继承与静态成员
  • 多继承及其菱形继承问题
    • 单继承
    • 多继承
    • 菱形继承(也称为钻石继承)
  • 虚继承
  • 继承和组合
  • 继承类模板
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文