【C语言】指针详解(2)

news2024/11/16 3:45:25

大家好,我是苏貝,本篇博客带大家了解指针(2),如果你觉得我写的还不错的话,可以给我一个赞👍吗,感谢❤️
在这里插入图片描述


文章目录

  • 一. 字符指针
  • 二 . 指针数组
    • 2.1 模拟一个二维数组
    • 2.2 维护多个字符串
  • 三 . 数组指针
    • 3.1 解释含义
  • 四 . 数组参数、指针参数
    • 4.1 一维数组传参
    • 4.2 二维数组传参
    • 4.3 一级指针传参
    • 4.4 二级指针传参
  • 五 . 函数指针

一. 字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ,一般这样使用:

	char ch = 'a';
	char* p = &ch;
	*p = 'w';

还有一种使用方式如下:

	char* p = "abcdefg";

这是将一个字符串放到p指针变量里了吗?
不是的,指针变量p中存放的是字符串首字符的地址,也就是字符a的地址,能否简单的证明一下呢?

int main()
{
	char* p = "abcdefg";
	printf("%c", p[3]);
	return 0;
}

答案:d。因为p中存放的是字符a的地址,p[3]==* (p+3)==d

还要注意:“abcdefg”是常量字符串,不能被修改,所以*p = ‘w’;是错误的,因此最好在char * p= “abcdefg”;之前加const

int main()
{
	const char* p = "abcdefg";
	//*p = 'w';//err
	printf("%s", p);
	return 0;
}

上面的了解清楚之后,我们来看下面的笔试题

#include <stdio.h>
int main()
{
	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	const char* str3 = "hello bit.";
	const 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;
}

在这里插入图片描述

拓展:
数组名是数组首元素地址,除了以下2种情况:
(1)sizeof(数组名),此时的数组名代表整个数组,所以计算结果是整个数组的大小
(2)&数组名,此时的数组名也代表整个数组,取出的是整个数组的地址,返回的是数组首元素的地址

解析:if语句中str1和str2是数组str1和str2的首元素地址,在定义两个字符数组时,会开辟出不同的内存块,所以它们的首元素地址当然不同。因为“hello bit”是常量字符串,不能修改,所以没有必要用两块内存空间存储相同的常量字符串,因此C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。


二 . 指针数组

我们知道,字符数组是存放字符类型的数组,整型数组是存放整型的数组,那么指针数组就当然是存放指针类型的数组,也就是说数组里面的元素都是指针类型的。指针数组可以写成如下形式:

int* arr[10];

[ ]的优先级高于 * ,所以arr先与[ ]结合成数组,数组有10个元素,元素的类型为int * 。那么指针数组有什么用呢?

2.1 模拟一个二维数组

下面代码的结果是打印arr1~arr3

int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };
	//指针数组
	int* arr[] = { arr1,arr2,arr3 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

解析:
1.因为该代码中数组名是首元素地址,所以数组arr中存储的是arr1~arr3的首元素地址,所以类型为int *。
2.arr[i]是找到数组arr中第i个元素,如arr[1]就是找到第2个元素即数组arr2的首元素地址。arr[1][ j ]== *(arr[1]+j),就是找到arr2的首元素地址后,再+j找到数组arr2中第j个元素

在这里插入图片描述

2.2 维护多个字符串

int main()
{
	char* str[] = { "qing","dian","ge","zan","ba" };
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%s\n", str[i]);
	}
	return 0;
}

由上面的字符指针知,数组str存放的是字符串首字符的地址,所以结果为:

在这里插入图片描述


三 . 数组指针

我们知道,字符指针是指针,指向字符。整型指针是指针,指向整型。那么数组指针也是指针,指向的是数组。可以写成如下形式:

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

*代表p为指针,因为[ ]的优先级高于 * ,所以为避免arr先与[ ]结合成数组,在 *p外面要加括号。[10]代表p指向的是数组,数组有10个元素,类型为int
注意:[ ]内一定要有数字,否则会报错

练习:指向下面指针数组的数组指针p该如何写呢?

char* arr[5];

先用 * 修饰p,代表p为指针,再将* p放在( )里,p指向的数组有5个元素,写成(*p)[5],数组的类型为char * ,所以最后写成:

char* (*p)[5] = &str;

数组指针有什么用呢?
对二维数组进行传参时,我们一般会选择下面这种方法:(形参的数组[ ]中,行数可以不写,列数一定要写)

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

int main()
{
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
	print(arr, 3, 5);
	return 0;
}

但我们知道,数组传参本质上传的时数组首元素地址,所以可用指针来接收。之所以形参也可用数组的形式,是为了初学者能更好地理解。事实上,即便形参采用的是数组的形式,也不会真正创建一个数组。那我们如何用指针来接收二维数组呢?

在解决这一问题之前,我们要知道,二维数组是下面这样存储的

在这里插入图片描述

但是我们可以想象成下面这种3行5列的形式

在这里插入图片描述
二维数组作为实参,因为数组名为首元素地址,即第一行的地址{1,2,3,4,5},所以用指针来接收时指针要指向第一行,又因为第一行有5个元素,所以指针是数组指针,如下:

void print(int(*p)[5], int row, int col)
{
	int i = 0;
	for (i = 0; i < row; i++)
	{
		int j = 0;
		for (j = 0; j < col; j++)
		{
			printf("%d ", p[i][j]);
			//p[i]=*(p+i)找到下标为i的行
			//p[i][j]=*(*(p+i)+j)找到下标为i的行的下标为j的元素
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
	print(arr, 3, 5);
	return 0;
}

3.1 解释含义

学了指针数组和数组指针,让我们来一起回顾并看看下面代码的意思

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

解释:
1.arr是数组,数组有5个元素,元素的类型为int
2.parr1是数组,数组有10个元素,元素的类型为int*
3.parr2是数组指针,指向数组,数组有10个元素,元素的类型为int*
4.parr3是数组,是存放数组指针的数组,数组有10个元素,存放的这个数组指针指向的这个数组,有5个元素,元素的类型为int


四 . 数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

4.1 一维数组传参

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

解析:
1.ok,用数组传参,可以用数组接收
2.ok,数组传参的本质是传首元素地址,事实上,即使形参写成数组形式,也不会真正创建一个数组,所以形参数组中[ ]里面的数字可写可不写,也可以写错,如写成10000
3.ok,数组传参的本质是传首元素地址,形参用一级指针接收
4.ok,用数组传参,可以用数组接收
5.ok,数组传参的本质是传首元素地址,形参用(*arr)代表是指针,指向数组首元素,元素类型为int *,所以形参为 int ** arr


4.2 二维数组传参

void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
void test(int* arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int(*arr)[5])//ok?
{}
void test(int** arr)//ok?
{}

int main()
{
	int arr[3][5] = { 0 };
	test(arr);
}

解析:
1.ok,用数组传参,可以用数组接收,行数可以不写,列数一定要写,因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
2.no
3.ok
4.no,用指针来接收二维数组:二维数组的数组名是首元素地址,即可以想象成第一行的地址,每一行有5个元素。用指针来接收二维数组时,因为指向的是一行5个元素的地址,所以指针为数组指针,即 int(*arr)[5]
5.no
6.ok
7.no,二级指针是指向一级指针的,即二级指针存储的是一级指针的地址


4.3 一级指针传参

#include <stdio.h>
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]);
	//一级指针p,传给函数
	print(p, sz);
	return 0;
}

思考:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

void text(int* p)
{}

int main()
{
	int a = 0;
	int p = &a;
	int arr[5] = { 0 };

	text(&a);//传整型变量的地址
	text(p);//传一级整型指针
	text(arr);//传整型一维数组的数组名

	return 0;
}

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

思考:
当一个函数的参数部分为二级指针的时候,函数能接收什么参数?

void text(int** pp)
{}

int main()
{
	int a = 0;
	int* p = &a;
	int** pp = &p;
	int* arr[10] = { 0 };

	text(&p);//传的是一级指针变量的地址
	text(pp);//传的是二级指针变量
	text(arr);//传的是int* 类型的一维数组的数组名

	return 0;
}

五 . 函数指针

数组指针–指向数组的指针–存放的是数组的地址–&数组名就是数组的地址–数组名是首元素地址
函数指针–指向函数的指针–存放的是函数的地址–如何得到函数的地址呢?&函数名吗?

带着疑问,我们来看下面的代码

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	printf("%p\n", &Add);
	printf("%p\n", Add);

	return 0;
}

在这里插入图片描述
因为&函数名==函数名,所以&函数名就是函数的地址,函数名也是函数的地址
那我们该如何写函数指针呢?以上面代码的函数Add为例

int (*p) (int, int)=&Add ;
int (*pp) (int, int)=Add ;

首先,用* 修饰p,表示p是个指针变量,再将* p放在( )中,在(*p)后面加( ),( )里面写函数的实参类型,最后在(*p)前写函数的返回类型
注意:*p必须放在( )里,否则因为( )的优先级高于 * ,p先与( )结合,即int * p (int, int),这样就是函数声明

如何用函数指针调用函数?

1.使用 (*pf1)(2, 3),因为pf1存放的是函数的地址,所以用 * 解引用找到该函数,再在( )里面写上对应的参数
2.使用 pf2(2, 3),我们平时调用函数时,也只是用函数名+( ),例如:Add(),因为pf2存放的是函数的地址,所以直接使用 pf2(2, 3)而不需要对pf2进行解引用
注意:因为pf1也是函数地址,所以 * 可写可不写,也可以写多个 * 。如果想使用 * 解引用,那么 *pf1必须放在( )中,否则先调用函数pf1(2,3)==5,然后执行 *5操作。

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int(*pf1)(int, int) = &Add;
	int ret1 = (*pf1)(2, 3);
	printf("%d\n", ret1);

	int(*pf2)(int, int) = Add;
	int ret2 = pf2(2, 3);
	printf("%d\n", ret2);

	return 0;
}

阅读两段有趣的代码

//代码1
(*(void (*)() )0 ) ();
//代码2
void (*signal(int, void(*)(int) ) )(int);

代码1:
我们以0为切入点,发现0前的( )内void ()( )是函数指针类型,( )内是类型,意思是强制类型转换,因为( )内是函数指针类型,所以0应该是地址而非数字,且0地址处的内容是函数。(void ( * )( ) ) 0前有 * ,意思是调用0地址处的函数,((void (*)() )0 ) 后面( )内无变量代表该函数无实参
代码2:
我们以signal函数为切入点,signal函数的参数有2个,第一个是int类型,第二个是函数指针类型,该函数指针指向的函数,参数是int,返回类型是void。发现缺失signal函数的返回类型,所以signal函数的返回类型也是函数指针类型,该函数指针指向的函数,参数是int,返回类型是void。所以这个代码是一次函数声明,声明的是signal函数。
我们发现该代码非常的复杂,能否简化一点呢?

//将void(*)(int)类型重命名为p_fun
//但不能写成typedef void(*)(int) p_fun;//err
typedef void(*p_fun)(int);//ok
p_fun signal(int, p_fun);

好了,那么本篇博客就到此结束了,如果你觉得本篇博客对你有些帮助,可以给个大大的赞👍吗,感谢看到这里,我们下篇博客见❤️

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

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

相关文章

软件工程(八) UML之类图与对象图

1、类图与对象图 1.1、类图与对象图的概念 类图(class diagram)描述一组类、接口、协作和它们之间的关系 对象图(object diagram)描述一组对象及它们之间的关系、对象图描述了在类图中所建立的事物实例的静态快照。 1.2、类图与对象图的区别 类图和对象图基本上是一样…

【Redis从头学-12】Redis主从复制和读写分离的多种部署方式解析(普通方式、Docker搭建方式、Docker-Compose搭建方式)

&#x1f9d1;‍&#x1f4bb;作者名称&#xff1a;DaenCode &#x1f3a4;作者简介&#xff1a;啥技术都喜欢捣鼓捣鼓&#xff0c;喜欢分享技术、经验、生活。 &#x1f60e;人生感悟&#xff1a;尝尽人生百味&#xff0c;方知世间冷暖。 &#x1f4d6;所属专栏&#xff1a;Re…

[MyBatis系列⑤]多表查询 | 一篇万字长文带你上手三种多表查询方式及其对象封装过程

目录 1、简介 2、业务场景及环境准备 2.1、环境 2.2、ER图 2.3、SQL 3、一对一 3.1、POJO 3.2、OrderMapper.xml 3.3、resultMap 3.4、执行结果 4、一对多 4.1、POJO 4.2、UserMapper.xml 4.3、resultMap 4.4、执行结果 5、多对多 5.1、POJO 5.2、UserMapper.…

2048小游戏成品源码

2048小游戏&#xff0c;可以自选背景颜色&#xff0c;方框颜色&#xff0c;音乐播放。 还可以展示当前玩家的排名&#xff0c;动态排名&#xff0c;及历史玩家的排名。 前期需求&#xff1a; 使用pygame加载目录音乐。MP3文件&#xff1a; def music_play():import pygame …

基于Java+SpringBoot+Vue前后端分离工厂车间管理系统设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

Android实现跟随滑块移动显示的seekBar

概述 详细讲述跟随滑块移动显示的seekBar效果的自定义实现过程 详细 前言 在Android开发过程中&#xff0c;我们有时会使用到自定义Seekbar,如在滑动滑块时&#xff0c;文字随滑块移动之类的效果&#xff0c;今天就来讲讲跟随滑块移动显示的seekBar的实现吧。 今天涉及内容…

IO进程线程、开启进程frok函数,exec函数族

进程是一个独立的可调度的任务 进程是一个抽象实体。当系统在执行某个程序时&#xff0c;分配和释放的各种资源 进程是一个程序的一次执行的过程 主要的进程标识&#xff1a; 进程号(Process Identity Number&#xff0c;PID) 父进程号(Parent Process ID&#xff0c;PPID) …

DML语句的用法(MySQL)

文章目录 前言一、DML介绍二、DML语句操作1、给指定字段添加数据2、给全部字段添加数据3、批量添加数据4、修改数据5、删除数据 总结 前言 本文主要介绍SQL语句中DML语句的用法。 在实验开始之前我们先创建一下所要使用表&#xff0c;如下图所示&#xff1a; 一、DML介绍 DM…

战略在集体学习过程中涌现

战略学习派&#xff1a;战略是涌现的学习过程&#xff0c;中国人的话&#xff0c;要交学习费&#xff01;【安志强趣讲269期】 趣讲大白话&#xff1a;出来混总要交学费 **************************** 中国人有这个意识 新进一个领域&#xff0c;要交学费&#xff0c;有学习过程…

【Flutter】Flutter 使用 infinite_scroll_pagination 实现无限滚动分页

【Flutter】Flutter 使用 infinite_scroll_pagination 实现无限滚动分页 文章目录 一、前言二、安装和基本使用1. 添加依赖2. 基础配置和初始化 三、实际业务中的用法1. 与 API 集成2. 错误处理 四、完整示例1. 创建一个无限滚动列表2. 使用在你的应用中3. 完整代码示例 五、总…

【Qt学习】08:文件读写操作

OVERVIEW 文件读写操作一、文件操作1.QFile2.QFileInfo 二、二进制文件读写三、文本文件读写 文件读写操作 文件操作是应用程序必不可少的部分&#xff0c;Qt 作为一个通用开发库提供了跨平台的文件操作能力。Qt 通过QIODevice提供了对 I/O 设备的抽象&#xff0c;这些设备具有…

Web自动化测试之图文验证码的解决方案

对于web应用程序来讲&#xff0c;处于安全性考虑&#xff0c;在登录的时候&#xff0c;都会设置验证码&#xff0c; 验证码的类型种类繁多&#xff0c;有图片中辨别数字字母的&#xff0c;有点击图片中指定的文字的&#xff0c;也有算术计算结果的&#xff0c;再复杂一点就是滑…

电子封条监控系统 yolov5

电子封条监控系统利用yoloov5python 深度学习训练模型技术&#xff0c;电子封条监控系统实现对画面内外的出入人员、人数变化及非煤矿山生产作业状态等情况的实时监测和分析&#xff0c;及时发现异常动态&#xff0c;减少了人为介入的过程。介绍Yolo算法之前&#xff0c;首先先…

Android开发之性能测试工具Profiler

前言 性能优化问题&#xff0c;在我们开发时都会遇到&#xff0c;但是在小厂和对自己要求不严格的情况下&#xff0c;我都很少去做性能优化&#xff1b; 在性能优化上&#xff0c;基本大家都是通过自己的开发经验和性能分析工具来发现问题&#xff0c;今天给大家分享一下小编最…

性能优化之分库分表

1、什么是分库分表 1.1、分表 将同一个库中的一张表&#xff08;比如SPU表&#xff09;按某种方式&#xff08;垂直拆分、水平拆分&#xff09;拆分成SPU1、SPU2、SPU3、SPU4…等若干张表&#xff0c;如下图所示&#xff1a; 1.2、分库 在表数据不变的情况下&#xff0c;对…

【GeoDa实用技巧100例】025:geoda空间回归分析案例教程

严重声明:本文来自专栏《GeoDa空间计量案例教程100例》,为CSDN博客专家刘一哥GIS原创,原文及专栏地址为:https://blog.csdn.net/lucky51222/category_12373659.html,谢绝转载或爬取!!! 文章目录 一、空间自回归模型二、Geoda空间回归分析普通最小二乘法回归(OLS)空间…

设计模式--建造者模式(Builder Pattern)

一、什么是建造者模式 建造者模式&#xff08;Builder Pattern&#xff09;是一种创建型设计模式&#xff0c;它关注如何按照一定的步骤和规则创建复杂对象。建造者模式的主要目的是将一个复杂对象的构建过程与其表示分离&#xff0c;从而使同样的构建过程可以创建不同的表示。…

基于配置类方式管理 Bean

目录 一、完全注解开发理解 二、配置类和扫描注解 三、Bean定义组件 四、Bean注解细节 五、import 扩展 一、完全注解开发理解 Spring 完全注解配置&#xff08;Fully Annotation-based Configuration&#xff09;是指通过 Java配置类 代码来配置 Spring 应用程序&#…

Folyd 多源最短路

目录 简介 实现 代码 关于Floyd的题目 简介 首先我们要知道a到b的最短路是什么 a到b的最短路是从a点到b点的最小距离&#xff08;花费&#xff09; 那多源最短路呢就是能求任意a和b&#xff0c;之间的最短路 那么Folyd是多源最短路&#xff0c;也就是求任意a和b&#x…

春秋云镜 CVE-2019-13275

春秋云镜 CVE-2019-13275 WordPress Plugin wp-statics SQLI 靶标介绍 WordPress VeronaLabs wp-statistics插件12.6.7之前版本中的v1/hit端点存在SQL注入漏洞。 启动场景 漏洞利用 exp time curl -X POST http://host/wp-json/wpstatistics/v1/hit --data "wp_stati…