C++11学习笔记(三)

【final和override】

final和override在其它的一些OOP中盛行,如今C++11也加入了这2个关键字。

通过下面这段代码来了解final的使用方式

struct Object{
    virtual void fun() = 0;
};

struct Base : public Object {
    void fun() final;   // 声明为final
};

struct Derived : public Base {
    void fun();     // 无法通过编译
};

由此可见,final关键字可以很方便的在继承关系中 终止  虚函数的可重载性。


先说一个C++中重载的一个特点,然后再说override关键字

在C++中,若基类中一个成员函数声明为virtual,则继承类中,无论是否使用virtual关键字修饰成员函数,它都依然是虚函数。这虽然方便了书写,却使得代码不够直观、明确。


因而引入了override关键字,下面看一个例子


struct Base {
    virtual void Turing() = 0;
    virtual void Dijkstra() = 0;
    virtual void VNeumann(int g) = 0;
    virtual void DKnuth() const;
    void Print();
};

struct DerivedMid: public Base {
    // void VNeumann(double g);
    // 接口被隔离了,曾想多一个版本的VNeumann函数
};

struct DerivedTop : public DerivedMid {
    void Turing() override;
    void Dikjstra() override;           // 无法通过编译,拼写错误,并非重载
    void VNeumann(double g) override;   // 无法通过编译,参数不一致,并非重载
    void DKnuth() override;             // 无法通过编译,常量性不一致,并非重载
    void Print() override;              // 无法通过编译,非虚函数重载
};


继承类中,Dijkstra()被误写成Dikjstra(),VNeumann(double g)与VNeumann(int g)函数原型 不匹配,并且错误的重写了废墟函数Print()。


如果没有override,那么上面代码中的错误就不会被发现。


【继承构造函数】

C++98中,继承构造函数的方式并不合用,尤其是类中具有多个构造函数的时候

struct A { 
    A(int i) {}
    A(double d, int i) {}
    A(float f, int i, const char* c) {}
    // ...
};

struct B : A { 
    B(int i): A(i) {} 
    B(double d, int i) : A(d, i) {}
    B(float f, int i, const char* c) : A(f, i, c){}
    // ...
    virtual void ExtraInterface(){}
};

类B中只有 ExtraInterface()这一个函数,却继承了类A的所有构造函数,这无疑是非常不方便的。


但是,若要继承基类的其它函数,可以使用using声明

#include <iostream>
using namespace std;

struct Base {
    void f(double i){ cout << "Base:" << i << endl; }
};

struct Derived : Base {
    using Base::f;
    void f(int i) { cout << "Derived:" << i << endl; }
};

int main() {
    Base b;
    b.f(4.5);  // Base:4.5

    Derived d;
    d.f(4.5);  // Base:4.5
}

派生类声明了和基类同名的函数f(),同时通过using声明也使用了基类的f(),在main()中,同时声明了b和d,并传入参数4.5,结果,b和d都使用了基类的f()函数。


C++11将这一特性也用在了构造函数上

struct A { 
    A(int i) {}
    A(double d, int i) {}
    A(float f, int i, const char* c) {}
    // ...
};

struct B : A { 
    using A::A;     // 继承构造函数
    // ...
    virtual void ExtraInterface(){}
};


不过,继承的构造函数无法初始化子类的成员,所以,我们需要使用C++11中的成员初始化来完成子类成员的初始化。

struct A {
    A(int i) {}
    A(double d, int i) {}
    A(float f, int i, const char* c) {}
    // ...
};

struct B : A { 
    using A::A;
    int d {0};
};

int main() {
    B b(356);   // b.d被初始化为0
}

有的时候,基类的构造函数会有默认参数,子类通过using声明继承基类的构造函数时,并不能继承其默认参数。如此一来,基类实际上会产生多个版本的构造函数,它们都会被子类继承。

struct A {
    A (int a = 3, double = 2.4){}
};

struct B : A{
    using A::A;
};

B可能从A中继承而来的候选继承构造函数如下

A(int=3,double=2.4);//使用2个参数
A(int=3);//减掉一个参数
A(const A&);//默认的复制构造函数
A();//不使用参数的情况

相应的,B的构造函数会包含以下一些

B(int,double);//一个继承构造函数
B(int);//减掉一个参数的继承构造函数
B(const B&);//复制构造函数-不是继承来的
B();//不使用参数的默认构造函数

在使用有默认参数值的构造函数的基类时,要特别小心

而有的时候,会遇到继承冲突的情况,这多发生在子类拥有多个基类的时候。

struct A { A(int) {} };
struct B { B(int) {} };

struct C: A, B {
    using A::A;
    using B::B; 
}; 

通过继承A、B,C拥有2个函数签名相同的构造函数,这根本无法通过编译。

解决方案是,通过显式声明C的构造函数

struct C: A, B {
    using A::A;
    using B::B; 
    C(int){}
}; 

此外,一旦使用了继承构造函数,则编译器不再为子类生成 默认构造函数。那么,下面的代码是不能通过编译的。

struct A { A (int){} };
struct B : A{ using A::A; };

B b;    // B没有默认构造函数

【委派构造函数】

委派构造函数的作用也是为了减少代码量,下面看一个构造函数代码冗余的例子

class Info {
public:
    Info() : type(1), name('a') { InitRest(); }
    Info(int i) : type(i), name('a') { InitRest(); }
    Info(char e): type(1), name(e) { InitRest(); }

private:
    void InitRest() { /* 其它初始化 */ }
    int  type;
    char name;
    // ...
};

可以看出,3个构造函数基本相似,需要优化。

首先,试试使用成员初始化来优化代码

class Info {
public:
    Info() { InitRest(); }
    Info(int i) : type(i) { InitRest(); }
    Info(char e): name(e) { InitRest(); }

private:
    void InitRest() { /* 其它初始化 */ }
    int  type {1};
    char name {'a'};
    // ...
};

代码确实优化了不少,但是,每个构造函数中都使用了 InitRest() 这个函数。

下面看看C++11中的解决方案

class Info {
public:
    Info() { InitRest(); }
    Info(int i) : Info() { type = i; }
    Info(char e): Info() { name = e; }

private:
    void InitRest() { /* 其它初始化 */ }
    int  type {1};
    char name {'a'};
    // ...
};

这里,基准函数 Info()被称为 目标构造函数,在初始化列表中 调用 基准函数的,则被称为 委派构造函数。委派构造函数不能使用初始化列表,只能在函数体内给变量赋初值。


上面的代码并不方便,我们可以改动一下,使得委派构造函数可以使用初始化列表

class Info {
public:
    Info() : Info(1, 'a') { }
    Info(int i) : Info(i, 'a') { }
    Info(char e): Info(1, e) { }

private:
    Info(int i, char e): type(i), name(e) { /* 其它初始化 */ }
    int  type;
    char name;
    // ...
};


这样一来,InitRest()函数也省了。


当构造函数比较多时,可能有不止一个委派构造函数,有时候,目标构造函数本身也是委派构造函数,这样一来,就可能形成链状关系。

class Info {
public:
    Info() : Info(1) { }    // 委托构造函数
    Info(int i) : Info(i, 'a') { } // 既是目标构造函数,也是委托构造函数
    Info(char e): Info(1, e) { }

private:
    Info(int i, char e): type(i), name(e) { /* 其它初始化 */ } // 目标构造函数
    int  type;
    char name;
    // ...
};


这样的情况下是无法通过编译的,应该尽量避免。


委派构造函数一个很实际的应用就是,使用构造模板函数产生目标构造函数。

#include <list>
#include <vector>
#include <deque>
using namespace std;

class TDConstructed {
    template<class T> TDConstructed(T first, T last) : 
        l(first, last) {}
    list<int> l;

public:
    TDConstructed(vector<short> & v): 
        TDConstructed(v.begin(), v.end()) {}
    TDConstructed(deque<int> & d): 
        TDConstructed(d.begin(), d.end()) {}
};


【列表初始化】

在C++98中,可以通过'{'、‘}’来对数组进行初始化,但是对于自定义类型,却无法使用这一特性。像STL中的vector,总是要先声明,再循环初始化,这无疑很不利于泛型编程。

C++11中使用了一种叫做 “初始化列表” 的方法

#include <vector>
#include <map>
using namespace std;

int a[] = {1, 3, 5};        // C++98 - 通过, C++11 - 通过
int b[] {2, 4, 6};          // C++98 - 失败, C++11 - 通过
vector<int> c{1, 3, 5};     // C++98 - 失败, C++11 - 通过
map<int, float> d = {{1, 1.0f}, {2, 2.0f} , {5, 3.2f}}; // C++98 - 失败, C++11 - 通过 

即便是自己定义的类,也可以通过#include<initializer_list>头文件,并且声明一个以initialize_list<T>模板类为参数的构造函数。

#include <vector>
#include <string>
using namespace std;

enum Gender {boy, girl};
class People {
public:
    People(initializer_list<pair<string, Gender>> l) {  // initializer_list的构造函数
        auto i = l.begin();
        for (;i != l.end(); ++i)
            data.push_back(*i);
    }

private:
    vector<pair<string, Gender>> data;
};

People ship2012 = {{"Garfield", boy}, {"HelloKitty", girl}};

同样,函数的参数列表一样可以使用初始化列表

#include <initializer_list>
using namespace std;

void Fun(initializer_list<int> iv){ }

int main() {
    Fun({1, 2});
    Fun({}); // 空列表
}

下面这个例子,重载了operator [] 和 operator =,以及使用辅助的数组。
#include <iostream>
#include <vector>
using namespace std;

class Mydata {
public:
    Mydata & operator [] (initializer_list<int> l)
    {
        for (auto i = l.begin(); i != l.end(); ++i)
            idx.push_back(*i);
        return *this;
    }
    Mydata & operator = (int v) 
    {
        if (idx.empty() != true) {
            for (auto i = idx.begin(); i != idx.end(); ++i) {
                d.resize((*i > d.size()) ? *i : d.size());
                d[*i - 1] = v;
            }
            idx.clear();
        }
        return *this;
    }

    void Print() {
        for (auto i = d.begin(); i != d.end(); ++i) 
            cout << *i << " ";
        cout << endl;
    }

private:
    vector<int> idx;    // 辅助数组,用于记录index
    vector<int> d;
};

int main() {
    Mydata d;
    d[{2, 3, 5}] = 7;
    d[{1, 4, 5, 8}] = 4;
    d.Print();  // 4 7 7 4 4 0 0 4
}

使用初始化列表的另一个优势就是——可以防止类型收窄

const int x = 1024;
const int y = 10;

char a = x;                     // 收窄,但可以通过编译
char* b = new char(1024);       // 收窄,但可以通过编译

char c = {x};                   // 收窄,无法通过编译
char d = {y};                   // 可以通过编译
unsigned char e {-1};           // 收窄,无法通过编译

float f { 7 };                  // 可以通过编译
int g { 2.0f };                 // 收窄,无法通过编译
float * h = new float{1e48};    // 收窄,无法通过编译
float i = 1.2l;                 // 可以通过编译


在C++11中,类型初始化是唯一一种可以防止类型收窄的初始化方式。



  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值