【嵌入式八股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()

注意staticextern不可同时修饰一个变量。因为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寄存器中。

七、全局变量和局部变量区别

  • 存储位置:全局变量存储在静态存储区,而局部变量存储在栈中。
  • 生命周期:全局变量的生命周期是整个程序的运行期间,而局部变量的生命周期仅限于定义它的代码块或函数执行期间。
  • 作用域:全局变量的作用域是整个程序,而局部变量的作用域仅限于定义它的代码块或函数内部。

八、堆栈溢出原因

  • 动态内存分配后未正确回收:在使用动态内存分配函数(如malloccallocrealloc)分配内存后,如果没有使用相应的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,万一有点用呢

全部评论
volatile用法mark
点赞 回复 分享
发布于 2025-03-06 21:43 山东
static全局变量作用域
点赞 回复 分享
发布于 2025-03-06 20:00 山东
static全局变量作用域
点赞 回复 分享
发布于 2025-03-03 11:24 陕西

相关推荐

03-23 22:04
江南大学 Java
程序员小白条:28届原因,这才研一,而且项目比较经典,东西也写的很简单,自我评价没啥用,应该写的是技术栈
点赞 评论 收藏
分享
评论
1
4
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务