c语言基础学习08_内存管理

时间:2024-10-26 16:07:45

=============================================================================
涉及到的知识点有:
一、内存管理、作用域、自动变量auto、寄存器变量register、代码块作用域内的静态变量、代码块作用域外的静态变量。

二、内存布局、代码区 code、静态区 static、栈区 stack、堆区 heap。

三、堆的分配和释放、c语言几个使用堆内存的库函数:malloc函数、free函数、calloc函数、realloc函数、
函数的返回值为指针类型01_(即函数的返回值是一个地址)、函数的返回值为指针类型02_、
堆的使用例子:通过堆空间实现动态大小变化的字符数组、函数calloc 和 函数realloc 的使用案例、
通过函数形参为一级指针时,在函数内部分配堆内存的错误案例、通过函数形参为二级指针时,在函数内部分配堆内存的正确案例。
=============================================================================
=============================================================================
一、内存管理

实际上内存管理是通过指针来管理的。要想写出高质量的代码,必须要了解计算机的内存。

1、作用域

一个c语言变量的作用域可以是代码块作用域、函数作用域、文件作用域

代码块:是指大括号{...}之间的一段代码。

同一个作用域不能有同名变量,但不同作用域变量名称可以相同。
打比方:同一个家里面的人的名字不能一样。

linux下示例代码如下:

 #include <stdio.h>

 int c = ;    //文件作用域,因为它属于这个文件本身,并不在任何一个函数里面。

 int main()
{
int a = ; //函数作用域。
{
int b = ; //代码块作用域。
} return ;
}
--------------------------------------
//3个作用域的名字可以一样,不冲突的。因为它们的作用域不一样! int a = ; //文件作用域,因为它属于这个文件本身,并不在任何一个函数里面。 int main()
{
int a = ; //函数作用域。
{
int a = ; //代码块作用域。
} return ;
}

=============================================================================
2、自动变量auto

一般情况下,代码块内部定义的变量都是自动变量。当然也可以显示的使用auto关键字,
所有的自动变量的生命周期就是变量所属的大括号。

例如:
auto signed int a = 0; //定义了一个自动变量。二者等价 int a = 0;
=============================================================================
3、寄存器变量register

通常变量在内存当中,如果能把变量放到cpu的寄存器里面,代码的执行效率会更高。

例如:
register int a = 0; //定义了一个寄存器变量。
=============================================================================
4、代码块作用域内的静态变量

静态变量是指在程序执行期间一直不改变的变量,一个代码块内部的静态变量只能被这个代码内部访问。

例如:
static int i = 0; //定义了一个静态变量。
-----------------------------------------------------------------------------
linux下示例代码如下:

 #include <stdio.h>

 void test()
{
auto signed int a = ; //等价于 int a = 0; a++;
printf("a = %d\n", a);
} int main()
{
register int a = ; //寄存器变量。
static int b = ; //静态变量。 int i;
for (i = ; i < ; i++)
{
test();
} return ;
}
输出的结果为:
a =
a =
a =
a =
a =
a =
a =
a =
a =
a =

-----------------------------------------------------------------------------
静态变量在程序刚加载进内存的时候就出现了,所以它和定义静态变量的大括号无关,
一直到程序结束的时候才从内存中消失,同时静态变量的值只初始化一次。

linux下示例代码如下:

 #include <stdio.h>

 void test()
{
static int a = ; //等价于 int a = 0; a++;
printf("a = %d\n", a);
} int main()
{
register int a = ; //寄存器变量。
static int b = ; //静态变量。 int i;
for (i = ; i < ; i++)
{
test();
} return ;
}
输出的结果为:
a =
a =
a =
a =
a =
a =
a =
a =
a =
a =

=============================================================================
5、代码块作用域外的静态变量

代码块之外的静态变量在程序执行期间一直存在,但只能被定义这个变量的文件访问,
代码块之外的静态变量只能在定义这个变量的文件中使用,在其他文件中不能被访问。

因为全局变量的名字是不能相同的,这样会带来一个什么问题?
因为一个项目往往是多个人写的,每个人都定义自己的全局变量,最后代码合并时会出错。
但是static定义的全局变量在不同文件中的名字是可以相同的。
=============================================================================
6、全局变量

全局变量的存储方式和静态变量相同,但可以被多个文件访问,定义在代码块之外的变量就是全局变量。

全局变量即使不在同一个文件里面,也不能重名。
--------------------------------------
linux下示例代码如下:

mem3.c文件内容如下:

 #include <stdio.h>

 extern int a;       //声明了一个变量a。extern的意思是:外面的,外来的。

 //void test();      //简便写法。
extern void test(); //声明了一个函数。更严谨的写法。意思是说:该函数是外部函数,在其他地方定义了。 int main()
{
test();
printf("a = %d\n", a ); return ;
}

--------------------------------------
mem4.c文件内容如下:

 int a = ;          //a是一个全局变量。

 void test()
{
a++;
}
输出结果为:
root@iZ2zeeailqvwws5dcuivdbZ:~///内存管理# gcc -o a mem3.c mem4.c
root@iZ2zeeailqvwws5dcuivdbZ:~///内存管理# a
a =
root@iZ2zeeailqvwws5dcuivdbZ:~///内存管理#

-----------------------------------------------------------------------------
静态全局变量只能在定义它的文件内部访问,对于文件外部其他的文件是不可以使用的(访问的)。
如果在代码块之外的一个变量或者函数,c语言默认都是全局的。除非写了个static就改变了它的类型了。
=============================================================================
=============================================================================
二、内存布局

1、代码区 code

程序被操作系统加载到内存的时候,所有的可执行代码都加载到代码区,也叫代码段,
这块内存是不可以在运行期间修改的。

代码区中所有的内容在程序加载到内存的时候就确定了,运行期间不可以修改,只可以执行。
=============================================================================
2、静态区 static

静态区是程序加载到内存的时候就确定了,程序退出的时候从内存消失。

所有的全局变量和静态变量在程序运行期间都占用内存。

所有的全局变量以及程序中的静态变量都存储到静态区。
--------------------------------------
linux下示例代码如下:

 #include <stdio.h>

 int a = ;            //在静态区存储。
static int b = ; //在静态区存储,与 int b = 1; 的地址是一样的。 int main()
{
static int c = ; //在静态区存储。
auto int d = ; //自动变量在栈中存储。
int e = ; //自动变量在栈中存储。 printf("%p, %p, %p, %p, %p\n", &a, &b, &c, &d, &e); //0x60104c, 0x601040, 0x601044, 0x7ffe3fddd1c0, 0x7ffe3fddd1c4 return ;
}

=============================================================================
3、栈区 stack

栈是一种先进后出的内存结构,所有的 自动变量、函数的形参、函数的返回值 都是由编译器自动放入栈中。

当一个自动变量超出其作用域时,会自动从栈中弹出。

不同的系统下栈的大小是不一样的,即使是相同的系统,栈的大小也是不一样的。一般来讲栈不会很大,单位是多少K字节。
windows系统下的程序在编译的时候就可以指定栈的大小,linux系统下栈的大小是可以通过环境变量来设置的。
=============================================================================
4、堆区 heap

堆和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但是没有栈那样先进后出的顺序。

堆的使用较复杂些,堆内存空间的申请和释放需要我们手动通过代码来完成。

对是一个大容器,它的容量要远远大于栈,但是在c语言中,堆内存空间的申请和释放需要我们手动通过代码来完成。

=============================================================================
=============================================================================
三、堆的分配和释放

c语言几个使用堆内存的库函数,需要用到头文件 #include <stdlib.h>
=============================================================================
1、malloc函数

void *malloc(size_t size);
malloc函数的功能是:在堆中分配指定大小的内存,单位是:字节。
函数返回值是:void *指针。(无类型指针)
=============================================================================
2、free函数
void free(void *ptr);
free函数的功能是:负责在堆中释放有malloc分配的内存。
参数是:ptr为malloc返回堆中的内存地址。
=============================================================================
3、calloc函数
void *calloc(size_t nmemb, size_t size);
calloc函数与malloc函数的功能类似,都是负责在堆中分配内存。
malloc只负责分配,但不负责清理内存。
calloc分配内存的同时把内存清空(即置0)。

第一个参数是:所需分配内存的单元数量;第二个参数是:每隔内存单元堆的大小(单位:字节)。
=============================================================================
4、realloc函数
void *realloc(void *ptr, size_t size);
realloc函数的功能是:重新分配用malloc函数或calloc函数在堆中分配内存空间的大小。
第一个参数是:ptr为之前用malloc或calloc分配的堆内存地址,第二个参数是:重新分配内存的大小,单位:字节。
realloc函数成功则返回重新分配的堆内存地址,失败返回NULL。
若擦数ptr = NULL,那么realloc和malloc的功能一样了。

realloc也不会自动清理增加后的内存,也需要手动清理。

如果指定地址后面有连续的空间,那么realloc就会在已有地址的基础上增加内存。
如果指定的地址后面没有多余的空间,那么realloc会重新分配新的连续内存,把进内存的值拷贝到新的内存,并同时释放旧内存。
(这是realloc的智能之处)
-----------------------------------------------------------------------------
linux下示例代码如下:

 #include <stdio.h>
#include <stdlib.h>
#include <string.h> int main()
{
//一个栈里面的自动指针变量s指向了一个堆的地址空间。
auto char *s;
s = malloc(); //在堆中申请了(分配了)10个字节的空间,又因为返回值是void *,所以该句为在堆中申请了(分配了)10个char的空间。 strcpy(s, "abcd");
printf("%s\n", s); //abcd
free(s); //释放堆中的内存。不释放的话就会一直占着! s = malloc(); //因为s是自动指针变量,释放后可以重新使用,这个时候s又重新指向了一个新的堆地址空间。
free(s); //free(s);并不是把自动指针变量s释放了,而是释放了s所指向的那块堆内存空间。 //一个程序的栈大小是有限的,如果一个数组特别大,有可能会导致栈溢出,所以不要在栈里面定义太大的数组。
//int a[100000000]; //定义了一个数组,这个数组在内存的栈区里面。
//a[99999999] = 0; //程序编译没有问题,但是程序运行出现Segmentation fault(段错误) printf("%lu\n", sizeof(int)); //4 //当一个数组特别大时,我们可以使用堆。
int *p = malloc( * sizeof(int));
p[] = ;
free(p); return ;
}

-----------------------------------------------------------------------------
什么时候在栈中使用一个数组呢?又什么时候在堆中使用一个数组呢?

1、如果使用一个特别大的数组,那么需要把数组放入堆中,而不是栈。

2、如果一个数组在定义的时候,大小不能确定,那么适合用堆,而不是栈。

3、如果malloc分配的内存忘记free,那么会发生内存泄漏,这个也是初学者最容易犯的错误。

malloc分配的空间里面的值是随机的,不会自动置0。

堆到底有多大呢?它取决于物理内存,取决于操作系统本身,并不取决于你的程序。如下代码:

//可以根据用户的输入在堆中分配大小不同的数组。
linux下示例代码如下:

 #include <stdio.h>
#include <string.h>
#include <stdlib.h> int main()
{
int i;
scanf("%d", &i); int *p = malloc(i * sizeof(int)); int a;
for (a = ; a < i; a++)
{
printf("%d\n", p[i]);
} free(p); return ;
}

=============================================================================
函数的返回值为指针类型01_(即函数的返回值是一个地址)

linux下示例代码如下:

 #include <stdio.h>
#include <stdlib.h>
#include <string.h> int *test()
{
int a = ; //a是auto,是局部变量。在栈里面。空间会自动释放。
return &a; //出现编译警告:warning: function returns address of local variable [-Wreturn-local-addr]
//警告:函数返回局部变量的地址[-Wreturn-local-addr]
} int *test1()
{
int *p = malloc( * sizeof(int)); //在堆中申请了(分配了)4个字节的空间,也即一个int的空间。在堆里面。空间不会自动释放。
*p = ;
return p;
} char *test2()
{
char a[] = "hello"; //a是auto,是局部变量。
return a; //同样出现编译警告:warning: function returns address of local variable [-Wreturn-local-addr]
//警告:函数返回局部变量的地址[-Wreturn-local-addr]
} char *test3()
{
char *a = malloc(); //在堆中申请了(分配了)100个字节的空间。也即100个char的空间。
strcpy(a, "hello");
return a;
}
//由test()和test2()可知:在函数内部不能直接返回一个auto类型变量的地址,因为auto类型变量的地址都是自动的,一旦该函数执行完后,这个地址就无效了。 //-----------------------------------------------------------------------------
char *test4(char *arg)
{
return arg;
} char *test5(char *arg)
{
return &arg[]; //返回下标为5的成员变量的地址。
} char *test6()
{
char *p = malloc();
*p = 'a'; //等价于 p[0] = 'a';
*(p + ) = 'b'; //等价于 p[1] = 'b';
*(p + ) = ''; //等价于 p[2] = '0'; 或 p[2] = 0; 或 *(p + 2) = 0; return p;
} char * test7()
{
char *p = malloc();
*p = 'a';
p++;
*p = 'b';
p++;
*p = ; return p;
} int main01()
{
//int *p = test(); //编译时出现警告,因为test执行完后内部的自动变量a已经不在内存了,所以p指向了一个无效的地址,但是这块内存的内容还在。也即变成了野指针了。
int *p = test1(); //是通过堆内存分配函数进行内存分配的,函数test1执行完后,内存不会自动释放的。
printf("%d\n", *p); //
free(p); //char *p1 = test2(); //编译时同样出现警告,因为test执行完后内部的自动变量a已经不在内存了,所以p指向了一个无效的地址,也即变成了野指针了。
//printf("%s\n", p1); //忽略警告后,执行输出结果不可知。
//free(p1); char *p2 = test3();
printf("%s\n", p2); //hello
free(p2); return ;
} int main()
{
char a[] = "hahahaha"; //定义了一个auto自动变量是数组变量,在栈里面。
char *p;
p = test4(a); //该句执行后:等价于 p = a;或 p = &a[0]; p指向的是栈里面的地址。
printf("%s\n", p); //hahahaha 即从角标为0的元素开始输出。 p = test5(a); //该句等价于 p = &a[5],即从数组a的角标为5的元素开始。
printf("%s\n", p); //aha 即从数组a的角标为5的元素开始输出。 p = test6();
printf("%s\n", p); //ab
free(p); p = test7();
printf("%s\n", p); //编译没有问题,执行出现问题。
free(p); return ;
}

输出结果为:

root@iZ2zeeailqvwws5dcuivdbZ:~///内存管理# gcc -o a mem8.c
root@iZ2zeeailqvwws5dcuivdbZ:~///内存管理# a
hahahaha
aha
ab *** Error in `a': free(): invalid pointer: 0x0000000000cc8422 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.(+0x777e5)[0x7f517e1cb7e5]
/lib/x86_64-linux-gnu/libc.so.(+0x8037a)[0x7f517e1d437a]
/lib/x86_64-linux-gnu/libc.so.(cfree+0x4c)[0x7f517e1d853c]
a[0x400916]
/lib/x86_64-linux-gnu/libc.so.(__libc_start_main+0xf0)[0x7f517e174830]
a[0x400599]
======= Memory map: ========
- r-xp fd: /root///内存管理/a
- r--p fd: /root///内存管理/a
- rw-p fd: /root///内存管理/a
00cc8000-00ce9000 rw-p : [heap]
7f5178000000-7f5178021000 rw-p :
7f5178021000-7f517c000000 ---p :
7f517df3e000-7f517df54000 r-xp fd: /lib/x86_64-linux-gnu/libgcc_s.so.
7f517df54000-7f517e153000 ---p fd: /lib/x86_64-linux-gnu/libgcc_s.so.
7f517e153000-7f517e154000 rw-p fd: /lib/x86_64-linux-gnu/libgcc_s.so.
7f517e154000-7f517e314000 r-xp fd: /lib/x86_64-linux-gnu/libc-2.23.so
7f517e314000-7f517e514000 ---p 001c0000 fd: /lib/x86_64-linux-gnu/libc-2.23.so
7f517e514000-7f517e518000 r--p 001c0000 fd: /lib/x86_64-linux-gnu/libc-2.23.so
7f517e518000-7f517e51a000 rw-p 001c4000 fd: /lib/x86_64-linux-gnu/libc-2.23.so
7f517e51a000-7f517e51e000 rw-p :
7f517e51e000-7f517e544000 r-xp fd: /lib/x86_64-linux-gnu/ld-2.23.so
7f517e737000-7f517e73a000 rw-p :
7f517e740000-7f517e743000 rw-p :
7f517e743000-7f517e744000 r--p fd: /lib/x86_64-linux-gnu/ld-2.23.so
7f517e744000-7f517e745000 rw-p fd: /lib/x86_64-linux-gnu/ld-2.23.so
7f517e745000-7f517e746000 rw-p :
7ffce8530000-7ffce8551000 rw-p : [stack]
7ffce85c5000-7ffce85c7000 r--p : [vvar]
7ffce85c7000-7ffce85c9000 r-xp : [vdso]
ffffffffff600000-ffffffffff601000 r-xp : [vsyscall]
Aborted (中止)
root@iZ2zeeailqvwws5dcuivdbZ:~///内存管理#

错诶原因如下图:指针位移后free的问题说明

c语言基础学习08_内存管理
=============================================================================
函数的返回值为指针类型02_

linux下示例代码如下:

 #include <stdio.h>

 char *test()
{
static char a[] = "hello"; return a;
} char *test1()
{
static char a[] = "hello";
char *p = a;
p++; return p;
} const char *test2()
{
const char *s = "hello"; //该意思是将s指向一个常量的地址,常量在程序运行期间是一直有效的。 return s;
} const char *test3() //test2() 和 test3() 是一样的!
{
return "hello world";
} char *test4()
{
return "haha"; //返回的是常量地址。而函数定义的却是变量地址。类型不符。
} int main()
{
char *str = test();
printf("%s\n", str); //hello char *str1 = test1();
printf("%s\n", str1); //ello const char *str2 = test2();
printf("%s\n", str2); //hello const char *str3 = test3();
printf("%s\n", str3); //hello world const char *str4 = test4();
printf("%s\n", str4); //haha 函数定义的地址和返回的地址类型不符! char *str4 = test4();
str4[] = 'a';
printf("%s\n", str4); //编译没有问题,但执行出现段错误!因为:常量区和静态区类似,程序运行期间有效,但常量区是只读的,不能修改。 return ;
}

=============================================================================
堆的使用例子:通过堆空间实现动态大小变化的字符数组

linux下示例代码如下:

 #include <stdio.h>
#include <string.h>
#include <stdlib.h> int main01()
{
char a[] = "hello"; //此时的栈不够用了或者栈用的很不灵活了。
char b[] = "haha"; strcat(a, b);
printf("%s\n", a); //用strcat的时候要注意,第一个字符串一定要有足够的空间容纳第二个字符串。 return ;
} int main()
{
char a[] = "hello";
char b[] = "hahahahahahahahahahaha";
char *p = malloc(strlen(a) + strlen(b) + ); strcpy(p, a);
strcat(p, b);
printf("%s\n", p); //hellohahahahahahahahahahaha
free(p); return ;
}

=============================================================================
函数calloc 和 函数realloc 的使用案例:

linux下示例代码如下:

 #include <stdio.h>
#include <stdlib.h>
#include <string.h> //现在用malloc或者calloc已经分配了10个int,如果想扩大或者缩小这块内存,怎么办?用realloc。
//注意:用realloc增加的空间也不会自动清0。
int main01()
{
char *s1 = calloc(, sizeof(char)); //在堆中分配了10个char空间。
char *s2 = calloc(, sizeof(char)); strcpy(s1, "");
strcpy(s2, "abcdef"); s1 = realloc(s1, strlen(s1) + strlen(s2) + ); //根据s1和s2的实际长度扩充s1的大小。
strcat(s1, s2); printf("%s\n", s1); //123456789abcdef free(s1);
free(s2); return ;
} //不用realloc函数来实现扩大或者缩小内存。
int main02()
{
char *s1 = calloc(, sizeof(char)); //在堆中分配了10个char空间。
char *s2 = calloc(, sizeof(char)); strcpy(s1, "");
strcpy(s2, "abcdef"); char *tmp = malloc(strlen(s1) + strlen(s2) + ); strcpy(tmp, s1);
free(s1);
strcat(tmp, s2);
free(s2); s1 = tmp;
free(s1);
printf("%s\n", s1); //123456789abcdef return ;
} //malloc的智能体现:如果指定的地址后面有连续的空间,那么就会在已有的地址的基础上增加内存,
//如果指定的地址后面没有空间,那么realloc会重新分配新的连续内存,把旧内存的值拷贝到新内容,同时释放旧内存。
int main()
{
char *s1 = malloc();
char *p = realloc(s1, ); if (s1 == p)
{
printf("在原有的基础上增加内存\n");
}
else
{
printf("不是在原有的基础上增加内存\n");
}
free(p); return ;
}

realloc的智能体现如下图所示:

c语言基础学习08_内存管理
=============================================================================
通过函数形参为一级指针时,在函数内部分配堆内存的错误案例

linux下示例代码如下:

 #include <stdio.h>
#include <stdlib.h>
#include <string.h> void test(char *s)
{
strcpy(s, "hello");
} void test1(char *s)
{
s = calloc(, );
strcpy(s, "hello");
} int main01()
{
char *p = calloc(, ); //堆中分配了10个char
test(p); printf("%s\n", p); //hello
free(p); return ;
} int main()
{
char *p = NULL;
test1(p); printf("%s\n", p); //编译没有问题,但执行出现Segmentation fault (core dumped)(段错误)。
free(p); return ;
}

通过函数形参为一级指针时,在函数内部分配堆内存的错误案例的图解如下图所示:

c语言基础学习08_内存管理

=============================================================================
通过函数形参为二级指针时,在函数内部分配堆内存的正确案例

linux下示例代码如下:

 #include <stdio.h>
#include <stdlib.h>
#include <string.h> void test(char **s)
{
*s = calloc(, );
strcpy(*s, "hello");
} int main()
{
char *p = NULL;
test(&p); printf("%s\n", p);
free(); return ;
}

通过函数形参为二级指针时,在函数内部分配堆内存的正确案例的图解如下图所示:

c语言基础学习08_内存管理

=============================================================================
windows系统分配内存的最小单位说明

vs2017下示例代码如下:

 #include <stdio.h>
#include <stdlib.h> int main()
{
while ()
{
char *s = malloc();
getchar(); //这个小函数的作用是:让程序执行到这里暂停一下。
}
return ;
}

在windows系统下 任务管理器/详细信息 下查看内存变化:

304K
308K
312K
316K
......

得出结论:
windows系统的每次堆变化是4K字节。
如果你需要1K的空间,操作系统会给4K;
如果你需要5K的空间,操作系统会给8K。
4K就是windows内存的最小页。内存是按照页来区分的。不是按照字节来区分的,不同的操作系统页的大小是不同的。
页的优点是:效率提升;缺点是:浪费了一些内存。

char *s = malloc(4 * 1024); //我们会发现:有些c语言源代码里面某些程序直接这样写的。

山寨机(机器配置比较低)上写程序,需要好好考虑内存的使用,比如嵌入式系统中写程序。

=============================================================================