C语言如何编写可变参数函数(涉及到二级指针)

2024-05-11 13:18

本文主要是介绍C语言如何编写可变参数函数(涉及到二级指针),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

C语言中函数支持可变参数,常见的可变参数的函数有printf()函数,他的函数原型是_CRTIMP int __cdecl printf(const char *, ...);

可变函数的核心就在,...这里。


既然printf()函数是可变函数,这里就简单模拟printf()函数的功能写个myprintf()函数

#include <iostream>
using namespace std;
//************************************
// Method:    myprintf
// FullName:  myprintf
// Access:    public 
// Returns:   int
// Qualifier:
// Parameter: const char * fmt
// Parameter: ...
//************************************
int myprintf(const char *fmt,...)
{int i = 0;			//记录传入数组fmt的下表
//	int index = 0;			//输出字符串下标int arg = -1;			//参数个数
//	char putfmt[1024];		//输出字符串char *s;			//临时存储参数int ii=0;bool status = true;		//正确标志//	memset(putfmt,0,1024);int length = strlen(fmt)+1;for(i=0;i<length;i++){if(fmt[i]=='%'){i++;int flag = 0;int *ppp;switch(fmt[i]){case 's':if(-1 == arg)arg = 0;flag = 1;ppp=(int *)&fmt+(1+arg);s = (char *)*ppp;ii = 0;while(s[ii])cout<<s[ii++];//	putfmt[index++]=s[ii++];arg++;break;case 'd':flag = 1;ppp=(int *)&fmt+(1+arg);cout<<*ppp;arg++;break;default:break;}if(0 == flag){status = false;break;}}elsecout<<fmt[i];//	putfmt[index++]=fmt[i];}return arg;}
void main(void)
{myprintf("hello %s","oba没有马!");
}


运行结果(图1):


    图1

大致的原理:

第一个形参起重要作用,用来判断后面有几个参数,其中可以用%d来识别后面有个int类型变量,%s识别后面有个字符串变量,以此类推。取的第一个参数后,可通过指针偏移来获取可变参数的地址,继而进行后续的操作。

以本文章中的例子为例,可通过调试的形式来更形象的解释(图2)。


         图2


其中第一个固定参数的地址为0x0043201c,通过查看内存地址可以看到对应的内容。而第二个参数可变参数紧跟在第一个参数对应的内存地址的后面(内存中并非显示正确的字是由于中文为两个字节,oba有三个字节,后面三个字的编码被错开放了,因此显示不一样的字)。那么,要如何正确的在内存中识别中相应的参数呢?

就上面的代码来说,函数中传递了两个参数,都是字符串类型,而字符串类型在传递给函数的过程中传递的是首地址,也可以理解为指针。既然是指针,而且同为一个函数的参数,那在指针的地址(二级指针)上会不会有所关联?

通过&fmt查看其地址(图3)


         图3


由图3可知,&fmt的地址为0x0019feec,在内存中追踪该地址,可看到在内存中,0x0019feec-0x0019feef对应的值为1C 20 43 00,由于在INTEL CPU中,数据的存储形式为小端模式(低地址存地位,不懂可以看我的另一篇博客大端模式,小端模式的区别),因此&fmt里面的值为0x43201C,该值就是fmt的地址。而&fmt后面跟的四个字节,跟&fmt的值有点相像,取其值0x432028查看对应的地址(图4):



    图4


看到了没有!?这个地址里面的存的值就是第二个参数的的内容!

理一下思路:在调用自定义可变参数函数myprintf("hello %s","oba没有马!");时,总共传递了两个参数,一个固定参数"hello %s",另一个传递了可变参数"oba没有马",这两个参数(一级指针)的内容是非连续的(图1,中间有4个字节的00隔开,后面再讨论),但是它们的地址(二级指针)是连续的(图3)。因此可以通过二级指针的偏移来定义可变参数。

在函数myprintf()中,第一个固定参数fmt的类型为char *,取其地址后为0x0019feec,需要偏移4个字节到下一个地址。偏移到0x0019fef0时,此时为二级指针,需进行降级操作,用取值符*降成一级指针,此时指向地址0x00432028,再取里面的值就是要的可变参数的值了。


前面研究的为一个可变参数,如果更多个呢?那在内存中的形式又是怎么样的?下面用三个可变参数作为测试:

修改后的main函数

void main(void)
{myprintf("hello %s %s %s","oba没有马!","5","you");
}
调试看其内存(图5)


图5


由图5可以看出,参数所占的内存大小以4字节对齐,不足4个字节的补全。参数跟参数之间有4个空白字节作为隔断(图5中的红色框部分)。再看它们的地址(二级指针,图6)


图6

图6中,红框部分为四个参数的二级指针!有兴趣的读者可将四个参数对应的指针代入图5中进行验证。


二级指针:

比如说一个字符串指针,char *arr=“hello",其中"hello"存在一个内存空间中,占5个字节,而指针只能存四个字节,因此要用一个指针表示5个地址的字符串,只能通过地址的形式表示,arr就是一级指针,指向"hello"的内存地址的首地址。现在问题来了,形参表中如何才能做到又能保存字符串,又能保存Int,float等其他类型的变量呢?

答案就是用二级指针(图7)。


把main函数中的代码改成:

void main(void)
{myprintf("hello %s %d %s","world",5,"you");
}
调试时结果如图7所示。

注:本文章的重点在于可变参数函数的研究,不在于printf()的研究,因此printf()只是简单的实现!


这篇关于C语言如何编写可变参数函数(涉及到二级指针)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/979644

相关文章

Go语言中make和new的区别及说明

《Go语言中make和new的区别及说明》:本文主要介绍Go语言中make和new的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 概述2 new 函数2.1 功能2.2 语法2.3 初始化案例3 make 函数3.1 功能3.2 语法3.3 初始化

MySQL 中的 CAST 函数详解及常见用法

《MySQL中的CAST函数详解及常见用法》CAST函数是MySQL中用于数据类型转换的重要函数,它允许你将一个值从一种数据类型转换为另一种数据类型,本文给大家介绍MySQL中的CAST... 目录mysql 中的 CAST 函数详解一、基本语法二、支持的数据类型三、常见用法示例1. 字符串转数字2. 数字

Python内置函数之classmethod函数使用详解

《Python内置函数之classmethod函数使用详解》:本文主要介绍Python内置函数之classmethod函数使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录1. 类方法定义与基本语法2. 类方法 vs 实例方法 vs 静态方法3. 核心特性与用法(1编程客

Python函数作用域示例详解

《Python函数作用域示例详解》本文介绍了Python中的LEGB作用域规则,详细解析了变量查找的四个层级,通过具体代码示例,展示了各层级的变量访问规则和特性,对python函数作用域相关知识感兴趣... 目录一、LEGB 规则二、作用域实例2.1 局部作用域(Local)2.2 闭包作用域(Enclos

Java内存分配与JVM参数详解(推荐)

《Java内存分配与JVM参数详解(推荐)》本文详解JVM内存结构与参数调整,涵盖堆分代、元空间、GC选择及优化策略,帮助开发者提升性能、避免内存泄漏,本文给大家介绍Java内存分配与JVM参数详解,... 目录引言JVM内存结构JVM参数概述堆内存分配年轻代与老年代调整堆内存大小调整年轻代与老年代比例元空

MySQL count()聚合函数详解

《MySQLcount()聚合函数详解》MySQL中的COUNT()函数,它是SQL中最常用的聚合函数之一,用于计算表中符合特定条件的行数,本文给大家介绍MySQLcount()聚合函数,感兴趣的朋... 目录核心功能语法形式重要特性与行为如何选择使用哪种形式?总结深入剖析一下 mysql 中的 COUNT

Go语言中nil判断的注意事项(最新推荐)

《Go语言中nil判断的注意事项(最新推荐)》本文给大家介绍Go语言中nil判断的注意事项,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1.接口变量的特殊行为2.nil的合法类型3.nil值的实用行为4.自定义类型与nil5.反射判断nil6.函数返回的

Go语言数据库编程GORM 的基本使用详解

《Go语言数据库编程GORM的基本使用详解》GORM是Go语言流行的ORM框架,封装database/sql,支持自动迁移、关联、事务等,提供CRUD、条件查询、钩子函数、日志等功能,简化数据库操作... 目录一、安装与初始化1. 安装 GORM 及数据库驱动2. 建立数据库连接二、定义模型结构体三、自动迁

MySQL 中 ROW_NUMBER() 函数最佳实践

《MySQL中ROW_NUMBER()函数最佳实践》MySQL中ROW_NUMBER()函数,作为窗口函数为每行分配唯一连续序号,区别于RANK()和DENSE_RANK(),特别适合分页、去重... 目录mysql 中 ROW_NUMBER() 函数详解一、基础语法二、核心特点三、典型应用场景1. 数据分

MySQL数据库的内嵌函数和联合查询实例代码

《MySQL数据库的内嵌函数和联合查询实例代码》联合查询是一种将多个查询结果组合在一起的方法,通常使用UNION、UNIONALL、INTERSECT和EXCEPT关键字,下面:本文主要介绍MyS... 目录一.数据库的内嵌函数1.1聚合函数COUNT([DISTINCT] expr)SUM([DISTIN