[TOC]

编程语言知识点一

一、在main执行之前和之后的操作

在 C++ 程序中,main 函数虽然是程序员编写代码的入口,但并不是程序运行的真正起点,也不是终点。在 main 执行前后,操作系统和 C++ 运行时库(C Runtime Library, CRT)会进行一系列重要的初始化和清理操作。

1、在 main 执行之前的操作(初始化阶段):

在控制权移交给 main 之前,程序主要进行静态资源的初始化运行环境的准备

a、所有静态存储期的变量初始化

在 main 之前初始化的:1、全局变量 2、全局 static 变量 3、全局对象 4、全局 static 对象 5、类的 static 成员变量(这个 static 变量属于类,而不是对象)

不在 main 之前初始化的:局部的 static 变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <iostream>
#include <string>
// 1. 全局变量 (main 之前初始化)
int globalVar = 100;

// 2. 全局 static 变量 (main 之前初始化,作用域限制在本文件)
static int globalStaticVar = 200;

// 3. 全局对象 (构造函数在 main 之前调用)
class InitTracker {
private:
std::string name;
int value;
public:
InitTracker(const std::string& n, int v) : name(n), value(v) {
std::cout << "[BEFORE MAIN] Constructor called for: " << name
<< " with value: " << value << std::endl;
}

~InitTracker() {
std::cout << "[AFTER MAIN] Destructor called for: " << name << std::endl;
}

int getValue() const { return value; }
};

// 全局对象 - 在 main 之前构造
InitTracker globalObj("GlobalObject", 1);

// 全局 static 对象 - 在 main 之前构造
static InitTracker globalStaticObj("GlobalStaticObject", 2);

// 4. 演示函数,包含局部 static 变量
void demonstrateLocalStatic() {
std::cout << "\n=== Inside demonstrateLocalStatic() ===" << std::endl;

// 局部 static 变量 - 第一次调用时初始化,不是在 main 之前!
static InitTracker localStaticObj("LocalStaticObject", 3);
static int localStaticInt = 300;

std::cout << "Local static int: " << localStaticInt << std::endl;
localStaticInt++; // 下次调用时会保持这个值
}

// 5. 类的 static 成员变量
class MyClass {
public:
static int staticMember;
static InitTracker staticMemberObj;
};

// 类的 static 成员需要在类外定义,在 main 之前初始化
int MyClass::staticMember = 400;
InitTracker MyClass::staticMemberObj("ClassStaticMember", 4);

// ==================== Main 函数 ====================

int main() {
std::cout << "\n========================================" << std::endl;
std::cout << "======== ENTERED MAIN FUNCTION ========" << std::endl;
std::cout << "========================================\n" << std::endl;

// 验证全局变量已经初始化
std::cout << "Global variable: " << globalVar << std::endl;
std::cout << "Global static variable: " << globalStaticVar << std::endl;
std::cout << "Global object value: " << globalObj.getValue() << std::endl;
std::cout << "Global static object value: " << globalStaticObj.getValue() << std::endl;
std::cout << "Class static member: " << MyClass::staticMember << std::endl;
std::cout << "Class static member object: " << MyClass::staticMemberObj.getValue() << std::endl;

// 第一次调用 - 局部 static 变量会在这里初始化
std::cout << "\n--- First call to demonstrateLocalStatic ---" << std::endl;
demonstrateLocalStatic();

// 第二次调用 - 局部 static 变量不会重新初始化
std::cout << "\n--- Second call to demonstrateLocalStatic ---" << std::endl;
demonstrateLocalStatic();

// 演示局部非 static 对象
std::cout << "\n--- Creating local non-static object ---" << std::endl;
{
InitTracker localObj("LocalObject", 5);
std::cout << "Local object value: " << localObj.getValue() << std::endl;
} // localObj 在这里被销毁

std::cout << "\n========================================" << std::endl;
std::cout << "======== EXITING MAIN FUNCTION ========" << std::endl;
std::cout << "========================================\n" << std::endl;

return 0;
}

b、GCC/Clang 特有扩展:Constructor 属性

GCC 和 Clang 编译器允许使用 __attribute__((constructor)) 将普通函数标记为在 main 之前运行。这常用于库的初始化。过度依赖这种自动初始化可能导致程序的可维护性下降,特别是在程序比较复杂时。

1
__attribute__((constructor)) void preMain() { printf("I run befor

c、 动态链接库(DLL/Shared Object)的加载

动态链接库(Dynamic Link Library,简称 DLL)是 Windows 操作系统中使用的一种文件格式,而在类 Unix 操作系统中,类似的文件通常被称为共享对象文件(Shared Object,简称 .so)。动态链接库的主要作用是允许多个程序共享库中的代码和资源,从而节省内存和存储空间,并允许程序在运行时根据需要加载和链接库。

与静态链接库(Static Library)不同,静态链接库的代码和数据在编译时就被直接复制到应用程序的执行文件中,而动态链接库的代码是在程序运行时通过操作系统加载的。动态链接库通常包含一些通用的功能,像图形处理、网络通信、数据库操作等,多个程序可以共享它们,从而减少了程序的大小和重复性。

如果程序依赖动态库,操作系统的加载器(Loader)会先加载这些库,并执行库中的初始化代码(如全局对象的构造)。

  • (由操作系统隐式完成,但如果在代码中显式加载)
1
// 动态库中的 _init() 函数或全局变量会在库加载时(通常在 main 前)执行

d、 初始化运行时环境(CRT Startup)

这是由编译器插入的引导代码(通常叫 _start)。它负责设置栈(Stack)、初始化堆(Heap)、初始化 I/O 缓冲区、以及解析命令行参数(argc, argv)。

  • (这是汇编层面的操作,C++ 中表现为 main 的参数准备好)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    _start:

    设置栈指针(Stack Pointer)

    调用 __libc_start_main()

    初始化:
    - 全局变量
    - 静态变量
    - C++ 全局对象构造函数
    - malloc 内部结构
    - I/O 缓冲区(stdout 缓冲模式)
    - TLS(线程本地存储)

    准备 main 的参数 argc, argv

    调用 main(argc, argv)

    等 main 返回,接着调用 exit()
    - 调用 static/global 对象的析构函数
    - 调用 atexit 注册的回调函数
    - 清理堆内存等

2、 main 执行之后的操作(清理阶段)

main 函数执行 return,或者调用 exit() 时,程序并未立即结束,而是进入清理阶段。

a. 注册的 atexit 函数

C++ 标准允许使用 std::atexit 注册函数,这些函数会按照“后注册先执行”(LIFO,栈的顺序)的顺序在 main 结束后被调用。

1
std::atexit(myCleanupFunction); // 该函数会在 main 返回后被系统自动调用

b. 全局变量和静态变量的销毁

与初始化顺序相反,所有全局对象和静态对象的析构函数会在 main 结束后被调用。

1
// 之前定义的 globalObject 的析构函数 ~MyClass() 会在此刻执行

c. GCC/Clang 特有扩展:Destructor 属性

类似于构造属性,被标记为 destructor 的函数会在 main 结束后执行。

1
__attribute__((destructor)) void postMain() { printf("I run after main!\n"); }

4. 刷新 I/O 缓冲区

系统会关闭打开的文件描述符,并强制刷新标准输出(stdout/stderr)缓冲区,确保所有未输出的日志都被写入文件或屏幕。

1
// C++ 运行时会自动执行 fflush(stdout) 和 fclose(all_streams)

二、指针 VS 引用

指针和引用的区别是 C++中最重要最经典的知识点,简单的来说:指针是一个存储地址的变量,而引用是原变量的一个别名(类似于别人给你取的绰号)

1、核心区别

a、初始化和空值(这是最大的区别)

指针: 可以不初始化(虽然危险),也可以设为 nullptr(空指针)。它可以在程序运行中暂时不指向任何东西。

引用: 必须在定义时立刻初始化,且绝对不存在“空引用”(Null Reference)。引用一旦诞生,就必须属于某个对象。

1
2
3
4
5
6
7
8
9
10
int a = 10;
// 指针
int* p; // 合法,但危险(野指针)
int* p2 = nullptr;// 合法,空指针
p = &a; // 合法,后续赋值

// 引用
// int& r; // 编译错误!必须初始化
int& r = a; // 合法
// int& r2 = nullptr; // 错误,不能绑定到空

b、可变性

指针: 是一个自由的变量。它今天可以指向 A,明天可以指向 B。

引用: 是“从一而终”的。一旦引用绑定了变量 a,它就永远是 a 的别名。如果你给引用赋值,不是改变引用指向谁,而是改变原变量的值

1
2
3
4
5
6
7
8
9
10
int x = 5;
int y = 6;

int* p = &x;
p = &y; // 现在 p 指向了 y,p 变了

int& r = x; // r 绑定了 x
r = y; // 注意:这并不是让 r 指向 y!
// 这一步等同于 x = y;
// r 依然绑定在 x 上,只是 x 的值变成了 6
#### c、内存地址和自身实体

指针: 是一个独立的实体。指针本身在内存中占用空间(32位系统占4字节,64位系统占8字节),指针也有自己的地址。

引用: 在 C++ 语法层面,它不是一个独立的对象,它只是原对象的另一个名字。对引用取地址(&r),得到的就是原对象的地址

1
2
3
4
5
6
7
8
9
10
int a = 10;
int* p = &a;
int& r = a;

// 假设 a 的地址是 0x100,p 存储在 0x200
// std::cout << p; -> 输出 0x100 (a 的地址)
// std::cout << &p; -> 输出 0x200 (指针变量 p 自己的地址)

// std::cout << &r; -> 输出 0x100 (和 &a 一模一样)
// 语言层面上,引用没有自己的地址

d、多级间接

指针: 可以有多级。int**(指向指针的指针),int*** 等等,合法且常见。

引用: 只有一级。没有“引用的引用”(int&& 是右值引用,那是另一个概念,不是指向引用的引用)。

1
2
3
4
5
6
int a = 10;
int* p = &a;
int** pp = &p; // ✅ 指向指针的指针

// int& r = a;
// int&& rr = r; // ❌ 错误(C++不支持引用的引用)

e、sizeof的含义

指针: sizeof(p) 永远返回指针类型的大小(4 或 8 字节)。

引用: sizeof(r) 返回的是被引用对象的大小

1
2
3
4
5
6
7
double d = 3.14;
double* dp = &d;
double& dr = d;

// 假设是 64 位系统
// sizeof(dp) -> 8 (指针大小)
// sizeof(dr) -> 8 (double 变量的大小,如果 d 是 struct 此值会变)

f、自增运算

指针: p++指针算术,表示指针向后移动一个单元(指向下一个内存地址)。

引用: r++ 等同于 a++,是对原变量的值进行数值加一。

2、底层实现

虽然 C++ 标准说引用不是对象,但在编译器的汇编实现层面: 引用通常就是作为一个 const 指针(指针常量)来实现的。

当你写:

1
2
int& r = a;
r = 10;

编译器在底层生成的汇编代码,和下面这段代码几乎是一模一样的:

1
2
int* const r_ptr = &a; // 指针常量:指针本身不能变,只能一直指向 a
*r_ptr = 10; // 自动解引用

区别在于: 指针需要你手动写 *p 来解引用,而引用由编译器自动解引用

3、什么时候用哪个?

能用引用,尽量用引用;必须用指针时,才用指针。)

a、优先使用引用(Reference)

  • 函数参数传递: 为了避免拷贝大对象(如 std::vector, std::string 或自定义类),使用 const Type&

    1
    void printImage(const Image& img); // 高效且安全,像传值一样方便
  • 函数返回值: 当需要实现链式调用(如 operator<<)或修改容器内元素时。

  • 运算符重载: operator[] 等通常返回引用。

b、 必须使用指针(Pointer)(在用完之后记得释放指针,不然会内存泄露)

  • 表示“可选”的存在: 如果这个参数可以为空(nullptr),必须用指针。
  • 动态内存管理: 虽然现在推荐用智能指针(std::unique_ptr),但本质还是指针。
  • 数组操作/缓冲区: C 风格的 API 交互。
  • 需要改变指向: 比如链表(Linked List),节点需要断开连接并指向新节点,引用做不到(因为引用不能重新绑定)。
特性 指针 (Pointer) 引用 (Reference)
本质 存储地址的变量 对象的别名
符号 * &
初始化 可选 (建议初始化) 必须初始化
空值 可以是 nullptr 不可以为空
重置指向 可以随时改变指向 一旦绑定,终身不改
语法 需要 *p 解引用,-> 访问成员 直接像原对象一样使用,. 访问成员
多级 **p (多级指针) 无 (没有引用的引用)
sizeof 地址的大小 (4/8 byte) 原对象的大小
主要用途 动态内存、数据结构(链表)、可选参数 函数传参、返回值、消除拷贝