0%

class of cpp

所有内容来自于c++ primer plus
https://zh.zxian.ru/book/11751558/efaa69/cprimer-plus%E7%AC%AC6%E7%89%88%E4%B8%AD%E6%96%87%E7%89%88.html

第十章exercise

10.7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <string>
using namespace std;
class plorg {
private:
string m_name;
int m_CI;

public:
explicit plorg(string name = "Plorga", int CI = 50)
: m_name(name), m_CI(CI) {}
plorg(int CI) : m_name("Plorga"), m_CI(CI) {}
void set_CI(int CI) { m_CI = CI; }
void show_plorg() { cout << m_name << " " << m_CI << endl; }
};
int main() {
plorg p1("myp1", 30);
plorg p2("myp2");
plorg p3(30);
p1.show_plorg();
p2.show_plorg();
p3.show_plorg();
p1.set_CI(40);
p1.show_plorg();
return 0;
}

注意其中的构造函数

第十一章

运算符重载

1
2
total = coding + fixing;
total = coding.operator+(fixing);

运算符重载中,左侧对象(coding)是调用对象,右侧(fixing)作为参数。

友元

1
2
A = B * 2.75;
A = B.operator*(2.75);

但是对于下面的语句:

1
A = 2.75 * B;

以上表达式不属于成员函数,因为2.75不是对象。解决方法为——非成员函数。

1
2
3
A = 2.75 * B;
A = operator*(2.75,B);
Time operator*(double m,const Time & t);
  • 原型放在类声明中
1
friend Time operator*(double m,const Time & t);
  1. 虽然operator*在类声明中声明,但不是成员函数
  2. 与成员函数访问权限相同
  • 编写定义不使用Time::和friend

重载<<

1
2
cout << trip;
operator<<(cout,trip);

第一个是ostream类对象(cout)

1
2
3
4
5
6
7
cout << x << y;
(cout << x) << y;
ostream & operator<<(ostream & os,const Time & t){
os << t.hour << t.minute;
return os;
}
cout << "Trip time" << trip << "Tuesday";

同时还可以用于写入文件

1
2
3
4
5
6
#include<fstream>
...
ofstream fout;
fout.open("savetime.txt");
Time trip(12,40);
fout << trip;

重载运算符:作为成员函数还是非成员函数

一般来说,非成员函数应该是友元函数,才能访问私有数据。

1
2
Time operator+(const Time & t) const;//member version
friend Time operator+(const Time & t1,const Time & t2);//nonmember version

对成员函数,一个操作数通过this指针隐式传递,另一个为参数;对于友元函数,两个都是参数。

类的自动转换和强制类型转换

1
2
3
Stonewt(double lbs);
Stonewt mycat;
mycat = 19.6;

程序将使用构造函数 Stanew(double)来创建一个临时的 Stonewt对象,并将19.6 作为初始化值,这一过程称为隐式转换,因为它是自动进行的, 而不需要显式强制类型转换。只有接受一个参数的构造函数才能作为转换函数。

下面的构造函数有两个参数,因此不能用来转换 类型:

1
Stonewt(int stn, double lbs);

然而,如果给第二个参数提供默认值,它便可用于转换int:

1
Stonewt(int stn, double lbs=0);

这种自动特性并非总是合乎需要的,因为这会导致意外的类型转换。因此,C++新增了关键字explicit, 用于关闭这种口动特性。也就是说,可以这样声明构造函数:

1
explicit Stonewt(double lbs);

这将关闭上述示例中介绍的隐式转换,但仍然允许显式转换,即显式强制类型转换:

1
2
3
4
Stonewt mycat;
mycat = 19.6;//not valid
mycat = Stonewt(19.6);
mycat =(Stonewt) 19.6;

如果在声明中使用了关链字 explicit,则 Stonew(double)将只用于显式强制类型转换,否则还可以用于下面的隐式转换:

  1. Stonewt 对象初始化为 double 值时。
  2. 将double 值赋给 Stonewt 对象时。
  3. double 值传递给接受 Stonewt 参数的函数时。
  4. 返回值被声明为 Stonewt 的函数试图返回 double 值时。

函数原型化提供的参数匹配过程,允许使用 Stonewt(double)构造函数来转换 其他数值类型。也就是说,下面两条语句都先将int转换为 double,然后使用 Stonewt(double)构造函数:

1
2
Stonewt Jumbo(7000);
Jumbo = 7000;

然而,当且仅当转换不存在二义性时,才会进行这种二步转换。也就是说,如果这个类还定义了构造 函数 Stonewt(long),则编译器将拒绝这些语句,可能指出:int 可被转换为long或double,因此调用存在 二义性。

转换函数

将 Stonewt 对 象转换为 double 值,必须使用特殊的 C++运算符函数.——转换函数。转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。

1
2
3
4
Stonewt wolfe(285.7);
double host = double (wolfe);
double thinker = (double) wolfe;
double star = wolfe;
  1. 转换函数必须是类方法;
  2. 转换函数不能指定返回类型;
  3. 换函数不能有参数。

operator typeNane();

1
2
3
Stonewt;:operator int() const{
return int (pounds + 0.5);
}

同样可以使用explicit。有了这些声明后,需要强制转换时将调用这些运算符。另种方法是,用一个功能相同的非转换函数替换该转换函数即可,但仅在被显式地调用时,该函数 才会执行。

1
2
 explicit operator double() const;
int Stonewt::Stone_to_Int(){ return int(pounds + 0.5);}

转换函数和友元函数

下面为 Stonewt类重载加法运算符,依旧有两种方式。任意一种都允许:

1
2
3
4
Stonewt jennySt(9,12);
Stonewt bennySt(12,8);
Stonewt total;
total = jennySt + bennySt;

如果提供了Stonewt (double) 构造函数,也可以这样做:

1
2
3
4
Stonewt jennySt(9,12);
double kennyD = 176.0;
Stonewt total;
total = jennySt + kennyD;

但只有友元函数才允许如下调用:

1
2
3
4
Stonewt jennySt(9,12);
double pennyD = 146.0;
Stonewt total;
total = pennyD + jennySt;

对此,将每一种加法转为相应的函数调用:

1
2
3
total = jennySt + bennySt;
total = jennySt.operator+(bennySt);
total = operator+(jennySt,bennySt);

上述两种转换中,实参的类型都和形参匹配。另外,成员函数是通过 Stonewt 对象调用的。

1
2
3
total = jennySt + kennyD;
total = jennySt.operator+(kennyD);
total = operator+(jennySt,kennyD);

同样,成员函数也是通过 Stonewt对象调用的。这一次,每个调用中都有一个参数(kennyD)是double 类型的,因此将调用Stonewt(double)构造函数,将该参数转换为Stonewt 对象。

1
2
3
total = pennyD + jennySt;
total = operator+(pennyD,jennySt);
total = pennyD.operator+(jennyst); // not meaningful

这没有意义,因为只有类对象才可以调用成员函数。C++不会试图将 pennyD转换为 Stonewt 对象。将 对成员函数参数进行转换。而不是调用成员函数的对象。

这里的经验是,将加法定义为友元可以让程序更容易适应白动类型转换。原因在于,两个操作数都成 为函数参数,因此与函数原型匹配。

  • 实现加法时的选择

要将 double 量和 Stonewt 量相加,有两种选择。第一种方法是将下面的函数定义为友元:

1
operator+(const Stonewt &, const Stonewt&)

第二种方法是。将加法运算符重载为一个显式使用 double 类型参数的函数:

1
2
3
Stonewt operator+(double x);//total = jennySt + kennyD;
friend Stonewt operator+(double x,Stonewt & s);
total = pennyD + jennySt;

第十二章

动态内存和类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class StringBad {
private:
char * str;
int len;
static int num_strings;//记录所创建的对象数目。

public:
StringBad(const char* s);
StringBad();
~StringBad();
StringBad & operator=(const StringBad & st);
StringBad & operator=(const StringBad & st);//用于普通字符串
char & operator[](int i);
const char & operator[](int i) const;
friend ostream & operator<<(ostream & os,const StringBad & st);
};

将 num_strings 成员声明为静态存储类。静态类成员有一个特点:无论创建了多少对象,程序都 只创建一个静态类变量副本。也就是说,类的所有对象共享同一个静态成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 int StringBad::num_strings = 0;
StringBad::StringBad(const char * s){//deep copy
len = strlen(s);
str = new char[len+1];//strlen返回字符串长度,但不包括末尾的空字符
strcpy(str, s);
num_strings++;
}
StringBad::StringBad(){
len = 0;
str = new char[1];
str[0] = '\0';//以上两句可简化为str = 0;即空指针
}
StringBad::~StringBad(){
--num_strings;
delete [] str;
}
ostream & operator<<(ostream & os,const StringBad & st){
os << st.str;
return os;
}

对于第一句,不能在类声明中初始化静态成员变量,这 是因为声明描述了如何分配内存,但并不分配内存。可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存 储的,而不是对象的组成部分。初始化语句指出了类型,并使用了作用域运算符,但没有使用关 键字 static。

初始化是在方法文件中,而不是在类声明文件(头文件)中进行的。但如果静态成员是整型或枚举型 const,则可以在类声明中初始化。

1
str = s;

这只保存了地址,而没有创建字符串副本。

1
2
StringBad sailor = sport;
StringBad sailor = StringBad(sport);//StringBad(const StringBad &);

使用一个对象来初始化另一个对象时,编译器将白动生成拷贝构造函数,不知道需要更新静态变量num_strings。

默认构造函数

带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。

1
Klunk(int n = 0){ klunk_ct = n; }

但只能有一个默认构造函数。也就是说,不能这样做:

1
2
Klunk(int n = 0){ klunk_ct = n; }
Klunk(){ klunk_ct = 0; }

拷贝构造

1
2
3
4
stringBad ditto(motto);
stringBad metto = motto;
stringBad also = stringBad(motto);
stringBad * pstringBad = new stringBad(motto);

赋值运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
StringBad & StringBad::operator=(const StringBad & st){
if (this == &st)
return *this;
delete [] str;
len = st.len;
str = new char [len+1];
strcpy(str, st.str);
return *this;
}//依旧深拷贝
StringBad & operator=(const StringBad & st){
delete [] str;
len = strlen(s);
str = new char [len+1];
strcpy(str, s);
return *this;
}

使用中括号表示法访问字符

1
2
3
4
5
6
char & StringBad::operator[](int i){
return str[i];
}
const char & StringBad::operator[](int i)const{
return str[i];
}

静态成员函数

可以将成员函数声明为静态的(函数声明必须包含关键字 static,但如果函数定义是独立的,则其中不 能包含关键字 static)。

  1. 不能通过对象调用静态成员函数;实际上,静态成员函数甚至不能使用this 指针。如果静态成 员函数是在公有部分声明的,则可以使用类各和作用域解析运算符来调用它。
  2. 由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。e.g.
1
2
static int howmany(){ return num_string;}
int count = String::howmany();

重载>>

1
2
3
4
5
6
7
8
9
10
11
private:static const int CINLIM = 80;//cin imput limit
public:friend istream & operator>>(istream & is,String & st);//声明
istream & operator>>(istream & is,String & st){
char temp[String::CINLIM];
is.get(temp, String::CINLIM);
if(is)
st = temp;
while (is && is.get() != '\n')
continue;
return is;
}

第十三章

一个简单的基类

1
2
3
4
5
6
7
8
class tabletennisplayer{
private:
string m_name;
bool hastable;
public:
tabletennisplayer(const string & name = "none",bool ht = false);
void name() const;
};
1
2
3
4
5
6
7
class ratedplayer : public tabletennisplayer{
private:
int rating;
public:
ratedplayer(int r = 0,const string & name = "none",bool ht = false);
ratedplayer(int r = 0,const tabletennisplayer & tp);
};

派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。具体地说,派生类构造函数必须使用基类构造函数。创建派生类对象时,程序首先创建基类对象。

1
2
3
4
5
6
ratedplayer::ratedplayer(int r = 0,const string & name = "none",bool ht = false) : tabletennisplayer(name,ht){
rating = r;
}
ratedplayer::ratedplayer(int r = 0,const tabletennisplayer & tp) : tabletennisplayer(tp){
rating = r;
}//列表初始化亦可
  1. 派生类对象可以使用基类的方法,条件是方法不是私 有的。
  2. 基类指针可以在不进行显式类型转换的情况下指向派生类对象。
  3. 基类引用可 以在不进行显式类型转换的情况下引用派生类对象。
1
2
3
4
5
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
TableTennisPlayer & rt = rplayer1;
TableTennisPlayer * pt = &rplayer1;
rt.Name(); // invoke Name() with reference
pt->Name(); // invoke Name() with pointer
  1. 基类指针或引用只能用于调用基类方法
  2. 不可以将基类对象和地址赋给派生类引用和指针。

多态公有继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// brass.h -- bank account classes
#ifndef BRASS_H_
#define BRASS_H_
#include <string>
// Brass Account Class

class Brass {
private:
std::string fullName;
long acctNum;
double balance;

public:
Brass(const std::string& s = "Nullbody", long an = -1, double bal = 0.0);
void Deposit(double amt);
virtual void Withdraw(double amt);
double Balance() const;
virtual void ViewAcct() const;
virtual ~Brass() {}
};

// Brass Plus Account Class
class BrassPlus : public Brass {
private:
double maxLoan;
double rate;
double owesBank;

public:
BrassPlus(const std::string& s = "Nullbody", long an = -1, double bal = 0.0,
double ml = 500, double r = 0.11125);
BrassPlus(const Brass& ba, double ml = 500, double r = 0.11125);
virtual void ViewAcct() const;
virtual void Withdraw(double amt);
void ResetMax(double m) { maxLoan = m; }
void ResetRate(double r) { rate = r; };
void ResetOwes() { owesBank = 0; }
};

#endif

两个 ViewAcct()原型表明将有2个独立的方 法定义。基类版本的限定名为 Brass::VicewAcct( ),派生类版本的限定名为 BrassPlus::ViewAcc( )。程序将 使用对象类型来确定使用哪个版本:

1
2
3
4
5
Brass dom("Dominic Banker", 11224, 4183.45);  
BrassPlus dot("Dorothy Banker", 12118, 2592.00);

dom.ViewAcct(); // use Brass::ViewAcct()
dot.ViewAcct(); // use BrassPlus::ViewAcct()

如果方法是通过引用或指针而不是对象调用的,它将确定使 用哪一种方法。如果没有使用关键字 virual,程序将根据引用类型或指针类型选择方法;如果使用了 virtual, 程序将根据引用或指针指向的对象的类型来选择方法。

如果ViewAcct( )不是虚的,则程序的行为如下:

1
2
3
4
5
6
7
Brass dom("Dominic Banker", 11224, 4183.45);  
BrassPlus dot("Dorothy Banker", 12118, 2592.00);

Brass & b1_ref = dom;
Brass & b2_ref = dot;
b1_ref.ViewAcct(); // use Brass::ViewAcct()
b2_ref.ViewAcct(); // use Brass::ViewAcct()

引用变量的类型为 Brass,所以选择了 Brass::ViewAccount( )。使用 Brass 指针代替引用时,行为将与 此类似。

如果ViewAcct( )是虚的,则程序的行为如下:

1
2
3
4
5
6
7
Brass dom("Dominic Banker", 11224, 4183.45);  
BrassPlus dot("Dorothy Banker", 12118, 2592.00);

Brass & b1_ref = dom;
Brass & b2_ref = dot;
b1_ref.ViewAcct(); // use Brass::ViewAcct()
b2_ref.ViewAcct(); // use BrassPlus::ViewAcct()

这里两个引用的类型都是 Brass.但 b2_ref引用的是一个 BrassPlus 对象,所以使用的是 BrassPlus::ViewAcct( ),使用 Brass指针代替引用时,行为将类似。

在基类中将派生类会重新定义的方法声明为 虚方法。方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。然而,在派生类声明中使用关 键字 virtual 来指出哪些函数是虚函数也不失为一个好办法。

基类声明了一个虚析构函数。这样做是为了确保释放派生对象时,按正确的顺序调用析构 函数。

注意,关键字 virtual只用于类声明的方法原型中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// brass.cpp -- bank account class methods
#include <iostream>
#include "brass.h"
using std::cout;
using std::endl;
using std::string;

// formatting stuff
typedef std::ios_base::fmtflags format;
typedef std::streamsize precis;
format setFormat();
void restore(format f, precis p);

// Brass methods

Brass::Brass(const string & s, long an, double bal)
{
fullName = s;
acctNum = an;
balance = bal;
}

void Brass::Deposit(double amt)
{
if (amt < 0)
cout << "Negative deposit not allowed; "
<< "deposit is cancelled.\n";
else
balance += amt;
}

void Brass::Withdraw(double amt)
{
// set up ###.## format
format initialState = setFormat();
precis prec = cout.precision(2);

if (amt < 0)
cout << "Withdrawal amount must be positive; "
<< "withdrawal canceled.\n";
else if (amt <= balance)
balance -= amt;
else
cout << "Withdrawal amount of $" << amt
<< " exceeds your balance.\n"
<< "Withdrawal canceled.\n";
restore(initialState, prec);
}

double Brass::Balance() const
{
return balance;
}

void Brass::ViewAcct() const
{
// set up ###.## format
format initialState = setFormat();
precis prec = cout.precision(2);
cout << "Client: " << fullName << endl;
cout << "Account Number: " << acctNum << endl;
cout << "Balance: $" << balance << endl;
restore(initialState, prec); // restore original format
}

// BrassPlus Methods
BrassPlus::BrassPlus(const string & s, long an, double bal, double ml, double r) : Brass(s, an, bal)
{
maxLoan = ml;
owesBank = 0.0;
rate = r;
}

BrassPlus::BrassPlus(const Brass & ba, double ml, double r) : Brass(ba) // uses implicit copy constructor
{
maxLoan = ml;
owesBank = 0.0;
rate = r;
}

// redefine how ViewAcct() works
void BrassPlus::ViewAcct() const
{
// set up ###.## format
format initialState = setFormat();
precis prec = cout.precision(2);

Brass::ViewAcct(); // display base portion
cout << "Maximum loan: $" << maxLoan << endl;
cout << "Owed to bank: $" << owesBank << endl;
cout.precision(3); // ###.#### format
cout << "Loan Rate: " << 100 * rate << "$\n";
restore(initialState, prec);
}

// redefine how Withdraw() works
void BrassPlus::Withdraw(double amt)
{
// set up ###.## format
format initialState = setFormat();
precis prec = cout.precision(2);

double bal = Balance();
if (amt <= bal)
Brass::Withdraw(amt);
else if ( amt <= bal + maxLoan - owesBank)
{
double advance = amt - bal;
owesBank += advance * (1.0 + rate);
cout << "Bank advance: $" << advance << endl;
cout << "Finance charge: $" << advance * rate << endl;
Deposit(advance);
Brass::Withdraw(amt);
}
else
cout << "Credit limit exceeded. Transaction cancelled.\n";
restore(initialState, prec);
}

format setFormat()
{
// set up ###.## format
return cout.setf(std::ios_base::fixed,
std::ios_base::floatfield);
}

void restore(format f, precis p)
{
cout.setf(f, std::ios_base::floatfield);
cout.precision(p);
}

这几个构造函数都使用成员初始化列表语法,将基类信息传递给基类构造两数,然后使用构造函数体 初始化 BrassPlus类新增的数据项。非构造函数不能使用成员初始化列表语法,但派生类方法可以调用公有的基类方法。例如,BrassPlus 版本的 ViewAcct( )核心内容如下:

1
2
3
4
5
6
7
8
void BrassPlus::ViewAcct() const {
...
Brass::ViewAcct();
cout << "Maximum loan: $" << maxLoan << endl;
cout << "Owed to bank: $" << owesBank << endl;
cout << "Loan Rate: " << 100 * rate << "$\n";
...
}

换句话说,BrassPlus::ViewAccet( )显示新增的 BrassPlus 数据成员,并调用基类方法 Brass:ViewAcct( ) 来显示基类数据成员。在派生类方法中,标准技术是使用作用域解析运算符来调用基类方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void BrassPlus::Withdraw(double amt)
{
...
double bal = Balance();
if (amt <= bal)
Brass::Withdraw(amt);
else if ( amt <= bal + maxLoan - owesBank)
{
double advance = amt - bal;
owesBank += advance * (1.0 + rate);
cout << "Bank advance: $" << advance << endl;
cout << "Finance charge: $" << advance * rate << endl;
Deposit(advance);
Brass::Withdraw(amt);
}
else
cout << "Credit limit exceeded. Transaction cancelled.\n";
...
}

该方法使用基类的 Balance()函数来确定结余。因为派生类没有重新定义该方法,代码不必对 Balance() 使用作用域解析运算符。

  • 应用

假设要同时管理 Brass 和 BrassPlus 账户,如果能使用同·个数组来保存 Brsss 和 BrassPlus 对象,将很有帮助,但这是不可能的。数组中所有元素的类型必须相同,Brass 和 BrassPlus 是不同的类型。然而,可以创建指向 Brass 的指针数组。这样,每个元素的类型都相同,但由于使用的是 公有继承模型,因此 Brass指针既可以指向 Brass对象,也可以指向 BrassPlus 对象。因此,可以使用一个 数组来表示多种类型的对象,这就是多态性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// usebrass2.cpp -- polymorphic example
// compile with brass.cpp
#include <iostream>
#include <string>
#include "brass.h"
const int CLIENTS = 4;

int main()
{
using std::cin;
using std::cout;
using std::endl;

Brass * p_clients[CLIENTS];
std::string temp;
long tempnum;
double tempbal;
char kind;

for (int i = 0; i < CLIENTS; i++)
{
cout << "Enter client's name: ";
getline(cin,temp);
cout << "Enter client's account number: ";
cin >> tempnum;
cout << "Enter opening balance: $";
cin >> tempbal;
cout << "Enter 1 for Brass Account or "
<< "2 for BrassPlus Account: ";

while (cin >> kind && (kind != '1' && kind != '2'))
cout <<"Enter either 1 or 2: ";
if (kind == '1')
p_clients[i] = new Brass(temp, tempnum, tempbal);
else
{
double tmax, trate;
cout << "Enter the overdraft limit: $";
cin >> tmax;
cout << "Enter the interest rate "
<< "as a decimal fraction: ";
cin >> trate;
p_clients[i] = new BrassPlus(temp, tempnum, tempbal,
tmax, trate);
}
while (cin.get() != '\n')
continue;
}
cout << endl;
for (int i = 0; i < CLIENTS; i++)
{
p_clients[i]->ViewAcct();
cout << endl;
}
for (int i = 0; i < CLIENTS; i++)
{
delete p_clients[i]; // free memory
}
cout << "Done.\n";
return 0;
}

多态性是由下述代码提供的:

1
2
3
4
for (int i = 0; i < CLIENTS; i++) {
p_clients[i]->ViewAcct();
cout << endl;
}

如果数组成员指向的是Brass 对象,则调用 Brass::ViewAcct()+如果指向的是 BrassPlus 对象,则调用 BrassPlus::ViewAcct( ).如果 Brass::ViewAcct( )没有被声明为虚的,则在任何情况下都将调用 Brass::ViewAcct()。

如果析构函数不是虚的,则将只调用对应于指针类型的析构两数。对于程 序清单13.10,这意味着只有 Brass的析构函数被调用,即使指针指向的是一个 BrassPlus 对象。如果析构函 数是虚的,将调用相应对象类型的析构函数。因此,如果指针指向的是BrassPlus 对象,将调用 BrassPlus 的 析构的数,然后自动调用基类的析构函数。

指针和引用类型的兼容性

通常,C++不允许将一种类型的地址赋给 另一种类型的指针,也不允许一种类型的引用指向另一种类型。然而,正如您看到的,指向基类的引用或指针可以引用派生类对象,而不必进行显式类型转换。例如, 下面的初始化是允许的:

1
2
3
BrassPlus dilly ("Annie Dill", 493222, 2000);
Brass * pb = &dilly; // ok
Brass & rb = dilly; // ok

将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasing),这使公有继承不需要进 行显式类型转换。相反的过程——将基类指针或引用转换为派生类指针或引用——称为向下强制转换(downcasting)。 如果不使用显式类型转换,则向下强制转换是不允许的。

有关虚函数注意事项

  1. 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生 出来的类)中是虚的。
  2. 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为 引用或指针类型定义的方法。
  3. 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。

  1. 构造函数不能是虚两数。创建派生类对象时,将调用派生类的构造函数。而不是基类的构造函数,然 后,派生类的构造函数将使用基类的一个构造函数。
  2. 析构函数应当是虚函数,除非类不用做基类。
  3. 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。如果由于这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决。
  4. 如果派生类没有重新定义函数,将使用该西数的基类版本。如果派生类位于派生链中,则将使用最新 的虚函数版本,例外的情况是基类版本是隐藏的。
  5. 重新定义将隐藏方法:
1
2
3
4
5
6
7
8
9
10
11
12
class Dwelling
{
public:
virtual void showperks(int a) const;
...
};
class Hovel : public Dwelling
{
public:
virtual void showperks() const;
...
};

新定义将 showperks( )定义为一个不接受任何参数的函数。重新定义不会生成函数的两个重载版本, 而是隐藏了接受一个int 参数的基类版本。总之,重新定义继承的方法并不是重载。如果在派生类中重 新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标 如何。

  • 如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返 回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Dwelling
{
public:
// a base method
virtual Dwelling & build(int n);
...
};

class Hovel : public Dwelling
{
public:
// a derived method with a covariant return type
virtual Hovel & build(int n); // same function signature
...
};

这种例外只适用于返回值,而不适用于参数。

  • 如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Dwelling
{
public:
// three overloaded showperks()
virtual void showperks(int a) const;
virtual void showperks(double x) const;
virtual void showperks() const;
...
};

class Hovel : public Dwelling
{
public:
// three redefined showperks()
virtual void showperks(int a) const;
virtual void showperks(double x) const;
virtual void showperks() const;
...
};

如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。注意,如果不需 要修改,则新定义可只调用基类版本。

访问控制:protected

关键字 protected 与 private 相似,在类外只能用公有类成员 来访问 protected 部分中的类成员。private 和 protected 之间的区别只有在基类派生的类中才会表现出来。派 生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说, 保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。

1
2
3
4
5
6
class Brass
{
protected:
double balance;
...
};

在这种情况下,BrassPlus类可以直接访问 balance,而不需要使用 Brass 方法。

1
2
3
4
void BrassPlus::Reset(double amt)
{
balance = amt;
}

Brass 类被设计成只能通过 Deposit( )和 Withdraw( )才能修改 balance,但对于 BrassPlus 对象,Reset( )方 法将忽略 Withdraw( )中的保护措施,实际上使 balance 成为公有变量。

最好对类数据成员采用私有访问控制,不要使用保护话问控制;同时通过基类方法使派生类能 够访问基类数据。热而,对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能使用的内部函数。

抽象基类

从 Ellipse和 Cicle 类中抽象出它们的共性,将这些特性放到一个 ABC中,然 后从该ABC派生出Circle 和 Elipse 类,这样,便可以使用基类指针数组同时管理 Circle 和 Ellipse 对象, 即可以使用多态方法)。在这个例子中,这两个类的共同点是中心坐标、Move( )方法(对于这两个类是相 同的)和Area( )方法(对于这两个类来说,是不同的)。确实,甚至不能在ABC中实现Area()方法.因为 它没有包含必要的数据成员。

C++通过使用纯虚函数(pure viitual function)提供末实现的函数。纯虚函数 声明的结尾处为=0。

1
2
3
4
5
6
7
8
9
10
11
12
13
class BaseEllipse // abstract base class
{
private:
double x; // x-coordinate of center
double y; // y-coordinate of center
...
public:
BaseEllipse(double x0 = 0, double y0 = 0) : x(x0),y(y0) {}
virtual ~BaseEllipse() {}
void Move(int nx, int ny) { x = nx; y = ny; }
virtual double Area() const = 0; // a pure virtual function
...
};

当类声明中包含纯虚函数时。则不能创建该类的对象。这里的理念是,包含纯虚函数的类只用作基类。原型中的-0使虚函数成为纯虚函数。这里的方法Area( ) 没有定义,但 C++甚至允许纯虚函数有定义。

使用这些类的程序将能够创建 Elipse 对象和 Cicle对象,但是不能创建 BascEllipse 对象。由于 Circle 和 Ellipse 对象的基类相同,因此可以用 BastEllipse指针数组同时管理这两种对象。像 Circle 和 Ellipse 这 样的类有时被称为具体(concrete)类,这表示可以创建这些类型的对象。总之,ABC 描述的是至少使用一个纯虚函数的接口,从ABC 派生出的类将根据派生类的具体特征, 使用常规虚函数来实现这种接口。

  • 应用

头文件声明了AcctABC类(ABC)、Brass类和 BrassPlus类(两者都是具体类)。为 帮助派生类访问基类数据,AcctABC提供了一些保护方法:派生类方法可以调用这些方法,但它们并不是 派生类对象的公有接口的组成部分。AcctABC 还提供 一个保护成员函数,用于处理格式化(以前是使用非 成员函数处理的),另外,AcctABC类还有两个纯虚函数,所以它确实是抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// acctabc.h -- bank account classes
#ifndef ACCTABC_H_
#define ACCTABC_H_
#include <iostream>
#include <string>

// Abstract Base Class
class AcctABC
{
private:
std::string fullName;
long acctNum;
double balance;
protected:
struct Formatting
{
std::ios_base::fmtflags flag;
std::streamsize pr;
};
const std::string & FullName() const {return fullName;}
long AcctNum() const {return acctNum;}
Formatting SetFormat() const;
void Restore(Formatting & f) const;
public:
AcctABC(const std::string & s = "Nullbody", long an = -1, double bal = 0.0);
void Deposit(double amt);
virtual void Withdraw(double amt) = 0; // pure virtual function
double Balance() const {return balance;}
virtual void ViewAcct() const = 0; // pure virtual function
virtual ~AcctABC() {}
};

// Brass Account Class
class Brass :public AcctABC
{
public:
Brass(const std::string & s = "Nullbody", long an = -1, double bal = 0.0) : AcctABC(s, an, bal) { }
virtual void Withdraw(double amt);
virtual void ViewAcct() const;
virtual ~Brass() {}
};

//Brass Plus Account Class
class BrassPlus : public AcctABC
{
private:
double maxLoan;
double rate;
double owesBank;
public:
BrassPlus(const std::string & s = "Nullbody", long an = -1, double bal = 0.0, double ml = 500, double r = 0.10);
BrassPlus(const Brass & ba, double ml = 500, double r = 0.1);
virtual void ViewAcct()const;
virtual void Withdraw(double amt);
void ResetMax(double m) { maxLoan = m; }
void ResetRate(double r) { rate = r; };
void ResetOwes() { owesBank = 0; }
};

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// acctabc.cpp -- bank account class methods
#include <iostream>
#include "acctabc.h"
using std::cout;
using std::ios_base;
using std::endl;
using std::string;

// Abstract Base Class
AcctABC::AcctABC(const string & s, long an, double bal)
{
fullName = s;
acctNum = an;
balance = bal;
}

void AcctABC::Deposit(double amt)
{
if (amt < 0)
cout << "Negative deposit not allowed; "
<< "deposit is cancelled.\n";
else
balance += amt;
}

void AcctABC::Withdraw(double amt)
{
balance -= amt;
}

// protected methods for formatting
AcctABC::Formatting AcctABC::SetFormat() const
{
// set up ###.### format
Formatting f;
f.flag = cout.setf(ios_base::fixed, ios_base::floatfield);
f.pr = cout.precision(2);
return f;
}

void AcctABC::Restore(Formatting & f) const
{
cout.setf(f.flag, ios_base::floatfield);
cout.precision(f.pr);
}

// Brass methods
void Brass::Withdraw(double amt)
{
if (amt < 0)
cout << "Withdrawal amount must be positive; "
<< "withdrawal canceled.\n";
else if (amt <= Balance())
AcctABC::Withdraw(amt);
else
cout << "Withdrawal amount of $" << amt
<< " exceeds your balance.\n"
<< "Withdrawal canceled.\n";
}

void Brass::ViewAcct() const
{
Formatting f = SetFormat();
cout << "Brass Client: " << FullName() << endl;
cout << "Account Number: " << AcctNum() << endl;
cout << "Balance: $" << Balance() << endl;
Restore(f);
}

// BrassPlus Methods
BrassPlus::BrassPlus(const string & s, long an, double bal,
double ml, double r) : AcctABC(s, an, bal)
{
maxLoan = ml;
owesBank = 0.0;
rate = r;
}

BrassPlus::BrassPlus(const Brass & ba, double ml, double r)
: AcctABC(ba) // uses implicit copy constructor
{
maxLoan = ml;
owesBank = 0.0;
rate = r;
}

void BrassPlus::ViewAcct() const
{
Formatting f = SetFormat();

cout << "BrassPlus Client: " << FullName() << endl;
cout << "Account Number: " << AcctNum() << endl;
cout << "Balance: $" << Balance() << endl;
cout << "Maximum loan: $" << maxLoan << endl;
cout << "Owed to bank: $" << owesBank << endl;
cout.precision(3);
cout << "Loan Rate: " << 100 * rate << "%\n";
Restore(f);
}

void BrassPlus::Withdraw(double amt)
{
Formatting f = SetFormat();

double bal = Balance();
if (amt <= bal)
AcctABC::Withdraw(amt);
else if ( amt <= bal + maxLoan - owesBank)
{
double advance = amt - bal;
owesBank += advance * (1.0 + rate);
cout << "Bank advance: $" << advance << endl;
cout << "Finance charge: $" << advance * rate << endl;
Deposit(advance);
AcctABC::Withdraw(amt);
}
else
cout << "Credit limit exceeded. Transaction cancelled.\n";
Restore(f);
}

保护方法 FullName( )和 AcctNum( )提供了对数据成员 fullName 和 acctNum的只读访间,使得可以进 一步定制每个派生类的 ViewAcct()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// usebrass3.cpp -- polymorphic example using an abstract base class
// compile with acctabc.cpp
#include <iostream>
#include <string>
#include "acctabc.h"
const int CLIENTS = 4;

int main()
{
using std::cin;
using std::cout;
using std::endl;

AcctABC * p_clients[CLIENTS];
std::string temp;
long tempnum;
double tempbal;
char kind;

for (int i = 0; i < CLIENTS; i++)
{
cout << "Enter client's name: ";
getline(cin,temp);
cout << "Enter client's account number: ";
cin >> tempnum;
cout << "Enter opening balance: $";
cin >> tempbal;
cout << "Enter 1 for Brass Account or "
<< "2 for BrassPlus Account: ";
while (cin >> kind && (kind != '1' && kind != '2'))
cout <<"Enter either 1 or 2: ";
if (kind == '1')
p_clients[i] = new Brass(temp, tempnum, tempbal);
else
{
double tmax, trate;
cout << "Enter the overdraft limit: $";
cin >> tmax;
cout << "Enter the interest rate "
<< "as a decimal fraction: ";
cin >> trate;
p_clients[i] = new BrassPlus(temp, tempnum, tempbal,
tmax, trate);
}
while (cin.get() != '\n')
continue;
}
cout << endl;
for (int i = 0; i < CLIENTS; i++)
{
p_clients[i]->ViewAcct();
cout << endl;
}

for (int i = 0; i < CLIENTS; i++)
{
delete p_clients[i]; // free memory
}
cout << "Done.\n";
return 0;
}

设计 ABC 之前,首先应开发一个模型 指出编程问题所需的类以及它们之间相互关系。一种学院派思 想认为,如果要设计类继承层次,则只能将那些不会被用作基类的类设计为具体的类。这种方法的设计更 清晰,复杂程度更低。可以将 ABC 看作是一种必须实施的接口。ABC 要求具体派生类覆盖其纯虚函数 迫使派生类遵循 ABC 设置的接口规则。这种模型在基于组件的编程模式中很常见。在这种情况下,使用ABC 使得组件设 计人员能够制定“接门约定”,这样确保了从ABC派生的所有组件都至少支持ABC指定的功能。

继承和动态内存分配

派生类不使用new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Base Class Using DMA
class baseDMA
{
private:
char * label;
int rating;

public:
baseDMA(const char * l = "null", int r = 0);
baseDMA(const baseDMA & rs);
virtual ~baseDMA();
baseDMA & operator=(const baseDMA & rs);
...
};

声明中包含了构造函数使用new时需要的特殊方法:析构函数、复制构造两数和重载赋值运算符。 现在,从baseDMA派生出lackDMA类,而后者不使用new,也未包含其他一些不常用的、需要特殊 处理的设计特性:

1
2
3
4
5
6
7
8
// derived class without DMA
class lacksDMA :public baseDMA
{
private:
char color[40];
public:
...
};

是否需要为 lackDMA类定义显式析构函数、复制构造函数和赋值运算符呢?不需要。

派生类对象的这些属性也适用于本身是对象的类成员。

派生类使用 new

1
2
3
4
5
6
7
8
// derived class with DMA
class hasDMA :public baseDMA
{
private:
char * style; // use new in constructors
public:
...
};

在这种情况下,必须为派生类定义显式析构函数、复制构造函数和赋值运算符。

  • 派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的进行清理。
1
2
3
4
5
6
7
8
9
baseDMA::~baseDMA() // takes care of baseDMA stuff
{
delete [] label;
}

hasDMA::~hasDMA() // takes care of hasDMA stuff
{
delete [] style;
}
  • hasDMA 复制构造函数只能访问hasDMA的数据,因此它必须调用baseDMA 复制构造函数来处理共 享的 baseDMA 数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
baseDMA::baseDMA(const baseDMA & rs)
{
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
}

hasDMA::hasDMA(const hasDMA & hs)
: baseDMA(hs)
{
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
}

baseDMA复制构造函数将使用hasDMA 参数 的 baseDMA 部分来构造新对象的 baseDMA 部分。

  • 由于hasDMA也使用动态内存分配,所以它也需要一个显式成值运算符。作为hasDMA 的方法,它只 能直接访问hasDMA的数据。然而,派生类的显式赋值运算符必须负责所有继承的 baseDMA 基类对象的 赋值,可以通过显式调用基类赋值运算符来完成这项工作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
baseDMA & baseDMA::operator=(const baseDMA & rs)
{
if (this == &rs)
return *this;
delete [] label;
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
return *this;
}

hasDMA & hasDMA::operator=(const hasDMA & hs)
{
if (this == &hs)
return *this;
baseDMA::operator=(hs); // copy base portion
delete [] style; // prepare for new style
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
return *this;
}
1
baseDMA::operator=(hs);//*this = hs;

编译器将使用 hasDMA:operztor-( ),从而形成递归 调用。使用函数表示法使得赋值运算符被正确调用。

使用动态内存分配和友元的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// dma.h -- inheritance and dynamic memory allocation
#ifndef DMA_H_
#define DMA_H_
#include <iostream>

// Base Class Using DMA
class baseDMA
{
private:
char * label;
int rating;

public:
baseDMA(const char * l = "null", int r = 0);
baseDMA(const baseDMA & rs);
virtual ~baseDMA();
baseDMA & operator=(const baseDMA & rs);
friend std::ostream & operator<<(std::ostream & os, const baseDMA & rs);
};

// derived class without DMA
// no destructor needed
// uses implicit copy constructor
// uses implicit assignment operator
class lacksDMA :public baseDMA
{
private:
enum { COL_LEN = 40};
char color[COL_LEN];
public:
lacksDMA(const char * c = "blank", const char * l = "null", int r = 0);
lacksDMA(const char * c, const baseDMA & rs);
friend std::ostream & operator<<(std::ostream & os, const lacksDMA & rs);
};

// derived class with DMA
class hasDMA :public baseDMA
{
private:
char * style;
public:
hasDMA(const char * s = "none", const char * l = "null", int r = 0);
hasDMA(const char * s, const baseDMA & rs);
hasDMA(const hasDMA & hs);
~hasDMA();
hasDMA & operator=(const hasDMA & rs);
friend std::ostream & operator<<(std::ostream & os, const hasDMA & rs);
};

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// dma.cpp --dma class methods

#include "dma.h"
#include <cstring>

// baseDMA methods
baseDMA::baseDMA(const char * l, int r)
{
label = new char[std::strlen(l) + 1];
std::strcpy(label, l);
rating = r;
}

baseDMA::baseDMA(const baseDMA & rs)
{
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
}

baseDMA::~baseDMA()
{
delete [] label;
}

baseDMA & baseDMA::operator=(const baseDMA & rs)
{
if (this == &rs)
return *this;
delete [] label;
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
return *this;
}

std::ostream & operator<<(std::ostream & os, const baseDMA & rs)
{
os << "Label: " << rs.label << std::endl;
os << "Rating: " << rs.rating << std::endl;
return os;
}

// lacksDMA methods
lacksDMA::lacksDMA(const char * c, const char * l, int r)
: baseDMA(l, r)
{
std::strncpy(color, c, 39);
color[39] = '\0';
}

lacksDMA::lacksDMA(const char * c, const baseDMA & rs)
: baseDMA(rs)
{
std::strncpy(color, c, COL_LEN - 1);
color[COL_LEN - 1] = '\0';
}

std::ostream & operator<<(std::ostream & os, const lacksDMA & ls)
{
os << (const baseDMA &) ls;
os << "Color: " << ls.color << std::endl;
return os;
}

// hasDMA methods
hasDMA::hasDMA(const char * s, const char * l, int r)
: baseDMA(l, r)
{
style = new char[std::strlen(s) + 1];
std::strcpy(style, s);
}

hasDMA::hasDMA(const char * s, const baseDMA & rs)
: baseDMA(rs)
{
style = new char[std::strlen(s) + 1];
std::strcpy(style, s);
}

hasDMA::hasDMA(const hasDMA & hs)
: baseDMA(hs) // invoke base class copy constructor
{
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
}

hasDMA::~hasDMA()
{
delete [] style;
}

hasDMA & hasDMA::operator=(const hasDMA & hs)
{
if (this == &hs)
return *this;
baseDMA::operator=(hs); // copy base portion
delete [] style; // prepare for new style
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
return *this;
}

std::ostream & operator<<(std::ostream & os, const hasDMA & hs)
{
os << (const baseDMA &) hs;
os << "Style: " << hs.style << std::endl;
return os;
}

公有继承的考虑因素

is-a关系

什么不能被继承

  1. 构造函数是不能继承的。创建派生类对象时,必须调用派生类的构造函数。然而,派生类 构造函数通常使用成员初始化列表语法来调用基类构造函数,以创建派生对象的基类部分。如果派生类构 造函数没有使用成员初始化列表语法显式调用基类构造函数。将使用基类的默认构造函数。在继承链中, 每个类都可以使用成员初始化列表将信息传递给相邻的基类。
  2. 析构函数也是不能继承的。然而,在释放对象时,程序将甘先调用派生类的析构函数,然后调用基类 的析构的数。如果基类有默认析构函数,编译器将为派生类生成默认析构函数。通常,对于基类,其析构 函数应设置为虚的。
  3. 赋值运算符是不能继承的。

赋值运算符

如果派生类使用了 new,则必须提供显式赋值运算符。必须给类的每个成员提供赋值运算符, 而不仅仅是新成员。

将派生类对象赋给基类对象将会如何呢?

1
2
3
Brass blips;                                         // 基类对象
BrassPlus snips("Rafe Plosh", 91191, 3993.19, 600.0, 0.12); // 派生类对象
blips = snips; // 将派生类对象赋值给基类对象

赋值语句将被转换成左边的对象调用的一个方法:

1
blips.operator=(snips);//Brass::operator=(const Brass &)

is-a 关系允 许 Brass引用指向派生类对象,赋值运算符只处理基类成员。

可以将基类对象赋给派生类对象吗?

1
2
3
Brass gp("Griff Hexbait", 21234, 1200); // 基类对象
BrassPlus temp; // 派生类对象
temp = gp; // temp.operator=(gp)

派 生类引用不能自动引用基类对象,因此上述代码不能运行,除非有下面的转换构造函数:

1
BrassPlus(const Brass &);

转换构造函数可以接受一个类型为基类的参数和其他参数,条件是其他参 数有默认值:

1
BrassPlus(const Brass & ba,double ml =500,double r = 0.1);

另一·种方法是,定义一个用于将基类赋给派生类的赋值运算符:

1
BrassPlus & BrassPlus::operator=(const Brass &){...}

私有成员与保护成员

对派生类而言,保护成员类似于公有成员:但对于外部而言,保护成员与私有成员类似。派生类可 以直接访问基类的保护成员,但只能通过基类的成员函数来访问私有成员。因此,将基类成员设置为私 有的可以提高安全性,而将它们设置为保护成员则可简化代码的编写工作,并提高访问速度。

虚方法

如果希望派生类能够重新定义方法,则应在基类中 将方法定义为虚的;如果不希望重新定义方法,则不必将其声明为虚 的。基类的析构函数应当是虚的。这样,当通过指向对象的基类指针或引用来删除派生 对象时,程序将首先调用派生类的析构两数,然后调用基类的析构函数,而不仅仅是调用基类的析构函数。

友元函数

由于友元函数并非类成员,因此不能继承。然而,您可能希望派生类的友元函数能够使用基类的友元 函数。为此,可以通过强制类型转换将,派生类引用或指针转换为基类引用或指针,然后使用转换后的指 针或引用来调用基类的友元函数:

1
2
3
4
5
6
7
ostream & operator<<(ostream & os, const hasDMA & hs)
{
// type cast to match operator<<(ostream & , const baseDMA &)
os << (const baseDMA &) hs; // 强制类型转换调用基类输出
os << "Style: " << hs.style << endl;
return os;
}

第十四章

包含对象成员的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// studentc.h -- defining a Student class using containment
#ifndef STUDENTC_H__
#define STUDENTC_H__

#include <iostream>
#include <string>
#include <valarray>

class Student
{
private:
typedef std::valarray<double> ArrayDb;
std::string name; // contained object
ArrayDb scores; // contained object
// private method for scores output
std::ostream & arr_out(std::ostream & os) const;

public:
// Constructors
Student() : name("Null Student"), scores() {}
explicit Student(const std::string & s)
: name(s), scores() {}
explicit Student(int n) : name("Nully"), scores(n) {}
Student(const std::string & s, int n)
: name(s), scores(n) {}
Student(const std::string & s, const ArrayDb & a)
: name(s), scores(a) {}
Student(const char * str, const double * pd, int n)
: name(str), scores(pd, n) {}

// Destructor
~Student() {}

// Public methods
double Average() const;
const std::string & Name() const;
double & operator[](int i);
double operator[](int i) const;

// friends
// input
friend std::istream & operator>>(std::istream & is, Student & stu); // 1 word
friend std::istream & getline(std::istream & is, Student & stu); // 1 line
// output
friend std::ostream & operator<<(std::ostream & os, const Student & stu);
};

#endif
  • 初始化被包含的对象

对于继承的对象,构造函数在成员初始化列表中使用类名来训用特定的基类构造函数,对于成员对象, 构造函数则使用成员名。

1
2
Student(const char * str, const double * pd, int n)
: name(str), scores(pd, n) {}

因为该构造函数初始化的是成员对象,而不是继承的对象,所以在初始化列表中使用的是成员名,而 不是类名。初始化列表中的每一项都调用与之匹配的构造函数。

  • 使用被包含对象的接口

被包含对象的接口不是公有的,但可以在类方法中使用它。e.g.

1
2
3
4
5
6
7
double Student::Average() const
{
if (scores.size() > 0)
return scores.sum() / scores.size();
else
return 0;
}

上述代码定义了可由Student 对象调用的方法,该方法内部使用了 valaray 的方法size( )和sum( )。

私有继承

C++还有实现 has-a 关系的途径——私有继承。使用私有继承,基类的公有成员和保护成员都将 成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成 员函数中使用它们。

1
2
3
4
5
class Student : private std::string, private std::valarray<double>
{
public:
...
};
  • 初始化基类组件

包含和私有继承之间的主要区别:

1
2
3
4
5
6
7
// 使用包含(成员对象)
Student(const char * str, const double * pd, int n)
: name(str), scores(pd, n) {} // 使用对象名称进行初始化

// 使用包含(成员对象)
Student(const char * str, const double * pd, int n)
: name(str), scores(pd, n) {} // 使用对象名称进行初始化

对于继承类,新版本的构造函数将使用成员初始化列表语法,它使用类名而不是成员名来标识构造函数

  • 访问基类的方法

使用私有维承时,只能在派生类的方法中使用基类的方法。但有时候可能希望基类工具是公有的。包含使用对象来调用方法,然而,私有继承使得能够使用类名和作用域解析运算符来调用基类的方法:

1
2
3
4
5
6
7
double Student::Average() const
{
if (ArrayDb::size() > 0)
return ArrayDb::sum()/ArrayDb::size();
else
return 0;
}

总之,使用包含时将使用对象名来调用方法,而使用私有继承时将使用类名和作用域解析运算符来调 用方法。

  • 访问基类对象

使用作用域解析运算符可以访问基类的方法。但如果要使用基类对象本身,使用强制类型转换。指针 this 指向用来调用 方法的对象,因此*this为用来调用方法的对象。

1
2
3
const string & Student::Name() const {  
return (const string &) *this;
}

上述方法返回一个引用,该引用指向用于调用该方法的 Student对象中的继承而来的 string对象。

  • 访问基类的友元函数

用类名显式地限定函数名不适合于友元函数,这是因为友元不属于类。然而,可以通过显式地转换为 基类来调用正确的函数。

1
2
3
4
5
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << (const String &) stu << ":\n";
...
}

显式地将 su转换为string对象引用,进而调用函数 operator<<(ostream &,const String &)。引用 stu不会自动转换为string引用。根本原因在于,在私有继承中,在不进行显式类型转换的情况下, 不能将指向派生类的引用或指针赋给基类引用或指针。

  • 包含还是继承

通常,应使用包含来建立 has-a 关系;加果新类需要访问原有类的保护成员,或需要重新定义 虚函数,则应使用私有继承。

保护继承

使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有私有继承一样,基 类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类派生出另一个类时,使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法:使用保护继承时,基类的公有方法在第二代中将变成 受保护的,因此第三代派生类可以使用它们。

使用using 重新定义访问权限

使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派 生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。

1
2
3
4
5
double Student::sum() const   // public Student method
{

return std::valarray<double>::sum(); // use privately-inherited method
}

另一种方法是,将函数调用包装在另一个函数调用中,即使用一个 using 声明(就像名称空间那样) 来指出派生类可以使用特定的基类成员,即使采用的是私有派生。

1
2
3
4
5
6
7
8
class Student : private std::string, private std::valarray<double>
{
...
public:
using std::valarray<double>::min;
using std::valarray<double>::max;
...
};

注意,using声明只使用成员名——没有圆括号、函数特征标和返回类型。using 声明只适用于继承,而不适用于包含。

多重继承MI

两个主要的问题是:从两个不同的基类维承同名方法;从两 个或更多相关基类那里维承同一个类的多个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// worker0.h -- working classes
#ifndef WORKER0_H__
#define WORKER0_H__

#include <string>

class Worker // an abstract base class {
private:
std::string fullname;
long id;

public:
Worker() : fullname("no one"), id(0L) {}
Worker(const std::string & s, long n)
: fullname(s), id(n) {}
virtual ~Worker() = 0; // pure virtual destructor
virtual void Set();
virtual void Show() const;
};

class Waiter : public Worker
{
private:
int panache;

public:
Waiter() : Worker(), panache(0) {}
Waiter(const std::string & s, long n, int p = 0)
: Worker(s, n), panache(p) {}
Waiter(const Worker & wk, int p = 0)
: Worker(wk), panache(p) {}
void Set();
void Show() const;
};

class Singer : public Worker
{
protected:
enum {other, alto, contralto, soprano,
bass, baritone, tenor};
enum {Vtypes = 7};
private:
static char *pv[Vtypes]; // string equivs of voice types
int voice;
public:
Singer() : Worker(), voice(other) {}
Singer(const std::string & s, long n, int v = other)
: Worker(s, n), voice(v) {}
Singer(const Worker & wk, int v = other)
: Worker(wk), voice(v) {}
void Set();
void Show() const;
};

#endif

假设首先从 Singct 和 Waiter 公有派生出 SingingWaiter:

1
class SingingWaiter:public Singer,public Waiter{...}

因为 Singer 和 Waiter 都继承了一个 Worker组件,因此 SingingWaiter 将包含两个 Worker 组件。通常可以将派生类对象的地址赋给基类指针,但现在将出现二义性:

1
2
SingingWaiter ed;
Worker * pw = &ed;

通常,这种赋值将把基类指针设置为派生对象中的基类对象的地址。但 ed中包含两个 Worker 对象, 有两个地址可供选择,所以应使用类型转换来指定对象:

1
2
Worker * pw1 = (Waiter *) &ed;
Worker * pw2 = (Singer *) &ed;
  • 虚基类

虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。通过在类声明中 使用关键学 virtual,可以使 Worker 被用作 Singer 和 Waiter的虚基类(virtual 和 public 的次序无关紧要):

1
2
3
4
class Singer : virtual public Worker {...};
class Waiter : public virtual Worker {...};

class SingingWaiter: public Singer, public Waiter {...};

现在,SingingWaiter 对象将只包含 Worker 对象的·个副本。从本质上说,继承的 Singer 和 Waiter对 象共享一个 Worker 对象,而不是各自引入自己的 Worker 对象副本,所以可以使用多态。

  • 构造函数

对于非虚基类,唯一可以出现在初始化列表中的构 造函教是即时基类构造函数。但这些构造函数可能需要将信息传递给其基类。如果 Worker 是虚基类,则这种信息自动传递将不起作用。例如,对于下面的 M1构造函数:

1
2
SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other)
: Waiter(wk,p), Singer(wk,v) {} // flawed

存在的问题是,自动传递信息时,将通过2条不同的途径(Waiter 和 Singer)将 wk传递给 Worker 对 象。为避免这种冲突,C++在基类是虚的时,禁止信息通过中间类自动传递给基类。因此,上述构造函数 将初始化成员 panache 和 voice.但 wk参数中的信息将不会传递给子对象 Waiter。然而,编译器必须在构 造派生对象之前构造基类对象组件;在上述情况下,编译器将使用 Worker 的默认构造函数。

如果不希望猷认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数。因此,构造函 数应该是这样:

1
2
SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other)
:Worker(wk), Waiter(wk,p), Singer(wk,v) {} // flawed

上述代码将显式地调用构造函数 worket(canst Worker&)。请注意,这种用法是合法的,对于虚基类, 必须这样做;但对于非虚基类,则是非法的。

  • 哪个方法

假设要在 SingingWaiter类中扩展 Show() 方法。因为 SingingWaiter 对象没有新的数据成员,所以可能会认为它只需使用继承的方法即可。假设没有在 SingingWaiter类中重新定义Shaw( )方法,并试图使用 SingingWaiter 对象调用维 承的Show( )方法:

1
2
SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
newhire.Show(); // ambiguous

在多重继承中,每个直接祖 先都有一个 Show( )函数,这使得上述调用是二义性的。可以使用作用域解析运算符来澄清编程者的意图:

1
2
SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
newhire.Singer::Show();

然而,更好的方法是在 SingingWaiter 中重新定义Show( ),并指出要使用哪个 Show( )。例如,如果希 望 SingingWaiter 对象使用 Singer版本的Show( ),则可以这样做:

1
2
3
4
void SingingWaiter::Show()
{
Singer::Show();
}

同时调用 Waitar版本的 Show( ):

1
2
3
4
5
void SingingWaiter::Show()
{
Singer::Show();
Waiter::show();
}

然而,这将显示姓名和ID两次,因为 Singer:Show()和 Waiter:Show( )都调用了 Worker:Show( )。

如果解决呢?一种办法是使用模块化方式,而不是递增方式,即提供一个只显示 Warker组件的方法和 一个只显示 Waiter组件或 Singer组件(而不是Waiter 和 Worker组件)的方法。然后,在 SingingWaiter:Show( ) 方法中将组件组合起来。另一种办法是将所有的数据组件都设置为保护的,而不是私有的,不过使用保护方法(而不是保护数 据)将可以重严格地控制对数据的访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void Worker::Data() const
{
cout << "Name: " << fullname << "\n";
cout << "Employee ID: " << id << "\n";
}

void Waiter::Data() const
{
cout << "Panache rating: " << panache << "\n";
}

void Singer::Data() const
{
cout << "Vocal range: " << pv[voice] << "\n";
}

void SingingWaiter::Data() const
{
Singer::Data();
Waiter::Data();
}

void SingingWaiter::Show() const
{
cout << "Category: singing waiter\n";
Worker::Data();
Data();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// workermi.h -- working classes with MI
#ifndef WORKERMI_H__
#define WORKERMI_H__

#include <string>

class Worker // an abstract base class
{
private:
std::string fullname;
long id;
protected:
virtual void Data() const;
virtual void Get();
public:
Worker() : fullname("no one"), id(0L) {}
Worker(const std::string & s, long n)
: fullname(s), id(n) {}
virtual ~Worker() = 0; // pure virtual function
virtual void Set() = 0;
virtual void Show() const = 0;
};

class Waiter : virtual public Worker
{
private:
int panache;
protected:
void Data() const;
void Get();
public:
Waiter() : Worker(), panache(0) {}
Waiter(const std::string & s, long n, int p = 0)
: Worker(s, n), panache(p) {}
Waiter(const Worker & wk, int p = 0)
: Worker(wk), panache(p) {}
void Set();
void Show() const;
};

class Singer : virtual public Worker
{
protected:
enum {other, alto, contralto, soprano,
bass, baritone, tenor};
enum {Vtypes = 7};
void Data() const;
void Get();
private:
static char *pv[Vtypes]; // string equivs of voice types
int voice;
public:
Singer() : Worker(), voice(other) {}
Singer(const std::string & s, long n, int v = other)
: Worker(s, n), voice(v) {}
Singer(const Worker & wk, int v = other)
: Worker(wk), voice(v) {}
void Set();
void Show() const;
};

// multiple inheritance
class SingingWaiter : public Singer, public Waiter
{
protected:
void Data() const;
void Get();
public:
SingingWaiter() {}
SingingWaiter(const std::string & s, long n, int p = 0,
int v = other)
: Worker(s,n), Waiter(s, n, p), Singer(s, n, v) {}
SingingWaiter(const Worker & wk, int p = 0, int v = other)
: Worker(wk), Waiter(wk,p), Singer(wk,v) {}
SingingWaiter(const Waiter & wt, int v = other)
: Worker(wt), Waiter(wt), Singer(wt,v) {}
SingingWaiter(const Singer & wt, int p = 0)
: Worker(wt), Waiter(wt,p), Singer(wt) {}
void Set();
void Show() const;
};

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// workermi.cpp -- working class methods with MI
#include "workermi.h"
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
// Worker methods
Worker::~Worker() { }

// protected methods
void Worker::Data() const
{
cout << "Name: " << fullname << endl;
cout << "Employee ID: " << id << endl;
}

void Worker::Get()
{
getline(cin, fullname);
cout << "Enter worker's ID: ";
cin >> id;
while (cin.get() != '\n')
continue;
}
// Waiter methods
void Waiter::Set()
{
cout << "Enter waiter's name: ";
Worker::Get();
Get();
}

void Waiter::Show() const
{
cout << "Category: waiter\n";
Worker::Data();
Data();
}

// protected methods
void Waiter::Data() const
{
cout << "Panache rating: " << panache << endl;
}

void Waiter::Get()
{
cout << "Enter waiter's panache rating: ";
cin >> panache;
while (cin.get() != '\n')
continue;
}

// Singer methods

char * Singer::pv[Singer::Vtypes] = {"other", "alto", "contralto",
"soprano", "bass", "baritone", "tenor"};

void Singer::Set()
{
cout << "Enter singer's name: ";
Worker::Get();
Get();
}

void Singer::Show() const
{
cout << "Category: singer\n";
Worker::Data();
Data();
}

// protected methods
void Singer::Data() const
{
cout << "Vocal range: " << pv[voice] << endl;
}

void Singer::Get()
{
cout << "Enter number for singer's vocal range:\n";
int i;
for (i = 0; i < Vtypes; i++)
{
cout << i << ": " << pv[i] << " ";
if ( i % 4 == 3)
cout << endl;
}
if (i % 4 != 0)
cout << '\n';
cin >> voice;
while (cin.get() != '\n')
continue;
}

// SingingWaiter methods
void SingingWaiter::Data() const
{
Singer::Data();
Waiter::Data();
}

void SingingWaiter::Get()
{
Waiter::Get();
Singer::Get();
}

void SingingWaiter::Set()
{
cout << "Enter singing waiter's name: ";
Worker::Get();
Get();
}

void SingingWaiter::Show() const
{
cout << "Category: singing waiter\n";
Worker::Data();
Data();
}
  • 混合使用虚基类和非虚基类

如果基类是虚基类。派生类将包含基类的一 个子对象;如果基类不是虚基类,派生类将包含多个子对象。当虚基类和非虚基类混合时,情况将如何呢? 例如:假设类B被用作类C和D的虚基类。同时被用作类X和Y的非虚基类,而类M是从C、D、X和 Y派生而来的。在这种情况下,类 M 从虚派生祖先(即类C和 D)那里共继承了一个 B类子对象,并从 每一个非虚派生祖先(即类X和Y)分别继承了一个B类子对象。因此,它包含三个B类子对象。当类 通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分 别表示各条非虚途径的多个基类子对象。

  • 虚基类和支配

用非虚基类时,如果类从不同的类那里继 承了两个或更多的同名成员(数据或方法),则使用该成员名时,如果没有用类名进行限定,将导致二义性。但如果使用的是虚基类,在这种情况下,如果某个名称优先于(dominates) 其他所有名称,则使用它时,即便不使用限定符,也不会导致二义性。

那么,一个成员名如何优先于另一个成员名呢?派生类中的名称优先于直接或间接祖先类中的相同名 称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class B
{
public:
short q();
...
};

class C : virtual public B
{
public:
long q();
int omg()
...
};

class D : public C
{
...
};
class E : virtual public B
{
private:
int omg();
...
};

class F: public D, public E
{
...
};

类C中的g()定义优先于类B中的q( )定义,因为类C是从类B派生而来的。因此,F中的方法可以 使用q( )来表示C:q()。另一方面,任何一个omg( )定义都不优先于其他omg( )定义,因为C 和 E 都不是 对方的基类。所以,在F中使用非限定的omg()将导致二义性。

虚二义性规则与访问规则无关,也就是说,即使E:omg()是私有的,不能在F类巾直接访问,但使用 omg()仍将导致二义性,同样,即使C:q( )是私有的,它也将优先于D:q()。在这种情况下,可以在类F 中调用 B::q( ),但如果不限定q( ),则将意味着要调用不可访问的 C:q( )。

类模板

template

当模板被调用时,Type 将被具体的类型值(如 int 或string)取代。在模板定义中,可以 使用泛型名来标识要存储在栈中的类型。同样,可以使用模板成员函数替换原有类的类方法。每个函数头都将以相同的模板声明打头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// stacktp.h -- a stack template
#ifndef STACKTP_H__
#define STACKTP_H__

template <class Type>
class Stack
{
private:
enum {MAX = 10}; // constant specific to class
Type items[MAX]; // holds stack items
int top; // index for top stack item
public:
Stack();
bool isempty();
bool isfull();
bool push(const Type & item); // add item to stack
bool pop(Type & item); // pop top into item
};

template <class Type>
Stack<Type>::Stack()
{
top = 0;
}

template <class Type>
bool Stack<Type>::isempty()
{
return top == 0;
}

template <class Type>
bool Stack<Type>::isfull()
{
return top == MAX;
}

template <class Type>
bool Stack<Type>::push(const Type & item)
{
if (top < MAX)
{
items[top++] = item;
return true;
}
else
return false;
}

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

#endif

这些模板不是类和成员函数定义。仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象, 方法是使用所需的具体类型替换泛型名。

1
2
Stack<int> kernels;     // create a stack of ints
Stack<string> colonels; // create a stack of string objects

必须显式地提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数 类型来确定要生成哪种函数:

1
2
3
4
5
6
7
template <class T>
void simple(T t) { cout << t << '\n';}

...

simple(2); // generate void simple(int)
simple("two"); // generate void simple(const char *)
  • 正确使用指针栈

使用指针栈的方法之一是,让调用程序提供一-个指针数组,其中每个指针都指向不同的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// stcktp1.h -- modified Stack template
#ifndef STCKTP1_H__
#define STCKTP1_H__

template <class Type>
class Stack
{
private:
enum {SIZE = 10}; // default size
int stacksize;
Type * items; // holds stack items
int top; // index for top stack item
public:
explicit Stack(int ss = SIZE);
Stack(const Stack & st);
~Stack() { delete [] items; }
bool isempty() { return top == 0; }
bool isfull() { return top == stacksize; }
bool push(const Type & item); // add item to stack
bool pop(Type & item); // pop top into item
Stack & operator=(const Stack & st);
};

template <class Type>
Stack<Type>::Stack(int ss) : stacksize(ss), top(0)
{
items = new Type [stacksize];
}

template <class Type>
Stack<Type>::Stack(const Stack & st)
{
stacksize = st.stacksize;
top = st.top;
items = new Type [stacksize];
for (int i = 0; i < top; i++)
items[i] = st.items[i];
}

template <class Type>
bool Stack<Type>::push(const Type & item)
{
if (top < stacksize)
{
items[top++] = item;
return true;
}
else
return false;
}

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

template <class Type>
Stack<Type> & Stack<Type>::operator=(const Stack<Type> & st)
{
if (this == &st)
return *this;
delete [] items;
stacksize = st.stacksize;
top = st.top;
items = new Type [stacksize];
for (int i = 0; i < top; i++)
items[i] = st.items[i];
return *this;
}

#endif

原型将赋值运算符函数的返回类型声明为 Stack 引用,而实际的模板函数定义将类型定义为 Stack.前者是后者的缩写,但只能在类中使用。即可以在模板声明或模板函数定义内使用 Stack, 但在类的外面,即指定返回类型或使用作用域解析运算符时,必须使用完整的 Stack。

数组模板示例和非类型参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//arrayTP.h -- Array Template
#ifndef ARRAYTP_H__
#define ARRAYTP_H__

#include <iostream>
#include <cstdlib>

template <class T, int n>
class ArrayTP
{
private:
T ar[n];
public:
ArrayTP() {};
explicit ArrayTP(const T & v);
virtual T & operator[](int i);
virtual T operator[](int i) const;
};

template <class T, int n>
ArrayTP<T,n>::ArrayTP(const T & v)
{
for (int i = 0; i < n; i++)
ar[i] = v;
}

template <class T, int n>
T & ArrayTP<T,n>::operator[](int i)
{
if (i < 0 || i >= n)
{
std::cerr << "Error in array limits: " << i
<< " is out of range\n";
std::exit(EXIT_FAILURE);
}
return ar[i];
}

template <class T, int n>
T ArrayTP<T,n>::operator[](int i) const
{
if (i < 0 || i >= n)
{
std::cerr << "Error in array limits: " << i
<< " is out of range\n";
std::exit(EXIT_FAILURE);
}
return ar[i];
}

#endif
1
template <typename T, int n>

这种参数(指定特殊的类型而不是用作泛型名)称为非类型(non-type)或表达式(expeession)参数。表达式参数有一些限制。表达式参数可以是整型、枚举、引用或指针。因此,doublem 是不合法的, 但 doublem和 doublepm是合法的。另外,模板代码不能修改参数的值,也不能使用参数的地址。所 以,在ArayTP 模板中不能使用诸如 n++和&n 等表达式。另外,实例化模板时,用作表达式参数的值必须 是常量表达式。

模板多功能性

可以将用于常规类的技术用于模板类。模板类可用作基类,也可用作组件类,还可用作其他模板的类 型参数,

  • 递归使用模板
1
Array< ArrayTP<int,5>,10> teodee;

请注意,在模板语法中,维的顺序与等价的二维数组相反。

  • 使用多个类型参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// pairs.cpp -- defining and using a Pair template
#include <iostream>
#include <string>

template <class T1, class T2>
class Pair
{
private:
T1 a;
T2 b;
public:
T1 & first();
T2 & second();
T1 first() const { return a; }
T2 second() const { return b; }
Pair(const T1 & aval, const T2 & bval) : a(aval), b(bval) {}
Pair() {}
};

template<class T1, class T2>
T1 & Pair<T1,T2>::first()
{
return a;
}

template<class T1, class T2>
T2 & Pair<T1,T2>::second()
{
return b;
}

int main()
{
using std::cout;
using std::endl;
using std::string;

Pair<string, int> ratings[4] =
{
Pair<string, int>("The Purpled Duck", 5),
Pair<string, int>("Jaquie's Frisco Al Fresco", 4),
Pair<string, int>("Cafe Souffle", 5),
Pair<string, int>("Bertie's Eats", 3)
};

int joints = sizeof(ratings) / sizeof (Pair<string, int>);
cout << "Rating:\t Eatery\n";
for (int i = 0; i < joints; i++)
cout << ratings[i].second() << "\t "
<< ratings[i].first() << endl;

cout << "Oops! Revised rating:\n";
ratings[3].first() = "Bertie's Fab Eats";
ratings[3].second() = 6;
cout << ratings[3].second() << "\t "
<< ratings[3].first() << endl;

return 0;
}
  • 默认类型模板参数
1
2
3
4
template<typename T1, typename T2 = int> class Topo{}

Topo<double, double>m1;
Topo<double>m2//T1 double T2 int

模板的具体化

  • 隐式实例化

它们声明一个或 多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义:

1
2
ArrayTP<double, 30> * pt;     // a pointer, no object needed yet
pt = new ArrayTP<double, 30>; // now an object is needed
  • 显式实例化

当使用关键字 templato 并指出所需类型来声明类时,编译器将生成类声明的显式实例化n)。声明必须位于模板定义所在的名称空间中。

1
template class ArrayTP<string, 100>;

在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化 ⋯样,也将根据通用模板来生成具体化。

  • 显式具体化

显式具体化(explicit specialization)是特定类型(用于替换模板中的泛型)的定义。有时候,可能需 要在为特殊类型实例化时,对模板进行修改,使其行为不同。在这种情况下,可以创建显式具体化。当具体化模板和通用模板都与实例化请求匹配时,编译器将使用具体化版本。

1
template <> class Classname<specialized-type-name>{ .. };

要使用新的表示法提供一个专供 const char*类型使用的 SartedAmay模板,可以使用类似于下面的代码:

1
template<>class StoreArray<const char char *>
  • 部分具体化
1
2
template <class Tl, class T2> class Pair {...}
template <class Tl> class Pair<T1,int> {...}

如果有多个模板可供选择。编译器将使用具体化程度最高的模板。

成员模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// tempmemb.cpp -- template members
#include <iostream>
using std::cout;
using std::endl;

template <typename T>
class beta
{
private:
template <typename V> // nested template class member
class hold
{
private:
V val;
public:
hold(V v = 0) : val(v) {}
void show() const { cout << val << endl; }
V Value() const { return val; }
};

hold<T> q; // template object
hold<int> n; // template object

public:
beta(T t, int i) : q(t), n(i) {}

template<typename U> // template method
U blab(U u, T t) { return (n.Value() + q.Value()) * u / t; }

void Show() const { q.show(); n.show(); }
};

int main()
{
beta<double> guy(3.5, 3);

cout << "T was set to double\n";
guy.Show();

cout << "V was set to T, which is double, then V was set to int\n";
cout << guy.blab(10, 2.3) << endl;

cout << "U was set to int\n";
cout << guy.blab(10.0, 2.3) << endl;

cout << "U was set to double\n";
cout << "Done\n";

return 0;
}

可以在 beta模板中声明 hold 类和 blah 方法,并在beta模板的外面定义它们。如果所用的编译器接受类外面的定义,则在 beta 模板之外定义模板方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
template <typename T>
class beta
{
private:
template <typename V> // declaration
class hold;
hold<T> q;
hold<int> n;
public:
beta(T t, int i) : q(t), n(i) {}
template<typename U> // declaration
U blab(U u, T t);
void Show() const { q.show(); n.show(); }
};

// member definition - nested template class
template <typename T>
template<typename V>
class beta<T>::hold
{
private:
V val;
public:
hold(V v = 0) : val(v) {}
void show() const { std::cout << val << std::endl; }
V Value() const { return val; }
};

// member definition - template method
template <typename T>
template <typename U>
U beta<T>::blab(U u, T t)
{
return (n.Value() + q.Value()) * u / t;
}

上述定义将T、V和U用作模板参数。因为模板是嵌套的,因此必须使用下雨的语法:

1
2
template <typename T>
template<typename V>

定义还必须指出 hold 和 biab 是beta类的成员,这是通过使用作用域解析运算符来完成的。

将模板用作参数

模板还可以包含本身就 是模板的参数,这种参数是模板新增的特性,用于实现 STL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// tempparam.cpp - templates as parameters
#include <iostream>
#include "stacktp.h"

template <template <typename T> class Thing>
class Crab
{
private:
Thing<int> s1;
Thing<double> s2;
public:
Crab() {}
// assumes the thing class has push() and pop() members
bool push(int a, double x) { return s1.push(a) && s2.push(x); }
bool pop(int & a, double & x) { return s1.pop(a) && s2.pop(x); }
};

int main()
{
using std::cout;
using std::cin;
using std::endl;
Crab<Stack> nebula;

// Stack must match template <typename T> class thing
int ni;
double nb;
cout << "Enter int double pairs, such as 4 3.5 (0 0 to end):\n";
while (cin >> ni >> nb && ni > 0 && nb > 0)
{
if (!nebula.push(ni, nb))
break;
}

while (nebula.pop(ni, nb))
cout << ni << ", " << nb << endl;
cout << "Done.\n";

return 0;
}
1
template <template <typename T> class Thing>

模板参数是 template class Thing,其中 template class 是类型,Thing 是参数。

1
Crab<King> legs;

为使上述声明被接受,模板参数King必须是一个模板类,其声明与模板参数 Thing的声明匹配:

1
template <typename T>class King{}

可以混合使用模板参数和常规参数

1
2
3
4
5
6
7
8
9
10
11
12
template <template <typename T> class Thing, typename U, typename V>
class Crab
{
private:
Thing<U> s1;
Thing<V> s2;
public:
Crab() {}
bool push(U a, V x) { return s1.push(a) && s2.push(x); }
bool pop(U & a, V & x) { return s1.pop(a) && s2.pop(x); }
};
Crab<Stack, int, double> nebula;//T=Stack,U=int,V=double

模板类和友元

模板类的非模板友元函数

在模板类中将一个常规函数产明为友元:

1
2
3
4
5
6
7
template <class T>
class HasFriend
{
public:
friend void counts(); // friend to all HasFriend instantiations
// ... other members
};

上述声明使counts()函数成为模板所有实例化的友元。

假设要为友元函数提供模板类参数,可以如下所示来进行友元声明吗?

1
friend void report (HasFriend &);

答案是不可以。原因是不存在HasFriend 这样的对象,而只有特定的具体化,如 HasFriend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// frnd2tmp.cpp -- template class with non-template friends
#include <iostream>
using std::cout;
using std::endl;

template <typename T>
class HasFriend
{
private:
T item;
static int ct;
public:
HasFriend(const T & i) : item(i) {ct++;}
~HasFriend() {ct--; }
friend void counts();
friend void reports(HasFriend<T> &); // template parameter
};

// each specialization has its own static data member
template <typename T>
int HasFriend<T>::ct = 0;

// non-template friend to all HasFriend<T> classes
void counts()
{
cout << "int count: " << HasFriend<int>::ct << "; ";
cout << "double count: " << HasFriend<double>::ct << endl;
}

// non-template friend to the HasFriend<int> class
void reports(HasFriend<int> & hf)
{
cout << "HasFriend<int>: " << hf.item << endl;
}

// non-template friend to the HasFriend<double> class
void reports(HasFriend<double> & hf)
{
cout << "HasFriend<double>: " << hf.item << endl;
}

int main()
{
cout << "No objects declared: ";
counts();

HasFriend<int> hfi1(10);
cout << "After hfi1 declared: ";
counts();

HasFriend<int> hfi2(20);
cout << "After hfi2 declared: ";
counts();

HasFriend<double> hfdb(10.5);
cout << "After hfdb declared: ";
counts();

reports(hfi1);
reports(hfi2);
reports(hfdb);

return 0;
}

模板类的约束模板友元函数

为约束模板友元作准备、要使类的每一 个具体化都获得与友元匹配的具体化。这比非模板友元复杂些,包含以下3步:

  • 在类定义的前面声明每个模板函数
1
2
template <typename T> void counts();
template <typename T> void report(T &);
  • 在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化
1
2
3
4
5
6
7
8
template <typename TT>
class HasFriendT
{
...
friend void counts<TT>();
friend void report<>(HasFriendT<TT> &);
};

声明中的<>指出这是模板具体化。

对于repont(),<>可以为空,因为可以从函数参数推断出如下模板类型参数:HasFriendT,也可以使用report<HasFriendT>(HasFriendT &);

但 counts( )函数没有参数,因此必须使用模板参数语法(<>)来指明其具体化。还需要注意的是, TT 是HasFriendT类的参数类型。

  • 为友元提供模板定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// tmp2tmp.cpp -- template friends to a template class
#include <iostream>
using std::cout;
using std::endl;

// template prototypes
template <typename T> void counts();
template <typename T> void report(T &);

// template class
template <typename TT>
class HasFriendT
{
private:
TT item;
static int ct;
public:
HasFriendT(const TT & i) : item(i) {ct++;}
~HasFriendT() { ct--; }
friend void counts<TT>();
friend void report<>(HasFriendT<TT> &);
};

template <typename T>
int HasFriendT<T>::ct = 0;

// template friend functions definitions
template <typename T>
void counts()
{
cout << "template size: " << sizeof(HasFriendT<T>) << "; ";
cout << "template counts(): " << HasFriendT<T>::ct << endl;
}

template <typename T>
void report(T & hf)
{
cout << hf.item << endl;
}

int main()
{
counts<int>();
HasFriendT<int> hfi1(10);
HasFriendT<int> hfi2(20);
HasFriendT<double> hfdb(10.5);
report(hfi1); // generate report(HasFriendT<int> &)
report(hfi2); // generate report(HasFriendT<int> &)
report(hfdb); // generate report(HasFriendT<double> &)
cout << "counts<int>() output:\n";
counts<int>();
cout << "counts<double>() output:\n";
counts<double>();

return 0;
}

模板类的非约来模版友元函教

通过在类内部声明模板,可以创建非约束友元雨数,即每个函数具体化都是每个类具体化的友元。 对于非约束友元,友元模板类型参数与模板类类型参数足不同的:

1
2
3
4
5
template <typename T>
class ManyFriend
{
template <typename C, typename D>friend void show2(C &,D &);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// manyfrnd.cpp -- unbound template friend to a template class
#include <iostream>
using std::cout;
using std::endl;

template <typename T>
class ManyFriend
{
private:
T item;
public:
ManyFriend(const T & i) : item(i) {}
template <typename C, typename D> friend void show2(C &, D &);
};

template <typename C, typename D> void show2(C & c, D & d)
{
cout << c.item << ", " << d.item << endl;
}

int main()
{
ManyFriend<int> hfi1(10);
ManyFriend<int> hfi2(20);
ManyFriend<double> hfdb(10.5);
cout << "hfi1, hfi2: ";
show2(hfi1, hfi2);
cout << "hfdb, hfi2: ";
show2(hfdb, hfi2);

return 0;
}

模板别名(C++11)

1
2
template<typename T>
using arrtype = std::array<T,12>;

这将 arrtype 定义为一个模板别名,可使用它来指定类型,如下所示:

1
arrtype<int>days;

总之,arrtype表示类型std::array<T,12>

C++11允许将语法using=用于非模板。用于非模板时,这种语法与常规 typedef等价。

第十五章

友元类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// tv.h -- Tv and Remote classes
#ifndef TV_H_
#define TV_H_

class Tv
{
public:
friend class Remote; // Remote can access Tv private parts
enum {Off, On};
enum {MinVal, MaxVal = 20};
enum {Antenna, Cable};
enum {TV, DVD};

Tv(int s = Off, int mc = 125) : state(s), volume(5),
maxchannel(mc), channel(2), mode(Cable), input(TV) {}
void onoff() {state = (state == On)? Off : On;}
bool ison() const {return state == On;}
bool volup();
bool voldown();
void chanup();
void chandown();
void set_mode() {mode = (mode == Antenna)? Cable : Antenna;}
void set_input() {input = (input == TV)? DVD : TV;}
void settings() const; // display all settings
private:
int state; // on or off
int volume; // assumed to be digitized
int maxchannel; // maximum number of channels
int channel; // current channel setting
int mode; // broadcast or cable
int input; // TV or DVD
};

class Remote
{
private:
int mode; // controls TV or DVD
public:
Remote(int m = Tv::TV) : mode(m) {}
bool volup(Tv & t) { return t.volup();}
bool voldown(Tv & t) { return t.voldown();}
void onoff(Tv & t) { t.onoff(); }
void chanup(Tv & t) {t.chanup();}
void chandown(Tv & t) {t.chandown();}
void set_chan(Tv & t, int c) {t.channel = c;}
void set_mode(Tv & t) {t.set_mode();}
void set_input(Tv & t) {t.set_input();}
};

#endif

构造函教外,所有的 Romote 方法都将一个 Ty 对象引用作为参数,这表明遥控器必须针对特定的电视机。

  • 友元成员函数

从上一个例子中的代码可知,大多数 Remote方法都是用Tv类的公有接口实现的,唯一直接访问 Ty成员的 Remote 方法是 Remote::set_chan( ),因此它是 唯一需要作为友元的方法。

让Remote::set_chan( )成为Tv类的友元的方法是,在Tv类声明中将其声明为友元:

1
2
3
class Tv{
friend void Remote::set_chan(Tv & t, int c);
}

然而,要使编译器能够处理这条语句,它必须知道 Remote的定义。这意味着应将 Remote 的定义放到 Tv的定义前面。Remote 的方法提到 了Tv对象,而这意味者Tv定义应当位于Remote 定义之前。避开这种循环依赖的方法是,使用前向声明 (forward declaration)。为此,需要在 Remote 定义的前面插入下面的语句:

1
2
3
class Tv;
class Remote{...};
class Tv{...};

Remote 声明包含了内联代码,例如:

1
void onoff(Tv & t) { t.onoff(); }

由于这将调用TV的一个方法,所以编译器此时必须已经看到了Tv类的声明,这样才能知道 Ty有哪 些方法,但正如看到的,该声明位于Remote声明的后面、这种问题的解决方法是,使 Remote 声明中只包 含方法声明,并将实际的定义放在Tv类之后。这样,排列顺序将如下:

1
2
3
4
class Tv;
class Remote{...};//Tv-using methods as prototypes only
class Tv{...};
//put Remote method definition here
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// tvfm.h -- Tv and Remote classes using a friend member
#ifndef TVFM_H__
#define TVFM_H__

class Tv; // forward declaration

class Remote
{
public:
enum State{Off, On};
enum {MinVal,MaxVal = 20};
enum {Antenna, Cable};
enum {TV, DVD};
private:
int mode;
public:
Remote(int m = TV) : mode(m) {}
bool volup(Tv & t); // prototype only
bool voldown(Tv & t);
void onoff(Tv & t);
void chanup(Tv & t);
void chandown(Tv & t);
void set_mode(Tv & t);
void set_input(Tv & t);
void set_chan(Tv & t, int c);
};

class Tv
{
public:
friend void Remote::set_chan(Tv & t, int c);
enum State{Off, On};
enum {MinVal,MaxVal = 20};
enum {Antenna, Cable};
enum {TV, DVD};

Tv(int s = Off, int mc = 125) : state(s), volume(5),
maxchannel(mc), channel(2), mode(Cable), input(TV) {}
void onoff() {state = (state == On)? Off : On;}
bool ison() const {return state == On;}
bool volup();
bool voldown();
void chanup();
void chandown();
void set_mode() {mode = (mode == Antenna)? Cable : Antenna;}
void set_input() {input = (input == TV)? DVD : TV;}
void settings() const;

private:
int state;
int volume;
int maxchannel;
int channel;
int mode;
int input;
};

// Remote methods as inline functions
inline bool Remote::volup(Tv & t) { return t.volup();}
inline bool Remote::voldown(Tv & t) { return t.voldown();}
inline void Remote::onoff(Tv & t) { t.onoff(); }
inline void Remote::chanup(Tv & t) {t.chanup();}
inline void Remote::chandown(Tv & t) {t.chandown();}
inline void Remote::set_mode(Tv & t) {t.set_mode();}
inline void Remote::set_input(Tv & t) {t.set_input();}
inline void Remote::set_chan(Tv & t, int c) {t.channel = c;}
#endif
  • 其他友元关系

对于使用Remote 对象的 Tv方法,其原型可在 Remot 类声明之前 声明,但必须在Ramnte 类声明之后定义,以便编译器有足够的信息来编译该方法。这种方案与下面类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Tv{
friend class Remote;
public:
void buzz(Remote & r);
...
};
class Remote{
friend class Tv;
public:
void Bool volup(Tv & t){ t.volup(); }
...
};
inline void Tv::buzz(Remote & r){
...
}

由于 Remote 的声明位于TV声明的后面,所以可以在类声明中定义 Rernote:volup( ),但 Tv::buzz( )方 法必须在 Tv声明的外部定义,使其位于Remote声明的后面。如果不希望buzz( )是内联的,则应在一个单 教的方法定义文件中定义它。

  • 共同的友元
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Analyzer;
class Probe{
friend void sync(Analyzer & a,const Probe & p);
friend void sync(Probe & p & a,const Analyzer & a);
...
};
class Amalyzer{
friend void sync(Analyzer & a,const Probe & p);
friend void sync(Probe & p & a,const Analyzer & a);
...
};
inline void sync(Analyzer & a,const Probe & p){
...
}
inline void sync(Probe & p & a,const Analyzer & a){
...
}

嵌套类

在C++中。可以将类声明放在另一个类中。在另一个类中声明的类被称为嵌套类(nested class),它通 过提供新的类型类作用域来避免名称混乱。包含类的成员函数可以创建和使用被嵌套类的对象;而仅当声 明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。

对类进行嵌奎与包含并不同。包含意味着将类对象作为另一个类的成员,而对类进行嵌套不创建类成 员,而是定义了一种类型,该类型仅在包含嵌套类声明的类中有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Queue
{
// class scope definitions
// Node is a nested class definition local to this class
class Node
{
public:
Item item;
Node * next;
Node(const Item & i) : item(i), next(0) {}
};
...
};

这个例子在类声明中定义了构造函数。假设想在方法文件中定义构造函数,则定义必须指出 Node类 是在 Queue类中定义的。这是通过使用两次作用域解析运算符来完成的:

1
queue::Node::Node(const Item & i):item(i),next(0){}

嵌套类和访问权

  • 作用域

如果嵌套类是在另一个类的私有部分声明的,则只有后者知道它。在前一个例子中,被嵌套在 Queue 声明中的 Node类就属于这种情况。因此,Qucue成员可以使用Nade 对象和指向 Node 对象的指针,但是程序的其他部分甚至不 知道存在Node类。对于从Queue派生而来的类,Node 也是不可见的,因为派生类不能直接访问基类的私 有部分。

如果联套类是在另一个类的保护部分声明的,则它对于后者来说是可见的,但是对于外部世界则是不 可见的。然而,在这种情况中,派生类将知道嵌套类,并可以直接创建这种类型的对象。

如果嵌套类是在另一个类的公有部分声明的,则允许后者、后者的派生类以及外部世界使用它,因为 它是公有的。然而,由子嵌套类的作用城为包含它的类,因此在外部世界使用它时,必须使用类限定符。

1
2
3
4
5
6
class Team{
public:
class Coach{...};
...
}
Team::Coach forhire;
  • 访问控制

类可见后,起决定作用的将是访问控制。对嵌套类访问权的控制规则与对常规类相同。总之,类声明的位置决定了类的作用城或可见性。类可见后,访问控制规则(公有、保护、私有、发 元)将决定程序对嵌套类成员的访问权限。

模板中的嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// queuetp.h -- queue template with a nested class
#ifndef QUEUETP_H__
#define QUEUETP_H__

template <class Item>
class QueueTP
{
private:
enum {Q_SIZE = 10};
// Node is a nested class definition
class Node
{
public:
Item item;
Node * next;
Node(const Item & i):item(i), next(0){ }
};

Node * front; // pointer to front of Queue
Node * rear; // pointer to rear of Queue
int items; // current number of items in Queue
const int gsize; // maximum number of items in Queue
QueueTP(const QueueTP & q) : gsize(0) {}
QueueTP & operator=(const QueueTP & q) { return *this; }

public:
QueueTP(int qs = Q_SIZE);
~QueueTP();
bool isempty() const
{
return items == 0;
}
bool isfull() const
{
return items == gsize;
}
int queuecount() const
{
return items;
}
bool enqueue(const Item &item); // add item to end
bool dequeue(Item &item); // remove item from front
};

// QueueTP methods
template <class Item>
QueueTP<Item>::QueueTP(int qs) : gsize(qs)
{
front = rear = 0;
items = 0;
}

template <class Item>
QueueTP<Item>::~QueueTP()
{
Node * temp;
while (front != 0) // while queue is not yet empty
{
temp = front; // save address of front item
front = front->next;// reset pointer to next item
delete temp; // delete former front
}
}

// Add item to queue
template <class Item>
bool QueueTP<Item>::enqueue(const Item & item)
{
if (isfull())
return false;
Node * add = new Node(item); // create node
// on failure, new throws std::bad_alloc exception
items++;
if (front == 0) // if queue is empty,
front = add; // place item at front,
else
rear->next = add; // else place at rear
rear = add; // have rear point to new node
return true;
}

// Place front item into item variable and remove from queue template <class Item>
bool QueueTP<Item>::dequeue(Item & item)
{
if (front == 0)
return false;
item = front->item; // set item to first item in queue
items--;
Node * temp = front; // save location of first item
front = front->next; // reset front to next item
delete temp; // delete former first item
if (items == 0)
rear = 0;
return true;
}

#endif

异常

对于被等除的情况,很多新式编 译器通过生成一个表示无穷大的特殊浮点值来处理,cout将这种值显示为 Inf、inf、INF或类似的东西:而其 他的编译器可能生成在发生被零除时崩溃的程序。

调用abort()

Abor( ) 函数的原型位于头文件 cstdlib中,其典型实现是向标准错误流(即cerr使用的错误流)发送 消息 abmarmal program termination(程序异常终止),然后终止程序。。它还返回一个随实现而异的值,告诉 操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。abort()是否刷新文件缰冲区(用 于存储读写到文件中的数据的内存区域)取决于实现。如果愿意,也可以使用exit(),该两数刷新文件缓 冲区,但不显示消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <cstdlib> // 需要包含此头文件以使用 std::abort()

// 函数前置声明
double hmean(double a, double b);

int main()
{
double x, y, z;

std::cout << "Enter two numbers: ";
while (std::cin >> x >> y)
{
z = hmean(x,y);
std::cout << "Harmonic mean of " << x << " and " << y
<< " is " << z << std::endl;
std::cout << "Enter next set of numbers <q to quit>: ";
}
std::cout << "Bye!\n"; // 修正拼写错误
return 0;
}

double hmean(double a, double b)
{
if (a == -b)
{
std::cout << "untenable arguments to hmean()\n";
std::abort();
}
return 2.0 * a * b / (a + b);
}

在hmean( )中调用abort( )函数将直接终止程序,而不是先返回到main()。

返回错误码

一种比异常终止更灵活的方法是,使用函数的返回值来指出问题。例如,ostream类的 get(void) 成员通常返回下一个输入字符的 ASCII码,但到达文件尾时,将返回特殊值 EOF。可使用指针参数或引用参数来将值返回给调用程序,并使用函数的返回值来指出成功还是失败。 istream 族重载>>运算符使用了这种技术的变体。通过告知调用程序是成功了还是失败了,使得程序 可以采取除异常终止程序之外的其他措施。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//error2.cpp -- returning an error code
#include <iostream>
#include <cfloat> // (or float.h) for DBL_MAX

bool hmean(double a, double b, double * ans);

int main()
{
double x, y, z;

std::cout << "Enter two numbers: ";
while (std::cin >> x >> y)
{
if (hmean(x,y,&z))
std::cout << "Harmonic mean of " << x << " and " << y
<< " is " << z << std::endl;
else
std::cout << "One value should not be the negative "
<< "of the other - try again.\n";
std::cout << "Enter next set of numbers <q to quit>: ";
}
std::cout << "Bye!\n";
return 0;
}

bool hmean(double a, double b, double * ans)
{
if (a == -b)
{
*ans = DBL_MAX;
return false;
}
else
{
*ans = 2.0 * a * b / (a + b);
return true;
}
}

异常机制

异常提供了将控制权从程序的一个部分传递到另一部分的途径。对异常的处理有3个组成部分:

  1. 引发异常
  2. 使用处理程序捕获异常
  3. 使用try块

程序在出现问题时将引发异常。throw语句实际上是跳转,即命令程序跳到另一条语句。throw关键字表示引发异常,紧 随其后的值(例如字符串或对象)指出了异常的特征。

程序使用异常处理程序(exception handler)来捕获异常,异常处理程序位于要处理问题的程序中。catch 关键字表示捕状异常。处理程序以关键字 catch 开头。随后是位于括号中的类型声明,它指出了异常处理 程序要响应的异常类型:然后是一个用花括号括起的代码块,指出要采取的指施。catch 关键字和异常类型 用作标签,指山当异常被引发时,程序应跳到这个位置执行。异常处理程序也被称为 catch块。

try 块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个 catch块。try 块是由关键字 ty 指示的,关键字try的后面是一个由花括号括起的代码块,表明需要注意这些代码引发的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// error3.cpp -- using an exception
#include <iostream>
double hmean(double a, double b);

int main()
{
double x, y, z;

std::cout << "Enter two numbers: ";
while (std::cin >> x >> y)
{
try {
// start of try block
z = hmean(x,y);
} // end of try block
catch (const char * s) // start of exception handler
{
std::cout << s << std::endl;
std::cout << "Enter a new pair of numbers: ";
continue;
} // end of handler
std::cout << "Harmonic mean of " << x << " and " << y
<< " is " << z << std::endl;
std::cout << "Enter next set of numbers <q to quit>: ";
}
std::cout << "Bye!\n";
return 0;
}

double hmean(double a, double b)
{
if (a == -b)
throw "bad hmean() arguments: a = -b not allowed";
return 2.0 * a * b / (a + b);
}

如果其中的某条语句导致异常被引发,则后面的catch块将对异常进行处理,如果程序在ty块的外面 调用hmean(),将无法处理异常。其中被引发的异常是字符串“bad hmean( Jarguments:a–b not allowed”。异常类型可以是字符串(就 像这个例子中那样)或其他C++类型;通常为类类型。

执行 throw 语句类似于执行返回语句,因为它也将终止函数的执行;但 throw 不是将控制权返回给调 用程序,而是导致程序沿两数调用序列后退,直到找到包含try块的函数。

catch块点类似于函数定义,但并不是函数定义。关键字 catch表明这是一个处理程序,而 char2s则表 明该处理程序与字符串异常匹配。s与函数参数定义极其类似,因为匹配的引发将被赋给 s。另外,当异常 与该处理程序匹配时,程序将执行括号中的代码。

执行完ty块中的谐句后,如果没有引发任何异常,则程序跳过ty块后面的 catch 块,直接执行处理 程序后面的第一条语句。

将对象用作异常类型

通常,引发异常的函数将传递一个对象。可以使用不同的异常类型来区分 不同的函数在不同情况下引发的异常,另外,对象可以携带信息,程序员可以根据这些信息来确定引发异 常的原因。同时,catch 块可以根据这些信息来决定采取什么样的措施。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// exc_mean.h -- exception classes for hmean(), gmean()
#include <iostream>

class bad_hmean
{
private:
double v1;
double v2;
public:
bad_hmean(double a = 0, double b = 0) : v1(a), v2(b) {}
void mesg();
};

inline void bad_hmean::mesg()
{
std::cout << "hmean(" << v1 << ", " << v2 << "): "
<< "invalid arguments: a = -b\n";
}

class bad_gmean
{
public:
double v1;
double v2;
bad_gmean(double a = 0, double b = 0) : v1(a), v2(b) {}
const char * mesg();
};

inline const char * bad_gmean::mesg()
{
return "gmean() arguments should be >= 0\n";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//error4.cpp - using exception classes
#include <iostream>
#include <cmath> // or math.h, unix users may need -lm flag
#include "exc_mean.h"
// function prototypes
double hmean(double a, double b);
double gmean(double a, double b);

int main()
{
using std::cout;
using std::cin;
using std::endl;

double x, y, z;

cout << "Enter two numbers: ";
while (cin >> x >> y)
{
try { // start of try block
z = hmean(x,y);
cout << "Harmonic mean of " << x << " and " << y
<< " is " << z << endl;
cout << "Geometric mean of " << x << " and " << y
<< " is " << gmean(x,y) << endl;
cout << "Enter next set of numbers <q to quit>: ";
}// end of try block
catch (bad_hmean & bh) // start of catch block
{
bh.mesg();
cout << "Try again.\n";
continue;
}
catch (bad_gmean & bg)
{
cout << bg.mesg();
cout << "Values used: " << bg.v1 << ", "
<< bg.v2 << endl;
cout << "Sorry, you don't get to play any more.\n";
break;
} // end of catch block
}
cout << "Bye!\n";
return 0;
}
double hmean(double a, double b)
{
if (a == -b)
throw bad_hmean(a,b);
return 2.0 * a * b / (a + b);
}

double gmean(double a, double b)
{
if (a < 0 || b < 0)
throw bad_gmean(a,b);
return std::sqrt(a * b);
}

栈解退

现在假设函数由于出现异常(而不是由于返回)而终止,则程序也将释放栈中的内存,但不会在释放栈的 第一个返回地址后伴止,而是继续释放栈,直到找到一个位于try块中的返回地址。随后,控制 权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退。引发机制的一个 非常重要的特性是,和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用。然而,函数返回仅仪 处理该函数放在栈中的对象,而 throw诉句则处理 try块和 hrow之间整个函数调用序列放在栈中的对象。如果 没有找解退这种特性,则引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数将不会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//error5.cpp -- unwinding the stack
#include <iostream>
#include <cmath>
#include <string>
#include "exc_mean.h"

class demo
{
private:
std::string word;
public:
demo(const std::string & str)
{
word = str;
std::cout << "demo " << word << " created\n";
}
~demo()
{
std::cout << "demo " << word << " destroyed\n";
}
void show() const
{
std::cout << "demo " << word << " lives!\n";
}
};

// function prototypes
double hmean(double a, double b);
double gmean(double a, double b);
double means(double a, double b);

int main()
{
using std::cout;
using std::cin;
using std::endl;

double x, y, z;
{
demo d1("found in block in main()");
cout << "Enter two numbers: ";
while (cin >> x >> y)
{
try {
// start of try block
z = means(x,y);
cout << "The mean mean of " << x << " and " << y
<< " is " << z << endl;
cout << "Enter next pair: ";
} // end of try block
catch (bad_hmean & bh) // start of catch block
{
bh.mesg();
cout << "Try again.\n";
continue;
}
catch (bad_gmean & bg)
{
cout << bg.mesg();
cout << "Values used: " << bg.v1 << ", "
<< bg.v2 << endl;
cout << "Sorry, you don't get to play any more.\n";
break;
} // end of catch block
}
d1.show();
}
cout << "Bye!\n";
cin.get();
cin.get();
return 0;
}

double hmean(double a, double b)
{
if (a == -b)
throw bad_hmean(a,b);
return 2.0 * a * b / (a + b);
}

double gmean(double a, double b)
{
if (a < 0 || b < 0)
throw bad_gmean(a,b);
return std::sqrt(a * b);
}

double means(double a, double b)
{
double am, hm, gm;
demo d2("found in means()");
am = (a + b) / 2.0; // arithmetic mean
try
{
hm = hmean(a,b);
gm = gmean(a,b);
}
catch (bad_hmean & bh) // start of catch block
{
bh.mesg();
std::cout << "Caught in means()\n";
throw; // rethrows the exception
}
d2.show();
return (am + hm + gm) / 3.0;
}

exception 类

excepion头文件定义了exception 类,c++可以把它用作其他异常类的基类。码可以引发 exception 异常,也 可以将 exception类用作基类。有一个名为 what()的虚拟成员函数,它返回一个字符串,该字符串的特征随 实现而异。然而,由于这是一个虚方法,因此可以在从 exception 派生而来的类中重新定义它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <exception>

class bad_hmean : public std::exception
{
public:
const char * what() { return "bad arguments to hmean()"; }
// ... 其他成员
};

class bad_gmean : public std::exception
{
public:
const char * what() { return "bad arguments to gmean()"; }
// ... 其他成员
};

如果不想以不同的方式处理这些派生而来的异常,可以在同一个基类处理程序中捕获它们:

1
2
3
4
5
6
try{
...
}
catch(std::exception & e){
cout << e.what() << endl;
}

C+库定义了很多基于 exception 的异常类型:

  1. tdexcept 异常类
  2. bad_alloc 异常和 new
  3. 空指针和 new

异常、类和继承

异常、类和继承以三种方式相互关联。首先,可以像标准C++库所做的那样,从一个异常类派生出另一 个;其次,可以在类定义中嵌套异常类声明来组合异常;第三,这种嵌套声明本身可被继承,还可用作基类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// sales.h -- exceptions and inheritance
#include <stdexcept>
#include <string>

class Sales
{
public:
enum {MONTHS = 12}; // could be a static const
class bad_index : public std::logic_error
{
private:
int bi; // bad index value
public:
explicit bad_index(int ix,
const std::string & s = "Index error in Sales object\n");
int bi_val() const {return bi;}
virtual ~bad_index() throw() {}
};

explicit Sales(int yy = 0);
Sales(int yy, const double * gr, int n);
virtual ~Sales() {}
int Year() const { return year; }
virtual double operator[](int i) const;
virtual double & operator[](int i);

private:
double gross[MONTHS];
int year;
};

class LabeledSales : public Sales
{
public:
class nbad_index : public Sales::bad_index
{
private:
std::string lbl;
public:
nbad_index(const std::string & lb, int ix,
const std::string & s = "Index error in LabeledSales object\n");
const std::string & label_val() const {return lbl;}
virtual ~nbad_index() throw() {}
};

explicit LabeledSales(const std::string & lb = "none", int yy = 0);
LabeledSales(const std::string & lb, int yy, const double * gr, int n);
virtual ~LabeledSales() {}
const std::string & Label() const {return label;}
virtual double operator[](int i) const;
virtual double & operator[](int i);

private:
std::string label;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// sales.cpp -- Sales implementation
#include "sales.h"
using std::string;

Sales::bad_index::bad_index(int ix, const string & s)
: std::logic_error(s), bi(ix)
{
}

Sales::Sales(int yy)
{
year = yy;
for (int i = 0; i < MONTHS; ++i)
gross[i] = 0;
}

Sales::Sales(int yy, const double * gr, int n)
{
year = yy;
int lim = (n < MONTHS) ? n : MONTHS;
int i;
for (i = 0; i < lim; ++i)
gross[i] = gr[i];
// for i > n and i < MONTHS
for ( ; i < MONTHS; ++i)
gross[i] = 0;
}

double Sales::operator[](int i) const
{
if(i < 0 || i >= MONTHS)
throw bad_index(i);
return gross[i];
}

double & Sales::operator[](int i)
{
if(i < 0 || i >= MONTHS)
throw bad_index(i);
return gross[i];
}

LabeledSales::nbad_index::nbad_index(const string & lb, int ix,
const string & s) : Sales::bad_index(ix, s)
{
lbl = lb;
}

LabeledSales::LabeledSales(const string & lb, int yy)
: Sales(yy)
{
label = lb;
}

LabeledSales::LabeledSales(const string & lb, int yy,
const double * gr, int n) : Sales(yy, gr, n)
{
label = lb;
}

double LabeledSales::operator[](int i) const
{
if(i < 0 || i >= MONTHS)
throw nbad_index(Label(), i);
return Sales::operator[](i);
}

double & LabeledSales::operator[](int i)
{
if(i < 0 || i >= MONTHS)
throw nbad_index(Label(), i);
return Sales::operator[](i);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// use_sales.cpp -- nested exceptions
#include <iostream>
#include "sales.h"

int main()
{
using std::cout;
using std::cin;
using std::endl;

double vals1[12] =
{
1220, 1100, 1122, 2212, 1232, 2334,
2884, 2393, 3302, 2922, 3002, 3544
};

double vals2[12] =
{
12, 11, 22, 21, 32, 34,
28, 29, 33, 29, 32, 35
};

Sales sales1(2011, vals1, 12);
LabeledSales sales2("Blogstar", 2012, vals2, 12);

cout << "First try block:\n";
try
{
int i;
cout << "Year = " << sales1.Year() << endl;
for (i = 0; i < 12; ++i)
{
cout << sales1[i] << ' ';
if (i % 6 == 5)
cout << endl;
}
cout << "Year = " << sales2.Year() << endl;
cout << "Label = " << sales2.Label() << endl;
for (i = 0; i <= 12; ++i)
{
cout << sales2[i] << ' ';
if (i % 6 == 5)
cout << endl;
}
cout << "End of try block 1.\n";
}
catch(LabeledSales::nbad_index & bad)
{
cout << bad.what();
cout << "Company: " << bad.label_val() << endl;
cout << "bad index: " << bad.bi_val() << endl;
}
catch(Sales::bad_index & bad)
{
cout << bad.what();
cout << "bad index: " << bad.bi_val() << endl;
}

cout << "\nNext try block:\n";
try
{
sales2[2] = 37.5;
sales1[20] = 23345;
cout << "End of try block 2.\n";
}
catch(LabeledSales::nbad_index & bad)
{
cout << bad.what();
cout << "Company: " << bad.label_val() << endl;
cout << "bad index: " << bad.bi_val() << endl;
}
catch(Sales::bad_index & bad)
{
cout << bad.what();
cout << "bad index: " << bad.bi_val() << endl;
}

cout << "done\n";
return 0;
}
  • 有关异常的注意事项

使用异常会增加程序代码,降低程序的运行速度。异常规范不适用于模板,因为模 板函数引发的异常可能随特定的具体化而异。异常和动态内存分配并非总能协同工作。

下面进一步讨论动态内存分配和异常:

1
2
3
4
5
6
7
8
9
void test1(int n)
{
string mesg("I'm trapped in an endless loop");
...
if (oh_no)
throw exception();
...
return;
}

string 类采用动态内存分配。通常,当函数结束时,将为 mesg调用 string的析构函数。虽然 throw 语句过早地终止了函数,们它仍然使得析构函数被调用,这要归功于栈解退。因此在这里,内存被正确 地管理。

1
2
3
4
5
6
7
8
9
10
void test2(int n)
{
double * ar = new double[n];
...
if (oh_no)
throw exception();
...
delete [] ar;
return;
}

解退栈时,将删除栈中的变量 ar。但函数过早的终止意味着函数末尾的 delete[ ]讲句 被忽略。指针消失了,但它指向的内存块未被释放,并且不可访问。总之,这些内存被泄漏了。

这种泄漏是可以避免的。例如,可以在引发异常的函数中捕获该异常,在 catch 块中包含一些清理代 码,然后重新引发异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void test3(int n)
{
double * ar = new double[n];
...
try {
if (oh_no)
throw exception();
}
catch(exception & ex)
{
delete [] ar;
throw;
}
...
delete [] ar;
return;
}

RTTI(???)

类型转换运算符

加4个类型转换运 算符,使转换过程更规范:

  1. dynamic_cast
  2. const_cast
  3. static_cast
  4. reinterpret_cast
  • dynamic_cast

dynamic_cast < type-name > (expression) 该运算符的用途是,使得能够在类层次结构中进行向上转换(由于 is-a关系,这样的类型转换是安全 的),而不尤许其他转换。

之,假设 High和 Low是两个类,而ph和pl的类型分别 为High和Low,则仅当Low是High的可访问基类(直接或间接)时,下面的语句才将一个 Low*指针 赋给 pl:

1
pl = dynamic_cast<Low *> ph;

否则,该语句将空指针赋给 pl。

  • const_cast

const_cast 运算符用于执行只有一种用途的类型转换,即改变值为 const 或 volatile,其语法与 dynanic_cat 运算符相同;如果类型的其他方面也被修改。则上述类型转换将出错。也就是说,除了 const或 volatile 特征(有或 无)可以不同外,type_name和 expression的类型必须相同。

再次假设 High和 Low是两个类:

1
2
3
4
High bar;
const High * pbar = &bar;
High * pb = const_cast<High *> (pbar); //valid
const Low * pl = const_cast<const Low *>; //invalid

第一个类型转换使得pb成为一个可用于修改bar对象值的指针,它删除 const 标签。第二个类型转换 是非法的,因为它同时尝试将类型从 const High改为 const Low*.

提供该运算符的原因是,有时候可能需要这样一个值,它在大多数时候是常量,而有时又是可以修改 的。在这种情况下,可以将这个值声明为 eonst,并在需要修改它的时候,使用 const_cast。这也可以通过 通用类型转换来实现,但通用转换也可能同时改变类型:

1
2
3
4
High bar;
const High * pbar = &bar;
High * pb = (High *) (pbar); //valid
Low * pl = (Low *) (pbar); //valid

由于编程时可能无意问同时改变类型和常量特征,因此使用 const_cast 运算符更安全。 const_cast 不是万能的。它可以修改指向一个值的指针,但修改 const 值的结果是不确定的。

  • static_cast

static_cast < type-name > (expression) 仅当 ope_name 可被隐式转换为 expression所属的类型或 expression 可被隐式转换为 type_name 所属的 类型时,上述转换才是合法的,否则将出错。假设 High 是 Low 的基类,而 Pond 是一个无关的类,则从 High到 Low 的转换、从Low到High的转换都是合法的,而从Low到Pond 的转换是不允许的:

1
2
3
4
5
6
High bar;
Low blow;
...
High * pb = static_cast<High *> (&blow); // valid upcast
Low * pl = static_cast<Low *> (&bar); // valid downcast
Pond * pmer = static_cast<Pond *> (&blow); // invalid, Pond unrelated

第一种转换是合法的,因为向上转换可以显示地进行,第二种转换是从基类指针到派生类指针,在不 进行显示类型转换的情况下,将无法进行。但由于无需进行类型转换,便可以进行另一个方向的类型转换, 因此使用 static_cast 来进行向下转换是合法的。

同理,由于无需进行类型转换,枚举值就可以被转换为整型,所以可以用 static_cast 将整型转换为枚 举值。同样,可以使用 static_cast将 double转换为 int、将float转换为long以及其他各种数值转换。

  • reinterprete_cast ???

第十六章

string类

  • 构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
string(const char * s)

string(size_type n, char c)

string(const string & str)

string( )

string(const char * s, size_type n)

template<class Iter>
string(Iter begin, Iter end)

string(const string & str, string size_type pos = 0, size_type n = npos)

string(string && str) noexcept

string(initializer_list<char> il)

通常,对于程序要查找的文本文件,应将其放在可执行程序或项目文件所在的目录中;否则必须提供 完整的路径名。在 Windows 系统中,C-风格字符串中的转义序列N表示一个斜杠:

1
fin.open("C:\\cpp\\Proge\\tobuy.txe");// file = C:\CpP\Progs\tabuy.txt
  • 重载find
1
2
3
4
5
6
7
size_type find(const string & str, size_type pos = 0) const

size_type find(const char * s, size_type pos = 0) const

size_type find(const char * s, size_type pos = 0, size_type n)

size_type find(char ch, size_type pos = 0) const
  • 宇符串种类

tring 库实际上是基于一个模板 类的:

1
2
template<class charT, class traits = char_traits<charT>,class Allocator = allocator<charT> >
basic_string {...};

模板 basic_string有 4个具体化,每个具体化都有一个 ypedef 名称:

1
2
3
4
typedef basic_string<char> string;
typedef basic_string<wchar_t> wstring;
typedef basic_string<char16_t> u16string; // C++11
typedef basic_string<char32_t> u32string; // C++11

raits 类描述关于选定字符类型的特定情况, 如如何对值进行比较。对于 wchar_t、char16_t、char32_t和 char类型,有预定义的 char_raits 模板具体化, 它们都是 raits 的默认值。Allocator 是一个管理内存分配的类。对于各种字符类型,都有预定义的 allocaror 模板具体化,它们都是默认的。它们使用 new和 delete。

智能指针模板类

1
2
3
4
5
6
7
8
9
10
void remodel(std::string & str)
{
std::string * ps = new std::string(str);
...
if (weird_thing())
throw exception();
str = *ps;
delete ps;
return;
}

当抛出异常时,delete不会执行,会导致内存泄漏

使用智能指针(auto_ptr已经被弃用)

这三个智能指针模板(auto_ptr、unique_ptr 和 shared_ptr)都定义了类似指针的对象,可以将ncw获得(直 接或间接)的地址赋给这种对象。当智能指针过期时,其析构函数将使用 delete 来释放内存。因此,如果将new返回的地址赋给这些对象,将无需记住稍后释放这些内存:在智能指针过期时,这些内存将自动被释放。

要创建智能指针对象,必须包含头文件 memory,该文件模板定义。然后使用通常的模板语法来实例 化所需类型的指针。例如,模板 auto_ptr 包含如下构造函数:

1
2
3
4
5
template<class X> class auto_ptr {
public:
explicit auto_ptr(X* p=0) throw();
...
}

throw()意味着构造函数不会引发异常。请求X类型的auto_ptr将获得指向X类型的auto_ptr:

1
auto_ptr<double> pd(new double);

new double是new返回的指针,是构造函数auto_ptr的参数。其他两种智能指针同理。

用智能指针重写remodel函数:

1
2
3
4
5
6
7
8
9
void remodel (string & str){
auto_ptr<string> ps (new string(str));
...
if(weird_thing())
throw exception();
str = *ps;
//delete ps
return;
}

由于智能指针模板类的定义方式,智能指针对象的很多方面都类似于常规指针。例如,如果ps 是一个 智能指针对象,则可以对它执行解除引用操作(“ps)、用它来访问结构成员(ps->pufflndex)、将它赋给指 向相同类型的常规指针。还可以将智能指针对象赋给另一个同类型的智能指针对象。

1
shared_ptr<string> pvac(&vacation); //NO

pvac过期时,delete用于非堆区内存,这是错误的

注意事项

1
2
3
auto_ptr<string> ps (new string("AAA"));
auto_ptr<string> vacation;
vacation = ps;

如果 ps和 vocation 是常规指针,则两个指针将指向同一个 string对 象,程序将试图删除同一个对象两次——一次是 ps过期时,另一次是 vocation 过期 时。要避免这种问题,方法有多种:

  1. 定义赋值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个 对象的副本。
  2. 建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有 对象的智能指针的构造函数会删除该对象。然后,让赋值操作转让所有权。这就是用于 auto_ptr 和 uniquepr的策略,但unique_ptr 的策略更严格。
  3. 建智能更高的指针,跟踪引州特定对象的智能指针数。这称为引用计数(reference counting)。 例如,赋值时,计数将加1.而指针过期时,计数将减1.仅当最后一个指针过期时,才调用 dekte。 这是 shared_pu 采用的策略。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// fowl.cpp -- auto_ptr a poor choice
#include <iostream>
#include <string>
#include <memory>

int main()
{
using namespace std;
auto_ptr<string> films[5] =
{
auto_ptr<string> (new string("Fowl Balls")),
auto_ptr<string> (new string("Duck Walks")),
auto_ptr<string> (new string("Chicken Runs")),
auto_ptr<string> (new string("Turkey Errors")),
auto_ptr<string> (new string("Goose Eggs"))
};

auto_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership

cout << "The nominees for best avian baseball film are\n";
for (int i = 0; i < 5; i++)
cout << *films[i] << endl;
cout << "The winner is " << *pwin << "!\n";
cin.get();
return 0;
}

下面是该程序的输出:

The naminees for best avian baseball film are

Fowl Balls

Duck Malks

Segmentation fault (core dumped)

消息core dumped 表明,错误地使用 auto_pr可能导致问题。这里的问题在于,下面的语句将所有权从 films[2]转让给 pwint

1
pwin = films[2];

这导致 films[2]不再引用该字符串。在 autopr 放弃对象的所有权后,便可能使用它来访问该对象。当 程序打印films[2]指向的字符串时,却发现这是一个空指针,这显然讨厌的意外。

如果使用shared_ptr代替auto_ptr,则可以正常运行。 auto_ptr 一样,unique_ptr 也采用所有权模型。但使用 unique_pr 时, 程序不会等到运行阶段崩溃,而在编译器因下述代码行出现错误:

1
pwin = films[2];

unique_ptr优于auto_ptr

1
2
3
string* p1 = new string("auto");
string* p2 = nullptr;
p2 = p1;

p2接管 string 对象的所有权后,pl的所有权将被剥夺。前面说过,这是件好事,可防 止p1和p2的析构函数试图删除同一个对象;但如果程序随后试图使用 p1,这将是件坏事,因为p1 不再 指向有效的数据。

下面来看使用 unique_ptr 的情况:

1
2
3
unique_ptr<string> p3(new string("auto"));
unique_ptr<string> p4;
p4 = p3;

编译器认为第三句非法,避免p3不指向有效数据。

但有时候,将一个智能指针赋给另一个并不会留下危险的悬挂指针。

1
2
3
4
5
6
7
unique_ptr<string> demo(const char * s)
{
unique_ptr<string> temp(new string(s));
return temp;
}
unique_ptr<string> ps;
ps = demo("Uniquely special");

demo()返回一个临时unique_pr,然后 ps 接管了原本灯返回的 unique_pur 所有的对象,而返回的 unique_pr 被销毁。这没有问题,因为ps抑有了string对象的所有权。但这里的另一个好处是,demo( )返 回的临时unique_pr 很快被销毁,没有机会使用它来访问无效的数据。

总之,程序试图将一个 unique_ptr 赋给另一个时,如果源 unique_pr 是个临时右值,编译器允许这样 做;如果源unique_ptr 将存在一段时间,编译器将禁止这样做。

仅当以非智能的方式使用遗弃的智能指针(如解除引 用时),这种赋值才不安全。要安全地重用这种指针,可给它赋新值。C+有一个标准库函数 std:move( ), 让您能够将一个 unique pe 赋给另·个。下面是一个使用前述 demo( )函数的例子,该函数返回一个 unique_ptr对象:

1
2
3
4
5
6
using namespace std;
unique_ptr<string> ps1, ps2;
ps1 = demo("Uniquely special");
ps2 = move(ps1); // enable assignment
ps1 = demo(" and more");
cout << *ps2 << *ps1 << endl;

相比于 auto_ptr,unique_ptr 还有另一个优点。它有一个可用于数组的变体。别忘了,必须将 dclete 和 new配对,将 delete[]和 new[]配对。模板 auto_pr 使 delete 而不是 delete[].因此只能与 new—起使用, 而不能与new[1一起使用。但uniquepr有使用new[]和 delete [ ]的版本:

1
unique_ptr<double[]>pda(new double(5));

使用new分配内存时,才能使用auto_pr和 shartd_ptr,使用 new[]分配内存时,不能使用它 们。不使用new分配内存时,不能使用autoptr或 shared_ptr;不使用 new或 new[]分配内存时,不能使用 unique ptr.

选择智能指针

果程序要使用多个指向同一个对象的指针,应选择 shared ptr。如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。如果函数使用 new 分配内存,并返回指向该内存的指针,将其返回类型声明为unique_ptr 是不错的选择。这样,所有权将转让给接受返回 值的 uniquc_pu,而该智能指针将负责调用 delete。可将 unique_pr 存储到 STL 容器中,只要不调用将一个 unique_ptr 复制或赋给另一个的方法或算法(如sort())。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unique_ptr<int> make_int(int n)
{
return unique_ptr<int>(new int(n));
}

void show(unique_ptr<int> & pi) // pass by reference
{
cout << *pi << ' ';
}

int main()
{
// ...
const int size = 5;
vector<unique_ptr<int>> vp(size);

for (int i = 0; i < vp.size(); i++)
vp[i] = make_int(rand() % 1000); // copy temporary unique_ptr

vp.push_back(make_int(rand() % 1000)); // ok because arg is temporary

for_each(vp.begin(), vp.end(), show); // use for_each()
// ...
}

其中的 push_back( )调用没有问题,因为它返回一个临时unique_pr.该 uniquepr 被赋给 vp中的一个 unique_pt。另外,如果按值而不是按引用给 show( )传递对象,fox_each( )语句将非法,因为这将导致使用 一个来自 vp的非临时unique_ptr 初始化 pi.而这是不允许的。

在 unique_ptr 为右值时,可将其赋给 shared_ptr,这与将一个 uniquept 赋给另一个需要满足的条件相 同。与前面一样,在下面的代码中,make_int( )的返回类型为 unique_ptr

1
2
3
unique_ptr<int> pup(make_int(rand() % 1000);   // ok
shared_ptr<int> spp(pup); // not allowed, pup an lvalue
shared_ptr<int> spr(make_int(rand() % 1000); // ok

模板 shared_ptr 包含一个显式构造两数,可用于将右值unique_pur 转换为 shared_ptr。shared_ptr 将接管 原来归 unique_ptr所有的对象。

标准模板库

泛型编程

函数对象

很多 STL 算法都使用两数对象——也叫函数符(functor)。函数符是可以以函数方式与( )结合使用的 任意对象。这包括函数名、指向函数的指针和重载了()运算符的类对象(即定义了函数cperator( )( )的类)。

1
2
3
4
5
6
7
8
9
10
class Linear
{
private:
double slope;
double y0;
public:
Linear(double sl_ = 1, double y_ = 0)
: slope(sl_), y0(y_) {}
double operator()(double x) {return y0 + slope * x; }
};

这样,重载的( )运算符将使得能够像函数那样使用Linear 对象:

1
2
Linear f1;
double y1 = f1(12.5);

函数for_each将指定函数用于区间中每个成员:

for_each(book.begin(),book.end(),ShowReview);

通常,第3个参数可以是常规函数,也可以是函数符。如何声明第3个 参数呢?不能把它声明为函数指针,因为函数指针指定了参数类型。由于容器可以包含任意类型,所以预 先无法知道应使用哪种参数类型。STL通过使用模板解决了这个问题。for_each 的原型看上去就像这样:

1
2
template<class InputIterator, class Function>
Function for_each(InputIterator first, InputIterator last, Function f);

ShowReview( )的原型如下:

1
void ShowReview(const Review &);

这样,标识符ShowRevicw的类型将为 void(*)(const Review&),这也是赋给模板参数 Function 的类型、 对于不同的函数调用,Function参数可以表示具有重载的( )运算符的类类型。最终,for_sach( )代码将具有 一个使用()的表达式。在 ShowReview()示例中,f是指向函数的指针,而代)调用该函数。如果最后的 for_each( )参数是一个付象,则()将是调用其重载的( )运算符的对象。

算法

第十七章

文件输入和输出

  • 简单的文件I/O

要让程序写入文件:

  1. 创建一个ofstream对象来管理输出流
  2. 将该对象与特定的文件关联起来
  3. 使用ostream的方法使用该对象,输出将进入文件

首先应包含头文件 fstream,然后声明一个ofstream对象:

1
ofstream fout;

接下来,必须将这个对象与特定的文件关联起来。为此,可以使用open()方法。e.g:

1
fout.open("jar.txt");

可以使用另一个构造函数将这两步(创建对象和关联到文件)合并成一条语句:

1
ofstream fout("jar.txt")

然后,以使用 cout的方式使用fout。例如,要将 Dull Data 放到文件中,可以这样做:

1
fout << "Dull Data";

由于 ostream是 ofstrcam类的基类,因此可以使用所有的 ostream 方法,包括各种插入运算符定义、格 式化方法和控制符。以这种方式打开文件来进行输出时,如果没有这样的文件,将创建一个新文件:如果有这样的文件, 则打开文件将清空文件,输出将进入到一个空文件中。

读取文件的要求与写入文件相似:

  1. 创建一个 ifstream 对象来管理输入流
  2. 将该对象与特定的文件关联起来
  3. 以使用cin的方式使用该对象

上述读立件的步骤类似于写文件,甘先,当然要包含头文件 fstream。然后声明一个 ifstream 对象,将 它与文件名关联起来。

1
2
3
4
ifstream fin;                    // create ifstream object called fin
fin.open("jellyjar.txt"); // open jellyjar.txt for reading

ifstream fis("jamjar.txt"); // create fis and associate with jamjar.txt

现在,可以像使用cin那样使用fin或fis。

1
2
3
4
char ch;
fin >> ch;
string line;
getline(fin, line);

当输入和输出流对象过期(如程序终止)时,到文件的连接将自动关闭。另外,也可以使用 close() 方法来显式地关闭到文件的连接:

1
fout.close();

关闭这样的连接并不会删除流,而只是断开流到文件的连接。然而,流管理装置仍被保留。例如, fin 对象与它管理的输入缓冲区仍然存在,可以将流重新连接到同一个文件或另一个 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// fileio.cpp -- saving to a file
#include <iostream> // not needed for many systems
#include <fstream>
#include <string>

int main()
{
using namespace std;
string filename;

cout << "Enter name for new file: ";
cin >> filename;

// create output stream object for new file and call it fout
ofstream fout(filename.c_str());

fout << "For your eyes only!\n"; // write to file
cout << "Enter your secret number: "; // write to screen
float secret;
cin >> secret;
fout << "Your secret number is " << secret << endl; // write to file
fout.close(); // close file

// create input stream object for new file and call it fin
ifstream fin(filename.c_str());
cout << "Here are the contents of " << filename << ":\n";
char ch;
while (fin.get(ch)) // read character from file and
cout << ch; // write it to screen
cout << "Done\n";
fin.close();
return 0;
}
  • 流状态检查和 is_open( )

C++文件流类从 ios_base 类那里继承了一个流状态成员。,该成员存储了指出流状态 的信息:一切顺利、已到达文件尾、VO操作失败等。如果一切顺利,则流状态为零(没有消息就是好消 息)。其他状态都是通过将特定位设置为1来记录的。文件流类还继承了 ios_base 类中报告流状态的方法。可以通过检查流状态来判断最后一个流操作是杏成功。对于文件流,这包括检查试图打开文件时是否成功。例如,试图打开一个不存在的文件进行输入时,将设置 failbit 位,因此 可以这样进行检查:

1
2
3
4
fin.open(argv[file]);
if (fin.fail()){
...
}

由于 ifstream 对象和 istream对象-样,被放在需要bool类型的地方时,将被转换为bool 值,因此您 也可以这样做:

1
2
3
4
fin.open(argv[file]);
if (!fin){
...
}

然而,较新的 C++实现提供了一种更好的检查文件是否被打开的方法——is_open( ):

1
2
3
if (!fin.is_open()){
...
}
  • 打开多个文件

如果需要同时打开两个 文件,则必须为每个文件创建一个流。然而,可能要依次处理一组文件。。在这种情况 下,可以打开一个流,并将它依次关联到各个文件。这在节省计算机资源方面。比为每个文件打开一个流 的效率高。使用这种方法,甘先需要声明一个 ifstream对象(不对它进行初始化),然后使用open( )方法将 这个流与文件关联起来。例如,下面是依次读取两个文件的代码:

1
2
3
4
5
6
7
8
9
ifstream fin;    // create stream using default constructor
fin.open("fat.txt"); // associate stream with fat.txt file
...
// do stuff
fin.close(); // terminate association with fat.txt
fin.clear(); // reset fin (may not be needed)
fin.open("rat.txt"); // associate stream with rat.txt file
...
fin.close();

命令行处理技术

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// count.cpp -- counting characters in a list of files
#include <iostream>
#include <fstream>
#include <cstdlib> // for exit()
int main(int argc, char * argv[]) {
using namespace std;
if (argc == 1) // quit if no arguments
{
cerr << "Usage: " << argv[0] << " filename[s]\n";
exit(EXIT_FALLURE);
}
ifstream fin; // open stream
long count;
long total = 0;
char ch;
for (int file = 1; file < argc; file++)
{
fin.open(argv[file]); // connect stream to argv[file]
if (!fin.is_open())
{
cerr << "Could not open " << argv[file] << endl;
fin.clear();
continue;
}
count = 0;
while (fin.get(ch))
count++;
cout << count << " characters in " << argv[file] << endl;
total += count;
fin.clear(); // needed for some implementations
fin.close(); // disconnect file
}
cout << total << " characters in all files\n";
return 0;
}