网站简繁转换,关键词排名代做,大连app开发定制,代理公司注册哪家好摘要#xff1a;本文首先介绍 C 的内存模型和变量周期作为知识背景#xff0c;接着对C中的引用和指针#xff08;原始指针和智能指针#xff09;进行介绍。
1. 对象生命周期
什么是对象生命周期#xff1f;简单来说#xff0c;对象生命周期指的是#xff1a;对象从创建…摘要本文首先介绍 C 的内存模型和变量周期作为知识背景接着对C中的引用和指针原始指针和智能指针进行介绍。
1. 对象生命周期
什么是对象生命周期简单来说对象生命周期指的是对象从创建直到被释放的时间跨度。很自然地我们会意识到不是所有变量的创建方式和释放时间都是一样的据此我们把对象的生命周期氛围四种类型静态存储周期、线程存储周期、自动存储周期和动态存储周期。
静态存储周期 Static Storage Duration
静态存储周期类型的对象在程序执行开始的时候就会分配内存直至整个程序结束了才会释放。主要包括全局变量、静态的类数据成员和函数中的局部变量如下例子所示
// 1. 全局变量
int global_var 10;
// 2. 类中的静态数据成员
class MyClass{static int static_var_class;
}
// 3. 函数中的静态局部变量
int myFunc(){static int static_var_func;
}线程存储周期 Thread Storage Duration
线程存储周期类型的对象只会在指定的线程内进行内存的分配与释放。在多线程编程中为了避免数据紊乱可以使用该方法当然以下提及的自动存储周期严格来说也能算是线程周期只不过这个线程是代码默认的主线程因此不需要额外标注。如下例子展示如何使用线程存储周期的变量
thread_local int thread_var;自动存储周期 Automatic Storage Duration
自动存储周期类型的对象只会存在于其被声明和定义的作用域内一旦退出作用域则自动释放如函数的参数或者其内部定义的局部变量。这是最常见或者说默认的定义类型不需要额外的关键字以下例子展示的是函数内部定义的局部变量
void myFunction() {int local_var; // Automatic storage duration
}动态存储周期 Dynamic Storage Duration
动态存储周期类型的对象在程序执行的时候通过关键字 new 或 malloc 实时分配内存直至整个作用域退出也不会自动释放内存必须通过使用关键字 delete 或 free 函数进行手动释放否则会造成内存泄漏的问题。如下例子展示如何分配和释放一个原始指针变量
int* ptr new int; // 定义一个原始指针
delete ptr; // 释放原始指针1*. 补充内存泄漏
内存泄漏指的是程序从堆分配内存但是不把内存释放到操作系统中导致内存耗尽或者程序奔溃。以下一个例子展示内存泄漏
void memory_leak(){int* ptr new int[100]; // 创建一个原始指针并指向一个整型数组// 其他功能代码 ...// 确实手动释放内存操作 delete[] ptr;
} // 导致内存泄漏函数作用域结束了ptr 指针已经没用了但是没有释放内存除了通过使用关键字 delete 或 free 函数进行手动释放外还可以通过智能指针、RAIIResource Acquisition Is Initialization和C标准库的容器vector来进行自动释放。
2. 内存模型
为什么需要了解C的内存模型内存模型定义了如何在C去存储和使用数据与变量的生命周期对应了解C的有利于优化对内存资源的使用和整个程序的表现。
C的数据模型主要包括四个部分栈、堆、数据段和代码段。
栈内存
自动生命周期的变量如函数参数或局部变量都是使用栈的形式进行存储的。栈内存通过编译器进行管理可以实现自动分配和释放。根据栈 先进后出 的特定很容易知道最后定义的变量其实是最先释放的。
堆内存
堆内存则被用于动态生命周期变量如通过 new 关键字手动定义的对象。根据堆 logn 的查找复杂度特定很容易知道由堆内存管理的对象存储空间更大但查找速度更快。
数据段
数据段包括两个部分初始化数据段和未初始化数据段。两者的区别在于是否在变量声明的同时做定义数据段主要包括全局变量、静态变量和常变量。以下例子进行简单解释
// 初始化数据段
int global_var 10; // 全局变量
static int static_var 20; // 静态变量
const int const_var 30; // 常变量// 未初始化数据段
int global_var2; // 只是声明了变量但是没有对取值进行初始化定义代码段
代码段也成为文本段用于存储程序的可执行代码机器语言通常存在仅读的内存防止被意外修改
3. 引用 reference
引用常跟别名联系在一起变量的引用和变量本身共享同一块内存修改变量的引用时变量本身的取值也会发生改变通俗来说两者只是一个对象的两个名字而已故称为别名。在另一个角度引用操作可以看作一个常指针一旦这个常指针存储了地址这个地址是无法修改的。
引用的声明与初始化见例子
int raw 10; // 原始变量
int ref raw; // 创建一个引用变量 ref 指向引用变量 raw
raw 20;
std::cout ref is: ref endl; // 修改原数据的取值引用变量的取值也会发生变换反之亦然引用作为函数参数浅拷贝/地址传递
void swap(int a, int b) {int temp a;a b;b temp;
}int main() {int x 5, y 10;cout Before Swap: x x y y endl; // Outputs 5 10swap(x, y);cout After Swap: x x y y endl; // Outputs 10 5
}4. 指针
指针本质上也是一个变量只是这个变量存储的是另外一个变量/函数的地址/首地址。
4.1 原始指针
原始指针指的是直接存储其他低层次数据的地址的指针如 int/float/char 、数组等。可以通过关键字 new/new[] 来创建指针通过关键字 delete/delete[] 来释放内存如下面例子所示
int* ptr_int new int; // 创建整型指针
float* ptr_array new float[100]; // 创建浮点型数组for(i0; i100;i){ptr_array[i] i; // 数组指针的使用
}delete[] ptr_array; // 释放数组指针
delete ptr_int; // 释放整型指针4.2 智能指针
智能指针相对原始指针一个显著的区别在于智能指针不需要手动释放。智能指针主要包括unique指针、shared指针和weak指针。
4.2.1 unique指针
unique指针可通过 std::unique_ptr 标准库创建本质上是一个用于管理单个对象或者数组的模板类。
unique顾名思义唯一的。它表示每个对象/数组只能被一个unique指针所指这个对象/数组可以可以通过所有权的转移实现被另一个unique指针所指但是无法同时被两个unique指针所指。
为什么需要unique指针
unique指针具有避免悬垂指针、减少内存泄漏和避免手动释放内存的好处 对于原始指针手动释放内存可以避免内存泄漏这个在1.1*有提及到。那么指针悬空指针是什么悬空指针的定义是指针最初指向的内存已经被释放了的一种指针其所指向的地址存储的值是无法预测的随机值以下举例说明出现指针悬空的情形。而在unique指针根本就不存在 delete unique_p1语句也不存在两个unique指针指向同一个变量故而指针悬空的问题。
#includeiostreamvoid func(int* p){// 局部作用域int var 5;p var;std::cout *p std::endl; // 输出5
}int main(){// 情形1指针所指的内存被释放则指针悬空返回值无法估计int* p1 new int;*p1 5;std::cout *p1 std::endl; // 输出5delete p1;std::cout *p1 std::endl; // 输出-1152576448// 情形2两个指针指向同一块内存其中指针所指的内存被释放则另外一个指针悬空返回值无法估计int* p2 new int;int* p3 new int;*p2 5;p3 p2;std::cout *p3 std::endl; // 输出5delete p2;std::cout *p3 std::endl; // 输出-2100685696// 情形3指针所指向的局部变量退出作用域则局部变量被自动释放指针悬空int* p4 new int;func(p4);std::cout *p4 std::endl; // 输出-1163005939
}接下来就是 unique指针 如何使用包括如何创建 unique 指针如何转移变量的所有权以及如何自定义删除智能指针
#includeiostream
#includememory // 1. 引入 memory 头文件int main(){// 2. 初始化变量创建unique指针指向整型变量的两种方式std::unique_ptrint p1(new int(666)); // 所指内存存储取值为666的整型变量std::unique_ptrint p2 std::make_uniqueint(999); // 更常用std::cout *p1 , *p2 std::endl; //输出 666 999// 3. 创建数组创建unique指针指向整型数组的两种方式std::unique_ptrint[] p3(new int[10]); // 长度为10的整型数组数组取值未初始化std::unique_ptrint[] p4 std::make_uniqueint[](10); // 更常用for(int i0;i10;i){p3[i] i;p4[i] i;std::cout p3[i] , p4[i] std::endl;}// 4. 变量所有权的转移std::unique_ptrint p5 std::move(p1); // p5 拥有变量而指针 p1 自动销毁if(p1){std::cout p1 owns the object std::endl;}else if (p5){std::cout p5 owns the object std::endl; // 输出p5 owns the object}// 5. 自定义析构函数智能指针默认会自动销毁但是也可以自定义销毁方法struct MyDeleter{void operator()(int* ptr){std::cout Custom Deleter: Deleting pointer std::endl;delete ptr;}};// std::unique_ptrint, MyDeleter p6 std::make_uniqueint, MyDeleter(999, MyDeleter()); 使用此方法自定义析构函数会报错std::unique_ptrint, MyDeleter p7(new int(999), MyDeleter());return 0; //主函数结束后自动调用 MyDeleter() 删除指针输出Custom Deleter: Deleting pointer
}4.2.2 shared 指针
unique 指针提到每个对象/数组只能被一个unique指针所指这个对象/数组可以可以通过所有权的转移实现被另一个unique指针所指但是无法同时被两个unique指针所指。
那么很自然的一个想法就是存不存在一种智能指针可以实现多个指针指向同一个变量 答案是可以的这种指针就是 shared 指针。
那么这种多个指针指向同一个变量智能指针会导致什么问题吗 很自然的一个问题就是被多个指针所指的变量什么时候才会销毁举个例子就是10个shared指针指向同一个变量那么其中的一个或两个指针销毁后被指的变量还在不在当然为了避免悬空指针我们通过希望的是所有指针销毁后变量才被销毁。为了实现这个直观的想法不得不引入一个引用计数的概念因此每当增加一个shared指针指向变量引用计数 1当引用计数等于0的时候证明已经没有指针指向该变量了该变量就可以自动销毁了。
那么我们下面用两个智能shared指针指向类对象来说明引用计数的使用方法
#includeiostream
#includememory // 1. 引入 memory 头文件// 2. 定义一个类
class MyClass{public:// 类里面只有构造函数和析构函数通俗来说就是在对象的创建和销毁时就会调用该函数// 我们在构造函数和析构函数print一些内容就可以知道被shared指针所指的对象何时创建/销毁MyClass(){ std::cout Object is Constructed ! std::endl;};~MyClass(){ std::cout Object is Destructed ! std::endl;};
};int main(){std::shared_ptrMyClass p1(new MyClass());{// 以下进入局部作用域std::shared_ptrMyClass p2 p1; // 类对象同时被 p1 和 p2 所指引用计数为 2std::cout Inside the inner scope. std::endl;// 退出局部作用域}// 退出局部作用域引用计数减少为 1std::cout Outside the inner scope. std::endl;
}该段程序依次打印的内容是
Object is Constructed !
Inside the inner scope.
Outside the inner scope.
Object is Destructed !Destructed 语句在 Outside the inner scope语句之后证明了只有引用计数为0才会销毁变量
4.2.3 weak 指针
弱指针也叫做弱智能指针顾名思义也就是处于智能指针和原始指针的指针类型。它能够处理 share 指针 中存在的循环引用的问题。
很自然什么是循环引用呢以下举个例子说明
#includeiostream
#includememory // 1. 引入 memory 头文件// 2. 定义一个类
class MyClassB;
class MyClassA{public:MyClassA(){ std::cout Object A is Constructed ! std::endl;};~MyClassA(){ std::cout Object A is Destructed ! std::endl;};std::shared_ptrMyClassB pB;
};
class MyClassB{public:MyClassB(){ std::cout Object B is Constructed ! std::endl;};~MyClassB(){ std::cout Object B is Destructed ! std::endl;};std::shared_ptrMyClassA pA;
};
int main(){// while(True) // 3. 如果添加 while 循环会造成内存验证泄漏可能导致死机欢迎试一试{// 以下进入局部作用域// 4. 创建两个 shared 指针std::shared_ptrMyClassA p1(new MyClassA()); // 对象 A 的引用计数为 1std::shared_ptrMyClassB p2(new MyClassB()); // 对象 B 的引用计数为 1// 两个指针的成员函数互相指向对方, - 表示取成员变量p1-pB p2; // 对象 B 的引用计数为 2p2-pA p1; // 对象 A 的引用计数为 2std::cout Inside the inner scope. std::endl;// 退出局部作用域智能指针无法自动释放}std::cout Outside the inner scope. std::endl;
}该段程序依次打印的内容是
Object A is Constructed !
Object B is Constructed !
Inside the inner scope.
Outside the inner scope.Destructed 语句在退出局部作用域之后(Inside the inner scope之后Outside the inner scope.之前)并没有打印证明了循环引用导致退出局部作用域后引用计数仍为2不会销毁变量导致内存泄漏如此一来A和B都互相指着对方吼“放开我的引用““你先发我的我就放你的”于是悲剧发生了。
那么接下来的问题就是什么是 weak 指针它是如何避免内存泄漏的 weak 指针和 shared 指针的显著区别在于 weak 指针不会增加被指对象的引用计数。这能保证当一个 shared 指针和一个weak指针同时指向一个对象时只要 shared 指针退出作用域后这个对象就会被自动销毁。
那么 weak 指针的使用方法和 shared 指针有什么区别如何去使用 weak 指针 在使用 weak 指针时必须要使用 lock() 函数基于weak指针创建一个新的 shared 指针然后在新的作用域里面安全地使用这个新的 shared 指针以下我们将基于上述循环引用的例子将类A的shared指针转化为weak指针讲述如何避免内存泄漏的。
#includeiostream
#includememory // 1. 引入 memory 头文件// 2. 定义一个类
class MyClassB;
class MyClassA{public:MyClassA(){ std::cout Object A is Constructed ! std::endl;};~MyClassA(){ std::cout Object A is Destructed ! std::endl;};std::weak_ptrMyClassB pB; // 使用弱指针
};
class MyClassB{public:MyClassB(){ std::cout Object B is Constructed ! std::endl;};~MyClassB(){ std::cout Object B is Destructed ! std::endl;};std::shared_ptrMyClassA pA;void DoSomething(){std::cout Doing something... std::endl;}
};
int main(){// while(True) // 3. 如果添加 while 循环会造成内存验证泄漏可能导致死机欢迎试一试{// 以下进入局部作用域// 4. 创建两个 shared 指针std::shared_ptrMyClassA p1(new MyClassA()); // 对象 A 的引用计数为 1std::shared_ptrMyClassB p2(new MyClassB()); // 对象 B 的引用计数为 1// 两个指针的成员函数互相指向对方, - 表示取成员变量p1-pB p2; // weak指针不增加引用计数对象 B 的引用计数为 1p2-pA p1; // 对象 A 的引用计数为 2if (auto shareFromWeak p1-pB.lock()){ // 通过lock函数创建新的 shared 指针shareFromWeak-DoSomething(); // 在新的局部作用域里面安全间接调用std::cout Shared uses count: shareFromWeak.use_count() std::endl; //此时对象 B 的引用计数为 2}// 退出 if 函数的局部作用域shareFromWeak指针自动释放对象 B 的引用计数变为 1std::cout Inside the inner scope. std::endl;// 退出局部作用域智能指针自动释放}std::cout Outside the inner scope. std::endl;
}该段程序依次打印的内容是
Object A is Constructed !
Object B is Constructed !
Doing something...
Shared uses count: 2
Inside the inner scope.
Object B is Destructed !
Object A is Destructed !
Outside the inner scope.可以看到退出局部作用域后对象A和对象B都得到了释放也就是说避免了循环引用导致的内存泄漏。
4. 总结
在本博客中为了讲述 智能指针这一个概念我们首先铺垫了一些基础知识例如变量的声明周期和C的内存模型这对于理解内存的释放和局部作用域等概念非常有用。接着我们快速地介绍了引用和原始指针针对原始指针的内存管理释放和泄漏问题我们进一步解释了智能指针这包括 unique, shared 和 weak指针。其中我们着重地介绍了shared因为循环引用导致的内存泄漏问题以及如何使用weak指针避免这个循环引用使得智能指针能够正确释放