【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; // 无法通过编译,非虚函数重载
};
如果没有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中,类型初始化是唯一一种可以防止类型收窄的初始化方式。