【C++】-教你实现一下简单的string类(面试常问)

本文介绍了在C++面试中常见的面试题——自定义一个string类。文章详细讲解了如何实现构造函数、析构函数、拷贝构造函数和赋值运算符重载,分别展示了传统写法和现代写法。现代写法利用临时对象和swap函数,简化了代码并避免了深浅拷贝问题,提高了代码的简洁性和可读性。
摘要由CSDN通过智能技术生成

C++面试时,面试官总是喜欢让学生自己来模拟实现一个string类。由于时间原因,这个string类肯定不能包含STL库里的全部功能,但至少要能进行简单的资源管理,比如初始化,赋值,释放。我认为面试应该需要给出一下四个接口:构造函数、析构函数、拷贝构造函数、赋值运算符重载。本篇文章中我会向大家介绍这四个接口的两种实现方式供大家参考,分别是传统版写法和现代版写法。

下面开始介绍。


1. 传统写法

1.1 成员变量

由于只进行简单的资源管理,因此此处string类的成员变量只设置一个指向动态数组的一个字符指针

namespace bit
{
	class string
	{
	public:

	private:
		char* _str;
	};
}

注意要点:
由于全局命名空间std中定义了STL中的string类,为了避免命名发生冲突,这里我将模拟实现的string类封装在一个名为bit的命名空间中去。(当然如果你没有展开std就不用了)

1.2 构造函数

我们先来看构造函数的一种错误写法:

namespace bit
{
	class string
	{
	public:
		string(const char* str)
			:_str(str)
		{};

	private:
		char* _str;
	};
}

int main()
{
	bit::string s1 = "hello world";
	return 0;
}

有人一上来直接用形参str去初始化成员变量_str,我们来看运行结果:
在这里插入图片描述
编译不通过,根据给出的错误信息也不难分析。注意看main函数中,用来初始化s1的字符串"hello world"是常量区一个常量字符串,上面的直接赋值等于让string对象的_str指针指向这个常量字符串。

对于一个string类来说,有许多接口是需要修改字符串本身内容的,所以string类里的字符指针指向的空间一定要是可以修改的,也就是非const指针。用const指针去初始化非const指针属于权限放大错误。

正确写法:

string(const char* str = "")
	:_str(new char[strlen(str) + 1])
{
    strcpy(_str, str);
}

采用初始化列表和函数体初始化结合的方式去实现构造函数。先在初始化列表中为string对象在堆区开辟与初始化字符串同样大小的空间,再在函数体内用strcpy函数将初始化字符串的内容拷贝过去。

到这里还不算全部完成,string对象还有这样一种定义方式:

string s1;

不传参怎么办?根据我们使用string的经验,不传参时的string对象应该是被初始化为一个空字符串。

而我们上面所写的构造函数只实现是有参数的情况,如果没有参数,定义对象时就没有合适的构造函数调用,因而就会导致错误。

为了解决这个问题,我们可以把上面的构造函数实现成一个带缺省值的函数:

string(const char* str = "")
	:_str(new char[strlen(str) + 1])
{
    strcpy(_str, str);
}

如果没有没有传参,默认形参是一个空字符串,用空字符串去初始化对象。

1.3 析构函数

析构函数实现起来就比较简单了,只用把成员变量_str delete掉即可:

~string()
{
    delete[]_str;
    _str = nullptr;
}

1.4 拷贝构造函数

对于string类这种涉及资源管理的类,实现拷贝是必须用深拷贝来实现:

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

为了避免拷贝对象和被拷贝对象指向同一块空间,拷贝构造必须用深拷贝来实现。深拷贝的做法是为拷贝对象开辟一块和被拷贝对象同样大小的空间,再将被拷贝对象的内容拷贝过去,这样就不会发生空间重叠了。

1.5 赋值运算符重载

赋值运算符重载的实现同样涉及深浅拷贝的问题。如果使用默认的赋值运算符重载函数,发生的就是浅拷贝,从而导致空间重叠,因此我们必须自己用深拷贝来实现一个:

string& operator=(const string& s)
{
    if (_str != s._str)
    {
        delete[] _str;
        _str = new char[strlen(s._str) + 1];
        strcpy(_str, s._str);
    }
    
    return *this;
}

代码中注意要点:

  1. 这里我们要先明确进行赋值运算符重载的两个对象,必定是两个已经创建好的对象。把一个对象的内容拷贝给另一个对象,这必然就会面临一个问题,那就是被赋值对象的空间大小和赋值对象的空间大小不一致。如果空间足够还好说,如果空间不够必然要重新进行扩容。
    这里我们采取的做法是,一上来直接把被赋值对象的空间delete掉,放弃被赋值对象原来的空间,重新为被赋值对象开辟出一块和复制对象同样大小的空间,再用strcpy进行内容拷贝

  2. 赋值时可能还会出现这样一个种情况,如果是自己给自己赋值,一上来直接把自己的空间释放掉就会导致错误,因为后面拷贝时的空间就不存在。因此一上来还要先判断是不是自己给自己赋值,如果是,直接返回自身引用,不用再进行拷贝了。

1.6 简易string类传统写法源码

传统写法到这就全部结束了,下面附上源码

class string
{
public:
    //构造函数
	string(const char* str = "")
		:_str(new char[strlen(str) + 1])
	{
		strcpy(_str, str);
	}

    //析构函数
	~string()
	{
		delete[]_str;
		_str = nullptr;
	}

    //拷贝构造函数
	string(const string& s)
	{
		_str = new char[strlen(s._str) + 1];
		strcpy(_str, s._str);
	}

    //赋值运算符重载
	string& operator=(const string& s)
	{
		if (_str != s._str)
		{
			delete[] _str;
			_str = new char[strlen(s._str) + 1];
			strcpy(_str, s._str);
		}

		return *this;
	}

private:
	char* _str;
};

2. 现代写法

现代写法的构造函数和析构函数与传统写法完全一样,其差别主要体现在拷贝构造函数和赋值运算符重载函数。

2.1 拷贝构造函数

string(const string& s)
	:_str(nullptr)
{
	string tmp(s._str);
	swap(_str, tmp._str);
}

现代版拷贝构造实现的主要思路是先用形参构造一个临时对象tmp,这时我们会发现,tmp对象所在的这块空间实际上是拷贝构造对象所需要的空间。于是我们就把这两个对象的空间进行交换,这样就等于完成了一次string对象的拷贝构造。

有一点需要注意,这里的临时对象tmp出了函数作用域后就会调用析构函数自动释放掉。tmp对象释放的空间实际上就是拷贝对象原来的空间,而我们知道,拷贝对象的成员变量_str一开始指向的是一块随机空间,直接去释放一块随机空间是不被允许的。所以为了避免这种情况发生,一开始我们先把拷贝对象的成员变量_str初始化为空指针,最后在析构tmp时释放一个空指针,这种做法在语法上是被允许的。

可能有人不是很理解,为什么这种写法被叫做现代版写法,它和我们上面写的传统写法到底有什么区别?

大家可以回看一下上面的传统写法,传统写法在实现是一直是调用对象自己去开辟空间,然后自己进行拷贝。而现代写法则是我让一个临时对象去帮我开辟好我想要的空间,然后我再把这块空间拿过来,最后再把我不要的空间给你,还要让你帮我释放掉。

这有点像什么?是不是就有点像现代社会中的资本运作。一个资本家想要完成一批产品时,他们通常不会自己亲自去做,而是雇一批工人帮他完成。资本家最后会拿走工人完成的产品为自己换取利益,实现自己的资本积累。

现代版写法实际上是把资本运作的思想抽象到了string类的实现中来,我们实现的这个string对象就像是一个资本家,这样做的好处是使得string对象的实现过程变得精简,不用考虑一些复杂情况。这种思想很有参考价值,在C++中用到的地方非常广泛。

2.2 赋值运算符重载

下面我们再来通过赋值运算符重载的实现感受一下现代写法的好处。

string& operator=(const string s)
{
	string tmp(s._str);
	swap(_str, tmp._str);
  
	return *this;
}

赋值运算符重载函数的实现更加直接,直接采取传值传参的形式,让形参去开辟赋值对象想要的空间。最后交换形参和赋值对象的空间,这样赋值对象就拿到了自己想要的空间,并且把自己丢弃不要的空间让形参去释放。

再和上面传统写法做对比,我们看到现代写法不用再判断自己给自己赋值情况,并且不用自己主动释放空间+开辟空间,所有事情都交给形参去做,调用对象只管坐享其成。从代码上我们也可以看到,现代写法的代码量更精简,易读。

2.3 简易string类现代写法源码

class string
	{
	public:
		//构造函数
		string(const char* str = "")
			:_str(new char[strlen(str) + 1])
		{
			strcpy(_str, str);
		}

		//析构函数
		~string()
		{
			delete[]_str;
			_str = nullptr;
		}

		//拷贝构造函数
		string(const string& s)
			:_str(nullptr)
		{
			string tmp(s._str);
			swap(_str, tmp._str);
		}

		//赋值运算符重载
		string& operator=(const string s)
		{
			string tmp(s._str);
			swap(_str, tmp._str);

			return *this;
		}

	private:
		char* _str;
	};

本篇文章所要介绍的内容到这里就全部结束了。文章中我主要向大家介绍了简单tring类的两种实现方式,两种方法希望大家都能够掌握,我个人比较推荐大家去用现代版写法实现。

最后希望这篇文章能够为大家带来帮助。

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值