深入理解C语言内存管理

之前在学Java的时候对于Java虚拟机中的内存分布有一定的了解,但是最近在看一些C,发现居然自己对于C语言的内存分配了解的太少。

问题不能拖,我这就来学习一下吧,争取一次搞定。 在任何程序设计环境及语言中,内存管理都十分重要。

内存管理的基本概念

分析C语言内存的分布先从Linux下可执行的C程序入手。现在有一个简单的C源程序hello.c

复制代码
1 #include <stdio.h>
2 #include <stdlib.h>
3 int var1 = 1;
4 
5 int main(void) {
6   int var2 = 2;
7   printf("hello, world!\n");
8   exit(0);
9 }
复制代码

经过gcc hello.c进行编译之后得到了名为a.out的可执行文件

[tuhooo@localhost leet_code]$ ls -al a.out
-rwxrwxr-x. 1 tuhooo tuhooo 8592 Jul 22 20:40 a.out

ls命令是查看文件的元数据信息

[tuhooo@localhost leet_code]$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=23c58f2cad39d8b15b91f0cc8129055833372afe, not stripped

file命令用来识别文件类型,也可用来辨别一些文件的编码格式。

它是通过查看文件的头部信息来获取文件类型,而不是像Windows通过扩展名来确定文件类型的。

[tuhooo@localhost leet_code]$ size a.out

text  data bss dec hex filename
(代码区静态数据) (全局初始化静态数据) (未初始化数据区)  (十进制总和) (十六制总和) (文件名)
1301 560 8 1869 74d a.out

显示一个目标文件或者链接库文件中的目标文件的各个段的大小,当没有输入文件名时,默认为a.out。

size:支持的目标: elf32-i386 a.out-i386-linux efi-app-ia32 elf32-little elf32-big srec symbolsrec tekhex binary ihex trad-core。

那啥,可执行文件在存储(也就是还没有载入到内存中)的时候,分为:代码区数据区未初始化数据区3个部分。

进一步解读

(1)代码区(text segment)。存放CPU执行的机器指令(machine instructions)。通常,代码区是可共享的(即另外的执行程序可以调用它),因为对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。

(2)全局初始化数据区/静态数据区(initialized data segment/data segment)。该区包含了在程序中明确被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。例如,一个不在任何函数内的声明(全局数据):

1 int maxcount = 99;

使得变量maxcount根据其初始值被存储到初始化数据区中。

1 static mincount = 100; 

这声明了一个静态数据,如果是在任何函数体外声明,则表示其为一个全局静态变量,如果在函数体内(局部),则表示其为一个局部静态变量。另外,如果在函数名前加上static,则表示此函数只能在当前文件中被调用。

(3)未初始化数据区。亦称BSS区(uninitialized data segment),存入的是全局未初始化变量。BSS这个叫法是根据一个早期的汇编运算符而来,这个汇编运算符标志着一个块的开始。BSS区的数据在程序开始执行之前被内核初始化为0或者空指针(NULL)。例如一个不在任何函数内的声明:

1 long sum[1000];

将变量sum存储到未初始化数据区。

下图所示为可执行代码存储时结构和运行时结构的对照图。一个正在运行着的C编译程序占用的内存分为代码区、初始化数据区、未初始化数据区、堆区和栈区5个部分。

 

再来看一张图,多个一个命令行参数区:

1)代码区(text segment)。代码区指令根据程序设计流程依次执行,对于顺序指令,则只会执行一次(每个进程),如果反复,则需要使用跳转指令,如果进行递归,则需要借助栈来实现。代码段: 代码段(code segment/text segment )通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。代码区的指令中包括操作码和要操作的对象(或对象地址引用)。如果是立即数(即具体的数值,如5),将直接包含在代码中;如果是局部数据,将在栈区分配空间,然后引用该数据地址;如果是BSS区和数据区,在代码中同样将引用该数据地址。另外,代码段还规划了局部数据所申请的内存空间信息。

2)全局初始化数据区/静态数据区(Data Segment)。只初始化一次。数据段: 数据段(data segment )通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。data段中的静态数据区存放的是程序中已初始化的全局变量、静态变量和常量。

3)未初始化数据区(BSS)。在运行时改变其值。BSS 段: BSS 段(bss segment )通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS 是英文Block Started by Symbol 的简称。BSS 段属于静态内存分配,即程序一开始就将其清零了。一般在初始化时BSS段部分将会清零。

4)栈区(stack)。由编译器自动分配释放,存放函数的参数值、局部变量的值等。存放函数的参数值、局部变量的值,以及在进行任务切换时存放当前任务的上下文内容。其操作方式类似于数据结构中的栈。每当一个函数被调用,该函数返回地址和一些关于调用的信息,比如某些寄存器的内容,被存储到栈区。然后这个被调用的函数再为它的自动变量和临时变量在栈区上分配空间,这就是C实现函数递归调用的方法。每执行一次递归函数调用,一个新的栈框架就会被使用,这样这个新实例栈里的变量就不会和该函数的另一个实例栈里面的变量混淆。栈(stack) :栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧"{}"中定义的变量(但不包括static 声明的变量,static 意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/ 恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

5)堆区(heap)。用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时有可能由OS回收。堆(heap): 堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并将在内存中为这些段分配空间。栈段亦由操作系统分配和管理,而不需要程序员显示地管理;堆段由程序员自己管理,即显式地申请和释放空间。

另外,可执行程序在运行时具有相应的程序属性。在有操作系统支持时,这些属性页由操作系统管理和维护。

C语言程序编译完成之后,已初始化的全局变量保存在DATA段中,未初始化的全局变量保存在BSS段中。TEXT和DATA段都在可执行文件中,由系统从可执行文件中加载;而BSS段不在可执行文件中,由系统初始化。BSS段只保存没有值的变量,所以事实上它并不需要保存这些变量的映像。运行时所需要的BSS段大小记录在目标文件中,但是BSS段并不占据目标文件的任何空间。

以上两图来自于《C语言专家编程》。

在操作系统中,一个进程就是处于执行期的程序(当然包括系统资源),实际上正在执行的程序代码的活标本。那么进程的逻辑地址空间是如何划分的呢?

 

左边的是UNIX/LINUX系统的执行文件,右边是对应进程逻辑地址空间的划分情况。

首先是堆栈区(stack),堆栈是由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。栈的申请是由系统自动分配,如在函数内部申请一个局部变量 int h,同时判别所申请空间是否小于栈的剩余空间,如若小于的话,在堆栈中为其开辟空间,为程序提供内存,否则将报异常提示栈溢出。  

其次是堆(heap),堆一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。堆的申请是由程序员自己来操作的,在C中使用malloc函数,而C++中使用new运算符,但是堆的申请过程比较复杂:当系统收到程序的申请时,会遍历记录空闲内存地址的链表,以求寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲 结点链表中删除,并将该结点的空间分配给程序,此处应该注意的是有些情况下,新申请的内存块的首地址记录本次分配的内存块大小,这样在delete尤其是 delete[]时就能正确的释放内存空间。

接着是全局数据区(静态区) (static),全局变量和静态变量的存储是放在一块的初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 另外文字常量区,常量字符串就是放在这里,程序结束后有系统释放

最后是程序代码区,放着函数体的二进制代码。

为什么要这么分配内存?

(1)一个进程在运行过程中,代码是根据流程依次执行的,只需要访问一次,当然跳转和递归有可能使代码执行多次,而数据一般都需要访问多次,因此单独开辟空间以方便访问和节约空间。

(2)临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。

(3)全局数据和静态数据有可能在整个程序执行过程中都需要访问,因此单独存储管理。

(4)堆区由用户自由分配,以便管理。

举例说明内存分布情况

复制代码
 1 /* memory_allocate.c用于演示内存分布情况 */
 2 
 3 int a = 0;                      /* a在全局已初始化数据区 */
 4 char *p1;                       /* p1在BSS区(未初始化全局变量) */
 5 
 6 int main(void) {
 7   int b;                        /* b在栈区 */
 8   char s[] = "abc";             /* s为数组变量, 存储在栈区 */
 9   /* "abc"为字符串常量, 存储在已初始化数据区 */
10   char *p1, p2;                 /* p1、p2在栈区 */
11   char *p3 = "123456";          /* "123456\0"已初始化在数据区, p3在栈区 */
12   static int c = 0;             /* c为全局(静态)数据, 存在于已初始化数据区 */
13   /* 另外, 静态数据会自动初始化 */
14   p1 = (char *)malloc(10);      /* 分配的10个字节的区域存在于堆区 */
15   p2 = (char *)malloc(20);      /* 分配得来的20个字节的区域存在于堆区 */
16   
17   free(p1);
18   free(p2);
19 }
复制代码

内存的分配方式

在C语言中,对象可以使用静态或动态的方式分配内存空间。

静态分配:编译器在处理程序源代码时分配。

动态分配:程序在执行时调用malloc库函数申请分配。

静态内存分配是在程序执行之前进行的因而效率比较高,而动态内存分配则可以灵活的处理未知数目的。

静态与动态内存分配的主要区别如下:

静态对象是有名字的变量,可以直接对其进行操作;动态对象是没有名字的一段地址,需要通过指针间接地对它进行操作。

静态对象的分配与释放由编译器自动处理;动态对象的分配与释放必须由程序员显式地管理,它通过malloc()和free两个函数来完成。

以下是采用静态分配方式的例子。

1 int a = 100;

此行代码指示编译器分配足够的存储区以存放一个整型值,该存储区与名字a相关联,并用数值100初始化该存储区。

以下是采用动态分配方式的例子:

1 p1 = (char *)malloc(10*sizeof(int));

此行代码分配了10个int类型的对象,然后返回对象在内存中的地址,接着这个地址被用来初始化指针对象p1,对于动态分配的内存唯一的访问方式是通过指针间接地访问,其释放方法为:

1 free(p1);

栈和堆的区别

前面已经介绍过,栈是由编译器在需要时分配的,不需要时自动清除的变量存储区。里面的变量通常是局部变量、函数参数等。堆是由malloc()函数分配的内存块,内存释放由程序员手动控制,在C语言为free函数完成。栈和堆的主要区别有以下几点:

(1)管理方式不同。

栈编译器自动管理,无需程序员手工控制;而堆空间的申请释放工作由程序员控制,容易产生内存泄漏。对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M。当然,这个值可以修改。碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问 题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在它弹出之前,在它上面的后进的栈内容已经被弹出,详细的可以参考数据结构。生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的, 比如局部变量的分配。动态分配由malloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址, EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果。

(2)空间大小不同。

栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将提示溢出。因此,用户能从栈获得的空间较小。

堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的,不会存在一个内存块从栈中间弹出的情况。

在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

(3)是否产生碎片。

对于堆来讲,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低(虽然程序在退出后操作系统会对内存进行回收管理)。对于栈来讲,则不会存在这个问题。

(4)增长方向不同。

堆的增长方向是向上的,即向着内存地址增加的方向;栈的增长方向是向下的,即向着内存地址减小的方向。

(5)分配方式不同。

堆都是程序中由malloc()函数动态申请分配并由free()函数释放的;栈的分配和释放是由编译器完成的,栈的动态分配由alloca()函数完成,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行申请和释放的,无需手工实现。

STACK: 由系统自动分配。例如,声明在函数中一个局部变量 int b;系统自动在栈中为b开辟空间。HEAP:需要程序员自己申请,并指明大小,在C中malloc函数。指向堆中分配内存的指针则可能是存放在栈中的。

(6)分配效率不同。

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行。堆则是C函数库提供的,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大的空间,如果没有足够大的空间(可能是由于内存碎片太多),就有需要操作系统来重新整理内存空间,这样就有机会分到足够大小的内存,然后返回。显然,堆的效率比栈要低得多。

栈由系统自动分配,速度较快。但程序员是无法控制的。

堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

(7)申请后系统的响应

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

(8)堆和栈中的存储内容

栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。栈中的内存是在程序编译完成以后就可以确定的,不论占用空间大小,还是每个变量的类型。

堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

(9)存取效率的比较

1 char s1[] = "a";
2 char *s2 = "b";

a是在运行时刻赋值的;而b是在编译时就确定的但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。

(10)防止越界发生

无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的

数据存储区域实例

此程序显示了数据存储区域实例,在此程序中,使用了etext、edata和end3个外部全局变量,这是与用户进程相关的虚拟地址。在程序源代码中列出了各数据的存储位置,同时在程序运行时显示了各数据的运行位置,下图所示为程序运行过程中各变量的存储位置。

mem_add.c

复制代码
 1 /* mem_add.c演示了C语言中地址的分布情况 */
 2 
 3 #include <stdio.h>
 4 #include <stdlib.h>
 5 
 6 extern void afunc(void);
 7 extern etext, edata, end;
 8 
 9 int bss_var;                    /* 未初始化全局数据存储在BSS区 */
10 int data_var = 42;              /* 初始化全局数据区域存储在数据区 */
11 #define SHW_ADDR(ID, I) printf("the %8s\t is at addr:%8x\n", ID, &I); /* 打印地址 */
12 
13 int main(int argc, char *argv[]) {
14 
15   char *p, *b, *nb;
16   printf("Addr etext: %8x\t Addr edata %8x\t Addr end %8x\t\n", &etext, &edata, &end);
17 
18   printf("\ntext Location:\n");
19   SHW_ADDR("main", main);       /* 查看代码段main函数位置 */
20   SHW_ADDR("afunc", afunc);     /* 查看代码段afunc函数位置 */
21   printf("\nbss Location:\n");
22   SHW_ADDR("bss_var", bss_var); /* 查看BSS段变量的位置 */
23   printf("\ndata Location:\n");
24   SHW_ADDR("data_var", data_var); /* 查看数据段变量的位置 */
25   printf("\nStack Locations:\n");
26 
27   afunc();
28   p = (char *)alloca(32);       /* 从栈中分配空间 */
29   if(p != NULL) {
30     SHW_ADDR("start", p);
31     SHW_ADDR("end", p+31);
32   }
33 
34   b = (char *)malloc(32*sizeof(char)); /* 从堆中分配空间 */
35   nb = (char *)malloc(16*sizeof(char)); /* 从堆中分配空间 */
36   printf("\nHeap Locations:\n");
37   printf("the Heap start: %p\n", b); /* 堆的起始位置 */
38   printf("the Heap end: %p\n", (nb+16*sizeof(char))); /* 堆的结束位置 */
39   printf("\nb and nb in Stack\n");
40 
41   SHW_ADDR("b", b);             /* 显示栈中数据b的位置 */
42   
43   SHW_ADDR("nb", nb);           /* 显示栈中数据nb的位置 */
44   
45   free(b);                      /* 释放申请的空间 */
46   free(nb);                     /* 释放申请的空间 */
47 }
复制代码

 afunc.c

复制代码
 1 /* afunc.c */
 2 #include <stdio.h>
 3 #define SHW_ADDR(ID, I) printf("the %s\t is at addr:%p\n", ID, &I); /* 打印地址 */
 4 void afunc(void) {
 5   static int long level = 0;    /* 静态数据存储在数据段中 */
 6   int stack_var;                /* 局部变量存储在栈区 */
 7   
 8   if(++level == 5)
 9     return;
10   
11   printf("stack_var%d is at: %p\n", level, &stack_var);
12   SHW_ADDR("stack_var in stack section", stack_var);
13   SHW_ADDR("level in data section", level);
14   
15   afunc();
16 }
复制代码

 gcc mem_add.c afunc.c进行编译然后执行输出的可执行的文件,可得到如下结果(本机有效):

然后可以根据地址的大小来进行一个排序,并可视化:

如果运行环境不一样,运行程序的地址与此将有差异,但是,各区域之间的相对关系不会发生变化。可以通过readelf命令来查看可执行文件的详细内容。

readelf -a a.out

其他知识点

来看一个问题,下面代码的输出结果是啥?

第一个文件code1.c

复制代码
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 char* toStr() {
 5   char *s = "abcdefghijk";
 6   return s;
 7 }
 8 
 9 int main(void) {
10   printf("%s\n", toStr());
11 }
复制代码

 第二个文件code2.c

复制代码
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 char* toStr() {
 5   char s[] = "abcdefghijk";
 6   return s;
 7 }
 8 
 9 int main(void) {
10   printf("%s\n", toStr());
11 }
复制代码

 其实我在用gcc编译第二的时候已经有warning了:

第一个可以正常输出,而第二个要么乱码,要么是空的。

两段代码都很简单,输出一段字符,类型不同,一个是char*字符串,一个是char[]数据。

结果:第一个正确输出,第二个输出乱码。

原因:在于局部变量的作用域和内存分配的问题,第一char*是指向一个常量,作用域为函数内部,被分配在程序的常量区,直到整个程序结束才被销毁,所以在程序结束前常量还是存在的。而第二个是数组存放的,作用域为函数内部,被分配在栈中,就会在函数调用结束后被释放掉,这时你再调用,肯定就错误了。

我发现了一个新的问题,如果你把这两个文件合成一个的话,第二个其实可以打印出正确的字符的,代码如下:

复制代码
 1 /* toStr.c演示内存分配问题哦 */
 2 
 3 #include <stdio.h>
 4 #include <stdlib.h>
 5 
 6 char* toStr1() {
 7   char *s = "abcdefghijklmn";
 8   return s;
 9 }
10 
11 char* toStr2() {
12   char s[] = "abcdefghijklmn";
13   return s;
14 }
15 
16 void printStr() {
17   int a[] = {1,2,3,4,5,6,7};
18 }
19 
20 int main(void) {
21   printf("调用toStr1()返回的结果: %s\n",toStr1());
22   printf("调用toStr2()返回的结果: %s\n",toStr2());
23   // printStr();
24   exit(0);
25 
26 }
复制代码

 

不知道为啥,第二个还是可以正常打印的。但是只打印第二个,或者先打印第二个,然后在打印第一个的话,不输出乱码,倒是输出空串。

顾名思义,局部变量就是在一个有限的范围内的变量,作用域是有限的,对于程序来说,在一个函数体内部声明的普通变量都是局部变量,局部变量会在栈上申请空间,函数结束后,申请的空间会自动释放。而全局变量是在函数体外申请的,会被存放在全局(静态区)上,知道程序结束后才会被结束,这样它的作用域就是整个程序。静态变量和全局变量的存储方式相同,在函数体内声明为static就可以使此变量像全局变量一样使用,不用担心函数结束而被释放。

  • 栈区(stack)—由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区(heap)—一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表
  • 全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态                                  变量在相邻的另一块区域。  程序结束后由系统释放。
  • 常量区—常量字符串就是放在这里的,直到程序结束后由系统释放。上面的问题就在这里!!!
  • 代码区—存放函数体的二进制代码。

一般编译器和操作系统实现来说,对于虚拟地址空间的最低(从0开始的几K)的一段空间是未被映射的,也就是说它在进程空间中,但没有赋予物理地址,不能被访问。这也就是对空指针的访问会导致crash的原因,因为空指针的地址是0。至于为什么预留的不是一个字节而是几K,是因为内存是分页的,至少要一页;另外几k的空间还可以用来捕捉使用空指针的情况。

char *a 与char a[] 的区别

char *d = "hello" 中的a是指向第一个字符‘a'的一个指针;char s[20] = "hello" 中数组名a也是执行数组第一个字符'h'的指针。现执行下列操作:strcat(d, s)。把字符串加到指针所指的字串上去,出现段错误,本质原因:*d="0123456789"存放在常量区,是无法修的。而数组是存放在栈中,是可以修改的。两者区别如下:

读写能力:char *a = "abcd"此时"abcd"存放在常量区。通过指针只可以访问字符串常量,而不可以改变它。而char a[20] = "abcd"; 此时 "abcd"存放在栈。可以通过指针去访问和修改数组内容。

赋值时刻:char *a = "abcd"是在编译时就确定了(因为为常量)。而char a[20] = "abcd"; 在运行时确定

存取效率:char *a = "abcd"; 存于静态存储区。在栈上的数组比指针所指向字符串快。因此慢,而char a[20] = "abcd"存于栈上,快。
另外注意:char a[] = "01234",虽然没有指明字符串的长度,但是此时系统已经开好了,就是大小为6-----'0' '1' '2' '3' '4' '5' '\0',(注意strlen(a)是不计'\0')

 

posted @ 2019-09-18 20:05  jack_hzm  阅读(1221)  评论(0编辑  收藏  举报