动态内存管理
我们知道,我们之前定义的变量、数组,都是我们事先定义好的,变量一旦被创建,就不能够再更改了。后来,C99标准又增加了变长数组,这一特性提高了我们程序对内存分配的灵活度,可是还是感觉不太灵活。那么有没有办法让它变得更灵活呢?
当然有,而且只需要几个库的函数就能够搞定。而这些库全部都包含在stdlib.h
这个头文件中:
- malloc ——申请动态内存空间
- free ——释放动态内存空间
- calloc ——申请并初始化一系列内存空间
- realloc ——重新分配内存空间
malloc
malloc
函数用于申请动态内存空间:
#include <stdlib.h>
void* malloc(size_t size);
malloc
函数向系统申请分配size
个字节的内存空间,并返回一个指向这块空间的指针。不过,申请的这块空间并没有被初始化,因此上面的数据是随机的(和局部变量一样)。
如果函数调用成功,那么会返回一个指向被申请的内存空间的指针,由于返回的是void类型的指针,所以它可以被转化成任何类型的数据。如果函数调用失败,返回值就是NULL
。另外,如果size
参数设置成0,那么返回值也有可能是0,但这种情况下并不一定代表调用失败。
//Exmple 01
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int* ptr;
ptr = (int*)malloc(sizeof(int));
if (ptr == NULL)
{
printf("分配内存失败!\n");
exit(1);//程序异常退出
}
printf("请输入一个整数:");
scanf("%d", ptr);
printf("你输入的数据是:%d\n", *ptr);
return 0;
}
运行结果为:
请输入一个整数:12
你输入的数据是:12
这段代码的意思是,使用malloc
函数申请一块int类型的空间,然后使用ptr
指针来指向它,然后将用户输入的数据存储在这块空间里面。
不过,malloc
函数申请的空间是在堆上,那么这就意味这它不会自动释放,直到程序执行结束。所以在使用完变量之后务必释放内存,否则很有可能造成内存泄漏。
free
释放动态内存空间需要用free
函数,函数原型:
#include <stdlib.h>
...
void free(void* ptr);
释放的空间必须要是malloc
calloc
realloc
函数申请的,否则将会导致未定义行为。如果ptr
的参数是NULL
,那么就不执行任何操作。
这个函数实际上并不会修改ptr
参数的值,所以调用后它仍然能够指向原来的地方,只不过变为非法空间罢了。
有人会说了,现代计算机内存都不小,动辄16GB甚至更多,那么这个是否就用不上了呢?
答案当然是否定的。不信你试试下面的这个程序:
//Infinity malloc
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
while (1)
{
malloc(1024);
}
return 0;
}
你会发现内存占用会飙升。这种情况就叫做内存泄漏。我们申请的空间,在使用完后应该要立即释放,不然很有可能会造成不堪设想的后果。
//Exmple 01 - Edited
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int* ptr;
ptr = (int*)malloc(sizeof(int));
if (ptr == NULL)
{
printf("分配内存失败!\n");
exit(1);//程序异常退出
}
printf("请输入一个整数:");
scanf("%d", ptr);
printf("你输入的数据是:%d\n", *ptr);
////释放内存////
free(ptr);
return 0;
}
因此,在使用完申请的内存之后应该要手动释放。
我们再来看一个例子:
//Example 02
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int* ptr;
int num = 123;
ptr = (int*)malloc(sizeof(int));
if (ptr == NULL)
{
printf("分配内存失败!\n");
exit(1);
}
printf("请输入一个整数:");
scanf("%d", ptr);
printf("你输入的整数是:%d\n", *ptr);
ptr = #
printf("你输入的整数是:%d\n", *ptr);
free(ptr);
return 0;
}
结果如下:
请输入一个整数:10
你输入的整数是:10
你输入的整数是:123
HEAP[ConsoleApplication2.exe]: Invalid address specified to RtlValidateHeap( 00D20000, 0096F8E0 )
ConsoleApplication2.exe 已触发了一个断点。
程序被中断了。
刚开始,我们使用malloc
函数申请了一段内存,并且只有ptr
才知道这块内存的地址。所以后面我们更改了ptr
后,这块内存就泄露了。后面我们尝试释放ptr
,却发现,现在ptr
所指向的变量是一个局部变量,不允许手动释放。这种情况的泄露一定要特别注意。
申请任意尺寸的内存空间
malloc
还可以用于申请一块任意尺寸的内存空间。对于后者,由于申请的空间是连续的,所以经常用数组的方式来进行索引。
还记得在《C语言之数组》里我们讲过,Visual Studio和C99之前的编译器是不支持可变长数组的。如果恰好碰到这种环境,怎么办呢?刚好可以用这种方式来曲线救国:
//Example 03
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int* array;
int n;
printf("请输入你要创建的数组元素个数:");
scanf("%d", &n);
array = (int*)malloc(sizeof(int) * n);
for (int i = 0; i < n; i++)
{
array[i] = i;
printf("%d\n", array[i]);
}
return 0;
}
运行结果如下:
//Consequence 03
请输入你要创建的数组元素个数:10
0
1
2
3
4
5
6
7
8
9
多维数组也一样可以(本质上也是线性存储):
//Example 04
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int** array;
int n, m;
scanf("%d %d", &n, &m);//n为几个大的数组,m是一个小数组中有几个元素
array = (int**)malloc(sizeof(int*) * n);//分配行数
for (int i = 0; i < n; i++)
{
array[i] = (int*)malloc(sizeof(int) * m);//分配各个元素
}
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
array[i][j] = j;
printf("%d ", array[i][j]);
}
printf("\n");
}
return 0;
}
运行结果为:
//Consequence 04
3 4
0 1 2 3
0 1 2 3
0 1 2 3
由于malloc
不会初始化申请的内存空间,所以需要字节进行初始化。当然可以写一个循环,像我们刚刚那样,不过还是略显繁琐。
好在,标准库提供了更加高效的函数,包含在string.h
头文件中:
- memset: 使用一个常量字节填充内存空间
- memcpy: 复制内存空间
- memmove: 移动内存空间
- memcmp: 比较内存空间
- memchr: 在内存空间中搜索一个字符
函数原型如下:
#include <string.h>
void* memset(void* s, int c, size_t n);
void* memcpy(void* dest, const void* src, size_t n);
void* memmove(void* dest, const void* src, size_t n);
int memcmp(const void* s1, const void* s2, size_t n);
void* memchr(const void* s, int c, size_t n);
使用memset
函数初始化空间:
//Example 05
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N 10
int main(void)
{
int* ptr = NULL;
ptr = (int*)malloc(N * sizeof(int));
if (ptr == NULL)
{
exit(1);
}
memset(ptr, 0, N * sizeof(int));
for (int i = 0; i < N; i++)
{
printf("%d ", ptr[i]);
}
putchar('\n');
free(ptr);
return 0;
}
运行结果为:
//Consequence 05
0 0 0 0 0 0 0 0 0 0
如果觉得这样太麻烦,那么我们就可以使用calloc
来一步到位。
calloc
calloc
函数用于申请并初始化一系列内存空间:
#include <stdlib.h>
...
void* calloc(size_t nmemb, size_t size);
calloc
函数在内存中动态地申请nmemb个长度为size的连续内存空间(即申请的总空间尺寸为nmemb*size
),这些内存空间全部被初始化成0。
如果函数调用成功,会返回一个指向申请的内存空间的指针,由于返回类型是void指针,因此可以被转换成任何类型的数据。如果函数调用失败,返回值是NULL
。如果nmemb
或size
参数设置为0,返回值也可能是NULL
,但这不一定意味着函数调用失败。
realloc
有时候可能需要对原来分配的空间进行拓展,但是没办法确保两次申请的空间是线性连续的。所以需要先申请一个足够大的空间,再把数据搬运过去。
当然,这样确实可以,但是手动写代码的话感觉有些太繁琐了。使用realloc
函数就可以帮我们完成这一系列的操作:
#include <stdlib.h>
...
void* realloc(void* ptr, size_t size);
以下几点是需要注意的:
realloc
函数将ptr
指向的内存空间大小修改为size
字节- 如果重新分配的内存比原来的大,则旧数据不会发生改变;若比原来小,数据有可能会丢失,慎用!
- 该函数会移动内存空间并返回新的指针
- 如果
ptr
的参数是NULL
,那么调用该函数就相当于调用malloc(size)
- 如果
size
的参数为0,并且ptr
的参数不为NULL
,那么调用该函数就相当于调用free(ptr)
- 除非
ptr
的参数为NULL
,否则ptr
的值必须由先前调用的malloc
calloc
realloc
函数返回
下面这个程序,不断接受用户输入整数,直到用户输入-1结束,然后将所有的数据输出:
//Example 06
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int num;
int count = 0;
int* ptr = NULL;//这里必须初始化为NULL
do
{
printf("请输入一个整数(输入-1表示结束:)");
scanf("%d", &num);
count++;
ptr = (int*)realloc(ptr, count * sizeof(int));
if (ptr == NULL)
{
exit(1);
}
ptr[count - 1] = num;
} while (num != -1);
printf("输入的整数分别是:");
for (int i = 0; i < count - 1; i++)
{
printf("%d ", ptr[i]);
}
printf("\n");
free(ptr);
return 0;
}
运行结果为:
//Consequence 06
请输入一个整数(输入-1表示结束:)5
请输入一个整数(输入-1表示结束:)23
请输入一个整数(输入-1表示结束:)4
请输入一个整数(输入-1表示结束:)51
请输入一个整数(输入-1表示结束:)22
请输入一个整数(输入-1表示结束:)31
请输入一个整数(输入-1表示结束:)1
请输入一个整数(输入-1表示结束:)9
请输入一个整数(输入-1表示结束:)-1
输入的整数分别是:5 23 4 51 22 31 1 9
C语言的内存布局
根据内存由低到高分别做如下划分:
- 代码段(text segment)
- 数据段(initialized data segment)
- BSS段(BSS segment / Uninitialized data segment)
- 栈(stack)
- 堆(heap)
代码段
代码段通常用来存放程序执行代码的一块内存区域。这部分区域的大小再程序运行前就已经确定,并且内存区域通常值属于只读。在代码段中,也有可能包含一些只读的常数变量,如字符串常量等等。
数据段
数据段通常用来存放已经初始化的全局变量和局部静态变量。
BSS段
BSS段通常用来存放程序中为初始化的全局变量的一块内存区域。BSS是英文Black Started by Symbol的简称。这个区段中的数据在程序运行前将被自动初始化为0。
堆
前面学习的动态内存管理,实际上就是在这里面进行的。堆里面主要放一些动态的内存段,能够扩展和缩小。
栈
大家平时所听到的堆栈这个词,实际上就是指的栈。栈是函数执行的内存区域,通常和堆共享同一片区域。堆和栈是C语言运行时的重要元素之一。而它们之间也有很大的不同;堆由程序员手动申请,而栈由系统自动分配;堆由程序员手动释放,而栈由系统自动释放;堆的生存周期由程序员来决定,并且不同函数之间可以自由访问,而栈的生存周期由函数调用开始到函数返回时结束,函数之间的局部变量不可以互相访问。
高级宏定义
作为C语言的三大预处理命令之一,宏定义的作用时替换。但是,宏定义就算再复杂,也只是替换不做任何的计算或者表达式求解。
不带参数的宏定义
这种就是我们常见的直接替换:
#define PI 3.14
注意:
- 为了和普通变量区分,宏的名字一般约定为全大写
- 宏定义只是简单的替换,且是在编译前就处理好了,所以编译器不会对宏定义的语法进行检查
- 宏定义的作用范围是从定义开始到程序的结束
- 可以使用#undef命令中止宏定义的作用域
- 宏定义允许嵌套(在宏定义中使用已定义的宏)
带参数的宏定义
C语言的宏定义可以带参数。和函数类似,在宏定义里出现的叫做形参,而在调用的时候实际传递的叫做 实参。
如:
#define MAX(x, y) (((x) > (y)) ? (x) : (y))
这个就是用来比较x和y哪个更大:
//Example 07
#include <stdio.h>
#define MAX(x, y) (((x) > (y)) ? (x) : (y))
int main(void)
{
int x, y;
printf("请输入两个整数:");
scanf("%d %d", &x, &y);
printf("%d更大!", MAX(x, y));
return 0;
}
运行结果如下:
//Consequence 08
请输入两个整数:3 5
5更大!
另外,参数的最外层建议加上一个小括号来确保优先级,不然很可能会出现隐式的bug。
内联函数
由于预编译命令在编译之前就已经处理好了。而函数每次的调用却还要申请栈空间。的确,使用含参宏定义效率确实要更高。
不过,有的时候却会出现一些bug,比如:
//Example 09
#define SQUARE(x) ((x) * (x))
#include <stdio.h>
int main(void)
{
int i = 1;
while (i <= 10)
{
printf("%d的平方根是%d\n", i-1, SQUARE(i++));
}
return 0;
}
结果为:
//Consequence 09
2的平方根是1
4的平方根是9
6的平方根是25
8的平方根是49
10的平方根是81
这是怎么回事呢?
我们刚刚说过,宏定义只是简单的替换。那么SQUARE(i++)
最终会被替换为((i++) * (i++))
,那么没调用一次宏,就要自增两次。
那么我又想用宏定义,却又向避开这个bug,怎么办呢?
就是调用内联函数:
inline int square(int);
和普通的函数定义一样,只不过在前面加上inline
即可。指定一个函数为内联函数,那么系统就会像处理宏定义那样,将整个函数直接在main
函数中展开。
不过,内联函数也不是万能的。虽然节省了运行的时间,但是每个地方都要进行替换,实际上也增加了编译的时间。再者,其实现在的编译器也很聪明,对内联函数也有一套像寄存器变量那样的优化机制,并不是所有你声明的内联函数都能够成为内联函数,而有些普通函数也有可能会成为内联函数。
一些小技巧
#和##
在含参宏定义中,#运算符后面应该跟一个参数,预处理器会把这个参数转换成一个字符串:
//Example 10
#define STR(s) # s
#include <stdio.h>
int main(void)
{
printf("%s\n", STR(TechZone));
return 0;
}
结果为:
//Consequence 10
TechZone
虽然笔者传入的不是字符串形式,但是#
将其变为了字符串,因此可以直接以%s
的形式输出。
并且传入字符中,所有的保留字符都会做转义处理,比如\
会被替换为\\
。存在多个空白字符的时候,会被替换为一个空格。
而##运算符被成为连接运算符,如:
//Example 11
#define TOGETHER(x, y) x ## y
#include <stdio.h>
int main(void)
{
printf("%d\n", TOGETHER(5, 20));
return 0;
}
结果为:
//Consequence 11
520
可变参数
之前学习了如何让函数支持可变参数,带参数的宏定义也支持使用可变参数:
#define SHOWLIST(...) printf(#__VA_AGES__)
如:
//Example 12
#define SHOWLIST(...) printf(#__VA_AGES__)
#include <stdio.h>
int main(void)
{
SHOWLIST(TechZone, HarrisWilde, C);
return 0;
}
由于在VS2019下无法支持编译,这次的结果使用Linux下的GCC来执行:
//Consequence 12 in GCC
TechZone, HarrisWilde, C
可变参数是允许存在空参数的,如果是空参数,则##前面的逗号也会一起被去掉,避免导致参数数量不一致:
//Example 13
#define PRINT(format, ...) printf(#format, ##__VA_AGES__)
int main(void)
{
PRINT(num = %d\n, 10);
PRINT(Hello, world!\n);
return 0;
}
结果为:
//Consequence 13 in GCC
num = 10
Hello, world!
函数的基础知识到这里就结束了,希望对大家有所帮助!