找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

909

积分

0

好友

115

主题
发表于 17 小时前 | 查看: 0| 回复: 0

#error的作用是什么?

#error 是一条预处理指令,它的作用是让预处理器发出一条错误信息,并立即中断编译过程。这在代码条件编译中非常有用,可以用于检查不满足的配置或条件。

下面是一个从实际代码中简化而来的例子:

#include <stdio.h>
#define RX_BUF_IDX 100
#if RX_BUF_IDX == 0
static const unsigned int rtl8139_rx_config = 0;
#elif RX_BUF_IDX == 1
static const unsigned int rtl8139_rx_config = 1;
#elif RX_BUF_IDX == 2
static const unsigned int rtl8139_rx_config = 2;
#elif RX_BUF_IDX == 3
static const unsigned int rtl8139_rx_config = 3;
#else
#error "Invalid configuration for 8139_RXBUF_IDX"
#endif
int main(void)
{
    printf("hello world\n");
    return 0;
}

C语言代码示例展示#error预处理指令的用法

这段代码的逻辑很简单:它检查宏 RX_BUF_IDX 的值。只有当这个值在0到3之间时,才会定义对应的 rtl8139_rx_config 变量。如果 RX_BUF_IDX 被定义为其他值(比如这里的100),预处理器就会执行 #error 指令,输出错误信息 "Invalid configuration for 8139_RXBUF_IDX" 并停止编译。

我们编译一下看看实际效果:

gcc编译错误,显示#error指令触发

可以看到,编译过程确实因为 #error 指令而失败了。这对于快速定位宏定义或配置错误非常高效。

位操作的基本使用

问题:如何用一个宏来给一个32位数据的特定位(bit)置1?

答案

#define SET_BIT(x, bit) (x |= (1 << bit)) /* 置位第bit位 */

这个宏利用了位或(|=)和左移(<<)操作符。(1 << bit) 会生成一个只有第 bit 位是1,其他位都是0的掩码。然后通过 |= 操作将其与目标变量 x 合并,从而实现将 x 的第 bit 位设置为1的功能。

来看一个具体的代码示例和运行结果:

#include <stdio.h>
#define SET_BIT(x, bit) (x |= (1 << bit)) /* 置位第bit位 */
int main(void)
{
    unsigned int a = 0x68; /* 二进制:01101000b */
    printf("0x68的第2位置1后的值变为:%#x\n", SET_BIT(a, 2));
    return 0;
}

C语言代码示例:使用宏进行位设置操作

编译并运行这个程序:

运行结果:显示0x68第2位置1后变为0x6c

0x68 的二进制是 0110 1000,第2位(从0开始计数)是0,将其置1后变成 0110 1100,即十六进制的 0x6c,与输出结果一致。

隐式转换规则

问题:下面这段代码的输出结果是什么?为什么?

#include<stdio.h>
int main(void)
{
    unsigned int a = 6;
    int b = -20;

    if (a + b > 6)
        printf("a+b大于6\n");
    else
        printf("a+b小于6\n");

    return 0;
}

输出结果

a+b大于6

原因分析
这个结果初看可能有些反直觉,-20 + 6 怎么会大于6呢?关键在于C语言的隐式类型转换,有时也称为“寻常算术转换”。

unsigned intint 进行运算时,编译器会将有符号数 b 转换为无符号数。也就是说,表达式 a + b 实际上等价于 a + (unsigned int)b

在32位环境下,-20 转换为无符号数后的值是 0xFFFFFFFF - 20 + 1 = 4294967276。所以 a + b 就变成了 6 + 4294967276,其结果远远大于6。

C语言遵循一套明确的类型转换规则,当操作数类型不同时,排名靠后的类型会自动(隐式)转换为排名靠前的类型。常见的排名顺序(从高到低)可以简单理解为:

double > float > unsigned long > long > unsigned int > int

掌握这些隐式转换规则对于避免整数溢出、理解复杂表达式的结果至关重要。

typedef与#define的区别

typedef#define 都可以用来为类型起别名,但它们有本质区别:

  1. 语法#define 之后不带分号,它是预处理指令;typedef 之后分号,它是C语言语句。
  2. 类型扩展#define 定义的别名可以用其他类型说明符进行扩展,而 typedef 定义的则不行。

    #define INT1 int
    unsigned INT1 n;  // 没问题,等同于 unsigned int n;
    
    typedef int INT2;
    unsigned INT2 n;  // 有问题!INT2已经被typedef定义为int的别名,不能再用unsigned修饰。
  3. 连续定义变量时的行为:这是最重要的区别之一。typedef 能保证定义的所有变量类型一致,而 #define 可能因为单纯的文本替换导致意外。

    #define P_INT1 int*;
    P_INT1 p1, p2;  // 展开为 int* p1, p2; 即 p1是指针,p2是普通int变量。
    
    typedef int* P_INT2;
    P_INT2 p1, p2;  // p1、p2 类型相同,都是指向int的指针。

    #define 的例子中,p2 只是一个 int 变量,这可能不是开发者想要的结果。而 typedef 则清晰地定义了一个“指针类型”,用它声明的所有变量都是该类型的指针

写一个MAX宏

最基础的 MAX 宏可以这样写:

#define MAX(x, y) ((x) > (y) ? (x) : (y))

用括号把参数 xy 分别括起来,是为了防止当参数是复杂表达式时,因运算符优先级问题而产生错误。例如,如果没有括号,MAX(a & 0xFF, b & 0xFF) 可能会被错误地展开。

这个基础版本在日常使用中基本够用。但要写出更严谨、能处理各种边界情况(如参数带副作用、不同类型比较等)的 MAX 宏,则需要更高级的技巧,这常常是考察开发者对C语言理解深度的好题目。

死循环

在嵌入式系统或需要无限循环的场景中,常用的C语言死循环写法有以下几种:

  1. while 循环

    while(1) {
        // 循环体
    }

    这是最直观、最常用的写法。

  2. for 循环

    for(;;) {
        // 循环体
    }

    for 语句的三个表达式都留空,同样构成一个无限循环。

  3. goto 循环

    Loop:
        // 循环体
        ...
    goto Loop;

    使用 goto 和标签,这种方式在现代编程中不常用,但在某些底层或特定场景下可能会见到。

static的作用

在C语言中,关键字 static 主要有三个作用,取决于它所修饰的对象:

  1. 在函数体内修饰局部变量
    将一个局部变量声明为 static,会使该变量在程序的整个生命周期内都存在(存储在静态存储区),而不是在函数调用结束时被销毁。这意味着该变量在多次函数调用之间能保持其值不变,实现了局部变量的“记忆”功能。

  2. 在模块内(函数体外)修饰全局变量
    将一个全局变量声明为 static,会限制该变量的链接属性。它依然在程序的整个生命周期内存在,但它的作用域被限制在声明它的源文件(模块)内,其他源文件无法通过 extern 引用它。这可以看作是一个“文件内部的全局变量”,有助于信息隐藏和减少命名冲突。

  3. 在模块内修饰函数
    将函数声明为 static,同样限制了函数的链接属性。该函数只能在声明它的源文件内被调用,对其他源文件不可见。这常用于实现模块内部的辅助函数,避免污染全局命名空间。

const的作用

const 关键字用于定义常量,即其值在初始化后不能被修改。理解 const 与指针结合时的声明是关键。请看下面几个声明:

const int a;
int const a;
const int *a;
int * const a;
int const * a const;

让我们逐一解读:

  • const int a;int const a;:这两个声明是等价的,都表示 a 是一个常整型数,其值不可变。
  • *`const int a;**:这表示a是一个**指向常整型数的指针**。也就是说,指针a所指向的那个整型数据是常量,不能被修改(*a = 10;是非法的),但指针a本身可以指向别的地址(a = &b;` 是合法的)。
  • *`int const a;**:这表示a是一个**指向整型数的常指针**。指针a本身是常量,初始化后不能再指向其他地址(a = &b;是非法的),但它所指向的整型数据可以被修改(*a = 10;` 是合法的)。
  • *`int const a const;**:这是最严格的声明。它表示a是一个**指向常整型数的常指针**。既不能通过a修改它所指向的数据(*a = 10;非法),也不能修改a本身使其指向其他地址(a = &b;` 非法)。

记忆口诀:const* 左边,修饰的是指向的数据;const* 右边,修饰的是指针本身。

volatile的作用

volatile 是嵌入式系统编程中一个至关重要的关键字。它告诉编译器,这个变量的值可能会被程序之外的、不可预知的因素改变(例如硬件寄存器、中断服务程序、多线程环境中的其他线程)。因此,编译器在对这个变量进行优化时必须“小心谨慎”:

  • 每次使用该变量时,都必须直接从其内存地址中重新读取,而不能使用之前可能存储在寄存器里的副本。
  • 对该变量的任何写入操作,都必须立即写回内存

哪些变量应该被声明为 volatile 呢?

  1. 并行设备的硬件寄存器(如状态寄存器、数据寄存器)。
  2. 一个中断服务子程序(ISR)中会访问到的非自动变量(即全局变量或 static 局部变量)。
  3. 多线程应用中被多个任务共享的变量

不理解 volatile 可能导致灾难性的后果,比如读取不到硬件的最新状态,或者多线程数据不一致。因此,它常被用作区分普通C程序员和嵌入式系统程序员的标志性问题。

下面是一些深入的问题:

  1. 一个参数既可以是 const 还可以是 volatile 吗?
    是的。一个典型的例子是只读的状态寄存器。它是 volatile 的,因为它的值可能被硬件随时改变;它也是 const 的,因为程序不应该、也不能去写这个寄存器。

  2. 一个指针可以是 volatile 吗?
    是的。虽然不常见,但确实存在。例如,一个指向某缓冲区的指针,这个指针变量本身的地址可能被一个中断服务程序修改。

  3. 下面的函数有什么错误?

    int square(volatile int *ptr)
    {
        return *ptr * *ptr;
    }

    答案:这个函数意图返回 *ptr 指向值的平方。但由于 ptr 被声明为指向 volatile int 的指针,编译器会认为 *ptr 的值可能在两次取值之间被改变。因此,它可能会生成类似下面的、读取两次的代码:

    int square(volatile int *ptr)
    {
        int a, b;
        a = *ptr; // 第一次读取
        b = *ptr; // 第二次读取
        return a * b;
    }

    如果 *ptr 在两次读取之间真的发生了变化(比如被中断修改),那么 ab 的值就会不同,函数返回的也就不是期望的平方值,而是两个不同值的乘积。
    正确写法是先将易变的值存入一个局部变量,再用这个变量进行计算:

    int square(volatile int *ptr)
    {
        int a;
        a = *ptr; // 读取一次
        return a * a; // 使用局部变量计算
    }

变量定义

用变量 a 给出下面的定义,这是对C语言声明语法基本功的考察:

a) 一个整型数
b) 一个指向整型数的指针
c) 一个指向指针的的指针,它指向的指针是指向一个整型数
d) 一个有10个整型数的数组
e) 一个有10个指针的数组,该指针是指向一个整型数的
f) 一个指向有10个整型数数组的指针
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数
h) 一个有10个函数指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数

答案

a) int a;
b) int *a;
c) int **a;
d) int a[10];
e) int *a[10];
f) int (*a)[10];
g) int (*a)(int);
h) int (*a[10])(int);

其中,f)g)h) 涉及到数组指针和函数指针,是语法中的难点,需要仔细理解括号的作用。掌握这些定义是理解复杂声明和进行高级程序开发的基础。

中断函数

中断是嵌入式系统的核心组成部分。许多编译器提供了扩展关键字(如 __interrupt)来支持用标准C编写中断服务子程序(ISR)。评论一下下面这段 __interrupt 函数的代码:

__interrupt double compute_area(double radius)
{
    double area = PI * radius * radius;
    printf(" Area = %f", area);
    return area;
}

这段代码存在多个问题:

  1. ISR 不能有返回值。中断处理函数通常是由硬件或操作系统异步调用的,没有合适的调用者来接收其返回值。因此,ISR 应声明为 void 类型。
  2. ISR 不能有参数。中断的发生是异步事件,没有标准的机制向ISR传递参数。获取信息通常需要通过查询硬件状态或访问全局变量。
  3. 在ISR中进行浮点运算通常是不可取的。首先,浮点运算在许多处理器上不是“可重入”的,且可能涉及额外的状态保存(如浮点寄存器入栈),这会增加中断响应时间。其次,ISR的设计原则是短小精悍、快速执行,耗时的浮点运算与此原则相悖。
  4. 在ISR中调用 printf() 等标准I/O函数是危险的。这些函数本身通常不可重入,且执行效率低下,可能导致性能问题甚至程序崩溃。在ISR中应避免任何可能引起阻塞或执行时间不确定的操作。

一个合格的嵌入式开发者必须对中断的特性有深刻理解。如果你想深入探讨嵌入式开发中的其他技术难点或面试问题,欢迎到云栈社区交流讨论。




上一篇:解锁Mac Dock隐藏功能,高效工作流与个性化设置全攻略
下一篇:微分与积分电路原理详解:从RC电路到运放实现与PID控制关联
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-2 22:07 , Processed in 0.369044 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表