目录
指针基础
在C++中,指针是一种特殊的变量 ,它存储的是另一个变量的内存地址 ,而不是数据本身。通过使用指针,我们可以直接访问和操作内存中的数据。指针也叫做地址。
与引用的区别
指针(pointer)是"指向(point to)"另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点。
其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
简单来说就是:指针可以改,前一秒指向变量A,下一秒可以指向变量B,但引用不能改,绑定了就一直是它。
cpp
//声明一个指向整数的指针,可以不赋初值
int *ptr;
//ip1和ip2都是指向int类型对象的指针
int *ip1, *ip2;
//dp2是指向double类型对象的指针,dp是double型对象
double dp, *dp2;
这里,ptr 是一个指针变量,它可以存储一个整数的内存地址。
指针赋值
我们可以通过下面方式(&变量)获取一个变量的地址
指针变量 = &变量名
指针可以通过两种方式赋值:
1.直接赋值内存地址:这通常不是常规做法,因为直接操作内存地址可能会导致未定义行为。
2.赋值变量的地址:这是更常见的做法,使用取地址运算符(&)来获取变量的地址,并将其赋值给指针。
cpp
//定义ptr2并且赋值,指向var地址
int var = 10;
int *ptr2 = &var;
这里,ptr 被赋值为 var 的内存地址。
我们可以对上述进行一个图解:

ptr2指向var的地址,也就是ptr2存储的是var的地址0x2be3。因为ptr2也是对象,所以计算机也会为它开辟空间,ptr2自己的地址为0x3f2b.
所以指针也是一个变量,在存别人的地址同时也有自己的地址。
cpp
//定义ptr2并且赋值,指向var地址
int var = 10;
int *ptr2 = &var;
//ptr_address 存储ptr2的地址
int** ptr_address = &ptr2;
std::cout << "var 的地址为: " << &var << std::endl;
std::cout << "ptr2 存储的值为: " << ptr2 << std::endl;
std::cout << "ptr2 地址为: " << &ptr2 << std::endl;
std::cout << "ptr_address 的值为: " << ptr_address << std::endl;
输出为:

可以看到我们通过ptr_address 可以存储ptr2的地址, ptr_address存储的是指针的地址,所以它是一个指针的指针类型(int**),也叫做二级指针。
类型匹配
因为在声明语句中指针的类型实际上被用于指定它所指向的对象的类型,所以二者必须匹配。如果指针指向了一个其他类型的对象,那么会产生错误。
cpp
double dval = 3.14;
//正确,初始值是double类型的对象的地址
double *pd = &dval;
//正确,初始值是指向double对象的指针
double *pd2 = pd;
//错误,pi的类型和pd类型不匹配
//int * pi = pd;
//错误,把double型对象的地址赋值给int型指针
//int * pi = &dval;
指针值
指针的值(即地址)应属下列4种状态之一:
1.指向一个对象。
2.指向紧邻对象所占空间的下一个位置。
3.空指针,意味着指针没有指向任何对象。
4.无效指针,也就是上述情况之外的其他值。
注意:空指针和无效指针都是访问不了的,必须给指针存储一个有效地址。
利用指针访问对象
我们可以利用*(解引用操作符)获取指针所指向的对象的数据,写法如下
数据变量 = *指针

我们举个例子:
cpp
//访问对象
int ival = 42;
//p_int存放着ival的地址,或者说p_int是指向变量ival的指针
int * p_int = &ival;
//由符号*得到指针p所指向的对象,输出42
std::cout << *p_int ;
上面的例子通过*p_int获取p_int所指向的对象ival的值,输出42。
大家可能遇到说为啥它能这么准确的取出数据,因为ptr2为int*类型,4个字节,找到首地址后偏移四个字节就是他的所有这个区间,取出4个字节就是把10所占空间都取出来了。
解引用 ,不管是赋值给别人或者修改内容,前提必须建立在有效的空间,无效的空间是不能解引用的。
利用指针修改对象
因为指针存储的是对象的地址,通过解引用获取对象的数据,我们也可以通过解引用修改对象的值,举例:
cpp
//访问对象
int ival = 42;
//p_int存放着ival的地址,或者说p_int是指向变量ival的指针
int * p_int = &ival;
//由符号*得到指针p所指向的对象,输出42
std::cout << *p_int << std::endl;
//由符号*得到指针p所指向的对象,即可经由p_int为变量ival赋值
*p_int = 0;
std::cout << * p_int << std::endl;
//打印ival的值
std::cout << "ival 的值为" << ival << std::endl;
注意:
解引用操作仅适用于那些**确实指向了某个对象的有效指针,**比如这个就是错误的:
cpp
//初始化一个空指针
int* empty_pointer = nullptr;
//打印空指针数据,非法
//std::cout << "空指针指向数据是 " << *empty_pointer << std::endl;
总的来说:当 * 号没有用于指针的声明的时候,这个***** 可为解引用作用,取出指针内容,用一个变量去存储,既可以取值,也能赋值。
符号的多重含义
像 & 和 * 这样的符号,既能用作表达式里的运算符,也能作为声明的一部分出现,符号的上下文决定了符号的意义:
cpp
//& *
int ival2=43;
//&紧跟着类型名(int)出现,并且再声明中,表示R是一个引用类型
int &r=ival2; //r是ival2的引用
int *p;
//*紧跟着类型名(int)出现,并且再声明中,表示p是一个指针类型
p=&ival2; //&在表达式中,表示取地址
*p=43; //*在表达式中,表示解引用
//*p是取出ival2的值
int &r2=*p; //r2是ival2的引用
r2=42; //通过引用修改ival2的值
std::cout<<"ival2="<<ival2<<std::endl; //输出ival2的值
空指针
空指针包含几种定义方式
cpp
/空指针定义方式
//C++11 最推荐方式
int *p1 = nullptr;
//直接将p2初始化为字面量0
int *p2 = 0;
//需要使用#include<cstdlib>
//等价于int * p3 = 0;
int *p3 = NULL;
std::cout << "p1: " << p1 << std::endl;
std::cout << "p2: " << p2 << std::endl;
std::cout << "p3: " << p3 << std::endl;
得到空指针最直接的办法就是用字面值nullptr来初始化指针,这也是C++11新标准刚刚引入的一种方法。
nullptr是一种特殊类型的字面值。
另一种办法就如对p2的定义一样,也可以通过将指针初始化为字面值0来生成空指针
注意:
把int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行
cpp
/空指针
int *hh=nullptr;
int zero=0;
//int *p8=zero; //错误的
地址变换:
我们可以修改指针指向的地址,进而达到指向其他变量的目的.
指针和引用都能提供对其他对象的间接访问,然而在具体实现细节上二者有很大不同,其中最重要的一点就是引用本身并非一个对象。
一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。
指针和它存放的地址之间就没有这种限制了。和其他任何变量(只要不是引用)一样,给指针赋值就是令它存放一个新的地址,从而指向一个新的对象:
cpp
int ival3=42;
//pval3为空指针,不指向任何对象
int *pval3=nullptr;
int *pval4=&ival3;
//指针之间赋值
pval3=pval4; //pval3指向ival3的地址
std::cout<<"ival3的地址="<<&ival3<<std::endl;
std::cout<<"pval3的地址="<<pval3<<std::endl;
std::cout<<"pval4的地址="<<pval4<<std::endl;
输出:

指针判空
有时候我们需要判断指针是否为空,可以通过if判断,在C++ 中0为false,非0为true,所以一个空指针通过if判断,是false, 非空指针为true。
cpp
//空指针判断
int *empty1=nullptr;
if(!empty1)
{
std::cout<<"empty1是一个空指针"<<std::endl;
}
int test=100;
int *q1=&test;
if(q1)
{
std::cout<<"q1不是一个空指针"<<std::endl;
}
指针同样可以支持比较运算,判断相等(==),判断不等(!=)。
cpp
if(q1!=empty1)
{
std::cout<<"q1和empty1不相等"<<std::endl;
}
if(q1==empty1)
{
std::cout<<"q1和empty1相等"<<std::endl;
}
再普及一下,sizeof(指针) 要么8字节,要么4字节,取决于系统,虽然有时指向int,double类型,但是本身存储的数据是固定的。
万能指针
**void***是一种特殊的指针类型,可用于存放任意对象的地址。一个
**void***指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:
cpp
//万能指针
double obj = 3.14, *obj_pd = &obj;
//void 可以存放任何类型的对象的地址
void * pv = &obj;
//pv 可以存储任意类型的地址
pv = obj_pd;
利用void*指针能做的事儿比较有限:
不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作,单作为参数传递。
cpp
//万能指针 void*
double obj=3.14,*pd=&obj;
void *pv=&obj; //void*万能指针,能指向任何类型的
std::cout<<"void*指针的大小="<<sizeof(pv)<<std::endl;
pv=pd;
std::cout<<*((double*)pv)<<std::endl;
//void*指针不能直接解引用,必须先转换为对应类型再去解引用
注意:
不解引用 输出为存储地址 ,解引用 输出为存储的值,不能定义一个指针去指向一个别名,要指向一个实际空间才行。
指向指针的指针
以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。通过*的个数可以区分指针的级别。也就是说,** 表示指向指针的指针,*******表示指向指针的指针的指针,以此类推:
cpp
/以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,
// 因此允许把指针的地址再存放到另一个指针当中。通过*的个数可以区分指针的级别。
// 也就是说,**表示指向指针的指针,***表示指向指针的指针的指针,以此类推:
int ival4= 1024;
// pival4指向了int型的数
int *pival4 = &ival4;
//ppi指向了一个int型的指针
int ** ppi = &pival4;
std::cout << "The value of ival\n"
<< "direct value: " << ival4 << "\n"
<< "indirect value: " << *pival4 << "\n"
<< "doubly indirect value : " << **ppi
<< std::endl;
该程序使用三种不同的方式输出了变量ival的值:第一种直接输出;第二种通过int型指针pi输出;第三种两次解引用ppi,取得ival的值。

指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
cpp
//引用本身不是一个对象,因此不能定义指向引用的指针。
// 但指针是对象,所以存在对指针的引用
int init = 42;
// pinit是一个int型的指针
int *pinit;
// rpinit是一个对指针pinit的引用
int *& rpinit = pinit;
// rpinit引用了一个指针,因此给rpinit赋值&init就是令pinit指向init
rpinit = &init;
//解引用rpinit得到i,也就是p指向的对象,将init改为0
*rpinit = 0;
引用一个指针类型,跟指针类型的变量进行关联,& 符号前要加上对应的指针类型。
指针运算
指针可以进行算术运算,如递增(++)和递减(--),这些操作会改变指针所指向的内存地址。但是,这种操作仅限于指向数组元素的指针。
指针和数组
在C++中,数组名在表达式中通常被当作指向数组首元素的指针。
cpp
int arr[5]={1,2,3,4,5};
int *parr=arr; //数组名是一个指针,指向数组
std::cout<<"数组首地址,第一个元素的地址="<<parr<<std::endl;
std::cout<<"数组首地址,第一个元素的值="<<*parr<<std::endl;
*parr=1024; //通过指针修改了数组第一个元素的值
std::cout<<"数组首地址,第一个元素的值="<<*parr<<std::endl;
parr++;
//指针加1,指向下一个元素
std::cout<<"数组第二个元素的地址="<<parr<<std::endl;
std::cout<<"数组第二个元素的值="<<*parr<<std::endl;
//定义ptr2并且赋值,指向var地址
输出:

数组元素地址:
简单来说:Int 类型,地址连续加4 ,longlong 类型,地址连续加 8

注意:
指针必须在使用前被初始化,否则它们可能包含垃圾值,导致未定义行为。
指针运算(如递增和递减)仅适用于指向数组元素的指针。
指针的解引用操作必须确保指针不是空指针(nullptr),否则会导致运行时错误。
结语:
今天任务就到这,指针确实比较晦涩难懂,需要好好消化一下。
