• 第10课 面向对象的增强(default/delete、override/final)


    一、default和delete关键字

    (一)编译器提供的“缺省函数

      1.类的成员函数:构造/析构函数、复制构造/复制赋值函数、移动构造/移动赋值函数

      2. 类的全局默认操作函数:operator new/delete、operator,、operator*、operator->、operator->*等。

    (二)“=default”

      1. default:显式指示编译器生成该函数的默认版本,但仅用于类的特殊成员函数(含析构函数)。

      2. 当类中自定义了构造函数后,该类将不再是POD类型(可用is_pod检查),但使用default可“恢复”其POD特质。

      3. default既可以在类体里定义,也可以在类外定义

      (三) “=delete”

      1. 禁止使用某个函数。必须在函数第一次声明的时候将其声明为delete。

      2. 不同于default,任何函数(含非成员函数或模板函数)都可以delete

      3. 在函数重载或模板特化中,可用delete来滤掉一些函数的形参类型,禁止编译器做一些不必要的类型转换或阻止特定的模板实例化

      4. explicit和delete混用会带来混乱。因此在使用delete显式删除时,应该总是避免用explicit来修饰函数,反之亦然。

    【编程实验】default和delete关键字

    #include <iostream>
    using namespace std;
    
    class Widget
    {
    private:
        int data;
    
    public:
        Widget() = default;  //指示编译器提供默认版本(不影响POD特质)
        Widget(int i):data(i){}
        Widget(double d) = delete; //删除double版本
        explicit Widget(char c) = delete; //注意,这里explicit与deltete混用,将产生一些混乱。
    
        Widget(const Widget&) = delete;
        Widget& operator=(const Widget&);//这里没使用default,本例将在类外定义
    
    private:
        //2.3.2 特化版本
        //Widget 类中声明了一个模板函数,当进行模板特化时,要求禁止参数为 void* 的函数调用。
        //本意是按照 C++98 的“私有不实现”思路,将特例化的函数声明为private。但模板特化不能放在
        //类作用域中定义,它必须放在命名空间作用域中定义。见后面类外定义部分
        //template<>
        //void processPointer<void>(void*);  //编译失败
    public:
        //2.3 delete在模板特化中的作用
        //2.3.1 泛化版本
        template<typename T>
        void proccessPointer(T* ptr){}
    };
    
    //在类外使用“=default”来指明使用默认版本
    inline Widget& Widget::operator=(const Widget&) = default;
    
    //2. delete重载函数
    template<>
    void Widget::proccessPointer<void>(void*) = delete; // 仍然是public, 但被delete
    
    void Func(Widget w){}
    
    void overloadFunc(int i) {};
    void overloadFunc(char c) = delete; //显式删除char版本
    
    //4. delete妙用
    //4.1 禁止在堆上创建类对象!
    class NoHeapAlloc
    {
    public:
        void* operator new(std::size_t) = delete; //注意这里!
    };
    
    //4.2 禁止在栈上创建类对象!
    class NoStackAlloc
    {
    public:
        NoStackAlloc()
        {
            cout <<"NoStackAlloc()" << endl;
        }
        ~NoStackAlloc() = delete; //注意这里,将导致无法自动析构函数。
    };
    
    int main()
    {
        cout <<is_pod<Widget>::value << endl;  //0
    
        //1. 测试default与delete
        Widget w;  //ok,调用无参构造函数,己被声明为default;
        Widget w1;
        //Widget w2(w1);   //无法编译通过,因为Widget(const Widget&)己被delete
        //Widget w3 = w1;  //无法编译通过,因为Widget(const Widget&)己被delete
        Widget w4;
        w4 = w1;  //ok,operator=被指显式定为的默认函数
    
        //2. delete重载函数
        //2.1 delete成员函数
        Widget w5(3);
        //Widget w6(1.0);   //error, double版本的构造函数被delete
    
        Func(5);
        //Func(1.0);        //error, 1.0转为Widget需要调用double版本的构造函数,但该函数己delete。
        
        //2.2 delete普通函数 
        overloadFunc(4);
        //overloadFunc('a'); //编译失败,char版本被delete。同时'a'也就没就机会隐式转为int。
    
        //2.3 delete特化的模板函数 
        int* pi = nullptr;
        void* pv = nullptr;
        w5.proccessPointer(pi); //ok;
        //w5.proccessPointer(pv); //error,void*版本的特化函数被delete
    
        //3. explicit与delete的冲突
        //Widget w6('a');    //error,char版本的构造函数被delete
        Func('a');           //编译通过!!!由于char版本的构造函数被声明为explicit,'a'将无法再隐式转为Widget,但
                             //可以隐式转为int,于是调用Widget(int)版本。这与Widget(char)声明为delete的初衷不符!!!
    
        //4. delete的妙用!
        //4.1 仅限在栈上创建对象
        NoHeapAlloc nh;
        //NoHeapAlloc* pnh = new NoHeapAlloc; //编译失败,因为operator new己被delete
    
        //4.2 仅限在堆上创建对象
        void* p = malloc(sizeof(NoStackAlloc));
        //NoStackAlloc nsa;  //编译失败!栈对象,会被自动析构,但析构函数己被delete。
        new (p) NoStackAlloc(); //placement new构造的对象(仍会调用构造函数来初始化对象),但编译器不会为
                                //其调用析构函数(可用于单例模式)。
        return 0;
    }
    /*输出结果
    0
    NoStackAlloc()
    */

    二、override和final关键字

    (一)final两个作用

      1. 作用于类时,可以禁止该类被用作基类。

      2. 作用于虚函数时,会阻止它在派生类中被重写(override)。

    (二)override:强制重写虚函数

      1. 重写必须满足的条件

      (1)基类中的函数必须是虚函数。

      (2)基类和派生类中的函数名字必须完全相同(析构函数除外)、函数形参的类型必须完全相同。

      (3)基类和派生类的函数的常量性必须完全相同

      (4)基类和派生类中的函数返回值和异常规格必须兼容。

      (5)基类和派生类中的函数引用饰词必须完全相同(详见《知识扩展》部分)

      2. 知识扩展——左值/右值引用类型的重载函数(&和&&)

      (1)可以利用引用饰词(&或&&)进行函数的重载。

      (2)左值引用类型的重载函数:形如,retType& func()&。(注意,返回左值,该函数仅在*this是左值时调用)

      (3)右值引用类型的重载函数:形如,retType func()&&。(注意,返回右值,该函数仅在*this是右值时调用)。

      (4)成员函数末尾加引用饰词(&和&&),类似末尾加const情形,后者表明只有*this为const才能调用。

    【编程实验】override和final关键字

    #include <iostream>
    #include <vector>
    #include <boost/type_index.hpp>
    using namespace std;
    using boost::typeindex::type_id_with_cvr;
    
    //辅助类模板,用于打印T的类型
    template <typename T>
    void printType(string s)
    {
        cout << s << " = " << type_id_with_cvr<T>().pretty_name() << endl;
    }
    
    //1. 重写(override)的条件
    class Base
    {
    public:
        virtual void f1() const {};
        virtual void f2(int x) {}
        virtual void f3() & {};
        void f4() const {};
    };
    
    //class Derived : public Base
    //{
    //public:
    //    virtual void f1() override {};  //error,const常量性不同!基类为void f1() const
    //    virtual void f2(unsigned int x) override{} //error,函数形参类型不同(基类为void f2(int x))
    //    virtual void f3()&& override{}  //error,引用饰词不同(基类为void f3() & )
    //    virtual f4() const override{}   //error,非虚函数,不能override
    //};
    
    //2. final用于类和虚函数
    class MathObject  //数学类(接口类)
    {
    public:
        virtual double Arith() = 0; //算术运算
        virtual void Print() = 0;   //打印
    };
    
    class Printable : public MathObject  
    {
    public:
        void Print() final //为了保证打印风格统一,加final以阻止在子类中被重写
        {
            cout << "Output is : " << Arith() << endl;
        }
    };
    
    class Add final: public Printable  //在类上加final表示该类不再被继承
    {
        double x, y;
    public:
        Add(double a, double b):x(a),y(b){}
        double Arith() { return x + y; }
    };
    
    //class Add3 : public Add  //Add被声明为final,无法作为基类
    //{
    //};
    
    //3. 左值 /右值引用类型的重载函数
    class Widget
    {
    public:
        using DataType = std::vector<double>;
    
        //左值引用类型版本
        DataType& data() & //对于*this为左值时,调用该函数。注意返回引用
        {
            cout <<"invoke DataType& data() & " << endl;
            return values;
        }
    
        //右值引用类型版本
        DataType data() && //对于*this为右值时,调用该函数。注意返回右值
        {
            cout << "invoke DataType data() && " << endl;
            return std::move(values);
        }
    private:
        DataType values;
    };
    
    //工厂函数
    Widget makeWidget()
    {
        return Widget();
    }
    int main()
    {
        //左值/右值引用类型的重载函数
        Widget w;
    
        decltype(auto) vals1 = w.data();           //由于w是左值,会调用DataType& data() &
        decltype(auto) vals2 = makeWidget().data();//由于makeWidget()返回临时对象(是个右值),会调用DataType data() &&
    
        printType<decltype(vals1)>("vals1");
        printType<decltype(vals2)>("vals2");
    
        return 0;
    }
    /*输出结果
    invoke DataType& data() &
    invoke DataType data() &&
    vals1 = class std::vector<double,class std::allocator<double> > &
    vals2 = class std::vector<double,class std::allocator<double> >
    */
  • 相关阅读:
    Python时钟,计算程序运行时间
    关于等高线绘制和全平面坐标节点生成
    Springboot配置文件映射
    Docker和Rancher
    ElasticSearch story(二)
    Elastic Story(一)
    由数量众多照片拼贴而成的马赛克图片
    lnmp一键安装包配置laravel项目
    mysql 创建用户与授权、修改密码
    centos 安装 ntpdate 并同步时间
  • 原文地址:https://www.cnblogs.com/5iedu/p/11285983.html
Copyright © 2020-2023  润新知