RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 628946
Accepted
MaximPro
MaximPro
Asked:2020-02-17 08:50:53 +0000 UTC2020-02-17 08:50:53 +0000 UTC 2020-02-17 08:50:53 +0000 UTC

printf 作为在 C 中打印变量的一种方式

  • 772

我不知道如何准确地表达我想问的问题,但它看起来像这样:

  1. 如何在C中打印变量的内容:

    char msg = 'k';
    printf("%c", msg);
    
  2. 如何在C++中打印变量的内容:

    char msg = 'k';
    cout << msg;
    

现在的问题是:

  1. 在C++中,您不需要为输出指定修饰符,那么为什么C中没有类似物cout(我知道该语言比C++古老得多,但仍然如此)?

  2. 他们经常写信给我说printf需要转换类型:

    char msg = 'k';
    printf("%f", (float)msg);
    

做什么的?在C++的类比中,您不必这样做!为什么我不能这样写:printf("%f", msg);?是什么原因?printf它不会自己将数据引导到我们指定的修饰符吗?

c++
  • 3 3 个回答
  • 10 Views

3 个回答

  • Voted
  1. Best Answer
    jfs
    2020-02-17T15:15:09Z2020-02-17T15:15:09Z

    在 C 语言中,无论变量的类型和值如何,printf(format, msg)在源代码中它都会调用相同的函数msg。C 是一种静态类型语言,这意味着在printf 执行过程中,函数不知道msg它是什么char,而且 printf 甚至不知道它传递了多少个参数。因此,您必须format在该行中手动设置所需的表示形式:从它所在的内存中加载什么类型的值以及如何格式化该值,以便将结果字节写入stdout 。msg

    在C++中,调用不同的函数(运算符重载)可以cout << msg针对不同的类型msg进行编译。您可以轻松定义自己的 operator ,例如,输出向量以进行调试:<<<<

    #include <iostream>
    #include <vector>
    
    template<class T>
    std::ostream& operator << (std::ostream& os, const std::vector<T>& v) {
      for (auto&& x : v)
        os << x << ' ';
      return os;
    }
    
    int main()
    {
      std::vector<int> v {1, 2, 3};
      std::cout << v << '\n';
    }
    

    例子:

    $ g++ -std=c++11     main.cc   -o main
    $ ./main
    1 2 3 
    

    简化一下,您可以想象如果这是, , 如果这是, , 如果这是等,编译器会cout << msg生成一个调用。在这里,每个函数都知道它接受什么类型,如果不使用 iomanip,则每种类型使用一个默认字节表示)。例如,默认情况下显示它的十进制数字(而不是十六进制或其他)。print_char(msg)msgcharprint_float(msg)msgfloatprint_vector(msg)msgvectorint

    C11引入了_Generic,它允许根据要调用的参数 ( ) 的类型来调用不同的函数controlling-expression,因此您可以定义print_arg(arg),这将适用于不同的类型arg(单个参数)。

    为什么我不能这样写:printf("%f", msg);?是什么原因?printf 不会自己将数据转换为我们指定的修饰符吗?
    ... printf("%f", (float)msg); \\107.000000和printf("%f", msg);\\0.000000为什么答案不同?

    当调用参数中用省略号 ( ) 声明的 printf 之类的可变参数函数时,可以采用编译器未知的...预期类型的​​不同数量的参数),默认转换发生:隐式转换为(整数提升)或如果在给定的平台(异国情调)上无法表示价值。类似地,float 在调用时会转换为 double -因此in 不需要它,而只是简单地使用它。formatcharintunsigned intcharintprintf()%lfdoubleprintf()%f

    对于这种情况,一个简单的模型可以与 printf 一起使用:编译器将参数(int、double 等)放入内存区域,然后 printf 根据行中的说明从那里读取它们format- printf 充当迷你 - computer: format 指定程序,printf读取va_arg存储其参数的内存 ( )格式化它们并将结果字节写入stdout.

    (double)msg对象在内存中可以与(int)msg对象不同。因此,printf 可以在内存中看到不同的位模式,因此,结果printf("%f", (int)msg)可以printf("%f", (double)msg)不同(相同的指令:%f应用于内存中的不同内容)。

    我的机器的示例(内存中的类型可能取决于平台(操作系统 + 处理器)和编译器选项)。我将以十六进制转储的形式显示内存中的字节(例如:6B 16 == 107 10)。

    对于printf("%c", msg):

    • 'k'在 C 中有类型int -6B 00 00 00
    • char msg —6B
    • 在 printf 中它被传递为int -6B 00 00 00
    • %c获取此参数6B 00 00 00并将其转换为unsigned char( 6B) 并将相应的字节 ( 6B) 打印到标准输出。有关格式,请参阅printf 文档c。

    对于printf("%f", (float)msg):

    • msg( 6B) 转换为(float)msg —00 00 D6 42
    • 传递给 printf 作为double -00 00 00 00 00 C0 5A 40
    • %f将内存解释00 00 00 00 00 C0 5A 40为浮点数并以固定格式(6小数位)输出:(107.000000点的字符可能因语言环境而异)。ascii编码对应的字节,写入stdout:31 30 37 2e 30 30 30 30 30 30

    对于printf("%f", msg):

    • msg(char - ) 作为 -6B传递给 printfint6B 00 00 00
    • %f将内存解释6B 00 00 00 XX XX XX XX为浮点数5.3e-322(if XX == 00) 并输出0.000000

    由于格式不正确,行为是未定义的(未定义行为 - UB),它printf("%f", msg)可以做任何事情,甚至可以发射火箭。在我的机器上,结果printf("%f", msg)取决于前面的代码,例如:

    char c = 'k';
    printf("%f\n", (float)c);
    printf("%f\n", c);
    

    印刷:

    107.000000
    107.000000 # XX XX XX XX == 00 C0 5A 40 (остатки от предыдущего вызова)
    

    但:

    printf("%f\n", 5.3e-322);
    printf("%f\n", c);
    

    印刷:

    0.000000
    0.000000  # XX XX XX XX == 00 00 00 00 (остатки от предыдущего вызова)
    

    确保对于编译时已知的格式(例如"%f\n"),您的编译器会针对无效类型生成警告 - 应避免使用 UB。

    printf("%i", (int)msg); \\\ 107 и printf("%i", msg);

    在这两种情况下char 都作为( )msg传递并且具有相同的格式,结果应该是相同的 (6B 16 == 107 10 )。int6B 00 00 00

    您可以使用 C 代码在您的机器上查看变量的内容:

    #include <stdio.h>
    
    static void print_memory(unsigned char *memory, size_t n)
    {
      for (unsigned char *p = memory; p != memory + n; ++p)
        printf("%02X ", *p);
      puts("");
    }
    
    
    int main(void)
    {
      char c = 'k';
      print_memory((unsigned char *)&c, sizeof c);
      int i = c;
      print_memory((unsigned char *)&i, sizeof i);
      float f = c;
      print_memory((unsigned char *)&f, sizeof f);
      double d = f;
      print_memory((unsigned char *)&d, sizeof d);
    }
    

    例子:

    $ gcc -std=c99     main.c   -o main
    $ ./main
    6B 
    6B 00 00 00 
    00 00 D6 42 
    00 00 00 00 00 C0 5A 40 
    

    记忆中的双重可能是什么样的

    适用于以下答案的示例107.0:IEEE 754 双精度数表示为
    d = ±sign (1 + mantissa / 2 52 ) 2nd order − 1023

    符号、尾数和指数以二进制表示形式打包为:

     00 00 00 00 00 C0 5A 40 // little-endian (8 byte as hex)
     40 5A C0 00 00 00 00 00 // big-endian
     0100000001011010110000000000000000000000000000000000000000000000 # 64-bit
     ^
     |-самый левый бит знак=
     0 (положительный)
    
      ^         ^
      |---------|
    Затем 11 бит порядок=
    0b10000000101 (==1029)
                 ^                                                  ^
                 |--------------------------------------------------|
    Оставшиеся 52 бита манитисса=
               0b1010110000000000000000000000000000000000000000000000
      = 3025855999639552
      = 0xac00000000000
    

    一起:

    d = +(1 + 3025855999639552 / 2 52 ) * 2 (1029 - 1023)
    = 64 + 3025855999639552 / 70368744177664
    = (4503599627370496 + 3025855999639552) / 70368744177664
    = 7529455627010048 / 70368744177664
    = 107.0

    这演示了为什么107.0它可以在内存中表示为00 00 00 00 00 C0 5A 40.

    精简 printf 函数的示例

    #include <stdarg.h> // va_list, va_arg()
    
    // emulate some printf() functionality using writec()
    static void print(const char* format, ...)
    {
      va_list args;
      va_start(args, format);
      int infmt = 0, is_char = 0;
      union
      {
        int i;
        double f;
      } arg;
      char buffer[23] = {0}; // enough for %c, %d, %f
    
      for (const char *p = format; *p; ++p) {
        if (infmt) { // print arg
          infmt = 0;
          switch(*p) {
          case '%': // "%%": print % literally
            writec(*p);
          break;
          case 'c': // "%c": print char
            is_char = 1;
            // fall through
          case 'd': // "%d": print int
          case 'i': // "%i": print int
            arg.i = va_arg(args, int); // load int arg
            if (is_char) {
              is_char = 0;
              writec((unsigned char)arg.i); // format as char, write
            } else {
              itoa(arg.i, buffer, sizeof buffer); // format as int
              for (char *pb = buffer; *pb; ++pb) writec(*pb); // write
            }
            break;
          case 'f':
            arg.f = va_arg(args, double); // load double arg
            ftoa(arg.f, buffer, sizeof buffer); // format as floating point
            for (char *pb = buffer; *pb; ++pb) writec(*pb); // write
            break;
          default:
            arg.i = va_arg(args, int); // load int arg
            scpy(buffer, "<unknown, load as int>");
            for (char *pb = buffer; *pb; ++pb) writec(*pb); // write
          };
        } else if (*p != '%') { // print literally
          writec(*p);
        } else { // *p == '%'
          infmt = 1;
        }
      }
      va_end(args);
    }
    
    • switch用于识别%d格式字符串中的转换说明符 ( )
    • va_arg()加载所需类型的参数
    • 辅助函数itoa()和ftoa()格式分别为 int 和 double
    • writec()写一个字节到stdout.

    这个定义print()对于代码来说已经足够了:

    int main(void)
    {
      char msg = 'k';
      print("%c %i %i %f %x\n", msg, msg, (int)msg, (float)msg, msg);
      print("%c ", msg);
      print("%i ", msg);
      print("%i ", (int)msg);
      print("%f\n", (float)msg);
      print("%f\n", msg); // XXX UB
    }
    

    例子:

    $ cc -std=c99 print-example.c -o print-example
    $ ./print-example
    k 107 107 107.000000 <unknown, load as int>
    k 107 107 107.000000
    107.000000
    

    要编译,定义辅助函数就足够了(print()必须在定义之前插入):

    #include <unistd.h> // POSIX write()
    
    static void writec(unsigned char c)
    {
      write(1, &c, 1);
    }
    
    static void scpy(char* dest, const char* src)
    {
      while (*dest++ = *src++);
    }
    
    static void ftoa(double d, char* buffer, int n)
    {
      if (d == 107) //XXX
        scpy(buffer, "107.000000");
      else
        scpy(buffer, "XXX");
    }
    
    /// format positive int as decimal ascii digits
    static char* utoa_rec(unsigned i, char* buffer, int *pn)
    {
      if (i >= 10)
        buffer = utoa_rec(i / 10, buffer, pn);
      if ((*pn)-- > 0)
        *buffer++ = '0' + (i % 10);
      return buffer;
    }
    
    static void itoa(int i, char* buffer, int n)
    {
      if (i < 0) {
        i = -i; //XXX ignore INT_MIN
        if (n-- > 0)
          *buffer++ = '-'; // sign
      }
      buffer = utoa_rec(i, buffer, &n);
      if (n > 0)
        *buffer++ = '\0';
    }
    

    提供了函数定义以便示例可以运行,但实际上它们只是存根(不用于重用),仅用于演示最简单的print(format, ...)实现之一。

    这是glibc的完整实现示例vfprintf()。

    • 33
  2. AnT stands with Russia
    2020-02-17T10:27:13Z2020-02-17T10:27:13Z

    为什么 C 中没有模拟 cout

    C++ 语言中的功能与库级别的函数重载cout机制密切相关,即 事实上,基于库(和用户)可用的函数重载机制的存在,以及伴随的运算符重载机制。重载机制根据对您指定的参数类型的分析来选择输出函数的正确版本。

    C在用户或库级别没有函数重载和运算符重载的机制。因此,不存在这种表面上“类型无关”的 I/O 操作。

    C 标准的 C11 版本引入了一种通用表达式机制,可用于模拟自定义/库函数重载。例如,这种机制在标准头文件中使用(可以<tgmath.h>使用)以实现“重载”数学宏函数。

    但是标准库中没有实现“类型无关”的 I/O 函数。如果你愿意,你可以使用这个新机制并尝试自己实现它们。

    我经常写到printf中的类型需要强制转换。做什么的?printf 不会自己将数据转换为我们指定的修饰符吗?

    printf就是所谓的可变参数函数。这个函数的所有参数,除了第一个,都是可变参数。...它们在函数参数列表的声明中匹配

    int printf( const char* format, ... );
    

    此类参数通过特殊的参数传递机制传递。它的特点是函数本身printf对实际传递的参数类型一无所知,因此,没有任何东西可以独立地得出正确的类型。

    从纯实用的角度来看,您可以认为所有可变参数都写入连续的二进制流。在内部,该函数printf将使用va_list/va_start/va_arg. 到底向这个流中写入了什么,函数printf本身无法知道。因此,它会根据您自己从外部传输给它的格式将此二进制流解析为多个部分。如果您以这种格式对她“撒谎”,那么她自己会在不怀疑任何事情的情况下错误地解析这个二进制流。因此,您放入此二进制流中的所有数据都必须与您在格式字符串中指定的格式说明符完全匹配。

    实现不需要以这种方式实现传递可变参数,但该模型非常准确地说明了传递可变参数的细节。

    • 18
  3. Ильдар Хайруллин
    2020-02-17T09:23:52Z2020-02-17T09:23:52Z
    1. 要工作cout,运算符重载是必要<<的,并且对于每种类型它都有自己的类型,因此不需要修饰符。在 C 中,运算符不会重载。

    2. 在我看来,“结果”这个词不太准确,我会说“解释”:printf它将显示变量的值,就好像它是格式字符串中指定的类型一样。变量不会改变它的值。它本身并没有printf能力在运行时确定参数与格式字符串的对应关系(而且 C 编译器不会教程序员如何生活以及在哪里布置耙子)。因此,如果您不小心在格式字符串中犯了错误并混淆了参数的类型或数量,则可能会出现各种意外情况。

    • 6

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    Python 3.6 - 安装 MySQL (Windows)

    • 1 个回答
  • Marko Smith

    C++ 编写程序“计算单个岛屿”。填充一个二维数组 12x12 0 和 1

    • 2 个回答
  • Marko Smith

    返回指针的函数

    • 1 个回答
  • Marko Smith

    我使用 django 管理面板添加图像,但它没有显示

    • 1 个回答
  • Marko Smith

    这些条目是什么意思,它们的完整等效项是什么样的

    • 2 个回答
  • Marko Smith

    浏览器仍然缓存文件数据

    • 1 个回答
  • Marko Smith

    在 Excel VBA 中激活工作表的问题

    • 3 个回答
  • Marko Smith

    为什么内置类型中包含复数而小数不包含?

    • 2 个回答
  • Marko Smith

    获得唯一途径

    • 3 个回答
  • Marko Smith

    告诉我一个像幻灯片一样创建滚动的库

    • 1 个回答
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Алексей Шиманский 如何以及通过什么方式来查找 Javascript 代码中的错误? 2020-08-03 00:21:37 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    user207618 Codegolf——组合选择算法的实现 2020-10-23 18:46:29 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5