C+ + 中的指针是一种强大而灵活的工具,它允许程序员以更 直接、更高效的方式与内存和数据进行交互。然而,由于指针的复杂 性和潜在的风险(如内存泄漏、野指针等), 因此在使用时需要格外小 心。本章涉及的知识点如下: 第5章 指针1 41 5.1 指针变量 5.1.1 指针的概念 内存中的数据可以通过变量名直接访问,还可以通过指针变量进行间接访问。在编程 语境中,当人们谈论“指针”的时候,通常指的是“指针变量”。指针变量是一种特殊的变量, 与一般变量不同,它存储的是某个变量的内存地址,而非数据本身。一个指针变量存放了哪 个变量的地址,就说这个指针指向了哪个变量,这使得我们能够通过这个地址间接地访问或 修改该地址所指向位置上的数据。指针变量必须初始化或者赋值(指向了变量)后,才能进 行间接访问操作。 5.1.2 指针变量的声明 1.指针变量的声明格式 指针变量声明的一般格式如下: 存储类型 数据类型 *指针名=初始值(另一个变量的地址); 或者 存储类型 数据类型 *指针名; 例如: int *pi=&x; 2.指针的赋值 ● 上面的语句声明了指针变量pi,pi是指针名,int是数据类型,声明指针变量可以存 储int类型变量的地址,不可以用其他类型变量的地址给指针变量pi赋初值或赋值。 ● 上面的语句虽然没有给指针变量pi指定存储类型,但默认的存储类型为auto类型, 即存储类型为自动类型,pi是自动类型局部指针变量,存放在栈空间,由编译系统管 理pi变量的所占用的内存单元。 ● 声明语句中变量pi前的星号“*”非常重要,有了星号“*”,才能说明pi是指针变 量,才能存放另外一个整型变量的地址,所有在声明语句中的星号“*”只是一个 标记。 ● 上面的语句声明了指针变量pi,并且给该指针变量赋初值,初始值为x变量的地址, x变量必须是声明了int类型的变量,那么就可以说pi指向了int类型的变量x。 ● 定义指针变量的时候可以赋初值,也可以不赋初值。如果赋初值,需要注意的是,初 值变量必须在指针初始化之前已声明过,且变量类型应与指针类型一致。也可以用 一个已赋初值的指针变量去初始化另一个同类型的指针变量。 循序渐进C++案例教程(微课视1 42 频版) ● 既然pi是一个指针变量,那么pi也会占用内存单元,因此变量pi也有地址,&pi就 是指针变量pi的地址。不管指向什么类型变量的指针,所占用的空间大小是一样 的,在32位编译器中都占用4字节的内存单元,在64位编译器中都是占用8字节的 内存单元。 ● 如果指针没有赋初值,则需在使用指针间接访问它所指向的变量前,一定要赋值。这 很好理解,指针没有指向变量,怎么能间接访问其指向变量的数据值呢? 因此说通过 指针间接访问前,指针不能为空值。 3.程序代码及运行结果 (1)程序代码 【例5.1】 指针定义。 #include <iostream> using namespace std; int main() { int x = 100; //声明x 变量,并赋初值100 int *pi = &x; //声明pi 指针变量,并赋值为x 的地址 cout << "x="<<x<< endl; //输出x 变量的数据值 cout << "x 的地址是"<<&x<<endl; //输出x 变量的地址值 cout << "*pi 是"<<*pi<<endl; //通过指针间接访问x 变量的值 cout << "pi="<<pi<<endl; //pi 变量的值,就是x 的地址 cout << "pi 的地址是"<<&pi << endl; //pi 变量的地址值 return 0; } (2)程序分析及运行结果 以上代码在VS2019(32位编译器)环境下运行结果如图5.1所示,在Dev-C++(64位编 译器)环境下运行结果如图5.2所示。 图5.1 程序段运行结果(32位编译器) 图5.2 程序段运行结果(64位编译器) 以32位编译器为例,分析上面程序的执行结果可以看出,pi指针保存的是x变量的地 址,第二条输出语句和第四条输出语句的结果是一样的,都是x变量的地址,00F2FBFC是 8个十六进制字符,一个32位的长整型数据,表示指向的变量在内存中的地址。 对于第三条输出语句 cout << *pi << endl; 输出结果为x变量的值100,是属于通过指针变量间接地访问了变量x的值。在此你可能 会有疑惑,这是声明语句int*pi=&x带来的困惑。在该声明语句中,看似*pi赋值为 &x,怎么到了输出*pi时,*pi就成了x变量的值了呢? 这是因为int*pi=&x语句为声 第5章 指针1 43 明语句,声明语句中的“*”号只是标志,表示紧跟其后的变量pi是指针类型变量,是指针类 型变量就可以把另外一个变量的地址赋值给它。离开了声明语句,cout<<*pi中的“*”为 间接引用运算符,这种操作也称为解引用操作,表示通过指针变量pi访问变量x中存放的 数据。因此cout<<*pi<<endl;是输出pi指针指向的内容。例5.1代码段中x变量的数据 值、x变量的地址值、pi变量的数据值、pi变量的地址值,在内存中的存储示意图如图5.3 所示。 pi变量和x变量间的逻辑关系见图5.4。 说明:两个小矩形框分别表示pi变量和x变量,画一个箭头,箭尾画在pi指针变量的 小矩形框里面,箭头指向变量x的小矩形框,不要指到x变量的矩形框里,这样就表示pi指 针变量存放的是x变量的地址。 图5.3 pi指针指向x变量的物理内存存储示意图图5.4 pi指针变量和x变量间的逻辑关系 5.1.3 声明指向不同数据类型的指针 指针变量可以声明为指向任何数据类型的变量,即指针变量的值可以是任何数据类型 变量的地址值。例如: int *pi; 语句声明pi是一个指向int型变量的指针。 float *pl; 循序渐进C++案例教程(微课视1 44 频版) 语句声明pl是一个指向float型变量的指针。 char *pc; 语句声明pc是一个指向char型变量的指针。 char (*pa)[3]; 语句声明pa是一个指向一维数组的指针,此一维数组是长度为3的字符数组。 char *pm[3]; 语句声明pm 是一个指针数组,pm 是数组名,数组的三个元素存放字符变量的地址。 int (*pf)(); 语句声明pf是一个指向函数的指针,该函数的返回的数据类型为int型数值。也就是说指 针不仅可以指向数据变量,也可以指向代码段,函数就是代码段,函数名就是函数的地址。 int * fun(); 语句声明函数的返回值是整型的指针变量。 int**pp; 语句声明pp是一个指向指针变量的指针,即二级指针。 要存储一个指针变量的地址,就需要一个二级指针,如果pi是指向整型变量的指针,pp 变量就可以存储pi变量的地址。一个星号“*”,*pp表示间接访问pp指向变量的值,实 际上是取得了pi变量的值,pi变量是x变量的地址,因此**pp才可以间接访问到x变量 的值。 struct Date { int year,month,day; //数据类型为整型的三个结构成员 }; //定义结构类型Date Date *pd; 在此加一个回车,换行语句声明指向结构变量的指针。 如果结构类型成员的指针类型是自身结构类型,称这种结构为自引用结构,也是定义链 表的格式。例如: struct Node { int data; struct Node *next; //结构类型成员是指向自身结构的结构类型的指针变量 }; 称这种结构类型为自引用结构类型。 指向对象的指针如下: 第5章 指针1 45 class Person //定义Person 类 {p rivate: //私有访问权限 char *name; //姓名 类的成员是指向字符的指针也叫字符串 int age; //年龄 char gender; //性别 public: //公有访问权限 Person(char *n,int nl,char xb) //类的构造函数 { strcpy(name,n); age=nl; gender=xb; } }; Person *pa; 语句声明pa是指向Person类对象的指针变量。 void *pv; 语句声明pv是空指针类型,允许声明指向void类型的指针,该指针可以被赋予任何类型对 象的地址。 例如: void *pv; 语句声明pv是void类型的指针。 int *pint, i; 语句声明pint是整型变量的指针。 pv=&i; 语句声明void类型指针可以指向整型变量。 pint=(int *)pv; 语句说明用void指针给int指针赋值时,需要类型强制转换。 【思考与练习】 1.简答题 (1)什么是指针? 它的值和类型是如何规定的? (2)声明指针变量的一般格式是什么? 举例说明声明不同指针类型的方法。 (3)如何给不同类型的指针赋值和赋初值? 循序渐进C++案例教程(微课视1 46 频版) 2.单选题 (1)已知语句: int i, j=2, *p=&j; 可完成i=j赋值功能的语句是( )。 A.i=*p; B.p*=*&j; C.i=&j; D.i=**p; (2)若有以下声明语句,0<i<4,则( )是错误的赋值。 int a[4][10],*p,*q[4]; A.p=a; B.q[i]=a[i]; C.p=a[i]; D.q[i]=&a[2][0]; 5.2 指针的运算 指针中存放的是变量的地址,除了可以通过间接引用运算符对变量存储的内容操作外, 指针还支持一些特殊的运算,如指针算术运算和关系运算。 5.2.1 使用指针访问数组元素 数组名是一个指针常量。一个数组的数组名就是该数组首元素的地址。指针常量不同 于指针变量,指针常量的值在程序执行过程中不能被修改,而指针变量的值可以被修改。 例如: int a[10], *pa; pa=&a[0]或pa=a两个赋值语句都是让pa指针指向数组a的首元素,经过上述声明 及赋值后,可以使用指针访问数组元素。*pa是a[0],*(pa+1)是a[1],…,*(pa+i)是a [i],因此a[i]、*(pa+i)、*(a+i)、pa[i]是等价的,都表示数组下标为i的数组元素,但是 不能写a++,因为a是数组名,是数组首地址,是常量,常量不能被修改。 【例5.2】 用指针访问数组元素。 #include <iostream> using namespace std; int main() { int a[5]={5,4,3,2,1}; int i,j; i=a[0]+a[4]; //用下标法读取数组元素 j=*(a+2)+*(a+4); //用指针读取数组元素,等价于j=a[2]+a[4] cout<<i<<endl<<j; return 0; } 第5章 指针1 47 程序的输出结果为: 64 5.2.2 指针的算术运算 在C++中,数组的一个显著特点是其元素被存放在一段连续的内存区域中。由于字符 串的存储方式也是占用一段连续的内存区域来存放字符序列,因此,通过指针访问数组元素 的方法同样可以应用于字符串这种连续的内存区域。我们可以使用指针遍历字符串中的每 个字符,直到遇到空字符\0为止,这标志着字符串的结束。 1.指针的移动 对指针变量加上或减去一个整数,可以实现指针的移动。指针变量P加上或减去整型 数据n,表示将指针P向前(n>0)或向后(n<0)移动n个位置。P=P+1表示将P指针向 前移动1个位置,但该"1"并不代表十进制数1,而是指一个存储单元的长度。这个长度取 决于指针所指向的数据类型。例如,如果指针指向int类型的数据,那么在32位系统中,每 增加一个单位就意味着指针向前移动4字节(因为int类型通常占用4字节)。如果是指向 char类型的数据,则每增加一个单位就是移动1字节。 【例5.3】 指针与整数相加。 #include <iostream> using namespace std; int main() { int *pi,a[10]={0,1,2,3,4,5,6,7,8,9}; double *pd,b[10]={9.1,8.1,7.1,6.1,5.1,4.1,3.1,2.1,1.1,0.1}; pi=a; //pi 为整型数组a 的首地址 pd=b; //pd 为double 型数组b 的首地址 for(int i=0;i<10;i++) cout<<pi+i<<" "<<*(pi+i)<<"----"<<pd+i<<" "<<*(pd+i)<<endl; return 0; } 图5.5 例5.3程序运行结果 例5.3程序的运行结果如图5.5所示。 程序分析:该程序声明了一个int型指针pi和一个double 型指针pd,分别声明了int型和double型数组,并且给数组赋 初值。首先,让pi指向int整型数组的首元素,pd指向double 型数组的首元素。接着,将两个指针分别与0到9相加,显示 指针与整数相加的结果,从图5.5可以看出,指针(地址)加上一 个整数结果仍然是地址,地址的变化与指针所指向的类型有 关,因为int型数据长度是4字节,double型数据长度是8字节,因此pi加1按4字节增加, pd加1按8字节增加。注意,在显示器上输出指针变量的值时,每次的运行结果不相同。 循序渐进C++案例教程(微课视1 48 频版) 读者每次运行该程序时pi和pd的值与图5.5中pi和pd的值也不一样。 2.元素个数的计算 当两个指针指向同一段连续的内存区域(同一数组或者同一字符串)时,两个指针可以 做减法运算,结果为两个指针之间相差数据元素的个数。例如p指向数组的首元素,q指向 数组的尾元素,q-p+1则是数组的长度。 【例5.4】 两个同类型的指针相减。 #include <iostream> using namespace std; int main() { int *p,*q,a[10]={0,1,2,3,4,5,6,7,8,9}; p=a; //p 指针指向第1 个数组元素 q=&a[9]; //q 指向第10 个数组元素 cout<<"数组的长度是: "<<q-p+1<<endl; //输出数组的长度10 return 0; } 注意:两个指针不能相加,如果想计算p和q两个指针的中间地址,用如下语句: pm=p+(q-p)/2; pm 是中间数组元素的地址值。计算中间数组元素下标与计算中间元素的指针不一样,如 果low是数组元素的低下标,high是数组元素的高下标,那么中间数组元素的下标为mid= (low+high)/2,在数组中进行二分查找就是采用这个公式计算两个数组元素的中间元素的 下标。 3.指针的自增和自减运算 p++等价于p=p+1,与p+1有区别,p++的结果是p指向下一个元素,p+1的结 图5.6 y=*pi+1和 y=*(pi+1) 果只是计算下一个元素的地址,而p指针本身的值不变,说明 如下。 (1)y=*pi+1和y=*(pi+1)的区别 *pi+1结果是间接访问pi指针变量内容再加1,*(pi+1) 结果是间接访问pi指针后面一个数据的内容,如果pi指针如 图5.6所示。 y=*pi+1; 则y的值是101。 y=*(pi+1); 则y的值为200。 (2)y=(*pi)++和y=*pi++的区别 y=(*pi)++先间接地访问pi所指向变量的值,因为++是后置运算符,所以把pi所 第5章 指针1 49 指向变量的值赋值给y变量后,pi所指向变量的值又进行了加1运算。 y=*pi++先去访问pi指针所指向变量值赋值给y变量,然后是pi指针加1。 假如指针pi如图5.7(a)所示,在执行完y=(*pi)++后,y的值为100,而*pi值为 101,指针pi的值不变,运算结果如图5.7(b)所示。如果执行y=*pi++,在运行后y的值 也是100,pi执向了下一个单元,运算结果如图5.7(c)所示。 图5.7 y=(*pi)++语句与y=*pi++语句的运行结果 5.2.3 指针的关系运算 两个同类型的指针可以进行==、!=、<、<=、>和>=这样的关系运算,指针的关系 运算实际就是比较地址。如果p指向数组的第一个元素,q指向数组的第2个数组元素,则 关系表达式p<q的结果为真。 例5.5将定义数组元素逆置函数。函数的功能为将长度为n的int型数组中数组元素 进行逆置。假如有长度为n的数组array[n](n是一个整型常量),数组元素逆置是指用 array[n-1]、array[n-2]、array[n-3]、……、array[2]、array[1]和array[0]共n个数组元素 分别给array[0]、array[1]、array[2]、……、array[n-3]、array[n-2]和array[n-1]共n个 元素赋值,即把数组中的n个元素进行前后交换。比如数组b[10]={10,20,30,40,50,60, 70,80,90,100}共10个数据,b数组逆置后,则b[0]=100,b[1]=90,b[2]=80,……, b[7]=30,b[8]=20,b[9]=10。 【例5.5】 指针的关系运算,分析程序的运行结果。 #include <iostream> using namespace std; void reverse(int a[],int n) { int *p,*q,t; p=&a[0]; //p 指向首元素 q=&a[n-1]; //q 指向尾元素 while(p<q) //指针关系运算 { //p 指针指向变量的值与q 指针指向变量的值进行交换 t=*p; *p=*q; *q=t; p++; //修改p 指针的值,p 指针后移 q--; //修改q 指针的值,q 指针前移 } }