2. 项目结构设计

说实话,我见过太多C++项目,一开始就几个人写,目录结构随便搞搞。等团队扩张到二三十人,代码量突破几十万行,那叫一个乱。我有个朋友接手过一个遗留项目,光找一个核心算法文件就花了三天——文件名叫utils_v2_final_final.cpp。嗯,你想想看,这种项目谁敢动?

今天我就把我在多个大型C++项目中沉淀下来的结构设计经验,掰开揉碎了讲给你听。说白了,就是让你少走弯路。

一、目录组织的黄金法则

我个人习惯,项目根目录下只放三样东西:src/(源码)、include/(公共头文件)、CMakeLists.txt(构建文件)。其他乱七八糟的,统统扔到子目录里去。

为什么这么干?因为根目录一旦膨胀,你连项目是干啥的都得猜半天。

核心原则:每个目录只做一件事,名字要让人一眼看懂。

我建议的典型结构长这样:

my_project/
├── src/                    # 源码目录
│   ├── core/               # 核心逻辑
│   ├── network/            # 网络模块
│   ├── storage/            # 存储模块
│   └── main.cpp            # 入口
├── include/                # 公共头文件
│   └── my_project/
│       ├── core/
│       ├── network/
│       └── storage/
├── tests/                  # 单元测试
├── docs/                   # 文档
├── scripts/                # 构建脚本
├── third_party/            # 第三方依赖
└── CMakeLists.txt          # 顶层构建文件

注意看,include/下面我多套了一层my_project/。这是为了防止头文件冲突。你想想看,如果两个库都叫config.h,那编译时就是一场灾难。

二、模块划分:别让代码变成一团乱麻

模块划分这件事,我踩过最大的坑就是「过度抽象」。曾经有个项目,把日志模块拆成了log_writer、log_formatter、log_filter、log_router……十几个小类。结果呢?新人入职两周都搞不清日志到底怎么写。

模块划分的原则其实很简单:高内聚、低耦合。每个模块只负责一个业务领域,对外暴露尽量少的接口。

我常用的划分方式:

模块 职责 对外接口数量
core 业务核心逻辑,不依赖任何外部库 3-5个
network 网络通信,封装TCP/UDP/HTTP 5-8个
storage 数据持久化,支持SQLite/MySQL 4-6个
ui 用户界面,只调用core的接口 2-3个

你看,每个模块的接口数量都控制在个位数。为什么?因为接口越少,模块之间的依赖就越清晰,改起来就越安全。

小技巧:模块之间只通过接口类(纯虚类)通信,不要直接依赖具体实现。这样你想换掉某个模块,只需要重新实现接口就行。

三、命名规范:统一就是生产力

命名这件事,我见过最离谱的是同一个项目里同时出现snake_case、camelCase和PascalCase。你猜怎么着?代码review的时候,一半时间都在吵命名。

我推荐一套经过实战检验的规范:

  • 类名:PascalCase,比如HttpServer、DatabaseManager
  • 函数名:camelCase,比如sendRequest()、parseConfig()
  • 变量名:snake_case,比如user_count、max_retry_times
  • 宏定义:全大写加下划线,比如MAX_BUFFER_SIZE
  • 命名空间:小写,比如my_project::core

为什么变量用snake_case?因为C++标准库就是这么干的,你想想看std::vector::size(),多顺眼。

避坑指南:我曾经在一个项目里用m_前缀表示成员变量,结果团队里有人用m_,有人用_,还有人什么都不用。最后代码里m_count、_count、count同时存在,查个bug能查一整天。所以,要么统一用m_,要么统一不用,别搞混搭。

四、代码分层:让依赖关系一目了然

代码分层,说白了就是规定「谁可以依赖谁」。我常用的分层结构是三层:

  1. 接口层:定义模块之间的契约,全是纯虚类
  2. 业务层:实现核心逻辑,只依赖接口层
  3. 实现层:具体的技术实现,比如数据库驱动、网络库封装

依赖方向是单向的:接口层 ← 业务层 ← 实现层。业务层永远不知道实现层的存在。

举个例子:

// 接口层
class IStorage {
public:
    virtual ~IStorage() = default;
    virtual bool save(const std::string& key, const std::string& value) = 0;
    virtual std::string load(const std::string& key) = 0;
};

// 业务层
class UserService {
public:
    explicit UserService(std::unique_ptr<IStorage> storage);
    void createUser(const std::string& name);
private:
    std::unique_ptr<IStorage> storage_;
};

// 实现层
class SqliteStorage : public IStorage {
    // 具体实现
};

你看,UserService根本不知道数据是存在SQLite还是MySQL里。想换存储后端?写个新实现类就行,业务代码一行都不用改。

五、知识体系总览

下面这张图,是我对本章内容的总结。你可以把它当成一个检查清单,每次新建项目时对照着看一遍。

大型C++项目结构设计知识体系 目录组织 src/ 源码 include/ 头文件 tests/ 测试 docs/ 文档 third_party/ 依赖 模块划分 高内聚 低耦合 接口数量 ≤ 8 单一职责 纯虚类通信 命名规范 类名: PascalCase 函数: camelCase 变量: snake_case 宏: UPPER_CASE 命名空间: 小写 代码分层 接口层 业务层 实现层 单向依赖 依赖反转 核心思想 目录清晰 → 模块独立 → 命名统一 → 分层明确 可维护、可扩展、可协作

这张图把四个核心维度串起来了。你从左上角开始看,目录组织是骨架,模块划分是器官,命名规范是细胞,代码分层是血管。缺一个,项目都会出问题。

最后说一句:结构设计没有银弹。我见过用扁平结构跑得飞快的项目,也见过严格分层但寸步难行的项目。关键是要根据团队规模、业务复杂度、迭代频率来动态调整。别为了设计而设计,结构是为人服务的。

可可精品资料库