C++继承详解

C++继承详解

  • 一、继承的概念和定义
    • 1.1、继承的概念
    • 1.2、继承的定义
    • [1.2.1 继承的使用](#1.2.1 继承的使用)
    • [1.2.2 继承权限问题](#1.2.2 继承权限问题)
      • [1.2.2.1 访问限定符和继承方式的区别](#1.2.2.1 访问限定符和继承方式的区别)
      • [1.2.2 继承后基类成员访问方式的变化](#1.2.2 继承后基类成员访问方式的变化)
    • [1.3 继承类模版的问题](#1.3 继承类模版的问题)
  • 二、基类和派生类的转换
  • 三、继承中的作用域
    • [3.1 隐藏的规则](#3.1 隐藏的规则)
    • [3.2 相关的题目](#3.2 相关的题目)
  • 四、派生类中基类对象的的拷贝构造、赋值和析构
    • [4.1 构造](#4.1 构造)
      • [4.1.1 存在默认构造函数](#4.1.1 存在默认构造函数)
      • [4.1.2 不存在默认构造函数](#4.1.2 不存在默认构造函数)
    • [4.2 赋值运算符重载](#4.2 赋值运算符重载)
    • [4.3 析构函数](#4.3 析构函数)
    • [4.4 析构和构造的顺序](#4.4 析构和构造的顺序)
  • 五、如何实现一个不能被继承的类
  • 六、友元和静态成员的继承
    • [6.1 友元成员变量的继承](#6.1 友元成员变量的继承)
    • [6.2 静态成员的继承](#6.2 静态成员的继承)
  • 七、多继承和菱形继承
    • [7.1 多继承的概念和操作](#7.1 多继承的概念和操作)
    • [7.2 菱形继承](#7.2 菱形继承)
  • 八、菱形继承的解决方案:虚继承
    • [8.1 虚继承的使用](#8.1 虚继承的使用)
    • [8.2 虚继承的注意事项](#8.2 虚继承的注意事项)
      • [8.2.1 虚继承的使用位置](#8.2.1 虚继承的使用位置)
      • [8.2.2 虚继承的使用实际选择](#8.2.2 虚继承的使用实际选择)
      • [8.2.3 使用虚函数后的初始化](#8.2.3 使用虚函数后的初始化)
  • 九、继承和组合
    • [9.1 is-a关系和has-a关系](#9.1 is-a关系和has-a关系)
    • [9.2 组合的优点](#9.2 组合的优点)
    • [9.3 组合和继承在实践中的选择](#9.3 组合和继承在实践中的选择)


递归何不归:个人主页
个人专栏 : 《C++庖丁解牛》《数据结构详解》

在广袤的空间和无限的时间中,能与你共享同一颗行星和同一段时光,是我莫大的荣幸


一、继承的概念和定义

1.1、继承的概念

继承是一种常用的代码复用手段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。

cpp 复制代码
class Person
{
public:
	Person(const string& name ,int age = 18)
		:_name(name)
		,_age(age)
	{}

	string _name;
	int _age;

};

class Student : public virtual Person 
{
public:
	Student(const string& name, int num = 114514)
		:Person(name)
		,_num(num)
	{}

	int _num;
};
class Teacher : public virtual Person
{
public:
	Teacher(const string& name ,const string& position = "沈阳大街")
		:Person(name)
		,_position(position)
	{}
	string _position;
};

就像是上述代码中的Student 和 Teacheer 类都属于Person这个范畴 ,只不过是在细节上存在差异,这时候就可以将Person继承给Student和Teacher这两个类,然后再实现他们自己的成员变量和成员函数,从而实现这个派生类

1.2、继承的定义

1.2.1 继承的使用


这里访问限定符和继承方式还是存在区别的,具体的下面会讲到

1.2.2 继承权限问题

1.2.2.1 访问限定符和继承方式的区别

虽然访问限定符和继承方式的关键字是一样的,但是这并不意味着我们可以将这两者混为一谈

1、从作用方式来看:访问限定符是限制单个对象,而继承方式是限定派生类中基类成员的访问权限

2、从服务对象上来看:访问限定符是服务于对象,而继承方式是服务于继承这个过程

3、从体现的思想上来看:访问限定符体现的是封装的思想 ,而继承方式体现的是代码复用的思想

1.2.2 继承后基类成员访问方式的变化

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见

这个表格很明显的显示出了继承中基类权限的变化,总体上呈现出以下两种特点:

1、权限大小: public > protected > private

2、继承方式更像是一种上限 ,当访问限定符的权限小于 继承方式时,其权限就不会发生变化,反之,当权限大于继承方式时,就会将权限缩小到继承的权限

1.3 继承类模版的问题

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

这里还是存在按需实例化的影响 ,在实例化stack的时候编译器认为push_back函数不是必须要实例化的 ,此时我们就需要显示指定 push_back 存在的类域:vector

二、基类和派生类的转换

出于各种考量,在C++中基类和派生类是可以相互转换的,我们这里着重讲解派生类向基类的转换,这一转换将在之后的继承中的赋值运算符重载中起到重要作用(我们之后再说)

1、在C++中,派生类对象 是可以将他的基类成员赋值给基类的指针 / 基类的引⽤ 。这种现象称为切割或者是切片

2、基类对象不能赋值给派⽣类对象。

3、基类的指针或者引⽤可以通过强制类型转换赋值 给派⽣类的指针或者引⽤。但是必须是基类的指针是指向派⽣类对象时才是安全的。

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;
}

三、继承中的作用域

在继承中,派生类和基类都有他们自己的作用域,这意味着在调用函数时需要加以区分 ,这就引出了隐藏这个规则

3.1 隐藏的规则

1、隐藏的条件: 在基类的派生类中函数名相同的两个函数 存在隐藏的关系,不管参数一样不一样

2、隐藏的效果: 构成隐藏的函数,未指定作用域 时调用自动调用派生类中的函数 ,想要调用基类的函数需要指定作用域

3、建议: 还是尽量不要在继承中定义同名的函数

3.2 相关的题目

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;
};

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

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

这个题是存在大坑的,看到这两个函数的函数名相同但是参数不同,大部分人可能就选了选项A:重载

但是要注意的是:两个函数构成重载的前提是这两个函数得在同一作用域内 ,但是本题中这两个函数并不在同一个作用域内,这时候因为这两个函数名字相等,他们构成了隐藏关系,所以选择选项:隐藏

四、派生类中基类对象的的拷贝构造、赋值和析构

  • 在派生类中,基类可以被看作是一个较为独立的成员变量,在构造、赋值和析构中表现的尤其明显。
  • 体现在基类的成员变量的初始化是完全依赖于基类初始化的,不能直接初始化基类成员变量。
  • 赋值和析构遵循和构造类似的规则

4.1 构造

4.1.1 存在默认构造函数

当基类存在默认构造函数时,可以不显式调用基类的构造函数

4.1.2 不存在默认构造函数

此时实现基类成员的构造需要在派生类的构造函数中显示调用基类的拷贝构造函数

cpp 复制代码
class Person
{
public:
	Person(const string& name ,int age = 18)
		:_name(name)
		,_age(age)
	{}

	string _name;
	int _age;

};

class Student : public virtual Person 
{
public:
	Student(const string& name, int num = 114514)
		:Person(name)
		,_num(num)
	{}
	
	int _num;
};

4.2 赋值运算符重载

对基类对象实现赋值需要显示调用基类的operator 函数

基类operator 函数的参数是基类类型的,由于之前讲到的切片机制 ,此时我们可以直接将派生类的对象作为参数传进去

cpp 复制代码
class Person
{
public:
	Person(const string& name ,int age = 18)
		:_name(name)
		,_age(age)
	{}
	void operator=(const Person& s)
	{
		_name = s._name;
		_age = s._age;
	}
	string _name;
	int _age;

};

class Student : public virtual Person 
{
public:
	Student(const string& name, int num = 114514)
		:Person(name)
		,_num(num)
	{}
	//这里是应用了切片
	void operator=(const Student& s)
	{
		Person::operator=(s);
		_num = s._num;
	}
	
	int _num;
};

4.3 析构函数

基类的析构函数将会在派生类的析构函数结束后自动调用。

4.4 析构和构造的顺序

构造:基类的构造是先于派生类的构造的 ,这是因为编译器认为基类对象的是在派生类其他成员之前声明的

析构:析构函数是在派生类的析构函数结束后自动调用的

五、如何实现一个不能被继承的类

方法1、使用final关键字 修饰基类(final关键字是C++11才有的新特性)

方法2、将基类的构造函数设为私有 ,这时候派生类就不能调用基类的构造函数,基类自然就无法被继承

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

六、友元和静态成员的继承

6.1 友元成员变量的继承

友元关是不可以被继承的,也就是在基类中声明了的友元函数不可以访问派生类的成员的

6.2 静态成员的继承

在继承中,派生类并不会生成一个新的静态成员,无论继承多少次,静态成员都只有一个 (就像是类中的静态成员也是自始至终只有一个一样)

七、多继承和菱形继承

7.1 多继承的概念和操作

多继承就是一个派生类有两个或以上的直接基类 ,此时在内存中先声明的基类在前,后声明的在后,派生类的新增成员在最后

cpp 复制代码
class Display : public Student,public Teacher
{
public:
	Display(const string& name)
		:Student(name)
		,Teacher(name)
		,Person(name)
	{}

	string _position;
};

7.2 菱形继承

菱形继承存在数据冗余和二义性的问题 ,就像在上述图中,Assistant类继承了Student 和 Teacher ,这时assistant 中存在两个Person对象,产生了数据冗余和二义性(不知道哪个Person对象是真正需要的)

cpp 复制代码
class Person
{
public:
	Person(const string& name ,int age = 18)
		:_name(name)
		,_age(age)
	{}
	void operator=(const Person& s)
	{
		_name = s._name;
		_age = s._age;
	}
	string _name;
	int _age;

};

class Student : public virtual Person 
{
public:
	Student(const string& name, int num = 114514)
		:Person(name)
		,_num(num)
	{}
	//这里是应用了切片
	void operator=(const Student& s)
	{
		Person::operator=(s);
		_num = s._num;
	}
	
	int _num;
};
class Teacher : public virtual Person
{
public:
	Teacher(const string& name ,const string& position = "沈阳大街")
		:Person(name)
		,_position(position)
	{}
	string _position;
};


//这里就是先在出现二义性得地方声明虚函数,但是此时二义性内容是存在一个公共空间,不属于任何一个对象
//出于从属关系的考量,我们需要在对象中显示调用Person类的构造函数
class Display : public Student,public Teacher
{
public:
	Display(const string& name)
		:Student(name)
		,Teacher(name)
		,Person(name)
	{}

	string _position;
};

八、菱形继承的解决方案:虚继承

为了解决菱形继承产生的种种问题,C++加入了虚继承特性,关键字是virual

8.1 虚继承的使用

在使用虚继承时在产生歧义的派生类的位置加上virtual关键字

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

8.2 虚继承的注意事项

8.2.1 虚继承的使用位置

需要注意的是,虚继承需要在产生歧义的位置声明,而不是一定在在派生类的直接基类 上声明

就像是这种情况,virtual没有在E的直接基类C和D上声明 ,而是在产生歧义的B和C上声明

8.2.2 虚继承的使用实际选择

在实际使用中,virtual的底层实现是很复杂的,所以不推荐预防式声明虚函数 ,因为这会大大降低运行效率

8.2.3 使用虚函数后的初始化

cpp 复制代码
class Display : public Student,public Teacher
{
public:
	Display(const string& name)
		:Student(name)
		,Teacher(name)
		,Person(name)
	{}

	string _position;
};

此时Person实际上是被单独摘出来的 ,其并不隶属于Student和Teacher ,所以需要显示调用Person的构造函数来对Person对象单独初始化

九、继承和组合

9.1 is-a关系和has-a关系

is-a:代表A是B,代表:张三是人,对应的编程思想:继承

has-a:代表包含关系,代表:汽车包含轮胎,对应的编程思想:组合

9.2 组合的优点

组合就是将另一个类包含进来,它具有以下优点:

1、不破坏封装,继承中派生类可以随意访问基类 斜体样式 的成员,这是不安全的

2、耦合度低,组合的方式只调用基类的部分接口,具体实现被屏蔽 ,不会因为基类的部分函数接口改变就出现需要大改的情况

9.3 组合和继承在实践中的选择

当组合和继承都是可选项时,优先选择组合

相关推荐
2501_908329851 小时前
嵌入式LinuxC++开发
开发语言·c++·算法
兑生1 小时前
【灵神题单·贪心】1833. 雪糕的最大数量 | 排序贪心 | Java
java·开发语言
实在智能RPA1 小时前
实在 Agent 支持哪些企业业务场景的自动化?全行业智能自动化场景深度拆解
java·运维·自动化
左左右右左右摇晃2 小时前
Java并发——偏向锁
java
moxiaoran57532 小时前
使用springboot+flowable实现一个简单的订单审批工作流
java·spring boot·后端
牧天白衣.2 小时前
07-常用API
java
Meepo_haha2 小时前
Tomcat闪退问题以及解决原因(三种闪退原因有解决办法)
java·tomcat·firefox
兑生2 小时前
【灵神题单·贪心】3010. 将数组分成最小总代价的子数组 I | Java
java·开发语言·算法
皮卡狮2 小时前
高阶数据结构:红黑树
c++