目录
3.1 第一题:下列代码存在什么问题?请指出问题并做出相应的修改。
3.3 第三题:下列代码存在什么问题?请指出问题并做出相应的修改。
3.4 第四题:下列代码存在什么问题?请指出问题并做出相应的修改。
1. Linux程序内存结构&内存分配
对于一个32位的linux操作系统,他的进程内存分布如下图:
内存四区:代码区 全局区 栈区 堆区
1.1 栈简介
栈由编译器自动分配释放 ,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。
int main()
{
int a;
char b;
long c;
}
- 其中函数中定义的局部变量按照先后定义的顺序依次压入栈中,也就是说相邻变量的地址之间不会存在其它变量。
- 栈的内存地址由高到低向下增长,所以后定义的变量地址低于先定义的变量,比如上面代码中变量 b 的地址小于变量 a 的地址,c 地址小于 b 的地址。
- 栈中存储的数据的生命周期随着函数的执行完成而结束。
1.2 堆简介
堆由开发人员分配和释放, 若开发人员不释放,程序结束时由操作系统回收,分配方式类似于链表。C用malloc (申请)、free (释放),C++用new (申请)、delete (释放)。参考如下代码:
int main() {
// C 中用 malloc() 函数申请
char* p1 = (char *)malloc(10);
cout << (int*)p1 << endl; //输出:00000000003BA0C0
// 用 free() 函数释放
free(p1);
// C++ 中用 new 运算符申请
char* p2 = new char[10];
cout << (int*)p2 << endl; //输出:00000000003BA0C0
// 用 delete 运算符释放
delete[] p2;
}
- 其中 p1 所指的 10 字节的内存空间与 p2 所指的 10 字节内存空间都是存在于堆。
- 堆的内存地址生长方向与栈相反,由低到高向上增长,但需要注意的是,后申请的内存空间并不一定在先申请的内存空间的后面,即 p2 指向的地址并不一定大于 p1 所指向的内存地址,原因是先申请的内存空间一旦被释放,后申请的内存空间则会利用先前被释放的内存,从而导致先后分配的内存空间在地址上不存在先后关系。
- 堆中存储的数据若未释放,则其生命周期等同于程序的生命周期。
2. 动态内存分配
所谓动态内存分配(Dynamic Memory Allocation) 就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
动态内存开辟是在堆上
为什么会有动态内存开辟呢? 因为有时没办法提前知道所需要多大的存储空间,只有在程序运行的时候才知道,这时在数组编译时开辟空间的方法就无法实现。
2.1 malloc函数 申请动态开辟内存空间
void* malloc (size_t size); //头文件stdlib.h
- 如果开辟成功,则返回一个指向开辟好空间的指针;
- 如果开辟失败,则返回一个 NULL 指针;
- 返回值的类型为 void* ,malloc 函数并不知道开辟空间的类型,由使用者自己决定;
- 如果 size 为 0(开辟0个字节),malloc 的行为是标准未定义的,结果将取决于编译器。
2.2 free函数 释放动态开辟内存空间
void free (void* ptr); //头文件stdlib.h
- 如果参数 ptr 指向的空间不是动态开辟的,那么 free 函数的行为是未定义的;
- 如果参数 ptr 是 NULL 指针,那么 free 将不会执行任何动作;
- 一定要记得使用 free 函数释放所开辟的内存空间;
- 使用指针指向动态开辟的内存,使用完并 free 之后一定要记得将其置为空指针。
//动态内存开辟10个整型空间
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
// 假设开辟10个整型空间
int arr[10]; // 在栈区上开辟
// 动态内存开辟
int* p = (int*)malloc(10*sizeof(int)); // 开辟10个大小为int的空间
// 使用这些空间的时候
if (p == NULL) {
perror("main"); // main: 错误信息
return 0;
}
// 使用
int i = 0;
for (i = 0; i < 10; i++) {
*(p + i) = i;
}
for (i = 0; i < 10; i++) {
printf("%d ", p[i]);
}
// 回收空间
free(p);
p = NULL; // 需要手动置为空指针
return 0;
}
Q:为什么 free 之后,需要把 p 置成空指针? p=NULL
A:因为 free 之后那块开辟的内存空间已经不在了,它的功能只是把开辟的空间回收掉,但是 p 仍然还指向那块内存空间的起始位置,所以我们需要使用 p = NULL 把他置成空指针。
Q:为什么 malloc 前面要进行强制类型转换呢? int* p = (int*)malloc(10*sizeof(int));
A:为了和 int* p 类型相呼应,所以要进行强制类型转换。你可以试着把强转删掉,其实也不会有什么问题。但是因为有些编译器要求强转,所以最好进行一下强转,避免不必要的麻烦。
除了malloc之外,还有calloc、realloc,原作者:柠檬叶子C的博文中有详细介绍,指路http://t.csdn.cn/CoMvW
2.3 常见的动态内存错误
- 对空指针进行解引用操作————需要对函数的返回值做判空处理;
- 对动态开辟空间的越界访问————即只申请了10个整型空间,但是却要求访问40个;
- 对非动态开辟的内存使用free释放————在栈上开辟的空间无需手动释放;
- 使用free释放一块动态开辟内存的一部分————必须从头开始释放;
- 对同一块动态内存多次释放————第一次free后直接 p = NULL;
- 动态开辟内存忘记释放导致内存泄漏。
3. 什么是内存泄漏?
在编写应用程序的时候,程序分配了一块内存,但已经不再持有引用这块内存的对象(通常是指针),虽然这些内存被分配出去,但是无法收回,将无法被其他的进程所使用,我们说这块内存泄漏了,被泄漏的内存将在整个程序声明周期内都不可使用。
主要原因:申请内存后未及时释放。
练习一下
选取 柠檬叶子C 大佬的一些题目进行巩固,呜呜讲的太好了
3.1 第一题:下列代码存在什么问题?请指出问题并做出相应的修改。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetMemory(char *p) {
p = (char*)malloc(100);
}
void Test() {
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main() {
Test();
return 0;
}
解析:
代码修改:
① 返回 p ,让 str 接收:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ↓ 修改返回类型为char*
char* GetMemory(char *p) {
p = (char*)malloc(100);
return p; // 将p带回来
}
void Test() {
char *str = NULL;
str = GetMemory(str); // 用str接收,此时str指向刚才开辟的空间
strcpy(str, "hello world"); // 此时copy就没有问题了
printf(str);
// 用完之后记得free,就可以解决内存泄露问题
free(str);
str = NULL; // 还要将str置为空指针
}
int main() {
Test();
return 0;
}
② 将值传递改为址传递:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ↓ 用char**接收
void GetMemory(char** p) {
*p = (char*)malloc(100);
}
void Test() {
char *str = NULL;
GetMemory(&str); // 址传递,就可以得到地址
strcpy(str, "hello world");
printf(str);
// 记得free,就可以解决内存泄露问题
free(str);
str = NULL; // 还要将str置为空指针
}
int main() {
Test();
return 0;
}
3.2 第二题:下列代码存在什么问题?
#include <stdio.h>
#include <stdlib.h>
char* GetMemory(void) {
char p[] = "hello world";
return p;
}
void Test(void) {
char *str = NULL;
str = GetMemory();
printf(str);
}
int main() {
Test();
return 0;
}
答案:GetMemory 函数内部创建的数组实在栈区上创建的,出了函数 p 数组的空间就还给了操作系统,返回的地址是没有实际意义的,如果通过返回的地址去访问内存,就会导致非法访问内存问题。
解析:
3.3 第三题:下列代码存在什么问题?请指出问题并做出相应的修改。
#include <stdio.h>
#include <stdlib.h>
void GetMemory(char **p, int num) {
*p = (char *)malloc(num);
}
void Test(void) {
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main() {
Test();
return 0;
}
答案:没有 free,导致内存泄漏。
解析:
修改:
#include <stdio.h>
#include <stdlib.h>
void GetMemory(char **p, int num) {
*p = (char *)malloc(num);
}
void Test(void) {
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
// 释放并置空
free(str);
str = NULL;
}
int main() {
Test();
return 0;
}
3.4 第四题:下列代码存在什么问题?请指出问题并做出相应的修改。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Test(void) {
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL) {
strcpy(str, "world");
printf(str);
}
}
int main() {
Test();
return 0;
}
答案:free 之后没有将 str 置为空指针,导致 if 为真,对已经释放掉的内存进行了访问,引发非法访问的问题。
解析:
修改:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Test(void) {
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
str = NULL; // 置空
if(str != NULL) {
strcpy(str, "world");
printf(str);
}
}
int main() {
Test();
return 0;
}
参考文章:
1.【操作系统】Linux程序内存结构&内存分配 作者:netlogs 链接:http://t.csdn.cn/uawUX
2. 一文读懂堆与栈的区别 作者:恋喵大鲤鱼 链接:http://t.csdn.cn/bhfa4
3. 【维生素C语言】第十三章 - 动态内存管理 作者:柠檬叶子C 链接:http://t.csdn.cn/CoMvW
4. c/c++ 内存泄漏 作者:MagicDong 链接:https://www.jianshu.com/p/5770c067a552
5. 【维生素C语言】经典动态内存分配笔试题(题目+答案+详解) 作者:柠檬叶子C 链接:http://t.csdn.cn/aCWuC