第九章 拷贝构造函数、深拷贝、浅拷贝、匿名对象、隐式构造、默认构造函数

拷贝构造函数(Copy Constructor)

  1. 拷贝构造函数是构造函数的一种
  2. 当利用已存在的对象创建一个新对象时(类似于拷贝),就会调用新对象的拷贝构造函数进行初始化
  3. 拷贝构造函数的格式是固定的,接收一个const引用作为参数
  4. 默认拷贝的本质

     class Car {
    	
     public:
         int m_price;
         int m_length;
         Car( int price = 0,int length =0) :m_price(price),m_length(length) {
             cout << "Car( int price = 0,int length =0)" << endl;
         }
         void display() {
             cout << "void display()"<<"-m_price-"<<m_price<<"-m_length-"<<m_length << endl;
         }
     }
     int main() {
         Car car1;
         Car car2(100);
         Car car3(100, 5);
            	
         //利用已经存在的car3对象创建了一个car4新对象
         //car4初始化时会调用拷贝构造函数
         //没有手动实现拷贝函数的话,默认也会拷贝操作
         Car car4(car3);
         //打印拷贝成功void display() - m_price - 100 - m_length - 5
         car4.display();
         getchar();
         return 0;
     }
    
    1. Car car4(car3);反汇编本质如下:

       mov         eax,dword ptr [ebp-30h]
       mov         dword ptr [ebp-40h],eax
       mov         ecx,dword ptr [ebp-2Ch]
       mov         dword ptr [ebp-3Ch],ecx
      
      1. ebp-30h是car3的首地址,也是car3第一个成员变量的地址;ebp-2Ch是car3的第二个成员变量的地址
      2. ebp-40h是car4的首地址,也是car4第一个成员变量的地址;ebp-3Ch是car4的第二个成员变量的地址
      3. 上面默认的本质是

         car4.m_length = car3.m_length;
         car4.m_price = car3.m_price;
        
  5. 手动实现拷贝函数

     class Car {
    	
     public:
     	int m_price;
     	int m_length;
     	Car( int price = 0,int length =0) :m_price(price),m_length(length) {
     		cout << "Car( int price = 0,int length =0)" << endl;
     	}
     	//手动实现拷贝构造函数
     	//需要手动实现拷贝操作
     	Car(const Car &car) :m_price(car.m_price),m_length(car.m_length){
     		/*m_price = car.m_price;
     		m_length = car.m_length;*/
     		cout << "Car(const Car &car) " << endl;
     	}
     	void display() {
     		cout << "void display()"<<"-m_price-"<<m_price<<"-m_length-"<<m_length << endl;
     	}
     };
        
     int main() {
     	Car car3(100, 5);
        
     	//利用已经存在的car3对象创建了一个car4新对象
     	//car4初始化时会调用拷贝构造函数
     	//手动实现拷贝构造函数
     	Car car4(car3);
     	//打印的值是乱码:void display() - m_price--858993460 - m_length--858993460
     	//因此,一旦手动实现拷贝函数,那么就需要自己手动完成拷贝操作
     	car4.display();
     	getchar();
     	return 0;
     }
    
  6. 总结:
    1. C++默认就有拷贝函数,而且默认拷贝就是将旧对象的成员变量赋值给新的成员变量
    2. 一旦手动实现拷贝构造函数,那么默认就无效,需要手动实现拷贝
    3. 但是我们发现上述的手动实现拷贝构造好像多此一举,因此,当对象的成员变量是基本数据类型时,不需要手动实现构造函数。

调用父类的拷贝构造函数

class Person {
public:
	int m_age;
	Person(int age = 0):m_age(age){}
	//拷贝构造
	Person(const Person &person):m_age(person.m_age){}
};

class Student : public Person {
public:
	int m_score;
	Student(int age = 0,int score = 0):Person(age),m_score(score){}
	//拷贝构造
	//初始化列表中直接调用父类的拷贝构造函数Person(student),来拷贝父类的成员变量
	Student(const Student &student) :Person(student), m_score(student.m_score) {}
};

//使用
Student student(18, 100);
Student student2(student);
cout << student.m_age << endl;
cout << student2.m_age << endl;
  1. 如何拷贝父类的成员?如何调用父类的拷贝构造函数?
    1. 在子类拷贝构造的初始化列表中直接调用父类的拷贝构造函数。

深拷贝、浅拷贝

  1. 成员变量为字符串的写法
    1. strcpy过期问题解决
      1. 方法一: 在写代码的前面加上如下宏定义:#define _CRT_SECURE_NO_WARNINGS
        1. 注意:一定要写在最前面!!!
      2. 方法二: 操作 vs 中,在项目 -> 属性 -> C/C++ -> 预处理器 -> 预处理器定义中添加 _CRT_SECURE_NO_WARNINGS 这个预定义。
    2. C++中字符串常量必须用const修饰
    3. 函数的形参接收字符串时,必须也用const修饰,否则实参为字符串常量传入时会报错
    4. 代码示例:

       class Car {
       	int m_price;
       	char *m_name;
       public:
       	//这里形参必须用const修饰,否则外部创建直接传入字符串常量,会报错不匹配,而且const形参也可以接收字符串变量
       	Car(int price = 0, const char *name = NULL) :m_price(price) {
       		//但是name是const,而成员m_name是非const,很显然不能直接赋值,怎么办呢?
       		//m_name = name;
       		if (name == NULL)return;
       		//申请堆空间存储字符串的内容
       		this->m_name = new char[strlen(name) + 1]{};
       		//拷贝字符串内容到堆空间
       		strcpy(this->m_name, name);
       		cout << "Car(int price = 0, char *name = NULL)" << endl;
       	}
       	~Car(){
       		if (this->m_name == NULL) return;
       		delete[] this->m_name;
       		this->m_name = NULL;
       	}
       	void display() {
       		cout  << "price-" << m_price << "name-" << m_name << endl;
       	}
       };
              
       int main() {
       	//定义字符串
       	//数组方法定义
       	char name[] = { 'b','m','w','\0' };
       	//因为“bmw”是一个字符串常量,因此在C++中必须用const修饰
       	const char *name2 = "bmw";
       	cout << name << endl;
       	//strlen函数不包含\0字符
       	cout << strlen(name) << endl;
              	
       	Car *car = new Car(100, "bwm");
       	car->display();
       }
      
  2. 浅拷贝
    1. 示例:

       //car类跟上面一模一样
       //使用
       int main() {
       	Car car1(100,"bwm");
       	//拷贝,将car1的内存空间复制给car2的内存空间,8个字节
       	//即car1、car2的m_price/m_name完全一样
       	//这么做的话,car1的m_name跟car2的m_name指向同一的堆空间
       	//导致结果:1. 一旦任何一个car的m_name被修改,另外一个一定也被修改 2. m_name指向的堆空间会被多次释放
       	Car car2 = car1;
              
       	getchar();
       	return 0;
       }
      
    2. Car car2 = car1;意义:

      1. 拷贝,将car1的内存空间复制给car2的内存空间,8个字节
      2. 即car1、car2的m_price/m_name完全一样
      3. 这么做的话,car1的m_name跟car2的m_name指向同一的堆空间
      4. 导致结果:
        1. 一旦任何一个car的m_name被修改,另外一个一定也被修改
        2. m_name指向的堆空间会被多次释放
      5. 这个就是浅拷贝
  3. 深拷贝
    1. 示例:

       class Car {
       	int m_price;
       	char *m_name;
       public:
       	//这里形参必须用const修饰,否则外部创建直接传入字符串常量,会报错不匹配,而且const形参也可以接收字符串变量
       	Car(int price = 0, const char *name = NULL) :m_price(price) {
       		//但是name是const,而成员m_name是非const,很显然不能直接赋值,怎么办呢?
       		//m_name = name;
       		if (name == NULL)return;
       		//申请堆空间存储字符串的内容
       		this->m_name = new char[strlen(name) + 1]{};
       		//拷贝字符串内容到堆空间
       		strcpy(this->m_name, name);
       		cout << "Car(int price = 0, char *name = NULL)" << endl;
       	}
       	//深拷贝:因为涉及到内存分配,因此要实现拷贝构造函数
       	Car(const Car &car) :m_price(car.m_price) {
       		if (car.m_name == NULL)return;
       		//申请堆空间存储字符串的内容
       		this->m_name = new char[strlen(car.m_name) + 1]{};
       		//拷贝字符串内容到堆空间
       		strcpy(this->m_name, car.m_name);
       		cout << "Car(const Car &car) :m_price(car.m_price) " << endl;
       	}
       	~Car(){
       		if (this->m_name == NULL) return;
       		delete[] this->m_name;
       		this->m_name = NULL;
       		cout << "~Car() " << endl;
       	}
       };
              
       //使用:
       int main() {
       	{
       		Car car1(100, "bwm");
       		//对象初始化
       		Car car2 = car1;
       		Car car3(car2);
       		//本质是调用默认参数构造函数:car1(0, NULL);
       		Car car4;
       		//变量赋值
       		car4 = car3;
       	}
              	
              
       	getchar();
       	return 0;
       }
      
    2. 总结:

      1. 实现拷贝构造后:
        1. car1/car2的m_price一样,但是m_name不一样,他们分别指向不同的堆空间。
        2. car1/car2释放时分别释放自己的堆内存
      2. car2、car3都是通过拷贝构造函数初始化的,car、car4是通过非拷贝构造函数初始化
      3. car4 = car3是一个赋值操作(默认是浅复制),并不会调用拷贝构造函数
        1. 注意:Car car2 = car1;的区别,前者是对象赋值,后者是对象初始化
        2. 构造函数:顾名思义只跟创建(初始化)对象有关
  4. 深拷贝、浅拷贝总结
    1. 编译器默认的提供的拷贝是浅拷贝(shallow copy)
      1. 将一个对象中所有成员变量的值拷贝到另一个对象
      2. 如果某个成员变量是个指针,只会拷贝指针中存储的地址值,并不会拷贝指针指向的内存空间
      3. 可能会导致堆空间多次free的问题
    2. 如果需要实现深拷贝(deep copy),就需要自定义拷贝构造函数
      1. 将指针类型的成员变量所指向的内存空间,拷贝到新的内存空间

对象型参数和返回值

  1. 使用对象类型作为函数的参数或者返回值,可能会产生一些不必要的中间对象
    1. 示例代码:

       class Car {
       	int m_price;
       public:
       	Car(int price = 0) :m_price(price) {
       		cout << "Car(int)-" <<this<<"-"<<this-m_price<< endl;
       	}
       	Car(const Car &car) :m_price(car.m_price) {
       		cout << "Car(const Car)-" << this << "-" << this - m_price << endl;
       	}
       };
       //对象为参数
       void test1(Car car) {}
       //对象为返回值
       Car test2() {
           	Car car(10);
           	return car;
       }
      
    2. 对象作为参数:

       //使用
       Car car1(10);
       test1(car1);
      
      1. 打印:

         Car(int)-003AF9C0-003AF998   //Car car1(10);调用的构造函数
         Car(const Car) - 003AF8E0 - 003AF8B8 //test1(car1);调用的?
        
      2. test1(car1);为何会调用拷贝构造呢?
      3. test1(car1);就相当于

         void test1(Car car = car1) {}
        
        1. 那么Car car = car1参数就相当于调用拷贝构造函数,此时的car就是一个新的对象,而不是原来的旧对象,我们通常是为了改变旧对象的值,那么怎么办呢?
        2. 解决办法:形参传引用

           void test1(Car &car) {}
          
    3. 对象作为返回值

      1. 使用1:

         Car car = test2();
        
        1. 打印:

           Car(int)-001EFA9C-001EFA74   //test2()内部创建对象
           Car(const Car) - 001EFB84 - 001EFB5C  //Car car = test2创建的对象,会调用拷贝
          
        2. test2创建一个临时对象然后拷贝给外部对象

      2. 使用2:

         Car car2 ;
         car2 = test2();
        
        1. 打印:

           Car(int) - 0041FD50 - 0041FD50  //Car car2 ;调用
           Car(int) - 0041FC5C - 0041FC34  // test2(); 调用
           Car(const Car) - 0041FC84 - 0041FC5C  //这个呢???
          
          1. car2 = test2();这个是赋值,不是拷贝呀?为何会调用拷贝构造呢?
          2. 因为test2()内部创建的是一个临时对象,当外部赋值时,C++为了安全起见,默认会自动调用拷贝构造,成一个新的对象赋值给外部,旧的对象释放掉
          3. 说白了就是:car2 = xxx,等号右边是对象的时候是赋值,等号右边是函数的时候是拷贝

匿名对象(临时对象)

  1. 匿名对象:没有变量名、没有被指针指向的对象,用完后马上调用析构

     class Person {
     public:
     	Person() {
     		cout << "Person()-" << this<< endl;
     	}
     	Person(const Person &person) {
     		cout << "Person(const Person &)-" << this << endl;
     	}
     	~Person()
     	{
     		cout << "~Person()-" << this << endl;
     	}
     	void display() {
     		cout << "display()-" << this << endl;
     	}
     };
        
     void test1(Person peroson) {}
        
     Person test2() {
     	//直接返回匿名对象
     	return Person();
     }
        
     int main() {
        
        
     	//匿名对象
     	//运行打印可知:改对象创建完毕之后立即销毁
     	//Person();
     	//Person().display();
        
        
     	//会调用拷贝构造
     	/*Person person;
     	test1(person);*/
     	//参数为匿名对象时就不会调用拷贝构造函数了
     	//test1(Person());
        
     	//直接返回匿名对象,也不会调用拷贝构造函数
     	Person person1;
     	person1 = test2();
        
     	getchar();
     	return 0;
     }
    

隐式构造

  1. C++中存在隐式构造的现象:某些情况下,会隐式调用单参数(不一定是单参数,也可以是多参数)的构造函数

     class Person {
     	int m_age;
     public:
     	Person() :Person(0){
     		cout << "Person()-" << this << endl;
     	}
     	Person(int age) : m_age(age) {
     		cout << "Person(int)-" << this << endl;
     	}
     	Person(const Person &person) {
     		cout << "Person(const Person &)-" << this << endl;
     	}
        
     	void display() {
     		cout << "display()-age is-" << this->m_age << endl;
     	}
     };
     void test1(Person peroson) {}
        
     Person test2() {
     	//直接返回一个值
     	return 70;
     }
     int main() {
        	
     	Person person(10);
     	person = 20;
     	person.display();
     	/*
     	//打印
     	Person(int)-002BF978
     	Person(int)-002BF8AC
     	display()-age is-20
        
     	会发现调用了2次构造函数Person(int),为什么呢?
     	第一次,肯定是Person person(10);这个调用的
     	那么第二次呢?
     	person = 20;这么赋值本来肯定是有错误的,但是为何运行没错呢?-隐式构造,等价于
     	person = Person(20);//先通过匿名对象创建出来,然后在赋值给person对象
        
     	*/
        	
     	//一定要注意区别!!!!!!!!!
     	//呈现方式1(定义一个对象时赋值)
     	Person person2 = 10;  //Person(int)
     	//等价于(通过单参数的构造函数创建一个对象)
     	//Person person2(10);
        
     	//呈现方式2(给一个已经创建的对象赋值)
     	Person person3(10);
     	person3 = 20;
     	//等价于(已存在对象 = (浅拷贝)匿名对象)
     	//person3 = Person(20);//匿名对象,注意哦,类(20)
        
     	//呈现方式3:
     	//隐式构造针对函数参数为对象
     	test1(50);
        
     	//呈现方式4:
     	//隐式构造真的函数返回值为对象
     	Person person4 = test2();
     	getchar();
     	return 0;
     }
    
  2. 可以通过关键字explicit禁止掉隐式构造

     explicit Person(int age) : m_age(age) {
         cout << "Person(int)-" << this << endl;
         }
    
    1. 只要添加了explicit 上面的直接赋值为数字的都会报错
    2. explicit作用:防止让别人误解-数字直接赋值给对象

编译器自动生成的构造函数

  1. 很多教材上说:编译器会为每一个类生成一个默认的无参的构造函数,这个结论是不正确

     class Person {
     public:
     	int m_age;
     };
        
     int main(   
         /*
         //这两句话反汇编
         Person person;
         person.m_age = 10;
         //结果
         mov         dword ptr [ebp-8],0Ah
         */
         //通过反汇编发现,这一句没有调用任何构造函数
         Person person;
         person.m_age = 10;
         getchar();
         return 0;
     }
    
    1. 但是某些情况下,确实会生成
  2. C++的编译器在某些特定的情况下,会给类自动生成无参的构造函数,比如
    1. 成员变量在声明的同时进行了初始化
    2. 有定义虚函数
    3. 虚继承了其他类
    4. 包含了对象类型的成员,这个成员有构造函数(编译器生成或自定义)
    5. 父类有构造函数(编译器生成或自定义)
  3. 总结一下
    1. 对象创建后,需要做一些额外操作时(比如内存操作、函数调用),编译器一般都会为其自动生成无参的构造函数
  4. 代码示例:

     class Car {
     public:
     	//有构造函数
     	Car() {
        
     	}
     };
        
     class Person {
     public:
     	//1.成员变量在声明的同时进行了初始化
     	//int m_age =0 ;
     	//2. 有定义虚函数
     	//virtual void test() {}
        	
     	//4. 包含了对象类型的成员,且这个成员有构造函数(编译器生成或自定义)
     	Car car;
        
     };
        
     //3. 虚继承了其他类
     class  Student : virtual public Person {
     };
    
     int main() {
     	/*
     	//Person person;
     	lea         ecx,[ebp-0Ch]  
     	call        00A6137F  //调用构造函数
     	//person.m_age = 10;
     	mov         dword ptr [ebp-0Ch],0Ah  
     	*/
     	//通过反汇编发现,这一句有调用构造函数
     	Person person;
     	//person.m_age = 10;
        
     	Student student;
     	getchar();
     	return 0;
     }
    
Table of Contents