面向懒惰程序员的 C++20 教程(六)
十七、运算符
您可能看到过以下错误:
char string1[] = "Hello", string2[] = "Hello";
if (string1 == string2) ...
这个条件不成立,因为数组的==比较的是内存地址而不是内容,并且地址不同。
这也导致了一些问题:
string2 = string1;
它复制的不是string2的内容,而是它的地址到string1。string1的内容丢失。这既浪费又容易出错:
string2[1] = 'a'; // string1 becomes "Hallo", though it
// wasn't even mentioned here!
因此,让我们创建自己的字符串类,迫使操作符做我们想做的事情,再也不用担心这个问题。
附录 B 列出了 C++ 让我们重载的运算符。简而言之:几乎任何,但你不能自己编。
基本字符串类
class String
{
public:
String (const char* other=""); // conversion from char* constructor;
// default constructor
String (const String &other);
private:
char* contents_;
};
我想让我的String类处理任意长度的字符串,所以我将使用动态内存,就像在第十四章一样。
以下是设置默认值的两种方法:
-
因为
nullptr:nullptr按照惯例什么都不是,所以这是有意义的。但是如果我这样做,我将需要每个函数在访问contents_之前检查nullptr。工作太多。 -
作为长度为 1 只包含
'\0'的字符数组,即作为""(空字符串)。我会用这个。
我现在写构造器: 1
String::String2 (const char* other = "")// conversion from char* constructor;
// default constructor
{
contents_ = new char[strlen(other) + 1];
// The +1 is room for final '\0'
strcpy(contents_, other);
}
String::String (const String &other)
{
contents_ = new char[strlen(other.contents_) + 1];
strcpy(contents_, other.contents_);
}
冗余代码太多了。也许我可以让一个建筑商把工作分包给另一个?当然可以。这个“委托构造器”让其他人做所有的工作。代码重用,更少的输入,耶:
String (const String &other) : String (other.c_str()) {}
现在我将创建一些新的函数。这些放在类定义中:
const char* c_str() const { return contents_; }
int size () const { return (int) strlen (c_str()); }
// Inefficient! Is there a better way?
析构函数
当使用动态分配的数组时,我们需要delete []在完成后抛出内存。但是contents_是String的私有成员,所以main做不到。也不应该;这是String的工作。我们需要一个函数在完成String后被调用。
输入析构函数(或常见的缩写“dtor”):
String::~String () { if (contents_) delete [] contents_; }
//Why "if (contents_)?" Paranoia. Deleting nullptr gives a crash.
这个名为~加上类名的函数,每当String消失时(例如,当String在函数内部声明并且函数结束时),就会被自动调用。
与我们在第十四章中所做的相比,这是
-
工作少:写一次就完事了。
-
自动的,所以你不会忘记。
只要您记住黄金法则,内存管理就变得简单多了:
Golden Rule of Destructors
如果你在一个类中使用动态内存,总是写析构函数。
我再补充一条黄金法则。你可以违反它,但是它确实减少了错误。
Golden Rule of Dynamic Memory
如果你不用它也能得到你想要的,那就不要用它。如果必须的话,试着将它隐藏在一个类中,并用析构函数来清理。
析构函数可以在一个变量的生命周期结束时用于其他事情…但是我从来没有这样做过。
二元和一元运算符:==,!=,和!
这是我们的第一个运算符:
bool String::operator== (const String& other) const
{
return strcmp
(c_str(), other.c_str()) == 0;
}
使用==操作符如下所示:
if (stringA == stringB)...
当计算机到达stringA == stringB时,它进入功能String::operator==。 3 有两个String用了。左边的那个stringA,是“这个”的:拥有这个operator==功能的那个。右边的那个stringB,是“另一个”的,作为参数传入的那个。
当在一个成员函数中,你引用一个指定所有者的成员,如在other.c_str()中,那是属于other的c_str()。如果你不说它属于谁,它属于“这”一个——左边的那个。
这里有个好听的:!=是==的反义词吧?所以不用两个都写:写==c++ 会隐式写!=作为它的否定。 4
这些运算符是二元;每个都需要两个String,一个一元运算符只有一个参数,比如(-myInt)+2中的-(“一元减号”)或者if (! isReady)中的!。举个例子,我来写!对于String类。! myString将意味着myString为空:
bool String::operator! () const { return ! size(); }
Golden Rule of Operators
如果一个操作符有一个参数,那么“this”对象——我们可以引用它的成员而不指定它们是谁的——是操作符调用中提到的唯一对象。
如果一个操作符有两个参数,“this”对象是调用中操作符左边的那个。作为参数传递的是右边的那个。
运算符没有三个参数。 5
所有其他比较运算符
还有其他比较String s 的方法:<(意为“按字母顺序排在前面”)、<=、>、>、??。为了避免让使用我们的类的程序员猜测哪些是我们写的,让我们把它们都提供出来。
但是如果我懒得写全部四个,我就只写一个操作符,让 C++20 完成其余的。6T7】我要写的是一个特殊的操作符<=>(称为“宇宙飞船”操作符,因为如果你仔细看,它看起来像一个 UFO)。它被定义为如果String1 < String2,那么String1 <=> String2应该返回一个负数;如果String1 > String2,那么应该返回一个正数;如果它们相等,那么应该返回 0。
…就像strcmp一样,一个<cstring>函数,给定两个字符数组,返回确切的值。这是我们的三向比较(“飞船”)运算符:
int String::operator<=> (const String& other) const
// automagically generates <, <=, >, and >=
{
return strcmp (c_str(), other.c_str());
}
Exercises
-
制作一个
Fraction类。您应该能够创建分数(指定分子和分母或默认为 0/1),打印它们,并使用所有可用的比较运算符进行比较。 -
制作一个
Point2D类。您应该能够创建点(指定坐标或默认为(0,0)),打印它们,并比较它们。如果两个Point2D的 Xs 和 Ys 相同,则它们相等,但是如果它们的大小(与(0,0)的距离)大于另一个,则其中一个大于另一个。
赋值运算符和*this
我们如何将一个String分配给另一个?
operator= (other)
delete the old memory
allocate the new memory, enough to hold other's contents
copy the contents over
还有一件事=总是做:它返回一些东西。我们通常称之为A=B;,但这也是合法的:
A=B=C;
由于=是从右向左处理的,这就意味着A=(B=C);真正的意思是:在做B=C的时候,把C的值赋给B;返回您获得的值;并通过=发送到A。因此B=C必须返回B变成的值:
operator= (other)
delete the old memory
allocate the new memory, enough to hold other's contents
copy the contents over
return "this"
或者
String& String::operator= (const String& other)7
{
if (contents_) delete[] contents_; // delete old memory
contents_ = new char[strlen(other.c_str()) + 1]; // get new memory
//The +1 is room for final '\0'
strcpy(contents_, other.c_str()); // copy contents over
return *this
;
}
this被定义为“this”对象的内存地址。既然this是指向对象的指针,*this就是对象本身。(我们很少在没有*的情况下使用this,尽管我们可以。)我们希望=返回“这个”对象已经变成的样子;现在有了。
*this是不是永远是从=返回的东西。因为+=、-=等的操作符也返回新修改的对象,所以它们也返回*this。
我想我会重写转换构造器和 operator=来提取它们共有的代码,并把它放在一个新函数copy中。代码重用:
String::String (const char* other="") { copy(other); }
String& String::operator= (const String& other)
{
if (contents_) delete[] contents_; copy (other.c_str());
return *this;
}
void String::copy(const char* str)
{
contents_ = new char[strlen(str) + 1]; // get new memory
// The +1 is room for final '\0'
strcpy(contents_, str); // copy contents over
}
另一个最需要解释的是=的返回类型。
假设是这样写的
String String::operator= (const String& other);
由于没有&,它将调用复制构造器来复制它返回的内容。这需要时间,因为它必须一个字符一个字符地复制数组。如果我们返回的不是副本而是事物本身(*this),我们可以节省时间:
String& String::operator= (const String& other);8
Golden Rule of Assignment Operators
每个赋值运算符(=、+=等)。)应该返回*this。
…通过引用(如String&)。
这是另一条规则。
Golden Rule of =
始终指定=。
原因和复制构造器一样:如果你不这么做,编译器会帮你做,而且可能会用很笨的方法来做。对于String,会定义它复制内存地址。我们试图让远离。
防错法
一个常见的错误是把TheClassName::放在错误的东西前面:
String::const char* c_str() const; //const is a member of String?!
根据编译器的不同,错误消息可能会令人困惑或一清二楚。无论哪种方法,解决方案都是将TheClassName::放在函数名的左端。const char*是返回类型;String::c_str是函数名。
Exercises
-
将前面练习中的
=添加到Fraction类中。 -
将前面练习中的
=添加到Point2D类中。 -
如果你这么说:
myStr = myStr;在String::operator=会发生什么?修复=以避免问题。我的答案在例 17-2 中。
算术运算符
现在我们将做一个“算术”操作符:+。我觉得把+定义为串联的意思是合理的。如果word是"cat",addon是"fish",那么word+addon应该是"catfish"。
我们会写+=和+。使用String的程序员可能想要其中任何一个,如果他们不得不猜测我们提供了哪个,他们有理由感到恼火。
operator+= (other String)
remember the old contents
allocate new contents, big enough that we can add other.contents
copy the old contents into the new
append other contents
delete the old contents
return *this
秩序很重要。如果我们在使用之前删除旧内容,我们将会丢失其中的内容。
下面是有效的 C++ 代码:
String& String::operator+= (const String& other)
{
char* oldContents = contents_;
contents_ = new char [size() + other.size() + 1];
// 1 extra space at the end for the // null char
strcpy (contents_, oldContents); // copy old into new
strcat (contents_, other.c_str());// append other contents
delete [] oldContents;
return *this;
}
很好。现在我可以在类定义中内联operator+。它也应该返回String&吗?
String& operator+ (const String& other) const
// There's something wrong here...
{
String result = *this; result += other; return result;
}
让我们追踪调用它时会发生什么。
假设,在main中,我们说copied = word+addon。首先,我们称之为operator+。使其result(图 17-1 )。
图 17-1
运算符+(有缺陷的版本)在工作
然后它返回它的result并离开(图 17-2 )。但是result,作为+的局部变量,在+完成时被析构,所以main得到的在它得到它的时候已经不存在了。使用它将是一个坏主意。
图 17-2
运算符+(有缺陷的版本)返回其值
解决办法就是还一份。它会一直持续到不再需要为止: 9
String String::operator+ (const String& other) const //That's better
{
String result = *this; result += other; return result;
}
Golden Rule of Returning
const &
局部变量不应该用&返回。
函数返回后会持久的东西,包括*this和数据成员,可能是。如果它们是类类型的,就应该是。
为什么 make + call +=,而不是反过来?+制作了两个副本:局部变量result和我们返回时制作的副本。+=没有局部变量,返回String&,所以效率相当高。如果我们让它调用+,它将不得不做额外的复制。
+应该总是像这里写的那样,不管我们是添加String s,数字,还是长鼻怪(不管它们是什么)——只要把String改成你想要的任何新类型。
Exercises
-
增加
+、-、*、/、+=、-=、*=、/=至Fraction。 -
在
Point2D中增加+、+=、-、-=。还要加上*、*=、/、/=。引用point1/point2可能没有意义,但是引用point1/2会有意义——你可以将两个坐标除以 2 得到一个新的Point2D。因此,*、*=、/和/=的“其他”参数将是一个数字。
[]和()
现在我们将支持使用[]来访问单个字符。
char String::operator[] (int index) const { return contents_[i]; }
我们只完成了一半,因为虽然我们可以说char ch = myString[0];,但是如果我们说myString[0] = 'a';,编译器会抱怨“需要 L 值”
这意味着(非常粗略地——我保持简单)在= (L 代表左)左边的东西不是那种可以在=左边的东西;它是不可修改的。如果你想改变myString,你不需要元素的副本*,而是元素本身:*
char& String::operator[] (int index) { return contents_[i]; }
如果两个函数除了返回类型之外完全相同,编译器不会感到困惑吗?但是它们和 ?? 的不一样;一个是const。所以 C++ 会将const应用于不能改变的事物,将非const应用于可以改变的事物:
const String S ("Hello");
cout << S[0] << '\n'; //OK; uses the const version of []
String T ("Goodbye");
T[0] = 'Z'; //Also OK; uses the non-const version, which
// returns something that can be changed
Golden Rule of [ ] Operators
如果定义[],需要两个版本:
<type> operator[] (int index) const { ... }
<type>& operator[] (int index) { ... }
{}之间的代码几乎肯定是相同的。
还可以添加()运算符。我们可能想说mystring (2, 5)来获得包含字符 2–5 的子串。这是它的宣言;实施例见 17-2 :
String String::operator () (int start, int finish) const;
您可以让()操作符具有不同数量的参数。
我不用()是因为对我来说像mystring (2, 5)这样的东西并不清楚,但是如果你想要它,它就在那里。
Exercises
-
在
Fraction中增加[](const和非const版本)。myFraction[0是分子,myFraction[1]是分母。 -
…或者到
Point2D。point1[0]是 x 坐标,point1[1]是 y 坐标。
++ 和-
说myString++没有多大意义,所以我将从练习转移到Fraction的例子。
myFraction++应该在myFraction上加 1。回想一下++有两个版本:++myFraction,意思是加 1,返回你得到的东西,myFraction++,意思是加 1,返回你加之前拥有的东西。
这是预增量版本:
Fraction& Fraction::operator++ () // used for ++myFraction
{
*this += 1; // add 1 to this Fraction
// (Surely Fraction can convert from int?)
return *this;
}
怎样才能区分后增量版本?不是通过参数的名称或数量…所以 C++ 有一个 hack 10 就是为了这个:
Fraction Fraction::operator++ (int junk) //used for myFraction++
{
Fraction result = *this;
++(*this); //code reuse again
return result;
}
这里的int论证真的是垃圾;这只是一个占位符,用来区分这个++操作符和其他操作符。
Exercises
-
将++ 和-两个版本都添加到
Fraction,并测试。 -
将
++和--两个版本添加到Point2D中,并进行测试。myPoint++会给x_分量加 1。
不是类成员的>>和<<:运算符
我还想用>>和<<打印String s,用cin和cout或其他文件。
我们不能把这些操作符写成成员:操作符左边的东西永远是“这个”对象。但是在cout << myString中,左操作数是cout。如果我们把operator<<写成String的一员,cout就得是String了。
解决方法是让运算符成为非成员:
// this goes OUTSIDE the class definition
ostream& operator<< (ostream& out, const String& foo)11
{
...
return out;
}
我让它返回ostream&,因为当我把<<链接在一起的时候(比如在cout << X << Y)。操作顺序为(cout << X) << Y;也就是说,cout << X做了它的工作,然后返回它的cout的“值”,所以下一个<<有cout作为它的左操作数,可以用来打印。
这是我的第一次尝试:
inline // in string.h
std::ostream& operator<< (std::ostream& out, const String& foo)
{
out << foo.contents_; return out;
}
这不会编译。foo.contents_是私人的。
我们可以通过访问函数返回contents_,但这是一个更通用的解决方案:
class String
{
public:
...
void print (std::ostream& out) const { out << c_str(); }
...
};
inline
std::ostream& operator<< (std::ostream& out, const String& foo)
{
foo.print(out); return out;
}
我们只是以一种适用于我们编写的任何类的方式修复了隐私侵犯——这是一件好事。让我们以类似的方式处理cin >>:
void String::read (std::istream& in);
inline
std::istream& operator>> (std::istream& in, String& foo)
// foo is not const!
{
foo.read (in); return in;
}
String::read比String::print更棘手。这是我的第一次尝试。
void String::read (istream& in) { in >> contents_; }
问题是我们不知道contents_是否有足够的空间来存储输入的内容。
解决方案:
class String
{
public:
static constexpr int BIGGEST_READABLE_STRING_PLUS_ONE = 257;
// biggest string we can read, incl '\0'
// What's this "static" thing? We'll get to that in the next section
...
};
void String::read (std::istream& in)
{
static char buffer [BIGGEST_READABLE_STRING_PLUS_ONE];
in >> buffer;
*this = buffer;
}
如果你是 C++20 兼容的,in >>会在溢出buffer之前停止。如果没有…最好确保buffer足够大。
你可以写其他操作函数作为非成员——传入我们调用的对象*this作为第一个参数:
const String& operator= ( String& left, const String& right);
bool operator== (const String& left, const String& right);
我们通常不会,因为这些函数明显属于String,需要访问私有数据成员。
Exercises
-
为
Fraction类添加ostream <<和istream >>运算符。 -
…还有
Point2D类。
static成员
C++ 爱重用——过度使用——关键字,所以static有三个意思。
一个你知道的是一个局部 变量,当函数关闭时它不会消失,而是保留到下一次调用。这种情况我们已经见过很多次了,最近一次是在这里:
void String::read (std::istream& in)
{
static char buffer [BIGGEST_READABLE_STRING_PLUS_ONE];
in >> buffer;
*this = buffer;
}
另一个我们不会想太多的是一个全局 常量、变量或函数,我们只希望在 .cpp 文件中可见,它是写在中的。
最后一个是应用于整个类的类成员,而不是它的一个特定实例:
class String
{
public:
static constexpr int BIGGEST_READABLE_STRING_PLUS_ONE = 257;
//biggest string we can read, incl '\0'
...
};
这不是某个*String的特征,而是所有*String都有的特征**
**你也可以有一个static成员函数,来报告对所有String都成立的事情:
class String
{
public:
...
static12 int biggestReadableString ()
{
return BIGGEST_READABLE_STRING_PLUS_ONE - 1;
}
...
};
对构造器的显式调用
这很好:
String A;
A = "moo"; // conversion constructor creates a
// String containing "moo", passes to =
A += "moo"; // conversion constructor creates another, passes to +=
// now A == "moomoo"
这里有一些事情不会工作:
A = "moo" + "moo";
当 C++ 处理+时,它不知道你想要属于String的+,因为两个操作数都不是String!所以它会尝试使用字符数组的+。那不会有好结果。
这是可行的:
A = String("moo") + "moo";
对String的调用是“对构造器的显式调用”它创建了一个临时的String变量,从未命名,然后 C++ 将operator+应用于它。当它完成将结果复制到A,时,它删除它。 13
我发现它对Point2D特别有用:
myPoints[0] = Point2D (2, 5);
myPoints[1] = Point2D (3, 7);
...
Exercises
-
编写一个程序,声明五个
Fraction并将它们相乘,不将它们命名为变量,通过使用对构造器的显式调用。 -
编写一个程序,声明五个
Point2D并打印它们,不将它们命名为变量,通过使用对构造器的显式调用。
最终字符串程序
...如示例 17-1 至 17-3 所示。
// class String, for char arrays
// -- from _C++20 for Lazy Programmers_
#include <cstring>
#include "string.h"
using namespace std;
String& String::operator= (const String& other)
{
if (this == &other) return *this; // never assign *this to itself
if (contents_) delete[] contents_; copy(other.c_str());
return *this;
}
void String::copy (const char* str)
{
contents_ = new char[strlen(str) + 1];
// The +1 is room for final '\0'
strcpy(contents_, str);
}
String& String::operator+= (const String& other)
{
char* temp = contents_;
contents_ = new char [size() + other.size() + 1];
// 1 extra space at the end for the null char
strcpy (contents_, temp);
strcat (contents_, other.c_str());
delete [] temp;
return *this;
}
String String::operator () (int start, int finish) const
{
// This constructs the substring
String result = *this;
strcpy (result.contents_, contents_+start);
// contents_+start is the char array that is
// "start" characters after contents_ begins
result.contents_[finish-start+1] = '\0';
// the number of chars in this sequence
// is the difference plus 1
return result;
}
void String::read (std::istream& in)
{
static char buffer [BIGGEST_READABLE_STRING_PLUS_ONE];
in >> buffer;
*this = buffer;
}
Example 17-2string.cpp
// String class
// -- from _C++20 for Lazy Programmers_
#ifndef STRING_H
#define STRING_H
#include <cstring> // uses cstring functions all over
#include <iostream>
class String
{
public:
static constexpr int BIGGEST_READABLE_STRING_PLUS_ONE = 257;
// biggest string we can read, incl '\0'
static int biggestReadableString()
{
return BIGGEST_READABLE_STRING_PLUS_ONE - 1;
}
String (const char* other="") { copy(other); }
String (const String &other) : String (other.c_str()) {}
// a "delegated" constructor
~String() { if (contents_) delete [] contents_; }
// access function
const char* c_str() const { return contents_; }
// functions related to size
int size () const { return (int) strlen (c_str()); }
//Inefficient! Is there a better way?
bool operator! () const { return ! size(); }
// comparisons
bool operator== (const String& other) const
{
return strcmp (c_str(), other.c_str()) == 0;
}
int operator<=> (const String& other) const
{
return strcmp (c_str(), other.c_str());
}
// assignment and concatenation
String& operator= (const String& other);
String& operator+= (const String& other);
String operator+ (const String& other) const
{
String result = *this; result += other; return result;
}
// [] and substring
char operator[] (int index) const { return contents_[index]; }
char& operator[] (int index) { return contents_[index]; }
String operator () (int start, int finish) const;
// I/O functions
void read (std::istream& in );
void print (std::ostream& out) const { out << c_str(); }
private:
char* contents_;
void copy(const char* str);
};
inline
std::istream& operator>> (std::istream& in, String& foo)
{
foo.read (in); return in;
}
inline
std::ostream& operator<< (std::ostream& out, const String& foo)
{
foo.print(out); return out;
}
#endif //STRING_H
Example 17-1string.h. The source code project containing this and Examples 17-2 and 17-3 is 1-2-3-string
驱动程序(例如 17-3 )使用函数void assert (bool condition),该函数验证condition为真,如果不为真,则使程序崩溃。很好。如果有什么不对劲,我们会知道的。
// Driver program to test the String class
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cassert
> // for assert, a function which crashes
// if the condition you give is false
// used for debugging
#include "string.h"
using namespace std;
int main ()
{
// using consts to ensure const functions are right
const String EMPTY;
const String ABC ("abc");
// Testing default ctor, conversion ctor from char*, ==, !=, !
assert (EMPTY == ""); assert (! EMPTY); assert (! (EMPTY != ""));
assert (ABC != ""); assert (! (ABC == ""));
// Testing c_str, size ...
assert (strcmp (ABC.c_str(), "abc") == 0);
assert (ABC.size() == 3);
// Test >, >=, <, <=, !=,
// We're doing lots of implicit calls to conversion ctor
// from const char*, so that's tested too
assert (ABC < "abd"); assert (! (ABC >= "abd"));
assert (ABC <= "abd"); assert (! (ABC > "abd"));
assert (ABC > "abb"); assert (! (ABC <= "abb"));
assert (ABC >= "abb"); assert (! (ABC < "abb"));
assert (ABC <= ABC); assert (ABC >= ABC);
// Test []
String xyz = "xyz";
assert (xyz[1] == 'y'); xyz[1] = 'Y';
assert (xyz[1] == 'Y'); xyz[1] = 'y';
assert (ABC[1] == 'b'); //const version
// Test =, ()
xyz = "xyz and more";
assert(xyz(4, 6) == "and");
// Test copy ctor
assert (ABC == String(ABC));
// Test + (and thereby +=)
String ABCDEF = ABC+"def";
assert (ABCDEF == "abcdef");
// Testing << and >>
String input;
cout << "Enter a string:\t"; cin >> input;
cout << "You entered:\t" << input << '\n';
cout << "If no errors were reported, "
<< "class String seems to be working!\n";
return 0;
}
Example 17-3A driver for String
Exercises
-
使用
assert和对构造器的显式调用,测试你的Fraction类。 -
…或者你的
Point2D课。 -
C++ 的内置库使用从
unsigned int派生的一种类型size_t作为数组大小。更新String类,包括它的测试器,将size_t用于howMany_和其他合适的东西。
#include <string>
下面是我一直隐藏的:C++ 已经有了一个 string 类,你现在知道如何使用它了。你需要#include <string>。型号是string,不是String。如果编译器完全兼容 C++20,您可以声明constexpr string s。
如果我不这样做,C++ 就会这样:
String::String () { contents_ = nullptr; };
String::String (const String& other) { contents_ = other.contents_; }
所以我们将最终使用nullptr,这是我决定反对的,我们将在String之间共享内存,因此改变一个会改变另一个。避免这些隐式构造器是对构造器黄金法则的完美辩护。
2
当讨论一个成员函数时,我通常会以String::开头,以澄清它是一个成员。在类定义中,我们省略了String::。
3
这整个运营商业务是语法糖。你可能叫==这种难听的方式:
if (stringA.operator== (stringB))...
4
如果你的编译器是 C++20 兼容的。如果没有,你就要写:
bool String::operator!= (const String& other) const { return !(*this == other); }
5
有一个例外:?:操作符。下面是它的用法示例:
cout << (x> =0?“正”:“负”);
这意味着if (x>=0) cout << “positive”; else cout << “negative”;
我不怎么用它。反正 C++ 是不会让你过载的。
6
同样,如果你的编译器不兼容 C++20,你必须自己编写它们。
7
关于这个算法的一个有趣的调整,请参见练习 3。
8
const String&可以防止类似(A=(B=C)=D)=E的怪异语句。(那个到底是做什么的?)但是为了兼容尚未涉及的特性,我选择了社区约定:String&,没有const。
9
低效?像往常一样,C++ 强迫性地追求效率,但有一个解决办法(见第十八章)。
10
不恰当的解决方法。
11
在编程语言 LISP 和其他地方,当foo是什么很明显的时候,foo被用来命名一个变量。如果需要两个这样的“占位符”,通常是foo和bar。很有可能这是来自军事缩写“FUBAR”,意思大概是“搞砸得面目全非”
一些程序员认为foo和bar是邪恶的,因为它们没有描述性,但是我想我更愿意读《??》而不是《??》或《??》。
12
static函数不能是const。不用担心;编译器会提醒你。
13
另一个临时拷贝使编译器做更多的工作…第十八章有一个修正。
***
十八、异常、移动构造器和=、递归和 O 符号
一个必要的东西——处理错误条件的更好的方法——和一些非常好的东西:更有效的复制(“移动”函数),调用自己的函数(递归),以及计算函数的时间效率的方法。
例外
程序应该如何处理运行时错误?一些选项:
-
鸵鸟算法:简单地希望问题永远不会发生——你的整数永远不会超过
INT_MAX,你对strcpy的调用永远不会超过char数组,等等。我们做了很多,而且很有效!我们可以在核反应堆的软件中试试吗?验证谁能进入你的银行账户?哎呀。 -
崩溃:也不要在核电厂提出这个建议。
-
打印一条错误消息:适合您的笔记本电脑;对于微波炉或智能手表来说不太好。
-
返回一个错误代码:让你的函数返回
int,如果返回值是表示“错误”的东西(可能是-1),一定是哪里出错了。不过,总是检查返回值是一项很大的工作。
不同的情况需要不同的解决办法。我们需要一种简单的方法来区分错误检测(不会改变)和错误解决(会改变)。
为了说明做到这一点的“异常”机制,让我们举一个例子。栈是计算机科学中常用的数据结构。(你已经在第九章中遇到了调用栈。)就像一堆自助餐厅的托盘。你所能做的,就是在不打扰食堂工作人员的情况下,把一个托盘放在上面(“推”托盘),从上面拿走一个(“砰”一声),看着最上面的一个,注意这个托盘是否是空的。That 和(因为这是 C++)构造并可能破坏它。
我会偷懒(当然!)并避免动态记忆。
以下是我们可能会遇到的问题:
-
我们可能会尝试将一个项目推到一个已经满了的栈上(“溢出”)。
-
我们可能试图从空的栈中查看顶部的项目,或者弹出一个项目(“下溢”)。
成员函数应该只注意到的错误,比如这里的top:
class // A stack of strings
{
public:
class UnderflowException {}; // An "exception class"
...
const std::string& top () const
{
if (empty()) throw UnderflowException ();
else return contents_[howMany_-1]; // the top element
}
...
};
如果一切顺利,top返回Stack中的最后一项,即howMany_-1。但是如果Stack为空,top使用对构造器的显式调用创建一个UnderflowException类型的对象。然后它抛出 s(报告)。
(BNF 版本:throw
<某些变量或值> 。)如果没有人知道如何处理错误,程序就会崩溃,这是应该的。我们已经看到过未处理的异常,比如,当我们试图加载一个文件名拼错的图像时,SSDL 抛出了一个异常。这可能是我们所需要的,为了这个或者为了溢出。
示例 18-1 显示了Stack类,突出显示了与异常相关的代码。
// Stack class, with a limited stack size
// -- from _C++20 for Lazy Programmers_
#ifndef STACK_H
#define STACK_H
#include <string>
class Stack
{
public:
class UnderflowException {}; // Exceptions
class OverflowException {};
static constexpr int MAX_ITEMS = 5;
Stack() { howMany_ = 0; }
Stack(const Stack& other) = delete
;1
const Stack& operator= (const Stack& other) = delete;
const std::string& top () const
{
if (empty()) throw UnderflowException ();
else return contents_[howMany_-1];
}
void push (const std::string& what)
{
if (full ()) throw OverflowException ();
else contents_[howMany_++] = what;
}
std::string pop ()
{
std::string result = top(); --howMany_; return result;
}
bool empty () const { return howMany_ == 0; }
bool full () const { return howMany_ >= MAX_ITEMS; }
// Why not just see if they're equal? howMany_ *can't* be
// bigger than MAX_ITEMS, can it?
// Not if I did everything perfectly, but...
// better to program defensively if you aren't perfect
private:
std::string contents_ [MAX_ITEMS];
int howMany_; // how many items are in the stack?
};
#endif //STACK_H
Example 18-1A Stack class. Find it and Example 18-2 in source code, ch18, as 1-2-stack
但是如果我们愿意,我们可以让调用栈中的函数知道如何捕捉并处理抛出的内容。
把异常当成一个烫手山芋。if语句抛出。如果它所在的函数知道如何捕捉它,很好。如果没有,它就把这个烫手山芋,我指的是UnderflowException,传递给任何一个被称为 it 的函数,然后是那个调用那个函数的函数,以此类推。每一次,函数停止它正在做的事情并立即返回,延迟足够丢弃它的局部变量,必要时析构。这种情况一直持续到我们返回一个可以捕捉错误的函数,或者我们带着错误退出程序。
假设我们决定main应该处理这个错误。我将为main配备一个试抓块(例如 18-2 )。try部分包含了我想做的事情;catch部分包含出错时的错误处理代码。
int main ()
{
Stack S;
try
{
...
cout << S.top (); // if top fails, skip the rest of the try block
// and go straight to the catch block
...
}
catch (const Stack::UnderflowException&)
{
cout << "Error in main: stack underflow.\n";
cout << "Saving everything and quitting...\n";
... code that handles any cleanup we need to do ...
cout << "Quitting now.\n";
return;
}
//maybe a catch for Stack::OverflowException too
return 0;
}
Example 18-2Code to catch an UnderflowException. Also in project/folder 1-2-stack
try - catch块的结构是
try { <do stuff> }
catch (<parameter>) {<error handling code> }
可能还有更多的渔获
**那么什么是是UnderflowException?这就是你所看到的:一个类型为UnderflowException的对象,没有数据成员和成员函数。这很蠢吗?一点也不。throw ing 它告诉main发生了下溢。它还想知道什么?如果出于某种原因,你确实希望你的异常类包含数据成员、函数等等,没问题;throw和catch的工作方式相同。我几乎从来没有。
如果你在一个异常的catch块中,并想再次抛出它,请不带任何参数地说throw。
如果你想禁止一个函数抛出一个异常,在最上面一行追加noexcept:void mustNotThrowExceptions () noexcept。我很少这样做,但是下一节将展示一种用法。
应该使用异常吗?
没错。它们非常适合在应该处理错误的地方进行错误处理,只需编写最少的额外代码。我经常使用它们。(我承认我很少抓到他们。也许那是因为我更喜欢为那些核电站写库而不是软件。)
Exercises
-
改编上一章的
String类,如果传递给[]操作符或子串操作符()的索引超出范围,抛出异常。测试以确保其工作正常。 -
添加并测试一个
Fraction成员函数以转换为double;如果分母为 0,则抛出异常。 -
将前一章中的
istream operator>>修改为Point2D或Fraction,这样如果istream出错(就像在需要int的时候输入了char,它就会抛出一个异常。可以这样检测问题:if (! myIstream) ...
移动构造器并移动=(可选)
做了过多的工作,这可能会减慢我们的速度。(好的,我从来没有注意到,但是 C++ 社区对效率很挑剔。)考虑以下代码:
newString = str1 + str2;
运算符+中有一个临时副本,重复如下。就是我们返回result的时候。这将调用复制构造器,其中包含对strcpy的调用。字符串越大,strcpy耗时越长。
String String::operator+ (const String& other) const
{
String result = *this; result += other; return result;
}
现代 C++ 有一种机制,通过这种机制,某些东西可以将它的内存让给需要它的其他东西,从而避免了复制的需要(例如 18-3 )。
String (String&& other) noexcept // The "move" constructor.
// I'll explain "noexcept" in a moment
{
contents_ = other.contents_; // 2 statements; no loops,
other.contents_ = nullptr; // no strcpy. Cheap!
}
Example 18-3A move constructor for String. In source code as part of project/folder 3-4-string
&&的意思是“如果参数可以放弃它的值,就应用这个函数。”operator+的result肯定是这样!所以我们从result(在 move 构造器中暂时称为other)获取内容。我们给它nullptr,这样当它遇到自己的析构函数时,它不会delete[]它给我们的contents_。
其他额外的工作是在我们离开+之后,当它提供的临时副本被operator=复制时,它也做一个strcpy。如果我们只是将副本的内容移动到newString中,我们可以节省时间。示例 18-4 展示了新的=运算符。
像这样移动,而不是复制,被称为移动语义——我们将再次看到这个术语。
String& String::operator= (String&& other)
noexcept //move =
{
if (contents_) delete[] contents_;
contents_ = other.contents_; //no loops! no strcpy!
other.contents_ = nullptr;
return *this;
}
Example 18-4A move assignment operator for String. Also part of 3-4-string in source code
为了测试这是否真的有效,我建议您这样做!–加载源代码(ch18的3-4-string项目),在新的“move”函数中放置断点,并查看它们是否被调用。这条线应该叫两个:newString = str1 + str2;。
如果 move =或 move 构造器抛出异常,可能会发生奇怪的事情,所以如果我不把noexcept放在最后,编译器可能会警告我。我喜欢让它开心,所以我喜欢。
因为我们现在有了更多的选择,所以我想用下面的内容来取代旧的构造器和=的黄金法则:
Golden Rule of Constructors and =
要么有
-
没有构造器并且没有指定=(本质上是旧式的
structs ),或者 -
指定了默认构造器、复制构造器和=或
-
默认构造器、复制构造器和=,加上移动构造器和移动=。
Exercises
- 改编第十六章的练习 5 及其
Track和Album来使用动态记忆。然后写一个 move 构造器,为Album移动=。使用调试器进行测试,以确保您在应该使用 move 函数的时候真的使用了它。
递归(可选;在下一节中引用)
有时候从事物本身来定义它是最简单的。
例如,考虑“阶乘”函数。5!(读作“5 阶乘”)就是 54321 = 120。0!和 1!都是 1;总的来说,n!是 n*(n-1)(n-2)...21.(n-1)!is (n-1)(n-2)...21;所以 n!= n(n-1)!。随着 n 的增加,结果越来越大。
这是一个计算 n 的算法!;示例 18-5 显示了完整的功能。
if n is 0, return 1
else return n * (n-1)!
这说明了确保递归(一个调用自身的函数)终止所需的两个一般原则:
-
**必须有一个结束条件。**否则,递归永远不会结束(直到程序因内存不足而崩溃)。
-
出于同样的原因,必须朝着那个结束条件前进。
int factorial (int n) // maybe give it unsigned --
// and return unsigned long long?
{
if (n == 0) return 1;
else return n * factorial (n-1);
}
Example 18-5A simple recursive function, in source code as 5-factorial
递归有效的原因是 C++ 为每个调用创建了一个新的副本(“激活记录”)。假设您的主程序调用了参数为 3 的factorial。
n不是 0,只好调用factorial (n-1)。
我们一次又一次地调用factorial,直到我们得到一个版本的factorial,它的 n = 0,所以不再递归。它将向factorial(1)调用返回 1。
然后,factorial(1)返回1*factorial(0),也是 1。
factorial(2)返回2*factorial(1),为 2。
最后,factorial(3)返回3 * factorial(2),即 6。
如果你在微软 Visual Studio 中调试一个带有递归函数的程序,你可以在调用栈中看到该函数的副本(图 18-1 )。
图 18-1
Visual Studio 中的调用栈
在ddd或gdb中,where向您显示调用栈。up和down带你在上面的函数副本之间切换——当你print一个变量时,它会使用你正在看的副本的上下文。
因为一个递归函数有多个副本,所以对于编译器来说,比我们通常的循环一个动作的方法,也就是迭代,要多做更多的工作。但是有时候递归编写函数比迭代更容易。
The Golden Rule of Recursion
每个递归函数都必须有,所以它会终止,
-
没有进一步递归调用的基本情况
-
在每次递归调用中向基本情况前进
防错法
-
你的程序运行了一会儿,然后崩溃说“分段错误”或“栈溢出”
要么你忘记了结束条件,要么你没有朝它前进。您可以使用调试器来判断。
Exercises
-
斐波那契数列是这样的:
斐波那契(1) = 1
Fibonacci (2) = 1
如果 n > 2,则 Fibonacci(n)= Fibonacci(n–1)+Fibonacci(n–2)。
使用递归来编写斐波那契函数和一个程序来测试它。
-
编写并测试一个函数
void indent (const char* what, int howMuch),它在缩进howMuch空格后打印字符串what。如果howMuch为 0,则只打印字符串。如果没有,它打印一个空格并用howMuch-1空格来调用自己。 -
编写并测试
pow函数的递归版本。pow (a, b)返回 a b 。提示:a b = a * a b-1 。 -
编写并测试一个递归函数 log,给定一个正整数
number和一个整数base,返回 log basenumber。log base (number)定义为在达到 1 之前,你可以将数字除以底数的次数。比如 8/2 得 4,4/2 得 2,2/2 得 1;那是三个师;所以 log 2 8 是 3。我们不会担心小数部分;log 2 15 也是 3,因为(用整数除法)15/2 是 7,7/2 是 3,3/2 是 1。
效率和 O 符号(可选)
假设我们想对一个名字列表进行排序。我知道!让我们生成所有可能的名字排序,当我们得到一个完全有序的名字时就停止!电脑很快,对吧?
do
generate a new permutation of the elements
while we haven't found an ordered sequence
这不是很多细节,但我预测一个问题。假设有四个元素。第一个元素有四种可能。这就给下一个留下了三种可能性,给下一个留下了两种可能性,给最后一个留下了一种可能性;有 432*1 种可能:4!。所以对于 N 个元素,我们有 N 个!要考虑的顺序。用 N = 100,那就是 10 158 。电脑很快,但它们没有 ?? 那么快。
有时候算法显然是个坏主意。有时直到你运行它,你才意识到它有多糟糕——除非你使用符号,这样你就能知道什么程序而不是要带给管理层,什么程序而不是要花时间去写。
考虑以下代码:
for (int i = 0; i < N; ++i)
sum += array[i];
初始化完成一次;比较、数组引用、赋值和递增都要进行 N 次。我们可以说有 1 + 4N 个东西被执行。
其他一些有循环的片段呢?可能有点不同,我们得到,哦,5 + 3N。哪个更快,或者它们是一样的?嗯。我们需要一个比较的方法。
O 符号极大地简化了我们描述这些时间需求的方式,从而帮助我们比较和评估它们。以下是 O 符号的简化规则:
-
当数据集很大时,如果一个加数明显小于另一个加数,则丢弃较小的加数。所以如果我们有 1 + 3N,我们丢弃 1,得到 3N。
-
放弃常量乘数。3N 变成 n。
结果写成 O(N)。for 循环是 O(N),或者“是 N 阶的”
这种简化是合理的。我们关心的是当数据集很大时会发生什么(小数据集总是很快)。当 N 较大时,3N + 1 约为 3N;3,000,001 和 3,000,000 的差别可以忽略不计。我们也不关心常量乘数。无论是 N 从 3000 翻倍到 6000,还是 3N 从 9000 翻倍到 18000,都还是翻倍,我们想知道增加 N 是如何降低性能的。这告诉我们。
这里还有几个例子。考虑这个算法:
read in N 1
read in M 1
read in P 1
add them 1
divide by 3 1
print the average 1
每一行是一个动作。把它们加起来,我们有六个。我们可以抛弃常量乘数;6 = 6 * 1,所以 O(6)=O(1)。这个算法是一阶的;不管我们给它什么值,它都需要相同的时间。
这里还有一个:
for each element in an array N x
if this element is negative (1
change it to positive + 1)
最后一行是一个动作。包含它的 if 语句多一个;O(2)=O(1)。因为是在 for 循环中,所以会做 N 次,其中 N 是数组的长度。所以这个算法是 O(N)。
这里还有一个:
do
for each successive pair of elements in an array
if they are in the wrong order
swap them
while our last iteration of the do-while loop had a swap in it
这个算法是一种对数组进行排序的方法。它是这样工作的。考虑一组芝麻街角色。
| 科米蛙 | 格罗弗 | 伯特(男子名ˌ等于 Burt) | 奥斯卡金像奖 | 小猪 | 埃尔默 |do-while 循环的第一次迭代根据需要对每个连续的对进行交换。克米特应该换成格罗弗:
伯特:
我们继续在数组中移动,直到到达末尾,交换任何顺序错误的数组对。
| 格罗弗 | 伯特(男子名ˌ等于 Burt) | 科米蛙 | 奥斯卡金像奖 | 埃尔默 | 小猪 |它仍然不正常,但我们取得了进展。这是我们再次遍历数组后得到的结果:
| 伯特(男子名ˌ等于 Burt) | 格罗弗 | 科米蛙 | 埃尔默 | 奥斯卡金像奖 | 小猪 |另一个:
| 伯特(男子名ˌ等于 Burt) | 格罗弗 | 埃尔默 | 科米蛙 | 奥斯卡金像奖 | 小猪 |另一个:
| 伯特(男子名ˌ等于 Burt) | 埃尔默 | 格罗弗 | 科米蛙 | 奥斯卡金像奖 | 小猪 |这种算法被称为“冒泡排序”,因为元素逐渐“冒泡”到正确的位置。(还有“bogo-sort”被人说是邪恶的,因为太慢了。我不知道他们会把我之前的排列方法叫做什么,但它不会很好。)
用 O 表示法需要多长时间?“如果它们的顺序不对,就交换它们”是 O(1)。我们遍历整个数组,进行 N-1 次比较;所以通过数组的次数是 O(N-1)=O(N)。我们通过了多少次?如果数组非常无序——比方说,如果 Bert 在最后一个槽中——我们将需要 N-1 次传递,因为每次传递最多将 Bert 向左移动一个槽。O(N(N-1))= O(N2-N)= O(N2)。
O(N 2 )称为“二次时间”;O(N)可预测地称为“线性时间”O(1)是“恒定时间”我们尽量避免 O(2 N ),“指数时间”
Online Extra
要测量实际的时间…“测量你的代码到毫秒”:在 YouTube 频道“以懒惰的方式编程”,或者在 www.youtube.com/watch?v=u_zyO7LgXog 找到它
运筹学
在 Apress 博客上用 C++ 计时的事情: www.apress.com/us/blog/all-blog-posts/timing-things-in-c-plus-plus/17405398 。
Exercises
-
对于示例 10-3 中返回数组中最小数字的函数,用 O 表示的时间是多少?
-
…对于一个函数来说,确定一个单词是否是回文?
-
…对于打印 N
×N 网格中所有元素的函数? -
写一个函数
intersection,在给定两个数组的情况下,找到所有相同的元素,并将它们放入一个新数组中。用 O 符号表示的时间要求是什么? -
编写冒泡排序,并验证它的工作原理。
-
写一个程序,使用厄拉多塞筛找出所有达到某个极限的素数:你遍历并消除除 2 之外的所有能被 2 整除的数,然后除 3 之外的所有能被 3 整除的数,依此类推。
用 O 符号表示的时间要求是什么?
-
上一节的练习 2、3 和 4 需要多长时间?
-
使用“在线附加”链接中的方法,对不同大小的数组进行时间泡排序。和 O 符号预测的相符吗?预计您测量的时间会有很多随机变化。
这表示,“我不会写这个函数,编译器也不会。”复制一个栈对我来说似乎是不安全的,我绝对看不到一个原因。如果有任何东西试图调用这个函数,编译器会说不。
你也可以删除默认的构造器和析构函数,虽然我很少这样做。
**
十九、继承
本章的目的是让我们能够在相似的类之间重用代码。
继承的基础
有一个不成文的规则,在 C++ 介绍文本中,你必须有一个使用雇员记录的例子,比如 Example 19-1 。有道理。我个人想不出比员工记录更令人兴奋的事了。
// Class Employee
// -- from _C++20 for Lazy Programmers_
#ifndef EMPLOYEE_H
#define EMPLOYEE_H
#include <iostream>
#include <string>
#include "date.h"
class Employee
{
public:
Employee ();
Employee (const Employee&) = delete;
Employee (const std::string& theFirstName,
const std::string& theLastName,
const Date& theDateHired, int theSalary);
Employee& operator= (const Employee&) = delete;
void print(std::ostream&) const;
// access functions
const std::string& firstName () const { return firstName_; }
const std::string& lastName () const { return lastName_; }
const Date& dateHired () const { return dateHired_; }
int salary () const { return salary_; }
bool isOnPayroll () const { return isOnPayroll_; }
int badPerformanceReviews () const
{
return badPerformanceReviews_;
}
void quit () { isOnPayroll_ = false;}
void start () { isOnPayroll_ = true; }
void meetWithBoss () { ++badPerformanceReviews_; }
private:
std::string firstName_, lastName_;
Date dateHired_;
int salary_;
bool isOnPayroll_;
int badPerformanceReviews_;
};
inline
std::ostream& operator<< (std::ostream& out, const Employee& foo)
{
foo.print (out); return out;
}
#endif //EMPLOYEE_H
Example 19-1Class Employee. To find this or any subsequent numbered example in source code, go to the appropriate chapter and find the project/folder with the example number near the start – in this case, 1-2-employees
由于我正在使用Date类,我需要将前一章中的date.h和date.cpp复制到我的新项目的文件夹中。用 g++ 我就做到了这一点;在 Visual Studio 中,我右键单击该项目,说添加➤现有项,并添加两者。
下面是一个Employee的示例声明。它显式调用了Date的构造器,以节省我们的一些输入:
Employee george ("George P.", "Burdell", Date (10, 3,1885), A_MERE_PITTANCE);
但不是所有的员工都一样。如果我在斯科特·亚当斯的呆伯特中读到的是准确的,经理就像任何其他员工一样,有名字和薪水,但有额外的特征:雇佣和解雇的权力,低智商,以及对折磨员工的痴迷。
为Manager写一个全新的类是多余的,重复Employee中的那些部分,比如firstName、lastName和salary。因此,我们将使Manager成为Employee的子类(或派生类,或子类),如示例 19-2 所示。这就是传承。
// Class Manager
// -- from _C++20 for Lazy Programmers_
#ifndef MANAGER_H
#define MANAGER_H
#include "employee.h"
using Meeting = std::string;
class Manager: public Employee
{
public:
Manager ();
Manager (const Manager&) = delete;
Manager (const std::string& theFirstName,
const std::string& theLastName,
const Date& theDateHired,
int theSalary);
~Manager () { if (schedule_) delete [] schedule_; }
Manager& operator= (const Manager&) = delete;
void hire (Employee& foo) const { foo.start (); }
void fire (Employee& foo) const { foo.quit (); }
void laugh() const
{
std::cout << firstName() << " says: hee-hee!\n";
}
void torment (Employee&) const;
private:
Meeting* schedule_;
int howManyMeetingsOnSchedule_;
void copy (const Manager& other);
};
#endif //MANAGER_H
Example 19-2Class Manager
class Manager: public Employee表示Manager是Employee的子类,一个Employee有一个Manager也有(图 19-1 )。(暂时不用担心public这个词。)
图 19-1
继承是如何工作的。Employee中的所有东西也在Manager中——但如果是private,就看不到了
所以这段代码是合法的:
Manager alfred ("Alfred E.", "Neumann", Date (10, 1,1952), OBSCENELY_LARGE_SALARY);
// firstName, salary are inherited from Employee
cout << alfred.firstName() << " makes " << alfred.salary ()<< " per month!\n";
没有重写这些函数,只是使用它们。你可以用一个Employee做什么,你也可以用一个Manager做什么——因为一个Manager 就是一个Employee。
继承和成员变量的构造器和析构函数
当我们写Manager的时候,我们肯定想要调用Employee的构造器。没问题。我们将使用与初始化数据成员相同的语法:
Manager::Manager (const string& theFirstName,
const string& theLastName,
const Date& theDateHired,
int theSalary) :
Employee (theFirstName, theLastName, theDateHired, theSalary),
schedule_ (nullptr), howManyMeetingsOnSchedule_ (0)
{
}
构造器按以下顺序工作:
-
“:”后面的构造器被调用,先调用父类构造器(即使你把它放在后面)。
-
在
{}之间的任何事情都会被完成(在这种情况下,什么都不做)。
你不说调用什么父构造器,它就调用默认的。
当一个Manager超出范围时,它的析构函数首先被调用,然后是它的父类(然后是它的祖父类,依此类推)。你不需要考虑这个,这是自动的。
如果你不写析构函数,你会得到默认的析构函数,它告诉所有有自己析构函数的成员——比如Employee的firstName_和 lastName_——清理它们的内存。同样,这是自动的——你不需要记住。
Exercises
对于这些练习,请确保
-
An eyeglasses prescription lists: sphere or power, additional correction for bifocals, and stuff for astigmatism (cylinder, axis) – see Figure 19-2. The two eyes are called “OD” and “OS,” not “left” and “right,” because what’s cooler than calling your eyeballs weird Latin abbreviations?
图 19-2
眼镜处方
编写并测试一个类来包含并整齐地打印这些信息。
为隐形眼镜编写并测试一个子类,以包含并打印更多的内容:背部曲率和直径,这些数字确保镜片舒适地适合眼球。
-
接受特殊教育的孩子可能会有一个“IEP”,一个个人教育计划,以阐明存在哪些特殊需求以及学校将如何解决这些需求。编写并测试一个学生记录的类(名字,其他相关的东西)和一个有 IEP 的学生记录的子类。你可以让 IEP 是一个单独的字符串。
-
数据成员是私有的(当然)。
-
目前,没有访问功能。这是为了确保子类只访问自己的数据,而不是父类的数据。例如,要打印,子类应该调用其父类的打印函数,然后打印自己的数据成员。
作为一个概念的继承
子类 ss 也是我们在计算机之外思考的一部分。生物学中,动物是生物的一个子类;哺乳动物是动物的一个子类;人类是哺乳动物的一个子类;而 ubergeek 是人类的子类(图 19-3 )。在每种情况下,子类都具有超类(或父类或基类)的所有特征,外加额外的特征。动物是可以移动的有机体;超级极客是一个编程非常好的人,连上帝都为之折服。
柏拉图和亚里士多德,两个最早争论面向对象编程的人。有几分地
你不是人类的子类,因为你不是一个类。你是人类的实例。(向我的外星读者道歉。)
这是提及面向对象思维中常用的区别的最佳点,在是-a 和有-a 之间。
超级极客是人类(以及哺乳动物和其他东西)。一个超级极客有一台电脑。所以当上帝创造超级极客时,他的代码一定是这样的:
class Ubergeek: public Human // an Ubergeek is-a human
{
...
private:
Computer myComputer_; // an Ubergeek has-a computer
};
事情一Ubergeek 已经走在了私人路段。什么是Ubergeek在第一行。
*Extra
你当地的哲学教授可能会因为我这么说而枪毙我,但是面向对象编程实在是太…柏拉图式了。
柏拉图认为阶级(“理想”)是最终真实的,而特定的例子——我们词汇中的物体或变量——是最终现实的不完美的例子。所以Human是真实的东西;你和我只是例子。
在激进的唯物主义中——离柏拉图越远越好——阶级是不真实的;只有实物才是。当然,由于激进的唯物主义不是一个物质对象,这可能是一个问题,但是懒惰的哲学,不像懒惰的编程,超出了本书的范围。
亚里士多德认为事物本身是真实的,类是它们固有的性质。你是真实的,人类才是真实的你。他分摊差额。
然而在现实中,C++ 是柏拉图式的:类是第一位的。(在创建该类型的变量之前,必须有类定义。)
纸牌游戏的课程
人们喜欢在电脑上玩纸牌,所以让我们制作课程来帮助我们建立各种各样的纸牌游戏。代码重用。
我将提供类Card(例子 19-3 )并赋予它任何类都应该有的东西:默认和复制构造器、operator=、访问函数和 I/O。我还将第十章的Rank和Suit enum拼凑起来。
// Card class
// -- from _C++20 for Lazy Programmers_
#ifndef CARD_H
#define CARD_H
#include <iostream>
// Rank and Suit: integral parts of Card
// I make these global so that I don't have to forget
// "Card::" over and over when I use them.
enum class Rank { ACE=1, JACK=11, QUEEN, KING }; // Card rank
enum class Suit { HEARTS, DIAMONDS, CLUBS, SPADES }; // Card suit
enum class Color { BLACK, RED }; // Card color
inline
Color toColor(Suit s)
{
using enum Suit;
using enum Color;
if (s == HEARTS || s == DIAMONDS) return RED; else return BLACK;
}
// I/O on Rank and Suit
std::ostream& operator<< (std::ostream& out, Rank r);
std::ostream& operator<< (std::ostream& out, Suit s);
std::istream& operator>> (std::istream& in, Rank& r);
std::istream& operator>> (std::istream& in, Suit& s);
// Told you we'd find a way to do arithmetic with enums
...
inline Rank operator+ (Rank r, int t) { return Rank(int(r) + t); }
inline Rank operator+= (Rank& r, int t) { return r = r + t; }
inline Rank operator++ (Rank& r) { r = Rank(int(r) + 1); return r; }
inline Rank operator++ (Rank& r, int junk)
{
Rank result = r; ++r; return result;
}
inline Suit operator++ (Suit& s) { s = Suit(int(s) + 1); return s; }
inline Suit operator++ (Suit& s, int junk)
{
Suit result = s; ++s; return result;
}
class BadRankException {}; // used if a Rank is out of range
class BadSuitException {}; // used if a Suit is out of range
// ...and class Card.
class Card
{
public:
Card (Rank r = Rank(0), Suit s = Suit(0)) : rank_ (r), suit_ (s)
{
}
Card (const Card& other) : Card(other.rank_, other.suit_){}
Card& operator= (const Card& other)
{
rank_ = other.rank(); suit_ = other.suit (); return *this;
}
bool operator== (const Card& other) const
{
return rank() == other.rank () && suit() == other.suit();
}
Suit suit () const { return suit_; }
Rank rank () const { return rank_; }
Color color() const { return toColor (suit()); }
void print (std::ostream &out) const { out << rank() << suit(); }
void read (std::istream &in );
private:
Suit suit_;
Rank rank_;
};
inline std::ostream& operator<< (std::ostream& out, const Card& foo)
{
foo.print (out); return out;
}
inline std::istream& operator>> (std::istream& in, Card& foo)
{
foo.read (in); return in;
}
#endif //CARD_H
Example 19-3The Card class (card.h). card.cpp is in the book’s source code
图 19-3
阶级等级制度。超级极客是人类,是哺乳动物,等等
继承层次结构
我们可能会创造一些游戏:
自由电池(图 19-4 ,左侧)。左上角是单元格,每个单元格可以存储一张卡;右上方是基础,各取 a,再取 2,以此类推,同花色;底部是一堆,随机分配。你可以从一堆牌中取出一张牌,或者如果牌是交替颜色的,你可以将一张牌添加到一堆牌中。例如,如果你有一张黑方 10,你可以把它移到方块 j 上。
图 19-4
两个流行的纸牌游戏:空当接龙(左)和克朗代克(右)
克朗代克(图 19-4 ,右)。和 Freecell 一样,它有基础(右上);它也有一个甲板,一个废物堆,在底部有自己类型的堆。
红心、黑桃和其他有一副牌和手牌的多人游戏。
常见的卡分组包括
-
甲板:你可以洗牌,并处理顶部。
-
废物(弃牌堆):你可以放一张牌在上面或者拿走一张。
-
单元格:像废物一样,你可以添加到单元格中或从顶部取走-但只能有一张卡片。
-
基础:清一色,从 ace 开始往上。
-
手:你可以添加它,并拿出任何你想要的卡。
-
自由电池堆:加一张牌,颜色交替向下;取下一张卡片。
-
克朗代克堆:更复杂,作为练习留下。
这些都有两个共同点:内容和大小。有更多的共同点吗?
我会说一个单元格是一个废物堆,因为你与它互动的方式是一样的:添加一张卡片,然后从顶部拿走一张。它只是在尺寸上有限制。
除此之外,我会说没有。你不能说基础是甲板的特殊情况,或者克朗代克桩是废物堆的特殊情况。(有些可能是见仁见智。)但它们都有一个共同点:都是一组组的牌。所以我们可以有一个CardGroup类,并从它继承。(A Hand是没有添加任何东西的CardGroup,所以我们将Hand作为CardGroup : using Hand = CardGroup;的别名)。
我基于CardGroup提出了图 19-5 中的继承层次。CardGroup如示例 19-4 所示。
图 19-5
卡片组的类别层次结构
// CardGroup class (for playing cards)
// -- from _C++20 for Lazy Programmers_
#ifndef CARDGROUP_H
#define CARDGROUP_H
#include "card.h"
class OutOfRange {}; // Exception classes
class IllegalMove {};
class CardGroup
{
public:
static constexpr int MAX_SIZE = 208; // if anybody wants a game
// w/ more than 4 decks,
// change this.
CardGroup () { howMany_ = 0; }
CardGroup (const CardGroup& other){ copy(other); }
CardGroup (const Card& other)
{
howMany_ = 0; addCard (other);
}
CardGroup& operator= (const CardGroup& other)
{
copy(other); return *this;
}
bool operator== (const CardGroup& other) const;
Card& operator[] (unsigned int index);
Card operator[] (unsigned int index) const;
Card remove (unsigned int index);
Card top () const { return (*this)[size()-1]; }
Card removeTop () { return remove (size()-1); }
unsigned int size () const { return howMany_; }
bool isEmpty() const { return size() == 0; }
bool isFull () const { return size() >= MAX_SIZE; }
// addCard does NOT check that it's legal to add a card.
// We need this for creating CardGroups during the deal.
void addCard (const Card&);
// makes sure the addition of the card is legal, then adds it
void addCardLegally (const Card& other);
void print (std::ostream&) const;
private:
unsigned int howMany_;
Card contents_ [MAX_SIZE];
void copy (const CardGroup&); // copy cards over; used by =, copy ctor
};
inline
std::ostream& operator<< (std::ostream& out, const CardGroup& foo)
{
foo.print(out); return out;
}
using Hand = CardGroup;
#endif //CARDGROUP_H
Example 19-4cardgroup.h
private继承
考虑一下Waste类。我们不应该允许通过[]操作符随机访问Waste;你只能看一堆Waste的顶牌。
为了限制访问,我们更改了继承的类型:
class Waste: private CardGroup
{
...
这使得CardGroup的公共成员进入Waste的私有部分(图 19-6 )。
图 19-6
公共(a)和私有(b)继承。如果有任何继承的公共成员必须在子类中保持私有,则使用私有
operator[]现在是私有的——很好——但是有一些CardGroup的公共成员我们希望 Waste对外开放:例如isEmpty和print。因为它们是私有的,我们用相同的名字创建新的公共函数,简单地调用父函数,如例 19-5 所示。
// Waste class
// -- from _C++20 for Lazy Programmers_
#ifndef WASTE_H
#define WASTE_H
#include "cardgroup.h"
class Waste: private CardGroup
{
public:
Waste () {}
Waste (const Waste& other) : CardGroup (other) {}
Waste (const CardGroup& other) : CardGroup (other) {}
Waste& operator= (const Waste& other) = delete;
bool operator== (const Waste& other) const
{
return CardGroup::operator== (other);
}
bool isEmpty () const { return CardGroup::isEmpty (); }
bool isFull () const { return CardGroup::isFull (); }
unsigned int size () const { return CardGroup::size (); }
Card top () const { return CardGroup::top(); }
Card removeTop () { return CardGroup::removeTop(); }
void addCardLegally (const Card& foo)
{
CardGroup::addCardLegally (foo);
}
void print (std::ostream& out) const{ CardGroup::print (out); }
};
inline
std::ostream& operator<< (std::ostream& out, const Waste& foo)
{
foo.print (out); return out;
}
#endif //WASTE_H
Example 19-5Class Waste, in waste.h, using private inheritance
隐藏继承的成员函数
Waste有来自CardGroup的成员函数isFull,如果Waste有MAX_SIZE卡则为真。它的子类Cell有不同的版本。如果你在一个Cell上调用isFull,它会用哪个?子版本“隐藏”了继承的版本;如果你在一个Cell上调用isFull,你会得到Cell的版本。
但是如果有时候我们仍然需要继承的版本呢?在 19-6 的例子中,Cell的版本addCardLegally调用Waste的版本,通过在调用前加上Waste::来指定。
// Cell class
// -- from _C++20 for Lazy Programmers_
#ifndef CELL_H
#define CELL_H
#include "waste.h"
class Cell: public Waste
{
public:
Cell () {}
Cell(const Cell& other) : Waste (other) {}
Cell& operator= (const Cell& other) = delete;
// public inheritance, so all public members of Waste are here...
bool isFull () const {return ! isEmpty (); }
void addCardLegally (const Card& card)
{
if (isFull ()) throw IllegalMove (); // Cell must be empty
else Waste::addCardLegally (card);
}
};
#endif //CELL_H
Example 19-6cell.h
蒙大拿州的一场比赛
蒙大拿纸牌游戏使用Cell和Deck,所以它应该是对我们等级制度的一个很好的测试。
规则是:在一个 4 × 13 的格子中分发所有的牌,去掉 a,得到一个如图 19-7 所示的混乱局面。
图 19-7
蒙大拿州的一场比赛
您的目标是从 2 到 king 排成四排,每排一套。
唯一有效的方法是把一张牌放进一个空的格子里。你放的牌必须跟在它左边的牌后面,在同一套牌里;比如,你只能跟随 2♥和 3♥.如果它在最左边的一列,你就要放一个 2。国王后面的空格不可用。
当你陷入困境时,从左边的 2 开始,按花色递增,重发所有不在序列中的牌。你有四笔交易。
示例 19-7 至 19-9 显示了程序,为简洁起见省略了一些内容;本书的示例代码包含一个完整的版本。
// class Montana, for a game of Montana solitaire
// -- from _C++20 for Lazy Programmers_
#include "gridLoc.h"
#include "cell.h"
#include "deck.h"
#ifndef MONTANA_H
#define MONTANA_H
class Montana
{
public:
static constexpr int ROWS = 4, COLS = 13;
static constexpr int NUM_EMPTY_CELLS = 4;// 4 empty cells in grid
static constexpr int MAX_TURNS = 4;// 4 turns allowed
class OutOfRange {}; // Exception class for card locations
Montana () {};
Montana (const Montana&) = delete;
Montana& operator= (const Montana&) = delete;
void play ();
private:
// displaying
void display () const;
// dealing and redealing
void deal (Deck& deck, Waste& waste);
void cleanup (Deck& deck, Waste& waste); // collect cards // for redeal
void resetGrid (); // make it empty
// playing a turn
void makeLegalMove (bool& letsQuitOrEndTurn);
void makeMove (const GridLoc& oldLoc,
const GridLoc& newLoc);
bool detectVictory () const;
void congratulationsOrCondolences(bool isVictory) const;
// working with empty cells
// store in emptyCells_ the location of each empty cell
void identifyEmptyCells ();
// which of the empty cells has this row and col? A B C or D?
char whichEmptyCell (int row, int col) const;
// Is this a valid cell index? It must be 0-3.
bool inRange (unsigned int emptyCellIndex) const
{
return (emptyCellIndex < NUM_EMPTY_CELLS);
}
// placing cards
Cell& cellAt (const GridLoc& loc)
{
if (inRange (loc)) return grid_[loc.row_][loc.col_];
else throw OutOfRange();
}
const Cell& cellAt (const GridLoc& loc) const
{
if (inRange (loc)) return grid_[loc.row_][loc.col_];
else throw OutOfRange();
}
// Is this location within the grid?
bool inRange (const GridLoc& loc) const
{
return (0 <= loc.row_ && loc.row_< ROWS && 0 <= loc.col_ && loc.col_< COLS);
}
// Can Card c follow other card?
bool canFollow (const Card& c, const Card& other) const
{
return c.suit() == other.suit() && c.rank() == other.rank() + 1;
}
// Can card c go at this location?
bool canGoHere (const Card& c, const GridLoc& loc) const;
// Is the cell at row, col ordered at its location? That is,
// could we put it here if it weren't already?
bool cellIsCorrect (int row, int col) const
{
return ! grid_[row][col].isEmpty () &&
canGoHere (grid_[row][col].top(), GridLoc (row, col));
}
// data members
Cell grid_ [ROWS][COLS]; // where the cards are
GridLoc emptyCells_ [NUM_EMPTY_CELLS];// where the empty cells are
};
#endif //MONTANA_H
Example 19-8montana.h
// A game of Montana solitaire
// -- from _C++20 for Lazy Programmers_
#include <cstdio> // for srand, rand
#include <ctime> // for time
#include "io.h" // for bool getAnswerYorN (const char[]);
#include "montana.h"
int main ()
{
srand ((unsigned int) time (nullptr)); // start rand# generator
Montana montanaGame;
do
montanaGame.play ();
while (getYorNAnswer ("Play again (Y/N)? "));
return 0;
}
Example 19-7montana_main.cpp: a game of Montana
每次被调用时,Montana::play创建一个新的Deck和Waste。你可以从中看出它们是如何被使用的。Montana::makeMove显示了如何使用Cell(cellAt返回给定位置的Cell)。
Montana::makeLegalMove使用 try-catch 块以防输入出错。
// class Montana, for a game of Montana solitaire
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include "deck.h"
#include "io.h" // for bool getAnswerYorN (const char[]);
#include "montana.h"
using namespace std;
// Playing the game
...
void Montana::play ()
{
Deck deck;
Waste waste;
bool isVictory = false;
resetGrid (); // prepare for deal by ensuring grid is empty
for (int turn = 1; turn <= MAX_TURNS && ! isVictory; ++turn)
{
cout << "********************* New turn! "
"**********************\n";
// To easily test the detectVictory func: uncomment // setupForVictory,
// comment out deal, and see if isVictory becomes true
// setupForVictory(grid_, deck, waste);
deck.shuffle (); // Shuffle deck
deal (deck, waste); // fill grid with cards
// and remove aces
identifyEmptyCells (); // remember where the aces were
// in a list of 4 emptyCells_
bool letsQuitOrEndTurn = false;
isVictory = detectVictory(); // already won? Unlikely, but...
while (! isVictory && ! letsQuitOrEndTurn)
{
display();
makeLegalMove (letsQuitOrEndTurn); // play a turn
isVictory=detectVictory(); // did we win?
}
cleanup (deck, waste); // collect cards for redeal
// If user won, we go on and leave loop
// If we're out of turns, we go on and leave loop
// Otherwise give user a chance to quit
if (!isVictory && turn < MAX_TURNS)
if (getYorNAnswer("Quit game (Y/N)?"))
break;
}
congratulationsOrCondolences (isVictory);
}
void Montana::makeMove (const GridLoc& oldLoc,
const GridLoc& newLoc)
{
cellAt(newLoc).addCardLegally (cellAt(oldLoc).removeTop ());
}
void Montana::makeLegalMove (bool& letsQuitOrEndTurn)
{
bool isValidMove = false;
do
{
cout << "Move (e.g. A 1 5 to fill cell A with "
<< "the card at row 1, col 5; q to quit/end turn)? ";
// Which empty space will we fill -- or are we quitting?
char letter; cin >> letter;
if (toupper(letter) == 'Q') letsQuitOrEndTurn = true;
else
{
int emptyCellIndex = toupper(letter) - 'A';
try
{
// Which cell are we moving from?
GridLoc from; cin >> from;
// Which cell are we moving to?
GridLoc to = emptyCells_[emptyCellIndex];
// If the empty cell exists, and is really empty...
if (inRange (emptyCellIndex) && cellAt(to).isEmpty())
// if card to move exists, and move is legal...
if (!cellAt(from).isEmpty() &&
canGoHere (cellAt(from).top(), to))
{
isValidMove = true;
makeMove(from, to);
emptyCells_[emptyCellIndex] = from;
}
}
catch (const BadInput&) {}
catch (const OutOfRange&) {}
// reading GridLoc went bad -- just try again
}
} while (! isValidMove && ! letsQuitOrEndTurn);
}
Example 19-9Part of montana.cpp (the rest is in the book’s source code)
下面是一个示例游戏的一部分,玩家将黑桃 2 和 3 移到最下面一行,并在顶部为更多的低牌腾出空间。看起来玩家可能会赢:
********************* New turn! **********************
0 1 2 3 4 5 6 7 8 9 10 11 12
0: JC 2H >A< 7C 10S 5C QS KC 5S KH 8D 7D 4D
1: >B< 6C JS 8C 8H >C< 6H KS 9C JH QD 2D 4C
2: 9S 6S 5H KD 2S 3C JD 6D 10C 3S 7S 10D 5D
3: >D< 3H 8S QH 3D 7H 4H 4S 2C 9H 9D 10H QC
Move (e.g. A 1 5 to fill cell A with card at row 1, col 5; q to quit/end turn)? a 3 1
0 1 2 3 4 5 6 7 8 9 10 11 12
0: JC 2H 3H 7C 10S 5C QS KC 5S KH 8D 7D 4D
1: >B< 6C JS 8C 8H >C< 6H KS 9C JH QD 2D 4C
2: 9S 6S 5H KD 2S 3C JD 6D 10C 3S 7S 10D 5D
3: >D< >A< 8S QH 3D 7H 4H 4S 2C 9H 9D 10H QC
Move (e.g. A 1 5 to fill cell A with card at row 1, col 5; q to quit/end turn)? d 2 4
0 1 2 3 4 5 6 7 8 9 10 11 12
0: JC 2H 3H 7C 10S 5C QS KC 5S KH 8D 7D 4D
1: >B< 6C JS 8C 8H >C< 6H KS 9C JH QD 2D 4C
2: 9S 6S 5H KD >D< 3C JD 6D 10C 3S 7S 10D 5D
3: 2S >A< 8S QH 3D 7H 4H 4S 2C 9H 9D 10H QC
Move (e.g. A 1 5 to fill cell A with card at row 1, col 5; q to quit/end turn)? a 2 9
0 1 2 3 4 5 6 7 8 9 10 11 12
0: JC 2H 3H 7C 10S 5C QS KC 5S KH 8D 7D 4D
1: >B< 6C JS 8C 8H >C< 6H KS 9C JH QD 2D 4C
2: 9S 6S 5H KD >D< 3C JD 6D 10C >A< 7S 10D 5D
3: 2S 3S 8S QH 3D 7H 4H 4S 2C 9H 9D 10H QC
Move (e.g. A 1 5 to fill cell A with card at row 1, col 5; q to quit/end turn)? a 0 0
0 1 2 3 4 5 6 7 8 9 10 11 12
0: >A< 2H 3H 7C 10S 5C QS KC 5S KH 8D 7D 4D
1: >B< 6C JS 8C 8H >C< 6H KS 9C JH QD 2D 4C
2: 9S 6S 5H KD >D< 3C JD 6D 10C JC 7S 10D 5D
3: 2S 3S 8S QH 3D 7H 4H 4S 2C 9H 9D 10H QC
Move (e.g. A 1 5 to fill cell A with card at row 1, col 5; q to quit/end turn)?
Exercises
-
编写前面的类
Date的子类,添加一个函数printInText。它不会以数字格式(例如 12/12/2012)打印Date,而是以您喜欢的使用 ASCII 的语言:12 de diciembre de 2012(西班牙语)、December 12, 2012(英语),或者您喜欢的任何语言。你会用什么样的传承? -
Write a subclass of
stringcalledUnixFilenamethat doesn’t allow spaces – it immediately replaces them with_’s. And it won’t let you interfere by changing the string’s individual letters:UnixFilename myFileName ("my file name"); // becomes "my.file.name" myFileName[2] = ' '; // forbidden你会用什么样的传承?
-
写一个
Reserve类。这是一组卡片。唯一合法的举动是从顶部拿走一张牌。它应该继承什么?接下来的四个练习对第二十一章的 Freecell 游戏练习特别有用:
-
为
Deck写洗牌算法,比源码里的快。 -
(需要 O 符号)…你的洗牌在 O 符号中的时间要求是多少?你能把它降低到 O(N)吗?
-
写一个
Foundation类。一个Foundation,记住,以ACE开头,穿西装上去,也可能是空的。如果调用方试图添加不合适的卡,则引发异常。 -
写
FreecellPile类。适当时抛出异常。 -
写
KlondikePile类。克朗代克牌堆类似于自由细胞牌堆,因为你只能通过交替颜色来添加一些东西,但它不同于其他牌堆,因为你可以添加这样的牌的序列,只要牌堆的顶部牌符合该标准。例如,如果克朗代克牌堆的顶部是一张黑王,你可以在上面放一个以红皇后开始的序列。您也可以从克朗代克牌堆中移除一系列牌,只要它是交替颜色的。所以显然你需要添加和删除序列。什么类是序列?你的 add-sequence 和 remove-sequence 函数应该属于哪个类?
此外,克朗代克堆底部有 0 张或更多面朝下的牌。您不能移动任何序列,包括面朝下的牌。如果您移除所有面朝上的牌,则可以露出(面朝下的)顶牌。
适当时抛出异常。
确保你理解规则的最好方法是玩游戏,但我绝不会鼓励任何人去寻找另一种在工作中浪费时间的方式。
-
(涉及,但不难)写一个围棋鱼的游戏(网上找规则)。
-
设计一个简单的计算器类,它可以有两个数字,并且可以完成四个基本函数+、-、*和/。
现在编写一个工程师计算器类,它完成所有这些工作,但也做一些其他有趣的事情(比如说,平方根和取幂)。你会用什么样的传承?*