1.悬挂指针和野指针
悬挂指针:指向已经被释放的内存区域或者指向无效的内存区域
野指针: 指向未知内存区域,或者指针本身未分配内存地址
悬挂指针和野指针都会导致程序出现错误,产生的原因不一样,是两个概念
1.1 悬挂指针
悬挂指针通常由于下面几个原因产生
- 指针指向的内存被释放后,未将指针置空(使用delete或者free等释放内存后,没有将指针设置为NULL), 此时如果尝试通过该指针访问已经释放的内存,将会导致未定义行为,如程序崩溃或数据损坏
- 指针指向函数内部的局部变量(栈上变量),该函数执行完,局部变量所占用内存会被回收。如果外部仍有指针指向这个局部变量,这个指针就会变成悬挂指针
悬挂指针可能会导致严重的错误和未定义的行为。当尝试通过悬挂指针解引用或访问其指向的内存时,可能会访问到无效的内存区域,导致程序崩溃、数据损坏或其他不可预测的行为
情况一:
#include <iostream>
using namespace std;
int main()
{
int *p = new int(5);
cout<<"*p = "<<*p<<endl;
cout<<"p 地址:"<<p<<endl;
free(p); // p 在释放后成为悬空指针
cout<<"p 地址:"<<p<<endl;
cout<<"*p = "<<*p<<endl;
p = NULL; // 非悬空指针
return 0;
}
输出结果:
linuxy@linuxy:~/wildPointer$ ./main
*p = 5
p 地址:0x55a885ef6eb0
p 地址:0x55a885ef6eb0
*p = 0
linuxy@linuxy:~/wildPointer$
p 指针在被 free 后,成为悬空指针,被 NULL 赋值后不再是悬空指针。
注意:这里 free 掉的是 p 的内存空间,并不是变量 p
free 前后 p 的地址是不变的,free 释放的是 p 指向的内存空间,释放后表示该快内存可以重新分配了,至于 free 后 *p 的值,视不同编译器情况而不同
情况二:
#include <iostream>
using namespace std;
int* getVal() {
int tmp = 10;
return &tmp;
}
int main()
{
int *p = getVal(); //悬空指针
cout<<"*p = "<<*p<<endl;
return 0;
}
在函数 getVal 执行完后,局部变量的内存空间会被释放,而这里 p 指向了函数内的局部变量,p 便成为了悬空指针,可以将 tmp 变为 static 的
1.2 野指针
一般是指针没有初始化
#include <iostream>
using namespace std;
int main()
{
int *p; // 野指针
int *q = NULL; // 非野指针
p = new int(5); // p 现在不再是野指针
q = new int(10);
cout<<"*p = "<<*p<<endl;
cout<<"*q = "<<*q<<endl;
free(p);
free(q);
return 0;
}
代码出自下面
原文链接:https://blog.csdn.net/nyist_zxp/article/details/119478944
2. 内存泄漏
内存泄露的是分配的内存空间在不再使用时未被正确释放,导致这些内存无法再被程序访问到,同时仍然占用系统资源。内存泄露的本质是对内存的浪费。
防止办法
- new和delete, malloc和free成对出现
- 智能指针
- 内存泄漏检测工具
3. 智能指针
由于原始指针忘记删除,或者释放内存没有将其置空,导致悬空指针,会产生程序崩溃或者其他不良后果。特别是函数内部有异常,delete的语句执行不到,这种情况就比较复杂
智能指针就可以方便我们控制指针对象的生命周期。在智能指针中,一个对象什么情况下被析构或被删除,是由指针本身决定的,并不需要用户进行手动管理,自动释放new出来的内存,从而避免内存泄漏!
- 智能指针解决的问题
- 自动内存管理:智能指针(如 std::unique_ptr、std::shared_ptr 和 std::weak_ptr)在析构时会自动释放所管理的内存,从而防止内存泄漏。
- 异常安全:当异常发生时,智能指针能够确保即使在构造函数或析构函数抛出异常的情况下,也能正确释放内存。
- 提供共享和独占的语义:std::unique_ptr 提供独占所有权的语义,即同一时间只能有一个智能指针指向同一个对象。std::shared_ptr 提供共享所有权的语义,允许多个智能指针共享同一个对象的所有权,并能够跟踪引用计数,自动释放内存。
- 与标准容器兼容:智能指针可以与标准容器(如 std::vector、std::list 等)一起使用,使得容器中的对象也可以自动管理内存
- 智能指针是什么?
智能指针是一个具有生命周期的对象,每种智能指针都是通过类模板的方式实现的, 就是对普通指针进行封装,通过重载 * 和 ->两个运算符,使得智能指针表现的就像普通指针一样
- RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。本质是使用栈对象管理资源,对象离开作用域时,会自动调用析构函数
实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源(对象在析构时会自动释放所管理的内存)
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
智能指针只能管理动态开辟的堆区上的内存(delete)
3.1 unique_ptr
是一个不共享的智能指针, 独占对象的所有权,两个指针不能指向同一资源。由于没有引用计数,性能较好。
- 基于排他所有权模式:两个指针不能指向同一个资源
- 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
- 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
- 在容器中保存指针是安全的
unique_ptr 是 C++ 11 提供的用于防止内存泄漏的智能指针中的一种实现,独享被管理对象指针所有权,同一个时间只允许一个智能指针管理动态开辟的内存
unique_ptr对象包装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。
#include <memory>
//unique_ptr 定义
// 单个对象 non-specialized
template <class T, class D = default<T>> class unique_ptr;
// 数组 array specialization
template <class T, class D> class unique_ptr<T[], D>;
Unique_ptr创建和使用
1. 使用构造函数初始化
std::unique_ptr<T> ptr(new T);
std::unique_ptr<int> ptr(new int())
#include <iostream>
#include <memory>
using namespace std;
int mian()
{
unique_ptr<T> ptr(new T); //指针指向T类型,开辟一个T类大小的空间
unique_ptr<int> p; // 可以指向int对象的一个空智能指针
unique_ptr<int> p(new int(105)); // p指向一个值为105的int对象
auto p(new int(105)); // 不能简写为auto,这里auto推断出是普通指针
unique_ptr<int> p1 = unique_ptr<int>(new int()); //临时对象
int *p = new int;
unique_ptr<int> p1(p); //这样会导致两个指针指向同一空间出错
}
struct Test1 {
int x;
double y;
};
struct Test2 {
virtual void GetT(int* T) = 0; //纯虚函数
};
int main() {
// 使用std::unique_ptr动态分配Test1对象
std::unique_ptr<Test1> ptr(new Test1{22, 2.22});
// 使用ptr
std::cout << ptr->x << ", " << ptr->y << std::endl;
std::unique_ptr<Test2> ptr1 = nullptr;
ptr1->GetT(5);
}
unique_ptr不允许复制, 但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它原本就不再拥有原来指针的所有权了
unique_ptr<T> ptr(new T); // 正确
unique_ptr<T> ptr1 = std::move(ptr); // 正确,运行结果ptr指向空,ptr1指向T
unique_ptr<T> ptr2 = ptr; // 报错,不能复制
2. 使用make_unique()函数模板
C++14才有的make_unique, make_unique不支持定制删除器
auto upw1(std::make_unique<Widget>()); // with make func
std::unique_ptr<T> ptr = make_unique<T>(); //不传入参数,创建一个指向一块T类型的指针空间
std::unique_ptr<T []> ptr = make_unique<T []>(5); //传入参数5,创建一个指向T类型的连续空间,5表示对应的个数
make_unique()的优点
不传入参数时,使用第一种方法(直接使用构造函数)创建的unique_ptr是指向nullptr的,不能直接赋值改变指向,而make_unique()不传入参数时,会分配一块地址给指针,并不是nullptr
提高了代码的安全性
3.2 shared_ptr
有时候多线程处理时需要对资源的共享,多个指针需要指向同一地址,unique_ptr就不适用于这种情况
shared_ptr的思路是记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么就释放
1.使用构造函数初始化
#include <iostream>
#include <memory>
class T {};
int main()
{
std::shared_ptr<T> f(new T()); // 裸指针直接初始化
2. 移动构造和拷贝构造
不同于unique_ptr,shared_ptr可以使用拷贝构造和拷贝赋值,unique_ptr不可以
#include <iostream>
#include <memory>
class T {};
int main()
{
std::shared_ptr<T> ptr(new T()); // 裸指针直接初始化
//std::shared_ptr<T> ptr1 = new T(); // Error,explicit禁止隐式初始化
std::shared_ptr<T> ptr2(ptr); // 拷贝构造函数
std::shared_ptr<T> ptr3 = ptr; // 拷贝构造函数
ptr2 = ptr; // copy赋值运算符重载
std::cout << ptr3.use_count() << " " << ptr3.unique() << std::endl;
std::shared_ptr<T> ptr4(std::move(new T())); // 移动构造函数
//std::shared_ptr<T> ptr5 = std::move(new T()); // Error,explicit禁止隐式初始化
std::shared_ptr<T> ptr6(std::move(ptr4)); // 移动构造函数
std::shared_ptr<T> ptr7 = std::move(ptr6); // 移动构造函数
std::cout << ptr7.use_count() << " " << ptr7.unique() << std::endl;
std::shared_ptr<T[]> ptr8(new T[10]()); // Error,管理动态数组时,需要指定删除器
std::shared_ptr<T> ptr9(new T[10](), std::default_delete<T[]>());
return 0;
}
3. 通过make_shared()构造
shared_ptr很好地包装了new表达式,但过多的显式new操作符也是个问题,至少使用两次new,并且内存不连续,make_shared会一次性申请一整块内存,一块内存给引用计数块使用,一块内存给源结构体使用
make_shared优点
用make_shared()内部仅分配一次内存,减少了堆内存的申请操作,性能有所提升,同时申请的内存是连续的,连续访问引用计数块和原结构体时,由于局部性原理,性能也会有提升
make_shared()函数可以接受最多10个参数,然后把它们传递给类型T的构造函数,创建一个shared_ptr<T>的对象并返回。
1.demo_01.cpp
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class StructA{
public:
int i;
string str;
//StructA(): i(100/*int()*/){//初始化列表
StructA(int i){
this->i = i;
cout<< "StructA(int), line = "<< __LINE__<< endl;
};
StructA(string str){
this->str = str;
cout<< "StructA(string), line = "<< __LINE__<< endl;
};
~StructA(){ cout <<"~StructA "<< endl;};
void show(){
cout << "show() is Called. line = "<< __LINE__<< endl;
}
} ;
int main(int argc, const char * argv[]){
//make_shared自动申请对象及内存,自动释放,不用手动执行new和delete
shared_ptr<StructA> pA = make_shared<StructA>(123);
cout << "pA->i = " << pA->i << endl;
shared_ptr<StructA> pB = make_shared<StructA>("Hello Kitty!");
cout << "pB->str = " << pB->str << endl;
pB->show();
auto t = make_shared<StructA>("Number One!");
cout << "t->str = " << t->str << endl;
t->show();
return 0;
}
2.demo_02.cpp
#include <iostream>
#include <vector>
#include <map>
#include <memory>
using namespace std;
class StructA{
public:
int i;
//StructA(): i(100/*int()*/){//初始化列表
StructA(int i){
this->i = i;
cout<< "StructA(int), line = "<< __LINE__<< endl;
};
~StructA(){ cout <<"~StructA "<< endl;};
void show(){
cout << "show() is Called. line = "<< __LINE__<< endl;
}
template<class T>
T add(T x){
return x;
}
} ;
int main(){
map<int, shared_ptr<StructA>> mStrA;
//插入map映射key-value数据
mStrA[0] = make_shared<StructA>(123);
mStrA[1] = make_shared<StructA>(456);
mStrA.insert(pair<int, shared_ptr<StructA>>(2,make_shared<StructA>(789)));
mStrA.insert(pair<int, shared_ptr<StructA>>(3,make_shared<StructA>(333)));
mStrA[0]->show();
//遍历方式1
for(auto &mA : mStrA){
cout << mA.first << " " << mA.second->i << endl;
//mA.second->show();
}
cout << endl;
//遍历方式2
map<int, shared_ptr<StructA>>::iterator iter;
for(iter = mStrA.begin(); iter != mStrA.end(); iter++){
cout << iter->first << " "<<iter->second->i<<endl;
//iter->second->show();
}
return 0;
}
4.make系列函数的局限:
- 如果想要创建的对象没有公有的构造函数,make_shared就无法使用
- 所有的make系列函数都不允许自定义删除器;
- make系列函数创建对象时,不能接受{}初始化列表(这是因为完美转发的转发函数是个模板函数,它利用模板类型进行推导。因此无法将{}推导为initializer_list)。换言之,make系列只能将圆括号内的形参完美转发;
- 自定义内存管理的类(如重载了operator new和operator delete),不建议使用make_shared来创建。因为:重载operator new和operator delete时,往往用来分配和释放该类精确尺寸(sizeof(T))的内存块;而make_shared创建的shared_ptr,是一个自定义了分配器(std::allocate_shared)和删除器的智能指针,由allocate_shared分配的内存大小也不等于上述的尺寸,而是在此基础上加上控制块的大小;
- 对象的内存可能无法及时回收。因为:make_shared只分配一次内存,减少了内存分配的开销,使得控制块和托管对象在同一内存块上分配。而控制块是由shared_ptr和weak_ptr共享的,因此两者共同管理着这个内存块(托管对象+控制块)。当强引用计数为0时,托管对象被析构(即析构函数被调用),但内存块并未被回收,只有等到最后一个weak_ptr离开作用域时,弱引用也减为0才会释放这块内存块。原本强引用减为0时就可以释放的内存, 现在变为了强引用和弱引用都减为0时才能释放, 意外的延迟了内存释放的时间。这对于内存要求高的场景来说, 是一个需要注意的问题。
3.3 weak_ptr
1.循环引用
在shared_ptr的使用过程中,当强引用计数为0是,就会释放所指向的堆内存。那么问题来了,如果和死锁一样,当两个shared_ptr互相引用,那么它们就永远无法被释放了
2. 原理
weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。从这个角度看,weak_ptr更像是shared_ptr的一个助手而不是智能指针。
3.初始化
- 通过构造函数直接初始化,也可以通过隐式转换
- 移动构造和拷贝构造
#include <iostream>
#include <memory>
class Frame {};
int main()
{
std::shared_ptr<Frame> f(new Frame());
std::weak_ptr<Frame> f1(f); // shared_ptr直接构造
std::weak_ptr<Frame> f2 = f; // 隐式转换
std::weak_ptr<Frame> f3(f1); // 拷贝构造函数
std::weak_ptr<Frame> f4 = f1; // 拷贝构造函数
std::weak_ptr<Frame> f5;
f5 = f; // 拷贝赋值函数
f5 = f2; // 拷贝赋值函数
std::cout << f.use_count() << std::endl; // 1
return 0;
}
如何判断weak_ptr指向对象是否存在?
既然weak_ptr并不改变其所共享的shared_ptr实例的引用计数,那就可能存在weak_ptr指向的对象被释放掉这种情况。这时,就不能使用weak_ptr直接访问对象。那么如何判断weak_ptr指向对象是否存在呢?C++中提供了lock函数来实现该功能。如果对象存在,lock()函数返回一个指向共享对象的shared_ptr(引用计数会增1),否则返回一个空shared_ptr。weak_ptr还提供了expired()函数来判断所指对象是否已经被销毁。
由于weak_ptr并没有重载operator ->和operator *操作符,因此不可直接通过weak_ptr使用对象,同时也没有提供get函数直接获取裸指针。典型的用法是调用其lock函数来获得shared_ptr示例,进而访问原始对象。
代码和部分内容出自下面

3313

被折叠的 条评论
为什么被折叠?



