一、简介
在C++中,拷贝构造函数是一种特殊的构造函数,用于创建一个新对象,并将其初始化为现有对象的副本。拷贝构造函数的函数名必须与类名相同,并且参数为该类的一个常引用。
拷贝构造函数是一种特殊的构造函数,用于创建对象的副本。其原型如下:
class MyClass{
public:
//拷贝构造函数
MyClass(count MyClass& other);{
//执行拷贝构造函数的操作
}
};
他接受一个同类型的对象(本类对象的引用)作为参数,并使用该对象的数据来初始化新对象。拷贝构造函数通常用于在程序中复制对象,以便在不修改原始对象的情况下对其进行操作。
拷贝构造函数通常会执行深拷贝操作,即复制指针指向的数据,而不是仅仅复制指针本身。这是因为如果只是浅拷贝,多个对象将共享同一块内存,可能会导致一些不可预测的错误。
如果类中包含指针类型的成员变量,就需要自己定义拷贝构造函数,以确保正确地复制指针指向的数据。例如,下面的示例代码中,类MyClass包含一个指向char类型的指针:
class MyClass {
private:
char* data;
public:
// 构造函数
MyClass(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
// 拷贝构造函数
MyClass(const MyClass& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
};
在上面的示例代码中,拷贝构造函数中使用new运算符为新对象分配一块内存,并将现有对象的数据复制到该内存中。这样,就可以确保新对象与现有对象完全独立,不会共享同一块内存。
需要注意的是,如果类中包含了其他的指针类型成员变量或者引用类型成员变量,就需要在拷贝构造函数中进行递归调用,以确保所有的数据都被正确地复制。
用一个对象创建一个新的对象,拷贝构造函数是把对象当作参数传入,利用传入的对象生成一个新的对象,而赋值运算符是将对象的值赋值给一个已经存在的实例。
注:
调用的是拷贝构造函数还是赋值运算符,主要看是否有新的对象产生。
函数参数要用引用类型,否则会递归调用拷贝构造,导致陷入死循环。
二、拷贝构造函数通常在以下情况被调用
-
用已经存在的对象初始化一个新的对象。例如:当一个对象被声明时,其拷贝构造函数被调用,以将已有对象的副本用于初始化新的对象
-
以值传递方式传递对象。当一个对象被作为参数传递给函数时,其拷贝构造函数被调用以创建该对象的副本并将其传递给函数
-
在返回对象时,当一个函数返回一个对象时,其拷贝构造函数被调用以创建一个新对象并 将其初始化为该函数返回的对象副本
三、拷贝构造函数的特点
-
实质上拷贝构造函数也是一种构造函数,拷贝构造函数的函数名必须与类名相同,并且参数为该类的一个常引用
-
拷贝构造函数用于创建一个新的对象,该对象的内容与现有对象相同
-
默认情况下,编译器会自动生成一个默认的拷贝构造函数,该函数执行浅拷贝操作,即直接复制对象的成员变量的值,而不是创建新的对象并复制其数据。如果需要执行深拷贝,即复制指针指向的数据,就需要自己定义拷贝构造函数
-
拷贝构造函数的定义可以通过复制构造函数的方式来实现,也可以使用赋值运算符重载函数来实现。但是,拷贝构造函数和赋值运算符的实现方式不同,赋值运算符重载函数用于已经存在的对象之间的赋值,而拷贝构造函数用于创建新的对象
-
拷贝构造函数的调用时机是在对象创建时或者对象的复制操作中,因此它的执行频率相对较高,需要注意其实现方式的效率和正确性
需要注意的是,为了确保代码的正确性,拷贝构造函数的实现需要考虑到以下几点:
- 对于类中包含指针类型的成员变量,需要进行深拷贝,以确保复制的数据是独立的,而不是共享同一块内存
-
拷贝构造函数的参数通常是一个常引用,这是为了避免不必要的复制操作,提高效率
-
如果一个类没有显式定义拷贝构造函数,编译器会自动合成一个默认的拷贝构造函数。这个默认的拷贝构造函数会执行浅拷贝操作,即仅仅复制对象的成员变量的值。如果类中包含指针类型的成员变量,这种默认的拷贝构造函数可能会导致不可预测的错误,因此需要自己定义拷贝构造函数
-
如果一个类中包含了其他的指针类型成员变量或者引用类型成员变量,就需要在拷贝构造函数中进行递归调用,以确保所有的数据都被正确地复制
代码示例:
#include<iostream>
using namespace std;
#include<string>//C++中 string是类,里面包含了方法 和 属性
#include<vector>
class A {
public:
int a;
char c;
A() {
cout << 1 << " ";
}
A(int a) {
this->a = a;
cout << "调用了构造函数" << endl;
//cout << a << " ";
}
/*
当对象以值传递的方式作为函数参数时,会调用拷贝构造函数。
当对象以引用的方式作为函数参数时,因为和实参共用一个内存,
不会产生新的对象,所以不会再次调用拷贝构造函数。
如果拷贝构造函数的参数是值传递的话,
在初始化拷贝构造函数的参数时就会用实参去初始化一新的对象(形参), A
other = 实参
进而会继续调用拷贝构造函数,继续初始化形参,因此会形成无限递归。递归就是函数自己调用自己
*/
//拷贝构造函数
A(const A &other) {//const 是为了防止在函数体里面修改外部对象other,& 是防止陷入递归,使之无限循环
//如果参数这里不使用引用传参,那么会一直调用拷贝构造
this->a = other.a;
this->c = other.c;
cout << "调用拷贝构造" << endl;
}
~A() {
cout << "调用了析构函数" << endl;
}
};
void fun(A& a) {}//参数为引用的时候 不会再次调用拷贝构造函数,因为此时形参和实参公用同一个地址,没有新的对象产生,所以不会再次调用拷贝构造
void fun1(A a) {}//在对象以值传递形式传参的时候 会调用拷贝构造
A f() {
A a(1);//a作为局部变量存放在栈区,函数结束会释放掉函数栈,所以a也会被释放掉。所以在下一行会调用拷贝构造函数 拷贝出来一份作为返回值
return a;//调用拷贝构造 到此空间被销毁
//a在栈区(结束就销毁) 执行后被销毁
}
int main() {
//A();//输出为 1
创建一个匿名对象 仅在当前行有效,执行结束销毁
//A* p2 = new A[3]{ A(1),A(2),A(3) };//输出1 2 3
//A a1, a2;
//a1 = a2;//赋值是一个已经存在的对象被另一个存在的对象赋值
拷贝是创建一个新的对象
//A a2(a1);//调用拷贝构造创建了a2
//A a3 = a2;//调用拷贝构造创建了a3
//A a4;
//a4 = a3;//调用赋值运算符,因为a4已经存在
//A a1;//执行完此a消失
//此时编译器会在外部建立一个拷贝,将a拷贝出来
//以值作为返回值时要调用拷贝构造
//如果没有拷贝函数 编译器会实现一个默认拷贝构造
//A a = f(a1);//赋值在函数结束时运行
A a(1);
A b(a);//用一个对象初始化,另一个对象会调用拷贝构造函数,如果没有实现拷贝构造函数,编译器会提供一个默认的拷贝构造函数。
cout << b.a << endl;
cout << "------------" << endl;
A c(2);
cout << c.a << endl;
c = a;//赋值 给一个已经存在的对象赋值,不会调用拷贝构造函数
cout << "------------" << endl;
fun1(c);
cout << "------------" << endl;
f();
cout << "------------" << endl;
return 0;
}
-----------------------------------------------------------------------------------------
输出:
调用了构造函数
调用拷贝构造
1
--------------
调用了构造函数
2
--------------
调用拷贝构造
调用了析构函数
--------------
调用了构造函数
调用拷贝构造
调用了析构函数
调用了析构函数
--------------
调用了析构函数
调用了析构函数
调用了析构函数
类名::类名(const 类名 &对象名)
{
拷贝构造函数的函数体;
}
class Score{
public:
Score(int m, int f); //构造函数
Score();
Score(const Score &p); //拷贝构造函数
~Score(); //析构函数
void setScore(int m, int f);
void showScore();
private:
int mid_exam;
int fin_exam;
};
Score::Score(int m, int f)
{
mid_exam = m;
fin_exam = f;
}
Score::Score(const Score &p)
{
mid_exam = p.mid_exam;
fin_exam = p.fin_exam;
}
调用拷贝构造函数的一般形式为:
类名 对象2(对象1);
类名 对象2 = 对象1;
Score sc1(98, 87);
Score sc2(sc1); //调用拷贝构造函数
Score sc3 = sc2; //调用拷贝构造函数
四、深拷贝和浅拷贝
在 C++ 中,对象的拷贝可以分为深拷贝和浅拷贝两种类型。
- 浅拷贝是指将一个对象的数据成员的值直接复制到另一个对象中,这样两个对象的数据成员指向同一块内存地址。这种拷贝方式会导致两个对象共享同一块内存,当其中一个对象被修改时,另一个对象也会受到影响。在 C++ 中,如果一个类没有显式地定义拷贝构造函数和赋值运算符重载函数,那么编译器会默认生成一个浅拷贝的拷贝构造函数和赋值运算符重载函数。
-
深拷贝是指将一个对象的数据成员的值复制到另一个对象中,并且为另一个对象分配一块新的内存空间来存储数据成员的值。这样两个对象的数据成员指向不同的内存地址,它们之间互相独立,互不影响。在 C++ 中,如果一个类需要进行深拷贝操作,通常需要显式地定义拷贝构造函数和赋值运算符重载函数,以确保对象的拷贝行为正确。
下面是一个用于演示深拷贝和浅拷贝的示例程序:
#include <iostream>
class ShallowCopy {
public:
ShallowCopy(int size) {
m_data = new int[size];
m_size = size;
}
// 浅拷贝构造函数
ShallowCopy(const ShallowCopy& other) {
m_data = other.m_data;
m_size = other.m_size;
}
// 浅拷贝赋值运算符重载函数
ShallowCopy& operator=(const ShallowCopy& other) {
if (this != &other) {
m_data = other.m_data;
m_size = other.m_size;
}
return *this;
}
~ShallowCopy() {
delete[] m_data;
}
void setData(int index, int value) {
m_data[index] = value;
}
void printData() {
for (int i = 0; i < m_size; ++i) {
std::cout << m_data[i] << " ";
}
std::cout << std::endl;
private:
int* m_data;
int m_size;
};
int main() {
ShallowCopy obj1(5);
obj1.setData(0, 1);
obj1.setData(1, 2);
obj1.setData(2, 3);
obj1.setData(3, 4);
obj1.setData(4, 5);
ShallowCopy obj2 = obj1; // 调用浅拷贝构造函数
obj1.setData(0, 10);
obj1.printData(); // 输出 "10 2 3 4 5"
obj2.printData(); // 输出 "10 2 3 4 5"
}
这段代码定义了一个名为ShallowCopy的类,它具有一个构造函数和一个析构函数,以及一个setData函数和一个printData函数。此外,它还有一个浅拷贝构造函数和一个浅拷贝赋值运算符重载函数。浅拷贝构造函数和浅拷贝赋值运算符重载函数都将m_data和m_size成员变量从一个对象复制到另一个对象,但它们只是复制指针,而不是复制指针所指向的数据。因此,当一个对象的数据发生变化时,另一个对象的数据也会发生变化。在main函数中,创建了一个ShallowCopy对象obj1,并将其数据设置为1、2、3、4和5。然后,创建了一个ShallowCopy对象obj2,并将其初始化为obj1。由于obj2是通过浅拷贝构造函数创建的,因此它与obj1共享相同的数据。然后,将obj1的第一个数据元素设置为10。最后,分别调用obj1和obj2的printData函数,以显示它们的数据。由于obj2与obj1共享相同的数据,因此它们的输出都是"10 2 3 4 5"。setData函数的作用是将指定索引处的数据成员设置为指定的值。要使用赋值运算符将obj1的值复制到obj2中,可以直接使用赋值运算符将obj1赋值给obj2。在这个例子中,没有执行深拷贝。浅拷贝的作用是在创建一个新对象时,将一个现有对象的数据成员复制到新对象中,但只是复制指针,而不是复制指针所指向的数据。这可以节省内存,但也可能导致数据共享和潜在的错误。如果需要执行深拷贝,需要在浅拷贝构造函数和浅拷贝赋值运算符重载函数中分配新的内存,并将数据复制到新的内存中。
下面是一个使用深拷贝的示例程序:
#include <iostream>
class DeepCopy {
public:
DeepCopy(int size) {
m_data = new int[size];
m_size = size;
}
// 深拷贝构造函数
DeepCopy(const DeepCopy& other) {
m_data = new int[other.m_size];
m_size = other.m_size;
for (int i = 0; i < m_size; ++i) {
m_data[i] = other.m_data[i];
}
}
// 深拷贝赋值运算符重载函数
DeepCopy& operator=(const DeepCopy& other) {
if (this != &other) {
delete[] m_data;
m_data = new int[other.m_size];
m_size = other.m_size;
for (int i = 0; i <m_size; ++i) {
m_data[i] = other.m_data[i];
}
}
return *this;
}
~DeepCopy() {
delete[] m_data;
}
void setData(int index, int value) {
m_data[index] = value;
}
void printData() {
for (int i = 0; i < m_size; ++i) {
std::cout << m_data[i] << " ";
}
std::cout << std::endl;
}
private:
int* m_data;
int m_size;
};
int main() {
DeepCopy obj1(5);
obj1.setData(0, 1);
obj1.setData(1, 2);
obj1.setData(2, 3);
obj1.setData(3, 4);
obj1.setData(4, 5);
DeepCopy obj2 = obj1; // 调用深拷贝构造函数
obj1.setData(0, 10);
obj1.printData(); // 输出 "10 2 3 4 5"
obj2.printData(); // 输出 "1 2 3 4 5"
return 0;
}
在这个示例程序中,我们定义了一个名为 DeepCopy 的类,它包含一个指向整型数组的指针 m_data 和一个整型变量 m_size。该类还定义了一个深拷贝构造函数和一个深拷贝赋值运算符重载函数,它们都会为新对象分配一块新的内存空间,并将原对象的数据复制到新的内存空间中。
五、深拷贝和浅拷贝分别在什么时候使用
在实际编程中,我们需要根据具体情况来选择使用浅拷贝还是深拷贝:
- 对于简单对象,通常可以使用浅拷贝。例如: 如果一个类只包含基本类型的成员变量(例如:整型、字符型等),那么默认的浅拷贝就足够了。
-
对于含有指针类型的成员变量的复杂对象,通常需要使用深拷贝。例如:如果一个类中包含指针类型的变量,那么拷贝对象时,应该使用深拷贝,以确保每个对象都用有自己的独立内存。
-
在某些情况下,我们需要手动实现拷贝构造函数和赋值操作符重载函数,以确保正确的靠欸。例如:如果一个类中包含资源(例如:文件句柄、网络连接等)或者使用了内存分配(例如:使用 new 或 malloc 分配内存),那么在拷贝对象时,应该用深拷贝,并且手动实现拷贝构造函数和赋值运算符重载函数,以确保正确的内存管理和资源释放。
- 编译器默认浅拷贝
-
如果对象中含有指针,却使用了浅拷贝,那么会导致两个指针变量指向同一块地址,那么在对象释放时便会产生一块空间释放两次的情况,产生野指针。
-
浅拷贝和深拷贝的区别在于两个指针变量指向的是一块空间还是指向不同的空间。如果没有创建内存的操作就是浅拷贝,否则就是深拷贝
#include<iostream>
using namespace std;
class A {
public:
int* p = nullptr;//如果不赋值为nullptr的话,那么会产生野指针,在析构函数释放的时候就会报错
int size = 0;
//默认构造函数
A() {
cout << "A构造" << endl;
}
//带参数的构造函数:创建一个指向堆区的内存的整型指针p和一个整型变量size,并为p分配size个整型空间
A(int size) {
this->size = size;
p = new int[size];//指向堆区内存
}
//拷贝构造:在拷贝构造函数中,首先进行了浅拷贝,即将当前对象的指针p指向other对象的指针p所指向的内存空间。但是这样会导致两个对象的指针p指向同一块内存空间,当其中一个对象被析构时,另一个对象的指针p就会变成野指针,因此需要进行深拷贝。在深拷贝中,首先开辟了一个大小与other对象相同的堆空间,然后将当前对象的指针p指向这个堆空间,并将堆空间的值与other对象的堆空间的值相同。
A(const A& other) {
//浅拷贝
this->p = other.p;
//深拷贝
this->p = new int[other.size];//开辟相同大小的堆空间
*p = *(other.p);//让堆区空间的值相同
}
//因此,在拷贝构造时,既进行了浅拷贝又进行了深拷贝,以保证两个对象的指针p指向不同的内存空间,避免了野指针的问题
~A() {
if (p) {
delete []p;
p = nullptr;//防止野指针
}
cout << "A析构" << endl;
}
};
int main() {
A a(3);
A b = a;//调用的拷贝构造函数
//两个对象两个析构
return 0;
}
-----------------------------------------------------------------------------------------
//输出:
//A析构
//A析构