C++ Primer Plus 学习笔记(第 10 章 对象和类)

C++ Primer Plus 学习笔记

第 10 章 对象和类

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

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

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

过程性编程和面向对象编程

采用过程性编程方法时,首先要考虑遵循的步骤,然后考虑如何表示这些数据。
采用 OOP 方法,首先从用户的角度考虑对象 —— 描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。最后,使用新的设计方案创建出程序。

抽象和类

抽象是将问题的本质特征抽象出来,并根据特征来描述解决方案。抽象是通往用户定义类型的捷径,在 C++ 中,用户定义类型指的是实现抽象接口的类设计。

类型是什么

指定基本类型完成了三项工作:

  • 决定数据对象需要的内存数量;
  • 决定如何解释内存中的位(longfloat在内存中占用的位数相同,但将它们转换为数值的方法不同);
  • 决定可使用数据对象执行的操作和方法。

对于内置类型来说,有关操作的信息被内置到编译器中。但 C++ 中定义用户自定义的类型时,必须自己提供这些信息。付出这些劳动换来了根据实际需要定制新数据类型的强大功能和灵活性。

C++ 中的类

类是一种将抽象转换为用户定义类型的 C++ 工具,它将数据表示和操纵数据的方法组合成一个整洁的包。
一般来说,类规范由两个部分组成。

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

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

什么是接口

接口是一个共享框架,供两个系统(如在计算机和打印机之间或者用户或计算机程序之间)交互时使用;例如,用户可能是您,而程序可能是字处理器。程序接口将您的意图转换为存储在计算机中的具体信息。
对于类,我们说公共接口。在这里,公众(public)是使用类的程序,交互系统由类对象组成,而接口由编写类的人提供的方法组成。接口让程序员能够编写与类对象交互的代码,从而让程序能够使用类对象。例如,要计算string对象中包含多少个字符,您无需打开对象,而只需使用string类提供的size()方法。类设计禁止公共用户直接访问类,但公众可以使用方法size()。方法size()是用户和string类对象之间的公共接口的组成部分。通常,方法getline()istream类的公共接口的组成部分,使用cin的程序不是直接与cin对象内部交互来读取一行输入,而是使用getline()
如果希望更人性化,不要将使用类的程序视为公共用户,而将编写程序的人视为公共用户。然而,要
使用某个类,必须了解其公共接口;要编写类,必须创建其公共接口。

通常,C++ 程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。
为帮助识别类,本书遵循一种常见但不通用的约定 —— 将类名首字母大写。

// stock00.h -- Stock class interface
// version 00
#ifndef STOCK00_H_
#define STOCK00_H_

#include 

class Stock    // class declaration
{
private:
    std:: string company;
    long shares;
    double share_val;
    double total_val;
    void set_tot() { total_val = shares * 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

首先,C++ 关键字 class指出这些代码定义了一个类设计(不同于模板参数中,在这里,classtypename不是同义词)。这种语法指出,Stock是这个新类的类型名。该声明让我们能够声明Stock类型的变量 —— 称为对象或实例。
接下来,要存储的数据以类数据成员(如companyshares)的形式出现。同样,要执行的操作以类函数成员(方法,如sell()update())的形式出现。成员函数可以就地定义(如set_tot()),也可以用原型表示。其他成员函数的完整定义包含在实现文件中;但对于描述函数接口而言,原型足够了。将数据和方法组合成一个单元是类最吸引人的特性。

访问控制

关键字privatepublic也是新的,它们描述了对类成员的访问控制。使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数(或友元函数)来访问对象的私有成员。因此公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。防止程序直接访问数据被称为数据隐藏。C++ 还提供了第三个访问控制关键字protected
类设计可能将公有接口与实现细节分开。公有接口表示设计的抽象组件。将实现细节放在一起并将它们与抽象分开被称为封装。数据隐藏(将数据放在类的私有部分中)是一种封装,将实现的细节隐藏在私有部分中,就像Stock类对set_tot()所做的那样,也是一种封装。封装的另一个例子是,将类函数定义和类声明放在不同的文件中。

OOP 和 C++

OOP 是一种编程风格,从某种程度说,它用于任何一种语言中。当然,可以将 OOP 思想融合到常规的 C 语言程序中。例如,第 9 章的一个示例中,头文件中包含结构原型和操纵该结构的函数原型,main()函数只需定义这个结构类型的变量,并使用相关函数处理这些变量即可;main()不直接访问结构成员。实际上,该示例定义了一种抽象类型,它将存储格式和函数原型置于头文件中,对main()隐藏了实际的数据表示。然而,C++ 中包括了许多专门用来实现 OOP 方法的特性,因此它使程序员更进一步。首先,将数据表示和函数原型放在一个类声明中(而不是放在一个文件中),通过将所有内容放在一个类声明中,来使描述成为一个整体。其次,让数据表示成为私有,使得数据只能被授权的函数访问。在 C 语言的例子中,如果main()直接访问了结构成员,则违反了 OOP 的精神,但没有违反 C 语言的规则。然而,试图直接访问Stock对象的shares成员便违反了 C++ 语言的规则,编译器将捕获这种错误。

数据隐藏不仅可以防止直接访问数据,还让开发者(类的用户)无需了解数据是如何被表示的。所需要知道的只是各种成员函数的功能;也就是说,需要知道成员函数接受什么样的参数以及返回什么类型的值。原则是将实现细节从接口设计中分离出来。如果以后找到了更好的、实现数据表示或成员函数细节的方法,可以对这些细节进行修改,而无需修改程序接口,这使程序维护起来更容易。

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

无论类成员是数据成员还是成员函数,都可以在类的公有部分或私有部分中声明它。但由于隐藏数据是 OOP 主要目标之一,因此数据项通常放在私有部分,组成类接口的成员函数放在公有部分;否则,就无法从程序中调用这些函数。通常,程序员使用私有成员函数来处理不属于公有接口的实现细节。
不必在类声明中使用关键字private,因为这是类对象的默认访问控制。然而,为强调数据隐藏的概念,本书显式地使用了private

类和结构

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

实现类成员函数

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

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

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

void Stock::update(double price)

这种表示法意味着我们定义的update()函数是Stock类的成员。这不仅将update()标识为成员函数,还意味着我们可以将另一个类的成员函数也命名为update()
因此,作用域解析操作符确定了方法定义对应类的身份。我们说,标识符update()具有类作用域(class scope)。Stock类的其他成员函数不必使用作用域解析操作符,就可以使用update()方法,这是因为它们属于同一个类,因此update()是可见的。然而,在类声明和方法定义之外使用update()时,需要采取特殊的措施。
类方法的完整名称中包括类名。我们说,Stock::update()是函数的限定名;而简单的update()是全名的缩写,它只能在类作用域中使用。
方法的第二个特点是,方法可以访问类的私有成员。

// stock00.cpp -- implementing the Stock class
// version 00
#include 
#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()
{
    using std::cout;
    using std::ios_base;
    // set format to #.###
    ios_base::fmtflags orig =
        cout.setf(ios_base::fixed, ios_base::floatfield);
    std::streamsize prec = cout.precision(3);
    
    cout << "Company: " << company
              << " Shares: " << shares << '\n';
    cout << " Share price: $" << share_val;
    // set format to #.##
    cout.precision(2);
    cout << " Total Worth: $" << total_val << '\n';
    
    // restore original format
    cout.setf(orig, ios_base::floatfield);
    cout.precision(prec);
}
成员函数说明

acquire()函数管理对某个公司股票的首次购买,而buy()sell()管理增加或减少持有的股票。方法buy()sell()确保买入或卖出的股数不为负。另外,如果用户试图卖出超过他持有的股票数量,则sell()函数将结束这次交易。这种使数据私有并限于对公有函数访问的技术允许我们能够控制数据如何被使用;在这个例子中,它允许我们加入这些安全防护措施,避免不适当的交易。
4个成员函数设置或重新设置了total_val成员值。这个类并非将计算代码编写 4 次,而是让每个
函数都调用set_tot()函数。由于set_tot()只是实现代码的一种方式,而不是公有接口的组成部分,因此这个类将其声明为私有成员函数(即编写这个类的人可以使用它,但编写代码来使用这个类的人不能使用)。如果计算代码很长,则这种方法还可以省去许多输入代码的工作,并可节省空间。然而,这种方法的主要价值在于,通过使用函数调用,而不是每次重新输入计算代码,可以确保执行的计算完全相同。另外,如果必须修订计算代码(在这个例子中,这种可能性不大),则只需在一个地方进行修改即可。

内联方法

其定义位于类声明中的函数都将自动成为内联函数,因此Stock::set_tot()是一个内联函数。类声明常将短小的成员函数作为内联函数,set_tot()符合这样的要求。
如果愿意,也可以在类声明之外定义成员函数,并使其成为内联函数。为此,只需在类实现部分中定
义函数时使用inline限定符即可:

class Stock
{
private:
    void set_tot();    // definition kept separate
public:
    ...
};

inline void Stock::set_tot ()    // use inline in definition
{
    total_val = shares * share_val;
}

内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。确保内联定义对多文件程序中的
所有文件都可用的、最简便的方法是:将内联定义放在定义类的头文件中(有些开发系统包含智能链接程序,允许将内联定义放在二个独立的实现文件)。
顺便说一句,根据改写规则(rewrite rule),在类声明中定义方法等同于用原型替换方法定义,然后在类声明的后面将定义改写为内联函数。也就是说,set_tot()的内联定义与上述代码(定义
紧跟在类声明之后)是等价的。

方法使用哪个对象

创建对象最简单的方法是声明类变量,使用对象的成员函数,和使用结构成员一样,通过成员运算符:

Stock kate, joe;
kate.show();
joe.show();

第 1 条语句调用kate对象的show()成员。这意味着show()方法将把shares解释为kate.shares,将share_val解释为kate.share_val。同样,函数调用joe.show()使show()方法将sharesshare_val分别解释为joe.sharejoe.share_val
注意:调用成员函数时,它将使用被用来调用它的对象的数据成员。
同样,函数调用kate.sell()在调用set_tot()函数时,相当于调用kate.set_tot(),这样该函数将使用kate对象的数据。
所创建的每

你可能感兴趣的:(c++,学习,开发语言)