设为首页 收藏本站
查看: 88|回复: 0

[经验分享] 震惊!C++程序真的从main开始吗?99%的程序员都答错了

[复制链接]
累计签到:50 天
连续签到:1 天
发表于 2025-4-11 14:03:51 | 显示全部楼层 |阅读模式
今天咱们来聊一个看似简单,但实际上99%的C++程序员都答错的问题:C++ 程序真的是从 main 函数开始执行的吗?
如果你毫不犹豫地回答"是",那恭喜你,你和大多数人一样——掉进了C++的第一个陷阱!别担心,等你看完这篇文章,你就能成为那个与众不同的1%了。
一、揭开C++启动的神秘面纱
还记得你写的第一个C++程序吗?可能是这样的:
[C++] 纯文本查看 复制代码
#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

[align=left]
老师告诉你:"程序从 main 函数开始执行"。然后你就这么相信了,一路写代码写到现在。但事实真的如此吗?
剧透一下:并不是!
如果你仔细思考,一定会冒出许多疑问:
·谁负责调用 main 函数?
· main 执行前,系统到底做了什么?
·为什么 main 前面的全局变量已经初始化好了?
·main 函数返回后又发生了什么?
今天,我们就来一起掀开这神秘的黑箱,看看C++程序启动的真相!
二、C++程序启动的真实过程
想象一下,一个C++程序的生命周期就像一次电影拍摄:
1、前期准备:搭建场景,准备道具(操作系统加载程序)
2、彩排:演员就位,准备开拍(初始化运行环境)
3、正式拍摄:导演喊"Action!"(执行main函数)
4、收尾工作:打包器材,清理现场(释放资源,结束程序)
而我们平时只关注"正式拍摄"阶段,却忽略了其他同样重要的环节。
第一幕:操作系统的角色
当你双击一个.exe文件或者以命令行./program 执行时,发生了什么?
操作系统会首先加载可执行文件到内存,然后做一系列准备工作:
1、创建进程和线程
2、分配栈空间和堆空间
3、加载依赖的动态链接库(DLL或so文件)
4、 设置各种环境变量
这就像电影开拍前,场务人员布置好拍摄场地,准备好所有道具。
第二幕:C/C++运行时的初始化
操作系统准备好后,并不会直接跳到main函数,而是先调用C/C++运行时库的初始化代码。在Windows中,这通常是_start或mainCRTStartup,在Linux中是_start。
这个启动函数负责完成以下工作
1、初始化C运行时库
2、设置堆管理器的数据结构
3、初始化I/O子系统
4、处理命令行参数(构建argc和argv)
5、初始化全局变量和静态变量
6、调用全局对象的构造函数
7、最后才调用main函数
看到了吗?main函数实际上是被运行时库调用的!它不是起点,而是运行时库准备好一切后才执行的函数。
我们来看个例子
[C++] 纯文本查看 复制代码
#include <iostream>

// 全局变量
int globalVar = 42;

// 全局对象
class GlobalObject {
public:
    GlobalObject() {
        std::cout << "全局对象构造函数被调用,此时main还没开始执行!" << std::endl;
    }
    ~GlobalObject() {
        std::cout << "全局对象析构函数被调用,此时main已经结束了!" << std::endl;
    }
};

GlobalObject g_obj; // 全局对象实例

int main() {
    std::cout << "现在才是main函数开始执行..." << std::endl;
    std::cout << "全局变量值:" << globalVar << std::endl;
    std::cout << "main函数结束..." << std::endl;
    return0;
}
运行这段代码,你会惊讶地发现输出是:
[C++] 纯文本查看 复制代码
全局对象构造函数被调用,此时main还没开始执行!
现在才是main函数开始执行...
全局变量值:42
main函数结束...
全局对象析构函数被调用,此时main已经结束了!
看到了吗?全局对象的构造函数在 main 函数之前就执行了!这就是最直接的证据:程序并非从 main 开始
第三幕:main函数 - 只是主角,而非导演
main函数的确很重要,它是我们编写业务逻辑的地方。但它就像电影中的主角,是整部戏的核心,却不是整个电影制作的起点。
main函数有两种标准形式
[C++] 纯文本查看 复制代码
int main() { /* ... */ }
或者带命令行参数的版本:
[C++] 纯文本查看 复制代码
int main(int argc, char* argv[]) { /* ... */ }
这些参数是谁准备的?没错,是运行时库!它将操作系统传来的命令行参数整理成C++程序易于使用的格式,然后再传给 main 函数。
第四幕:main函数结束后的故事
很多人以为 main 函数结束,程序就立刻退出了。但实际上,这只是电影的高潮过去了,还有结尾要拍。
main 函数返回后
1、运行时库接收到 main 的返回值
2、调用全局对象的析构函数(按照创建的相反顺序)
3、释放程序资源
4、将main的返回值传递给操作系统
5、最后结束进程
这就解释了为什么全局对象的析构函数在 main 函数结束后才被调用。
实战例子:我们来抓个现行!
光说不练假把式。我们来做个实验,亲眼看看 main 函数前后都发生了什么。
[C++] 纯文本查看 复制代码
#include <iostream>

// 定义一个计数器
int initCounter = 0;

// 全局变量初始化
int globalA = ++initCounter;  // 应该是1
int globalB = ++initCounter;  // 应该是2

// 使用__attribute__((constructor))在main之前执行函数(GCC编译器特性)
__attribute__((constructor))
void beforeMain() {
    std::cout << "【main之前】beforeMain函数执行,计数器值:" << initCounter << std::endl;
    std::cout << "【main之前】全局变量globalA = " << globalA << ", globalB = " << globalB << std::endl;
}

// 使用__attribute__((destructor))在main之后执行函数
__attribute__((destructor))
void afterMain() {
    std::cout << "【main之后】afterMain函数执行,计数器值:" << initCounter << std::endl;
}

// 全局类
class GlobalTracer {
public:
    GlobalTracer(constchar* name) : name_(name) {
        std::cout << "【main之前】全局对象 " << name_ << " 构造,计数器值:" << ++initCounter << std::endl;
    }
    
    ~GlobalTracer() {
        std::cout << "【main之后】全局对象 " << name_ << " 析构,计数器值:" << ++initCounter << std::endl;
    }
private:
    constchar* name_;
};

// 创建全局对象
GlobalTracer tracerA("A");  // 计数器应该是3
GlobalTracer tracerB("B");  // 计数器应该是4

// main函数
int main(int argc, char* argv[]) {
    std::cout << "\n【main开始】main函数开始执行,计数器值:" << ++initCounter << std::endl;
    std::cout << "【main中】命令行参数数量: " << argc << std::endl;
    
    // 创建局部对象
    GlobalTracer localObj("Local");  // 计数器应该是6
    
    std::cout << "【main结束】main函数即将结束,计数器值:" << ++initCounter << std::endl;
    return0;
}
Linux 下用g++编译运行这段代码,你会得到类似这样的输出:
[C++] 纯文本查看 复制代码
【main之前】beforeMain函数执行,计数器值:0
【main之前】全局变量globalA = 0, globalB = 0
【main之前】全局对象 A 构造,计数器值:3
【main之前】全局对象 B 构造,计数器值:4

【main开始】main函数开始执行,计数器值:5
【main中】命令行参数数量: 1
【main之前】全局对象 Local 构造,计数器值:6
【main结束】main函数即将结束,计数器值:7
【main之后】全局对象 Local 析构,计数器值:8
【main之后】全局对象 B 析构,计数器值:9
【main之后】全局对象 A 析构,计数器值:10
【main之后】afterMain函数执行,计数器值:10
从输出中,我们可以清晰地看到整个流程
1、首先初始化全局变量globalA和globalB
2、然后执行标记为constructor的beforeMain函数
3、接着构造全局对象A和B
4、之后才开始执行main函数
5、main函数返回后,首先析构局部对象Local
6、然后按照与构造相反的顺序析构全局对象B和A
7、最后执行标记为destructor的afterMain函数
三、初始化顺序:魔鬼藏在细节里
现在我们知道 C++ 程序不是从 main 开始的了,接下来要面对的是另一个容易让人头疼的问题:全局变量和对象的初始化顺序。这个问题就像是魔鬼一样,藏在细节里,稍不注意就会导致奇怪的bug。
同一个.cpp文件中的初始化是有序的
好消息是,如果所有全局变量和对象都在同一个.cpp文件中,那么它们的初始化顺序是完全可预测的:
·全局变量按照你写代码的顺序初始化(从上到下)
·全局对象也按照你写代码的顺序构造(从上到下)
举个简单的例子
[C++] 纯文本查看 复制代码
#include <iostream>

int apple = 5;
int banana = apple * 2;  // banana = 10,因为apple已经初始化为5了

class Fruit {
public:
    Fruit(const char* name) {
        std::cout << name << "被构造了,此时banana = " << banana << std::endl;
    }
};

Fruit orange("橙子");  // 输出"橙子被构造了,此时banana = 10"
Fruit grape("葡萄");   // 输出"葡萄被构造了,此时banana = 10"
在这个例子中,一切都按照我们的预期进行:apple先初始化,然后banana初始化,接着orange构造,最后grape构造。这很简单,对吧?
但是...不同.cpp文件中的初始化顺序是个迷
现在问题来了!当你的程序有多个.cpp文件,每个文件都有自己的全局变量和对象时,它们之间的初始化顺序就变得不确定了。
想象一下这种情况
[C++] 纯文本查看 复制代码
// 文件1:breakfast.cpp
#include <iostream>

// 声明一个在dinner.cpp中定义的变量
externint dinnerTime;

// 定义早餐时间
int breakfastTime = 7;

// 计算从早餐到晚餐的时间
int hoursBetweenMeals = dinnerTime - breakfastTime;

class Breakfast {
public:
    Breakfast() {
        std::cout << "早餐准备好了!距离晚餐还有"
                  << hoursBetweenMeals << "小时" << std::endl;
    }
};

// 创建早餐对象
Breakfast myBreakfast;

// 文件2:dinner.cpp
#include <iostream>

// 声明一个在breakfast.cpp中定义的变量
externint breakfastTime;

// 定义晚餐时间
int dinnerTime = 18;

// 计算从早餐到晚餐的时间(和breakfast.cpp中的计算相同)
int mealGap = dinnerTime - breakfastTime;

class Dinner {
public:
    Dinner() {
        std::cout << "晚餐准备好了!距离早餐已经过了"
                  << mealGap << "小时" << std::endl;
    }
};

// 创建晚餐对象
Dinner myDinner;
问题来了
·谁会先被初始化?Breakfast Time还是dinner Time?
·Hours Between Meals和meal Gap的值会是多少?
·My Breakfast和my Dinner哪个会先构造?
答案是:完全不确定!这完全取决于编译器和链接器如何组合这些文件,而这些通常不在我们的控制范围内。
这就会导致非常诡异的问题。比如,如果dinner.cpp先初始化:
Dinner Time被设为18
breakfast Time还没初始化,它的值可能是任意垃圾值
·meal Gap = 18 - 垃圾值,得到一个无意义的结果
·my Dinner构造时打印这个无意义的值
然后breakfast.cpp才开始初始化...
这种情况下,程序不会崩溃,但会输出错误的结果,这种bug特别难找!
拯救方案:用函数内的静态变量
幸好,C++新标准提供了一个简单而优雅的解决方案,叫做"函数内静态变量"。这种方式有个特点:它们只在第一次调用该函数时才会被初始化。
我们来看看如何利用这个特性解决问题:
[C++] 纯文本查看 复制代码
// 使用函数包装我们的全局变量
int& getBreakfastTime() {
    staticint breakfastTime = 7;  // 只在第一次调用时初始化
    return breakfastTime;
}

int& getDinnerTime() {
    staticint dinnerTime = 18;  // 只在第一次调用时初始化
    return dinnerTime;
}

// 需要用到这些值时,调用函数获取
int getHoursBetweenMeals() {
    return getDinnerTime() - getBreakfastTime();  // 现在顺序没问题了!
}
这种方式,我们不再依赖全局变量的初始化顺序,而是在需要用到这些值的时候才去获取它们。由于函数内静态变量保证只初始化一次,所以无论你调用多少次,都只会有一份数据。
还可以把这种思路扩展为"单例模式",用于全局对象:
[C++] 纯文本查看 复制代码
class Restaurant {
public:
    // 获取唯一的Restaurant实例
    static Restaurant& getInstance() {
        // 这个static对象只在第一次调用时创建
        static Restaurant instance;
        return instance;
    }
    
    void serveBreakfast() {
        std::cout << "早餐时间到!" << std::endl;
    }
    
    void serveDinner() {
        std::cout << "晚餐时间到!" << std::endl;
    }
    
private:
    // 构造函数设为私有,防止外部创建对象
    Restaurant() {
        std::cout << "餐厅开业了!" << std::endl;
    }
};

// 使用方式
void morningRoutine() {
    // 第一次调用会初始化Restaurant
    Restaurant::getInstance().serveBreakfast();
}

void eveningRoutine() {
    // 再次调用会返回同一个Restaurant实例
    Restaurant::getInstance().serveDinner();
}
这样,无论morning Routine()和evening Routine()哪个先被调用,Restaurant对象都只会在第一次调用时被创建,而且我们可以确保在使用它之前它已经被正确初始化了。
这就是为什么单例模式在C++中如此流行 - 它不仅能保证全局只有一个实例,还能解决初始化顺序的问题!厉害吧?
四、深入理解:一个完整程序的启动过程
让我们把整个过程连起来,看看从你双击程序到 main 函数执行再到程序结束,完整的流程是怎样的:
1、操作系统加载阶段
·加载可执行文件到内存
·创建进程和线程
·分配内存空间(栈、堆等)
·加载所需的动态链接库
·跳转到程序入口点(通常是_start)
2、 C/C++运行时初始化阶段
·初始化C运行时库
·设置堆管理器的数据结构
·初始化I/O子系统
·设置环境变量
·准备命令行参数(argc, argv)
·初始化全局/静态变量和对象
·调用constructor属性的函数
3、 main函数执行阶段
·调用main(argc, argv)
·执行用户代码
·返回退出码
4、 程序终止阶段
·接收main函数的返回值
·调用全局/静态对象的析构函数
·调用destructor属性的函数
·释放程序资源
·将退出码返回给操作系统
·终止进程
五、实际应用:为什么这些知识很重要?
你可能会想:"知道这些有什么用?反正我的代码还是从main开始写起。"
实际上,理解这个过程对解决许多实际问题非常有帮助:
1. 全局对象的依赖问题
如果你的程序使用全局对象,并且这些对象之间有依赖关系,那么初始化顺序就至关重要。了解C++的初始化机制可以帮你避免因初始化顺序不确定导致的微妙bug。
2. 资源管理
理解main函数返回后的清理过程,有助于你正确管理资源,避免其他资源泄漏问题。
3. 构造函数中的陷阱
全局对象的构造函数中不应该依赖其他全局对象(除非你能确保初始化顺序),因为这可能导致"静态初始化顺序问题"。
4. 调试复杂问题
当你遇到一些奇怪的问题,比如程序启动崩溃但没有明显错误时,了解启动过程可以帮你定位问题。
5. 面试加分项
这绝对是面试中的一个亮点!当面试官问"C++程序从哪里开始执行"时,如果你能详细解释整个过程,一定会给面试官留下深刻印象。
六、高级技巧:控制main函数前后的执行
了解了C++程序的启动过程,我们还可以利用这些知识来做一些有趣的事情:
1. 在main之前执行代码
除了前面提到的__attribute__((constructor)),还有其他方法可以在main之前执行代码:
全局对象的构造函数
[C++] 纯文本查看 复制代码
class StartupManager {
public:
    StartupManager() {
        // 这里的代码会在main之前执行
        std::cout << "程序启动中..." << std::endl;
        // 做一些初始化工作
    }
};

// 创建全局对象
StartupManager g_startupManager;
编译器特定的扩展
GCC中:
[C++] 纯文本查看 复制代码
void beforeMain() __attribute__((constructor));
void beforeMain() {
    // 这里的代码会在main之前执行
}
2. 在main之后执行代码使用atexit注册清理函数
[C++] 纯文本查看 复制代码
#include <cstdlib>

void cleanupFunction() {
    // 这里的代码会在main之后执行
    std::cout << "程序清理中..." << std::endl;
}

int main() {
    // 注册清理函数
    atexit(cleanupFunction);
    
    std::cout << "main已结束..." << std::endl;
    // 正常的main函数代码
    return0;
}
全局对象的析构函数
[C++] 纯文本查看 复制代码
class ShutdownManager {
public:
    ~ShutdownManager() {
        // 这里的代码会在main之后执行
        std::cout << "程序关闭中..." << std::endl;
    }
};

// 创建全局对象
ShutdownManager g_shutdownManager;
编译器特定的扩展
GCC中:
[C++] 纯文本查看 复制代码
void afterMain() __attribute__((destructor));
void afterMain() {
    // 这里的代码会在main之后执行
}
总结:揭开C++启动的神秘面纱
通过这篇文章,我们已经揭开了C++程序启动过程的神秘面纱:
1、C++程序根本不是从main函数开始的!在main执行前,系统和运行时库已经偷偷做了大量工作
2、全局变量和对象在main函数执行前就已经初始化完毕,这就是为什么main函数一开始就能使用它们
3、main函数结束不等于程序结束,之后还有全局对象析构、资源释放等一系列"收尾工作"
4、跨文件的全局对象初始化顺序是个"定时炸弹",搞不好就会引发难以察觉的bug
5、掌握了这些知识,你可以利用constructor/destructor属性、全局对象构造/析构函数、atexit函数等工具在main函数前后插入自己的代码,实现自动初始化和清理功能
下次有人告诉你"C++程序从main开始执行",你可以自豪地纠正他们了!
是不是觉得C++比想象的要复杂得多?别担心,这正是C++的魅力所在 — 它让你能掌控程序的每一个细节,从出生到死亡的全过程。真正的C++高手,就是了解这些不为人知的秘密!



运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-1005711-1-1.html 上篇帖子: C++26 即将上线,它将如何颠覆我们的编程认知? 下篇帖子: 为何 C++ 多态设计总出错?虚函数底层逻辑
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

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