【C++】继承(二)深入理解继承:派生类默认成员函数与友元、静态成员的奥秘


前言

我们在上一章讲解了: 继承三部曲,本篇基于上次的基础继续深入了解继承的相关知识,欢迎大家和我一起学习继承

派生类的默认成员函数

在这里插入图片描述
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

①派生类的构造函数

派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

1.1、有默认构造的情况:

class Person
{
public:
	Person(const char* name="hhh")
		:_name(name)
	{
		cout << "Person() " << endl;
	}
protected:
	string _name;
};
class Student :public Person
{
protected:
	int _stuid;
};
int main()
{
	Student s;
	return 0;
}

在有默认构造的情况下,Student s,创建的派生类s对象会自动调用自己的默认构造,它里面的内置类型_stuid不做处理,但是继承父类里面的_name会被当做一个Person类的对象,也就是自定义类型成员,_name会调用Person的默认构造来初始化自己

1.2、没有默认构造的情况:

这个基类我们没有写无参默认构造,但是我们写了带参的默认构造,所以编译器不会为我们生成默认无参的构造函数

class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
		cout << "Person() " << endl;
	}
protected:
	string _name;
};

在派生类中如何初始化成员?
下面是❌示范

class Student:public Person
{
public:
	Student(int stuid=1001,const char* name="peter")
		:_name(name)//这里这样写会直接报错,有红色波浪线的那种,只是这里看不出来
		,_stuid(stuid)
	{
		cout << "Student()" << endl;
	}
protected:
	int _stuid;
};

我们不能够直接拿父类的成员出来单独进行初始化,父类的初始化要看作是一个整体,可以理解为父类的成员是隐藏在子类中的自定义类型成员,然而在初始化时,自定义类型需要走它的默认构造,即使我们不在初始化列表显示写,它也会走初始化列表,然而这里我们没有默认的Person构造函数,所以我们需要显示调用这个构造,我们看下面的正确写法

✔写法
我们显示调用Person类的构造来初始化从Person那边继承过来的成员变量就行了,这里就充分体现了父类的初始化要看成是一个整体

class Student:public Person
{
public:
	Student(int stuid=1001,const  char* name="peter")
		:Person(name)//如果这个构造函数有多个参数,那我们就传多个参数,看具体构造函数来传参
		,_stuid(stuid)
	{
		cout << "Student()" << endl;
	}
protected:
	//Person _p;//父类的成员就好似这样,需要我们走Person的构造,不能单独初始化里面的成员
	int _stuid;
};

当然除了上面这种写法我们还可以去父类自己写一个无参默认构造,这里我就不做演示了,如果不会可以评论,我再进行补充✍

总结:

派生类的初始化=父类+自己(内置类型和自定义类型),父类调用父类的构造函数初始化自己(这里体现了复用),在派生类中,要把父类成员当成一个整体的自定义类型成员,子类的其他成员和以前一样(对内置类型不做处理,对自定义类型去调它的默认构造)

形象的理解一下:父类是一个整体的概念

class BB
{
public:
	BB(int num,const char* name)
		:_p(name)//会在初始化列表调用Person的构造函数来初始name
		,_num(num)
	{}
private:
	Person _p;//这里显示有Person的对象
	int _num;
};

②派生类的拷贝构造函数

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

以下面这个父类来举例:

class Person
{
public:
	Person(const char* name = "hhh")
		:_name(name)
	{
		cout << "Person() " << endl;
	}
	Person(const Person& p)//拷贝构造
		:_name(p._name)
	{}
protected:
	string _name;
};

2.1、子类中不显示写拷贝构造,就使用编译器默认生成的拷贝构造

class Student :public Person
{
public:
	Student(int num=1001,const char* name="peter")
		:_num(num)
		,Person(name)
	{}
protected:
	int _num;
};
int main()
{
	Student s(1002, "okk");
	Student s1(s);
	return 0;
}

在实现用s拷贝s1时,派生类的拷贝构造和上面我们所说的默认构造有异曲同工之妙,他们都把父类成员当成一个整体的自定义类型成员,在走拷贝构造时,会去调用自定义类的拷贝构造

2.2、假如派生类需要写拷贝构造完成一些深拷贝,那我们要显示的写出拷贝构造,要怎么写父类的那一块呢?

class Student :public Person
{
public:
	Student(const Student& s)
		:_num(s._num)
		,Person(s)//显示调用基类的拷贝构造函数,用s来初始化Person部分 
	{}
	Student(int num=1001,const char* name="peter")
		:_num(num)
		,Person(name)
	{}
protected:
	int _num;
};

另外这里我们要知道,当一个派生类(如Student)的对象被创建时,其基类(如Person)的部分会首先被初始化。这是对象构造过程的一部分,它确保基类部分在派生类部分之前处于有效状态

③派生类的赋值构造

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

基类

class Person
{
public:
	Person(const char* name = "hhh")
		:_name(name)
	{
		cout << "Person() " << endl;
	}
	Person(const Person& p)
		:_name(p._name)
	{}
	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(const Student& s)
		:_num(s._num)
		,Person(s)
	{}
	Student(int num=1001,const char* name="peter")
		:_num(num)
		,Person(name)
	{}
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			//operator=(s);
			//在子类中这样调用父类中的赋值构造是不对的,他们函数名相同,会隐藏掉父类的operator=函数
			//这里如果这样写,会一直反复调用子类中的operator=,这样会栈溢出
			//如果想调到父类的operator=函数可以显示调用:Person::operator=(s);
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}
protected:
	int _num;
};

④派生类的析构函数

派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

class Person
{
public:
	Person(const char* name = "hhh")
		:_name(name)
	{
		cout << "Person() " << endl;
	}
	Person(const Person& p)
		:_name(p._name)
	{}
	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(const Student& s)
		:_num(s._num)
		,Person(s)
	{}
	Student(int num=1001,const char* name="peter")
		:_num(num)
		,Person(name)
	{}
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}
	~Student()
	{
		//~Person();//这里不能直接访问父类的析构函数,因为后续多态的需要,析构函数名字会被统一处理成destructor
		//所以这里析构也是被隐藏了,根本找不到这个析构名,所以直接报红色波浪线了
		//如果你想访问也可以,就显示调用他就好了
		Person::~Person();
		cout << "~Student()" << endl;
	}
Student s(1002, "okk");

如果我在子类析构函数中显示调用父类的析构函数,就会出问题1
在这里插入图片描述
这里我们父类的构造只构造了一次,却析构了两次,这样会造成不可预料的问题,所以我们就不该显示的写父类的析构函数

问题2:

~Student()
{
	Person::~Person();
	cout<<_name<<endl;//这里是父类的成员
	cout<<"~Student()"<<endl;
}

还有就是,如果我们先析构了父类,但是我们还需要用到父类的成员就会出现访问不到的情况,或者是其他不可预料的问题,在继承机制中,子类的析构函数通常会自动调用其父类的析构函数,所以父类的析构不需要我们显示写,不要画蛇添足

派生类对象析构清理先调用派生类析构再调基类的析构,要保证这个原则,所以我们不能显示调用父类的析构,将上面的子类析构函数改成:

~Student()
{
	cout<<"~Student()"<<endl;
}

总结:
派生类对象在初始化时:先父后子(如果你不信,可以调试看一下)
派生类对象在析构时:先子后父(这个就是继承机制的原因了)

继承与友元

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

class Person
{
public:
 friend void Display(const Person& p, const Student& s);
protected:
 string _name; // 姓名
};
class Student : public Person
{
protected:
 int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
 cout << p._name << endl;
 //cout << s._stuNum << endl;Display()函数是父类的友元,并不是子类的友元,不能访问子类的私有或者保护
}

如果你需要访问子类和父类的私有成员和保护成员,那你可以让这个函数即是父类的友元,也是子类的友元,一个函数可以同时是多个类的友元

继承与静态成员

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

注意:静态成员是属于类本身的,而不是类的实例(对象)的

class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。---统计Person及其Person对象一共产生了多少个
};
int Person::_count = 0;
//注意静态成员要在外面定义,定义的时候才会为他开空间
//由于静态成员是属于类本身的,而不是类的任何实例,所以它们需要有一个唯一的存储空间
//类外的定义确保了这一点

class Student : public Person
{
protected:
	int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};
int main()
{
	//_count 静态成员只有一份,当前类和它的派生类共用一个
	//父类静态成员属于当前类,也属于当前类的所有派生类
	cout << &(Person::_count) << endl;
	cout << &(Student::_count) << endl;
	cout << &(Graduate::_count) << endl;
	return 0;
}

由于Student类继承了Person类,所以他可以使用这个静态_count成员,至于Graduate 类他继承的是Student类,但是Student类继承了Person类,所以Graduate也可以使用_count成员,上面分别是从这三个类中找到_count对象并取出它的地址,打印出来我们会发现这是同一个地址,这就更验证了 父类静态成员属于当前类,也属于当前类的所有派生类

有了这个特性之后,我们可以用他来求父类在一个程序中总共创建了多少个对象,在构造函数里面加上_count++就可以统计出该程序从运行到结束一共创建了多少个对象,如果只想知道现在还存在的对象一共有多少个,就可以在析构函数里面写上_count--


本篇暂且先到这里,我们下篇见✋

  • 16
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值