C++开发(一)

C++开发(一)

1. Visual Studio Installer

Visual Studio Installer 是一个用于安装、更新和管理 Visual Studio 及其相关组件的工具。它的主要作用包括:

  • 安装开发环境:你可以使用 Visual Studio Installer 安装不同的工作负载(如 C++ 桌面开发、.NET 开发、Web 开发等),这些工作负载包含了构建项目所需的工具和库。
  • 管理和更新工具:通过 Visual Studio Installer,你可以添加、移除或更新已安装的组件,如 MSBuild、C++ 编译器、Windows SDK 等。
  • 修复环境问题:如果开发环境出现问题,可以通过 Visual Studio Installer 修复现有的安装,确保所有组件都正常工作。

Visual Studio 是微软开发的集成开发环境(IDE),用于开发、调试和部署应用程序,提供了强大的工具集成来帮助开发者高效地编写代码、测试和发布软件。可以只使用 Visual Studio Installer 配置必要的开发环境,而不安装 Visual Studio IDE。

2. C++ 开发的基本代码结构

C++ 项目的基本代码结构通常由头文件(.h 文件)和源文件(.cpp 文件)组成,通常按照以下目录结构组织:

  • include/ 目录:存放头文件(.h),定义类、函数的接口,供其他源文件包含和使用。头文件中代码仅用于接口定义,具体的实现细节放在源文件中。每个 .cpp 文件通常包含与之对应的 .h 文件,并实现其中声明的所有类或函数。
  • src/ 目录:存放源文件(.cpp),包含函数和类的具体实现。每个.cpp文件通常包含与之对应的.h文件,并实现其中声明的所有类或函数。
  • build/ 目录:用于存放编译生成的中间文件和最终的可执行文件。CMake 通常会将生成的构建文件(如 Makefile 或 Visual Studio 解决方案)放在这个目录中。
  • CMakeLists.txt:CMake 项目的配置文件,用于定义如何构建项目、链接库和生成可执行文件。

为什么我们同时需要源文件与头文件呢?为什么在编译一个源文件时,不去其他源文件查找声明,就如后世的Java、C#一样呢?为什么编译和链接过程要分开,并让用户可知呢?

这主要是由于在上个世纪60、70年代,各个obj文件可能并不是同一种语言源文件编译得到的,他们可能来自于汇编,也可能是Fortran这样与C一样的高级语言。

1
2
3
4
5
alpha.c  -> alpha.obj
\
beta.asm -> beta.obj --> program.exe
/
gamma.f -> gamma.obj

所以编译阶段,C源文件(当然也包括其他语言的源文件)是不与其他源文件产生关系的,因为编译器(这里指的是狭义的编译器,不包括连接器)本身有可能并不能识别其他源。

说到这里,定然有人要问:连函数参数和返回值都不知道,直接链接然后调用,会不会出现问题。答案是不会,至少当时不会。因为当时的C只有一种数据类型,即“字长”。

不过,后来出现了不同的数据类型。例如出于可移植性和内存节省的考虑,出现了short intlong int;为了加强对块处理IO设备的支持,出现了char。如此就带来了一个问题,即函数的调用者不知道压栈的长度。

在这种情况下,函数调用需要提前声明,以便让调用者得知函数的参数与返回值尺寸。于是,头文件就出现了。为什么不直接用源文件呢?因为编译器的技术路径,C源文件在编译时不会与其他源文件产生关系,编译一次只处理一个源文件。

from wzsayiie.

3. CMake 的作用和使用

CMake 是一个跨平台的构建系统生成工具,它可以生成适用于不同平台的构建文件(如 Makefile、Visual Studio 解决方案等)。CMake 的主要功能和使用场景包括:

  • 跨平台构建:通过一个统一的配置文件(CMakeLists.txt),CMake 可以为不同平台生成合适的构建文件,简化了跨平台项目的开发。
  • 管理依赖:CMake 可以自动处理项目的依赖关系,确保所有文件按正确顺序编译。
  • 配置和编译流程:通过运行 cmake 命令,CMake 会读取 CMakeLists.txt 文件并生成相应的构建文件(如 Makefile),然后可以通过 make 或其他工具编译项目。
4. CMake Cache 和生成器
  • CMake Cache:CMake 在配置项目时,会生成一个 CMakeCache.txt 文件,其中缓存了项目的配置选项、生成器信息等。如果你想更改生成器或重新配置项目,通常需要删除这个缓存文件。
  • 生成器选择:CMake 支持多种生成器,如 Unix MakefilesNinjaVisual Studio 等。你可以在运行 CMake 时通过 -G 选项指定生成器。
5. Make 和 Makefile

Make 是一种构建自动化工具,使用 Makefile 来定义项目的构建规则。Makefile 中包含了如何编译和链接各个源文件的指令。通过运行 make 命令,系统会根据 Makefile 中的规则自动编译项目。

  • Makefile 的生成:在使用 CMake 时,你可以选择 Unix Makefiles 生成器,让 CMake 为你生成一个适用于 make 工具的 Makefile
  • 构建和编译:通过 make 命令,项目会按照 Makefile 中的规则被编译和链接,最终生成可执行文件。
6. 路径和编码问题

在项目开发过程中,文件路径的命名和编码可能会影响工具的正常运行:

  • 避免非 ASCII 字符:在跨平台开发中,使用纯英文字符命名文件和目录可以避免编码问题,确保所有工具都能正确解析路径。
  • 路径配置:当项目路径或环境变量中包含不兼容的字符时,可能导致构建错误。因此,建议使用简单、清晰的目录结构和文件命名。
7. MSBuild 和 VCTargetsPath
  • MSBuild:MSBuild 是用于构建 Visual Studio 项目的命令行工具,负责编译、链接和生成最终的应用程序。你可以通过命令行使用 MSBuild 编译 Visual Studio 项目,而无需打开 Visual Studio IDE。
  • VCTargetsPath:这个环境变量指向 MSBuild 所使用的配置文件的路径。如果这个路径配置错误,可能导致编译失败。
8. 头文件保护

每个头文件都应该使用包含保护(include guards)或者#pragma once来防止重复包含。

1
2
3
4
5
6
7
8
9
10
#ifndef MY_CLASS_H
#define MY_CLASS_H

class MyClass {
public:
void someMethod();
};

#endif // MY_CLASS_H

9. C++工具链

不同平台上有许多工具链可供选择,每种工具链都有其独特的编译器、链接器、库和工具集。以下是一些常见平台及其主要工具链:

  1. Windows平台
  • Microsoft Visual Studio (MSVC)
    • 编译器cl.exe
    • 链接器link.exe
    • :MSVC C++标准库、Windows SDK
    • 构建工具:MSBuild、Ninja、CMake
    • 用途:广泛用于Windows应用程序开发,包括桌面应用、游戏、驱动程序等。
  • MinGW/MinGW-w64
    • 编译器gcc.exe(GCC for Windows)
    • 链接器ld.exe
    • :GNU C++标准库、Windows API库(通过Win32 API实现)
    • 构建工具:Make、Ninja、CMake
    • 用途:提供GNU工具链的Windows移植版本,常用于跨平台开发和Unix风格的开发环境。
  • Intel oneAPI (formerly Intel Parallel Studio)
    • 编译器icx.exe(DPC++/C++ 编译器)
    • 链接器:通常使用MSVC的link.exe
    • :Intel C++标准库、Math Kernel Library (MKL)、Integrated Performance Primitives (IPP)
    • 构建工具:支持与MSVC、CMake等集成
    • 用途:优化高性能计算(HPC)、科学计算和需要极致性能的应用程序。
  1. Linux平台
  • GCC (GNU Compiler Collection)
    • 编译器gccg++
    • 链接器ld
    • :GNU C++标准库、系统库(如glibc
    • 构建工具:Make、CMake、Ninja、Autotools
    • 用途:Linux系统上最常用的工具链,广泛用于从系统级别到应用程序级别的开发。
  • Clang/LLVM
    • 编译器clangclang++
    • 链接器lld(LLVM的链接器)或系统默认的ld
    • :LLVM C++标准库(libc++)或GNU C++标准库
    • 构建工具:Make、CMake、Ninja、Autotools
    • 用途:作为GCC的替代品,提供更快的编译速度和更好的诊断信息,常用于高性能和嵌入式开发。
  • Intel oneAPI
    • 编译器icxicpx
    • 链接器:通常使用ldlld
    • :Intel Math Kernel Library (MKL)、Integrated Performance Primitives (IPP)
    • 构建工具:与CMake等工具集成
    • 用途:用于科学计算、高性能计算和数据密集型应用,优化Intel硬件性能。
  1. macOS平台
  • Apple Clang (Xcode)
    • 编译器clangclang++
    • 链接器ld
    • :Apple的C++标准库(libc++)、系统库(如Foundation
    • 构建工具:Xcode、CMake、Make
    • 用途:macOS和iOS应用开发的标准工具链,集成在Xcode IDE中。
  • GCC (via Homebrew or MacPorts)
    • 编译器gccg++
    • 链接器ld
    • :GNU C++标准库
    • 构建工具:Make、CMake
    • 用途:在macOS上使用GNU工具链进行开发,适用于需要跨平台兼容性的项目。
  • Clang/LLVM
    • 编译器clangclang++
    • 链接器lld
    • :LLVM C++标准库(libc++
    • 构建工具:Make、CMake、Ninja
    • 用途:提供高性能、跨平台的开发环境,适合macOS上的大多数开发任务。
  1. 嵌入式平台
  • ARM GCC (GNU Arm Embedded Toolchain)
    • 编译器arm-none-eabi-gcc
    • 链接器arm-none-eabi-ld
    • :Newlib、标准C++库
    • 构建工具:Make、CMake
    • 用途:嵌入式开发,特别是ARM Cortex-M系列微控制器。
  • ARM Compiler (Arm Development Studio)
    • 编译器armclang
    • 链接器armlink
    • :标准库和Cortex Microcontroller Software Interface Standard (CMSIS) 库
    • 构建工具:Make、CMake
    • 用途:用于嵌入式系统的商业编译器,特别是高性能嵌入式开发。
  1. 跨平台工具链
  • Clang/LLVM
    • Clang/LLVM是一个高度模块化和可扩展的编译器框架,支持多种目标架构(x86, ARM, MIPS等)。它广泛用于跨平台开发,并且可以在Windows、Linux、macOS等平台上使用。
  • GCC
    • GCC是GNU项目的编译器集合,支持几乎所有的操作系统和硬件架构,适合跨平台和嵌入式开发。

每个平台通常都有多个工具链可供选择,每个工具链都有其特定的用途和优势。工具链的选择通常取决于项目需求、目标硬件、操作系统以及开发者的偏好。关键是保持PATH, LIB, INCLUDE等环境变量的一致性,确保它们指向同一套工具链,以避免兼容性问题和构建错误。

10. MSVC+SDK环境变量配置

Microsoft Visual StudioWindows SDK的工具栈,通常用于开发基于Intel x86/x64架构的Windows应用程序。

  • PATH:
    • G:\VS\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin:这是Visual Studio自带的CMake工具的路径,用于生成构建系统。
    • G:\VS\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja:这是Ninja构建工具的路径,用于高效地构建CMake项目。
    • G:\VS\VC\Tools\MSVC\14.39.33519\bin\Hostx64\x64:这是MSVC(Microsoft Visual C++)编译器的路径,cl.exelink.exe等工具位于此处。
    • E:\Windows Kits\10\bin\10.0.22621.0\x64:这是Windows SDK中的工具路径,包含rc.exemt.exe等工具。
  • LIB:
    • E:\Windows Kits\10\Lib\10.0.22621.0\um\x64:Windows SDK中用户模式库文件的路径,包括kernel32.libuser32.lib等基本系统库。
    • G:\VS\VC\Tools\MSVC\14.39.33519\lib\x64:MSVC编译器自带的库路径,包含C++标准库等其他依赖。
    • E:\Windows Kits\10\Lib\10.0.22621.0\ucrt\x64:Windows通用C运行时库(UCRT)的路径,这是现代Windows应用程序通常需要的库。
  • INCLUDE:
    • G:\VS\VC\Tools\MSVC\14.39.33519\include:C++标准库头文件。
    • E:\Windows Kits\10\Include\10.0.22621.0\ucrt;E:\Windows Kits\10\Include\10.0.22621.0\um;E:\Windows Kits\10\Include\10.0.22621.0\shared:Windows SDK头文件。
11. cmake与ninja

太好了!恭喜你成功解决了问题!以下是对你的问题的总结和一些解释:

  1. 问题背景
    • 你在使用CMake生成构建文件并通过Ninja构建项目时,遇到了编译器无法找到标准C++库头文件的错误(如 cstddef, iostream, cstdio 等)。
    • 该问题的根源在于你的 INCLUDE 环境变量未配置,导致MSVC编译器找不到所需的头文件。
  2. 解决方法
    • 通过配置 INCLUDE 环境变量,将MSVC和Windows SDK的头文件路径添加进去,确保编译器能够找到所需的标准库头文件。
    • 成功配置后,重新运行Ninja,编译和构建过程顺利完成。
  3. 关键路径配置
    • INCLUDE 环境变量
      • 添加MSVC和Windows SDK的头文件路径。
      • 这些路径通常包括C++标准库和Windows API库的头文件目录。
    • LIB 环境变量
      • 添加MSVC和Windows SDK的库文件路径。
      • 这些路径包括kernel32.lib等系统库和其他链接所需的库文件。
    • PATH 环境变量
      • 添加编译器、链接器、CMake、Ninja等工具的可执行文件路径。
  4. 为什么CMake能正常运行,而Ninja报错?
    • CMake阶段
      • CMake在配置阶段主要生成构建系统(如Ninja文件或Makefile),它不会立即编译代码。CMake本身在配置过程中不需要访问所有的头文件和库文件,只需确认编译器和工具链的基本信息。因此,即使 INCLUDE 没有正确配置,CMake依然可以成功运行并生成构建文件。
    • Ninja构建阶段
      • 当Ninja执行构建任务时,编译器(cl.exe)需要实际编译代码,这时编译器需要找到所有的头文件和库文件。如果 INCLUDE 环境变量未正确配置,编译器就找不到标准库头文件,从而导致编译失败。
  5. 保持工具链一致性的重要性
    • 同一套工具链
      • 在C++开发中,确保 PATHLIBINCLUDE 等环境变量指向同一套工具链是非常重要的。工具链包括编译器、链接器、库文件和头文件等。
      • 如果你混合使用来自不同工具链的编译器和库文件,可能会导致兼容性问题、编译错误或运行时错误。例如,MSVC的编译器生成的代码可能不兼容MinGW提供的库,反之亦然。
    • 推荐做法
      • 当你使用MSVC工具链时,确保所有相关路径(PATH, LIB, INCLUDE)都指向Visual Studio和Windows SDK提供的目录。
      • 如果使用其他工具链(如MinGW或Intel编译器),也应确保相应的环境变量配置与该工具链一致。
12. 软件设计的核心想法

在程序设计中,我们不应该关注具体的对象,而是关注他们之间抽象的关系。把真正依赖的地方,抽象出来最小的点。这样的话,并不是真正和某些类相耦合,而是和接口相耦合,具体的类只不过是实现了这些接口而已,他们真正是什么我们根本不关心。一切设计模式,是在特定场景下应用设计原则得到的解。在面对一个实际系统进行建模设计时,关注抽象之间的依赖关系,而非具体对象之间的依赖,是实现高效、灵活、可扩展系统的关键。

13. .lib静态链接库

.lib 文件是库文件,包含了已经编译好的C++代码。

静态库(Static Library)

  • 一个静态库(例如 gtest.lib)包含了一组已编译好的目标文件的集合。每个目标文件包含了由源代码编译成的机器代码。
  • 在链接阶段,链接器会从静态库中提取需要的机器代码片段,并将它们与应用程序的其他部分链接在一起,生成一个自包含的可执行文件。
  • 这样,静态库中的机器代码被“复制”到可执行文件中,实际上是通过将机器代码整合到最终的二进制文件中。

动态库(Dynamic Library)

  • 一个动态库(例如 gtest.dlllibgtest.so)包含了在运行时加载的机器代码。动态库的机器代码不会在链接阶段被直接嵌入到可执行文件中。
  • 相反,链接器会生成一个指向动态库的引用,并在可执行文件运行时加载这些库。
  • 在这种情况下,库的代码不会被嵌入到可执行文件中,而是由操作系统在运行时动态链接和加载。

静态链接

  • 优点:生成的可执行文件是自包含的,不需要依赖外部库,适合不易改变的环境。
  • 缺点:可执行文件体积较大,每次库更新都需要重新链接生成可执行文件。

动态链接

  • 优点:减少了可执行文件的大小,可以共享库文件,适合频繁更新的环境。
  • 缺点:需要在运行时加载库文件,可能遇到版本不匹配等问题。

所以我们在使用GoogleTest测试框架时,我们需要两个东西:

  1. 编译出来的.lib文件,他们就是实现功能编译好的C++代码。
  2. 包含接口声明的头文件,确保编译器知道怎么编译我们自己的代码。

因此,我们也需要配置好这两个东西的环境变量,大体上有两种思路。一种是在系统中创建一个全局的第三方库目录,并将其添加到系统的环境变量中(如 PATHLIBINCLUDE)。另一种是在CMake中显示指定这些目录,让CMake手动去添加这些第三方库。

14. 运行时库

在 C++ 中,Runtime Library(运行时库) 是一组支持程序在运行时执行的库函数。这些库函数提供了一些基本功能,如内存分配、输入/输出处理、异常处理、调试支持等。

Runtime Library 的种类

在 Visual C++ 中,主要有以下几种运行时库:

  1. /MD (Multi-threaded DLL)
    • 用途:链接到多线程的动态运行时库。
    • 特性:程序依赖于共享的运行时库(MSVCRT.dll 或 MSVCPRT.dll 等)。编译出的二进制文件更小,因为库中的代码在多个程序之间共享。
    • 使用场景:适用于大多数 Windows 应用程序,特别是当你希望减少最终应用程序的大小时。动态链接允许多个程序共享同一个库实例。
  2. /MT (Multi-threaded)
    • 用途:链接到多线程的静态运行时库。
    • 特性:程序独立于运行时库,所有库代码在编译时嵌入到二进制文件中。生成的二进制文件较大,但不需要依赖外部的 DLL。
    • 使用场景:适用于希望生成独立可执行文件的应用程序,避免了外部 DLL 版本冲突的问题。
  3. /MDd (Multi-threaded DLL, Debug)
    • 用途:链接到多线程的动态调试运行时库。
    • 特性:类似于 /MD,但为调试版本提供额外的调试信息(如内存泄漏检测)。依赖调试版的 MSVCRTD.dll 和 MSVCPRTD.dll。
    • 使用场景:用于调试模式下的开发,以帮助发现和修复内存相关的问题。
  4. /MTd (Multi-threaded, Debug)
    • 用途:链接到多线程的静态调试运行时库。
    • 特性:类似于 /MT,但为调试版本提供额外的调试信息。库代码嵌入到二进制文件中,适用于不依赖外部 DLL 的调试。
    • 使用场景:用于调试模式下的独立可执行文件。

为什么选择一致的 Runtime Library 很重要?

  • 冲突问题:不同的模块或库使用不同的运行时库会导致链接时的符号冲突或运行时行为不一致。例如,一个模块使用动态链接库(DLL),另一个模块使用静态库(LIB),就会在链接时引发 _ITERATOR_DEBUG_LEVELRuntimeLibrary 不匹配等错误。

  • 调试和发布:在调试和发布模式之间,选择正确的运行时库非常重要。在调试模式下使用 /MDd/MTd 可以提供额外的调试信息,而在发布模式下使用 /MD/MT 可以优化性能和减少最终二进制文件的大小。

  • 依赖管理:动态运行时库(如 /MD/MDd)会让程序依赖外部 DLL,这意味着你需要确保这些 DLL 在目标系统上可用。这在部署时可能会带来额外的复杂性。而静态运行时库(如 /MT/MTd)则避免了这个问题。

实践中的选择

在项目开发中,通常会根据需求和部署环境来选择合适的运行时库: - 开发调试:使用 /MDd/MTd 进行调试,以便捕获更多的调试信息。 - 最终发布:选择 /MD/MT 生成发布版本,视具体需求决定是否使用静态链接。

使用一致的运行时库设置有助于避免链接错误,确保项目各个部分在编译和运行时能无缝协作。所以在项目以及GoogleTest的编译过程中,在Cmake文件中都使用同样的选项

1
set(CMAKE_CXX_FLAGS_DEBUG "/MTd")
15. 运行时

runtime,泛指那些【供代码运行所需的最基础的软件】。怎样理解runtime library呢?要知道C、C++和Rust这类【系统级语言】相比于JavaScript这类【应用级语言】最大的特点之一,就在于它们可以胜任嵌入式裸机、操作系统驱动等贴近硬件性质的开发。虽然C等的if、for和函数等语言特性都可以很朴素且优雅地映射到汇编,但必然会有些没法直接映射到系统调用和汇编指令的常用功能。对于这些工作,需要由运行时库来实现。

img

注意,运行时库并不只是标准库。就算不显式include任何标准库,也有一些额外代码会被编译器插入到最后的可执行文件里。比如main函数,它在真正执行时需要大量运行时库的辅助。

img

from doodlewind

所以整个C++项目的构建流程变得清晰了,实际上是把各种组件组合(链接)到一起,形成最后的可执行文件。那么拼图有哪几部分呢?一部分是第三方库,他们可能是静态的,可能是动态的,提前准备好,即插即用。一部分是自己的源文件,我们要把他们编译成目标文件。另一部分是运行时库,这部分在编译时隐式插入到目标文件中,我们要确保源文件和第三方库用到的运行时库是一样的。

所以C++的构建过程可以看作一个拼图,各个部分拼接在一起形成最终的可执行文件。

  1. 第三方库

    • 静态库(.lib/.a):静态库在链接时将其代码嵌入到最终的可执行文件中。静态库的优点是可以让你的程序独立于外部依赖,缺点是可能会导致较大的可执行文件。
    • 动态库(.dll/.so):动态库在运行时由操作系统加载,多个程序可以共享同一个动态库。使用动态库可以减小可执行文件的大小,并简化更新,但需要在运行时确保动态库可用。
  2. 自己的源文件

    • 源代码(.cpp/.h):这是你编写的代码部分。在构建过程中,编译器会将源代码编译成目标文件(.obj/.o)。
    • 目标文件(.obj/.o):编译器将每个源文件单独编译成目标文件,目标文件包含了已翻译成机器码的代码,但还没有解决外部引用(如调用其他目标文件中的函数)。
  3. 运行时库

    • 静态运行时库:在链接阶段,静态运行时库会被嵌入到可执行文件中,最终程序不依赖外部库。
    • 动态运行时库:动态运行时库需要在运行时加载,程序需要依赖这些外部的 DLL 文件。
  4. 链接过程

    • 链接器:链接器将所有目标文件、第三方库和运行时库组合在一起,解决符号引用,生成最终的可执行文件。
    • 链接的两种方式
      • 静态链接:所有必要的代码都包含在最终的可执行文件中,生成的文件可以独立运行。
      • 动态链接:最终生成的可执行文件在运行时依赖外部动态库。
  5. 确保一致性

    • 一致的运行时库:确保源文件、第三方库和运行时库使用相同的运行时设置(如都使用 /MD/MT),避免不匹配引起的链接错误。
    • 一致的编译设置:在团队协作时,所有开发者应使用相同的编译器选项和工具链,以确保生成的一致性。

通过这些步骤,所有的部分最终会组合在一起,形成完整的、可执行的程序。这就像拼图一样,所有的部分都需要正确组合,才能生成最终的完整图像。