string

一.string简介

string其实是一个存储字符的顺序结构,也就是字符数组??

string

从文档可以看出来,string是typedef出来的,它其实是basic_string<>这个类模板显式实例化为basic_string的一个类。

string

basic_string还实例化除了其他的类?

这些类的区别在于使用的编码不同

  • string-> UTF8
  • u16string->UTF16
  • u32string->UTF32
  • wstring->宽字符

string

string的其他介绍可查看该文档?string - C++ Reference

二.string的使用

1.constructor

C++98提供了7个构造函数,只需要掌握重要的即可string

1.1无参构造(默认构造函数)

构造一个空的string对象,长度和容量都是0.

string s1;

1.2拷贝构造

用一个已经存在的对象,初始化一个正在创建的对象。这两个对象有着相同的内容、长度、容量

string s1;
string s2(s1);

1.3带参构造

用一个以\0为结尾的字符串构造出一个string对象

string s3("hello world");

前三个都是需要重点掌握的构造函数,剩余的构造函数只需知道即可。?


string (const string& str, size_t pos, size_t len = npos);

获取str内容的一部分,从pos位置,长度为len个字符string

长度len带有缺省值,如果不传长度,就会默认从pos位置拷贝到字符串结束。

npos是string类的一个const静态成员,值是整型的最大值。

如果传的len比当前内容的长度还长,也会默认从pos位置拷贝到字符串结束。

string (const char* s, size_t n);

获取字符串s的前n个字符

string

n大于字符串长度时,只会提取该字符串\0之前的内容

string (size_t n, char c);

?用n个c字符构造string对象string

template 
  string  (InputIterator first, InputIterator last);

?用一个迭代器区间初始化一个string对象string

2.destructor?

string对象会自动调用其析构函数,所以不需要管

3.流插入和流提取

string类重载了流插入和流提取string

4.sring对象的访问

4.1operator[]

string重载了[],可以像数组那样用下标访问数据。

它返回的是该位置元素的引用,意味着可以修改

string

string

4.2其他的访问方式?

string s1("hello world");
cout << s1[0] << endl;
s1[0] = 'x';
cout << s1 << endl;

string s2("hello world");
cout << s2.at(0) << endl;

cout << s2.front() << endl;
cout << s2.back() << endl;

at的作用和[]的作用相同,都是返回指定下标位置元素的引用

区别:1、[]越界会报错,at越界会抛异常;2、当下标等于length时,[]返回\0,at会抛异常

front是返回string对象的第一个字符

back是返回string对象的最后一个字符(\0之前的字符)string

注意:空的string对象不能调用front()、back()

?5.string对象的遍历

通过下标遍历,size()是string的成员函数,返回的是string对象的长度

int i = 0;
for (; i < s1.size(); i++)
{
	cout << s1[i] << " ";
}

通过迭代器遍历

string::iterator si = s1.begin();
while (si != s1.end())
{
	cout << *si << " ";
	++si;
}

?可以将迭代器理解成类似于指针的东西,begin()会返回一个指向对象第一个位置的迭代器,end()会指向对象的最后一个位置的下一个位置的迭代器string

范围for遍历

for (auto e : s1)
{
	cout << e << ' ';
}

范围for是一种自动推导类型,自动遍历,自动判断结束的一种遍历方式,他底层其实就是迭代器。?

auto是一个关键字,用来自动推导类型,当我们知道类型时,也可以显示写类型

for(char e : s1)

在范围for中,e只是s1中每一个字符的拷贝,e的修改不会影响s1本身,如果想要修改,就加上引用

for(auto& e : s1)

6.iterator?

iterator——迭代器,是stl的六大组件之一。虽然string出现的比stl早,但是string在形式上也是一种容器。

迭代器是所有容器通用的遍历方式,因为不是所有的容器都支持下标访问。

那么所有的容器也都可以使用范围for来遍历,因为范围for底层就是迭代器。

迭代器有可能是原生指针,也有可能不是,但可以借助指针的逻辑来理解迭代器。

迭代器一共有四种:

  • iterator->普通迭代器,返回的是元素的引用,可以修改
  • cosnt_iterator->const迭代器,只可读,不可修改
  • reverse_iteraotr->反向迭代器,逆序遍历
  • const_reverse_iterator->const反向迭代器,逆序遍历,且不可修改
string s1("hello world");
cout << s1 << endl;

string::iterator si = s1.begin();
while (si != s1.end())
{
	*si += 2;
	cout << *si << " ";
	++si;
}
cout << endl;

string::reverse_iterator rsi = s1.rbegin();
while (rsi != s1.rend())
{
	cout << *rsi << " ";
	++rsi;
}
cout << endl;

const string s2(s1);
string::const_iterator csi = s2.begin();
while (csi != s2.end())
{
	*csi += 2;//不可修改
	cout << *csi << " ";
	++csi;
}
cout << endl;

需要注意的是,反向迭代器遍历时不是--,依旧是++?

?普通迭代器也实现了const版本,但是C++11后,重新设计了一个cbegin()和cend(),const对象也可以调用这个迭代器,与普通版本的const作用相同。

7.auto?

在C++11中,auto是一个类型指定符,它可以自动推导类型

auto a = 1;//int
auto b = a;//int
auto c = 1.1;//double
auto d = 'x';//char

typeid().name()可以获取变量的类型?

string

当一个类型使用auto时必须初始化?

auto e;

string

不可以用auto定义数组

auto arrary[] = {1,2,3};

string

?auto不可以做函数参数,但可以做返回值(谨慎使用)

void func(auto a)
{}

string

auto不能做返回值可以认为是一个规定,即使给了缺省值也不行?string

auto func2()
{
	return 1;
}

?auto做返回值时,其类型由返回的值来确定。

但auto做返回值需要谨慎使用,遇到函数链式调用时,不方便推断其类型:string

当然为了解决这种问题,我们可以直接用auto来接收返回值:

auto a = func4();

auto真正的作用其实是为了省略那些长类型:?

string::const_reverse_iterator crsi = s.crbegin();
auto crsi = s.crbegin();

长类型写起来麻烦费时间,利用auto就可以很快速的写完。但是会牺牲掉一些可读性。?

8.string的容量操作

string其实就是一个可动态增长的字符数组,所以我们猜测其结构如下:

class string
{
private:
	char* _str;
	size_t size;
	size_t capacity;
};

_str指向的是堆上面的数组,size是数组的有效长度,capacity是其容量。当size==capacity时,就需要进行扩容操作,以此来实现动态增长的效果。?

8.1size()/length()

返回string对象中字符串的长度,单位是字节。

size()和length()两者都是该效果,没有任何区别。string

对于字符串来说,length这个接口就很符合。但是对其他容器来说,长度这个接口就不再合适了。比如map。所以为了与后续的stl统一,就给string引入了size这个接口。?

8.2capacity()

返回已分配的空间大小,也就是其容量,要与size区分开。size是当前有效字符的长度,capacity则是当前string对象可以容纳的字符的个数。string

?分析VS环境与g++环境下的扩容:

VS:

string

观察到,当一个空string时,就已经有了容量,这和我们推测的结构不同。实际上,在VS中,有一个buffer数组,大小是16,当字符串小于16时,会存到buffer中,当超过时,才会存到堆上。

class string
{
private:
	char buffer[16];
	char* _str;
	size_t _size;
	size_t _capacity;
};

我们可以通过sizeof来验证:string?

16+4+4+4 = 28;?

?容量是之所以是15是因为没有包含\0,所以在VS下,第一次扩容时2倍i,接下来的都是1.5倍。

g++:?

string

g++环境下,空string的容量就是0,所以和我们最先推测的结构类似?,而其扩容遵循严格的2倍扩容。

8.3.reserve(size_t n)

reserve会请求容量的变化,但是不会影响字符串的长度,也就是不会引起size的变化

resereve有以下三种情况:

1、n > capacity,一定会扩容,使capacit到达n或者比n还大

2、size < n < capacity,有可能会发生缩容,也有可能不会,取决于编译器的实现

3、n < size,一定不会缩容,因为reserve一定不会对原字符串产生影响

//测试代码
void test_string7()
{
	string s("hello worldxxxxxx");
	cout << "size:" << s.size() << endl;
	cout << "capacity" << s.capacity() << endl << endl;

	s.reserve(100);
	cout << "size:" << s.size() << endl;
	cout << "capacity" << s.capacity() << endl << endl;

	s.reserve(20);
	cout << "size:" << s.size() << endl;
	cout << "capacity" << s.capacity() << endl << endl;

	s.reserve(10);
	cout << "size:" << s.size() << endl;
	cout << "capacity" << s.capacity() << endl << endl;
}

VS下结果:string?在VS2022编译环境下,当n

g++下结果:

string

?g++环境下,当 n

8.4resize()

resize是对size进行请求,使其改变。它会使size变化到n:

n

size

n>capacity:不仅会将size扩大到n,用指定字符填充,还会进行扩容

如果没有指定字符的话,就会用\0填充

string

8.5.clear()

清空当前对象的内容,但是不引起capacity的变化。

VS和g++下都遵循这一原则

string

string

8.6.empty()

判断当前string对象是否为空

string s("hello worldxxxxxx");
cout << s.empty() << endl;
s.clear();
cout << s.empty() << endl;

string

8.7shrik_to_fit()

shrink_to_fit是C++11提供的一个接口,其功能类似于reserve的sizestring

9.string的修改操作

9.1尾插

push_back()

在字符串后面插入一个字符string

append()

string?在字符串结尾插入一个string对象string对象的一部分一个字符串一个字符串的前n个字符n个c字符(没指定字符就会默认插入\0)或者一段迭代器区间。

注意:在插入一个string对象的一部分时,如果没有指定长度,会默认将string对象的pos位置开始到结束都插入到原字符串后面;如果指定的长度大于剩余部分的话,也会将剩余的所有字符全都尾插到后面string

?operator+=

尾插一个string对象、一个字符串、一个字符?


	string s3("hello world");

	s3 += 'x';
	s3 += "abcdefg";
	s3 += s1;

因为+=非常形象,所以使用的最多?

?9.2insert()

string

insert就是在指定位置上插入数据。

insert设计了非常多的接口,?但是因为insert涉及到数据的挪动以及扩容等,效率较低,所以使用的也不是很多

9.3assign()?

用指定内容替代原内容

assign也设计了多个接口,但assign依旧不常用,需要用时查文档即可

9.4erase()

1、删除指定位置开始的n个字符,如果不指定长度或者长度大于剩余字符,都会将剩余字符全部删除

string s("hello world");
cout << s << endl;

//s.erase(6);
s.erase(6, 100);
cout << s << endl;//hello

2、删除一个指定迭代器位置上的字符

可以借此来实现头删

//s.erase(0,1);
s.erase(s.begin());

3、删除一个迭代器区间

因为erase删除数据也要挪动数据,而且对于字符串来说经常都是只加不减,所以这个接口也不常用?

9.5replace()

replace就是用指定的内容提供替换指定位置上的元素

replace可以和find等查找接口结合使用,达到替换指定元素的目的

下面代码的功能就是将字符串中的所有空格都替换成%%。?

string s1("hello wrold sunday xxx");

size_t pos = s1.find(' ');
while (pos != string::npos)
{
	s1.replace(pos, 1, "%%");
	pos = s1.find(' ', pos + 2);//指定从那个位置开始查找
}

因为这里是将一个字符替换成了两个字符,不仅需要扩容还需要挪动数据。但是如果遇到空格比较多且连续的话需要频繁扩容,效率低?string

可以采取另一种方式来实现替换:创建一个空string,然后遍历原字符串,遇到非空格就加等该字符,遇到空格就加等%%。?而且我们还可以提前开好空间,避免多次扩容

string tmp;
tmp.reserve(s1.size());
for (auto e : s1)
{
	if (e == ' ')
	{
		tmp += "%%";
	}
	else
	{
		tmp += e;
	}
}

9.6swap()、pop_back()

swa()就是交换两个string对象

pop_back()是C++11提供的一个尾删接口

10.string的其他操作

string s("hello.zip.cpp");
size_t pos = s.rfind('.');
string ans = s.substr(pos);

10.1查找

find

从指定位置开始查找一个string对象/字符串/字符出现的第一个位置,如果找到了就会返回该位置的下标,如果没找到就会返回npos。

?rfind

和find的功能类似,只不过是倒着查找第一个出现的位置。

当遇到需要查找文件的后缀时,且该文件不只一个‘.’。如果采用find查找的话就会出错,这时候从后向前查找更好。

string s("hello.zip.cpp");
size_t pos = s.rfind('.');
string ans = s.substr(pos);

?string

find_first_of

?从字符串中找出第一个与指定字符串中的任何一个元素

	std::string str("Please, replace the vowels in this sentence by asterisks.");
	std::cout << str << '\n';

	std::size_t found = str.find_first_of("abcdef");
	while (found != std::string::npos)
	{
		str[found] = '*';
		found = str.find_first_of("abcdef", found + 1);
	}

	std::cout << str << '\n';

就如这段代码,我们可以将指定字符串中的abcdef都替换成*。?string

find_last_of?

与find_first_of类似,其实就是倒着开始找。

这个接口可以用来实现路径与文件名的分离。而在windows操作系统和Linux操作系统中,Windows的文件分隔符是\,而Linux是/。为了同时区分这两个系统下的文件,我们就可以采用find_last_of来实现。?

void SplitFilename(const std::string& str)
{
	std::cout << "Splitting: " << str << '\n';
	std::size_t found = str.find_last_of("/\\");
	std::cout << " path: " << str.substr(0, found) << '\n';
	std::cout << " file: " << str.substr(found + 1) << '\n';
}

int main ()
{
  std::string str1 ("/usr/bin/man");
  std::string str2 ("c:\\windows\\winhelp.exe");

  SplitFilename (str1);
  SplitFilename (str2);

  return 0;
}

?因为在程序中\是转义字符,所以不会单独存在,为了表示\,所以需要用\\来表示。string

find_first_not_of和find_last_not_of

它们的功能就是上面两者的反面,返回第一个不是指定字符里的任意一个的字符

下面代码的意思就是将abcdef之外的字符全部替换成*。?

std::string str("Please, replace the vowels in this sentence by asterisks.");
	std::cout << str << '\n';

	std::size_t found = str.find_first_not_of("abcdef");
	while (found != std::string::npos)
	{
		str[found] = '*';
		found = str.find_first_not_of("abcdef", found + 1);
	}

	std::cout << str << '\n';

10.2c_str()

?返回堆上存储字符串的空间的地址

这个接口的实现,主要是为了兼容C语言。使C++程序可以调用C语言的接口。

10.3copy()

?从string对象拷贝内容到指定字符串中

string

10.4substr()

生成一个字串string?默认会从头开始获取,当不指定长度或者长度大于剩余字符都会获取到字符串结尾。

11.string的非成员函数?

11.1string重载了比较运算符,其比较逻辑与strcmp相同。string

11.2operator+?

重载+是为了可以实现多种相加,不只局限于第一个参数是string对象。但接口设计的过于冗余且+的使用场景也不多。string

11.3getline?

getline是string对象专用的一种流提取方式。

因为以往使用cin的话,会以空格或者换行为分隔符,但是如果一个字符串中含有空格,用cin输入的话,就会丢失空格之后的内容。

而getline是以换行符为分隔符,遇到空格不会停止读取,就不会丢失内容。stringstring

三.string的模拟实现?

在VS2022环境下实现string时,为了简单实现,就不再设计buffer字符数组用来存储长度小于15的字符串,且只实现些重要接口

模拟实现string的结构

为了避免与库里面的string冲突,所以将模拟实现的string定义到命名空间中。我们这里实现声明与定义到两个文件中:string.h string.cpp。

namespace xsc
{
	class string
	{
	private:
		char* _str;
		size_t _size;
		size_t _capacity;

		static const size_t npos;
	};
}

npos是一个静态成员,属于类,所以要在类外定义,放在.cpp文件中

namespace xsc
{
	static const size_t npos = -1;
}

不同文件的同一命名空间会合并。

1.构造函数?

首先实现无参构造和有参构造

在实现无参构造时,不能直接给_str赋空指针,也要开一个空间给\0,否则当打印由无参构造创建的对象时会发生对空指针的解引用

实现有参构造时,要注意多来一个空间给\0,因为size和capacity都是不包含\0的。?

string()
{
	_str = new char('\0');
	_size = 0;
	_capacity = 0;
}

string(const char* s)
{
	size_t len = strlen(s);
	_str = new char[len + 1];
	_size = len;
	_capacity = len;

	strcpy(_str, s);
}

我们也可以将两者合并,写一个带缺省值的有参构造:

string(const char* s = "")
{
	size_t len = strlen(s);
	_str = new char[len + 1];
	_size = len;
	_capacity = len;

	strcpy(_str, s);
}

我们给了一个空的常量字符串,而常量字符串结尾会自带一个\0,所以上面的写法就涵盖了之前的两种写法。

2.析构函数

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

3.c_str()/size()/capacity

const char* c_str() const
{
	return _str;
}

size_t size() const
{
	return _size;
}

size_t capacity() const
{
	return _capacity;
}

4.迭代器

迭代器iterator其实是一种封装的体现。每一个容器都有iterator,但是每一个容器的底层的实现又是不一样的。而且iterator的实现也是不固定的,有可能时原生指针,也有可能是其他的机制来实现的。

所以每一个容器中的iterator其实是typedef出来的,实现一个统一的接口,让每一个容器的遍历方式都变得相同。

string的模拟实现过程中,可以用原生指针来实现迭代器,因为可以发现,在string中,迭代器其实是在模拟指针的行为。

typedef char* iterator;
typedef const char* const_iterator;

iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}


const_iterator begin() const
{
	return _str;
}

const_iterator end() const
{
	return _str + _size;
}

string

迭代器实现后,范围for也就可以使用了。这也就从侧面反映出范围for的底层是迭代器。?

5.operator[]?

对于string的访问来说,没必要实现其他的接口,只需要实现该接口即可。

char& operator[](size_t pos)
{
	assert(pos < _size && pos >= 0);

	return _str[pos];
}

const char& operator[](size_t pos) const
{
	assert(pos < _size && pos >= 0);

	return _str[pos];
}

6.clear()

void clear()
{
	_str[0] = '\0';
	_size = 0;
}

7.尾插

push_back及reserve

在每一次插入前都要先判断是否还有容量,如果容量满了就需要扩容?

扩容的操作交给reserve来实现,每次开空间时都要多开一个空间给\0。因为在VS环境下,n

void string::reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];
		_capacity = n;
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;
	}
}

void string::push_back(char ch)
{
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}

	_str[_size] = ch;
	++_size;
	_str[_size] = '\0';
}

append

实现append时,不能直接2倍扩容,如果s很长,扩大2倍可能还是不够,所以就要判断目前的长度加上s的长度与2*capacity谁大,来判断扩多少。?

void string::append(const char* s)
{
	size_t len = strlen(s);

	if (len + _size > _capacity)
	{
		reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
	}
	_size += len;
	strcpy(_str + _size, s);
}

operator+=

实现+=时,可以直接复用前面的尾插操作。?

string& string::operator+=(char ch)
{
	push_back(ch);

	return *this;
}
string& string::operator+=(const char* s)
{
	append(s);

	return *this;
}

?8.insert

在指定位置插入一个字符

void string::insert(size_t pos, char ch)
{
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}
	size_t end = _size + 1;
	while (end >= pos + 1)
	{
		_str[end] = _str[end - 1];
		--end;
	}

	_str[pos] = ch;
	++_size;
}

在指定位置插入一个字符串

void string::insert(size_t pos, const char* s)
{
	size_t len = strlen(s);

	if (len + _size > _capacity)
	{
		reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
	}

	size_t end = _size + len;
	while (end >= pos + len)
	{
		_str[end] = _str[end - len];
		--end;
	}

	size_t i = 0;
	for (; i < len; i++)
	{
		_str[pos + i] = s[i];
	}

	_size += len;
}

9.erase

void string::erase(size_t pos, size_t len)
{
	assert(pos < _size);

	if (len > _size - pos)
	{
		_str[pos] = '\0';
	}
	else
	{
		size_t i = pos + len;
		for (; i <= _size; ++i)
		{
			_str[i - len] = _str[i];
		}
	}
	
	_size -= len;
}

10.find/substr

实现substr时,这里返回了一个临时变量,出了作用域就销毁了,实际上会返回由这里临时变量拷贝出来的中间变量,但是这里默认实现的是浅拷贝。而且string类里面涉及到了资源的申请,实现浅拷贝的话,就会导致对同一块空间多次释放。

所以这里还需要实现拷贝构造来实现深拷贝。?

size_t string::find(char ch, size_t pos)
{
	assert(pos < _size);

	size_t i = pos;
	for (; i < _size; ++i)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}
	
	return -1;
}

size_t string::find(const char* s, size_t pos)
{
	assert(pos < _size);

	char* tmp = strstr(_str + pos, s);
	if (tmp == nullptr)
	{
		return -1;
	}
	return tmp - _str;
}

string string::substr(size_t pos, size_t len)
{
	string ans;

	if (len > _size - pos)
	{
		len = _size - pos;
	}

	ans.reserve(len);

	size_t i = 0;
	for (; i < len; i++)
	{
		ans += _str[i+pos];
	}

	return ans;
}

拷贝构造

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

赋值运算符重载

//s1 = s
string& operator=(const string& s)
{
	//避免自己对自己赋值
	if (this != &s)
	{
		delete[]_str;

		_str = new char[s._capacity + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;
}

11.Cmpare

bool operator<(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) < 0;
}

bool operator<=(const string& s1, const string& s2)
{
	return s1 < s2 || s1 == s2;
}

bool operator>(const string& s1, const string& s2)
{
	return !(s1 <= s2);
}

bool operator>=(const string& s1, const string& s2)
{
	return !(s1 < s2);
}

bool operator==(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) == 0;
}

bool operator!=(const string& s1, const string& s2)
{
	return !(s1 == s2);
}	

12.流插入和流提取

在实现流提取时,为了避免多次扩容,可以开一个数组先用来存储数据,等数组满了在插入string中,减少扩容次数。

ostream& operator<<(ostream& out, const string& s)
{
	for (auto ch : s)
	{
		out << ch;
	}

	return out;
}

istream& operator>>(istream& in, string& s)
{
	s.clear();

	const int N = 256;
	char buff[N];
	int i = 0;

	char ch;
	//in >> ch;
	ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == N - 1)
		{
			buff[i] = '\0';
			s += buff;

			i = 0;
		}

		//in >> ch;
		ch = in.get();
	}

	if (i > 0)
	{
		buff[i] = '\0';
		s += buff;
	}

	return in;
}

四.现代写法?

拷贝构造现代写法

void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

//s1(s)
string(const string& s)
{
	string tmp(s.c_str());
	swap(tmp);
}

现代写法与传统写法的区别就在于:传统写法是自己开空间自己挪动数据;现代写法则是将这些过程都交给tmp去干。

我们用我们用s的内容去构造一个string对象,此时tmp就有和s一样大的空间和内容。此时我们再交换tmp和this的内容、size和capacity即可。

string

需要注意的是,tmp交换之后就得到this的值,而this还没有创建是随机值,tmp出了作用域就会销毁,此时就会导致对野指针进行销毁。为了避免这样,我们可以在声明私有成员变量时,给出缺省值

string


赋值运算符重载的现代写法?

//s1 = s
string& operator=(const string& s)
{
	string tmp(s.c_str());
	swap(tmp);

	return *this;
}

之前依旧时自己开空间拷贝数据,现代写法用一个tmp来帮忙实现之前需要自己干的事情。对于赋值运算符重载来说,s1要先释放自己之前的空间,再开一个和s一样大的空间和内容。

先用tmp创建一个和s一样的string对象,然后交换s1和tmp,此时s1就完成了目的,然后在该函数结束之后,tmp就会销毁,而它现在指向的又是s1之前的空间,刚好销毁了,不会造成内存泄露的问题。string?

//s1 = s
string& operator=(string s)
{
	swap(s);

	return *this;
}

?现代写法可以再进行简化:此时传参不再传引用,而只是s的拷贝,此时直接交换s1和s,s1就得到了目标内容,而s得到了s1需要释放的空间,而s又是临时变量,出了作用域会销毁,会带走s1之前的空间,依旧不会造成内存泄露。

string


swap

在实现现代写法时,涉及到了交换,这个交换是我们自己实现的,为什么不直接调用库里面的swap呢?string

库里面swap实现成了模板,当我们直接调用时,在该函数中会发生三个深拷贝。而我们用其交换自定义类型时就不会产生太多消耗。

但其实并不需要太多担心,当我们调用库里的swap时,其会调用swap(string& str).string

五.引用计数和写时拷贝

浅拷贝的问题在于:

1、同一块空间析构两次或多次

2、修改一个另一个也会跟着改变

引用计数是指,一开始并不采用深拷贝,而是浅拷贝。只不过此时有一个count变量用来记录有多少个对象指向该空间string

?当我们要析构s2时,不会直接析构该空间,而是让count--,直到count等于0时,才会真正释放该空间。

而当我们要对s2进行修改时,为了不改变s1,此时才会进行深拷贝,让s2指向另一块空间,这时候修改变不会影响s1。

引用计数和写时拷贝很好的解决了浅拷贝出现的问题。但是这在现在已经慢慢退出使用了。

VS2022环境下:

可以看到它们指向的地址并不相同,所以并没有采用引用计数和写时拷贝这种方式

string

g++环境下: 在g++环境下,我们可以看到,s1和s2指向的是同一块堆空间。

string

当我们对s2进行修改时,此时就会发生深拷贝,s2不在指向该地址:?

string

六.编码

编码其实是在计算机中,值和符号的一种映射关系。在计算机中只能存储010101这样的整数,然而我们需要经常表示字母符号中文等等。

Ascii码值就是一种编码,它将英文中常用的字母符号数字等等用整数来表示。string

我们看到的abc在内存中其实就是97、98、99等数字,只不过在内存中是以十六进制存储的。?

而我们现在常用的就是Unicode(统一码、万国码)。它为世界上所有的文字和符号提供一个统一且唯一的编码的标准。

而其中包括了三种编码方式:UTF-8,UTF-16,UTF-32

最常用的是UTF-8,它是一种变长编码。string?它即可以表示字母,也可以表示中文等其他符号。中文通常用2个或者3个字节来表示。不同的符号用不同的字节数来表示。

UTF-16则都用两个字节来表示

UTF-32?则都用四个字节来表示

上一篇:360手机奇酷旗舰极客版(360手机奇酷旗舰极客版搭载安卓5.1界面是什么)
下一篇:oppok7多少w闪充(oppok7充电头是多少瓦的)