字符串是以ASCII字符并且以NUL(即'\0')结尾 表示的字符序列
C中的字符串机制
以字符串字面量定义字符串时会将其分配到字面量池中,这个内存区域通常保存组成字符串的字符序列,该内存区域通常被认为是全局/静态的。字符字面量在池中通常只有一份副本并且是只读的,这样可以减少程序的内存占用率。
首先,理解C的字符串运行机制,下面一段简单的代码可以得出关于字符串的不同结论
#include <stdio.h>
char *g="Hello";
int main(int argc, char const *argv[])
{
char s[]="Hello";
char *c="Hello";
printf("字符指针c的内存地址:%p\n",&c);
printf("数组s的内存地址:%p\n",&s);
printf("字符指针g的内存地址:%p\n",&g);
printf("字符指针c指向Hello的的内存地址: %p\n",&c[0]);
printf("字符指针g指向Hello的的内存地址: %p\n",&g[0]);
printf("变量s数组的Hello副本的内存地址:%p\n",&s[0]);
printf("Hello字面量的尺寸 %lu\n",sizeof("Hello"));
return 0;
}
从示例代码中,全局区声明并以字符串字面量初始化了字符指针g,g的内存地址是0x108993018,这个地址位于全局数据区内,而指针g它指向的“Hello”字面量地址是0x108992e9e.这个地址位于字面量池内. 同时,我们也从main函数内部定义了两个局部变量字符指针变量c,c指针指向的字面量的内存地址和全局字符指针指向的"Hello"字面量是一样的,那么这里可以得出以下关于声明char指针并以字面量初始化的时候,可以得出以下特性。
- 将同样一份字符串字面量的地址直接赋给不同的字符指针,不会产生额外字符串的副本,这些字符指针指向同一份字符串字面量。
另外,在main函数内部的数组s,它内部持有字符串数组的地址和字面量池的字符串的地址是不一样的。换句话说,
2.以字符串数组初始化字符串字面量会在对应的函数栈内生成另外一份的字面量副本。
char s[]="HELLO"
等价于strcpY(s,"HELLO")
在不同的作用域字符串初始化-内存分布3.再次,如果你在全局区以字符串数组初始化会,会在全局数据区产生同样的字符串副本。
以下是运行的随机结果:
屏幕快照 2019-08-15 下午4.09.34.png
C++中的字符串的机制
string是一个包含多个数据成员的字符串对象,这里只是补充《C++ Primer》这本书关于string对象内部机制没详细阐述,做个笔录,这里不会罗列所有的string对象的api,不熟悉的同学可以看《C++ Primer》相关内容。
-
c_str()是一个指针指向动态分配空间中的字符数组中第一个字符的地址。
-
size()包含字符串的长度。
-
capacity()包含当前可能存储在数组中的有效字符数(额外的NUL字符不计算在内)。
-
malloc内存分配
一个涉及到malloc内存管理程序的实现需要3个字段,每个字段都是三个不同指针:- 指向已分配内存的指针;
- 字符串的逻辑大小(该字符串末尾是NUL字符);
-
分配的内存大小(必须大于或等于逻辑大小);
C++中的string对象内部基本原理
#include <iostream>
#include <cstdlib>
#include <string>
using std::string;
using std::cout;
using std::endl;
//重写string类的new操作符,添加一个可以识别malloc操作的输出
void* operator new(std::size_t n){
cout<<"分配"<<n<<"字节"<<endl;
return malloc(n);
}
void operator delete(void *p) throw(){
free(p);
}
int main(int argc, char const *argv[])
{
string s("HELLO"); //直接初始化
cout<<"初始化时的状态:"<<endl;
cout<<"sizeof:"<<sizeof(s)<<endl;
cout<<"size:"<<s.size()<<endl;
cout<<"分配的内存尺寸(capacity):"<<s.capacity()<<endl;
for(size_t i=6;i<24;++i){
s.push_back('+');
cout<<i<<":"<<s<<endl;
}
cout<<"push_back('+')之后的内存尺寸是"<<endl;
cout<<"sizeof:"<<sizeof(s)<<endl;
cout<<"size:"<<s.size()<<endl;
cout<<"分配的内存尺寸(capacity):"<<s.capacity()<<endl;
运行结果
屏幕快照 2019-08-16 上午5.50.35.png
在for循环前:我们通过调用string三个提到三个基本方法,起初分配的内存是24字节,但允许容纳有效的字符是22个,为什么呢?
因为HELLO后的第6个位置(索引5)包含一个NUL字符(即'\0'),而malloc初始化分配的24个字节里的最后一个字节位置也包含一个界定符,我认为也是NUL字符。有效字符的长度是不将NUL字符计算在内的,所有capacity方法才显示22.初始化的状态如下图所示(重申:另外不同的计算机硬件,不同的OS和编译器环境,malloc初始化时,申请的内存空间是不一样的):
c++ string对象初始化的状态
在for循环过程中,我们向malloc剩余的备用空间塞入'+'字符(覆盖了索引5的NUL字符算起),直到索引22的位置也就是最后一个NUL字符的前一个字节位置),string对象内部就触发malloc申请扩容的操作,而申请的内存总数是之前的2倍。
屏幕快照 2019-08-16 上午6.23.08.png
上面的代码,for的循环的长度加大,总之大于24的任意一个正整数,多实验几次。会得到如下基本特征。
- 当size()方法得出字符数达到capacity()得出的有效容纳的字符数,string对象内部就会触发malloc的内存重新扩容。
- 每次malloc扩容后的申请的内存空间尺寸是之前的内存空间尺寸的2倍。
基于这两点,C++的string对象内部封装了涉及malloc操作的指针操作,这大大减轻了程序猿对指针操作不当,带来程序不可预测的可能性。同时使用双倍扩容的方法也最大限度减少了因字符串长度后续增加的频繁malloc操作带来的系统消耗。但是它是以内存空间为代价的,堆里和字面量池总有着相同一份相同的字符串副本。
后记:
理解C++的string对象底层其实就是malloc动态分配堆内存的机制之后,后面关于字符串的拼接,复制,查找等基本原理,你心里就有底了.要彻底理解字符串的话,推荐阅读《深入理解c指针》这本书,里面关于字符串的描述比《征服C指针》讲得更加深入。
额外问题:
调试C++程序的时候,有时你需要查看string对象内部的指针,虽然c_str()可以输出字符串首个字符的内存地址,但标准库cout操作会自动对c_str()的内部指针做解引操作,因此cout不能直接得出字符串的地址,而是打印对应的字符串。不需要折腾cout,直接简单的粗暴方法是用C的printf函数
#include <stdio.h>
#include <string>
int main(int argc, char const *argv[])
{
string s("HELLO"); //直接初始化
printf("变量的s内存地址:%p\n",&s);
printf("变量的s2内存地址:%p\n",&s2);
printf("字符串对象s的地址:%p\n",&s[0]);
printf("字符串对象s2的地址:%p\n",s2.c_str());
printf("字符串字面量:%p\n",&"HELLO");
return 0;
}
输出
变量的s内存地址:0x7ffee7221568
变量的s2内存地址:0x7ffee7221550
字符串对象s的地址:0x7ffee7221569
字符串对象s2的地址:0x7ffee7221551
字符串字面量:0x1089dfea8
网友评论