C++学习笔记-预编译头文件

预编译头文件(Precompiled Headers, PCH)是一种编译优化技术,通过预先编译常用的头文件来显著减少编译时间。特别适用于包含大量标准库头文件或第三方库头文件的项目。

预编译头文件的概念

预编译头文件是将经常使用且很少改变的头文件预先编译成二进制格式,在后续编译中直接使用这些预编译的结果,从而避免重复编译相同的代码。

优势

  • 显著减少编译时间:避免重复编译相同的头文件
  • 提高开发效率:特别是在大型项目中效果明显
  • 减少内存使用:编译器可以重用预编译的符号表

适用场景

  • 包含大量标准库头文件的项目
  • 使用大型第三方库(如Boost、Qt等)
  • 头文件很少改变的稳定代码库

Visual Studio中的预编译头文件

1. 创建预编译头文件

pch.h (预编译头文件)

 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
#pragma once

// 标准库头文件
#include <iostream>
#include <vector>
#include <string>
#include <memory>
#include <algorithm>
#include <map>
#include <unordered_map>
#include <set>
#include <queue>
#include <stack>
#include <fstream>
#include <sstream>
#include <chrono>
#include <thread>
#include <mutex>
#include <future>
#include <random>

// C标准库
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <cassert>

// Windows特定头文件(如果需要)
#ifdef _WIN32
#include <windows.h>
#endif

// 常用的第三方库头文件
// #include <boost/algorithm/string.hpp>
// #include <nlohmann/json.hpp>

// 项目中稳定的头文件
// #include "CommonTypes.h"
// #include "Utilities.h"

pch.cpp (预编译头文件的实现)

1
2
3
4
#include "pch.h"

// 这个文件通常是空的,只是为了生成预编译头文件
// 在Visual Studio中,这个文件需要设置为"创建预编译头文件"

2. 项目配置

在Visual Studio中配置预编译头文件:

  1. 项目属性C/C++预编译头
  2. 设置 预编译头使用(/Yu)
  3. 设置 预编译头文件pch.h
  4. 对于 pch.cpp 文件,单独设置为 创建(/Yc)

GCC/Clang中的预编译头文件

创建预编译头文件

1
2
3
4
5
# 使用GCC创建预编译头文件
g++ -x c++-header pch.h -o pch.h.gch

# 使用Clang创建预编译头文件
clang++ -x c++-header pch.h -o pch.h.pch

使用预编译头文件

1
2
3
4
5
# GCC会自动使用.gch文件(如果存在)
g++ -include pch.h main.cpp -o main

# Clang需要明确指定
clang++ -include-pch pch.h.pch main.cpp -o main

CMake中的预编译头文件支持

CMakeLists.txt

 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
cmake_minimum_required(VERSION 3.16)
project(PCHExample)

set(CMAKE_CXX_STANDARD 17)

# 创建可执行文件
add_executable(PCHExample
    src/main.cpp
    src/math_utils.cpp
    src/string_utils.cpp
)

# 设置预编译头文件(CMake 3.16+)
target_precompile_headers(PCHExample PRIVATE
    <iostream>
    <vector>
    <string>
    <memory>
    <algorithm>
    <map>
    <chrono>
)

# 或者使用头文件
# target_precompile_headers(PCHExample PRIVATE "pch.h")

实际示例项目

项目结构

PCHExample/
├── include/
│   ├── pch.h
│   ├── math_utils.h
│   └── string_utils.h
├── src/
│   ├── pch.cpp
│   ├── main.cpp
│   ├── math_utils.cpp
│   └── string_utils.cpp
└── CMakeLists.txt

头文件示例

include/math_utils.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#pragma once
#include "pch.h"

class MathUtils
{
public:
    static std::vector<int> GenerateRandomNumbers(int count, int min = 0, int max = 100);
    static double CalculateAverage(const std::vector<int>& numbers);
    static std::pair<int, int> FindMinMax(const std::vector<int>& numbers);
    static std::vector<int> SortNumbers(std::vector<int> numbers);
};

include/string_utils.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#pragma once
#include "pch.h"

class StringUtils
{
public:
    static std::string ToUpper(const std::string& str);
    static std::string ToLower(const std::string& str);
    static std::vector<std::string> Split(const std::string& str, char delimiter);
    static std::string Join(const std::vector<std::string>& strings, const std::string& delimiter);
    static bool StartsWith(const std::string& str, const std::string& prefix);
    static bool EndsWith(const std::string& str, const std::string& suffix);
};

实现文件示例

src/math_utils.cpp

 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
#include "pch.h"
#include "math_utils.h"

std::vector<int> MathUtils::GenerateRandomNumbers(int count, int min, int max)
{
    std::vector<int> numbers;
    numbers.reserve(count);
    
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(min, max);
    
    for (int i = 0; i < count; ++i)
    {
        numbers.push_back(dis(gen));
    }
    
    return numbers;
}

double MathUtils::CalculateAverage(const std::vector<int>& numbers)
{
    if (numbers.empty())
        return 0.0;
    
    long long sum = std::accumulate(numbers.begin(), numbers.end(), 0LL);
    return static_cast<double>(sum) / numbers.size();
}

std::pair<int, int> MathUtils::FindMinMax(const std::vector<int>& numbers)
{
    if (numbers.empty())
        return {0, 0};
    
    auto minmax = std::minmax_element(numbers.begin(), numbers.end());
    return {*minmax.first, *minmax.second};
}

std::vector<int> MathUtils::SortNumbers(std::vector<int> numbers)
{
    std::sort(numbers.begin(), numbers.end());
    return numbers;
}

src/string_utils.cpp

 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
#include "pch.h"
#include "string_utils.h"

std::string StringUtils::ToUpper(const std::string& str)
{
    std::string result = str;
    std::transform(result.begin(), result.end(), result.begin(), ::toupper);
    return result;
}

std::string StringUtils::ToLower(const std::string& str)
{
    std::string result = str;
    std::transform(result.begin(), result.end(), result.begin(), ::tolower);
    return result;
}

std::vector<std::string> StringUtils::Split(const std::string& str, char delimiter)
{
    std::vector<std::string> tokens;
    std::stringstream ss(str);
    std::string token;
    
    while (std::getline(ss, token, delimiter))
    {
        tokens.push_back(token);
    }
    
    return tokens;
}

std::string StringUtils::Join(const std::vector<std::string>& strings, const std::string& delimiter)
{
    if (strings.empty())
        return "";
    
    std::ostringstream oss;
    oss << strings[0];
    
    for (size_t i = 1; i < strings.size(); ++i)
    {
        oss << delimiter << strings[i];
    }
    
    return oss.str();
}

bool StringUtils::StartsWith(const std::string& str, const std::string& prefix)
{
    return str.length() >= prefix.length() && 
           str.substr(0, prefix.length()) == prefix;
}

bool StringUtils::EndsWith(const std::string& str, const std::string& suffix)
{
    return str.length() >= suffix.length() && 
           str.substr(str.length() - suffix.length()) == suffix;
}

src/main.cpp

 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 "pch.h"
#include "math_utils.h"
#include "string_utils.h"

void DemoMathUtils()
{
    std::cout << "=== Math Utils Demo ===" << std::endl;
    
    auto numbers = MathUtils::GenerateRandomNumbers(10, 1, 100);
    
    std::cout << "Generated numbers: ";
    for (int num : numbers)
    {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    
    double average = MathUtils::CalculateAverage(numbers);
    std::cout << "Average: " << average << std::endl;
    
    auto minmax = MathUtils::FindMinMax(numbers);
    std::cout << "Min: " << minmax.first << ", Max: " << minmax.second << std::endl;
    
    auto sorted = MathUtils::SortNumbers(numbers);
    std::cout << "Sorted: ";
    for (int num : sorted)
    {
        std::cout << num << " ";
    }
    std::cout << std::endl;
}

void DemoStringUtils()
{
    std::cout << "\n=== String Utils Demo ===" << std::endl;
    
    std::string text = "Hello, World! This is a Test.";
    std::cout << "Original: " << text << std::endl;
    std::cout << "Upper: " << StringUtils::ToUpper(text) << std::endl;
    std::cout << "Lower: " << StringUtils::ToLower(text) << std::endl;
    
    auto words = StringUtils::Split(text, ' ');
    std::cout << "Words: ";
    for (const auto& word : words)
    {
        std::cout << "[" << word << "] ";
    }
    std::cout << std::endl;
    
    std::string joined = StringUtils::Join(words, " | ");
    std::cout << "Joined: " << joined << std::endl;
    
    std::cout << "Starts with 'Hello': " << StringUtils::StartsWith(text, "Hello") << std::endl;
    std::cout << "Ends with 'Test.': " << StringUtils::EndsWith(text, "Test.") << std::endl;
}

void PerformanceTest()
{
    std::cout << "\n=== Performance Test ===" << std::endl;
    
    auto start = std::chrono::high_resolution_clock::now();
    
    // 执行一些计算密集的操作
    for (int i = 0; i < 1000; ++i)
    {
        auto numbers = MathUtils::GenerateRandomNumbers(1000, 1, 10000);
        auto sorted = MathUtils::SortNumbers(numbers);
        auto average = MathUtils::CalculateAverage(sorted);
        
        std::string text = "Performance test iteration " + std::to_string(i);
        auto upper = StringUtils::ToUpper(text);
        auto words = StringUtils::Split(upper, ' ');
        auto joined = StringUtils::Join(words, "-");
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    
    std::cout << "Performance test completed in " << duration.count() << " ms" << std::endl;
}

int main()
{
    std::cout << "Precompiled Headers Example" << std::endl;
    std::cout << "============================" << std::endl;
    
    DemoMathUtils();
    DemoStringUtils();
    PerformanceTest();
    
    return 0;
}

编译时间对比

不使用预编译头文件

1
2
# 编译时间测试
time g++ -std=c++17 -I include src/*.cpp -o without_pch

使用预编译头文件

1
2
3
4
5
# 创建预编译头文件
g++ -std=c++17 -x c++-header include/pch.h -o include/pch.h.gch

# 编译项目
time g++ -std=c++17 -I include -include pch.h src/*.cpp -o with_pch

预编译头文件的最佳实践

1. 包含策略

1
2
3
4
5
6
7
// 好的做法:在pch.h中包含稳定的头文件
#include <iostream>     // 标准库,很少改变
#include <vector>       // 标准库,很少改变
#include <algorithm>    // 标准库,很少改变

// 避免包含:
// #include "my_header.h"  // 经常改变的项目头文件

2. 条件编译

 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
// pch.h
#pragma once

// 根据平台包含不同的头文件
#ifdef _WIN32
    #include <windows.h>
    #include <tchar.h>
#elif defined(__linux__)
    #include <unistd.h>
    #include <sys/types.h>
#elif defined(__APPLE__)
    #include <CoreFoundation/CoreFoundation.h>
#endif

// 根据构建配置包含不同的头文件
#ifdef DEBUG
    #include <cassert>
    #include <iostream>
#endif

// 根据功能需求包含头文件
#ifdef USE_NETWORKING
    #include <curl/curl.h>
#endif

#ifdef USE_DATABASE
    #include <sqlite3.h>
#endif

3. 项目组织

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 将预编译头文件分层
// pch_core.h - 核心系统头文件
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <memory>

// pch_math.h - 数学相关头文件
#pragma once
#include "pch_core.h"
#include <cmath>
#include <random>
#include <numeric>

// pch_graphics.h - 图形相关头文件
#pragma once
#include "pch_core.h"
// #include <OpenGL/gl.h>
// #include <GLFW/glfw3.h>

注意事项和限制

1. 依赖管理

  • 预编译头文件必须是第一个包含的头文件
  • 所有源文件必须包含相同的预编译头文件
  • 修改预编译头文件会导致整个项目重新编译

2. 调试影响

  • 可能影响调试信息的准确性
  • 某些调试器可能无法正确显示预编译头文件中的符号

3. 编译器兼容性

  • 不同编译器的预编译头文件格式不兼容
  • 编译器版本升级可能需要重新生成预编译头文件

总结

  1. 预编译头文件优势:显著减少编译时间,特别是在大型项目中
  2. 适用场景
    • 包含大量标准库头文件的项目
    • 使用大型第三方库的项目
    • 头文件稳定的代码库
  3. 最佳实践
    • 只包含稳定、很少改变的头文件
    • 避免包含经常修改的项目头文件
    • 合理组织预编译头文件的层次结构
  4. 注意事项
    • 必须是第一个包含的头文件
    • 修改预编译头文件会导致全量重编译
    • 不同编译器格式不兼容
  5. 现代工具支持
    • Visual Studio原生支持
    • CMake 3.16+提供跨平台支持
    • GCC和Clang都支持预编译头文件
updatedupdated2025-09-202025-09-20