【C语言航路】第十站:指针进阶(一)

news2024/12/27 13:06:17

目录

一、字符指针

二、指针数组

三、数组指针

1.数组指针的定义

2.数组名和&数组名

 3.数组指针的使用

四、数组参数、指针参数

1.一维数组传参

2.二维数组传参

3.一级指针传参

4.二级指针传参

五、函数指针

总结


一、字符指针

我们知道指针有一种类型叫做字符指针char*,他一般是下面这种使用的

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

当然这是一种最基本的使用方法,其实还有一种使用方法是这样的,如下图所示,看上去好像是将一个字符串直接赋值给p这个指针了?其实这段代码不是这个意思,这个代码的意思是将abcdef这个字符串的首元素地址赋值给p,让p指向这个字符串,而且是将字符串放入p这个指针也放不下去啊,因为p这个指针在32位环境下只有4个字节。而这个字符串已经八个字符了,需要八个字节,所以其实是将这个字符串的的地址赋值给p的。

 注意,字符串常量是放在只读常量区的,是不能被修改的。我们之前所说的内存分为栈区,堆区,静态区只是最常用的几个分区,他还有很多更细的分区,如字符串常量就放在只读常量区。既然只读,那么我们也就便明白了,这个字符串是不可以被修改的。所以我们严格来说要加上一个const修饰,如果不加的话,在vs2022上会报警告,甚至在未来我们不小心修改的话,则直接程序崩溃

 而我们加上const以后,程序直接报错误,直接编译不过去。有利于我们排查错误

而我们需要打印这个字符串的话,也就直接将字符串的地址放上去就可以打印了

#include<stdio.h>
int main()
{
	const char* p = "abcdef";
	printf("%s", p);
	return 0;
}

我们来看下面一个例子

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

对于这段代码,他最终输出的结果是,一和二不一样,三和四一样。那么这是为什么呢?其实这是因为hello world这个字符串,在一和二中是先开辟了这两个数组空间,这两个数组空间的地址是不一样的,然后才将这个字符串放到里面去,我们使用的是==,也就是比较一和二的地址,两个数组空间的地址当然是不一样的了,而三和四是直接将只读数据区的字符串常量的首元素地址直接赋值给这个指针,那么当然是一样的了。

 

二、指针数组

我们知道整型数组是一个数组,用来存放整型,字符数组是一个数组,用来存放字符,那么同理,指针数组也是一个数组,用来存放指针,每一个变量都是一个指针

比如说下面这段代码中。

#include<stdio.h>
int main()
{
	const char* arr[4] = { "abcdef","qsja","hello world","hehe" };
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%s\n", arr[i]);
	}
	return 0;
}

我们定义了一个字符指针数组,他是一个数组,数组中的每一个元素都是一个指针,虽然看上去是一个字符串,但是我们这是将一些字符串放入每一个元素中的,这每一个元素都是一个指针。就相当于将一个字符串赋值给一个指针,那我们就知道,这就是是相当于将字符串的首元素的地址赋值给指针了。而这些字符串都是只读的,所以要加上const。然后直接打印即可

 当然还有下面这个整型指针数组的例子

#include<stdio.h>
int main()
{
	int arr1[4] = { 0,1,2,3 };
	int arr2[4] = { 1,2,3,4 };
	int arr3[4] = { 2,3,4,5 };
	int arr4[4] = { 3,4,5,6 };
	
	int* arr[4] = { arr1,arr2,arr3,arr4 };

	int i = 0;
	for (i = 0; i < 4; i++)
	{
		int j = 0;
		for (j = 0; j < 4; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

这段代码就是先创建了四个整型数组,然后再创建一个整型指针数组,由于数组名就是首元素的地址,所以直接将数组名放进去可以了。此时这个指针数组里面存放的四个指针就是这四个数组的首元素的地址,然后我们进行打印,首先是arr[i]找到了这四个数组的首元素的地址,然后在通过一个下标i来继续访问后面的元素即可。

当然我们这种打印的写法还可以转化成指针形式的写法,而且还有我们之前的操作符转化的方式。可以拓展出下面几种写法

 除过整型指针数组和字符指针数组以外,还有二级指针数组等等

int* arr1[10]         //整型指针数组

char* arr2[10]      //字符指针数组

char** arr3[10]     //二级字符指针数组

...............

三、数组指针

1.数组指针的定义

我们知道

字符指针 :存放字符地址的指针 ,指向字符的指针,char*

整型指针 :存放整型地址的指针 ,指向整型的指针,int*

浮点型的指针:存放浮点型地址的指针,指向浮点型的指针,float*,double*

那么我们可以推出:数组指针就是一个存放数组地址的指针,指向数组的指针

那么数组指针该如何写出来呢?应该这样写

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int(*pa)[10] = &arr;
	return 0;
}

这样写的话我们也不难看出,arr这个数组的类型是int [10],pa是一个指针,这颗*说明pa是一个指针,由于[]的优先级高于*,所以要将*pa给括号括起来。pa指向的arr这个数组的地址,pa的所指向的类型是int [10],pa他本身的类型就是int (*)[10],由于数组名就是在中类型的中间夹着的,导致了这个数组指针的类型中,指针名就夹在了类型的中间。

2.数组名和&数组名

我们看下面的这段代码

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	
	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+1相差四个字节的地址,&arr[0]和&arr[0]+1的地址相差四个字节,而&arr和&arr+1却相差40个字节,这是因为,数组名代表首元素的地址。&arr是整个数组的地址,数组首元素的地址和数组的地址从值的角度来看是一样的,但是意义是不一样的,他们的步长是有区别的。数组首元素的地址,他+1后,移动一个元素的地址。而整个数组取地址后+1,移动整个数组的地址,从指针的角度来看的话,数组名代表首元素的地址,他的类型就是int*,而取地址数组名的话,他代表整个数组的地址,他的类型是int(*)[10],那么步长当然就有区别了。

关于这两个的类型,我们还要注意不要用混了,比如说把一个数组首元素的地址传给了数组指针,这样的话就是不合理的,除非进行强制类型转化,当然那样做虽然不合理,但是vs2022编译器却能通过

比如下面这样就可以进行强制类型转化。

当然还有字符数组指针的声明,我们也可以类比的写出来

 3.数组指针的使用

数组指针是如何使用的呢?我们知道如何使用数组的首元素地址去打印整个数组,那么能不能用整个数组的地址去打印整个数组呢?我们可以这样做

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

当然我们这样显得有点大材小用了,其实数组指针一般不是用于一维数组的,而是用在二维数组中的。我们看下面的代码

这是一种二维数组传参的方式,目的是打印这个二维数组,这个我们都很容易能理解的。但是我们要思考一下,如果是一维数组传参的话,可以写一个数组的模样,也可以写一个指针,因为数组传参传的是首元素的地址。那么二维数组传参可以使用一个指针来接收吗

当然是可以的,其实二维数组传参传的也是首元素的地址,只不过这个首元素有一点不同,我们之前说过二维数组的首元素是第一行一维数组。所以我们传参传的直接就是一行一维数组的地址,要接收这个一维的数组的地址,我们就只能使用一个数组指针来进行接受。于是我们的代码可以写成如下所示

 根据这段代码,我们也瞬间理解了,为什么二维数组必须要有列,但是行是不必要的。这是因为我们二维数组名可以看成一个个指向一行数组的指针。先通过一次解引用,访问到这一行数组,然后再通过一个解引用,就可以很自然的访问到每一个元素了

#include<stdio.h>
void print1(int arr[3][4], int r, int c)
{
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		for (j = 0; j < c; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
void print2(int(*arr)[4], int r, int c)
{
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		for (j = 0; j < c; j++)
		{
			printf("%d ", (*(arr + i))[j]);
			//等价于 printf("%d ",arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
	//print1(arr, 3, 4);
	print2(arr, 3, 4);
	return 0;
}

有了上面的理解,我们来看一下下面这几个代码的意思

int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];

解析

int arr[5];                //一个整型数组,数组有五个元素,每个元素都是int类型
int *parr1[10];        //一个指针数组,数组有十个元素,每个元素都是int*类型
int (*parr2)[10];       //这是一个数组指针,这个指针指向一个有十个元素的,每个元素都是int类型的数组
int (*parr3[10])[5];//首先parr3先跟[]结合,所以他是一个数组,这个数组有十个元素,每个元素的类型我们只需要将parr3[10]抽出来就知道了,所以他的每个元素的类型是int (*)[5],也就是每个元素都是一个数组指针,而这每一个数组指针都指向一个数组,这个数组是由五个元素的,每个元素都是int类型的

四、数组参数、指针参数

1.一维数组传参

我们看如下所示的一些代码

首先这是一些伪代码,但是我们要能够知道,上面这些传参是否都是合理的呢?

一维数组传参,要么直接照着数组本来的样子直接模仿着写过去,要么就要写成指针的方式,就这两种方式

显然第一种,第二种属于直接模仿这数组的模样写过去的,所以当然是没有任何问题的,第三种是写成了指针的方式,数组名就是首元素地址,所以当然也是没有任何问题的。

第四种,他是传一个指针数组,他直接写了模仿者写了一个数组,当然是没有任何问题的,而且这个数组甚至可以不用写具体的个数

第五种,他是使用了一个二级指针,二级指针的意思是,一颗*代表着arr是一个指针,int*代表着,arr指向一个整形指针。而我们传参的时候,arr2就是一个指针数组,数组的每一个元素都是一个指针,我们传参传的是数组名,数组名就是首元素的地址,也就是相当于我们将一个指针的地址传了过去,所以当然是用一个二级指针来接受了,如果用一级指针来接受,那就报错了

2.二维数组传参

我们来分析下面的代码

 显然这也是一段伪代码,但是我们是可以进行分析的。

第一种方式直接模仿数组的样子写出来的形参,是没有任何问题的

第二种方式模仿但没模仿完,因为他缺少了行标和列标,我们知道,行标可以省略,列标是万万不可以省略的。我们也可以从指针的角度进行分析,因为数组名就是首元素地址,二维数组的首元素地址就是第一个一维数组的地址,那么如果缺少了列标,那就相当于不知道首元素地址的类型了,这就出现问题了

第三种方式没有省略列标,这是正确的

第四种方式、第五种方式、第六种方式、第七种方式中,只有第六种方式是正确的,因为二维数组传参传的是一个一维数组的地址,要使用数组指针来接收,只有第六种是数组指针类型的,其他的指针类型都不匹配,所以只有第六个正确。

3.一级指针传参

我们来看下面这个代码

这个方式是当然可行的,实际上一级指针传参也只能放一个一级指针的形参过去。否则类型也不匹配

那么我们思考一下形参是一级指针,那么实参可以是什么呢?

其实形参是一级指针情况下

比如说int* p是形参

那么我们可以传一个整型变量的地址

比如

int a;

test(&a);

我们也可以传一个一级指针过去

int* p=&a;

test(p);

我们也可以传一个数组名过去

int arr[5];

test(arr);

总的来说就三种方式:变量的地址,一级指针,数组名

4.二级指针传参

我们来看下面的代码

这段代吗就是将一个二级指针传过去的,那么我们想要接收一个二级指针,只能使用一个二级指针来接收

那么我们反过来思考一下:如果形参是二级指针,那么实参可以有哪些呢?

实际上,如果形参是二级指针

那么实参可以是一级指针的地址,也可以是二级指针,也可以是一个指针数组

五、函数指针

我们已经知道了数组指针、就是指向整个数组的地址

那么函数指针呢,其实就是指向函数的指针,我们可以类比的写出他的类型

#include<stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pA)(int, int) = &Add;
	return 0;
}

因为函数也是我们自定义出来的类型,而决定一个函数的主要因素就是返回类型,参数类型,和函数名。去掉函数名,就是函数的类型,然后使用一颗*代表着这个变量是一个指针。而这就是一个函数指针,他指向一个函数,他指向的函数的类型就是去掉他后的类型。这与数组指针是极其类似的。

但是也与数组指针不同的地方,数组名是数组首元素的地址,那么函数名是否又代表了某种含义呢?其实是的,函数名和&函数名都是函数的地址,这是函数与数组类型的一个区别。所以我们也可以这样定义函数指针,这两种方式都是可行的

那么我们知道了函数指针的定义,该如何调用它呢?其实也是一样的道理,先解引用它,然后直接输入参数即可

其实,函数指针调用中的那颗*是一个摆设,我们可以不用写它的,也可以写很多个,都是没有任何问题的

 这一点也是函数指针与其他指针的一个区别,还有一个区别是函数指针是无法进行加减运算的,因为也没有任何意义

我们最后来看两个代码,分析他们

#include<stdio.h>
int main()
{
	//代码1
	(*(void (*)())0)();
	//代码2
	void (*signal(int, void(*)(int)))(int);
	return 0;
}

首先是代码1:

这段代码可以一看的话,就懵逼了,其实大可不必,这段代码的意思是是将0强制类型转化为void (*)()类型的函数指针,然后再去调用0地址处的这个函数

我们在来分析代码2:

这是一次函数声明

声明的函数名字是signal

signal有两个参数,一个是int,一个是函数指针类型:void(*)(int),

signal的返回类型是也是一个函数指针:类型是void(*)(int)

当然这样我们会发现还是比较难以理解,我们可以使用一个typedef来简化代码

如下图所示:

 要注意的这个重定义类型可能看起来比较奇怪,这是对指针的typedef时要放在*旁边

同样的,对于数组使用typedef也要放在[]旁边


总结

本小节讲解了字符指针、指针数组、数组指针、函数指针,以及数组和指针传参的问题

如果对你有帮助,不要忘记点赞加收藏哦!!!

想获得更多优质内容,一定要关注我哦!!!

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

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

相关文章

Vue3新特性

文章目录一 新特性之组合API1.1 ref&reactive1.2 methods1.3 props和context1.4 完整代码&效果演示二 Vue3新特性之生命周期函数(在setup中)三 父子级组件间数据传递3.1 Provide&Inject四 Fragment&#xff08;碎片&#xff09;一 新特性之组合API 1.1 ref&re…

XMLHttpRequest和Referer

XMLHttpRequest 对象简介 1999年&#xff0c;微软公司发布 IE 浏览器5.0版&#xff0c;第一次引入新功能&#xff1a;允许 JavaScript 脚本向服务器发起 HTTP 请求。这个功能当时并没有引起注意&#xff0c;直到2004年 Gmail 发布和2005年 Google Map 发布&#xff0c;才引起广…

承蒙时光不弃,做个好人!

落幕 2022年博客之星终于要在今晚2023年1月7日24点整落下帷幕&#xff0c;从上个月28号开始&#xff0c;仿佛经历了一场噩梦&#xff0c;本是抱着随便玩玩的心态报了名&#xff0c;没成想&#xff0c;刚开始自投五星之后竟然显示10几名&#xff0c;那是不是我稍加努力就进前十…

狂揽两千星,速度百倍提升,高性能 Python 编译器 Codon 火了

前言 众所周知&#xff0c;Python 是一门简单易学、具有强大功能的编程语言&#xff0c;在各种用户使用统计榜单中总是名列前茅。相应地&#xff0c;围绕 Python&#xff0c;研究者开发了各种便捷工具&#xff0c;以更好的服务于这门语言。 编译器充当着高级语言与机器之间的…

TensorFlow笔记之单神经元完成多分类任务

文章目录前言一、逻辑回归1.二分类问题2.多分类问题二、数据集调用三、TensorFlow1.x1.定义模型2.训练模型3.结果可视化四、TensorFlow2.x1.定义模型2.训练模型3.结果可视化总结前言 记录分别在TensorFlow1.x与TensorFlow2.x中使用单神经元完成MNIST手写数字识别的过程。 一、…

Linux出现ping: www.baidu.com: 未知的名称或服务解决方法

文章目录解决对象方法先找到网关在Windows下进行VMnet8的配置ping成功Linux出现ping: www.baidu.com: 未知的名称或服务解决方法 解决对象 本文的方法用于各位大佬已经用过以下方法仍然无法ping成功 Linux防火墙已关闭和Windows防火墙已经关闭已经配置好 vim /etc/sysconfig/…

手撕C语言理论知识(上)粗略讲解C语言的部分入门知识

目录 C语言的一些基础知识 操作符简介 Scanf的%[ ] 语句&#xff08;分支、循环、goto&#xff09; 函数 C语言的一些基础知识 主函数 - 程序的入口 - main函数有且仅有一个。char - short - int - long - long long - float - double%d - 十进制整型 %u - 无符号整型 %…

【博学谷学习记录超强总结,用心分享|产品经理基础总结和感悟15】

互联网产品设计背后的心理学02&#xff1a;你就是会被其他人的行为所影响一、前言二、实验设计及结果分析三、实验原理四、实验方法总结五、产品设计中的应用六、结束语前文回顾&#xff1a;让人们做出决定并不是信息本身&#xff0c;而是这些信息呈现的背景或情景。我们这个信…

Spring Cloud Alibaba Dubbo(服务远程调用)

一、软件环境 &#xff08;1&#xff09;自己部署服务器 所有软件及服务器自己进行管理提供&#xff0c;可以直接在项目中添加Spring Cloud依赖。推荐 <dependencyManagement> <dependencies> <dependency> <groupId>com.a…

liunx centos9中安装flask并在pycharm中使用图文攻略

liunx centos9中安装flask并在pycharm中使用图文攻略1.首先在liunx的终端中输入2.安装好flask之后就在pycharm创建新的项目处添加flask项目3.点击绿色三角箭头开始运行flask项目4. 然后登录ip地址就出现Hllo world就代表flask环境搭建完成需要注意事项1.首先在liunx的终端中输入…

ngx_thread_pool_init()

ngx_thread_pool_cycle()函数的主要工作是从待处理的任务队列中获取一个任务&#xff0c;然后调用任务对象的handler()函数处理任务&#xff0c;完成后把任务放置到完成队列中&#xff0c;并通过ngx_notify()通知主线程 手写线程池与性能分析 - 知乎 pthread_cond_wait函数的原…

【5G RRC】5G系统消息介绍

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…

一键绕过ID锁激活,为什么很多人都会失败?绕ID这一篇就够了

最近阳了所以暂时断更&#xff0c;你们也要注意身体&#xff0c;最好不要阳 现在绕ID的方法已经非常完善&#xff0c;一个小白选手只要有设备就可以正常绕过ID&#xff0c;总的来说绕ID分为两个步骤&#xff1a;第一步是手机的越狱&#xff0c;这里只能是用checkra1n越狱&…

数据在内存中存储☞(超详解)

目录 一.数据类型大家族 1.了解类型的意义 2.数据类型大家族的分类 二.详解☞数据储存之整形 1.储存方式 &#xff08;1&#xff09;.原码反码补码的概念 &#xff08;2&#xff09;.原码反码补码出现的原因&#xff1a; 计算机中只有加法器没有减法器&#xff0c;所有只…

SemanticKITTI: A Dataset for Semantic Scene Understanding of LiDAR Sequences

Paper name SemanticKITTI: A Dataset for Semantic Scene Understanding of LiDAR Sequences Paper Reading Note URL: https://arxiv.org/pdf/1904.01416.pdf TL;DR 2019 ICCV 论文&#xff0c;提出了一个大规模的真实场景 LiDAR 点云标注数据集 SemanticKITTI&#xff…

数字信号处理第六次试验:数字信号处理在双音多频拨号系统中的应用

数字信号处理第六次试验&#xff1a;数字信号处理在双音多频拨号系统中的应用前言一、实验目的二、实验原理和方法1.关于双音多频拨号系统2.电话中的双音多频&#xff08;DTMF&#xff09;信号的产生与检测3.检测DTMF信号的DFT参数选择4.DTMF信号的产生与识别仿真实验三、实验内…

菜鼠的保研总结

1.个人基本情况 本科学校&#xff1a;山东某双非 本科专业&#xff1a;网络工程 成绩排名&#xff1a;1/46 英语成绩&#xff1a;四级529&#xff0c;六级502 科研竞赛&#xff1a;美国大学生数学建模比赛特等奖提名、全国英语翻译比赛三等奖、山东省蓝桥杯java大学生B组三等奖…

C++基础:KMP

让我们先看一个问题&#xff1a;给定一个字符串 S&#xff0c;以及一个模式串 P&#xff0c;所有字符串中只包含大小写英文字母以及阿拉伯数字。模式串 P 在字符串 S 中多次作为子串出现。求出模式串 P 在字符串 S 中所有出现的位置的起始下标。输入格式第一行输入整数 N&#…

【Python】杨辉三角中的排成一列编号的问题

题目描述 下面的图形是著名的杨辉三角形&#xff1a; 如果我们按从上到下、从左到右的顺序把所有数排成一列&#xff0c;可以得到如下数列&#xff1a; 1,1,1,1,2,1,1,3,3,1,1,4,6,4,1,⋯ 给定一个正整数 N&#xff0c;请你输出数列中第一次出现 N是在第几个数&#xff1f; …

Go语言设计与实现 -- 内存管理器

不同的编程语言选择不同的方式管理内存&#xff0c;本节会介绍Go语言内存分配器。 Go内存分配的设计思想是&#xff1a; 内存分配算法采用Google的TCMalloc算法&#xff0c;每个线程都会自行维护一个独立的内存池&#xff0c;进行内存分配时优先从该内存池中分配&#xff0c;…