MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ main函数执行前的代码执行情况

2024-01-311.2k 阅读

全局变量的初始化

在C++ 中,全局变量在 main 函数执行前就已经开始初始化。这是因为全局变量的生命周期贯穿整个程序的运行过程,从程序启动到程序结束。全局变量存储在静态存储区,它们的初始化发生在程序加载阶段,早于 main 函数的执行。

静态全局变量

静态全局变量与普通全局变量类似,只不过它的作用域被限制在定义它的文件内。静态全局变量同样在 main 函数执行前初始化。

// file1.cpp
#include <iostream>
static int staticGlobal = 10;
void printStaticGlobal() {
    std::cout << "Static global variable: " << staticGlobal << std::endl;
}
// main.cpp
#include <iostream>
// 这里不能直接访问file1.cpp中的staticGlobal
int main() {
    // 如果试图访问staticGlobal会导致编译错误
    // std::cout << staticGlobal << std::endl;
    return 0;
}

在上述代码中,staticGlobal 是一个静态全局变量,在 main 函数执行前就已经被初始化为10。虽然它不能在其他文件中直接访问,但在定义它的 file1.cpp 文件内,它在程序启动时就已经准备好。

普通全局变量

普通全局变量的作用域从定义处开始到整个程序结束。多个文件可以通过 extern 关键字共享同一个全局变量。

// file1.cpp
#include <iostream>
int globalVar;
void setGlobalVar(int value) {
    globalVar = value;
}
// main.cpp
#include <iostream>
extern int globalVar;
int main() {
    setGlobalVar(20);
    std::cout << "Global variable: " << globalVar << std::endl;
    return 0;
}

在这个例子中,globalVar 是一个普通全局变量。在 main 函数执行前,它已经在内存中分配了空间。如果没有显式初始化,它会被默认初始化为0(对于内置类型)。在 file1.cpp 中定义,在 main.cpp 中通过 extern 声明后可以使用。

全局对象的构造函数

当全局变量是一个自定义对象时,在 main 函数执行前,该对象的构造函数会被调用。

class GlobalObject {
public:
    GlobalObject() {
        std::cout << "GlobalObject constructor called" << std::endl;
    }
};
GlobalObject globalObj;
int main() {
    std::cout << "Inside main" << std::endl;
    return 0;
}

在上述代码中,GlobalObject 类的对象 globalObj 是一个全局对象。在程序启动时,在 main 函数执行前,GlobalObject 的构造函数会被调用,输出 “GlobalObject constructor called”,然后才进入 main 函数输出 “Inside main”。

静态局部变量的初始化

静态局部变量虽然定义在函数内部,但它的初始化也发生在 main 函数执行前(更准确地说,是在首次调用包含该静态局部变量的函数之前)。静态局部变量存储在静态存储区,它的生命周期和全局变量一样贯穿整个程序,但作用域仅限于定义它的函数内部。

静态局部变量的首次初始化

void func() {
    static int staticLocal = 5;
    std::cout << "Static local variable: " << staticLocal << std::endl;
    staticLocal++;
}
int main() {
    func();
    func();
    return 0;
}

在上述代码中,staticLocal 是一个静态局部变量。在 main 函数执行前,staticLocal 已经被初始化为5。每次调用 func 函数时,staticLocal 的值会保留上次修改后的值并继续递增。第一次调用 func 输出 “Static local variable: 5”,第二次调用输出 “Static local variable: 6”。

静态局部对象的构造

当静态局部变量是一个自定义对象时,在首次调用包含该变量的函数前,其构造函数会被调用。

class StaticLocalObject {
public:
    StaticLocalObject() {
        std::cout << "StaticLocalObject constructor called" << std::endl;
    }
};
void funcWithObject() {
    static StaticLocalObject localObj;
    std::cout << "Inside funcWithObject" << std::endl;
}
int main() {
    funcWithObject();
    funcWithObject();
    return 0;
}

在这个例子中,StaticLocalObject 类的对象 localObj 是一个静态局部对象。在首次调用 funcWithObject 函数前,StaticLocalObject 的构造函数会被调用,输出 “StaticLocalObject constructor called”,然后进入 funcWithObject 函数输出 “Inside funcWithObject”。再次调用 funcWithObject 时,构造函数不会再次调用,因为对象已经存在。

初始化顺序

全局变量之间的初始化顺序

在同一个翻译单元(通常是一个源文件及其包含的头文件)中,全局变量按照它们定义的顺序进行初始化。

int var1 = 1;
int var2 = var1 + 1;
int main() {
    std::cout << "var1: " << var1 << ", var2: " << var2 << std::endl;
    return 0;
}

在上述代码中,var1 先被定义并初始化为1,然后 var2 被定义并初始化为 var1 + 1,即2。所以在 main 函数中输出 “var1: 1, var2: 2”。

然而,当涉及多个翻译单元时,全局变量的初始化顺序是未定义的。

// file1.cpp
#include <iostream>
int globalInFile1;
void printGlobalInFile1() {
    std::cout << "Global in file1: " << globalInFile1 << std::endl;
}
// file2.cpp
#include <iostream>
extern int globalInFile1;
int globalInFile2 = globalInFile1 + 1;
void printGlobalInFile2() {
    std::cout << "Global in file2: " << globalInFile2 << std::endl;
}
// main.cpp
#include <iostream>
extern int globalInFile1;
extern int globalInFile2;
int main() {
    globalInFile1 = 10;
    printGlobalInFile1();
    printGlobalInFile2();
    return 0;
}

在这个例子中,globalInFile2 的初始化依赖于 globalInFile1。但由于不同翻译单元中全局变量初始化顺序未定义,在 main 函数中先给 globalInFile1 赋值10,可能在 file2.cppglobalInFile2 初始化时 globalInFile1 还未被赋值,这会导致未定义行为。

全局变量与静态局部变量的初始化顺序

全局变量总是在任何静态局部变量之前初始化。因为全局变量在程序启动时就开始初始化,而静态局部变量在首次调用包含它们的函数前才初始化。

int globalVar = 1;
void func() {
    static int staticLocal = globalVar + 1;
    std::cout << "Static local: " << staticLocal << std::endl;
}
int main() {
    func();
    return 0;
}

在上述代码中,globalVar 先被初始化为1,然后在调用 func 函数时,staticLocal 被初始化为 globalVar + 1,即2,输出 “Static local: 2”。

预处理器指令与 main 函数前的执行

宏定义与替换

预处理器指令在编译的预处理阶段执行,远在 main 函数执行前。宏定义是预处理器的一项重要功能,它可以进行文本替换。

#define PI 3.14159
int main() {
    double radius = 5.0;
    double area = PI * radius * radius;
    std::cout << "Area of circle: " << area << std::endl;
    return 0;
}

在上述代码中,在预处理阶段,所有的 PI 都会被替换为 3.14159。这个替换过程在编译器对代码进行语法分析和语义分析之前就已经完成,与 main 函数的执行没有直接关系,但它对最终生成的代码有重要影响。

文件包含

#include 指令用于将一个文件的内容插入到当前文件中。这同样发生在预处理阶段。

// common.h
#define MAX(a, b) ((a) > (b)? (a) : (b))
// main.cpp
#include <iostream>
#include "common.h"
int main() {
    int num1 = 10, num2 = 20;
    int maxNum = MAX(num1, num2);
    std::cout << "Max number: " << maxNum << std::endl;
    return 0;
}

main.cpp 中,#include "common.h" 指令使得 common.h 的内容在预处理阶段被插入到 main.cpp 中。这样 MAX 宏定义在 main 函数所在的文件中可用,在 main 函数执行前,宏替换就已经完成。

构造函数与 main 函数前的执行

全局对象构造函数的执行

如前面提到的,全局对象的构造函数在 main 函数执行前被调用。这对于一些需要在程序启动时就进行初始化的资源管理类非常有用。

class DatabaseConnection {
public:
    DatabaseConnection() {
        std::cout << "Connecting to database..." << std::endl;
        // 实际的数据库连接代码
    }
    ~DatabaseConnection() {
        std::cout << "Disconnecting from database..." << std::endl;
        // 实际的数据库断开连接代码
    }
};
DatabaseConnection dbConn;
int main() {
    std::cout << "Inside main, database is connected" << std::endl;
    return 0;
}

在这个例子中,DatabaseConnection 类的对象 dbConn 是全局对象。在 main 函数执行前,DatabaseConnection 的构造函数会被调用,输出 “Connecting to database...”,表示数据库连接操作在程序启动时就已经完成。当 main 函数结束,dbConn 的析构函数会被调用,输出 “Disconnecting from database...”。

静态成员变量的初始化

类的静态成员变量在 main 函数执行前也会被初始化。静态成员变量属于类,而不是类的对象,它在程序启动时就存在。

class MyClass {
public:
    static int staticMember;
};
int MyClass::staticMember = 10;
int main() {
    std::cout << "MyClass static member: " << MyClass::staticMember << std::endl;
    return 0;
}

在上述代码中,MyClass::staticMemberMyClass 类的静态成员变量。在 main 函数执行前,它被初始化为10。在 main 函数中可以直接访问并输出其值。

线程局部存储与 main 函数前的执行

线程局部存储变量的初始化

线程局部存储(Thread - Local Storage,TLS)变量在每个线程中都有独立的实例。对于线程局部存储的全局变量,它们的初始化发生在每个线程启动时,而不是在 main 函数执行前的程序启动阶段。

#include <iostream>
#include <thread>
thread_local int threadLocalVar = 0;
void threadFunction() {
    threadLocalVar++;
    std::cout << "Thread local variable in thread: " << threadLocalVar << std::endl;
}
int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);
    t1.join();
    t2.join();
    return 0;
}

在上述代码中,threadLocalVar 是一个线程局部存储变量。当 t1t2 线程启动时,各自的 threadLocalVar 实例被初始化为0,然后在 threadFunction 中递增并输出。两个线程中的 threadLocalVar 是相互独立的,所以输出结果可能是 “Thread local variable in thread: 1” 两次。

线程局部存储对象的构造

如果线程局部存储变量是一个自定义对象,其构造函数在每个线程启动时被调用。

class ThreadLocalObject {
public:
    ThreadLocalObject() {
        std::cout << "ThreadLocalObject constructor in thread" << std::endl;
    }
};
thread_local ThreadLocalObject threadObj;
void threadFuncWithObject() {
    std::cout << "Inside threadFuncWithObject" << std::endl;
}
int main() {
    std::thread t3(threadFuncWithObject);
    std::thread t4(threadFuncWithObject);
    t3.join();
    t4.join();
    return 0;
}

在这个例子中,ThreadLocalObject 类的对象 threadObj 是线程局部存储对象。当 t3t4 线程启动时,各自的 threadObj 构造函数会被调用,输出 “ThreadLocalObject constructor in thread”,然后进入 threadFuncWithObject 函数输出 “Inside threadFuncWithObject”。

动态链接库与 main 函数前的执行

动态链接库全局变量的初始化

当程序使用动态链接库(DLL 或共享库)时,动态链接库中的全局变量在动态链接库被加载时进行初始化,这通常发生在 main 函数执行前(如果动态链接库在程序启动时就被加载)。

// dllmain.cpp (动态链接库的入口文件,不同平台略有差异)
#include <windows.h>
#include <iostream>
int dllGlobal = 100;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
        std::cout << "DLL loaded, initializing global variable" << std::endl;
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
// main.cpp
#include <iostream>
#include <windows.h>
extern int dllGlobal;
int main() {
    HINSTANCE hDLL = LoadLibrary(TEXT("YourDLL.dll"));
    if (hDLL != NULL) {
        std::cout << "DLL global variable: " << dllGlobal << std::endl;
        FreeLibrary(hDLL);
    }
    return 0;
}

在上述代码中,动态链接库中的 dllGlobal 变量在动态链接库被加载(DLL_PROCESS_ATTACH 时)初始化。在 main 函数中加载动态链接库后可以访问该全局变量。

动态链接库静态局部变量的初始化

动态链接库中的静态局部变量同样在首次调用包含它们的函数前初始化,这与普通可执行文件中的静态局部变量类似。

// dllfunctions.cpp
#include <iostream>
void dllFunction() {
    static int dllStaticLocal = 5;
    std::cout << "DLL static local variable: " << dllStaticLocal << std::endl;
    dllStaticLocal++;
}
// main.cpp
#include <iostream>
#include <windows.h>
typedef void (*DLLFUNC)();
int main() {
    HINSTANCE hDLL = LoadLibrary(TEXT("YourDLL.dll"));
    if (hDLL != NULL) {
        DLLFUNC func = (DLLFUNC)GetProcAddress(hDLL, "dllFunction");
        if (func != NULL) {
            func();
            func();
        }
        FreeLibrary(hDLL);
    }
    return 0;
}

在这个例子中,dllFunction 中的 dllStaticLocal 是动态链接库中的静态局部变量。每次调用 dllFunction 时,dllStaticLocal 的值会保留上次修改后的值并递增,类似于普通可执行文件中的情况。

异常处理与 main 函数前的执行

全局对象构造函数中的异常

如果全局对象的构造函数抛出异常,程序在 main 函数执行前就会终止,除非异常被捕获。

class ThrowingObject {
public:
    ThrowingObject() {
        throw std::runtime_error("Constructor exception");
    }
};
ThrowingObject globalThrowingObj;
int main() {
    std::cout << "This will not be printed" << std::endl;
    return 0;
}

在上述代码中,ThrowingObject 类的对象 globalThrowingObj 是全局对象。其构造函数抛出一个 std::runtime_error 异常。由于这个异常没有在全局对象构造期间被捕获,程序会在 main 函数执行前终止,“This will not be printed” 不会被输出。

静态局部对象构造函数中的异常

静态局部对象构造函数抛出的异常同样会导致程序问题。如果在静态局部对象构造函数抛出异常且未被捕获,当包含该对象的函数再次被调用时,可能会导致未定义行为。

class StaticLocalThrowingObject {
public:
    StaticLocalThrowingObject() {
        throw std::runtime_error("Static local constructor exception");
    }
};
void funcWithThrowingObject() {
    static StaticLocalThrowingObject localThrowObj;
    std::cout << "This will not be printed" << std::endl;
}
int main() {
    try {
        funcWithThrowingObject();
    } catch (const std::runtime_error& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }
    return 0;
}

在这个例子中,StaticLocalThrowingObject 类的对象 localThrowObj 是静态局部对象。其构造函数抛出异常。在 main 函数中调用 funcWithThrowingObject 时,异常被捕获并输出 “Caught exception: Static local constructor exception”,“This will not be printed” 不会被输出。如果没有 try - catch 块,程序可能会异常终止。

通过对上述各个方面的深入分析,我们全面了解了C++ 中在 main 函数执行前代码的执行情况,这对于编写健壮、高效且正确的C++ 程序至关重要。无论是全局变量、静态局部变量的初始化,还是预处理器指令、构造函数、线程局部存储、动态链接库以及异常处理等方面,都相互关联并影响着程序的启动过程。