对于c++学习,在GitHub上有这几个开源的项目值得推荐:
- GitHub - DoctorWkt/acwj: A Compiler Writing Journey c语言编译器实战
- GitHub - EZLippi/Tinyhttpd: Tinyhttpd 是J. David Blackstone在1999年写的一个不到 500 行的超轻量型 Http Server,用来学习非常不错,可以帮助我们真正理解服务器程序的本质。官网:http://tinyhttpd.sourceforge.net c语言入门学习
- GitHub - qinguoyi/TinyWebServer: :fire: Linux下C++轻量级WebServer服务器 500行构建超轻量web server
- GitHub - Light-City/CPlusPlusThings: C++那些事 c++学习入门到深入
- GitHub - TheAlgorithms/C-Plus-Plus: Collection of various algorithms in mathematics, machine learning, computer science and physics implemented in C++ for educational purposes. c++算法学习
对于c++程序中经常出现的越界或者内存溢出等问题,这里推荐ASAN进行扫描.
对于c++一般项目而言,整个项目结构如下,
project/
├── include/
│ ├── MyClass.h
│ └── Utils.h
├── src/
│ ├── MyClass.cpp
│ └── Utils.cpp
└── main.cpp
其中需要知道的几点如下:
(1)头文件
- #include 主要用于系统头文件和标准库头文件。
- #include "filename" 主要用于用户定义的头文件,并且优先在当前目录中搜索。
(2)项目细节
将头文件放在 include 文件夹内,对应的实现文件(.cpp 文件)放在 src 文件夹内是一种常见的项目组织方式。头文件和实现文件的名称不一定需要完全对应,但为了方便管理和维护,通常会遵循这种对应的命名约定。
头文件 MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_H
class MyClass {
public:
void doSomething();
};
#endif // MYCLASS_H
实现文件 MyClass.cpp
#include "MyClass.h"
#include <iostream>
void MyClass::doSomething() {
std::cout << "Doing something!" << std::endl;
}
头文件 Utils.h
#ifndef UTILS_H
#define UTILS_H
namespace Utils {
void helperFunction();
}
#endif // UTILS_H
实现文件 Utils.cpp
#include "Utils.h"
#include <iostream>
void Utils::helperFunction() {
std::cout << "Helper function!" << std::endl;
}
主函数 main.cpp
#include "MyClass.h"
#include "Utils.h"
int main() {
MyClass obj;
obj.doSomething();
Utils::helperFunction();
return 0;
}
将文件链接起来
》为了编译和链接这些文件,你需要一个编译器和构建工具。这里以 g++ 为例:
g++ -Iinclude -o main src/MyClass.cpp src/Utils.cpp main.cpp
- -Iinclude:告诉编译器头文件所在的目录。
- -o main:指定输出可执行文件的名称。
- src/MyClass.cpp src/Utils.cpp main.cpp:列出所有源文件。
》使用 Makefile
为了简化编译过程,可以使用 Makefile:
# Makefile
CXX = g++
CXXFLAGS = -Iinclude -std=c++11
SRC = src/MyClass.cpp src/Utils.cpp main.cpp
OBJ = $(SRC:.cpp=.o)
TARGET = main
all: $(TARGET)
$(TARGET): $(OBJ)
$(CXX) -o $@ $^
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -f $(OBJ) $(TARGET)
运行 make 命令即可编译整个项目。
》使用CMakeLists.txt
简单使用示例
cmake_minimum_required(VERSION 3.10)
project(MyProject)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# 包含头文件目录
include_directories(include)
# 添加源文件
set(SOURCES
src/MyClass.cpp
src/Utils.cpp
main.cpp
)
# 生成可执行文件
add_executable(MyProject ${SOURCES})
》如果你的项目更复杂,可以扩展 CMakeLists.txt,如添加子目录或设置更详细的编译选项。
cmake_minimum_required(VERSION 3.10)
project(MyProject)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# 包含头文件目录
include_directories(include)
# 添加子目录
add_subdirectory(src)
# 生成可执行文件
add_executable(MyProject main.cpp)
target_link_libraries(MyProject MyLib)
同时增加src/CMakeLists.txt
# 添加源文件
set(SOURCES
MyClass.cpp
Utils.cpp
)
# 创建静态库
add_library(MyLib STATIC ${SOURCES})
新建build文件夹,然后运行cmake和make指令即可完成编译。其中在 src 目录中添加 CMakeLists.txt 文件,可以为项目带来以下好处:
- 模块化构建:将构建配置分成多个小的、独立的部分,使项目结构更加清晰和可维护。每个子目录可以包含自己的 CMakeLists.txt,这使得管理大项目变得更容易。
- 重用代码:通过在子目录中创建库,可以在多个可执行文件或项目中重用代码。这样做避免了代码重复,并且使得库的更新和维护更加集中化。
- 分离关注点:可以将库的构建与可执行文件的构建分离开来,使得各部分的构建规则独立,从而简化了复杂项目的管理。
这种方式在大型项目中尤为重要,因为它有助于组织代码、减少复杂性并提高可维护性。
这个项目结构和编译方法可以帮助你有效地组织和管理代码,并确保所有头文件和源文件正确链接在一起。
(3)编译
对于一个完整的c++项目,尤其是一个较为复杂的大型项目,这里一般推荐基于cmakelists.txt进行编译,这时候需要对cmakelists.txt的一些基本语法有一定了解。在 `CMakeLists.txt` 文件中,常见的命令用于定义项目的构建规则和配置。以下是常用的命令简要说明和使用方式:
1. file(GLOB_RECURSE ...):
- 用途:递归地收集指定模式的文件。
- 使用方式:通常用于收集源文件和头文件,例如: 下面使用 GLOB_RECURSE
子命令来收集匹配指定模式的文件,并将它们存储在一个名为 SOURCES
的变量中。这里的 SOURCES
是一个列表变量,用于存储所有找到的文件的路径。
file(GLOB_RECURSE SOURCES "src/*.cpp" "include/*.h")
收集到的 SOURCES
变量之后可以用于:
- 创建可执行文件或库:使用
add_executable
或add_library
命令,并传递SOURCES
变量作为源文件列表。 - 指定为其他命令的参数:例如,您可以将
SOURCES
变量作为参数传递给target_sources
命令,以向目标添加源文件。
2. set(...):
- 用途:用于创建新变量或修改已存在的变量。
- 使用方式:可以设置项目特定的变量或CMake的变量,后面如果想要取该变量只需要${变量名}即可,例如:
# 设置一个简单变量
set(EXAMPLE_VAR "This is a variable")
# 设置具有多个值的变量
set(MULTIVALUES_VAR value1 value2 value3)
# 设置缓存变量
set(CACHE_VAR "This is a cache variable" CACHE STRING "Description of CACHE_VAR")
# 条件设置变量
if(MY_CONDITION)
set(CONDITION_VAR "Value if condition is true")
endif()
# 向列表添加元素
set(LIST_VAR value1 value2)
list(APPEND LIST_VAR value3)
3. find_package(...):
- 用途:查找并加载外部项目的配置文件,取它们配置信息以便在项目中使用,这些项目可能是第三方库。
- 使用方式:指定要查找的包和版本,
-基本格式: find_package(<PackageName> [version] [EXACT] [QUIET] [REQUIRED])
其中,
<PackageName>
是要查找的包的名称,例如Eigen3
。version
可以指定需要的包的版本号,如果没有则基于本机安装版本操作。EXACT
如果指定,表示只接受精确的版本号。QUIET
如果指定,查找过程中不会输出任何消息。REQUIRED
如果指定,如果找不到包,CMake 将报错并停止构建过程。
例如:
find_package(Eigen3 3.3 REQUIRED)
if(EIGEN3_FOUND)
include_directories(${EIGEN3_INCLUDE_DIRS})
target_link_libraries(my_target Eigen3::Eigen)
endif()
成功找到包后,配置文件会设置一系列的变量,提供关于包的信息,例如包含目录、库文件、编译定义等。如果包提供了导入的目标(Imported Targets),可以直接使用 target_link_libraries
命令链接这些目标,而不需要显式指定库文件路径。配置文件会设置如 EIGEN3_INCLUDE_DIRS
这样的变量,包含包的头文件路径,这些路径可以传递给 include_directories
或 target_include_directories
。
在使用 `find_package(Eigen3 REQUIRED)` 命令后,除了 `EIGEN3_INCLUDE_DIRS` 之外,可能还会定义其他与 Eigen3 相关的变量,这些变量通常取决于 Eigen3 库的 CMake 配置文件。以下是一些常见的变量,它们可能在加载 Eigen3 配置后被设置:
》EIGEN3_FOUND:
- 一个布尔变量,如果找到 Eigen3 库,则设置为 `TRUE`。
》EIGEN3_VERSION 或 EIGEN3_VERSION_STRING:
- 包含找到的 Eigen3 库的版本号。
》EIGEN3_DEFINITIONS:
- 包含编译时需要定义的宏,例如条件编译标志。
》EIGEN3_LIBRARIES:
- 如果 Eigen3 需要链接到特定的库,这个变量可能会被设置。
》EIGEN3_INCLUDE_DIR 或 EIGEN3_INCLUDE_DIRS:
- 包含头文件的路径,类似于 `EIGEN3_INCLUDE_DIRS`,但可能是一个单一的路径或路径列表。
》EIGEN3_ROOT_DIR:
- Eigen3 库安装的根目录。
》EIGEN3_TARGETS 或 EIGEN3_TARGET:
- 如果 Eigen3 使用 CMake 的 `install(EXPORT)` 命令安装,可能会导出目标,这些目标可以被导入到其他项目中。
》EIGEN3_USE_FILE:
- 一个路径,指向一个 CMake 脚本文件,该文件包含了使用 Eigen3 所需的所有变量和函数。
请注意,这些变量的确切名称和存在性取决于 Eigen3 的安装和配置方式。一些变量可能在某些安装中不存在,或者可能有不同的名称。
#要确定哪些变量可用,您可以在 CMake 命令行中使用 `-c` 选项查看变量
cmake -LAH | grep EIGEN3
####或者在 CMakeLists.txt 文件中使用 `message` 命令打印变量:
message(STATUS "Eigen3 include directories: ${EIGEN3_INCLUDE_DIRS}")
这将输出 `EIGEN3_INCLUDE_DIRS` 变量的值,如果该变量被设置的话。您可以用这种方式检查其他可能的变量。
4. include_directories(...):
- 用途:用于向项目添加头文件的搜索路径,这样编译器在编译源文件时能够找到所需的头文件。
-基本语法是 include_directories(<path1> [<path2> ...<pathN>])
,其中 <pathX>
是要添加的头文件路径,其中路径应该指向包含头文件的目录,而不是单个头文件,而且多路径中每个路径都应该用空格分隔。
- 使用方式:指定头文件所在的目录,include_directories
默认是全局的,添加的路径对项目中的所有目标都有效。也可以通过特定的语法添加局部路径,只对特定的目标有效。
例如:
# 添加单个路径
include_directories(include)
# 添加多个路径
include_directories(include src/external_lib)
# 使用 find_package 获取的路径
include_directories(${EIGEN3_INCLUDE_DIRS})
当项目依赖于第三方库或框架时,通常需要包含这些库的头文件。用 include_directories
可以指定这些库的头文件目录。经常与 find_package
命令结合使用,通过获取外部库的头文件路径(例如通过 EIGEN3_INCLUDE_DIRS
这样的变量),然后将这些路径传递给 include_directories。include_directories
是一个简单直接的命令,但在大型或复杂的项目中,使用更现代的命令如 target_include_directories
可以提供更好的模块化和灵活性。
5. target_link_libraries(...):
- 用途:用于将一个或多个库链接到指定的目标上,确保在链接阶段目标能够找到并使用这些库。比如可以链接静态库(.lib 或 .a 文件)、动态库(.so 或 .dll 文件)以及别名库(通过 CMake 的 alias
创建)。
- 基本语法是 target_link_libraries(<target> <library1> [<library2> ...])
,其中 <target>
是要链接库的目标名称,<library1>
等是库的名称或别名。
- 使用方式:指定目标和需要链接的库,例如:
# 链接单个库
target_link_libraries(my_app Eigen3::Eigen)
# 链接多个库
target_link_libraries(my_app Eigen3::Eigen ${OpenCV_LIBS})
通常与 find_package
命令结合使用,通过获取外部库的目标名称或路径,然后将这些信息传递给 target_link_libraries
。
6. add_library(...):
- 用途:用于创建库目标,可以是静态库(Static Library,通常以 .a
结尾)或动态库(Shared Library,通常以 .so
或 .dll
结尾)。其中
- 静态库:通常用于在编译时将库的代码整合到最终可执行文件中。
- 动态库:在运行时被加载,允许多个程序共享同一份库代码。
-基本语法: add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] <source1> <source2> ...)
。
<name>
是库目标的名称。STATIC
、SHARED
或MODULE
指定库的类型,默认为SHARED
。EXCLUDE_FROM_ALL
表示该库不会自动被包含在all
目标中- 通过
<source1> <source2> ...
参数指定库的源文件列表。
- 使用方式:指定库的名称和源文件,可以是静态库或共享库,例如:
# 创建一个名为 my_lib 的静态库
add_library(my_lib STATIC src/lib1.cpp src/lib2.cpp)
# 创建一个名为 my_lib 的动态库
add_library(my_lib SHARED src/lib1.cpp src/lib2.cpp)
一般结合使用 target_link_libraries
将其他目标链接到这个库,或者将这个库链接到其他目标。使用 target_include_directories
为使用该库的目标指定头文件搜索路径。
7. add_executable(...):
- 用途:用于创建一个可执行文件目标,即最终将生成可直接运行的程序。
-基本语法: add_executable(<name> [EXCLUDE_FROM_ALL] <source1> <source2> ...)
。
<name>
是可执行文件目标的名称,也是最终生成的可执行文件的名称。EXCLUDE_FROM_ALL
选项表示该目标不会自动被包含在all
目标中,允许用户通过指定目标名称来构建。- 通过
<source1> <source2> ...
参数指定构成可执行文件的源文件列表。
- 使用方式:指定可执行文件的名称和源文件,例如:
# 创建一个名为 my_exe 的可执行文件,由 main.cpp 和 other_source.cpp 组成
add_executable(my_exe main.cpp other_source.cpp)
一般结合使用 target_link_libraries
将所需的库链接到可执行文件目标上,使用 target_include_directories
为可执行文件目标指定头文件搜索路径。
这些命令是构建C++项目时常用的CMake命令。使用时,您需要根据项目的具体需求和结构来编写 `CMakeLists.txt` 文件。例如,如果您的项目依赖于外部库,您将使用 `find_package` 来查找这些库,并使用 `target_link_libraries` 将它们链接到您的目标。如果您的项目包含多个源文件,您可以使用 `file(GLOB_RECURSE ...)` 来收集它们,然后使用 `add_library` 或 `add_executable` 来定义您的目标。
(4)总结
编写 `CMakeLists.txt` 时,您应该遵循以下步骤:
- - 使用 `cmake_minimum_required` 指定CMake的最低版本要求。(必须的)
- - 使用 `project` 定义项目名称和语言。(必须的)
- - 设置编译选项,如C++标准。(没设置则会根据系统选择)
- - 使用 `find_package` 查找并配置外部依赖。(需要依赖第三方脚本,配合target_link_libraries使用)
- - 使用 `include_directories` 添加头文件搜索路径,推荐
target_include_directories
。( 添加头文件搜索路径) - - 使用 `add_library` 或 `add_executable` 定义构建目标。(必须的,确定项目目标,有时配合file一块使用)
- - 使用 `target_link_libraries` 将依赖库链接到目标。(需要依赖第三方库时用,放在add_executable等后面)
- - 可选地,使用 `install` 命令定义安装规则。(将项目编译安装)
高质量 CMakeLists.txt
文件的指导原则和最佳实践:
-
项目初始化:使用
cmake_minimum_required
和project
明确项目信息和 CMake 版本。 -
目标定义:用
add_executable
和add_library
清晰定义可执行文件和库。 -
依赖管理:通过
find_package
和导入目标管理外部依赖。 -
条件编译:利用条件语句处理不同平台或配置的编译选项。
-
编译特性:使用
target_compile_features
设置 C++ 标准和编译特性。 -
源码组织:避免过度使用
file(GLOB...)
,尽量手动列出源文件。 -
链接管理:使用
target_link_libraries
精确链接库,避免使用link_directories
。 -
包含路径:使用
target_include_directories
指定目标的头文件搜索路径。 -
安装规则:用
install
命令定义安装目标和文件的规则。 -
构建类型:为 Debug 和 Release 等构建类型设置不同的编译选项。
-
测试支持:利用 CTest 框架添加自动化测试。
-
文档注释:在
CMakeLists.txt
中使用注释清晰地说明构建逻辑。 -
避免全局变量:尽量使用目标特定的变量,避免全局变量污染。
-
跨平台兼容性:确保
CMakeLists.txt
可在不同平台和编译器上工作。 -
持续集成:确保
CMakeLists.txt
兼容 CI 系统,提供清晰的构建和测试脚本。 -
错误处理:在找不到依赖或配置错误时提供清晰的错误信息。
-
示例和指南:提供
CMakeLists.txt
使用示例和开发者指南。
编写 CMakeLists.txt
时,保持简洁、明确和可维护性是关键。遵循这些精炼的原则,可以创建出既强大又易于理解的构建系统。