【C++ 继承】—— 青花分水、和而不同,继承中的“明明德”与“止于至善”

news2025/3/27 23:55:12

欢迎来到ZyyOvO的博客✨,一个关于探索技术的角落,记录学习的点滴📖,分享实用的技巧🛠️,偶尔还有一些奇思妙想💡
本文由ZyyOvO原创✍️,感谢支持❤️!请尊重原创📩!欢迎评论区留言交流🌟
个人主页 👉 ZyyOvO
本文专栏➡️C++ 进阶之路

在这里插入图片描述

继承中的“明明德”与“止于至善”

  • 继承
    • 继承的概念
    • 基本语法
    • 继承类模板
  • 基类和派生类的转换
    • 内存布局与继承的关系
    • 向上转型
    • 向下转型
  • 继承中的作用域
    • 作用域嵌套规则
    • 隐藏规则
    • 多层继承的作用域链
  • 派生类的默认成员函数
    • 默认构造函数
    • 拷贝构造函数
    • 拷贝赋值运算符
    • 析构函数
  • 继承和友元
  • 继承和静态成员
    • 静态成员的可见性
    • 静态数据成员的初始化
    • 静态成员函数与多态(TODO)
    • 同名静态成员的隐藏
  • 多继承及其菱形继承
    • 单继承
    • 多继承
    • 菱形继承
    • I0库中的菱形虚拟继承
  • 继承和组合
  • 思考题
  • 写在最后

继承

继承的概念

继承是面向对象编程(OOP)中的一个核心概念,它允许一个类(派生类或子类)继承另一个类(基类或父类)的属性和方法。
继承的核心思想是代码复用和创建类之间的层次关系。通过继承,我们可以定义一个通用的基类,包含一些通用的属性和方法,然后派生出更具体的子类,这些子类可以继承基类的特性,并添加自己独特的属性和方法。

下面我们举一个例子来帮助大家理解:

想象一下你正在管理一个动物园,里面有各种各样的动物。这些动物有一些共同的特征和行为,比如它们都需要吃东西、睡觉,同时不同种类的动物又有各自独特的行为,像鸟儿会飞翔,鱼儿会游泳。

为了更有条理地管理这些动物信息,我们可以先把动物们的共同特征和行为总结出来,形成一个通用的描述,然后再针对每种动物的独特之处进行单独描述。

  • 这个通用的描述包含所有动物的共同特点和行为,用一个类 Animal 来实现,我们把这个类成为 基类父类
class Animal {
public:
    Animal(const std::string& n) : name(n) {}
    void eat() {
        std::cout << name << " is eating." << std::endl;
    }
    void sleep() {
        std::cout << name << " is sleeping." << std::endl;
    }
private:
    std::string name;
};

  • 鸟类是一种动物,包含了动物的所有公共特点,所以我们可以用一个类 Brid 来实现对 Animal 类的继承,这个 Brid 类就叫做 派生类 或者 子类,代表是在基类的基础上继承而来的,同时也可以包含鸟类独有的特点和行为,比如翅膀,飞翔。
class Bird : public Animal {
public:
    Bird(const std::string& n) : Animal(n) {}
    void fly() {
        std::cout << getName() << " is flying." << std::endl;
    }
};
  • 鱼类同样是动物的一种,Fish 类也可以继承 Animal 类。鱼类有自己独特的行为,比如游泳。
class Fish : public Animal {
public:
    Fish(const std::string& n) : Animal(n) {}
    void swim() {
        std::cout << getName() << " is swimming." << std::endl;
    }
};

基本语法

class DerivedClassName : access-specifier BaseClassName {
    // 派生类成员定义
};

派生类 (Derived Class)

  • 新定义的类,继承自基类
  • 可以添加新成员,修改或扩展基类功能

基类 (Base Class)

  • 被继承的现有类,也称为父类或超类

访问说明符 (access-specifier)

  • 控制基类成员在派生类中的访问权限

可选值:public、protected、private

默认值:

  • 对于 class 定义的类:private
  • 对于 struct 定义的类:public

访问限定符说明:

类成员 \ 继承方式public 继承protected 继承private 继承
基类的 public 成员派生类的 public派生类的 protected派生类的 private
基类的 protected 成员派生类的 protected派生类的 protected派生类的 private
基类的 private 成员不可见不可见不可见
  1. 基类 private 成员在派生类中无论以什么方式继承都是不可见。这里的不可见是指基类的私有成员虽然被继承到了派生类对象中,但是语法上限制派生类对象无论在类里面还是类外面都无法访问它。
  2. 基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为 protected。可以看出保护成员限定符是因继承才出现的。
  3. 基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承⽅式),public > protected >private
  • 代码示例:

基类:

class Base {
public:
    int publicVar;     // 基类 public 成员
protected:
    int protectedVar;  // 基类 protected 成员
private:
    int privateVar;    // 基类 private 成员(所有继承方式均不可访问)
};
  • 公有继承
class PublicDerived : public Base {
public:
    void access() {
        publicVar = 1;    // 继承为 public → 类外可访问
        protectedVar = 2; // 继承为 protected → 仅派生类内部可访问
    }
};
  • 保护继承
class ProtectedDerived : protected Base {
public:
    void access() {
        publicVar = 1;    // 继承为 protected → 仅派生类内部可访问
        protectedVar = 2; // 继承为 protected → 仅派生类内部可访问
    }
};
  • 私有继承
class PrivateDerived : private Base {
public:
    void access() {
        publicVar = 1;    // 继承为 private → 仅派生类内部可访问
        protectedVar = 2; // 继承为 private → 仅派生类内部可访问
    }
};
  • 在实际运用中⼀般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

继承类模板

在面向对象编程中,“is - a” 关系指的是类的继承关系,即一个类(派生类)是另一个类(基类)的特殊化 ;“has -a” 关系指的是一个类包含另一个类的对象作为成员变量,即聚合关系。

  • 对于 stackvector 的关系既符合 is - a,也符合 has - a

vector 本质是 std 命名空间中实现的类模板:

template<class T>
class vector{};
  • 我们可以基于 stackvectoris - a 关系,让 stack 通过继承 vector 类模板来实现其功能。
namespace test{
	template <class T>
	class stack : public std::vector<T>	{
	public:
		void push(const T &x)	{
			std::vector<T>::push_back(x); //要指定类域
			//或者this->push_back(x);
		}
		void pop(){
			std::vector<T>::pop_back();
		}
		const T &top(){
			return std::vector<T>::back();
		}
		bool empty(){
			return std::vector<T>::empty();
		}
	};
}

注意:模板类继承另一个模板类时,基类的成员函数需要通过作用域限定符this指针访问

  • 基类是类模板时,需要指定一下类域来调用其成员,否则编译报错:
    在这里插入图片描述
error C3861: “push_back”: 找不到标识符

这里涉及到对编译器对C++类模板的编译编译过程

两阶段名称查找(Two-phase name lookup

C++模板的编译分为两个阶段:

  • 模板定义阶段:编译器解析模板的非依赖型名称(Non-dependent Names),解析模板本身的语法,检查不依赖模板参数的名称
  • 模板实例化阶段:解析依赖型名称(Dependent Names,即与模板参数相关的名称),生成具体类型代码时,检查依赖模板参数的名称

对于继承自模板基类的成员访问,需要显式指明来源:

  • 因为 基类 std::vector 的类型依赖于模板参数 T,其成员函数 push_back() 属于依赖型名称(Dependent name),编译器在模板定义阶段无法确定这些成员是否存在。
  • 因为 stack 实例化时,也实例化 vector 了,但由于模板是按需实例化,push_back等成员函数未实例化,所以编译器找不到 push_back 成员函数。

另一种解决方案是利用 this->push_back 替代

  • this 指针的作用机制:将成员访问变为依赖型名称

依赖型名称的标记

  • this 的类型是 Derived<T>*,与模板参数 T 相关
  • this->push_back() 成为依赖型表达式,编译器推迟其名称查找

stack<int> 被实例化时:

// 实例化后的代码等价形式
class stack<int> : public vector<int> {
public:
    void push(const T &x)	{
			this->push_back(x);此时vecotr<int>已完全实例化
		}
};
  • 编译器在实例化后的 vector<int> 中查找 push_back()
  • 通过 this 指针访问成员,使得表达式成为类型依赖表达式,符合延迟查找规则。

测试:

int main(){
    test::stack<int> st;
    st.push(1);
    st.push(2);
    st.push(3);
    while (!st.empty())  {
        std::cout << st.top() << " ";
        st.pop();
    }
    std::cout << std::endl;
    return 0;
}

输出:

3 2 1

基类和派生类的转换

public 继承的派生类对象 可以赋值给 基类的指针 /基类的引用。这里有个形象的说法叫切片。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。

  • 基类对象不能赋值给派生类对象。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。

为什么呢?下面为大家分析基类和派生类之间的转换是如何进行的,以及底层的原理。


内存布局与继承的关系

首先我们需要了解基类和派生类中的成员变量是如何在内存中存储的

在这里插入图片描述
基类和派生类的内存结构

  • 基类对象:仅包含基类定义的成员变量。
  • 派生类对象:在内存中先存储基类部分,再存储派生类新增的成员变量。

关键点

  • 派生类对象的起始地址就是基类部分的起始地址。
  • 基类指针/引用可以直接指向派生类对象的基类部分,无需任何偏移计算。

向上转型

定义

  • 将派生类指针/引用隐式转换为基类指针/引用。

特点:

  • 隐式转换:无需手动强制类型转换,编译器自动完成。
  • 安全:因为派生类对象必然包含基类的所有成员,转换不会丢失基类部分的数据。

安全性问题:

  • 注意:向上转型是安全的,指的是派生类的指针或者引用转换成基类的指针或引用是安全的!

这里会涉及到一个陷阱:

  • 如果通过值传递将派生类直接赋值给基类,派生类特有的成员变量会被丢弃,也就是"切片"
| 基类成员 | 派生类新增成员 | → 值传递后 → | 基类成员 |

下面我们举个例子来让大家理解值传递向上转型的安全问题:

基类:

// 基类
class Animal {
public:
    int age = 0;
    virtual void speak() { // 虚函数
        cout << "Animal sound (age: " << age << ")" << endl;
    }
};
  • 有关虚函数在多态章节会详细介绍

派生类:

// 派生类
class Cat : public Animal {
public:
    int lives = 9; // 派生类特有成员
    void speak() override { // 覆盖虚函数
        cout << "Meow (lives: " << lives << ", age: " << age << ")" << endl;
    }
};

值传递:

// 值传递函数:参数为基类对象
void processByValue(Animal animal) {
    animal.speak(); // 调用虚函数
    animal.age = 100; // 修改基类成员
}
  • 发生对象切片:Cat对象被强制转换为Animal基类对象,丢失派生类特有成员lives
  • 虚函数调用:由于切片后对象类型为Animal,调用基类的speak()。

引用传递:

// 引用传递函数:参数为基类引用
void processByRef(Animal& animal) {
    animal.speak();
    animal.age = 200;
}
  • 保持多态性:通过基类引用操作派生类对象
  • 虚函数调用:动态绑定到Cat::speak()。

main函数:

int main() {
    Cat cat;
    cat.age = 3;
    cat.lives = 9;
    
    cout << "----- 值传递 -----" << endl;
    processByValue(cat); // 值传递触发对象切片
    cout << "值传递后 cat 的 age: " << cat.age << endl; // age 未被修改
    cout << "值传递后 cat 的 lives: " << cat.lives << endl; // lives 保持原值

    cout << "\n----- 引用传递 -----" << endl;
    processByRef(cat); // 引用传递保持多态性
    cout << "引用传递后 cat 的 age: " << cat.age << endl; // age 被修改
    return 0;
}

运行结果:

Animal sound (age: 3)
值传递后 cat 的 age: 3
值传递后 cat 的 lives: 9

----- 引用传递 -----
Meow (lives: 9, age: 3)
引用传递后 cat 的 age: 200

向下转型

定义:

  • 向下转型是将基类的指针或引用转换为派生类指针或引用的操作。

特点:

  • 方向性:与自然的向上转型(派生类→基类)相反,向下转型是逆向操作。
  • 显式强制:必须通过 static_castdynamic_cast 显式转换,无法隐式完成。

为什么需要向下转型:

当基类指针/引用实际指向的是派生类对象时,若需要访问派生类特有的成员(方法或属性),必须通过向下转型恢复其原始类型才能访问。

class Animal {}; // 基类
class Cat : public Animal { 
public: 
    void meow() { /* 派生类特有方法 */ } 
};

Animal* animalPtr = new Cat(); // 基类指针指向派生类对象
animalPtr->meow(); // 错误!基类指针无法直接访问派生类方法

此时必须通过向下转型操作:

Cat* catPtr = static_cast<Cat*>(animalPtr); // 向下转型
catPtr->meow(); // 正确

向下转型的两种方式

  • static_cast(静态转型)

特点:

  • 在编译期完成类型转换。
  • 不进行运行时类型检查,若转换错误可能导致未定义行为(如访问非法内存)。

  • dynamic_cast(动态转型)

特点:

  • 在运行时检查类型是否合法(依赖RTTI)。

若转换非法:

  • 对指针返回 nullptr
  • 对引用抛出 std::bad_cast 异常。

要求基类至少有一个虚函数(多态类型)。


安全性问题:

static_cast 的未检查风险

  • 本质:static_cast 在编译期完成类型转换,但不验证实际对象类型。

典型UB(未定义行为)场景:

class Animal {};
class Cat : public Animal { public: void meow() {} };
class Dog : public Animal {};

Animal* animal = new Dog(); // 实际指向Dog对象
Cat* cat = static_cast<Cat*>(animal); // 编译通过,但实际类型不匹配
cat->meow(); // 未定义行为!可能崩溃或破坏内存

dynamic_cast 的局限性

  • 依赖 RTTI:若基类无虚函数,dynamic_cast 无法使用
class Base {}; // 无虚函数
class Derived : public Base { public: void foo() {} };

Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base); // 编译错误!

继承中的作用域

作用域嵌套规则

  • 派生类作用域 嵌套在 基类作用域 内。

名字查找顺序:

  • 在派生类中访问成员时,优先在 当前类作用域 查找,若未找到则向 直接基类作用域 逐层向上查找。

示例:

class Base {
public:
    int value = 10;
    void print() { cout << "Base: " << value << endl; }
};

class Derived : public Base {
public:
    int value = 20;  // 隐藏基类的value成员
    void print() {   // 隐藏基类的print函数
        cout << "Derived: " << value << endl;
        cout << "Base::value: " << Base::value << endl;  // 显式访问基类成员
    }
};
  • 成员隐藏:派生类中定义的 value 和 print 会隐藏基类的同名成员。
  • 显式访问:通过 Base::value 可绕过隐藏访问基类成员。

隐藏规则

同名成员隐藏

  • 规则:派生类中定义与基类同名的成员(数据或函数)会隐藏基类的成员,无论参数是否一致。
class Base {
public:
    void func(int x) { cout << "Base::func(int)" << endl; }
};

class Derived : public Base {
public:
    void func(double x) {  // 隐藏Base::func(int)
        cout << "Derived::func(double)" << endl;
    }
};

Derived d;
d.func(5);    // 输出 "Derived::func(double)"(参数隐式转换)
d.Base::func(5); // 显式调用基类函数

虚函数与作用域

覆盖(Override)条件:

  • 基类函数声明为 virtual
  • 派生类函数签名完全相同(包括返回类型、参数、const修饰符)。

隐藏非虚函数:

class Base {
public:
    virtual void foo() { cout << "Base::foo" << endl; }
    void bar() { cout << "Base::bar" << endl; }
};

class Derived : public Base {
public:
    void foo() override { cout << "Derived::foo" << endl; }  // 正确覆盖
    void bar(int) { cout << "Derived::bar(int)" << endl; }   // 隐藏Base::bar()
};

Derived d;
d.bar(); // 错误!Base::bar()被隐藏
d.Base::bar(); // 正确

- 使用 using 声明解除隐藏

class Base {
public:
    void func(int) {}
    void func(double) {}
};

class Derived : public Base {
public:
    using Base::func;  // 引入基类所有重载版本的func
    void func(const char*) {}  // 添加新重载
};

Derived d;
d.func(5);     // 调用Base::func(int)
d.func("abc"); // 调用Derived::func(const char*)

多层继承的作用域链

作用域逐层嵌套

class A { public: void f() {} };
class B : public A { public: void f(int) {} };  // 隐藏A::f()
class C : public B { public: void f() {} };     // 隐藏B::f(int)

C c;
c.f();       // 调用C::f()
c.B::f(5);   // 显式调用B::f(int)
c.A::f();    // 显式调用A::f()

虚函数的多层覆盖

class A { public: virtual void f() { cout << "A::f" << endl; } };
class B : public A { public: void f() override { cout << "B::f" << endl; } };
class C : public B { public: void f() override { cout << "C::f" << endl; } };

C c;
A* ptr = &c;
ptr->f();  // 输出 "C::f"(动态绑定)

派生类的默认成员函数

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

在这里插入图片描述

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用基类的构造函数。
  • 派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类的拷贝初始化。
  • 派生类的 operator= 必须调用基类的 operator= 完成基类的复制。需要注意的是,派生类的 operator= 会隐藏基类的operator=,因此显式调用基类的 operator= 时,需指定基类作用域(例如Base::operator=)。
  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。这是为了保证派生类对象先清理派生类成员、再清理基类成员的顺序。
  • 派生类对象初始化时,先调用基类构造函数,再调用派生类构造函数。
  • 派生类对象析构清理时,先调用派生类析构函数,再调用基类析构函数。

由于多态中一些场景下析构函数需要构成重写(重写条件之一是函数名相同,具体在多态章节讲解),编译器会对析构函数名进行特殊处理,统一处理为 destructor()。因此,若基类析构函数未加 virtual,派生类析构函数与基类析构函数构成隐藏关系(而非重写)。

在这里插入图片描述

默认构造函数

(Derived() = default;)

基类构造规则:

  • 自动调用 基类的默认构造函数。
  • 若基类无默认构造函数,必须显式调用基类的其他构造函数。

成员初始化:

  • 对派生类新增的成员变量,按默认初始化规则处理(内置类型不初始化,类类型调用默认构造函数)。

示例:

class Base {
public:
    Base(int x) : value(x) {}  // 无默认构造函数
private:
    int value;
};

class Derived : public Base {
public:
    // 错误!基类无默认构造函数,必须显式调用
    // Derived() = default; 

    // 正确:显式调用基类构造函数
    Derived() : Base(0) {}  
};

拷贝构造函数

(Derived(const Derived&) = default;)

基类拷贝规则:

  • 调用 基类的拷贝构造函数。

成员拷贝规则:

  • 对派生类新增成员,执行 成员拷贝初始化(浅拷贝)。

示例:

class Base {
public:
    Base(const Base&) { cout << "Base copy" << endl; }
};

class Derived : public Base {
public:
    int* data;
    // 默认拷贝构造函数行为:
    // 1. 调用 Base::Base(const Base&)
    // 2. 拷贝 data 指针(浅拷贝)
    Derived(const Derived&) = default;
};

Derived d1;
d1.data = new int(10);
Derived d2 = d1; // 调用默认拷贝构造函数,data 指针被浅拷贝

拷贝赋值运算符

(Derived& operator=(const Derived&) = default;)

基类赋值规则:

  • 调用 基类的拷贝赋值运算符。

成员赋值规则:

  • 对派生类新增成员,执行 成员拷贝赋值。

示例:

class Base {
public:
    Base& operator=(const Base&) {
        cout << "Base copy assign" << endl;
        return *this;
    }
};

class Derived : public Base {
public:
    string str;
    Derived& operator=(const Derived&) = default; // 自动调用基类拷贝赋值
};

Derived d1, d2;
d1 = d2; // 调用 Base::operator= 和 string::operator=

析构函数

(~Derived() = default;)

析构顺序:

  • 先执行 派生类析构函数体。
  • 然后按成员声明逆序销毁 派生类新增成员。
  • 最后自动调用 基类析构函数。

虚析构函数:

  • 若基类析构函数为 virtual,则派生类析构函数自动成为虚函数。

示例:

class Base {
public:
    virtual ~Base() { cout << "~Base" << endl; }
};

class Derived : public Base {
public:
    ~Derived() { cout << "~Derived" << endl; }
};

Base* ptr = new Derived();
delete ptr;  // 输出:~Derived → ~Base

注意事项:

显式调用基类版本

  • 在自定义派生类成员函数中,需手动调用基类对应函数:
class Derived : public Base {
public:
    Derived(const Derived& d) : Base(d) {  // 调用基类拷贝构造函数
        // 拷贝派生类成员...
    }

    Derived& operator=(const Derived& d) {
        Base::operator=(d);  // 调用基类拷贝赋值
        // 赋值派生类成员...
        return *this;
    }
};

继承构造函数(C++11)

  • 使用 using Base::Base; 继承基类构造函数:
class Base {
public:
    Base(int x) {}
};

class Derived : public Base {
public:
    using Base::Base;  // 继承 Base(int x)
};

Derived d(5);  // 合法

继承和友元

C++继承体系中,友元函数是不可被继承的。基类的友元函数不会自动成为派生类的友元。也就是说基类友元不能访问派生类私有和保护成员 。除非派生类也声明该函数为友元函数。

class Base {
    friend void foo(Base&);
private:
    int a;
};

class Derived : public Base {
private:
    int b;
};

void foo(Base& b) {
    b.a = 42;  // 合法:foo是Base的友元
    // b.b = 42;  // 错误:无法访问Derived的私有成员
}
  • foo能访问Base的私有成员a,但无法访问Derived新增的私有成员b,除非在Derived中显式声明foo为友元。

基类的友元函数可以通过基类引用/指针,访问派生类对象中继承自基类的私有成员。

Derived d;
foo(d);  // 合法:传递派生类对象给基类引用
  • 虽然d是Derived类型,但foo通过Base&访问的是其基类部分的a,这是允许的。

派生类需显式声明友元

  • 若派生类需要允许外部函数访问其私有成员,必须独立声明友元。
class B {
    friend class A;
private:
    int secret;
};

class A {};
class C : public A {};

void test() {
    C c;
    // c.secret = 42;  // 错误:C不是B的友元
}

继承和静态成员

静态成员的可见性

静态成员属于定义它的类,不会被派生类继承,但可以通过作用域运算符 (::) 访问。基类定义了 static 静态成员,则整个继承体系里面只有⼀个这样的成员。无论派⽣出多少个派生类,都只有⼀个 static 成员实例。

派生类可以直接访问基类的 公有(public)或保护(protected)静态成员。

示例:

class Base {
public:
    static int count;  // 静态成员声明
    static void print() { cout << "Base: " << count << endl; }
};
int Base::count = 0;  // 静态成员初始化

class Derived : public Base {
public:
    void increment() { 
        Base::count++;  // 合法:访问基类的公有静态成员
    }
};

int main() {
    Derived d;
    d.increment();
    Base::print();  // 输出 "Base: 1"
    Derived::print();  // 同样合法:调用基类的静态函数
}

静态数据成员的初始化

静态数据成员必须在类外单独初始化,且初始化位置不影响继承。即使通过派生类访问基类的静态成员,初始化仍需在基类作用域中完成

class Base {
public:
    static int x;
};
int Base::x = 100;  // 必须初始化

class Derived : public Base {};

int main() {
    Derived::x = 200;  // 修改基类的静态成员
    cout << Base::x;   // 输出 200
}

静态成员函数与多态(TODO)

静态成员函数不能是虚函数,因为它们不依赖于对象实例(没有 this 指针)。

即使派生类定义了同名的静态函数,也不会覆盖基类的静态函数。

示例:

class Base {
public:
    static void foo() { cout << "Base::foo\n"; }
};

class Derived : public Base {
public:
    static void foo() { cout << "Derived::foo\n"; }
};

int main() {
    Derived::foo();      // 输出 "Derived::foo"
    Derived::Base::foo(); // 输出 "Base::foo"
}

同名静态成员的隐藏

如果派生类定义了与基类同名的静态成员,基类的静态成员会被隐藏。

需要通过作用域运算符 (Base::) 显式访问基类的静态成员。

class Base {
public:
    static int value;
};
int Base::value = 10;

class Derived : public Base {
public:
    static int value;  // 隐藏基类的静态成员
};
int Derived::value = 20;

int main() {
    cout << Base::value;    // 输出 10
    cout << Derived::value; // 输出 20
    cout << Derived::Base::value; // 输出 10(显式访问基类静态成员)
}

多继承及其菱形继承

  • 单继承:当一个派生类只有一个直接基类时,这种继承关系被称为单继承。
  • 多继承:当一个派生类有两个或以上直接基类时,这种继承关系被称为多继承
  • 多继承对象在内存中的模型是,先继承的基类位于前面,后继承的基类位于后面,派生类成员则放在最后。
  • 菱形继承:菱形继承是多继承的一种特殊情况。从下面的对象成员模型构造可以看出,菱形继承存在数据冗余和二义性的问题。

在这里插入图片描述

单继承

定义

  • 一个派生类(Derived Class)只有一个直接基类(Base Class)的继承关系。

内存模型

  • 派生类对象的内存布局:基类成员在前,派生类新增成员在后。
  • 指针转换时,基类指针可以直接指向派生类对象(隐式向上转型)。
class Animal {
public:
    int age;
};

class Dog : public Animal {  // 单继承
public:
    int weight;
};

int main() {
    Dog dog;
    dog.age = 2;     // 访问基类成员
    dog.weight = 10; // 访问派生类成员
    Animal* ptr = &dog; // 合法:基类指针指向派生类对象
    return 0;
}

多继承

定义

  • 一个派生类有多个直接基类的继承关系。

内存模型

  • 基类按声明顺序排列,派生类成员在最后。
  • 指针转换时需显式指定基类类型(避免二义性)。
class LandAnimal {
public:
    void walk() { cout << "Walking\n"; }
};

class WaterAnimal {
public:
    void swim() { cout << "Swimming\n"; }
};

class Frog : public LandAnimal, public WaterAnimal {  // 多继承
public:
    void jump() { cout << "Jumping\n"; }
};

int main() {
    Frog frog;
    frog.walk();  // 调用 LandAnimal 方法
    frog.swim();  // 调用 WaterAnimal 方法
    frog.jump();  // 调用派生类方法

    // 显式指定基类指针类型
    LandAnimal* landPtr = &frog;
    WaterAnimal* waterPtr = &frog;
    return 0;
}

菱形继承

Diamond Inheritance

问题

  • 数据冗余:派生类会包含多个基类的同一份成员。
  • 二义性:访问基类成员时需显式指定路径。

示例代码(问题演示)

class Person {
public:
    string name;
};

class Student : public Person {};  // 继承 Person
class Teacher : public Person {};  // 继承 Person

class Assistant : public Student, public Teacher {};  // 菱形继承

int main() {
    Assistant assistant;
    // assistant.name = "Alice";  // 错误:二义性(无法确定是 Student::name 还是 Teacher::name)
    assistant.Student::name = "Alice"; // 显式指定路径
    assistant.Teacher::name = "Bob";   // 数据冗余:Person 被存储两次
    return 0;
}

解决方案:虚继承(Virtual Inheritance

  • 使用 virtual 关键字声明基类,确保公共基类在派生类中只保留一份。
  • 初始化时必须直接调用公共基类的构造函数。
class Person {
public:
    string name;
};

class Student : virtual public Person {};  // 虚继承
class Teacher : virtual public Person {};  // 虚继承

class Assistant : public Student, public Teacher {};

int main() {
    Assistant assistant;
    assistant.name = "Alice";  // 合法:Person 只保留一份
    return 0;
}
  • 优先使用单继承:避免复杂性。
  • 慎用多继承:仅在明确需要组合多个独立功能时使用。
  • 避免菱形继承:如必须使用,务必通过虚继承解决冗余问题。

I0库中的菱形虚拟继承

在这里插入图片描述

template<class CharT, class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<CharT, Traits>
{};
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits>
{};

继承和组合

  • public 继承是一种 “is-a” 的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种 “has-a” 的关系。假设 B 组合了 A,那么每个 B 对象中都有一个 A 对象。
  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white - box reuse)。术语“白箱”是相对于可视性而言的:在继承方式中,基类的内部细节对派生类可见。继承在一定程度上破坏了基类的封装性,基类的改变会对派生类产生很大的影响。派生类和基类之间的依赖关系很强,耦合度高。
  • 对象组合是类继承之外的另一种复用选择。新的、更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black - box reuse),因为对象的内部细节是不可见的。对象只以 “黑箱” 的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于保持每个类的封装性。

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


思考题

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

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

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

  • A. 编译报错 B. 运行报错 C. 正常运行
#include <iostream>
using namespace std;

class A
{
public:
    void fun()
    {
        cout << "func()" << endl;
    }
};
class B : public A
{
public:
    void fun(int i)
    {
        cout << "func(int i)" << i << endl;
    }
};
int main()
{
    B b;
    b.fun(10);
    b.fun();
    return 0;
};

多继承中指针偏移问题?下⾯说法正确的是( )

  • A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
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;
}

写在最后

本文到这里就结束了,有关C++更深入的讲解,如多态,C++11新语法新特性,以及智能指针和异常级话题,后面会发布专门的文章为大家讲解。感谢您的观看!

如果你觉得这篇文章对你有所帮助,请为我的博客 点赞👍收藏⭐️ 评论💬或 分享🔗 支持一下!你的每一个支持都是我继续创作的动力✨!🙏
如果你有任何问题或想法,也欢迎 留言💬 交流,一起进步📚!❤️ 感谢你的阅读和支持🌟!🎉
祝各位大佬吃得饱🍖,睡得好🛌,日有所得📈,逐梦扬帆⛵!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2321131.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FPGA_YOLO(二)

上述对cnn卷积神经网络进行介绍,接下来对YOLO进行总结,并研究下怎么在FPGA怎么实现的方案。 对于一个7*7*30的输出 拥有49个cell 每一个cell都有两个bbox两个框,并且两个框所包含的信息拥有30个 4个坐标信息和一个置信度5个,剩下就是20个类别。 FPGA关于YOLO的部署 1…

蓝桥杯学习-14子集枚举,二进制枚举

子集枚举 一、回溯3-子集枚举&#xff08;递归实现指数型枚举&#xff09; 一旦涉及选与不选&#xff0c;删和不删&#xff0c;留和不留-->两种状态-->就要想到子集枚举例题1–递归实现指数型枚举19685 其实看不懂这个题目&#xff0c;好奇怪的题目。根据老师的解析来写…

人工智能时代大学教育范式重构:基于AI编程思维的能力培养路径研究

人工智能技术的快速发展正在重塑高等教育的内容与方法。本文以AI编程教育为切入点&#xff0c;通过文献分析与案例研究&#xff0c;探讨AI时代大学教育的核心能力需求与教学范式转型路径。研究发现&#xff0c;AI编程中蕴含的系统性思维训练、项目架构能力和元认知能力培养机制…

<数据集>轨道异物识别数据集<目标检测>

数据集下载链接&#xff1a;https://download.csdn.net/download/qq_53332949/90527370 数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;1659张 标注数量(xml文件个数)&#xff1a;1659 标注数量(txt文件个数)&#xff1a;1659 标注类别数&#xff1a;6 标注类别…

Pyecharts功能详解与实战示例

一、Pyecharts简介 Pyecharts是一个基于Python的开源数据可视化库&#xff0c;它基于百度的Echarts库&#xff0c;提供了丰富的图表类型和强大的交互功能。通过Pyecharts&#xff0c;你可以轻松创建各种精美的图表&#xff0c;如折线图、柱状图、饼图、散点图、地图等&#xf…

EasyUI数据表格中嵌入下拉框

效果 代码 $(function () {// 标记当前正在编辑的行var editorIndex -1;var data [{code: 1,name: 1,price: 1,status: 0},{code: 2,name: 2,price: 2,status: 1}]$(#dg).datagrid({data: data,onDblClickCell:function (index, field, value) {var dg $(this);if(field ! …

C语言:扫雷

在编程的世界里&#xff0c;扫雷游戏是一个经典的实践项目。它不仅能帮助我们巩固编程知识&#xff0c;还能锻炼逻辑思维和解决问题的能力。今天&#xff0c;就让我们一起用 C 语言来实现这个有趣的游戏&#xff0c;并且通过图文并茂的方式&#xff0c;让每一步都清晰易懂 1. 游…

操作系统必知的面试题

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…

清华大学.智灵动力-《DeepSeek行业应用实践报告》附PPT下载方法

导 读INTRODUCTION 今天分享是由清华大学.智灵动力&#xff1a;《DeepSeek行业应用实践报告》&#xff0c;主要介绍了DeepSeek模型的概述、优势、使用技巧、与其他模型的对比&#xff0c;以及在多个行业中的应用和未来发展趋势。为理解DeepSeek模型的应用和未来发展提供了深入的…

可视化图解算法:链表的奇偶重排(排序链表)

1. 题目 描述 给定一个单链表&#xff0c;请设定一个函数&#xff0c;将链表的奇数位节点和偶数位节点分别放在一起&#xff0c;重排后输出。 注意是节点的编号而非节点的数值。 数据范围&#xff1a;节点数量满足 0≤n≤105&#xff0c;节点中的值都满足 0≤val≤10000 要…

SAP Activate Methodology in a Nutshell Phases of SAP Activate Methodology

SAP Activate Methodology in a Nutshell Phases of SAP Activate Methodology

开源AI大模型、AI智能名片与S2B2C商城小程序源码:实体店引流的破局之道

摘要&#xff1a;本文聚焦实体店引流困境&#xff0c;提出基于"开源AI大模型AI智能名片S2B2C商城小程序源码"的技术整合方案。通过深度解析各技术核心机制与协同逻辑&#xff0c;结合明源云地产营销、杭州美甲店裂变等实际案例&#xff0c;论证其对流量精准获取、客户…

JVM 02

今天是2025/03/23 19:07 day 10 总路线请移步主页Java大纲相关文章 今天进行JVM 3,4 个模块的归纳 首先是JVM的相关内容概括的思维导图 3. 类加载机制 加载过程 加载&#xff08;Loading&#xff09; 通过类全限定名获取类的二进制字节流&#xff08;如从JAR包、网络、动态…

pyecharts在jupyter notebook中不能够渲染图表问题。

在使用jupyter notebook中使用pyecharts绘制可视化图表的时候,发现图表不能渲染到页面中,生成的html是没问题的,本文主要解决在jupyter notebook中不能渲染这个问题。 1、原因分析 2、解决办法 如果是使用的虚拟环境,需要下你提前激活虚拟环境,再进行下列操作。 因为需要…

《AI大模型趣味实战 》第7集:多端适配 个人新闻头条 基于大模型和RSS聚合打造个人新闻电台(Flask WEB版) 1

AI大模型趣味实战 第7集&#xff1a;多端适配 个人新闻头条 基于大模型和RSS聚合打造个人新闻电台(Flask WEB版) 1 摘要 在信息爆炸的时代&#xff0c;如何高效获取和筛选感兴趣的新闻内容成为一个现实问题。本文将带领读者通过Python和Flask框架&#xff0c;结合大模型的强大…

基于Spring Boot的健身房管理系统的设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…

WSL Linux 子系统download

WSL各Linux 子系统下载 WSL Linux 最新下载 微软应用商店 | Microsoft StoreWSL Linux 历史版下载复制应用商店Linux地址到转换下载地址https://store.rg-adguard.net/ Version百度网盘离线下载OracleLinux提取

Qt中通过QLabel实时显示图像

Qt中的QLabel控件用于显示文本或图像&#xff0c;不提供用户交互功能。以下测试代码用于从内置摄像头获取图像并实时显示&#xff1a; Widgets_Test.h&#xff1a; class Widgets_Test : public QMainWindow {Q_OBJECTpublic:Widgets_Test(QWidget *parent nullptr);~Widgets…

基于springboot的校园资料分享平台(048)

摘要 随着信息互联网购物的飞速发展&#xff0c;国内放开了自媒体的政策&#xff0c;一般企业都开始开发属于自己内容分发平台的网站。本文介绍了校园资料分享平台的开发全过程。通过分析企业对于校园资料分享平台的需求&#xff0c;创建了一个计算机管理校园资料分享平台的方案…

CS2 demo manager 安装

CS2DM CS Demo Managerhttps://cs-demo-manager.com/PostgreSQL&#xff08;CS2DM需要17以上&#xff09; EDB: Open-Source, Enterprise Postgres Database Managementhttps://www.enterprisedb.com/downloads/postgres-postgresql-downloads 新CS2dm现在打开是这样的&…