深度了解c语言指针

1. 指针初阶

1.1 什么是指针

在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元

1.2 指针类型

指针变量就是一个变量,它存储的内容是一个指针。变量的指针就是变量的地址,该地址存储着变量的内容。而指针变量所存储的内容就是一个地址,这个地址指向另外一个变量。同时指针变量依旧是个变量,是变量他就依然有属于自己的地址。
举个例子,我们可以将指针变量当作一张银行卡,卡上存储了你在该银行的账户信息。当我们定义变量的时候,我们需要确定他到底是int型char型,float型还是double类型等等。在定义指针变量时也是一样的,必须确定指针类型。对应类型的变量需要用对应类型的指针存储。就像你的农行卡只能存储你的农行的账户信息,建行卡只能存储你建行的账户信息一样。
指针

1.3 野指针

野指针,按照名字而言,就是指向位置不明确的指针。(随机的,不明确的,没有限制的)
那么野指针是如何造成的呢?
产生野指针的主要原因是:
①指针未初始化:

#include<stdio.h>
int main()
{
	int*a;//局部变量指针在定义的时候没有进行初始化,默认为随机值
	return 0;
}

②指针越界访问:

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

③指针指向空间释放。
当指针所指向的那个对象,被释放(free)后,p就会变成野指针。
如何避免野指针:

①在定义指针的时候对指针进行初始化
②在使用指针的时候注意,防止指针越界
③释放掉指针所指内容后,指针即置NULL
④在使用指针前检查指针是否有效。

1.4 指针运算

1.4.1 指针+(-)整数

#include<>stdio.h>
#define SIZE 15
int main()
{
	float value[SIZE];
	float* p;
	for(int p=&value[0];p<&value[SIZE])
	{
		*p=0;
		p=p+1;//因为int类型占四个字节,所以将指针+1就是将指针所存内容+4。
	}
	return 0;
}

1.4.2 指针-指针

//模拟实现strlen()函数。
int my_Strlen(char* s)
{
	char* p=s;
	while(*p!='\0')
	p++;
	return p-s;
}

1.4.3 指针的关系运算

#include<>stdio.h>
#define SIZE 15
int main()
{
	float value[SIZE];
	float* p;
	for(int p=&value[0];p<&value[SIZE])
	//p<&value[SIZE]将指针指向的地址与变量value[SIZE]的地址进行比较。
	{
		*p=0;
		p=p+1;
	}
	return 0;
}

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

1.5 二级指针

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

a的地址存放在pa中,pa的地址存放在ppa中。所以pa是一级指针,ppa为二级指针

2. 指针进阶

2.1 字符指针

在一般情况下,字符指针(char*)指向一个字符变量,

#include<stdio.h>
int main()
{
	char ch='a';//定义并初始化一个字符
	char* p=&ch;//定义一个字符指针(char*)指向ch;
	if(ch==(*p))
	printf("ch=*p");
	return 0;
}

还有一种情况,是字符指针指向一个字符串的首地址。

#include<stdio.h>
#include<string.h>
int main()
{
	char* str="hello .c";//这里实际上是将字符串的首地址传给了str;
	printf("%s",str);
	return 0;
}

经典面试例题:

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

求输出。
在这里插入图片描述
为什么str3等于str4呢?

实际上:str3和str4都是创建一个字符指针指向一个字符串常量,而字符串常量创建在常量区,当几个指针同时指向同一个常量的时候,那么他们将指向同一块内存。但用相同的常量去初始化数组的时候,则会开辟新的空间,所以str1不等于str2,str3等于str4。

2.2 指针数组与数组指针

2.2.1 指针数组

指针数组:就名字而言他肯定是个数组,以指针的形式来访问一个数组,所以叫指针数组

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

运行结果为:运行结果

可见数组名和数组的首地址是一样的。由此可得出结论:数组名表示的是数组首元素的地址。
既然数组名可以表示收地址,那么把数组名传给一个指针,可以用指针访问数组么?
代码:

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

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

所以p+i就是指向数组下标为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;
}

2.2.2 数组指针

在数组中,数组名即为该数组的首地址,结合指针和整数的加减运算,我们可以实现指针访问数组元素。

int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。
//所以p是一个指针,指向一个数组,叫数组指针。
//注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

数组名与数组名取地址的区别:

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("arr=%p\n", arr);
	printf("&arr=%p\n", &arr);
	printf("arr+1=%p\n", arr + 1);
	printf("&arr+1=%p\n", &arr + 1);
	return 0;
}

运行结果:
运行结果
根据以上运行结果,我们可以发现&arr与arr的内容相同,但所代表的含义有所不同。
在实际上:&arr表示整个数组的地址而不是数组首元素的地址。
由上述代码运行结果可见,数组的地址+1跳过的是整个数组的大小,而数组首元素的地址加一跳过的是一个元素的大小。
练习:下述代码的意思是?

int arr[5];//定义一个大小为5的整型数组
int parr1[10];//定义一个(int)类型的指针数组
int (*parr2)[10];//定义一个指向整型数组的指针。
int (*parr3[10])[5];//定义一个数组指针数组。

2.3 数组参数与指针参数

2.3.1 一维数组传参

#include <stdio.h>
void test(int arr[])//正确
{}
void test(int arr[10])//正确
{}
void test(int *arr)//正确
{}
void test2(int *arr[20])//正确
{}
void test2(int **arr)//正确
{}
int main()
{
	int arr[10] = {0};
	int *arr2[20] = {0};
	test(arr);
	test2(arr2);
}

2.3.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);
}

2.3.3 一维指针传参

#include <stdio.h>
void print(int *p, int sz)//形参p为一级指针。
{
	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);//传入的p为数组的首元素的地址。
	return 0;
}

当函数的形参为一级指针时,函数的实参可以是:变量的地址和一级指针变量。

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

当函数的实参为二级指针时,函数的实参可以是:一级指针地址,二级指针变量和指针数组。

2.4 函数指针

·什么是函数指针?
在程序中定义一个函数,在编译时系统会为这个函数代码分配一段存储空间,而这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫“函数指针变量”,简称“函数指针”。
·函数指针的定义

(函数返回值类型) (*指针变量名)(函数参数列表)
“函数返回值类型”表示该指针变量可以指向具有什么返回值类型的函数;
“函数参数列表”表示该指针变量可以指向具有什么参数列表的函数。
参数列表只写参数类型,不用写参数名。
指针变量名俩端的括号不能省略。如果省去括号,意思会发生翻天覆地的变化。

·两段有趣的代码:
代码一:

((void ()())0)();
点击查看水木无痕大佬的详细讲解

代码二:

void (signal(int , void()(int)))(int);
点击查看钟子悦大佬的详细讲解

2.5 函数指针数组

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

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

那么函数指针数组也一样,他仍然是一个数组,把函数的地址存放在数组中,就是函数指针数组。
函数指针数组的定义:

函数的返回值类型 (*数组名[数组大小]])();

函数指针数组的用途(转移表):
例:利用函数指针实现计算器的编写。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<Windows.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);
		system("pause");
		system("cls");
	}
	return 0;
}

注意:函数指针数组中,函数的返回值类型,参数个数以及参数类型必须相同。

2.6 指向函数指针数组的指针

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

#include<stdio.h>
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;
}

2.7 回调函数

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

最著名的回调函数调用有C/C++标准库stdlib.h/cstdlib中的快速排序函数qsort和二分查找函数bsearch中都会要求的一个与strcmp类似的参数,用于设置数据的比较方法。接下来我将演示qsort函数的使用:
①调用库函数qsort进行排序:

#include <stdio.h>
#include<stdilib.h>
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2)
{
	return (*(int *)p1 - *(int *)p2);
}
int main()
{
	int arr[] = { 1,9,2,8,3,7,4,6,5 };
	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 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;
	}
}
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++)
		{
			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,9,2,8,3,7,4,6,5 };
	int i = 0;
	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;
}
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

RONIN_WZ

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

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

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

打赏作者

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

抵扣说明:

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

余额充值