目录
前情回顾:
一.构造函数和析构函数:
情况1:子类没有写构造和析构函数时:
运行结果:
构造函数:
析构函数:
情况2:父类的构造函数并没有为成员变量初始化赋值,而子类中的构造函数为父类继承过来的成员变量赋了值
解决办法:
对于子类的析构函数而言:
二.拷贝构造和赋值重载:
子类的拷贝构造
子类的赋值重载函数:
正确的解决方法:
有了继承后的子类的几个成员函数简单总结:
三.子类成员变量的初始化赋值:
在前两篇博客中,我们对C++继承特性有了比较深刻的理解,今天来看看继承在类的默认六大函数中又会发生什么情况。
前情回顾:
之前提到过类有六大默认成员函数:构造函数、析构函数、拷贝构造函数、赋值重载函数、取地址函数、。在这六大函数中,前四个是重点要学习的。在一个类中,我们主动写了这几大成员函数,那么编译器也就不会主动生成;若我们不写,编译器则自动生成——这就是默认行为。
但对于类的成员变量,有两种类别:1是内置类型(int、double、char、size_t等);2是自定义类型(String、vector、自己写的一个类......)。我们自己写的构造/拷贝构造函数,能对该类的内置成员和自定义成员都有详细而又具体的初始化赋值;但若不写,编译器生成的这几个函数对内置类型的成员变量是采取放任不管理的方式——给随机值。对自定义类型则会自动调用该自定义类中的构造/拷贝构造函数。
若是成员变量中有指向堆区空间的、打开文件的指针变量,默认行为的成员函数会造成浅拷贝,导致多个类对象在使用编译器默认生成的成员函数时会指向同一块空间,最后会发生多次析构和内存泄漏等异常情况,导致系统崩溃。所以有时需要我们自己去写这些成员函数,完成深拷贝。
回顾完类的这几大成员函数后,我们来学习在继承状态下,子类和父类的这几大成员函数直接又会擦出怎样的火花吧!
一.构造函数和析构函数:
情况1:子类没有写构造和析构函数时:
class Person {
public:
Person(int age = 18, string name = "王圆")
:_age(age)
, _name(name){
cout << "Person()构造函数" << endl;
}
~Person() {
cout << "~Person()的析构函数" << endl;
}
public:
int _age;
string _name;
};
class Student :public Person {
public:
//子类并没有写构造函数,也没有写析构函数
public:
int _id;
};
int main() {
Student s1; //创建子类对象
return 0;
}
在上方代码中,子类的构造函数和析构函数全是默认生成的函数,在创建子类对象后,会调用默认构造函数,子类中的成员变量有三个,一个是自家类定义的_id,剩下的是从父类继承过来的_age和_name。
运行结果:
构造函数:
因为子类Student并没有自己的构造函数,而且Student类是继承的Person类,说明白点就是:Student是Person的子女,Student既然继承了Person,那么Student也拥有了父类成员_name的使用权,但是当Student类创建对象,并为对象的成员变量初始化的时候,子类并不能给_name(父类的成员变量)赋值,它需要调用父类的构造函数为_name赋值才行。
总结:对于从父类继承过来的成员变量而言,子类对象只有使用权,那么在使用之前,需要让父类进行创建初始化!!!子类是没有这个权限的!
析构函数:
在程序即将结束时,子类对象需要析构自家类的成员变量,又因为子类继承了父类的_name和_age,子类不能去析构父类传承下来的成员,所以它只能去调用父类的析构函数去解决(说白了就是打狗还得看主人呢!小狗是父类的,父类传参给子类——只是给了子类养狗的权力,但子类没有权利决定小狗的生死)
总结:子类对象使用完从父类继承来的成员变量后,在作用域即将销毁之际,需要让父类进行销毁!!!子类没有这个权限!
情况2:父类的构造函数并没有为成员变量初始化赋值,而子类中的构造函数为父类继承过来的成员变量赋了值
错误案例示范:
class Person {
public:
//注:下面构造函数中,并没有给成员变量赋初值
Person(const char* name="小李", int age=20)
:_name(name)
,_age(age)
{
cout << "Person()构造函数" << endl;
}
~Person() {
cout << "~Person()的析构函数" << endl;
}
public:
const char* _name;
int _age;
};
class Student :public Person {
public:
//子类的构造函数为父类继承过来的_name,_age做了赋初值操作
Student(const char* name="zzz", int age=5, int id=0105)
:_name(name)
,_age(age)
,_id(id) {
cout << "Student()的构造函数" << endl;
}
public:
int _id;
};
int main() {
Student s1("张三",25,202001); //报错
return 0;
}
上面就是子类对继承父类的成员所做的初始化操作——错误示范。
_name,_age是父类继承过来的成员变量,第一次赋初值仍得由父类的构造函数去实现,子类的这种做法,越界了,不该你做的事抢着做,就会付出代价!!!
解决办法:
class Person {
public:
Person(const char* name = "王圆", int age = 18)
:_name(name)
, _age(age)
{
cout << "Person()构造函数" << endl;
}
~Person() {
cout << "~Person()的析构函数" << endl;
}
public:
const char* _name;
int _age;
};
class Student :public Person {
public:
Student(const char* name,int age, int id)
//改进具体:让父类的成员调用它自己的构造函数
:Person(name, age)
, _id(id) {
cout << "Student()的构造函数" << endl;
}
public:
int _id;
};
int main() {
Student s1("张三", 25, 202001);
return 0;
}
运行结果:
从图上看,子类对象创建时,进入Student构造函数,先初始化父类成员,进入父类的构造函数中,之后才轮到子类的成员初始化。
对于子类的析构函数而言:
作用域即将结束时,子类对象会销毁,其子类析构函数被编译器调用,但是子类析构函数体中不用显示调用父类的析构,编译器会自动调用的,若是强行写上,万一父类的成员变量是指针类指向的空间,多次析构会造成崩溃!! !
总结:子类对象被创建后,构造的顺序:先构父后构子; 析构的顺序:先析子后析父
二.拷贝构造和赋值重载:
子类的拷贝构造
由于子类继承了父类成员,子类的拷贝构造只能解决自己类中的成员,对于继承过来的还是得需要调用父类的拷贝构造去解决。
class Person {
public:
Person(const char* name = "王圆",int age=18)
:_age(age)
, _name(name) {
cout << "Person()构造函数" << endl;
}
//拷贝构造
Person(const Person& p) {
cout << "Person(const Person & p)的拷贝构造函数" << endl;
}
~Person() {
cout << "~Person()的析构函数" << endl;
}
public:
int _age;
const char* _name;
};
class Student :public Person {
public:
Student(const char* name,int age,int id)
:Person(name,age)
,_id(id) {
cout << "Student()的构造函数" << endl;
}
//子类的拷贝构造函数
Student(const Student& s)
:Person(s) {
cout << "Student(const Student& )的拷贝构造函数" << endl;
}
~Student() {
cout << "~Student()的析构函数" << endl;
}
public:
int _id;
};
在子类的拷贝构造中,初始化列表的Person()中为什么能填形参s呢?
原因:之前讲了子类对象是能够赋值给父类对象的——切片操作,回顾C++继承——子类对象赋值转换父类对象。那么调用子类对象的拷贝构造,从父类继承的成员变量也需要让父类调用它自己的拷贝构造完成变量初始化!而形参s只是把它从父类继承来的成员变量切片出来传给了父类对象,由父类完成这些成员的拷贝构造
子类对象赋值父类对象向上转换知识点:
(489条消息) C++——继承(2)详解_橙予清的zzz~的博客-CSDN博客https://blog.csdn.net/weixin_69283129/article/details/132011160?spm=1001.2014.3001.5502
运行结果:
子类的赋值重载函数:
子类的赋值重载运算符函数与拷贝构造也是同理,在子类的赋值重载中,调用父类的赋值重载函数。
class Person {
public:
Person(const char* name = "王圆", int age = 18)
:_age(age)
, _name(name) {
cout << "Person()构造函数" << endl;
}
//赋值重载
Person& operator=(const Person& p) {
cout << "Person operator=()赋值重载函数" << endl;
if (this != &p)
_name = p._name;
_age = p._age;
return *this;
}
~Person() {
cout << "~Person()的析构函数" << endl;
}
public:
int _age;
const char* _name;
};
class Student :public Person {
public:
Student(const char* name, int age, int id)
:Person(name, age)
, _id(id) {
cout << "Student()的构造函数" << endl;
}
Person& operator=(const Student& s) {
cout << "Student operator=()赋值重载函数" << endl;
if (this != &s) {
operator=(s);
_id = s._id;
}
return *this;
}
~Student() {
cout << "~Student()的析构函数" << endl;
}
public:
int _id;
};
int main() {
Student s1("武七",10,12306);
Student s2("李九",46,00000);
s2 = s1;
运行结果:
由运行结果知:s1在赋值s2的过程中发生了栈溢出,系统崩溃了,从最右边的图可知,编译器一直在重复调用operator=()赋值重载函数, 这是为什么?
原因:
正确思路如上!
错误的解决方法如上!
我们原本想着子类自己的成员可以自行处理,继承过来的就要交给父类的赋值重载,由于operator=函数是父类和子类都有的函数,它们俩形成了隐藏关系,但是编译器是优先找子类中的operator=函数,所以造成了自己递归自己的情形。
正确的解决方法:
给编译器指定operator=函数的位置去调用。
有了继承后的子类的几个成员函数简单总结:
1. 子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用。
2. 子类的拷贝构造函数必须调用父类的拷贝构造完成父类成员的拷贝初始化。
3. 子类的operator= 必须要调用父类的operator = 才能完成父类成员的赋值。
4. 子类的析构函数会在被调用完成后自动调用父类的析构函数清理基类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序。
5. 子类对象初始化先调用父类构造,再调用子类构造。
6. 子类对象析构清理先调用子类析构再调父类的析构。
三.子类成员变量的初始化赋值:
子类继承了父类,那么子类中的成员变量就划分为了三部分:
a.父类继承过来的成员变量
b.子类的内置类型的成员变量c.子类的自定义类型的成员变量
对于这三部分的成员变量,我们在初始化赋值的时候,就会有限制:
一.当子类对象被创建后,这三部分的成员变量需要干什么?
1.父类继承过来的成员,子类无能为力,子类有使用权,但子类没有生杀权,父类继承来的成员,仍得由父类去赋值初始化,结束时也仍得由父类去销毁。2.子类创建出的内置类型成员,就可以通过自身的构造函数去初始化;
3.子类创建出的自定义类型成员,需要调用自定义类的构造函数去初始化;二.当程序即将结束时,子类对象会被销毁:
1.首先编译器在消除子类对象时,优先调用子类的析构函数,析构子类的内置类型成员,然后调用自定义类的析构函数销毁自定义类型成员变量;
2.最后才会轮到父类继承过来的成员变量被析构。