1、1,二. C/C 预备知识,变量的存储类型函数参数的传递封装 类及其成员继承 基类与派生类派生类对象访问基类成员一致性难题多态 虚函数 一致性与功能虚函数实现机制object slice多重继承多重继承的虚表二义性RTTI,类型转换显示转换(强行转换)动态转换静态转换动态链接库名字改编函数模板类模板友元函数.操作符重载,2,1 变量的存储类型,全局变量:在任何函数之外定义,可以被任何代码所访问.在其他的文件中使用这些变量时,使用extern来声明之,使得编译器了解这些变量的类型和名字,但是不重新分配内存.Static全局变量:只在文件范围内可见. (使得开发者可以任意地使用变量名字,不必担心重
2、名.)自动变量:函数体内部.在stack中Static局部变量: 在函数体内部定义,编译器为之生成永久存储单元,在调用之前就存在.只在函数内部可见.动态分配变量: new delete 在heap中.类的static成员变量:只与类有关.属于类而不属于对象的变量.(静态成员函数,没有this指针,其中只能访问静态成员变量.)以下例子见virtualfun工程,3,2 函数参数的传递,引用运算符 C+中缺省的参数初始化方法是把实参的值拷贝到参数的存储区中. 形参的值可以改变, 但此函数并不能改变实参的值.,4,2 void swap2(int *px,int *py) int temp; tem
3、p=*px; *px=*py; *py=temp; ;同上,函数不能改变实参的值(即地址值),在这种意义下, 传地址和传值是等价的.虽然不能改变实参的的值,但是改变了实参(即地址)所存储的值. 如果要改变一个指针的值,那么就要使用双重指针.3 void swap3(int 与前两个不同,形参是实参的别名.实参形参是同一个变量.,5,调用方法: 1.int a=2, b=5; swap1(a,b); coutab; /2 5 2. int a=2,b=5; swap2( /5 2 若希望通过一个函数改变一个指针,那么可以使用指针的引用或者更常用地使用双重指针作为函数的参数.,6,3 封装 类及其
4、成员,类。mobile telephone 手机class mtprivate:int price; public:void work();void setprice(int initp) price=inip;void getprice(int *oldp) *oldp=price; /数据成员是私有的,客户只能通过公有的方法,在方法内部来访问。mt(void);mt(void);,7,C+编译器对于数据成员和函数成员的处理:数据成员: 静态的: 定义了类,就会给静态数据分配内存. 非静态的: new了实例,每个实例都为之分配.函数成员:定义了类,实现了该函数后,就有该函数的入口指针.对于新
5、new的实例并不产生新的指针.必须通过实例来调用它,不同的实例都是调用同一个函数. 因此存在者重入性和并发性的安全问题,如果函数体里使用了静态的变量(函数级的或者是外部的)的话. 如果是非静态的成员函数,则每一个实例调用它时,会传入一个指向实例本身的指针 this.实例借此可以访问其他的成员(变量或函数)如果是静态的成员函数,则在调用时无this指针,因此不能访问类的非静态的成员(变量或者函数).如果是虚函数,则会为此类建立一个虚表,每个表项是一个指针,指向此函数的实现.这是额外的开销. 见后文.虚函数不能是静态的。,8,4 继承 基类与派生类,4.1 派生类对象访问基类成员共有的性质提升到基
6、类中。class EE /电子设备protected:int price; /保护的数据成员可以被派生类的成员函数访问public:void Work();void SetPrice(int initp);void GetPrice(int *oldp);void NVWhoAmI() cout“I am a EE”NVWhoAmI (); / 结果为:I am a EE pmtpbvar=; /也可以直接访问基类的公有的数据成员:假设pbvar是EE的公共数据成员基类保护成员的访问方法: 对于基类的保护的数据成员或方法则只能在派生类的成员内部访问,比如: void MT:MTSetPrice
7、 (int price) EE:price =price; /派生类成员函数访问基类的保护数据成员客户使用方法: MT *pmt=new MT; pmt-MTSetPrice(5); /客户调用派生类的成员函数,函数内部访问基类 的保护数据成员. pmt-price=5; /出错.不能直接访问.,11,4.2 一致性难题,单单使用普通的成员函数有力不从心之处:这是另一个派生类:class CA :public EE /照相机 protected: int pixel; /象素public:void TakePhoto(int *pix);CA(void);CA(void);对于以下代码:MT
8、*pmt=new MT;CA *pca=new CA; pmt-NVWhoAmI (); /派生类访问基类的公有的方法。pca-NVWhoAmI (); /另一个派生类访问基类的公有的方法。,12,结果为:I am a EE I am a EE 结果当然是一样的.但是我们期望能得到更精细的信息,为此在派生类中覆盖此函数class MT :public EE protected:int rent;public:void Call();void NVWhoAmI(); /签名虽然完全相同,但它是派生类定义的成员, 或者理解成为碰巧完全相同。当然我们可以再加上一个前缀MT, 但是会显得太罗嗦, C+
9、支持重载. 虽然这种方式不好,但是是合法的. 其实现如下: void MTSetPrice(int price); void MTGetPrice(int *oldp);MT(void);MT(void);,13,void MT:NVWhoAmI () coutNVWhoAmI (); / I am a MT pca-NVWhoAmI (); / I am a CA 比刚才有进步.但是问题是,如果针对别的电子设备,我们不得不写下这样的代码: ptv-NVWhoAmI (); / I am a Television ppc-NVWhoAmI (); / I am a Personal Compu
10、ter. prd-NVWhoAmI (); / I am a Radio 。 维护困难。,14,因此, 为了追求统一性,我们往往把基类指针指向子类(派生类)的对象. 在直观上当然是合理的. (彩屏手机也是手机啊!). 子类对象比基类对象更“大”, 子类对象中含有一个基类的子对象 “subobject”. 基类的指针将指向这个子对象. (不包括子类定义的成员). 如下图所示:,15,对于以下代码: EE *pee2;pee0=new MT; /基类指针指向子类的对象pee1=new CA;for(int i=0;iNVWhoAmI (); /基类指针只能调用基类定义的成员 结果为: I am a
11、 EE I am a EE我们用一个基类的指针指向派生类的对象时(MT当然是一个EE,CA也是一个EE),由基类指针来调用NVWhoAmI(我们蒙上眼睛,双手各拿着一个EE(电器设备),它到底是什么?手机还是照相机?) 结果是令人失望的,它们都说:我是一个EE。Dilemma:要么睁大眼睛,分别使用不同的派生类指针来询问其身份(如果有10个不同的派生类,我们要写10句雷同的p*-NVWhoAmI() ) ;要么虽然很优雅,使用基类指针通过一个循环来调用. 但只能得到一个笼统的回答。,16,5 多态 虚函数,5.1 一致性与功能 我们对于效率,美观,功能的需求导致我们需要多态.虚函数是实现多态的
12、一种方式.1.当然,还有别的方式.2.我们付出了代价. 相对于普通函数而言,虚表及虚表指针是额外的开销,处理不当,可能会达到不能忍受的地步,而且我们还要付出脑力的代价 .class EE protected:int price;public:void Work();void SetPrice(int initp);void GetPrice(int *oldp);void NVWhoAmI();virtual void WhoAmI()cout“I am a EE”endl; /虚函数 关键字virtualEE(void);EE(void); ;派生类的定义如下:,17,class MT :p
13、ublic EE protected:int rent;public:void Call();void NVWhoAmI();void WhoAmI(); /虽然没有加(也可以加.) virtual 也是虚函数。MT(void);MT(void);void MT:WhoAmI ()cout“ I am a MT”endl; /改写(override)此虚函数,18,对于另一个类CA我们也作类似的处理:void CA:WhoAmI ()coutWhoAmI (); / 1. 一致,优雅结果为: I am a MT I am a CA / 2. 信息准确,丰富.,19,5.2 虚函数实现机制,编译
14、器为每个包含虚函数的类产生一个静态函数指针数组,即虚函数表vtbl,在这个类或它的基类中定义的每一个虚函数都有一个相应的函数指针,该指针指向它的实现。该类的每个实例包含一个不可见的数据成员,即虚函数指针vptr,这个指针被构造函数自动初始化,指向类的虚表。当客户调用虚函数的时候,编译器产生代码指向vptr,索引到虚表中,找到函数指针发出调用。此虚表在类有了定义以后就由编译器分配了.它是与类(而不是对象)相关的静态的指针数组.类实例化为对象后,对象的虚表指针将指向此虚表.对象的虚表指针往往放在其他数据成员的前面.对象的this指针将指向此虚表指针.虚函数的额外开销: (32位系统中)类: 包含此
15、虚函数的类(定义的或继承来的)虚表(一个函数一个表项 4字节)对象: 每生成一个对象,有一个虚表指针 4字节.,20,纯虚函数: 没有实现的虚函数成为纯虚函数。包含有纯虚函数的类称为抽象类。抽象类的虚表中纯虚函数的表项为空指针。由此,一个包含纯虚函数的类是不能实例化的.否则,它的实例调用此函数该怎么办? 纯虚函数的意义在于规定一个方法的签名,而由其派生类去实现它。EE与MT的虚表示意图如下:,21,虚表与虚表指针的示例:,22,使用虚函数实现多态。 EE *pee2;pee0=new MT;pee1=new CA;for(int i=0;iWhoAmI (); 基类的指针指向不同的子类对象。这
16、些子类对基类所定义的虚函数有不同的实现。当通过基类指针调用这些函数的时候,调用的是子类的带有个性的实现。 如下图所示:,23,24,5.3 object slice,当基类指针指向派生类对象时,通过基类指针调用虚函数时,编译器会找到其所指向的实际对象对虚函数的实现.通过基类指针或引用间接指向派生类的对象时,多态性才起作用.使用基类对象并不能保留住派生类的类型身份. MT mt;EE ee;ee=mt; /object slice 非正常EE *pee= / I am a MT ee=mt 进行了一次object slice. Mt被slice只剩下一半,而且WhoAmI函数还要使用mt的thi
17、s指针,情况会如何?实际上编译器自动为EE生成了一个拷贝构造函数.这时ee是一个完全的EE对象.它调用WhoAmI时将使用自己的实现. 我们要避免使用object slice以上例子见virtualfun工程,25,6 多重继承,6.1 多重继承的虚表以下例子见multiinh工程. 把以上三个类整理如下:/ 1. 电器class EE protected:int price;public: virtual void Work()coutEE is workingendl; virtual void WhoAmI()cout I am a EEendl; EE(void); EE(void);
18、 ;/2 手机class MT : public EE protected:int rent; public:void WhoAmI()cout“I am a MT”endl; /改写 virtual void Call()cout“MT is calling”endl;/打电话 MT(void); MT(void); ;,26,/3. 照相机class CA :public EE protected: int pixel; public:void Work()cout“CA is working”endl;/这里Work也改写了,作为对比,MT没有改写它. void WhoAmI()cout
19、“I am a CA”endl; /改写 virtual void TakePhoto() cout“CA is takephoto”endl; /照像 CA(void);CA(void); ;假设现在有一款能拍照的手机:class MTCA :public MT,public CA / 4. 能拍照的手机public:virtual void WhoAmI()coutI am a MTCAendl; /继承自EE. 改写.virtual void Call()cout MTCA is callingendl; /继承自MT 改写virtual void TakePhoto()coutMTCA
20、 is TakePhotoWhoAmI (); / I am a MTCA /没有二义性。因为MTCA对其进行了改写,消除了MT和CA带来的差异性。通过MTCA指针调用WhoAmI将指向自己的实现pmtca-Work(); / MTCA 有两个Work, 二义性, 调用不明确,编译出错 MTCA没有对Work进行覆盖,其虚表中有两项Work,分别指向MT和CA对其的实现.无法通过编译. 编译器不知道要调用哪一个. pmtca-CA:Work(); / CA is working /将调用CA的实现,而CA覆盖了它; pmtca-MT:Work(); / EE is working /将调用MT
21、的实现,而MT没有覆盖它,直接使用EE的实现.(类似地, pmtca-price,也有二义性。必须象pmtca-CA:price pmtca-MT:price来使用),31,2. 转换二义性: EE *pee3; pee0=(EE*)pmtca; /存在二义性, 转换不明确,编译出错 pee1=(EE*)(MT*)pmtca; /转换途径1pee2=(EE*)(CA*)pmtca; /转换途径2 这种二义性正是多重继承遭到诸多非议的原因.使用虚继承可以消除这种二义性.但是虚继承和多重继承在语义上已经有了差别。,32,7 RTTI,为了一致性,我们往往把基类指针指向子类对象.而且这样做总是安全的
22、。我们甚至可以通过此指针调用到子类所实现的虚函数。但是除此之外,我们通过这个指针并不能做更多的工作。基类指针对子类其他独特的个性是一无所知的。 面对一个基类指针,在行动之前,要准确地探明它指向的是什么.即RTTI.我们定义的WhoAmI就是一个简单的RTTI.MFC中使用类型识别网的方法来实现RTTI。C提供了两个操作符来支持RTTI。typeid 和dynamic_cast,33,运算符typeid是一个重载的运算符,它可以有一种以上的形式,它的参数可以是类名称或对象的指针或对象变量,它能在运行期判断变量的类型。运算符的返回值是一个type_info的引用类型。type_info是一个类,定
23、义如下:class type_info public: virtual type_info(); int operator=(const type_info 类type_info 重载了= 和!=,用以对两个类型进行比较操作,它的成员函数name可以输出类型的字符串型的名字.,34,typeid有两种典型的用法:用法一:void main() MT* pm = new MT; EE* pe = pm; cout typeid( pe ).name() endl; /class EE 变量是EE类型cout typeid( *pe ).name() endl; /class MT 指向的却是MT
24、cout typeid( pm ).name() endl; / class MT cout typeid( *pm ).name() Call(); if (typeid(CA)=typeid(*pe) (CA*)pe-TakePhoto();不能直接使用 pe-Call(); 编译期出错。也不能直接使用 (MT*)pe-Call(); 潜在的运行期错误可以使用我们定义的WhoAmI来代替typeid完成其功能 if(pe-WhoAmI().) (MT*)pe-Call(); 在准确地了解了对象的类型后,就可以正确地转换,从而进行正确的调用.,36,这样我们可以安全地使用基类指针来操作子类对
25、象。void main() EE * pe1=new MT;EE * pe2=new CA; DoYourWork(pe1); DoYourWork(pe2);,37,8 类型转换,EE * pe1=new MT;使用基类指针指向子类对象实际上是一个类型转换。称为向上转换,向上转换是为了统一性。if (typeid(MT)=typeid(*pe) (MT*)pe-Call();把基类指针再转换为子类指针,称为向下转换。向下转换是为了差异性。在C中有多种转换方法:隐式转换,显示转换,静态转换,动态转换。所谓隐式转换即直接使用操作符。不做任何处理。比如EE * pe1pmt;隐式转换只能用于向上转
26、换。而且总是能成功。8.1 显示转换(强行转换):8.1.1 向上转换向上显式转换总可以成功(等价于隐示转换). MTCA *pmtca=new MTCA;MT *pmt= (MT*) pmtca; /等价于MT *pmt=pmtca;转换以后的效果如下图所示:( 蓝色加亮部分),38,39,8.1.2 向下转换向下的隐式转换无法通过编译,向下的显示转换虽然可以通过编译,但是应用程序无法得到转换的信息,而且有可能发生运行时错误(实际上,显示转换表面上总是可以成功,哪怕根本不相关的转换. 有可能是假象,从而造成运行时错误): MT *pmt1=new MT; MT *pmt2=new MTCA;
27、MTCA *pmatca0=pmt1; /向下的隐式转换出错MTCA *pmtca1,*pmtca2;pmtca1=(MTCA*)pmt1; /pmt1指向的是一个MT 成功 pmtca2=(MTCA*)pmt2; / pmt2指向的是一个MTCA. 成功pmtca1-TakePhoto (); / 危险!此调用虽然通过编译,将引发运行时错误!因为pmtca1根本就指向的是MT,没有TakePhoto方法! pmtca2-TakePhoto ();/这次,碰巧成功了.但是用户完全无法控制. pmtca1-WhoAmI ();/ I am a MT pmtca2-WhoAmI ();/ I am
28、 a MTCA 用户只有通过“RTTI”才能分清. 向下转换如下图所示:,40,向下的显示的转换,从pmt-pmtca. 即(pmtca=(MTCA*)pmt; 用户无法从转换结果中判断pmt最初指向的是一个完整的子类对象(白色加蓝色),还是一个基类对象(蓝色).当pmt是一个完整的对象时(蓝色+白色). 转换真正成功. 当pmt是纯蓝时,是一个假象.使用pmtca调用白色区域.产生运行期错误.无论哪种情况,客户都将得到一个指针pmtca. 除非使用RTTI, 客户莫辨真伪.,41,8. 1.3 交叉转换交叉转换是为了执行目标类对象所能执行的功能.(等价于一次向上转换和一次向下转换的综合)MT
29、 *pmt= new MTCA; CA *pca=pmt; /交叉隐式转换出错CA *pca=(CA*)pmt; /交叉显式转换可以成功,但是.pca-WhoAmI (); / I am a MTCApca-TakePhoto (); / MTCA is calling ! /不是MTCA is takephoto!而且进一步, pca-pixel实际上仍然是MT所定义的rent的值! (如果,比如在MT的构造函数内对rent赋值为50, 在CA的构造函数对pixel赋值为300万. 我们看到pca-pixel=50.)交叉转换如下图所示:理想的转换结果希望得到绿色.实际上这正是下文介绍的动态
30、转换.,42,而实际上显示的cross cast的结果,见棕色部分.尤其注意TakePhoto 函数和Pixel变量,在它们的内存位置实际上保存的是Call的函数指针和rent变量.,43,使用显式的交叉转换并不安全.虚表中的可见部分不能被改变. Pca所指向的对象的虚表仍然是原来的虚表.(即MT类的) 编译器根据名字TakePhoto在虚表中找到第三项,但是其中的指针指向的是MT类的Call,其实现是MTCA类的对它的实现 MTCA is takephoto.所以声明虚函数成员的时候,其次序也很重要! 如果在MT中再声明一个虚函数 virtual void dd()coutTakePhoto
31、 ();的结果将为: dfdfdfdfd这种特性,有时也有其独特的用途.(见COM 聚合模型),44,8.2. 动态转换,运算符语法: dynamic_cast ( expression )type-id是事先定义的类的指针或引用类型.Expression是一个指针或一个左值.这里讨论type-id 是一个指针的情形。8.2.1 Upcast:void f(MTCA* pmc) MT* pm = dynamic_cast(pmc); / 转换到基类 pm指向pmc的MT型子对象EE* pe = dynamic_cast(pm); /转换到基类 pe指向pm的EE型子对象 实际上upcast不需
32、要使用dynamic_cast,直接使用隐式转换也可。,45,8.2.2 Downcast:void f()MT* pmt1 = new MTCA; /基类指针指向一个派生类对象MT* pmt2 = new MT; /基类指针指向一个基类对象 MTCA* pmtca1 = dynamic_cast(pmt1); / 成功!pmt1本来指的就是一个派生类对象 MTCA* pmtca2 = dynamic_cast(pmt2); / 失败!pmt2 指的是一个基类对象,无法转换为子类, pmtca2 = NULL .用户可以根据返回值来决定下一步的走向.使用dynamic_cast可以安全地进行向
33、下转换.避免使用显示的向下转换出现的问题.(见前文显示的向下转换),46,8.2.3 cross cast :MT *pmt= new MTCA; CA *pca=(CA*)pmt; /交叉显示转换可以成功,但是.pca-WhoAmI (); / I am a MTCApca-TakePhoto (); / MTCA is calling ! /不是MTCA is takephoto! /这里pca的虚表指针所指向的虚表的可见部分是MT的虚表 pca=dynamic_cast(pmt);pca-WhoAmI (); / I am a MTCApca-TakePhoto (); / MTCA i
34、s takephoto! 转换成功 / pca的虚表指针所指向的虚表的可见部分是CA的虚表! 见前图p34,47,8.3 静态转换,语法: static_cast ( expression )静态转换.功能类似于显示的类型转换.不能提供安全性.8.3.1. Upcast 总是成功.同显示转换.8.3.2 DownCast:MT *pmt1=new MT; MT *pmt2= new MTCA pmtca;/向上隐式转换 pmtca1=dynamic_cast(pmt1);/pmtca1=NULL,应用可借此判断pmt1不是指向的MTCA对象, 不应继续使用pmtca1.pmtca2=dynam
35、ic_cast(pmt2);/pmtca2!=NULL,pmtca2成功地指向一个MTCA对象,可以使用!pmtca1=static_cast(pmt1); pmtca1-TakePhoto (); /同显示转换,虽通过编译,但会引发运行时错误. pmtca2=static_cast(pmt2); /在这种情况下与dynamic_cast,显示转换等价.pmtca2-TakePhoto ();,48,8.3.3 交叉转换: class a; class b; a* pa=new a; b* pb=(b*) pa; /编译通过,但是之后的调用会引起运行时错误. pb=static_cast(pa
36、); /编译时出错, /static_cast不支持交叉转换,避免了运行时出错.这种特性也是static_cast的安全性较显示转换较高的原因,是静态转换存在的价值所在.(产生编译期的错误,而不是运行期不可预见的错误.) static_cast的实现方式是在对象与子对象之间进行偏移计算而得出. 而dynamic_cast,有时会改变虚表指针所指向虚表的可见部分.安全性对比: 显示 静态 动态 递增. 隐式转换总是安全的,但是功能有限,只能进行向上转换.,49,9 动态链接库,动态链接库(Dynamic Link Library DLL).是一个可执行程序.它包含一些库函数、变量、或者是一些资源
37、(对话框、图标等等)。自己不能单独运行,必须依附在其他的可执行程序中运行。运行时期动态地加载到其他进程的地址空间中,而不是在编译时刻链接到应用程序中,这是它的名字的由来(相对于静态链接库)。 DLL可以向外引出(export)变量或函数。1。静态库 (.lib) 或.o2。动态库 (.dll) 或.so简化了项目的管理。节省内存。(访问同一个DLL的进程代码页面共享)资源共享。(图标等资源)多种语言编程,50,例子:在VS 2003中新建一个DLL工程mangle,选择输出符号。 extern “C” _declspec(dllexport) int myfun(); /头文件 _decls
38、pec(dllexport) int fnmangle(); int myfun()return 10; /实现文件中 int fnmangle() return 100; 关键字_declspec(dllimport) 指示编译器这个函数将对外输出。也可以使用DEF文件的方式列出输出的函数和变量。LIBRARYmangle EXPORTS myfun 1 语法见msdn文档。可以使用dumpbin.exe工具查看一个dll的输出符号。(在IDE中工具外部工具添加 vc7 bin 目录下找到dumpbin.exe 选择使用输出窗口和提示输入参数。)Dumpbin 输入参数 目标路径 $(Tar
39、getPath) 选项 /exports结果为: 1 0 0001126C ?0CmangleQAEXZ 2 1 00011212 ?4CmangleQAEAAV0ABV0Z 3 2 00011082 ?fnmangleYAHXZ 4 3 00036B40 ?nmangle3HA 5 4 000112C1 myfun,51,动态库的链接 1.静态地、隐式地链接 可执行文件中链接一个引入库(.lib),加载可执行文件的同时加载了dll在CBuilder 下的使用方法: 1。新建一个项目bcbcli,加入mangle.h头文件 2。使用COFF2OMF.exe对mangle.lib进行转换 COF
40、F2OMF mangle.lib mangle2.lib 在项目中加入mangle2.lib 3。include “mangle.h” int res=myfun(); 4.编译 5。把mangle.dll放在应用程序能找到的路径下。,52,2. 动态地、显示地链接 程序执行loadlibrary加载dll 1。 新建一个项目bcbcli,加入mangle.h头文件 2。include “mangle.h” 3。typedef int ( * MYFUN)(void); MYFUN myf; HANDLE h=LoadLibrary(mangle.dll); myf=(MYFUN)GetProcAddress(h,myfun); int res=(*myf)(); /或res=myf(); 4。编译执行。 这种方法更加灵活,效率更高,只是编码稍稍复杂一点。,