C++第三十弹---C++继承机制深度剖析(中)

✨个人主页:熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】

目录

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

1.1、派生类的构造函数

1.2、派生类的拷贝构造函数

1.3、派生类的赋值重载

1.4、派生类的析构函数

2、继承与友元

3、继承与静态成员


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

6个默认成员函数,"默认"的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类

中,这几个成员函数是如何生成的呢?

1.1、派生类的构造函数

派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。 如果基类没有默认的构造函数(无参构造函数,全缺省构造函数,自己没写编译器自动生成的构造函数),则必须在派生类构造函数的初始化列表阶段显示调用。

1.有默认构造

父类

class Person
{
public:
    // 默认构造 全缺省构造函数
	Person(const char* name = "jack")
		: _name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name; // 姓名
};

子类

class Student : public Person
{
	int _num;	
	string _str;
};

主函数

int main()
{
    Student s;
    return 0;
}

当父类中有默认构造时,Student s ,实例化子类对象会自动调用自己的默认构造,子类中的内置类型不做处理,自定义类型调用它的构造函数,父类则调用父类的默认构造。

2.没有默认构造

class Person
{
public:
	Person(const char* name)
		: _name(name)
	{
		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;
	}
protected:
	int _num;	
	string _str;
};

注意:

需要把父类对象类比成自定义类型来处理,初始化时需要调用父类的构造函数,不能直接初始化父类成员。

下面为错误示范:

Student(int num, const char* str, const char* name)
		:_name(name) // 错误的初始化
		, _num(num)
		, _str(str)
	{
		cout << "Student()" << endl;
	}

1.2、派生类的拷贝构造函数

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

父类

class Person
{
public:
	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}
protected:
	string _name; // 姓名
};

子类

手动写拷贝构造

class Student : public Person
{
public:
	// s2(s1)
	Student(const Student& s)
		:Person(s)// 调用父类拷贝构造,传子类会切片
		, _num(s._num)
		, _str(s._str)
	{}
protected:
	int _num;	 //学号
	string _str;
};

如果拷贝中没有深拷贝的情况,那么派生类的拷贝构造可以不写,因为编译器会自动生成,但是如果拷贝中存在深拷贝的情况,那么则需要我们手动写派生类的拷贝构造。

1.3、派生类的赋值重载

派生类的operator=必须要调用基类的operator=完成基类的复制。

class Person
{
public:
	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;
		return *this;
	}
protected:
	string _name; // 姓名
};

手动写派生类的赋值重载

class Student : public Person
{
public:
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			// 派生类与基类构成隐藏
			// operator=(s);//直接调用赋值操作符重载会崩溃,因为构成隐藏,会调用自己
			Person::operator=(s);
			_num = s._num;
			_str = s._str;
		}
		return *this;
	}
protected:
	int _num;	 //学号
	string _str;
};

如果赋值中没有深拷贝的情况,那么派生类的赋值重载可以不写,因为编译器会自动生成,但是如果赋值中存在深拷贝的情况,那么则需要我们手动写派生类的赋值重载。

1.4、派生类的析构函数

派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员 。因为这样才能

保证派生类对象先清理派生类成员再清理基类成员的顺序。

class Person
{
public:
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
};

手动写派生类的析构函数

class Student : public Person
{
public:
	// 子类的析构也会隐藏父类
	// 因为后续多态的需要,析构函数名字会被统一处理成destructor
	~Student()
	{
		// 显示写无法先子后父
		Person::~Person();
		//~Person();//无法调用,与父类构成隐藏,根据就近原则会调用自己
		cout << "~Student()" << endl;
		// 注意,为了析构顺序是先子后父,子类析构函数结束后会自动调用父类析构
	}
	// 构造函数是先父后子
protected:
	int _num;	 //学号
	string _str;
};

int main()
{
	Student s(1, "xxxx", "张三");
}

当我们手动写析构函数,会多调用一次父类构造,正确的做法是不写派生类的析构函数,因为编译器会自动调用。

知识补充:

  1. 派生类对象初始化先调用基类构造再调用派生类构造

  2. 派生类对象析构清理先调用派生类析构再调用基类的析构

因为子类可能子类还没有析构时可能会调用父类成员,如果先析构父类,可能无法正常访问父类成员,因此需要保证析构先子后父。

  1. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

2、继承与友元

友元关系不能继承 ,也就是说基类友元可以访问基类的私有和保护成员,但是不能访问子类私有和保护成员。

例如下面的代码:

// 友元关系不能继承
class Student;// 必须先声明,否则会报错
class Person
{
public:
	// 声明Display是Person的友元
	friend void Display(const Person& p, const Student& s);
protected:
	string _name = "Bob"; // 姓名
};
class Student : public Person
{
protected:
	int _stuNum = 1021; // 学号
};
// 友元可以在类外调用成员
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;// 在派生类中加入友元则可以调用
}

注意:

Student类需要先声明,因为Person类中声明友元时不知道Student是类。

主函数

int main()
{
	Person p;
	Student s;

	Display(p, s);
	return 0;
}

将子类加入友元声明之后:

class Student : public Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	int _stuNum = 1021; // 学号
};

打印函数在父类定义了友元但是在子类函数没有定义友元,且还想访问子类成员的办法是:在子类中也定义一个友元。

3、继承与静态成员

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

统计Person实例化对象的个数?

因为子类实例化对象时会调用父类的构造函数,因此调用多少次父类的构造函数,则实例化了多个个Person对象。

class Person
{
public:
	Person() { ++_count; }// 默认构造++
	Person(const Person& p) { ++_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; // 研究科目
};

主函数

int main()
{
	Person p;
	Student s1;
	Student s2;
	Student s3(s1);
	Graduate g1;

	cout << &Person::_count << endl;//不同类域中访问的地址相同
	cout << &Student::_count << endl;

	cout << Student::_count << endl;//打印结果相同
	cout << Person::_count << endl;
	return 0;
}

测试结果

统计出了结果且通过打印地址证明了成员变量处于同一块空间。

相关推荐
励志成为嵌入式工程师2 分钟前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉32 分钟前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer36 分钟前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq39 分钟前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
wheeldown1 小时前
【数据结构】选择排序
数据结构·算法·排序算法
记录成长java2 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山2 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
hikktn2 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust
青花瓷2 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
睡觉谁叫~~~2 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust