C++:继承

前言:欢迎进入C++进阶的学习,我认为学习更加需要注重是的过程,而并非结果, 就如人固然会死但还是要吃饭。生命的意义在于每个人,每个人都是自己人生中的主角。人一到群体中,智商就严重降低,人们为了获得群体的认同,愿意抛弃独立性去换取那份让人倍感安全的归属感。所以在这个社会群体中,我们要找到属于自己的那条路。

我们先复习一下封装的概念:

1.数据和方法放到一起,把想给访问定义成公有,不想给你访问定义成私有和保护

2.一个类型放到另一个类型里面,通过typedef成员函数调整,封装成另一个全新的类型

继承的概念及定义

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

cpp 复制代码
class Person
{
public:
 void Print()
 {
 cout << "name:" << _name << endl;
 cout << "age:" << _age << endl;
 }
protected:
 string _name = "peter"; // 姓名
 int _age = 18;  // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
//Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象可
//以看到变量的复用。调用Print可以看到成员函数的复用。
class Student : public Person
{
protected:
 int _stuid; // 学号
};

class Teacher : public Person
{
protected:
 int _jobid; // 工号
};

int main()
{
 Student s;
 Teacher t;
 s.Print();
 t.Print();
 return 0;
}

继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分,子类和父类统一

总结:

  1. 基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的 不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它
  2. 基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected 。 可以看出保护成员限定符是因继承才出现的
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min( 成员在基类的访问限定符,继承方式 )
    public > protected > private。
  4. 使用关键字 class 时默认的继承方式是 private ,使用 struct 时默认的继承方式是 public , 不过
    最好显示的写出继承方式
  5. 在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承 ,也不提倡
    使用 protetced/private 继承,因为 protetced/private 继承下来的成员都只能在派生类的类里
    面使用,实际中扩展维护性不强。
    private:表示只能在该类中/类对象调用
    protect:表示派生子类中/子类对象调用,或该类中/类对象调用
    public:表示其他类对象,子类中/子类对象,本类中/类对象均可以调用
    想要在子类中调用父类中的private,可以将父类private间接成protect或public中的函数调用

想要在外界调用父类中的private,可以将父类private间接成public中的函数调用

基类和派生类对象赋值转换

1.派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用.这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。
2.基类对象不能赋值给派生类对象。
3.基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用.但是必须是基类
的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-
Time Type Information)的dynamic_cast 来进行识别后进行安全转换。

public继承后

每一个子类对象都是一个特殊的父类对象

cpp 复制代码
    切割/切片赋值兼容
	Student st;
	Person p = st;
	Person& ref = st;
	Person* ptr = &st;

	ref._name += 'x';
	ptr->_name += 'y';

这里不发生隐式类型转换,将子类对象包含的父类部分赋值给另一个父类对象,指针引用同理

继承中的作用域

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

cpp 复制代码
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
 void fun()
 {
 cout << "func()" << endl;
 }
};
class B : public A
{
public:
 void fun(int i)
 {
 A::fun();
 cout << "func(int i)->" <<i<<endl;
 }
};
void Test()
{
 B b;
 b.fun(10);
};

派生类的默认成员函数

6个默认成员函数,"默认"的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类 中,这几个成员函数是如何生成的呢?
1. 派生类的构造函数必须先调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员.因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。(先父后子)
6. 派生类对象析构清理先调用派生类析构再调基类的析构。(先子后父)
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同 。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系

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& 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(int num, const char* str, const char* name)
		:Person(name)
		,_num(num)
		,_str(str)
	{
		cout << "Student()" << endl;
	}

	// s2(s1)
	Student(const Student& s)
		:Person(s)
		,_num(s._num)
		,_str(s._str)
	{}

	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_num = s._num;
			_str = s._str;
		}

		return *this;
	}

	// 子类的析构也会隐藏父类
	// 因为后续多态的需要,析构函数名字会被统一处理成destructor
	~Student()
	{
		// 显示写无法先子后父
		//Person::~Person();

		cout << _name << endl;
		cout << "~Student()" << endl;

		// 注意,为了析构顺序是先子后父,子类析构函数结束后会自动调用父类析构
	}

protected:
	int _num;	 //学号
	string _str;

	// 父类成员,当成一个整体的一个自定义类型成员
	// 子类的成员(跟以前一样)
	// a、内置类型
	// b、自定义类型
};

构造函数:如果父类中没有默认构造函数,那么就需要在子类中复用,先父后子

如果父类中有默认构造函数,那么系统会按先父后子的顺序调用

析构函数:无需复用,系统直接先调用父类再调用子类

拷贝构造:显示调用则需要复用成先父后子

**operator=:**显示调用则需要复用成先父后子

相关推荐
tyler_download1 分钟前
golang 实现比特币内核:实现基于椭圆曲线的数字签名和验证
开发语言·数据库·golang
小小小~2 分钟前
qt5将程序打包并使用
开发语言·qt
hlsd#2 分钟前
go mod 依赖管理
开发语言·后端·golang
小春学渗透4 分钟前
Day107:代码审计-PHP模型开发篇&MVC层&RCE执行&文件对比法&1day分析&0day验证
开发语言·安全·web安全·php·mvc
杜杜的man6 分钟前
【go从零单排】迭代器(Iterators)
开发语言·算法·golang
亦世凡华、7 分钟前
【启程Golang之旅】从零开始构建可扩展的微服务架构
开发语言·经验分享·后端·golang
神仙别闹14 分钟前
基于MFC实现的赛车游戏
c++·游戏·mfc
小c君tt21 分钟前
MFC中 error C2440错误分析及解决方法
c++·mfc
测试界的酸菜鱼21 分钟前
C# NUnit 框架:高效使用指南
开发语言·c#·log4j
GDAL21 分钟前
lua入门教程 :模块和包
开发语言·junit·lua