C++之深浅拷贝

在C++中当
- 1.用已经存在的对象去构造出另一个新的对象。
- 2.当函数的形参是类的对象时。
- 3.当函数的返回值时类的对象时会用到拷贝构造函数。

会用到拷贝函数。

浅拷贝
class Test
{
public:
    //构造函数
    Test(int data)
        :_data(data)
    {}
    //拷贝构造函数
    Test(const Test& c) 
    {
        _a = c._a;
    }

private:
    int _data;
}; 
int main()
{ 
    Test b(10);
    Test c(b);
    return 0; 
}

浅拷贝指的就是当在进行对象的复制的时候,知识对类对象的数据成员的拷贝,其默认的拷贝构造函数也是浅拷贝。

浅拷贝的缺陷:

大多数情况下,浅拷贝时没有问题的。但对于指针成员不可行。多个对象共用同一块空间,同一内存地址,但是在调用析构函数释放空间的时候,多次调用析构函数,这块空间被释放了多次,此时程序就会崩溃。

引用计数的拷贝

1.概念:为了解决多个指针同时指向同一块空间可能导致的内存空间的多次释放这个问题,我们引入了带引用计数的拷贝。(是用来解决浅拷贝的问题的,这也是一种浅拷贝)

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
using namespace std;
class String
{
public:
    //构造函数
    String(const char* ptr = "")
    {
        if (ptr == NULL)
        {
            _ptr = new char[1];
            _pcount = 1;
            *_ptr = '\0';
        }
        else
        {
            _pcount = 1;
            _ptr = new char[strlen(ptr) + 1];
            strcpy(_ptr, ptr);
        }
    }
    //拷贝构造函数
    String(String& s)
        :_ptr(s._ptr)
        , _pcount(s._pcount)
    {
        _pcount++;
    }
    //赋值运算符重载
    String& operator=(const String& s)
    {
        if (this != &s)
        {
            if (--_pcount == 0)
            {
                delete[] _ptr;
                //delete _pcount;
            }
            else
            {
                _ptr = s._ptr;
                _pcount = s._pcount;
                (_pcount)++;
            }
        }
        return *this;
    }
    //析构函数
    ~String()
    {
        if ((0 == --_pcount) && _ptr != NULL)
        {
            delete[]_ptr;
            //delete _pcount;
            _ptr = NULL;
        }

    }
    //重载[]
    char& operator[](size_t size)
    {
        if (--_pcount >1)
        {
            char* ptemp = new char[strlen(_ptr) + 1];
            int pcount = 1;
            strcpy(ptemp, _ptr);
            _pcount--;
            _ptr = ptemp;
            _pcount = pcount;
        }
        return _ptr[size];
    }
private:
    char* _ptr;
    int _pcount;
};

void FunTest()
{
    String s1("hello");
    String s2(s1);
    String s3(s2);
    s3 = s2;
}
int main()
{
    FunTest();
    system("pause");
    return 0;
}

如何来实现这个引用计数呢?

我们为每个内存的字符数组添加一个引用计数pcount,,表示有多少个对象使用这块内存,每多个对象使用,就让pcount值加1,当对象被析构的时候,让pcount值减1,当pcount值为0的时候,将这块内存释放掉。当然pcount也要实现内存共享,所以它也是一个堆中的数据,每个对象都有一个指向它的指针。

这里写图片描述
使用整形来作为引用计数,结果如图所示,在三个对象指向这个空间的时候,引用计数分别为1,2,3,说明引用计数不同步,所以并不能用整形来作为引用计数。

class String  
{  
public:  
    //构造函数  
    String(const char* ptr = "")  
    {  
        if(ptr == NULL)  
        {  
            _ptr = new char[1];  
            _pcount = 1;  
            *_ptr = '\0';  
        }  
        else  
        {  
            _pcount = 1;  
            _ptr = new char[strlen(ptr)+1];  
            strcpy(_ptr,ptr);  
        }  
    }  
    //拷贝构造函数  
    String(String& s)  
        :_ptr(s._ptr)  
    {  
        _pcount++;  //因为是静态的,所以直接进行计数的增值就可以了  
    }  
    //赋值运算符重载  
    String& operator=(const String& s)  
    {  
        if(this != &s)  
        {  
            if(--_pcount == 0)  
            {  
                delete[] _ptr;  
                //delete _pcount;  
            }  
            else  
            {  
                _ptr = s._ptr;  
                _pcount = s._pcount;  
                (_pcount)++;  
            }  
        }  
        return *this;  
    }  
    //析构函数  
    ~String()  
    {  
        if((0 == --_pcount) && _ptr!= NULL)  
        {  
            delete[]_ptr;  
            //delete _pcount;  
            _ptr = NULL;  
        }  

    }  
    //重载[]  
    char& operator[](size_t size)  
    {  
        if(--_pcount >1)  
        {  
            char* ptemp = new char[strlen(_ptr)+1];  
            int pcount = 1;  
            strcpy(ptemp,_ptr);  
            _pcount--;  
            _ptr = ptemp;  
            _pcount = pcount;   
        }  
        return _ptr[size];  
    }  
private:  
    char*_ptr;  
    static int _pcount;  
};  
int String::_pcount = 0;  
void FunTest()  
{  
    String s1("hello");  
    String s2(s1);  
    String s3(s2);  
    s3 = s2;  
    String s4("world");  
    String s5(s4);  
}  
int main()  
{  
    FunTest();  
    return 0;  
}

如果一个对象第一次开辟空间存放字符串再开辟一块新的空间存放新的额引用计数,当它拷贝构造其它对象时让其它对象的引用计数都指向存放引用计数的同一块空间,pcount设置成int*就可以啦,但是这种方式有缺陷。

缺陷一:每次new两块空间,创建多个对象的时候效率比较低

缺陷二:它多次分配小块空间,容易造成内存碎片化,导致分配不出来大块内存

深拷贝

深拷贝也就是地址拷贝,在堆中申请新的空间来存取数据,这样数据之间相互独立。

String(const String& s)
{
      _ptr = new char[strlen(s._ptr)+1];
      strcpy(_ptr,s._ptr);
}
写时拷贝

在多个指针同时指向一个空间的时候,我们会遇到一个问题,在改变一个指针所指向内容的时候,其他指针指向的内容也是随之改变,所以我们引入了写时拷贝。

给要改变值的那个对象重新new出一块内存,然后先把之前的引用的字符数据复制到新的字符数组中,这就是写时拷贝。注意,同时还要把之前指向的内存的引用计数减1(因为它指向了新的堆中的字符数组),并在堆中重新new一个块内存,用于保存新的引用计数,同时把新的字符数组的引用计数置为1。因为此时只有一个对象(就是改变值的对象)在使用这个内存。

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <string.h>

using namespace std;

class String
{
    friend ostream& operator<<(ostream &os, String &str);

public:
    String(const char *str = NULL); //通用构造函数
    String(const String &str);      //拷贝构造函数
    ~String();                     //析构函数

    String& operator=(const String &str);       //重载=
    char& operator[](int n) const;              //重载[]

    friend ostream& operator<<(ostream &os, String &str);//输出

private:
    char *data;     //字符串
    size_t length;  //长度
};

String::String(const char *str)
{
    if (!str)
    {
        length = 0;
        data = new char[1];
        data = '\0';
    }
    else
    {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }
}
String::String(const String &str)
{
    length = str.length;
    data = new char[length + 1];
    strcpy(data, str.data);
}
String::~String()
{
    delete[] data;
    length = 0;
}
String& String::operator=(const String &str)
{
    if (this == &str)   
        return *this;
    delete[]data;
    length = str.length;
    data = new char[length + 1];
    strcpy(data, str.data);
    return *this;
}

inline char& String::operator[](int n) const//重载[]
{
    if (n >= length)
        return data[length - 1]; //限定范围
    else
        return data[n];
}
ostream& operator<<(ostream &os, String &str)
{
    os << str.data;
    return os;
}

void test()
{
    String s1("hello");
    String s2(s1);

    String s3;
    s3 = s2;

    s1[0] = 'a';
    cout << s1 << endl;
    cout << s2 << endl;
    cout << s3 << endl;

}

int main()
{
    test();
    system("pause");
    return 0;
}

这里写图片描述

<think>好的,我现在需要回答用户关于C++深拷贝和浅拷贝的问题。首先,我得回忆一下自己对这两个概念的理解,确保准确无误。 浅拷贝,应该是指默认的拷贝构造函数或赋值运算符进行的成员逐一复制的操作。这时候,如果类中有指针成员,两个对象的指针会指向同一块内存。这可能会导致问题,比如双重释放或者一个对象修改数据影响另一个对象。这时候,用户可能想知道具体的问题例子,比如修改一个对象的数据会影响另一个对象,或者析构时崩溃。 然后是深拷贝,应该是为了解决浅拷贝的问题,自己实现拷贝构造函数和赋值运算符,为每个对象创建独立的指针内存副本。这样每个对象都有自己的资源,互不影响。需要解释深拷贝如何分配新内存,并复制内容,而不仅仅是复制指针的值。 接下来需要考虑用户的可能背景。他们可能正在学习C++的类、动态内存管理,或者遇到了相关问题。用户可能想知道什么时候需要深拷贝,以及如何正确实现它。可能需要给出示例代码,说明浅拷贝的问题,然后展示深拷贝的解决方案。 要确保回答结构清晰,分步骤解释。首先定义两者,然后比较区别,说明问题,最后给出实现方法。同时,按照系统级指令,数学公式要用$...$,独立公式用$$...$$,但这个问题可能涉及代码示例,需要注意正确使用代码块。 另外,要检查是否有LaTeX语法,但这个问题可能更多是代码和概念,可能不需要太多数学公式。但如果有需要,比如解释内存地址或指针运算,可能需要用行内公式,比如指针的值是$0x7ffeeb0c$这样的表示,不过可能代码块更适合。 需要确保回答用中文,技术术语准确。例如,“拷贝构造函数”、“赋值运算符”、“动态内存分配”、“析构函数”等术语的正确翻译和使用。 可能还需要提醒用户,当类管理资源(如动态内存、文件句柄等)时,必须实现深拷贝,遵循Rule of Three(C++03)或Rule of Five(C++11及以后)。这可能是一个扩展点,但用户的问题可能集中在深浅拷贝本身,所以可能需要简要提及。 现在组织回答的结构: 1. 定义浅拷贝和深拷贝 2. 浅拷贝的问题(举例说明) 3. 如何实现深拷贝(代码示例) 4. 使用场景和最佳实践 5. 总结对比两者的区别 在写代码示例时,要注意正确性。例如,展示一个包含指针成员的类,默认拷贝构造函数导致的问题,然后实现深拷贝的拷贝构造函数和赋值运算符。 需要检查是否有语法错误,比如在代码示例中正确使用new和delete,避免内存泄漏。同时,注意在说明问题时,强调双重释放或悬垂指针的风险。 可能还需要提到,在C++11中移动语义的影响,但用户的问题可能不涉及这些,所以可能暂时不深入,除非必要。 最后,确保回答符合用户可见层的结构要求,逐步引导用户理解问题,并提供可靠的解决方案。</think>让我们逐步理解C++中的深拷贝与浅拷贝: 1. **基本定义** - 浅拷贝:仅复制指针值(内存地址) ```cpp class Shallow { int* data; public: Shallow(const Shallow& other) : data(other.data) {} // 浅拷贝构造函数 }; ``` 此时两个对象的`data`指针指向同一内存区域:`$0x7ffdeb0c \rightarrow [内存块]$` - 深拷贝:创建新的内存空间并复制内容 ```cpp class Deep { int* data; public: Deep(const Deep& other) { data = new int(*other.data); // 深拷贝构造函数 } }; ``` 此时内存布局为: ``` obj1.data → $0x7ffdeb0c [内存块1]$ obj2.data → $0x8abf3200 [内存块2]$ ``` 2. **问题场景演示** ```cpp void shallow_problem() { Shallow a; Shallow b = a; // 拷贝构造 // a和b的data指向同一内存 delete &a; // 释放内存 b.show_data(); // 访问已释放内存 → 段错误 } ``` 3. **实现深拷贝的关键步骤** ```cpp class SafeArray { int* arr; int size; public: // 深拷贝构造函数 SafeArray(const SafeArray& other) : size(other.size) { arr = new int[size]; std::copy(other.arr, other.arr + size, arr); } // 深拷贝赋值运算符 SafeArray& operator=(const SafeArray& other) { if (this != &other) { delete[] arr; // 释放旧资源 size = other.size; arr = new int[size]; // 分配新资源 std::copy(other.arr, other.arr + size, arr); } return *this; } ~SafeArray() { delete[] arr; } // 析构函数 }; ``` 4. **使用场景对比表** | 特征 | 浅拷贝 | 深拷贝 | |-----------|---------------------------|---------------------------| | 内存开销 | 小(仅复制指针) | 大(复制完整数据) | | 执行速度 | 快($O(1)$时间复杂度) | 慢($O(n)$时间复杂度) | | 安全性 | 低(可能双重释放) | 高(资源独立) | | 适用场景 | POD类型、无资源管理的类 | 包含指针/动态资源的类 | 5. **最佳实践建议** - 遵循三/五法则(Rule of Three/Five): - 如果定义了拷贝构造函数、拷贝赋值运算符或析构函数中的任意一个,通常需要定义全部三个(C++03) - 在C++11中增加移动构造函数和移动赋值运算符(五法则) - 使用`std::unique_ptr`等智能指针可以自动管理资源,减少手动深拷贝的需求 当类包含以下资源时必须使用深拷贝: - 动态分配的内存(`new`/`new[]`) - 文件句柄(`FILE*`) - 网络连接套接字 - 数据库连接 - 其他需要独占访问的系统资源 通过正确实现深拷贝,可以避免: 1. 悬垂指针(Dangling Pointer) 2. 内存泄漏(Memory Leak) 3. 双重释放错误(Double Free) 4. 数据竞争(Data Race)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值