复制构造函数被自动调用的时机

对象参数传参时

示例代码

#include <stdio.h>
#include <string.h>

class Person {
public:
  Person() {
    name = NULL;//无参构造函数,初始化指针
  }
  Person(const Person& obj) {
    // 注:如果在拷贝构造函数中直接复制指针值,那么对象内的两个成员指针会指向同一个资源,这属于浅拷贝
    // this->name = obj.name;
    // 为实参对象中的指针所指向的堆空间制作一份副本,这就是深拷贝了
    int len = strlen(obj.name);
    this->name = new char[len + sizeof(char)]; // 为了便于讲解,这里没有检查指针
    strcpy(this->name, obj.name);
  }
  ~Person(){					// 析构函数,释放资源
    if (name != NULL) {
      //如果使用浅拷贝,执行到这里会产生错误,因为源对象和复制的对象在作用域结束时会调用到此处,所以会产生同一个资源释放两次的错误。
      delete[] name;		
      name = NULL;
    }
  }


  void setName(const char* name) {
    int len = strlen(name);
    if (this->name != NULL) {
      delete [] this->name; 
    }
    this->name = new char[len + sizeof(char)]; // 为了便于讲解,这里没有检查指针
    strcpy(this->name, name);
  }
public:
  char * name;
};

void show(Person person){	// 参数是对象类型,会触发拷贝构造函数
  printf("name:%s\n", person.name);
}

int main(int argc, char* argv[]) {
  Person person; 
  person.setName("Hello");
  show(person);
  return 0;
}

汇编

mov     [rsp+arg_8], rdx
mov     [rsp+arg_0], ecx
sub     rsp, 48h
lea     rcx, [rsp+48h+var_20]
call    String_constructor ; Microsoft VisualC v14 64bit runtime
                        ; Microsoft VisualC 64bit universal runtime
lea     rdx, aHello     ; "Hello"
lea     rcx, [rsp+48h+var_20]
call    String_set
lea     rax, [rsp+48h+var_10]
mov     [rsp+48h+var_18], rax
lea     rdx, [rsp+48h+var_20]
mov     rcx, [rsp+48h+var_18]
call    String_constructor_copy
mov     rcx, rax
call    show
mov     [rsp+48h+var_28], 0
lea     rcx, [rsp+48h+var_20]
call    String_dectructor
mov     eax, [rsp+48h+var_28]
add     rsp, 48h
retn

上面对象var_20的地址被作为参数,被var_18的构造函数调用,然后var_18的地址作为show的参数,但是实际这里用c++代码只有下面这个一句

 show(person);

对象值传递会先调用复制构造函数初始化一个临时对象(参数对象),然后将这个临时对象的地址传入目标函数(show)使用,注意这个参数对象的生存周期只存在与目标函数(show),虽然对象的位置是在上一层(main)的栈帧中。

看show的汇编代码

mov     [rsp+arg_0], rcx
sub     rsp, 28h
mov     rax, [rsp+28h+arg_0]
mov     rdx, [rax]
lea     rcx, Format     ; "name:%s\n"
call    printf
mov     rcx, [rsp+28h+arg_0]
call    String_dectructor
add     rsp, 28h
retn

在show函数中直接把参数对象析构了

如果没有定义复制构造函数,就会直接内存浅拷贝。

综上所述,对象在传参之前会调用内存拷贝函数复制一份对象,然后将该对象的地址赋值给函数,并在函数内析构。

返回对象时

示例代码

#include <stdio.h>
#include <string.h>

class Person {
public:
  Person() {
    name = NULL;//无参构造函数,初始化指针
	printf("ctor\n");
  }
 Person(const Person& obj) {
    // // // 注:如果在拷贝构造函数中直接复制指针值,那么对象内的两个成员指针会指向同一个资源,这属于浅拷贝
this->name = obj.name;
    // // // 为实参对象中的指针所指向的堆空间制作一份副本,这就是深拷贝了
   int len = strlen(obj.name);
  this->name = new char[len + sizeof(char)]; // 为了便于讲解,这里没有检查指针
   strcpy(this->name, obj.name);
   printf("copy ctor\n");
  }
  ~Person(){					// 析构函数,释放资源
    if (name != NULL) {
      //如果使用浅拷贝,执行到这里会产生错误,因为源对象和复制的对象在作用域结束时会调用到此处,所以会产生同一个资源释放两次的错误。
      delete[] name;		
      name = NULL;
    }
  }


  void setName(const char* name) {
    int len = strlen(name);
    if (this->name != NULL) {
      delete [] this->name; 
    }
    this->name = new char[len + sizeof(char)]; // 为了便于讲解,这里没有检查指针
    strcpy(this->name, name);
  }
public:
  char * name;
};

Person getObject() {	
  Person person;   //定义局部对象
  person.setName("Hello");
  return person;              
}

int main(int argc, char* argv[]) {
 // Person person;
 // person = getObject();
 getObject();
  return 0;
}

main

mov     [rsp+arg_8], rdx
mov     [rsp+arg_0], ecx
sub     rsp, 38h
lea     rcx, [rsp+38h+var_18]
call    getObject
lea     rcx, [rsp+38h+var_18]
call    Person_dtor
xor     eax, eax
add     rsp, 38h
retn

getObject

mov     [rsp+arg_0], rcx
sub     rsp, 38h
lea     rcx, [rsp+38h+var_18]
call    Person_ctor
lea     rdx, aHello     ; "Hello"
lea     rcx, [rsp+38h+var_18]
call    Person_setName
lea     rdx, [rsp+38h+var_18]
mov     rcx, [rsp+38h+arg_0]
call    Person_ctor_copy
lea     rcx, [rsp+38h+var_18]
call    Person_dtor
mov     rax, [rsp+38h+arg_0]
add     rsp, 38h
retn

可以看到在main函数中,编译器分配了一个临时对象,将它的地址传入getObject,并在getObject调用了赋值构造函数,并返回了该临时对象的地址,该临时对象在main函数中执行完getObject后立即析构

有一种特殊情况,不会产生临时对象,或者说将临时对象的生命周期延长

mian函数c++代码


int main(int argc, char* argv[]) {
  
 Person person = getObject();
 person.setName("你好");
  return 0;
}

main函数汇编代码

mov     [rsp+arg_8], rdx
mov     [rsp+arg_0], ecx
sub     rsp, 38h
lea     rcx, [rsp+38h+var_10]
call    getObject
lea     rdx, asc_1400132F8 ; "你好"
lea     rcx, [rsp+38h+var_10]
call    Person_setName
mov     [rsp+38h+var_18], 0
lea     rcx, [rsp+38h+var_10]
call    Person_dtor
mov     eax, [rsp+38h+var_18]
add     rsp, 38h
retn

可以看到临时对象在getObject结束以后并没有立即析构,而是在main函数结束以后才析构

综上所述,若编译器检测到返回对象时,函数内部会自动生成对象指针参数,并使用对象指针参数调用复制构造函数,并将该指针返回,而外部则分配一个临时对象,并在调用完后,立即析构,若有变量在声明时承接函数返回结果,则将该变量当作临时变量,生命周期和正常的局部对象一样

对象赋值时

代码示例

#include <stdio.h>
#include <string.h>

class Person {
public:
  Person() {
    name = NULL;//无参构造函数,初始化指针
	printf("ctor\n");
  }
 Person(const Person& obj) {
    // // // 注:如果在拷贝构造函数中直接复制指针值,那么对象内的两个成员指针会指向同一个资源,这属于浅拷贝
this->name = obj.name;
    // // // 为实参对象中的指针所指向的堆空间制作一份副本,这就是深拷贝了
   int len = strlen(obj.name);
  this->name = new char[len + sizeof(char)]; // 为了便于讲解,这里没有检查指针
   strcpy(this->name, obj.name);
  }
  ~Person(){					// 析构函数,释放资源
    if (name != NULL) {
      //如果使用浅拷贝,执行到这里会产生错误,因为源对象和复制的对象在作用域结束时会调用到此处,所以会产生同一个资源释放两次的错误。
      delete[] name;		
      name = NULL;
    }
  }


  void setName(const char* name) {
    int len = strlen(name);
    if (this->name != NULL) {
      delete [] this->name; 
    }
    this->name = new char[len + sizeof(char)]; // 为了便于讲解,这里没有检查指针
    strcpy(this->name, name);
  }
//void operator = (Person & obj){};

public:
  char * name;
};

Person getObject() {	
  Person person;   //定义局部对象
  person.setName("Hello");
  return person;              
}


int main(int argc, char* argv[]) {
  
 Person person;
 person.setName("你好");
 Person person2;
 person2 =person;
 printf("%x\n",person.name);
 printf("%x\n",person2.name);
  return 0;
}

对象赋值时有两种情况,一种是会自动调用赋值构造函数,一种会调用operator =函数,由于这两个成员函数的默认代码是一样的浅复制内存,所以非常容易搞混

自动调用赋值构造函数

main c++

int main(int argc, char* argv[]) {
  
 Person person;
 person.setName("你好");
 Person person2 = person;
 
 printf("%x\n",person.name);
 printf("%x\n",person2.name);
  return 0;
}

汇编

push    ebp
mov     ebp, esp
sub     esp, 0Ch
lea     ecx, [ebp+person]
call    Person_ctor
push    offset asc_412168 ; "你好"
lea     ecx, [ebp+person]
call    Person_setName
lea     eax, [ebp+person]
push    eax
lea     ecx, [ebp+person2]
call    Person_ctor_copy
mov     ecx, [ebp+person]
push    ecx
push    offset Format   ; "%x\n"
call    _printf
add     esp, 8
mov     edx, [ebp+person2]
push    edx
push    offset asc_412174 ; "%x\n"
call    _printf
add     esp, 8
mov     [ebp+var_C], 0
lea     ecx, [ebp+person2]
call    Person_dtor
lea     ecx, [ebp+person]
call    Person_dtor
mov     eax, [ebp+var_C]
mov     esp, ebp
pop     ebp
retn

可以看到,当对象声明的时候进行对象赋值时,此时会自动调用复制构造函数

自动调用operator =

main c++

int main(int argc, char* argv[]) {
  
 Person person;
 person.setName("你好");
 Person person2;
 person2 =person;
 printf("%x\n",person.name);
 printf("%x\n",person2.name);
  return 0;
}

汇编

push    ebp
mov     ebp, esp
sub     esp, 0Ch
lea     ecx, [ebp+person]
call    Person_ctor
push    offset asc_412168 ; "你好"
lea     ecx, [ebp+person]
call    Person_setName
lea     ecx, [ebp+person2]
call    Person_ctor
lea     eax, [ebp+person]
push    eax
lea     ecx, [ebp+person2]
call    operator等于
mov     ecx, [ebp+person]
push    ecx
push    offset Format   ; "%x\n"
call    _printf
add     esp, 8
mov     edx, [ebp+person2]
push    edx
push    offset asc_412174 ; "%x\n"
call    _printf
add     esp, 8
mov     [ebp+var_C], 0
lea     ecx, [ebp+person2]
call    Person_dtor
lea     ecx, [ebp+person]
call    Person_dtor
mov     eax, [ebp+var_C]
mov     esp, ebp
pop     ebp
retn

可以看到,person和person2构造之后,person2就调用了operator = 函数

综上所述,单纯的对象赋值并不会触发复制构造函数,而是触发了operator = 函数,只有对象声明时发生复制才会触发复制构造函数

posted @ 2022-04-22 20:53  乘舟凉  阅读(78)  评论(0编辑  收藏  举报