C++11重要的新特性
自动类型推导:auto、decltype
C++11中新增了自动类型推导的功能,包括auto和decltype两种方式。它可以减少代码中的重复定义,使代码更简洁。
auto
auto可以根据初始化表达式自动推导变量类型。例如:1
2
3
4auto x = 1; // x被推导为int
auto y = 1.0; // y被推导为double
std::vector<int> vec{1,2,3};
auto iter = vec.begin(); // iter被推导为vector<int>::iteratorauto在需要定义长类型名的场景下非常有用。
decltype
decltype可以获取一个表达式的类型。例如:1
2
3
4
5int x = 1;
decltype(x) y = 2; // y的类型与x相同,为int
std::vector<int> vec{1,2,3};
decltype(vec)::iterator iter = vec.begin(); // iter的类型为vector<int>::iteratordecltype对于获取模板表达式的结果类型也很有用。
总结:
- auto用于变量类型推导,减少重复代码
- decltype用于表达式类型获取,可处理模板表达式
合理利用auto和decltype可以简化代码,消除类型重复定义。但也需要避免过度使用,降低代码可读性。
统一初始化语法:{}
C++11在初始化方面做了统一和增强,使用{}可以统一多种初始化方式。
主要特点:
将构造函数初始化和均值初始化统一起来:
1
2
3
4
5int x(1); //调用构造函数初始化
int y{1}; //也是调用构造函数初始化
int x = 1; //均值初始化
int y = {1}; //现在也可以用{}均值初始化列表初始化可以避免窄化转换:
1
2int x = {1.1}; //错误,double到int有窄化转换
int y{1.1}; //正确,由编译器选择匹配的构造函数强制指定初始化方式:
1
2vector<int> v(10, 1); //调用构造函数初始化
vector<int> v{10, 1}; //调用重复元素初始化初始化类对象:
1
2
3class A {...};
A a(1, 2); //调用构造函数
A b{1, 2}; //也是调用构造函数初始化数组:
1
2int arr[3] = {1, 2, 3};
int arr[3]{1, 2, 3}; //现在也可以使用{}初始化数组
总结:{}可以统一许多初始化方式,使代码更简洁,也能避免一些问题。
Lambda表达式和函数对象包装器(如std::function)
C++11中引入了lambda表达式和std::function,可以方便地定义和使用函数对象。
Lambda表达式
Lambda表达式可以直接在代码中定义匿名函数,例如:1
2
3auto add = [](int x, int y) {
return x + y;
};自动根据参数和返回值类型推导出函数签名。Lambda可捕获外部变量,支持多种形式。
std::function
std::function是一个通用的函数对象包装器,可以包装函数指针、Lambda表达式等为统一的函数对象。
例如:1
2
3
4
5std::function<int(int,int)> func = [](int x, int y) {
return x + y;
};
int result = func(1, 2);std::function提供统一的函数调用接口,可以对任意可调用对象进行存储、复制和调用操作。
两者结合使用可以实现类似策略模式的效果:
1 | std::vector<std::function<void()>> handlers; |
这里是一个完整的示例
1 |
|
编译与运行
1 | 编译 |
上面代码定义了一个普通函数add,然后用函数指针和lambda表达式分别配合std::function进行了包装,使其可以通过统一函数调用的方式使用。
这个示例演示了std::function可以包装函数指针和lambda表达式,作为函数对象使用的方法。
总结:
- Lambda方便定义匿名函数
- std::function提供函数对象的统一封装和调用方式
- 两者结合可以实现函数的存储与延迟执行
强类型枚举类enum class
C++11在enum的基础上新增了enum class,它被称为强类型枚举(Scoped Enumeration)。
相比普通enum,enum class有以下特点:
enum class的枚举值需要进行显式转换才能进行运算。
1
2
3
4
5
6
7enum class Color { RED, GREEN, BLUE};
Color c = Color::RED;
if (c == Color::RED) { // 需要比较相同类型
}
int i = (int)c; // 需要显式转换才能进行算术运算强类型enum不会将枚举值泄露到外部作用域。
1
2enum class Secret { CODE1, CODE2, CODE3 };
int i = CODE1; // 错误,CODE1未定义使用强类型enum可以避免不同enum命名冲突。
1
2enum class Fruit { Apple, Banana };
enum class Color { Apple, Orange }; // 不会冲突
总结:
- 使用enum class定义强类型enum
- 需要进行显式转换才能混合运算
- 避免命名冲突,增强封装
enum class总体上比普通enum更安全,适合对枚举进行更严格的控制。
nullptr和强制类型转换操作
C++11中nullptr和强制类型转换操作的主要新特性如下:
nullptr
引入nullptr关键字
C++11引入nullptr关键字来表示空指针,以代替过去的0或NULL。
nullptr是指针类型,不能隐式转换为整数,避免了空指针与0的混淆。
nullptr避免了整数0与空指针的混淆。0可以隐式转换为指针,但nullptr不行。
nullptr是指针的专属空值,而强制类型转换如(int)nullptr会产生未定义行为。
nullptr可以隐式转换为任何指针类型,如int* p = nullptr; 不需要强制转换。
用nullptr初始化智能指针可以正确构造空值,而使用0可能会导致编译错误。nullptr支持指针类型的重载
nullptr可以用于指针类型的函数重载,而0不行。1
2
3
4
5void foo(int) {..}
void foo(char*) {..}
foo(0); // 调用foo(int)
foo(nullptr); // 调用foo(char*)nullptr的类型推导
在模板类型推导中,nullptr推导为std::nullptr_t类型,而不是整数类型。1
2
3
4
5template <typename T>
void foo(T t);
foo(0); // T被推导为int
foo(nullptr); // T被推导为std::nullptr_t
总结:
- nullptr是指针专属的空值标记
- 避免了整数0与空指针的混淆
- 不能进行数值运算,只能转换为指针类型
强制类型转换
强制类型转换的新语法
C++11使用括号替代老的func风格强制转换,如:(int)x。强制转换为bool不再是隐式转换
要将其他类型转换为bool,必须使用直接初始化或静态_cast。强制转换为bool会产生编译警告
编译器会针对其他整数或指针类型强制转换为bool产生警告。强制类型转换需遵循const转换规则
转换结果类型必须遵循const转换兼容规则。
总结起来,C++11简化和规范了nullptr及强制类型转换的使用,使其更安全和一致。这是现代C++的重要改进。
标准线程库
C++11提供了标准线程库,包含在
std::thread - 代表一个执行线程的对象。可以通过构造函数和thread对象启动新线程:
1
2
3
4
5
6void foo() {
// 线程执行函数
}
std::thread t(foo); // 创建新线程
t.join(); // 等待线程结束std::async - 用来异步执行函数,返回一个std::future对象。
1
2
3auto result = std::async(foo);
// do something else
result.get(); // 获取结果std::mutex - 互斥量,用于保护共享数据的互斥访问。
std::lock_guard - 栈封装的锁,自动加锁和解锁。
std::promise/std::future - 提供设置和获取异步运算结果的机制。
std::condition_variable - 条件变量,用于线程等待和通知。
通过组合使用上述组件,可以方便地实现多线程程序,例如使用mutex保护共享数据,用condition_variable实现线程同步等。
整个线程库提供了较底层的线程控制功能,可以建立在OS原生线程之上,跨平台使用。
这里是一个使用std::promise和std::future的示例代码:
1 |
|
编译和运行
1 | 编译 |
在这个例子中,我们在一个线程中进行实际的工作并计算结果。然后通过std::promise对象进行通信,将结果传递给std::future对象。
主线程通过future来获取结果。这样就实现了异步运算与结果获取。
std::promise/future提供了一个强大的多线程同步和通信机制,是C++11多线程编程中的重要工具。
容器增强:unordered_map等哈希表
下面是一个使用C++11中unordered_map的示例代码:
1 |
|
编译与输出:
1 | 编译 |
unordered_map是一个无序的哈希表,它可以快速进行插入、删除和查找操作。
它常用于如下场景:
- 需要快速判断某个键是否存在时,可以使用unordered_map的count()方法判断,效率很高。
- 需要快速根据键查找值时,unordered_map的[]操作符可以快速取得值。
- 需要构建键值映射关系时,unordered_map的键值对存储方式非常适合构建映射表。
- 需要对数据进行计数统计时,可以将值作为计数器,利用unordered_map快速增减计数。
- 需要对大量数据进行去重时,可以将元素作为键放入unordered_map,即可高效地进行去重。
- 需要高效地对元素进行分类时,可以将分类作为键,将元素放入map中进行分类统计。
总之,unordered_map是一个非常有用和高效的哈希表容器,在很多场景下都可以提供很好的性能。正确使用它可以大大优化程序的性能。
c++11有哪些容器增强了
C++11对标准库容器做了很多增强,主要新增和优化了以下几个方面:
无序容器:新增了unordered_map、unordered_set、unordered_multiset、unordered_multimap等无序哈希表容器,提供了基于哈希表的高效插入和查询。
移动语义:大多数容器现在支持移动语义,可以更高效地移动对象而非拷贝对象。
初始化列表:支持直接用初始化列表初始化容器,如std::vector
v{1,2,3}。 容器遍历:新增范围for循环语法,可以简化容器遍历,如for(auto& x : v)。
容器操作:一些容器增加了空间预留、容器合并、拷贝操作等新方法。
容器性能:优化了很多标准容器的性能,如vector随机访问性能提升了。
智能指针:新增智能指针shared_ptr、unique_ptr等,可以安全地管理堆内存。
元组:新增tuple可以存储不同类型的元素。
数组:新增数组容器array等。
正则表达式:新增正则表达式库
。
总之,C++11对标准库容器进行了全方位的增强和优化,使用新特性可以大大提高代码效率和质量。正确运用这些容器特性是编写高质量C++11代码的关键。
元组:std::tuple
std::tuple是C++11新增的一个模板类,它允许你存储不同类型的数据。
下面给出std::tuple的详细介绍:
定义Tuple
使用模板参数定义tuple,参数类型可以不同:
1 | std::tuple<int, std::string, float> myTuple; |
初始化Tuple
可以通过构造函数初始化tuple:
1 | std::tuple<int, std::string, float> myTuple(1, "hello", 3.14f); |
也可以通过make_tuple函数初始化:
1 | auto myTuple = std::make_tuple(1, "hello", 3.14f); |
访问Tuple元素
可以通过std::get来访问元素:
1 | int x = std::get<0>(myTuple); // x = 1 |
也可以用tie函数拆包Tuple:
1 | int a; std::string b; float c; |
Tuple使用场景
- 返回多个值时,可以用Tuple打包返回,避免构造结构体。
- 需要把多个值组合成一个整体传递时,可以用Tuple组织。
- 同时遍历多个容器时,可以把多个容器打包为Tuple处理。
- 需要函数模板支持不同参数类型时,可以用Tupleuniform传入不同参数。
总之,Tuple是一个非常有用的工具类,可以方便地将多个值组合为一个整体。在C++编程中有很多用途。
长长整数类型long long
long long类型其实不是C++11新增的,它在之前的C++标准里就已经存在了。
具体来说,long long类型是在C++98标准里就引入的,用于支持比long更大的整数类型。后来也被C11所采用。
但是C++11标准对long long类型的支持做了增强和明确:
- 提供了ll或LL后缀表示long long常量
- 明确规定了long long的最小大小(不小于64bit)
- 提供了与long long相关的库函数,如std::llround等
所以可以说C++11标准增强和扩展了long long的功能,但并不是最初引入这个类型。
这里是一个使用C++11标准long long类型的简单示例
1 |
|
编译和输出
1 | 编译 |
在C++11下,直接使用long long并以LL表示即可。
编译时需要加上-std=c++11参数,启用C++11支持。
当然,实际使用时需要注意long long可能溢出的范围限制。
对象构造增强:委托构造、继承构造
C++11中增加了两种对象构造的新机制:
委托构造(Delegating Constructor)
在一个构造函数中调用另一个构造函数,减少冗余代码。1
2
3
4
5
6
7
8class Foo {
public:
Foo(int a) {
// ...
}
Foo(double b) : Foo(int(b)) { } // 委托其他构造函数
};继承构造(Inheriting Constructor)
使用基类的构造函数初始化派生类。1
2
3
4
5
6
7
8struct Base {
Base(int);
Base(double);
};
struct Derived : Base {
using Base::Base; // 继承所有基类构造函数
}
一个完整的示例代码:
1 | #include <iostream> |
编译及输出结果:
1 | 编译 |
使用using就可以不用重新定义构造函数。
这两个特性可以简化对象构造代码,提高复用性。
委托构造减少重复逻辑,继承构造简化初始化。
内联命名空间
内联命名空间(inline namespace)是C++11新增的命名空间特性,其主要功能和使用方式如下:
- 内联命名空间的成员可以不需要命名空间限定就直接使用
- 同一编译单元内,内联命名空间等同于直接在外层命名空间声明
举个例子:
1 | namespace A { |
这使得内联命名空间B里的成员可以直接当做A空间的成员那样使用。
主要作用:
- 版本化,可以兼容旧代码
- 减少命名空间污染
- 提高可读性
内联命名空间需要同一编译单元内才生效。跨编译单元无法内联。
适当使用内联命名空间可以简化代码编写,改善命名空间使用体验。
内联命名空间必须要加inline关键字吗?
不必一定要使用inline关键字声明内联命名空间。
C++11在命名空间的声明中允许省略inline关键字:
也就是说下面两种声明是等价的:
1 | namespace A { |
所以inline关键字对于内联命名空间的行为没有实际影响,只是增加了语义描述,表示该命名空间为内联的。
编译器会将命名空间内的成员当作外层命名空间成员那样对待,这才是内联命名空间的实质。
所以inline关键字是可选的,不影响内联性质。但建议保留inline以提高代码可读性。
基于范围的for循环
基于范围的for循环(range-based for loop)是C++11新增的一种简化的遍历容器/数组的语法。
主要的形式是:
1 | for (元素声明 : 容器) { |
这会遍历容器中的每个元素,并自动获取元素。
相比传统的基于下标遍历,这种写法更简洁清晰。
例如遍历一个vector:
1 | std::vector<int> vec = {1,2,3}; |
范围for循环可用于数组、容器等提供开始/结束迭代器的类型。
需要注意它只遍历,不能用于修改容器中的元素。
override和final关键字
override 和 final 是C++11新加入的两个非常有用的关键字,其含义和用法如下:
override:
- 用于函数后,表示该函数覆盖(重写)了基类的虚函数
- 会检查基类是否存在此虚函数,避免重载错写成重写
例如:
1 | struct Base { |
final:
- 用于类后,表示该类不能被继承,防止继续扩展
- 用于函数后,表示该函数是最后的覆盖版本,禁止子类重写
final关键字在C++中可以用于类和函数,主要使用场景有:
防止类被继承时,将类声明为final。
1
2
3
4
5class Base final {
//...
};
class Derived : Base // 错误,Base不能被继承防止基类虚函数被override时,将虚函数声明为final。
1
2
3
4
5
6
7class Base {
virtual void foo() final;
};
class Derived : Base {
void foo(); // 错误,foo不能被重写
};将一个重写的虚函数声明为final版本。
1
2
3class Derived : Base {
virtual void foo() final; // foo的最终重写版本
};
所以当不希望类再被继承,或者一个虚函数重写到最后不希望继续override时,可以使用final关键字进行控制。
这提高了程序的稳定性和安全性。final带来限制的同时,也明确了设计意图。
override和final为继承与重写提供了更严格的控制,使多态性更加可靠。良好使用可以增强程序稳定性。
右值引用和移动语义
右值引用和移动语义是C++11新增的两个重要特性,可以更高效地转移资源。
右值引用
右值引用:
- 使用&&语法,允许绑定到临时对象和将要销毁的对象
- 使临时对象可以直接传入函数内部而不需要复制
1 | void process(vector<int>&& v) { |
转移语义和交换函数
右值引用实现移动语义,避免复制开销1
2
3
4
5std::swap(T& a, T& b) {
T tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}转发函数参数
通过模板参数转发,保留参数左右值属性:1
2
3
4template <typename T>
void foo(T&& t) {
func(std::forward<T>(t));
}完美转发
配合std::forward可以实现完美转发:1
2
3
4template <typename T>
void wrapper(T&& arg) {
foo(std::forward<T>(arg));
}右值引用返回值优化
函数返回右值引用避免复制:1
2
3
4Matrix&& factory() {
Matrix m;
return std::move(m);
}
这些都是右值引用的典型应用场景,可以提高性能和效率。
移动语义
移动语义:
- 通过重载移动构造函数和移动赋值运算符实现
- 将右值对象的资源直接转移,避免深拷贝
1 | class String { |
移动构造函数
直接将右值对象资源转移过来,避免深拷贝:1
2
3
4
5
6class String {
public:
String(String&& str) : data(str.data) {
str.data = nullptr;
}
};移动赋值运算符
释放左边资源,直接转移右边资源:1
2
3
4
5
6
7
8
9
10
11class String {
public:
String& operator=(String&& str) {
if (this != &str) {
free(data);
data = str.data;
str.data = nullptr;
}
return *this;
}
};对容器使用std::move
强制使用移动语义插入容器:1
2std::vector<String> v;
v.push_back(std::move(s));对返回值使用std::move
让返回值成为右值:1
2
3
4String foo() {
String s;
return std::move(s);
}移动语义可以大幅优化资源管理,减少拷贝和内存分配,提高效率。
右值引用让临时对象可以高效利用,移动语义转移对象所有权,减少复制开销。
Modern C++中会广泛使用它们实现性能优化。