一、什么是 __attribut
__attribute__ 是 GCC 编译器的一个特殊关键字 ,用来告诉编译器关于变量、函数、类型的额外信息。
简单理解:你在代码里给编译器"递小纸条",告诉它"这件事按我说的做"。
c
变量/函数/类型 __attribute__((属性1, 属性2))
二、为什么需要它?
| 场景 | 不用 __attribute__ |
用了 __attribute__ |
|---|---|---|
| 结构体大小 | 编译器自动填充,8字节 | 强制紧凑,5字节 |
| 变量地址 | 编译器随便放 | 强制按16字节对齐 |
| 函数执行时机 | 必须在main里调用 | main之前自动执行 |
| 代码存放位置 | 默认.data段 | 放到指定段(如RAM) |
三、最常用的 6 个属性
1. packed ------ 去掉结构体的"空位"
// 不用 packed
struct Normal {
char c; // 1 字节
int i; // 4 字节 → 实际占 8 字节(编译器自动填充3个空位)
};
// 用 packed
struct __attribute__((packed)) Packed {
char c; // 1 字节
int i; // 4 字节 → 紧挨着,共 5 字节
};
printf("%d\n", sizeof(struct Normal)); // 8
printf("%d\n", sizeof(struct Packed)); // 5
典型场景:网络协议包、硬件寄存器映射、文件格式解析
2. aligned ------ 强制地址对齐
// 强制 buffer 的地址是 16 的倍数
char buffer[1024] __attribute__((aligned(16)));
// 强制结构体按 32 字节对齐
struct __attribute__((aligned(32))) Data {
int a;
int b;
};
典型场景:DMA 缓冲区、CPU 缓存行对齐、SIMD 指令
3. section ------ 放到指定位置
// 把变量放到自定义段
int my_var __attribute__((section(".my_section"))) = 0x1234;
// 把函数放到高速 RAM
void __attribute__((section(".ram_code"))) fast_func(void) {
// 频繁调用的代码
}
典型场景:嵌入式(放到后备RAM、ITCM/DTCM)、启动代码、Bootloader
4. constructor / destructor ------ 自动执行
// main 之前自动执行
void __attribute__((constructor)) init(void) {
printf("1. 我先执行\n");
}
// main 之后自动执行
void __attribute__((destructor)) cleanup(void) {
printf("3. 我最后执行\n");
}
int main() {
printf("2. main 函数\n");
return 0;
}
// 输出顺序:1 → 2 → 3
典型场景:库初始化/清理、驱动注册、全局对象模拟
5. weak ------ 允许被覆盖
// 库文件:提供一个弱定义的默认实现
void __attribute__((weak)) default_handler(void) {
while(1); // 默认死循环
}
// 用户文件:可以覆盖,不会报重复定义
void default_handler(void) {
printf("用户自己的处理\n");
}
典型场景:中断默认处理、钩子函数、弱符号链接
6. unused / used ------ 控制警告
// 告诉编译器:这个函数暂时没用,别警告我
void __attribute__((unused)) debug_func(void) {
// 调试代码
}
// 告诉编译器:这个函数别优化掉
void __attribute__((used)) boot_init(void) {
// 启动初始化
}
四、嵌入式实战:STM32 启动代码
#include <stdint.h>
// 栈顶地址(链接脚本定义)
extern uint32_t _estack;
// 复位函数
void reset_handler(void) {
// 系统初始化
main();
while(1);
}
// 弱定义:用户可覆盖
void __attribute__((weak)) default_handler(void) {
while(1);
}
// 中断向量表:必须放在 .isr_vector 段,256字节对齐
__attribute__((section(".isr_vector"), aligned(256), used))
const struct {
uint32_t stack_ptr;
uint32_t reset;
uint32_t nmi;
uint32_t hardfault;
// ... 更多中断
} vector_table = {
.stack_ptr = (uint32_t)&_estack,
.reset = (uint32_t)reset_handler,
.nmi = (uint32_t)default_handler,
.hardfault = (uint32_t)default_handler,
};
五、常用属性速查表
| 属性 | 作用 | 一句话总结 |
|---|---|---|
packed |
紧凑排列 | 去掉结构体空位 |
aligned(n) |
n字节对齐 | 地址必须是n的倍数 |
section("name") |
放到指定段 | 代码/数据放哪我说了算 |
constructor |
main前执行 | 自动初始化 |
destructor |
main后执行 | 自动清理 |
weak |
弱符号 | 允许别人覆盖我 |
used |
强制保留 | 别优化掉我 |
unused |
抑制警告 | 暂时没用别骂我 |
noinline |
禁止内联 | 保持独立函数 |
always_inline |
强制内联 | 必须内联展开 |
六、注意事项
1. 可移植性问题
__attribute__ 是 GCC/Clang 专用,MSVC(Windows)不支持。
| 编译器 | 替代方案 |
|---|---|
| MSVC | __declspec()、#pragma pack |
| 其他 | 条件编译 #ifdef __GNUC__ |
2. packed 的代价
-
访问 packed 结构体成员更慢(需要非对齐访问)
-
某些架构(如 ARM)访问 packed 成员可能触发硬 fault
3. 不要过度使用
只在确实需要底层控制时才用,普通应用代码不需要。
七、快速上手代码
#include <stdio.h>
// 1. packed 演示
struct __attribute__((packed)) PackedData {
char c;
int i;
};
// 2. constructor 演示
void __attribute__((constructor)) auto_init(void) {
printf("[自动] 初始化开始\n");
}
// 3. aligned 演示
char buffer[64] __attribute__((aligned(32)));
int main() {
printf("[main] 结构体大小: %d 字节\n", (int)sizeof(struct PackedData));
printf("[main] buffer 地址: 0x%lx\n", (unsigned long)buffer);
// 检查地址是否32对齐
if ((unsigned long)buffer % 32 == 0) {
printf("[main] buffer 已按32字节对齐 ✓\n");
}
return 0;
}
编译运行:
bash
gcc -o test test.c
./test
输出:
[自动] 初始化开始
[main] 结构体大小: 5 字节
[main] buffer 地址: 0x7ffd12345640
[main] buffer 已按32字节对齐 ✓
八、一句话总结
__attribute__是 GCC 给开发者的"底层控制权"------让你告诉编译器如何特殊处理某个变量、函数或类型。嵌入式、系统编程必备,普通应用代码用不到
内联(inline)详解
内联 是编译器的一种优化技术,核心思想是:把函数调用处直接替换成函数体代码,而不是真的去"调用"函数。
一、先用例子理解
普通函数调用
int add(int a, int b) {
return a + b;
}
int main() {
int x = add(3, 4); // 调用函数
return 0;
}
实际执行过程:
-
把参数 3、4 压栈
-
跳转到
add函数的地址 -
执行
add里的代码 -
返回结果
-
跳转回
main继续执行
开销:跳转、压栈、弹栈(几十个时钟周期)
内联后的效果
// 相当于编译器把代码变成了这样
int main() {
int x = 3 + 4; // 直接替换,没有函数调用
return 0;
}
开销:只有一个加法指令(几个时钟周期)
二、对比图
| 普通函数 | 内联函数 | |
|---|---|---|
| 执行过程 | 跳转 → 执行 → 返回 | 直接展开代码 |
| 代码体积 | 函数只有一份 | 每次调用都复制一份 |
| 执行速度 | 慢(有跳转开销) | 快(无跳转开销) |
| 调试 | 方便(可以单步进入) | 不方便(没有调用栈) |
三、C/C++ 中的 inline 关键字
写法
// 告诉编译器:建议把这个函数内联
inline int add(int a, int b) {
return a + b;
}
重要说明
inline 只是建议 ,编译器可以拒绝:
-
函数太复杂(有循环、递归)
-
函数体积太大
-
优化级别设置
inline int complex_func() {
for(int i = 0; i < 100; i++) { // 有循环
// 复杂操作
}
return 0;
}
// 编译器很可能拒绝内联这个函数
四、编译器提供的"强制内联"
GCC 强制内联
__attribute__((always_inline)) inline int add(int a, int b) {
return a + b;
}
MSVC 强制内联
__forceinline int add(int a, int b) {
return a + b;
}
五、什么时候该用内联?
✅ 适合内联的场景
| 场景 | 例子 |
|---|---|
| 简单的小函数 | int max(int a, int b) { return a > b ? a : b; } |
| 频繁调用的函数 | 循环里调用几百万次 |
| getter/setter | int getX() { return x; } |
❌ 不适合内联的场景
| 场景 | 原因 |
|---|---|
| 复杂函数 | 内联后代码爆炸 |
| 递归函数 | 无法内联 |
| 大函数 | 代码体积暴增 |
| 虚函数(C++) | 多态调用无法内联 |
六、内联的代价:代码膨胀
例子:100 次调用
inline int add(int a, int b) { return a + b; }
for(int i = 0; i < 100; i++) {
result += add(i, i+1);
}
不内联 :add 函数只存在 1 份,调用 100 次
内联后 :add 的代码被复制 100 份,体积变大 100 倍
这就是用空间换时间
七、头文件中的 inline
为什么要在头文件里写 inline 函数?
因为编译器需要看到函数体才能内联,所以 inline 函数通常定义在 .h 文件里:
// utils.h
#ifndef UTILS_H
#define UTILS_H
static inline int max(int a, int b) {
return a > b ? a : b;
}
#endif
注意 :static inline 是常见写法,防止多个 .c 文件包含时产生重复定义。
八、inline vs 宏
inline 函数 |
宏 #define |
|
|---|---|---|
| 类型安全 | ✅ 是 | ❌ 否 |
| 参数求值次数 | 一次 | 可能多次 |
| 调试支持 | ✅ 可以 | ❌ 不行 |
| 代码复杂度 | 可写复杂逻辑 | 很难写复杂逻辑 |
// 宏的坑
#define SQUARE(x) (x * x)
int a = SQUARE(1+2); // 结果是 1+2*1+2 = 5,不是 9!
// inline 函数安全
inline int square(int x) { return x * x; }
int a = square(1+2); // 结果是 9 ✓
九、真实世界:嵌入式开发中怎么用
场景:读写寄存器
// 内联读写寄存器(STM32 就是这么干的)
static inline void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) {
GPIOx->BSRR = GPIO_Pin;
}
static inline uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx) {
return GPIOx->IDR;
}
// 调用时直接展开成寄存器操作,零开销
GPIO_SetBits(GPIOA, GPIO_Pin_0);
场景:简单的数学运算
static inline int clamp(int value, int min, int max) {
if (value < min) return min;
if (value > max) return max;
return value;
}
十、总结
| 问题 | 答案 |
|---|---|
| 什么是内联? | 把函数调用替换成函数体代码 |
| 优点 | 快(无调用开销) |
| 缺点 | 代码体积变大 |
| 何时用 | 小函数、频繁调用 |
| 何时不用 | 大函数、递归、虚函数 |
| 关键字 | inline(建议) |
| 强制内联 | __attribute__((always_inline)) |
一句话总结
内联 = 把函数代码直接"抄"到调用的地方,用代码体积换执行速度。适合小而频繁的函数,不适合复杂或体积大的函数。
如果你是在嵌入式开发中想优化性能,内联是最简单有效的手段之一。有具体场景我可以帮你判断是否该内联。