C++新版本特性
C++新特性
1、C++11 中的新特性
C++11 引入了许多新特性,包括自动类型推导、lambda 表达式、右值引用等。下面介绍其中的一些重要特性。
1.1 自动类型推导(Type Inference)
C++11 中引入了 auto
关键字,它可以用于自动推导变量的类型。例如:
auto i = 10; // 推导为 int auto name = "John"; // 推导为 const char*
自动类型推导使得代码更加简洁,并且可以避免显式指定类型的麻烦。
auto只能推导类型,推导出来的类型不能用来定义对象,decltype解决了这点,推导类型后可以用来定义对象。
#include<cstring> int main() { int i = 10; auto p = &i; decltype(p) pi;//int* pi = &i; cout << *pi << endl;//10 return 0; }
1.2 Lambda 表达式
Lambda 表达式是一种用于定义匿名函数的语法。它可以在需要函数对象的地方使用,并且可以捕获上下文中的变量。例如:
auto sum = [](int a, int b) { return a + b; }; int result = sum(5, 3); // 调用 lambda 表达式
Lambda 表达式提供了一种简洁的方式来定义和使用函数对象,特别是在需要传递函数作为参数的情况下。
1.3 右值引用(Rvalue References)
C++11 引入了右值引用,它允许我们绑定到临时对象(右值),并且可以实现移动语义和完美转发。右值引用由双引号 &&
表示。
什么是左值和右值:
- 一般来说,位于= 前的表达式为左值;存储在内存中、有明确存储地址(可取地址)的数据;
- 右值是指可以提供数据值的数据(不可取地址)。
int&& rvalue = 520; // 绑定到右值 520
右值引用的主要作用和意义如下:
1.3.1.移动语义:右值引用可以用于实现移动语义,即将一个对象的资源所有权从一个对象转移给另一个对象,而不需要进行深拷贝或浅拷贝。例如:
class MyString { public: // 移动构造函数 MyString(MyString&& other) noexcept : data_(other.data_), size_(other.size_) { other.data_ = nullptr; other.size_ = 0; } private: char* data_; size_t size_; }; MyString str1("Hello"); // 创建一个 MyString 对象 MyString str2(std::move(str1)); // 将 str1 转移给 str2
在上述代码中,MyString
类定义了一个移动构造函数,用于将一个右值引用转移给一个新对象。在创建 str2
对象时,使用 std::move
函数将 str1
转移给 str2
,从而避免了不必要的深拷贝和内存分配操作,提高了程序的性能和效率。
1.3.2完美转发:右值引用可以用于实现完美转发,即在函数调用时将参数按照原样转发给其他函数,从而避免了不必要的复制和拷贝操作。例如:
template<typename T> void process(T&& arg) { other_func(std::forward<T>(arg)); // 将 arg 按照原样转发给 other_func }
在上述代码中,process
函数接受一个右值引用参数 arg
,并使用 std::forward
函数将 arg
按照原样转发给 other_func
函数,从而避免了不必要的复制和拷贝操作,提高了程序的性能和效率。
1.4基于范围的for循环
在C++98/03中,不同的容器和数组遍历的方式不尽相同,写法不统一,也不够简洁,而C++11基于范围的for循环可以以简洁、统一的方式来遍历容器和数组,用起来也更方便了。
例如,传统for循环:
#include <iostream> #include <vector> using namespace std; int main() { vector<int> t{ 1,2,3,4,5,6 }; for (auto it = t.begin(); it != t.end(); ++it) { cout << *it << " "; } return 0; }
C++11基于范围的for
循环,语法格式:
for (decl : expr) { // 循环体 }
decl表示遍历声明,在遍历过程中,当前被遍历到的元素会被存储到声明的变量中.
expr是要遍历的对象,它可以是表达式、容器、数组、初始化列表等。
使用基于范围的 for
循环遍历容器,示例代码如下:
#include <iostream> #include <vector> using namespace std; int main(void) { vector<int> t{ 1,2,3,4,5,6 }; for (auto value : t) { cout << value << " "; } return 0; }
在上面的例子中,是将容器中遍历的当前元素拷贝到了声明的变量value
中,因此无法对容器中的元素进行写操作,如果需要在遍历过程中修改元素的值,需要使用引用。
此外,对容器的遍历过程中,如果只是读数据,不允许修改元素的值,可以使用const
定义保存元素数据的变量,在定义的时候建议使用const auto &
,减少一次数据的拷贝,这样相对于auto
效率要更高。
#include <iostream> #include <vector> using namespace std; int main(void) { vector<int> t{ 1,2,3,4,5,6 }; for (const auto& value : t) { cout << value << " "; } return 0; }
1.5 智能指针
在C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针。C++11中基于RAII机制,设计提供了三种智能指针,使用这些智能指针时需要引用头文件<memory>
:
std::shared_ptr
:共享的智能指针;std::unique_ptr
:独占的智能指针;std::weak_ptr
:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr
的。
1.5.1 unique_ptr
std::unique_ptr
是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个unique_ptr
赋值给另一个unique_ptr
。
// 通过构造函数初始化对象 { unique_ptr<int> ptr1(new int(10)); // error unique_ptr<int> ptr2 = ptr1; }
但是可以通过函数返回给其他的 std::unique_ptr,还可以通过 std::move 来转译给其他的 std::unique_ptr,这样原始指针的所有权就被转移了,这个原始指针还是被独占的。
unique_ptr<int> func()
{ return unique_ptr<int>(new int(520)); } int main()
{ unique_ptr<int> ptr1(new int(10)); unique_ptr<int> ptr2 = move(ptr1); unique_ptr<int> ptr3 = func(); // 这里的本质上是有RVO优化 return 0; }
1.5.2 shared_ptr
shared_ptr
的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。
对shared_ptr
进行初始化有三种方式:通过构造函数,std::make_shared
函数以及reset
方法。
shared_ptr<int> ptr0(new A); shared_ptr<int> ptr1(new int(9)); shared_ptr<int> ptr2(ptr1); shared_ptr<int> ptr3 = make_shared<int>(520); shared_ptr<int> ptr4 = make_shared<int>(100); shared_ptr<int> ptr5 = ptr4; ptr5.reset(new int(200));
对于一个未初始化的共享智能指针,可以通过 reset 方法来初始化,当智能指针中有值的时候,调用 reset 会使引用计数减 1。
创建一个智能指针的时候,更推荐使用make_shared。内存分配的次数会比方法一少一次。
1.5.3 weak_ptr
std::weak_ptr
可以看做是 shared_ptr
的助手,它不管理 shared_ptr
内部的指针。std::weak_ptr
没有重载操作符 *
和 ->
,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用就是作为一个旁观者监视 shared_ptr
中管理的资源是否存在。
智能指针如果循环引用会导致内存泄露,比如下面的例子:
struct A { public: shared_ptr<B> bptr; ~A() { cout << "class A is disstruct ..." << endl; } }; class B { public: shared_ptr<A> aptr; ~B() { cout << "class B is disstruct ..." << endl; } }; { shared_ptr<A> ap(new A); shared_ptr<B> bp(new B); std::cout << ap->use_count(); // 1 std::cout << bp->use_count(); // 1 ap->bptr = bp; bp->aptr = ap; std::cout << ap->use_count(); // 2 std::cout << bp->use_count(); // 2 } // 引用计数无法正常为0,资源无法释放,内存泄漏
weak_ptr
可以解决循环引用的问题:
class A { public: weak_ptr<B> bptr; ~A() { cout << "class A is disstruct ..." << endl; } }; class B { public: shared_ptr<A> aptr; ~B() { cout << "class B is disstruct ..." << endl; } }; { shared_ptr<A> ap(new A); shared_ptr<B> bp(new B); std::cout << ap->use_count(); // 1 std::cout << bp->use_count(); // 1 ap->bptr = bp; bp->aptr = ap; std::cout << ap->use_count(); // 1 std::cout << bp->use_count(); // 1 }
1.5.4 性能与安全的权衡
使用智能指针虽然能够解决内存泄漏问题,但是也付出了一定的代价。以shared_ptr
举例:
shared_ptr
的大小是原始指针的两倍,因为它的内部有一个原始指针指向资源,同时有个指针指向引用计数。- 引用计数的内存必须动态分配。虽然一点可以使用
make_shared()
来避免,但也存在一些情况下不能够使用make_shared()
。 - 增加和减小引用计数必须是原子操作,因为可能会有读写操作在不同的线程中同时发生。比如在一个线程里有一个指向一块资源的
shared_ptr
可能调用了析构(因此所指向的资源的引用计数减一),同时,在另一线程里,指向相同对象的一个shared_ptr
可能执行了拷贝操作(因此,引用计数加一)。原子操作一般会比非原子操作慢。但是为了线程安全,又不得不这么做,这就给单线程使用环境带来了不必要的困扰。
https://en.cppreference.com/w/cpp/header/memory
2、C++14 中的新特性
C++14 对 C++11 进行了一些改进,并引入了一些新特性,例如变长模板参数、二进制字面量等。
2.1 变长模板参数(Variadic Templates)
C++14 允许定义可变数量的模板参数,这被称为变长模板参数。通过使用省略号 ...
,可以在模板参数列表中指定任意数量的参数。例如:
template <typename... Args> void print(Args... args) { ((std::cout << args << " "), ...); } print(1, "hello", 3.14); // 输出: 1 hello 3.14
变长模板参数提供了更大的灵活性,可以处理不同数量和类型的参数。
2.2 二进制字面量(Binary Literals)
C++14 允许使用二进制字面量来表示二进制数值。使用前缀 0b
或 0B
,后跟一串二进制数字。例如:
int binary = 0b1010; // 二进制数值 10
二进制字面量提供了一种直观和简洁的方式来表示和使用二进制数值。
3、C++17 中的新特性
C++17 引入了一些有用的新特性,包括结构化绑定、折叠表达式、文件系统库等。
3.1 结构化绑定(Structured Bindings)
结构化绑定允许将元组或其他复杂类型的成员解包并绑定到独立的变量中。这样可以方便地访问和操作复杂类型的成员。例如:
std::pair<int, std::string> person{ 25, "John" }; auto [age, name] = person; std::cout << "Age: " << age << ", Name: " << name; // 输出: Age: 25, Name: John
结构化绑定简化了处理复杂类型的过程,使代码更加简洁易读。
3.2 折叠表达式(Fold Expressions)
折叠表达式是一种用于处理可变数量参数包的语法。它允许在模板展开过程中对参数包进行操作。例如:
template <typename... Args> bool allTrue(Args... args) { return (true && ... && args); } bool result = allTrue(true, true, false); // 返回 false
折叠表达式提供了一种简洁的方式来处理参数包,可以在编译时对参数进行组合和计算。
3.3 文件系统库(Filesystem Library)
C++传统文件操作需要使用std::ifstream
和 std::ofstream
类:分别表示输入文件流和输出文件流,可以用于读写文件内容。例如:
#include <fstream> #include <iostream> int main() { std::ifstream input_file("input.txt"); // 打开输入文件 if (!input_file.is_open()) { std::cerr << "Failed to open input file.\n"; return 1; } std::ofstream output_file("output.txt"); // 打开输出文件 if (!output_file.is_open()) { std::cerr << "Failed to open output file.\n"; return 1; } int num; while (input_file >> num) { // 从输入文件中读取数字 output_file << num * 2 << '\n'; // 将每个数字乘以 2 并写入输出文件 } input_file.close(); // 关闭输入文件 output_file.close(); // 关闭输出文件 return 0; }
C++17 引入了标准文件系统库,用于处理文件和目录的操作。该库提供了一组类和函数,用于创建、删除、移动、遍历文件和目录等操作。例如:
#include <filesystem> namespace fs = std::filesystem; int main() { fs::path dir_path = "dir"; // 定义目录路径 fs::create_directory(dir_path); // 创建目录 fs::path file_path = dir_path / "file.txt"; // 定义文件路径 fs::ofstream file(file_path); // 打开文件 if (!file.is_open()) { std::cerr << "Failed to open file.\n"; return 1; } file << "Hello, world!\n"; // 写入文件内容 file.close(); // 关闭文件 fs::remove(file_path); // 删除文件 fs::remove(dir_path); // 删除目录 return 0; }
文件系统库简化了文件和目录操作的实现,使得操作更加方便和可移植。
4、C++20 中的新特性
C++20 引入了一系列新特性,包括概念、协程、三路比较运算符等。
4.1 概念(Concepts)
概念是 C++20 中的一项重要特性,用于对模板的类型参数进行约束。概念允许我们对类型进行条件检查,从而限制模板的实例化。例如:
template <typename T> concept Arithmetic = std::is_arithmetic<T>::value; template <Arithmetic T> T square(T value) { return value * value; } int result = square(5); // 正确,T 为算术类型 std::string str = "hello"; // 错误,T 不是算术类型 int result = square(str);
概念提供了一种声明式的方式来定义模板参数的约束条件,使代码更具表达力和安全性。
4.2 协程(Coroutines)
C++20 引入了协程支持,使得异步编程更加简洁和可读。协程允许函数在执行期间暂停和恢复,以便于异步任务的处理。例如:
#include <iostream> #include <coroutine> struct Generator { struct promise_type { int current_value; auto get_return_object() { return Generator{ std::coroutine_handle<promise_type>::from_promise(*this) }; } auto initial_suspend() { return std::suspend_always{}; } auto final_suspend() noexcept { return std::suspend_always{}; } void return_void() {} auto yield_value(int value) { current_value = value; return std::suspend_always{}; } void unhandled_exception() { std::terminate(); } }; std::coroutine_handle<promise_type> coroutine; bool move_next() { coroutine.resume(); return !coroutine.done(); } int current_value() { return coroutine.promise().current_value; } }; Generator generate() { co_yield 1; co_yield 2; co_yield 3; } int main() { Generator generator = generate(); while (generator.move_next()) { std::cout << generator.current_value() << " "; } // 输出: 1 2 3 return 0; }
协程提供了一种简洁的方式来编写异步代码,提高了代码的可读性和可维护性。
4.3 三路比较运算符(Three-Way Comparison)
C++20 引入了三路比较运算符(<=>
),用于比较对象的大小关系。它返回一个可比较的结果,可以是小于、等于或大于。例如:
struct Person { std::string name; int age; auto operator<=>(const Person& other) const = default; //auto表示函数的返回类型将由编译器自动推导.在这里,返回类型将会是一个 std::strong_ordering 类型,这是 C++20 引入的一种枚举类型,用于表示强制排序关系。 //= default:这个关键字表示使用默认实现,即使用编译器自动生成的代码来实现这个成员函数。在这里,编译器会自动生成一个使用 spaceship 运算符比较对象的代码。 }; Person john{"John", 25}; Person alice{"Alice", 30}; if (john < alice) { std::cout << "John is younger than Alice."; } else if (john > alice) { std::cout << "John is older than Alice."; } else { std::cout << "John and Alice have the same age."; }
三路比较运算符简化了比较操作的实现,提供了一种统一和直观的比较语法。