CPP 11部分实用特性总结
1. 字符串原始字面量
使用R "xxx(…)xxx"输出原始字符串
cout << R"(/t)";
cout << R"(/t
232
//
/n)"
2. 指针空值类型 nullptr
在C中指针的初始化通常赋值NULL 在C++中我们赋值nullptr 那么他们有什么区别?
NULL 是一个宏常量
#define NULL 0;
#define NULL (void*)0;
nullptr是一个std::nullptr_t类型的的空指针常量
因此使用nullptr可以防止出现强制类型转换导致的问题
null和nullptr都可以用来表示空指针,但nullptr更加安全和规范。在C++11及以后的版本中,建议使用nullptr来表示空指针。
3. constexpr 修饰常量表达式
在C中我们常用const 赋值常量 在C++中我们使用constexpr 那么他们有什么区别?
不容置疑的是无论是const还是constexpr他们都是表示常量的类型。
-
主要区别在于const声明的常量,编译器无法判断,无法在编译阶段就确定。例如 const int a = 10;
-
而constexpr定义的常量 可以在编译阶段就确定 这可以提高程序运行效率。例如 constexpr int a = 10;
另外constexper也可以作为函数返回类型。例如
constexpr int returnconst(int x) { return x* x; }
注意 在constexpr返回值的函数中,不得出现逻辑运算,这是因为该函数会在编译阶段运行,所以只能执行简单语句 例如 数学运算,赋值等
4. 使用auto的自动类型推导
使用auto 自动类型推导可以帮助我们节省开发时间,提高开发效率
auto语法简单实用,例如
int x[10];
for (auto x_ : x) cout << x << ’ ';
但值得一提的是要注意auto的使用场景
5. decltype的使用
一些场景中我们需要使用decltype帮助我们推导变量数据类型
decltype的简单使用
int a = 10;
decltype(a) x = 114514;
int func (int x) {…}
decltype(func(x)) x_ = 114514;
如果函数返回一个纯粹的右值
例如 const int func() {…}
decltype(func())的推导即为int (因为此时const int返回了一个纯粹的数值 会被推导为int)
- decltype在范型编程中的应用
#include <list>
using namespace std;
template <class T>
class Container
{
public:
void func(T& c)
{
for (m_it = c.begin(); m_it != c.end(); ++m_it)
{
cout << *m_it << " ";
}
cout << endl;
}
private:
??? m_it; // 这里不能确定迭代器类型
};
int main()
{
list<int> lst;
Container<const list<int>> obj;
obj.func(lst);
return 0;
}
在private中的迭代器因为传入类的不同迭代器会分为两种 T::const_iterator 与 T::iterator,在decltype下我们就可以解决问题,通过decltype类型推导我们就可以得到想要的类型
当然我们可以基于auto改写for循环并且取消私有迭代器,类中的代码就会变成这样
template <class T>
class Container
{
public:
void func(T& c)
{
for (auto m_it = c.begin(); m_it != c.end(); ++m_it)
{
cout << *m_it << ' ';
}
cout << endl;
}
}
6.返回值类型后置
返回值类型后置也是decltype的一种用法
在范型编程中我们经常遇到我们不明确知道模板函数返回值类型的情况
例如:
template <typename R, typename T, typename U>
R add(T t, U u)
{
return t + u;
}
此时R的类型应当是 t + u 并且类型转换的类型,为使得函数模板支持所有类型我们必须要使得返回类型是动态的,使用decltype可以做到这一点
template <typename T, typename U>
decltype(t+u) add(T t, U u)
{
return t + u;
}
我们发现直接使用这种写法,是无法通过编译的,因为编译器并不知道我们后面会有t,u两个参数,所以使用类型后置语法改写
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
return t + u;
}
通过上述语法分析我们可以得到结论:auto会自动追踪decltype()推导出的类型。
7. using的使用
对于using我们常用命名空间std,可以说是很熟悉了,但是在cpp11中using有什么样的新特性?
-
使用using定义别名
这种用法类似typedef,代码形如 using xxx = 类型名称
可以定义类型的别名,被重新定义的类型是原有类型的别名,不是创造新类型
使用using进行别名的定义更加具有可读性,例如
typedef int(*function)(int, string) //函数指针
using function = int(*)(int, string)
-
为模板定义别名
使用typedef重定义类似很方便,但是它有一点限制,比如无法重定义一个模板,对于using是很简单的
tamplate <typename T>
using mytamplate = map<int, T>
在之后可以使用mytamplate 这样的形式为T指定类型
8. 列表初始化
关于C++中的变量中有不同的初始化方法,但没有一种方法能够适用于所有情况。为了统一初始化方式,并且让初始化行为具有确定的效果,在C++11中提出了列表初始化的概念
- 直接在变量名后面跟上初始化列表,进行对象、变量的初始化
double a = {1.123};
int* p = new int{1};
int* array = new int{1, 2, 3};
- 初始化列表还可以用在函数返回值中
class person
{
public:
person(int id, string name)
{
cout << "id: " << id << ' ' << name < '\n';
}
};
person func()
{
return {1, "name"};
}
- 聚合体的初始化列表
因为列表初始化的使用范围大大增强,一些模糊的概念也随之而来,在前面的例子可以得知对于一个自定义类型的初始化可能有两种执行结果
#include <iostream>
#include <string>
using namespace std;
struct T1
{
int x;
int y;
}a = { 123, 321 };
struct T2
{
int x;
int y;
T2(int, int) : x(10), y(20) {}
}b = { 123, 321 };
int main(void)
{
cout << "a.x: " << a.x << ", a.y: " << a.y << endl;
cout << "b.x: " << b.x << ", b.y: " << b.y << endl;
return 0;
}
在上边的程序中都是用列表初始化的方式对对象进行了初始化,但是得到结果却不同,对象b并没有被初始化列表中的数据初始化。
对象1是一个自定义的聚合类型进行初始化,它将以拷贝的形式使用初始化列表的数据来初始化结构体中的成员。
对象2自定义了一个构造函数,实际的初始化是通过这个构造函数完成的
如果使用列表初始化时,还需要判断初始化的对象是否是聚合体,如果是初始化列表的数据就会拷贝到对象中
什么类型会被认为是聚合体呢?
- 普通数组
int x[] = {1,2,3,4,5,6};
double y[3][3] = {
{1.23, 2.34, 3.45},
{4.56, 5.67, 6.78},
{7.89, 8.91, 9.99},
};
char carry[] = {'a', 'b', 'c', 'd', 'e', 'f'};
std::string sarry[] = {"hello", "world", "nihao", "shijie"};
- 无用户自定义构造函数、无基类、无虚函数、无private、protected的非静态数据成员的(class、struct、union)可以被看做一个聚合类型(初始化列表只能初始化非静态成员变量)
struct T1
{
int x;
long y;
protected:
int z;
}t{ 1, 100, 2}; // error
struct T2
{
int x;
long y;
protected:
static int z;
}t{ 1, 100, 2}; // error
struct T2
{
int x;
long y;
protected:
static int z;
}t{ 1, 100}; // ok
// 静态成员的初始化
int T2::z = 2;
//结构体中的静态变量 z 不能使用列表初始化进行初始化,它的初始化遵循静态成员的初始化方式。
- 非聚合体的变量的初始化方法——使用类中的构造函数
- 聚合类型的定义是非递归 ,也就是说一个类的非静态成员是非聚合类型时,这个类也可能是聚合类型
#include <iostream>
#include <string>
using namespace std;
struct T1
{
int x;
double y;
private:
int z;
};
struct T2
{
T1 t1;
long x1;
double y1;
};
int main(void)
{
T2 t2{ {}, 520, 13.14 };
return 0;
}
在示例中,T1并非聚合类型因为它有一个private z,但是T2仍然可以使用初始化列表的方式进行初始化。
T2对象的初始化过程,对于非聚合类型的成员T1做初始化时,可以直接写成=={ }==,这就相当于调用T1的无参构造函数
- std::initializer_list
STL容器中,我们可以实现任意长度数据的初始化,使用初始化列表也只能固定参数的初始化,如果想要有任意长度参数初始化的能力,可以使用std::initializer_list 这样的轻量级类模板实现
std::initializer_list 模板的一些特点
- 它是一个轻量级的容器类型,内部定义了iterator等必须的概念,遍历时得到的iterator是只读的
- std::initializer_list而言,它可以接收任意长度的初始化列表,但是要求元素必须是同类型的T
- 在std::initializer_list内部有三个成员接口:size() 、 begin() 、 end().
- std::initializer_list 对象只能被整体初始化或者赋值
如果想要自定义一个函数接受任意个数的同类型参数,只需要把函数的参数指定为std::initializer_list,使用初始化列表=={ }==作为实参传递即可
#include <iostream>
#include <string>
using namespace std;
void traversal(std::initializer_list<int> a)
{
for (auto it = a.begin(); it != a.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
}
int main(void)
{
initializer_list<int> list;
cout << "current list size: " << list.size() << endl;
traversal(list);
list = { 1,2,3,4,5,6,7,8,9,0 };
cout << "current list size: " << list.size() << endl;
traversal(list);
cout << endl;
list = { 1,3,5,7,9 };
cout << "current list size: " << list.size() << endl;
traversal(list);
cout << endl;
traversal({ 2, 4, 6, 8, 0 });
cout << endl;
traversal({ 11,12,13,14,15,16 });
cout << endl;
return 0;
}
std::initializer_list拥有一个无参构造函数,因此,它可以直接定义实例,此时将得到一个空的std::initializer_list,因为遍历这种类型的容器时只得到一个只读迭代器,我们不能修改里面的数据,只能通过值覆盖进行数据的修改,std::initializer_list的效率是非常高的,它的内部并不保存初始化列表中元素的拷贝,仅仅储存了初始化列表中元素的引用。
自定义的类与上述同理,我们在构造函数参数指定为std::initializer_list类型,在自定义类的内部使用容器来接受多个实参
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Test
{
public:
Test(std::initializer_list<string> list)
{
for (auto it = list.begin(); it != list.end(); ++it)
{
cout << *it << " ";
m_names.push_back(*it);
}
cout << endl;
}
private:
vector<string> m_names;
};
int main(void)
{
Test t({ "jack", "lucy", "tom" });
Test t1({ "hello", "world", "nihao", "shijie" });
return 0;
}
9. 可调用对象
可调用对象是C++中的一种概念
可调用对象一般有如下定义
- 是一个函数指针
void print(string a) {
cout << a << endl;
}
using funcptr = void(*)(string);
- 一个仿函数
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct Test
{
void operate() (string msg)
{
cout << "msg: " << msg << endl;
}
};
int main(void)
{
Test t;
t("Test func");
return 0;
}
- 一个可重载为函数指针的类对象
#include <iostream>
#include <string>
#include <vector>
using namespcae std;
using funcptr = void(*)(string);
struct Test
{
static void print(string a) {
cout << a << endl;
}
operator funcptr()
{
return print;
}
};
int main(void)
{
Test t;
t("Test func");
return 0;
}
- 一个类成员函数指针或者类成员指针
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct Test
{
void print(int a, string b)
{
cout << "name: " << b << ", age: " << a << endl;
}
int m_num;
};
int main(void)
{
// 定义类成员函数指针指向类成员函数
void (Test::*func_ptr)(int, string) = &Test::print;
// 类成员指针指向类成员变量
int Test::*obj_ptr = &Test::m_num;
Test t;
// 通过类成员函数指针调用类成员函数
(t.*func_ptr)(19, "Monkey D. Luffy");
// 通过类成员指针初始化类成员变量
t.*obj_ptr = 1;
cout << "number is: " << t.m_num << endl;
return 0;
}
在上面的例子中满足条件的这些可调用对象对应的类型被统称为可调用类型。C++中的可调用类型虽然具有比较统一的操作形式,但定义方式五花八门,这样在我们试图使用统一的方式保存,或者传递一个可调用对象时会十分繁琐。现在,C++11通过提供std::function 和 std::bind统一了可调用对象的各种操作
可调用对象包装器
std::function是可调用对象的包装器。它是一个类模板,可以容纳除了类成员(函数)指针之外的所有可调用对象。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们
下面的实例代码中演示了可调用对象包装器的基本使用方法:
#include <iostream>
#include <functional>
using namespace std;
int add(int a, int b)
{
cout << a << " + " << b << " = " << a + b << endl;
return a + b;
}
class T1
{
public:
static int sub(int a, int b)
{
cout << a << " - " << b << " = " << a - b << endl;
return a - b;
}
};
class T2
{
public:
int operator()(int a, int b)
{
cout << a << " * " << b << " = " << a * b << endl;
return a * b;
}
};
int main(void)
{
// 绑定一个普通函数
function<int(int, int)> f1 = add;
// 绑定以静态类成员函数
function<int(int, int)> f2 = T1::sub;
// 绑定一个仿函数
T2 t;
function<int(int, int)> f3 = t;
// 函数调用
f1(9, 3);
f2(9, 3);
f3(9, 3);
return 0;
}
通过测试代码可以得到结论:std::function可以将可调用对象进行包装,得到一个统一的格式,包装完成得到的对象相当于一个函数指针,和函数指针的使用方式相同,通过包装器对象就可以完成对包装的函数的调用了。
绑定器
std::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。通俗来讲,它主要有两大作用:
- 将可调用对象与其参数一起绑定成一个仿函数
- 将多元(参数个数为n,n>1)可调用对象转换为一元或者(n-1)元可调用对象,即只绑定部分参数
绑定器使用示例:
#include <iostream>
#include <functional>
using namespace std;
void callFunc(int x, const function<void(int)>& f)
{
if (x % 2 == 0)
{
f(x);
}
}
void output(int x)
{
cout << x << " ";
}
void output_add(int x)
{
cout << x + 10 << " ";
}
int main(void)
{
// 使用绑定器绑定可调用对象和参数
auto f1 = bind(output, placeholders::_1);
for (int i = 0; i < 10; ++i)
{
callFunc(i, f1);
}
cout << endl;
auto f2 = bind(output_add, placeholders::_1);
for (int i = 0; i < 10; ++i)
{
callFunc(i, f2);
}
cout << endl;
return 0;
}
在上面的程序中,使用了std::bind绑定器,在函数外部通过绑定不同的函数,控制了最后执行的结果。std::bind绑定器返回的是一个仿函数类型,得到的返回值可以直接赋值给一个std::function,在使用的时候我们并不需要关心绑定器的返回值类型,使用auto进行自动类型推导就可以了。
placeholders::_1是一个占位符,代表这个位置将在函数调用时被传入的第一个参数所替代。同样还有其他的占位符placeholders::_2、placeholders::_3、placeholders::_4、placeholders::_5等……
有了占位符的概念之后,使得std::bind的使用变得非常灵活 (出现bind中已经定义的参时,优先使用bind中的)
#include <iostream>
#include <functional>
using namespace std;
void output(int x, int y)
{
cout << x << " " << y << endl;
}
int main(void)
{
// 使用绑定器绑定可调用对象和参数, 并调用得到的仿函数
bind(output, 1, 2)();
bind(output, placeholders::_1, 2)(10);
bind(output, 2, placeholders::_1)(10);
// error, 调用时没有第二个参数
// bind(output, 2, placeholders::_2)(10);
// 调用时第一个参数10被吞掉了,没有被使用
bind(output, 2, placeholders::_2)(10, 20);
bind(output, placeholders::_1, placeholders::_2)(10, 20);
bind(output, placeholders::_2, placeholders::_1)(10, 20);
return 0;
}
在用绑定器绑定类成员函数或者成员变量的时候需要将它们所属的实例对象一并传递到绑定器函数内部。f1的类型是function<void(int, int)>,通过使用std::bind将Test的成员函数output的地址和对象t绑定,并转化为一个仿函数并存储到对象f1中。
使用绑定器绑定的类成员变量m_number得到的仿函数被存储到了类型为function<int&(void)>的包装器对象f2中,并且可以在需要的时候修改这个成员。其中int是绑定的类成员的类型,并且允许修改绑定的变量,因此需要指定为变量的引用,由于没有参数因此参数列表指定为void。
示例程序中是使用function包装器保存了bind返回的仿函数,如果不知道包装器的模板类型如何指定,可以直接使用auto进行类型的自动推导,这样使用起来会更容易一些。
10. Lambda表达式
许多现代编程语言中都存在Lambda表达式,Lambda简洁、灵活的特点赋予C++更多的活力
lambda表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda表达式的语法形式简单归纳如下:
[capture](params) opt -> ret {body;};
其中capture是捕获列表,params是参数列表,opt是函数选项,ret是返回值类型,body是函数体。
- 捕获列表[ ]: 捕获一定范围内的变量
- 参数列表( ): 和普通的参数列表一样(没有参数传入的情况下可以省略)
- opt选项(不需要时也可以省略)
- mutable : 捕获的变量将在Lambda表达式内修改
- exception : 指定函数抛出的异常,如抛出的整数类异常,可以使用throw( );
- 返回值类型:在c++11中,Lambda表达式的返回值是通过返回值后置语法定义(可以使用auto自动推导此时就可以省略返回值后置——前提不返回一个不被显性定义的初始化列表)
- 函数体:函数内容
捕获列表
Lambda表达式的捕获列表可以捕获一定范围内的变量,使用方法如下
- [ ] - 不捕捉任何变量
- [ & ] - 捕获外部作用域中的所有变量,做引用在函数体内使用
- [ = ] - 捕获外部作用域中的所有变量,值传递入函数体内使用(在值捕获的参数在Lambda表达式函数体中默认为只读)
- [ =, &arg ] - 按值捕获外部作用域所有的变量,并且按照引用捕获外部变量arg
- [ bar ] - 按值捕获bar变量,同时不捕获其他变量
- [ &bar ] - 上述捕获方式的引用版本
- [ this ] - 捕获当前类中的this指针 (Lambda表达式将获得当前类成员同等的访问权限,如果使用了 & 或 = 默认捕获 this 指针)
函数本质
使用Lambda表达式捕获外部变量,如果我们希望修改捕获的变量,那么如何处理呢?,使用mutable-opt
为什么值拷贝捕获的变量是只读的?
- lambda表达式类型在C++11中被看做是一个带operator()的类
- 按照C++标准,Lambda的operator()是默认为const, const成员函数是无法修改成员变量值的
- mutable作用在于取消const属性
因为Lambda在C++中被看做为一个仿函数,我们可以使用std::function std::bind 进行包装与绑定
- 文章参考——爱编程的大丙 (subingwen.cn)