c++缺陷与不足

C++的缺陷与不足

摘 要:

    C++无疑是一门优秀的程序设计语言,很多文献都有介绍[1~4]。任何一门程序设计语言的诞生,都有其强烈的时代背景,C++也不例外。没有必要评价语言设计者做出的各项设计决策,但应该依据现代软件工程的指导思想去理解和把握语言的本质与核心。要想彻底地掌握一种语言,不但需要知道它的优点和长处,也要了解它的缺陷和不足。软件开发尤其是编码阶段的效率很大程度上取决于开发人员对语言的了解程度。理解语言设计的原则,洞悉语言的缺陷,能够帮助人们在实践中绕开这些“绊脚石”,实现高效编程的目标。

1 内存管理问题

    在C语言中,虽然可通过库函数malloc()和free()来分配和释放内存,但却是非常危险的内存管理方法[5]。我们知道,malloc函数可分配指定大小的内存空间,并返回该内存块的首地址。free函数则根据指定的地址释放内存块。这一切看似很正常,但却隐藏着“杀机”:

    ①内存分配未成功(失败),却使用了它。初学者经常犯这样的错误。

    ②内存分配成功,但尚未初始化就引用它,误以为内存的缺省初值全为零。

    ③内存分配成功并且已经初始化,但操作越过了内存块的边界。这是非常危险的行为!

    ④忘记了释放不再使用的内存,造成内存损耗,降低整个系统的运行速度,并且这些没用的内存垃圾还要被虚拟存储系统对换到外存上。严重时,可用的内存资源被耗尽,导致程序崩溃。

    ⑤释放了内存块后,没有将指针设置为NULL,导致产生“野指针”。“野指针”的存在将带来非常大的安全隐患(有可能使用已释放的内存块)。

    ⑥释放一块未被分配的内存空间,系统自然也会出问题。

    ⑦再次释放已释放的内存块,会造成系统的崩溃。C++为了与C完全兼容,允许使用库函数malloc()和free(),这就给高质量的程序设计带来了很大的安全隐患。虽然C++增加了两个运算符new和delete,但其用法与malloc和free相同,它们返回的仍然是对象的地址,而不是对象的“句柄”。当分配的对象在程序运行过程需要在内存中移动时,其地址就会发生变化。如果使用句柄就不用考虑这个问题,也便于“自动垃圾回收”.发生内存错误非常麻烦,编译时不能发现这些错误,程序运行时才出现问题。并且大都没有确定的故障现象,甚至时隐时现,严重时可导致系统崩溃,轻微时又没有任何反应,给程序维护增加了非常大的难度。

2 面向对象方面的问题

    211 向下兼容面向对象技术是软件界非常有意义的一项技术,许多C++文献都认为C++很好地顺应了这种潮流。但遗憾的是C++并不能真正满足面向对象编程,原因是向下兼容。向下兼容通常被当作一个优秀的特性,因为它能减轻程序员学习新语言的难度,并且将许多经过测试的代码与已有的市场份额相结合会确保成功。但对C++而言,与C向下兼容是它最讨人喜欢的特性又是它最脆弱的一环。为了与C兼容,C++被迫作出了很多重大的设计妥协,导致语言过分华丽,过分复杂。C++作者BjarneStroustrup也意识到了这一点:“无论是从语法还是语义角度,与C兼容的子集(尤其是类型转换的部分)都是C++中最欠合理优雅的部分”[6]。总之,C++过于依赖C,它是面向对象与传统的面向过程相结合的产物。

    212 数据类型与C一样,C++中包含了int,float,double,char等简单的数据类型。一种严格的面向对象语言,它的所有成分都应该和类或者对象有关的,也就是说,严格的面向对象语言中不应该有基本数据类型的存在。但事实上,这些简单的数据类型应用得太广、太多了,为了简化编程,C++中保留了与面向过程语言一致的、与类无关的基本数据类型。从这个意义上来说,C++继承了面向过程的一些语言机制,并不是严格意义上的、完全面向对象的语言。        213 程序结构在纯面向对象的程序结构中,不应该有独立的主函数或子函数,所有的函数都必须封装到类中。程序中除了对象就是消息,对象间靠发送消息来完成计算。对象之间无主次之分,也不便于分清主次。例如,组成某个程序的若干个对象都是某一个类的实例,他们都是平等的,不可能存在主次之别;即便这些对象源于不同的类,也不可能说某一个类的对象处于控制地位,而另一个类的对象处于被控制地位。而在C++程序结构中,有且必须有一个main函数,这一点是为了与面向过程的C程序结构保持一致,但却与面向对象的思想不吻合。

    214 多继承C++支持多继承,但多继承会带来一些问题。例如,如何处理从两个类中继承的具有相同名字的实体?它们之间是否兼容?如果是的话,那他们是否应该被合并为一个实体?如果不兼容,那应该如何区分它们?…,类似这样的问题还有很多。

    215 多态性在C++中,当子类改写重定义(overrideredefine)了在父类中定义了的函数时,关键字virtual使得该函数具有了多态性,但是virtual关键字也并不是必不可少的(只要在父类中被定义一次就行了)。这样,问题就产生了:如果设计父类的人员不能预见到子类可能会改写哪个函数,那么子类就不能使这个函数具有多态性。这对C++来说应该是一个很严重的缺陷[6]。

 3 安全性问题

    程序的安全性[2,3]是一个非常重要的概念,不安全的程序会带来许多的隐患,甚至会造成系统的崩溃。而程序的安全性是和语言紧密相关的,如果程序设计语言提供了非常安全的机制,则程序的安全性就有了根本的保障。遗憾的是,C++在安全性方面考虑得还不够,只能依靠程序员和外部环境来支持。

    311 全局变量C++中,依赖于不加封装的全局变量常常造成系统的崩溃。由于任何代码都可以修改全局变量,这就带来了严重的副作用问题。有时候代码的某一部分需要全局变量保持不变,而另一部分代码却改变了这些值。更糟糕的是,看起来无关紧要的、改变全局变量的值,却会导致系统出现灾难性的后果

    312 不安全的结构C++(准确地说是C)试图通过称为struct的结构声明来封装数据,用union来实现多态。遗憾的是结构和联合中所有成员均为公有,这就带来了安全性问题。并且这两种方法不能掩盖由机器内存对齐和大小限制所带来的弊端。

    313 不安全的参数表C++可通过称为varargs的可变长参数表来传递任何类型的指针。如果想用sprintf以“%s”方式打印一个参数,或没有将形参或实参一一对准,那么在运行时就会发现不可检测的错误。可变长参数表只不过是允许将任何地址映射到任何类型而将类型检查的任务留给程序员的一种扩展。

    314 不安全的类型转换类型转换是CC++中允许随意改变指针类型的有力工具。在C中,经常可以看到下面的语句:memset((void3)p,0,sizeof(structp));这种用法虽然不规范,但它假定p是指向内存块的指针,内存块大小为sizeof(structp),有足够的信息说明类型转换是正确的。但C++中的对象很多是指向内存的指针,因此在运行时没有办法来检测类型转换是否正确。在C++中,通过指针进行的类型转换,常常带来安全性问题,比如:char  ch=‘A’;  int  3ptr;ptr=(int 3)&ch;  3ptr=1234;自动数据类型转换使得两个数据类型互不兼容的变量可以相互赋值,常会导致错误或精度损失等问题。例如,把一个带符号的32位整数赋值给一个无符号整型变量,则结果变成了无符号数。

    315 数组越界问题CC++为了提高程序的效率而放弃了数组的越界检查,把这个可能带来严重的安全性问题留给了程序员自己去解决,是很不可取的。因为数组越界是CC++程序中非常容易出现的问题,且导致的错误现象难以预测。也许CC++这么做是受限于当时的硬件背景,但在机器速度日益提高的今天,把数组越界检查这样的问题留给程序员解决,实在是得不尝失。

    316 表达式求值表达式除了求值之外,还可能产生“副作用”(sideeffects)。所谓副作用是指表达式求值之外的任何其它动作。C++也秉承了这样的特性。例如表达式中可通过赋值符号修改变量的值,从而产生“副作用”。

    317 指针指针是C、C++中最灵活、最有效,同时又是最有害或者说最容易产生错误的数据类型。内存管理问题就与指针密切相关。由于允许使用类型无关的指针访问任意的内存区,也就可以随意访问一个对象的私有数据,对象的“安全”和“封装”特性也就无从谈起。在一个可随意访问内存地址的环境中试图保护数据的完整性和安全性是不可能的。很多计算机病毒和黑客程序就是通过使用指针访问和修改计算机内存来达到目的的。

 

4 可移植性问题

    可移植性是指程序能在两种或更多种计算机(或者软硬件平台)上运行的性质,或与此有关的事件。C++在移植性方面确有改进,但问题还是存在的,并且可移植性的获得,是以编程灵活性的丧失和性能的损失作为代价的。

    411 固有的问题虽然CC++语言也是一种可移植性好的语言,但由于其设计时,保留对系统底层的操作,程序就有了“依赖性”,再加上其“开放”的策略,各个厂家“各自为政”,争先制定“标准”,结果造成版本众多,互不兼容。

    412 数据类型C++继承C中所有的通用数据类型,如int,float,char等,这些类型可以表示不同范围和精度的数值。不幸的是,实际的范围和精度依赖于具体的编译环境。在一台机器上编译和运行得很好的程序,到另一台机器上就表现得不完全相同。对于不同的平台,编译器对于简单数据类型如int,float等采取了不同的分配策略,例如:int在IBMPC中为16位,在VAX211中为32位。可见,大量使用C中脆弱的构造数据类型(如struct)或通用的数据类型将导致不可移植的代码。

5 可读性可理解性问题

    众所周知,可读性已经是衡量程序质量的一个非常重要的指标,甚至在开发大型程序时,宁愿牺牲部分效率,也要换取程序的可读性[5,7,8]。程序的可读性主要取决于程序设计风格,但和语言本身也有密切的关系。

    511 预处理器也许为了与C兼容,C++借助于一种早期的宏汇编方式,利用预处理器来处理以“#”开始的命令行,这些命令行执行简单的条件编译和宏替换。事实上,C预处理器经常弄得程序十分难懂,也就是说,C++中用宏定义来实现的代码给程序的可读性带来了麻烦。

    512 goto语句另一个拙劣特性是为了达到快速跳转却破坏程序可读性的goto语句[9]。在C++引入异常处理以前,goto语句经常被用来在异常处理中跳出循环。Jacopini和Bohm从理论上证明了“任何程序均可用三种基本的控制结构通过复合、叠加来构成”。Dijkstra还进一步指出:“goto语句是有害的”;“应该把goto语句从所有的高级语言中剔除出去”。因为不适当地使用goto语句,会造成程序杂乱无章,结构混乱,既不易读,也不易修改。然而,C++语言中仍然保留了goto语句。

    513 分离的头文件另一个重要的问题就是头文件。允许在头文件中声明类的原型以及全局变量、库函数等,然后将其与编译过的类实现一起发送,该特点使C++编译器环境很难使用。因为在大的系统中,要维护这些头文件使其与编译过的类实现保持一致是非常困难的,甚至是不可能的。保存这些类声明信息最好的地方是与编译的类实现放在同一文件中,这样就不会造成类声明与类实现的版本脱节。但是,C++的编译文件格式是与机器有关的,因此又不可能将头文件放在里面。由于程序员是通过头文件来访问编译的类,因此编译的代码受头文件的支配。假设程序员想访问被编译的类中的私有数据,可以将头文件中该类的访问修饰符从私有改为公有,这显然是不合适的。

    514 虚函数virtual是一种难以掌握的语法。虚拟函数的实现机制要求编译器为其在class中建立起virtualtable入口,而globalanalysis并不是由编译器完成的,所以一切的重担都压在了程序员的肩上。程序员必须了解一些底层的概念,甚至要超过了解那些高层次的面向对象的概念[10~12]。

    515 操作符重载C++支持操作符重载,这实际上意味着编程人员可以扩充C++语言的语法,这种特性有时是非常方便的。例如,通过操作符重载,可以把等号(=)重载成执行对象的赋值操作。但与此同时,操作符重载无疑又降低了程序的可读性。

 6 结语

    一种高级的编程语言应该将一些琐碎且与问题本身无关的细节移出程序员的职责范围,而由编译器或运行支持系统代劳。而C++却仍然需要程序员关注大量的细节问题,并且其间陷阱密布。一个看似符合语法的语句很可能是造成错误的根源,而这种错误只能依靠程序员的经验来识别。这种不得已的对程序员的依赖性或多或少的成了软件危机的诱因。因此,现代高级程序设计语言应该专注于解决问题本身,而不是在与问题无关的实现细节上纠缠不休,这些细节完全可以成为编译器的职责所在;另外,语言对软件分析、设计支持的匮乏,给软件的设计与实现之间的同步造成了极大的困难。因此,语言中也需要融入更多分析、设计成分

转载于:https://my.oschina.net/lcniuren33/blog/82906

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值