【8.0】C-指针
前言
兜兜转转,终于到了指针,作为C语言最重要的功能。也是C语言最强大,最复杂的机制。
指针在C程序中应用非常广泛,从基本的数据结构,如链表和树,到大型程序中常用的数据索引和复杂数据结构的组成,都离不开指针的使用。之所以说指针时C最强大的机制,在于指针可以使程序员直接按地址直接访问指定的存储空间,也可以在权限许可范围内对存储空间的数据进行任意解释和操作。
Pointer是指针的英文单词
变量的地址和指针
在程序中,当我们定义一个变量时,首先要定义变量的数据类型,数据类型决定了一个变量在内存中所占用的存储空间的大小。其次要定义变量名。C语言的编译系统会根据变量的类型在适当的时候为指定的变量分配存储空间。例如,在Visual C++环境下,一个int
类型数据占据4个字节的存储空间。
在计算机内部,所有的存储空间都要统一进行“编号”,即所有的存储空间都要有地址,每一存储空间具有唯一的内存地址。系统为每一个已定义的变量分配一定的存储空间,使变量名与内存的一个地址相对应,为一个变量进行赋值操作,实质就是要将变量的值存入系统为该变量分配的存储空间中,即变量的值要存入变量名对应的内存地址中。例如:
1 | int i,j,k; |
编译程序可能会为它们在内存中做如下图形式的分配。
也就是说变量i
占据以2000开始的4字节,j
占据从2004开始的4
字节,k
占据从2008开始的4
字节。在确定了变量的地址之后,就可以通过变量名对内存中变量对应的地址进行操作。对编程者来说,可以使用变量名进行程序设计。程序运行时需要进行计算时,要根据地址取出变量所对应的存储空间中存放的值,参与各种运算,计算结果最后还要存入变量名对应的存储空间中。例如:
1 | i = 10; |
语句i=10
是将整数值10存入从2000开始的地址单元,语句j=20
是将整数值20存入2004开始的地址单元。而
1 | k = i + j; |
则是将2000中存放的值和2004中存放的值取出来相加,然后放入2008开始的单元中去。这个赋值语句执行完毕后的情况如下图所示:
通过变量名获取变量的地址,再从变量的地址对应的存储空间中取值,或将某值存入变量地址对应的存储空间中的过程,称为直接寻址访问。
如果将变量i
的地址存放在另一个变量p
中,通过访问变量p
,间接达到访问变量i
的目的,这种方式称为变量的间接访问。保存其他变量地址的变量就称为指针变量。因此,我们可以认为:指针是用于指向其他变量的变量。
要取出变量i
的值10,既可以通过使用变量i
直接访问,也可以通过变量i
的地址间接访问。
间接访问变量i
的方法是:从地址为3000的存储空间中,先找到变量i
在存储空间中的地址2000,再从地址为2000的单元中取出i
的值10,这种对应关系如下图:
所谓指针变量,就是专门用来保存指针的一类变量,它的值是其他变量的地址,该地址就是某个变量在存储空间中对应的存放位置。这种间接存取关系反应了指针的特性。
要注意区分“值”和“地址”
指针用于存放其他数据的地址,指针可以指向变量,利用指针可以引用变量;指针还可以指向数组,利用指针可以访问数组中的所有元素;指针还可以指向函数,存放函数的入口地址,利用指针调用函数;指针还可以指向结构体(欠结构体,后面有),引用结构体变量的成员。
指针变量的定义
指针变量与一般变量一样,必须先声明后使用。定义一个指针变量需要解决两个问题:一是声明指针变量的名字,二是声明指针变量指向的数据类型,即指针变量所指的变量的数据类型。语法格式如下:
1 | //语法格式 |
说明:
指针变量名前面的符号
*
在声明/定义时不可以省略,它是把其后变量声明为指针类型的标志。其他类型的变量允许和指针变量在同一个语句中定义。例如:
1
int a,b,*p;
指针变量定义中的数据类型是指针指向的目标数据类型,而不是指针变量的数据类型。指针变量的数据类型由
*
声明为指针类型。
指针运算
取地址运算符
&
运算符是取地址运算符,它是单目运算符,其功能是返回其后所跟操作数的地址,其结合性为从右向左,例如:
1 | int i=10,*p; |
将变量i
的地址赋值给p
。这个赋值语句可以理解为p
接收i
的地址,如下图所示:
注意区分取地址运算符
&
与双目运算符&
(按位与)
指针运算符
*
运算符是指针运算符,也称为间接运算符,它也是单目运算符。其功能是取该指针变量所指向的存储单元的值。代码示例:
1 | int x=10,*p,y; //声明和定义x=10,指针类型p,和整型变量y |
赋值运算
指针变量的初始化
指针变量的初始化,就是在定义指针变量的同时为其赋初值。由于指针变量是指针类型,所赋初值应是一个地址值。其一般格式如下:
1 | 数据类型 * 指针变量名=地址1; |
其中地址形式有多种,例如:&变量名,数组名,其他的指针变量等。例如:
1 | //取一般变量地址 |
说明:
不能用未声明的变量给指针变量赋初值,代码示例:
1
2int *p = &i;
int i;当用一个变量地址为指针变量赋值时,该变量的数据类型必须与指针变量指向的数据类型一致。
除0之外,一般不把其他整数作为初值赋给指针变量。程序运行期间,变量的地址是由计算机分配的,当用一个整数作为指针变量赋初值后,可能会造成难以预料的后果。当用0对指针赋初值时,系统会将该指针变量初始化为一个空指针,不指向任何对象。
使用赋值语句赋值
语法格式如下:
1 | //语法格式 |
另外,指针变量和一般变量一样,存放在它们之中的值可以改变,也就是说可以改变它们的指向,例如:
1 | int a=10,b=20,*p; |
通过指针访问它所指向的一个变量是以间接访问的形式进行的,所以比直接访问一个变量要费时间,而且不直观,因为通过指针要访问哪一个变量,取决于指针的值(即指向),例如*p1=*p2;
实际上是将p2
的变量的值赋值给p1
变量的值。由于指针是变量,可以通过改变指向来间接访问不同的变量,这给编写者以灵活性,使程序代码编写更加间接灵活。
数据类型 *
+变量名
表示指针类型,*
+变量名
表示取指针的值。
空指针与void
指针
空指针
空指针就是不指向任何对象的指针,表示该指针没有指向任何存储空间。构造空指针有下面两种方法:
赋0值,这是唯一的允许不经转换就赋予指针的数值。
赋
NULL
值,NULL
值等于0,即两者等价,例如:1
2
3
4int *p;
p=0;
//或者
p=NULL;
引入空指针的目的就是为了防止使用指针出错。
空指针常常用来初始化指针,避免野指针的出现。
对指针变量赋0或者NULL值与不赋值是不同的。指针变量赋0值后,该指针被初始化为空指针,空指针是不可以使用的。而指针变量未赋值时,可以是任意值,可能指向任何地方,该指针被形象的称为野指针。不要使用野指针,否则可能会出现意外错误。
为了避免上述错误的发生,习惯的做法是定义指针变量时立即将其初始化为空指针,在使用指针之前再给指针变量赋值,也就是在指针有了具体指向之后再使用指针。
void
指针
C语言规定,指针变量可以定义为void
类型,例如:
1 | void *p; |
这里p
仍然是一个指针变量,且有自己的内存空间,但不指定p
指向哪种类型的变量。在这种情况下,应该注意:
任何指针都可以赋值给
void
指针。void
指针赋值给其他类型指针需要进行类型转换。例如:1
int *t=(int *)p; //需要强制类型转换
void
指针不能参与指针运算,除非进行转换。例如:1
2
3
4
5int main(){
void *c;
c++; //编译出错,原因是不知道c的指向的类型
return 0;
}
指针与函数
在函数之间可以传递变量的值,同样也可以传递地址(指针)。函数与指针的相关关系:指针作为函数的参数,函数的返回值为指针以及指向函数的指针。
指针作为函数参数
问题从编写一个两个数交换的问题出手。
【实例】编写swap
函数,实现交换两个整型变量的值。
【代码示例】
1 |
|
【输出】
【说明】从输出结果来看就可以知晓,对于调用函数来说,是实现了数的交换,但是只是形参的交换,而实参a
,b
并没有发生交换。这与我们想要的结果相差甚远,怎么办?
以指针为参数的函数就可以解决这个问题了,虽然全局函数也可以实现,但是对于程序来说,每次都需要全局函数的对于程序的可维护,可读性有一定的影响。
【实例】以指针为参数的交换swap
函数,实现两数的交换。
【代码示例】
1 |
|
【输出】
但是如果将上面的代码进行简单的调整,得到的结果又是不同的,代码示例:
1 |
|
【输出】
调整后的代码a=b
,只是在形参a
和b
指针的地址发生了交换,而对于地址指向的存储空间内容没有发生交换,也就是说仅仅是交换了形参a
和b
之间存储的变量地址。对于调整前的代码来说*a=*b
,意味着将指针a
指向的变量的存储空间的值修改成指针b
指向的存储空间的值,也就是说修改了指针指向地址的存储空间,也就实现了原变量的值交换。
“指针变量所指向单元的存储空间的值”和“指针变量存储的地址值是根本不同的。前者指的是指针指向单元的值,而后者指的是指针变量的值。
指针作为函数的返回值
除了可以将基本类型作为函数返回值类型之外,还可以将地址作为函数返回值,当把地址作为函数的返回值时,该函数称为指针函数。语法格式如下:
1 | 数据类型 * 函数名(形参列表){ |
其中,函数名前面的数据类型 *
表示该函数的返回类型为某数据类型的指针。
【实例】输入若干数值,判断并获取最大值,且值的范围在$100$~$0$之间。
【代码示例】
1 |
|
【输出】
如果函数返回指针,注意不要返回局部变量的地址,因为局部变量在执行完成函数后被释放了,返回的是野指针。
函数指针
在定义一个函数之后,编译系统为每个函数确定一个入口地址,当调用该函数时,系统会从入口地址开始指向该函数。存放函数的入口地址的指针就是一个指向函数的指针,简称函数指针。语法格式如下:
1 | 类型标识符 (*指针变量名)(); |
类型标识符是函数的返回值的类型。需要注意的是,由于C语言中,括号的优先级比*
高,因此,* 指针变量名
外部必须使用括号,否则指针变量名首先与后面的括号结合,就是前面说明的“返回指针的函数”。例如:
1 | int *P(); //函数返回值类型为指针的函数,该指针指向整型 |
与变量指针一样,必须给函数指针赋值,才能指向具体的函数。由于函数名代表了该函数的入口地址,因此,一个简单的方法就是直接使用函数名来为函数指针赋值,即:
1 | //语法格式 |
函数指针经定义和赋值之后,在程序中就可以引用该指针,目的是调用被指针指向的函数,可以通过这种方式增加对函数调用的方式。
【实例】使用函数指针,计算两个数的和
【代码示例】
1 |
|
【输出】
【实例】关于函数指针的一些用法
【代码示例】
1 |
|
【输出】
这样在调用函数的时候,可以直接传入指定函数,就可以执行函数。
指针与数组
一维数组的指针表示
定义指向一维数组的指针变量
在C语言中,指针和数组有紧密的联系,其原因在于,凡是由数组下标完成的操作都可以用指针来实现。我们以及知道,在数组中可以通过数组的下标来确定唯一数组元素在数组中的顺序和存储地址,这种方式也称为下标表示法。例如:
1 | int a[3]={1,2,3},x; |
对于数组的引用可以使用指针表示法来实现。由于每个数组元素相当于一个变量,因此指针变量既然可以指向一般变量,同样也可以指向数组中的元素,也就是可以用指针来访问数组中的元素。例如:
1 | int a[3]={1,2,3}; |
由于一维数组的数组名是一个地址常量,程序运行时,它的值是一个一维数组第一元素的地址。所以可以通过数组名把数组的首地址赋值给指针变量,即:
1 | int a[3]={1,2,3}; |
输出结果:1,输出的是数组a
的首地址,即第一个元素的值。
由此可知,&a[0]
,a
,*p
指向同一单元,是数组a
的首地址。
通过指针引用数组元素
C语言规定:如果指针变量p
已指向数组中的一个元素,这p+1
指向同一数组中的下一个元素。
引入指针变量后,现在可以通过两种方式来访问数组:
1 | //常规方式 |
输出结果:1,2,3
数组中的指针运算
加减算术运算
对于指向数组的指针变量,可以加上或者减去一个整数
n
。设p
是指向数组a
的指针变量,则p+n
,p-n
,p++
,p--
等运算都是合法的。指针变量加减整数的意义是将指针指向的位置向前或者向后移动n
个位置,这里加减的单位不是以字节为单位,而是以指向的数据类型所占用的字节数为单位。【实例】指针算术运算示例
【代码示例】
1
2
3
4
5
6
7
8
9
10
11
12
13
int main(){
int a[5] = {1, 2, 3, 4, 5}, *p, y = 30;
for (int i = 0; i < 5; i++)
{
printf("a[%d]=%d ", i, a[i]); //常规方法遍历输出数组元素
}
p = a; //指针指向数组首地址
y = *++p; //变量值赋值为指针向后移动一位的值
printf("\ny=%d ", y);
printf("指针值为:%d", *p);
return 0;
}【输出】
两个指针变量之间的运算
两个指针变量之间的加减运算是不合法的,也是无意义的,但是指针变量之间可以进行关系运算。
假设指针
p
和指针q
指向同一数组的元素,那么:p<q
:当p
所指的元素在q
所指的前面时,表达式值为1;反之则为0;p>q
:当p
所指的元素在q
所指的后面时,表达式值为1;反之则为0;p==q
:当p
和q
指向同一元素时,表达式值为1;反之则为0;p!=q
:当p
和q
不指向同一元素时,表达式值为1;反之则为0;
指针变量还可以与
0
或者NULL
比较。设
p
为指针变量,则p==0
或者p==NULL
表面是空指针,它不指向任何变量;p!=0
或者p!=NULL
表示p
指针不是空指针。【实例】指针变量间的关系运算
【代码示例】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main(){
int a[5] = {1, 2, 3, 4, 5}, *y, *x;
x = a;
y = a;
if(x<y){
printf("x<p\n");
printf("地址为:%p 值为:%d\n", x,*x);
printf("地址为:%p 值为:%d", y,*y);
}else{
printf("x>p\n");
printf("地址为:%p 值为:%d\n", x,*x);
printf("地址为:%p 值为:%d", y,*y);
}
if(y==0){
printf("\ny指针是空指针");
}else{
printf("\ny指针不是空指针");
}
return 0;
}【输出】
二维数组的指针表示
用二维数组名表示数组元素
如果有
1 | int a[M][N]; |
则**将二维数组中的元素a[i][j]
转换为唯一线性地址的一般公式为:线性地址$= a+i \times M + j$**。
其中:a
为数组的首地址,M
和N
分别为二维数组行和列的元素个数。
如下代码示例:
1 | int a[3][3],*p; |
则二维数组a
的数据元素在内存中的存储顺序以及地址关系如下图所示:
对于数组元素a[i][j]
,用数组名a
的表示形式为:
1 | //*(a+i) 取出指向i层首地址的地址 |
数组名是一种常量指针
【实例】关于数组指针相关问题理解
【代码示例】
1 |
|
【输出】
用指针表示二维数组元素
根据下图可知,可以通过对指针的加减来实现对二维数组元素的指向。
对于如下代码,指针指向数组元素a[x][y]
的公式为:
$$p+M \times N +y$$
1 | int a[M][N]; |
a[0]
和*a
指的是同一个地址,即首层首元素地址。
【实例】利用指针输出二维数组元素值
【代码示例】
1 |
|
【输出】
在C语言中,可以将一个二维数组理解为若干个一维数组构成的一维数组。所以对于a[3][3]
数组来说,可以分解为三个一维数组:a[0][0]
,a[0][1]
,a[0][2]
……a[2][0]
,a[2][1]
,a[2][2]
,即可以看作由a[0][0]
,a[1][0]
,a[2][0]
三个行首元素数组。
行指针是一种特殊的指针变量,它们专门用于指向一维数组。定义一个行指针一般的格式是:
1 | 类型关键字 (*行指针名)[M] |
其中M
规定了行指针所指一维数组的长度,而类型关键字则指明了一维数组的类型。例如:
1 | int (*p)[M]; |
定义了行指针p
,可以使用该行指针指向二维数组单行元素为M
个的整型数组。
【实例】使用行指针输出二维数组中全部的元素值
【代码示例】
1 |
|
对
p
层加减操作是改变层数的指向,对*p
的操作是改变的元素的指向。
【关于指针指向多维数组的个人看法】
C语言底层就是将多维数组,例如二维数组,看成两个一维数组,这就意味着,对于二维数组a
,之前说过数组是一个常量指针,那么a
是指向第一个一维数组的指针,也就是说a
存储的是一个地址,这也就是为什么如果使用普通指针指向二维数组a
,例如:*a
获取的只是第一个一维数组的地址,而**a
才是获取第一个一维数组第一个元素的首元素的值。
指针与字符串
C语言没有字符串变量,对字符串的访问有两种方法。
使用字符数组来存放一个字符串,然后采用字符数组来完成操作。例如:
1
2
3char a[12]="hello world";
//输出
printf("%s",a);使用字符指针指向一个字符串
如果把字符数组的首地址赋给一个指针变量,那么这个指针变量指向这个数组,就可以对齐完成数组操作。
1
2//使用字符串常量对字符指针进行初始化,此时指针变量指向字符串的首地址。
char *str="hello world";
【实例】编写程序完成字符串的输出
1 |
|
【输出】
虽然没有定义数组
str
,但是字符串在内存中的存储方式为数组,即str[2]
=*(str+2)
指针数组和指向指针的指针
指针数组
指针数组的定义
如果数组中每个元素的数据类型为指针类型,则这种数组称为指针数组,它是指针的集合。语法格式如下:
1
2
3
4//语法格式
类型 * 数组名[常量表达式];
//代码示例
int *p[10]; //表示定义一个由10个指针变量构成的指针数组。指针数组在字符串中的使用
指针数组常用来表示一组字符串,这时指针数组的每个元素被赋予每个字符串的首地址。代码示例:
1
char *str[7]={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"};
当然也可以使用一个二维数组来表示上面的信息,代码示例:
1
char week[7][10]={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"};
它们在内存的存储结结构如下所示:
该数组一共占用70个字节。
如果采用指针数组来表示,由于指针数组的每个元素都是指针,因此指向每个元素的首地址,通过首地址访问该字符串。相对于二维数组可以节省内存空间。如下:
【实例】编写程序,用星期的英文名来初始化字符指针数组,输入整数,当数为0~6时,输出对应星期的英文名,否则显示错误信息。
【代码示例】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void PutOut(char *p[], int value);
int main(){
char *weekday[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
int value;
printf("请输入数值");
scanf("%d", &value);
PutOut(weekday,value);
return 0;
}
void PutOut(char *p[],int value){
if(value>=0 && value<=6){
printf("%s", p[value]);
}else{
printf("输出格式不正确");
}
}【输出】
指向指针的指针
一个指针可以指向任何一种数据类型,包括指向一个指针。当指针变量p
中存放另一个指针q
的地址时,则称为指针型指针,也称为多级指针。指针型指针(二级指针)的语法格式:
1 | //语法格式 |
由于指针变量的类型是被指针所指的变量的类型,因此,上述定义中的类型标识符应为:被指针类型的指针指向的指针所指的变量的类型。代码示例:
1 | int x=2; //初始化整型变量x=2 |
对于输出值:**q
=*(*q)
=*p
=x
End
指针很重要,万物皆指针!!!