C++ 学习笔记——十二、探讨 C++ 新标准

目录:点我

一、复习

1. 新类型

C++ 11 新增了类型 long long 和 unsigned long long ,以支持 64 位(或更宽)的整型;新增了类型 char16_t 和 char32_t ,以支持 16 位和 32 位的字符表示;还新增了“原始字符串”。

2. 统一的初始化

C++ 11 扩大了用大括号括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户定义的类型(即类对象)。使用初始化列表时,可添加等号,也可不添加:

// 常规类型
int x = {5};
double y {2.75};
short quar[5] {4, 5, 2, 76, 1};
int * ar = new int [4] {2, 4, 6, 7};  // new表达式

// 类
class Stump {
private:
	int roots;
	double weight;
public:
	Stump(int r, double w) : roots(r), weight(w) {}
};
Stump s1(3, 15.6);  // old style
Stump s2{5, 43.4};  // c++11
Stump s3 = {4, 32.1};  //c++11

// 缩窄转换
char c1 = 1.57e27;  // double to char, allow but undefined behavior
char c2 = 459585821  // int to char, allow but undefined behavior
// 防止缩窄转换
char c1 {1.57e27};  // double to char, not allow
char c2 = {459568434};  // int to char, out of range, not allow
// 允许转换为更宽的类型
char c1 {66};  // int to char, in range, allow
double c2 = {66};  // int to double, allow

// 模板
vector<int> a1(10);  // 未初始化的,大小为10的向量
vector<int> a2{10};  // 初始化列表,大小为1,值为10;
vector<int> a3{4, 6, 1};  // 初始化列表,大小为3,值为4 6 1

3. 声明

C++ 11 提供了多种简化声明的功能:

auto maton = 112;  // 自动推断 int
auto pt = &maton;  // int *
double fm(double, int);
auto pf = fm;  // double (*)(double, int)
decltype(x) y;  // 将y类型转换为x的类型
double x; int n;
decltype(x*n) q;  // double
decltype(&x) pd;  // double *

// 模板常用方法
template<typename T, typename U>
void ef(T t, U u) {
	decltype(T*U) tu;  // 模板实例化时确定类型
	...
}

// 返回类型后置
double f1(double, int);  // 传统语法
auto f2(double, int) -> double;  // 新语法,返回double
auto eff(T t, U u) -> decltype(T*U);  // 常规用法

// 模板别名
typedef std::vector<std::string>::iterator itType;  // 传统别名,不能用于模板部分具体化
using itType = std::vector<std::string>::iterator;  // C++ 11新方法,可以用于模板部分具体化
template<typename T>
using arr12 = std::array<T, 12>;  // 模板部分具体化

// 空指针
nullptr <=> 0

4. 智能指针

如果在程序中使用 new 从堆分配内存,等到不再需要时,应使用 delete 释放,新增智能指针帮助自动化完成该过程。C++ 11 摒弃了 auto_ptr ,新增了 unique_ptr 、shared_ptr 、weak_ptr 。

5. 异常规范

用于指出函数可能引发哪些异常:

// 传统方法,因不好用被C++11摒弃
void f501(int) throw(bad_dog);  // 抛出bad_dog异常
void f733(long long) throw();  //  不会引发异常
// 第二种情况可能有意义,因此新增关键字noexcept
void f875(short, short) noexcept;  // 不会引发异常

6. 作用域内枚举

传统枚举:

  • 提供了一种创建名称常量的方法,但其类型检查相当低级。
  • 枚举名的作用域为枚举定义所属的作用域,这意味着如果在同一个作用域内定义两个枚举,它们的枚举成员不能同名。
  • 枚举可能不是可完全移植的,因为不同的实现可能选择不同的底层类型。

为解决这些问题,新增枚举:

// 旧枚举
enum old {yes, no, matbe};
// 新枚举,需要显示指定枚举成员作用域
enum class new1 {never, sometimes, often, always};
enum struct new2 {never, lever, sever};

7. 对类的修改

禁止隐式转换运算符:

class Plebe {
	operator int() const;
	explicit operator double() const;
};
Plebe a, b;
int n = a;  // 隐式转换为int
double x = b;  // 隐式转换为double,not allow
x = double(b);  // 显示转换为double,allow

类内成员初始化:

class Session {
	int mem1 = 10;  // 类内初始化
	double mem2 {1966,.54};  // 类内初始化
	short mem3;
public:
	Session(){}
	Session(short s) : mem3(s) {}
	Session(int n, double d, short s) : mem1(n), mem2(d), mem3(s) {}
};

可以使用等号和大括号初始化,但不能使用圆括号。通过类内初始化可以避免在构造函数内编写重复代码。

8. 模板和 STL

基于范围的 for 循环:

vector<int> v(10);
for(int i : v)
	cout << i << endl;

新增 STL 容器:

容器 作用
forward_list 单向链表
unordered_map 无序 map
unordered_multimap 无序、可重复 map
unordered_set 无序 set
unordered_multiset 无需、可重复 set
array 数组

新增 STL 方法:

方法 说明
cbegin() 指向容器第一个元素,并将元素视为 const
cend() 指向容器最后一个元素的后面,并将元素视为 const

对 valarray 进行升级,添加了 begin() 和 end() ,使之可以被迭代器访问,从而能够将基于范围的 STL 算法用于该容器。

摒弃 export ,原本作用为让程序员能够将模板定义放在接口文件和实现文件中,其中前者包含原型和模板声明,后者包含模板函数和方法的定义。

新增尖括号的识别,避免了与 >> 运算符混淆。

9. 右值引用

左值是一个表示数据的表达式(变量名、解除引用的指针),最初只能出现在赋值语句左边,但是随着 const 引入,也可以出现在赋值语句右边:

int n;
int * pt = new int;
const int b = 101;  // 不能修改b
int & rn = n;  // 引用n,即取到了n的地址
int & rt = *pt;  // 取到了指针的地址
const int & rb = b;  // 取到了b的地址,但不能修改rb

右值则是一个表示运算的表达式(不能对其赋值),不能应用取地址符,这种方式更像是创建并保存了一个表达式结果的副本,并引用了这个副本:

int x = 10;
int y = 23;
int && r1 = 13;
int && r2 = x + y;  // r2关联运算的结果,因此改变x或y不影响r2
double && r3 = sqrt(2.0);
double *p = &r3;  // 可以取到右值的地址

二、移动语义和右值引用

1. 移动语义

vector<string> vstr(20000string(1000));  // 2w个string,每个string有1k个char
vector<string> vstr_copy1(vstr);

vectorstring 类都使用动态内存分配,因此它们必须定义使用某种 new 版本的拷贝构造函数。为初始化对象 vstr_copy1 ,拷贝构造函数 vector 将使用 new 给 2 万个 string 对象分配内存,而每个 string 对象又将调用 string 的拷贝构造函数,该构造函数使用 new 为 1 千个字符分配内存。接下来全部的字符都将从 vstr 控制的内存种复制到 vstr_copy1 控制的内存中。这样的工作量巨大,并且存在问题:

vector<string> allcaps(const vector<string> & vs) {
	vector<string> tmp;
	// todo: tmp存储了vs全大写版本的数据
	return tmp;
}
vector<string> vstr(20000string(1000));
vector<string> vstr_copy1(vstr);  // 拷贝vstr
vector<string> vstr_copy2(allcaps(vstr));  // 创建tmp变量,函数运行结束时删除tmp变量,然后返回其它的副本,非常浪费资源

解决这个问题的方法为不删除 tmp 变量,而是转移所有权,该操作称为移动语义(move semantics),这样避免了删除和拷贝原数据。要实现它,需要让编译器直到何时需要复制,何时不需要,这就是右值发挥作用的地方。可以定义两个构造函数,一个是常规拷贝构造函数,使用 const 左值引用作为参数,这个引用关联到左值实参,如语句 1 的 vstr ;另一个是移动构造函数,使用右值引用作为参数,该引用关联到右值实参,如语句 2 种 allcaps 函数的返回值 tmp 。拷贝构造函数可执行深复制,而移动构造函数只负责调整记录(转换所有权)。由于移动构造函数可能修改其实参,因此右值引用不应是 const 。

Useless::Useless(Useless && f) : n(f.n) {  // 此处的f可以是运算表达式
	pc = f.pc;  // pc是一个指针(私有成员),指向现有数据
	f.pc = nullptr;  // 转换了所有权
	f.n = 0;  // 转换了所有权后原来的对象的长度变为0
}
Useless four(one + three);  // 传递一个对象运算表达式,这将生成临时对象,然后调用有移动构造函数

虽然右值引用可支持移动语义,但是需要执行两个步骤来执行。

  • 首先,让编译器直到何时可使用移动语义:
    Useless two = one;  // 拷贝构造函数
    Useless our(one + three);  // 移动构造函数
    
    对象 one 是左值,因此与左值引用匹配,调用拷贝构造函数;one + three 是右值,与右值引用匹配,调用移动构造函数。
  • 然后,编写移动构造函数,提供所需的方法。

2. 赋值

适用于构造函数的移动语义考虑也适用于赋值运算符,即移动赋值运算符:

Useless & Useless::operator=(Useless && f) {
	pc = f.pc;  // pc是一个指针(私有成员),指向现有数据
	f.pc = nullptr;  // 转换了所有权
	f.n = 0;  // 转换了所有权后原来的对象的长度变为0
	return *this;
}

与上一种情况相同,由于修改了源对象,因此不能使用 const 引用。

3. 强制移动

上述两个例子使用了右值,如果要使用左值,就需要强制移动:

Useless choices[10];
Useless best = choices[pick];  // 这种方式会复制一个副本给best

由于 choices[pick] 是左值,因此上述赋值语句将使用复制赋值运算符,而不是移动赋值运算符,但如果能让它看起来像右值,便将使用移动赋值运算符。为此可使用运算符 static_cast<> 将对象的类型强制转换为 Useless && ,但 C++ 11 在 utility 头文件中提供了一种更简单的方式 std::move

Chunk one, two;
two = move(one);

如果定义了移动赋值运算符,则调用移动赋值运算符;如果没有定义则调用复制赋值运算符;如果复制赋值运算符也没有定义,则不允许上述赋值。

三、新的类功能

1. 特殊成员函数

C++ 11 在原有的 4 个特殊成员函数(默认构造函数、复制构造函数、复制赋值运算符和析构函数)的基础上,新增了移动构造函数和移动赋值运算符。这些成员函数是编译器在各种情况下自动提供的。但是存在一些例外:

  • 如果显式的提供了析构函数、复制构造函数或复制赋值运算符,编译器将不会自动提供移动构造函数和移动赋值运算符;
  • 如果显式的提供了移动构造函数或移动赋值运算符,编译器将不会自动提供复制构造函数和复制赋值运算符;
  • 默认的移动构造函数和移动赋值运算符的工作方式与复制版本类似,将会采取复制的策略处理;
  • 如果由于显式的提供了移动构造函数,导致系统没有自动创建默认的构造函数、拷贝构造函数和复制赋值运算符,此时可以使用 default 关键字显式地生命这些方法为默认版本,该方法只能用于这 6 个特殊的成员函数:
    class Someclass {
    public:
    	Someclass(Someclass &&);
    	Someclass() = default;
    	Someclass(const Someclass &) = default;
    	Someclass & operator=(const Someclass &) = default;
    	...
    };
    
  • 相对的,关键字 delete 可用于禁止使用特定方法,可以用于任何成员函数:
    class Someclass {
    public:
    	Someclass() = default;
    	Someclass(const Someclass &) = delete;  // 禁止使用拷贝构造函数
    	Someclass & operator=(const Someclass &) = delete;  // 禁止使用复制赋值运算符
    	Someclass(Someclass &&) = default;
    	Someclass & operator=(Someclass &&) = default;
    	Someclass & operator+(const Someclass &) const;
    	...
    };
    
  • 如果要启动移动方法的同时禁用复制方法,则会导致左值的操作失效:
    Someclass one, two;
    Someclass three(one);  // not allow
    Someclass four(one + two);  // allow
    

2. 委托构造函数

如果需要给类提供多个构造函数,其中部分构造函数的代码需要重用,C++ 11 允许在一个构造函数的定义中使用另一个构造函数,这被称为委托构造函数:

class Notes {
	int k;
	double x;
	string st;
public:
	Notes();
	Notes(int);
	Notes(int, double);
	Notes(int, double, string);
};
Notes::Notes(int kk, double xx, string stt) : k(kk), x(xx), st(stt) {}
Notes::Notes() : Notes(0, 0.01, "oh") {}
Notes::Notes(int kk) : Notes(kk, 0.01, "Ah") {}
Notes::Notes(int kk, double xx) : Notes(kk, xx, "Uh") {}

3. 继承构造函数

为了进一步简化编码工作,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 j) {...}
	double fn(double w) {...}
	void fn(const char * s) {...}
};
class C2 : public C1 {
	...
public:
	...
	using C1::fn;
	double fn(double) {...};
};
...
C2 c2;
int k = c2.fn(3);  // 使用C1的方法
double z = c2.fn(2.4);  // 使用C2的方法

C++ 11 将这种方法用于构造函数,这让派生类继承基类的所有构造函数(除默认构造函数、复制构造函数和移动构造函数),但不会使用与派生类构造函数的特征标匹配的构造函数(理解为重写,因此不调用父类的方法而是使用派生类自己的)。

4. 管理虚方法

假设基类声明了一个虚方法,而要在派生类中提供不同的版本,这将覆盖旧版本。但是由之前的讨论可知,如果特征标不匹配,将隐藏而不是覆盖旧版本:

class Action {
	int a;
public:
	Action(int i = 0) : a(i) {}
	int val() const {return a;}
	virtual void f(char ch) const {cout << val() << ch << endl;}  // 基类的虚函数
};
class Bingo : public Action {
public:
	Bingo(int i = 0) : Action(i) {}
	virtual void f(char * ch) const {cout << val() << ch << endl;}  // 派生类的虚函数
};

此时由于派生类定义的虚函数参数为指针,而不是 char ,因此导致程序不能执行:

Bingo b(10);
b.f('@');  // 无法执行,因为基函数被隐藏

C++ 11 可使用虚说明符 override 在派生类中指出要覆盖一个虚函数:

virtual void f(char * ch) const override {  // 此时将编译错误,因为虚方法的参数与基类不同
	cout << val() << ch << endl;
}

如果想要禁止派生类覆盖特定方法,可以在基类中使用 final 虚说明符:

virtual void f(char ch) const final {
	cout << val() << ch << endl;
}

由于虚说明符不是关键字,因此编译器将根据上下文来确定其是否具有特殊含义,在其他场景中可以作为常规标识符使用。

四、Lambda 函数

Lambda 函数是 λ 演算——一种定义和应用函数的数学系统,可以使用匿名函数替代函数指针和函数符。首先使用函数指针、函数符和 Lambda 函数来实现一个函数:判断有多少个元素能够被 3 整除。

  • 函数指针:
    vector<int> numbers(100) {...};
    bool f3(int x) {return x % 3 == 0;}
    count = count_if(numbers.begin(), numsbers.end(), f3);  // 将函数指针传递给count_if
    
  • 函数符:
    class f_mod {
    private:
    	int dv;
    public:
    	f_mod(int d = 1) : dv(d) {}
    	bool operator()(int x) {return x % dv == 0;}
    };
    count = count_if(numbers.begin(), numbers.end(), f_mod(3));  // 使用构造函数创建dv为3的对象,然后使用函数符判断
    
  • Lambda 函数:
    [](int x) {return x % 3 == 0;}  // 首先用[]代替了函数名,其次没有声明返回类型,它会通过decltype自动推断,此处为bool,默认为void
    count = count_if(numbers.begin(), numbers.end(), [](int x){return x % 3 == 0;})
    
    需要注意的是,自动推断只有在一条返回语句组成时才有效,否则需要显式给出具体类型:
    [](double x)->double{int y= x; return x - y;}
    

如果需要多次使用同一个 Lambda 函数,可以给它指定一个名称,并且可以作为正常函数使用:

auto mod3 = [](int x) {return x % 3 == 0;}
bool result = mod3(z);

Lambda 函数可访问作用域内的任何动态变量,需将其放入 [] 内:

  • 按值使用:[z] ;
  • 引用:[&z] ;
  • 访问所有动态变量:[=] 、[&];
  • 混合使用:[&, z] ,表示 z 按值使用,其他变量按引用使用。
	// 原始版本
	count = count_if(numbers.begin(), numbers.end(), [](int x){return x % 3 == 0;})  
	// 修改版本
	int y = 0, z = 0;
	count = for_each(numbers.begin(), numbers.end(), [&y](int x){y += x % 3 == 0; z += x % 13 == 0;})  // 同时实现两种整除

五、包装器

包装器(wrapper,也叫适配器 adapter),用于给其他编程接口提供更一致或更合适的接口。

answer = ef(q);

ef 可以是函数名、函数指针、函数对象或有名称的 lambda 表达式,这些都为可调用的类型(callable type),由于可调用的类型丰富,因此可能导致模板的效率降低,下面是一个例子:

#include
using namespace std;
template <typename T, typename F>
T use_f(T v, F f) {
	static int count = 0;
	count++;
	cout << "use_f count = " << count << ", &count = " << &count << ", ";
	return f(v);
}

class Fp {
private:
	double z_;
public:
	Fp(double z = 1.0) : z_(z) {}
	double operator()(double p) {return z_*p;}
};

class Fq {
private:
	double z_;
public:
	Fq(double z = 1.0) : z_(z) {}
	double operator()(double q) {return z_ + q;}
};

double dub(double x) {return 2.0 * x;}
double square(double x) {return x * x;}

int main() {
	double y = 1.21;
	cout << use_f(y, dub);  // use_f count = 1, &count = 0x402028, 2.42
	cout << use_f(y, square);  // use_f count = 2, &count = 0x402028, 1.4641
	cout << use_f(y, Fp(5.0));  // use_f count = 1, &count = 0x402020, 6.05
	cout << use_f(y, Fq(5.0));  // use_f count = 1, &count = 0x402024, 6.21
	cout << use_f(y, [](double u){return u * u;});  // use_f count = 1, &count = 0x405020, 1.4641
	cout << use_f(y, [](double u){return u + u / 2.0;});  // use_f count = 1, &count = 0x40501c, 1.815
}

可以发现,在每次调用中,模板参数 T 都被设置为类型 double ,但是 F 的类型却各不相同,下面进行分析:

  • use_f(y, dub); :dub 是一个函数指针,因此 F 的类型为 double(*)(double)
  • use_f(y, square); :与上一个相同;
  • use_f(y, Fp(5.0)); :F 的类型为 Fp ;
  • use_f(y, Fq(5.0)); :F 的类型为 Fq;
  • use_f(y, [](double u){return u * u;}); :F 为 lambda 表达式使用的类型;
  • use_f(y, [](double u){return u + u / 2.0;}); :F 为 lambda 表达式使用的类型;

因此可以发现同样的模板代码,生成了 5 个不同的实例,而由于这 6 个调用中的调用特征标均为 double(double) ,导致造成了浪费。模板 function 是在头文件 functional 中声明的,他从调用特征标的角度定义了一个对象,可用于包装调用特征标相同的函数指针、函数对象或 labmda 表达式:

function<double(char, int)> fdci;  // 接收char和int参数,返回double值,可将函数指针、函数对象或labmda表达式赋给它

下面改写上一个例子:

int main() {
	double y = 1.21;
	function<double(double)> ef1 = dub;
	function<double(double)> ef2 = square;
	function<double(double)> ef3 = Fp(5.0);
	function<double(double)> ef4 = Fq(5.0);
	function<double(double)> ef5 = [](double u){return u * u;};
	function<double(double)> ef6 = [](double u){return u + u / 2.0;};
	cout << use_f(y, ef1);  // use_f count = 1, &count = 0x402020, 2.42
	cout << use_f(y, ef2);  // use_f count = 2, &count = 0x402020, 1.4641
	cout << use_f(y, ef3);  // use_f count = 3, &count = 0x402020, 6.05
	cout << use_f(y, ef4);  // use_f count = 4, &count = 0x402020, 6.21
	cout << use_f(y, ef5);  // use_f count = 5, &count = 0x402020, 1.4641
	cout << use_f(y, ef6);  // use_f count = 6, &count = 0x402020, 1.815
}

此时只创建了一个实例,节省了资源,还可以进一步简化:

typedef function(double) fdd;
int main() {
	double y = 1.21;
	cout << use_f(y, fdd(dub));
	cout << use_f(y, fdd(square));
	...
}

另外可以对源函数进行修改:

template <typename T, typename F>
T use_f(T v, function<T(T)> f) {
	static int count = 0;
	count++;
	cout << "use_f count = " << count << ", &count = " << &count << ", ";
	return f(v);
}
cout << use_f<double>(y, dub);
cout << use_f<double>(y, Fp(5.0));

由于函数本身的类型不是 function ,因此在 use_f 后面使用了 来指出所需的具体化。

六、可变参数模板

如果想要对一个函数传递任意的参数列表,根据之前所学的知识是对其进行重载,但是这样很难满足“任意”的需求,因此需要可变参数模板。

1. 模板和函数参数包

template<typename T>
void show_list0(T value) {
	cout<<value<<",";
}

show_list0(2.15);

上述代码中有两个参数列表,模板参数列表中只包含 T ,函数参数列表只包含 value 。C++ 11 提供了一个用省略号表示的元运算符(mera-operator),可以声明表示模板参数包的标识符(类型列表),还能够声明表示函数参数包的标识符(值列表):

template<typename... Args>
void show_list1(Args... args) {...}  // args的类型为Args

其中,Args 是一个模板参数包,而 args 是一个函数参数包:

show_list1('S', 80, "sweet", 4.5);  // 参数包Args包含于函数调用中的参数匹配的类型:char int const char* double

2. 展开参数包

template<typename... Args>
void show_list1(Args... args) {
	show_list1(args...);  // args展开为5, 'L', 0.5,因此导致无限循环
}
show_list1(5, 'L', 0.5);  // 将3个参数封装到args中

上面的代码存在缺陷,会导致无限循环,正确的使用方法如下:

template<typename T, typaname... Args>
void show_list3(T value, Args... args) {
	cout << value << ", ";
	show_list3(args...);
}
int main() {
	int n =14;
	double x = 2.7;
	string s = "example";
	show_list3(n, x);
	show_list3(x*x, '!', 7, mr);  // 第一个实参导致T为double,后3个参数封装为args
	return 0;
}

考虑到参数结束的情况,还可以对其进行改进,重新定义一个只接受一项参数的版本:

template<typename T>
void show_list3(T value) {
	cout << value << "\n";
}

考虑到直接传值效率很低,因此可以使用引用:

show_list3(const Args&... args);

你可能感兴趣的:(C/C++,c++,学习,开发语言)