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

【本节目标】

  • 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位置处的下标就是_sizeassert(pos <= _size);return _str[pos];
}
const char& operator[](size_t pos) const
{//这里可以=因为\0处也有空间//hello world\0//\0位置处的下标就是_sizeassert(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()
{//第一个字符位置是beginreturn _str;
}
iterater end()
{//\0位置就是endreturn _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.hqwc.cn/news/235191.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C#测试开源运行耗时库MethodTimer.Fody

微信公众号“dotNET跨平台”的文章《一个监控C#方法运行耗时开源库》介绍了支持测量方法耗时的包MethodTimer.Fody&#xff0c;使用方便&#xff0c;还可以自定义输出信息格式。本文学习并测试MethodTimer.Fody包的使用方式。   新建控制台程序&#xff0c;通过Nuget包管理器…

Linux命令--根据端口号查看进程号(PID)

Linux命令–根据端口号查看进程号&#xff08;PID&#xff09; 查找8080端口对应的进程号: netstat -nlp|grep :8297对应的进程号1061,如果想杀掉此进程&#xff0c;可以用一下命令&#xff1a; kill -9 1061

STM32CubeIDE(CUBE-MX hal库)----定时器

系列文章目录 STM32CubeIDE(CUBE-MX hal库)----初尝点亮小灯 STM32CubeIDE(CUBE-MX hal库)----按键控制 STM32CubeIDE(CUBE-MX hal库)----串口通信 文章目录 系列文章目录前言一、定时器二、使用步骤三、HAL库实验代码三、标准库代码 前言 STM32定时器是一种多功能外设&#…

Java数据结构之《链式线性表的插入与删除》问题

一、前言&#xff1a; 这是怀化学院的&#xff1a;Java数据结构中的一道难度中等偏下的一道编程题(此方法为博主自己研究&#xff0c;问题基本解决&#xff0c;若有bug欢迎下方评论提出意见&#xff0c;我会第一时间改进代码&#xff0c;谢谢&#xff01;) 后面其他编程题只要我…

【Linux】基础IO--文件基础知识/文件操作/文件描述符

文章目录 一、文件相关基础知识二、文件操作1.C语言文件操作2.操作系统文件操作2.1 比特位传递选项2.2 文件相关系统调用2.3 文件操作接口的使用 三、文件描述符fd1.什么是文件描述符2.文件描述符的分配规则 一、文件相关基础知识 我们对文件有如下的认识&#xff1a; 1.文件 …

Youtube如何做SEO关键词挖掘

做好Youtube的SEO优化&#xff0c;可以使我们的视频得到更多的展示&#xff0c;更多的点击和观看&#xff0c;就能获得更多的粉丝和流量。一方面通过视频做引流到目标网站进行转化赚钱&#xff0c;另一方面可以通过涨粉接youtube广告赚钱。要做seo最关键的一步在于关键词的挖掘…

什么是网络可视化?网络可视化工具有用吗

网络可视化定义是自我描述的&#xff0c;因为它在单个屏幕上重新创建网络布局&#xff0c;以图形和图表的形式显示有关网络设备、网络指标和数据流的信息&#xff0c;为 IT 运营团队提供一目了然的理解和决策。 网络是复杂的实体&#xff0c;倾向于持续进化&#xff0c;随着业…

pycharm编译报错处理

1.c生成工具下载 https://visualstudio.microsoft.com/visual-cpp-build-tools/ 在这里插入图片描述 pip install pycocotools

【驱动】SPI驱动分析(四)-关键API解析

关键API 设备树 设备树解析 我们以Firefly 的SPI demo 分析下dts中对spi的描述&#xff1a; /* Firefly SPI demo */ &spi1 {spi_demo: spi-demo00{status "okay";compatible "firefly,rk3399-spi";reg <0x00>;spi-max-frequency <48…

Provisioning Profile的重要性

大家好&#xff0c;我是咕噜-凯撒。在iOS和macOS开发中&#xff0c;Provisioning Profile&#xff08;配置文件&#xff09;是一个至关重要的组成部分&#xff0c;它包含开发者证书、App ID和设备信息的文件&#xff0c;不仅用于验证应用程序的身份和权限&#xff0c;还包括了很…

软件测试面试经历和上岸后工作分享

哈喽、因为最近很多小伙伴私信问我的比较多&#xff0c;今天就专门说下&#xff0c;之前为甚转行和怎么选机构就不和大家细说了&#xff0c;之前的文章和视频也都有提到过。 今天主要是和大家说下自己转行后的感受和面试时候的一些经历&#xff0c;希望能给正在转行&#xff0c…

PCB布线为什么不能走直角或锐角-笔记

PCB布线为什么不能走直角或锐角-笔记 摘要一.PCB走线在直角转弯的地方&#xff0c;信号前后部分相互影响这几个理由我们来一一分析一下传输线的直角带来的寄生电容从阻抗的角度来看直角的尖角产生放电或者电磁辐射走线直角的工艺问题 摘要 有一定熟悉画过PCB板的人或者PCB教学…