一次性过指针!!!


很多人第一眼看到指针时的表情

在这里插入图片描述
那么,指针到底是什么,请看下面!!!

初级指针

1. 指针是什么

指针是什么?

  • 在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。
  • 指针就是变量,用来存放地址的变量(存放内存单元的地址)
    在这里插入图片描述
    代码举例
#include <stdio.h>
int main()
{
 	int a = 10;//在内存中开辟一块空间
 	int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
   //将a的地址存放在p变量中,p就是一个之指针变量。
 	return 0; 
 }

我们知道了指针是用来存放内存单元的地址,但指针的大小我们还不知道,请接着看下面

一个小的单元到底是多大的字节那?

答案:一个字节

编址过程
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电/负电(1或 者0)
那么32根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001

111111111 111111111 111111111 111111111
每个地址标识一个字节,这里就有(2^32Byte == 2^32/1024KB ==
232/1024/1024MB==232/1024/1024/1024GB == 4GB) 4G的空闲进行编址。
64位机器类推
所以在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,在64位上地址就得用8个字节的空间来存储。
所以在32位机器上,指针占4个字节,64位机器上,指针占8个字节。

2. 指针和指针类型

首先,C语言有多种不同的内置数据类型,当然也有相关指针的类型,指针类型如下:

char  *pc = NULL; //指向char型的指针
int   *pi = NULL; //指向整形的指针
short *ps = NULL; //指向short型的指针
long  *pl = NULL; //指向long型的指针
float *pf = NULL; //指向单精度浮点型的指针
double *pd = NULL;//指向双精度浮点型的指针

那指针类型存在的意义是什么?

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

看下面的例子:

#include <stdio.h>
//演示实例
int main()
{
	 int n = 10;
	 char *pc = (char*)&n; //这里截断,数据存储里面讲到了的
	 int *pi = &n;
	 
	 printf("%p\n", &n);//n的地址
	 printf("%p\n", pc);//n的一个字节的地址
	 printf("%p\n", pc+1);//往下一个地址走
	 printf("%p\n", pi);//n的地址
	 printf("%p\n", pi+1);//n下一个内存空间的地址,跟n一样(4个字节)
	 return  0; 
}

指针的解引用

//演示实例
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 int *pi = &n; 
 *pc = 0;   //重点在调试的过程中观察内存的变化。
 *pi = 0;   //重点在调试的过程中观察内存的变化。
 return 0; }

在这里插入图片描述
通过调试可以看到内存地址的一些变化。(根据类型不同内存地址会有变化)
总之:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。

3. 野指针

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

  • 1.指针未初始化
#include <stdio.h>
int main()
{ 
	int *p;//局部变量指针未初始化,默认为随机值
    *p = 20;
	return 0; 
}
  • 2.指针越界访问
#include <stdio.h>
int main()
{
    int arr[10] = {0};
    int *p = arr;
    int i = 0;
    for(i=0; i<=11; i++)
   {
        //当指针指向的范围超出数组arr的范围时,p就是野指针(越界相当于未初始化)
        *(p++) = i;
   }
    return 0; 
}
  • 3.指针指向的空间释放
	char *Ptr = NULL;
	Ptr = (char *)malloc(100 * sizeof(char));
	if (NULL == Ptr) //检查指针的有效性
	{
	exit (1);
	}
	//如果不继续使用该指针,需要对char型Ptr指针进行的空间释放
	//加上free(Ptr)释放空间,最后置为NULL,避免形成野指针
	//如果没加,就会造成指针指向位置不明确,也就成了野指针。

如何避免野指针

  • 1.指针初始化
  • 2.小心指针越界访问
  • 3.指针指向空间释放即使置NULL
  • 4.指针使用之前检查有效性

4. 指针运算

指针±整数

	float values[5];
	float *vp=NULL;
	//指针+-整数;指针的关系运算
	for (vp = &values[0]; vp < &values[5];)
	{
	     *vp++ = 0; }
	//这里通过指针的+整数,将values数组的每个值都初始化为0了

指针-指针

int my_strlen(char *s) {
       char *p = s;
       while(*p != '\0' )
              p++;
       return p-s; }//通过p指针找到s指针的末尾,然后p-s计算字符串s的长度

指针的关系运算

for(vp = &values[4]; vp >= &values[0];vp--)
{
    *vp = 0; }

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

5. 指针和数组

数组名是数组首元素的地址

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

结果显示如下:
在这里插入图片描述
那么我们可以通过指针来访问数组中的元素

	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	int* p = arr;//通过指针指向数组首元素地址
	int len = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < len; i++)
	{
		printf("%d %p\n", *(p + i),p + i);
	}

通过p指针输出结果:
在这里插入图片描述
地址显示:
在这里插入图片描述
这里的p+i,虽然i是增加的1,但内存地址中其实是跳的4个字节(也就是一个整形),而非这里的 i ,所以才能依次访问数组中的元素(注意区分!!!)
所以 p+i 其实计算的是数组 arr 下标为i的元素的地址

6. 二级指针

指针变量也是变量,也有地址,所以二级指针用来存放指针变量的地址。
在这里插入图片描述

	int a = 10;
	int* p = &a;//存放a的地址
	int** pa = &p;//存放指针变量p的地址
	*pa = &a;
	**pa = 20;
	//等价于*p = 30;
	//等价于a = 30;
	printf("%d %d %d", a, *p, **pa);
  • *pa通过对pa的地址进行解引用,这样找到的是p,*pa就是访问的p
  • **pa先通过*pa找到p,然后对p进行解引用操作:*p,找到的就是a
    在这里插入图片描述

指针的进阶

通过上面的讲解,我们知道了以下的内容:

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
  2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
  3. 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
  4. 指针的运算。

1. 字符指针

我们知道了整形指针,那么上面是字符指针那?当然就是char*定义的指针。

int main()
{
	char a='w';
	char *p=&a; //是一个字符指针,取a的地址
	return 0;}

还有一种使用方式是最容易出错的!!!

int main()
{
	char arr[] = "hello world!";
	char* s = arr;
	printf("%s",s);
	printf("%p\n", &arr[0]);
	printf("%p", s);
	return 0;}

这里很多人会认为把hello world!放到字符指针s中,其实是把第一个字符,即h的地址放入字符指针s中。
在这里插入图片描述
上面所写就是为了表示常量字符串的第一个字符的地址放入到指针变量s中。
那一起来看看下面的面试题吧!!!

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

结果如下:
在这里插入图片描述
原因:其实str3和str4指向的是同一个常量字符串,C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,其实它们会指向同一块内存,但是相同的字符串去初始化不同的数组就会开辟出不同的内存区域,所以str1与str2不同,str3与str4相同。

这里要注意的是:c++中,定义是const char *s1=“hello bit.”,因为"hello bit."是常量字符串,c++风格严谨,所以左值也必须为常量字符串类型,即const char *s1。这里需要与c区别分开!!!

2. 指针数组

指针数组,顾名思义,是一个存放指针的数组。
比如:
int *arr1[10]; 存放整形指针的数组,每个元素都是一个int *指针
char *arr2[10]; 存放字符指针的数组,每个元素都是一个char *指针
其他类型一样类推,double *arr3[10];float *arr4[10];等等
来看看下面的例子加深理解:

int main()
{
	char a[] = { 'a','b','c','d','e' };
	char b[] = { 'b','c','d','e' ,'f'};
	char c[] = { 'c','d','e' ,'f' ,'g' };
	//a,b,c首元素的地址为字符数据的地址,所以可以用char*指针来接受
	char* arr1[3] = { a,b,c };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%c ", *(arr1[i] + j));
		}
		printf("\n");
	}
	return 0;
}

在这里插入图片描述

3. 数组指针

数组指针:能够指向数组的指针。
类型为:int (*p)[10]; //int可以换成其他类型,这里以int举例。
*先与p结合,表示p是一个指针变量,然后指向一个大小为10个整形的数组。所以p是一个指针,指向一个数组,叫数组指针。

[ ]的优先级要高于 * 号的,所以必须加上()来保证p先和 * 结合。

为了加深对数组指针的理解,我们先来了解一下数组名的问题!!!

&数组名VS数组名

  • &数组名,表示的是数组的地址,而不是数组首元素的地址。
  • 数组名,表示的数组首元素的地址。

先来看看例子加深一下印象吧!!!

int main()
{
	int arr[10] = { 0 };
	printf("arr的地址:%p\n", arr); 
	printf("&arr的地址:%p\n\n", &arr);
	printf("arr+1的地址:%p\n", arr+1);
	printf("&arr+1的地址:%p", &arr+1);
	return 0;

通过运行图,可以看到,arr和&arr都是表示的第一个元素的地址,但arr+1表示的是第二个元素的地址006FFD18 - 006FFD=14,而&arr+1表示的是整个数组的地址+1,006FFD3C - 006FFD14=40

结论:数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40。
在这里插入图片描述
数组指针的使用
我们知道了,数组指针指向的是数组,那么数组指针中应该存放的是数组的地址。
那么,我们就可以写出下面的代码举例了。

#include <stdio.h>
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
    //我们便可以通过数组指针来对arr数组进行访问了
    for(int i=0;i<10;i++)
    {
    	printf("%d ",(*p)[i]);
    }
    return 0; }

当然,输出结果肯定为arr数组的内容
在这里插入图片描述
通过上面的例子,我们知道了,数组指针本就是存放数组的指针,那么我们也可以用于二维数组,来表示第一行的地址。

void print_arr(int(*arr)[5], int row, int col) {
	int i = 0;
	for (i = 0; i < row; i++)
	{
		int j = 0;
		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); //将第一行的地址传给数组指针(*arr)[5]
	return 0;
}

照样输出二维数组arr的结果
在这里插入图片描述
所以,我们这里又学会用数组指针作为参数,来对二维数组进行输出了。但需要注意的是:数组指针存放的是数组的地址!!!

看完指针数组和数组指针,在看这里的代码,进行理解

	int arr[5]; //整形数组,存放整形数据,每个元素是int
	int *parr1[10];//指针数组,存放整形指针的数组,每个元素都是int*
	int (*parr2)[10];//数组指针,存放数组的指针,每个元素都是int
	int (*parr3[10])[5];
//是一个存放数组指针的数组,有10个数组指针,每个数组指针指向一个数组,数组大小为5

4. 数组传参和指针传参

1. 一维数组传参

#include <stdio.h>
void test(int arr[])//ok,省略了长度,本就是一维数组地址传参过来,当然可以
{}
void test(int arr[10])//ok,相同的类型传参,肯定可以
{}
void test(int *arr)//ok,一级指针传参,也是可以的,指向数组首元素地址
{}
void test2(int *arr[20])//ok,都是指针数组,对的
{}
void test2(int **arr)//ok,传参给二级指针,arr2是指针数组,每个元素都是int *,所以一级指针的地址传参给二级指针是正确的
{}
int main()
{
	 int arr[10] = {0};
	 int *arr2[20] = {0};
	 test(arr);
	 test2(arr2);
}

2. 二维数组传参

void test(int arr[3][5])//ok,相同类型,可以
{}
void test(int arr[][])//no,省略了第二个[]的数字,不知道一行多少元素,错误
{}
void test(int arr[][5])//ok,省略第一个[]数字,保留了第二个[]数字,是可以的
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//所以第二个[]的数字不能省略。
void test(int *arr)//no,二维数组首元素的地址其实是第一行元素的地址,也就是一维数组,可以用数组指针进行保存,
{}
void test(int* arr[5])//no,这是一个指针数组,每个元素是int *,不是int,所以错误
{}
void test(int (*arr)[5])//ok,二维数组首元素地址是第一行的数据,所以就是有五个元素的数组,数组指针就是指向有几个元素的数组的,所以正确
{}
void test(int **arr)//no,一维数组地址跟二级指针不匹配,错误
{}
int main()
{
	 int arr[3][5] = {0};
	 test(arr);
}

3. 一级指针传参

#include <stdio.h>
//一级指针上面讲了的,这里就是用指针变量p指向一维数组arr首元素的地址
//然后依次对arr数组进行遍历
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; }

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);//取一级指针地址给二级指针**ptr
	 //这里有不懂的地方,去看上面的二级指针
	 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);
//Ok,这里传的是指针数组arr首元素的地址,由于每个元素都是char *
//所以取一级指针地址是可以传参给二级指针**p的
//二维数组传参也涉及到了的
	 return 0; }

总之一句话,你实参传的是什么,你形参就要写相同的类型,反过来也就是形参写的什么,实参也要传相同的类型!!!

5. 函数指针

函数指针是为了有时候方便我们调用函数,比如下面涉及到的转移表。
作用:1、传递参数给另一个函数;2、使用转移表简化代码;

大家首先来看一段代码

#include <stdio.h>
void test()
{
	 printf("hehe\n");
}
int main()
{
	 printf("%p\n", test);//取的函数名,也就是函数地址
	 printf("%p\n", &test);//取函数地址,是整个函数的地址
	 //这里需要注意 test!=&test;
	 return 0; }

代码中涉及到函数的地址,那我们如何去保存函数的地址,那就需要用到函数指针了,函数指针是用来保存函数地址的。

void test()
{
 	printf("hehe\n");
}
void (*pfun1)();//函数指针的写法
//*先与pfun1结合,说明pfun1是一个指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void

我们再来进一步了解下函数指针和函数,先看代码块!!!

#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 (*p1)(int, int) = add;//取add函数地址
	int (*p2)(int, int) = sub;//取sub函数地址
	int (*p3)(int, int) = mul;//取mul函数地址
	int (*p4)(int, int) = div;//取div函数地址
	int x = 10, y = 5;
	int ret = 0;
	//1、原来的调用方法是通过函数名传参进行调用,如下
	ret = add(x,y); //这样进行调用
	//2、函数指针调用函数方法
	ret = (*p1)(x, y);//通过函数指针p1调用add函数
	printf("%d\n", ret);
	ret = (*p2)(x, y);//通过函数指针p2调用sub函数
	printf("%d\n", ret);
	ret = (*p3)(x, y);//通过函数指针p3调用mul函数
	printf("%d\n", ret);
	ret = (*p4)(x, y);//通过函数指针p4调用div函数
	printf("%d\n", ret);
	return 0;
}

在这里插入图片描述
上面代码看完了,再来看看套娃的函数指针,加深理解,下面代码在《C陷阱和缺陷》这本书中涉及到。

//代码1 
(*(void (*)())0)();
//调用0地址处的函数,该函数无参,返回类型为void
//1、void (*)() - 函数指针类型
//2、(void (*)())0 - 将0进行强制类型转换,被解释为一个函数的地址
//3、*(void (*)())0) - 对0地址进行了解引用操作
//4、(*(void (*)())0)() - 调用0地址处的函数
//代码2
void (*signal(int , void(*)(int)))(int);
//1、signal先与()结合,说明signal是一个函数
//2、signal函数第一个参数类型为int,第二个参数类型为函数指针
//该函数指针,指向一个参数为int,返回值为void的函数
//3、signal含糊的返回类型也是一个函数指针
//该函数指针,指向一个参数为int,返回值为void的函数
//signal是一个函数的声明

//代码2的简化
typedef void(*pfun)(int);
//对函数指针类型重命名
pfun signal(int,pfun);

6. 函数指针数组

上面说了函数指针的用处,那么到底是如何用的,先来看看下面的代码块。
函数指针数组写法:void (*p[5])(void x,void y) ,void根据函数类型更改
函数指针数组的作用:用来保存函数的地址

  • (看转移表)
#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)[10])(const char*) = &pfunArr;
 return 0; }

简单来说,就是函数指针数组的指针指向函数指针数组函数指针数组又指向函数指针

8. 回调函数

回调函数概念:回调函数就是一个通过函数指针调用的函数。
如果你将函数指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其指向的函数时,我们就说这是回调函数。(下面以qsort排序的例子进行讲解!!!)
这里先讲解qsort函数:
在这里插入图片描述
比较两个元素的函数:

  • 类型:int comparator ( const void * elem1, const void * elem2 );
  • 函数必须接受两个指向元素的指针形参,形参类型转换为void*。这些参数应该转换为某种数据类型并进行比较
  • 这个函数的返回值应该通过return返回,一个负数,零或一个正值来表示是否认为eleml小于,等于,或大于lem。

这里对qsort函数不清楚的可以去查手册或者去cppreference网站查

#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);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0; }

qsort的原理理解了,再来理解回调函数的本质!!!

//以冒泡排序为例子
#include <stdio.h>
int cmp(const void * p1, const void * p2) {
  	return (*( int *)p1 - *(int *) p2);
}
void _swap(void *p1, void * p2, int size) {
    int i = 0;
    for (i = 0; i< size; i++)
   {
        char tmp = *((char *)p1 + i);
       *(( char *)p1 + i) = *((char *) p2 + i);
       *(( char *)p2 + i) = tmp;
   }
}
//模拟实现qsort的本质
//函数原型:void qsort(void * base,size_t num,size_t size, int(* comparator)(const void *,const void *));
void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{
    int i = 0;
    int j = 0;
    for (i = 0; i< count - 1; i++)
   {
       for (j = 0; j<count-i-1; j++)
       {
       		//调用cmp函数,进行两个元素大小的比较,由于不知道比较是什么类型,这里以char*类型进行比较,一个字节一个字节进行比较
            if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0)
           {
           		//这里同上,不知道数据类型是什么,只能一个字节的去进行比较,所以传参的时候,不仅需要传两个函数的地址,也要该数据类型大小,字节一个一个的进行交换。
               _swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
           }
       }
   }
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;
    //这里同qsort库函数
    bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0; }

当然,除了对整数进行排序,也可以对字符串,结构体等进行排序,直接上代码了,不懂的地方大家在下方评论哦!

struct MyStruct
{
	char arr[10];
	int age;
};
int sort_byage(const void* e1, const void* e2)
{
	return ((MyStruct*)e1)->age - ((MyStruct*)e2)->age;
}
int sort_byname(const void* e1, const void* e2)
{
	return strcmp(((MyStruct*)e1)->arr, ((MyStruct*)e2)->arr);
}
int main()
{
	MyStruct s[3] = { {"zs",1},{"ls",9},{"ww",5} };
	//qsort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), sort_byage);
	qsort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), sort_byname);
	for (int i = 0; i < 3; i++)
	{
		printf("%s %d\n", s[i].arr, s[i].age);
	}
	return 0;
}

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

这里是一些关于指针和数组的面试题,可能会有点难度,大家一定要先理解清楚上面的内容,在来看这里的题,或者得不偿失,希望大家能更上一层楼!!!

//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a)); //16,数组名单独在sizeof里,计算是整个数组的大小
printf("%d\n",sizeof(a+0));//4,数组名没有单独在sizeof里面,表示首元素地址
printf("%d\n",sizeof(*a));//4,这里为首元素,类型大小为4个字节
printf("%d\n",sizeof(a+1));//4,同第二个
printf("%d\n",sizeof(a[1]));//4,为第二个元素,类型大小为4个字节
printf("%d\n",sizeof(&a));//4,取的整个数组的地址
printf("%d\n",sizeof(*&a));//16,取的是整个数组的地址,然后进行解引用,整个数组大小为16
printf("%d\n",sizeof(&a+1));//4,&a:整个数组地址,+1刚好指向数组的尾部,也是一个地址
printf("%d\n",sizeof(&a[0]));//4,取第一个元素的的地址
printf("%d\n",sizeof(&a[0]+1));//4,取第一个元素的地址,在+1,指向下一个元素的地址
//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));//6,整个数组大小
printf("%d\n", sizeof(arr+0));//4,表示第一个元素的地址
printf("%d\n", sizeof(*arr));//1,取第一个元素地址,然后解引用,为第一个元素的字节大小
printf("%d\n", sizeof(arr[1]));//1,第二个元素字节大小
printf("%d\n", sizeof(&arr));//4,整个数组的地址
printf("%d\n", sizeof(&arr+1));//4,第二个元素的地址
printf("%d\n", sizeof(&arr[0]+1));//4,第二个元素的地址

printf("%d\n", strlen(arr));//随机值,不知道在内存何时遇到'\0'
printf("%d\n", strlen(arr+0));//同上
printf("%d\n", strlen(*arr));//错误,strlen库函数原型:strlen(char *str),传参类型不匹配,错误
printf("%d\n", strlen(arr[1]));//同上
printf("%d\n", strlen(&arr));//随机值,上面strlen形参为字符指针,虽然传过去的是整个数组的地址,但strlen只会取第一个元素的地址,所以也是从第一个元素的地址往后数
printf("%d\n", strlen(&arr+1));//随机值-6,跳过整个数组,同上
printf("%d\n", strlen(&arr[0]+1));//随机值-1,从第二个元素开始,同上
//字符串
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//7,遇到'\0'停止,sizeof会计算'\0'
printf("%d\n", sizeof(arr+0));//4,arr没有单独放在sizeof内,这里为首元素地址
printf("%d\n", sizeof(*arr));//1,数组首元素字节大小
printf("%d\n", sizeof(arr[1]));//1,第二个元素字节大小
printf("%d\n", sizeof(&arr));//4,整个数组的地址
printf("%d\n", sizeof(&arr+1));//4,跳过整个数组,但还是表示地址
printf("%d\n", sizeof(&arr[0]+1));//4,第二个元素的地址

printf("%d\n", strlen(arr));//6,整个数组的长度
printf("%d\n", strlen(arr+0));//同上
printf("%d\n", strlen(*arr));//错误,传参类型不同
printf("%d\n", strlen(arr[1]));//同上
printf("%d\n", strlen(&arr));//6,传参过去,strlen库函数会从首元素地址进行计算,遇到'\0',所以为6
printf("%d\n", strlen(&arr+1));//随机值,跳过字符串,然后从'\0'往后面数,在内存中不知道何时在遇到'\0'
printf("%d\n", strlen(&arr[0]+1));//5,从第二个元素开始数,直到遇到'\0'

char *p = "abcdef";
printf("%d\n", sizeof(p));//4,上面讲到,这里*p其实存放的是首元素的都在
printf("%d\n", sizeof(p+1));//4,第二个元素地址
printf("%d\n", sizeof(*p));//1,首元素字节大小
printf("%d\n", sizeof(p[0]));//同上
printf("%d\n", sizeof(&p));//4,取首元素的地址
printf("%d\n", sizeof(&p+1));//4,第二个元素地址
printf("%d\n", sizeof(&p[0]+1));//4,第二个元素地址

printf("%d\n", strlen(p));//6,从首元素开始数
printf("%d\n", strlen(p+1));//5,从第二个元素开始数
printf("%d\n", strlen(*p));//错误,strlen传参类型不符合
printf("%d\n", strlen(p[0]));//错误,传参不符合
printf("%d\n", strlen(&p));//随机值,在p指针中存的是什么根本不知道,内存好也不知道
printf("%d\n", strlen(&p+1));//随机值,p指针后面存的什么也不知道
printf("%d\n", strlen(&p[0]+1));//随机值-1

在这里插入图片描述

//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));//48,整个数组大小,3*4*4=48
printf("%d\n",sizeof(a[0][0]));//4,首元素字节大小
printf("%d\n",sizeof(a[0]));//16,表示的是第一行元素的大小
printf("%d\n",sizeof(a[0]+1));//4,第一行第二个元素的地址,
printf("%d\n",sizeof(*(a[0]+1)));//4,第一行第二个元素字节大小
printf("%d\n",sizeof(a+1));//4,a没有单独放在sizeof内,表示第一行元素地址,+1表示第二行的地址
printf("%d\n",sizeof(*(a+1)));//16,第二行元素大小
printf("%d\n",sizeof(&a[0]+1));//4,&a[0]表示第一行地址,+1表示第二行地址
printf("%d\n",sizeof(*(&a[0]+1)));16,第二行元素的大小
printf("%d\n",sizeof(*a));//16,a表示第一行元素,所以这里为第一行元素大小
printf("%d\n",sizeof(a[3]));//16,a[3]其实是第四行元素的数组名,所以这里为16

总结 数组名的意义:

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
  3. 除此之外所有的数组名都表示首元素的地址。

面试题,重点!!!
笔试题1:

int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1); //&a表示整个数组地址,+1跳过整个数组,指向数组的尾端
    printf( "%d,%d", *(a + 1), *(ptr - 1));
    //a表示首元素地址,+1第二个元素地址,(a+1)通过*解引用,结果为第二个元素值
    //ptr本指向数组尾端,-1指向第五个元素地址,(ptr-1)通过*解引用,结果为第五个元素值
    return 0; }
//程序的结果是什么?
//结果:2  5

笔试题2:

结构体的大小是20个字节
struct Test
{
	 int Num;
	 char *pcName;
	 short sDate;
	 char cha[2];
	 short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知结构体Test类型的变量大小为20个字节
int main()
{
	 printf("%p\n", p + 0x1);
	 //0x100014,+1相当于+20
	 printf("%p\n", (unsigned long)p + 0x1);
	 //0x100001,强制类型转换为无符号整形,+0x1相当于+整数1
	 printf("%p\n", (unsigned int*)p + 0x1);
	 //0x100004,强制类型转换为无符号整形,+0x1相当于+1个整形
	 return 0; }

笔试题3:

int main()
{
    int a[4] = { 1, 2, 3, 4 };
	int* ptr1 = (int*)(&a + 1);//ptr1指向数组尾端
	int* ptr2 = (int*)((int)a + 1);
	//这里假设为小端存储,将a转换为整形,+1就执行第二个字节,ptr2就指向第二个字节
	//小端存储不知道的,看我主页博客:数据在内存中的存储
	printf("%x,%x", ptr1[-1], *ptr2);
	//01 00 00 00 | 02 00 00 00 | 03 00 00 00 | 04 00 00 00
	//ptr[-1]相当于*(ptr1-1),所以ptr1指向04的前面,也就是第四个元素
   //ptr2这里指向01后面,*ptr2就是 00 00 00 02,但在内存中是0x02 00 00 00,所以输出为2000000
    return 0; }

在这里插入图片描述
笔试题4:

#include <stdio.h>
int main()
{
	//注意这里是逗号表达式,取最大值,所以数组a真实值为{1,3,5};
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    int *p;
    p = a[0];
    printf( "%d", p[0]);//所以这里为1
 	return 0; }

笔试题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]);
    //结果:FFFFFFFC,-4
    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));
    //ptr1-1指向最后一个元素10,ptr-1指向第一行的最后一个元素5
    //结果为:10,5
    return 0; }

笔试题7:

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

在这里插入图片描述
笔试题8:

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

在这里插入图片描述
最后一道面试题,需要多思考,指针的指向,所以大家一定要先把上面的掌握了,在做题!!!
累成狗,看到的朋友记得三连,你们的鼓励就是我的动力呀!!!欢迎大家前来指教!!!
在这里插入图片描述

  • 35
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

雨轩(爵丶迹)

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

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

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

打赏作者

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

抵扣说明:

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

余额充值