【C++进阶】继承(下)——挖掘继承深处的奥秘!

🎬 个人主页MSTcheng · CSDN
🌱 代码仓库MSTcheng · Gitee
🔥 精选专栏 : 《C语言
数据结构
C++由浅入深

💬座右铭: 路虽远行则将至,事虽难做则必成!


前言:在上一篇文章中,我们探讨了继承的基本概念、不同继承方式的组合运用,以及基类和派生类对象之间的赋值转换问题与作用域规则。本文将延续这一主题,深入介绍继承相关的后续内容。

继承(上): 【C++ 进阶】继承(上):解锁代码复用的核心密码,体会代码复用的魅力!------CSDN博客

文章目录

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

我们知道类和对象中存在6个默认成员函数,默认的意思就是指我们不写,编译器会变我们自动生成⼀个,那么在派生类中,这几个成员函数是如何生成的呢?

1.1派生类的构造函数

cpp 复制代码
#include<iostream>
#include<string>
uisng namespace std;
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,int age)
		//:_name(name) 这里不能这样写 派生类的构造函数必须调用基类的构造函数来初始化基类的那一部分成员
		:Person(name)								
		,_num(num)
		,_age(age)
	{
		cout<<"Student(const char* name,int num,int age)派生类构造"<<endl;
	}
protected:
	int _num;
	int _age;
};

int main()
{
	Student s("张三", 2416010101, 18);
}


注意:

  1. 派生类构造函数 必须显式或隐式调用父类构造函数 。若未显式调用 ,编译器会自动调用父类的默认构造函数。若父类没有默认构造函数,则必须显式调用某个特定的父类构造函数。
  2. 在派生类构造函数的初始化列表中,基类成员的构造总是最先完成,随后才会执行派生类自身成员的构造。

1.2派生类的拷贝拷贝构造

cpp 复制代码
#include<iostream>
#include<string>
uisng namespace std;
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
		cout << "Person()基类构造" << endl;
	}
	 Person(const Person& p)
		:_name(p._name)
	{
		cout << "Person(const Person& p)基类的拷贝构造" << endl;
	}
protected:
	string _name;
};

class Student :public Person
{
public:
	Student(const char* name,int num,int age)
		:Person(name)								
		,_num(num)
		,_age(age)
	{
		cout<<"Student(const char* name,int num,int age)派生类构造"<<endl;
	}
	Student(const Student& s)
	//把派生类中的基类看作是一个整体 调用基类的拷贝构造
	:Person(s)//传的是一个Person类对象
	,_num(s._num)
	,_age(s._age)
{
	cout << "Student(const Student& s)派生类的拷贝构造" << endl;
}
protected:
	int _num;
	int _age;
};

int main()
{
	Student s("张三", 2416010101, 18);
	Student s2(s);
}



注意:

  1. 派⽣类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  2. 在没有资源的情况下/不存在要深拷贝时派生类可以不写拷贝构造 ,让编译器自动生成。编译器自动生成的拷贝构造,对于派生类的基类部分会调用基类的拷贝构造对于派生类新增的部分会浅拷贝。

1.3派生类的赋值重载

cpp 复制代码
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
		cout << "Person()基类构造" << endl;
	}
	Person(const Person& p)
		:_name(p._name)
	{
		cout << "Person(const Person& p)基类的拷贝构造" << endl;
	}
	//赋值重载
	Person& operator=(const Person& s)
	{
		if (&s != this)
		{
			_name = s._name;
		}
		return *this;
	}
protected:
	string _name;
};

class Student :public Person
{
public:
	Student(const char* name,int num,int age)
		:Person(name)								
		,_num(num)
		,_age(age)
	{
		cout << "Student(const char* name,int num,int age)派生类构造" << endl;
	}
	Student(const Student& s)
		:Person(s)
		,_num(s._num)
		,_age(s._age)
	{
		cout << "Student(const Student& s)派生类的拷贝构造" << endl;
	}
	//赋值重载
	Student& operator=(const Student& s)
	{
		if (&s != this)
		{
			//基类部分调用基类的赋值重载 而由于基类的operator=()与派生类的operator=()构成隐藏
			//所以在调用基类的operator=()时要指定类域
			Person::operator=(s);
			_num = s._num;
			_age = s._age;
		}
		return* this;
	}
protected:
	int _num;
	int _age;
};

int main()
{
	Student s("张三", 01, 18);
	cout << endl;
	Student s2(s);
	cout<<endl;
	Student s3("李四", 2, 25);
	s3 = s2;
}


注意:

  1. 派生类 的赋值运算符(operator=)必须显式调用基类的赋值运算符 来完成基类部分的复制。需要注意的是,派生类operator=隐藏基类operator=,因此在调用时需通过基类作用域来明确指定。
  2. 派生类的赋值重载一般来说,编译器默认生成的就够用了,只有当存在深拷贝的时候才需要自己写。

1.4派生类的析构函数

cpp 复制代码
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
		cout << "Person()基类构造" << endl;
	}
	Person(const Person& p)
		:_name(p._name)
	{
		cout << "Person(const Person& p)基类的拷贝构造" << endl;
	}

	Person& operator=(const Person& s)
	{
		if (&s != this)
		{
			_name = s._name;
		}
		return *this;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name;
};

class Student :public Person
{
public:
	Student(const char* name, int num, int age)
		:Person(name)
		, _num(num)
		, _age(age)
	{
		cout << "Student(const char* name,int num,int age)派生类构造" << endl;
	}

	Student(const Student& s)
		:Person(s)
		, _num(s._num)
		, _age(s._age)
	{
		cout << "Student(const Student& s)派生类的拷贝构造" << endl;
	}
	Student& operator=(const Student& s)
	{
		if (&s != this)
		{
			Person::operator=(s);
			_num = s._num;
			_age = s._age;
		}
		return*this;
	}
	//析构函数名字因为后续多态(重写)章节原因,会被处理成destructor
	//所以派生类和基类析构构成隐藏关系
	~Student()
	{
		 //Person::~Person();这行代码可以不用写,因为~Student本身会自动调用
	} // 自动调用父类析构, 才能保证先子后父的析构顺序

	// 派生类析构调用后,会自动调用父类析构,所以自己实现析构时不需要显示调用
	// 构造初始化,先父类后子。析构清理资源,先子后父。
protected:
	int _num;
	int _age;
};

int main()
{
	Student s("张三", 01, 18);
	cout << endl;
	Student s2(s);
	cout << endl;
	Student s3("李四", 2, 25);
	s3 = s2;
}

注意:

  1. 派生类对象 在析构时会先调用派生类的析构函数 ,再自动调用 基类的析构函数 完成清理工作。因为这样才能保证派生类对象 先清理派生类成员清理基类成员的顺序
  2. 因为多态中⼀些场景析构函数需要构成重写 ,重写的条件之⼀是函数名相同(这个我们多态章节介绍)。那么编译器会对析构函数名进⾏特殊处理处理成destructor() ,所以基类析构函数不加virtual的情况下派生类析构 函数和基类析构 函数构成隐藏关系

派生类默认成员函数行为总结:

二、继承与友元

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

cpp 复制代码
//前置声明
class Student;

//基类
class Person
{
public:
	//友元函数
	friend void Display(const Person& p, const Student& s);//这里用到了Student当参数
						//编译器向上找找不到会报错 所以要在上面加上前置声明
protected:
	string _name; // 姓名

private:
	int _a;
};

//派生类
class Student : public Person
{
public:
	//把Display函数也声明
	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;
}


int main()
{
	Person p;
	Student s;
	// 编译报错:error C2248: "Student::_stuNum": ⽆法访问 protected 成员
	// 解决⽅案:Display也变成Student 的友元即可
	Display(p, s);
	return 0;
}

注意友元关系是不能继承的Display声明成Person的友元 ,则Display中可以访问Person中的私有成员子类Student 继承了父类Person 后,并不能使用Display函数 ,因为DisplayPerson的友元而不是Student的友元。就相当于你父亲的朋友不一定是你的朋友一样。解决方法:在Student里也声明友元。

三、继承与静态成员

基类中定义的静态成员在整个继承体系中是唯一的。无论派生出多少个子类,该静态成员始终只有一个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;
}

四、继承与组合

继承(is-a)与组合(has-a)对比表格

特性 继承(is-a 组合(has-a
关系类型 is-a 关系,派生类是基类的一种类型 has-a 关系,对象包含另一个对象
复用类型 白箱复用:派生类可访问基类实现细节 黑箱复用:对象细节不可见,仅通过接口访问
耦合度 高,基类变化可能影响派生类 低,改变一个对象不会影响另一个对象
优点 代码复用,支持多态和功能扩展 类间关系松散,低耦合,易于维护和替换
缺点 破坏封装,强依赖性,基类变动风险 需定义清晰接口,可能增加代码量
使用场景 类间存在逻辑层级关系且需多态支持(如动物→狗) 类间需功能组合但无层级关系(如汽车→发动机)

五、总结

C++继承核心要点💡

📢继承类型公有继承保持基类访问权限,保护继承降级为保护成员,私有继承降级为私有成员。

📢构造与析构派生类构造函数隐式调用基类默认构造,显式调用需通过初始化列表;析构顺序与构造相反。

📢访问控制派生类可访问基类公有和保护成员,外部对象仅能访问公有成员(公有继承时)。

📢继承与组合继承体现"is-a"关系(特化),组合体现"has-a"关系(包含),需根据设计目标选择。

MSTcheng 始终坚持用直观图解 + 实战代码,把复杂技术拆解得明明白白!

👁️ 【关注】 看普通程序员如何用实用派思路搞定复杂需求

👍【点赞】 给 "不搞虚的" 技术分享多份认可

🔖 【收藏】 把这些 "好用又好懂" 的干货技巧存进你的知识库

💬 【评论】 来唠唠 ------ 你踩过最 "离谱" 的技术坑是啥?

🔄 【转发】把实用技术干货分享给身边有需要的程序员伙伴

技术从无唯一解,让我们一起用最接地气的方式,写出最扎实的代码! 🚀💻

相关推荐
RisunJan42 分钟前
【HarmonyOS】鸿蒙开发语言的选择
开发语言·华为·harmonyos
学困昇42 分钟前
Linux基础开发工具(上):从包管理到“进度条”项目实战,掌握 yum/vim/gcc 核心工具
linux·运维·开发语言·数据结构·c++·vim
雨落在了我的手上43 分钟前
C语言入门(二十五):自定义类型:结构体
c语言·开发语言
Yan-英杰44 分钟前
openEuler 25.09 VM虚拟机实测:性能与安全双维度测评
服务器·开发语言·科技·ai·大模型
兩尛1 小时前
HJ52 计算字符串的编辑距离
java·开发语言·算法
武子康1 小时前
Java-183 OSS 上传实战:Java 原生与 Spring Boot 集成
java·开发语言·spring boot·分布式·spring·阿里云·oss
ALex_zry1 小时前
系统编程的基石:补码循环溢出与Rust变量绑定的深度探索
开发语言·后端·rust
ALex_zry1 小时前
Rust语言基础分析与C++对比:系统编程的现代演进
java·c++·rust
Molesidy1 小时前
【QT】【C++】基于QT的多线程分别管理GUI和运算任务
开发语言·c++·qt