C语言:指针详解续

一、字符指针变量

我们知道有种指针类型为字符指针(char*)。

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

其实它还有一种使用方式。

#include <stdio.h>
int main()
{char* pstr = "hello world";printf("%s\n", pstr);return 0;
}

在代码 char* pstr = "hello world"; 中,指针 pstr 存放的正是字符串 "hello world" 首字符 'h' 的地址。在 C 语言里,字符串是以字符数组的形式存储在内存中的,并且以 '\0'(空字符)作为字符串结束的标志。当定义一个指针指向一个字符串常量时,该指针的值就是这个字符串存储在内存中起始位置(也就是首字符所在位置)的地址。当在printf函数中使用%s格式化符并且参数是一个字符指针(如char *pstr)时,printf函数会把这个指针当作字符串的起始地址。它会从这个地址开始读取字符,一直读取到遇到'\0'(字符串结束标志)为止。例如,在代码char* pstr="hello world"; printf("%s\n", pstr);中,pstr指向字符串"hello world"的首字符'h'printf会从这个地址开始,逐个字符地输出,直到遇到'\0'才停止。

现在我们来看一看《剑指offer》中的一道题。

#include <stdio.h>
int main()
{char str1[] = "hello";char str2[] = "hello";const char* str3 = "hello";const char* str4 = "hello";if (str1 == str2)printf("str1 and str2 are same\n");elseprintf("str1 and str2 are not same\n");if (str3 == str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return 0;
}

我们来分析一下结果为什么是这样的。当我们使用char数组存放字符串时,就会为数组分配独立的内存空间来存放字符串,所以str1和str2都指的是这两个数组的首字符的地址,即使字符串相同,所对应的地址也不相同。在 C 语言中,像 "hello" 这样的常量字符串在程序中通常只存储一份在只读的内存区域。当定义 str3 和 str4 这两个指针并让它们指向同一个常量字符串"hello" 时,它们实际上都指向了这个字符串的起始地址。所以当进行 str3 == str4 比较时,比较的是两个指针所存储的地址值,因为它们都指向同一个 "hello" 字符串所在的内存位置,所以地址是相同的。其实造成差异的本质原因是前两个都是在定义字符串变量,也就是字符数组,它本身是没有地址的,而后两个常量字符串本身就是有地址的,这里只是将地址取出来存放在指针中。

二、数组指针变量

数组指针变量是一种指针变量,存放的应该是数组的地址,能够指向数组的指针变量。
int(*p)[10];

这就是数组指针的形式。p是这个变量的名字,*说明这个变量是指针,还剩下int [10]就说明它指向的对象是大小为10的整型数组类型的。

注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。否则这个变量就不是数组指针了,而是指针数组。

那么数组指针变量怎么初始化呢?其实和我们之前初始化指针变量一样。

我们调试也能看到 &arr p 的类型是完全一致的。
三、二 维数组传参的本质
#include<stdio.h>
void test(int arr[2][3],int a,int b)
{for (int i = 0; i < a; i++){for (int j = 0; j < b; j++){printf("%d ", arr[i][j]);}printf("\n");}
}
int main()
{int arr[2][3] = { {1,2,3},{2,3,4} };test(arr,2,3);return 0;
}

之前我们使用二维数组传参都是如上图的代码这样去进行操作的。其实二维数组就是一维数组的数组,也就是二维数组的每个元素是一个一维数组。所以二维数组的数组名表示的就是第一行的地址,是一个一维数组的地址。根据上面的例子,第一行的一维数组的类型就是 int [3] ,所以第一行的地址的类型就是数组指针类型 int(*)[3] 。那就意味着二维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址,那么形参也是可以写成指针形式的。如下:

#include<stdio.h>
void test(int(*arr)[3], int a, int b)
{for (int i = 0; i < a; i++){for (int j = 0; j < b; j++){printf("%d ", arr[i][j]);}printf("\n");}
}
int main()
{int arr[2][3] = { {1,2,3},{2,3,4} };test(arr,2,3);return 0;
}

四、函数指针变量

函数指针变量就是存放函数地址的指针变量。我们将刚才的代码稍微改动一下,我们就会发现函数其实有自己的地址。而且函数名就是函数的地址,当然使用&函数名的方式也可以。
那么函数指针变量是什么样的呢?我们再将代码改动一下。
#include<stdio.h>
void test(int(*arr)[3], int a, int b)
{for (int i = 0; i < a; i++){for (int j = 0; j < b; j++){printf("%d ", arr[i][j]);}printf("\n");}
}
int main()
{int arr[2][3] = { {1,2,3},{2,3,4} };test(arr,2,3);void (*p)(int(*arr)[3], int a, int b) = test;return 0;
}

p是变量名,*表示这个变量是指针,前面的void是函数的返回值,后面加的是函数的参数。而且在参数中可以省略具体的变量名,只留下参数的数据类型。在这里就是将a,b去掉,只留下(int,int)。那我们怎么去使用这个函数指针变量呢?

#include<stdio.h>
int add(int a, int b)
{return a + b;
}
int main()
{int (*p)(int, int) = add;printf("%d", p(1, 2));return 0;
}

使用函数指针名加参数就能实现,当然将函数指针名解引用后加参数也能实现,但是直接使用函数指针名编译器就可以理解这是对函数指针所指向的函数的调用,使用起来会更加方便,一般不推荐显式解引用来使用函数。

接下来,我们来看两段c陷阱与缺陷中的代码。当然,这些代码只是为了让我们更加理解指针等知识,其实某些写法并不推荐。

1.(*(void (*)())0)();

来分析一下代码,我们能看到中间的void (*)(),知道这是一个函数指针,参数为空,返回值为void,后面还跟着一个0,那我们就可以知道,这里是将0强制类型转换了,将它变成了函数指针类型,然后将整体解引用,后面再加上空参,就是在调用函数。

2.void (*signal(int , void(*)(int)))(int);

看到signal(int , void(*)(int))应该就能知道这是一个函数名加上参数,一个是整型类型的,一个是函数指针类型的。其实,剩下的void(*)(int)是这个函数的返回值,这种写法确实比较奇怪,我们在这里也不需要多纠结,这个代码整体就是一个函数的声明。

五、typedef 关键字

typedef 是用来类型重命名的,可以将复杂的类型,简单化。比如,你觉得 unsigned int 写起来不方便,如果能写成 uint 就方便多了,那么我们可以使用:
typedef unsigned int uint;
//将unsigned int重命名为uint

一般类型都可以这样进行操作,但是对于数组指针和函数指针稍微有点区别,新的类型名必须在*的右边

#include <stdio.h>
int add(int x, int y)
{return x + y;
}
int main()
{int arr[5] = { 0 };typedef int(*parr_t)[5];//int(*)[5]->parr_ttypedef void(*pf_t)(int);//void(*)(int)->pf_tparr_t p1 = arr;pf_t p2 = add;return 0;
}

六、函数指针数组

要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?是 int (*)() 类型的函数指针。
int (*parr1[3])();

那么函数指针数组有什么用呢,接下来我们来看转移表。举例:计算器的⼀般实现:

#include<stdio.h>
void menu()
{printf("***0.exit***\n");printf("***1.add****\n");printf("***2.sub****\n");printf("***3.mul****\n");printf("***4.div****\n");printf("请输入...\n");
}
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 n;int(*p[5])(int, int) = { 0,add,sub,mul,div };while (1){menu();scanf("%d", &n);if (n > 0 && n < 5){int a, b;printf("请输入两个操作数...\n");scanf("%d%d", &a, &b);printf("结果是:%d\n", p[n](a, b));}else if (n == 0){printf("退出计算器\n");break;}elseprintf("输入有误\n");}return 0;
}

如上图的代码,我们就能实现简易的加减乘除计算器,这就使用了函数指针数组。那么转移表又是什么呢?其实它指的就是运用函数指针数组以数组方式去调用里面的函数,使代码变得更加简洁。

七、回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。 我们可以把调用的函数的地址以参数的形式传递过去,使用函数指针接收,函数指针指向什么函数就调用什么函数。下面我们就使用回调函数来实现上面说的简易计算器。
#include<stdio.h>
void menu()
{printf("***0.exit***\n");printf("***1.add****\n");printf("***2.sub****\n");printf("***3.mul****\n");printf("***4.div****\n");printf("请输入...\n");
}
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 is(int(*p)(int, int))
{int a, b;printf("请输入两个操作数...\n");scanf("%d%d", &a, &b);printf("结果是:%d\n", p(a, b));
}
int main()
{int n;int(*p[5])(int, int) = { 0,add,sub,mul,div };while (1){menu();scanf("%d", &n);if (n > 0 && n < 5){switch (n){case 1:{is(add);break;}case 2:{is(sub);break;}case 3:{is(mul);break;}case 4:{is(div);break;}}}else if (n == 0){printf("退出计算器\n");break;}elseprintf("输入有误\n");}return 0;
}

八、qsort 使用举例

qsort的作用是对数组的元素进行排序,使用 compar 函数确定顺序,将base 指向的数组的 num 个元素进行排序,每个元素大小为size。该函数不返回任何值,但通过对 compar 定义的元素进行基数重新排序来修改指向的数组的内容。我们能发现参数中的指针都是void*类型的,这是因为我们进行操作的数据类型不是固定的。

我们先来看一下compar 函数,它可以比较元素对,并将指向它们的指针作为参数。其返回值如下图,一般可以简化成小于返回-1,等于返回0,大于返回1。

1.使用qsort函数排序整型数据

#include<stdio.h>
#include<stdlib.h>
int int_compar(const void* p1, const void* p2)
{return *((int*)p1) - *((int*)p2);
}
int main()
{int arr[] = { 4,3,6,12,8,2,9,7,1,5 };qsort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(int), int_compar);for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){printf("%d ", arr[i]);}return 0;
}

2.使用qsort排序结构数据

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu
{char name[20];int age;
};
int cmp_stu_by_age(const void* e1, const void* e2)
{return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_stu_by_name(const void* e1, const void* e2)
{return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void test1()
{struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}
void test2()
{struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{test1();test2();return 0;
}

通过调试我们可以看出结构体中的数据确实被排序了。

3.qsort函数的模拟实现

使用回调函数,模拟实现qsort(采用冒泡的方式)。
#include <stdio.h>
int int_cmp(const void* p1, const void* p2)
{return (*(int*)p1 - *(int*)p2);
}
void _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 bubble(void* base, int count, int size, int(*cmp)(void*, void*))
{int i = 0;int j = 0;for (i = 0; i < count - 1; i++){for (j = 0; j < count - i - 1; j++){if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0){_swap((char*)base + j * size, (char*)base + (j + 1) * size,size);}}}
}
int main()
{int arr[5] = { 2,3,4,1,5 };bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), int_cmp);for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){printf("%d ", arr[i]);}return 0;
}

九、sizeof和strlen的对比

1.sizeof

sizeof 计算变量所占内存内存空间大小的,单位是字节,如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。sizeof 只关注占用内存空间的大小,不在乎内存中存放什么数据。操作数是变量可以不加括号。
#include <stdio.h>
int main()
{int a = 10;printf("%d\n", sizeof(a));printf("%d\n", sizeof a );printf("%d\n", sizeof(int));return 0;
}

2.strlen

strlen 是C语言库函数,功能是求字符串长度。函数原型如下:
size_t strlen(const char* str);
统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数。
strlen 函数会⼀直向后找 \0 字符,直到找到为止,所以可能存在越界查找。
3. sizeof 和 strlen的对比
sizeof:
(1)  sizeof是操作符
(2) sizeof计算操作数所占内存的大小,单位是字节
(3) 不关注内存中存放什么数据
strlen:
(1) strlen是库函数,使用需要包含头文件 string.h
(2) srtlen是求字符串长度的,统计的是 \0 之前字符的个数
(3) 关注内存中是否有 \0 ,如果没有 \0 ,就会持续往后找,可能
会越界

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

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

相关文章

HarmonyOS-高级(一)

文章目录 一次开发、多端部署自由流转 &#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;HarmonyOS专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年12月09日12点19分 一次开发、多端部署 布局能力 自适应布局 拉伸能力均分能力占比能力缩放…

【嵌入式系统】第4章 嵌入式最小系统,供电电路,时钟电路,复位电路,程序下载电路

关注作者了解更多 我的其他CSDN专栏 过程控制系统 工程测试技术 虚拟仪器技术 可编程控制器 工业现场总线 数字图像处理 智能控制 传感器技术 嵌入式系统 复变函数与积分变换 单片机原理 线性代数 大学物理 热工与工程流体力学 数字信号处理 光电融合集成电路…

期权懂|交易个股期权需要注意哪些风险?

期权小懂每日分享期权知识&#xff0c;帮助期权新手及时有效地掌握即市趋势与新资讯&#xff01; 交易个股期权需要注意哪些风险&#xff1f; 一、交易个股期权需要注意合约到期风险&#xff1a; 需关注到期日&#xff0c;及时平仓或行权&#xff0c;避免合约作废。二、交易个…

MVC配置文件配置及位置

配置文件位置 默认位置 WEB-INF目录下&#xff1a;-servlet.xml 指定位置 在web.xml中配置 DispatcherServlet中的contextConfigLocation属性可以指定配置文件位置 确保配置文件存在于类路径&#xff08;Resources&#xff09;下 web.xml <?xml version"1.0" …

可视化逻辑表达式编辑器

优质博文&#xff1a;IT-BLOG-CN 一、QueryBuilder介绍 QueryBuilder 是一个用于创建查询和过滤器的 UI 组件。 QueryBuilder的特点 1、支持的输入属性丰富&#xff0c;常见的 字符串&#xff0c;整数&#xff0c;浮点数&#xff0c;布尔类型&#xff0c;日期类型&#xff0…

Linux下mysql环境的搭建

1.mysql的下载 去MySQL官网下载mysql的linux压缩包 MySQL :: Download MySQL Community Server 如果下载慢请到网盘中自行下载 通过网盘分享的文件&#xff1a;mysql-8.0.40-1.el7.x86_64.rpm-bundle.tar 链接: https://pan.baidu.com/s/1vUJ-VuTwer1nLPT-haQCqw?pwd6342 提…

基于Qwen2-VL模型针对LaTeX OCR任务进行微调训练 - 多图推理

基于Qwen2-VL模型针对LaTeX OCR任务进行微调训练 - 多图推理 flyfish 基于Qwen2-VL模型针对LaTeX_OCR任务进行微调训练_-_LoRA配置如何写 基于Qwen2-VL模型针对LaTeX_OCR任务进行微调训练_-_单图推理 基于Qwen2-VL模型针对LaTeX_OCR任务进行微调训练_-_原模型_单图推理 基于Q…

图像识别 | Matlab基于卷积神经网络(CNN)的宝可梦识别源程序,GUI界面。附详细的运行说明。

图像识别 | Matlab基于卷积神经网络(CNN)的宝可梦识别源程序&#xff0c;GUI界面。附详细的运行说明。 目录 图像识别 | Matlab基于卷积神经网络(CNN)的宝可梦识别源程序&#xff0c;GUI界面。附详细的运行说明。预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Matlab基…

设置IMX6ULL开发板的网卡IP的两种方法(临时生效和永久有效两种方法)

设置开发板网卡的IP&#xff0c;有两种方法。 方法一&#xff1a;临时生效 第一种方式是临时设置&#xff0c;只有本次有效&#xff0c;重启后又要重新设&#xff0c;命令为&#xff1a; ifconfig eth0 192.168.5.9设置成功后可以使用ifconfig命令来查看已设置的 IP 地址。 …

22. Three.js案例-创建旋转的圆环面

22. Three.js案例-创建旋转的圆环面 实现效果 知识点 WebGLRenderer (WebGL渲染器) THREE.WebGLRenderer 是Three.js中最常用的渲染器&#xff0c;用于将场景渲染到WebGL画布上。 构造器 new THREE.WebGLRenderer(parameters) 参数类型描述parametersObject可选参数对象&…

【D3.js in Action 3 精译_044】5.1 饼图和环形图的创建(四):数据标签的添加

当前内容所在位置&#xff1a; 第五章 饼图布局与堆叠布局 ✔️ 5.1 饼图和环形图的创建 ✔️ 5.1.1 准备阶段&#xff08;一&#xff09;5.1.2 饼图布局生成器&#xff08;二&#xff09;5.1.3 圆弧的绘制&#xff08;三&#xff09; ✔️5.1.4 数据标签的添加&#xff08;四&…

java全栈day13-后端Web实战2

接上述查询部门实现&#xff0c;完成后续要求 一、统一响应结果 1.1步骤 资料如下 对一开始的代码修改如下 结果如下 1.2测试 指定请求方式 结果 小结 二、前后端联调测试 资料如下&#xff1a; (不行&#xff0c;一定要不带空格和不带中文&#xff0c;要不然启动不了试了半天…

vscode 排除文件夹搜索

排除的文件夹 node_modules/,dist/

优雅的@ObservedV2和@Trace装饰器

Hello&#xff0c;大家好&#xff0c;我是 V 哥。在HarmonyOS NEXT开发中&#xff0c;ObservedV2装饰器和Trace装饰器是用于状态管理的两个装饰器&#xff0c;它们在HarmonyOS应用开发中用于增强对类对象中属性的观测能力。如果你学过观察者模式的原理&#xff0c;你会更容易理…

一款免费、简单、快速的JS打印插件,web 打印组件,基于JavaScript开发,支持数据分组,快速分页批量预览,打印,转pdf,移动端,PC端

前言 在数字化办公时代&#xff0c;打印需求呈现多样化和复杂化的趋势。现有的打印软件往往存在cao作繁琐、兼容性差、功能单一等问题&#xff0c;难以满足现代企业高效、灵活的打印需求。 为了解决这些痛点&#xff0c;一款简单、高效、多功能的打印插件成为了迫切需求。 介…

TCP/IP杂记

TCP三次握手、四次挥手 从应用角度&#xff0c;不用多考虑为什么有三次&#xff0c;遵循标准即可。 ubuntu 下 wireshark安装&#xff1a; sudo add-apt-repository universe sudo apt install wireshark 三次握手实证&#xff1a; 第一次握手的情况如下&#xff1a;&#…

Vue前端开发-接收跳转参数

路由携带参数跳转到目标页面后&#xff0c;页面组件可以接收到携带传入的参数&#xff0c;接收的方式与携带的方式相关&#xff0c;如果是采用查询字符串方式携带&#xff0c;那么可以通过路由中的query对象获取到参数&#xff0c;如果是其他方式&#xff0c;通常都是通过路由中…

[ComfyUI]批量生成图片的节点:输入一个prompt列表批量生成图像

文章目录 1.参考资料2.两个节点的部署FizzNodes节点comfyui-mixlab-nodes 生成的结果展示 1.参考资料 如何使用ComfyUI一次批量生成不同内容的图片 ComfyUI工作流】随机提示词批量出图&#xff0c;懒人刷图福音&#xff0c;根据提示 2.两个节点的部署 FizzNodes节点 fizzn…

【实操GPT-SoVits】声音克隆模型图文版教程

项目github地址&#xff1a;https://github.com/RVC-Boss/GPT-SoVITS.git官方教程&#xff1a;https://www.yuque.com/baicaigongchang1145haoyuangong/ib3g1e/tkemqe8vzhadfpeu本文旨在迅速实操GPT-SoVits项目&#xff0c;不阐述技术原理&#xff08;后期如果有时间研究&#…

JAVA (Springboot) i18n国际化语言配置

JAVA i18n国际化语言配置 一、简介二、功能三、Java配置国际化步骤四、Java国际化配置工具类五、Spring Boot配置六、测试 一、简介 在Java中&#xff0c;国际化&#xff08;Internationalization&#xff0c;通常简称为i18n&#xff09;是一个过程&#xff0c;它允许应用程…