抽象和类(C++)

面向对象编程(OOP)是一种特殊的、设计程序的概念性方法,C++通过一些特性改进了C语言,使得应用这种方法更容易。下面是最重要的OOP特性:

  • 抽象;
  • 封装和数据隐藏;
  • 多态;
  • 继承;
  • 代码的可重用性。

为了实现这些特性并将它们组合在一起,C++所做的最重要的改进是提供了类。

抽象和类:

C++中的类:

类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包。下面来看一个表示股票的类。
首先,必须考虑如何表示股票。可以将一股作为基本单元,定义一个表示一股股票的类。然而,这意味着需要100个对象才能表示100股,这不现实。相反,可以将某人当前持有的某种股票作为一个基本单元,数据表述中包含他持有的股票数量。一种比较现实的方法是,必须记录最初购买价格和购买日期(用于计算纳税)等内容。另外,还必须管理诸如如拆股等事件。首次定义类就考虑这么多因素有些困难,因此我们对其进行简化。具体地说,应该将可执行的操作限制为:

  • 获得股票;
  • 增持;
  • 卖出股票;
  • 更新股票价格;
  • 显示关于所持股票的信息。

可以根据上述清单定义stock类的共有接口。为支持该接口,需要存储一些信息。我们再次进行简化。例如,不考虑标准的美式股票计价方式。我们将存储下面的信息:

  • 公司名称;
  • 所持股票的数量;
  • 每股的价格;
  • 股票总值。

接下来定义类。一般来说,类规范有两个部分组成。

  • 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述共有接口。
  • 类方法定义:描述如何实现类成员函数。

简单的说,类声明提供了类的蓝图,而方法定义则提供了细节。

接口是一个共享框架,供两个系统交互时使用;例如,用户可能是您,而程序可能是字处理器。使用字处理器时,您不能直接将脑子中想到的词传输到计算机内存中,而必须同程序提供的接口交互。
对于类,我们说公共接口。在这里,公共(public)时使用类的程序,交互系统由类对象组成,而接口由编写类的人提供的方法组成。接口让程序员能够编写与类对象交互的代码,从而让程序能够使用类对象。

为开发一个类并编写一个使用它的程序,需要完成多个步骤。通常,C++程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。

stock00.h

#pragma once
#ifndef STOCK00_H_
#define STOCK00_H_

#include<string>

class Stock {   //class declaration
private:
    std::string company;
    long shares;
    double share_val;
    double total_val;
    void set_tot(){
        total_val = share * share_val;
    }
public:
    void acquire(const std::string & co, long n, double pr);
    void buy(long num, double price);
    void sell(long num, double price);
    void update(double price);
    void show();
};  //note semicolon at the end


#endif // !STOCK00_H_

首先,C++关键字class指出这些代码定义了一个类设计(不同于在模板参数中,在这里,关键字class和typename不是同义词,不能使用typename代替class)。这种语法指出,Stock是这个新类的类型名。该声明让我们能够声明Stock类型的变量——称为对象或实例。每个对象都表示一支股票。例如:

Stock sally;
Stock solly;

sally对象可以表示Sally持有的某公司股票。
接下来,要存储的数据以类数据成员(如company和shares)的形式出现。同样,要执行的操作以类函数成员的形式出现。成员函数可以就地定义(如set_tot()),也可以用原型表示(如其他成员函数)。

访问控制:

关键字private和public描述了对类成员的访问控制。使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数来访问对象的私有成员。例如,要修改Stock类的shares成员,只能通过Stock的成员函数。因此,公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。

类设计尽可能将公有接口与实现细节分开。公有接口表示设计的抽象组件。将实现细节放在一起并将它们与抽象分开被称为封装。数据隐藏(将数据放在类的私有部分中)是一种封装,将实现的细节隐藏在私有部分中,就像Stock类对set_tot()所做那样,也是一种封装。封装的另一个例子是,将类函数定义和类声明放在不同的文件中。
数据隐藏不仅可以防止直接访问数据,还让开发者(类的用户)无需了解数据是如何被表示的。

控制对成员的访问:共有还是私有:

无论类成员是数据成员还是成员函数,都可以在类的公有部分或私有部分中声明它。但由于隐藏数据是OOP主要的目标之一,因此数据项通常放在私有部分,组成类接口的成员函数放在公有部分中。不能直接从程序中调用这种函数,但公有方法却可以使用它们。通常,程序员使用私有成员函数来处理不属于公有接口的实现细节。

不必在类声明中使用关键字private,因为这是类对象的默认访问控制:

class World {
    float mass; //private by default
    char name[20];  //private by default
public:
    void tellall(void);
    ...
};

类和结构:类描述看上去很像是包含成员函数以及public和private可见性标签的结构声明。实际上,C++对结构进行了扩展,使之具有与类相同的特性。它们之间唯一的区别是,结构的默认访问类型是public,而类为private。C++程序员通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象。

实现类成员函数:

还需要创建类描述的第二部分:为那些由类声明中的原型表示的成员函数提供代码。成员函数定义与常规函数定义非常相似,它们有函数头和函数体,也可以有返回类型和参数。但是它们还有两个特殊的特征:

  • 定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类;
  • 类方法可以访问类的private组件。

首先,成员函数的函数头使用作用域运算符解析(::)来指出函数所属的类。例如,update()成员函数的函数头如下:

void Stock::update(double price)

这不仅将update()标识为成员函数,还意味着我们可以将另一个类的成员函数也命名为update()。例如,Buffoon()类的update()函数的函数头如下:

void Buffoon::update()

因此,作用于解析运算符确定了方法定义对应的类的身份。我们说,标识符update()具有类作用域(class scope)。Stock类的其他成员函数不必使用作用域解析运算符,就可以使用update()方法,这是因为它们属于同一个类,因此update()是可见的。

类方法的完整名称中包括类名。我们说,Stock::update()是函数的限定名;而简单的update()是全名的缩写(非限定名),它只能在类作用域中使用。

方法的第二个特点是:方法可以访问类的私有成员。

stock00.cpp

#include<iostream>
#include"stock00.h"

void Stock::acquire(const std::string & co, long n, double pr) {
    company = co;
    if (n < 0) {
        std::cout << "Number of shares can't be negative; " << company << " shares set to 0.\n";
        shares = 0;
    }
    else
    {
        shares = n;
    }
    share_val = pr;
    set_tot();
}

void Stock::buy(long num, double price) {
    if (num < 0) {
        std::cout << "Number of shares purchased can't be negative." << "Transaction is aborted.\n";
    }
    else
    {
        shares += num;
        share_val = price;
        set_tot();
    }
}

void Stock::sell(long num, double price) {
    using std::cout;
    if (num < 0) {
        cout << "Number of shares sold can't be negative. " << "Transaction is aborted.\n";
    }
    else if (num > shares) {
        cout << "You can't sell more than you have! " << "Transaction is aborted.\n";
    }
    else
    {
        shares -= num;
        share_val = price;
        set_tot();
    }
}

void Stock::update(double price) {
    share_val = price;
    set_tot();
}


void Stock::show() {
    std::cout << "Company: " << company
        << "  Shares: " << shares << '\n'
        << "  Share Price: $" << share_val
        << "  Total Worth: $" << total_val << '\n';
}

acquire()函数管理对某个公司股票的首次购买,而buy()和sell()管理增加或减少持有的股票。

内联方法:其定义位于类声明中的函数都将自动称为内联函数,因此Stock::set::tot()是一个内联函数。类声明常将短小的成员函数作为内联函数。

如果愿意,也可以再类声明之外定义成员函数,并使其称为内联函数。为此,只需再类实现部分中定义函数时使用inline限定符即可:

inline void Stock::set_tot() {
    total_val = shares * share_val;
}

内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。确保内联定义对多文件程序中的所有文件都可用的、最简便的方法是:将内联定义放在定义类的头文件中。

创建对象:

Stock kate, joe;

这将创建两个Stock类对象,一个为kate,另一个为joe。
接下来,看看如何使用对象的成员函数。和使用结构成员一样,通过成员运算符:

kate.show();
joe.show();

所创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法,即每种方法只有一个副本。

使用类:

usestock00.cpp

#include<iostream>
#include"stock00.h"
int main() {
    Stock fluffy_the_cat;
    fluffy_the_cat.acquire("NanoSmart", 20, 12.50);
    fluffy_the_cat.show();
    fluffy_the_cat.buy(15, 18.125);
    fluffy_the_cat.show();
    fluffy_the_cat.sell(400, 20.00);
    fluffy_the_cat.show();
    fluffy_the_cat.buy(300000, 40.125);
    fluffy_the_cat.show();
    fluffy_the_cat.sell(300000, 0.125);
    fluffy_the_cat.show();
    return 0;
}

结果:
结果

C++的目标是使得使用类与使用基本的内置类型(如int和char)尽可能相同。要创建类对象,可以声明类变量,也可以使用new为类对象分配存储空间。可以将对象作为函数的参数和返回值,也可以将一个对象赋给另一个。

客户/服务器模型:OOP程序员常依照客户/服务器模型来讨论程序设计。在这个概念中,客户是使用类的程序。类声明(包括类方法)构成了服务器,它是程序可以使用的资源。客户只能通过以公有方式定义的接口使用服务器,这意味着客户(客户程序员)唯一的责任是了解该接口。服务器(服务器设计人员)的责任是确保服务器根据该接口可靠并准确地执行。服务器设计人员只能修改类设计的实现细节,而不能修改接口。这样程序员独立地对客户和服务器进行改进,对服务器的修改不会客户的行为造成意外的影响。

小结:

指定类设计的第一步是提供类声明。类声明类似结构声明,可以包括数据成员和函数成员。声明有私有部分,在其中声明的成员只能通过成员函数进行访问;声明还有公有部分,在其中声明的成员可被使用类对象的程序直接访问。通常,数据成员被放在私有部分中,成员函数被放在公有部分中。
共有部分的内容构成了设计的抽象部分——公有接口。将数据封装到私有部分中可以保护数据的完整性,这被称为数据隐藏。因此,C++通过类使得实现抽象、数据隐藏和封装等OOP特性很容易。

指定类设计的第二步是实现类成员函数。可以在类声明中提供完整的函数定义,而不是函数原型,但是通常的做法是单独提供函数定义(除非函数很小)。在这种情况下,需要使用作用域解析运算符来指出成员函数属于哪个类。

声明:以上整理自个人理解和Stephen Prata 著的《C++ Primer Plus》

猜你喜欢

转载自blog.csdn.net/MoooLi/article/details/82747255