ShuangChenYue ShuangChenYue
首页
  • Cpp之旅
  • Cpp专栏
  • Effective_CPP
  • muduo网络库
  • Unix环境高级编程
  • Cpp提高编程
  • 计算机网络
  • 操作系统
  • 数据结构
  • Linux
  • 算法
  • 基础篇
  • MySql
  • Redis
  • 电子嵌入式通信协议
  • 深入浅出SSD
  • 文件系统
  • 汇编语言
  • STM32
  • 随笔(持续更新)
  • Git知识总结
  • Git 创建删除远程分支
  • nvm使用小结
  • 虚拟机固定 IP 地址
  • Shell 脚本学习笔记
  • VScode 插件 CodeGeeX 使用教程
  • KylinV10 将项目上传至 Github教程
  • KylinV10 安装 MySQL 教程(可防踩雷)
  • kylinV10-SP1 安装 QT
  • 高并发内存池
  • USBGUARD 项目编译环境配置
  • Power_Destory 项目
  • U 盘清除工具编译教程
  • 个人博客代码推送教程
  • HTML与CSS
  • JS学习
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • MFC编程随记
  • MFC实现ini配置文件的读取
  • MFC实现点击列表头排序
  • 贴图法美化Button按钮
  • 如何高效阅读嵌入式项目代码
  • NAND Flash
  • ARM 处理器
  • 嵌入式基础知识-存储器
  • 闪存存储和制造技术概述
  • 芯片IO驱动力
  • 主流先进封装技术介绍
  • 虎牙C++技术面经
  • 金山一面复习
  • 完美世界秋招 C++ 游戏开发面经(Cpp部分)
  • 博客搭建
  • 网站收藏箱
首页
  • Cpp之旅
  • Cpp专栏
  • Effective_CPP
  • muduo网络库
  • Unix环境高级编程
  • Cpp提高编程
  • 计算机网络
  • 操作系统
  • 数据结构
  • Linux
  • 算法
  • 基础篇
  • MySql
  • Redis
  • 电子嵌入式通信协议
  • 深入浅出SSD
  • 文件系统
  • 汇编语言
  • STM32
  • 随笔(持续更新)
  • Git知识总结
  • Git 创建删除远程分支
  • nvm使用小结
  • 虚拟机固定 IP 地址
  • Shell 脚本学习笔记
  • VScode 插件 CodeGeeX 使用教程
  • KylinV10 将项目上传至 Github教程
  • KylinV10 安装 MySQL 教程(可防踩雷)
  • kylinV10-SP1 安装 QT
  • 高并发内存池
  • USBGUARD 项目编译环境配置
  • Power_Destory 项目
  • U 盘清除工具编译教程
  • 个人博客代码推送教程
  • HTML与CSS
  • JS学习
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • MFC编程随记
  • MFC实现ini配置文件的读取
  • MFC实现点击列表头排序
  • 贴图法美化Button按钮
  • 如何高效阅读嵌入式项目代码
  • NAND Flash
  • ARM 处理器
  • 嵌入式基础知识-存储器
  • 闪存存储和制造技术概述
  • 芯片IO驱动力
  • 主流先进封装技术介绍
  • 虎牙C++技术面经
  • 金山一面复习
  • 完美世界秋招 C++ 游戏开发面经(Cpp部分)
  • 博客搭建
  • 网站收藏箱
  • Cpp之旅

    • 第1章 基础
    • 第2章 用户自定义类型
    • 第3章 模块化
      • 3.1 分离编译
      • 3.2 模块
      • 3.3 模块与头文件两种方法的区别
      • 3.4 命名空间
      • 3.5 函数参数与返回值
        • 3.5.1 结构化绑定
      • 3.6 建议
    • 第4章 错误处理
    • 第5章 类
    • 第6章 基本操作
    • 第7章 模板
    • 第8章 概念和泛型编程
    • 第9章 标准库
  • Cpp专栏

  • Effetcive_CPP

  • muduo网络库

  • Unix环境高级编程

  • Cpp提高编程

  • CPP语言
  • Cpp之旅
霜晨月
2023-11-24
目录

第3章 模块化

# Cpp之旅(学习笔记)第3章 模块化

# 3.1 分离编译

Cpp支持一种名为分离编译的概念,用户代码只能看见所用类型和函数的声明。

有两种方法可以实现它:

  • 头文件:将声明放进一个名为头文件的独立文件,然后将头文件以文本方式#include到代码中你需要声明的地方。
  • 模块:定义module文件,独立地编译它们,然后在需要时import它们。在import对应module时,只有其中显示export的声明是可见的。

优点:

  • 可以尽可能地减少编译时间,并且强制要求程序中逻辑独立地部分分离开来(从而尽可能降低发生错误地概率)。

模块技术是在Cpp20中出现地新特性,其提供了实质性地优势,对改善代码组织与编译耗时都有好处。

一个单独编译的.cpp文件(包含它#include的.h文件)被称作一个翻译单元。

使用#include及头文件实现模块化是一种传统方法,它具有明显地缺点:

  • 编译时间: 如果你在101个翻译单元中#include header.h,这个header.h的头文件将被编译器处理101次。
  • 依赖顺序: 如果你在header2.h之前#include header1.h,在header1.h中的定义与宏可能会影响header2.h中代码的含义,反之亦然。
  • 不协调: 如果你在一个文件中定义一个实体,比如类型或者函数,然后在另一个文件中定义一个稍微不同的版本,则可能导致崩溃或者难以觉察的错误。
  • 传染性: 所有表达头文件中某一个声明所需的代码,都必须出现在头文件中。这会导致代码膨胀,因为头文件为了完成声明需要#include其他头文件,这会导致头文件的用户需要(有意或者无意地)依赖头文件包含地实现细节。

# 3.2 模块

Cpp20中出现了语言级的方式来直接实现模块化。

export module Vector;			//定义一个module,名为Vector
export class Vector {
public:
    Vector(int s);
    double& operator[](int i);
    int size();
private:
    double* elem;				//elem指向一个数组,该数组包含sz个double类型的元素
    int sz;
};
Vector::Vector(int s) : elem{new double[s], sz{s}} {}//初始化元素
double& Vector::operator[](int i){
    return elem[i];
}
int Vector::size() {
    return sz;
}
export bool operator==(const Vector& v1,const Vector& v2) {
    if(v1.size() != v2.size())
        return false;
    for(int i = 0; i < v1.size(); ++i)
        if(v1[i] != v2[i])
            return false;
    return true;
}
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

上诉代码定义了一个 module,名为 Vector,这个模块导出了 Vector 类及所有成员函数,还有成员函数的操作符 ==。

要使用上述 module,只要在需要用到它的地方 import 就可以了。

例如:

//user.cpp
import Vector;							//获得Vector的相关接口
#include <cmath>						//获得标准库的数学函数接口,包含sqrt()
double sqrt_sum(Vector& v){
    double sum = 0;
    for(int i = 0; i != v.size(); ++i)
        sum +=std::sqrt(v[i]);			//平方根之和
    return sum;
}
1
2
3
4
5
6
7
8
9

#inclue <cmath>也可以改为import cmath;这里仅仅是为了演示新旧方式的混合。

# 3.3 模块与头文件两种方法的区别

头文件与模块的区别不仅仅是在语法上:

  • 模块只被编译一次,不会在每个用到它的翻译单元那里都被重新编译。
  • 两个模块 import 的顺序不影响其含义。
  • 如果你在模块内部 import 或者 #include 其他内容,模块的使用者不会隐式地获得那些模块地访问权:这意味着 import 没有传染性。
  • 模块在维护性与编译时间方面地改进非常显著。
例如:作者测试过使用 `import std;` 的 "Hello,world!" 程序
它的编译速度比使用 `#include<iostream>` 的版本 快10倍 
这还是在 std 模块因包含了整个标准库足足有 <iostream> 10 倍大小的前提下实现的。
1
2
3

提升的原理:

  • 模块只导出接口,但头文件需要传递所有直接的或者间接的信息给编译器。

不幸的是,module std还没有进入Cpp20。 附录A介绍了如何获得一份 module std 的方法,这里不详细展开。

示例:

当定义一个模块时,不需要将实现与声明分开写成两个文件,如果想改进你的源代码,可以这么做:

export module Vector;		//定义 module,名叫 Vector
export class Vector {
    //...
};
export bool operator==(const Vector& v1, const Vector& v2){
    //...
}
1
2
3
4
5
6
7

编译器负责把(用 export 指定的)模块的接口从实现细节中分离出来,因此,Vector 接口由编译器生成,不需要由用户指定。

使用module时,不需要为了在接口文件内隐藏实现细节而将代码变得复杂;因为模块只导出显示export的声明。

考虑如下代码:

export module vector_printer;
import std;
export
template<typename T>
void print(std::vector<T>& v)	//这是唯一能被用户看见的函数
{
    cout << "{\n";
    for(const T& val : v)
        std::cout << " " << val << '\n';
    cout << '}';
}
1
2
3
4
5
6
7
8
9
10
11

# 3.4 命名空间

  • Cpp还提供了一种名为命名空间的机制,一方面表达某些声明是属于一个整体的,另一方面表面它们的名字不会与其他命名空间中的名字冲突。
  • 要想访问其他命名空间中的某个名字,最简单的方法是在这个名字前加上命名空间的名字作为限定(例如,std::cout和My_code::main)。
  • ”真正的main()“定义在全局命名空间中,换句话说,它不属于任何自定义的命名空间、类或者函数。
  • 如果觉得反复使用命名空间限定显得冗长及干扰了可读性,可以使用using声明将命名空间中的名字放进当前作用域。
void my_code(vector<int>& x,vector<int>& y) {
    using std::swap; //将标准库的swap放进本地作用域
    //...
    swap(x,y);		 //std::swap()
    other::swap(x,y);//某个其他的swap()
}
1
2
3
4
5
6

# 3.5 函数参数与返回值

默认情况使用复制(传值),如果希望直接指向调用者环境中的对象,我们使用引用(传引用)的方式。

从性能方面考虑,我们通常对小数据传值、对大数据传引用。这里的小意味着”复制开销很低“。通常而言,”尺寸在两到三个指针以内“是一个不错的标准。

# 3.5.1 结构化绑定

一个函数只能返回一个值,但这个值可以是拥有很多成员的类对象。这往往是函数体面地返回多个值地方法。

例如:

struct Entry {
    string name;
    int value;
};
Entry read_entry(istream& is)//简单地读函数
{
    string s;
    int i;
    is >> s >> i;
    return {s,i};
}
auto e = read_entry(cin);
cout << "{" << e.name << "," << e.value << "}\n";
1
2
3
4
5
6
7
8
9
10
11
12
13

在这里,{s,i}被用于构造Entry类型地返回值。类似地,我们也可以将Entry的成员”解包"为局部变量:

auto [n,v] = read_entry(is);
cout << "{" << n << " , " << v << " }\n";
1
2

这里的auto[n,v]声明了两个变量n和v,它们的类型来自对read_entry()返回类型的推导。

这种把类对象成员的名称赋予局部变量名称的机制叫作结构化绑定。

# 3.6 建议

区分声明(用作接口)和定义(用作实现);

优先选择module而非头文件(在支持module的地方);

使用头文件描述接口、强调逻辑结构;

在头文件中应避免定义非内联函数;

不要在头文件中使用using指令;

采用传值方式传递“小”值,采用传引用方式传递“大”值;

优先选择传const引用方式而非传普通引用方式;

不要过度使用返回类型推断;

不要过度使用结构化绑定;使用命名的返回类型通常可以使代码更为清晰。

上次更新: 2024/6/3 14:54:44
第2章 用户自定义类型
第4章 错误处理

← 第2章 用户自定义类型 第4章 错误处理→

Theme by Vdoing | Copyright © 2023-2024 霜晨月
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式