C++中指针相关问题
本文章主要讲解C++中指针相关问题,包括原始指针、野指针、空悬指针、智能指针(shared_ptr、unique_ptr、weak_ptr)、内存泄漏、循环引用、智能指针与多线程等知识。
原始指针
指针的概念
int *p = new int(3); cout << "p:" << p << endl; // 输出p所占内存空间所保存的值,即堆上的地址 cout << "&p:" << &p << endl; // 输出p所占内存空间的地址 cout << "*p:" << *p << endl; // 输出p所指向的堆上的内存的值,即3
空悬指针
当我们delete一个指针后,指针值就变为无效了。虽然指针值已经无效了,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了空悬指针(dangling pointer)。
在delete指针之后,最好赋值为nullptr
int *p = new int(3); delete p; p = nullptr;
野指针
即未经初始化的指针。若使用该指针,则该指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于访问一个本不存在的位置上的本不存在的对象。
释放问题
- 重复delete某个指针两次,将会是未定义的行为(崩溃Abort),重复释放。
- delete空指针不会有问题。所以再次印证了,释放某个指针之后,要把它赋值为nullptr。
内存耗尽
new的时候,如果内存耗尽,会抛出bad_alloc()异常,但是并不会返回空指针,所以以下代码是错误的
int *pbigarr = new int[5147483647]; if(pbigarr == nullptr) { cerr << "new failure!" << endl; }
如果想要在new失败的时候返回空指针,需要使用定位new,修改为如下:
int *pbigarr = new(nothrow) int[5147483647]; if(pbigarr == nullptr) { cerr << "new failure!" << endl; }
智能指针
shared_ptr
构造
1. 使用make_shared函数进行构造 shared_ptr<int> p = make_shared<int>(42); shared_ptr<string> pstr = make_shared<string>(10, 'c'); 2. 与new结合使用来进行构造 shared_ptr<int> p2(new int(43));
其它接口
// 获取值 cout << *p << endl; // 获取原始指针 cout << p.get() << endl; // 判断p是否是唯一指向该共享对象的指针 cout << std::boolalpha << p.unique() << endl; // 返回指向该共享对象的指针的数量 cout << p.use_count() << endl; // 释放p所指向对象的内存,并令p重新指向p2 p.reset(p2);
指定删除器:
数组
// c++17前不能传递数组类型作为shared_ptr的模板参数 std::shared_ptr<int> sp1(new int[10](), std::default_delete<int[]>()); // c++17后,支持数组,无需提供额外的删除器 std::shared_ptr<int[]> sp1(new int[10]());
其它类型(删除器为可调用对象即可)
// 自定义删除器函数,释放int型内存 void deleteIntPtr(int* p) { delete p; cout << "int 型内存被释放了..."; } shared_ptr<int> ptr(new int(250), deleteIntPtr); // lambda表达式 shared_ptr<int> ptr(new int(250), [](int* p) {delete p; }); // C风格的网络库写法 struct event_base *base = event_base_new(); shared_ptr<event_base> p(base, event_base_free);
unique_ptr
构造
unique_ptr<int> p2(new int(42)); unique_ptr<string> p3(new string("liuguangxuan")); unique_ptr<string> p4 = make_unique<string>(10,'a'); // C++14新增 cout << *p2 << endl; cout << *p3 << endl; cout << *p4 << endl;
其它接口
- u = nullptr 释放所指向的对象,并且将u置为空 - u.release() u放弃对指针的控制,并返回指针,将u置为空 - u.reset() 释放u指向的对象,并将u置为空,等同于直接赋值为nullptr - u.reset(q) 释放u指向的对象,并将u指向q - u.reset(nullptr) 和直接赋值为nullptr一样
指定删除器
数组
// C++11支持数组类型,所以无需额外指定删除器 std::unique_ptr<int[]> up1(new int[10]());
其它类型
// 必须指定类型,然后再指定删除器 // decltype作用于函数的时候,返回的为函数类型,所以必须加上* // C风格的网络库写法 struct event_base *base = event_base_new(); unique_ptr<event_base, delctype(event_base_free)*> p(base, event_base_free);
weak_ptr
weak_ptr为一种伴随类,和shared_ptr搭配使用,解决循环引用的问题。
循环引用
#include <iostream>
#include <memory>
using namespace std;
class Son;
class Father
{
public:
Father()
{
cout << __FUNCTION__ << endl;
}
~Father()
{
cout << __FUNCTION__ << endl;
}
public:
shared_ptr<Son> son_;
};
class Son
{
public:
Son()
{
cout << __FUNCTION__ << endl;
}
~Son()
{
cout << __FUNCTION__ << endl;
}
public:
shared_ptr<Father> father_; // 会造成循环引用,无法调用析构
//weak_ptr<Father> father_; // 正确
};
int main(int argc, char *argv[])
{
shared_ptr<Father> f(new Father());
shared_ptr<Son> s(new Son());
f->son_ = s;
s->father_ = f;
cout << "father count:" << f.use_count() << endl;
cout << "son count:" << s.use_count() << endl;
return 0;
}
上面的输出为:
// 可见没有执行析构函数,lsan会报内存泄漏
Father
Son
father count:2
son count:2
=================================================================
==2055250==ERROR: LeakSanitizer: detected memory leaks
Indirect leak of 24 byte(s) in 1 object(s) allocated from:
#0 0x7fabaf806647 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:99
#1 0x560aa1484cd1 in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<Son*>(Son*) /usr/include/c++/10/bits/shared_ptr_base.h:628
#2 0x560aa1484ba8 in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<Son*>(Son*, std::integral_constant<bool, false>) /usr/include/c++/10/bits/shared_ptr_base.h:639
#3 0x560aa1484a25 in std::__shared_ptr<Son, (__gnu_cxx::_Lock_policy)2>::__shared_ptr<Son, void>(Son*) /usr/include/c++/10/bits/shared_ptr_base.h:1128
#4 0x560aa1484752 in std::shared_ptr<Son>::shared_ptr<Son, void>(Son*) /usr/include/c++/10/bits/shared_ptr.h:159
#5 0x560aa1484255 in main /root/workspace/test/main.cpp:52
#6 0x7fabaf39c209 in __libc_start_call_main ../sysdeps/x86/libc-start.c:58
Indirect leak of 24 byte(s) in 1 object(s) allocated from:
#0 0x7fabaf806647 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:99
#1 0x560aa1484c4b in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<Father*>(Father*) /usr/include/c++/10/bits/shared_ptr_base.h:628
#2 0x560aa1484b72 in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<Father*>(Father*, std::integral_constant<bool, false>) /usr/include/c++/10/bits/shared_ptr_base.h:639
#3 0x560aa14849dd in std::__shared_ptr<Father, (__gnu_cxx::_Lock_policy)2>::__shared_ptr<Father, void>(Father*) /usr/include/c++/10/bits/shared_ptr_base.h:1128
#4 0x560aa148472c in std::shared_ptr<Father>::shared_ptr<Father, void>(Father*) /usr/include/c++/10/bits/shared_ptr.h:159
#5 0x560aa148422c in main /root/workspace/test/main.cpp:51
#6 0x7fabaf39c209 in __libc_start_call_main ../sysdeps/x86/libc-start.c:58
Indirect leak of 16 byte(s) in 1 object(s) allocated from:
#0 0x7fabaf806647 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:99
#1 0x560aa1484236 in main /root/workspace/test/main.cpp:52
#2 0x7fabaf39c209 in __libc_start_call_main ../sysdeps/x86/libc-start.c:58
Indirect leak of 16 byte(s) in 1 object(s) allocated from:
#0 0x7fabaf806647 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:99
#1 0x560aa148420d in main /root/workspace/test/main.cpp:51
#2 0x7fabaf39c209 in __libc_start_call_main ../sysdeps/x86/libc-start.c:58
SUMMARY: AddressSanitizer: 80 byte(s) leaked in 4 allocation(s).
将shared_ptr替换成weak_ptr之后的输出为:
(只需要将其中一个类的shared_ptr修改成weak_ptr即可,如果都修改成了weak_ptr,但是weak_ptr又不增加引用计数,容易引起对象提前被销毁)
// 没有内存泄漏
Father
Son
father count:1
son count:2
~Father
~Son
构造
shared_ptr<string> p3(new string("liuguangxuan"));
weak_ptr<string> ws(p3);
weak_ptr<string> ws = p3;
其它接口
w.reset()
// 将w置为空
w.use_count()
// 与w共享对象的shared_ptr的个数
w.expired()
// 若w.use_count()为0,返回true,否则返回false
w.lock()
// 如果expired()为true,则返回一个空shared_ptr,否则返回指向w的对象的shared_ptr
auto_ptr
auto_ptr在C++17中被移除,建议使用shared_ptr和unique_ptr来代替。
模拟智能指针
template <typename T>
class Shared_Ptr
{
public:
Shared_Ptr(T *t = nullptr) // 构造
{
count_ = new int(1);
resource_ = t;
}
Shared_Ptr(const Shared_Ptr *rhs) // 拷贝 构造
{
count_ = rhs->count_;
resource_ = rhs->resource_;
++*count_;
}
Shared_Ptr& operator=(const Shared_Ptr *rhs) // 拷贝赋值
{
-- *count_; // 先释放自身的
if(*count_ == 0)
{
delete count_;
delete resource_;
}
count_ = rhs->count_; // 再拷贝rhs的到自身
resource_ = rhs->resource_;
++*count_;
}
~Shared_Ptr() // 析构
{
--*count_;
if(*count_ == 0)
{
delete count_;
delete resource_;
}
}
int use_count()
{
return *count_;
}
int *count_;
T *resource_;
};
智能指针与多线程
此问题不能一概而论,分3种情况。
- 引用计数:引用计数为线程安全的。
shared_ptr对象自身:非线程安全的。线程安全级别同内置类型、标准库容器、std::string一样。
- 一个shared_ptr对象实体可被多个线程同时读取
- 两个shared_ptr对象实体可被两个线程同时写入
- 如果要从多个线程读写同一shared_ptr对象,那么需要加锁
- shared_ptr所管理的对象:非线程安全的。
智能指针性能
先说结论:
raw : unique_ptr : shared_ptr的效率约为:1 : 1.3 : 2.5。
即原始指针最快,其次unique_ptr的效率会比原始指针慢一点,最后shared_ptr最慢。但是考虑到智能指针的便利性,所以在没有共享需求的场景下,最好使用unique_ptr,其次是shared_ptr。
测试步骤:
分别执行1亿次的分配int内存并释放,统计耗时。
原始(raw)指针
int main(int argc, char *argv[]) { const int count = 100000000; for(int i = 0;i < count;++i) { int *p = new int(30); delete p; } return 0; }
执行3次分别耗时:
root@debian:~/workspace/cpp11/build# time ./main real 0m22.031s user 0m21.619s sys 0m0.392s root@debian:~/workspace/cpp11/build# time ./main real 0m22.830s user 0m22.444s sys 0m0.384s root@debian:~/workspace/cpp11/build# time ./main real 0m21.997s user 0m21.612s sys 0m0.384s
平均每次耗时约为:22.286秒
unique_ptr
int main(int argc, char *argv[]) { const int count = 100000000; for(int i = 0;i < count;++i) { unique_ptr<int> u(new int(30)); } return 0; }
执行3次分别耗时:
root@debian:~/workspace/cpp11/build# time ./main real 0m30.154s user 0m29.839s sys 0m0.313s root@debian:~/workspace/cpp11/build# time ./main real 0m28.627s user 0m28.241s sys 0m0.384s root@debian:~/workspace/cpp11/build# time ./main real 0m29.240s user 0m28.935s sys 0m0.304s
平均每次耗时约为:29.340秒
shared_ptr
int main(int argc, char *argv[]) { const int count = 100000000; for(int i = 0;i < count;++i) { shared_ptr<int> p(new int(30)); } return 0; }
执行3次分别耗时:
root@debian:~/workspace/cpp11/build# time ./main real 0m55.556s user 0m55.414s sys 0m0.139s root@debian:~/workspace/cpp11/build# time ./main real 0m56.702s user 0m56.511s sys 0m0.180s root@debian:~/workspace/cpp11/build# time ./main real 0m53.917s user 0m53.731s sys 0m0.176s
平均每次耗时约为:55.392秒
检测内存泄漏
安装Sanitizer
apt-get install libasan8
CMake配置
// 检测内存泄漏 set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=leak") // 检测其它指针问题(释放后使用、堆溢出、栈溢出等) // 此配置也能检测部分内存泄漏,但是循环引用的内存泄漏得需要上面那句话来检测 // set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address")