9.【C++进阶】继承

发布于:2025-08-16 ⋅ 阅读:(26) ⋅ 点赞:(0)

本博客开始进入C++更深层次的内容梳理,首先来看继承相关知识

一、继承的概念及定义

1.继承的概念

继承机制是面向对象程序设计使代码可以复用的最重要的手段,是类层次的复用。允许我们在保持原有类特性的基础上进行扩展,增加成员函数(方法)成员变量(属性

  • 例子
    Student类继承Person,既拥有基类的方法与成员,又可以拥有自己新增加的方法与成员
class Person 
{ 
public: 
    // 进入校园/图书馆/实验室刷二维码等⾝份认证 
    void identity() 
    { 
        cout << "void identity()" <<_name<< endl; 
    } 
protected: 
    string _name = "张三"; // 姓名 
    string _address; // 地址 
    string _tel; // 电话 
    int _age = 18; // 年龄 
}; 
class Student : public Person
{ 
public: 
    // 学习 
    void study() 
    {  ... } 
protected: 
    int _stuid; // 学号 
}; 
class Teacher : public Person 
{ 
public: 
    // 授课 
    void teaching() 
    { ... } 
protected: 
    string title; // 职称
}; 

2.继承的定义

(1)格式:Person是父类(基类),Student是子类(派生类)
继承格式
(2)继承父类成员访问方式的变化
继承父类成员访问方式的变化
①父类private成员在子类中无论以什么方式继承都不可见,但实际上父类的private成员还是继承到了子类中,只是语法限制子类对象不管在子类里面还是外面都不能访问
注意:父类的private在子类不能用,除非调用父类的成员函数时访问其private成员
②除了第三行,父类的其它成员在子类的中的访问方式,取决于min权限{父类的访问限定符,继承方式},也就是父类其他成员在在子类中的访问方式取决于访问限定符和继承方式权限小的那一个
③父类的private成员在子类中不能被访问,如果父类成员不想在类外被访问,但在子类中要能访问,则限定为protected,因此protected是因为继承才出现的
④使用class时默认继承方式是private,使用struct时默认继承方式时public,最好显示写出继承方式
实践中一般使用public继承,private/protected成员都只能在子类使用,扩展维护性不强,不推荐
(3)继承类模板
①模板按需实例化:需要用到哪个函数,就实例化哪个
②基类是类模板时,调用其成员函数需要指定一下类域

#include<iostream>
#include<vector>
using namespace std;
namespace my_stack
{
    template<class T>
    class stack : public std::vector<T>
    {
    public:
        void push(const T& x)
        {
            // 编译报错:error C3861: “push_back”: 找不到标识符
            // push_back(x);
            // 基类是类模板时,调用其成员函数需要指定一下类域
            // 告诉编译器,要调用vector<T>里面的push_back,让他实例化
            vector<int>::push_back(x);
        }
        void pop() { vector<T>::pop_back(); }
        const T& top() { return vector<T>::back(); }
        bool empty() { return vector<T>::empty(); }
    };
}

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

二、基类和派生类之间的赋值转换

  • public继承的派生类对象可以赋值给基类的指针/引用,而不需要加const,这里的指针和引用都是派生类对象的基类部分,意味着基类指针或引用可能指向基类对象,也可能指向派生类对象
  • 基类对象不能赋值给派生类对象
  • 派生类对象赋值给基类对象通过调用基类的拷贝构造或赋值重载完成,此过程叫切割/切片
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使⽤RTTI(Run-Time
    Type Information)的dynamic_cast 来进行识别后进行安全转换。【详情见后续】
    基类和派生类之间的赋值转换
class Person
{
protected:
    string _name; // 姓名
    string _sex; // 性别
    int _age; // 年龄
};
class Student : public Person
{
public:
    int _No; // 学号
};
int main()
{
    Student s;
    // 1.派生类对象可以赋值给基类的指针/引用
    Person* pp = &s;
    Person& rp = s;
    // 派⽣类对象可以赋值给基类的对象是通过调用基类的拷贝构造完成的
    Person p = s;
    //2.基类对象不能赋值给派生类对象,这里会编译报错
    s = p;
    return 0;
}

编译报错

三、继承中的作用域

1.隐藏规则

(1)基类和派生类都有独立的作用域
(2)基类和派生类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,称为隐藏(派生类成员函数中,可以用基类::基类成员显示访问基类的同名成员)
(3)只要函数名相同就构成隐藏
(4)实践中最好不要在继承体系里面定义同名成员

// Student的_num和Person的_num构成隐藏关系
class Person
{
protected:
    string _name = "张三"; // 姓名
    int _num = 111; // ⾝份证号
};
class Student : public Person
{
public:
    void Print()
    {
        cout << "姓名:" << _name << endl;
        cout << "身份证号:" << Person::_num << endl;//111
        cout << "学号:" << _num << endl;//999
    }
protected:
    int _num = 999; // 学号
};
int main()
{
    Student s1;
    s1.Print();
    return 0;
};

四、派生类的默认成员函数

1.4个常见的默认成员函数

(1)派生类的构造函数必须调用基类的构造函数初始化基类那部分成员,如果基类没有默认构造,则必须在派生类构造函数初始化列表阶段显示调用
(2)一般构造函数需要自己写,严格说拷贝构造,赋值和析构用系统生成的就够了,除非申请了资源。自己写构造/拷贝构造/赋值时,要显示调父类的
(3)派生类的operator=隐藏了基类的operator=,显示调用基类的operator=时要指定基类作用域
(4)派生类的析构函数:在被调用完成之后自动调用基类的析构函数清理基类成员,保证先子后父
(5)派生类对象初始化先调用基类构造再调派生类构造
(6)因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(详见多态),编译器会对析构函数名特殊处理,处理成destructor(),基类析构函数不加virtual的情况下,基类和派生类析构函数构成隐藏关系

//派生类的默认成员函数
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:
    Student(const char* name, int num)
        : Person(name)
        , _num(num)
    { cout << "Student()" << endl; }
    Student(const Student& s)
        : Person(s)
        : _num(s._num)
    { cout << "Student(const Student& s)" << endl; }
    Student& operator = (const Student& s)
    {
        cout << "Student& operator= (const Student& s)" << endl;
        if (this != &s)
        {
            // 构成隐藏,所以需要显示调用
            Person::operator =(s);
            _num = s._num;
        }
        return *this;
    }
    ~Student()
    { cout << "~Student()" << endl; }
protected:
    int _num; //学号
};
int main()
{
    Student s1("jack", 18);
    Student s2(s1);
    Student s3("rose", 17);
    s1 = s3;
    return 0;
}

2.实现一个不能被继承的类

(1)方法1:将父类的构造函数私有,子类中无法调用父类的构造函数
(2)方法2:C++11中新增final关键字【最终类】,用final修饰父类,则子类无法继承

// C++11的方法
class Base final
{
public:
    void func5() { cout << "Base::func5" << endl; }
protected:
    int a = 1;
    //private:
    // C++98的⽅法
    /*Base()
    {}*/
};

class Derive :public Base
{
    void func4() { cout << "Derive::func4" << endl; }
protected:
    int b = 2;
};

int main()
{
    Base b;
    Derive d;
    return 0;
}

五、继承与友元

友元关系不能能被继承,也就是说父类的友元不能访问子类的private和protected成员

class Student;
class Person
{
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name; // 姓名
};
class Student : public Person
{
protected:
    int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;//可以访问
    cout << s._stuNum << endl;//不能访问,除非Display也变成Student的友元
}
int main()
{
    Person p;
    Student s;
    // 编译报错:error C2248: “Student::_stuNum”: 无法访问 protected 成员
    // 解决方案:Display也变成Student的友元即可
    Display(p, s);
    return 0;
}

六、继承与静态成员

父类定义了static成员,则整个继承体系里面只有一个这样的成员,只有一个static成员实例
七、多继承及其菱形继承问题

1.继承模型

(1)单继承:只有一个直接父类
(2)多继承:有两个或以上直接父类,多继承对象在内存中的模型:先继承的父类在前面,后继承的父类在后面,子类成员放到最后面
(3)菱形继承:造成数据冗余和二义性。在Assistant对象中Person成员会有两份,实践中不建议设计菱形继承
单继承与多继承
菱形继承

2.虚继承

虚继承可以用来解决数据冗余和二义性的问题

class Person 
{ 
public: 
    string _name; // 姓名  
}; 
// 使⽤虚继承
class Teacher : virtual public Person 
{ 
protected: 
    int _id; // 职工编号 
}; 
// 教授助理 
class Assistant : public Student, public Teacher 
{ 
protected: 
    string _majorCourse; // 主修课程 
}; 
int main() 
{ 
    // 使用虚继承,可以解决数据冗余和二义性 
    Assistant a;////Assitant里面的_name只有一份,下面代码执行后_name都会改变
    a._name = "peter"; 
    a.Student::_name = "张三";
    a.Teacher::_name = "李四";
    return 0;  
}

3.IO库中的菱形虚拟继承

介绍一下:标准IO库里面用到了菱形虚拟继承
IO库中的菱形虚拟继承

八、继承和组合

  • public继承是is-a关系,即每个子类对象都是一个父类对象
  • 组合是has-a关系,若B组合了A,则每个B对象中都有一个A对象
  • 继承的复用模式:白箱复用——父类的内部细节对子类可见。继承一定程度上破坏了父类的封装,父类的改变,对子类有很大的影响,父类和子类的依赖关系很强,耦合度高
  • 组合的复用模式:黑箱复用——对象的内部细节不可见
  • 优先使用组合,而不是继承,实际上应该尽量多用组合。组合的耦合度低,代码维护性好,但也不太绝对,类之间的关系适合继承就用继承,此外要实现多态,也必须要继承。如果类之间的关系即适合is-a也适合has-a,就用组合
// Tire(轮胎)和Car(⻋)更符合has-a的关系 
class Tire
{
protected:
    string _brand = "Michelin"; // 品牌 
    size_t _size = 17; // 尺⼨ 
};
class Car
{
protected:
    string _colour = "⽩⾊"; // 颜⾊ 
    string _num = "陕ABIT00"; // ⻋牌号 
    Tire _t1; // 轮胎 
    Tire _t2; // 轮胎 
    Tire _t3; // 轮胎 
    Tire _t4; // 轮胎 
};
class BMW : public Car
{
public:
    void Drive() { cout << "BMW" << endl; }
};

// Car和BMW/Benz更符合is-a的关系 
class Benz : public Car
{
public:
    void Drive() { cout << "Benz" << endl; }
};

template<class T>
class vector
{
};

// stack和vector的关系,既符合is-a,也符合has-a 
template<class T>
class stack : public vector<T>
{
};

template<class T>
class stack
{
public:
    vector<T> _v;
};
int main()
{
    return 0;
}

网站公告

今日签到

点亮在社区的每一天
去签到