《C++ Primer Plus》读书笔记 第18章 探讨C++新标准

第18章 探讨C++新标准

1.复习前面介绍过的C++11功能

C++11新增了类型long longunsigned long long,以支持64位(或更宽)的整型;新增了类型char16_tchar32_t,以支持16位和32位的字符表示。

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};

然而,如果类有将模板std::initializer_list作为参数的构造函数,则只有该构造函数可以使用列表初始化形式

如果使用初始化列表语法,编译器禁止将数值赋给无法存储它的数值变量。但允许转换为更宽的类型。另外,只要值在较窄类型的取值范围内,将其转换为较窄的类型也是允许的。

C++11提供了模板类initializer_list,可将其用作构造函数的参数。如果类有接受initializer_list作为参数的构造函数,则初始化列表语法就只能用于该构造函数。列表中的元素必须是同一种类型或可转换为同一种类型。

C++11将关键字auto用于实现自动类型推断。关键字auto还可以简化模板声明。

关键字decltype将变量的类型声明为表达式指定的类型:

decltype(x) y;

C++11新增了一种函数声明语法:在函数名和参数列表后面指定返回类型

auto eff(T t, U u) -> decltype(t * u)
{
	//...
}

对于冗长或复杂的标识符,以前C++提供了typedef来创建别名。C++11使用using来创建别名:

using itType = std::vector<std::string>::iterator;

新语法也可以用于模板部分具体化:

template<typename T>
using arr12 = std::array<T, 12>;

上述语句具体化模板array<T, int>。例如,对于下述声明:

std::array<double, 12> a1;
std::array<std::string, 12> a2;

可将它们替换为如下声明:

arr12<double> a1;
arr12<std::string> a2;

C++11新增了关键字nullptr来表示空指针。它是指针类型,不能转换为整型类型。C++11仍允许使用0来表示空指针,因此表达式nullptr == 0为true。

C++11摒弃了auto_ptr并新增了三种智能指针:unique_ptr、shared_ptr和weak_ptr。

C++11摒弃了原来的异常规范,但新增了一种特殊的异常规范:关键字noexcept。例如,下面的语句指出该函数不会引发异常:

void f875(short, short) noexcept;

C++11新增了作用域内枚举。解决了作用域内枚举成员不能同名的问题:

enum class New1 {never, sometimes, often, always};
enum class New2 {never, lever, sever};

新枚举要求进行显式限定,以免发生名称冲突。因此,引用特定枚举时,需要使用New1::never和New2::never等。

C++引入了关键字explicit来禁止单参数构造函数导致的自动转换。C++11拓展了这种用法,使得可对转换函数做类似的处理。

C++11允许在类定义中使用等号或大括号进行成员初始化。

C++11提供了基于范围的for循环:

double prices[5] = {4.99, 10.99, 6.87, 7.99, 8.49};
for (auto x : prices)
	std::cout << x << std::endl;

C++11新增了STL容器forward_list、unordered_map、unordered_multimap、unordered_set和unordered_multiset。还新增了模板array

C++11新增了方法cbegin()和cend()。与begin()和end()一样,这些新方法也返回一个迭代器,指向容器的第一个元素和最后一个元素的后面。区别是这些新方法将元素视为const。与此类似,crbegin()和crend()是rbegin()和rend()的const版本。

C++11对模板valarray添加了两个函数begin()和end()。

C++11摒弃了关键字export的用法,仍保留该关键字供以后使用。

C++11允许在声明嵌套模板时不使用空格将尖括号分开:

std::vector<std::list<int>> v1;

C++11新增了右值引用,这是使用&&表示的。

2.移动语义和右值引用

移动语义就是将一个对象的所有权直接交给另一个对象,原对象将失去之前拥有的内容,而不进行内容复制的方法。要实现移动语义,就要定义移动构造函数。移动构造函数使用右值引用作为参数,该引用关联到右值实参。复制构造函数可执行深复制,而移动构造函数值调整记录。在将所有权转移给新对象的过程中,移动构造函数可能修改其实参,这意味着右值引用参数不应是const。下面是一个移动构造函数的例子:

class Useless
{
private:
	int n;
	char * pc;
	//...
public:
	Useless();
	explicit Useless(int k);
	Useless(int k, char ch);
	Useless(const Useless & f);
	Useless(Useless && f);
	~Useless*();
	Useless operator+(const Useless & f)const;
	//...
};
//...
Useless:Uesless(Uesless && f) : n(f.n)
{
	pc = f.pc;
	f.pc = nullptr;
	f.n = 0;
}

移动构造函数让pc指向现有的数据,以获取这些数据的所有权。此时,pc和f.pc指向相同的数据,调用析构函数将带来麻烦,因为程序不能对同一个地址调用delete[]两次。为避免这种问题,移动构造函数将原来的指针设置为空指针,因为对空指针执行delete[]没有问题。这种夺取所有权的方式常被称为窃取

对于下面的语句:

Useless two = one;
Useless four (one + two);

表达式one + two是右值,与右值引用匹配。如果定义了移动构造函数,编译器将使用移动构造函数来初始化对象four。如果没有定义移动构造函数,编译器将使用复制构造函数,由于左值不能指向右值,这时将会生成一个临时变量。

移动语义也使用与赋值运算符:

Useless & Useless::operator=(Useless & f)
{
	if (this == &f)
		return *this;
	delete [] pc;
	n = f.n;
	pc = f.pc;
	f.n = 0;
	f.pc = nullptr;
	return *this;
}

移动赋值运算符删除目标对象中的原始数据,并将源对象的所有权转让给目标。

如果希望对左值引用使用移动语义,以使用运算符static_case<>将对象的类型强制转换为右值引用。C++11提供了一种更简单的方式:使用头文件utility中声明的函数std::move():

Useless one(10, ‘x’);
Useless four;
four = std::move(one);

但如果Useless没有定义移动运算符,编译器将使用复制赋值运算符。如果也没有定义赋值运算符,将根本不允许上述赋值。

3.新的类功能

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

在没有提供任何参数的情况下,将调用默认构造函数。如果没有为类定义构造函数,编译器将提供一个默认的默认构造函数。对于使用内置类型的成员,默认的默认构造函数不对其进行初始化;对于属于类对象的成员,则调用该类的默认构造函数。

如果没有提供复制构造函数,而代码又需要使用它,编译器将提供一个默认的复制构造函数;如果没有提供移动构造函数,而代码又需要使用它,编译器将提供一个默认的移动构造函数。

如果没有提供析构函数,编译器将提供一个默认的析构函数。

如果提供了析构函数、复制构造函数或复制赋值运算符,编译器将不会自动提供移动构造函数和移动赋值运算符;如果提供了移动构造函数或移动赋值运算符,编译器将不会自动提供复制构造函数或复制赋值运算符。

假定要使用某个默认的函数,而这个函数由于某种原因不会自动创建。在这种情况下,可以使用关键字default显式地声明这些方法的默认版本:

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;
	//...
};

关键字default只能用于6个特殊成员函数,但delete可用于任何成员函数。

C++11允许在一个构造函数的定义中使用另一个构造函数,这被称为委托。委托使用成员初始化列表语法的变种:

class Notes
{
	int k;
	double x;
	std::string st;
public:
	Notes();
	Notes(int);
	Notes(int, double);
	Notes(int, double , std::string);
};
Notes::Notes(int kk, double xx, std::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”) {}

为进一步与简化编码工作,C++11提供了一种让派生类能够继承基类构造函数的机制:在派生类中使用using声明可以让派生类继承基类的所有构造函数(默认构造函数、复制构造函数和移动构造函数除外),但不会使用与派生类构造函数的特征标匹配的构造函数:

class BS
{
	int q;
	double w;
public:
	BS() : q(0), w(0) {}
	BS(int k) : q(k), w(100) {}
	BS(double x) : q(-1), w(x) {}
	BS(int k, double x) : q(x), w(x) {}
};

class DR : public BS
{
	short j;
public:
	using BS::BS;
	DR() : j(-100) {}
	DR(double x) : BS(2 * x), j(int(x)) {}
	DR(int i) : j(-2), BS(i, 0.5 * i) {}
};

int main()
{
	DR o1;				//使用DR()
	DR o2(18.81);	//使用DR(double)而不是BS(double)
	DR o3(10, 1.8);	//使用BS(int, double)
	//...
}

注意,继承基类的构造函数只初始化基类成员,如果还要初始化派生类成员,则应使用成员列表初始化语法:

DR(int i, int k, double x) : j(i), BS(k, x) {}

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

class Action
{
	int a;
public:
	Action(int i = 0) : a(i) {}
	int val() const { return a; }
	virtual void f(char ch) const { std::cout << val() << ch << “\n”;}
};

class Bingo : public Action
{
public:
	Bingo(int i = 0) : Action(i) {}
	virtual void f(char * ch) const {std::cout < val() <<ch <<!\n”;}
};

这导致程序不能使用类似于下面的代码:

Bingo b(10);
b.f(‘@’);

在C++11中,可使用虚说明符override指出要覆盖一个虚函数:将其放在参数列表后面。如果声明与基类方法不匹配,编译器将视为错误。这将防止方法被隐藏。因此,下面的Bingo::f()版本将生成一条编译错误消息:

virtual void f(char * ch) const  override
{	std::cout << val() << ch <<!\n”; }

如果想禁止派生类覆盖特定的虚方法,可在参数列表后面加上final。例如,下面的代码禁止Action类的派生类重新定义函数f():

virtual void f(char * ch) const final 
{ std::cout << val() << ch << “\n”’; }

4.Lambda函数

名称lambda来自lambda calculus(λ演算)——一种定义和应用函数的数学系统。这个系统能使用匿名函数。在C++11中,对于接受函数指针或函数符的函数,可使用匿名函数定义(lambda)作为其参数。

例如,要计算一个矢量中能被3整除的元素数量,可以使用下面的语句:

std::vector<int> numbers(1000);
std::generate(numbers.begin(), numbers.end(), std::rand);
count3 = std::count_if(numbers.begin(), numbers.end(), [](int x) {return x % 3 == 0;} );

其中,generate()函数接受一个区间(有前两个参数指定),并将每个元素设置为第三个参数返回的值,而第三个参数是一个不接受任何参数的函数对象。count_if()函数的前两个参数指定了一个区间,第三个参数是一个返回true或false的函数对象。该函数可以计算这样的元素数,即它使得指定的函数对象返回true。count_if()函数的第三个参数使用了lambda表达式:

[](int x) {return x % 3 == 0;}

lambda函数与常规函数的差别有两个:使用[]替代了函数名;没有声明返回类型。返回类型相当于使用decltype根据返回值推断得到的。如果lambda不包含返回语句,推断出的返回类型将为void。仅当lambda表达式完全由一条返回语句组成时,自动类型推断才管用;否则,需要使用新增的返回类型后置语法

[](double x)->double(int y = x; return x - y;}

可以给lambda表达式指定一个名称:

auto mod3 = [](int x){return x % 3 == 0;}
count1 = std::cout_if(n1.begin(), n1,end(), mod3);

甚至可以像使用常规函数那样使用有名称的lambda:

bool result = mod3(z);

lambda可访问作用域内的任何动态变量;要捕获要使用的变量,可将其名称放在中括号内。如果只指定了变量名,将按值访问变量;如果在名称前加上&,将按引用访问变量。如果只有一个&而无名称,即[&],则可以按引用访问所有动态变量,而[=]可以按值访问所有动态变量。还可以混合使用这两种方式。例如,可以将下述代码:

int conut3;
count3 = std::count_if(numbers.begin(), numbers.end(), [](int x){return x % 3 == 0;});

替换为如下代码:

int count3 = 0;
std::for_each(numbers.begin(), numbers.end(),  [&count3](int x){count3 += x % 3 == 0;});

5.包装器

C++11提供了一些包装器。如bind可替代bind1st和bind2nd,但更灵活;模板mem_fn能够将成员函数作为常规函数进行传递;模板reference_wrapper能够创建因为像引用但可被复制的对象;包装器function能够以统一的方式处理多种类似与函数的形式。下面详细地介绍包装器function。

调用特征标是有返回类型以及用括号括起并用逗号分隔的参数类型列表定义的。

模板function是在头文件function中声明的,它从调用特征标的角度定义了一个对象,可用于包装调用特征标相同的函数指针、函数对象或lambda表达式。例如,下面的声明创建一个名为fdci的function对象,它接受一个char参数和一个int参数,并返回一个double值:

std::function<double(char, int)> fdci;

然后,可以将接受一个char参数和一个int参数,并返回一个double值的任何函数指针、函数对象或lambda表达式赋给它。

这样就可以将包装器传递给模板参数,或把包装器作为模板参数。

6.可变参数模板

C++11提供了一个用省略号表示的元运算符,能够声明表示模板参数包的标识符,模板参数包基本上是一个类型列表。同样,它还能够声明表示函数参数包的标识符,而函数参数包基本上是一个值列表。其语法如下:

template<typename... Args>
void show_list(Args... args)
{
	//...
}

这意味着函数参数包args包含的值列表与模板参数包Args包含的类型列表匹配——无论是类型还是数量。

那么要如何展开参数包呢?使用递归:

template<typename T, typename... Args>
void show_list(T value, Args... args)
{
	std::cout << value <<,;
	show_list(args...);
}

上面的版本在输出时每一项后面都会显示一个逗号,但如果能省去最后一个逗号就好了。为此,可以添加一个处理一项的模板,并让其行为与通用模板稍有不同:

template<typename T>
void show_list(T value)
{
	std::cout << value << ‘\n’;
}

这样,当args包缩短到只有一项时,将调用这个版本,而它打印换行符而不是逗号。

可以指定参数包的展开模式,为此,可以将下述代码:

show_list(Args... args);

替换为如下代码:

show_list(const Args &... args);

这样将对每个函数参数应用模式const &。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值