原帖地址: LeoRanbom的博客
博主:LeoRanbom
只在原帖地址的博客上发布,其他地方看到均为爬取。
如果觉得不错希望来点个赞。
前言
结束了前2天水了一个基础的小程序,现在开始深入学习。本节我将从字符串入手开始复习,将涉及到C语言风格的字符串、C++的字符串、还有C++17在字符串方面提出的新标准新特性。
1.动态字符串
在接触了java、Python等面向对象的语言,它们的字符串有很多方便的特性:可拓展至任意大小,提取或替换子字符串。但转过头来看c语言就会感觉特别地蛋疼。它连边界检测都没有。
1.1.C风格的字符串
众所周知,C语言的字符串是char类型的数组。通常我们会以char*的形式来给它动态分配内存。
缺点:
- C字符串不会记录自己的长度信息,获取字符串长度时,会遍历字节数组,直到遇到null字符,复杂度O(n)。(官方将null字符(' ')定义为NUL,只有一个L)
- C字符串不记录长度,修改时也不会判断内存空间是否足够。若不够,会造成缓冲区溢出。
虽然用的是C++,但是经常会调用一些用C编写的接口。因此我们也要去了解C风格的字符串
1.1.1.易错点
C字符串经常会被遗漏掉' '。举个例子,"hello"是5个字母,但是却由'h','e','l','l','o',' '这6个字符组成,所以在内存空间中其也是6个字节。而C语言对字符串的操作strcpy()和strcat()函数是极其不安全的,他们不会对字符串的长度进行检测。
1.1.2.strcpy()
这是对字符串进行复制的函数。它有2个字符串参数,将第二个字符串复制到第一个字符串,但是却不会考虑空间是否足够。
这里用函数包装改良一下来解决原有的问题:
char* copyString(const char* str){
char* newStr = new char[strlen(str) + 1];//strlen()函数可以获得字符串的长度,但是不算
strcpy(newStr,str);
return newStr;
}//记得使用完后释放copyString()分配的堆内存
1.1.3.strcat()
这里是对字符串进行拼接的函数。它也是2个字符串参数,将第二个字符串赋值到第一个后面,并储存在第一个。同样不会判断内存空间。
这里也用函数来包装一下,让它能拼接3个字符串,并且自动扩充空间。
char* addStrings(const char* str1,const char* str2,const char* str3){
char* newStr = new char[strlen(str1) + strlen(str2) + strlen(str3) + 1];
strcpy(newStr,str1);
strcat(newStr,str2);
strcat(newStr,str3);
return newStr;
}
1.1.4.对字符串用sizeof()关键字或者strlen()函数的区别
在上面的1.1.2与1.1.3中能看到,我是用strlen()来获取字符串的长度。sizeof是获取字节数,按道理也可以表示长度啊。
这里我来排一下坑(实验室组织的某次考核中,我因为概念不清,稀里糊涂地用混了,但王学长私下告诉我那次是第一,并且让我继承了他的电路书,这电路书是我疫情期间接触过的唯一一本课本,感激涕零)
在C风格的字符串里,sizeof()会根据字符串的存储方式来返回不同的大小。如果是char[],则返回实际存储的类型,包括' ';而如果是char*,则sizeof()会根据平台的不同,返回你的系统下指针的内存大小(32位是4,64位是8)
1.1.5.安全C库
VS里用C风格的字符串函数时会出现警告,说是已经废弃了。使用strcpy_s()和strcat_s()这些“安全C库”即可避免。不过还是最好切换到C++的std::string类。
1.2.字符串字面量
1.2.1.字面量
cout<<"hello"<<endl;
这样包含字符串本身,而不是包含字符串变量。它本身是一个字符串字面量(string literal),以值的形式写出,而不是以变量的形式。而与字面量相关联的内存位于内存只读区。通过这种机制,编译器可以对相同的字符串字面量进行复用,从而优化内存使用。e.g.一个程序用了几百次"hello"字符串字面量,但其实只创建了一个hello实例,这就是字面量池(literal pooling)
C++标准指出字符串字面量是const char的数组。它可以被赋值给变量,但在用指针指向的时候会产生风险。如:
char* ptr = "hello";//声明一个字符串变量(指针指向字符串字面量)
ptr[1] = 'a';//Undefined Behaviour
本身字面量是const不允许修改,但这里硬是修改的话很明显就是一个ub。它根据编译器的不同,可能会导致程序崩溃,可能会继续运行;可能会产生改变,可能会忽视改变。
更安全一点的写法习惯则是:
const char* ptr = "hello";
ptr[1] = 'a';//ERROR,attempt to write to read-only memory
这样子写的话,编译器会因为“尝试向只读区域写入”而报错。
注:如果用的是字符数组char[]的形式,开辟了对应大小的内存。那么这个字面量不会放在只读的内存区,也不会使用字面量池。当然也就可以自由改变。
1.2.2.原始字符串字面量
原始字符串字面量是不对转义字符进行转换的处理方式。它以R"(开头,以)"结尾。
cout<<R"(hello "world"!
)";
控制台会输出
hello "world"!
而如果想要换行的话,在原始字符串字面量里直接打回车就可以了。
但因为结尾是)",所以其中不能有)"存在,否则会报错。解决方法是使用拓展的原始字符串字面量语法——可选的分隔符序列。
R"d-char-sequence(lalala)d-char-sequence"
分隔符序列最多能有16个字符。
1.3.C++ std::string类
C++中,std::string是一个类(实际上是basic_string模板类的一个实例),这个类支持
1.3.1.有C的字符串,为什么还有C++的字符串?
C风格字符串的优势和劣势
优势:
- 简单,只用到基本数据类型(char)和数组结构
- 轻量,由于结构简单,可以只占用所需的内存。
- 低级,可以按照操作原始内存的方式来对字符串进行操作。
- 能够被C程序员理解
劣势:
- 为了模拟字符串,要花很多努力。
- 使用难度大,容易产生bug,不安全
- 没有用到C++的oop思想
- 要求程序员理解底层
1.3.2.使用string类
尽管string是一个类,但我更喜欢把它看做一个简单的数据类型。通过运算符重载,让对string的操作更为直观和简单。
string A("12");
string B("34");
string C;
C = A + B;//C is "1234"
+=,==,!=,<,[]等都被重载了。
而C语言中字符串的不安全(如果一个是char[],一个是char*,则会返回false,因为它比较的是指针的值,而非字符串内容。需要用strcmp(a,b)0来比较)
为了达到兼容的目的,可以用string类的c_str()方法来获得一个C风格的const字符指针。不过一旦string进行内存重分配,或者对象被销毁,这个指针也会跟着失效。
注意,不要从函数中返回在基于堆栈的string上调用c_str()方法得来的结果。
在C++17中,data()方法在非const字符调用时,会返回char(而14或更早的版本始终返回const char)
1.3.3.std::string字面量
源代码中的字符串仍是const char*,但如果"xxx"s,那么可以把字面量变成std::string。
但需要std::string_literals或者std命名空间。
1.3.4.高级数值转换
std名称空间中有很多辅助函数,以便完成数值和字符串的转换
- string to_string(int val);
- string to_string(unsigned val);
- string to_string(long val);
- string to_string(unsigned long val);
- string to_string(long long val);
- string to_string(unsigned long long val);
- string to_string(float val);
- string to_string(double val);
- string to_string(long double val);
还有将字符串转换为数值,其中str是要转换的字符串,idx是一个指针,接收第一个未转换字符的索引,base表示转换过程中使用的进制数。指针可以是空指针,如果是空指针,则被忽略。如果不能执行任何转换,则会跑出invalid_argument异常,如果超出返回类型的范围,则抛出out_of_range异常
- int stoi(const string& str, size_t *idx=0,int base=0);
- long stol(const string& str, size_t *idx=0,int base=0);
- unsigned long stoul(const string& str, size_t *idx=0,int base=0);
- longlong stoll(const string& str, size_t *idx=0,int base=0);
- unsigned long long stoull(const string& str, size_t *idx=0,int base=0);
- float stof(const string& str, size_t *idx=0);
- double stod(const string& str, size_t *idx=0);
- long double stold(const string& str, size_t *idx=0);
1.3.5.低级转换(C++17)
C++17中提供了许多低级数值转换函数,在
若要将整数转换为字符,可以使用下面这组函数:to_char_result to_chars(char* first, char* last, IntegerT value, int base = 10);
。这里IntegerT可以是任何整数类型或字符类型。返回结果是to_char_result类型,看看定义:
struct to_char_result{
char* ptr;
errc ec;
};
如果成功转换,ptr成员将等于所写入字符的下一个位置(one-past-the-end)的指针,如果转换失败(即ec == errc.value_to_large),则它等于last。
下面举个示例:
类似地,浮点数也可以被转换:
to_chars_result to_chars(char* first,char* last, FloatT value);
to_chars_result to_chars(char* first,char* last, FloatT value, chars_format format);
to_chars_result to_chars(char* first,char* last, FloatT value, chars_format format, int precision);
可以通过修改chars_format来修改浮点数的数据类型
enum class chars_float{
scientific, //Style:(-)d.ddde±dd
fixed, //Style:(-)ddd.ddd
hex, //Style:(-)h.hhhp±d (NoteL no 0x)
general = fixed|scientific
}
默认格式是chars_format::general,将导致to_chars()将浮点值转换为fixed的十进制表示形式或者scientific的十进制表示形式,得到最短的表示形式,小数点前至少有以为存在。如果指定了格式,但是没有指定精度,那么会自动确定最简短的表示形式。最大精度是6个数字。
而对于相反的转换(即数->字符串),则有一下一组函数:
from_chars_result from_chars(const char* first, const char* last, IntegerT& value, int base = 10);
from_chars_result from_chars(const char* first, const char* last, FloatT& value, chars_format format = chars_format::general);
Here ,from_chars_result is a type defined as follows:
struct from_chars_result{
const char* ptr;
errc ec;
};
from_chars不会忽略任何前导空白。ptr指向未转换的第一个字符。如果全部转换,那么指向last,如果全没转换,指向first,ec将为errc::invalid_argument。如果值过大,则为errc::result_out_of_range。
1.4.std::string_view类(C++17)
在之前,为了接受只读字符串的函数选择形参类型一直让人很犹豫。不知道是用const string&还是const char*。C++17中引入了std::string_view类来解决这类问题。
它是std::basic_string_view类模板的实例化,在<string_view>头文件中定义。是const string&的简易替代品。不会额外复制字符串,所以不会额外产生内存开销,它支持和string类似的接口,但是缺少c_str()。并且添加了remove_prefix(size_t)和remove_suffix(size_t)方法。前者将起始指针 前移一定偏移量来收缩字符串,后者则将结尾指针倒退一定的偏移量来收缩。
注意:无法连接一个string和string_view,无法编译(解决方法:使用string_view.data()这一方法)
当形参是string_view时,你就可以传入string、 char*、字符串字面量(常量).
而如果以const string&为参数,则不能传入字符串字面量常量和 char*。只能用string。(string_view转换为string类方法:
1.xxx.data();
2.string(xxx)//explicit ctor。
1.4.1.string_view字面量
可使用"xxxxx"sv来让字面量解释为std::string_view。需要命名空间std::string_view_literals或者直接std。
1.5.非标准字符串
很多人不喜欢用C++风格的字符串,有些是因为不知道,有些则是不合口味。但无论是用MFC、QT内置的字符串,还是用自己开发的字符串,都应为项目或团队确立一个标准。