C语言-指针进阶

目录

前言

一、指针数组与数组指针

1.arr 与 &arr 有什么区别?

2.指针数组

3.数组指针

4.识别数组指针与指针数组

二、数组传参和指针传参

1.一维数组传参

2.二维数组传参

3.数组传参分析

4.指针传参

三、函数指针

1.函数有地址吗?

2.复杂的函数指针

3.运用函数指针数组自定义一个计算器

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

五、回调函数

1.概念理解

2.应用举例

3.快速排序qsort 

六、数组与指针的例题分析

1.整型数组和指针

2.字符数组与指针

1)字符数组

2)字符指针

3.二维数组

总结



前言

        要想学好C语言,就一定要把指针学好!!!

        虽然指针是C语言中较难的一部分,但是如果你真心愿意花时间慢慢学,一定还是能学好的,首先不要被它吓倒了。


一、指针数组与数组指针

1.arr 与 &arr 有什么区别?

arr -- 通常情况下表示数组首元素的地址,但是有两个特殊情况表示整个数组

        1)sizeof(arr) ,计算的是整个数组的大小

        2)&arr ,取出的是整个数组的地址

注意:arr、&arr[0]、&arr 数值上相同,但是表示的意义不同

2.指针数组

本质上是数组,每个元素的类型为指针类型

char* ch_arr[4];
int* i_arr[4];

下面代码是否存在问题?

1    int main()
2    {
3    	int* arr[4];
4    	int a = 1, b = 2, c = 3, d = 4;
5    	int* pa = &a, pb = &b, pc = &c, pd = &d;
6
7    	return 0;
8    }

其实我们建议第5行的代码中,' * ' 靠近 变量名 而不是 类型
这样可以避免我们误认为后面的 pb、pc、pd也是指针类型的变量

3.数组指针

本质是指针,存放的是数组的地址

int arr[5];
int (*p)[5] = &arr;

//演示代码
int main()
{
	int arr1[4] = { 1,2,3,4 };
	/*int arr2[4] = { 2,3,4,5 };
	int arr3[4] = { 3,4,5,6 };*/
	//下标访问数组
	for (int i = 0; i < 4; i++)
	{
		printf("%d ", arr1[i]);//arr[i] <=> *(arr + i)
	}
	printf("\n");
	//指针访问数组
	int* p = arr1;
	for (int i = 0; i < 4; i++)
	{
		printf("%d ", *(p + i));//p[i] <=> *(p + i)
	}
	printf("\n");
	//数组指针访问数组
	int(*pa)[4] = &arr1;
	//p -- &arr
	//*p -- *&arr
	//*p -- arr
	for (int i = 0; i < 4; i++)
	{
		///把(*pa)[i] 写成 *pa[i] 这样的形式
		//编译器可能跑的过去,但这样是存在问题的
		printf("%d ", (*pa)[i]); //*(*pa + i) <=> (*pa)[i]
	}
	printf("\n");
	return 0;
}

运行结果如下:

4.识别数组指针与指针数组

如果你觉得你已经认识了它们,可以试着分析下面的代码分别代表什么

int arr1[10];

int *parr2[8];//指针数组
//类型: int *
//变量名: parr2[8]

int (*parr3)[6];//数组指针
//类型: int ()[6]
//变量名: *parr3

int (*parr4[3])[4];//parr4是数组
//数组中存放的是指针,指针指向的又是数组
//类型: int (*)[4]
//变量名: parr4[3]


二、数组传参和指针传参

1.一维数组传参

一维数组传参,形参是指针
void print(int arr[], int sz)//int arr[] <=> int *arr
{
	for (int i = 0; i < sz; i++)
	{
		//printf("%d ", arr[i]);//*(arr + i) <=> arr[i]
		printf("%d ", *(arr + i));
	}
}

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print(arr, sz);
	return 0;
}

能不能只传递一个参数呢?把数组的大小sz放到函数里面去计算
int sz = sizeof(arr) / sizeof(arr[0]);

因为函数里面的arr表示的不是数组,而是指针了,所以无法正确计算出数组原本的大小了

2.二维数组传参

二维数组的数组名 -- 表示首元素地址
int arr[3][4] = {1,2,3,4, 2,3,4,5, 3,4,5,6};

我们也可以这样理解:
二维数组其实可以看成是一维数组的数组
arr[0] = {1, 2, 3, 4};
arr[1] = {2, 3, 4, 5};
arr[2] = {3, 4, 5, 6};

arr[3][4] = {arr[0], arr[1], arr[2]};

//打印输出时可以做如下替换
arr[i][j] <=> *(*(arr + i) + j)
//函数调用创建形参可以做如下替换
int arr[3][4] <=> int (*arr)[4]

3.数组传参分析

下面哪些一维数组传参方式是正确的呢?

void test1(int arr[]) {}

void test1(int *arr) {}

void test1(int arr[10]) {}

void test2(int *arr2[]) {}

void test2(int *arr2[10]) {}

void test2(int** arr2) {}

int main()
{
	int arr1[4] = { 0 };
	int* arr2[10] = { 0 };
	test1(arr1);
	test2(arr2);
	return 0;
}


//都是正确的
下面哪些二维数组传参方式是正确的呢?

void test(int arr[3][4]) {}

void test(int arr[3][]) {}

void test(int arr[][4]) {}

void test(int* arr[4]) {}

void test(int(*arr)[4]) {}

void test(int** arr) {}

int main()
{
	int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
	test(arr);
	return 0;
}



//1、3、5、6

//1、2、3.行可以省略,但是列不行。
//如果我们只知道列数,也能够判断一行有多少个元素
//如果我们只知道行数,就不知道一行有多少个元素了
//4.指针数组类型
//5.数组指针类型
//6.二级指针

4.指针传参

一级指针传参

void print(int* p, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
}

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int *p = arr;
    //一级指针p传参
	print(p, sz);
	return 0;
}



思考:当函数的参数部分为一级指针的时候,这个函数能够接收什么?
int a;
print(&a, sz);//地址
int b;
int p = &b;
print(p, sz);//指针变量
int arr[10];
print(arr, sz);//数组名

二级指针传参

void print(int** pstr)
{
	printf("%d ", **pstr);
}

int main()
{
	int num = 10;
	int* p = &num;
	int** pp = &p;
	print(&p);
	print(pp);
	return 0;
}

思考:当函数的参数部分为一级指针的时候,这个函数能够接收什么?
print(&p);//一级指针的地址
print(pp);//二级指针变量
int *arr[10];
print(arr);//指针数组


三、函数指针

字符指针    char*        
整型指针    int*
数组指针    int (*)[10]    指向数组的指针
函数指针    ???            指向函数的指针

1.函数有地址吗?

如果你觉得已经认识函数指针了,
请试着补充下面的函数指针吧

char* test(int x, float* y)
{

}

int main()
{
	pt = test;
	return 0;
}

//char* (*pt)(int, float*) = test;


void** test(int** x, char y, double* z)
{

}

int main()
{
	pt = test;
	return 0;
}

//void** (*pt)(int**, char, double*) = test;

2.复杂的函数指针

接下来我们可以试试挑战难度更大的

(*(void(*)())0)();
//上述代码是一次函数调用
//其中 void(*)() 是函数指针类型
//放到括号里面表示强制类型转换
//把 0 强制类型转换为 函数指针类型
//然后解引用,调用 0地址处的这个函数
//';'前的一对括号用于传参,此函数的参数类型为空void

void ( *signal(int, void(*)(int)) )(int);
//上述代码是一次函数声明
//一个函数,除去它的名字与参数,剩下的就是它的返回类型
//函数与参数  signal(int, void(*)(int))
//返回类型是一个函数指针  void (*)(int);
//signal函数有两个参数,一个是 int 类型的,另一个是 void (*)(int) 类型的函数指针
//signal函数的返回值也是一个函数指针



3.运用函数指针数组自定义一个计算器

指针数组 -- 每个元素的类型为 指针类型
函数指针数组 -- 每个元素的类型为 函数指针类型
这次我们不使用switch选择语句,用今天学习的内容


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

void menu()
{
	printf("******************\n");
	printf("*** 简易计算器 ***\n");
	printf("****   0.exit ****\n");
	printf("*****  1.Add *****\n");
	printf("*****  2.Sub *****\n");
	printf("*****  3.Mul *****\n");
	printf("*****  4.Div *****\n");
	printf("******************\n");
}

//函数指针数组实现计算器
int main()
{
	int input = 0;
	int x = 0, y = 0;
	int ret = 0;
	int (*pfArr[5])(int, int) = { 0, Add, Sub, Mul, Div };
	do
	{
		menu();
		printf("请选择需要进行的操作:>");
		scanf("%d", &input);
		if (0 == input)
		{
			printf("退出程序\n");
		}
		else if (input >= 1 && input <= 4)
		{
			printf("请选择两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = pfArr[input](x, y);
			printf("%d\n", ret);
		}
		else
		{
			printf("非法输入!\n");
		}
	} while (input);
	return 0;
}

运行截图


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

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

int my_strlen(const char* str)
{
	... ...
    return 0;
}

int main()
{
	//指针数组
	char* arr1[4] = { 0 };
	//数组指针
	char arr2[] = "abcd";
	char(*p)[5] = &arr2;
	//函数指针
	int (*pf)(const char*) = my_strlen;
	//函数指针数组
	int (*pfArr[10])(const char*);
	return 0;
}

演示代码:

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

int Sub(int x, int y)
{
	return x - y;
}

int main()
{
	int (*pf)(int, int) = Add;
	//函数指针数组
	int (*pfArr[4])(int, int) = { Add, Sub };
	//指向函数指针数组的指针
	int (*(*ppfArr)[4])(int, int) = &pfArr;
	return 0;
}


五、回调函数

1.概念理解

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

2.应用举例

我们依然使用上文的计算器代码作为示例

1)如果使用switch语句,我们可以发现存在很多重复的代码

2)在这里我们可以巧妙地运用回调函数解决代码冗余的问题

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

void menu()
{
	printf("******************\n");
	printf("*** 简易计算器 ***\n");
	printf("****   0.exit ****\n");
	printf("*****  1.Add *****\n");
	printf("*****  2.Sub *****\n");
	printf("*****  3.Mul *****\n");
	printf("*****  4.Div *****\n");
	printf("******************\n");
}

void Calc(int (*pf)(int, int))
{
	int x = 0, y = 0;
	int ret = 0;
	printf("请选择两个操作数:>");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("%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;
}

3.快速排序qsort 

qsort是一个库函数,可以排序任意类型的数据,头文件是 <stdlib.h>
这里我们不关心qsort是如何实现排序的,只需要学会如何使用就够了

//void qsort(void* base,//指向待排序数组的第一个元素
//	size_t num,		  //待排序的元素个数
//	size_t size,	  //每个元素的大小(字节)
//	//指向一个函数,这个函数可以比较两个数据的大小
//	int (*compare)(const void*, const void*)
//);

//注意: void* 类型的指针不能进行解引用操作,也不能进行加一减一操作
//void* -- 无具体类型的指针,可以接收任何类型的地址

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//qsort使用者需要自己实现该函数
int cmp_int(const void* buf1, const void* buf2)
{
	return *(int*)buf1 - *(int*)buf2;
}
//测试qsort排序整型数组
void test1()
{
	int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//用qsort排序一个整型数组,我们需要创建一个函数,能够比较两个整型的大小
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

//测试qsort排序结构体类型的数据
struct Hum
{
	char name[20];
	int age;
};

int cmp_H_age(const void* buf1, const void* buf2)
{
	return ((struct Hum*)buf1)->age - ((struct Hum*)buf2)->age;
}

int cmp_H_name(const void* buf1, const void* buf2)
{
	return strcmp(((struct Hum*)buf1)->name, ((struct Hum*)buf2)->name);
}

void print_H(struct Hum* h, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d, %s\n", (h + i)->age, (h+i)->name);
	}
}

void test2()
{
	struct Hum h[] = { {"zhangshan", 17},{"lisi", 19},{"wangwu", 38} };
	int sz = sizeof(h) / sizeof(h[0]);
	qsort(h, sz, sizeof(h[0]), cmp_H_age);
	print_H(h, sz);
	qsort(h, sz, sizeof(h[0]), cmp_H_name);
	print_H(h, sz);
}

int main()
{
	test1();
	test2();
	return 0;
}

运行截图


六、数组与指针的例题分析

1.整型数组和指针

数组 -- 能够存放一组相同类型的元素,数组的大小取决于它的元素个数和元素类型
指针 -- 泛指地址,也可以是指针变量,大小是4/8个字节

数组名(arr) -- 通常情况下表示数组首元素的地址,但是有两个特殊情况表示整个数组

        1)sizeof(arr) ,计算的是整个数组的大小

        2)&arr ,取出的是整个数组的地址

注意:arr、&arr[0]、&arr 数值上相同,但是表示的意义不同



分析下面代码的运行结果

int main()
{
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(a + 0));
	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));
	printf("%d\n", sizeof(&a + 1));
	printf("%d\n", sizeof(&a[0]));
	printf("%d\n", sizeof(&a[0] + 1));
	return 0;
}

2.字符数组与指针

1)字符数组

strlen() 与 sizeof()

strlen() 是库函数,头文件是 <string.h> ,strlen函数只针对字符串,
求的是字符串的长度,本质上统计的是\0之前出现的字符个数

sizeof() 是操作符,计算的是占用内存空间的大小,单位是字节,
不关注内存中存放的内容

分析下面代码的运行结果

#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", sizeof(arr));
	printf("%d\n", sizeof(*arr));
	printf("%d\n", sizeof(arr + 0));
	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));
	printf("%d\n", strlen(arr + 0));
	printf("%d\n", strlen(arr[1]));
	printf("%d\n", strlen(&arr));
	printf("%d\n", strlen(&arr + 1));
	printf("%d\n", strlen(&arr[0] + 1));
	return 0;
}

思考:如果把 arr 改为 "abcdef" 呢?
分析结果

#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
    //arr表示整个数组,1*6个字节
	printf("%d\n", sizeof(arr));//6
    //arr表示首元素地址,*arr表示第一个元素
	printf("%d\n", sizeof(*arr));//1
    //arr+0 表示首元素的地址
	printf("%d\n", sizeof(arr + 0));// 4/8
    //数组下标为1的元素
	printf("%d\n", sizeof(arr[1]));//1
    //&arr整个数组的地址
	printf("%d\n", sizeof(&arr));// 4/8
    //跳过整个数组,指向数组后面一个位置
	printf("%d\n", sizeof(&arr + 1));// 4/8
    //指向第二个元素的地址
	printf("%d\n", sizeof(&arr[0] + 1));// 4/8
    
    //strlen统计\0前面字符类型的元素的个数
    //自动在内存中寻找\0,我们无法知道什么时候在哪里找到\0,所以是随机值
    //arr表示整个数组,strlen遍历整个数组都没有找到\0,自动向后寻找
	printf("%d\n", strlen(arr));//随机值
    //分析同上
	printf("%d\n", strlen(arr + 0));//随机值
    //strlen函数里面的数据应该为开始统计的地址
	printf("%d\n", strlen(*arr));//非法访问
    //'b' - 98 ,访问地址为98处的内容
	printf("%d\n", strlen(arr[1]));//非法访问
    //arr表示整个数组的地址,这个随机值R与上面代码的随机值不同
	printf("%d\n", strlen(&arr));//随机值R
    //跳过了整个数组(大小为6个字节),虽然还是随机值,但是比随机值R小6个字节
	printf("%d\n", strlen(&arr + 1));//随机值R-6
    //跳过了一个字节,虽然还是随机值,但是比随机值R小1个字节
	printf("%d\n", strlen(&arr[0] + 1));//随机值R-1
	return 0;
}

---------- ---------- ---------- ----------

int main()
{
	char arr[] = "abcdef";
	printf("%d\n", sizeof(arr));// 7
	printf("%d\n", sizeof(*arr));// 1
	printf("%d\n", sizeof(arr + 0));// 4/8
	printf("%d\n", sizeof(arr[1]));// 1
	printf("%d\n", sizeof(&arr));// 4/8
	printf("%d\n", sizeof(&arr + 1));// 4/8
	printf("%d\n", sizeof(&arr[0] + 1));// 4/8

	printf("%d\n", strlen(arr));// 6
	printf("%d\n", strlen(*arr));// error
	printf("%d\n", strlen(arr + 0));// 6
	printf("%d\n", strlen(arr[1]));// error
	printf("%d\n", strlen(&arr));// 6
	printf("%d\n", strlen(&arr + 1));// 随机值
	printf("%d\n", strlen(&arr[0] + 1));// 5 从第二个元素的地址开始访问
	return 0;
}

2)字符指针

分析下面代码的运行结果

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

int main()
{
	const 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));
	return 0;
}
分析结果

int main()
{
	const char* p = "abcdef";
	printf("%d\n", sizeof(p));// 4/8
	printf("%d\n", sizeof(p + 1));//'b'的地址 4/8
	printf("%d\n", sizeof(*p));//1
	printf("%d\n", sizeof(p[0]));//'a' 1
	printf("%d\n", sizeof(&p));// 4/8
	printf("%d\n", sizeof(&p + 1));// 4/8
	printf("%d\n", sizeof(&p[0] + 1));//'b'的地址 4/8

	printf("%d\n", strlen(p));//6
	printf("%d\n", strlen(p + 1));//p+1是'b'的地址 5
	printf("%d\n", strlen(*p));//error
	printf("%d\n", strlen(p[0]));//error
	printf("%d\n", strlen(&p));//随机值
	printf("%d\n", strlen(&p + 1));//随机值
	printf("%d\n", strlen(&p[0] + 1));//&p[0]+1是'b'的地址 5
	return 0;
}

3.二维数组

分析下面代码的运行结果

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

int main()
{
	int a[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
	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]));
	return 0;
}
分析结果

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

int main()
{
	int a[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
	//计算整个二维数组的大小,4*12
	printf("%d\n", sizeof(a));//48
	//计算第一行第一个元素的大小
	printf("%d\n", sizeof(a[0][0]));//4
	//我们可以把二维数组看作是一维数组的数组
	//计算二维数组中第一个数组的大小,4*4
	printf("%d\n", sizeof(a[0]));//16
	//计算a[0][1]的地址的大小
	printf("%d\n", sizeof(a[0] + 1));//8
	//计算a[0][1]的大小
	printf("%d\n", sizeof(*(a[0] + 1)));//4
	//跳过二维数组中的第一个一维数组,指向第二个一维数组
	printf("%d\n", sizeof(a + 1));//8
	//计算二维数组中第二个数组的大小,4*4
	printf("%d\n", sizeof(*(a + 1)));//16
	//&a[0]是第一行的地址,&a[0] + 1是第二行的地址
	printf("%d\n", sizeof(&a[0] + 1));//8
	//计算二维数组中第二个数组的大小,4*4
	printf("%d\n", sizeof(*(&a[0] + 1)));//16
	//a就是二维数组中第一个数组的地址
	printf("%d\n", sizeof(*a));//16
	//注意,这里并没有越界
	//a[3]表示维数组中的第四个一维数组的地址
	//如果存在第四个一维数组,那么它的大小应当与前面三个一维数组的大小相等
	printf("%d\n", sizeof(a[3]));//16
	return 0;
}


总结

        要想学好C语言,就一定要把指针学好!!!

希望本文能够对你有所帮助,也欢迎广大读者批评指正<^w^>

猜你喜欢

转载自blog.csdn.net/weixin_72501602/article/details/129214372