C/C++ 常见误区
2010-08-14 13:45
489 查看
<!--
/* Font Definitions */
@font-face
{font-family:宋体;
panose-1:2 1 6 0 3 1 1 1 1 1;
mso-font-alt:SimSun;
mso-font-charset:134;
mso-generic-font-family:auto;
mso-font-pitch:variable;
mso-font-signature:3 135135232 16 0 262145 0;}
@font-face
{font-family:宋体;
panose-1:2 1 6 0 3 1 1 1 1 1;
mso-font-alt:SimSun;
mso-font-charset:134;
mso-generic-font-family:auto;
mso-font-pitch:variable;
mso-font-signature:3 135135232 16 0 262145 0;}
@font-face
{font-family:Calibri;
panose-1:2 15 5 2 2 2 4 3 2 4;
mso-font-charset:0;
mso-generic-font-family:swiss;
mso-font-pitch:variable;
mso-font-signature:-520092929 1073786111 9 0 415 0;}
@font-face
{font-family:"/@宋体";
panose-1:2 1 6 0 3 1 1 1 1 1;
mso-font-charset:134;
mso-generic-font-family:auto;
mso-font-pitch:variable;
mso-font-signature:3 135135232 16 0 262145 0;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{mso-style-unhide:no;
mso-style-qformat:yes;
mso-style-parent:"";
margin:0cm;
margin-bottom:.0001pt;
text-align:justify;
text-justify:inter-ideograph;
mso-pagination:none;
font-size:10.5pt;
mso-bidi-font-size:11.0pt;
font-family:"Calibri","sans-serif";
mso-ascii-font-family:Calibri;
mso-ascii-theme-font:minor-latin;
mso-fareast-font-family:宋体;
mso-fareast-theme-font:minor-fareast;
mso-hansi-font-family:Calibri;
mso-hansi-theme-font:minor-latin;
mso-bidi-font-family:"Times New Roman";
mso-bidi-theme-font:minor-bidi;
mso-font-kerning:1.0pt;}
.MsoChpDefault
{mso-style-type:export-only;
mso-default-props:yes;
font-family:"Calibri","sans-serif";
mso-bidi-font-family:"Times New Roman";
mso-bidi-theme-font:minor-bidi;}
/* Page Definitions */
@page
{mso-page-border-surround-header:no;
mso-page-border-surround-footer:no;}
@page WordSection1
{size:595.3pt 841.9pt;
margin:72.0pt 90.0pt 72.0pt 90.0pt;
mso-header-margin:42.55pt;
mso-footer-margin:49.6pt;
mso-paper-source:0;
layout-grid:15.6pt;}
div.WordSection1
{page:WordSection1;}
-->
在此论坛上发现了一些特别的问题,这些问题在其他地方并不存在,猜想是因为这里以学生为主,而学校的教材和教师与
IT
发展脱节严重。
1. C++
虽然主要是以
C
的基础发展起来的一门新语言,但她不是
C
的替代品,不是
C
的升级,
C++
和
C
是兄弟关系。没有谁比谁先进的说法,更重要
的一点是
C
和
C++
各自的标准委员会是独立的,最新的
C++
标准是
C++98
,最新的
C
标准是
C99
。因此也没有先学
C
再说
C++
的说法,也不再(注意这
个
"
不再
"
)有
C++
语法是
C
语法的超集的说法。
2. C++/CLI
和
C#
是微软的,它们与
C
和
C++
没有任何关系,虽然部分语法相似。但哪两种语言不相似呢?都是
abc
这
26
个字母。
3.
不要使用
TC/TC++/BC/CB
等古老的编译器来学习
C/C++
,因为它们太古老了,不支持新的
C/C++
标准。不要使用
CBX /VC++6.0 /VC2005
等对
C/C++
标准支持不好的编译器,虽然这些编译器适合工作,但不适合学习,因为它们中的语法陷阱很多。记住唯一适合学习的编译器是
gcc/mingw
。
[antigloss
注:
Dev-C++
使用的编译器就是
gcc
& g++]
4.
不要用
""
代替
<>
来包含系统头文件
,虽然有些编译器允许你这样做,但它不符合
C/C++
标准。
错误的示例:
#include "stdio.h"
,
#include "iostream"
。
[antigloss
注:习惯上,
<>
用于包含标准头文件
和系统头文件
,
""
用于包含自定义头文件
。标准似乎没有明确规定不准用
""
包含标准头文件和系统头文件。使用
""
包含标准头文件或者系统头文件只能说是一种
不良风格
。
]
5.
不要将
main
函数的返回类型定义为
void
,虽然有些编译器允许你这样做,但它不符合
C/C++
标准。不要将函数的
int
返回类型省略不写,在
C++
中要求编译器至少给一个警告。错误的示例:
void main() {}
,
main() {} [antigloss
注:
C99
和
C++98
都要求编译器对省略
int
至少发出一个警告
]
6.
不要把
VC++
中的
#include "stdafx.h"
贴出来,它是预编译头文件。如同上菜时不要把厨师也放到托盘中。
7. [C++]
不要
#include <
iostream.h>
,不要
#include <
string.h>
,因为它们已经被
C++
标准明确的废弃了,请改为
#include <
iostream>
和
#include <
cstring>
。规则就是:
a.
如果这个头文件是旧
C++
特有的,那么去掉
.h
后缀,并放入
std
名字空间,
比如
iostream.h
变为
iostream
。
b.
如果这个头文件是
C
也有的,那么去掉
.h
后缀,增加一个
c
前缀,比如
string.h
变为
cstring
;
stdio.h
变为
cstdio,
等等。
BTW
:不要把
string
、
cstring
、
string.h
三个头文件搞混淆
BTW
:
windows.h
不是
C/C++
的标准文件,因此它的命名
C/C++
不管。
8.
不要再写
char* p = "XXX"
这种语句,要写成
const char* p = "XXX"
,编译器之所以让前者通过编译是为了兼
容以前的大量的旧代码。
[antigloss
注:这段话对
C++
而言是正确的。但是,目前的
C99
标准似乎并没有定义
"XXX"
一定是常量。
]
BTW
:
const TYPE* p
和
TYPE const* p
是一样的,风格不同而已。
BTW
:
C
语言中也有
const
关键字。
9.
不要在同一条语句中包含一个变量的多个
++/--
,因为它们的解析在
C/C++
标准中没有规定,完全取决于编译器的个人行为。
10. C/C++
是平台无关性语言,因此系统相关的
process/GUI
等不在标准
C/C++
库中。比如
graphics.h
和
windows.h
等是由某个编译器提供的,而不是由
C/C++
提供的。
11. C/C++
只是语言,而且是平台无关性语言。论坛上有部分人甚至认为
C
就是
dos
,
C++
就是
windows
,那么请问
linux
是什么?
12. [C++]
面向对象曾经是设计
C with class
(
C++
的前身)的主要目的,但
C++
不是,
C++
是一个多典范语言。主要支持过程调用、基于对
象、面向对象、泛式编程这四种编程典范。当然还支持
functional, generative,metaprogramming
等典范。
13.
语法学家不是文学家,所以当你学会了一门计算机语言时,你还需要学习数据机构和算法,还需要掌握工具和平台
API
的用法。
14. C/C++
是通用语言,因此语法很复杂,你应当裁减成适合你自己的语法集合,比如裁减成
better C
和
ADT
。
15. C/C++
是通用语言,因此只含通用的库,你应该丰富自己需要的库,比如汽车工业协会有自己的
C/C++
函数
/
类
/
模板库。
C/C++
误区一:
void main()
很多人甚至市面上的一些书籍,都使用了
void
main( )
,其实这是错误的。
C/C++
中从来没有定义过
void
main( )
。
C++
之父
Bjarne Stroustrup
在他的主页上的
FAQ
中明确地写着
The
definition void main( ) { /* ... */ } is not and never has been C++, nor has it
even been C.
(
void main( )
从来就不存在于
C++
或者
C
)。下面我分别说一下
C
和
C++
标准中对
main
函数的定义。
1. C
在
C89
中,
main(
)
是可以接受的。
Brian W. Kernighan
和
Dennis M. Ritchie
的经典巨著
The C programming Language 2e
(《
C
程序设计语言第二版》)用的就是
main( )
。不过在最新的
C99
标准中,只有以下两种定义方式是正确的:
int main( void )
int main( int argc, char *argv[] )
(参考资料:
ISO/IEC
9899:1999 (E) Programming languages — C 5.1.2.2.1 Program startup
)
当然,我们也可以做一点小小的改动。例如:
char
*argv[]
可以写成
char **argv
;
argv
和
argc
可以改成别的变量名(如
intval
和
charval
),不过一定要符合变量的命名规则。
如果不需要从命令行中获取参数,请用
int
main(void)
;否则请用
int main( int argc, char *argv[] )
。
main
函数的返回值类型必须是
int
,这样返回值才能传递给程序的调用者(如操作系统)。
如果
main
函数的最后没有写
return
语句的话,
C99
规定编译器要自动在生成的目标文件中(如
exe
文件)加入
return
0;
,表示程序正常退出。不过,我还是建议你最好在
main
函数的最后加上
return
语句,虽然没有这个必要,但这是一个好的习惯。注意,
vc6
不会在目标文件中加入
return
0;
,大概是因为
vc6
是
98
年的产品,所以才不支持这个特性。现在明白我为什么建议你最好加上
return
语句了吧!不过,
gcc3.2
(
Linux
下的
C
编译器)会在生成的目标文件中加入
return 0;
2. C++
C++98
中定义了如下两种
main
函数的定义方式:
int main( )
int main( int argc, char *argv[] )
(参考资料:
ISO/IEC
14882(1998-9-01)Programming languages — C++ 3.6 Start and termination
)
int main( )
等同于
C99
中的
int
main( void )
;
int main( int argc, char *argv[] )
的用法也和
C99
中定义的一样。同样,
main
函数的返回值类型也必须是
int
。如果
main
函数的末尾没写
return
语句,
C++98
规定编译器要自动在生成的目标文件中加入
return 0;
。同样,
vc6
也不支持这个特性,但是
g++3.2
(
Linux
下的
C++
编译器)支持。
3.
关于
void main
在
C
和
C++
中,不接收任何参数也不返回任何信息的函数原型为
“void foo(void);”
。可能正是因为这个,所以很多人都误认为如果不需要程序返回值时可以把
main
函数定义成
void
main(void)
。然而这是错误的!
main
函数的返回值应该定义为
int
类型,
C
和
C++
标准中都是这样规定的。虽然在一些编译器中,
void main
可以通过编译(如
vc6
),但并非所有编译器都支持
void main
,因为标准中从来没有定义过
void
main
。
g++3.2
中如果
main
函数的返回值不是
int
类型,就根本通不过编译。而
gcc3.2
则会发出警告。所以,如果你想你的程序拥有很好的可移植性,请一定要用
int
main
。
4.
返回值的作用
main
函数的返回值用于说明程序的退出状态。如果返回
0
,则代表程序正常退出;返回其它数字的含义则由系统决定。通常,返回非零代表程序异常退出。下面我们在
winxp
环境下做一个小实验。首先编译下面的程序:
int main( void )
{
return 0;
}
然后打开附件里的
“
命令提示符
”
,在命令行里运行刚才编译好的可执行文件,然后输入
“echo %ERRORLEVEL%”
,回车,就可以看到程序的返回值为
0
。假设刚才编译好的文件是
a.exe
,如果输入
“a && dir”
,则会列出当前目录下的文件夹和文件。但是如果改成
“return -1”
,或者别的非
0
值,重新编译后输入
“a
&& dir”
,则
dir
不会执行。因为
&&
的含义是:如果
&&
前面的程序正常退出,则继续执行
&&
后面的程序,否则不执行。也就是说,利用程序的返回值,我们可以控制要不要执行下一个程序。这就是
int main
的好处。如果你有兴趣,也可以把
main
函数的返回值类型改成非
int
类型(如
float
),重新编译后执行
“a
&& dir”
,看看会出现什么情况,想想为什么会出现那样的情况。顺便提一下,如果输入
a ||
dir
的话,则表示如果
a
异常退出,则执行
dir
。
5.
那么
int main( int argc, char *argv[], char *envp[] )
呢?
这当然也不是标准
C/C++
里面定义的东西!
char
*envp[]
是某些编译器提供的扩展功能,用于获取系统的环境变量。因为不是标准,所以并非所有编译器都支持,故而移植性差,不推荐使用。
C/C++
误区二:
fflush(stdin)
1.
为什么
fflush
(
stdin
)
是错的
首先请看以下程序:
include <stdio.h>
int main
(
void
)
{
int i
;
for
(;;)
{
fputs
(
"Please
input an integer
:
"
,
stdout
);
scanf
(
"%d"
,
&i
);
printf
(
"%d/n"
,
i
);
}
return 0
;
}
这个程序首先会提示用户输入一个整数,然后等待用户输入,如果用户输入的是整数,程序会输出刚才输入的整数,并且再次提示用户输入一个整数,然后等待用
户输入。但是一旦用户输入的不是整数(如小数或者字母),假设
scanf
函数最后一次得到的整数是
2
,那么程序会不停地输出
“Please input an integer
:
2”
。这是因为
scanf
(
"%d"
,
&i
);
只能接受整数,如果用户输入了字母,则这个字母会遗留在
“
输入缓冲区
”
中。因为缓冲中有数据,故而
scanf
函数不会等待用户输入,直接就去缓冲中读取,可是缓冲中的却是字母,这个字母再次被遗留在缓冲中,如此反复,从而导致不停地输出
“Please input an integer
:
2”
。
也许有人会说:
“
居然这样,那么在
scanf
函数后面加上
‘fflush
(
stdin
);
’
,把输入缓冲清空掉不就行了?
”
然而这是错的!
C
和
C++
的标准里从来没有定义过
fflush
(
stdin
)。也许有人会说:
“
可是我用
fflush
(
stdin
)
解决了这个问题,你怎么能说是错的呢?
”
的确,某些编译器(如
VC6
)支持用
fflush
(
stdin
)
来清空输入缓冲,但是并非所有编译器都要支持这个功能(
linux
下的
gcc
就不支持),因为标准中根本没有定义
fflush
(
stdin
)。
MSDN
文档里也清楚地写着
fflush
on input stream is an extension to the C standard
(
fflush
操作输入流是对
C
标准的扩充)。当然,如果你毫不在乎程序的移植性,用
fflush
(
stdin
)
也没什么大问题。以下是
C99
对
fflush
函数的定义:
int fflush
(
FILE
*stream
);
如果
stream
指向输出流或者更新流(
update
stream
),
⑶
艺飧龈
铝髯罱
葱械牟僮鞑皇鞘淙耄
敲
?fflush
函数将把这个流中任何待写数据传送至宿主环境(
host environment
)写入文件。否则,它的行为是未定义的。
原文如下:
int fflush
(
FILE
*stream
);
If
stream points to an output stream or an update stream in which the most recent
operation was not input
,
the
fflush function causes any unwritten data for that stream to be delivered to
the host environment to be written to the file
;
otherwise
,
the behavior is undefined.
其中,宿主环境可以理解为操作系统或内核等。
由此可知,如果
stream
指向输入流(如
stdin
),那么
fflush
函数的行为是不确定的。故而使用
fflush
(
stdin
)
是不正确的,至少是移植性不好的。
2.
清空输入缓冲区的方法
虽然不可以用
fflush
(
stdin
),但是我们可以自己写代码来清空输入缓冲区。只需要在
scanf
函数后面加上几句简单的代码就可以了。
/* C
版本
*/
#include <stdio.h>
int main( void )
{
int i, c;
for ( ; ; )
{
fputs("Please input an integer: ", stdout);
scanf("%d", &i);
if ( feof(stdin) || ferror(stdin) )
{ /*
如果用户输入文件结束标志(或文件已被读完),
*/
/*
或者发生读写错误,则退出循环
*/
/* do something */
break;
}
/*
没有发生错误,清空输入流。
*/
/*
通过
while
循环把输入流中的余留数据
“
吃
”
掉
*/
while ( (c = getchar()) != '/n' && c != EOF ) ;
/*
使用
scanf("%*[^/n]");
也可以清空输入流,
*/
/*
不过会残留
/n
字符。
*/
printf("%d/n", i);
}
return 0;
}
/* C++
版本
*/
#include <iostream>
#include <limits> //
为了使用
numeric_limits
using std::cout;
using std::endl;
using std::cin;
using std::numeric_limits;
using std::streamsize;
int main()
{
int value;
for ( ; ; )
{
cout << "Enter an integer: ";
cin >> value;
if ( cin.eof() || cin.bad() )
{ //
如果用户输入文件结束标志(或文件已被读完),
//
或者发生读写错误,则退出循环
// do something
break;
}
//
读到非法字符后,输入流将处于出错状态,
//
为了继续获取输入,首先要调用
clear
函数
//
来清除输入流的错误标记,然后才能调用
// ignore
函数来清除输入流中的数据。
cin.clear();
// numeric_limits<streamsize>::max()
返回输入缓冲的大小。
// ignore
函数在此将把输入流中的数据清空。
//
这两个函数的具体用法请读者自行查询。
cin.ignore( numeric_limits<streamsize>::max(), '/n' );
cout << value << '/n';
}
return 0;
}
C/C++
误区三:强制转换
malloc()
的返回值
首先要说的是,使用
malloc
函数,请包含
stdlib.h
(
C++
中是
cstdlib
),而不是
malloc.h
。因为
malloc.h
从来没有在
C
或者
C++
标准中出现过!因此并非所有编译器都有
malloc.h
这个头文件。但是所有的
C
编译器都应该有
stdlib.h
这个头文件。
在
C++
中,强制转换
malloc()
的返回值是必须的,否则不能通过编译。但是在
C
中,这种强制转换却是多余的,并且不利于代码维护。
起初,
C
没有
void
指针,那时
char*
被用来作为泛型指针(
generic pointer
),所以那时
malloc
的返回值是
char*
。因此,那时必须强制转换
malloc
的返回值。后来,
ANSI C
(即
C89
)
标准定义了
void
指针作为新的泛型指针。
void
指针可以不经转换,直接赋值给任何类型的指针(函数指针除外)。从此,
malloc
的返回值变成了
void*
,再也不需要强制转换
malloc
的返回值了。以下程序在
VC6
编译无误通过。
#include <
stdlib.h>
int main( void )
{
double *p =
malloc( sizeof *p ); /*
不推荐用
sizeof( double ) */
free(p);
return 0;
}
当然,强制转换
malloc
的返回值并没有错,但画蛇添足!例如,日后你有可能把
double *p
改成
int *p
。这时,你就要把所有相关的
(double *) malloc (sizeof(double))
改成
(int
*)malloc(sizeof(int))
。如果改漏了,那么你的程序就存在
bug
。就算你有把握把所有相关的语句都改掉,但这种无聊乏味的工作你不会喜欢吧!不使用强制转换可以避免这样的问题,而且书写简便,何乐而不为呢?使用以下代
码,无论以后指针改成什么类型,都不用作任何修改。
double *p =
malloc( sizeof *p );
类似地,使用
calloc
,
realloc
等返回值为
void*
的函数时,也不需要强制转换返回值。
参考资料:
ISO/IEC
9899:1999 (E) Programming languages — C 7.20.3.3 The malloc function
ISO/IEC 9899:1999 (E) Programming languages — C P104 (6.7.2.2)
C/C++
误区四:
char c = getchar();
getchar
由宏实现:
#define getchar() fgetc(stdin)
。
getchar
有一个
int
型的返回值
.
当程序调用
getchar
时
.
程序就等着用户按键
.
用户输入的字符被存放在键盘缓冲区中
.
直到用户按回车为止
(
回
车字符也放在缓冲区中
).
当用户键入回车之后
,getchar
才开始从
stdio
流中每次读入一个字符
.getchar
函数的返回值是用户输入的第一个字
符的
ASCII
码
,
如出错返回
-1,
且将用户输入的字符回显到屏幕
.
如用户在按回车之前输入了不止一个字符
,
其他字符会保留在键盘缓存区中
,
等待后续
getchar
调用读取
.
也就是说
,
后续的
getchar
调用不会等待用户按键
,
而直接读取缓冲区中的字符
,
直到缓冲区中的字符读完为后
,
才等待用户按
键
.
getch
与
getchar
基本功能相同
,
差别是
getch
直接从键盘获取键值
,
不等待用户按
回车
,
只要用户按一个键
,getch
就立刻返回
,
getch
返回值是用户输入的
ASCII
码
,
出错返回
-1.
输入的字符不会回显在屏幕上
.getch
函数常用于程序调试中
,
在调试时
,
在关键位置显示有关
的结果以待查看
,
然后用
getch
函数暂停程序运行
,
当按任意键后程序继续运行
.
这个版本忽略了个重点,
getch()
是非缓冲输入函数,就是不能用
getch
()来接受缓冲区已存在的字符,如以下
C++
程序,
int
i;while
(
cin>>i);cin.clear();getchar();
运行时如果输入
1 2 3
a
时必须用
getchar
()才能在后面程序获得正常输入,即使先前已经恢复流了,此处用
getch
()是万万不行的。
另外补充个函数,
getche()
,这个函数与前两上类似,功能也相近,都是输入一个字符,返回值同样是输入字符的
ASCII
码,但不同的是,此函数在输入后立即从控制台取字符,不以回车为结束
(
带回显
)
许多初学者都习惯用
char
型变量接收
getchar
、
getc
,
fgetc
等函数的返回值,其实这么做是不对的,并且隐含着足以致命的错误
。
getchar
等函数的返回值类型都是
int
型,当这些函数读取出错
或者读完文件后
,会返回
EOF
。
EOF
是一个宏,标准规定它的值必须是一个
int
型的负数常量
。通常编译器都会把
EOF
定义为
-1
。问题就出在这里,使用
char
型变量接收
getchar
等函数的返回值会导致对
EOF
的辨认出错,或者错把好的数据误认为是
EOF
,或者把
EOF
误认为是好的数据。例如:
int c; /*
正确。应该使用
int
型变量接收
fgetc
的返回值
*/
while ( (c = fgetc(fp)) != EOF )
{
putchar(c);
}
如上例所示,我们很多时
候都需要先用一个变量接收
fgetc
等函数的返回值,然后再用这个变量和
EOF
比较,判断是否已经读完文件。上面这个例子是正确的,把
c
定义为
int
型保证了它能正确接收
fgetc
返回的
EOF
,从而保证了这个比较的正确性。但是,如果把
c
定义为
char
型,则会导致意想不到的后果
。
首先,因为
fgetc
等函数的返回值是
int
型的,当赋值给
char
型变量时,会发生
降级
,从而导致数据截断。例如:
---------------------------------
|
十进制
|
int | char |
|--------|--------------|-------|
| 10 | 00 00 00 0A | 0A |
| -1 | FF FF FF FF | FF |
| -2 | FF FF FF FE | FE |
---------------------------------
在此,我们假设
int
和
char
分别是
32
位和
8
位的。由上表可得,从
int
型到
char
型,损失了
3
个字节的数据。而当我们要拿
char
型和
int
型比较的时候,
char
型会自动
升级
为
int
型。
char
型升级为
int
型后的值会因为它到底是
signed char
还是
unsigned
char
而有所不同。不幸的是,如果我们没有使用
signed
或者
unsigned
来修饰
char
,那么我们无从知晓
char
到底是指
unsigned char
还是指
signed char
,因为这是由编译器决定
的。不过,无论
char
是
signed
的也好,
unsigned
的也罢,都不能改变使用
char
型变量接收
fgetc
等函数的返回值是错误
的这个事实。唯一能改变的是该错误导致的后果。前面我们说了,
char
型和
int
型比较时,
char
会自动升级为
int
,下面我们来看看
signed char
和
unsigned char
在转换成
int
后,它们的值有什么不同:
---------------------------------------
| char | unsigned |
signed |
|-------|---------------|-------------|
| 10 | 00 00 00 0A | 00 00 00 0A |
| FF | 00 00 00 FF | FF FF FF FF |
| FE | 00 00 00 FE | FF FF FF FE |
---------------------------------------
由上表可知,当
char
是
unsigned
的时候,其转换为
int
后的值是正数
。也就是说,假如我们把
c
定义为
char
型变量,而编译器默认
char
为
unsigned char
,那么以下表达式将永远成立
。
(c = fgetc(fp)) != EOF /* c
的值永远为正数,而标准规定
EOF
为负数
*/
也就是说以下循环是一个死循环
。
while ( (c
= fgetc(fp)) != EOF )
{
putchar(c);
}
读到这里,可能有些读者朋友会说:
“
那么我明确把
c
定义为
signed char
型的就没问题了吧!
”
很遗憾,就算把
c
定义为
signed char
,仍然是错误的。假设
fgetc
等函数读到一个字节的值为
FF
,那么返回值就是
00 00 00 FF
。把这个值赋值给
c
后,
c
的值变成
FF
。然后
c
的值为了和
EOF
比较,会自动升级为
int
型的值,也就是
FF FF FF FF
。从而导致以下表达式不成立
。
(c =
fgetc(fp)) != EOF /*
读到值为
FF
的字符,误认为
EOF */
也就是说以下循环在没有读完文件
的情况下提前退出
。
while ( (c
= fgetc(fp)) != EOF )
{
putchar(c);
}
综上所述,使用
char
型变量接收
fgetc
等函数的返回值是错误的,我们必须使用
int
型变量接收这些函数的返回值
,然后判断接收到的值是否
EOF
。只有判断发现该返回值并非
EOF
,我们才可以把该值赋值给
char
型变量。
同理,
C++
中,用
char
型变量接收
cin.get()
的返回值也是错误的。不过,把
char
型变量当作参数
传递给
cin.get
则是正确的。例如:
char c =
cin.get(); //
错误,理由同上
char c;
cin.get(c); //
正确
C/C++
误区五:检查
new
的返回值
首先澄清一下,这个误区仅对
C++
成立,这里不过是沿用
“C/C++
误区
”
这个衔头罢了。
我们都知道,使用
malloc/calloc
等分配内存的函数时,一定要检查其返回值是否为
“
空指针
”
(亦即检查分配内存的操作是否成功),这是良好的编程习惯,也是编写可靠程序所必需的。但是,如
果你简单地把这一招应用到
new
上,那可就不一定正确了。我经常看到类似这样的代码:
int* p =
new int[SIZE];
if ( p == 0 ) //
检查
p
是否空指针
return -1;
//
其它代码
其实,这里的
if (
p == 0 )
完全是没啥意义的。
C++
里,如果
new
分配内存失败,默认是抛出异常
的。所以,如果分配成功,
p == 0
就绝对不会成立;而如果分配失败了,也不会执行
if (
p == 0 )
,因为分配失败时,
new
就会抛出异常跳过后面的代码
。如果你想检查
new
是否成功,应该捕捉异常
:
try {
int* p = new
int[SIZE];
//
其它代码
} catch ( const bad_alloc& e ) {
return -1;
}
据说一些老的编译器里,
new
如果分配内存失败,是不抛出异常的(大概是因为那时
C++
还没加入异常机制),而是和
malloc
一样,返回空指针。不过我从来都没遇到过
new
返回空指针的情况。
当然,标准
C++
亦提供了一个方法来抑制
new
抛出异常
,而返回空指针:
int* p = new (std::nothrow) int; //
这样如果
new
失败了,就不会抛出异常,而是返回空指针
if ( p == 0 ) //
如此这般,这个判断就有意义了
return -1;
//
其它代码
/* Font Definitions */
@font-face
{font-family:宋体;
panose-1:2 1 6 0 3 1 1 1 1 1;
mso-font-alt:SimSun;
mso-font-charset:134;
mso-generic-font-family:auto;
mso-font-pitch:variable;
mso-font-signature:3 135135232 16 0 262145 0;}
@font-face
{font-family:宋体;
panose-1:2 1 6 0 3 1 1 1 1 1;
mso-font-alt:SimSun;
mso-font-charset:134;
mso-generic-font-family:auto;
mso-font-pitch:variable;
mso-font-signature:3 135135232 16 0 262145 0;}
@font-face
{font-family:Calibri;
panose-1:2 15 5 2 2 2 4 3 2 4;
mso-font-charset:0;
mso-generic-font-family:swiss;
mso-font-pitch:variable;
mso-font-signature:-520092929 1073786111 9 0 415 0;}
@font-face
{font-family:"/@宋体";
panose-1:2 1 6 0 3 1 1 1 1 1;
mso-font-charset:134;
mso-generic-font-family:auto;
mso-font-pitch:variable;
mso-font-signature:3 135135232 16 0 262145 0;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{mso-style-unhide:no;
mso-style-qformat:yes;
mso-style-parent:"";
margin:0cm;
margin-bottom:.0001pt;
text-align:justify;
text-justify:inter-ideograph;
mso-pagination:none;
font-size:10.5pt;
mso-bidi-font-size:11.0pt;
font-family:"Calibri","sans-serif";
mso-ascii-font-family:Calibri;
mso-ascii-theme-font:minor-latin;
mso-fareast-font-family:宋体;
mso-fareast-theme-font:minor-fareast;
mso-hansi-font-family:Calibri;
mso-hansi-theme-font:minor-latin;
mso-bidi-font-family:"Times New Roman";
mso-bidi-theme-font:minor-bidi;
mso-font-kerning:1.0pt;}
.MsoChpDefault
{mso-style-type:export-only;
mso-default-props:yes;
font-family:"Calibri","sans-serif";
mso-bidi-font-family:"Times New Roman";
mso-bidi-theme-font:minor-bidi;}
/* Page Definitions */
@page
{mso-page-border-surround-header:no;
mso-page-border-surround-footer:no;}
@page WordSection1
{size:595.3pt 841.9pt;
margin:72.0pt 90.0pt 72.0pt 90.0pt;
mso-header-margin:42.55pt;
mso-footer-margin:49.6pt;
mso-paper-source:0;
layout-grid:15.6pt;}
div.WordSection1
{page:WordSection1;}
-->
在此论坛上发现了一些特别的问题,这些问题在其他地方并不存在,猜想是因为这里以学生为主,而学校的教材和教师与
IT
发展脱节严重。
1. C++
虽然主要是以
C
的基础发展起来的一门新语言,但她不是
C
的替代品,不是
C
的升级,
C++
和
C
是兄弟关系。没有谁比谁先进的说法,更重要
的一点是
C
和
C++
各自的标准委员会是独立的,最新的
C++
标准是
C++98
,最新的
C
标准是
C99
。因此也没有先学
C
再说
C++
的说法,也不再(注意这
个
"
不再
"
)有
C++
语法是
C
语法的超集的说法。
2. C++/CLI
和
C#
是微软的,它们与
C
和
C++
没有任何关系,虽然部分语法相似。但哪两种语言不相似呢?都是
abc
这
26
个字母。
3.
不要使用
TC/TC++/BC/CB
等古老的编译器来学习
C/C++
,因为它们太古老了,不支持新的
C/C++
标准。不要使用
CBX /VC++6.0 /VC2005
等对
C/C++
标准支持不好的编译器,虽然这些编译器适合工作,但不适合学习,因为它们中的语法陷阱很多。记住唯一适合学习的编译器是
gcc/mingw
。
[antigloss
注:
Dev-C++
使用的编译器就是
gcc
& g++]
4.
不要用
""
代替
<>
来包含系统头文件
,虽然有些编译器允许你这样做,但它不符合
C/C++
标准。
错误的示例:
#include "stdio.h"
,
#include "iostream"
。
[antigloss
注:习惯上,
<>
用于包含标准头文件
和系统头文件
,
""
用于包含自定义头文件
。标准似乎没有明确规定不准用
""
包含标准头文件和系统头文件。使用
""
包含标准头文件或者系统头文件只能说是一种
不良风格
。
]
5.
不要将
main
函数的返回类型定义为
void
,虽然有些编译器允许你这样做,但它不符合
C/C++
标准。不要将函数的
int
返回类型省略不写,在
C++
中要求编译器至少给一个警告。错误的示例:
void main() {}
,
main() {} [antigloss
注:
C99
和
C++98
都要求编译器对省略
int
至少发出一个警告
]
6.
不要把
VC++
中的
#include "stdafx.h"
贴出来,它是预编译头文件。如同上菜时不要把厨师也放到托盘中。
7. [C++]
不要
#include <
iostream.h>
,不要
#include <
string.h>
,因为它们已经被
C++
标准明确的废弃了,请改为
#include <
iostream>
和
#include <
cstring>
。规则就是:
a.
如果这个头文件是旧
C++
特有的,那么去掉
.h
后缀,并放入
std
名字空间,
比如
iostream.h
变为
iostream
。
b.
如果这个头文件是
C
也有的,那么去掉
.h
后缀,增加一个
c
前缀,比如
string.h
变为
cstring
;
stdio.h
变为
cstdio,
等等。
BTW
:不要把
string
、
cstring
、
string.h
三个头文件搞混淆
BTW
:
windows.h
不是
C/C++
的标准文件,因此它的命名
C/C++
不管。
8.
不要再写
char* p = "XXX"
这种语句,要写成
const char* p = "XXX"
,编译器之所以让前者通过编译是为了兼
容以前的大量的旧代码。
[antigloss
注:这段话对
C++
而言是正确的。但是,目前的
C99
标准似乎并没有定义
"XXX"
一定是常量。
]
BTW
:
const TYPE* p
和
TYPE const* p
是一样的,风格不同而已。
BTW
:
C
语言中也有
const
关键字。
9.
不要在同一条语句中包含一个变量的多个
++/--
,因为它们的解析在
C/C++
标准中没有规定,完全取决于编译器的个人行为。
10. C/C++
是平台无关性语言,因此系统相关的
process/GUI
等不在标准
C/C++
库中。比如
graphics.h
和
windows.h
等是由某个编译器提供的,而不是由
C/C++
提供的。
11. C/C++
只是语言,而且是平台无关性语言。论坛上有部分人甚至认为
C
就是
dos
,
C++
就是
windows
,那么请问
linux
是什么?
12. [C++]
面向对象曾经是设计
C with class
(
C++
的前身)的主要目的,但
C++
不是,
C++
是一个多典范语言。主要支持过程调用、基于对
象、面向对象、泛式编程这四种编程典范。当然还支持
functional, generative,metaprogramming
等典范。
13.
语法学家不是文学家,所以当你学会了一门计算机语言时,你还需要学习数据机构和算法,还需要掌握工具和平台
API
的用法。
14. C/C++
是通用语言,因此语法很复杂,你应当裁减成适合你自己的语法集合,比如裁减成
better C
和
ADT
。
15. C/C++
是通用语言,因此只含通用的库,你应该丰富自己需要的库,比如汽车工业协会有自己的
C/C++
函数
/
类
/
模板库。
C/C++
误区一:
void main()
很多人甚至市面上的一些书籍,都使用了
void
main( )
,其实这是错误的。
C/C++
中从来没有定义过
void
main( )
。
C++
之父
Bjarne Stroustrup
在他的主页上的
FAQ
中明确地写着
The
definition void main( ) { /* ... */ } is not and never has been C++, nor has it
even been C.
(
void main( )
从来就不存在于
C++
或者
C
)。下面我分别说一下
C
和
C++
标准中对
main
函数的定义。
1. C
在
C89
中,
main(
)
是可以接受的。
Brian W. Kernighan
和
Dennis M. Ritchie
的经典巨著
The C programming Language 2e
(《
C
程序设计语言第二版》)用的就是
main( )
。不过在最新的
C99
标准中,只有以下两种定义方式是正确的:
int main( void )
int main( int argc, char *argv[] )
(参考资料:
ISO/IEC
9899:1999 (E) Programming languages — C 5.1.2.2.1 Program startup
)
当然,我们也可以做一点小小的改动。例如:
char
*argv[]
可以写成
char **argv
;
argv
和
argc
可以改成别的变量名(如
intval
和
charval
),不过一定要符合变量的命名规则。
如果不需要从命令行中获取参数,请用
int
main(void)
;否则请用
int main( int argc, char *argv[] )
。
main
函数的返回值类型必须是
int
,这样返回值才能传递给程序的调用者(如操作系统)。
如果
main
函数的最后没有写
return
语句的话,
C99
规定编译器要自动在生成的目标文件中(如
exe
文件)加入
return
0;
,表示程序正常退出。不过,我还是建议你最好在
main
函数的最后加上
return
语句,虽然没有这个必要,但这是一个好的习惯。注意,
vc6
不会在目标文件中加入
return
0;
,大概是因为
vc6
是
98
年的产品,所以才不支持这个特性。现在明白我为什么建议你最好加上
return
语句了吧!不过,
gcc3.2
(
Linux
下的
C
编译器)会在生成的目标文件中加入
return 0;
2. C++
C++98
中定义了如下两种
main
函数的定义方式:
int main( )
int main( int argc, char *argv[] )
(参考资料:
ISO/IEC
14882(1998-9-01)Programming languages — C++ 3.6 Start and termination
)
int main( )
等同于
C99
中的
int
main( void )
;
int main( int argc, char *argv[] )
的用法也和
C99
中定义的一样。同样,
main
函数的返回值类型也必须是
int
。如果
main
函数的末尾没写
return
语句,
C++98
规定编译器要自动在生成的目标文件中加入
return 0;
。同样,
vc6
也不支持这个特性,但是
g++3.2
(
Linux
下的
C++
编译器)支持。
3.
关于
void main
在
C
和
C++
中,不接收任何参数也不返回任何信息的函数原型为
“void foo(void);”
。可能正是因为这个,所以很多人都误认为如果不需要程序返回值时可以把
main
函数定义成
void
main(void)
。然而这是错误的!
main
函数的返回值应该定义为
int
类型,
C
和
C++
标准中都是这样规定的。虽然在一些编译器中,
void main
可以通过编译(如
vc6
),但并非所有编译器都支持
void main
,因为标准中从来没有定义过
void
main
。
g++3.2
中如果
main
函数的返回值不是
int
类型,就根本通不过编译。而
gcc3.2
则会发出警告。所以,如果你想你的程序拥有很好的可移植性,请一定要用
int
main
。
4.
返回值的作用
main
函数的返回值用于说明程序的退出状态。如果返回
0
,则代表程序正常退出;返回其它数字的含义则由系统决定。通常,返回非零代表程序异常退出。下面我们在
winxp
环境下做一个小实验。首先编译下面的程序:
int main( void )
{
return 0;
}
然后打开附件里的
“
命令提示符
”
,在命令行里运行刚才编译好的可执行文件,然后输入
“echo %ERRORLEVEL%”
,回车,就可以看到程序的返回值为
0
。假设刚才编译好的文件是
a.exe
,如果输入
“a && dir”
,则会列出当前目录下的文件夹和文件。但是如果改成
“return -1”
,或者别的非
0
值,重新编译后输入
“a
&& dir”
,则
dir
不会执行。因为
&&
的含义是:如果
&&
前面的程序正常退出,则继续执行
&&
后面的程序,否则不执行。也就是说,利用程序的返回值,我们可以控制要不要执行下一个程序。这就是
int main
的好处。如果你有兴趣,也可以把
main
函数的返回值类型改成非
int
类型(如
float
),重新编译后执行
“a
&& dir”
,看看会出现什么情况,想想为什么会出现那样的情况。顺便提一下,如果输入
a ||
dir
的话,则表示如果
a
异常退出,则执行
dir
。
5.
那么
int main( int argc, char *argv[], char *envp[] )
呢?
这当然也不是标准
C/C++
里面定义的东西!
char
*envp[]
是某些编译器提供的扩展功能,用于获取系统的环境变量。因为不是标准,所以并非所有编译器都支持,故而移植性差,不推荐使用。
C/C++
误区二:
fflush(stdin)
1.
为什么
fflush
(
stdin
)
是错的
首先请看以下程序:
include <stdio.h>
int main
(
void
)
{
int i
;
for
(;;)
{
fputs
(
"Please
input an integer
:
"
,
stdout
);
scanf
(
"%d"
,
&i
);
printf
(
"%d/n"
,
i
);
}
return 0
;
}
这个程序首先会提示用户输入一个整数,然后等待用户输入,如果用户输入的是整数,程序会输出刚才输入的整数,并且再次提示用户输入一个整数,然后等待用
户输入。但是一旦用户输入的不是整数(如小数或者字母),假设
scanf
函数最后一次得到的整数是
2
,那么程序会不停地输出
“Please input an integer
:
2”
。这是因为
scanf
(
"%d"
,
&i
);
只能接受整数,如果用户输入了字母,则这个字母会遗留在
“
输入缓冲区
”
中。因为缓冲中有数据,故而
scanf
函数不会等待用户输入,直接就去缓冲中读取,可是缓冲中的却是字母,这个字母再次被遗留在缓冲中,如此反复,从而导致不停地输出
“Please input an integer
:
2”
。
也许有人会说:
“
居然这样,那么在
scanf
函数后面加上
‘fflush
(
stdin
);
’
,把输入缓冲清空掉不就行了?
”
然而这是错的!
C
和
C++
的标准里从来没有定义过
fflush
(
stdin
)。也许有人会说:
“
可是我用
fflush
(
stdin
)
解决了这个问题,你怎么能说是错的呢?
”
的确,某些编译器(如
VC6
)支持用
fflush
(
stdin
)
来清空输入缓冲,但是并非所有编译器都要支持这个功能(
linux
下的
gcc
就不支持),因为标准中根本没有定义
fflush
(
stdin
)。
MSDN
文档里也清楚地写着
fflush
on input stream is an extension to the C standard
(
fflush
操作输入流是对
C
标准的扩充)。当然,如果你毫不在乎程序的移植性,用
fflush
(
stdin
)
也没什么大问题。以下是
C99
对
fflush
函数的定义:
int fflush
(
FILE
*stream
);
如果
stream
指向输出流或者更新流(
update
stream
),
⑶
艺飧龈
铝髯罱
葱械牟僮鞑皇鞘淙耄
敲
?fflush
函数将把这个流中任何待写数据传送至宿主环境(
host environment
)写入文件。否则,它的行为是未定义的。
原文如下:
int fflush
(
FILE
*stream
);
If
stream points to an output stream or an update stream in which the most recent
operation was not input
,
the
fflush function causes any unwritten data for that stream to be delivered to
the host environment to be written to the file
;
otherwise
,
the behavior is undefined.
其中,宿主环境可以理解为操作系统或内核等。
由此可知,如果
stream
指向输入流(如
stdin
),那么
fflush
函数的行为是不确定的。故而使用
fflush
(
stdin
)
是不正确的,至少是移植性不好的。
2.
清空输入缓冲区的方法
虽然不可以用
fflush
(
stdin
),但是我们可以自己写代码来清空输入缓冲区。只需要在
scanf
函数后面加上几句简单的代码就可以了。
/* C
版本
*/
#include <stdio.h>
int main( void )
{
int i, c;
for ( ; ; )
{
fputs("Please input an integer: ", stdout);
scanf("%d", &i);
if ( feof(stdin) || ferror(stdin) )
{ /*
如果用户输入文件结束标志(或文件已被读完),
*/
/*
或者发生读写错误,则退出循环
*/
/* do something */
break;
}
/*
没有发生错误,清空输入流。
*/
/*
通过
while
循环把输入流中的余留数据
“
吃
”
掉
*/
while ( (c = getchar()) != '/n' && c != EOF ) ;
/*
使用
scanf("%*[^/n]");
也可以清空输入流,
*/
/*
不过会残留
/n
字符。
*/
printf("%d/n", i);
}
return 0;
}
/* C++
版本
*/
#include <iostream>
#include <limits> //
为了使用
numeric_limits
using std::cout;
using std::endl;
using std::cin;
using std::numeric_limits;
using std::streamsize;
int main()
{
int value;
for ( ; ; )
{
cout << "Enter an integer: ";
cin >> value;
if ( cin.eof() || cin.bad() )
{ //
如果用户输入文件结束标志(或文件已被读完),
//
或者发生读写错误,则退出循环
// do something
break;
}
//
读到非法字符后,输入流将处于出错状态,
//
为了继续获取输入,首先要调用
clear
函数
//
来清除输入流的错误标记,然后才能调用
// ignore
函数来清除输入流中的数据。
cin.clear();
// numeric_limits<streamsize>::max()
返回输入缓冲的大小。
// ignore
函数在此将把输入流中的数据清空。
//
这两个函数的具体用法请读者自行查询。
cin.ignore( numeric_limits<streamsize>::max(), '/n' );
cout << value << '/n';
}
return 0;
}
C/C++
误区三:强制转换
malloc()
的返回值
首先要说的是,使用
malloc
函数,请包含
stdlib.h
(
C++
中是
cstdlib
),而不是
malloc.h
。因为
malloc.h
从来没有在
C
或者
C++
标准中出现过!因此并非所有编译器都有
malloc.h
这个头文件。但是所有的
C
编译器都应该有
stdlib.h
这个头文件。
在
C++
中,强制转换
malloc()
的返回值是必须的,否则不能通过编译。但是在
C
中,这种强制转换却是多余的,并且不利于代码维护。
起初,
C
没有
void
指针,那时
char*
被用来作为泛型指针(
generic pointer
),所以那时
malloc
的返回值是
char*
。因此,那时必须强制转换
malloc
的返回值。后来,
ANSI C
(即
C89
)
标准定义了
void
指针作为新的泛型指针。
void
指针可以不经转换,直接赋值给任何类型的指针(函数指针除外)。从此,
malloc
的返回值变成了
void*
,再也不需要强制转换
malloc
的返回值了。以下程序在
VC6
编译无误通过。
#include <
stdlib.h>
int main( void )
{
double *p =
malloc( sizeof *p ); /*
不推荐用
sizeof( double ) */
free(p);
return 0;
}
当然,强制转换
malloc
的返回值并没有错,但画蛇添足!例如,日后你有可能把
double *p
改成
int *p
。这时,你就要把所有相关的
(double *) malloc (sizeof(double))
改成
(int
*)malloc(sizeof(int))
。如果改漏了,那么你的程序就存在
bug
。就算你有把握把所有相关的语句都改掉,但这种无聊乏味的工作你不会喜欢吧!不使用强制转换可以避免这样的问题,而且书写简便,何乐而不为呢?使用以下代
码,无论以后指针改成什么类型,都不用作任何修改。
double *p =
malloc( sizeof *p );
类似地,使用
calloc
,
realloc
等返回值为
void*
的函数时,也不需要强制转换返回值。
参考资料:
ISO/IEC
9899:1999 (E) Programming languages — C 7.20.3.3 The malloc function
ISO/IEC 9899:1999 (E) Programming languages — C P104 (6.7.2.2)
C/C++
误区四:
char c = getchar();
getchar
由宏实现:
#define getchar() fgetc(stdin)
。
getchar
有一个
int
型的返回值
.
当程序调用
getchar
时
.
程序就等着用户按键
.
用户输入的字符被存放在键盘缓冲区中
.
直到用户按回车为止
(
回
车字符也放在缓冲区中
).
当用户键入回车之后
,getchar
才开始从
stdio
流中每次读入一个字符
.getchar
函数的返回值是用户输入的第一个字
符的
ASCII
码
,
如出错返回
-1,
且将用户输入的字符回显到屏幕
.
如用户在按回车之前输入了不止一个字符
,
其他字符会保留在键盘缓存区中
,
等待后续
getchar
调用读取
.
也就是说
,
后续的
getchar
调用不会等待用户按键
,
而直接读取缓冲区中的字符
,
直到缓冲区中的字符读完为后
,
才等待用户按
键
.
getch
与
getchar
基本功能相同
,
差别是
getch
直接从键盘获取键值
,
不等待用户按
回车
,
只要用户按一个键
,getch
就立刻返回
,
getch
返回值是用户输入的
ASCII
码
,
出错返回
-1.
输入的字符不会回显在屏幕上
.getch
函数常用于程序调试中
,
在调试时
,
在关键位置显示有关
的结果以待查看
,
然后用
getch
函数暂停程序运行
,
当按任意键后程序继续运行
.
这个版本忽略了个重点,
getch()
是非缓冲输入函数,就是不能用
getch
()来接受缓冲区已存在的字符,如以下
C++
程序,
int
i;while
(
cin>>i);cin.clear();getchar();
运行时如果输入
1 2 3
a
时必须用
getchar
()才能在后面程序获得正常输入,即使先前已经恢复流了,此处用
getch
()是万万不行的。
另外补充个函数,
getche()
,这个函数与前两上类似,功能也相近,都是输入一个字符,返回值同样是输入字符的
ASCII
码,但不同的是,此函数在输入后立即从控制台取字符,不以回车为结束
(
带回显
)
许多初学者都习惯用
char
型变量接收
getchar
、
getc
,
fgetc
等函数的返回值,其实这么做是不对的,并且隐含着足以致命的错误
。
getchar
等函数的返回值类型都是
int
型,当这些函数读取出错
或者读完文件后
,会返回
EOF
。
EOF
是一个宏,标准规定它的值必须是一个
int
型的负数常量
。通常编译器都会把
EOF
定义为
-1
。问题就出在这里,使用
char
型变量接收
getchar
等函数的返回值会导致对
EOF
的辨认出错,或者错把好的数据误认为是
EOF
,或者把
EOF
误认为是好的数据。例如:
int c; /*
正确。应该使用
int
型变量接收
fgetc
的返回值
*/
while ( (c = fgetc(fp)) != EOF )
{
putchar(c);
}
如上例所示,我们很多时
候都需要先用一个变量接收
fgetc
等函数的返回值,然后再用这个变量和
EOF
比较,判断是否已经读完文件。上面这个例子是正确的,把
c
定义为
int
型保证了它能正确接收
fgetc
返回的
EOF
,从而保证了这个比较的正确性。但是,如果把
c
定义为
char
型,则会导致意想不到的后果
。
首先,因为
fgetc
等函数的返回值是
int
型的,当赋值给
char
型变量时,会发生
降级
,从而导致数据截断。例如:
---------------------------------
|
十进制
|
int | char |
|--------|--------------|-------|
| 10 | 00 00 00 0A | 0A |
| -1 | FF FF FF FF | FF |
| -2 | FF FF FF FE | FE |
---------------------------------
在此,我们假设
int
和
char
分别是
32
位和
8
位的。由上表可得,从
int
型到
char
型,损失了
3
个字节的数据。而当我们要拿
char
型和
int
型比较的时候,
char
型会自动
升级
为
int
型。
char
型升级为
int
型后的值会因为它到底是
signed char
还是
unsigned
char
而有所不同。不幸的是,如果我们没有使用
signed
或者
unsigned
来修饰
char
,那么我们无从知晓
char
到底是指
unsigned char
还是指
signed char
,因为这是由编译器决定
的。不过,无论
char
是
signed
的也好,
unsigned
的也罢,都不能改变使用
char
型变量接收
fgetc
等函数的返回值是错误
的这个事实。唯一能改变的是该错误导致的后果。前面我们说了,
char
型和
int
型比较时,
char
会自动升级为
int
,下面我们来看看
signed char
和
unsigned char
在转换成
int
后,它们的值有什么不同:
---------------------------------------
| char | unsigned |
signed |
|-------|---------------|-------------|
| 10 | 00 00 00 0A | 00 00 00 0A |
| FF | 00 00 00 FF | FF FF FF FF |
| FE | 00 00 00 FE | FF FF FF FE |
---------------------------------------
由上表可知,当
char
是
unsigned
的时候,其转换为
int
后的值是正数
。也就是说,假如我们把
c
定义为
char
型变量,而编译器默认
char
为
unsigned char
,那么以下表达式将永远成立
。
(c = fgetc(fp)) != EOF /* c
的值永远为正数,而标准规定
EOF
为负数
*/
也就是说以下循环是一个死循环
。
while ( (c
= fgetc(fp)) != EOF )
{
putchar(c);
}
读到这里,可能有些读者朋友会说:
“
那么我明确把
c
定义为
signed char
型的就没问题了吧!
”
很遗憾,就算把
c
定义为
signed char
,仍然是错误的。假设
fgetc
等函数读到一个字节的值为
FF
,那么返回值就是
00 00 00 FF
。把这个值赋值给
c
后,
c
的值变成
FF
。然后
c
的值为了和
EOF
比较,会自动升级为
int
型的值,也就是
FF FF FF FF
。从而导致以下表达式不成立
。
(c =
fgetc(fp)) != EOF /*
读到值为
FF
的字符,误认为
EOF */
也就是说以下循环在没有读完文件
的情况下提前退出
。
while ( (c
= fgetc(fp)) != EOF )
{
putchar(c);
}
综上所述,使用
char
型变量接收
fgetc
等函数的返回值是错误的,我们必须使用
int
型变量接收这些函数的返回值
,然后判断接收到的值是否
EOF
。只有判断发现该返回值并非
EOF
,我们才可以把该值赋值给
char
型变量。
同理,
C++
中,用
char
型变量接收
cin.get()
的返回值也是错误的。不过,把
char
型变量当作参数
传递给
cin.get
则是正确的。例如:
char c =
cin.get(); //
错误,理由同上
char c;
cin.get(c); //
正确
C/C++
误区五:检查
new
的返回值
首先澄清一下,这个误区仅对
C++
成立,这里不过是沿用
“C/C++
误区
”
这个衔头罢了。
我们都知道,使用
malloc/calloc
等分配内存的函数时,一定要检查其返回值是否为
“
空指针
”
(亦即检查分配内存的操作是否成功),这是良好的编程习惯,也是编写可靠程序所必需的。但是,如
果你简单地把这一招应用到
new
上,那可就不一定正确了。我经常看到类似这样的代码:
int* p =
new int[SIZE];
if ( p == 0 ) //
检查
p
是否空指针
return -1;
//
其它代码
其实,这里的
if (
p == 0 )
完全是没啥意义的。
C++
里,如果
new
分配内存失败,默认是抛出异常
的。所以,如果分配成功,
p == 0
就绝对不会成立;而如果分配失败了,也不会执行
if (
p == 0 )
,因为分配失败时,
new
就会抛出异常跳过后面的代码
。如果你想检查
new
是否成功,应该捕捉异常
:
try {
int* p = new
int[SIZE];
//
其它代码
} catch ( const bad_alloc& e ) {
return -1;
}
据说一些老的编译器里,
new
如果分配内存失败,是不抛出异常的(大概是因为那时
C++
还没加入异常机制),而是和
malloc
一样,返回空指针。不过我从来都没遇到过
new
返回空指针的情况。
当然,标准
C++
亦提供了一个方法来抑制
new
抛出异常
,而返回空指针:
int* p = new (std::nothrow) int; //
这样如果
new
失败了,就不会抛出异常,而是返回空指针
if ( p == 0 ) //
如此这般,这个判断就有意义了
return -1;
//
其它代码
相关文章推荐
- C/C++ 常见误区一
- C/C++ 常见误区
- C/C++ 常见误区
- 基础知识:C/C++ 常见误区
- C/C++ 常见误区
- C/C++ 常见误区
- C/C++ 常见误区
- C/C++ 常见误区
- C/C++基础之解答C/C++常见误区
- (转)C/C++常见误区
- C/C++基础之解答C/C++常见误区
- (转)C/C++常见误区
- 基于C/C++ 常见误区详解
- c/c++常见误区
- C/C++ 常见误区
- C/C++程序员应聘常见面试题深入剖析(2)
- VS C++ 常见调试错误
- 常见的几种排序算法(java和C++版)(参考《算法》)
- C++常见面试
- C++ 语法实验室之指针、常量const、字符串和等号初学误区理解