来自面向对象思考的个人blog:https://blog.csdn.net/craftsman1970/article/details/79717944
搞了挺长时间rust,C++的语法知识忘得差不多了,是时候捡一捡了。
Table of Contents
4:const, const expression和constexpr
50:正则表达式库(regular-expression library)
53:有作用域的enum(scoped enumeration)
56:标准库mem_fn类模板和std::function()
1:longlong
数据类型long long是在C++11中重新定义的,标准规定它最小是64bit
在这之前为了提供超过32bit的整数,各个开发环境(编译器)分别定义了各自的64bit整数类型。结果当然就是影响了代码地兼容性。
现在好了。C++11直接定义了long long类型。
我猜许多人应该使用过这个类型,当然在C++11之前,这种尝试会被编译器无情拒绝,自C++11之后就不会在发生这样地情况了。因此我认为:在C++11新特性中,long long一定是最容易被接受的一个。多数程序员看到它时甚至不会意识到这是一个新特性。
相应地,C++11规定:在指定long long字面值类型时,使用ll或LL。这也可以从long的l或L推断出来。
2:列表初始化
C++11中扩展了使用花括号初始化变量的应用范围,称这种初始化方式为列表初始化。
例如:
可以像下面这样初始化vector:
vector<int> int_vector = {5, 4, 3, 2, 1};
可以像下面这样初始化list:
list<int> int_list = {5, 4, 3, 2, 1};
甚至可以像下面这样初始化map
map<int, const char*> id2Name = {{1,"Zhang"},{2, "Wang"},{3, "Li"}};
另一种形式
下面和写法也合法,和上面的几种写法等价。
vector<int> int_vector{5, 4, 3, 2, 1};
list<int> int_list {5, 4, 3, 2, 1};
map<int, const char*> id2Name{{1,"Zhang"},{2, "Wang"},{3, "Li"}};
3:空指针(nullptr)
假设我们有下面的代码:
void output(int value){
std::cout << "value=" << value << std::endl;
}
void output(char* msg){
if(msg != 0){
std::cout << "msg=" << msg << std::endl;
}else{
std::cout << "msg=NULL" << std::endl;
}
}
这是两个重载的函数。如果我们希望调用output(char*)函数,下面的那种方式都不行:
output(0);
output(NULL);
原因很简单,0是整数,NULL实际上也是整数0。当然也不是没有办法,代码可以改成这样:
outpu((char*)0);
output((char*)NULL);
其实我们需要的就是一个字面值来表示空指针,而不是借用数字0。C++11中定义了这个字面值:nullptr。有个它,再调用output函数是就清晰多了。
output(0); //int参数版本
output(nullptr); //char* 参数版本。
当然,下面的代码也会发生编译错误。
int i = nullptr;
4:const, const expression和constexpr
C++11允许将变量声明为constexpr类型以便由编译器验证变量的值是否是一个常量表达式。
变量声明为constexpr类型,就意味着一方面变量本身是常量,也意味着它必须用常量表达式来初始化。
constexpr int mf = 20;
constexpr int limit = mf + 1;
如果初始值不是常量表达式,那么就会发生编译错误。
constexpr函数
除了能用常量表达式初始化constexpr变量以外,还可以使用constexpr函数。它是指能用于常量表达式的函数,也就是说它的计算结果可以在编译 时确定。定义的方法就是在返回值类型前加constexpr关键字。但是为了保证计算结果可以在编译是确定,必须满足以下条件:
返回值和形参必须都是字面值类型。
函数中只能有一条return语句。
例如下面的计算阶乘的constexpr函数。
constexpr long long factorial(int n){
return n <= 1? 1 : (n * factorial(n - 1));
}
constexpr long long f18 = factorial(20);
可以用它来初始化constexpr变量。所有计算都在编译时完成。比较有趣的是像溢出这样错误也会在编译是被检出。
简单地说
constexpr可以
加强初始值的检查
计算的时机从运行时提前到编译时,比宏定义效率更高。
5:类型别名
假设有一个二维图形计算的程序,定义了一个point结构体。
struct point
{
int x;
int y;
};
在有些系统中,int类型的精度,范围都足够,在其他的系统中可能就不能满足需求,可能需要扩大字长,或者需要提高精度等等。
方法有多种,其中之一就是定义别名。在C++11中定义别名的方法如下:
using dtype = int;
它的含义是为int指定一个别名,dtype。指定别名以后,point结构体变成下面这样:
struct point
{
dtype x;
dtype y;
};
这样一来,只要改变dtype所对应的数据类型,所有使用point的代码都会适应这种变化。
类型别名和typedef有什么区别?
typedef也能提相同的功能,但是形式略有不同。
typedef int dtype; //等价于using dtype = int;
typedef vector<point> PointVector; //等价于using PointVector = vector<point>
typedef void(*PtoF)(int); //等价于using PtoF=void(*)(int);
C++11的别名定义方式似乎更容易理解一些。除此以外区别似乎不大。
6:auto类型修饰符
看下面的代码:
vector<point> v = {{1, 2}, {3, 4}};
vector<point>::iterator it = v.begin();
while(it != v.end()){
cout << (*it).x << "," << (*it).y << endl;
it++;
}
如果实际编过这样的程序,就一定有过这样的经验:不知道如何定义it的类型。C++11中可以使用auto,就像下面这样:
vector<point> v = {{1, 2}, {3, 4}};
auto it = v.begin();
while(it != v.end()){
cout << (*it).x << "," << (*it).y << endl;
it++;
}
7:decltype修饰符
在存在初始化代码的情况下,可以使用auto来自动决定变量的类型。还存在另外一种情况,我们希望变量的类型通过初始化代码以外的表达式推断得到。
假设有下面的结构体:
struct Point{
int x;
int y;
};
在其他地方,可能这样定义point类型的变量:
Point point;
同样我们也可以定义指向point的指针:
Point* p1 = nullptr;
在C++11中提供了另一种方式来决定变量的类型:decltype修饰符。利用它可以通过表达式的类型来决定变量的类型:
decltype(point)* p2 = nullptr;
这两种方式有什么不同呢?当point的类型发生变化时,p1的类型需要一起修改,p2的类型就不需要修改。
8:类内初始化
C++11中引入了类内初始化器,以减少构造函数和初始化代码的数量。说起来挺玄,其实就是下面代码中背景加亮的部分。
class Rect
{
public:
Rect(int l, int t, int r, int b)
:left{l}, top{t}, right{r}, bottom{b}
{
}
Rect(int l, int t, int r, int b, LineStyle ls)
:left{l}, top{t}, right{r}, bottom{b}
,style{ls}
{
}
private:
int top{0};
int left{0};
int right{0};
int bottom{0};
LineStyle style{lsSolid};
};
类内初始化之后,构造函数只需要负责和缺省值不同的部分就好,代码精炼很多了。
9:范围for语句
C++11引入了范围for语句:
int array[]{1, 2, 3, 4};
int sum = 0;
for(int a : array){
sum += a;
}
vector<int> vect{1, 2, 3, 4};
int sum = 0;
for(int v: vect){
sum += v;
}
for(int v : vect)读作“对于vect中的每一个v”。应该说,程序简练了不少。
运用条件
是不是所有地数据集合可以交给范围for遍历呢?答案是否定的。
数据v被范围for遍历的条件是,该数据支持v.begin()/v.end()或者是begin(v)/end(v)并返回一个迭代器。STL中的容器都满足上述条件。对于内置类型的数组来讲C++编译器提供了等同于上述接口的机制,因此也可以在范围for中使用。
10:容器的cbegin和cend函数
vector本身是const类型,生成的迭代器就必须是const类型。这样,在编译层次就避免了可能发生的对vector数据的修改。
还有另外一种情况,数据本身不是const类型,但是从设计的角度来讲有些处理不应该修改该数据。这时也应该要求const类型的迭代器,以避免数据被意外修改。
C++11为此提供了cbegin和cend方法。
vector<int> v{1, 2, 3, 4, 5, 6};、
auto ait = v.cbegin();
while(ait != v.cend()){
sum += *ait;
*ait = sum; //编译错误
ait++;
}
cbegin()/cend()决定了返回的迭代器类型为const。这时即使vector的类型不是const,也可以防止对该数据的误操作。
11:标准库函数begin和end
C++11引入了begin和end函数。使用begin和end的代码如下:
for(int* p1 = begin(a1); p1 != end(a1); ++p1){
cout << *p1 << endl;
}
12:initializer_list形参
C++11中的可变参数
C++11在标准库中提供了initializer_list类,用于处理参数数量可变但类型相同的情况。使用initializer_list最常用的方式是通过大括号包围的值列表对其进行初始化:
initializer_list<int> vlist{9, 8, 7, 6};
除了不能修改vlist中的值以外,可以像一般的list一样使用。
继续看下面的函数:
template<typename T>
void output(initializer_list<T> lst)
{
for(auto &a : lst){
cout << a << endl;
}
}
这个函数很简单,就是输出list中的内容,它有几个特点:
通过模版,auto的使用,是它可以自动适应参数的类型
通过initializer_list的使用,自动适应参数的个数。
函数弄好以后,怎么使用就可以看心情了。
initializer_list<int> vlist{9, 8, 7, 6};
output(vlist);
output({1, 3, 4, 5});
output({"How", "are", "you", "!"});
13:返回类型后置
除了构造函数和析构函数以外,函数声明都需要明确函数的返回类型,在传统的C或者C++中,函数声明大致是这个样子:
int getSum(int a, int b);
第一个int就是函数的返回类型,它表明函数的返回值类型为整数。在新的C++11以后,我们也可以这样声明:
auto getSum(int a, int b)->int;
在原来放返回值类型的位置写auto,在函数声明结束以后接一个'->'再跟着写函数的返回值类型。两种方式的效果是一样的。
14:使用=default生成默认构造函数
先看下面代码:
struct Point{
int x;
int y;
};
一旦因为其他原因添加了其他有参数的构造函数,编译器就不再生成缺省的构造函数了。
C++11允许我们使用=default来要求编译器生成一个默认构造函数:
struct Point{
Point()=default;
Point(int _x, int _y):x(_x),y(_y){}
int x = 0;
int y = 0;
};
如果是自己编写的无参构造函数的话,就需要指定成员的构造方式。默认构造函数会对数据成员进行默认初始化,所以就不需要另外指定了。这样可以省去一些麻烦。
15:array容器
C++11中引入了array容器,基本上解决了内置数组的问题:
std::array<int, 5> c11ary;
c11ary.fill(0);
unsigned int i = 0;
while(i<c11ary.size()){
c11ary.at(i) = i;
i++;
}
这段代码中,
使用fill方法实现了数据填充。
使用size方法取得数组的大小。
虽然at(i)方法实现带有越界检查的读写。
16:右值引用
代码如下
void merge(Image&& img){
//接管img中的数据。
img.height = 0;
img.width = 0;
img.data = nullptr;
}
我们将参数声明为右值引用,要求像一个临时变量一样任性地使用数据。使用这个函数的方法如下:
Image img1(100, 100);
Image img2(100, 200);
img1.merge(std::move(img2));
注意代码中的std::move,这是标准库中提供的方法,它可以将左值显式转换为右值引用类型,从而告诉编译器,可以像右值(临时变量)一样处理它。同时也意味着接下来除了对img2赋值或销毁以外,不再使用它。
C++11通过使用右值引用提供了一种接管数据的标准方法。
17:更快的swap
C++11之前的swap
先看swap的实现:
template<classT>voidswap ( T& a, T& b )
{
T c(a); a=b; b=c;
}
下面结合示例下面的代码看看发生了什么。
当swap调用了T C(a)的时候,实际上是调用了拷贝构造函数,当swap代码调用了赋值操作时,实际上是调用了赋值运算符。
由于拷贝构造函数和赋值运算符包含内存拷贝操作,而这样的操作共执行了三次,所以在一个swap中一共存在三次内存拷贝的操作。这种不必要的内存操作很多情况下都会影响C++的执行效率。
C++11之后的swap
引入了右值引用和数据移动的概念之后,代码变成下面的样子:
template<classT>voidswap (T& a, T& b)
{
T c(std::move(a)); a=std::move(b); b=std::move(c);
}
由于std::move将变量类型转换为右值引用,TestData有机会提供下面针对右值引用的构造函数和赋值运算符。
TestData(TestData&& d)
:size(d.size)
,data(d.data)
{
d.size = 0;
d.data = nullptr;
}
TestData& operator=(const TestData&& d)
{
size = d.size;
data = d.data;
return *this;
}
由于代码中使用内存移管代替了不必要的内存拷贝,因此效率会大大提高。
如果观察C++11的标准库,会发现很多类都增加了右值引用的参数,这实际上就是对数据移动的支持,也就是对高效率的支持。
18:容器的insert成员
填充多个元素
iterator insert (const_iterator position,
size_type n,
const value_type& val);
这个方法可以在指定位置填充n个val。除了参数以外,方法的返回值从void变为iterator,返回最后一个添加的元素的位置。有了这个返回值,在同一个位置填充元素就会很方便。例如下面的代码:
std::list<int> demo{1, 2, 3, 4, 5, 6};
auto position = std::find(demo.begin(), demo.end(), 3);
for(int i = 0; i < 5; ++i){
position = demo.insert(position, 2 , i);
}
for (int var: demo) {
std::cout << var << ",";
}
std::cout << endl;
在3的前面连续添加0,1,2,3,4。代码输出如下:
1,2,4,4,3,3,2,2,1,1,0,0,3,4,5,6,
以移动方式插入数据
iterator insert (const_iterator position,
value_type&& val);
这个方法是C++11中追加的新方法,提供了对数据移动的支持。实例代码如下:
std::list<string> strlist{"are", "you"};
std::string str("How");
strlist.insert(strlist.begin(), std::move(str));
for (auto var: strlist) {
std::cout << var << ",";
}
std::cout << endl;
std::cout << "str=" << str << endl;
输出结果为:
How,are,you,
str=
支持initializer_list
这个方法也是C++11中新追加的,提供对initializer_list的支持。示例代码如下:
strlist.insert(strlist.begin(), {"C++", "11"});
for (auto var: strlist) {
std::cout << var << ",";
}
std::cout << endl;
在前面示例的基础上添加再次在list的开头插和“C++”和“11”两个字符串。执行结果如下:
C++,11,How,are,you,
19:容器的emplace成员
考虑下面的Rect类:
struct Rect
{
Rect(int l, int t, int r, int b)
:left{l}, top{t}
,right{r}, bottom{b}
{}
int left;
int top;
int right;
int bottom;
};
当需要向容器添加Rect对象时,代码大致是这样的:
std::list<Rect> rlist;
rlist.push_front(Rect(10, 10, 20, 20));
在调用push_front时,首先构造一个临时的Rect对象传递给push_front方法,然后在push_front的内部,在复制一个Rect对象添加到容器中。全过程会发生一次创建动作和一次拷贝动作,才能将对象的内容添加到list当中去。
为了减少拷贝动作的次数,当然可以使用右值引用参数的成员函数。除此之外,C++11还提供了另一种方法:emplace成员。使用这个成员可以直接传递用于生成对象的参数,对象的创建过程交给容器去执行:
std::list<Rect> rlist;
rlist.emplace_front(10, 10, 20, 20);
用法非常简单,只要保证参数和元素构造函数的参数相同即可。
除了emplace_front以外,C++11还提供了emplace和emplace_back方法,分别对应insert和push_back方法。
20:管理容器的容量
capacity和size
理解capacity和size的区别非常重要,容器的size是指已经保存在容器中的数据的个数,而容量是指在不再重新分配内存的前提下容器最大可以包含的数据的个数。举个例子:容量为2升的瓶子装了1升水。2升是capacity,1升是size。
管理容器的容量
在绝大多数情况下,程序员不必关注容器类内存管理的细节,把这些工作完全交给C++标准库。但是有时也会有例外:
要求操作的响应非常快,快到不能忽略从堆中申请内存的时间。
使用的空间非常大,大到不希望容器保持多余的内存空间。
这时就需要主动干预内存的取得和释放动作。C++标准库为此提供了相应的成员函数。
capacity:取得容器的容量
size:取得已经保存在容器中数据的个数。
reserve:分配至少可以容纳指定数量元素的内存空间。
shrink_to_fit:释放多余的内存空间,只保留可以容纳容器中数据的最小内存。
21:string的数值转换函数
string是标准库中最常用的类,说活跃在字符串处理的各种场景中。但是长期以来string和数值之间的转换一直比较繁琐。这种情况到C++11以后有了很大的改观,因为标准库中为string和数值的相互转换提供了一套函数。
数值到string
函 数非常简单明快,只需要一个函数:to_string。但是实际上它是一组重载的函数,分别对应从int,long,long long,unsigned int,unsigned long,unsigned long long,float,double,long double到string的转换。
使用起来也非常简单:
string value = to_string(2.5);
string到数值
针对基本的数值类型,C++11提供了相应的转换方法。
stoi:string 到 int
stol: string 到 long
stoll:string 到 long long
stoul:sting 到 unsigned long
stoull:string 到 unsigned long long.
stof:string 到 float
stod:string 到 double
stold:string 到 long double.
以下是示例代码:
string value = to_string(2.5);
int iv = stoi(value);
cout << "iv=" << iv << endl;
double dv = stod(value);
cout << "dv=" << dv << endl;
内容为"2.5"的字符串,转换为整数以后结果是2,转换为double以后的结果是2.5。
string values("1.0,2.4,3.5,4.6,5.7");
while(values.size()>0){
string::size_type sz;
cout << stod(values, &sz) << endl;
if(sz < values.size()){
values = values.substr(sz + 1);
}
else{
values.clear();
}
}
22:lambda表达式
可以认为lambda表达式取得信息有两种方式,或者说两个时机:一个是参数列表,其内容是在表达式被调用时决定;另一个捕获列表,其内容是在是表达式被创建的时候决定,本文讨论捕获列表。
值捕获
先看如下代码:
int factor = 2;
auto multiply = [factor](int value)
{return factor * value;};
factor = 4;
cout << multiply(2) << endl;
代码中首先为factor赋值2,创建lambda表达式以后,再次赋值4。由于lambda表达式的捕获是在该表达式创建是进行的,而第二次赋值在lambda表达式创建之后,所以muliply(2)的执行结果为4。
引用捕获
还是这段代码,只要在捕获列表中变量的前面多了一个&,就变成了引用捕获。
int factor = 2;
auto multiply = [&factor](int value)
{return factor * value;};
factor = 4;
cout << multiply(2) << endl;
捕获的时机并没有变化,只是捕获的是factor的引用而不是factor的值,所以定义lambda之后,对factor再次赋值4依然会影响multiply(2)的结果。此时的输出为8。
隐式捕获
前面例子中使用捕获列表时,具体指定了变量名,属于显式捕获。另外还有隐式捕获,由lambda表达式推断需要捕获的变量。具体方法是:
当需要隐式值捕获时,使用[=];
当需要隐式引用捕获时,使用[&];
在上面例子中使用隐式捕获以后,结果不会发生变化。
可变lambda
假设有如下vector,保存的内容是学生的考试成绩:
vector<int> score{45, 70, 56, 86, 28, 60, 90};
可以用以下代码来寻找第一个及格成绩:
find_if(score.begin(), score.end(),
[](int v){return (v >=60);});
如果需要找到第n个及格成绩,很自然地会考虑使用下面的代码:
int counter = 2;
find_if(score.begin(), score.end(),
[counter](int v){
return (v >=60)&&(--counter == 0);
});
但 是很遗憾,这时会出现编译错误,告诉你counter是只读的。其原因是因为在lambda表达式中很少有需要修改捕获值的场景,因此默认捕获值具有 const属性。如果出现本例这样,确实希望修改捕获值的情况,C++11使用mutable关键字来解决这个问题。来看完整代码:
int counter = 2;
auto iter find_if(score.begin(), score.end(),
[counter](int v)mutable{
return (v >=60)&&(--counter == 0);
});
cout << *iter << endl;
当然了,由于是值捕获,处于lambda表达式外面的counter值依然不会改变。
如果希望连外面的counter一起修改,使用引用捕获即可。
23:参数绑定
lambda表达式也有缺点:在类似功能多次使用的时候,每次定义lambada表达式也会比较麻烦。本文介绍另一种方式:参数绑定。
标准库bind函数
继续用lambda表达式中用过的例子,如果希望找到第一个长度小于2的string,可以使用以下代码:
bool istarget(const string& s){
return s.size() < 2;
}
vector<string> v{"This","is", "a", "predicate", "."};
auto found = find_if(v.begin(), v.end(), istarget);
cout << *found << endl;
如果我们希望在istarget中选择string时使用变量而不是固定的2的时候,一般的函数就不能满足需求了(虽然使用全局变量算是一个选项)。除了和函数对象和lambda表达式以外,还可以使用标准库bind函数来实现,其步骤如下:
根据需求定义比较函数
在本例中,就是定义一个接受选择对象string对象和最小长度参数的istarget函数:
bool istarget(const string& s, int sz){
return s.size() < sz;
}
使用参数绑定定义新的可调用对象
C++11标准库提供了一个bind函数,按照C++ Primer的说法,可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。调应bind的一般形式为:
auto newCallable = bind(callable, arg_list);
具体到本例,可以这样定义
auto isTarget = bind(istarget, _1, 2);
istarget:bind适配的对象,就是第一步中定义具有两个参数的istarget函数
接下来是传递给istarget的参数。参数的顺序和istarget参数列表中规定的一致。
_1:占位符,_1代表isTarget被调用时的接受的第一个实参,这个_1处在bind参数列表的第一个位置表明isTarget的第一个实参会在调用istarget时作为istarget的第一个实参使用。
2:比较长度信息,形式和占位符不同,处在参数列表的第二个位置,这个值会在调用istarget时作为istarget的第二个实参使用。
istarget函数定义一次之后,可以使用bind函数适应各种算法的要求,从而实现了实现一次定义,多次使用的目标。
#include "../include/hello.h"
using namespace std;
int main(){
vector<int> v{11,22,33,44,55,66};
unsigned int count =2;
auto fun1 = [&count](int a, int b)mutable{
cout<<b<<endl;
return (a>=44)&&(--count == 0);};
auto fun_new = std::bind(fun1,std::placeholders::_1,888);
auto iter = find_if(v.begin(),v.end(),fun_new);
cout<<*iter<<endl;
cout<<count<<endl;
return 0;
}
24:无序关联容器
C++11另外引入了4种无序关联容器(unordered associative container)。这些容器将存储组织为一组桶,根据哈希值将数据映射到桶。与有序关联容器类似,无序关联容器也可以用同样的标准分类:
除了哈希管理操作以外,无序容器还提供了与有序容器相同的操作。也就是说有序容器和无序容器可以互换
25:智能指针shared_ptr
基本原理
shared_ptr在内部维护一个相当于引用计数的机制,允许多个指针同时指向一个对象。某个指针被销毁之后,引用计数同时较少,当所有指针都被销毁之后,自动释放管理的对象。
声明和初始化
shared_ptr是一个模版类,在声明时必须声明指向对象的类型:
上面的代码只是定义了一个空的shared_ptr,如果需要同时初始化,最安全的方式是使用make_shared标准库函数:
之所以说这种方式安全,我想是因为make_shared虽然也生成了MyString对象,但是这个对象直接装配到shared_ptr上,利用侧根本摸不着。
赋值
我们也可以把一个shared_ptr的值赋值给另一个shared_ptr:
使用shared_ptr
可以像普通指针一样使用shared_ptr:
代码全貌
输出结果:
shared_prt的本身是一个类,所以它的初始化实际上就是调用shared_ptr类的构造函数。通过分析shared_ptr的构造函数,就可以准确把握shared_ptr初始化的方法。
https://blog.csdn.net/craftsman1970/article/details/80765625
修饰符说明
explicit:保证该构造函数不会被隐式调用
noexcept:该函数不会抛出异常,
constexpr:该函数可以在编译期间求值
26:智能指针unique_ptr
可以自动管理内存,只是这块内存不和其他的unique_ptr分享
由于unique_ptr对于内存的独占特性,unique_ptr不支持直接的赋值操作,而只能支持右值引用的赋值,基本形式如下:
必须是先前的持有者明确放弃权利之后,才能赋值给新的持有者。实际的程序中,上面的代码并没有太大的意义,真正常见的应该是下面的代码:
getvalue函数返回的是一个右值,所以也会执行右值引用赋值
从函数返回unique_ptr的时候涉及到一个例外:即将销毁的unique_ptr可以被拷贝或赋值。
27:智能指针weak_ptr
weak_ptr看起来更像shared_ptr的附属品,它从shared_ptr衍生,但不会控制所指向对象的生命周期。weak_ptr的弱就弱在这里。
https://blog.csdn.net/craftsman1970/article/details/80834199
在使用上,C++11标准库没有提供通过weak_ptr直接访问对象的方法,而是调用weak_ptr的lock方法生成一个shared_ptr,再通过shared_ptr访问对象。
输出结果为 127.
使用shared_ptr以后,代码不再需要显式释放申请的内存,使内存的管理更加简单。
使用weak_ptr之后,可以通过lock方法来确认对象是否有效,使得内存的相互参照的管理更加容易。
28:将=default用于拷贝控制成员
作为C++编译器,一旦程序员定义了构造函数,就可以认为默认初始化已经不能满足程序员的需求而不再生成默认的构造函数了。这种处理方式在大多数情况下会更安全。
C++11以后,如果程序员还是希望编译器生成默认构造函数,可以通过=default来实现。
和自动生成默认构造函数的规则类似,如果程序员定义了某个拷贝控制成员,编译器就不再自动生成其他的。
当然也存在像浅拷贝那样,编译器生成的拷贝控制成员就可以满足需求的情况,这时可以对拷贝控制成员使用=default要求编译器生成某些拷贝控制成员。
29:使用=delete 阻止拷贝类对象
如果编译器没有生成默认构造函数或拷贝控制函数,可以使用=default要求编译器生成;同样地,有时我们也会希望某些函数函数不要被调用,这时可以使用=delete修饰该函数。
举个例子:
例如在Singleton设计模式中就希望类的对象只能通过getInstance静态方法得到。在C++11发布之前,类是通过将其拷贝构造函数和赋值运算符私有化来实现的。
C++11增加了=delete修饰符,明确表达虽然声明了某函数,但是又禁止它们被使用的意思。本例中的拷贝构造函数和赋值运算符可以如下声明:
这样做最直接的效果就是,test方法本身就会发生编译错误(编译时),而不需要等到test方法真正被使用时(运行时)。
30:用移动类对象代替拷贝类对象
31:移动构造函数通常应该是noexcept
https://blog.csdn.net/craftsman1970/article/details/81087262
移动构造函数通常应该是noexcept,即,不会抛出异常的移动构造函数
拷贝构造函数通常伴随着内存分配操作,因此很可能会抛出异常;移动构造函数一般是移动内存的所有权,所以一般不会抛出异常。
C++11中新引入了一个noexcept关键字,用来向程序员,编译器来表明这种情况。
对于永远不会抛出异常的函数,可以声明为noexcept的。这一方面有助于程序员推断程序逻辑,另一方面编译器可以更好地优化代码。
32:移动迭代器
https://blog.csdn.net/craftsman1970/article/details/81122149
2 using namespace std;
3
4 class Test{
5 public:
6 Test()
7 {
8 cout<<"default construction"<<endl;
9 };
10 ~Test(){};
11 Test(const Test& t){
12 cout<<"copy construction"<<endl;
13 }
14
15 Test& operator=(const Test&t){
16 cout<<"equal construction"<<endl;
17 return *this;
18 }
19 Test(const Test&& t){
20 cout<<"move copy construction"<<endl;
21 }
22
23 Test& operator=(const Test&& t)
24 {
25 cout<<"move equal construction"<<endl;
26 return *this;
27 }
28
29
30 };
31
32
33 int main(){
34 Test ts[4];
35 Test dts[4];
36 copy(begin(ts),end(ts),begin(dts));
构造函数,默认构造函数,拷贝构造函数分别被执行4次
至此,引出C++11的概念
int main(){
34 Test ts[4];
35 allocator<Test> alloc;
36 auto dts = alloc.allocate(4);
37
38 uninitialized_copy(make_move_iterator(begin(ts)),make_move_iterator(end(ts)),dts);
代码首先使用allocator预先取得保存对象的内存空间而不调用初始化函数。
然后使用unitialize_copy来迭代调用每个对象的构造函数。这里又存在两种情况:如果只是简单地使用通常的迭代器,那么被调用的将是拷贝构造函 数;本例中使用的make_move_iterator适配器告诉编译器迭代对象是可以移动的,因此调用的是移动构造函数。
这种可以生成右值引用的迭代器就是移动迭代器。
可以看出,实现了和单个实例同样的高效率。
33:引用限定成员函数
引用限定符(reference qualifier)
目的很简单,就是希望加一个限制,使得右值对象不能调用setText方法。手段也同样简单,只要在方法签名的后面添加一个“&“,就可以通知编译器,这个函数只对左值(引用)有效。就像下面这样:
添加了引用限定以后,下面的代码就会产生编译错误。
添加引用限定符的位置也是添加const, noexcept的位置,如果需要同时添加的话,引用限定符需要在const之后,noexcept之前。
34:function类模板
代码首先是function<int(int,int)>定义了返回值为int,参数为两个int的可调用对象类型,然后定义了从string到该类型的映射。
接下来就是初始化映射和使用映射了。
在本例中可以看到:虽然每个操作的类型并不相同,但是由于它们拥有相同的调用形式,通过引入function类模版,可以像同一种类型一样使用它们。
35:explicit类型转换运算符
36:override说明符
在实际的开发中随着开发规模的扩大,类的继承关系会变得越来越深,成员函数的参数也会越来越多,经常会遇到派生类中定义的成员函数的签名和覆盖对象的签名不一致的而导致覆盖失败的情况。
而且要命的是,这种错误不会产生编译错误,不容易被发现。
为了解决这个问题,C++11中引入了一个方法:在声明、定义派生类中的覆盖函数时使用override说明符:
由于明确的函数的用意,所以当编译器无法在基类中找到相同签名的虚函数的时候,就会产生编译错误
37:final说明符
38:delete说明符
39:继承的构造函数
直接上代码:
使用前:
1 #include "../include/hello.h"
2 using namespace std;
3
4 class Test{
5 public:
6 Test(double a,double b):m_a(a),m_b(b)
7 {
8 cout<<"default construction"<<endl;
9 }
10
11 ~Test(){};
12
13 double m_a = 0;
14 double m_b = 0;
15
16
17 };
18
19
20 class Test1 : public Test{
21 public:
22 Test1(double a, double b ):Test(a,b)
23 {
24 }
25 ~Test1(){};
26
27
28 };
29
30 int main(){
31
32 Test c1(1,1);
33 Test1 c2(1,1);
34
35 return 0;
36 }
使用后:
1 #include "../include/hello.h"
2 using namespace std;
3
4 class Test{
5 public:
6 Test(double a,double b):m_a(a),m_b(b)
7 {
8 cout<<"default construction"<<endl;
9 }
10 ~Test(){};
11 double m_a = 0;
12 double m_b = 0;
13 };
14
15 class Test1 : public Test{
16 public:
17 using Test::Test;
18 ~Test1(){};
19 };
20
21 int main(){
22
23 Test c1(1,1);
24 Test1 c2(1,1);
25
26 return 0;
27 }
注意Test1类的构造函数,是不是肥肠的方便。
40:模板类型别名
还是废话少说,上代码:
1 #include "../include/hello.h"
2 using namespace std;
3
4 class Test{
5 public:
6 Test()=default;
7 Test(double a,double b):m_a(a),m_b(b)
8 {
9 cout<<"default construction"<<endl;
10 }
11 ~Test(){};
12 double m_a = 0;
13 double m_b = 0;
14 };
15
16 class Test1 {
17 public:
18 Test1(){};
19 ~Test1(){};
20 };
21
22 typedef map<int,int> IntMap;
23 template<typename T> using TemMap = map<int,T>;
24
25 int main(){
26 //=============================
27 map<int,int> map1;
28 map<int,int>map2;
29 IntMap map3;
30 //=============================
31 map<int,Test> map4;
32 map<int,Test1> map5;
33 TemMap<Test> map6;
34 //====================================
35 return 0;
36 }
41:模板函数的默认模板参数
template<typename T, typename F=less<T>>
int compare(const T &v1, const T &v2, F f=F())
{
if(f(v1,v2)) return -1;
if(f(v2,v1)) return 1;
return 0;
}
int main(){
cout<<compare(1,2)<<endl;
cout<<compare(1,2,greater<int>())<<endl;
}
除了F=less<T>部分以外,就是一个普通的模板比较函数。而高亮的部分就是本文的主题:模板函数的模板参数。这种写法的含义就是如果程序员没有指定第二个模板参 数,编译器就默认使用less<T>;如果程序员另外指定了模板参数,例如greater<T>,那么就使用指定的那个模板参 数。
第一行没有指定第三个模板参数,并且1<2,所以返回-1;第二行另外指定了greater<int>,执行的是相反的比较逻辑,返回1。
模板函数的默认模板参数很像函数的默认参数,无论是用法还是注意事项。
42:模板函数与返回类型后置
渐进式说明
最简单的情况
先考虑我们有一个函数,功能是从一个整数数组中取得其中一个元素。
代码很简单,但这只是一个引子。本文的所有示例代码都不考虑下标越界的情况,这样可以更加突出主题。
适用于其他类型
如果希望这个函数可以适应更多类型的数组,只要引入模板即可。
也没难多少。
更加通用
如果除了数据类型可以扩展之外,还希望可以将其应用于vector的话,就没有那么容易了。例如下面的代码是不能通过编译的。
不能通过编译是由于解引用不是数据类型,而是操作。如果模板变量为T,而返回值为T*的话是可以正常编译的。
解决这个问题的方法是使用前面讲到过的C++11新特性:返回值类型后置和decltype。代码如下:
由于decltype需要取得it解引用的类型,所以取得返回值类型的操作必须在it出现之后,即所谓的返回值类型后置。有了这个模板函数之后,下面的3中情况,代码都能够正确无误地执行:
43:引用合并和完美转发
C++11的另一个新特性,当引用和右值引用同时出现时,遵循下面的原则:
左值引用 + 左值引用 = 左值引用
左值引用 + 右值引用 = 左值引用
右值引用 + 左值引用 = 左值引用
右值引用 + 右值引用 = 右值引用
上代码:
6 void inc (int& a, int& b)
7 {
8 b = a+99;
9 cout << b <<endl;
10 }
11
12 template <typename F, typename T1, typename T2>
13 void exe(F f, T1&& t1, T2&& t2)
14 {
15 f(t1,t2);
16 }
17 int main(){
18 int a = 0;
19 exe(inc,1,a);
20 cout<<a<<endl;
21 return 0;
22 }
可以发现,左值引用a +右值引用t2 = 左值引用b
归纳起来原则很简单:永远是左值优先。这种现象称为引用合并(reference collapse)。
完美转发是为了解决引用合
并而引入的机制:
上代码:
void inc (int&& a, int&& b)
{
>> cout << a + b <<endl;
}
template <typename F, typename T1, typename T2>
void exe(F f, T1&& t1, T2&& t2)
{
>> f(t1,t2);
}
int main(){
exe(inc,1,1);
return 0;
}
6 void inc (int&& a, int&& b)
7 {
8 cout << a + b <<endl;
9 }
10
11 template <typename F, typename T1, typename T2>
12 void exe(F f, T1&& t1, T2&& t2)
13 {
14 f(std::forward<T1>(t1),std::forward<T2>(t2));
15 }
16 int main(){
17
18 exe(inc,1,1);
19 return 0;
20 }
如此修改,就可以正常运行了。
std::forward是为了在使用右值引用参数的函数模板中解决参数的完美转发问题
std::move和std::forward只是执行转换的函数(确切的说应该是函数模板)。std::move无条件的将它的参数转换成一个右值,而std::forward当特定的条件满足时,才会执行它的转换。
std::move表现为无条件的右值转换,就其本身而已,它不会移动任何东西。 std::forward仅当参数被右值绑定时,才会把参数转换为右值。 std::move和std::forward在运行时不做任何事情。
move属于强转,forward对于左值还是会转换成左值,对于右值转换成右值。一般在模板元编程里面,对于forward需求比较多,因为可以处理各种不同场景。而一般的代码里面,由于可以确认传入的是左值还是右值,所以一般直接就调用std::move了。
44:可变参数模板
C++11增加了可变参数模板,可以接受可变数目,类型的参数。我们通过开发中常用的输出调试信息的例子介绍绍可变参数模板的使用方法。首先是声明可变参数模板。
在模板参数定义的部分,通过typename...定义可变模板参数。三个点的含义和C语言中的可变参数定义类似,不同的是后面接着指定了可变参数列表的名称Args。
在参数定义的部分,使用可变模板参数定义的Args以相同的格式定义函数的可变参数列表。
这个参数列表可以在模板函数的实现中直接使用,这样在定义可变参数模板时就解决了可变参数函数的第二个难点。
可变参数模板的实现
可变参数模板的实现通常需要一些小技巧:递归和重载。还是先看代码。
template<typename T>
void write(const T& t)
{
cout<<t<<endl;
}
template<typename T, typename... Args>
void write(const T& t, const Args... rest)
{
write(t);
write(rest...);
}
int main(){
write(1,2,3,4,5,6);
return 0;
}
代码中定义了两个重载的writeLog函数,一个只接受类型为T的参数t,另一个除了t之外,还接受可变参数rest。当使用一个参数调用writeLog的时候,实际调用上面的函数;当使用多个参数调用writeLog的时候调用下面的writeLog。
下面的writeLog首先使用第一个参数t调用上面的writeLog之后,使用rest递归调用writeLog(严格讲是rest中有多于一个参数的 时候)。从调用者来看,每次处理一个参数之后,使用其余的参数再次调用writeLog,直到最后调用一个参数的writeLog
例如log输出中经常需要的时间信息,就可以这样实现:
using namespace std;
class Logtime{
public :
Logtime(){
time(&t);
}
void output(){
tm * p = localtime(&t);
cout<<p->tm_hour<<":"<<p->tm_min<<":"<<p->tm_sec;
}
time_t t;
};
void write(Logtime t)
{
t.output();
}
template<typename T>
void write(const T& t)
{
cout<<t<<endl;
}
template<typename T, typename... Args>
void write(const T& t, const Args... rest)
{
write(t);
write(rest...);
}
int main(){
Logtime t;
write(t,1,2,3,4,5,6);
return 0;
}
45:sizeof...运算符
假设有一个程序,需要接受文字信息并生成学生档案,信息的形式为:
"Name:ABC", "Age:20", "Wight:73","Address:Dalian", "Interest:football"
程序解析上述信息后,形成以下形式的数据:
根据本应用的要求,姓名,年龄和体重三项为必填项,地址和兴趣为可选项。
参考前一篇文章的做法,代码可以这样实现:
但是存在一个问题,就是参数数目可能会少于3个,也可能会多于5个。无论哪种情况都不可能生成正确的数据,于是希望在递归处理之前将这些情况排掉。而取得实际参数个数的方法就是sizeof...。参考下面的代码:
46:包扩展
光从形式上来看,两个函数的签名完全不同,但是程序可以正常执行。其原因就是发生了包扩展,编译器根据add的需求将arg包进行了扩展。包扩展的格式就是在包名后面加上三个小点。
template <typename T>
T add(T a,T b, T c){
return a+b+c;
}
template<typename T, typename... Args>
T sum(T t, Args... arg)
{
return add(t,arg...);
}
int main(){
cout<<sum(1,2,3)<<endl;
return 0;
}
第一个实参1赋值给了形参t,arg则包含了另外两个实参2和3。arg展开的结果就是2,3。也就是说,add(t, arg...)等价于add(1, 2, 3)。
47:可变参数模板的参数转发
参考:
https://blog.csdn.net/craftsman1970/article/details/82594888
using namespace std;
class Segment{
public:
Segment()=default;
virtual ~Segment()=default;
virtual ostream& output(ostream& os)const =0;
};
ostream& operator<<(ostream& os, const Segment& seg)
{
return seg.output(os);
}
class StringSegment: public Segment{
public:
StringSegment(string&& str):m_msg(std::move(str)){}
StringSegment(string& str):m_msg(str){}
virtual ~StringSegment()=default;
ostream& output(ostream& os)const {
os<<m_msg;
return os;
}
private:
string m_msg;
};
class MsgHolder{
public:
MsgHolder()=default;
~MsgHolder()=default;
template<typename T>
void add(T&& msg)
{
m_segs.push_back(make_shared<StringSegment>(std::forward<T>(msg)));
}
template<typename T, typename... Args>
void add (T&& t, Args&&... rest){
add(std::forward<T>(t));
add(std::forward<Args>(rest)...);
}
ostream& output (ostream& os)const{
for(auto seg:m_segs){
os<<(*seg);
}
return os;
}
private:
vector<shared_ptr<Segment>> m_segs;
};
ostream& operator<<(ostream& os, MsgHolder& holder)
{
return holder.output(os);
}
int main(){
MsgHolder mh;
string ltest("lvalue test");
string rtest("rvalue test");
mh.add("testof ", ltest,"and", std::move(rtest));
cout<<"mg="<<mh<<endl;
return 0;
}
48:标准库tuple模板
初始化
tuple初始化有几种方式,首先是默认初始化。这种情况下,tuple的每个成员都被默认初始化。
也可以在初始化时指定各个成员的值。下面代码中第一种方式使用的是tuple的构造函数,第二种方式使用的是初始化列表。
使用make_tuple函数配合auto类型指示符,还可以更简洁地初始化tuple。
访问tuple成员
取得成员的数量和类型
如果tuple数据的生成者和使用者不在一个模块中,可能就需要对数据进行某种检查,这时就很可能希望知道成员的数量或者类型。直接上代码。
49:新的bitset运算
https://blog.csdn.net/craftsman1970/article/details/82667464
50:正则表达式库(regular-expression library)
正则表达式(regular expression)是一种描述字符序列的方法,从C++11起,C++正则表达式库(regular-expression library)成为新标准库的一部分。
这块内容,需要的话自行查找学习就行,关键内容还在正则表达式上。
https://blog.csdn.net/craftsman1970/article/details/82748045
51:noexcept异常指示符
软件开发的规模越来越大,函数库/类库的调用层级也越来越多,这时确定一个调用是否会抛出异常也 变得越来越困难。作为解决手段之一,C++11中通过noexcept说明符来对外宣称处理不会抛出异常。这样程序员就不必深入所有的调用层级自己去确认 了。
明明声明了某个处理是noexcept,实际的内部处理还是抛出了异常。在这种情况下,即使进行了错误捕捉,也不会正常工作。
52:内联命名空间(常用于类库升级)
命名空间简介
随着软件开发规模的扩大,类名,函数名重复的可能性也越来越大。最朴素的解决办法就是改名,这种方法在向已经存在的类库中添加代码时问题不大,但是如果是将两个从未谋面的代码库结合在一起时就不再适用了。
C++解决这个问题的办法就是引入命名空间。假设有下面两个命名空间:
代码中分别定义了std_namespace1和lstd_namespace2两个命名空间。在两个命名空间内分别定义了相同名称的Normal类。这两个类的成员函数是否一样其实不重要,这里姑且使用了相同的名称。
有了命名空间以后,使用【命名空间名称::类名】的方式,就可以对这两个同名的Normal类加以区分了。
如果使用using语句,可以让某个命名空间的内容释放出来,就好像它们都存在与外层命名空间一样。
内联命名空间
C++11中引入了内联命名空间(inline namespace),它的特点就是不需要使用using语句就可以直接在外层命名空间使用该命名空间内部的内容,而且无需使用命名空间前缀。先看代码:
内联命名空间的声明方法就是在原来的声明语法前面增加inline关键字。除此之外上面代码还有以下特点:
两处声明的命名空间同名,它们同属一个命名空间。这是C++命名空间从来就有的特性。
第一次声明命名空间时使用了inline关键字,这叫显式内联;第二次没有使用inline关键字,但是由于第一次已经声明了inline,这里声明的还是内联命名空间。这种情况成为隐式内联。
内联命名空间声明之后,就可以在外层命名空间不适用前缀而直接使用它们了。
上述代码中test_inline_namespace处在linline_namespace1的外层,所以可以直接使用Inline1和Inline2。test_inline_namespace2处在更外层,这时也只是需要使用外层命名空间inline_test前缀即可。
看起来inline_namespace就像不存在一样。
前面提到内联命名空间就像不存在一样,那么就产生了一个严肃的问题:它有什么用?为了回答这个问题,我们举一个更加接近实际开发的例子。假设有如下类库代码:
接下来才是重点:假设我们队类库进行了升级,同时又希望:
-
使用者代码不受影响,除非使用者自己想改。
-
可以自由使用新类库的功能
-
如果有需要仍然可以使用原来的类库
解决方法当然是使用内联命名空间。首先是对类库进行处理:
代码中为每个版本的类库定义了命名空间,同时将最新版本定义为内联命名空间。有了这样的准备之后,使用者代码可以是下面这样:
使用最新类库的时候,就好像没有定义过命名空间一样;如果实在是需要原来的类库,可以通用版本前缀加类名的方式。
还有一点很重要:由于隐式内联语法的存在,将来出现ver3的时候,只要将唯一的一个inline关键字移动到第一次出现的ver3定义之前就可以了!
53:有作用域的enum(scoped enumeration)
传统解决方法,将枚举类型的定义放到不同的一个作用域(类或命名空间)中。例如:
这样两个枚举定义就不会发生冲突了。可以用如下方式使用这两个枚举类型:
C++11中引入了限定作用域的枚举类型的概念。其用法如下:
和前面的方式进行比较可以发现:只是在标准的枚举类型定义格式中增加了class关键字。它的效果就是为枚举值同时定义了一个和枚举类型同名的作用域。定义了限定作用域的枚举类型之后,可以以如下方式使用:
54:指定enum类型的大小
C++11新标准中,允许使用enum类型名后接冒号加类型的方式来指定枚举类型的大小。例如我们可以将scope_enum_2的大小指定位8个字节:
55:enum前置声明(包括C++前置声明的复习)
https://blog.csdn.net/craftsman1970/article/details/83063500
代 码中的变化有两点,一个是数据ImportantClass成员的类型变成了指针类型;另一个是include语句变成了class声明语句。两个变化缺 一不可,第二个变化是第一个变化的前提条件。因为这里只是声明了指针类型,所以只要告诉编译器ImportantClass一个类就够了。真正的类型信息 在userclass.cpp中提供。这种方式就是前置声明。
56:标准库mem_fn类模板和std::function()
C++中的可调用对象包括函数,函数对象,lambada表达式,参数绑定等,它们都可以作为算法的参数。
使用function生成可调用对象
我们希望可以直接使用Test的成员函数compare成员函数。这个需求可以使用C++11中引入的function来解决:
function<bool(const Test&, const Test&)> tmp = &Test::compare;
通过这种方式,我们可以将类的成员函数转换为可调用对象
使用mem_fn生成可调用对象
使用function的方法,还是有点麻烦:虽然&Test::compare的签名已经决定了可调用对象的形式,程序员还是需要另外指定。解决这个问题的方法是使用mem_fn(注意不是mem_fun)来生成可调用对象,mem_fn会根据成员函数指针推断可调用函数的类型,就省去了另外指定的步骤。
struct Test{
Test(int i):a(i){}
bool compare (const Test& t)const{
return (a < t.a);
}
int a;
};
//bool compare(const Test& t1, const Test& t2){
// return t1.compare(t2);
//}
//std::function<bool(const Test&, const Test&)> tmp = &Test::compare;
int main(){
Test t1(1);
Test t2(2);
function<bool(const Test&, const Test&)> tmp = &Test::compare;
cout<<"error!!"<<mem_fn(&Test::compare)(t1,t2)<<endl;
return 0;
}
57:类类型的union成员