工作了两年过后,感觉对C++的一些特性反而忘了一些,很多概念都模糊了。昨天看了《MFC深入浅出》的第二章,对C++的知识进行了或多或少的回顾,其中一些概念,歇久了不去看,总是会遗忘。"温故而知新,不亦说乎",趁着空暇时间去看看那些基础知识,总能够收获不少。
C++是专门为面向对象方法而设计语言,使得我们能够用现实生活中的经验来编写程序。但是它保留了C语言的相关特性,并非像C#、Java那样的纯面向对象语言,如并不是所有属性或方法都要放在对象中,它保留了C语言的函数。既然是面向对象语言,肯定得具备封装、继承、多态这三个最重要的特性,这里我就谈谈自己的理解,还希望众多高手能够多多指教。
封装,是面向对象语言的最基础的特点,没有封装,继承和多态就无法实现。封装就是将数据和处理数据的方法绑定在一起,将数据设定为private,将方法设定为public,使得外界无法随意存取数据,只能够通过指定的方法来操作数据。C++的类就是我们对现实生活中对象的一种抽象,我们提取出共同的属性和动作,通过封装,我们就能够得到一类事物的描述。通过将对象属性隐藏起来由方法进行访问控制,我们就为数据的操作提供了安全性、可扩展性、兼容性等很多好处。很多时候,封装的并不一定是一个具体的事物,也可能是一种功能,即我们常说的操作接口。
继承,在封装的基础上实现通用属性和方法的复用,我们不需要在多个类中重复的编写相同的代码了,只要父类中有了相同的功能,子类就可以坐享其成。继承实际上也是对现实生活的一种诠释,人们通常将事物进行分门别类,如动物这一大类,可以分为鸟类、鱼类、爬行类等,鸟类中又有燕子、老鹰等等子类,它们都有相同的属性和方法,但是它们也有各自独特的地方。通过继承,将一般的属性和方法提取到父类中,将特殊的属性和方法放到子类中,我们可以在程序中是实现这种分类。
要实现继承,如B继承自A,那么一定满足:B是一个A,但是A不一定是B,否则这种继承就不是真正的继承,就可以使用如包含等方法来改写。通常我们不应该为了方便或其他原因随意的继承,例如狗和鸟类都有叫的方法,而让狗继承自鸟类,以便重用鸟类的方法,这在程序中可以实现,但未免不符合逻辑。
假设我们有三个类:
class CAnimal
{
public:
void Eat() { printf("Animal Eat\n"); };
}
class CBird : public CAnimal
{
public:
void Eat() { printf("Bird Eat\n"); };
}
class CSwallow: public CBird
{
public:
void Eat() { printf("Swallow Eat\n"); };
}
这时我们就可以通过new得到它们各自的实例lpAnimal,lpBird,lpSwallow。我们可以这样操作,lpAnimal->Eat(),lpBird->Eat(),lpSwallow->Eat(),它们都会正确的调用各自的Eat方法。由于CSwallow必定是一个CBird,也必定是一个CAnimal,所以我们可以这样操作,CAnimal *lpAniB= lpBird,*lpAniS = lpSwallow,也都可以调用lpAniB->Eat(),lpAniS ->Eat(),问题出现了!结果不如预期,它们都是调用CAnimal的Eat方法。通过指针只能调用定义该指针的原始类型的对象的方法,也就是说lpAniB、lpAniS 的原始类型都是CAnimal,所以它们必然都是调用CAnimal的方法。
既然CAnimal定义了通用的、一般的方法和属性,我们就会指望通过它的指针能够操作不同的子类,以实现特殊实例的一般化操作。这时,虚函数就要登场了。通过在方法声明前加一个virtual关键字,我们就可以将该方法定义为虚函数。例如将CAnimal定义如下:
class CAnimal
{
public:
virtual void Eat() { printf("Animal Eat\n"); };
}
CBird、CSwallow的定义都不用改,因为只要父类的函数被定义为virtual,子类继承后也会是virtual的。再次通过指针调用lpAniB->Eat(),lpAniS ->Eat(),就会分别调用CBird、CSwallow的方法了。这就是多态的特性,我们通过CAnimal的指针可以调用所有子类对象的方法,而不需要关心子类的具体实现了,即实现了一般的操作。我们可以将每个预期会在子类中被改写的函数定义为virtual。
多态,是一种动态绑定的函数调用方式,在运行时才能够确定调用哪个函数,而普通函数在编译时就已经被转换为一个固定的地址调用了。虚函数是C++实现多态和动态绑定的关键,编译器会为每个包含虚函数的类分配一个虚函数表vtable,并增加变量bptr指向这个虚函数表的地址。vtable中的每个元素都指向一个虚函数的地址,程序在运行时会查看虚函数表找到将要调用的函数的地址。当子类继承父类时,也会继承这个vtable,并且如果子类中改写了某个虚函数,那么vtable中记录的函数地址也会相应的发生改变,以指向它自己的函数。所以,通过指针调用虚函数时,会有一个查询vtable的动作,以确定真正的函数地址。但是这种特性只能通过指针和引用来实现,如CAnimal anAnim; CBird aBird; anAnim = (CAnimal)aBird; 通过anAnime调用Eat方法的结果仍然是anAnie的方法的执行结果。更重要的是,当执行anAnim = (CAnimal)aBird这样的强制向上转换时,会发生对象切割,aBird中定义的属性和方法将会丢失,只会保留父类中的属性和方法。
我们可以再看一个问题,CAnimal的实例是什么,CBird的实例是什么,世界上没有一样东西叫做动物、鸟,而lpSwallow就指向了一个具体的鸟类:燕子。那么怎样才能符合逻辑呢?这时,就需要将CAnimal、CBird定义为抽象抽象对象了,它们是我们对具体事物共同特性的一种抽象,是不存在的,所以我们也不能生成这些抽象类的实例,否则就不符合逻辑了。通过如下定义,将Eat方法定义为纯虚函数,CAnimal、CBird就变成抽象对象了,当我们再次生成实例时,编译器就会报错了。
class CAnimal
{
public:
virtual void Eat() = 0;
}
class CBird : public CAnimal
{
public:
virtual void Eat() = 0;
}
转载于:https://blog.51cto.com/jslmes/1159030