嵌入式面试题:C 语言基础重点总结
题目1:
static 全局变量与普通全局变量,static 局部变量与普通局部变量,static 函数与普通函数的区别。
在 C 语言中,static
关键字具有多种作用,深刻影响着变量和函数的特性。以下从 static
全局变量、局部变量、函数三个维度,全面解析其与普通变量、函数的区别,并拓展相关应用场景。
1.1 static
全局变量 vs 普通全局变量
知识点详细解答
- 作用域
普通全局变量的作用域是整个工程(多个源文件),通过extern
关键字可在其他文件中引用。例如:c
// file1.c int global_var = 10; // file2.c extern int global_var; // 正确引用,使用 file1.c 中定义的 global_var
而static
全局变量的作用域仅限于定义它的文件,对其他文件不可见。// file1.c static int static_global_var = 20; // file2.c extern int static_global_var; // 编译错误,无法引用其他文件的 static 全局变量
- 链接属性
普通全局变量具有 外部链接属性,可在多个文件间共享;static
全局变量具有 内部链接属性,仅在本文件内有效。 - 内存分配
两者都存储于静态存储区,程序运行期间始终存在。
拓展
在大型工程中,若多个文件定义同名普通全局变量,会引发 “重复定义” 错误。而 static
全局变量能有效避免命名冲突,例如:多个团队开发不同模块时,可使用 static
全局变量隐藏模块内专用的全局数据,防止与其他模块命名冲突。
1.2 static
局部变量 vs 普通局部变量
知识点详细解答
- 生命周期
普通局部变量存储在栈中,函数执行结束后内存释放,生命周期随函数调用结束而终止。
static
局部变量存储在静态存储区,生命周期贯穿程序运行始终。例如:void count() { int normal_var = 0; static int static_var = 0; normal_var++; static_var++; printf("normal_var: %d, static_var: %d\n", normal_var, static_var); } // 多次调用 count 函数,normal_var 每次从 0 开始计数,而 static_var 持续累加
- 初始化
普通局部变量每次函数调用时重新初始化;static
局部变量仅在程序初次执行到定义处时初始化一次。 - 存储位置
普通局部变量在栈区,static
局部变量在静态存储区。
拓展
利用 static
局部变量的特性,可实现 “记忆” 功能。例如,统计某个函数被调用的次数:
int func_call_count() { static int count = 0; count++; return count;
}
每次调用 func_call_count
,count
都会记录调用次数,且无需在函数外部维护计数变量。
1.3 static
函数 vs 普通函数
知识点详细解答
- 作用域
普通函数的作用域是整个工程,其他文件通过extern
声明后可调用。
static
函数的作用域仅限于定义它的文件,对其他文件不可见。例如:c
// file1.c static void static_func() { printf("This is a static function.\n"); } void normal_func() { static_func(); } // 正确,file1.c 内可调用 static_func // file2.c extern void static_func(); // 编译错误,无法引用 file1.c 的 static 函数
- 链接属性
普通函数具有外部链接属性,static
函数具有内部链接属性。
拓展
在模块化编程中,static
函数可隐藏模块内部实现细节。例如,开发一个数学计算模块,内部辅助函数(如某个特定算法的子函数)可声明为 static
,避免被外部错误调用或依赖,同时防止与其他模块的函数名冲突,增强代码的封装性和可维护性。
通过对 static
相关特性的深入剖析,不仅能应对面试考查,更能在实际嵌入式开发中合理运用 static
,优化代码结构,提升程序的稳定性和可维护性。
题目2:
2、如何引用一个已经定义过的全局变量?全局变量可不可以定义在可被多个.C文件包含的头文件中?为什么?
2.1 引用已定义的全局变量
-
知识点:
extern
关键字的作用、变量声明与定义的区别、跨文件引用全局变量的流程。 -
详细解答:
extern
关键字用于声明一个全局变量,它的作用是告知编译器该变量已在其他地方定义,此时编译器不会为其分配内存空间。而变量的定义则是为变量分配内存并赋予初始值的过程。步骤与例子:
- 新建
data.c
,定义全局变量:// data.c int global_var = 50; // 定义全局变量,为其分配内存并初始化为 50
- 新建
main.c
,通过extern
声明后使用:// main.c extern int global_var; // 声明 global_var,告知编译器它在其他文件中定义 int main() { printf("global_var: %d\n", global_var); // 使用已定义的全局变量 return 0; }
这样通过
extern
实现了跨文件引用,main.c
无需重复定义global_var
,避免了内存的重复分配。 - 新建
2.2 全局变量不可定义在头文件
-
知识点:头文件的包含特性、C 语言的 “单一定义原则”(One Definition Rule, ODR)、在头文件定义全局变量导致编译错误的原因、正确的全局变量定义与声明方式。
-
详细解答:
头文件的特性是会被多个.c
源文件包含。C 语言的 “单一定义原则” 规定,一个变量在整个程序中只能有一个定义。如果在头文件中定义全局变量,当多个.c
文件包含该头文件时,每个.c
文件中都会有该全局变量的定义,这就违反了 “单一定义原则”,在链接阶段会导致重复定义的编译错误。正确步骤与例子:
- 定义头文件
global_def.h
,放置extern
声明:// global_def.h extern int common_global; // 仅声明,不分配内存
- 新建
global_impl.c
,定义全局变量:// global_impl.c #include "global_def.h" int common_global = 0; // 唯一的定义,分配内存并初始化
- 其他文件(如
use_global.c
)包含头文件后使用:// use_global.c #include "global_def.h" #include <stdio.h> int main() { common_global = 10; printf("common_global: %d\n", common_global); return 0; }
错误示例步骤:
- 新建
bad_header.h
,定义全局变量:// bad_header.h int error_global = 10; // 错误:在头文件中定义全局变量
file1.c
和file2.c
都包含它:// file1.c #include "bad_header.h"
// file2.c #include "bad_header.h"
编译时,
file1.c
和file2.c
中都有error_global
的定义,链接时会报错 “重复定义”。 - 定义头文件
拓展
在团队协作开发大型项目时,严格遵循 “头文件声明、.c
文件定义” 的规范管理全局变量至关重要。例如上述 global_def.h
(声明)和 global_impl.c
(定义)的组合,能确保多人开发不同模块时,全局变量的使用规范统一,避免命名冲突。若违反规范,如 bad_header.h
的错误示例,会导致编译错误,大幅增加调试成本。遵循该规范可有效提升代码的可维护性与团队协作效率,是嵌入式开发等场景中必须遵守的基本准则。
题目3:
#include <stdio.h>
int main(void) {int a, b, c, d;a = 10;b = a++; // 先赋值b=10,a自增到11c = ++a; // a先自增到12,赋值c=12d = 10 * a++; // 先计算10*12=120赋给d,a自增到13printf("b, c, d: %d, %d, %d", b, c, d); // 输出:10, 12, 120return 0;
}
知识点:
后置自增运算符(a++
)与前置自增运算符(++a
)的区别、赋值运算顺序。
详细解答:
- 首先看
int a, b, c, d; a = 10;
,这一步是定义整型变量a
、b
、c
、d
,并给a
赋初始值10
。 - 执行
b = a++;
:- 后置自增运算符
a++
的特点是 “先取值,后自增”。 - 先将
a
的当前值10
赋给b
,然后a
自增1
,此时a
的值变为11
。所以b = 10
,a = 11
。
- 后置自增运算符
- 执行
c = ++a;
:- 前置自增运算符
++a
的特点是 “先自增,后取值”。 a
先自增1
,值变为12
,再将这个值赋给c
。所以c = 12
,a = 12
。
- 前置自增运算符
- 执行
d = 10 * a++;
:- 先进行乘法运算,此时使用
a
的当前值12
,计算10 * 12 = 120
赋给d
。 - 然后
a
自增1
,值变为13
。所以d = 120
,a = 13
。
- 先进行乘法运算,此时使用
- 最后
printf("b, c, d: %d, %d, %d", b, c, d);
输出b
、c
、d
的值,即10, 12, 120
。
题目4:
#include <stdio.h>
int inc(int a) {return (++a); // a自增后返回
}
int multi(int* a, int* b, int* c) {return (*c = *a * *b); // *c赋值为*a乘*b,同时返回该值
}
typedef int(FUNC1)(int);
typedef int(FUNC2)(int*, int*, int*);
void show(FUNC2 fun, int arg1, int* arg2) {FUNC1 p = &inc;int temp = p(arg1); // 等价于temp = inc(arg1),arg1自增1后赋给tempfun(&temp, &arg1, arg2); // 调用multi,*arg2 = temp * arg1printf("%d\n", *arg2);
}
// 假设调用示例(需补充main函数):
// int x = 2, y;
// show(multi, x, &y);
// 执行过程:temp = inc(2) → temp=3;multi中*arg2 = 3*2=6 → 输出6
涉及知识点:
- 函数指针的定义与使用
- 值传递与指针传递的本质区别
- 前置自增运算符(
++a
)的特性 - 指针解引用(
*
)与内存操作 - 回调函数的实现逻辑
1. 函数指针的定义与使用
知识点解析
- 函数指针:是指向函数的指针变量,本质是函数在内存中的入口地址。
- 定义格式:
typedef 返回值类型(函数名)(参数列表);
,用于简化函数指针的声明。
举例说明
// 定义函数指针类型 FUNC1:指向参数为 int、返回值为 int 的函数
typedef int(FUNC1)(int);
// 定义函数指针类型 FUNC2:指向参数为三个 int*、返回值为 int 的函数
typedef int(FUNC2)(int*, int*, int*); // 具体函数实现
int inc(int a) { return ++a; } // 符合 FUNC1 类型
int multi(int* a, int* b, int* c) { // 符合 FUNC2 类型 return (*c = *a * *b); // 解引用指针,操作指向的变量
}
- 使用函数指针:
void show(FUNC2 fun, int arg1, int* arg2) { FUNC1 p = &inc; // 指针 p 指向 inc 函数(&可省略,函数名本身是地址) int temp = p(arg1); // 等价于 temp = inc(arg1) fun(&temp, &arg1, arg2); // 调用传入的函数指针 fun(此处为 multi) }
2. 值传递与指针传递的区别
知识点解析
- 值传递:函数参数是原始变量的副本,函数内修改不影响外部变量。
- 指针传递:函数参数是变量的地址,通过解引用(
*
)直接操作原始变量的内存。
举例对比
- 值传递示例(
inc
函数):int inc(int a) { a = a + 1; // 仅修改副本 a,外部变量不变 return a; } int x = 2; inc(x); // x 仍为 2,因为传递的是副本
- 指针传递示例(
multi
函数):int multi(int* a, int* b, int* c) { *c = *a * *b; // 通过指针修改 c 指向的变量(如 y) return *c; } int y; multi(&temp, &arg1, &y); // y 的值会被修改为 temp * arg1
3. 前置自增运算符(++a
)的特性
知识点解析
- 前置自增:
++a
先将变量自增 1,再使用新值(先增后用)。 - 与后置自增(
a++
)的区别:后置是先使用原值,再自增(先用后增)。
举例说明
int a = 2;
int b = ++a; // a 先变为 3,b = 3(前置)
int c = a++; // c = 3(取原值),a 变为 4(后置)
在 inc
函数中:
int inc(int a) { return ++a; // 等价于 a = a+1; return a;
}
int temp = inc(2); // temp = 3(a 先自增到 3,再返回)
4. 指针解引用与内存操作
知识点解析
- 解引用运算符(
*
):通过指针访问其指向的变量,格式为*指针变量
。 - 指针传递的本质:传递变量的内存地址,允许函数直接操作该地址上的数据。
举例说明
- 指针定义与解引用:
int x = 10; int* ptr = &x; // ptr 存储 x 的地址 *ptr = 20; // 通过指针修改 x 的值,此时 x = 20
multi
函数中的指针操作:int multi(int* a, int* b, int* c) { // *a 是 a 指向的值(如 temp),*b 是 b 指向的值(如 arg1) // *c = *a * *b:将乘积存入 c 指向的变量(如 y) return *c; }
5. 完整代码执行流程(含 main
函数)
#include <stdio.h> // 1. inc 函数:值传递,前置自增
int inc(int a) { return ++a; // 先自增,再返回新值(3)
} // 2. multi 函数:指针传递,计算乘积并存储
int multi(int* a, int* b, int* c) { return (*c = *a * *b); // *c = 3 * 2 = 6,返回 6
} // 3. 定义函数指针类型
typedef int(FUNC1)(int);
typedef int(FUNC2)(int*, int*, int*); // 4. show 函数:通过函数指针实现回调
void show(FUNC2 fun, int arg1, int* arg2) { FUNC1 p = inc; // 函数名直接作为指针,无需取地址符 & int temp = p(arg1); // 调用 inc(2),temp = 3(arg1 仍为 2,值传递不修改原值) fun(&temp, &arg1, arg2); // 调用 multi(&temp, &arg1, &y) printf("%d\n", *arg2); // 输出 y 的值:6
} // 5. main 函数:程序入口
int main() { int x = 2, y; // x=2(作为 arg1),y 用于存储结果 show(multi, x, &y); // 传入 multi 函数和参数 return 0;
}
分步执行解析
-
调用
show(multi, 2, &y)
:fun
指向multi
函数,arg1=2
(值传递,副本),arg2=&y
(指针,指向y
的地址)。
-
FUNC1 p = inc;
:p
是函数指针,指向inc
函数(无需&
,函数名即地址)。
-
int temp = p(arg1);
:- 等价于
temp = inc(2)
:inc
函数内a=2
,执行++a
后a=3
,返回3
。- 由于
arg1
是值传递,外部的x=2
未被修改,temp=3
。
- 等价于
-
fun(&temp, &arg1, arg2);
:- 等价于
multi(&temp, &arg1, &y)
:*a
是temp
的值(3),*b
是arg1
的值(2,因为arg1
未被修改)。- 计算
3 * 2 = 6
,通过*c = 6
将结果存入y
的内存地址(arg2
指向y
)。
- 等价于
-
输出结果:
y
的值为 6,最终打印6
。
易错点与核心区别总结
特性 | 值传递(如 inc 的 a ) | 指针传递(如 multi 的 *a ) |
---|---|---|
内存操作 | 操作副本,不影响原始变量 | 直接操作原始变量的内存地址 |
参数传递内容 | 变量的值 | 变量的地址 |
修改是否影响外部 | 否 | 是(通过解引用) |
- 函数指针的核心作用:实现回调机制,使函数可接受不同逻辑的子函数(如
show
函数可传入multi
或其他符合FUNC2
类型的函数),提高代码灵活性。
拓展:函数指针在嵌入式开发中的典型应用
1. 回调函数(事件驱动场景)
当外设(如传感器)触发中断时,通过函数指针调用用户自定义的处理函数:
// 定义中断处理函数指针类型
typedef void (*IRQ_Handler)(void); // 注册中断时传入回调函数
void register_irq(IRQ_Handler handler) { irq_handler = handler; // 保存函数指针
} // 中断发生时调用回调函数
void irq_triggered() { irq_handler(); // 调用用户自定义的处理逻辑
} // 用户代码:注册自定义处理函数
void user_irq_process() { // 处理传感器数据
}
register_irq(user_irq_process); // 注册回调
2. 状态机设计(简化逻辑跳转)
用函数指针数组表示不同状态的处理逻辑,状态切换时直接调用对应函数:
// 定义状态函数指针类型
typedef void (*StateFunc)(); // 定义各状态的处理函数
void state_init();
void state_run();
void state_stop(); // 状态机数组
StateFunc states[] = {state_init, state_run, state_stop}; // 切换状态时调用对应函数
int current_state = 0;
states[current_state](); // 调用初始化状态函数
3. 注意事项
- 指针有效性:确保指针指向合法的内存地址,避免野指针(如未初始化的指针)。
- 类型匹配:函数指针的参数列表和返回值必须与目标函数完全一致,否则会编译报错。
通过本例,不仅能掌握函数指针、指针操作等值传递与指针传递的核心区别,还能理解如何通过这些特性实现灵活的代码架构。这些知识在嵌入式开发的驱动编程、接口设计中至关重要,是理解底层硬件交互的基础。
通过这些题目,深入理解 C 语言的内存管理、作用域、运算符优先级等核心概念,为嵌入式开发打下坚实基础。希望本文能帮助新手系统掌握这些关键知识,在面试中脱颖而出。