跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

字符串常量

文章目录 1.内存分布 (1) 代码区 (2) 全局区 (3) 栈区(stack) (4) 堆区(heap) 2.字符串常量的存储 字符数组与字符指针 3.字符串常量内存释放问题 4.字符串常量生命周期 5.字符串常量定义 6.字符数组 7.字符指针 8.内存图 9.补充 1.内存分布 一个编译的C程序占用的内存分为以下几个部分: 1、栈区(stack)—也称自动类型存储区,由编译器自动分配释放,存放函数的参数值,局部变量的值等,例如函数调用结束后自动释放。

2、堆区(heap)—也称动态分配内存区,由程序员分配释放,从分配到程序结束为止,若不释放,程序结束时可能由OS回收,比如malloc分配的内存,free释放的内存。

3、全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后由系统释放。

4、文字常量区— 常量字符串放在这里,程序结束后由系统释放 。

5、程序代码区—编译后的程序代码放在这里。

来看一个具体的C程序

下图所示为可执行代码存储时结构和运行时结构的对照图。

一个正在运行着的C编译程序占用的内存分为代码区、初始化数据区、未初始化数据区、堆区和栈区5个部分。

(1) 代码区 存放 CPU 执行的机器指令。

通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。

代码区通常是只读的 ,使其只读的原因是防止程序意外地修改了它的指令。

另外,代码区还规划了局部变量的相关信息。

总结:你所写的所有代码都会放入到代码区中,代码区的特点是共享和只读。

(2) 全局区 全局区中主要存放的数据有: 全局变量 、静态变量、常量(如字符串常量) 全局区的叫法有很多:全局区、静态区、数据区、全局静态区、静态全局区 这部分可以细分为data区和bss区 2.1 data区 data区里主要存放的是已经初始化的全局变量、 静态变量 和常量 2.2 bss区 bss区主要存放的是未初始化的全局变量、静态变量,这些未初始化的数据在程序执行前会自动被系统初始化为0或者NULL 2.3 常量区 常量区是全局区中划分的一个小区域,里面存放的是常量,如const修饰的全局变量、 字符串 常量等 在VS下运行结果如下:

总结:全局区存放的是全局变量、静态变量和常量 在程序运行后由产生了两个区域,栈区和堆区。

(3) 栈区(stack) 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。

在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。

vs运行效果如下

(4) 堆区(heap) 堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。

用于动态内存分配。

堆在内存中位于BSS区和栈区之间。

一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

vs运行效果如下:

当我们把几个案例放在一起执行,就可以看到内存将每个区域划分的很有条理。

每个区域互不干涉,区域中的数据地址也是非常接近的

2.字符串常量的存储 首先,毫无疑问,即使是常量(字符串常量)也是要占据空间的。

一般来说,基本类型(整型、字符型等)常量会在编译阶段被编译成立即数,占的是代码段的内存。

(代码段是只读的,而且不允程序员获取代码段的地址,所以在c++中,尽量不为const分配数据段的内存,但是一旦取cosnt的地址,就不得不分配了,但是读const的时候,依然是从代码段读取那个立即数)。

当然,占代码段的内存一般不在我们常说的“占内存”范围中。

代码段不是寄存器。

而字符串常量或基本类型的常量数组占用的是数据段内存。

程序中但凡出现“XXX”形式,这都是代表字符串常量,是要事先存储在程序的只读数据区的。

void test(){

char name[32] = "hello,world"; // "hello,workd" 存储在程序的只读数据区, name 存储在test函数的栈区。

}

**字符串常量的存储:**字符串常量使用一对双引号括起来的字符序列,与基本类型常量的存储相似,字符串常量在内存中的存放位置由系统自动安排。

由于字符串常量是一串字符,通常被看作一个特殊的一位字符数组,与数组的存储类似, 字符串常量中的所有字符在内存中连续存放 。

所以,系统在存储一个字符串常量时先给定 一个起始地址 ,从该地址指定的存储单元开始,连续存放该字符串中的字符。

显然,该起始地址代表了存放字符串常量首字母的存储单元的地址,被称为字符串常量的值,也就是说,字符串常量实质上是一个指向该字符串首字符的指针常量。

例如字符串**"Hello" 的值是一个地址,从它指定的存储单元开始连续存放该字符串的6个字符 (‘H’ ‘e’ ‘l’ ‘l’ ‘o’ ‘\n’)** 字符数组与字符指针 如果定义一个字符指针接收字符串常量的值,该指针就指向字符串的首字符 ,例如:

char sa[]="array ";

char *sp="point "; printf("%s", sa); //数组名sa作为printf的输出参数 printf("%s", sp); //字符指针sp作为printf的输出参数 printf("%s \n", "string"); //字符串常量作为printf的输出参数

输出: array point string 调用printf()函数,以%s的格式输出字符串时,作为输出参数, 数组名sa 、 指针sp 和 字符串"string"的值都是地址,从该地址所指定的单元开始连续输出其中的内容(字符) ,直到遇到**‘\0’**为止。

由此可见,输出字符串时,输出参数给出的 起始位置(地址) ,**‘\0’**用来控制结束。

因此,字符串中其它字符的地址也能作为输出参数。

例如:

printf("%s", sa+2); //数组元素sa[2]的资质作为输出参数

printf("%s", sp+3); //sp+3作为起始数组 printf("%s \n", "string" +1 ); //t作为起始输出

输出:

字符数组与字符指针都可以处理字符串,但两者之间有重要区别,如:

char sa[]="This is a string";

char* sp="This is a string";

字符数组sa在内存中占用了一块连续的单元,有确定的地址,每个数组元素放字符串的一个字符,字符串就存放在数组中。

字符指针sp只占用一个可以存放地址的 内存单元 ,而 不是将字符串放到字符指针变量中去 。

如果要改变数组sa所代表的字符串,只能改变数组元素的内容。

如果要改变指针sp所代表的字符串,通常直接改变指针的值,让它指向新的字符串。

因为sp是指针变量,它的值可以改变,转向指向其它单元。

当定义字符指针后,如果没有赋值,指针的值是不确定的,引用未赋值的指针可能出现难以预料的结果:

char* s;

scanf("%s",s);

没有对指针s赋值,却对s指向的单元赋值,如果该单元已经分配给其他变量,其值就改变了。

所以以下写法str有确定的存储单元,这才是正确的:

char* s,str[20];

s=str; scanf("%s",s);

为了避免引用未赋值的指针造成的危害,在定义指针时,可先将它的初值置为空,如: char* s=NULL; 一般在使用指针类型后,为避免出现内存泄漏,都需要手动释放内存,如:

char*s =new char[128];

delete []s; s = NULL;

但如果是像 const char* str 这种指针,则不需要手动释放内存。

这是因为

const char* str

定义的是一个指向常量的指针。

如果str是局部变量,则字符串会随着变量所在的函数的退出而自动释放;如果str是全局变量,则程序退出时才自动释放。

#include

int main(void){ //字符串指针 const char* s = "Hello World!"; printf("%s\n", s); //字符串数组 char ch[] = "Hello World!"; printf("%s\n", ch); //整型变量和指针 int* i, a = 233333; i = &a; printf("%d\n", *i); //字符型变量和指针 char* cha, b = 'a'; cha = &b; printf("%c\n", *cha); return 0; }

输出结果:

在处理 整型和字符型 等时, printf()函数输出时指针变量前需要 带上 ***** 号 而处理 字符串 时,输出时指针变量前则 不需要 带 ***** 号。

3.字符串常量内存释放问题

#include

using namespace std; char *str1() { char *str="hello world"; return str; } char *str2() { char str[]="hello world"; //str[]在栈上,子函数结束后自动释放内存,返回的其实是首地址 return str; //拷贝后,返回和”hello world“无关,返回存在str[]中的内容 } char *str3() { static char str[]="hello world"; return str; } int main() { char *str=NULL; str=str1(); cout<<"指针指向内存内容:"<

字符串常量存放在

静态存储区

str1返回指针指向内存首地址,由于字符串常量存在静态区,所以内容不变,还是“hello world” str2将静态存储区的内容拷贝一份到栈中,由于栈在str2结束时释放栈内存,所以输出为乱码 str3返回存在静态存储区的内容,“hello world” 可以返回一个局部变量的值,也可以返回一个局部静态指针的地址,但不应该返回一个局部自动指针的地址

int get()

{ int a=152; return a; //可以正确返回 }

int *get()

{ int a=152; return & a; //无意义 }

4.字符串常量生命周期 字符串char *s=“hello”; 与char s[]=“hello”;,看似都是将hello字符串的地址赋值给指针 *p。

但是前面一个表达式是字符串常量的地址赋值给指针,该指针指向的字符串中的字符是不允许被更改的。

而后面一个表达式是将该字符串的每一个字符赋值给数组,该指针指向的数组的首地址,而数组成员是变量,因此可以允许被更改赋值。

下面讨论关于字符串常量在内存中存在生命周期的的问题。

问题如下: 假如

char *s0="hello";

s0="world";

就是说如果开始s0指针指向“hello”这个字符串这个常量的地址,但是当s0指针指向了“world”符串的时候,那么“hello”这个字符串在内存中的是否会被释放掉,还是会一直存在内存中,直到程序结束。

我们不妨先试着写几行代码,通过运行结果来进行分析: 案例 一: 代码:

#include

int main() { char *s0,*s1,*s2; s0="hello"; s1="hello"; s2="hello"; printf("%p\n",s0); printf("%p\n",s1); printf("%p\n",s2); return 0; }

运行结果: 三个指针变量的指向的地址都是相同的,如图

分析: 通过运行结果我们可以看出,赋给不同字符指针的相同的字符串,所有的指针都指向了相同的地址。

但是案例一由于三个指针变量都在同一个函数,因此不能看出其生命周期,但是可以为下面的案例解释做好铺垫。

案例二: 代码:

#include

char *s0="hello"; void a(){ char *s1="hello"; printf("%p\n",s1); s1="world"; } void b(){ char *s2="hello"; printf("%p\n",s2); s2="world"; } int main(){ char *s3=(char *)0; printf("%p\n",s0); a(); b(); s3="hello"; printf("%p\n",s3); return 0; }

运行结果:

四个字符指针变量指向的地址还是一样的。

分析: 这就说明问题了,在函数a(),b()中,局部指针变量*s1,*s2都指向”hello“字符串,但是在退出之前有改变指针指向其他字符串,但是两个字符串指针打印出来的地址还是一样的,说明在使用了字符串常量后,该字符串常量并没有在随着函数的结束而消失,而是依旧存在于内存中,因此当其他函数中使用一样的字符串常量时,指向的依旧是跟还是一样的地址。

但是也许有人会问到,因为有在全局字符指针变量 char *s0 =“hello”;指向了该字符串常量,会像全局变量一样,在真个程序运行都不会释放,因此其他函数调使用该字符串常量时才指向了同一地址。

确实确实有这种可能,那么我们就可以看下面案例三。

案例三:

#include

void a(){ char *s1="hello"; printf("%0x\n",s1); s1="world"; } void b(){ char *s2="hello"; printf("%0x\n",s2); s2="world"; } int main(){ char *s3=(char *)0; a(); b(); s3="hello"; printf("%0x\n",s3); return 0; }

三个字符串指针变量指向的地址还是一样的,如图:

分析: 当我们去掉全局指针变量后,其结果依旧是*s1,*s2, s3 三个指针变量指向的地址依旧是没有变。

在运行完a(),b()两个函数之后,我们再将”hello”赋值给空指针 s3指针,其指向地址与*s1,*s2都一样的。

因此我们可以得出结论: 一旦有字符串常量在运行期间创建,就会在内存中一直保持到程序结束 ,当使用相同的字符串常量的时候,不会再创建字符串常量,而是指向之前的那个。

因此字符串常量是贯穿整个程序的生命周期的。

附:既然说到这里了,那就再多说一点。

案例:当将

char* s0

改为

char s0[ ]

时。

代码:

#include

char s0[]="hello"; void a(){ char *s1="hello"; printf("%0x\n",s1); s1="world"; } void b(){ char *s2="hello"; printf("%0x\n",s2); s2="world"; } int main(){ char *s3=(char *)0; printf("%0x\n",s0); a(); b(); s3="hello"; printf("%0x\n",s3); return 0; }

运行结果:

s0与s1 、s2、 s3指向的地址不同。

分析: 造成这样的结果的原因是因为

char s0[]="hello"

,而 s0指向的是该数组的首地址,而不是字符串的首地址。

而 s1、 *s2、 *s3都是指向字符串的首地址,因此不同。

#include

int main(int argc, char *argv[]) { printf("Hello C-Free!\n");

//定义一个数组变量,用字符串常量初始化其值 。

char a[] = "123"; //定义一个字符指针,再定义一个字符串常量,指针指向的常量首地址 const char* b = "321"; const char* c = "321"; //打印变量在内存里的地址,栈区【高地址->低地址】 printf("%d,%d,%d\n",&a, &b, &c);//6356772,6356768,6356764 //打印指针的首地址,b和c相同因为都指向同一常量区 printf("%d,%d,%d\n",a, b, c); //6356772,4206607,4206607 //打印内容 printf("%s,%s,%s\n",a, b, c); //123,321,321

//从b内存拷贝3个字节给a变量,正常 memcpy(a, b, 3); //从a内存拷贝3个字节给a常量指针,编译失败或运行失败,常量不可修改 memcpy(b, a, 3);

printf("%s,%s,%s\n",a, b, c); //123,321,321 return 0; }

5.字符串常量定义 定义:用双引号(“”)括起来的0个或者多个字符组成的序列。

存储:每个字符串尾自动加一个 ‘\0’ 作为字符串结束标志。

字符串常量在内存的常量存储区是按顺序存储的,如:

char* a = "123";

char* b = "456"; char* c = "456";

定义a时,存储一个“123\0”; 定义b时,判断存储区是否有"456\0",发现没有则在后面追加"456\0"; 定义c时,判断存储区是否有"456\0", 发现有,则不再存储 ,此时b和c两个指针存储的都是"456\0"这片内存地址 既然是常量,那么不可被修改,所以memcpy(b, a, 3);是错误的。

6.字符数组

char a[] = "123";

首先声明一个字符数组a,大小没有确定,但是将一个字符串常量“123\0”赋值给了a,故a的length就是4个字节。

【注意“123\0”并没有存储在常量区】 注意a归根结底是一个数组,数组是一个变量,不是指针,虽然可把a当做一个指针,因为它指向数组的首地址,但归根结底不是指针。

正因为a是变量,所以a能够修改其存储的值。

比如:

int main(){

char a[]="123"; a[0]='a'; printf("%s\n",a); }

输出结果:

7.字符指针

const char* b = "321";

既然名字是字符指针,那么它一定是个指针,指针存储地址。

故解读这句就是: 首先声明一个字符指针b,然后定义一个字符串常量"321\0"。

字符串常量存储在常量区,b存储在栈区,b存储的值是字符串常量"321\0"的地址。

这里用到了const,在C语言里不加const也行,C++里不加会有个警告,但不影响编译。

但是建议加上const,能够让程序员一眼就知道此指针指向的是常量,也就是最终内容无法修改。

虽然最终指向不能修改,但是指针自身的取值,是可以修改的,即可修改指针指向的地方。

8.内存图

所有存储类型代码区、常量区、静态区(全局区)、堆区、栈区,只有栈是从高地址往低地址存储,其他都是低地址往高地址存储。

9.补充 来分析几段代码。

代码:test0.c

#include

#include char* toStr() { char *s = "hello word!"; return s; } int main(void) { printf("%s\n", toStr()); }

运行结果:

运行没有任何问题,因为 “hello world” 是一个字符串常量,存放在文字常量区,把该字符串常量存放的地址赋值给了指针 s,toStr 函数退出时,该该字符串常量所在内存不会被回收,故能够通过指针顺利无误的访问。

代码:test1.c

#include

#include char* toStr() { char s[] = "hello word!"; return s; } int main(void) { printf("%s\n", toStr()); }

编译运行结果:

调用 toStr 函数,定义了一个局部变量( char [] 型数组),该局部变量存放在栈中,当 toStr 函数退出时,栈要清空,局部变量的内存也被清空了,所以这时的函数返回的是一个已被释放的内存地址,出现段错误。

如果函数的返回值非要是一个局部变量的地址,那么该局部变量一定要申明为 static类型 。

代码:test2.c

#include

char *returnStr() { static char p[]="hello world!"; return p; } int main() { char *str=NULL; str=returnStr(); printf("%s\n", str); return 0; }

运行结果:

综合性代码:

#include

//返回的是局部变量的地址,该地址位于动态数据区,栈里 char *s1() { char p[] = "Hello world!";

printf("in s1 p=%p\n", p); printf("in s1: string's address: %p\n", &("Hello world!"));

return p; }

//返回的是字符串常量的地址,该地址位于静态数据区 char *s2() { char *q = "Hello world!";

printf("in s2 q=%p\n", q); printf("in s2: string's address: %p\n", &("Hello world!"));

return q; }

//返回的是静态局部变量的地址,该地址位于静态数据区 char *s3() { static char r[] = "Hello world!";

printf("in s3 r=%p\n", r); printf("in s3: string's address: %p\n", &("Hello world!")); return r; } int main() { char *t1, *t2, *t3;

t1 = s1(); t2 = s2(); t3 = s3();

printf("\nin main:\n"); printf("p = %p\nq = %p\nr = %p\n", t1, t2, t3);

// printf("%s\n", t1); /* 运行会出现段错误,先屏蔽 */ printf("%s\n", t2); printf("%s\n", t3);

return 0; }

运行结果:

根据运行结果我们就很清楚了,这里不再赘述。

🔖 部分参考文章: (37条消息) 关于字符串常量在内存中的生命周期_w_16822的博客-CSDN博客 (39条消息) 【C语言入门】笔记十 (指针中)_Liues233的博客-CSDN博客_假设字符数组sa为一个英文字符串,设计程序,当用户用键盘输入字符串sa后,将 (40条消息) C语言—内存的管理和释放_^不加糖^的博客-CSDN博客_c语言释放内存

相关文章