前言
继续上篇文章再谈C++中的构造函数,深入理解构造函数(上篇),来聊C++中的构造函数。
文章目录
一. 再谈对构造函数初始化列表的理解(2)
构造函数的初始化列表不仅仅是比在构造函数体内的赋值语句初始化看得高级:
其实有些情况1. 必须使用构造函数初始化列表初始化;2. 而且构造函数初始化列表初始化对于非内置类型的成员变量初始化能达到少调用几次该非内置类型成员变量的一些特殊函数的作用,简而言之,就提升了执行效率。
1) 构造函数初始化列表是如何提升效率的
我们先聊聊第二点:也就是构造函数初始化列表是如何提升效率的。
首先,来一段代码:该代码有两个类Person类 和 Student类,Student类有一个Person 成员变量:
这是不在构造函数初始化列表初始化Person类的成员变量
class Person
{
public:
int m_price;
Person(int price = 0) //单参数默认构造函数,有默认参数
{
m_price = price;
cout << "Person()的构造函数调用,m_price = " << m_price<<endl;
}
~Person()
{
cout << "Person()析构函数调用" << endl;
}
Person& operator= (const Person& person)
{
m_price = 30;
cout << "operator=()调用,m_price = " << m_price << endl;
return *this;
}
};
class Student
{
public:
int m_age;
double m_score;
Person p; //在Student 包含了Person 类的成员变量
Student(int age, double score) :m_age(age), m_score(score)
{
p = 20; //会隐式调用单参数构造函数
cout << "Student()构造函数调用" << endl;
}
~Student()
{
cout << "Student()析构函数调用" << endl;
}
};
int main()
{
Student s(10,20.0); //成功创建对象,且调用了默认构造函数初始化对象
return 0;
}
首先,你好好看看这段代码:
在Person 类中,有单参数构造函数(有默认参数的),析构函数,有构造函数,有等号=运算符重载函数:这些都是所谓的特殊函数,是我特地设计出来测试用的;
在Student类中,有Person类的成员变量p,有构造函数的初始化列表方式和一个函数体内的初始化赋值语句,是给 类成员变量 p赋值,有析构函数;
我想问的是:上面,当我执行这段代码时候:Student s(10,20.0);即创建一个Student类对象s时候,到底会调用什么函数?
我们先来预测一下:
首先会调用 Person类的构造函数数一次;
再调用Student的构造函数一次;
再调用Person类的析构函数;
再调用Student的析构函数。
那到底是不是呢?
我们看看运行结果:
惊呼!!!和我们的预测结果不一样,多了好多莫名其妙的函数调用啊;
到底为什么会这样?先别急,来看看我们在初始化列表初始化的结果是什么,首先要注释掉 p = 20;然后在初始化列表写上代码:p(10),观察结果:
结果显示:少了一次调用构造函数,和一次赋值重载函数,一次析构函数;
为什么会这样呢?我们继续来做一些事情,把上面的代码构造函数初始化列表中的p(10)注释掉,此时就说明Student类中没有对Person类的构造函数,看看结果是什么?
发现这里也和上面的调用什么打区别,唯一区别是 m_price = 0;不一样了
为什么呢会这样?
因为我们知道,在一个Student类中有Person类的成员变量的话,那么即是没有在Student构造函数中调用Person类的构造函数,也会自动帮你调用Person类构造构造函数地;所以上面的Student类即使没有写构造函数,也是会帮你自动调用滴,这是编译器偷偷干的事,它一定会偷偷的帮你做,所以你发现再那里调用的Person的构造函数结果为: “Person()的构造函数调用,m_price = 0”;而不是 “Person()的构造函数调用,m_price = 10";因为没有给它参数咯,所以就是默认参数 0;
那我们回到再上一个问题,当我们在构造函数初始化列表写上了 p(10),即在Student类中的构造函数初始化列表写了p(10):很自然而然有参数了,那么就会调用自己的构造函数,把参数传10,所以打印结果有: Person()的构造函数调用,m_price = 10";这也是符合我们的预期。
最不符合预期的上上上的问题:不在Student的构造函数初始化列表初始化哪个Person类的成员变量,而在Student类的构造函数体内赋值 p = 20;
由于我们知道有类成员变量,那么一定会在Student构造函数初始化列表调用Person类的构造函数的,
于此同时啊,你又在函数体内,p= 20;这个语句又会调用单参数构造函数,隐式构造,于此同时,由于你的Student类还有 = 等号赋值语句,又会调用该赋值语句,在次赋值。
你发现嘛?这隐式构造,这个赋值语句的,于此同时,还会多调用析构函数,这些都是不必要的呢。
我们只要在构造函数初始化列表给有类成员变量赋初始值,这都是可以避免多次调用的。
总结:
就是在构造函数初始化列表初始化含有类的成员变量时候,会减少调用不必要的一些特殊函数(构造析构拷贝,赋值等函数),这样就减少调用次数,提高运行效率。
所以能用初始化列表就用初始化列表方式初始化吧
2) 必须要构造函数初始化列表里初始化的情况
我们再回来看看必须要构造函数初始化列表里初始化的情况:
第一个:首先我们之前就说过const 成员变量的初始化必须在构造函数初始化列表初始化。
第二个:有继承体系下的,基类的构造函数,在派生类的构造函数初始列表初始化。
第三个:对于菱形继承(有虚继承的体系下):虚基类也必须由派生类的初始化列表初始化,并且有个特殊的是,最底层的派生类的构造函数初始化列表,无论继承其他类的顺序如何,都是先执行虚基类的构造函数。
就不代码验证了,感兴趣的可以自行验证一下。
二. 构造函数初始化顺序问题(类之间组合和类之间的继承)
一个很简单的话题:
1. 当有组合行为时,也就是一个类1中含有类2成员变量,那么创建类1对象时候,就会先调用类2成员变量的构造函数,再调用类1的构造函数;
这个就不代码演示了,上面的案例也讲过这个问题。
2. 当有继承时候,就先调用基类的构造函数,再调用派生类的构造函数.(在多重继承也是这样);当有多重继承就从继承列表顺序往上一层一层的找到最顶层,开始调用。
代码演示:有个基类Base,A类继承了Base,B类不继承,C类继承了A和B(C类这里时继承列表有顺序的,先A,在B)
class Base //基类
{
public:
int i;
Base( )
{
cout << "Base()的构造函数" << endl;
}
};
class A :public Base //继承了基类
{
public:
int j;
A()
{
cout << "A()的构造函数" << endl;
}
};
class B
{
public:
int k;
B( )
{
cout << "B()的构造函数" << endl;
}
};
class C :public A ,public B
{
public:
int m;
C( )
{
cout << "C()的构造函数" << endl;
}
};
int main()
{
C c;
return 0;
}
预测结果:
执行代码:C c
;先调用 C类继承列表的中 A类的构造函数,但是由于A类构造函数有基类,所以就会先调用A类继承的的基类Base构造函数,再调用A类的构造函数,然后再调用B类的构造函数,最后调用C类的构造函数。
验证结果:和预测的一样。
假如我把C类的继承列表修改,先继承B再继承A,即class C :public B ,public A
,那么执行代码:C c
时候,先调用B类,再调用A类,由于A类有继承Base类,所以调用Base类,再调用A类,最后调用C 类。
就一个小问题,注意一下就行
三. 构造函数初始化成员变量的顺序问题
在构造函数数中,初始化的顺序是按成员变量的声明时候的顺序初始化的,不是按构造函数初始化列表的顺序初始化,也不是按构造函数体内赋值语句初始化的顺序滴,所以说,按道理来说,你初始化列表的初始化时候成员变量的位置可以随意,但是初始化的顺序就是按声明顺序的哦。
这个问题注意一下就行,就不给代码演示了。
差不多就讨论到这里了,其实构造函数还有很多可以讨论的问题,就单单继承体系,多态体系的构造函数就很不一样。以后有兴趣的就深入理解吧。