【指针合集】全方位理解C语言指针

1.什么是指针

指针的两个要点
1.指针是内存中的一个最小单元的编号,也就是地址。
2.平时口语所说的指针,通常指的是指针变量,是用来存放内存地址的变量。

总结
指针就是地址,口语所说的指针通常是指针变量

内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的。
所以为了有效的使用内存,就要把内存划分成一个个小的内存单元,每个内存单元的大小都是一个字节。
为了能够有效的访问到内存的每个单元,就要给内存单元进行编号,这些编号被称为内存单元的地址。
在写程序时,创建的变量、数组等都要在内存上开辟空间。
每个内存都有唯一的编号,这个编号也被称为地址 地址 == 编号
内存
变量是创建内存中的(在内存中分配空间的),每个内存单位都有地址,所以变量也是有地址的。
可以利用&来取出变量的地址。
指针变量

通过&(取地址符)取出变量内存的地址,把地址可以存放在一个变量当中,这个变量就是指针变量。

#include <stdio.h>
int main()
{
	int a = 0;
	int* pa = &a;//这里的pa就是指针变量
	*pa = 10;//*就是根据a的地址取找到a
	//这样我们就可以间接的改变a的值
	printf("%d\n",a);
	return 0;
}
//打印结果:10

总结:

指针变量就是用来存放地址的变量。(存放在指针中的值会被当成地址处理)。

在内存当中是如何编址的呢?
上面我们提到了一个字节对应一个地址,为什么会这样呢?
其实在计算机当中会存在地址线,32位的机器上就存在32根地址线,这些地址线会发出高电压(高电平)和低电压(低电平)就是(1或者0);
那么32根地址线就可以产生2的32次方种情况。

00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001

11111111 11111111 11111111 11111111

2的32次方种情况,每种情况就对应着每个地址,就标识着一个字节。这里右2的32次方字节,大概是4G的空间。
同样的方法在64位机器,可以标识的空间就非常大了。
这里我们明白了:

  • 在32位机器上,地址是32个0或者1组成的二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4字节。
  • 因此在64位的机器上就是一个指针变量大小对应8个字节。
    总结:
  • 指针是用来存放地址的,地址就是唯一标识一块地址的。
  • 指针的大小在32位平台是4个字节,在64位平台是8个字节。

2.指针和指针类型

前面我学习了,整型,短整型,浮点型,字符型。这些都是变量的类型,那么指针有没有类型呢?
有的

int num = 10;
p = &num

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

char* pc = NULL;
int* pi = NULL;
short* ps = NULL;
long* pl = NULL;
long long* pll = NULL;

我们可以发现,指针的定义方式是type + *
但是我们又知道,指针变量的大小都是是固定的不是4个字节就是8个字节。那么为什么要搞出指针的类型呢?有什么意义吗?
意义就在于给*发出信息

指针类型可以决定指针解引用的时候访问多少字节
指针类型决定了指针解引用操作的权限
指针的类型决定了指针向前或者向后走一步有多大距离

2.1 指针±整数

#include <stdio.h>
int main()
{
	int a = 0;
	char* pc = (char*)&a;
	int* pi = &a;
	printf("%p\n",&a);
	printf("%p\n",pc);
	printf("%p\n",pc+1);
	printf("%p\n",pi);
	printf("%p\n",pi+1);
	return 0;
}
//打印结果
/*
006FFE20
006FFE20
006FFE21
006FFE20
006FFE24
*/

可以发现用char* 作为指针类型的+1只能向后移动一个字节,而用int*作为指针类型的+1却可以向后移动4个字节。
也就是说:
指针的类型决定了指针向前或者向后走一步有多大距离

2.2 指针的解引用

#include <stdio.h>
int main()
{
	int n = 0x11223344;
	char* pc = (char*)&n;
	int* pi = &n;
	*pc = 0;
	*pi = 0;
	return 0;
}

下面我们观察在调试过程当中内存的变化。
编译编译
编译

从这三张图我们可以了解到:
指针的类型决定了,对这种解引用有多大的权限(能操作几个字节)
比如 char*的指针解引用就只能访问一个字节,而int*的指针就能访问4个字节。

3.野指针

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

3.1为什么会有野指针

1.指针未初始化

#include <stdio.h>
int main()
{
	int* p;//局部变量指针未初始化,默认为随机值
	*p = 100;
	return 0;
}

2.指针越界访问

#include <stdio.h>
int main()
{
	int arr[10] = {0};
	int* p = arr;
	for(int i = 0;i<=10;++i)
	{
		//当指针指向的范围超出数组arr的范围时,p就是野指针
		*(p++) = i;
	}
	return 0;
}

3.指针指向的局部变量释放

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

3.2 如何规避野指针

1.指针初始化
2.小心指针越界
3.指针指向空间释放即置为NULL
4.避免返回局部变量的地址
5.指针使用前检查其有效性

#include <stdio.h>
int main()
{
	int* p = NULL;
	//明确知道指针应该初始化为谁的地址,就直接初始化
	//不知道指针初始化为什么值,就暂时初始化为NULL;
	//...
	int a = 10;
	p = &a;
	if(p!=NULL)
	{
		*p = 100;
	}
	return 0;
}

4.指针运算

  • 指针±整数
  • 指针-指针
  • 指针的关系运算

4.1 指针±整数

#include <stdio.h>
int main()
{
	int arr[5] = {0};
	for(int* p = arr;p<=&arr[4];)
	{
		*p++ = 0;
	}
	return 0;
}

4.2 指针减指针

指针-指针返回绝对值是它们间的元素个数,

#include <stdio.h>
int main()
{
	int arr[5] = {0};
	int* pa = &arr[0];
	int* pb = &arr[4]printf("%d\n",pb-pa);
	return 0;
}
//打印结果
//4

4.3 指针的关系运算

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

//代码简化
for(vp = &values[N_VALUES-1];vp>=&values[0];vp--)
{
	*v = 0;
}

实际上大部分的编译器上都是可以完成上面的代码通过的,然而我们还要要避免这样写,因为标准不保证它可行。

规定:

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

5.指针与数组

指针变量就是指针变量,不是数组。指针变量的大小是4/8字节,专门是用来存放地址的
数组就是数组,不是指针,数组是一块连续的空间,可以存放一个或多个类型相同的数据
数组中,数组名就是数组首元素的地址,数组名 == 地址 == 指针
当我们知道数组首元素的地址的时候,因为数组又是连续存放的,所以通过指针就可以遍历访问数组,数组是可以通过指针来访问的。

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

可见数组名和首元素的地址是一样的。
数组名表示的就是数组首元素的地址。(两种情况)

1.sizeof(数组名),计算的是整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
2.&数组名,取出的整个数组的地址。&数组名,数组名表示整个数组,但是整个数组会以首元素的的地址显示。

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

#include <stdio.h>
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	int* p  = arr;
	for(int i = 0;i<10;++i)
	{
		printf("&arr[%d] = %p == p+%d = %p\n",i,&arr[i],i,p+i);
	}
	return 0;
}
//打印结果:
/*
&arr[0] = 00B3F708 == p+0 = 00B3F708
&arr[1] = 00B3F70C == p+1 = 00B3F70C
&arr[2] = 00B3F710 == p+2 = 00B3F710
&arr[3] = 00B3F714 == p+3 = 00B3F714
&arr[4] = 00B3F718 == p+4 = 00B3F718
&arr[5] = 00B3F71C == p+5 = 00B3F71C
&arr[6] = 00B3F720 == p+6 = 00B3F720
&arr[7] = 00B3F724 == p+7 = 00B3F724
&arr[8] = 00B3F728 == p+8 = 00B3F728
&arr[9] = 00B3F72C == p+9 = 00B3F72C
*/

所以p+i就是计算的数组arr下标为i的地址。
那我们就可以直接通过指针来访问数组。

#include <stdio.h>
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	int* p  = arr;
	for(int i = 0;i<10;++i)
	{
		printf("%d ",*(p+i));
	}
	return 0;
}
//打印结果:1 2 3 4 5 6 7 8 9 10

也就是说arr[i] = *(p+i),这样的话,对于计算机来说,肯定是按*(p+i)来处理的,就是把arr[i]转换成*(p+i)。然后我们知道*(p+i)和*(i+p)是没有区别的。所以我们是可以写i[arr]来打印数组的。

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	for (int i = 0; i < 10; ++i)
	{
		printf("%d ",i[arr]);
	}
	return 0;
}
//打印结果:1 2 3 4 5 6 7 8 9 10

注意:不建议这样写,会有点装了。

6.二级指针

指针变量也是指针,是变量就有地址,那指针变量的地址存放在哪里呢?
二级指针

a的地址存放在pa中,pa的地址存放在ppa中,pa是一级指针。而ppa是二级指针。
对于二级指针的运算有:

  • *ppa通过对ppa中的地址进行解引用,这样找到的是pa,*ppa其实访问的是pa
int a = 10;
*ppa = &a;//等价于pa = &b
  • ppa先通过*ppa找到pa进行解引用操作:*pa,那找到的就是a

7.指针数组

指针数组就是存放指针的数组
比如整型数组是存放整型的数组,字符数组是存放字符的数组。
整型数组

那么指针数组就是:

int* arr2[5];

arr2是一个数组,有5个元素,每一个元素是一个整型指针;
整型指针数组

8.字符指针

在指针的类型中我们知道有一种指针类型为字符指针char*
一般使用情况

#include <stdio.h>
int main()
{
	char c = 'a';
	char* pc = &c;
	*pc = 'y';
	return 0;
}

还有一种情况

#include <stdio.h>
int main()
{
	const char* str = "hello world";//把"hello world"的首元素的地址给了str
	//但是不能单纯的理解为数组,这里的"hello world"是存放代码区中的不可修改,是常量字符串,所以我们在前面加了const修饰
	printf("%s\n",str);
	return 0;
}

指针

本质就是把常量字符串hello world的首元素的地址放到了str当中,也就是将常量字符串的首元素h的地址放到str中
练习

#include <stdio.h>
int main()
{
	char str1[] = "hello world";
	char str2[] = "hello world";

	const char* str3 = "hello bit";
	const char* str4 = "hello bit";

	if(str1==str2)
		printf("same\n");
	else
		printf("not same\n");
	if(str3==str4)
		printf("same\n");
	else
		printf("not same\n");
	return 0;
}

//打印结果:
/*
not same
same
*/

这里比较的都是地址。
str1和str2都是数组,当用相同的常量字符串去初始化不同的数组的时候就会开辟不同的空间。而str3和str4指向的同一个常量字符串。在c/c++中会把常量字符串单独存储在一个内存区域(代码段),当我们用几个指针去指向同一个字符串时,它们实际会指向同一块内存的。
可以这么理解:str1和str2是可以修改数组中的元素的,如果不同数组间的修改会相互影响,那岂不是乱遭了。而str3和str4是不可以被修改的,那么让它们两指向同一块空间也是完全没有问题的。

9.指针数组

指针数组就是存放指针的数组。
我们可以进行类比:
整型数组是存放整型的数组,字符数组是存放字符的数组。那么指针数组肯定就是存放指针的数组咯。

int* arr1[10];//整型指针的数组
char* arr2[4];//一级字符指针数组
char** arr3[10];//二级字符指针数组

10.数组指针

10.1 数组指针的定义

我们知道整型指针是指向整型的指针(存放整型变量的地址的指针变量)
还有字符指针是指向字符的指针(存放字符变量的地址的指针变量)
如此类比的话
数组指针就是指向数组的指针(存放数组变量的地址的指针变量)
数组指针的正确写法

int *p1[10];//错
int (*p2)[10];//对
int (*p)[10];
//p与*结合,说明p是一个指针变量,然后指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//加()的原因是因为,根据操作符的优先性,[]的优先级是要高于*的,为了保证*与p的结合需要添加括号

10.2 &数组名与数组名

int arr[10];

arr&arr分别是什么呢?
arr是数组名,数组名又表示数组首元素的地址。
&arr表示的整个数组的地址。

#include <stdio.h>
int main()
{
	int arr[10];
	printf("%p\n",arr);
	printf("%p\n",&arr);
	return 0;
}
//打印结果:
/*
00DCFBDC
00DCFBDC
*/

打印它们的地址可以发现是一样的。但其实又不完全一样。

#include <stdio.h>
int main()
{
	int arr[10];
	printf("%p\n",arr);
	printf("%p\n",arr+1);
	
	printf("%p\n",&arr);
	printf("%p\n",&arr+1);
	return 0;
}
//打印结果:
/*
004FF970
004FF974
004FF970
004FF998
*/

arr+1跳过的4个字节的地址。而&arr跳过的是40个字节的地址。
正如前面所说&arr是整个数组的地址,整个数组大小就是40个字节。
本例中&arr的类型就是int(*)[10],是一种数组指针类型。
数组地址+1,跳过整个数组的大小,所以&arr+1相对于&arr的差值就是40.

10.3 数组指针的使用

了解到数组指针指向的数组,那么数组指针中存放的就是数组的地址。

#include <stdio.h>
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,0};
	int(*p)[10] = &arr;
	//把整个数组的地址存放在数组指针变量当中
	//但是很少这么写
	return 0;
}

数组指针的使用

#include <stdio.h>
void print1(int arr[3][5],int row,int col)
{
	for(int i = 0;i<row;++i)
	{
		for(int j = 0;j<col;++j)
		{
			printf("%d ",arr[i][j]);
		}
		printf("\n");
	}
}

void print2(int (*arr)[5],int row,int col)
{
	for(int i = 0;i<row;++i)
	{
		for(int 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,0};
	print1(arr,3,5);
	//arr是数组的数组名,表示数组首元素的地址。而这又是一个二维数组,二维数组的首元素地址就是第一行的地址,所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址,可以利用数组指针接收。
	print2(arr,3,5);
	
	return 0;
}

区分

int arr[5];//整型数组
int* parr1[10];//整型指针数组
int (*parr2)[10];//数组指针
int (*parr3[10])[5];//数组指针数组

11.数组传参、指针参数

在写代码时不可避免的要把【数组】或者【指针】传递给函数,那么函数的参数设计要怎么做呢?

11.1 一维数组传参

#include <stdio.h>
void test(int arr[])//可行,最容易理解的写法([]内的数字可以随便写,不影响系统的判断)该传参的本质就是int* arr
{}
void test(int arr[10])//可行,最容易理解的写法。([]内的数字可以随便写,不影响系统的判断)该传参的本质就是int* arr
{}
void test(int* arr)//可行,实参传递的是arr代表数组首元素的地址,利用整型指针接收合情合理
{}
void test2(int *arr[20])//可行,本质是int** arr
{}
void test2(int **arr)//可行,实参传递的是arr2也代表首元素的地址,因为arr2是指针数组,一维指针的地址要有二级指针接收,合情合理
{}

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

11.2 二维数组传参

#include <stdio.h>

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])//可行
{}
void test(int** arr)//不可行,二维数组的数组名代表的是数组第一行的地址,是数组指针.这里是二级指针,不一致。
{}
int main()
{
	int arr[3][5] = {0};
	test(arr);
	return 0;
}

11.3 一级指针传参

#include <stdio.h>
void print(int* arr,int sz)
{
	for(int i = 0;i<sz;++i)
	{
		printf("%d ",*(arr+i));
	}
}
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,0};
	int sz = 10;
	int* p = arr;
	print(arr,sz);
	return 0;
}
//打印结果:1 2 3 4 5 6 7 8 9 0

当函数的参数部分为1级指针的时候,函数能接受的的参数为该一级指针对应类型的地址。

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

当函数的参数部分为二级指针的时候,函数能接受的的参数为该二级指针对应一级指针类型的地址。

12.函数指针

其实函数也是有地址的。

#include <stdio.h>
void test()
{
	printf("hello\n");
}
int main()
{
	printf("%p\n",test);
	printf("%p\n",&test);
	return 0;
}
//打印结果
/*
00A41267
00A41267
*/

从这里可以看出,函数不仅有地址,而且函数的函数名就代表了函数的地址,&函数名同样也表示函数的地址。
既然函数有地址,那么也就说明可以利用变量来存储。这个存储函数地址的变量就是函数指针。
函数指针的正确写法

#include <stdio.h>
void test()
{
	printf("hello\n");
}
int main()
{
	void (*pf1)() = test;//正确写法
	void *pf2() = test;//错误写法
	return 0;
}

pf1可以存储,和数组指针类似,这里的*要先和pf1结合,确定pf1是一个指针,()的优先级有比较高。因此需要用()将*pf1括起来。pf1指向的是一个函数,指向函数无参数,返回类型为void。

#include <stdio.h>
int Add(int x,int y)
{
	return x+y;
}
int main()
{
	int (*pf)(int int) = Add//指向有参数的函数指针
	return 0;
}

练习

//代码1
(*(void(*)())0)();
/*
解释:
先看void(*)()这是一个函数指针类型。再往外看,这个函数指针类型被括号括住了(void(*)())
一个类型被()住就是表示强制类型转换的意思。也是说明0被强制类型转换成了函数指针类型。然后*表示对一个函数指针类型进行解引用取出指向的函数*(void(*)())0,最后再调用这个函数。
总结:调用0地址处的函数(实际是无法调用的)
*/

//代码2
void (*signal(int,void(*)(int)))(int);//signal为函数名
/*
解释:signal是函数名,那signal()中的就是函数的参数类型,类型分别为整型和函数指针类型,现在一个函数有了函数名和函数的参数,就差函数的返回类型,如果我们把signal(int,void(*)(int))删除就得到了void (*)(int)这不就是函数指针类型吗,那也就是说signal的函数的返回类型就是void(*)(int)
总结:这是一个函数的声明,找到其函数参数和函数返回类型就可以了。
*/

简化代码2

typedef void (*pf)(int);
pf signal (int,pf);//利用typedef将类型重命名,来简化代码

13.函数指针数组

数组是存放相同类型数据的存储空间
前面我们已经学习了指针数组

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

同样的我们也可以把函数指针存放进数组,就叫做函数指针数组,那函数指针数组的是如何定义的呢?

int (*pf)();//这是一个函数指针
//我们将[]添加到变量名后面就可以了
int (*pf[10])();//这就是函数指针数组

pf[]结合说明pf是一个数组,然后数组存放的类型就是int(*)()
函数指针数组的运用
下面以实现一个简单的计算器为例

#include <stdio.h>
void menu()
{
	printf("***************************\n");
	printf("**** 1.Add   2.Sub     ****\n");
	printf("**** 3.Mul   4.Div     ****\n");
	printf("**** 0.exit            ****\n");
	printf("***************************\n");
}
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;
}
int main()
{
	int input = 0;
	int a = 0,b = 0;
	int ret = 0;
	do
	{
		menu();
		printf("选择你所要用到的功能>\n");
		scanf("%d",&input);
		switch(input)
		{
			case 1:
				printf("请输入两数>");
				scanf("%d %d",&a,&b);
				ret = Add(a,b);
				printf("ret = %d\n",ret);
				break;
			case 2:
				printf("请输入两数>");
				scanf("%d %d",&a,&b);
				ret = Sub(a,b);
				printf("ret = %d\n",ret);
				break;
			case 3:
				printf("请输入两数>");
				scanf("%d %d",&a,&b);
				ret = Mul(a,b);
				printf("ret = %d\n",ret);
				break;
			case 4:
				printf("请输入两数>");
				scanf("%d %d",&a,&b);
				ret = Div(a,b);
				printf("ret = %d\n",ret);
				break;
			case 0:
				printf("退出\n");
				break;
			default:
				printf("输入错误\n");
				break;
		}
	}while(input);
	return 0;
}

这样写的话其实是很繁琐的。实现这种简单的功能都写了这么长的代码,而且如果后续在添加什么函数功能的话,代码又要增加好的。所以我们要简化。
通过观察,这4个函数的参数和返回类型都是相同的,那么不就说明了可以写成函数指针数组吗。数组中存放这个函数指针类型就可以了。

#include <stdio.h>
void menu()
{
	printf("***************************\n");
	printf("**** 1.Add   2.Sub     ****\n");
	printf("**** 3.Mul   4.Div     ****\n");
	printf("**** 0.exit            ****\n");
	printf("***************************\n");
}
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;
}
int main()
{
	int input = 0;
	int a = 0,b = 0;
	int ret = 0;
	do
	{
		menu();
		printf("选择你所要用到的功能>\n");
		scanf("%d",&input);
		int(*pf[5])(int,int) = {NULL,Add,Sub,Mul,Div};//存入NULL是为了可以和菜单对应
		if(input>0&&input<5)
		{
			printf("请输入两数>");
			scanf("%d %d",&a,&b);
			ret = pf[input](a,b);
			printf("ret = %d\n",ret);
		}
		else if(input == 0)
			printf("退出\n");
		else
			printf("输入有误\n");
	}while(input);
	return 0;
}

利用函数指针数组我们将该程序充分简化,而且如果后续还要添加类似的函数功能的话,我们只需要将新写的函数添加进数组,在改变一下判断条件即可。

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

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

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的指针ppfunarr
	void(*(*ppfunarr)[5])(const char*) = &pfunarr;
}

还可以再绕下去的

15.回调函数

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

首先演示一下qsort函数的使用:

//对整型数组进行排序
#include<stdio.h>
int int_cmp(const void* a,const void* b)
{
	return (*(int*)a) - (*(int*)b);
}
int main()
{
	int arr[10] = {1,3,5,7,9,2,4,6,8,0};
	qsort(arr,10,sizeof(int),int_cmp);
	for(int i = 0;i<10;++i)
	{
		printf("%d ",arr[i]);
	}
	printf("\n");
	return 0;
}
//打印结果:
//0 1 2 3 4 5 6 7 8 9

//对结构体数组进行排序
#include <stdio.h>
#include <string.h>
struct stu
{
	int age;
	char name[10];
};
int struct_cmp_age(const void* a, const void* b)//利用年龄排序
{
	return ((struct stu*)a)->age - ((struct stu*)b)->age;
}
int struct_cmp_name(const void* a, const void* b)//利用名字排序,因为字符串无法相减
//所以这里利用了strcmp进行字符串的比较
{
	return strcmp(((struct stu*)a)->name,((struct stu*)b)->name);
}
int main()
{
	struct stu s[3] = { {17,"yui"},{14,"anna"},{20,"hua"} };
	qsort(s, 3, sizeof(s[0]), struct_cmp_age);
	printf("调用struct_cmp_age\n");
	for (int i = 0; i < 3; ++i)
	{
		printf("%d %s\n", s[i].age, s[i].name);
	}
	qsort(s, 3, sizeof(s[0]), struct_cmp_name);
	printf("调用struct_cmp_name\n");
	for (int i = 0; i < 3; ++i)
	{
		printf("%d %s\n", s[i].age, s[i].name);
	}
	return 0;
}
//打印结果:
/*
调用struct_cmp_age
14 anna
17 yui
20 hua
调用struct_cmp_name
14 anna
20 hua
17 yui

*/

qsort
打开cplusplus网站->qsort
qsort

void qsort(void* base,//需要排序的数组首元素地址
		  size_t num,//需要排序的数组的元素个数
		  size_t size,//需要排序数组的单个元素的大小
		  int (*compar)(const void*,const void*)//传递函数指针,需要自己写
		  }

可以看到的时,这里接受数组首元素的地址是用void*来接收。
提问:为什么呢?
回答:Void*指针 是无具体类型的指针。Void* 类型的指针可以接任意类型的地址(这种类型的指针是不能直接解引用操作的,也不能直接进行指针运算的)。
所以用void*接收是没问题的。然后,这个qsort函数不仅可以对整型数组排序,还可以对字符数组,浮点型数组,甚至是结构体数组。这也就造成了不能使用特定类型指针来接收的情况,如果使用了特定的类型,那其它类型就不能被接收了,所以才会选择使用void*来接收。
模拟实现qsort,但是因为目前还没有学快速排序,所以这里我们利用冒泡排序替代。

//主要逻辑
void swap(char* a, char* b, int size)
{
	char tmp = 0;
	for (int i = 0; i < size; ++i)//交换的实质其实就是指针所指向内容的交换.
	//因为char只能指向一个字节,所以我们需要传递size了解到要交换的字节大小,然后一个字节一个字节的交换。
	{
		tmp = *a;
		*a = *b;
		*b = tmp;
		a += 1;
		b += 1;
	}
}
void bubble_sort(void* base, int num, int size, int(*cmp)(const void*, const void*))
{
	for (int i = 0; i < num - 1; ++i)
	{
		for (int j = 0; j < num - i - 1; ++j)
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size)>0)//因为void类型的指针是不能直接解引用操作的,也不
			//能直接进行指针运算的。为了拿到比较位置的地址,我们需要将base强转为(char*),因为char*的加减整数时只会跳过一个字节,
			//这是最小的位移距离了。所以我们可以通过强转后的base拿到base[j]和base[j+1]的地址进行比较。
			{
				swap((char*)base + j * size, (char*)base + (j + 1) * size, size);//开始交换
			}
		}
	}
}

测试

#include <stdio.h>
#include <string.h>
struct stu
{
	int age;
	char name[10];
};
int int_cmp(const void* a, const void* b)
{
	return (*(int*)a) - (*(int*)b);
}
int struct_cmp_age(const void* a, const void* b)//利用年龄排序
{
	return ((struct stu*)a)->age - ((struct stu*)b)->age;
}
int struct_cmp_name(const void* a, const void* b)//利用名字排序,因为字符串无法相减
//所以这里利用了strcmp进行字符串的比较
{
	return strcmp(((struct stu*)a)->name,((struct stu*)b)->name);
}
void swap(char* a, char* b, int size)
{
	char tmp = 0;
	for (int i = 0; i < size; ++i)//交换的实质其实就是指针所指向内容的交换,因为char只能指向一个字节,所以我们需要传递size了解到要交换的字节大小,然后一个字节一个字节的交换。
	{
		tmp = *a;
		*a = *b;
		*b = tmp;
		a += 1;
		b += 1;
	}
}
void bubble_sort(void* base, int num, int size, int(*cmp)(const void*, const void*))
{
	for (int i = 0; i < num - 1; ++i)
	{
		for (int j = 0; j < num - i - 1; ++j)
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size)>0)//因为void类型的指针是不能直接解引用操作的,也不能直接进行指针运算的。为了拿到比较位置的地址,我们需要将base强转为(char*),因为char*的加减整数时只会跳过一个字节,这是最小的位移距离了。所以我们可以通过强转后的base拿到base[j]和base[j+1]的地址进行比较。
			{
				swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}
}
int main()
{
	int arr[10] = { 1,3,5,7,9,2,4,6,8,0 };
	bubble_sort(arr, 10, sizeof(int), int_cmp);//冒泡排序
	printf("对arr排序:\n");
	for (int i = 0; i < 10; ++i)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	struct stu s[3] = { {17,"yui"},{14,"anna"},{20,"hua"} };
	bubble_sort(s, 3, sizeof(s[0]), struct_cmp_age);
	printf("调用struct_cmp_age:\n");
	for (int i = 0; i < 3; ++i)
	{
		printf("%d %s\n", s[i].age, s[i].name);
	}
	bubble_sort(s, 3, sizeof(s[0]), struct_cmp_name);
	printf("调用struct_cmp_name:\n");
	for (int i = 0; i < 3; ++i)
	{
		printf("%d %s\n", s[i].age, s[i].name);
	}
	return 0;
}
//打印结果:
/*
对arr排序:
0 1 2 3 4 5 6 7 8 9
调用struct_cmp_age:
14 anna
17 yui
20 hua
调用struct_cmp_name:
14 anna
20 hua
17 yui
*/

16. 练习

数组名的意义:

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

16.1一维数组

16.1.1 整型数组

int main()
{
	int a[] = {1,2,3,4};
	printf("%d\n",sizeof(a));//16,sizeof单独加上数组名计算的是整个数组的大小
	printf("%d\n",sizeof(a+0));//4或者8,这里数组名不是单独在sizeof内,所以会变成数组首元素的地址,而指针的大小根据选择的是32位还是64位而不同。
	printf("%d\n",sizeof(*a));//4,a是首元素的地址,被解引用后就是首元素。整型为4个字节大小
	printf("%d\n",sizeof(a+1));// 4/8,表示数组第二个元素的地址
	printf("%d\n",sizeof(a[1]));//4,表示数组第一个元素是整型
	printf("%d\n",sizeof(&a));// 4/8,&数组名拿到是整个数组的地址,而整个数组的地址也是指针,所以大小还是4或者8
	printf("%d\n",sizeof(*&a));//16,取地址和解引用相互抵消,使得本质上是sizeof(a)
	printf("%d\n",sizeof(&a[0]));//4/8,计算的数组首元素的地址,也就是指针
	printf("%d\n",sizeof(&a[0]+1));//4/8,计算的是数组第二个元素的地址,也就是指针
	return 0;
}
//打印结果
/*
16
8
4
8
4
8
16
8
8
*/

16.1.2 字符数组

#include <stdio.h>
int main()
{
	char arr[] = {'a','b','c','d','e','f'};
	printf("%d\n",sizeof(arr));
	printf("%d\n",sizeof(arr+1));
	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));
	return 0;
	//与上面解释类似
}
//打印结果:
/*
6
8
8
1
1
8
8
8
*/
#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = {'a','b','c','d','e','f'};
	printf("%d\n",strlen(arr));//随机值,表示的是从数组的首元素开始计算直到碰到'\0'为止的长度
	printf("%d\n",strlen(arr+0));//随机值,表示的是从数组的首元素开始计算直到碰到'\0'为止的长度
	printf("%d\n",strlen(*arr));//错误写法,strlen函数参数要传地址
	printf("%d\n",strlen(arr[1]));//错误写法,strlen函数参数要传地址
	printf("%d\n",strlen(&arr));/*随机值,虽然&arr表示的是整个数组的地址,但是整个数组的地
	址依旧是用数组首元素的地址代表。表示的是从数组的首元素开始计算直到碰到'\0'为止的长度*/
	printf("%d\n",strlen(&arr+1));//随机值,表示的是从数组的最后一个元素后的地址开始计算直到碰到'\0'为止的长度。会比第一个随机值数少6个字节
	printf("%d\n",strlen(&arr[0]+1));//随机值,表示从数组第二个元素地址开始计算直到碰到'\0'为止的长度。会比第一个随机值数少1个字节
	
	return 0;
}
//打印结果:
/*(注释掉错误写法后)
42
42
42
36
41
*/
#include <stdio.h>
int main()
{
	char arr[] = "abcdef";
	printf("%d\n",sizeof(arr));
	printf("%d\n",sizeof(arr+1));
	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));
	//整体解释和第一道题类型,不过要注意的是sizeof会计算隐藏的'\0'的大小。
	return 0;
}
//打印结果:
/*
7
8
8
1
1
8
8
8
*/
#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = "abcdef";
	printf("%d\n",strlen(arr));//6,该数组arr最后隐藏了一个'\0',strlen计算到'\0'为止
	printf("%d\n",strlen(arr+0));//6,该数组arr最后隐藏了一个'\0',strlen计算到'\0'为止
	printf("%d\n",strlen(*arr));//错误写法,strlen函数参数要传地址
	printf("%d\n",strlen(arr[1]));//错误写法,strlen函数参数要传地址
	printf("%d\n",strlen(&arr));//6,&arr虽然表示整个数组的地址,但是地址是用数组首元素地址来代表的。
	printf("%d\n",strlen(&arr+1));//随机值,&arr表示整个数组的地址,+1跳过整个数组,也跳过了'\0',直到找到后面的'\0'才会停止
	printf("%d\n",strlen(&arr[0]+1));//5,表示从数组的第二的元素地址开始往后计算长度。	
	return 0;
}
//打印结果
/*(注释掉错误写法后)
6
6
6
26
5
*/
#include <stdio.h>
int main()
{
	char* arr = "abcdef";
	printf("%d\n",sizeof(arr));//4/8,这里的arr不能等同于数组,arr就是指针,这个指针指向了"abcdef"这个常量字符串
	printf("%d\n",sizeof(arr+1));//4/8,arr+1就表示该常量字符串第二个元素的地址
	printf("%d\n",sizeof(*arr));//1,对指针解引用得到的就是首元素'a',类型char大小为1字节。
	printf("%d\n",sizeof(arr[0]));//1,arr[0]数组首元素'a'
	printf("%d\n",sizeof(&arr));//4/8,&arr表示arr的地址也就是一个二级指针
	printf("%d\n",sizeof(&arr+1));//4/8,&arr+1也是二级指针,
	printf("%d\n",sizeof(&arr[0]+1));//4/8,表示第二个元素'b'的地址
	return 0;
}
//打印结果:
/*
8
8
1
1
8
8
8
*/
#include <stdio.h>
#include <string.h>
int main()
{
	char* arr = "abcdef";
	printf("%d\n",strlen(arr));//6,arr存放的就是首元素的地址
	printf("%d\n",strlen(arr+1));//5,传入第二个元素的地址
	printf("%d\n",strlen(*arr));//错误写法,strlne必须传地址进去
	printf("%d\n",strlen(arr[0]));//错误写法,strlne必须传地址进去
	printf("%d\n",strlen(&arr));//随机值,&arr是首元素'a'地址的地址,是一个二级指针,会一直找到下一个'\0'为止。
	printf("%d\n",strlen(&arr+1));//随机值,是一个二级指针(不一定在&arr后面),会一直找到下一个'\0'为止。
	printf("%d\n",strlen(&arr[0]+1));//5,传入第二个元素的地址
	return 0;
}
//打印结果:
/*(注释掉错误写法后)
6
5
3
11
5
*/

16.2 二维数组

#include <stdio.h>
int main()
{
	int a[3][4] = {0};
	printf("%d\n",sizeof(a));//48,sizeof(数组名)计算的整个数组的大小
	printf("%d\n",sizeof(a[0][0]));//4,a[0][0]就是首元素
	printf("%d\n",sizeof(a[0]));//16,a[0]是第一行这个一维数组的数组名
	//数组名算是单独放在sizeof内部了,计算的是整个数组的大小,大小是16个字节
	printf("%d\n",sizeof(a[0]+1));//4/8,a[0]是第一行数组名,+1就不算单独放到sizeof中了。
	printf("%d\n",sizeof(*(a[0]+1)));//4,相当于a[0][1],拿到了数组第1行第2列的元素
	printf("%d\n",sizeof(a+1));// 4/8,数组名除俩个的特殊情况外都是数组首元素的地址,
	//这里加1拿到就是第二行的地址,是指针
	printf("%d\n",sizeof(*(a+1)));//16,相当于a[1],计算的第二行的大小
	printf("%d\n",sizeof(&a[0]+1));//4/8,&a[0]+1,a[0]相当于第一行的数组名,&a[0]就是第一行数组的地址,加1跳过第一行,就是第二行数组的地址,是指针。
	printf("%d\n",sizeof(*(&a[0]+1)));//16,计算的是第二行的大小,&a[0]拿到第一行的地址,然后+1拿到第2行的地址,再解引用拿到就是第二行了。
	printf("%d\n",sizeof(*a));//16,a表示的一行的地址,*a就是拿到第一行,
	printf("%d\n",sizeof(a[3]));//16,虽然数组越界了但是,在sizeof的执行过程中是不会使用a[3]的,sizof(a[3])会把a[3]理解为第4行的数组名,
	return 0;
}
//打印结果:
/*
48
4
16
8
4
8
16
8
16
16
16
*/

16.3 指针笔试题

16.3.1 练习1

#include <stdio.h>
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,5
/*
解释:&a拿到的是整个数组的地址,加1后跳过整个数组来到数组最后一个元素后面。也就说明了ptr的指向的地址再5的地址的后面,然后强制类型转换成整型指针,而整型指针-1只会前移动4个字节,ptr会指向5的地址。而a是数组名,表示数组首元素的地址,+1后就来到了第二个元素的地址。
*/

指针的指向

3.2 练习2

#include <stdio.h>
//这个结构体的大小是20个字节
struct test
{
	int Num;
	char* pcname;
	short sdata;
	char cha[2];
	short sba[4];
}*p = (struct Test*)0x100000;
//假设p的值为0x100000.
//已知test结构体的大小是20个字节
int main()
{
	printf("%p\n",p+0x1);
	printf("%p\n",(unsigned long)p+0x1);
	printf("%p\n",(unsigned long*)p+0x1);
	return 0;
}
//打印结果:
/*
00100014
00100001
00100004
*/
/*
解释:这里p指向的地址是0x100000。0x1就是1的16进制表达。p加1,因为p指向类型是struct test,类型大小20个字节,加1后也会跳过20这个字节(转换成16进制就是14).
后面p指针被强制类型转换成了一个无符号长整形的变量,这个类型不是指针变量,加1就是整数加1.
最后一个强制类型转换成了一个无符号长整形指针变量,是指针,然后无符号长整形的大小是4个字节,加1也就是跳过4个字节。
*/

3.3 练习3

#include <stdio.h>
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);//%x为16进制打印
	return 0;
}
//打印结果:4,2000000
/*
解释放下面
*/

ptr1:&a+1表示的跳过这个数组后的地址,也就是4后面的地址,然后被强制类型转换为了int*
ptr2:a是数组首元素的地址,都是被强制类型转换为了整型,然后+1,因为是整型加1就是数字加1.然后又被强制类型转换成了int*。
此时的指向如图所示
指针的指向
因为元素在系统当中是小端存储的,
1会被存储为:01 00 00 00
此时ptr2指向的就是01后面00的位置。
我们又知道数组中1的后面是2。
小端存储就是 01 00 00 00 02 00 00 00
ptr2被强制类型转换为了int*,解引用会一次取出4个字节,取出的就是00 00 00 02,将顺序反转成正常顺序就是02000000,打印时省略掉了第一个0变成了200000
ptr1[-1]的意思就是*(prt1-1).就拿出来4.

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Yui_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值