指针在C语言中是一块很重要的内容,也是比较难理解的一块内容,我们需要反复学习反复巩固才可以对其有所了解。

复杂类型说明

以下这部分内容主要来自《让你不再害怕指针》

要了解指针,多多少少会出现一些比较复杂的类型,所以,先介绍一下如何完全理解一个复杂类型,要理解复杂类型其实很简单。

一个类型里会出现很多运算符,他们也像普通的表达式一样,有优先级,其优先级和运算优先级一样,所以我总结了一下其原则: 从变量名处起,根据运算符优先级结合,一步一步分析。

下面让我们先从简单的类型开始慢慢分析吧:

int p;

这是一个普通的整型变量 。

int *p;

首先从 P处开始,先与* 结合,所以说明 P 是一个指针。

然后再与 int 结合,说明指针所指向的内容的类型为 int 型。所以 P 是一个返回整型数据的指针。

int p[3];

首先从 P 处开始,先与[]结合,说明 P 是一个数组,然后与 int 结合,说明数组里的元素是整型的,所以 P 是一个由整型数据组成的数组。

int *p[3];

首先从 P 处开始,先与[]结合,因为其优先级比 * 高。所以 P 是一个数组,然后再与 * 结合,说明数组里的元素是指针类型。

然后再与 int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组 。

int (*p)[3];

首先从 P 处开始,先与 * 结合,说明 P 是一个指针然后再与[]结合与"()"这步可以忽略,只是为了改变优先级)。

说明指针所指向的内容是一个数组,然后再与 int 结合,说明数组里的元素是整型的。所以 P 是一个指向由整型数据组成的数组的指针。

int **p;

首先从 P 开始,先与后再与 * 结合,说明指针所指向的元素是指针,然后再与 int 结合,说明该指针所指向的元素是整型数据。

由于二级指针以上的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针。

int p(int);

从 P 处起,先与()结合,说明 P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数然后再与外面的 int 结合,说明函数的返回值是一个整型数据。

int (*p)(int);

从 P 处开始,先与指针结合,说明 P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个 int 型的参数,再与最外层的int 结合。

说明函数的返回类型是整型,所以 P 是一个指向有一个整型参数且返回类型为整型的函数的指针。

说到这里也就差不多了,我们的任务也就这么多,理解了这几个类型,其它的类型对我们来说也是小菜了。

不过我们一般不会用太复杂的类型,那样会大大减小程序的可读性,请慎用,这上面的几种类型已经足够我们用了。

分析指针的方法

指针是一个特殊的变量, 它里面存储的数值被解释成为内存里的一个地址。

要搞清一个指针需要搞清指针的四方面的内容:指针的类型、 指针所指向的类型、 指针的值(指针所指向的内存区)、 指针本身所占据的内存区。让我们分别说明。

先声明几个指针放着做例子:

(1)int *ptr;
(2)char*ptr;
(3)int **ptr;
(4)int (*ptr)[3];
(5)int *(*ptr)[4];

1、指针的类型

从语法的角度看, 你只要把指针声明语句里的指针名字去掉, 剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:

(1)int*ptr;//指针的类型是 int*
(2)char*ptr;//指针的类型是 char*
(3)int**ptr;//指针的类型是 int**
(4)int(*ptr)[3];//指针的类型是 int(*)[3]
(5)int*(*ptr)[4];//指针的类型是 int*(*)[4]

2、指针所指向的类型

当你通过指针来访问指针所指向的内存区时, 指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。

从语法上看, 你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉, 剩下的就是指针所指向的类型。例如:

(1)int*ptr; //指针所指向的类型是 int
(2)char*ptr; //指针所指向的的类型是 char
(3)int**ptr; //指针所指向的的类型是 int*
(4)int(*ptr)[3]; //指针所指向的的类型是 int()[3]
(5)int*(*ptr)[4]; //指针所指向的的类型是 int*()[4]

在指针的算术运算中, 指针所指向的类型有很大的作用。

3、指针的值

指针的值是指针本身存储的数值, 这个值将被编译器当作一个地址, 而不是一个一般的数值。

在 32 位程序里, 所有类型的指针的值都是一个 32 位  整数, 因为 32 位程序里内存地址全都是 32 位长。

指针所指向的内存区就是从指针的值所代表的那个内存地址开始, 长度为 sizeof(指针所指向的类型)的一片内存区。

以后, 我们说一个指针的值是 XX, 就相当于说该指针指向了以 XX 为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址

指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中, 指针所指向的类型已经有了, 但由于指针还未初始化, 所以它所指向的内存区是不存在的, 或者说是无意义的。

以后, 每遇到一个指针, 都应该问问:这个指针的类型是什么?指针指向的类型是什么?该指针指向了哪里?(重点注意) 。

4、指针本身所占据的内存区

指针本身占了多大的内存?你只要用函数 sizeof(指针的类型)测一下就知道了。

在 32 位平台里, 指针本身占据了 4 个字节的长度。指针本身占据的内存这个概念在判断一个指针表达式(后面会解释) 是否是左值时很有用。

指针的算术运算

指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的, 以单元为单位。

这在内存上体现为:相对这个指针向后偏移多少个单位或向前偏移了多少个单位,这里的单位与指针变量的类型有关。

在32bit环境下,int类型占4个字节,float占4字节,double类型占8字节,char占1字节。

【注意】一些处理整数的操作不能用来处理指针。例如,可以把两个整数相乘,但是不能把两个指针相乘。

示例程序

#include <stdio.h>int main(void)
{int    a = 10, *pa = &a;float  b = 6.6, *pb = &b;char   c = 'a', *pc = &c;double d = 2.14e9, *pd = &d;//最初的值printf("pa0=%d, pb0=%d, pc0=%d, pd0=%d\n", pa, pb, pc, pd);//加法运算pa += 2;pb += 2;pc += 2;pd += 2;printf("pa1=%d, pb1=%d, pc1=%d, pd1=%d\n", pa, pb, pc, pd);//减法运算pa -= 1;pb -= 1;pc -= 1;pd -= 1;printf("pa2=%d, pb2=%d, pc2=%d, pd2=%d\n", pa, pb, pc, pd);return 0;
}

运行结果为:

pa0=6422268, pb0=6422264, pc0=6422263, pd0=6422248
pa1=6422276, pb1=6422272, pc1=6422265, pd1=6422264
pa2=6422272, pb2=6422268, pc2=6422264, pd2=6422256

解析:

举例说明pa0→pa1→pa2的过程,其他类似。pa0+2*sizeof(int)=pa1,pa1-1*sizeof(int)=pa2。因为pa为int类型的指针,所以加减运算是以4字节(即sizeof(int))为单位地址向前向后偏移的。看下图:

如图:pa1所指向的地址在pa0所指向地址往后8字节处,pa2指向地址在pa1指向地址往前4字节处。

从本示例程序中,还可以看出:连续定义的变量在内存的存储有可能是紧挨着的,有可能是分散着的。

数组和指针的联系

数组与指针有很密切的联系,常见的结合情况有以下三种:

  • 数组指针

  • 指针数组

  • 二维数组指针

1、数组指针

数组指针:指向数组的指针。如:

int arr[] = {0,1,2,3,4};
int *p = arr; //也可写作int *p=&arr[0]

也就是说,p,arr,&arr[0]都是指向数组的开头,即第0个元素的地址。

如果一个指针p指向一个数组arr[]的开头,那么p+i为数组第i个元素的地址,即&arr[i],那么*(p+i)为数组第i个元素的值,即arr[i]。

同理,若指针p指向数组的第n个元素,那么p+i为第n+1个元素的地址;不管 p 指向了数组的第几个元素,p+1 总是指向下一个元素,p-1 也总是指向上一个元素。

下面示例证实了这一点:

#include <stdio.h>int main(void)
{int arr[] = {0, 1, 2, 3, 4};int *p = &arr[3];  //也可以写作 int *p = arr + 3;printf("%d, %d, %d, %d, %d\n",*(p-3), *(p-2), *(p-1), *(p), *(p+1) );return 0;
}

运行结果为:

0, 1, 2, 3, 4

2、指针数组

指针数组:数组中每个元素都是指针。如:

int a=1,b=2,c=3;
int *arr[3] = {&a,&b,&c};

示例程序:

#include <stdio.h>
int main(void)
{int a = 1, b = 2, c = 3;//定义一个指针数组int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *parr[]//定义一个指向指针数组的指针int **parr = arr;printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));return 0;
}

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

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

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

#include <stdio.h>
int main(void)
{char *str[3] ={"hello C","hello C++","hello Java"};printf("%s\n%s\n%s\n", str[0], str[1], str[2]);return 0;
}

运行结果为:

hello C
hello C++
hello Java

3、二维数组指针

二维数组指针:指向二维数组的指针。如:

int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
int (*p)[4] = a;

a [3] [4]表示一个3行4列的二维数组,其所有元素在内存中是连续存储的。

请看如下程序:

#include <stdio.h>
int main(void)
{int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };int i,j;for( i = 0; i < 3; i++ ){for( j = 0; j < 4; j++ ){printf("a[%d][%d]=%d\n", i, j, &a[i][j]);}}return 0;
}

运行结果为:

a[0][0]=6422216
a[0][1]=6422220
a[0][2]=6422224
a[0][3]=6422228
a[1][0]=6422232
a[1][1]=6422236
a[1][2]=6422240
a[1][3]=6422244
a[2][0]=6422248
a[2][1]=6422252
a[2][2]=6422256
a[2][3]=6422260

可见,每个元素的地址都是相差4个字节,即每个连续在内存中是连续存储的。

按照以上定义可归纳出如下4个结论:

(1)p指向数组a的开头,也即第1行;p+1前进一行,指向第2行。

(2)*(p+1)表示取第2行元素(一整行元素)。

(3)*(p+1)+1表示第2行第2个元素的地址。

(4)((p+1)+1)表示第2行第2个元素的值。

综上4点,可得出如下结论:

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

以上就是数组与指针常用的三种结合形式。

指针与数组的区别

数组与指针在多数情况是可以等价的,比如:

int array[10]={0,1,2,3,4,5,6,7,8,9},value;
value=array[0]; //也可写成:value=*array;
value=array[3]; //也可写成:value=*(array+3);
value=array[4]; //也可写成:value=*(array+4)

但也有不等价的时候,比如如下三种情况:

  • 数组名不可以改变,而指向数组的指针是可以改变的。

  • 字符串指针指向的字符串中的字符是不能改变的,而字符数组中的字符是可以改变的。

  • 求数组长度时,借用数组名可求得数组长度,而借用指针却得不到数组长度。

1、区别一

数组名的指向不可以改变,而指向数组的指针是可以改变的。

请看如下代码:

#include <stdio.h>int main(void)
{int a[5] = {0, 1, 2, 3, 4}, *p = a;char i;// 数组遍历方式一for ( i = 0; i < 5; i++ ){printf("a[%d] = %d\n", i, *p++);}// 数组遍历方式二for ( i = 0; i < 5; i++ ){printf("a[%d] = %d\n", i, *a++);}return 0;
}

数组遍历方式一:使用指针遍历数组元素,* p++等价于*(p++),即指针指向的地址每次后移一个单位,然后再取地址上的值。这里的一个单位是sizeof(int)个字节。

数组遍历方式二:使用数组名自增遍历数组元素,编译出错,错误如下:

error: value required as increment operand

因为数组名的指向是不可以改变的,使用自增运算符自增就会改变其指向,这是不对的,数组名只能指向数组的开头。但是可以改为如下遍历方式:

for ( i = 0; i < 5; i++ )
{printf("a[%d] = %d\n", i, *(a+i));
}

这可以正确遍历数组元素。因为*(a+i)与a[i]是等价的。

2、区别二

字符串指针指向的字符串中的字符是不能改变的,而字符数组中的字符是可以改变的。

请看如下代码:

//字符串定义方式一
char str[] = "happy";//字符串定义方式二
char *str = "happy";

字符串定义方式一:字符串中的字符是可以改变的。如可以使用类似str[3]='q'这样的语句来改变其中的字符。原因就是:这种方式定义的字符串保存在全局数据区或栈区,是可读写的。

字符串定义方式二:字符串中的字符是不可以改变的。原因就是:这种方式定义的字符串保存在常量区,是不可修改的。

2、区别三

求数组长度时,借用数组名可求得数组长度,而借用指针却得不到数组长度。

请看如下代码:

#include <stdio.h>int main(void)
{int a[] = {0, 1, 2, 3, 4}, *p = a;char len = 0;// 求数组长度方式一printf("方式一:len=%d\n",sizeof(a)/sizeof(int));// 求数组长度方式二printf("方式二:len=%d\n",sizeof(p)/sizeof(int));return 0;
}

运行结果

方式一:len=5
方式二:len=1

求数组长度方式一:借用数组名来求数组长度,可求得数组有5个元素,正确。

求数组长度方式二:借用指针求数组长度,求得长度为1,错误。原因是:

p只是一个指向int类型的指针,编译器不知道其指向的是一个整数还是指向一个数组。sizeof(p)求得的是p这个指针变量本身所占用的字节数,而不是整个数组占用的字节数。

下面还需要注意数组名的一个问题: 声明了一个数组 TYPE array[n] , 则数组名是一个常量指针,  该指针的值是不能修改的, 即类似 array++的表达式是错误的。

指针函数与函数指针

函数、指针这两个词结合的顺序不同其意义也不同,即指针函数与函数指针的意义不同。

1、指针函数

指针函数的本质是一个函数,其返回值是一个指针。示例如下:

int *pfun(int, int);

由于“*”的优先级低于“()”的优先级,因而pfun首先和后面的“()”结合,也就意味着,pfun是一个函数。即:int *(pfun(int, int));

接着再和前面的“*”结合,说明这个函数的返回值是一个指针。由于前面还有一个int,也就是说,pfun是一个返回值为整型指针的函数。

指针函数示例程序如下:

#include <stdio.h>
//这是一个指针函数的声明
int *pfun(int *arr, int n);int main(void)
{int array[] = {0, 1, 2, 3, 4};int len = sizeof(array)/sizeof(array[0]);int *p;int i;//指针函数的调用p = pfun(array, len);for (i = 0; i < len; i++){printf("array[%d] = %d\n", i, *(p+i));}return 0;
}//这是一个指针函数,其返回值为指向整形的指针
int *pfun(int *arr, int n)
{int *p = arr;return p;
}

程序运行结果如下:

主函数中,把一个数组的首地址与数组长度作为实参传入指针函数pfun里,把指针函数的返回值(即指向数组的指针)赋给整形指针p。最后使用指针p来遍历数组元素并打印输出。

2、函数指针

函数指针其本质是一个指针变量,该指针变量指向一个函数。C程序在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。函数指针示例:

/*声明一个函数指针 */
int (*fptr) (int, int);
/* 函数指针指向函数func */
fptr = func;  // 或者fptr = &func;

func是一个函数名,那么func与&func都表示的是函数的入口地址。同样的,在函数的调用中可以使用:方式一:func(),也可以使用方式二:(*fun)()。这两种调用方式是等价的,只是我们平时大多都习惯用方式一的调用方法。

至于为什么func与&func的含义相同,《嵌入式Linux上的C语言编程实践》这本书中有如下解释:

对于函数func来说,函数的名称就是函数代码区的常量,对它取地址(&func)可以得到函数代码区的地址,同时,func本身也可以视为函数代码区的地址。因此,函数名称和对其取地址其含义是相同的。

函数指针示例程序如下:

#include <stdio.h>int add(int a, int b);int main(void)
{int (*fptr)(int, int); //定义一个函数指针int res;fptr = add;  //函数指针fptr指向函数add/* 通过函数指针调用函数 */res = (*fptr)(1,2); //等价于res = fptr(1,2);printf("a + b = %d\n", res);return 0;
}int add(int a, int b)
{return a + b;
}

程序运行结果如下:

其中,函数指针广泛应用于嵌入式软件开发中,其常用的两个用途:调用函数和做函数的参数。

以上就是关于指针的的一些基础知识总结,如有错误欢迎指出!谢谢

-END-


老朋友胡船长的《C++/服务器开发4天实战特训营》终于开张了,他曾拿过ACM亚洲区金牌,把C++讲的非常透彻,相当值得一听。

特训营原价399,现仅需1元即可参加,但只有100个名额,扫码查看详情

猜你喜欢(点击下划线即可跳转阅读

聊聊指针

嵌入式大佬 | 嵌入式C语言知识点万字总结

嵌入式牛人 | 这些单片机编程思想超硬核

最 后

若觉得文章不错,转发分享,也是我们继续更新的动力。

5T资源大放送!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,PCB、FPGA、DSP、labview、单片机、等等

在公众号内回复「更多资源」,即可免费获取,期待你的关注~

长按识别图中二维码关注

遇到指针别害怕!先把这篇笔记看一遍~相关推荐

  1. 钉钉日志范文100篇_看图写话范文328:暑假旅行(4篇)

    范文01:暑假旅行400字 暑假,我们一家去了嘉兴游玩.嘉兴南湖因红船而成为革命圣地.老爸对于中国近代革命史可谓是如数家珍:"星星之火可以燎原!你们看,当初共产党就是在这么小的一艘船里点燃了 ...

  2. 菜鸟学Linux 第044篇笔记 算法和私有CA

    菜鸟学Linux 第044篇笔记 算法和私有CA 证书吊销列表CRL(Certificate Revocation List ) 如何解决私钥丢失 PKI: Public Key Infrastruc ...

  3. windows pxe 安装linux,菜鸟学Linux 第103篇笔记 pxe自动化安装linux

    菜鸟学Linux 第103篇笔记 pxe自动化安装linux 内容总览 linux的系统安装 kickstart文件的组成部分 DHCP (Dynamic Host Configuration Pro ...

  4. 菜鸟学Linux 第050篇笔记 dhcp

    菜鸟学Linux 第050篇笔记 dhcp DHCP (Dynamic Host Configuration Protocol) 早期bootp (boot protocol) lease Clien ...

  5. 菜鸟学Linux 第090篇笔记 corosync+drbd+mysql

    菜鸟学Linux 第090篇笔记 corosync+drbd+mysql 内容总览 上节回顾 DRBD (Distributed Replicated Block Device) 分布式复制块设备 配 ...

  6. 菜鸟学Linux 第033篇笔记 bootloader,inittab

    菜鸟学Linux 第033篇笔记 bootloader,inittab Linux 系统自启动流程 PC OS (Linux) POST-->BIOS(Boot Sequence)-->M ...

  7. 菜鸟学Linux 第052篇笔记 httpd-install and section2

    菜鸟学Linux 第052篇笔记  httpd-install and section2 apache 17years NCSA, httpd A Patchey Server = Apache FS ...

  8. Linux 日志 klogd,菜鸟学Linux 第038篇笔记 日志系统 syslogd,klogd

    菜鸟学Linux 第038篇笔记 日志系统 syslogd,klogd Linux上的日志系统 syslog开源 syslog-ng  商业版 日志系统 syslog syslog 服务 syslog ...

  9. python采用pika库使用rabbitmq总结,多篇笔记和示例

    这一段时间学习了下rabbitmq,在学习的过程中,发现国内关于python采用pika库使用rabbitmq的资料很少,官网有这方面的资料,不过是都英文的.于是笔者结合自己的理解,就这方面内容写了一 ...

最新文章

  1. Apache 编译安装
  2. Oracle 10g R2 RAC手动打补丁PSU(10.2.0.5.19)
  3. BZOJ1415 [Noi2005]聪聪和可可 【SPFA + 期望dp记忆化搜索】
  4. xp系统怎么弄清微软服务器名称,xp系统电脑怎么远程云服务器
  5. Windows下nginx-http-flv-module服务部署
  6. 基于国密算法SM2SSL证书的https加密,如何实现?
  7. 游族内部信:年终奖如期发放 继续招聘全球化游戏人才
  8. 笨方法学python - 04
  9. 参考资料:图片效果展示
  10. Prescan入门教程之避坑笔记:初学者初用
  11. Yahoo的Spark实践
  12. 混沌初开:全新HarmonyOS 2正式到来!
  13. 嵌入式系统设计(三):Vim编辑器的学习
  14. 弱口令审计-工具用法大全
  15. c#窗体编辑个人简历_C#开发工程师完整简历范文
  16. error MSB6006: “CL.exe”已退出,代码为 2 问题
  17. 计算机网络:基础概念
  18. YUV和libyuv相关了解
  19. Python爬取冰冰的第一条vlog并进行数据分析
  20. 录制电脑内部声音,2个方法,轻松解决

热门文章

  1. 如何使用SAP事务码SAT进行UI应用的性能分析
  2. 程序员谈敏捷开发团队里成员间的相互信任
  3. SAP Customer Experience Extensibility gold rule
  4. Batch request processing in backend
  5. SAP UI5 函数节流和异步完成令牌的应用
  6. Kubenetes里pod和service绑定的实现方式
  7. 在ABAP里模拟实现Java Spring的依赖注入
  8. Opportunity的chance of success的赋值逻辑
  9. 多线程 java 实例_Java多线程实例学习
  10. python注释_python中的单行注释、多行注释