一、普通类的构造函数
class A
{
int a,b;
public:
A(){cout<<"默认构造函数"<<endl;}//自己定义的默认构造函数,什么也不坐
A(int x):a(x){cout<<"转换构造函数1"<<endl;}//转换构造函数1
A(int x,int y):a(x),b(y){cout<<"转换构造函数2"<<endl;}//转换构造函数2
A(A& _A){
a=_A.a;
b=_A.b;
cout<<"复制构造函数"<<endl;
}
};
上面的类A有三个显示定义的构造函数,和一个显示定义的复制构造函数。它们的作用是构造对象,并进行一定的初始化。只要有构造函数,默认构造函数就不会生成;只要定义了复制构造函数,默认复制构造函数就不会生成。所以如果不想舍弃默认构造函数,就定义一个什么都不做的构造函数吧。因为我们希望知道哪个构造函数被执行了,所以都显示定义出来了。
使用无参构造函数初始化对象时,不准用空括号
A a;//调用无参数构造函数
A a();//虽然没有报错,其实没有创建对象
1、转换构造函数可生成临时对象
一般使用转换构造函数1、转换构造函数2时,我们并不会意识到“转换“的意思,如下面定义的两个对象所示,正常执行了两个构造函数
A a1(1);
A a2(1,2);
运行结果
但如果这样使用转换构造函数,就能体现转换的意义(叫单参数构造函数的隐形类型转换更适合)
A a1=1;//相当于A a1=A(1);
A a2={1,2};
这种情况编译器将数字类型转换成了临时对象,然后复制给a1、a2。需要注意的是,虽然有临时对象的生成,但并没有多执行构造函数,运行结果与上面一样。
理解了转换构造函数的意义,我们在定义对象数组时就可以很方便,但是这种方便可能会带来混乱,explicit关键字可以禁止这种隐形类型转换;(详细见文尾巴)
A array1[2];//默认构造函数执行两次
A array2[2]={1,2}//转换构造函数1执行两次
A array3[2]={{1,2},{3,4}}//转换构造函数2执行两次
A array4[2]={{1,2}}//转换构造函数2执行一次,默认构造函数执行一次
注意,在定义对象的指针数组时,不new就没有构造函数的调用,也就没有对象的生成。(new返回的就是地址,定义指针其实就是定义了整型,只不过这个整型可以存放地址)
A* ptr[3]={new A(1),new A(1,2)};//调用转换构造函数1一次,转换构造函数2一次,共生成两个对象
2、复制构造函数稍不小心就会被执行
复制构造函数简介:
1、用来复制已有的对象,产生一个新对象。
2、只有一个人参数,即同类对象的引用(为什么不能是对象呢,在下面会解释)
3、参数可以是 A&或const A&。推荐使用const A& 作为参数,因为这样既可以保证不改变被复制对象的值,也可以将常量对象作为参数。
4、不定义复制构造函数,编译器会自己生成一个默认的复制构造函数,这易于形成指针悬挂问题。
复制构造函数执行的三种情况:
1、利用复制构造函数初始化对象
A a2(a1);
A a2=a1;//这两种是一样的,都是初始化语句,不是赋值语句
2、某函数的参数是类的对象,那这个函数在执行时就会先调用类的复制构造函数。
void print(A _A)
{
cout<<_A.a;
}
A a=1;
print(a);//先执行A _A=a;调用一次类的复制构造函数
可见2与1中,复制构造函数执行情景是一样的,都是产生了新对象。可以回答类的复制构造函数为啥只能传引用了,因为传对象的话必须要调用复制构造函数,这时复制构造函数还没完成呢,就会陷入循环调用复制构造函数。
3、如果某函数(包括成员函数)的返回值是类的对象,那么函数在返回时会调用类的复制构造函数。
A A::fun()
{
return *this;
}
A a;
a.fun();//会执行复制构造函数,A a.fun() =a; "a.fun()"可以看成一个对象名。
可见3与1中,复制构造函数执行情景是一样的,都是产生了新对象。这在重载运算符时很有用,要实现连续赋值,a=b=c时,返回引用就可以使(a=b)=c也不违背连续赋值。
二、封闭类的构造函数。
含有成员对象的类叫做封闭类。
1、封闭类的构造函数。
class A
{
public:
A(int x) :a(x) { cout << "A构造" << endl; }
int a;
};
class B
{
int b;
A _A=1;//不能写成A _A(1);会默认为函数。
public:
B(int x):b(x){ cout << "B构造" << endl; }
};
类B为一封闭类,含有成员对象_A。C++11之后允许在成员变量定义时初始化,这和在构造函数的初始化列表中进行初始化是一样的。
在类B实例化时,先执行类A的构造函数,再执行类B的构造函数。如果想指定使用A的哪个构造函数,就要在B构造函数的初始化列表中加以说明。不说明就使用A的默认构造函数(没有就报错)
2、封闭类的复制构造函数。
如果封闭类没有显性定义复制构造函数,那么在使用默认复制构造函数去初始化封闭类对象时,编译器会主动调用成员对象的复制构造函数来生成成员对象,然后使用默认构造函数来生成封闭类的对象。
class A
{
public:
A(){cout<<"A默构";}
A(int x) :a(x) { cout << "A构造" << endl; }
A(A& oA) { a = oA.a; cout << "A复制构造" << endl;}
int a;
};
class B
{public:
int b;
A _A;
public:
B(){}
B(int x):b(x),_A(2*x){ cout << "B构造" << endl; }
};
int main()
{
B b1(3);
B b2(b1);
cout << b2.b << "," << b2._A.a;
system("pause");
return 0;
}
可见完成了使用b1初始化b2。
但如果在封闭类中定义了复制构造函数
B(B&oB) { b = oB.b; }
再执行上述程序时
编译器并没有自动调用A的复制构造函数(而是默认在初始化列表中执行了A的默认构造函数),来帮助b2._A完成初始化。需要自己在复制构造函数的初始化列表中显示的调用A的复制构造函数
B(B&oB):_A(oB._A) { b = oB.b;}
输出是
可见完成了使用b1初始化b2。编译器的意思可能就是如果你定义了复制构造函数,那你就自己完成成员对象的初始化,我也不知道你到底要不要使用复制构造函数,你可以使用默认构造函数,也可以使用转换构造函数,也可以使用复制构造函数来完成成员对象实例化。如果你不定义封闭类的构造函数,那我就使用成员对象的复制构造函数。
三、继承和派生时的构造函数
B继承了A,其实就是B的对象内存中含有一个A的对象。很像是封闭类,但使用场景又不一样。
1.构造函数问题
先执行A的构造函数再执行B的构造函数。如果想指定使用A的哪个构造函数,就要在B构造函数的初始化列表中加以说明。不说明就使用A的默认构造函数(没有就报错)
class A
{
public:
A(){ cout << "A默构" << endl; }
A(int x) :a(x) { cout << "A构造" << endl; }
int a;
};
class B:public A
{public:
int b;
public:
B(){ cout << "B默构" << endl; }
B(int x):b(x),A(3){ cout << "B构造" << endl; }
};
int main()
{
B b1;
B b2(3);
cout << b1.b << "," << b1.a<<endl;
cout << b2.b << "," << b2.a;
system("pause");
return 0;
}
2、复制构造函数问题
派生类不显性定义复制构造函数,编译器就会自动调用基类复制构造函数,来初始化基类那部分。如果编译器显性定义了复制构造函数,就要在该函数中指明如何初始化基类那部分,可以使用构造函数,也可以使用复制构造函数,不说就是使用默认构造函数。
总结:
1、封闭类和派生类的构造函数问题是一样的,因为它们内部都有一个子对象。当一个类既是派生类又含成员对象时,先执行基类构造函数。因为在内存里基类那部分排在前面(也在虚函数表指针后面)。写复制构造函数时既要指明成员对象初始化方式,也要指明基类那部分对象的初始化方式。
2、只有知道产生了哪些对象,才知道调用了哪些构造函数。
四、关键字explicit
1、explicit关键字只需用于类内的单参数构造函数前面。由于无参数的构造函数和多参数的构造函数总是显示调用,这种情况在构造函数前加explicit无意义。
用以禁止类型转化,如 A a=1;是利用单参数转换构造函数将1隐形类型转换为了A类型对象,如果我们在单参数构造函数前加上explicit,就可以禁止这种类型转化。这样A a=1;
就是错误的了,但我们可以进行显示类型转换
A A=(A)1;
隐形类型转换在我看来是好东西,在优秀程序员看来是祸害(笑哭.jpg)如:
google的c++规范中提到explicit的优点是可以避免不合时宜的类型变换,缺点无。所以google约定所有单参数的构造函数都必须是显示的,只有极少数情况下拷贝构造函数可以不声明称explicit。例如作为其他类的透明包装器的类。
2、 也有一个例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数。
3、复制构造函数,参数类型为const默认允许临时对象的产生,如果不是常引用,则不允许进行类型转换。
3、复制构造函数前加explicit,可以禁止在初始化时利用“=”
来调用复制构造函数。因为等号就应该是重载的那个等号,这种混乱的东西应该禁止。