很久以前,我接触的最初几本C语言书中,我记得有类似这么一句话“C语言是一种弱类型的语言,类型之间可以进行隐式的转换;而C++是强类型的语言,需要进行强制类型转换”。我忘了是哪本书,但这句话我一直记得。因为实际写代码中一直也没有触碰隐式的转换(我一般都会强制转换),所以也没有深究过这个问题。然而最近的一段代码却给我带来了一些困惑。
copyright@ www.spongeliu.com
先抛开我遇到的问题不说,简单回顾下C语言的隐式类型转换,如下一段代码:

#include <stdio.h>
double sum(double x, double y); //声明一个函数
 
int main()
{
    int x=10;
    int y=10;
 
    double z=sum(x,y);
    printf("%lf\n",z);
    return 0
}

没问题,我们得到的结果是20.000000。

同时,上面代码中,首先声明了sum函数的原型“double sum(double x,double y);”。 熟悉C的人都知道,在“old style c”中,我们可以声明一个不带参数原型的函数,也就是这么声明“double sum();” 这表示sum函数可能有无限个参数,具体由函数定义来决定。那么,如果在上述代码中使用这种方式会怎么样呢?

double sum(); //声明一个函数
 
int main()
{
    int x=10;
    int y=10;
 
    double z=sum(x,y);
    printf("%lf\n",z);
    return 0
}

由于各个编译器的具体实现不同,我的实验环境是Linux+gcc4.4.5。在这样的编译环境下,你会发现程序得到了一个不确定的值

很显然,这段代码跳入了一个陷阱。即时你在编译的时候加入了-Wall选项,也不会出现任何的警告!C既然允许我们声明一个参数未定义的函数原型,却为什么不让我们得到一个正确的程序呢?

让我们来看C99标准中section 6.5.2.2 "Function calls" 的Paragraphs 6, 7:

Paragraphs6
1、默认参数提升:如果一个函数的形参类型未知,那么调用函数时要对相应的实参做“整数提升(integer promotions)”,除此以外,float类型的参数会被提升为double。
2、如果形参和实参个数不相等的时候,行为未定义;
3、 如果函数定义的时候指定参数原型,那么a)参数原型包含(...),即变参;b)形参类型和实参类型不符合。这两种行为都是未定义的;
4、 如果函数定义的时候不指定参数原型,如果提升后的实参类型和形参类型不相符,则行为未定义;除了两种情况a)提升后一个是unsigned int一个是signed int,则值可以被表示成这两种的任何形式;b)实参或形参都是指针,分别指向限定和非限定(如const)的char或者void。

Paragraphs7
1、 如果一个函数的形参类型已知,则实参的类型会被隐式的转换成形参的类型,并且转换成非限定的对应类型;
2、如果函数原型中有(...)参数,那么对应的实参会被进行默认参数提升;

(以上纯属个人翻译,如想看原文,请参照c99标准手册

你也许会稍许有些疑惑,什么情况下函数定义会没有原型(参数类型未知)呢?让我们来看一下:

首先,无论函数声明还是函数定义,都是可以没有原型的。如这样的代码:

1
2
void func(int a, char b, float c);
void func(int a, char b, float c) { /* ... */ }

line1是带参数原型的函数声明;line2是带参数原型的函数定义。

再看这样的代码:

1
2
3
4
5
6
void func();
void func(a, b, c)
    int a;
    char b;
    float c;
{ /* ... */ }

line1是不带参数原型的函数声明;line2往后是不带参数原型的函数定义 (K&R C style)

简单来说,以上标准可以归结为两点:

1、对于有参数原型的函数(非变参),实参会被隐式转换成相应的实参的类型;

2、对于没有参数原型的函数或者变参函数,实参会被进行“默认参数提升”

那现在来看,我们之前的问题出再哪里了?首先,sum函数声明的时候没有定义参数原型,因此main函数在调用sum的时候,对参数作了int类型的提升,而不是隐式的转换;其次,sum函数在定义的时候指定了参数的类型,其类型同提升后得到的int类型不相同,命中标准中Paragraphs6的第三条,得到一个未定义的行为,至于怎么处理,那就是编译器的事情了。

C语言的类型转换,除了上述的这些标准,还涉及到很多很复杂的事情,比如有符号、无符号、浮点等,每一种类型转换都要定义一种转换规则,而且不同的编译器不同的体系结构往往会带来不同的结果,很多类型转换都是C标准中未定义的,很可能就导致错误的出现。比如上述的这个例子,很显然是一个很阴暗的用法,我们在实际写程序的时候往往不会这么用,但是很不巧,我的一个程序中的一个函数参数很多,我在声明该函数的时候就偷懒没有指定参数的原型,而恰巧其中一个参数被作了提升而类型同函数定义的不一样,导致程序出错。因此,在C语言中,我们要尽量的避免使用隐式的类型转换。

此外,对于文章开头提到的“C语言是弱类型”,实在不敢苟同。其实争论一个语言是强弱类型本身就没有意义,但我个人认为,C语言这种在编译时就已经确定了数据类型的语言,如果硬要划分的话,怎么也不应该算是一个弱类型!

参考数目:
C和C++经典著作:C陷阱与缺陷
你必须知道的495个C语言问题

anyShare分享到:
          

9 Responses to “弱类型?C语言参数提升带来的一个陷阱”

  1. 老栋 says:

    第一次听说 old style c
    不过说明了,规范编程很重要,是吧

  2. wildpointer says:

    double sum(void);
    这表示不接收任何参数吧?

  3. bitstream says:

    你需要打开:

    -Wstrict-prototypes (C and Objective-C only)
    Warn if a function is declared or defined without specifying the argument types. (An old-style function definition is
    permitted without a warning if preceded by a declaration which specifies the argument types.)

  4. Xie Yu says:

    楼主代码里面,printf里面使用lf是未定义的,用f就可以了,和scanf不同

  5. Hana says:

    “其实争论一个语言是强弱类型本身就没有意义,但我个人认为,C语言这种在编译时就已经确定了数据类型的语言,如果硬要划分的话,怎么也不应该算是一个弱类型!”

    ‘编译时确定数据类型’这个证据,说明C是‘静态类型’(相对于动态类型,不能从源码就完全确定类型)的语言,与强弱类型还是无关。

Leave a Reply

Note: Commenter is allowed to use '@User+blank' to automatically notify your reply to other commenter. e.g, if ABC is one of commenter of this post, then write '@ABC '(exclude ') will automatically send your comment to ABC. Using '@all ' to notify all previous commenters. Be sure that the value of User should exactly match with commenter's name (case sensitive).