竹影扫阶尘不动,月轮穿沼水无痕 ——深入理解指针

>>解释指针变量的大小

指针是个变量,存放内存单元的地址(编号),一个指针变量的大小可以这样理解
在这里插入图片描述

这里有2的32次方个地址
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所
以一个指针变量的大小就应该是4个字节。
那如果在64位机器上,如果有64根地址线,那一个指针变量的大小是8个字节

>>野指针问题

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。

成因:
指针变量未初始化
指针操作超越变量作用域
指针指向的空间释放后,指针未置空,这里涉及到动态内存开辟

避免野指针问题的方法:
指针初始化
小心越界访问
指针指向的空间释放后,指针置空
使用指针前检查有效性

>>指针的关系运算

指针可以比较大小吗?
标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许
指向第一个元素之前的那个内存位置的指针进行比较。

>>指针数组和数组指针

指针数组是一个存放指针的数组

int* arr3[5];
arr3是一个数组,有五个元素,每个元素是一个整形指针

数组指针是指向一个数组的指针

int (*p)[10];
p先和*结合,说明p是一个指针变量,指向的是一个大小为10个整型的数组
p是一个指针,指向一个数组,叫数组指针
[ ]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

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

以上代码是数组还是指针? 是数组
paar先和[ ]结合,是存放十个元素的数组,数组存放什么类型的元素?
定义一个数组,去掉数组名就是它存放的元素类型,如 int a[9],去掉a[9],剩下int
上例去掉parr[10],剩下 int (*) [5],代表的类型是数组指针。综上这是一个存放数组指针的数组

>>数组指针使用详解

数组指针指向整个数组,数组名可以表示数组首元素地址,整个数组的地址可以用&数组名表示

int arr[10] = { 0 };
&arr和arr,虽然值是一样的,但是意义不一样
实际上 &arr 表示的是数组的地址,而不是数组首元素的地址
数组的地址+1,跳过整个数组的大小

举一个可以用数组指针作为函数参数的例子:
打印一个二维数组:

void print_arr(int (*arr)[5], int row, int col) {
    int i = 0;
    for(i=0; i<row; i++)
   {
        for(j=0; j<col; j++)
       {
            printf("%d ", arr[i][j]);
       }
        printf("\n");
   }
}
int main()
{
    int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
    print_arr(arr, 3, 5);
    return 0; 
}

数组名arr,表示首元素的地址
但是二维数组的首元素是二维数组的第一行
所以这里传递的arr,其实相当于第一行的地址是一维数组的地址
可以数组指针来接收

>>数组传参和指针传参

我们经常会把数组和指针传参给函数,函数的参数该如何设计?

① 一维数组传参

int main()
{
 int arr[10] = {0};
 int *arr2[20] = {0};
 test1(arr);
 test2(arr2);
}

tset1 接收的是一维数组首元素的地址,参数可以设计成这样
void test(int arr[10]) //10可有可无
void test(int *arr)

test2 接收的是指针数组首元素的地址,是一个指针的地址(二级指针),参数可以设计成这样
void test2(int *arr[20]) //20可有可无
void test2(int **arr)

② 二维数组传参

int main()
{
  int arr[3][5] = {0};
  test(arr);
}

test 接收的是二维数组首元素的地址,前面提到二维数组首元素的地址是二维数组第一行的地址
所以参数可以设计成这样
void test(int arr[3][5]) //3可以省略,而列数5不可以省略
void test(int (*arr)[5]) //指向二维数组第一行的数组指针

③ 一级指针传参

十分常用,举一个简单的例子:

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

④ 二级指针传参

#include <stdio.h>
void test(int** ptr) 
{
  printf("num = %d\n", **ptr); 
}
int main()
{
  int n = 10;
  int*p = &n;
  int **pp = &p;
  test(pp);
  test(&p);
  return 0; 
 }

二级指针就是指向一级指针的指针(一级指针的地址),所以参数传pp 和&p都可以

>>函数指针

① 浅识函数指针

顾名思义,函数指针是指向一个函数的指针,保存的是该函数的地址
举例说明:

void test()
{
   printf("hehe\n");
}

test函数的地址,可以用&test表示,也可以用test表示,二者意义相同
printf("%p\n", test);
printf("%p\n", &test);
这两行代码都可以打印出test函数的地址

函数指针的表示方法:

int Add(int x,int y)
{
  return x+y;
}

指向Add函数的指针这样表示:

int (*p)(int,int)=Add;
int (*p)(int,int)=&Add;

用函数指针调用Add函数:

p(3,5);
(*p)(3,5);

以上都是传参数(3,5)给Add函数,但*没有实际意义,加上便于理解

② 深入理解函数指针

《C陷阱与缺陷》中有两个很有趣的关于函数指针的例子
在这里插入图片描述

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

代码1的作用是调用0地址处的函数
在这里插入图片描述

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

代码2的作用是一次函数声明
在这里插入图片描述我们知道,去掉函数名和参数类型即为函数返回值类型
在这里插入图片描述代码2出现两次void (*)(int)类型,可以用 typedef 改写一下增加可读性,这里要注意新名称要跟在*后

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

如果读者能够快速辨识有关函数指针的代码,相信对函数指针已经有一定理解了。

③ 函数指针数组

定义一个函数指针数组

int (*parr1[10])();

数组名 parr1[10]
去掉数组名就是数组元素的类型:int (*)()

函数指针数组应用实例:
写一个简易计算器,把实现各个运算功能的函数的地址存放在一个函数指针数组中,通过访问数组元素调用各个函数,代码如下

#include <stdio.h>
int add(int a, int b) 
{
     return a + b;
}
int sub(int a, int b) 
{
     return a - b; 
}
int mul(int a, int b) 
{
     return a*b; 
}
int div(int a, int b) 
{
      return a / b; 
}
int main()
{
     int x, y;
     int input = 1;
     int ret = 0;
     int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
     while (input)
     {
          printf( "*************************\n" );
          printf( " 1:add           2:sub \n" );
          printf( " 3:mul           4:div \n" );
          printf( "*************************\n" );
          printf( "请选择:" );
          scanf( "%d", &input);
          if ((input <= 4 && input >= 1))
          {
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = (*p[input])(x, y);
          }
          else
              printf( "输入有误\n" );
              printf( "ret = %d\n", ret);
     }
      return 0; 
}

④ 指向函数指针数组的指针😂

其实并不复杂,本质上是数组指针,类型为函数指针

 //函数指针pfun
 void (*pfun)(const char*) = test;
 //函数指针的数组pfunArr
 void (*pfunArr[5])(const char* str);
 //指向函数指针数组pfunArr的指针ppfunArr
 void (*(*ppfunArr)[5])(const char*) = &pfunArr;

>>回调函数

通过函数指针调用的函数 ,把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数
qsort就应用了回调函数,qsort是C语言编译器函数库自带的排序函数,原型如下
在这里插入图片描述
各个参数的意义:
在这里插入图片描述在这里插入图片描述
下面代码使用qsort函数对数组 { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 }排序,比较函数int_cmp是自己实现的,把int_cmp函数的地址(指针)传给了qsort函数,在需要时通过指针调用其指向的int_cmp函数,这就是回调函数的一个鲜明例子

#include <stdio.h>
//qosrt函数的使用者需要实现一个比较函数
int int_cmp(const void * p1, const void * p2) 
{
    return (*( int *)p1 - *(int *) p2);
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;
    
    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
    
    return 0; 
}

>>指针相关经典题目

牢记一点:指针+1,偏移的字节数就是指针指向类型的字节数

int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1);
    printf( "%d,%d", *(a + 1), *(ptr - 1));
    return 0; 
}
//程序的结果是什么?

运行结果:
在这里插入图片描述
解析:
在这里插入图片描述

struct Test
{
   int Num;
   char *pcName;
   short sDate;
   char cha[2];
   short sBa[4];
}*p;
//告知结构体的大小是20个字节假设p 的值为0x100000。 如下表表达式的值分别为多少?
int main()
{
   printf("%p\n", p + 0x1);
   printf("%p\n", (unsigned long)p + 0x1);
   printf("%p\n", (unsigned int*)p + 0x1);
   return 0; 
}

运行结果:
在这里插入图片描述
解析:
p是结构体指针,+1时偏移量就是这个结构体的大小20
转换成unsigned long类型就是一个整型数据,+1就是数值+1
转换成unsigned int* 类型就是一个整型指针,+1偏移量就是4
按题中要求的16进制计算即可

int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int *ptr1 = (int *)(&a + 1);
    int *ptr2 = (int *)((int)a + 1);
    printf( "%x,%x", ptr1[-1], *ptr2);
    return 0; 
}

运行结果:
在这里插入图片描述
解析:
数组元素 1 2 3 4按小端模式存储(什么是大小端)在内存中如下图:

在这里插入图片描述

#include <stdio.h>
int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    int *p;
    p = a[0];
    printf( "%d", p[0]);
    return 0; 
}

运行结果:
在这里插入图片描述
解析:
你没有看错,答案是1
int a[3][2] = { (0, 1), (2, 3), (4, 5) };注意这里是圆括号
括号表达式(0,1)的值为1,所以相当于int a[3][2] = { 1,3,5 };
p = a[0]; 表示的是p指向数组首行
在这里插入图片描述
p[0]显然是1

int main()
{
    int a[5][5];
    int(*p)[4];
    p = a;
    printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
    return 0; 
}

运行结果
在这里插入图片描述
解析:
在这里插入图片描述指针p是整型数组指针,+1的偏移量为4个整型
我们知道,指针相减得到的是元素个数,所以&p[4][2] - &a[4][2]的值为-4,在内存中以补码存储,当作地址打印时,被看作无符号数,即为FFFFFFFC

int main()
{
    int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    int *ptr1 = (int *)(&aa + 1);
    int *ptr2 = (int *)(*(aa + 1));
    printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
    return 0; 
}

运行结果:
在这里插入图片描述
解析:
在这里插入图片描述

int main()
{
	char *a[] = { "work", "at", "alibaba" };
	char**pa = a;
	pa++;
	printf("%s\n", *pa);
	return 0;
}

运行结果:
在这里插入图片描述
解析:数组元素都为char* 类型,首元素的地址要用二级指针接收
在这里插入图片描述

int main()
{
   char *c[] = {"ENTER","NEW","POINT","FIRST"};
   char**cp[] = {c+3,c+2,c+1,c};
   char***cpp = cp;
   printf("%s\n", **++cpp);
   printf("%s\n", *--*++cpp+3);
   printf("%s\n", *cpp[-2]+3);
   printf("%s\n", cpp[-1][-1]+1);
   return 0; 
}

运行结果:
在这里插入图片描述
解析:
用图表示三级指针的指向关系:
在这里插入图片描述

printf("%s\n", **++cpp);

++cpp ------------ cpp指向c+2
*++cpp------------c+2
**++cpp-----------*(c+2)----------------&POINT

此时cpp指向是这样的
在这里插入图片描述再看这行代码

   printf("%s\n", *--*++cpp+3);

++cpp--------------cpp指向c+1
++cpp-------------c+1
++cpp-----------c
++cpp----------*c-----------&ENTER
++cpp+3------------&ER

此时cpp指向c+1
在这里插入图片描述再看这行代码

   printf("%s\n", *cpp[-2]+3);

cpp[-2]相当于*(cpp-2)-----------c+3
cpp[-2]-----------------------------(c+3) ----------------------&FIRST
*cpp[-2]+3----------------&ST

此时cpp仍然指向c+1
在这里插入图片描述再看这行代码

       printf("%s\n", cpp[-1][-1]+1);

cpp[-1]相当于*(cpp-1)--------c+2
cpp[-1][-1]相当于*(c+2-1)--------&NEW
cpp[-1][-1]+1相当于*(c+1)+1-----&EW

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神奇dyl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值