【C++】 继承与多态(上)

继承 · d0746b7 · 加油少年/CCCCC - Gitee.com


C++ 继承

一、什么是继承

继承是面向对象三大特性(封装、继承、多态)之一。

作用:让一个类复用另一个类的成员,减少重复代码,实现代码复用。

  • 被继承的类:父类 / 基类
  • 继承过来的类:子类 / 派生类

通俗理解:父类写通用属性和行为,子类在父类基础上扩展独有功能,不用重复写相同代码。


二、继承基本语法

cpp 复制代码
class 派生类 : 继承方式 基类
{
    // 派生类独有成员
};

三种继承方式

  1. public 公有继承(最常用)
  2. protected 保护继承
  3. private 私有继承
基类成员 公有继承 保护继承 私有继承
public public protected private
protected protected protected private
private 不可访问 不可访问 不可访问

1. 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。
2. 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员在派⽣类的访问⽅式 == Min(成员在基类的访问限定符,继承⽅式),public > protected > private。
4. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式。
5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤ protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实 际中扩展维护性不强。


三、继承类模板(组合与继承)

对比项 继承(派生) 组合(包含)
关系 is-a 从属关系 has-a 整体 - 部分
耦合 高耦合,强依赖父类 低耦合,依赖极小
访问权限 可继承公有 / 保护成员 只能调用公开接口
代码复用 直接继承全部代码 调用对象功能复用
多复用 只能单类继承(C++ 单继承) 可组合多个类
多态 支持虚函数多态 不天然支持多态
设计原则 少用、慎用 优先使用
cpp 复制代码
namespace bit
{
	//template<class T>
	//class vector
	//{};
	// stack和vector的关系,既符合is-a,也符合has-a
	template<class T>
	class stack : public 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()
{
	bit::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	return 0;

四、基类和派⽣类间的转换

类型转换

通常情况下我们把⼀个类型的对象赋值给另⼀个类型的指针或者引⽤时,存在类型转换,中间会产⽣临时对象,所以需要加const,如: int a = 1; const double& d = a; public继承中,就是⼀个特殊处理的例外,派⽣类对象 可以赋值给 基类的指针 / 基类的引⽤,⽽不需要加const,这⾥的指针和引⽤绑定是派⽣类对象中的基类部分,如下图所⽰。也就意味着⼀个基类的指针或者引⽤,可能指向基类对象,也可能指向派⽣类对象。

对象切片(切割)

派⽣类对象赋值给基类对象是通过基类的拷⻉构造函数或者赋值重载函数完成的(这两个函数的细节后⾯⼩节会细讲),这个过程就像派⽣类⾃⼰定义部分成员切掉了⼀样,所以也被叫做切割或者切片。

cpp 复制代码
class Base{};
class Derive : public Base{};

Derive d;
Base b1(d);      // 1.拷贝构造切片
Base b2 = d;     // 2.隐式转换切片
Base b3;
b3 = d;          // 3.赋值重载切片
关键特点
  • 切片后丢失派生类独有属性与方法
  • 只会向上切片(派生→基类),不能基类赋值给派生类
  • 普通对象赋值必切片,指针 / 引用不会切片
    子类赋父类,切掉子类独有成员,只剩父类内容,称为对象切片

赋值关系

cpp 复制代码
// ⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的
 Person pobj = sobj;
 
// 基类对象不能赋值给派⽣类对象,这⾥会编译报错
 sobj = pobj;

基类指针 / 引用转派生类

基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。
但是必须是基类的指针是指向派⽣类对象时才是安全的。
这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time Type Information)的dynamic_cast 来进⾏识别后进⾏安全转换。(ps:这个我们后⾯类型转换章节再单独专⻔讲解,这⾥先提⼀下)


五、继承中的作用域

1. 在继承体系中基类和派⽣类都有独⽴的作⽤域。
2. 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。

cpp 复制代码
class Base{
public:
    int a = 10;
    void fun(){ }
};
class Der : public Base{
public:
    int a = 20;        // 隐藏基类a
    void fun(int x){}  // 名字相同,构成隐藏

    void test()
    {
        cout << a;        // 访问自己的a=20
        cout << Base::a;  // 显式访问基类a=10
        Base::fun();      // 调用基类被隐藏函数
    }
};

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

cpp 复制代码
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();    // ❌ 编译报错!
}

只要函数名相同,就构成隐藏,不管参数是否一样!

  • 父类 Afun()
  • 子类 Bfun(int)
  • 名字相同 → 子类隐藏父类

结果:b.fun() 想调用父类的无参函数,但被隐藏了,找不到。


六、派⽣类的默认成员函数

6个默认成员函数,默认的意思就是指我们不写,编译器会变我们⾃动⽣成⼀个,那么在派⽣类中,这⼏个成员函数是如何⽣成的呢?
1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。

cpp 复制代码
情况 1:基类有默认构造(无参
class Base{
public:
    Base(){} // 默认构造
};
class Der : public Base{
public:
    Der()
    {
        // 编译器自动:Der() : Base() {}
    }
};
情况 2:基类无默认构造,只有带参构造
class Base{
public:
    Base(int a){} // 只有带参,无默认构造
};
class Der : public Base{
public:
    // 正确:初始化列表显式调用基类构造
    Der(int x) : Base(x)
    {
        
    }
};

2. 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。

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

class Base
{
public:
    int a;
    Base(int x) : a(x) {}
    // 基类拷贝构造
    Base(const Base& b)
    {
        a = b.a;
        cout << "基类拷贝构造" << endl;
    }
};

class Der : public Base
{
public:
    int b;
    int c;
    // 派生类多参数构造
    Der(int x, int y, int z) : Base(x), b(y), c(z) {}

    // 派生类拷贝构造(自身多个成员)
    Der(const Der& d) 
        : Base(d)   // 1. 优先调用基类拷贝构造,拷贝继承成员
        , b(d.b)    // 2. 拷贝自己第一个成员
        , c(d.c)    // 3. 拷贝自己第二个成员
    {
        cout << "派生类拷贝构造" << endl;
    }
};

int main()
{
    Der d1(10,20,30);
    Der d2 = d1; // 触发拷贝构造
    return 0;
}

3. 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的
operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域。

cpp 复制代码
Person& operator=(const Person& p)
{
	if (this != &p)
		_name = p._name;
	
	return *this;
}


Student& operator=(const Student& s)
{
	// 编译默认生成的就够用了
	// 存在深拷贝时,才自己写
	if (this != &s)
	{
		Person::operator=(s);
		_num = s._num;
		_address = s._address;
	}

	return *this;
}

4. 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。
5. 派⽣类对象初始化先调⽤基类构造再调派⽣类构造。
6. 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。

cpp 复制代码
~Student()
{
	// Person::~Person();
}
// 自动调用父类析构, 才能保证先子后父的析构顺序
// 派生类析构调用后,会自动调用父类析构,所以自己实现析构时不需要显示调用
// 构造初始化,先父类后子。析构清理资源,先子后父。


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


七、无法被继承的类

⽅法1: 基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。
⽅法2: C++11新增了⼀个 final关键字 ,final修改基类,派⽣类就不能继承了。

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

八、继承与友元

  • 基类的友元,不是派生类的友元基类外的友元函数 / 友元类,只能访问基类私有成员,无权访问派生类私有成员。

  • 派生类的友元,不能继承获得基类友元权限派生类自己声明的友元,只对派生类生效,不会自动成为基类友元。

  • 成员函数友元同样不继承基类某个成员函数是外部类友元,派生类同名函数不会自动成为友元。

cpp 复制代码
class Student;//注意编译器向上查找,所以这里要再次声明
class Person
{
public:
     friend void Display(const Person& p, const Student& s);

protected:
     string _name; // 姓名
};

相关推荐
ch.ju1 小时前
Java程序设计(第3版)第四章——静态部分
java·开发语言
05候补工程师1 小时前
【线性代数】核心考点:二次型、矩阵三大关系综合与正定矩阵判别法
笔记·线性代数·考研·算法·矩阵
ZHOUPUYU1 小时前
PHP 开发实战:从零搭建一个高性能的 RESTful API 服务
运维·开发语言·后端·html·php
不负岁月无痕1 小时前
STL -- C++ string 类 模拟实现
java·开发语言·c++
亅-丿-丶丿丶一l一丶-/^n1 小时前
RLHF|PPO算法原理(一)
算法·自然语言处理
·心猿意码·1 小时前
OCCT源码解析(六):TKG3d 模块——三维曲面体系
c++·3d
ʚ希希ɞ ྀ2 小时前
打家劫舍----背包dp
数据结构·算法·leetcode
Anastasiozzzz2 小时前
万字深度实战!AI Agent 接入万物的底层密码:MCP 协议传输机制与开发指南(下篇)
java·开发语言·数据库·人工智能·ai·架构
zcongfly2 小时前
Claude code使用笔记
笔记