C++编译和内存管理

内存管理

ELF文件(可执行可链接文件):

是Linux等类Unix操作系统中用于表示可执行文件、共享库、目标文件和核心转储文件的标准二进制文件格式。每个 ELFELF 文件都由一个 ELF header 和紧跟其后的文件数据部分组成。

C++内存分区:

C++内存分区在程序执行时,为了更有效地管理内存资源,通常将内存划分为不同的区域。

程序存储区 全局/静态存储 栈区 堆区 常量存储区
操作系统管理,运行期间不变 执行时已分配好,运行期间不变 编译器管理,分配和释放的效率很高 程序员管理,要自己要求和释放 存放常量,运行期间不变

以下是C++内存分区的详细解释:

程序存储区(程序代码区)
  • 功能:存放函数体的二进制代码,由操作系统进行管理。
  • 特点:这部分内存主要存储了程序的指令和数据结构,它们在程序开始执行时就已经加载到内存中,并在程序运行期间保持不变
全局/静态存储区
  • 功能:存放全局变量和静态变量(包括全局静态变量和局部静态变量)。
  • 特点:
    • 在程序开始执行时,这部分内存就已经分配好了,并且它们的存储单元是固定的。
    • 变量的生命周期与程序的运行时间相同,程序结束时才会释放这部分内存。
    • 由于静态分配,其访问速度相对较快。
栈区(Stack)
  • 功能:
    • 由编译器自动分配和释放。
    • 存放函数的参数值、局部变量等。
  • 特点:
    • 栈区采用后进先出(LIFO)的管理方式。
    • 分配和释放的效率非常高,因为栈内存分配运算内置于处理器的指令集中。
    • 但栈区的空间大小是有限制的,过多的局部变量或递归调用可能导致栈溢出。
堆区(Heap)
  • 功能:
    • 由程序员分配和释放。
    • 若程序员不释放,程序结束时由操作系统回收。
  • 特点:
    • 堆区是动态内存分配的主要区域,程序员可以使用newdelete(或new[]delete[])来分配和释放内存。
    • 堆区的空间大小相对较大,但在频繁地分配和释放内存时,可能会导致内存碎片。
    • 使用堆区时要特别注意内存泄漏的问题,即分配的内存没有被正确释放。
常量存储区
  • 功能:存放常量字符串和其他常量值。
  • 特点:
    • 常量存储区中的值在程序运行期间不可修改。
    • 常量字符串通常与其他只读数据一起存放在只读存储区,以防止程序意外修改它们。
自由存储区
  • 说明:自由存储区是一个相对抽象的概念,通常指的是通过new操作符分配的内存区域。虽然从技术上来说,这部分内存也属于堆区,但C++标准库允许程序员通过重载newdelete操作符来改变自由存储区的实现。
总结

C++内存分区是程序设计和内存管理的重要基础。了解不同内存分区的功能和特点有助于程序员更合理地使用和管理内存资源,从而提高程序的性能和稳定性。在编写C++程序时,应该尽量避免内存泄漏和栈溢出等问题,以确保程序的正确性和健壮性。

堆和栈

当我们讨论操作系统和内存管理时,我们更经常指的是进程或线程的调用栈(Call Stack)或执行栈(Execution Stack),这是一个由操作系统自动管理的内存区域。

特性 描述
自动分配和释放 栈内存由系统自动分配和释放。当一个函数被调用时,它的参数和局部变量会在栈上被分配空间。当函数返回时,这些空间会被自动释放。
后进先出(LIFO) 栈遵循后进先出(Last In First Out)的原则。这意味着最后入栈的元素会最先出栈。
栈帧(Stack Frame) 每次函数调用时,都会在栈上分配一个栈帧(Stack Frame)。栈帧包含了函数的局部变量、参数、返回地址等信息。
栈溢出(Stack Overflow) 如果函数递归调用过深,或者局部变量占用了过多的栈空间,就可能导致栈溢出。栈溢出是一种严重的错误,通常会导致程序崩溃。
栈指针(Stack Pointer) 栈指针是一个指向栈顶的内存地址的指针。当数据入栈时,栈指针会向下移动(在大多数系统中,栈是向下增长的);当数据出栈时,栈指针会向上移动。
局部变量和函数参数 在C++中,局部变量(包括自动存储期对象)和函数参数通常都存储在栈上。这意味着它们的生命周期与它们所在的函数或代码块相同。当函数返回或代码块结束时,这些变量就会被销毁。
返回地址 每次函数调用时,除了局部变量和参数外,还会在栈上保存一个返回地址。这个地址是函数执行完毕后应该跳转到的位置,通常是调用该函数之后的下一条指令。

在C++中,堆(Heap)是一种重要的内存管理机制,它允许程序员在运行时动态地分配和释放内存。与栈(Stack)不同,堆内存是由程序员显式管理的,而不是由系统自动管理的。

特性 描述
动态分配 堆内存允许程序在运行时根据需要动态地分配内存。这是通过使用诸如newdelete(在C++中)或mallocfree(在C中)等操作符或函数来实现的。
显示管理 与栈内存不同,程序员必须显式地管理堆内存。这意味着程序员需要负责在不再需要内存时释放它,以避免内存泄漏。
内存碎片 由于堆内存是动态分配的,因此可能会产生内存碎片。内存碎片是当小块内存被释放并留下未使用的空间时发生的,这些空间太小而无法容纳其他请求,但又无法合并成更大的连续块。
分配和释放时间 堆内存的分配和释放通常比栈内存慢,因为堆内存管理涉及到查找可用空间、跟踪分配和释放等复杂操作。
存储打对象或者长生命周期对象 由于堆内存是动态分配的,因此它通常用于存储大对象或具有长生命周期的对象。这些对象不适合在栈上分配,因为栈的大小通常是有限的。
异常安全性 在C++中,使用new操作符分配内存时,如果内存分配失败,会抛出std::bad_alloc异常。这使得堆内存分配具有更好的异常安全性,因为它允许程序员处理分配失败的情况。
智能指针 为了简化堆内存的管理,C++11引入了智能指针(如std::unique_ptrstd::shared_ptrstd::weak_ptr)。智能指针能够自动管理堆内存的生命周期,减少内存泄漏的风险。
堆和栈的区别

简而言之,堆是程序员手动管理的动态内存区域,而栈是自动管理的静态内存区域。

堆(Heap)
  • 堆用于动态内存分配。程序员使用newmalloc等函数在堆上请求内存,并在不再需要时使用deletefree释放它。
  • 堆内存的生命周期由程序员控制。
  • 堆内存通常用于存储大型对象或对象数组。
栈(Stack)
  • 栈是自动管理的内存区域,用于存储局部变量和函数调用的信息。
  • 栈内存的生命周期与函数或代码块的执行周期相同。当函数返回时,栈上的内存会被自动释放。
  • 栈内存通常用于存储小型对象和简单数据类型。

变量定义和生存周期

变量作用域

在C++中,变量的作用域(Scope)指的是变量可以被访问的代码区域。这决定了变量的生命周期和可见性。C++中有几种不同的作用域:

作用域 性质
局部作用域(Local Scope) 局部作用域是在函数、代码块或循环内部定义的变量的作用域。这些变量在它们被声明的代码块内是可见的,一旦代码块结束,这些变量就会被销毁(对于非静态局部变量)。
全局作用域(Global Scope) 全局作用域是在所有函数外部定义的变量的作用域。这些变量在整个程序中都是可见的,从定义它们的文件开始到文件结束。如果它们被声明为extern,则可以在其他文件中通过包含适当的头文件来访问。
命名空间作用域(Namespace Scope) 命名空间作用域是在命名空间中定义的变量的作用域。命名空间用于将相关的标识符(如变量、函数、类等)组合在一起,以避免命名冲突。
类作用域(Class Scope) 类作用域是在类中定义的成员变量的作用域。这些变量是类的实例(对象)的一部分,并且只能通过类的对象或指针来访问。

在下面这段代码中,我们定义了:

  • 一个全局变量 globalVar
  • 一个命名空间 MyNamespace,其中包含一个命名空间变量 namespaceVar
  • 一个类 MyClass,其中包含一个类成员变量 classVar 和一个成员函数 printVar
  • 一个函数 foo,其中包含一个局部变量 localVar,并访问了全局变量、命名空间变量和类实例的变量。
  • main 函数中,我们访问了全局变量、命名空间变量,并创建了一个 MyClass 的实例来访问其成员变量。
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
#include <iostream>    
// 全局作用域
int globalVar = 100;
// 命名空间作用域
namespace MyNamespace {
int namespaceVar = 200;
}
// 类作用域
class MyClass {
public:
int classVar = 300; // 类成员变量(实例变量)
// 成员函数(也在类作用域内)
void printVar() {
std::cout << "classVar: " << classVar << std::endl;
}
};
// 函数作用域(包括局部作用域)
void foo() {
int localVar = 42; // 局部变量
std::cout << "localVar: " << localVar << std::endl;
std::cout << "globalVar: " << globalVar << std::endl;
std::cout << "MyNamespace::namespaceVar: " << MyNamespace::namespaceVar << std::endl;
MyClass obj; // 创建MyClass的实例
obj.printVar(); // 调用成员函数打印classVar
}
int main() {
std::cout << "globalVar: " << globalVar << std::endl;
std::cout << "MyNamespace::namespaceVar: " << MyNamespace::namespaceVar << std::endl;
foo(); // 调用foo函数,该函数将打印其局部变量和全局/命名空间变量
MyClass obj; // 在main函数中创建MyClass的实例
std::cout << "obj.classVar: " << obj.classVar << std::endl; // 直接访问obj的classVar
return 0;
}
变量生命周期

在C++中,变量的生命周期指的是变量从创建到销毁的时间段。这取决于变量在何处以及如何被声明。以下是C++中变量生命周期的几种主要情况:

变量名 描述
局部变量(Local Variables) 局部变量在函数、代码块或循环内定义。它们的生命周期从定义点开始,到包含它们的代码块执行完毕时结束。一旦变量离开其作用域(即包含它的代码块),它就会被销毁,其占用的内存会被释放。
全局变量(Global Variables) 全局变量在函数外部定义,通常在所有函数外部的文件顶部。它们的生命周期是整个程序的执行期间,从程序开始执行到程序结束。
静态局部变量(Static Local Variables) 静态局部变量在函数内部定义,但它们的生命周期与全局变量类似。它们在第一次进入包含它们的函数时被初始化,并在程序执行期间一直存在,即使函数返回也不会被销毁。在下次函数调用时,静态局部变量的值保持不变。
动态分配的内存(Dynamically Allocated Memory) 使用new操作符在堆上动态分配的内存的生命周期是由程序员控制的。当使用new分配内存时,该内存会一直存在,直到显式地使用delete操作符释放它。忘记释放内存会导致内存泄漏。
类的成员变量(Class Member Variables) 类的成员变量(包括静态和非静态)的生命周期取决于类的实例(对象)的生命周期。非静态成员变量与对象一起创建和销毁,而静态成员变量在第一次创建类的对象时初始化,并在程序执行期间一直存在,即使所有对象都被销毁。

在接下来这段代码中,我们有:

  • 一个全局变量globalVar
  • 一个类MyClass,它有一个非静态成员变量instanceVar和一个静态成员变量staticVar
  • main函数中,我们访问了全局变量并创建了一个MyClass的实例,然后通过这个实例访问了其成员变量。
  • 我们还定义了一个函数foo,其中包含了静态局部变量staticLocalVar和局部变量localVar
  • 此外,我们还在foo函数中动态分配了一个整数,并在使用后立即释放了它。
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
#include <iostream>  
// 全局变量
int globalVar = 100;
// 类的声明
class MyClass {
public:
int instanceVar; // 类的非静态成员变量(实例变量)
static int staticVar; // 类的静态成员变量
MyClass(int value) : instanceVar(value) {} // 构造函数
void printVars() {
std::cout << "Instance variable: " << instanceVar << std::endl;
std::cout << "Static variable: " << staticVar << std::endl;
}
};
// 类的静态成员变量的初始化
int MyClass::staticVar = 200;
// 函数定义
void foo() {
// 静态局部变量
static int staticLocalVar = 0;
staticLocalVar++;
std::cout << "Static local variable: " << staticLocalVar << std::endl;
// 局部变量
int localVar = 42;
std::cout << "Local variable: " << localVar << std::endl;
// 动态分配的内存
int* ptr = new int(84);
std::cout << "Dynamically allocated variable: " << *ptr << std::endl;
delete ptr; // 释放内存
}
int main() {
// 访问全局变量
std::cout << "Global variable: " << globalVar << std::endl;
// 创建MyClass的实例并访问其成员变量
MyClass obj(300);
obj.printVars();
// 调用foo函数,访问静态局部变量和局部变量
foo();
// 注意:在main函数结束时,全局变量和静态成员变量仍然存在,但局部变量(包括静态局部变量在foo函数外部)已经销毁
return 0;
}

内存对齐

在C++中,内存对齐(Memory Alignment)是计算机内存访问性能优化的一种技术。当数据在内存中按照某种特定的规则对齐时,处理器可以更高效地访问这些数据。这是因为现代处理器在访问内存时,通常会一次性读取多个字节(比如4字节、8字节等),这种读取方式被称为“字”(word)或“双字”(doubleword)访问。如果数据没有正确对齐,那么处理器可能需要执行额外的操作来访问这些数据,这会降低访问速度。

alignas关键字

在大多数情况下,最好让编译器自动处理内存对齐,你也可以使用特定的编译器指令或C++17引入的alignas关键字来指定对齐要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>  
#include <cstdint>
// 指定16字节对齐的结构体
struct alignas(16) AlignedStruct {
int32_t a;
double b;
// ... 可能还有其他成员 ...
};
int main() {
AlignedStruct s;
std::cout << "Alignment of s: " << alignof(s) << " bytes\n";
// 输出应该是 16 bytes(取决于平台和编译器)
return 0;
}

智能指针

C++中的智能指针是一种特殊的指针,它们可以自动管理动态分配的内存的生命周期,从而避免内存泄漏和其他与内存管理相关的问题。C++11标准引入了三种主要的智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr。需要注意的是,智能指针不能像普通指针那样支持加减运算。

指针 特点
unique_ptr unique_ptr 独占其指向的对象。这意味着两个 unique_ptr 不能指向同一个对象。 当 unique_ptr 被销毁(例如,离开其作用域)时,它所指向的对象也会被自动删除。 unique_ptr 提供了对原始指针的独占所有权语义。
shared_ptr shared_ptr 实现共享所有权的语义。多个 shared_ptr 可以指向同一个对象,并且当最后一个拥有该对象的 shared_ptr 被销毁时,对象才会被删除。 shared_ptr 使用引用计数来跟踪有多少 shared_ptr 指向一个对象。当计数变为0时,对象会被删除。
weak_ptr weak_ptr 是对 shared_ptr 所管理对象的一个弱引用,它不会增加对象的引用计数。 weak_ptr 主要用于解决 shared_ptr 之间的循环引用问题。 weak_ptr 可以安全地用于检查一个对象是否仍然存在,并且可以通过它获取到 shared_ptr(如果对象仍然存在的话)。##### 指针建立
指针建立
  • 优先选用 std::make_uniquestd::make_shared,而非直接 new
  • make_unique:减少代码量,能够加快编译速度,定义两遍类型时,编译器需要进行类型推导会降低编译速度,某些意外意外情况下可能会导致内存泄漏。
  • make_shared:这个主要是可以减少对堆中申请内存的次数,只需要申请一次即可,make_share 虽然效率高,但是同样不能自定义析构器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::unique_ptr<int> ptr=std::make_unique<int>(5);  
// ... 使用 ptr
// 当 ptr 离开作用域时,它指向的 int 会被自动删除
std::shared_ptr<int> ptr1 = std::make_shared<int>(5);
std::shared_ptr<int> ptr2 = ptr1; // ptr1 和 ptr2 现在共享同一个 int
// ... 使用 ptr1 和 ptr2
// 当 ptr1 和 ptr2 都离开作用域时,它们共享的 int 才会被删除
std::shared_ptr<int> shared = std::make_shared<int>(5);
std::weak_ptr<int> weak = shared;
// ...
if (auto strong = weak.lock()) {
// strong 是一个有效的 shared_ptr,我们可以使用它
// ...
} else {
// 原始对象可能已经被删除了
}
代码示例

这段代码将会展示如何使用std::unique_ptrstd::shared_ptrstd::weak_ptr来管理MyClass对象的生命周期。你可以看到,当智能指针离开其作用域时,它们会自动删除所指向的对象(除非有其他的shared_ptr仍然指向该对象)。std::weak_ptr用于避免循环引用问题,并允许你检查原始对象是否仍然存在。

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
#include <iostream>  
#include <memory>
// 一个简单的类
class MyClass {
public:
MyClass(int value) : value_(value) {
std::cout << "MyClass(" << value_ << ") created.\n";
}
~MyClass() {
std::cout << "MyClass(" << value_ << ") destroyed.\n";
}
void printValue() const {
std::cout << "Value: " << value_ << "\n";
}
private:
int value_;
};

int main() {
// 使用std::unique_ptr
{
std::unique_ptr<MyClass> uniquePtr(new MyClass(10));
uniquePtr->printValue();
// 当uniquePtr离开作用域时,它指向的对象会被自动删除
} // 这里MyClass(10)对象会被销毁

// 使用std::shared_ptr
std::shared_ptr<MyClass> sharedPtr1 = std::make_shared<MyClass>(20);
sharedPtr1->printValue();
{
std::shared_ptr<MyClass> sharedPtr2 = sharedPtr1; // sharedPtr1和sharedPtr2现在共享同一个对象
sharedPtr2->printValue();
// 当sharedPtr2离开作用域时,它指向的对象不会被删除,因为sharedPtr1仍然指向它
}

// 当sharedPtr1离开作用域时,如果这是最后一个shared_ptr指向该对象,则对象会被删除
// 这里MyClass(20)对象会被销毁

// 尝试使用std::weak_ptr
{
std::shared_ptr<MyClass> sharedPtr3 = std::make_shared<MyClass>(30);
std::weak_ptr<MyClass> weakPtr = sharedPtr3;
if (auto shared = weakPtr.lock()) { // 尝试获取shared_ptr
shared->printValue();
}
// 当sharedPtr3离开作用域时,它指向的对象会被删除
// 因为weakPtr只是一个弱引用,所以它不会影响对象的生命周期
} // 这里MyClass(30)对象会被销毁
return 0;
}

编译和链接

在C++中,编译和链接是两个不同的步骤,尽管它们通常在一个命令中一起执行。可以分为四个阶段:

  • 预处理(Preprocessing)
  • 编译(Compilation)
  • 汇编(Assembly)
  • 链接(Linking)
预处理
  • 替换所有的宏定义(#define)。
  • 插入包含的头文件内容(#include)。
  • 删除注释。
  • 添加行号和文件名信息(用于调试)。
编译
  • 检查代码中的语法错误。
  • 将代码转换为更低级别的表示形式(通常称为“汇编代码”)。
  • 简单来说编译的过程即为将 .cpp 源文件翻译成 .s 的汇编代码
汇编
  • 将编译步骤中生成的汇编代码转换为机器码,这是计算机可以直接执行的指令。
  • 将汇编代码 .s 翻译成机器指令 .o 文件,一个 .cpp 文件只会生成一个 .o 文件
链接
  • 合并程序中所有单独编译的部分(多个.o文件链接到一起)。
  • 解决程序中引用的外部函数或变量(例如库函数)的地址。
  • 生成一个可执行文件(例如.exe文件在Windows上),这个文件可以被计算机直接执行。
1
2
3
g++ -c main.cpp -o main.o  #生成main.o文件,-c表示只进行编译步骤,并生成目标代码文件
g++ -c utility.cpp -o utility.o#生成utility.o文件,如果使用外部库,要加-l +库名或-L +库路径
g++ main.o utility.o -o my_program#链接成my_program可执行文件
静态链接与动态链接

静态链接和动态链接是程序编译和链接过程中的两种不同方式,它们在程序运行时对外部库文件的依赖和处理方式上有所不同。静态链接和动态链接各有优缺点,具体选择哪种方式取决于实际需求和应用场景。

  • 如果对程序的独立性和安全性要求较高,可以选择静态链接
  • 如果对程序的体积和升级方便性要求较高,可以选择动态链接。
特性 静态链接 动态链接
定义 编译时将外部库直接嵌入到可执行文件中 编译时只生成可执行文件和必要的资源文件,运行时加载外部库
完整性 包含所有代码和数据,无需额外加载 依赖外部库文件,运行时加载
稳定性 运行时库不会被卸载或替换,稳定性高 运行时库可以被卸载或替换,可能影响稳定性
独立性 与目标程序依赖性强,修改库需重新编译程序 与目标程序依赖性弱,修改库无需重新编译程序
体积 体积较大,包含所有代码和数据 体积较小,多个程序可共享同一个库
资源占用 磁盘和内存占用较大 磁盘和内存占用较小
升级和维护 升级或维护需要替换所有相关库并重新编译程序 升级或维护库时,无需重新编译程序,只需替换库文件
兼容性 不易出现兼容性问题,因为所有代码和数据都在一起 可能出现兼容性问题,特别是当多个程序共享同一个库时
安全性 安全性较高,因为不依赖于外部文件 安全性较低,可能受到外部文件的影响
适用场景 对独立性和安全性要求较高的程序 对体积和升级方便性要求较高的程序

大端小端

字节序

字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。具体来说,它决定了在一个多字节的数据项中,字节的哪一端存储在该数据项的起始地址处。

大端(Big-Endian)

高位字节在前,低位字节在后。即数据的高位字节存放在内存的低地址处,而数据的低位字节存放在内存的高地址处。例如,一个32位整数0x12345678在大端序系统中会被存储为12 34 56 78(十六进制表示)。

小端(Little-Endian)

低位字节在前,高位字节在后。与大端序相反,数据的低位字节存放在内存的低地址处,而数据的高位字节存放在内存的高地址处。同样以32位整数0x12345678为例,在小端序系统中它会被存储为78 56 34 12

影响

在网络编程中,网络字节序通常指的是大端序(Big-Endian)。因此,当数据需要在网络上进行传输时,如果主机字节序是小端序(Little-Endian),则需要在发送前将数据从主机字节序转换为网络字节序,接收端在接收到数据后也需要进行相应的转换。

内存泄漏

内存泄漏(Memory Leak)是指程序在运行过程中,未能正确释放已经不再需要使用的内存空间,导致系统中的可用内存逐渐减少,直至耗尽所有可用内存并可能引发系统崩溃的严重问题。常见:

特点 描述
定义 程序在运行过程中未能正确释放不再使用的内存空间,导致系统可用内存减少,可能引发系统崩溃。
原因 程序员忘记释放内存、异常处理不当、循环引用、资源未关闭等。
类型 常发性、偶发性、一次性、隐式内存泄漏。
影响 系统性能下降、内存占用持续增加、可能导致系统崩溃。
解决办法 及时释放内存、正确处理异常、解除循环引用、正确关闭资源、使用检测工具、养成良好编程习惯。
重要性 内存泄漏是严重的编程错误,需要特别注意预防和修复。

内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因此,程序员需要特别注意在编写代码时避免内存泄漏的发生,并定期检查代码以发现和修复可能存在的内存泄漏问题。

#include" "与< >

以下是一个表格,展示了 #include 指令中使用尖括号 < > 和双引号 "" 的区别:

特点 尖括号 < > 双引号 ""
用途 通常用于包含编译器提供的标准库头文件 通常用于包含项目中的自定义头文件
搜索路径 编译器会在其标准库路径中查找文件 编译器首先在当前源文件所在的目录中查找文件,如果找不到,则会查找编译器设置的其他路径
处理自定义头文件 如果尝试使用尖括号来包含自定义头文件,并且该文件不在标准库路径中,编译器通常会报错 使用双引号可以包含当前目录或编译器设置的其他路径中的自定义头文件
示例 <iostream><stdio.h> "myheader.h"(假设 myheader.h 在当前目录或编译器设置的其他路径中)
注意事项 编译器可能会根据设置来改变默认行为 编译器可能会根据设置来改变默认行为,但通常优先查找当前目录