【纯纯干货直接拉到 “C 的字符串和指针” 部分】
首先交代一下这个问题的背景。
我们要做的是一个类似于字符串拼接的工作。实际是把一个关系数据库内,来自不同表的不同record拼接起来,就是所谓的join操作。
在这个数据库里,所有的 record 是以 void* 的形式存储的。很多朋友可能不理解数据库存储的方式,简单说一下。比如一个数据库,它的表头是(id,name,age),假如说有一条record是(1,“Liming”,17)。这一条 record 里面的三个数字,是完完全全以首尾相连的字节形式存储在磁盘上的。也就是说,机器只认你这块的二进制码,不在乎你是什么类型。类型是程序语言设计的,存储设备是不知道的。只是程序语言把从存储设备里读出来的东西,按照它的做法变成了某种类型让人操作而已。
在insert的时候,先声明一个临时的 char* 用来存储这些字节,之后把每一个字段的值memcpy到这个char* 里面(每个字段的长度都是建表的时候规定好的),最后把这个临时的 char* 赋值给实际用于存储的 void*。
我们要做的第一件事,是把这些字节读进一个vector<string>里。
字符串从C到C++
C风格字符串就是一个char*,而C++风格字符串是一个string。string其实是一个容器,它里面封装的其实就是一个 char*,但是因为封装成 string,就可以用在很多STL的算法里。C++ Primer把string和ector放在一起去讲的,原因就是它们都是C 里面不能变长的一些东西的封装(字符串和数组,注意C是没有字符串类型的)。
先来点基本的:
int main(){
cout << sizeof(char) << endl; //1
cout << sizeof(char*) << endl; //8
cout << sizeof(int*) << endl; //8
cout << sizeof(string) << endl; //32
char* a = "123";
cout << "*a的值是 " << *a << endl; //1
cout << "*a的size是 " << sizeof(*a) << endl; //1
cout << "a的值是 " << a << endl; //123
cout << "a的size是 " << sizeof(a) << endl; //8
string b = a; // string b = "123"; 这两句等价
cout << "b的值是 " << b << endl; //123
cout << "b的size是 " << sizeof(b) << endl; //32
}
逐句分析以上代码:
- char 类型数据在内存中占据1个字节。
- 任何指针类型在内存中占据8个字节。我的机器是64位的,32位就是4咯。指针的值是一个地址,地址长度就是 CPU 里面寄存器的数据宽度(现代计算机的核心就是“取值-执行”,寄存器里存的是要操作数据的地址)。这也就是所谓机器字长,即计算机进行一次整数运算所能处理的二进制数据的位数。机器字长通常就是 CPU 内部数据通道的宽度,这是效率最大化的。一个能一次处理64位的机器,但是数据通道一次只能运输32位,那就得运输两次才能处理一次。数据库里也有类似的思想,数据在内存中以 4KB 大小的 page 存储,因此 buffer pool 和 disk 间的 I/O 大小就是 4kb,一次刚好取一页。
- string 类型数据在内存中占据32个字节。string 是一个类,它里面封装了一个 char* 的指针,其实还有别的其它数据成员。这32个字节里有8个属于 char*,其它24个字节我们也不需要知道是啥。它的 sizeof 绝对不是它存的那个字符串的 sizeof。
- C语言中字符串实际是以字符数组(char[])的形式保存的。char * a 这个 a,就被视为数组名。对数组名解引用,得到的就是 a[0] 的值,即1。这个1是个存在字符数组里的东西,因此是个 char 类型,它的 sizeof 就是1。
- C/C++里的 len 才是真正获取字符串长度的东西。两种语言的 len 的值都是3,说明C和C++取字符串长度都不计算字符串末尾的 \0。
继续看:
int main(){
char a[] = {
'1', '2', '\0', '3', '4'}; //写成char* 会直接error
char b[] = "12\034"; //写成char* 会报warning
cout << "a的len是 " << strlen(a) << endl; //2
cout << "b的len是 " << strlen(b) << endl; //3
cout << "a的值是 " << a << endl; //12
cout << "b的值是 " << b << endl; //12
}
- 大括号初始化 char a[] 没问题,如果用大括号初始化 char * 就会报【Error】
scalar object 'a' requires one element in initializer
,也就是说列表初始化的对象一定要是一个“组”,不管是数组还是容器。初始化 char* 扔到 .c文件能通过编译,会报【Warrning】,但是这个 a 是不可用的。尽管C并没有列表初始化,但它也只支持用大括号去初始化数组,不能用这玩意初始化指针。 - strlen() 是C里用来计算长度的函数,截止到 \0,这玩意在C++里对应的是 string 类的 length() 方法。那明明大家都是2后面跟个\0,为啥 a 和 b 的长度不相等呢?
- 都知道 C 字符串会在末位加个\0,也就是说 a 实际存的时候占了6个字节的空间,4后面还有一个自动填上的 \0。这东西的作用就是不需要搞长度了,编译器读到 \0就自动认为当前字符串已经结束。那这就造成了整个字符串使用过程中最大的麻烦:字符串里一旦有我们手动添加的 \0,编译器不知道这个是不是它添加的就直接截断了。毕竟设计这个事的人可能觉着正常人是没有闲着没事往字符串里加 \0的。所以 a 被截断了,长度就是2。
- 为啥 b 的长度就是3呢?因为编译器看到"12\034",根本就不认为是12和34之间加了个 \0,因为有个双引号转义字符叫 \034。所以实际上编译器认为b 是这样的 ‘1’ ‘2’ ‘\034’ ‘\0’。因此 len 长度是3,毕竟编译器唯爱 \0。那读b+4的值就是未定义的行为了,溢出了。要注意,\034 这个玩意儿是存在一个 char 里的,我现在要把两个字符串拼一起(memcpy),会不会出现第一串尾巴的 \0 和后一串开头可能会出现的34碰面消了的情况呢?不会。因为 \0 独占一个字节,3和4各占一个字节,字符串的copy都是按字节copy的。就是说假如说我输入 \034 会出现二义性,是因为我这东西要经过编译器解析,编译器它设计解析树的时候就没办法避免这个事。但是copy,直接走字节,全是0和1,这玩意连意义都没有,更别提有什么二义的可能。
再继续:
int main(){
char a[] = {
'1', '2', '\0', '3', '4'}; //2
cout << strlen(a) << endl;
string s = {
'1', '2', '\0', '3', '4'}; //5
cout << s.length() << endl;
cout << s << endl; //1234
}
为什么string里遇到了 \0,还是能完整读出1234,长度也是5?有人会说,这是因为 C++ 的 string 不以 \0 结束。是这样的吗?
int main(){
string s("12345");
for (int i = 0; i <= s.size()+3; i++){
cout << s[i];
if(s[i] == '\0')<