【C++现代#14】C++20概念与约束

📃个人主页:island1314
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
- 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》
🔥 目录
官方文档:https://en.cppreference.com/w/cpp/language/constraints.html
一、概念
1.1 Concepts 是什么?
“A concept is a named set of requirements.”
概念是一个命名的需求集合。
🔑 核心思想
- 传统模板:编译时"试错" → 报错信息晦涩难懂
- Concepts:编译时"契约" → 提前约束 + 清晰报错
- 定义:概念(concept)是C++20引入的模板参数约束机制,它允许程序员明确指定模板参数必须满足的条件。概念本质上是一个 编译时谓词,用于验证模板参数是否满足特定要求。
- 作用:概念会在 模板实例化前检查类型是否满足条件,而不是在实例化后产生难以理解的错误。
注意:概念的定义必须出现在命名空间范围内
1.2 语法结构拆解
1️⃣ 基本语法形式
template <template-parameter-list>
concept concept-name attr(optional) = constraint-expression;
| 组成部分 | 说明 | 示例 |
|---|---|---|
template<...> | 模板参数列表 | <class T> |
concept | 关键字 | 固定写法 |
concept-name | 概念名称 | Integral, Derived |
constraint-expression | 约束表达式 | std::is_integral_v<T> |
-
不允许对概念进行显式实例化、显式特化或部分特化(不能改变约束的原始定义的含义)。
-
可以在 ID 表达式中命名概念。
-
ID 表达式的值为:如果约束表达式得到满足 true ,否则 false
-
概念也可以在类型约束中命名,作为……的一部分
-
类型模板参数声明
-
占位符类型说明符
-
化合物需求
-
在类型约束中,概念 的模板参数比其参数列表要求的少一个,因为上下文推断的类型被隐式地用作概念的第一个参数。
2️⃣ 示例解析
示例 1:基础概念定义
template<class T, class U>
concept Derived = std::is_base_of<U, T>::value;
- 含义:
Derived<T, U>为true当且仅当T是U的派生类 - 使用场景:约束模板参数必须满足继承关系
- 底层原理:Concept 本质是一个编译期布尔谓词
示例 2:递归概念(非法)
template<typename T>
concept V = V<T*>; // error: recursive concept
- 原因:Concept 在编译期求值,递归会导致无限展开
- 类比:类似
constexpr函数不能无限递归
示例 3:约束概念定义(非法)
template<class T>
concept C1 = true;
template<C1 T> // 错误:不能用 C1 约束概念定义
concept Error1 = true;
template<class T> requires C1<T> // 错误:同样不允许
concept Error2 = true;
- 核心规则:Concept 定义本身不能被约束
- 设计哲学:Concept 是"元约束",用于约束普通模板,而非约束自己
示例 4:Concept 的使用方式
template<Derived<Base> T>
void f(T); // 等价于:requires Derived<T, Base>
- 关键技巧:在
type-constraint中,Concept 少写一个参数 - 隐式规则:
Derived<Base> T→ 自动推导为Derived<T, Base> - 记忆口诀:“占位类型 T 自动补到最前面”
1.3 Concepts 的使用场景
场景 1:简单使用
// 方式1:使用标准库类型萃取
template<typename T>
concept Integral = std::is_integral_v<T>;
// 方式2:简单的常量表达式
template<typename T>
concept SmallType = sizeof(T) <= 4;
调用如下:

场景 2:函数模板约束(最常用)
// ✅ 方式 1:abbreviated function template(简洁)
std::integral auto add(std::integral auto a, std::integral auto b) {
return a + b;
}
// ✅ 方式 2:requires 子句(灵活)
template<typename T>
requires std::integral<T>
T multiply(T a, T b) { return a * b; }
// ✅ 方式 3:concept 命名约束(可复用)
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<Number T>
T compute(T x) { return x * x; }
场景 3:类模板约束
template<typename T>
requires std::copyable<T>
class Buffer {
std::vector<T> data;
public:
void push(T item) { data.push_back(std::move(item)); }
};
// 或简写:
template<std::copyable T>
class Queue { /*...*/ };
场景 4:概念组合与逻辑运算
template<typename T>
concept Printable = requires(T t) {
{ std::cout << t } -> std::convertible_to<std::ostream&>;
};
template<typename T>
concept Serializable = Printable<T> && requires(T t) {
{ t.to_json() } -> std::convertible_to<std::string>;
};
template<Serializable T>
void save(const T& obj); // 必须同时支持打印 + 序列化
1.4 Concepts 如何工作?
编译期求值流程
- 编译器遇到模板实例化请求
- 检查关联的 concept 约束表达式
- 若表达式为 true → 继续实例化
- 若表达式为 false → 丢弃该重载(SFINAE)或报错
- 若无匹配重载 → 给出清晰的"constraint not satisfied"错误
🎁 附:Concepts 速查卡片
// 🔹 标准概念(<concepts>)
std::integral<T> // 整数类型
std::floating_point<T> // 浮点类型
std::same_as<T, U> // 类型完全相同
std::convertible_to<T, U> // 可隐式转换
std::invocable<F, Args...> // 可调用
std::copyable<T> // 可拷贝(= movable + copy_constructible)
// 🔹 自定义概念模板
template<typename T>
concept HasSize = requires(T t) {
{ t.size() } -> std::convertible_to<std::size_t>;
};
// 🔹 组合逻辑
template<typename T>
concept Container = HasSize<T> && requires(T t) {
{ t.begin() } -> std::input_or_output_iterator;
};
// 🔹 使用方式
template<Container C>
void print_size(const C& c) {
std::cout << c.size() << "\n";
}
💬 小结
Concepts 的本质:把"运行时文档"提前到"编译时契约"。
它不是银弹,但对模板库作者、接口设计者、系统编程者(比如你)来说,是提升代码质量、降低维护成本的利器。
1.5 SFINAE 和 Concepts
- SFINAE 是
Substitution Failure Is Not An Error的缩写,翻译为替换失败不是错误,通过模板替换失败静默排除候选函数,实现编译期条件分支。本质是技巧性机制,依赖模板实例化规则如std::enable_if、decltype等 Concepts直接声明模板参数的语义约束(如“类型必须可比较”),提供显式接口描述。目标是提升代码可读性和编译错误信息SFINAE代码冗长且晦涩,Concepts是SFINAE的官方替代方案,解决了后者在可维护性和表达力上的缺陷。
| 特性 | C++17 及之前 | C++20 Concepts |
|---|---|---|
| 模板约束 | SFINAE / enable_if(黑魔法) | requires / concept(白魔法✨) |
| 报错信息 | “no type named ‘type’ in…” 😵 | “constraint not satisfied: ‘Integral’” 🎯 |
| 代码意图 | 隐藏在模板元编程中 | 直接写在函数签名上 |
| 可读性 | 专家友好 | 新人友好 |
// C++17:使用 enable_if 约束整型参数
template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void foo(T x) {}
// C++20:使用 concept 约束
template<std::integral T>
void foo(T x) {}
二、约束
2.1 约束本质
Constraints(约束),这是 Concepts 的"灵魂"。简单来说:
Concept 是命名的约束,Constraint 是具体的布尔表达式。
📚 约束的本质:编译期的"契约检查"
核心关系图
Constraint Expression(约束表达式)
↓
[求值] → true/false(编译期)
↓
Concept(命名的约束) ← 可复用、可组合
↓
Template/Function/Class(被约束的目标)
约束 vs 概念 vs SFINAE
| 特性 | SFINAE (enable_if) | Concept + Constraint |
|---|---|---|
| 表达位置 | 模板参数默认值/返回类型 | 函数签名/requires 子句 |
| 可读性 | ❌ typename = std::enable_if_t<...> | ✅ requires std::integral<T> |
| 组合能力 | ⭐ 嵌套模板元编程 | ⭐⭐⭐ && ` |
| 报错信息 | ❌ “substitution failure” | ✅ “constraint not satisfied: ‘Packet’” |
| 重载解析 | ⭐ 需手动控制优先级 | ⭐⭐⭐ 自动按"更特化"排序 |
2.2 约束的基本形式
1️⃣ 原子约束(Atomic Constraints)
最基础的布尔表达式,通常是 std::is_xxx 或 requires 表达式:
// 类型特质
std::is_integral_v<T>
std::is_pointer_v<T>
// 概念实例化
std::same_as<T, int>
MyConcept<T, U>
// requires 表达式(重点!)
requires(T t) {
{ t.size() } -> std::convertible_to<std::size_t>;
{ t.begin() } -> std::input_iterator;
}
💡 关键:原子约束是约束的"最小单元",编译器会对它们进行正规化(normalization) 以便比较。
2️⃣ 逻辑组合约束
用 && || ! 组合原子约束,形成复杂条件:
- **合取 && **:逻辑与,所有约束都必须为true,整个合取约束才为true,使用&&运算符连接多个原子约束。
- **析取 || **:逻辑或,多个约束中至少有一个为true,整个析取约束就为true,使用运算符连接多个约束。
// 1. 逻辑与(&&)
template<typename T>
concept IntegralAndPrintable = std::integral<T> && requires(T t) {
std::cout << t;
};
// 2. 逻辑或(||)
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
// 3. 逻辑非(!)
template<typename T>
concept NonPointer = !std::is_pointer_v<T>;
3️⃣ Requires 表达式(最强大!)
用于检查表达式是否合法,而非仅仅类型特质:
#include <concepts>
#include <iostream>
#include <vector>
#include <string>
// 1、简单要求
// 最简单的形式,只检查表达式是否合法(能否编译通过)
template<typename T>
concept Addable = requires(T a, T b) {
a + b; // 检查类型T是否支持+运算
};
// 2、类型要求
// 检查某个嵌套类型是否存在
template<typename T>
concept HasValueType = requires {
typename T::iterator; // 检查T是否有iterator嵌套类型
typename T::value_type; // 检查T是否有value_type嵌套类型
};
// 3、复合要求
// 不仅检查表达式是否合法,还可以指定返回类型和异常规范:
// 复合要求的完整语法为:
// { 表达式 }[noexcept]->类型约束;
template<typename T>
concept ConvertibleAddable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // 要求a+b的结果可转换为T
{ a += b } noexcept -> std::same_as<T&>; // 要求a+=b不抛异常且返回T&
};
// 4、嵌套要求
// 在 requires 内部再使用 requires 进行更复杂的约束
// requires (参数列表) {
// //... 其它要求 (如类型要求、复合要求等)
// ...requires <常量布尔表达式>;
// }
template<typename T>
concept ComplexConcept = requires(T t) {
requires sizeof(T) <= sizeof(long);
requires std::is_class_v<T>;
};
template<Addable T>
void f1(const T& x1, const T& x2)
{
T ret = x1 + x2;
std::cout << ret << std::endl;
}
template<HasValueType T>
void f2(const T& x){}
template<ConvertibleAddable T>
void f3(T& x1, const T& x2)
{
T ret = x1 + x2;
std::cout << ret << std::endl;
x1 += x2;
std::cout << x1 << std::endl;
}
template<ComplexConcept T>
void f4(const T& x){}
int main()
{
f1(1, 2);
// f1("xxx", "yyy");
f2(std::vector<int>{1, 2, 3});
// f2(1);
std::string s1("111");
// f3(s1, std::string("222"));
int i = 1;
f3(i, 2);
// f4(1);
// f4(std::vector<int>{1, 2, 3});
f4(std::less<int>());
return 0;
}
Requires 表达式的语法细节
requires (参数列表可选) {
要求 1;
要求 2;
...
}
| 要求形式 | 含义 | 示例 |
|---|---|---|
{ expr } | 表达式必须合法 | { t.size() } |
{ expr } -> type-concept | 表达式合法 + 返回类型满足概念 | { t.begin() } -> std::input_iterator |
requires { ... } | 嵌套约束 | requires { typename T::value_type; } |
typename T::X | 检查嵌套类型是否存在 | typename Packet::header_t |
补充:折叠扩展约束,可变参数包的约束扩展,对参数包的多个参数进行包扩展的约束,C++26才开始支持。
2.3 约束的使用场景
场景 1:函数模板约束(最常用)
#include <iostream>
#include <string>
#include <vector>
#include <concepts>
// 定义一个要求T是整形的概念
template< class T >
concept Integral = std::is_integral_v<T>;
template< class T >
concept Float = std::is_floating_point_v<T>;
// 简单的requires⼦句
template<typename T>
concept Incrementable = requires(T t) {
++t; // 检查前缀++是否有效
t++; // 检查后缀++是否有效
};
// 1、模板参数后直接使用
template<Integral T>
void f1(T x){
std::cout << "有 concepts 约束" << std::endl;
}
// 2、requires 子句
template<typename T>
requires Integral<T> || Float<T>
void f2(T x) {}
// 3、尾置requires 子句
template<typename T>
void f3(T x) requires Integral<T> || Float<T> {}
// 4、临时约束 - 使用 requires requires
template<typename T>
requires requires(T x) { x.size(); x + x; }
void f4(T x) {}
// 5、auto占位符约束
void f5(Integral auto x) {}
int main(){
f1(1); // 符合concept要求则实例化调用对应函数模板
//f1("xxx"); // 不符合concept要求,报错
return 0;
}
场景 2:类模板约束(接口设计)
// 约束:T 必须可拷贝 + 可序列化
template<std::copyable T>
requires requires(const T& obj, std::vector<std::byte>& buf) {
{ obj.serialize(buf) } -> std::same_as<bool>;
}
class MessageQueue {
std::deque<T> queue;
public:
void push(const T& item) {
if constexpr (requires { item.validate(); }) {
if (!item.validate()) throw invalid_message{};
}
queue.push_back(item);
}
};
2.4 约束的求值规则(编译器如何工作)
1. 正规化(Normalization)
编译器会把复杂约束表达式拆解为原子约束的集合,便于比较和排序:
// 原始约束
requires (std::integral<T> && std::signed_integral<T>) || std::floating_point<T>
// 正规化后(伪代码)
{
{ std::integral<T>, std::signed_integral<T> }, // AND 组
{ std::floating_point<T> } // OR 组
}
2. 蕴含(Subsumption)→ 重载解析的关键
约束 A 蕴含 约束 B:当 A 为 true 时,B 一定为 true。
template<typename T>
concept Integral = std::integral<T>;
template<typename T>
concept SignedIntegral = Integral<T> && std::is_signed_v<T>;
// 重载函数
template<Integral T> void process(T); // #1
template<SignedIntegral T> void process(T); // #2
process(42); // 调用 #2:SignedIntegral<int> 蕴含 Integral<int>,更特化优先
process(42u); // 调用 #1:unsigned 不满足 SignedIntegral
3. 偏序(Partial Ordering) —> 重点
概念:C++20概念的 约束偏序规则 用于在函数重载或模板特化时,判断哪个约束的模板 “更特化” 或 “更受约束”。编译器使用这个规则来选择最合适的模板。
当多个重载都满足约束时,编译器按**“更特化”** 原则,偏序规则的核心是:
- 如果约束A包含(蕴含)约束B,那么A比B"更受约束"。
- 这意味着任何满足A的类型也一定会满足B,但满足B的类型不一定满足A。即 A = > B A=>B A=>B (A 蕴含 B)
- 当两个模板匹配时,编译器会选择"更受约束"的那一个。
#include <iostream>
#include <concepts>
// 定义一个要求T是整形的概念
template< class T >
concept Integral = std::is_integral_v<T>;
// 1、模板参数后直接使用
template<Integral T>
void f(T x){
std::cout << "有 concepts 约束" << std::endl;
}
template<class T>
void f(T x){
std::cout << "无 concepts 约束" << std::endl;
}
template <class T> concept A = std::is_move_constructible_v<T>;
template <class T> concept B = std::is_copy_constructible_v<T>;
template <class T> concept C = A<T> && B<T>;
template <class T>
requires A<T> void g(T x){
std::cout << "g()->A" << std::endl;
}
template <class T>
requires C<T> void g(T x){
std::cout << "g()->C" << std::endl;
}
int main(){
// 符合concept要求则实例化调用对应的函数模板
f(1);
// 不符合concept要求,查找无concept要求的模板实例化调用,没找到则报错
f("xxx");
g(1.1); // 这里同时满足A和B,会优先匹配C
g(std::unique_ptr<int>()); // 这里不满足C,只满足A,会匹配A
return 0;
}
2.5 常见问题
陷阱 1:约束"太松"导致意外匹配
template<typename T>
concept HasSize = requires(T t) { t.size(); }; // ❌ 只检查表达式存在,不检查返回类型
template<HasSize T>
void print_size(const T& c) {
std::cout << c.size(); // 如果 size() 返回 std::string 而非 size_t?编译通过但逻辑错误!
}
// ✅ 改进:加上返回类型约束
template<typename T>
concept HasSize = requires(T t) {
{ t.size() } -> std::convertible_to<std::size_t>;
};
陷阱 2:约束"太紧"导致无法扩展
// ❌ 直接写死类型
template<typename T>
concept MyInt = std::same_as<T, int>; // 无法支持 long, short 等
// ✅ 用标准概念组合
template<typename T>
concept MyInt = std::integral<T> && (sizeof(T) <= sizeof(int));
陷阱 3:requires 表达式中的"隐式转换"
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b }; // ✅ 只要表达式合法即可,不检查返回类型
};
template<typename T>
concept AddableStrict = requires(T a, T b) {
{ a + b } -> std::same_as<T>; // ✅ 额外要求:返回类型必须是 T
};
🔍 调试技巧
技巧 1:静态断言 + 概念
static_assert(std::integral<int>); // 编译期验证概念定义
static_assert(!std::integral<double>);
技巧 2:概念"打印"(C++20 不支持 runtime,但可借助模板元编程)
template<typename T>
struct concept_check {
static constexpr bool is_integral = std::integral<T>;
static constexpr bool is_pointer = std::is_pointer_v<T>;
};
// 使用:
if constexpr (concept_check<MyType>::is_integral) {
// ...
}
技巧 3:Compiler Explorer 查看约束求值
访问 godbolt.org,选择 MSVC 19.38,开启 -std:c++20,观察:
- 约束失败时的报错信息
- 重载解析时哪个函数被选中
⚠️ 注意事项
- IntelliSense 延迟:复杂
requires表达式可能导致 VS 提示变慢- 解决:临时关闭"增强型 IntelliSense",或用
#if INTELLISENSE条件编译简化
- 解决:临时关闭"增强型 IntelliSense",或用
- 模块 + 约束:若用
import std;,确保<concepts>已导出(VS 2022 17.8+ 已支持) - 调试信息:开启
/Zi后,约束失败会在调用栈中显示 concept 名称
🎁 附:约束速查卡片
// 🔹 原子约束示例
std::is_same_v<T, int>
std::derived_from<T, Base>
requires(T t) { t.foo(); }
// 🔹 逻辑组合
concept A = X<T> && Y<T>;
concept B = X<T> || Y<T>;
concept C = !X<T>;
// 🔹 requires 表达式模板
template<typename T>
concept MyConcept = requires(T t) {
// 检查表达式
{ t.method() };
// 检查表达式 + 返回类型
{ t.get_value() } -> std::convertible_to<int>;
// 检查嵌套类型
typename T::value_type;
// 嵌套 requires
requires std::is_copy_constructible_v<T>;
};
// 🔹 使用方式汇总
template<typename T> requires MyConcept<T> // 前置 requires
void f1(T);
template<typename T>
void f2(T) requires MyConcept<T>; // 后置 requires
template<MyConcept T> // concept 作为类型约束
void f3(T);
MyConcept auto f4(MyConcept auto x); // abbreviated function template
三、两者关系
类模板、函数模板(包括通用 lambda 表达式)和其他模板化函数(通常是类模板的成员)可以与约束 相关联,该约束规定了对模板参数的要求,可用于选择最合适的函数重载和模板特化。
此类需求的命名集合称为概念 。每个概念都是一个谓词,在编译时进行求值,并成为模板接口的一部分,用作约束条件:
#include <cstddef>
#include <concepts>
#include <functional>
#include <string>
// 声明概念“Hashable”,任何类型“T”都满足该概念
// 使得对于类型为“T”的值“a”,表达式 std::hash<T>{}(a)
// 可以编译,并且其结果可以转换为 std::size_t
template < typename T >
concept Hashable = requires ( T a )
{
{ std:: hash < T > { } ( a ) } - > std:: convertible_to < std:: size_t > ;
} ;
struct meow { } ;
// 受约束的 C++20 函数模板:
template < Hashable T >
void f ( T ) { }
//
// 应用相同约束的其他方法:
// template<typename T>
// requires Hashable<T>
// void f(T) {}
//
// template<typename T>
// void f(T) requires Hashable<T> {}
//
// void f(Hashable auto /* parameter-name */) {}
int main ( )
{
using std :: operator "" s ;
f ( "abc" s ) ; // OK,std::string 满足 Hashable 条件
// f(meow{}); // 错误:meow 不满足 Hashable 条件
}
违反约束条件的情况会在编译时,也就是模板实例化过程的早期阶段被检测到,从而生成易于理解的错误消息:
std:: list <int> l = { 3 , -1 , 10 } ;
std:: sort ( l.begin() , l.end()) ; //典型的编译器诊断(不包含概念):
//二元表达式的操作数无效('std::_List_iterator<int>' and // 'std::_List_iterator<int>')
// std::__lg(__last - __first) * 2);
// ~~~~~~ ^ ~~~~~~~
// ... 50 行输出 ... //
// 典型的编译器诊断(包含概念):// 错误:无法使用 std::_List_iterator<int> 调用 std::sort
// 注意:概念 RandomAccessIterator<std::_List_iterator<int>> 未满足
概念的目的是对语义类别(例如数字、范围、常规函数)进行建模,而不是对语法限制(例如有加、数组)进行建模。根据ISO C++ 核心指南 T.20,“能够指定有意义的语义是真正概念的决定性特征,而不是语法约束。”
综合实践样例
① 实践中解决哈希表对key参数要求的问题
#include <cstddef>
#include <concepts>
#include <functional>
#include <string>
// 概念 HashType 的声明可以被符合以下条件的任意类型 T 满⾜:
// 对于 T 类型的值 a,表达式 std::hash<T>{}(a) 可以编译并且它的结果可以转换到
std::size_t
template<typename T>
concept HashType = requires(T a)
{
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
// 受约束的 C++20 类模板
template<HashType K, class HashFunc = std::hash<K>>
class hashmap
{
public:
bool insert(const K& key)
{
size_t hashi = HashFunc()(key);
}
};
int main()
{
hashmap<int> hm1;
hashmap<std::string> hm2;
//hashmap<std::pair<std::string, int>> hm3;
return 0;
}
② 多种复杂条件组合和嵌套的
#include <concepts>
#include <iostream>
#include <vector>
#include <mutex>
template<typename T>
concept PrintableRange = requires(T t) {
{ t.begin() }->std::same_as<typename T::iterator>;
{ t.end() }->std::same_as<typename T::iterator>;
requires std::integral<typename T::value_type>;
// 对T内部的内容的要求,所以要在嵌套定义⼀个requires的参数
requires requires(typename T::value_type e) { std::cout << e; };
};
template<PrintableRange R>
void print_range(const R& r) {
for (const auto& e : r) {
std::cout << e << ' ';
}
std::cout << '\n';
}
int main() {
std::vector<int> v{ 1, 2, 3 };
print_range(v); // OK
std::vector<std::mutex> vm;
print_range(vm); // 错误: 不满⾜PrintableRange
return 0;
}
③ 使用概念解决一些模板匹配问题
#include <concepts>
#include <iostream>
template<typename T>
concept IteratorType = requires(T it) {
// 要求只支持前置++和解引⽤
++it;
*it;
};
namespace bit
{
template<class T>
class vector
{
public:
typedef T* iterator;
// 强制编译器⽣成默认构造
vector() = default;
// 函数模板
// 任意类型容器迭代器初始化
//template <IteratorType InputIterator>
template <typename InputIterator>
vector(InputIterator first, InputIterator last){
while (first != last){
push_back(*first);
++first;
}
}
vector(size_t n, const T& val = T()){}
void push_back(const T& x){}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
}
int main()
{
bit::vector<int> v7(10, 1);
bit::vector<size_t> v9(10, 1);
return 0;
}
【★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【C++】的内容,请持续关注我 !!

更多推荐


所有评论(0)