【QandA C++】C++11新特性、Lambda表达式、左值引用、右值引用、完美转发、智能指针、move、强制类型转换等重点知识汇总

C++11 新特性

  • nullptr 替代 NULL
  • 引入了 auto 实现了类型推导
  • 基于范围的 for 循环for(auto& i : res){}
  • 类和结构体的中初始化列表
  • Lambda 表达式(匿名函数)
  • std::forward_list(单向链表)
  • 右值引用和move语义
  • 无序容器和正则表达式
  • 成员变量默认初始化
  • 智能指针等

lambda

lambda表达式书写格式[捕捉列表] (参数列表) mutable- > 返回值类型 {函数体}

  1. 捕捉列表。该列表总是出现在lambda函数的开始位 置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  2. 参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
  3. mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  4. ->return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可以省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
  5. 函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
int a = 10, b = 20;
// lambda表达式是一个匿名函数,该函数无法直接调用,
// 如果想要直接调用可借助auto将其赋值给一个变量,此时这个变量就可以像普通函数一样使用。
auto Swap = [](int& x, int& y)->void
{
    int tmp = x;
    x = y;
    y = tmp;
};
Swap(a, b); //交换a和b

如果以传值方式进行捕捉,那么首先编译不会通过,
因为传值捕获到的变量默认是不可修改的,如果要取消其常量性,
就需要在lambda表达式中加上mutable,并且此时参数列表不可省略。比如:
int a = 10, b = 20;
auto Swap = [a, b]()mutable
{
    int tmp = a;
    a = b;
    b = tmp;
};
Swap(); //交换a和b?

捕获列表说明:

  1. 捕获列表描述了上下文中哪些数据可以被lambda函数使用,以及使用的方式是传值还是传引用。
  2. [var]:表示值传递方式捕捉变量var。
  3. [=]:表示值传递方式捕获所有父作用域中的变量(成员函数包括this指针)。
  4. [&var]:表示引用传递捕捉变量var。
  5. [&]:表示引用传递捕捉所有父作用域中的变量(成员函数包括this指针)。
  6. [this]:表示值传递方式捕捉当前的this指针。

lambda表达式可以用作函数对象,用于在函数或算法中定义短小的、临时的操作。

lambda底层实现原理

实际编译器在底层对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的。函数对象就是我们平常所说的仿函数,就是在类中对()运算符进行了重载的类对象。

本质就是因为lambda表达式在底层被转换成了仿函数

  1. 当我们定义一个lambda表达式后,编译器会自动生成一个类,在该类中对()运算符进行重载,实际lambda函数体的实现就是这个仿函数的operator()的实现。
  2. 在调用lambda表达式时,参数列表和捕获列表的参数,最终都传递给了仿函数的operator()。

右值引用和左值引用的区别

区别总结如下:

传统的C++语法中就有引用的语法,而C++11中新增了右值引用的语法特性,为了进行区分,于是将C++11之前的引用就叫做左值引用。但是无论左值引用还是右值引用,本质都是给对象取别名

  1. 左值引用就是对左值的引用,给左值取别名,通过“&”来声明。
  2. 右值引用就是对右值的引用,给右值取别名,通过“&&”来声明。
  3. 需要注意的是,右值本身是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,这时这个右值可以被取到地址,并且可以被修改,如果不想让被引用的右值被修改,可以用const修饰右值引用。
  4. 左值引用做参数,防止传参时进行拷贝操作。左值引用做返回值,防止返回时对返回对象进行拷贝操作。
  5. 右值引用:主要用于实现移动语义、完美转发和资源管理,例如通过移动构造函数和移动赋值运算符优化对象的拷贝。
  6. 左值引用可以用于修改左值,右值引用不能用于修改右值。
  7. 左值引用绑定到左值,其生命周期由左值的生命周期决定。右值引用绑定到右值,其生命周期通常较短,可以用于资源管理(如移动语义)。

左值是指具有标识符(变量名)的表达式,可以出现在赋值操作符的左边。左值通常表示具有持久性的、有名字的内存位置,可以被多次引用和修改。例如,变量、对象成员、函数返回的左值引用等都是左值。

右值是指不具有标识符(变量名)的临时表达式,通常表示即将被丢弃的值。右值通常表示临时的、短暂的值,不能被多次引用或修改。例如,常量、临时表达式、字面量、函数返回的右值引用等都是右值。

左值引用可以引用右值吗?

  1. 左值引用不能引用右值,因为这涉及权限放大的问题,右值是不能被修改的,而左值引用是可以修改。
  2. 但是const左值引用可以引用右值,因为const左值引用能够保证被引用的数据不会被修改。

因此const左值引用既可以引用左值,也可以引用右值。

右值引用可以引用左值吗?

  1. 右值引用只能引用右值,不能引用左值。
  2. 但是右值引用可以引用move以后的左值
  3. move函数是C++11标准提供的一个函数,被move后的左值能够赋值给右值引用。

右值引用的使用场景及如何减少拷贝

左值引用的短板:

左值引用虽然能避免不必要的拷贝操作,但左值引用并不能完全避免不必要的拷贝操作

  1. 左值引用做参数,能够完全避免传参时不必要的拷贝操作。
  2. 左值引用做返回值,并不能完全避免函数返回对象时不必要的拷贝操作。

如果函数返回的对象是一个局部变量,该变量出了函数作用域就被销毁了,这种情况下不能用左值引用作为返回值,只能以传值的方式返回,这就是左值引用的短板

右值引用的主要使用场景包括:

  1. 移动语义:用于将资源从一个对象转移到另一个对象,而不进行深拷贝。

在移动构造函数和移动赋值函数中使用右值引用可以避免对象的拷贝和内存的分配,提高效率;

  1. 完美转发:右值引用在模板函数中可以用于实现完美转发,即将参数按原样转发给其他函数,保持原有的左值或右值特性。这在泛型编程和实现容器等通用数据结构时非常有用。
  2. 临时对象的处理:右值引用可以直接绑定到临时对象,这使得我们能够对临时对象进行操作,如在函数返回值为临时对象时,可以通过右值引用进行优化, 这样减少拷贝, 提高效率.

如何减少拷贝?

  1. 右值引用通过移动语义来减少不必要的拷贝操作,从而提高性能。
  2. 在C++中,当将一个对象赋值给另一个对象或作为函数参数传递时,通常会触发拷贝构造函数或拷贝赋值运算符的调用。如果是深拷贝的话代价就会非常大!
  3. 右值引用通过使用移动构造函数和移动赋值运算符来转移对象的资源所有权,而不是进行深拷贝操作。移动构造函数和移动赋值运算符接收一个右值引用参数,并将资源从源对象“移动”到目标对象,而不是复制数据。这样可以避免不必要的内存分配和数据复制,提高性能。
  4. 移动操作通常在临时对象或即将被销毁的对象上使用。这些对象不再需要其资源,因此可以将资源所有权移交给新的对象,而不需要进行深拷贝。
  5. 常见示例是在容器中插入元素。当插入一个对象时,如果该对象是一个临时对象(右值),可以通过移动构造函数将其资源转移到容器中,而不需要进行不必要的拷贝。

完美转发

完美转发是允许你在泛型代码中精确地传递函数参数,并保留原始参数的值类别(左值或右值)。完美转发通常与右值引用一起使用,尤其是在函数模板中,以避免不必要的拷贝和维护类型信息。

为什么需要完美转发?

在C++中,函数可以接受左值引用和右值引用作为参数,例如:

void foo(int& x);   // 函数接受左值引用
void bar(int&& x);  // 函数接受右值引用

当你要编写一个泛型函数或类,以便将参数传递给其他函数,问题就出现了。如果你简单地将参数传递给其他函数,参数可能会被视为左值,导致不必要的拷贝。这就是完美转发派上用场的地方。

使用完美转发的步骤

步骤 1:使用模板参数

首先,你需要将模板参数引入函数,通常使用template来表示可变数量的模板参数。这将允许你以通用的方式接受任何类型的参数。

template
void forwarder(Args&&... args) {
    // ...
}

步骤 2:使用参数包

使用Args&&... args语法创建参数包,它接受任意数量的参数,并保留它们的值类别。Args是参数类型的模板参数包,args是参数名。

步骤 3:使用std::forward

在调用其他函数时,使用std::forward来保留原始参数的值类别。std::forward是一个模板函数,它根据参数的值类别返回左值引用或右值引用。

template
void forwarder(Args&&... args) {
    other_function(std::forward(args)...);
}

完美转发的示例

#include 
#include 

void foo(int& x) {
    std::cout << "Lvalue reference: " << x << std::endl;
}
void bar(int&& x) {
    std::cout << "Rvalue reference: " << x << std::endl;
}

template
void A(Args&&... args) {
    foo(std::forward(args)...);
    bar(std::forward(args)...);
}

int main() {
    int x = 42;
    A(x);        // 传递左值
    A(10);       // 传递右值
    A(std::move(x)); // 传递被std::move包装的右值
    return 0;
}

在上述示例中,forwarder函数接受任何类型的参数,并使用std::forward将它们传递给foo和bar函数。通过这种方式,你可以确保参数的值类别得到正确地保留,避免不必要的拷贝,实现了完美转发。

智能指针的原理

C++11中引入了智能指针的概念,是解决动态内存管理的问题,减少内存泄漏与手动内存管理相关的问题。

使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

智能指针的行为类似常规的指针,重要的区别是它负责自动释放所指向的对象!!

auto_ptr

管理权转移

auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,这可能导致悬挂指针的问题。保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。

但一个对象的管理权转移后也就意味着,该对象不能再用对原来管理的资源进行访问了,否则程序就会崩溃。

悬挂指针,它发生在一个指针引用的是已经被释放或无效的内存地址的情况下。这种问题可能导致程序的不稳定性、崩溃和不可预测的行为。

指针指向已释放的内存:当你释放了一块内存(使用delete或free等操作),但之后仍然保留了指向该内存的指针,并尝试使用这个指针,就会导致悬挂指针问题。

int* ptr = new int;

delete ptr;

*ptr = 42; // 这里的ptr就是悬挂指针

unique_ptr

防拷贝

unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,这样也能保证资源不会被多次释放。

但防拷贝其实也不是一个很好的办法,因为总有一些场景需要进行拷贝。

shared_ptr

引用计数

shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题。

  1. 每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块资源。
  2. 当新增一个对象管理这块资源时则将该资源对应的引用计数进行++,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--。
  3. 当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放了。

通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。引用计数需要存放在堆区

weak_ptr

shared_ptr的循环引用问题

shared_ptr的循环引用问题在一些特定的场景下才会产生。比如定义俩个list的结点类,俩节点相互指向。

  1. 将这两个结点分别交给两个shared_ptr对象进行管理。
  2. 这时程序运行结束后两个结点都没有被释放,但如果去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用

循环引用导致资源未被释放的原因:

  1. 资源1的释放取决于资源2当中的prev成员,而资源2的释放取决于资源1当中的next成员。
  2. 而资源1当中的next成员的释放又取决于资源1,资源2当中的prev成员的释放又取决于资源2。
  3. 因为资源之间存在这种相互依赖关系,它们的引用计数永远无法降为零,从而导致了内存泄漏。

解决循环引用问题

  1. weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。
  2. weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数。

将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。

写一个 shared_ptr

简易版的shared_ptr的实现步骤如下:

  1. 在shared_ptr类中增加一个成员变量count,表示智能指针对象管理的资源对应的引用计数。
  2. 在构造函数中获取资源,并将该资源对应的引用计数设置为1,表示当前只有一个对象在管理这个资源。
  3. 在拷贝构造函数中,与传入对象一起管理它管理的资源,同时将该资源对应的引用计数++。
  4. 在拷贝赋值函数中,先将当前对象管理的资源对应的引用计数--(如果减为0则需要释放)
  5. 然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++。
  6. 在析构函数中,将管理资源对应的引用计数--,如果减为0则需要将该资源释放。
  7. 对*和->运算符进行重载,使shared_ptr对象具有指针一样的行为。

为什么引用计数需要存放在堆区

  1. 如果将shared_ptr中的引用计数count定义成一个指针,当一个资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,如果有其他对象也想要管理这个资源,那么除了将这个资源给它之外,还需要把这个引用计数也给它。
  2. 这时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数了,相当于将各个资源与其对应的引用计数进行了绑定。
namespace shangs
{
	template
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}
		~shared_ptr()
		{
			if(--(*_pcount) == 0)
			{
				if(_ptr != nullptr)
				{
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					_ptr = nullptr;
				}
				delete _pcount;
				_pcount = nullptr;
			}
		}
		shared_ptr(shared_ptr& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			(*_pcount)++;
		}
		shared_ptr& operator=(shared_ptr& sp)
		{
			if(_ptr != sp._ptr)//管理同一块空间的对象之间无需进行赋值操作
			{
				if(--(*_pcount) == 0)//将管理的资源对应的引用计数--
				{
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					delete _pcount;
				}
				_ptr = sp._ptr; //与sp对象一同管理它的资源
				_pcount = sp._pcount; //获取sp对象管理的资源对应的引用计数
				(*_pcount)++; //新增一个对象来管理该资源,引用计数++
			}
			return *this;
		}
		//获取引用计数
		int use_count()
		{
			return *_pcount;
		}
		//可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr; //管理的资源
		int* _pcount; //管理的资源对应的引用计数
	};
}

说说 move 函数

C++中的move语义是一种高效的资源转移机制,可以帮助我们避免不必要的拷贝操作,提高程序性能。

move的使用场景

当需要将资源从一个对象转移到另一个对象时,可以使用move。例如,在容器中移动元素、在算法中交换数据等。需要注意的是,只有可移动的对象才能使用移动语义,否则可能导致未定义行为。

使用移动语义可以避免不必要的拷贝操作,从而提高性能。例如,在复制一个大型对象时,如果使用移动语义,只需要进行一次内存分配和一次指针拷贝,而不需要进行多次拷贝操作。

使用例子

  • 移动构造函数:move可以将一个左值转换为右值引用,从而实现资源的转移。
  • 移动赋值运算符:move也可以用于将一个对象的资源转移到另一个对象。

四种强制类型转换及作用

static_cast 静态转换

static_cast用于相近类型之间的转换,编译器隐式执行的任何类型转换都可用static_cast,但它不能用于两个不相关类型之间转换。

double d = 3.14;
int i = static_cast(d); // 将double转换为int,结果是3

reinterpret_cat 重新解释转换

reinterpret_cast用于两个不相关类型之间的转换

通常将一种指针类型转换为另一种指针类型,不进行类型检查。

int* pInt = new int;
double* pDouble = reinterpret_cast(pInt);

const_cast 常量转换

const_cast用于删除变量的const属性,转换后就可以对const变量的值进行修改。

说明一下:

  1. 代码中用const_cast删除了变量a的地址的const属性,这时就可以通过这个指针来修改变量a的值。
const int x = 10;
int* y = const_cast(&x);
*y = 20; // 合法,但修改const对象的值是不安全的

dynamic_cast 动态转换

class Base { /* ... */ };
class Derived : public Base { /* ... */ };

Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast(basePtr);
if (derivedPtr != nullptr) {
    // 安全的向下转型
}
  1. dynamic_cast用于将父类的指针(或引用)转换成子类的指针(或引用)
  2. dynamic_cast只能用于含有虚函数的类,因为运行时类型检查需要运行时的类型信息,而这个信息是存储在虚函数表中的,只有定义了虚函数的类才有虚函数表。

向上转型与向下转型

  1. 向上转型: 子类的指针(或引用)→ 父类的指针(或引用)。切割/切片,语法天然支持
  2. 向下转型: 父类的指针(或引用)→ 子类的指针(或引用)。语法不支持,需要进行强制类型转换

向下转型的安全问题:

向下转型分为两种情况:

  1. 如果父类的指针(或引用)指向的是一个父类对象,那么将其转换为子类的指针(或引用)是不安全的,因为转换后可能会访问到子类的资源,而这个资源是父类对象所没有的。
  2. 如果父类的指针(或引用)指向的是一个子类对象,那么将其转换为子类的指针(或引用)则是安全的。

使用C风格的强制类型转换进行向下转型是不安全的,

  • 因为此时无论父类的指针(或引用)指向的是父类对象还是子类对象都会进行转换

而使用dynamic_cast进行向下转型则是安全的,

  • 如果父类的指针(或引用)指向的是子类对象,那么dynamic_cast会转换成功
  • 但如果父类的指针(或引用)指向的是父类对象,那么dynamic_cast会转换失败并返回一个空指针

你可能感兴趣的:(c++,开发语言)