C++:String类的实现(浅拷贝、深拷贝)

本文深入探讨了C++中深拷贝与浅拷贝的概念,通过具体实例讲解了两者之间的区别,以及如何在类中实现深拷贝以避免资源管理问题。同时,提供了String类的实现代码,并分析了拷贝构造函数与赋值运算符重载的重要性。


首先大家看下以下string类的实现是否有问题?

class String
{
	char* _str;
public:
    String(const char* str = "")
    {
		// 构造string类对象时,如果传递nullptr指针,认为程序非法,此处断言下 
		if(nullptr == str)
		{
            assert(false);
			return; 
		}
        _str = new char[strlen(str) + 1];
        strcpy(_str, str);
    }
	~String() {
        delete[] _str;
		_str = nullptr;
	}
};

// 测试
void TestString() {
    String s1("hello bit!!!");
    String s2(s1);
}

在这里插入图片描述

上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。

浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。 如果 对象中管理资源,最后就会 导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规

要解决浅拷贝问题,C++中引入了深拷贝。

深拷贝

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

在这里插入图片描述

实现 String 类

class String
{
	char* _str;
public:
    String(const char* str = "")
    {
		// 构造string类对象时,如果传递nullptr指针,认为程序非法,此处断言下 
		if(nullptr == str)
		{
            assert(false);
			return; 
		}
        _str = new char[strlen(str) + 1];
        strcpy(_str, str);
    }

    String(const String& s)
        :_str(new char[strlen(s._str)+1])
    {
        strcpy(_str, s._str);    
    }

    // const 是因为对 s 不需要修改,安全性更高
    // 参数 & 是因为不需要传值拷贝、效率高
    // 返回值 & 是为了连续赋值(效率高)
    String& operator=(const String& s){
        if(this != &s)  // 检测是否自己给自己赋值
        {
            /*delete[] _str;
            _str = new char[strlen(s._str)+1];
            strcpy(_str, s._str);*/

            // 开辟空间如果失败,不会破坏原本对象
            char *temp = new char[strlen(s._str)+1];
            strcpy(temp, s._str);
            delete[] _str;
            _str = temp;
        }
        return *this;
    }

	~String() {
		if(_str)    // 判断当前空间是否已经释放、置空
        {
        	delete[] _str;
			_str = nullptr;
    	}
	}
};

测试代码:

void TestString() {
    String s1("hello bit!!!");
    String s2(s1);
}

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

知识点习题:

  1. 以下代码是否完全正确,执行可能得到的结果是____。
class A{ 
    int i;
};
class B{
    A *p; 
public:
    B(){
        p=new A;
    }
    ~B(){
        delete p;
    } 
};
void sayHello(B b){ 
}
int main(){
    B b;
    sayHello(b); 
}

A. 程序正常运行
B. 程序编译错误
C. 程序崩溃
D. 程序死循环

正确答案:

C

答案解析:

形参传递析构指针两次

以下三种情况需要用到拷贝函数:

  1. 一个对象以值传递的方式传入函数体;
  2. 一个对象以值传递的方式从函数返回;
  3. 一个对象需要通过另外一个对象进行初始化。

本例中,在调用sayHello(b)时符合上面1. 的情况,因此会调用拷贝构造函数。由于类中未显式声明一个拷贝构造函数,编译器将会自动生成一个默认的拷贝构造函数,来完成对象间的位拷贝(或称浅拷贝),也就是把对象里的值完全复制给另一个对象,如ClassX = ClassY。这时,如果ClassY中有一个成员变量指针已经申请了内存,那ClassX中的那个成员变量也指向同一块内存。这就出现了问题:当ClassY把内存释放了(如:析构),这时ClassX内的指针就是野指针了,出现运行错误。(即sayHello()函数体结束析构了一次,main()函数结束又一次析构,而这时p已经是野指针了,因而出错。)

#include<iostream>
using namespace std;
class MyClass
{
public:
    MyClass(int i = 0)
    {
        cout << i;
    }
    MyClass(const MyClass &x)
    {
        cout << 2;
    }
    MyClass &operator=(const MyClass &x)
    {
        cout << 3;
        return *this;
    }
    ~MyClass()
    {
        cout << 4;
    }
};
int main()
{
    MyClass obj1(1), obj2(2);
    MyClass obj3 = obj1;
    return 0;
}

运行时的输出结果是()

A. 11214444
B. 11314444
C. 122444
D. 123444

正确答案 : C

答案解析

C D 辨析:

关键是区分 浅/深拷贝操作 和 赋值操作:

  • 没有重载=之前:
A a ;
A b;
a = b;

这里是赋值操作。

A a;
A b = a; 

这里是浅拷贝操作。

  • 重载 = 之后:
A a ;
A b;
a = b;

这里是深拷贝操作(当然这道题直接返回了,通常我们重载赋值运算符进行深拷贝操作)。

A a;
A b = a; 

这里还是浅拷贝操作。

所以 MyClass obj3 = obj1; 调用的是拷贝构造函数。

如果写成 MyClass obj3; obj3 = obj1; 输出的结果就是 123444


如有不同见解,欢迎留言讨论~~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值