从零开始C++----七.继承及相关模型和底层(上篇)

系列文章

从零开始C++----二.(下篇)模版进阶与编译全过程的复习


提示:可以查看目录先了解大致内容哦

文章目录

系列文章

前言

一、继承的概念

二、继承的定义

1.定义格式

2.继承基类成员访问方式的变化

3.继承类模板

三、基类和派生类间的转换

四、继承中的作用域

1.隐藏

[1.1 隐藏规则](#1.1 隐藏规则)

1.2相关例题考察

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

[1. 4个常见默认成员函数](#1. 4个常见默认成员函数)

2.final关键字

3.继承与友元

4.继承与静态成员

六、继承与组合


前言

提示:此章只讲继承的上半部分哦!


提示:以下是本篇文章正文内容

一、继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们

在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的

类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前

我们接触的函数层次的复用,继承是类设计层次的复用。

下面我们看到没有继承之前我们设计了两个类 Student 和 Teacher,Student 和 Teacher 都

有姓名 / 地址 / 电话 / 年龄等成员变量,都有 identity 身份认证的成员函数,设计到两个类里面就

是冗余的。当然他们也有一些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有

成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课

cpp 复制代码
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;         // 职称
};

int main()
{
    return 0;
}

下面我们公共的成员都放到 Person 类中,Student 和 Teacher 都继承 Person,就可以复用这些成员,就不需要重复定义了,省去了很多麻烦

cpp 复制代码
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; // 职称
};

int main()
{
    Student s;
    Teacher t;

    s.identity();
    t.identity();

    return 0;
}

通过以上的例子,我们可以很直观的感受到"继承"


二、继承的定义

1.定义格式

下面我们看到 Person 是基类,也称作父类。Student 是派生类,也称作子类。(因为翻译的原因,所以既叫基类 / 派生类,也叫父类 / 子类)

在定义的类名后面加个冒号,前面为派生类,后面为基类

2.继承基类成员访问方式的变化

对表格我们进行一下总结会发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min (成员在基类的访问限定符,继承方式),public > protected > private


1.基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它

2.基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为 protected。可以看出保护成员限定符是因继承才出现的

cpp 复制代码
#include <iostream>
using namespace std;

// 父类(基类)
class Person {
private:
    // 私有成员:只有自己能访问
    string _name = "张三";

protected:
    int _age = 20;
};

// 子类(派生类)------ 用 public 继承
class Student : public Person {
public:
    void test() {
        // ✔ 可以访问 protected 成员
        _age = 18;

        // ❌ 错误!不能访问 基类的 private 成员 _name
        // _name = "小明";  // 编译报错!
    }
};

int main() {
    Student s;
    // ❌ 类外面也访问不到 _name
    // s._name = "李四";

    // ❌ 类外面也访问不到 _age(protected)
    // s._age = 22;
    return 0;
}

4.使用关键字 class 时默认的继承方式是 private,使用 struct 时默认的继承方式是 public,不过最好显示的写出继承方式

5.在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承,也不提倡使用 protetced/private 继承,因为 protetced/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强


3.继承类模板

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

namespace CD
{
	//template<class T>
	//class vector
	//{};

	// stack和vector的关系,既符合is-a,也符合has-a
	template<class T>
	class stack : public std::vector<T>
	{
	public:
		void push(const T& x)
		{
			// 基类是类模板时,需要指定⼀下类域,
			// 否则编译报错:error C3861: "push_back": 找不到标识符
			// 因为stack<int>实例化时,也实例化vector<int>了
			// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
			vector<T>::push_back(x);
			//push_back(x);
		}

		void pop()
		{
			vector<T>::pop_back();
		}

		const T& top()
		{
			return vector<T>::back();
		}

		bool empty()
		{
			return vector<T>::empty();
		}
	};
}

int main()
{
	CD::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);

	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}

	return 0;
}

三、基类和派生类间的转换

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

cpp 复制代码
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;

    // 派生类对象可以赋值给基类的对象
    // 是通过调用后面会讲解的基类的拷贝构造完成的
    Person pobj = sobj;

    // 2. 基类对象不能赋值给派生类对象,这里会编译报错
    sobj = pobj;

    return 0;
}

基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针

是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI (Run-Time Type

Information) 的 dynamic_cast 来进行识别后进行安全转换

总结:

基类指针 / 引用 不能自动 转成派生类指针 / 引用

必须 手动强转

只有一种情况安全这个基类指针,本来就指向一个派生类对象

例子:

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

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

int main()
{
    Student sobj;

    // 1. 子类 → 父类(自动,安全,切片)
    Person* pp = &sobj;

    // 2. 父类指针 → 子类指针:必须强转
    Student* sp = (Student*)pp;

    // 转换成功,可以访问子类成员
    sp->_No = 12345;
}

从上面可知道:

✅ 安全情况

基类指针本来就指向子类对象:

cpp 复制代码
Student s;
Person* pp = &s;
Student* sp = (Student*)pp;  // 安全

❌ 危险情况

基类指针指向的是纯基类对象

cpp 复制代码
Person p;
Person* pp = &p;
Student* sp = (Student*)pp;  // 不安全!乱转
sp->_No = 10; // 直接越界、崩溃

更安全的转换:dynamic_cast

如果你的基类是多态类(有虚函数) ,可以用 dynamic_cast 让它运行时检查类型对不对

cpp 复制代码
Student* sp = dynamic_cast<Student*>(pp);

转换成功 → 返回有效指针,转换失败 → 返回 nullptr

、继承中的作用域

1.隐藏

子类里定义了和父类 同名的函数(不管参数一不一样),父类的这个函数就会被隐藏

隐藏 = 子类把父类的同名函数 "遮住了",在子类里直接调用,看不见父类版本

1.1 隐藏规则

  1. 在继承体系中,基类和派生类都有独立的作用域
  2. 派生类和基类中有同名成员时,派生类成员将屏蔽对基类同名成员的直接访问,这种情况叫隐藏。(在派生类成员函数中,可以使用 基类::同名成员 显式访问基类成员)
  3. 需要注意的是,如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意在实际开发中,继承体系里最好不要定义同名的成员
cpp 复制代码
// 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;
        cout << " 学号:" << _num << endl;
    }
protected:
    int _num = 999; // 学号
};

int main()
{
    Student s1;
    s1.Print();

    return 0;
}

1.2相关例题考察

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

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

A 和 B 类中的两个 func 构成什么关系()A. 重载 B. 隐藏 C. 没关系

| 对比维度 | 重载(Overload) | 隐藏(Hide / Name Hiding) |
| 作用域要求 | 必须在同一个作用域(同一个类、同一个命名空间、全局作用域) | 仅需函数名相同,与参数列表是否相同无关 |
| 返回值影响 | 不参与判断,仅靠返回值不同不能构成重载 | 不参与判断,仅靠函数名相同就会触发隐藏 |
| 调用方式 | 直接调用,编译器会根据传入的参数自动匹配最合适的版本 | 派生类对象直接调用,只能匹配到派生类自身的同名函数;若要调用基类版本,必须显式写 对象.基类名::函数名() |
| 编译器处理逻辑 | 编译期根据参数类型进行静态匹配 | 编译期优先在派生类作用域查找,找到同名函数后就不再向上查找基类 |
| 示例 | void func();void func(int); 在同一个类中 | 基类 void func();,派生类 void func(int); |

核心区分点 参数列表不同区分,目的是提供多种调用方式 作用域不同区分,目的是屏蔽基类的同名成员

从这张表格我们可以知道答案一定是"B",两者函数名都为"fun",但是作用域并不相同,一个在基类中,一个在派生类中

cpp 复制代码
int main()
{
    B b;
    b.fun(10);
    b.fun();

    return 0;
}

还是上面的条件:

程序的编译运行结果是什么()A. 编译报错 B. 运行报错 C. 正常运行

首先这一题的答案是"A"

核心原因是:基类 A::func() 被派生类 B::func(int) 隐藏了

编译器的查找规则:

当写 b.func() 时,编译器的查找顺序是:

  1. 先在 B 自己的作用域里找有没有叫 func 的函数
  2. 找到了 B::func(int),就不会再去基类 A 里找了
  3. 你调用的是无参的 func(),但 B 里只有带 int 参数的版本,参数不匹配,直接报错

那如何解决呢?

必须显式告诉编译器:我要调用基类的版本

cpp 复制代码
b.A::func(); // 加上基类作用域,就能找到A::func()了

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

1. 4个常见默认成员函数

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

1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用

2.派生类对象初始化先调用基类构造,再调用派生类构造

情况 1: 基类有默认构造函数(无参)

派生类构造函数不用显式调用,编译器自动调用基类构造

cpp 复制代码
class Person
{
public:
    // 默认构造函数(无参)
    Person()
    {
        cout << "Person()" << endl;
    }
};

class Student : public Person
{
public:
    // 派生类构造
    Student()
    {
        cout << "Student()" << endl;
    }
};

int main()
{
    Student s;
    return 0;
}

运行结果:

情况 2: 基类没有默认构造函数(只有带参构造)

这时候派生类必须在初始化列表显式调用基类构造,否则编译报错!

cpp 复制代码
class Person
{
public:
    // 只有带参构造,没有无参构造
    Person(const char* name)
    {
        cout << "Person()" << endl;
    }
};

class Student : public Person
{
public:
    // 必须显式调用基类构造
    Student()
        : Person("张三")  // 必须写在初始化列表!
    {
        cout << "Student()" << endl;
    }
};

运行结果:

要注意这里,显示调用基类构造的写法


3.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化

**情况 1:**基类有默认拷贝构造,派生类也用默认拷贝构造

这种情况编译器会自动帮你调用基类的拷贝构造,不用手动写:

cpp 复制代码
class Person
{
protected:
    string _name;
    int _age;
public:
    // 默认拷贝构造(编译器自动生成)
    Person(const Person& p) 
        : _name(p._name), _age(p._age) {}
};

class Student : public Person
{
protected:
    int _id;
public:
    // 派生类默认拷贝构造(编译器自动生成)
    // 它会自动先调用 Person(const Person&) 拷贝基类部分
    Student(const Student& s) 
        : Person(s)
        , _id(s._id) 
        {} // 编译器帮你写的代码就是这样
};

int main()
{
    Student s1;
    Student s2 = s1; // 调用派生类拷贝构造,自动完成基类+派生类成员的拷贝
}

**情况 2:**手动写了派生类的拷贝构造

这时候必须在初始化列表里显式调用基类的拷贝构造,否则基类成员会用默认构造初始化,而不是拷贝:

cpp 复制代码
class Person
{
protected:
    string _name;
    int _age;
public:
    Person(const string& name, int age) 
        : _name(name), _age(age) {}
    // 基类拷贝构造
    Person(const Person& p) 
        : _name(p._name), _age(p._age) {}
};

class Student : public Person
{
protected:
    int _id;
public:
    Student(const string& name, int age, int id) 
        : Person(name, age), _id(id) {}

    // 手动写派生类拷贝构造
    Student(const Student& s) 
        : Person(s), _id(s._id) // 关键:这里必须调用 Person(s),拷贝基类部分
    {
        cout << "Student拷贝构造" << endl;
    }
};

如果不写 : Person(s),编译器会尝试调用 Person() 初始化基类部分,而不是拷贝 s 里的基类成员,这就会导致基类数据丢失,拷贝出来的对象是错的


4.派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。需要注意的是派生类的 operator= 隐藏了基类的 operator=,所以显式调用基类的 operator=,需要指定基类作用域

派生类的 operator= 要完成两件事:

  1. 子类自己的成员赋值
  2. 父类的成员 也赋值父类那部分,必须调用父类的 operator= 才能完成
cpp 复制代码
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;
}

为什么必须写 Person::

因为子类的 operator= 和父类的 operator= 同名 → 构成隐藏 → 子类作用域里看不见父类的 operator=

这里赋值顺序无论是先父后子,还是先子后父都是没问题的,随便怎么写


5.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序

6.派生类对象析构清理先调用派生类析构,再调用基类的析构

这个需要记住:构造:先父 → 后子 析构:先子 → 后父

cpp 复制代码
class Person
{
public:
    ~Person() { cout << "~Person()" << endl; }
};

class Student : public Person
{
public:
    ~Student() { cout << "~Student()" << endl; }
};

int main()
{
    Student s;
    return 0;
}

运行结果:


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

这里来简单讲解一下,明明上面这两个析构函数长得明明都不一样呀,为什么却构成隐藏呢?

因为****析构函数的名字,编译器会偷偷统一改成 destructor(),所以父子类的析构函数天然同名


这张图怕大家看不懂解释一下,左边两个箭头意思是,派生类构造函数或者析构函数都会自动调用基类的构造函数或者基类的析构函数,右边的💙箭头从上往下是调用的顺序


2.final关键字

实现⼀个不能被继承的类,如何实现呢?
方法 1:将基类的构造函数设为私有派生类构造时必须调用基类的构造函数。如果基类的构造函数被私有化,派生类就无法访问、无法调用,最终导致派生类无法实例化出对象。
方法 2:使用 C++11 新增的 final 关键字 用 final 修饰基类后,派生类就 不能再继承这个基类,从根源上禁止继承

cpp 复制代码
#include <iostream>
using namespace std;

// 1. 用 final 修饰基类,表示这个类不能被继承
class Person final
{
public:
    void print()
    {
        cout << "我是 Person 类" << endl;
    }
};

// 2. 尝试继承 final 修饰的类
// 下面这行代码编译时会直接报错!
class Student : public Person
{
};

int main()
{
    Student s;
    return 0;
}

3.继承与友元

友元关系不能继承,也就是说基类友元不能访问派生类私有和保护成员

cpp 复制代码
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;
}

int main()
{
    Person p;
    Student s;

    // 编译报错:error C2248: "Student::_stuNum": 无法访问 protected 成员
    // 解决方案:让 Display 也成为 Student 的友元
    Display(p, s);

    return 0;
}

4.继承与静态成员

基类如果定义了 static 静态成员 ,那么在整个继承体系中只会存在唯一一个该成员实例。无论从这个基类派生出多少个派生类,所有派生类对象和基类对象,都共用这同一个 static 成员

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

int Person::_count = 0;

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

int main()
{
    Person p;
    Student s;

    // 这里的运行结果可以看到非静态成员_name的地址是不一样的
    // 说明派生类继承下来了,父派生类对象各有一份
    cout << &p._name << endl;
    cout << &s._name << endl;

    // 这里的运行结果可以看到静态成员_count的地址是一样的
    // 说明派生类和基类共用同一份静态成员
    cout << &p._count << endl;
    cout << &s._count << endl;

    // 公有的情况下,父派生类指定类域都可以访问静态成员
    cout << Person::_count++ << endl;
    cout << Student::_count << endl;

    return 0;
}

运行结果:

六、继承与组合

| 对比维度 | public 继承 | 组合 |
| 核心关系 | is-a(派生类对象 "是一个" 基类对象) | has-a(类对象 "包含一个" 另一个类对象) |
| 复用类型 | 白箱复用 | 黑箱复用 |
| 内部细节可见性 | 基类细节对派生类可见 | 被组合对象细节不可见,仅通过接口交互 |
| 封装性 | 较差(破坏基类封装) | 较好(保持各类独立封装) |
| 耦合度 | 高(基类改动直接影响派生类) | 低(类之间依赖弱,独立性强) |
| 适用场景 | 1. 类关系天然为 is-a 2. 需要实现多态 | 大多数复用场景,追求低耦合、易维护 |

优先级 次级(特定场景使用) 优先(无特殊需求时首选)

示例:

cpp 复制代码
// 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; // 轮胎
};

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

class Benz : public Car {
public:
	void Drive() { cout << "好坐-舒适" << endl; }
};

template<class T>
class vector
{};

// stack 和 vector 的关系:既符合 is-a,也符合 has-a

// 方式1:继承(is-a)
template<class T>
class stack : public vector<T>
{};

// 方式2:组合(has-a)------ 推荐
template<class T>
class stack
{
public:
	vector<T> _v;
};

int main()
{
	return 0;
}
相关推荐
沐知全栈开发2 小时前
XML CDATA
开发语言
APIshop2 小时前
Python 爬虫获取闲鱼商品详情 API 接口实战指南
开发语言·爬虫·python
代码羊羊3 小时前
rust-字符串(切片)、元组、结构体、枚举、数组
开发语言·后端·rust
逻辑驱动的ken3 小时前
Java高频面试考点场景题08
java·开发语言·面试·求职招聘·春招
tankeven3 小时前
HJ182 画展布置
c++·算法
W23035765733 小时前
【改进版】C++ 固定线程池实现:基于调用者运行的拒绝策略优化
开发语言·c++·线程池
星辰_mya4 小时前
PV之系统与并发的核心wu器
java·开发语言·后端·学习·面试·架构师
谭欣辰4 小时前
C++ 控制台跑酷小游戏
c++·游戏
做时间的朋友。4 小时前
Java虚拟线程详解:从原理到实战,解锁百万并发新姿势
java·开发语言