IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C语言中可变参数函数的实现——printf -> 正文阅读

[C++知识库]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);

当我们得到的输出为??

v0_addr = 0x16b34b32c
v1_addr = 0x16b34b328
v2_addr = 0x16b34b324

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

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);

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

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

v0 = 0
v1 = 100
v2 = 1

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

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

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

void
printf(char *fmt, ...)
{
	...
}

这个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);

重新解释一下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);
}

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

输出结果为??

helo. a = 100000, str = aloha

可以发现上面的内容可以做进一步优化,即,地址计算和变量输出每次都用指针操作有点麻烦了。其实真正的printf中是使用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 /* 结束参数处理 */

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

但是这里需要注意的是:我们的地址计算并不是通用的,这里的地址是由编译器的特点决定的,因此需要首先确定编译器的类型和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);
}

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

  • 加锁;——这是显然的,防止其他进程 / 线程打断,导致printf输出一半;
  • 使用constputc来输出内容;——原因同样显而易见,我们在定义printf,肯定只能使用更加底层的函数调用。——其实再往里是调用了uartputc,即往uart设备中输出一个字符串,uart就是我们的console设备的名称。
  • 不同的类型,有不同的print函数,比如printint, printptr等,其实里面还是使用uartputc,并且做了一些取余操作,以便以正确的格式输出(正确的进制)。
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-13 10:54:41  更:2022-09-13 10:59:16 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 14:55:52-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码