首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++进阶】2.继承与多态详解

【C++进阶】2.继承与多态详解

作者头像
用户11952558
发布2026-01-09 14:10:38
发布2026-01-09 14:10:38
1760
举报

1. 继承

在原有类上扩展。

首先,假设在一个系统里有老师、学生的信息。

代码语言:javascript
复制
class Student
{
public:
    void identity()
    {
        // ...
    }
    void study()
    {
        // ...
    }
protected:
    string _name = "peter";  // 姓名
    string _address;         // 地址
    string _tel;             // 电话
    int _age = 18;           // 年龄
    int _stuid;              // 学号
};

class Teacher
{
public:
    void identity()
    {
        // ...
    }
    void teaching()
    {
        // ...
    }
protected:
    string _name = "张三";   // 姓名
    int _age = 18;           // 年龄
    string _address;         // 地址
    string _tel;             // 电话
    string _title;           // 职称
};

但是,如姓名、地址等都是两者共同的,我们就可以将相同的放在一个类 Person 里,师生可以共同继承这个类。

代码语言:javascript
复制
class Person
{
public:
    void identity()
    {
        // ...
    }
protected:
    std::string _name = "张三";  // 姓名
    int _age = 18;               // 年龄
    std::string _address;        // 地址
    std::string _tel;            // 电话
};

class Student : public Person
{
public:
    void study()
    {
        // ...
    }
protected:
    int _stuid;  // 学号
};

class Teacher : public Person
{
public:
    void teaching()
    {
        // ...
    }
protected:
    std::string _title;  // 职称
};

继承的基础写法:class Student : public Person,在类之后加一个类。

Person 叫父类(基类),Student 叫子类(派生类)。

继承关系
1. 父类私有在子类不可见

我们将 protected 改为 private,会报错:"Person::_name": 无法访问 private 成员(在"Person"类中声明)

Protected 大概率为专门为了继承而来的。

2. 父类其它成员在子类访问权限 = min(父类限定符,子类限定符)

下面是表格。

栈的继承写法(继承 vector)

由于栈是容器适配器,因此可以直接继承,复用 vector。

代码语言:javascript
复制
template <class T>
class Stack : public std::vector<T>
{
public:
    void push(const T& val)
    {
        push_back(val);
    }
    void pop()
    {
        pop_back();
    }
    const T& top()
    {
        return back();
    }
    bool empty()
    {
        return empty();
    }
};

但是,这样写会报错:"pop_back": 找不到标识符

原因:按需实例化

  1. Stack<int> st; 初始化 vector 的构造函数。
  2. st.push(1); 仅初始化了 push,在 push 里没有找到 push_back,就报错。

解决方案: 在开始时就告诉编译器:push_back 是 vector 里的,叫它一起实例化掉。

代码语言:javascript
复制
template <class T>
class Stack : public std::vector<T>
{
public:
    void push(const T& val)
    {
        std::vector<T>::push_back(val);
    }
    void pop()
    {
        std::vector<T>::pop_back();
    }
    const T& top()
    {
        return std::vector<T>::back();
    }
    bool empty()
    {
        return std::vector<T>::empty();
    }
};

用宏修改容器

代码语言:javascript
复制
#define CONTAINER std::list<T>

void push(const T& val)
{
    CONTAINER::push_back(val);
}
子对象可以赋值给父对象/指针/引用
代码语言:javascript
复制
Student s1;
Person p1 = s1;
Person& p2 = s1;
Person* p3 = &s1;

三种写法都可以。

但是,不是强制类型转换。如果是的话,Person& p2 = s1; 语句将会报错,因为 s1 强转后变为临时对象,有常性,因此要用 const 引用,而代码中不用。

父对象可以强转给子对象

代码语言:javascript
复制
Person p1;
Student* s1 = (Student*)&p1;
继承中的注意点
1. 子类父类变量同名会屏蔽父类(只是屏蔽,不是消失)
代码语言:javascript
复制
class Father
{
protected:
    int a = 2;
};

class Child : public Father
{
public:
    void print()
    {
        std::cout << a << std::endl;  // 输出 1,调用子类的 a
    }
protected:
    int a = 1;
};

就近调用变量。

std::cout << Father::a << std::endl; 加上以上语句能打印父类的 a

2. 成员函数同名会直接屏蔽(不构成重载,因为不在同一作用域)
代码语言:javascript
复制
class Father
{
public:
    void print()
    {
        std::cout << "father" << std::endl;
    }
};

class Child : public Father
{
public:
    void print(int a)
    {
        std::cout << "child" << std::endl;
    }
};

如果调用不传参版本的,会报错。

父类、子类默认函数关系
1. 构造

父类如果有构造函数,可以在子类初始化列表调用。

代码语言:javascript
复制
class Father
{
public:
    Father(int a1, const char* s1)
        : _a1(a1)
        , _s1(s1)
    {}
protected:
    int _a1;
    string _s1;
};

class Child : public Father
{
public:
    Child(int a1, int a2, const char* s1, const char* s2)
        : _a2(a2)
        , _s2(s2)
        , Father(a1, s1)
    {}
protected:
    int _a2;
    string _s2;
};

如果没有构造函数,想用编译器生成的,那也要在初始化列表写空括号,此时自定义类型调用默认构造,内置类型随机。

代码语言:javascript
复制
class Father
{
public:
    //Father(int a1, const char* s1)
    //    : _a1(a1)
    //    , _s1(s1)
    //{}
protected:
    int _a1;
    string _s1;
};

class Child : public Father
{
public:
    Child(int a1, int a2, const char* s1, const char* s2)
        : _a2(a2)
        , _s2(s2)
        , Father()
        /*, Father(a1, s1)*/
    {}
protected:
    int _a2;
    string _s2;
};
2. 拷贝构造

在我们的 FatherChild 类中,自动生成的够用,其实可以不写。

代码语言:javascript
复制
Child(const Child& c)
    : _a2(c._a2)
    , _s2(c._s2)
    , Father(c)
{}

会切片,将 Child 类里的 Father 类的元素切出来构造。

要是父类没写拷贝构造,编译器就会自动生成,进行浅拷贝。

3. 赋值拷贝重载
代码语言:javascript
复制
Child& operator=(const Child& c)
{
    _a2 = c._a2;
    _s2 = c._s2;
    Father::operator=(c);
    return *this;
}

和拷贝函数差不多。

注意: 加上 Father:: 指定作用域,否则会无限递归。

4. 析构
代码语言:javascript
复制
~Child()
{
    Father::~Father();
}

也记得加作用域 Father::,否则两个函数的析构底层名字一样,会构成重载而报错。

但如果这样直接调用,Father 明明只有 2 个元素,却析构了 4 次。

原因: 父类的析构会在子类析构完后自动调用。因此不用写。

这么做可以确保先定义后析构。

同理,子类初始化先调用父再调用子,和构造列表出现的顺序无关。

总之,将父类看成一个自定义类型,子类调用。

不可继承的子类
1. 构造函数私有的类

父类私有成员在子类不可见,子类无法调用父类构造函数。

缺点: 不去定义派生类对象不会报错。

代码语言:javascript
复制
class Father
{
protected:
    int _a1;
    string _s1;
private:
    Father(int a1, string s1)
        : _a1(a1)
        , _s1(s1)
    {}
};

class Child : public Father
{
public:
    //Child(int a1, string s1, int a2, string s2)
    //    : Father(a1, s1)
    //    , _a2(a2)
    //    , _s2(s2)
    //{}
protected:
    //int _a2;
    //string _s2;
};

比如这样写,是不会报错的。

2. final(C++11)

在父类最后写上 final 关键字。

代码语言:javascript
复制
class Father final
{
    // ...
};
友元关系不能被继承

父类的友元不是子类的友元。

解决方式: 再声明一次友元。

代码语言:javascript
复制
class Student;

class Person
{
public:
    Person(string name = "sss")
        : _name(name)
    {}
    friend void Display(const Person& p, const Student& s);  // 友元关系不能被继承
protected:
    string _name;  // 姓名
};

class Student : public Person
{
public:
    Student(string name = "ppp", int stuNum = 1)
        : Person(name)
        , _stuNum(stuNum)
    {}
    friend void Display(const Person& p, const Student& s);
protected:
    int _stuNum;
};

void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;
    cout << s._stuNum << endl;
}

由于以上代码无论是 Person 还是 Student 都要提到对方,因此要加前置声明。

父类静态变量

父类和子类为同一个。

代码语言:javascript
复制
class Person
{
public:
    static int _a;
};

int Person::_a = 1;

class Student : public Person
{};

int main()
{
    Person p;
    Student s;
    cout << &p._a << endl;
    cout << &s._a << endl;
    // 地址相同
}

代码中,&p._a&s._a 地址是一样的,为同一个。

继承模型

1. 单继承

即普通的继承。

2. 多继承

一个子类有 2 个及以上的父类。

代码语言:javascript
复制
class Student
{
protected:
    int _num;
};

class Teacher
{
protected:
    int _id;
};

class Assistant : public Student, public Teacher
{
protected:
    string _majorCourse;
};

学生和老师都算人员。

3. 菱形继承

如果在学生和老师上面加一个 Person,就变成了菱形继承。

代码语言:javascript
复制
class Person
{
protected:
    string _name;
};

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

class Teacher : public Person
{
protected:
    int _id;
};

class Assistant : public Student, public Teacher
{
protected:
    string _majorCourse;
};

会出现问题: 数据冗余,二义性。

  • 冗余: 每个 Assistant 对象包含:
    • Student 继承来的 Person::_name
    • Teacher 继承来的另一个 Person::_name
  • 二义性: 当访问 _name 时,编译器不知道使用哪个版本。

冗余解决方式: 加虚拟继承(virtual)。

规则: 谁有冗余数据就加谁(不能只加一个)。

在图中,要给 BC 加上虚拟继承。​​​​​​​

多继承的指针偏移

先继承的在前面。

代码语言:javascript
复制
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;
    // p1 == p3 != p2
}

P1p2p3 指针的关系。 由于 Derive 先继承 Base1,因此 d 的指针和 b1 的指针是一个位置,b2b1 后。 因此 p1 == p3 != p2

继承与组合
  • 继承: class Stack : public std::list<T>
  • 组合: class Stack { std::list<T> _lt; }
  • Public 继承is-a 关系,每个子类对象都是一个父类对象。
  • 组合has-a 关系,每个 B 都有一个 A 对象。
  • 继承是白箱复用:父类的保护、公有对子类都透明,依赖性强。
  • 组合为黑箱复用:内部结构不可见,耦合度低。

最好低耦合高内聚,因此优先组合。

多态

  • 静态多态: 函数重载和函数模板。
  • 动态多态: 传不同对象就会有不同行为。

必须条件:

  1. 指针或引用调用虚函数(父类必须虚,子类随便,因为父类的 virtual 会继承下来)。
  2. 子类必须对父类函数重写或覆盖(返回类型、函数名、参数列表都相同)。

写法:

代码语言:javascript
复制
class Person
{
public:
    virtual void ticket()
    {
        cout << "全价" << endl;
    }
};

class Student : public Person
{
public:
    void ticket()
    {
        cout << "打折" << endl;
    }
};

void func(Person* p)
{
    p->ticket();
}

int main()
{
    Student* s = new Student;
    Person* p = new Person;
    func(s);  // 输出:打折
    func(p);  // 输出:全价
}

这样子,只需要传入父或子类的指针,就可以自动"识别",调用对应的函数。​​​​​​​

若不满足多态,调用父类;满足,看指向对象。 去掉 virtual,调用父类。​​​​​​​

以下程序运行结果
代码语言:javascript
复制
class A
{
public:
    virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
    virtual void test() { func(); }
};

class B : public A
{
public:
    void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};

int main()
{
    B* p = new B;
    p->test();
    return 0;
}

结果: B->1

原因:

  1. test 调用 func 虚函数,此时由于 pB 的指针,因此 this 指的是 B,调用 Bfunc
  2. 由于编译时,编译器就能看到 A 中调用 func 默认值为 1,而此时 B 调用 func 本质上是在 A 中调用的,因此值为 1,不为 B 的默认值 0。
协变(了解)
代码语言:javascript
复制
class A {};
class B : public A {};

class Person
{
public:
    virtual A* ticket()
    {
        cout << "全价" << endl;
        return nullptr;
    }
};

class Student : public Person
{
public:
    virtual B* ticket()
    {
        cout << "半价" << endl;
        return nullptr;
    }
};

void buyTicket(Person* p)
{
    p->ticket();
}

有两组继承。

  • 组二继承的父类返回组一继承的父类。
  • 组二继承的子类返回组一继承的子类。

这样一样能有多态的效果。

析构函数的重写

在前面,讲到了父类和子类的析构函数名称会被处理成相同。 因此只要父类的析构为虚,子类和父类的析构就构成重写。

代码语言:javascript
复制
class A
{
public:
    ~A()
    {
        cout << "~A()" << endl;
    }
};

class B : public A
{
public:
    ~B()
    {
        cout << "~B()" << endl;
        delete _p;
    }
protected:
    int* _p = new int[10];
};

上文中,我们提到:子类的析构函数会自动调用父类的,以确保先子后父的析构。 但是正常父类指针不会调用子类的析构。 因此在上面的程序中写:A* a2 = new B; delete a2; 会发现只调用了 A 的析构,没有调用 B 的析构,这样就会造成内存泄露。​​​​​​​

解决方案:A 的析构函数上加 virtual。 这样就会调用子类的析构。

那上文父类子类的析构函数名字处理相同的原因就知道了:为了让析构函数可以同时调用。

Override、final 关键字(C++11)

由于有时不确定是否函数被重写,因此可以加入这个关键字。

代码语言:javascript
复制
class A
{
public:
    virtual ~A()
    {
        cout << "~A()" << endl;
    }
};

class B : public A
{
public:
    ~B() override  // 确保构成重写
    {
        cout << "~B()" << endl;
        delete _p;
    }
protected:
    int* _p = new int[10];
};

这样就能确保先构成重写,再运行,降低内存泄露风险。

final:不可被重写。

重写、重载、隐藏
  1. 重载: 1. 同个作用域 2. 函数名同,参数不同。
  2. 重写(覆盖): 1. 父子作用域 2. 函数名/参数/返回值相同(协变除外) 3. 为虚函数。
  3. 隐藏: 父子作用域,函数名相同情况下,不是重写就是隐藏。
纯虚函数和抽象类
代码语言:javascript
复制
class A
{
public:
    virtual void func() = 0;  // 纯虚函数
};

在函数后面加一个 =0 就代表它是纯虚函数,必须在派生类中实现抽象类: 有纯虚函数的类。

使用方式: 在子类定义这个函数。

代码语言:javascript
复制
class A
{
public:
    virtual void func() = 0;
};

class B : public A
{
public:
    virtual void func()
    {
        cout << "可以" << endl;
    }
};

多态原理

类的大小
代码语言:javascript
复制
class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
protected:
    int _b = 1;
    char _ch = 'x';
};

这个类的大小为(32位):12。 由:整型 _b,字符 _ch,以及一个指针构成。​​​​​​​

这个指针为:函数指针数组的指针。 人话,即为虚函数表的指针。

在继承中,无论父类子类的虚函数都会被整合成一个虚函数表,里面存着函数指针。 运行时,根据指向对象的虚函数表中找到虚函数地址,进行调用,因此,只有在运行时才能确定地址。

动态绑定和静态绑定
  • 静态绑定: 不满足多态条件,在编译时就确定调用地址。
  • 动态绑定: 多态,运行到才知道地址。
虚函数表问题
  1. 父类虚函数表放父类虚函数地址。
  2. 子类的虚函数表先会继承父类的虚函数表,但和父类的虚函数表不是同一个(地址不同),可以类比于父类和子类的元素,名字相同,但不是同一个。
  3. 子类虚函数表继承父类的虚函数表后,有虚函数,会在生成一个虚函数,并覆盖父类继承下来的地址,因此子类虚函数表包含:继承父类的、子类重写的、子类生成的地址。
  4. 虚函数存储在常量区(代码段),因为它本质上是一串指令,只是地址存在虚函数表中。
虚函数表存储位置
代码语言:javascript
复制
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";

printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);

B* b1 = new B;
printf("虚表指针:%p\n", *(int**)b1);  // 取b1对象的前4/8个字节(虚表指针)

结果看,b1 的前 4 个字节的地址小于常量区,大于静态区,因此可以推测在 VS 下,处于常量区。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-09,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 继承
    • 继承关系
      • 1. 父类私有在子类不可见
      • 2. 父类其它成员在子类访问权限 = min(父类限定符,子类限定符)
    • 栈的继承写法(继承 vector)
    • 子对象可以赋值给父对象/指针/引用
    • 继承中的注意点
      • 1. 子类父类变量同名会屏蔽父类(只是屏蔽,不是消失)
      • 2. 成员函数同名会直接屏蔽(不构成重载,因为不在同一作用域)
    • 父类、子类默认函数关系
      • 1. 构造
      • 2. 拷贝构造
      • 3. 赋值拷贝重载
      • 4. 析构
    • 不可继承的子类
      • 1. 构造函数私有的类
      • 2. final(C++11)
    • 友元关系不能被继承
    • 父类静态变量
  • 继承模型
    • 1. 单继承
    • 2. 多继承
    • 3. 菱形继承
    • 多继承的指针偏移
    • 继承与组合
  • 多态
    • 以下程序运行结果
    • 协变(了解)
    • 析构函数的重写
    • Override、final 关键字(C++11)
    • 重写、重载、隐藏
    • 纯虚函数和抽象类
  • 多态原理
    • 类的大小
    • 动态绑定和静态绑定
    • 虚函数表问题
    • 虚函数表存储位置
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档