• 现代C++(Modern C++)基本用法实践:N、其他零散的常用特性


    概述

    这一篇简单介绍一些其他的比较实用的特性,如果读者想了解现代C++的全部特性,参考:cpp reference

    其他特性

    预置和弃置函数default&delete

    在 C++11 中引入了 defaultdelete 关键字,允许程序员更加明确地控制类的默认操作(如默认构造函数,拷贝构造函数,拷贝赋值运算符,析构函数等)。

    default

    default 关键字用于明确地要求编译器生成默认的实现。例如,如果你想要一个类拥有默认的构造函数,你可以这样做:

    class Obj {
    public:
        Obj() = default;  // 使用编译器生成的默认构造函数
    };
    

    delete

    delete 关键字用于禁止编译器生成默认的实现。例如,如果你不希望你的类被拷贝,你可以这样做:

    class Obj {
    public:
        Obj(const Obj&) = delete;  // 禁止拷贝构造函数
        Obj& operator=(const Obj&) = delete;  // 禁止拷贝赋值运算符
    };
    

    在这个例子中,如果你尝试拷贝 Obj 的实例,编译器将会报错。

    这两个关键字可以让你更明确地控制类的行为,防止编译器生成你不希望的默认操作。

    继承和多态的控制final&override

    override

    override 关键字用于明确表示一个虚函数覆盖了它的基类中的版本。这可以帮助编译器检查你的代码,防止你因为拼写错误或参数不匹配而无意中没有覆盖基类的函数。例如:

    class Base {
    public:
        virtual void foo(int) {}
    };
    
    class Derived : public Base {
    public:
        void foo(int) override;  // 明确表示这个函数覆盖了基类的版本
    };
    

    final

    final 关键字可以用于类或虚函数。当用于类时,它表示这个类不能被继承。例如:

    class Base final {};  // 这个类不能被继承
    
    class Derived : public Base {};  // 错误:Base 是 final 的
    

    final 用于虚函数时,它表示这个函数在派生类中不能被覆盖。例如:

    class Base {
    public:
        virtual void foo() final;  // 这个函数不能被覆盖
    };
    
    class Derived : public Base {
    public:
        void foo();  // 错误:Base::foo 是 final 的
    };
    

    这两个关键字可以更明确地控制类的继承和多态行为,防止错误的覆盖或继承。

    有作用域的枚举

    在 C++11 之前,枚举类型的值可以隐式转换为整数,而且枚举类型的成员在枚举类型的作用域之外是可见的。这可能会导致命名冲突和类型安全问题。

    作用域枚举通过使用 enum class 关键字来定义,如下所示:

    enum class Color {
        Red,
        Green,
        Blue
    };
    

    作用域枚举的成员只能通过作用域解析运算符 :: 来访问,这可以避免命名冲突。例如,不能直接写 Red,而应该写 Color::Red

    此外,作用域枚举的值不能隐式转换为整数,这有助于提高类型安全

    列表初始化

    C++11 引入了一种新的初始化语法,通常被称为列表初始化或统一初始化。这种语法使用花括号 {} 来初始化对象,可以用于几乎所有的初始化情况。

    如:

    int a = {5};  // 初始化基本类型
    std::string s = {"hello"};  // 初始化类类型
    int arr[] = {1, 2, 3, 4, 5};  // 初始化数组
    std::vector<int> v = {1, 2, 3, 4, 5};  // 初始化容器
    struct Point {int x, y;} p = {5, 10};  // 初始化聚合类型
    

    列表初始化有几个优点:

    1. 统一的语法:列表初始化可以用于所有的初始化情况,包括基本类型、数组、容器、聚合类型等。

    2. 防止窄化转换:列表初始化不允许窄化转换,即从一个类型到另一个类型的转换可能丢失信息。如,试图用浮点数初始化一个整数,或者用一个大的整数初始化一个小的整数,这样的代码将无法通过编译。

    int a = {3.14};  // 错误:窄化转换
    char c = {300};  // 错误:窄化转换
    
    1. 初始化类的成员:在类的构造函数的初始化列表中,可以使用列表初始化来初始化类的成员。
    class MyClass {
    public:
        std::vector<int> v;
        MyClass() : v{1, 2, 3, 4, 5} {}  // 使用列表初始化来初始化类的成员
    };
    

    列表初始化是一种非常有用的特性,可以帮助你编写更清晰、更安全的代码。

    nullptr 空指针

    在C++11之前,我们通常使用NULL来表示空指针。然而,NULL其实就是整数0,这可能会导致一些问题。例如,当函数重载中有一个接受int参数的版本和一个接受指针参数的版本时,如果你传递NULL给这个函数,编译器会选择接受int参数的版本,这可能不是你想要的结果。

    为了解决这个问题,C++11引入了nullptr关键字来表示空指针。nullptr的类型是nullptr_t,它可以隐式转换为所有的指针类型,但不能转换为其他的类型。这使得nullptr在函数重载中的行为更符合预期。

    下面是一个例子:

    void f(int) {
        std::cout << "f(int) called" << std::endl;
    }
    
    void f(char*) {
        std::cout << "f(char*) called" << std::endl;
    }
    
    int main() {
        f(NULL);      // 输出 "f(int) called"
        f(nullptr);   // 输出 "f(char*) called"
        return 0;
    }
    

    在这个例子中,当你传递NULLf函数时,编译器选择了接受int参数的版本。但是当你传递nullptrf函数时,编译器选择了接受指针参数的版本。这是因为nullptr的类型是nullptr_t,它可以隐式转换为char*,但不能转换为int

    因此,C++11推荐使用nullptr来表示空指针,而不是NULL或者0。

    noexcept 不会抛出异常承诺

    noexcept是C++11引入的一个新关键字,用于指定函数是否会抛出异常。如果一个函数被声明为noexcept,那么它保证不会抛出任何异常。如果在运行时该函数确实抛出了异常,那么程序将调用std::terminate来立即结束执行。

    如:

    void f() noexcept {
        // 这个函数保证不会抛出任何异常
    }
    
    void g() {
        throw std::runtime_error("An error occurred");  // 这个函数可能会抛出异常
    }
    

    使用noexcept关键字有两个主要的好处:

    1. 性能优化:如果编译器知道一个函数不会抛出异常,那么它可能会生成更优化的代码。例如,一些需要异常安全保证的操作(如对象的移动)在知道不会抛出异常的情况下,可以被编译器优化。

    2. 提供更清晰的接口:通过在函数声明中使用noexcept关键字,你可以明确地告诉调用者该函数不会抛出任何异常。这可以帮助调用者更好地理解函数的行为,并据此来编写代码。

    需要注意的是,noexcept是一个承诺,如果声明一个函数为noexcept,那么你需要确保它在任何情况下都不会抛出异常。如果不能保证这一点,最好不要使用noexcept关键字。

    另外,你也可以使用noexcept运算符来检查一个表达式是否保证不抛出异常:

    static_assert(noexcept(f()));  // 编译时检查f()是否不抛出异常
    

    三路比较(c++ 20)

    假设我们有一个 Person 类,它有一个名字和年龄属性。我们想要比较两个 Person 对象,首先比较他们的名字,如果名字相同,再比较他们的年龄。

    在C++20中,我们可以使用三路比较运算符 <=> 来实现这个比较逻辑

    #include 
    #include 
    
    struct Person {
        std::string name;
        int age;
    
        auto operator<=>(const Person& other) const {
            if (auto cmp = name <=> other.name; cmp != 0) {
                return cmp;
            }
            return age <=> other.age;
        }
    };
    
    #include 
    
    int main() {
        Person alice{"Alice", 20};
        Person bob{"Bob", 20};
        Person charlie{"Charlie", 25};
    
        std::cout << ((alice <=> bob) < 0 ? "Alice < Bob" : "Alice >= Bob") << std::endl;
        std::cout << ((alice <=> charlie) < 0 ? "Alice < Charlie" : "Alice >= Charlie") << std::endl;
        std::cout << ((bob <=> charlie) < 0 ? "Bob < Charlie" : "Bob >= Charlie") << std::endl;
    }
    

    operator<=> 首先比较 name,如果 name 不相同,就返回 name 的比较结果;如果 name 相同,就比较 age,并返回 age 的比较结果。我们也可以使用auto operator<=>(const Person& other) const = default让编译器生成,它会按照成员的声明顺序比较每个成员。

    值得一提的特性

    • alignof 与 alignas 内存对齐相关
    • static_assert 静态断言
    • c++17:if和switch语句中进行初始化,如if (int i = f(); i > 10) {}switch (int i = f(); i) {}
    • 聚合初始化(花括号),c++20允许实用圆括号

    C++20 一些新的、未实践但感觉有用的特性

    • 协程
    • 模块
    • 限定与概念
    • 缩略函数模板
    • DR :数组 new 可推导数组长度
  • 相关阅读:
    广告行业中那些趣事系列56:超实用的多模态学习模型VILT源码实践
    WIN32 动态 UAC 提权
    Okhttp通用工具类
    C# OpenVINO 人脸识别
    ServletConfig接口干什么
    java计算机毕业设计农田节水灌溉监测系统源码+程序+lw文档+mysql数据库
    特征工程完整指南 - 第二部分
    智慧用电监控装置:引领0.4kV安全用电新时代
    springboot整合全文搜索引擎Elasticsearch Spring Boot 28
    SpringBoot中任务是什么/Quartz和SpringTask在Spring Boot中怎么使用/SpringBoot怎么给用户发邮件
  • 原文地址:https://www.cnblogs.com/hggzhang/p/17549875.html