用C++20和将来的C++23写Hello,World!是一种怎样的体验?
C++的脚步,永远没有停止……
查看原题请点击 B2002 Hello,World!
本文将使用 C++20 和将于 2023 年秋季发布的 C++23 编写 Hello,World!
C++20 模块化编程
我们都知道,C++ 的输入输出需要包含头文件 iostream,但如果使用模块化编程,一切都不一样了。
C++20 模块化编程(单文件)
先来看代码:(由于模块化编程在 C++20 中还是草案,因此下面代码不一定能够运行成功)
import <iostream>
using namespace std;
int main() {
cout << "Hello,World!" << endl;
return 0;
}
这时写 Java 和 Python 的就开始惊讶了:C++ 中有 import?
没错,import 是 C++ 新加入的标识符,用于模块化编程。实际上这里并没有使用到模块化编程,它与 #include <iostream> 没有区别。
C++20 头文件编程·正常(多文件)
那怎么自定义头文件呢?我们先来写一个头文件,里面的函数将显示 Hello,World:
// hello.h
// 头文件——包含函数声明(函数原型)
void sayhello();
在使用一个 cpp 文件实现该函数。
// hello.cpp
// 源文件——包含函数定义(函数实现)
#include <iostream>
#include "hello.h"
using namespace std;
void sayhello() {
cout << "Hello,World!" << endl;
}
注意:#include 指令如果将尖括号换为双引号,则默认先从当前目录下寻找头文件。所以需要把 hello.h 与 hello.cpp 放在同一目录下面。
接下来,在同一个目录里面编写主程序:
// main.cpp
// 源文件——包含主函数的主程序
#include "hello.h"
int main() {
sayhello();
return 0;
}
然后编译和链接(有些编辑器支持一键编译,如 Visual Studio、VS Code 等。如果使用 Dev-C++,请新建一个项目,然后在项目里添加 hello.h、hello.cpp 和 main.cpp,然后一键编译项目(或者编译 main.cpp)。)
生成的程序是可以运行的。
C++20 头文件编程·重复定义(多文件)
如果我把函数定义写在 hello.h 中:
// hello.h (2)
// 头文件——包含函数原型和函数定义
#include <iostream>
using namespace std;
void sayhello(); // 函数原型
void sayhello() { // 函数定义
cout << "Hello,World!" << endl;
}
然后重写 hello.cpp:
// hello.cpp (2)
// 源文件——使用 hello.h
#include "hello.h"
void sayhello2() {
sayhello();
}
再重写 main.cpp:
// main.cpp (2)
// 源文件——包含了 hello.h 和 hello.cpp
#include "hello.h"
#include "hello.cpp"
int main() {
sayhello2();
return 0;
}
看似一切正常,但如果这时候编译,就会出现链接错误,说找到了 sayhello() 的多个定义。
这个就是令人诟病的重复定义问题,经常发生在头文件中。
C++ 程序员忍受不了这样的错误,于是……
重复定义解决方案
那有没有解决方案呢?有的,那就是模块化编程。由于 C++20 还是草案,所以模块化编程将在下面的 C++23 中讲解。
C++23 模块化编程
传统头文件编程 |VS| C++23 模块化编程
2023年秋季将发布的 C++23 完全实现了模块化编程。主要为了解决下面问题:
- 头文件中所有的东西都可以被外界使用。
- 如果在头文件中加入定义,会导致重复定义的链接错误。
- 由于
#include指令做的是将头文件复制到源文件(例如在 main.cpp 中包含iostream,就是将iostream的内容复制到 main.cpp)。这样就有一个问题:所有的程序都将重复地编译iostream,导致编译速度下降。
而 C++23 加入了模块化编程,其一部分优点就是:
- 可以选择要导出的东西,如果某一个东西不导出,则外界将无法使用,做到了数据隐藏。
- 可以直接在模块中加入定义,不会导致重复定义的错误。
- 由于采用模块化编程,只需要将模块编译一次,其他所有程序都可以使用(不会重复编译)。模块修改时,只需要重新编译一次模块。在某些情况下,编译速度将会比原来快 10 倍!
- 采用模块化编程可以使原来不会多文件编程或者害怕多文件编程的程序员、OI参赛者或程序爱好者更容易地使用多文件编程,在程序复杂度较高时,模块化编程可以更好地管理程序。
- 模块可以分区,可以更容易地管理模块。
既然模块化编程这么好,就让我们来看看模块化编程写Hello,World是怎样的吧!
建议阅读:
- Visual C++ 中的模块化编程文档(中文版)C++ 中的模块概述
- Visual C++ 中使用模块化编程(中文版)教程:使用模块导入 C++ 标准库
- Visual C++ 命名模块教程(中文版)C++ 中的命名模块教程
C++23 模块化编程——单文件 Hello,World!
先来看看使用模块化编程编写的 Hello,World! 程序:
import std;
int main() {
Std::cout << "Hello,World!" << Std::endl;
return 0;
}
相信你也看出来了,C++23 真的完全实现了模块化编程,连标准输入输出都装到了模块里!
想要深入了解模块,我们先来自己写一个模块。
C++23 模块化编程——多文件(声明与定义分开)
新建一个项目,在项目里增加一个文件 hello.ixx。这里的 .ixx 就是模块接口单元文件。
// hello.ixx
// 模块接口单元——包含函数声明(函数定义)
export module hello; // 声明一个可以被导出的模块
/*
在模块接口单元中,使用 export 可以使某个东西能够导出。
如果不使用 export,则这个东西在模块内可用,在模块外不能直接使用。
module 表示这是一个模块,后面通常跟着模块名称(也可能有分区,但这里没有)
*/
export void sayhello(); // 声明一个可以被导出的函数
再加入一个文件,这个文件就是模块实现单元,后缀名随意,但通常是 .cpp。加入 hello.cpp:
// hello.cpp
// 模块实现单元——包含函数定义
import hello; // 导入我们的模块
import std; // 导入标准模块
using namespace Std;
/*
注意:import 是一个标识符
所有 import 语句应放在程序的最前面(当然注释除外),否则无效。
模块导入的顺序无关紧要。即使导入重复的模块,程序里也只会使用一个模块。
*/
void sayhello() { // 提供函数定义
cout << "Hello,World!" << endl;
}
最后主程序 main.cpp:
// main.cpp
// 源文件——主程序
import hello; // 导入我们的模块
// import std;
/*加上上面这句也没问题,即使与 hello 模块里的 std 冲突。*/
int main() {
sayhello();
return 0;
}
编译链接,程序成功!
C++23 模块化编程——多文件(声明与定义综合)
事实上,不知道你有没有发现这样一个问题:
如果有一个东西是不导出的(即没有 export 声明),那么模块实现单元将无法实现它(因为它不导出)
那么如何实现这个问题呢?
把定义也放到模块接口单元中?
重写 hello.ixx:
// hello.ixx (2)
// 模块接口单元——把定义也放到这里
export module hello;
// 注意 module 声明要放到 import 之前。
import std;
using namespace Std;
export void sayhello();
export void sayhello() { // 定义
cout << "Hello,World!" << endl;
}
或者使用模块化编程更常用的内联定义:
// hello.ixx (3)
// 模块接口单元——内联定义
export module hello;
import std;
using namespace Std;
export void sayhello() { // 内联定义,即省略函数原型
cout << "Hello,World!" << endl;
}
有了内联定义,我们不需要模块实现单元了,直接编译,然后运行成功。(这里不需要重写 main.cpp)
C++23 其他预告
距离 C++23 标准发布已经接近尾声。C++23 的制定过程简直跟小说一样带感。
新增标头
C++ 新增的标准头文件如下: