Cherno C++学习笔记 P33 字符串的字面量
在这篇文章当中我们介绍一下有关于字符串更加深入的知识,也就是字符串的字面量。
首先什么是字面量?其实也很简单,就是双引号里面的那一坨,其实就是字面量,我们举一个最简单的例子:
#include<iostream>
#include<string>
int main() {
std::string str = "Cherno";
std::cout << str << std::endl;
std::cin.get();
}
这个例子当中,"Cherno"就是字符串字面量。如果我们把鼠标放到上面去,会看到提示变量类型为const char[7],因为实际上它还自带了一个终止字符,所以我们会看到总长度是7。
那么这样就带来了一个神奇的问题,如果我们把终止字符写在了字面量的中间,会发生什么?如下所示,我们这样修改我们的代码:
const char* str = "Che\0rno";
std::cout << str << std::endl;
std::cout << strlen(str) << std::endl;
这个时候就会看到,输出的结果分别是"Che"和3。因为当我们在字面量中间混杂了一个终止字符的时候,我们已经破坏了这个字符串。然后我们可以在调试模式种看看内存里面发生了什么:
我们可以在右侧的ASCII码翻译处看到,在"Cherno"中间出现了一个".",而且在左侧中间位置有一个“00”,这个就是我们添加进去的终止符,编译器在遇到了它之后自动认为这里是这个字符串的终止位置,所以输出了Che。
接下来需要注意这样一个写法:
char* name = "Cherno";
name[2] = 'a';
至少在VS2022当中,它已经不再是合法的写法了,可能在其他的编译器当中还有允许这样去写的,但是现在相当多编译器已经不再允许我们这样去写了。这种写法被称为是“未定义行为”,这是很危险的,所以不要去尝试。这些编译器当中,字面量已经被严格的限定为const char*了。
那么为什么这种行为危险且有可能不合法?这是因为字符串字面量是非常特殊的。我们用指针指向了字面量,但是字面量只存在于内存当中的只读部分。我们可以来仔细地看一下。接下来我们可以使用之前用过的方法,让我们的编译器为我们输出一下汇编语言。
我们可以看到,这段汇编语言中,我们刚才输入的Cherno被标记为了const segment,也就是常量。接着我们使用以下HxD来打开一下我们生成的exe file再看看在可执行文件当中,这个Cherno被放在了哪里。
可以看到, Cherno被直接内嵌进了我们的exe file,也就是它是定死的了,就连我们的可执行文件里面,都已经把它给定死了。那么如果我们还用一个指针指向它,且尝试修改它的值,那么这个行为肯定是很危险的。
如果我们使用的是VS2017,那么在release模式下,是可以运行的,但是会发现给出的结果是没有被修改的,就是“Cherno”,当然在VS2022里面,就连release模式下也是报错不误了。就不要用一个不是const的指针指向字面量啦,因为字面量是被钉死在可执行文件里面的。
所以对于字面量,我们一般都用const char*,如果真的想要对其进行修改,那么需要使用数组而不是指针,这个时候我们相当于是把这个字面量复制了过来,复制到了我们的数组中,那么当然就可以修改了。如果一定要用非const指针,那么只能进行强制类型转换了。当然实际测试下来,强制类型转换只是不报错,但是也有问题,所以最好还是用数组或者是const char*。
那么除了普通的char类型,我们还有哪些其他类型选项?就像上一篇文章提到的,如果只有1个字节的char,那么很多字符没有办法表示。所以C++还给我们提供了包括宽字符w_char,16位字符char_16t以及32位字符char_32t等其它字符。当然,我们平时用的char其实也可以前面加上u8,如下所示:
const wchar_t* name = L"Cherno";
const char16_t* name = u"Cherno";
const char32_t* name = U"Cherno";
const char* name = u8"Cherno";
其中,char16_t占位2个字节,char32_t占位是4个字节,wchar_t在各个平台上都有区别, 在Windows上是2个字节,在Linux和iOS上是4个字节。
那么在上一篇当中,我们说到了没有办法将两个字面量相加,除非对第一个字面量进行强制类型转换成string。那么如果我们非得相加要怎么办?有这样一个办法,如下所示:
using namespace std::string_literals;
std::string name = "Cherno"s + "CPP";
这样就可以正常使用了。其中的s其实是一个函数。
当然string也有32位的情况,如下所示:
std::u32string name = U"Cherno";
根据上面的例子,我们知道了L关键字可以将字符串转变为宽字符,U可以转变为32位字符,那么还有另外一个关键字R,可以完全依照字面来输出字符串,如下所示:
std::string name = R"(aaaa\nbbbb\ncccc\
ddddeeefff)";
输出的结果如图所示:
当然如果不用R关键字,其实还是有一些诡异写法:
const char* ex = "Line1\n"
"Line2\n"
"Line3\n";
这样也是可以正常输出的。
最后我们来看看如果使用数组和指针来处理字面量,分别会发生什么。首先来看数组:
char name[] = "Cherno";
我们打开了汇编语言界面看看什么情况:
我们可以看到,分别移动了目标的字符串和常量到对应的寄存器,然后通过mov指令分别进行复制,最后我们设定操作次数为7次,并使用rep movsb命令,将rsi当中的内容一个字节一个字节的搬运到了rdi当中,这样通过复制,我们完成了用字面量给数组赋值的操作。这样因为是复制过来的,所以当然可以修改。
然后我们再来看看如果使用常指针的情况:
const char* name = "Cherno";
然后我们再来查看汇编语言:
可以看到我们首先还是移动了常量到rax寄存器,但是随后我们直接使用了name指针指向了这个寄存器,也就是我们把rax的地址直接放到了name当中,所以并没有复制这样一个过程,自然也就是无法修改的了。