【嵌入式八股7】基础语法
一、static关键字
在C语言中,static关键字的使用场景和作用如下:
1. 修饰函数体内的局部变量
当static用于修饰函数体内的局部变量时,该变量的访问权限仅限于该函数内部。不过,它与普通局部变量不同,仅会被初始化一次,并且存储在静态存储区。这意味着在函数的多次调用过程中,该静态局部变量会保留上一次调用结束时的值。示例代码如下:
#include <stdio.h>
void test_static_local() {
static int count = 0;
count++;
printf("Static local variable count: %d\n", count);
}
int main() {
test_static_local(); // 输出 1
test_static_local(); // 输出 2
return 0;
}
2. 修饰模块内、函数体外的全局变量
若static修饰模块内(.c文件)、函数体外的全局变量,会将该全局变量的作用域限制在当前模块内部,即该变量只能在当前.c文件中使用,不能被其他文件通过extern关键字跨文件共享。这样可以避免不同模块间全局变量的命名冲突。例如:
// file1.c
static int static_global = 10;
// file2.c
// 无法通过 extern int static_global; 来引用 static_global
3. 修饰模块内的函数
当static修饰模块内的函数时,该函数仅可被本模块调用,不能作为接口暴露给其他模块。这有助于隐藏模块内部的实现细节,提高代码的封装性和安全性。示例如下:
// file1.c
static void static_function() {
printf("This is a static function.\n");
}
void call_static_function() {
static_function();
}
// file2.c
// 无法直接调用 static_function()
注意:static与extern不可同时修饰一个变量。因为static限制变量或函数的作用域为当前模块,而extern用于声明外部可访问的变量或函数,二者语义冲突。
二、const关键字
const关键字用于声明常量,变量一旦被const修饰并初始化后,其值就无法再被修改。下面详细介绍const在指针中的应用:
1. 常量指针与指针常量
在指针的使用中,*(指针)和const(常量)的位置决定了指针和指针所指向的值是否可以被修改,遵循“谁在前先读谁”的原则,*象征着地址,const象征着内容,谁在前面谁就不允许改变。
- 指针常量:
int * const p;
// 这里的 const 修饰指针 p,意味着 p 是一个指向整型数的常指针,
// 指针指向不可以修改,但指针指向的整型数可以修改
int a = 10, b = 20;
p = &a; // 初始化指针指向
// p = &b; // 错误,指针指向被限定
*p = 20; // 指针指向的值可以修改
- 常量指针:
const int *p;
// const 修饰 *p,表明 p 是一个指向常整型数的指针,
// 指针指向可以修改,但指针指向的整型数不可修改
int a = 10, b = 20;
p = &a;
// *p = 20; // 错误,指针指向的值被限定
p = &b; // 指针指向可以修改
- 指向常整形数的常指针:
const int * const a;
// 这种情况下,指针指向和指针指向的值都不可修改
2. 在函数参数和返回值中的应用
const还常用于函数参数和返回值,以防止对传入参数或返回值的意外修改。
void printArray(const int *arr, int size) {
// 防止修改入参 arr 所指向的数组内容
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
const char* getString() {
// 防止修改返回值,返回值为指针的时候
static char str[] = "Hello, World!";
return str;
}
三、volatile关键字
1. 作用
volatile关键字的作用是告诉编译器,每次对该变量的访问都要从内存或对应外设寄存器中取值放入CPU通用寄存器后再进行操作,防止编译器对代码进行优化。
2. 详解
CPU在读取数据时,会从指定地址处取值并搬运到CPU通用寄存器中进行处理。在不加volatile关键字时,对于频繁的操作,编译器可能会对代码的汇编指令进行优化,从而导致一些预期之外的结果。例如:
// 比如要往某一地址送两指令:
int *ip = (int *)0x12345678; // 设备地址
*ip = 1; // 第一个指令
*ip = 2; // 第二个指令
// 编译器可能优化为:
int *ip = (int *)0x12345678; // 设备地址
*ip = 2; // 第二个指令
// 造成第一条指令被忽略
// 使用 volatile 关键字
volatile int *ip = (volatile int *)0x12345678; // 设备地址
*ip = 1; // 第一个指令
*ip = 2; // 第二个指令
3. 使用场合
volatile关键字通常用于以下场合:
- 寄存器:在访问硬件寄存器时,由于寄存器的值可能会被硬件随时修改,因此需要使用
volatile关键字确保每次访问都能获取到最新的值。 - 临界区访问的变量:在多线程或多任务环境中,临界区的变量可能会被其他线程或任务修改,使用
volatile可以保证数据的一致性。 - 中断函数访问的全局或static变量:中断函数可能会在任何时候打断主程序的执行,对全局或静态变量进行修改,使用
volatile可以确保主程序能够及时感知到这些变化。
4. 与Cache的区别
volatile是对编译器的约束,它可以控制每次从RAM读取数据到通用寄存器,但无法控制从RAM到通用寄存器的过程(从RAM到寄存器要经过cache)。若两次被volatile修饰的读取指令过快,即使RAM中的值改变了,但由于读取过快没有更新cache,那么实际上搬运到通用寄存器的值来自于cache,此类情况下需要禁用cache。- 编译器优化是针对于LDR命令的,从内存中读取数据到寄存器时不允许优化这一过程,而None-cache保护的是对内存数据的访问(
volatile无法控制LDR命令执行后是否刷新cache)。
四、#define 与 const区别
| #define | 编译的预处理阶段展开替换 | 低,只是简单的文本替换,没有类型检查 | 占用代码段空间(.text) |
无法调试,因为在预处理阶段就已经被替换掉了 |
| const | 编译、运行阶段 | 有数据类型检查,能在编译时发现类型不匹配的错误 | 占用数据段空间(.data常量区) |
可调式,可以在调试器中查看常量的值 |
五、防止头文件重复引用
在C语言中,为了防止头文件被重复引用,通常使用#ifndef、#define和#endif预处理指令。其原理是:当程序中第一次#include该文件时,由于_NAME_H尚未定义,所以会定义_NAME_H并执行“头文件内容”部分的代码;当发生多次#include时,因为前面已经定义了_NAME_H,所以不会再重复执行“头文件内容”部分的代码。示例如下:
#ifndef _NAME_H
#define _NAME_H
// 头文件内容
#endif
六、函数调用与栈、寄存器
在函数调用过程中,参数的传递和返回值的处理与栈和寄存器密切相关。
1. 参数传递
当调用函数时,入栈顺序为参数从右往左,这样在取参数时就可以从左往右依次获取。例如:
void fun(int a, int b);
fun(1, 2); // 调用函数时,入栈顺序为参数从右往左
// 栈中数据布局
| 1 |
| 2 |
——
一般来说,右边的参数先入栈,前4个参数存放在R0 - R3寄存器中,多余4个的参数存放在任务栈中。
2. 返回值
函数的返回值通常存放在R0寄存器中。
七、全局变量和局部变量区别
- 存储位置:全局变量存储在静态存储区,而局部变量存储在栈中。
- 生命周期:全局变量的生命周期是整个程序的运行期间,而局部变量的生命周期仅限于定义它的代码块或函数执行期间。
- 作用域:全局变量的作用域是整个程序,而局部变量的作用域仅限于定义它的代码块或函数内部。
八、堆栈溢出原因
- 动态内存分配后未正确回收:在使用动态内存分配函数(如
malloc、calloc、realloc)分配内存后,如果没有使用相应的free函数释放内存,会导致内存泄漏,最终可能导致堆栈溢出。 - 函数递归调用深度太深:递归函数在调用自身时会不断在栈上分配新的栈帧,如果递归调用深度太深,栈空间会被耗尽,从而导致栈溢出。
九、局部变量与全局变量重名
当局部变量与全局变量重名时,局部变量存储在栈中,全局变量存储在静态存储区。局部变量的作用域在定义它的{}内,遵循就近原则,即在该{}内,对该变量的访问将优先使用局部变量。示例如下:
#include <stdio.h>
int global_var = 10;
void test() {
int global_var = 20;
printf("Local variable: %d\n", global_var); // 输出 20
printf("Global variable: %d\n", ::global_var); // 在C++中可以使用 :: 访问全局变量
}
int main() {
test();
return 0;
}
十、访问内存中某地址数据
1. 读取内存数据
// 方法1
int result = *(int *)0x123456;
// 方法2
int *ptr = (int *)0x123456;
int result = *ptr;
2. 修改内存数据
// 方法1
*(int * const)(0x56a3) = 0x3344;
// 方法2
int * const ptr = (int *)0x56a3;
*ptr = 0x3344;
十一、枚举类型
枚举类型是一种用户自定义的数据类型,用于定义一组命名的常量。示例如下:
#include <stdio.h>
enum DAY {
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
enum COLOR {
black, // 默认为0
white, // 默认+1
red
};
enum COLOR2 {
black2 = 1, // 手动指定起始值
white2,
red2
};
enum COLOR3 {
black3, // 0
white3 = 3,
red3 // 4
};
int main() {
enum DAY day;
day = WED;
printf("%d\n", day); // 输出 3
enum COLOR color = white;
printf("%d\n", color); // 输出 1
enum COLOR2 color2 = white2;
printf("%d\n", color2); // 输出 2
enum COLOR3 color3 = red3;
printf("%d\n", color3); // 输出 4
return 0;
}
十二、float精度
- 精度:
float类型的精度是保证至少7位有效数字是准确的。 - 取值范围:
float的取值范围是[-3.4028235E38, 3.4028235E38],精确范围是[-340282346638528859811704183484516925440, 340282346638528859811704183484516925440]。
十三、结构体字节对齐
1. 字节对齐的作用
字节对齐规定数据在内存中的存储起始地址必须是某个特定字节数(通常是数据类型的大小)的整数倍,主要有以下两个方面的原因:
- 读取效率问题:以32位机为例,它每次取32个位,也就是4个字节。以
int型数据为例,如果它在内存中存放的位置按4字节对齐,即1个int的数据全部落在计算机一次取数的区间内,那么只需要取一次就可以了。如果访问未对齐的内存,处理器需要作两次内存访问,效率会降低。 - 存储空间占用:结构体中成员变量的排列顺序不同时,占用的内存空间也不同。
2. 实际使用
可以使用#pragma pack指令来指定编译器的字节对齐方式。示例如下:
#include <stdio.h>
#pragma pack (1) // 1字节对齐
typedef struct TestNoAlign {
unsigned char u8_test1; // 1
unsigned int u32_test2; // 4
double d8_test3; // 8
} TestNoAlign;
#pragma pack () // 取消
typedef struct TestAlign {
unsigned char u8_test1; // 1+3
unsigned int u32_test2; // 4
double d8_test3; // 8
} TestAlign;
int main(void) {
printf("sizeof(TestNoAlign) is %d sizeof(TestAlign) is %d \n",
sizeof(TestNoAlign), sizeof(TestAlign));
return 0;
}
上述代码中,TestNoAlign结构体采用1字节对齐,总大小为13字节;TestAlign结构体采用默认的字节对齐方式,总大小为16字节。
十四、联合体
联合体(union)是一种特殊的数据类型,它允许在同一地址空间中存储不同类型的数据,但同一时间只能使用其中一个成员。
1. 判断字节序
#include <stdio.h>
typedef union test_u {
int a;
char b;
} test;
int main() {
test t;
t.a = 0x12345678;
if (t.b == 0x78) {
printf("小端\n"); // 低地址0x00000000 放低字节0x78
} else {
printf("大端\n"); // 低地址0x00000000 放高字节0x12
}
return 0;
}
2. 分离高低字节
#include <stdio.h>
union div {
int n; // n中存放要进行分离高低字节的数据
char a[4]; // 一个整形占四个字节,char占一个字节,a[4]将n分为了四部分
};
int main() {
union div test;
test.n = 0x12345678; // 寄存器赋值
unsigned char TH1 = test.a[0]; // test.a[0]中存储的是低位数据 0x78
unsigned char TL1 = test.a[3]; // test.a[3]中储存了test.n的高位数据 0x12
printf("TH1 = 0x%x, TL1 = 0x%x\n", TH1, TL1);
return 0;
}
3. 寄存器定义与位域
#include <stdio.h>
#include <stdint.h>
union test {
uint32_t reg;
struct {
uint32_t reserve:4; // 占用低字节的4bit
uint32_t ctrl:4;
uint32_t enable:5;
uint32_t dis:3;
uint32_t stat:1;
uint32_t loop:7;
uint32_t ext:2;
uint32_t mode:6; // 位域和为32
} bits;
};
int main(void) {
union test mytest;
mytest.reg = 0xa5a5a5a5;
printf("reg value=0x%x\n", mytest.reg);
printf("reserve(3:0)=0x%x\n", mytest.bits.reserve);
printf("ctrl(7:4)=0x%x\n", mytest.bits.ctrl);
printf("enable(12:8)=0x%x\n", mytest.bits.enable);
printf("dis(15:13)=0x%x\n", mytest.bits.dis);
printf("stat(16:16)=0x%x\n", mytest.bits.stat);
printf("loop(23:17)=0x%x\n", mytest.bits.loop);
printf("ext(25:24)=0x%x\n", mytest.bits.ext);
printf("mode(31:26)=0x%x\n", mytest.bits.mode);
return 0;
}
#牛客激励计划#一些八股模拟拷打Point,万一有点用呢

查看14道真题和解析