GCC __attribute__ 完全指南:从入门到实战


一、什么是 __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;
}

实际执行过程

  1. 把参数 3、4 压栈

  2. 跳转到 add 函数的地址

  3. 执行 add 里的代码

  4. 返回结果

  5. 跳转回 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))

一句话总结

内联 = 把函数代码直接"抄"到调用的地方,用代码体积换执行速度。适合小而频繁的函数,不适合复杂或体积大的函数。

如果你是在嵌入式开发中想优化性能,内联是最简单有效的手段之一。有具体场景我可以帮你判断是否该内联。

相关推荐
zz34572981131 小时前
C语言中字符串常量存储位置
c语言·开发语言·算法·青少年编程
noipp1 小时前
推荐题目:洛谷 P16510 [GKS 2015 #C] gRanks
java·c语言·开发语言·c++·python·算法
程序喵大人1 小时前
从内存/汇编角度看C与C++:指针、引用、对象的底层差异
c语言·汇编·c++·指针·引用·对象
Swift社区2 小时前
OpenHarmony鸿蒙PC平台移植 gifsicle:CC++ 三方库适配实践(Lycium tpc_c_cplusplus)
c语言·c++·harmonyos
東隅已逝,桑榆非晚2 小时前
编译和链接
c语言·笔记
FFZero13 小时前
[mpv插件系统] (一) Lua 闭包与上值 — 从概念到 C API
c语言·junit·lua
秋越3 小时前
从工程角度理解嵌入式C语言关键字
c语言·开发语言·嵌入式·嵌入式软件开发·嵌入式c语言·c语言关键字
代码地平线4 小时前
C++ 入门篇类和对象·上篇:从本质深剖类与对象与C++基本用法
c语言·开发语言·数据结构·c++·笔记·算法
m0_377108144 小时前
stm32-mpu6050
stm32·单片机·嵌入式硬件