深入理解C语言指针

目录

一、什么是指针

1.内存和地址

2.指针变量和地址

4.const修饰指针

5.指针的运算

6.野指针及其避免方法 

二、指针与数组

1.指针与数组名的深入理解

2.指针数组

3.指针数组模拟二维数组

三、指针变量

1.字符指针变量

2.数组指针变量

3.函数指针和变量和回调函数

4.函数指针数组和转移表

四、指针深入应用举例(qsort)

2.qsort及使用举例

3.qsort模拟实现 


 

一、什么是指针

1.内存和地址

想理解内存和地址,我们可以举一个简单的例子:一栋居民楼里,每个房间有自己的“编号”,如301、302、303......这样如果有朋友来拜访你的时候,就可以通过这个编号来快速找到你所在的房间,如果没有这个“编号”的话,朋友来拜访你的时候,就只能挨个房间来寻找,效率极低,十分繁琐。

对应到计算机中,内存就像是居民楼,计算机把内存划分成一个个的内存单元,就像是一个个房间,每个内存单元的大小是1个字节,一个字节空间里有8个比特位,一个比特位可以存储一个2进制的1或0。每个内存单元也有一个编号,就像居民楼的房间号一样,有了了这个内存单元的编
号,CPU就可以快速找到一个内存空间,这个内存编号也称为地址,C语言中给地址起了一个新的名字:指针。

所以我们可以理解成:内存单元的编号——地址——指针

2.指针变量和地址

我们通过取地址操作符(&)拿到的是一个数值,比如:0x006FFD70,这个数值有时候也是需要
存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?答案是:指针变量中。

#include <stdio.h>
int main()
{
	int a = 10;
	int* p = &a;
	return 0;
}

指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。 

p的左边写的是int** 是在说明p是指针变量,而前面的int是在说明p指向的是整形类型的对象,如果是存放char类型变量的地址,那么指针变量应该使用char*类型。

当我们创建了指针变量并赋予其地址了以后,想要使用指针变量对指向的对象操作,就要用到解引用操作符*

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

上述代码中,*p的意思是通过p中存放的地址,找到指向的空间,*p其实就是a变量了,所以*p=0就等于把变量a改成了0 。

指针变量也是有大小的,32位平台下地址是32个bit位,指针变量大小是4个字节;64位平台下地址是64个bit位,指针变量大小是8个字节。(注意:指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。)

有一种特殊的指针类型:void*,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址,但是也有局限性,void* 类型的指针不能直接进行指针的+-整数和解引用的运算。

4.const修饰指针

变量是可以被修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量。但是,如果我们希望一个变量加上一些限制,不能被修改,怎么做呢?这就要引出我们要介绍的const了。我们通过下面一段代码来理解const修饰指针的作用。

#include <stdio.h>
void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 20;//OK
	p = &m;//OK
}
void test2()
{
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;//错误
	p = &m;//OK
}
void test3()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = 20;//OK
	p = &m;//错误
}
void test4()
{
	int n = 10;
	int m = 20;
	int const* const p = &n;
	*p = 20;//错误
	p = &m;//错误
}
int main()
{
	test1();
	test2();
	test3();
	test4();
	return 0;
}

总结:在const修饰指针变量的时候:

1.const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。
但是指针变量本身的内容可变。

2.const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指
向的内容,可以通过指针改变。

5.指针的运算

指针的运算基本有以下三种:

• 指针+-整数

• 指针-指针

• 指针的关系运算

首先让我们来看一下指针+-整数,因为数组在内存中是连续存放的,借助指针,只要知道第一个元素的地址,就能通过指针+-整数顺藤摸瓜找到后面的所有元素,以下面的代码为例,理解体会指针+-整数的效果:

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));//p+i 这里就是指针+整数
	}
	return 0;
}

运行结果: 

指针-指针(运算的前提是两个指针指向了同一块空间),即两个指针的地址相减,其绝对值得到的是两个指针之间的元素个数:

#include <stdio.h>
int my_strlen(char* s)
{
	char* p = s;
	while (*p != '\0')
		p++;
	return p - s;
}
int main()
{
	printf("%d\n", my_strlen("abc"));
	return 0;
}

上述代码的运行结果是:3 

即模拟实现了strlen函数的效果,统计了字符串的长度

指针的关系运算:

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	while (p < arr + sz) //指针的大小比较
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

运行结果: 

6.野指针及其避免方法 

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

在使用指针过程中,应避免野指针

野指针的几种典型成因:

1.指针未初始化

int main()
{
	int* p;//局部变量指针未初始化,默认为随机值
	*p = 20;
	return 0;
}

2.指针越界访问

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

3.指针指向的空间释放 

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

如何避免野指针:

1.初始化:如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL

2.避免指针越界

3.指针变量不再使用时,及时置NULL,指针使用之前检查有效性

4.运用assert断言,让程序运行时自动验证指针是否为野指针

二、指针与数组

1.指针与数组名的深入理解

我们在上一个部分使用指针访问数组的内容时,有这样的代码:

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

这里我们使用&arr[0]的方式拿到了数组第一个元素的地址,但是其实数组名本来就是地址,而且是数组首元素的地址,当我们使用arr代替&arr[0]的时候,在我们用printf打印的时候会发现值是一样的,所以大家要记住:数组名就是首元素的地址!但是除了以下几种情况:

sizeof(数组名)sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,
单位是字节 

&数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素
的地址是有区别的,例如(以上面提到的代码为例):&arr[0]+1会跳过一个字节,arrarr+1也是相差四个字节,是因为&arr[0]arr都是首元素的地址,+1就是跳过一个元素。但是,&arr&arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。)

2.指针数组

听到指针数组这个名字,很多人可能会疑惑,指针数组到底是指针还是数组?其实我们可以通过类比的方法理解它:整形数组是存放整形的数组;字符数组是存放字符的数组,那么同理,指针数组就是存放指针的数组。

指针数组的每个元素都是地址,又可以指向一块区域

3.指针数组模拟二维数组

我们看一段代码:

#include <stdio.h>
int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };
	//数组名是数组首元素的地址,类型是int*的,就可以存放在parr数组中
	int* parr[3] = { arr1, arr2, arr3 };
	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ", parr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型一维数组,parr[i][j]就是整型一维数组中的元素。上述的代码模拟出二维数组的效果,实际上并非完全是二维数组,因为每一行并非是连续的。

三、指针变量

1.字符指针变量

字符指针变量使用一般如下:

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

通过下面的这种使用方式,我们加深理解一下字符指针变量的本质:

#include <stdio.h>
int main()
{
	const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
	printf("%s\n", pstr);
	return 0;
}

 上述代码特别容易让同学以为是把字符串hello bit放到字符指针pstr 里了,但是实际本质上是把字符串hello bit首字符地址放到了pstr中。

2.数组指针变量

之前我们学习了指针数组,指针数组是一维数组,数组中存放的是地址(指针)。那么数组指针是指针变量还是数组?答案是:指针变量。其存放的是数组的地址,能够指向数组的指针变量。

int* p1[10];
int(*p2)[10];

我们要明确分辨指针数组和数组指针,上述代码中第一条代码是前面讲到的数组指针,而第二条代码才是数组指针。

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

数组指针变量的初始化:

int arr[10] = { 0 };
int(*p2)[10] = &arr;

如果通过调试其实可以发现,&arrp2的类型是完全一致的。

3.函数指针和变量和回调函数

什么是函数指针变量呢?根据前面我们学习整形指针、数组指针的经验,通过类比我们可以得出结论:函数指针变量是用来存放函数地址的,未来通过地址能够调用函数。正如数组一样,函数也是有地址的,函数名就是函数的地址,当然也可以通过&函数名的方式获得函数的地址。

如果我们要将函数的地址存起来,那么就要创建函数指针变量,函数指针变量的写法其实和数组指针非常类似。如下:

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int(*pf)(int, int) = Add;
	//此处即为函数指针的使用
	//int指的是pf指向函数的返回类型
	//pf是函数指针变量名
	//(int x,int y)指的是pf指向函数的参数类型和个数的交代,此处x和y可写可不写
	printf("%d\n", (*pf)(2, 3));
	printf("%d\n", pf(3, 5));
	return 0;
}

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

函数指针的应用其实就是一个使用回调函数的很好的实例,我们看一下这段代码:

#define _CRT_SECURE_NO_WARNINGS 1
#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;
}
void calc(int(*pf)(int, int))
{
	int ret = 0;
	int x = 0, y = 0;
	printf("请输入操作数");
	scanf("%d""%d", &x, &y);
	ret = pf(x, y);
	printf("ret = %d\n", ret);
}
int main()
{
	int input = 1;
	do
	{
		printf("*************************\n");
		printf(" 1:add             2:sub \n");
		printf(" 3:mul             4:div \n");
		printf("        0:exit \n");
		printf("*************************\n");
		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");
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

上述代码中通过函数指针void calc(int(*pf)(int, int))接收调用函数的地址,然后使用时其指向什么函数就调用什么函数,实现了回调函数的功能 

4.函数指针数组和转移表

数组是一个存放相同类型数据的存储空间,我们已经学习了指针数组,比如:int *arr[10]; 那么,要把函数的地址放到一个数组中,这个数组就叫函数指针数组,其定义如:int (*parr1[3])();其中parr1先和[]结合,说明parr1是数组,然后其内容是int (*)()类型的函数指针。

上面我们了解了函数指针数组,那么大家可能好奇其用途是什么呢,下面为大家举一个常见的用途,即转移表:

#define _CRT_SECURE_NO_WARNINGS 1
#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 };//转移表(此处要注意数组是从0开始的)
	do
	{
		printf("*************************\n");
		printf(" 1:add             2:sub \n");
		printf(" 3:mul             4:div \n");
		printf("        0:exit \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		if ((input <= 4 && input >= 1))
		{
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = (*p[input])(x, y);
			printf("ret = %d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出计算器\n");
		}
		else
		{
			printf("输入有误\n");
		}
	} while (input);
		return 0;
}

上面的代码通过函数指针数组大大简化了代码!

四、指针深入应用举例(qsort)

2.qsort及使用举例

qsort是一种用来排序的库函数,底层是使用的快速排序的方式,qsort的强大之处就是在于可以用来排序任意类型的数据。其原型为:

void qsort(void* base, size_t num, size_t size, int (*compare)(const void*, const void*));
//base:指针,指向待排序的数组的第一个元素;
//num:base指向的待排序数组的元素个数;
//size:base指向的待排序数组元素的大小;
//int (*compare)(const void*, const void*):函数指针,指向的是两个函数的比较函数

下面来看一下qsort的使用举例:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct stu
{
	char name[20];
	int age;
};//不管有没有定义结构体变量,分号都不能落下
int cmp_int(const void* p1, const void* p2)
//void*类型的指针是无具体类型的指针,这种类型的指针不能直接解引用,也不能进行+—整数的运算
{
	return *(int*)p1 - *(int*)p2;//如果要从大到小的话就改成p2-p1
}
void Print(int arr[], int sz)
{
	int i = 0;
	for (; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
int cmp_stu_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct stu*)p1)->name, ((struct stu*)p2)->name);
	//strcmp是按照对应字符串中ASCII码值比较的
	//-> 结构体成员的间接访问操作
}
int cmp_stu_by_age(const void* p1, const void* p2)
{
	return ((struct stu*)p1)->age - ((struct stu*)p2)->age;
}
void test2()
{
	struct stu arr[3] = { {"Joatro",30},{"Joseph",76},{"Jolyne",17} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
	for (int i = 0; i < sz; i++)
	{
		printf("%s ", arr[i].name);
	}
	printf("\n");
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i].age);
	}
}
void test1()
{
	int arr[] = { 3,8,5,2,6,7,9,4,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	Print(arr, sz);
	printf("\n");
}
int main()
{
	test1();
	test2();
	return 0;
}

上述代码运用了qsort对不同类型的数据进行排序。 

3.qsort模拟实现 

知道了qsort的定义及用法以后,我们来尝试实现一下qsort函数(冒泡方式):

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void Swap(char* p1, char* p2,int width)
{
	int i = 0;
	for (; i < width; i++)
	{
		char tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
		p1++;
		p2++;
	}
}
void bubble_qsort(void* base, size_t sz, size_t width, int(*cmp)(const void* p1, const void* p2))
{
	int i = 0;
	for (; i < sz - 1; i++)
	{
		int j = 0;
		for (; j < sz - 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 cmp_int(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
	//void*这种类型的指针不能直接解引用和运算,要先强制转化成int*类型
}
void Print(int* p, int sz)
{
	int i = 0;
	for (; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
}
int main()
{
	int arr[] = { 9,8,6,5,7,3,4,1,2,0 };
	size_t sz = sizeof(arr) / sizeof(arr[0]);
	bubble_qsort(arr, sz, sizeof(arr[0]),cmp_int);
	Print(arr, sz);
}

上面的代码相对有些复杂,大家需要对比qsort的定义多思考以便理解。

本文对C语言指针的讲解就到这里,如有不足还望多多指出,谢谢! 

  • 10
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值