进阶指针的理解

完结!!觉得的对自己有用的话,留下三连吧

1. 字符指针

字符指针就是char*

正常我们这样写

int main()
	{
    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';
    return 0; 
    }

但是我们也可以这样写

int main()
{
    const char* pstr = "hello bit.";
    printf("%s\n", pstr);
    return 0; }

思考:这里是把一个字符串放到pstr指针变量里了吗?
回答:当然不是,他只是把字符串的首元素地址存到pstr里了,而不是存了整个字符串!!!!
我们再思考一下下面的代码能不能运行通过呢!!

int main()
{
     char* pstr = "hello bit.";
     *pstr=‘w’;
    printf("%s\n", pstr);
    return 0; 
    }

答案是运行不过的!!!因为这里的字符串是常量,而不是变量,他是存放在内存中的常量区(数据区)的,而常量是不能被改变的!!
所以我们写代码就可在这里的char前加上const!

int main()
{
    const char* pstr = "hello bit.";
     *pstr=‘w’;
    printf("%s\n", pstr);
    return 0; 
    }

因为这样根本就不能编译过!!
提醒一下!在创建常量时,相同的常量地址是一样的,而不会重复创建!!!
所以让我们来看下下面一道面试题。

#include <stdio.h>
int main()
{
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    const char *str3 = "hello bit.";
    const char *str4 = "hello bit.";
    if(str1 ==str2)
 printf("str1 ==str2\n");
    else
 printf("str1 !=str2\n");
       
    if(str3 ==str4)
 printf("str3 ==str4\n");
    else
 printf("str3 !=str4\n");
       
    return 0; 
    }

大家思考一下看看呢
答案是第一个是=,第二个是!=
这是因为我上面所述的在创建常量时,相同的常量地址是一样的,而不会重复创建!!!
而创建变量时是会地址是不会一样的即使元素一样,举一个简单的例子 i=1;j=1这里的i和j的元素是一样的,但是他们的地址不可能一样的!!

2. 指针数组

数组指针就是存放指针(地址)的数组
比如
int a=10;
int b=20;
int c=30;
int *pa=&a
int *pb=&b
int *pc=&c

如果变量很多的话 ,我们可以采用指针数组才存放各个元素的地址,也就是说对所有地址整合,方便查找,这理解方式和我们刚开始学数组的理解方式是一样的,只不过是将原来的元素都换成了地址了。

直接上题目来理解!

#include<stdio.h>
int main()
{
	int arr1[5]={1,2,3,4,5};
	int arr2[5]={6,7,8,9,10};
	int * p[2]={arr1,arr2};
	return 0;
	}

这个代码就是基本形式,相信大家不难理解。
现在我们对代码进一步理解!

#include<stdio.h>
int main()
{
	int arr1[5]={1,2,3,4,5};
	int arr2[5]={6,7,8,9,10};
	int * p[2]={arr1,arr2};
	for(int i=0;i<2;i++)
	{
		for(int j=0;j<5;j++)
		{
		printf("%d",(*(p+i))[j])//*p+i就是取出指针数组的元素(也就是arr1和arr2),我们也可以写成p[i][j]
		}
		printf("\n")
		}
	return 0;
	}

这时候应该就有同学问啦,这里的p[i][j]怎么和二维数组的表示一样呢,这是我声明一下,他们是完全不一样的,因为二维数组的地址是连续的,而这里的p[i][j]是解指针数组,但是每个整个数组的元素地址是不连续的!!!!所以不一样。

3. 数组指针

3.1数组指针的定义

这是一个重头戏哦,在这里我们将更一步理解数组指针和指针数组的区别。
数组指针顾名思义就是存放整个数组地址的指针。
我们先看下面这两种表达形式
int *p1[10];
int (*p2)[10];
我们怎么理解int (p2)[10]呢?
解释:p先和
结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个
指针,指向一个数组,叫数组指针。

3.2&数组名和数组名的区别

对于下面的数组:

int arr[10];

arr 和 &arr 分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥?
我们先看看这个

#include <stdio.h>
int main()
{
    int arr[10] = {0};
    printf("%p\n", arr);
    printf("%p\n", &arr);
    return 0; }

结果都是一样的
在这里插入图片描述
我们再来看看这个代码

#include <stdio.h>
int main()
{
    int arr[10] = { 0 };
    printf("%p\n", arr);
    printf("%p\n", arr + 1);
    printf("%p\n", &arr);
    printf("%p\n", &arr + 1);
    return 0;
}

在这里插入图片描述
我们一个可以看出arr+1是加了4个字节,而&arr+1是加了40个字节,所以我们不然想象arr取的是首元素地址,而&arr取的是整个数组的地址。
所以数组指针一般都是int (*p)[10]=&arr这种形式,也有个例,按具体情况而定。

3.3 数组指针的使用

直接上实例

#include <stdio.h>
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int (*p)[10] = &arr;//把数组arr的整个地址赋值给数组指针变量p
    //但是我们一般很少这样写代码
    return 0; }

再看

#include <stdio.h>
void print_arr(int arr[3][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]);
       }
int main()
{
    int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
   print_arr(arr, 3, 5);
    return 0; }
#include <stdio.h>
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));//arr+i解引用就是得到第i行首元素地址
       }
        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传的是二维数组第一行的地址(并非第一行第一个的地址,在一维数组中代表的是首元素地址),那么第一次解引用时就是解的i行的地址,第二次解引用是解的第i行第j个的地址,我们比较一下这两种代码,第一种实际上就是二维,而第二种则是将二维数组当做一维数组来分析了,就是每一行当做一个元素。
这里为什么不用&arr呢,因为我们知道&arr是取的整个二维数组的地址,然后再解引用的话刚刚达不到我们所需的效果!
我们来测试一下指针数组和数组指针的理解情况
看题

int *arr[5];
p=&arr
这里的指针变量p应该怎么写呢?
答案是

int * arr[5];
int * ( * p )[5]=&arr//这可以理解为将整个指针数组地址存放的数组指针中(就是地址的地址存到指针中)
因为数组指针的变量类型是:取地址的变量类型(*)[取地址的数组元素个数]
比如 char arr[5] 将它存到数组指针中的形式应该是char (*p)[5]=&arr

学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:

int arr[5];//数组,里面有5个元素
int *parr1[10];//指针数组,里面有10个指针
int (*parr2)[10];//数组指针,是指向含有10个元素数组的整个地址
int (*parr3[10])[5];//这是数组指针指针数组套嵌到中指针数组,也就是说现在有一个指针数组可以存放10个指针,而这10个指针是10个指向含有5个元素数组整个地址的数组指针

4. 数组传参和指针传参

4.1 一维数组传参

一维数组传参传递的其实是首元素地址,我们在形参接受的时候我们可以用指针和数组直接接受,但是我们并不建议用数组接受,因为数组传参本质传的是地址,当然也不是不行,我们来看一些例子就懂了!
我们还要理解一点就是我们用数组来接收的时候,其实就是将地址作为数组的首元素然后按照数组实际大小向后开辟空间的,再原数组也是向后开辟空间的,所以传参后的数组地址其实是和原数组地址是一样的,但是在开辟大小的时候就可能出错,比如原数组大小为[100],后来我一不小心将传参的函数地址开辟成[10],那就导致开辟的数组过小,在运行的时候就会越界,导致编译器警报!

#include <stdio.h>
void test(int arr[])//ok?-------yes 
{}
void test(int arr[10])//ok?-------yes 
{}
void test(int *arr)//ok?-------yes 
{}
void test2(int *arr[20])//ok?-------yes int *是数组类型 就是指针数组而已
{}
void test2(int **arr)//ok?-------yes int *是数组类型 就是指针数组而已
{}
int main()
{
 int arr[10] = {0};
 int *arr2[20] = {0};
 test(arr);
 test2(arr2);
}

4.2 二维数组传参

void test(int arr[3][5])//ok?---yes
{}
void test(int arr[][])//ok?---no  二维数组空间开辟格式就出错了
{}
void test(int arr[][5])//ok?---yes
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok?---yes
{}
void test(int* arr[5])//ok?---no 这是指针数组,是用来存指针的
{}
void test(int (*arr)[5])//ok?---yes  这我在数组指针里也说过了,arr在二维数组里代表是首行地址,而*arr就代表这某一行,[]就代表着这一行某个元素!
{}
void test(int **arr)//ok?--no  这是一个二级指针,我们来分析一下,二级指针是指向一级指针的,而arr就是一个地址,*arr就不是一个地址了,那么对*arr解引用就纯属扯淡!!
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);
}

4.3 一级指针传参

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

当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
比如:
void test1(int *p)
{}
//test1函数能接收什么参数?
*p是一个一级指针 那么必须得传一个地址
像&a,int *p=&a中的p,还有一维数组的数组名

4.4 二级指针传参

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

思考:
当函数的参数为二级指针的时候,可以接收什么参数?

void test(char **p)
{

}
int main()
{
 char c = 'b';
 char*pc = &c;
 char**ppc = &pc;
 char* arr[10];
 test(&pc);
 test(ppc);
 test(arr);//这三种都行
 return 0;
}

5. 函数指针

函数指针是指向函数的指针
那么函数有地址吗??
这不然猜测是有的,不然也不会有函数指针,哈哈
但是函数指针地址我们怎么获取呢?
我们可以用&函数名或者直接用函数名,不要问为什么可以不加&,问就是规则!
我们看下面的代码:

#include <stdio.h>
void test()
{
 printf("hehe\n");
}
int main()
{
 printf("%p\n", test);
 printf("%p\n", &test);
 return 0;
}

在这里插入图片描述
输出的是两个地址,这两个地址是 test 函数的地址。
那我们的函数的地址要想保存起来,怎么保存?
下面我们看代码:

void test(int x)
{
....
}

//下面pfun1和pfun2哪个有能力存放test函数的地址?

void (*pfun1)(int);
void *pfun2(int);

首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?
答案是:
pfun1可以存放。pfun1先和结合,说明pfun1是指针,指针指向的是一个函数,指向的函数是int类型
那么这个函数指针类型就是 void (
)( int)
数,返回值类型为void。

阅读两段有趣的代码:

//代码1
(*(void (*)())0)();//void (*)()这是一个函数指类型,将0转化为十六进制,然后将这个十六进制当做地址,再将地址强行转换成void的函数指针类型,所以(*(void (*)())0)()就是一个函数指针,地址是0的地址
//代码2
void (*signal(int , void(*)(int)))(int);// signal是一个函数声明,它的参数是int和void(*(int))signal的返回类型是void(*)(int)类型的  我们可以类比 int Add(int,int),这是一个声明,函数是Add,返回类型是int  ,我们将void (*signal(int , void(*)(int)))(int)可以看成void (*)(int)   signal(int , void(*)(int))  这样应该可以看出来了吧 ,但是这种写法肯定是错误的哈,只是可以看成这样方便理解!
//但是我们可以用typedef来重新定义别名  我们定义typedef void(*tp_y)(int) 那么原来的那个代码可以写成tp_y signal(int,tp_y)!!!这就简化代码了哈!!

我们学了函数指针怎么用呢???我们再看看下一节,可以就略懂一二啦!

6. 函数指针数组

函数指针数组就是来存放函数指针的数组
那么函数指针数组怎么定义呢??
我们知道int (*p)(int)是函数指针,而且数组是在名称后面加上[],难道是int (p)(int)[10]吗???
切记这是不对的,函数指针名称是p,因为函数指针类型是int(
)()所以这里的p就是名称,所以我们应该写成int (p[10])(int)这就是函数指针数组的写法了,那么函数指针类型就是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;
    do
   {
        printf( "*************************\n" );
        printf( " 1:add           2:sub \n" );
        printf( " 3:mul           4:div \n" );
        printf( "*************************\n" );
        printf( "请选择:" );
        scanf( "%d", &input);
        switch (input)
       {
         case 1:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = add(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 2:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = sub(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 3:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = mul(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 4:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = div(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 0:
                printf("退出程序\n");
 breark;
        default:
              printf( "选择错误\n" );
              break;
       }
 } while (input);
    
    return 0;
}

现在我们将所有函数指针都整合起来,那我们就得用到函数指针数组啦。
我们来看函数指针数组的写法:

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

这样来看是不是简洁多啦,但是也是有一定的局限性的,这里的所有函数指针类型都应该一样,但是这里好处更多一点,其实函数指针数组的用途:转移表(有一种跳转的感觉)。上面就是简单应用之一,用了函数指针数组优化代码不仅仅看起来简洁,而且后续增加功能更方便!!!

7. 指向函数指针数组的指针

我们有函数指针数组,那么函数指针数组一定也有它的地址,这就是指向函数指针数组的指针,那么怎么定义这个指针变量呢??
我们看下面这段代码

>void test(const char* str)
{
 printf("%s\n", str);
}
int main()
{
 //函数指针pfun
 void (*pfun)(const char*) = test;
 //函数指针的数组pfunArr
 void (*pfunArr[5])(const char* str);
 pfunArr[0] = test;
 //指向函数指针数组pfunArr的指针ppfunArr
 void (*(*ppfunArr)[5])(const char*) = &pfunArr;
 return 0;
}

指向函数指针数组的指针我们是先定义一个数组指针(*p)[10]=&Add,返回类型是函数指针类型int ( *(*p)[10])(int ,int)=&Add

8. 回调函数

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

struct Stu//定义结构体
{
	char name[20];
	int age;
	double score;
};
void Swap(char* baff1, char* baff2, int size)//用char好处是一个字节,可以将某个类型的所有字节一一交换,这样就要了通用性
{
	for (int i = 0; i < size; i++)
	{
		int tmp = *baff1;
		*baff1 = *baff2;
		*baff2 = tmp;
		baff1++;
		baff2++;
	}
}
int cmp_stu_by_age(const void* p1, const void* p2)//比较年龄
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
int cmp_stu_by_name(const void* p1, const void* p2)//比较姓名长度
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}

int com_int(const void* p1, const void* p2)//比较大小
{
	return *(int*)p1 - *(int*)p2;
}
}

void test2()
{
	struct Stu arr[3] = { {"zhangsan", 20, 55.5},{"lisi", 30, 88.0},{"wangwu", 10, 90.0} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);//回调函数
	//Myqsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
}

void test1()
{
	int arr[] = { 2255,288,1155,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int size = sizeof(arr[0]);
	qsort(arr, sz, size, com_int);//回调函数
	for (int i = 0; i < sz; i++)
		printf("%d ", arr[i]);
}
int main()
{
	test1();
	test2();//结构体类型排序
	return 0;
}

上面的代码是qsort函数的使用。我来详细说说qsort是什么以及原理!

qsort()函数:快速排序的函数 -引用stdlib.h头文件
在这里插入图片描述
参数说明:
void qsort (

void* base, //要排序的目标数组
size_t num,     //待排序的元素个数
size_t width,    //一个元素的大小,单位是字节
int(*cmp)(const void* e1, const void* e2)

);

其中cmp是函数指针,cmp指向的是:排序时,用来比较两个元素的函数。需要自己编写。
返回值:
在这里插入图片描述
qsort函数是一个可以排任意类型数据的一个函数,其函数设计原理就用到了回调函数!!
那么我们来用冒泡排序来设计一下qsort函数吧。

struct Stu//定义结构体
{
	char name[20];
	int age;
	double score;
};
void Swap(char* baff1, char* baff2, int size)//用char好处是一个字节,可以将某个类型的所有字节一一交换,这样就要了通用性
{
	for (int i = 0; i < size; i++)
	{
		int tmp = *baff1;
		*baff1 = *baff2;
		*baff2 = tmp;
		baff1++;
		baff2++;
	}
}
int cmp_stu_by_age(const void* p1, const void* p2)//比较年龄
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
int cmp_stu_by_name(const void* p1, const void* p2)//比较姓名长度
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}

int com_int(const void* p1, const void* p2)//比较大小
{
	return *(int*)p1 - *(int*)p2;
}
void Myqsort(void* p, int sz, int size, int (*com)(const void* p1, const void* p2))
{
	int i = 0, j = 0;
	for (i = 0; i < sz-1; i++)
	{
		for (j = 0; j < sz-1 - i; j++)
		{
			if (com(((char*)p + j * size), ((char*)p + (j + 1) * size)) > 0)//这里至于为什么强制转换为char类型是因为这样操作可以拿到任意类型第一个字节的地址
			{
				Swap((char*)p + j * size, (char*)p + (j + 1) * size, size);//交换
			}
		}
	}
}

void test2()
{
	struct Stu arr[3]={ {"zhangsan", 20, 55.5},{"lisi", 30, 88.0},{"wangwu", 10, 90.0} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	Myqsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);//回调函数
	//Myqsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
}

void test1()
{
	int arr[] = {2255,288,1155,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int size = sizeof(arr[0]);
	Myqsort(arr, sz, size, com_int);//回调函数
	for (int i = 0; i < sz; i++)
		printf("%d ", arr[i]);
}
int main()
{
	test1();
	test2();//结构体类型排序
	return 0;
}

看了代码你是否能理解回调函数怎么使用呢??
其实啊,回调函数就是你通过函数指针进行传参,传给一个函数的参数(这个函数可以叫做中间函数),这个参数是一个函数指针变量,然后在中间函数里通过这个函数指针变量调用的函数,这个函数就叫做回调函数!
我将上面的代码画个图帮助理解:
在这里插入图片描述
我们应该可以感觉到回调函数用的时候,只需将不同的函数指针传到中间函数中,那么就可以通过中间函数,找到对应的回调函数,不然的话每一个函数我们要写一个中间函数似乎有点太麻烦,所以我们通过回调函数将他整合起来,我们可以看一套简单的代码来理解(就是我们上面写的计算器暴力写法!):

#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;
    do
   {
        printf( "*************************\n" );
        printf( " 1:add           2:sub \n" );
        printf( " 3:mul           4:div \n" );
        printf( "*************************\n" );
        printf( "请选择:" );
        scanf( "%d", &input);
        switch (input)
       {
         case 1:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = add(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 2:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = sub(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 3:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = mul(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 4:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = div(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 0:
                printf("退出程序\n");
 breark;
        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");
}

void calc(int (*pf)(int,int))
{
	int x = 0;
	int y = 0;
	int ret = 0;

	printf("请输入2个操作数:>");
	scanf("%d%d", &x, &y);
	ret = pf(x, y);
	printf("ret = %d\n", ret);
}

int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			calc(Add);
			break;
		case 2:
			calc(Sub);
			break;
		case 3:
			calc(Mul);
			break;
		case 4:
			calc(Div);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);

	return 0;
}

这样是不是更简洁了呢?

9. 指针和数组面试题的解析

温故而知新!
来看面试题,让我们看看公司是如何来考察我们的哈。

9.1数组面试题

数组名是什么呢?
数组名通常来说是数组首元素的地址
但是有2个例外:

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小
  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址

sizeof是一个操作符
sizeof 计算的是对象所占内存的大小-单位是字节,size_t
不在乎内存中存放的是什么,只在乎内存大小

strlen 库函数
求字符串长度,从给定的地址向后访问字符,统计\0之前出现的字符个数*

int main()
{
	int a[] = { 1,2,3,4 };
		           0 1 2 3
	int (*p)[4] = &a;
	printf("%d\n", sizeof(a));//4*4 = 16
	printf("%d\n", sizeof(a + 0));//4/8 a+0是数组第一个元素的地址,是地址,大小就是4/8个字节
	printf("%d\n", sizeof(*a)); //4 a表示数组首元素的地址,*a表示数组的第一个元素,sizeof(*a)就是第一个元素的大小-4
	printf("%d\n", sizeof(a + 1));//4/8 a表示数组首元素的地址,a+1数组第二个元素的地址,sizeof(a+1)就是第二个元素的地址的大小
  	printf("%d\n", sizeof(a[1]));//4 计算的是第二个元素的大小
	printf("%d\n", sizeof(&a));//4/8 &a取出的是数组的地址,数组的地址也是地址呀,是地址大小就是4/8字节
	printf("%d\n", sizeof(*&a));//16 计算的整个数组的大小 
	printf("%d\n", sizeof(&a + 1));//4/8 - &a是数组的地址,+1跳过整个数组,产生的4后边位置的地址
	printf("%d\n", sizeof(&a[0]));//4/8 取出的数组第一个元素的地址
	printf("%d\n", sizeof(&a[0] + 1));//4/8 数组第二个元素的地址

	//字符数组
	char arr[] = { 'a','b','c','d','e','f' };//[a b c d e f]

printf("%d\n", strlen(arr));//随机值,arr数组中没有\0,所以strlen函数会继续往后找\0,统计\0之前出现的字符个数
printf("%d\n", strlen(arr + 0));//随机值,arr+0还是数组首元素的地址
printf("%d\n", strlen(*arr));//err - arr是数组首元素的地址,*arr是数组的首元素,‘a’-97
printf("%d\n", strlen(arr[1]));//err -'b' - 98
printf("%d\n", strlen(&arr));//随机值
printf("%d\n", strlen(&arr + 1));//随机值
printf("%d\n", strlen(&arr[0] + 1));//随机值

//64位平台
printf("%llu\n", sizeof(arr));//6
printf("%llu\n", sizeof(arr + 0));//4/8 arr + 0是数组首元素的地址
printf("%llu\n", sizeof(*arr));//1 - *arr是首元素,首元素是一个字符,大小是一个字节
printf("%llu\n", sizeof(arr[1]));//1 - arr[1]是数组的第二个元素,大小是1个字节
printf("%llu\n", sizeof(&arr));//4/8 &arr是数组的地址
printf("%llu\n", sizeof(&arr + 1));//4/8 &arr + 1是从数组地址开始向后跳过了整个数组产生的一个地址
printf("%llu\n", sizeof(&arr[0] + 1));//4/8 &arr[0] + 1 是数组第二个元素的地址

	return 0;
}


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

char arr[] = "abcdef";
[a b c d e f \0]
	printf("%d\n", strlen(arr));//6
	printf("%d\n", strlen(arr + 0));//6
	printf("%d\n", strlen(*arr));//err
	printf("%d\n", strlen(arr[1]));//err
	printf("%d\n", strlen(&arr));//6
	printf("%d\n", strlen(&arr + 1));//随机值
	printf("%d\n", strlen(&arr[0] + 1));//5
	printf("%d\n", sizeof(arr));//7
	printf("%d\n", sizeof(arr + 0));//4/8 arr+0是数组首元素的地址
	printf("%d\n", sizeof(*arr));//1 - *arr 数组的首元素
	printf("%d\n", sizeof(arr[1]));//1 arr[1]数组的第二个元素
	printf("%d\n", sizeof(&arr));//4/8 - &arr数组的地址,但是数组的地址依然是地址,是地址大小就是4/8
	printf("%d\n", sizeof(&arr + 1));//4/8 - &arr + 1是\0后边的这个地址
	printf("%d\n", sizeof(&arr[0] + 1));//4/8 - &arr[0] + 1是数组第二个元素的地址

char* p = "abcdef";

	printf("%d\n", strlen(p));//6
	printf("%d\n", strlen(p + 1));//5 从b的位置开始向后数字符
	printf("%d\n", strlen(*p));  //err
	printf("%d\n", strlen(p[0]));//err
	printf("%d\n", strlen(&p));//随机值
	printf("%d\n", strlen(&p + 1));//随机值
	printf("%d\n", strlen(&p[0] + 1));//5  从b的位置开始向后数字符

	printf("%d\n", sizeof(p));//4/8  p是指针变量,计算的是指针变量的大小
	printf("%d\n", sizeof(p + 1));//4/8 p+1是'b'的地址
	printf("%d\n", sizeof(*p)); //1  - *p 其实就是'a'
	printf("%d\n", sizeof(p[0]));//1 - p[0]-> *(p+0)-> *p
	printf("%d\n", sizeof(&p));//4/8 &p 是指针变量p在内存中的地址
	printf("%d\n", sizeof(&p + 1));//4/8 - &p+1是跳过p之后的地址
	printf("%d\n", sizeof(&p[0] + 1));//4/8 &p[0]是‘a’的地址,&p[0]+1就是b的地址


	二维数组
	int a[3][4] = { 0 };

	printf("%d\n", sizeof(a));//计算的是整个数组的大小,单位是字节3*4*4 = 48
	printf("%d\n", sizeof(a[0][0]));//4 第1行第一个元素的大小
	printf("%d\n", sizeof(a[0]));//16 - a[0]是第一行的数组名,sizeof(a[0])就是第一行的数组名单独放在sizeof内部,计算的是第一行的大小
	printf("%d\n", sizeof(a[0] + 1));//4/8 a[0]作为第一行的数组名,并没有单独放在sizeof内部,也没有被取地址
	//所以a[0]就是数组首元素的地址,就是第一行第一个元素的地址,a[0]+1就是第一行第二个元素的地址

	printf("%d\n", sizeof(*(a[0] + 1)));//4 - *(a[0] + 1))表示的是第一行第二个元素
	printf("%d\n", sizeof(a + 1));//4/8 - a表示首元素的地址,a是二维数组,首元素的地址就是第一行的地址
	//所以a表示的是二维数组第一行的地址,a+1就是第二行的地址
	printf("%d\n", sizeof(*(a + 1)));//16 对第二行的地址解引用访问到就是第二行
	*(a+1) -> a[1]
	sizeof(a[1])
	
	printf("%d\n", sizeof(&a[0] + 1));//4/8 - a[0]是第一行的数组名,&a[0]取出的就是第一行的地址
	//&a[0] + 1 就是第二行的地址

	printf("%d\n", sizeof(*(&a[0] + 1)));//16 - 对第二行的地址解引用访问到就是第二行
	printf("%d\n", sizeof(*a));//16 - a就是首元素的地址,就是第一行的地址,*a就是第一行
	//*a - > *(a+0) -> a[0]

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

	int a = 10;
	printf("%d\n", sizeof(a));//4
	printf("%d\n", sizeof(int));//4

	return 0;
}

9.2指针面试题

笔试题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;
}
//程序的结果是什么?

在这里插入图片描述

笔试题2:

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

在这里插入图片描述
这里我说一下哈,做这种题型的时候一定要看清+1是加在指针(地址)上还是普通的变量上,加在指针上,那么+1就是在原来的指针上加上该指针指向的变量类型所占用的字节数!这个我一开始也搞混了,不知道到底该加多少,所以大家一定得注意,细心!!

笔试题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;
}

在这里插入图片描述

笔试题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;
}

这题就考的细节了哈,大家不知道有没有看出来。
我们正常初始化二维数组的时候,我们不是用的圆括号,而是花括号,所以区别就是这个圆括号,显然是逗号表达式,所以二维数组里存的应该是13 50 00,p=a[0],那么p是指向二维数组第一行首元素地址,所以p[0]=1

笔试题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;
}

在这里插入图片描述

笔试题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));
    printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
    return 0;
}

在这里插入图片描述

这题主要得找到ptr1和ptr2指向的位置,我们来分析一下,&aa是整个数组地址,那么+1就得跳过整个数组,所以ptr1指向aa[2][5]后面一个地址,在强制转换为整型指针,所以prt1-1就向前移动一个整型,结果就是10,那么aa代表的是什么呢?在一维数组aa就代表的是首元素地址aa[0],那在二维数组中aa代表的是首行的地址aa[0],所以aa+1就跳过一行到达下一行,所以aa+1指向的是第二行地址,然后解引用得到第二行首元素地址,所以*(aa+1)就是第二行首元素地址,所以ptr2-1就是第一行最后一个元素地址了,解引用后就是5
在这里插入图片描述

笔试题7:

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

这题也好理解,我们先得知道*a[]存的是什么,这存的是每个字符串首元素地址!
而pa的地址也就是a[0]的地址,pa++不就是a[1]的地址了嘛,那么a[1]指向的是第二窜字符串的首元素地址,那结果就是at了!
在这里插入图片描述

在这里插入图片描述

笔试题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;
}

我们有了前几题的基础了,那么我们来想想这道题怎么写呢?
推荐大家画画图,容易理解哈,注意优先级哦!
在这里插入图片描述
这是没有执行前的图,我们先看第一个打印,有个cpp++,那么我们就得将cpp下移一个了看图:
在这里插入图片描述
所以结果就是POINT了!
看第二个打印,cpp又++了所以这次cpp指向了c+1,再次解引用前–了,所以指向了c,然后+3就是c指向的地址+3,那么就是首窜里E的地址了,打印结果就是ER
第三次打印前我们知道++ – 是改变了cpp的位置了而不是每次都会重新开始哦,我们再来了解一个东西,那就是在编译的过程中,arr[i]=(arr+i),arr[i][j]=(*(arr+i)+j),所以在第三次和第四次打印的时候不会改变cpp的位置,那么第三次第四次打印结果应该懂了吧!

第三次,cpp[-2]=(cpp-2)指向的是c+3
第四次,cpp[-1][-1]=
(*(cpp-1)-1),指向的是c+1.
我们来看结果:
在这里插入图片描述

觉得有用的话,留下三连吧!

  • 36
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 29
    评论
评论 29
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

相知-

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

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

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

打赏作者

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

抵扣说明:

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

余额充值