二. c/c++ 预备知识

59
1 二. C/C ++ 二二二二 1. 二二二二二二二 2. 二二二二二 3. 二二 二二二二二 4. 二二 二二二二二二 1. 二二二二二二二二二二二 2. 二二二二 5. 二二 二二二 1. 二二二二二 2. 二二二二二二二 3. object slice 6. 二二二二 1. 二二二二二二二 2. 二二二 7. RTTI 8. 二二二二 1. 二二二二 ( 二二二二 ) 2. 二二二二 3. 二二二二 9. 二二二二二 10. 二二二二 11. 二二二二 12. 二二二 13. 二二二二 . 14. 二二二二二

Upload: nuri

Post on 17-Mar-2016

79 views

Category:

Documents


2 download

DESCRIPTION

二. C/C++ 预备知识. 类型转换 显示转换(强行转换) 动态转换 静态转换 动态链接库 名字改编 函数模板 类模板 友元函数. 操作符重载. 变量的存储类型 函数参数的传递 封装 类及其成员 继承 基类与派生类 派生类对象访问基类成员 一致性难题 多态 虚函数 一致性与功能 虚函数实现机制 object slice 多重继承 多重继承的虚表 二义性 RTTI. 1 变量的存储类型. - PowerPoint PPT Presentation

TRANSCRIPT

Page 1: 二.   C/C++  预备知识

1

二 . C/C ++ 预备知识1. 变量的存储类型2. 函数参数的传递3. 封装 类及其成员4. 继承 基类与派生类

1. 派生类对象访问基类成员2. 一致性难题

5. 多态 虚函数1. 一致性与功能2. 虚函数实现机制3. object slice

6. 多重继承1. 多重继承的虚表2. 二义性

7. RTTI

8. 类型转换1. 显示转换 ( 强行转换 )2. 动态转换3. 静态转换

9. 动态链接库10. 名字改编11. 函数模板12. 类模板13. 友元函数 .14. 操作符重载

Page 2: 二.   C/C++  预备知识

2

1 变量的存储类型1. 全局变量 : 在任何函数之外定义 , 可以被任何代码所访问 . 在其他的文件中使用这些变量时 , 使用 extern 来声明之 , 使得编译器了解这些变量的类型和名字 , 但是不重新分配内存 .2. Static 全局变量 : 只在文件范围内可见 . ( 使得开发者可以任意地使用变量名字 , 不必担心重名 .)3. 自动变量 : 函数体内部 . 在 stack 中4. Static 局部变量 : 在函数体内部定义 , 编译器为之生成永久存储单元 , 在调用之前就存在 . 只在函数内部可见 .5. 动态分配变量 : new delete 在 heap 中 .6. 类的 static 成员变量 : 只与类有关 . 属于类而不属于对象的变量 .

( 静态成员函数 , 没有 this 指针 , 其中只能访问静态成员变量 .)以下例子见 virtualfun 工程

Page 3: 二.   C/C++  预备知识

3

2 函数参数的传递 引用运算符 & 用来定义一个引用 .int num;int &refn=num; refn 是 num 的别名 . 此语句并没有在内存中创建一个新的变量 refn, 它只不过通知编译器 num 有了一个新的名字叫

refn. Refn 和 num 是同一个变量 . 1. 传值void swap1(int x,int y){ int temp; temp=x; x=y; y=temp;}; C++ 中缺省的参数初始化方法是把实参的值拷贝到参数的存储区中 . 形参的值可以改变 , 但此函数并不能改变实参的值 .

Page 4: 二.   C/C++  预备知识

4

2 void swap2(int *px,int *py) { int temp;

temp=*px; *px=*py; *py=temp;

};1. 同上 , 函数不能改变实参的值 ( 即地址值 ), 在这种意义下 , 传地址和传值是等价的 .2. 虽然不能改变实参的的值 , 但是改变了实参 ( 即地址 ) 所存储的值 . 3. 如果要改变一个指针的值 , 那么就要使用双重指针 . 3 void swap3(int &x,int &y) { int temp;

temp=x; x=y; y=temp;

} 与前两个不同 , 形参是实参的别名 . 实参形参是同一个变量 .

Page 5: 二.   C/C++  预备知识

5

调用方法 : 1.int a=2, b=5; swap1(a,b); cout<<a<<b; //2 5 2. int a=2,b=5; swap2(&a,&b); cout<<a<<b; //5 2 3.int a=2;b=5; swap3(a,b); cout<<a<<b; //5 2 若希望通过一个函数改变一个指针 , 那么可以使用指针的引用或者更常用地使用双重指针作为函数的参数 .

Page 6: 二.   C/C++  预备知识

6

3 封装 类及其成员 类。 mobile telephone 手机class mt{private:

int price; public:

void work();void setprice(int initp) {price=inip;};void getprice(int *oldp) { *oldp=price;};

// 数据成员是私有的,客户只能通过公有的方法 , 在方法内部来访问。mt(void);~mt(void);

};

Page 7: 二.   C/C++  预备知识

7

C++ 编译器对于数据成员和函数成员的处理 :1. 数据成员 :

1. 静态的 : 定义了类 , 就会给静态数据分配内存 .2. 非静态的 : new 了实例 , 每个实例都为之分配 .

2. 函数成员 :1. 定义了类 , 实现了该函数后 , 就有该函数的入口指针 . 对于新

new 的实例并不产生新的指针 . 都是使用同一个函数 . 因此存在者重入性和并发性的安全问题 , 如果函数体里使用了静态的变量 ( 函数级的 ) 的话 . 2. 如果是非静态的成员函数 , 则每一个实例调用它时 , 会传入一个指向实例本身的指针 this. 实例借此可以访问其他的成员

( 变量或函数 )3. 如果是静态的成员函数 , 则在调用时无 this 指针 , 因此不能访问类的数据成员 .4. 如果是虚函数 , 则会为此类建立一个虚表 , 每个表项是一个指针 , 指向此函数的实现 . 这是额外的开销 . 见后文 .

Page 8: 二.   C/C++  预备知识

8

4 继承 基类与派生类 4.1 派生类对象访问基类成员 共有的性质提升到基类中。class EE // 电子设备{protected:

int price; // 保护的数据成员可以被派生类的成员函数访问public:

void Work();void SetPrice(int initp);void GetPrice(int *oldp);void NVWhoAmI() {cout<<“I am a EE”<<endl;};

// 我是谁 ? 前缀 NV 表示非虚的意思 .EE(void);~EE(void);

};派生类继承了基类公共的和保护的成员 :

Page 9: 二.   C/C++  预备知识

9

class MT : public EE{protected:

int rent; // 月租费 . 派生类可以有自己的数据成员和方法。 price无需再进行申明 .public:

void Call();void MTSetPrice(int price);void MTGetPrice(int *oldp);// 派生类成员函数可以访问基类的保护成员,这里故意加上前缀“MT” ,已示区别,其实没有必要。MT(void);~MT(void);

};

基类公有成员的访问方法 :

Page 10: 二.   C/C++  预备知识

10

通过派生类可以直接访问基类的公有的方法和数据。 MT *pmt=new MT;

pmt->NVWhoAmI (); // 结果为: I am a EE pmt - >pbvar=……; //也可以直接访问基类的公有的数据成员:假设 pbvar 是 EE 的公共数据成员 基类保护成员的访问方法 : 对于基类的保护的数据成员或方法则只能在派生类的成员内部访问 ,比如 : void MT::MTSetPrice (int price) {EE::price =price;} // 派生类成员函数访问基类的保护数据成员客户使用方法: MT *pmt=new MT; pmt->MTSetPrice(5); // 客户调用派生类的成员函数 , 函数内部访问基类 的保护数据成员 . pmt->price=5; // 出错 . 不能直接访问 .

Page 11: 二.   C/C++  预备知识

11

4.2 一致性难题单单使用普通的成员函数有力不从心之处 :这是另一个派生类:class CA : public EE //照相机{ protected: int pixel; // 象素public:

void TakePhoto(int *pix);CA(void);~CA(void);

}; 对于以下代码 :

MT *pmt=new MT;CA *pca=new CA;

pmt->NVWhoAmI (); // 派生类访问基类的公有的方法。pca->NVWhoAmI (); //另一个派生类访问基类的公有的方法。

Page 12: 二.   C/C++  预备知识

12

结果为: I am a EE I am a EE 结果当然是一样的 . 但是我们期望能得到更精细的信息 , 为此在派生类中覆盖此函数class MT : public EE{ protected: int rent;public:

void Call();void NVWhoAmI(); //签名虽然完全相同,但它是派生类定义的成员 , 或者理解成为碰巧完全相同。当然我们可以再加上一个前缀MT, 但是会显得太罗嗦 , C++支持重载 . 虽然这种方式不好 , 但是是合法的 .

其实现如下 : void MTSetPrice(int price); void MTGetPrice(int *oldp);

MT(void);~MT(void);

};

Page 13: 二.   C/C++  预备知识

13

void MT::NVWhoAmI (){ cout<<"I am a MT"<<endl;}类似地 : void CA::NVWhoAmI (){ cout<<"I am a CA"<<endl;} 于是我们可以分别出不同的类对象: MT *pmt=new MT; CA *pca=new CA;

pmt->NVWhoAmI (); // I am a MT pca->NVWhoAmI (); // I am a CA 比刚才有进步 .

但是 , 为了追求统一性 , 我们往往把基类指针指向子类 ( 派生类 )的对象 . 在直观上当然是合理的 . (彩屏手机也是手机啊 !). 子类对象比基类对象更“大” , 子类对象中含有一个基类的子对象 “ subobject”. 基类的指针将指向这个子对象 . ( 不包括子类定义的成员 ).

对于以下代码:

Page 14: 二.   C/C++  预备知识

14

EE *pee[2];pee[0]=new MT; // 基类指针指向子类的对象pee[1]=new CA;for(int i=0;i<2;i++) pee[i]->NVWhoAmI (); // 基类指针只能调用基类定义的成员

结果为: I am a EE I am a EE 我们用一个基类的指针指向派生类的对象时(MT当然是一个

EE , CA也是一个 EE ),由基类指针来调用 NVWhoAmI(我们蒙上眼睛,双手各拿着一个 EE(电器设备),它到底是什么?手机还是照相机?) 结果是令人失望的,它们都说:我是一个 EE 。Dilemma:1. 要么睁大眼睛,分别使用不同的派生类指针来询问其身份 ( 如果有 1

0 个不同的派生类 , 我们要写 10 句雷同的 p*->NVWhoAmI() ) ;2. 要么虽然很优雅 , 使用基类指针通过一个循环来调用 . 但只能得到一个笼统的回答。

Page 15: 二.   C/C++  预备知识

15

5 多态 虚函数5.1 一致性与功能 我们对于效率 ,美观 , 功能的需求导致我们需要多态 . 虚函数是实现多态的一种方式 .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); };派生类的定义如下 :

Page 16: 二.   C/C++  预备知识

16

class MT :public 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) 此虚函数

Page 17: 二.   C/C++  预备知识

17

对于另一个类 CA 我们也作类似的处理:void CA::WhoAmI (){cout<<" I am a CA"<<endl;}

此时对于以下代码 EE *pee[2];

pee[0]=new MT;pee[1]=new CA;for(int i=0;i<2;i++)

pee[i]->WhoAmI (); // 1. 一致 ,优雅结果为: I am a MT I am a CA // 2. 信息准确 ,丰富 .

Page 18: 二.   C/C++  预备知识

18

5.2 虚函数实现机制 编译器为每个包含虚函数的类产生一个静态函数指针数组,即虚函数表 vtbl ,在这个类或它的基类中定义的每一个虚函数都有一个相应的函数指针 , 该指针指向它的实现。该类的每个实例包含一个不可见的数据成员,即虚函数指针 vptr, 这个指针被构造函数自动初始化,指向类的虚表。当客户调用虚函数的时候,编译器产生代码指向 vptr,索引到虚表中,找到函数指针发出调用。 此虚表在类有了定义以后就由编译器分配了 . 它是与类 ( 而不是对象 )相关的静态的指针数组 . 类实例化为对象后 , 对象的虚表指针将指向此虚表 . 对象的虚表指针往往放在其他数据成员的前面 . 对象的 this 指针将指向此虚表指针 . 虚函数的额外开销 :

1. 类 : 包含此虚函数的类 ( 定义的或继承来的 ) 虚表 ( 一个函数一个表项 4 字节 )2. 对象 : 每生成一个对象 , 有一个虚表指针 4 字节 .

纯虚函数 : 如果是纯虚函数 ( 只是在自己的派生类中实现的 ), 不论是自己定义的还是继承自它的某个基类 (直接或间接地 ), 那么 , 这一项地址为空 ; 如果在自己或自己以前已经实现 , 那么将指向这个函数的实现地址 ( 二进制代码的存储地址 ). 由此 , 一个包含纯虚函数的类是不能实例化的 .否则 , 它调用此函数该怎么办 ?

Page 19: 二.   C/C++  预备知识

19

虚表与虚表指针的示例 :

EE 对虚函数的实现WhoAmI

EE’s other vf1已实现EE’s other vf2纯虚 未实现 为空

WhoAmI

vf1

EE 的虚表

MT 对虚函数的实现WhoAmI 被覆盖EE’s vf1 未覆盖 仍然使用基类的

EE’s vf2在此实现

WhoAmI

vf2

MT 的虚表

MT’s own vg自己实现 vg

vptrpricerent

this

EE 类由于有纯虚函数 .不能实例化

MT 类

MT 类的实例对象

Page 20: 二.   C/C++  预备知识

20

5.3 object slice 当基类指针指向派生类对象时 , 通过基类指针调用虚函数时 , 编译器会找到其所指向的实际对象对虚函数的实现 . 通过基类指针或引用间接指向派生类的对象时 , 多态性才起作用 .使用基类对象并不能保留住派生类的类型身份 . MT mt;

EE ee;ee=mt; //object slice 非正常EE *pee=&mt;//正常EE &ree=mt; //正常ee.WhoAmI(); // I am a EEpee->WhoAmI (); // I am a MTree.WhoAmI (); // I am a MT

ee=mt 进行了一次 object slice. Mt 被 slice 只剩下一半 , 而且 WhoAmI 函数还要使用 mt 的 this 指针 ,情况会如何 ? 实际上编译器自动为 EE 生成了一个拷贝构造函数 . 这时 ee 是一个完全的 EE 对象 . 它调用 WhoAmI 时将使用自己的实现 . 我们要避免使用 object slice以上例子见 virtualfun 工程

Page 21: 二.   C/C++  预备知识

21

6 多重继承 6.1 多重继承的虚表 以下例子见 multiinh 工程 . 把以上三个类整理如下 :// 1. 电器class EE {protected: int price;public: virtual void Work(){cout<<"EE is working"<<endl;};

virtual void WhoAmI(){cout<<" I am a EE"<<endl;}; EE(void); ~EE(void); };//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); };

Page 22: 二.   C/C++  预备知识

22

//3. 照相机class CA : public EE {protected: int pixel; public: void Work(){cout<<“CA is working”<<endl;};// 这里 Work也改写了 , 作为对比 ,MT 没有改写它 . void WhoAmI(){cout<<“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(){cout<<"I am a MTCA"<<endl;}; // 继承自 EE. 改写 .

virtual void Call(){cout<<" MTCA is calling"<<endl;}; // 继承自 MT 改写

virtual void TakePhoto(){cout<<"MTCA is TakePhoto"<<endl;}; // 继承自 CA 改写

MTCA(void); ~MTCA(void); }; 其虚表及其虚表指针示意图如下 :

Page 23: 二.   C/C++  预备知识

23多重继承 的派生类的虚表

EE 对虚函数的实现

WhoAmI

Work

WhoAmI

Work

EE 的虚表

MT 对虚函数的实现

WhoAmI 被覆盖Call

WhoAmI

Call

MT 的虚表Work

CA 对虚函数的实现

WhoAmI 被覆盖TakePhoto

WhoAmI

TakePhoto

CA 的虚表Work 被覆盖

WhoAmI 被覆盖Call 被改写MTCA 的虚表

Work

Work

WhoAmI 被覆盖TakePhoto 被改写

Work

MTCA 对虚函数的实现WhoAmI

注意两个 Work 的指向不一样 , 两个WhoAmI 的指向一样 !

Call

TakePhoto

继承自 MT

继承自 CA

Page 24: 二.   C/C++  预备知识

24 虚表指针

WhoAmI 被覆盖Call 被改写

MTCA 的虚表

Work

WhoAmI 被覆盖TakePhoto 被改写

Work

继承自 MT

继承自 CA

vptr2

price

vptr1

pixel

rent

this

MTCA 类MTCA 实例对象

price

Page 25: 二.   C/C++  预备知识

25

6.2 二义性1. 成员函数二义性 :MTCA *pmtca=new MTCA;

pmtca->WhoAmI (); // I am a MTCApmtca->Work(); // MTCA 有两个 Work, 二义性 , 调用不明确 , 编译出错pmtca->CA::Work(); // CA is workingpmtca->MT::Work(); // EE is working

MTCA 对 WhoAmI 进行了覆盖 , 通过 MTCA 指针调用 WhoAmI将指向自己的实现 . MTCA 没有对 Work 进行覆盖 , 其虚表中有两项 Work, 分别指向 MT 和 CA 对其的实现 . 因此 pmtca->Work(). 无法通过编译 . 因为编译器不知道要调用哪一个 . pmtca->CA::Work(); 将调用 CA 的实现 , 而 CA覆盖了它 ; pmtca->MT::Work(); 将调用 MT的实现 , 而 MT 没有覆盖它 ,直接使用 EE 的实现 .

MTCA 对象中含有一个 MT 子对象和一个 CA 子对象 , 这两个子对象分别含有一个EE 子对象 :( 如图 ). 通过这两个 EE 子对象调用 Work结果是不同的 .

EE 子对象 1

MT 子对象EE 子对象 2

MTCA 对象CA 子对象

Page 26: 二.   C/C++  预备知识

26

2. 转换二义性 : EE *pee[3];

MT *pmt=pmtca;CA *pca=pmtca;pmt->Work (); // EE is workingpca->Work (); // CA is workingpee[0]=(EE*)(MT*)pmtca; // 转换途径 1pee[1]=(EE*)(CA*)pmtca; // 转换途径 2

pee[2]=(EE*)pmtca; // 存在二义性 , 转换不明确 , 编译出错for(int i=0;i<2;i++) pee[i]->Work ();

// EE is working, CA is working 多态 .仍由虚函数引起 .

这种二义性正是多重继承遭到诸多非议的原因 . 使用虚继承可以消除这种二义性 .

Page 27: 二.   C/C++  预备知识

27

7 RTTI 为了一致性 , 我们往往把基类指针指向子类对象 . 面对一个基类指针 , 在行动之前 , 要准确地探明它指向的是什么 . 即 RTTI. 我们定义的 WhoAmI 就是一个简单的 RTTI. 运算符 typeid 是一个重载的运算符,它可以有一种以上的形式,它的参数可以是类名称或对象的指针或对象变量,它能在运行期判断变量的类型。 运算符的返回值是一个 type_info 的引用类型。 type_info 是一个类,定义在 typeinfo.h 中 :class type_info { public: virtual ~type_info(); int operator==(const type_info& rhs) const; int operator!=(const type_info& rhs) const; int before(const type_info& rhs) const; const char* name() const; const char* raw_name() const; private: ... }; 类 type_info 重载了 == 和 !=,用以对两个类型进行比较操作 ,它的成员函数 name 可以输出类型的字符串型的名字 .

Page 28: 二.   C/C++  预备知识

28

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 指向的却是 MTcout << typeid( pm ).name() << endl; // class MT cout << typeid( *pm ).name() << endl; // class MT delete pd; }

Page 29: 二.   C/C++  预备知识

29

用法二 :void DoYourWork(EE * pe){ if (typeid(MT)==typeid(*pe)) (MT*)pe->Call(); if (typeid(CA)==typeid(*pe)) (CA*)pe->TakePhoto();} // 在准确地了解了对象的类型后 , 就可以正确地转换 ,从而进行正确的调用 .void main() {EE * pe1=new MT;EE * pe2=new CA; DoYourWork(pe1); DoYourWork(pe2);}

Page 30: 二.   C/C++  预备知识

30

8 类型转换 8.1 显示转换 ( 强行转换 ): 8.1.1 向上隐式转换总可以成功 ( 等价于显示转换 ). 向上的转换是为了得到统一性 ,一致性 .MTCA *pmtca=new MTCA;MT *pmt=pmtca; // 等价于 MT *pmt=(MT*)pmtca;转换以后的效果如下图所示 :( 蓝色加亮部分 )

WhoAmI 被覆盖Call 被改写

MTCA 的虚表

Work

WhoAmI 被覆盖TakePhoto 被改写

Work

继承自 MT

继承自 CA

vptr2

price

vptr1

price

rent

pmtca

MTCA 类MTCA 实例对象

pmt

pixel

Page 31: 二.   C/C++  预备知识

31

8.1.2 向下的隐式转换无法通过编译 , 向下的显示转换虽然可以通过编译 , 但是应用程序无法得到转换的信息 , 而且有可能发生运行时错误( 实际上 , 显示转换表面上总是可以成功 ,哪怕根本不相关的转换 . 但是有可能是假象 ,从而造成运行时错误 ): 向下的转换是为了调用子类定义的成员 .使用基类指针是不能调用的 .

MT *pmt1=new MT; MTCA *perr=pmt1;// 向下的隐式转换出错MTCA *pmtca1,*pmtca2;pmtca1=(MTCA*)pmt1;// 向下显示转换可以成功 pmt1 指向的是一个MT

pmtca2=(MTCA*)pmt; // pmt 指向的是一个 MTCA. 见前页 .pmtca1->WhoAmI ();// I am a MT pmtca2->WhoAmI ();// I am a MTCA 用户只有通过“ RTTI”才能分清 .

pmtca1->TakePhoto (); // 更为危险的是 , 此调用虽然通过编译 ,将引发运行时错误 ! 因为 pmtca1根本就指向的是 MT, 没有 TakePhoto 方法 !

pmtca2->TakePhoto ();// 这次 ,碰巧成功了 . 但是用户完全无法控制 . 向下转换如下图所示 :

Page 32: 二.   C/C++  预备知识

32

向下的显示的转换 , 从 pmt->pmtca. 即 (pmtca=(MTCA*)pmt; 用户无法从转换结果中判断 pmt最初指向的是一个完整的子类对象(白色加蓝色 ), 还是一个基类对象 (蓝色 ).

当 pmt 是一个完整的对象时 (蓝色 +白色 ). 转换是真正成功 . 否则 , 是一个假象 . 无论哪种情况 , 客户都将得到一个指针 pmtca. 除非使用 RTTI, 客户莫辨真伪 . 当 pmt 是纯蓝时 , 使用 pmtca 调用白色区域 .产生运行期错误 .

WhoAmI 被覆盖Call 被改写

MTCA 的虚表

Work

WhoAmI 被覆盖TakePhoto 被改写

Work

继承自 MT

继承自 CA

vptr2

price

vptr1

pixel

rent

pmtca

MTCA 类MTCA 实例对象

pmt

price

Page 33: 二.   C/C++  预备知识

33

8. 1.3 交叉转换交叉转换是为了执行目标类对象所能执行的功能 .MT *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.)交叉转换如下图所示 :理想的转换结果希望得到绿色 . 实际上这正是下文介绍的动态转换 .

Page 34: 二.   C/C++  预备知识

34

而实际上显示的 cross cast 的结果 , 见棕色部分 .尤其注意TakePhoto 函数和 Pixel 变量 , 在它们的内存位置实际上保存的是Call 的函数指针和 rent 变量 .

WhoAmI 被覆盖Call 被改写

MTCA 的虚表

Work

WhoAmI 被覆盖TakePhoto 被改写

Work

继承自 MT

继承自 CA

vptr2vptr1

price

rent

pmtca

pmt

pca

pixel

price

pca

WhoAmI 被覆盖TakePhoto-> Call

MTCA 的虚表

Work

WhoAmI 被覆盖TakePhoto 被改写

Work

继承自 MT

继承自 CA

vptr2

price

vptr2

price

piexel-> rent

pmtca

pmt

pixel

Page 35: 二.   C/C++  预备知识

35

使用显式的交叉转换并不安全 . 虚表中的可见部分不能被改变 . Pca 所指向的对象的虚表仍然是原来的虚表 .( 即 MT 类的 ) 编译器根据名字 TakePhoto 在虚表中找到第三项 , 但是其中的指针指向的是 MT 类的 Call, 其实现是 MTCA 类的对它的实现 MTCA is takephoto.

所以声明虚函数成员的时候 , 其次序也很重要 ! 如果在 MT 中再声明一个虚函数 virtual void dd(){cout<<“dfdfdfdfd”<<endl;}; 并且排在 Call 函数之前 , 那么 : 以上 pca->TakePhoto (); 的结果将为 : dfdfdfdfd 这种特性 , 有时也有其独特的用途 .( 见 COM 聚合模型 )

Page 36: 二.   C/C++  预备知识

36

8.2. 动态转换运算符语法 : dynamic_cast < type-id > ( expression ) type-id 是事先定义的类的指针或引用类型 .Expression 是一个指针或一个左值 .这里讨论 type-id 是一个指针的情形。8.2.1 Upcast :void f(MTCA* pmc) { MT* pm = dynamic_cast<MT*>(pmc); // 转换到基类 pm 指向 pmc 的 MT 型子对象EE* pe = dynamic_cast<EE*>(pm); // 转换到基类 pe 指向 pm 的 EE 型子对象 }

实际上 upcast 不需要使用 dynamic_cast ,直接使用隐式转换也可。

Page 37: 二.   C/C++  预备知识

37

8.2.2 Downcast :void f(){MT* pmt1 = new MTCA; // 基类指针指向一个派生类对象MT* pmt2 = new MT; // 基类指针指向一个基类对象 MTCA* pmtca1 = dynamic_cast<MTCA*>(pmt1); // 成功!

pmt1 本来指的就是一个派生类对象 MT* pmtca2 = dynamic_cast<MTCA*>(pmt2); // 失败! pmt2 指的是一个基类对象,无法转换为子类,

pmtca2 == NULL ...}用户可以根据返回值来决定下一步的走向 . 使用 dynamic_cast 可以安全地进行向下转换 .避免使用显示的向下转换出现的问题 .( 见前文显示的向下转换 )

Page 38: 二.   C/C++  预备知识

38

8.2.3 交叉转换 :MT *pmt= new MTCA; CA *pca=pmt; //交叉隐式转换出错CA *pca;pca=(CA*)pmt; //交叉显示转换可以成功 , 但是 ......pca->WhoAmI (); // I am a MTCApca->TakePhoto (); // MTCA is calling !!!! // 不是 MTCA is takephoto!!!!

// 这里 pca 的虚表指针所指向的虚表的可见部分是 MT 的虚表 pca=dynamic_cast<CA*>(pmt);

pca->WhoAmI (); // I am a MTCApca->TakePhoto (); // MTCA is takephoto!!!! 转换成功

pca 的虚表指针所指向的虚表的可见部分是 CA 的虚表 ! 见前图 p34

Page 39: 二.   C/C++  预备知识

39

8.3 静态转换语法 : static_cast < type-id > ( expression )静态转换 . 功能类似于显示的类型转换 . 不能提供安全性 .8.3.1. Upcast 总是成功 . 同显示转换 .8.3.2 DownCast: MTCA *pmtca=new MTCA;

MT *pmt=pmtca;// 向上隐式转换 MT *pmt1=new MT; pmtca1=dynamic_cast<MTCA*>(pmt1);//pmtca1==NULL,应用可借此判断 pmt1 不是指向的 MTCA 对象 ,应用不应继续使用

pmtca1.pmtca2=dynamic_cast<MTCA*>(pmt);//pmtca2!=NULL,pmtca2成功地指向一个 MTCA 对象pmtca1=static_cast<MTCA*>(pmt1); pmtca1->TakePhoto (); // 同显示转换 , 虽通过编译 , 但会引发运行时错误 .

pmtca2=static_cast<MTCA*>(pmt); // 在这种情况下与 dynamic_cast 等价 .

pmtca2->TakePhoto ();

Page 40: 二.   C/C++  预备知识

40

8.3.3 交叉转换 : MTCA *pmtca=new MTCA;

MT *pmt=pmtca;CA *pca;pca=static_cast<CA*>(pmt); // 编译出错 , static_cast 不支持交叉转换 . pmt 是 MT* 型的 , 与 CA* 不兼容 . 这种特性也是 static_cast 的安全性较显示转换较高的原因 , 是静态转换存在的价值所在 .(产生编译期的错误 , 而不是运行期不可预见的错误 .)

class a; class b; a* pa=new a; b* pb=(b*) pa; // 编译通过 , 但是之后的调用会引起运行时错误 . pb=static_cast<b*>(pa); // 编译时出错 ,避免了运行时出错 . static_cast 的实现方式是在对象与子对象之间进行偏移计算而得出 . 而 dynamic_cast, 有时会改变虚表指针所指向虚表的可见部分 . 安全性对比 : 显示 静态 动态 递增 . 隐式转换总是安全的 , 但是功能有限 , 只能进行向上转换 .

Page 41: 二.   C/C++  预备知识

41

9 动态链接库 动态链接库 (Dynamic Link Library DLL). 是一个可执行程序 . 它包含一些库函数、变量、或者是一些资源(对话框、图标等等)。自己不能单独运行,必须依附在其他的可执行程序中运行。运行时期动态地加载到其他进程的地址空间中,而不是在编译时刻链接到应用程序中,这是它的名字的由来(相对于静态链接库)。 DLL 可以向外引出( export)变量或函数。1 。静态库 ( .lib) 或 .o2 。动态库 ( .dll) 或 .so简化了项目的管理。 节省内存。(访问同一个 DLL 的进程代码页面共享) 资源共享。(图标等资源) 多种语言编程

Page 42: 二.   C/C++  预备知识

42

例子:在 VS studio.net 2003 中新建一个 DLL 工程 mangle ,选择输出符号。 extern “C” __declspec(dllexport) int myfun(); //头文件 __declspec(dllexport) int fnmangle(); int myfun(){return 10;} // 实现文件中 int fnmangle() {return 100;} 关键字 __declspec(dllimport) 指示编译器这个函数将对外输出。也可以使

用 DEF 文件的方式列出输出的函数和变量。LIBRARY mangle EXPORTS myfun @1 语法见 msdn 文档。可以使用 dumpbin.exe 工具查看一个 dll 的输出符号。(在 IDE 中工具-》外部工具-》添加-》 vs.net vc7 bin 目录下找到

dumpbin.exe 选择使用输出窗口和提示输入参数。)Dumpbin 输入参数 目标路径 $(TargetPath) 选项 /exports结果为: 1 0 0001126C ??0Cmangle@@QAE@XZ 2 1 00011212 ??4Cmangle@@QAEAAV0@ABV0@@Z 3 2 00011082 ?fnmangle@@YAHXZ 4 3 00036B40 ?nmangle@@3HA 5 4 000112C1 myfun

Page 43: 二.   C/C++  预备知识

43

动态库的链接 1. 静态地、隐式地 可执行文件中链接一个引入库( .lib ),加载可执行文件的同时加载了 dll在 C ++ Builder 下的使用方法: 1 。新建一个项目 bcbcli ,加入 mangle.h头文件 2 。使用 COFF2OMF.exe 对 mangle.lib 进行转换 COFF2OMF mangle.lib mangle2.lib 在项目中加入 mangle2.lib 3 。# include “mangle.h” int res=myfun(); 4. 编译 5 。把 mangle.dll放在应用程序能找到的路径下。

Page 44: 二.   C/C++  预备知识

44

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 。编译执行。 这种方法更加灵活,效率更高,只是编码稍稍复杂一点。

Page 45: 二.   C/C++  预备知识

45

10 名字改编( name mangling )

但是对于未使用 extern”C” 保护的函数,比如 fnmangle, 在 C ++ Builder下,无法顺利地链接。这是因为编译器对函数进行了名字改编( name mangling )。

在 C ++中,为了实现函数重载,以区分名字相同但参数不同的函数,编译器在函数的名字尾部添加了一段文字,这段文字记录了函数的参数类型、参数个数等方面的信息。这样对于重载函数的调用,编译器就能方便地找到应执行的重载函数了。使用 dumpbin 或者 impdef ( BCB 提供的工具,输入为 dll输出为 def)我们可以看到 __declspec(dllimport) int myfun() 被改编后的名字为“ ?

fnmangle@@YAHXZ“ 。如前一小节的例子所示 . 然而,不同的编译器厂商对函数名字进行重载的方案是不兼容的。 比如同样的这个函数在 bc环境下被改编为 : @fnmangle$qv ( 使用 bc 建一个 DLL 工程 ,输出同样的函数 , 使用 dumpbin查看 ) A 编译器改编后的名字不能被 B 编译器所识别。因此对于经过名字改编后的函数,(在以上例子中),在 VC环境下可以顺利调用,但是在 C

++ Builder环境下则遇到了困难。 为了使得 vc 编译的 dll 能被 bc 使用 , 可以使用别名 . 在 def 文件中加上 : @fnmangle$qv=?fnmangle@@YAHXZ

Page 46: 二.   C/C++  预备知识

46

我们可以使用 extern”C” 通知编译器不要对某函数进行名字改编。以便能在 C环境下顺利使用( C 不支持函数重载)。 但是类的成员函数都是经过改编了的。即使使用 extern”C” 也无法防止这种改编操作。

Page 47: 二.   C/C++  预备知识

47

11 函数模板 虚函数:运行时多态性。模板:编译时多态性,或参数式多态性。 函数模板 我们可以分别就求绝对值写两个函数,对应 int 型和 double 型的参数 int Abs(int N) { return N<0? -N:N;}; double Abs(double N) { return N<0? -N:N;}; 但是更方便地可以写一个函数模板: template<class T> T Abs(T N) { return N<0? -N:N;}; 当给函数模板传递参数时,编译器能根据不同的参数类型生成不同的函数: 比如: cout<<"absolute value of -5 is "<< Abs(-5); 编译器会生成 T 为 int 的函数即 int Abs(int N) { return N<0? -N:N;}; 而针对 cout<<"absolute value of -1.2 is "<< Abs(-1.2); 编译器会生成 T 为 double 的函数即 double Abs(double N) { return N<0? -N:N;};

Page 48: 二.   C/C++  预备知识

48

语法: template<class T1,class T2,...> ....f(T1...,T2...,T1...) { ...... }1. 关键字 template < class……>2. class 指任意合法的类型,并不一定是“ class” ,包括系统的和自定义的。3. <...> 内, class 关键字可以出现多次 , 即 T1 , T2…… 但不能重复 .4. 函数的返回值可以是 T 类型,当然也可以不是 .5. 函数的参数列表中,必须出现类型列表中的每一个。每一个参数类型至少在参数类表中出现一次。6. 在函数体中,类型参数可以出现在任何可以出现合法类型的地方。

Page 49: 二.   C/C++  预备知识

49

覆盖模板 模板生成的每个函数版本包含基本相同的代码,只是类型参数不同,但是也可以对特定的参数类型做特殊处理,为此,只要定义与函数模板名同名的普通 C ++函数,用具体类型而不是类型参数。普通函数将覆盖函数模板,即如果传递普通函数所指的参数类型,编译器将调用这个函数而不是根据模板生成函数。 比如:int Abs(int i){ return i;}将覆盖模板函数cout<<"absolute value of -5 is "<< Abs(-5)<<endl;absolute value of -5 is –5,而 int Abs(int i,int j) {return i;} 则无法覆盖它,因为参数个数不一样。

Page 50: 二.   C/C++  预备知识

50

12 类模板 以下是存放 100 个整数的类。class IntList{ public: IntList(); int SetItem(int Index,const int &Item); int GetItem(int Index,int &Item); private: int Buffer[100];};问题是,如果我们现在要存储 200 个整数,或者要存放 100 个

double 型的数。以上类就无能为力了。从以上类中派生新类也无济于事。我们定义如下的类模板:

Page 51: 二.   C/C++  预备知识

51

template<class T,int I> class CList{ public: CList(T intVal) int SetItem(int Index,const T&Item); int GetItem(int Index,T&Item); private: T Buffer[I];};1. T 是类型参数, I 是常量参数。 T 和 I 的值在生成具体类的实例时指定。2. <> 内可以包括任意个类型参数和常量参数。当然也不能重复。3. 类型参数可以是任何有效类型。4. 在类定义中,可以在任何可以使用类型指定的地方使用类型参数,在任何使用类型常量的地方使用常量参数。 模板类的成员函数:

Page 52: 二.   C/C++  预备知识

52

template<class T,int I> int class CList<T,I>::SetItem(int Index,const T &Item)

{ if(Index<0||Index>I-1) return 0;Buffer[Index]=Item;return 1;}

1 。以关键字开头: template<class T,int I> 2 。类名后应接上模板参数名单 CList<T,I>

根据模板类生成对象: 实例化,专门化。CList<int ,100>IntList;IntList 是 CList 的实例。所有的 T 换成 int ,所有的 I 换成 100 。又如 :CList<char*, 150> StringList;

模板类的对象也可以动态生成(放在 heap 中)CList<double, 50> * DoubleList;DoubleList=new CList<double,50>;

也可以给模板类加入构造函数:

Page 53: 二.   C/C++  预备知识

53

template<class T, int I> CList<T,I>::CList(T iniVal){ for (int i=0;i<I,i++) Buffer[i]=iniVal;}有了构造函数,可以在实例化的同时赋初值CList<int ,100> IntList(10);

类型参数可以使用自定义的类型: struct Record { char Name[20]; char Phone[20];}Record rec={“Jack”,”1234567890”};CList<Record,100> RecordList(Rec);

Page 54: 二.   C/C++  预备知识

54

13 友元函数 .

只有成员函数才能访问类的私有成员 . friend 关键字可以使得私有成员被其他非成员函数访问 .class myClass{private: int value; public: void setValue( int v){ value=v;} friend void setV(myClass & ob, int v);friend class yourClass;// 友元类};void setV(myClass & ob, int v){ ob.value=v;} setValue 是成员函数 ,含有隐含的 this 指针 : this->value=v, setV 是非成员函数 , 没有 this 指针 , 它需要一个对象作为其参数 .

ob.value=v 友元函数只是一个普通的函数 , 只不过在类中说明 , 可以访问类的对象的私有成员 . 一个类可以是另一个类的友元 . yourClass 的所有成员函数都是

myClass 的友元函数 .

Page 55: 二.   C/C++  预备知识

55

14 操作符重载c 语言中操作符重载的例子 : *int a=3,b=5;int c=a*b; // 表示乘积int *p=&a; // * 表示 p 是一个指针int i=*p; //* 作用在地址上表示该地址所记录的值 一个运算符可以有多种含义 , 用户在使用时并不需要特别指出使用它的哪一种功能 , 编译器会根据操作符的操作数自动判断对操作符赋予哪一种含义 . 这就是运算符重载 . C++ 编译器把运算符看成一个函数 : c=a*b // 解释为 c=operator*(a,b); i=*p // 解释为 i =operator*(p)operator*(), 看成一个函数 ,称为运算符函数 , 这个函数被重载了 .C++ 中 ,允许程序员对操作符函数进行重载 .操作符函数重载的例子 : ( 一般简称为操作符重载 )

Page 56: 二.   C/C++  预备知识

56

class Complex {public:

double real;// 实部double imag;// 虚部Complex(double r=0,double i=0){real=r;imag=i;}void show(){ cout<<"real= "<<real<<" "<<"imag= "<<imag<<endl;}

}; 重载了运算符函数 operator+, 此函数的参数为两个 Complex, 且在计算过程中不会被改变 , 返回值也是一个 Complex:Complex operator+(const Complex &op1,const Complex &op2){ Complex t; t.real =op1.real +op2.real ; //读取 , 设置了 Complex 的公有的数据成员 t.imag =op1.imag +op2.imag ; return t;}void main(int argc, _TCHAR* argv[]){ Complex ob1(2,3);Complex ob2(0.5,0.6);Complex sum;

sum=ob1+ob2; // 使用运算符函数 sum=operator+(ob1, ob2)sum.show ();}

Page 57: 二.   C/C++  预备知识

57

以上对 + 进行了重载 , 可以进行 Complex 类的运算 , 但它与Complex 并没有直接的联系 , 它只能访问 Complex 的公共成员 . Complex 的两个数据成员都是公共的 , 但是这是不符合 OOP 的封装原则的 , 存在安全隐患 . 一般说来 , Complex 的两个数据成员都应该是私有的 , 但此时上述 + 不能访问它了 . 为此 , 有两种解决方法 :

1. 把 operaor+() 运算符函数作为类的成员函数 , 以使得其可以访问私有的成员 ,称为成员操作符函数 .2. 把 operaor+() 运算符函数作为类的友元函数 ,称为友元操作符函数 .

Page 58: 二.   C/C++  预备知识

58

class Complex{private: //注意这里的数据成员是私有的

double real;// 实部double imag;// 虚部

public:Complex(double r=0,double i=0){real=r;imag=i;}void show(){ cout<<"real= "<<real<<" "<<"imag= "<<imag<<endl;}Complex operator+(const Complex &op);// 成员操作符函数

}; 重载了成员操作符函数 ,注意参数只有一个 !Complex Complex::operator +(const Complex &op){ Complex t; t.real =real +op.real ; // 即 t.real= this->real+op.real t.imag =imag +op.imag ; return t;}void main(int argc, _TCHAR* argv[]){ Complex ob1(2,3);Complex ob2(0.5,0.6);Complex sum;

sum=ob1+ob2; // 使用成员运算符函数 : sum=ob1.operator+(ob2)sum.show ();}

Page 59: 二.   C/C++  预备知识

59

使用友元操作符函数重载 :在类定义中加上 :friend Complex operator +(int i,Complex &op ){Complex t; t.real =op.real +i ; t.imag =op.imag +i ; // 为了展示用法 , 这里的加法对实部和虚部同时加上一个整数 . return t;};Complex ob1(2,3);

sum=3+ob1;sum将为 (5,6).