C 指针
1、什么是指针?
指针(pointer)是一个值为内存地址的变量(或数据对象)。
正如 char 类型变量的值是字符, int 类型变量的值是整数,指针变量是一种特殊的变量,是专门用来存储内存地址的。
1.1 变量的名称、地址、值之间的关系?
编写程序时,可以认为变量有两个属性:名称、值(还有其他性质,如变量类型,暂且不讨论)。计算机编译和加载程序时,认为变量也有两个属性:地址、值。地址就是变量在计算机内部的名称。
一个指针变量有两个属性:地址值、指针类型。地址值用来标识指针所指向的变量的首地址; 指针类型告诉编译器, 应该以什么数据类型对指定的内存区域进行访问。
简而言之,普通变量把值作为基本量,把地址作为通过 & 运算符获得的派生量;而指针变量,把地址作为基本量,把值作为通过 * 运算符获得的派生量。
1.2 什么是地址?
既然指针变量的值是地址,那么地址又是什么?为什么不直接使用地址,还要使用指针,搞的这么麻烦?
内存中,以 1 个字节(8 位二进制)作为 1 个基本存储单元,每个存储单元都有一个地址,是一个整数编号,一般用十六进制表示。
如果一个变量占有多个字节(即 占有多个存储单元),那么这个编号就是该变量的首地址(连续内存中最前面的存储单元的编号)。
通过这个地址,可以找到内存中对应的存储单元;根据指针类型,可以知道这个变量在内存中占有多少个字节(多少存储单元);有了对应的存储单元,又有了占有的字节数,就能访问内存中所存储的数据了。
1.3 指针 与 地址 的关系?
指针与地址的关系.png
前文已经说过,一个指针变量有两个属性:地址值、指针类型。指向数据的指针不仅记录该数据在内存中存放的地址,还记录该数据的类型,即在内存中占有多少字节,这是地址所不具备的。正因为地址是没有类型的,所以不能对地址进行算术操作。
变量的地址是内存地址,是系统给每个存储单元拟定的编号。
存放地址的变量,称之为指针变量。
1.4 关于指针的运算符
1.4.1 一元 & 运算符 :给出变量的内存地址
int a = 10;
printf("变量 a 的内存地址 :%p \n",&a);
//变量 a 的内存地址 :0x7fff5fbff5e0
从上面代码看出,& 运算符写在变量前面,是取地址运算符, 任何变量都是放在内存中的, & 就是获得变量在内存中地址。
1.4.2 解引用运算符(dereference operator)*
int a = 10;
int *p = &a;
printf("*p :%d \n",*p);//*p :10
printf(" p :%p \n",p);// p :0x7fff5fbff5e0
上面代码, * 在声明处 和 后面操作变量出现两次:
- 运算符
*在声明指针变量时 : 是指针声明符, 说明声明的这个变量p是指针; - 运算符
*在操作指针变量时: 是取值符, 取出指针p所指向地址的值。
注意:
虽然指针p的值为 0x7fff5fbff5e0 ,是一个十六进制的整数,但是并不能用处理整数的操作来处理指针,两个整数可以相乘,但是两个把两个指针相乘。实际上,指针是一种新类型,而非整数类型。下文将会提到指针类型
1.5 指针本身所占据的内存区
int *p1;
char *p2;
double *p3;
printf("sizeof(p1) : %zd ; sizeof(p2) : %zd ; sizeof(p3) : %zd \n",sizeof(p1),sizeof(p2),sizeof(p3));
//sizeof(p1) : 8 ; sizeof(p2) : 8 ; sizeof(p3) : 8
在64位系统中,采用 sizeof()函数打印出,指针本身占据 8 个字节的长度。
2、指针(pointer)的声明:
如何使用一个指针呢?就像其他类型的变量一样,在使用指针变量之前,必须先对其声明。
指针变量声明的一般形式为: type *varName;
-
type是指针的基本类型,它必须是一个有效的 C 数据类型:int、char等; -
type *是指针变量的数据类型; -
varName是指针变量的名称。
2.1 指针的类型 和 指针所指向的类型:
上文提到了指针的类型 和 指针变量的数据类型,这是两个概念,切忌混淆。
2.1.1 指针的类型:
任何变量都有自己的类型,指针变量也有其数据类型,指针的类型是指针变量本身所具有的数据类型。例如:
//变量 a 的数据类型为 int,
int a = 1;
//我们可以理解为:用什么声明这个变量,那么这个变量的类型就是什么。
从语法上看:把指针声明语句里的指针变量名去掉,剩下的部分就是这个指针的类型。
int *p1;//指针的类型是 int * (用 int *声明的变量p1)
char *p2;//指针的类型是 char *
int **p3;//指针的类型是 int ** (用 int **声明的变量p3)
int (*p4)[3];//指针的类型是 int (*)[3]
int *(*p5)[4]; // 指针的类型是int *(*)[4]
2.1.2 指针所指向的类型:
指针所指向的类型:决定了编译器把那块内存区的数据当做什么来看待。
int a = 1;
int *p = &a;
//指针变量 p 指向变量 a 的地址,那么指针 p 所指向的类型为 int
从语法上看:把指针声明语句中的指针名及其左边的第一个指针声明符 * 去掉,剩下的就是指针所指向的类型。
int *p1;//指针所指向的类型是 int
char *p2;//指针所指向的类型是 char
int **p3;//指针所指向的类型是 int *
int (*p4)[3];//指针所指向的类型是 int ()[3]
int *(*p5)[4]; //指针所指向的类型是int *()[4]
每当遇到一个指针,我们都应该问自己:这个指针的类型是什么?指针指向的类型是什么?该指针指向了哪里?
2.2 空指针 NULL
注意:声明一个指针变量时,如果没有确切的地址,为指针变量赋一个 NULL 值是一个良好的编程习惯。
例如:
int *a = NULL;
2.2.1 解引用未初始化的指针:
void statementPointer(void)
{
int *p;//未初始化的指针
//waring:Variable 'p' is uninitialized when used here
printf("指针 p 的地址为: %p \n",p);
//指针 p 的地址为: 0x1f0000001e
*p = 5;//严重的错误
//报错:Thread 1: EXC_BAD_ACCESS (code=1, address=0x1f0000001e)
}
为何不行?将数据 5 存储在指针变量 p 指定地址的这块内存,但是 p 未被初始化,其值是一个随机的内存地址,所以不知道数据 5 将被存储在哪块内存。
这时会出现什么错误?可能会擦写数据或代码,或者导致程序崩溃。
切记:创建一个指针时,系统只分配了存储指针本身的内存,并未分配存储数据的内存,因此在使用指针之前,必须先用已分配的地址初始化它。
2.2.2 关于空指针 NULL
上文例子说明:声明指针变量时,如果暂时不能确定其指向,可以先赋值为NULL 。
NULL 是一个定义在标准库中的值为 0 的指针常量。赋为 NULL 的指针被称为空指针。
#define NULL ((void *)0) // NULL的宏定义
外层的括号是为了防止宏定义歧义; 里层的括号则是强制类型转换, 把0 转换成 void *类型, 本来 void * 型就是用来存放地址的, 那么这里的 0自然就是地址 0 了。
空指针是有指向的指针, 但它指向的地址是很小的地址, 约定俗成为地址0x0, 是程序的起始, 这个地址不保存数据, 同时不允许程序访问。所以空指针不能操作该地址, 我们就理解为 指针指向了空, 无法操作了。
void statementNULLPointer(void)
{
int *p = NULL;//初始化为NULL的指针
printf("指针 p 的地址为: %p \n",p);
//指针 p 的地址为: 0x1f0000001e
*p = 5;//严重的错误:0x 地址不保存数据, 同时不允许程序访问,所以 NULL 不能操作该地址
//报错:Thread 1: EXC_BAD_ACCESS (code=1, address=0x1f0000001e)
}
2.3 关于无确定类型 void *
上文 NULL 的宏定义里提到的 void * 型指针又是什么呢?看起来像是空指针。然而,void * 型指针并不是空指针,这个指针指向了实实在在存放数据的地址,但是该地址存放数据的数据类型我们暂时不知道, 可以理解为无确定类型指针。
void * 型指针可以通过类型转换强制转换为其他类型的指针。
3 指针的初始化与赋值
前文我们声明了一个指针变量,要给它初始化才有意义。给指针变量初始化或者赋值,其实就是让指针指向某个内存地址。
下面我们看一段程序:(注:printf() 语句后面的注释为该语句打印的内容)
void pointerInitialize(void)
{
int a = 10, b = 20;
printf("&a = %p , &b = %p \n",&a,&b);
//&a = 0x7fff5fbff65c , &b = 0x7fff5fbff658
/*
1、定义一个指针变量 p ,并初始化其值为变量 a 的地址(指向a的地址)
2、这个指针变量名为 p ,而不是 *p
3、指针的类型为 int * ,指针所指向的类型是 int,
4、指针的类型(指针本身的类型) 与 指针所指向的类型是两个概念
5、解引用运算符 * :给出指针指向地址上存储的值
6、& 运算符:给出变量的存储地址, &a 给出变量 a 的内存地址
7、通过 printf() 语句,可以知道我们成功初始化指针变量 p 的值为 int 型变量 a 的内存地址,
这时通过解引用 * 可以打印出:存储在指针变量 p 所指向地址的数据为 5,
即变量 a 的值
8、注意:地址应该和指针类型兼容,不能把 double 类型的地址赋给指向 int 的指针
*/
int *p = &a;
printf("a = %d, *p = %d ,p = %p \n", a, *p ,p);
// a = 10, *p = 10 ,p = 0x7fff5fbff65c
/*
如同变量 a 可以重新赋值一样,指针变量也可以修改其指向的内存地址(给指针变量重新赋值)
通过 & 运算符,将变量 b 的地址赋给指针变量 p
*/
p = &b;
printf("b = %d, *p = %d ,p = %p \n", b, *p ,p);
// b = 20, *p = 20 ,p = 0x7fff5fbff658
// 允许修改指针变量所指向的内存地址的值
// 此时 p 所指向的地址是变量 b 所在的内存地址,修改此地址的内容也就是给该地址所存储的变量 b 重新赋值
*p = 30;
printf("b = %d, &b = %p ,*p = %d ,p = %p \n", b, &b, *p ,p);
// b = 30, &b = 0x7fff5fbff658 ,*p = 30 ,p = 0x7fff5fbff658
//变量 b 的内存地址不变,将该内存的内容换成 40;
//指针变量 p 的值也不变,是变量 b 的内存地址,所以 *p 是 40
b = 40;
printf("b = %d, &b = %p ,*p = %d ,p = %p \n", b, &b, *p ,p);
//b = 40, &b = 0x7fff5fbff658 ,*p = 40 ,p = 0x7fff5fbff658
}
从上述程序,我们可以了解到:
- 我们给指针变量赋值后,还可以再次重新为指针变量赋值;给指针变量重新赋值,也就是将该指针再次指向别的内存地址;
- 允许通过
*运算符修改指针变量所指向的内存地址的值。
4 指针 和 数组
前文介绍过,指针提供了一种以符号形式使用地址的方法。在接下来我们就会了解到数组表示法其实是在变相的使用指针。
4.1 数组名是数组首元素的地址
void pointerAndArray(void)
{
int array[3] = {1,2,3};
int *p = array;
printf("p = %p ,array = %p ,&array[0] = %p \n",p,array,&array[0]);
//p = 0x7fff5fbff61c ,array = 0x7fff5fbff61c ,&array[0] = 0x7fff5fbff61c
printf("*p = %d ,*array = %d ,array[0] = %d \n",*p,*array,array[0]);
//*p = 1 ,*array = 1 ,array[0] = 1
}
我们可以观察到:数组名 array 就是数组首元素地址,将 array 赋值给指针变量 p 后,对变量 p 通过解引用运算符 * 可以得到数组首元素 1。
4.1.1 指针加上一个数时,它的值会发生什么变化?
我们来看下面程序:
void pointerAndArray(void)
{
//在我们的系统中,地址按字节编址,short 类型占 2 个字节,double 类型占 8 个字节
printf("sizeof(short) : %zd ;sizeof(double) : %zd \n",sizeof(short),sizeof(double));
//sizeof(short) : 2 ;sizeof(double) : 8
short dates[4];
short * pti;
short index;
double bills[4];
double * ptf;
pti = dates;//把数组地址赋给指针
ptf = bills;
printf("%30s %15s \n","short","double");
for (index = 0 ; index < 4; index ++)
{
printf("pointers + %d : %10p %10p \n",index,pti + index,ptf + index);
printf("array element + %d : %10p %10p \n",index,&dates[index],&bills[index]);
}
}
打印结果:
short double
pointers + 0 : 0x7fff5fbff5b0 0x7fff5fbff590
array element + 0 : 0x7fff5fbff5b0 0x7fff5fbff590
pointers + 1 : 0x7fff5fbff5b2 0x7fff5fbff598
array element + 1 : 0x7fff5fbff5b2 0x7fff5fbff598
pointers + 2 : 0x7fff5fbff5b4 0x7fff5fbff5a0
array element + 2 : 0x7fff5fbff5b4 0x7fff5fbff5a0
pointers + 3 : 0x7fff5fbff5b6 0x7fff5fbff5a8
array element + 3 : 0x7fff5fbff5b6 0x7fff5fbff5a8
在 C 中,指针加 1 指的是增加一个存储单元(这个存储单元的大小依据变量的类型来确定)。对于数组而言,这意味着加 1 后的地址是下一个元素的地址,而不是下一个字节的地址。这也是为什么必须声明指针所指向的对象类型的原因之一。只知道内存首地址是不够的,还需要知道这个变量占据多少的字节的存储空间。
4.2 指针变量的基本操作
C 提供了一些基本的指针操作;我们来看程序:
void pointerOperation(void)
{
printf("sizeof(int) = %zd \n",sizeof(int));
// sizeof(int) = 4 (int 型占 4 个字节)
/*
1、声明一个含有5个int 类型元素 的 数组
2、初始化每个元素的值
3、array 数组名,是数组首元素的地址
4、int 占四个字节,这个数组有五个 int 类型元素,占 20 个字节
*/
int array[5] = {100,200,300,400,500};//声明一个整型数组,并初始化
printf("sizeof(array) = %zd \n",sizeof(array));
// sizeof(array) = 20
/*
1、声明三个 int* 类型的指针变量
2、由于未初始化,所以指针变量指向的地址,既指针变量的值不确定是随机值
*/
int *p1 , *p2 , *p3;//声明指针变量
printf("p1 = %p , p2 = %p , p3 = %p \n",p1,p2,p3);
//p1 = 0x2b00000028 , p2 = 0x280000003d , p3 = 0x3d00000024
/*
此时:p1 与 p2 的值 相差 8 个字节,2个 int 元素
指针 p1 指向内存地址的值 为100 ,既数组 array 的首位元素 100 ,是第0位元素的地址
指针 p2 指向内存地址的值 为300 ,既数组 array 的第2位元素 300,是第2位元素的地址
以上说明:说明数组元素的内存地址是连续的
指针变量 p1 的内存地址为 0x7fff5fbff638
指针变量 p2 的内存地址为 0x7fff5fbff630
为什么 p1 比 p2 的地址高呢? 因为栈空间是从 高地址 向 低地址 扩展的,先声明的 p1,那么 p1 的内存地址自然比 p2 的高
*/
p1 = array;//数组名是数组首元素的地址
p2 = &array[2];//把一个地址赋给指针
printf("p1 = %p , *p1 = %d , &p1 = %p \n",p1,*p1,&p1);
//p1 = 0x7fff5fbff640 , *p1 = 100 , &p1 = 0x7fff5fbff638
printf("p2 = %p , *p2 = %d , &p2 = %p \n",p2,*p2,&p2);
//p2 = 0x7fff5fbff648 , *p2 = 300 , &p2 = 0x7fff5fbff630
/*
指针 p1 + 4 = 0x7fff5fbff640 + 4 * sizeof(int) = 0x7fff5fbff640 + 16 = 0x7fff5fbff650
指针减去一个整数:指针必须是减数,整数是被减数
*/
p3 = p1 + 4;//指针加法
printf("p1 + 4 = %p , *(p1 + 4) = %d , &p3 = %p \n",p1 + 4,*(p1 + 4),&p3);
//p1 + 4 = 0x7fff5fbff650 , *(p1 + 4) = 500 , &p3 = 0x7fff5fbff628
printf("p3 = %p , *p3 = %d , &p3 = %p \n",p3,*p3,&p3);
//p3 = 0x7fff5fbff650 , *p3 = 500 , &p3 = 0x7fff5fbff628
/*
递增:让 该指针 移动至数组的下一个元素
此时:变量 p1 的内存地址 仍为 0x7fff5fbff638
注意:变量不会因为值发生变化就移动位置
*/
p1 ++;//递增指针
printf("p1++ = %p , *(p1++) = %d , &p1 = %p \n",p1,*p1,&p1);
//p1++ = 0x7fff5fbff644 , *(p1++) = 200 , &p1 = 0x7fff5fbff638
p2 --;//递减指针
printf("p2-- = %p , *(p2--) = %d , &p2 = %p \n",p2,*p2,&p2);
//p2-- = 0x7fff5fbff644 , *(p2--) = 200 , &p2 = 0x7fff5fbff630
/*
两个指针相减:指针求差 (通常求差的两个指针分别是同一数组的不同元素)
通过求差,计算出两个元素之间的距离
*/
printf("p3 - p2 = %td \n",p3 - p2);
//p3 - p2 = 3
//指针减去整数
printf("p3 - 2 = %p \n",p3 - 2);
//p3 - 2 = 0x7fff5fbff648
}
上面一段程序演示了指针变量的基本操作:
-
赋值: 可以把地址赋值给指针。例如:使用数组名、带地址运算符
&的变量名、另一个指针进行赋值。
注意:地址应该与指针类型兼容;不能把double型的地址赋给指向int的指针。 -
解引用:
*运算符给出指针指向地址上存储的值。 - 取址: 和所有变量一样,指针变量也有自己的地址 和 值。
-
指针与整数相加: 可以使用
+运算符把指针与整数相加,或者整数与指针相加。整数和指针所指向类型的大小(以字节为单位)相乘,然后把乘积与指针的值相加;
注意:在做加法时,编译器不会检查指针是否仍然指向数组元素,C 只能保证指向数组任意元素的指针 和 数组后面第一个位置的指针有效,如果超出了这个范围,则是未定义。 - 递增指针: 指向数组元素的指针可以通过递增让该指针移动至数组的下一个元素。
-
指针减去一个整数: 可以使用
-运算符从一个指针减去一个整数。
必须指针是减数,整数是被减数。如果相减的结果超出初始指针所指向的数组的范围,计算结果是未定义的。 - 递减指针: 指向数组元素的指针可以通过递减让该指针移动至数组的上一个元素。
- 指针求差: 可以计算两个指针的差值。求差的两个指针分别指向同一数组的不同元素,通过求差计算出两个元素之间的距离
- 比较: 使用关系运算符可以比较两个指针的值,前提是这两个指针都指向相同类型的对象。