在C++编程中,拷贝构造函数是对象复制的核心机制,尤其是在处理对象间的值传递时。当一个对象通过拷贝另一个对象来初始化时,浅拷贝只是简单地复制对象的成员变量的值。如果对象包含指针成员,浅拷贝只复制指针地址,这可能会导致两个对象共享同一块内存资源,容易引发内存泄漏或悬挂指针的问题。深拷贝则会在拷贝过程中为指针成员分配新的内存,并复制实际的数据,确保每个对象都有独立的资源,避免了上述问题。理解并正确实现这两种拷贝方式,不仅可以避免常见的资源管理问题,还能提升代码的健壮性和稳定性。接下来,我们将深入探讨C++中拷贝构造函数的深浅拷贝问题及实现MyString。
一、引入浅拷贝问题
自己实现一个字符串的类,并且使用系统编译器默认生成的拷贝构造函数,代码如下:
#include<iostream>
#include<string.h>
using namespace std;
class MyString
{
private:
char* ptr;
public:
MyString(const char* str = nullptr)
{
if (str != nullptr)
{
int len = strlen(str); //字符串实际长度
ptr = new char[len + 1]; //申请堆区内存空间要比实际字符串长度多一个,因为要存储'\0'
strcpy(ptr, str);
}
else
{
ptr = new char[1];
*ptr = '\0';
}
}
~MyString()
{
if (ptr != nullptr)
{
delete[] ptr;
}
ptr = nullptr;
}
void PrintString() const
{
if (ptr != nullptr)
{
cout << ptr << endl;
}
}
};
int main()
{
MyString s1("wanghenghello");
s1.PrintString();
MyString s2(s1);
s2.PrintString();
return 0;
}
分析上述代码存在的问题?
如果String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,或者用s1给s2赋值时,编译器会调用默认的拷贝构造或者赋值运算符重载。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
则会导致以下问题:
- 双重释放:如果两个对象共享同一块内存,当其中一个对象被销毁时,它会释放这块内存。然后,另一个对象在销毁时也会尝试释放这块内存,导致程序崩溃或未定义行为。
- 数据损坏:两个对象共享同一块内存时,对其中一个对象的修改可能会影响另一个对象的数据,导致数据不一致或错误。
如何解决这个问题呢?
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享,深拷贝:给每个对象独立分配资源,保证多个对象之间不会因共享资源而造成多次释放造成程序奔溃问题!
二、自己实现深拷贝
#include<iostream>
#include<string.h>
using namespace std;
class MyString
{
private:
char* ptr;
public:
MyString(const char* str = nullptr)
{
if (str != nullptr)
{
int len = strlen(str); //字符串实际长度
ptr = new char[len + 1]; //申请堆区内存空间要比实际字符串长度多一个,因为要存储'\0'
strcpy(ptr, str);
}
else
{
ptr = new char[1];
*ptr = '\0';
}
}
~MyString()
{
if (ptr != nullptr) //必须要判空,空字符指针不可以求字符串长度!
{
delete[] ptr;
}
ptr = nullptr;
}
void PrintString() const
{
if (ptr != nullptr)
{
cout << ptr << endl;
}
}
//实现深拷贝的拷贝构造函数
MyString(const MyString& it)
{
if (it.ptr != nullptr) //必须要判空,空字符指针不可以求字符串长度!
{
int len = strlen(it.ptr);
this->ptr = new char[len + 1];
strcpy(this->ptr, it.ptr);
}
else
{
this->ptr = nullptr;
}
}
//实现深拷贝的赋值运算符重载operator=()
/***********************************************************关于函数的返回值设计是传值返回还是传引用返回,该如何选取?*************************************************
*
1、传值返回,会构建临时的将亡值对象,调用拷贝构造函数;
2、传引用返回,不会构建临时的将亡值对象,直接返回对象本身;
3、但是需要注意一点:传引用返回,必须是该对象的生命周期不会受到函数调用的影响,才可以以引用返回;
4、总结:实际开发中,如果该对象对象的生命周期不会受到函数调用的影响(比如全局对象、静态对象、堆区数据的定义、主函数定义的对象),传值返回和传引用返回都可以,但是为了减少拷贝构造提高效率,通常采用传引用返回!
**************************************************************************************************************************************/
//s2=s1;
//s2.operator=(s1);
//operator=(&s2,s1);
MyString& operator=(const MyString& it)
{
if (this != &it) //自我复制没有意义,提前判断。
{
if (this->ptr != nullptr) //s2对象本身与一块内存绑定,为防止后续指针赋值,原来的内存出现泄露,提前判断,并且释放原内存,将指针置空。
{
delete [] this-> ptr;
this->ptr = nullptr;
}
if (it.ptr != nullptr)
{
int len = strlen(it.ptr);
ptr = new char[len + 1];
strcpy(ptr, it.ptr);
}
}
return *this; //支持连续赋值
}
};
int main()
{
MyString s1("wanghenghello");
s1.PrintString();
MyString s2(s1);
s2.PrintString();
MyString s3("111");
MyString s4("222");
s3.PrintString();
s4.PrintString();
s4=s3;
s3.PrintString();
s4.PrintString();
return 0;
}
关于函数的返回值设计是传值返回还是传引用返回,该如何选取?
- 传值返回,会构建临时的将亡值对象,调用拷贝构造函数;
- 传引用返回,不会构建临时的将亡值对象,直接返回对象本身;
- 但是需要注意一点:传引用返回,必须是该对象的生命周期不会受到函数调用的影响,才可以以引用返回;
总结:实际开发中,如果该对象对象的生命周期不会受到函数调用的影响(比如全局对象、静态对象、堆区数据的定义、主函数定义的对象),传值返回和传引用返回都可以,但是为了减少拷贝构造提高效率,通常采用传引用返回!赋值运算符重载这里必须以引用返回,return *this; 因为这样它可以支持连续赋值!
核心代码如下:
MyString& operator=(const MyString& it)
{
if (this != &it) //自我复制没有意义,提前判断。
{
if (this->ptr != nullptr) //s2对象本身与一块内存绑定,为防止后续指针赋值,原来的内存出现泄露,提前判断,并且释放原内存,将指针置空。
{
delete [] this-> ptr;
this->ptr = nullptr;
}
if (it.ptr != nullptr)
{
int len = strlen(it.ptr);
ptr = new char[len + 1];
strcpy(ptr, it.ptr);
}
}
return *this; //支持连续赋值
}
三、移动构造和移动赋值
3.1 引入
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
class MyString
{
private:
char* ptr;
public:
MyString(const char* str = nullptr)
{
if (str != nullptr)
{
int len = strlen(str); //字符串实际长度
ptr = new char[len + 1]; //申请堆区内存空间要比实际字符串长度多一个,因为要存储'\0'
strcpy(ptr, str);
}
else
{
ptr = new char[1];
*ptr = '\0';
}
cout << "Create MyString : " << this << endl;
}
~MyString()
{
if (ptr != nullptr)
{
delete[] ptr;
}
ptr = nullptr;
cout << "~MyString : " << this << endl;
}
void PrintString() const
{
if (ptr != nullptr)
{
printf("%p => %s \n", ptr, ptr);
}
}
//实现深拷贝的拷贝构造函数
MyString(const MyString& it)
{
if (it.ptr != nullptr)
{
int len = strlen(it.ptr);
this->ptr = new char[len + 1];
strcpy(this->ptr, it.ptr);
}
else
{
this->ptr = nullptr;
}
cout <<this<< "Copy MyString : " << &it << endl;
}
MyString& operator=(const MyString& it)
{
if (this != &it)
{
if (this->ptr != nullptr)
{
delete [] this-> ptr;
this->ptr = nullptr;
}
if (it.ptr != nullptr)
{
int len = strlen(it.ptr);
ptr = new char[len + 1];
strcpy(ptr, it.ptr);
}
}
cout << this << "operator=() " << &it << endl;
return *this; //支持连续赋值
}
};
MyString func(const char*p)
{
MyString tmp(p);
tmp.PrintString();
return tmp;
}
int main()
{
MyString s1("newdata");
s1.PrintString();
s1 = func("hello"); //将亡值对象为s1对象赋值
s1.PrintString();
return 0;
}
存在问题:在堆区反复构建对象,尤其是构建将亡值对象和深赋值运算符重载函数的时候都需要构建新的对象!如何减少呢?先学习下面的内容,最后解决。
3.2 移动构造
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
class MyString
{
private:
char* ptr;
public:
MyString(const char* str = nullptr)
{
if (str != nullptr)
{
int len = strlen(str); //字符串实际长度
ptr = new char[len + 1]; //申请堆区内存空间要比实际字符串长度多一个,因为要存储'\0'
strcpy(ptr, str);
}
else
{
ptr = new char[1];
*ptr = '\0';
}
cout << "Create MyString : " << this << endl;
}
~MyString()
{
if (ptr != nullptr)
{
delete[] ptr;
}
ptr = nullptr;
cout << "~MyString : " << this << endl;
}
void PrintString() const
{
if (ptr != nullptr)
{
printf("%p => %s \n", ptr, ptr);
}
}
//实现深拷贝的拷贝构造函数
MyString(const MyString& it)
{
if (it.ptr != nullptr)
{
int len = strlen(it.ptr);
this->ptr = new char[len + 1];
strcpy(this->ptr, it.ptr);
}
else
{
this->ptr = nullptr;
}
cout <<this<< "Copy MyString : " << &it << endl;
}
//实现移动构造函数
MyString(MyString&& it):ptr(it.ptr)
{
it.ptr = nullptr;
cout << this << "Move Copy MyString : " << &it << endl;
}
MyString& operator=(const MyString& it)
{
if (this != &it)
{
if (this->ptr != nullptr)
{
delete [] this-> ptr;
this->ptr = nullptr;
}
if (it.ptr != nullptr)
{
int len = strlen(it.ptr);
ptr = new char[len + 1];
strcpy(ptr, it.ptr);
}
}
cout << this << "operator=() " << &it << endl;
return *this; //支持连续赋值
}
};
int main()
{
MyString s1("newdata");
MyString s2(std::move(s1)); //C++11提供的移动拷贝构造函数
s1.PrintString();
s2.PrintString();
return 0;
}
核心代码如下:
//实现移动构造函数
MyString(MyString&& it):ptr(it.ptr)
{
it.ptr = nullptr;
cout << this << "Move Copy MyString : " << &it << endl;
}
3.3 移动赋值运算符重载
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
class MyString
{
private:
char* ptr;
public:
MyString(const char* str = nullptr)
{
if (str != nullptr)
{
int len = strlen(str); //字符串实际长度
ptr = new char[len + 1]; //申请堆区内存空间要比实际字符串长度多一个,因为要存储'\0'
strcpy(ptr, str);
}
else
{
ptr = new char[1];
*ptr = '\0';
}
cout << "Create MyString : " << this << endl;
}
~MyString()
{
if (ptr != nullptr)
{
delete[] ptr;
}
ptr = nullptr;
cout << "~MyString : " << this << endl;
}
void PrintString() const
{
if (ptr != nullptr)
{
printf("%p => %s \n", ptr, ptr);
}
}
//实现深拷贝的拷贝构造函数
MyString(const MyString& it)
{
if (it.ptr != nullptr)
{
int len = strlen(it.ptr);
this->ptr = new char[len + 1];
strcpy(this->ptr, it.ptr);
}
else
{
this->ptr = nullptr;
}
cout <<this<< "Copy MyString : " << &it << endl;
}
//实现移动构造函数
MyString(MyString&& it):ptr(it.ptr)
{
it.ptr = nullptr;
cout << this << "Move Copy MyString : " << &it << endl;
}
//实现的赋值运算符重载函数
MyString& operator=(const MyString& it)
{
if (this != &it)
{
if (this->ptr != nullptr)
{
delete [] this-> ptr;
this->ptr = nullptr;
}
if (it.ptr != nullptr)
{
int len = strlen(it.ptr);
ptr = new char[len + 1];
strcpy(ptr, it.ptr);
}
}
cout << this << "operator=() " << &it << endl;
return *this; //支持连续赋值
}
//实现的移动赋值运算符重载函数
MyString& operator=( MyString &&it)
{
if (this != &it)
{
if (this->ptr != nullptr)
{
delete[] this->ptr;
this->ptr = nullptr;
}
if (it.ptr != nullptr)
{
this->ptr = it.ptr;
it.ptr = nullptr;
}
}
cout << this << " operator= move " << &it << endl;
return *this; //支持连续赋值
}
};
int main()
{
MyString s1("newdata");
MyString s2("hello");
s1.PrintString();
s2.PrintString();
s2 = std::move(s1); //C++11提供的移动赋值运算符重载函数
s1.PrintString();
s2.PrintString();
return 0;
}
移动赋值和移动构造函数实现的原理一样,将指针的指向进行赋值,再将原来的对象的指针置为空!它的实现条件也是两个,首先他要是一个右值对象(通过move函数实现),然后,我们需要实现移动赋值运算符重载函数!
核心代码如下:
//实现的移动赋值运算符重载函数
MyString& operator=( MyString &&it)
{
if (this != &it)
{
if (this->ptr != nullptr)
{
delete[] this->ptr;
this->ptr = nullptr;
}
if (it.ptr != nullptr)
{
this->ptr = it.ptr;
it.ptr = nullptr;
}
}
cout << this << " operator= move " << &it << endl;
return *this; //支持连续赋值
}
3.4 解决问题
到现在,我们来解决前面留下的问题,加入移动构造和移动赋值即可!
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
class MyString
{
private:
char* ptr;
public:
MyString(const char* str = nullptr)
{
if (str != nullptr)
{
int len = strlen(str); //字符串实际长度
ptr = new char[len + 1]; //申请堆区内存空间要比实际字符串长度多一个,因为要存储'\0'
strcpy(ptr, str);
}
else
{
ptr = new char[1];
*ptr = '\0';
}
cout << " Create MyString : " << this << endl;
}
~MyString()
{
if (ptr != nullptr)
{
delete[] ptr;
}
ptr = nullptr;
cout << " ~MyString : " << this << endl;
}
void PrintString() const
{
if (ptr != nullptr)
{
printf("%p => %s \n", ptr, ptr);
}
}
//实现深拷贝的拷贝构造函数
MyString(const MyString& it)
{
if (it.ptr != nullptr)
{
int len = strlen(it.ptr);
this->ptr = new char[len + 1];
strcpy(this->ptr, it.ptr);
}
else
{
this->ptr = nullptr;
}
cout << this<< " Copy MyString : " << &it << endl;
}
//实现移动构造函数
MyString(MyString&& it):ptr(it.ptr)
{
it.ptr = nullptr;
cout << this << " Move Copy MyString : " << &it << endl;
}
//实现的赋值运算符重载函数
MyString& operator=(const MyString& it)
{
if (this != &it)
{
if (this->ptr != nullptr)
{
delete [] this-> ptr;
this->ptr = nullptr;
}
if (it.ptr != nullptr)
{
int len = strlen(it.ptr);
ptr = new char[len + 1];
strcpy(ptr, it.ptr);
}
}
cout << this << "operator=() " << &it << endl;
return *this; //支持连续赋值
}
//实现的移动赋值运算符重载函数
MyString& operator=( MyString &&it)
{
if (this != &it)
{
if (this->ptr != nullptr)
{
delete[] this->ptr;
this->ptr = nullptr;
}
if (it.ptr != nullptr)
{
this->ptr = it.ptr;
it.ptr = nullptr;
}
}
cout << this << " operator= move " << &it << endl;
return *this; //支持连续赋值
}
};
MyString func(const char*p)
{
MyString tmp(p);
tmp.PrintString();
return tmp;
}
int main()
{
MyString s1("newdata");
s1.PrintString();
s1 = func("hello");
s1.PrintString();
return 0;
}
分析:
可以看到,加入移动构造函数和移动赋值运算符重载函数后,用tmp构建不具名的将亡值对象时,调用的是移动构造函数,然后返回给主函数,在进行赋值的时候,由于不具名的将亡值对象是右值对象,因此,它也调用的是移动赋值运算符重载函数!!!这样减少了在堆区重新开辟内存空间的次数!提高了效率!
至此,C++面向对象-中全部内容就学习完毕,这一节内容比较重要,建议多看几遍,认真复习消化,熟练使用,C++相对来说较为复杂,我们应该时刻理清自己的思路,耐下心来,一点点积累, 星光不问赶路人,加油吧,感谢阅读,如果对此专栏感兴趣,点赞加关注!