目录:
  • 继承的概念:
  • 基类和派生类的赋值兼容转换
  • 继承中的作用域
  • 派生类的默认成员函数
    • 构造函数
    • 拷贝函数
    • 赋值函数
    • 析构函数
  • 多继承与菱形继承
  • 虚继承
  • 继承和组合
  • 思考:
    • 为什么说继承会破坏封装?
    • 实现一个不能被继承的类。
    • C++的类之间的五种关系。
    • inline函数可以是虚函数吗?
    • 静态函数可以是虚函数吗?
    • 虚函数和虚基表的区别?
    • 构造函数可以是虚函数吗?
    • 虚函数的生成阶段、在哪生成?
    • 什么是抽象类?
    • …………

😁继承的概念:

        继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有
类的特性的基础上进行扩展,增加方法和属性,这样产生的类,称派生类。
        继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。

  •          需要注意的是,我们派生类的继承类型并不影响基类相关成员使用以及派生类成员使用。影响的是我们站在派生类外部再去通过派生类的接口访问它们,这个时候接口体现出的就是继承中的基类与派生类中添加的最小权限;
  •         另外,别忘了基类对派生类来说会有点见外,愿意分享private成员继承给派生类,但是派生类却无法使用(就像那谁说的“钱是给……看的,不是给……花的”)。
  •         相当来说派生类的限制访问域继承,在一定程度上来讲,由于接口性质的改变,变相是对于被继承成员的访问域的限制和缩小。

😎基类和派生类之间的转换:(赋值兼容转换)

我们知道,一般在类型转化的过程中会产生临时对象。但这里存在一个特殊处理:
                                  ———赋值兼容转换(派生中切割出来的父类那一部分(不产生临时对象)
  • public继承的派生类对象,可以赋值给 基类的指针 / 基类的引用。这里有个形象的说法叫切片,即把派生中基类那部分切出来,基类指针或者引用指向的是派生类中切出来的基类那部分
  • 基类对象不能赋值给派生类对象。

这里需要格外注意!

问:为什么只能是 public 继承?

答: 因为 public 继承 = “是一个”(is-a)关系。

规则:

  • public 继承:派生类 是一个 基类✅ 可以赋值给基类指针 / 引用✅ 可以切片

  • protected/private 继承:不是 “是一个” 关系❌ 不能赋值给基类指针 / 引用❌ 不能切片❌ 编译器直接报错

综上:只有 public 继承,才满足 is-a 关系,才能发生向上转型(切片)。

补充:

  • public 继承:是 - a(is-a),派生类对象可被当作基类对象使用。
  • protected 继承:是 - a,但对外隐藏,对内是 - a,对外不是。
  • private 继承:不是 - a,派生类是基类的一种实现,而非特殊版本。

代码展示: (public继承派生类赋值给基类指针)

class Person
{
protected:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};

class Student : public Person
{
public:
	int _No; // 学号
};

int main()
{
	Student sobj;

	//1.派生类对象可以赋值给基类的指针 / 引用
	Person* pp = &sobj;
	Person& rp = sobj;
}
  • 子类可以赋值构造父类,父类不可以赋值构造子类。


    🥳继承中的作用域:

    🤨隐藏规则:

    1. 在继承体系中基类和派生类都有独立的作用域。
    2. 派生类和基类中有同名成员,派生类成员将屏蔽基类同名成员的直接访问,这种情况叫隐藏(派生类成员函数中被隐藏的基类成员,可以使用 基类::基类成员 显示访问)。   
    3. 需要注意都是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
    4. 注意在实际中在继承体系里面最好不要定义同名的成员~ 
    综上:此处并非虚函数,基类和派生类之间的同名函数构成隐藏。
    • 🫡如果派生类和基类中拥有相同名称的成员:
    •         派生类中使用同名成员时,编译器会优先调用派生类中的值就像我们函数中的临时变量一样,会先调用函数的,再调用全局的!
    •         如果一定要在派生类中去调用基类中的同名成员!我们要指定定义域,基类::同名成员!
    class Person_2
    {
    protected:
    	string _name = "小李子"; // 姓名
        int _num = 111; // 身份证号
    };
    
    class Student_2 : public Person_2
    {
    public:
        void Print()
        {
            cout << " 姓名:" << _name << endl;
            cout << " 学号:" << _num << endl;		// 隐藏
            cout << " 基类 " << Person_2::_num << endl;
        }
    protected:
        int _num = 999; // 学号
    };
    
    int main()
    {
    	Student_2 s;
    	s.Print();
    
        return 0;
    }
    两个例题加深作用域理解:
    第一题选B、第二题选A
            我们先明确函数重载要求是函数名相同,参数列表相同且在同一作用域(重载函数要在同一作用域~这里划重点)
            派生类的作用域是嵌套在基类作用域内部的:
    编译器查找变量顺序:派生类作用域 → 基类作用域 → ... → 基类的基类作用域 → 全局作用域。
             在知晓上述补充信息后我们知道:void func(int) 和 void func()是两个构成函数相同的“ 隐藏~ ” 。
    代码顺序:
            先在派生类中寻找func,如果找到的话,就不再会继续向上级跑,不存在同名不匹配自动向上寻找的情况,它们不是函数重载,没有那套规矩。
    既然我们的void func( )已经隐藏了,我们在传空参数的时候就会报错。
    🫡A和B类中的两个func构成什么关系()
    A. 重载 B. 隐藏 C.没关系
    🫡🫡下⾯程序的编译运⾏结果是什么()
    A. 编译报错 B. 运⾏报错 C. 正常运⾏
    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;
    }
    接第二问:修改报错 ( b.A::func() )在调用的时候指定父类调用 ~~~~。
    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.A::fun();
        return 0;
    }

    派生类中的默认成员函数:

    前情提要:各个默认成员函数默认生成规则:

    • 默认构造:写了任意构造 → 不生成
    • 析构:写了析构 → 不生成
    • 拷贝构造:写拷贝 / 移动相关 → 不生成
    • 拷贝赋值:写拷贝 / 移动 + 有 const / 引用 → 不生成
    • 移动构造:写拷贝 / 析构 / 赋值 任意 → 不生成
    • 移动赋值:写拷贝 / 析构 / 赋值 + 有 const / 引用 → 不生成
    构造是先爹后儿,析构是先儿后爹,主打“孝”道。
    注意:这里的基类一般指的是直系父类,不需要手动隔辈调用。
            先前可知有6个默认成员函数,我们常用的有4个常见默认成员函数,默认的意思是指我们不写,编译器会自动帮我们生成一个。(这里是C++98版本,C++11的移动构造和移动赋值这里暂不论述)。
    🤔4个常见的默认成员函数与继承的关系:
    1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有构造函数,则必须在派生类构造函数的初始列表显示调用。
    2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
    3. 派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域。
    4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
    5. 派生类对象初始化先调用基类构造再调派生类构造。
    6. 派生类对象析构清理先调用派生类析构再调用基类的析构。
    7. 因为多态中一些场景析构函数需要构成重写,重写的条件之一就是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。

    😗派生类构造函数:

            派生类中的成员分为 : 父类成员和子类成员两种。(把父类的成员整体当作一个自定义类)
    子类成员又遵循我们的类构造函数规则,分为内置类型和自定义类型两个分别进行两种方式构造。
    这部分我们将目光聚焦于下方代码示例来理解:
    • 当我们依赖系统默认的构造函数机制时:
            依照规则,父类成员调用父类的构造函数,"name"初始为peter,子类的string对象调用其自己的初始为"",int类型num为随机值。
    • 在父类不存在默认构造函数时派生类成员的初始化顺序:
            我们派生类在初始化成员时,同样遵循基类的按声明顺序调用 ,这里由于父类的成员声明始终先于基类,所以无论将父类的显示构造放在初始化列表的何处,在初始化列表走上台面的那一时刻,都会率先执行父类的构造函数。(下方 Person(name) )
    注意事项:
            我们要注意,一定要成功且正确的调用父类的构造函数:如果父类是默认构造函数(无参数的构造函数、给定缺省参数的构造函数),我们不用显示调用,我们的派生类的构造函数会自动调用;但如果父类是没有无参构造函数,我们就要在派生类中显示调用。
    分析:
            基类的那些构造我们派生类不用多顾,只需要傻傻的写我们的派生类的构造,顺手调用一下父类成员即可。
            什么时候必须要显示调用:当父类没有无参构造函数,只有带参数的构造函数时。
    #include<iostream>
    
    using namespace std;
    
    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
    {
    public:
    
    protected:
    	string _address;
    	int _num; //学号
    };
    
    int main()
    {
    	Student s;
    	return 0;
    }
    错误示范:
    派生类错误的妄图插手基类的成员构造: “儿子想当爷爷”(此处_name是基类的成员)。
    // 派生类
    class Student : public Person
    {
    public:
    	Student(const char* name, string address, int num)
    		:_name(name),                     ←—————————————————————— ❌
    		_address(address),
    		_num(num)
    	{
    
    	}
    protected:
    	string _address;
        int _num; //学号
    };

    😗派生类构造函数——拷贝构造:

    • 当我们依赖系统默认的拷贝构造函数机制时:
            依照规则,我们的基类成员同样还是调用基类的拷贝构造函数,派生类的只需傻傻的写自己的拷贝构造函数即可。
    • 思考:我们调用派生类的拷贝构造,那么其中基类的拷贝构造所需要的参数,派生类对象怎么才能提供呢?
            其实不难想到,我们先前学习的派生类在赋值给基类的指针、引用时的切片行为(赋值兼容转换),此时底部逻辑产生父类的引用这般临时对象,传给父类的拷贝构造函数,完成父类成员的构造任务。
    // 派生类
    class Student : public Person
    {
    public:
        Student(const char* name, string address, int num)
            :Person(name),
            _address(address),
            _num(num)
        {
        
        }
        赋值兼容转换——切片切割出父类的成员,从而合理的传给父类。       
        Student(const Student& s)
            :Person(s),
            _address(s._address),
            _num(s._num)
        {
        
        }
    protected:
    	string _address;	
        int _num; //学号
    };
    
    int main()
    {
    	Student s("baby", "baby", 2);
        return 0;
    }

    何时基类拷贝构造需显示调用:

    • 基类编译器默认合成拷贝 子类自定义拷贝构造 → 自动调基类拷贝,不用写。

    • 基类自己手写拷贝构造(且没有默认合成) 子类自定义拷贝构造 → 必须显式调用基类拷贝

    • 基类拷贝 = delete 子类无论写不写,全都不能拷贝,显式调用直接报错。

    😗派生类构造函数——赋值拷贝:

    • 幽默实现:由于隐藏的缘故,调用父类operator=函数,误调用到了自己。
    	Student& operator=(const Student& s)
    	{
    		if (this != &s)
    		{
    			operator=(s);
    			_address = s._address;
    			_num = s._num;
    		}
    		return *this;
    	}
    错误->栈溢出:Stack overflow,栈里面是函数栈帧,当我们递归无限循环时,我们的栈就会溢出
    • 正确实现
            注意operator的同名隐藏,我们要调用父类时,由于父类和基类的作用域不同,所以一定要指定父类作用域来调用父类的operator=。
    	Student& operator=(const Student& s)
    	{
    		if (this != &s)
    		{
    			Person::operator=(s);
    			_address = s._address;
    			_num = s._num;
    		}
    		return *this;
    	}

    😗派生类析构函数:

    不用特意的在派生类的析构函数中实现基类的析构函数,编译器会帮我们去释放。
    • 错误函数:函数再次调用到了自己
    	~Student()
    	{
            ~Person();
    	}

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

    所以因为这里函数中的~Student 与 ~ Person构成同名隐藏而无法在派生类析构函数中调用到基类的析构函数。所以我们在这里和
    上方的赋值拷贝函数一致,需要我们指定定义域来调用。
    • 版本一
    ~Student() { }
    • 版本二
    • 我们观察到这里的一个对象调用了两次析构:
    原因是:我们的派生类的析构函数在结束后会自动调用基类的析构函数,目的是为了保证先子后父的析构顺序。
    为什么:因为我们派生类方法或相关成员中可能会调用到基类的成员,如果提前进行基类成员的析构,可能会导致报错。
    所以说:我们在写派生类析构函数中不用特地显示调用基类的析构函数。

    😉多继承与菱形继承问题:

    单继承:一个派生类只有一个直接基类时称这个继承关系为单继承。 (一个儿子一个爹)
    多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是:先
            继承在基类前面,后继承在基类后面,派生类成员放到最后面。(多继承依旧默认private继承)
    多继承的结构中,先继承的在内存结构的前列,后继承的在内存结构的后列,派生类在基类之后。
    菱形继承:
    这里的二义性可以通过点名定义域赋值来缓解,但是数据的冗余性还是无法缓解

    😙虚继承:

            当我们虚继承两个类构成菱形结构时,我们的菱下类面临的菱上公共类,在这里优化为一份。这里随之而来的问题是,我们的菱上类成员不会在直系父类中构造,而是在其中跳过,所以打破只需要显示构造直系父类的规则,在此进行自行构造。
    • 例如下方我们的Person需要在Assitant中自行构造,虽然Person在Student和Teacher中也进行了构造,但并不会影响到我们Assitant中的_Person的值。
    class Person
    {
    public:
    	Person(int a)
    		:_Person(a)
    	{
    
    	}
    	int _Person;
    };
    
    class Student : virtual public Person
    {
    public:
    	Student(int name, int num)
    		:Person(name),
    		_Student(num)
    	{
    
    	}
    	int _Student;
    };
    
    class Teacher : virtual public  Person
    {
    public:
    	Teacher(int name, int num)
    		:Person(name),
    		_teacher(num)
    	{
    
    	}
    	int _teacher;
    };
    
    class Assitant : public Student, public Teacher
    {
    public:
    	Assitant(int a, int b, int c)
    		:Student(a, c),
    		Teacher(a, b),
    		Person(c)
    	{
    
    	}
    	int _Assitant;
    };
    
    int main()
    {
    	Assitant At(1, 2, 3);
    	cout << At._Person << endl;
    	//At.Teacher::_Person = 2;
    	//At.Student::_Person = 2;
    	//At._Person = 1;
    	return 0;
    }

    I / O流的库是标准的菱形继承:

    在多继承对象中,先继承的对象在结构的前列:

    😏继承和组合:

    具体关系看附录:
    黑白箱解释:
    •         白箱:关注内部结构与工作原理。
    •         黑箱:关注外部功能与输入输出。

    🐶

    • public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象
    • 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象
    • 继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤
    (white-box reuse)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可⻅ 。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度⾼。
    • 对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
    • 优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合
    组合(Composition) 是一种面向对象设计原则,其核心思想是:
    • 在新类中包含现有类的对象作为成员变量
    • 通过调用成员对象的方法来实现功能复用
    • 建立的是"has-a"(拥有)关系
    #include <iostream>
    #include <string>
    
    // 旧的类 - 引擎
    class Engine {
    private:
        std::string type;
        
    public:
        Engine(const std::string& engineType) : type(engineType) {}
        
        void start() {
            std::cout << type << " 引擎启动...嗡嗡嗡!" << std::endl;
        }
        
        void stop() {
            std::cout << type << " 引擎停止" << std::endl;
        }
    };
    
    // 旧的类 - 车轮
    class Wheel {
    private:
        int size;
        
    public:
        Wheel(int wheelSize) : size(wheelSize) {}
        
        void rotate() {
            std::cout << size << "寸车轮开始旋转" << std::endl;
        }
    };
    
    // 新类 - 汽车(通过组合复用旧类)
    class Car {
    private:
        Engine engine;        // 组合:拥有一个引擎
        Wheel wheels[4];      // 组合:拥有四个车轮
        std::string brand;
        
    public:
        // 在构造函数中初始化成员对象
        Car(const std::string& carBrand, const std::string& engineType, int wheelSize)
            : brand(carBrand), engine(engineType) {
            
            // 初始化四个车轮
            for(int i = 0; i < 4; ++i) {
                wheels[i] = Wheel(wheelSize);
            }
        }
        
        void start() {
            std::cout << brand << " 汽车启动:" << std::endl;
            engine.start();      // 调用成员对象的方法
            for(int i = 0; i < 4; ++i) {
                wheels[i].rotate();  // 调用成员对象的方法
            }
            std::cout << brand << " 开始行驶!" << std::endl;
        }
        
        void stop() {
            std::cout << brand << " 汽车停止:" << std::endl;
            engine.stop();
            std::cout << "汽车已停稳" << std::endl;
        }
    };
    
    // 使用示例
    int main() {
        // 创建汽车对象,自动组合了引擎和车轮
        Car myCar("宝马", "V8", 18);
        Car yourCar("奥迪", "V6", 17);
        
        myCar.start();
        std::cout << "---" << std::endl;
        yourCar.start();
        std::cout << "---" << std::endl;
        myCar.stop();
        
        return 0;
    }

    😜思考:

    为什么说继承会破坏封装:

    省流:耦合度过高
            说“继承会破坏封装”,主要是从设计层面而非语法层面来讨论的。它指的是,如果不加选择地使用公有继承,子类可能会以一种不可控的方式依赖和干扰父类的内部实现细节,从而导致父类的设计变更异常困难,违背了封装“隐藏实现细节,只暴露稳定接口”的初衷。
    下面我们从几个具体方面来详细解释:
    1. 对实现细节的依赖,而非对接口的依赖
    封装的核心思想:使用者(客户端代码)只关心对象“能做什么”(公有接口),而不需要知道它“怎么做”(私有/保护成员)。
    继承带来的问题:为了实现功能,子类常常需要了解甚至直接访问父类的内部实现。
    • 访问保护成员:父类中的 protected 成员对子类是可见的。这意味着父类的内部状态和辅助函数暴露给了子类。
      • 后果:一旦父类需要修改这些 protected 成员(例如,改变一个变量的类型,或删除一个辅助函数),所有依赖它的子类都将被迫需要修改,甚至无法编译。父类的作者在进行重构时会束手束脚。
    示例
    class Stack {
    protected:
        int* m_data;    // 子类可以看到这是一个动态数组
        int m_top;      // 子类可以看到栈顶指针
    public:
        void push(int v) { /* ... */ }
        int pop() { /* ... */ }
    };
    
    class FancyStack : public Stack {
    public:
        void clear() {
            // 子类直接操纵父类的内部状态
            m_top = -1; // 这依赖于父类用 m_top 来管理栈顶
            // 如果父类将来改用链表实现,移除了 m_top,这个子类就崩溃了。
        }
    };

            在这里,FancyStackclear 方法并不是通过 Stack 的公有接口(如连续调用 pop)实现的,而是直接“侵入”了父类的内部实现。这破坏了 Stack 的封装。

    2. 父类实现的脆弱性影响子类
    由于子类依赖于父类的实现,父类实现的任何微小改动都可能像“蝴蝶效应”一样,在子类中引发难以预料的错误。
    • 示例:重写虚函数对内部调用的依赖
    class Account {
    public:
        virtual void withdraw(double amount) {
            if (amount <= getBalance()) {
                // ... 执行扣款
                updateLog(); // 可能是一个非虚的私有函数
            }
        }
    private:
        void updateLog() { /* 记录日志 */ }
    };
    
    class MicroAccount : public Account {
    public:
        void withdraw(double amount) override {
            // 子类可能不知道父类 withdraw 内部调用了 updateLog()
            // 它只覆盖了扣款逻辑,但忘记记录日志了?
            // 或者,父类将来在 withdraw 中增加了新的内部调用,子类覆盖时根本不知道。
            if (amount <= getBalance() + 10) { // 允许透支10元
                // ... 执行扣款
                // 但忘记了调用 updateLog()? 或者不知道要调用?
            }
        }
    };

            子类在重写虚函数时,必须了解父类该函数的内部行为(例如它调用了哪些私有函数,改变了哪些状态),而不仅仅是它的接口契约。这使得重写变得复杂且容易出错,本质上是因为子类破坏了父类函数的“实现封装”。

    3. 名字隐藏与重载问题
    在C++中,如果子类重定义了父类的一个函数(即使是重载版本中的一个),它会隐藏父类中所有同名的其他重载函数,除非使用 using 显式引入。
    class Base {
    public:
        virtual void func(int);
        virtual void func(double); // 重载函数
    };
    
    class Derived : public Base {
    public:
        void func(int) override; // 只重写了 func(int)
    };
    
    int main() {
        Derived d;
        d.func(3.14); // 错误!Base::func(double) 被 Derived::func(int) 隐藏了
    }

            这迫使子类的设计者必须了解父类接口的所有细节,并手动处理名字隐藏问题,否则会破坏客户端代码对父类接口的预期。

    如何避免继承破坏封装?(组合优于继承)
            为了维持良好的封装性,“组合” 通常是比“继承”更安全、更灵活的选择。
    组合
            在新类中包含一个已有类的对象作为成员,通过调用该成员的公有接口来使用其功能。
    对比示例
    • 使用继承(可能破坏封装):
    class FancyStack : public Stack { // “是一个” Stack
        // 可以直接访问 Stack 的 protected 成员,产生强耦合
    };
    • 使用组合(保持封装):
    class FancyStack { // “有一个” Stack
    private:
        Stack m_stack; // 内部包含一个 Stack 对象
    public:
        void push(int v) { m_stack.push(v); }
        int pop() { return m_stack.pop(); }
        void clear() {
            // 只能通过 Stack 的公有接口来操作
            while (!m_stack.empty()) {
                m_stack.pop();
            }
        }
    };

    组合的优势

    1. 封装完好FancyStack 完全无法触及 Stack 的内部实现,只能使用其稳定的公有接口。Stack 的内部实现可以自由更改,只要接口不变,FancyStack 就无需改动。
    2. 依赖更少:耦合度显著降低。
    3. 更灵活:可以在运行时动态更换所包含的对象(例如,通过注入不同的 Stack 实现)。
    结论
    “继承破坏封装”是一个警示,提醒我们:
    • 谨慎使用公有继承,严格遵循 Liskov 替换原则(子类必须能替换掉父类)。
    • 继承应该只用于建立 “是一个” 的语义关系,而不是为了代码复用。
    • 对于代码复用,“组合” 通常是比继承更好的工具,因为它保持了类之间的界限清晰,维护了封装性。
    因此,在C++设计中,有一条著名的准则:“优先使用对象组合,而非类继承”

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

    1. 方法一:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构造函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。
    2. 方法二:C++11新增了一个final关键字,final修改基类,派生类就不能继承了。
    #include<iostream>
    using namespace std;
    
    class Base final
    {
    public:
        void func5() { cout << "Base::func5" << endl; }
    protected:
        int a = 1;
    private:
        //C98的方法
        /*Base() {}*/
    };
    
    //class Driver : public Base  ❌编译器报错
    //{
    //
    //};
    
    int main()
    {
        return 0;
    }

    友元的继承:
    友元关系不能继承,基类的友元不能访问派生类私有和保护成员!
    解决办法:再增加一个友元。

    继承和静态成员:
    基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,这个成员为派生类和基类所公有,在公有的情况下,基类与派生类指定类域都可以访问静态成员。

    C++中的五种关系:

    C++ / OOP 类与类的五种关系(考试必背,除继承、组合全覆盖)
    继承(泛化)
    • 关系:is-a
    • 语法:class B : public A
    • 特点:父子、代码复用、权限继承
    组合(整体–部分)[ private: T* class_ ]
    • 关系:contains-a / 强 has-a
    • 特点:生命周期一致
    • 整体销毁,局部直接销毁
    class A { B b; };

    聚合(聚集)
    • 关系:weak has-a
    • 整体和部分生命周期独立
    • 整体没了,部分还能单独存在
    • 语法:用指针 / 引用持有
    class A { B* pb; };
    组合:密不可分;聚合:临时搭伙

    关联
    最普通的临时使用关系
    一个类在成员函数参数 / 局部变量里用到另一个类
    无长期成员持有
    class A{ public: void fun(B b){} // 只是函数参数用到 };

    依赖
    最弱关系,临时依赖
    比如:
    • 函数参数
    • 局部对象
    • 头文件简单引用
    关系总结:
    • 继承:is-a
    • 组合:强 has-a(成员对象,同生死)
    • 聚合:弱 has-a(指针 / 引用,各活各的)
    • 关联:长期引用
    • 依赖:临时使用

    更多推荐