C++PrimerPlus学习笔记
第10章对象和类
面向对象编程是一种特殊的、设计程序的概念性方法,C++通过一些特性改进了C语言,使得应用这种方法更容易。下面是最重要的OOP特性:
抽象;封装和数据隐藏;多态;继承;代码的可重用性。
为了实现这些特性并将它们给在一起,C++所做的最重要的改进是提供了类。
过程性编程和面向对象编程
采用过程性编程方法时,首先要考虑遵循的步骤,然后考虑如何表示这些数据。采用OOP方法,首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。使用新的设计方案创建出程序。
抽象和类
抽象是将问题的本质特征抽象出来,并根据特征来描述解决方案。抽象是通往用户定义类型的捷径,在C++中,用户定义类型指的是实现抽象接口的类设计。
类型是什么
指定基本类型完成了三项工作:
决定数据对象需要的内存数量;决定如何解释内存中的位;决定可使用数据对象执行的操作和方法。
对于内置类型来说,有关操作的信息被内置到编译器中。但C++中定义用户自定义的类型时,必须自己提供这些信息。付出这些劳动换来了根据实际需要定制新数据类型的强大功能和灵活性。
C++中的类
类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包。一般来说,类规范由两个部分组成。
简单地说,类声明提供了类的蓝,而方法定义则提供了细节。
什么是接口
C++程序员将接口放在头文件中,并将实现放在源代码文件中。为帮助识别类,本书遵循一种常见但不通用的约定——将类名首字母大写。
// stock00.h -- Stock class interface
// version 00
#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 = 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指出这些代码定义了一个类设计。这种语法指出,Stock是这个新类的类型名。该声明让我们能够声明Stock类型的变量——称为对象或实例。接下来,要存储的数据以类数据成员的形式出现。同样,要执行的操作以类函数成员的形式出现。成员函数可以就地定义,也可以用原型表示。其他成员函数的完整定义包含在实现文件中;但对于描述函数接口而言,原型足够了。将数据和方法组合成一个单元是类最吸引人的特性。
访问控制
关键字private和public也是新的,它们描述了对类成员的访问控制。使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数来访问对象的私有成员。因此公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。防止程序直接访问数据被称为数据隐藏。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。
类和结构
类描述看上去很像是包含成员函数以及private和public可见性标签的结构声明。实际上,C++对结构进行了扩展,使之具有与类相同的特性。它们之间唯一的区别是,结构的默认访问类型是public,而类为private。C++程序员通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象。
实现类成员函数
创建类描述的第二部分:为那些由类声明中的原型表示的成员函数提供代码。成员函数定义与常规函数定义非常相似,它们有函数头和函数体,也可以返回类型和参数。但是它们还有两个特殊的特征:
定义成员函数时,使用作用域解析操作符来标识函数所属的类;类方法可以访问类的private组件。
成员函数的函数头使用作用域解析操作符来指出函数所属的类。例如,update成员函数的函数头如下:
void Stock::update(double price)
这种表示法意味着我们定义的update函数是Stock类的成员。这不仅将update标识为成员函数,还意味着我们可以将另一个类的成员函数也命名为update。作用域解析操作符确定了方法定义对应类的身份。我们说,标识符update具有类作用域。Stock类的其他成员函数不必使用作用域解析操作符,就可以使用update方法,这是因为它们属于同一个类,因此update是可见的。然而,在类声明和方法定义之外使用update时,需要采取特殊的措施。类方法的完整名称中包括类名。我们说,Stock::update是函数的限定名;而简单的update是全名的缩写,它只能在类作用域中使用。方法的第二个特点是,方法可以访问类的私有成员。
// stock00.cpp -- implementing the Stock class
// version 00
#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.
";
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.
";
}
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.
";
}
else if (num > shares)
{
cout << "You can"t sell more than you have! "
<< "Transaction is aborted.
";
}
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 << "
";
cout << " Share price: $" << share_val;
// set format to #.##
cout.precision(2);
cout << " Total Worth: $" << total_val << "
";
// 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;
}
内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。确保内联定义对多文件程序中的所有文件都可用的、最简便的方法是:将内联定义放在定义类的头文件中,在类声明中定义方法等同于用原型替换方法定义,然后在类声明的后面将定义改写为内联函数。也就是说,set_tot的内联定义与上述代码是等价的。
方法使用哪个对象
创建对象最简单的方法是声明类变量,使用对象的成员函数,和使用结构成员一样,通过成员运算符:
Stock kate, joe;
kate.show();
joe.show();
第1条语句调用kate对象的show成员。这意味着show方法将把shares解释为kate.shares,将share_val解释为kate.share_val。同样,函数调用joe.show使show方法将shares和share_val分别解释为joe.share和joe.share_val。注意:调用成员函数时,它将使用被用来调用它的对象的数据成员。同样,函数调用kate.sell在调用set_tot函数时,相当于调用kate.set_tot,这样该函数将使用kate对象的数据。所创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法,即每种方法只有一个副本。例如,假设kate和joe都是Stock对象,则kate.shares将占据一个内存块,而joe.shares占用另一个内存块,但kate.show和joe.show都调用同一个方法,也就是说,它们将执行同一个代码块,只是将这些代码用于不同的数据。在OOP中,调用成员函数被称为发送消息,因此将同样的消息发送给两个不同的对象将调用同一个方法,但该方法被用于两个不同的对象。
使用类
知道如何定义类及其方法后,来创建一个程序,它创建并使用类对象。C++的目标是使得使用类与使用基本的内置类型如尽可能相同。要创建类对象,可以声明类变量,也可顶使用new为类对象分配存储空间。可以将对象作为函数的参数和返回值,也可以将一个对象赋给另一个。C++提供了一些工具,可用于初始化对象、让cin和cout识别对象,甚至在相似的类对象之间进行自动类型转换。虽然要做到这些工作还需要一段时间,但可以先从比较简单的属性着手。实际上,您己经知道如何声明类对象和调用成员函数。以下程序清单提供了一个使用上述接口和实现文件的程序,它创建了一个名为fluffy_the_cat的Stock对象。该程序非常简单,但确实测试了这个类的特性。要编译该程序,可使用用于多文件程序的方法。具体地说,将其与stock00.cpp一起编译,并确保stock00.h位于当前文件夹中。
// usestock0.cpp -- the client program
// compile with stock00.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;
}
注意,main只是用来测试Stock类的设计。当Stock类的运行情况与预期的相同后,便可以在其他程序中将Stock类作为用户定义的类型使用。要使用新类型,最关键的是要了解成员函数的功能,而不必考虑其实现细节。
客户服务器模型
OOP程序员常依照客户/服务器模型来讨论程序设计。在这个概念中,客户是使用类的程序。类声明构成了服务器,它是程序可以使用的资源。客户只能通过以公有方式定义的接口使用服务器,这意味着客户唯一的责任是了解该接口。服务器的责任是确保服务器根据该接口可靠并准确地执行。服务器设计人员只能修改类设计的实现细节,而不能修改接口。这样程序员独立地对客户和服务器进行改进,对服务器的修改不会客户的行为造成意外的影响。
指定类设计的第一步是提供类声明。类声明类似结构声明,可以包括数据成员和函数成员。声明有私有部分,在其中声明的成员只能通过成员函数进行访问;声明还具有公有部分,在其中声明的成员可被使用类对象的程序直接访问。数据成员被放在私有部分中,成员函数被放在公有部分中,因此典型的类声明的格式如下:
class className
{
private:
data member declarations
public:
member function prototypes
}
公有部分的内容构成了设计的抽象部分——公有接口。将数据封装到私有部分中可以保护数据的完整性,这被称为数据隐藏。C++通过类使得实现抽象、数据隐藏和封装等OOP特性很容易。指定类设计的第二步是实现类成员函数。可以在类声明中提供完整的函数定义,而不是函数原型,但是通常的做法是单独提供函数定义。在这种情况下,需要使用作用域解析运算符来指出成员函数属于哪个类。例如,假设Bozo有一个名为Retort的成员函数,该函数返回char指针,则其函数头如下所示:
char * Bozo::Retort()
换句话来说,Retort不仅是一个char*类型的函数,而是一个属于Bozo类的char*函数。该函数的全名为Bozo::Retort。而名称Retort是限定名的缩写,只能在某些特定的环境中使用,如类方法的代码中。另一种描述这种情况的方式是,名称Rtotrt的作用域为整个类,因此在类声明和类方法之外使用该名称时,需要使用作用域解析运算符进行限定。要创建对象,只需将类名视为类型名即可:
Bozo bozetta;
这样做是可行的,因为类是用户定义的类型。类成员函数可通过类对象来调用。为此,需要使用成员运算符句点:
cout << bozetta.Retort();
这将调用Retort成员函数,每当其中的代码引用某个数据成员时,该函数都将使用bozetta对象中相应成员的值。
类的构造函数和析构函数
应为类提供被称为构造函数和析构函数的标准函数。C++的目标之一是让使用类对象就像使用标准类型一样,然而,到现在为止,本章提供的代码还不能像初始化int或结构那样来初始化Stock对象。也就是说,常规的初始化语法不适用于类型Stock:
int year = 2001; // valid initialization
struct thing
{
char * pn;
int m;
};
thing amabob = {"wodget", -23}; // valid initialization
Stock hot = {"Sukie"s Autos, Inc.", 200, 50.25}; // NO! compile error
Stock gift;
gift.buy(10, 24.75);
就Stock类当前的实现而言,gift对象的company成员是没有值的。类设计假设用户在调用其他成员函数之前调用acquire,但无法强加这种假设。避开这种问题的方法之一是在创建对象时,自动对它进行初始化。为此,C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值赋给它们的数据成员。更准确地说,C++为这些成员函数提供了名称和使用语法,而程序员需要提供方法定义。名称和类名相同。例如,Stock类一个可能的构造函数名为Stock的成员函数。构造函数的原型和函数头有一个有趣的特征——虽然没有返回值,但没有被声明为void类型。实际上,构造函数没有声明类型。
声明和定义构造函数
现在需要创建Stock的构造函数。由于需要为Stock对象提供3个值,因此应为构造函数提供3个参数。(第4个值,total_val成员,是根据shares和share_val计算得到的,因此不必为构造函数提供这个值。)程序员可能只想设置company成员,而将其他值设置为O;这可以使用默认参数来完成。原型如下所示:
// constructor prototype with some default arguments
Stock(const string & co, long n = 0, double pr = 0.0);
第一个参数是指向字符串的指针,该字符串用于初始化成员company。n和pr参数为shares和share_val成员提供值。注意,没有返回类型。原型位于类声明的公有部分。下面是构造函数的一种可能定义:
// constructor definition
Stock::Stock (const 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.
";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
上述代码和本章前面的函数acquire相同。区别在于,程序声明对象时,将自动调用构造函数。
成员名和参数名
不熟悉构造函数的可能会试将类成员名称用作构造函数的参数名,如下所示:
// No!
Stock::Stock(const string & company, long shares, double share_val)
{
...
}
这是错误的。构造函数的参数表示的不是类成员,而是赋给类成员的值。参数名不能与类成员相同,否则最终的代码将是这样的:
shares = shares;
为避免这种混乱,一种常见的做法是在数据成员名中使用m_前缀:
class Stock
{
private:
string m_company;
long m_shares;
...
另一种常见的做法是,在成员名中使用后缀_:
class Stock
{
private:
string company_;
long shares_;
...
无论采用哪种做法,都可在公有接口中在参数名中包含company和shares。
使用构造函数
C++提供了两种使用构造函数来初始化对象的方式。第一种方式是显式地调用构造函数,另一种方式是隐式地调用构造函数:
Stock food = Stock("World Cabbage", 250, 1.25);
Stock garment("Furry Mason", 50, 2.5);
第二种格式更紧凑,与第一种显示调用等价。每次创建类对象时,C++都使用类构造函数。下面是将构造函数与new一起使用的方法:
Stock *pstock = new Stock("Electroshock Games", 18, 19.0);
这条语句创建一个Stock对象,将其初始化为参数提供的值,并将该对象的地址赋给pstock指针。在这种情况下,对象没有名称,但可以使用指针来管理该对象。构造函数的使用方式不同于其他类方法。一般来说,使用对象来调用方法:
stockl.show(); //stockl object invokes show() method
但无法使用对象来调用构造函数,因为在构造函数构造出对象之前,对象是不存在的。因此构造函数被用来创建对象,而不能通过对象来调用。
默认构造函数
默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。也就是说,它是用于下面这种声明的构造函数:
Stock fluffy_the_cat; // uses the default constructor
这条语句管用的原因在于,如果没有提供任何构造函数,则C++将自动提供默认构造函数。它是默认构造函数的隐式版本,不做任何工作。对于Stock类来说,默认构造函数可能如下:
Stock::Stock(){}
因此将创建fluffy_the_cat对象,但不初始化其成员,这和下面的语句创建x,但没有提供值给它一样:
int x;
默认构造函数没有参数,因为声明中不包含值。奇怪的是,当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。为类定义了构造函数后,程序员就必须为它提供默认构造函数。如果提供了非默认构造函数,但没有提供默认构造函数,则下面的声明将出错:
Stock stockl; //not possible with current constructor
这样做的原因可能是想禁止创建未初始化的对象。然而,如果要创建对象,而不显式地初始化,则必须定义一个不接受任何参数的默认构造函数。定义默认构造函数的方式有两种。一种是给已有构造函数的所有参数提供默认值,另一种方式是通过函数重载来定义另一个构造函数——一个没有参数的构造函数:
Stock(const string & co ="Error", int n =0, double pr = 0.0);
Stock();
由于只能有一个默认构造函数,因此不要同时采用这两种方式。实际上,通常应初始化所有的对象,以确保所有成员一开始就有己知的合理值。用户定义的默认构造函数通常给所有成员提供隐式初始值。例如,下面是为Stock类定义的一个默认构造函数:
Stock::Stock() // default constructor
{
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
提示:在设计类时,通常应提供对所有类成员做隐式初始化的默认构造函数。使用上述任何一种方式创建了默认构造函数后,便可以声明对象变量,而不对它们进行显式初始化:
Stock first; // calls default constructor implicitly
Stock first = Stock(); // calls it explicitly
Stock *prelief =new Stock; // calls it implicitly
然而,不要被非默认构造函数的隐式形式所误导:
Stock first("Concrete Conglomerate"); // calls constructor
Stock second(); // declares a function
Stock third; // calls default constructor
第一个声明调用非默认构造函数,即接受参数的构造函数;第二个声明指出,second是一个返回Stock对象的函数。隐式地调用默认构造函数时,不要使用圆括号。
析构函数
用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数,该函数的名称令人生畏——析构函数。析构函数完成清理工作,因此实际上很有用。例如,如果构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存。Stoek的构造函数没有使用new,因此析构函数实际上没有需要完成的任务。在这种情况下,只需让编译器生成一个什么也不做的隐式析构函数即可,Stock类第一版正是这样做的。然而,了解如何声明和定义析构函数是绝对必要的,下面为Stock类提供一个析构函数。和构造函数一样,析构函数的名称也很特殊:在类名前加上~。Stock类的析构函数为~Stock。和构造函数一样,析构函数也可以没有返回值和声明类型。与构造函数不同的是,析构函数没有参数,因此Stock析构函数的原型必须是这样的:
~Stock();
由于Stock的析构函数不承担任何重要的工作,因此可以将它编写为不执行任何操作的函数:
Stock::~Stack()
{
}
然而,为了能看出析构函数何时被调用,这样编写其代码:
Stock::~Stock() //class destructor
{
cout << "Bye, " << company <<"!
";
}
什么时候应调用析构函数呢?这由编译器决定,通常不应在代码中显式地调用析构函数。如果创建的是静态存储类对象则其析构函数将在程序结束时自动被调用。如果创建的是自动存储类对象,则其析构函数将在程序执行完代码块时自动被调用。如果对象是通过new创建的,则它将驻留在栈内存或自由存储区中,当使用delete来释放内存时,其析构函数将自动被调用。程序可以创建临时对象来完成特定的操作,在这种情况下,程序将在结束对该对象的使用时自动调用其析构函数。由于在类对象过期时析构函数将自动被调用,因此必须有一个析构函数。如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。
改进Stock类
下面将构造函数和析构函数加入到类和方法的定义中,头文件为stockh。类方法放在文件stockcpp中。将使用这些资源的程序放在第三个文件中,这个文件名为usestockcpp。
头文件
头受件将构造函数和析构函数的原型加入到原来的类声明中。它还删除了acquire函数——现在己经不再需要它了,因为有构造函数。
// stock10.h -- Stock class declaration with constructors, destructor added
#ifndef STOCK10_H_
#define STOCK10_H_
#include <string>
class Stock
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot() { total_val = shares * share_val;}
public:
// tow constructors
Stock(); // defaul constructor
Stock(const std::string & co, long n = 0, double pr = 0.0);
~Stock(); // noisy destructor
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
};
#endif
实现文件
stockcpp提供了方法的定义。它包含了文件stockh,以提供类声明(将文件名放在双引号而不是方括号中意味着编译器将源文件所在的日录中搜索它)。还包含了头文件iostream以提供I/O支持。该文件将构造函数和析构函数的方法定义添加到以前的方法定义中。为让您知道这些方法何时被调用,它们都显示一条消息。这并不是构造函数和析构函数的常规功能,但有助于您更好地了解类是如何使用它们的。
// stock10.cpp -- Stock class with constructors, destructor added
#include <iostream>
#include "stock10.h"
// constructors (verbose versions)
Stock::Stock() // default constructor
{
std::cout << "Default constructor called
";
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
Stock::Stock(const std::string & co, long n, double pr)
{
std::cout << "Constructor using " << co << " called
";
company = co;
if (n < 0)
{
std::cout << "Number of shares can"t be negative; "
<< company << " shares set to 0.
";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
// class destructor
Stock::~Stock() // verbose class destructor
{
std::cout << "Bye, " << company << "!
";
}
// other methods
void Stock::buy(long num, double price)
{
if (num < 0)
{
std::cout << "Number of shares purchased can"t be negative. "
<< "Transaction is aborted.
";
}
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.
";
}
else if (num > shares)
{
cout << "You can"t sell more than you have! "
<< "Transaction is aborted.
";
}
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 << "
";
cout << " Share price: $" << share_val;
// set format to #.##
cout.precision(2);
cout << " Total Worth: $" << total_val << "
";
// restore original format
cout.setf(orig, ios_base::floatfield);
cout.precision(prec);
}
客户文件
// usestock1.cpp -- using the Stock class
// compile with stock10.cpp
#include <iostream>
#include "stock10.h"
int main()
{
{
using std::cout;
cout << "Using constructor to create new objects
";
Stock stock1("NanoSmart", 12, 20.0); // syntax 1
stock1.show();
Stock stock2 = Stock("Boffo Objects", 2, 2.0); // syntax 2
stock2.show();
cout << "Assigning stock1 to stock2:
";
stock2 = stock1;
cout << "Listing stock1 and stock2:
";
stock1.show();
stock2.show();
cout << "Using a constructor to reset an object
";
stock1 = Stock("Nifty Foods", 10, 50.0); // temp object
cout << "Revised stock1:
";
stock1.show();
cout << "Done
";
}
return 0;
}
// 使用某个编译器得到的可执行程序输出
Using constructor to create new objects
Constructor using NanoSmart called
Company: NanoSmart Shares: 12
Share price: $20.000 Total Worth: $240.00
Constructor using Boffo Objects called
Company: Boffo Objects Shares: 2
Share price: $2.000 Total Worth: $4.00
Assigning stock1 to stock2:
Listing stock1 and stock2:
Company: NanoSmart Shares: 12
Share price: $20.000 Total Worth: $240.00
Company: NanoSmart Shares: 12
Share price: $20.000 Total Worth: $240.00
Using a constructor to reset an object
Constructor using Nifty Foods called
Bye, Nifty Foods!
Revised stock1:
Company: Nifty Foods Shares: 10
Share price: $50.000 Total Worth: $500.00
Done
Bye, NanoSmart!
Bye, Nifty Foods!
// 使用某些编译器编译该程序时,该程序输出的前半部分可能如下,(比前面多了一行):
Using constructor to create new objects
Constructor using NanoSmart called
Company: NanoSmart Shares: 12
Share price: $20.000 Total Worth: $240.00
Constructor using Boffo Objects called
Bye, Boffo Objects! // additional line
Company: Boffo Objects Shares: 2
Share price: $2.000 Total Worth: $4.00
提示:main的开头和末尾多了一个大括号。诸如stock1和stock2等自动变量将在程序退出其定义所属代码块时消失。如果没有这些大括号,代码决将为整个main,因此仅当main执行完毕后,才会调用析构函数。在窗口环境中,这意味着将在两个析构函数调用前关闭,导致无法看到最后两条消息。但添加这些大括号后,最后两个析构函教调用将在到达返回语句前执行,从而显示相应的消息。
程序说明
Stock stockl("NanoSmart", 12, 20.0);
创建一个名为stock1的Stock对象,并将其数据成员初始化为指定的值:
Constructor using NanoSmart called
Company: NanoSmart Shares: 12
下面的语句使用另一种语法创建并初始化一个名为stock2的对象:
Stock stock2 = Stock("Boffo Objects, 2, 2.0);
C++标准允许编译器使用两种方式来执行第二种语法。一种是使其行为和第一种语法完全相同:
Constructor using Boffo Objects called
Company: Boffo Objects Shares: 2
另一种方式是允许调用构造函数来创建一个临时对象,然后将该临时对象复制到stock2中,并丢弃它。如果编译器使用的是这种方式,则将为临时对象调用析构函数,因此生成下面的输出:
Constructor using Boffo Objects called
Bye, Boffo Objects!
Company: Boffo Objects Shares: 2
生成上述输出的编译器可能立刻删除临时对象,但也可能会等一段时间,在这种情况下,析构函数的消息将会过一段时间才显示。下面的语句表明可以将一个对象赋给同类型的另一个对象:
stock2 = stockl; // object assignment
与给结构赋值一样,在默认情况下,给类对象赋值时,将把一个对象的成员复制给另一个。在这个例子中,stock2原来的内容将被覆盖。注意:在默认情况下,将一个对象赋给同类型的另一个对象时,C++将源对象的每个数据成员的内容复制到目标对象中相应的数据成员中。构造函数不仅仅可用于初始化新对象。例如,该程序的main中包含下面的语句:
stock1 = Stock("Nifty Foods", 10, 50.0);
stock1对象已经存在,因此这条语句不是对stock1进行初始化,而是将新值赋给它。这是通过让构造程序创建一个新的、临时的对象,然后将其内容复制给stock1来实现的。随后程序调用析构函数,以删除该临时对象,如下面经过注释后的输出所示:
Using a constructor to reset an object
Constructor using Nifty Foods called >> temporary object created
Bye, Nifty Foods! >> temporary object destroyed
Revised stock1:
Company: Nifty Foods Shares: 10 >> data now copied to stock1
Share price: $50.000 Total Worth: $500.00
有些编译器可能要过一段时间才删除临时对象,因此析构函数的调用将延迟。程序显示了下面的内容:
Done
Bye, NanoSmart!
Bye, Nifty Foods!
函数main结束时,其局部变量将消失。由于这种自动变量被放在栈中,因此最后创建的对象将最先被删除,最先创建的对象将最后被删除(“NanoSmart”最初位于stock1中,但随后被传输到stock2中,然后stock1被重置为“NiftyFood”)。输出表明,下面两条语句有根本性的差别:
Stock stock2 = Stock("Boffo Objects", 2, 2.0);
stock1 = Stock("Nifty Foods", 10, 50.0); // temporary object
第一条语句是初始化,它创建有指定值的对象,可能会创建临时对象;第二条语句是赋值。像这样在赋值语句中使用构造函数总会导致在赋值前创建一个临时对象。提示:如果既可以通过初始化,也可以通过赋值来设置对象的值,则应采用初始化方式。通常这种方式的效率更高。
C++11列表初始化
在C++11中,可将列表初始化语法用于,只要提供与某个构造函数的参数列表匹配的内容,并用大括号将它们括起:
Stock hot_tip = {"Derivatives PlusPlus", 100, 45.0);
Stock jock {"Sport Age Storage,Inc");
Stock temp {};
const成员函数
请看下面的代码片段:
const Stock land = Stock("Kludgehorn Properties");
land.show();
对于当前的C++来说,编译器将拒绝第二行。因为show的代码无法确保调用对象不被修改——调用对象和const一样,不应被修改。我们以前通过将函数参数声明为const引用或指向const的指针来解决这种问题。但这里存在语法问题:show方法没有任何参数。相反,它所使用的对象是由方法调用隐式地提供的。需要一种新的语法——保证函数不会修改调用对象。C++的解决方法是将const关键字放在函数的括号后面。也就是说,show声明应像这样:
void show() const; // promises not to change invoking object
// 同样,函数定义的开头应像这样:
void Stock::show() const // promises not to change invoking object
以这种方式声明和定义的类函数被称为const成员函数。就像应尽可能将const引用和指针用作函数形参一样,只要类方法不修改调用对象,就应将其声明为const。从现在开始,我们将遵守这一规则。
构造函数和析构函数小结
构造函数是一种特殊的类成员函数,在创建类对象时被调用。构造函数的名称和类名相同,但通过函数重载,可以创建多个同名的构造函数,条件是每个函数的特征标都不同。构造函数没有声明类型。构造函数用于初始化类对象的成员,初始化应与构造函数的参数列表匹配。例如,假设Bozo类的构造函数的原型如下:
Bozo(const char * fname, const char * lname); // constructor prototype
// 则可以使用它来初始化新对象:
Bozo bozetta = Bozo("Bozetta", "Biggens"); // primary form
Bozo fufu("Fufu", "O"Dweeb"); // short form
Bozo *pc = new Bozo("Popo", "Le Peu"); // dynamic object
// 如果编译器支持 C++11,则可使用列表初始化:
Bozo bozetta ={"Bozetta", "Biggens"}; // C++11
Bozo fufu{"Fufu", "O"Dweeb"}; // C++11
Bozo *pc = new Bozo{"Popo", "Le Peu"}; // C++11
如果构造函数只有一个参数,则将对象初始化为一个与参数的类型相同的值时,该构造函数将被调用。例如,假设有这样一个构造函数原型:
Bozo(int i);
// 则可以使用下面的任何一种形式来初始化对象:
Bozo dribble = Bozo(44); // primary form
Bozo roon(66); // secondary form
Bozo tubby = 32; // special form for one-argument constructors
Classname object = value;
this指针
const Stock topval(const Stock & s) const;
该函数隐式地访问一个对象,而显式地访问另一个对象,并返回其中一个对象的引用。括号中的const表明,该函数不会修改被显式地访问的对象;而括号后的cosnt表明,该函数不会修改被隐式地访问的对象。由于该函数返回了两个const对象之一的引用,因此返回类型也应为const引用。假设要对Stock对象stock1和stock2进行比较,并将其中股价总值较高的那一个赋给top对象,则可以使用下面两条语句之
top = stock1.topval(stock2);
top = stock2.topval(stock1);
const Stock & Stock::topval(const Stock & s) const
{
if (s.total_val > total_val)
return s; // argument object
else
return ????? // invoking object
}
其中,s.total_val是作为参数传递的对象的总值,total_val是用来调用该方法的对象的总值。如果s.total_val大于total_val,则函数将返回指向s的引用;否则,将返回用来调用该方法的对象,但stock1没有别名。C++解决这种问题的方法是:使用被称为this的特殊指针。this指针指向用来调用成员函数的对象。这样,函数调用stock1::topval将this设置为stock1对象的地址,使得这个指针可用于topval方法。同样,函数调用stocktopval将this设置为stock2对象的地址。一般来说,所有的类方法都将this指针设置为调用它的对象的地址。确实,topval中的total_val只不过是this->total_val的简写.。注意:每个成员函数都有一个this指针。this指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式*this。在函数的括号后面使用const限定符将this限定为const,这样将不能使用this来修改对象的值。然而,要返回的并不是this,因为this是对象的地址,而是对象本身,即*this。现在,可以将*this作为调用对象的别名来完成前面的方法定义。
const Stock & Stock::topval(const Stock & s) const
{
if (s.total_val > total_val)
return s; // argument object
else
return *this; // invoking object
}
返回类型为引用意味着返回的是调用对象本身,而不是其副本。
对象数组
Stock mystuff[4]; // creates an array of 4 Stock objects
前面讲过,当程序创建未被显式初始化的类对象时,总是调用默认构造函数。上述声明要求,这个类要么没有显式地定义任何构造函数,要么定义了一个显式默认构造函数。每个元素都是Stock对象,可以使用Stock方法:
mystuff[0].update(); // apply update() to 1st element
mystuff[3].show(); // apply show() to 4th element
const Stock tops = mystuff[2].topval(mystuff[1]);
// compare 3rd and 2nd elements and set tops
// to point at the one with a higher total value
// 可以用构造函数来初始化数组元素。在这种情况下,必须为每个元素调用构造函数:
const int STKS = 4;
Stock stocks[STKS] {
Stock("NanoSmart", 12.5, 20),
Stock("Boffo objects", 200, 2.0),
Stock("Monolithic Obelisks", 130, 3.25),
Stock("Fleep Enterprises", 60, 6.5)
};
// 这里的代码使用标准格式对数组进行初始化;用括号括起的、以逗号分隔的值列表。
// 其中,每次构造函数调用表示一个值。
// 如果类包含多个构造函数,则可以对不同的元素使用不同的构造函数:
const int STKS 10;
Stock stocks [STKS] = {
Stock("NanoSmart", 12.5, 20),
stock(),
Stock ("Monolithic Obelisks", 130, 3.25),
};
上述代码使用Stock初始化stock[0]和stock,使用构造函数Stock初始化stock。由于该声明只初始化了数组的部分元素,因此余下的7个元素将使用默认构造函数进行初始化。初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。要创建类对象数组,则这个类必须有默认构造函数。顺便说一句,知道this指针就可以更深入了解C++的工作方式。例如,最初的UNIX实现使用C++前端cfront将C++程序转换为C程序。处理方法的定义时,只需将下面这样的C++方法定义:
void Stock::show() const
{
cout << "Company:" << company
<< " Shares:" << shares << "
"
<< " Share Price: $" << share_val
<< " Total Worth: $" << total_val <<"
";
// 转换为下面这样的 C-风格定义:
void show(const Stock * this)
{
cout << "Company: " << this->company
<< " Shares: " << this->shares << "
"
<< " Share Price: $" << this->share_val
<< " Total Worth: $" << this->total_val << "
";
}
即将Stock::限定符转换为函数参数,然后用这个指针来访问类成员。同样,该前端将下面的函数调用:
top.show()
// 转换为:
show(&top);
这样,将调用对象的地址赋给了this指针。
类作用域
Stock sleeper("Exclusive Ore", 100, 0.25); // create object
sleeper.show(); // use object to invoke a member function
show(); // invalid -- can"t call method directly
// 同样,在定义成员函数时,必须使用作用域解析运算符:
void Stock::update(double price)
{
...
}
总之,在类声明或成员函数定义中,可以使用未修饰的成员名称,就像sell调用set_tot成员函数时那样。构造函数名称在被调用时,才能被识别,因为它的名称与类名相同。在其他情况下,使用类成员名时,必须根据上下文使用直接成员运算符、间接成员运算符或作用域解析运算符。下面的代码片段演示了如何访问具有类作用域的标识符:
class Ik
{
private:
int fuss; // fuss has class scope
public:
Ik(int f = 9) {fuss = f;} // fuss is in scope
void viewIk() const; // viewIk has class scope
};
void Ik::ViewIk() const // Ik::places viewIk into Ik scope
{
cout << fuss << endl; // fuss in scope within class methods
}
...
int main()
{
Ik pik = new Ik;
Ik ee =Ik(8); // constructor in scope because has class name
ee.viewIk(); // class object brings viewIk into scope
pik->viewIk(); // pointer-to-Ik brings viewIk into scope
...
作用域为类的常量
有时候,使符号常量的作用域为类很有用。例如,类声明可能使用字面值30来指定数组的长度,由于该常量对于所有对象来说都是相同的,因此创建一个由所有对象共享的常量是个不错的主意。您可能以为这样做可行:
class Bakery
{
private:
const int Months = 12; // declare a constant? FAILS
double costs[Months];
...
class Bakery
{
private:
enum {Months = 12};
double costs[Months];
...
注意,用这种方式声明枚举并不会创建类数据成员。也就是说,所有对象中都不包含枚举。Months只是一个符号名称,在作用域为整个类的代码中遇到它时,编译器将用30来替换它。由于这里使用枚举只是为了创建符号常量,并不打算创建枚举类型的变量,因此不需要提供枚举名。顺便说一句,在很多实现中,ios_base类在其公有部分中完成了类似的工作,诸如ios_base::fixed等标识符就来自这里。其中,fixed是ios_base类中定义的典型的枚举量。C++提供了另一种在类中定义常量的方式——使用关键字static`:
class Bakery
{
private:
static const int Months = 12;
double costs[Months];
...
作用域内枚举
传统的枚举存在一些问题。其中之一是两个枚举定义中的枚举量可能发生冲突。假设有一个处理鸡蛋和T恤的项目,其中可能包含类似下面这样的代码:
enum egg {Small, Medium, Large, Jumbo};
enum t_shirt {Small, Medium, Large, xlarge};
这将无法通过编译,因为eggSmall和t_shirtSmall位于相同的作用域内,它们将发生冲突。为避免这种问题,C++11提供了一种新枚举,其枚举量的作用域为类。这种枚举的声明类似于下面这样:
enum class egg {Smal, Medium, Large, Jumbo};
enum class t_shirt {Small, Medium, Large, xlarge};
// 也可使用关键字`struct`代替`class`。无论使用哪种方式,都需要使用枚举名来限定枚举量:
egg choice = egg::Large; // the Large enumerator of the egg enum
t_shirt Floyd = t_shirt::Large; //the Large-enumerator of the t_shirt enum
枚举量的作用域为类后,不同枚举定义中的枚举量就不会发生名称冲突了,而您可继续编写处理鸡蛋和T恤的项目。C++11还提高了作用域内枚举的类型安全。在有些情况下,常规枚举将自动转换为整型,如将其赋给int变量或用于比较表达式时,但作用域内枚举不能隐式地转换为整型:
enum egg_old {Small, Medium, Large, Jumbo}; // unscoped
enum class t_shirt {small, Medium, Large, Xlarge}; // scoped
egg_old one = Medium; // unscoped
t_shirt rolf = t_shirt::Large; // scoped
int king = one; // implicit type conversion for unscoped
int ring = rolf; // not allowed, no implicit type comversion
if (king < Jumbo) // allowed
std::cout << "Jumbo converted to int before comparison.
";
if (king < t_shirt::Medium) // not allowed
std::cout << "Not allowed: < not defined for scoped enum.
";
// 但在必要时,可进行显式类型转换:
int Frodo = int(t_shirt::Small); //Frodo set to 0
枚举用某种底层整型类型表示,在C++98中,如何选择取决于实现,因此包含枚举的结构的长度可能随系统而异。对于作用域内枚举,C++11消除了这种依赖性。默状情况下,C++11作用域内枚举的底层类型为int。还提供了一种语法,可用于做出不同的选择:
// underlying type for pizza is short
enum class : short pizza {Small, Medium, Large, XLarge};
:short将底层类型指定为short。底层类型必须为整型。在C++11中,也可使用这种语法来指定常规枚举的底层类型,但如果没有指定,编译器选择的底层类型将随实现而异。
抽象数据类型
可创建空栈。可将数据项添加到堆顶。可从栈顶山删除数据项。可查看栈否填满。可查看栈是否为空。
可以将上述描述转换为一个类声明,其中公有成员函数提供了表示栈操作的接口,而私有数据成员负责存储栈数据。类概念非常适合于ADT方法。私有部分必须表明数据存储的方式。例如,可以使用常规数组、动态分配数组或更高级的数据结构。然而,公有接口应隐藏数据表示,而以通用的术语来表达,如创建栈、压入等。以下程序演示了一种方法,它假设系统实现了bool类型。如果您使用的系统没有实现,可以使用int、0和1代替bool、false和true。
// stack.h -- class definition for the stack ADT
#ifndef STACK_H_
#define STACK_H_
typedef unsigned long Item;
class Stack
{
private:
enum {MAX = 10}; // constant specific to class
Item items[MAX]; // holds stack items
int top; // index for top stack
public:
Stack();
bool isempty() const;
bool isfull() const;
// push() returns false if stack already if full, true otherwise
bool push(const Item & item); // add item to stack
// pop() returns false if stack already is empty, true otherwise
bool pop(Item & item); // pop top into item
};
#endif
// stack.cpp -- Stack member functions
#include "stack.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;
}
默认构造函数确保所有栈被创建时都为空。pop和push的代码确保栈顶被正确地处理。这种保证措施是OOP更可靠的原因之假设要创建一个独立数组来表示栈,创建一个独立变量来表示栈顶索引。则每次创建新栈时,都必须确保代码是正确的。没有私有数据提供的保护,则很可能由于无意修改了数据而导致程序出现非常严重的故障。下面来测试该栈。以下程序模拟了售货员的行为一使用栈的后进先出方式,从购物筐的最上面开始处理购物订单。
// stacker.cpp -- testing the Stack class
#include <iostream>
#include <cctype> // or ctype.c
#include "stack.h"
int main()
{
using namespace std;
Stack st; // create an empty stack
char ch;
unsigned long po;
cout << "Please enter A to add a purchase order,
"
<< "P to process a PO, or Q to quit.
";
while (cin >> ch && toupper(ch) != "Q")
{
while (cin.get() != "
")
continue;
if (!isalpha(ch))
{
cout << "a";
continue;
}
switch(ch)
{
case "A":
case "a": cout << "Enter a PO number to add: ";
cin >> po;
if (st.isfull())
cout << "stack already full
";
else
st.push(po);
break;
case "p":
case "P": if (st.isempty())
cout << "Stack already empty
";
else {
st.pop(po);
cout << "PO #" << po << " popped
";
}
break;
}
cout << "Please enter A to add a purchase order,
"
<< "P process a PO, or Q to quit.
";
}
cout << "Bye
";
return 0;
}
程序清单中的while循环删除输入行中剩余部分,就现在而言这并非是必不可少的,但它使程序的修改更方便。
第10章总结
文章为作者独立观点,不代表 股票程序化软件自动交易接口观点