您的位置:首页 > 编程语言 > C语言/C++

C++学习笔记(6)

2005-10-16 02:41 330 查看
44[/b].谈谈重载(overload[/b])覆盖(override[/b])与隐藏
[/b]

这三个概念都是与OO中的多态有关系的。如果单是区别重载与覆盖这两个概念是比较容易的,但是隐藏这一概念却使问题变得有点复杂了,下面说说它们的区别吧。 重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数。 覆盖(也叫重写)是指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样。 隐藏是指派生类中的函数把基类中相同名字的函数屏蔽掉了。隐藏与另外两个概念表面上看来很像,很难区分,其实他们的关键区别就是在多态的实现上。什么叫多态?简单地说就是一个接口,多种实现吧。 还是引用一下别人的代码来说明问题吧(引用自林锐的《高质量C/C++编程指南》)。仔细看下面的代码:

#include <iostream.h>

class Base

{

public:

virtual void f(float x){ cout << "Base::f(float) " << x << endl; }

void g(float x){ cout << "Base::g(float) " << x << endl; }

void h(float x){ cout << "Base::h(float) " << x << endl; }

};

class Derived : public Base

{

public:

virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }

void g(int x){ cout << "Derived::g(int) " << x << endl; }

void h(float x){ cout << "Derived::h(float) " << x << endl; }

};

看出什么了吗?下面说明一下:(1)函数Derived::f(float)覆盖了Base::f(float)。

(2)函数Derived::g(int)隐藏了Base::g(float),而不是重载。

(3)函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。

嗯,概念大概明白了,但是在实际的编程中,我们会因此遇到什么问题呢?再看下面的代码:void main(void)

{

Derived d;

Base *pb = &d;

Derived *pd = &d;

// Good : behavior depends solely on type of the object

pb->f(3.14f); // Derived::f(float) 3.14

pd->f(3.14f); // Derived::f(float) 3.14

// Bad : behavior depends on type of the pointer

pb->g(3.14f); // Base::g(float) 3.14

pd->g(3.14f); // Derived::g(int) 3 (surprise!)

// Bad : behavior depends on type of the pointer

pb->h(3.14f); // Base::h(float) 3.14 (surprise!)

pd->h(3.14f); // Derived::h(float) 3.14

}

在第一种调用中,函数的行为取决于指针所指向的对象。在第二第三种调用中,函数的行为取决于指针的类型。所以说,隐藏破坏了面向对象编程中多态这一特性,会使得OOP人员产生混乱。

不过隐藏也并不是一无是处,它可以帮助编程人员在编译时期找出一些错误的调用。但我觉得还是应该尽量使用隐藏这一些特性,该加virtual时就加吧。

45[/b].调用约定
[/b]

调用约定(Calling convention)决定以下内容:函数参数的压栈顺序,由调用者还是被调用者把参数弹出栈,以及产生函数修饰名的方法。MFC支持以下调用约定:

(1)_cdecl::

按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于“C”函数或者变量,修饰名是在函数名前加下划线。对于“C++”函数,有所不同。

如函数void test(void)的修饰名是_test;对于不属于一个类的“C++”全局函数,修饰名是?test@@ZAXXZ。

这是MFC缺省调用约定。由于是调用者负责把参数弹出栈,所以可以给函数定义个数不定的参数,如printf函数。

(2)_stdcall:

按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。对于“C”函数或者变量,修饰名以下划线为前缀,然后是函数名,然后是符号“@”及参数的字节数,如函数int func(int a, double b)的修饰名是_func@12。对于“C++”函数,则有所不同。

所有的Win32 API函数都遵循该约定。

(3)_fastcall:

头两个DWORD类型或者占更少字节的参数被放入ECX和EDX寄存器,其他剩下的参数按从右到左的顺序压入栈。由被调用者把参数弹出栈,对于“C”函数或者变量,修饰名以“@”为前缀,然后是函数名,接着是符号“@”及参数的字节数,如函数int func(int a, double b)的修饰名是@func@12。对于“C++”函数,有所不同。

未来的编译器可能使用不同的寄存器来存放参数。

(4)thiscall:

仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压栈。thiscall不是关键词,因此不能被程序员指定。

naked call

采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。

(5)naked call不是类型修饰符,故必须和_declspec共同使用,如下:

__declspec( naked ) int func( formal_parameters )

{

// Function body

}

(6)过时的调用约定

原来的一些调用约定可以不再使用。它们被定义成调用约定_stdcall或者_cdecl。例如:

#define CALLBACK __stdcall

#define WINAPI __stdcall

#define WINAPIV __cdecl

#define APIENTRY WINAPI

#define APIPRIVATE __stdcall

#define PASCAL __stdcall

46[/b].关于复制构造函数
[/b]

也许很多C++的初学者都知道什么是构造函数,但是对复制构造函数(copy constructor)却还很陌生。对于我来说,在写代码的时候能用得上复制构造函数的机会并不多,不过这并不说明复制构造函数没什么用,其实复制构造函数能解决一些我们常常会忽略的问题。 为了说明复制构造函数作用,我先说说我们在编程时会遇到的一些问题。对于C++中的函数,我们应该很熟悉了,因为平常经常使用;对于类的对象,我们也很熟悉,因为我们也经常写各种各样的类,使用各种各样的对象;对于指针的操作,我们也不陌生吧?嗯,如果你还不了解上面三个概念的话,我想这篇文章不太适合你,不过看看也无碍^_^。我们经常使用函数,传递过各种各样的参数给函数,不过把对象(注意是对象,而不是对象的指针或对象的引用)当作参数传给函数的情况我们应该比较少遇见吧,而且这个对象的构造函数还涉及到一些内存分配的操作。嗯,这样会有什么问题呢? 把参数传递给函数有三种方法,一种是值传递,一种是传地址,还有一种是传引用。前者与后两者不同的地方在于:当使用值传递的时候,会在函数里面生成传递参数的一个副本,这个副本的内容是按位从原始参数那里复制过来的,两者的内容是相同的。当原始参数是一个类的对象时,它也会产生一个对象的副本,不过在这里要注意。一般对象产生时都会触发构造函数的执行,但是在产生对象的副本时却不会这样,这时执行的是对象的复制构造函数。为什么会这样?嗯,一般的构造函数都是会完成一些成员属性初始化的工作,在对象传递给某一函数之前,对象的一些属性可能已经被改变了,如果在产生对象副本的时候再执行对象的构造函数,那么这个对象的属性又再恢复到原始状态,这并不是我们想要的。所以在产生对象副本的时候,构造函数不会被执行,被执行的是一个默认的构造函数。当函数执行完毕要返回的时候,对象副本会执行析构函数,如果你的析构函数是空的话,就不会发生什么问题,但一般的析构函数都是要完成一些清理工作,如释放指针所指向的内存空间。这时候问题就可能要出现了。假如你在构造函数里面为一个指针变量分配了内存,在析构函数里面释放分配给这个指针所指向的内存空间,那么在把对象传递给函数至函数结束返回这一过程会发生什么事情呢?首先有一个对象的副本产生了,这个副本也有一个指针,它和原始对象的指针是指向同块内存空间的。函数返回时,对象的析构函数被执行了,即释放了对象副本里面指针所指向的内存空间,但是这个内存空间对原始对象还是有用的啊,就程序本身而言,这是一个严重的错误。然而错误还没结束,当原始对象也被销毁的时候,析构函数再次执行,对同一块系统动态分配的内存空间释放两次是一个未知的操作,将会产生严重的错误。 上面说的就是我们会遇到的问题。解决问题的方法是什么呢?首先我们想到的是不要以传值的方式来传递参数,我们可以用传地址或传引用。没错,这样的确可以避免上面的情况,而且在允许的情况下,传地址或传引用是最好的方法,但这并不适合所有的情况,有时我们不希望在函数里面的一些操作会影响到函数外部的变量。那要怎么办呢?可以利用复制构造函数来解决这一问题。复制构造函数就是在产生对象副本的时候执行的,我们可以定义自己的复制构造函数。在复制构造函数里面我们申请一个新的内存空间来保存构造函数里面的那个指针所指向的内容。这样在执行对象副本的析构函数时,释放的就是复制构造函数里面所申请的那个内存空间。 除了将对象传递给函数时会存在以上问题,还有一种情况也会存在以上问题,就是当函数返回对象时,会产生一个临时对象,这个临时对象和对象的副本性质差不多。 关于复制构造函数的性质还有很多,不过一时说不清楚,头脑还比较乱。^_^ C++真是复杂,不过复杂得来还挺有意思。47[/b].按位或运算和按位与运算
[/b]

在一个很大的系统中,我们经常会见到有类似以下的宏定义: #define DATA1 0x00000001 #define DATA2 0x00000002#define DATA3 0x00000004#define DATA4 0x00000008#define DATA5 0x00000010…….#define DATAn 0x04000000那些16进制的数可不是任意写上去的哦。如果把上面的十六进制转成二进制来看的话,那么:0x00000001就是000000000000000000000000000000010x00000002就是000000000000000000000000000000100x00000004就是000000000000000000000000000001000x00000008就是000000000000000000000000000010000x00000010就是000000000000000000000000000100000x04000000就是00000100000000000000000000000000当你把两个数进行或运算的话,得出的结果就有两个位为1,把三个数进行或运算的话,那么结果就有三个位为1。例如:#define MYDATA (DATA2 | DATA4)那么MYDATA的值就是00000000000000000000000000001010,即把DATA2和DATA4不同位置的“1”信息整合到一个新的32位数据上了。如果某函数返回来一个DWORD的结果给你,这个结果是多个DWORD数据进行或运算后得来的,我们就可以用与运算来判断某个DWORD数据是否参与了之前或运算,举例说明:如果得到一个DWORD的返回值,将它保存在dwData中。之后我们选择需要判断的数据来与dwData进行与运算,例如选择DATA3,有 DATA3&dwData,如果结果为0,那么DATA3并没有参与之前的或运算,否则就是有参与。利用这种特性,我们可以很方便地对参数进行组合,同样也可以很方便地判断某个参数是否参与过这种组合。48[/b].关于extern “C”
[/b]

使用extern “C”可以声明一个函数或变量是使用C库来进行链接的。一般来说,在编译源代码的时候,C++编译器会对函数或变量进行一些函数名或变量名的转换,例如对于函数foo(int a,int b),C++编译器会将函数的名字变成_foo_int_int,这样做是为了支持C++中函数的重载特性。然而在C中是没有函数重载的,所以C编译器就不会进行这样的名字转换,它只是将函数名变成_foo而已。当我们在C++的环境中使用C编译器所编译的库文件时,如果不使用extern “C”来声明一个C库函数,那么在链接的时候就会找不到正确的C库函数。在C++源文件中使用extern “C”可以避免C++编译器对函数名或变量名所做的那些转换,这样在链接的时候就可以找到正确的C库函数啦。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: