连续熬夜爆肝C指针,你想知道的这里都有

以下内容知识量巨大,建议大家分次反复琢磨。

指针是什么?

在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。
我们用图来理解:
地址是按照十六进制形式来表示的
每个内存单元都对应着一个地址,指针存放的就是地址,所以指针本质上也就是地址,指针指向该内存单元。

指针

指针是个变量,存放内存单元的地址(编号)。
对应到代码:

#include <stdio.h>
int main()

{

 int a = 10;//在内存中开辟一块空间

 int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。

   //将a的地址存放在p变量中,p就是一个之指针变量。

 return 0; 
}

总结:指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
那这里的问题是:

一个小的单元到底是多大?(1个字节)

如何编址?

经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。

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

那么32根地址线产生的地址就会是:

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

11111111 11111111 11111111 11111111

这里就有2的32次方个地址。

每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==

2^32 / 1024 / 1024MB==2^32 / 1024 / 1024 / 1024GB == 4GB) 4G的空间进行编址。

同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,大家自行计算。

这里我们就明白:

在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。

那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

总结:

指针是用来存放地址的,地址是唯一标示一块地址空间的。

指针的大小在32位平台是4个字节,在64位平台是8个字节。

指针和指针类型

指针也是分类型的,不同类型的指针指向不同类型的元素

int num = 10; 
p = &num;

//要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢? 我们

给指针变量相应的类型。
char  *pc = NULL;

int   *pi = NULL;

short *ps = NULL;

long  *pl = NULL;

float *pf = NULL;

double *pd = NULL;

这里可以看到,指针的定义方式是: type + * 。 其实: char* 类型的指针是为了存放 char 类型变量的地址。 short* 类型的指针是为了存放 short 类型变量的地址。 int* 类型的指针是为了存放int 类型变量的地址。

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

指针±整数

#include <stdio.h>

//演示实例

int main()

{

 int n = 10;

 char *pc = (char*)&n;

 int *pi = &n;

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

 printf("%p\n", pc);

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

 printf("%p\n", pi);

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

 return  0; 
}

我们先看结果:
在这里插入图片描述
我们可以看到pc的值与&n的值相同,所以可以进一步确信指针是用来存放地址的,再看pc+1的结果,地址大小增加了1,而pi+1的结果地址大小增加了4。所以,不同类型的指针决定了向前或向后移动的距离有多大。
就比如不同身高的人迈一步的距离是不一样的,char* 的“身高”就比较矮,所以它跨过的距离就很小,而int* 的“身高”就比较高,跨过的距离就比char* 大得多。

指针的解引用
*是解引用操作符,可以通过它来通过指针存放的地址找的该内存单元,也可对其进行更改。
看下面代码:

int main()
{
	int a = 10;
	printf("%d\n", a);
	int* p = &a;
	*p = 20;
	printf("%d\n", a);
	return 0;
}

先看结果:
在这里插入图片描述
p是一个指针变量名,int* 代表的是它的类型,说明他是一个整形指针变量,就像int a = 10 ; a是一个变量名,int是a这个变量的类型,而*p则是对它存放的地址进行解引用来找到该内存单元。

演示实例

#include <stdio.h>

int main()

{

 int n = 0x11223344;

 char *pc = (char *)&n;//赋值要把n的地址进行强制类型转换,否则一个整型的地址与char*的指针不匹配

 int *pi = &n;

 *pc = 0;   //重点在调试的过程中观察内存的变化。

 *pi = 0;   //重点在调试的过程中观察内存的变化。

 return 0; 
}

这里把n的值赋成了一个十六进制数字,这样是为了我们更好观察它内存的存放,因为我们同样可以把内存的存放形式也设置成十六进制形式
看下图,我们先看未修改n的值时n在内存中的存放,可以看到是“倒着放的”,这是因为我们的机器采取的是小端存储,详情可以看作者的另外一条博客
大小端介绍
在这里插入图片描述
在内存中的存储情况如下图:
在这里插入图片描述

先看* pc = 0,pc是一个char类型的指针,只能访问一个字节的内存,顺着地址由低到高访问,44在最低的一个字节位,所以44被改成了0,而其他位仍然不变
如下图
在这里插入图片描述
我们通过调试也可以发现结果和上图相同
在这里插入图片描述

再看*pi = 0,pi是int型的指针,所以可以访问4个字节的内存,故n就全都被修改为0了
如下图
在这里插入图片描述

我们再看调试时n在内存中的改变:
在这里插入图片描述

总结: 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

野指针

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

  1. 指针未初始化
#include <stdio.h>

int main()

{ 

int *p;//局部变量指针未初始化,默认为随机值

*p = 20;

return 0; 
}
  1. 指针越界访问
#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; 
}
  1. 指针指向的空间释放

如何规避野指针

  1. 指针初始化

  2. 小心指针越界

  3. 指针指向空间释放及时置NULL

  4. 指针使用之前检查有效性

例:

#include <stdio.h>

int main()

{

    int *p = NULL;

    //....

    int a = 10;

    p = &a;

    if(p != NULL)//检查指针有效性

   {

        *p = 20;

   }

    return 0; 
}

指针运算

指针±整数

#define N_VALUES 5

float values[N_VALUES];

float *vp;

//指针+-整数;指针的关系运算

for (vp = &values[0]; vp < &values[N_VALUES];)

{

     *vp++ = 0;   先使用再++
}

指针加减一个整数,就是向前或向后移动,同理,指针加减整数本质上就是地址加减整数,所以地址加减整数的效果与此相同而移动的距离取决于指针(或地址)本身的类型,如果是char * (或char类型元素的地址) 加减一,则只能向前或向后访问一个字节,而如果是int * (或int类型元素的地址)加减一,则只能向前或向后访问四个字节,这一点在前面的指针类型我们也提到了。这一运算常用于访问数组元素,如上面代码所示。

指针(或地址)+ 1,这里的1的大小取决于指针(或地址)的类型,如果是个整型指针(或地址),1就表示一个整型的大小,如果是个数组指针(或地址),1就表示一个数组的大小。

请大家记住,指针即地址

指针-指针

指针减指针得到的是两个指针之间的元素个数(语法规定)

即arr[0]之后的元素(包括arr[0])一直到arr[end](不包括arr[end])

如下面的代码所示,不仅使用了指针加整数的运算,还进行了指针减指针的运算

int my_strlen(char *s) //实现的是计算字符串的长度
{

       char *p = s;//s表示数组首元素的地址

       while(*p != '\0' )//当p指向/0时,找到了数组的结尾,跳出循环

              p++;

       return p-s; 
}

我们再来看一个例子
在这里插入图片描述
解释:
在这里插入图片描述

指针加指针意义不大

指针的关系运算

指针比较的前提:两个指针指向同一块空间(数组中)

for(vp = &values[N_VALUES]; vp > &values[0];
//vp跳出循环时指向的数组的第一个元素
{

    *--vp = 0; )//将values数组中的元素全部赋值为0
}
代码简化, 这里将代码修改如下:

for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--) //vp跳出循环时指向的是位于数组之前的那个元素
{

    *vp = 0; 
}

因为在数组中,数组元素是按照地址从低到高连续存放的,所以地址可进行比较,也就是指针可以进行比较。

实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

标准规定:

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

我们用图来解释:
在这里插入图片描述

二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里? 这就是二级指针
如:
在这里插入图片描述
对于二级指针的运算有:

*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa

int a = 20; 

int *pa = &a;

int **ppa = &pa;

*ppa = &b;//等价于 pa = &a;

//**ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a
**ppa = 30;

//等价于*pa = 30;

//等价于a = 30;

二级指针点到即止,之后还有讲解到它的地方。

指针和数组

在了解指针与数组的联系之前,我们先了解一下数组名的意义

数组名的意义

先看一个例子:

#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);
    printf("%p\n", &arr[0]);

    return 0; 
}

结果为:
在这里插入图片描述

可见数组首元素的地址、数组的地址和数组名是一样的,我们先来区分数组首元素地址和数组的地址:
可以利用指针的加减运算来进行证明。
在这里插入图片描述
我们看到,当数组地址加1时,跳过大小的是40个字节即10个数组元素的大小(地址是十六进制的,两个地址相减得到40),而当数组首元素地址加1时,跳过的是4个字节即1个数组元素的大小,所以我们发现了他们的区别,两者虽然数值上一样,但表示的意义并不一样。

再来比较arr和&arr:
在这里插入图片描述

我们发现arr+1与arr相减也是4个字节即一个数组元素的大小,并且与&arr[0],&arr[0] + 1的结果相同,此时我们可以得出结论:数组名代表数组首元素的地址。

所以下面代码也就成立了:

int arr[10] = {1,2,3,4,5,6,7,8,9,0};

int *p = arr;//p存放的是数组首元素的地址

既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个数组就成为可能。

例如:

#include <stdio.h>

int main()

{

    int arr[] = {1,2,3,4,5,6,7,8,9,0};

    int *p = arr; //指针存放数组首元素的地址

    int sz = sizeof(arr)/sizeof(arr[0]);//计算数组元素个数

    for(int i=0; i<sz; i++)

   {

        printf("&arr[%d] = %p   <====> p+%d = %p\n", i, &arr[i], i, p+i);

   }

    return 0; 
}

结果为:
在这里插入图片描述
&arr[i] 与 p+i 的结果时是相同的

所以 p+i 其实计算的是数组 arr 下标为i的地址。

那我们就可以直接通过指针来访问数组。

如下:

int main()

{

 int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };

 int *p = arr; //指针存放数组首元素的地址

 int sz = sizeof(arr) / sizeof(arr[0]);

 int i = 0;

 for (i = 0; i<sz; i++)

 {

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

 }

 return 0; 
}

结果为:
在这里插入图片描述
所以可通过指针来访问数组元素。

我们再进行拓展,既然* (p + i) = arr[i],而p表示首元素的地址,而arr也表示首元素的地址,那* (p+i)也可以写成* (arr + i),同样,是不是也把arr[i]可以写成p[i]呢?还有,* (arr + i) 是不是又等同于 * (i+arr)=arr[i],那写成 i [arr],是不是也可以呢?
答案是:都可以。
我们来进行验证:

int main()

{

	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };

	int* p = arr; //指针存放数组首元素的地址

	int sz = sizeof(arr) / sizeof(arr[0]);

	int i = 0;

	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	for (i = 0; i < sz; i++)
	{
		printf("%d ", p[i]);
	}
	printf("\n");
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(i + arr));
	}
	printf("\n");
	for (i = 0; i < sz; i++)
	{
		printf("%d ", i[arr]);
	}
	printf("\n");
	return 0;
}

在这里插入图片描述
虽然事实摆在这里,大家可能也还是很难想象,为啥是这样呢?
其实,[ ]是一个地址操作符,arr[i]其实就等于*(arr+i),这样大家是不是就更好理解了,所以[ ]的前面不一定要是数组名,只要是地址都能被使用,当然,一般都是用于数组中,比如p代表数组首元素地址,也可以被结合,p[i]就等于* (p+i),且[ ]的里外也没有要求,因为都要被转化为*(arr+i)的形式进行编译。
使用p[i]这种写法时,应该注意:p当前指向的是数组首元素地址,否则当p指向arr[2]时,再使用p[i],实际上指向的就是arr[2+i]了。

以上提到数组名代表首元素的地址,但有两种情况不一样:
1.当用sizeof(arr)计算大小时,arr单独放在sizeof内部,表示的是整个数组,所以计算的也就是整个数组的大小
2.当使用&arr时,取出的是整个数组的地址,虽然数值上和数组首元素地址相同,但意义是不相同的。

学会利用指针引用数组元素及运算后,我们来看下面这些表达式:

	int a[10];
	int* p = a;

	//1、p++;
	// *p;  //p++使p指向下一元素a[1],再执行*p,则得到a[1]的值

	//2. *p++;  //++的优先级高于*,因此它等价于*(p++);先取p的值,再解引用p,最后使p自增1  
	//假设p指向a[i],
	//所以 *p++就相当于a[i++]
	//因此下面两个代码的效果就相同
	/*1.
	for (int i = 0; i < 10; i++, p++)
	{
		printf("%d ,*p");
	}
	
	2.
	for (int i = 0; i < 10; i++)
	{
		printf("%d ,*p++");
	}*/

	//3. *(p++)与*(++p)
	//前者先取*p的值,再使p自增1
	//后者是先使p自增1,再取*p的值
	//比如此时p指向a[0],若输出*(p++),则结果是a[0]的值,若输出*(++p),则结果是a[1]的值
	// *(p++)就相当于p[i++]
	// *(++p)就相当于a[++i]


	//4. ++(*p);
	//括号的优先级最高,p先与*结合得到a[0]的值,再使a[0]自增一
	//注意,这里a[0]自增1是指a[0]的值自增一,比如a[0]的值如果是1,自增后a[0] = 2
	//所以++(*p)的效果就相当于 ++a[i]

字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ;

一般使用:

int main()

{

    char ch = 'w';

    char *pc = &ch;

    *pc = 'w';

    return 0; 
}

还有一种使用方式如下:

int main()

{

    char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?

    printf("%s\n", pstr);

    return 0; 
}

代码 char* pstr = “hello bit.”; 特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr 里了,但是本质是把字符串 hello bit. 首字符的地址放到了pstr中。即把 ‘h’ 的地址放在了指针变量pstr中。

准确来说,这类指针应该叫做字符串指针。

我们来看一道题:

#include <stdio.h>

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

我们可能会认为输出结果为:
str1 and str2 are not same
str3 and str4 are not same

但事实真的如此吗?

来看结果:
在这里插入图片描述
这里str3和str4指向的是同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2相同,str3和str4不同。
因为常量字符串是不能被修改的,它本质上是个常量,所以为了节省空间,只开辟一块内存空间存放它即可,而数组中的字符串是可以进行修改的,因此数组str1和str2存在于内存的两块不同区域,故str1(首元素地址)和str2(首元素地址)不相等。

所以使用字符串指针变量时,是不能修改字符串内容的,只可对其引用。

指针数组

指针数组是指针还是数组?

答案:是数组。是存放指针的数组。

数组我们已经知道整形数组,字符数组。

如:

int arr1[5];

char arr2[6];
在这里插入图片描述

那指针数组是怎样的?

int* arr3[5];//是什么?

arr3是一个数组,有五个元素,每个元素是一个整形指针。

在这里插入图片描述

数组指针

数组指针的定义

数组指针是指针?还是数组?

答案是:指针。

我们已经熟悉: 整形指针: int * pint; 能够指向整形数据的指针。 浮点型指针: float * pf; 能够

指向浮点型数据的指针。

那数组指针应该是:能够指向数组的指针。

下面代码哪个是数组指针?

int *p1[10];

int (*p2)[10];

//p1, p2分别是什么?

p1是指针数组,p2是数组指针

解释:

	int (*p)[10];

解释:p先和*结合,说明p是一个指针变量,然后指针指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
附上图分析:
在这里插入图片描述

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

如何使用数组指针

数组指针的使用

那数组指针是怎么使用的呢?

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

我们看代码:

#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_arr1(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]);
       }
        printf("\n");
   }
}

void print_arr2(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_arr1(arr, 3, 5);
    //数组名arr,表示首元素的地址

    //但是二维数组的首元素是二维数组的第一行

    //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址

    //可以用数组指针来接收

    print_arr2(arr, 3, 5);
    return 0; 
}

print_arr2中,使用数组指针接收一个二维数组,为什么写成了接收一个含有10个整形元素的数组呢?

提到这里,我们就不得不讲一下关于二维数组的数组名了

二维数组的数组名实际上表示的是第一行的地址

我们来看一个二维数组

int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};

我们可以把这个二维数组看成含有三个元素的一维数组,这三个元素分别是第一行第二行第三行,所以arr就表示首元素地址,即第一行的地址,那arr+1就表示跳过一行元素,指向第二行
那 * arr又代表什么呢?
既然arr表示第一行的地址,那 * arr是不是就是找到了第一行呢,所以 * arr就表示第一行的数组名,即第一行第一列元素的地址, * arr <=> *(arr + 0) <=>arr [0],arr[0]是不是就表示这个数组的首元素了?而首元素又表示一个数组,所以它代表的也是一个数组名,即1的地址,再对其解引用,**arr就表示第一行第一列的元素1了,也就是 * ( * ( arr+0 ) + 0 ),即arr [0] [0]

所以arr就相当于&arr[0], * arr就相当于&arr[0][0]

这里建议大家花时间消化一下,需要自己思考一段时间后,才能对其理解的更加透彻。

再回到上面数组指针的例子:int (*arr)[5]
数组传参时,传递的是首元素地址,所以二维数组传参传递的就是第一行的地址,也就是一个一维数组的地址,所以用一个指向含有五个整形元素的数组指针来接收,而使用起来也就如同二维数组一样,arr[i][j]就可以表示数组里的元素了

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

int arr[5];

int *parr1[10];

int (*parr2)[10];

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

arr是个整型数组

parr1先与[10]结合,说明他是一个数组,而数组的类型即为int*,所以parr1是一个整形指针数组

parr2先与*结合,说明它是个指针,而指针的类型就是int [10],即存放10个整形元素的数组,所以parr2是一个数组指针

parr3先与[10]结合,说明它是一个数组,parr3[10]再与*结合,所以是一个存放数组指针数组,而每个数组指针又指向int [5],即含有五个整形元素的数组。所以parr3是一个存储数组指针的数组。

我们可以通过调试来观察这到底是个什么玩意儿

在这里插入图片描述
看图解:
在这里插入图片描述

数组与指针的传参

数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

一维数组传参

#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? 允许,实参实际上是指针数组首元素的地址
//即一级指针的地址,用二级指针可以接收一级指针的地址

{}

int main()

{

 int arr[10] = {0};

 int *arr2[20] = {0};

 test(arr);//传的是首元素地址

 test2(arr2);//传的是首元素地址

}

当以数组形式作为形参时,形参并不真的是一个数组,而是一个指针,这么定义只是为了体现传递给函数的实参是一个数组,而实参传参时真正代表的是首元素地址,所以用形参接收时,用指针接收就行了。

二维数组传参

void test(int arr[3][5])//ok?  可以,形参为二维数组可以接收作为二维数组的实参
                      //但形参实际上是指向首行数组的指针

{}

void test(int arr[][])//ok? 不可以,未定义列的大小
                      //二维数组作为形参需要知晓列的大小

{}

void test(int arr[][5])//ok?  可以,定义了列的大小
                    //实际上是指向首行列数为五的数组

{}

//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。

//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。

//这样才方便运算。

void test(int *arr)//ok?  不行,这是定义了一个整型指针
                         //无法接收首行数组的起始地址

{}

void test(int* arr[5])//ok?  不行,定义的是一个指针数组
                             //不能接收首行数组的地址

{}

void test(int (*arr)[5])//ok?  可以,定义的是一个指向含有5个整形元素的数组的指针
                            //可以接收首行数组的起始地址

{}

void test(int **arr)//ok?   不行,实参传递的是首行的地址
                               //不能用二级指针来接收

{}

int main()

{

 int arr[3][5] = {0};

 test(arr);

}

一级指针传参

#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函数能接收什么参数?   

void test2(char* p)

{}

//test2函数能接收什么参数?

结论:一级指针如 int* p 作为形参时,能接收的实参是整形变量的地址、一级整型指针变量、整型数组首元素地址(数组名)
而如 char*p 能接收的实参是字符变量地址、一级字符指针变量、字符数组首元素地址(数组名)、字符串首元素地址(即字符串本身)。

其他的都与int类型差不多,只不过一级字符指针多了个字符串类型

在定义指针变量时,如果写成 int * p1, p2, 则p1是整型指针变量,p2是整型变量。
若想都定义为指针,需写成 int * p1, *p2

二级指针传参 ——接收的是一级指针的地址

#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);//Ok?  数组首元素的地址即字符指针的地址,可以

 return 0; 
}

结论:形参为二级指针变量时,可接收的实参是二级指针,一级指针地址,指针数组首元素地址(数组名即一级指针的地址)

函数指针

我们先来看一段代码

#include <stdio.h>

void test()

{

 printf("hehe\n");

}

int main()

{

 printf("%p\n", test);

 printf("%p\n", &test);

 return 0; 
}	

结果:
在这里插入图片描述
输出函数名的结果和输出函数名地址的结果相同,所以我们得出:

函数名代表函数的地址,而&函数名也代表的是函数的地址
注意:函数名并不可以进行运算,这一点与数组不同。

输出的是两个地址,这两个地址是 test 函数的地址。 那我们的函数的地址要想保存起来,怎么保存呢?

我们自然就想到了指针,那么函数指针的形式是怎样的呢?

举例:
函数add:

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

而表示add函数的指针则为:
int (* p) ( int, int ) = &add(或者直接写add,都表示函数的地址)

在这里插入图片描述
再来看一段代码:

void test()

{

 printf("hehe\n");

}

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

void (*pfun1)();

void *pfun2();

首先,能够存储地址,就要求pfun1或者pfun2是指针,那哪个是指针? 答案是:
pfun1可以存放。pfun1先和 * 结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
而pfun2表示的是pfun2的函数声明,pfun2先和( )结合,故表示的是一个返回类型为void *,无参的函数。

那如何使用函数指针调用函数呢?
以上面的add函数为例:
(*p)(2,3),此时等价于add(2,3)
返回结果即为2+3=5
也可以写成p(2,3)
因为函数名本来表示的就是一个地址,故p(2,3)也等价于add(2,3)

所以函数指针解不解引用效果都是一样的,写出解引用的符号只是为了我们能更直观地理解它。但加上 * 时记得用括号括起来,否则p会先与(2,3)结合,导致语法错误。

为了说明 * 的作用是无效的,我们看下图:
在这里插入图片描述
但是也仅限于函数指针可以这么使用,其它类型的指针要使用所指向的内存都需要加上 *。

接下来我们阅读两段有趣的代码:

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

//代码2

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

建议大家拿纸出来画一画,先自己思考一下它们表示的是什么

解释:

代码1是一个函数的调用

在这里插入图片描述

大家可能会奇怪,为啥0还能被当成函数地址,0地址不是不能被使用吗?
其实,这串代码出自一本书,叫做《C陷阱与缺陷》
在这里插入图片描述
所以这是用硬件设置好的,我们只需要明白这是一个函数地址就行
我们也可以这么使用,但不能是任意一个数字,必须是这个数字本就表示一个函数的地址,才可以这么这么写代码。

单独拿出这个地址,它只是一串数字,需要经过强制类型转换成为一个地址,才能够找到相应的函数。

我们再对其进行拆分
在这里插入图片描述

代码2是一个函数的声明,同样也出自《C陷阱与缺陷》,希望大家有时间能够仔细阅读这本经典书籍。

在这里插入图片描述
我们同样再进行拆分:
在这里插入图片描述

我们可能会觉得,返回类型为函数指针的话,应该写成:

void(*)(int) signal(int , void(*)(int))

但语法是不允许这么写的

语法规定就是上图中的写法,所以看起来眼花缭乱。
但我们可以用typedef进行简化
用typedef进行类型重定义:

 typedef void(*pfun_t)(int);//注意,重命名的名字也应写在里面,也是语法规定

  pfun_t signal(int, pfun_t);//等价于void(*)(int) signal(int , void(*)(int))

此时就可以把函数指针返回类型写在函数前面了
但在这里我们应该注意:

typedef int* pi_t;
#define PINT_T int*//这两个效果并不一样
int* p1,p2;  //p1为指针,p2为整型
pi_t p3,p4;  //p3,p4均为指针
PINT_T p5,p6;  //p5为指针,p6为整型

第三行和第五行作用效果相同。

函数指针数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组, 比如:

int *arr[10];
//数组的每个元素是int*

那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

来看一下下面的代码:

int (*parr1[10])();

int *parr2[10]();

int (*)() parr3[10];

答案是:parr1。 parr1 先和 [] 结合,说明parr1是数组,数组的内容是什么呢? 是 int (*)() 类型的函数指针。

用图分析:
在这里插入图片描述
那parr2和parr3呢?

parr2是想定义一个函数数组,但c语言中是不允许的,只可定义函数指针数组

而parr3则是想定义成函数指针数组,只不过把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 }; //转移表
      //因为我们输入的是1234来选择计算方法,而数组下标是从0开始的
     //所以把第一个元素设置为0,其他函数往后。     

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

所以函数指针数组就是把一些相同返回类型、参数类型的函数放在数组里以方便进行调用。

指向函数指针数组的指针

指向函数指针数组的指针是一个 指针,指针指向一个 数组 ,数组的元素都是 函数指针 ;

如何定义?

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

在这里插入图片描述
我们仍然通过调试来看看它又是个啥玩意儿

在这里插入图片描述

学习到这里,对函数指针的学习已经够深入了,所以不必再往下深究,指向函数指针数组的指针实际使用也很少。

回调函数

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

首先演示一下qsort函数(快速排序函数)的使用:
我们在MSDN中查找一下qsort函数所需要的参数:
在这里插入图片描述
在这里插入图片描述
所以我们知道了qsort函数的四个参数,而第四个参数即为一个函数指针,利用它来进行回调compare函数,我们再接着看:

在这里插入图片描述

compare函数是需要我们自己提供的,用来实现判断方法,不同数组类型的判断方法不一样,所以我们要自己实现,并且qsort实现的是升序排序,所以要调用真正的排序,elem1的大小需大于elem2的大小,此时函数返回值大于0
类似地,若两个元素相等,返回值等于0,若elem1小于elem2,返回值小于0。

如果想实现降序,可以把compare函数里的elem1 - elem2调换顺序,即elem2 - elem1,这时如果elem2大于elem1,就会交换顺序最后实现降序排列

我们发现,为什么这里使用的都是void* 指针?
void * 指针是无具体类型的指针,可接受任意数据类型的指针,但不能直接进行加减整数等操作,并不能直接进行解引用。
因为要接收的数组类型我们不能确定,可能是整型,也可能是浮点型,所以只能用void* 来接收所有类型的元素。
并且下面我们在使用void*指针进行比较时,都会先进行强制类型转换,这也是为什么compare函数需要我们自行设计。

下面我们先使用qsort的排序,再自行实现qsort。

#include <stdio.h>

//qosrt函数的使用者得实现一个比较函数

int cmp1(const void * p1, const void * p2) //提供的是整型数组元素大小的比较方式
{

  return (*( int *)p1 - *(int *) p2);
  //强制类型转换为int*,说明比较的是整型元素
  //p1,p2接收的是对应元素的地址
  //如果返回值大于0,则两元素进行交换
  }

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), cmp1);
    //sizeof(arr) / sizeof(arr[0])即数组元素个数
    //sizeof (int)即数组元素大小

    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)

   {

       printf( "%d ", arr[i]);

   }

    printf("\n");

    return 0; 
}

看结果:

我们还可以设计其他类型的比较函数,如浮点型:

int cmp2(const void* elem1, const void* elem2)
{
	return	 (*(float*)elem1 - *(float*)elem2);
}

还有结构体类型:

int cmp3(const void* elem1, const void* elem2)
{
	return  (strcmp(((struct stu*)elem1)->name, ((struct stu*)elem2)->name));//比较结构体成员中的字符串类型
}
int cmp4(const void* elem1, const void* elem2)//比较结构体成员中的int类型
{
	return  (((struct stu*)elem1)->num - ((struct stu*)elem2)->num);
}

结构体一定要创建在比较结构体大小函数之前,否则识别不出。

使用strcmp函数比较的是字母的顺序先后,而不是字符串长度

在设计比较函数时需要注意,compare函数的返回类型,参数类型都必须与qsort规定的一致,参数都记得加上const!

使用回调函数,模拟实现qsort(采用冒泡的方式)。

#define  _CRT_SECURE_NO_WARNINGS  1

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int cmp1(const void* p1, const void* p2) //提供的是整型数组元素大小的比较方式
{
	return (*(int*)p1 - *(int*)p2);
}

int cmp2(const void* elem1,const  void* elem2) //提供的是浮点型数组元素大小的比较方式
{
	return	 (*(float*)elem1 - *(float*)elem2);
}

struct stu2 {
	char name[20];
	int age = 0;
};
int cmp3(const void* elem1, const void* elem2) //提供的是结构体中stu2中name成员的比较方式
{
	return  (strcmp(((struct stu2*)elem1)->name, ((struct stu2*)elem2)->name));
}

int cmp4(const void* elem1, const void* elem2)//提供的是结构体中stu2中age成员的比较方式
{
	return  (((struct stu2*)elem1)->age - ((struct stu2*)elem2)->age);
}

void customer_sort(void*base, int num, int width, int (*cmp)(const void*elem1,const void* elem2))
{
	int i = 0;
	for (; i < num - 1; i++)//确定排序趟数
	{
		for (int b = 0; b < num - 1 - i; b++)//每趟排序进行比较的次数
				{
			if (cmp(((char*)base + b * width), ((char*)base + (b + 1) * width)) > 0)
			//如果cmp函数返回值大于0,则进行交换
			{
				char tmp = 0;
				for (int j = 0; j < width; j++)
				{
					tmp = *((char*)base + b * width + j);
					*((char*)base + b * width + j) = *((char*)base + (b + 1) * width + j);
					*((char*)base + (b + 1) * width + j) = tmp;
				}
			}
		}
	}
}
int main()
{
	int arr1[10] = { 2,4,6,8,1,3,9,5,7,10 };
	float arr2[5] = { 3.0,5.0,1.0,7.0,8.0 };
	int sz = sizeof(arr2) / sizeof(*arr2);
	customer_sort(arr2, sz, sizeof(*arr2),cmp2);//如果要排序其他类型数组
	//把cmp函数换成对应的就行了
	for (int a = 0; a < sz; a++)
	{
		printf("%f ", arr2[a]);
	}
	return 0;
}

实现结果:
在这里插入图片描述
需要注意的地方:
1.这里使用的排序方法与冒泡排序相似,用两层循环对数组进行两两交换,再根据cmp函数的比较结果,决定是否要对两个元素进行交换。
2.因为我们无法确定传进来的数组元素是啥类型,所以接受数组起始位置的指针使用的是空指针void* base,方便接收所有类型的指针,cmp函数的两个参数也是如此。
3.传递数组元素给cmp函数时,因为无法确定元素类型,所以指针往后走一步的大小也无法确定,所以只能用char* 对base进行强制类型转换,然后循环跳到下一个元素时,使用b * width,width表示对应元素类型的大小,b * width即跳过b * width个字节,找到对应元素的地址,(char * )base + 1 * width即为数组第二个元素,(char*)base + b*width即为第b+1个元素,下标为b,这样可以跳到对应位置,交换元素时,也需要按照这种方式来进行交换。并且交换也只能按一个字节一个字节的内容进行交换,故第三个for循环就是让两元素的每个字节都进行交换,循环次数也取决于width的大小,这样才能保证一个元素的所有字节都进行了交换。

再次通过画图来更直观地了解原理,以int类型元素为例:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
希望大家能自己去尝试实现qsort函数,这样才能够更明白其中的原理

虽然我们讲了很多关于如果实现这个函数,但最重要的是明白compare是通过函数指针的形式被调用,即回调函数。不同类型元素的比较有不同的比较方式,但这些函数的返回类型,参数类型都是一致的,这样才能通过同一个函数指针进行调用。

一些指针和数组练习

答案在下面,先自己尝试完成

//一维数组

int a[] = {1,2,3,4};

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

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

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

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

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

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

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

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

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

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

//字符数组

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

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

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

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

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

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

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

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

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

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

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

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

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

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

char arr[] = "abcdef";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

char *p = "abcdef";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

//二维数组

int a[3][4] = {0};

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

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

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

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

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

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

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

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

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

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

我们复习一下数组名是什么:

  1. sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。

  2. &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。
    除1,2两种情况之外,所有的数组名都表示数组首元素的地址。

//一维数组

int a[] = {1,2,3,4};

printf("%d\n",sizeof(a)); //16 即4*4个字节
                       //此时a表示的是整个数组而不是数组首元素地址

printf("%d\n",sizeof(a+0)); //4 a已经先进行了运算
          //故此时的a表示数组首元素地址,32位平台下指针(地址)大小为4

printf("%d\n",sizeof(*a));//4 a表示首元素地址
                  //对a进行解引用即获取数组首元素1,大小为4个字节

printf("%d\n",sizeof(a+1));//4 同第4行,此时为地址

printf("%d\n",sizeof(a[1]));//4
printf("%d\n",sizeof(&a));//4,为数组地址,但本质还是个地址,故大小为4

printf("%d\n",sizeof(*&a));//16 虽然已经取了a的地址
              //但之后又对它进行解引用,得到的即为a,表示整个数组

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

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

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


//字符数组

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

printf("%d\n", sizeof(arr));//6 计算的是整个数组的大小,即6*1个字节

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

printf("%d\n", sizeof(*arr));//1 数组首元素即‘a’的大小    

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));//随机值,因为此时数组arr中并没有'\0'

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

printf("%d\n", strlen(*arr));//错误,strlen会把字符a的ASCII码值当成地址
                    //然后往后计算,程序会崩溃,看下面图片解释

printf("%d\n", strlen(arr[1]));//错误  与strlen(arr+0)相同

printf("%d\n", strlen(&arr));//随机值 此时取出的是arr的地址
                           //会从该地址开始往后寻找\0

printf("%d\n", strlen(&arr+1));//随机值 与strlen(&arr)相同  
                			//但这个数字会比strlen(&arr)结果小6

printf("%d\n", strlen(&arr[0]+1));//随机值 与strlen(arr+0)相同
								//这个结果比strlen(arr+0)小1


char arr[] = "abcdef";

printf("%d\n", sizeof(arr));//7  f后还有一个\0,sizeof也会把\0计算
printf("%d\n", sizeof(arr+0));//4 地址

printf("%d\n", sizeof(*arr));//1 表示'a'
printf("%d\n", sizeof(arr[1]));//1 与sizeof(*arr)相同
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 strlen不计算\0
printf("%d\n", strlen(arr+0));// 6

printf("%d\n", strlen(*arr));// 错误,把'a'传给了strlen
printf("%d\n", strlen(arr[1]));// 错误  与strlen(*arr)相同

printf("%d\n", strlen(&arr));// 6 尽管这里取了arr的地址
				//但还是会按照这个地址往后计算,这里与sizeof不同
printf("%d\n", strlen(&arr+1));// 随机值 

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


char *p = "abcdef";

printf("%d\n", sizeof(p));// 4  'a'地址

printf("%d\n", sizeof(p+1));// 4 'b'地址
printf("%d\n", sizeof(*p));//  1 'a'的大小
printf("%d\n", sizeof(p[0]));// 1 与sizeof(*p)相同
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 从'a'开始计算
printf("%d\n", strlen(p+1));// 5 从'b'开始计算
printf("%d\n", strlen(*p));// 错误 
printf("%d\n", strlen(p[0]));// 错误
printf("%d\n", strlen(&p));// 随机值 取的是p的地址,无法判断地址后的数据是什么
printf("%d\n", strlen(&p+1));// 随机值 但和strlen(&p)没有数据上的关系
							//因为可能在读取p地址过程中遇见了\0
		//p的地址可能为0x01007842,此时可能读取地址过程中就遇到了\0
printf("%d\n", strlen(&p[0]+1));// 5  取的是'b'的地址
//二维数组

int a[3][4] = {0};

printf("%d\n",sizeof(a));// 48  计算的是整个二维数组的大小,即12*4=48

printf("%d\n",sizeof(a[0][0]));// 4  数组元素的大小
printf("%d\n",sizeof(a[0]));// 16  计算的是第一行数组的大小,即4*4=16
printf("%d\n",sizeof(a[0]+1));// 4 地址
printf("%d\n",sizeof(*(a[0]+1)));// 4 计算的是第二行首元素的大小

printf("%d\n",sizeof(a+1));// 4 第二行的地址
printf("%d\n",sizeof(*(a+1)));// 16 虽然这里a进行了加1,表示第二行的地址
							//但对其进行解引用,表示的是第二行

printf("%d\n",sizeof(&a[0]+1));// 4 地址
printf("%d\n",sizeof(*(&a[0]+1)));// 16 &a[0]表示第一行地址
					//  +1表示第二行地址,解引用之后表示第二行
printf("%d\n",sizeof(*a));// 16 表示第一行
printf("%d\n",sizeof(a[3]));// 16 sizeof是一个操作符
				//并不会访问内存,只是计算理论上第四行数组的大小
			//也不会对里面的表达式进行计算,即不会改变变量原有的值。

为了验证sizeof不会计算括号内部的表达式,我们给出例子:
在这里插入图片描述

strlen把’a’的ASCII值的解释:
我们看到读取位置0x00000061有冲突,而把这个十六进制换算成十进制数字即97,即a的ASCII码值,所以我们可以看到当把a传给strlen,会把a的ASCII值当成地址往后计算长度

在这里插入图片描述
总结: 数组名的意义:

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

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

  3. 除此之外所有的数组名都表示首元素的地址。
    注:多维数组也同样如此,拿上面的二维数组举例,a[0]表示的是第一行数组首元素地址,即&a[0][0],其他的以此类推

以上题目大家可以拷贝下来自行打印看结果。

看到这里希望大家也可以亲自动手尝试,这些只是作者自己的理解,真正的理解是无数个深夜思考换来的。

能看到这里的小伙伴是真的热爱学习无疑了,如果觉得本篇文章对你有帮助的话,记得给个三连哦!!!

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

WoLannnnn

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

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

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

打赏作者

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

抵扣说明:

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

余额充值