watermelon 发表于 2018-7-7 16:35:38

自制printf函数

本帖最后由 溯影 于 2018-7-7 16:39 编辑

小弟最近看到了WindowsAPI中一个API是WriteConsole,书上讲利用这个函数可以自己来写一个printf,因为书上是这么说的:“实际上,类似于printf等标准C函数在Windows系统中都是通过系统的动态链接库crtdll.dll导出,printf函数的实现程序也位于crtdll.dll中,分析printf等函数的实现代码可以发现,在Windows平台上,实际printf函数在做了格式化字符串的处理后,是调用WriteConsole等API来进行界面操作的”-----《精通WindowsAPI》 人民邮电出版社 范文庆等著 P231语

那我想,我也要自己动手写一个printf函数。

printf函数中有一个地方之前一直困扰着我,就是他那个不定的参数问题,printf可以用来打印字符串,打印格式字符,例如printf("哈哈,你好,我是%s\n","周西瓜"); 这个语句中逗号运算符右边的参数是可以改变的,个数也是不确定的,我已开始想用python来写这个函数,因为python中有一个可以def函数中*params(可变长参数),但是心里总是感觉怪怪的。

不过通过网上来查找资料,我终于找到了方法,C语言中可变参数函数是通过栈来进行实现的,正常情况下C的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈。我们只要之前有一个确定的参数的地址,那么通过变量的字节数来进行偏移量的确定最终就可以实现确定所有可变参数,例如我们要写一个可变参数函数void Test(const char *fmt,...){........},根据上一句画,最右边的参数先入栈,那么const char *fmt我们是有一个确定的参数,我们可以用fmt这个参数的地址来进行偏移量的计算,最终确定所有的参数。
但是C库帮我们做好了这个可变参数的一系列函数,我们可以用stdarg.h这个头文件中的函数来进行可变参数的确定,不用我们自己再用sizeof和地址来计算偏移量了。

这里我推荐一篇博客,我的知识盲区是在这篇博客中得到填补的:http://www.cnblogs.com/cpoint/p/3368993.html

把最恼火的printf中可变参数的问题解决完毕以后就轻松多了。我们用的WriteConsole函数定义如下:
BOOL WINAPI WriteConsole(
        in        HANDLE hConsoleOutput,                        //控制台句柄,应该为标准输出句柄
        in        const VOID* lpBuffer,                        //要输出内容的指针
        in        DWORD nNumberOfCharsToWrite,                //需要输入字符的数量
        out        LPVOID lpNumberOfCharsWritten,                //实际输入的字符的数量
        LPVOID lpReversed                                //保留参数,设置为NULL
);

获取控制台标准输出句柄用GetStdHandle函数

下面的这个程序中我们不引入头文件stdio.h,不使用printf函数来打印,用WriteConsole函数来进行打印结果,不使用与printf功能相似的函数或者相关的函数,例如不使用wsprintf等函数

#include <stdlib.h>
#include <stdarg.h>                                                                //用于可变参数
#include <windows.h>


VOID DoubleToString(DOUBLE number, CHAR *result);        //浮点数转化为字符串的函数说明

//自定义的SelfPrintf函数,模仿printf
VOID SelfPrintf(CHAR *fmt, ...)
{
        //为可变参数做准备
        va_list va_Ptr;
        va_start(va_Ptr, fmt);


        HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);                        //获取控制台的标准输出句柄
        if (hConsole == INVALID_HANDLE_VALUE || hConsole == NULL)
        {
                MessageBox(NULL, "GetStdHandle出错", "Console Error", MB_OK);
                return;
        }

        CHAR *fmtPtr = fmt;                                                        //指向fmt的指针
        for (; *fmtPtr; )                                                        //开始遍历字符串
        {
                if (*fmtPtr == '%' && *(fmtPtr + 1))
                {
                        switch (*(fmtPtr + 1))
                        {
                        case 'd':
                        {
                                CHAR szTemp;
                                itoa(va_arg(va_Ptr, INT), szTemp, 10);                //将那个位置上的整数转化为字符串       

                                //用WriteConsole进行控制台输出
                                WriteConsole(hConsole,                                //控制台句柄
                                        szTemp,
                                        lstrlen(szTemp),
                                        NULL,
                                        NULL);
                                break;
                        }
                        case 'c':
                        {
                                WriteConsole(hConsole,
                                        &va_arg(va_Ptr, CHAR),                        //打印当前位置上的字符
                                        1,
                                        NULL,
                                        NULL);
                                break;
                        }
                        case 's':
                        {
                                CHAR *szTemp = va_arg(va_Ptr, CHAR*);

                                WriteConsole(hConsole,
                                        szTemp,                                        //打印当前参数位置上的字符串
                                        lstrlen(szTemp),
                                        NULL,
                                        NULL);
                                break;
                        }
                        case '%':
                        {
                                CHAR c = '%';
                                WriteConsole(hConsole,
                                        &c,
                                        1,
                                        NULL,
                                        NULL);
                                break;
                        }
                        case 'f':
                        {
                                CHAR result;
                                DoubleToString(va_arg(va_Ptr, double), result);        //将浮点数转化为字符串
                                WriteConsole(hConsole,
                                        result,
                                        lstrlen(result),
                                        NULL,
                                        NULL);
                                break;
                        }
                        }
                        fmtPtr += 2;                                                //指针偏移两个字符,跳过%和字母
                       
                }
                //不是格式字符,就按照平常的方式打印出来就好了
                else
                {
                        WriteConsole(hConsole,
                                fmtPtr,
                                1,
                                NULL,
                                NULL);
                        fmtPtr++;                                                //指针变量自增1
                }
        }
        CloseHandle(hConsole);                                                        //关闭句柄
}

       

//浮点数转化为字符串
VOID DoubleToString(DOUBLE number, CHAR *result)
{
        CHAR left;                                                                //小数点左边整数部分
        itoa((INT)number, left, 10);
        lstrcpy(result, left);
        lstrcat(result, ".");                                                        //拼接上小数点


        number = number - (INT)number;                                                //取小数部分
        while (number)
        {
                number = number * 10;
                CHAR temp;
                itoa((INT)number, temp, 10);
                lstrcat(result, temp);
                number = number - (INT)number;
        }

}
                               

int main(void)
{
        INT a = 1;
        DOUBLE b = 1.23456;
        CHAR c = 'K';
        CHAR d = "Hello World!";
        SelfPrintf("%%哈哈,这是测试实例.\n* %d\n* %f\n* %c\n* %s\n%%测试完毕\n", a, b, c, d);
        return 0;
}







运行结果:
%哈哈,这是测试实例.
* 1
* 1.2345600000000001017497197608463466167449951171875
* K
* Hello World!
%测试完毕
请按任意键继续. . .

唐凌 发表于 2018-7-7 22:24:19

赞一个!
不过printf是开源的,VC的目录里有源码的。
int __cdecl printf (
      const char *format,
      ...
      )
/*
* stdout 'PRINT', 'F'ormatted
*/
{
    va_list arglist;
    int buffing;
    int retval;

    _VALIDATE_RETURN( (format != NULL), EINVAL, -1);

    va_start(arglist, format);

    _lock_str2(1, stdout);
    __try {
      buffing = _stbuf(stdout);

      retval = _output_l(stdout,format,NULL,arglist);

      _ftbuf(buffing, stdout);

    }
    __finally {
      _unlock_str2(1, stdout);
    }

    return(retval);
}

watermelon 发表于 2018-7-7 23:05:51

tangptr@126.com 发表于 2018-7-7 22:24
赞一个!
不过printf是开源的,VC的目录里有源码的。
int __cdecl printf (


感谢tangptr大佬指导,学习了:D

Golden Blonde 发表于 2018-7-8 03:28:24

printf是C库函数,几乎所有平台的C编译器都支持使用这个函数,除了极少数单片机平台。

WriteConsole是WIN32API,只支持在WINDOWS上使用。

个人还是比较喜欢使用C库函数,跨平台的时候几乎不用改什么代码。

watermelon 发表于 2018-7-8 09:26:15

美俪女神 发表于 2018-7-8 03:28
printf是C库函数,几乎所有平台的C编译器都支持使用这个函数,除了极少数单片机平台。

WriteConsole是WIN3 ...

哦哦,的确是的

0xAA55 发表于 2018-7-8 12:24:02

正常情况下C的函数参数入栈规则为_cdecl,而非_stdcall。

watermelon 发表于 2018-7-8 16:01:57

0xAA55 发表于 2018-7-8 12:24
正常情况下C的函数参数入栈规则为_cdecl,而非_stdcall。

刚刚百度百科并且在vs上项目属性里看了一下,调用方式果然是默认的_cdecl,_stdcall是可以自动清栈,一般用于Win32API里,并且百科上说可变参数的函数调用方式必须是_cdecl(因为是手动清栈),但是我在可变参数的函数前面改成_stdcall也没有问题可以运行的啊。不知道我哪里理解有错误了。

0xAA55 发表于 2018-7-8 16:23:26

溯影 发表于 2018-7-8 16:01
刚刚百度百科并且在vs上项目属性里看了一下,调用方式果然是默认的_cdecl,_stdcall是可以自动清栈,一般 ...

_stdcall是被调用者清栈,但是这是不科学的,事实上_stdcall的参数个数是钦定的,并不能像_cdecl那样可以让调用者决定传几个参数过去。毕竟,从各种方面上来讲,强行用_stdcall实现随便压栈传随意个数参数的方式不仅不安全,而且效率也不行(这里的效率指的是通过强行修改机器码来改变ret xx指令的xx值,这会影响牙膏厂指令预读取与缓存等)

watermelon 发表于 2018-7-8 16:34:20

0xAA55 发表于 2018-7-8 16:23
_stdcall是被调用者清栈,但是这是不科学的,事实上_stdcall的参数个数是钦定的,并不能像_cdecl那样可以 ...

哦哦了解了,感谢站长指导:D
页: [1]
查看完整版本: 自制printf函数