C语言——存储相关

1. 局部变量与全局变量

  • 局部变量函数运行结束后即释放。
  • 局部变量会在作用域内屏蔽同名的全局变量。

2. 作用域与链接

2.1 作用域

2.1.1 代码块作用域(block scope)

  • 在代码块中定义的变量,具有代码块作用域。作用范围是从变量定义的位置开始,到标志该代码块结束的右大括号(})处。
  • 尽管函数的形式参数不在大括号内定义,但其同样具有代码块作用域,隶属于包含函数体的代码块。

以前不知道括号有什么用,原来可以用来划分作用域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main() { // 作用域 1
int i = 1;
{ // 作用域 2
int i = 2;
{ // 作用域 3
int i = 3;
printf("i = %d\n", i);
}
{ // 作用域 4
int i = 4;
printf("i = %d\n", i);
}
printf("i = %d\n", i);
}
printf("i = %d\n", i);

return 0;
}

2.1.2 文件作用域(file scope)

  • 任何在代码块之外声明的标识符都具有文件作用域,作用范围是从它们的声明位置开始,到文件的结尾处都是可以访问的。
  • 尽管函数的形式参数不在大括号内定义,但其同样具有代码块作用域,隶属于包含函数体的代码块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

void func(void); // 声明 func 函数在后面实现了

int main(void) { // C99 规定,没有参数时,要加上 void,更规范
extern int count; // 此处声明 count 在文件其他地方定义了,告诉编译器先别报错,去后面找

func();
count++;
printf("In main, count = %d\n", count);

return 0;
}

int count; // 全局变量,作用域是整个文件

void func(void){
count++;
printf("In func, count = %d\n", count);
}

2.1.3 原型作用域(prototype scope)

原型作用域只适用于那些在函数原型中声明的参数名。函数在声明的时候可以不写参数的名字(但参数类型是必须要写上的),其实函数原型的参数名还可以随便写一个名字,不必与形式参数相匹配(当然,这样做没有任何意义!)。

2.1.4 函数作用域(function scope)

函数作用域只适用于 goto 语句的标签,作用将 goto 语句的标签暖制在同一个函数内部,以及防止出现重名标签。

2.1.5 区分定义和声明

2.2 链接属性

链接:将编译后的文件与库文件链接起来。

比如,printf 等函数并没有自己实现,但是可以直接调库使用。

static 可以用于保护一个变量不被其他文件修改。

同时如果希望一个函数只能在当前文件中执行,可以在函数前加上。

3. 生存期和存储类型

3.1 生存空间

3.2 存储类型

自动变量(auto)

  • 在代码块中声明的变量默认的存储类型就是自动变量,使用关键字 auto 来描述。

  • 由于这时默认的存储类型,所以不写 auto 是完全没问题的。(当某处要用局部变量屏蔽全局变量的话,可以使用 auto 作为提示)

寄存器变量(register)

  • 将一个变量声明为寄存器变量,那么该变量可能被存放于 CPU 的寄存器中。(由于寄存器数量有限,可能会分配失败,也可能编译器觉得另一个变量更适合放入 register,从而忽略程序员的想法。)
  • 寄存器变量和自动变量子在很多方面是一样的, 他们都拥有代码块作用域,自动存储期和空连接属性。
  • 寄存器变量无法通过取址符获取它的地址。(寄存器本身就没有分配地址空间,自身就是空间。)

静态变量(static)

声明为静态变量的局部变量,将不会随着函数执行的结束而被释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

void func(void);

void func(void) {
static int count = 0;

printf("count = %d\n", count);

count++;
}

int main() {
int i;

for (i = 0; i < 5; i++) func();

return 0;
}

如果修改为普通局部变量,则函数执行结束时释放:

1
2
3
4
5
6
7
void func(void) {
int count = 0;

printf("count = %d\n", count);

count++;
}

有时候没有用 extern 关键字声明也不会报错,是因为隐式声明了(说白了还是声明了)。

test.c

1
2
3
4
5
6
void func(void);
int count = 520;

int main() {
func();
}

test1.c

1
2
3
4
5
6
7
#include <stdio.h>

int count; // 此处只是声明 等价于 extern int count;

void func(void) {
printf("count = %d\n", count);
}

从上面的例子可以看出编译器按照 main() 到其他函数的顺序编译,count 认为是在 test.c 中定义的,而在 test1.c 视为声明。

test1.c

1
2
3
4
5
6
7
#include <stdio.h>

int count = 1; // 此处只是声明,如果再赋值则视为重复定义,编译器将报错

void func(void) {
printf("count = %d\n", count);
}

4. 动态内存管理

  • malloc

    申请动态内存空间

  • free

    释放内存空间

  • calloc

    申请并初始化一系列内存空间

  • realloc

    重新分配内存空间

4.1 malloc

函数原型:

1
void *malloc(size_t size);
  • malloc 函数向系统申请分配 size 个字节的内存空间,并返回一个指向这块空间的指针。

  • 如果函数调用成功,返回一个指向申请的内存空间的指针,由于返回类型是 void 指针 (void *) ,所以它可以被转换成任何类型的数据;如果函数调用失败,返回值是 NULL 。另外,如果 size 参数设置为 0 ,返回值也可能是 NULL ,但这并不意味着函数调用失败。

用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>

int main() {
int *ptr;

ptr = (int *)malloc(sizeof(int)); // 动态申请内存空间,并进行数据;类型的转换
if (ptr == NULL) { // 检查是否分配成功
printf("内存分配失败\n");
exit(1);
} else {
*ptr = 10; // 使用分配的空间
}

return 0;
}

4.2 free

函数原型:

1
void free(void *ptr);

free 函数释放 ptr 参数指向的内存空间。该内存空间必须是由 malloc , calloc 或 realloc 函数申请的。否则,该函数将导致未定义行为。如果 ptr 参数是 NULL ,则不执行任何操作。

注意:该函数并不会修改 ptr 参数的值,所以调用后它仍然指向原来的地方(变为非法空间)。

下面例子将演示该事实:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>

int main() {
int *ptr;

ptr = (int *)malloc(sizeof(int));
if (ptr == NULL) {
printf("内存分配失败\n");
exit(1);
} else {
*ptr = 10;
}

printf("free 前 *ptr = %d\n", *ptr);
free(ptr);
printf("free 后 *ptr = %d\n", *ptr);

return 0;
}

内存泄漏

注意:如果申请动态内存不及时释放可能导致 内存泄漏

某些高级语言有垃圾回收机制,在使用完动态分配的内存后会自动释放,但是C 语言没有垃圾回收机制。

可以释放动态内存,但是不能释放局部变量的值,因为局部变量存储在栈上,而不在堆中。

导致内存泄漏的主要原因有两种:

  • 隐式内存泄漏(即用完内存块没有及时使用 free 函数释放。)
  • 丢失内存块地址(分配内存后,指针指向其他地址,导致动态内存的地址丢失。此时若没有垃圾回收机制,将会导致该部分内存无法回收。)

4.3 calloc

函数原型:

1
void *calloc(size_t nmemb, size_t size);
  • calloc 函数在内存中动态地申请 nmemb 个长度为 size 的连续内存空间(即申请的总空间尺寸为 nmemb*size ),这些内存空间全部被初始化为 0 。

  • calloc 函数与 malloc 函数的一个重要区别是:

    • calloc 函数在申请完内存后,自动初始化该内存空间为 0 。
    • malloc 函数不进行初始化操作,里边的数据是随机的。

如果申请完一块动态内存,用着用着发现不够用了怎么办?

最好的方法是,再申请一片更大的动态内存,将之前的数据拷贝进去。(之所以用新的一部分,是为了让数据的空间局部性得到更好的利用,即每次申请的空间是连续的一块,而两次申请的内存并不连续。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
int *ptr1 = NULL;
int *ptr2 = NULL;

// 第一次申请内存空间
ptr1 = (int *)malloc(10 * sizeof(int));

// 进行若干操作之后发现 ptr1 申请的内存空间竟然不够用了!!!

// 第二次申请的内存空间
ptr2 = (int *)malloc(20 * sizeof(int));

// 将 ptr1 中的数据拷贝到 ptr2 申请的空间中
memcpy(ptr2, ptr1, 10);
free(ptr1); // 释放空间便于之后其他用户申请使用

// 对 ptr2 的空间进行操作

// 使用后释放空间
free(ptr2);

return 0;
}

4.4 realloc

可以修改动态申请的内存大小。

函数原型:

1
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
int i, 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 (i = 0; i < count; i++) printf("%d ", ptr[i]);
putchar('\n');
free(ptr);

return 0;
}

5. C语言的内存布局

5.1 内存布局实例及特点分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>

int global_uninit_var;
int global_init_var1 = 520;
int global_init_var2 = 880;

void func(void);

void func(void) {
;
}

int main(void) {
int local_var1;
int local_var2;

static int static_uninit_var;
static int static_init_var = 456;

char *str1 = "I love awellfrog.cc!";
char *str2 = "You are right!";

int *malloc_var = (int *)malloc(sizeof(int));

printf("addr of func -> %p\n", func);
printf("addr of str1 -> %p\n", str1);
printf("addr of str2 -> %p\n", str2);
printf("addr of global_init_var1 -> %p\n", &global_init_var1);
printf("addr of global_init_var2 -> %p\n", &global_init_var2);
printf("addr of static_init_var -> %p\n", &static_init_var);
printf("addr of static_uninit_var -> %p\n", &static_uninit_var);
printf("addr of global_uninit_var -> %p\n", &global_uninit_var);
printf("addr of malloc_var -> %p\n", malloc_var);
printf("addr of local_var1 -> %p\n", &local_var1);
printf("addr of local_var2 -> %p\n", &local_var2);

return 0;
}

由上图各种变量的地址分配情况可以看出,C 语言的内存布局有如下规律。

从 2 个局部变量和 2 个全局变量的地址可以看出,栈的地址分配从高位向低位分配,而其他则是从低位向高位分配。

5.2 内存区域

5.2.1 代码段(Text segment)

代码段通常是指用来存放 程序执行代码 的一块内存区域。这部分区域的大小再程序运行前就已经确定,并且内存区域通常属于 只读 。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

5.2.2 数据段(Initialized data segment)

数据段通常用来存放 已经初始化的全局变量局部静态变量

5.2.3 BSS 段(Bss segment/Uninitialized data segment)

通常是指用来存放程序中 未初始化的全局变量 的一块内存区域。BSS 是英文 Block Started by Symbol 的简称,这个区段中的数据在程序运行前将被自动初始化未数字 0。

5.2.4 堆

堆是用于存放进程运行中 被动态分配的内存段 ,它的大小并不固定,可动态扩展或缩小。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态添加到堆上;当利用 free 等函数释放内存时,被释放的内存从堆中被剔除。

5.2.5 栈

平时听得比较多得是堆栈,特指栈。栈是 函数执行的内存区域 ,通常和堆共享同一片区域。

函数的参数,函数的返回值 等都放在栈中。

堆和栈的区别

  • 申请方式:
    • 堆由程序员手动申请
    • 栈由系统自动分配
  • 释放方式:
    • 堆由程序员手动释放
    • 栈由系统自动释放
  • 生存周期:
    • 堆的生存周期由动态申请到程序员主动释放为止,不同函数之间均可自由访问
    • 栈的生存周期由函数调用开始到函数返回时结束,函数之间的局部变量不能互相访问
  • 发展方向:
    • 堆和其他区段一样,都是从低地址向高地址发展
    • 栈则相反,是由高地址向低地址发展

代码实例

实例1:

下面实例可以看出栈的地址由高-》低;而堆的地址由低-》高。

而且栈两个变量之间地址相差为 8,而堆的两个变量之间地址相差 32。可见栈堆内存的利用率更高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>

int main(void) {
int *ptr1 = NULL;
int *ptr2 = NULL;

ptr1 = (int *)malloc(sizeof(int));
ptr2 = (int *)malloc(sizeof(int));

printf("stack: %p -> %p\n", &ptr1, &ptr2);
printf("heap: %p -> %p\n", ptr1, ptr2);

return 0;
}

实例2:

1
2
3
4
5
6
7
8
'''
printf("heap: %p -> %p\n", ptr1, ptr2);

ptr1 = (int *)realloc(ptr1, 2 * sizeof(int)); // 扩展为 2 * sizeof(int)
printf("heap: %p -> %p\n", ptr1, ptr2);

return 0;
'''

观察上面的运行结果,重新多给 ptr1 分配一个 int 大小的空间,可以看到,ptr1 之后的空闲空间还可分配,所以首地址不变,在 ptr1 的基础上扩充即可。

1
2
3
4
5
6
7
8
'''
printf("heap: %p -> %p\n", ptr1, ptr2);

ptr1 = (int *)realloc(ptr1, 20 * sizeof(int)); // 扩展为 20 * sizeof(int)
printf("heap: %p -> %p\n", ptr1, ptr2);

return 0;
'''

观察上面的运行结果显示,给 ptr1 再扩展到 20 * sizeof(int) 时,其后的空间不足,所以重新开辟了一片空间。

实例3:

这是一个有趣的例子,看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(void) {
char a = 0, b = 0;
int *p = (int *)&b;

*p = 258; // 高位地址 1 0000 0010 低位地址
// a b

printf("a=%d b=%d\n", a, b);
return 0;
}

由于 char 地址空间不够存放一个 int ,于是覆盖了相邻的连续区间。

具体解释见注释部分。