【C语言】指针篇(指针数组,数组指针,函数指针,一级、二级指针)

一、指针基础

1.什么是指针

C语言中指针是一种数据类型,指针是存放数据的内存单元地址。

指针是内存中一个最小单元(1个字节)的编号,也就是地址
平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量

我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个
变量就是指针变量。

后面提到的指针均是指 指针变量。

总结:指针就是地址。

2.指针的定义和初始化

语法格式:数据类型 *指针变量名[=初始地址值]。

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

指针变量除了可以存放变量的地址外,还可以存放其他数据的地址,比如可以存放数组和函数的地址,后面都会讲解。

指针变量的初始化,除了可以是已定义变量的地址,也可以是已初始化的同类型的指针变量,也可以是NULL(空指针)。在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个NULL 值是一个良好的编程习惯。

int *p = NULL;

3.指针的解引用

取地址操作符&和解引用操作符*是搭配使用的。

int a = 10;
int* pa = &a;
printf("%d\n", *pa);

将a的地址通过&存放到指针变量pa中,再对pa解引用找到地址中的内容:a中存放的数据10

4.野指针和空指针

空指针:指针赋值为NULL,不指向任何空间。

对指针解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未指向任何东西,因为对一个NULL指针解引用是一个非法的操作,所以在解引用之前,必须确保它不是一个NULL指针。

野指针:

野指针就是指针指向的位置是不可知的。

野指针是学习C语言中比较常犯的一个错误,野指针分为三种:

1.指针未初始化就使用

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

2.指针越界访问

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

3.指针指向的空间释放后未置空

指针指向的空间释放后指向的就是无效内存,这时指针应立即置NULL,不然会成为野指针。
具体内容等后面动态内存开辟的时候总结。

规避野指针:

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向空间释放,及时置NULL
  4. 避免返回局部变量的地址 (当被调函数结束后 ,栈区中局部变量的内存空间被释放)
  5. 指针使用之前检查有效性 (是否为NULL)

5.指针的类型

指针的类型与指针指向的类型是不一样的,一定要区分清楚!

从语法的角度看,只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。
把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。

例如:

int* ptr; //指针的类型是int*	 指针所指向的类型是int
char* ptr;//指针的类型是char*  指针所指向的类型是char
int** ptr;//指针的类型是int**  指针所指向的类型是int*

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

在这里插入图片描述

char* 类型的指针存放 char 类型变量的地址,每走一步跳过一个char类型即1个字节。
short* 类型的指针存放 short 类型变量的地址,每走一步跳过一个short类型即2个字节。
int* 类型的指针存放 int 类型变量的地址,每走一步跳过一个int类型即4个字节。

6.指针的大小

对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平和低电平(1或者0),那么32根地址线产生就会产生2^32 次方个地址,内存中是以字节为单位的,也就是会有 2^32 个字节。2^32 Byte = 2^22 KB = 2^12 MB = 4GB 的空间进行编址;同样64位机器中可以编址8GB空间。

所以32位平台上,一个指针变量的大小就是4字节;64位平台上是8字节。

7.指针的运算

指针 + - 整数: 上面分析指针的类型中提过,指针的类型决定了指针+1或者-1移动的距离就是指针所指向类型的大小。比如:int* 类型的指针+1 则往后走了4个字节,char* 类型指针-2 则往前移动了2个字节。

指针 - 指针:两个指针相减代表指针之间所经历的元素(由参与运算的指针类型决定)个数
例如写一个求字符串长度的函数:

int my_strlen(char *s)
{
	char *p = s;
	while(*p != '\0' )
	{
		p++;
	}
	return p-s;
}

8.指针和数组

前面讲到过,指针的类型决定了指针向前或者向后走一步有多大距离,比如:char* 类型的指针存放 char 类型变量的地址,每走一步跳过一个char类型即1个字节;int* 类型的指针存放 int 类型变量的地址,每走一步跳过一个int类型即1个字节。 所以,我们可以也通过指针来访问数组

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;//数组首元素的地址存放p中
	for (int i = 0; i < 10; i++)
	{
		//printf("%d ", arr[i]);或者printf("%d ", *(arr + i));
		printf("%d ", *(p + i));//printf("%d ", p[i]);也是正确的
	}
	return 0;
}

上述中四种写法都是正确的,在数组篇我们了解到,除了&(数组名)和sizeof(数组名)这两种特殊情况外,其他情况下数组名表示首元素的地址指针变量p也表示数组首元素的地址,所以这四种写法本质上都是一样的。

9.指针和字符串

char* ps = "abcdef";
printf("%s\n", ps);
char s[] = "abcdef";
char* ps = s;
printf("%s\n", ps);

注:并不是将字符串赋予指针变量,而是将存放字符串的连续内存单元的首地址赋予指针变量。

看下面这道例题:

在这里插入图片描述

p1和p2指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当
几个指针指向同一个字符串的时候,他们实际会指向同一块内存。所以p1和p2相等。
但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块(数组存放在栈区),所以s1与s2的地址不同。

可修改性

char* ps = "hello";
p[1] = 'a';//错误

char str[] = "hello";
str[0] = 'w';//正确

因为ps指向的是一个字符串常量,字符串字面量通常存储在程序的只读内存区域(如常量区),因此不能修改。而str是一个字符数组,是存储在栈上的,可以被修改。

10.二级指针

指针变量存放其他数据的地址,而指针变量的地址是存放在二级指针中。

#include<stdio.h>
int main()
{
	int a = 0;
	int* pa = &a;
	int** ppa = &pa;

	**ppa = 25;
	printf("%d\n", a);
	printf("%d\n", *pa);
	printf("%d\n", **ppa);
	return 0;
}

**ppa = *(*ppa) = *pa = a
:二级指针指向的类型是指针类型,所以二级指针+1等于加上4字节(32位)或者8字节(64位)

二、常量指针和指针常量

常量指针——指向常量的指针

常量指针本质上是一个指针,常量表示指针指向的内容,说明该指针指向一个“常量”。
常量是不可改变的,所以指针指向的内容是不可改变的;但指针的指向可以改变。

指针常量——指针类型的常量
本质上是一个常量,指针用来说明常量的类型,表示该常量是一个指针类型的常量。
指针自身的值是一个常量,不可改变,始终指向同一个地址,在定义的同时必须初始化。但指针指向的内容可以变。

二者可以通过const和*的相对位置来区分:

const*的左边:常量指针
const*的右边:指针常量

常量指针:

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	const int* p = &a;		//p是常量指针
	//int const* p = &a;	//这两种写法都一样,只要const在*
	//*a = 100;		//错误:指针所指空间的值不能改变
	p = &b;			//指针的指向可以改变
	return 0;
}

指针常量:

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int* const p = &a;//指针常量
	//p = &b;//错误:指针的指向不能改变
	*p = 100;//指针所指空间的值可以改变
	return 0;
}

三、指针数组和数组指针

1.指针数组

指针数组是数组,数组指针是指针,只需要看末尾名词确定类型。

指针数组的定义

指针数组就是存放指针的数组,本质上是数组。

例如:

int* arr[5];

arr是一个数组,数组中有5个元素,每个元素的类型是int*整型指针。

首先,通过前面操作符篇我们知道[]的优先级比* 高,所以arr先与[]结合,确定是一个数组,数组中每个元素是都是指向int类型的指针,所以是指针数组。

指针数组的使用

#include<stdio.h>
int main()
{
	int arr[3][5] = { 1,2,3,4,5,6,7,8 };
	int* p[3] = { arr[0], arr[1], arr[2] };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ", p[i][j]);//printf("%d ", *(*(p + i) + j));
		}
		printf("\n");
	}
	return 0;
}

p首先是一个数组,数组中的3个元素是int*类型,将二维数组的每一行元素的首地址赋给p中各元素初始化,p的用法相当于一个二维数组。

在这里插入图片描述

2.数组指针

数组指针的定义

数组指针就是指向数组的指针,本质上是指针。

例如:

int (*arr)[5];

由于[]的结合性比*高,所以加上()使arr先与*结合,表示arr是一个指针,这个指针指向的类型是int[5],即指向一个长度为5的整型数组,所以是数组指针。

数组指针的使用

一维数组

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3 };
	int (*p)[10] = &arr;
	for(int i = 0; i < 10; i++)
		printf("%d ", (*p)[i]);
	return 0;
}

*p相当于*(&arr) = arr 数组首元素的地址 或者由定义可得*p的类型是int[10],即数组

二维数组

#include<stdio.h>
int main()
{
	int arr[3][5] = { 1,2,3,4,5,6,7,8 };
	int(*p)[5] = arr;
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ", p[i][j]);//printf("%d ", *(*(p + i) + j));
		}
		printf("\n");
	}
	return 0;
}

p首先是一个指针,指向一个整型数组,这个数组中有5个int类型的数据,所以p+1是跳过这个数组即跳过5个int类型的大小,而通过前面数组篇我们知道二维数组在内存中是连续存储的,也就是说二维数组的下一行首元素的地址是与上一行末尾元素的地址相邻的。所以数组指针p可以用来访问二维数组。
在这里插入图片描述

3.练习

指针数组和数组指针已经了解过了,我们来做几个练习:

指针数组

int* p1[10];

p1是一个数组,数组中有10个元素,每个元素的类型是int*

数组指针

int(*p2)[10];

p2是一个指向数组的指针,指向的数组中有10个元素,每个元素的类型是int

数组指针数组

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

p3先与[10]结合是一个数组,数组中有10个元素,每个元素的类型是int(*)[5]
p3是一个存放数组指针的数组

指针数组指针

int* (*p)[5];

p4先与*结合是一个指针,指向一个数组,数组中有5个元素,每个元素的类型是int*,所以这个数组是指针数组。
p4是一个指向指针数组的指针。

如何判断指针的类型呢?

指针的类型中提到过,现在又多加了指针数组和数组指针的判断

把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型;
把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。

int* ptr; //指针的类型是int*	 指针所指向的类型是int
char* ptr;//指针的类型是char*  指针所指向的类型是char
int** ptr;//指针的类型是int**  指针所指向的类型是int*
int(*ptr)[10];//指针的类型是int(*)[10] 指针所指向的的类型是int[10]
int*(*ptr)[5];//指针的类型是int*(*)[5] 指针所指向的的类型是int*[5]

例如:
将arr强制类型转换成与p相同类型

int(*p)[10] = (int(*)[10])arr;

四、数组传参和指针传参

数组篇虽然写过,但在这里再强调一遍加深巩固。
通常情况下,我们所说的数组名都是数组首元素的地址,但有两种特殊情况:

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

1.一维数组传参

#include <stdio.h>
void test(int a[])//√
{}			//数组传参时可以不写元素个数
void test(int a[10])//√
{}			//正常传参
void test(int* a)//√
{}			//数组传参传的是数组首元素的地址,指针就是地址,接收正常
void test(int (*a)[10])//√
{}		//数组指针,指向有10个int元素的数组,符合arr条件,可以接收
void test2(int* a[20])//√
{}			//正常传参
void test2(int** a)//√
{}//arr2是指针数组,所指数组中的元素类型是指针,数组名又是首元素地址,指针的地址用二级指针接收

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

2.二维数组传参

二维数组在定义时行数可以省略,但列数不能省略,因为可以根据列数确定行数,但不能根据行数确定列数。
同样,二维数组传参时,函数形参只能省略第一个[]的数字。
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素,这样方便运算

void test(int arr[3][5])//√
{}			//正常传参
void test(int arr[][])//×
{}		    //不可以省略列数
void test(int arr[][5])//√
{}			//可以省略行数
void test(int* arr)//×
{}			//二维数组传的是第一行数组的地址,数组的地址不能用一级指针来接收
void test(int* arr[5])//×
{}			//这是存放指针的数组,不是指针,也不是二维数组,完全不搭边
void test(int(*arr)[5])//√
{}			//数组指针,指向的数组有5个int元素,可以接收二维数组第一行数组的地址
void test(int** arr)//×
{}			//二级指针是用来接收一级指针的地址的,不能接收数组的地址
int main()
{
	int arr[3][5] = { 0 };
	test(arr);
}

所以,二维数组的传参只有三种方式:行列都不省略,只省略列数,用数组指针。

注:数组地址与数组首元素的地址不一样! 数组地址解引用才能得到数组首元素的地址

3.一级指针传参

#include <stdio.h>
void Print(int *p, int sz)
{
	for(int 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 sz = sizeof(arr)/sizeof(arr[0]);
	int *p = arr;//p指向数组首元素的地址
	Print(p, sz);//一级指针p,传给函数
	return 0;
}

当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

#include <stdio.h>
void test(int* p)
{}
int main()
{
	int a = 10;
	test(&a);//变量的地址

	int* p = &a;
	test(p);//一级指针

	int arr[5];
	test(arr);//数组首元素的地址

	return 0;
}

4.二级指针传参

#include <stdio.h>
void test(int** ptr)
{
	printf("num = %d\n", **ptr);
}
int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
	test(ppa);//传二级指针
	test(&pa);//传一级指针的地址
	return 0;
}

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

#include <stdio.h>
void test(int** ptr)
{}
int main()
{
	int* p;
	test(&p);//一级指针的地址

	int** pp;
	test(pp);//二级指针

	int* arr[10];//指针数组,指向的数组的元素是int*指针类型
	test(arr);//传的是数组首元素的地址即int*指针类型的地址,可以用二级指针接收

	return 0;
}

五、函数指针

函数指针就是指向函数的指针变量。
函数指针可以像一般函数一样,用于调用函数、传递参数。

在这里插入图片描述

函数指针的使用

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int(*pf)(int, int) = Add;//int(*pf)(int, int) = &Add;
	int ret = pf(3, 5);//int ret = (*pf)(3, 5);两种写法都可以
	printf("%d\n", ret);
	return 0;
}

注:&函数名和函数名都表示函数的地址

嘿嘿,来看两段有趣的代码

第一段代码来自《C陷阱与缺陷》

	(*(void (*)())0)();
分析:void(*)() 表示一个函数指针类型,所指函数的参数为空即没有参数,返回类型是void(void(*)())00进行强制类型转换,转换成一个函数指针类型,0被当成一个函数的地址;
	(*(void (*)())0)() 调用0地址处的函数,该函数的返回值是void,无参

第二段代码:

	void (* signal(int, void(*)(int)))(int);
分析:这是一次函数的声明,声明的函数名叫signal;
	 signal函数的参数有两个,第一个是int类型,第二个是void(*)(int)函数指针类型
	 该指针指向的函数的参数类型是int,返回类型是void;
	 
	 把signal(int, void(*)(int))去掉再看,signal函数的返回类型是一个void(*)(int)函数指针
	 该指针指向的函数的参数类型是int,返回类型是void

不难发现signal函数的返回类型第二个参数类型相同,都是void(*)(int)类型
所以可以给void(*)(int)类型重命名来简化代码;

typedef void(*)(int) pf_t;//错误写法

这样写符合逻辑,但不符合语法,应当这样写

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

这样就更好理解了:声明一个叫signal的函数,该函数的第一个参数类型是int,第二个参数类型是pf_t,返回类型也是pf_t;而pf_t是一个void(*)(int)函数指针类型,该指针指向的函数的参数是int类型,返回类型是void

六、函数指针数组

把函数的地址存到一个数组中,那这个数组就叫函数指针数组,函数指针数组中存放的是函数的地址。

比如说写一个加减乘除的计算器:

#include <stdio.h>
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 (*pf[5])(int, int) = { NULL, Add, Sub, Mul, Div };//函数指针数组 存放函数地址
	int x = 0, y = 0, ret = 0;
	while (1)
	{
		printf("***1.Add  2.Sub***\n");
		printf("***3.Mul  4.Div***\n");
		printf("***   0.exit   ***\n");
		printf("请选择功能:->");
		int input = 0;
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:->");
			scanf("%d %d", &x, &y);
			ret = pf[input](x, y);
			printf("%d\n", ret);
		}
		else if(input == 0)
		{
			printf("已退出\n");
			break;
		}
		else
		{
			printf("输入错误,请重新选择\n");
		}
	}
	return 0;
}

七、指向函数指针数组的指针

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

#include <stdio.h>
void test(const char* str)
{
	printf("%s\n", str);
}
int main()
{
	//函数指针pf
	void (*pf)(const char*) = test;
	//函数指针的数组pfArr
	void (*pfArr[5])(const char* str);
	pfArr[0] = test;
	//指向函数指针数组pfArr的指针ppfArr
	void (*(*ppfArr)[5])(const char*) = &pfArr;
	return 0;
}

八、回调函数

回调函数就是一个通过函数指针调用的函数

如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

注: 回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

简单回调函数示例:

#include <stdio.h>
void test()
{
	printf("hehe\n");
}
void print_hehe(void(*pf)())
{
	if (1)
		pf();
}
int main()
{
	//test();
	print_hehe(test);
	return 0;
}

练习: 使用回调函数,模拟实现qsort函数(采用冒泡的方式)
库函数qsort的使用方法具体请查阅参考手册,这里简单示范下:

#include <stdio.h>
int int_cmp(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}

int main()
{
	int a[] = { 1,2,5,6,9,3,8,4,7,0 };
	int len = sizeof(a) / sizeof(a[0]);
	qsort(a, len, sizeof(a[0]), int_cmp);
	for (int i = 0; i < len; i++)
		printf("%d ", a[i]);
	return 0;
}

接下来利用回调函数模拟实现qsort函数(万能通用适用于各种类型排序):

#include<stdio.h>
void Swap(char* buf1, char* buf2, int width)
{
	for (int i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}
//整型排序
int int_cmp(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;//被排序元素什么类型就用什么指针
}

//指针数组排序
int pa_cmp(const void* e1, const void* e2)
{
	return strcmp(*(char**)e1, *(char**)e2);
}

//改进qsort 排序任意类型的数组
void bubble_sort(void* base, int len, int width, int (*cmp)(const void* e1, const void* e2))
{
	for (int i = 0; i < len - 1; i++)
	{
		for (int j = 0; j < len - 1 - i; j++)
		{
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
		}
	}
}

int main()
{
	char* pa[] = { "abce", "bcde", "abcd"};
	int len = sizeof(pa) / sizeof(pa[0]);
	bubble_sort(pa, len, sizeof(pa[0]), pa_cmp);
	for (int i = 0; i < len; i++)
		printf("%s ", pa[i]);
	return 0;
}

注:内存中最小存储单元是字节,char类型是1字节大小,所以可以将各种类型的数据拆分成单个字节,通过单个字节地进行调整,可以满足各种类型的排序,实现qsort功能。

总结完毕,指针的内容就到这里,感谢大家的观看。

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-10 04:50:04       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-10 04:50:04       100 阅读
  3. 在Django里面运行非项目文件

    2024-04-10 04:50:04       82 阅读
  4. Python语言-面向对象

    2024-04-10 04:50:04       91 阅读

热门阅读

  1. C语言编译过程

    2024-04-10 04:50:04       32 阅读
  2. [C++/Linux] UDP编程

    2024-04-10 04:50:04       35 阅读
  3. 【LeetCode热题100】【二叉树】二叉树的层序遍历

    2024-04-10 04:50:04       42 阅读
  4. 经典面试排序题(快排堆排)

    2024-04-10 04:50:04       34 阅读
  5. SVN(Subversion)代码版本管理

    2024-04-10 04:50:04       34 阅读
  6. linux查看用户登录情况

    2024-04-10 04:50:04       30 阅读
  7. python | ttkbootstrap,一个神奇的 Python 库!

    2024-04-10 04:50:04       35 阅读
  8. Macbook M1版安装安卓模拟器

    2024-04-10 04:50:04       34 阅读