目录
前言
一、统一的列表初始化
1、{}初始化
2、std::initializer_list
二、新的声明
1、auto
2、decltype
3、nullptr
三、范围for循环
四、右值引用与移动语义
1. 左值 vs 右值
2、移动构造与移动赋值
3、 move转换
4、完美转发:forward
五、lambda表达式
六、包装器
1、function包装器
2、bind
总结
前言
在2003年,C++标准委员会发布了一份技术勘误表(简称TC1),使得C++03取代了C++98成为C++标准的最新版本。然而,C++03(TC1)主要集中于修复C++98中的缺陷,而语言的核心部分并没有重大变化。因此,人们通常将这两个标准统称为C++98/03。
C++11,这个被称为“C++0x”的标准,经过十年的发展终于正式发布。与C++98/03相比,C++11带来了许多显著的变化和改进。它包含了约140个新特性,并修正了C++03标准中的约600个缺陷,使得C++11不仅仅是C++98/03的延续,而是一个真正意义上的新标准。
C++11的核心优势在于:
- 语言功能的增强:新特性使得C++11在系统开发和库开发中表现更加出色。
- 语法的简化和泛化:C++11引入了自动类型推导、范围-based for 循环、
nullptr
等,使得代码更加简洁和易于维护。 - 性能和安全性提升:移动语义和右值引用减少了不必要的拷贝,提高了性能,新的线程库增强了多线程编程的安全性和效率。
在实际开发中,C++11的引入不仅丰富了语言的功能,而且极大地提升了程序员的开发效率。虽然C++11的特性非常广泛,本篇文章将重点讲解一些实际中最为实用的语法和功能,以帮助更好地掌握这一现代C++标准。
一、统一的列表初始化
1、{}初始化
在C++98版本,我们可以使用{}来对数组和结构体元素来进行统一的列表初始值设定,例如:
//在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
struct Point
{
int x;
int y;
};
int main()
{
int arr[] = { 1,2,3,4,5 };
int arr2[5] = { 1 };
Point p = { 1,2 };
return 0;
}
但是在C++11版本中,其扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
struct Point
{
int x;
int y;
};
int main()
{
int x1 = 1;
int x2{ 2 };
int arr1[]{ 1,2,3,4,5 };
int arr2[5]{ 1 };
Point p{ 1,2 };
//甚至列表初始化也能运用到new表达式中
int* pa = new int[4] {0};
return 0;
}
就连自定义类型创建对象时,也可以通过列表初始化的方式调用构造函数初始化。
2、std::initializer_list
std::initializer_list
是 C++11 引入的一种用于支持列表初始化的类型。它可以让你方便地用一组元素来初始化容器、对象或自定义类型。std::initializer_list
的主要用途是提供一种简便的语法,用于将一系列值传递给函数、构造函数或其他结构。
void foo(std::initializer_list<int> values)
{
for (auto value : values)
{
std::cout << value << " ";
}
}
int main()
{
foo({ 1,2,3,4,5 });
return 0;
}
std::initializer_list通常有以下三种运用方法:
-
构造函数: 允许类通过
initializer_list
来接收多个初始化参数:class MyClass { public: MyClass(std::initializer_list<int> list) { for (auto elem : list) { std::cout << elem << " "; } std::cout << std::endl; } }; MyClass obj = {1, 2, 3, 4}; //用initializer_list 来接收一个初始化列表,并打印它的内容。
-
容器的初始化: C++ STL 容器(如
std::vector
,std::set
)也可以使用initializer_list
来进行初始化:std::vector<int> vec = {1, 2, 3, 4, 5}; std::set<int> s = {10, 20, 30};
-
函数参数: 可以通过
initializer_list
传递不定数量的参数:void sum(std::initializer_list<int> list) { int result = 0; for (auto elem : list) { result += elem; } std::cout << "Sum: " << result << std::endl; } sum({1, 2, 3, 4}); //输出结果为10
二、新的声明
1、auto
#include<map>
#include<string>
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}
2、decltype
int main()
{
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
auto it = dict.begin();
vector<decltype(it)>arr;
return 0;
}
3、nullptr
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
三、范围for循环
C++11 引入了范围 for 循环,这是一个简洁、方便的语法,用于遍历容器或数组中的元素。相比传统的 for
循环,范围 for
循环无需显式管理索引,代码更加简洁明了。
for (declaration : container)
{
// 循环体
}
declaration
是一个变量,用来表示当前循环迭代的元素,可以使用自动类型推导auto
。container
是一个支持迭代的容器,比如数组、std::vector
、std::map
、std::set
等。
当我们不需要改变值时就可以使用按值传递,需要改变值时就按引用传递:
int main()
{
int arr[] = {1, 2, 3, 4, 5};
for (int x : arr)
{
x *= 2; // 修改的是副本,不会影响原数组
}
for (int x : arr)
{
cout << x << " "; // 输出依然是 1 2 3 4 5
}
for (int& x : arr)
{
x *= 2; // 修改的是原数组
}
for (int x : arr)
{
cout << x << " "; // 输出 2 4 6 8 10
}
return 0;
}
四、右值引用与移动语义
C++11 引入了右值引用(Rvalue References)和移动语义(Move Semantics),它们大大提高了程序的效率,特别是在涉及大对象或资源管理时。右值引用允许我们利用临时对象的资源,而移动语义则通过转移资源所有权来避免不必要的拷贝。
1. 左值 vs 右值
-
左值(Lvalue):左值是一个表达式,它指向内存中的一个固定地址,并且可以出现在赋值操作的左侧。左值通常指的是变量的名字,它们在程序的整个运行期间都存在。左值的判断标准:可以取地址、有名字的就是左值。
// 以下的p、b、c、*p都是左值 int* p = new int(0); int b = 1; const int c = 2; // 以下几个是对上面左值的左值引用 int*& rp = p; int& rb = b; const int& rc = c; int& pvalue = *p;
-
右值(Rvalue):右值是一个临时的、不可重复使用的表达式,它不能出现在赋值操作的左侧。右值通常包括字面量、临时生成的对象以及即将被销毁的对象。右值的判断标准:不可以取地址、没有名字的就是右值。
// 以下几个都是常见的右值 10; x + y; fmin(x, y); // 以下几个都是对右值的右值引用 int&& rr1 = 10; double&& rr2 = x + y; double&& rr3 = fmin(x, y);
2、左值引用与右值引用的比较
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
int main()
{
int&& r1 = 10;
int a = 10;
int&& r2 = a;//会报错,无法将左值绑定到右值引用
// 右值引用可以引用move以后的左值
int&& r3 = move(a);
return 0;
}
2、移动构造与移动赋值
class string
{
public:
string(const char* s = "")//默认构造
:size(strlen(s))
, capacity(size)
{
str = new char[capacity + 1];
strcpy(str, s);
}
void swap(string& s)
{
std::swap(str, s.str);
std::swap(size, s.size);
std::swap(capacity, s.capacity);
}
string(const string& s)//拷贝构造
:str(nullptr)
{
cout << "拷贝构造" << endl;
string tmp(s.str);
swap(tmp);
}
string& operator=(const string& s)
{
cout << "赋值重载" << endl;
string tmp(s);
swap(tmp);
}
string(string&& s)
:str(nullptr)
,size(0)
,capacity(0)
{
cout << "移动构造" << endl;
swap(s);
}
string& operator=(string&& s)
{
cout << "移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
delete[]str;
str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < size);
return str[pos];
}
void reserve(size_t n)
{
if (n > capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, str);
delete[] str;
str = tmp;
capacity = n;
}
}
void push_back(char ch)
{
if (size >= capacity)
{
size_t newcapacity = capacity == 0 ? 4 : capacity * 2;
reserve(newcapacity);
}
str[size] = ch;
++size;
str[size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return str;
}
private:
char* str;
size_t size;
size_t capacity;//不包含最后做标识的\0
};
我们这里实现了一个简单的string类
void func1(bit::string s)
{}
void func2(const bit::string& s)
{}
int main()
{
bit::string s1("hello world");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
func1(s1);
func2(s1);
// string operator+=(char ch) 传值返回存在深拷贝
// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
s1 += '!';
return 0;
}
先屏蔽掉两个移动构造函数我们可以发现,左值引用明显减少了我们资源不必要的拷贝,但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回。例如:test::string to_string(int value)函数中可以看到,这里只能使用传值返回, 传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
所以此时右值引用的移动构造与移动赋值就出现帮我们解决这个问题了。
3、 move转换
int main()
{
test::string s1("hello world");
// 这里s1是左值,调用的是拷贝构造
test::string s2(s1);
// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
// 资源被转移给了s3,s1被置空了。
test::string s3(std::move(s1));
return 0;
}
4、完美转发:forward
void Fun(int &x)
{
cout << "左值引用" << endl;
}
void Fun(const int &x)
{
cout << "const 左值引用" << endl;
}
void Fun(int &&x)
{
cout << "右值引用" << endl;
}
void Fun(const int &&x)
{
cout << "const 右值引用" << endl;
}
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
五、lambda表达式
在C++11之前,倘若我们要排序一个自定义类型的数组,就需要手动写一个算法。随着C++算法的发展,人们开始觉得这样做实在是太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名, 这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
对于一个水果自定义类型来说:
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
我们可以这样写lambda:
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._evaluate > g2._evaluate; });
}
int main()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[]{};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=]{return a + 3; };
// 省略了返回值类型,无返回值类型
auto fun1 = [&](int c){b = a + c; };
fun1(10)
cout<<a<<" "<<b<<endl;
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int{return b += a+ c; };
cout<<fun2(10)<<endl;
// 复制捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;
return 0;
}
六、包装器
1、function包装器
C++ 提供了多种可调用对象,例如:
- 普通的函数(函数指针)
- 函数对象(重载了
operator()
的类或结构体) - Lambda 表达式
- 成员函数指针
不同类型的可调用对象在不同场景下可能有不同的语法和接口,这使得在通用库设计中难以统一处理。std::function
出现的主要动机之一是提供一个统一的接口来包装这些可调用对象,使得用户无需关心它们具体的类型。
同时在设计某些库(如算法库、回调机制)时,通常需要接受函数作为参数。例如,STL 中的算法如 std::sort
可以接受一个比较函数。std::function
可以作为这样的通用接口,使得程序设计更加抽象和模块化,库的使用者可以自由地传入不同类型的可调用对象,而库的实现者只需针对 std::function
进行设计。
#include <iostream>
#include <functional>
int add(int a, int b)
{
return a + b;
}
int main() {
std::function<int(int, int)> func = add;//std::function<int(int)> 可以存储任何接受一个 int 并返回 int 的可调用对象
std::cout << func(2, 3) << std::endl; // 输出 5
return 0;
}
2、bind
// 使用举例
#include <functional>
int Plus(int a, int b)
{
return a + b;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,
placeholders::_2);
//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
//func2的类型为 function<void(int, int, int)> 与func1类型一样
//表示绑定函数 plus 的第一,二为: 1, 2
auto func2 = std::bind(Plus, 1, 2);
cout << func1(1, 2) << endl;
cout << func2() << endl;
Sub s;
// 绑定成员函数
std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,
placeholders::_1, placeholders::_2);
//参数调换顺序
std::function<int(int, int)> func4 = std::bind(&Sub::sub, s,
placeholders::_2, placeholders::_1);
cout << func3(1, 2) << endl;
cout << func4(1, 2) << endl;
return 0;
}
总结
C++11标准引入了众多新特性,显著增强了C++语言的功能和性能。主要更新包括:
-
统一的列表初始化:通过大括号
{}
可以统一对内置类型和用户自定义类型进行初始化,同时std::initializer_list
简化了多个参数的初始化过程。 -
新的声明方式:
auto
:实现自动类型推导,简化代码。decltype
:用于获取表达式的类型,常与auto
配合使用。nullptr
:引入了空指针nullptr
,替代原先的NULL
。
-
范围for循环:简化了容器和数组的遍历过程,代码更加简洁。
-
右值引用与移动语义:通过右值引用和移动语义优化了内存管理,特别是处理临时对象时,避免了不必要的深拷贝操作,提升了程序的效率。
-
Lambda表达式:引入了匿名函数,使得可以在函数内部快速定义和使用简单函数。
-
其他改进:包括多线程支持、
std::tuple
、std::function
等,使得C++11不仅在语法上变得简洁,同时在性能、可维护性、并发编程等方面有了显著提升。(部分重要改动本文并未介绍,例如线程要配合linux的线程知识)