当前位置:   article > 正文

C语言-指针进阶详解

C语言-指针进阶详解

指针基本概念

在初阶指针中我们了解到一些指针的基本概念:
1.指针就是个变量,用来存放地址,地址唯一标识一块内存

2.指针的大小是固定的4/8个字节(32位/64位平台)

3指针是有类型的,指针的类型决定了指针的+整数的步长,指针解引用时操作的权限

例如,char*类型的指针解引用,它的权限(或步长)在32位平台下只有一个字节,而int*型的指针在32位平台下的权限(步长)为4个字节。

1.字符指针

  1. #include<stdio.h>
  2. int main()
  3. {
  4. char ch = 'w';
  5. char* pc = &ch;
  6. *pc = 'w';//这里的pc就是一个字符指针
  7. return 0;
  8. }

 这里的pc就是一个字符指针,可以通过解引用pc来对w进行操作。

  1. #include<stdio.h>
  2. int main()
  3. {
  4. char arr[]="abcdef";//数组内容为字符串
  5. //char *p="abcdef";//指针存储的内容为字符串"abcdef"的首个字母的地址
  6. const char *p="abcdef"//abcdef为常量字符串,需要加const来修饰
  7. return 0;
  8. }

 这两个定义有什么区别, char arr[]='' abcdef''只是一个对数组普通的初始化,将字符串''abcdef''放数组arr中

char *p="abcdef"中,abcdef是常量字符串,其前面需要用const来修饰,那么第二个操作是不是直接将整个字符串保存在字符指针中了呢?答案是否定的。

该操作实际上是将常量字符串''abcdef''的首个字符的地址保存到字符指针*p中

而我们应该如何验证呢?

  1. #include<stdio.h>
  2. int main()
  3. {
  4. char arr[] = "abcdef";
  5. const char* p = "abcdef";
  6. printf("%s\n",p);
  7. printf("%c",*p);
  8. return 0;
  9. }

 若此时p中存储的是字符串"abcdef"首字符的地址,那么第一个printf打印的结果应为"abcdef"

第二个printf中对字符指针p进行*解引用的结果应该是字符串首字符a。

ef2e6e602e104f79be573c565a2cb493.png

 于是我们得到了结论:该操作实际上是将常量字符串''abcdef''的首字符的地址保存到字符指针*p中

 有这样一道面试题来练手,如下:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. char str1[] = "hello bit.";
  5. char str2[] = "hello bit.";
  6. const char *str3 = "hello bit.";
  7. const char *str4 = "hello bit.";
  8. if(str1 == str2)
  9. printf("str1 and str2 are same\n");
  10. else
  11. printf("str1 and str2 are not same\n");
  12. if(str3 == str4)
  13. printf("str3 and str4 are same\n");
  14. else
  15. printf("str3 and str4 are not same\n");
  16. return 0;
  17. }

 1ab25603975b46e89894b9de6cf29285.png

可以看到str1与str2是不相同的,而str3与str4是相同的,那么这究竟是什么原因呢?

 char str1[] = "hello bit.";
 char str2[] = "hello bit.";

两个数组的内容是相同的,但是开辟的并不是同一块空间 ,str1和str2都是数组名,代表了数组首元素的地址,而两个数分别开辟了不同的内存空间,所以首元素的地址不同,所以str1与str2不同而对于str3和str4,"hello bit."由const修饰,为常量字符串同时str3与str4都是字符指针,存储的是字符指针"hello bit."的首字符的地址,且其是由const修饰的常量字符串,故str3与str4指向的空间的地址相同,所以str3==str4;

2.指针数组

类比:

整型数组-存放整型的数组;

字符数组-存放字符的数组;

指针数组-存放指针的数组;

  1. #include<stdio.h>
  2. int main()
  3. {
  4. int* arr1[10];//整形指针的数组
  5. char *arr2[4];//一级字符指针的数组
  6. char **arr3[5];//二级字符指针的数组
  7. return 0;
  8. }

使用指针数组模拟实现二维数组

  1. #include<stdio.h>
  2. int main()
  3. {
  4. int arr1[] = { 1,2,3,4,5 };
  5. int arr2[] = { 2,3,4,5,6 };
  6. int arr3[] = { 3,4,5,6,7 };
  7. int* arr[3] = {arr1,arr2,arr3};
  8. int i = 0;
  9. for (i = 0; i < 3; i++)
  10. {
  11. int j = 0;
  12. for (j = 0; j < 5; j++)
  13. {
  14. printf("%d", arr[i][j]);
  15. }
  16. printf("\n");
  17. }
  18. return 0;
  19. }

4252ba41bb284befa008b6a4fc10f0c4.png

用i遍历左边的数组,用j遍历右边的数组 ,结果如下图:

8d8807b8eab7469abbb9039f414e4fba.png

需要特别注意的是,指针数组模拟实现的二维数组与真正的二维数组不同的是,该指针数组中各元素地址的存放不一定是连续的。

3.数组指针

3.1数组指针概念

首先来做一个区分

整形指针-指向整型变量的指针,存放整型变量的地址的指针变量;

字符指针-指向字符变量的指针,存放字符变量的地址的指针变量;

数组指针-指向数组变量的指针,存放数组变量的地址的指针变量;

  1. int*p1[10];//指针数组(p1先与[]结合)
  2. int(*p2)[10];//数组指针(p2先与*运算符结合)

p2是指针指向的是数组,称为数组指针变量。

3.2对数组名的理解

数组名是数组首元素的地址,但是有两个例外,如下

1.sizeof(数组名),这里的数组名不是数组首元素的地址,数组名此时表示整个数组,sizeof(数组名)计算是整个数组的大小,单位是字节。

2.&数组名,这里的数组名表示整个数组,&数组名取出的是整个数组的地址

除此之外,所有地方的数组名都是数组首元素的地址。

  1. #include<stdio.h>
  2. int main()
  3. {
  4. int arr[10]={0};
  5. printf("%p\n",arr);
  6. printf("%p\n",&arr[0]);
  7. printf("%p\n",&arr);
  8. return 0;
  9. }

7499578574e54ccea31012a28a968abf.png

 前两个输出都表示的是数组首元素的地址,而第三个应该表示的是整个数组的地址,但是我们可以观察到三者的输出结果是相同的,

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

观察运行结果

d0c0140edb49403aa8f009cce4fd94d7.png

 前两个输出+1,都只跳过了4个字节,而第三个+1跳过了40个字节,正好是数组十个元素所占的字节数,由此可见    &数组名    代表的是整个数组的地址。

再观察下图,首元素的地址和数组的地址在占用内存的情况,更加证实了我们上面的说法。

7184e40539464fb1835594375ed8cffb.png

3.3数组指针的使用

int(*p)[10]=&arr; 定义了一个数组指针,p的类型是int (*)[10](但是在监视窗口中,显示的类型为int[10]*,这大概是编译器为了方便我们理解才这样写的吧),下面是数组指针的写法。

  1. #include<stdio.h>
  2. int main()
  3. {
  4. int(*p)[10]=&arr;
  5. return 0;
  6. }

下面是一个使用数组指针打印二维数组的例子。

下面是打印二维数组的一种较为普通常见的写法:二维数组传参,形参是二维数组的形式

  1. #include<stdio.h>
  2. void Print(int arr[3][5], int r, int c)
  3. {
  4. int i = 0;
  5. for (i = 0;i < 3; i++)
  6. {
  7. int j = 0;
  8. for (j = 0; j < 5; j++)
  9. {
  10. printf("%d", arr[i][j]);
  11. }
  12. printf("\n");
  13. }
  14. }
  15. int main()
  16. {
  17. int arr[3][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 };
  18. Print(arr,3,5);
  19. return 0;
  20. }

首先,对于二维数组,我们可以这样来理解:二维数组的每一行可以理解为二维数组的一个元素,每一行又是一个一维数组,所以我们可以这样理解二维数组其实是一维数组的数组。除此之外,二维数组的数组名,代表的是首元素地址,而首元素为第一个一维数组,即为第一行的地址,也是第一行一维数组的地址 

762dbbb1e2414704993c122e66e69b45.png

在学习数组指针之后,我们可以尝试二维数组传参,形参是指针的形式。 如下:

  1. #include<stdio.h>
  2. void Print(int (*p)[5], int r, int c)
  3. {
  4. int i = 0;
  5. for (i = 0; i < r; i++)
  6. {
  7. int j = 0;
  8. for (j = 0; j < c; j++)
  9. {
  10. printf("%d",*(*(p+i)+j));
  11. }
  12. printf("\n");
  13. }
  14. }
  15. int main()
  16. {
  17. int arr[3][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 };
  18. //arr为二维数组的数组名,为首元素的地址,指针传参
  19. Print(arr, 3, 5);
  20. return 0;
  21. }

 对于这里的*(*(p+i)+j)进行分析,p为传参所传来的二维数组首元素的地址,也就是第一行一维数组的地址,在对其解引用*(p+i),相当于某一个一维数组的数组名,同时在用另外一个数j来对这个一维数组中的其他元素进行遍历获得他们的地址也就是*(p+i)+j,最后得到这个地址后在进行解引用即可获得二维数组中的所有元素*(*(p+i)+j)

一维数组传参,形参的部分可以是数组也可以是指针,同样二维数组传参的部分可以是数组也可以是指针,下面以二维数组为例:
 

  1. #include<stdio.h>
  2. void test1(char arr[3][5], int r, int c)
  3. {}
  4. void test2(char(*p)[5], int r, int c)
  5. {}
  6. int main()
  7. {
  8. char arr[3][5] = { 0 };
  9. test1(arr, 3, 5);
  10. test2(arr, 3, 5);
  11. return 0;
  12. }

 该二维数组为3行5列,指针传参的时候传过去的是第一行的地址,第一行有五个元素,故为

char(*p)[5]。                                            补充:(p[i]==*(p+i));

4.数组参数,指针参数

4.1一维数组传参

例子如下:

  1. #include<stdio.h>
  2. void test(int arr[]) {}//ok? ok
  3. void test(int arr[10]){}//ok? ok
  4. void test(int *arr){}//ok? ok
  5. void test2(int *arr[20]){}//ok? ok
  6. void test2(int **arr){}//ok? ok
  7. int main()
  8. {
  9. int arr[10] = { 0 };
  10. int* arr2[20] = { 0 };
  11. test(arr);
  12. test2(arr2);
  13. return 0;
  14. }

 重点讲解void test2(int **arr){},  arr2的类型是int *[20],而arr2是一个指针,将int *类型的数据传过去,是一个二级指针,用int** arr接受,合理。

4.2二维数组传参

  1. #include<stdio.h>
  2. void test(int arr[3][5]) {}//ok? √
  3. void test(int arr[][]) {}//ok? ×
  4. void test(int arr[][5]) {}//ok? √
  5. void test(int* arr) {}//ok? ×
  6. void test(int* arr[5]) {}//ok? ×
  7. void test(int (*arr)[5]) {}//ok? √
  8. void test(int **arr) {}//ok? ×
  9. int main()
  10. {
  11. int arr[3][5] = {0};
  12. test(arr);
  13. }

二维数组的列是不能够省略的,传参过去后,可以用数组指针来接受,而不是指针数组。

最后一个,传参过去的是数组名,是一级指针,不能够用二级指针来接收。

1.当一个函数的参数部分为一级指针时,函数能接受什么参数?

void test(char *p){}

char ch='2';

char ptr=&ch;

char arr[]="abcdef";

test(&ch);

test(ptr);

test(arr);//传过去的参数为一级指针即可。

2.当函数的参数为二级指针时,可以接受什么参数?

void test(char** p){}

int n=10;

int *p=&n;

int **p=&p;

char* arr[5];

test(&p);

test(pp);

test(arr)

5.函数指针

5.1函数指针的概念

即为指向函数的指针。

int (*)(int,int)就是函数指针的类型

  1. #include<stdio.h>
  2. int Add(int x, int y)
  3. {
  4. return x + y;
  5. }
  6. int main()
  7. {
  8. int (*pf)(int,int)=&Add;//pf为函数指针
  9. return 0;
  10. }

5.2对函数名的李姐

  1. #include<stdio.h>
  2. int Add(int x, int y)
  3. {
  4. return x + y;
  5. }
  6. int main()
  7. {
  8. printf("%p\n", &Add);
  9. printf("%p\n", Add);
  10. return 0;
  11. }

7af8dd08a0484605956c31b7a3aa252c.png

 函数名和&函数名的运算结果都是函数的地址。

5.3函数指针的使用

练习填写函数指针

给出test函数的定义,用pf表示test。

  1. #include<stdio.h>
  2. void test(char* pc, int arr[10])
  3. {
  4. }
  5. int main()
  6. {
  7. void (*pf)(char*, int[10]) = &test;
  8. return 0;
  9. }

函数名的用法

  1. #include<stdio.h>
  2. int Add(int x, int y)
  3. {
  4. return x + y;
  5. }
  6. int main()
  7. {
  8. int (*pf)(int, int) = Add;
  9. int r = Add(3, 5);
  10. printf("%d\n", r);
  11. int m = (*pf)(4, 5);//int m=pf(4,5)也可以。
  12. printf("%d\n", m);
  13. }

2c27b25871b541298c30ab49a16f2d6e.png

    int m = (*pf)(4, 5);也可以实现Add函数,那么是怎么实现的呢?

    int (*pf)(int, int) = Add;首先将Add存在了pf中,这时*pf相当于函数名。

下面有两段interesting的代码

  1. int main()
  2. {
  3. (*( void (*)() )0 )();
  4. return 0;
  5. }

 这一代码其实就是调用0地址处的函数。在调用前将0转换成了void(*)()类型的指针。

这一段代码是这样实现的:

1.将0强制类型转化为void(*)()类型的函数指针

2.调用0地址处的这个函数

第二段interesting的代码:

  1. int main()
  2. {
  3. void(* signal(int,void(*)(int)) )(int);
  4. }

通过分析我们知道了signal是一个函数名,参数是int型和void(*)(int)函数指针类型。

void(* signal(int,void(*)(int)) )(int);

signal(int,void(*)int)) ,整个代码是一个函数指针,再看看去掉这一大块之后留下的内容

void(* )(int);就是上面内容的函数返回类型。

 5.4小优化

  1. #include<stdio.h>
  2. typedef unsigned int uint;
  3. typedef int* prt_t;
  4. typedef int(*parr_t)[10];//错误的写法typedef int(*)[10]parr_t;
  5. typedef int (*pf_t)(int, int);//此时pf_t就是这个函数指针的类型
  6. int main()
  7. {
  8. uint u1;
  9. prt_t p1;
  10. int* p2;
  11. return 0;
  12. }

 再对上面signal函数进行小小优化:

  1. #include<stdio.h>
  2. typedef void(*pf_t)(int);
  3. int main()
  4. {
  5. void(*signal(int, void(*)(int)))(int);
  6. pf_t signal(int, pf_t);
  7. return 0;
  8. }

可以看到啊,非常的简洁啊。

6.函数指针数组

函数指针存放在数组中。

小结

创作不易,留下你的赞可好?

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/650835
推荐阅读
相关标签
  

闽ICP备14008679号