C++11:类的改进

  为了简化和扩展类的设计,C++ 11对类做了多项改进。这包括允许构造函数被继承和彼此调用、更佳的方法访问控制方式以及移动构造函数和移动赋值运算符。

显示转化运算符

  自动类型转换有时候会导致意转换问题的发生。C++ 11引入关键字 explicit ,以禁止单参数构造函数导致的自动转换。即被 explicit 关键字修饰的类构造函数,不能进行自动地隐式类型转换,只能显式地进行类型转换。

注意:只有一个参数的构造函数,或者构造函数有n个参数,但有n-1个参数提供了默认值,这样的情况才能进行类型转换。

#include <iostream>

using namespace std;

class CExplict {
public:
    CExplict( bool _explicit) {
        this->is_explict = _explicit;
    }

    CExplict(const CExplict& other) {
        this->is_explict = other.is_explict;
    }

    friend void printExplicit(const CExplict& cx);

private:
    bool is_explict;
};

void printExplicit(const CExplict& cx) {
    cout<<"is_explict="<<cx.is_explict<<endl;
}

int main( int argc, char* argv[]) {
    CExplict cx1{1};
    CExplict cx2{3.14};
    CExplict cx3 = false;
    printExplicit(cx1);
    printExplicit(cx2);
    printExplicit(cx3);
    printExplicit(false);
    printExplicit(3.14);
    return 0;
}

/*
int main( int argc, char* argv[])  {
    CExplict cx1 = CExplict(true);
    CExplict cx2 = cx1;
    printExplicit(cx1);
    printExplicit(cx2);
    printExplicit(CExplict(false));
    return 0;
}
*/

在上述代码的执行过程过程中,编译器首先会以 true 构建一个临时对象,在将此临时对象通过赋值构造函数赋值给 cx1.

于是,在 CExplict 构造函数前添加 explicit,在执行上面一段代码,则会报:

error C2440: “初始化”: 无法从“bool”转换为“CExplict”的错误,为了使程序能正确运行,需要将main函数内的代码改为:

explicit 关键字的作用: 禁止隐式调用类内的单参数构造函数, 这主要包括如下三层意思:

  • 该关键字只能用来修饰类内部的构造函数
  • 禁止隐式调用拷贝构造函数
  • 禁止类对象之间的隐式转换

总结

  1. explicit 关键字只需用于类内的单参数构造函数前面。由于无参数的构造函数和多参数的构造函数总是显示调用,这种情况在构造函数前加 explicit 无意义。

  2. 如果想禁止类A对象被隐式转换为类B对象,可在类B中使用关键字explicit。

   Google 的 C++ 规范中提到 explicit 的优点是可以避免不合时宜的类型变换,缺点无。所以 Google 约定所有单参数的构造函数都必须是显示的,只有极少数情况下拷贝构造函数可以不声明称explicit。例如作为其他类的透明包装器的类。

  Effective C++中说:被声明为 explicit 的构造函数通常比其 non-explicit 兄弟更受欢迎。因为它们禁止编译器执行非预期的类型转换,除非我有一个好理由允许构造函数被用于隐式类型转换,否则我会把它声明为explicit。鼓励大家遵循相同的政策。

继承构造

  为了进一步简化编码,C++ 11提供了一种让派生类能够继承基类构造函数的机制。C++ 98提供了一种让命名空间中函数可用的方法:

namespace Box {
	int fn(int) {}
	int fn(double) {}
	int fn(const char*) {}
}
using Box::fn;

  这让函数 fn的所有重载版本都可以使用。也可以使用这种方法让基类的所有非特殊成员函数对派生类可用。

class C1 {
public:
	int fn(int x) {
		cout << "C1::fn(int x)" << endl;
		return 0;
	}
	double fn(double x) {
		cout << "C1::fn(double x)" << endl;
		return 0;
	}
	void fn(const char* s) { cout << "C1::fn(const char* s)" << endl; }
};

class C2 :public C1 {
public:
	using C1::fn;
	double fn(double x) {
		cout << "C2::fn(double x)" << endl;
		return 0;
	}
};

int main() {
	C2 c2;
	int k = c2.fn(3);  //输出 "C1::fn(int x)"
	double z = c2.fn(2.4);//输出 "C2::fn(double x)",有点重写内味了
	return 0;
}

  C2 中的 using 声明让 C2 对象可以使用 C1 的三个 fn() 方法,但将选择 C2 而不是 C1 定义的方法。

   C++ 11 将这种方法用于构造函数。让派生类继承基类的所有构造函数(默认构造函数、拷贝构造函数和移动构造函数除外),但不会使用派生类构造函数的特征匹配的构造函数。

class Base {
protected:
	int x;
	double y;
public:
	Base() :x(0), y(0) {
		cout << "Base::Base() :x(0), y(0)" << endl;
	}
	Base(int i) :x(i), y(100) {
		cout << "Base::Base(int i) :x(i), y(100)" << endl;
	}
	Base(double i) :x(-1), y(i) {
		cout << "Base::Base(double i):x(-1), y(i)" << endl;
	}
	Base(int i, double j) :x(i), y(j) {
		cout << "Base::Base(int i, double j) :x(i), y(j)" << endl;
	}
	void Show()const {
		cout << x << "," << y << endl;
	}
};

class Son :public Base {
protected:
	short z;
public:
	using Base::Base;
	Son() :z(-100) {
		cout << "Son::Son():z(-100)" << endl;
	}
	Son(double x) :Base(2 * x), z(int(x)) {
		cout << "Son::Son(double x) :Base(2 * x), z(int(x))" << endl;
	}
	Son(int i) :z(-2), Base(i, 0.5 * i) {
		cout << "Son::Son(double x) :Son(int i) :z(-2), Base(i, 0.5 * i)" << endl;
	}
	void Show() {
		cout << z << ",";
		Base::Show();
	}
};

void test2() {
	Son son1;  //先创建基类,在创建子类
	Son son2(18.81);  //先调用父类的Base(double i),然后创建子类。
	Son sone(10, 1.8); //直接调用父类的Base(int i, double j)
}

上述代码输出如下

Base::Base() :x(0), y(0)
Son::Son():z(-100)
Base::Base(double i):x(-1), y(i)
Son::Son(double x) :Base(2 * x), z(int(x))
Base::Base(int i, double j) :x(i), y(j)

由于 Son 类中没有符合 Son(int, double) 的构造函数,在创建 Son son3 时,将使用继承而来的 Base(int, double)。而继承的基类构造函数只初始化基类成员; 如果还要初始化派生类成员,则应该使用成员列表初始化语法

Son(int i, int j, double k) :z(i), Base(j, k) {}

注意

  1. 继承的构造函数只能初始化基类中的成员变量,不能初始化派生类的成员变量,如果需要请使用成员列表初始化的方法
  2. 如果基类的构造函数被声明为私有,或者派生类是从基类中虚继承,那么不能继承构造函数
  3. 一旦使用继承构造函数,编译器不会再为派生类生成默认构造函数

委托构造

  和继承构造函数类似,委托构造函数也是 C++ 11 中对 C++ 的构造函数的一项改进,其目的也是为了减少程序员书写构造函数的时间。

  如果一个类包含多个构造函数,C++ 11 允许在一个构造函数中的定义中使用另一个构造函数,但这必须通过初始化列表进行操作,如下:

class Test {
public:
	Test() :Test(1, 'a') {
		cout << "Test() :Test(1, 'a')" << endl;
	}
	Test(int x) :Test(x, 'b') {
		cout << "Test(int x) :Test(x, 'b')" << endl;
	}
	Test(char x) :Test(11, x) {
		cout << "Test(char x) :Test(11, x)" << endl;
	}
	Test(int x, char y) : a(x), b(y) {
		cout << "Test(int x, char y) : a(x), b(y)" << endl;
	}
public:
	int a;
	char b;
};

void test3() {
	Test obj('z');
	cout << obj.a << endl;
	cout << obj.b << endl;
}

  上述构造函数先使用其他构造函数从初始化成员数据并且执行其函数体,然后再执行自己函数体,输出如下:

Test(int x, char y) : a(x), b(y)
Test(char x) :Test(11, x)
11
z

管理虚方法:final和override

  虚函数在 C++ 中会引起很多问题,因为没有一个强制的机制来标识虚函数在派生类中被重写了。virtual 关键字并不是强制性的,这给代码的阅读增加了一些困难,因为你可能不得不去看继承关系的最顶层以确认这个方法是不是虚方法。例如下面这个例子

class B  {
public:
   virtual void f(short) {std::cout << "B::f" << std::endl;}
};

class D : public B {
public:
   virtual void f(int) {std::cout << "D::f" << std::endl;}
};

  D::f 本应该重写 B::f,但是这两个函数的参数并不相同,一个参数是 short,另一个则是 int因此,B::f 仅仅是另外一个和 D::f 命名相同的函数,是重载而不是重写 。你有可能会通过 B 类型的指针调用 f(),并且期盼输出 D::f 的结果,但是打印出来的结果却是 B::f

  这里还有另外一个不明显的错误:参数是相同的,但是在基类中的函数是 const 成员函数,而在派生类中则不是。

class B  {
public:
   virtual void f(int) const {std::cout << "B::f " << std::endl;}
};

class D : public B {
public:
   virtual void f(int) {std::cout << "D::f" << std::endl;}
};

  又一次,这两个函数的关系是重载而非重写,因此,如果你想通过 B 类型的指针来调用 f(),程序会打印出 B::f,而不是 D::f

  为此C++ 11添加了两个新的关键字 override,可以指定在基类中的虚函数应该被重写; final,可以用来指定派生类中的函数不会重写基类中的虚函数。

override

  override 确保在派生类中声明的函数跟基类的虚函数有相同的签名,将上述声明改写后

class B  {
public:
    //这是第一个虚函数,没有重写,不能用override修饰
    virtual void f(short) {std::cout << "B::f" << std::endl;}
};

class D : public B {
public:
    //在重写虚函数地方,加上override, 要求重写的虚函数和基类一模一样
    virtual void f(int) override {std::cout << "D::f" << std::endl;}
};

  此时会出现一个编译错误

'D::f': 有override标识符的函数并没有重写任何基类函数,应将重写的函数签名声明为一致

final

  如果想要一个函数永远不能被重写(顺着继承层次往下都不能被重写),可以把该函数标识为 final,在基类中和派生类中都可以这么做。

//final阻止类的进一步派生,虚函数的进一步重写
class B1 final { //加上final,指定B1不能派生
	int a;
};

class B2 :public B1 {  //编译器提示:错误,不能将 'final' 类类型作为基类

};
class B {
public:
	virtual void f(int) { std::cout << "B::f" << std::endl; }
};

class D : public B {
public:
	//这是最终版本的虚函数,不能再重写
	virtual void f(int) override final { std::cout << "D::f" << std::endl; }
};

class F : public D {
public:
	 //err, 无法重写 'final' 函数 `D::f` 已经声明
	virtual void f(int) override { std::cout << "F::f" << std::endl; }
};