面向对象编程入门指南






3.82/5 (29投票s)
2004年4月7日
7分钟阅读

113838

1496
解释了 C++ 中的面向对象。以及关于模板的一些内容。
引言
本文解释了 C++(以及其他面向对象语言)的发明原因。与过程式语言相比,它的优点是什么,并展示了一个例子。
什么是面向对象 (OO)?
很久很久以前...
简单来说:OO 是代码组织上的一大改进。面向对象编程 (OOP) 是一种思想。让我来解释一下:假设有一个公司,一个大公司。公司里的每个员工都可以访问所有东西(这还算公司吗?)。即使是清洁工也能接触到科学部门,随意摆弄一些危险的毒素。幸运的是,每个人都非常忠诚,不会在不久的将来做这样的事情。然后,有一天,随着这家公司变得越来越大、越来越复杂,整个组织都崩溃了。快点,问题出在哪里?正是,组织变得太复杂了。一切都很混乱,科学家们再也找不到毒素了,因为清洁工摆弄了它们。推销员们疯了,因为接线员不小心卖掉了所有东西,等等。
现在,把这家公司想象成一个程序。这个程序没有真正的结构。一切都知道一切,这对于一个简单的“Hello World”程序来说并不是什么大问题。但正如我所说,这是一个大程序(公司)。在这个程序中的清洁工、科学家、推销员和接线员就是函数。毒素和待售产品就是数据。当然还有更多,请发挥你的想象力。关键在于,每个函数都可以访问所有数据,这在 C 等过程式语言中是普遍存在的(尽管 C 有一些防止这种情况的特性,但我认为它们作用不大)。这意味着,在某些情况下,当创建一个新函数时,数据很容易丢失或损坏。如果需要编辑一个数据项,你还需要编辑所有使用该特定数据项的函数……当你工作了,比如说……几个月后,这会非常令人沮丧。
OO 来拯救!
这就是 OO 的意义所在,它让上面这种非理性的程序变得更加理性。让混乱变得有序等等。让我们再次发挥想象力……假设有一家公司,一家大公司。这家公司非常成功,并分为多个部门。每个部门都有自己的目的和安全措施。这次,接线员无法出售任何产品,因为所有产品都存放在仓库中,只有推销员才能访问(哈!)。清洁工也无法进入科学部门,因为只有科学家才有钥匙!这可以称之为一家公司。它具有理性的组织结构和安全措施,以防止公司内部的任何损坏或疏忽。现在,老板(是的,这次有老板了)可以安心回家,而不必担心清洁工的疯狂幻想。一个词:OOP。我们可以通过使用 **类** 来对程序进行此操作。这就是我们要做的。
公司示例
无 OOP
让我们来看看一个没有 OO 的程序可能发生的灾难。(示例很小且不真实,但可以帮助你理解。)注意:它仍然是用 C++ 编写的,但没有考虑 OO 概念。
//FILE: Evil_Workers.cpp #include <iostream.h> #include <conio.h> //for getche const int LastChecked = 10; //unaltered value int toxin = LastChecked; //toxin data item int products = LastChecked; //products in stock void cleaner() { cout << "\nHAHAH I AM AN EVIL CLEANER!!\ntoxin = " << toxin << "\n*Alters toxin!*\n"; //yes he is evil! toxin += 2; //cleaner CAN alter toxin! cout << "toxin = " << toxin << endl; } void telephonist() { cout << "\nHAHAH I AM AN EVIL TELEPHONIST!!\nproducts = " << products << "\n*Sells all products!*\n"; //NOOOOO!! products -= 10; //telephonist CAN sell products! cout << "products = " << products << endl; } void scientist() { cout << "\nScientist:\n"; //scientist checks if toxin is still unaltered if(toxin == LastChecked) cout << "I'm glad nobody messed with my toxin!\n"; else cout << "Oh my god, somebody messed with my toxin!!!!\n"; } void salesman() { cout << "\nSalesman:\n"; //salesman checks if no products are sold if(products == LastChecked) cout << "I'm glad nobody sold anything!\n"; else cout << "Oh my god, somebody sold stuff!!!!!\n"; } void main() { scientist(); //scientist checks salesman(); //salesman checks cleaner(); //cleaner alters telephonist(); //telephonist sells scientist(); //scientist checks salesman(); //salesman checks cout << "\n\nPress enter."; cout.flush(); getche(); //just so the program doesnt terminate immediatly }
正如你在这里看到的,`toxin` 和 `products` 是 **全局** 定义的。这意味着程序的所有部分都可以访问它们。所以现在,`cleaner()` 和 `telephonist()` 都可以访问这些变量。当然,这不是我们想要的。我们只希望 `scientist()` 和 `salesman()` 能够访问这些变量。同样,我们不希望 `scientist()` 能够修改 `products`,`salesman()` 和 `toxin` 也是如此。
上面的代码还有另一个问题。该程序并不真正符合现实生活中的情况。以 `scientist()` 为例,它是一个 **函数**。而在现实生活中,科学家更像是一个 **对象** 而不是一个 **函数**。而 **科学家检查毒素** 才是科学家 **函数**。那么 OO 是如何解决这一切的呢?在 C++ 中,它通过使用 **类** 来解决。我不会在这篇文章中深入探讨类(如果你想深入了解类,请访问 这里)。
OOP 的魅力
现在,让我们看看 OOP 如何在这个示例中使用 **类** 来解决问题。
//FILE: Good_Workers.cpp #include <iostream.h> //a class named Scientist will be defined here class Scientist { private: //important! int toxin; public: //constructor with initialization list Scientist(int Toxin_Value) : toxin(Toxin_Value) {} //member functions which do something with toxin int GetToxin() //return the value of toxin { return toxin; } void AlterToxin(int Toxin_Value) //set toxin to Toxin_Value { toxin = Toxin_Value; } }; //for OO's sake let's create a Cleaner class class Cleaner { public: void Evil_Altertoxin(Scientist& victim, int value) { victim.toxin = value; //this will generate a compiler error } }; void main() { const int DaveVar = 10; //correct toxin value Scientist Dave(DaveVar); //create object Dave Cleaner John; //create object John //Dave checks to see if toxin is still unaltered cout << "\nScientist Dave:\n"; if(Dave.GetToxin() == DaveVar) cout << "I'm glad nobody messed with my toxin!\n"; else cout << "Oh my god, somebody altered my toxin!\n"; //John attempts to alter toxin cout << "\nEVIL Cleaner John:\nLet's try to alter Dave's toxin!"; John.Evil_Altertoxin(Dave, 0); //just for demonstration toxin = 0; //another compiler error }
“Good_Workers.cpp” 会产生 2 个编译器错误。第一个是这个
victim.toxin = value; on line 32.
另一个是这个
toxin = 0; on line 54.
首先,让我们看看我们的类。`Scientist` 类有一个数据项,两个 **成员函数** 和一个 **构造函数**。**成员函数** 定义在 **public** **作用域解析运算符** 下。当某项定义在 **public** **作用域解析运算符** 下时,整个程序都可以访问该数据,但是,它下面的任何数据或成员函数仍然不是 **全局** 定义的。这就是为什么语句 `toxin = 0;` 会产生编译器错误,因为编译器看不到全局的 toxin。它会显示类似如下的错误信息:
error C2065: 'toxin' : undeclared identifier
但是,你可能会问,如何访问类成员呢?通过 **点运算符**,就像你看到的 `Dave.GetToxin()` 和 `John.Evil_Altertoxin(Dave, 0)` 一样。说起这个,让我们来看看那个成员函数 `Evil_Altertoxin()`。
封装
void Evil_Altertoxin(Scientist& victim, int value) { victim.toxin = value; //this will generate a compiler error }
这个函数接受一个类型为(类)`Scientist` 的参数,以及一个类型为 `int` 的参数。然后它尝试通过将 `value` 赋给 `victim` 的 `toxin` 来修改 `victim` 的 `toxin`。现在一切看起来都正常,因为我们正式使用了 **点运算符** 来访问 `victim` 的 `toxin`。然而,编译器会给我们一个错误,说我们仍然无法访问它!这是因为我们将 `toxin` 声明在 **private** **访问说明符** 下。当类中出现 **private** **访问说明符** 时,意味着该访问说明符 **之后** 的所有内容只能由类 **内部** 的其他部分访问(**直到出现另一个访问说明符**)。在 `Scientist` 类中,这意味着只有 `GetToxin()` 和 `AlterToxin()` 函数(以及构造函数)可以访问 `toxin`。这也意味着像 `Evil_Altertoxin()` 这样的函数 **不能** 访问 `toxin`,因为它不是 `Scientist` 类的成员(这就是所谓的 **封装**)。
请友善一点!
不过,`John` 有一种方法可以“访问”`toxin`。他可以请 `Dave` 将 `toxin` 修改为所需的值。然后,而不是
victim.toxin = value; //this will generate a compiler error
我们必须写
victim.AlterToxin(value); //this will compile
即使如此,`John` 并没有真正访问 `toxin`,因为 `Dave` 仍然在这里完成所有工作。
新的数据类型!
const int DaveVar = 10; //correct toxin value Scientist Dave(DaveVar); //create object Dave Cleaner John; //create object John
你有没有注意到 `Dave` 和 `John` 的声明?是的,人们,我们创建了 2 个新的数据类型:`Scientist` 和 `Cleaner`!现在,它们不再是函数,而是实例数据,这比现实世界中的情况要可比得多。一切都完美契合!
“感谢上帝,只有一个!”
至于“不必重写每个函数”这件事呢?假设你有一个返回参数平方数的函数,并且希望该函数能够操作尽可能多的数据类型。无论是否有重载,你仍然需要编写很多函数。多亏了一个新功能,**模板**,我们只需要编写一个!
//FILE: Templates.cpp #include <iostream.h> #include <conio.h> //for getche template <class T> //warning: translate class as type! T SqrNum(T argNum) { return (argNum * argNum); //return type is of T } void main() { int inum = 2; long lnum = 4; double dnum = 5.6; cout << "inum * inum = " << SqrNum(inum) << endl << "lnum * lnum = " << SqrNum(lnum) << endl << "dnum * dnum = " << SqrNum(dnum) << endl << endl << "Press enter."; cout.flush(); getche(); //just so the program doesnt terminate immediatly }
这是怎么回事!?我来给你解释一下,不过我不会深入探讨语法。函数 `SqrNum()` 在定义时实际上并没有被创建。它更像是一个创建其他函数的 **蓝图**。当编译器收到对模板函数的“调用”时,它实际上不会调用模板函数。相反,编译器将 **使用** 模板函数为正确的数据类型创建正确的函数。在第一次“调用”`SqrNum(inum)` 时,编译器将开始一个类似这样的过程
- 嗯…… `inum` 的类型是 `int`,所以我们将 `T` 视为 `int`。
- 嗯……让我们创建一个可以处理我们 `int` 参数的函数。
(它看起来会像这样:)
int SqrNum(int argNum) //notice how all the Ts have been replaced by ints { return (argNum * argNum); //return type is of int }
然后,实际的函数调用就完成了。当然,`SqrNum(lnum)` 和 `SqrNum(dnum)` 也是如此。
另外请注意:编译器实际上 **不会** 在源代码文件中将模板替换为函数!
无限可能...
现在,关于类,通过类,或者在类中,还有 **大量** 的其他事情可以做。例如,运算符重载、函数重载、继承和多态、抽象类等等。这超出了本文的范围。OO 就是你对 OO 的运用,以及更广阔的领域…… ;-)
本文有问题吗?大胆说出来吧!我写这篇文章既是为了分享知识,也是为了学习知识。(我注意到我把 OO 解释得好像它只是封装,请不要那样理解。)
历史
- 2004 年 4 月 7 日:文章发布。