笔记的大部分内容来自《C++ Primer 5th》英文版,如不另外说明,以:p.xxx 形式注明的是这本书中出处所在的页数。
1.lambda表达式
这是C++11中的新特性,又叫匿名函数(Anonymous function),通常用在行数特别少而且只使用一次的函数上,对于这种函数而言没有为它命名的必要,因此可以用lambda表达式代替,它的格式为:
[capture list] (parameter list) -> return type { function_body }
这种定义返回值的方式被称为尾置返回类型(trailing return type),其中返回值类型和前面的parameter list以及->
可以被省略,如:
void foo() {
auto f = [] {return 42;};
cout << f() << endl;
}
捕获
capture list以及函数体不可被省略。lambda表达式通常不会有默认参数,因此传入的参数数量要和定义中的一样。表达式内可以直接使用外部函数(surrounding function)中的static变量或函数外部的全局变量,对于函数内部的局部变量,我们可以对其进行捕获,方括号内规定的就是捕获变量的名称,以及规定是按by value
还是by reference
捕获,下面是维基中的定义:
lambda capture list | |
---|---|
[] | No variables defined. Attempting to use any external variables in the lambda is an error. |
[x, &y] | x is captured by value, y is captured by reference |
[&] | Any external variable is implicitly captured by reference if used |
[=] | Any external variable is implicitly captured by value if used |
[&, x] | x is explicitly captured by value. Other variables will be captured by reference |
[=, &z] | z is explicitly captured by reference. Other variables will be captured by value |
看一个简单的例子:
int t = 2;
auto test = [&t](int i) {
t++;
return t * i;
};
cout << "test of 5 is : " << test(5) << endl;
cout << "type of test is : " << typeid(test(5)).name() << endl;
cout << "t = " << t << endl;
此时test(5)
会输出15,类型为int
,而t
会递增为3,因为是引用捕获。这里如果把test
声明时的类型auto
改为int
会出现(no suitable conversion function from lambda …)的错误,原因是lambda表达式返回的是一个函数而非整型,观察第二个cout的结果会发现输出的不是任何一种C++内置的类型,也不是函数指针,而是一个新的类型,这个类型是在lambda被声明的时候创建的,每个lambda的类型都不一样。
按值捕获的变量是在创建lambda时被拷贝到lambda中,而不是当调用时,也就是说如果创建lambda后改变了被捕获的值,不会对之前lambda内捕获的结果造成影响。而按引用捕获的变量会随着外部对其的操作产生变化,这里就存在风险,如果当前lambda所在的函数已经执行完毕,而依然可以调用里面的lambda时,就会出现问题。就像函数返回引用时,不能返回一个local的引用或指针一样,因为一旦函数执行完毕那些内存就会被释放,返回的引用或指针就野了,当函数需要返回lambda时一定不能有引用捕获。为了安全,也应该尽量少的捕获引用。另外,通常来讲按值捕获的变量在lambda内是不能更改的,如果需要更改则需要加上mutable
关键词:
void func(){
size_t v1 = 42;
auto f = [v1] () mutable { return ++v1; };
v1 = 0;
auto j = f(); // j is 43
}
如果有时需要用到一些无法被拷贝的参数,比如ostream,就可以使用函数,然后用functional
库中的bind方法解决参数问题:
// define words
ostream &print(ostream &os, const string &s, char c){
return os << s << c;
}
for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));
注意由于ostream不能拷贝,因此要用库函数ref
来获得一个包含它引用的对象。
返回值
如果lambda只有return这一条语句,那么返回值类型在定义时就可以省略,编译器会隐式推断类型,但只要lambda中有return以外的语句,那么默认的返回类型就会是void,我们需要用尾置返回类型来定义:
transform(v.begin(), v.end(), v.begin(),
[] (int i) -> int {
if (i < 0)
return -i;
else
return i;
});
尾置返回类型
这是C++11的新特性,可以被用在任何函数中,但最常用的还是用于lambda表达式和较为复杂的函数返回类型,如指向数组的指针(或引用),返回类型用->
接在参数列表后面,然后用auto
代替它原本的位置:
int (*func(int i))[10];
auto func(int i) -> int(*)[10];
上面两个定义是等价的,但第二种写法很容易看出func是返回类型为指向大小为10的int型数组的指针。
应用
(1)在main执行前运行语句
看下面这段代码(来自知乎):
int a = []() {
std::cout << "a" << endl;
return 0;
}();
int main() {
cout << "b" << endl;
system("pause");
return 0;
}
控制台会先输出 ab ,也就是说在main
运行前 a 就已经被输出了,这是因为在运行main
之前,会先构造全局变量,如果此时将全局变量的赋值语句写成一个 lambda 表达式,就可以达到在main
运行前就运行其他语句的目的,最后变量a
会被赋值为0。
还有另一种方法,就是创建一个类的全局对象变量,然后在该类的构造函数中放进要执行的语句,也可以达到相同的目的:
class TestClass{
public:
TestClass() {
cout << "TestClass" << endl;
}
};
TestClass Ts; // 定义个全局变量,让类里面的代码在main之前执行
int main(){
cout<<"main"<<endl;
return 0;
}
虽然我不知道这种操作有什么卵用,但是面试可能会考。
(2)使用STL时简化代码
对于sort来说直接定义一个函数与使用lambda表达式的区别不大,但如果我们需要使用find_if找到容器中与外部函数变量有关的值时,lambda表达式就可以帮助我们捕获所需的变量,这不好用单独的函数实现,因为find_if不接受参数多于一个的函数(or callable object),下面看代码:
vector<int> v {4, 1, 3, 5, 2, 3, 1, 7};
for_each(v.begin(), v.end(), [](int i) {
std::cout << i << " ";
});
// 输出第一个大于4的元素
vector<int>:: iterator p = find_if(v.begin(), v.end(), [](int i) {
return i > 4;
});
cout << "First number greater than 4 is : " << *p << endl;
// 按递增顺序排序数组
sort(v.begin(), v.end(), [](const int& a, const int& b) {
return a > b;
});
// 计算容器中大于等于5元素的个数
int count_5 = count_if(v.begin(), v.end(), [](int a) {
return (a >= 5);
});
cout << "The number of elements greater than or equal to 5 is : "
<< count_5 << endl;
// 去重,注意unique去重之后并不会对容器的大小进行调整,而是将目前最后的一个元素用迭代器的方式返回
p = unique(v.begin(), v.end(), [](int a, int b) {
return a == b;
});
// 将容器的大小重新调整
v.resize(distance(v.begin(), p));
如果将上面每一次操作后的v
输出会得到:
4 1 3 5 2 3 1 7
First number greater than 4 is : 5
7 5 4 3 3 2 1 1
The number of elements greater than or equal to 5 is : 2
7 5 4 3 2 1
2.函数中的指针和引用
传入指针参数和引用
这个问题看起来很简单但十分容易出错,当函数的参数传入指针时,其实是值传递,但由于指针本身储存的是地址,那么函数体中赋值给形参的值也是传入变量的地址,这里需要注意的是指针本身也有地址具体看下面的代码:
void foo1(int* p) {
cout << "in foo1: p = " << p << ", " << "*p = " << *p << endl;
int b = 4;
p = &b;
cout << "in foo1: p = " << p << ", " << "*p = " << *p << endl;
}
void foo2(int* p) {
cout << "in foo2: p = " << p << ", " << "*p = " << *p << endl;
int b = 4;
*p = b;
cout << "in foo2: p = " << p << ", " << "*p = " << *p << endl;
}
int main() {
int* p;
int a = 3;
p = &a;
cout << "in main: p = " << p << ", " << "*p = " << *p << endl;
foo1(p);
cout << "after foo1: p = " << p << ", " << "*p = " << *p << endl;
cout << endl;
foo2(p);
cout << "after foo2: p = " << p << ", " << "*p = " << *p << endl;
system("pause");
return 0;
}
输出:
这里main
中的指针p
和两个foo
中的参数p
确实指向同一块内存,但它们各自拥有自己的内存,如果在foo
中对p
直接赋值,相当于把一个不同的指针指向了另一块内存区域,原有的传入的指针p
不会有任何改变。若要达到改变的目的,需要改变的是p
指向地址中储存的信息,因此用解引用符号赋值即可。
如果传入的是指针的指针,就可以直接改变main
中p
所指向的内存区域,而不是修改同一块内存保存的值。
void goo(int** pp) {
cout << "in goo: pp = " << pp << ", *pp = " << *pp << ", **pp = " << **pp << endl;
*pp = new int(4);
cout << "in goo: pp = " << pp << ", *pp = " << *pp << ", **pp = " << **pp << endl;
}
int main() {
int* p;
int a = 3;
p = &a;
cout << "in main: p = " << p << ", *p = " << *p << endl;
int** pp = &p;
goo(pp);
cout << "after goo2: p = " << p << ", *p = " << *p << endl;
system("pause");
return 0;
}
在不需要改变传入参数的情况下,尽量用const &去声明,这样可以省去拷贝的麻烦,尤其是对于字符串来说,而且有些类型可能不允许拷贝(比如IO类型)。要注意的是指针的const还分高层const(top-level)和低层(low-level)const,简单来说高层const就是指针自身是const,不可变更指向的对象,而低层const就是指向的对象为const,在拷贝的时候高层const是可以被忽略的,也就是说const指针可以被赋值到一个非const指针上,而const对象不可以被赋值到非const对象上(反过来可以),因此如果不把函数的参数定义成const,那么const实参传进来就会报错。
传入数组
前面提到过数组不能拷贝,也不能赋值,所以将数组传入函数时实际上传入的是指向数组第一个元素的指针。尽管本质上传入的并不是数组,但写法上我们可以这么写:
void print(const int*);
void print(const int[]);
void print(const int[10]); // dimension for documentation purposes
上面的三种声明是等价的,第三个声明中数组的大小只是方便阅读,编译器得到的参数都是const int*
,在调用时也可以直接传入一个int型的指针,不会出现编译错误。
也不能有元素是引用的数组,但是可以是数组的引用:
f(int &arr[10]); // error: arr is an array of refs
f(int (&arr)[10]); // ok: arr is a ref to an array of ten ints
要注意的是这里数组的大小是参数类型的一部分,声明后我们能且仅能传入大小为10的int型数组。
数组的多维数组如二维数组,本质上是数组的数组,也就是元素是指向数组第一个元素指针的数组,因此若要传入多维数组,则需声明参数为指向一个数组的指针:
void print(int (*matrix)[10], int rowSize);
void print2(int matrix[][10], int rowSize);
类似的,第二维的大小是类型的一部分。
返回指针和引用
除非为静态局部变量,否则永远也不要返回一个局部变量的引用或指针,因为局部变量会在函数结束时被销毁。
函数返回的引用是左值(lvalue),因此可以在调用时就对其赋值。
函数指针
顾名思义就是指向函数的指针,一个函数的类型由它的返回值和它的参数决定,与函数名无关:
bool (*pf1)(const string &, const string &);
bool *pf2(const string &, const string &);
注意上面两个声明是不一样的,pf1是指向参数为两个const string&,返回类型为bool的函数指针,而如果没有括号,pf2则是一个函数,返回值类型是指向bool的指针,这个同样可以用前面讲到过的左右法则阅读表达式来判断。
再举个例子:
int (*f1(int))(int*, int);
f1是一个函数,参数为int,返回类型为指针,指针指向一个参数为int*和int,返回类型为int的函数。用前面尾置返回写法可以改写为:
auto f1(int) -> int(*)(int*, int);
- 使用函数指针来调用函数时直接调用就可以,不需要使用
*
从操作符来解引用,但是加上也没错,同样需要括号 - 给函数指针赋值时可以省略
&
操作符,直接使用函数名 - 函数指针类型之间不可互相转换
- 与普通指针一样,给函数指针赋值常量0或
nullptr
表示该指针不指向任何函数 - 函数定义中可以使用函数指针作为形参,调用时可以直接将对应的函数名作为实参传入
3.智能指针
程序中的静态内存储存本地static对象、类的static数据成员和函数外定义的变量,栈内存(stack memory)储存函数内定义的nonstatic对象,这两部分内存由编译器管理分配和释放,占内存中的对象只有在它们被定义的块(block)执行时才会存在,而静态对象在它们被使用前就被分配了空间,并且直到程序结束时才会被释放。程序中还有一块堆内存(heap),用来动态的分配空间,然而这存在风险,可能造成内存泄漏或野指针。通常来说有三个需要使用动态内存的情况(p.454):
- 不确定所需对象的数量时
- 不确定所需对象的类型时
- 需要在多个对象中共享数据时
智能指针就是为了让动态内存的使用更加安全而被加入的新特性,我们像使用正常指针一样使用它们,不同的是他们会自动释放不需要的空间。在memory头文件中一共定义了三种智能指针,shared_ptr、unique_ptr以及weak_ptr。
shared_ptr
shared_ptr允许多个指针指向同一对象,声明时类似vector,在<>中声明类型:
shared_ptr<list<int>> p;
p指向类型为int的list的指针。更安全的方法是使用make_shared函数,它会在动态内存中为对象分配空间,并返回一个shared_ptr指向该对象:
shared_ptr<int> p = make_shared<int>(42);
当我们对shared_ptr做拷贝或赋值操作时,每个shared_ptr都会追踪其他同样指向该对象shared_ptr的数量,这实际上是一种引用计数(reference count),当用其赋值(作为右值)、初始化其他指针、以值传递进入函数或按值从函数返回(经过拷贝)时计数器会增加,而当其被赋值为其他指针或自身被销毁时计数器会减少,当计数器为零时便会自动释放其指向对象的空间。这样的好处是我们不用主动的去释放空间,但只要还有shared_ptr还指向那个对象,该内存就不会被释放,因此为了不造成空间浪费我们要记得去销毁那些已经不被需要的shared_ptr。
p.use_count()
返回目前有多少个shared_ptr共享该内存,这个方法效率比较低,通常debug时才用。
由于普通指针不能隐式转换成智能指针,因此若要使用new关键字将指向动态分配空间的指针初始化智能指针,必须使用直接初始化(direct initialization):
shared_ptr<int> p1 = new int(42); // error
shared_ptr<int> p2(new int(42)); // ok
这个规则同样适用于返回类型为智能指针的函数。当我们用普通指针和智能指针指向同一块内存时,应尽量使用智能指针去访问,因为这段内存现在有智能指针管理了,普通指针无法得知它什么时候被释放。同样的道理,不能将同一块内存分配给两个独立创建的智能指针(independently created),比如使用get
关键词去初始化另一个shared_ptr,这两个相互独立的智能指针并不知道对方的存在,都会自己去释放空间,这会导致野指针或重复释放。
智能指针的另一个好处是,即便发生异常导致程序终止,也不会影响内存的释放,因为不论何种原因导致程序停止(比如exception),局部变量都会被销毁,这不会影响智能指针的作用,而程序终止时并不会帮我们释放动态内存,若是exception发生在new与delete之间,那段内存就无法被释放了。
智能指针同样可以按我们想要的方式释放内存而不是单纯的执行delete,只需要在声明时在第二个参数传入一个函数,在引用计数归零时就会帮我们调用它:
shared_ptr<T> p(p2 ,d); // d is a callable object in place of delete
还有一点,初始化智能指针时务必记得用new返回的动态空间的指针,不然释放内存时可能会出错。
unique_ptr
unique_ptr拥有(owns)它所指向的对象,也就是说只能有一个unique_ptr指向同一个对象,它会随着unique_ptr的销毁而销毁,我们同样通过直接初始化的方式:
unique_ptr<int> p(new int(42));
unique_ptr<int> p1(p); // error
unique_ptr<int> p2;
p2 = p1; // error
尽管不能拷贝,但是unique_ptr指向对象的所有权可以发生转变:
unique_ptr<string> p1(new string("42"));
unique_ptr<string> p2(p1.release()); // release makes p1 null
unique_ptr<string> p3(new string("Text"));
p2.reset(p3.release()); // transfer ownership from p3 to p2
p2.release(); // wrong: memory leak
注意release方法可以帮助我们转换对对象的使用权,但并不能帮我们释放空间。有一种特殊情况允许拷贝unique_ptr,就是作为函数的返回值返回时,因为此时编译器知道本地变量在返回后就会被销毁。shared_ptr是没有这个方法的,因为可以直接使用赋值操作,不存在转移控制权的情况。
weak_ptr
weak_ptr指向shared_ptr管理的对象,但是不会影响引用计数,内存依旧在没有shared_ptr存在时被释放,由于对象可能已经被销毁,使用weak_ptr时通常要做一下判断:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);
// ...
if (shared_ptr<int> np = wp.lock()){ // true if wp's object exists
// np shares its object with p
}
如果指向的对象还存在,lock方法会返回一个shared_ptr,这样就确保我们使用时对象不会被销毁。
expired
方法可以检查目前是否还有shared_ptr在管理对象,如果有则返回true,反之返回false。
4.右值引用(rvalue reference)
C++11的新特性,首先复习一下左值右值的定义,左值指的是既能出现在等号左边也能出现在等号右边的值(或表达式),右值指的是只能出现在等号右边的值(或表达式),这是C中的定义,C++对此有更复杂的定义(p.135):左值表达式产出(yield)一个对象或函数。但有些左值(const对象)并不能放在赋值操作左边,有些表达式产出对象但以右值返回,当我们把一个对象当成右值时,实际上是在使用它的值(contents),而把对象用作左值时使用的是它的身份(identity),即内存所在的位置。需要时可以用左值来代替右值,此时使用的就是对象中的值,但不能反过来。举例来说,&
取地址时的操作数必须是一个左值,而&
返回的是一个指向操作数的指针,它是右值。要注意的是把一个表达式传入decltype
时,如果表达式产出一个左值,那么其结果为引用类型(直接传入变量则不是,且不能有括号,否则会被解释为表达式),如若p的类型为int*
,那么decltype(*p)
的类型为int&
,对应的,由于&
操作符产生一个右值,decltype(&p)
的类型为int**
。
右值引用是一种只能和右值绑定的引用,使用&&
操作符得到,和左值引用一样,右值引用同样是对象的别名,右值只能和const型的左值引用或右值引用绑定:
int i = 42;
int &r = i;
int &&rr = i; // error: i is lvalue
int &r2 = i * 42; // error: i * 42 is an rvalue
const int &r3 = i * 42; // ok
int &&rr2 = i * 42 // ok
要注意的是一个右值引用变量是一个左值,因此它也不能和右值引用绑定。左值与右值还有一个区别就是左值是会持续存在的(如变量),而右值通常只是临时的对象或文字(literals),马上就会被销毁。尽管右值引用不能直接与左值绑定,但我们可以对左值进行显示转换,utility头文件中定义的move
函数可以得到一个与该左值绑定的右值引用。
int &&rr1 = 42; // ok
int &&rr2 = rr3; // error: rr3 is lvalue
int &&rr3 = std::move(rr1); // ok
就像前面提到的,这相当于告知编译器我们不再需要变量rr1
了,因此调用完之后rr1的值可能会变得未知。