目录
第10章 对象和类
10.1 过程性编程和面向对象编程
10.2 抽象和类
10.2.1 类简介
10.2.2 实现类成员函数
10.3 类的构造函数和析构函数
10.3.1 声明和定义构造函数
10.3.2 使用构造函数
10.3.3 默认构造函数
10.3.4 析构函数
10.4 this指针
10.5 对象数组
10.6 类作用域
10.7 抽象数据类型
第11章 对象和类
11.1 运算符重载
11.2 计算时间: 一个运算符重载示例
11.3 友元
11.4 重载运算符: 作为成员函数还是非成员函数
11.5 再谈重载: 一个矢量类
11.6 类的自动转换和强制类型转换
接下来定义类。 一般来说, 类规范由两个部分组成。
// stock00.h -- Stock class interface
// version 00
#ifndef STOCK00_H_
#define STOCK00_H_
#include
// class声明
class Stock
{
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
// 创建两个Stock对象, 它们分别名为sally和solly
Stock sally;
Stock solly;
程序解释:
(1)Stock是这个新类的类型名。 该声明让我们能够声明Stock类型的变量——称为对象或实例。接下来, 要存储的数据以类数据成员(如company和shares) 的形式出现。 例如, sally的company成员存储了公司名称, share成员存储了Sally持有的股票数量, share_val成员存储了每股的价格, total_val成员存储了股票总价格。 同样, 要执行的操作以类函数成员(方法, 如sell()和update( )) 的形式出现。 成员函数可以就地定义(如set_tot( )) , 也可以用原型表示(如其他成员函数) 。
(2)无论类成员是数据成员还是成员函数, 都可以在类的公有部分或私有部分中声明它。 但由于隐藏数据是OOP主要的目标之一, 因此数据项通常放在私有部分, 组成类接口的成员函数放在公有部分。
(3)不必在类声明中使用关键字private, 因为这是类对象的默认访问控制。
类还有两个特殊的特征:
// stock00.cpp -- implementing the Stock class
// version 00
#include
#include "stock00.h"
using namespace std;
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';
}
程序解释:
(1)其定义位于类声明中的函数都将自动成为内联函数, 因此Stock::set_tot( )是一个内联函数。 类声明常将短小的成员函数作为内联函数, set_tot( )符合这样的要求。
(2)所创建的每个新对象都有自己的存储空间, 用于存储其内部变量和类成员; 但同一个类的所有对象共享同一组类方法, 即每种方法只有一个副本。
C++提供了一个特殊的成员函数——类构造函数, 专门用于构造新对象、 将值赋给它们的数据成员。 更准确地说, C++为这些成员函数提供了名称和使用语法, 而程序员需要提供方法定义。 名称与类名相同。 例如, Stock类一个可能的构造函数是名为Stock( )的成员函数。 构造函数的原型和函数头有一个有趣的特征——虽然没有返回值,但没有被声明为void类型。 实际上, 构造函数没有声明类型。
构造函数声明和定义如下:
// 构造函数声明
Stock(); // default constructor
Stock(const std::string & co, long n = 0, double pr = 0.0);
// 构造函数定义
Stock::Stock() // default constructor
{
std::cout << "Default constructor called\n";
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\n";
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();
}
程序注释:
(1)构造函数的声明中:第一个参数是指向字符串的指针, 该字符串用于初始化成员company。 n和pr参数为shares和share_val成员提供值。 注意, 没有返回类型。 原型位于类声明的公有部分。
(2)程序声明对象时, 将自动调用构造函数
(3)如下的表示是错误的。 构造函数的参数表示的不是类成员, 而是赋给类成员的值。 因此, 参数名不能与类成员相同。为避免这种混乱, 一种常见的做法是在数据成员名中使用m_前,另一种常见的做法是, 在成员名中使用后缀_。
Stock(const string & company, long share, double share)
{
...
}
C++提供了两种使用构造函数来初始化对象的方式。 第一种方式是显式地调用构造函数,另一种方式是隐式地调用构造函数。
// 显式地调用构造函数
Stock garment = Stock("Furry Mason", 50, 2.5);
// 隐式地调用构造函数
Stock garment("Furry Mason", 50, 2.5);
下面是将构造函数与new一起使用的方法,这条语句创建一个Stock对象, 将其初始化为参数提供的值, 并将该对象的地址赋给pstock指针。 在这种情况下, 对象没有名称, 但可以使用指针来管理该对象。
Stock *pstock = new Stock("Electroshock Games", 18, 19.0);
(1)默认构造函数是在未提供显式初始值时, 用来创建对象的构造函数。
(2)如果没有提供任何构造函数, 则C++将自动提供默认构造函数。
(3)当且仅当没有定义任何构造函数时, 编译器才会提供默认构造函数。 为类定义了构造函数后, 程序员就必须为它提供默认构造函数。 如果提供了非默认构造函数(如Stock(const char * co, int n, doublepr)) , 但没有提供默认构造函数,(Stock stock1;)声明将出错。
(4)定义默认构造函数的方式有两种。 一种是给已有构造函数的所有参数提供默认值,另一种方式是通过函数重载来定义另一个构造函数——一个没有参数的构造函数。
(5)用户定义的默认构造函数通常给所有成员提供隐式初始值。
Stock::Stock() // default constructor
{
std::cout << "Default constructor called\n";
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
(6)在设计类时, 通常应提供对所有类成员做隐式初始化的默认构造函数。
(7)不要被非默认构造函数的隐式形式所误导。如下声明,第一个声明调用非默认构造函数, 即接受参数的构造函数; 第二个声明指出, second( )是一个返回Stock对象的函数。 隐式地调用默认构造函数时, 不要使用圆括号。
Stock first("Concreate Conglomerate");
Stock second(); //错误表达,不要()
Stock third; //调用默认构造函数
(1)析构函数完成清理工作。 如果构造函数使用new来分配内存, 则析构函数将使用delete来释放这些内存。
(2) 析构函数的名称在类名前加上~。 因此, Stock类的析构函数为~Stock( )。 另外, 和构造函数一样, 析构函数也可以没有返回值和声明类型。 与构造函数不同的是, 析构函数没有参数, 因此Stock析构函数的原型必须是这样的:
// 析构函数原型
~Stock();
// 析构函数不承担任何重要的工作
Stock::~Stock()
{
}
// 析构函数执行语句
Stock::~Stock()
{
cout << "Bye, " << company << "!\n";
}
(3)什么时候调用析构函数由编译器决定。
(4)构造函数匹配
Stock hot_tip = {"Derivatives Plus",100,45.0};
Stock jock {"sport Age Storage,Inc"};
Stock temp {};
在前两个声明中, 用大括号括起的列表与构造函数匹配,因此, 将使用该构造函数来创建这两个对象,创建对象jock时, 第二和第三个参数将为默认值0和0.0。 第三个声明与默认构造函数匹配,因此将使用该构造函数创建对象temp。
例如:定义一个成员函数, 它查看两个Stock对象, 并返回股价较高的那个对象的引用。比较的
方法的原型如下:
const Stock & topval(const Stock & s) const;
该函数隐式地访问一个对象, 而显式地访问另一个对象, 并返回其中一个对象的引用。 括号中的const表明, 该函数不会修改被显式地访问的对象; 而括号后的const表明, 该函数不会修改被隐式地访问的对象。由于该函数返回了两个const对象之一的引用, 因此返回类型也应为const引用。
假设要对Stock对象stock1和stock2进行比较, 并将其中股价总值较高的那一个赋给top对象, 则可以使用下面两条语句之一:
top = stock1.topval(stock2);
top = stock2.topval(stock1);
第一种格式隐式地访问stock1, 而显式地访问stock2; 第二种格式显式地访问stock1, 而隐式地访问stock2。
(1)每个成员函数(包括构造函数和析构函数) 都有一个this指针。 this指针指向调用对象。 如
果方法需要引用整个调用对象, 则可以使用表达式*this。 在函数的括号后面使用const限定符
将this限定为const, 这样将不能使用this来修改对象的值。
(2)然而, 要返回的并不是this, 因为this是对象的地址, 而是对象本身, 即*this(将解除引用
运算符*用于指针, 将得到指针指向的值) 。 现在, 可以将*this作为调用对象的别名来完成前
面的方法定义
和Stock示例一样, 用户通常要创建同一个类的多个对象,可以创建对象数组实现。
const int STKS = 4;
int main()
{
{
// create an array of initialized objects
Stock stocks[STKS] = {
Stock("NanoSmart", 12, 20.0),
Stock("Boffo Objects", 200, 2.0),
Stock("Monolithic Obelisks", 130, 3.25),
Stock("Fleep Enterprises", 60, 6.5)
};
std::cout << "Stock holdings:\n";
int st;
for (st = 0; st < STKS; st++)
stocks[st].show();
// set pointer to first element
const Stock* top = &stocks[0];
for (st = 1; st < STKS; st++)
top = &top->topval(stocks[st]);
// now top points to the most valuable holding
std::cout << "\nMost valuable holding:\n";
top->show(); }
// std::cin.get();
return 0;
}
(1)在类中定义的名称(如类数据成员名和类成员函数名) 的作用域都为整个类, 作用域为整个类的名称只在该类中是已知的, 在类外是不可知的。因此, 可以在不同类中使用相同的类成员名而不会引起冲突。
(2)在定义成员函数时, 必须使用作用域解析运算符。
(3)使用类成员名时, 必须根据上下文使用直接成员运算符(. ) 、 间接成员运算符(->) 或作用域解析运算符(::)。
(4)作用域为类的常量:因为声明类只是描述了对象的形式, 并没有创建对象,在类如下声明不可行:const int Months = 12; 然而, 在类中有两种方式可以实现这个目标, 并且效果相同。
class Bakery
{
private:
enum {Months=12};
double consts[Months];
...
}
class Bakery
{
private:
static const int Months =12;
double consts[Months];
...
}
(5)作用域内枚举(C++11)
enum class egg{Small, Medium, Large,Jumbo};
enum class t_shirt{Small, Medium, Large, Xlarge};
默认情况下, C++11作用域内枚举的底层类型为int。 另外, 还提供了一种语法, 可用于做出不同的选择,:short将底层类型指定为short。
enum class:short pizza{Small,Medium,Large,XLarge};
实现栈:
// 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 item
public:
Stack();
bool isempty() const;
bool isfull() const;
// push() returns false if stack already is 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;
}
// stacker.cpp -- testing the Stack class
#include
#include // or ctype.h
#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,\n"
<< "P to process a PO, or 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 PO number to add: ";
cin >> po;
if (st.isfull())
cout << "stack already full\n";
else
st.push(po);
break;
case 'P':
case 'p': if (st.isempty())
cout << "stack already empty\n";
else {
st.pop(po);
cout << "PO #" << po << " popped\n";
}
break;
}
cout << "Please enter A to add a purchase order,\n"
<< "P to process a PO, or Q to quit.\n";
}
cout << "Bye\n";
return 0;
}
运算符重载使用户能够定义多个名称相同但特征标(参数列表) 不同的函数的,这被称为函数重载或函数多态, 旨在让您能够用同名的函数来完成相同的基本操作。
运算符函数的格式如下:
operatorop(argument-list)
operator +( )重载+运算符, operator *( )重载*运算符。 op必须是有效的C++运算符, 不能虚构一个新的符号。 例如, 不能有operator@( )这样的函数, 因为C++中没有@运算符。
C++对用户定义的运算符重载的限制:
sizeof: sizeof运算符。
.: 成员运算符。
. *: 成员指针运算符。
::: 作用域解析运算符。
?:: 条件运算符。
typeid: 一个RTTI运算符。
const_cast: 强制类型转换运算符。
dynamic_cast: 强制类型转换运算符。
reinterpret_cast: 强制类型转换运算符。
static_cast: 强制类型转换运算符。
=: 赋值运算符。
( ): 函数调用运算符。
[ ]: 下标运算符。
->: 通过指针访问类成员的运算符。
// mytime0.h -- Time class before operator overloading
#ifndef MYTIME0_H_
#define MYTIME0_H_
class Time
{
private:
int hours;
int minutes;
public:
Time();
Time(int h, int m = 0);
void AddMin(int m);
void AddHr(int h);
void Reset(int h = 0, int m = 0);
const Time Sum(const Time & t) const;
void Show() const;
};
#endif
// mytime0.cpp -- implementing Time methods
#include
#include "mytime0.h"
Time::Time()
{
hours = minutes = 0;
}
Time::Time(int h, int m )
{
hours = h;
minutes = m;
}
void Time::AddMin(int m)
{
minutes += m;
hours += minutes / 60;
minutes %= 60;
}
void Time::AddHr(int h)
{
hours += h;
}
void Time::Reset(int h, int m)
{
hours = h;
minutes = m;
}
const Time Time::Sum(const Time & t) const
{
Time sum;
sum.minutes = minutes + t.minutes;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
void Time::Show() const
{
std::cout << hours << " hours, " << minutes << " minutes";
}
程序解释:
(1)来看一下Sum( )函数的代码。 注意参数是引用, 但返回类型却不是引用。 将参数声明为引用的目的是为了提高效率。 如果按值传递Time对象, 代码的功能将相同, 但传递引用, 速度将更快, 使用的内存将更少。
(2)然而, 返回值不能是引用。 因为函数将创建一个新的Time对象(sum) , 来表示另外两个Time对象的和。 返回对象(如代码所做的那样) 将创建对象的副本, 而调用函数可以使用它。 然而, 如果返回类型为Time &, 则引用的将是sum对象。 但由于sum对象是局部变量, 在函
数结束时将被删除, 因此引用将指向一个不存在的对象。 使用返回类型Time意味着程序将在删除sum之前构造它的拷贝, 调用函数将得到该拷贝。
(3)不要返回指向局部变量或临时对象的引用。 函数执行完毕后, 局部变量和临时对象将消失,
引用将指向不存在的数据。
C++控制对类对象私有部分的访问。 通常, 公有类方法提供唯一的访问途径, 但是有时候这种限制太严格, 以致于不适合特定的编程问题。 在这种情况下, C++提供了另外一种形式的访问权限: 友元。 友元有3种:
通过让函数成为类的友元, 可以赋予该函数与类的成员函数相同的访问权限。
为何需要友元:
// 表达式和成员函数调用
A = B*2.75;
A = B.operator*(2.75);
// 表达式不对应于成员函数, 因为2.75不是Time类型的对象
A = 2.75 * B;
// 有另一种解决方式——非成员函数
A = 2.75 * B;
A = operator*(2.75,B); //非成员函数调用匹配
Time operator*(double m, const Time & t); //函数的原型
// 非成员函数不能直接访问类的私有数据,友元函数可以访问类的私有成员
friend Time operator*(double m, const Time & t); //原型声明前加上关键字friend:
(1)创建友元
friend Time operator*(double m, const Time & t); //原型声明前加上关键字friend:
(2)常用的友元: 重载<<运算符
cout << trip;
void operator<<(ostream & os, const Time & t)
{
os << t.hours << "hours," << t.minutes << "minutes";
}
cout << "Trip time:" << trip << "(Tuesday)\n";
// 修改operator<<( )函数, 让它返回ostream对象的引用即可
ostream &operator<<(ostream & os, const Time & t)
{
os << t.hours << "hours," << t.minutes << "minutes";
return os;
}
friend Time operator*(double m, const Time & t)
{
return t*m;
}
friend std::ostream & operator<<(std::ostram & os, const Time & t);
std::ostream & operator<<(std::ostream & os, const Time & t)
对于很多运算符来说, 可以选择使用成员函数或非成员函数来实现运算符重载。
Time operator+(const Time & t) const;
friend Time operator+(const Time & t1, const Time & t2);
加法运算符需要两个操作数。 对于成员函数版本来说, 一个操作数通过this指针隐式地传递, 另一个操作数作为函数参数显式地传递; 对于友元版本来说, 两个操作数都作为参数来传递。
// stonewt1.h -- revised definition for the Stonewt class
#ifndef STONEWT1_H_
#define STONEWT1_H_
class Stonewt
{
private:
enum {Lbs_per_stn = 14}; // pounds per stone
int stone; // whole stones
double pds_left; // fractional pounds
double pounds; // entire weight in pounds
public:
Stonewt(double lbs); // construct from double pounds
Stonewt(int stn, double lbs); // construct from stone, lbs
Stonewt(); // default constructor
~Stonewt();
void show_lbs() const; // show weight in pounds format
void show_stn() const; // show weight in stone format
// conversion functions
operator int() const;
operator double() const;
};
#endif
// stonewt1.cpp -- Stonewt class methods + conversion functions
#include
using std::cout;
#include "stonewt1.h"
// construct Stonewt object from double value
Stonewt::Stonewt(double lbs)
{
stone = int (lbs) / Lbs_per_stn; // integer division
pds_left = int (lbs) % Lbs_per_stn + lbs - int(lbs);
pounds = lbs;
}
// construct Stonewt object from stone, double values
Stonewt::Stonewt(int stn, double lbs)
{
stone = stn;
pds_left = lbs;
pounds = stn * Lbs_per_stn +lbs;
}
Stonewt::Stonewt() // default constructor, wt = 0
{
stone = pounds = pds_left = 0;
}
Stonewt::~Stonewt() // destructor
{
}
// show weight in stones
void Stonewt::show_stn() const
{
cout << stone << " stone, " << pds_left << " pounds\n";
}
// show weight in pounds
void Stonewt::show_lbs() const
{
cout << pounds << " pounds\n";
}
// conversion functions
Stonewt::operator int() const
{
return int (pounds + 0.5);
}
Stonewt::operator double()const
{
return pounds;
}}
(1)只有接受一个参数的构造函数才能作为转换函数。 下面的构造函数有两个参数, 因此不能用来转换类型,然而, 如果给第二个参数提供默认值, 它便可用于转换int。
Stonewt(int stn, double lbs);
Stonewt(int stn, double lbs = 0);
(2)只接受一个参数的构造函数定义了从参数类型到类类型的转换。 如果使用关键字explicit限定
了这种构造函数, 则它只能用于显示转换, 否则也可以用于隐式转换。
// 声明构造函数
explict Stonewt(double lbs);
// 上述将关闭隐式转换, 但仍然允许显式转换, 即显式强制类型转换
Stone myCat;
myCat = 19.6; // ×
maCat = Stonewt(19.6); // √
myCat = (Stonewt) 19.6; // √
(3)函数原型化提供的参数匹配过程, 允许使用Stonewt(double) 构造函数来转换其他数值类型。 也就是说, 下面两条语句都首先将int转换为double, 然后使用Stonewt(double) 构造函
数。当且仅当转换不存在二义性时, 才会进行这种二步转换。 也就是说, 如果这个类还定义了构造函数Stonewt(long) , 则编译器将拒绝这些语句, 可能指出: int可被转换为long或double
(4)转换函数:可以将Stonewt对象转换为double值。转换为typeName类型, 需要使用这种形式的转换函数:
operator typeName();
请注意以下几点:
总之, C++为类提供了下面的类型转换。