构造函数
构造函数是创建类类型对象时,由编译器自动调用,用来初始化对象的函数,它保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。函数名与类名相同,无返回值。构造函数可以重载。
- 什么是默认构造函数?
- 无参的构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以称之为默认构造函数;
- 默认构造函数只能有一个
例1:验证默认构造函数只能有一个
class Date
{
public:
// 无参构造函数
Date()
{
_year = 2021;
_month = 4;
_day = 18;
}
// 全缺省构造函数
Date(int year = 2000, int month = 4, int day = 21)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
system("pause");
return 0;
}
创建d对象时会出错,因为编译器不知道要调用哪个默认构造函数,存在二义性
1. 如何理解 “ 如果用户不显示定义构造函数,编译器会生成默认构造函数 ” ?
例2:不显式定义构造函数
class Date
{
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
system("pause");
return 0;
}
从监视窗口可以看到,成员变量被初始化了,只不过是随机值而已。说明如果我们不显式定义构造函数,编译器可能生成了一个默认构造函数
为什么说可能呢?接着往下看
从反汇编看,创建对象d时,并没有调用Date的构造函数的指令,而是直接跳到了system(“pause”)
结论:
从语法来说,如果一个类没有显式定义任何构造函数,编译器会生成一个默认构造函数;
实际上,编译器对于是否生成构造函数会进行优化,只有编译器觉得需要才会生成
2、编译器会生成默认构造函数的情况
1)A类中包含B类的对象,B类定义了默认构造函数(无参构造函数 或 全缺省构造函数)
原因:因为类成员对象有默认构造函数,那么编译器就需要显式的来调用这个类成员对象的构造函数。要想显式调用,那编译器必须自己合成一些代码来调用,所以就会生成默认构造函数了。
注意:编译器产生的这个默认构造函数仅仅调用类成员对象的构造函数,而不对类中其他变量做任何初始化操作。
例3:Time类显式定义默认构造函数,Date类不显式定义默认构造函数;Date类中包含一个Time类对象
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_min = 0;
_sec = 0;
}
private:
int _hour;
int _min;
int _sec;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
Time _t; // 另一个类的类对象
};
int main()
{
Date d;
system("pause");
return 0;
}
这次从汇编代码可以看到,创建Date d时,调用了编译器生成的默认构造函数
需要注意的是,初始化Time _t的时候不是编译器直接去调用Time类中定义的构造函数,而是先给Date类生成默认构造函数,然后在Date类的默认构造函数中再去调用Time类中我们自己显式定义的默认构造函数
例4:验证如果被包含类对象的类中没有定义默认构造函数,则编译器不会生成默认构造函数
对例3的代码稍稍改动一下下;这次Time类中也不显式定义默认构造函数
class Time
{
private:
int _hour;
int _min;
int _sec;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
Time _t; // 自定义类型
};
int main()
{
Date d;
system("pause");
return 0;
}
汇编代码中没有调用编译器没有生成默认构造函数的指令,也就是说,编译器没有给Date类生成默认构造函数
2)基类带有默认构造函数(无参构造函数 或 全缺省构造函数),创建派生类对象时
(类A是基类,类B是派生类,类B继承自类A,类A中带有默认构造函数)
例5:
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
};
class Derived :public Base
{
public:
int _d;
};
int main()
{
Derived d;
system("pause");
return 0;
}
3)类中含有虚函数,编译器一定会给该类生成默认的构造函数
① 类本身定义了自己的虚函数
② 类从继承体系中继承了虚函数
这两种情况都使一个类成为带有虚函数的类。因为含有虚函数的类对象都含有一个虚表指针vptr,编译器需要对vptr设置初值,以满足虚函数机制的正确运行,编译器会把这个设置初值的操作放在默认构造函数中。如果设计者没有定义任何一个默认构造函数,则编译器会生成一个默认构造函数完成上述操作,否则,编译器将在每一个构造函数中插入代码来完成相同的事情。
4)在虚拟继承中,编译器一定会给子类生成构造函数
虚继承是用于解决多继承条件下的菱形继承问题
底层实现原理与编译器有关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基表(不占用类对象的存储空间) (需要注意的是,虚基类依旧会在其子类中存在拷贝,但是只有一份);当虚基类的子类被当作父类继承时,虚基类指针也会被继承
实际上,vbptr指的是虚基表指针,该指针指向了一个虚基表,虚基表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基表)的两份同样的拷贝,节省了存储空间