VC6.0中printf()的前后减减作为参数的压栈问题

本文深入探讨了在不同的C编译器(如VC6.0、GCC)中,printf函数处理自增自减运算符时的参数解析顺序差异,揭示了这些差异背后的原因及其实现机制。

文章最早完成发布于 2020-03-18 15:58:06,如实际情况有任何变化请见谅。

简单来讲,在VC6.0中printf()是从右到左入栈的,而且如果函数语句中存在后++或者后- -时,++与- -将在最后出栈。
*后来发现,好像C-Free对printf()编译时也遵循同样的规则。
*此处实际涉及到 未定义行为(undefined behavior) 的概念。也就是说,虽然所有C语言编译器都要遵循 ISO/IEC 9899:2024 这个C语言国际标准,但是下面这个逻辑属于标准里没有定义的部分1

引入

首先,我们要知道,在Standard C中并没有对stdio.h库中函数
int __cdecl printf(const char * __restrict__ _Format,...);(以下简称为printf)的具体入栈方式做定义,所以会出现printf中如果带有自加自减运算的时候,使用不同的C编译器就会输出不同结果的情况。

现象

示例1

#include <stdio.h>

int main()
{
    int i=8;
    printf("%d, %d, %d, %d\n",i,--i,i,i--);
    return 0;
}

当上述代码运行在VC6.0上时,就会出现如下结果:
VC6.0_Test01
但如果在CLion或者是Code::Blocks上编译时,运行结果就会变成:
CLion_Test01
C::B_Test01

出现上述结果的主要原因是CLion和Code::Blocks目前使用的都是GCC(GNU Compiler Collection,GNU编译器套件)编译,所以结果一致;
但由于部分头文件是C99标准引入的新特性,微软的VC编译器不支持C99,那是不是VC就不能用了呢?好东西,当然人人眼馋,微软虽然表面上说不支持C99,但是有用的特性还是会引入,因此VS2010也引入了新版头文件,在VS2010及其以后的版本中,可以放心使用。但是要注意,VC6.0只是引入了这个新特性,而不是支持C99。2

解释

VC6.0的入栈方法

以上述例子为例,VC6.0编译printf函数的时候遵循两个原则:

  1. 从右往左入栈;
  2. 后自加后自减永远在栈底。

按第一点的逻辑,我们发现VC6.0在实际编译时应该如下表操作:

栈顶
i - -
i
– i
i
栈底

依照栈的先进后出的特点,第一个计算的应该是i - -,而后自加后自减的执行与汇编的EBP寻址有关3,可以简单理解为在执行后自加或后自减时,i原来的值保存在了一个缓存中,然后再进行自减操作。
但是如果只是这样的话,按右序计算i, --i, i, i--再printf也应该输出6, 6, 7, 8才对,而这个错误的发生就与第二点规则有关;
用两个实例可以证明第二点规则:
修改原示例1为:

#include <stdio.h>

int main()
{
    int i=8;
    printf("%d, %d, %d, %d\n",i,--i,i,i--);
    printf("%d\n",i); //输出所有计算完成后的i的值
    return 0;
}

则结果为
VC6.0_Test02
说明运行结束后,i的值变成了6,所以我们可以把整个计算过程看成(i,i,--i,i)--;
这样就解释得通为什么在VC6.0中输出的结果是7,7,8,8而不是6, 6, 7, 8
如果还不能解释的话,就请修改示例1为:

#include <stdio.h>

int main()
{
    int i=8;
    printf("%d, %d, %d, %d\n",i,--i,i--,i--);
    printf("%d\n",i); //输出所有计算完成后的i的值
    return 0;
}

输出结果如下:
VC6.0_Test03

其他

以GCC编译器为基础的IDE(Integrated Development Environment,集成开发环境)们对printf的编译规律大致可以理解为:4

  1. 从右至左依次计算;
  2. 每个后自加/后自减都有一个独立的缓存空间;
  3. 所有计算结束,最后格式化输出。
验证

我们可以用命令行工具(cmd.exe)在.c文件所在路径下使用gcc -S -fverbose-asm Test01.c5把示例1以汇编语句混杂注释C语言语句输出。这样,在.c文件的同一路径下我们就得到了一个以ASM(ASseMbly,汇编)代码写的.s文件(可以直接用记事本打开,这里我用的是notepad++.exe)
ASM_Test01
6

Reference


  1. cppreference.com相关说明 ↩︎

  2. 程序员C语言快速上手——基础篇(三)——血色v残阳 ↩︎

  3. printf函数压栈(i++/i–,++i/–i) 终极解密——spring-go ↩︎

  4. 关于printf(“%d,%d”,i–,i++)的问题——问路1 ↩︎

  5. GCC -S选项:生成汇编文件——C语言中文网 ↩︎

  6. 汇编语言实例讲解(1)——niedong0816 ↩︎

#include <stdio.h> #include <stdlib.h> #include <string.h> // 链节点定义 typedef struct StackNode { char data; struct StackNode* next; } StackNode; // 顶指针 StackNode* top = NULL; // 初始化 void initStack() { top = NULL; } // 判断是否为空 int isEmpty() { return top == NULL; } // 入 void push(char ch) { StackNode* newNode = (StackNode*)malloc(sizeof(StackNode)); if (!newNode) { printf("内存分配失败!\n"); exit(1); } newNode->data = ch; newNode->next = top; top = newNode; } // 出 char pop() { if (isEmpty()) { return '\0'; } StackNode* temp = top; char ch = temp->data; top = top->next; free(temp); return ch; } // 获取对应左括号 char getMatchingLeft(char ch) { if (ch == ')') return '('; if (ch == ']') return '['; if (ch == '}') return '{'; return '\0'; } // 判断是否为左括号 int isLeftBracket(char ch) { return ch == '(' || ch == '[' || ch == '{'; } // 主函数 int main() { char expr[200]; printf("请输入表达式: "); fgets(expr, 200, stdin); initStack(); int len = strlen(expr); for (int i = 0; i < len; i++) { char ch = expr[i]; if (isLeftBracket(ch)) { push(ch); } else if (ch == ')' || ch == ']' || ch == '}') { char match = getMatchingLeft(ch); if (isEmpty() || pop() != match) { printf("括号不匹配!\n"); return 0; } } } if (isEmpty()) { printf("括号匹配成功!\n"); } else { printf("括号不匹配!\n"); } return 0; } 该代码是与队列实验中我所写的代码,要求你根据实验要求完成实验报告: 实验要求 一、实验要求 1、掌握的顺序表示、链表表示以及相应操作的实现。注意空和满的条件; 2、掌握队列的顺序表示、链表表示以及相应操作的实现。注意队列中队头与队尾指针的变化情况。 二、实验环境 VC6.0 三、实验内容 1.利用链编写一个算法判断输入的表达式中的括号是否配对(假设含有左右圆括号O). 2.思考如何扩展支持其他括号类型(}、[)。 【思考题】 试各举一个实例,简要说明和队列在程序设计中所起的作用。 你需要给出的: 实验原理及方法 实验方案及程序流程(给出简洁的关键代码) 实验过程中遇到的问题及解决方法 实验结果分析与总结 完成思考题 要求你以初学者视角完成
最新发布
11-03
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学渣戊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值