- 提⾼运⾏效率的语⾔特性:右值引⽤、泛化常量表达式
- 原有语法的使用性增强:初始化列表、统⼀的初始化语法、类型推导、范围 for 循环、Lambda 表达式、final 和 override、构造函数委托
- 语⾔能⼒的提升:空指针 nullptr、default 和 delete、⻓整数、静态 assert
- C++ 标准库的更新:智能指针、正则表达式、哈希表等
具体
-
空指针nullptr
C++11 引⼊了 nullptr 关键字,专⻔⽤来区分空指针、0。nullptr 的类型 -
Lambda表达式
-
右值引用
-
泛化的常量表达式 constexpr
constexpr 告诉编译器这是⼀个编译期常ᰁ,甚至可以把⼀个函数声明为编译期常量表达式。 -
初始化列表 C++11 提供了 initializer_list 来接受变⻓的对象初始化列表,注意初始化列表特性只是现有语法增强,并不是提供了动态的可变参数。该列表只能静态地构造。
-
统一的初始化语法 不同的数据类型具有不同的初始化语法。如何初始化字符串?如何初始化数组?如何初始化多维数组?如何初始化对象?C++11给出了统⼀的初始化语法:均可使⽤“{}-初始化变量列表”:
-
类型推导auto和decltype auto 和 decltype 来静态推导类型,在我们知道类型没有问题但⼜不想完整地写出类型的时候, 便可以使⽤静态类型推导。
-
基于范围的for循环
-
构造函数委托 这使得构造函数可以在同⼀个类中⼀个构造函数调⽤另⼀个构造函数,从⽽达到简化代码的⽬的:
-
final和override
C++ 借由虚函数实现运⾏时多态,但 C++ 的虚函数⼜很多脆弱的地⽅:- ⽆法禁⽌⼦类重写它。可能到某⼀层级时,我们不希望⼦类继续来重写当前虚函数了。
- 容易不⼩⼼隐藏⽗类的虚函数。⽐如在重写时,不⼩⼼声明了⼀个签名不⼀致但有同样名称的新函数。
C++11 提供了 final 来禁⽌虚函数被重写/禁⽌类被继承, override 来显示地重写虚函数。 这样编译器给我们不⼩⼼的⾏为提供更多有⽤的错误和警告。
-
default和delete
-
静态assertion
-
智能指针
-
正则表达式
-
哈希表
C++ 的 map , multimap , set , multiset 使⽤红⿊树实现, 插⼊和查询都是 O(lgn) 的复杂度,但 C++11 为这四种模板类提供了(底层哈希实现)以达到 O(1) 的复杂度:
C++20协程
C++20 之前,C++ 不直接支持协程,但开发人员可以使用第三方库(如 Boost.Coroutine)来使用协程。C++20 引入了对协程的原生支持,包括一套新的关键字和库类型,使得开发人员可以更容易地在 C++ 程序中使用协程。提高并发编程效率。
两种协程都非常适用于需要处理大量并发任务或异步操作的场景,但C++协程提供了更多的低级控制和优化的可能性,而Go协程提供了简单、高效的并发编程模型。
智能指针
智能指针作用
智能指针其作⽤是管理⼀个指针,避免咋们程序员申请的空间在函数结束时忘记释放,造成内存泄漏这种情况滴发⽣。
然后使⽤智能指针可以很⼤程度上的避免这个问题,因为智能指针就是⼀个类,当超出了类的作⽤域是,类会⾃动调⽤析构函数,析构函数会⾃动释放资源。所以智能指针的作⽤原理就是 在函数结束时⾃动释放内存空间,不需要⼿动释放内存空间
在C/C++指针引发的错误中有如下两种:内存泄漏和指针悬挂。使用智能指针可以较好地解决这两个问题。
内存泄露
用动态存储分配函数(如malloc)动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元,直到程序结束。它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩
悬空指针
当指针指向的对象被释放,但是该指针没有任何改变,以至于其仍然指向已经被回收的内存地址,一般由以下几种情况:
- 指针未初始化
- 指针拷贝后删除了指针 如果在使用指针过程中对指针进行了拷贝,然后其中一个拷贝被删除,则另外一个拷贝就成了悬挂指针。
- 指针delete后并未把指针置空
delete指针只是释放了那个指针原本所指的内存而已,并没有删除该指针。如上代码所示,被delete后的指针p的值(地址值)并非就是nullptr值,而是随机值。因此,指针被delete后,如果不置为空,那么指针p就成了悬挂指针,就会在内存里乱指一通。
delete后置空指针的最大价值就在于明确资源当前状态。你想判断一个资源是否有效时,你当然没法直接跑去看这个资源在不在,而是得询问资源的持有者是否仍然持有这个资源。如果所有被delete的指针都被置为nullptr,之后再去访问这个指针的时候,我们可以通过其与nullptr的比较轻松判断出资源是否已经被delete。
悬空指针与野指针区别
野指针(wild pointer):就是没有被初始化过的指针。⽤ gcc -Wall 编译, 会出现 used uninitialized 警告。
悬空指针:是指针最初指向的内存已经被释放了的⼀种指针。
⽆论是野指针还是悬空指针,都是指向⽆效内存区域(这⾥的⽆效指的是"不安全不可控")的指针。 访问"不安全不可控"(invalid)的内存区域将导致"Undefined Behavior"。
如何避免使⽤野指针? 在平时的编码中,养成在定义指针后且在使⽤之前完成初始化的习惯或者使⽤智能指针。
auto——ptr(C++98方案、C11已抛弃))采⽤所有权模式。
auto_ptr可以修复由于程序员未妥善处理导致的内存泄漏问题和悬挂指针问题(通过智能指针的构造函数完成拷贝)。
原理 作用
特点or缺点:
存在问题:
所有权模式: 两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。
unique—ptr
从第三节的内容可以看出auto_ptr具有比较多的缺陷,使用时容易出错。在C++ 11标准中出现了新的智能指针unique_ptr、 shared_ptr与weak_ptr等,这里首先介绍unique_ptr,可以将unique_ptr看成是auto_ptr的升级替代品。unique_ptr 实现独占式拥有或严格拥有概念,保证同⼀时间内只有⼀个智能指针可以指向该对象。它对于避免资源泄露特别有⽤。
编译器会认为auto-ptr存在问题中的p4=p3非法,避免了p3不再指向有效数据的问题。unique_ptr ⽐ auto_ptr 更安全。unique_ptr禁止赋值和复制,“唯一”地拥有其所指对象,同一时刻只能有一个unique_ptr实例指向给定对象。也就是说模板类unique_ptr的copy构造函数以及等号(“=”)操作符是无法使用的。通过禁止复制和赋值可以较好的改善auto_ptr的所有权转移问题。
实现:
unique_ptr(const unique_ptr<T>&) noexcept = delete;
unique_ptr& operator = (const unique_ptr&) noexcept = delete;
虽然赋值禁用了,但是愿意交出控制权,交给赋值的人,那还是可以允许的。(拷贝禁用实现unique,但允许移动)
自定义unique_ptr
#include <iostream>
template<typename T>
class MyUniquePtr{
private:
T* ptr;
public:
// 构造析构
explicit MyUniquePtr(T *p = nullptr): ptr(p){}
~MyUniquePtr(){if(ptr) delete ptr;}
// 禁用拷贝构造和拷贝赋值运算符
MyUniquePtr(const MyUniquePtr&) = delete;
MyUniquePtr& operator=(const MyUniquePtr&) = delete;
// 移动拷贝构造函数
/* 参数为什么不是常量的?
许在移动语义中获取并修改被移动对象的资源,右值引用的整个设计思想是为了资源所有权的转移,而不是对资源的共享。
*/
MyUniquePtr(MyUniquePtr&& other) noexcept: ptr(other.ptr){other.ptr = nullptr; }
// 移动赋值运算符
MyUniquePtr& operator=(MyUniquePtr &&other) noexcept{
if(this != &other){
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
// 运算符,常量成员函数表示不会修改
T& operator*() const{ return *ptr;}
T* operator->() const{return ptr;}
}
int main(){
MyUniquePtr<int> uniquePtr(new int(42)); // (new int(42))是右值
MyUniquePtr<int> anotherPtr(move(uniquePtr));
if (!uniquePtr) { std::cout << "After move, UniquePtr is empty." << std::endl; } return 0;
}
shared-ptr(共享型、强引用)
shared_ptr 实现共享式拥有概念,多个智能指针可以指向相同对象,该对象和其相关资源会在“最后⼀个引⽤被销毁”时候释放。从名字 share 就可以看出了资源可以被多个指针共享,它使⽤计数机制来表明资源被⼏个指针共享。是 c++ 标准库中的一种智能指针,用于管理动态分配的内存资源,采用计数技术来进行内存管理。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性 (auto_ptr 是独占的),在使⽤引⽤计数的机制上提供了可以共享所有权的智能指针。
使用: 可以通过成员函数 use_count() 来查看资源的所有者个数,除了可以通过 new 来构造,还可以通过传⼊auto_ptr, unique_ptr,weak_ptr 来构造。当我们调⽤ release() 时,当前指针会释放资源所有权,计数减⼀。当计数等于 0 时,资源会被释放。
实现原理:
std::shared_ptr 使用引用计数来管理资源的生命周期。它包含两个部分:指向对象的指针和一个引用计数。当 shared_ptr 被拷贝时,引用计数会增加;当 shared_ptr 被销毁或者重置时,引用计数会减少。只有当引用计数降为零时,shared_ptr 才会自动释放内存。
shared_ptr的构造函数中会开辟新的引用计数的资源。 shared_ptr的拷贝构造函数没有开辟新的引用计数的资源,只是引用计数加1。
三个问题:
- 多个独立的shared_ptr实例不能共享一个对象
- 循环引用问题:
问题怎么产生:
导致后果:涉及到循环引用的智能指针无法调用析构函数、内存泄漏。
解决:可以使用 weak_ptr 来解决这个问题,weak_ptr 不会增加引用计数,只是提供了对 shared_ptr 所指向对象的弱引用,不会影响内存的回收。 - 线程不安全问题
- shared_ptr智能指针对象中的引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时进行++或者 - -操作,在为加锁的情况下,会导致计数混乱,这样有可能造成资源泄漏或者程序奔溃的问题,而且 ++和- -操作本身也不是原子的。
- 智能指针管理的对象保存在堆上,两个线程同时去访问,也会导致线程安全问题。
- 保证线程安全,需要在合适的地方加上锁在访问临界资源的地方(临界区)加上锁;访问完后解锁。
weak—ptr(弱引用)
weak_ptr是为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它没有重载operator*和->,故而不具有普通指针的行为。它的最大作用在于协助shared_ptr工作。 weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。weak_ptr 是⼀种不控制对象生命周期的智能指针,它指向⼀个 shared_ptr 管理的对象。进⾏该对象的内存管理的是那个强引⽤的 shared_ptr。
weak_ptr 只是提供了对管理对象的⼀个访问⼿段。weak_ptr 设计的⽬的是为配合shared_ptr ⽽引⼊的⼀种智能指针来协助 shared_ptr ⼯作,它只可以从⼀个 shared_ptr 或另⼀个 weak_ptr 对象构造,,它的构造和析构不会引起引⽤记数的增加或减少。
weak_ptr 是⽤来解决shared_ptr 相互引⽤时的死锁问题,如果说两个 shared_ptr 相互引⽤,那么这两个指针的引⽤计数永远不可能下降为0,也就是资源永远不会释放。它是对对象的⼀种弱引⽤,不会增加对象的引⽤计数,和 shared_ptr 之间可以相互转化,shared_ptr 可以直接赋值给它,它可以通过调⽤ lock 函数来获得shared_ptr。
怎么使用:
出现场景:
原理实现:
怎么检测循环引用:
自定义智能指针实现
左值和右值引用 && 移动语义
左值和右值区别:
- 左值可以位于赋值语句的左侧,右值则不能
- 左值是持久对象,可以取地址(左值具有对应可由用户访问的存储单元,能由用户改变其值的量);
右值是临时对象,不可以取地址 - 当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候用的是对象的身份(在内存中的位置);
左值引用和右值引用的区别:
- 左值引用是对一个左值(lvalue)的引用,即一个具有名称的、可寻址的内存位置的表达式。左值是可以被修改的,例如变量、对象成员、数组元素等。
- 右值引用是对一个右值(rvalue)的引用,即一个临时的、无法寻址的表达式。右值是不可修改的,例如字面常量、临时变量、函数返回值(如果返回的是临时对象)等。
- 声明形式不同,左值引用的声明形式为 Type& ref,右值引用的声明形式为 Type&& ref
- 右值引用通常用于移动语义,即将一个对象的资源(比如动态分配的内存)从一个对象转移到另一个对象,避免深拷贝。右值引用还是完美转发(perfect forwarding)的基础。
右值引用解决问题:移动语义和完美转发
移动语义:
解决问题: 提供可以移动而非拷贝对象的能力,避免对象尤其是大型数据结构拷贝后被立即销毁带来的性能损失。总体思路通过定义移动拷贝构造函数,当入参为右值引用时调用。通过move函数可以将左值强制转换为右值引用。
std::move:
我们知道移动语义是通过右值引用来匹配临时值,那么,普通的左值是否也能借助移动语义来优化性能呢,C++11为了解决这个问题,提供了std::move方法来将左值转换成右值。
move实际上并不能移动任何东西,它只是将一个左值强制转换成一个右值引用,使我们可以通过右值引用使用该值,以用于移动语义,强制转换为右值的目的是为了方便实现移动构造。
使用例子:
// 1. 移动拷贝构造函数
class MyString{
public:
MyString(string str):data(move(str)){}; // 使用拷贝构造函数将传入参数传递给成员变量
MyString(MyString&& other) noexcept: data(move(other.data)){}; // 移动构造函数,将资源从other对象移动到当前对象, 基于string的移动拷贝函数
// 若底层维护对象是字符指针,则直接通过改变指针指向,原对象指针指空实现
const std::string& getString() const { return data; }
private:
string data;
}
int main() {
string hello = "Hello, World!";
MyString str1(move(hello)); // 使用std::move将hello的所有权转移到str1
cout << "Original string: " << hello << std::endl; // 输出为空,hello的内容已经被移动走了
cout << "Moved string: " << str1.getString() << std::endl; // 输出Hello, World! return 0; }
move对于拥有形如对内存、文件句柄等子资源的成员的对象有效,如果是一些基本类型, 如int和char[4]数组时,如果使用move,仍然会发生拷贝(因为没有对应的移动构造函数),所以说move对于含资源的对象来说更有意义。
将移动拷贝函数声明为noexcept的作用和目的:
noexcept 是C++11引入的关键字,用于指示一个函数是否可能抛出异常。当你声明一个函数为 noexcept,它表示该函数不会抛出异常。这样的声明可以帮助编译器进行优化,提高程序性能。
- 性能优化: 不抛出异常,提高程序性能
- 异常安全性:资源移动可能会涉及内存的分配和释放以及文件句柄的转移操作,如果移动操作失败抛出异常会导致崩溃或者资源无法正确释放导致资源泄露。程序的异常安全性造成威胁。
完美转发
Lambda函数(匿名函数),
利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象;当定义一个Lambda表达式的时候,编译器会自动生成一个匿名类(默认重载了()运算符以便调用),这个匿名类为闭包类型。在运行时,Lambda表达式会返回一个匿名的闭包实例。通过闭包来实现捕获,即通过传值或引用的方式捕捉其封装作用域内的变量。
语法定义:[capture(捕获块)] (parameters) mutable ->return-type {statement};
mutable:在lambda匿名函数体里边,按值捕获到的变量,实质上是调用者函数中变量的只读拷贝(read-only),加入了mutable后,匿名函数体内部可以修改这个拷贝的值,
捕获列表类型:
- 引用捕获
为lambda调用时变量的值 - 值捕获
在lambda定义时变量的值就固定不变; - 隐式捕获
用法:
string strTemp = "非静态局部变量";
int iTemp = 10;
int iTemp = 10;
double dValue = 13.14;
bool bOK = true;
//定义lambda
auto fun = [strTemp, &iTemp]()
{
cout << strTemp << endl;
cout << iTemp << endl;
}
auto fun_ref = [&, strTemp, iTemp]() mutable //strTemp和iTemp为值捕获,其他局部变量为引用捕获
{
strTemp = "local"; //显式值捕获
iTemp = 15; //显式值捕获
dValue = 5.20; //隐式引用捕获
bOK = false; //隐式引用捕获
};
fun_ref();
cout << strTemp << endl;
cout << iTemp << endl;
cout << dValue << endl;
cout << boolalpha << bOK << endl;
闭包是什么:
在C++中,闭包是一个能够捕获作用域变量的未命名函数对象,它包含了需要使用的“上下文”(函数与变量),同时闭包允许函数通过闭包的值或引用副本访问这些捕获的变量,即使函数在其范围之外被调用。
优点:简洁可读不会污染命名空间。
使用场景:
- STL中谓词使用,用于定义排序、查找、过滤等操作的逻辑。
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(numbers.begin(), numbers.end(), [](int num) {
std::cout << num << std::endl;
});
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(numbers.begin(), numbers.end(), [](int num) {
std::cout << num << std::endl;
});
- lambda常常作为线程执行函数使用,这时尤其要注意调用者上下文环境的变量(及其指向的内存空间)的生命周期,是否能够和以lambda作为线程执行函数的线程的生命周期一样长,
多线程std::lock_guard<std::mutex> locker(mutex_)
通常不直接使用 mutex,lock_guard更加安全, 更加方便。
lock_guard简化了 lock/unlock 的写法, lock_guard在构造时自动锁定互斥量, 而在退出作用域时会析构自动解锁, 保证了上锁解锁的正确操作, 正是典型的 RAII 机制。
- std::lock_guard类的构造函数禁用拷贝构造,且禁用移动构造。std::lock_guard类除了构造函数和析构函数外没有其它成员函数。
- 对互斥锁变量mutex_进行所有权获取,并不是每一处就重新创建一个锁进行锁住
- std::lock_guardstd::mutex locker(mutex_);是对互斥锁变量mutex_进行管理,在这句话后面的作用域内,代码都处于mutex_上锁状态,别的位置代码获取这个锁mutex_会失败。他们的目的就是为了保护同一个共享内存区域不会被多线程同时访问(同时读和写、或 同时写和写)
- 在std::lock_guard对象构造时,传入的mutex对象(即它所管理的mutex对象)会被当前线程锁住。在lock_guard对象被析构时,它所管理的mutex对象会自动解锁,不需要程序员手动调用lock和unlock对mutex进行上锁和解锁操作。