C++是一种扭转程序员思维模式的语言,一个人思维模式的扭转,不可能轻而易举一蹴而就。
C++是最重要的面向对象语言,因为它站在C语言的肩膀上,而C语言拥有绝对多数的使用者,C++并非纯粹的面向对象程序语言,但有时候混血并不是坏事,纯种不见得就有多好。(所谓纯面向对象语言,是指不管什么东西,都应该存在于对象之中,java就是纯面向对象语言)。C++语言范围何其广大,这部分主题的挑选完全是以MFC Programming所需技术为前提。
1、类及其成员----谈封装
让我们把世界看成是一个由对象(object)所组成的大环境。对象是什么?说白了,“东西”是也!任何实际的物体你都可以说他是对象。为了描述对象,我们应该先把对象的属性描述出来,好,给“对象的属性”一个比较学术的名词,就是“类”(class)。
对象的属性有2大成员,一是属性,一是方法。在面向对象的术语中,前者常被称为property,后者常被称为method。另有一种比较像程序设计领域的术语,名为member variable(或data member)和member function。为求统一,本部分使用第二组术语,也就是member variable(或data member)和member function。一般而言,成员变量通常由成员函数处理。
如果我们以CSquare代表“正方形”这种类。正方形有color,正方形可以display。好,color就是一种成员变量,display就是一种成员函数:
-
- class CSquare
- {
- private:
- int m_color;
- public:
- void display();
- void setcolor(int color){m_color=color;}
- };
- void main()
- {
- CSquare square;
- square.setcolor(RED);
- square.display();
- }
成员变量可以只在类内部被处理,也可以开放给外界处理。以数据封住的目的而言,自然是前者较为妥当,但有时候也不得不放开。为此,C++提供了private、public、protected三种修饰词。一般而言,成员变量尽量声明为private,成员函数通常声明为public。上例中的m_color既然声明为private,我们势必得准备一个成员函数setcolor,供外界设定颜色用。
把数据声明为private,不允许外界随意存取,只能通过特定的接口来操作,这就是面向对象的封住特性。
2、基类与派生类:谈继承(inheritance)
其它语言欲完成封装性质,并不太难。以C为例,在结构中放置资料以及处理资料的函数的指针,就可以得到某种程度的封装。C++神秘而特有的性质其实在于继承。矩形是形、椭圆形也是形、三角形也是形。苍蝇是昆虫、蜜蜂是昆虫、蚂蚁也是昆虫。是的,人类习惯把相同的性质抽取出来,成立一个基类(base class),再从中演化出派生类(derived class)。所以,关于形状,我们就有了这样的类层次结构。
- class CShape
- {
- private:
- int m_color;
- public:
- void setcolor(int color){m_color=color;}
- };
-
- class CRect : public CShape
- {
- public:
- void display(){...}
- };
-
- class CEllipse : public CShape
- {
- public:
- void display(){...}
- };
-
- class CTriangle : public CShape
- {
- public:
- void display(){...}
- };
-
- class CSquare : public CRect
- {
- public:
- void display(){...}
- };
-
- class CCircle : public CEllipse
- {
- public:
- void display(){...}
- };
-
- CSquare square;
- CRect rect1,rect2;
- CCircle circle;
- square.setcolor(1);
- square.display();
- rect1.setcolor(2);
- rect1.display();
- rect2.setcolor(3);
- rect2.display();
- circle.setcolor(4);
- circle.display();
注意以下这些问题与事实:
1)所有类都是由CShape派生下来的,所以它们都自然而然继承了CShape的成员,包括变量和函数。也就是说,所有的形状都暗自具备了m_color变量和setcolor函数。所谓的暗自(implicit),意思是无法从各派生类的声明中直接看出来。
2)两个矩形对象rect1和rect2各有自己的m_color,但关于setcolor函数却是共享相同的CRect::setcolor(其实更应该说是CShape::setcolr)。想一下,同一个函数如何处理不同的数据,为什么rect1.setcolor和rect2.setcolor明明都是调用CRect::setcolor(),却能够有条不紊的处理rect1.m_color和rect2.m_color?答案在于所谓的this指针,下一节我们就会提到它。
3)既然所有类都有display操作,那么把它提升到老祖宗CShape去,然后再继承它好吗?不好,因为display函数应该因不同的形状而操作不同。
4)如果display不能提升到基类去,我们就不能够以一个for循环或while循环干净漂亮的完成下列操作(此操作模式在面向对象方法中重要无比):
CShape shapes[5];
//...令5个shape各为矩形、正方形、椭圆、圆形、三角形
for(int i=0;i<5;i++)
shapes[i].display();
5)Shape只是一种抽象概念,世界上并没有“形状”这种东西,你可以在一个C++程序中做以下操作,但是不符合生活法则:
CShape shape;//世界上没有“形状”这种东西
shape.setcolor();//所以这个操作就有点古怪
这同时也说出了第三点的另一个否定理由:按理你不能把一个抽象的“形状”显示出来,不是吗?
3、this指针
刚才讲过,两个矩形对象rect1和rect2各有自己的m_color成员变量,但rect1.setcolor()和rect2.setcolor()却都通往唯一的CRect::setcolor()成员函数,那么CRect::setcolor()如何处理不同对象中的m_color?答案是:成员函数有一个隐藏参数,名为this指针,但你调用:
rect1.setcolor(2);
rect2.setcolor(3);
时,编译器实际上为你做出来的代码是:
CRect::setcolor(2,CRect*(&rect1));
CRect::setcolor(3,CRect*(&rect2));
不过,由于CRect本身并没有setcolor,它是从CShape继承来的,所以编译器实际上产生的代码是:
CShape::setcolor(2,CRect*(&rect1));
CShape::setcolor(3,CRect*(&rect2));
多出来的参数就是所谓的this指针,至于类之中成员函数的定义:
- class CShape
- {
- ......
- public:
- void setcolor(int color){n_color=color}
- };
被编译过后,其实是:
- class CShape
- {
- ......
- public:
- void setcolor(int color,(CShape*)this){this->n_color=color}
- };
4、虚函数与多态(polymorphism)
前面曾经提到过,前一个例子不能完成这样的操作:
CShape shape[5];
//...令5个shape各为矩形、正方形、椭圆、圆形、三角形
for(int i=0;i<5;i++)
shapes[i].display();
但是这种所谓“对象操作的一般化操作”在application framework中非常重要。我们希望display函数能根据CShape派生出来的类的不同,只需调用display就能显示自己的形状特性。
为了支持这种能力,C++提供了所谓的虚函数(virtual function)。
虚拟+函数?!听起来很恐怖的样子,如果你了解汽车的离合器踩下去 代表 汽车空挡,空挡表示失去引擎本身的牵制力,你就会了解“高速行驶时刹车决不能踩离合器”的道理并矢志遵守它。好,如果你真的了解为什么需要虚拟函数 以及 什么情况下需要它,你就能够掌握它的灵魂与内涵,真正了解它的设计原理,并且发现它非常合乎人性。并且,真正知道怎么用它。
看下面的一个例子:
- #include <string.h>
-
- class CEmployee
- {
- private:
- char m_name[30];
- public:
- CEmployee();
- CEmployee(const char* nm){strcpy(m_name,nm);}
- };
-
- class CWage : public CEmployee
- {
- private:
- float m_wage;
- float m_hours;
- public:
- CWage(const char* nm):CEmployee(nm)
- {
- m_wage=250.0;
- m_hours=40.0;
- }
- void setWage(float wg)
- {
- m_wage=wg;
- }
- void setHours(float hrs)
- {
- m_hours=hrs;
- }
- float computePay()
- {
- return (m_wage*m_hours);
- }
- };
-
- class CSales : public CWage
- {
- private:
- float m_comm;
- float m_sale;
- public:
- CSales(const char* nm):CWage(nm)
- {
- m_comm=m_sale=0;
- }
- void setCommission(float comm)
- {
- m_comm=comm;
- }
- void setSales(float sale)
- {
- m_sale=sale;
- }
- float computepay()
- {
- return CWage::computePay() + m_comm*m_sale;
- }
- };
-
- class CManager : public CEmployee
- {
- private:
- float m_salary;
- public:
- CManager(const char* nm):CEmployee(nm)
- {
- m_salary=15000;
- }
- void setSalary(float salary)
- {
- m_salary=salary;
- }
- float computePay()
- {
- return m_salary;
- }
- };
虚函数的故事要从薪水的计算说起,根据不同职员的计薪方式,设计了computePay()函数。看上面代码中computePay函数的实现,注意代码中的作用域操作符(::)。
接下来我们要触及对象类型的转换,这关系到指针的运用,更直接关系到为什么需要虚函数。了解它,对application framework如MFC者的运用十分十分重要。
假设我们有2个对象:
CWage aWager;
CSales aSales("张三");
销售员是时薪员之一,因此这样做是合理的:
aWager=aSales;//合理,销售员必定是时薪员
这样就不合理:
aSales=aWager;//错误,时薪员未必是销售员
如果你一定要转换,必须使用指针,并且明显的做类型转换(cast)操作:
- CWage* pWager;
- CSales* pSales;
- CSales aSales("张三");
- pWager=&aSales;
- pSales=(CSales*)pWager;
为了某种便利(这个便利稍候即可看到),我们也会想以“一个通用的指针”表示所有可能的职员类型。无论如何,销售员、时薪职员、经理都是职员,所以下面的操作合情合理:
- CEmployee* pEmployee;
- CWage aWager("john");
- CSales aSales("jack");
- CManager aManager("mary");
- pEmployee=&aWager;
- pEmployee=&aSales;
- pEmployee=&aManager;
也就是说,可以把一个“职员指针”指向任何一种职员。这带来的好处就是程序设计的巨大弹性,譬如说你设计一个链表,各个元素都是职员,你的add函数可能因此希望有一个“职员指针”作为参数:add(CEmployee* pEmp);//pEmp可以指向任何一种职员
5、晴天霹雳
我们渐渐接触问题的核心,上述C++性质使真实生活经验 的确在 计算机语言中仿真了出来,但是万里无云的日子里却出现了一个晴天霹雳:如果你以一个“基类之指针”指向一个“派生类之对象”,那么经由此指针,你就只能够调用基类(而不是派生类)所定义的函数。
- CSales aSales("Jack");
- CSales* pSales;
- CWage* pWager;
- pSales=&aSales;
- pWager=&aSales;
- pWager->setSales(800.0);
- pSales->setSales(800.0);
虽然pSales和pWager指向同一个对象,但却因指针的原始类型不同而使2者之间有了差异,,延续此例,我们看另一种情况:
pWager->computePay();//调用CWage::computePay()
pSales->computePay();//调用CSales::computePay()
虽然pWager和pSales指向同一个对象CSales,但两者调用的computePay却不相同。到底该调用哪个函数,必须视指针的原始类型而定,与指针实际所指对象无关。
三个结论:
1)如果你以一个“基类指针”指向“派生类对象”,那么经由该指针你只能够调用基类所定义的函数。
2)如果你以一个“派生类之指针”指向“基类对象”,你必须先做明星的类型转换。这种做法很危险,不符合真实生活经验,在程序设计上也会给程序员带来困惑。
3)如果基类和派生类都定义了“相同名称的成员函数”,那么通过对象指针调用成员函数时,到底调用哪一个函数,必须视指针的原始类型而定,而不是视指针实际所指的对象类型而定,这与第一点的意义相通。
得到这些结论以后,看看什么事情会困扰我们,前面我曾提到一个由职员组成的链表,如果我想写一个printNames函数遍历链表中的每一个元素并打印出职员的名字,我们可以在CEmployee(最基类)中多加一个getName函数,然后再设计一个While循环,以此打印职员链表中职员的名字(遍历链表的每个元素,调用其getName函数即可)。
但是函数的调用是依赖指针的原始类型而不管它实际上指向何方(何种对象),所以,上面所想象的循环都执行的是同一条语句。即基类CEmployee的getName函数,不能达到我们预期的目的。
6、虚函数与一般化
你可以体会,上述的while循环其实就是把操作“一般化”。“一般化”之所以重要,在于它可以把现在的、未来的统统纳入考虑。将来即使有另一种名曰“顾问”的职员,上述计薪循环应该仍然能够正常运行。当然了,“顾问”的computePay必须设计好。
“一般化”如此重要,解决上述问题因此也就迫切起来,我们需要的是什么呢?是能够“依旧以基类指针代表每一种职员”,而又能够在“实际指向不同种类之职员”时,调用到不同版本只computePay的能力。这种性质就是多态(polymorphism),靠虚函数来完成。
从 操作性 定义来看,什么是虚函数呢?如果你预期派生类有可能重新定义某一个成员函数,那么你就在基类中把此函数设为virtual。MFC有2个非常重要的虚函数,与document有关的Serialize函数和与View有关的OnDraw函数。你应该在自己的CMyDoc和CMyView中改写这两个函数。
7、多态(polymorphism)
你看,我们以相同的指令却调用了不同的函数,这种性质称为多态,编译器无法再编译时判断到底该调用哪一个函数,必须在执行时才能判断,这成为后期绑定或动态绑定。至于C函数或C++的non-virtual函数,在编译时期就转化为一个固定地址的调用了,这成为前期绑定或静态绑定。
多态的目的,就是要让处理“基类之对象”的程序代码,能够完全无碍的继续适当处理“派生类之对象”。可以说,虚函数是多态以及动态绑定的关键,同时,它也是了解如何使用MFC的关键。
再次回到前面CShape的例子。我们说CShape是抽象的,所有它根本不应该有display这个操作,但为了在各具体派生类中绘图,我们又不得不在基类CShape中加上display虚函数,你可以定义它什么也不做:
- class CShape
- {
- public:
- virtual void display(){}
- };
- 或只是给个消息:
- class CShape
- {
- public:
- virtual void display(){cout<<"Shape"<<endl;}
- };
这两种做法都不高明,因为这个函数根本就不应该被调用,我们根本就不该定义它,步定义但又必须保留一块空间给它,于是C++提供了所谓的纯虚函数:
- class CShape
- {
- public:
- virtual void display()=0;
- };
纯虚函数不需要定义实际操作,它的存在只是为了在派生类中被重新定义,只是为了提供一个多态接口。只要是拥有纯虚函数的类,就是一种抽象类,它是不能够被实例化的,也就是说,你不能根据它产生一个对象,如果硬要用抽象类来产生一个对象,那么会换来这样的编译消息:error:illegal attempt to instantiate abstract class.
关于抽象类还有一点需要补充,CCircle继承了CShape之后,如果没有改写CShape中的纯虚函数,那么CCircle本身也就成为一个拥有纯虚函数的类,于是它也是一个抽象类。
对虚函数的总结:
1)如果你期望 派生类 重新定义 一个成员函数,那么你应该在基类中把此函数设为virtual。
2)以单一指令调用不同函数,这种性质成为多态
3)虚函数是C++语言多态以及动态绑定的关键
4)既然抽象类中的虚函数不打算被调用,我们就不应该定义它,应该把他设为纯虚函数(在函数声明之后加上“=0”即可)
5)我们可以说,拥有纯虚函数者为抽象类,以别于所谓的具体类
6)抽象类不能产生出对象实例,但我们可以拥有指向抽象类的指针,以便于操作抽象类的各个派生类
7)虚函数派生下去仍为虚函数,而且可以省略virtual关键词
分享到:
相关推荐
标签 "C++ 面试题"、"经典" 和 "题库" 强调了这个压缩包的性质,它是一个全面且具有代表性的C++面试问题集,适合用于自我测试和复习。标签中的“经典”可能意味着这些题目在过去多年中被反复引用,对于理解C++的核心...
红黑树是一种自平衡二叉查找树(BST),由Rudolf Bayer在1972年提出,它的每个节点都带有颜色属性,可以是红色或黑色。...理解和掌握红黑树的原理及操作对于提升程序设计能力、优化算法性能具有重要意义。
标签"c++学习 学习好资料"再次确认了文件内容的性质,表明这是一份对C++初学者非常有价值的资源集合。 在"要学C++的来下啦!"这个压缩包子文件的文件名称列表中,虽然没有具体的文件名,但可以推测其中可能包含一...
总的来说,这个项目提供了一个基于C++实现的IAPWS-IF97水蒸气性质计算工具,对于理解水蒸气状态变化及其相关工程应用具有重要意义。它涉及到计算机编程、数值计算、热力学等多个领域的知识,是IT技术与科学理论紧密...
以上就是C++中关于数据结构的一些基本实现,这些数据结构是计算机科学和软件工程中不可或缺的部分,熟练掌握它们对于编写高效的代码至关重要。通过实际编写和测试这些数据结构的代码,你可以加深对它们的理解并提升...
在C++编程中,二元关系的性质是数学逻辑和关系理论的重要概念,它们在算法设计、数据库理论以及形式逻辑等多个领域都有广泛应用。本代码主要关注五个关键性质:自反性、对称性、传递性、反自反性和反对称性。下面将...
解析这些信息对于理解网络通信的性质至关重要。 5. **封包过滤**: 源码中可能包含过滤规则,允许用户根据特定条件(如IP地址、端口、协议等)选择性地捕获数据包。BPF(Berkeley Packet Filter)语法常用于定义...
提到的标签“C++标准库函数文档”则指向了本内容作为标准库函数相关文档的性质,意味着它应该包含标准库函数的详细介绍、用法示例和相关说明。文档通常是学习和使用标准库函数的重要参考材料。 在部分内容中,虽然...
它强调了书籍的“探索”性质,意味着书中内容不仅仅是对C++语言特性的简单介绍,而是更深层次的讲解,以及如何在实践中应用这些特性来解决编程问题。此外,“程序员的C++入门教程”表明这本书将从基础开始,带领读者...
理解C++的跨平台性质,不要因为某种编程环境或语言的流行而轻易改变学习方向。学习C++的目标是掌握编程思维和技术,这些是可以在不同语言间迁移的。最后,保持耐心和毅力,编程需要时间和实践才能精通。 总结来说,...
求行列式则是判断矩阵是否可逆的重要手段,对于方阵,其行列式的值可以决定该矩阵是否有逆矩阵。行列式的计算通常通过递归的LU或LAPACK库来实现。 最后,特征值和特征向量是矩阵理论中的关键概念,它们揭示了矩阵的...
在C++中实现伽马函数,我们需要理解其基本性质和计算方法。 伽马函数的定义为积分形式: \[ \Gamma(z) = \int_0^{\infty} t^{z-1}e^{-t} dt \] 这个积分定义适用于所有复数z,除了z=0和负整数。对于正整数n,伽马...
在本文中,我们将深入探讨如何在C++程序中调用REFPROP库,这是一个广泛用于流体性质计算的软件包。REFPROP(Reference Properties of Fluids)由美国国家标准与技术研究所(NIST)开发,提供了精确的流体性质计算,...
- **文档性质**: 这份文档被视为C++核心编程指南的一个早期草稿版本,它由C++语言的创造者Bjarne Stroustrup与Herb Sutter共同编辑,并由中国开发者李一楠负责翻译。 - **版本状态**: 当前版本为0.6版,作为开源项目...
总结来说,理解堆的性质和C++中的实现方法对于提升编程能力非常重要。通过手动构建堆和使用STL中的堆函数,我们可以灵活地解决各种问题,如排序、优先队列和查找特定元素等。而堆排序作为一种原地排序算法,虽然平均...
C++11是C++编程语言的一个重要更新,它引入了大量的新特性,旨在提升效率、安全性和可读性。以下是一些从提供的压缩包文件名中提炼出的关键知识点的详细解释: 1. **可变参数模板(Variadic Templates)**:在`9C++...
图论是计算机科学中的一个重要分支,它研究的是网络和图的结构、性质及其在问题解决中的应用。在C++编程中,理解并掌握图论基础知识能够帮助开发者解决复杂的问题,如最短路径、网络流、搜索算法等。下面将详细阐述...
这本书被广泛认为是学习C++以及提升C++编程能力的重要资源。下面将围绕《Effective C++》的内容展开知识点的说明,但请注意,这些内容并非直接引自您提供的文件。 ### 知识点一:构造函数与析构函数的最佳实践 C++...
总之,"c++常用运行库集合"对于确保C++程序在Windows上的正常运行至关重要。这些库不仅包含了C++语言的基本功能,还涵盖了与Windows操作系统交互的接口,是开发者和用户都需要了解的重要部分。在遇到“xxx.dll文件...
标签“c++练习题”和“c++课本作业”明确了文件的性质,这些是C++编程语言的学习材料,特别是与解决实际编程问题和完成课后作业相关的。 压缩包内的文件名称列表包括: 1. p149_3.zip.cpp:这可能是第149页上的第三...