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

C++专栏:C++_Yupureki的博客-CSDN博客
目录
上一篇:从零开始的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的地址
更多推荐


所有评论(0)