编译器自动生成构造函数的情况
回顾
在前面构造函数的学习中,我们提到了一个广为流传的错误:
默认情况下,编译器会为每一个类生成空的、无参的构造函数。 这句话在C++里是错的,不严谨
我们在此节中详细说明一下,这句话不严谨在哪。即,C++在某些特定的情况下,会给类自动生成无参的构造函数。
通过汇编检查是否调用了构造函数
我们声明一个类,声明一个无参的构造函数,通过创建对象,查看汇编函数调用情况
class Person
{
int m_age;
public:
Person() {}
};
int main()
{
Person person;
return 0;
}
查看汇编代码,可以看到call Person::Person,说明调用了该构造函数
然后,我们将构造函数删除,再次创建对象,查看是否调用构造函数(即查看编译器是否生成了默认的无参构造函数)
class Person
{
int m_age;
public:
};
int main()
{
Person person;
return 0;
}
可以看出,创建对象的时候,什么也没做,并没有调用构造函数。这也就说明编译器并没有为该类生成了默认的无参构造函数。
因此,我们证明了那个广为流传的错误:默认情况下,编译器会为每一个类生成空的、无参的构造函数。既然,我们给出了结论:C++在某些特定的情况下,会给类自动生成无参的构造函数。那我们接下来就探索一下,在哪些特定情况下,即使不写构造函数,编译器也会默认生成无参的构造函数。
哪些情况下,会默认生成无参的构造函数?
成员变量在声明的时候,就进行初始化
class Person
{
int m_age = 10;
public:
};
int main()
{
Person person;
return 0;
}
通过汇编也可以看出,竟然调用了Person构造函数
我们进入构造函数的内部,查看汇编代码。可以看到存在传递值10的操作,也就是给age赋值10
到这里其实我们可以得出一个结论了:编译器为该类默认的生成了无参的构造函数,且在构造函数内部完成了成员变量的初始化。别以为代码写的时候是在创建变量时初始化的,初始化操作就在外面完成。我们通过汇编分析,得出结论:即使在外部初始化,依然会在构造函数内部完成初始化操作。
因此,下面两种写法是等价的
class Person
{
int m_age = 10;
public:
};
class Person
{
int m_age;
public:
Person()
{
m_age = 10;
}
};
好了言归正传,回到标题:成员变量在声明的时候,就进行初始化,那么编译器就会为该类生成一个无参的构造函数,来完成成员变量的初始化
类中存在定义的虚函数
在该类中,我声明一个空的虚函数。创建对象时,也会调用构造函数。意味着,编译器也会默认的创建一个无参的构造函数
class Person
{
int m_age;
public:
virtual void run()
{
}
};
我们通过查看构造函数内部,发现存在这样的一个操作:将虚表地址放在对象内存的最前面,用于查找虚表(细节可以查看我之前的博文)
博文中:一旦类中有虚函数,创建对象时,对象最前面会多出四个字节存放虚表地址
然后就可以得出一个结论:类中存在虚函数时,即使不写构造函数,编译器也会默认的生成一个无参的构造函数,从而在函数内部完成虚表地址的存放。
虚继承了其它类
虚继承是怎么回事,看之前的博文
class Person
{
public:
};
class Student : virtual public Person
{
};
int main()
{
Student student;
return 0;
}
即使两个类什么都不写,但是Student虚继承了Person类,创建Student对象时,依然会调用构造函数,也就是说编译器生成了一个无参的构造函数。
可以看到,构造函数里依然有虚表地址的传递,也就说创建对象时,依然有事干。
类中包含其他类的对象成员,同时其它类存在构造函数
这句话什么意思,下面代码直接看懂。在Person类里面,声明一个Car类型的成员,且Car中是有构造函数的(无论这个构造函数是自定义的还是编译器默认生成的)
class Car
{
public:
Car(){}
};
class Person
{
public:
Car car;
};
int main()
{
Person person;
return 0;
}
此时,可以看到Person内部生成了默认的构造函数。
我们可以在Person的构造函数里看到,调用了Car的构造函数。
我们梳理一下这个过程:Person创建对象时,有事做,所以编译器给生成无参构造函数。做了什么事:对象类型成员变量car需要调用Car的构造函数,就像普通成员变量需要初始化值一样,都叫有事要做。
父类有构造函数(无论是自定义的还是编译器默认生成的)
父类中有构造函数,子类在创建对象时,会优先调用父类的构造函数完成父类成员变量的初始化(在我的这篇博文里有讲解)
class Person
{
public:
Person(){}
};
class Student : public Person
{
public:
};
int main()
{
Student student;
return 0;
}
所以,子类在创建对象时,会调用父类构造函数,这叫有事可做,所以编译器会为子类生成一个无参的构造函数
总结
通过几个例子我们似乎可以摸清规律了。
什么特定情况下编译器会默认的创建一个无参的构造函数,通俗点说就是:当对象创建的那一刻,有事情要做!
对象创建完毕,需要做一些额外操作(比如内存操作:存放虚表地址,初始化成员变量;函数调用:调用父类构造函数)时,编译器一般都会为类生成一个无参的构造函数。
切记,理解对象创建完毕,需要做一些额外操作这句话,指的是对象创建完毕那一刻,而不是对象创建完成之后的事。
person对象创建完毕之后,调用了run函数。这不叫对象创建时有事要做,必须是创建完毕那一刻有事要做,而不是过一会才有事要做。要理解程序运行的时序。
class Person
{
public:
void run()
{
cout << "do something" << endl;
}
};
int main()
{
Person person;
person.run();
return 0;
}