面向对象编程简介

在对象与变量的介绍中,我们将C++中的对象定义为“一块可以用来存储值的内存”。具有名称的对象称为变量。我们的C++程序由一系列指令组成,这些指令定义了数据(通过对象)以及对这些数据执行的操作(通过包含语句和表达式的函数)。

迄今为止,我们所从事的编程类型被称为过程式编程。在过程式编程中,重点是创建“过程”(在C++中称为函数),以实现程序逻辑。我们将数据对象传递给这些函数,这些函数对数据执行操作,然后可能会返回一个结果供调用者使用。

在过程式编程中,函数和这些函数所操作的数据是分离的实体。程序员负责将函数和数据结合起来,以产生期望的结果。这导致代码看起来像这样:

eat(you, apple);

现在,环顾四周——你所看到的到处都是对象:书籍、建筑物、食物,甚至是你自己。这些对象有两个主要组成部分:1)一定数量的关联属性(例如重量、颜色、大小、坚固性、形状等),2)一定数量的可以展现的行为(例如被打开、使其他东西变热等)。这些属性和行为是不可分割的。

在编程中,属性由对象表示,行为由函数表示。因此,过程式编程对现实的表示相当差,因为它将属性(对象)和行为(函数)分开。

什么是面向对象编程?

面向对象编程(通常缩写为OOP)中,重点是创建包含属性和一组明确定义行为的程序定义数据类型。在OOP中,“对象”一词指的是从这些类型实例化出来的对象。

这导致代码看起来更像是这样:

you.eat(apple);

这使得主体(you)更加清晰,被调用的行为(eat())更加明确,以及哪些对象是该行为的附属物(apple)也更加直观。

由于属性和行为不再分离,对象更容易模块化,这使得我们的程序更容易编写和理解,并且提供了更高的代码复用性。这些对象还通过允许我们定义如何与对象交互,以及它们如何与其他对象交互,提供了一种更直观的方式来处理数据。

我们将在下一课中讨论如何创建这样的对象。

过程式与面向对象编程示例对比

以下是一个用过程式编程风格编写的简短程序,用于打印动物的名称和腿的数量:

#include <iostream>
#include <string_view>

enum AnimalType
{
    cat,
    dog,
    chicken,
};

constexpr std::string_view animalName(AnimalType type)
{
    switch (type)
    {
    case cat: return "cat";
    case dog: return "dog";
    case chicken: return "chicken";
    default:  return "";
    }
}

constexpr int numLegs(AnimalType type)
{
    switch (type)
    {
    case cat: return 4;
    case dog: return 4;
    case chicken: return 2;
    default:  return 0;
    }
}

int main()
{
    constexpr AnimalType animal{ cat };
    std::cout << "A " << animalName(animal) << " has " << numLegs(animal) << " legs\n";

    return 0;
}

在这个程序中,我们编写了函数,允许我们获取动物的腿数和名称。

虽然这完全可以正常工作,但考虑一下当我们希望更新程序,使动物变成蛇时会发生什么。要将蛇添加到代码中,我们需要修改AnimalTypenumLegs()animalName()。如果这是一个更大的代码库,我们还需要更新所有使用AnimalType的其他函数——如果AnimalType在许多地方被使用,那么可能需要修改大量代码(并且可能会破坏代码)。

现在,让我们用更具面向对象思维的方式来编写相同的程序(产生相同的输出):

#include <iostream>
#include <string_view>

struct Cat
{
    std::string_view name{ "cat" };
    int numLegs{ 4 };
};

struct Dog
{
    std::string_view name{ "dog" };
    int numLegs{ 4 };
};

struct Chicken
{
    std::string_view name{ "chicken" };
    int numLegs{ 2 };
};

int main()
{
    constexpr Cat animal;
    std::cout << "a " << animal.name << " has " << animal.numLegs << " legs\n";

    return 0;
}

在这个例子中,每种动物都是其自己的程序定义类型,该类型管理与该动物相关的一切(在这种情况下,只是跟踪名称和腿的数量)。

现在考虑一下,如果我们将动物更新为蛇,会发生什么。我们只需要创建一个Snake类型并用它替换Cat。几乎不需要修改现有代码,这意味着打破已经工作的代码的风险要小得多。

如上所述,我们的CatDogChicken示例存在大量重复(因为每个都定义了完全相同的成员集合)。在这种情况下,创建一个通用的Animal结构体并为每种动物创建一个实例可能更可取。但如果我们要为Chicken添加一个新成员,而该成员不适用于其他动物(例如wormsPerDay),该怎么办?使用通用的Animal结构体,所有动物都会获得该成员。而在我们的面向对象模型中,我们可以将该成员限制为Chicken对象。

面向对象编程带来的其他好处

在学校里,当你提交编程作业时,你的工作基本上就完成了。你的教授或助教将运行你的代码,看看它是否能产生正确的结果。它要么能,要么不能,然后你会根据结果得到相应的评分。你的代码可能会在那时被丢弃。

另一方面,当你将代码提交到其他开发人员使用的代码库中,或者提交到真实用户使用的应用程序中时,情况就完全不同了。新的操作系统或软件版本可能会破坏你的代码。用户会发现你犯下的逻辑错误。合作伙伴会要求一些新功能。其他开发人员需要在不破坏代码的情况下扩展你的代码。你的代码需要能够进化,可能需要显著地进化,并且需要以最少的时间投入、最少的麻烦和最少的破坏来实现。

解决这些问题的最好方法是尽可能保持代码的模块化(且不冗余)。为此,面向对象编程还带来了一些其他有用的概念:继承封装抽象多态

作者注:语言设计者有一种哲学:永远不要在可以用大词的时候用小词。

另外,为什么“缩写”这个词这么长?

我们会适时地介绍这些概念是什么,以及它们如何帮助你的代码减少冗余,更容易修改和扩展。一旦你熟悉了面向对象编程并且理解了它,你可能就再也不想回到纯过程式编程了。

话虽如此,面向对象编程并没有取代过程式编程——相反,它为你提供了更多的工具,以便在需要时管理复杂性。

关于“对象”一词的说明

请注意,“对象”一词有些多义,这可能会引起一些混淆。在传统编程中,对象是一块用来存储值的内存。仅此而已。在面向对象编程中,“对象”意味着它既是传统编程意义上的对象,又结合了属性和行为。在这些教程中,我们将倾向于使用对象的传统含义,并在特别指代面向对象编程中的对象时,更倾向于使用“类对象”这一术语。

测验时间

问题1

更新上面的动物过程式示例,用蛇替换猫进行实例化。

显示答案
#include <iostream>
#include <string_view>

enum AnimalType
{
    snake,
    dog,
    chicken,
};

constexpr std::string_view animalName(AnimalType type)
{
    switch (type)
    {
    case snake: return "snake";
    case dog: return "dog";
    case chicken: return "chicken";
    default:  return "";
    }
}

constexpr int numLegs(AnimalType type)
{
    switch (type)
    {
    case snake: return 0;
    case dog: return 4;
    case chicken: return 2;
    default:  return 0;
    }
}

int main()
{
    constexpr AnimalType animal{ snake };
    std::cout << "A " << animalName(animal) << " has " << numLegs(animal) << " legs\n";

    return 0;
}

问题2

更新上面的动物面向对象示例,用蛇替换猫进行实例化。

显示答案
#include <iostream>
#include <string_view>

struct Snake
{
    std::string_view name{ "snake" };
    int numLegs{ 0 };
};

struct Dog
{
    std::string_view name{ "dog" };
    int numLegs{ 4 };
};

struct Chicken
{
    std::string_view name{ "chicken" };
    int numLegs{ 2 };
};

int main()
{
    constexpr Snake animal;
    std::cout << "a " << animal.name << " has " << animal.numLegs << " legs\n";

    return 0;
}

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"