博文
我所理解的构造函数与析构函数(2006-03-12 20:44:00)
摘要: 学习c++一段时间了,入门教材是〈〈c++基础教程(第2版)〉〉(清华大学出版社;(美)Herbert Schildt 著,王军 译),花两周粗粗地看了一遍,感觉有点印象了。后来又看〈〈c,c++程序员实用大全〉〉,看〈〈标准c++宝典〉〉,感觉越来越糊涂,特别对const的使用和类的构造函数,析构函数,拷贝构造函数和赋值函数的认识有些摸棱两可,于是决定写篇文章,做一个阶段性的总结,同时希望拙文能够给各位初学者一些帮助。
1.1 构造函数与析构函数的起源 作为比C更先进的语言,C++提供了更好的机制来增强程序的安全性。C++编译器具有严格的类型安全检查功能,它几乎能找出程序中所有的语法问题,这的确帮了程序员的大忙。但是程序通过了编译检查并不表示错误已经不存在了,在“错误”的大家庭里,“语法错误”的地位只能算是小弟弟。级别高的错误通常隐藏得很深,就象狡猾的罪犯,想逮住他可不容易。 根据经验,不少难以察觉的程序错误是由于变量没有被正确初始化或清除造成的,而初始化和清除工作很容易被人遗忘。Stroustrup在设计C++语言时充分考虑了这个问题并很好地予以解决:把对象的初始化工作放在构造函数中,把清除工作放在析构函数中。当对象被创建时,构造函数被自动执行。当对象消亡时,析构函数被自动执行。这下就不用担心忘了对象的初始化和清除工作。 构造函数与析构函数的名字不能随便起,必须让编译器认得出才可以被自动执行。Stroustrup的命名方法既简单又合理:让构造函数、析构函数与类同名,由于析构函数的目的与构造函数的相反,就加前缀‘~’以示区别。 除了名字外,构造函数与析构函数的另一个特别之处是没有返回值类型,这与返回值类型为void的函数不同。构造函数与析构函数的使命非常明确,就象出生与死亡,光溜溜地来光溜溜地去。如果它们有返回值类型,那么编译器将不知所措。为了防止节外生枝,干脆规定没有返回值类型。(引自〈〈高质量c++编程指南〉〉)
1.2 构造函数概述 创建对象实例时,程序通常初始化对象的数据成员,为了简化初始化对象的过程,c++使用了一个特殊的函数——构造函数,程序每次创建对象实例时,自动执行构造函数。在正式讲解之前,我们先看看c++对构造函数的一个基本定义。 1.C++规定,每个类必须有构造函数,......
第18次比赛第2题程序(2006-03-05 12:01:00)
摘要:/*iAkiak热衷于网络俄罗斯方块对战游戏。为了能够赢得更多的比赛,他决定编写一个俄罗斯方块的外挂程序。他希望这个程序能够尽快的把游戏中所有的方块消除。
虽然俄罗斯方块游戏几乎耳熟能详、人尽皆知,但为了让大家明确,还是先介绍一下:
俄罗斯方块游戏内有19种不同的4*4的方块组合。每种组合都只有4个连续的有效方块(用*表示),其余部分是空白方块(用0表示)。有的方块仅仅是其他方块旋转得到的。下面列举出所有19种方块,为了便于讨论,给它们依次编号0..18:
**** *0000000 *0000000 *0000000 *000(0) (1)
0*00 0*00 ***0 *000***0 **00 0*00 **000000 0*00 0000 *0000000 0000 0000 0000(2) (3) (4) (5)
0**0 *000 **00 0*00**00 **00 0**0 **000000 0*00 0000 *0000000 0000 0000 0000(6) (7) (8) (9)
**00**0000000000(10)
*000 00*0 **00 ***0*000 ***0 0*00 *000**00 0000 0*00 00000000 0000 0000 0000(11) (12) (13) (14)
0*00 ***0 **00 *0000*00 00*0 *000 ***0......
第17次编程比赛第2题解法(2006-02-25 18:10:00)
摘要:/* 现有3×3的九个方格,空出中心的方格,将数字1-8随机的放入到其它的8个方格中,数字1不动,其它的数字可以移动, 最终,使所有数字按1-8的顺序顺时针排列外围方格中。移动的规则是: 都可以移向横排或竖排相临空着的方格,而且,都可以移向中心空着的方格。 要求计算出移动移动最少的次数。 函数接口: int minMove(int a[]); 其中 a[] 表示从左上角顺时针放入的8个数。*//*2006-2-25*/ /*算法介绍: 1。设定位置标号:从左上角按顺时针依次设定位置标号为1-8,中间的空格位置标号为0。*2。创建一个类,其对象为数字1-8。为每个对象的属性包括位置标号pos和数字大小num。*3。定义一个对象数组,数组元素依次为数字1-8。为方便起见,这里使每个元素的大小等于其下标值。*4。从数字2开始依次按照移动规则操作每个数字,使其按顺序顺时针排列到外围方格中。 操作方法:若数字n(2 <= n <=8 )已经位于n-1之后,则对n不做任何操作,直接操作n+1; 若数字n不在n-1之后,则: 1。若n处于中间的方格中(指位置标号为2,4,6,8处,非中间空格处),则将其移至中间空格处,然后将其位置前面的数字依次按逆时针移动,直到空出一个合适的方格,即方格的前一位置处是数字n-1,将n移到该空方格处。 2。若n不处于中间的方格中(即位置标号为1,3,5,7处),则将其移到中间的方格中。 具体操作为:将n位置前面的数字移至中间空格处,然后将它及其位置后面的数字依次按逆时针移动,直到空出一个合适的方格,即方格的后一位置处的数字比n大或者是数字1,将中间空格处的......
我的第15次编程比赛第2题程序(2006-02-13 10:02:00)
摘要:/*有十个开关等间距排成一线,每个开关对应其上方的一盏灯(十盏灯也排成一线)。每按动一下开关,可以使对应的灯改变状态(原来亮着的将熄灭,原来熄灭的将被点亮)。但是,由于开关之间的距离很小,每次按动开关时,相邻的一个开关也将被按动。例如:按动第5个开关,则实际上第4、5、6个开关都被按动。而按动靠边的第1个开关时,第1、2个开关都被按动。并且,无法只按动最靠边的一个开关。现在给出十盏灯的初始的状态和目标状态,要求计算:从初始状态改变到目标状态所需要的最少操作次数。函数接口:int MinChange(const int Start[],const int End[]);其中:Start表示了初始状态,End表示了目标状态。表示状态的数组(Start和End)中,若某元素为0表示对应的灯亮着,否则表示对应的灯没有亮。调用函数时保证Start和End数组长度均为10,并保证有解。*操作方法:设当前灯编号为i,其下一盏灯编号为i+1若i灯的初始状态与末状态相同,且i+1灯的初始状态与末状态相同:i = i + 1,不按开关;若i灯的初始状态与末状态相同,但i+1灯的初始状态与末状态不同:i = i + 2,按下开关;若i灯的初始状态与末状态不同,不论i+1灯的初始状态与末状态是否相同:i = i + 1,按下开关。若出现无解的情况,则操作次数取32768; 从左边和右边各顺序操作一次,取操作数较小的返回 *注:因为按照如上的操作方法,最边上的两个开关是不用亲自去按的;而这样会漏掉一些解。所以把初始状态扩展到4种情况:1。与给定的初始状态一模一样2。在原状态下,先按下最左边的开关 3。在原状态下,先按下最右边的开关4。在原状态下,先按下最左边和最右边的开关*//*2006-2-12 梁见斌*/ #include <iostream>#include <cmath>
using namespace std;
int MinChange(const int Start[],const int End[]);//把初始状态扩展到四种情况 void Change(int array[], int lable, int max);//按下开关,改变对应的灯及其相邻的灯的状态 int DMinChange(const int start[], const in......
高质量C++/C编程指南13(转帖)(2006-01-15 10:52:00)
摘要:第11章 其它编程经验11.1 使用const提高函数的健壮性 看到const关键字,C++程序员首先想到的可能是const常量。这可不是良好的条件反射。如果只知道用const定义常量,那么相当于把火药仅用于制作鞭炮。const更大的魅力是它可以修饰函数的参数、返回值,甚至函数的定义体。 const是constant的缩写,“恒定不变”的意思。被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。所以很多C++程序设计书籍建议:“Use const whenever you need”。 11.1.1 用const修饰函数的参数 如果参数作输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”,都不能加const修饰,否则该参数将失去输出功能。 const只能修饰输入参数: 如果输入参数采用“指针传递”,那么加const修饰可以防止意外地改动该指针,起到保护作用。 例如StringCopy函数: void StringCopy(char *strDestination, const char *strSource);其中strSource是输入参数,strDestination是输出参数。给strSource加上const修饰后,如果函数体内的语句试图改动strSource的内容,编译器将指出错误。
如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const修饰。 例如不要将函数void Func1(int x) 写成void Func1(const int x)。同理不要将函数void Func2(A a) 写成void Func2(const A a)。其中A为用户自定义的数据类型。
对于非内部数据类型的参数而言,象void Func(A a) 这样声明的函数注定效率比较底。因为函数体内将产生A类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。 为了提高效率,可以将函数声明改为void Func(A &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数void Func(A &a) 存在一个缺点......
高质量C++/C编程指南12(转帖)(2006-01-15 10:51:00)
摘要:第10章 类的继承与组合
对象(Object)是类(Class)的一个实例(Instance)。如果将对象比作房子,那么类就是房子的设计图纸。所以面向对象设计的重点是类的设计,而不是对象的设计。 对于C++程序而言,设计孤立的类是比较容易的,难的是正确设计基类及其派生类。本章仅仅论述“继承”(Inheritance)和“组合”(Composition)的概念。 注意,当前面向对象技术的应用热点是COM和CORBA,这些内容超出了C++教材的范畴,请阅读COM和CORBA相关论著。10.1 继承 如果A是基类,B是A的派生类,那么B将继承A的数据和函数。例如: class A { public: void Func1(void); void Func2(void);};
class B : public A { public: void Func3(void); void Func4(void);};
main() { B b; b.Func1(); // B从A继承了函数Func1 b.Func2(); // B从A继承了函数Func2 b.Func3(); b.Func4(); } 这个简单的示例程序说明了一个事实:C++的“继承”特性可以提高程序的可复用性。正因为“继承”太有用、太容易用,才要防止乱用“继承”。我们应当给“继承”立一些使用规则。 【规则10-1-1】如果类A和类B毫不相关,不可以为了使B的功能更多些而让B继承A的功能和属性。不要觉得“白吃白不吃”,让一个好端端的健壮青年无缘无故地吃人参补身体。【规则10-1-2】若在逻辑上B是A的“一种”(a kind of ),则允许B继承A的功能和属性。例如男人(Man)是人(Human)的一种,男孩(......
高质量C++/C编程指南11(转帖)(2006-01-15 10:51:00)
摘要:第9章 类的构造函数、析构函数与赋值函数 构造函数、析构函数与赋值函数是每个类最基本的函数。它们太普通以致让人容易麻痹大意,其实这些貌似简单的函数就象没有顶盖的下水道那样危险。 每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A产生四个缺省的函数,如 A(void); // 缺省的无参数构造函数 A(const A &a); // 缺省的拷贝构造函数 ~A(void); // 缺省的析构函数 A & operate =(const A &a); // 缺省的赋值函数
这不禁让人疑惑,既然能自动生成函数,为什么还要程序员编写? 原因如下:(1)如果使用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会,C++发明人Stroustrup的好心好意白费了。(2)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。 对于那些没有吃够苦头的C++程序员,如果他说编写构造函数、析构函数与赋值函数很容易,可以不用动脑筋,表明他的认识还比较肤浅,水平有待于提高。 本章以类String的设计与实现为例,深入阐述被很多教科书忽视了的道理。String的结构如下: class String { public: String(const char *str = NULL); // 普通构造函数 String(const String &other); // 拷贝构造函数 ~ String(void); // 析构函数 String &......
高质量C++/C编程指南10(转帖)(2006-01-14 10:16:00)
摘要:第8章 C++函数的高级特性 对比于C语言的函数,C++增加了重载(overloaded)、内联(inline)、const和virtual四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数,const与virtual机制仅用于类的成员函数。 重载和内联肯定有其好处才会被C++语言采纳,但是不可以当成免费的午餐而滥用。本章将探究重载和内联的优点与局限性,说明什么情况下应该采用、不该采用以及要警惕错用。8.1 函数重载的概念8.1.1 重载的起源 自然语言中,一个词可以有许多不同的含义,即该词被重载了。人们可以通过上下文来判断该词到底是哪种含义。“词的重载”可以使语言更加简练。例如“吃饭”的含义十分广泛,人们没有必要每次非得说清楚具体吃什么不可。别迂腐得象孔已己,说茴香豆的茴字有四种写法。 在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,即函数重载。这样便于记忆,提高了函数的易用性,这是C++语言采用重载机制的一个理由。例如示例8-1-1中的函数EatBeef,EatFish,EatChicken可以用同一个函数名Eat表示,用不同类型的参数加以区别。
void EatBeef(…); // 可以改为 void Eat(Beef …); void EatFish(…); // 可以改为 void Eat(Fish …); void EatChicken(…); // 可以改为 void Eat(Chicken …); 示例8-1-1 重载函数Eat
C++语言采用重载机制的另一个理由是:类的构造函数需要重载机制。因为C++规定构造函数与类同名(请参见第9章),构造函数只能有一个名字。如果想用几种不同的方法创建对象该怎么办?别无选择,只能用重载机制来实现。所以类可以有多个同名的构造函数。
8.1.2......
高质量C++/C编程指南09(转帖)(2006-01-14 10:15:00)
摘要:第7章 内存管理 欢迎进入内存这片雷区。伟大的Bill Gates 曾经失言: 640K ought to be enough for everybody — Bill Gates 1981 程序员们经常编写内存管理程序,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的。本章的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存管理。7.1内存分配方式 内存分配方式有三种:从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。7.2常见的内存错误及其对策 发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。 常见的内存错误及其对策如下:内存分配未成功,却使用了它。 编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。 内存分配虽然成功,但是尚未初始化就引用它。 犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。 内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。 内存分......
高质量C++/C编程指南08(转帖)(2006-01-14 10:14:00)
摘要:第6章 函数设计 函数是C++/C程序的基本功能单元,其重要性不言而喻。函数设计的细微缺点很容易导致该函数被错用,所以光使函数的功能正确是不够的。本章重点论述函数的接口设计和内部实现的一些规则。 函数接口的两个要素是参数和返回值。C语言中,函数的参数和返回值的传递方式有两种:值传递(pass by value)和指针传递(pass by pointer)。C++ 语言中多了引用传递(pass by reference)。由于引用传递的性质象指针传递,而使用方式却象值传递,初学者常常迷惑不解,容易引起混乱,请先阅读6.6节“引用与指针的比较”。6.1 参数的规则【规则6-1-1】参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用void填充。 例如:void SetValue(int width, int height); // 良好的风格void SetValue(int, int); // 不良的风格float GetValue(void); // 良好的风格float GetValue(); // 不良的风格
【规则6-1-2】参数命名要恰当,顺序要合理。 例如编写字符串拷贝函数StringCopy,它有两个参数。如果把参数名字起为str1和str2,例如 void StringCopy(char *str1, char *str2); 那么我们很难搞清楚究竟是把str1拷贝到str2中,还是刚好倒过来。 可以把参数名字起得更有意义,如叫strSource和strDestination。这样从名字上就可以看出应该把strSource拷贝到strDestination。 还有一个问题,这两个参数那一个该在前那一个该在后?参数的顺序要遵循程序员的习惯。一般地,应将目的参数放在前面,源参数放在后面。 如果将函数声明为: void StringCopy(char *strSource, char *strDestination); 别人在使用时可能会不假思索地写成如下形式:char str[20];StringCopy......
