C++ --- 继承

C++ --- 继承

一、什么是继承

继承是C++面向对象语言三大特点之一,继承体现的是类与类之间的关系,被继承的类称为基类(父类),继承此基类(父类)的类称为派生类(子类),目的是为了提高代码的复用性。

二、继承语法

在C++编程语言中,继承语法是:派生类类名: 继承类型 基类名

下面是一个最简单的继承演示:

cpp 复制代码
#include<iostream>

// 定义基类A
class A
{
private:
	int a1;
	int a2;

public:
	void FuncA()
	{
		std::cout << "这是基类" << std::endl;
	}
};

// 定义派生类B
class B: public A
{
private:
	int b1;
	int b2;

public:
	void FuncB()
	{
		std::cout << "这是派生类" << std::endl;
	}
};


int main()
{
	B b;
	b.FuncA();      // 调用到了A类的函数FuncA
	b.FuncB();      // 调用自己B类的函数FuncB
	return 0;
}

运行结果如下:

2.1 继承类型

继承类型和之前类当中的成员访问限定符是类似的,有三种继承类型:public继承,private继承,protected继承。
注意:不写继承类型class默认是private继承,struct默认是public继承。

在前面的学习中类内部的成员也是有自己的访问限定符的限制的,这样和继承类型组合总共有九种组合:

最常用的继承类型是public继承,我图上标错了。

对于public访问,在基类和派生类内部可以直接访问,同时在类外也可以直接访问。

对于protected访问,只能在基类和派生类内部可以直接访问,不能在类外访问。

2.2 模板类继承

当派生类继承的基类是模板类的时候,派生类内部访问模板基类的成员需要指定基类的类域:

c 复制代码
namespace dyj
{
	//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等成员函数未实例化,所以找不到
			std::vector<T>::push_back(x);
			//push_back(x);
		}
		void pop()
		{
			std::vector<T>::pop_back();
		}
		const T& top()
		{
			return std::vector<T>::back();
		}
		bool empty()
		{
			return std::vector<T>::empty();
		}
	};
}
int main()
{
	dyj::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		std::cout << st.top() << " ";
		st.pop();
	}
	return 0;
}

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

一句话:派生类实例化的对象可以赋值给给基类的指针或者引用;派生类的对象可以直接赋值给基类对象(也叫做切片),但是基类对象不能直接赋值给派生类对象 ,总结就是子可以给给父,但是父不能给给子。

例如:

cpp 复制代码
// 基类Person
class Person
{
protected:
	std::string _name; // 姓名
	std::string _sex; // 性别
	int _age; // 年龄
};

// 派生类Student
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;
}

在前面的学习中一个类型的对象赋值给给另一个对象的指针或者引用的时候,存在类型转换,会产生临时对象,临时对象具有常性,需要在引用或者指针变量之前加上const,而继承这里派生类对象给给基类的指针或者引用时就不需要加上const了。

四、继承的作用域

基类和派生类拥有各自独立的作用域,由此得到一个新的访问关系叫做隐藏。

4.1隐藏关系

当基类和派生类中存在相同的成员(属性,方法)时,基于派生类实例化出来的对象,访问其同名成员时,会将基类的同名成员彻底隐藏起来,访问的全部都是派生类中的成员,若要指定访问基类中的成员时,需要指定基类的类域:基类类域::基类成员。

例如:

基类Person中有_age属性,继承基类的派生类Student中也有_age属性,通过派生类实例化出来的sobj对象,调用打印方法打印属性时访问的就是Student中也有_age属性,基类中的_age属性就被隐藏起来了。

cpp 复制代码
// 基类Person
class Person
{
protected:
	std::string _name = "张三"; // 姓名
	std::string _sex = "男"; // 性别
	int _age = 10; // 年龄

public:
	void Print()
	{
		std::cout << "_name:" << _name << std::endl;
		std::cout << "_sex:" << _sex << std::endl;
		std::cout << "_age:" << _age << std::endl;
		std::cout << "_age:" << Person::_age << std::endl;
	}
};

// 派生类Student
class Student : public Person
{
protected:
	int _No = 10; // 学号
	int _age = 999; // 与基类同名的属性

public:
	void Print()
	{
		std::cout << "_name:" << _name << std::endl;
		std::cout << "_sex:" << _sex << std::endl;
		std::cout << "_age:" << _age << std::endl;
		std::cout << "_age:" << Person::_age << std::endl;   // 指定访问基类中的_age
		std::cout << "_No:" << _No << std::endl;

	}
};

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

	return 0;
}

运行结果为:

注意:成员函数只要函数名相同就构成隐藏,所以在实际情况中建议不要定义同名成员,这样容易引起混淆。

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

这里之举例最重要的四个:构造,拷贝构造,赋值重载,析构

5.1析构函数

派生类继承基类之后,成员就包含继承自基类的成员,以及自己特有的成员。

在派生类中我们不写编译器默认生成的构造函数(构造函数多半还是会自己去显示写的)会将派生类的成员分为两部分:

(1)继承自基类的成员

(2)自己特有的成员

对继承自基类的成员会将其看作整体,调用基类的拷贝构造;对自己特有的成员处理和类和对象那里讲的一样:对内置类型不做处理或初始化成0/1(看实际编译器的处理结果),对自定义类型调用它自己的默认构造函数。
我们显示写的构造函数也依旧分为了两部分:

(1)继承自基类的成员

(2)自己特有的成员

对自己特有的成员,就按正常的初始化方式(初始化列表/函数体内部初始化)进行初始化;对继承自基类的成员必须使用基类类名(对应的属性),这样进行初始化,否则会报错的。

举例伪代码:

cpp 复制代码
class Person
{
// 基类的构造函数
public:
	Person()
		:...
	{}
protected:
	string _name = "张三";
	// ...
}

class Student: pubilc Person
{
// 显示写构造函数
public:
	Student(const string& name,const int& id)
		:Person(name)
		,_id(id)
	{}

private:
	int _id = 0;
	// ...
}

5.2拷贝构造 and 赋值重载

知道了构造函数在派生类中的处理,其实剩下的处理方式都是差不多的,将成员分成两部分,继承基类的成员调用对应的默认成员函数,派生类独有的按照正常方式处理即可。

同时这两个基本上是一模一样的处理方式。

我们不写编译器默认生成的拷贝构造/赋值重载,对继承基类的成员调用基类的拷贝构造/赋值重载;对派生类独有的成员,对于内置类型进行值拷贝,对于自定义类型调用自己的拷贝构造/赋值重载。

当有指向的资源这种成员就需要自己显示写拷贝构造/赋值重载,同样对于派生类独有的成员按照正常的写法去写即可;对继承基类的成员,拷贝构造需要:基类类名(传值过来的对象),赋值重载因为都是叫operator=,所以派生类和基类的赋值重载构成隐藏关系,需要指定基类类域::operator=(传值过来的对象),这里的特殊化处理道理同构造函数。

5.3析构函数

有了上面三个成员函数的演示,析构函数其实也是大差不差的处理方式:

编译器默认生成的析构函数,对于继承基类的成员调用基类的析构函数;对于派生类独有的成员,对于内置类型不做处理,对于自定义类型调用自己的析构函数。

当有资源释放的时候需要显示写析构函数,在这里又经过特殊化处理,对继承基类的成员不用去写它的析构函数,编译器会默认调用基类的析构,因为必须要满足合理的析构顺序:先子后父,以免先析构基类的成员,后析构子类成员时,子类中有调用父类的成员时会发生访问已析构的成员而导致报错;对派生类独有的成员该delete就去delete。

补充一下:虽然说继承基类的那部分成员的析构函数不用显示去写,但是实际调用形式还是要知道的,在多态的影响(重写关系)下,~类名的析构函数名字会被特殊处理destructor(就是析构函数的英文名),同样派生类和基类会构成隐藏关系,需要指定基类类域,实际上的名称是基类类域:: ~基类类名()。

六、继承中的特殊情况

除了上述四当中的隐藏关系,还有两种特殊的情况

6.1继承中的友元

友元关系的作用时,我属于你的友元,我就可以访问你的私有属性,但是在继承这里,友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员。

6.2继承中的静态成员

在基类中定义了一个静态成员,派生类继承基类,派生类和基类共用此静态成员,验证就是访问两个类中的静态成员地址,发现它们俩的地址是同一份,而非静态成员继承下来后地址是不一样的。

七、多继承问题

在C++中可以进行多继承,但是用的很少,继承关系多了起来就很难处理,例如菱形继承。

菱形继承是一个派生类继承了两个基类,而两个基类又同时继承另一个基类,形象化表示就为一个菱形,菱形继承有两个问题,数据冗余和二义性。

为了解决这个问题,设计了一个虚继承(virtual关键字),放在菱形的腰部,也就是两个基类同时继承另一个基类中的两个基类这里,这样就可以解决菱形继承的问题,但是虽然有解决方法,依旧不推荐写出菱形继承。

相关推荐
冉佳驹1 小时前
C++ ——— 基本特性解析
c++·引用·内联函数·范围for·命名空间·缺省参数·auto
沐知全栈开发1 小时前
HTML DOM 对象
开发语言
IMPYLH1 小时前
Lua 的 pairs 函数
开发语言·笔记·后端·junit·单元测试·lua
7***n751 小时前
JavaScript混合现实案例
开发语言·javascript·mr
xlq223221 小时前
18.Stack——queue(上)
开发语言·c++
程序员-周李斌2 小时前
Java 代理模式详解
java·开发语言·系统安全·代理模式·开源软件
G***T6912 小时前
Python项目实战
开发语言·python
重启的码农2 小时前
enet源码解析(5)事件驱动服务 (Event Service)
c++·网络协议
Elias不吃糖2 小时前
SQL 注入与 Redis 缓存问题总结
c++·redis·sql