C/C++内存管理详解

在计算机系统,特别是嵌入式系统中,内存资源是非常有限的。尤其对于移动端开发者来说,硬件资源的限制使得其在程序设计中首要考虑的问题就是如何有效地管理内存资源。本文是作者在学习C语言内存管理的过程中做的一个总结,如有不妥之处,望读者不吝指正。

因为不同的编译器和平台,对于内存的管理(段的划分)不尽相同,所以这里以 Linux 为参考总结C语言的内存管理

几个基本概念

在C语言中,关于内存管理的知识点比较多,如函数、变量、作用域、指针等,在探究C语言内存管理机制时,先简单复习下这几个基本概念:

变量

不解释。但需要搞清楚这几种变量类型:

局部变量(自动变量)

一般情况下,代码块{}内部定义的变量就是自动变量,也可使用auto显示定义。

auto只能用来标识局部变量的存储类型,对于局部变量,auto是默认的存储类型,不需要显示的指定。因此,auto标识的变量存储在栈区中。示例如下:

 #include <stdio.h>  
int main(void)  
{  
 auto int i=1; //显示指定变量的存储类型  
 int j=2;  

 printf("i=%d\tj=%d\n",i,j);  

 return 0;  
}  

全局变量(外部变量)

出现在代码块{}之外的变量就是全局变量。

extern

注意:extern修饰变量时,根据具体情况,既可以看作是定义也可以看作是声明;但extern修饰函数时只能是定义,没有二义性。

extern用于定义的时候,用来声明在当前文件中引用在当前项目中的其它文件中定义的全局变量(必须是全局变量)。如果全局变量未被初始化,那么将被存在BBS区(存放的是未初始化的全局变量和静态变量)中,且在编译时,自动将其值赋值为0,如果已经被初始化,那么就被存在数据区中。

全局变量,不管是否被初始化,其生命周期都是整个程序运行过程中,为了节省内存空间,在当前文件中使用extern来声明其它文件中定义的全局变量时,就不会再为其分配内存空间。

例如:
file.c文件

 #include <stdio.h>  

int i=5; //定义全局变量,并初始化  
void test(void)  
{  
 printf("in subfunction i=%d\n",i);  
}  

test.c文件

 #include <stdio.h>  

extern i; //声明引用全局变量i  
int main(void)  
{  
 printf("in main i=%d\n",i);  
 test();  
 return 0;  
}  

编译并运行:

gcc -o test test.c file.c #编译连接  
./test #运行  

结果:

结果:  

in main i=5  
in subfunction i=5  

extern关键字只需要指明类型和变量名就行了,不能再重新赋值,初始化需要在原文件所在处进行,如果不进行初始化的话,全局变量会被编译器自动初始化为0。像这种写法是不行的。

extern int num=4;

但是在声明之后就可以使用变量名进行修改了,像这样:

#include<stdio.h>

int main()
{
    extern int num;
    num=1;
    printf("%d",num);
    return 0;
}

如果不想这个变量被修改可以使用const关键字进行修饰,写法如下:
mian.c

#include<stdio.h>

int main()
{
    extern const int num;
    printf("%d",num);
    return 0;
}

b.c

#include<stdio.h>

const int num=5;
void func()
{
    printf("fun in a.c");
}

使用include将另一个文件全部包含进去可以引用另一个文件中的变量,但是这样做的结果就是,被包含的文件中的所有的变量和方法都可以被这个文件使用,这样就变得不安全,如果只是希望一个文件使用另一个文件中的某个变量还是使用extern关键字更好。

extern除了引用另一个文件中的变量外,还可以引用另一个文件中的函数,引用方法和引用变量相似。

mian.c

#include<stdio.h>

int main()
{
    extern void func();
    func();
    return 0;
}

b.c

#include<stdio.h>

const int num=5;
void func()
{
    printf("fun in a.c");
}

这里main函数中引用了b.c中的函数func。因为所有的函数都是全局的,所以对函数的extern用法和对全局变量的修饰基本相同,需要注意的就是,需要指明返回值的类型和参数。

静态变量

被声明为静态类型的变量,无论是全局的还是局部的,都存储在数据区中,其生命周期为整个程序,如果是静态局部变量,其作用域为一对{}内,如果是静态全局变量,其作用域为当前文件。静态变量如果没有被初始化,则自动初始化为0。静态变量只能够初始化一次

**总结:**用static声明局部变量,使其变为.data或者.bss段,作用域不变;用static声明外部变量,其本身就是静态变量,这只会改变其连接方式,使其只在本文件内部有效,而其他文件不可连接或引用该变量。

 #include <stdio.h>  
int sum(int a)  
{  
 auto int c=0;  
 static int b=5;  
 c++;  
 b++;  
 printf("a=%d,\tc=%d,\tb=%d\t",a,c,b);  
 return (a+b+c);  
}  

int main()  
{  
 int i;  
 int a=2;  
 for(i=0;i<5;i++)  
  printf("sum(a)=%d\n",sum(a));  
 return 0;  
}  

运行结果

 $ gcc -o test test.c  
$ ./test   
a=2, c=1, b=6 sum(a)=9  
a=2, c=1, b=7 sum(a)=10  
a=2, c=1, b=8 sum(a)=11  
a=2, c=1, b=9 sum(a)=12  
a=2, c=1, b=10 sum(a)=13  

字符串常量

字符串常量存储在**常量数据(.rodata段)**中,其生存期为整个程序运行时间,但作用域为当前文件,示例如下:

 #include <stdio.h>  
char *a="hello";  
void test()  
{  
 char *c="hello";  
 if(a==c)  
  printf("yes,a==c\n");  
 else  
  printf("no,a!=c\n");  
}  

int main()  
{  
 char *b="hello";  
 char *d="hello2";  

 if(a==b)  
  printf("yes,a==b\n");  
 else  
  printf("no,a!=b\n");  

 test();  
 if(a==d)  
  printf("yes,a==d\n");  
 else  
  printf("no,a!=d\n");  

 return 0;  
}  

运行结果:

$ gcc -o test test.c  
$ ./test   
yes,a==b  
yes,a==c  
no,a!=d  

register存储类型

声明为register的变量在由内存调入到CPU寄存器后,则常驻在CPU的寄存器中,因此访问register变量将在很大程度上提高效率,因为省去了变量由内存调入到寄存器过程中的好几个指令周期。如下示例:

 #include <stdio.h>  

int main(void)  
{  
 register int i,sum=0;  
 for(i=0;i<10;i++)  
  sum=sum+1;  
 printf("%d\n",sum);  
 return 0;  
}  

常见问题

static全局变量与普通的全局变量有什么区别?
答:全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量。全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。这两者的区别在于作用域的扩展上。非静态的全局变量可以用extern扩展到组成源程序的多个文件中,而静态的全局变量的作用域只限于本文件,不能扩展到其它文件,由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。把全局变量改变为静态全局变量后是改变了它的作用域,限制了它的使用范围。

static局部变量和普通局部变量有什么区别?
答:把局部变量改变为静态局部变量后是改变了它的存储方式即改变了它的生存期。

static函数与普通函数有什么区别?
答:static函数与普通函数作用域不同,仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。

综上所述:
static全局变量与普通的全局变量有什么区别:
static全局变量只初使化一次,防止在其他文件单元中被引用;

static局部变量和普通局部变量有什么区别:
static局部变量只被初始化一次,下一次依据上一次结果值;

static函数与普通函数有什么区别:
static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝

作用域

通常指的是变量的作用域,广义上讲,也有函数作用域及文件作用域等。我理解的作用域就是指某个事物能够存在的区域或范围,比如一滴水只有在0-100摄氏度之间才能存在,超出这个范围,广义上讲的“水”就不存在了,它就变成了冰或气体。

生存周期

作用域可以看作是变量的一个有效范围,就像网游中的攻击范围一样;生存周期可以看成是一个变量能存在多久,能在那些时段存在,就像网游中的魔法持续时间……

简单的以一个局部变量来举个例子:

在main函数中声明了变量a,那么a的作用域就是main函数内部,脱离了main函数,a就无法使用了,main函数之外的函数或者方法,都无法去使用a。那么a的生存周期是指a在那些时候存在,具体到这个例子,a什么时候存在,要取决于main函数,或者说,main函数只要被调用,且调用没有完成,那么a就将存在。除此以外的情况,a都将被释放。

生存周期也可以理解为从声明到释放的之间的时间。

函数

不解释。

注意:C语言中函数默认都是全局的,可以使用static关键字将函数声明为静态函数(只能被定义这个函数的文件访问的函数)

内存六段

计算机中的内存是分段来管理的,程序和程序之间的内存是独立的,不能互相访问,比如QQ和浏览器分别所占的内存段是不能相互访问的。而每个程序的内存也是分段管理的,一个应用程序所占的内存可以分为很多个段,在Linux下主要需要了解六个段。

.text段

程序被操作系统加载到内存的时候,所有的可执行代码(程序代码指令)、部分整数常量(有些立即数与指令编译在一起)都加载到代码区,这块内存在程序运行期间是不变的。代码区是平行的,里面装的就是一堆指令,在程序运行期间是不能改变的。函数也是代码的一部分,故函数都被放在代码区,包括main函数。

注意:"int a = 0;"语句可拆分成"int a;""a = 0",定义变量a"int a;"语句并不是代码,它在程序编译时就执行了,并没有放到代码区,放到代码区的只有"a = 0"这句。

运行前就已经确定(编译时确定),通常为只读,可以直接在ROM或Flash中执行,无需加载到RAM。在嵌入式中,有时为了特别的需求(例如加速),也可将某个模块搬移到RAM中执行。

.bss段

bss段用来存放 没有被初始化已经被初始化为0全局变量(静态局部变量)。如下例代码:

#include<stdio.h>
 
int bss_array[1024*1024];
int main(int argc, char *argv[])
{ 
    return 0;
}

编译并查看:

$ gcc -g mainbss.c -o mainbss
$ ls -l mainbss
-rwxrwxr-x. 1 hy hy 8330 Apr 22 19:33 mainbss
$ objdump -h mainbss |grep bss
mainbss:     file format elf32-i386
 24 .bss          00400020  0804a020  0804a020  00001018  2**5
 
$ size mainbss
   text    data     bss     dec     hex filename
   1055     272 4194336 4195663  40054f mainbss

全局变量bss_array的大小为4MB = 1024*1024*sizeof(int) Byte = 4194304 Byte。 通过size 查看可知数据被存在了 bss 段

而 可执行文件mainbss 有8KB左右,命令 **ls -l mainbss** 查看得知。可知,bss类型的全局变量只占用 运行时的内存空间,而不占用可执行文件自身的文件空间。若在运行时内存空间不足的问题,编译器会帮忙检查的。这些是数据,若是在运行时的 堆和栈 不足,这点编译器没法检查。

总结:.bss不占据实际的磁盘空间,只在段表中记录大小,在符号表中记录符号。当文件加载运行时,才分配空间以及初始化。

.data段

data段用来存放已经被初始化为非零的全局变量还有static声明的变量。如下代码,只将矩阵的第一个元素初试化为1:

#include<stdio.h>
 
int data_array[1024*1024]={1};
int main(int argc, char *argv[])
{
    return 0;
}

编译查看

[hy@localhost memcfg]$ gcc -g maindata.c -o maindata
[hy@localhost memcfg]$ ls -l maindata
-rwxrwxr-x. 1 hy hy 4202682 Apr 22 19:48 maindata
[hy@localhost memcfg]$ objdump -h maindata |grep \\.data
 23 .data         00400020  0804a020  0804a020  00001020  2**5
[hy@localhost memcfg]$ size maindata
   text    data     bss     dec     hex filename
   1055 4194604       4 4195663  40054f maindata

而 可执行文件maindata 有4MB左右。通过size 查看可知数据被存在了 data 段

可知,data类型的全局变量既占用运行时的内存空间,也占用可执行文件自身的文件空间

链接时初值加入执行文件;执行时,因为这些变量的值是可以被改变的,所以执行时期必须将其从ROM或Flash搬移到RAM。总之,data段会被加入ROM,但却要寻址到RAM的地址。

这就是.bss 和 .data的区别。.bss使可执行文件更小更快加载

.rodata段

rodata用来存放常量数据。 ro: read only。在有的嵌入式系统中, rodata放在 ROM(或 NOR Flash)里,运行时直接读取,不须加载到RAM内存中。

诸如“Hello World”的字符串常量、加 const 关键字的全局变量(const修饰的局部变量只是为了防止修改,没有放入常量区)、#define定义的常量,会被编译器自动放在rodata中。

所以,在嵌入式开发中,常将已知的常量系数,表格数据等造表加以 const 关键字。存在ROM中,避免占用RAM空间。

编译器会去掉重复的字符串常量,程序的每个字符串常量只有一份。有些系统中rodata段是多个进程共享的,目的是为了提高空间利用率。

stack

栈(stack)是一种先进后出的内存结构,所有的自动变量(局部变量)、函数形参都存储在栈中,这个动作由编译器自动完成,我们写程序时不需要考虑。栈区在程序运行期间是可以随时修改的。当一个自动变量超出其作用域时,自动从栈中弹出。

总结:

  1. stack 存放函数的局部变量和函数参数

  2. 每个线程都有自己专属的栈;被调用函数的参数和返回值 被存储到当前程序的栈区,之后被调用函数再为自身的自动变量和临时变量在栈区上分配空间

  3. 函数返回时,栈区的数据会被释放掉,先入后出(FILO)的顺序。

  4. 栈的最大尺寸固定,超出则引起栈溢出;

实验一

//实验一:观察代码区、静态区、栈区的内存地址
#include "stdafx.h"
int n = 0;
void test(int a, int b)
{
 printf("形式参数a的地址是:%d\n形式参数b的地址是:%d\n",&a, &b);
}
int _tmain(int argc, _TCHAR* argv[])
{
 static int m = 0;
 int a = 0;
 int b = 0;
 printf("自动变量a的地址是:%d\n自动变量b的地址是:%d\n", &a, &b);
 printf("全局变量n的地址是:%d\n静态变量m的地址是:%d\n", &n, &m);
 test(a, b);
 printf("_tmain函数的地址是:%d", &_tmain);
 getchar();
}

结果分析:自动变量ab依次被定义和赋值,都在栈区存放,内存地址只相差12,需要注意的是a的地址比b要大,这是因为栈是一种先进后出的数据存储结构,先存放的a,后存放的b,形象化表示如上图(注意地址编号顺序)。一旦超出作用域,那么变量b将先于变量a被销毁。这很像往箱子里放衣服,最先放的最后才能被拿出,最后放的最先被拿出。

实验二

//实验二:栈变量与作用域
#include "stdafx.h"
//函数的返回值是一个指针,尽管这样可以运行程序,但这样做是不合法的,因为
//非要这样做需在x变量前加static关键字修饰,即static int a = 0;
int *getx()
{
    int x = 10;
    return &x;
}

int _tmain(int argc, _TCHAR* argv[])
{
    int *p = getx();
    *p = 20;
    printf("%d", *p);
    getchar();
}

这段代码没有任何语法错误,也能得到预期的结果:20。但是这么写是有问题的:因为int *p = getx()中变量x的作用域为getx()函数体内部,这里得到一个临时栈变量x的地址,getx()函数调用结束后这个地址就无效了,但是后面的*p = 20仍然在对其进行访问并修改,结果可能对也可能错,实际工作中应避免这种做法,不然怎么死的都不知道。

不能将一个栈变量的地址通过函数的返回值返回,切记!

另外,栈不会很大,一般都是以K为单位。如果在程序中直接将较大的数组保存在函数内的栈变量中,很可能会内存溢出,导致程序崩溃(如下实验三),严格来说应该叫栈溢出(当栈空间以满,但还往栈内存压变量,这个就叫栈溢出)。

//实验三:看看什么是栈溢出
int _tmain(int argc, _TCHAR* argv[])
{
    char array_char[1024*1024*1024] = {0};
    array_char[0] = 'a';
    printf("%s", array_char);
    getchar();
}

怎么办?这个时候就该堆出场了。

heap

堆(heap)和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但没有栈那样先进后出的顺序。更重要的是堆是一个大容器,它的容量要远远大于栈,这可以解决上面实验三造成的内存溢出困难。一般比较复杂的数据类型都是放在堆中。但是在C语言中,堆内存空间的申请和释放需要手动通过代码来完成。对于一个32位操作系统,最大管理管理4G内存,其中1G是给操作系统自己用的,剩下的3G都是给用户程序,一个用户程序理论上可以使用3G的内存空间。堆上的内存必须手动释放(C/C++),除非语言执行环境支持GC(如C#在.NET上运行就有垃圾回收机制)。那堆内存如何使用?

接下来看堆内存的分配和释放:

malloc与free

malloc函数用来在堆中分配指定大小的内存,单位为字节(Byte),函数返回void *指针free负责在堆中释放malloc分配的内存。malloc与free一定成对使用。看下面的例子:

//实验四:解决栈溢出的问题
#include "stdafx.h"
#include "stdlib.h"
#include "string.h"

void print_array(char *p, char n)
{
    int i = 0;
    for (i = 0; i < n; i++)
    {
        printf("p[%d] = %d\n", i, p[i]);
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    char *p = (char *)malloc(1024*1024*1024);//在堆中申请了内存
    memset(p, 'a', sizeof(int) * 10);//初始化内存
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        p[i] = i + 65;
    }
    print_array(p, 10);
    free(p);//释放申请的堆内存
    getchar();
}

程序可以正常运行,这样就解决了刚才实验三的栈溢出问题。堆的容量有多大?理论上讲,它可以使用除了系统占用内存空间之外的所有空间。实际上比这要小些,比如我们平时会打开诸如QQ、浏览器之类的软件,但这在一般情况下足够用了。实验二中说到,不能将一个栈变量的地址通过函数的返回值返回,如果我们需要返回一个函数内定义的变量的地址该怎么办?可以这样做:

//实验五:
#include "stdafx.h"
#include "stdlib.h"

int *getx()
{
    int *p = (int *)malloc(sizeof(int));//申请了一个堆空间
    return p;
}

int _tmain(int argc, _TCHAR* argv[])
{
    int *pp = getx();
    *pp = 10;
    free(pp);
}

这样写是没有问题的,可以通过函数返回一个堆地址,但记得一定用通过free函数释放申请的堆内存空间"int *p = (int *)malloc(sizeof(int));"换成"static int a = 0"也是合法的。因为静态区的内存在程序运行的整个期间都有效,但是后面的free函数就不能用了!

注意:malloc动态分配内存后,加上memset函数对其初始化。否则若某一行malloc方法被多次频繁调用,buf指向的动态内存也是被重复创建,释放。但可能都是指向同一块堆内存。没有初始化的话,除第一次外buf中的内容总是有上几次字符转码的残留字符

// 实验6
#include<stdio.h>
#include<stdlib.h>
 
int main(int argc, char *argv[])
{
    int *p = (int *)malloc(10*1);
    // p= (int *)malloc(10*1);
    if(p==NULL) {
        printf("malloc p err\n");
        return -1;
    }
    free(p);
    printf("p = %4x\n",p);
    p = NULL;
    printf("p = %4x\n",p);
    return 0;
}

程序运行结果:

[hy@localhost memcfg]$ gcc maindata.c
[hy@localhost memcfg]$ ./a.out
p = 9cf4008
p =    0
  • 开辟了空间,就要适时的释放。释放时,指针应指向开辟时的内存空间,所以在使用指针时,要注意不要修改了其地址,或者将开辟时的起始地址保存起来。
  • 对指针free后,其地址不一定就为NULL。如代码中的 p,在 free§后,printf(“p=%4x”,p)后并非为0。所以建议在free§后,立即加一句p=NULL。
  • 检查p的地址 if(p!=NULL){ … }

用来在堆中申请内存空间的函数还有calloc和realloc,用法与malloc类似。

calloc与free

void *calloc(size_t nmemb, size_t size);

参数nmemb表示要分配元素的个数,size表示每个元素的大小,分频的内存空间大小是nmemb*size; 返回值是 void*类型的指针,指向分配好的内存首地址。

用法一:分配1024*sizeof(int)字节大小的内存,并清空为0

int *p = (int *)calloc(1024,sizeof(int));

用法二:与 alloc等价的 malloc 用法

int *p = (int *)malloc(1024*sizeof(int));
memset(p,0,1024*sizeof(int));

差异:

  • 用法一calloc,会根据分配的的类型来初始化为0,如:分配int型,则初始化为(int)0; 若为指针类型,则初始化为空指针;若为浮点,则初始化为浮点型。
  • 用法二memset,不能保证初试化为空指针值和浮点型。(与NULL常量和浮点型的定义有关)

realloc与free

realloc()用来重新分配正在使用的一块内存大小。

定义:

void *realloc(void *ptr, size_t size);

用法示例:

int *p = (int *)malloc(1024);    //
p = (int *)realloc(512);        // 重新分配为 512字节大小内存,缩小数据丢失
p = (int *)realloc(2048);      // 重新分配为2048字节大小内存

注意:经过realloc()调整后的内存空间起始地址有可能与原来的不同。

总结

段名称 存储内容 执行文件是否包含 执行过程
.text 代码、部分常量数据(立即数) 在ROM或Flash中执行
.bss 没有被初始化、已经被初始化为0的全局变量(含静态局部变量) RAM中执行
.data 已经被初始化为非零的全局变量、static声明的变量 RAM中执行(加载进来)
.rodata const全局变量、含字符串常量、#define定义变量 ROM中执行
stack 函数调用语句的下一条可执行语句的地址、局部变量、函数参数 RAM中执行
heap 用户申请和释放 RAM中执行

在rom中执行的在编译的时候已经确定,执行的时候不可改变。

作用域和生存域是相对于变量的含义而言的,所以针对各个变量总结如下:

类型 作用域 生存域 存储位置
auto变量 一对{}内 当前函数 变量默认存储类型,存储在stack
extern函数 整个程序 整个程序运行周期 函数默认存储类型,存储在 .text
extern变量 整个程序 整个程序运行周期 初始化在.data段,未初始化在.bss段
static函数 当前文件 整个程序运行周期 代码段
static全局变量 当前文件 整个程序运行周期 初始化在.data段,未初始化在.bss段
static局部变量 一对{}内 整个程序运行周期 初始化在.data段,未初始化在.bss段
register变量 一对{}内 当前函数 运行时存储在CPU寄存器中
字符串常量 当前文件 整个程序运行周期 .rodata

案例分析

案例一

部分分析如下:

  • main函数UpdateCounter为代码的一部分,故存放在代码区
  • 数组a默认为全局变量,故存放在静态区
  • main函数中的"char *b = NULL"定义了自动变量b(variable),故其存放在栈区
  • 接着"b = (char *)malloc(1024*sizeof(char));"向堆申请了部分内存空间,故这段空间在堆区

案例二

需要注意以下几点:

  • 栈是从高地址向低地址方向增长;

  • 在C语言中,函数参数的入栈顺序是从右到左,因此UpdateCounter函数的3个参数入栈顺序是a1、c、b;

  • C语言中形参和实参之间是值传递,UpdateCounter函数里的参数a[1]、c、b与静态区的a[1]、c、b不是同一个

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

  • "char *b = NULL"定义一个指针变量b,b的地址是0xFFF8,值为空–>运行到"b = (char*)malloc(1024*sizeof(char))"时才在堆中申请了一块内存(假设这块内存地址为0x77a0080)给了b,此时b的地址并没有变化,但其值变为了0x77a0080,这个值指向了一个堆空间的地址(栈变量的值指向了堆空间),这个过程b的内存变化如下:

    ---------->

案例三

这是一个前辈写的,非常详细

//main.cpp
int a = 1; // 全局初始化区
char *p1; // 全局未初始化区
int main()  
{  
    int b; // 栈  
    char s[] = "abc"; // 栈  
    char *p2; // 栈  
    char *p3 = "123456"; // 123456\0在常量区,p3在栈上。  
    static int c =0; // 全局(静态)初始化区
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20);
    // 分配得来得10和20字节的区域就在堆区。
    strcpy(p1, "123456"); // 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
    return 0;
}  

案例四

#include <stdio.h>
#include <string.h>
int g_var1 = 0;
int  g_var   = 10;
static int g_static_var = 11;
const char *gp_str = "helloworld";   //此处的const可以省略,结果相同
const int int1 = 10;

int main()
{
    int i;
    static int l_static_var = 12;
    const char *lp_str = "bye-bye";  //此处的const可以省略,结果相同
    printf("g_var1       = %p\n", &g_var1);
    printf("g_var        = %p\n", &g_var);
    printf("g_static_var = %p\n", &g_static_var);
    printf("l_static_var = %p\n", &l_static_var);

    printf("gp_str       = %p\n", gp_str);
    printf("int1        = %p\n", &int1);
    printf("lp_str       = %p\n", lp_str);

    for(i=0; i<strlen(gp_str)+1+strlen(lp_str)+1; i++)
    {
        printf("%p\t", &gp_str[i]);
        if((gp_str[i] >= '!') && (gp_str[i] <= '~'))
        {
            printf("'%c'\n", gp_str[i]);
        }
        else
        {
            printf("0x%02x\n", gp_str[i]);
        }
    }

    const int int2 = 2;
    char a[] = "helloworld";
    static int int3 = 0;
    printf("a           = %p\n", &a);  // 和 a 的结果相同,都是数组首地址
    printf("int2        = %p\n", &int2);
    printf("i           = %p\n", &i);
    printf("int3        = %p\n", &int3);
    return 0;
}

输出结果:

g_var1       = 00450008  //全局初始化为0的变量
g_var        = 00402000  //全局整型变量的存储地址
g_static_var = 00402004  //文件静态变量的存储地址
l_static_var = 0040200C  //函数静态变量的存储地址
                         //可以看出以上三个地址都是连续的
gp_str       = 00403024  //全局字符指针,指向一个字符串常量"helloworld"
int1        = 00403030   //全局int变量
lp_str       = 00403034  //局部字符指针,指向一个字符串常量"bye-bye"
                        //可以看出以上三个地址都是连续的
00403024        'h'
00403025        'e'
00403026        'l'
00403027        'l'
00403028        'o'
00403029        'w'
0040302A        'o'
0040302B        'r'
0040302C        'l'
0040302D        'd'
0040302E        0x00
0040302F        0x00
00403030        0x0a
00403031        0x00
00403032        0x00
00403033        0x00
00403034        'b'
00403035        'y'
00403036        'e'
a           = 2228FF09   // 局部变量
int2        = 0028FF14  // 局部静态变量
i           = 0028FF18   // 局部变量
                      //可以看出以上三个地址都是连续的,先入栈,地址高
int3        = 0040500C  // 初始化为0的静态局部变量

案例五

#include<iostream>
using namespace std;

char* test(void)
{
    char str[]="hello world!";
    return str;
}

int main(void)
{
    char *p;
    p=test();
    cout<<p<<endl;
    return 0;
}

输出结果可能是hello world!,也可能是乱码。出现这种情况的原因在于:在test函数内部声明的str数组以及它的值"hello world”是在栈上保存的,当用return将str的值返回时,将str的值拷贝一份传回,当test函数执行结束后,会自动释放栈上的空间,即存放hello world的单元可能被重新写入数据,因此虽然main函数中的指针p是指向存放hello world的单元,但是无法保证test函数执行完后该存储单元里面存放的还是hello world,所以打印出的结果有时候是hello world,有时候是乱码。

案例六

#include<iostream>
using namespace std;

int test(void)
{
    int a=1;
    return a;
}

int main(void)
{
    int b;
    b=test();
    cout<<b<<endl;
    return 0;
}

输出结果为 1。有人会问为什么这里传回来的值可以正确打印出来,不是栈会被刷新内容么?是的,确实,在test函数执行完后,存放a值的单元是可能会被重写,但是在函数执行return时,会创建一个int型的临时变量,将a的值复制拷贝给该临时变量,因此返回后能够得到正确的值,即使存放a值的单元被重写数据,但是不会受到影响。而在上例中,返回的是char *指针,指针指向的内容可能被释放。

案例七

#include<iostream>
using namespace std;

char* test(void)
{
    char *p=(char *)malloc(sizeof(char)*100);
    strcpy(p,"hello world");
    return p;
}

int main(void)
{
    char *str;
    str=test();
    cout<<str<<endl;
    return 0;
}

运行结果 hello world。这种情况下同样可以输出正确的结果,是因为是用malloc在堆上申请的空间,这部分空间是由程序员自己管理的,如果程序员没有手动释放堆区的空间,那么存储单元里的内容是不会被重写的,因此可以正确输出结果。

案例八

#include<iostream>
using namespace std;

void test(void)
{
    char *p=(char *)malloc(sizeof(char)*100);
    strcpy(p,"hello world");
    free(p);
    if(p==NULL)
    {
        cout<<"NULL"<<endl;
    }
}

int main(void)
{
    test();
    return 0;
}

没有输出。在这里注意了,**free()释放的是指针指向的内存!注意!释放的是内存,不是指针!**这点非常非常重要!指针是一个变量,只有程序结束时才被销毁。释放了内存空间后,原来指向这块空间的指针还是存在!只不过现在指针指向的内容的垃圾,是未定义的,所以说是垃圾。因此,释放内存后应把把指针指向NULL,防止指针在后面不小心又被使用,造成无法估计的后果。

学习内存管理的目的

学习内存管理就是为了知道日后怎么样在合适的时候管理我们的内存。那么问题来了?什么时候用堆什么时候用栈呢?一般遵循以下三个原则:

  • 如果明确知道数据占用多少内存,那么数据量较小时用栈,较大时用堆;
  • 如果不知道数据量大小(可能需要占用较大内存),最好用堆(因为这样保险些);
  • 如果需要动态创建数组,则用堆。
//实验六:动态创建数组
int _tmain(int argc, _TCHAR* argv[])
{
    int i;
    scanf("%d", &i);
    int *array = (int *)malloc(sizeof(int) * i);
    //...//这里对动态创建的数组做其他操作
    free(array);
}

最后的最后

操作系统在管理内存时,最小单位不是字节,而是内存页(32位操作系统的内存页一般是4K)。比如,初次申请1K内存,操作系统会分配1个内存页,也就是4K内存。4K是一个折中的选择,因为:内存页越大,内存浪费越多,但操作系统内存调度效率高,不用频繁分配和释放内存;内存页越小,内存浪费越少,但操作系统内存调度效率低,需要频繁分配和释放内存。嵌入式系统的内存内存资源很稀缺,其内存页会更小,因此在嵌入式开发当中需要特别注意。

对于Windows下的内存划分:
我们需要了解的主要有四个区域,通常叫内存四区,如下图:

参考

C语言知识整理(3):内存管理(详细版
Linux内存管理(text、rodata、data、bss、stack&heap)
笔记:程序内存管理 .bss .data .rodata .text stack heap
malloc 之后使用memset初始化
字符串常量到底存放在哪个存储区
C/C++堆区、栈区、常量区、静态数据区、代码区详解


One more thing

更多关于人工智能、Python、C++、计算机等知识,欢迎访问我的个人博客进行交流, 点这里~~

展开阅读全文
©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值