文章目录
- 一,什么是指针
- 1,内存
- 2,指针是什么?
- 二,指针的声明
- 1,声明指针类型变量
- 2,二级指针
- 三,指针的计算
- 1,两个指针运算符
- 1.1 *运算符
- 1.2 & 运算符
- 1.3 &运算符与*运算符的关系
- 2,指针变量的初始化
- 2.1 指针变量的大坑
- 2.2 指针变量初始化
- 2.3 指针变量初始化最佳实践
- 三,指针的运算
- 1,指针与整数值的加减运算
- 2,指针与指针的加法运算
- 3,指针与指针的减法
- 4,指针与指针的比较运算
指针是 C 语言的重点,也是难点,这篇文章主要讲解指针是什么以及如何使用。
一,什么是指针
1,内存
指针与内存是息息相关的,在学习指针之前,先回忆下内存相关的知识。
内存是程序运行期间存储数据的硬件设备,为了方便管理,计算机将内存划分为一个个小的单元,每个单元的大小是一个字节。
如果把内存比作一栋酒店大楼,内存单元就像是一个个小的房间,数据就住在小房间里。
我们知道,为了客人能准确找到属于自己的房间,酒店房间是有房号的。
同样,内存单元也是有编号的,这个编号在计算机中称之为“内存地址
”。
2,指针是什么?
指针就是内存单元的编号,本质上是一个内存地址,相当于房卡上的房间号。
从形式上看,指针和整型数据并没有什么区别,都是数字。区别在于,指针是内存地址,不用于与其他数据进行加减乘除等运算,也不会展示给用户。
二,指针的声明
1,声明指针类型变量
在编写代码过程中,通常会声明一个变量,然后对变量进行赋值或者其他各种运算。
要使用指针,也需要声明一个指针类型的变量。
一定要牢记:指针变量就是一个普通变量,只不过它的值是内存地址而已。
指针类型由两部分构成:
- ①指针标识符,C语言用字符
*
表示指针 - ②指针类型,指针不能单独存在,必须和数据类型一起出现,表明这个指针是某种数据类型的指针
比如,char*
表示一个指向字符的指针,float*
表示一个指向float类型的值的指针。
int* intPtr;
上面示例声明了一个变量intPtr,它是一个指针,指向的内存地址存放的是一个整数。
星号*可以放在变量名与类型关键字之间的任何地方,下面的写法都是正确的。
int *intPtr;
int * intPtr;
int* intPtr;
推荐使用星号紧跟在类型关键字后面的写法,即int* intPtr;
。
声明指针变量时需要注意,如果要在一行声明多个指针变量,每个变量前都要携带字符*
。
// 正确
int * foo, * bar;
// 错误
int* foo, bar;
上面示例中,第二行实际上仅仅声明了一个指针变量,foo
是整数指针变量,而bar
是整数变量,即*只对第一个变量生效。
2,二级指针
一个指针指向的可能还是指针,这时就要用两个星号**
表示,这种指针通常称为二级指针
。
int** foo;
上面示例表示变量foo是一个指针,即变量foo存储的还是一个内存地址,这个内存地址指向的内存中存储的则是一个整数。
int a = 10;
// &a表示a变量的内存地址
int* pa = &a;
// &a表示指针变量pa的内存地址
int** ppa = &pa;
三,指针的计算
1,两个指针运算符
1.1 *运算符
*
这个字符除了声明变量时代表指针外,还可以作为运算符,用来获取指针指向的内存中的值。
void plus1(int* p) {
*p = *p + 1;
}
上面代码中,函数plus1的参数是一个整数指针p。
函数体里面,*p就表示指针p所指向的那个整数值。
对*p赋值,就是改变指针p指向的内存中的值。
这有点绕,和普通变量对比更容易理解。
int a = 10;
int b = 100;
// 将b的地址赋于指针pb
int* pb = &b;
*pb = *pb +1;
a = a + 1;
对于上述代码的最后两行:
*pb = *pb +1
,这个表达式可以拆解为4步,①计算机首先从指针变量pb中取出地址0xffeecc
,②再去这个地址指向的内存单元中获取整数100
,③然后执行运算100+1
,执行完成后,④0xffeecc
这个内存单元的值就变成101。a = a + 1
,相当于上面的表达式,执行过程更简单。①计算机从变量a
对应的内存直接取出整数10
,②然后执行运算10+1
,③执行完成后,a变量对应的内存的数据更新为11
。
1.2 & 运算符
&运算符用来取出一个变量所在的内存地址。
int x = 1;
printf("x's address is %p\n", &x);
上面示例中,x是一个整数变量,&x就是x的值所在的内存地址。printf()的%p是内存地址的占位符,可以打印出内存地址。
上一小节中,参数变量加1的函数,可以像下面这样使用。
void plus1(int* p) {
*p = *p + 1;
}
int x = 1;
plus1(&x);
printf("%d\n", x); // 2
注意,调用plus1()函数以后,打印变量x的值,发现结果是2,但是我们并没有对x进行显示的重新赋值,原因调用plus1函数时,将变量c的地址作为参数进行传递,plus1直接根据地址取出初始值,执行加1的运算,然后更新内存中的值为2,不必使用变量x就可以修改x变量的值。
1.3 &运算符与*运算符的关系
&运算符与*运算符互为逆运算,下面的表达式,是成立的。
int i = 5;
if (i == *(&i)) // 正确
2,指针变量的初始化
2.1 指针变量的大坑
声明指针变量之后,编译器会为指针变量本身分配一个内存空间,这个内存空间可能还保存着历史数据。
也就是说,这个指针变量可能指向一个随机的地址。
如果此时就去读写这个地址对应的内存,可能出现非常严重的后果,必然这个地址指向的是账户余额,有可能导致账户虚增或者虚减。
int* p;
*p = 1; // 错误
上述代码是必须避免的,因为指针p指向的内存单元是随机的。
2.2 指针变量初始化
正确写法是声明指针变量声明,立即指向一个明确的地址,这就是指针变量的初始化,初始化之后再进行读写。
int* p;
int i;
p = &i;
*p = 13;
上面示例中,p是指针变量,声明这个变量后,p会指向一个随机的内存地址。
这时要将它指向一个已经分配好的内存地址,上例就是再声明一个整数变量i,编译器会为i分配内存地址,然后让p指向i的内存地址(p = &i;)。
完成初始化之后,就可以对p指向的内存地址进行赋值了(*p = 13;)。
2.3 指针变量初始化最佳实践
强烈推荐,声明指针变量的同时,将指针变量的值设为NULL。
int* p = NULL;
NULL在 C 语言中是一个常量,表示地址为0的内存空间,这个地址是无法使用的,读写该地址会报错。
这样即使之后我们忘记了把指针变量p指向预期的内存地址,在程序运行过程中会报错,而不是以可怕的、随机的方式运行。
三,指针的运算
我们现在知道了,指针虽然代表的是内存地址,但其本质上是一个无符号整数。
C语言允许指针参与运算,但是指针的运算规则和整数的运算规则是相差很大的。
1,指针与整数值的加减运算
指针与整数值的运算,表示指针的移动。
short* j;
j = (short*)0x1234;
j = j + 1; // 0x1236
上面示例中,j是一个指针,指向内存地址0x1234
。
由于0x1234本身是整数类型(int),跟j的类型(short*)并不兼容,所以强制使用类型投射,将0x1234转成short*。
表明上看,j + 1应该等于0x1235,但正确答案是0x1236。
原因是j + 1表示指针向内存地址的高位移动一个单位,而一个单位的short类型占据两个字节的宽度,所以相当于向高位移动两个字节。同样的,j - 1得到的结果是0x1232。
指针移动的单位,与指针指向的数据类型有关。数据类型占据多少个字节,每单位就移动多少个字节。
2,指针与指针的加法运算
指针只能与整数值进行加减运算,两个指针进行加法是非法的。
unsigned short* j;
unsigned short* k;
x = j + k; // 非法
上面示例是两个指针相加,这是非法的。
3,指针与指针的减法
相同类型的指针允许进行减法运算,返回它们之间的距离,即相隔多少个数据单位。
高位地址减去低位地址,返回的是正值;低位地址减去高位地址,返回的是负值。
这时,减法返回的值属于ptrdiff_t
类型,这是一个带符号的整数类型别名,具体类型根据系统不同而不同。这个类型的原型定义在头文件stddef.h里面。
short* j1;
short* j2;
j1 = (short*)0x1234;
j2 = (short*)0x1236;
ptrdiff_t dist = j2 - j1;
printf("%td\n", dist); // 1
上面示例中,j1和j2是两个指向 short 类型的指针,变量dist是它们之间的距离,类型为ptrdiff_t,值为1,因为相差2个字节正好存放一个 short 类型的值。
4,指针与指针的比较运算
指针之间的比较运算,比较的是各自的内存地址哪一个更大,返回值是整数1(true)或0(false)。