以下是C++11标准中的15个新特性的详细解释和示例代码,这些特性对于C++程序员来说具有重要意义:
C++11新规范及代码示例
- 1. 自动类型推导(Auto)
- 2. 基于范围的for循环
- 3. nullptr关键字
- 4. 初始化列表(Initializer lists)
- 5. 统一的初始化语法
- 6. Lambda表达式
- 7. 右值引用和移动语义(Rvalue References and Move Semantics)
- 8. constexpr关键字
- 9. 强类型枚举(Strongly-typed enums)
- 10. 委托构造函数(Delegating Constructors)
- 11. 模板增强
- 12. 多线程支持
- 13. 类型别名(Type Aliases)和类型推断
- 14. 线程局部存储(Thread-local storage)
- 15. 属性规范(Attributes)
1. 自动类型推导(Auto)
auto
关键字让编译器能够自动推导表达式的类型。这使得编程时不必显式指定类型,尤其在处理复杂的类型如STL容器的迭代器时特别有用。
示例代码:
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
2. 基于范围的for循环
C++11引入了基于范围的for循环,使得遍历数组和容器更加直观和简洁。
示例代码:
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {10, 20, 30, 40, 50};
for (int x : vec) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
3. nullptr关键字
nullptr
是一个类型安全的空指针字面量,它可以替代C++中传统的NULL。使用nullptr
可以减少与整数和指针类型间转换的错误。
示例代码:
#include <iostream>
void func(int) {
std::cout << "func(int) called" << std::endl;
}
void func(char*) {
std::cout << "func(char*) called" << std::endl;
}
int main() {
func(NULL); // Ambiguous call
func(nullptr); // Unambiguous, calls func(char*)
return 0;
}
4. 初始化列表(Initializer lists)
初始化列表允许使用花括号来初始化任何对象,这样使得代码更加简洁且易于维护。
示例代码:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5}; // 使用初始化列表给vector赋值
for (int i : v) {
std::cout << i << ' ';
}
std::cout << std::endl;
return 0;
}
5. 统一的初始化语法
C++11推出了一种统一的语法(使用花括号{}
),用于初始化任何类型的变量,这提高了代码的一致性和清晰度。
示例代码:
#include <iostream>
#include <vector>
struct Point {
int x, y;
};
int main() {
int a{5};
std::vector<int> v{1, 2, 3, 4, 5};
Point p{10, 20};
std::cout << "a: " << a << std::endl;
for (auto& i : v) {
std::cout << i << ' ';
}
std::cout << std::endl;
std::cout << "Point: " << p.x << ", " << p.y << std::endl;
return 0;
}
6. Lambda表达式
Lambda表达式是一种定义匿名函数对象的简洁方式。Lambda表达式允许你在需要函数对象的地方直接定义和使用它们,无需显式定义一个函数或函数对象。Lambda广泛应用于算法、事件处理、异步编程等领域。
Lambda表达式的基本语法
Lambda表达式的语法如下:
[ capture_clause ] ( parameters ) -> return_type {
function_body
}
- capture_clause: 捕获外部变量列表,可以按值捕获(
=
)或按引用捕获(&
)。=
或者&
后如果没有写具体的变量,表示按值捕获或按引用捕获所有外部作用域中可见的变量,两者后面不能同时不写变量。 - parameters: 类似于常规函数的参数列表。
- return_type: 可选的返回类型,如果省略,编译器会自动推断。
- function_body: 函数体,包含Lambda函数的代码。
示例代码:使用Lambda表达式进行排序
假设你有一个std::vector<std::pair<int, std::string>>
,其中存储的是整数和对应的字符串。现在,你需要根据整数进行排序。使用Lambda表达式可以非常简洁地完成这一任务:
#include <algorithm>
#include <iostream>
#include <vector>
#include <string>
int main() {
// 创建一个vector,存储pair
std::vector<std::pair<int, std::string>> items = {
{5, "five"}, {3, "three"}, {8, "eight"}, {1, "one"}, {7, "seven"}
};
// 使用Lambda表达式进行排序
std::sort(items.begin(), items.end(), [](const std::pair<int, std::string>& a, const std::pair<int, std::string>& b) {
return a.first < b.first;
});
// 输出排序结果
for (const auto& item : items) {
std::cout << item.first << " - " << item.second << std::endl;
}
return 0;
}
Lambda表达式的捕获方式
Lambda可以捕获外部作用域中的变量,这使得它们非常适用于编写闭包(即捕获其创建环境的函数)。你可以指定是按值捕获还是按引用捕获:
- 按值捕获 (
[=]
): Lambda表达式内部的变量是外部变量的副本,不会影响原始数据。 - 按引用捕获 (
[&]
): Lambda表达式可以直接修改外部作用域的变量。
示例代码:捕获外部变量
下面的示例显示了如何使用Lambda表达式捕获并修改外部变量:
#include <iostream>
#include <vector>
#include <functional>
int main() {
int x = 10;
int y = 20;
// 创建一个Lambda表达式,捕获x按值,y按引用
auto func = [=, &y]() mutable {
std::cout << "x = " << x << std::endl; // x是只读的
std::cout << "y = " << y << std::endl; // y可以修改
y += x; // 修改y
};
func();
std::cout << "After func, y = " << y << std::endl; // 显示修改后的y
return 0;
}
运行结果:
x = 10
y = 20
After func, y = 30
这些示例展示了Lambda表达式如何在实际的C++代码中使用,从简单的排序到复杂的变量捕获和修改。Lambda表达式的引入显著提高了C++的表达力和功能性,使得编写现代C++代码更加高效和灵活。
7. 右值引用和移动语义(Rvalue References and Move Semantics)
这两个特性对于C++的性能优化特别重要,它们使得编程语言能更有效地处理资源管理和数据传递。这两个特性的好处:
1. 减少不必要的对象复制
在引入移动语义之前,对象数据通常通过复制构造函数和赋值运算符进行传递,这涉及到完整数据的复制。对于包含大量数据的对象(如大型容器、字符串等),这种复制是非常昂贵的操作。移动语义允许这些对象的资源(如动态分配的内存)被“移动”而非复制。这意味着原始对象将变为一个有效的、但未定义的状态,这通常比创建数据的完整副本要快得多。
2. 提高性能
移动语义直接影响程序的运行效率,尤其是在涉及到大量数据操作或临时对象的场合(如函数返回大型对象时)。通过避免不必要的复制,程序运行更快,消耗更少的资源。
3. 资源安全管理
在C++中,资源(如文件句柄、网络连接、动态分配的内存)的管理是一个关键问题。使用移动语义,可以确保资源的所有权可以安全、有效地从一个对象转移到另一个对象,而不必担心资源的多次释放或泄漏。
4. 简化代码和提高可读性
移动语义让开发者可以写出更简洁和直观的代码。例如,在使用标准库如std::vector
时,移动语义允许直接传递临时对象到容器中,而不需要显式地写出复杂的拷贝和销毁操作。
示例代码
来看一个简单的例子,演示在使用移动语义之前和之后的效果对比:
#include <iostream>
#include <vector>
class LargeObject {
public:
LargeObject() {
data.resize(1000); // 模拟大对象
}
// 移动构造函数
LargeObject(LargeObject&& other) noexcept : data(std::move(other.data)) {
std::cout << "Move constructor called" << std::endl;
}
// 拷贝构造函数
LargeObject(const LargeObject& other) : data(other.data) {
std::cout << "Copy constructor called" << std::endl;
}
private:
std::vector<int> data;
};
void process(LargeObject lo) {
// 处理对象
}
int main() {
LargeObject obj;
process(std::move(obj)); // 使用移动语义
return 0;
}
这个例子中,process
函数通过移动语义接收一个LargeObject
。当在main
函数中以std::move(obj)
调用process
时,将触发移动构造函数而不是拷贝构造函数。这避免了数据的完整复制,大大提高了效率。
8. constexpr关键字
constexpr
关键字用于定义可以在编译时求值的常量表达式。这对于性能优化非常重要,因为使用constexpr
可以减少运行时的计算量。适用于函数、构造函数及其他表达式。
示例代码:
#include <iostream>
constexpr int factorial(int n) {
return n <= 1 ? 1 : (n * factorial(n - 1));
}
int main() {
constexpr int val = factorial(5); // 编译时计算
std::cout << "5! = " << val << std::endl;
return 0;
}
9. 强类型枚举(Strongly-typed enums)
C++11引入了新的枚举类,通过enum class
关键字定义。这些枚举类型是强类型的,不会隐式转换到整型,也不会和其他枚举类型混淆,增强了类型安全。
示例代码:
#include <iostream>
enum class Color { Red, Green, Blue };
int main() {
Color color = Color::Red;
if (color == Color::Red) {
std::cout << "Color is Red" << std::endl;
} else {
std::cout << "Color is not Red" << std::endl;
}
return 0;
}
10. 委托构造函数(Delegating Constructors)
C++11允许构造函数从同一个类的其他构造函数中委托调用,这减少了代码的冗余,使构造函数的初始化更加清晰。
示例代码:
#include <iostream>
#include <string>
class MyClass {
public:
MyClass() : MyClass("Default Name") {
std::cout << "Delegate constructor called." << std::endl;
}
MyClass(std::string name) : name_(name) {
std::cout << "Primary constructor called with name = " << name_ << std::endl;
}
private:
std::string name_;
};
int main() {
MyClass obj; // 会首先调用带有std::string参数的构造函数
return 0;
}
运行结果:
Primary constructor called with name = Default Name
Delegate constructor called.
11. 模板增强
C++11对模板进行了多项增强,包括外部模板和变参模板,使模板更加强大且灵活。
变参模板
变参模板允许模板接受任意数量和类型的参数,这在以前的标准中是无法做到的。变参模板极大地增强了模板的灵活性和功能,尤其是在需要根据不定数量的参数构建类或函数时。
示例代码(变参模板):
#include <iostream>
template<typename T>
void print(T arg) {
std::cout << arg << std::endl;
}
template<typename T, typename... Args>
void print(T firstArg, Args... args) {
std::cout << firstArg << ", ";
print(args...); // 递归调用
}
int main() {
print(7, "Hello", 3.14, "World");
return 0;
}
外部模板(Explicit Template Instantiation)是C++11中引入的一个特性,允许程序员显式地指示编译器在一个特定的位置实例化模板,而不是让编译器自动在首次使用时进行实例化。这可以帮助控制模板实例化的编译时间和链接时间,尤其是在模板代码较多或编译多个文件时非常有用。
外部模板
外部模板(Explicit Template Instantiation)在C++11之前的版本中其实已经存在,但C++11和之后的版本中加强了这一特性,并使其在现代C++编程中更为重要。这里我们讨论一下外部模板在C++11中的使用以及相比早期C++版本的区别和优点。
早期C++中的模板实例化
在C++11之前,模板的实例化主要是自动进行的。当你在代码中使用一个模板类或模板函数时,编译器会在编译时自动为所使用的具体类型生成模板实例。这种方式称为隐式模板实例化。隐式实例化有以下特点:
- 编译器控制:完全由编译器控制,开发者无法指定实例化的具体位置。
- 代码膨胀:如果在多个源文件中使用相同的模板类型,每个源文件可能都会生成一份实例化代码,增加最终可执行文件的大小。
- 编译时间:在多个源文件中使用相同的模板类型会导致每个文件都需要重新实例化模板,这增加了编译时间。
C++11中的外部模板
尽管外部模板在C++11之前就已存在,但C++11对此进行了更多的推广和标准化,鼓励开发者使用显式实例化来优化代码和编译过程。外部模板的显式实例化具有以下优势:
- 编译时间优化:通过在项目的一个源文件中显式地实例化模板,可以避免在多个源文件中重复实例化,从而减少编译时间。
- 减少代码膨胀:显式实例化确保模板类型只生成一次代码,减少了生成的二进制大小。
- 更好的控制:开发者可以精确控制模板的实例化位置和时间,这对于大型项目的构建时间和最终可执行文件大小优化非常重要。
示例代码(变参模板):
文件1: my_template.h
#pragma once
// 定义一个模板类
template <typename T>
class MyTemplate {
public:
void doSomething(T value) {
// 实现略
}
};
// 定义一个模板函数
template <typename T>
void myFunction(T value) {
// 实现略
}
文件2: explicit_instantiation.cpp
#include "my_template.h"
// 显式实例化模板类
template class MyTemplate<int>;
// 显式实例化模板函数
template void myFunction<double>(double);
文件3: main.cpp
#include "my_template.h"
int main() {
MyTemplate<int> myObj;
myObj.doSomething(123); // 使用显式实例化的模板类
myFunction<double>(456.789); // 使用显式实例化的模板函数
return 0;
}
12. 多线程支持
C++11在标准库中引入了多线程支持,包括线程、互斥锁和条件变量等。
示例代码:
#include <iostream>
#include <thread>
#include <vector>
void task(int n) {
std::cout << "Running task " << n << std::endl;
}
int main() {
std::vector<std::thread> threads;
for (int i = 1; i <= 5; ++i) {
threads.emplace_back(task, i);
}
for (auto& t : threads) {
t.join();
}
return 0;
}
13. 类型别名(Type Aliases)和类型推断
C++11通过using
关键字提供了一种新的类型别名语法,与typedef
相比,using
的语法更为直观和一致,特别是在模板别名的情况下。
示例代码:
#include <iostream>
#include <vector>
#include <map>
template<typename T>
using Vec = std::vector<T>;
int main() {
Vec<int> vec
= {1, 2, 3, 4, 5};
for (auto v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
return 0;
}
14. 线程局部存储(Thread-local storage)
thread_local
关键字用于声明线程局部变量,每个线程有自己的变量实例。
示例代码:
#include <iostream>
#include <thread>
thread_local int n = 1;
void printThreadID(int id) {
n += id;
std::cout << "Thread " << id << " has n = " << n << std::endl;
}
int main() {
std::thread t1(printThreadID, 1);
std::thread t2(printThreadID, 2);
t1.join();
t2.join();
return 0;
}
运行结果:
Thread 1 has n = 2
Thread 2 has n = 3
15. 属性规范(Attributes)
C++11引入了属性规范,通过双方括号语法提供更多编译器级别的优化和检查。
示例代码:
#include <iostream>
[[noreturn]] void fail() { // fail函数被标注为[[noreturn]],并在其函数体内抛出一个异常。这表明fail函数不会正常返回,它将通过抛出异常来结束执行。
throw std::runtime_error("Failed!");
}
int main() {
try {
fail();
} catch (const std::exception& e) {
std::cout << e.what() << std::endl;
}
return 0;
}
这些新特性让C++程序更加高效,易于编写和维护,同时也为复杂的编程任务提供了强大的语言工具。