以下是对这组 C 语言笔记核心知识的总结与补充,围绕变量作用域、生存期、存储类型及数组传参展开,帮你梳理清晰 C 语言变量管理的底层逻辑:
一、变量的作用域与可见性
1. 作用域分类
- 局部作用域:花括号
{}
包裹的范围(函数体、代码块),变量仅在该范围内有效,出了范围“不可见”。 - 全局作用域:花括号外部的变量(文件顶部定义),默认在整个程序可见(跨文件需
extern
配合 )。 - 函数形参作用域:局限于函数体,等同局部变量。
2. 可见性规则(命名冲突处理)
- 先定义后使用:用变量前必须定义/声明,否则编译器报错。
- 无嵌套作用域:同名不干扰:两个独立作用域(如不同函数)的同名变量,互不影响。
- 同作用域:同名不允许:同一作用域(如单个函数、单个文件全局区),不能重复定义同名变量(类型不同也不行 )。
- 嵌套作用域:内层屏蔽外层:嵌套作用域(如函数内嵌套代码块)定义同名变量时,外层变量被“屏蔽” ,优先用内层;退出内层后,外层恢复可见。
示例补充:
// 全局作用域
int i = 10;
void func() {
// 内层作用域,屏蔽全局 i
int i = 20;
printf("%d", i); // 输出 20
}
int main() {
func();
// 全局 i 恢复可见
printf("%d", i); // 输出 10
return 0;
}
#include <stdio.h>
// 全局变量
int global_var = 100;
void func() {
// 局部变量,屏蔽全局同名变量
int global_var = 200;
printf("函数内局部 global_var:%d\n", global_var); // 输出 200
}
int main() {
printf("main 中全局 global_var:%d\n", global_var); // 输出 100
func();
// 离开 func 后,局部变量消失,恢复全局变量
printf("main 中再次访问全局 global_var:%d\n", global_var); // 输出 100
return 0;
}
二、变量的生存期(“活多久” + 存储管理 )
变量生存期是从分配内存到释放内存的周期,决定变量存在时长,分两类:
1. 静态生存期
- 适用变量:全局变量、
static
修饰的局部变量(静态局部变量 )。 - 存储区域:程序运行前(编译/链接阶段 ),在静态存储区(全局区 )分配内存;程序结束时释放。
- 特性:
- 全局变量:作用域默认覆盖整个程序(跨文件用
extern
扩展 )。 - 静态局部变量:
static
修饰后,作用域仍局限于当前函数/代码块,但生存期与程序同周期;多次进入作用域时,值会保留(“记忆性” )。
- 全局变量:作用域默认覆盖整个程序(跨文件用
示例补充:
void count() {
// 静态局部变量,仅初始化一次
static int num = 0;
num++;
printf("%d ", num);
}
int main() {
count(); // 输出 1
count(); // 输出 2(保留上次值)
count(); // 输出 3
return 0;
}
三:两种变量:
1. 全局变量(静态存储区,程序周期存活 )
#include <stdio.h>
// 全局变量,未手动初始化,编译器自动赋 0
int global_uninit;
int main() {
printf("全局未初始化变量:%d\n", global_uninit); // 输出 0
return 0;
}
说明:全局变量存在静态存储区,未初始化时自动赋 0(区别于局部变量未初始化是随机值 )。
静态局部变量(static 修饰,“记忆性” )
#include <stdio.h>
void count() {
// 静态局部变量,程序运行前初始化,仅执行 1 次
static int num = 0;
num++;
printf("静态局部变量 num:%d\n", num);
}
int main() {
count(); // 输出 1
count(); // 输出 2(保留上次值)
count(); // 输出 3
return 0;
}
说明:static 修饰局部变量后,生存期变为程序周期(存在静态存储区 ),作用域仍局限于函数内,但值会 “记住” 上次结果。
static+全局变量:
限制变量进作用于本文件
2. 动态生存期
- 适用变量:普通局部变量(未用
static
修饰 )、动态分配内存(malloc
等 )的变量。 - 存储区域:进入作用域(函数调用、代码块执行 )时,在栈区分配内存;退出作用域时,自动释放内存(动态分配需手动
free
)。 - 特性:作用域与生存期一致,离开作用域后变量“消失”,值无法保留。
动态生存期(普通局部变量 )
#include <stdio.h>
void func() {
// 普通局部变量,进入函数时分配栈内存,离开销毁
int local_var = 10;
printf("函数内局部变量:%d\n", local_var); // 输出 10
}
int main() {
func();
// 错误:local_var 是 func 的局部变量,main 作用域不可见
// printf("%d", local_var);
return 0;
}
说明:普通局部变量存在栈区,作用域和生存期一致(函数执行期间 ),离开函数后变量销毁,外部无法访问。
四、特殊变量修饰(register
)
register
变量:- 意图:请求编译器将变量存到CPU寄存器(而非内存 ),加速访问(适合频繁用的变量,如循环计数器 )。
- 特性:
- 编译器可忽略请求(寄存器不足或变量不适合时 )。
- 无法取地址(
&
操作 ),因为寄存器不是内存地址。 - 现代编译器优化强,手动加
register
必要性降低,但理解原理仍有用。
示例:
// 建议存寄存器(编译器决定是否采纳)
register int i;
for (i = 0; i < 1000000; i++); // 频繁使用,适合寄存器
// &i; // 报错!无法取寄存器变量地址
#include <stdio.h>
int main() {
// 请求编译器将 i 存寄存器(实际由编译器决定)
register int i;
// 模拟频繁使用:循环计数
for (i = 0; i < 5; i++) {
printf("register 变量 i:%d\n", i);
}
// 错误:寄存器变量无法取地址
// &i;
return 0;
}
说明:
register 是 “请求”,编译器可能忽略(比如变量要取地址时,必然存在内存 )。
现代编译器优化强,即使不加 register,也可能自动将频繁使用的变量放寄存器,但理解语法仍有意义。
五、二维数组传参(C语言数组传参细节 )
- 传参语法:二维数组传参时,第二维长度必须显式写(第一维可省略 ),因为数组在内存是“连续线性存储”,需列数算偏移。
示例函数声明:// rows:行数,第二维 [4] 显式指定列数 void printArray2D(int arr[][4], int rows) { for (int i = 0; i < rows; i++) { for (int j = 0; j < 4; j++) { printf("%d ", arr[i][j]); } printf("\n"); } }
- 传参本质:数组名退化为“指向数组首元素的指针”,传参实际传地址;因此,函数内无法用
sizeof
获数组真实长度(需手动传行数、列数 )。
#include <stdio.h>
// 二维数组传参:第二维长度必须显式写(如 [4])
void printArray2D(int arr[][4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main() {
// 定义二维数组:3 行 4 列
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 传参:行数 3,第二维 [4] 匹配函数声明
printArray2D(arr, 3);
return 0;
}
输出结果:
1 2 3 4
5 6 7 8
9 10 11 12
说明:
二维数组传参时,第二维长度必须固定(函数声明写 [4] ),因为数组在内存是连续存储,需列数计算偏移。
函数内无法通过 sizeof(arr) 获取原数组长度(传参后退化为指针 ),需手动传行数 rows。
六、命名冲突(不同作用域同名变量 )
c
运行
#include <stdio.h>
// 全局作用域变量
int var = 100;
void func1() {
// 局部作用域同名变量,屏蔽全局
int var = 10;
printf("func1 内 var:%d\n", var); // 输出 10
}
void func2() {
// 另一个局部作用域,同名不影响 func1
int var = 20;
printf("func2 内 var:%d\n", var); // 输出 20
}
int main() {
printf("main 中全局 var:%d\n", var); // 输出 100
func1();
func2();
return 0;
}
输出:
plaintext
main 中全局 var:100
func1 内 var:10
func2 内 var:20
说明:不同作用域的同名变量互不干扰(func1 和 func2 的 var 是独立局部变量 ),全局变量可被局部作用域屏蔽。
如何使用ubuntu的终端编辑项目:
一个项目可以由多个.c文件编写函数,每个.c文件创建对应的头文件(.h文件)
使用乌班图linux终端创建多个文件:
vsp + 文件名+ .c
.c文件:创建函数。
打开新窗口后解决无法操作两个窗口的问题:
set mouse = a
从而让鼠标能操作多个窗口。
创建头文件(.h文件)
“sp” + ".c 文件对应的文件名 “+ " .h”
三种文件输入要求:
在(.c)文件中创建函数:
在头文件(.h)文件中声明要调用的函数:
在函数声明前加extern使头文件更加规范化。
强调函数的外部链接属性
C/C++ 中的函数默认具有外部链接(即可以被其他源文件访问),extern 关键字在这里主要是显式地强调这一点,让代码意图更清晰。例如:
c
运行
// math.h 头文件
extern int add(int a, int b); // 声明外部函数add
即使省略 extern,函数声明默认也是外部的,但加上 extern 可以让读者明确知道该函数的定义在其他文件中。
主函数格式:
在有头文件的情况下无需声明要调用的函数,引入头文件格式:
static+全局变量:
限制变量进作用于本文件
课上代码:
第一部分:求二维整型数组的外围元素之和
#include<stdio.h>
//求二维整型数组的外围元素之和:
int sumOFEdgeArray2D(int a[][4],int rows)
{
int sum=0;
int i,j;
for(i=0;i<rows;++i) // 遍历每一行
{
for(j=0;j<4;++j) // 遍历每行的4个元素
{
// 判断是否为外围元素:第一行、最后一行、第一列或最后一列
if(i==0||j==0||i==rows-1||j==3)
{
sum+=a[i][j]; // 累加外围元素值
}
}
}
return sum; // 返回总和
}
int main(void)
{
// 定义一个3行4列的二维数组
int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12};
// 计算数组的行数:总字节数/一行的字节数
int rows=sizeof(a)/sizeof(a[0]);
// 调用函数并输出结果
printf("%d\n",sumOFEdgeArray2D(a,rows));
return 0;
}
这段代码功能是计算二维数组外围元素的总和。对于3行4列的数组[[1,2,3,4],[5,6,7,8],[9,10,11,12]]
,外围元素是:1,2,3,4,5,8,9,10,11,12,总和为65。
第二部分:二维整型数组的行元素翻转
//二维整型数组的行元素翻转:
void reverse2D(int a[][4],int rows)
{
int i,j;
for(i=0;i<rows;++i) // 遍历每一行
{
// 每行只需要交换一半元素即可完成翻转
for(j=0;j<4/2;++j)
{
// 交换第j个和第(4-1-j)个元素
int t=a[i][j];
a[i][j]=a[i][4-1-j];
a[i][4-1-j]=t;
}
}
}
// 打印二维数组
void printarray2D(int a[][4],int rows)
{
int i,j;
for(i=0;i<rows;++i)
{
for(j=0;j<4;++j)
{
printf("%2d ",a[i][j]); // 格式化输出,占2个字符位置
}
printf("\n"); // 每行结束换行
}
}
int main(void)
{
int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12};
int rows=sizeof(a)/sizeof(a[0]);
reverse2D(a,rows); // 调用翻转函数
printarray2D(a,rows); // 打印翻转后的数组
return 0;
}
这段代码实现了二维数组每行元素的翻转。原数组每行[1,2,3,4]
会变成[4,3,2,1]
,以此类推。
第三部分:优化版二维数组行翻转(函数复用)
// 翻转一维数组的函数
void reverse(int a[],int len)
{
int i;
for(i=0;i<len/2;++i)
{
// 交换第i个和第(len-i-1)个元素
int t=a[i];
a[i]=a[len-i-1];
a[len-i-1]=t;
}
}
// 二维数组行翻转(调用一维翻转函数)
void reverse2D(int a[][4],int rows,int len)
{
int i;
// 对每一行调用一维数组翻转函数
for(i=0;i<rows;++i)
{
reverse(a[i],len);
}
}
// 打印二维数组(同前)
void printarray2D(int a[][4],int rows)
{
int i,j;
for(i=0;i<rows;++i)
{
for(j=0;j<4;++j)
{
printf("%2d ",a[i][j]);
}
printf("\n");
}
}
int main(void)
{
int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12};
// 计算列数:一行的总字节数/一个元素的字节数
int len=sizeof(a[0])/sizeof(a[0][0]);
int rows=sizeof(a)/sizeof(a[0]);
reverse2D(a,rows,len); // 调用翻转函数
printarray2D(a,rows);
return 0;
}
这是第二部分的优化版本,将一维数组的翻转功能封装成独立函数reverse()
,然后在reverse2D()
中调用,实现了代码复用,更符合模块化设计思想。
第四部分:字符串数组排序
#include<string.h> // 包含字符串处理函数库
// 打印字符串数组
void printStrings(char s[][100],int rows)
{
int i;
for(i=0;i<rows;++i)
{
puts(s[i]); // 输出字符串并换行
}
}
// 对字符串数组进行排序(冒泡排序)
void sortStrings(char s[][100],int rows)
{
int i,j;
for(i=0;i<rows-1;++i)
{
for(j=i+1;j<rows;++j)
{
// strcmp返回值>0表示s[i]大于s[j]
if(strcmp(s[i],s[j])>0)
{
// 交换两个字符串
char t[100];
strcpy(t,s[i]);
strcpy(s[i],s[j]);
strcpy(s[j],t);
}
}
}
}
int main(void)
{
char s[][100]={"hello","world","china"}; // 字符串数组
int rows=sizeof(s)/sizeof(s[0]); // 计算字符串个数
sortStrings(s,rows); // 排序
printStrings(s,rows); // 打印排序结果
return 0;
}
这段代码实现了字符串数组的排序功能,使用strcmp()
比较字符串大小,使用strcpy()
复制字符串,采用冒泡排序算法。对于输入{"hello","world","china"}
,排序后结果为china, hello, world
(按字母顺序)。