🔥个人主页:Quitecoder
🔥专栏:c++笔记仓
朋友们大家好啊,本篇内容带大家深入了解拷贝构造函数
目录
- 1.拷贝构造函数
- 1.1传值调用的无限调用
- 1.2浅拷贝
- 1.3深拷贝
- 1.4深拷贝的实现
1.拷贝构造函数
拷贝构造函数是一种特殊的构造函数,在对象需要以同一类的另一个对象为模板进行初始化时被调用。它的主要用途是初始化一个对象,使其成为另一个对象的副本
我们先引用前面所用到的日期类的例子:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
简单来说,假如我现在定义了一个日期对象:
int main()
{
Date d1(2005, 6, 23);
return 0;
}
我需要定义一个d2,与我的d1的数据相同,如何定义呢?
方法如下:
int main()
{
Date d1(2005, 6, 23);
Date d2(d1);
return 0;
}
这里用到了拷贝构造,那么拷贝函数是如何实现的呢?我们接下来来探讨一下
拷贝构造函数通常声明为接受一个对同一类对象的常量引用参数:
class ClassName {
public:
ClassName(const ClassName& other);
};
- 参数:
const ClassName& other
是对另一个同类型对象的引用,使用const确保不会无意中修改other。 - 函数体:在函数体内部,你可以决定如何复制other对象的成员到新对象中。对于简单的情况,这可能仅仅是复制每个成员变量的值。对于涉及动态分配内存或其他资源的类,可能需要进行深拷贝
下面来探讨上述的Date类的实现
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2005, 6, 23);
Date d2(d1);
return 0;
}
拷贝构造函数是构造函数的一个重载形式,拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用,这个我们后面进行讲解
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
这里的d2就相当于this,d1就是另一个参数
1.1传值调用的无限调用
我们上面提到,拷贝构造函数参数只有一个且必须是类类型对象的引用,那么如果我使用传值调用会有什么结果呢??
我们下面先来进行简单的铺垫
void fun1(Date d)
{
}
void fun2(Date& rd)
{
}
int main()
{
Date d1(2005, 6, 23);
fun1(d1);
fun2(d1);
return 0;
}
构造两个函数,他们的参数不同,第一个函数为传值传参,在c语言中我们知道,传值传参是一个拷贝的过程,即把d1的值拷贝给d,c++规定,自定义类型的拷贝,都会调用拷贝构造
我们进行调试
在这里按F11,我们目的是进入fun1,函数,这里却跳入拷贝构造函数
再按f11,才会进入fun1函数中
大概过程如下
传值传参需要调用拷贝构造
fun2函数可以直接进入
在上述讲解后,我们来探讨,如果拷贝函数是传值引用,会发生什么?
调用拷贝构造,需要传参,这里传值传参,就会调用一个新的拷贝构造
所以,这里也是我们为什么只能用引用传参
1.2浅拷贝
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2005, 6, 23);
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
我们现在屏蔽掉拷贝构造,看会发生什么
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象(内置类型成员)按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
那如果有自定义类型呢?
我们看下面的代码:
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 2024;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
在这个代码示例中,我们有两个类:Time
和 Date
。Date
类中包含了一些基本类型的成员变量(_year
, _month
, _day
)和一个自定义类型的成员变量(_t
,一个 Time
类型的对象)。当创建 Date
类的对象时,不仅会初始化其基本类型的成员变量,也会调用其自定义类型成员的构造函数来初始化
函数的调用过程
-
Date
对象的默认构造函数调用:当Date
类的对象被创建时,它的默认构造函数(编译器自动生成的,因为没有显式定义)会被调用。由于成员变量_year
,_month
,_day
在类定义中已经被直接初始化,编译器将这些初始化纳入默认构造函数的操作中。 -
Time
成员的构造函数调用:在Date
的构造函数执行过程中,会自动调用_t
(Time
类型的成员变量)的默认构造函数来初始化_t
。Time
的默认构造函数设置_hour
,_minute
,_second
为 1,并不打印任何信息。 -
拷贝
Date
对象:当Date d2(d1);
执行时,d2
是通过拷贝构造函数初始化的。因为Date
类没有显式定义拷贝构造函数,编译器会为它生成一个默认的拷贝构造函数。这个默认的拷贝构造函数会逐个拷贝Date
类中的所有成员变量,包括基本类型和自定义类型的成员。- 对于基本类型成员(如
_year
,_month
,_day
),直接进行值的复制 - 对于自定义类型的成员(
_t
),会调用该成员的拷贝构造函数(Time
类中定义的Time(const Time&)
)来进行拷贝。在这个过程中,Time
的拷贝构造函数会输出信息:Time::Time(const Time&)
- 对于基本类型成员(如
因此,在执行 Date d2(d1);
时,调用过程如下:
- 首先,调用
Date
的默认拷贝构造函数(自动生成)来初始化d2
。 - 在初始化
d2
的过程中,对于其自定义类型成员_t
,调用Time
的拷贝构造函数来初始化,此时会输出Time::Time(const Time&)
。
这就是自定义类型成员在 Date
类拷贝过程中构造函数的调用情况,其他的基本类型成员变量则是通过简单的值复制来初始化的
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的
如果我们删掉Time的默认的拷贝构造函数呢?
class Time
{
public:
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 2024;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
拷贝构造本身就是一种构造函数,所以编译器不会生成默认构造函数
在这个代码中,由于
Time
类中没有显式定义一个无参数的默认构造函数(只定义了一个拷贝构造函数),而Date
类的实现依赖于Time
类的这个默认构造函数来初始化其_t
成员,所以编译器将尝试调用Time
类的默认构造函数时会失败,因为找不到合适的构造函数来初始化_t
当尝试创建
Date
类的实例d1
时,Date
类的默认构造函数(由编译器隐式生成)会被调用。默认构造函数会尝试初始化所有成员变量,对于基本类型的成员变量_year
,_month
,_day
,由于它们已经在类定义中直接初始化,不会有问题。但对于_t
(Time
类型的成员变量),编译器需要调用Time
类的默认构造函数来初始化它。由于Time
类中没有定义无参数的默认构造函数,编译过程中会出现错误
当尝试通过拷贝构造函数创建
d2
时(Date d2(d1);
),同样会遇到问题。虽然Date
类的拷贝构造函数(编译器自动生成的)会尝试逐个拷贝所有成员变量,对于_t
,它会尝试调用Time
类的拷贝构造函数,这部分没有问题。但在创建d1
时已经失败,因此这一步也无法成功执行
c++也可以加入这串代码进行强制生成:
Time() = default;
1.3深拷贝
如果你没有为类显式定义拷贝构造函数,C++编译器会自动生成一个默认的拷贝构造函数。默认拷贝构造函数会逐个复制对象的所有成员(浅拷贝)。对于基本数据类型和指向动态分配内存的指针成员,这意味着只复制指针值而不复制指针指向的数据
我们来看下面的代码:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
我们没有提供拷贝构造函数,编译器默认提供,我们来看运行结果:
程序崩溃,我们进行调试观察
当通过 Stack s2(s1)
; 这样的语句创建一个 Stack 类的对象时,如果没有显式定义拷贝构造函数,C++ 编译器会提供一个默认的拷贝构造函数,它进行浅拷贝。这意味着 _array
指针的值被复制过来,但指向的内存空间没有被复制。这会导致多个对象共享同一块内存空间,进而导致双重释放等问题
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝
- **浅拷贝(Shallow Copy)**只复制对象的顶层结构,如果对象中包含指针指向动态分配的内存,则副本的这些指针将指向与原始对象相同的内存地址。这意味着两个对象共享部分资源。浅拷贝通常是通过默认的拷贝构造函数和赋值操作符实现的
- 深拷贝则复制对象所有的层级结构。对于对象内部的每一个指针指向的内存,深拷贝都会在堆上分配新的内存,然后将原始数据复制到这块新分配的内存中。这样,原始对象和副本对象将拥有完全独立的数据副本
1.4深拷贝的实现
深拷贝需要我们手动实现,对于上述的代码,我们需要手动补充,于对象内部的每个指向动态分配内存的指针,都需要:
- 为副本分配新的内存空间。
- 将原始对象指针指向的数据复制到新分配的内存中:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
//注意,这里s2是this,s是s1
Stack(const Stack& s)
{
DataType* tmp == (DataType*)malloc(sizeof(DataType) * s._capacity);
if (tmp = nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(tmp, s._array, sizeof(DataType) * s._size);
_array = tmp;
_size = s._size;
_capacity = s._capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
//注意,这里s2是this,s是s1
Stack(const Stack& s)
{
DataType* tmp = (DataType*)malloc(sizeof(DataType) * s._capacity);
if (tmp == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(tmp, s._array, sizeof(DataType) * s._size);
_array = tmp;
_size = s._size;
_capacity = s._capacity;
}
这个拷贝构造函数的主要功能是创建一个新的 Stack 对象,该对象是对现有 Stack 对象(称为 s)的深拷贝。深拷贝意味着新对象将拥有与原对象相同的数据副本,但这些数据存储在新分配的内存中。这样,两个对象的状态互不影响,修改一个对象的内容不会影响另一个
-
内存分配:
使用 malloc 根据原栈 (s) 的容量 (_capacity) 分配足够的内存空间来存储数据副本。这里的内存大小是s._capacity * sizeof(DataType)
-
数据复制:
使用 memcpy 将原栈 (s) 的数据 _array 复制到新分配的内存 tmp 中。复制的长度是s._size * sizeof(DataType)
,即仅复制原栈中实际存在的元素 -
更新成员变量:
将新栈的 _array 指针更新为指向新分配的内存 tmp。
将新栈的 _size 和 _capacity 设置为与原栈 (s) 相同的值。这样保证了新栈在逻辑上与原栈完全相同,拥有相同数量的元素和相同的容量
这下我们的问题也就解决了
class myqueue {
private:
Stack st1;
Stack st2;
};
int main()
{
myqueue q1;
myqueue q2(q1);
return 0;
}
有一个 Stack
类,它实现了一个简单的栈,并提供了深拷贝功能。然后,创建一个 myqueue
类,它内部使用了两个 Stack
实例。在 main
函数中,创建了一个 myqueue
对象 q1
并尝试使用 q1
来初始化另一个 myqueue
对象 q2
。这里的关键点在于理解 Stack
的深拷贝实现如何影响 myqueue
对象的复制行为
myqueue
类及其复制行为
myqueue
类内部包含两个Stack
对象:st1
和st2
。当使用一个myqueue
对象来初始化另一个(如myqueue q2(q1);
)时,myqueue
的隐式(或默认)拷贝构造函数被调用。C++ 默认的拷贝构造函数会逐个复制类的成员,使用各成员自己的拷贝构造函数。因此,q1
中的st1
和st2
会使用它们各自的深拷贝构造函数来初始化q2
中的st1
和st2
由于
Stack
类已经提供了深拷贝的实现,myqueue
类中的st1
和st2
成员在myqueue
对象被复制时也会被深拷贝。这意味着q1
和q2
中的st1
和st2
在内存上是独立的:q1.st1
和
q2.st1
指向不同的内存区域,q1.st2
和q2.st2
同理。因此,q1
和q2
在逻辑上是完全独立的队列,它们内部的栈互不影响
- 隐式拷贝构造函数:
myqueue
类在这段代码中并没有显式定义自己的拷贝构造函数。它依赖于 C++ 自动生成的默认拷贝构造函数来正确地复制其成员。这在Stack
提供深拷贝的情况下是安全的
本篇内容到此结束,感谢大家观看!!!!