C++基础浅谈.doc_第1页
C++基础浅谈.doc_第2页
C++基础浅谈.doc_第3页
C++基础浅谈.doc_第4页
C++基础浅谈.doc_第5页
已阅读5页,还剩67页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

技术中心教科书 C+基础知识第一章C家族的故事以下描述摘自C+对话系列“最初,计算机语言非常混乱,高级语言根本不存在,连固定的语言形式也没有。贝尔实验室的Richard Martin在使用了计算机语言的过程中意识到了高级语言的必要性。他深入地研究后,开发出了他认为不错的BCPL语言。 “然后Ken Thompson使用了BCPL,虽然他觉得很不错,但他认为如果想在一台PDP-7上使用BCPL,就必须精简BCPL。Ken Thompson深入地研究后,他开发出了一门新的语言,命名为B,它是BCPL的一个简化版本,他认为这是一门很好的语言。 “然而B语言没有类型的概念。Dennis Ritchie意识到了这一点,他深入研究后,对B语言进行了扩展。Ritchie 添加了结构和类型,他把这门语言叫作C语言,因为C是B的下一个字母,无论是在字母表还是在BCPL中。Ritchie 认为这门语言已经相当好了,但是他并不满足,继续投入大量的心血和汗水去完善这门语言。在1978年,Brian Kernighan 和Dennis Ritchie合作出版了The C Programming Language.3这为人们带来了很多的喜悦,人们看到了C的美妙,耶,这门语言真的很棒!人们纷纷议论。 “C语言很快流传开来。新的特征不断的被添加,但并不是被所有的编译器厂商支持。人们开始感到沮丧,开始呼吁“我们需要标准C!” ANSI响应了这一要求,在1989年ANSI 宣布, 请注意,我将给所有的程序员带来快乐。因为在今天,C的标准X3.159-1989将诞生.接着ISO采纳了这一标准,发布了ISO/IEC 9899-1990。这又一次为人们带来喜悦。 “事情在进一步发展,早在C标准被发布之前,Bjarne Stroustrup就已经致力于改善C语言。Stroustrup致力于在C语言里增加类、函数参数类型检查和其他的一些优秀的特征。他继续深入,于1980年发布了C With Classes.这为人们带来了更多的喜悦和兴奋。 Stroustrup 并没有止步不前。他在对C语言做了很大的改变后,产生了一门新的语言,他命名这门语言为C+,就是C的增强的意思。他继续努力,在1986年出版了The C+ Programming Language,这再一次为人们带来了喜悦。 象所有的事物一样,C+语言也在不断的进化着。模板,异常处理(exception handling)以及其它的特征陆续被添加到C+中,人们再次为新事物而兴奋。 “然而人们又开始抱怨了。那时候,不同的编译器开发商使用不同的解决方案支持模板和异常以及其它的特征,甚至有些开发商拒绝支持这些新特性。因此ISO又行动了, 在1998年-克林顿上台后第六年,克雷蒂安成了除魁北克人之外所有加拿大人的总理。莱温斯基成了媒体的大红人,因为没有第二个辛普森诞生,那年没有什么大的新闻-在九月的第一天,ISO 宣布“ 请注意,我将给所有的程序员带来快乐的消息。因为在今天,C+的标准ISO/IEC 14882:1998(E)将诞生。”接着ANSI接受了这一建议,在七月的二十七号发布了几乎相同的标准,甚至早于ISO标准的发布,有时候事情就是这样。这又一次为人们带来喜悦,“啊,太好了,我们可以踩在巨人的肩膀上前进了”大家是这样欢呼的。 故事并未结束,当时Patrick Naughton为Sun Microsystems工作了一段时间后,深感厌烦,想离开Sun ,寻求新的发展。然而公司挽留了他,你可以拥有一支开发队伍,只要你愿意,一切都可以由你指挥,但要给我们带来点酷的东西。于是一个名叫Green的团队产生了。 Green小组孤独地在荒野上不断的探索。他们寻求一种可用于嵌入式设备的面向对象语言,他们一开始在C+的基础上修改,但是C+的庞大使之无法满足他们的需要,于是他们在C+的基础上创建了一门新的语言Oak-这个命名仅仅因为James Gosling看到了相窗外的一颗橡树(Oak)。开发队伍仔细审视了这门语言,认为它相当的好。 也是在那个时代,美国巨型计算机应用中心开发出了Mosaic,这为我们带来了令人兴奋的WWW。随后Bill Joy试图公开Oak的源代码,使Oak能使用于网页浏览。Sun审视了这个想法,觉得不错,但Oak这个商标已经被人占用,所以Sun把这门新语言称为Java,并发布了the Java programming Language。这又一次使人们激动,耶!我们又有了一个真正与开发平台无关的语言!我们认为,这真是太酷了! 这就是C语言家族的早期故事,故事还在继续。2000年左右,微软宣布有史以来这个星球上最好的语言出现了-C#。Delphi的作者设计了C#。到2007年C#已经是一个成熟的语言,并且广为流行,C#宣传它优于C+的一个重要特点就是自动的内存管理,这将极大的解放程序员的负担,并使得程序更加的健壮。然后程序世界里很难有完美的解决方案,垃圾回收器一旦工作,将导致整个应用程序暂停,同时在内存受限系统中,仍然需要依靠程序员高超的技巧去控制内存的分配和回收,并且既然可以用C+为Java和C#开发让它们引以为傲的垃圾回收器,为什么就不能为C+自己实现垃圾回收管理呢?Herb Sutter已经证实,C+09标准一定会实现垃圾回收器。尽管Java、C#声势逼人,还有现在的新贵动态语言,但是C+世界仍然没有停止前进的脚步,泛型是C+对比其他语言很重要的优势,因为C+对泛型的支持最好。根据stroustrup的计划,2009年,C+09标准将出台,到时候对泛型的支持将进一步的提升,使得更容易理解和使用,同时C+世界还在努力做到跨平台,10多年间推出了很多重量级的C+库,包括Boost、Loki、QT、ACE、GSoap等,了解并在合适的时候使用这些知名库,对于我们提高软件开发效率、软件质量和个人的C+修养都极为重要。C+再也不是那个当年因为不能支持广泛的操作系统平台,而被Sun公司抛弃并创立Java的那个C+。故事还将继续下去,期待C+09标准的到来。第二章C+代表什么?根据stroustrup(C+之父)的最新定义,C+是多范型语言,它包含了以下四个子集:1) 标准C语言体现了过程化设计思想,优点是性能高,缺点是大规模应用难以维护,但是仍然是很重要并且有时候必须的技术2) 面向对象的C+体现了面向对象的设计思想,符合人类思考问题的习惯,设计模式面向对象技术的一个浓缩3) 模板C+体现了泛型设计思想,是完全不同于面向对象的另一个种技术,近年来大放光彩,比如知名的泛型库,Microsoft的ATL/ATL Server/WTL,彻底影响标准C+的STL等,boost和Loki4) STLSTL这里单独提出来,是因为它实在太重要了,提供了很多方便的容器、算法、迭代器,直接引起了整个软件工业界的一场革命第三章 内存分配(勿在浮沙筑高台)C+的内存区域分为几种:常量数据、栈、自由存储、堆、全局或静态。第一节 栈分配学过数据结构的人都知道,栈是一种先进后出的连续内存块。C+中每个函数执行时,都会创建一个私有栈,然后将参数依次放入栈中(通常由右向左),在这个函数中分配的自动变量,都会存入栈中,当函数返回时,栈中的数据会以入栈的相反顺序弹出。int main(int argc, _TCHAR* argv)int i=3;int j=9;return 0;在上面的main函数中,会存在一个属于该函数的栈,除了参数外,变量i会先于j获得栈中的内存并初始化,当main返回时,main的私有栈会将栈里存放的东西逐个弹出,j会先于i被清除。由于栈是一块大小固定的预先分配好的内存,所以在栈中分配一块内存给变量i仅仅是移动一个栈顶的指针,所以分配速度要比自由存储和堆中快的多。在C+中,栈是无处不在的,每个函数有,每个对象也有,栈的基本原理构成了智能指针的理论依据。第二节 在堆上分配内存void *malloc(size_t size);标准C风格的内存分配方法,以字节为基本单位,返回分配的一块内存的起始地址(如果成功的话),无类型概念。如果没有足够的内存,会返回NULL。凡是malloc分配的内存,在不用的时候,需要使用free释放。#include int main(int argc, _TCHAR* argv)size_t length=10;void* p=malloc(length);free(p);return 0;malloc分配内存时遍历一个链表,链表中各个元素均指向某块内存,通常从小到大排列,找到足够大的内存块后,该内存快将被拆分开来,同时链表上相应节点上的指针进行调整。malloc/freee比较适合用于分配大中型对象(数百个或者数千个bytes),但是并没有对分配小内存做优化。所以.Net的内存分配机制通常要快于C的malloc,因为他通过各种手段把内存造成一个连绵不绝没有尽头的连续内存块,分配内存只是移动一下指针。但是.Net也有自己的问题,必须依靠垃圾回收器回收不用的内存,然后重新整理内存,保证内存块总是够用,而垃圾回收器一旦工作,整个应用程序处于停止状态。第三节 在自由存储区域分配内存1. new/delete的幕后通常我们这样写代码:int* p=new int;.delete p;new int语句幕后发生了什么。首先调用标准c+提供的void* operator new(std:size_t _Count) throw(bad_alloc)函数分配sizeof(int)大小的内存,int类型的大小通常和机器内存总线大小相同,所以32位机器上,sizeof(int)=4字节。然后要看我们分配的是什么类型,如果是预定义类型,则什么都不做,直接返回一分配的内存块的指针,如果是一个c+的class,则会调用默认构造函数在已分配的内存上对对象初始化,然后返回指针。那么,int*p =new int()又代表什么呢?因为int是预定义类型,所以分配完内存后,初始化内存值为0。所以不用再写下面的代码了:int*p =new int*p=0;如果类型是一个class,则int* p=new string和int* p=new string()是等价的。现在我们明白了,平时我们用new int的时候,这个new是c+提供的一个基本操作符,和operator new函数不是一回事。当我们调用delete p的时候,如果p指向string,则先调用string的析构函数,然后调用operator delete回收内存。如果p指向的是预定义类型如int,则直接调用operator delete回收内存。幕后就这么多么?不是。实际上,每次使用new分配一块内存的时候,c+编译器会分配一块内存(4-32字节)用于管理,可能保存了要分配内存的大小,这样delete才能保证正确释放掉合适大小的内存。这是一个很麻烦的问题,如果你只分配了4字节,结果管理这4字节的内存耗用了16字节,天哪!Andrei说,老练的c+程序员都不会在这种情况下使用标准new。那么用什么呢?你可以使用Loki:SmallObject、Loki:SmallValueObject或者boost的pool库,他们使用了各自的策略,避免了这些额外的管理开销,以及频繁的new和delete。通常都是先分配一大块内存,然后慢慢用。2. operator new/delete 函数void* operator new(std:size_t _Count)throw(bad_alloc);void* operator new(std:size_t _Count,const std:nothrow_t&) throw( );void* operator new(std:size_t _Count, void* _Ptr) throw( );很多编译器在operator new的实现代码中使用了malloc,但是c+标准并没有规定事情一定会是这样,c+标准只规定了malloc实现代码一定不会调用operator new。为了不把我们的代码基于一个今后可能会变化的假设上,逻辑上我们应该认为operator new/delete和malloc/free并没有必然的联系,所以我们不能使用free来释放operator new分配的内存,或者调用delete来释放malloc分配的内存。 由于malloc天生的缺陷,我希望看到将来的operator new不用malloc实现。第一种形式的operator new如果分配内存失败会抛出bad_alloc异常对象,请不要检查返回的指针是否为NULL,那是过时的做法,不是标准做法。void* operator new(std:size_t _Count,const std:nothrow_t&) throw( );这种形式保证分配内存即使失败也不会抛出异常,只会返回NULL。void* p=:operator new(sizeofe(string),std:nothrow);if(p!=NULL).为什么要提供这种重载形式,内存分配错误是个十分严重的错误,因此用异常报告错误是十分合适的。但是如果你打算在一个循环里面分配内存,或者在一个时间要求极高的场合下分配内存,异常对象的传递还是慢了点。除此之外,还是尽量使用第一种重载形式比较好。第三种形式其实不分配内存,而是你提供一块内存,然后它帮助你初始化。void* p=:operator new(sizeof(string);/使用operator new的第一种形式分配内存new (p) string; /在已分配的内存上初始化一个string对象与operator new相对应的,也有三种重载的operator delete。void operator delete(void* _Ptr) throw( );void operator delete(void *, void *) throw( );void operator delete(void* _Ptr,const std:nothrow_t&) throw( );3. operator new/delete 函数void *operator new(std:size_t _Count)throw(std:bad_alloc);void *operator new(std:size_t _Count,const std:nothrow_t&) throw( );void *operator new(std:size_t _Count, void* _Ptr) throw( );上面第一个new函数负责分配数组空间,char* p=new char10; /如果是基本预定义类型,只分配数组空间不初始化char* p=new char10(); /如果是基本预定义类型,先分配数组空间然后每个元素初始化为0string* p=new string10;/ 如果是c+类,则不仅分配数组空间而且调用默认构造函数初始化每个元素。注意,请使用delete 销毁用new 创建的内存,如果是预定义类型,则也可以使用delete。这三个new和上面三个的区别就是分配数组和分配单个对象,其余几乎相同。void operator delete(void* _Ptr) throw( );void operator delete(void *, void *) throw( );void operator delete(void* _Ptr, const std:nothrow_t&) throw( );4. 出错处理void* operator new(std:size_t _Count)throw(bad_alloc);当我们调用上面形式的operator new分配内存时,函数最后面throw(bad_alloc)异常规格说明代表一旦分配内存出错,将抛出一个bad_alloc异常对象。我们该怎么处理?看看C+标准委员会主席Herb Sutter怎么阐述这个问题?他的观点概括为: (1) 在绝大多情况下,你都不需要关心,因为new通常都会成功好极了,我们的生活压力将因为这句话大大减轻。这个观点有几个理由:有些操作系统只有当内存真正使用的时候,才会真正的分配内存,也就是说,new总是工作得很好,等你用那块内存的时候,才有可能因为分配不到内存报错,所以检查new是否成功无意义;在一个虚拟内存系统上,new失败的可能性很小;即便new真的失败了,你又能怎么样呢?让程序崩溃实际上是一个很好的办法。 (2 )在某些特定情况下,检查new是否成功有意义:如果能够预见到应用程序将使用多少内存,一开始就分配好所有需要的内存,如果要失败,就在一开始失败巴;如果试图分配一个巨大的内存块,new失败后,也许你要做的事情就是再次调用new,只不过不要那么贪婪,少要点内存。如果你真的有足够的理由要检查并处理new的错误,那就需要了解operator new内部错误处理流程。当operator new的第一种重载形式在内部分配内存失败,他会通过指向new-handler函数的指针调用new-handler函数,如果new-handler函数指针为NULL,则抛出异常,如果指针有效,就在一个循环里面调用函数new-handler函数,直到new_handler函数1)释放了一定的内存,使得循环内部下一次分配内存成功,则满意退出2)当前这个new-handler函数认为自己没有办法,只好设置另一个函数为new-handler函数,循环会在下一次分配内存失败使调用新的new-handler函数3)如果认为没有办法继续下去,new-handler内部可以抛出bad_alloc异常或者将当前的new-handler设置为NULL,最终导致退出循环,客户代码收到异常对象。如何编码的细节,我们将在讲述class专有operator new的时候描述。这里你们需要知道,因为默认情况下new-handler函数的指针为NULL,所以总是在错误的时候抛出异常。第四节 常量区域顾名思义,只有常量才能存在的区域,该区域内的变量将存在直到整个程序结束,并且不能被修改。第五节 全局或静态区域程序启动时,全局变量或者静态变量将被分配内存并初始化,直到整个程序结束才销毁。如果几个全局或者静态变量在多个C+文件中使用,他们的初始化顺序是不确定的,所以他们之间最好不要有依赖关系。注意,在常量、全局或者静态区域定义的预定义变量通常不仅分配了内存,而且会自动初始化,比如int变量会初始化为0,而栈中创建的预定义变量不会自动初始化。第四章 指针第一节 指针代表什么?我接触过的很多对C+心存敬畏之心的人(他们大多不懂C+)都跟我说“指针是非常容易出错的,但是一旦学会了指针也就学会了C+”。虽然我不同意这个观点,但或许这是从某个角度提出了指针在C+语言中的重要地位。指针是什么?指针是一个占用四字节内存的特殊整型变量,它里面保存的是另一块内存的起始地址,并通过指针的静态类型标志这块内存是什么类型。看看下面的代码:int main(void)int * p=new int(5);短短几句话,发生了多少动作呢?简单分析一下:首先进入main函数前,为该函数分配了私有栈,栈的大小通常是固定的并且可以配置。进入函数后执行new int(5)使得在堆上分配4个字节,并且将这四个字节初始化为整形值5,然后栈顶指针向上移动四个字节,将这四个字节作为p变量的空间,这四个字节将保存刚才存放5的堆地址。当函数返回时,栈将被销毁,因此p将不再存在,但是new int(5)获得的堆上的4字节空间将依然存在,但是却没有办法再使用或者回收它,因此发生了最可怕的事情内存泄漏。第二节 野指针好,刚才至少我们明白了指针也就是个4字节的变量。现在看下面的代码:int * p1=NULL;int * p2;p1变量内部存放的值为0,这使得它不能指向任何有效地址。p2 由于之分配了4字节空间,并没有初始化,因此它里面的值应该是上次对该块内存使用后遗留下来的,是多少谁都不知道,或许是0,或许指向某处你绝对不想让它指向的地方,这称为野指针。所以下面的代码就很危险,*p2=0 ;你都不知道你把什么内存给改写了,结果是无法预料的。野指针相当危险,所以比较好的做法是初始化为NULL。但是C+中很难有什么绝对遵守的准则,C+给了你很大的权限去选择不同的方案。如果在一个你确信不会出现问题的地方,并且性能是很关键的地方,我为什么多此一举要赋初值呢?我本人就有时候故意不这样做。我对大家的建议是了解原理,然后自己控制,在对自己的控制能力没有信心的时候,遵守较安全的做法是明智的。还有一种产生野指针的常见情况:char* p=new char(b);.delete p;.coutpendl;delete语句已经把p指向的堆上的一字节内存销毁了,但是并不会清空p的值,也就是说p仍然指向堆上的那个字节,然后coutpendl;会出现什么情况,无法预料。也许堆上的那个字节已经被改写,或者没有。下面的代码会对这种情况有所帮助:char* p=new char(b);.delete p;p=NULL;.if(p!=NULL)/p仍然有效coutpendl;但其实这是一个逻辑错误,既然delete p都指向了,无论如何,都不应该再使用p。修正逻辑才是治本,if(p!=NULL)只是打补丁的做法。第三节 指针的类型(1)、指针的静态类型:int* p=new int(5);这句话里我们的p变量的静态类型是int*,这就是告诉编译器p所指向的内存应该看作int变量,起始地址是p里面的值,大小是sizeof(int)。char* p=new char100;char* pChar=p;int* pInt=p;+pChar;+pInt;由于pChar的静态类型为char*,所以每次执行+,都会向后移动一个字节,由于pInt静态类型是int*,所以每次执行+,都会向后移动sizeof(int)个字节(通常为4字节)。(2)、指针的动态类型:指针的动态类型是指在多态的情况下,静态类型为指向基类的指针,实际指向的子对象的类型就是该指针的动态类型。比如class B派生自class A,我们写了下面的代码:A* p=new B();这句话说明p的静态类型是A*,但是实际指向的对象类型是B,该指针得动态类型是B*。动态类型在多态运用中起到十分重要的作用,绝大多数设计模式都以此为基础,微软的著名技术COM也是基于此。后面在虚函数部分我们会详细讨论。第四节 智能指针通常如果我们通过new操作获得了一个指针,我们需要记住在不需要使用的时候使用delete操作。如:void f()string* p=new string(“hello,world”);.delete p;但是,可能会遇到这种情况,在delete p被执行之前的语句里面出错而抛出了一个异常对象,f函数将立刻返回,delete p将不会被执行,这样内存就泄露了。遇到这种情况,我们有几个办法:1)不要使用new/delete,改在栈内创建对象void f()string str(“hello,world”);.这是个好办法,而且速度很快,如果能用,尽量用这种.2)写一个class,利用栈的机制来管理class StringManagerpublic:StringManger(string* pStr):_pStr(pStr)StringManager()delete _pStr;private:string* _pStr;void f()StringManager manager(new string(“hello,world”);.无论函数f内部是否抛出异常,只要f函数返回,私有栈必然要销毁,那么栈上分配的StringManager对象的析构函数一定会被调用,所以delete _pStr语句一定会被执行。这就是目前广为使用的智能指针的基本原理。目前标准C+2003修正版中常用的智能指针有auto_ptr和shared_ptr,我们公司的BFL类库中提供了其他的一些智能指针类。在后面我会逐步介绍,并分析优缺点,智能指针有其优点,但是并不是万能的药方,只有当你充分明白了它们的优缺点,才可以安全的用好它们。第五节 指针用作参数在前面我们介绍栈的时候,说过一个函数拥有一个私有栈,当函数执行时,会先将参数值拷贝到栈中,比如:void f(int i,int* p)*p=i;int main(void)int a(5);int b;f(a,&b);return 0;f函数执行时,通常从右到左的顺序拷贝p和i到栈中,这样栈中有一个p的副本变量p和i的副本i,然后通过*p=i 将i的值赋给了p指向的变量b。这就是常说的传址和传值,对于b变量,是传址,对于a变量是传值。这样使用指针会带来什么好处呢?首先可以在函数f内部修改外面b变量的值,其次如果b变量不是简单类型,而是复杂如string的对象,只传递4字节的指针性能是非常快的。我们经常见到类似这样的指针参数void f(int* p),指针的指针,为什么要这么用呢?看下面的示例代码:void f(int* p)*p=new int(5);int main(void)int* pValue=NULL;f(&pValue);.delete pValue;f函数在堆上分配了一块4字节整数区域,初始化为5,然后让外部的指针变量pValue指向这块堆上的内存。我们来分析一下:一开始,pValue指针变量被创建,但是内容为0,即什么都不指向,然后将pValue指针变量所在的内存地址作为int* p传递给f函数,f函数将在自己的栈中保存p指针变量的地址副本,写成伪代码应该是:void f(int* p)int* p=p;*p=new int(5);*p其实就是pValue,所以等价于外部pValue=new int(5);然后函数f返回p被销毁,但是pValue已经指向堆上的有效内存了。请注意,涉及这样的函数应该写上注释,告诉用户是使用什么函数释放内存delete还是free或者其他,因为有可能用户看不到f内部实现的代码。微软的COM总是使用这种策略。第五章 指针和引用引用很有可能就是常指针实现的,但是引用有特殊的约束。引用不会为空,所以当函数接收一个引用参数时,不需要检测该引用所指定的对象是否为空,指针可以为空,所以当某函数接收指针作为参数时,你经常会看到这样的代码:assert(p!=NULL)引用必须被初始化,而指针变量没有这个限制。引用一旦被初始化后,只能代表初始化设定的对象,而指针是可以改变指向的对象。第六章 课后练习11)设计一个DynamicCharArray类,要求使用char* _pData作为私有成员,实现下列成员函数:class DynamicCharArraypublic:DynamicCharArray();DynamicCharArray(size_t size); DynamicCharArray();char getAt(size_t index);void setAt(size_t index,char value);private:char* _pData;2)告诉我,下面的代码执行后发生了哪些事情?void Func(int x,string & str)string strTmp=str;string* pStr=new string(“hello,world”);int i=6;str=”ok”;return;第七章 Const作为一个基本知识点,要理解const char * const p的含义。第一个const代表p所指向的存储区域是常量,当初始化后不可以被修改;第二个const代表的是变量p一旦指向了某个存储区域后,它就不可以指向别的区域。在我的文章里面,经常会看到char const* p或者int const a这种用法。他们等价于const char* p和const int a。const char* p和 char const* p都代表p指向的内存区域是常量,const都是修饰char类型的,但是显然后者更明显。我们应该把const看成是对前面类型的修饰,这样const将char和*p很自然的分隔开来。char * const p中,const修饰char*类型,代表该指针是不可变的常量指针。你不觉得这种做法和修饰函数的做法一样么?你肯定见过void A:f() const的用法。在typedef的使用中,后置const的用法不会产生感觉上的混乱,比如:typedef char* CHARS;typedef const CHARS CPTR;/指向char类型的常量指针,不要觉得奇怪,事实就是这样typedef char* CHARS;typedef const CHARS CPTR;int _tmain(int argc, _TCHAR* argv)CPTR p=a;p=b;return 0;error C3892: p : you cannot assign to a variable that is const有一天,我们忽然用CHARS的等价形式修改了第二句话typedef const char* CPTR;typedef char* CHARS;typedef const char* CPTR;int _tmain(int argc, _TCHAR* argv)CPTR p=a;p=b;return 0;正确。问题出在typedef const char* CPTR;和typedef const CHARS CPTR;居然不等价。当你用后置const来表示,混乱就消失了。typedef char* CHARS;typedef CHARS const CPTR;int _tmain(int argc, _TCHAR* argv)CPTR p=a;p=b;return 0;和下面的typedef char* CHARS;typedef char* const CPTR;int _tmain(int argc, _TCHAR* argv)CPTR p=a;p=b;return 0;都会报同样的错误,error C3892: p : you cannot assign to a variable that is const。我们应该习惯多用const,并且知道何时不该滥用。1)如果一个类的成员函数不会修改内部数据,用const修饰这是一种提醒,并且也会带来方便。比如下面的代码中,如果你不加const修饰函数,就会遇到麻烦:class Apublic:void f();void Sample(A const& a)a.f();int _tmain(int argc, _TCHAR* argv)A a;Sample(a);return 0;error C2662: A:f : cannot convert this pointer from const A to A &而像Sample这样的函数你有很大的几率遇到。2)我们也有可能写Sample这样的函数,当我们的参数是值传递时,不要用const,因为这不会带来好处,还会带来误导,当我们的参数是引用或者指针时,使用const修饰,明确告诉用户传址以提高效率,但是决不会在函数内部修改。3)如果你不想让别人对你的函数返回值作修改,给它一个const修饰符也不错4)哲学上,const可以被理解为物理常量和逻辑常量,当一个成员函数被修饰为const,物理常量的理解认为该函数内部绝不可以修改类的成员,逻辑常量理论认为,出于优化的目的,你可以悄悄地修改某个变量,只要不让客户感觉到就可以。我站在逻辑常量这一边。如果你想要在一个const成员函数内修改某个变量,你需要为这个变量加上一个修饰符mutable。比如:class Apublic:void f() const_f=2;private:mutable int _f;int _tmain(int argc, _TCHAR* argv)A a;a.f();return 0;5) 我们有时候需要进行const相关的类型转换从非const引用(或者指针)到const引用是自动完成的。例如: char* p=f;char const* p2=p;反过来去掉const约束的时候,你需要const_cast转换符 char const* p=f;char* p2=const_cast(p);第八章 类的技术细节第一节struct和classstruct和class最重要的区别是哲学上的,struct代表着C风格设计思想,class代表着面向对象c+的设计思想,延伸一下,typename代表着c+泛型的思想。class和struct在大多数情况下可以互换,是因为c+要兼容c的代码所致,就像typename和class可以互换一样。这里我们有足够的自由去选择用struct和class,看你打算在自己的设计中使用哪一种编程哲学。除此之外,struct默认访问权限是public,而class默认访问权限是private。还有一点,struct里面的成员变量的内存布局总是按照声明的次序排列的,但是class不一样。在同一个访问权限块内部的成员变量内存布局总是按照声明的次序排列,但是如果多个访问权限块内部的成员变量,顺序不能假定一定是连续的。同样的情况,如果一个类派生自另一个类,我们不能假定这个子类的成员变量总是排在父类的成员变量之后。第二节 类的基本元素成员变量分为静态和非静态成员函数分为静态函数和非静态非虚函数以及虚函数同时还有从基类继承下来的成员变量和成员函数采用虚继承导致的额外的成员变量和数据结构(由各编译器自己实现,标准没有规定)例如:类ostream,istream分别代表输出和输入流,它们均派生自ios类,由于考虑到iostream从它们两个派生以同时具备输入和输出流的能力,因此ostream和istream均虚继承自ios类,使得iostream对象中不会拥有两份ios对象的数据成员拷贝。class ostream:virtual public iosclass istream:virtual public iosclass iostream:public ostream,public istream虚继承的语法就是提醒编译器,不要将父类的数据成员放到子类的内存空间中。C+标准并没有规定具体要怎么做,编译器可以实现自己的策略。一种可能的布局是:ostream和istream以及iostream中都有一个指针,指针指向一个表格,表格中存放的是虚基类的起始地址。第三节 虚函数1. 虚函数的内存布局一个拥有虚函数的类内部通常会有一个成员变量vptr,一个四字节大小的指针,指向虚函数表,虚函数表中记录了该类的各个虚函数的入口地址,如果该类重写了继承的虚函数,那么就存放自己的虚函数地址,否则就是父类的虚函数地址。以下是一个Point类的声明:class Pointpublic:Point(float);virtual Print(ostream& stream);virtual Point();static int PointCount();static int _point_count;float x();private:float _x;他的可能的内存布局如下:在这里我们可以看到,静态成员函数、静态成员变量、非静态非虚成员函数都不会占用对象的内存空间。占用空间的通常是非静态成员变量和从父类继承下来的非静态成员变量以及虚函数指针,当然虚继承导致的额外的成员指针也要占用空间。class Apublic:virtual void f();virtual A();class B:public Avoid f()int i=0;A* pA=new B();pA-f();对于f的调用操作编译器有如下动作:void B:f()函数解释为void f(B* this);pA-f()解释为 (*pA-vptr1)(this);/1是f函数在虚拟函数表格中的索引所以我们可以看出,虽然指针pA静态类型为A类的指针,但是对于f的调用是依赖于B对象内部的vptr指向的虚拟函数表,而此时函数表内的f函数已经是B类的重载版本,因此这就构成了运行时多态,这个c+的基本特征。虚继承情况下的一种可能的内存布局,摘自c+对话系列。class parent /* whatever */ ;class child1 : public virtual parent /* whatever */ ;class child2 : public virtual parent /* whatever */ ;class multi : public child1, public child2 /* whatever */ ;parent:vptrparent datachild1:vptrchild1 datachild2:vptrchild2 datamulti:vptrmulti data在这种复杂的情况下,最底层派生对象内部拥有三个vptr,指向三个虚函数表。注意,经过我的实验,这种内存布局各种编译器表现不一样,比如vc就是先把child1放在最前面,然后是child2,最后是parent,并且至少vc中,我们可以通过调用static_cast获得各个类型的vptr值,这种典型的应用是在com的QueryInterface函数的实现里面。经过上面的分析,我知道上面的虚继承会带来多个虚函数表以及多个vptr,这是内存上的额外的开销,当然避免了多个顶级父类的内存副本和模棱两可的继承,也有它的好处。我们可以看看com里面常常出现的多继承带来的对象内存布局class CPenguin : public IBird, public ISnappyDresser .;IBird和ISnappyDresser接口都继承自IUnknown接口,内存布局如下图:2. 纯虚函数和虚函数的区别参考 3th Edition,这里作个简单的概括:纯虚函数分为函数定义和没有函数定义-没有函数定义的纯虚函数目的是为了让子类继承接口,强制子类实现该函数;有函数定义的纯虚函数目的是为了让子类继承接口,而父类的实现函数必须在子类的该函数中手动调用非纯虚函数目的是让子类自动的继承接口和函数实现 3. 虚函数与访问权限虚函数的重写机制和访问权限是相互独立的两套机制,互相没有干扰,但是可以结合使用。一个private权限的虚函数可以被子类重新实现,但是子类不能访问该虚函数,而父类却可以通过运行时多态的方式来调用子类重载后的虚函数。一个protected权限的虚函数可以被子类重新实现,子类也可以访问该虚函数,外部

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论