高效编程十八式.pdf
《高效编程十八式.pdf》由会员分享,可在线阅读,更多相关《高效编程十八式.pdf(46页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、 1/46 高效编程十八式 王伟冰 2/46 目录目录 导言:编程五大原则.3 复数运算:类与函数.4 数据统计:泛型与委托.7 矩阵类:封装与约束.11 形体建模:继承与多态.15 宏思想与语法糖.18 命名、陷阱与异常.23 性能优化.27 多线程.31 代码编辑.35 测试.38 调试.40 总结.43 后记.45 3/46 导言:编程五大原则 本文讨论的是如何提高编程的质量和效率,涉及编程的十八个方面:类,函数,泛型,委托,封装,约束,继承,多态,宏思想,语法糖,命名,陷阱,异常,性能优化,多线程,代码编辑,测试,调试。示例代码主要是用 C+写的,但是我所讨论的原则同样适用于其它语言。
2、我根据自已实际编程的经验,以及阅读过的编程书籍,总结出编写代码的五条基本原则:简洁,安全,快速,灵活,清晰。下面用一些小小的例子,来接触一下这五个原则。我们在编程的时候,经常会遇到常量,比如说圆周率,我们可以这样写:const double PI=3.14159265;这样做的第一个好处就是,每一次用到圆周率时,只需要写 PI,就不用写什么 3.14一大串了,这就引出了第一个原则:简洁原则:写出来的代码简洁原则:写出来的代码要要尽量简洁尽量简洁,避免重复,避免重复。使用 PI 的另一个好处是,不容易出错。如果我们每一次都写 3.14,那么有可能某一次会不小心写错了哪位数字,导致计算结果不正确,
3、这种错误很难被发现。但是用 PI 的话,即使我们不小心把 PI 拼错了,编译器也会提醒我们。这就是第二个原则:安全安全原则:原则:写出来的代码要不易出错,易于查错。写出来的代码要不易出错,易于查错。那么,为什么要写成 const 呢?直接写 double PI=3.14159265 不也可以吗?这涉及到性能问题,const 型的变量编译时就能确定它的值,而非 const 型的变量在运行才能得到它的值,执行速度会慢一些。于是引出第三个原则:快快速原则:写出来的代码要快速运行,尽量省时。速原则:写出来的代码要快速运行,尽量省时。再来看另一个例子,比如你想输入 10 个数,然后排好序输出来:(省略具
4、体过程)const int Length=10;int aLength;for(int i=0;i Length;i+)/逐个输入/排序 for(int i=0;ix=a.x+b.x;pc-y=a.y+b.y;return*pc;实际上,Java 和 C#就是这样做的。但是 C+这样做会有一个严重问题:你在堆中创建了变量,什么时候回收?如果不回收,则会浪费内存(或者叫内存泄漏),如果要回收,则每次用完这个返回值之后都要自己手动回收,比如说:complex&c=add(a,b);/用 c 进行其它运算 delete&c;/手动回收 这简直就是麻烦。Java 和 C#以及其它很多高级语言都有自动垃
5、圾回收机制,所以不需要手动回收,但是 C+却只能这样。总之,在堆中创建的变量,要保证能回收。在堆中创建的变量,要保证能回收。(安全(安全原则原则2)现有 C+很难解决这个问题,不过,即将发布的 C+新标准,加入了“右值引用”的新特性,可以完美地解决这个问题。网上有很多资料,具体我就不介绍了。在现有的情况下,只能根据实际需要进行折衷设计,一般牺牲快速原则用 c=a+b。6/46 也许你会觉得我无聊,不就是一个复数加法吗,几行代码就完事,用得着讨论这么多吗?其实我只是想借用这个最简单例子来阐述一些普适性的原则。当你运用这些原则解决了复数加法的问题之后,你会发现同样的方法可以用来实现复数减法、乘法、
6、除法,甚至矩阵的加减乘除,集合的交、并、差,很多很多。7/46 数据统计:泛型与委托 我们常常会遇到这样的问题,比如说,统计一个班的学生中数学成绩大于 60 分的人数。假如说所有学生的成绩储存在一个 int 型数组中,那么我们可以定义这样的函数:int count(int scores,int n)int m=0;for(int i=0;i60)m+;return m;其中 scores 中储存学生成绩的数组,n 是数组的长度。于是我们就可以用诸如 count(a,30)这样的代码来统计一个 30 人的班上的及格人数了。但是,这样的一个函数,限死了只能统计大于 60 分的人数,不能统计大于 7
7、0 分、80分的人数,所以我们可以把函数改成这样:int count(int scores,int n,int min)int m=0;for(int i=0;imin)m+;return m;这样不就提高了灵活性了吗?所以,对于对于有可能改变的有可能改变的数值,不要写死,可以作为函数值,不要写死,可以作为函数参数传进来。数参数传进来。(灵活(灵活原则原则 1)但是可能过几天之后,你发现有的学生的成绩不是整数,可惜你的这个函数只能处理整数的情况,只好另写一个:double count(double scores,int n,double min)麻不麻烦?其实,如果从一开始就预料到类型可能会发
8、生改变,那么不妨把函数定义成模板:template int count(T scores,int n,T min)int m=0;for(int i=0;imin)m+;return m;其中 T 称为模板参数,如果 T 是 int,那么就得到前面 int 版本的 count 函数;如果 T是 double,就得到前面 double 版本的 count 函数。比如这样调用:m=count(a,30,60);/调用 int 版本 m=count(b,40,60);/调用 double 型版本 m=count(c,30,60);/编译器会根据 c 的类型自动推断出用哪个版本 所以,对于对于有可能改
9、变的有可能改变的类型,不要写死,可以写成模板,把类型作为模板参数传进类型,不要写死,可以写成模板,把类型作为模板参数传进来。来。包括类模板和函数模板。(灵活包括类模板和函数模板。(灵活原则原则 2)这就是“泛型编程”。而且这样也体现了简洁 8/46 原则,因为不需要对每种类型分别写一个单独的版本。但是,问题还没有完,有时我们不仅需要统计成绩大于某个值的人数,还要统计小于某个值的人数,或者是介于某两个值之间的人数。考虑到要尽量利用已有的类和函数来构尽量利用已有的类和函数来构造新的功能,而不改变类和函数的内部代码。造新的功能,而不改变类和函数的内部代码。(灵活(灵活原则原则 3)你可以这样子来实现
10、:m=30-count(a,30,60);/总人数减去大于 60 的人数得到小于 60 的人数 m=count(a,30,60)-count(a,30,70);/大于 60 的人数减去大于 70 的人数得到介于 60 和 70之间的人数 尽管这种方法很巧妙,在很多情况下是一种实用的思路,但是这毕竟是一种治标不治本的做法(而且性能不好),假如我要你统计出成绩是奇数或偶数的人数呢?无能为力了吧。根本的方法是使用函数指针:template int count(T scores,int n,bool func(T)/func 代表一个函数指针 int m=0;for(int i=0;i60;bool
11、func2(int x)/判断 x 是否是偶数 return x%2=0;这样你就可以这样子做统计:m=count(a,30,func1);/统计分数大于 60 的人数 m=count(a,30,func2);/统计分数是偶数的人数 其中 bool func(T)表示一个名为 func 的函数指针,它指向一个函数,这个函数的参数为T 型,返回值为 bool 型。当你把 func1(或 func2)作为参数传给 count 函数时,func 指针就指向了 func1(或 func2)函数。当执行到 func(scoresi)时,实际上就执行了func1(scoresi)(或 func2(scor
12、esi))。所以,当一个函数里有一段当一个函数里有一段有可能改变的有可能改变的代码时,不要写死,可以把这段代码委托给代码时,不要写死,可以把这段代码委托给一个传进来的函数指针去执行。一个传进来的函数指针去执行。(灵活(灵活原则原则 4)这种思想就叫做“委托”。回顾前面的灵活原则 1,我们用一个 min 参数来代替 60,从而提高了灵活性和简洁性。我们希望 func1 函数也能做到这一点,能够判断 x 是否大于一个设定的数值 min.于是对它进行了改造:bool func1(double x,double min)return xmin;这样做对吗?那调用 count 怎么调用?count(a,
13、30,func(,60)?没有任何语言支持这种语法,count 要求传入的函数指针必须是 bool func(double)型的,也就是接受一个 double 型参数并返回 bool 型的,所以 func1 不能接受两个参数。解决方法 9/46 可以是这样:double min;bool func1(double x)return xmin;我们可以在调用 count 之前先设定 min 的值:min=60;m=count(a,30,func1);可惜的是,min 变量变成了一个全局变量,所有的函数都可以访问它,通常我们要严严格控制变量作用域,只有格控制变量作用域,只有真正需要用到真正需要用到
14、某个变量的代码段才可以访问到这个变量。某个变量的代码段才可以访问到这个变量。(清晰清晰原则原则 2)你可能觉得这没什么大不了,但是在大项目里,过多不必要的全局变量可能会导致不经意的命名冲突,使得在别的地方对另外一个变量的访问不小心变成了对这个变量的访问。即使不存在任何同名现象,在多线程的程序里对全局变量的异步访问也会导致意想不到的结果,这个后面还会说到。更好的实现应该使用函数对象,我们需要更改 count 和 func1 的定义:template int count(T scores,int n,T1 func)/func 的类型未定 int m=0;for(int i=0;imin;于是就可
15、以这样子:m=count(a,30,Func1(60);/统计分数大于 60 的人数 怎么这么复杂?这个过程发生了什么事?或许这样写会清楚一些:Func1 func1(60);m=count(a,30,func1);首先,Func1 是一个类型,而不是一个函数。Func1 func1(60)调用了 Func1 类的构造函数,新建了一个 Func1 型的对象 func1,并把它的成员变量 min 设为 60。然后把 func1 传给 count 函数的第三个参数,即 T1 func。所以 T1 就是 Func1,func 就是 func1。当执行 func(scoresi)的时候,就相当于执行
16、func1(scoresi),即调用了 Func1 类的()运算符,把 scorei作为参数 x 传入,返回 xmin 即 scorei60 的值。在这里,func1 对象就叫做函数对象,因为它本质是一个对象,却表现得像一个函数指针。10/46 太麻烦了。为了增加灵活性得写这么长一个 Func1 类。有没有简单一点的办法?有。可以利用 C+内置的函数对象(需要#include):m=count(a,30,bind2nd(greater(),60);这样就不用写 Func1 类了。但是这个实在很难看懂,我也不打算在这里解释它的意思你猜,如果要统计 60 到 70 间的人数该怎么写?m=count
17、(a,30,logical_and(bind2nd(greater(),60),bind2nd(less_equal(),70);也许有的人会觉得这个很酷,但是你看一下 C#是怎么写的:m=a.Count(x)=x60&x x60&x”是 lambda 表达式的标志,后面表示函数返回 x60&x return x60&x60&x”变成了“”,并且提到前面而已。总之,如果需要对函数指针进行更加灵活的定制,可以使用函数对象或者如果需要对函数指针进行更加灵活的定制,可以使用函数对象或者 lambda 表表达式。达式。(灵活(灵活原则原则 5)11/46 矩阵类:封装与约束 矩阵的元素可以用一个数组来
18、储存,我们希望建立一个矩阵类,来封装各种矩阵操作(比如加减乘、求逆、转置等等)。我们可以这么写:class matrix int width,height;/矩阵的宽度和高度 double*data;/储存矩阵数据的数组 public:matrix(int w1,int h1)/构造函数 width=w1;height=h1;data=new doublew1*h1;void add(const matrix&m1,const matrix&m2)/加法 for(int i=0;iwidth*height;i+)datai=m1.datai+m2.datai;/其它矩阵操作,不一一写出 ;在这
19、里我们把 width、height 和 data 都设为私有变量,因为我们不希望使用 matrix 类的代码对这些变量作随意的更改,如果外界代码对类的某个成员变量或函数的随意访问可如果外界代码对类的某个成员变量或函数的随意访问可能导致不好的结果,则应该把它设为私有成员。能导致不好的结果,则应该把它设为私有成员。(安全(安全原则原则 3)随意更改矩阵的高度、宽度或数据的储存位置,都可能导致原有数据的丢失。现在我们还不能对矩阵的具体元素进行访问,可以通过在 matrix 类中重载()运算符来实现:double&operator()(int i,int j)/返回第 i 行第 j 列元素 retur
20、n datai*width+j;由于返回类型是引用类型,所以不仅可以读取,还可以对它进行赋值:matrix m(3,3);double x=m(0,1);/读取第 0 行第 1 列元素 m(0,1)=x;/设置第 0 行第 1 列元素 这样,我们提供了对矩阵数据的封装,虽然外部代码无法直接访问矩阵数据,但是可以通过()运算符对它进行访问。现在我们的矩阵类看似可以做很多事情了,但实际上它有很多漏洞。比如,在构造函数里面用 new doublew1*h1分配了内存,但是却没有考虑它的回收,我们可以在 matrix 类中定义析构函数,让它自动回收:matrix()delete data;当一个 ma
21、trix 型变量销毁时(比如说,局部变量在函数返回时会自动销毁),就会调用析构函数matrix(),释放 data 指向的内存。12/46 然而这样还不够,假如我这样用,会发生什么事?matrix m1(3,3);matrix m2(m1);/调用 matrix 类的复制构造函数 m2=m1;/调用 matrix 类的=运算符 等等,我们并没有定义什么“复制构造函数”和“=运算符”啊?这是 C+为每一个类自动生成的,这两个函数都会自动地复制 m1 中变量的值到 m2 里去。上面代码的本意应该是把 m1 的数据复制给 m2,但实际上只是复制 data 指针的值,也就是说,m1、m2 里的data
22、 指针指向的是同一个地址,更改任一个矩阵的元素值也会同时更改另一个矩阵的元素值。而且,当这两个 matrix 对象销毁时,它们的析构函数会分别被调用,执行 delete data操作,同一个 data 地址被 delete 两次,这样很可能会使程序崩溃。所以 C+自动生成的这两个函数并不是什么好东西。解决方法是把它们显式地实现为复制数据,而不是复制指针:public:matrix(const matrix&m1)width=m1.width;/复制宽度 height=m1.height;/复制高度 data=new doublewidth*height;/新分配内存 for(int i=0;i
23、=0&i=0&jheight);return datai*width+j;assert 的作用就是,如果传给它的参数为 true,那么什么事都没发生,如果为 false,那么程序就会终止,或者进入调试状态。(需要包含 assert.h 头文件)所以,要确保每个数组的下标访问不会越界,每个函数的输入参数都合法。要确保每个数组的下标访问不会越界,每个函数的输入参数都合法。(安全(安全原原则则 5)void add(const matrix&m1,const matrix&m2)这个函数的输入参数就有可能不合法,要保证两个输入矩阵的宽与高都和当前矩阵一样:assert(m1.width=width&
24、m1.height=height 13/46&m2.width=width&m2.height=height);总之,为了防止外部代码随意访问而可能导致的不良后果,我们应当对这些访问加以约束。再来考虑一下矩阵的转置,比如:void transpose(matrix&m1)assert(m1.width=height&m1.height=width);for(int i=0;iheight;i+)for(int j=0;jdata1 if(&m1=this)/记得删除这个副本 delete pm1;就可以让自身转置了。总之,尽量减少尽量减少对对类和函数的类和函数的用法的用法的无理规定。无理规定。
25、(清晰原则清晰原则 3)注意,这里 transpose 函数设计成这样,只是为了引出上面的两条原则,这种设计并不是好的设计,好的设计应该是像 m=m1.transpose()这样。你有没有发现,本节提到的安全原则,基本上都是以牺牲灵活性为代价的,要么是限制访问类的某个成员,要么是限制数组下标或函数参数的范围,要么是限制两个指针不能指向同一变量。面向对象程序设计的教科书往往很推崇这种限制,或者叫做“封装”。但是我认为,这种限制有时候是没有必要的。比如我写这个 matrix 类只是为了给我自己用,我自己很清楚它是怎么实现的,所以我也不会笨到去乱改它的成员变量,不会笨到去传给它一个不合法的参数,那我
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 高效 编程 十八
限制150内