C++构造函数
1. 构造函数(Constructor)
- 概念
构造函数是一个特殊的成员函数,构造函数没有返回值,函数名和类名一致 - 语法形式
class 类名
{
public:
类名(形参表):实参(形参),...{
//主要负责对象的初始化,即初始化成员变量
}
}
- 构造函数特点
- 构造函数函数名和类名一致,没有返回类型
- 构造函数在创建对象时被自动调用,不能像普通成员函数一样显示调用
- 在每个对象的生命周期中,构造函数一定会被调用,但是也仅仅会被调用一次
- 构造函数支持重载
string s;//匹配string无参构造函数
string s("hello");//匹配string有参构造函数
2. 默认构造函数
-
概念
默认构造函数是可以不用实参进行调用的构造函数,有以下两种形式-
没有带形参的构造函数
-
带有形参但是有提供默认参数的形参
-
-
调用时机
当我们定义对象但是没有对对象进行初始化时,编译器就会调用默认构造函数 -
做了什么
1.对于基本类型的成员变量什么也不做
2.对于类类型的成员变量(成员子对象),将会自动调用相应类无参构造函数来初始化 -
编译器自动生成默认构造函数的时机
唯有默认构造函数"被编译器需要"时,编译器才会生成默认的构造函数
-
那么什么时候"被编译器需要"
1.当该类的成员子对象有默认的构造函数时
如果一个类没有任何的构造函数,但是他含有一个类类型的数据成员(成员子对象),而且该成员子对象有默认构造函数,那么编译器就会为该类合成一个默认构造函数,不过这个合成操作只有在构造函数真正需要被调用的时候才会发生
Class A
{
public:
A(int num = 0){ m_num = num; }; //默认构造函数
int m_num;
};
class B
{
public:
A a;//类A含有默认构造函数
int m_b;
//...
};
int main()
{
B b; //编译至此时,编译器将为B合成默认构造函数
return 0;
}
被合成的默认构造函数
//被合成的默认构造函数的样子
B::B()
{
a.A::A();
}
//默认构造函数中只包含必要的代码,完成了对数据成员a的初始化,但不产生任何代码来初始化B::b
//初始化类的内置类型或复合类型成员时程序的责任而不是编译器的责任,因此一般我们惠子即来写构造函数对B::b进行初始化
如果类中有多种类对象成员,则编译器按照这些类对象成员的声明顺序,在构造函数按顺序插入调用各个类默认构造函数的代码
2.当该类的基类有默认构造函数时
当一个类派生自一个含有默认构造函数的基类时,该类也符合编译器需要合成默认构造函数的条件。编译器合成的默认构造函数将根据基类声明顺序调用上层的基类默认构造函数。同样的道理,如果设计者定义了多个构造函数,编译器将不会重新定义一个合成默认构造函数,而是把合成默认构造函数的内容插入到每一个构造函数中去
3.当该类有虚函数时
类带有虚函数可以分为两种情况:
1> 类本身定义了自己的虚函数
2> 类从继承体系中继承了虚函数(成员函数一旦被声明为虚函数,继承不会改变虚函数的”虚性质“)
这两种情况都使一个类成为带有虚函数的类。这样的类也满足编译器需要合成默认构造函数的类,原因是含有虚函数的类对象都含有一个虚表指针vptr,编译器需要对vptr设置初值以满足虚函数机制的正确运行,编译器会把这个设置初值的操作放在默认构造函数中。如果设计者没有定义任何一个默认构造函数,则编译器会合成一个默认构造函数完成上述操作,否则,编译器将在每一个构造函数中插入代码来完成相同的事情。
4.当该类的基类为虚基类时
虚基类的概念是存在于类与类之间的,是一种相对的概念。例如类A虚继承于类X,则对于A来说,类X是类A的虚基类,而不能说类X就是一个虚基类。虚基类是为了解决多重继承下确保子类对象中每个父类只含有一个副本的问题,比如菱形继承
注意事项
- 避免“无参数的默认构造函数”和“带缺省参数的默认构造函数”同时存在
无参数的默认构造函数
和带缺省参数的默认构造函数
同时存在时,编译器会产生二义性,从而生成编译错误
class Sample {
public:
// 默认构造函数
Sample() {
// do something
printf("Sample()");
}
// 默认构造函数
Sample(int m = 10) {
// do something
printf("Sample(int m = 10)");
}
};
int main()
{
Sample s; // error C2668: “Sample::Sample”: 对重载函数的调用不明确
return 0;
}
- 使用无参构造函数创建对象时,不应该在对象名后面加上括号
使用无参构造函数创建对象时,不应在对象名后面加上括号,否则会产生编译警告“warning C4930: “Sample s(void)
”: 未调用原型函数(是否是有意用变量定义的?)”。因为编译器误认为Sample s();
语句时要声明返回值为Sample
对象的函数s
,而又没找到函数s
的定义,所以产生了警告
class Sample {
public:
// 默认构造函数
Sample() {
// do something
printf("Sample()");
}
};
int main()
{
Sample s(); // warning C4930: “Sample s(void)”: 未调用原型函数(是否是有意用变量定义的?)
return 0;
}
总结
只有在编译器需要默认构造函数来完成编译任务的时候,编译器才会为没有任何构造函数的类合成一个默认构造函数,或者是把这些操作插入到已有的构造函数中去
编译器需要默认构造函数的四种情况:
a) 调用对象成员或基类的默认构造函数。
b) 为对象初始化虚表指针与虚基类指针。
3. 类型转换构造函数(单参构造函数)
class 类名{
[explicit] 类名(源类型){...}
};
注意:
- 可以实现源类型变量到当前类型对象的隐式转换
- 可以使用
explicit
关键字修饰类型转换构造函数,强制要求这种转换必须要显示完成
C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生,声明为explicit的构造函数不能在隐式转换中使用
当构造函数只有一个参数或除了第一个参数以外其他参数都有默认参数,这样的构造函数有两个作用,一个是构造函数,一个是默认且隐含的类型转换操作符
例如AAA = XXX
, 这样的代码, 且恰好XXX
的类型正好是AAA
单参数构造器的参数类型, 这时候编译器就自动调用这个构造器, 创建一个AAA
的对象
这样看起来很方便。 但在某些情况下, 却违背了程序员的本意。 这时候就要在这个构造器前面加上explicit修饰, 指定这个构造器只能被明确的调用/使用, 不能作为类型转换操作符被隐含的使用
#include <iostream>
using namespace std;
class Test1
{
public :
Test1(int num):n(num){}
private:
int n;
};
class Test2
{
public :
explicit Test2(int num):n(num){}
private:
int n;
};
int main()
{
Test1 t1 = 12;//先使用12作为构造实参,构造临时对象,然然后使用临时对象对i进行赋值
Test2 t2(13);
Test2 t3 = 14;//test2 的单参构造被explicit修饰
return 0;
}
编译时,会指出 t3那一行error:无法从“int”转换为“Test2”
。而t1却编译通过。注释掉t3那行,调试时,t1已被赋值成功。
注意:当类的声明和定义分别在两个文件中时,explicit
只能写在在声明中,不能写在定义中
- 隐式转换使代码可读性差,不推荐使用,推荐使用显示类型转换
//`C`风格
i = (Test)123;
//`C++`风格
i = Test(123);
//使用静态类型转换
static_cast<Test>(123);
4. 拷贝构造函数
- 概念
用一个已存在的对象作为同类型对象的构造实参,创建新的对象时会调用拷贝构造函数 - 语法形式
class 类名{
类名(const 类名& that){....} // 常引用
};
- Code
eg:
class A{....};
A a1(...);
A a2(a1);//匹配A的构造函数A a2 = a1;
eg:
void func(A a){}
func(a1);//拷贝构造函数
eg:
A foo(void){
A a;
return a;//A temp = a;//中间变量
}
A a3 = a;//A temp = a;//中间变量
如果类中自己没有定义拷贝构造函数,那么编译器就会提供一个缺省的拷贝构造函数,对于基本类型的成员变量按字节赋值,对于类类型的成员变量(成员子对象)将会调用相应类的缺省的拷贝构造函数来进行初始化
注:在实际开发中,大多数情况下不需要自己定义拷贝构造函数,编译器提供的缺省的拷贝构造函数就可以满足正常的开发需求,如果类中有指针类型的类成员
,那么在拷贝构造过程中要注意浅拷贝
问题
- 拷贝构造函数调用时机
- 用已定义的对象作为同类型对象的构造实参
- 以对象形式象函数传递参数
- 以对象形式从函数中作为返回值