从零开始的C++学习生活 16:C++11新特性全解析

个人主页:Yupureki-CSDN博客
C++专栏:C++_Yupureki的博客-CSDN博客
目录
前言
1. 列表初始化
std::initializer_list
2. 右值引用与移动语义
2.1 左值与右值
2.2 右值引用
2.3 引用延长生命周期
2.4 左值与右值的参数匹配
2.5 移动构造与移动赋值
2.6 类型分类
2.7 引用折叠
2.8 完美转发
3. 可变参数模板
3.1 基本语法
3.2 包扩展
4.3 empalce系列接口
4. 新的类功能
4.1 默认的移动构造和移动赋值
4.2 defult和delete
4.3 final与override
5. Lambda
5.1 捕捉列表
6. 包装器
6.1 function
6.2 bind
上一篇:从零开始的C++学习生活 15:哈希表的使用和封装unordered_map/set-CSDN博客
前言
我们之前所讲的大部分都是C++98的内容。由于语言版本迭代与编译器支持的跟进问题,并且考虑到内容的更新内容的含金量,我们对于一款编程语言新版本的学习始终保观察态度,但C++11是我们必须所学的内容。
C++11 是自 C++98 以来最重要的一次更新,它不仅引入了大量现代化特性,还极大地提升了开发效率和代码性能。从 2011 年发布至今,C++11 已成为现代 C++ 开发的基石。我将系统介绍 C++11 的核心特性,包括列表初始化、右值引用与移动语义、可变参数模板、lambda 表达式、包装器等,并附上代码示例,帮助你快速掌握这些内容。

1. 列表初始化
C++11 引入了统一的列表初始化语法,使用 {} 来初始化任何类型的对象,包括内置类型、自定义类型和容器。
在C++98中我们一般只能对结构体和数组进行{}初始化
struct Point {int x, y;
};int main() {// C++98 方式Point p1 = {1, 2};int arr1[] = {1, 2, 3};
}
但在C++11以后想统⼀初始化方式,试图实现⼀切对象皆可用{}初始化,{}初始化也叫做列表初始化。并且{}初始化的过程中,可以省略掉=
内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化 了以后变成直接构造。
int main{// C++11 方式Point p2{1, 2};int arr2[]{1, 2, 3};std::vector<int> vec = {1, 2, 3, 4, 5};
}
std::initializer_list
vector、map)支持使用 std::initializer_list 进行初始化,使得多元素初始化更加简洁
std::vector<int> v = {1, 2, 3};
std::map<std::string, int> m = {{"Alice", 1}, {"Bob", 2}};
2. 右值引用与移动语义
2.1 左值与右值
-
左值:有名称、有地址的对象(如变量)。
-
右值:临时对象、字面量等(如
10、x + y)。
左值即我们平常定义的变量等,这些变量最主要的特性就是在内存中开辟了空间,所以有地址,并且具有持久性。可以出现在=左边或者右边
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常⻅的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
cout << &c << endl;
cout << (void*)&s[0] << endl;
右值一般是字面值常量或者临时变量,因此右值一般声明周期仅存在于这一行,不存在地址。只能出现在=右边
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
10;
x + y;
fmin(x, y);
string("11111");
2.2 右值引用
左值引用不能直接引用右值,因为右值具有常属性,除非使用const修饰
右值引用不能直接引用左值,但是可以引用move(左值)的返回值
这里的move为C++库里面定义的一个返回值,其本质是强制类型转换,即把左值强转成右值
需要注意的是,例如
std::string&& s = std::string("hello");
虽然=右边的是临时对象,因此是右值,但是s本身不是右值而是左值
因此右值引用的变量只是引用的是右值而已,但其本身还是左值
2.3 引用延长生命周期
int main()
{std::string s1 = "Test";// std::string&& r1 = s1; // 错误:不能绑定到左值const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期// r2 += "Test"; // 错误:不能通过到 const 的引⽤修改std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改std::cout << r3 << '\n';return 0;
}
2.4 左值与右值的参数匹配
C++中可以重载函数。我们知道,编译器选定函数一定是选定最匹配的。
例如const对象一定只能选定传const的函数
普通左值对象可以传const和不带const的参数,但是如果二选一还是优先不带const,除非没有这种函数
右值对象可以传带const的或者右值引用的参数,但还是优先右值引用
void f(int& x)
{std::cout << "左值引⽤重载 f(" << x << ")\n";
}void f(const int& x)
{std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}void f(int&& x)
{std::cout << "右值引⽤重载 f(" << x << ")\n";
}int main()
{int i = 1;const int ci = 2;f(i); // 调⽤ f(int&)f(ci); // 调⽤ f(const int&)f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)f(std::move(i)); // 调⽤ f(int&&)int&& x = 1;f(x); // 调⽤ f(int& x)f(std::move(x)); // 调⽤ f(int&& x)return 0;
}
2.5 移动构造与移动赋值
class String {
public:String(String&& other) { // 移动构造data_ = other.data_;other.data_ = nullptr;}String& operator=(String&& other) { // 移动赋值if (this != &other) {delete[] data_;data_ = other.data_;other.data_ = nullptr;}return *this;}
}; 移动构造和移动赋值都是对于右值而言,并且都是浅拷贝,我们使用浅拷贝就相当于掠夺其资源。
因为我们传的右值一般都是临时对象,临时对象的声明周期只有这一行,我们以后不会也不能使用这个临时对象的资源,那么把他的资源抢过来也无伤大雅,还能大幅提高效率。
这就相当于遗产一样,人过世之后其财产自然对其而言也没用了,自然需要继承遗产的人
2.6 类型分类

2.7 引用折叠
C++中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错。但剋通过模板或 typedef
typedef int& lref;
typedef int&& rref; template<class T>
void f1(T& x)
{}template<class T>
void f2(T&& x)
{} //参数为左值引用时,无论传的是左值还是右值,x都引用的都是左值
template<class T>
void f1(T& x)
{}int main()
{int i = 1;f1(i);f1(1);//x引用的都是左值return 0;
} //参数为右值引用时,只有传递的是右值,x才是右值引用
template<class T>
void f2(T&& x)
{}int main()
{int i = 1;f1(i);//左值引用f1(1);//右值引用return 0;
} 2.8 完美转发
void f(int& x)
{std::cout << "左值引⽤重载 f(" << x << ")\n";
}void f(const int& x)
{std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}void f(int&& x)
{std::cout << "右值引⽤重载 f(" << x << ")\n";
}template<class T>
void f2(T&& x)//无论传的是左值还是右值,x本身永远为左值
{f(forward<T>(x))//x如果是右值引用就调用f(int&& x)函数
}int main()
{int i = 1;f1(i);//左值引用f1(1);//右值引用return 0;
}
3. 可变参数模板
C++11 支持可变参数模板,允许函数或类接受任意数量和类型的参数。
3.1 基本语法
template<typename... Args>
void print(Args... args) {// 使用 sizeof...(args) 获取参数个数std::cout << sizeof...(args) << std::endl;
} 原来的函数模板只能自定义参数的类型,但可变参数模板可以自定义参数的个。因此可以看作是模板的模板
同时参数类型既可以是左值引用也可以是右值引用
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {} template <class ...Args>
void Print(Args&&... args)
{cout << sizeof...(args) << endl;
}int main()
{double x = 2.2;Print(); // 包⾥有0个参数Print(1); // 包⾥有1个参数Print(1, string("xxxxx")); // 包⾥有2个参数Print(1.1, string("xxxxx"), x); // 包⾥有3个参数return 0;
} 3.2 包扩展
void ShowList()
{// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数cout << endl;
}template <class T, class ...Args>
void ShowList(T x, Args... args)
{cout << x << " ";// args是N个参数的参数包// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包ShowList(args...);
}// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{ShowList(args...);
}int main()
{Print();Print(1);Print(1, string("xxxxx"));Print(1, string("xxxxx"), 2.2);return 0;
} args的展开是自动展开的,是编译器干的事,我们无需插手。当展开了一个参数后,紧接着会展开第二个参数。最后没有参数时,就要调用无参的函数,也是我们制造一个无参showlist的原因
4.3 empalce系列接口
template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position,
Args&&... args); int main()
{list<pair<string,int>> lt;lt.push_back({"苹果",1});lt.emplace_back("苹果,1);
}
我们使用push_back需要先创造一个临时对象才能插入。但是emplace由于支持可变参数,我们可以直接按照顺序插入元素,跳过了临时对象创建这一过程
4. 新的类功能
4.1 默认的移动构造和移动赋值
4.2 defult和delete
class Person
{public:Person(const char* name = "", int age = 0):_name(name), _age(age){}Person(const Person& p):_name(p._name),_age(p._age){}Person(Person&& p) = default;//Person(const Person& p) = delete;
private:bit::string _name;int _age;
};
int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
} 4.3 final与override
final用于继承当中,意思为该类无法被继承
override用于修饰派生类的虚函数,强制检查是否重写基类的虚函数
这些我们在之前讲过,在此不再过多赘述
5. Lambda
[capture](parameters) -> return_type { body } auto add1 = [](int x, int y)->int {return x + y; };
cout << add1(1, 2) << endl; (int x,int y)其实就是函数的参数
->int则是函数的返回值类型,也可以不写让系统自己去推
{}里面则是函数体了
5.1 捕捉列表
[]是lambda的捕捉列表
-
[=]:值捕获 -
[&]:引用捕获 -
[a, &b]:混合捕获
int main()
{// 只能⽤当前lambda局部域和捕捉的对象和全局对象int a = 0, b = 1, c = 2, d = 3; auto func1 = [a, &b]{// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改//a++;b++;int ret = a + b;return ret;};cout << func1() << endl;// 隐式值捕捉// ⽤了哪些变量就捕捉哪些变量auto func2 = [=]{int ret = a + b + c;return ret;};cout << func2() << endl;
} 6. 包装器
6.1 function
std::function<int(int, int)> func;
func = [](int a, int b) { return a + b; };
std::cout << func(2, 3); // 输出 5 function内部的<int>即为对象的返回值,(int,int)为参数列表
并且function也能组成数组,要求就是返回值和参数都相同的可调用对象,因此和函数指针极为相似
auto func1 = [](int x, int y)->int {return x + y; };
auto func2 = [](int x, int y)->int {return x - y; };
auto func3 = [](int x, int y)->int {return x * y; };
auto func4 = [](int x, int y)->int {return x / y; };
function<int(int, int)> func[4] = { func1,func2,func3,func4 };
vector<int> v = { 5,4,3,2,1 };
sort(v.begin(), v.end(), [](int x, int y)->int {return x < y; });
for (auto& it : v)
{cout << it << " ";
}
cout << func[0](1, 2) << endl; 6.2 bind
auto add5 = std::bind(std::plus<int>(), std::placeholders::_1, 5);
std::cout << add5(10); // 输出 15 上面bind里面的plus为可调用对象,std::placeholders::_1即为plus的第一个参数,因此可以看见序号为_1,本来第二个参数为_2,但是bind用5覆盖,就意味着plus的第二个参数会自动传递5
所以add5只需要传递一个10给第一个参数
那么利用上述所学的内容,可以实现一个简易的加减乘除运算
template<class X>
class cal {
public:X plus(X x, X y){return x + y;}X del(X x, X y){return x - y;}X mul(X x, X y){return x * y;}X div(X x, X y){return x / y;}private:
};using placeholders::_1;
using placeholders::_2;
int main()
{cal<int> c;Print(c.plus(1, 2), c.del(1, 2), c.mul(1, 2), c.div(1, 2));function<int(int)> func[4] = { bind(&cal<int>::plus, &c, _1, 3),bind(&cal<int>::del, &c, _1, 3),bind(&cal<int>::mul, &c, _1, 3),bind(&cal<int>::div, &c, _1, 3) };Print(func[0](15), func[1](15), func[2](15), func[3](15));return 0;
} bind本身是一个函数模板,也是一个可调用对象,因此可以用function来组成一个数组
必须要注意的是,bind内部必须有可调用对象的所有参数,那么就会发现bind内部还多了个&c
因为类函数的第一个参数默认是传递this指针,因此bind所对应的也必须传递c的地址
