【字符串探秘:手工雕刻的String类模拟实现大揭秘】

news2024/11/15 4:22:50

【本节目标】

  • 1. string类的模拟实现

  • 2.C++基本类型互转string类型

  • 3.编码表 :值 --- 符号对应的表

  • 4.扩展阅读

1. string类的模拟实现

1.1 经典的string类问题

上面已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己 来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。大家看下以 下string类的实现是否有问题?为了防止和库里面的string类发生冲突,我们在这里使用命名空间来限制我们写的string类。

构造函数

namespace yu
{
	class string
	{
	public:
		string(const char* str)
			:_str(str)
		{}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

上面的代码有什么问题吗?

我们之前提到权限可以缩小,可以平移,但是就是不能放大,那我们下面的写法还有错误吗?

namespace yu
{
	class string
	{
	public:
		string(const char* str)
			:_str(str)
		{}
	private:
		const char* _str;
		size_t _size;
		size_t _capacity;
	};

	void test()
	{
		string str("hello world");
	}
}

这里也是不可以的,因为常量字符串存在代码区,只能可读,不能写,那我们上面就只能完成一个打印输出的工作,不能完成扩容,修改等其他增删改操作。所以我们可以开辟一个同样的空间

namespace yu
{
	class string
	{
	public:
		string(const char* str)
			//strlen求取'\0'之前字符的个数
			:_str(new char[strlen(str)+1])
			,_size(strlen(str))
			//capacity是存储有效字符的个数,不包括'\0'
			,_capacity(strlen(str))
		{}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

	void test()
	{
		string str("hello world");
	}
}

但是上面strlen这个需要计算3次,而且strlen的实践复杂度是O(N),所以我们写成下面的形式。

namespace yu
{
	class string
	{
	public:
		string(const char* str)
			:_size(strlen(str))
			//capacity是存储有效字符的个数,不包括'\0'
			,_capacity(_size)
			//strlen求取'\0'之前字符的个数
			,_str(new char[_capacity + 1])
		{}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

	void test()
	{
		string str("hello world");
	}
}

随后我们写一下c_str函数,看看是否打印输出成功。

namespace yu
{
	class string
	{
	public:
		string(const char* str)
			:_size(strlen(str))
			//capacity是存储有效字符的个数,不包括'\0'
			,_capacity(_size)
			//strlen求取'\0'之前字符的个数
			,_str(new char[_size + 1])
		{
			strcpy(_str, str);//拷贝
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

	void test()
	{
		string str("hello world");
		cout << str.c_str() << endl;
	}
}
int main()
{
	yu::test();
	return 0;
}

此时我们的程序发生了崩溃,因为初始化的顺序和声明的顺序一致,所以程序会先执行_str(new char[_capacity + 1]),但是此时_capacity还没有初始化,此时编译器可能给了随机值或者0。

那么此时开的空间就只有1个字符的空间,开空间小导致拷贝时程序报错。

那怎么解决呢?我们可以初始化的顺序和声明的顺序一致。

但是这样的写法不好,我们这里可以不使用初始化列表,可以使用函数体内初始化。

namespace yu
{
	class string
	{
	public:
		string(const char* str)
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];

			strcpy(_str, str);//拷贝
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

	void test()
	{
		string str("hello world");
		cout << str.c_str() << endl;
	}
}
int main()                                         
{
	yu::test();
	return 0;
}

我们再来来实现一下析构函数

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

string类里面还提供了无参的构造函数

namespace yu
{
	class string
	{
	public:
		string()
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{}
		string(const char* str)
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];

			strcpy(_str, str);//拷贝
		}

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

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

	void test()
	{
		string str;
		cout << str.c_str() << endl;
	}
}
int main()                                         
{
	yu::test();
	return 0;
}

我们这里程序又崩溃了,为什么?cout在识别到char *类型的时候,会认为当前输出的是字符串,会进行解引用行为,这里报错就是空指针解引用的原因。所以我们这里可以设置一个空间存储'\0'

string()
	:_str(new char[1])
	,_size(0)
	,_capacity(0)
	{
		_str[0] = '\0';
	}

但是实践上我们一般写成全缺省构造函数,不分别写有参和无参两种形式。

//string(const char* str = nullptr)//error:strlen(nullptr)会报错
//string(const char* str = '\0')//error:char不能给char*
string(const char* str = "")//常量字符串默认结尾是\0
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];

	strcpy(_str, str);//拷贝
}

再来实现一下size和[ ]操作符重载,库中我们还实现了cosnt[ ]操作符重载形式,这种形式函数内部未对对象(*this)作出改变,所以可以加上const。

//不包括'\0'
size_t size() const
{
	return _size;
}

//返回pos位置值的引用
//	1.减少拷贝
//  2.修改返回值
char& operator[](size_t pos) 
{
	//这里可以=因为\0处也有空间
	//hello world\0
	//\0位置处的下标就是_size
	assert(pos <= _size);
	return _str[pos];
}
const char& operator[](size_t pos) const
{
	//这里可以=因为\0处也有空间
	//hello world\0
	//\0位置处的下标就是_size
	assert(pos <= _size);
	return _str[pos];
}

函数内部未对对象(*this)作出改变,所以可以加上const,我们可以验证一下。

void test()
{
	string str("hello world");

	for (size_t i = 0; i < str.size(); i++)
	{
		str[i]++;
	}
	cout << str.c_str() << endl;
}

运行结果:

除了上面的[ ]可以遍历和修改,迭代器也可以修改,我们来模拟实现一下。

typedef char* iterater;
iterater begin()
{
    //第一个字符位置是begin
	return _str;
}
iterater end()
{
    //\0位置就是end
	return _str + _size;
}

我们来测试一下

void test()
{
	string str("hello world");

	string::iterater it = str.begin();
	while (it != str.end())
	{
		cout << *it;
		it++;
	}
}

运行结果:

但是上面这种写法只适合底层空间连续,后面遇到不连续的我们就要修改写法,除了上面的打印工作,我们还有范围for。

for (auto ch : str)
{
	cout << ch;
}

其实范围for底层也是用的迭代器,通过反汇编我们可以看到。

如果我们上面把begin变成Begin,此时范围for就会报错,因为范围for是傻瓜式的替换成迭代器,只有我们自定义写的迭代器没有按照规则命名,范围for就不能使用。

我们再来实现一下打印输出的工作

void print_str(const string& s)
{
	for (size_t i = 0; i < s.size(); i++)
	{
		s[i]++;
	}
	cout << s.c_str() << endl;
}

但是我们发现我们的代码出现错误了,为什么?

因为我们上面的size和[ ]操作符重载传入的对象是非const类型的,而我们的打印输出是const类型的,这里会存在权限放大的方法,所里这里会报错,所以size和c_str函数内部未对对象(*this)作出改变,所以可以加上const。而[ ]操作符重载可以使用cosnt版本的。 

void print_str(const string& s)
{
	for (size_t i = 0; i < s.size(); i++)
	{
		//s[i]++;//此时是const,也就不能修改
		cout << s[i];
	}
	cout << endl;
}

运行结果:

我们再将迭代器放入刚刚的输出打印函数,我们发现也出现了同样的问题。

所以这里要使用const迭代器,所以我们要实现一下。

typedef const char* const_iterater;
const_iterater begin() const
{
	return _str;
}
const_iterater end() const
{
	return _str + _size;
}

因此我们的程序就可以正常输出,但是此时指针指向的内容不可被改变。

我们再来实现一下string类的增删查改。字符串的增加操作必定都要开空间,对于字符串追加的函数,我们这里不能实现每次开2倍的空间操作,如果要追加的字符串的长度过长,开辟的空间必定不够,因此这里我们先实现reserve函数,解决空间开辟的问题。

void reserve(size_t n)
{
	if (n > _capacity)
	{
		//扩容步骤
		/*
			1.开辟空间
			2.拷贝数据
			3.释放旧空间
			4.指向新空间
		*/
		char* tmp = new char[n + 1];//多开一个给'\0'的位置
		strcpy(tmp, _str);//会拷贝'\0'
		delete[] _str;
		_str = tmp;
		_capacity = n;
	}
	//不缩容
	return;
}

现在预备条件已经写好了,我就可以开始写轮子了。

void append(const char* str)
{
	size_t len = strlen(str);
	//这里都不包含'\0',因此可以不用处理
	//而且我们开空间都给\0开好了位置
	//空间永远都比capacity多一个
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	strcpy(_str + _size, str);
	_size += len;
	//这里插入的str字符串已经拷贝过来\0,就不需要单独处理了
}
void push_back(char ch)
{
	if (_size == _capacity)
	{
		//当上面构造函数没有传参时,capacity值为0,这里需要单独处理一下
		size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newCapacity);
	}
	_str[_size] = ch;
	++_size;
	_str[_size] = '\0';//处理\0
}
//这里我们就可以复用上面的接口
string& operator+=(const char* str)
{
	append(str);
	return *this;
}
string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}

验证一下

void test()
{
	string str("hello world");
		
	str += '!';
	str += "!!";

	print_str(str);
}

运行结果:

我们再来实现一下插入函数

void insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		//当上面构造函数没有传参时,capacity值为0,这里需要单独处理一下
		size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newCapacity);
	}
	size_t end = _size;
	while (end >= pos)
	{
		//依次向后挪动
		_str[end + 1] = _str[end];
		--end;
	}
	_str[pos] = ch;
	++_size;
	//挪动的时候挪动了'\0'
	//_str[_size] = '\0';这里不用处理
}

void test()
{
	string str("hello world");
		
	str += '!';
	str += "!!";

	print_str(str);
	cout << endl;
	str.insert(5, '*');
    str.insert(5, '*');
    str.insert(5, '*');
	print_str(str);
}

运行结果:

我们看一下我们的代码有没有什么问题?我们试一下我们的头插:str.insert(0, '*');

 头插的时候,end减到为0,下次减减的时候减到-1,但是此时end为size_t,会变成整型的最大值,因此程序会一直运行,进入死循环,那我们将上面的end变量类型变为int呢?我们来看一下下面的程序。

上面的结果为什么是yes!当无符号整型和有符号整型比较时,有符号整型会隐式提升为无符号整型,-1此时就能转化为整型的最大值。所以将上面的end变量类型变为int也不行,因为我们的pos也是无符号整型,不过我们可以通过强制类型转换解决。

void insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		//当上面构造函数没有传参时,capacity值为0,这里需要单独处理一下
		size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newCapacity);
	}
	int end = _size;
	while (end >= (int)pos)
	{
		//依次向后挪动
		_str[end + 1] = _str[end];
		--end;
	}
	_str[pos] = ch;
	++_size;
	//挪动的时候挪动了'\0'
	//_str[_size] = '\0';这里不用处理
}

或者也可以这样写,这样end变量的值只能减到0,就不会出现上面的错误。

void insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		//当上面构造函数没有传参时,capacity值为0,这里需要单独处理一下
		size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newCapacity);
	}
	size_t end = _size + 1;
	while (end > pos)
	{
		//依次向后挪动
		_str[end] = _str[end - 1];
		--end;
	}
	_str[pos] = ch;
	++_size;
	//挪动的时候挪动了'\0'
	//_str[_size] = '\0';这里不用处理
}

我们再来实现一下字符串的插入。

void insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	int end = _size;
	while (end >= (int)pos)
	{
		_str[end + len] = _str[end];
		--end;
	}
	strncpy(_str+pos,str,len);
}

这里需要注意不能使用strcpy,因为它会拷贝'\0',会覆盖后面的'w'字符。接下来我们再实现一下删除,库里面接口为我们提供了缺省值npos,它属于静态成员变量,类里面定义,类外初始化。

private:
	char* _str;
	size_t _size;
	size_t _capacity;

	//不用给缺省值
	static size_t npos;//不会初始化列表,属于整个类,属于所有对象
};

size_t yu::string::npos = -1;

但是我们发现库里面是这样写的。

我们发现当我们给双精度浮点型的时候程序报错了,error C2864: yu::string::x: 带有类内初始化表达式的静态 数据成员 必须具有不可变的常量整型类型,或必须被指定为“内联”。对于上面这种写法只能支持整型变量。上面的这种写法可以算是编译器的特殊处理,此时的npos只读,不能修改,不能加加减减。

void erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);
	if (len == npos || pos + len >= _size)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
}

测试一下:

void test()
{
	string str("hello world");
	print_str(str);
	cout << endl;
	str.erase(5, 1);
	print_str(str);
	cout << endl;
	str.erase(2);
	print_str(str);
}

运行结果:

我们看一下上面判断的条件,第二个条件是否可以覆盖第一个条件。这里是不能的,npos已经是最大值了,如果再加上pos就会溢出,程序就会有问题。我们再来写来写一下交换的成员函数。

void swap(string& str)
{
	//交换指向的内容即可
	std::swap(str._str, _str);
	std::swap(str._size, _size);
	std::swap(str._capacity, _capacity);

	//std::swap(str, *this);效率较低,拷贝构造消耗大
}

我们来看一下库中这几个函数的区别,如果没有第二个swap函数,就只能调用第三个swap函数,第三个函数首先要拷贝,然后赋值,这里都是深拷贝,要开空间区拷贝,代价大。                         

我们再来实现一下find函数。

size_t find(char ch, size_t pos = 0)
{
	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == ch)
			return i;
	}
	return npos;
}
size_t find(const char* str, size_t pos = 0)
{
	//暴力匹配
	const char* ptr = strstr(_str + pos, str);
	if (ptr == nullptr)
	{
		return npos;
	}
	else
	{
		//指针相减是元素之间的个数
		return ptr - _str;
	}
}

测试一下:

void test1()
{
	string str("hello world");
	cout << str.find('h') << endl;
	cout << str.find("lo") << endl;
}

运行结果:

我们再来实现一下substr函数。

string substr(size_t pos = 0, size_t len = npos)
{
	assert(pos < _size);
	size_t end = pos + len;
	if (len == npos || pos + len >= _size)
	{
		end = _size;
	}
	string str;
	str.reserve(end - pos);
	//小于后面字符的个数
	for (size_t i = pos; i < end; i++)
	{
		str += _str[i];//会自动处理'\0'
	}
	return str;
}

我么们来看一下我们的代码有没有什么问题?

这里主要就是浅拷贝的问题,临时对象和str对象指向了同一块空间。

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

string(const string& s)//拷贝构造 - 深拷贝
{
	_str = new char[s._capacity + 1];
	strcpy(_str, s._str);
	_size = s._size;
	_capacity = s._capacity;
}

通过深拷贝就可以解决问题指向同一块空间。

那赋值的深拷贝呢?

string& operator=(const string& s)//赋值 - 深拷贝
{
	if (this != &s)
	{
		char* tmp = new char[s._capacity + 1];
		strcpy(tmp, s._str);
		delete[] _str;
		_str = tmp;
		_size = s._size;
		_capacity = s._capacity;
	}
}

此时也能解决问题。

我们上面都是通过C语言的形式中print_str或者str.c_str()输出我们的字符串的,我们实现一下直接使用C++流插入操作符来输出字符串。

//流插入和流提取重载在全局
ostream& operator<<(ostream& out, const string& s)
{
	for (auto ch :s)
	{
		out << ch;
	}
	return out;
}

再来实现一下流提取。

// 这里会修改s,所以不用带上const
istream& operator>>(iostream& in, string& s)
{
	char ch;
	in >> ch;
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		in >> ch;
	}
}

我们来测试一下

我们发现我们的程序无论是输入空格或者换行都不能结束。我们来测试一个数据,当什么输入"12345空格6",安装我们想要的逻辑,cin应该只会读取到”123345”。我们来一下我们的调式结果。

按照常理,输入到'5'字符后应该输入' ',但是上面的调式结果却跳过了空格,而是输出字符'6',ch没有拿到空格。

我们来看一下cin的特性,当我们连续往两个字符中写入'x',' '和'y',cin会将空格当为分隔符,会忽略这个空格,从而直接读取后续的字符,也就是读取'y','\n'也是如此,我们上面的程序是将字符串分为一个个字符,然后字符依次输入,此时就是cin输入单个字符,遇到空格或者换行就会被忽略掉,所以上面的程序无论输入什么都不会被结束,因为拿不到空格或者换行,我们可以使用getchar来获取单个字符的输入,但是我们今天学习的是C++语言,最好应用C++函数。

我们这里使用我们的C++istream提供的get函数,它通常不会跳过分隔符或空白字符,而是将其留在输入流中,因此我们就可以修改我们的流提取重载了。

// 这里会修改s,所以不用带上const
istream& operator>>(iostream& in, string& s)
{
	char ch = in.get();
	//in >> ch;//拿不到空格或者换行
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = in.get();
	}
	return in;
}

然后我们再来测试一下,输入"12345 678"。

结果确实被显示出来了,并且" 678"被输入到我们的缓冲区从而没有被显示在显示器上,但是我们发现我们的流提取重载并没有清空之前存在的字符串,因为我们实现的时候是使用了+=重载,如果要输入的字符串之前没有清空,那么后续输入的字符串就会在之前的字符串上追加。但是我们的库函数就是直接输入什么字符串就会显示什么字符串。

所以在输入字符串之前,如果之前的字符串还有内容,我们就要清空,所以我们要实现一下我们的clear函数。

void clear()
{
	//删除数据,但是不会释放空间
	_size = 0;
	_str[_size] = '\0';
}

注意:这里我们一定要将0位置处设置为'\0',否则就会出现错误。

然后再来改造一下我们的流提取重载。

// 这里会修改s,所以不用带上const
istream& operator>>(iostream& in, string& s)
{
	//清空历史数据
	s.clear();

	char ch = in.get();
	//in >> ch;//拿不到空格或者换行
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = in.get();
	}
	return in;
}

流提取重载这样就写好了,但是还是有一个小问题,如果我们要输入的字符非常长,那我们就要经过多次开辟空间,这样消耗很大,那我们可以用reserve提前开辟空间吗?不行,因为我们不确定用户要输入的字符到底有多长,同时我们也不能获取缓冲区输入字符的长度,这里就有人设计出了一个字符数组来解决这个问题,我们来看看是怎么设计的。

istream& operator>>(iostream& in, string& s)
{
	//清空历史数据
	s.clear();
	char buff[128];
	char ch = in.get();
	int i = 0;
	//in >> ch;//拿不到空格或者换行
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;//该数组出了作用域就被销毁
		if (i == 127)
		{
			//下标为127,此时数组就有128个元素
			buff[i] = '\0';
			s += ch;
			i = 0;
		}
		ch = in.get();
	}
	//i没有走到127的情况
	if (i > 0)
	{
		buff[i] = '\0';
		s += buff;
	}
	return in;
}

1.2.浅拷贝问题

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

就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一不想分享就 你争我夺,玩具损坏。

可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩 子都买一份玩具,各自玩各自的就不会有问题了。

1.3 深拷贝问题

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

1.4.传统版写法的String类的拷贝构造和赋值拷贝

string(const string& s)//拷贝构造 - 深拷贝
{
	_str = new char[s._capacity + 1];
	strcpy(_str, s._str);
	_size = s._size;
	_capacity = s._capacity;
}
string& operator=(const string& s)//赋值 - 深拷贝
{
	if (this != &s)
	{
		char* tmp = new char[s._capacity + 1];
		strcpy(tmp, s._str);
		delete[] _str;
		_str = tmp;
		_size = s._size;
		_capacity = s._capacity;
	}
}

1.5.现代版写法的String类的拷贝构造和赋值拷贝

string(const string& s)//拷贝构造 - 深拷贝
{
	string tmp(s.c_str());
	swap(tmp);
}

但是这里有些编译器可能会出现一个问题,s2和tmp交换,s2确实获得了tmp的数据,但是tmp交换之后指向谁呢?此时我们需要让他指向nullptr。

再来看一下赋值拷贝的现代写法。

// s1 = s3
string& operator=(string s)//赋值 - 深拷贝
{
	swap(s);
    return *this;
}

这里不能加引用,因为加上引用s就是s3的别名,此时就是交换是s1和s3。此时不带上引用,s对象是通过s3对象拷贝构造的,然后s再和s1交换,s交换后指向s1,当s出了作用域,就会调用析构函数释放空间。

注意:此时我们就不用考虑自己给自己赋值的问题,因为此时我们传参的时候已经拷贝构造了。如果要考虑的话就要这样写

string& operator=(const string& s)//赋值 - 深拷贝
{
	if (this != &s)
	{
		string tmp(s);
		swap(tmp);
	}
}

使用上面的赋值现代写法不能兼容我们的substr函数,substr函数返回的是str的临时拷贝,临时拷贝具有常属性,而赋值现代写法参数是非cosnt,这里会存在权限放大的原因,所以会出现错误。

2.C++基本类型转string函数

3.编码表 :值 --- 符号对应的表

计算机中数据都是由二进制存储的,那我们怎么通过这些01序列分辨出我们的数据是什么呢?计算机中为我们提供了ASCII表。

比如今天我们要存储"apple!",实际上计算机存储的就是97 112 112 108 101 33 0这几个序列,我们来验证一下。

 此时的计算机只能存储显示英文,那怎么显示其他国家的语言呢?以我们国家为例。也要对应的值和对应的符号相对于起来,但是中国文化上下五千年文明,博大精深,如果我们国家也用8个比特位,一个字节表示一个符号,也就是256中符号肯定不能存储文明国家所有的文字,所有我们国家就用两个字节到四个字节表示一个文字,一般常见的汉字可以考虑用两个字节对应一个汉字,所以这样就有256*256个表示情况,微软平台使用的都是GBK编码。

我们来看一下内存中是怎么样的,通过两个字节去编码表找对应的汉字。

我们可以再来看一下编码表的顺序。

编码表不是乱编的,是按照一定顺序编码的,将同音字编码在一起。我们国家的编码表是兼容SASCII表的,但是当同时出现中文和英文,我们国家的编码表怎么识别呢?它是当成两个字节去国家的编码去寻找呢?还是当成一个字节去寻找呢?在双字节编码表中,英文字符会占用一个字节,而中文字符会占用两个字节。在处理文本时,系统可以通过检查字节的高位信息来确定是一个英文字符还是一个中文字符,然后再在编码表中找到对应的字符。但是其他国家语言呢?还需要兼容其他国家的编码表,太繁琐了,于是就衍生出来万国码。

        "万国码" 广泛指的是 Unicode(统一码),而不是特指某一种具体的编码。Unicode 是一种用于文本字符的国际化标准,目的是为了能够涵盖全球范围内的所有语言和符号。

编码方式:

  • UTF-8: 是一种可变长度编码方式,使用1到4个字节来表示字符。对于ASCII字符,使用一个字节表示,对于其他字符,使用更多的字节。

  • UTF-16: 使用16位(2字节)来表示一个字符。基本多文本平面(BMP)上的字符使用16位表示。

  • UTF-32: 是一种固定长度编码,每个字符使用32位(4字节)表示,不论字符在Unicode中的位置。

Linux下一般都使用UTF-8编码。

C++标准库提供了多个字符串类型(stringwstringu16stringu32string)以适应不同的字符编码需求。这些字符串类型是为了支持不同的字符集和编码方式:

  1. string

    • std::string 是标准 C++ 中用于存储单字节字符的字符串类型。它使用了默认的字符集(通常是 ASCII 或 UTF-8)。
  2. wstring

    • std::wstring 是宽字符字符串类型,在 Windows 平台上通常使用。它使用 wchar_t 类型存储字符,这个类型在不同的编译器和平台上可能占据不同的字节大小(例如,Windows 上通常是 2 字节,而在 Linux 上可能是 4 字节)。
  3. u16string

    • std::u16string 是存储 UTF-16 编码的字符串类型,每个字符通常占用 2 个字节。
  4. u32string

    • std::u32string 是存储 UTF-32 编码的字符串类型,每个字符通常占用 4 个字节。

        这些字符串类型的选择取决于需要处理的文本数据的特定要求。在多语言环境中,特别是处理 Unicode 字符时,选择适当的字符串类型非常重要。例如,如果需要处理表情符号、不同语言的字符集或者需要支持各种语言的国际化应用程序,那么使用宽字符或者 UTF-16/UTF-32 编码的字符串类型可能更为适合。

4.扩展阅读

面试中string的一种正确写法

STL中的string类怎么了?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1272505.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Panalog 日志审计系统 前台RCE漏洞复现

0x01 产品简介 Panalog是一款日志审计系统&#xff0c;方便用户统一集中监控、管理在网的海量设备。 0x02 漏洞概述 Panalog日志审计系统 sy_query.php接口处存在远程命令执行漏洞&#xff0c;攻击者可执行任意命令&#xff0c;接管服务器权限。 0x03 复现环境 FOFA&#xf…

突发,合肥一废品回收站发生火灾,富维AI神器助力防灾

昨晚&#xff0c;合肥一废品回收站突发火灾&#xff0c;火光冲天&#xff0c;烟雾蔓延。幸亏及时发现&#xff0c;消防人员迅速到场&#xff0c;控制了火势。这起事件让我们再次认识到火灾报警的重要性。而在这方面&#xff0c;北京富维图像公司的FIS智能图像识别系统就发挥了巨…

如何使用Qchan搭建更好保护个人隐私的本地图床并在公网可访问

文章目录 前言1. Qchan网站搭建1.1 Qchan下载和安装1.2 Qchan网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar云端设置2.2 Cpolar本地设置 3. 公网访问测试总结 前言 图床作为云存储的一项重要应用场景&#xff0c;在大量开发人员的努力下&#xff0c;已经开发出大…

PowerDesigner数据库建模软件的安装

解压&#xff1a; 解压好以后&#xff0c;点击PowerDesigner.exe安装 这个安装的版本是15 选择安装路径&#xff0c;可以默认可以自定义&#xff1a; 直接点next&#xff1a; 全选了 点击next&#xff1a; 点击next&#xff1a; 点finish 汉化&#xff1a; 先把pojie和汉化文件…

STM32 CUBEIDE Outline is disabled due to scalability mode

项目场景&#xff1a; 问题描述 Outline is disabled due to scalability mode 看不到函数 解决方案&#xff1a;

【JavaEE】多线程 -- 死锁问题

目录 1. 问题引入 2.死锁问题的概念和原因 3. 解决死锁问题 1. 问题引入 在学习死锁之前, 我们先观察下面的代码能否输出正确的结果: 运行程序, 能正常输出结果: 这个代码只管上看起来, 好像是有锁冲突的, 此时的 locker 对象已经是加锁的状态, 在尝试对 locker 加锁, 不应该…

DCAMnet网络复现与讲解

距论文阅读完毕已经过了整整一周多。。。终于抽出时间来写这篇辣&#xff01;~ 论文阅读笔记放这里&#xff1a; 基于可变形卷积和注意力机制的带钢表面缺陷快速检测网络DCAM-Net&#xff08;论文阅读笔记&#xff09;-CSDN博客 为了方便观看&#xff0c;我把结构图也拿过来了。…

java+springboot物流管理系统设计与实现wl-ssmj+jsp

物流管理系统的开发和综合性的物流信息网站平台的建设。研究的重点是运输管理信息系统&#xff0e;本系统是一套基于运输作业流程的管理系统&#xff0c;该系统以运输任务、货品、商务三大线索设计开发。运输任务是该管理系统的核心&#xff0c;系统通过对运输任务中的接收、调…

智能优化算法应用:基于树种算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于树种算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于树种算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.树种算法4.实验参数设定5.算法结果6.参考文献7.MATLAB…

渗透测试|HW蓝队

公众号&#xff1a;老油条运维 记录某个对某个钓鱼事件中获取的钓鱼样本进行分析&#xff0c;以及简单的制作学习 样本行为分析 首先看到是 qq 邮箱发来的某个压缩包大概本身是带密码的&#xff0c;反手就丢到虚拟机先看下大概文件&#xff0c;解压后是这样的一个快捷方式 然…

[iOS开发]UITableView的性能优化

一些基础的优化 &#xff08;一&#xff09;CPU 1. 用轻量级对象 比如用不到事件处理的地方&#xff0c;可以考虑使用 CALayer 取代 UIView CALayer * imageLayer [CALayer layer]; imageLayer.bounds CGRectMake(0,0,200,100); imageLayer.position CGPointMake(200,200…

windows文件删除权限

一、普通文件 这里指的是所有可以被随意删除的文件。 二、可更改权限的文件 如果想要删除的文件无法被删除&#xff0c;那大概是权限不够&#xff0c;这时候&#xff1a;鼠标右键、属性、安全、编辑、选择相应的组或用户&#xff08;如果不知道哪个可以全选&#xff0c;反正…

MMdetection3.0 问题

MMdetection3.0 问题 希望各位路过的大佬指教一下&#xff1a; 问题&#xff1a; 1、NWPU-VHR-10有标注的数据一共650张&#xff0c;我将其分为了455张训练集&#xff0c;195张验证集。 2、然后使用MMdetection3.0框架中的Faster-rcnn网络进行训练&#xff0c;设置训练参数b…

【vue实战项目】通用管理系统:信息列表,信息录入

本文为博主的vue实战小项目系列中的第六篇&#xff0c;很适合后端或者才入门的小伙伴看&#xff0c;一个前端项目从0到1的保姆级教学。前面的内容&#xff1a; 【vue实战项目】通用管理系统&#xff1a;登录页-CSDN博客 【vue实战项目】通用管理系统&#xff1a;封装token操作…

Python的换行和转义:深入理解代码排版与字符串处理

更多Python学习内容&#xff1a;ipengtao.com 大家好&#xff0c;我是涛哥&#xff0c;今天为大家分享 Python的换行和转义&#xff1a;深入理解代码排版与字符串处理&#xff0c;全文2700字&#xff0c;阅读大约8分钟。 在Python编程中&#xff0c;正确使用换行和转义字符是保…

spring框架的事务传播级别经典篇

一 spring事务传播级别 1.1 总结概述 方法A:外围方法&#xff0c;方法B&#xff1a;内部方法&#xff0c;在A中调用B 1.事务级别PROPAGATION_REQUIRED&#xff1a; 如果A为PROPAGATION_REQUIRED&#xff1a;B 不管有没有设置事务级别&#xff0c;都会加入到A的事务级别中。如…

ssm+vue的仓库在线管理系统的设计与实现(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的仓库在线管理系统的设计与实现&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三…

5、DMA Demo(STM32F407)

DMA简介 DMA 全称Direct Memory Access&#xff0c;即直接存储器访问。 DMA传输将数据从一个地址空间复制到另一个地址空间。当CPU初始化这个传输动作&#xff0c;传输动作本身是由DMA控制器来实现和完成的。 DMA传输方式无需CPU直接控制传输&#xff0c;也没有中断处理方式那…

倒计时(JS计时器)

<script>function countDown() {document.body.innerHTML ;//清空页面内容var nowTimer new Date(); //现在时间的毫秒数var valueTimer new Date("2024-1-1 12:00"); //用户输入年份倒计时时间毫秒数var timer (valueTimer - nowTimer) / 1000; //倒计时秒…

网工内推 | 云计算运维,云相关认证优先,最高30K,带薪年假

01 安畅网络 招聘岗位&#xff1a;云计算运维工程师 职责描述&#xff1a; 1、负责对公有云平台的计算、存储、网络资源等IAAS/SAAS/PAAS层产品组件日常交付部署运维工作&#xff0c;包括调试、配置、维护、监控、优化等工作&#xff1b; 2、负责对操作系统及应用日常运行维护…