Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2295885
  • 博文数量: 218
  • 博客积分: 5767
  • 博客等级: 大校
  • 技术积分: 5883
  • 用 户 组: 普通用户
  • 注册时间: 2008-03-01 14:44
文章存档

2012年(53)

2011年(131)

2009年(1)

2008年(33)

分类: WINDOWS

2011-10-18 12:34:14

[转]C++与C语言细节差异分析
 

C和C++是很流行的语言,目前大学程序设计基础课大部分用的是C或者C++,但是由于种种原因,使用者对这两种语言的了解不够,本文指出,C++虽然是从C发展过来的,但是除了一些明显的不同,他们存在着细节上的差异。本文通过简要分析这些差异来说明这个观点。

C++是从“带类的C”发展过来的。在此基础上发展成为面向对象的语言,这个可以说是C++与C最大的差别。面向对象的讨论是一个很大的话题,涉及面向对象的讨论将使问题复杂化,也超出了本文的范围,在后面的介绍中,涉及面向对象的东西将尽量避开。

C++的“点心”

一、注释

                C++的注释允许采取两种形式。第一种是传统C采用的/*和*/,另一种新采用的则是//。同时允许这两种注释风格,程序员可以选用他们所喜欢的形式。大部分的程序员,包括C++的设计者更喜欢//的注释风格。在小段代码的注释,//显然比/* */方便很多。然而,在注释掉很大段代码的时候,/* */就要比//方便了。
                设计者认为,这种注释只是C++增加的一个次要的功能。
                增加//的注释风格,会导致一些C与C++的不兼容,例如:

                x = a //* divide */ b

               在C中表示x = a / b,在C++中表示x = a。这个例子被大部分程序员认为没有什么实际价值。但它的确表现了由//引起的一些不兼容。

二、布尔类型与逻辑运算

在C中没有布尔类型,一般布尔值就用整型代替,C++增加了布尔类型bool,同时也增加常量true和false。实际上,逻辑类型的变量在程序设计中是一个无法避免的东西,而C的程序员也经常通过整型用各种方法“造出”“布尔”类型。于是,C++索性就增加了bool数据类型。
而在一些人用的键盘上,没有如&,|,^等键,于是,C++增加了一些关键字,见表1:

关键字 含义
and     &&(逻辑与)
or    ||(逻辑或)
not !(逻辑非)
not_eq !=(逻辑不等于)
bitand &(按位与)
and_eq &=(按位与赋值)
bitor |(按位或)
or_eq |=(按位或赋值)
xor ^(按位异或)
xor_eq ^=(按位异或赋值)
compl ~(按位非)

表1

三、引用(Reference)的出现与函数调用

                引用的出现主要是为了支持运算符的重载。不过他在函数调用方面也有不小的好处。通常,在C中,想要改变参数的值可以通过传递指针来实现,现在,有了引用,这样的做法显得很方便。比如:

                int f(int &x)
                {
             return ++x;
                }
                f(y);

                上面,当f(y);执行完,y的值增加了1,在C中,我们会这么做:
                int f(int *x)
                {
             return ++(*x);
                }

                这只是一个很小的例子,但是,用引用的函数“干净”很多。在不涉及面向对象编程的时候,我更喜欢把引用当作C++给我们的“点心”。

四、C++定义块的消除

                C规定,变量的声明块必须在语句执行块的前面。但是C++的作者认为,这样会降低程序的可读性,于是,C++的声明和执行语句可以混合起来。比较典型的例子是:

                for (int i = 0; …;…)…[2]
    
                这样,变量i在这层for循环结束后就结束后就被释放,不会与前面或者后面用到的某个变量i混淆起来。

五、强制转换

                C提供了一种类型的强制转换,形如(type)的方法,C++中保留了C的强制转换的方法,对于相同功能的强制转换,C++还增加了一种很像函数调用的强制转换的方法,例如可以这样写:

                int x;
                long y;
                x = (int)y;

                在C++中,也可以写成:

x = int(y);

或许,强制转换可以体现C的灵活性,但是,它也是一种很不安全的做法。C++增加了与强制转换有关的四个关键字,如表2:

static_cast “行为良好的”和“行为合理的”强制转换,包括可能不需要的强制转换(比如自动类型转换的)
const_cast 转换掉const和/或volatile
reinterpret_cast 转换成完全不同的含义,关键是你需要转换回去并且安全地使用。转换成的类型一般是你想要耍点小把戏或者其他神秘的目的。这是最危险的强制转换
dynamic_cast 安全类型向下转换(与类有关)

表2

                C++的转换在类上有很多应用,比如,dynamic_cast是主要应用于类的转换(本文不讨论)。而其他三种转换,不涉及类时,与C对应转换含义上没有多少区别,但是,由于用了这样的关键字,所以,C++程序员很容易在程序里找到相应的转换,并且清楚是怎么转换的。

六、struct含义的变化

对struct来说,struct在C++中已经变成了class,只不过,struct默认成员是公用的(public),而class默认成员是私用的(private)。于是,struct在C和C++中的含义也不同了。比如:

struct
{
             int x, y;
             int fun(int);
};

                这个定义在C中是不合法的,而在C++中是合法的。

                之所以把它称之为“点心”,是因为即使在面向过程的编程中,也可以利用一点点类的方法来使编程方便一些。有兴趣的读者可以用类写一些递归(如线段树的构造)程序。
                在用struct定义变量的时候,C与C++也有一点区别,在C中,定义变量时struct是不能省略的,但是C++中是可以省略struct的(包括class也能省略)。比如:

                struct str_test
                {
             //definition
                };
                str_test b;

                在C++中,是允许的,但是在ANSI C中不允许这样做。

C++对C的不向下兼容

C++:尽可能地与C靠近,但又不过分地近——《C++语言的设计和演化》

一、C++字符常量的出现

在字符的处理上,C和C++也是有不同的。在C中没有字符常量,而在C++中有字符常量,可以用这样的代码来验证:

printf(“%d\n”, sizeof(‘x’));

用C编译,输出的结果是sizeof(int)的值,而用C++编译,输出的是1。


二、C++对enum严格的类型检查

C++的类型检查要比C严格很多,这点在下文还会有体现。我们先以数据类型enum为例,在C中,enum类型会直接转化成为int,实际上,在C++中也是,但是,为了保证运算时类型的对应,C++禁止了一些操作,比如:

enum {a, b, c, d, e, f, g, h, i} x;

x = a;

                x++;

在C中是允许的,但是在C++中是不允许的。因为增加运算需要两次类型转换,在C++中,一次是合法的,另外一次是不合法的。
把enum类型与int区分开来,这在实现函数重载上也会有很大的好处(下文会讲到重载)。


三、C++对指针运算的一些限制

在指针方面,可以体现C++对类型的严格要求,C++中void*不能直接被赋值给某个指针,而需要强制转换。如:

int *p;

void *a;

p = a;

在C中是可以编译通过的,在C++编译器处理的时候会出现编译错误。另外,C++中void *类型是不能进行指针算术运算的。比如上例中,表达式:

a + 1;

在C++中是不合法的,而在C中是合法的,与

(void *)((int)a + 1);

等价。

C++种禁止的另外一项是指针的减法,而在C中,指针的减法是被允许的。例如:

int a, b;

在C中,&b - &a的值为((int)&b – (int)&a) / sizeof(int),在C++中,这是不允许的。


四、函数使用的一些不兼容

                在函数的一些规则细节方面,C++与C是有些不同的。通常,我们这样定义函数:

                type function_name (parameter list) { declarations statements}
    
                这种方法不管在C或者C++中都适用。但是C语言有一些是可以省略的,比如,type,默认为int,但是C++中不可以省略。另外,parameter list可以不指定变量的数据类型,而在parameter list与declarations之间定义参数的数据类型,例如:

                f(x) int x;{…}

                这在C中是允许的,但是,C++里会出现这样的编译错误:

`x' was not declared in this scope

ISO C++ forbids declaration of `f1' with no type

syntax error before `int'

                另外一个数据类型规则上的不同是,对于没有parameter list的为空的解释,C认为,这样的parameter list表示,这个函数可以有任意多个任意类型的参数。但是C++则认为,这样的parameter list表示,这个函数没有任何参数。当然,C++的这种解释,C可以把parameter list定为void。
                还有一点不同的是,虽然函数定义了返回类型(即type),但是在C中,函数可以不返回任何类型。比如:

                int f(int x)
                {
             return ;
                }

                在C中是允许的,但是,C++编译的时候会说:

                return-statement with no value, in function declared with a non-void return type

                简单地说,在数据类型这方面,C++要比C严格很多,而这种严格(特别是参数的一些方面),保证了函数重载可以出现在C++中(下文将提到)。
                除了数据类型,函数方面,C++还有一些比C严格的地方。在C中函数可以没有声明而直接调用,但是C++保证,调用函数之前必须声明函数。即C很相信程序员,但是C++却不。
                比如这样:

                int main()
                {
             f();
             return 0;
                }

                这样的程序在C中是不会出现编译错误的,只不过,如果这个函数在连接的库中没有定义,连接的时候会说:

                undefined symbol _f in module ……[3]

                但是,在C++中,编译器会说:

                'f' undeclared (first use this function)

                关于函数,C++提供了两个功能,重载与默认参数,下文会提到。

C功能的C++新方法

一、#define与const

#define是C和C++的预处理指令,在C中,用#define可以用来定义常量,由此增加程序的可读性,降低维护的成本。
但是const的出现,使得#define定义常量变得不受欢迎。因为#define只是做简单的文本替换,从定义的那刻起,一直到程序的末尾或者有指令#undef停止#define继续文本替换。但是const具有清晰的作用域的概念,因此使用const定义常量被认为是更好的方法。

例如:

#define b 10
int f()
{
                int b;
}

会出现这样的编译错误:

parse error before numeric constant

对于初学者来说,可能对这个编译错误莫名奇妙。因此,尤其是在C++编程中,用const被认为一种好的编程习惯。
不过,对于关键字const的处理方法,C与C++也是不同的,C中const的作用域默认是全局的,而C++中const的作用域默认是内部的。如果要在C++中使const的作用域是全局,那么可以用extern来声明。另外,在C中,const是有存储空间的,但是C++中,一个const有没有存储空间取决于这个const怎么用,如果没有必要,就不会给这个const开辟空间,比如仅仅需要用某个值来代替这个变量名(和C的#define很像)。
                但是,一个变量被声明为const,并不是说这个变量的值不能被改变,而只是,编译器尽可能不让程序员改变变量的值。但是,由于C的类型检查非常宽松[4],所以,改变一个const的值是很容易的。如下的程序:

const int a = 10;
int *p = &a;
(*p)++;

在C中,这样就把a的值改变了,虽然这样做是危险的。但是用C++编译这个程序的时候,会出现这样的编译错误:

invalid conversion from `const int*' to `int*'
                另外的一些不同是,C中,const可以不赋初值,但是C++中const一定要被初始化。如
                const int a;
                在C中是被允许的,但是在C++中是不允许的。而且,C中的const可以不指定类型,默认为int,而C++在定义或者声明const的时候,必须制定变量的类型。


二、#define与inline[5]

                在C中,利用#define增加程序可读性的另外一个应用是用#define来“定义函数”,例如:
                #define f(x) (x) * (x)
                在以后的程序中,如果可以这样“调用”:
                y = f(3);
                它的好处是,碰到比较复杂的表达式,写程序的时候方便,读的时候也方便;另外一方面,由于这里只是作文本替换,而不是另外一个函数来实现,所以,速度比用函数快。
                但是,有几点是需要小心的,首先,由于#define只是作文本替换,所以写这样的“函数”的时候要格外小心,如上例,如果写成:
                #define f(x) x * x

                那么如果这样“调用”:
    
                y = f(2 + 1);
    
                那么就变成了

y = 2 + 1 * 2 + 1;


并不是想要的
y = (2 + 1) * (2 + 1);

另外一个更加限制#define定义函数的使用的是,这样的文本替换,如果“参数”本身是一个表达式,那么,这个表达式所产生的作用很可能是无法预料的,比如,对上例

#define f(x) (x) * (x)

如果程序员这样调用:

y = f(++x);

就变成了:

y = (++x) * (++x);

一般程序员都只想让x加1,但是调用完f(x)之后,x的值增加了2。
在C中,这两个问题是很严重的,因为这样的错误很难查出来。在C++中,inline函数的出现很好得解决了这些问题。
inline在C++中出现的必要性并不是因为以上所说的原因,而是因为类的使用的需要。然而,由于涉及面向对象,本文并不讨论inline在类上的应用。本文讨论的是,inline函数的出现将很好得解决。
inline函数和普通的函数差不多。区别是,普通的函数通过调用来实现函数的功能,而inline函数直接把函数的代码编译,嵌入使用的地方。inline函数的定义与#define一样简单,只要在定义在使用之前。例如:

inline int f(int x)
{
                return ++x;
}

值得注意的是,inline函数的声明对inline与否并没有作用。另外,代码是否被嵌入使用的地方(即inline与否),也并不是完全取决于inline关键字,由于某些函数是不可能做到inline的(比如,递归调用的函数),因此,inline关键字只是告诉编译器,如果可能的话,inline吧。
inline函数的优点在于,执行速度比普通的函数快,没有#define“定义函数”带来的烦恼。它的缺点是会加大编译后的目标文件,所以inline函数也是不能不加节制地使用的。

二、函数重载,运算符重载和默认参数

                重载是否要加入到C++中,设计者犹豫了一下。实现(语言设计)困难、教学困难、代码阅读困难和执行效率会低是主要原因,后来,在证明了没有这些不利因素之后,他把重载加入了C++中。
                函数重载可以让程序员使用名字相同的函数,然后实现类似的功能,比如,要计算一个数(整型和实型)的平方,C++中可以这样写:

                int sqr(int x)
                {
             return x * x;
                }
                double sqr(double x)
                {
             return x * x;
                }

                在C中,如果要实现这样的功能,则需要两个不同的函数名字,调用的时候写需要注意,是以什么数据类型作为参数,从而调用不同的函数。[6]
                重载的另一方面是运算符的重载,但实际上,运算符的重载是用函数来实现的,运算符的重载使得程序阅读起来很方便。因此有人把运算符的重载称为“语法点心”。但是,为了避免不必要的麻烦,C++对运算符的重载加了很大的限制,比如,只能对已经存在的运算符进行重载,另外,重载运算符不能违反该运算符原来的含义。
                函数重载的功能有时候可以用另外的一种方法来实现,即默认参数。[7]比如这样的例子:

                int f(int x)
                {
             return x;
                }
                int f(int x, int y)
                {
             return x * y;
                }

                那么,有了默认参数,我们可以这样做:

                int f(int x, int y = 1)
                {
             return x * y;
                }

                这样可以只写一个函数,效果是相同的。调用的时候,可以有一个参数,也可以有两个。
                默认参数也有严格的规定,有默认值的参数只能放在参数表的最后。
                支持重载是好事吗?Java并没有引入重载。有这样的一种观点:重载是危险的。因为这样会使程序员不知道自己调用的是什么函数,特别是C++中含有的自动类型转换,这样使编程变得需要很小心翼翼。


三、C++的iostream与C的stdio

                iostream和stdio的差异实际上并不小,也涉及到C和C++的库,但是由于它们太常用了,因此这里提一下。
                C的stdio通过让程序员对文件指针的操作(stdin和stdout可以认为是特殊的指针),一个个函数来实现对文件的操作和数据的输入输出。
                stdio似乎已经足够了,因为它很高效。而且,也是比较方便的。但是C++设计者认为,它不够类型安全。因此设计了类型安全且高效的iostream,一部分原因是因为它是用类做的,至少有constructors和destructors保证一些操作(比如,不必担心忘了把文件关掉,constructor和destructors是面向对象的内容,本文不讨论)。
                不过,我们可以这样认为,大部分时候,iostream不只是类型安全,而且,很方便,比如,我们要输入变量x的值。在c中我们需要知道他的数据类型,至少要知道,以什么样的数据类型输出,比如,我们要输出整数x,用stdio,这样写:

                printf(“%d”, x);
    
                但是,用iostream,连x的类型都不需要管,这样写就可以了:
    
                cout << x;
    
                这是个简单的例子,但是已经看出来iostream这种情况下的方便了。
                另外一个问题是,iostream真的高效吗、足够高效吗?答案是,它很高效,但是与stdio比起来,它是低效的,在大规模的数据输入输出的时候,这种差距就显示出来了。


C语言,还是C++语言?

using namespace std开始说起

                现在的程序设计基础的教学,用什么语言呢?刚开始的时候,教员会告诉学生,在写程序之前,写上这样的两句:
                #include
                using namespace std;
                在刚开始的时候,似乎不需要去理解这是为什么,但这也就是C++增加的内容——名字空间(namespace),这是C语言没有的。
                名字空间的由来是因为大型程序设计中,名字(变量、函数等)会很多,一不小心就会引起混乱、冲突。于是要寻求一种解决方案,如果让程序员来刻意注意这个问题,就增加了编程成本,也不方便。于是由C++语言本身来解决这个问题,于是出现了名字空间。让程序员可以把名字放在不同的名字空间里,方便引用,也避免冲突。using namespace std;就是指能在当前的程序中可以直接用名字空间std里面的名字;否则,可以通过std::name的方法使用std里面的名字,std是很多C++系统的库的提供的一个名字空间,包含了一些常用的名字。
                程序设计基础课程一般不会在乎所用的语言,于是索性将C与C++混淆,实际上,一般程序设计基础课程用的是C++的编译器,但是教的都是面向过程的内容,换句话说没,用的大部分是C++的C真子集。

1. 关于变参的宏定义:

DECLARE(status_t) log_add(log_level_e level, char *format, ...);
#define ADDLOG(args,...) log_add(LL_INFO, ##args)

#ifdef  DEBUG
#define debug(fmt,args,...)      printf (fmt ,##args)
#else
#define debug(fmt,args...)
#endif  /* DEBUG */
C中可以不用那个逗号:#define debug(fmt,args...)      printf (fmt ,##args)

但是在VC中必须要,否则提示error C2010: “.”: 宏形参表中的意外

有意思的是:g++是必须不能要逗号的;vc是一定要逗号的;

 

小结

                通过上面的比较,我们可以看出,C++不仅仅是由C发展而来的面向对象程序设计语言,C并不是C++的真子集。另外,C++对C所做的改动也都是有原因的。从事物发展的角度看,C++并不完美(比如强制转换),但是可以从这个角度看出程序设计语言发展的一些规律:在进步,但难以做到完美。

参考文献

Al Kelley & Ira Pohl. A book on C: Programming in C (Forth Edition). 机械工业出版社 2004
Bruce Eckel. Thinking in C++ (Second Edition) 机械工业出版社2002
Scott Meyers. Effective C++ HTML版
Bjarne Stroustrup 著, 裘宗燕 译《C++程序设计语言》 机械工业出版社 2002
Bjarne Stroustrup 著, 裘宗燕 译《C++语言的设计和演化》机械工业出版社 2002

你可以使用这个链接引用该篇文章:http://publishblog.blogchina.com/blog/tb.b?diaryID=6171006

阅读(4560) | 评论(0) | 转发(3) |
给主人留下些什么吧!~~