C++编程实用技巧——专家讲述C++程序设计的窍门

转载 2012年08月06日 07:13:40
从C转向C++

对每个人来说,习惯C++需要一些时间,对于已经熟悉C的程序员来说,这个过程尤其令人苦恼。因为C是C++的子集,所有的C的技术都可以继续使用,但很多用起来又不太合适。例如,C++程序员会认为指针的指针看起来很古怪,他们会问:为什么不用指针的引用来代替呢?

C是一种简单的语言。它真正提供的只有有宏、指针、结构、数组和函数。不管什么问题,C都靠宏、指针、结构、数组和函数来解决。而C++不是这样。宏、指针、结构、数组和函数当然还存在,此外还有私有和保护型成员、函数重载、缺省参数、构造和析构函数、自定义操作符、内联函数、引用、友元、模板、异常、名字空间,等等。用C++比用C具有更宽广的空间,因为设计时有更多的选择可以考虑。

在面对这么多的选择时,许多C程序员墨守成规,坚持他们的老习惯。一般来说,这也不是什么很大的罪过。但某些C的习惯有悖于C++的精神本质,他们都在下面的技巧进行了阐述。

内存管理

c++中涉及到的内存的管理问题可以归结为两方面:正确地得到它和有效地使用它。好的程序员会理解这两个问题为什么要以这样的顺序列出。因为执行得再快、体积再小的程序如果它不按你所想象地那样去执行,那也一点用处都没有。“正确地得到”的意思是正确地调用内存分配和释放程序;而“有效地使用”是指写特定版本的内存分配和释放程序。这里,“正确地得到”显得更重要一些。

然而说到正确性,c++其实从c继承了一个很严重的头疼病,那就是内存泄露隐患。虚拟内存是个很好的发明,但虚拟内存也是有限的,并不是每个人都可以最先抢到它。
在c中,只要用malloc分配的内存没有用free返回,就会产生内存泄露。在c++中,肇事者的名字换成了new和delete,但情况基本上是一样的。当然,因为有了析构函数的出现,情况稍有改善,因为析构函数为所有将被摧毁的对象提供了一个方便的调用delete的场所。但这同时又带来了更多的烦恼,因为new和delete是隐式地调用构造函数和析构函数的。而且,因为可以在类内和类外自定义new和delete操作符,这又带来了复杂性,增加了出错的机会。下面的技巧(还有技巧m8)将告诉你如何避免产生那些普遍发生的问题。

类和函数:设计与声明

在程序中声明一个新类将导致产生一种新的类型:类的设计就是类型设计。可能你对类型设计没有太多经验,因为大多数语言没有为你提供实践的机会。在C++中,这却是很基本的特性,不是因为你想去做才可以这么做,而是因为每次你声明一个类的时候实际上就在做,无论你想不想做。

设计一个好的类很具有挑战性,因为设计好的类型很具有挑战性。好的类型具有自然的语法,直观的语义和高效的实现。在C++中,一个糟糕的类的定义是无法实现这些目标的。即使一个类的成员函数的性能也是由这些成员函数的声明和定义决定的。

那么,怎么着手设计高效的类呢?首先,必须清楚你面临的问题。实际上,设计每个类时都会遇到下面的问题,它的答案将影响到你的设计。
  • 对象将如何被创建和摧毁?它将极大地影响构造函数和析构函数的设计,以及自定义的operator new, operator new[], operator delete, 和operator delete[]。(技巧M8描述了这些术语的区别)
  • 对象初始化和对象赋值有什么不同?答案决定了构造函数和赋值运算符的行为以及它们之间的区别。
  • 通过值来传递新类型的对象意味着什么?记住,拷贝函数负责对此做出回答。
  • 新类型的合法值有什么限制?这些限制决定了成员函数(特别是构造函数和赋值运算符)内部的错误检查的种类。它可能还影响到函数抛出的例外的种类以及函数的例外规范,如果你使用它们的话。
  • 新类型符合继承关系吗?如果是从已有的类继承而来,那么新类的设计就要受限于这些类,特别是受限于被继承的类是虚拟的还是非虚拟的。如果新类允许被别的类继承,这将影响到函数是否要声明为虚拟的。
  • 允许哪种类型转换?如果允许类型A的对象隐式转换为类型B的对象,就要在类A中写一个类型转换函数,或者,在类B中写一个可以用单个参数来调用的非explicit构造函数。如果只允许显式转换,就要写函数来执行转换功能,但不用把它们写成类型转换运算符和或单参数的非explicit构造函数。(技巧M5讨论了用户自定义转换函数的优点和缺点)
  • 什么运算符和函数对新类型有意义?答案决定了将要在类接口中声明什么函数。
  • 哪些运算符和函数要被明确地禁止?它们需要被声明为private。
  • 谁有权访问新类型的成员?这个问题有助于决定哪些成员是公有的,哪些是保护的,哪些私有的。它还有助于确定哪些类和/或函数必须是友元,以及将一个类嵌套到另一个类中是否有意义。
  • 新类型的通用性如何?也许你实际上不是在定义一个新的类型,而是在定义一整套的类型。如果是这样,就不要定义一个新类,而要定义一个新的类模板。

这些都是很难回答的问题,所以C++中定义一个高效的类远不是那么简单。但如果做好了,C++中用户自定义的类所产生的类型就会和固定类型几乎没什么区别,如果能达到这样的效果,其价值也就体现出来了。

上面每一个问题如果要详细讨论都可以单独组成一本书。所以后面技巧中所介绍的准则决不会面面俱到。但是,它们强调了在设计中一些很重要的注意事项,提醒一些常犯的错误,对设计者常碰到的一些问题提供了解决方案。很多建议对非成员函数和成员函数都适用,所以本章节我也考虑了全局函数和名字空间中的函数的设计和声明。

类和函数: 实现

C++是一种高度类型化的语言,所以,给出合适的类和模板的定义以及合适的函数声明是整个设计工作中最大的一部分。按理说,只要这部分做好了,类、模板以及函数的实现就不容易出问题。但是,往往人们还是会犯错。

犯错的原因有的是不小心违反了抽象的原则:让实现细节可以提取类和函数内部的数据。有的错误在于不清楚对象生命周期的长短。还有的错误起源于不合理的前期优化工作,特别是滥用inline关键字。最后一种情况是,有些实现策略会导致源文件间的相互联结问题,它可能在小规模范围内很合适,但在重建大系统时会带来难以接受的成本。

所有这些问题,以及与之类似的问题,都可以避免,只要你清楚该注意哪些方面。以下的技巧就指明了应该特别注意的几种情况。

继承和面向对象设计

很多人认为,继承是面向对象程序设计的全部。这个观点是否正确还有待争论,但本书其它章节的技巧数量足以证明,在进行高效的C++程序设计时,还有更多的工具听你调遣,而不仅仅是简单地让一个类从另一个类继承。

然而,设计和实现类的层次结构与C语言中的一切都有着根本的不同。只有在继承和面向对象设计领域,你才最有可能从根本上重新思考软件系统构造的方法。另外,C++提供了多种很令人困惑的面向对象构造部件,包括公有、保护和私有基类;虚拟和非虚拟基类;虚拟和非虚拟成员函数。这些部件不仅互相之间有联系,还和C++的其它部分相互作用。所以,对于每种部件的含义、什么时候该用它们、怎样最好地和C++中非面向对象部分相结合 ---- 要想真正理解这些,就要付出艰苦的努力。

使得事情更趋复杂的另一个原因是,C++中很多不同的部件或多或少地好象都在做相同的事。例如:
  • 假如需要设计一组具有共同特征的类,是该使用继承使得所有的类都派生于一个共同的基类呢,还是使用模板使得它们都从一个共同的代码框架中产生?
  • 类A的实现要用到类B,是让A拥有一个类型为B的数据成员呢,还是让A私有继承于B?
  • 假设想设计一个标准库中没有提供的、类型安全的同族容器类(技巧49列出了标准库实际提供的容器类),是使用模板呢,还是最好为某个 "自身用普通(void*)指针来实现" 的类建立类型安全的接口呢?

在部分的技巧中,我将指导大家怎样去回答这类问题。当然,我不可能顾及到面向对象设计的方方面面。相反,我将集中解释的是:C++中不同的部件其真正含义是什么,当使用某个部件时你真正做了什么。例如,公有继承意味着 "是一个" (详见技巧35),如果使它成为别的什么意思,就会带来麻烦。相似地,虚函数的含义是 "接口必须被继承",非虚函数的含义是 "接口和实现都要被继承"。不能区分它们之间的含义会给C++程序员带来无尽的痛苦。

如果能理解C++各种部件的含义,你将发现自己对面向对象设计的认识大大转变。你将不再停留在为区分C++语言提供的不同部件而苦恼,而是在思考要为你的软件系统做些什么。一旦知道自己想做什么,将它转化为相应的C++部件将是一件很容易的事。

做你想做的,理解你所做的!这两点的重要性绝没有过分抬高。接下来的技巧将对如何高效地实现这两点进行了详细的讨论。技巧44总结了C++面向对象构造部件间的对应关系和它们的含义。它是本章节最好的总结,也可作为将来使用的简明参考。

杂项

进行高效的C++程序设计有很多准则,其中有一些很难归类。本章就是专门为这些准则而安排的。不要因此而小看了它们的重要性。要想写出高效的软件,就必须知道:编译器在背后为你做了些什么,怎样保证非局部的静态对象在被使用前已经被初始化,能从标准库得到些什么,从何处着手深入理解语言底层的设计思想。本书最后的这个章节,我将详细说明这些问题,甚至更多其它问题。

相关文章推荐

C++编程入门系列之二十一(C++程序设计必知:类的静态成员)

鸡啄米在上一讲数据和函数中讲到,函数之间共享数据也就是此函数访问彼函数的数据主要是通过局部变量、全局变量、类的数据成员、类的静态成员及友元实现的,前三个已经讲过了,这一讲鸡啄米来讲讲静态成员。静态成员...

C++编程入门系列之二十三(C++程序设计必知:常引用、常对象和对象的常成员)

数据的封装实现了数据的隐藏,让数据更安全,但是前面讲到的通过局部变量、全局变量、类的数据成员、类的静态成员及友元实现了数据的共享,这样又降低了数据的安全性。有些数据是需要共享而又不能被改变的,那么这时...

C/C++语言经典、实用、趣味程序设计编程百例精解

C/C++语言经典、实用、趣味程序设计编程百例精解(1)  1.绘制余弦曲线 在屏幕上用“*”显示0~360度的余弦函数cos(x)曲线 *问题分析与算法设计 如果在程序中使用数组...

C/C++语言经典、实用、趣味程序设计编程百例精解

C/C++语言经典、实用、趣味程序设计编程百例精解(1)  1.绘制余弦曲线 在屏幕上用“*”显示0~360度的余弦函数cos(x)曲线 *问题分析与算法设计 如果在程序中使用数组...

C/C++语言经典、实用、趣味程序设计编程百例精解

C/C++语言经典、实用、趣味程序设计编程百例精解(1)  1.绘制余弦曲线 在屏幕上用“*”显示0~360度的余弦函数cos(x)曲线 *问题分析与算法设计 如果在程序中使用数组,这个问...
  • lxdfigo
  • lxdfigo
  • 2012年12月10日 21:53
  • 9914

C++/GDI+ 学习笔记(四)——实用技巧——调色板(ColorPalette)

在使用的过程中,遇到了这样的一个情况。维护很久前的一个项目的时候,想把之前的程序作成DLL。里面有一部分是描画一张8位DIB图片的,用的是纯C写的一段代码。可是在使用的时候发现,GDI+中由于使用了A...

第十六章 内存管理(1)====高质量程序设计指南C/C++编程

内存分配方式:                  1.从静态存储区分配,内存在程序编译的时候就已经分配好了(即已经编址),这些内存在程序的整个运行期间都存在,如全局变量,static变量等。     ...

VS2017下安装fltk库——C++程序设计原理与实践图形编程指南

VS2017下安装fltk库——C++程序设计原理与实践图形编程指南前言最近,我在学习《C++程序设计原理与实践》(原书第一版)遇到了安装图形库的问题,我花了两天时间,通过各种途径查找解决办法,终于成...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:C++编程实用技巧——专家讲述C++程序设计的窍门
举报原因:
原因补充:

(最多只允许输入30个字)