C++学习笔记
- Stack unwinding:栈展开。此概念常常在讨论C++异常处理时提及。当异常被抛出时,将寻找最近的try-catch块。如果找不到处理该异常的块,则当前栈帧被弹出,并到Caller函数中寻找。此操作会递归的进行,直到找到异常处理代码——整个过程就是所谓Unwinding。Unwinding过程中会执行栈上对象的自动析构,这种自动析构能力正是概念RAII(Resource Acquisition Is Initialization)的基础,可以帮助自动化内存、数据库连接或者其它资源的管理
- 在构造函数、析构函数体中,虚函数的多态性得不到体现,这点与Java不同
- 使用基类对象,而非基类的引用或指针时,虚函数的多态性得不到体现
- 函数调用时,实参的构造顺序是不被保证的,并不是像Java那样从第一个开始构造
- 内置类型、函数对象、STL迭代器往往适合pass-by-value
- 必须返回对象时,勿尝试返回其引用,例如:切勿尝试返回局部栈变量的引用
- 尽可能延迟变量定义式的出现时间,避免不必要的构造、析构开销
- 尽可能使用类的声明式代替定义式:例如:class Date; Date today(); void clear(Date);//后面两个表达式不需要知道定义式
- 如果使用&或者*可以完成任务,则不使用object本身
- 使用纯虚函数用来保证类必须被继承
- 不要定义继承来的非virtual函数
- 不要定义继承来的缺省参数值:虚函数是动态绑定,而其缺省参数却是静态绑定
- public继承:基类的public成员仍然为public,protected仍然为protected
protected继承,基类的public、protected成员均为protected
私有继承:基类的public、protected均为private - 合理的使用私有继承:私有继承意味着仅仅继承实现,接口部分被略去,在软件设计层面没有意义。尽可能使用复合代替
- 钻石型继承层次问题(通过大于1个路径到达同一个基类):使用虚拟继承(使得派生类如果继承基类多次,但只有一份基类的拷贝在派生类对象中),防止重复的继承变量:
1234class A;//虚拟基类最好不包含任何数据class B1 : public virtual A;class B2 : public virtual A;class C : public B1, public B2; - 如果函数抛出异常:C++保证,所有函数中的局部变量的destructor都被调用
- 全局变量的初始化:保证在main函数执行前,完成全局变量的初始化
- 静态变量的初始化:C语言中的全局、静态变量的内存空间都是全局的,初始化发生在任何代码执行之前。而C++规定,函数内的局部静态变量只有在第一次使用时才初始化
- 在编写类的定义(和声明分离)时,不要给函数添加static、virtual等修饰符
关键字 | 说明 | ||||||
const |
用于表示变量不可改变,或者成员函数不能修改对象的状态 |
||||||
explicit |
用于禁止构造函数参数的隐式类型转换,只能用于单参的构造函数 |
||||||
extern |
声明函数或全局变量为一个外部链接(即在别处定义),且要在本模块引用,可以跨文件作用域的共享函数、全局变量
|
||||||
extern "C" | 用于实现C/C++混合编程,具有双重含义:
|
||||||
mutable |
用于类的非静态和非常量数据成员, 如果一个类的成员函数声明为const类型,则表示其不会改变对象的状态,但是标注为mutable的字段除外(也就是允许被const方法修改) |
||||||
static |
静态全局变量作用范围在一个文件内,程序开始时分配空间,结束时释放空间,默认初始化为0 静态局部变量,在线程第一次访问时创建,析构时依创建的相反顺序进行 静态成员和静态成员函数,类的所有实例共用一个副本 |
||||||
volatile |
类型修饰符,用它声明的类型变量随时可能被某些编译器所不知晓的因素更改,比如:操作系统、硬件或者其它线程 遇到这个关键字声明的变量,编译器对访问该变量的代码不进行优化。对此类变量的读操作总是从它的内存地址进行 |
||||||
using |
可以导入名字空间到当前名字空间中:
可以导入名字空间中的某个成员到当前名字空间中:
C++11中,using可以定义别名,类似于typedef:
|
||||||
virtual |
用于定义虚函数成员,实现多态。注意:
|
||||||
throw | 用于抛出异常:
也用于声明函数可能抛出的异常(C++ 11开始deprecated):
|
||||||
thread_local | 从C++ 11开始可用,用于声明线程本地存储。示例:
|
包括:整数、浮点数、单个字符和布尔值的算术类型、一个void类型。
内置类型的机器表示:通常使用块chunk来使存储具有结构,块的bit数是2的幂,通常8位的块为字节;32位或4字节的为字(word)。
存储器中的每一个字节和一个称为地址的数关联起来。
关于有符号数(signed)和无符号数(unsigned):无符号数的所有位都表示数值,有符号数的第一位是符号位(0表示正数),8位无符号整数的范围为0 - 255;有符号整数的范围为 -127-127(某些实现允许-128 - 127)
整型 int、short 和 long 都默认为带符号型。要获得无符号型则必须指定该类型为 unsigned。unsigned int 类型可以简写为unsigned。
算术类型的存储空间(bit数)依机器而定,语言规定了最小存储空间:
类型 | 含义 | 最小存储空间 |
bool | boolean(可以将任何算数类型赋予bool,0假其余真) | NA |
char | character | 8bits |
wchar_t | wide character(用于扩展字符集,例如汉字) | 16bits |
short | short integer(一般为半个机器字长) | 16 bits |
int | integer (一般为1个机器字长) | 16 bits |
long |
long integer (一般为1-2个机器字长,在 32 位机器中 int 类型和 long 类型通常字长是相同)
|
32 bits |
float | single-precision floating-point (一般1个字) | 6 significant digits |
double | double-precision floating-point (一般2个字) | 10 significant digits |
long double | extended-precision floating-point (扩展精度,一般3-4个字) | 10 significant digits |
数据类型 | 举例 |
整数 |
L表示Long,U表示无符号;0开头表示8进制,0x开头表示16进制 20、20L、20UL、024、0x14
|
浮点数 |
3.14159F、 .001f、 12.345L、 0.、3.14159E0f、 1E-3F 、1.2345E1L 、0e0
|
布尔型 |
true false
|
字符型 |
L表示宽字符
'a' L'a'
|
字符串 |
为兼容C,C++的字符串字面值,由编译器在尾部添加\0
"string "、L"宽字符"
结尾反斜杠,可以把多行作为一个字符串处理:
"str\
ing" |
数据类型 | 说明 | ||
引用 |
不能定义引用类型的引用,声明时就必须初始化,一旦初始化就不能更改目标对象:
|
||
指针 |
指针可以为0,即不指向任何对象。指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,在32位的机器中,指针的数据长度为32bit。指针语法:
指针相关的两个运算符:取地址&、解引用* |
||
函数指针 |
指向函数的指针,可以直接作为函数调用:
|
||
typedef |
可以定义类型的同义词; 可以作为类型修饰符
|
||
枚举 |
默认枚举值从0开始,后续的值递增,可以指定某个元素的值(值可重复),但是递增的规律不变。枚举值必须是常量:
|
||
结构体 |
可以认为是仅仅有公共成员变量的类类型
|
||
联合体 |
主要达到共享内存的效果,sizeof与其sizeof最大的成员一致:
|
||
类 |
类定义一般会放入头文件。类定义示例:
|
||
函数对象 |
函数对象是类的一种,它是重载了()运算符的类
|
优先级 | 操作符 | 描述 | 示例 | 结合性 |
1 | () [] -> . :: ++ -- |
分组操作符 数组访问符 指针成员访问符 对象成员访问符 作用域操作符 后递增 后递减 |
(a + b) / 4; array[4] = 2; ptr->age = 34; obj.age = 34; Class::age = 2; for( i = 0; i < 10; i++ ) ... for( i = 10; i > 0; i-- ) ... |
左 |
2 | ! ~ ++ -- - + * & (type) sizeof |
逻辑非 按位否 前递增 前递减 一元负号 一元正号 解引用 取地址 强制类型转换 按字节返回占用内存长度 |
if( !done ) ... flags = ~flags; for( i = 0; i < 10; ++i ) ... for( i = 10; i > 0; --i ) ... int i = -1; int i = +1; data = *ptr; address = &obj; int i = (int) floatNum; int size = sizeof(floatNum); |
右 |
3 | ->* .* |
成员指针选择器 | ptr->*var = 24; obj.*var = 24; |
左 |
4 | * / % |
乘 除 模 |
int i = 2 * 4; float f = 10 / 3; int rem = 4 % 3; |
左 |
5 | + - |
加 减 |
int i = 2 + 3; int i = 5 - 1; |
左 |
6 | << >> |
按位左移 按位右移 |
int flags = 33 << 1; int flags = 33 >> 1; |
左 |
7 | < <= > >= |
小于 小于等于 大于 大于等于 |
if( i < 42 ) ... if( i <= 42 ) ... if( i > 42 ) ... if( i >= 42 ) ... |
左 |
8 | == != |
等于 不等于 |
if( i == 42 ) ... if( i != 42 ) ... |
左 |
9 | & | 按位与 | flags = flags & 42; | 左 |
10 | ^ | 按位异或 | flags = flags ^ 42; | 左 |
11 | | | Bi按位或 | flags = flags | 42; | 左 |
12 | && | 逻辑与 | if( conditionA && conditionB ) ... | 左 |
13 | || | 逻辑或 | if( conditionA || conditionB ) ... | 左 |
14 | ? : | 三元条件符 | int i = (a > b) ? a : b; | 右 |
15 | = += -= *= /= %= &= ^= |= <<= >>= |
赋值操作符 | int a = b; a += 3; b -= 4; a *= 5; a /= 2; a %= 3; flags &= new_flags; flags ^= new_flags; flags |= new_flags; flags <<= 2; flags >>= 2; |
右 |
16 | , | 序列估算操作符 | for( i = 0, j = 0; i < 10; i++, j++ ) ... | 左 |
为了让多个文件访问同一个的变量,C++对声明和定义进行了区分:
声明 | 定义 |
向程序表明变量的类型和名称 同一变量的声明可以发生多次 可以使用extern来多次声明,而不定义 |
向程序表明变量的类型和名称,并进行初始化 定义也同时是声明。只要提供初始化式,就认为是定义,即使标注extern关键字 |
- 左值(ell-value):左值可以出现在赋值语句的左边或右边。左值对应了内存中具有明确存储地址的对象
- 右值( are-value):右值只能出现在赋值的右边,不能出现在赋值语句的左边。不是左值,也就是没有明确存储地址的对象,均为右值
初始化不是赋值:初始化指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替。未初始化的变量,用于任何其他用途其行为都是未定义的
1 2 3 4 5 |
int ival(1024); // 直接初始化,效率更高 int ival = 1024; // 赋值初始化 //对于类类型的对象,直接初始化是唯一的方式,必须通过调用构造函数完成 std::string titleA = "C++ Primer, 4th Ed.";//先调用默认工作函数,再赋值 std::string titleB("C++ Primer, 4th Ed.");//调用构造函数 |
内置类型 | 类类型 | |
文件作用域 |
自动初始化为0 |
如果提供了默认构造函数,自动调用完成初始化 |
局部作用域 |
必须手工初始化,否则是未定义的 |
如果提供了默认构造函数,自动调用完成初始化 |
堆作用域 |
必须显示调用 |
必须显式调用构造函数创建 |
销毁方式 | |||
文件作用域 |
包括:全局对象、定义在namespace内的对象,class内、函数内、文件作用域内声明为static的对象 与程序生命周期一致,程序退出时析构。注意:全局对象的析构顺序和它们的构造顺序相反,但是全局对象的构造顺序不可预知,只能确保同一个文件中先定义的全局变量先构造 特别注意一下,函数的局部static变量:C++保证其在函数第一次调用时被初始化,可以用于解决跨编译单元的共享变量初始化不能保证完成的问题 |
||
局部作用域 |
局部作用域(包括块作用域)退出后,自动析构 |
||
堆作用域 |
必须手工删除
|
- 支持块级作用域,块用{}限定
- 函数内定义的局部变量,只能在函数内访问
- 函数外定义的变量,具有全局作用域,可以被整个程序访问(其它文件访问需要extern声明此变量)
使用限定符:const修饰,定义后就不能被修改,定义时必须初始化。默认为文件的局部变量:除非指定extern,否则不能被其他文件访问。允许在多个文件中进行const extern VAR的定义。
转型语法 | 说明 | ||
传统语法 |
C风格的转型(T)exp 函数风格的转型T(exp)
|
||
dynamic_cast(expr)
|
主要进行安全的向下转型,用于对象的指针和引用
对于指针:如果失败返回NULL;对于引用:如果失败抛出bad_cast异常
|
||
static_cast(expr)
|
进行任意的隐式转换(向上转换)和向下转型
|
||
const_cast(expr)
|
进行常量性移除或者设置 | ||
reinterpret_cast(expr)
|
重解释转型,可以转换一个指针为其它类型的指针,或者将指针与整数进行转换,不做任何相容性检查。示例:
|
所谓泛型编程,即以独立于任何特定类型的方式编写代码,这些类型(或者值)由模板的客户端在使用模板时提供。C++标准库中有大量模板的例子,每种容器,例如Vector,具有单一的定义,但是其使用者可以定义多种不同的Vector,其区别仅仅是容器包含的元素的类型不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
/* 函数模板(function template) */ // 模板定义以关键字template开始,后接模板形参表,使用尖括号包围 // 其中包含一个或者多个模板形参的列表,模板形参表不能为空 // 模板形参表类似于函数形参表,定义却不初始化,在函数运行时(模板实例化时)才初始化 // typename 与 class 可以替换使用,没有区别 template<typename T> int compare( const T &v1, const T &v2 ) { //编译器接口,要求实际类型T支持小于操作符 if ( v1 < v2 ) return -1; if ( v2 < v1 ) return 1; return 0; } // 模板定义支持内联(也就是在调用模板函数的地方展开函数体),注意inline的位置 template<typename T> inline T min( const T&, const T& ); // 函数模板的返回值被指定为特殊的形参,这样是没法自动推导的,必须每次调用时显式指定 template<class T1, class T2, class T3> T1 sum( T2, T3 ); sum<int,int,int>(1, 2); // OK sum<int>(1, 2); // OK sum(1, 2); // ERROR typedef double Type; // 该类型定义在下面的模板内部不生效,被模板形参Type覆盖 /* 类模板(class template) */ template<class Type> class Queue { public: Queue(); Type &front(); // 模板作为成员函数返回值 const Type &front() const; void push( const Type & ); // 模板作为成员函数参数 void pop(); bool empty() const; //类模板中亦可声明成员函数模板 template<class V> V returnV( V &v ); }; /*函数模板的使用*/ // 使用函数模板时,编译器会推断哪个模板实参绑定到模板形参,一旦推定完毕,就称“实例化”了函数模板的实例 // 推导出实际模板实参后,编译器使用实参代替相应的模板形参产生编译该版本的函数 int main( int argc, char **argv ) { // 下面创建了函数模板的两个实例 compare( 1L, 2L ); // T推断为long类型 compare( "1", "2" ); // T推断为string类型 //可以将函数模板赋值给函数指针,编译器将自动实例化模板为函数指针需要的类型 int (*pf1)( const int&, const int& ) = compare; //函数模板的显式实参 sum<int>( 1, 2 ); // 模板类在实例化时,必须为模板形参指定实参 Queue<int> qi; } // 模板还可以这样写 template<int i> void hello() { std::cout << i << endl; } int main() { hello<1>(); } |
参考下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Widget { public: Widget(); virtual ~ Widget(); virtual size_t size() const; }; //面向对象:显式接口与运行时多态 void doProcessing ( Widget& w ) { w.size(); } //面向模板:隐式接口与编译期多态 template <typename T> //声明一个模板类型参数 void doProcessing ( T& t ) { //模板参数需要支持的接口,由在参数上执行的操作决定: //模板内依赖于模板参数的名称,称为从属名称(dependent names) //例如下行的size就是一个从属名称: t.size(); //注意,默认情况下,从属名称被认为是一个数据成员的名字,如果期望将其看做类型 //应当显式的添加typename前缀。下面的例子防止const_iterator被解释为T的静态字段 typename T::const_iterator iter ( t .begin () ); //可以为作为类型的从属名称定义别名 typedef typename T ::const_iterator iter ( t .begin () ) ITER; } |
所谓模板特化(template specialization),是指定制模板的具体实例(即所有形参被确定为具体的值、类型)的定义。
模板特化分为函数模板特化、类模板特化两种,对于后者,特化的类模板可以修改基模板中的函数定义、添加新的函数,或者删除基模板中的函数。尽管特化的类模板可以修改基模板的接口规格,但是应当尽量保持一致,避免违反模板使用者的直觉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#include <string.h> /* 函数模板的特化 */ //template<>表示完全的模板特化 template<> //函数名后面附加尖括号,其中声明特化定义的模板形参 int compare<const char*>( const char* const &v1, const char* const &v2 ) { return strcmp( v1, v2 ); } //如果特化模板形参可以自动从函数形参推导,则不声明特化模板形参亦可 template<> int compare( const char* const &v1, const char* const &v2 ) { return strcmp( v1, v2 ); } //注意与下面的声明的区别:下面是声明一个普通的函数 int compare( const char* const &, const char* const & ); /* 类模板的特化 */ template<> class Queue<const char*> { public: void push( const char* ); void pop(); bool empty() const; // 特化可以定义与模板本身完全不同的成员,下面函数的定义与基模板不同 std::string front(); const std::string &front() const; }; //在外部定义特化类模板的成员时,前面不能添加template<>前缀 void Queue<const char*>::push( const char* val ) { } //只特化某个成员,而不特化整个类模板也是可以的 template<> void Queue<const char*>::push( const char * const &val ) { } |
偏特化,即部分特化,则是指多个形参的类模板,针对其中一部分特化。类的偏特化模板本身仍然是模板。
1 2 3 4 5 6 7 8 9 |
template<class T1, class T2> class some_template { }; //固化T2参数为int类型,允许T1参数变化 template<class T1> class some_template<T1, int> { }; some_template<int, string> foo; // 使用基模板版本 some_template<string, int> bar; // 使用偏特化模板版本 |
注意:只能偏特化整个类,而不能仅仅偏特化某个成员函数。
Traits是一种小对象,其用途是,携带其它对象或算法所需要的信息,这些信息决定了“策略”或实现细节。
在C++中Traint通常都实现为结构,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Traint类型 template <Address::SocketType T> struct NetworkSocketTrait {}; template <> struct NetworkSocketTrait<Address::SocketType::Stream> { static constexpr Address::SocketType type = Address::SocketType::Stream; }; template <> struct NetworkSocketTrait<Address::SocketType::Datagram> { static constexpr Address::SocketType type = Address::SocketType::Datagram; }; // 其它对象 template <typename T> class NetworkListenSocket : public ListenSocketImpl { public: NetworkListenSocket(const Address::InstanceConstSharedPtr& address) // Traint携带的type被使用,决定创建UDP还是TCP套接字 : ListenSocketImpl(address->socket(T::type), address) {} // 使用Traint using TcpListenSocket = NetworkListenSocket<NetworkSocketTrait<Address::SocketType::Stream>>; socket_ = std::make_unique<Network::TcpListenSocket>(address); |
Trait作为模板,它:
- 声明了统一的接口(包括类型、枚举、函数方法等、字段)
- 通过模板特化,针对不同数据类型或其他模板参数,为类、函数或者通用算法在因为使用的数据类型不同而导致处理逻辑不同时,提供了区分不同类型的具体细节,从而把这部分用Traits实现的功能与其它共同的功能区分开来
考虑一个容器模板类,当元素的move行为取决于元素类型时,可以将这个行为提取到Trait中实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
struct Dog { void move() { } }; template<typename M> struct MoveableTrait { void move(M m) {} }; template<> struct MoveableTrait<Dog> { void move(Dog d) { d.move(); } }; template<typename T> struct Container { // 此行为取决于元素类型,委托给元素类型T的trait处理 void move(T *p) { MoveableTrait<T>().move(p); }; }; |
运算符重载是具有特殊函数名的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 形式一:重载普通运算符 operator op // 形式二:自定义类型转换函数,转换为type类型 operator type // 示例:转换为bool operator bool() const throw(){ return (0 < pn.use_count()); } // 形式三:分配函数 operator new operator new [] // 形式四:删除函数 operator delete operator delete [] // 形式五:自定义字面值,C++ 11 operator "" |
下表中的@ 是表示所有匹配运算符的占位符:
- @a 中为所有前缀运算符
- a@ 为除外 -> 的所有后缀运算符
- a@b为除外 = 的所有其他运算符
表达式 | 重载为 | 示例 | |||
成员函数 | 非成员函数 | ||||
@a | (a).operator@ ( ) | operator@ (a) |
|
||
a@b | (a).operator@ (b) | operator@ (a, b) | |||
a=b | (a).operator= (b) | 不得作为非成员函数 | |||
a(b...) | (a).operator()(b...) | ||||
a[b] | (a).operator[](b) | ||||
a-> | (a).operator-> ( ) |
|
|||
a@ | (a).operator@ (0) | operator@ (a, 0) |
可以将运算符重载作为函数,显式的调用:
1 2 3 |
std::string str = "Hello, "; str.operator+=("world"); // 等价于 str += "world"; operator<<(operator<<(std::cout, str) , '\n'); // 等价于std::cout << str << '\n'; |
即raw字符串,支持多行、不需要转义:
1 2 3 4 5 6 |
const std::string BPF_PROGRAM = R"( int on_sys_clone(void *ctx) { bpf_trace_printk("Hello, World! Here I did a sys_clone call!\n"); return 0; } )"; |
你可以声明任意表达式甚至函数为编译期常量:
1 2 3 |
constexpr int N = 5; constexpr int five() { return 5; } int a[N]; |
此关键字在C++11中用于自动类型推导,推导在编译阶段完成。自动类型推导避免了复杂的类型声明,在模板编程中特别有用,因为在编写模板库时你不知道模板实参的类型。
用于从表达式中推导出类型:
1 2 |
int x = 1; decltype( x ) y = x; // 等价于 int y = x; |
decltype不会对表达式进行求值,仅仅会推导其结果的类型。推导规则如下:
- 如果表达式为变量、函数参数、类成员,则返回表达式的声明类型
- 否则,根据表达式的值类别决定推导结果,假设表达式的类型为T:
- 如果表达式为左值(lvalue,可寻址值),则推导为 T&
- 如果表达式为临终值(xvalue,对象生命周期已经结束,但是内存尚未回收),推导为 T&&
- 如果表达式为纯右值(pvalue)推导为 T
final用于禁止类被继承,或者禁止虚函数被子类覆盖:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class User final {}; class Admin:User{}; // error: cannot derive from ‘final’ base ‘User’ in derived type ‘Admin’ class User { protected: virtual void say(string what) final{} }; class Admin : User { protected: virtual void say(string what){} // error: overriding final function ‘virtual void User::say(std::string)’ }; |
override则用于显式的说明当前函数是覆盖了基类的虚函数:
1 2 3 4 |
class Admin : User { protected: virtual void say(string what) override {} }; |
用于显式的指定、禁止编译器自动生成的成员(例如构造器、析构器):
1 2 3 4 5 6 7 |
class User { protected: // 启用默认构造函数 User() = default; // 禁止默认的new运算符重载 void *operator new(size_t) = delete; }; |
标准化的,至少64位的整数。
静态断言,在编译期完成的断言,示例:
1 2 3 4 |
template< class T > struct Check { static_assert( sizeof(int) <= sizeof(T), "T is not big enough!" ) ; } ; |
相对的:
- assert宏在运行期起作用
- 预处理指令 #error在预处理期间生效
为了解决NULL的二义性问题引入:
1 2 3 4 5 6 |
void f( int i ) { cout << "int" << endl; } void f( User *user ) { cout << "User" << endl; } int main() { f( NULL ); // error: call of overloaded ‘f(NULL)’ is ambiguous f( nullptr ); } |
类似于Java的迭代器遍历语法:
1 2 3 4 |
map<string, int> m{{"a", 1}, {"b", 2}, {"c", 3}}; for (auto p : m){ cout<<p.first<<" : "<<p.second<<endl; } |
适用范围:数组、容器、string、任何迭代器(即具有begin/end函数的对象)。
现在你可以在构造函数的初始化列表中调用其它构造函数了:
1 2 3 4 5 6 |
class User { private : User( string name, int age, bool gender ) {} public: User(string name):User(name,0,0){} }; |
这种语法原先仅仅用于数组的初始化:
1 |
int arr[3]{1, 2, 3}; |
现在你可以:
1 2 3 4 5 6 7 8 9 10 11 12 |
// 初始化集合 vector<int> iv{1, 2, 3}; map<int, string>{{1, "a"}, {2, "b"}}; // 初始化字符串 string str{"Hello World"}; // 初始化自定义对象 struct User { User( string name, int age ){}; }; User alex{ "Alex", 30 }; // 任何时候你都可以适用 = 变体 User alex = { "Alex", 30 }; |
1 2 3 4 5 6 7 |
[函数对象参数](操作符重载函数参数)[mutable|exception声明] -> 返回值类型 { 函数体 } //举例: [ &a, =b, this ]( int c, &int d ) mutable -> void { }( 1, 2 ); |
标识一个Lambda的开始,这部分为必须
函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义Lambda为止时Lambda所在作用范围内可见的局部变量 (包括Lambda所在类的this)。
函数对象参数有以下形式:
- 空 没有使用任何函数对象参数
- = 函数体内可以使用Lambda所在作用范围内所有可见的局部变量 (包括Lambda所在类的this),并且是值传递方式
- & 函数体内可以使用Lambda所在作用范围内所有可见的局部变量 (包括Lambda所在类的this),并且是引用传递方式
- this 函数体内可以使用Lambda所在类中的成员变量
举例:
- a 将a按值进行传递。按值进行传递时,函数体内不能修改传递进來的a的拷贝,除非添加mutable修饰符
- &a 将a按引用进行传递
- a, &b 将a按值进行传递,b按引用进行传递
- =, &a,&b 除a和b按引用进行传递外,其他参数都按值进行传递
- &, a, b 除a和b按值进行传递外,其他参数都按引用进行传递
声明重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:a,b)和按引用(如:&a,&b)两种方式进行传递
按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)
exception声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw(int)
声明函数返回值的类型,当返回值为void,或若函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略
标识函数的实现,这部分不能省略,但函数体可以为空
右值引用实现了移动语义 (Move Sementics) 和精确转发(完美转发,Perfect Forwarding)。其意义在于:
- 消除两个对象交互时不必要的对象(特别是深度)拷贝,节省运算存储资源,提高效率
- 能够更简洁明确地定义泛型函数
C/C++中所有的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,那些可以在多条语句中使用的对象,左值可以出现在 = 操作符的左边,右值则不可以:
1 2 3 4 5 6 |
// i是左值,0是右值(临时对象) // i可以被引用,0则不可以 int i = 0; // 左侧的整体是左值,但是 0 不能独立出现在左侧 ((i>0) ? i : j) = 1; |
在C++ 11 之前右值仅仅能够被常量引用:
1 |
const int &a = 1; |
在函数声明中,使用 &&表示引用右值:
1 2 3 4 5 6 7 8 9 |
void process( int &i ) {} // 重载版本,如果传入右值 void process( int &&i ) {} int main() { int i = 0; process( i ); process( 1 ); } |
当传入临时对象1时,自动调用重载版本,临时对象不会被拷贝,而是被”移动“,减少了内存拷贝的开销。
此函数支持将左值引用转变为右值引用(赋予其临时对象语义):
1 2 3 |
#include <algorithm> int i = 0; process( std::move(i) ); |
使用此函数可以避免不必要的内存拷贝:
1 2 3 4 5 6 7 8 9 10 11 |
template <class T> swap(T& a, T& b){ T tmp(a); // copy a to tmp a = b; // copy b to a b = tmp; // copy tmp to b } // 下面的实现避免了三次内存拷贝 template <class T> swap(T& a, T& b){ T tmp(std::move(a)); // move a to tmp a = std::move(b); // move b to a b = std::move(tmp); // move tmp to b } |
std::move的关键之处在于,它定义了移动语义 —— 声明一个对象放弃其持有的资源(内存),无条件的转换为右值引用。上面基本类型的移动语义不明显,可以考虑下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class User { public: string name; User( string name ) : name( name ) {} }; int main() { User u1 = User( "Alex" ); cout << u1.name << endl; // Alex User u2 = std::move( u1 ); cout << u1.name << endl; // 空 } |
在把右值赋值给变量u2时,调用了User的隐含移动构造器,导致u1持有的内存资源(字段name)转移。
注意:在使用了转移语义之后,原对象不需要析构,因为它变成了一个空壳。
所谓移动构造器,就是指类型T的这样的构造器:
- 第一个参数类型为T&&, const T&&, volatile T&&, 或者 const volatile T&&
- 没有后续参数,或者后续参数均具有默认值
以下情况下移动构造器会被调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
T b; // 1 T a = std::move(b); // 2 T a(std::move(b)); // 3 void f(T t); f(std::move(b)); // 4 T f(){ // 返回语句调用移动构造器 return T(); } |
如果用户没有为class/struct/union定义移动构造器,并且:
- 没有自定义拷贝构造器
- 没有重载拷贝赋值操作符
- 没有重载移动赋值操作符
- 没有自定义析构器
则编译器会自动声明一个隐含的移动构造器,其签名为 T::T(T&&)。
即使不符合上述条件,你也可以强制要求编译器生成某个移动构造器的默认实现(其逻辑和隐含移动构造器相同),使用语法:
1 |
T ( T && ) = default; |
如果类型T符合下面任何一种情况:
- T的某个非静态成员不能被移动 —— 例如定义了不可见、删除的(deleted)、二义性的移动构造器
- T的直接或者virtual基类不能被移动
- T的直接或者virtual基类具有删除的(deleted)、不可见的析构器
则T的默认/隐含移动构造器实现是删除的(deleted):
1 |
T ( T && ) = delete; |
在某些场景下,我们需要将一组参数从一个函数原封不动的传递给另一个函数。所谓原封不动,不仅仅是指其值不变,还要求参数的左/右值、常量/非常量属性不变。
考虑下面的函数模板:
1 2 3 4 5 6 |
template <typename T> void forward_value(const T& val) { process_value(val); } template <typename T> void forward_value(T& val) { process_value(val); } |
为了能够转发常量/非常量版本的参数,在以前我们不得不定义重载版本,这种工作很无聊。现在,我们只需要使用右值引用:
1 2 3 |
template <typename T> void forward_value(T&& val) { process_value(val); } |
就可以了。不管实参是左值、右值,常量、非常量,都可以精确的转发给process_value。
std::forward可以用来将,模板函数参数转换为调用者指定的值类别(左/右):
1 2 3 4 5 6 7 8 9 10 |
// std::make_unique<Envoy::MainCommon>(argc, argv) // 模板变量声明 template<typename _Tp, typename... _Args> // 内联 // 返回值,用typename提示__single_object是结构模板_MakeUniq<_Tp>中定义的从属名称,而非静态成员 inline typename _MakeUniq<_Tp>::__single_object make_unique(_Args&&... __args) // 精确转发,上面的argc argv是什么类别,就转换为什么类别 ———— 左值作为左值传递,右值作为右值传递,给_Tp() { return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); } |
C++ 11之前,模板参数的数量是固定的,现在可以使用新语法来定义可变参数长度的模板:
1 2 3 4 5 6 7 8 9 |
// 可变参数模板 template<typename... Values> class tuple; // 上述类型的模板参数个数是任意的,0个也可以: tuple<int, std::vector<int>, std::map<<std::string>, std::vector<int>>> some_instance_name; tuple<> some_instance_name; // 要求至少有一个模板参数的可变参数模板: template<typename First, typename... Rest> class tuple; |
此标记有两个用途:
- 放在形参名字左侧,声明一个参数包(parameter pack)。使用这个参数包,可以绑定0个或多个模板实参给这个参数包
- 省略号出现在包含参数包的表达式的右侧,把此参数包解开为一组实参。省略号左侧的表达式整体使用解开后的每个实参分别求值,求值结果用逗号分隔
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <algorithm> #include <iostream> using namespace std; template<typename... Args> void print( Args... args ) { auto argList = { args... }; for ( auto i = argList.begin(); i != argList.end(); ++i ) { cout << *i << endl; } } int main() { print( 1, 2, 3 ); } |
C++ 11引入的函数对象标准库,包含了各种内建函数对象和相关的操作函数。
std::function、std::bind、std::result_of、std::thread::thread、std::call_once、std::async、std::packaged_task、std::reference_wrapper等都可以向函数那样被调用,即支持()操作符。
这是一个通用的函数包装器,可以容纳任何可调用类型 —— 例如函数、函数指针、Lambda、bind表达式、成员函数、其它函数对象。该模板类的声明如下:
1 |
template< class R, class... Args > class function<R(Args...)> |
用法示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void plus(int a, int b) { std::cout << a + b << std::endl; } int main() { // 函数 std::function<void(int, int)> plus_func = plus; plus_func(1, 2); // 3 //Lambda std::function<int(int, int)> plus_lmbda = [](int a, int b) -> int { return a + b; }; // 绑定,std::placeholders用于占位绑定后的函数的参数 // plus5()的第1参数对应plus的第2参数 auto plus5 = std::bind(&plus, 5, std::placeholders::_1); plus5(1); // 6 } |
为函数绑定一部分参数,产生一个新的偏函数(Partial Function)。 新函数的参数使用std::placeholders中的成员来占位:
1 2 3 4 5 6 |
void abcde(char a, char b, char c, char d, char e) { std::cout << a << b << c << d << e << std::endl; } int main() { auto bd = std::bind(abcde, 'a', std::placeholders::_2, 'c', std::placeholders::_1, 'e'); bd('b', 'd'); // adcbe } |
注意,std::placeholders::_1为偏函数的第一个参数占位,最多可以到std::placeholders::_29
C++11废弃了C++98中的auto_ptr,同时把shared_ptr、uniq_ptr引入到std名字空间。C++ 11包含的智能指针有:
指针 | 说明 | ||
unique_ptr | 确保资源仅仅能够被一个unique_ptr管理,如果unique_ptr生命周期结束,则资源自动销毁。可以利用右值引用来在不同指针之间转移对象:
|
||
shared_ptr | 多个shared_ptr可以共享一个资源,使用引用计数管理资源,当所有使用资源的shared_ptr生命周期结束,自动销毁资源 | ||
weak_ptr | 配合shared_ptr使用,weak_ptr不会影响引用计数 |
注意,传递智能指针时,总是传值。
相比其原来就有的可以定义二元组的std::pair,std::tuple可以定义任意元组:
1 |
std::tuple<int, double, string> t{ 1, 1.0, string( "" ) }; |
原先STL中的C++的map, multimap, set, multiset均基于红黑树实现,插入/查询的复杂度是O(lgn)。C++11提供了基于哈希的实现版本:std::unordered_set、std::unordered_multiset、std::unordered_map、std::unordered_multimap,它们的插入/查询的复杂度是O(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
private: static bool & get_lock(){ //返回引用类型 static bool lock = false; return lock; //对于静态变量,可以安全返回,不会引用函数的退出而销毁 } public: static void lock(){ get_lock() = true; //返回引用类型,可以直接作为左值使用 } static void unlock(){ get_lock() = false; } static bool is_locked() { return get_lock(); } }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
//list、vector的erase方法,返回下一个迭代器,故可以直接使用 using namespace boost::assign; vector v; v += 1, 2, 3, 4, 5; for ( vector< int>::iterator it = v .begin(); it != v.end(); ) { if ( *it > 2 && *it < 5 ) { it = v.erase( it++ ); } else { ++ it; } } //map、set的erase函数返回的 void,而在进行 erase 之后,当前迭代器会失效,无法再用于获取下一个迭代器: set s; s += 1, 2, 3, 4, 5; for ( set::iterator it = s.begin(); it != s.end(); ) { if ( * it > 2 && * it < 5 ) { s.erase( it++ ); } else { ++ it; } } //从反向迭代器获取正向迭代器,需要调用iter.base()方法。 using namespace boost::assign; vector v; v += 1, 2, 3, 4, 5; for ( vector::reverse_iterator it = v.rbegin(); it != v.rend(); ) { if ( * it > 2 && * it < 5 ) { it = vector< int>::reverse_iterator( v .erase( ( ++it ).base() ) ); } else { ++ it; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 文件输入流 std::ifstream pipe("/sys/kernel/debug/tracing/trace_pipe"); // 读取到字符串 std::string line; while (true) { if (std::getline(pipe, line)) { // 最后一行 std::cout << line << std::endl; break; } else { // 还可以读取 } } |
可能是由于使用Unicode引起的,把字符串直接量使用_T()包含即可。
可能原因:
- 声明但没有某个地方定义类的静态成员(static class member)
- 没有链接到适当的库
普通函数指针、类成员函数指针不是一类东西,不能相互转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <iostream> class Duck { public: // 类成员函数指针 typedef void (Duck::*VoidClassFuncPtr)(); void quack() { } }; // 自由函数指针 typedef void (*VoidFuncPtr)(); void callFp( VoidFuncPtr fp ) { } int main() { Duck duck; // OK Duck::VoidClassFuncPtr cfp = &Duck::quack; // 调用成员函数指针的语法,注意括号不能少 ( duck.*cfp )(); // 报错:类型不兼容 VoidFuncPtr fp = &Duck::quack; return 0; } |
类似的,带有捕获(Capture)的Lambda表达式也不是普通函数(自由函数),不能赋值给自由函数指针。
普通函数指针只能指向一个全局的、单独的函数,其关键是目标函数不会有关联的状态信息。没有关联状态的类静态函数,则可以安全的赋值给自由函数指针:
1 2 3 4 5 6 7 8 9 10 11 |
class Duck { public: static void quack() {} }; typedef void (*VoidFuncPtr)(); int main() { Duck duck; // OK VoidFuncPtr fp = &Duck::quack; return 0; } |
所谓的段错误 就是指访问的内存超出了系统所给这个程序的内存空间。段错误应该就是访问了不可访问的内存,这个内存区要么是不存在的,要么是受到系统保护的。 指针没有初始化可能导致该错误。
报错信息 | 原因分析 |
include/boost/mpl/eval_if.hpp:60:31: error: no type named 'type' in 'struct boost::mpl::eval_if_c <true, boost::range_const_iterator, boost::range_mutable_iterator >::f_' |
join函数的分隔符,使用char类型导致 |
include/boost/algorithm/string/detail/finder.hpp:578:29: error: '((const boost::algorithm::detail::token_finderF*)this)->boost::algorithm::detail::token_finderF::m_Pred' cannot be used as a function | 用法错误,注意:split( fileNameSeg, fileName, is_any_of("\\/" ) ); 其中任意一个字符用于分隔 |
Leave a Reply