参考Andrew Koenig《C 陷阱与缺陷第二版》4.3节目录形参是变量实参是值返回类型没声明就默认 int参数类型少写一个 doublesquare(2) 从 4 变成 0默认实参提升float→doublechar/short→int回到 sqrt书里说它错在两处最后一个坑scanf(%d, c)形参是变量实参是值4.3节是这么起头的任何 C 函数都有一个形参列表列表中的每个参数都是一个变量该变量在函数调用过程中被初始化。//形参列表写在函数名后面圆括号()里面的一组变量声明这些变量叫形式参数形参。他给的例子是一个整型形参int abs(int n) { return n 0 ? -n : n; }n就是形参。函数一被调用这个变量就被赋上值函数体里当普通局部变量用。形参列表也可以为空void eatline() { int c; do c getchar(); while (c ! EOF c ! \n); }调用的时候调用方把实参递过去if (abs(a - b) n) printf(difference is out of range\n);这里a - b就是实参——一个表达式不是变量。实参可以随便写只要算出来的值能对上形参。形参列表为空实参列表也就只能为空。就这么对应关系。返回类型没声明就默认 int书里有个细节值得注意——先讲返回类型再说参数类型理由是任何一个 C 函数都有返回类型要么是 void要么是函数生成结果的类型。函数的返回类型理解起来要比参数类型相对容易一些因此我们将首先讨论它。返回类型好懂在哪好懂在不写也能猜如果一个函数在被定义或声明之前被调用那么它的返回类型就默认为整型。这就是隐式声明。调用一个编译器没见过的函数它不报错自己在心里记一句int 函数名();然后按返回值是 int去生成代码。想确认编译器真的写下了那句int sqrt();有一个办法把函数定义搬到调用点后面。#include stdio.h int main(void) { printf(%g\n, square(0.3)); return 0; } double square(double x) { return x * x; }只写过一次square它却说重定义。因为编译器已经替你写了一句int square();跟你自己写的double square(double x)撞上了。这条错误就是隐式声明的实物。参数类型少写一个 doublesquare(2)从 4 变成 0书里对参数部分的评价C 语言中形参与实参匹配的规则稍微有一点复杂。看这段实验。两个文件/* square.c */ double square(double x) { return x * x; }/* main.c —— 版本 A */ #include stdio.h double square(); int main(void) { printf(square(2) %g\n, square(2)); printf(square(2.0) %g\n, square(2.0)); return 0; }square(2) 0 square(2.0) 4之后再把main中的double square(); 换成 double square(double);其余不变/* square.c */ double square(double x) { return x * x; }/* main.c —— 版本 B */ #include stdio.h double square(double); int main(void) { printf(square(2) %g\n, square(2)); printf(square(2.0) %g\n, square(2.0)); return 0; }square(2) 4 square(2.0) 4同一份实现同一个调用点square(2)一个得 0 一个得 4。差别在于调用方知不知道参数长什么样。double square();只说了返回 double没说什么参数。调用方手里没有参数类型就只能按实参本来的类型传2是 int那就按 int 递过去。可函数的实现是double square(double x)它按 double 的约定去取参数——取的地方不对拿到 0。至于为什么拿到0就先不讨论了。写成double square(double);之后调用方知道这里要一个 double于是把2先转成2.0再传。书里的话根据这个声明square(2) 是合法的整数 2 将会被自动转换为双精度类型就好像程序员写成 square((double)2) 或者 square(2.0) 一样。默认实参提升float→doublechar/short→int既然省略参数类型有风险什么时候能省书里给了条件如果一个函数没有 float、short 或者 char 类型的参数在函数声明中完全可以省略参数类型的说明注意函数定义中不能省略参数类型的说明。因此即使是在 ANSI C 中像下面这样声明 square 函数也是可以的double square();为什么偏偏是这三个类型因为实参在传过去之前会被默认提升这样做依赖于调用者能够提供数目正确且类型恰当的实参。这里“恰当”并不意味着“等同”float 类型的参数会自动转换为 double 类型short 或 char 类型的参数会自动转换为 int 类型。注意判据不是信息丢没丢而是提升完之后类型还对不对得上。默认提升是单向的——它只会把实参往终点推推到int或double就停char / short ───┐ ├───→ int float ───┴───→ double形参就在终点上int、double实参推上去正好落在它身上类型一致 → 可以省形参在终点下面char、short、float实参被推过了它的头顶落在它上方类型不一致 → 必须声明所以对char来说往大里转恰恰就是问题所在——值一个字节没丢类型却从char变成了int跟形参的char对不上了。一旦形参是char就省不得了int isvowel(char c) { return c a || c e || c i || c o || c u; }因为其形参为 char 类型所以在调用该函数的其他文件中必须声明int isvowel(char);不这么写调用方就会把实参自动提升成int递过去。书里的原话是否则调用者将把传递给 isvowel 函数的实参自动转换为 int 类型这样就与形参类型不一致了。不一致——问题从来不在信息量在类型。或许会想我char的值至少没丢提升到int也没问题吧有问题的这里就不探讨了。反过来形参直接写成int就一劳永逸int isvowel(int c) { ... }书里紧接着的反面例子如果函数 isvowel 是这样定义的int isvowel(int c) {...}那么调用者就无需进行声明即使调用者在调用时传递给 isvowel 函数一个 char 类型的参数也是如此。这里还牵出一个老语法。ANSI C 之前的写法是参数类型写在参数表外面int isvowel(c) char c; { return c a || c e || c i || c o || c u; }书里点破了它的真实含义上面 isvowel 函数的最后一个定义实际上相当于int isvowel(int i) { char c i; return c a || ...; }老式定义里的char c先被提升成 int 进函数进函数后再转回 char。所以int isvowel(c) char c;和int isvowel(char c)不是同一件事。新旧两种写法含义不一样。回到 sqrt书里说它错在两处原因有两个第一个原因是sqrt 函数本应接受一个双精度值实参而实际上却被传递了一个整型参数第二个原因是sqrt 函数的返回类型是双精度类型但却并没有这样声明。对照开头那三种写法正好一一对应声明写法犯了什么错sqrt(2)不声明返回类型 参数类型两样都错-256double sqrt();返回类型对了参数类型还是没给0double sqrt(double);都对了1.41421书里给的第二种更正方式特别能说明问题——它把sqrt(2.0)里的2 写成 2.0double sqrt(); main() { double s; s sqrt(2.0); /* 注意这里 */ }既然double sqrt();没告诉调用方参数类型那就人工把实参写成 double绕过自动转换这道关。实测下来sqrt(2.0)确实得1.41421——参数这一半修好了返回类型那一半之前也已经声明过所以两种配置下都对。最省事的当然是第三种#include math.h把原型一次性搬进来。最后一个坑scanf(%d, c)书里最后举了一个 printf/scanf 的例子#include stdio.h main() { int i; char c; for (i 0; i 5; i) { scanf(%d, c); printf(%d , i); } printf(\n); }表面上读 5 个数、打印 5 个索引。书里说实际输出是0 0 0 0 0 1 2 3 4原因是scanf(%d, ...)要往一个 int 里写 4 个字节而c只有一个字节的位置。多出来的 3 个字节糊到了紧挨着c的内存上——书里那台机器上正好是i的低位。于是每读一次i就被清回 0循环卡住出不来。我在编译器上跑出来的正常的所以这个现象的前提是 c 存在 i 的低位上这就看编译器了c和i在栈上不相邻没糊到循环正常结束。但这不是说程序对了——写 4 字节到 1 字节的变量本身就是未定义行为结果取决于编译器怎么摆变量。另外书里说大多数 C 语言实现却不能检测出这种错误这句话今天已经不成立了。编译器直接给警告warning C4477: “scanf”: 格式字符串“%d”需要类型“int *”的参数但可变参数 1 拥有了类型“char *”