首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >面向对象编程:继承从理论到实战

面向对象编程:继承从理论到实战

作者头像
用户11831438
发布2025-12-30 13:38:34
发布2025-12-30 13:38:34
1720
举报

一、什么是继承:类与类之间的关系

1.1 继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。

简单来说:继承是一种复用,代码的复用

ok,我们来举个例子:

  • 假设现在我想搞一个什么管理系统,我需要建几个类,并用这些类保存一些相关信息:
  • 我们看到上面的三个类中的私有成员有很多是重复的:

那如果我们在写这三个类的时候,在每个类中都写上这些重复的成员,是不是显得有点冗余,对于空间来说是不是有点浪费!!!

  • 那我们就可以将这些重复成员(或者说是公共的成员)抽取出来放在一个公共类中:

然后,student 和 teacher 这两个类就可以都继承person这个类,就可以复用这些成员,就不需要重复定义了,省去了很多麻烦

通过上面的例子,是不是很清晰的看出继承的作用!!!

下面我们通过代码来看一下:

1.1.1 继承机制的优势与便利性

下面我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址电话/年龄等成员变量,都有identity身份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有一些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课——

代码语言:javascript
复制
#include<iostream>
using namespace std;
class student
{
public:
	//进入校园需要身份验证
	void identity()
	{
		//……
	}
	void study()
	{
		//学习
	}
protected:
	string _name = "胡萝卜";//姓名
	string _address;//地址
	string _tel;//电话
	int _age = 18;//年龄
};
class teacher
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	} 
	// 授课
	void teaching()
	{
		//...
	}
protected:
	string _name = "张三"; // 姓名
	int _age = 18; // 年龄
	string _address; // 地址
	string _tel; // 电话
	string _title; // 职称
};

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

代码语言:javascript
复制
#include<iostream>
using namespace std;
class Person
{
public:
	//进入校园需要身份验证
	void identity()
	{
		//……
	}
protected:
	string _name;   //姓名
	string _address;//地址
	string _tel;    //电话
	int _age;       //年龄
};
class student:public Person
{
public:
	void study()
	{
		//学习
	}
protected:
	string _num;//学号
};
class teacher:public Person
{
public:
	// 授课
	void teaching()
	{
		//...
	}
protected:
	string _title; // 职称
};
int main()
{
	student s;
	teacher t;
	s.identity();
	t.identity();
	return 0;
}

在日常的生活中,我们知道有些信息、数据、方法是公共的,有些信息又是各自独立的,就比如上面的学生和老师的信息:

那我们就可以将这些公共的特性(成员变量、成员函数),抽取出来放到一个公共类中,然后其余各自独有的特性放在各自的类中。

1.2 继承语法与访问控制
1.2.1 继承定义的标准格式

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

ok,了解完什么是基类,什么是派生类,接下来我们来看一下什么是:继承方式

继承方式有public继承,protected继承,private继承访问限定符有public访问,protected访问,private访问;这~会擦出什么样的火花呢?

正是因为有三种继承方式和三种访问,所以才有了继承基类成员访问方式的九种变化。

1.2.2 继承后,父类成员的权限怎么变?

我们先来看这一行的内容:

什么叫做在派生类中不可见? 基类的所有成员都会继承下来,但是基类中的private成员不能直接被使用。 (这基类的私有成员就好比如爸爸的私房钱,如果你直接跟你爸说“把你的私房钱给点,我要去下馆子”,你爸爸肯定不会给你,但是我们可以间接的说“我在你的车上发现了鱼竿,这个鱼竿是怎么来的;或者我好像听你们公司的啥叔叔说好像发了奖金”,这时候你爸爸肯定会说:“走,现在就去下馆子”)。 基类的私有成员在派生类中不可见,语法上是指不能直接被使用,但是可以间接使用,可以调用父类中的一个方法,这个方法中可以访问基类的私有成员

但是我们可以间接使用:

剩下的两行就没什么好说的呢,我们只要记住下面的总结即可——

  • 总结:

基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected >private。

  • 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
  • 在实际运用中一般使用public继承,几乎很少使用protected/priavte继承,也不提倡使用protected/priavte继承,因为protected/priavte继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
1.3 强强联合:如何继承一个类模板?
代码语言:javascript
复制
//继承类模板
namespace carrot
{
	//定义一个栈
	template<class T>
	class stack:public vector<T>
	{
	public:
		void push(const T& x)
		{
			vector<T>::push_back(x);
		}
		void pop()
		{
			vector<T>::pop_back();
		}
		const T& top()
		{
			return vector<T>::back();
		}

	};
}
int main()
{
	carrot::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	return 0;
}
  • 注意1
  • 注意2:里面复用时需要显示实例化,指定属于哪个类域中的。

正确写法:

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

public继承派生类 可以赋值给 基类的指针基类的引用或者是基类

为什么这里可以这么写? ok,我们一一来看:

  • public继承派生类 可以赋值给基类的引用
  • public继承派生类 可以赋值给 基类的指针
  • public继承派生类 可以赋值给 基类

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

这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。

  • 基类对象不能赋值给派生类对象。

为什么?这是因为派生类中有自己独有的成员,我基类可以将公有的部分赋值给派生类成员,单机派生类中自己独有的成员,我基类没有办法给!!!

  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time-Type Information)的dynamic_cast来进行识别后进行安全转换。(后面会详细介绍)

代码演示:

代码语言:javascript
复制
#include<string>
class Person
{
protected:
	string _name;
	string _sex;
	int _age;
};
class Student :public Person
{
public:
	int _No;
};
int main()
{
	Student s;
	//派生类可以赋值给基类的引用/指针
	Person& rp = s;
	Person* ptr = &s;

	//派⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的
	Person p = s;

	//基类对象不能赋值给派生类对象,会编译报错
	Person p;
	Student s = p;
	return 0;
}

三、继承中的作用域

在前面的学习中,我们学习过:在相同作用中不能存在相同的名字;在不同作用域中可以出现相同名字。

那么派生类和基类是相同的域,还是不相同的域呢?我们一起来看——

3.1 隐藏规则

在继承体系中基类和派生类都有独立的作用域,可以有同名成员

但是,不知道有没有uu发现这里打印的结果是派生类中的_num的结果,为什么不去打印基类中的_num 中的结果呢?

那为什么会先去派生类域中找,而不是先在基类中进行查找操作呢? 这是因为:派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫做隐藏,也就是说派生类同名成员隐藏基类同名成员。

如果我们就想访问基类中的_num,我们可以使用 基类 :: 积累成员 显示访问

ok,接下来,我们来做两道比较坑的题~~~

3.2 考察继承作用域相关选择题

(1)A和B类中的两个func构成什么关系(B)

A. 重载 B. 隐藏 C.没关系

(2)下面程序的编译运行结果是什么(A)

A. 编译报错 B. 运行报错 C. 正常运行

代码语言:javascript
复制
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();
	return 0;
};

为什么选择B和A呢?A中的fun函数和B中的fun函数不是构成函数重载吗?

注意:如果是成员函数的隐藏(也就是有同名函数,但是参数可能不同),只需要函数名相同就构成隐藏(参数相不相同都行)!!!

总结一下~

  • 在实际中的继承体系里面最好不要定义同名的成员!!!

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

4.1 6个默认成员函数

ok,我们的关注点还是前四个默认成员函数:构造、析构、拷贝构造和赋值重载

在前面类和对象的学习中,我们的关注点是:

  • 我们不显示写,编译器默认生成的成员函数的行为是什么样的(对内置成员初不初始化,对自定义类型的行为什么样的)
  • 如果编译器默认生成的成员函数不能满足我们的要求,我们就需要显示写,那我们这些成员函数该怎么写

那到了派生类的默认成员函数这里,我们的关注点还是一样的

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

4.1.1 构造

在派生类中,我们不写构造,编译器自动生成的构造函数对于基类中的那一部分成员必须调用基类中的构造函数去初始化基类的那一部分成员,如果基类中没有默认的构造函数,则必须在派生类构造函数的初始化列表中显示调用;

在派生类中,我们不写构造,编译器自动生成的构造函数对于派生类中的内置类型初不初始化不知道(要看编译器),对于自定义类型(比如:stirng,vector)会去调用他们自己的构造

补充知识:

那如果基类中没有默认构造呢~

这里的基类没有默认构造的原因是:不知道给_name初始化成什么(因为没有给相应的字符串进行初始化)

所以,基于以上原因:派生类中没有显示写构造,基类不也提供默认构造,派生类中的_num和_address也要我们自己去初始化,我们需要自己写一个~

这里有点问题,我们应该将继承的基类成员看成一个整体,并显示调用基类的构造

完整代码:

代码语言:javascript
复制
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& 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=999,const char* address="中国")
		:Person(name)//显示调用基类的构造
		,_num(num)
		,_address(address)
	{}
	void print()
	{
		cout << _name << " " << _num << " " << _address << endl;
	}
protected:
	int _num; //学号
	string _address;
};
int main()
{
	Student s;
	s.print();
	return 0;
}

本质可以把派生类当做多了一个自定义类型的成员变量(基类)的普通类!!!

4.1.2 拷贝构造

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

对于拷贝构造而言,派生类中不写,编译器自动生成的拷贝构造就够用了

但是如果有需要深拷贝的成员,就需要我们自己写一个~

为什么这里可以这么写:Person(s)?

也许会有UU想问:这里为什么要放在初始化列表中,不能放在 { } 中吗?

  • 原因分析:

在C++中,基类部分必须在派生类之前构造完成。一旦进入函数体,所有基类和成员都已经构造完成了。

代码语言:javascript
复制
// 错误写法!
Student(const Student& s)
{
    // 到这里时,基类Person已经默认构造完成了!
    // 无法再调用Person的拷贝构造函数
    Person(s);  // 这行代码没用!会创建一个临时Person对象然后立即销毁
    _num = s._num;      // 这是赋值,不是初始化
    _address = s._address;
}

基类的也要放到初始化列表中!!!

  • 代码:
代码语言:javascript
复制
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& 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=999,const char* address="中国")
		:Person(name)
		,_num(num)
		,_address(address)
	{}
    //显示写拷贝构造
	Student(const Student& s)
		:Person(s)
		,_num(s._num)
		,_address(s._address)
	{}
	void print()
	{
		cout << _name << " " << _num << " " << _address << endl;
	}
protected:
	int _num; //学号
	string _address;
};
int main()
{
	Student s("jerry",111,"美国");
	s.print();
	Student s1(s);

	return 0;
}
4.1.3 赋值重载

在派生类中,我们不显示写赋值重载,编译器会自动生成一个,对于派生类中的基类部分会去调用基类中的赋值重载,对于派生类中的内置类型会完成浅拷贝,对于自定义类型会去调用自己的赋值重载

对于有资源的,需要进行深拷贝的,就需要我们自己写一个赋值重载

运行一下:

嗯?为什么报错了?

这是因为:基类中有operator=,派生类中也有operator=,这就构成隐藏,如果这么写,会导致无法调用到基类中的赋值重载,所以我们要显示调用基类中赋值重载

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

  • 代码:
代码语言:javascript
复制
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& 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=999,const char* address="中国")
		:Person(name)
		,_num(num)
		,_address(address)
	{}
	Student(const Student& s)
		:Person(s)
		,_num(s._num)
		,_address(s._address)
	{}
	//赋值重载
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_num = s._num;
			_address = s._address;
		}
		return *this;
	}
	void print()
	{
		cout << _name << " " << _num << " " << _address << endl;
	}
protected:
	int _num; //学号
	string _address;
};
int main()
{
	Student s("jerry",111,"美国");
	s.print();
	Student s1("Tom", 222, "中国");
	s1.print();
	s = s1;
	s.print();

	return 0;
}

ok,学完拷贝构造和赋值重载之后,我们就可以来看看上面学习的派生类可以给基类 派生类对象赋值给基类对象是通过基类的拷贝构造函数或者赋值重载函数完成的

4.1.4 析构

在派生类中,我们不显示写析构,编译器会自动生成一个,对派生类中的基类那一部分会去调用基类的析构,对于派生类中的内置类型不做处理,对自定义类型会去调用自己的析构。

如果需要显示写析构,我们应该这么写:

?为什么这么写不对

因为多肽中一些场景析构函数需要重写,重写的条件之一是函数名相同(多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),这样就导致派生类中的析构函数和基类中的析构函数构成隐藏关系

所以我们需要指定作用域:

ok,运行一下:

嗯?为什么这里会打印两次析构? ok,这是因为调用了两次析构,其实这里不用显示调用基类析构,编译器会在派生类析构结束后,自动调用基类析构。

  • 简单理解:

在构造中我们是按照:先父类后子类的顺序;析构是先子后父

如果显示调用父类析构,无法保证先子后父的析构顺序

  • 深层度理解:

如果是先父后子,子类可以访问父类中的成员,若先析构父类的成员,子类可能会指向野指针,若此时对野指针进行接引用,会有问题,所以析构要先子后父

总结:如果在派生类中显示写析构,不用在析构里面显示写基类的析构,在派生类的析构结束后,编译器会自动调用基类的析构

4.2 默认成员函数总结

派生类中一般要自己实现构造,不需要写拷贝构造、赋值重载和析构,除非派生类中有深拷贝的资源需要处理!!!

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、什么是继承:类与类之间的关系
    • 1.1 继承的概念
      • 1.1.1 继承机制的优势与便利性
    • 1.2 继承语法与访问控制
      • 1.2.1 继承定义的标准格式
      • 1.2.2 继承后,父类成员的权限怎么变?
    • 1.3 强强联合:如何继承一个类模板?
  • 二、基类和派生类间的转换
  • 三、继承中的作用域
    • 3.1 隐藏规则
    • 3.2 考察继承作用域相关选择题
  • 四、派生类的默认成员函数
    • 4.1 6个默认成员函数
      • 4.1.1 构造
      • 4.1.2 拷贝构造
      • 4.1.3 赋值重载
      • 4.1.4 析构
    • 4.2 默认成员函数总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档