目录
第10章 对象和类
10.1 过程性编程和面向对象编程
10.2 抽象和类
10.2.1 类简介
接下来定义类。 一般来说, 类规范由两个部分组成。
- 类声明: 以数据成员的方式描述数据部分, 以成员函数(被称为方法) 的方式描述公有接口。
- 类方法定义: 描述如何实现类成员函数。
// stock00.h -- Stock class interface
// version 00
#ifndef STOCK00_H_
#define STOCK00_H_
#include <string>
// 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, 因为这是类对象的默认访问控制。
10.2.2 实现类成员函数
类还有两个特殊的特征:
- 定义成员函数时, 使用作用域解析运算符(::) 来标识函数所属的类;
- 类方法可以访问类的private组件。
// stock00.cpp -- implementing the Stock class
// version 00
#include <iostream>
#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)所创建的每个新对象都有自己的存储空间, 用于存储其内部变量和类成员; 但同一个类的所有对象共享同一组类方法, 即每种方法只有一个副本。
10.3 类的构造函数和析构函数
C++提供了一个特殊的成员函数——类构造函数, 专门用于构造新对象、 将值赋给它们的数据成员。 更准确地说, C++为这些成员函数提供了名称和使用语法, 而程序员需要提供方法定义。 名称与类名相同。 例如, Stock类一个可能的构造函数是名为Stock( )的成员函数。 构造函数的原型和函数头有一个有趣的特征——虽然没有返回值,但没有被声明为void类型。 实际上, 构造函数没有声明类型。
10.3.1 声明和定义构造函数
构造函数声明和定义如下:
// 构造函数声明
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)
{
...
}
10.3.2 使用构造函数
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);
10.3.3 默认构造函数
(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; //调用默认构造函数
10.3.4 析构函数
(1)析构函数完成清理工作。 如果构造函数使用new来分配内存, 则析构函数将使用delete来释放这些内存。
(2) 析构函数的名称在类名前加上~。 因此, Stock类的析构函数为~Stock( )。 另外, 和构造函数一样, 析构函数也可以没有返回值和声明类型。 与构造函数不同的是, 析构函数没有参数, 因此Stock析构函数的原型必须是这样的:
// 析构函数原型
~Stock();
// 析构函数不承担任何重要的工作
Stock::~Stock()
{
}
// 析构函数执行语句
Stock::~Stock()
{
cout << "Bye, " << company << "!\n";
}
(3)什么时候调用析构函数由编译器决定。
- 如果创建的是静态存储类对象, 则其析构函数将在程序结束时自动被调用。
- 如果创建的是自动存储类对象, 则其析构函数将在程序执行完代码块时自动被调用。
- 如果对象是通过new创建的, 则它将驻留在栈内存或自由存储区中, 当使用delete来释放内存时, 其析构函数将自动被调用。
- 最后, 程序可以创建临时对象来完成特定的操作, 在这种情况下,程序将在结束对该对象的使用时自动调用其析构函数。
- 由于在类对象过期时析构函数将自动被调用, 因此必须有一个析构函数。 如果程序员没有提供析构函数, 编译器将隐式地声明一个默认析构函数, 并在发现导致对象被删除的代码后, 提供默认析构函数的定义。
(4)构造函数匹配
Stock hot_tip = {"Derivatives Plus",100,45.0};
Stock jock {"sport Age Storage,Inc"};
Stock temp {};
在前两个声明中, 用大括号括起的列表与构造函数匹配,因此, 将使用该构造函数来创建这两个对象,创建对象jock时, 第二和第三个参数将为默认值0和0.0。 第三个声明与默认构造函数匹配,因此将使用该构造函数创建对象temp。
10.4 this指针
例如:定义一个成员函数, 它查看两个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作为调用对象的别名来完成前
面的方法定义
10.5 对象数组
和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;
}
10.6 类作用域
(1)在类中定义的名称(如类数据成员名和类成员函数名) 的作用域都为整个类, 作用域为整个类的名称只在该类中是已知的, 在类外是不可知的。因此, 可以在不同类中使用相同的类成员名而不会引起冲突。
(2)在定义成员函数时, 必须使用作用域解析运算符。
(3)使用类成员名时, 必须根据上下文使用直接成员运算符(. ) 、 间接成员运算符(->) 或作用域解析运算符(::)。
(4)作用域为类的常量:因为声明类只是描述了对象的形式, 并没有创建对象,在类如下声明不可行:const int Months = 12; 然而, 在类中有两种方式可以实现这个目标, 并且效果相同。
- 第一种:用这种方式声明枚举并不会创建类数据成员。 也就是说, 所有对象中都不包含枚举。 另外, Months只是一个符号名称, 在作用域为整个类的代码中遇到它时, 编译器将用30来替换它。
class Bakery
{
private:
enum {Months=12};
double consts[Months];
...
}
- 第二种:使用关键字static,创建一个名为Months的常量, 该常量将与其他静态变量存储在
一起, 而不是存储在对象中。
class Bakery
{
private:
static const int Months =12;
double consts[Months];
...
}
(5)作用域内枚举(C++11)
- 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};
10.7 抽象数据类型
实现栈:
// 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 <iostream>
#include <cctype> // 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;
}
第11章 对象和类
11.1 运算符重载
运算符重载使用户能够定义多个名称相同但特征标(参数列表) 不同的函数的,这被称为函数重载或函数多态, 旨在让您能够用同名的函数来完成相同的基本操作。
运算符函数的格式如下:
operatorop(argument-list)
operator +( )重载+运算符, operator *( )重载*运算符。 op必须是有效的C++运算符, 不能虚构一个新的符号。 例如, 不能有operator@( )这样的函数, 因为C++中没有@运算符。
C++对用户定义的运算符重载的限制:
- 重载后的运算符必须至少有一个操作数是用户定义的类型, 这将防止用户为标准类型重载运算符。 因此, 不能将减法运算符(-) 重载为计算两个double值的和, 而不是它们的差。 虽然这种限制将对创造性有所影响, 但可以确保程序正常运行。
- 使用运算符时不能违反运算符原来的句法规则。 例如, 不能将求模运算符(%) 重载成使用一个操作数。
- 不能修改运算符的优先级。 因此, 如果将加号运算符重载成将两个类相加, 则新的运算符与原来的加号具有相同的优先级。
- 不能创建新运算符。 例如, 不能定义operator **( )函数来表示求幂
- 不能重载下面的运算符。
sizeof: sizeof运算符。
.: 成员运算符。
. *: 成员指针运算符。
::: 作用域解析运算符。
?:: 条件运算符。
typeid: 一个RTTI运算符。
const_cast: 强制类型转换运算符。
dynamic_cast: 强制类型转换运算符。
reinterpret_cast: 强制类型转换运算符。
static_cast: 强制类型转换运算符。
- 大多数运算符都可以通过成员或非成员函数进行重载, 但下面的运算符只能通过成员函数进行重载.
=: 赋值运算符。
( ): 函数调用运算符。
[ ]: 下标运算符。
->: 通过指针访问类成员的运算符。
11.2 计算时间: 一个运算符重载示例
// 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 <iostream>
#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)不要返回指向局部变量或临时对象的引用。 函数执行完毕后, 局部变量和临时对象将消失,
引用将指向不存在的数据。
11.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:
- 虽然operator *( )函数是在类声明中声明的, 但它不是成员函数, 因此不能使用成员运算符来调用;
- 虽然operator *( )函数不是成员函数, 但它与成员函数的访问权限相同。
(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)
11.4 重载运算符: 作为成员函数还是非成员函数
对于很多运算符来说, 可以选择使用成员函数或非成员函数来实现运算符重载。
Time operator+(const Time & t) const;
friend Time operator+(const Time & t1, const Time & t2);
加法运算符需要两个操作数。 对于成员函数版本来说, 一个操作数通过this指针隐式地传递, 另一个操作数作为函数参数显式地传递; 对于友元版本来说, 两个操作数都作为参数来传递。
11.5 再谈重载: 一个矢量类
11.6 类的自动转换和强制类型转换
// 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 <iostream>
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++为类提供了下面的类型转换。
- 只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。 例如, 将int值赋给Stonewt对象时, 接受int参数的Stonewt类构造函数将自动被调用。 然而, 在构造函数声明中使用explicit可防止隐式转换, 而只允许显式转换。
- 被称为转换函数的特殊类成员运算符函数, 用于将类对象转换为其他类型。 转换函数是类成员, 没有返回类型、 没有参数、 名为operator typeName( ), 其中, typeName是对象将被转换成的类型。将类对象赋给typeName变量或将其强制转换为typeName类型时,该转换函数将自动被调用。
第13章 类继承
13.1 一个简单的基类
从一个类派生出另一个类时, 原始类称为基类, 继承类称为派生类。
// tabtenn1.h -- a table-tennis base class
#ifndef TABTENN1_H_
#define TABTENN1_H_
#include <string>
using std::string;
// simple base class
class TableTennisPlayer
{
private:
string firstname;
string lastname;
bool hasTable;
public:
TableTennisPlayer (const string & fn = "none",
const string & ln = "none", bool ht = false);
void Name() const;
bool HasTable() const { return hasTable; };
void ResetTable(bool v) { hasTable = v; };
};
// simple derived class
class RatedPlayer : public TableTennisPlayer
{
private:
unsigned int rating;
public:
RatedPlayer (unsigned int r = 0, const string & fn = "none",
const string & ln = "none", bool ht = false);
RatedPlayer(unsigned int r, const TableTennisPlayer & tp);
unsigned int Rating() const { return rating; }
void ResetRating (unsigned int r) {rating = r;}
};
#endif
(1)冒号指出RatedPlayer类的基类是TableTennisplayer类。 上述特殊的声明头表明TableTennisPlayer是一个公有基类, 这被称为公有派生。
(2)使用公有派生, 基类的公有成员将成为派生类的公有成员; 基类的私有部分也将成为派生类的一部分, 但只能通过基类的公有和保护方法访问。
(3)需要在继承特性中添加什么呢?
- 派生类需要自己的构造函数。
- 派生类可以根据需要添加额外的数据成员和成员函数。
(4)第二个Ratedplayer构造函数使用一个TableTennisPlayer参数, 该参数包括firstname、 lastname和hasTable。
(5)派生类不能直接访问基类的私有成员, 而必须通过基类方法进行访问。 例如, RatedPlayer构造函数不能直接设置继承的成员(firstname、lastname和hasTable) , 而必须使用基类的公有方法来访问私有的基类成员。
(6)RealPlayer构造函数将把实参“Mallory”、 “Duck”和true赋给形参fn、 In和ht, 然后将这些参数作为实参传递给TableTennisPlayer构造函数。
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
rating = r;
}
(7)必须首先创建基类对象, 如果不调用基类构造函数, 程序将使用默认的基类构造函数。
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, bool ht) : TableTennisPlayer()
{
rating = r;
}
(8)由于tp的类型为TableTennisPlayer &, 因此将调用基类的复制构造函数。 基类没有定义复制构造函数, 如果需要使用复制构造函数但又没有定义, 编译器将自动生成一个。
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, bool ht) : TableTennisPlayer(tp)
{
rating = r;
}
(9)也可以对派生类成员使用成员初始化列表语法。 在这种情况下, 应在列表中使用成员名, 而不是类名。
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, bool ht) : TableTennisPlayer(tp), rating(r)
{
rating = r;
}
(10)创建派生类对象时, 程序首先调用基类构造函数, 然后再调用派生类构造函数。 基类构造函数负责初始化继承的数据成员; 派生类构造函数主要用于初始化新增的数据成员。 派生类的构造函数总是调用一个基类构造函数。 可以使用初始化器列表语法指明要使用的基类构造函数, 否则将使用默认的基类构造函数。
派生类对象过期时,程序将首先调用派生类析构函数, 然后再调用基类析构函数。
(11)基类指针可以在不进行显式类型转换的情况下指向派生类对象; 基类引用可以在不进行显式类型转换的情况下引用派生类对象。
(12)基类指针可以在不进行显式类型转换的情况下指向派生类对象; 基类引用可以在不进行显式类型转换的情况下引用派生类对象,然而, 基类指针或引用只能用于调用基类方法, 因此, 不能使用rt或pt来调用派生类的ResetRanking方法。
(13)不可以将基类对象和地址赋给派生类引用和指针。
TableTennisPlayer player("Betsy", "Bloop", "true");
RatedPlayer & rr = player; //NOT ALLOWED
RatedPlayer & pr = player; //NOT ALLOWED
(14)引用兼容性属性也让您能够将基类对象初始化为派生类对象,同样, 也可以将派生对象赋给基类对象,类定义中没有这样的构造函数, 但存在隐式复制构造函数。
RatedPlayer olaf1(1840, "Betsy", "Bloop", "true");
TableTennisPlayer olaf2(olaf1);