一个小程序引发的讨论(运算优先级、参数传递与调用约定的问题)

#include <iostream>
#include <cstdio>
 
int main()
{
    int arr[] = { 6, 7, 8, 9, 10 };
    int *ptr = arr;
    *(ptr++) += 123;
 
    //printf("%d, %d, %d\n", *ptr, *(++ptr), *ptr);//8, 8, 7
    //std::cout << *ptr << ", " << *(++ptr) << ", " << *ptr << std::endl;//7, 8, 8
 
    //printf("%d, %d, %d\n", *ptr, *(ptr++), *ptr);//8, 7, 7
    std::cout << *ptr << ", " << *(ptr++) << ", " << *ptr << std::endl;//7, 7, 8
    for(int i=0; i<5; ++i)
    {
        std::cout << arr[i] << "\t";
    }
}

 

如果不能理解输出结果,请从两个方面考虑:

  1. 运算符优先级与结合性
  2. 参数传递顺序与调用约定
  3. 重视:表达式的值!!表达式可以划分为子表达式

就上面例子而言。

*(i++)

虽然你用括号强制要求先算i++,不过要认清本质,i++表达式,实际上等价于

int tmp=i;

//do other operation, take the rvalue of tmp as the rvalue of expression (i++) for other use

i=i+1;

期间会生成一个临时变量作为i++表达式的值。即i++表达式的值为保存原始i值的临时变量的值

其实,*(i++)和*i++效果是一样的,因为++优先级高于*

 

问题一: 运算优先级

Precedence Operator Description Example Associativity
1 ()
[]
->
.
::
++
--
Grouping operator
Array access
Member access from a pointer
Member access from an object
Scoping operator
Post-increment
Post-decrement
(a + b) / 4;
array[4] = 2;
ptr->age = 34;
obj.age = 34;
Class::age = 2;
for( i = 0; i < 10; i++ ) ...
for( i = 10; i > 0; i-- ) ...
left to right
2 !
~
++
--
-
+
*
&
(type)
sizeof
Logical negation
Bitwise complement
Pre-increment
Pre-decrement
Unary minus
Unary plus
Dereference
Address of
Cast to a given type
Return size in bytes
if( !done ) ...
flags = ~flags;
for( i = 0; i < 10; ++i ) ...
for( i = 10; i > 0; --i ) ...
int i = -1;
int i = +1;
data = *ptr;
address = &obj;
int i = (int) floatNum;
int size = sizeof(floatNum);
right to left
3 ->*
.*
Member pointer selector
Member pointer selector
ptr->*var = 24;
obj.*var = 24;
left to right
4 *
/
%
Multiplication
Division
Modulus
int i = 2 * 4;
float f = 10 / 3;
int rem = 4 % 3;
left to right
5 +
-
Addition
Subtraction
int i = 2 + 3;
int i = 5 - 1;
left to right
6 <<
>>
Bitwise shift left
Bitwise shift right
int flags = 33 << 1;
int flags = 33 >> 1;
left to right
7 <
<=
>
>=
Comparison less-than
Comparison less-than-or-equal-to
Comparison greater-than
Comparison geater-than-or-equal-to
if( i < 42 ) ...
if( i <= 42 ) ...
if( i > 42 ) ...
if( i >= 42 ) ...
left to right
8 ==
!=
Comparison equal-to
Comparison not-equal-to
if( i == 42 ) ...
if( i != 42 ) ...
left to right
9 & Bitwise AND flags = flags & 42; left to right
10 ^ Bitwise exclusive OR flags = flags ^ 42; left to right
11 | Bitwise inclusive (normal) OR flags = flags | 42; left to right
12 && Logical AND if( conditionA && conditionB ) ... left to right
13 || Logical OR if( conditionA || conditionB ) ... left to right
14 ? : Ternary conditional (if-then-else) int i = (a > b) ? a : b; right to left
15 =
+=
-=
*=
/=
%=
&=
^=
|=
<<=
>>=
Assignment operator
Increment and assign
Decrement and assign
Multiply and assign
Divide and assign
Modulo and assign
Bitwise AND and assign
Bitwise exclusive OR and assign
Bitwise inclusive (normal) OR and assign
Bitwise shift left and assign
Bitwise shift right and assign
int a = b;
a += 3;
b -= 4;
a *= 5;
a /= 2;
a %= 3;
flags &= new_flags;
flags ^= new_flags;
flags |= new_flags;
flags <<= 2;
flags >>= 2;
right to left
16 , Sequential evaluation operator for( i = 0, j = 0; i < 10; i++, j++ ) ... left to right
上表可以看出:绝大多数运算符都是从左到右结合的,除了单目、多目、赋值三类运算符是右结合的

对于复杂的运算先后问题,确定运算顺序的算法如下:

1. 先找出所有运算符中优先级最低的,以这些运算符为分界将表达式分成多段,每段加上括号()

2. 对于同一优先级考虑结合性顺序

3. 编译器分析运算顺序时是从左向右扫描的(??)

因此,我们对表达式运算的分析也应该从左到右,并只和自己临近运算符的优先级比较,加上括号以便清晰。分析到完,并加完括号,然后可以考虑结合性顺序了,是从左到右算还是从右到左算

其实整个分析过程也是编译器分析的过程,只不过编译器是生成一棵多叉树。其中,我们给手工加上的括号对应树上的树枝

实例分析:

 

!x&&x+y++-y;

先找出所有运算符中优先级最低的,以这些运算符为分界将表达式分成多段

&&优先级最低

(!x)&&(x+y++-y);

分析后半部分,+,-优先级最低

(!x)&&((x)+(y++)-(y));

你可以试一下另一个例子:

x-++y+x>y?x++:x%y--/x+1

(x-++y+x>y)?(x++):(x%y--/x+1)

((x-++y+x)>y)?(x++):((x%y--/x)+1)

(((x)-(++y)+(x))>y)?(x++):(((x)%(y--)/x)+1)

image

最后,良好的编程风格完全可以避免这些复杂而繁琐的运算优先级问题。建议,复杂表达式多加括号。

 

另外注意:C++和C中的运算符优先级是不一样的。下面是C的运算符优先级,可见++和*是在一个优先级

image 

于是,见《C程序设计语言》P79

image

 

问题二:参数传递与调用约定

首先谈谈与调用约定有关的参数传递方向、堆栈清除相关问题。

__stdcall, __cdecl, __fastcall, ..被这些修饰关键字修饰的函数,其参数都是从右向左通过堆栈传递的(__fastcall的前面部分由ecx,edx传),
函数调用在返回前要清理堆栈,但由调用者还是被调用者清理不一定。
1、__stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,
自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。 int f(void *p)  -->>   _f@4(在外部汇编语言里可以用这个名字引用这个函数)
2、__cdecl,C调用约定(The C default calling convention)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数vararg的函数(如printf)只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。 _cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。
是MFC缺省调用约定。
3、__fastcall调用的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。__fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。
4、thiscall仅仅应用于“C++”成员函数。this指针存放于CX/ECX寄存器中,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。
5、naked call。 当采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。
(这些代码称作 prolog and epilog code,一般,ebp,esp的保存是必须的).
但是naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。
关键字 __stdcall、__cdecl和__fastcall可以直接加在要输出的函数前。它们对应的命令行参数分别为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。
要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs。
2)名字修饰约定
1、修饰名(Decoration name)
“C”或者“C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出“C++”重载函数、构造函数、析构函数,又如在汇编代码里调用“C””或“C++”函数等。
修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。
2、名字修饰约定随调用约定和编译种类(C或C++)的不同而变化。函数名修饰约定随编译种类和调用约定的不同而不同,下面分别说明。
a、C编译时函数名修饰约定规则:
__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,
格式为_functionname@number。
__cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。
__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,
格式为@functionname@number。
它们均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。
b、C++编译时函数名修饰约定规则:
__stdcall调用约定:
1、以“?”标识函数名的开始,后跟函数名;
2、函数名后面以“@@YG”标识参数表的开始,后跟参数表;
3、参数表以代号表示:
X--void ,
D--char,
E--unsigned char,
F--short,
H--int,
I--unsigned int,
J--long,
K--unsigned long,
M--float,
N--double,
_N--bool,
....
PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代
表一次重复;
4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前

5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。
其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如
int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”
void Test2() -----“?Test2@@YGXXZ”
__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。
__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。
VC++对函数的省缺声明是"__cedcl",将只能被C/C++调用. 
6 用法举例
BOOL _declspec(dllexport)_stdcall InstallHook()
{
     hkb = SetWindowsHookEx(WH_KEYBOARD,
                                    (HOOKPROC)KeyboardProc,
                                    hins,
                                    0);
     return TRUE;
}

 

具体的函数名修饰约定应该是编译器相关的。可以详细查看另一篇日志..

posted @ 2008-09-17 16:09  中土  阅读(745)  评论(0编辑  收藏  举报
©2005-2008 Suprasoft Inc., All right reserved.