lambda表达式书写格式:[捕捉列表] (参数列表) mutable- > 返回值类型 {函数体}
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?
捕获列表说明:
lambda表达式可以用作函数对象,用于在函数或算法中定义短小的、临时的操作。
lambda底层实现原理
实际编译器在底层对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的。函数对象就是我们平常所说的仿函数,就是在类中对()运算符进行了重载的类对象。
本质就是因为lambda表达式在底层被转换成了仿函数。
区别总结如下:
传统的C++语法中就有引用的语法,而C++11中新增了右值引用的语法特性,为了进行区分,于是将C++11之前的引用就叫做左值引用。但是无论左值引用还是右值引用,本质都是给对象取别名。
左值是指具有标识符(变量名)的表达式,可以出现在赋值操作符的左边。左值通常表示具有持久性的、有名字的内存位置,可以被多次引用和修改。例如,变量、对象成员、函数返回的左值引用等都是左值。
右值是指不具有标识符(变量名)的临时表达式,通常表示即将被丢弃的值。右值通常表示临时的、短暂的值,不能被多次引用或修改。例如,常量、临时表达式、字面量、函数返回的右值引用等都是右值。
左值引用可以引用右值吗?
因此const左值引用既可以引用左值,也可以引用右值。
右值引用可以引用左值吗?
左值引用的短板:
左值引用虽然能避免不必要的拷贝操作,但左值引用并不能完全避免不必要的拷贝操作。
如果函数返回的对象是一个局部变量,该变量出了函数作用域就被销毁了,这种情况下不能用左值引用作为返回值,只能以传值的方式返回,这就是左值引用的短板。
右值引用的主要使用场景包括:
在移动构造函数和移动赋值函数中使用右值引用可以避免对象的拷贝和内存的分配,提高效率;
如何减少拷贝?
完美转发是允许你在泛型代码中精确地传递函数参数,并保留原始参数的值类别(左值或右值)。完美转发通常与右值引用一起使用,尤其是在函数模板中,以避免不必要的拷贝和维护类型信息。
为什么需要完美转发?
在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通过引用计数的方式解决智能指针的拷贝问题。
通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。引用计数需要存放在堆区
weak_ptr
shared_ptr的循环引用问题
shared_ptr的循环引用问题在一些特定的场景下才会产生。比如定义俩个list的结点类,俩节点相互指向。
循环引用导致资源未被释放的原因:
解决循环引用问题
将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。
简易版的shared_ptr的实现步骤如下:
为什么引用计数需要存放在堆区?
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; //管理的资源对应的引用计数
};
}
C++中的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变量的值进行修改。
说明一下:
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) {
// 安全的向下转型
}
向上转型与向下转型
向下转型的安全问题:
向下转型分为两种情况:
使用C风格的强制类型转换进行向下转型是不安全的,
而使用dynamic_cast进行向下转型则是安全的,