9.指针

参考资料:
C语言中文网:http://c.biancheng.net/
《C语言编程魔法书基于C11标准》
视频教程:C语音深入剖析班(国嵌 唐老师主讲)

指针的概念

计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用4个字节,char 占用1个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。

下图是 4G 内存中每个字节的编号(以十六进制表示):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JA2UOnny-1581688568851)(img/1.png)]

我们将内存中字节的编号称为地址或指针。地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。

下面的代码演示了如何输出一个地址:

#include <stdio.h>
int main(){
    int a = 100;
    char str[20] = "www.baidu.com";
    printf("%#X, %#X\n", &a, str);
    return 0;
}

运行结果:
0X28FF3C, 0X28FF10

%#X表示以十六进制形式输出,并附带前缀0X。a 是一个变量,用来存放整数,需要在前面加&来获得它的地址;str 本身就表示字符串的首地址,不需要加&

一切都是地址

C语言用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供 CPU 使用。

数据和代码都以二进制的形式存储在内存中,计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据。

CPU 只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。如果程序不小心出错,或者开发者有意为之,在 CPU 要写入数据时给它一个代码区域的地址,就会发生内存访问错误。这种内存访问错误会被硬件和操作系统拦截,强制程序崩溃,程序员没有挽救的机会。

CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。

假设变量 a、b、c 在内存中的地址分别是 0X1000、0X2000、0X3000,那么加法运算c = a + b;将会被转换成类似下面的形式:

0X3000 = (0X1000) + (0X2000);

( )表示取值操作,整个表达式的意思是,取出地址 0X1000 和 0X2000 上的值,将它们相加,把相加的结果赋值给地址为 0X3000 的内存

变量名和函数名为我们提供了方便,让我们在编写代码的过程中可以使用易于阅读和理解的英文字符串,不用直接面对二进制地址,那场景简直让人崩溃。

需要注意的是,虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址。

指针变量

数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。

在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。指针变量的值就是某份数据的地址,这样的一份数据可以是数组、字符串、函数,也可以是另外的一个普通变量或指针变量。

现在假设有一个 char 类型的变量 c,它存储了字符 ‘K’(ASCII码为十进制数 75),并占用了地址为 0X11A 的内存(地址通常用十六进制表示)。另外有一个指针变量 p,它的值为 0X11A,正好等于变量 c 的地址,这种情况我们就称 p 指向了 c,或者说 p 是指向变量 c 的指针。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wIFUlqAr-1581688568853)(img/2.png)]

定义指针变量

定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*,格式为:

datatype *name;

或者

datatype *name = value;

*表示这是一个指针变量,datatype表示该指针变量所指向的数据的类型 。例如:

int *p1;

p1 是一个指向 int 类型数据的指针变量,至于 p1 究竟指向哪一份数据,应该由赋予它的值决定。再如:

int a = 100;
int *p_a = &a;

在定义指针变量 p_a 的同时对它进行初始化,并将变量 a 的地址赋予它,此时 p_a 就指向了 a。值得注意的是,p_a 需要的一个地址,a 前面必须要加取地址符&,否则是不对的。

和普通变量一样,指针变量也可以被多次写入,只要你想,随时都能够改变指针变量的值,请看下面的代码:

//定义普通变量
float a = 99.5, b = 10.6;
char c = '@', d = '#';
//定义指针变量
float *p1 = &a;
char *p2 = &c;
//修改指针变量的值
p1 = &b;
p2 = &d;

*是一个特殊符号,表明一个变量是指针变量,定义 p1、p2 时必须带*。而给 p1、p2 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*

假设变量 a、b、c、d 的地址分别为 0X1000、0X1004、0X2000、0X2004,下面的示意图很好地反映了 p1、p2 指向的变化:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qfKnPA1B-1581688568854)(img/3.jpg)]

指针类型

float *p1char *p2 的类型分别是float*char*,而不是floatchar,它们是完全不同的数据类型。

指针变量也可以连续定义,例如:

int *a, *b, *c;  //a、b、c 的类型都是 int*

注意每个变量前面都要带*。如果写成下面的形式,那么只有 a 是指针变量,b、c 都是类型为 int 的普通变量:

int *a, b, c;

通过指针变量取得数据

指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:

*pointer;

这里的*称为指针运算符,用来取得某个地址上的数据,请看下面的例子:

#include <stdio.h>
int main(){
    int a = 15;
    int *p = &a;
    printf("%d, %d\n", a, *p);  //两种方式都可以输出a的值
    return 0;
}

运行结果:
15, 15

假设 a 的地址是 0X1000,p 指向 a 后,p 本身的值也会变为 0X1000,*p 表示获取地址 0X1000 上的数据,也即变量 a 的值。从运行结果看,*pa是等价的,可以把*想象成一把钥匙,用*这把钥匙才能打开对应的内容。

CPU 读写数据必须要知道数据在内存中的地址,普通变量和指针变量都是地址的助记符,虽然通过 *pa 获取到的数据一样,但它们的运行过程稍有不同:a 只需要一次运算就能够取得数据,而 *p 要经过两次运算,多了一层“间接”。

假设变量 a、p 的地址分别为 0X1000、0XF0A0,它们的指向关系如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4mQ3ImiC-1581688568854)(img/4.jpg)]

程序被编译和链接后,ap 被替换成相应的地址。使用*p 的话,要先通过地址 0XF0A0 取得变量 p 本身的值,这个值是变量 a 的地址,然后再通过这个值取得变量 a 的数据,前后共有两次运算;而使用 a 的话,可以通过地址 0X1000 直接取得它的数据,只需要一步运算。

也就是说,使用指针是间接获取数据,使用变量名是直接获取数据,所以说用指针比直接使用变量名获取数据代价要更高。

指针除了可以获取内存上的数据,也可以修改内存上的数据,例如:

#include <stdio.h>
int main(){
    int a = 15, b = 99, c = 222;
    int *p = &a;  //定义指针变量
    *p = b;  //通过指针变量修改内存上的数据
    c = *p;  //通过指针变量获取内存上的数据
    printf("%d, %d, %d, %d\n", a, b, c, *p);
    return 0;
}

运行结果:
99, 99, 99, 99

*p 代表的是 a 中的数据,它等价于 a,可以将另外的一份数据赋值给它,也可以将它赋值给另外的一个变量。

*在不同的场景下有不同的作用:*可以用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;使用指针变量时在前面加*表示获取指针指向的数据,或者说表示的是指针指向的数据本身。

也就是说,定义指针变量时的*和使用指针变量时的*意义完全不同。以下面的语句为例:

int *p = &a;
*p = 100;

第1行代码中*用来指明 p 是一个指针变量,第2行代码中*用来获取指针指向的数据。

需要注意的是,给指针变量本身赋值时不能加*。修改上面的语句:

int *p;
p = &a;
*p = 100;

第2行代码中的 p 前面就不能加*

指针变量也可以出现在普通变量能出现的任何表达式中,例如:

int x, y, *px = &x, *py = &y;
y = *px + 5;  //表示把x的内容加5并赋给y,*px+5相当于(*px)+5
y = ++*px;  //px的内容加上1之后赋给y,++*px相当于++(*px)
y = *px++;  //相当于y=(*px)++
py = px;  //把一个指针的值赋给另一个指针

【示例】通过指针交换两个变量的值。

#include <stdio.h>
int main(){
    int a = 100, b = 999, temp;
    int *pa = &a, *pb = &b;
    printf("a=%d, b=%d\n", a, b);
    /*****开始交换*****/
    temp = *pa;  //将a的值先保存起来
    *pa = *pb;  //将b的值交给a
    *pb = temp;  //再将保存起来的a的值交给b
    /*****结束交换*****/
    printf("a=%d, b=%d\n", a, b);
    return 0;
}

运行结果:
a=100, b=999
a=999, b=100

从运行结果可以看出,a、b 的值已经发生了交换。需要注意的是临时变量 temp,它的作用特别重要,因为执行*pa = *pb;语句后 a 的值会被 b 的值覆盖,如果不先将 a 的值保存起来以后就找不到了。

*与&

假设有一个 int 类型的变量 a,pa 是指向它的指针,那么*&a&*pa分别是什么意思呢?

*&a可以理解为*(&a)&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 *pa),绕来绕去,又回到了原点,*&a仍然等价于 a。

&*pa可以理解为&(*pa)*pa表示取得 pa 指向的数据(等价于 a),&(*pa)表示数据的地址(等价于 &a),所以&*pa等价于 pa。

对星号*的总结

星号*主要有三种用途:

  • 表示乘法,例如int a = 3, b = 5, c; c = a * b;
  • 表示定义一个指针变量,以和普通变量区分开,例如int a = 100; int *p = &a;
  • 表示获取指针指向的数据,是一种间接操作,例如int a, b, *p = &a; *p = 100; b = *p;

指针汇编分析

指针的汇编代码分析

代码:

#include <stdio.h>

int main()
{
	int* p = NULL;
	int num = 10,num1,num2;
	p = &num;
	num1 = *p;
	num2 = num;
    
	return 0;

}

汇编代码

	int* p = NULL;
00DD1017  mov         dword ptr [p],0  //把0赋值到p中,其实NULL也就是0
	int num = 10,num1,num2;
00DD101E  mov         dword ptr [num],0Ah  //把10赋值到num
	p = &num;
00DD1025  lea         eax,[num]  
00DD1028  mov         dword ptr [p],eax 
//指针的赋值操作做了两步
//第一步是把num中的有效地址赋值到eax寄存器中
//第二步是把eax寄存器中的值(也就是num的地址)赋值到p中
	num1 = *p;
00DD102B  mov         eax,dword ptr [p]  
00DD102E  mov         eax,dword ptr [eax]  
00DD1030  mov         dword ptr [num1],eax  
//指针赋值需要三步,第一步是先把p中的值(也就是p,即num的地址)赋值给eax,第二步再把eax中的值赋值给eax(也就是num地址上的值),第三步是把eax中的值,也就是num的值赋值给num1
	num2 = num;
00DD1033  mov         eax,dword ptr [num]  
00DD1036  mov         dword ptr [num2],eax  

由上面的汇编语句可看到指针赋值的效率是没有直接赋值的效率高的

指针变量的运算

指针变量保存的是地址,本质上是一个整数,可以进行部分运算,例如加法、减法、比较等,请看下面的代码:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int a = 10, *pa = &a, *paa = &a;
	double b = 99.9, *pb = &b;
	char c = '@', *pc = &c;

	//最初的值
	printf("&a=%#X,  &b=%#X,  &C=%#X\n", &a, &b, &c);
	printf("pa=%#X,  pb=%#X,  &pc=%#X\n", pa, pb, pc);
	//加法运算
	pa++; 
	pb++; 
	pc++;
	printf("pa=%#X,  pb=%#X,  pc=%#X\n", pa, pb, pc);
	//减法运算
	pa -= 2;
	pb -= 2;
	pc -= 2;
	printf("pa=%#X,  pb=%#X,  pc=%#X\n", pa, pb, pc);
	//比较运算
	if (pa == paa)
	{
		printf("一样:%d\n", *paa);
	}
	else
	{
		printf("不一样:%d\n", *pa);
	}
	system("pause");
	return 0;
}

运行结果:

&a=0X10FF968,  &b=0X10FF940,  &C=0X10FF92B
pa=0X10FF968,  pb=0X10FF940,  &pc=0X10FF92B
pa=0X10FF96C,  pb=0X10FF948,  pc=0X10FF92C
pa=0X10FF964,  pb=0X10FF938,  pc=0X10FF92A
不一样:-858993460

从运算结果可以看出:pa、pb、pc 每次加 1,它们的地址分别增加 4、8、1,正好是 int、double、char 类型的长度;减 2 时,地址分别减少 8、16、2,正好是 int、double、char 类型长度的 2 倍。

以 a 和 pa 为例,a 的类型为 int,占用 4 个字节,pa 是指向 a 的指针,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kJUR91Fa-1581688568855)(img/5.jpg)]

刚开始的时候,pa 指向 a 的开头,通过 *pa 读取数据时,从 pa 指向的位置向后移动 4 个字节,把这 4 个字节的内容作为要获取的数据,这 4 个字节也正好是变量 a 占用的内存。

如果pa++;使得地址加 1 的话,就会变成如下图所示的指向关系:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YAyVSce6-1581688568856)(img/6.jpg)]

这个时候 pa 指向整数 a 的中间,*pa 使用的是红色虚线画出的 4 个字节,其中前 3 个是变量 a 的,后面 1 个是其它数据的,把它们“搅和”在一起显然没有实际的意义,取得的数据也会非常怪异。

如果pa++;使得地址加 4 的话,正好能够完全跳过整数 a,指向它后面的内存,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YGCczcKw-1581688568857)(img/7.jpg)]

我们知道,数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加 1 就表示指向下一个元素,减 1 就表示指向上一个元素,这样指针的加减运算就具有了现实的意义

不过C语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,我们往往不进行加减运算,虽然编译器并不会报错,但这样做没有意义,因为不知道它后面指向的是什么数据。

实际上指针运算增加的大小或减少的大小是增加指针指向的类型的大小,比如int *p;那么p++也就是增加int类型的大小p–也就是减少int类型的大小

总结

指针运算的规则:

p + n;(p为指针) == (unsigned int)p + (n * sizeof(*p))

例子:

int a = 10,*p = &a;
p + 2
//假设p的值为0x1234,也就是a的地址也为0x1234,那么p + 2就等于,(unsigned int)p + (n * sizeof(*p)) == (unsigned int)0x1234 + (2 * sizeof(int)) == (unsigned int)0x1234 + (2 * 4) = 0x123C

下面的例子是一个反面教材,警告不要尝试通过指针获取下一个变量的地址:

在 VS2017 Debug 模式下的运行结果为:

3,  -858993460,  -858993460,  2,  -858993460,  -858993460,  1,  -858993460,

可以发现,变量 a、b、c 并不挨着,它们中间还参杂了别的辅助数据。

指针变量除了可以参与加减运算,还可以参与比较运算。当对指针变量进行比较运算时,比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。

上面的代码(第一个例子)在比较 pa 和 paa 的值时,pa 已经指向了 a 的上一份数据,所以它们不相等。而 a 的上一份数据又不知道是什么,所以会导致 printf() 输出一个没有意义的数,这正好印证了上面的观点,不要对指向普通变量的指针进行加减运算。

另外需要说明的是,不能对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义。

大小比较

指针变量也能进行大小比较

#include <stdio.h>
#include <stdbool.h>
#include <stdint.h>

int main(int argc,const char * argv[])
{
	//声明了两个int32_t类型的对象,分别为a和b	
	int32_t a = 5, b = 10;
	//声明了三个int32_t类型的指针对象,分别为p、q和r。
	//注意,这里用逗号分隔的声明符列表中,如果要指明是指针类型,
	//那么每个指针对象标识符前都必须添加*。
	//对象c是一个int32_t类型的对象
	int32_t* p = &a, * q = &b, c = 0, * r = &c;
	printf("p = %x,q = %x\n", p, q);
	//指针可以进行比较大小及判断是否相等
	bool e = p > q;
	printf("p > q? %d\n", e);	
	e = p == r;
	printf("p等于r? %d\n", e);
	//比较r是否与对象e的地址相同
	printf("r等于&c? %d\n",r == &c);
	return 0;
}

运行结果:

p = effd2c,q = effd28
p > q? 1
p等于r? 0
r等于&c? 1

由上面的运算结果可得

  1. 指针比较大小,实际上是比较指针地址的大小(因为地址实际上也是一个无符号整数),所以整数越大,它就越大,0x123456就大于0x123455。
  2. 指针判断是否相等,那么就判断两个地址是否相等,即判断两个地址的数是否一致。

指针与指针的运算

指针与指针值间还能进行减法运算,其它的运算不行,并且必须参与运算的指针类型是相同的

例:

#include <stdio.h>


int main(int argc, const char* argv[])
{
	int a[] = { 1,2,3,4,5,6 },*p1 = NULL,*p2 = NULL;
	
	p1 = &a[5];
	p2 = &a[0];
	printf("p1 = %x,p2 = %x\n", p1, p2);
	printf("p1 - p2 = %d\n", p1 - p2);

	return 0;
}

运算结果:

p1 = eff8e8,p2 = eff8d4
p1 - p2 = 5

可以算出数组的下标差

公式

p1 - p2; == ( (unsigned int)p1 - (unsigned int)p2 ) / sizeof(type);
//套用公式计算 ((unsigned int)0xeff8e8 - (unssigned int)0xeff8d4) / sizeof(int *) = 14 / 4 = 5

注意:

只有当两个指针指向同一个数组中的元素时,指针相减才有意义,其意义为指针所指元素的下标差,当两个指针指向的元素不在同一个数组中时,结果未定义

指针的强制类型转换

指针也是数据类型,也能进行数据类型的转换

#include <stdio.h>

int main(int argc, const char* argv[])
{
	unsigned short a = 65535;
	unsigned char* b = &a,*c;	//自动类型转换,但会出现精度的丢失,并且编译的时候会出现警告
	c = (unsigned char*)&a;	//把a地址中的数据强制转换成了与c相对应的数据类型

	printf("a = %d,b = %d,c = %d\n", a, *b,*c);	//虽然c输出的与b一样,但这样是可以避免编译器的警告的,我们写代码要尽量的0警告,0错误
	return 0;
}

运行结果:

a = 65535,b = 255,c = 255

在C语言中,类型一定是要一一对应的,否则就会出现一些未知的BUG。

指针与数组的关系

指针对象与数组对象属于两种不同的类别,数组属于聚合类型,而指针属于标量类型,数组和指针不等价的一个典型案例就是求数组的长度,这个时候只能使用数组名,不能使用数组指针

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int a[6] = { 0,1,2,3,4,5 };
	int *p = a;
	int len_a = sizeof(a) / sizeof(int);
	int len_p = sizeof(p) / sizeof(int);
	printf("len_a = %d, len_p = %d\n", len_a, len_p);
	system("pause");
	return 0;
}

运行结果:

len_a = 6, len_p = 1

数组是一系列数据的集合,没有开始和结束标志,p 仅仅是一个指向 int 类型的指针,编译器不知道它指向的是一个整数还是一堆整数,对 p 使用 sizeof 求得的是指针变量本身的长度。也就是说,编译器并没有把 p 和数组关联起来,p 仅仅是一个指针变量,不管它指向哪里,sizeof 求得的永远是它本身所占用的字节数。

站在编译器的角度讲,变量名、数组名都是一种符号,它们最终都要和数据绑定起来。变量名用来指代一份数据,数组名用来指代一组数据(数据集合),它们都是有类型的,以便推断出所指代的数据的长度。

数组也有类型,我们可以将 int、float、char 等理解为基本类型,将数组理解为由基本类型派生得到的稍微复杂一些的类型。sizeof 就是根据符号的类型来计算长度的。

对于数组 a,它的类型是int [6],表示这是一个拥有 6 个 int 数据的集合,1 个 int 的长度为 4,6 个 int 的长度为 4×6 = 24,sizeof 很容易求得。

对于指针变量 p,它的类型是int *,在 32 位环境下长度为 4,在 64 位环境下长度为 8。

归根结底,a 和 p 这两个符号的类型不同,指代的数据也不同,它们不是一码事,sizeof 是根据符号类型来求长度的,a 和 p 的类型不同,求得的长度自然也不一样。

对于二维数组,也是类似的道理,例如int a[3][3]={1, 2, 3, 4, 5, 6, 7, 8, 9};,它的类型是int [3][3],长度是 4×3×3 = 36。

数组转换为指针

数组名的本意是表示一组数据的集合,它和普通变量一样,都用来指代一块内存,但在使用过程中,数组名有时候会转换为指向数据集合的指针(地址),而不是表示数据集合本身。

数据集合包含了多份数据,直接使用一个集合没有明确的含义,将数组名转换为指向数组的指针后,可以很容易地访问其中的任何一份数据,使用时的语义更加明确。

**C语言标准规定,当数组名作为数组定义的标识符(也就是定义或声明数组时)、sizeof 、_Alignof或 & 的操作数时,它才表示整个数组本身,在其他的表达式中,数组名会被转换为指向第 0 个元素的指针(地址)(也就是type []类型被转换为type *类型)。 **

数组下标[]

C语言标准还规定,数组下标与指针的偏移量相同。通俗地理解,就是对数组下标的引用总是可以写成“一个指向数组的起始地址的指针加上偏移量”。假设现在有一个数组 a 和指针变量 p,它们的定义形式为:

int a = {1,2,3,4,5},*p,i = 2;

以通过以下任何一种方式来访问 a[i]:

p = a;
p[i]
p = a;
*(p + i);
p = a + i;
*p;

对数组的引用 a[i] 在编译时总是被编译器改写成*(a+i)的形式,C语言标准也要求编译器必须具备这种行为。取下标操作符[ ]是建立在指针的基础上,它的作用是使一个指针和一个整数相加,产生出一个新的指针,然后从这个新指针(新地址)上取得数据;假设指针的类型为T *,所产生的结果的类型就是T

取下标操作符的两个操作数是可以交换的,它并不在意操作数的先后顺序,就像在加法中 3+5 和 5+3 并没有什么不一样。以上面的数组 a 为例,如果希望访问第 3 个元素,那么可以写作a[3],也可以写作3[a],这两种形式都是正确的,只不过后面的形式从不曾使用,它除了可以把初学者搞晕之外,实在没有什么实际的意义。

a[3] 等价于 *(a + 3),3[a] 等价于 *(3 + a),仅仅是把加法的两个操作数调换了位置。

使用下标时,编译器会自动把下标的步长调整到数组元素的大小。数组 a 中每个元素都是 int 类型,长度为 4 个字节,那么a[i+1]a[i]在内存中的距离是 4(而不是 1)。

关于数组和指针可交换性的总结

  1. 用 a[i] 这样的形式对数组进行访问总是会被编译器改写成(或者说解释为)像 *(a+i) 这样的指针形式。

  2. 指针始终是指针,它绝不可以改写成数组。你可以用下标形式访问指针,一般都是指针作为函数参数时,而且你知道实际传递给函数的是一个数组。

  3. 在特定的环境中,也就是数组作为函数形参,也只有这种情况,一个数组可以看做是一个指针。作为函数形参的数组始终会被编译器修改成指向数组第一个元素的指针。

  4. 当希望向函数传递数组时,可以把函数参数定义为数组形式(可以指定长度也可以不指定长度),也可以定义为指针。不管哪种形式,在函数内部都要作为指针变量对待。

数组指针

数组指针,实际上就是指向数组的指针,例如,我们声明一个指向int [3]的指针,那么就要int (*p)[3]。这里的p就是数组指针。

例子,指向一维数组的指针:

#include <stdio.h>
#include <stdint.h>

int main(int argc, const char* argv[])
{
	//声明了一个数组对象a,具有5个元素
    //隐式会转变成int *
	int a[] = { 1,2,3,4,5 };
	
	//声明了一个指向int类型的指针对象p,并将它指向数组a的首元素的地址
	//int *p = a;就相当于:int *p = &a[0];
	int* p = a;

	//声明了一个指向int [5]数组类型的指针对象,并用数组a的地址对它初始化
	int(*pa)[5] = &a;

	//与sizeof(p)结果相同,因为两者都是指针类型的对象
	printf("size of pa is: %zu\n", sizeof(pa));
	
	//这里sizeof(*pa)与sizeof(*p)的结果就不同了;
	//因为(*pa)的类型是int[5],所以sizeof(*pa)就相当于sizeof(int[5]);
	//而(*p)的类型是int,所以sizeof(*p)的结果就相当于sizeof(int)
	printf("size of (*pa) = %zu,sizeof (*p) = %zu\n", sizeof(*pa), sizeof(*p));

	int sum = 0;

	for (int i = 0; i < 5; i++)
	{
		sum += (*pa)[i];	//这里需要注意,这里的圆括号不能省,因为[]的优先级高于*
	}
	printf("sum = %d\n", sum);

	//这里声明了一个二维数组b[3][5],并对其初始化
    //隐式的会转变成 int (*)[5]
	int b[3][5] = {
		{1,2,3,4,5},
		{6,7,8,9,10},
		{11,12,13,14,15}
	};

	//我们用pa指针对象指向b的首个元素的地址。这里相当于:pa = &b[0]
	//一个二维数组其本质是一个数组,该数组的每个元素是一个一维数组。
	//b是一个含有3个元素的数组,其中每个元素是一个int[5]的数组对象。
	pa = b;

	pa[0][0]++;	//数组{1,2,3,4,5}就变成了{2,2,3,4,5}
	(*(pa + 1))[0]--;	//相当于pa[1][0]--   也就是数组{6,7,8,9,10}变成了{5,7,8,9,10}

	uint64_t address = (uint64_t)pa;

	pa += 1;	//相当于pa = &b[1];

	//pa += 1也可看作:
	address += sizeof(b[0]);

	//b[0]的类型为int[5]
	printf("address == pa? %d\n", (uint64_t)pa == address);

	printf("b[0][0] = %d,b[2][3] = %d\n", pa[-1][0], pa[1][3]);	//这时pa所在的地址是数组{6,7,8,9,10}这里

	int n = a[2];

	//声明一个变成数组对象v,此时,n为3,所以v具有三个元素
    //在运行的时候实际上就变成了int v[3];
	int v[n];

	//对数组对象v的每个元素进行赋值
	v[0] = 1; v[1] = 2; v[2] = 3;

	//声明了一个指向变长数组int[n]的指针对象pv
	//并用数组对象v的地址对它初始化
    //在运行的时候实际上就变成了int (*pv)[3] = &v;
	int(*pv)[n] = &v;

	n++;

	//n++对数组对象v以及指向数组对象v的指针pv均无影响
	//v的元素个数仍然是3个;(*pv)的类型int[n]
	//n也为3(这里的n不是声明的变量n的值,而是与声明变长数组时所绑定的值)
	printf("n = %d\n",n);
	printf("size of (*pv) is: %zu\n",sizeof(pv[0]));
	printf("size of v is: %zu\n",sizeof(v));
	
	//声明了一个指向int[n]的数组的指针对象pv2
	int(*pv2)[n] = pv;

	//这里,(*pv2)的大小即为sizeof(int) * 4,即n为加1后的值
	printf("size of (*pv2) is: %zu\n", sizeof(pv2[0]));

	printf("v[2] = %d\n", pv2[0][2]);

	return 0;

}

运行结果:

size of pa is: 4
size of (*pa) = 20,sizeof (*p) = 4
sum = 15
address == pa? 1
b[0][0] = 2,b[2][3] = 14
n = 4
size of (*pv) is: 12
size of v is: 12
size of (*pv2) is: 16
v[2] = 3

例子,指向二维数组的指针

#include <stdio.h>

int main(int argc, const char* argv[])
{
	//声明了一个二维数组a,具有2个元素,
	//其中每个元素为一个int[3]的数组
	int a[2][3] =
	{
		{1,2,3},
		{4,5,6}
	};

	//声明了一个指向二维数组int[2][3]的指针对象p,
	//并用数组a的地址对它初始化
	int(*p)[2][3] = &a;

	// 修改二维数组元素a[0][0]的值,也就是数组变成了{11,2,3}
	(*p)[0][0] += 10;
	printf("a[0][0] = %d\n", a[0][0]);

	// 修改二维数组a[1][2]的值
	//这里相当于把二维数组指针当成三维数组指针的首元素,也就是三维数组指针指向的是一个二维数组的指针
	//这与指向⼀维数组的指针可直接指向⼀个⼆维数组的首个元素的地址的特性类似
	//实际上p[0]也是与p指向相同的地址
	p[0][1][2] += 100;
	printf("a[1][2] = %d\n", a[1][2]);

	// (*p)的大小为2 * 3 * sizeof(int)
	printf("size of (*p) = %zu\n", sizeof(*p));

	return 0;
}

运行结果

a[0][0] = 11
a[1][2] = 106
size of (*p) = 24

多维数组本质

多维数组实际上就是多维是一个指针(比如二维数组,第二维就是指针),指针记录了低维的指针(比如二维数组,第一维就是数组),不管有多少维的数组,只有第一位是真正的数据,其它上面的高维都只是指针,因为内存是线性的,只能按顺序的进行存储。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eJqFwzWx-1581688568858)(img\8.png)]

例如上图为int arr[2][3] = {1,2,3,4,5,6};,那么二维数组arr[0]就相当于取了0x1230,那么arr[0]指向的类型是int [3],所以二维数组也就会隐含转换成int (*)[3],如果是三维数组,那么int arr[1][2][3]的话会隐含转换成int (*)[2][3],然后就表示arr[0]指向的还是一个指针,并且指针有两个,这两个指针指向的类型是int [3],所以arr[0]就与arr[0][0]的值是一样的。

总结

1.多维数组的隐含指针转换就是去了一个[]然后把[]换成*就是隐含转变的指针类型

例如:int a[5]隐含转变的类型就是int *,那么二维数组就是int a[2][5]隐含转变的类型就是int (*)[5]

2.高维数组的数组名可以看做是指向数组的常量指针,然后高维数组的指针指向的类型都是相同的低维数组。

例如:二维数组的第二维是一个常量指针,常量指针指向的一维数组的类型都是相同的,如int a[2][5]转变成的类型是int (*)[5],那么就是指向的类型是int [5]

注意

数组int a[10];,这里a表示的数组的首地址,&a也表示数组的首地址,但实际上这两个并不一样,a为数组是数组首元素的地址,&a为整个数组的地址,a和&a的意义不同其区别在于指针运算

a + 1 --> (unsigned int)a + sizeof(*a)
&a + 1 --> (unsigned int)(&a) + sizeof(*&a) 运算完就是(unsigned int)(&a) + sizeof(a)   也就是增加了整个数组的大小,因为a在变量表中的大小为整个数组的大小

例如:

int main(int argc, const char* argv[])
{
	int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("a = %x\n", a);
	printf("a + 1 = %x,&a + 1 = %x\n",a+1,&a+1);
	return 0;
}

运行结果

a = 96fe04
a + 1 = 96fe08,&a + 1 = 96fe2c

我们按照公式计算下

a + 1 --> (unsigned int)96fe04 + sizeof(4) --> 0x96fe04 + 0x4 = 96fe08

&a + 1 --> (unsigned int)96fe04 + sizeof(*&a) --> (unsigned int)96fe04 + sizeof(a) --> 0x96fe04 + 0x28(也就是十进制的40) =96 FE2C‬

运算的结果与公式运算的结果相等c

字符串指针

字符数组归根结底还是一个数组,上面讲到的关于指针和数组的规则同样也适用于字符数组。使用指针的方式来输出字符串:

第一种方式

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
	char str[] = "www.baidu.com";	//字符数组
	char *pstr = str;	//字符指针
	int len = strlen(str), i;

	//使用*(pstr+i)
	for (i = 0; i < len; i++)
	{
		printf("%c", *(pstr + i));
	}
	printf("\n");
	//使用pstr[i]
	for (i = 0; i < len; i++)
	{
		printf("%c", pstr[i]);
	}
	printf("\n");
	//使用*(str+i)
	for (i = 0; i < len; i++)
	{
		printf("%c", *(str + i));
	}
	printf("\n");
	system("pause");
	return 0;
}

运行结果:

www.baidu.com
www.baidu.com
www.baidu.com

除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:

char *str = "http://www.baidu.com";

或者:

char *str;
str = "http://www.baidu.com";

字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0 个字符的地址称为字符串的首地址。字符串中每个字符的类型都是char,所以 str 的类型也必须是char *

下面的例子演示了如何输出这种字符串,第二种方式:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	char *str = "http://www.baidu.com";
	int len = strlen(str), i;

	//直接输出字符串
	printf("%s\n", str);
	//使用*(str + i)
	for (i = 0; i < len; i++)
	{
		printf("%c", *(str + i));
	}
	printf("\n");
	//使用str[i]
	for (i = 0; i < len; i++)
	{
		printf("%c", str[i]);
	}
	printf("\n");
	system("pause");
	return 0;
}

运行结果:

http://www.baidu.com
http://www.baidu.com
http://www.baidu.com

这一切看起来和字符数组是多么地相似,它们都可以使用%s输出整个字符串,都可以使用*[ ]获取单个字符,这两种表示字符串的方式是不是就没有区别了呢?

有!它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。

内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。

我们将第二种形式的字符串称为字符串常量,意思很明显,常量只能读取不能写入。请看下面的演示:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	char *str = "Hello World!";
	printf("%#X\n", str);
	str = "I love C!";//正确,因为这等于重新赋值了一个地址给这个字符串指针
	printf("%#X\n", str);
	system("pause");
	return 0;
}

运行结果

0XD07B30
0XD07B48

#include <stdio.h>
int main(){
    char *str = "Hello World!";	//如果Hello world!\0,保存到常量区的地址是0x1230,那么str也就等于0x1230,并且这个区域没有修改权限
    str = "I love C!";  //正确
    str[3] = 'P';  //错误,因为字符串存储在常量区,不能进行修改
    return 0;
}

这段代码能够正常编译和链接,但在运行时会出现段错误(Segment Fault)或者写入位置错误。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vnig4aZl-1581688568859)(img/9.PNG)]

第4行代码是正确的,可以更改指针变量本身的指向;第5行代码是错误的,不能修改字符串中的字符。

到底使用字符数组还是字符串常量

在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。

从编译器中看现象
在这里插入图片描述
可以看到char *b = "hello";类型是一个二级的指针,那么就是b中存储的是指向hello这个字符串的指针,然后第二级指针才是真正存储hello的地址,从值也可以看出这点,第一个值是b的值,也就是0x00cffd50,类型是char * ,然后这个地址存储的值是0x00e6400c,类型是char,然而a[]c[]的类型是属于char[6]*类型,也就是属于数组指针类型,这个指针指向的类型是char [6],既然是数组,那么就能进行修改,这个就是为什么字符数组保存的字符串可以进行修改,而字符指针保存的字符串不能进行修改的原因。

获取用户输入的字符串就是一个典型的写入操作,只能使用字符数组,不能使用字符串常量,请看下面的代码:

#include <stdio.h>
int main(){
    char str[30];
    gets(str);
    printf("%s\n", str);
    return 0;
}

运行结果:
C C++ Java Python JavaScript
C C++ Java Python JavaScript

最后我们来总结一下,C语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量,它们在内存中的存储位置不同,使得字符数组可以读取和修改,而字符串常量只能读取不能修改。

结构体指针

指针也可以指向结构体,并通过结构体指针访问结构体中的元素或者通过结构体指针修改结构体中的元素,结构体指针访问结构体中的元素时或者修改结构体中的元素时,需要使用->取成员运算符进行操作。

#include <stdio.h>

int main(int argc, const char* argv[])
{
	struct Student {
		char name[256];
		unsigned char age;
	} stu1 = {"xiaoming",18};	//声明了一个结构体,并进行初始化

	struct Student* studentP = &stu1;	//定义了一个结构体指针
	printf("stu1_name = %s,stu1_age = %d\n", stu1.name, stu1.age);
	printf("studentP_name = %s,studentP_age = %d\n", studentP->name, studentP->age);

	studentP->age = 20;	//用指针的方式修改结构体中的数据
	printf("stu1_name = %s,stu1_age = %d\n", stu1.name, stu1.age);
	printf("studentP_name = %s,studentP_age = %d\n", studentP->name, studentP->age);
	return 0;
}

运行结果

stu1_name = xiaoming,stu1_age = 18
studentP_name = xiaoming,studentP_age = 18
stu1_name = xiaoming,stu1_age = 20
studentP_name = xiaoming,studentP_age = 20

共用体指针

指针也可以指向共用体,并通过共用体指针访问共用体中的元素或者通过共用体指针修改共用体中的元素,共用体指针访问共用体中的元素时或者修改共用体中的元素时,需要使用->取成员运算符进行操作。

#include <stdio.h>

int main(int argc, const char* argv[])
{
    union data {
        int n;
        char ch;
        short m;
    };  //声明一个共用体
    union data data1;   //声明一个共用体变量
    union data* dataP = &data1; //定义一个共用体指针
    data1.n = 0;    //把共用体中的垃圾数据清零
    data1.ch = 255;
    printf("data1.n = %d,data1.ch = %d,data1.m = %d\n",data1.n,data1.ch,data1.m);
    printf("dataP.n = %d,dataP.ch = %d,dataP.m = %d\n", dataP->n, dataP->ch, dataP->m);//读取共用体中的数据
    dataP->n = 65535;   //修改共用体中的数据
    printf("data1.n = %d,data1.ch = %d,data1.m = %d\n", data1.n, data1.ch, data1.m);
    printf("dataP.n = %d,dataP.ch = %d,dataP.m = %d\n", dataP->n, dataP->ch, dataP->m);
    return 0;
}

运行结果

data1.n = 255,data1.ch = -1,data1.m = 255
dataP.n = 255,dataP.ch = -1,dataP.m = 255
data1.n = 65535,data1.ch = -1,data1.m = -1
dataP.n = 65535,dataP.ch = -1,dataP.m = -1

函数指针

一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。

函数指针的定义形式为:

returnType (*pointerName)(param list);

returnType 为函数返回值类型,pointerNmae 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。

注意( )的优先级高于*,第一个括号不能省略,如果写作returnType *pointerName(param list);就成了函数原型,它表明函数的返回值类型为returnType *

【实例】用指针来实现对函数的调用。

#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b) {
    return a > b ? a : b;
}
int main() {
    int x, y, maxval;
    //定义函数指针
    int (*pmax)(int, int) = max;  //也可以写作int (*pmax)(int a, int b)
    printf("Input two numbers:");
    scanf("%d %d", &x, &y);
    maxval = (*pmax)(x, y); //通过函数指针调用函数
    printf("Max value: %d\n", maxval);
    return 0;
}

运行结果:
Input two numbers:10 50
Max value: 50

(*pmax)(x, y)函数进行了调用。pmax 是一个函数指针,在前面加 * 就表示对它指向的函数进行调用。注意( )的优先级高于*,第一个括号不能省略。

数组作函数参数

C语言标准规定,作为“类型的数组”的形参应该调整为“类型的指针”。在函数形参定义这个特殊情况下,编译器必须把数组形式改写成指向数组第 0 个元素的指针形式。编译器只向函数传递数组的地址,而不是整个数组的拷贝。

这种隐式转换意味着下面三种形式的函数定义是完全等价的:

void func(int *parr){ ...... }
void func(int arr[]){ ...... }
void func(int arr[5]){ ...... }

在函数内部,arr 会被转换成一个指针变量,编译器为 arr 分配 4 个字节的内存,用 sizeof(arr) 求得的是指针变量的长度,而不是数组长度。要想在函数内部获得数组长度必须额外增加一个参数,在调用函数之前求得数组长度

参数传递是一次赋值的过程,赋值也是一个表达式,函数调用时不管传递的是数组名还是数组指针,效果都是一样的,相当于给一个指针变量赋值。

把作为形参的数组和指针等同起来是出于效率方面的考虑。数组是若干类型相同的数据的集合,数据的数目没有限制,可能只有几个,也可能成千上万,如果要传递整个数组,无论在时间还是内存空间上的开销都可能非常大。而且绝大部分情况下,我们其实并不需要整个数组的拷贝,我们只想告诉函数在那一时刻对哪个特定的数组感兴趣 ,所以,如果希望在函数内部操作数组,必须传递数组指针。下面的例子定义了一个函数 max(),用来查找数组中值最大的元素:

#include <stdio.h>
#include <stdlib.h>

int Max(int *intArr, int len)
{
	int i, maxValue = intArr[0];	//假设第0个元素是最大值
	for (i = 1;i < len;i++)
	{
		if (maxValue < intArr[i])
		{
			maxValue = intArr[i];
		}
	}
	return maxValue;
}

int main()
{
	int nums[6], i;
	int len = sizeof(nums) / sizeof(int);	//求出数组大小
	//读取用户输入的数据并赋值给数组元素
	for (i = 0;i < len;i++)
	{
		scanf("%d", nums + i);	//对数组进行赋值
	}
	printf("Max value is %d!\n", Max(nums, len));

	system("pause");
	return 0;
}

运行结果

12 55 30 8 93 27
Max value is 93!

参数 intArr 仅仅是一个数组指针,在函数内部无法通过这个指针获得数组长度,必须将数组长度作为函数参数传递到函数内部。数组 nums 的每个元素都是整数,scanf() 在读取用户输入的整数时,要求给出存储它的内存的地址,nums+i就是第 i 个数组元素的地址。

用数组做函数参数时,参数也能够以“真正”的数组形式给出。例如对于上面的 max() 函数,它的参数可以写成下面的形式:

int Max(int intArr[6], int len){
    int i, maxValue = intArr[0];  //假设第0个元素是最大值
    for(i=1; i<len; i++){
        if(maxValue < intArr[i]){
            maxValue = intArr[i];
        }
    }
    return maxValue;
}

int intArr[6]好像定义了一个拥有 6 个元素的数组,调用 max() 时可以将数组的所有元素“一股脑”传递进来。

也可以省略数组长度,把形参简写为下面的形式:

int max(int intArr[], int len){
    int i, maxValue = intArr[0];  //假设第0个元素是最大值
    for(i=1; i<len; i++){
        if(maxValue < intArr[i]){
            maxValue = intArr[i];
        }
    }
    return maxValue;
}

虽然定义了一个数组,但没有指定数组长度,好像可以接受任意长度的数组。**实际上这两种形式的数组定义都是假象,不管是int intArr[6]还是int intArr[]都不会创建一个数组出来,编译器也不会为它们分配内存,实际的数组是不存在的,它们最终还是会转换为int *intArr这样的指针。**这就意味着,两种形式都不能将数组的所有元素“一股脑”传递进来,大家还得规规矩矩使用数组指针。

int intArr[6]这种形式只能说明函数期望用户传递的数组有 6 个元素,并不意味着数组只能有 6 个元素,真正传递的数组可以有少于或多于 6 个的元素。

需要强调的是,不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。

C语言为什么不允许直接传递数组的所有元素,而必须传递数组指针呢?

参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。

对于像 int、float、char 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行内存拷贝有可能是一个漫长的过程,会严重拖慢程序的效率,为了防止技艺不佳的程序员写出低效的代码,C语言没有从语法上支持数据集合的直接赋值。

除了C语言,C++、Java、Python 等其它语言也禁止对大块内存进行拷贝,在底层都使用类似指针的方式来实现。

参数的传递本质上是一次赋值的过程

#include <stdio.h>
#include <stdlib.h>


int Max(int *intArr, int len)
{
	printf("intArr:%x,len:%x\n", &intArr,&len);
	printf("intArr:%x\n", intArr);
	int i, maxValue = intArr[0];	//假设第0个元素是最大值
	for (i = 1;i < len;i++)
	{
		if (maxValue < intArr[i])
		{
			maxValue = intArr[i];
		}
	}
	return maxValue;
}

int main()
{
	int nums[6], i;
	int len = sizeof(nums) / sizeof(int);
	//读取用户输入的数据并赋值给数组元素
	for (i = 0;i < len;i++)
	{
		scanf("%d", nums + i);
	}
	printf("Max value is %d!\n", Max(nums, len));
	printf("nums:%x,len:%x\n", &nums,&len);
	printf("nums:%x\n", nums);

	system("pause");
	return 0;
}

结果

1 2 3 4 5 6
intArr:eff700,len:eff704
intArr:eff7f0
Max value is 6!
nums:eff7f0,len:eff7d8
nums:eff7f0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QpN99XIA-1581688568861)(img/11.png)]

指针变量作为函数参数

在C语言中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。

像数组、字符串、动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些数据集合。

有的时候,对于整数、小数、字符等基本类型数据的操作也必须要借助指针,一个典型的例子就是交换两个变量的值。

有些初学者可能会使用下面的方法来交换两个变量的值:

#include <stdio.h>
#include <stdlib.h>

void swap(int a, int b)
{
	int temp;//临时变量
	temp = a;
	a = b;
	b = temp;
}

int main()
{
	int a = 66, b = 99;
	swap(a, b);
	printf("a = %d, b = %d\n", a, b);
	system("pause");
	return 0;
}

运行结果:
a = 66, b = 99

从结果可以看出,a、b 的值并没有发生改变,交换失败。这是因为 swap() 函数内部的 a、b 和 main() 函数内部的 a、b 是不同的变量,占用不同的内存,它们除了名字一样,没有其他任何关系,swap() 交换的是它内部 a、b 的值,不会影响它外部(main() 内部) a、b 的值。

改用指针变量作参数后就很容易解决上面的问题:

#include <stdio.h>
#include <stdlib.h>

void swap(int *p1, int *p2)
{
	int temp;//临时变量
	temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

int main()
{
	int a = 66, b = 99;
	swap(&a, &b);
	printf("a = %d, b = %d\n", a, b);
	system("pause");
	return 0;
}

运行结果

a = 99, b = 66

调用 swap() 函数时,将变量 a、b 的地址分别赋值给 p1、p2,这样 *p1、*p2 代表的就是变量 a、b 本身,交换 *p1、*p2 的值也就是交换 a、b 的值。函数运行结束后虽然会将 p1、p2 销毁,但它对外部 a、b 造成的影响是“持久化”的,不会随着函数的结束而“恢复原样”。

需要注意的是临时变量 temp,它的作用特别重要,因为执行*p1 = *p2;语句后 a 的值会被 b 的值覆盖,如果不先将 a 的值保存起来以后就找不到了。

指针作为函数返回值

C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。下面的例子定义了一个函数 strlong(),用来返回两个字符串中较长的一个:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


char *strlong(char *str1, char *str2)
{
	if (strlen(str1) >= strlen(str2))
	{
		return str1;
	}
	else
	{
		return str2;
	}
}

int main()
{
	char str1[30], str2[30], *str;
	gets(str1);
	gets(str2);
	str = strlong(str1, str2);
	printf("Longer string:%s\n", str);

	system("pause");
	return 0;
}

运行结果

www.baidu.com
pipy.douban.com
Longer string:pipy.douban.com

用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。请看下面的例子:

#include <stdio.h>
#include <stdlib.h>

int *func()
{
	int n = 100;
	return &n;
}

int main()
{
	int *p = func(), n;
	n = *p;
	printf("value = %d\n", n);
	system("pause");
	return 0;
}

运行结果

value = 100

n 是 func() 内部的局部变量,func() 返回了指向 n 的指针,根据上面的观点,func() 运行结束后 n 将被销毁,使用 *p 应该获取不到 n 的值。但是从运行结果来看,我们的推理好像是错误的,func() 运行结束后 *p 依然可以获取局部变量 n 的值,这个上面的观点不是相悖吗?

为了进一步看清问题的本质,不妨将上面的代码稍作修改,在第9~10行之间增加一个函数调用,看看会有什么效果:

#include <stdio.h>
#include <stdlib.h>

int *func()
{
	int n = 100;
	return &n;
}

int main()
{
	int *p = func(), n;
	printf("hello,world\n");
	n = *p;
	printf("value = %d\n", n);
	system("pause");
	return 0;
}

运行结果

hello,world
value = 12

可以看到,现在 p 指向的数据已经不是原来 n 的值了,它变成了一个毫无意义的甚至有些怪异的值。与前面的代码相比,该段代码仅仅是在 *p 之前增加了一个函数调用,这一细节的不同却导致运行结果有天壤之别,究竟是为什么呢?

前面我们说函数运行结束后会销毁所有的局部数据,这个观点并没错,大部分C语言教材也都强调了这一点。但是,这里所谓的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。对于上面的两个例子,func() 运行结束后 n 的内存依然保持原样,值还是 100,如果使用及时也能够得到正确的数据,如果有其它函数被调用就会覆盖这块内存,得到的数据就失去了意义。

第一个例子在调用其他函数之前使用 *p 抢先获得了 n 的值并将它保存起来,第二个例子显然没有抓住机会,有其他函数被调用后才使用 *p 获取数据,这个时候已经晚了,内存已经被后来的函数覆盖了,而覆盖它的究竟是一份什么样的数据我们无从推断(一般是一个没有意义甚至有些怪异的值)。

函数定义时定义为int 返回的就时int的值,所以定义int *返回的就是int的指针类型的数据,也就是一个地址

数组灵活多变的访问形式

#include <stdio.h>
#include <stdlib.h>

int main()
{
	char str[20] = "www.baidu.com"; //定义一个数组

	char *s1 = str;	//定义一个char类型的指针,指向数组
	char *s2 = str + 2;//定义一个char类型的指针,指向数组第三个元素

	char c1 = str[4];//c1赋值了str的第五个元素
	char c2 = *str;//c2赋值了str上的第一个元素
	char c3 = *(str + 4);//c3赋值了str的第五个元素
	char c4 = *str + 2;//str上的第一个元素的编码+2
	char c5 = (str + 1)[5]; //str上的第七个元素

	int num1 = *str + 2; //w的编码的数字+2
	long num2 = (long)str;//str是地址,强制转换long的类型
	long num3 = (long)(str + 2);//str的地址+2,强制转换成long的类型

	printf("s1 = %s\n", s1);
	printf("s2 = %s\n", s2);

	printf("c1 = %c\n", c1);
	printf("c2 = %c\n", c2);
	printf("c3 = %c\n", c3);
	printf("c4 = %c\n", c4);
	printf("c5 = %c\n", c5);

	printf("num1 = %d\n", num1);
	printf("num2 = %ld\n", num2);
	printf("num3 = %ld\n", num3);

	system("pause");
	return 0;

}

运行结果

s1 = www.baidu.com
s2 = w.baidu.com
c1 = b
c2 = w
c3 = b
c4 = y
c5 = i
num1 = 121
num2 = 9435152
num3 = 9435154
  1. str 既是数组名称,也是一个指向字符串的指针;指针可以参加运算,加 1 相当于数组下标加 1。

printf() 输出字符串时,要求给出一个起始地址,并从这个地址开始输出,直到遇见字符串结束标志\0s1 为字符串 str 第 0 个字符的地址,s2 为第 2 个字符的地址,所以printf() 的结果分别为www.baidu.comw.baidu.com

  1. 指针可以参加运算,str+4 表示第 4 个字符的地址,c3 = *(str+4) 表示第4个字符,即 ‘a’。

  2. 其实,数组元素的访问形式可以看做 address[offset]address 为起始地址,offset 为偏移量:c1 = str[4]表示以地址 str 为起点,向后偏移4个字符,为 ‘b’;c5 = (str+1)[5]表示以地址 str+1 为起点,向后偏移5个字符,等价于str[6],为 ‘i’。

  3. 字符与整数运算时,先转换为整数(字符对应的ASCII码)。num1c4 右边的表达式相同,对于 num1*str+2 == 'w'+2 == 119+2 == 121,即 num1 的值为 121,对于 c4,121 对应的字符为 ‘y’,所以 c4 的输出值为 ‘y’。

  4. num2num3 分别为字符串 str 的首地址和第 2 个元素的地址。

#include <stdio.h>
#include <stdlib.h>

int main()
{
	char str[20] = { 0 };
	int i;

	for (i = 0; i < 10; i++)
	{
		*(str + i) = 97 + i;//97为字符a的ASCII码值
		//*(str + i) 相当与str[i]
	}
	printf("%s\n", str);
	printf("%s\n", str + 2);
	printf("%c\n", str[2]);
	printf("%c\n", (str + 2)[2]);

	system("pause");
	return 0;
}

运行结果

abcdefghij
cdefghij
c
e

char str[20] = { 0 };用来将字符数组中的所有元素都初始化为\0,这样在循环结束时就无需添加字符串结束标志。

前面三个 printf() 比较容易理解,第四个 printf() 可以参照上面的说明 3),str+2 表示指向第 2 个元素,(str+2)[2] 相当于 *(str+2+2),也就是取得第 4 个元素的值。

二级指针

指针可以指向一份普通类型的数据,例如 int、double、char 等,也可以指向一份指针类型的数据,例如 int *、double *、char * 等。

如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。

假设有一个 int 类型的变量 a,p1是指向 a 的指针变量,p2 又是指向 p1 的指针变量,它们的关系如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9t7qbB1D-1581688568862)(img/12.jpg)]

将这种关系转换为C语言代码:

int a = 100;
int *p1 = &a;
int **p2 = &p1;

指针变量也是一种变量,也会占用存储空间,也可以使用&获取它的地址。C语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号*。p1 是一级指针,指向普通类型的数据,定义时有一个*;p2 是二级指针,指向一级指针 p1,定义时有两个*

如果我们希望再定义一个三级指针 p3,让它指向 p2,那么可以这样写:

int ***p3 = &p2;

四级指针也是类似的道理:

int ****p4 = &p3;

实际开发中会经常使用一级指针和二级指针,几乎用不到高级指针。

想要获取指针指向的数据时,一级指针加一个*,二级指针加两个*,三级指针加三个*,以此类推,请看代码:

#include <stdio.h>
#include <stdlib.h>


int main()
{
	int a = 100;
	int *p1 = &a;
	int **p2 = &p1;
	int ***p3 = &p2;

	printf("%d, %d, %d, %d\n", a, *p1, **p2, ***p3);
	printf("&p2 = %#X, p3 = %#X\n", &p2, p3);
	printf("&p1 = %#X,p2 = %#X, *p3 = %#X\n", &p1, p2, *p3);
	printf("&a = %#X,p1 = %#X,*p2 = %#X,**p3 = %#X\n", &a, p1, *p2, **p3);
	system("pause");
	return 0;
}

运行结果

100, 100, 100, 100
&p2 = 0X10FF750, p3 = 0X10FF750
&p1 = 0X10FF75C,p2 = 0X10FF75C, *p3 = 0X10FF75C
&a = 0X10FF768,p1 = 0X10FF768,*p2 = 0X10FF768,**p3 = 0X10FF768

以三级指针 p3 为例来分析上面的代码。***p3等价于*(*(*p3))p3 得到的是 p2 的值,也即 p1 的地址;(p3) 得到的是 p1 的值,也即 a 的地址;经过三次“取值”操作后,(*(*p3)) 得到的才是 a 的值。

假设 a、p1、p2、p3 的地址分别是 0X00A0、0X1000、0X2000、0X3000,它们之间的关系可以用下图来描述:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PA8HR66b-1581688568863)(img/13.jpg)]

方框里面是变量本身的值,方框下面是变量的地址。

理解一个*就代表上一级的数据,例如*p1就相当于获取了a的数据,*p2就等于获取它的上一级的数据,就是a的地址,记忆口诀,一个猩猩(星星)跳一格(获取上一级的数据)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EBdtHaBa-1581688568864)(img/14.jpg)]

空指针NULL以及void指针

空指针NULL

一个指针变量可以指向计算机中的任何一块内存,不管该内存有没有被分配,也不管该内存有没有使用权限,只要把地址给它,它就可以指向,C语言没有一种机制来保证指向的内存的正确性,程序员必须自己提高警惕。

很多初学者会在无意间对没有初始化的指针进行操作,这是非常危险的,请看下面的例子:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	char *str;
	gets(str);
	printf("%s\n", str);
	system("pause");
	return 0;
}

这段程序没有语法错误,能够通过编译和链接,但当用户输入完字符串并按下回车键时就会发生错误,在 Linux 下表现为段错误(Segment Fault),在 Windows 下程序直接崩溃。如果你足够幸运,或者输入的字符串少,也可能不报错,这都是未知的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DWUStygH-1581688568864)(img/15.PNG)]

未初始化的局部变量的值是不确定的,C语言并没有对此作出规定,不同的编译器有不同的实现,我曾警告大家不要直接使用未初始化的局部变量。上面的代码中,str 就是一个未初始化的局部变量,它的值是不确定的,究竟指向哪块内存也是未知的,大多数情况下这块内存没有被分配或者没有读写权限,使用 gets() 函数向它里面写入数据显然是错误的。

我强烈建议对没有初始化的指针赋值为 NULL,例如:

char *str = NULL;

NULL 是“零值、等于零”的意思,在C语言中表示空指针。从表面上理解,空指针是不指向任何数据的指针,是无效指针,程序使用它不会产生效果。

注意区分大小写,null 没有任何特殊含义,只是一个普通的标识符。

很多库函数都对传入的指针做了判断,如果是空指针就不做任何操作,或者给出提示信息。更改上面的代码,给 str 赋值 NULL,看看会有什么效果

#include <stdio.h>
#include <stdlib.h>

int main()
{
	char *str = NULL;
	gets(str);
	printf("%s\n", str);
	system("pause");
	return 0;
}

运行程序后发现,还未等用户输入任何字符,printf() 就直接输出了(null)。我们有理由据此推断,gets() 和 printf() 都对空指针做了特殊处理:

  • gets() 不会让用户输入字符串,也不会向指针指向的内存中写入数据;
  • printf() 不会读取指针指向的内容,只是简单地给出提示,让程序员意识到使用了一个空指针。

我们在自己定义的函数中也可以进行类似的判断,例如:

void func(char *p){
    if(p == NULL){
        printf("(null)\n");
    }else{
        printf("%s\n", p);
    }
}

这样能够从很大程度上增加程序的健壮性,防止对空指针进行无意义的操作。

其实,NULL 是在stdio.h中定义的一个宏,它的具体内容为:

#define NULL ((void *)0)

(void *)0表示把数值 0 强制转换为void *类型,最外层的( )把宏定义的内容括起来,防止发生歧义。从整体上来看,NULL 指向了地址为 0 的内存,而不是前面说的不指向任何数据。

在进程的虚拟地址空间中,最低地址处有一段内存区域被称为保留区,这个区域不存储有效数据,也不能被用户程序访问,将 NULL 指向这块区域很容易检测到违规指针。在大多数操作系统中,极小的地址通常不保存数据,也不允许程序访问,NULL 可以指向这段地址区间中的任何一个地址。

注意,C语言没有规定 NULL 的指向,只是大部分标准库约定成俗地将 NULL 指向 0,所以不要将 NULL 和 0 等同起来,例如下面的写法是不专业的:

int *p = 0;

而应该坚持写为:

int *p = NULL;

注意 NULL 和 NUL 的区别:NULL 表示空指针,是一个宏定义,可以在代码中直接使用。而 NUL 表示字符串的结束标志 ‘\0’,它是ASCII码表中的第 0 个字符。NUL 没有在C语言中定义,仅仅是对 ‘\0’ 的称呼,不能在代码中直接使用。

void指针

对于空指针 NULL 的宏定义内容,上面只是对((void *)0)作了粗略的介绍,这里重点说一下void *的含义。void 用在函数定义中可以表示函数没有返回值或者没有形式参数,用在这里表示指针指向的数据的类型是未知的。

也就是说,void *表示一个有效指针,它确实指向实实在在的数据,只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换。

C语言动态内存分配函数 malloc() 的返回值就是void *类型,在使用时要进行强制类型转换,请看下面的例子:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	//分配可以保存30个字符的内存,并把返回的指针转换为char *
	char *str = (char *)malloc(sizeof(char) * 30);
	gets(str);
	printf("%s\n", str);
	system("pause");
	return 0;
}

运行结果:

www.baidu.com
www.baidu.com

这里重点是让大家理解void *,它不是空指针的意思,而是实实在在的指针,只是指针指向的内存中不知道保存的是什么类型的数据。

指针数组

如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。指针数组的定义形式一般为:

dataType *arrayName[length];

[ ]的优先级高于*,该定义形式应该理解为:

dataType *(arrayName[length]);

括号里面说明arrayName是一个数组,包含了length个元素,括号外面说明每个元素的类型为dataType *

除了每个元素的数据类型不同,指针数组和普通数组在其他方面都是一样的,下面是一个简单的例子

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int a = 16, b = 932, c = 100;
	//定义一个指针数组,数组类型为int (*)[3]
	int *arr[3] = { &a,&b,&c };//也可以不指定长度,直接写作 int *parr[]
	//定义一个指向指针数组的指针
	int **parr = arr;
	printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);	//*arr[0]应该理解为*(arr[0]),arr[0]中的是&a,然后取&a上的数据
	printf("%d, %d, %d\n", **(parr + 0), **(parr + 1), **(parr + 2));
	system("pause");
	return 0;
}

运行结果:

16, 932, 100
16, 932, 100

arr 是一个指针数组,它包含了 3 个元素,每个元素都是一个指针,在定义 arr 的同时,我们使用变量 a、b、c 的地址对它进行了初始化,这和普通数组是多么地类似。

parr 是指向数组 arr 的指针,确切地说是指向 arr 第 0 个元素的指针,它的定义形式应该理解为int *(*parr),括号中的*表示 parr 是一个指针,括号外面的int *表示 parr 指向的数据的类型。arr 第 0 个元素的类型为 int *,所以在定义 parr 时要加两个 *。

第一个 printf() 语句中,arr[i] 表示获取第 i 个元素的值,该元素是一个指针,还需要在前面增加一个 * 才能取得它指向的数据,也即 *arr[i] 的形式。

第二个 printf() 语句中,parr+i 表示第 i 个元素的地址,*(parr+i) 表示获取第 i 个元素的值(该元素是一个指针),**(parr+i) 表示获取第 i 个元素指向的数据。

指针数组还可以和字符串数组结合使用,请看下面的例子:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	char *str[3] = {
		"www.baidu.com",
		"百度一下你就知道",
		"百度"
	};
	printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
	system("pause");
	return 0;

}

运行结果

www.baidu.com
百度一下你就知道
百度

需要注意的是,字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的。

也只有当指针数组中每个元素的类型都是char *时,才能像上面那样给指针数组赋值,其他类型不行。

为了便于理解,可以将上面的字符串数组改成下面的形式,它们都是等价的。

#include <stdio.h>
#include <stdlib.h>

int main()
{
	char *str0 = "www.baidu.com";
	char *str2 = "百度一下你就知道";
	char *str3 = "百度"
	char *str[3] = {str0,str1,str2};
	printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
	system("pause");
	return 0;

}

一题玩转指针数组和二级指针

代码

#include <stdio.h>
#include <stdlib.h>

int main()
{
    //这里面存的实际上是地址
	char *lines[5] = {
		"pythoncookbook",
		"Programming",
		"Techniques",
		"is",
		"great fun"
	};

	char *str1 = lines[1];	//取Programming的地址
	char *str2 = *(lines + 3);	//取is的地址
	char c1 = *(*(lines + 4) + 6);	//取great fun的地址,然后再取这个地址上的第7号元素的数据f
	char c2 = (*lines + 5)[5];	//这里是取了lines中的第一个元素的地址,然后 + 5,然后再加5的地方再[5]也就是 + 5,那么就相当于pythoncookbook的地址 + 10,所以是b
	char c3 = *lines[0] + 2;	//先运算lines[0]相当于已经取了pythoncookbook的地址,然后再取pythoncookbook第一个元素出来,再进行+2,那么p相当于112+ 2 = 114,那么就变成了r

	printf("str1 = %s\n", str1);
	printf("str2 = %s\n", str2);
	printf("  c1 = %c\n", c1);
	printf("  c2 = %c\n", c2);
	printf("  c3 = %c\n", c3);

	system("pause");
	return 0;
}

运行结果

str1 = Programming
str2 = is
c1 = f
c2 = b
c3 = r

为了方便说明问题,我们将上面的字符串数组改成下面的形式,它们都是等价的:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	char *string0 = "pythoncookbook";
	char *string1 = "Programming";
	char *string2 = "Techniques";
	char *string3 = "is";
	char *string4 = "great fun";

	char *lines[5];
	lines[0] = string0;
	lines[1] = string1;
	lines[2] = string2;
	lines[3] = string3;
	lines[4] = string4;

	char *str1 = lines[1];
	char *str2 = *(lines + 3);
	char c1 = *(*(lines + 4) + 6);
	char c2 = (*lines + 5)[5];
	char c3 = *lines[0] + 2;

	printf("str1 = %s\n", str1);
	printf("str2 = %s\n", str2);
	printf("  c1 = %c\n", c1);
	printf("  c2 = %c\n", c2);
	printf("  c3 = %c\n", c3);

	system("pause");
	return 0;
}

char *lines[5]定义了一个指针数组,它的每个元素的类型都是char *。在表达式中使用 lines 时,它会转换为一个类型为char **的指针,这样*lines就表示一个指向字符的指针,而**lines表示一个具体的字符

指针是可以进行运算的,lines 表示数组的首地址(第0个元素的地址),lines+0、lines+1、lines+2 … 分别表示第0、1、2 …个元素的地址,(lines+0)或lines[0]、(lines+1)或lines[1]、*(lines+2)或lines[2] … 分别是字符串 string0, string1, string2 … 的首地址。所以:

*lines == *(lines+0) == lines[0] == string0
*(lines+1) == lines[1] == string1
*(lines+2) == lines[2] == string2

注意:lines 是二级指针,*(lines+i) 是一级指针, **(lines+i) 才是具体的字符。

上面的题目中:

  • lines[1]:它是一个指针,指向字符串string1,即 string1 的首地址。
  • (lines + 3):lines + 3 为数组中第 3 个元素的地址,(lines + 3) 为第 3 个元素的值,它是一个指针,指向字符串 string3。
  • ((lines + 4) + 6):*(lines + 4) + 6 == lines[4] + 6 == string4 + 6,表示字符串 string4 中第 6 个字符的地址,即 f 的地址,所以 ((lines + 4) + 6) 就表示字符 f。
  • (*lines + 5)[5]:*lines + 5 为字符串 string0 中第 5 个字符的地址,即 n的地址,(lines + 5)[5]等价于(*lines + 5 + 5),表示第10个字符,即 b。
  • *lines[0] + 2:lines[0] 为字符串 string0 中第 0 个字符的地址,即 p 的地址;*lines[0]也就表示第 0 个字符,即字符 C。字符与整数运算,首先转换为该字符对应的 ASCII 码,然后再运算,所以*lines[0]+ 2 = 112+ 2 = 114,114 对应的字符为 r。

彻底攻克C语言指针

C语言标准规定,对于一个符号的定义,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析。对,从名字开始,不是从开头也不是从末尾,这是理解复杂指针的关键!

对于初学者,有几种运算符的优先级非常容易混淆,它们的优先级从高到低依次是:

  • 定义中被括号( )括起来的那部分。
  • 后缀操作符:括号( )表示这是一个函数,方括号[ ]表示这是一个数组。
  • 前缀操作符:星号*表示“指向xxx的指针”。

学会了“绝杀招式”,接下来我们就由浅入深,逐个击破上面的指针定义。

1) int *p1[6];

从 p1 开始理解,它的左边是 *,右边是 [ ],[ ] 的优先级高于 *,所以编译器先解解、析p1[6],p1 首先是一个拥有 6 个元素的数组,然后再解析int *,它用来说明数组元素的类型。从整体上讲,p1 是一个拥有 6 个 int * 元素的数组,也即指针数组。

2) int (*p3)[6];

从 p3 开始理解,( ) 的优先级最高,编译器先解析(*p3),p3 首先是一个指针,剩下的int [6]是 p3 指向的数据的类型,它是一个拥有 6 个元素的一维数组。从整体上讲,p3 是一个指向拥有 6 个 int 元素数组的指针,也即数组指针。

3) int (*p4)(int, int);

从 p4 开始理解,( ) 的优先级最高,编译器先解析(*p4),p4 首先是一个指针,它后边的 ( ) 说明 p4 指向的是一个函数,括号中的int, int是参数列表,开头的int用来说明函数的返回值类型。整体来看,p4 是一个指向原型为int func(int, int);的函数的指针。

4) char * (* c[10])(int **p);

这个定义有两个名字,分别是 c 和 p,乍看起来 p 是指针变量的名字,不过很遗憾这是错误的。如果 p 是指针变量名,c[10]这种写法就又定义了一个新的名字,这让人匪夷所思。

以 c 作为变量的名字,先来看括号内部(粗体):

char * (* c[10]) (int **p);

[ ] 的优先级高于 *,编译器先解析c[10],c 首先是一个数组,它前面的*表明每个数组元素都是一个指针,只是还不知道指向什么类型的数据。整体上来看,( * c[10])说明 c 是一个指针数组,只是指针指向的数据类型尚未确定。

跳出括号,根据优先级规则(() 的优先级高于 *)应该先看右边(粗体):

char * (* c[10]) (int **p);

( )说明是一个函数,int **p是函数参数。

再看左边(粗体):

char * (* c[10]) (int **p);

char *是函数的返回值类型。

从整体上看,我们可以将定义分成两部分:

char * (* c[10]) (int **p);

中间粗体表明 c 是一个指针数组,最后的粗体表明指针指向的数据类型,合起来就是:c 是一个拥有 10 个元素的指针数组,每个指针指向一个原型为char *func(int **p);的函数。

5) int (*(*(*pfunc)(int *))[5])(int *);

从 pfunc 开始理解,先看括号内部(粗体):

int (*(*(*pfunc)(int *))[5])(int *);

pfunc 是一个指针。

跳出括号,看它的两边(粗体):

int (*(*(*pfunc)(int *))[5])(int *);

根据优先级规则应该先看右边的(int *),它表明这是一个函数,int *是参数列表。再看左边的*,它表明函数的返回值是一个指针,只是指针指向的数据类型尚未确定。

将上面的两部分合成一个整体,如下面的粗体所示,它表明 pfunc 是一个指向函数的指针,现在函数的参数列表确定了,也知道返回值是一个指针了(只是不知道它指向什么类型的数据)。

int (* **(*(pfunc)(int )) [5])(int *);

粗体以外的部分,就用来说明函数返回什么类型的指针。

我们接着分析,再向外跳一层括号(粗体):

int (* (*(*pfunc)(int *))[5])(int *);

[ ] 的优先级高于 *,先看右边,[5] 表示这是一个数组,再看左边,* 表示数组的每个元素都是指针。也就是说,* [5] 是一个指针数组,函数返回的指针就指向这样一个数组。

那么,指针数组中的指针又指向什么类型的数据呢?再向外跳一层括号(粗体):

int (* (*(*pfunc)(int *))[5]) (int *);

先看粗体部分的右边,它是一个函数,再看左边,它是函数的返回值类型。也就是说,指针数组中的指针指向原型为int func(int *);的函数。

将上面的三部分合起来就是:pfunc 是一个函数指针,该函数的返回值是一个指针,它指向一个指针数组,指针数组中的指针指向原型为int func(int *);的函数。

指针的总结

指针就是内存的地址,C语言允许用一个变量来存放指针,这种变量称为指针变量。指针变量可以存放基本类型数据的地址,也可以存放数组、函数以及其他指针变量的地址。

程序在运行过程中需要的是数据和指令的地址,变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符:在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址;程序被编译和链接后,这些名字都会消失,取而代之的是它们对应的地址。

定 义含 义
int *p;p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组。
int **p;p 为二级指针,指向 int * 类型的数据。
int *p[n];p 为指针数组。[ ] 的优先级高于 *,所以应该理解为 int *(p[n]);
int (*p)[n];p 为数组指针。
int *p();p 是一个函数,它的返回值类型为 int *。
int (*p)();p 是一个函数指针,指向原型为 int func() 的函数。
  1. 指针变量可以进行加减运算,例如p++p+ip-=i。指针变量的加减运算并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关。

  2. 给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如int *p = 1000;

是没有意义的,使用过程中一般会导致程序崩溃。

  1. 使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值NULL

  2. 两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数。

  3. 数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof& 运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组的指针。

  4. 数组的与指针的隐藏转换规则,那就是去了变量名旁边的[]替换成*放到变量名左边,口诀就是去方框,把*放到变量名左边,例如:int a[3]转变成int *aint a[2][3]转变成int *a[3]

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值