C++知识点大杂烩(重点)


file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] C++语言风格流变史 程序代码也有风格,这算不得什么新鲜事。早在20世纪80年代, C语言程序员就必须在K&R风格和ANSI风格 之间择善而从。但平心而论,我确实没有见过哪一种语言能像C++这样,在代码风格方面表现得如此诡谲和难 以捉摸:谁也说不清C++代码究竟能衍生出多少种迥异的风格,但我知道,有许多C++初学者在面对不同风格的 C++代码时,经常会误以为自己看到的是好几种完全不同的编程语言——仅此一点就足以提醒我们,研究和廓 清C++语言风格的演化和发展规律已是当务之急了。 和文体学家们研究历朝历代文体变迁的工作相仿,研究C++语言风格的流变史也没有什么捷径可走。我们 只能依据刘勰在《文心雕龙》中提倡的“原始以表末”[1]的研究方法,循着历史的脉络,推求代码风格的来 源,探寻风格演化的内因,并借以阐明技术发展的趋势和规律。 1. 带类的C——对C语言风格的因袭 在1983年12月Bjarne Stroustrup采纳Rick Mascitti的建议,将其发明的新语言命名为“C++”之前,人 们一直用“带类的C(C with Classes)”来称呼这种脱胎于C语言的,带有数据抽象机制的“方言”。虽然带 类的C在本质上仅仅是一种可以被预处理程序Cpre转换为传统C语言代码(这类似于我们在Oracle中见到的 Pro*C语言的预处理过程)的扩展性语言,但它的确在风格上奠定了后来所有C++代码的基础。 class stack { char s[SIZE]; char* min; char* top; char* max; void new(); public: void push(char); char pop(); }; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 这段“带类的C”代码录自Stroustrup所著的《C++语言的设计和演化》。代码中的new()其实是类stack的 构造函数,这与后来的C++语言有很大的不同。 显而易见,带类的C在风格上几乎完整地承袭了C语言的衣钵。代码中的声明语句看上去与C语言一模一 样,class的结构也与C语言中struct的结构大致相仿,这些迹象反映出C++语言来源于C又尽量与C保持兼容的 设计思想——这种设计思想既为C++的迅速普及提供了便利(C++语言的顺利推广显然得益于C语言已有的庞大 用户群),也在C++的语言风格中深深地烙上了C语言的印记,以至于在若干年后,当C++语言已经基本具备 了“独立人格”的时候,Stroustrup还不得不时常提醒人们要尽量抛开C语言的思维方式。 另一方面,Stroustrup从Simula语言借用的类、派生、访问控制等面向对象概念在带类的C中牢牢地扎下 了根。据Stroustrup介绍,他为C语言引入面向对象机制的本意在于寻找一种“合适的工具”[2],以便实现分 布式系统或解决类似的复杂问题。但无论怎样,Stroustrup将C的高效和Simula的优雅捆绑在一起的做法都在 事实上为C++语言埋下了“双重性格”的种子——很难说这不是C++语言风格多样化的直接诱因。 2. I/O流——C++的新形象 如果说C++语言的生身父母分别是C语言和Simula语言的话,那么,1984年出现的,借助操作符重载实现的 I/O流技术就是C++这个幼童甩开父母的庇护,向新的代码风格迈出的第一步了。 ostream& operator<<(ostream&s, const complex& z) { return s << '(' << z.real() << ',' << z.imag() << ')'; } 上面几行代码来自Stroustrup所著《C++程序设计语言》中的示例程序。注意那一行由“<<”连接的代 码,I/O流、变量、字符常量在代码中被巧妙地串联在一起。从技术角度看,这种全新语法的引入弥补了C语言 中printf()函数族缺乏类型安全机制和扩展能力的弱点。从代码风格上说,“<<”等通俗易懂的运算符大大改 变了程序员对C++语言的第一印象。我自己第一次接触C++ I/O流库时,就曾清晰地感觉到,一个试图摆脱C语 言风格束缚的C++精灵正顺着“<<”和“>>”组成的溪水“流淌”而来——这种行云流水般的代码风格在十几 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 年前就已经显示出了C++语言在塑造新形象、引进新观念方面的决心和勇气。 3. OWL和MFC——窗口环境下的风格变异 20世纪80年代末到90年代初,X Window、Mac OS、Windows等窗口环境的先后出现为程序设计提出了新的 课题,而C++语言兼顾面向对象和传统开发方法的特性无疑使其成为了窗口环境下编程语言的最佳选择。一批 基于C++语言的窗口框架不仅在商业上取得了成功,也在很大程度上改变了C++语言本身的风格特点。 最早在窗口开发中赢得大多数程序员青睐的C++框架是Borland公司于1992年内置在Borland C++ 3.1中的 OWL(Object Windows Library)框架库。下面这段代码取自Borland C++ 3.1的示例程序: class TGDIDemoWindow : public TMDIFrame { public: TGDIDemoWindow( LPSTR ATitle, LPSTR MenuName ) : TMDIFrame(ATitle, MenuName) {}; virtual void SetupWindow(); virtual void ArtyDemo( TMessage& ) = [CM_FIRST + ArtyDemoID]; virtual void Quit( TMessage& ) = [CM_FIRST + QuitID]; virtual void WMTimer( TMessage& ) = [WM_FIRST + WM_TIMER]; virtual void WMDestroy( TMessage& ) = [WM_FIRST + WM_DESTROY]; }; 为了解决窗口编程中最关键的消息映射问题,OWL的设计者为C++语言的成员函数引入了“=[⋯]”的古怪 语法,这是许多用过Borland C++的程序员至今都无法忘怀的一种语言风格。我承认,Borland公司在C++语言 的发展初期为我们提供了最好的编译器和最出色的集成开发环境(IDE),但Borland通过OWL框架为C++引入的 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 另类语言风格的确让人不敢恭维(客观地讲,这笔账也不应全算在Borland头上,因为OWL的前身是Borland从 White Water公司购买的框架代码)。 就在Borland C++ 3.1统治市场两年以后,Microsoft凭借其当仁不让的霸气和著名的Visual C++系列产品 逐渐夺回了Windows开发工具市场的主导权。与Borland不同的是,Visual C++中的MFC(Microsoft Foundation Class)框架库没有向OWL那样肆意篡改C++的语法,而是采用了下面这样的方式来实现消息映射 (代码取自MSDN示例程序): // Example for BEGIN_MESSAGE_MAP BEGIN_MESSAGE_MAP( CMyWindow, CFrameWnd ) ON_WM_PAINT() ON_COMMAND( IDM_ABOUT, OnAbout ) END_MESSAGE_MAP( ) 事实上,用MFC框架编写的C++代码在大量使用宏定义等预编译指令的同时,还把WIN32平台下常见的匈牙 利风格(有关标识符大小写和前缀的书写规范)发挥到了极限。这一点用不着我多费口舌,许多程序员仅从代 码的大小写特征上就能百分之百地确定代码中是否使用了MFC框架。 很遗憾,MFC为C++打造的语言风格并没有得到C++专家们的首肯。例如,包括Stroustrup在内的许多学者 都建议我们尽量少用甚至不用宏定义等预处理指令。在这一点上,MFC的做法显然和专家们的论调背道而驰。 应当说,是Microsoft的霸气造就了MFC的巨大成功;但从纯粹的语言学角度看,MFC在语言风格上的贡献远不 如它在窗口框架技术方面的贡献大。 4. 模板——现代C++风格的基础 Stroustrup于1988年首次公布了与模板(template)有关的语法设计。毫无疑问,这是一项对现代C++的 语言风格影响最大的技术改进。模板的概念来自Clu语言,并综合了Smalltalk和Ada语言中相关技术的优 点。1991年后,包含模板机制的开发环境(DEC C++、IBM C++、Borland C++等)陆续问世。但直到1995年 STL(Standard Template Library)模板库逐渐发展成熟以后,模板技术才在程序员中迅速普及开来。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 下面的例子取自SGI STL的示例代码,它基本反映了使用模板技术后C++代码的整体风格: template InputIterator find(InputIterator first, InputIterator last, const T& value) { while (first != last && *first != value) ++first; return first; } 在这样的C++代码中,除了少数几个关键字和操作符以外,我们几乎找不到多少C语言的痕迹了。模板技术 兼顾了类型安全和编码灵活性的双重需求,但它同时也为C++语言引入了一种更加精妙但也较难理解(相对于 没有模板的代码而言)的代码风格。许多传统的C语言拥护者讨厌这种风格的代码,但更多的新生代程序员对 其钟爱有加。1998年,在ANSI/ISO标准化委员会的支持下,STL被作为标准C++库(Standard C++ Library)的 一部分收入了C++国际标准之中。今天,以模板、异常等现代C++技术为代表的语言风格也已在事实上成为了 C++世界的“官方风格”。 5. ATL——COM时代的另类C++ 除了STL模板库之外,还有一个与模板风格相关的例子。下面的代码片断取自Visual C++自动生成的ATL控 件工程: class ATL_NO_VTABLE CMyATLObj : public IMyATLObj, public IpersistStreamInitImpl , public IOleControlImpl, public IOleObjectImpl, public IoleInPlaceActiveObjectImpl file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] , public IViewObjectExImpl, public IoleInPlaceObjectWindowlessImpl , public IPersistStorageImpl, public IspecifyPropertyPagesImpl , public IQuickActivateImpl, public IDataObjectImpl, public IProvideClassInfo2Impl <&__uuidof(CMyATLObj), NULL>, public CComControl ...... 注意控件类CMyATLObj的代码,CMyATLObj类居然是从N个接口类和控件类中派生出来的,类的声明语句中 随处可见模板的身影——这就是Microsoft为我们设计的别具一格的ATL风格的代码了。之所以要不惜代价地大 量使用模板、多重继承等语言特性,这主要为了适应COM、OLE、ActiveX等在架构上本来就相对复杂的技术体 系。但这样一来,使用ATL的代码在所有C++代码中,就拥有了一副异乎寻常的长相了:到处都是尖括号,到处 都是以“I”打头的标识符,甚至还有多重尖括号的嵌套⋯⋯如果要求一个刚学会C++语言的程序员立刻读懂一 大段ATL代码,我想,用不了几分钟,他就会被代码中那些晦涩、离奇的语言风格折磨得精神崩溃了。 6. 标准C++——一种全新的语言? C++语言的标准化进程远远落后于语言本身的普及速度。1990年以后,ANSI/ISO的C++标准化委员会才将包 括Stroustrup在内的大批专家以及包括Apple、Borland、DEC、HP、IBM、Microsoft、Sun、Unisys在内的知名 公司召集在一起,像所有国家的议会或人民代表大会一样通过没完没了的会议、讨论和投票制定C++的国际标 准。标准直到1998年9月才正式发布。在国际标准化组织的档案库里,C++标准的代号是ISO/IEC 14882:1998。 Stroustrup建议我们把标准C++当作一种全新的语言来学习[3]。这一说法显然是基于这样一个事实:标准 C++语言已经拥有了一种稳定的、可以推广的语言风格,即,通过对STL等既有技术的肯定,ANSI/ISO委员会在 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 1998年的标准中正式认可了包括模板、容器类、I/O流库、异常处理等典型语言特征的现代C++风格。风格的稳 定意味着语言本身的进步和成熟,也意味着程序员们对C++的认识必须上升到一个新的层次——那些至今还在 编写仅由类和C语言库函数组成的C++代码的程序员,一定会成为Stroustrup及其同仁们的取笑对象的。 Stroustrup的《C++程序设计语言》第3版对标准C++风格做了最权威的阐释。在Stroustrup等专家学者的 号召下,越来越多的项目开始编写符合标准C++风格的代码。这一点在许多开放源代码的项目中体现得特别明 显。这多半是由于,使用C++语言的开源项目大多都不会像大企业里的项目组那样,在语言风格上会受到公司 背景或历史习惯的羁绊。在具体的编程实践中,开源项目的程序员们一方面可以坚决地贯彻标准C++的语言风 格,另一方面也可以根据自己的喜好为代码增添一些感情色彩。例如,在OpenOffice的源码中,标识符的前缀 规范就相当有特点,连指针和引用类型的变量都由不同的前缀字母区分;下面给出的Linux桌面管理器KDE 3.1.4的源代码片断则显示出,开发KDE的程序员在代码风格上或多或少受到了Java语言风格的影响: class delUser: public KDialogBase { Q_OBJECT public: delUser(KUser *AUser, QWidget *parent = 0, const char *name = 0); bool getDeleteHomeDir() { return m_deleteHomeDir->isChecked(); } bool getDeleteMailBox() { return m_deleteMailBox->isChecked(); } private: QCheckBox *m_deleteHomeDir; QCheckBox *m_deleteMailBox; }; 7. 读不懂的代码——兼容并包的语言风格 说到标准C++语言风格,有必要给大家看一段非常古怪但也非常有趣的代码。你看得懂下面这段C++代码 吗?它是真正的C++代码吗? file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] %:include using namespace std; %:define MAX 5 void main() <% int m<:MAX:>; int i = 1; for (i = 0; i < MAX; i++) <% m<:i:> = i; if (i not_eq 3 and i < 5) cout << i << endl; %> %> 这是我自己编写的一段代码。你也许无法在Visual C++环境下运行它,但它的语法的确符合1998年C++标 准的规定。在GNU C++环境下,我曾成功地将其编译为可执行程序。 简单说来,这段风格诡异的C++代码其实是根据C++标准中关于可替换标记(Alternative Tokens)的规定 而编写的。该规定的设计初衷是要适应欧洲某些国家的标准字符集缺少“{”、“#”等标点符号(特别是在一 些传统的终端设备上)的现状。严格地讲,这算不得一种真正的语言风格,但类似的规定的确体现了ANSI/ISO 委员会在语言设计上兼容并包的宽广胸襟。 8. C++Builder——Borland的复兴之路 Borland公司在发布了Borland C++ 3.1之后,就因为不思进取而将C++开发工具的市场拱手让给了 Microsoft[4]。在经历了Borland C++ 4.0、4.5和5.0等版本的失败后,1997年,Borland推出了全新的C++开 发工具C++Builder。这个在市场上为Borland挽回了颜面的产品不但在界面风格上与Borland的支柱产品Delphi 别无二致,甚至还在产品内部直接照搬了Delphi的VCL(Visual Component Library)库。结果,使用 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] C++Builder开发的代码天生就受到了Delphi风格的传染,长相酷似Pascal语言了(以下代码取自C++Builder 6.0的示例代码): class TFormClrDlg : public TForm { published: // IDE-managed Components TColorDialog *ColorDialog; TButton *Button; TPanel *Panel1; void fastcall ButtonClick(TObject *Sender); private: // User declarations public: // User declarations virtual fastcall TFormClrDlg(TComponent* Owner); }; 说实话,尽管C++Builder在市场上的表现不错,但我还是不喜欢Borland将C++语言与Delphi中的Object Pascal语言刻意混淆的做法。也许在Borland这种做法的背后有提高产品通用性、缩短产品开发周期等体面的 理由,但使用C++Builder开发出的代码在外表上已经离标准C++风格越来越远了。 值得注意的是,Borland于2003年推出了其下一代C++开发工具 ——C++BuilderX。让人哭笑不得的是,这 一次Borland居然将C++开发环境构筑在了用Java语言实现的PrimeTime平台上,这多少将C++语言推向了一种极 为尴尬的处境。不过,C++BuilderX也为我们带来了一些好消息:在后续的版本中,C++BuilderX将集成 vxWindows框架库[5],在这种框架下开发的C++代码显然要比使用VCL的代码具备更多的标准C++风格。 9. Visual C++ .NET——革命还是叛逆? Microsoft将C++引入.NET环境的举动其实比Borland还要激进。单从风格上说,使用Visual C++ .NET开发 的代码可能兼具MFC、ATL、标准C++、.NET托管代码等多种不同的风格。其中,对C++语言本身影响最大的,当 然要数.NET托管代码为C++注入的若干新鲜血液了: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] #using using namespace System; using namespace System::Reflection; using namespace System::Security::Permissions; public __value enum SomeStuff { e1 = 1, e17 = 17 }; [attribute(AttributeTargets::Class, AllowMultiple=true)] public __gc class ABC { public: ABC(int __gc[]) {} ABC() {} ABC(int) {} ABC(int, float) {} ABC(SomeStuff) {} ABC(String*) {} int rgnField __gc []; double rgdField __gc []; double dField; }; 上述代码来自MSDN中的示例程序。看到Microsoft大刀阔斧地为C++语言引入的垃圾收集、Attribute属性 等新特性和新技术,看到.NET托管代码新奇得近乎离经叛道的语言风格,我不知道是应该为Microsoft在发展 通用语言平台上的努力而欢呼雀跃,还是应该为C++在C#语言阴影下日渐屈居.NET大戏中的配角而灰心丧气。 也许,语言风格和程序员的感受在Microsoft眼中,都是些不值一提的小事,它们哪能和.NET的宏伟战略及 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] Microsoft的强大帝国相提并论呢? 10. 回顾和展望 语言风格的变迁从一个侧面反映了技术思想和产业需求的嬗变规律。从1979年Stroustrup完成第一个Cpre 预处理程序算起,C++语言来到这个世界上已经快满25个年头了。这是一种在实践中诞生、成长和发展起来的 语言。也许,Stroustrup从一开始就压根儿也没想把它设计成像Smalltalk那样纯粹的面向对象语言。开放 性、高效率、兼容性和扩展性的需求将C++语言塑造成了一种典型的多模式(Multiparadigm)语言。无论是 C++早期对Simula语言的继承,还是后来对Smalltalk、Ada、Clu等语言的借鉴,无论是ANSI/ISO标准风格的迅 速普及,还是Visual C++ .NET在技术创新上的不懈努力,所有这些历史变迁都说明,C++在风格上的多样性主 要源自C++语言本身“海纳百川”的胸襟和气概。 5年以后,当C++步入而立之年的时候,它会给我们带来新的惊喜吗?我们还会看到更加新奇的C++语言风 格吗?也许,没有谁能给出准确的答案。但作为程序员,我们至少应该知道:无论面对什么样的软件需求,无 论使用什么样的思维方式,C++语言都赋予了我们选择语言风格的最大自由;当我们真正理解了C++语言的精神 实质之后,这种自由也必将成为所有优秀软件和优雅代码的坚实基础。 参考文献 [1] 刘勰. 文心雕龙·序志. [2] Stroustrup B. C++ 语言的设计和演化. 北京: 机械工业出版社, 2002 [3] Stroustrup B. Learning Standard C++ as a New Language. C/C++ Users Journal. pp 43-54. May 1999. [4] 李维. Borland 传奇. 北京: 电子工业出版社, 2003 [5] 李维. 细说Borland C++BuilderX. 程序员. 2003.11 新手入门:C++下的引用类型 文章来源:PConline 作者:管宁 引用类型也称别名,它是个很有趣的东西。在c++ 下你可以把它看作是另外的一种指针,通过引用类型我们 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 同样也可以间接的操作对象,引用类型主要是用在函数的形式参数上,通常我们使用它是把类对象传递给一个 函数。 引用对象采用类型名加上&符号和名称的方式进行定义。例如:(int &test;),这里我们就定义了一个int 类型的名为test 的引用,但是int &test;这样的方式是不能够被编译成功的,因为引用的定义必须同时给应 用进行赋值操作,这里的赋值并不是说把变量的值传递给引用,而是把引用指向变量,写成这样就对了:(int &test=变量名;)。 #include using namespace std; void main(void) { int a=10; int &test=a; test=test+2; cout << &a << "|" << &test << "|" << a << "|" < using namespace std; void main(void) { int a=10; //double &test = a + 1.2f; //这句就是错误的! const double &test = a + 1.2f; cout << &a << "|" << &test << "|" << a << "|" < using namespace std; void main(void) { bool found = true; if (found) { cout << "found条件为真!" << endl; } } 但是一些概念不清的人却不知道布尔类型的对象也可以被看做是一种整数类型的对象,但是他不能被声明 成signed,unsigned,short long,如果你生成(short bool found=false;),那么将会导致编译错误。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 其为整数类型的概念是这样的: 当表达式需要一个算术值的时候,布尔类型对象将被隐式的转换成int类型也就是整形对象, false就是 0,true就是1,请看下面的代码! #include #include using namespace std; void main(void) { bool found = true; int a = 1; cout << a + found << endl; cin.get(); } a+found 这样的表达式样是成立的,输出后的值为2进行了加法运算! 那么说到这里很多人会问指针也可以吗?回答是肯定的这样一个概念对于指针同样也是有效的,下面我们 来看一个将整形指针对象当作布尔对象进行使用的例子: #include using namespace std; void main(void) { int a = 1; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] int *pi; pi=&a; if (*pi) { cout << "*pi为真" << endl; } cin.get(); } 上面代码中的*pi进行了隐式样的布尔类型转换表示为了真也就是true。 新手入门:C/C++中数组和指针类型的关系 文章来源:PConline 作者:管宁 对于数组和多维数组的内容这里就不再讨论了,前面的教程有过说明,这里主要讲述的数组和指针类型的关 系,通过对他们之间关系的了解可以更加深入的掌握数组和指针特性的知识! 一个整数类型数组如下进行定义: int a[]={1,2,3,4}; 如果简单写成: a;//数组的标识符名称 这将代表的是数组第一个元素的内存地址,a;就相当于&a[0],它的类型是数组元素类型的指针,在这个例 子中它的类型就是int* file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 如果我们想访问第二个元素的地址我们可以写成如下的两种方式: &a[1]; a+1//注意这里的表示就是将a数组的起始地址向后进一位,移动到第二个元素的地址上也就是a[0]到a[1]的过 程! 数组名称和指针的关系其实很简单,其实数组名称代表的是数组的第一个元素的内存地址,这和指针的道理 是相似的! 下面我们来看一个完整的例子,利用指针来实现对数组元素的循环遍历访问! #include using namespace std; void main(void) { int a[2]={1,2}; int *pb=a; //定义指针*pb的地址为数组a的开始地址 int *pe=a+2; //定义指针*pb的地址为数组a的结束地址 cout << a << "|" << a[0] << "|" << *(a+1) << "|" << pb << "|" << *pb < using namespace std; int test(int a,int b); float test(float a,float b); void main() { cout << test(1,2) << endl << test(2.1f,3.14f) << endl; cin.get(); } int test(int a,int b) { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] return a+b; } float test(float a,float b) { return a+b; } 在上面的程序中我们同样使用了两个名为test的函数来描述int类型和操作的和float类型和操作,这样一来 就方便了程序员对相同或者相似功能函数的管理。 看了上面的解释很多人会问,这么一来计算机该如何来判断同名称函数呢?操作的时候会不会造成选择错误 呢? 回答是否定的。c++内部利用一种叫做名称粉碎的机智来内部重命名同名函数,上面的例子在计算重命名后 可能会是testii和testff 他们是通过参数的类型或个数来内部重命名的,关于这个作为程序员不需要去了解 它,说一下只是为了解释大家心中的疑问而已。好了,关于函数学重载的基础知识就说到这里,至于如何利用 这个功能,就靠大家在日常的学习或者是工作中逐渐摸索了。 新手入门:C/C++中枚举类型(enum) 文章来源:PConline 作者:管宁 如果一个变量你需要几种可能存在的值,那么就可以被定义成为枚举类型。之所以叫枚举就是说将变量或者 叫对象可能存在的情况也可以说是可能的值一一例举出来。 举个例子来说明一吧,为了让大家更明白一点,比如一个铅笔盒中有一支笔,但在没有打开之前你并不知 道它是什么笔,可能是铅笔也可能是钢笔,这里有两种可能,那么你就可以定义一个枚举类型来表示它! file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] enum box{pencil,pen};//这里你就定义了一个枚举类型的变量叫box,这个枚举变量内含有两个元素也称枚举 元素在这里是pencil和pen,分别表示铅笔和钢笔。 这里要说一下,如果你想定义两个具有同样特性枚举类型的变量那么你可以用如下的两种方式进行定义! enum box{pencil,pen}; enum box box2;//或者简写成box box2; 再有一种就是在声明的时候同时定义。 enum {pencil,pen}box,box2; //在声明的同时进行定义! 枚举变量中的枚举元素系统是按照常量来处理的,故叫枚举常量,他们是不能进行普通的算术赋值的, (pencil=1;)这样的写发是错误的,但是你可以在声明的时候进行赋值操作! enum box{pencil=1,pen=2}; 但是这里要特别注意的一点是,如果你不进行元素赋值操作那么元素将会被系统自动从0开始自动递增的进 行赋值操作,说到自动赋值,如果你只定义了第一个那么系统将对下一个元素进行前一个元素的值加1操作, 例如 enum box{pencil=3,pen};//这里pen就是4系统将自动进行pen=4的定义赋值操作! 前面说了那么多,下面给出一个完整的例子大家可以通过以下的代码的学习进行更完整的学习! #include using namespace std; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] void main(void) { enum egg {a,b,c}; enum egg test; //在这里你可以简写成egg test; test = c; //对枚举变量test进行赋予元素操作,这里之所以叫赋元素操作不叫赋值操作就是为了让大家 明白枚举变量是不能直接赋予算数值的,例如(test=1;)这样的操作都是不被编译器所接受的,正确的方式是 先进行强制类型转换例如(test = (enum egg) 0;)! if (test==c) { cout <<"枚举变量判断:test枚举对应的枚举元素是c" << endl; } if (test==2) { cout <<"枚举变量判断:test枚举元素的值是2" << endl; } cout << a << "|" << b << "|" << test < using namespace std; void main(void) { enum test {a,b}; int c=1+b; //自动提升为算术类型 cout << c < #include using namespace std; inline string dbtest(int a); //函数原形声明为inline即:内联函数 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] void main() { for (int i=1;i<=10;i++) { cout << i << ":" << dbtest(i) << endl; } cin.get(); } string dbtest(int a)//这里不用再次inline,当然加上inline也是不会出错的 { return (a%2>0)?"奇":"偶"; } 上面的例子就是标准的内联函数的用法,使用inline修饰带来的好处我们表面看不出来,其实在内部的工作 就是在每个for循环的内部所有调用dbtest(i)的地方都换成了(i%2>0)?"奇":"偶"这样就避免了频繁调用函数 对栈内存重复开辟所带来的消耗。 说到这里很多人可能会问,既然inline这么好,还不如把所谓的函数都声明成inline,嗯,这个问题是要注 意的,inline的使用是有所限制的,inline只适合函数体内代码简单的函数使用,不能包含复杂的结构控制语 句例如while switch,并且不能内联函数本身不能是直接递归函数(自己内部还调用自己的函数)。 说到这里我们不得不说一下在c语言中广泛被使用的#define语句,是的define的确也可以做到inline的这些 工作,但是define是会产生副作用的,尤其是不同类型参数所导致的错误,由此可见inline有更强的约束性和 能够让编译器检查出更多错误的特性,在c++中是不推荐使用define的。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 关于内联函数的更多例子我就不一一举出了,灵活的使用也多靠学习者本身,我只在此抛砖引玉,让大家尽 可能多的学习到c++中的一些新的先进的特性知识点。 新手入门:C++中堆内存(heap)的概念和操作方法 文章来源:PConline 作者:管宁 堆内存是什么呢? 我们知道在c/c++中定义的数组大小必需要事先定义好,他们通常是分配在静态内存空间或者是在栈内存空 间内的,但是在实际工作中,我们有时候却需要动态的为数组分配大小,在这里c库中的malloc.h头文件中的 malloc()函数就为您解决了问题(bc或者是在老的标准中是alloc.h),它的函数原形是void* malloc(size_t size),在动态开辟的内存中,在使用完后我们要使用free()函数来释放动态开辟的内存空间。 下面我们来看一个完整的例子: #include #include using namespace std; main() { int arraysize; //元素个数 int *array; //用于动态开辟数组的指针变量 cin>>arraysize; array=(int*)malloc(arraysize * sizeof(int));//利用malloc在堆内存中开辟内存空间,它的大小是元 素的个数乘以该数据类型的长度 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] for(int i=0;i file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] using namespace std; main() { int arraysize; //元素个数 int *array; cin>>arraysize; array=new int[arraysize];//开辟堆内存 for(int i=0;i符号来访问成员,下面我们就以上所说的看一个完整的例子: #include #include using namespace std; struct test//定义一个名为test的结构体 { int a;//定义结构体成员a int b;//定义结构体成员b }; void main() { test pn1;//定义结构体变量pn1 test pn2;//定义结构体变量pn2 pn2.a=10;//通过成员操作符.给结构体变量pn2中的成员a赋值 pn2.b=3;//通过成员操作符.给结构体变量pn2中的成员b赋值 pn1=pn2;//把pn2中所有的成员值复制给具有相同结构的结构体变量pn1 cout<a=99;//通过结构指针修改结构体变量pn2成员a的值 cout<a<<"|"<b< #include using namespace std; struct test { char name[10]; float socre; }; void print_score(test pn)//以结构变量进行传递 { cout<name<<"|"<socre< #include file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] using namespace std; struct test { char name[10]; float socre; }; void print_score(test &pn)//以结构变量进行传递 { cout< #include using namespace std; struct test { char name[10]; float socre; }; void print_score(test &pn) { cout<>pn.name>>pn.socre; return pn; } void main() { test a[2]; int num = sizeof(a)/sizeof(test); for(int i=0;i #include using namespace std; struct test { char name[10]; float socre; }; void print_score(test &pn) { cout<>pn.name>>pn.socre; } file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] void main() { test a[2]; int num = sizeof(a)/sizeof(test); for(int i=0;i>pn.name>>pn.socre; return pn; } file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 调用的时候在内部要在栈空间开辟一个名为pn的结构体变量,程序pn的时候又再次在栈内存空间内自动生 成了一个临时结构体变量temp,在前面的教程中我们已经说过,它是一个copy,而例程2中的: void get_score(test &pn) { cin>>pn.name>>pn.socre; } 却没有这一过程,不开辟任何新的内存空间,也没有任何临时变量的生成。 第二: 例程1在mian()中,必须对返回的结构体变量进行一次结构体变量与结构体变量直接的相互赋值操作。 for(int i=0;i #include using namespace std; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] struct test { char name[10]; float socre; }; test a; test &get_score(test &pn) { cin>>pn.name>>pn.socre; return pn; } void print_score(test &pn) { cout< #include using namespace std; int test(int a); void main(int argc,char* argv[]) { cout< #include using namespace std; int test(int a); void main(int argc,char* argv[]) { cout< #include using namespace std; int test(int); int test2(int (*ra)(int),int); void main(int argc,char* argv[]) { cout< #include using namespace std; void t1(){cout<<"test1";} void t2(){cout<<"test2";} void t3(){cout<<"test3";} void main(int argc,char* argv[]) { void* a[]={t1,t2,t3}; cout<<"比较t1()的内存地址和数组a[0]所存储的地址是否一致"< main() { int a,b; /* 定义a,b两个整形变量用于输入两个整数 */ int *point_1,*point_2,*temp_point; /* 定义三个指针变量 */ scanf("%d,%d",&a,&b); /* 格式化输入a,b的值 */ point_1=&a; /* 把指针变量point_1的值指向变量a的地址 */ point_2=&b; /* 把指针变量point_2的值指向变量b的地址 */ if (a main() { int a,b; /* 定义a,b两个整形变量用于输入两个整数 */ int *point_1,*point_2; /* 定义三个指针变量 */ scanf("%d,%d",&a,&b); /* 格式化输入a,b的值 */ point_1 = &a; /* 把指针变量point_1的值指向变量a的地址 */ point_2 = &b; /* 把指针变量point_2的值指向变量b的地址 */ compositor(point_1,point_2); /* 调用自定义的排序涵数,把a,b的地址传递给point_1和point_2 */ printf("%d,%d",a,b); /* 打印出a,b的值 */ } static compositor(p1,p2) int *p1,*p2; /* 定义形式参数p1,p2为指针变量 */ { int temp; /* 建立临时存储变量 */ if (*p1<*p2) /* 如果*p1 0); exit(1); } file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] for(i=1;i<=n;i++) { temp=temp+flag*i; flag=(-1)*flag; } return temp; } 搞定!当我用期待的目光看着面试官的时候,他微笑着跟我说,执行结果肯定是没有问题!但当n很大的时 候我这个程序执行效率很低,在嵌入式系统的开发中,程序的运行效率很重要,能让CPU少执行一条指令都是 好的,他让我看看这个程序还有什么可以修改的地方,把程序优化一下!听了这些话,我的心情当时变的有点 沉重,没想到他的要求很严格,之后我对程序进行了严格的分析,给出了改进了的方案! long fn(long n) { long temp=0; int j=1,i=1,flag=1; if(n<=0) file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { printf("error: n must > 0); exit(1); } while(j<=n) { temp=temp+i; i=-i; i>0?i++:i--; j++; } return temp; } 虽然我不敢保证我这个算法是最优的,但是比起上一个程序,我将所有涉及到乘法指令的语句改为执行加 法指令,既达到要题目的要求而且运算时间上缩短了很多!而代价仅仅是增加了一个整型变量!但是我现在的 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 信心已经受了一点打击,我将信将疑的看者面试官,他还是微笑着跟我说:“不错,这个程序确实在效率上有 的很大的提高!”我心里一阵暗喜!但他接着说这个程序仍然不能达到他的要求,要我给出更优的方案!天 啊!还有优化!我当时真的有点崩溃了,想了一会后,我请求他给出他的方案!然后他很爽快的给出了他的程 序! long fn(long n) { if(n<=0) { printf("error: n must > 0); exit(1); } if(0==n%2) return (n/2)*(-1); else return (n/2)*(-1)+n; } file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 搞笑,当时我目瞪口呆,没想到他是这个意思,这么简单的代码我真的不会写吗,但是我为什么没有往那 方面上想呢!他说的没有错,在n很大很大的时候这三个程序运行时间的差别简直是天壤之别!当我刚想开口 说点什么的时候,他却先开口了:“不要认为CPU运算速度快就把所有的问题都推给它去做,程序员应该将代 码优化再优化,我们自己能做的决不要让CPU做,因为CPU是为用户服务的,不是为我们 程序员服务的!”多么精辟的语言,我已经不想再说什么了!接着是第二个问题: 2). 他要求我用一种技巧性的编程方法来用一个函数实现两个函数的功能n为如: fn1(n)=n/2!+n/3!+n/4!+n/5!+n/6! fn2(n)=n/5!+n/6!+n/7!+n/8!+n/9! 现在用一个函数fn(int n,int flag)实现,当flag为0时,实现fn1功 能,如果flag为1时实现fn2功能!他的要求还是效率,效率,效率!说实在话,如果我心情好的话我应该能给 出一种比较好的算法,但我那时真的没有什么心思再想了,我在纸上胡乱画了一些诸如6!=6*5!的公式后直截 了当的跟他说要他给出他的答案!面试官也没有说什么,给出了他的思路: 定义一个二维数组 float t[2][5]存入{{2!,3!,4!,5!,6!},{5!,6!,7!,8!,9!}}然后给出一个循环: for(i=0;i<6;i++) { temp=temp+n/t[flag]; } 最后得到计算值!呵呵,典型的空间换时间的算法! 这些总共花了50分钟的时间,还有十分钟我就跟他很随意的聊聊天,聊了一些编程以及生活的问题,那时 的我已经很放松了,因为我知道这次面试结果只有一个:失败。5:30的时候面试官要我等通知,于是我离开了 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 他们公司。这就是面试的整个经过! 2.由面试想到的 真的是很失败啊!我记得那天下好大的雨,气温也很低,我边走边想,从5:30一直走到7:30,全身都湿透 了,又冷又饿,但是我只是一直走,脑子里面充满了疑惑,我也想让雨把自己淋醒!看到这里有些朋友可能觉 得那些面试题目不算什么如果让自己做的话肯定能全部答对,我肯定相信你,因为我从未怀疑过中国程序员的 能力,我认为中国有世界上最好的程序员,我也从未认为自己是高手,所以我做不出来不代表中国程序员比台 湾或者别的地方的程序员差,所以我就从我的角度,我的所见所想来谈一些感想: 不错全世界都有优秀的程序员,中国也不例外,但是我疑惑的是:到底中国和台湾或者国外的优秀的程序 员的比例到底是多少?台湾我不知道,中国100个程序员里有几个是优秀的呢?我根本算不上,从上面的表现就 足以说明一切了!是1个?5个?10个?50个?这个数字我不敢乱猜,恐遭网友一顿痛骂,那么我们国内有多少 人学习计算机呢?拿我们学校来说,计算机97级4个班,98级5个班,99级10个班,2000级17个班,人多了,老 师怎么办?我们学校的做法是让研究生上课,然后呢?补考一抓一大把,大把大把的补考费落入了学校的口 袋,还说现在的学生素质低!真是好笑,我都不知道学校这么做是为了什么,为国内培养大量的程序员吗?学 生们能真正学到计算机知识吗?好了,我敢讲,在我们学校学习编程学生和优秀程序员(注意我指的是优秀, 只会编几个糟烂程序的人算不上)的比例应该是100:0.1 在这种比例下虽然我们中国学习编程的人铺天盖地,但是想想有多少个人能真正为中国软件业发展作出贡 献,有多少人能真正写出优秀的程序名扬海外! 我从学习编程以来,不管是自学还是老师指导,从来都是解决问题就好,编出程序来就行,我的疑惑是: 我们有真正的强调过程序的效率,程序的质量吗?我们有仔细分析过我们写的东西,看看有没有可以改进的地 方,看看有没有简单的方法来达到同样的目的呢?我问心自问,我发现,我从来没有对我写出来的程序进行过 优化,最多就是进行详细的测试,然后Debug,但是这就足够了吗?这些天我偶尔发现我曾经写过的一个游 戏,那是一年前我刚加入vcroad.net做为其中一员时候,感觉应该拿点东西出来,然后花了一个星期的时间写 出来的!程序不算复杂,但是用到了不少数据结构的东西,也用到了一些精彩的算法,加上windows的界面和 游戏的可玩性,写完后受到了不少好评,我当时真的很佩服自己!但是现在看呢:没有一句注释,好多丑陋的 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 函数名比如:void chushihua(),好多没有必要的变量,可以用简单语句完成工作的我使用华丽的算法,大量 使用全局变量.....,说不好听的话,六百多行的程序除了能运行之外就是一陀屎!如果一年前我能听到一些反 面意见的话,大概我能早一点觉悟,但是自从原代码在网站发布以来听到的都是赞美之词,没有一个人向我提 出程序改进的意见,这又说明了一个什么问题呢?很值得思考啊! 还有一个疑惑是:我们说的和做的真的一样吗?我在学校的时候曾经受学院指派承办过一个计算机大赛, 请了一个老师出决赛的题目,主要是一些算法题目,这个老师可能是我上大学以来唯一敬佩的老师了,从程序 调试到打分,对于每个程序都仔细分析其时间效率和空间效率,然后综合打分,四十个人的卷子,老师从下午 三点一直调试到晚上十点,在有些写的精彩的语句后还加上批注。我真是高兴很遇到这样的老师并且和他做深 入的交流,但在事后,却发生了一件不愉快的事,在比赛中获得第二名的学生找到我,说他程序全部调试成功 应该给他满分,并且应该得第一,我说不过他,最后调出了他的原程序和第一名的原程序对比,不错,两个程 序都运行的很好,这时,那个同学开口了:“我的程序写的十分简捷明了,仅仅数行就完成了题目要求,而他 的却写了一大堆,为什么给他的分多过给我的分。”我当时很是气愤,如果不是老师负责的话,那么现在第一 名和第二名的位置真的要互调了,拜托,不是程序的行数越少程序的质量就越高,我记得我跟他大谈这方面的 道理,最后说服他了!哈哈,但是我,只能说说而已,我不知道还有多少人一样,说起来头头是道,但心里却 压根就从未重视过它! 3.我打算做的! 其实那天我想到的远不止上面那么多,但是我不想再说了,因为我猜想看这篇文章的网友大概都有一肚子 的感想,一肚子的抱怨,借用这篇文章发泄可不是我想达到的目的,在上面我把自己骂的一文不值也不是妄自 菲薄,但是在某些方面我真的做错了,或者说是偏离了正确方向,现在是矫正方向和重整旗鼓的时候了,就象 我前面说过的,我相信中国有世界上最好的程序员,我也相信我的水平不会一直保持现状,我现在就收拾起牢 骚真正的实干起来! 真的很巧,就写到这里的时候我在网上偶尔发现了这篇手册,我不知道这预示着什么,但是我想如果我照 下面这个基本原则一直踏实做下去,我一定会实现我的理想---一名优秀的软件设计师! (下面这些文字不是我的原创,是我偶尔在网上发现的,我真的很幸运能看到这些,这篇文章也随着下面 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 的文字而结束,我真心的希望您能从这篇文章中得到启发,这篇文章欢迎大家随意转载,您可以不写作者是 谁,但是请您写上vcroad.net原创,谢谢您的支持) 作者:金蝶中间件公司CTO袁红岗 不知不觉做软件已经做了十年,有成功的喜悦,也有失败的痛苦,但总不敢称自己是高手,因为和我心目 中真正的高手们比起来,还差的太远。世界上并没有成为高手的捷径,但一些基本原则是可以遵循的。 1. 扎实的基础。数据结构、离散数学、编译原理,这些是所有计算机科学的基础,如果不掌握他们,很难 写出高水平的程序。据我的观察,学计算机专业的人比学其他专业的人更能写出高质量的软件。程序人人都会 写,但当你发现写到一定程度很难再提高的时候,就应该想想是不是要回过头来学学这些最基本的理论。不要 一开始就去学OOP,即使你再精通OOP,遇到一些基本算法的时候可能也会束手无策。 2. 丰富的想象力。不要拘泥于固定的思维方式,遇到问题的时候要多想几种解决问题的方案,试试别人从 没想过的方法。丰富的想象力是建立在丰富的知识的基础上,除计算机以外,多涉猎其他的学科,比如天文、 物理、数学等等。另外,多看科幻电影也是一个很好的途径。 3. 最简单的是最好的。这也许是所有科学都遵循的一条准则,如此复杂的质能互换原理在爱因斯坦眼里不 过是一个简单得不能再简单的公式:E=mc2。简单的方法更容易被人理解,更容易实现,也更容易维护。遇到 问题时要优先考虑最简单的方案,只有简单方案不能满足要求时再考虑复杂的方案。 4. 不钻牛角尖。当你遇到障碍的时候,不妨暂时远离电脑,看看窗外的风景,听听轻音乐,和朋友聊聊 天。当我遇到难题的时候会去玩游戏,而且是那种极暴力的打斗类游戏,当负责游戏的那部分大脑细胞极度亢 奋的时候,负责编程的那部分大脑细胞就得到了充分的休息。当重新开始工作的时候,我会发现那些难题现在 竟然可以迎刃而解。 5. 对答案的渴求。人类自然科学的发展史就是一个渴求得到答案的过程,即使只能知道答案的一小部分也 值得我们去付出。只要你坚定信念,一定要找到问题的答案,你才会付出精力去探索,即使最后没有得到答 案,在过程中你也会学到很多东西。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 6. 多与别人交流。三人行必有我师,也许在一次和别人不经意的谈话中,就可以迸出灵感的火花。多上上 网,看看别人对同一问题的看法,会给你很大的启发。 7. 良好的编程风格。注意养成良好的习惯,代码的缩进编排,变量的命名规则要始终保持一致。大家都知 道如何排除代码中错误,却往往忽视了对注释的排错。注释是程序的一个重要组成部分,它可以使你的代码更 容易理解,而如果代码已经清楚地表达了你的思想,就不必再加注释了,如果注释和代码不一致,那就更加糟 糕。 8. 韧性和毅力。这也许是"高手"和一般程序员最大的区别。A good programming is 99 weat and 1 offee。高手们并不是天才,他们是在无数个日日夜夜中磨练出来的。成功能给我们带来无比的喜悦,但过程 却是无比的枯燥乏味。你不妨做个测试,找个10000以内的素数表,把它们全都抄下来,然后再检查三遍,如 果能够不间断地完成这一工作,你就可以满足这一条。 这些是我这几年程序员生涯的一点体会,希望能够给大家有所帮助。 入门教程:C++中的const限定修饰符 文章来源:PConline 作者:管宁 const修饰符可以把对象转变成常数对象,什么意思呢? 意思就是说利用const进行修饰的变量的值在程序的任意位置将不能再被修改,就如同常数一样使用! 使用方法是: const int a=1;//这里定义了一个int类型的const常数变量a; 但就于指针来说const仍然是起作用的,以下有两点要十分注意,因为下面的两个问题很容易混淆! file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 我们来看一个如下的例子: #include using namespace std; void main(void) { const int a=10; int b=20; const int *pi; pi=&a; cout <<*pi << "|" << a < file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] using namespace std; void main(void) { int a=10; const int *const pi=&a; cout <<*pi << "|" < using namespace std; void main(void) { const int a=10;//这句和上面不同,请注意! file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] const int *const pi=&a; cout <<*pi << "|" < file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] using namespace std; class Internet { public: Internet(char *name,char *address) { strcpy(Internet::name,name); strcpy(Internet::address,address); } friend void ShowN(Internet &obj);//友元函数的声明 public: char name[20]; char address[20]; }; void ShowN(Internet &obj)//函数定义,不能写成,void Internet::ShowN(Internet &obj) { cout< using namespace std; class Country; class Internet { public: Internet(char *name,char *address) { strcpy(Internet::name,name); strcpy(Internet::address,address); } friend void ShowN(Internet &obj,Country &cn);//注意这里 public: char name[20]; char address[20]; }; class Country { public: Country() { strcpy(cname,"中国"); } friend void ShowN(Internet &obj,Country &cn);//注意这里 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] protected: char cname[30]; }; void ShowN(Internet &obj,Country &cn) { cout< using namespace std; class Internet; class Country { public: Country() { strcpy(cname,"中国"); file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] } void Editurl(Internet &temp);//成员函数的声明 protected: char cname[30]; }; class Internet { public: Internet(char *name,char *address) { strcpy(Internet::name,name); strcpy(Internet::address,address); } friend void Country::Editurl(Internet &temp);//友元函数的声明 protected: char name[20]; char address[20]; }; void Country::Editurl(Internet &temp)//成员函数的外部定义 { strcpy(temp.address,"edu.cndev-lab.com"); cout< using namespace std; class Internet; class Country { public: Country() { strcpy(cname,"中国"); } friend class Internet;//友类的声明 protected: char cname[30]; }; class Internet { public: Internet(char *name,char *address) { strcpy(Internet::name,name); strcpy(Internet::address,address); file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] } void Editcname(Country &temp); protected: char name[20]; char address[20]; }; void Internet::Editcname(Country &temp) { strcpy(temp.cname,"中华人民共和国"); } void main() { Internet a("中国软件开发实验室","www.cndev-lab.com"); Country b; a.Editcname(b); cin.get(); } 在上面的代码中我们成功的通过Internet类Editcname成员函数操作了Country类的保护成员cname。 在编程中,我们使用友元的另外一个重要原因是为了方便重载操作符的使用,这些内容我们将在后面的教 程着重讨论! C++运算符重载函数基础及其值返回状态 文章来源:PConline 作者:管宁 运算符重载是C++的重要组成部分,它可以让程序更加的简单易懂,简单的运算符使用可以使复杂函数的理解 更直观。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 对于普通对象来说我们很自然的会频繁使用算数运算符让他们参与计算,但是对于自定义类的对象来说, 我们是无论如何也不能阻止写出像下面的代码一样的程序来的。 例子如下: class Test { //过程省略 } int main() { Test a,c; c=a+a; } 当然这样的代码是不能够通过编译的,c++对自定类的算术运算部分保留给了程序员,这也是符合c++灵活 特性的。 在c++中要想实现这样的运算就必须自定义运算符重载函数,让它来完整具体工作。 在这里要提醒读者的是,自定义类的运算符重载函数也是函数,你重载的一切运算符不会因为是你自己定义 的就改变其运算的优先级,自定义运算符的运算优先级同样遵循与内部运算符一样的顺序。 除此之外,c++也规定了一些运算符不能够自定义重载,例如.、::、.*、.->、?:。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 下面我们来学习如何重载运算符,运算符重载函数的形式是: 返回类型 operator 运算符符号 (参数说明) { //函数体的内部实现 } 运算符重载函数的使用主要分为两种形式,一种是作为类的友元函数进行使用,另一种则是作为类的成员 函数进行使用。 下面我们先看一下作为类的友元函数使用的例子: //程序作者:管宁 //站点:www.cndev-lab.com //所有稿件均有版权,如要转载,请务必著名出处和作者 #include using namespace std; class Test { public: Test(int a = 0) { Test::a = a; } friend Test operator +(Test&,Test&); friend Test& operator ++(Test&); public: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] int a; }; Test operator +(Test& temp1,Test& temp2)//+运算符重载函数 { //cout< using namespace std; class Test { public: Test(int a = 0) { Test::a = a; } friend Test operator +(Test&,const int&); public: int a; }; Test operator +(Test& temp1,const int& temp2)//+运算符重载函数 { Test result(temp1.a * temp2); return result; } int main() { Test a(100); file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] Test c = a + 10; cout< using namespace std; class Test { public: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] Test(int a = 0) { Test::a = a; } Test(Test &temp) //运算符重载函数为值返回的时候会产生临时变量,临时变量与局部变量result的复制会调用拷贝构 造函数,临时变量的生命周期是在拷贝构造函数运行完成后才结束,但如果运算符重载函数返回的是引用,那 么不会产生临时变量,而局部变量result的生命周期在运算符重载函数退出后立即消失,它的生命周期要比临 时变量短,所以当外部对象获取返回值的内存地址所存储的值的时候,获得是一个已经失去效果的内存地址中 的值,在这里的值返回与引用返回的对比,证明了临时变量的生命周期比局部变量的生命周期稍长。 { cout<<"载入拷贝构造函数"<<"|"<a<a+temp2.a); return result; } Test& operator ++()//++运算符重载函数 //递增运算符是单目运算符,使用返回引用的运算符重载函数道理就在于它需要改变自身。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] //在前面我们学习引用的单元中我们知道,返回引用的函数是可以作为左值参与运算的,这一点也符 合单目运算符的特点。 //如果把该函数改成返回值,而不是返回引用的话就破坏了单目预算改变自身的特点,程序中的 ++(++c)运算结束后输出c.a,会发现对象c只做了一次递增运算,原因在于,当函数是值返回状态的时候括号 内的++c返回的不是c本身而是临时变量,用临时变量参与括号外的++运算,当然c的值也就只改变了一次。 { this->a++; return *this; } public: int a; }; int main() { Test a(100); Test c=a+a; cout<a+temp2.a); return result; } 执行运算符重载函数返回引用将不产生临时变量,外部的Test c=a+a; 将获得一个局部的,栈空间内存地址 位置上的值,而栈空间的特性告诉我们,当函数退出的时候函数体中局部对象的生命周期随之结束,所以保存 在该地址中的数据也将消失,当c对象去获取存储在这个地址中的值的时候,里面的数据已经不存在,导致c获 得的是一个随机值,所以作为双目运算的加运算符重载函数是不益采用返回引用方式编写的,当然如果一定要 返回引用,我们可以在堆内存中动态开辟空间存储数据,但是这么做会导致额外的系统开销,同时也会让程序 更难读懂。 对于递增运算符来说,它的意义在于能够改变自身,返回引用的函数是可以作为左值参与运算的,所以作 为单目运算符,重载它的函数采用返回引用的方式编写是最合适的。 如果我们修改递增运算符重载函数为值返回状态的时候,又会出现什么奇怪的现象呢? 代码如下: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] Test operator ++() { return this->a++; } 表面上是发现不出什么特别明显的问题的,但是在main()函数中++(++c);的执行结果却出乎意料,理论上应 该是204的值,却只是203,这是为什么呢? 因为当函数是值返回状态的时候括号内的++c返回的不是c本身而是临时变量,用临时变量参与括号外的++运 算,当然c的值也就只改变了一次。结果为203而不是204。 对于运算符重载函数来说,最后我们还要注意一个问题,当运算符重载函数的形式参数类型全部为内部类 型的时候,将不能重载。 C++运算符重载赋值运算符 文章来源:PConline 作者:管宁 自定义类的赋值运算符重载函数的作用与内置赋值运算符的作用类似,但是要要注意的是,它与拷贝构造函 数与析构函数一样,要注意深拷贝浅拷贝的问题,在没有深拷贝浅拷贝的情况下,如果没有指定默认的赋值运 算符重载函数,那么系统将会自动提供一个赋值运算符重载函数。 赋值运算符重载函数的定义与其它运算符重载函数的定义是差不多的。 下面我们以实例说明如何使用它,代码如下: //程序作者:管宁 //站点:www.cndev-lab.com //所有稿件均有版权,如要转载,请务必著名出处和作者 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] #include using namespace std; class Internet { public: Internet(char *name,char *url) { Internet::name = new char[strlen(name)+1]; Internet::url = new char[strlen(url)+1]; if(name) { strcpy(Internet::name,name); } if(url) { strcpy(Internet::url,url); } } Internet(Internet &temp) { Internet::name=new char[strlen(temp.name)+1]; Internet::url=new char[strlen(temp.url)+1]; if(name) { strcpy(Internet::name,temp.name); } if(url) file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { strcpy(Internet::url,temp.url); } } ~Internet() { delete[] name; delete[] url; } Internet& operator =(Internet &temp)//赋值运算符重载函数 { delete[] this->name; delete[] this->url; this->name = new char[strlen(temp.name)+1]; this->url = new char[strlen(temp.url)+1]; if(this->name) { strcpy(this->name,temp.name); } if(this->url) { strcpy(this->url,temp.url); } return *this; } public: char *name; char *url; }; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] int main() { Internet a("中国软件开发实验室","www.cndev-lab.com"); Internet b = a;//b对象还不存在,所以调用拷贝构造函数,进行构造处理。 cout< using namespace std; class Test { public: Test(int a = 0) { cout<a<a< using namespace std; class Test { public: Test(int a = 0) { cout<a<a< using namespace std; class Test { public: Test(int a = 0) { cout<a<a< using namespace std; class Test { public: Test(int a = 0) { cout<a<a+temp2.a); return result; } operator int() file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { cout<a< using namespace std; class B; class A { public: A(B &);//转换构造函数,他的作用是用B类对象构造A类对象 void Edita(int temp) { A::a=temp; } public: int a; }; class B file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { public: B(int a=0) { B::a=a; } int Ra() { return B::a; } operator A()//转换运算符重载函数,他的作用则是将B类对象转换成A类对象 { return *this; } protected: int a; }; A::A(B &temp) { cout< using namespace std; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] int main() { int a=0; ++(++a);//正确,(++a)返回的是左值 (a++)++;//错误,(a++)返回的不是左值 system("pause"); } 代码中(a++)++编译出错误,返回“++”需要左值的错误,这正是前递增与后递增的差别导致的,那么又是 为什么呢? 原因主要是由C++对递增(增量)运算符的定义引发的。 他们之间的差别主要为以下两点: 1、运算过程中,先将对象进行递增修改,而后返回该对象(其实就是对象的引用)的叫前递增(增量)运 算。在运算符重载函数中采用返回对象引用的方式编写。 2、运算过程中,先返回原有对象的值,而后进行对象递增运算的叫后递增(增量)运算。在运算符重载函数 中采用值返回的方式编写(这也正是前面(a++)++出错误的原因,(a++)返回的不是引用,不能当作左值继续参 加扩号外部的++运算),重载函数的内部实现必须创建一个用于临时存储原有对象值的对象,函数返回的时候 就是返回该临时对象。 那么在编写运算符重载函数的时候我们该如何区分前递增运算符重载函数与后递增运算符重载函数呢? 方法就是:在后递增运算符重载函数的参数中多加如一个int标识,标记为后递增运算符重载函数。 具体见如下实例(例一为非成员方式,例二为成员方式): file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] //例一 //程序作者:管宁 //站点:www.cndev-lab.com //所有稿件均有版权,如要转载,请务必著名出处和作者 #include using namespace std; class Test { public: Test(int a=0) { Test::a = a; } friend Test& operator ++ (Test&); friend Test operator ++ (Test&,int); public: int a; }; Test& operator ++ (Test &temp)//前递增 { temp.a++; return temp; } Test operator ++ (Test &temp,int)//后递增,int在这里只起到区分作用,事实上并没有实际作用 { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] Test rtemp(temp);//这里会调用拷贝构造函数进行对象的复制工作 temp.a++; return rtemp; } int main() { Test a(100); ++(++a); cout< using namespace std; class Test { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] public: Test(int a=0) { Test::a = a; } Test& operator ++ (); Test operator ++ (int); public: int a; }; Test& Test::operator ++ ()//前递增 { this->a++; return *this; } Test Test::operator ++ (int)//后递增 { Test rtemp(*this);//这里会调用拷贝构造函数进行对象的复制工作 this->a++; return rtemp; } int main() { Test a(100); ++(++a); cout< using namespace std; class test { private://私有成员类外不能够直接访问 int number; public://共有成员类外能够直接访问 float socre; public: int rp() { return number; } void setnum(int a) { number=a; } file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] }; void main() { test a; //a.number=10;//错误的,私有成员不能外部访问 a.socre=99.9f; cout< using namespace std; int pp=0; class test { private: int number; public: float socre; int pp; public: void rp(); file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] }; void test::rp()//在外部利用域区分符定义test类的成员函数 { ::pp=11;//变量名前加域区分符给全局变量pp赋值 pp=100;//设置结构体变量 } void main() { test a; test b; a.rp(); cout<pp=100; 所以你把上面的成员函数写成如下形势也是正确的: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] void test::rp() { ::pp=11; this->pp=100;//this指针就是指向a对象的指针 } 类的成员函数和普通函数一样是可以进行重载操作的,关于重载函数前面已经说过,这里不再说明。 给出例子仔细看: #include using namespace std; class test { private: int number; public: float socre; int pp; public: void rp(int); void rp(float); }; void test::rp(int a)//在外部利用域区分符定义test类的成员函数 { cout<<"调用成员函数!a:"< using namespace std; class test { private: int number; public: float socre; int pp; public: int rp(int); }; int test::rp(int a)//在外部利用域区分符定义test类的成员函数 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { number=100; return a + number; } void run(test *p)//利用指针调用 { cout<rp(100)< #include "ballscore.h" using namespace std; void main() { ballscore jeff; cout< using namespace std; class ballscore { protected: const static int gbs = 5;//好球单位得分 const static int bbs = -3;//坏球单位扣分 float gradescore;//平均成绩 public: float GetGS(float goodball,float badball) //goodball为好球数量,badball为坏求数量 { int gradescore=0; //新定义一个和成员变量float gradescore相同名字的类成员函数局部变量 ballscore::gradescore = (goodball*gbs + badball*bbs) / (goodball + badball); //由于局部变量与类成员变量同名使用的时候必须在其前加上类名和 域区分符 return ballscore::gradescore;//返回平均成绩 } }; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] int ballscore=0;//定义一个与类名称相同的普通全局变量 int test; void main() { class test//局部类的创建 { float a; float b; }; test test; ::test=1; //由于局部类名隐藏了外部变量使用需加域区分符 class ballscore jeff; //由于全局变量int ballsocre和类(ballsocre)名称相同,隐藏了类名称,这时 候定义类对象需加class前缀以区分 cout< using namespace std; class Student { public: Student()//无参数构造函数 { number = 1; score = 100; } void show(); protected: int number; int score; }; void Student::show() file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { cout< using namespace std; class Teacher { public: Teacher(char *input_name)//有参数的构造函数 { name=new char[10]; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] //name=input_name;//这样赋值是错误的 strcpy(name,input_name); } void show(); protected: char *name; }; void Teacher::show() { cout< using namespace std; class Teacher { public: Teacher(char *input_name) { name=new char[10]; //name=input_name;//这样赋值是错误的 strcpy(name,input_name); } Teacher()//无参数构造函数,进行函数重载 { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] } void show(); protected: char *name; }; void Teacher::show() { cout< using namespace std; class Teacher { public: Teacher() { director = new char[10]; strcpy(director,"王大力"); } char *show(); protected: char *director; }; char *Teacher::show() { return director; } class Student { public: Student() { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] number = 1; score = 100; } void show(); protected: int number; int score; Teacher teacher;//这个类的成员teacher是用Teacher类进行创建并初始化的 }; void Student::show() { cout< #include using namespace std; class Teacher file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { public: Teacher() { director = new char[10]; strcpy(director,"王大力"); //director = new string; // *director="王大力";//string情况赋值 } ~Teacher() { cout<<"释放堆区director内存空间1次"; delete[] director; cin.get(); } char *show(); protected: char *director; //string *director; }; char *Teacher::show() { return director; } class Student { public: Student() { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] number = 1; score = 100; } void show(); protected: int number; int score; Teacher teacher; }; void Student::show() { cout< #include using namespace std; class Teacher { public: Teacher(char *temp) { director = new char[10]; strcpy(director,temp); } ~Teacher() { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] cout<<"释放堆区director内存空间1次"; delete[] director; cin.get(); } char *show(); protected: char *director; }; char *Teacher::show() { return director; } class Student { public: Student() { number = 1; score = 100; } void show(); protected: int number; int score; Teacher teacher("王大力");//错误,一个类的成员如果是另外一个类的对象的话,不能在类中使用带参 数的构造函数进行初始化 }; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] void Student::show() { cout< using namespace std; class Teacher { public: Teacher(char *temp) { director = new char[10]; strcpy(director,temp); } ~Teacher() { cout<<"释放堆区director内存空间1次"; delete[] director; cin.get(); } char *show(); protected: char *director; }; char *Teacher::show() { return director; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] } class Student { public: Student(char *temp):teacher(temp) { number = 1; score = 100; } void show(); protected: int number; int score; Teacher teacher; }; void Student::show() { cout< #include using namespace std; class Teacher { public: Teacher(char *temp) { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] director = new char[10]; strcpy(director,temp); } ~Teacher() { cout<<"释放堆区director内存空间1次"; delete[] director; cin.get(); } char *show(); protected: char *director; }; char *Teacher::show() { return director; } class Student { public: Student(char *temp,int &pk):teacher(temp),pk(pk),ps(10) { number = 1; score = 100; } void show(); protected: int number; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] int score; Teacher teacher; int &pk; const int ps; }; void Student::show() { cout< #include using namespace std; class Test { public: Test(int a) { kk=a; cout<<"构造参数a:"< using namespace std; class Test { public: Test(int j):pb(j),pa(pb+5) { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] } public: int pa; int pb; }; void main() { Test a(10); cout< ////////////////// // 一个典型的类——有三个数据成员... // class CFooble { protected: int x,y,z; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] public: // 两个构造函数... CFooble(int i) { x=y=z=i; } CFooble(int xx, int yy, int zz) : x(xx),y(yy),z(zz) { } // 一个输出函数 void print() { printf("CFooble at %p: (%d,%d,%d)\n", this, x, y, z); } // 这个函数检查是否为空... int IsEmpty() { return x==0 && y==0 && z==0; } }; #ifdef NEVER // 如下这样将不能运行—不能“生硬”地进行C++类对象的初始化! CFooble table[] = { { 1,2,3 }, { 4,5,6 }, { 0,0,0 } }; #endif // 以下是如何初始化一个类数组: CFooble table[] = { CFooble(1,2,3), file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] CFooble(4,5,6), CFooble(0), // 甚至可以是用不同的构造器! }; void main() { for (CFooble* pc=table; !pc->IsEmpty(); pc++) { pc->print(); } } // 在C++中,你能用用任何构造函数初始化数组元素,并且C++甚至用缺省的构造函数来初始化额外的元素, 而不用外部的初始华例程。对我来说,这是一种进步,而不是不足。 C++基础:常量成员函数特殊说明 文章来源:csdn 作者:smallmark 1. 传指针时,我们可以通过指针来修改它在外部所指向的内容。但如果要修改外部指针所指向的对象是不 可能的。例如传递外部指针到函数内来分配空间,必须传递指针的指针或指针的引用。 2. char carry[10] = {0}; 编译器会将其后所有的东西都置0; 3. 函数返回值为const时,返回的东西付给一个类型相同的标示后其不能为左值; 4. const int *i; int const *i; int * const i; 前两个功能相同,说明I所指向的内容不变;最后一个 说明指针指向的地址不变,但内容可变。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 5. 类中的const成员函数。定义为在原型后加const。常量函数不能修改类中的任何属性。但有两种方法可 以修改。 a) {(myclass *)this->member1 = values;} b) 将一个成员定义成mutable即可被常量函数修改。 6. 类中的常量const 类型的,不能在类中被用来定义数组。而enum {ONE=100; TWO=2};定义的ONE、TWO却 可以。通常的enum定义的置分配问题:enum A{ L=9, Z};此时Z的值为10。 7. 用const定义的int可用来开辟数组,但const定义的常量数组中的元素,不能用来定义数组。 8. 用sizeof计算变量的空间,如果是数组,按实际空间返回;常量字符串(实际上是在静态内存区开辟的 变量)sizeof返回比实际长度加一。如果是指针则不考虑它指向的空间大小,仅仅返回指针类型的大小。如果 用sizeof计算函数的行参,即使是属组也仅仅返回一个相关类型指针的大小。 9. 形如int iarray[] = {12, 124, 433};编译器会自动给iarray分配3个元素的长度。元素长度的个数计算 公式为sizeof(iarray) / sizeof(*iarray)。 10. 拷贝构造函数:当行参和实参结合时,如果是复杂对象的传值类型,则调用拷贝构造函数生成一个临时 对象作为实参,退出函数时,临时对象被调用析构函数释放。当返回值是复杂对象是,也是调用拷贝构造函数 来赋值。这就出现构造函数和析构函数被调用次数不相等的情况。拷贝构造函数的原型为A(A&),我们可在类 中重载。(缺省的拷贝构造函数是使用位(bit)拷贝方法:浅层拷贝,不拷贝指针指向的内容)。 11. volatile类型的变量告诉编译器,本变量不需要进行代码优化。在多线程的应用中,我们如果读入一个 变量到寄存器,此时时间片到期,去处理其他线程了,在重新获得处理机时,volatile类型告诉处理机,重新 从变量读取数据到寄存器,而不是用寄存器数据直接处理,这样可以防止脏数据。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 12. class 和struct在一定程度上有相同的功能,只不过前者缺省的成员是私有的,后者在缺省时成员为共 有的。故而class不是c++必需的保留字 13. c和c++编译器,对相同的函数名编译后生成的相同的标示不同,故而在引用c的库文件时必须使用 extern “C”告诉编译器,它是c的函数,按c的规则编译。通常我们使用的标准头文件已被处理过。 14. #include “filename”; #include ,前者先在当前目录下寻找文件,如果找不到再到系 统规定的路径下找,后者直接到系统规定的路径下找。 15. 任何地方分配的静态变量(static),其生命周期和主进程相同。第二次定义一个已存在的static变 量,对变量的内用无影响,但它的可见范围只在定义的范围内。(考研曾作错!)(从静态变量的特性不难理 解,类中的static类型是所有对象共享的) 16. 内联函数(inline)在实现上实际和宏类似,在内联函数出现的地方将函数展开来避免函数调用时的出 栈、如栈,提高效率。但内联函数的代价是:代码增大。inline函数适合成员函数和自由函数。在类中实现的 函数自动为内联函数。inline必须定义到函数的实现上,例如:inline int PlusOne(int) 是无效的。友元函 数在类的体内被实现自动变为内联函数。 17. #include #define DEBUG(X) cout<<#X"="<) file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 19. 静态对象在main结束或exit()被调用时才调用自身的析构函数。这意味着,在对象的析构函数中调用 exit()是很危险的,有可能进入一个死循环中。调用abort()来退出函数,静态对象的析构函数并不会被调 用。我们可以用atexit()来指定跳出main或调用exit时要执行的操作,用atexit注册的函数,可以在所有对象 的析构函数之前调用。 void exit_fn2(void) { printf("Exit function #2 called\n"); } //处理函数 atexit(exit_fn2); 20. 全局变量实际上用的是静态存储。静态变量的构造是在进入main之前调用的,在main结束时调用它的析 构函数。变量的名字由小范围(c++而言): //*.cpp int a; //静态变量,但为 extern int a; 即它是全局的,外部可见的 static int b; //静态变量,static 和extern相反,只在*.cpp中有效,对其他单元(文件)是不可见的。 函数的定义和上面相同。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] main() { } 类的静态成员变量可以如下赋值:int X::s=23;(在*.cpp中,无论公私都可以) 21. 名字空间(namespace): 定义一个名字空间,然后使用unsing就可以将当前的类型上下文转换名字空间 所定地的. namespace math { enum sign{positive, negative}; class integer{ int i; sign s; public: interger(int I=0): i(i) {⋯⋯⋯} file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] sign Sign() {⋯⋯⋯} ⋯⋯⋯⋯⋯⋯⋯.. };//end class interger A, B, C; interger divide(interger, interger); }//no ; void q() { using namespace math; interger A; //hides math::A A.Sign(negative); Math::A.Sign(positive); } file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 22. 一般对于函数flaot f(int a, int b); 某些c++编译器编译后生成_f_int_int的名字,有些c编译器则 生成_f的名字。故在c++中链接c的库函数时要用extern “C”告诉编译器,按c的规则来编译函数。类似的还 有extern “C”{#include “myhead.h”},c++还支持extern “C++”{}. 23. 在函数调用时,传引用也是将指针压栈。 24. 构造函数、析构函数、赋值构造函数、重载的=,四者的调用顺序:(三种函数都已实现) a) X x; X a=x; result: X:construct X:copy_struct b) X x; X a; a=x; Result: X:construct file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] X:construct X:copy_stru operator = X:destruct 如果没有赋值构造函数则结果: X:construct X:construct operator = X:destruct (如果直接X a=x;这不掉用一般的构造函数,调用复制构造函数) 指向类的成员函数的指针:设 int X:: a(void){} file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] X x; int (X:: *pf)(void)= &X::a; (x.*pf)(); 指向成员变量的指针: 设int i; 是X的成员变量 int X::*pm = &X::i; X x; C++中的虚函数(virtual function) 文章来源:c++社区 一.简介 虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设 我们有下面的类层次: class A { public: virtual void foo() { cout << "A::foo() is called" << endl;} }; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] class B: public A { public: virtual void foo() { cout << "B::foo() is called" << endl;} }; 那么,在使用的时候,我们可以: A * a = new B(); a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的! 这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所 谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定 的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。 虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是虚函数,但它不是 多态的: class A { public: virtual void foo(); }; class B: public A { virtual void foo(); }; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] void bar() { A a; a.foo(); // A::foo()被调用 } 1.1 多态 在了解了虚函数的意思之后,再考虑什么是多态就很容易了。仍然针对上面的类层次,但是使用的方法变 的复杂了一些: void bar(A * a) { a->foo(); // 被调用的是A::foo() 还是B::foo()? } 因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,无从确定这里被调用的是A::foo()还是 B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调用,如果a指向的是B类的实例,则 B::foo()被调用。 这种同一代码可以产生不同效果的特点,被称为“多态”。 1.2 多态有什么用? 多态这么神奇,但是能用来做什么呢?这个命题我难以用一两句话概括,一般的C++教程(或者其它面向对 象语言的教程)都用一个画图的例子来展示多态的用途,我就不再重复这个例子了,如果你不知道这个例子, 随便找本书应该都有介绍。我试图从一个抽象的角度描述一下,回头再结合那个画图的例子,也许你就更容易 理解。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个 类层次的使用者在使用它们的时候,如果仍然在需要基类的时候写针对基类的代码,在需要派生类的时候写针 对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变(增加了新类),都需 要使用者“知道”(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合,有人把这种情况列为程 序中的“bad smell”之一。 多态可以使程序员脱离这种窘境。再回头看看1.1中的例子,bar()作为A-B这个类层次的使用者,它并不知 道这个类层次中有多少个类,每个类都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来 后,bar()也不需要“知道”(修改)。这完全归功于多态--编译器针对虚函数产生了可以在运行时刻确定被 调用函数的代码。 编译器是如何针对虚函数产生可以再运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被 编译器处理的呢?Lippman在深度探索C++对象模型[1]中的不同章节讲到了几种方式,这里把“标准的”方式 简单介绍一下。 我所说的“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数, 就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的 一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基 类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译 器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到 一个虚函数调用的时候,就会将这个调用改写,针对1.1中的例子: void bar(A * a) { a->foo(); } file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 会被改写为: void bar(A * a) { (a->vptr[1])(); } 因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样 的方法可以在运行时刻决定调用哪个foo()函数。 虽然实际情况远非这么简单,但是基本原理大致如此。 1.4 overload和override 虚函数总是在派生类中被改写,这种改写被称为“override”。我经常混 淆“overload”和“override”这两个单词。但是随着各类C++的书越来越多,后来的程序员也许不会再犯我 犯过的错误了。但是我打算澄清一下: override是指派生类重写基类的虚函数,就象我们前面B类中重写了A类中的foo()函数。重写的函数必须有 一致的参数表和返回值(C++标准允许返回值不同的情况,这个我会在“语法”部分简单介绍,但是很少编译 器支持这个feature)。这个单词好象一直没有什么合适的中文词汇来对应,有人译为“覆盖”,还贴切一 些。 overload约定成俗的被翻译为“重载”。是指编写一个与已有函数同名但是参数表不同的函数。例如一个函数 即可以接受整型数作为参数,也可以接受浮点数作为参数。 二. 虚函数的语法 虚函数的标志是“virtual”关键字。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 2.1 使用virtual关键字 考虑下面的类层次: class A { public: virtual void foo(); }; class B: public A { public: void foo(); // 没有virtual关键字! }; class C: public B // 从B继承,不是从A继承! { public: void foo(); // 也没有virtual关键字! }; 这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,基类声明的虚函数,在派生 类中也是虚函数,即使不再使用virtual关键字。 2.2 纯虚函数 如下声明表示一个函数为纯虚函数: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] class A { public: virtual void foo()=0; // =0标志一个虚函数为纯虚函数 }; 一个函数声明为纯虚后,纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚函数用来规范派生 类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有这个函数。 2.3 虚析构函数 析构函数也可以是虚的,甚至是纯虚的。例如: class A { public: virtual ~A()=0; // 纯虚析构函数 }; 当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子: class A { public: A() { ptra_ = new char[10];} ~A() { delete[] ptra_;} // 非虚析构函数 private: char * ptra_; }; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] class B: public A { public: B() { ptrb_ = new char[20];} ~B() { delete[] ptrb_;} private: char * ptrb_; }; void foo() { A * a = new B; delete a; } 在这个例子中,程序也许不会象你想象的那样运行,在执行delete a的时候,实际上只有A::~A()被调用 了,而B类的析构函数并没有被调用!这是否有点儿可怕? 如果将上面A::~A()改为virtual,就可以保证B::~B()也在delete a的时候被调用了。因此基类的析构函数 都必须是virtual的。 纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类(不能实例化的 类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。 2.4 虚构造函数? 构造函数不能是虚的。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 三. 虚函数使用技巧 3.1 private的虚函数 考虑下面的例子: class A { public: void foo() { bar();} private: virtual void bar() { ...} }; class B: public A { private: virtual void bar() { ...} }; 在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者 protected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情 况,也不会发生B::bar()对A::bar()的override不起作用的情况。 这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用 这个函数。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 3.2 构造函数和析构函数中的虚函数调用 一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。 也就是说不能在构造函数和析构函数中让自己“多态”。例如: class A { public: A() { foo();} // 在这里,无论如何都是A::foo()被调用! ~A() { foo();} // 同上 virtual void foo(); }; class B: public A { public: virtual void foo(); }; void bar() { A * a = new B; delete a; } 如果你希望delete a的时候,会导致B::foo()被调用,那么你就错了。同样,在new B的时候,A的构造函 数被调用,但是在A的构造函数中,被调用的是A::foo()而不是B::foo()。 3.3 多继承中的虚函数 3.4 什么时候使用虚函数 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设 计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法, 就可以将对象的行为抽象化。 以设计模式[2]中Factory Method模式为例,Creator的factoryMethod()就是虚函数,派生类override这个 函数后,产生不同的Product类,被产生的Product类被基类的AnOperation()函数使用。基类的AnOperation() 函数针对Product类进行操作,当然Product类一定也有多态(虚函数)。 另外一个例子就是集合操作,假设你有一个以A类为基类的类层次,又用了一个std::vector来保存这个类 层次中不同类的实例指针,那么你一定希望在对这个集合中的类进行操作的时候,不要把每个指针再cast回到 它原来的类型(派生类),而是希望对他们进行同样的操作。那么就应该将这个“一样的操作”声明为 virtual。 现实中,远不只我举的这两个例子,但是大的原则都是我前面说到的“如果发现一个函数需要在派生类里 有不同的表现,那么它就应该是虚的”。这句话也可以反过来说:“如果你发现基类提供了虚函数,那么你最 好override它”。 附:C++中的虚函数和纯虚函数用法 1.虚函数和纯虚函数可以定义在同一个类(class)中,含有纯虚函数的类被称为抽象类(abstract class),而只含有虚函数的类(class)不能被称为抽象类(abstract class)。 2.虚函数可以被直接使用,也可以被子类(sub class)重载以后以多态的形式调用,而纯虚函数必须在子 类(sub class)中实现该函数才可以使用,因为纯虚函数在基类(base class) 只有声明而没有定义。 3.虚函数和纯虚函数都可以在子类(sub class)中被重载,以多态的形式被调用。 4.虚函数和纯虚函数通常存在于抽象基类(abstract base class -ABC)之中,被继承的子类重载,目的是 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 提供一个统一的接口。 5.虚函数的定义形式:virtual {method body} ;纯虚函数的定义形式:virtual { } = 0; 在虚函数和纯 虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时候要求前期bind,然而虚函 数却是动态绑定(run-time bind),而且被两者修饰的函数生命周期(life recycle)也不一样。 6.如果一个类中含有纯虚函数,那么任何试图对该类进行实例化的语句都将导致错误的产生,因为抽象基类 (ABC)是不能被直接调用的。必须被子类继承重载以后,根据要求调用其子类的方法。 以下为一个简单的虚函数和纯虚寒数的使用演示,目的是抛砖引玉! #include //father class class Virtualbase { public: virtual void Demon()= 0; //prue virtual function virtual void Base() {cout<<"this is farther class"<}; //sub class class SubVirtual :public Virtualbase { public: void Demon() { cout<<" this is SubVirtual!"< void Base() { cout<<"this is subclass Base"<}; /* instance class and sample */ void main() { Virtualbase* inst = new SubVirtual(); //multstate pointer file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] inst->Demon(); inst->Base(); // inst = new Virtualbase(); // inst->Base() return ; } C++中类的多态与虚函数的使用 文章来源:互联网 类的多态特性是支持面向对象的语言最主要的特性,有过非面向对象语言开发经历的人,通常对这一章节的 内容会觉得不习惯,因为很多人错误的认为,支持类的封装的语言就是支持面向对象的,其实不然,Visual BASIC 6.0 是典型的非面向对象的开发语言,但是它的确是支持类,支持类并不能说明就是支持面向对象,能 够解决多态问题的语言,才是真正支持面向对象的开发的语言,所以务必提醒有过其它非面向对象语言基础的 读者注意! 多态的这个概念稍微有点模糊,如果想在一开始就想用清晰用语言描述它,让读者能够明白,似乎不太现 实,所以我们先看如下代码: //例程1 #include using namespace std; class Vehicle { public: Vehicle(float speed,int total) { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] Vehicle::speed=speed; Vehicle::total=total; } void ShowMember() { cout< using namespace std; class Vehicle { public: Vehicle(float speed,int total) { Vehicle::speed=speed; Vehicle::total=total; } void ShowMember() { cout< using namespace std; class Vehicle { public: Vehicle(float speed,int total) { Vehicle::speed = speed; Vehicle::total = total; } file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] virtual void ShowMember()//虚函数 { cout< using namespace std; class Vehicle { public: Vehicle(float speed,int total) { Vehicle::speed=speed; Vehicle::total=total; } virtual void ShowMember() { cout<ShowMember(); DelPN(a); cin.get(); } 从上例代码的运行结果来看,当调用DelPN(a);后,在析构的时候,系统成功的确定了先调用Car类的析构函 数,而如果将析构函数的virtual修饰去掉,再观察结果,会发现析构的时候,始终只调用了基类的析构函 数,由此我们发现,多态的特性的virtual修饰,不单单对基类和派生类的普通成员函数有必要,而且对于基 类和派生类的析构函数同样重要。 C++类对象的复制-拷贝构造函数 文章来源:pconline 作者:管宁 在学习这一章内容前我们已经学习过了类的构造函数和析构函数的相关知识,对于普通类型的对象来说,他 们之间的复制是很简单的,例如: int a = 10; int b =a; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 自己定义的类的对象同样是对象,谁也不能阻止我们用以下的方式进行复制,例如: #include using namespace std; class Test { public: Test(int temp) { p1=temp; } protected: int p1; }; void main() { Test a(99); Test b=a; } 普通对象和类对象同为对象,他们之间的特性有相似之处也有不同之处,类对象内部存在成员变量,而普通 对象是没有的,当同样的复制方法发生在不同的对象上的时候,那么系统对他们进行的操作也是不一样的,就 类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的,在上面的代码中,我们并没有看 到拷贝构造函数,同样完成了复制工作,这又是为什么呢?因为当一个类没有自定义的拷贝构造函数的时候系 统会自动提供一个默认的拷贝构造函数,来完成复制工作。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 下面,我们为了说明情况,就普通情况而言(以上面的代码为例),我们来自己定义一个与系统默认拷贝构 造函数一样的拷贝构造函数,看看它的内部是如何工作的! 代码如下: #include using namespace std; class Test { public: Test(int temp) { p1=temp; } Test(Test &c_t)//这里就是自定义的拷贝构造函数 { cout<<"进入copy构造函数"< using namespace std; class Internet { public: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] Internet(char *name,char *address) { cout<<"载入构造函数"< using namespace std; class Internet { public: Internet() { }; Internet(char *name,char *address) { cout<<"载入构造函数"< using namespace std; class Internet { public: Internet(char *name,char *address) { cout<<"载入构造函数"< using namespace std; class Internet { public: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] Internet(char *name,char *address) { cout<<"载入构造函数"< #include using namespace std; class Test { public: Test(int a=0,int b=0) file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { Test::a=a; Test::b=b; } int a; int b; }; int main() { Test t(100,50); printf("%???",t);//不明确的输出格式 scanf("%???",t);//不明确的输入格式 cout<>t;//同样不够明确 system("pause"); } 由于自定义类的特殊性,在上面的代码中,无论你使用c风格的输入输出,或者是c++的输入输出都不是不明 确的一个表示,由于c语言没有运算符重载机制,导致stdio库的不可扩充性,让我们无法让printf()和 scanf()支持对自定义类对象的扩充识别,而c++是可以通过运算符重载机制扩充iostream库的,使系统能能够 识别自定义类型,从而让输入输出明确的知道他们该干什么,格式是什么。 在上例中我们之所以用printf与cout进行对比目的是为了告诉大家,C与C++处理输入输出的根本不同,我 们从c远的输入输出可以很明显看出是函数调用方式,而c++的则是对象模式,cout和cin是ostream类和 istream类的对象。 C++中的iostream库主要包含下图所示的几个头文件: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 我们所熟悉的输入输出操作分别是由istream(输入流)和ostream(输出流)这两个类提供的,为了允许双向的 输入/输出,由istream和ostream派生出了iostream类。 类的继承关系见下图: iostream库定义了以下三个标准流对象: 1.cin,表示标准输入(standard input)的istream类对象。cin使我们可以从设备读如数据。 2.cout,表示标准输出(standard output)的ostream类对象。cout使我们可以向设备输出或者写数据。 3.cerr,表示标准错误(standard error)的osttream类对象。cerr是导出程序错误消息的地方,它只能允 许向屏幕设备写数据。 输出主要由重载的左移操作符(<<)来完成,输入主要由重载的右移操作符(>>)完成。 >>a表示将数据放入a对象中。 < using namespace std; int main() { ofstream myfile("c:\\1.txt",ios::out|ios::trunc,0); myfile<<"中国软件开发实验室"< #include using namespace std; int main() { ofstream myfile("c:\\1.txt",ios::app,0); if(!myfile)//或者写成myfile.fail() { cout<<"文件打开失败,目标文件状态可能为只读!"; system("pause"); exit(1); } myfile<<"中国软件开发实验室"< #include using namespace std; int main() { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] ofstream myfile; myfile.open("c:\\1.txt",ios::out|ios::app,0); if(!myfile)//或者写成myfile.fail() { cout<<"文件创建失败,磁盘不可写或者文件为只读!"; system("pause"); exit(1); } myfile<<"中国软件开发实验室"< #include #include using namespace std; int main() { ifstream myfile; myfile.open("c:\\1.txt",ios::in,0); if(!myfile) file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { cout<<"文件读错误"; system("pause"); exit(1); } char ch; string content; while(myfile.get(ch)) { content+=ch; cout.put(ch);//cout< #include using namespace std; int main() { fstream myfile; myfile.open("c:\\1.txt",ios::out|ios::app,0); if(!myfile) { cout<<"文件写错误,文件属性可能为只读!"< #include using namespace std; int main() { char *name = "www.cndev-lab.com"; int arraysize = strlen(name)+1; istrstream is(name,arraysize); char temp; is>>temp; cout< #include using namespace std; int main() { int arraysize=1; char *pbuffer=new char[arraysize]; ostrstream ostr(pbuffer,arraysize,ios::out); ostr< #include using namespace std; int main() { istringstream istr; istr.str("1 56.7",); //上述两个过程可以简单写成 istringstream istr("1 56.7"); file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] cout << istr.str()<>a; cout<>b; cout< #include #include file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] using namespace std; int main() { ostringstream ostr; //ostr.str("abc");//如果构造的时候设置了字符串参数,那么增长操作的时候不会从结尾开始增加,而是修改 原有数据,超出的部分增长 ostr.put('d'); ostr.put('e'); ostr<<"fg"; string gstr = ostr.str(); cout< #include #include using namespace std; int main() { stringstream ostr("ccc"); ostr.put('d'); ostr.put('e'); ostr<<"fg"; string gstr = ostr.str(); cout<>a; cout< file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] #include #include using namespace std; int main() { stringstream sstr; //--------int转string----------- int a=100; string str; sstr<>str; cout<>cname; cout< using namespace std; int main() { int a; cin>>a; cout< using namespace std; int main() { int a; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] cin>>a; cout< using namespace std; int main() { int a; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] cin>>a; cout< using namespace std; int main() { int a; while(1) { cin>>a; if(!cin)//条件可改写为cin.fail() { cout<<"输入有错!请重新输入"< #include using namespace std; int main() { ifstream myfile("c:\\1.txt",ios_base::in,0); if(myfile.fail()) { cout<<"文件读取失败或指定文件不存在!"< using namespace std; int main() { float pi=3.14159f; cout< #include using namespace std; int main() { float pi=3.14159f; cout< #include using namespace std; int main() { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] char str[100]; cin.getline(str,sizeof(str),'\n'); cout< using namespace std; class Test { public: Test(int age = 0,char *name = "\0") { Test::age = age; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] strcpy(Test::name,name); } void outmembers(ostream &out) { out<<"Age:"<name< using namespace std; class Test { public: Test(int age = 0,char *name = "\0") { Test::age = age; strcpy(Test::name,name); } void outmembers(ostream &out) { out<<"Age:"<name<outmembers(out); return out; } protected: int age; char name[50]; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] }; int main() { Test a(24,"管宁"); a< #include using namespace std; class Test { public: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] Test(int age = 0,char *name = "\0") { Test::age = age; strcpy(Test::name,name); } void outmembers(ostream &out) { out<<"Age:"<name< #include using namespace std; class Student { public: Student(int age = 0,char *name = "\0") { Student::age = age; strcpy(Student::name,name); } virtual void outmembers(ostream &out) = 0; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] friend ostream& operator <<(ostream& ,Student&); protected: int age; char name[50]; }; ostream& operator <<(ostream& out,Student &temp) { temp.outmembers(out); return out; } class Academician:public Student { public: Academician(int age = 0,char *name = "\0",char *speciality="\0"):Student(age,name) { strcpy(Academician::speciality,speciality); } virtual void outmembers(ostream &out) { out<<"Age:"< using namespace std; class Test { public: Test(int age = 0,char *name = "\0") { Test::age = age; strcpy(Test::name,name); } void inputmembers(istream &out) { cout<<"please input age:"; cin>>Test::age; cout<<"please input name:"; cin>>Test::name; } friend istream& operator >>(istream& ,Test&); public: int age; char name[50]; }; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] istream& operator >>(istream& input,Test &temp) { temp.inputmembers(input); return input; } int main() { Test a; cin>>a; cout< using namespace std; int main() { int x,y; // mouse coordinates // ..assign values to x and y ofstream archive("coord.dat", ios::binary); archive.write(reinterpret_cast(&x), sizeof (x)); archive.write(reinterpret_cast(&x), sizeof (x)); file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] archive.close(); } 使用reinterpret_cast<>是必要的,因为write()的第一个参数类型为const char*,但&x和&y是int*类 型。 以下代码读取刚才存储的值: #include using namespace std; vint main() { int x,y; ifstream archive("coord.dat"); archive.read((reinterpret_cast(&x), sizeof(x)); archive.read((reinterpret_cast(&y), sizeof(y)); } 序列化对象 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 要序列化一个完整的对象,应把每个数据成员写入文件中: class MP3_clip { private: std::time_t date; std::string name; int bitrate; bool stereo; public: void serialize(); void deserialize(); //.. }; void MP3_clip::serialize() { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { int size=name.size();// store name's length //empty file if it already exists before writing new data ofstream arc("mp3.dat", ios::binary|ios::trunc); arc.write(reinterpret_cast(&date),sizeof(date)); arc.write(reinterpret_cast(&size),sizeof(size)); arc.write(name.c_str(), size+1); // write final '\0' too arc.write(reinterpret_cast(&bitrate), sizeof(bitrate)); arc.write(reinterpret_cast(&stereo), sizeof(stereo)); } 实现deserialize() 需要一些技巧,因为你需要为字符串分配一个临时缓冲区。做法如下: void MP3_clip::deserialize() file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { ifstream arce("mp3.dat"); int len=0; char *p=0; arc.read(reinterpret_cast(&date), sizeof(date)); arc.read(reinterpret_cast(&len), sizeof(len)); p=new char [len+1]; // allocate temp buffer for name arc.read(p, len+1); // copy name to temp, including '\0' name=p; // copy temp to data member delete[] p; arc.read(reinterpret_cast(&bitrate), sizeof(bitrate)); arc.read(reinterpret_cast(&stereo), sizeof(stereo)); } file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 性能优化 你可能会感到迷惑,为什么不把整个对象一次性转储到文件中,而必须对每个数据成员进行序列化呢?换 句话说,难道不能用下面的方式实现serialize() 吗? void MP3_clip::serialize() { ofstream arc("mp3.dat", ios::binary|ios::trunc); arc.write(reinterpret_cast(this),sizeof(*this)); } 不行,不能这样做。这种方式至少存在两个问题。通常,当被序列化的对象还包含其它一些对象时,你不 能简单地把该对象转储到一个文件中并指望以后从中重建一个有效的对象。在我们的例子中,外层对象包含一 个std::string成员,一个浅拷贝(shallow copy)操作会把std::string成员归档,但其值是时变的,意思是 说每次运行程序时都可能改变。更糟的是,由于std::string事实上并不包含一个字符数组,而是一个指针, 使用浅拷贝试图重建原始字符串是不可能的。为克服这个问题,程序没有序列化string对象,而是归档其含有 的字符和长度。一般来说,指针,数组和句柄应以相同的方式进行处理。 另一个问题设计到多态对象。每个多态对象都含有一个vtpr,即一个指向虚拟函数地址分配表的隐藏指 针。vtpr的值是时变的,如果你把整个多态对象转储到一个文件中,然后强行把归档后的数据添加到一个新的 对象上,则其vptr可能无效并导致未定义的行为。再次提醒,解决方案是只对非时变的数据成员进行序列化和 反序列化。另一种方法是计算vptr的确切偏移量,在从文件重建对象时不要动它。记住,vptr的位置是与实现 相关的,因此这样的代码是不可移植的。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 小结 虽然C++不直接支持对象持久性,但手工实现它并不难,只要你遵从一些基本的准则:首先把每个复合对象 分解为原始数据类型,然后对这些原始数据类型进行序列化。当序列化数据时,记住要跳过时变的值。在反序 列化过程中,读取刚才存储的值。处理string对象、数组和句柄需要一些技巧:总是要对它们解引用,存储它 们所指向的值。记住在一个单独的字段中存储string或数组的大小。 C++:谁动了我的指针 文章来源:csdn 作者:lifanxi 译者序: 本文介绍了一种在调试过程中寻找悬挂指针(野指针)的方法,这种方法是通过对new和delete运算符的重 载来实现的。 这种方法不是完美的,它是以调试期的内存泄露为代价来实现的,因为文中出现的代码是绝不能出现在一个 最终发布的软件产品中的,只能在调试时使用。 在VC中,在调试环境下,可以简单的通过把new替换成DEBUG_NEW来实现功能更强更方便的指针检测,详情可 参考MSDN。DEBUG_NEW的实现思路与本文有相通的地方,因此文章中介绍的方法虽然不是最佳的,但还算实 用,更重要的是,它提供给我们一种新的思路。 简介: 前几天发生了这样一件事,我正在调试一个程序,这个程序用了一大堆乱七八糟的指针来处理一个链表,最 终在一个指向链表结点的指针上出了问题。我们预计它应当指向的是一个虚基类的对象。我想到第一个问题 是:指针所指的地方真的有一个对象吗?出问题的指针值可以被4整除,并且不是NULL的,所以可以断定它曾 经是一个有效的指针。通过使用Visual Studio的内存查看窗口(View->Debug Windows->Memory)我们发现这 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 个指针所指的数据是FE EE FE EE FE EE ...这通常意味着内存是曾经是被分配了的,但现在却处于一种未分 配的状态。不知是谁、在什么地方把我的指针所指的内存区域给释放掉了。我想要找出一种方案来查出我的数 据到底是怎么会被释放的。 背景: 我最终通过重载了new和delete运算符找到了我丢失的数据。当一个函数被调用时,参数会首先被压到栈上 后,然后返回地址也会被压到栈上。我们可以在new和delete运算符的函数中把这些信息从栈上提取出来,帮 助我们调试程序。 代码: 在经历了几次错误的猜测后,我决定求助于重载new和delete运算符来帮我找到我的指针所指向的数据。下 面的new运算符的实现把返回地址从栈上提了出来。这个返回地址位于传递过来的参数和第一个局部变量的地 址之间。编译器的设置、调用函数的方法、计算机的体系结构都会引响到这个返回地址的实际位置,所以您在 使用下面代码的时候,要根据您的实际情况做一些调整。一旦new运算符获得了返回地址,它就在将要实际分 配的内存前面分配额外的16个字节的空间来存放这个返回地址和实际的分配的内存大小,并且把实际要分配的 内存块首地址返回。 对于delete运算符,你可以看到,它不再释放空间。它用与new同样的方法把返回地址提取出来,写到实际 分配空间大小的后面(译者注:就是上面分配的16个字节的第9到第12个字节),在最后四个字节中填上DE AD BE EF(译者注:四个十六进制数,当成单词来看正好是dead beef,用来表示内存已释放真是很形象!),并 且把剩余的空间(译者注:就是原本实际应该分配而现在应该要释放掉的空间)都填上一个重复的值。 现在,如果程序由于一个错误的指针而出错,我只需打开内存查看窗口,找到出错的指针所指的地方,再往 前找16个字节。这里的值就是调用new运算符的地址,接下来四个字节就是实际分配的内存大小,第三个四个 字节是调用delete运算符的地址,最后四个字节应该是DE AD BE EF。接下的实际分配过的内存内容应该是77 77 77 77。 要通过这两个返回地址在源程序中分别找到对应的new和delete,可以这样做:首先把表示地址的四个字节 的内容倒序排一下,这样才能得到真正的地址,这里因为在Intel平台上字节序是低位在前的。下一步,在源 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 代码上右击点击,选“Go To Diassembly”。在反汇编的窗口上的左边一栏就是机器代码对应的内存地址。按 Ctrl + G或选择Edit->Go To...并输入你找到的地址之一。反汇编的窗口就将滚动到对应的new或delete的函 数调用位置。要回到源程序只需再次右键单击,选择“Go To Source”。您就可以看到相应的new或delete的 调用了。 现在您就可以很方便的找出您的数据是何时丢失的了。至于要找出为什么delete会被调用,就要靠您自己 了。 #include void * :perator new(size_t size) { int stackVar; unsigned long stackVarAddr = (unsigned long)&stackVar; unsigned long argAddr = (unsigned long)&size; void ** retAddrAddr = (void **)(stackVarAddr/2 + argAddr/2 + 2); void * retAddr = * retAddrAddr; unsigned char *retBuffer = (unsigned char*)malloc(size + 16); memset(retBuffer, 0, 16); memcpy(retBuffer, &retAddr, sizeof(retAddr)); memcpy(retBuffer + 4, &size, sizeof(size)); file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] return retBuffer + 16; } void :perator delete(void *buf) { int stackVar; if(!buf) return; unsigned long stackVarAddr = (unsigned long)&stackVar; unsigned long argAddr = (unsigned long)&buf; void ** retAddrAddr = (void **)(stackVarAddr/2 + argAddr/2 + 2); void * retAddr = * retAddrAddr; unsigned char* buf2 = (unsigned char*)buf; buf2 -= 8; memcpy(buf2, &retAddr, sizeof(retAddr)); size_t size; buf2 -= 4; memcpy(&size, buf2, sizeof(buf2)); buf2 += 8; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] buf2[0] = 0xde; buf2[1] = 0xad; buf2[2] = 0xbe; buf2[3] = 0xef; buf2 += 4; memset(buf2, 0x7777, size); // deallocating destroys saved addresses, so dont // buf -= 16; // free(buf); } 其它值得关注的地方: 这段代码同样可以用于内存泄露的检测。只需修改delete运算符使它真正的去释放内存,并且在程序退出 前,用__heapwalk遍历所有已分配的内存块并把调用new的地址提取出来,这就将得到一份没有被delete匹配 的new调用列表。 还要注意的是:这里列出的代码只能在调试的时候去使用,如果你把它段代码放到最终的产品中,会导致程 序运行时内存被大量的消耗。 C/C++中结构体(struct)知识点强化 文章来源:pconline 作者:管宁 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 在上一个教程中我们已经简单的阐述了什么是结构体了,为了进一部的学习结构体这一重要的知识点,我们 今天来学习一下链表结构。 结构体可以看做是一种自定义的数据类型,它还有一个很重要的特性,就是结构体可以相互嵌套使用,但 也是有条件的,结构体可以包含结构体指针,但绝对不能在结构体中包含结构体变量。 struct test { char name[10]; float socre; test *next; };//这样是正确的! struct test { char name[10]; float socre; test next; };//这样是错误的! 利用结构体的这点特殊特性,我们就可以自己生成一个环环相套的一种射线结构,一个指向另一个。 链表的学习不像想象的那么那么容易,很多人学习到这里的时候都会碰到困难,很多人也因此而放弃了学 习,在这里我说,一定不能放弃,对应它的学习我们要进行分解式学习,方法很重要,理解需要时间,不必要 把自己逼迫的那么紧,学习前你也得做一些最基本的准备工作,你必须具备对堆内存的基本知识的了解,还有 就是对结构体的基本认识,有了这两个重要的条件,再进行分解式学习就可以比较轻松的掌握这一节内容的难 点。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 下面我们给出一个完整的创建链表的程序,不管看的懂看不懂希望读者先认真看一下,想一想,看不懂没 有关系,因为我下面会有分解式的教程,但之前的基本思考一定要做,要不即使我分解了你也是无从理解的。 代码如下,我在重要部分做了注解: #include using namespace std; struct test { char name[10]; float socre; test *next; }; test *head;//创建一个全局的引导进入链表的指针 test *create() { test *ls;//节点指针 test *le;//链尾指针 ls = new test;//把ls指向动态开辟的堆内存地址 cin>>ls->name>>ls->socre; head=NULL;//进入的时候先不设置head指针指向任何地址,因为不知道是否一上来就输入null跳出程序 le=ls;//把链尾指针设置成刚刚动态开辟的堆内存地址,用于等下设置le->next,也就是下一个节点的位置 while(strcmp(ls->name,"null")!=0)//创建循环条件为ls->name的值不是null,用于循环添加节点 { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] if(head==NULL)//判断是否是第一次进入循环 { head=ls;//如果是第一次进入循环,那么把引导进入链表的指针指向第一次动态开辟的堆内存地 址 } else { le->next=ls;//如果不是第一次进入那么就把上一次的链尾指针的le->next指向上一次循环结束 前动态创建的堆内存地址 } le=ls;//设置链尾指针为当前循环中的节点指针,用于下一次进入循环的时候把上一次的节点的next 指向上一次循环结束前动态创建的堆内存地址 ls=new test;//为下一个节点在堆内存中动态开辟空间 cin>>ls->name>>ls->socre; } le->next=NULL;//把链尾指针的next设置为空,因为不管如何循环总是要结束的,设置为空才能够在循环显 链表的时候不至于死循环 delete ls;//当结束的时候最后一个动态开辟的内存是无效的,所以必须清除掉 return head;//返回链首指针 } void showl(test *head) { cout<<"链首指针:"<name<<"|"<socre<next; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] } } void main() { showl(create()); cin.get(); cin.get(); } 上面的代码我们是要达到一个目的:就是要存储你输入的人名和他们的得分,并且以链状结构把它们组合成 一个链状结构。 程序种有两个组成部分 test *create() 和 void showl(test *head) 这两个函数,create是用来创建链表的 ,showl是用来显示链表的。 create函数的返回类型是一个结构体指针,在程序调用的时候我们用了showl(create());,而不用引用的 目的原因是引导指针是一个全局指针变量,我们不能在showl()内改变它,因为showl()函数内有一个移动操作 head=head->next;,如果是引用的话我们就破坏了head指针的位置,以至于我们再也无法找会首地址的位置 了。 下面我们来分解整个程序,以一个初学者的思想来思考整个程序,由浅入深的逐步解释。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 首先,我们写这个程序,要考虑到由于是一个链表结构,我们不可能知道它的大小到底是多大,这个问题 我们可以用动态开辟堆内存来解决,因为堆内存在程序结束前始终是有效的,不受函数栈空间生命期的限制, 但要注意的是我们必须有一个指针变量来存储这一链状结构的进入地址,而在函数内部来建立这一指针变量显 然是不合适的,因为函数一旦退出,这个指针变量也随之失效,所以我们在程序的开始声明了一个全局指针变 量。 test *head;//创建一个全局的引导进入链表的指针 好解决了这两个问题,我们接下去思考 有输入就必然有输出,由于输出函数和输入函数是相对独立的,为了不断测试程序的正确性好调试我们先 写好输出函数和main函数捏的调用,创建函数我们先约定好名为create。 我们先写出如下的代码: #include using namespace std; struct test { char name[10]; float socre; test *next; }; test *head;//创建一个全局的引导进入链表的指针 test *create() file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { return head;//返回链首指针 } void showl(test *head) { cout<<"链首指针:"<name<<"|"<socre<next; } } void main() { showl(create()); cin.get(); cin.get(); } 程序写到这里,基本形态已经出来,输入和调用我们已经有了。 下面我们来解决输入问题,链表的实现我们是通过循环输入来实现的,既然是循环我们就一定得考虑终止循 环的条件,避免死循环和无效循环的发生。 在create()函数内部我们先写成这样: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] test *create() { test *ls;//节点指针 test *le;//链尾指针 ls = new test;//把ls指向动态开辟的堆内存地址 cin>>ls->name>>ls->socre; head=NULL;//进入的时候先不设置head指针指向任何地址,因为不知道是否一上来就输入null跳出程序 le=ls;//把链尾指针设置成刚刚动态开辟的堆内存地址,用于等下设置le->next,也就是下一个节点的位置 le->next=NULL;//把链尾指针的next设置为空,因为不管如何循环总是要结束的,设置为空才能够在循环显 链表的时候不至于死循环 delete ls;//当结束的时候最后一个动态开辟的内存是无效的,所以必须清除掉 return head;//返回链首指针 } 在循环创建之前我们必须考虑一个都不输入的情况。 程序一单进入create函数我们首先必然要创建一个节点,我们先创建一个节点指针,后把者个节点指针指 向到动态开辟的test类型的动态内存地址位置上。 所以我们有了 test *ls; ls = new test; 程序既然是循环输入,而结构成员test *next又是用来存储下一个接点的内存地址的,每次循环我们又要动 态创建一个新的内存空间,所以我们必须要有一个指针来存储上一次循环动态开辟的内存地址,于是就有了 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] test *le; 接下来在进入循环前我们要创建链表的第一个节点,第一个节点必然是在循环外创建,于是就有了 cin>>ls->name>>ls->socre; 程序执行者的情况是位置的,所以我们必然要考虑,一上来就不想继续运行程序的情况,所以我们一开始先 把head引导指针设置为不指向任何地址也就是 head=NULL; 为了符合le也就是链尾指针的设计思路,我们在循环前一定要保存刚刚动态开辟的内存地址,好在下一次循 环的时候设置上一个节点中的next成员指向,于是我们便有了: le=ls; 为了实现循环输入我们又了下面的代码: while(strcmp(ls->name,"null")!=0) { if(head==NULL) { head=ls; } else { le->next=ls; } file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] le=ls; ls=new test; cin>>ls->name>>ls->socre; } 程序是循环必然要有终止循环的条件,所以我们的循环条件是: while(strcmp(ls->name,"null")!=0) 输入的名字是null的时候就停止循环。 为了保证第一次进入循环,也就是在循环内准备创建第二个节点前,设置引导指针的指向我们有了如下的 判断代码: if(head==NULL) { head=ls; } else { le->next=ls; } 代码中的else条件是为了设置前一个节点next指向而写的,这点我们记住先看下面的代码,稍后大家回过头想 就明白了 le=ls; ls=new test; cin>>ls->name>>ls->socre; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] le=ls;这么写就是为了保存上一次循环指针的位置而设的,正是为了上面的else代码而做的预先保留 ls=new test; cin>>ls->name>>ls->socre; 这两行代码的意思就是继续开辟下一个节点空间,和输入节点内容! 循环一旦结束也就结束了程序,为了保持程序不出错,也就是最后一个节点的next成员指向为空我们有了下 面的代码 le->next=NULL; 程序的思路始终是以先开辟后判断为思路的,所以到最后一个不成立的时候总会有一个多开辟的内存空间,为 了删除掉它,我们有了下面的代码 delete ls; 程序到最后由于返回head指针 return head; 显示链表的函数没有什么太多特别的也只需要注意下面这样就可以了! head=head->next; 我们之所以不用head+=1;来写就是因为链表是我们动态开辟的,而每一个节点的位置并不是相连的,next成 员指针的意义也就是下一个节点的内存地址。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 到这里整个创建函数的设计思路也都说完了,笔者不一定说的很好,但基本思路是这样的,希望读者多思 考,多对比,相信此教程还是对大家有帮助的,程序设计就是利用逐步思考的方式进行的,写好的代码往往直 接看看不懂就是因为中间的细节并不是一次都能够想到的。 下面我们来说一下链表节点的删除! 我们以上面的程序为基础,但为了我们方便学习删除我们休整结构体为 struct test { int number; float socre; test *next; }; number为唯一的编号每一个节点的。 删除的我就不多说了,里面重要部分有注解。 特别注意deletel函数的参数意义,指针的引用在这里很重要,如果只是指针,或者只是应用都是不行的, 为什么仔细思考,很多知名的教材在这一问题上都很模糊,而且很多书还有错误,程序不错,但思路是错的, 我这里特别不说,请大家仔细阅读程序,如果还是有问题,可以回此帖,我会回答的。 完整代码如下: #include using namespace std; struct test { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] int number; float socre; test *next; }; test *head;//创建一个全局的引导进入链表的指针 test *create() { test *ls;//节点指针 test *le;//链尾指针 ls = new test;//把ls指向动态开辟的堆内存地址 cin>>ls->number>>ls->socre; head=NULL;//进入的时候先不设置head指针指向任何地址,因为不知道是否一上来就输入null跳出程序 le=ls;//把链尾指针设置成刚刚动态开辟的堆内存地址,用于等下设置le->next,也就是下一个节点的位置 while(ls->number!=0)//创建循环条件为ls->number的值不是null,用于循环添加节点 { if(head==NULL)//判断是否是第一次进入循环 { head=ls;//如果是第一次进入循环,那么把引导进入链表的指针指向第一次动态开辟的堆内存地 址 } else { le->next=ls;//如果不是第一次进入那么就把上一次的链尾指针的le->next指向上一次循环结束 前动态创建的堆内存地址 } le=ls;//设置链尾指针为当前循环中的节点指针,用于下一次进入循环的时候把上一次的节点的next 指向上一次循环结束前动态创建的堆内存地址 ls=new test;//为下一个节点在堆内存中动态开辟空间 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] cin>>ls->number>>ls->socre; } le->next=NULL;//把链尾指针的next设置为空,因为不管如何循环总是要结束的,设置为空才能够在循环显 链表的时候不至于死循环 delete ls;//当结束的时候最后一个动态开辟的内存是无效的,所以必须清除掉 return head;//返回链首指针 } void showl(test *head) { cout<<"链首指针:"<number<<"|"<socre<next; } } void deletel(test *&head,int number)//这里如果参数换成test *head,意义就完全不同了,head变成了复制 而不是原有链上操作了,特别注意,很多书上都不对这里 { test *point;//判断链表是否为空 if(head==NULL) { cout<<"链表为空,不能进行删除工作!"; return; } if(head->number==number)//判删除的节点是否为首节点 { point=head; cout<<"删除点是链表第一个节点位置!"; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] head=head->next;//重新设置引导指针 delete point; return; } test *fp=head;//保存连首指针 for(test *&mp=head;mp->next;mp=mp->next) { if(mp->next->number==number) { point=mp->next; mp->next=point->next; delete point; head=fp;//由于head的不断移动丢失了head,把进入循环前的head指针恢复! return; } } } void main() { head=create();//调用创建 showl(head); int dp; cin>>dp; deletel(head,dp);//调用删除 showl(head); cin.get(); cin.get(); } file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 最后我学习一下如何在已有的链表上插入节点 我们要考虑四中情况, 1.链表为空! 2.插入点在首节点前 3.插入点找不到的情况我们设置放在最后! 4.插入点在中间的情况! 今天的程序在昨天的基础上做了进一步的修改,可以避免删除点找不到的情况,如果找不到删除点就退出函 数! #include using namespace std; struct test { int number; float socre; test *next; }; test *head;//创建一个全局的引导进入链表的指针 test *create() { test *ls;//节点指针 test *le;//链尾指针 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] ls = new test;//把ls指向动态开辟的堆内存地址 cout<<"请输入第一个节点number和节点score,输入0.0跳出函数"<>ls->number>>ls->socre; head=NULL;//进入的时候先不设置head指针指向任何地址,因为不知道是否一上来就输入null跳出程序 le=ls;//把链尾指针设置成刚刚动态开辟的堆内存地址,用于等下设置le->next,也就是下一个节点的位置 while(ls->number!=0)//创建循环条件为ls->number的值不是null,用于循环添加节点 { if(head==NULL)//判断是否是第一次进入循环 { head=ls;//如果是第一次进入循环,那么把引导进入链表的指针指向第一次动态开辟的堆内存地 址 } else { le->next=ls;//如果不是第一次进入那么就把上一次的链尾指针的le->next指向上一次循环结束 前动态创建的堆内存地址 } le=ls;//设置链尾指针为当前循环中的节点指针,用于下一次进入循环的时候把上一次的节点的next 指向上一次循环结束前动态创建的堆内存地址 ls=new test;//为下一个节点在堆内存中动态开辟空间 cout<<"请下一个节点number和节点score,输入0.0跳出函数"<>ls->number>>ls->socre; } le->next=NULL;//把链尾指针的next设置为空,因为不管如何循环总是要结束的,设置为空才能够在循环显 链表的时候不至于死循环 delete ls;//当结束的时候最后一个动态开辟的内存是无效的,所以必须清除掉 return head;//返回链首指针 } void showl(test *head) file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { cout<<"链首指针:"<number<<"|"<socre<next; } } void deletel(test *&head,int number)//这里如果参数换成test *head,意义就完全不同了,head变成了复制 而不是原有链上操作了,特别注意,很多书上都不对这里 { test *point;//判断链表是否为空 if(head==NULL) { cout<<"链表为空,不能进行删除工作!"; return; } int derror=1;//设置找不到的情况的条件,预先设置为真 test *check=head; while(check)//利用循环进行查找 { if (check->number==number) { derror=0;//条件转为假 } check=check->next; } if(derror)//如果为假就跳出函数 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] { return; } if(head->number==number)//判删除的节点是否为首节点 { point=head; cout<<"删除点是链表第一个节点位置!"; head=head->next;//重新设置引导指针 delete point; return; } test *fp=head;//保存连首指针 for(test *&mp=head;mp->next;mp=mp->next) { if(mp->next->number==number) { point=mp->next; mp->next=point->next; delete point; head=fp;//由于head的不断移动丢失了head,把进入循环前的head指针恢复! return; } } } void insterl(int number) { test *point=new test; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] cout<<"请输入节点number和节点score"<>point->number>>point->socre; if(head==NULL)//链表为空的情况下插入 { head=point; point->next=NULL; return; } int ierror=1;//设置找不到的情况的条件,预先设置为真 test *le; test *check=head; while(check)//利用循环进行查找 { if (check->number==number) { ierror=0;//条件转为假 } le=check; check=check->next; } if(ierror) { cout<number; le->next=point; point->next=NULL; return; } file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] if(head->number==number)//检测是否是在第一个节点处插入 { point->next=head; head=point; return; } for(test *&mp=head;mp->next;mp=mp->next)//在链表中间插入 { if(mp->next->number==number) { point->next=mp->next; mp->next=point; return; } } } void main() { head=create();//调用创建 showl(head); int dp; cout<<"请输入删除点如果找不到就跳出函数"<>dp; deletel(head,dp);//调用删除 showl(head); int ip; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] cout<<"请输入插入点如果找不到就在链尾添加"<>ip; insterl(ip); showl(head); cin.get(); cin.get(); } 到此关于结构体的内容已经全部讨论结束,链表的建立删除插入操作可以很好的对前面所学知识进行一个总 结,它既考察了程序员对内存大理解(堆内存操作、指针操作)也考察了对结构化编程掌握的熟悉程序。 以后的教程我们将着重训练面向对象的编程的相关知识点。 C/C++编程新手错误语录 文章来源:pconline 作者:宋宝华 1.引言 还记得当年学数学、英语都有个窍门,那就是搞个错题集。经常复习一下这个错题集,就可以避免下次犯 同样的错误。而几乎所有的程序员都是从犯错误开始的,我们也很有必要总结一下编程新手的常见错误,本文 的目的在于此。文中所列出的都是笔者在项目开发中接触到的新手真实的言谈,笔者学学文革腔调,姑且称之 为“错误语录”。 2.语录 (1)“我的程序都是对的,可结果不对” 想想你的周围,是不是也有人说这样的话?如果你也曾经说过,那就此打住,不要再说这句话,因为这句 话只会显示说话者的无知。既然程序都是对的,那为什么结果不对? (2)“程序=算法+数据结构” file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 如果刚刚学完C语言,我们说这样的话,完全可以理解,而且可以说是正确的。但是如果你是一位即将从事 C/C++编程的程序员,那么很遗憾,这个说法只能判错,殊不知,世界上还有另一种说法: 程序 = 对象 + 消息 “程序=算法+数据结构”只对面向过程的语言(C)成立,而对面向对象的语言(C++),则只能表述为“程序 =对象+消息”。传统的过程式编程语言以过程为中心以算法为驱动,面向对象的编程语言则以对象为中心以消 息为驱动。这里的消息是广义的,对象A调用了对象B的成员函数,可看作对象A给B发消息。 (3)“程序编出来,运行正确就行了” 运行正确的程序并不一定是好程序,程序员时刻要牢记的一条就是自己写的程序不仅是给自己看的,要让 别人也能轻易地看懂。很遗憾,许多的编程新手不能清晰地驾驭软件的结构,对头文件和实现文件的概念含糊 不清,写出来的程序可读性很差。 C程序采用模块化的编程思想,需合理地将一个很大的软件划分为一系列功能独立的部分合作完成系统的需 求,在模块的划分上主要依据功能。模块由头文件和实现文件组成,对头文件和实现文件的正确使用方法是: 规则1 头文件(.h)中是对于该模块接口的声明,接口包括该模块提供给其它模块调用的外部函数及外部全 局变量,对这些变量和函数都需在.h中文件中冠以extern关键字声明; 规则2 模块内的函数和全局变量需在.c文件开头冠以static关键字声明; 规则3 永远不要在.h文件中定义变量; 许多程序员对定义变量和声明变量混淆不清,定义变量和声明变量的区别在于定义会产生内存分配的操 作,是汇编阶段的概念;而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。 如: /*模块1头文件:module1.h*/ int a = 5; /* 在模块1的.h文件中定义int a */ /*模块1实现文件:module1 .c*/ #include “module1.h” /* 在模块1中包含模块1的.h文件 */ /*模块2实现文件: module2.c*/ file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] #include “module1.h” /* 在模块2中包含模块1的.h文件 */ /*模块2 实现文件:module3 .c*/ #include “module1.h” /* 在模块3中包含模块1的.h文件 */ 以上程序的结果是在模块1、2、3中都定义了整型变量a,a在不同的模块中对应不同的地址单元,这明显不符 合编写者的本意。正确的做法是: /*模块1头文件:module1.h*/ extern int a; /* 在模块1的.h文件中声明int a */ /*模块1实现文件:module1 .c*/ #include “module1.h” /* 在模块1中包含模块1的.h文件 */ int a = 5; /* 在模块1的.c文件中定义int a */ /*模块2 实现文件: module2 .c*/ #include “module1.h” /* 在模块2中包含模块1的.h文件 */ /*模块3 实现文件: module3 .c*/ #include “module1.h” /* 在模块3中包含模块1的.h文件 */ 这样如果模块1、2、3操作a的话,对应的是同一片内存单元。 规则4 如果要用其它模块定义的变量和函数,直接包含其头文件即可。 许多程序员喜欢这样做,当他们要访问其它模块定义的变量时,他们在本模块文件开头添加这样的语句: extern int externVar; 抛弃这种做法吧,只要头文件按规则1完成,某模块要访问其它模块中定义的全局变量时,只要包含该模块 的头文件即可。 (4)“数组名就是指针” file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 许多程序员对数组名和指针的区别不甚明了,他们认为数组名就是指针,而实际上数组名和指针有很大区 别,在使用时要进行正确区分,其区分规则如下: 规则1 数组名指代一种数据结构,这种数据结构就是数组; 例如: char str[10]; char *pStr = str; cout << sizeof(str) << endl; cout << sizeof(pStr) << endl; 输出结果为: 10 4 这说明数组名str指代数据结构char[10]。 规则2 数组名可以转换为指向其指代实体的指针,而且是一个指针常量,不能作自增、自减等操作,不能 被修改; char str[10]; char *pStr = str; str++; //编译出错,提示str不是左值 pStr++; //编译正确 规则3 指向数组的指针则是另外一种变量类型(在WIN32平台下,长度为4),仅仅意味着数组的存放地 址; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 规则4 数组名作为函数形参时,在函数体内,其失去了本身的内涵,仅仅只是一个指针;很遗憾,在失去 其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。 例如: void arrayTest(char str[]) { cout << sizeof(str) << endl; //输出指针长度 str++; //编译正确 } int main(int argc, char* argv[]) { char str1[10] = "I Love U"; arrayTest(str1); return 0; } (5)“整形变量为32位” 整形变量是不是32位这个问题不仅与具体的CPU架构有关,而且与编译器有关。在嵌入式系统的编程中,一 般整数的位数等于CPU字长,常用的嵌入式CPU芯片的字长为8、16、32,因而整形变量的长度可能是 8、16、32。在未来64位平台下,整形变量的长度可达到64位。 长整形变量的长度一般为CPU字长的2倍。 在数据结构的设计中,优秀的程序员并不会这样定义数据结构(假设为WIN32平台): typedef struct tagTypeExample { unsigned short x; unsigned int y; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] }TypeExample; 他们这样定义: #define unsigned short UINT16 //16位无符号整数 #define unsigned int UINT32 //32位无符号整数 typedef struct tagTypeExample { UINT16 x; UINT32 y; }TypeExample; 这样定义的数据结构非常具有通用性,如果上述32平台上的数据发送到16位平台上接收,在16位平台上仅仅 需要修改UINT16、UINT32的定义: #define unsigned int UINT16 //16位无符号整数 #define unsigned long UINT32 //32位无符号整数 几乎所有的优秀软件设计文档都是这样定义数据结构的。 (6)“switch和if ⋯else⋯可随意替换” switch语句和一堆if⋯else⋯的组合虽然功能上完全一样,但是给读者的感受完全不一样。if⋯else⋯的 感觉是进行条件判断,对特例进行特别处理,在逻辑上是“特殊与一般”的关系,而switch给人的感觉是多个 条件的关系是并列的,事物之间不存在特殊与一般的关系,完全“对等”。 譬如: //分别对1-10的数字进行不同的处理,用switch switch(num) { case 1: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] ⋯ case 2: ⋯ } //对1-10之间的数字进行特殊处理,用if if(num < 10 && num > 1) { ⋯ } else { ⋯ } 许多时候,虽然不同的代码可实现完全相同的功能,但是给读者的感觉是完全不同的。譬如无条件循环: while(1) { } 有的程序员这样写: for(;;) { } file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 这个语法没有确切表达代码的含义,我们从for(;;)看不出什么,只有弄明白for(;;)在C/C++语言中意味着 无条件循环才明白其意。而不懂C/C++语言的读者看到while(1)也可猜到这是一个无条件循环。 (7)“免得麻烦,把类里面的成员函数都搞成public算了” 许多人编C++程序的时候,都碰到这样的情况,先前把某个成员函数定义成类的private/protected函数, 后来发现又要从外面调用这个函数,就轻易地将成员函数改为public类型的。甚至许多程序员为了避免访问的 麻烦,干脆把自己添加的成员函数和成员变量都定义成public类型。 殊不知,这是一种规划的失败。在类的设计阶段,我们就要很清晰地知道,这个类的成员函数中哪些是这 个类的接口,哪些属于这个类内部的成员函数和变量。一般的准则是接口(public成员)应在满足需求的前提 下尽可能简单! 所以不要轻易地将private/protected成员改为public成员,真正的工作应该在规划阶段完成。 3.结束语 所有的程序员都要经历一个从糊涂到清晰的过程,文中的错误如果你也犯了,切勿自惭。 更多的错误语录,希望能在后续文章中陆续推出。 精华:C++编程新手错误语录(续一) 文章来源:pconline 作者:宋宝华 废话不说,直接进入正题,本文承接先前发布的《C/C++编程新手错误语录》,继续归纳错误语录。 (8)“我想用malloc”、“我用不好malloc” 来看看一个变态程序: /* xx.c:xx模块实现文件 */ file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] int *pInt; /* xx模块的初始化函数 */ xx_intial() { pInt = ( int * ) malloc ( sizeof( int ) ); ... } /* xx模块的其他函数(仅为举例)*/ xx_otherFunction() { *Int = 10; ... } 这个程序定义了一个全局整型变量指针,在xx模块的初始化函数中对此指针动态申请内存,并将pInt指向 该内存首地址,并在xx模块的其他函数中都使用pInt指针对其指向的整数进行读取和赋值。 这个程序让我痛不欲生了好多天,扼腕叹息!这是我母校计算机系一位硕士的作品!作者为了用上 malloc,拼命地把本来应该用一个全局整型变量摆平的程序活活弄成一个全局整型指针并在初始化函数中“动 态”申请内存,自作聪明而正好暴露自己的无知!我再也不要见到这样的程序。 那么malloc究竟应该怎么用?笔者给出如下规则: 规则1 不要为了用malloc而用malloc,malloc不是目的,而是手段; 规则2 malloc的真正内涵体现在“动态”申请,如果程序的特性不需动态申请,请不要用malloc; 上面列举的变态程序完全不具备需要动态申请的特质,应该改为: /* xx.c:xx模块实现文件 */ file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] int example; /* xx模块的初始化函数 */ xx_intial() { ... } /* xx模块的其他函数(仅为举例) */ xx_otherFunction() { example = 10; ... } 规则3 什么样的程序具备需要动态申请内存的特质呢?包含两种情况: (1)不知道有多少要来,来了的又走了 不明白?这么说吧,譬如你正在处理一个报文队列,收到的报文你都存入该队列,处理完队列头的报文后 你需要取出队列头的元素。 你不知道有多少报文来(因而你不知道应该用多大的报文数组),这些来的报文处理完后都要走(释 放),这种情况适合用malloc和free。 (2)慢慢地长大 譬如你在资源受限的系统中编写一文本编辑器程序,你怎么做,你需要这样定义数组吗? char str[10000]; 不,你完全不应该这么做。即使你定义了一个10000字节大的字符串,用户如果输入10001个字符你的程序 就完完了。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 这个时候适合用malloc,因为你根本就不知道用户会输入多少字符,文本在慢慢长大,因而你也应慢慢地 申请内存,用一个队列把字符串存放起来。 那么是不是应该这样定义数据结构并在用户每输入一个字符的情况下malloc一个CharQueue空间呢? typedef struct tagCharQueue { char ch; struct tagCharQueue *next; }CharQueue; 不,这样做也不对!这将使每个字符占据“1+指针长度”的开销。 正确的做法是: typedef struct tagCharQueue { char str[100]; struct tagCharQueue *next; }CharQueue; 让字符以100为单位慢慢地走,当输入字符数达到100的整数倍时,申请一片CharQueue空间。 规则4 malloc与free要成对出现 它们是一对恩爱夫妻,malloc少了free就必然会慢慢地死掉。成对出现不仅体现在有多少个malloc就应该 有多少个free,还体现在它们应尽量出现在同一函数里,“谁申请,就由谁释放”,看下面的程序: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] char * func(void) { char *p; p = (char *)malloc(⋯); if(p!=NULL) ⋯; /* 一系列针对p的操作 */ return p; } /*在某处调用func(),用完func中动态申请的内存后将其free*/ char *q = func(); ⋯ free(q); 上述代码违反了malloc和free的“谁申请,就由谁释放”原则,代码的耦合度大,用户在调用func函数时 需确切知道其内部细节!正确的做法是: /* 在调用处申请内存,并传入func函数 */ char *p=malloc(⋯); if(p!=NULL) { func(p); ⋯ free(p); p=NULL; } /* 函数func则接收参数p */ file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] void func(char *p) { ⋯ /* 一系列针对p的操作 */ } 规则5 free后一定要置指针为NULL,防止其成为“野”指针 (9)“函数add编译生成的符号就是add” int add(int x,int y) { return x + y; } float add(float x,float y) { return x + y; } 即便是在C语言中,add函数被多数C编译器编译后在符号库中的名字也不是add,而是_add。而在C++编译器 中,int add(int x,int y)会编译成类似_add_int_int这样的名字(称为“mangled name”),float add(float x,float y)则被编译成_add_float _float,mangled name包含了函数名、函数参数数量及类型信 息,C++依靠这种机制来实现函数重载。 所以,在C++中,本质上int add( int x, int y )与float add( float x, float y )是两个完全不同的函 数,只是在用户看来其同名而已。 这就要求初学者们能透过语法现象看问题本质。本质上,语言的创造者们就是在玩各种各样的花样,以使 语言具备某种能力,譬如mangled name花样的目的在于使C++支持重载。而C语言没有玩这样的花样,所以int file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] add( int x, int y )与float add( float x, float y )不能在C程序中同时存在。 (10)“没见过在C语言中调用C++的函数”、“C/C++不能调用Basic、Pascal语言的函数” 这又是一个奇天下之大怪的问题,“打死我都不相信C、C++、basic、pascal的函数能瞎调来调去”,可是 有句话这么说: 没有你见不到的,只有你想不到的! 既然芙蓉姐姐也有其闻名天下的道理,那么C、C++、Basic、Pascal的函数为什么就不能互相调用呢? 能! 你可以用Visual C++写一个DLL在Visual Basic、Delphi(Pascal的孙子,Object Pascal的儿子)中调 用,也可以在Visual Basic、Delphi中写一个DLL在Visual C++中调用不是? 让我们来透过现象看本质。首先看看函数的调用约定(以Visual C++来说明): (1) _stdcall调用 _stdcall是Pascal程序的缺省调用方式,参数采用从右到左的压栈方式,被调函数自身在返回前清空堆栈。 WIN32 Api都采用_stdcall调用方式,这样的宏定义说明了问题: #define WINAPI _stdcall 按C编译方式,_stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如 _functionname@number。 (2) _cdecl调用 _cdecl是C/C++的缺省调用方式,参数采用从右到左的压栈方式,传送参数的内存栈由调用者维 护。_cedcl约定的函数只能被C/C++调用,每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文 件大小会比调用_stdcall函数的大。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 由于_cdecl调用方式的参数内存栈由调用者维护,所以变长参数的函数能(也只能)使用这种调用约定。 关于C/C++中变长参数(⋯)的问题,笔者将另文详述。 由于Visual C++默认采用_cdecl 调用方式,所以VC中中调用DLL时,用户应使用_stdcall调用约定。 按C编译方式,_cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。 (3) _fastcall调用 _fastcall调用较快,它通过CPU内部寄存器传递参数。 按C编译方式,_fastcall调用约定在输出函数名前面加“@”符号,后面加“@”符号和参数的字节数,形 如@functionname@number。 关键字_stdcall、_cdecl和_fastcall可以直接加在函数前,也可以在Visual C++中设置, 在创建DLL时,一般使用_stdcall调用(Win32 Api方式),采用_functionname@number命名规则,因而各种语 言间的DLL能互相调用。也就是说,DLL的编制与具体的编程语言及编译器无关,只要遵守DLL的开发规范和编 程策略,并安排正确的调用接口,不管用何种编程语言编制的DLL都具有通用性。 推而广之,如果有这样一个IDE开发环境,它能识别各种语言,所有语言采用相同的调用约定和命名规则, 一个软件内各种语言书写的函数将能互相调用! 这个世界上可能永远不需要这样一个IDE。 (11)“英语、数学不好就学不好C/C++” 这也许是20世纪最大的谎言,这句话最先是哪位大师的名人名言已无可考证,可此后一批批的人被它误导。 许多初学者因为这句话被吓倒,放弃了做程序员的理想。还有许多后来成为优秀程序员的人,在他们的成长过 程中并没有依靠深奥的数学,可他们还是在总结经验时制造恐慌,号称一定要具备高深的数学知识,唯恐别人 笑话其学术水平不高。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 在下则认为,大多数情况下,程序设计不需要太深奥的数学功底,除非你所从事的程序设计涉及特定的专 业领域(如语音及图像处理、数字通信技术等)。在下这一观点也许是革旧立新,而革命必然要流血牺牲(谭 嗣同),所以恭候大家板砖。 那么英语在C/C++的学习中处于什么地位呢?那就是能看懂资料,看懂MSDN。 学编程的终极之道不在看书,而在大量地不断地实践。 (12)“C++太难了,我学不会” 又不知是谁的悲观论调,许多初学者被C++吓倒,“太难了,我学不好”,如弱者自怜。如果C++真的难到学 不会,那么C++的创造者们所从事的工作岂不是“非人力所能及也”? 在下认为,学习C++的态度应该是:战略上藐视它,战术上重视它,要敢于胜利(《毛主席语录》)。当然 也不可轻敌,不能因为掌握了一点皮毛就以为自己牛B轰轰了(笔者曾经牛B轰轰了好一阵子,现在想来,甚觉 当时幼稚)。 如果你征服了C++,透彻理解了C++的语言特性及STL,那么,其他语言想不被你征服都难了。 本回书着落此处,更多错误语录,当然是待续。 C/C++编程新手错误语录(续二) 文章来源:pconline 作者:宋宝华 前文回顾:C/C++编程新手错误语录 错误语录(续一) (13)“整型变量仅仅意味着一个整数” file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 当我们还是一个新手,看整型就是整数; 当我们成为高手,看什么都是整型。 整型,在所有C/C++基本数据类型中最富有艺术魅力和奇幻色彩。 我们从某著名论坛的一篇帖子开始一窥整型的奥妙。 问:Vxworks操作系统启动一个任务的函数是taskSpawn(char* name, int priority, int options, int stacksize, FUNCPTR function, int arg1,.. , int arg10),它只接受整型参数,我该怎么办才能给它传一 个结构体(在32位PowerPC平台下)? 答:可以传入结构体的指针,在32位PowerPC平台下,指针本质上就是一个32位整数,在函数体内将整型强 制转化为结构体指针就可访问结构体的每一个元素。 如: //启动任务1 taskSpawn(“task1”, 180, NULL, 10000, Task1Fun, &pStructAr,0,0,0,0,0,0,0,0,0); //task1函数 Task1Fun ( int arg1 ) { struct_x * pStructx = (struct_x *) arg1; //将整型强制转化为结构体指针 ⋯ } 在此提出“泛整型”的概念,(unsigned)char、(unsigned)short int、(unsigned)int、 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] (unsigned)long int等都属于这个范畴,指针必然属于“泛整型”的范围。用指针的高超境界,也为将其看 做一个“泛整型”。 看看软件的详细设计文档,其数据结构定义部分经常看 到“INT8、UINT8、INT16、UINT16、INT32、UINT32、INT64、UINT64”或“BYTE、WORD、DWORD”等数据类 型,它们在本质上都是(unsigned)char、(unsigned)short int、(unsigned)int、(unsigned)long int宏定义的结果,都属于“泛整型”。所以,“泛整型”的概念真实地体现在日常的软件设计当中。 正因为各种指针类型在本质上都是“泛整型”,因此它们可以互相转化: int a, b; memset( (char*) &a, (char*) &b, sizeof(int) ); 等价于: int a, b; a = b; 从来没有人会用memset( (char*) &a, (char*) &b, sizeof(int) )来代替a = b,这里只是为了说明问 题。下面的代码则经常用到: int *p = (int *) malloc(100*sizeof(int)); memset ( p, 0, 100*sizeof(int) ); //将申请的内存空间清0 我们看memset的函数原型为: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] void * memset ( void * buffer, int c, size_t num ); 实际上它接受的第一个参数是无类型指针,在memset函数体内,其它任意类型的指针都向void *转化了。 类似的内存操作函数memcpy所接受的源和目的内存地址也是无类型指针。 char *转化为int *后的值虽然不变(还是那个地址),但是其++、--等操作的含义却发生了变化,这也是 要注意的。 char *p; ++p; 与 char *p; ++(int *)p; 的结果是不一样的,前者的p值加了1,而后者的则增加了sizeof(int)。 下面来剥Windows程序设计中消息传递函数两个参数的皮,看看它们究竟是什么: typedef UINT WPARAM; typedef LONG LPARAM; 原来,WPARAM和LPARAM其实都属于“泛整型”,所以不要报怨消息处理函数只能接受“泛整型”。实际 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 上,从指针的角度上来讲,在C/C++中,可以获得任何类型实例(变量、结构、类)的指针,所以Windows的消 息处理函数实际上可以接受一切类型的参数。 惊天动地一句话:“泛整型”可表征一切。 (14)“值传递一定不会改变参数” 理论而言,值传递的确不会改变参数的内容。但是,某年某月的某一天,隔壁office的硕士mm写了这么一 段程序,参数的值却被改变了: int n = 9; char a[10]; example ( n, a ); //调用函数example(int n,char *pStr) printf (“%d”, n ); //输出结果不是9 大概整个office的人都被搞懵了,都说编译器瞎搞,有问题。找到笔者,笔者凭借以往的经常,一眼就看 出来不是什么编译器出错,而是在函数example内对字符串a的访问越界! 当在函数example内对a的访问越界后,再进行写操作时,就有可能操作到了n所在的内存空间,于是改变了 n的值。 给出这个语录,并非为了推翻“值传递不会改变参数”的结论,而是为了从侧面证明在C/C++语言中,数组 越界是多么危险的错误! 下面的两个函数有明显的数组越界: void example1() { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] char string[10]; char* str1 = "0123456789"; strcpy( string, str1 ); } void example 2(char* str1) { char string[10]; if( strlen( str1 ) <= 10 ) { strcpy( string, str1 ); } } 而这个函数的越界就不这么明显: void example3() { char string[10], str1[10]; int i; for(i=0; i<10; i++) { str1[i] = 'a'; } strcpy( string, str1 ); } 其实,这个函数危险到了极点。因为对于strcpy函数而言,拷贝的时候要碰到’\0’才结束,str1并没有 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 被赋予结束符,因而你根本就不知道strcpy( string, str1 )的结果究竟会是拷贝多大一片内存! 遗憾的是,C/C++永远不会在编译和连接阶段提示数组越界,它只会在运行阶段导致程序的崩溃。 数组越界,是大多数C/C++编程新手常犯的错误,而它又具有极大的隐蔽性,新手们一定要特别注意。 (15)“C不高级,学C++、JAVA、C#才够味” 也许谭浩强老师的C语言教材是绝大多数高校学生学习的第一门编程课程,所以在许多学生的心目中,觉得 C是一种入门级的语言,他们舍弃基础而追逐花哨的Visual XXX、Java、ASP、PHP、.net,他们以为这样 做“赚大了”。 非也! C是一种多么富有魅力的语言!在今时的绝对多数底层开发中,仍然几乎被C完全垄断。这些领域包括操作 系统、嵌入式系统、数字信号处理等。舍弃C的经济基础搭.net的高层建筑实在是危险。 我们总是以为自己掌握了C,那么请写一个strcpy的标准函数。您的答案若是: void strcpy( char *strDest, char *strSrc ) { while( (*strDest++ = * strSrc++) != ‘\0’ ); } 很遗憾,您的程序只能拿到E。看看拿A的strcpy: char * strcpy( char *strDest, const char *strSrc ) { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] assert( (strDest != NULL) && (strSrc != NULL) ); char *address = strDest; while( (*strDest++ = * strSrc++) != ‘\0’ ); return address; } 这个程序考虑了什么? (1)程序要强大:为了实现链式操作,将目的地址返回,函数返回类型改为char * (2)程序要可读:源字符串指针参数加const限制,表明为输入参数 (3)程序要健壮:验证strDest和strSrc非空 如果这三点中您只考虑到0点或1点,那么请回家好好修炼一下C。因为这个最简单的strcpy已验证出您的C 语言基础只能叫做“入门”。 再写个简单的strlen,这么写就好了: int strlen( const char *str ) //输入参数为const { assert( strt != NULL ); //断言字符串地址非0 int len; while( (*str++) != '\0' ) { len++; } return len; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] } 由此可见,写好这些简单的函数也需要深厚的基本功,永远不要放弃对基本功的培养。 (16)“语言学得越多越好” 许多的初学者都经历过这样的一个阶段,面对大量的编程语言和开发环境,他们俩感到难以取舍,不知道 自己究竟应该学习什么。于是他们什么都学,今天看一下Visual Basic,明天看学一下C++,后天在书点看到 了本Java便心血来潮买回来翻翻,大后天又发现必须学.net了。他们很痛苦,什么都在看,结果什么都没学 会,忙忙碌碌而收获甚微。 我们真的没有必要在什么语言都不甚精通的情况下乱看一气。认准了一种真正语言就应该坚持不懈地努 力。因为任何一门语言的掌握都非一朝一夕一事,笔者从六年前开始接触C++,直到现在,每一阶段仍有新的 启发,在项目开发的过程中也不断有新的收获。今日我还是绝对不敢宣称自己“精通”这门语言。 许多刚毕业的大学生,动不动就在简历上写上自己精通一堆语言。与之相反,大多数优秀的工程师都不敢 这么写。也许,研究越深,便越敢自身的无知。 在下认为,一个成熟的语言体系应该是: 程序员的语言体系 = 一种汇编 + C + 一种面向对象(C++、JAVA、C#等) 如果还要加,那就加一种解释型语言,perl或tcl(也许其它)。 语言具有极大的相似性,从C++过渡到JAVA只需要很短的一段时间。各种语言的发展历史也体现了编程思想 的发展史。我们学习一种语言,语法也许并不是最重要的,最重要的是蕴藏在语法外表下的深层特性和设计用 意。 本回书着落此处,更多错误语录,当然是待续。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] C/C++语言void及void指针深层探索 文章来源:pconline 作者:宋宝华 1.概述 许多初学者对C/C++语言中的void及void指针类型不甚理解,因此在使用上出现了一些错误。本文将对void 关键字的深刻含义进行解说,并详述void及void指针类型的使用方法与技巧。 2.void的含义 void的字面意思是“无类型”,void *则为“无类型指针”,void *可以指向任何类型的数据。 void几乎只有“注释”和限制程序的作用,因为从来没有人会定义一个void变量,让我们试着来定义: void a; 这行语句编译时会出错,提示“illegal use of type 'void'”。不过,即使void a的编译不会出错,它 也没有任何实际意义。 void真正发挥的作用在于: (1) 对函数返回的限定; (2) 对函数参数的限定。 我们将在第三节对以上二点进行具体说明。 众所周知,如果指针p1和p2的类型相同,那么我们可以直接在p1和p2间互相赋值;如果p1和p2指向不同的 数据类型,则必须使用强制类型转换运算符把赋值运算符右边的指针类型转换为左边指针的类型。 例如: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] float *p1; int *p2; p1 = p2; 其中p1 = p2语句会编译出错,提示“'=' : cannot convert from 'int *' to 'float *'”,必须改为: p1 = (float *)p2; 而void *则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换: void *p1; int *p2; p1 = p2; 但这并不意味着,void *也可以无需强制类型转换地赋给其它类型的指针。因为“无类型”可以包容“有 类型”,而“有类型”则不能包容“无类型”。道理很简单,我们可以说“男人和女人都是人”,但不能 说“人是男人”或者“人是女人”。下面的语句编译出错: void *p1; int *p2; p2 = p1; 提示“'=' : cannot convert from 'void *' to 'int *'”。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 3.void的使用 下面给出void关键字的使用规则: 规则一 如果函数没有返回值,那么应声明为void类型 在C语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。但是许多程序员却误以为 其为void类型。例如: add ( int a, int b ) { return a + b; } int main(int argc, char* argv[]) { printf ( "2 + 3 = %d", add ( 2, 3) ); } 程序运行的结果为输出: 2 + 3 = 5 这说明不加返回值说明的函数的确为int函数。 林锐博士《高质量C/C++编程》中提到:“C++语言有很严格的类型安全检查,不允许上述情况(指函数不 加类型声明)发生”。可是编译器并不一定这么认定,譬如在Visual C++6.0中上述add函数的编译无错也无警 告且运行正确,所以不能寄希望于编译器会做严格的类型检查。 因此,为了避免混乱,我们在编写C/C++程序时,对于任何函数都必须一个不漏地指定其类型。如果函数没 有返回值,一定要声明为void类型。这既是程序良好可读性的需要,也是编程规范性的要求。另外,加上void 类型声明后,也可以发挥代码的“自注释”作用。代码的“自注释”即代码能自己注释自己。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 规则二 如果函数无参数,那么应声明其参数为void 在C++语言中声明一个这样的函数: int function(void) { return 1; } 则进行下面的调用是不合法的: function(2); 因为在C++中,函数参数为void的意思是这个函数不接受任何参数。 我们在Turbo C 2.0中编译: #include "stdio.h" fun() { return 1; } main() { printf("%d",fun(2)); file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] getchar(); } 编译正确且输出1,这说明,在C语言中,可以给无参数的函数传送任意类型的参数,但是在C++编译器中编 译同样的代码则会出错。在C++中,不能向无参数的函数传送任何参数,出错提示“'fun' : function does not take 1 parameters”。 所以,无论在C还是C++中,若函数不接受任何参数,一定要指明参数为void。 规则三 小心使用void指针类型 按照ANSI(American National Standards Institute)标准,不能对void指针进行算法操作,即下列操作都 是不合法的: void * pvoid; pvoid++; //ANSI:错误 pvoid += 1; //ANSI:错误 //ANSI标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指向数据类型大小的。 //例如: int *pint; pint++; //ANSI:正确 pint++的结果是使其增大sizeof(int)。 但是大名鼎鼎的GNU(GNU's Not Unix的缩写)则不这么认定,它指定void *的算法操作与char *一致。 因此下列语句在GNU编译器中皆正确: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] pvoid++; //GNU:正确 pvoid += 1; //GNU:正确 pvoid++的执行结果是其增大了1。 在实际的程序设计中,为迎合ANSI标准,并提高程序的可移植性,我们可以这样编写实现同样功能的代 码: void * pvoid; (char *)pvoid++; //ANSI:正确;GNU:正确 (char *)pvoid += 1; //ANSI:错误;GNU:正确 GNU和ANSI还有一些区别,总体而言,GNU较ANSI更“开放”,提供了对更多语法的支持。但是我们在真实 设计时,还是应该尽可能地迎合ANSI标准。 规则四 如果函数的参数可以是任意类型指针,那么应声明其参数为void * 典型的如内存操作函数memcpy和memset的函数原型分别为: void * memcpy(void *dest, const void *src, size_t len); void * memset ( void * buffer, int c, size_t num ); 这样,任何类型的指针都可以传入memcpy和memset中,这也真实地体现了内存操作函数的意义,因为它操 作的对象仅仅是一片内存,而不论这片内存是什么类型。如果memcpy和memset的参数类型不是void *,而是 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] char *,那才叫真的奇怪了!这样的memcpy和memset明显不是一个“纯粹的,脱离低级趣味的”函数! 下面的代码执行正确: //示例:memset接受任意类型指针 int intarray[100]; memset ( intarray, 0, 100*sizeof(int) ); //将intarray清0 //示例:memcpy接受任意类型指针 int intarray1[100], intarray2[100]; memcpy ( intarray1, intarray2, 100*sizeof(int) ); //将intarray2拷贝给intarray1 有趣的是,memcpy和memset函数返回的也是void *类型,标准库函数的编写者是多么地富有学问啊! 规则五 void不能代表一个真实的变量 下面代码都企图让void代表一个真实的变量,因此都是错误的代码: void a; //错误 function(void a); //错误 void体现了一种抽象,这个世界上的变量都是“有类型”的,譬如一个人不是男人就是女人(还有人 妖?)。 void的出现只是为了一种抽象的需要,如果你正确地理解了面向对象中“抽象基类”的概念,也很容易理 解void数据类型。正如不能给抽象基类定义一个实例,我们也不能定义一个void(让我们类比的称void为“抽 象数据类型”)变量。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 4.总结 小小的void蕴藏着很丰富的设计哲学,作为一名程序设计人员,对问题进行深一个层次的思考必然使我们 受益匪浅。 C/C+语言struct深层探索 文章来源:pconline 作者:宋宝华 1. struct的巨大作用 面对一个人的大型C/C++程序时,只看其对struct的使用情况我们就可以对其编写者的编程经验进行评估。 因为一个大型的C/C++程序,势必要涉及一些(甚至大量)进行数据组合的结构体,这些结构体可以将原本意义 属于一个整体的数据组合在一起。从某种程度上来说,会不会用struct,怎样用struct是区别一个开发人员是 否具备丰富开发经历的标志。 在网络协议、通信控制、嵌入式系统的C/C++编程中,我们经常要传送的不是简单的字节流(char型数 组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。 经验不足的开发人员往往将所有需要传送的内容依顺序保存在char型数组中,通过指针偏移的方法传送网络 报文等信息。这样做编程复杂,易出错,而且一旦控制方式及通信协议有所变化,程序就要进行非常细致的修 改。 一个有经验的开发者则灵活运用结构体,举一个例子,假设网络或控制协议中需要传送三种报文,其格式分 别为packetA、packetB、packetC: struct structA { int a; char b; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] }; struct structB { char a; short b; }; struct structC { int a; char b; float c; } 优秀的程序设计者这样设计传送的报文: struct CommuPacket { int iPacketType; //报文类型标志 union //每次传送的是三种报文中的一种,使用union { struct structA packetA; struct structB packetB; struct structC packetC; } }; 在进行报文传送时,直接传送struct CommuPacket一个整体。 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 假设发送函数的原形如下: // pSendData:发送字节流的首地址,iLen:要发送的长度 Send(char * pSendData, unsigned int iLen); 发送方可以直接进行如下调用发送struct CommuPacket的一个实例sendCommuPacket: Send( (char *)&sendCommuPacket , sizeof(CommuPacket) ); 假设接收函数的原形如下: // pRecvData:发送字节流的首地址,iLen:要接收的长度 //返回值:实际接收到的字节数 unsigned int Recv(char * pRecvData, unsigned int iLen); 接收方可以直接进行如下调用将接收到的数据保存在struct CommuPacket的一个实例recvCommuPacket中: Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) ); 接着判断报文类型进行相应处理: switch(recvCommuPacket. iPacketType) { case PACKET_A: ⋯ //A类报文处理 break; case PACKET_B: ⋯ //B类报文处理 break; case PACKET_C: ⋯ //C类报文处理 break; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] } 以上程序中最值得注意的是 Send( (char *)&sendCommuPacket , sizeof(CommuPacket) ); Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) ); 中的强制类型转换:(char *)&sendCommuPacket、(char *)&recvCommuPacket,先取地址,再转化为char型 指针,这样就可以直接利用处理字节流的函数。 利用这种强制类型转化,我们还可以方便程序的编写,例如要对sendCommuPacket所处内存初始化为0,可以 这样调用标准库函数memset(): memset((char *)&sendCommuPacket,0, sizeof(CommuPacket)); 2. struct的成员对齐 Intel、微软等公司曾经出过一道类似的面试题: 1. #include 2. #pragma pack(8) 3. struct example1 4. { 5. short a; 6. long b; 7. }; 8. struct example2 9. { file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 10. char c; 11. example1 struct1; 12. short e; 13. }; 14. #pragma pack() 15. int main(int argc, char* argv[]) 16. { 17. example2 struct2; 18. cout << sizeof(example1) << endl; 19. cout << sizeof(example2) << endl; 20. cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) << endl; 21. return 0; 22. } 问程序的输入结果是什么? 答案是: 8 16 4 不明白?还是不明白?下面一一道来: file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 2.1 自然对界 struct是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以 是一些复合数据类型(如array、struct、union等)的数据单元。对于结构体,编译器会自动进行成员变量的 对齐,以提高运算效率。缺省情况下,编译器为结构体的每个成员按其自然对界(natural alignment)条件 分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。 自然对界(natural alignment)即默认对齐方式,是指按结构体的成员中size最大的成员对齐。 例如: struct naturalalign { char a; short b; char c; }; 在上述结构体中,size最大的是short,其长度为2字节,因而结构体中的char成员a、c都以2为单位对 齐,sizeof(naturalalign)的结果等于6; 如果改为: struct naturalalign { char a; int b; char c; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] }; 其结果显然为12。 2.2指定对界 一般地,可以通过下面的方法来改变缺省的对界条件: · 使用伪指令#pragma pack (n),编译器将按照n个字节对齐; · 使用伪指令#pragma pack (),取消自定义字节对齐方式。 注意:如果#pragma pack (n)中指定的n大于结构体中最大成员的size,则其不起作用,结构体仍然按照 size最大的成员进行对界。 例如: #pragma pack (n) struct naturalalign { char a; int b; char c; }; #pragma pack () 当n为4、8、16时,其对齐方式均一样,sizeof(naturalalign)的结果都等于12。而当n为2时,其发挥了作 用,使得sizeof(naturalalign)的结果为8。 在VC++ 6.0编译器中,我们可以指定其对界方式(见图1),其操作方式为依次选择projetct > setting > file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] C/C++菜单,在struct member alignment中指定你要的对界方式。 另外,通过__attribute((aligned (n)))也可以让所作用的结构体成员对齐在n字节边界上,但是它较少被使 用,因而不作详细讲解。 2.3 面试题的解答 至此,我们可以对Intel、微软的面试题进行全面的解答。 程序中第2行#pragma pack (8)虽然指定了对界为8,但是由于struct example1中的成员最大size为4(long 变量size为4),故struct example1仍然按4字节对界,struct example1的size为8,即第18行的输出结果; struct example2中包含了struct example1,其本身包含的简单数据成员的最大size为2(short变量e), 但是因为其包含了struct example1,而struct example1中的最大成员size为4,struct example2也应以4对 界,#pragma pack (8)中指定的对界对struct example2也不起作用,故19行的输出结果为16; 由于struct example2中的成员以4为单位对界,故其char变量c后应补充3个空,其后才是成员struct1的内 存空间,20行的输出结果为4。 3. C和C++间struct的深层区别 在C++语言中struct具有了“类” 的功能,其与关键字class的区别在于struct中成员变量和函数的默认访 问权限为public,而class的为private。 例如,定义struct类和class类: struct structA { char a; ⋯ file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] } class classB { char a; ⋯ } 则: struct A a; a.a = 'a'; //访问public成员,合法 classB b; b.a = 'a'; //访问private成员,不合法 许多文献写到这里就认为已经给出了C++中struct和class的全部区别,实则不然,另外一点需要注意的是: C++中的struct保持了对C中struct的全面兼容(这符合C++的初衷——“a better c”),因而,下面的操 作是合法的: //定义struct struct structA { char a; char b; int c; }; structA a = {'a' , 'a' ,1}; // 定义时直接赋初值 即struct可以在定义的时候直接以{ }对其成员变量赋初值,而class则不能,在经典书目《thinking C++ file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 2nd edition》中作者对此点进行了强调。 4. struct编程注意事项 看看下面的程序: 1. #include 2. struct structA 3. { 4. int iMember; 5. char *cMember; 6. }; 7. int main(int argc, char* argv[]) 8. { 9. structA instant1,instant2; 10.char c = 'a'; 11. instant1.iMember = 1; 12. instant1.cMember = &c; 13.instant2 = instant1; 14.cout << *(instant1.cMember) << endl; 15.*(instant2.cMember) = 'b'; 16. cout << *(instant1.cMember) << endl; file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 17. return 0; } 14行的输出结果是:a 16行的输出结果是:b Why?我们在15行对instant2的修改改变了instant1中成员的值! 原因在于13行的instant2 = instant1赋值语句采用的是变量逐个拷贝,这使得instant1和instant2中的 cMember指向了同一片内存,因而对instant2的修改也是对instant1的修改。 在C语言中,当结构体中存在指针型成员时,一定要注意在采用赋值语句时是否将2个实例中的指针型成员指 向了同一片内存。 在C++语言中,当结构体中存在指针型成员时,我们需要重写struct的拷贝构造函数并进行“=”操作符重 载。 C/C++头文件一览 文章来源:互联网 传统 C++ #include //设定插入点 #include //字符处理 #include //定义错误码 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] #include //浮点数处理 #include //文件输入/输出 #include //参数化输入/输出 #include //数据流输入/输出 #include //定义各种数据类型最值常量 #include //定义本地化函数 #include //定义数学函数 #include //定义输入/输出函数 #include //定义杂项函数及内存分配函数 #include //字符串处理 #include //基于数组的输入/输出 #include //定义关于时间的函数 #include //宽字符处理及输入/输出 #include //宽字符分类 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] 标准 C++ (同上的不再注释) #include //STL 通用算法 #include //STL 位集容器 #include #include #include #include #include //复数类 #include #include #include #include #include //STL 双端队列容器 #include //异常处理类 #include file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] #include //STL 定义运算函数(代替运算符) #include #include //STL 线性列表容器 #include //STL 映射容器 #include #include //基本输入/输出支持 #include //输入/输出系统使用的前置声明 #include #include //基本输入流 #include //基本输出流 #include //STL 队列容器 #include //STL 集合容器 #include //基于字符串的流 #include //STL 堆栈容器 file:///C|/Documents and Settings/Administrator/桌面/C++知识点大杂烩(重点).txt[2010-6-28 17:27:09] #include //标准异常类 #include //底层输入/输出支持 #include //字符串类 #include //STL 通用模板类 #include //STL 动态数组容器 #include #include using namespace std; C99 增加 #include //复数处理 #include //浮点环境 #include //整数格式转换 #include //布尔环境 #include //整型环境 #include //通用类型数学宏
还剩299页未读

继续阅读

pdf贡献者

bm56

贡献于2016-02-03

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf