虚拟内存
现代OS都会给应用程序每个进程分配独立的虚拟地址空间。这样做的目的是为了保证安全,防止应用程序破坏内存空间。。这和 C 语言本 身并没有关系,而是操作系统和 CPU 协同工作的结果。即:应用程序面对的是虚拟地址空间。
作用域
有如下三种:
-
全局变量
在函数外定义的变量,默认就是全局变量。全局变量在任何地方都是可见的。当程序被分割到多个源码文件进行编译时,声明为全局变量的变量也是可以从其他源码文件中引用。 -
文件内部的静态变量
同样是定义在函数外部,一旦有static修饰,就变成了文件内部的静态变量,作用域就仅限定在当前所在的源代码文件中。通过static修饰的变量(包括函数),对于其他源代码是不可见的。 -
局部变量
指在函数中声明的变量。通常在语句块结束的时候释放。
存储期(storage duration)
变量除了作用域的不同,还有存储期的不同。
-
静态存储期(static storage duration)
全局变量,文件内的static变量以及用static修饰的局部变量都是静态存储期,都可以称为静态变量。注意:静态变量的寿命从程序运行时开始,到程序关闭时结束,即静态变量一直存在于内存的同一个地址上。 -
自动存储期(auto storage duration)
没有指定static的局部变量,持有自动存储期。这样的变量称为自动变量,这样的变量是程序进入函数时统一进行内存区域分配的。这个特性使用“栈”来实现。 -
通过malloc()动态分配的内存
它的寿命一直延续到使用free()释放它为止。
print_addr.c示例,演示各种不同作用域,存储期变量的指针地址
/**
* 演示不同作用域、存储期的变量在内存中的地址分布
* @author liu
*/
#include <stdio.h>
#include <stdlib.h>
// 全局变量
int global_variable;
// 文件内静态变量
static int file_static_variable;
void func1()
{
// 局部变量
int func1_variable;
// 局部静态变量
static int func1_static_variable;
printf("&func1_variable.. %p\n", &func1_variable);
printf("&func1_static_variable.. %p\n", &func1_static_variable);
}
void func2()
{
int func2_variable;
printf("&func2_variable..%p\n", &func2_variable);
}
int main(int argc, char *argv[])
{
int *p;
// 打印两个函数指针
printf("&func1..%p\n", func1);
printf("&func2..%p\n", func2);
// 打印字符常量指针
printf("string literal..%p\n", "abc");
// 打印全局变量指针
printf("&global_variable..%p\n", &global_variable);
// 打印文件内静态变量指针
printf("&file_static_variable...%p\n", &file_static_variable);
// 执行两个函数,打印局部变量和局部静态变量
func1();
func2();
// 用malloc函数分配内存块
p = malloc(sizeof(int));
// 打印malloc分配内存块指针
printf("malloc address..%p\n", p);
return 0;
}
result:
&func1..0x4005d6
&func2..0x40060b
string literal..0x4007d8
&global_variable..0x601038
&file_static_variable...0x601030
&func1_variable.. 0x7ffc1d1fd04c
&func1_static_variable.. 0x601034
&func2_variable..0x7ffc1d1fd04c
malloc address..0xd246b0
可以看出函数体,字符串分配在一起, 静态变量(全局变量,文件内静态变量,静态局部变量)分配在一起, 局部变量分配在一起,最后malloc内存块再单独的位置。
其中func1_variable和func2_variable完全一样,正好在同一个地址分配,因为func1()和func2()是前后调用,func1_variable是先释放完再分配给func2_variable。
它们在内存空间的分布大概是这样:
函数和字符串常量
只读内存区域
函数(即程序自身)和字符串常量被配置在内存里相邻的地址上。如今大多数OS都是将函数和字符串常量存储在一个只读内存区域。原因是两者本身都不需要再改写。另外在同一份程序被启动多次时,这样能能不同进程之间共享以实现节约物理内存。
指向函数的指针
函数在表达式中可以解读为指向函数的指针。所以本质上也是指针,可以赋给指针型变量。示例如下:
#include <stdio.h>
/**
* 演示函数型指针的赋值,使用
*
*/
void printhello()
{
puts("hello world c programming");
}
int main(int argc, char *argv[])
{
void (*fun)() = printhello;
fun();
return 0;
}
静态变量
什么是静态变量
即从程序开始运行到结束持续存在的变量,所以静态变量总是在虚拟地址空间上占据固定的区域。静态变量前一节已经描述过,即:全局变量,文件内static变量以及static局部变量。这些静态变量作用域各不相同,编译和连接时具有不同的意义,但是在运行时都是以相似的方式被使用。
分割编译和连接
一定规模的C语言工程源代码一定是分割到多个文件中,这些源文件可以各自编译最后连接起来。函数和全局变量,如果名称相同,即使跨越了多个源文件也被作为相同的对象来看待,进行这项工作的是一个被称为“链接器”的程序。
为了在链接器中将名称结合起来,各目标代码大多都具备一个符号表(symbol table)。如在UNIX中,可以使用nm命令窥视符号表的内容。
可以使用 gcc -c xx.c 生成名称为xx.o的目标代码,再使用nm xx.o窥探符号表。示例如下:
#include <stdio.h>
int global;
int static static_global;
int main(int argc, char *argv[])
{
int local;
int static static_local;
return 0;
}
可以看出全局变量使用C标记,文件内静态变量, static局部变量标记为b, 局部变量local符号表中没有出现。
局部变量local没有出现好理解,它的作用域是main函数范围,不需要跟其他文件链接,并且它的地址是在运行时决定,不属于链接器的范围。
文件内static变量和static局部变量它们的作用域也限制在文件内为什么也出现在符号表中?原因是静态变量必须提前要给它们分配一些地址。
链接器就是根据这些符号表信息,给这些只是名称的对象分配地址。
自动变量(stack: 栈)
内存区域重复使用
&func1_variable.. 0x7ffc1d1fd04c
&func2_variable..0x7ffc1d1fd04c
前面的示例演示了两个局部变量分配在了同样的内存地址,所以该区域是重复利用的。当局部变量所在的函数执行完成之后,自动变量所在的内存空间就释放了。每次分配给自动变量的内存地址都不是固定的。
函数调用
函数调用本质上一个栈结构,在一个函数中调用另一个函数即压栈,函数调用完即出栈。
示例如下:
参数在栈中堆积方式:
基本的思想是:
- 在调用方,函数参数从后向前堆积在栈中,即假设函数有两个参数,第二个参数先入栈,第一个参数后入栈。原因后面会讲。
- 和函数相关联的返回信息(返回地址等)也被堆积在栈中,返回地址即函数返回后应该返回的地址。
- 跳转到作为被调用对象的函数地址。
- 栈为当前函数所使用的自动变量增长所需大小的内存区域。
- 在函数的执行过程中,为了进行复杂的表达式运算,有时候会将计算过程中的值 放在栈中。
- 一旦函数调用结束,局部变量占用的内存区域就被释放,并且使用返回信息返回 到原来的地址。
可变长参数
C采用函数参数从后往前往栈中堆积,这个做法跟java语言相反,看上去也违反习惯。 C这么实现的目的是实现可变参数列表的功能,如最常见的printf函数,参数列表是可变的。如:prinf("%d, %s\n", 100, “hello”);
调用支持可变参数列表函数的栈结构:
这样即使可变参数列表再长,也能顺利找到第一个参数的地址,再依次取出可变参数列表。
自定义一个可变列表函数
#include <stdio.h>
#include <stdarg.h>
#include <assert.h>
/**
* 自定义的printf,实现可变参数列表效果
*/
void tiny_printf(char *format, ...)
{
va_list ap;
va_start(ap, format);
for (int i = 0; format[i] !='\0'; i++) {
switch (format[i]) {
case 's':
printf("%s", va_arg(ap, char*));
break;
case 'd':
printf("%d", va_arg(ap, int));
break;
default:
assert(0);
}
}
va_end(ap);
putchar('\n');
}
int main(int argc, char *argv[])
{
tiny_printf("sddddd", "result:", 1, 2, 3, 4, 5);
return 0;
}
递归调用
递归调用是指对函数自身的调用。C在栈中分配自动变量内存,该区域可重复使用,采用该结构可方便实现递归调用。
现实中递归调用常常用来实现树结构遍历,图结构遍历,快速排序等。
快速排序(quicksort)基本思路:
- 从需要排序的序列中找到一个基准值(pivot)
- 将需要排序的数据按照小于pivot和大于pivot进行分类
- 对分类后的两组数据再进行1步骤和2步骤的处理
大体算法思路是:
- 从左到右,检索比pivot大的数据
- 从右到左,检索比pivot小的数据
- 如果两个方向都能检索到数据,执行交换
- 重复1-3的步骤,直到左右检索下标冲突为止
实现快速排序的源代码如下:
#include <stdio.h>
/**
* 快速排序算法C语言实现
*
*/
void quick_sort_sub(int *data, int left, int right)
{
int left_index = left;
int right_index = right;
int pivot = data[(left + right) / 2];
while (left_index <= right_index) {
// 两边开始遍历
for (; data[left_index] < pivot; left_index ++)
;
for (; data[right_index] > pivot; right_index --)
;
// 符合条件的交换顺序
if (left_index <= right_index) {
int tmp = data[left_index];
data[left_index] = data[right_index];
data[right_index] = tmp;
left_index ++;
right_index --;
}
}
// 基准点左侧的递归执行排序
if (right_index > left) {
quick_sort_sub(data, left, right_index);
}
// 基准点右边的递归执行排序
if (left_index < right) {
quick_sort_sub(data, left_index, right);
}
}
void quick_sort(int *data, int size)
{
quick_sort_sub(data, 0, size - 1);
}
int main(int argc, char *argv[])
{
int a[] = {4, 3, 67, 1, 0, -5, 45, 23, 11, 34, 10};
quick_sort(a, 11);
for (int i = 0; i < 11; i ++) {
printf("a[%d]: %d, ", i, a[i]);
}
puts("");
}
利用malloc()来进行动态内存分配(heap: 堆)
malloc()可以根据参数指定的尺寸来分配内存块,它将返回指向内存块初始位置的指针,通常用于动态分配结构体的内存。
int *p = malloc(size);
free(p);
以上是mallc使用的基本方式。
像这样能动态的进行内存分配,并且可以通过任意的顺序释放的内存区域,称为heap。在英文中heap是指像山一样堆得高高的东西(如干草堆),malloc()就像是从内存堆成的山上扣了一块内存。
在链表结构中malloc用的很广泛,示例:
#include <stdio.h>
#include <stdlib.h>
typedef struct struct_node {
char *name;
struct struct_node *next;
} Node;
void printLinkedlist(Node *head)
{
Node *next = head -> next;
while(next) {
printf("Node: %s\n", next -> name);
next = next -> next;
}
}
int main(int argc, char *argv[])
{
Node *head = malloc(sizeof(Node));
Node *node = malloc(sizeof(Node));
node -> name = "1";
head -> next = node;
printLinkedlist(head);
return 0;
}