C++继承保姆级讲解下

前言

在上一篇文章,我们讲到了继承的概念,作用域与友元函数的关系,接下来我们来继续深入学习继承的知识,如果文章有错误,欢迎在评论区指出!

接下来我们来看一个重点,派生类的默认成员函数!

派生类的默认成员函数

相信大家对于类的成员函数十分的不陌生了,知道类会有六大默认成员函数,如下图。派生类也属于类,理所应当也会有自己的默认成员函数,但由于继承的特殊性,他的默认成员函数会与我们普通类的默认成员函数不一样,接下来让我们来一一剖析吧。

构造函数

我们首先来看构造函数,每个类创建的时候都必须要先调用其构造函数,对于相应的数据进行初始化。

我们看如下情景,我们定义了一个Person类,然后Teacher类继承了Person类,此时我们老师类的组成就可以看为两大部分,第一部分是继承Person类的成员,第二部分是Teacher类特有的成员,在我们创建子类的对象时,就会调用子类的构造函数。

在这里为了直观些,将构造函数里面没有进行初始化,而是打印构造函数的名字。

cpp 复制代码
#include<iostream>
using namespace std;

class Person
{
public:
	Person(string name = "", int age = 0)
	{
		cout << "Person(string name = "", int age = 0)" << endl;
	}	
protected:
	string _name;
	int _age;
};

class Teacher:public Person
{
public:
	Teacher(string name="", int age=0, string workName="")
	{
		cout << "Teacher(string name="", int age=0, string workName="")" << endl;
	}

private:
	string _workName;
};



int main()
{
    Person pe("张三", 18);
	Teacher te("李老师", 18, "高级教师");
	
	return 0;
}

我们创建pe对象后会打印什么呢?pe就是一个普通的类,毫无疑问他会打印自己的构造函数,如下运行图。

但te对象实例化的就不是普通的类,他时一个子类,继承了父类的成员。此时我们可以推想下子类构造函数有两部分,一部分和父类成员完全相同,一部分是自己独有的,既然这样我们为什么不在子类构造函数中调用父类的构造函数,这样我们既可以省区重新写初始化父类成员的代码,又可以复用之前写的代码,就提高了代码的复用性,降低耦合性。

然后在子类的构造函数中初始化子类独有的成员。所以上述的代码运行结果如下。其中红色框是创建子类时调用的。

到了这里我们还没有结束,还有一个比较重要的问题,既然子类的构造函数要调用父类的构造函数,那么他在什么时候调用呢?在最开始还是结束,又或者说是中间?如下图

在结束时调用可以么?也说的过去,可以接受,初始化完子类的成员后,在初始化父类的成员。但这在一种情况下十分致命,那就是如果子类初始化要调用父类的成员就会出现访问未初始化变量。导致报错。

例如如下的情况,假如高级职称必须要30岁以上才可以,那么我们就会加个判断年龄的if语句,但此时的_age是父类成员还没有初始化,访问就会导致错误。因此放在结束的位置有些缺陷。

cpp 复制代码
Teacher(string name = "", int age = 0, string workName = "")
{
	if(_age>30)
	{
		_workName = workName;
	}
	cout << "Teacher(string name="", int age=0, string workName="")" << endl;
}

当然这个简单的情况,我们只需要将_age换成age函数参数就可以了,但在父类比较复杂的情况下,就不是换个变量名就可以解决的了。

在中间可以么?在中间的问题和在结束面临的问题一样,可能会访问到父类的对象,因此放在中间的位置也有些缺陷。

放在开始可以么?我们可以将子类中父类的成员抽象为一部分,假如在一开始调用父类的构造函数,父类的构造函数会使用子类的成员么?结果是不用的,父类本身就是一个独立的部分,他可以不依靠子类的部分存在,他可以完全依靠自己的成员初始化,这就没有了访问出错的机会了,所以放在开始相较于其他两种是最好的选择。

下面我们来看段代码

cpp 复制代码
class Person
{
public:
	Person(string name = "", int age = 0)
		:_name(name),
		_age(age)
	{
		cout << "Person(string name = "", int age = 0)" << endl;
	}
protected:
	string _name;
	int _age;
};

class Teacher :public Person
{
public:
	Teacher(string name = "", int age = 0, string workName = "")
	{
		_workName = workName;
		cout << "Teacher(string name="", int age=0, string workName="")" << endl;
	}
	void Print()
	{
		cout << _name << _age << _workName << endl;
	}

private:
	string _workName;
};



int main()
{
	Person pe("张三", 18);
	Teacher te("李老师", 18, "高级教师");
	
	te.Print();

	return 0;
}

这段代码的打印是什么呢?前面三行相信大家在看完上面的讲解后可以理解,那为什么第四行没有打印出名字和正确的年龄呢?

原因是我们在Teacher构造函数中没有显示的调用父类的构造函数,他调用的就是父类的默认构造函数,父类的默认构造函数就是没有名字的Person(string name = "", int age = 0)。改变十分简单,只要我们显示的调用父类的构造函数,并且传递参数就可以了。

将构造函数修改如下,

cpp 复制代码
Teacher(string name = "", int age = 0, string workName = "")
	:Person(name,age)
{
	_workName = workName;
	cout << "Teacher(string name="", int age=0, string workName="")" << endl;
}

修改后的结果就是正确的了,但注意我们为了先使用父类的构造函数,将他放在了成员初始化列表中,确保了他最先运行。语法规定也必须在初始化列表,如果在函数体中调用,就会产生如下多次调用构造函数情况。

到这里构造函数就讲的差不多了,最后相信大家一定见过下面的代码,那么他是错的还是对的呢?

cpp 复制代码
Teacher(string name = "", int age = 0, string workName = "")
	:_workName(workName),
	Person(name, age)	
{
	cout << "Teacher(string name="", int age=0, string workName="")" << endl;
}

上述代码没有将父类的构造函数放在成员初始化列表的第一个,而是第二个。这个其实是对的,理解这个首先我们要明白的是初始化列表的初始化顺序不是按照构造函数中写的顺序_workName(workName), Person(name, age) ,而是按照声明的顺序,C++规定子类中父类成员是第一个声明的,所以无论父类构造函数写在成员初始化列表的那个位置上,他都是最先运行的。

拷贝构造与赋值重载函数

理解完构造函数后,那么拷贝构造与赋值重载函数不都是初始化一个变量么?那么他们的调用顺序就和构造函数一样,先调用父类的拷贝构造/赋值重载函数,再调用子类的拷贝构造/赋值重载函数即在子类对应函数体中实现。

同时为了保持先调用父类的,就必须在初始化列表调用函数,与构造函数的逻辑一模一样,不过是函数名字换了罢了。

析构函数

接下来我们来看下析构函数,负责清理对应类开辟的空间,与构造函数的用法类似,既然我们有父类的析构函数,我们对父类对象就直接调用父类的析构函数就可以了,没必要在自己写了。

既然如此,我们同样面对一个问题,父类的析构函数应该放在哪里?

放在开始?放在开始先释放父类的成员,在释放子类的成员,似乎也是个不错的选择,但还是有一个致命的缺陷,如果子类析构函数实现要借助父类的成员怎么办?此时父类已经释放了,此事再调用就是非法访问了,会造成错误,所以有所缺陷。

放在中间?遇到的问题和放在开始一样,子类可能调用父类已经释放的成员,从而造成错误。

放在结束?此时子类释放可以调用父类的成员,不会造成错误,父类是一个单独的个体,有自己完善的析构函数,不借助子类释放空间,因此又不影响父类成员释放,所以综合之下最好。

那么接下来看段代码

cpp 复制代码
#include<iostream>
using namespace std;


class Person
{
public:
	Person(string name = "", int age = 0)
		: _name(name),
		_age(age)
	{

	}

	~Person()
	{
		cout << "^ Person()" << endl;
	}
protected:
	string _name;
	int _age;
};

class Teacher :public Person
{
public:
	Teacher(string name = "", int age = 0, string workName = "")
		:_workName(workName),
		Person(name, age)	
	{
	}
	~Teacher()
	{
		cout << "^Teacher()" << endl;
	}
private:
	string _workName;
};



int main()
{
	Teacher te("李老师", 18, "高级教师");
	
	return 0;
}

上述的代码结果是什么呢?结构如下,相信大家看完上面的解释可以明白调用顺序。注意再这里子类的析构函数中我们没有显示的调用父类的析构函数,编译器默认在子类的析构函数结束后调用父类的析构函数,如果我们显示的加上析构函数就会造成空间多次释放报错!!

在这里我们可以多想一下,为什么构造函数必须在初始化列表写?而析构函数编译器默认帮我们调用?因为构造函数有带参的重载,而析构函数只有一个无参的函数,编译器可以明确调用那个析构函数,而构造函数要用户自己确定!!

取地址重载函数

这两个默认成员函数,我们用的默认生成的就可以了,不需要再修改什么了。对于对象取地址够用了。

继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例 。

父类与子类共享这个静态成员。下面我们来看段代码

cpp 复制代码
#include<iostream>
using namespace std;
class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};
void TestPerson()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << " 人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << " 人数 :" << Person::_count << endl;
}

int main()
{
	TestPerson();
	return 0;
}

结果如下,不管是子类还是父类,我们创建了对象就会使静态成员_count加一,所以第一个打印4,随会将其值为零,打印0.

菱形继承

在继承中,我们不仅有单继承,还有多继承。单继承就是继承一个父类,多继承就是继承多个父类。多继承的父类之间以逗号隔开。如下图。

在现时的生活中,很多的人都是多重身份的,比如说大学中的助教,他在老师哪里就是学生,在大学新生这里就是老师,所以我们定义一个助教的身份继承老师和学生类是合情合理的。

大家看如下代码,为了更好的体现问题将,成员设置为了公有可以在外部访问。

cpp 复制代码
#include<iostream>
using namespace std;

class Person
{
public:
	Person(string name = "", int age = 0)
		: _name(name),
		_age(age)
	{
	}

	~Person()
	{
		cout << "^ Person()" << endl;
	}

	string _name;
	int _age;
};

class Teacher :public Person
{
public:
	Teacher(string name = "", int age = 0, string workName = "")
		:_workName(workName),
		Person(name, age)	
	{
	}
	~Teacher()
	{
		cout << "^Teacher()" << endl;
	}

	string _workName;
};

class Student:public Person
{
public:
	Student(string name = "", int age = 0, string major = "")
		:Person(name,age)
	{
		_major = major;
	}
	string _major;
};

class Assistant :public Student, public  Teacher
{
public:
	Assistant(string name = "", int age = 0, string major = "", string workName = "")
		:Teacher(name,age, workName),
		Student(name,age,major)
	{

	}

};



int main()
{
	Assistant as("张三", 18, "高分子", "助教");
	
	return 0;
}

此时我们的继承图如下

形状类似菱形,我们称之为菱形继承,那么这样做有什么问题么?

假如我们定义一个Assistant对象as,然后访问其中的age设置为18会发送什么?

结果是出现错误,为什么呢?我们已经将age设置为公有了!其实这里是编译器不知道你要访问哪一个,前面提到了我们继承老师与学生类,他们每个类中都有age变量,直接使用age不知道调用哪一个父类的age对象,修改如下加上类域就可以了。

上述修改后的代码就没有错误了,虽然我们可以解决上述的问题,但是一个人不可能有两个年龄,一个对象存两份相同的代码不仅没用,还会消耗内存,于是C++引入了语法虚继承,在可能·发生菱形继承的基类上加个关键字virtual就可以了。

修改后的代码就可以直接访问age了

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

	~Person()
	{
		cout << "^ Person()" << endl;
	}

	string _name;
	int _age;
};

class Teacher : virtual public Person
{
public:
	Teacher(string name = "", int age = 0, string workName = "")
		:_workName(workName),
		Person(name, age)	
	{
	}
	~Teacher()
	{
		cout << "^Teacher()" << endl;
	}

	string _workName;
};

class Student:virtual public  Person
{
public:
	Student(string name = "", int age = 0, string major = "")
		:Person(name,age)
	{
		_major = major;
	}
	string _major;
};

class Assistant :public Student, public  Teacher
{
public:
	Assistant(string name = "", int age = 0, string major = "", string workName = "")
		:Teacher(name,age, workName),
		Student(name,age,major)
	{

	}

};



int main()
{
	Assistant as("张三", 18, "高分子", "助教");
	as._age = 19;
	return 0;
}

到这里C++继承有关的知识就已经讲完了,如果文章有错误还望多多包涵,在评论区指出。看到这,喜欢的点点关注吧!

相关推荐
_.Switch1 分钟前
Python 自动化运维持续优化与性能调优
运维·开发语言·python·缓存·自动化·运维开发
徐*红2 分钟前
java 线程池
java·开发语言
尚学教辅学习资料2 分钟前
基于SSM的养老院管理系统+LW示例参考
java·开发语言·java毕设·养老院
1 9 J4 分钟前
Java 上机实践4(类与对象)
java·开发语言·算法
Code apprenticeship4 分钟前
Java面试题(2)
java·开发语言
J不A秃V头A7 分钟前
Python爬虫:获取国家货币编码、货币名称
开发语言·爬虫·python
SRY122404193 小时前
javaSE面试题
java·开发语言·面试
李元豪3 小时前
【智鹿空间】c++实现了一个简单的链表数据结构 MyList,其中包含基本的 Get 和 Modify 操作,
数据结构·c++·链表
无尽的大道3 小时前
Java 泛型详解:参数化类型的强大之处
java·开发语言
ZIM学编程3 小时前
Java基础Day-Sixteen
java·开发语言·windows