Bootstrap

c++ 指针详解

指针

指针是c++中的一个核心概念,是一名c++程序员可以直接对内存进行操作的一种工具,这样的工具就是一把双刃剑,一面可以实现一些非常优化的程序,另一方面也会导致一些难以调试的错误。


1、 使用指针遍历数组

示例:

#include<iostream>
using namespace std;

//使用指针遍历数组

int main()
{
	int arr[5] = { 0, 1, 2, 3, 4 };
	int *ptr = arr;
	for (int i = 0;i < 5 ;i++)
	{
		cout << *ptr << " ";
		ptr++;
		//也可以直接写成 cout << *(ptr++) << " ";
		}
	cout << endl;
	return 0 ;
}

运行结果:
在这里插入图片描述
示例中涉及指针的初始化、解引用操作以及自增,看一个有趣的语句“int *ptr = arr;”,该语句用数组名初始化指针。在这里数组名代表的是数组第一个元素的地址,之后在循环内程序会递增指针以指向数组的后面几个元素。


2、指针的概念与理解

指针(Pointer),从其英文字面上来理解就是一个指向某一物件的东西,在程序中就是指向数据的地址(Address)。计算机的内存可以看作是一个紧密排列的数据序列,每一小块数据序列,每一小块数据(也就是字节)的旁边都有一个编号代表数据地址。这在现实中可以用房屋的地址来理解,我们可以说这一栋房子是小李家,也可以说一栋房子是xx路xxx号(指针表示)。对于上面的示例可以用一个图来理解ptr 和 arr 到底指的是什么。

arrptr
203204205206207
01234
假设arr的地址是203,那么数组后面几个元素的地址依次递增(这个例子中因为数组的类型为 int ,所以 其实真是的地址需要依次增加4个字节)。指针实际上就是内存地址,所以arr的值就是203,而当ptr指向数组最后一个元素的时候,它的值为207.如果我们想要获取某一个地址下存储的数据,就可以使用ptr来获得。

指针的含义:

#include <iostream>
using namespace std;

//指针的含义

int main()
{
	int arr[5] = { 0, 1, 2, 3, 4 };
	int *ptr = arr;
	for (int i = 0; i < 5 ; i++)
	{
		cout << *ptr << " ";
		cout << "地址:" << ptr <<endl;
		ptr++;
	}
	return 0 ;
}

	

运行结果:
 指针代表地址
可以看到,数组的第一个元素的十六进制地址是:0x7ffd83fdbcd0
(不同计算机以及同一计算机每次运行该程序得到的地址都有可能与示例中的地址不同),第二个元素的地址是:0x7ffd83fdbcd4
,每个元素之间的距离正好是 int 的大小-----4 字节。


3、指针的创建与初始化

指针的创建与初始化示例:

#include<iostream>
using namespace std;

//指针的创建和初始化

int main()
{
	float *floatPtr = NULL;
	string *strPtr;
	int *intPtr1, *intPtr2;
	int* intPtr3,  intPtr4; //intPtr4只是一个整数
       return 0 ;
}

通过示例可以看到,指针的声明就是在变量类型名和变量名之间加上星号(*),并可以并可以任意选择让星号紧贴类型名
(int * intPtr3,)或者变量名
(float *floatPtr = NULL; string *strPtr; int *intPtr1, *intPtr2;)的代码风格。然而紧贴类型名的代码风格会给人造成“int *”是一个整体的感觉,初学者很容易在声明多个指针的时候遗漏后面变量名前的星号,就像intPtr4一样,感觉像是定义了一个指针,其实只是一个整型。正确的语法是像intPtr2那样在前面加上一个星号,不管与星号之间有没有空格。

此外,示例中只有第一样的floatPtr初始化了,但是实际编程中我们一定要初始化所有指针,就跟变量一样。floatPtr的初始值NULL是一个宏定义,它的实际数值是0,也就是地址:0x00000000.一般我们都会把指针初始化为NULL,也叫做空指针,这给我们一个统一可管理的异常值。在程序中,我们只要检查指针是否为空就知道指针是否指向有效数据了。

**提示:**如果指针没有初始化,它可能指向一个未知的地址,那么我们在尝试读取数据的时候就可能造成程序崩溃。此外,在指针初始化的时候,不能使用0以外的整型给指针赋值。

除了例子中的指针类型外,c++还有一种通用的void*指针。我们知道指针就是地址,指针的类型不过表示了地址指向的位置所存储的数据类型。如果我们将int * 指针转换为float * 指针,那么程序也只是将数据重新解读为浮点类型。所以这里void * 只是代表了一个地址,而我们不知道它所指向的数据类型,但我们也可以重新定义它所指向的数据类型。void * 一般会在一些内存处理的系统函数时候使用


4、指针的基本操作

对于指针来说,解引用和取地址是最重要的两个操作符。
指针的基本操作

#include<iostream>
using namespace std;

//指针的基本操作

int main()
{
	int num = 4 ;
	int *intPtr = &num;
	cout << "num的地址是:" << &num << endl;
	cout << "指针的值是:" << intPtr << endl;
	if ( intPtr )
	{
		//检查指针是否为空
		cout << "指针所指的数字是:" << *intPtr << endl;
	}
	return 0 ;
}

运算结果:
在这里插入图片描述
可以看到,符号“&”表示了取地址的操作,它可以获得变量的内存地址。将其赋值给我们的指针intPtr后,打印&num和intPtr将同时获得num的地址。而当我们使用解引用操作符“ * ”的时候,* intPtr将会得到intPtr所指向的地址中的数据,也就是num的值。

在示例中,还加上了一个条件来检查指针是否为NULL,以此保证对intPtr解引用一定是安全的。这里利用了数值与布尔值之间的隐性转换,只写了intPtr作为条件,因为intPtr为空的话值会转化为false。条件intPtr也可以写成“intPtr != NULL”。
例子中用解引用操作符读取了指针指向的数据,而解引用操作符也可以用来作为赋值语句的左值以修改为数据。

左值解引用:

#include <iostream>
using namespace std;

//左值解引用

int main()
{
	int num = 4 ;
	int *intPtr = &num;
	if ( intPtr )
	{
		//检查指针是否为空
		cout << "指针所指的数字是:" << *intPtr << endl;
		cout << "num的值是: " << num << endl;
		*intPtr = 3;
		cout << "修改后,指针所指的数字是: " << *intPtr << endl;
		cout << "num的值是:" << num << endl;
	}
	return 0 ;
}

运行结果:
在这里插入图片描述
示例中,使用了左值解引用操作将指向num的指针中的数据修改为3,由于指针与num的地址相同,因此num也会变成3.指针的这一种行为可能会让初学者感到困惑,接下来我们是用图片来直观地解释指针解引用、取地址和左值解引用的行为。


5、指针的算数操作

指针可以像整形那样进行一部分算数操作,我还可以对地址进行修改。因为计算后的指针不一定会指向具有有效数据的地址,所以在进行指针算数操作的时候需要格外小心。

指针与整形的算数操作:

#include<iostream>
using namespace std;

//指针与整型的算数操作

int main()
{
	int arr[5] = { 0, 1, 2, 3, 4 };
	int *ptr = arr;
	cout << "arr + 4:" << *(arr + 4 ) << endl;
	cout << "ptr + 4:" << *(ptr + 4 ) << endl;
	cout << "ptr: " << ptr + 2 << endl;
	cout << "++ptr: " << ++ptr << endl;
	cout << "ptr - 2: " << ptr - 2 << endl;
	cout << " --ptr:" << --ptr << endl;
	return 0;
}

运行结果:
在这里插入图片描述
可以看到,指针与整型的算数操作不同于一般的数字加减,而是与指针类型绑定的。由于一个int 的大小是4字节,那么ptr+2 会将地址加上8,在组中就指向第三个元素。在示例中,除了指针ptr,我们也对数组arr做了加法,得到的结果都是第五元素的值。此外,示例末尾的ox7ffd4207fe80 已经比数组的第一个元素地址还小了,如果对这个地址进行解引用,可能会使程序崩溃。

提示数组名其实可以看做是指向数组第一个元素的指针。指针的各种操作都适用于数组名但只有一点区别,那就是数组名不能被重新赋值。这样是很容易理解的,因为数组是静态的,数组名代表了当前作用域唯一的一个数组,不可能先指针那样指向其他地址。

指针除了与整型的算数操作以外,还可以进行指针相减
指针相减

#include <iostream>
using namespace std;
int main()
{
	int arr[5] = { 0, 1, 2, 3, 4 };
	int *ptr1 = arr + 1;
	int *ptr2 = arr + 3;
	cout << "ptr1:" << ptr1 << endl;
	cout << "ptr2:" << ptr2 << endl;
	cout << "ptr2 - ptr1: " << ptr2 - ptr1 << endl;
	cout << "ptr1 - ptr2: " << ptr1 - ptr2 << endl;
	return 0 ;
}

运行结果:
在这里插入图片描述
指针相减返回的是指针地址之间的距离,并且是分正负的。这个距离也与类型绑定,单位是该类型数据的个数。指针之间不存在加法,每个指针代表的地址在计算机中都是唯一确定的,相加没有任何意义。这就好像是门牌号32减掉30得到2,表示他们之间,隔着两户,而32加上30却不能表示什么。


6、const指针

之前有使用左值解引用来修改指针指向的原变量的例子,但如果变量是const,值是不能被修改的,因此我们也需要有一种特殊的指针来保证原变量不会被修改,这就是指向const对象的指针。

指向const对象的指针

#include<iostream>
using namespace std;

//指向const对象的指针

int main()
{
	const int num = 3;
	//普通指针不能指向const变量
	//int *ptr1 = &num;
	const int *ptr2 = &num;
	cout << "*ptr2:" << *ptr2 << endl;
	//指向const对象的指针不能修改解引用后的值
	//*ptr2 = 4;
	//指向const对象的指针可以修改指向的地址
	const int num1 = 4;
	ptr2 = &num1;
	cout << "*ptr2: " << *ptr2 << endl;
	//指向const对象的指针也可以指向普通变量
	int num2 = 5;
	ptr2 = &num2;
	cout << "*ptr2 : " << *ptr2 << endl;
	return 0 ;
}

运行结果:

要定义一个指向const对象的指针,就要在const对象类型名后加上星号。“int* ptr1 = &num;”这一行如果去掉注释,编译器就会报错,因为普通指针不能指向const对象。“ * ptr2 =4;”这一行如果去掉注释相当于修改const对象的值,编译器也会报错。

注意虽然ptr2指向的地址不能修改,但是它本身指向的地址可以修改。示例中,我们先后又让它指向了另外两个变量,其中也有一个非const的变量,指向非const变量的这一种指针也不能修改解引用后的值。
既然指向const对象的指针还可以修改地址的,那么应该也有另外一种不能修改地址的指针,也就是const指针。

const指针

#include<iostream>
using namespace std;

//const指针

int main()
{
	int num1 = 3 ;
	int num2 = 4 ;
	int *const ptr1 = &num1;
	//const指针不能修改指向地址
	ptr1 = &num2 ;
	const int num3 = 5 ;
	const int num4 = 6 ;
	//指向const对象的const指针既不能修改地址,也不能修改值
	const int *const ptr2 = num3;
	ptr2 = num4;
	return 0 ;
}

示例展示了尝试修改const指针而导致的编译错误。const指针的创建语法是将const移动到了星号后面,一开始ptr1指向num1,而当我们尝试把num2的地址赋值给ptr1的时候编译器报错。一个指向const对象的const指针ptr2 ,这个指针只能指向const int变量,它指向的地址也不能改变。


7、指针的数组和数组的指针

标题看似是两个类似的概念,二者却截然不同。指针作为一种变量类型,当然可以被声明为数组,而数组作为一种变量类型,也可以有指向它的指针。指针的数组是一种数组,而数组的指针则是一种指针。

指针的数组和数组的指针

#include<iostream>
using namespace std;

//指针的数组和数组的指针

int main()
{
	int arr[5] = { 0, 1, 2, 3, 4 };
	//数组的指针
	int (*arrPtr)[5] = &arr;
	//指针的数组
	int *ptrArr[5] = { &arr[0], &arr[1], &arr[2], &arr[3], &arr[4] };
	cout << "arrPtr: " << arrPtr << endl;
	cout << "*arrPtr:" << *arrPtr << endl;
	for (int i = 0; i < 5;i++)
	{
		cout << ( *arrPtr ) [i] << " ";
		cout << ptrArr[i] << " ";
		cout << *(ptrArr[i] ) << " "<< endl;
	}
	return 0 ;
}

运行结果:
在这里插入图片描述
可以看到,数组的指针和指针的数组的语法区别在于:数组的指针需要在星号和变量名外面加一个括号,而指针的数组却没有。这一点其实很好理解,因为声明数组的时候元素类型名int和数组大小[5] 就是被变量名隔开的,在这里我们添加一个星号,并用括号括起来,表示这个指针int( * arrPtr)[5]是指向整个数组的;如果不加括号,编译器就只会将星号联系到前面的类型名int,所以ptrArr就只是声明了一个数组,数组的元素类型就是int*。

在声明arrPtr的时候,我们把数组的地址赋值给他作为初值,由于数组的指针解引用以后就相当于数组,我们可以用( * arrPtr)[i]来读取数组的元素。
ptr Arr是一个指针的数组,它的每一个元素都是一个指针,这里就将数组的每个元素的地址分别赋值,而在遍历的时候使用 * (ptrArr[i])来读取数组中某一个指针指向的元素值。
由于这里比较不直观的一点arrPtr和*arrPtr代表的地址完全一样,为了解释这一点,再看一个示例

#include<iostream>
using namespace std;

//数组的指针的地址

int main()
{
	int arr1[5] = { 0, 1, 2, 3, 4 };
	//数组的指针
	int (*arrPtr)[5] = &arr1;
	cout <<"arrPtr: " << arrPtr << endl;
	cout <<"*arrPtr:" << *arrPtr << endl;
	int arr2[5] = { 0, 1, 2, 3, 6 };
	//数组的指针必须指向大小相同的数组
	arrPtr = &arr2;
	cout << "arrPtr: " << arrPtr << endl;
	cout << "*arrPtr:" << *arrPtr << endl;
	//数组的指针指向数组,而数组是不可修改的
	//*arrPtr = arr1
	return 0 ;
}

运算结果:
在这里插入图片描述
我们可以看到,数组的指针必须指向相同大小的数组,如果arr2只有4个元素,“ arrPtr = &arr2; ”的赋值机会产生编译错误,并且由于数组的指针指向是不可修改的数组,我们不能把arrPtr作为左值修改。

至于为什么arrPtr和 * arrPtr的地址一样,可以看做是编译器不得已的安排。数组名arr1代表着数组首元素的地址,而一般的变化量比如int1就放着一个数值,而&int1才放着int1 的地址。由于数组的这一特殊性,导致了&arr得到的数组地址与arr代表的数组地址是一样的,因此相应的arrPtr和 * arrPtr的地址也只能是一样的,* arrPtr也要搭配下标操作符才能取得数组的对应元素。


8、指针的指针

指针可以指向任何变量或者对象,所以也可以指向指针。

#include <iostream>
using namespace std;

//指针的指针

int main()
{
	int num = 3 ;
	int *numPtr = &num;
	int **numPtrPtr = &numPtr;
	cout << "num: " << num << endl;
	cout << "*numPtr: " << *numPtr << endl;
	cout << "numPtr: " << numPtr << endl;
	cout << "*numPtrPtr: " << *numPtrPtr << endl;
	cout << "numPtrPtr: " << numPtrPtr << endl;
	return 0;
}

运行结果:
在这里插入图片描述
可以看出,指针的指针声明就多加了一个星号,以表示指针指向的指针类型,因此将numPtr的地址赋值给它。
指针的指针一般用于函数传参数时修改传入的指针。


9、const_cast 与reinterpret_cast

const_cast的作用是将一个变量转换成const限定的常量。

const_cast

#include<iostream>
using namespace std;

//const_cast

int main()
{
	int intNum = 2 ;
	intNum = 3;
	//const_cast后面必须跟引用或指针类型
	const int &consIntNum = const_cast<int&>(intNum);
	//转换以后数字不能在修改
	//constIntNum = 2;
	return 0 ;
}

示例中,可以看到intNum在转换前是可以修改的变量,在转换以后就变成常量,不能再进行修改了。

reinterpret_cast 比较特殊。reinterpret 的意思是重新解读,而reinterpret_cast 就是将一段数据按照二进制表示重新解读成另一种数据,所以他并没有对数据做任何改变,只是改变了类型。

reinterpret_cast

#include<iostream>
using namespace std;

//reinterpret_cast

int main()
{
	int intNum = 0x00646362;
	int *intPtr = &intNum;
	char *str = reinterpret_cast<char *>(intPtr);
	cout << "str的值为:" << str << endl;
	return 0;
}

运行结果:
在这里插入图片描述
在示例中,可以看到热interpret_cast将一个指向整数的指针转换成了指向字符的指针,也就是c风格的字符串,十六进制的62、63、64在ASCLL码中分别代表b、c、d,所以打印了“bcd”。
reinterpret_cast 的作用就是将一个类型的指针转换成另一个类型的指针,而指针指向的内存将被原封不动地重新解读。当然这也是一种比较危险的举动。

如果本文对您有帮助,请点赞支持一下~

;