定义:一种便捷的初始化类内成员变量的方式。
初始化成员变量通常在构造函数里执行,如下例所示。成员变量的初始化可以通过调用有参的构造函数进行传参初始化,也可以通过调用无参的构造函数在函数体内部直接初始化(可以看我之前的博文)。下例展示了调用有参的构造函数始化成员变量的方式。
class Person
{
int m_age;
int m_height;
public:
Person(int age,int height)
{
m_age = age;
m_height = height;
}
};
int main()
{
Person person(10,60);
cout << person.m_age << endl; //10
cout << person.m_height << endl; //60
return 0;
}
既然初始化列表是一种便捷的初始化成员变量的方式,那具体的“相貌”如何呢?。下例展示了初始化列表初始化成员变量的方式:
class Person
{
int m_age;
int m_height;
public:
Person(int age, int height) : m_age(age), m_height(height) {}
};
int main()
{
Person person(10,60);
cout << person.m_age << endl;
return 0;
}
我们可以轻而易举的看出两者的区别:构造函数的书写有些差异。初始化列表所在的构造函数没有函数体,在函数之后以“:”连接。
Person(int age, int height) : m_age(age), m_height(height) {}
m_age(age)中的m_age是即将被初始化的成员变量,括号内的age是构造函数的形参。当初始化多个成员变量时,用逗号分离。大括号还是有的,只是函数体内部没有代码。
初始化列表的本质,就是生成函数体代码。这样写:
Person(int age, int height) : m_age(age),m_height(height){ }
实际上是执行的:
Person(int age, int height) {
m_age = age;
m_height = height;
}
初始化成员列表的写法,比普通的函数体内部赋值看起来更加好看,但是这两种写法的效率是一模一样的。读到这里就会有疑问,仅仅便捷到这种程度,没必要弄一个初始化列表出来吧?那我们下面介绍一下,初始化列表比普通函数体内部实现更便捷的应用场合。
我们知道优势是相对的。初始化列表的问世就是为了便捷以往初始化成员变量的方式,那相对于普通的构造函数初始化有什么便捷之处呢?
Person(int age, int height) : m_age(age + 2), m_height(height)
可以将表达式作为形式参数,初始化成员变量
int function(int a)
{
return a*a;
}
Person(int age, int height) : m_age(function(age)), m_height(height) { }
可以将形参经过函数,然后将函数的返回值作为初始化成员变量的值。这种情况还是比较常见的,我们在有些时候,需要对初始化的参数做一定的计算,然后作为成员变量的初始值。
我们初始化列表的时候,被初始化的成员变量之间用逗号分隔:
class Person
{
int m_age;
int m_height;
public:
Person(int age, int height) : m_age(age), m_height(height) {}
};
m_age(age) 然后再 m_height(height)。我们有没有想过是不是先初始化 m_age然后在初始化 m_height呢?那我们就来验证一下:
Person(int age, int height) : m_age(m_height), m_height(height) { }
我们初始化m_age选用m_height的值作为初始化值,然后运行main函数,打印两个成员变量的值,结果如下
int main()
{
Person person(10,60);
cout << person.m_age << endl; //-858993460
cout << person.m_height<< endl; //60
return 0;
}
发现m_age并没有被初始化。这样的结果也不奇怪,因为如果是先初始化m_age,传入的形参是m_height,而此时m_height还没有被赋值,自然m_age就得不到正常数值的初始化了。到这里其实并不能证明,初始化成员列表的书写顺序就是初始化的顺序。下面继续一个例子:
class Person
{
int m_age;
int m_height;
public:
Person(int age, int height) : m_height(height) , m_age(m_height) { }
};
如果按照我们上面的分析,列表的书写顺序决定了初始化的顺序。这也的书写顺序,会先初始化m_height,然后将m_height的值作为形参,传递给m_age,进而m_age得到初始化。那事实是这样吗?打印结果如下:
int main()
{
Person person(10,60);
cout << person.m_age << endl; //-858993460
cout << person.m_height<< endl; //60
return 0;
}
咦?怎么回事?按理说应该都打印60才对。这个初始化的顺序难道不是列表的书写顺序吗?
答案是:初始化列表中,列表的顺序是没有意义的,初始化成员变量的顺序,只跟成员变量在内存中的地址值有关。即:在类中声明时,写在前面的成员变量被先初始化,写在后面的成员变量被后初始化,因此,对于本类来说,永远先初始化m_age,后初始化m_height。所以谜底揭开了。
class Person
{
int m_age;
int m_height;
public:
Person(int age, int height) : m_height(height) , m_age(m_height) { }
};
也就是说,由于成员变量m_age写在前面,所以初始化列表中对于m_age的初始化操作先执行,也就是说先执行这句m_age(m_height),所以初始化列表中的书写顺序是意义的。
我们要做到成员变量的定义顺序和初始化列表顺序一致,增强程序的可读性。
首先回顾一下默认参数是啥(可以看我之前的博文)。默认参数的作用是:声明对象时,不传递参数也有默认参数传递参数。
class Person
{
int m_age;
int m_height;
public:
Person(int age = 10 , int height = 50) : m_age(age),m_height(height){ }
}
假如我的对象声明这样写:直接将 age = 10 和 height = 50 传递给成员变量进行初始化
Person person;
假如我的对象声明这样写:直接将 age = 12 和 height = 50 传递给成员变量进行初始化
Person person(12);
假如我的对象声明这样写:直接将 age = 12 和 height = 60 传递给成员变量进行初始化
Person person(12,60);
我们发现了,初始化列表与默认参数搭配使用最大的好处就是:我写一个构造函数,相当于写了三个构造函数。这种搭配要记住,要时常使用,可提高代码的精简
class Person
{
int m_age;
int m_height;
public:
Person(int age, int height) { }
void run(int age, int height) : m_height(height), m_age(m_height) { }
};
我们已经知道了,初始化列表其实就相当于把代码插入到构造函数的函数体内:(两种写法完全等价)
Person(int age, int height) : m_age(age), m_height(height) { }
Person(int age, int height) {
m_age = age;
m_height = height;
}
按照我们之前的逻辑,好像初始化列表就是为了替代函数体代码而出现的。所以认为有了初始化列表就不能在函数体内部写代码,但其实是可以在函数体内写代码的。比如下方代码:
Person(int age, int height) : m_age(age), m_height(height) {
m_age = 10;
}
我在初始化列表里通过传递形age,初始化了成员变量m_age,但我又有了另外一个需求,在函数体内部对m_age进行了额外的处理,也就是额外的初始化。那么问题来了:最终成员变量是按照初始化列表里的来呢,还是按照函数体里的来呢?
回答这个问题很简单,就看是先执行初始化列表的操作,还是先执行函数体里的操作呗!
Person(int age, int height) : m_age(age), m_height(height) {
m_age = age;
m_height = height;
m_age = 10;
}
int main()
{
Person person(20,60);
cout << person.m_age << endl; //10
cout << person.m_height<< endl; //60
return 0;
}
答案是,先执行初始化列表里的操作,即把初始化列表里做的事插入到函数体的最顶端,先执行。既然是先执行,那最终的结果必然是依据后执行的结果了。所以,m_age最终被初始化为10。
我在类中,写了两个构造函数(一个是有参的构造函数,另一个是无参的构造函数)。我们经常会在类中写多种构造函数,以适应不同的初始化成员变量需求。当声明无参的构造函数时,就赋值0给成员变量,当声明有参的构造函数时,就将参数传给成员变量,这很合理。如下方代码所示:
class Person
{
int m_age;
int m_height;
public:
Person() {
m_age = 0;
m_height = 0;
}
Person(int age , int height) {
m_age = age;
m_height = height;
}
};
但是,不觉得这样太麻烦了吗,因为两个构造函数体内部都是给成员变量赋值。两个成员变量还可以接受,如果是10个成员变量,就会很麻烦了。这时候或许你会想到一个解决这个问题的方法:在有参的构造函数赋予默认参数,这样不就解决了上面的两个构造函数的问题吗?为何要学习构造函数调用构造函数呢?
class Person
{
int m_age;
int m_height;
public:
Person(int age = 0 , int height = 0) {
m_age = age;
m_height = height;
}
};
那我说你的格局就小了!因为构造函数虽然最大的作用就是初始化成员变量,但是它还可以执行别的代码,函数体内还可以执行别的操作。因此构造函数的互相调用是有应用场景的。就像下方代码所示:
class Person
{
int m_age;
int m_height;
public:
Person() {
Person(0, 0) ;
}
Person(int age , int height) {
m_age = age;
m_height = height;
}
};
然后我在main函数中,示例无参的对象,调用无参的构造函数。理论上会进入无参构造函数里,然后执行有参构造函数,然后将0赋值给成员变量。实际上这个过程也是这样运行的,但是为什么打印的乱码呢?
int main()
{
Person person;
cout << person.m_age << endl; //-82939913
cout << person.m_height<< endl; //-82939913
return 0;
}
直接说答案:因为构造函数的互相调用必须在初始化列表里调用,即正确写法:
class Person
{
int m_age;
int m_height;
public:
Person() : Person(0, 0) {}
Person(int age , int height) {
m_age = age;
m_height = height;
}
};
下面解释,为什么构造函数必须放在初始化列表里,而不能放在函数体内部(为什么放在函数体内部不行?)
class Person
{
int m_age;
int m_height;
public:
Person() {
Person(0, 0);//这种调用方式,其实相当于声明了一个Person对象,干巴巴的声明
// 我们通常通过 new Person来声明一个指向对象的指针,而这种相当于 Person person,
// 但是,这个对象,是临时的对象。不是main函数中创建的对象,意思是这个构造函数不是main函数中
// 对象调用的构造函数,而是一个临时对象调用的构造函数
}
Person(int age , int height) {
m_age = age;
m_height = height;
}
};
这里需要回顾一个重点的知识。对象调用成员函数的时候,会有一个默认的this指针,出现在函数体内部。这个this指针指向这个对象
class Person
{
int m_age;
int m_height;
public:
Person() {
m_age = 10;
m_height = 20;
}
Person() {
// this = &person 把对象的地址默认的赋值给this指针,编译器做的事
this->m_age = 10; //等同于person.m_age = 10;
this->m_height = 20; //等同于person.m_height = 20;
}
};
int main()
{
Person person;
return 0;
}
我们想起来了this指针的事,然后从本质解释一下:为什么调用构造函数,写在函数体内,main函数打印出来的是乱码呢?
class Person
{
int m_age;
int m_height;
public:
Person() {
Person(0, 0);//执行这句,相当于执行下面两句
// Person temp;//这是存放在栈空间的临时对象,main函数中声明一个对象之后,这个临时对象的内存就转瞬即逝
// temp.Person(0, 0)
}
Person(int age , int height) {
this = &temp //然后临时变量的地址值,赋值给这个函数的this指针,而不是main函数中的person对象的地址值
this->m_age = age;
this->m_height = height;
}
};
int main()
{
Person person;
cout << person.m_age << endl; //-82939913
cout << person.m_height<< endl; //-82939913
return 0;
}
所以,写在函数体内部的构造函数,会创建一个临时对象,然后临时对象的地址赋值给调用函数的this指针。这时,main函数中声明的对象,没有将地址传递给构造函数的this指针,那么person.m_age也就不会被赋值了。
而正确的调用:将构造函数写在初始化列表里,会将main函数中对象的person对象传给被调用的构造函数,进而赋值给this指针。
总结:构造函数调用构造函数是一种比较特殊的调用,被调用者必须放在初始化列表里面。构造函数调用普通的类内成员函数,可以写在函数体内部,但是构造函数必须写在初始化列表里。
假设构造函数的声明和实现是分离的(我们创建类时,基本上都是分离的),有一个非常关键的点,就是初始化列表的操作只能写在实现中,不可以写在声明中。
下面的类中,声明和实现是分开的。然而我将初始化列表写在了声明中,这也的书写方式是错误的。
class Person
{
int m_age;
int m_height;
public:
Person(int age, int height) : m_age(age), m_height(height) { };
};
Person::Person(int age, int height)
{
}
而,这样的书写方式才是正确的:
class Person
{
int m_age;
int m_height;
public:
Person(int age, int height);
};
Person::Person(int age, int height): m_age(age), m_height(height){ }
想没想过,为什么初始化列表要写在实现里呢?
答案:因为初始化列表的本质就是将这一段列表插入在函数体内部,所以要与实现绑在一起
假如,默认参数和初始化列表搭配起来使用,且恰好此时构造函数的声明和实现是分离的,那我们应该怎么做呢?下面的程序会报错,这是因为当声明和实现分离时,默认参数只能写在声明里。
class Person
{
int m_age;
int m_height;
public:
Person(int age = 20, int height = 60);
};
Person::Person(int age = 20, int height = 60): m_age(age), m_height(height){ }
所以,必须改成下面的形式
class Person
{
int m_age;
int m_height;
public:
Person(int age = 20, int height = 60);
};
Person::Person(int age , int height): m_age(age), m_height(height){ }
那想没想过,为什么默认参数又要写在构造函数的声明里呢?
因为我在调用函数的时候,编译器是先看声明,再看实现,如果声明中没有默认参数,那么编译器就不会把默认参数值push出来(看我的博文),而是直接调用函数,直接执行函数体代码,这样的话就会报错。
搭配使用时,我们要记住两件事:当构造函数的声明和实现是分离的时候,默认参数只能写在声明里,而初始化列表只能写在实现处,切记切记。
子类的构造函数会默认调用父类的无参构造函数(前提是父类中写了无参的构造函数)
class Person
{
public:
Person(){
cout << "Person::Person()" << endl;
}
};
class Student : public Person
{
public:
Student(){
cout << "Student::Student()" << endl;
}
};
int main()
{
Student student;
// 打印这两句
//Person::Person()
//Student::Student()
return 0;
}
如果子类的构造函数显式的调用了父类的有参构造函数,就不会默认的调用父类的无参构造函数了
class Person
{
int m_age;
public:
Person(){
cout << "Person::Person()" << endl;
}
Person(int age){
cout << "Person::Person(int)" << endl;
}
};
class Student : public Person
{
public:
Student() : Person(10) {
cout << "Student::Student()" << endl;
}
};
int main()
{
Student student;
// 打印这两句
//Person::Person(int)
//Student::Student()
return 0;
}
如果父类中没有无参的构造函数,只有有参的构造函数。那么子类必须主动调用有参的构造函数,否则编译器会报错。
class Person
{
int m_age;
public:
Person(int age){
cout << "Person::Person(int)" << endl;
}
};
class Student : public Person
{
public:
// 会报错
Student() {
cout << "Student::Student()" << endl;
}
// 不会报错
Student() : Person(10) {
cout << "Student::Student()" << endl;
}
};
如果父类没有写构造函数,那么子类如何调用父类的构造函数呢?(那我就不调用了)。网上有的博客说,如果没有写构造函数,就会默认生成一个,然后调用。这句话是错的。根本不会默认生成。
思考:为什么子类会在没有进行任何操作的情况下,默认调用父类的构造函数?
因为子类就是要继承父类的东西。如果子类对象想要初始化父类成员变量怎么办,那就必须得调用父类的构造函数。特别的就是父类中如果有private成员,子类根本无法初始化,只能通过父类初始化,也就是只能通过调用父类的构造函数来初始化。就像下面的例子一样。
class Person
{
int m_age;//是私有的
public:
Person(int age){
m_age = age;
cout << "Person::Person(int)" << endl;
}
};
class Student : public Person
{
public:
Student() : Person(10) {
// m_age = 10 如果直接赋值给m_age会报错,因为m_age是私有的,子类不可以直接访问,必须通过父类的构造函数访问
cout << "Student::Student()" << endl;
}
};
所以说,子类调用父类的构造函数最大的价值其实是,初始化父类中的private成员。
总结一下子类调用父类构造函数的规则: