0
点赞
收藏
分享

微信扫一扫

【C++】拷贝构造函数 Copy Constructor

小云晓云 2022-01-25 阅读 64

拷贝构造

拷贝构造函数 Copy Constructor

也是构造函数的一种

  • 当利用已存在的对象创建一个新对象时(类似于拷贝),就会调用新对象的拷贝构造函数进行初始化。

  • 拷贝构造函数的格式是固定的,接收一个const引用作为参数

    class Car {	
        int m_price;
        int m_length;public:	
        Car(int price = 0, int length = 0) :m_price(price), m_length(length) {	
            cout << "Car(int price = 0, int length = 0)" << endl;	
        }	 
        // 拷贝构造函数
        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 << "price=" << m_price << ", length=" << m_length << endl;
        }
    };
    int main() {  
        Car car1;   
        Car car2(100);  
        Car car3(100, 5); 
        // 利用已经存在的car3对象创建了一个car4对象  
        // car4初始化时会调用拷贝构造函数  
        // 就算不写拷贝构造函数也可以   
        Car car4(car3);   
        // 相当于
        /*
        car4.m_price = car3.m_price;
        car4.m_length = car3.m_length;
        */
    }
    

继承父类

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) {}
    Student(const Student &student) :m_score(student.m_score) {} 
    // 但Student的age并没有传进来 
    // 所以应该这样写  
    Student(const Student &student) :Person(student), m_score(student.m_score) {}  
    // 但如果不写的话,默认也会直接拷贝过去
};

一些注意点

class Car {
    int m_price;
    int m_length;
public:
    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) {		
        cout << "Car(const Car &car)" << endl;	
    }	
    void display() {		
        cout << "price=" << m_price << ", length=" << m_length << endl;	
    }
};
int main() {
    Car car1(100, 5);
    // Car(int price = 0, int length = 0)
    Car car2(car1);
    // Car(const Car &car)
    Car car3 = car2;
    // Car(const Car &car)
    Car car4;
    // Car(int price = 0, int length = 0)     
    car4 = car3;	
    car4.display(); 
    // price=100, length=5 拷贝成功,但并没有调用拷贝构造函数   
    return 0;
}

第二种和第三种写法是等价的,都是调用拷贝构造函数

为什么car4 = car3;并没有调用拷贝构造函数?

  • 因为拷贝构造函数是在初始化的时候用到的,此时car4和car3都是已经存在的对象
  • 所以这仅仅是赋值,并不是拷贝构造

子类的构造函数会默认调用父类无参的构造函数,如果子类的构造函数指明调用父类的拷贝构造函数,那就不会调用父类无参的构造函数。

浅拷贝、深拷贝

浅拷贝(shallow copy):指针类型的变量只会拷贝地址值
深拷贝(deep copy):将指针指向的内容拷贝到新的存储空间

class Car {	
    int m_price;
    char *m_name;
public:
    Car(int price = 0, const char *name = NULL) :m_price(price) {	
        if (name == NULL) return;	
        // 申请新的堆空间	
        m_name = new char[strlen(name) + 1] {};
        // 拷贝字符串数据到新的堆空间	
        strcpy(m_name, name);	
    }
    ~Car() {	
        if (m_name == NULL) return;	
        delete[] m_name;	
        m_name = NULL;
    }	
    void display() {	
        cout << "price is " << m_price << ", name is " << m_name << endl;
    }
};
int main() {
    const char *name = "bmw";	
    char name[] = {'b', 'm', 'w', '\0'};	
    Car *car = new Car(100, "bmw");
    car->display();	
    delete[] name;	
    getchar();	
    return 0;
}

name的地址和m_name的地址是不一样的

因为m_name申请了新的堆空间并且把name的内容拷贝了过去。

  • 编译器默认提供的拷贝是浅拷贝

    比如拷贝构造

    • 是将一个对象中所有成员变量的值拷贝到另一个对象
    • 如果某个成员变量是指针,只会拷贝指针中存储的地址值,并不会拷贝指针指向的内存空间
    • 可能会导致堆空间多次free的问题
  • 如果要实现深拷贝,就需要自定义拷贝构造函数

    • 将指针类型的成员变量所指向的内存空间,拷贝到新的内存空间

堆空间指向栈空间很危险

野指针,堆空间指向栈空间的指针,当栈空间的内容被回收

所以应该堆空间指向堆空间

但如果使用拷贝构造

int main() {
    Car car1(100, "bmw");
    Car car2 = car1;	
    car2.display();
    getchar();	
    return 0;
}


两个car会指向同一块堆空间(浅拷贝:没有产生新的地址空间)

而且可能会导致double free,同一块堆空间被释放两次

所以应该是再申请一份堆空间,然后将car2指向新的堆空间地址,一人一份就不会有问题。(深拷贝:产生新的地址空间)

Car(const Car &car) :m_price(car.m_price) { 
    if (car.m_name == NULL) return;    
    // 申请新的堆空间 
    m_name = new char[strlen(car.m_name) + 1]{};
    // 拷贝字符串数据到新的堆空间 
    strcpy(m_name, car.m_name);
}

现在完成了两个构造函数,分别都申请了新的堆空间,可以将申请堆空间这部分提取出来封装成函数。

class Car {
    int m_price;	
    char *m_name;
    void copy(const char *name = NULL) {
        if (name == NULL) return;
        // 申请新的堆空间	
        m_name = new char[strlen(name) + 1] {};	
        // 拷贝字符串数据到新的堆空间	
        strcpy(m_name, name);	
    }
public:	
    Car(int price = 0, const char *name = NULL) :m_price(price) {
        copy(name);
    }	
    Car(const Car &car) :m_price(car.m_price) {	
        copy(car.m_name);	
    }

如果类里的数据都是int类型,则不需要深拷贝。只有当类里有指针类型,需要指向某块内存空间的时候才使用深拷贝。

对象型参数和返回值

  • 使用对象类型作为函数的参数或者返回值,可能会产生一些不必要的中间对象

    • 作为参数

      class Car {
      public:
          Car() {	
              cout << "Car() - " << this << endl;
          }	
          Car(const Car &car) {	
              cout << "Car(const Car &) - " << this << endl;
          }
          ~Car() {	
              cout << "~Car() - " << this << endl;
          }
          void run() {
              cout << "run()" << endl;	
          }
      };
      void test1(Car car) {
          
      }
      int main() {	
          Car car1;  
          test1(car1);
          getchar();
          return 0;
      }
      

      • 运行后打印一个普通构造函数和一个拷贝构造函数

        int main() {	
            Car car1;  
            // Car()	
            test1(car1);
            // Car(const Car &car)	
            getchar();
            return 0;
        }
        

      为什么test1调用的是拷贝构造函数?

      • 因为相当于void test1(Car car = car1) ,使用一个已经存在的对象构建新的对象,就是拷贝构造

      但不需要重新创建一个新的对象,我们把对象传进去是为了在函数内部能够使用这个对象的内容,没有必要创建新对象。

      那么可以改成引用

      void test1(Car &car) {
          
      }
      

      这样就是直接引用传入的car的对象,就不会创建一个新的对象

    • 作为返回值

      Car test2() {	
          Car car; 
          // Car()	
          return car;
      }
      int main() {   
          Car car2;
          // Car() 调用普通构造函数  
          car2 = test2();
          // 相当于car2 = car; 
          return 0;
      }
      

      结果:

      在这里插入图片描述

      产生三个新的对象,为什么会有最后一次调用拷贝构造呢?

      因为在调用test2之后,test2在栈空间的内存会被释放掉,说明生成的car对象也会被释放掉,但现实中并不会这样,会把在test2空间生成的car对象拷贝到main空间中(因为是在main中调用的)

      所以会利用test2中生成的car对象拷贝构造出来一个新的car对象,这个car对象是在main函数的空间中。

      就像是

      car2 = Car(test2());
      

      所以会产生一些不必要的中间对象。

匿名对象(临时对象)

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

比如

int main() {
    Car();   
    return 0;
}

Car()生成匿名对象,没有名字,但还是会创造新的对象。

构造完就析构掉

隐式构造

C++中存在隐式构造的现象:某些情况下,会隐式调用单参数的构造函数。

class Person {	
    int m_age;
public:
    Person() {
        cout << "Person() - " << this << endl;
    }	
    Person(int age) :m_age(age) {	
        cout << "Person(int) - " << this << endl;	
    }	
    Person(const Person &person) {	
        cout << "Person(const Person &person) - " << this << endl;	
    }	
    ~Person() {
        cout << "~Person() - " << this << endl;
    }
    void display() {
        cout << "display() - age is " << m_age << endl;	
    }
};
int main() {
    /*
    Person p1;
    Person p2(10);	
    Person p3 = p2;
    */	
    Person p1 = 20;  
    getchar();	
    return 0;
}

这里Person p1 = 20;是在干什么?

运行发现调用的单参数的构造函数。等价于Person p2(20)

如果再加一个函数,然后再main函数中调用。

void test1(Person person) {
    
}
int main() {
    /*
    Person p1;	
    Person p2(10);
    Person p3 = p2;
    */
    // Person p1 = 20;  
    test1(30);    
    getchar();
    return 0;
}

这里test1(30);相当于调用了test1(Person person = 30),和前面一样,也会调用单参数的构造函数。

再如果加一个函数,返回值是Person对象

Person test2() {  
    return 40;
}
int main() {  
    test2();    
    getchar();	
    return 0;
}

会将40传递给Person对象的单参数构造函数来构造Person对象。

这样就叫做隐式构造

如果是

int main() {    
    Person p1;
    p1 = 40;	
    getchar();	
    return 0;
}

会调用一次无参构造、一次单参数构造和一次析构函数

无参构造:Person p1;

单参数构造:因为如果要p1 = 40;左边是person对象,右边也得是person对象,所以右边调用单参数构造函数,但是隐式构造,将这个构造函数传递给p1后马上析构。

  • 可以通过关键字explicit禁止掉隐式构造

    explicit Person(int age) :m_age(age) {		
        cout << "Person(int) - " << this << endl;	
    }
    

    不允许隐式构造

举报

相关推荐

0 条评论