【C\C++】从内存的角度玩转字符串

【纯纯干货直接拉到 “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
}

逐句分析以上代码:

  1. char 类型数据在内存中占据1个字节。
  2. 任何指针类型在内存中占据8个字节。我的机器是64位的,32位就是4咯。指针的值是一个地址,地址长度就是 CPU 里面寄存器的数据宽度(现代计算机的核心就是“取值-执行”,寄存器里存的是要操作数据的地址)。这也就是所谓机器字长,即计算机进行一次整数运算所能处理的二进制数据的位数。机器字长通常就是 CPU 内部数据通道的宽度,这是效率最大化的。一个能一次处理64位的机器,但是数据通道一次只能运输32位,那就得运输两次才能处理一次。数据库里也有类似的思想,数据在内存中以 4KB 大小的 page 存储,因此 buffer pool 和 disk 间的 I/O 大小就是 4kb,一次刚好取一页。
  3. string 类型数据在内存中占据32个字节。string 是一个类,它里面封装了一个 char* 的指针,其实还有别的其它数据成员。这32个字节里有8个属于 char*,其它24个字节我们也不需要知道是啥。它的 sizeof 绝对不是它存的那个字符串的 sizeof
  4. C语言中字符串实际是以字符数组(char[])的形式保存的。char * a 这个 a,就被视为数组名。对数组名解引用,得到的就是 a[0] 的值,即1。这个1是个存在字符数组里的东西,因此是个 char 类型,它的 sizeof 就是1。
  5. 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
} 
  1. 大括号初始化 char a[] 没问题,如果用大括号初始化 char * 就会报【Error】scalar object 'a' requires one element in initializer,也就是说列表初始化的对象一定要是一个“组”,不管是数组还是容器。初始化 char* 扔到 .c文件能通过编译,会报【Warrning】,但是这个 a 是不可用的。尽管C并没有列表初始化,但它也只支持用大括号去初始化数组,不能用这玩意初始化指针。
  2. 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')<
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值