目录
1. 列表初始化
1.1 初始化
1. 内置类型:
2. 自定义类型
2. 变量类型推导
2.1 auto
2.2 decltype
2.3 nullptr
3. 新增加容器---静态数组array、forward_list以及unordered系列
1. 容器内部的变化:
4. 左值引用 && 右值引用
1. 移动构造
2. 移动赋值
3. 完美转发
5. 默认成员函数控制
5.1 移动构造和移动赋值的默认形成条件
5.2. 强制生成默认函数的关键字default:
5.3. 禁止生成默认函数的关键字delete:
6. 可变参数模板
6.1. 递归函数方式展开参数包:
7. lambda表达式
7.1. lambda表达式的语法
7.2. 捕捉列表说明
7.3. 函数对象与lambda表达式
8.包装器
8.1. function包装器
8.2. bind
8.2.1. 调整形参顺序
8.2.2. 调整形参个数
在 C++98 中,标准允许使用花括号 {} 对数组或者结构体元素进行统一的列表初始值设定,而在C++11中, C++11 扩大了用大括号括起的列表 ( 初始化列表 ) 的使用范围,使其可用于所有的内置类型和用户自 定义的类型, 使用初始化列表时,可添加等号 (=) ,也可不添加 。例如下面的代码
void Test1(void)
{
int x = 1;
// 建议: 对于内置类型来讲,我们需要看懂下面两种的初始化方式,但不建议使用
// 注意: 这个 ‘=’ 可以不添加
int y = {2};
int z{3};
}
列表初始化对于内置类型来说,用的相对较少;但是对于自定义类型,特别是标准库的容器,还是很有意义的。例如:
void Test2(void)
{
std::vector v1;
std::vector v2 = {1,2,3};
std::vector v3{10,20,30};
}
那么问题来了,那上面的代码是如何被支持的呢?
我们需要引出一个新类型:initializer_list
编译器会把上面代码的{1,2,3}当成一个initializer_list
的匿名对象,而在C++11中,标准库的容器都会支持一个这样的构造函数,我们在这里看一下list和vector的initializer list构造函数,用于说明:
也就是说,上面的代码之所以编译成功,是因为标准库的vector实现了initializer_list这个构造函数,通过initializer_list的匿名对象构造了我们的vector;
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加
std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。同时也可以作为operator=的参数,这样就可以用大括号赋值,例如:
void Test3(void)
{
std::map my_map{ { "effective", "有效的" }, { "career", "事业" }, { "success", "成功" } };
for (auto& e : my_map)
{
std::cout << e.first << " - " << e.second << std::endl;
}
std::initializer_list> tmp{ {"exist","存在"} };
//标准库里的operator=也提供了 initializer_list版本
my_map = tmp; // map& operator= (initializer_list il);
for (auto& e : my_map)
{
std::cout << e.first << " - " << e.second << std::endl;
}
}
总结:C++11以后一切对象都可以用列表初始化。但我们建议普通对象还是以前的方式初始化,容器如果有需求可以用列表初始化。
在C++98 中 auto 是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto 就没什么价值了。C++11 中废弃 auto 原来的用法,将 其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
void Test4(void)
{
auto x = 10;
auto y = 10.1;
std::map my_map{{"effective","有效的"}};
auto it = my_map.begin();
std::cout << typeid(x).name() << std::endl;
std::cout << typeid(y).name() << std::endl;
std::cout << typeid(it).name() << std::endl;
}
g++编译,需要加上 #include
这个头文件,得到的结果分别是:i,d,St17_Rb_tree_iteratorISt4pairIKSsSsEE vs编译,得到的结果是:int,double,class std::_Tree_iterator
,class std::allocator > const ,class std::basic_string ,class std::allocator > > > > >
typeid(对象).name 只是单纯的拿到这个对象的类型的字符串,不能用这个结果去定义新的对象,而关键字decltype将变量的类型声明为表达式指定的类型。
void Test5(void)
{
int x = 10;
decltype(x) y = 5; // 将y的类型指明为 decltype(x)表达式的类型,即x的类型
std::cout << typeid(x).name() << std::endl; // int
std::cout << typeid(y).name() << std::endl; // int
}
可能会遇到的场景如下:
template
void F(T1 x, T2 y)
{
// 此时x和y的类型不确定,我们可以(x*y)的结果去推出ret的类型
decltype(x * y) ret;
cout << typeid(ret).name() << endl;
}
void Test6(void)
{
int x = 10;
double y = 1.1;
F(x, y);
}
由于 C++ 中 NULL 被定义成 0 ,这样就可能回带来一些问题,因为 0 既能表示指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11 中新增了 nullptr ,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++11增加的新容器,亮点就是unordered_set和unordered_map,但是array 和 forward_list 很鸡肋
可以看到,std::array是一个大小为N的静态数组。那么C++11增加array的初衷是什么呢?
其中最主要的点就是,c数组和array对越界的判定
void Test7(void)
{
int arr[5] = { 1, 2, 3, 4, 5 };
// C数组对越界读是很难检查出来的
std::cout << arr[-1] << " - " << arr[6] << std::endl; // 非法读操作,检查不出来
// C数组对越界写的检查是有限的,或者说是一种抽查检查
//++arr[6]; // 在数组的边界可以检查出来
++arr[20]; // 非法写操作,无法检查出来
// 但是对于 std::array来说,不论你是越界读亦或者是越界写都可以检查出来
std::array std_arr;
std::cout << std_arr[6] << std::endl;
++std_arr[20];
// 因为std::array的读写操作(operator[])都会强制检查 pos < size();
}
因此C++11 增加array的初衷就是:希望杜绝或者减少使用C数组
C数组对越界的判定:是一种抽查检查,越界读检查不出来,越界写可能会查出来(一种边界/抽查检查)
std::array对越界的判定:operator[] 会强制检查,pos < size();对越界读和越界写都会检查出来
从这个角度来讲,array还是有一定的价值的,但为什么array用的不多呢?
一方面,大家用C数组用习惯了;另一方面,如果你担心越界,那么用array还不如用vector;其次,array是一个静态数组,具有一定的局限性。
forward_list是一个单链表,和list没有太大差别
在这里说一个差别:
forward_list的insert和erase都是在当前位置的下一个位置进行操作,insert会在当前迭代器的后面插入 ,而erase,它不是删除当前位置的迭代器,而是删除下一个位置的迭代器。因为如果要删除当前位置的迭代器,那么时间复杂度就是O(N)(需要找前一个位置的迭代器)
1、都支持initializer_list构造,用来支持列表初始化
2、比较鸡肋的接口。比如cbegin、cend系列
3、移动构造和移动赋值,可以在某些地方提高效率(对于需要深拷贝的类意义很大)
4、右值引用参数的插入,其作用也是提高效率。例如,insert系列,emplace系列。
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,之前学习的引用就叫做左值引用,但无论左值引用还是右值引用,都是给对象取别名。在 C++ 中,左值表达式表示一个标识符或者一个具有持久性的对象,而右值表达式表示一个临时对象或者一个表达式的结果,它没有持久性。
左值引用是指向左值类型的引用,通过使用
&
来声明。它允许我们修改引用所绑定的对象的值,并且可以将这个引用用作函数的参数和返回值。右值引用是指向右值类型的引用,通过使用
&&
来声明。它主要用于实现移动语义和完美转发,可以在不发生额外内存分配的情况下将资源从一个对象转移到另一个对象。
第一个问题:什么叫左值?什么叫右值?
简单的理解:可以被取地址的对象,都被称之为左值;反之,不能被取地址的对象,称之为右值。
在C++中,左值(L-value)是可以被标识符引用的表达式或对象,它具有持久性并且可以被修改。通常,变量、对象、数组元素和函数返回的左值引用都被认为是左值。
左值可以取地址+可以对它赋值。左值可以出现赋值符号的左边,右值不能出现在赋值符号左边,特例:定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址
void Test1(void)
{
int x = 10; // x是一个左值
int arr[5]; // arr是一个左值
int& ref = x; // ref是一个左值引用
}
右值(R-value)是临时的表达式或对象,它在表达式求值后就会消失。通常,字面常量、临时对象和表达式的结果都被认为是右值。
右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边(右值不可修改)右值不能 取地址。右值引用通常用于处理右值。右值引用具有特殊的语法形式(使用
&&
),可以扩展右值的生命周期,并且可以支持移动语义和完美转发等高级功能。
void Test2(void)
{
10; // 字面常量是一个右值
int&& ref = 10; // ref是一个右值引用
int x = 10, y = 20;
(x + y); // (x+y)表达式的结果会生成一个临时对象,这个临时对象就是一个右值
}
第二个问题:左值可以引用右值吗?
普通情况下不可以,但是const左值引用可以引用右值
const 左值引用 既可以引用左值,也可以引用右值
template
void func1(T x) {}
// 对于func1()来说,由于此时是值传递,不管你的形参是左值亦或者是右值
// 我都可以接收,因为此时是拷贝,不受影响
template
void func2(T& x) {}
// 而对于func2()来说,此时是左值引用传参,那么此时我只能引用左值,无法引用右值(权限被放大)
template
void func3(const T& x) {}
// 因此,我们以前就提过,对于左值引用传参,最好加上const,即可以引用左值,亦可以引用右值
void Test3(void)
{
int& x = 10; // 默认情况下,左值是不可以引用右值的,我们理解为权限被放大了
const int&x = 10; // const的左值引用是可以引用右值的
}
第三个问题:右值引用可以引用左值吗?
默认情况下,右值引用不可以引用左值,但是可以引用std::move(左值)
void Test4(void)
{
int x = 10;
int&& R_ref1 = x; //编译报错,默认情况下,右值引用不可以引用左值
int&& R_ref2 = std::move(x); // 但是右值引用可以引用std::move(左值)
}
总结:
对于左值引用来说:
左值引用可以引用左值,默认情况下不可以引用右值,但是const左值引用既可以引用左值,也可以引用右值。
对于右值引用来说:
右值引用可以引用右值,默认情况下不可以引用左值,但是右值引用可以引用std::move(左值)。
注意:
默认情况下,右值是不可以取地址的,但是给右值取别名后,会导致别名被存储到特定位置,且可以取到这个别名的地址。例如:
void Test5(void)
{
//&(10); // 编译报错,默认情况下,不可以对右值取地址
int&& R_ref = 10; // 对右值取别名
std::cout << &R_ref << std::endl; // 对右值取别名后,我们发现即可以取R_ref的地址
std::cout << ++R_ref << std::endl; // 同时可以修改R_ref的值
// 当然,如果不想R_ref被修改,可以用const int&& R_ref去引用
// const int&& R_ref = 10;
// 可以这样理解,右值的别名是一个左值
}
上面的问题了解了之后,我们回到开始,在C++98,标准是不区分左值引用和右值引用的,
那为什么在C++11中还要右值引用呢?
首先,左值引用有什么用途呢?或者说左值引用解决了哪些问题呢?
左值引用的用途:
其一:做函数参数:a. 减少拷贝,提高效率;b. 可以做输出型参数
其二:做返回值:a. 减少拷贝,提高效率;b. 引用返回,可以修改返回对象
这样看来,左值引用已经解决了很多问题,但实际上,对于第二种用途,左值引用在某些情况下,不能起到很好的作用。例如:std::to_string
string to_string(int num)
{
// 具体实现省略
// 可以看到tmp是一个局部对象,出了函数作用域
// 就会被销毁,因此不可以用传引用返回,故在此传值返回
// 而传值返回(拷贝)代价是很大的
string tmp;
return tmp;
}
而C++11的右值引用就是为了解决类似上面的情形的;而为了更好的理解,我们在这里写了一个简陋版本的string,为了更好地说明问题;
namespace Xq
{
class string
{
public:
string(const char* str = "")
{
_size = strlen(str);
_str = new char[_size + 1];
_capacity = _size;
strcpy(_str, str);
std::cout << "string(const char* str = "")" << std::endl;
}
string(const string& copy)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
string tmp(copy._str);
swap(tmp);
std::cout << "string(const string& str)" << std::endl;
}
void swap(string& tmp)
{
std::swap(_str, tmp._str);
std::swap(_size, tmp._size);
std::swap(_capacity, tmp._capacity);
}
string& operator=(const string& str)
{
string tmp(str._str);
swap(tmp);
std::cout << "string operator=(const string& str)" << std::endl;
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
_capacity = _size = 0;
std::cout << "~string()" << std::endl;
}
private:
char* _str;
size_t _capacity;
size_t _size;
};
string to_string(int num)
{
// 具体实现省略
string tmp;
return tmp;
}
}
第一个问题:请看如下代码
这段代码,会发生什么呢?
是一次构造 + 两次拷贝构造
还是 一次构造 + 一次拷贝构造呢?
void Test6(void)
{
Xq::string ret = Xq::to_string(1234);
}
结果:
string(const char* str = )
string(const string& str)
~string()
~string()结果很明确,一次构造 + 一次拷贝构造,为什么呢?我们之前说过,传值返回会拷贝构造一个临时对象,这个临时对象具有常性(也就是说,是一个右值),再由临时对象拷贝构造ret这个对象,但是,编译器认为中间的这个临时对象意义不大,于是编译器进行了优化,将这个对象进行了省略,而是将tmp直接拷贝构造ret这个对象((一般如果tmp比较小(4 or 8字节),tmp会存于寄存器中,如果比较大,那么tmp会在上一级函数栈帧中))
而我们也可以去掉这种编译器的优化,在这里用g++演示
-fno-elide-constructors 编译的时候带上该选项,可以去掉编译器的这种优化
结果符合预期,当去掉编译器的优化时,此时就是两次拷贝构造
但是,有可能我们会觉得这个临时对象是没有价值的,哪能不能省略呢?
答案是,不可以。因为会有下面的场景:
void Test6(void)
{
Xq::string ret;
ret = Xq::to_string(1234);
}
因为编译器的优化只会发生在连续的一个式子中,而上面的式子是不会发生优化的,因此必须要这个临时变量返回给to_string,在调用operator=给ret。
那么C++11的右值引用是如何解决上面的问题的呢?
首先,C++11的右值引用并不是直接起作用,而是借助移动构造和移动赋值来达到目的的。
而C++11中,又将右值分为两种:
1、内置类型的右值 --- 称之为纯右值
2、自定义类型右值 --- 称之为将亡值,例如上面to_string的临时对象
那么,移动构造和移动赋值如何实现呢?
// str是一个右值
string(string&& str)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
// 将str这个将亡值的资源转移给*this
// 移动构造体现的是资源的转移
swap(str);
std::cout << "string(string&& str)" << std::endl;
}
void Test6(void)
{
// xq::to_string(1234)返回的临时对象是一个右值
// 此时会调用移动构造,而不是拷贝构造
Xq::string ret1 = Xq::to_string(1234);
}
结果:
string(const char* str = )
string(string&& str)
~string()
~string()
可以看到,移动构造减少了拷贝构造,间接提高了效率。
为了更好地理解移动构造和拷贝构造的区别:
移动构造是转移将亡值的资源。
拷贝构造是重新开辟一块空间,构造一个新的对象。
void Test7(void)
{
Xq::string str1 = "haha";
// 调拷贝构造
Xq::string str2(str1);
// move(str1) 是一个右值,调用移动构造
Xq::string str3(std::move(str1));
}
string& operator=(string&& copy)
{
// 在这里不仅将copy这个将亡值的资源转移给了*this
// 同时copy还会去调用析构释放*this原有的资源
swap(copy);
std::cout << "string& operator=(string&& copy)" << std::endl;
return *this;
}
void Test8(void)
{
// 在没有实现移动构造和移动赋值的情况下:
// 一次构造
Xq::string str1;
// 三次构造(拷贝构造和赋值以及to_string各自调了一次构造) + 一次拷贝构造 + 一次赋值
str1 = Xq::to_string(1234);
std::cout << "----------------------------------------" << std::endl;
// 当实现移动构造和移动赋值的情况下:
// 一次构造
Xq::string str2;
// 移动构造 + 移动赋值 + 一次构造(to_string里面的构造)
str2 = Xq::to_string(1234);
}
结果:
string(const char* str = )
string(const char* str = )
string(const char* str = )
string(const string& str)
~string()
~string()
string(const char* str = )
string operator=(const string& str)
~string()
~string()
~string()
----------------------------------------string(const char* str = )
string(const char* str = )
string(string&& str)
~string()
string& operator=(string&& copy)
~string()
~string()
移动赋值也是对资源的转移,从而达到减少拷贝的目的。
在C++11中,标准库的vector和list的成员函数push_back提供了新的版本,不仅如此,标准库的容器只要是插入都会增加右值引用的版本
插入过程中,如果传递对象是一个右值,那么就会调移动语句,进行资源转移,减少拷贝。
void Test9(void)
{
std::vector v;
Xq::string str1("hehe");
// 左值,调用void push_back(const value_type& val);
// 调用拷贝构造
v.push_back(str1);
std::cout << "-----------------------------" << std::endl;
// 匿名对象 右值,调用void push_back(value_type&& val);
// 调用移动构造
v.push_back(Xq::string("haha"));
}
void Test10(void)
{
std::list lt;
Xq::string str1("hehe");
// 左值,调用void push_back(const value_type& val);
// 调用拷贝构造
lt.push_back(str1);
std::cout << "-----------------------------" << std::endl;
// 匿名对象 右值,调用void push_back(value_type&& val);
// 调用移动构造
lt.push_back(Xq::string("haha"));
}
对于右值引用来说,如果你是确定的类型,那么你就是右值引用,但如果你是模板,那么我们称之为"万能引用",也称之为"引用折叠 "。
万能引用:既能引用左值,也能引用右值
例如:
void test_func(int&& ref)
{
// 首先,这里是一个右值引用,默认情况下只能引用右值,或者引用std::move(左值)
// 但在这里,ref是一个左值,为什么?
// 我们之前说过,右值的引用是一个左值,因为它可以取地址,即是一个左值
std::cout << "我是右值引用" << std::endl;
}
template
void test_func(T&& ref)
{
// 如果增加了模板,这里不是一个右值引用,称之为"万能引用"或者"引用折叠"
// 什么意思呢? 就是说,此时的ref 既可以引用左值,也可以引用右值
// 也就是说,万能引用提供了能够同时接收左值引用和右值引用的能力
// 但是,此时ref就是一个左值了
std::cout << "我是万能引用" << std::endl;
}
void func(int& ref){ std::cout << "左值引用" << std::endl; }
void func(const int& ref){ std::cout << "const 左值引用" << std::endl; }
void func(int&& ref){ std::cout << "右值引用" << std::endl; }
void func(const int&& ref){ std::cout << "const 右值引用" << std::endl; }
template
void perfect_forwarding(T&& t)
{
func(t);
}
void Test11(void)
{
int i = 10;
const int j = 20;
perfect_forwarding(i); // 左值引用
perfect_forwarding(j); // const 左值引用
perfect_forwarding(std::move(i)); // 右值引用
perfect_forwarding(std::move(j)); // const 右值引用
}
这四条函数调用会得到什么结果呢? 是根据它们的原有属性去调用对应的函数吗?
结果如下:
左值引用
const 左值引用
左值引用
const 左值引用可以发现,不管你是左值还是右值,最后调用的接口都是左值的引用,只不过,最后会根据是否具有const属性去调用对应的函数。
那么我们可以这样认为,万能引用虽然提供了能够同时接收左值引用和右值引用的能力,但是此时的ref却无条件的成为了左值,即后续的使用都会成为左值, 但是如果我们希望能够在传递过程中保持它的左值或者右值的属性,该如何做呢?
如果要保持对象的原有属性,需要完美转发,完美转发会保持原对象的属性,例如:
template
void perfect_forwarding(T&& t)
{
// 此时我想保持t的原有属性,因此需要完美转发
// 语法: std::forward(要保持的对象)
func(std::forward(t));
}
此时的结果:
左值引用
const 左值引用
右值引用
const 右值引用
总结:
模板的万能引用提供了能够接收同时接收左值引用和右值引用的能力, 但是这种能力也会带来一些"副作用", 它会让该函数接收后的类型都成为左值 。完美转发(perfect forwarding)是使用万能引用来传递参数的过程,主要目的是提供一种机制,使得函数模板能够将参数传递给其他函数,同时保持参数的原始特征。
在C++11之前,我们认为类的默认成员函数有六个,构造、析构、拷贝构造、赋值运算符重载、取地址、const取地址,这里就不多解释了。
而在C++11中,新增了两个类的默认成员函数,分别是:移动构造(move constructor),移动赋值(move operator=)
而我们以前说过,类的默认成员函数,如果我们实现了,编译器不会在自动提供这些默认成员函数;如果我们不写,编译器会自动生成一份,而对于它们俩也不例外,但是,以前的默认成员函数,只要我们不写,编译器就会自动生成一份,例如,我们不写构造,那么编译器就会自动生成一份构造,然而,移动构造和移动赋值自动生成的条件却更为苛刻
首先,我们应该知道,移动构造和移动赋值的初衷是为了减少拷贝,提高效率。而对于不需要进行深拷贝的类来说,移动构造和移动赋值的价值不大。换句话说,移动构造和移动赋值是为了需要进行深拷贝的类而实现的。因此:
默认移动构造生成的条件是:如果你没有实现移动构造,并且没有实现析构函数和拷贝构造以及赋值运算符重载的任意一个,那么编译器就会自动生成一份默认移动构造函数。
默认生成的移动构造的功能:编译器默认生成的移动构造对内置类型按字节序的方式进行拷贝,对自定义类型会去调用它的移动构造,如果这个自定义类型没有实现移动构造,那么会去调用它的拷贝构造
默认移动赋值生成的条件:如果你没有实现移动赋值重载函数,且没有实现析构函数和拷贝构造以及赋值运算符重载的任意一个,那么编译器会自动生成一个默认移动赋值
默认生成的移动赋值的功能:编译器默认生成的移动赋值对内置类型按字节序的方式进行拷贝,对自定义类型会去调用它的移动赋值,如果这个自定义类型没有实现移动赋值,那么会去调用它的赋值运算符重载
总结,对于一个类来说,如果要写析构,那么说明有资源需要被释放,那么也就意味着需要实现深拷贝和赋值运算符重载。也就是说,看似移动语义的自动生成条件很苛刻,但是如果一个类没有资源需要清理,那么就可以满足移动语义的自动生成条件,反之,如果有资源需要被清理,那么移动语义不会被自动生成
C++11 可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default 关键字显示指定移动构造生成。
class B
{
public:
B(int b = 0) :_b(b) {}
B(const B& b)
:_b(b._b)
{
std::cout << " B(const B& b) " << std::endl;
}
// 由于此时已经实现了拷贝构造,编译器不会默认生成移动构造
// 因此我们可以强制生成移动构造
B(B&& ref) = default;
private:
int _b = 0;
};
如果想要限制某些默认函数的生成,在 C++98 中,是将该函数设置成 private ,并且只声明不定义,这样只要其他人想要调用就会报错。在C++11 中更简单,只需在该函数声明加 =delete 即可,该语法指示编译器不生成对应函数的默认版本,称=delete 修饰的函数为删除函数。
class A
{
public:
A(int a = 0) :_a(a) {}
// 假如我不想让别人通过拷贝我以构造一个新对象
A(const A& a) = delete;
private:
int _a = 0;
};
void Test1(void)
{
A a;
// 编译报错,此时就无法调用拷贝构造函数
A copy(a);
}
class heap_only
{
public:
// 禁止生成析构函数
~heap_only() = delete;
};
void Test3(void)
{
// 下面的两种实例化对象的方式都会报错,因为此时没有析构函数,无法创建对象
heap_only ho;
static heap_only s_ho;
// 那此时如何创建对象呢?
// 此时可以new一个对象
heap_only* new_ho = new heap_only;
}
上面的问题解决了,那假如此时这个类有资源需要被清理,该如何解决?
class heap_only
{
public:
heap_only()
:_str(new char[10])
{}
// 禁止生成析构函数
~heap_only() = delete;
void destroy()
{
// "内层"资源:
delete[] _str;
// "外层"资源
operator delete(this);
}
private:
char* _str = nullptr;
};
void Test3(void)
{
// 因为此时析构无法调用,那么如何释放资源呢?
heap_only* new_ho = new heap_only;
//delete[] new_ho; // 报错,无法调用析构
// 但是,此时我可以调用一个destroy,释放这些资源
new_ho->destroy();
}
可变参数(variadic arguments)是指在函数或模板中可以接受任意数量的参数。C++中有两种方式来实现可变参数:函数的可变参数和模板的可变参数。
参数包(parameter pack)是C++中的一个特性,它允许在模板中一次传递多个参数。参数包可以包含任意数量的参数,包括类型、非类型和模板参数。
参数包用三个点号(
...
)表示,它里面包含了0到N(N>=0)个参数下面是一个可变参数的函数模板,如何使用递归展开这个参数包?
我们无法直接获取参数包args中的每个参数的, 只能通过展开参数包的方式来获取参数包中的每个参数
// 递归终止函数
void print()
{
std::cout << std::endl;
}
template
void get_size(Args... args)
{
// 在这里可以用sizeof计算当前的参数个数 sizeof...(参数包)
std::cout << sizeof...(args) << std::endl;
}
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个函数参数包Args... args,这个参数包中可以包含0到任意个模板参数。
// 参数包展开函数
// 这个递归过程是一个编译时决议,编译过程中推args这个参数包有几个参数
template
void print(const T& t,Args... args)
{
std::cout << t << std::endl;
print(args...);
}
void Test4(void)
{
print(1);
print(1,'x');
print(1, 'x', std::string("haha"));
}
在这里利用了C++11的一个特性 --- 列表初始化,通过列表初始化来初始化一个变长数组, {(printarg(args), 0)...}将会展开成{ printarg(arg0), printarg(arg1), printarg(arg2), ... printarg(argN) },最终会创建一个元素值都为0的数组 int arr[sizeof... (args)]的数组
template
int print_arg(const T& t)
{
std::cout << t << std::endl;
// 这里的返回值: 用于初始化数组
return 0;
}
template
void print(Args... args)
{
// 这是一个列表初始化,用于初始化一个变长数组
// { print_arg(args)...} 将会被展开 { print_arg(args0) }, { print_arg(args1) } , ... , { print_arg(argsN) }
//这个数组的目的就是单纯为了在数组构造的过程展开参数包
int a[] = { print_arg(args)...};
}
void Test4(void)
{
print(1);
print(1,'x');
print(1, 'x', std::string("haha"));
}
STL容器中的empalce相关接口函数:
emplace的接口还有很多,在这就不一一举例了, 首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。我们在这里以list举例,push_back和emplace系列接口的优势到底在哪里呢?
class Time
{
public:
Time(int hours, int minute, int second)
:_hours(hours)
, _minute(minute)
, _second(second)
{std::cout << "Time(int hours, int minute, int second)" << std::endl;}
Time(const Time& copy)
{
std::cout << "Time(const Time& copy)" << std::endl;
}
Time& operator=(const Time& copy)
{
std::cout << "Time& operator=(const Time& copy)" << std::endl;
}
private:
int _hours = 0;
int _minute = 0;
int _second = 0;
};
void Test5(void)
{
std::list
结果:
Time(int hours, int minute, int second)
Time(const Time& copy)
------------------------
Time(int hours, int minute, int second)
push_back: 是将给定元素以副本的方式添加到容器的末尾。它接受一个参数,该参数会被复制(或者移动)到容器中。
emplace_back(): 是在容器的末尾直接创建对象,而不是通过复制或移动已有对象。它接受任意数量的参数,并将这些参数传递给对象的构造函数在容器中就地构造元素。这样可以避免额外的复制或移动操作,提高插入操作的效率。
总结:
push_back()
需要提供已有类型的对象,会进行一次复制或移动操作。emplace_back()
可以直接在容器中就地构造元素,避免了额外的复制或移动操作,可以提高效率。emplace_back()
的参数会被传递给元素的构造函数,因此可以接受任意数量和类型的参数。尽可能使用
emplace_back()
来避免不必要的对象复制或移动,以提高性能和代码效率。
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}表达式各部分说明[capture-list] : 捕捉列表 ,该列表总是出现在 lambda 函数的开始位置, 编译器根据 []来判断接下来的代码是否为lambda 函数 , 捕捉列表能够捕捉上下文中的变量供lambda函数使用。(parameters):参数列表。与 普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略mutable:默认情况下, lambda 函数总是一个 const 函数, mutable可以取消其常量性。使用该修饰符时,参数列表不可省略( 即使参数为空 )。->returntype:返回值类型 。用 追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。注意:在lambda 函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此 C++11 中 最简单的 lambda 函数为: []{} ; 该 lambda函数不能做任何事情
用lambda实现减法
void Test6(void)
{
int x = 20;
int y = 10;
// 用lambda实现一个减法
// lambda是一个匿名函数对象,上层拿不到名字,一般用auto自动推导名字
// [] 里面的内容是捕捉列表,在这里可以省略
auto sub1 = [](int x, int y)->int{ return x - y; };
std::cout << sub1(x, y) << std::endl;
// lambda可以省略返回值类型,让其自己推导
auto sub2 = [](int x, int y){return x - y; };
std::cout << sub2(x, y) << std::endl;
}
用lambda实现swap,理解捕捉列表的细节
void Test7(void)
{
int x = 10;
int y = 20;
std::cout << "original status:> " << x << " " << y << std::endl; // 10 20
// 用lambda实现一个swap函数,返回值类型可以带,在这里试一下
auto swap_one = [](int& x, int& y)->void{
int tmp = x;
x = y;
y = tmp;
};
swap_one(x, y);
std::cout << "swap status:> " << x << " " << y << std::endl; // 20 10
// 假如我现在要求lambda不传参数如何实现呢?
// 我们需要借用捕捉列表
x = 10;
y = 20;
std::cout << "original status:> " << x << " " << y << std::endl; // 10 20
// 但是我们发现,默认捕捉列表的对象是一个右值,不可修改
//因此此时需要加上mutable, mutable: 可以取消常性
auto swap_two = [x, y]()mutable{
int tmp = x;
x = y;
y = tmp;
};
swap_two();
std::cout << "swap status:> " << x << " " << y << std::endl; // 10 20
// 我们发现,此时结果并没有改变,因为上面的捕捉列表是传值捕捉,是一种拷贝
// 捕捉列表的x,y是定义的x,y的一份拷贝,因此我们可以传引用捕捉
x = 10;
y = 20;
std::cout << "original status:> " << x << " " << y << std::endl; // 10 20
//&x,&y是 外面x,y的引用,注意: 在这里不是取地址
auto swap_three = [&x, &y]()mutable{
int tmp = x;
x = y;
y = tmp;
};
swap_three();
std::cout << "swap status:> " << x << " " << y << std::endl; // 20 10
}
捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式传值还是传引用。[var]:表示值传递方式捕捉变量var[=]:表示值传递方式捕获所有父作用域中的变量(包括this)[&var]:表示引用传递捕捉变量var[&]:表示引用传递捕捉所有父作用域中的变量(包括this)[this]:表示值传递方式捕捉当前的this指针注意:a. 父作用域指包含lambda函数的语句块b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复d. 在块作用域以外的lambda函数捕捉列表必须为空。e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。f. lambda表达式之间不能相互赋值,即使看起来类型相同g: 捕捉列表本质是在传参(值传递和引用传递 )
int e = 3;
static int f = 3;
void Test1(void)
{
int a, b, c, d;
a = b = c = d = 1;
//值传递方式捕捉变量a和b
auto lam1 = [a,b](){
std::cout << a << b << std::endl;
};
lam1(); // 11
//引用传递方式捕捉变量c和d
auto lam2 = [&c, &d](){
++c;
++d;
std::cout << c << d << std::endl;
};
lam2(); // 22
//以值传递捕捉所有父作用域的变量(包含this)
auto lam3 = [=](){
std::cout << a << b << c << d << std::endl;
};
lam3(); // 1122
//以引用传递捕捉所有父作用域的变量(包含this)
auto lam4 = [&]()
{
a = b = c = d = 4;
std::cout << a << b << c << d << std::endl;
};
lam4();
// 混合捕捉
// =代表以值传递捕捉所有父作用域的变量,而a也代表以值传递捕捉a
// 因此在这里会发生报错,捕捉列表不可以重复
//auto lam5 = [=, a]{}; // 编译报错
//正确情况
//以值传递捕捉所有父作用域的变量,且以引用传递捕捉a
auto lam5 = [=, &a]{};
//或者
//以引用传递捕捉所有父作用域的变量,且以值传递捕捉a
auto lam6 = [&, a]{};
// 父作用域可以理解为当前函数所在的栈帧
// 例如我当前在Test1()内,可以捕捉Test1的所有变量
// 但不可以捕捉Test1()外的变量
// 特例:对于全局变量或者全局的静态变量而言,是不可以被捕捉的
// 但是却可以使用这个全局变量或者全局的静态变量
auto lam7 = []{
std::cout << e << f << std::endl;
};
lam7(); // 33
}
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象,而其实,lambda在底层上就是一个仿函数。
struct Add
{
int operator()(int left, int right)
{
return left + right;
}
};
void Test2(void)
{
// 一个仿函数对象
Add add1;
std::cout << add1(10, 20) << std::endl;
// lambda
auto add2 = [](int x, int y){return x + y; };
std::cout << add2(10, 20) << std::endl;
}
我们借助反汇编代码,观察lambda的底层细节:
通过对比,我们发现,lambda表达式的底层其实也是一个仿函数。即如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator(),这个仿函数的名字是lambda_"字符串" ,而这个字符串称之为UUID(通用唯一标识符)
// 我们可以理解为这里是在调用类的构造函数
auto add2 = [](int x, int y){return x + y; };
// 而这里可以理解为是在调用operator()
std::cout << add2(10, 20) << std::endl;
为什么lambda表达式之间不能相互赋值,即使看起来类型相同
void(*Pf)();
void Test3(void)
{
// 有了对lambda底层的理解
// 我们就知道为什么lambda表达式之间不能相互赋值,即使看起来类型相同
auto lam1 = [](){std::cout << "haha" << std::endl; };
auto lam2 = [](){std::cout << "haha" << std::endl; };
// 这里编译不通过
// 原因是因为lambda底层是一个仿函数,在这里就是两个类
// 没有实现特定的operator=,因此编译报错
lam1 = lam2;
// lambda是一个匿名函数对象
// 但是允许拷贝构造一个新的函数对象
// 在这里调用编译器默认生成的拷贝构造
auto lam3(lam1);
lam3();
// lambda表达式可以赋值给同类型的函数指针
Pf = lam3;
Pf();
}
UUID的拓展:
UUID,全称为通用唯一标识符(Universally Unique Identifier),是一种标识符的格式,用于在计算机系统中唯一标识信息。它是由128位二进制数表示的,通常以32个十六进制数的形式展示,以连字符分隔开。
UUID的目的是在分布式系统中为每个实体分配一个唯一的标识符,以避免冲突和重复。它在各个系统和应用程序之间具有全局唯一性,即使这些实体在不同的地方、不同的时间创建,也能保证标识符的唯一性。
UUID的生成算法通常基于计算机的MAC地址、时间戳和随机数等因素,保证生成的标识符在一定程度上具有全局唯一性。常见的UUID版本有1、3、4和5,每个版本对应不同的生成方式和标识符的含义。
UUID在很多领域都有广泛的应用,特别是在分布式系统、数据库、网络通信以及唯一标识的需求场景中。由于其全局唯一性和较低的重复概率,它被广泛认可为一种可靠的标识符格式。
function 包装器也叫作适配器。 C++ 中的 function 本质是一个类模板,也是一个包装器
void Test4(void)
{
//std::function 在头文件
// 类模板原型如下:
template function; // undefined
//模板参数说明:
//Ret : 被调用函数的返回类型
//Args...: 被调用函数的形参
template
class function;
}
那如何使用呢?
struct Add
{
int operator()(int left, int right)
{
return left + right;
}
};
class A
{
public:
int get_min_num(int a,int b)
{
return a < b ? a : b;
}
static void show()
{
std::cout << "haha" << std::endl;
}
};
void Test4(void)
{
// 包装get_max_num这个函数
std::function f1 = get_max_num;
// 我们也可以包装一个仿函数(函数对象)
std::function f2 = Add();
auto lam1 = [](double a, double b){return a + b; };
// 我们也可以包装一个lambda表达式
std::function f3 = lam1;
// 我们也可以包装一个类的静态成员函数
std::function f4 = A::show;
// 我们也可以包装一个类的非静态成员函数
// 注意: 取非静态成员函数的地址,要加上&(语法规定)
// 且这里的参数包要多传一个参数,传一个类的类型过去
// 因为非静态的成员函数只能让对象去调用它
std::function f5 = &A::get_min_num;
std::cout << f1(20, 10) << std::endl;
std::cout << f2(10, 20) << std::endl;
std::cout << f3(1.1, 2.2) << std::endl;
f4();
int left = 10;
int right = 20;
std::cout << f5(A(),10,20) << std::endl;
}
包装器的应用:
150. 逆波兰表达式求值 - 力扣(LeetCode)
class Solution {
public:
int evalRPN(vector& tokens) {
std::stack st;
// 在这里利用列表初始化
// std::map> ret_map =
// {
// {"+",[](long long left,long long right){return left + right;}},
// {"-",[](long long left,long long right){return left - right;}},
// {"*",[](long long left,long long right){return left * right;}},
// {"/",[](long long left,long long right){return left / right;}}
// };
//
// 在这里利用map的operator[]
std::map> ret_map;
ret_map["+"] = [](long long left,long long right)->long long{return left + right;};
ret_map["-"] = [](long long left,long long right)->long long{return left - right;};
ret_map["*"] = [](long long left,long long right)->long long{return left * right;};
ret_map["/"] = [](long long left,long long right)->long long{return left / right;};
// 遇到操作数就入栈
// 遇到操作符就运算,运算结果入栈
for(auto& str : tokens)
{
// 如果对应的字符在,就说明是操作符
if(ret_map.count(str))
{
long long right = st.top();
st.pop();
long long left = st.top();
st.pop();
st.push(ret_map[str](left,right));
}
else
st.push(std::stoll(str));
}
return st.top();
}
};
std::bind 函数定义在头文件中, 是一个函数模板,它就像一个函数包装器 ( 适配器 ) , 接受一个可 调用对象( callable object ),生成一个新的可调用对象来 “ 适应 ” 原对象的参数列表 。一般而言,我们用它可以把一个原本接收N 个参数的函数 fn ,通过绑定一些参数,返回一个接收 M 个( M可以大于N ,但这么做没什么意义)参数的新函数。同时,使用 std::bind 函数还可以实现参数顺序调整等操作
#include
#bind: 用来调整函数参数
template
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template
/* unspecified */ bind (Fn&& fn, Args&&... args);
有时候我们遇到如下的场景:
class Sub
{
public:
int sub(int left, int right)
{
return left - right;
}
};
int add(int left, int right)
{
return left + right;
}
void Test6(void)
{
// 如果我想用包装器将上面的加法和减法运算包起来
std::map> cal_map;
cal_map["+"] = add;
// 但是我们的sub不能被上面的包装器包起来
// 因为sub是一个非静态的成员函数
// 它的包装器: std::function f1 = &Sub::sub;
cal_map["-"] = &Sub::sub; // 编译报错
// 而我们的bind就可以处理这样的问题
}
bind的用途:std::bind是 C++ 标准库中的一个函数模板,用于创建一个新的可调用对象(函数对象、函数指针等),同时绑定某些参数、调整形参顺序或指定默认参数值
std::placeholders
是 C++ 标准库中的一个命名空间,用于占位符的定义。它定义了一组特殊的占位符对象,用于在使用std::bind
时代表尚未绑定的参数位置。在使用
std::bind
时,可以通过使用占位符对象将函数参数位置预留下来,而无需提供实际的参数值,这些预留的位置将在实际调用时动态绑定。std::placeholders
提供了占位符_1
、_2
、_3
等,用于指代相应位置的参数。
double Div(double left, double right)
{
return left / right;
}
void Test6(void)
{
// 解释:_1,_2 定义在std::placeholders命名空间中,代表被绑定函数未绑定的形参
// _1,_2分别代表函数的未绑定第一个形参,未绑定的第二个形参
// std::bind(Div, std::placeholders::_1, std::placeholders::_2) 是一个匿名函数对象
// 可以用auto自动推导它的类型
auto bind_func1 = std::bind(Div, std::placeholders::_1, std::placeholders::_2);
std::cout << bind_func1(10, 5) << std::endl; // 2
// 也可以用function包装器接受
std::function bind_fun2 = std::bind(Div, std::placeholders::_2, std::placeholders::_1);
std::cout << bind_fun2(10, 5) << std::endl; // 0.5
}
回到开始,Sub之所以不能被上面的包装器包起来,原因是为它的包装器需要三个参数的参数包,因此我们可以利用bind进行绑定一个形参
class Sub
{
public:
int sub(int left, int right)
{
return left - right;
}
};
int add(int left, int right)
{
return left + right;
}
void Test6(void)
{
// 如果我想用包装器将上面的加法和减法运算包起来
std::map> cal_map;
cal_map["+"] = add;
// 你不是要三个参数的参数包吗?
// 由于你的第一个参数是固定的,就是一个Sub对象,因此我将你的这个参数给绑定
cal_map["-"] = std::bind(&Sub::sub, Sub(), std::placeholders::_1, std::placeholders::_2);
std::cout << cal_map["+"](10, 30) << std::endl; // 40
std::cout << cal_map["-"](20, 10) << std::endl; // 10
}
void Test7(void)
{
std::map> cal_map;
cal_map["+"] = [](int left, int right){return left + right; };
//由于乘法有三个参数,因此我们需要进行绑定
// 在这里我们可以用一个确定的值进行绑定
cal_map["*"] = std::bind(mul, std::placeholders::_1, std::placeholders::_2, 10);
std::cout << cal_map["+"](10, 20) << std::endl;
std::cout << cal_map["*"](3, 5) << std::endl;
}