现在的位置: 首页 > 综合 > 正文

C和C++之间有一点区别

2013年10月02日 ⁄ 综合 ⁄ 共 8630字 ⁄ 字号 评论关闭

 

一、注释

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

                x = a / 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

跟三元转义符相似,C++里面还有一种token替代语法,跟三元转义符相比,它们不是纯文本替换,它们自己是token的一种,可以算作对应token的别名,数量则稍微多了点:

替代的 原有的 替代的 原有的 替代的 原有的
<%         {        and      &&          and_eq    &=
%>         }      bitor       |           or_eq        |= 
<:         [         or         ||          xor_eq      ^= 
  :>           ]        xor    ^            not           !
%:       #        compl    ~     not_eq     !=

                                                                                                                      ##         bitand       &    

  这个规则不会替换字符串和注释中的符号。

三、引用(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

    在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比起来,它是低效的,在大规模的数据输入输出的时候,这种差距就显示出来了。

 

 

【上篇】
【下篇】

抱歉!评论已关闭.