【C++】继承详解——基类/派生类、作用域、默认函数、菱形继承(超详细)

文章目录

一、继承开篇

前面我们学了类和对象、模板,今天我们进入面向对象最核心的特性之一:继承

继承是类层次的复用,可以让我们在不改动原有类的基础上扩展新功能,是C++面试、工程开发必学知识点。如果还没学过类与对象、封装,建议先看前面的文章打牢基础,再来学继承会轻松很多

本篇内容非常细,我会一一把继承概念、定义、赋值转换、作用域隐藏、默认成员函数、友元、静态、多继承、菱形继承、虚继承、继承与组合全部讲透


二、继承的概念及定义

1. 继承是什么

继承就是:
在已有类的基础上,扩展属性和函数,生成新的类。

  • 原来的类:基类 / 父类
  • 新的类:派生类 / 子类

没有继承之前,我们写StudentTeacher会出现大量重复代码:姓名、地址、电话、身份认证函数全都要写两遍,非常冗余

而有了继承,我们就可以把公共部分抽到Person,让StudentTeacher继承它,就能直接复用,不用重复写,定义如下:

cpp 复制代码
class Person
{
public:
    void identity()
    {
        cout << "身份认证: " << _name << endl;
    }

protected:
    string _name = "张三";
    string _address;
    string _tel;
    int _age = 18;
};

// Student 公有继承 Person
class Student : public Person
{
public:
    void study()
    {}

protected:
    int _stuid; // 学号
};

// Teacher 公有继承 Person
class Teacher : public Person
{
public:
    void teaching()
    {}

protected:
    string _title; // 职称
};

然后我们就可以直接使用:

cpp 复制代码
int main()
{
    Student s;
    Teacher t;

    s.identity(); // 直接用父类的函数
    t.identity();
    return 0;
}

这就是继承最直观的作用:复用代码,减少冗余


2. 继承定义格式

最常用的是公有继承

cpp 复制代码
class 派生类 : 继承方式 基类

继承方式有三种:

  • public(最常用)
  • protected
  • private

3. 继承后成员访问权限变化(超级重要)

这张表一定要背下来:

基类成员 public继承 protected继承 private继承
public public protected private
protected protected protected private
private 不可见 不可见 不可见

我给你总结成最简单的口诀:

  1. 基类 private 成员,无论怎么继承,在子类都不可见
  2. 访问权限 = 取更小的那个
    public > protected > private
  3. 实际开发大部分用public 继承,另外两种几乎不用

示例如下:

cpp 复制代码
class Person
{
public:
    void Print()
    {
        cout << _name << endl;
    }

protected:
    string _name;

private:
    int _age;
};

// 公有继承
class Student : public Person
{
protected:
    int _stunum;
};
  • Print() 在子类里是 public
  • _name 在子类里是 protected
  • _age 在子类里不可见

三、基类和派生类的赋值转换(切片/切割)

public 继承下,派生类对象可以赋值给基类对象/指针/引用

这叫切片 / 切割:把派生类里"属于基类"的那部分切出来用,示例类如下:

cpp 复制代码
class Person
{
protected:
    string _name;
    string _sex;
    int _age;
};

class Student : public Person
{
public:
    int _No;
};

切片的使用如下:

cpp 复制代码
int main()
{
    Student sobj;

    // 子类对象 → 父类指针/引用/对象 都可以
    Person* pp = &sobj;
    Person& rp = sobj;
    Person pobj = sobj;

    // 父类 → 子类 不行!
    // sobj = pobj; 报错
    return 0;
}

记住:子类可以自动转父类,父类不能转子类,这在后面的多态很重要,我们先暂时把切片介绍到这里,到多态我们再详细讲解切片的作用


四、继承中的作用域(隐藏 / 重定义)

继承里有一个超级重要的规则:同名隐藏

规则:

  1. 基类和子类是两个独立作用域
  2. 同名成员(变量/函数)会构成隐藏
  3. 子类会屏蔽父类的同名成员,直接访问只会访问到子类的成员
  4. 想访问父类同名成员必须加 类名::

1. 成员变量隐藏

cpp 复制代码
class Person
{
protected:
    string _name = "小凡";
    int _num = 111; // 身份证号
};

class Student : public Person
{
public:
    void Print()
    {
        cout << "姓名:" << _name << endl;
        cout << "身份证:" << Person::_num << endl; // 必须指定类域
        cout << "学号:" << _num << endl;
    }

protected:
    int _num = 999; // 学号,和父类同名,构成隐藏
};

这里父类和子类都有_num成员,那么就会构成隐藏,默认访问子类的_num,而不能直接访问到父类的_num,除非使用访问时加::

2. 成员函数隐藏

只要函数名相同,就构成隐藏,不管参数!,这也是最容易踩的坑:

cpp 复制代码
class A
{
public:
    void func()
    {
        cout << "func()" << endl;
    }
};

class B : public A
{
public:
    void func(int i)
    {
        cout << "func(int)" << i << endl;
    }
};

这里父类和子类都有func函数,虽然参数不同,但是还是构成了隐藏,默认调用子类,如果你的本意不是这样,就注意不要让父类和子类的成员函数名相同,上述代码的调用结果:

cpp 复制代码
B b;
b.func(10); // 正确
// b.func();  // 报错!父类func被隐藏了
b.A::func(); // 必须加类域才能访问

结论:继承体系里尽量不要写同名成员, 除非你本意就是要隐藏父类成员函数


五、派生类的默认成员函数(超级重点)

子类的6个默认成员函数,必须调用父类对应的函数

我给你总结最核心的7条:

  1. 子类构造 必须先调用父类构造
  2. 子类拷贝构造 必须先调用父类拷贝构造
  3. 子类赋值重载 必须先调用父类赋值重载
  4. 子类析构 自动调用父类析构,不用我们写
  5. 构造顺序:先父后子
  6. 析构顺序:先子后父
  7. 析构函数名会被编译器统一处理,不加virtual就构成隐藏

1. 构造函数

如果父类没有默认构造,子类必须在初始化列表显式调用

cpp 复制代码
class Person
{
public:
    Person(const char* name)
        : _name(name)
    {
        cout << "Person()" << endl;
    }

protected:
    string _name;
};

class Student : public Person
{
public:
    // 必须初始化列表调用父类构造
    Student(const char* name, int num)
        : Person(name)
        , _num(num)
    {
        cout << "Student()" << endl;
    }

protected:
    int _num;
};

2. 拷贝构造

cpp 复制代码
Student(const Student& s)
    : Person(s) // 切片调用父类拷贝构造
    , _num(s._num)
{
    cout << "Student拷贝构造" << endl;
}

3. 赋值重载

cpp 复制代码
Student& operator=(const Student& s)
{
    if (this != &s)
    {
        Person::operator=(s); // 必须指定类域,否则隐藏
        _num = s._num;
    }
    return *this;
}

4. 析构函数

子类析构结束后,自动调用父类析构,我们不用写。

cpp 复制代码
~Student()
{
    cout << "~Student()" << endl;
    // 自动调用 ~Person()
}

5. 写一个不能被继承的类

两种方法:

  1. C++98:把父类构造私有
    子类构造无法调用,就不能实例化
  2. C++11:final 关键字
cpp 复制代码
// C++11 最简单
class Base final
{
};

// 报错:无法继承final类
// class Derive : public Base
// {};

六、继承与友元

友元关系不能继承!

父类的友元函数,不能访问子类的私有/保护成员

cpp 复制代码
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的友元


七、继承与静态成员

基类的 static 成员,整个继承体系只有一份!

所有子类和父类共享同一个静态变量

cpp 复制代码
class Person
{
public:
    static int _count;
};

int Person::_count = 0;

class Student : public Person
{};

使用:

cpp 复制代码
int main()
{
    Person p;
    Student s;

    // 地址相同,是同一个变量
    cout << &p._count << endl;
    cout << &s._count << endl;

    // 都可以访问
    Person::_count++;
    Student::_count++;
    return 0;
}

八、多继承与菱形继承(C++最复杂的点)

1. 概念

  • 单继承:一个子类只有一个父类
  • 多继承:一个子类有两个或以上父类
  • 菱形继承:多继承的特殊情况,有共同祖先
cpp 复制代码
class Person {};
class Student : public Person {};
class Teacher : public Person {};
// 菱形继承
class Assistant : public Student, public Teacher {};

2. 菱形继承的两大问题

  1. 数据冗余Person成员存两份
  2. 二义性 :访问_name不知道是哪一个
cpp 复制代码
Assistant a;
// a._name; 报错:不明确

可以指定类域解决二义性,但解决不了冗余

cpp 复制代码
a.Student::_name;
a.Teacher::_name;

3. 虚继承(解决菱形继承问题)

中间层virtual 继承:

cpp 复制代码
class Student : virtual public Person {};
class Teacher : virtual public Person {};

这样:

  • Person成员只存一份
  • 无冗余、无二义性
cpp 复制代码
Assistant a;
a._name = "peter"; // 正确

注意:虚继承只用来解决菱形继承,平时不要乱用。


九、继承与组合(面试高频)

最后我们讲一个设计思想:优先使用组合,少用继承

  • 继承:is-a 关系
    Student is a Person
  • 组合:has-a 关系
    Car has a Tire

1. 继承(白箱复用)

  • 父类对子类透明
  • 耦合度高
  • 父类改,子类全受影响

2. 组合(黑箱复用)

  • 内部不可见
  • 耦合度低
  • 维护性极强
  • 优先使用

示例:

cpp 复制代码
class Tire {};

class Car
{
protected:
    Tire _t1;
    Tire _t2;
    Tire _t3;
    Tire _t4;
};

stack 和 vector 更适合组合,不适合继承。


十、继承总结

  1. 继承是类层次复用,减少冗余
  2. 继承方式大部分只用 public
  3. 基类private成员在子类不可见
  4. 子类可以赋值给父类(切片),父类不能转子类
  5. 同名成员构成隐藏,必须加类域访问
  6. 子类默认函数必须调用父类对应函数
  7. 构造先父后子,析构先子后父
  8. 友元不能继承,static全类族共享
  9. 菱形继承用虚继承解决
  10. 设计优先组合,少用继承

那么今天关于C++ 继承 就全部讲完了,内容非常多,大家多敲代码多体会。

有什么不懂欢迎私信问我,我会及时做出解答!

下一篇我们开始学习多态,面向对象最后的核心知识点,敬请期待吧!

bye~

相关推荐
zmsofts1 小时前
IntelliJ IDEA)因为内存不足而崩溃
java·ide·intellij-idea
小侯不躺平.1 小时前
C++ Boost库【2】 --stringalgo字符串算法
linux·c++·算法
Dlrb12111 小时前
C语言-字符串指针与函数指针
java·c语言·前端
铅笔小新z1 小时前
【C语言】数据类型和变量
c语言·开发语言
萝卜白菜。1 小时前
通过cmdline-jmxclient.jar采集TongWeb8.0监控值
java·jar
weixin_537217061 小时前
职场沟通资源合集
经验分享
code_whiter1 小时前
C++11(stack和queue)
开发语言·c++
流年如夢1 小时前
二叉树详解
c语言·数据结构·算法
最后一支迷迭香1 小时前
苹果的MacOS系统适合做Java开发吗
java·开发语言·macos