C语言——一些进阶知识(一)
C语言——存储相关
1. 局部变量与全局变量
- 局部变量函数运行结束后即释放。
- 局部变量会在作用域内屏蔽同名的全局变量。
2. 作用域与链接
2.1 作用域
2.1.1 代码块作用域(block scope)
- 在代码块中定义的变量,具有代码块作用域。作用范围是从变量定义的位置开始,到标志该代码块结束的右大括号(
}
)处。 - 尽管函数的形式参数不在大括号内定义,但其同样具有代码块作用域,隶属于包含函数体的代码块。
以前不知道括号有什么用,原来可以用来划分作用域。
1 |
|
2.1.2 文件作用域(file scope)
- 任何在代码块之外声明的标识符都具有文件作用域,作用范围是从它们的声明位置开始,到文件的结尾处都是可以访问的。
- 尽管函数的形式参数不在大括号内定义,但其同样具有代码块作用域,隶属于包含函数体的代码块。
1 |
|
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 |
|
如果修改为普通局部变量,则函数执行结束时释放:
1 | void func(void) { |
有时候没有用 extern
关键字声明也不会报错,是因为隐式声明了(说白了还是声明了)。
test.c
1 | void func(void); |
test1.c
1 |
|
从上面的例子可以看出编译器按照 main()
到其他函数的顺序编译,count
认为是在 test.c
中定义的,而在 test1.c
视为声明。
test1.c
1 |
|
4. 动态内存管理
-
malloc
申请动态内存空间
-
free
释放内存空间
-
calloc
申请并初始化一系列内存空间
-
realloc
重新分配内存空间
4.1 malloc
函数原型:
1 | void *malloc(size_t size); |
-
malloc
函数向系统申请分配 size 个字节的内存空间,并返回一个指向这块空间的指针。 -
如果函数调用成功,返回一个指向申请的内存空间的指针,由于返回类型是 void 指针
(void *)
,所以它可以被转换成任何类型的数据;如果函数调用失败,返回值是 NULL 。另外,如果 size 参数设置为 0 ,返回值也可能是 NULL ,但这并不意味着函数调用失败。
用法如下:
1 |
|
4.2 free
函数原型:
1 | void free(void *ptr); |
free 函数释放 ptr 参数指向的内存空间。该内存空间必须是由 malloc , calloc 或 realloc 函数申请的。否则,该函数将导致未定义行为。如果 ptr 参数是 NULL ,则不执行任何操作。
注意:该函数并不会修改 ptr 参数的值,所以调用后它仍然指向原来的地方(变为非法空间)。
下面例子将演示该事实:
1 |
|
内存泄漏
注意:如果申请动态内存不及时释放可能导致 内存泄漏 。
某些高级语言有垃圾回收机制,在使用完动态分配的内存后会自动释放,但是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 |
|
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 |
|
5. C语言的内存布局
5.1 内存布局实例及特点分析
1 |
|
由上图各种变量的地址分配情况可以看出,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:
1 | ''' |
观察上面的运行结果,重新多给 ptr1 分配一个 int 大小的空间,可以看到,ptr1 之后的空闲空间还可分配,所以首地址不变,在 ptr1 的基础上扩充即可。
1 | ''' |
观察上面的运行结果显示,给 ptr1 再扩展到 20 * sizeof(int) 时,其后的空间不足,所以重新开辟了一片空间。
实例3:
这是一个有趣的例子,看下面的代码:
1 |
|
由于 char 地址空间不够存放一个 int ,于是覆盖了相邻的连续区间。
具体解释见注释部分。