【问题标题】:Changing stdout (putch() function) on the fly in C在 C 中动态更改标准输出(putch() 函数)
【发布时间】:2013-05-05 07:55:37
【问题描述】:

我正在使用 XC8 编译器。为此,您必须定义自己的 void putch(char data) 函数才能使 printf() 之类的函数正常工作,如 here 所述。基本上,putch() 是用于向stdout 写入字符的函数。

我现在想即时更改此功能。我有两个不同的函数,putch_a()putch_b(),并且希望能够即时更改用于 putch() 本身的函数。

我想到了这个:

unsigned use_a_not_b;
void putch(char data) {
   if (use_a_not_b) {
      putch_a(data);
   } else {
      putch_b(data);
   }
}

但是,这会降低执行速度。有没有办法为此使用指针?我已阅读this answer,并编写了以下代码:

void putch_a(char data);
void putch_b(char data);

void (*putch)(char) = putch_a; // to switch to putch_a
void (*putch)(char) = putch_b; // to switch to putch_b

这行得通吗?有没有更快或更实用的方法?

【问题讨论】:

  • 它可能甚至无法编译。
  • 一个 if...else 和一个调用/返回真的会显着降低 i/o 函数的执行速度吗?与检查缓冲区空间是否空闲、计算缓冲区偏移量、通知硬件数据可用、在数据实际传输到硬件时被中断等所有时间相比。你的第一个块应该没问题。
  • @ThePhoton 您适合这种情况,但如果您想选择更多功能怎么办?这就是我认为应该有另一种选择来做到这一点的方式。
  • 如果 putch_a 和 putch_b 是可内联的,这甚至不需要额外的调用/返回。对于更多情况,您可以有一个整数 which_putch 和一个 case 语句……编译器会很好地优化它们。如果您想获得花哨的@undefinedbehavior 的解决方案也可以,但确实需要额外的电话/退货。
  • @ThePhoton 当然取决于您要使用多少不同的功能。对于少量,switch 语句可能确实是最快的。对于更大的金额(我没有测试过有多大),答案会更快。

标签: c function pointers function-pointers stdout


【解决方案1】:

不,为什么不

回答您的问题:不,您不能按照您的想法(即函数指针)。函数指针是一个具有另一个变量地址的变量。为了说明,考虑当你有一个函数指针foo 指向函数bar 时它是如何工作的。

int bar() {
}

void baz(int (*foo)()) {
   int x = foo();     // Calls the function pointed to bar foo
}

int main() {
   int (*foo)();
   foo = &bar;
   baz(foo);    // Cal baz() passing it foo, which points to bar()
}

foo 持有的是bar 的地址。当您将foo 传递给需要函数指针参数的某个函数(在本例中为baz())时,该函数取消引用指针,即查看与foo 关联的内存地址,获取其中存储的地址,在我们的例子是bar 的地址,然后在该地址调用一个函数(在我们的例子中是bar)。对此要非常小心:在上面的例子中baz()

  • 让我看看与foo关联的内存,里面还有另一个地址
  • 从内存中加载该地址,并在该地址调用函数。该函数返回一个int 并且不接受任何参数。

让我们将其与直接调用bar() 的函数进行对比:

void qux() {
   int x = bar();     // Call bar()
}

在这种情况下,没有函数指针。有一个地址,由链接器提供。链接器列出程序中的所有函数,它知道,例如bar() 位于地址0xDEADBEEF。所以在qux() 中只有一个jump 0xDEADBEEF 调用。相比之下,baz() 有类似 (pseudo-addembly):

pop bar off the stack into register A
read memory address pointed to by register A into register B
jump to memory location pointed to by register B

例如,putch()printf() 调用的方式与qux() 调用bar() 的方式完全一样,而不像baz() 的方式:putch 得到静态链接 em> 到你的程序中,所以putch() 的地址是硬编码的,很简单,因为fprintf() 不需要函数指针来调用参数。

为什么#define 不是答案

#define 是一个预处理器 指令,也就是说,用#define 定义的“符号”在编译器看到你的代码之前就被它们的值替换了。这意味着#define 使您的程序动态修改更少。在某些情况下这是可取的,但在您的情况下,它对您没有帮助。为了说明您是否定义了这样的符号:

#define Pi 3.14

然后,无论您在哪里使用Pi,就好像您输入了3.14。因为Pi不存在,就编译器而言,你甚至不能取它的地址来指向它。

最接近动态的putch

正如其他人所说,您可以有某种 case 语句、条件语句或全局指针,但 putch 函数本身必须以相同的形式存在。

全局函数指针解决方案:

   void (*myPutch)(char);

   putch(char ch) {
       myPutch(ch);
   }

   int main() {
       myPutch = putch_Type_A();

       ...

       myPutch = putch_Type_B();

   }

If/then/else 解决方案已在其他答案中提供

goto 解决方案:这将是一个丑陋(但有趣!)的 hack,并且只能在冯诺依曼型机器上实现,但在这些情况下,您可以让您的 putch 看起来像这样:

   putch(char ch) {
       goto PutchTypeB
   PutchTypeA:
       // Code goes here
       return; 
   PutchTypeB:
       // Code goes here
       return; 

   }

然后,您将使用 goto 覆盖 goto 指令到某个其他内存地址。您必须弄清楚执行此操作的操作码(可能来自反汇编),这在哈佛架构机器上是不可能的,所以它在 AVR 处理器上可用,但如果笨拙的话,它会很有趣。

【讨论】:

    【解决方案2】:

    没有。由于代码的生成和链接方式,这不能保证有效。不过……

    void (*output_function)(char) = putch_a;
    
    void putch(char c) {
        output_function(c);
    }
    

    现在您可以随时更改 output_function...

    C 中没有“速度”的概念。这是由实现引入的属性。有快速实现(或者更确切地说,在“编译器”的情况下产生快速代码的实现)和慢速实现(或产生慢代码的实现)。

    无论哪种方式,这都不太可能成为重大瓶颈。生成一个解决有用程序的程序,对其进行分析以确定最重要的瓶颈并努力优化这些瓶颈。

    【讨论】:

    • 谢谢!我也可以做一个goto#define 吗?那不是更快吗?
    • @CamilStaps 查看我的答案的编辑...我根本看不出goto#define 是如何工作的。解决这个问题的最佳机会是使用我生成的代码。
    【解决方案3】:

    在优化它之前,请确保您真正拥有的东西确实会显着降低执行速度。在 i/o 函数中,通常会发生很多其他事情(检查缓冲区空间是否空闲、计算缓冲区偏移量、通知硬件数据可用、在数据实际传输到硬件时被中断等),这会使单个额外的 if/else 无关紧要。

    在大多数情况下,您的第一个块应该没问题。

    在 cmets 中,您提到可能需要将此结构扩展到多个 putch() 函数。

    不妨试试

    enum PUTCH { sel_putch_a, sel_putch_b, ... };
    
    enum PUTCH putch_select;
    
    void putch(char c) {
        switch(putch_select) {
            case sel_putch_a : putch_a(c); break;
            case sel_putch_b : putch_b(c); break;
            /* ... */
        }
    }
    

    编译器应该能够将 switch 语句优化为简单的计算和 goto。如果putch_<n> 函数是可内联的,这甚至不需要额外的调用/返回。

    在另一个答案中使用指向函数的解决方案在能够动态更改可用的 putch 函数或在其他文件中定义它们方面更加灵活(例如,如果您正在编写一个库或其他人使用的框架),但它确实需要额外的调用/返回开销(与仅定义单个 putch 函数的简单情况相比)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-04
      • 1970-01-01
      • 2014-07-24
      • 2012-09-30
      • 2011-02-13
      • 1970-01-01
      相关资源
      最近更新 更多