当前位置:   article > 正文

C语言 | 指针详解_pc指针

pc指针

        在学习C语言的时候,很多人都因为指针而被劝退,其实,当我们仔细一点一点的啃下这块硬骨头时,回过头来看,其实指针也并没有我们想象的那样难,这篇博客小编就带着大家有由入门到进阶,一起细细体会指针的奥妙之处。

前言

总有人要赢,为什么不能是我?     ----   科比

一.指针的定义 

什么是指针?

1. 指针是内存中一个最小单元的编号,也就是地址

2.平常口语中的指针通常是指针变量

1.那么什么是地址呢?地址又是怎么能来的呢?

前面我们讲过,地址就是一个编号,那么这个编号怎么来的呢?实际上,在32位的机器上,有32根地址线,每根地址线由高低电频表示1和0,而由这32根地址线可以表示2^32个编号,每个编号就是我们所说的地址了,可按照下图理解

         每个地址都能找到对应的一块空间,每块空间大小都是1字节,我们也可以将地址编号理解成为我们生活中酒店的门牌号,而每个门牌号都对应一个房间,每个地址也有对应的一块内存空间。

2.指针变量

所谓指针变量就是储存指针的变量,我们可以通过&来取出变量的内存地址,如下

int a = 5;

int* pa = &a;

此时pa中储存的便是a的地址。我们将pa称作指针变量。

        简而言之,指针是地址,指针变量是储存地址(指针)的变量。我们有时口语称指针变量为指针。 

3.指针变量的大小

        我们了解指针变量的概念以后,我们必须知道指针的大小,前面我们也有说过,在32位机器下,指针是由32根地址线产生的01信号组成的编号,每一个信号都要由一个比特位储存,因此,在32位机器下,指针的大小是4字节(32比特位),在64位机器下,指针的大小是8字节(64比特位)。

小结:指针的大小与指针储存的数据类型无关,只与操作系统的机器数有关。

4.指针和指针的类型 

        我们都知道普通变量都有不同的类型,由整型,字符型,浮点型等等,而指针变量也有不同的类型,即  "type" + *

int a = 5;

假设有以上a变量,那么我们可以将a的地址储存进以下指针变量中

char* pc = &a;

short* ps = &a;

int* pi = &a;

long* pl = &a;

float* pf = &a;

double* pd = &a;

        既然一个整型类型的数据可以被别的不同类型数据的指针储存,那么指针类型的意义又是什么呢?这需要引出指针的另外一个概念了,以下将会讲解指针类型的意义究竟是什么。

5.指针加减整数 

观察以下代码,揣测代码输出结果的原因。

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

 

         我们将n的地址分别储存进pc和pi两种不同类型的指针变量中,我们发现他们会指向该变量的同一起始地址,而分别对他们加一时,字符型指针变量的地址加了1,而整型指针变量的地址加了4,我们发现指针变量的类型决定了指针向前或向后移动的距离。

总结:指针的类型决定了指针向前或向后移动的步长。

 6.指针的解引用

        既然我们可以通过将变量的地址储存进指针中,那我们是否可以通过该指针来引用该变量呢?答案当然时肯定的,这时,我们引入另一个符号,解引用符号(*)。

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int n = 0x11223344;
  5. char *pc = (char *)&n;
  6. int *pi = &n;
  7. *pc = 0;
  8. *pi = 0;
  9. return 0;
  10. }

        我们在内存监视窗口中,确实发现了pc和pi指向同一块内存区域

 此时我们的代码还未执行第23行,接下来我们执行第23行后的代码如下图

       我们发现pc指针确实修改了数据,但他仅仅只是将第一个字节修改成了0,再一次验证了指针类型的作用,char类型的指针一次只能访问一个字节。接下来我们执行第24行代码。

        这一次我们发现pi指针修改了四个字节,因为pi指针是整型指针,一次可以访问四个字节 ,指针的类型也会影响一次访问字节的个数。

补充一下:

         很多新手在学习指针的时候可能会有以下疑惑,定义指针和解引用中的星号的差异。

虽然这两个星号是同一符号,但是其意义有本质区别。看以下代码

 二.野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

 1.野指针的成因

(1)指针未初始化

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int *p;//局部变量指针未初始化,默认为随机值
  5. *p = 20;
  6. return 0;
  7. }

(2)指针越界访问

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int arr[10] = {0};
  5. int *p = arr;
  6. int i = 0;
  7. for(i=0; i<=11; i++)
  8. {
  9. //当指针指向的范围超出数组arr的范围时,p就是野指针
  10. *(p++) = i;
  11. }
  12. return 0;
  13. }

三.指针的基本运算

指针的基本运算一般分为以下三种 :

  • 指针加减整数
  • 指针减指针
  • 指针的关系运算

1.指针的加减整数 

 指针加减的步长与指针的类型相关:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int a[10];
  5. int* pi = a;
  6. for (int i = 0; i < 10; i++)
  7. {
  8. *pi = i;
  9. pi += 1;
  10. }
  11. return 0;
  12. }

2.指针-指针

指针减指针得到的是两指针间的元素个数

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

 3.指针的比较运算

 指针与指针之间也可以进行比较,高地址处的指针大于低地址处指针

  1. #include <stdio.h>
  2. #define Len 5
  3. int main()
  4. {
  5. int values[Len];
  6. int* vp;
  7. for (vp = &values[Len - 1]; vp >= &values[0]; vp--)
  8. {
  9. *vp = 0;
  10. }
  11. return 0;
  12. }

四.二级指针 

        学习了上面的内容,我们知道每个变量都有自己的地址,那么指针变量的地址应该存放在哪里呢?这时候引出一个新概念----二级指针

 1.二级指针的定义

什么叫二级指针呢,简单来说,一级指针变量的地址便是二级指针,看以下代码;

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int num = 10;
  5. int* pi = &num;
  6. //这里定义中有两个颗星,分别有不同的意义
  7. //第二颗星星先与ppi结合,告诉我们,ppi是一个指针
  8. //而第一颗星与int结合,告诉我们ppi指针指向数据的类型是一个整型指针的类型
  9. int** ppi = &pi;
  10. return 0;
  11. }

        理解定义中星星的作用是理解二级指针的关键,依次类推,依次由三级指针,四级指针等等,这些指针统称为多阶指针,实际应用中,多阶指针的使用并不常见,这里仅仅作为了解即可。

2.二级指针的解引用

观察以下代码,体会二级指针的解引用;

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int a = 3;
  5. int* pa = &a;
  6. int** ppa = &pa;
  7. //当对二级指针ppi进行一次解引用时,我们得到的是pa变量中储存的值
  8. //也就是a变量的地址
  9. printf("%p\n", *ppa);
  10. //当我们对二级指针两次解引用时,我们得到的是a变量的值
  11. printf("%d\n", **ppa);
  12. return 0;

 

 我们也可以通过下图来理解二级指针;

        变量a中储存的是数据3,变量pa中储存的是a的地址,对pa一次解引用,也就是通过a的地址访问到变量a,变量ppi中储存的是pa的地址,我们对ppi一次解引用,也就是通过pa的地址访问到变量pa,对ppi第二次解引用也就是对pa解引用,访问到变量a。

五.字符指针 

 字符指针是用char*来定义,一般使用方式为:

char ch = 'a';

char* pc = &ch;

 但是除了以上的用法,字符指针还有另外一种用法,如下代码;

  1. #include <stdio.h>
  2. int main()
  3. {
  4. char* str = "hello world!";
  5. printf("%s\n", str);
  6. return 0;
  7. }

与上面代码,还有一种类似的写法,很多萌新都搞不清这两种的区别,如下;

  1. #include <stdio.h>
  2. int main()
  3. {
  4. char str[] = "hello world!";
  5. printf("%s\n", str);
  6. return 0;
  7. }

两种写法都可打印出字符串 "hello world!",但是两者之间却有很大的差距: 

第一种写法中字符指针只是储存了字符串常量首字符 'h' 的地址,对应关系如下图;

 而第二种写法中,数组str将字符串中的每个字符都存进了str数组中,具体关系如下图;

在了解以上知识后,对于下面面试题应该可以很轻松解决了

以下代码的输出是什么?

  1. #include <stdio.h>
  2. int main()
  3. {
  4. char str1[] = "hello world.";
  5. char str2[] = "hello world.";
  6. const char* str3 = "hello world.";
  7. const char* str4 = "hello world.";
  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;

        在观察以上代码后,我们可以知道,str1和str2是两个字符数组,每个字符数组会在内存中开辟自己的空间,而str3和str4是字符指针,他们储存的是字符串常量中字符h的地址,通过以上分析,str1和str2不相同,因为数组名代表首元素地址,而这两个数组在内存中分别有着自己的空间,故str1和str2不同,而str3和str4是字符指针,他们储存的都是字符串常量中字符h的地址,故str3和str4相同。

六.指针数组和数组指针

1.指针数组

指针数组是指针还是数组呢?

指针数组的本质是数组

以下是指针数组的定义:

int* arr[4]; 

该数组在内存中以如下方式储存:

数组中的每个元素都是一个指针 

2.数组指针 

(1)指针数组的定义

数组指针的本质是指针;

以下为数组指针的定义方式:

int (*p)[5]; 

数组指针的定义仅仅只是在指针数组定义上加上了一个括号;

int *p[5];          -----   指针数组

int (*p)[5];        -----   数组指针

        我们可以这么理解,当没有小括号时,因为方括号[] 优先级高于星号,故p先与方括号结合,形成数组,拿下方括号,剩下的便是数组的类型;而第二组,由于小括号,p先于*组合,形成指针,拿去星号,便是该指针指向的数据类型。

 (2)数组名与&数组名

 我们之前学习过,数组名代表首元素地址。观察下列代码。

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

        输出如上所示,我们发现数组名和&数组名最后输出的地址相同。那么两者又有怎么样的区别呢?看如下代码

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

        通过计算我们不难看出,数组名加1跳过了4个字节,而&数组名加1跳过了40个字节,虽然数组名和&数组名的值是相同的,而他们加1的值却有很大的差异,原因是&数组名的本质其实是一个数组指针,结合前面的知识,指针加1的步长由指针的类型决定,而上面的代码指针指向的类型是一个十个元素的整型数组,因此加1跳过40字节,而数组名仅仅代表首元素地址,即数组名在上面的代码可以理解为是一个整型指针,加1移动4个字节。

(3)数组指针的使用

 二维数组的数组名可以理解成为数组指针

  1. #include <stdio.h>
  2. //此处的第一个参数还可以写成arr[][5]或arr[3][5]
  3. void print(int(*arr)[5], int row, int col)
  4. {
  5. int i = 0;
  6. for (i = 0; i < row; i++)
  7. {
  8. for (int j = 0; j < col; 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,6,7,8,9,10,11,12,13,14,15 };
  18. print(arr, 3, 5);
  19. return 0;
  20. }

 可根据下图来理解为什么可以用数组指针来接收;

        上图是一个3行5列的二维数组,我们arr数组名代表首元素的地址,而这个二维数组的首元素仍然是一个数组,我们将其数组名看作arr[1],取其地址,接收该地址的类型便是数组指针。

(4)小试牛刀 

分析以下变量

int arr[5];

int *parr1[10];

int (*parr2)[10];

int (*parr3[10])[5];

        arr是一个整型数组,数组中一共有5个元素,每个元素都是整型。

        parr1是一个指针数组,数组一共有10个元素,每个元素都是整型指针类型。

        parr2是一个数组指针,指针指向的数组有十个元素,每个元素都是整型。

        parr3是一个数组指针数组,给数组一共有十个元素,每个元素都是一个指向一个有五个整形的数组的指针。

七.数组参数和指针参数 

1.一维数组传参 

观察以下代码,形参是否正确书写?

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

        在test1中,第一种和第二种形参为一维数组,其中一维数组作为形参时,方括号中的数组大小可省略,形参也为一维数组,故都正确。第三种形参为一级指针,在传实参中,实参数组名代表首元素地址,而首元素地址类型也是int*,故第三种也正确。

        在test2中,第一种形参为数组,且为指针数组,而实参也是指针数组,故第一种正确;第二种形参为二级指针,实参是指针数组,且数组中每个元素为int*类型,而传过去的数组名,数组名单独出现是表示首元素地址,首元素为int*,故int*取地址为int**,故第二种也正确。

2.二维数组传参

 观察以下代码,形参是否正确书写?

  1. void test(int arr[3][5])//ok?
  2. {}
  3. void test(int arr[][])//ok?
  4. {}
  5. void test(int arr[][5])//ok?
  6. {}
  7. void test(int *arr)//ok?
  8. {}
  9. void test(int* arr[5])//ok?
  10. {}
  11. void test(int (*arr)[5])//ok?
  12. {}
  13. void test(int **arr)//ok?
  14. {}
  15. int main()
  16. {
  17. int arr[3][5] = {0};
  18. test(arr);
  19. }

        通过观察,我们发现以上有7种传参方式,而123是形参为数组的形式,4567为形参为指针的形式 ;

        当实参为二维数组名,形参也为二维数组时,形参中数组行下标可以省略,列下标不能省略,故1 3正确,2错误。

        当实参为二维数组名,形参为指针时,实参传过去的是二维数组的首元素地址,而二维数组首元素是一个元素为5的整型数组,也就是实际传的是这个数组的地址,而第6种用的正是一个数组指针,且该指针指向一个5个整型的数组,符合实参,而第4种形参用的是一个一级指针,明显不行,第5种用的是一个指针数组,也不符合,第7种用的是一个二级指针,二级指针是用来接收一级指针的地址,而实参传过来的是一个数组的地址,也不符合,所以4567中,只有6符合。

3.一级指针传参 

以下为一级指针传参的实例;

  1. #include <stdio.h>
  2. void print(int *p, int sz)
  3. {
  4. int i = 0;
  5. for(i=0; i<sz; i++)
  6. {
  7. printf("%d\n", *(p+i));
  8. }
  9. }
  10. int main()
  11. {
  12. int arr[10] = {1,2,3,4,5,6,7,8,9};
  13. int *p = arr;
  14. int sz = sizeof(arr)/sizeof(arr[0]);
  15. //一级指针p,传给函数
  16. print(p, sz);
  17. return 0;
  18. }

那么假设给个有一个函数的形参为一级指针,那么它的实参实参传参方式有哪些呢?

  1. void test1(int *p)
  2. {
  3. }

分别为以下三种:

1. &a

2.传一级指针

3.传一维数组名

4.二级指针传参

以下为二级指针传参实例;

  1. #include <stdio.h>
  2. void test(int** ptr)
  3. {
  4. printf("num = %d\n", **ptr);
  5. }
  6. int main()
  7. {
  8. int n = 10;
  9. int*p = &n;
  10. int **pp = &p;
  11. test(pp);
  12. test(&p);
  13. return 0;
  14. }

 反过来思考,那么一个二级指针的形参,我们可以传入哪些实参呢?

  1. void test(char **p)
  2. {
  3. }
  4. int main()
  5. {
  6. char ch = 'c';
  7. char* pc = &ch;
  8. char** ppc = &ch;
  9. char* arr[5];
  10. //传一级指针取地址
  11. test(&pc);
  12. //传二级指针
  13. test(ppc);
  14. //传指针数组的数组名
  15. test(arr);
  16. return 0;
  17. }

以上三种都可。

八.函数指针 

1.函数指针的概念与定义 

所谓函数指针,便是指向函数的指针。 观察以下代码

  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. }

        取函数的地址与取数组的地址类似,我们可以通过函数名得到函数的地址,或者通过取地址符得到函数地址,这两者不像数组有差异,这两者是等价的。

函数指针的定义如下代码

  1. #include <stdio.h>
  2. int Add(int x, int y)
  3. {
  4. return x + y;
  5. }
  6. int main()
  7. {
  8. //以下两种写法均可
  9. int (*pf)(int, int) = Add;
  10. int (*pf)(int x, int y) = Add;
  11. return 0;
  12. }

       在定义函数指针时,我们可以回忆数组指针的定义,函数指针也是类似,首先我们需要将星号和变量名用小括号圈起来,这使变量名首先与星号结合,形成指针,随后我们在后面写上函数的参数,前面写上函数的返回值类型,其中函数参数的形参名可以省略。

2.函数指针的调用 

观察以下代码,揣摩函数指针的调用

  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. //以下两种写法均可
  10. //int sum = (*pf)(2, 5);
  11. int sum = pf(2, 5);
  12. printf("%d\n", sum);
  13. return 0;
  14. }

 在利用函数指针调用函数时,可以不需要解引用;

 在学习了以上的代码后来阅读下面两段代码;(来自《C陷阱与缺陷》)

  1. //代码1
  2. (*(void (*)())0)();

         以上代码是将整型0看作一个地址,并强制类型转换为void (*) ()类型,并对其解引用,调用该函数。

  1. //代码2
  2. void (*signal(int , void(*)(int)))(int);

提示:突破点signal;

该段代码是一个函数定义;

函数名是signal;

signal函数的第一个参数是int;

signal函数的第二个参数是一个函数指针 ---  void(*)(int),该函数指针的参数是int,返回值是void 

signal函数的返回值是一个函数指针  ---  void(*)(int),该函数指针的参数是int,返回值是void。

 代码2还可以简化为以下代码

  1. typedef void(*pfun_t)(int);
  2. pfun_t signal(int, pfun_t);

 九.函数指针数组

 函数指针数组可以理解为将函数指针储存进数组中,这个数组便是函数指针数组。

1.函数指针数组的定义

  1. #include <stdio.h>
  2. int Add(int x, int y)
  3. {
  4. return x + y;
  5. }
  6. int Sub(int x, int y)
  7. {
  8. return x - y;
  9. }
  10. int main()
  11. {
  12. //函数指针数组的定义
  13. int (*Farr[2])(int x, int y) = { Add, Sub };
  14. //调用
  15. int ret = Farr[1](2, 5);
  16. printf("%d\n", ret);
  17. return 0;
  18. }

        函数指针数组的定义与数组指针数组的定义类似,我们定义的Farr先与方括号结合,形成数组,拿去数组后,剩下的便是该数组的类型,也就是函数指针。

2.函数指针数组的应用

我们可以利用函数指针数组完成计算器功能(转移表)

  1. #include <stdio.h>
  2. int Add(int x, int y)
  3. {
  4. return x + y;
  5. }
  6. int Sub(int x, int y)
  7. {
  8. return x - y;
  9. }
  10. int main()
  11. {
  12. int input = 0;
  13. //我们将函数添加进函数指针数组中
  14. int (*Farr[])(int, int) = {0, Add, Sub };
  15. do
  16. {
  17. printf("请选择算法(1.Add 2.Sub 0.exit):>");
  18. scanf("%d", &input);
  19. if (input == 0)
  20. {
  21. printf("退出计算器\n");
  22. break;
  23. }
  24. if (input >= 1 && input <= 2)
  25. {
  26. int x = 0;
  27. int y = 0;
  28. printf("请输入两个操作数:>");
  29. scanf("%d%d", &x, &y);
  30. //通过调用函数指针数组使用对应的函数
  31. int ret = Farr[input](x, y);
  32. printf("%d\n", ret);
  33. }
  34. else
  35. {
  36. printf("输入有误,请重新输入\n");
  37. }
  38. } while (input);
  39. return 0;
  40. }

         使用函数指针数组后,我们可以避免switch语句的使用,是代码看起来更整洁。

十.回调函数 

        回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

 观察以下代码,学习如何使用回调函数;

  1. #include <stdio.h>
  2. int Add(int x, int y)
  3. {
  4. return x + y;
  5. }
  6. int Sub(int x, int y)
  7. {
  8. return x - y;
  9. }
  10. void calc(int (*p)(int, int))
  11. {
  12. int x = 0, y = 0;
  13. printf("请输入两个操作数:>");
  14. scanf("%d%d", &x, &y);
  15. int ret = p(x, y);
  16. printf("%d\n", ret);
  17. }
  18. int main()
  19. {
  20. int input = 0;
  21. do
  22. {
  23. printf("请选择算法(1.Add 2.Sub 0.exit):>");
  24. scanf("%d", &input);
  25. switch (input)
  26. {
  27. case 1:
  28. calc(Add);
  29. break;
  30. case 2:
  31. calc(Sub);
  32. break;
  33. case 0:
  34. printf("退出计算器\n");
  35. break;
  36. default:
  37. printf("输入有误,请重新输入!\n");
  38. break;
  39. }
  40. } while (input);
  41. return 0;
  42. }

        在每个switch语句中,我们都使用了calc函数,并传递我们需要进行的运算函数的地址,我们在calc函数中又调用了传递过来的参数,这个被调用的函数便被称为回调函数。

        在库函数中,有一个排序函数,我们需要自己写排序的方式的函数,然后将排序方法给这个给排序函数作为参数,本质上也是一种回调函数,本文就不做详细介绍,感兴趣可以翻阅官网,查阅函数使用方法(cplusplus.com - The C++ Resources Network).

        关于指针本文就介绍到这里,后期会更新相关配套练习,大家可以通过练习提升对指针的了解,最后感谢大家的支持,看到这给个免费的关注呗,十分感谢。

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

闽ICP备14008679号