由浅入深讲解 attribute 及相关内容

1. 需求解析

在 C/C++ 中,一般代码的执行顺序是从 main() 开始,但 GNU 编译器(如 GCC)允许我们使用 attribute(函数属性)来改变默认行为,使某些函数在 main() 之前或之后执行。


2. 什么是 attribute(属性)?

在 GCC(GNU 编译器)中,attribute 允许我们给函数、变量、类型添加特殊的属性,影响它们的行为。比如:

  • 指定一个函数在 main() 之前执行
  • 告诉编译器优化某个函数
  • 定义函数别名等

3. constructor 和 destructor

我们主要关注 constructor 和 destructor:

  • constructor:表示该函数会在 main() 之前执行。
  • destructor:表示该函数会在 main() 之后执行。
示例代码
#include <stdio.h>

// `constructor`:main 函数之前执行
void before() __attribute__((constructor));

// `destructor`:main 函数之后执行
void after() __attribute__((destructor));

void before() {
    printf("this is function %s\n", __func__);  // __func__ 代表当前函数名称
}

void after() {
    printf("this is function %s\n", __func__);
}

int main() {
    printf("this is function %s\n", __func__);
    return 0;
}

4. 代码执行流程

按执行顺序来看:

  1. before() 先执行(因为 constructor 使它在 main() 之前运行)。
  2. main() 执行。
  3. after() 最后执行(因为 destructor 使它在 main() 之后运行)。
输出结果
this is function before
this is function main
this is function after

5. 其他常见的 attribute 关键字

GNU attribute 还可以用于很多情况,下面挑一些常见的讲解。

(1) alias(设置函数别名)
#include <stdio.h>

void original_function() {
    printf("This is the original function\n");
}

// 定义别名 `alias_function`,它与 `original_function` 作用相同
void alias_function() __attribute__((alias("original_function")));

int main() {
    alias_function();  // 其实执行的是 original_function()
    return 0;
}

输出:

This is the original function

(2) aligned(对齐方式)
struct __attribute__((aligned(16))) AlignedStruct {
    int data;
};

上面的代码强制 AlignedStruct 以 16 字节对齐,在处理 SIMD(如 AVX 指令)时很有用。


(3) always_inline/gnu_inline(内联函数)
  • always_inline:强制编译器内联该函数,提高执行效率。
  • gnu_inline:告诉编译器这个函数应该尽量内联,但不是强制的。
inline void my_function() __attribute__((always_inline));
void my_function() {
    printf("This function is always inlined.\n");
}

(4) noreturn(无返回值)

适用于程序调用后不会返回的函数,比如 exit() 结束进程的情况:

#include <stdlib.h>

void fatal_error() __attribute__((noreturn));

void fatal_error() {
    printf("Fatal error occurred! Exiting...\n");
    exit(1);  // 退出进程
}

int main() {
    fatal_error();  // 这个函数不会返回
    return 0;  // 这行代码永远不会执行
}

6.关键字归纳与适用场景

关键字作用适用场景
constructor让函数在 main 之前 运行日志系统初始化、硬件设备初始化
destructor让函数在 main 之后 运行释放资源,如关闭数据库连接
alias给函数创建一个别名库开发者定义多个别名
aligned指定数据的对齐方式SIMD 计算、结构体优化
always_inline强制函数内联小型函数优化,提高性能
noreturn指定不会返回的函数程序终止函数(如 exit())

7. 结论

通过 GNU 的 attribute 关键字,我们可以控制函数的执行时机、优化方式、对齐方式等。

析构函数和虚函数相关知识点

解析析构函数与虚函数

1. 什么是析构函数?

析构函数是 C++ 中的一种特殊成员函数,它在对象销毁时自动调用,主要用于释放资源,例如关闭文件、释放内存等。

示例代码:

#include <iostream>
using namespace std;

class FileHandler {
public:
    FileHandler() {
        cout << "文件打开" << endl;
    }
    ~FileHandler() { // 析构函数
        cout << "文件关闭" << endl;
    }
};

int main() {
    FileHandler file; // 作用域结束,自动调用析构函数
    return 0;
}
// 输出:
// 文件打开
// 文件关闭

这里的 ~FileHandler() 就是析构函数,它在对象 file 销毁时被调用,用于关闭文件。


2. 为什么析构函数必须是虚函数?

当一个类有继承关系时,析构函数必须是虚函数,否则会导致资源泄漏。

问题:如果析构函数不是虚函数会发生什么?
假设我们有一个基类和子类,如果基类的析构函数不是虚函数,当我们用基类指针删除子类对象时,只会调用基类的析构函数,不会调用子类的析构函数,导致子类的资源没有被释放,从而发生内存泄漏。

错误示例(析构函数不是虚函数):

#include <iostream>
using namespace std;

class Base {
public:
    Base() { cout << "Base 构造函数" << endl; }
    ~Base() { cout << "Base 析构函数" << endl; } // 不是虚函数
};

class Derived : public Base {
public:
    Derived() { cout << "Derived 构造函数" << endl; }
    ~Derived() { cout << "Derived 析构函数" << endl; } 
};

int main() {
    Base* obj = new Derived(); // 用基类指针指向子类对象
    delete obj; // 只调用 Base 的析构函数
    return 0;
}
// 输出:
// Base 构造函数
// Derived 构造函数
// Base 析构函数 (⚠️ 没有调用 Derived 析构函数)

这里 Derived 的析构函数没有被调用,导致 Derived 中的资源没有释放,出现内存泄漏。

解决方案:让基类的析构函数变成虚函数

class Base {
public:
    Base() { cout << "Base 构造函数" << endl; }
    virtual ~Base() { cout << "Base 析构函数" << endl; } // 变成虚函数
};

现在删除 Base* obj = new Derived(); 时,会先调用 Derived 的析构函数,再调用 Base 的析构函数,避免内存泄漏。


为什么子类析构函数不会被调用?

这里的关键在于 C++ 的对象模型和析构函数的调用机制:对象的布局(对象模型)
在 C++ 中,类的对象内存是按照从基类到子类的顺序进行布局的。
Base 作为父类,会先被构造,Derived 作为子类,会后被构造。
但如果析构函数不是虚函数,编译器只会认为 Base* obj 是一个Base 类型的对象,不会管它实际上指向的是 Derived 对象。

为什么 virtual 可以让析构函数正确调用?

核心原因:虚函数表(vtable)机制

当 Base 的析构函数是虚函数时,Base 对象会包含一个虚表指针**(vptr),指向一个虚函数表(vtable)。**
在运行时,通过 Base* 指针调用析构函数时,会查找 vtable,确定当前对象的实际类型(即 Derived),然后调用 Derived 的析构函数。
这样就能确保先调用子类的析构函数,再调用基类的析构函数,防止资源泄漏。

3. 为什么 C++ 默认的析构函数不是虚函数?

虚函数需要额外的虚表(vtable)和虚表指针(vptr),占用额外的存储空间。如果一个类不会被继承,它的析构函数设为虚函数就是浪费内存。

总结:

  • 不会被继承的类 → 析构函数不需要虚函数(节省空间)
  • 可能被继承的类 → 析构函数必须是虚函数(防止内存泄漏)

4. 总结

关键点说明
析构函数作用释放资源,如关闭文件、释放内存
析构函数必须是虚函数的情况当类可能被继承时,确保子类对象被正确销毁
析构函数默认不是虚函数的原因避免虚表带来的额外开销,节省内存
正确做法如果类可能被继承,一定要将析构函数设为 virtual

更多推荐