【C++11新特性】智能指针,右值引用,移动语义与完美转发,函数对象...
C++11新特性
- 智能指针
- 左值与右值及右值引用
- move移动语义 与 forward完美转发
- C++类型转换
- 函数模板与模板函数
C++11新特性概述:
auto
:自动推导变量的类型,可以根据右值,推导出右值的类型,然后左边的类型也就已知了nullptr
:给指针专用(能够和整数进行区分),#define NULL 0- 右值引用: 通过右值引用和移动构造函数实现。能够高效的转移资源拥有权,避免不必要的复制和内存分配。临时变量在拷贝构造,赋值构造函数减少内存开辟与释放。
- 移动语义: 通过右值引用和移动构造函数实现,用于高效地转移资源拥有权,避免不必要的复制和内存分配。
- 智能指针: 自动管理资源,weak_ptr多线程的线程安全问题(观察,lock提升)
- labbda表达式: 匿名函数的语法,更简洁的、具有局部作用域
- 容器: unordered_set、unordered_map,高贵的哈希实现,O(1)
- 语言级别的多线程
智能指针
- 利用栈上的对象出作用域自动析构这个特点,在智能指针的析构函数中保证释放资源
- 自定义删除器
unique_ptr<int, function<void (int*)>> ptr1(new int[10], [](int *p)->void{std::cout << "in intDelteor" << endl;delete[] p; });
std::unique ptr<T>
(独占指针)
- 不能被复制,只能移动(std::move)
- 适用于独占资源管理(如文件、网络连接)。
- 用
std::make_unique<T>(args...)
创建(C++14+)
std::shared ptr<T>
(共享智能指针)
- 共享所有权,采用 引用计数,多个
shared_ptr
可共享同一对象,最后一个销毁时释放资源。 - 存在循环引用风险,可配合
std::weak_ptr
解决。 - 用
std::make_shared<T>(args...)
创建,减少内存分配开销
std::weak ptr<T>
(弱引用)
- 依赖
shared_ptr
,不会增加引用计数 - 用于解决
shared_ptr
循环引用问题 - 他只是个观察者,只能观察,但不能使用,但可通过
lock()
提升shared_ptr
,然后判断对象是否仍然有效,可使用对象 - 主要用于多线程的线程安全,通过引用wark_ptr,在线程中监测对象是否存活,并调用对象的方法。
- 一般定义对象的时候使用强智能指针,引用对象的时候使用弱智能指针
二、std::shared_ptr 原理是什么?,请手动实现
- 每个
shared_ptr
实例指向同一对象的其他 shared_ptr,实例共享一个计数器 - 当创建新
shared_ptr
或拷贝现有shared_ptr
时,计数器增加。 - 当
shared_ptr
被销毁(例如通过析构函数)或重置(通过 reset 方法)时,计数器减少。 - 当计数器为零时,对象被自动删除,通常通过调用
delete
操作符。
这种机制确保了对象在最后一个 shared ptr 销毁时被释放,防止了内存泄漏。特别适合多线程或复杂数据结构中需要共享对象的场景。
shared_ptr代码实现:
#include <iostream>
#include <atomic>using namespace std;// 共享引用计数类
template<typename T>
class SharedCount{
private:T* ptr; // 指向同一对象的指针atomic_int count; // 引用计数SharedCount(const SharedCount& ) = delete;SharedCount& operator=(const SharedCount& ) = delete;
public:SharedCount(T *p) : ptr(p),count(1) {}~SharedCount() {delete ptr;}// 增加引用计数void increment(){count++;}// 减少引用计数void decrement(){if(--count == 0)delete this;}T* get() const {return ptr;}
};template<typename T>
class shared_ptr{
private:T* ptr; // 指向管理的对象SharedCount<T>* countPtr; // 指向引用计数类对象
public:// 构造函数shared_ptr(T *p = nullptr) : ptr(p) {if(p) {countPtr = new SharedCount<T>(p);} }// 拷贝构造shared_ptr(const shared_ptr& other) : ptr(other.ptr), countPtr(other.countPtr) {if(countPtr)countPtr->increment();}// 移动构造shared_ptr(shared_ptr&& other) : ptr(other.ptr), countPtr(other.countPtr) {// 清空原内存other.ptr = nullptr;other.countPtr = nullptr;}~shared_ptr() {if(countPtr)countPtr->decrement();}T* operator->() const{return ptr;}T& operator*() const{return *ptr;}void reset(T *p = nullptr) {if(p == ptr) return ;if(countPtr)countPtr->decrement();ptr = p;if(p) {countPtr = new SharedCount<T>(p);}elsecountPtr = nullptr;}T* get() {return ptr;}
};int main()
{shared_ptr<int> ptr1(new int(10));shared_ptr<int> ptr2 = ptr1;cout << "ptr1: " << *ptr1 << endl;cout << "ptr2: " << *ptr2 << endl;ptr1.reset();cout << "ptr2: " << *ptr2 << endl;shared_ptr<int> ptr3 = move(ptr2);// cout << "ptr2: " << *ptr2 << endl;cout << "ptr3: " << *ptr3 << endl;
}
三、make_shared 相 比 shared_ptr(newT(args…))有什么好处?
1. 避免额外的内存分配
std::make_shared
会在一次内存分配中同时分配对象本体和引用计数,并且是连续的,而std::shared ptr<T>(new T(args...))
需要两次分配(一次给T,一次给shared_ptr
的控制块,而且不连续)。- 这不仅减少了
malloc/free
的开销,还能提高缓存命中率。
2. 减少异常安全问题
std::shared ptr<T>(new T(args...))
是两个独立的操作,new T(args...)
可能会抛出异常,而shared_ptr
还未成功构造,导致内存泄漏。std::make shared
进行的是原子操作,不存在这个问题。
3. 更高效的引用计数管理
- 由于
std::make_shared
在一个内存块中存储对象和引用计数,指针访问时可以减少额外的缓存访问,提高运行效率。 std::shared_ptr<T>(new T(args...))
由于分开分配对象和控制块,会导致额外的指针间接访问。
4.代码更简洁
auto ptr = std::make_shared<T>(args...)
比auto ptr = std::shared ptr<T>(new T(args...))
更简短,可读性更好。
左值与右值及右值引用
左值:
- 具有地址,存储在内存中
- 可是出现在赋值号 = 的左侧
- 可以取地址 &
- 变量、对象、数组元素都是左值
int x = 10; // x 是左值 x = 20; // 左值可以出现在赋值号的左侧 int* p = &x; // 可以取地址 const int&& y = std::move(x); // 现在这个move(x)是右值,触发移动构造,避免深拷贝
右值
- 通常没有地址,存在在寄存器或临时内存中,除非利用move()
- 不能出现在赋值号 = 的左侧
- 不能取地址 & (除非绑定到 const 左值引用)
- 自变量、表达式计算结果都是右值
int y = 10 + 5; // (10 + 5) 就是右值 10 = y; // ❌ 右值不能在 `=` 的左侧 int &red = 10; // ❌ 普通引用不能绑定右值 const int& cref = 10; // ✔️ const 引用可以绑定右值
右值转为左值:
- 通过左值变量:
int a = 10;
- 通过右值引用变量
int&& rref = 10; rref = 20;
- 通过const 左值引用绑定右值,但不可修改:
const int& cref = 10;
左值转为右值:
- 通过move,这时候就有了地址,但左值变成了亡值,亡值可以取地址,但不能赋值
int a = 10; // a 是左值 int&& rref = std::move(a); // 用 std::move 将 a 转为右值引用
左值引用: T& 绑定到左值,可以修改变量,不能绑定右值
右值引用的作用主要体现在以下几个方面:
- 避免不必要的拷贝: 通过标识和操作右值,可以避免在操作临时对象时进行不必要的拷贝操作,提高程序的性能。
- 实现移动语义: 通过右值引用和移动操作,可以在对象的资源拷贝过程中,将资源所有权从一个对象转移给另一个对象,避免了不必要的资源拷贝。
- 支持完美转发: 通过右值引用,可以保持传递参数的值类别,实现参数的完美转发,避免了临时对象的额外拷贝操作。
move移动语义 与 forward完美转发
move的作用:
-
将左值转化为为右值,以触发移动语义(上述案例中)
-
不会真正移动数据,只是改变对象的属性
-
用于触发移动构造和移动赋值,避免深拷贝,提高性能
int main() {// 假设字符串很长std::string str1 = "hello world";std::string str2;str2 = move(str1);std::cout << "str1:" << str1 << std::endl;std::cout << "str2:" << str2 << std::endl; return 0; }
完美转发
- 高效传递参数的方式,保持参数的原始特性(左值或右值),避免不必要的拷贝或移动
std::forward<T>(arg)
通过 引用折叠 和 类型推导 来决定参数是否应该保留右值特性。
为了实现完美转发,通常要使用两个重要的特性:
- 模板类型推导: 函数模板使用模板参数来承载传递的参数,通过类型推导来确定参数的类型。
- 转发引用: 转发引用是指使用 std::forward 函数来将参数转发给其他函数。std::forward 的原理是根据参数的值类别和是否为左值引用来决定将参数转发为左值引用或右值引用。
引用折叠demo:
- 模板推导中,若传入左值
int x
,T
会被推导为int&
,此时T&&
折叠为int&
(左值引用); - 若传入右值
5
,T
会被推导为int
,此时T&&
就是int&&
(右值引用) - std::forward(t) 的内部实现会根据 T 的类型(int& 或 int),分别返回 static_cast<T&>(t)(左值)或 static_cast<T&&>(t)(右值),确保转发后的类型与原始参数一致,避免丢失。
完美转发demo:
void process(int &x) {std::cout << "LVaule reference" << std::endl;
}
void process(int&& x) {std::cout << "RVaule reference" << std::endl;
}
// 泛型函数,使用完美转发
template<typename T>
void forwardExample(T&& arg){ // 万能引用 = 模板 + 万能引用process(std::forward<T> arg);
}
int main(){int a = 10;forwardExample(a);forwardExample(10);return 0;
}
面试考点:
1. forward
和 move
的区别
move
无条件将参数转换为右值引用(T&&),本质是一个类型转换函数(static_cast<T&&>(t))- forward有条件地转发参数的原始类型,仅用于模板参数的完美转发。它会根据模板参数的推导结果,决定将参数转为左值引用还是右值引用
2. T&&是万能引用还是右值引用
- 当 T 是模板参数且发生类型推导时,T&& 是万能引用,可以绑定到左值或右值。
- 其他情况(T 非模板参数,或未发生类型推导),T&& 是右值引用,仅能绑定到右值。
3. 为什么 forward
可以避免参数类型丢失
- 引用折叠
- 模板推导中,若传入左值
int x
,T
会被推导为int&
,此时T&&
折叠为int&
(左值引用); - 若传入右值
5
,T
会被推导为int
,此时T&&
就是int&&
(右值引用) - std::forward(t) 的内部实现会根据 T 的类型(int& 或 int),分别返回 static_cast<T&>(t)(左值)或 static_cast<T&&>(t)(右值),确保转发后的类型与原始参数一致,避免丢失。
4. 如何用 forward
实现高效的 构造函数参数传递
通过完美转发,forward 可将构造函数的参数原封不动地传递给成员变量的构造函数,避免不必要的拷贝(尤其适合临时对象或可移动对象)
C++类型转换
-
static_cast
:一种编译时,静态转化类型,主要用于已知安全的转化,比C风格更安全。- 基础数据类型转换,int,double
- 子类指针转化为基类指针,向上转换,子类多的部分会做对象切割
- 不能向下转化,static_cast 访问不到子类的其他变量,那就需要
dynamic_cast
-
dynamic_cast
- 用于基类与派生类的转化,主要用于向下转化,会检测类型是否安全,例如
Any
类中的Derive
与Base
,Derive *ptr = dynamic_cast<Derive<T>*>(basePtr.get());
- 基类必须有虚函数,否则则 dynamic_cast 无法执行正确的运行时检查,结果将是未定义。
- 用于基类与派生类的转化,主要用于向下转化,会检测类型是否安全,例如
-
const_cast
:如果对象本身不是const
,通过const_cast
去除指针的const
属性,如果本身就是const的话,那是不可以的void modify(const char *str) {char *p = const_cast<char*>(str);p[0] = 'H'; }int main() {char str[] = "hello";modify(str);std::cout << str << std::endl; }
-
reinterpret_cast
重解释转换int main() {int a = 42;//将指针转换为整数uintptr_t addr = reinterpret_cast<uintptr_t>(&a);int* p= reinterpret_cast<int*>(addr);std::cout << "*p " << *p << std::endl; //输出:42 }
函数模板与模板函数
函数模板是一种通用的函数模板声明,其中函数的参数和返回类型可以使用通用的模板参数来表示。函数模板的定义通常以 template 或 template 开始,后跟函数的声明或定义。
下面是一个简单的函数模板的示例:
- 在这个例子中,add 是一个函数模板,它可以接受相同类型的参数 a 和 b,并返回它们的和。
- 模板参数 T 是一个占位符,表示函数中的类型。在函数调用时,编译器会根据实际的参数类型来实例化函数模板。
template<typename T> T add(T a, T b) {return a + b; }int intResult = add(5, 10); // 实例化为 add<int>(5, 10),返回 15 double doubleResult = add(3.14, 2.71); // 实例化为 add<double>(3.14, 2.71),返回 5.85
模板函数
是对特定模板参数进行特化的函数定义。特化是指针对特定的模板参数类型编写的特殊版本。特化函数可以提供对特定数据类型的定制化行为。
下面是一个函数模板特化的示例:
template<typename T>
T max(T a, T b) {return (a > b) ? a : b;
}template<>
const char* max<const char*>(const char* a, const char* b) {return strcmp(a, b) > 0 ? a : b;
}
在这个例子中,max 是一个函数模板,用于比较两个值并返回较大的值。然后,通过模板特化 template<> 来定义 max 函数针对 const char* 类型的特殊版本。这个特殊版本使用了 strcmp 函数来比较两个 C 字符串并返回较大的字符串。
不同点:
- 函数模板是一个通用的模板声明,可以用于多种数据类型,根据实际参数类型来实例化。
- 模板函数是对特定模板参数进行特化的函数定义,提供了对特定数据类型的定制化行为。