• C语言中可变参数函数的实现——printf


    C语言中可变参数函数的实现(printf()的实现)

    1. 可变参数的实现

    1.1 参数的地址如何获取

    1.1.1 参数地址观察👀

    首先看一下程序里面的参数是怎样存储的,看下面这段程序⬇️

    我们在函数print_v_address中使用三个参数v0, v1, v2,并分别输出三个参数的地址。

    void print_v_address(int v0, int v1, int v2) {
        printf("v0_addr = %p\n", &v0);
        printf("v1_addr = %p\n", &v1);
        printf("v2_addr = %p\n", &v2);
    }
    
    
    print_v_address(0, 1000, 1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    当我们得到的输出为⬇️

    v0_addr = 0x16b34b32c
    v1_addr = 0x16b34b328
    v2_addr = 0x16b34b324
    
    • 1
    • 2
    • 3

    可以发现输出地址是连续的,并且是递减的,则上述程序和输出可以抽象为⬇️

    image-20220907154736443

    从图中可以看出,我们的函数变量是存在栈上的。

    ——因为地址是递减的,再加上我们的知识储备(其实一般来说前8个变量是存在栈上的),可以确定这一点⬆️。

    1.1.2 参数地址获取

    根据1.1.1中的观察,我们可以通过第一个参数的地址来获取后续参数的地址⬇️

    void get_values(int v0, int v1, int v2) {
    
        uint64_t v0_p, v1_p, v2_p;
    
        v0_p = (uint64_t)(&v0);
        v1_p = v0_p - sizeof(v0);
        v2_p = v1_p - sizeof(v1);
    
        printf("v0 = %d\n", *((int*)v0_p));
        printf("v1 = %d\n", *((int*)v1_p));
        printf("v2 = %d\n", *((int*)v2_p));
    }
    
    // get_values(0, 100, 1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    上述函数的主要功能就是,将v0地址取出来,并且根据v0类型,获取到v1地址(使用v0地址减去v0的大小),v2地址同理也可以计算得到。

    调用上述函数的输出如下⬇️,可以看到我们已经根据v0地址,还有参数个数和类型,正确获取到了所有变量v0,v1,v2的内容。

    v0 = 0
    v1 = 100
    v2 = 1
    
    • 1
    • 2
    • 3

    1.2 参数的个数和类型如何确定?——尝试自己写一个printf?

    1.2.1 printf的传入参数怎么写?(参数个数和类型如何确定?)

    xv6系统(一个类Unix系统)中的printf定义如下⬇️

    void
    printf(char *fmt, ...)
    {
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这个fmt是什么意思呢?

    ——平时我们使用的printf如下⬇️,从这里大致就能看出来,printf的第一个传入参数应该是一个字符串,里面会包含一些%d,%p,%s…等信息。因此上面的printf的实现中char *fmt是什么就显而易见了(就是形如"a = %d, str = %s, ptr = %p"的字符串)。

    printf("a = %d, str = %s, ptr = %p", a, str, p);
    
    • 1

    重新解释一下printf中的可变参数实现:

    其实就是通过char *fmt中包含的%d,%p,%s … 来识别传入参数的个数和数量的。

    1.2.3 自己动手实现一个传入参数个数和类型的函数⬇️

    // 仅支持%d和%s输出
    void my_first_printf (char *fmt, ...) {
        char *cur_p = fmt;
        char* cur_v_p = (char*)&fmt;
    
        printf("%s\n", *((char* *)cur_v_p));
        char *p = (char *)&fmt;
        p -= 16;
        printf("%s\n", *((char* *)p));
    
        char c = 0;
    
        for (int i = 0; (c = fmt[i]) != '\0'; ++i) {
            if (c != '%') {
                printf("%c", c);
                continue;
            }
    
            if (c == '%') {
                char c_next = fmt[++i];
                switch(c_next) {
                    case 'd': 
                        cur_v_p -= sizeof(int);
                        printf("%d", *((int *)cur_v_p));
                        fflush(stdout);
                        break;
                    case 's':
                        cur_v_p -= sizeof(char*);
                        printf("%s", *((char* *)cur_v_p));
                        fflush(stdout);
                        break;
                }
            }
        }
    
    }
    
    int main() {
      int a = 100000;
    	printf("helo. a = %d, str = %s\n", a, "aloha");
      exit(0);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    这里仅作演示,其中fflush是为了flush输出缓冲区,防止内容不能正确输出。

    输出结果为⬇️

    helo. a = 100000, str = aloha
    
    • 1

    可以发现上面的内容可以做进一步优化,即,地址计算和变量输出每次都用指针操作有点麻烦了。其实真正的printf中是使用va_list宏来实现的。

    PS: 为什么只写了%d和%s的识别?

    ——不是因为我懒,多种类型组合起来,我暂时识别不了——因为具体怎么存的,要问编译器,当参数类型比较多的时候,在不清楚编译器对变量的存储规则之前,我们是很难准确找到每个参数的地址的。

    ——所以更要使用va_list宏了,因为它是知道编译器的对参数的存储规则的。

    2. va_list

    主要包含以下几个宏⬇️(这不是我的机器里面的,是从别处找到的,我的机器上只能使用宏,但是看不到它的具体定义。)

    #define va_list char*   /* 可变参数地址 */
    #define va_start(ap, x) ap=(char*)&x+sizeof(x) /* 初始化指针指向第一个可变参数 */
    #define va_arg(ap, t)   (ap-=sizeof(t),*((t*)(ap-sizeof(t)))) /* 取得参数值,同时移动指针指向后续参数 */
    #define va_end(ap)  ap=0 /* 结束参数处理 */
    
    • 1
    • 2
    • 3
    • 4

    其实内容也很简单,就是实现我们的地址计算的功能。

    但是这里需要注意的是:我们的地址计算并不是通用的,这里的地址是由编译器的特点决定的,因此需要首先确定编译器的类型和CPU的型号,才能确定地址是如何计算的(比如,博文 中,地址就是递增的,而不是像我们这样递减操作,此外我在实验过程中,还发现了一些奇怪的现象:比如地址并不是 -= sizeof(char *),而是减了12,我猜测可能和对齐有关,总之,定义va_list的时候一定要确定编译器是如何分配地址的才行)。

    3.实际上的 printf()实现⬇️

    下面是摘自 xv6内核中的printf函数实现⬇️。

    // Print to the console. only understands %d, %x, %p, %s.
    void
    printf(char *fmt, ...)
    {
      va_list ap;
      int i, c, locking;
      char *s;
    
      locking = pr.locking;	
      if(locking)
        acquire(&pr.lock);
    
      if (fmt == 0)
        panic("null fmt");
    
      va_start(ap, fmt);
      for(i = 0; (c = fmt[i] & 0xff) != 0; i++){
        if(c != '%'){
          consputc(c);
          continue;
        }
        c = fmt[++i] & 0xff;
        if(c == 0)
          break;
        switch(c){
        case 'd':
          printint(va_arg(ap, int), 10, 1);
          break;
        case 'x':
          printint(va_arg(ap, int), 16, 1);
          break;
        case 'p':
          printptr(va_arg(ap, uint64));
          break;
        case 's':
          if((s = va_arg(ap, char*)) == 0)
            s = "(null)";
          for(; *s; s++)
            consputc(*s);
          break;
        case '%':
          consputc('%');
          break;
        default:
          // Print unknown % sequence to draw attention.
          consputc('%');
          consputc(c);
          break;
        }
      }
    
      if(locking)
        release(&pr.lock);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    可以看到,这里和我们实现的区别主要有:

    • 加锁;——这是显然的,防止其他进程 / 线程打断,导致printf输出一半;
    • 使用constputc来输出内容;——原因同样显而易见,我们在定义printf,肯定只能使用更加底层的函数调用。——其实再往里是调用了uartputc,即往uart设备中输出一个字符串,uart就是我们的console设备的名称。
    • 不同的类型,有不同的print函数,比如printint, printptr等,其实里面还是使用uartputc,并且做了一些取余操作,以便以正确的格式输出(正确的进制)。
  • 相关阅读:
    【排序专题】不会吧,不会吧居然还有人不懂排序算法?有彩蛋哦
    如何避免重复创建线程?创建线程池的方式有哪些?各自优缺点有哪些?
    LeetCode --- 1539. Kth Missing Positive Number 解题报告
    SpringCloud微服务(十一)——Sentinel服务熔断限流
    《HelloGitHub》第 87 期
    【每日一题】买卖股票的最佳时机 III
    csapp attack lab phase3
    mdkarm怎么编程:深度探索与实用指南
    PHP-函数(定义,带缺省值,值传递、地址传递)php跨越+移植(include require include_once require_once)
    移动app抓包工具——fiddler抓包指南
  • 原文地址:https://blog.csdn.net/ahundredmile/article/details/126749769