当前位置:   article > 正文

【C语言】深入理解指针(一篇让你完全搞懂指针)_深入理解c语言 深入理解指针

深入理解c语言 深入理解指针

一、什么是指针?

1.1指针概念

本质上指针就是地址,口语中所说的指针,其实就是指针变量指针变量是用来存放地址的一个指针。

 我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是内存中读取的,处理后的数据也会放回内存中。

电脑上内存是8CB/16GB/32GB等,这些内存空间如何高效的管理?

其实就是把内存划分成一个一个的内存单元,每个内存单元的大小取1字节,每个内存单元都有一个编号

有了内存单元的编号,CPU就可以快速找到一个内存空间。

内存单元的编号 == 地址 == 指针

二、指针变量和地址

2.1 取地址操作符(&)

在C语言中,创建变量其实就是向内存申请空间

 上述代码创建整型变量a,向内存申请了4个字节,用于存放整数10,其中每个字节都有地址,上图中4个字节地址分别是:

0x00F5FCEC 
0x00F5FCED 
0x00F5FCEE 
0x00F5FCEF

 那我们如何得到a的地址?   

这里就用到取地址操作符(&)

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

 上述代码,运行之后,打印:0x00F5FCEC 

&a取出来的是a所占4个字节中地址最小的字节的地址。

 2.2 指针变量和解引用操作符(*)

2.2.1 指针变量

我们通过取地址操作符(&)拿到的地址是一个数组,比如:0x00F5FCEC,这个数组有时候需要存储起来,方便后期使用,那我们就可以把地址值存放在指针变量

 总结:变量a和指针变量pa都有各自的地址,只是把变量a的地址存放在指针变量pa里。

 指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址

 

2.2.2 解引用操作符(*)

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int a = 10;
  5. int* pa = &a;//取出a地址,并存放在指针变量中
  6. //①int说明pa指向的对象是int类型 ②*说明pa是指针变量
  7. *pa=100;
  8. printf("%d",*pa);
  9. return 0;
  10. }

 输出:100

①*的作用是引用指针指向的变量值,引用其实就是引用该变量的地址“解”就是把该地址对应的东西解开,解出来,就像打开一个包裹一样,那就是该变量的值了,所以称为“解引用”。 也就是说,解引用是返回内存地址中对应的对象

解引用也可以改变该变量的数值。

②需要注意的是,在变量声明的时候,*不能当做解引用使用,只是表示你声明的变量是一个指针类型。

 

2.3 指针变量的大小

指针变量的大小取决于地址的大小

①32平台下地址是32bit位,指针变量大小是4字节

②64平台下地址是64bit位,指针变量大小是8字节

注意:指针变量的大小和类型无关,只要指针类型的变量,在相同平台下,大小都是相同的。

 

三、指针类型变量的意义

3.1 指针解引用

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

任务:上面两段代码,调试观察内存的变化,对比发现了什么?

发现:第一段代码会将n的4个字节全部改为0;但是第二段代码只是将n的第一个字节改为0。

结论:指针类型决定了对指针解引用的时候有多大权限(一次可以操作几个字节)。

比如:char*的指针解引用只能访问一个字节,而int*的指针解引用就可以访问4个字节。

 3.2 指针 +- 整数

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

 运行结果:

 结论:指针类型决定了指针向前或者向后走一步有多大(距离)。

 3.3 void* 指针

void* 类型,无具体类型的指针(泛型指针),void* 类型的指针大部分使用在函数参数的部分,用来接收不同类型数据的地址。

但是void* 类型的指针不能直接进行指针的+-整数和解引用运算。

 

四、const修饰指针

在C语言中,const是constant的缩写,翻译是“恒定不变的”。它是定义只读变量的关键字。或者说const是定义常变量的关键字

const修饰的变量被称为常变量,具有常属性,但是本质上还是变量

保护被修饰的东西,防止被意外修改,增强程序的健壮性

提高程序的运行效率。编译器通常不为普通const常量分配存储空间,而是将他们保护在符号表中,使得它成为一个编译期间的常量,没有存储和读取内存的操作,使得它的运行效率也很高。

 4.1 const int* p (int const *p)和int *const p的区别

  1. #include <stdio.h>
  2. void test1()
  3. {
  4. int n = 10;
  5. int m = 10;
  6. /* const放在*左边,修饰的是*p,表示的是指针指向的内容,不能通过指针来改变,
  7. 但是指针变量本身是可以被修改的 */
  8. const int* p = &n;//const放在*的左边
  9. *p = 20;//err *p不可改变(指针指向的内容不能被修改)
  10. p = &m; //ok 指针变量p可以修改
  11. }
  12. void test1()
  13. {
  14. int n = 10;
  15. int m = 10;
  16. /* const放在*右边,修饰的是指针变量p本身,表示的是指针变量不可以被修改,
  17. 但是指针指向的内容是可以被修改的 */
  18. int* const p = &n;//const放在*的右边
  19. *p = 20;//ok //指针指向的内容可以被修改
  20. p = &m; //err p不可改变(指针变量p不能被修改)
  21. }
  22. int main()
  23. {
  24. test1();//const放在*的右边
  25. test2();//const放在*的左边
  26. return 0;
  27. }

 总结:

①const int* p (int const *p):const放在*的左边。修饰的是*p,*p(指针指向的内容)不能修改;p(指针变量本身)可以修改

②int * const p :const放在*的右边。修饰的是p,p(指针变量本身)不可以修改;*p(指针指向的内容)可以修改。

③int const * const p (const int * const p):*的左右两边都有const,修饰的是*p和p,所以*p和p都不能被修改

 

五、指针运算 

5.1 指针 +- 整数

数组在内存中是连续存放的,只要知道第一个元素的地址,顺藤摸瓜就可以找到后面所有元素。

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
  5. int* p = arr[0];
  6. int sz = sizeof(arr) / sizeof(arr[0]);
  7. int i = 0;
  8. for (i = 0; i < sz; i++)
  9. {
  10. printf("%d ", *(p + i)); //p+i 这就是指针的+-整数 *(p+i)相当于arr[i]
  11. }
  12. return 0;
  13. }

 指针p+i,表示指针向后移动i个单位(每个单位为指针p的类型所占的字节数),指向原先指向的元素后的第i个元素。若p指向arr[0],则p+i指向arr[i];

5.2 指针 - 指针

  1. #include <stdio.h>
  2. int my_strlen(char* s)
  3. {
  4. //指针-指针
  5. char* p = s;
  6. while (*p != '\0')
  7. p++;
  8. return p - s; //返回结束指针地址-开始指针地址 的差值 得到的就是指针之间元素的个数
  9. }
  10. int main()
  11. {
  12. printf("%d\n", my_strlen("abc"));
  13. return 0;
  14. }

 两个指针相减,得到的就是指针之间的元素个数。指针s是字符串首地址,第一次p=s,随着p++,p指向最后一个字符后面的位置。所以p-s,就得到字符串的个数。

5.3 指针的关系运算

  1. int main()
  2. {
  3. int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
  4. int* p = &arr[0];
  5. int sz = sizeof(arr) / sizeof(arr[0]);
  6. int i = 0;
  7. while (p < arr + sz) //指针的大小比较
  8. {
  9. printf("%d ", *p);
  10. p++;
  11. }
  12. return 0;
  13. }

 指针的比较,依赖于指针所指向的两个元素的相对位置,若指针p指向arr[i],指针q指向arr[j],p和q的结果由i和j的大小决定。

5.4 指针运算笔试题

C语言的间接寻址运算符*常和++--运算符一起使用,具有以下四种不同的形式:

表达式含义
*p++   或   *(p++)首先计算表达式的值为*p,然后p自增1
(*p)++首先计算表达式的值为*p,然后*p自增1
*++p  或  *(++p)首先p自增1,然后计算*p
++*p  或  ++(*p)首先*p自增1,然后计算表达式的值为(*p)
  1. #include <stdio.h>
  2. #define N 10
  3. int main()
  4. {
  5. int a[10] = {0,1,2,3,4,5,6,7,8,9};
  6. int temp = 0;
  7. int *p = a;
  8. printf("p的初始值为%p, *p值为%d\n", p, *p);
  9. temp = *p++;
  10. printf("*p++的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);
  11. printf("p的初始值为%p, *p值为%d\n", p, *p);
  12. temp = *(p++);
  13. printf("*(p++)的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);
  14. printf("p的初始值为%p, *p值为%d\n", p, *p);
  15. temp = (*p)++;
  16. printf("(*p)++的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);
  17. printf("p的初始值为%p, *p值为%d\n", p, *p);
  18. temp = *++p;
  19. printf("*++p的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);
  20. printf("p的初始值为%p, *p值为%d\n", p, *p);
  21. temp = *(++p);
  22. printf("*(++p)的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);
  23. printf("p的初始值为%p, *p值为%d\n", p, *p);
  24. temp = ++*p;
  25. printf("++*p的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);
  26. printf("p的初始值为%p, *p值为%d\n", p, *p);
  27. temp = ++(*p);
  28. printf("++(*p)的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);
  29. return 0;
  30. }

运行结果示例:

p的初始值为005AFE5C, *p值为0
*p++的值为0, 运算后p的值为005AFE60, *p的值为1

p的初始值为005AFE60, *p值为1
*(p++)的值为1, 运算后p的值为005AFE64, *p的值为2

p的初始值为005AFE64, *p值为2
(*p)++的值为2, 运算后p的值为005AFE64, *p的值为3

p的初始值为005AFE64, *p值为3
*++p的值为3, 运算后p的值为005AFE68, *p的值为3

p的初始值为005AFE68, *p值为3
*(++p)的值为4, 运算后p的值为005AFE6C, *p的值为4

p的初始值为005AFE6C, *p值为4
++*p的值为5, 运算后p的值为005AFE6C, *p的值为5

p的初始值为005AFE6C, *p值为5
++(*p)的值为6, 运算后p的值为005AFE6C, *p的值为6

六、野指针

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

 6.1 野指针的成因

① 指针未初始化

② 指针越界访问

③ 指针指向的空间释放

 6.2 如何规避野指针

① 指针初始化

② 小心指针越界

③ 指针变量不再使用,及时置NULL,指针使用之前检查有效性

④ 避免返回局部变量的地址

七、指针的使用和传址调用

7.1 strlen的模拟实现

库函数strlen的功能是求字符串长度,统计的是字符串\0之前的字符个数。

库函数原型:

size_t strlen ( const char* str );

 模拟实现,从起始地址开始向后逐个字符的遍历,只要不是\0字符,计算器就+1,直到\0就停止。

  1. int my_strlen(const char* s)
  2. {
  3. int count = 0;
  4. while (*s != '\0')
  5. {
  6. count++;
  7. s++;
  8. }
  9. return count++;
  10. }
  11. int main()
  12. {
  13. int len = my_strlen("abcdef");
  14. printf("%d\n", len);
  15. return 0;
  16. }

7.2 传值调用和传址调用

任务:写一个函数,交换两个变量的值

  1. #include <stdio.h>
  2. void Swap(int x, int y)
  3. {
  4. int tmp = x;
  5. x = y;
  6. y = tmp;
  7. }
  8. int main()
  9. {
  10. int a = 10;
  11. int b = 20;
  12. printf("交换之前a = %d b = %d\n", a, b);
  13. Swap(a, b);
  14. printf("交换之后a = %d b = %d\n", a, b);
  15. }

 运行结果,如下:

结论:实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参修改不影响实参。

所以Swap()失败了。

 答案:使用指针,在main函数中将a和b的地址传递给Swap函数,Swap函数里通过地址间接的操作main函数中的a和b,并达到交换的效果。

  1. #include <stdio.h>
  2. void Swap(int *x, int *y)
  3. {
  4. int tmp = 0;
  5. tmp = *x;
  6. *x = *y;
  7. *y = tmp;
  8. }
  9. int main()
  10. {
  11. int a = 10;
  12. int b = 20;
  13. printf("交换之前a = %d b = %d\n", a, b);
  14. Swap(&a, &b);
  15. printf("交换之后a = %d b = %d\n", a, b);
  16. }

运行结果,如下:

 将变量的地址传给函数,这种函数调用方式叫:传址调用

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

闽ICP备14008679号