c语言指针的总结(从入门基础到进阶)

首先明确一个概念,令人闻风丧胆的指针就是一个内存的地址

概念

这个时候就可以用到vs的调试的查看内存的功能

这个时候的&a就可以查看a的起始地址,而&就是取地址操作符

如果我们要查看一个数的地址的时候我们可以这样打印

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

我们也可以用一个变量来存储地址

*就是解引用操作符,可以拿到这个地址里的数值

理解

接下来我们来深入了解指针

我们来思考一下这个会输出什么

答案是输出的都是8

因为 无论什么类型的地址都是64为0/1组成的二进制序列,所以长度都是一样的(如果是x89环境的话就是32为0/1组成的二进制序列)(因为sizeof得出来的是字节,8个bite位等于一个字节)

下面验证一下我所说的

 以下是指针的入门操作

下面是一个错误示范,char*可以放跟int*一样的地址,但是只可以改变一个字节

 总结一下

 我们再来编写一段代码加深理解指针加减整数的问题

 对于不同的指针类型,与整数相加减的时候跳过的字节也是不一样的,这样才能确保访问下一个地址的时候能够正确访问,加一减一的时候跳过的是字节类型的指向值

//

操作

那么指针类型的这些特点,我们要怎么使用呢?

我们也从最简单的数组入手

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

///

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

///

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

注意点 关于数组理解

这样应用的就是加1跳过四个字节,三种写法都是可以的,加深我们对理解(我们不难发现a+i其实就是&a[i]),然后我们可以知道&a[0]跟a 这两种表达都是指向数组首元素的地址

const修饰

然后我们学一下const是如何修饰指针的,const首先是修饰变量的,使得这个变量不得修改

但是先别高兴的太早

#include <stdio.h>
int main()
{
	const int n = 100;
	int* p = &n;
	*p = 20;
	printf("%d\n", n);
	return 0;
}

我们给它用指针一顿操作,诶,这下改变了,这种事情是不合理的,它违背了我们的初衷,那我们也要进行相应的修改,我们来用const修饰指针

#include <stdio.h>
int main()
{
	const int n = 100;
	const int* const p = &n;
	*p = 20;
	p = &m;
	printf("%d\n", n);
	return 0;
}

注意const放在*的前面是限制指针指向的内容,意思是不能通过指针修改指针指向的内容,但是可以修改指针的指向

const放在*的右边限制的是指针变量本身,意思是不能修改指针变量的指向,但是可以修改指针变量指向的内容

/

下面来讲指针的基本运算

 还是指针加减整数,这次我们来看一下字符串,也是一个简单的例子

#include <stdio.h>
int main()
{
	char arr[] = "abcdef";
	char* pc = &arr[0];
	while (*pc != '\0')
	{
		printf("%c", *pc);
		pc++;
	}
	return 0;
}

接下来讲一下指针减去指针,前提是两个指针指向同一个空间,否则会没有意义,结果是什么全部取决与编译器

这样我们会得到指针与指针之间元素的个数 (绝对值,避免小地址减去大地址)

然后我们来巩固一下指针的用法

我们来用指针自己模拟一个strlen函数 

函数传过去的就是数组首元素的地址

 为了呼应上文的指针减去指针,这里再提供一种写法

再来一个更安全的写法,自己体会一下

#include <stdio.h>
#include <string.h>
int mstrlen(const char*arr)
{
	char* start = arr;
	while (*arr != '\0')
	{
		arr++;
	}
	return arr- start;
}
int main()
{
	char arr[] = "abcdef";
	int len = mstrlen(arr);
	printf("%d\n", len);
	return 0;
}

// 

 然后是指针的关系运算

我们利用这个关系运算来打印一下数组

野指针

 我们初始化指针的时候一定不能空着,否则指针就会变成野指针,很危险!要么我们就给它设置为空指针

要是数组的指针越界也会变成野指针

还有就是函数返回的时候,也要特别注意,举一个例子

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

看起来好像没问题,但是从函数出来的时候n已经被销毁了,所以*p就变成野指针了,也就是这个指针没有意义了,失去有效性了,也就是指针指向的空间释放,避免返回局部变量的地址

那既然野指针那么危险,我们也要利用一些手段来注意预防,最大限度避免野指针的出现,造成程序的不稳定性

assert断⾔

⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。

这里就会报错 

虽然if语句也可以做到这种效果,但是assert在报错的时候我们可以知道是哪一行错了

 

 在我们不想使用assert的时候,我们在前面加上一个定义就可以关闭assert,也是很便捷的

指针的使⽤和传址调⽤

 这个传址调用跟传值调用是不一样的

来用一个简单的交换函数来测试一下

这个是传值调用

打印出来的结果并不符合我们的交换想法

 然后我们来利用一下传址调用

我们就达到目的了,是不是很疑惑

这种现象的原因是 传值调用的时候,实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。所以Swap是失败的了。那么传址的时候调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接 将a和b的值交换了。那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap函数⾥边通过地址间接的操作main函数中的a和b就好了。

总结一下,当要改变主函数里的值的时候就用传址,要不然就传值就可以了

系统理解数组与指针

 我们要知道&arr 代表整个数组,在下面的代码中可以自己试一下,+1就可以看出arr &arr[0]和&arr的区别

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

再来一个代码加深理解,可以复制到本地ide试试

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int * p = arr;
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		scanf("%d", p);
		p++;
	}
	p = &arr[0];
	for (i=0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}
#include <stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int * p = arr;
	int i = 0;
	/*for (i = 0; i < sz; i++)
	{
		scanf("%d", p);
		p++;
	}
	p = &arr[0];*/
	for (i=0; i < sz; i++)
	{
		printf("%d ", p[i]);//首元素地址
	}
	return 0;
}

 看起来很神奇的写法背后底层逻辑其实是一样的,我们要清楚,但是平时还是要挑最清晰的写法来用 

还再强调一下arr[i]==*(arr+i),还是要多多理解这些基础

一维数组传参的本质

我们用函数来打印一下一维数组

但是结果貌似错了

这是因为我们传过去的是数组首元素的地址,所以sz是错的 ,我们在主函数算出sz再传进去我们就可以正确打印了

我们用指针实现一下冒泡排序


#include <stdio.h>
void pao_sort(int* arr,int sz)
{
	for (int i = 0; i < sz - 1; i++)//交换趟数
	{
		for (int j = 0; j < sz - 1 - i; j++)//交换
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}
int main()
{
	int arr[] = { 15,2,3,4,84,6,7,66,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	pao_sort(arr,sz);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", i[arr]);
	}
	return 0;
}

二级指针

我们理解一下,二级指针存放一级指针,三级指针存放二级指针,所以这些并没有什么特别的

贴上代码,可以根据这个以此类推

#include <stdio.h>
int main()
{
	int a = 10;
	int *p = &a;
	int* * pp = &p;//pp旁边的星表示pp是一个指针,再旁边的int*表示的是p的类型
    printf("%d",**pp);二级指针要解引用两次
	int** * ppp = &pp;//三级指针,促进理解
	return 0;
}

强调一下,pp旁边的星表示pp是一个指针,再旁边的int*表示的是p的类型 ,这对后面的数组指针理解有帮助

指针数组

 存放指针的数组

来一个简单的代码实现

#include <stdio.h>
int main()
{
	int a = 1;
	int b = 2;
	int c = 3;
	int d = 4;
	int* par[4] = { &a,&b,&c,&d };
	for (int i = 0; i < 4; i++)
	{
		printf("%d ", *(i[par]));//也可以**(par+i)
	}
	return 0;
}

我们用新学的知识来模拟一下二维数组

#include <stdio.h>
int main()
{
	int a1[] = { 1,2,3 };
	int a2[] = { 4,5,6 };
	int a3[] = { 7,8,9 };
	int* a[3] = { a1,a2,a3 };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			printf("%d ", *(*(a + i) + j));//也可以看成二维数组a[i][j]  都是两个**的意思
		}
		printf("\n");
	}
	return 0;
}

字符指针变量

这个代码加深引出常量字符串,加深我们对指针的理解

#include <stdio.h>
int main()
{
	//char*p= "abcdef";//不是将全部存进p中,而是将首字符a的地址存进p中
	const char* p = "abcdef";
	printf("%c\n", *p);//
	//"abcdef"这是一个常量字符串,存放在常量区,不可修改,根据前面知识,我们加上const确保安全
	*p='w';//这样是不行的,因为常量区不能修改,加上const就不能运行了,更加安全
	printf("%s\n",p);//这样是可以的,因为只是打印,不是修改
	return 0;
}

在理解一下

#include <stdio.h>
int main()
{
	const char* a1 = "hello";//只保存一份
	const char* a2 = "hello";
	if (a1 == a2)
	{
		printf("same");
	}
	return 0;
}

数组指针变量

 

 数组指针变量是⽤来存放数组地址的,那获得数组的地址就是我们之前学习的 &数组名

下面给出代码例子加强理解

#include <stdio.h>
int main()
{
	int arr[6] = { 1,2,3,4,5,6 };
	int (*ptr)[6] = &arr;//ptr是数组指针  注意arr跟&arr的区别

	char* ch[8];
	char* (*str)[8] = &ch;

	return 0;
}

我们用这个来打印出数组里的元素来加深理解

#include <stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int (*p)[10] = &arr;
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", (*p)[i]);//因为p=&arr,加一个*来抵消	
	}
	return 0;
}

在数组指针的基础上我们来拓展一下

⼆维数组传参的本质

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

把(*arr)[3]跟直接传arr是一个意思,我们可以把二维数组看成看成是多个一维数组的合并,至此我们就和上一个知识点穿起来了

总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式

函数指针变量 

函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的

这个的写法跟数组指针很类似

#include <stdio.h>
int add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pf)(int, int) = &add;//第一个int是返回类型 ()里面的代表函数的参数
	int a = 1;
	int b = 2;
	printf("%d\n",(*pf)(a, b));//这里不加*也可以,但是写了更容易理解,可读性更好
	return 0;
}

 分享一段很有意思的代码

#include <stdio.h>
int main()
{
	(*(void (*)())0)();
	return 0;
}

这个代码很考验我们对函数指针的理解,这段代码的意思是调用0地址处的函数,函数没有参数,返回类型是void,要我们一层层抽丝剥茧的去分析

typedef关键字

typedef 是⽤来类型重命名的,可以将复杂的类型,简单化

例如

typedef 是⽤来类型重命名的,可以将复杂的类型,简单化

 如果是指针类型也是可以的

typedef int* pt;

⽐如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那要把名字放到括号里

 typedef int(*parr_t)[5]; 

 函数指针类型的重命名也是⼀样的

typedef void(*pfun)(int);//新的类型名必须在*的右边

 函数指针数组

 那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,也就是存放函数指针的数组,先给上模板

#include <stdio.h>
int main()
{
	int (*def_zu[2])(int, int) = { hanshu1,hanshu2 };
	return 0;
}

这个有什么作用呢,我们来实现一个简单计算器来感受一下

#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 }; //转移表
	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);//合理利用0
	return 0;
}

这样写的话整体就简洁了很多,节省了很多代码量,可读性也好,要是用switch语句的话就看起来有一些冗余了,要是后来功能更多的话switch语句就越来越长,就看起来非常的不舒服

回调函数

回调函数就是⼀个通过函数指针调⽤的函数

当这个指针被⽤来调⽤其所指向的函数 时,被调⽤的函数就是回调函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条 件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应

 这个需要我们熟练使用函数指针传参

写一个简单的例子

#include <stdio.h>
int add(int a, int b)
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
void use_def(int (*p)(int,int))
{
	int x, y;
	scanf("%d%d", &x, &y);
	int ret = p(x, y);
	printf("%d\n", ret);
}
int main()
{
	int n;
	scanf("%d", &n);
	switch (n)
	{
	case 1:
		use_def(add);
		break;
	case 2:
		use_def(sub);
		break;
	}
	return 0;
}

为了巩固指针运算,同时加强排序知识,我们来下一个部分

qsort函数的模拟实现

因为qsort大家还没那么清楚,所以我们用冒泡排序的内核来模拟 

这个模拟我们要能排序多种类型的变量的数组

#include <stdio.h>
int arr[100100] = { 0 };
int my_compar(const void* a, const void* b)
{
	return *(int*)a - *(int*)b;
}
void my_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 my_sort(void * base,int n,int size,int (*my_compar)(void*,void*))//冒泡排序逻辑
{
	int i = 0;
	int j = 0;
	for (i = 0; i < n - 1; i++)
	{
		for (j = 0; j < n - i - 1; j++)
		{
			if (my_compar((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
			{
				my_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}
}
int main()
{
	int n;
	scanf("%d", &n);
	for (int i = 0; i < n; i++)
	{
		scanf("%d", &arr[i]);
	}
	my_sort(arr,n,sizeof(arr[0]),my_compar);//把多少个字节传过去
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);//打印出有序数组
	}
	return 0;
}

笔试题解析

我们学会指针就得有解决问题的能力

下面我来解析几道面试题

第一道

这是最基础的指针

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

第二道

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct Test
{
	int Num;
	char* pcName;
	short sDate;
	char cha[2];
	 short sBa[4];
}*p = (struct Test*)0x100000;

int main()
{
	printf("%p\n", p + 0x1);
	printf("%p\n", (unsigned long)p + 0x1);
	printf("%p\n", (unsigned int*)p + 0x1);
	return 0;
}

这道题的答案是

就是要明白强制类型转换或者地址加一是什么意思

第三道

int main()
{
	int a[3][2] = { (0, 1), (2, 3), (4, 5) };
	int* p;
	p = a[0];
	printf("%d", p[0]);
	return 0;
}

下面是解析 

意思就是只有1 3 5放进去了

然后答案就显而易见是1了

第四道 

int main()
{
	int a[5][5];
	int(*p)[4];
	p = a;
	printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
	return 0;
}

我们把二维数组跟p的关系画一个图

然后自己相减就可以了

注意%p的强制类型转换

 第五道

int main()
{
	int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int* ptr1 = (int*)(&aa + 1);
	int* ptr2 = (int*)(*(aa + 1));
	printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
	return 0;
}

跟前面差不多的,这里会输出10 5,画一个图就很好理解了,整形指针减去1也就是整形数组往后退一个

重点其实还是前面的基础知识,基础牢固才能熟练运用

  • 35
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值