最重要的OOP特性: 抽象; 封装和数据隐藏; 多态; 继承; 代码的可重用性。
10.1 过程性编程OPP和面向对象编程OOP
- 采用过程性编程方法时,首先考虑要遵循的步骤,然后考虑如何表示这些数据。
- 采用OOP方法时,首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。最后,使用新的设计方案创建出程序。
10.2 抽象和类
抽象 是通往用户定义类型的捷径,在C++中,用户定义类型指的是实现抽象接口的类设计。 类 是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包。
表示股票的类: 操纵方法:获得股票;增持;卖出股票;更新股票价格;显示关于所持股票的信息。 数据表示:公司名称;所持股票的数量;每股的价格;股票总值。
一般来说,类规范由两个部分组成:
- 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口。
- 类方法定义:描述如何实现类成员函数(类的接口)。
类声明和成员函数的定义:
- 通常,C++程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。
- 头文件中,一种常见但不通用的约定——将类名首字母大写。
- 头文件中,数据项通常放在私有部分
private: (这个关键字可以省略),组成类接口的成员函数放在公有部分public: - 头文件中,将const关键字放在函数的括号后面,保证函数不会修改调用对象。
- 使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数(或友元函数)来访问对象的私有成员。
- 定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类;
void Stock::update(double price) - 类成员函数(方法)可通过类对象来调用。为此,需要使用成员运算符句点(.)
- 头文件中,其定义位于类声明中的函数都将自动成为内联函数。
- 头文件中,在类声明外面定义成员函数,并且加上inline,也可使其称为内联函数。注意此时函数名也需要加上(::)。
- 可以将一个对象赋给同类型的另一个对象。
- 类和结构体: C++程序员通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象
指定类设计的第一步:提供类声明
- 类声明类似结构声明,可以包括数据成员和函数成员。
- 声明有私有部分,在其中声明的成员只能通过成员函数进行访问;
- 将数据封装到私有部分中可以保护数据的完整性,这被称为数据隐藏。
- 声明还具有公有部分,在其中声明的成员可被使用类对象的程序直接访问。
- 公有部分的内容构成了设计的抽象部分——公有接口。
stock00.h // 类的声明头文件
#ifndef PRIMERPLUS_STOCK00_H
#define PRIMERPLUS_STOCK00_H
#include <string>
class Stock
{
private:
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 Stock & topval(const Stock &s) const;
const string &company_name() const {return company;}
};
#endif
指定类设计的第二步:实现类成员函数
- 可以在类声明中提供完整的函数定义,而不是函数原型,但是通常的做法是单独提供函数定义 (除非函数很小)。
- 在这种情况下,需要使用作用域解析运算符(::)来指出成员函数属于哪个类。
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)
return s;
else
return *this;
}
指定类设计的第三步:创建类对象和类方法的使用
- 创建对象(类的实例),只需将类名视为类型名即可;
- 类成员函数(方法)可通过类对象来调用。为此,需要使用成员运算符句点(.)。
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"};
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];
for (i=1; i<STKS; i++)
topsand = &(topsand->topval(sand[i]));
topsand->show();
}
return 0;
}
10.3 类的构造函数和析构函数
10.3.1 构造函数
为什么需要构造函数?数据部分的访问状态是私有的,这意味着程序不能直接访问数据成员。程序只能通过成员函数来访问数据成员,因此需要设计合适的成员函数,才能成功地将对象初始化。C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值赋给它们的数据成员。
- 用类创建对象时,自动调用构造函数。
- 构造函数的函数名和类的名称相同。通过函数重载,可以创建多个同名的构造函数。
- 接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值。
- 构造函数的原型和函数头有一个有趣的特征——虽然没有返回值,但没有被声明为void类型。实际上,构造函数没有声明类型。
- 构造函数的参数表示的不是类成员,而是赋给类成员的值。因此,参数名不能与类成员相同。
- 无法使用对象来调用构造函数,因为在构造函数构造出对象之前,对象是不存在的。因此构造函数被用来创建对象,而不能通过对象来调用。
- 默认构造参数可以有形参(有的话必须全都有初始化的默认参数),也可以没有。
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 fluffy_the_cat = Stock{"Mooncake"};
Stock garment{"apple", 30, 123.45};
Stock first;
10.3.3 析构函数
- 用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数——析构函数。
- 析构函数完成清理工作:如果构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存。
- 析构函数的原型:名称在类名前加上~。没有返回值和声明类型。没有参数。Stock类的析构函数原型
~Stock(); - 如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。
- 类的自动变量储存是以栈的方式,则调用析构函数释放的时候先进后出,后进先出。
- 如果构造函数使用了new,则必须提供使用delete的析构函数。
析构函数何时被调用:
- 如果创建的是静态存储类对象,则其析构函数将在程序结束时自动被调用。
- 如果创建的是自动存储类对象(就像前面的示例中那样),则其析构函数将在程序执行完代码块时(该对象是在其中定义的)自动被调用。
- 如果对象是通过new创建的,则它将驻留在栈内存或自由存储区中,当使用delete来释放内存时,其析构函数将自动被调用。
- 最后,程序可以创建临时对象来完成特定的操作,在这种情况下,程序将在结束对该对象的使用时自动调用其析构函数。
10.4 this指针
this指针指向调用成员函数的对象,this是该对象的地址。
比较两个对象中哪个total_val值大并返回大的对象:
- 如何将两个要比较的对象提供给成员函数呢?如果希望该方法对两个对象进行比较,则必须将第二个对象作为参数传递给它。出于效率方面的考虑,可以按引用来传递参数。
- 如何将方法的答案传回给调用程序呢?最直接的方法是让方法返回一个引用,该引用指向股价总值较高的对象。
函数原型: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)
return s;
else
return *this;
}
- 该函数隐式地访问一个对象stock1,而显式地访问另一个对象stock2,并返回其中一个对象的引用。
- 括号中的const表明,该函数不会修改被显式地访问的对象stock2;
- 括号后的const表明,该函数不会修改被隐式地访问的对象stock1。
- 由于该函数返回了两个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];
for (i=1; i<STKS; i++)
topsand = &(topsand->topval(sand[i]));
topsand->show();
10.6 类作用域
- 在类中定义的名称(如类数据成员名和类成员函数名)的作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。
- 在不同类中使用相同的类成员名而不会引起冲突。
- 不能从外部直接访问类的成员,要调用公有成员函数,必须通过对象。
- 在定义成员函数时,必须使用作用域解析运算符(::)
- 在类声明或成员函数定义中,可以使用未修饰的成员名称。
作用域为类的常量
声明类只是描述了对象的形式,并没有创建对象。因此,在创建对象前,将没有用于存储值的空间。尽管是一个const变量。 第一种方式是在类中声明一个枚举:enum {Month = 12}; // 只是为了创建符号常量,Months只是一个符号名称,不需要枚举名。作用域为整个类。 第二种方式是在类中使用关键字static: static const int Month = 12; // 静态全局变量,作用域为整个类。
10.7 抽象数据类型(abstract data type,ADT)
ADT以通用的方式描述数据类型,而没有引入语言或实现细节。
例如,通过使用栈,可以以这样的方式存储数据,即总是从堆顶添加或删除数据。 例如,C++程序使用栈来管理自动变量。当新的自动变量被生成后,它们被添加到堆顶;消亡时,从栈中删除它们。
例程:用类的成员函数实现对栈的操作:。。。。。。编程练习5
10.8 总结
- 面向对象编程强调的是程序如何表示数据。使用OOP方法解决编程问题的第一步是根据它与程序之间的接口来描述数据,从而指定如何使用数据。然后,设计一个类来实现该接口。一般来说,私有数据成员存储信息,公有成员函数(又称为方法)提供访问数据的唯一途径。类将数据和方法组合成一个单元,其私有性实现数据隐藏。
- 通常,将类声明分成两部分组成,这两部分通常保存在不同的文件中。类声明(包括由函数原型表示的方法)应放到头文件中。定义成员函数的源代码放在方法文件中。这样便将接口描述与实现细节分开了。从理论上说,只需知道公有接口就可以使用类。当然,可以查看实现方法(除非只提供了编译形式),但程序不应依赖于其实现细节,如知道某个值被存储为int。只要程序和类只通过定义接口的方法进行通信,程序员就可以随意地对任何部分做独立的改进,而不必担心这样做会导致意外的不良影响。
- 类是用户定义的类型,对象是类的实例。这意味着对象是这种类型的变量,例如由new按类描述分配的内存。C++试图让用户定义的类型尽可能与标准类型类似,因此可以声明对象、指向对象的指针和对象数组。可以按值传递对象、将对象作为函数返回值、将一个对象赋给同类型的另一个对象。如果提供了构造函数,则在创建对象时,可以初始化对象。如果提供了析构函数方法,则在对象消亡后,程序将执行该函数。
- 每个对象都存储自己的数据,而共享类方法。如果mr_object是对象名,try_me( )是成员函数,则可以使用成员运算符句点调用成员函数:mr_object.try_me( )。在OOP中,这种函数调用被称为将try_me消息发送给mr_object对象。在try_me( )方法中引用类数据成员时,将使用mr_object对象相应的数据成员。同样,函数调用i_object.try_me( )将访问i_object对象的数据成员。
- 如果希望成员函数对多个对象进行操作,可以将额外的对象作为参数传递给它。如果方法需要显式地引用调用它的对象,则可以使用this指针。由于this指针被设置为调用对象的地址,因此*this是该对象的别名。
- 类很适合用于描述ADT。公有成员函数接口提供了ADT描述的服务,类的私有部分和类方法的代码提供了实现,这些实现对类的客户隐藏。
10.9 复习题
- 什么是类?
类是用户定义的类型的定义。类声明指定了数据将如何存储,同时指定了用来访问和操纵这些数据的方法(类成员函数)。 - 类如何实现抽象、封装和数据隐藏?
类表示人们可以类方法的公有接口对类对象执行的操作,这是抽象。 类的数据成员可以是私有的(默认值),这意味着只能通过成员函数来访问这些数据,这是数据隐藏。 实现的具体细节(如数据表示和方法的代码)都是隐藏的,这是封装。 - 对象和类之间的关系是什么?
类定义了一种类型,包括如何使用它。 对象是一个变量或其他数据对象(如由new生成的),并根据类定义被创建和使用。 类和对象之间的关系同标准类型与其变量之间的关系相同。 - 除了是函数之外,类函数成员与类数据成员之间的区别是什么?
如果创建给定类的多个对象,则每个对象都有其自己的数据内存空间; 但所有的对象都使用同一组成员函数(通常,方法是公有的,而数据是私有的,但这只是策略方面的问题,而不是对类的要求)。 - 类构造函数在何时被调用?类析构函数呢?
在创建类对象或显式调用构造函数时,类的构造函数都将被调用。当对象过期时,类的析构函数将被调用。 - 什么是默认构造函数,拥有默认构造函数有何好处?
默认构造函数是没有参数或所有参数都有默认值的构造函数。 拥有默认构造函数后,可以声明对象,而不初始化它,即使已经定义了初始化构造函数。它还使得能够声明数组。 - 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];
int top;
public:
Stack();
bool isempty() const;
bool isfull() const;
bool push(const Item & item);
bool pop(Item & item);
void visit(void (*pf)(Item &));
};
#endif
p5.cpp
#include "p5.h"
Stack::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>
#include "p5.h"
int main()
{
using namespace std;
Stack st;
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;
}
|