面经学习——C/C++:attribute和析构函数与虚函数
由浅入深讲解 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. 代码执行流程
按执行顺序来看:
before()先执行(因为constructor使它在main()之前运行)。main()执行。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 |
更多推荐


所有评论(0)