【C++ 17 新特性 】拥抱现代C++:深入C++17特性以获得更高效、更安全的代码

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 【C++ 17 新特性 】拥抱现代C++:深入C++17特性以获得更高效、更安全的代码

1. 引言

C++17的背景与目标

C++17是C++编程语言的一个重要版本,于2017年12月正式发布。它在C++11和C++14的基础上继续完善和扩展C++语言特性和标准库组件。C++17的主要目标是进一步提高C++程序的性能、可用性和安全性,同时引入一些新的编程范式,使C++编程更加现代化和高效。

C++17包含许多新特性,如if constexprstructured bindingsconstexpr lambda等,以及标准库的扩展,如std::optionalstd::variantstd::filesystem等。这些特性旨在简化C++代码的编写,提高代码质量和运行时性能。

C++17相对于C++14的改进与新增特性概述

C++17在C++14的基础上引入了许多改进和新增特性。主要的语言特性和库扩展包括:

  1. if constexpr:允许编译时条件编译,简化模板元编程。
  2. structured bindings:简化多返回值的处理和局部变量的声明。
  3. constexpr lambda:允许在编译时使用Lambda表达式。
  4. inline variables:允许在头文件中定义内联变量,简化类静态成员的使用。
  5. std::optional:提供可选值的封装,避免空指针问题。
  6. std::variant:支持类型安全的多类型容器。
  7. std::any:提供类型擦除功能,允许存储任意类型的对象。
  8. std::filesystem:提供跨平台文件系统操作支持。
  9. std::invoke:统一对函数、函数指针、成员函数指针等可调用对象的调用语法。
  10. std::string_view:高效地引用字符串片段,提高字符串处理性能。
  11. std::shared_mutexstd::shared_lock:提供共享锁定机制,提高并发性能。
  12. std::byte:提供类型安全的字节类型,用于表示原始内存数据。

以上特性和库扩展为C++编程带来了更强大的功能和更简洁的语法,使C++代码更加优雅、可读和高效。

2. 结构化绑定

结构化绑定简介

结构化绑定(Structured Bindings)是C++17引入的一种新语法特性,它允许你将结构化数据(例如数组、元组和结构体)分解为单独的变量。这种语法简化了访问和操作结构化数据的成员的过程,使得代码更加简洁和可读。

用法与示例

使用结构化绑定,你可以将一个元组或结构体的成员绑定到独立的变量中。以下是结构化绑定的一些示例:

#include <iostream>
#include <tuple>
#include <map>
int main() {
    // 使用结构化绑定从元组中解析变量
    std::tuple<int, double, std::string> t = {42, 3.14, "Hello"};
    auto [a, b, c] = t;
    std::cout << a << ", " << b << ", " << c << std::endl;
    // 使用结构化绑定从map遍历中解析键值对
    std::map<int, std::string> m = {{1, "One"}, {2, "Two"}, {3, "Three"}};
    for (const auto& [key, value] : m) {
        std::cout << key << " -> " << value << std::endl;
    }
    // 使用结构化绑定从结构体中解析成员
    struct Point {
        int x;
        int y;
    };
    Point p = {1, 2};
    auto [x, y] = p;
    std::cout << "Point: (" << x << ", " << y << ")" << std::endl;
    return 0;
}

结构化绑定与自定义类型

对于自定义类型,你可以通过实现get函数和特化std::tuple_sizestd::tuple_element来支持结构化绑定。

#include <tuple>
class MyType {
public:
    int a = 1;
    double b = 2.0;
    std::string c = "Three";
};
// 提供get函数
template <std::size_t N>
decltype(auto) get(const MyType& mt) {
    if constexpr (N == 0) {
        return mt.a;
    } else if constexpr (N == 1) {
        return mt.b;
    } else {
        return mt.c;
    }
}
// 特化std::tuple_size
namespace std {
    template <>
    struct tuple_size<MyType> : std::integral_constant<std::size_t, 3> {};
}
// 特化std::tuple_element
namespace std {
    template <>
    struct tuple_element<0, MyType> {
        using type = int;
    };
    template <>
    struct tuple_element<1, MyType> {
        using type = double;
    };
    template <>
    struct tuple_element<2, MyType> {
        using type = std::string;
    };
}
int main() {
    MyType mt;
    auto [my_a, my_b, my_c] = mt;  // 现在MyType支持结构化绑定
    return 0;
}

3. if constexpr

编译时if语句简介

if constexpr是C++17引入的编译时if语句,它在编译时执行条件检查,根据条件的真假决定是否保留相应的分支代码。这种特性使得在编写模板函数和模板类时可以根据模板参数类型选择性地保留代码,从而简化模板元编程,并提高生成的代码的效率。

使用if constexpr简化模板元编程的示例

以下示例展示了如何使用if constexpr简化模板元编程:

#include <iostream>
#include <type_traits>
template <typename T>
auto print_type_info(const T& t) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << t << " is an integral number." << std::endl;
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << t << " is a floating-point number." << std::endl;
    } else {
        std::cout << "Unknown type." << std::endl;
    }
}
int main() {
    int i = 42;
    double d = 3.14;
    std::string s = "hello";
    print_type_info(i);
    print_type_info(d);
    print_type_info(s);
    return 0;
}

在这个示例中,print_type_info函数根据参数类型选择性地执行不同的输出操作。if constexpr根据类型特征值决定保留哪个分支代码。

if constexpr与SFINAE的关系

SFINAE(Substitution Failure Is Not An Error,替换失败不是错误)是C++模板元编程中的一种技巧,用于在编译时为模板函数和模板类生成合适的实例。SFINAE基于编译器在遇到无法完成替换的模板参数时不产生错误,而是退化到其他可用的模板。

if constexpr与SFINAE在某种程度上有类似的作用,都可以实现根据模板参数类型选择性地执行代码。然而,if constexpr的语法更加简洁和直观,使得在某些场景下,你不再需要复杂的SFINAE技巧。

尽管如此,if constexpr并不能完全替代SFINAE。在某些情况下,例如需要根据模板参数的某种特性选择不同的函数重载时,SFINAE仍然是必要的。在C++20中,引入了concept特性,使得SFINAE技巧变得更加简单和直观。

4. 内联变量

内联变量的概念与用途

内联变量(Inline Variables)是C++17引入的一种新特性,它允许在头文件中定义具有唯一地址的变量。内联变量的声明使用inline关键字进行修饰。这种特性使得跨多个源文件的共享变量更加简单和可靠。

在C++17之前,为了实现跨多个源文件的共享变量,通常需要在一个源文件中定义变量,然后在其他源文件中使用extern关键字声明该变量。这种方法容易引发链接错误和重复定义的问题。

使用内联变量,你可以在头文件中直接定义变量,同时避免链接错误和重复定义问题。

内联变量与C++11 constexpr变量的区别

内联变量与C++11中的constexpr变量有一定的相似性,因为它们都可以在头文件中定义。然而,它们之间还是有以下区别:

  1. constexpr变量必须是编译时常量,而内联变量没有这个限制。内联变量可以是非常量,并且可以在运行时进行修改。
  2. constexpr变量在每个使用它的源文件中都有一个独立的实例,这些实例在编译时被替换为常量值。而内联变量在程序中具有唯一的地址。

使用内联变量解决链接问题的示例

假设你有一个项目,其中有多个源文件需要共享一个全局计数器。在C++17之前,你需要这样实现

// counter.h
extern int counter; // 在其他源文件中声明变量
// counter.cpp
int counter = 0; // 在一个源文件中定义变量
// main.cpp
#include "counter.h"
// 在main.cpp中使用counter

使用C++17的内联变量,你可以直接在头文件中定义共享变量:

// counter.h
inline int counter = 0; // 使用内联变量在头文件中定义变量
// main.cpp
#include "counter.h"
// 在main.cpp中使用counter,无需额外的链接操作

5. 基于文件系统的库

std::filesystem库简介

C++17引入了一个新的库std::filesystem,用于处理文件系统相关操作。该库提供了一系列实用的类和函数,用于查询、遍历和操作文件及目录。std::filesystem库采用跨平台设计,支持各种操作系统(如Windows、macOS、Linux等)。

常用文件系统操作

std::filesystem库包含以下常用的文件系统操作:

  • 查询文件或目录的属性,如大小、权限、创建时间等。
  • 操作文件路径,如拼接、拆分、解析等。
  • 遍历目录,包括递归和非递归方式。
  • 创建和删除文件或目录。
  • 文件重命名和移动。

使用std::filesystem库的示例

以下示例展示了如何使用std::filesystem库进行简单的文件和目录操作:

#include <iostream>
#include <filesystem>
int main() {
    namespace fs = std::filesystem;
    // 创建一个新目录
    fs::create_directory("test_directory");
    // 在新目录下创建一个文件
    fs::path file_path = "test_directory/test_file.txt";
    std::ofstream file(file_path);
    file << "Hello, Filesystem!";
    file.close();
    // 查询文件大小
    std::uintmax_t file_size = fs::file_size(file_path);
    std::cout << "File size: " << file_size << " bytes" << std::endl;
    // 重命名文件
    fs::path new_file_path = "test_directory/renamed_file.txt";
    fs::rename(file_path, new_file_path);
    // 删除文件和目录
    fs::remove(new_file_path);
    fs::remove("test_directory");
    return 0;
}

在这个示例中,我们创建了一个新目录,然后在其中创建了一个文件,并写入一些内容。接着,我们查询了文件的大小,将文件重命名,最后删除了文件和目录。这仅仅是std::filesystem库的冰山一角,它还包含许多其他实用的功能。

6. 并行算法

C++17中并行算法的引入

C++17标准引入了并行算法,这些并行算法是对现有STL算法的扩展,它们能够利用多核处理器的并行计算能力。通过使用并行算法,你可以提高程序的性能,使其在多核处理器上运行得更快。这些并行算法被添加到头文件中。

std::execution策略

并行算法通过std::execution策略参数来指定执行方式。C++17定义了以下三种执行策略:

  1. std::execution::seq:顺序执行策略,与传统的STL算法相同,不涉及并行计算。
  2. std::execution::par:并行执行策略,允许算法在多个线程上并行执行。
  3. std::execution::par_unseq:并行+向量化执行策略,允许算法在多个线程上并行执行,并充分利用CPU的向量化能力(如SIMD指令集)。

使用并行算法加速计算的示例

以下示例演示了如何使用并行算法对一组整数进行排序:

#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <random>
#include <chrono>
int main() {
    // 生成一个包含1000000个随机整数的向量
    std::vector<int> data(1000000);
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(1, 1000000);
    std::generate(data.begin(), data.end(), [&]() { return dis(gen); });
    // 使用顺序算法排序
    auto seq_data = data;
    auto start = std::chrono::high_resolution_clock::now();
    std::sort(std::execution::seq, seq_data.begin(), seq_data.end());
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsed = end - start;
    std::cout << "Sequential sort time: " << elapsed.count() << " seconds" << std::endl;
    // 使用并行算法排序
    auto par_data = data;
    start = std::chrono::high_resolution_clock::now();
    std::sort(std::execution::par, par_data.begin(), par_data.end());
    end = std::chrono::high_resolution_clock::now();
    elapsed = end - start;
    std::cout << "Parallel sort time: " << elapsed.count() << " seconds" << std::endl;
    return 0;
}

在这个示例中,我们首先使用顺序算法对整数向量进行排序,然后使用并行算法进行排序。通过比较两者的执行时间,我们可以看到并行算法通常可以显著提高排序性能。需要注意的是,并行算法的性能提升取决于具体硬件和编译器支持情况。

7. std::optional

std::optional简介

C++17引入了std::optional类模板,用于表示一个可能有值,也可能没有值的对象。std::optional对于表示可能失败的计算或那些可能没有合法值的情况特别有用。std::optional提供了一种类型安全的方式来表示这种情况,避免了使用指针或特殊值来表示缺失值的问题。

使用std::optional表示可选值的示例

以下示例演示了如何使用std::optional表示一个可能有值,也可能没有值的计算结果:

#include <iostream>
#include <optional>
#include <cmath>
// 计算平方根,当输入值为负数时返回std::nullopt
std::optional<double> sqrt_optional(double x) {
    if (x >= 0) {
        return std::sqrt(x);
    } else {
        return std::nullopt;
    }
}
int main() {
    auto result1 = sqrt_optional(4.0);
    auto result2 = sqrt_optional(-1.0);
    if (result1) {
        std::cout << "Square root of 4.0 is: " << *result1 << std::endl;
    } else {
        std::cout << "Cannot compute square root of 4.0" << std::endl;
    }
    if (result2) {
        std::cout << "Square root of -1.0 is: " << *result2 << std::endl;
    } else {
        std::cout << "Cannot compute square root of -1.0" << std::endl;
    }
    return 0;
}

在这个示例中,我们定义了一个函数sqrt_optional,它返回一个std::optional。当输入值为正数或零时,它返回平方根的值;当输入值为负数时,它返回std::nullopt,表示没有合法的结果。

std::optional与指针、异常的比较

  1. 指针:在C++中,指针常被用于表示可选值,比如用空指针表示没有值。然而,使用指针可能会导致安全问题,如悬挂指针、空指针解引用等。而std::optional为表示可选值提供了一种类型安全的替代方案,避免了这些问题。
  2. 异常:在某些情况下,异常可以用于表示函数执行失败或无法产生合法值。但异常通常用于处理错误情况,而非表示可选值。此外,异常在某些场景下可能导致性能下降。与异常相比,std::optional在表示可选值时具有更清晰的语义,且不会引入额外的性能开销。

8. std::variant

std::variant简介

C++17引入了std::variant类模板,它是一个类型安全的联合体。std::variant可以存储其类型参数中的任何一个类型,并在运行时保持其当前类型的信息。std::variant对于在运行时处理多种类型的数据非常有用,它提供了一种类型安全且灵活的方式来表示和处理不同类型的数据。

使用std::variant的示例

以下示例演示了如何使用std::variant存储多种类型的数据:

#include <iostream>
#include <variant>
#include <string>
int main() {
    std::variant<int, double, std::string> my_variant;
    my_variant = 42;
    std::cout << "my_variant contains an int: " << std::get<int>(my_variant) << std::endl;
    my_variant = 3.14;
    std::cout << "my_variant contains a double: " << std::get<double>(my_variant) << std::endl;
    my_variant = "hello";
    std::cout << "my_variant contains a string: " << std::get<std::string>(my_variant) << std::endl;
    return 0;
}

在这个示例中,我们定义了一个std::variant类型的对象my_variant,可以存储intdoublestd::string类型的数据。然后,我们为my_variant分别赋值并输出结果。

std::variant与其他联合类型的比较

  1. C联合体:C语言中的联合体(union)是一种灵活的数据结构,允许在同一内存区域中存储不同类型的数据。然而,C联合体在使用时存在类型安全问题,因为它无法保留当前存储类型的信息。相比之下,std::variant提供了类型安全的保证,并能自动处理类型间的转换和访问。
  2. **void***指针void*指针可以用于表示任何类型的数据,但它不提供类型信息,因此在使用void*指针时,需要手动管理类型转换和内存管理。相比之下,std::variant可以自动处理类型转换和内存管理,并提供了类型安全的访问方式。
  3. boost::variant:在C++17之前,boost::variant是C++程序员常用的类型安全联合体实现。std::variant的设计借鉴了boost::variant,它们的功能和用法非常相似。然而,std::variant作为C++17标准库的一部分,不再需要依赖Boost库。

9. std::any

std::any简介

std::any是C++17中引入的一个类型安全的通用类型容器。它可以存储任意类型的数据,并在运行时保持其类型信息。std::any对于在运行时处理多种类型的数据非常有用,尤其是在类型信息不确定的情况下。

使用std::any存储任意类型的示例

以下示例演示了如何使用std::any存储和访问任意类型的数据:

#include <iostream>
#include <any>
#include <string>
int main() {
    std::any my_any;
    my_any = 42;
    std::cout << "my_any contains an int: " << std::any_cast<int>(my_any) << std::endl;
    my_any = 3.14;
    std::cout << "my_any contains a double: " << std::any_cast<double>(my_any) << std::endl;
    my_any = std::string("hello");
    std::cout << "my_any contains a string: " << std::any_cast<std::string>(my_any) << std::endl;
    return 0;
}

在这个示例中,我们定义了一个std::any类型的对象my_any,可以存储任意类型的数据。然后,我们为my_any分别赋值并使用std::any_cast来访问和输出结果。

std::any与其他通用类型容器的比较

  1. **void***指针void*指针可以用于表示任何类型的数据,但它不提供类型信息。因此,在使用void*指针时,需要手动管理类型转换和内存管理。相比之下,std::any可以自动处理类型转换和内存管理,并提供了类型安全的访问方式。
  2. boost::any:在C++17之前,boost::any是C++程序员常用的类型安全通用类型容器。std::any的设计借鉴了boost::any,它们的功能和用法非常相似。然而,std::any作为C++17标准库的一部分,不再需要依赖Boost库。
  3. std::variantstd::variant是C++17中的另一个类型安全的通用类型容器,但它仅限于存储预定义类型列表中的类型。相比之下,std::any可以存储任意类型的数据。然而,std::any的灵活性带来了额外的性能开销,因此在类型信息明确的情况下,使用std::variant可能更合适。

10. 更多语言特性与库扩展

无序容器节点的提取和插入

C++17为std::unordered_mapstd::unordered_setstd::unordered_multimapstd::unordered_multiset提供了节点提取和插入功能。这些操作允许我们在不复制元素的情况下高效地将元素从一个容器移动到另一个容器。以下是一个示例:

#include <iostream>
#include <unordered_set>
int main() {
    std::unordered_set<int> set1{1, 2, 3, 4};
    std::unordered_set<int> set2{5, 6, 7, 8};
    auto node = set1.extract(2); // 提取节点
    set2.insert(std::move(node)); // 插入节点到set2
    for (const auto &elem : set2) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    return 0;
}

std::string_view

std::string_view是C++17引入的一个轻量级字符串视图,它允许我们在不创建新字符串的情况下操作字符串片段。std::string_view主要用于提高性能和降低内存消耗。以下是一个示例:

#include <iostream>
#include <string_view>
void print_string_view(std::string_view sv) {
    std::cout << sv << std::endl;
}
int main() {
    std::string s = "hello, world!";
    std::string_view sv(s);
    print_string_view(sv.substr(0, 5)); // 输出 "hello"
    return 0;
}

std::invoke与函数包装器

C++17中的std::invoke是一个通用的函数调用实用程序,它可以用于调用普通函数、成员函数、Lambda表达式和函数对象。std::invoke的一个主要用途是与std::functionstd::bind等函数包装器配合使用。以下是一个示例:

#include <iostream>
#include <functional>
void print(int x) {
    std::cout << x << std::endl;
}
struct Printer {
    void operator()(int x) const {
        std::cout << x << std::endl;
    }
};
int main() {
    auto lambda = [](int x) { std::cout << x << std::endl; };
    std::invoke(print, 42); // 调用普通函数
    std::invoke(lambda, 42); // 调用Lambda表达式
    std::invoke(Printer{}, 42); // 调用函数对象
    return 0;
}

constexpr Lambda表达式

C++17允许在Lambda表达式中使用constexpr关键字。这意味着Lambda表达式可以在编译时执行,从而提高运行时性能。以下是一个示例:

constexpr auto square = [](int x) { return x * x; };
int main() {
    constexpr int result = square(4); // 编译时执行
    static_assert(result == 16, "Error: Incorrectsquare computation!");
    return 0;
   }

其他实用库特性

C++17还引入了其他实用的库特性,如std::clampstd::scoped_lockstd::apply等。以下是这些特性的简要介绍:

  • std::clamp:用于将值限制在指定范围内。例如,std::clamp(x, low, high)将确保返回的值不小于low且不大于high
  • std::scoped_lock:允许同时锁定多个互斥锁,避免死锁。例如,std::scoped_lock lock(mutex1, mutex2);将锁定mutex1mutex2,并在离开作用域时解锁。
  • std::apply:允许将元组的元素作为参数传递给函数。例如,std::apply(func, args)将使用args元组的元素调用func函数。
#include <iostream>
#include <tuple>
#include <functional>
int add(int a, int b) {
    return a + b;
}
int main() {
    auto args = std::make_tuple(1, 2);
    int result = std::apply(add, args); // 调用add(1, 2)
    std::cout << "1 + 2 = " << result << std::endl;
    return 0;
}

11. 结论与展望

C++17特性在现代C++编程中的价值与应用

C++17为现代C++编程带来了许多新特性和库扩展,这些新特性提高了代码的可读性、可维护性和性能。这些特性在很多方面帮助我们编写更简洁、高效且安全的代码,提高了整体的开发效率。

C++20与C++23中更多的语言特性与库扩展

C++20和C++23继续为我们带来更多的语言特性和库扩展。例如,C++20引入了概念(concepts)、范围(ranges)、协程(coroutines)、模块(modules)等重要特性。这些特性将进一步改善C++编程的体验。C++23预计将引入更多有趣的特性,如线性代数库、网络库、扩展的并发支持等。

保持对C++标准发展的关注与学习

作为一名C++程序员,保持对C++标准发展的关注与学习是非常重要的。了解新特性以及如何正确地使用它们有助于我们编写高质量的代码。随着C++的不断发展,学习新特性、实践新技术并将其应用到实际工作中是我们持续提高自己的关键。

目录
相关文章
|
26天前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
100 59
|
1月前
|
Linux C语言 C++
vsCode远程执行c和c++代码并操控linux服务器完整教程
这篇文章提供了一个完整的教程,介绍如何在Visual Studio Code中配置和使用插件来远程执行C和C++代码,并操控Linux服务器,包括安装VSCode、安装插件、配置插件、配置编译工具、升级glibc和编写代码进行调试的步骤。
174 0
vsCode远程执行c和c++代码并操控linux服务器完整教程
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
20天前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
31 0
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析继承机制(三)
【C++】面向对象编程的三大特性:深入解析继承机制
|
1天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
10 4
|
24天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
22 4
|
24天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
20 4
下一篇
无影云桌面