目录
一、复制构造函数
1、定义
复制构造函数同默认构造函数一样是编译器在程序没有显示定义对应函数时自动添加的特殊成员函数,复制构造函数的入参是一个const或者非const的类引用,可以有其他入参,但是必须有默认值。
class ClassA{
private:
int a=1;
int b=2;
public:
//构造函数
ClassA(int i,int j);
//析构函数
~ClassA();
//复制构造函数
// ClassA(const ClassA & ca);
// ClassA(ClassA & ca);
ClassA(ClassA & ca,int i=2,int j=2);
};
ClassA::ClassA(int i,int j){
a=i;
b=j;
std::cout<<"execute ClassA(int i,int j),this->"<< this<<"\n";
}
//ClassA::ClassA(const ClassA & ca){
// std::cout<<"execute ClassA(const ClassA & ca),this->"<<this<<"\n";
//}
//ClassA::ClassA(ClassA & ca){
// std::cout<<"execute ClassA(ClassA & ca),this->"<<this<<"\n";
//}
ClassA::ClassA(ClassA & ca,int i,int j){
std::cout<<"execute ClassA(ClassA & ca),this->"<<this<<"\n";
}
ClassA::~ClassA(){
std::cout<<"execute ~ClassA,this->"<< this<<"\n";
}
int main(){
ClassA a(1,2);
ClassA b=a;
ClassA b2=a;
ClassA b3=a;
return 0;
}
执行结果如下,注意析构函数按对象创建的顺序倒序执行,即后创建先执行,主要是为了避免后面创建的对象对前面创建的对象有依赖的情形下删除前面创建的对象会产生不可预知的后果。
2、隐式调用场景
《C++ Primer Plus》中提到下列情形下编译器会隐式调用复制构造函数:
- 以某个对象实例初始化另一个同类型的对象
- 对象以实例而非引用或者指针的形式作为方法入参
- 以对象实例而非引用或者指针的形式作为方法的返回值
- 方法连续调用过程中产生临时对象
第四种实际上跟第三种是一样的,因为只有方法的返回值是对象实例而非引用或者指针的时候才会产生临时对象,大部分博客说的是前面三种情形。但是在当前支持C++11标准的GNU C比如版本 4.8.5 下,第三条在返回的对象实例是在方法中创建的情形下不成立,参考如下测试用例:
#include <iostream>
class ClassA{
private:
int a=1;
int b=2;
static int c;
public:
//构造函数
ClassA(int i,int j);
//析构函数
~ClassA();
//复制构造函数
ClassA(const ClassA & ca);
//运算符重载
ClassA operator+(const ClassA & ca);
//友元函数
friend void print(ClassA cl);
};
int ClassA::c=0;
ClassA::ClassA(int i,int j){
a=i;
b=j;
c++;
std::cout<<"execute ClassA(int i,int j),this->"<< this<<"\n";
}
ClassA::~ClassA(){
c--;
std::cout<<"execute ~ClassA,this->"<< this<<"\n";
}
ClassA::ClassA(const ClassA & ca){
a=ca.a;
b=ca.b;
c++;
std::cout<<"execute ClassA(const ClassA & ca),this->"<<this<<"\n";
}
ClassA ClassA::operator +(const ClassA & ca){
return ClassA(a+ca.a,b+ca.b);
}
void print(ClassA cl){
std::cout <<"print a=" <<cl.a <<",b="<<cl.b <<",c="<< cl.c <<"\n";
}
ClassA getA(int a,int b){
std::cout <<"getA a=" <<a <<",b="<<b <<"\n";
return ClassA(a,b);
}
int main(){
ClassA a(1,1.2);
std::cout<<"a=" <<&a <<"\n";
//情形1
ClassA b=a;
std::cout<<"b=" <<&b <<"\n";
//情形2
print(b);
//情形3
ClassA c=getA(2,3);
std::cout<<"c=" <<&c<<"\n";
//情形4
ClassA d=a+b+c;
std::cout<<"d=" <<&d <<"\n";
return 0;
}
执行的结果如下:
为什么第三种和第四种情形下调用了构造函数而没有调用复制构造函数?从反汇编找答案。之前在Java程序员自我修养——C与汇编语言探讨过结构体作为入参和返回值时编译器会通过寄存器传递或者直接内存拷贝的方式在调用函数与被调函数的栈帧之间复制结构体占用的内存,类在内存的表示跟结构体是一样的,那么处理方式是否一样了?
main方法中执行print(b)的汇编代码如下:
lea -0x50(%rbp),%rdx
lea -0x30(%rbp),%rax
mov %rdx,%rsi
mov %rax,%rdi
callq 0x4009dc <ClassA::ClassA(ClassA const&)> 调用复制构造函数,rdi是待创建的对象的地址,即this指针,rsi中是方法入 参类引用。
lea -0x30(%rbp),%rax
mov %rax,%rdi
callq 0x400a9e <print(ClassA)> 调用print方法,因为print是非类成员没有this指针,rdi中保存的是方法入参ClassA类实例的地 址,该实例是通过复制构造函数创建出来的
lea -0x30(%rbp),%rax
mov %rax,%rdi
callq 0x400994 <ClassA::~ClassA()> 调用析构函数销毁调用print()时临时创建的一个对象
执行复制构造函数ClassA::ClassA(ClassA const&)的主要汇编代码如下:
mov %rdi,-0x8(%rbp)
mov %rsi,-0x10(%rbp) 将寄存器参数拷贝至栈帧中
mov -0x8(%rbp),%rax
movl $0x1,(%rax) 初始化属性 int a=1
mov -0x8(%rbp),%rax
movl $0x2,0x4(%rax) 初始化属性 int b=2
mov -0x10(%rbp),%rax
mov (%rax),%edx
mov -0x8(%rbp),%rax
mov %edx,(%rax) 完成属性赋值 a=ca.a
mov -0x10(%rbp),%rax
mov 0x4(%rax),%edx
mov -0x8(%rbp),%rax
mov %edx,0x4(%rax) 完成属性赋值 b=ca.b
mov 0x201773(%rip),%eax
add $0x1,%eax
mov %eax,0x20176a(%rip) 修改静态属性 c++
执行print(ClassA)的的部分汇编代码:
mov 0x2016df(%rip),%ebx 将静态变量c的值放入ebx中
mov -0x28(%rbp),%rax
mov 0x4(%rax),%r12d 将 属性b放入r12d中
mov -0x28(%rbp),%rax
mov (%rax),%r13d 将 属性a放入r12d中
main方法中执行getA方法的部分代码如下:
lea -0x60(%rbp),%rax
mov $0x3,%edx
mov $0x2,%esi
mov %rax,%rdi 准备调用参数,getA不是类成员函数,但是依然将待创建的对象的内存地址即rdi中的值作为第一个参数传入 函数中
callq 0x400b25 <getA(int, int)>
getA方法的部分代码如下