指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解

指针的进阶

文章目录

  • 指针的进阶
    • 字符指针
    • 指针数组
    • 数组指针
      • &数组名与数组名
      • 数组指针的使用
        • 利用数组指针访问一维数组
        • 传统的方式访问二维数组
        • 利用数组指针来访问二维数组
    • 数组参数、指针参数
      • 一维数组传参
      • 二维数组传参
      • 一级指针传参
      • 二级指针传参
    • 函数指针—指向函数的指针
      • 函数指针
      • 函数调用
      • C陷阱和缺陷中的两个代码
      • 函数指针数组
    • 实现一个计算器
      • 普通实现
      • 函数指针数组实现
    • 指向函数指针数组的指针
    • 回调函数
      • qsort函数
        • qsort函数对int型数组排序
        • qsort对结构体排序
        • 模拟实现qsort函数
    • 指针和数组笔试题
      • 一维数组的sizeof和&的那些事
      • 字符数组与sizeof、&、strlen的那些事
        • 当字符串初始化数组时的那些事
      • 字符指针的sizeof、&、strlen的那些事
      • 二维数组的sizeof、&、strlen的那些事
      • 超硬核指针笔试题,良心画图+文字详细讲解,你值得学会

指针初阶的基础知识:

1、指针就是地址,地址就是指针,指针存在变量里叫指针变

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

3、指针类型的意义:决定指针±整数的步长,指针解引用访问的字节

4、指针的运算

本人初阶指针的总结:C语言指针初阶

字符指针

int main()
{
     
   // char ch='q';
   // char *pc=&ch;
    //一个指针是可以指向一个字符串的,这个字符串是常量字符串
    char *ps="hello world";
    //ps本质上把字符串首字符'h'的地址存进去了
    char arr[]="hello world";
    //和ps不相同,arr是数组,ps是变量,arr是吧hello world都存进数组里,而ps只存了首字符的地址
    printf("%c\n",*ps);//h
    //两种定义的名字都能打印字符串,都是首字符地址
    printf("%s\n",ps);
    printf("%s\n",arr);
    return 0;
}

注意:

ps本质上把字符串首字符’h’的地址存进去了

arr和ps不相同,arr是数组,ps是变量,arr是把hello world所有字符都存进数组里,而ps只存了首字符的地址

我们来看一道笔试题

#include 
int main()
{
     
  char str1[] = "hello bit.";
  char str2[] = "hello bit.";
  char *str3 = "hello bit.";
  char *str4 = "hello bit.";
  if(str1 ==str2)
		printf("str1 and str2 are same\n");
  else
		printf("str1 and str2 are not same\n");
  
  if(str3 ==str4)
		printf("str3 and str4 are same\n");
  else
		printf("str3 and str4 are not same\n");
  
  return 0;
}

这个代码结果会打印什么呢?

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第1张图片

解读:

数组名是首元素的地址,数组str1和str2创建的空间不同,首元素地址就不相同,所以str1和str2肯定不相等

str3和str4是指针变量,str3和str4存放的是hello world字符串的首字符的地址,hello world这个字符串叫做常量字符串 ,是不能修改的,我们看一下我们如果要改的话的测试结果

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第2张图片

这里代码直接挂掉了,我们进行调试后发现会进行报错:

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第3张图片

所以常量字符串是不能修改的

str3和str4中的常量字符串是不能被修改的,两份相同的内容,而且不能被修改,是没有必要存两份的,所以这样的str3和str4在内存中只会存一份,所以常量字符串hello world只有一份,把h的地址存在str3中,把h的地址也存在str4中,str3中存的地址和str4中存的地址一样,所以str3等于str4。

下面我们来看另外一个知识点:

指针数组

存放指针的数组,它本质上是个数组,数组中存放的是地址(指针)。

int main()
{
     
    //int *arr[3];//arr是存放3个整形指针的数组
    int a=10;
    int b=20;
    int c=30;
    int *arr[3]={
     &a,&b,&c};
    int i=0;
    for(i=0;i<3;i++)
    {
     
        printf("%d ",*(arr[i]);
    }
    return 0;
}

这样的写法可行,是没有什么错误的,但是不太好,没什么应用场景,我们也不经常这样使用指针数组,我们来看下面这种写法:

int main()
{
     
	int a[5]={
     1,2,3,4,5};
	int b[5]={
     2,3,4,5,6};
	int c[5]={
     3,4,5,6,7};

	int *arr[3]={
     a,b,c};
    int i=0;
    for(i=0;i<3;i++)
    {
     
        int j=0;
        for(j=0;j<5;j++)
        {
     
            printf("%d ",*(arr[i]+j));
            printf("%d ", arr[i][j]);
            //arr[i]==*(arr+i)  
            //arr[i][j]==*(arr[i]+j)
            //[j]==*(+j)
        }
        printf("\n");
    }
    return 0;
}

这个写法将a,b,c三个数组名放在了指针数组中,而数组名是首元素地址,拿到了首元素地址,我们就可以访问数组当中的元素,而这种写法是我们经常使用的。

下面我们试着来辨别以下声明是什么:

int *arr1[10];

arr先和[]结合,说明它是个数组,然后去掉arr1[10]发现数组的每个元素为int*类型。所以它是存放整形指针的数组

char *arr2[4];

存放一级字符指针的数组,arr先和[]结合,说明它是个数组,然后去掉arr2[4]发现数组的每个元素为char*类型。

char **arr3[10];

存放二级字符指针的数组,arr先和[]结合,说明它是个数组,然后去掉arr3[10]发现数组的每个元素为char**类型。

指针数组就讲到这里,下面我们来看数组指针。

数组指针

数组指针是数组还是指针呢?答案是指针。

整形指针是指向整形的指针

int a = 10;
int* pa = &a;

字符指针是指向字符的指针

char ch = 'w';
char* pc = &ch;

顾名思义,数组指针就是指向数组的指针

接下来我们看看是如何定义数组指针的呢?我们来看下面的代码:

int main()
{
     
    double* d[5];//指针数组
    double* (*pd)[5]= &d;//pd就是一个数组指针
    int arr[10] = {
      1,2,3,4,5 };
    int (*parr)[10] = &arr;//取出的是数组的地址
     //parr 就是一个数组指针 - 其实存放的是数组的地址
    //arr;//arr-数组名是首元素的地址- arr[0]的地址
    return 0;
}

我们在定义数组指针时,需要注意[]的优先级比*高,所以我们需要使用()强制先于*结合。

&数组名与数组名

我们首先看以下代码:

int main()
{
     
    int arr[10]={
     0};
    printf("%p\n",arr);
    printf("%p\n",&arr);   
    return 0;
}

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第4张图片

打印上面代码后发现他们两个值一样,但是这两个的意义是不一样的,&arr是整个数组的地址,arr是数组首元素地址

那么&arr和arr区别怎么体现呢?以下代码就体现了他们的区别:

int main()
{
     
    int arr[10]={
     0};
    int *p1=arr;
    int (*p2)[10] = &arr;
    printf("%p\n",arr);
    printf("%p\n",&arr);   
    return 0;
}

我们用指针p1,p2存放这两个地址时,int *p1=arr;int (*p2)[10];

p1是个整形指针就可以了,而p2要是个数组指针,因为他存放的是&arr,是整个数组的地址。

既然他们的类型不一样,加减整数的步幅就不一样了

int main()
{
     
    int arr[10]={
     0};
    int *p1=arr;
    int (*p2)[10] = &arr;
    printf("%p\n",p1);
    printf("%p\n",p1+1);
    
    printf("%p\n",p2);
    printf("%p\n",p2+1);   
    return 0;
}

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第5张图片

我们能够发现p1加1的步幅是4个字节,p2加1的步幅却是40个字节

说明数组名和&数组名还是有差别的。

我们经常说数组名是数组首元素的地址

但是有两个例外:

1.sizeof(数组名) - 数组名表示整个数组,计算的是整个数组的大小

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

数组指针的使用

利用数组指针访问一维数组

int main()
{
     
    int arr[10]={
     1,2,3,4,5,6,7,8,9,10};
    int *p=arr;//这个方便
    int (*pa)[10]=&arr;//不方便
    int i=0;
    for(i=0;i<10;i++)
    {
     
        *((*pa)+i);//*pa拿到arr
    }
    
    return 0;
}

我们也可以通过数组指针来访问一维数组的指针,但是不方便,将问题复杂化了。我们完全可以利用整形指针和[]下标引用操作符来访问,所以我们一般一维数组不用数组指针,数组指针的使用一般用于二维指针,接下来我们看一下二维数组是如何访问的。

传统的方式访问二维数组

void print1(int arr[3][5],int r,int c)
{
     
    int i=0;
    int j=0;
    for(i=0;i<r;i++)
    {
     
        for(j=0;j<c;j++)
        {
     
            printf("%d ",arr[i][j]);
        }
        printf("\n");
    }
}
int main()
{
     
    int arr[3][5]={
     {
     1,2,3,4,5},{
     2,3,4,5,6},{
     3,4,5,6,7}};
    print1(arr,3,5);
    print2(arr,3,5);//arr数组名,表示数组首元素的地址
    return 0;
}

利用数组指针来访问二维数组

//p是一个数组指针---指向一维数组的指针
void print2(int(*p)[5],int r,int c)
{
     
    int i=0;
    int j=0;
    for(i=0;i<r;i++)
    {
     
        for(j=0;j<c;j++)
        {
     
            printf("%d ",*(*(p+i)+j));
        }
        printf("\n");
    }
}
int main()
{
     
    int arr[3][5]={
     {
     1,2,3,4,5},{
     2,3,4,5,6},{
     3,4,5,6,7}};
    print2(arr,3,5);//arr数组名,表示数组首元素的地址
    return 0;
}

我们需要注意:

二维数组的数组名表示首元素的地址

二维数组的首元素是:第一行!

这里我们将第一行的地址传过去了,需要用一个指向一维数组的指针来接收

printf("%d ",*(*(p+i)+j));

这句代码的解读:

p是一个指向一维数组的指针,他存的是整个数组的地址,p+i跳过的是整个数组,p+0指向二维数组第一行arr[0]的整个一维数组的地址&arr[0],p+1指向二维数组第二行arr[1]的整个一维数组的地址,p+2指向二维数组第三行arr[2]的整个一维数组的地址,对p+i解引用找到的是第i行的数组名,数组名就是首元素地址,*(p+i)+j拿到的是第i行第j个元素的地址,在对它解引用就拿到了元素。

下面我们来辨别几个声明:

int arr[5];//整形数组
int *parr1[10];//指针数组
int (*parr2)[10];//数组指针,数组的元素为int类型,10个元素
int (*parr3[10])[5];//指针数组,每个元素为数组指针

注释是答案,我们重点来看一下最后一个怎么辨别:

image-20210524174018183

parr3首先和[]结合,说明它是个数组,将圈起来红色部分去掉,剩下的就是数组元素的类型,例如int arr[10];去掉arr[10],剩下的int就是数组元素类型

所以首先parr3是有10个元素的数组,数组的每一个元素又是数组指针。每个数组指针能够指向一个数组,数组5个元素,元素类型为int

数组参数、指针参数

一维数组传参

我们一维数组传参,函数参数类型可以写成什么呢?

int main()
{
     
    int arr[10]={
     0};
    int *arr2[20]={
     0};//arr2是存放整形指针的数组
    test(arr);
}

首先我们知道一维数组的数组名是首元素的地址,我们传过去的是首元素的地址,是arr[0]的地址,我们可以用哪些类型的参数来接收呢?

void test(int arr[10])
{
     }

传过来的是数组,当然可以用数组接收

void test(int arr[])//参数部分写成数组,数组元素可省略
{
     }

传过来的是数组,当然可以用数组接收,参数部分写成数组,数组元素可省略,操作系统会自行计算

void test(int *arr)//参数部分写成指针
{
     }

参数部分写成指针,数组名是首元素的地址,当然可以用一个指针来接收

我们再来看看指针数组传参,函数参数类型该如何写?

int main()
{
     
    int *arr2[20]={
     0};//arr2是存放整形指针的数组
    test2(arr2);//数组名表示首元素地址,而首元素又是int*,所以用int**接收
}

我们依次来看以下的传参方式可行不可行:

void test2(int *arr[20])//指针数组
{
     }

可以,我们传过来的是指针数组,所以可以直接用指针数组来接收

void test2(int **arr)//数组名表示首元素地址,而首元素又是指针,所以要用一个二维指针来接收
{
     }

可以,我们知道数组名表示首元素地址,而数组里面存放的是指针,首元素是一个指针,所以我们可以用一个二维指针来接收

二维数组传参

我们二维数组传参,函数参数类型可以写成什么呢?

int main()
{
     
    int arr[3][5]={
     0};
    test(arr);//传过去的是首元素的地址,首元素是arr[0],是第一行一维数组的地址&arr[0]
    return 0;
}

首先我们知道二维数组的数组名是首元素的地址,我们传过去的是首元素的地址,是第一行一维数组的地址&arr[0],我们可以用哪些类型的参数来接收呢?

我们来看以下的函数形参的类型是否可行:

void test(int arr[3][5])//可以,传过来的是二维数组,当然可以用二维数组接收
{
     }

可以,传过来的是二维数组,当然可以用二维数组接收

void test(int arr[][])//不行,不能省略列
{
     }

不行,二维数组不能省略列

void test(int arr[][5])//可以,可以省略行
{
     }

可以,二维数组可以省略行

void test(int *arr)//不可以,传过来的是一个一维数组的地址,要用数组指针来接收
{
     }

不可以,传过来的是一个一维数组的地址,要用数组指针来接收

void test(int* arr[5])//更不行,这是一个指针数组
{
     }

更不行,这是一个指针数组

void test(int (*arr)[5])//可以,指向一个5个元素的数组的指针
{
     }

可以,指向一个5个元素的数组的指针

一级指针传参

void print(int *ptr,int sz)//用一级指针接收
{
     
    int i=0;
    for(i=0;i<sz;i++)
    {
     
        printf("%d ",*(ptr+i));
    }
}
int main()
{
     
    int arr[10]={
     1,2,3,4,5,6,7,8,9,10};
    int *p=arr;
    int sz=sizeof(arr)/sizeof(arr[0]);
    print(p,sz);//p时一级指针,一级指针的传参
    return 0;
}

image-20210528190832199

print(p,sz);

在普遍情况下,我们传一个参数过去,函数形参类型的书写我们需要根据传过去的这个参数而定,我们要形参与实参类型相匹配,不管已经知道形参还是实参,我们都要明白可以将什么样的实参传给函数或者可以用什么样的实参来接收实参。

二级指针传参

将二级指针传参:

二级指针传参,当然可以用二级指针来接受,下面代码中test函数我们用二级指针形参来接收实参

void test(int **ppa)//用二级指针接收
{
     
    **ppa=20;
}
int main()
{
     
    int a=10;
    int *pa=&a;
    int **ppa=&pa;//pa是一级指针,ppa是二级指针
    //将二级指针进行传参
    test(ppa);
    printf("%d\n",a);
    return 0;
}

image-20210528191638948

那么,反过来,当函数形参为二级指针时,我们可以接收什么参数呢?如果函数这样设计,我们又可以传什么样的参数呢?

void test(int **ppa)//用二级指针接收
{
     
    **ppa=20;
}
int main()
{
     
    int a=10;
    int *pa=&a;
    int **ppa=&pa;//pa是一级指针,ppa是二级指针
    int *arr[10]={
     0};
    return 0;
}

1、

test(ppa);//直接传二级指针

我们可以直接传二级指针

2、

test(&pa);//传一级指针的地址

可以传一级指针的地址

3、

test(arr);//数组名是首元素地址,首元素为一级指针,所以相当于传的也是一级指针的地址,传存放一级指针的数组

arr是一个指针数组,存放指针的数组,数组名是首元素地址,相当于我们将arr[0]的地址传过去,arr[0]是一个指针,所以我们用二级指针传参。

总结一下我们已经了解介绍过的指针:

一级指针:

int *p;—整形指针-指向整形的指针

char *pc;—字符指针-指向字符的指针

void *pv;—无类型的指针

二级指针:

char **p;

int **p;

数组指针:指向数组的指针

int(*p)[5];

接下来我们继续介绍一个新的指针—函数指针。

函数指针—指向函数的指针

函数指针

在接触函数指针之前,我们先看看我们了解学习过的指针:

1、整形指针,存放整形地址的指针

    int a=10;
    int *pa=&a;

2、字符指针,存放字符地址的指针

    char ch='a';
    char *pc=&ch;

3、数组指针,存放数组地址的指针

    int arr[10]={
     0};
    int (*parr)[10]=&arr;//parr是指向数组的指针-存放数组的地址

接下来我们看一看函数函数指针,有了前面的铺垫,我们很容易知道,函数指针就是指向函数的指针,存放函数地址的指针。

我们首先看看函数是不是也应该有地址呢,又是怎么知道函数的地址呢?

int Add(int x,int y)
{
     
    return x+y;
}
int main()
{
     
    //函数指针 - 存放函数地址的指针
    printf("%p\n",&Add);
    printf("%p\n",Add);
    return 0;
}

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第6张图片

由上面代码可知道,&函数名和函数名拿到的就是函数的地址

我们知道&数组名和数组名打印的地址是一样的,但是意义是不一样的,由上面的代码及其运行结果,我们知道了&函数名和函数名打印的地址是一样的

我们特别要注意:

&数组名!=数组名 意义是不相同的

&函数名==函数名 完全相同

我们知道了函数也是有地址的,那么我们怎么定义一个函数指针呢?

	int (*pf)(int,int)=&Add;
	//pf就是一个函数指针

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第7张图片

我们遇到一个复杂的指针,不要慌!不要慌!不要慌!我们一步一步进行解析,我们来看一个简单的例子:

int *f();

我们要想推断出它的含义,我们必须确定表达式*f()是如何进行求值的。首先执行的是函数调用操作符(),因为它的优先级高于间接访问操作符。因此f首先是一个函数,它的返回类型为int *。

接下来再看一个声明:

int (*f)();

我们首先确定各个括号的含义,这一步是及其重要的,第一对括号起到的作用是聚组,它迫使函数调用操作符()的优先级低于间接访问操作符*,所以f首先是一个指针,然后我们看第二个括号,第二个括号是函数调用操作符,所以指针的指向是一个函数,函数的返回类型为int。

相信通过上面两个例子的讲解,聪明伶俐的大家应该明白了函数指针类型的书写啦!,我们接着上面的内容往下讲解

前面我们函数指针的初始化可以写成下面这样:

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

那么既然&函数名==函数名,那么我们还可以这样写

	int (*pf)(int,int)=Add;

将Add函数地址放入pf,此时Add等价于pf

那么我们如何可以调用函数呢?我们可以利用以下几种方式

函数调用

1、函数名调用

int Add(int x,int y)
{
     
    return x+y;
}
int main()
{
     
    int ret = Add(3,5);//8 1、通过函数名调用
    printf("%d\n",ret);
    return 0;
}

最简单的方法:我们可以直接通过—函数名(实参)的形式调用函数

2、通过函数指针调用

int Add(int x,int y)
{
     
    return x+y;
}
int main()
{
     
    //函数指针 - 存放函数地址的指针
	int (*pf)(int,int)=Add;//Add==pf
    int ret = (*pf)(3,5);//*pf找到Add函数,后面括号进行传参调用
    printf("%d\n",ret);
    return 0;
}

看一下函数指针是如何调用的:

int ret = (*pf)(3,5);

首先*pf先找到Add函数,后面括号然后再进行传参调用。

我们还可以怎么样调用呢?我们接着往下走

int (*pf)(int,int)=Add;//Add==pf

我们将Add函数地址放入pf,此时Add等价于pf,我们第一中调用方法通过函数名可以这样调用,int ret = Add(3,5);,那么既然Add等价于pf,我们可不可以将Add换成pf呢?答案是可以的

int Add(int x,int y)
{
     
    return x+y;
}
int main()
{
     
	int (*pf)(int,int)=Add;//Add==pf
    //int ret = Add(3,5);//8 通过函数名调用
    int ret = pf(3,5);//8 
    printf("%d\n",ret);
    return 0;
}

image-20210528194233072

妙不妙?妙呀!哈哈哈哈哈,我们可以正常调用函数。

那么我们得到了函数指针调用的两种方法:

 	int ret = pf(3,5);//8 
	int ret = (*pf)(3,5);//8

所以我们发现这两者是等价的。所以这里的间接访问操作符*是没有什么作用的,加不加都可以的。

接下来我们看两个有意思的代码

C陷阱和缺陷中的两个代码

1、(*(void (*)())0)();

int main()
{
     
    (*(void (*)())0)();
    //调用0地址处的函数
    //该函数无参,返回类型是void
    //1.void (*)() - 函数指针类型
    //2.(void (*)())0 - 对0进行强制类型转换,被解释成一个函数地址
    //3.*(void (*)())0) - 对0地址进行了解引用操作
    //4.(*(void (*)())0)() - 调用0地址处的函数
    return 0;
}

这个语句的作用是:调用0地址处的函数,该函数无参,返回类型是void

我们进行解析:

1.void (*)() - 函数指针类型
2.(void (*)())0 - 对0进行强制类型转换,被解释成一个函数地址
3.*(void (*)())0) - 对0地址进行了解引用操作
4.(*(void (*)())0)() - 调用0地址处的函数

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

int main()
{
     
    void(* signal(int,void(*)(int)))(int);
    //typedef - 对类型进行重定义
    typedef void(*pfun_t)(int);//对void(*)(int)的函数指针类型重命名
    pfun_t signal(int,pfun_t);
        //1.signal和()先结合,说明signal是函数名
        //2.signal函数的第一个参数的类型是int,第二个参数的类型是函数指针,该函数指针,指向一个参数为int,返回类型是void的函数
        //3.signal函数的返回类型也是一个函数指针
        //该函数指针,指向一个参数为int,返回类型是void的函数
        //signal是一个函数的声明
    return 0;
}

我们进行解析:

1.signal和()先结合,说明signal是函数名
2.signal函数的第一个参数的类型是int,第二个参数的类型是函数指针,该函数指针,指向一个参数为int,返回类型是void的函数
3.signal函数的返回类型也是一个函数指针,该函数指针,指向一个参数为int,返回类型是void的函数
signal是一个函数的声明

我们可以将signal函数的书写变简短一些,这里我们可以用到typedef—类型重定义

void(* signal(int,void(*)(int)))(int);
//typedef - 对类型进行重定义
typedef void(*pfun_t)(int);//对void(*)(int)的函数指针类型重命名
pfun_t signal(int,pfun_t);

相当于给void(*)(int)类型换了个名字—pfun_t

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

此时这两个声明就一样的意思了。

函数指针数组

我们看看之前讲解的整形指针数组—存放整形指针的数组

整形指针 int*

整形指针数组 int arr[5];*

那么函数指针数组顾名思义就为存放函数指针的数组

我们来看一看函数指针数组是如何定义的:

int Add(int x,int y)
{
     
    return x+y;
}
int Sub(int x,int y)
{
     
    return x-y;
}
int main()
{
     
    int (*pf1)(int,int)=Add;
    int (*pf2)(int,int)=Add;
    int (*pfArr[2])(int,int) = {
     Add,Sub};//函数指针数组
    return 0;
}

pfArr就是一个函数指针数组

int (*pfArr[2])(int,int) = {
     Add,Sub};

我们通过上面给大家讲解分析指针的方法来分析pfArr,首先先执行[],则pfArr首先是一个数组,数组元素是什么类型呢?我们将pfArr[2]去掉,剩下的就是数组元素类型,int(*)(int,int)就是我们pfArr[2]数组元素类型,元素类型首先它是一个指针,它指向参数为两个int的函数,函数的返回类型为int。

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第8张图片

知识小插曲:

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第9张图片

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第10张图片

经过了上面的学习,我们实现一个简单的计算器

实现一个计算器

普通实现

int Add(int x, int y)
{
     
    return x + y;
}
int Sub(int x, int y)
{
     
    return x - y;
}
int Mul(int x, int y)
{
     
    return x * y;
}
int Div(int x, int y)
{
     
    return x / y;
}
void menu()
{
     
    printf("***********************\n");
    printf("*****1.Add   2.Sub*****\n");
    printf("*****3.Mul   4.Div*****\n");
    printf("********0.exit*********\n");
    printf("***********************\n");

}
int main()
{
     
    int input = 0;
    int ret = 0;
    do {
     
        menu();
        
        int a = 0;
        int b = 0;
        printf("请选择:");
        scanf("%d", &input);

        switch (input)
        {
     
        case 1:
            printf("请输入两个操作数:");
            scanf("%d %d", &a, &b);
            ret = Add(3, 5);
            printf("%d\n", ret);
            break;
        case 2:
            printf("请输入两个操作数:");
            scanf("%d %d", &a, &b);
            ret = Sub(3, 5);
            printf("%d\n", ret);
            break;
        case 3:
            printf("请输入两个操作数:");
            scanf("%d %d", &a, &b);
            ret = Mul(3, 5);
            printf("%d\n", ret);
            break;
        case 4:
            printf("请输入两个操作数:");
            scanf("%d %d", &a, &b);
            ret = Div(3, 5);
            printf("%d\n", ret);
            break;
        default:
            printf("选择错误,重新选择\n");
            break;
        }
    } while (input);
    return 0;
}

函数指针数组实现

int Add(int x, int y)
{
     
    return x + y;
}
int Sub(int x, int y)
{
     
    return x - y;
}
int Mul(int x, int y)
{
     
    return x * y;
}
int Div(int x, int y)
{
     
    return x / y;
}
void menu()
{
     
    printf("***********************\n");
    printf("*****1.Add   2.Sub*****\n");
    printf("*****3.Mul   4.Div*****\n");
    printf("********0.exit*********\n");
    printf("***********************\n");

}
int main()
{
     
    int input = 0;
    do {
     
        menu();
        int (*pfArr[5])(int,int)={
     NULL,Add,Sub,Mul,Div};//第一个元素设置为NULL,以方便用户输入选择刚好对应我们的函数指针
        int ret = 0;
        int a = 0;
        int b = 0;
        printf("请选择:");
        scanf("%d", &input);
		if(input>=1&&input<=4)
        {
     
            printf("请输入两个操作数:");
            scanf("%d %d",&a,&b);
        	ret = (pfArr[input])(a,b);
      		printf("%d\n",ret);
        }
        else if(input==0)
        {
     
            printf("退出程序\n");
        }
        else
        {
     
            printf("输入错误,请重新输入\n");
        }  
    }while(input);
    return 0;
}

指向函数指针数组的指针

首先我们先讲解一下前面的知识,来类比学习指向函数指针数组的指针。

整形数组

int arr[5];

指向整形数组的指针

int (*p1)[5]=&arr;

&整形数组==整个数组的地址,用数组指针接收

整形指针的数组

int*arr[5];//指针数组

我们&arr就相当于是&整形指针数组,将整形指针数组的地址赋给一个指针变量,该指针变量的类型该怎么写呢?请看下面:

指向整形指针的数组的指针

int *(*p2)[5]=&arr;

p2是指向整形指针数组的指针

通过以上的铺垫,我们现在来看一看指向函数指针数组的指针:

函数指针数组

指向函数指针数组的指针我们需要传入&函数指针数组

int (*p)(int,int);//函数指针
int (*p2[4])(int,int);//函数指针的数组

&p2;//取出的是函数指针数组的地址

指向函数指针数组的指针

int (*(*p3)[4])(int,int)=&p2;

p3就是一个指向函数指针数组的指针

p3是首先是一个指针,指针指向的是一个数组,数组的元素是函数指针。

回调函数

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

将A函数的地址传给B函数,B在里面又通过函数指针形参去调用A函数

回调函数解决上面的计算器:

int Add(int x, int y)
{
     
    return x + y;
}
int Sub(int x, int y)
{
     
    return x - y;
}
int Mul(int x, int y)
{
     
    return x * y;
}
int Div(int x, int y)
{
     
    return x / y;
}
void menu()
{
     
    printf("***********************\n");
    printf("*****1.Add   2.Sub*****\n");
    printf("*****3.Mul   4.Div*****\n");
    printf("********0.exit*********\n");
    printf("***********************\n");

}
int Clc(int (*pf)(int,int))
{
       
    int a = 0;
    int b = 0;
    printf("请输入两个操作数:");
    scanf("%d %d", &a, &b);
    ret = pf(a,b);
    printf("%d\n",ret);
}
int main()
{
     
    int input = 0;
    int ret = 0;
    do {
     
        menu();
        printf("请选择:");
        scanf("%d", &input);
        switch (input)
        {
     
        case 1:
            Clc(Add);
            break;
        case 2:
            Clc(Sub);
            break;
        case 3:
            Clc(Mul);
            break;
        case 4:
            Clc(Div);
            break;
        default:
            printf("选择错误,重新选择\n");
            break;
        }
    } while (input);
    return 0;
}

qsort函数

qsort ()函数是 C 库中实现的快速排序算法,它可以对任何类型的数组进行排序,还可以对字符串和结构体进行排序。

整形数据

字符数据

浮点型数据

字符串类型

结构体数据

我们首先来看一看库里面的qsort函数是什么样子的

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第11张图片

如图,我们可以看到qsort函数具有4个参数,还可以看到qsort函数的头文件为stdlib.h,我们使用它时,需要引用这个头文件。qsort中这4个参数分别是什么意思呢?看如下解释:

void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) );

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第12张图片

base中存放的是待排序中第一个元素的地址,void*是无具体类型的指针,什么类型的指针都可以接收。

num-待排序的数据元素个数

width-一个元素的字节大小,base不能确定传进来的指针类型,所以不能确定我们的元素大小,所以我们将元素的字节大小传进去

compare-用来比较待排序数据中的2个元素的函数

我们下面单独来看一看第4个参数—函数指针

compare指针指向的这个函数用来比较两个元素。

int (__cdecl *compare )(const void *elem1, const void *elem2 ) );

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第13张图片

我们看到元素1小于元素2时,这个函数返回值<0,元素1大于元素2时,这个函数返回值>0,元素1等于元素2时,这个函数返回值=0

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第14张图片

通过上面的讲解我们利用qsort函数对整形排序

qsort函数对int型数组排序

#include
#include
void print_arr(int arr[], int sz)
{
     
    int i = 0;
    for (i = 0; i < sz; i++)
    {
     
        printf("%d ", arr[i]);
    }
}
int cmp_int(const void* elem1, const void* elem2)
{
     
    //从小到大排序
    return *(int*)elem1 - *(int*)elem2;
}
//int cmp_int(const void* elem1, const void* elem2)
//{
     
//    //从大到小排序
//    return *(int*)elem2 - *(int*)elem1;
//}
void test1()
{
     
    int arr[] = {
      9,8,7,6,5,4,3,2,1,0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, sz, sizeof(arr[0]), cmp_int);
    print_arr(arr, sz);
}
int main()
{
     
    print_arr(arr,sz);
    test1();//排序整形
    print_arr(arr,sz);
    return 0;
}

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第15张图片

使用qsort函数的关键在于使用者自己设计的比较函数,我们要比较的元素类型为整形,所以我们先将元素一元素二指针强制类型转换为整形指针,然后解引用拿到整形。然后返回我们比较函数应该返回的值

qsort对结构体排序

struct Stu
{
     
    char name[20];
    int age;
};
int sort_by_age(const void* e1, const void* e2)//按照年龄排序
{
     
    return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int sort_by_name(const void* e1, const void* e2)//按照姓名排序
{
     
    return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void test2()
{
     
    //使用qsort函数排序结构体数据
    struct Stu s[] = {
      {
     "zhangsan",30},{
     "lisi",20},{
     "wangwu",25} };
    int sz = sizeof(s) / sizeof(s[0]);
    //按照年龄来排序
    qsort(s, sz, sizeof(s[0]), sort_by_age);
    //按照名字来排序
    //qsort(s, sz, sizeof(s[0]), sort_by_name);
}
int main()
{
     
    test2();//排序结构体
    return 0;
}

我们要比较的元素类型为结构体类型,所以我们先将元素一元素二指针强制类型转换为结构体类型指针,我们想按照结构体中的哪个成员变量来排序,那么指针就指向这个这个成员变量。然后解引用拿到值,然后返回我们比较函数应该返回的值

模拟实现qsort函数

模仿qsort实现一个冒泡排序的通用算法

首先我们实现模拟qsort函数需要有上面讲过的四个参数,我们使用冒泡排序,我们发现所有类型的排序使用冒泡排序框架是一样的,只有在每一趟的排序的比较部分才是不一样的

void bubble_sort(void* base,
                 int sz,
                 int width,
                 int (*cmp)(const void* e1,const 					 void* e2))
{
     
    int i=0;
    
    for(i=0;i<sz-1;i++)
    {
     
        //一趟排序
        int j=0;
        for(j=0;j<sz-1-i;j++)
        {
     
            //两个元素比较,比较方法是不确定的
            if(cmp((char*)base+j*width,
               (char*)base+(j+1)*width)>0)
            {
     
                //交换
                swap((char*)base+j*width,
               (char*)base+(j+1)*width,width);
            }
        }
    }
}

上面的代码我们主要讲解一下函数指针调用比较函数时的参数是如何传的,因为qsort函数的模拟实现最重要的部分就是这里。

  if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)

如何给比较函数传参?
我们首先是不知道元素的类型的,我们首先将存放首元素的地址的指针强制类型转换为char*,为什么呢?
我们是知道一个元素是多少个字节的,因为我们已经通过传参传进来了,char*类型的指针+一个元素的字节大小,我们跳过的就是一个元素。妙不妙?哈哈哈

交换部分的函数如下:

void swap(char*buff1,char*buff2,int width)
{
     
    int i=0;
    for(i=0;i<width;i++)
    {
     
        char temp=*(buff1+i);
        *(buff1+i)=*(buff2+i);
        *(buff2+i)=temp;
    }
}

交换元素如何交换呢?

我们也不知道元素的类型,*元素不确定访问的是几个字节,那么我们怎么交换呢?—我们一个字节一个字节的交换,我们需要将两个元素地址传到交换的函数这是必然的,我们一个字节一个字节交换,所以我们还需要传单位元素字节。所以实现了以上交换元素的代码。

整体的模拟实现qsort函数代码如下:(里面有整形排序测试案例和结构体排序测试案例)

void swap(char*buff1,char*buff2,int width)
{
     
    int i=0;
    for(i=0;i<width;i++)
    {
     
        char temp=*(buff1+i);
        *(buff1+i)=*(buff2+i);
        *(buff2+i)=temp;
    }
}
void bubble_sort(void* base,
                 int sz,
                 int width,
                 int (*cmp)(const void* e1,const 					 void* e2))
{
     
    int i=0;
    
    for(i=0;i<sz-1;i++)
    {
     
        //一趟排序
        int j=0;
        for(j=0;j<sz-1-i;j++)
        {
     
            //两个元素比较,比较方法是不确定的
            if(cmp((char*)base+j*width,
               (char*)base+(j+1)*width)>0)
            {
     
                //交换
                swap((char*)base+j*width,
               (char*)base+(j+1)*width,width);
            }
        }
    }
}

//整形排序
int cmp_int(const void *elem1, const void *elem2)
{
     
   return *(int*)e1-*(int*)e2;
}

//结构体排序
struct Stu
{
     
    char name[20];
    int age;
};
int sort_by_age(const void* e1,const void* e2)
{
     
    return ((struct Stu*)e1)->age-((struct Stu*)e2)->age;
}
int sort_by_name(const void* e1,const void* e2)
{
     
    return strcmp(((struct Stu*)e1)->name,((struct Stu*)e2)->name);
}

void test1()
{
     
    int arr[]={
     9,8,7,6,5,4,3,2,1,0};
    int sz=sizeof(arr)/szieof(arr[0]);
    bubble_sort(arr,sz,sizeof(arr[0]),cmp_int);
    print_arr(arr,sz);
}

void test2()
{
     
    //使用qsort函数排序结构体数据
    struct Stu s[]={
     {
     "zhangsan",30},{
     "lisi",20},{
     "wangwu",25}};
    int sz=sizeof(s)/sizrof(s[0]);
    //按照年龄来排序
    bubble_sort(s,sz,sizeof(s[0]),sort_by_age);
    //按照名字来排序
    bubble_sort(s,sz,sizeof(s[0]),sort_by_name);
}

int main()
{
     
    test1();
    test2();
    return 0;
}

指针和数组笔试题

注意:

sizeof(数组名)–数组名表示整个数组-计算的是整个数组的大小

&数组名-取出的是整个数组的地址

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

一维数组的sizeof和&的那些事

int a[] = {
     1,2,3,4};
printf("%d\n",sizeof(a));

16—sizeof(数组名),数组名是表示整个数组,计算的是整个数组的大小

printf("%d\n",sizeof(a+0));//4/8

4(32位)/8(64位)—此时数组名a不是整个数组,而是首元素的地址,首元素地址+0没有动,还是首元素地址,是地址就是4或者8个字节。

printf("%d\n",sizeof(*a));

4 —*a是拿到的是数组的第一个元素,sizeof(*a)计算的是第一个元素的大小

 printf("%d\n",sizeof(a+1));

4(32位)/8(64位) —a是首元素的地址,a+1是第二个元素的地址,sizeof(a+1)计算的是地址的大小

printf("%d\n",sizeof(a[1]));

4—计算的是第二个元素的大小

printf("%d\n",sizeof(&a));

4(32位)/8(64位)—&a是整个数组的地址,sizeof(&a)计算的是地址的大小

printf("%d\n",sizeof(*&a));

16—&a是整个数组的地址,将地址解引用,拿到了整个数组,计算的是数组的大小

printf("%d\n",sizeof(&a+1));

4(32位)/8(64位) —&a是整个数组的地址,&a+1跳过整个数组,也是地址

printf("%d\n",sizeof(&a[0]));

4(32位)/8(64位)—第一个元素地址

printf("%d\n",sizeof(&a[0]+1));

4(32位)/8(64位)—第二个元素地址

字符数组与sizeof、&、strlen的那些事

char arr[] = {
     'a','b','c','d','e','f'};

注意:

char arr[] = {‘a’,‘b’,‘c’,‘d’,‘e’,‘f’};

这样的初始化是不含’\0’的。

printf("%d\n", sizeof(arr));//6

计算整个数组的大小,有6个元素,数组类型为char类型,所以为6个字节

printf("%d\n", sizeof(arr+0));//4/8

4(32位)/8(64位)—此时arr为首元素的地址,首元素的地址+0,还为首元素地址。

printf("%d\n", sizeof(*arr));//1

arr为数组首元素地址,解引用拿到第一个元素,元素类型为char,所以大小为1个字节

printf("%d\n", sizeof(arr[1]));//1

arr[1]是第二个元素,第二个元素类型为char,大小为1字节

printf("%d\n", sizeof(&arr));//4/8

&arr是整个数组的地址,但也是地址,大小为4(32位)或者8(64位)

printf("%d\n", sizeof(&arr+1));//4/8

&arr是整个数组的地址,&arr+1跳过整个数组,指向该数组之后的位置,但也是地址,大小为4(32位)或者8(64位)

printf("%d\n", sizeof(&arr[0]+1));//4/8

&arr[0]是第一个元素的地址,&arr[0]+1是第二个元素的地址,大小为4(32位)或者8(64位)

printf("%d\n", strlen(arr));//首元素地址 随机值

strlen函数计算字符串长度,它的结束标志为\0,数组名arr是首元素地址,strlen计算时找不到\0,所以结果为随机值

printf("%d\n", strlen(arr+0));//随机值

arr+0也为首元素地址,strlen向后计算时,无结束标志,所以也是随机值

printf("%d\n", strlen(*arr));

将首元素地址解引用,找到了a,a的ascii码值为97,strlen是需要传地址过去的,这里是error的

printf("%d\n", strlen(arr[1]));

arr[1]是第二个元素,而strlen是需要传地址过去的,也是error的

printf("%d\n", strlen(&arr));//随机值

&arr是整个数组的地址,同样道理,没有结束标志,也是随机值

printf("%d\n", strlen(&arr+1));//随机值-6

&arr是整个数组的地址,&arr+1,跳过了整个数组,指向数组最后一个元素后的地址,数组元素又是有6个,所以计算到的应该是随机值-6

printf("%d\n", strlen(&arr[0]+1));//随机值-1

&arr[0]为第一个元素地址,&arr[0]+1是第二个元素地址,所以计算得到的是随机值-1

当字符串初始化数组时的那些事

char arr[] = "abcdef";

**注意:**字符串初始化时,此时arr数组里面后面还有\0—字符串结束标志

printf("%d\n", sizeof(arr));//7

sizeof(arr)是计算整个数组的大小,6个元素+’\0’==7个字符,每个字符是一个字节,所以一共7个字节

printf("%d\n", sizeof(arr+0));//4/8

这里的arr为首元素地址,+0还是首元素地址,所以是4(32位)或8(64位)

printf("%d\n", sizeof(*arr));//1

对数组第一个元素地址解引用,拿到第一个元素,元素类型为char,所以为1个字节

printf("%d\n", sizeof(arr[1]));//1

arr[1]为第二个元素,元素类型为char,所以为1个字节

printf("%d\n", sizeof(&arr));//4/8 char(*)[7]

&arr是整个数组的地址,所以是4(32位)或8(64位)

printf("%d\n", sizeof(&arr+1));//4/8

&arr+1跳过整个数组地址,指向数组最后一个元素后的地址,但也是地址,4(32位)或8(64位)

printf("%d\n", sizeof(&arr[0]+1));//4/8

&arr[0]是第一个元素地址,+1是第二个元素地址,4(32位)或8(64位)

printf("%d\n", strlen(arr));

有结束标志\0,所以为6

printf("%d\n", strlen(arr+0));

arr为首元素地址,+0还是首元素地址,有结束标志\0,所以为6

printf("%d\n", strlen(*arr));

解引用arr,找到第一个元素,strlen需要传地址,所以是error

printf("%d\n", strlen(arr[1]));

也会报错,理由同上

printf("%d\n", strlen(&arr));

&arr是整个数组的地址,有结束标志,所以为6

printf("%d\n", strlen(&arr+1));

&arr+1跳过整个数组地址,指向数组最后一个元素后的地址,所以计算时是随机值

printf("%d\n", strlen(&arr[0]+1))

&arr[0]+1是第二个元素的地址,所以为5

字符指针的sizeof、&、strlen的那些事

char *p = "abcdef";

注意:字符指针里面存储的是字符串首元素的地址

printf("%d\n", sizeof(p));

p是指针,大小为4(32位)或者8(64位)

printf("%d\n", sizeof(p+1));

p+1指向b,但还是指针,大小为4(32位)或者8(64位)

printf("%d\n", sizeof(*p));

p指向a,解引用拿到a,a的大小为1

printf("%d\n", sizeof(p[0]));

p[0]相当于*(p+0),拿到的是a,a的大小为1

printf("%d\n", sizeof(&p));

&p是地址,大小为大小为4(32位)或者8(64位)

printf("%d\n", sizeof(&p+1));

&p+1也是地址,大小为大小为4(32位)或者8(64位)

printf("%d\n", sizeof(&p[0]+1));

&p[0]是a的地址,+1,是b的地址,但也是地址,大小为大小为4(32位)或者8(64位)

printf("%d\n", strlen(p));

p存的的a的地址,有字符串结束标志,所以计算的长度为6

printf("%d\n", strlen(p+1));

p+1指向了字符b,并且有字符串结束标志,所以计算的长度为5

printf("%d\n", strlen(*p));

*p拿到的是a字符,而strlen是需要传地址过去的,这里会报错

printf("%d\n", strlen(p[0]));

这个和上一个是相同的道理,也是会报错

printf("%d\n", strlen(&p));

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第16张图片

printf("%d\n", strlen(&p+1));

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第17张图片

printf("%d\n", strlen(&p[0]+1));

&p[0]是a的地址,+1拿到b的地址,strlen从b开始数,所以是5个

二维数组的sizeof、&、strlen的那些事

int a[3][4] = {
      0 };
printf("%d\n", sizeof(a));//48

a是数组名,sizeof(数组名),数组名表示整个数组,3*4*sizeof(int)

printf("%d\n", sizeof(a[0][0]));//4

这是第一行第一个元素,是int类型,为4个字节

printf("%d\n", sizeof(a[0]));//16

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第18张图片

arr[0]相当于第一行的数组名,sizeof(arr[0])—数组名arr[0]单独放在sizeof内部,arr[0]表示整个第一行,计算的是第一行的大小—16

printf("%d\n", sizeof(a[0] + 1));//4

a[0]作为数组名,数组名没有单独的放在sizeof中,也没有取地址,数组名表示首元素地址,所以a[0]表示第一行第一个元素的地址,a[0]+1就是第一行第二个元素的地址,它是地址,所以为4字节或者8字节

printf("%d\n", sizeof(*(a[0] + 1)));//4

a[0]+1是第一行第二个元素的地址,对他解引用找到它,它是int类型,为4个字节

printf("%d\n", sizeof(a + 1));//4

二维数组数组名没有单独放在sizeof中,也没有取地址,他表示二维数组首元素地址,二维数组首元素地址为第一行地址,a+1就是二维数组第二行的地址

printf("%d\n", sizeof(*(a + 1)));//16

a+1是第二行的地址,所以*(a+1)表示第二行,所以计算的是第二行的大小

printf("%d\n", sizeof(&a[0] + 1));//4

a[0]是第一行的数组名,&a[0]取出的是第一行的地址,&a[0]+1就是第二行的地址

printf("%d\n", sizeof(*(&a[0] + 1)));//16

&a[0]+1就是第二行的地址,*第二行的地址,拿到的是第二行,所以我们计算的是第二行的大小16

printf("%d\n", sizeof(*a));//16

a是数组名,二维数组数组名没有单独放在sizeof中,也没有取地址,他表示二维数组首元素地址,二维数组首元素地址为第一行地址,将第一行地址解引用,拿到第一行,计算的是第一行的大小

printf("%d\n", sizeof(a[3]));//16

讲这个之前,先讲一下表达式的属性:

例如3+5这个表达式,一个表达式有两个属性,一个是值属性-8,一个是类型属性-int

而sizeof(3+5),sizeof内部的表达式是不会运算的,只是会进行推测它算的结果类型是int类型,然后算出字节大小。

再看这个sizeof(a[3]):

在这里a[3]其实是第四行的数组名(如果有的话),sizeof(数组名),数组名单独在sizeof中,他代表的是整个数组,a[3]虽然在这个数组中是不存在的,但是我们这里是不会真正访问a[3]的,这里是不会看他的值属性的,只会看它的类型属性,a[3]的类型属性为–int[4],所以sizeof在知道了它的类型属性后,就将结果算出来了,4*sizeof(int),故计算的是16字节

超硬核指针笔试题,良心画图+文字详细讲解,你值得学会

1、

int main()
{
     
  int a[5] = {
      1, 2, 3, 4, 5 };
  int *ptr = (int *)(&a + 1);//a是数组的地址&a+1类型为数组指针
  printf( "%d,%d", *(a + 1), *(ptr - 1));
  return 0;
}

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第19张图片

首先我们创建了有5个int类型元素的数组a,重点是下面这个代码,&a取出的是整个数组的地址,&a+1是跳过整个a数组后面的那个地址,而&a+1类型为数组指针,被强制类型转换为int*,放入指针变量ptr中,数组名是首元素地址,a+1是第二个元素的地址,解引用拿到内容2,ptr-1指向了第五个元素的地址,解引用拿到内容5,所以打印的结果为2,5

image-20210602153537500

2、

struct Test
{
     
    int Num;
    char *pcName;
    short sDate;
    char cha[2];
    short sBa[4];
}*p;
//假设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;
}

这道题考察的是:指针类型决定了指针的运算!

 printf("%p\n", p + 0x1);

首先p是结构体指针,这个结构体的大小为20个字节,p+1,p的初值为0x100000,p是结构体指针,指针+1,跳过了20个字节,所以p+1的值变为了0x100014

printf("%p\n", (unsigned long)p + 0x1);

第二个printf将p强制类型转换为整形,+1就仅仅是+1,得到0x100001

printf("%p\n", (unsigned int*)p + 0x1);

第三个printf将p强制类型转换为int*,p指向的是无符号的整形,p+1,int*的大小为4个字节,所以p+1,相当于0x100000+4=0x100004

3、

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

我们将代码分开解读:

int *ptr1 = (int *)(&a + 1);

首先我们创建了有4个int类型元素的数组a,&a取出的是整个数组的地址,&a+1是跳过整个a数组后面的那个地址,而&a+1类型为数组指针,被强制类型转换为int*,放入指针变量ptr1中

int *ptr2 = (int *)((int)a + 1);

a是数组名,是数组首元素的地址,此时a被强制类型转换为int,然后+1,相当于是首元素的地址内容加了1,首元素地址内容加了1,加了一个字节,相当于是存储1的下一个字节,将(int)a+1强制类型转换为int*,存放给指针变量ptr2,ptr2指向内存中存储1的下一个字节,假设我们的内存是小端存储,可以得如图解释:

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第20张图片

printf( "%x,%x", ptr1[-1], *ptr2);

我们看需要打印什么,ptr1[-1]等价于*(ptr-1),ptr-1指向了a数组第4个元素,解引用拿到4

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第21张图片

第二个打印*ptr2,相信博主的下面这幅图你可以理解,因为博主得编译器是小端存储,就以小端存储形式讲解,所以最后*ptr2打印的是2000000

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第22张图片

看,结果果然是我们上面得到的结果!

image-20210602162407858

4、

#include 
int main()
{
     
	int a[3][2] = {
      (0, 1), (2, 3), (4, 5) };//1,3,5
	int *p;
	p = a[0];//第一行首元素的地址
	printf( "%d", p[0]);//*(p+0)
	return 0;
}

这道题有一个坑,再初始化二维数组时,里面用的是逗号表达式,而不是大括号,不仔细看,很容易会看错,所以二维数组初始化的是1,3,5;a[0]为二维数组第一行的数组名,为第一行首元素的地址,指针p指向该地址,p[0]等价于*(p+0),拿到了第一行第一个元素1。所以打印的就为1

image-20210602163751797

5、

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

a是一个五行五列的二维数组,p是一个指向有4个元素的数组的指针,a是二维数组数组名,数组名代表首元素的地址,二维数组首元素为a[0],即为a[0]的地址,这里将a[0]的地址赋给了指针p,p是一个指向4个元素数组的指针,而a[0]是拥有5个元素的,有人就会想这不是溢出了吗?会不会报错?在这里是不会报错的,只是会有警告,这里确实将a[0]的地址给了p,我们不管它放下还是放不下,放不下截断就是了。

printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);

我们来看题目需要打印的东西,首先p[4][2]等价于*(*(p+4)+2),p+1的步幅是四个元素,因为p的指针类型为int*[4],上面代码要打印的是指针-指针,而指针-指针是什么?是指针之间的元素个数(只是在数组中),我们分别以%p和%d的形式打印,请看下图解释:

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第23张图片

代码运行如图:

image-20210602171700529

6、

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));//aa[1]—第二行的数组名,第二行数组名表示第二行首元素的地址,指向6
  printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
  return 0;
}
int *ptr1 = (int *)(&aa + 1);

&aa为二维数组整个数组的地址,+1跳过整个数组最后一个元素后面的地址,强制类型转换为int*,ptr1指向该地址

int *ptr2 = (int *)(*(aa + 1));

*(aa+1)等价于aa[1]—第二行的数组名,第二行数组名表示第二行首元素的地址,这里的强制类型转换是没啥用的,因为它本来就是int*类型,ptr2指向该地址,也可以这样理解—aa是二维数组数组名,数组名是首元素地址,首元素为a[0],所以为a[0]的地址,+1,跳过整个a[0],指向a[1]的地址,解引用拿到a[1]首元素的地址,ptr2指向该地址

printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));

由上面的解释,再根据画图理解,可知道打印出10和5

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第24张图片

image-20210602174740190

7、

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

a是一个指针数组,数组元素类型为char*,存放在每个字符串的首字符地址,数组名代表首元素地址,pa将指向a[0],pa++后,pa指向a[1],解引用pa拿到了a[1],以%s形式打印,打印出字符串at

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第25张图片

image-20210602175836374

8、

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

接下来我们对这道题每句代码进行分析:

char *c[] = {
     "ENTER","NEW","POINT","FIRST"};

首先c是一个数组,数组的每个元素类型为char*,每个元素是字符串指针,每个指针都指向了一个字符串,指针里面存的是字符串首字符的地址

char**cp[] = {
     c+3,c+2,c+1,c};

cp是一个数组,数组的每个元素为char**类型,即为二级指针,我们再看cp数组里面的元素,我们知道数组名是首元素的地址,c+1即为c数组中第二个元素的地址,c+2即为c数组中第三个元素的地址,c+3即为c数组中第四个元素的地址。而c数组中的元素都为一级指针,一级指针的地址当然是二级指针啦!cp数组中的元素为c数组中的元素的地址。

char***cpp = cp;

数组名为首元素地址,将cp数组的首元素地址存放在cpp指针变量中。

经过了上面的分析,我们可以画出c数组和cp数组及三级指针cpp之间的关系图:

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第26张图片

接下来我们依次看下面的四个打印:

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

文字解释:

首先++cpp,我们的cpp指向了cp数组的第二个元素c+2的地址,*++cpp我们拿到了c+2,c+2即为c数组中第三个元素的地址,再进行解引用*我们拿到了第三个元素,第三个元素是指针,他存的是POINT字符串中的首字符的P地址,我们%s打印,打印出来的就是POINT

画图解释:

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第27张图片

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

文字解释:

+号的优先级是最低的,我们最后才使用,首先++cpp,因为上一个打印将cpp指向了cp数组的第二个元素,这里的++cpp将指向cp数组的第三个元素,*++cpp,我们拿到了第三个元素的内容c+1,–*++cpp,我们将c+1-1,拿到了c,c是c数组首元素的地址,然后解引用,拿到了c数组的首元素,c数组的首元素是个指针,指向字符串ENTER的首字符E的地址,最后+3,指向了字符串ENTER的E字符,我们以%s打印,所以打印的是ER。

画图解释:

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第28张图片

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

文字解释:

cpp[-2]本质上程序会解读成*(cpp+(-2)),那么这个表达式就可以等价于**(cpp+(-2))+3,首先我们cpp-2,cpp指向了cp数组的第一个元素,*(cpp+(-2)),我们能拿到了cp数组的第一个元素c+3,c+3是c数组第四个元素的地址,然后再进行解引用,我们拿到了c数组的第四个元素,它是个指针,存放的是字符串FIRST的首字符F的地址,最后+3,指向了FIRST中的S字符,我们以%s打印,就打印了ST。

画图解释:

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第29张图片

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

文字解释:

cpp[-1][-1]+1等价于*(*(cpp+(-1))+(-1))+1,cpp此时是指向cp数组的第三个元素的,cpp-1指向cp数组的第二个元素,然后解引用,拿到了cp数组第二个元素的内容c+2,然后c+2-1,cp数组第二个元素的内容变成了c+1,c+1是c数组的第二个元素的地址,然后解引用拿到了c数组第二个元素的内容,c数组的第二个元素为指针,指向NEW字符串的首字符N,最后+1,指向了NEW字符串的第二个字符E,我们以%s打印,打印了EW。

画图解释:

指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解_第30张图片

有关指针进阶的讲解就到这里,由于博主水平有限,如有错误之处,还望指正,欢迎大家学习交流!

你可能感兴趣的:(C语言,qsort函数,C语言指针进阶,指针笔试题,指针)