• 一些C++概念辨析


    const:指针常量与常量指针

    const与指针的结合方式有时候令人迷惑,如:

    int * a0;
    int * const a1;
    int const * a2;
    const int * a3;
    int const * const a4;
    const int * const a5;
    const int const * const a6;
    

    相比于一个裸指针 a0,const 限定了指针的可变性,分为两层:顶层指针的可变性、底层数据的可变性。
    顶层指针的可变性,指的是指针本身的指向是否可变,是否能从指向一个元素变成指向另一个元素,比如是否能用该类型的指针遍历一个数组。
    底层数据的可变性,指的是指针指向的数据是否可变,是否能从一个值原地改变成另一个值,比如常用的qsort传入的cmp函数就限制了不能修改底层数据。

    区分const限制的是顶层指针还是底层数据,要看 const 相对于 * 的位置。 * 将一个基础类型的裸指针分为两部分,左边描述了底层数据的类型,右边描述了这个裸指针的名字。如果 const 靠近左边底层数据类型,则限制的是底层数据不可变;如果 const 靠近右边顶层指针,则限制的是顶层指针不可变。

    所以,a1 是顶层指针不可变,a2 和 a3 是底层数据不可变,a4、a5 和 a6 是指针数据都不可变。当然,最常用的还是 a3 这种形式,用于qsort函数的参数。
    C++里的引用相当于不可变的顶层指针,C++里的const引用,则相当于指针数据都不可边的指针,广泛用于 sort 和 priority_queue。
    也就是说:

    int b;
    int &a = b;       // eq. int * const pa = &b; a = *pa;
    const int &a = b; // eq. const int * const pa = &b; a = *pa;
    

    但是引用有很多优点:写法简单、省掉一次解引用、不会发生解空指针、不会有野指针……

    同名函数:override、overload、hide

    • 函数重载(overload) 指的是函数名相同、参数类型和数量不同的情况。由于历史原因,返回值的不同无法作为区分不同函数的判断依据,所以不允许函数名相同、参数类型数目也相同、仅仅返回值类型不同的函数。此外,重载和是否是虚函数无关,虚函数可以和其他成员函数一起参与重载。如例子中 class A 的几个函数之间,是overload。
    • 函数重写/覆盖(override) 指的是派生类中覆盖了基类中的同名虚函数,二者函数签名完全相同,只有函数体不同。C++关键字 override 可以添加到派生类函数后面,以便利用编译器完成这一检查。例子中,double fun(double, double) 是override。
    • 隐藏(hide) 指的是派生类中的函数屏蔽了基类中的同名函数。包括两种情况:
      1. 第一种情况是参数列表相同,但是基类函数不是虚函数,此时 hide 与 override 的差别在于基类函数是否是虚函数。例子中,void A::fun(int)void B::fun(int) hide。
      2. 第二种情况是参数列表不同,无论基类函数是不是虚函数,都会被隐藏,此时 hide 和 overload 的区别在于两个函数不在同一个类中。例子中,void A::fun()virtual void A::fun(const char*)void B::fun(int) hide。
    class A {
      public:
        int fun() { cout << "fun" << endl; }
        void fun(int a) { cout << a << endl; }
        virtual double fun(double x, double y) { return x + y; }
        virtual void fun(const char* str) { cout << str << endl; }
        static void fun(char);
    };
    void A::fun(char c) { cout << c << c << c << endl; }
    class B : public A {
      public:
        void fun(int b) { cout << b << ' ' << b << endl; }
        virtual double fun(double x, double y) override { return x * y; }
    };
    
    int main() {
        A a;
        a.fun();
        a.fun(3);
        cout << a.fun(2.718, 3.14) << endl;
        a.fun("fun");
        a.fun('c');
        B b;
        b.fun(); // 编译错误,int A::fun() 已被 void B::fun(int) 隐藏
        b.fun(3);
        cout << b.fun(2.718, 3.14) << endl;
        b.fun("fun"); // 编译错误,virtual void A::fun(const char*) 已被 void B::fun(int) 隐藏
        b.fun('c'); // 匹配 void B::fun(int b), 而非 static void A::fun(char);
    };
        return 0;
    }
    

    可以看到,overload 是同一层级内部的水平关系,override 和 hide 是不同层级之间的垂直关系。

    "=":拷贝与赋值

    C++ 中,利用同类型的一个对象来构造另一个对象,有两种不同的形成方式:赋值和拷贝。一个简答的例子如下:

    class A {
      private:
        int x, y;
      public:
        A (const A& a) {}
        A& operator= (const A& a) { return *this; }
    };
    int main() {
        A a;
        A a1 = a;
        A a2;
        a2 = a;
        return 0;
    }
    

    在这里例子中,a1是调用的拷贝构造函数,直接凭空生成一个新对象;a2调用了赋值操作符,在已有对象的基础上进行赋值,修改了原有的对象。

    构造函数:拷贝、移动、转换、委托

    class T {
      private:
        int x, y;
      public:
        T () : T(0, 0) {}                 // 默认构造函数,无参
        explicit T (int r) : T (r, 0) {}  // 转换构造函数,单参,且参数是其他类型
        T (int a, int b) : x(a), y(b) {}  // 初始化构造函数,有参
        T (const T&);            // 拷贝构造函数
        T (T&&);                 // 移动构造函数
        T& operator= (const T&); // 拷贝赋值
        T& operator= (T&&);      // 移动赋值
    }
    

    单个参数的构造函数称为转换构造函数,使用 explicit 修饰之后,可以禁止隐式类型转换,只允许以显式转换构造。建议的做法是总是使用 explicit,这样,T t = 10;之类的语句就无法通过编译了。

    拷贝和赋值的区别前面已经提到过,这里另外介绍两个关键字 defaultdelete。当不指定构造函数的时候,编译器默认提供无参构造函数,以及浅拷贝的拷贝构造函数。当给定了构造函数,编译器就不再提供默认构造函数,但是可以通过使用 T() = default; 来启用编译器提供的构造函数。与之相对的,可以通过 T& operator= (const T&) = delete; 来取消编译器提供的赋值操作。典型应用场景是 unique_ptr 以及单例。

    委托构造函数是另一种特例。早期的时候,每个构造函数都需要手动初始化类的每一个成员,无法一次性指定每个成员的默认值。委托构造函数的写法是,使用一个构造函数写好所有的构造逻辑,其他构造函数只需要调用这个构造函数,达到 委托 的效果。上面的例子中,单参和无参的构造函数,就是委托了双参构造函数来完成工作。

    移动构造,是配合右值引用来实现的。主要使用场景是对象的“搬运”,也就是利用一个将要消失的对象,来创建一个仍要使用的对象。可用于代替RVO。

    结构化绑定 + 右值引用 踩坑

    C++17引入了结构化绑定(structured binding),这种好东西一方面为我们简化代码带来了方便,比如不用写 pairA.first 这种;另一方面与其他特性的结合有时会有一些意想不到的情况发生。
    比如下面这个BFS的例子,类型推导 + 右值引用 + 结构化绑定 会导致内存出错:

    queue<pair<int,int>> q;
    q.push({0, 0});
    while (!q.empty()) {
        auto&& [i, j] = q.front();
        q.pop();
    ...
    }
    

    问题出在 auto&& [i, j] = q.front(); q.pop(); 这两句上,我们使用的是一个引用,但是后来这个元素被 pop 释放掉了,所以会报内存错误。
    使用 auto& [i, j] 也会出错,因为也是引用类型;只有使用 auto [i, j] 做拷贝赋值才不会引起内存错误。

    什么时候用 auto&& 是安全的呢?在变量被使用前不会释放内存的时候,比如 for (auto&& x : vec);或者对象的所有权被直接转移给右值引用的时候 auto&& res = func();

    结构化绑定与原地构造的适用对象

    除了结构化绑定(structured binding),可以将一个结构体拆解开分别赋值之外;C++还加入了一堆 emplace/emplace_back/emplace_front 函数,分别对应于 push/push_back/push_front 系列函数,与之不同的是不适用拷贝赋值的方式,而是直接在容器内“原地构造”省掉了临时对象的开销,基本相当于 push + move 操作。

    什么对象才能用结构化绑定和原地构造呢?
    答案是可以准确知道结构的类型,比如 struct、pair、tuple 等,而 vector 这一类不定长的则不能使用结构化绑定来获取,也不能适用emplace来存入。如以下写法是错误的:

    vector<vector<int>> res; res.emplace_back(2, 3); // can't get { {2,3} } but get { {3, 3} } for it calls vector<int>(n, val)
    auto [start, end] = res[0]; // CE, can't decompose vector with structured binding
    

    与结构化绑定类似,原地构造也是需要知道对象的内存布局,才能调用 统一初始化函数,不然可能调用的某个构造函数。比如上面的例子,vector 的单参和双参构造函数都有其特殊含义,和统一初始化的结构并不一致。

  • 相关阅读:
    hdoj_1800Flying to the Mars
    SPFA模版
    树状数组
    hdoj_1385Minimum Transport Cost
    hdoj_2112
    hdoj_3665Seaside
    Java的垃圾回收之算法
    Oracle和MySQL、PostgreSQL特性对比
    什么是java对象的强、软、弱和虚引用
    线程池(java.util.concurrent.ThreadPoolExecutor)的使用(一)
  • 原文地址:https://www.cnblogs.com/zhcpku/p/14440542.html
Copyright © 2020-2023  润新知