[ C++ ] string类之构造,拷贝,赋值 及其模拟实现

  • Post author:
  • Post category:其他


本篇文章开始我们将步入C++中重要的标准库函数,以及C++标准库函数中最重要的组成部分—STL

首先我们先学习了解一个重要的string类。


目录


1.string出现的原因


1.1C语言处理字符串


2.标准库中的string类


2.1 string类


3.string类的常见构造及模拟实现


3.1string类对象的常见构造


3.2 string类的构造函数


3.3 string类的拷贝构造


3.4 string类的赋值构造


3.4.1 常规解法


3.4.2考虑异常安全法的解法:


4.全部代码


5.参考文献


1.string出现的原因

1.1C语言处理字符串

在C语言中,字符串都是以 ‘\0’ 结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,例如:strlen,strcpy,strcat…. 但是这些库函数与字符串是分离开的,不太符合C++中面向对象的思想,而且底层空间仍需要用户自己管理,并且很有可能会越界访问。

因此在C++中,为了更简单,方便,快捷的使用字符串类型,C++提供了string类。

2.标准库中的string类

2.1 string类


1string类文档介绍



在使用string类时,必须包含string的头文件 #include <string>


1.字符串是表示字符序列的类.


2.标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。


3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。

4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。

5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

总结:


1、string是表示字符串的字符串类


2、该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。


3、string在底层实际是:basic_string模板类的别名


typedef basic_string<char, char_traits, allocator> string;
4、不能操作多字节或者变长字符的序列。

3.string类的常见构造及模拟实现

3.1string类对象的常见构造



constructor

)函数名称
功能说明
string() 构造空的string类对象,即空字符串
string(const char*s) 用C-string来构造string类对象
string(size_t n,char c) string类对象中包含n个字符c
string(const string& s) 拷贝构造函数

int main()
{
	string s1;   //构造空的s1  等价于string s1("");
	string s2("hello");//用C格式字符串构造
	string s3(s2);

	return 0;
}

我们来自己模拟实现一下string类的构造函数

3.2 string类的构造函数

class string
{
public:
	//全缺省的构造函数
	string(const char* str = "")
		:_size(strlen(str))
		,_capacity(_size)
	{
		//实际上要多开一个 留给'\0'
		_str = new char[_capacity + 1];
		strcpy(_str, str);//会把'\0也拷过去'
	}
private:
	char* _str;
	size_t _size;//有效字符个数 不算'/0';
	size_t _capacity;//实际存储有效字符的空间
};

想必这段代码大家肯定是可以看的懂得。

3.3 string类的拷贝构造

基本写法

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

现代写法

        void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
		//现代写法
		string(const string& s)
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
			string tmp(s._str);
			swap(tmp);
		}

基本写法想必大家肯定很好理解,但是现代写法可能会有点疑惑,我们在下面的赋值拷贝就会分析。

3.4 string类的赋值构造

3.4.1 常规解法

我们在写任何一个类型的赋值运算符重载函数时一定要考虑到下面这几个方面:

1》是否把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身的引用(*this)。只有返回一个引用,才能支持连续赋值。否则,如果函数的返回值是void,在使用该赋值运算符将不能支持连续赋值。这与我们日常使用是违背的。

2》是否把传入的参数的类型声明为常量引用。如果传入的参数不是引用而是实例,那么从形参到实参会调用一次复制拷贝函数。把参数声明为引用可以避免这样无谓消耗。能提高代码效率。同时,我们在赋值运算符函数内不会改变传入的实例的状态,因此应该为传入的引用参数加上const关键字。

string& operator=(const string& s)

3》是否释放实例自己已有的内存。如果我们忘记在分配新内存之前释放自身已有的空间,则程序会出现内存泄漏

4》判断传入的参数和当前的实例(*this)是不是同一个实例。如是同一个,则不进行赋值操作,直接返回即可。如果实现不做判断就进行赋值,那么在释放实例自身空间内存的时候就会导致严重的问题:当*this和传入的参数是同一个实例时,一旦释放了自身的内存,传入的参数的内存也同时被释放了,因此再也找不到需要赋值的内容了。

结合以上4点,我们可以写出:

	string& operator=(const string& s)
	{
		if (this == &s)
			return *this;//自己赋值自己 直接返回
		delete[] _str;
		_str = nullptr;
		_str = new char[strlen(s._str) + 1];
		strcpy(_str, s._str);
		return *this;
	}

这种经典解法已经能够解决问题了,但是我们如果在极端情况下考虑如果delete释放_str后,内存不足导致new char抛出异常,则_str将是一个空指针,这样很容易导致程序崩溃。也就是说,一旦在赋值运算符函数内部抛出一个异常,string的实例不再保持有效的状态,这就违背了异常安全原则。

3.4.2考虑异常安全法的解法:

我们有两种解法:

1》一种简单的办法是我们先用new分配新内容,再用delete释放已有的内容。这样只在分配内容成功之后才会释放原来的内容,也就是当分配内存失败时我们能确保string的实例不会被修改。

2》第二种解法是我们先创建一个临时实例,再交换临时实例和原来的实例。由于我们创建的临时实例时一个局部变量,一旦程序除了该局部变量的作用域,就会自动调用析构函数,把这个临时空间所指向的内存释放掉。由于这个临时空间指向的内存就是我们原来的实例的内存。这就相当于自动调用析构函数释放实例的内存。

并且我们在string的构造函数里面用new分配内存,如果由于内存不足抛出异常,但是我们还没有修改原来实例的状态,因此实例的状态还是有效的,这也就保证了异常安全性。

下面是考虑异常安全的代码:

	string& operator=(const string& s)
	{
		if (this != &s)
		{
			string tmp(s._str);

			std::swap(_str, tmp._str);
			std::swap(_size, tmp._size);
			std::swap(_capacity, tmp._capacity);
		}

		return *this;
	}

我们使用我们自己模拟实现的看看效果:

int main()
{
	//std库
	//std::string s1;   //构造空的s1  等价于string s1("");
	//std::string s2("hello");//用C格式字符串构造
	//std::string s3;

	//自己模拟实现
	s::string s1("hello world");
	s::string s2;
	s::string s3;

	s3 = s2 = s1;

	return 0;
}

4.全部代码

以下是本篇文章模拟实现的string类相关代码。

namespace s
{
	class string
	{
	public:
		//全缺省的构造函数
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//实际上要多开一个 留给'\0'
			_str = new char[_capacity + 1];
			strcpy(_str, str);//会把'\0也拷过去'
		}

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

		//string& operator=(const string& s)
		//{
		//	if (this == &s)
		//		return *this;//自己赋值自己 直接返回
		//	delete[] _str;
		//	_str = nullptr;
		//	_str = new char[strlen(s._str) + 1];
		//	strcpy(_str, s._str);
		//	return *this;
		//}
		string& operator=(const string& s)
		{
			if (this != &s)
			{
				string tmp(s._str);

				std::swap(_str, tmp._str);
				std::swap(_size, tmp._size);
				std::swap(_capacity, tmp._capacity);
			}

			return *this;
		}

		~string()
		{
			if (_str)
			{
				delete[] _str;
				_str = nullptr;
				_size = 0;
				_capacity = 0;
			}
		}
	private:
		char* _str;
		size_t _size;//有效字符个数 不算'/0';
		size_t _capacity;//实际存储有效字符的空间
	};
}

5.参考文献

《剑指offer(第二版)》—作者:何海涛

(剑指offer面试题1:赋值运算符函数)



版权声明:本文为qq_58325487原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。