6 指针
约 4620 字大约 15 分钟
2025-06-22
new、delete和malloc、free的区别: new和delete在申请空间的时候对类对象会自动调用构造函数和析构函数 malloc和delete却不行 使用智能指针可以减少内存释放的工作量
智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。
需要引用头文件< memory >,空指针不需要
nullptr空指针(指针字面量)
零值整数字面量
在C++标准中有一条特殊的规则,即0既是一个整型常量,又是一个空指针常量。0作为空指 针常量还能隐式地转换为各种指针类型。 NULL是一个宏,在C++11标准之前其本质就是0
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++将NULL定义为0,而C语言将NULL定义为(void * )0。之所以有所区别, 是因为C++和C的标准定义不同,C++标准中定义空指针常量是评估为0的整数类型的常量表达式右值,而C标准中定义0为整型常量或者类型为void * 的空指针常量。
使用0代表不同类型的特殊规则给C++带来了二义性,对C++的学习和使用造成了不小的麻 烦
// 例子1
void f(int) {
cout << "int" << endl;
}
void f(char *) {
cout << "char *" << endl;
}
int main() {
f(NULL);
f(reinterpret_cast<char *>(NULL));
}
f(NULL)函数调用的是f(int)函数,因为NULL会被优先解析为整数类型。没有 办法让编译器自动识别传入NULL的意图,除非使用类型转换,将NULL转换到 char*,f(reinterpret_cast(NULL))可以正确地调用f(char * )函数。上面的代码可以在MSVC中编译执行。在GCC中,我们会得到一个NULL有二义性的错误提示。
// 例子2
string s1(false);
string s2(true);
以上代码可以用MSVC编译,其中s1可以成功编译,但是s2则会编译失败。原因是false被隐 式转换为0,而0又能作为空指针常量转换为const char * const,所以s1可以编译成功,true则没有 这样的待遇。在GCC中,编译器对这种代码也进行了特殊处理,如果用C++11(-std=c++11)及其之 后的标准来编译,则两条代码均会报错。但是如果用C++03以及之前的标准来编译,则虽然第一 句代码能编译通过,但会给出警告信息,第二句代码依然编译失败。
nullptr关键字
nullptr表示空指针的字面量,它是一个std::nullptr_t类型的纯右值。 nullptr无法隐式转换为整形,但可以隐式匹配指针类型 它不允许运用在算术表达式中或者与非指针类型进行比较(除了空指针常量0)。它还可以隐式转换为各种指针类型,但是无法隐式转换到非指针类型。 0依然保留着可以代表整数和空 指针常量的特殊能力,保留这一点是为了让C++11标准兼容以前的C++代码。
将指针变量初始化为0或者nullptr的效果是一样的,在初始化以后它们也能够与0或者nullptr 进行比较。
cout<<boolalpha<<( nullptr==0)<<endl;//true
虽然nullptr可以和0 进行比较,但这并不代表它的类型为整型,同时它也不能隐式转换为整型
nullptr_t类型
它并不是一个关键字,而是使用decltype将nullptr 的类型定义在代码中,C++标准规定该类型的长度和void * 相同
namespace std
{
using nullptr_t = decltype(nullptr);
// 等价于
typedef decltype(nullptr) nullptr_t;
}
cout<<boolalpha<<(sizeof(nullptr_t) == sizeof(void *))<<endl;
我们可以使用std::nullptr_t去创建自己的nullptr,并且有与nullptr相同的功能:
nullptr_t null1, null2;
char* ch = null1;
char* ch2 = null2;
assert(ch == 0);
assert(ch == nullptr);
assert(ch == null2);
assert(null1 == null2);
assert(nullptr == null1);
虽然这段代码中null1、null2和nullptr的能力相同,但是它们还是有很大区别 nullptr是关键字,而其他两个是声明的变量。nullptr是一个纯右值,而其他两个是左值
nullptr_t null1, null2;
cout << "&null1 = " << &null1 << endl; // null1和null2是左值,可
// 以成功获取对象指针,
cout << "&null2 = " << &null2 << endl; // 并且指针指向的内存地址不同
cout << &nullptr <<endl;//error
void f(int) {cout << "int" << endl;}
void f(char *) {cout << "char *" << endl;}
int main() {
f(nullptr);//char *
}
f(nullptr)会调用f(char * ),因为nullptr可以隐式转换为指针类型,而无法隐式转 换为整型,所以编译器会找到形参为指针的函数版本。 不过,如果这份代码中出现多个形参是指 针的函数,则使用nullptr也会产生二义性,因为nullptr可以隐式转换为任何指针类型,所以编译 器无法决定应该调用哪个形参为指针的函数。
使用nullptr可以为函数模板或者类设计一些空指针类型的特化版本。 在C++11以前这是不可能实现的,因为0的推导类型是int而不是空指针类型。现在可以利用 nullptr的类型为std::nullptr_t写出下面的代码
template<class T>
struct widget {
widget() {cout << "template" << endl;}
};
template<>
struct widget<nullptr_t> {
widget() {cout << "nullptr" << endl;}
};
template<class T>
widget<T> *make_widget(T) {
return new widget<T>();
}
int main() {
auto w1 = make_widget(0);
auto w2 = make_widget(nullptr);
}
shared_ptr共享指针
成员函数
use_count() 查看当前有多少智能指针控制这块内存
get() 返回管理对象的内存地址
初始化
std::shared_ptr<T> 名字(创建堆内存);//通过构造函数初始化
shared_ptr<T> 名字=要拷贝的指针;//通过拷贝初始化
shared_ptr<T> 名字=move(要拷贝的指针);//通过移动初始化
shared_ptr<T> make_shared(Args&&……args);//通过make_shared初始化
//reset操作时直接指定另一块内存(重置指针,让指针指向新的内存)
Args&&……args:要初始化的数据 如果通过make_shared创建对象,需要根据构造函数的参数列表指定
class A{
private:
int num;
public:
A(){cout<<"默认构造函数"<<endl;}
A(int x):num(x){cout<<"构造函数"<<x<<endl;}
A(string s){cout<<"构造函数"<<s<<endl;}
~A(){cout<<"析构函数"<<endl;}
void setvalue(int x){num=x;}
void print(){cout<<num<<endl;}
};
//通过构造函数初始化
shared_ptr<int> p1(new int(3));
cout<<p1.use_count()<<endl;
//通过移动构造函数初始化
shared_ptr<int> p2= move(p1);
cout<<p1.use_count()<<endl;
cout<<p2.use_count()<<endl;
//通过拷贝构造函数初始化
shared_ptr<int> p3=p2;//因为p1的数据已经转移,p1本身已经没有数据了
cout<<p3.use_count()<<endl;
cout<<p2.use_count()<<endl;
//通过make_shared初始化
shared_ptr<int> p4= make_shared<int>(6);
shared_ptr<A> p5= make_shared<A>(6);//调用带整形参数的构造函数
shared_ptr<A> p6= make_shared<A>("tom");//调用带字符串参数的构造函数
//通过reset初始化
p6.reset();
cout<<p6.use_count()<<endl;
p5.reset(new A());
cout<<p6.use_count()<<endl;
//获取原始指针
cout<<p5.get()<<endl;
删除器
当智能指针管理的内存计数变为0时,这块内存就会被智能指针释放,调用自带的删除函数。 我们也能自己指定删除器函数(在初始化智能指针时在指向地址后面再传入一个地址)本质是一个回调函数
shared_ptr<A> p1(new A(6),[](A* a){delete a;});
shared_ptr<A> p2(new A[5]);//只析构了一次
shared_ptr<A[]> p2(new A[5]);//11标准之后在模板参数中也指定为数组类型也能删除数组了
自带的删除器函数不能删除数组类型,因此如果是数组类型的需要自己指定删除器函数
shared_ptr<A> p2(new A[5],[](A* a){delete []a;});
在删除数组的时候除了自己编写删除器也可以使用编译器提供的defult_delete< T>()函数作为删除器
shared_ptr<A> p3(new A[5],default_delete<A[]>());
unique_ptr独占指针
独占的智能指针计数永远是1,如果新的独占指针想要管理这块内存就需要使用move进行转移
初始化
unique_ptr<T> 名字(创建堆内存);//通过构造函数初始化
unique_ptr<T> 名字=move(要移动的独占指针);//通过移动构造函数初始化,不能通过拷贝构造初始化
unique_ptr<T> 名字=func();//函数的返回值类型是一个同类型的独占指针,这个指针即将被销毁所以可以赋值给新的独占指针
//通过reset初始化
//通过构造函数初始化
unique_ptr<int> p1(new int(9));
//通过移动构造函数初始化
unique_ptr<int> p2= move(p1);
//通过reset初始化
p2.reset(new int(8));
//获取原始指针
unique_ptr<A> p3(new A(1));
cout<<&p3<<endl;
cout<<p3.get()<<endl;
删除器
unique_ptr 指定删除器和 shared_ptr 指定删除器是有区别的,unique_ptr 指定删除器的时候还需要在模板参数里面确定删除器的类型,所以不能像shared_ptr 那样直接指定删除器 删除器一般都是函数,所以在指定函数类型的时候指定的都是函数指针
匿名函数在C++中默认看成仿函数(如果不对外部变量进行捕捉,可以看作是一个函数指针,如果对外部变量进行捕捉就会看作是个仿函数) 仿函数定义函数指针就不好使了,所以就需要可调用对象包装器对其包装,再把类型指定过去
typedef void(*funptr)(A*);
unique_ptr<A,funptr> p1(new A("tom"),[](A* a){delete a;});
unique_ptr< A,function<void(A*)> > p2(new A("jerry"),[=](A* a){delete a;});
unique_ptr 可以管理数组类型的地址,能够自动释放
unique_ptr<A[]> p1(new A[5]);
weak_ptr弱引用指针
不共享指针,不能操作资源,是用来辅助shared_ptr的,因此这个weak_ptr也能监测shared_ptr指向的地址,因为是监测,所以不影响共享指针的计数
初始化
weak_ptr<int> wp1; //构造了一个空weak_ptr对象
weak_ptr<int> wp2(wp1); //通过一个空weak_ptr对象构造了另一个空weak_ptr对象Pweak_ptr<int> wp3(sp); //通过一个shared_ptr对象构造了一个可用的weak_ptr实例对象
wp4 = sp;//通过一个shared ptr对象构造了一个可用的weak_ptr实例对象(这是一个隐式类型转换)
wp5 = wp3; //通过一个weak_ptr对象构造了一个可用的weak_ptr实例对象
成员函数
use_count();//返回当前观测资源的引用计数
expired();//判断当前观测对象释放被释放,true表示被释放,false表示未被释放
lock();//返回管理所监测资源的shared_ptr对象
reset();//重置弱引用智能指针对象
不能使用一个原始地址初始化多个共享智能指针
class A{
public:
A(){cout<<"构造函数"<<endl;}
shared_ptr<A> get(){return shared_ptr<A>(this);}
~A(){cout<<"析构函数"<<endl;}
};
A* a=new A;
shared_ptr<A> p1(a);
shared_ptr<A> p2(a);
//shared_ptr<A> p2=p1;//ok
构造只构造一次,但析构两次
使用的两种场景
- 当函数返回值返回的是shared_ptr包装的一个this指针,这个指针就会被释放两次
shared_ptr<A> get(){return shared_ptr<A>(this);}
一个模板类叫做 std::enable_shared_from_this< T >,这个类中有一个方法叫做shared_from_this(),通过这方法可以返回一个共享智能指针法返回一个智能指针。 在函数的内部就是使用 weak ptr 来监测 this 对象,并通过调用weak_ptr 的 lock()方法返回一个shared_ptr 对象
class A:public enable_shared_from_this<A>{
public:
A(){cout<<"构造函数"<<endl;}
shared_ptr<A> get(){return shared_from_this();}
~A(){cout<<"析构函数"<<endl;}
};
shared_ptr<A> p1(new A);
shared_ptr<A> p2=p1->get();
- 循环引用 一个智能指针对象指向另一个智能指针对象,这个智能指针对象又指向原来的智能指针对象,这样导致在释放的时候计数永远不为0
class B;
class A{
public:
shared_ptr<B> pb;
A(){cout<<"A struct"<<endl;}
~A(){cout<<"A disstruct"<<endl;}
};
class B{
public:
shared_ptr<A> pa;
B(){cout<<"B struct"<<endl;}
~B(){cout<<"B disstruct"<<endl;}
};
shared_ptr<A> ap(new A);
shared_ptr<B> bp(new B);
cout<<'a'<<ap.use_count()<<endl;
cout<<'b'<<bp.use_count()<<endl;
ap->pb=bp;
bp->pa=ap;
cout<<'a'<<ap.use_count()<<endl;
cout<<'b'<<bp.use_count()<<endl;
虽然没报错但A和B并没有析构 这种情况类A和B其中一个的shared_ptr改成weak_ptr即可或两个都改
class A{
public:
weak_ptr<B> pb;
A(){cout<<"A struct"<<endl;}
~A(){cout<<"A disstruct"<<endl;}
};
默认模板参数
template<typename T=long,typename U=int>
void fun(T t='a',U u='b'){
cout<<t<<','<<u<<endl;
}
fun('a','b'); //a,b
fun();//97,98
调用函数如果有参数就按照参数的类型进行处理 如果没有参数就按照默认类型转换(fun()运行默认把a和b转换成long和int型)
可调用对象包装器、绑定器
可调用对象
函数指针、具有operator()的类对象(仿函数)、可被转换为函数指针的类对象、类成员的函数指针或类成员指针
将类对象转换为函数指针
typedef void(*funcptr)(int,string);
class Test{
public:
int a=10;
void hello(int a,string s){cout<<"hello"<<a<<s<<endl;}
static void world(int a,string s){cout<<"world"<<a<<s<<endl;}
operator funcptr(){
return hello; //error
return world; //ok
}
};
类成员在实例化类对象前是不存在的,但静态成员是存在的(静态方法是属于类的)
类的函数指针
如果想要创建能调用类的非静态成员的指针就需要创建类的函数指针了 对于以上案例只需要在函数指针声明中加上类的作用域即可
Test t;
using fptr= void(Test::*)(int,string);
fptr f1=&Test::hello;
(t.*f1)(16,"jerry");//相等(&t->*f1)(16,"jerry");
类的成员指针
typedef int Test::*ptr1;
ptr1 p=&Test::a;
cout<<t.*p<<endl;
可调用对象包装器
需要包含头文件< functional > std::function是可调用对象的包装器,他是一个类模板,可以容纳除类成员(函数)指针之外所有可调用对象通过它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们 语法
function<返回值类型(参数类型列表)> diy_name = 可调用对象;
声明和调用
typedef void(*funcptr)(int,string);
class Test{
public:
int a=10;
void hello(int a,string s){cout<<"类的成员函数"<<a<<s<<endl;}
static void world(int a,string s){cout<<"类的静态函数"<<a<<s<<endl;}
void operator()(string s){cout<<"仿函数"<<s<<endl;}
operator funcptr(){
cout<<"转换成函数指针的类对象"<<endl;
return world;
}
};
void test(int a,string s){cout<<"普通函数"<<a<<s<<endl;}
//包装普通函数
function<void(int,string)> p1=test;
p1(1,"a");
//包装类的静态成员
function<void(int,string)> p2=Test::world;
p2(2,"b");
//包装仿函数
Test t;
function<void(string)> p3=t;//仿函数可调用对象就是t本身
p3("c");
//包装转换为函数指针的类对象
function<void(int,string)> p4=t;
p4(4,"d");
通过可调用对象包装器将他们打包之后就能得到统一的类型,基于这个类型我们可以像函数一样调用,也可以作为函数的参数传递 实际应用
class A{
private:
function<void(int,string)>callback;
public:
A(const function<void(int,string)> & f):callback(f){}
void notify(int id,string name){
callback(id,name);
}
};
A a(test);
a.notify(1,"as");
A b(Test::world);
b.notify(2,"df");
A c(t);
c.notify(3,"cx");
可调用对象绑定器
stdfunction进行保存,并延迟调用到任何我们需要的对象 作用 1、将可调用对象与其参数一起绑定成一个仿函数 2、将多元(参数个数为m,n>1)可调用对象转换为一元或者(n+1)元可调用对向,即只绑定部分参数 语法
//绑定非类成员函数/变量
auto f=std::bind(可调用对象地址,绑定的参数/占位符);
//绑定类成员函数/变量
auto f=std::bind(类函数/成员地址,类实例对象地址,绑定的参数/占位符);
静态函数不属于类对象,因此应当使用第一种方式 绑定器返回的是一个仿函数类型,得到的返回值可以直接赋值给一个std::function,在使用的时候我们不需要关心绑定器的放回值类型,使用auto进行了自动类型推导即可
占位符
placeholders_ 1、placeholders_ 3、placeholders_ 5等 占位符在调用时就转换为后面小括号里传入的参数 占位符后面的数是多少就表示找后面小括号里第几个参数
void test(int a,int s){cout<<"普通函数"<<a<<s<<endl;}
bind(test,1,placeholders::_1)(3); //test的x参数传入1,y参数为后面括号第一个参数
bind(test,placeholders::_1,2)(3); //test的y参数传入2,x参数为后面括号第一个参数
bind(test,placeholders::_1,placeholders::_2)(3,4); //test的y参数为后面括号第一个参数,x参数为后面括号第二个参数
bind(test,placeholders::_2,placeholders::_1)(3,4);//test的y参数为后面括号第二个参数,x参数为后面括号第一个参数
bind(test,2,placeholders::_1)(3,4);//使用绑定的数值而使用参入的数值
绑定器函数绑定类成员函数(变量)
//成员函数绑定
Test t;
auto f1=bind(&Test::hello,&t,20,placeholders::_1);
function<void(int,string)> f11=bind(&Test::hello,&t,20,placeholders::_1);
f1("2077");
//成员变量
auto f2= bind(&Test::a,&t);
function<int()> f22=bind(&Test::a,&t);//只读
function<int&()> f23=bind(&Test::a,&t);//加上引用可修改
cout<<f2()<<endl;
f2()=6;//可修改
cout<<f2()<<endl;
贡献者
版权所有
版权归属:PinkDopeyBug