欢迎关注博主 Mindtechnist 或加入【Linux C/C++/Python社区】一起探讨和分享Linux C/C++/Python/Shell编程、机器人技术、机器学习、机器视觉、嵌入式AI相关领域的知识和技术。
C语言作用域与内存布局
- 作用域
- 1. 局部变量
- 2. 静态局部变量
- 3. 全局变量
- 4. 静态全局变量
- 5. extern声明全局变量
- 6. 全局函数和静态函数
- 内存布局
- 1. 内存分区
- 2. 存储类型
- 3. 内存操作函数
专栏:《精通C语言》
作用域
C语言变量的作用域分为:
- 代码块作用域(代码块是大括号{}之间的一段代码)
- 函数作用域
- 文件作用域
1. 局部变量
局部变量也叫auto自动变量(auto关键字可以省略不写),一般情况下代码块{}内部定义的变量都是局部变量,它有如下特点:
- 在一个函数内定义,只在函数范围内有效
- 在复合语句中定义,只在复合语句中有效
- 随着函数调用的结束或复合语句的结束,即作用域的结束,局部变量的生命周期也结束
- 如果没有给局部变量赋初值,那么随机变量的值为随机值
#include <stdio.h>
void test()
{
//auto只能出现在{}内部
auto int b = 10; //相当于 int b = 10; //auto可省略
}
int main(void)
{
//b = 100; //error, 在main作用域中没有b, b的生命周期在test()内部
if (1)
{
//在复合语句中定义,只在复合语句中有效
int a = 10;
printf("a = %d\n", a);
}
//a = 10; //error离开if()的复合语句,a生命周期结束
return 0;
}
2. 静态局部变量
static修饰的局部变量称为静态局部变量
- static局部变量的作用域也是在定义的函数内有效
- static局部变量的生命周期和程序运行周期一样,同时staitc局部变量的值只初始化一次,但可以赋值多次(记忆功能)
- static局部变量若未赋以初值,则由系统自动赋值,数值型变量自动赋初值0,字符型变量赋空字符
#include <stdio.h>
void fun1()
{
int i = 0;
i++;
printf("i = %d\n", i);
}
void fun2()
{
//静态局部变量,没有赋值,系统赋值为0,而且只会初始化一次
static int a;
a++;
printf("a = %d\n", a);
}
int main(void)
{
fun1();
fun1();
fun2();
fun2();
return 0;
}
3. 全局变量
- 在函数外定义,可被本文件及其它文件中的函数所使用,如果其它文件中的函数想要调用此变量,必须通过extern声明为外部定义
- 全局变量的生命周期和程序运行周期一样
- 不同文件的全局变量不可重名
4. 静态全局变量
static修饰的全局变量称为静态全局变量
- 在函数外定义,作用域被限制在所定义的文件中
- 不同文件静态全局变量可以重名,但作用域不冲突
- static全局变量的生命周期和程序运行周期一样,同时staitc全局变量的值只初始化一次
5. extern声明全局变量
通过extern声明一个变量,表示这个变量在别的文件中已经定义了,这里只是声明,而不是定义。
6. 全局函数和静态函数
在C语言中函数默认都是全局的,使用关键字static可以将函数声明为静态,函数被声明为static静态函数就意味着这个函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数都没用。对于不同文件中的staitc函数名字可以相同。
注意:
- 允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,互不干扰。
- 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用。
- 所有的函数默认都是全局的,意味着所有的函数都不能重名,但如果是staitc函数,那么作用域是文件级的,所以不同的文件static函数名是可以相同的。
内存布局
1. 内存分区
C源代码经过预处理、编译、汇编、链接4步后生成一个可执行程序。
在 Linux 下,程序是一个普通的具有可执行权限的文件,以下列出一个二进制可执行文件的基本情况:
通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有时候也可以把data和bss合起来叫做静态区或全局区)。
-
代码区:存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。
-
全局初始化数据区/静态数据区(data段):该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。
-
未初始化数据区(又叫 bss 区):存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
-
代码区(text segment)
加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。 -
未初始化数据区(BSS)
加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。 -
全局初始化数据区/静态数据区(data segment)
加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。 -
栈区(stack)
栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。 -
堆区(heap)
堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
2. 存储类型
类型 | 作用域 | 生命周期 | 存储位置 |
---|---|---|---|
auto变量 | 代码块{}内 | 当前函数 | 栈区 |
static局部变量 | 代码块{}内 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern变量 | 整个程序 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
static全局变量 | 当前文件 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern函数 | 整个程序 | 整个程序运行期 | 代码区 |
static函数 | 当前文件 | 整个程序运行期 | 代码区 |
register变量 | 代码块{}内 | 当前函数 | 运行时存储在CPU寄存器 |
字符串常量 | 当前文件 | 整个程序运行期 | data段 |
3. 内存操作函数
- memset
#include <string.h>
void *memset(void *s, int c, size_t n);
功能:将s的内存区域的前n个字节以参数c填入
参数:
s:需要操作内存s的首地址
c:填充的字符,c虽然参数为int,但必须是unsigned char , 范围为0~255
n:指定需要设置的大小
返回值:s的首地址
- memcpy/memmove
#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
功能:拷贝src所指的内存内容的前n个字节到dest所值的内存地址上。
参数:
dest:目的内存首地址
src: 源内存首地址,注意, dest和src所指的内存空间不可重叠
n: 需要拷贝的字节数
返回值:dest的首地址
memmove()功能用法和memcpy()一样,区别在于dest和src所指的内存空间重叠时,memmove()仍然能处理,不过执行效率比memcpy()低些。
- memcmp
#include <string.h>
int memcmp(const void *s1, const void *s2, size_t n);
功能:比较s1和s2所指向内存区域的前n个字节
参数:
s1:内存首地址1
s2:内存首地址2
n:需比较的前n个字节
返回值:
相等:=0
大于:>0
小于:<0