【C语言进阶】指针进阶

news2025/1/11 11:06:04

今日所做之事勿候明天,自我所做之事勿候他人。             --歌德

 

目录

指针进阶(更深层次的理解):

一.字符指针

二.指针数组

​三.数组指针

1.数组指针的定义:

 2.&数组名和数组名:

3.数组指针的使用:

四.数组参数,指针参数

 1.一维数组传参:

2.二维数组传参:

3.一级指针传参:

4.二级指针传参:

五.函数指针:

六.函数指针数组:

七.指向函数指针数组的指针:

八.回调函数 


指针进阶(更深层次的理解):

前言:

之前我们已经学习了指针的初阶,接下来我们将进入指针的进阶了,更加深刻的理解指针。

先回忆一下指针的简单知识:

1.指针就是个变量,用来存放地址,地址唯一标识一块内存空间。

2.指针的大小是固定的4/8个字节(32位平台/64位平台)。

3.指针是有类型的,指针的类型决定了指针的±整数的步长,指针的解引用操作的时候的权限。

4.指针的运算。

一.字符指针

字符指针就是类型为char*。

int main()
{
	char ch = 'a';
	char* p = &ch;//p就是一个字符指针
	return 0;
}
int main()
{
	char* p1 = "abcdef";
	//这里字符串表达式的值就是首字符的地址
	//把字符串首元素的地址存放在p中
	//可以把它理解为字符串数组
	char arr[] = "abcedf";
	char* p2 = arr;
	//唯一的区别就是p2指向的是数组的首元素,而数组的可以修改的。
	//但是p1是不能修改的,因为p1对应的是常量字符串
	*p2 = 'h';
	printf("%s", arr);
    return 0;
}

当我们修改了常量字符串会发现什么呢?

int main()
{
	char* p1 = "abcdef";
	*p1 = 'h';
	return 0;
}

所以常量字符串是不能修改的。 

面试题:

int main()
{
	char str1[] = "hello world";
	char str2[] = "hello world";
	const char *str3= "hello world";
	const char* str4 = "hello world";
	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;
}

总结:常量的字符串是不能修改的。而数组里面的字符串是可以修改的。 

二.指针数组

指针数组本质上是数组,数组里的内容是指针。

字符指针数组:

int main()
{
    char* arr[] = { "abcdef","hello","nihao" };//这就是一个字符类型的指针数组
    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (i = 0; i < sz; i++)
    {
        printf("%s\n", arr[i]);
        //arr[i]就是字符串首元素的地址
        //通过首元素的地址就可以打印整个字符串
    }
    return 0;
}

整型指针数组:

int main()
{
    int arr1[] = { 1,2,3,4,5 };
    int arr2[] = { 6,7,8,9,10 };
    int arr3[] = { 11,12,13,14,15 };
    int* arr[] = { arr1,arr2,arr3 };//整型的指针数组
    int i = 0;
    int j = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (i = 0; i < sz; i++)
    {
        for (j = 0; j < 5; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

因为arr[i]=*(arr+i),所以这里打印我们还可以写成*(*(arr+i)+j)*(arr[i]+j)

三.数组指针

1.数组指针的定义:

数组指针不是数组,而是指针,是指向数组的指针,指针存放的是数组的地址。

int main()
{
    int arr[5] = { 1,2,3,4,5 };
    int(*pa)[5] = &arr;
    //这里的p就是数组指针,记得*和pa一定要括起来,不然就是指针数组了
    return 0;
}

 2.&数组名和数组名:

数组名其实是数组首元素的地址,这个是毋庸置疑的。

但是有两个例外:

1.sizeof(数组名),数组名如果单独放在sizeof内部,这里的数组名表示整个数组,计算的是整个数组的大小。

2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址

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

这里可以看出arr和&arr[0]和&arr的地址是一样的,分别加1的地址也是一样的,这就可以说明数组名是首元素的地址是完全没问题的。

不是说&arr是全部的地址吗?那为什么和arr的地址是一样的呢?但是当&arr+1的地址就完全不一样了。 

printf("%p\n", &arr);
	         //地址0133FB1C
printf("%p\n", &arr + 1);
	         //地址0133FB44

地址:01333FB1C和0133FB044总共差16进制的28,28转换为十进制是40,由此得出&arr+1跳过的是整个数组40个字节。

但&arr和arr的地址是一样的呢?因为它们都是从首地址开始的,虽然它们的值是一样的,但是他们所表达的含义是不同的。

3.数组指针的使用:

常规指针的用法打印数组内容:

int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = arr;
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", *(p + i));
    }
    return 0;
}

数组指针打印(用的很少,不提倡使用):

int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int(*p)[10] = &arr;
    //p--&arr
    //*p--*&arr
    //*p--arr
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", (*p)[i]);
    }
    return 0;
}

使用数组指针打印二维数组:

void print(int(*p)[5], int row, int col)
{
    int i = 0;
    int j = 0;
    for (i = 0; i < row; i++)
    {
        for (j = 0; j < col; j++)
        {
            //printf("%d ",p[i][j]);
            printf("%d ", *(p[i] + j));
        }
        printf("\n");
    }

}
int main()
{
    int arr[2][5] = { 1,2,3,4,5,6,7,8,9,10 };
    print(arr, 2, 5);
    //数组名arr,表示首元素的地址
    //但是二维数组的首元素是二维数组的第一行
    //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
    //可以用数组指针来接收
    return 0;
}

这里有一道题我们就可以来理解一下二维数组的数组名的含义:

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

 &aa的类型是int (*)[2][5],加一操作会导致跳转一个int [2][5]的长度,直接跑到刚好越界的位置。减一以后回到最后一个位置1处。*(aa + 1)相当于aa[1],也就是第二行的首地址,自然是5的位置。减一以后由于二维数组空间是连续性的,会回到上一行末尾的6处。

int arr[5];
int* arr[5];//指针数组
int(*arr)[5];//数组指针
int(*pa[10])[5];//这个怎么理解呢

pa是和[10]连在一起的,所以pa是一个数组,而数组里面是10个指针,而指针里面又是数组。

相当于数组里面是数组指针。 

理解:

四.数组参数,指针参数

 1.一维数组传参:

           //一维数组传参
void test(int arr[])//数组接收没问题
{}
void test(int arr[10])//数组接收没问题
{}
void test(int *arr)//指针接收没问题
{}
void test2(int *arr[20])//指针数组接收也没问题
{}
void test2(int **arr)//指针接收也没问题,类型和原类型一样,都是int*
{}
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])//使用数组指针可行
{}
void test(int **arr)//使用二级指针更不可行
{}
int main()
{
    int arr[3][5] = { 0 };
    test(arr);
    //这里的arr代表第一行的地址,只能用数组指针来接收
}

3.一级指针传参:

 //一级指针传参
void print(int* p, int sz)//使用指针接收,也可以使用数组接收
{
    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]);
    print(p, sz);
    return 0;
}

4.二级指针传参:

          //二级指针传参
void test(int** ptr)//二级指针接收
{
    printf("num=%d\n", **ptr);
    //第一次解引用找到p,第二次解引用找到num=10
}
int main()
{
    int num = 10;
    int* p = &num;
    int** pp = &p;
    //pp就是&p
    test(pp);
    test(&p);
    return 0;
}

五.函数指针:

函数指针就是指向函数的指针。函数有地址吗?

int Add(int x, int y)
{
    return x + y;
}
int main()
{
    printf("%p\n", &Add);
    printf("%p\n,Add");
    //&函数名就是函数的地址
    return 0;
}

所以&函数名和函数名是一样的,没有区别。 

int Add(int x, int y)
{
    return x + y;
}
int main()
{
    int (*pa)(int, int) = Add;//pa就是一个指向函数的函数指针
    //int(*)(int,int)就是pa的类型
    //参数和返回类型是和函数是一一对应的,这里的参数x,y可以不写
    int ret = (*pa)(3, 5);//这里的(*pa)只是为了方便理解,不写*也是可以的
    //int ret = Add(3, 5);
    //int ret = pa(3, 5);
    printf("%d\n", ret);
    return 0;
}

这里我们来理解两个比较复杂的表达式:

第一个:

int main()
{
	(*( (void (*)()) 0 ) )( )
	//我们如何来里理解这个代码呢
	//首先我们看最里面void(*)()这就是一个函数指针类型,无返回类型,无参数
	//然后(void(*)())0就是把0强制转换为void(*)()这个函数指针
	//然后(*( (void (*)()) 0 ) )这个就是函数的解引用操作
	//最后再加个括号,(*((void (*)()) 0))(),整个代码就是一次函数调用
	return 0;
}

第二个:

int main()
{
	void (*signal(int, void(*)(int)))(int); 
	//signal先和后面的括号结合,第一个参数是int,第二个参数是void(*)(int)类型的函数指针
	//该函数指针指向的函数参数是int,返回类型是void
	//signal(int, void(*)(int))这个的返回类型又是void(*)(int)
	//函数名也有,返回类型也有,参数也有,这个整体的代码就是一个函数的声明
	//就可以理解为 void(*)(int) signal(int, void(*)(int))
	//但是语法是不支持这样写的
	return 0;
}

如果你觉得第二个表达式比较复杂,你还可以用 rypedf (类型重命名)来简化代码:

typedef void(*pf_t)(int);  //就是将void(*)(int)重新起个名字为pf_t
//不能写成typedef void(*)(int) pf_t ,这样语法不支持
int main()
{
	void (*signal(int, void(*)(int)))(int);
	//这个代码就可以简化成下面的代码
	pf_t signal(int, pf_t);
	return 0;
}

六.函数指针数组:

函数指针数组本质是数组,只不过数组的元素是函数指针。

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(*pa)(int, int) = Add;
	int(*pb)(int, int) = Sub;
	//这里pa和pb都是函数指针,并且类型都是一样的,我们就可以把它们放在一个函数指针数组里面
	int(*ptr[4]) = { Add,Sub,Mul,Div };//这就是一个函数指针数组
	return 0;
}

在函数指针数组使用的时候和数组的使用反式大同小异,都是通过下标引用操作符。

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(*ptr[4])(int ,int) = {Add,Sub,Mul,Div};
	printf("请你输入要计算的两个值:>");
	int i = 0;
	int x = 0;
	int y = 0;
	scanf("%d %d", &x, &y);
	for (i = 0; i < 4; i++)
	{
		int ret = ptr[i](x, y);
		printf("%d\n",ret);
	}
	return 0;
}

通过上述的例子我们就可以实现一个可以计算加减乘除的计算器

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 Calc(int(*pa)(int, int))
{
	int x = 0;
	int y = 0;
	printf("请你输入要计算的值:");
	scanf("%d %d", &x, &y);
	int ret = pa(x, y);
	printf("计算的值为%d\n", ret);

}
int main()
{
	int(*ptr[5])(int,int) = {NULL,Add,Sub,Mul,Div};
	int input = 0;
	do
	{
		printf("请你输入要计算的方法\n");
		scanf("%d", &input);
		switch (input)
		{
		case 0:
			printf("退出计算\n");
			break;
		case 1:
			Calc(Add);
			break;
		case 2:
			Calc(Sub);
			break;
		case 3:
			Calc(Mul);
			break;
		case 4:
			Calc(Div);
			break;
		default:
			printf("输入错误,请重新输入\n");
			break;
		}
	} while (input);
	return 0;
}

七.指向函数指针数组的指针:

int Add(int x, int y)
{
	return 0;
}
int main()
{
	int(*ptr[5])(int, int) = { Add };//这个是函数指针数组
	int(*(*AAptr)[5])(int, int) = &ptr;
	//因为*和AAptr先结合,说明AAptr就是一个指针
	//然后指针又和数组结合,剩下的int(* )(int ,int)就是这个数组的元素的类型:函数指针
	//所以AAptr就是一个指向函数指针数组的指针变量
	return 0;
}

八.回调函数 

回调函数其实就是一个通过函数指针调用的函数!假如你把A函数的指针当作参数传给B函数,然后在B函数中通过A函数传进来的这个指针调用A函数,那么这就是回调机制。

我们来举个例子来理解一下这句话,就像之前的计算器就是一个回调机制。

int A(int x, int y)
{
	return x + y;
}
void B(int(*ptr)(int, int))
{
	printf("输入你要计算的两个值:");
	int x = 0;
	int y = 0;
	scanf("%d %d", &x, &y);
	int ret = ptr(x, y);//通过传过来的指针来访问A函数,这就是回调函数
	printf("%d\n", ret);
}
int main()
{
	int(*pa)(int, int) = A;
	B(A);//把A函数的地址传给B函数
	return 0;
}

感谢大家的支持,接下来的内容将会是qsort快排,敬请期待。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/379961.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

好代码的五个特质

CUPID出自Daniel的一篇名为《CUPID—for joyful coding》的博文&#xff0c;即《CUPID-为了快乐编程》。CUPID是Composable/Unix philosophy/Predictable/Idiomatic/Domain based几个单词的缩写&#xff0c;有经验的同学一看就知道这是好代码的一些属性。知道Cupid这个单词的同…

软件测试3

一 路径 路径&#xff1a;写想要找到的资源位置。 1.相对路径&#xff1a;相对于当前html文件去找其他资源的方式 &#xff08;1&#xff09;同级查找&#xff1a;当前html和目标在同一级别目录中&#xff0c;语法&#xff1a;直接写资源的名字即可 &#xff08;2&#xff09…

林心如常驻《向往的生活》,周杰却陷地域黑,做人的差别太大了吧

十年前如果有人提起周杰&#xff0c;就算是不能如雷贯耳&#xff0c;最起码也是妇孺皆知&#xff0c;毕竟那时候他太有名气了。因为拍摄《还珠格格》&#xff0c;让他和林心如等人一起爆红&#xff0c;不过此后的林心如&#xff0c;却很少再有优秀作品问世。 而周杰却不一样&am…

CCNP350-401学习笔记(易错题合集)

CCNP350-401学习笔记&#xff08;1-50题&#xff09;_殊彦_sy的博客-CSDN博客CCNP350-401学习笔记&#xff08;2023.2.17&#xff09;https://blog.csdn.net/shuyan1115/article/details/129088574?spm1001.2014.3001.5502CCNP350-401学习笔记&#xff08;51-100题&#xff09…

Win10系统开始菜单无法点击解决方法分享

Win10系统开始菜单无法点击解决方法分享。有用户电脑一开机之后&#xff0c;就出现了开始菜单无法正常点击的情况。我们很多设置项都是通过开始菜单来进行开启的。那么这个功能无法点击了怎么办呢&#xff1f;接下来我们一起来看看以下的解决方法分享吧。 方法一&#xff1a; 1…

面试官:给你一段有问题的SQL,如何优化?

大家好&#xff0c;我是飘渺&#xff01;我在面试的时候很喜欢问候选人这样一个问题&#xff1a;“你在项目中遇到过慢查询问题吗&#xff1f;你是怎么做SQL优化的&#xff1f;”很多时候&#xff0c;候选人会直接跟我说他们在编写SQL时会遵循的一些常用技巧&#xff0c;比如&a…

【力扣(LeetCode)】【MySQL】【185.部门工资前三高的所有员工】

学习时间&#xff1a; 2023年3月1日 题目描述&#xff1a; 题解分享&#xff1a; # 作 者 : 繁 华 倾 夏# 力扣(LeetCode):185. 部门工资前三高的所有员工# MySQL 数据库selectd.Name as Department, e1.Name as Employee, e1.Salary fromEmployee e1joinDepartment d on …

数据结构初阶 -- 顺序表

数据结构初阶 链表的讲解 目录 一. 线性表 1.1 定义 1.2 特点 二. 顺序表 2.1 定义 2.2 代码 2.3 功能需求 2.4 静态顺序表的特点以及缺点 2.5 动态的顺序表 2.6 动态顺序表接口的实现 三. 代码 头文件 主文件 一. 线性表 1.1 定义 线性表&#xff08;linear li…

【算法】Tire字符串

作者&#xff1a;指针不指南吗 专栏&#xff1a;算法篇 &#x1f43e;或许会很慢&#xff0c;但是不可以停下&#x1f43e; 文章目录1.Trie的基本思想1.1什么是Trie1.2字符串条件1.3如何存储字符串1.4如何查找字符串2.Trie的代码实现2.1怎么用数组建树2.2完整代码1.Trie的基本思…

柔性电路板的优点、分类和发展方向

柔性电路板是pcb电路板的一种&#xff0c;又称为软板、柔性印刷电路板&#xff0c;主要是由柔性基材制作而成的一种具有高可靠性、高可挠性的印刷电路板&#xff0c;具有厚度薄、可弯曲、配线密度高、重量轻、灵活度高等特点&#xff0c;主要用在手机、电脑、数码相机、家用电器…

CSGO社区服搭建服务器架设游戏服务端教程

CSGO社区服搭建服务器架设游戏服务端教程 我是艾西&#xff0c;上一篇说了搭建CSGO服务器需要准备服务器以及安装好所需要的环境&#xff0c;那么今天说一下CSGO社区私人服务器怎么搭建游戏服务端 搭建CSGO服务器比较简单&#xff0c;Valve开发者社区wiki也给出了安装指导&…

仓库管理如何实现扫码出入库?

仓库实现扫描出入库管理具体应该怎么做&#xff1f;以下方参考模板为例&#xff0c;可以点击打开配合阅读&#xff1a; 参考模板&#xff1a;通用仓库管理&#xff08;官方&#xff09;① 库存卡设计&#xff1a; 此表单主要用于代替传统纸质库存卡的记录功能&#xff0c;货物…

【备战面试】每日10道面试题打卡-Day1

本篇总结的是Java基础知识相关的面试题&#xff0c;后续也会更新其他相关内容 文章目录1、JVM、JRE和JDK的关系&#xff1f;2、Java语言有哪些特点&#xff1f;3、Java和C的区别有哪些&#xff1f;4、Java有哪些数据类型&#xff1f;5、访问修饰符 public、private、protected&…

企业如何实现精细化人员管理?五大业务场景值得关注

近年来&#xff0c;随着大数据、人工智能和云计算等信息技术不断升级与渗透&#xff0c;处在数字化变革的劳动力密集型企业希望利用更加智能化的劳动力管理软件&#xff0c;帮助企业实现规范化的管理。 面对企业劳动力管理理念的变化&#xff0c;以及数字化转型的发展渗透&…

Puppeteer项目结构梳理

最近接触了一个个人感觉很奈斯的项目&#xff0c;故记录思路如下&#xff1a; puppeteer项目梳理&#xff1a; 入口文件 run.js 入口命令 node run.js YourConfig.json 1、我们可以在自己的config.json里面设置好 ①、登录的用户名密码;aws或其它服务器的access等id,accessKey…

二叉树的性质(概念/特性/存储结构)

目录1 二叉树的定义及主要特性1.1 二叉树的定义1.2 特殊二叉树1.2.1 满二叉树1.2.2 完全二叉树1.2.3 二叉排序树1.2.4 平衡二叉树1.3 二叉树的性质1.3.1 非空二叉树上的叶结点数1.3.2 非空二叉树第k层结点数1.3.3 高度为h的二叉树至多结点数1.3.4 完全二叉树结点与双亲的关系1.…

电子价格标签-系统结构

一、V2.4基站软件电子标签 接收PC下达的操作指令&#xff0c;解析后再通过RF发送给电子标签&#xff0c;接收路由器发送的数据信息并解析&#xff0c;更新数据。 1. 2.1寸电子价签 2. 2.9寸电子价签 ​ 3. 4.2寸电子价签 ​ 4. 7.5寸电子价签 ​ 5. 10.2寸电子价签 二、V4…

当审稿人回复这些审稿意见时,其实是在暗示你这些细节需要注意

当我们翘首以盼SCI论文投稿的审稿决定时。心情往往是复杂的&#xff0c;想收到审稿决定又担心收到的是不好的审稿决定。不管审稿结论如何&#xff0c;我们首先要明白的是那几个简短字母的审稿决定对我们来说&#xff0c;意味着什么。只有这样才有可能既不妄自菲薄也不杞人忧天。…

Intel CSME 简述

SME 算是 Intel X86 PC 上最神秘的部分了,本文根据 us-19-Hasarfaty-Behind-The-Scenes-Of-Intel-Security-And-Manageability-Engine 一文写成。讲述内容无法证伪,各位随便听听即可,了解这些能够帮助BIOS 工程师更好的理解一些操作的实现。文章基于 Intel 第八代第九代CPU(…

springCloud之seata

一、Seata是什么 Seata 是一款开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式解决方案。 官方文档 https://seata.io/zh-cn/docs/overview/wha…