【C++ Primer Plus】第10章 对象和类

最重要的OOP特性: 抽象; 封装和数据隐藏; 多态; 继承; 代码的可重用性。

10.1 过程性编程OPP和面向对象编程OOP

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

10.2 抽象和类

抽象 是通往用户定义类型的捷径,在C++中,用户定义类型指的是实现抽象接口的类设计。
是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包。

表示股票的类:
操纵方法:获得股票;增持;卖出股票;更新股票价格;显示关于所持股票的信息。
数据表示:公司名称;所持股票的数量;每股的价格;股票总值。

一般来说,类规范由两个部分组成:

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

类声明和成员函数的定义:

  1. 通常,C++程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。
  2. 头文件中,一种常见但不通用的约定——将类名首字母大写。
  3. 头文件中,数据项通常放在私有部分private:(这个关键字可以省略),组成类接口的成员函数放在公有部分public:
  4. 头文件中,将const关键字放在函数的括号后面,保证函数不会修改调用对象。
  5. 使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数(或友元函数)来访问对象的私有成员。
  6. 定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类;void Stock::update(double price)
  7. 类成员函数(方法)可通过类对象来调用。为此,需要使用成员运算符句点(.)
  8. 头文件中,其定义位于类声明中的函数都将自动成为内联函数。
  9. 头文件中,在类声明外面定义成员函数,并且加上inline,也可使其称为内联函数。注意此时函数名也需要加上(::)。
  10. 可以将一个对象赋给同类型的另一个对象。
  11. 类和结构体: C++程序员通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象

指定类设计的第一步:提供类声明

  1. 类声明类似结构声明,可以包括数据成员和函数成员。
  2. 声明有私有部分,在其中声明的成员只能通过成员函数进行访问;
  3. 将数据封装到私有部分中可以保护数据的完整性,这被称为数据隐藏。
  4. 声明还具有公有部分,在其中声明的成员可被使用类对象的程序直接访问。
  5. 公有部分的内容构成了设计的抽象部分——公有接口。

stock00.h // 类的声明头文件

// 类的声明:类的变量和成员函数,数据和操纵数据的方法
#ifndef PRIMERPLUS_STOCK00_H
#define PRIMERPLUS_STOCK00_H
#include <string>
class Stock // 首字母一般大写
{
    
    
private:    // 这个关键字可以省略,防止其他源代码访问里面的数据,凡是在private里面的数据,只有public里面的方法可以调用。
    std::string company;    // 成员
    long shares;
    double share_val;
    double total_val;
    void set_total() {
    
    total_val = shares * share_val;} // 内联函数
public:     // 这个关键字不能省略

    Stock();    // 默认构造函数,在声明和定义时不加形参,但是定义时给每个成员初始化
                // 函数重载,自定义构造函数,没有返回值,部分形参使用默认参数
    Stock(const std::string &co, long n = 1, double pr = 1.0);
    ~Stock();                           // 析构函数的声明
    void buy(long num, double price);   // 成员函数
    void sell(long num, double price);
    void update(double price);
    void show() const;                  // const成员函数,保证函数不会修改调用对象
    const Stock & topval(const Stock &s) const;
	const string &company_name() const {
    
    return company;}	// 返回公司的名字且不希望被修改
};
#endif //PRIMERPLUS_STOCK00_H

指定类设计的第二步:实现类成员函数

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

stock00.cpp // 类的定义源文件

// 定义类的成员函数
#include <iostream>
#include "stock00.h"
using namespace std;
Stock::Stock()  // 默认构造函数
{
    
    
    company = "stock";
    shares = 0;
    share_val = 0.0;
    set_total();
}

Stock::Stock(const std::string &co, long n, double pr)  // 自定义构造函数
{
    
    
    company = co;
    if (n<0)
    {
    
    
        cout << "Number of shares can't be negative; "
             << company << " shares set to be 0." << endl;
        shares = 0;
    }
    else
        shares = n;
    share_val = pr;
    set_total();
}

Stock::~Stock() // 析构函数定义,没有参数,没有返回值,自动调用
{
    
    
    cout << "Bye " << company << endl;  // 类储存是以栈的方式:先进后出,后进先出
}

void Stock::buy(long num, double price)
{
    
    
    if (num < 0)
        cout << "Number of shares can't be negative; " << endl;
    else
    {
    
    
        shares += num;
        share_val = price;
        set_total();
    }
}

void Stock::sell(long num, double price)
{
    
    
    if (num < 0)
        cout << "Number of shares can't be negative; " << endl;
    else if (num > shares)
        cout << "You can't sell more than you have!" << endl;
    else
    {
    
    
        shares -=num;
        share_val = price;
        set_total();
    }
}

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

void Stock::show() const
{
    
    
    cout << "Company : " << company << endl;
    cout << "Shares : " << shares << endl;
    cout << "Share price : " << share_val << endl;
    cout << "Total worth : " << total_val << endl;
}

const Stock & Stock::topval(const Stock &s) const
{
    
    
    if (s.total_val > total_val)    // total = this->total_val
        return s;
    else
        return *this;   // this指针指向调用成员函数的对象,this是该对象的地址。
}

指定类设计的第三步:创建类对象和类方法的使用

  1. 创建对象(类的实例),只需将类名视为类型名即可;
  2. 类成员函数(方法)可通过类对象来调用。为此,需要使用成员运算符句点(.)。

usestock00.cpp // 类的使用源文件

#include <iostream>
#include "stock00.h"
using namespace std;
int main(void)
{
    
    
    {
    
    
        Stock wind1 = Stock{
    
    "wind1"};       // 显示调用构造函数
        Stock wind2{
    
    "wind2", 20, 20.2};     // 隐式调用构造函数
        Stock wind3;                        // 自动调用默认构造函数
        wind3 = wind2;                      // 可以将一个对象赋给同类型的另一个对象
        wind3 = Stock("wind3", 30, 30.3);   // 构造函数可对已存在的对象赋值
        const Stock wind4 = Stock{
    
    "wind4"}; // wind4只能调用const成员函数,show()

        Stock top;
        top = wind1.topval(wind2);
        top.show();

        const int STKS = 4;
        Stock mystuff[STKS];                // 创建对象数组,自动调用默认构造函数
        Stock sand[STKS] = {
    
                    // 创建对象数组,对不同的元素使用不同的构造函数
                Stock("sand1", 11, 11.1),   // 用括号括起,以逗号分割
                Stock(),                    // 使用默认构造函数
                Stock("sand3", 33)          // 只初始化了三个元素,剩下的自动调用默认构造函数
        };
        int i;
        for (i=0; i<STKS; i++)
            sand[i].show();
        const Stock *topsand = &sand[0];    // 定义一个类的const指针
        for (i=1; i<STKS; i++)
            topsand = &(topsand->topval(sand[i]));
        topsand->show();                    // 显示价格最高的一个对象

//        Stock fluffy_the_cat;
//        fluffy_the_cat.acquire("Mooncake", 20, 12.5);
//        fluffy_the_cat.show();    		// 通过类的对象来访问类的公有成员函数
//        fluffy_the_cat.buy(15, 18.125);
//        fluffy_the_cat.show();
//        fluffy_the_cat.sell(400, 20.0);
//        fluffy_the_cat.update(15.2);
//        fluffy_the_cat.show();

    }   // 添加这个大括号后,析构函数调用将在到达返回语句前执行。
    return 0;
}

10.3 类的构造函数和析构函数

10.3.1 构造函数

为什么需要构造函数?数据部分的访问状态是私有的,这意味着程序不能直接访问数据成员。程序只能通过成员函数来访问数据成员,因此需要设计合适的成员函数,才能成功地将对象初始化。C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值赋给它们的数据成员。

  1. 用类创建对象时,自动调用构造函数。
  2. 构造函数的函数名和类的名称相同。通过函数重载,可以创建多个同名的构造函数。
  3. 接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值。
  4. 构造函数的原型和函数头有一个有趣的特征——虽然没有返回值,但没有被声明为void类型。实际上,构造函数没有声明类型。
  5. 构造函数的参数表示的不是类成员,而是赋给类成员的值。因此,参数名不能与类成员相同。
  6. 无法使用对象来调用构造函数,因为在构造函数构造出对象之前,对象是不存在的。因此构造函数被用来创建对象,而不能通过对象来调用。
  7. 默认构造参数可以有形参(有的话必须全都有初始化的默认参数),也可以没有。
// 默认构造函数,在定义时不加形参,但是给每个成员初始化
Stock();   	
Stock::Stock() {
    
    ...}	// Stock类的默认构造函数的定义,没有形参,但是给每个成员初始化

// 函数重载,自定义构造函数,没有返回值,部分形参使用默认参数
Stock(const std::string &co, long n=1, double pr=1.0); 	
Stock::Stock(const std::string &co, long n, double pr) {
    
    ...} // Stock类的构造函数的定义
    
Stock fluffy_the_cat = Stock{
    
    "Mooncake"};   // 显示调用自定义构造函数
Stock garment{
    
    "apple", 30, 123.45};         // 隐式调用自定义构造函数
Stock first;                                // 自动调用默认构造函数

10.3.3 析构函数

  1. 用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数——析构函数。
  2. 析构函数完成清理工作:如果构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存。
  3. 析构函数的原型:名称在类名前加上~。没有返回值和声明类型。没有参数。Stock类的析构函数原型~Stock();
  4. 如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。
  5. 类的自动变量储存是以栈的方式,则调用析构函数释放的时候先进后出,后进先出。
  6. 如果构造函数使用了new,则必须提供使用delete的析构函数。

析构函数何时被调用:

  1. 如果创建的是静态存储类对象,则其析构函数将在程序结束时自动被调用。
  2. 如果创建的是自动存储类对象(就像前面的示例中那样),则其析构函数将在程序执行完代码块时(该对象是在其中定义的)自动被调用。
  3. 如果对象是通过new创建的,则它将驻留在栈内存或自由存储区中,当使用delete来释放内存时,其析构函数将自动被调用。
  4. 最后,程序可以创建临时对象来完成特定的操作,在这种情况下,程序将在结束对该对象的使用时自动调用其析构函数。

10.4 this指针

this指针指向调用成员函数的对象,this是该对象的地址。

比较两个对象中哪个total_val值大并返回大的对象:

  1. 如何将两个要比较的对象提供给成员函数呢?如果希望该方法对两个对象进行比较,则必须将第二个对象作为参数传递给它。出于效率方面的考虑,可以按引用来传递参数。
  2. 如何将方法的答案传回给调用程序呢?最直接的方法是让方法返回一个引用,该引用指向股价总值较高的对象。

函数原型:const Stock & topval(const Stock & s) const;
函数调用:top = stock1.topval(stock2); // top也是一个对象
函数定义:

const Stock & Stock::topval(const Stock & s) const
{
    
    
	if (s.total_val > total_val)	// total = this->total_val
        return s;		// argument object
	else
		return *this; 	// this 指针指向调用对象
}
  1. 该函数隐式地访问一个对象stock1,而显式地访问另一个对象stock2,并返回其中一个对象的引用。
  2. 括号中的const表明,该函数不会修改被显式地访问的对象stock2;
  3. 括号后的const表明,该函数不会修改被隐式地访问的对象stock1。
  4. 由于该函数返回了两个const对象之一的引用,因此返回类型也应为const引用。

10.5 对象和数组

const int STKS = 4;
Stock mystuff[STKS];                // 创建对象数组,自动调用默认构造函数
Stock sand[STKS] = {
    
                    // 创建对象数组,对不同的元素使用不同的构造函数
        Stock("sand1", 11, 11.1),   // 用括号括起,以逗号分割
        Stock(),                    // 使用默认构造函数
        Stock("sand3", 33)          // 只初始化了三个元素,剩下的自动调用默认构造函数
};
int i;
for (i=0; i<STKS; i++)
    sand[i].show();
const Stock *topsand = &sand[0];    // 定义一个类的const指针
for (i=1; i<STKS; i++)
    topsand = &(topsand->topval(sand[i]));
topsand->show();                    // 显示价格最高的一个对象

10.6 类作用域

  1. 在类中定义的名称(如类数据成员名和类成员函数名)的作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。
  2. 在不同类中使用相同的类成员名而不会引起冲突。
  3. 不能从外部直接访问类的成员,要调用公有成员函数,必须通过对象。
  4. 在定义成员函数时,必须使用作用域解析运算符(::)
  5. 在类声明或成员函数定义中,可以使用未修饰的成员名称。

作用域为类的常量

声明类只是描述了对象的形式,并没有创建对象。因此,在创建对象前,将没有用于存储值的空间。尽管是一个const变量。
第一种方式是在类中声明一个枚举:enum {Month = 12}; // 只是为了创建符号常量,Months只是一个符号名称,不需要枚举名。作用域为整个类。
第二种方式是在类中使用关键字static: static const int Month = 12; // 静态全局变量,作用域为整个类。

10.7 抽象数据类型(abstract data type,ADT)

ADT以通用的方式描述数据类型,而没有引入语言或实现细节。

例如,通过使用栈,可以以这样的方式存储数据,即总是从堆顶添加或删除数据。
例如,C++程序使用栈来管理自动变量。当新的自动变量被生成后,它们被添加到堆顶;消亡时,从栈中删除它们。

例程:用类的成员函数实现对栈的操作:。。。。。。编程练习5

10.8 总结

  1. 面向对象编程强调的是程序如何表示数据。使用OOP方法解决编程问题的第一步是根据它与程序之间的接口来描述数据,从而指定如何使用数据。然后,设计一个类来实现该接口。一般来说,私有数据成员存储信息,公有成员函数(又称为方法)提供访问数据的唯一途径。类将数据和方法组合成一个单元,其私有性实现数据隐藏。
  2. 通常,将类声明分成两部分组成,这两部分通常保存在不同的文件中。类声明(包括由函数原型表示的方法)应放到头文件中。定义成员函数的源代码放在方法文件中。这样便将接口描述与实现细节分开了。从理论上说,只需知道公有接口就可以使用类。当然,可以查看实现方法(除非只提供了编译形式),但程序不应依赖于其实现细节,如知道某个值被存储为int。只要程序和类只通过定义接口的方法进行通信,程序员就可以随意地对任何部分做独立的改进,而不必担心这样做会导致意外的不良影响。
  3. 类是用户定义的类型,对象是类的实例。这意味着对象是这种类型的变量,例如由new按类描述分配的内存。C++试图让用户定义的类型尽可能与标准类型类似,因此可以声明对象、指向对象的指针和对象数组。可以按值传递对象、将对象作为函数返回值、将一个对象赋给同类型的另一个对象。如果提供了构造函数,则在创建对象时,可以初始化对象。如果提供了析构函数方法,则在对象消亡后,程序将执行该函数。
  4. 每个对象都存储自己的数据,而共享类方法。如果mr_object是对象名,try_me( )是成员函数,则可以使用成员运算符句点调用成员函数:mr_object.try_me( )。在OOP中,这种函数调用被称为将try_me消息发送给mr_object对象。在try_me( )方法中引用类数据成员时,将使用mr_object对象相应的数据成员。同样,函数调用i_object.try_me( )将访问i_object对象的数据成员。
  5. 如果希望成员函数对多个对象进行操作,可以将额外的对象作为参数传递给它。如果方法需要显式地引用调用它的对象,则可以使用this指针。由于this指针被设置为调用对象的地址,因此*this是该对象的别名。
  6. 类很适合用于描述ADT。公有成员函数接口提供了ADT描述的服务,类的私有部分和类方法的代码提供了实现,这些实现对类的客户隐藏。

10.9 复习题

  1. 什么是类?
    类是用户定义的类型的定义。类声明指定了数据将如何存储,同时指定了用来访问和操纵这些数据的方法(类成员函数)。
  2. 类如何实现抽象、封装和数据隐藏?
    类表示人们可以类方法的公有接口对类对象执行的操作,这是抽象。
    类的数据成员可以是私有的(默认值),这意味着只能通过成员函数来访问这些数据,这是数据隐藏。
    实现的具体细节(如数据表示和方法的代码)都是隐藏的,这是封装。
  3. 对象和类之间的关系是什么?
    类定义了一种类型,包括如何使用它。
    对象是一个变量或其他数据对象(如由new生成的),并根据类定义被创建和使用。
    类和对象之间的关系同标准类型与其变量之间的关系相同。
  4. 除了是函数之外,类函数成员与类数据成员之间的区别是什么?
    如果创建给定类的多个对象,则每个对象都有其自己的数据内存空间;
    但所有的对象都使用同一组成员函数(通常,方法是公有的,而数据是私有的,但这只是策略方面的问题,而不是对类的要求)。
  5. 类构造函数在何时被调用?类析构函数呢?
    在创建类对象或显式调用构造函数时,类的构造函数都将被调用。当对象过期时,类的析构函数将被调用。
  6. 什么是默认构造函数,拥有默认构造函数有何好处?
    默认构造函数是没有参数或所有参数都有默认值的构造函数。
    拥有默认构造函数后,可以声明对象,而不初始化它,即使已经定义了初始化构造函数。它还使得能够声明数组。
  7. this和*this是什么?
    this指针是类方法可以使用的指针,它指向用于调用方法的对象。因此,this是对象的地址,*this是对象本身。

10.10 编程练习

5、编写一个程序,它从栈中添加和删除customer结构(栈用Stack类声明表示)。每次customer结构被删除时,其payment的值都被加入到总数中,并报告总数。注意:应该可以直接使用Stack类而不作修改;只需修改typedef声明,使Item的类型为customer,而不是unsigned long即可。

(下面的程序包含第八题的题意)
p5.h

#ifndef PRIMERPLUS_P5_H
#define PRIMERPLUS_P5_H
#include <iostream>
using namespace std;
struct customer
{
    
    
    char fullname[35];
    double payment;
};
typedef customer Item;         // 起别名,为存放不同的数据类型
void visit_item(Item &item);
class Stack
{
    
    
private:                // 私有部分放成员变量
    enum {
    
    MAX = 10};    // 枚举类型的符号常量
    Item items[MAX];    // holds stack items
    int top;            // 顶部堆栈项的索引,栈顶指针
public:
    Stack();                // 默认构造函数
    bool isempty() const;   // 判断是否为空
    bool isfull() const;    // 判断是否满了
    // push() returns false if stack already is full, true otherwise
    bool push(const Item & item);   // 入栈
    // pop() returns false if stack already is empty, true otherwise
    bool pop(Item & item);          // 出栈

    void visit(void (*pf)(Item &)); // 访问数据项以及执行操作
    // pf指向一个将Item引用作为参数的函数(不是成员函数)
    // visit( )函数将该函数用于列表中的每个数据项。
};
#endif //PRIMERPLUS_P5_H

p5.cpp

#include "p5.h"

Stack::Stack() // create an empty stack
{
    
    
    top = 0;            // 初始化栈顶指针
}

bool Stack::isempty() const
{
    
    
    return top == 0;    // 是否等于最底层
}

bool Stack::isfull() const
{
    
    
    return top == MAX;  // 是否等于最高层
}

bool Stack::push(const Item & item)
{
    
    
    if (top < MAX)      // 入栈条件
    {
    
    
        items[top++] = item;
        return true;
    }
    else
        return false;
}

bool Stack::pop(Item & item)
{
    
    
    if (top > 0)
    {
    
    
        item = items[--top];
        return true;
    }
    else
        return false;
}

void Stack::visit(void (*pf)(Item &))
{
    
    
    for (int i=0; i<top; i++)
        pf(items[i]);
}

void visit_item(Item &item)
{
    
    
    cout << "fullname:" << item.fullname << endl;
    cout << "payment:" << item.payment << endl;
}

usep5.cpp

#include <iostream>
#include <cctype>   // or ctype.h
#include "p5.h"
int main()
{
    
    
    using namespace std;
    Stack st;       // create an empty stack
    char ch;
    customer cust;
    double sum = 0.0;
    cout << "Please enter A/a to add a purchase order, "
         << "P/p to process a PO, or Q/q to quit.\n";
    while (cin >> ch && toupper(ch) != 'Q')
    {
    
    
        while (cin.get() != '\n')   // 消耗回车
            continue;
        if (!isalpha(ch))
        {
    
    
            cout << '\a';
            continue;
        }
        switch(ch)
        {
    
    
            case 'A':
            case 'a':   cout << "Enter a customer's fullname you want to push to stack (string):";
                        cin.getline(cust.fullname, 35);
                        cout << "Enter a customer's payment (double):";
                        cin >> cust.payment;
                        if (st.isfull())
                            cout << "stack already full\n";
                        else
                        {
    
    
                            st.push(cust);
                            st.visit(visit_item);   // 显示
                        }
                        break;
            case 'P':
            case 'p':   if (st.isempty())
                            cout << "stack already empty\n";
                        else
                        {
    
    
                            st.pop(cust);
                            sum += cust.payment;
                            cout << cust.fullname << " is popped\n";
                            cout << cust.payment << " is popped\n";
                            cout << "sum panyment :" << sum << endl;
                        }
                        break;
        }
        cout << "Please enter A/a to add a purchase order, "
             << "P/p to process a PO, or Q/q to quit.\n";
    }
    cout << "Bye\n";
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_39751352/article/details/126808697