15.10 术语表
最后更新于:2022-04-01 06:25:17
**有序集(ordered set)**:指一种数据结构,其中每个元素只出现一次,而且每个元素都有一个索引来标识它。
**流(stream)**:表示从一个位置到另一个位置的数据流或数据序列的数据结构。C++ 中流用来表示输入和输出。
**累加器(accumulator)**:循环中用于累加结果的变量,一般每次迭代过程会在该变量后添加或连接一些东西。
15.9 一个更合理的距离矩阵
最后更新于:2022-04-01 06:25:14
虽然这段代码可以工作,但它本可以组织的更好。既然我们已经写了一个原型,那么我们就处于评价其设计并改进之的有利位置了。
那现在的代码有些什么问题呢?
1. 我们提前不知道要创建多大的距离矩阵,所以我们选择了一个任意大的数字(50),然后创建了一个固定大小的矩阵。更好的方式是允许距离矩阵以类似Set的方式扩充,而apmatrix类的resize函数使之成为可能。
2. 矩阵中的数据没有很好的封装。我们不得不以城市名的集合与矩阵本身作为参数传给processLine,这样很不合适。再就是,因为我们没提供执行错误检查的访问函数,所以使用距离矩阵容易出错。有个好的想法是,将表示城市名的Set对象和表示距离的apmatrix对象组合到DistMatrix类中。
下面是DistMatrix类头文件大概形式的一个草稿:
~~~
class DistMatrix {
private:
Set cities;
apmatrix<int> distances;
public:
DistMatrix (int rows);
void add (const apstring& city1, const apstring& city2, int dist);
int distance (int i, int j) const;
int distance (const apstring& city1, const apstring& city2) const;
apstring cityName (int i) const;
int numCities () const;
void print ();
};
~~~
我们可以使用这个接口来简化main函数:
~~~
void main ()
{
apstring line;
ifstream infile ("distances");
DistMatrix distances (2);
while (true) {
getline (infile, line);
if (infile.eof()) break;
processLine (line, distances);
}
distances.print ();
}
~~~
也可以简化 processLine函数:
~~~
void processLine (const apstring& line, DistMatrix& distances)
{
char quote = ’\"’;
apvector<int> quoteIndex (4);
quoteIndex[0] = line.find (quote);
for (int i=1; i<4; i++) {
quoteIndex[i] = find (line, quote, quoteIndex[i-1]+1);
}
// 将行分割为子串
int len1 = quoteIndex[1] - quoteIndex[0] - 1;
apstring city1 = line.substr (quoteIndex[0]+1, len1);
int len2 = quoteIndex[3] - quoteIndex[2] - 1;
apstring city2 = line.substr (quoteIndex[2]+1, len2);
int len3 = line.length() - quoteIndex[2] - 1;
apstring distString = line.substr (quoteIndex[3]+1, len3);
int distance = convertToInt (distString);
// 将新数据添加到距离矩阵中
distances.add (city1, city2, distance);
}
~~~
我把实现DistMatrix类的成员函数留作练习请读者完成。
15.8 距离矩阵
最后更新于:2022-04-01 06:25:12
最后,我们准备把数据从文件读入一个矩阵中。具体来说,每个城市在该矩阵中都一个相应的行和列。
我们将在main函数中创建该矩阵,它会剩余大量空间:
~~~
apmatrix<int> distances (50, 50, 0);
~~~
在processLine内部,我们从Set中得到两个城市的索引,并以这两个索引为矩阵的索引,向矩阵中添加了新信息:
~~~
int dist = convertToInt (distString);
int index1 = cities.add (city1);
int index2 = cities.add (city2);
distances[index1][index2] = distance;
distances[index2][index1] = distance;
~~~
最后,在main函数中我们可以将信息以可读的形式打印出来:
~~~
for (int i=0; i<cities.getNumElements(); i++) {
cout << cities.getElement(i) << "\t";
for (int j=0; j<=i; j++) {
cout << distances[i][j] << "\t";
}
cout << endl;
}
cout << "\t";
for (int i=0; i<cities.getNumElements(); i++) {
cout << cities.getElement(i) << "\t";
}
cout << endl;
~~~
这段代码的输出就是本章开头的矩阵。原始数据可以从本书网站获取。
15.7 apmatrix类
最后更新于:2022-04-01 06:25:10
apmatrix是二维的,除此之外它与apvector很像。不同于向量的长度,apmatrix有两个维度,称为numrows和numcols,分别表示“行数”和“列数”。
矩阵中的每个元素用两个索引来识别,其中一个是行编号,另一个是列编号。
要创建一个矩阵,有四种可选的构造函数:
~~~
apmatrix<char> m1;
apmatrix<int> m2 (3, 4);
apmatrix<double> m3 (rows, cols, 0.0);
apmatrix<double> m4 (m3);
~~~
第一个构造函数什么都没做,它创建的矩阵行数和列数都是0。第二个有两个整型数类型的参数,依次是行数和列数的初始值。第三个构造函数添加了一个参数用于初始化矩阵的元素,其余与第二个相同。第四个是复制构造函数,它以另一个apmatrix对象为参数。
就像apvectors一样,我们可以创建任何类型的apmatrix对象 (包括apvector,甚至apmatrix等类型)。
要访问矩阵的元素,我们使用[]操作符来指定行和列的信息:
~~~
m2[0][0] = 1;
m3[1][2] = 10.0 * m2[0][0];
~~~
如果我们想尝试访问范围之外的元素,程序会打印错误信息并退出。
numrows和numcols两个函数分别获取矩阵的行数和列数。记住,行索引是0到numrows() -1之间的数,而列索引是0和numcols() -1之间的数。
常用嵌套循环来遍历矩阵。下面循环将矩阵中每个元素的值设置为其行索引和列索引的和:
~~~
for (int row=0; row < m2.numrows(); row++) {
for (int col=0; col < m2.numcols(); col++) {
m2[row][col] = row + col;
}
}
~~~
循环打印时,矩阵每一行的元素使用制表符分隔,列之间以换行符分隔:
~~~
for (int row=0; row < m2.numrows(); row++) {
for (int col=0; col < m2.numcols(); col++) {
cout << m2[row][col] << "\t";
}
cout << endl;
}
~~~
15.6 集合数据结构Set
最后更新于:2022-04-01 06:25:08
数据结构是一个容器,用于将一些数据组织到单个对象中。我们已经见过了几个数据结构,比如apstring是一些字符组成,而apvector是一组相同类型(可以是任意数据类型)的元素组成。
有序集是由一些项组成的集合,它有两个决定性的属性:
**有序性**:集合中的元素都有一个相应的索引。我们可以通过这些索引确定集合中的元素。
**唯一性**:集合中每个元素只能出现一次。向集合中添加一个已经存在的元素是没有效果的。
此外,我们实现的有序集还有下面一个属性:
**大小任意**:随着我们向集合中添加元素,它会扩充以容纳新元素。apstring和apvector都是有序的;每个元素都有一个索引,我们可以通过索引来确定元素。但是我们见到的数据结构都不具有唯一性和大小任意这两个属性。
要满足唯一性,我们编写的add函数必须先查找以确定集合中是否存在要添加的元素。集合随着添加元素扩张这一特点可以利用apvector的resize函数实现。
下面是Set类定义的开始部分:
~~~
class Set {
private:
apvector<apstring> elements;
int numElements;
public:
Set (int n);
int getNumElements () const;
apstring getElement (int i) const;
int find (const apstring& s) const;
int add (const apstring& s);
};
Set::Set (int n)
{
apvector<apstring> temp (n);
elements = temp;
numElements = 0;
}
~~~
实例变量包括字符串的向量和记录集合中有多少元素的整型数。一定要记住集合中的元素数numElement与apvector的大小不是一个东西。通常前者会小一些。
Set的构造函数接受一个参数,该参数是apvector的初始大小。元素个数初始值总是0。
getNumElements和getElement是私有实例变量的访问函数。 numElements是只读变量,所以我们只提供了get函数而没有提供set函数。
~~~
int Set::getNumElements () const
{
return numElements;
}
~~~
为什么我们必须阻止客户程序修改getNumElements呢? 因为这是该类型的不变式,客户程序怎么能破坏不变式呢。我们看下Set其余的成员函数,看你能否说服自己它们都维护了不变式。
当我们使用[]操作符访问apvector时,它会检查并确认索引值大于等于0且小于向量的长度。不过要访问集合的元素,我们需要更强的条件验证。index必须小于元素数,元素数可能是个比向量长度小的值。
~~~
apstring Set::getElement (int i) const
{
if (i < numElements) {
return elements[i];
} else {
cout << "Set index out of range." << endl;
exit (1);
}
}
~~~
如果getElement得到的索引值超出了范围,它会打印错误信息(我承认,并不是最有用的信息)后退出。 find和add是比较有趣的函数。到目前为止,遍历和查找的模式还是老样子:
~~~
int Set::find (const apstring& s) const
{
for (int i=0; i<numElements; i++) {
if (elements[i] == s) return i;
}
return -1;
}
~~~
现在就剩下add了。像add这样的函数的返回类型一般是void,不过在这个例子中,更有意义的可能是返回元素的索引。
~~~
int Set::add (const apstring& s)
{
// 如果元素已经在集合中,返回其索引
int index = find (s);
if (index != -1) return index;
// 如果apvector满了,将它的大小调整为原来的2倍
if (numElements == elements.length()) {
elements.resize (elements.length() * 2);
}
// 添加新元素并返回其索引
index = numElements;
elements[index] = s;
numElements++;
return index;
}
~~~
这里有个技巧,numElements会以两种方式使用:其一就是表示集合中的元素数目,其二是用作下一个要添加的元素的索引。
可能要花点时间才能相信这行得通,但是考虑一下:当元素数目为0时,下一个要加入元素的索引也是0。当元素数目等于向量的长度时,这就说明向量已经满了,要加入新元素必须先通过resize分配更多的空间。
下面是一个Set对象的状态图,该对象初始包含2个元素的空间: ![enter image description here](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-02_55e688db32326.jpg)
现在我们可以使用Set类来记录在文件中找到的城市。在main函数中我们以2为初始大小创建Set对象:
~~~
Set cities (2);
~~~
然后在processLine函数中我们把两个城市添加到Set中,并保存返回的索引值。
~~~
int index1 = cities.add (city1);
int index2 = cities.add (city2);
~~~
我修改了processLine函数,使它以城市对象为第二个参数。
15.5 解析数字
最后更新于:2022-04-01 06:25:05
下一个任务是把文件中的数字从字符串形式转换为整型数。在书写较大的数字时,人们往往会用逗号将数字分组,如1,750。而计算机处理数字时,绝大部分情况是不包括逗号的,而且内置的读取数字的函数通常不能处理逗号。这就增加了转换的困难,不过也给了我们一个机会来编写去掉逗号的函数,所以这也没什么。去掉逗号之后,我们就可以使用库函数atoi将字符串转换为整型数了。atoi在头文件stdlib.h中定义。
要去掉逗号,一个选择就是遍历字符串,检查每个字符是否是数字。如果是的话,我们就将其加入结果字符串中。在循环结束时,原始字符串中的所有数字就都按顺序包含到结果字符串中了。
~~~
int convertToInt (const apstring& s)
{
apstring digitString = "";
for (int i=0; i<s.length(); i++) {
if (isdigit (s[i])) {
digitString += s[i];
}
}
return atoi (digitString.c_str());
}
~~~
变量digitString是**累加器**的一个例子。累加器和我们在第7.9节见过的计数器比较相似,不过计数器是不断地增加值,而累加器是每次以字符串连接的方式增加一个字符。
表达式
~~~
digitString += s[i];
~~~
等价于表达式
~~~
digitString = digitString + s[i];
~~~
两条语句都是在现有字符串末尾添加一个字符。
因为atoi以一个C字符串作为参数,所以我们必须先把digitString转化为C字符串,然后才能将其作为atoi的参数。
15.4 解析输入
最后更新于:2022-04-01 06:25:03
在1.4节,我们把“解析”定义为分析自然语言句子或形式语言语句之结构的过程。比如,编译器在将代码翻译成机器语言程序之前必须先进行解析。
此外,当你从文件或键盘读取输入时,一般也需要进行解析,以提取想要的信息并发现错误。
例如,我有一个文件distances,其中包含了美国主要城市之间的距离信息。这些信息是我从一个随机选择的网页([http://www.jaring.my/usiskl/usa/distance.html](http://www.jaring.my/usiskl/usa/distance.html))中得到的,所以数据可能不是很准确,不过这也没什么关系。文件格式看起来是这样的:
~~~
"Atlanta" "Chicago" 700
"Atlanta" "Boston" 1100
"Atlanta" "Chicago" 700
"Atlanta" "Dallas" 800
"Atlanta" "Denver" 1450
"Atlanta" "Detroit" 750
"Atlanta" "Orlando" 400
~~~
文件中的每一行包含了两个城市的名字以及它们之间的距离,其中城市名用引号标记,距离以英里为单位。引号是有用的,因为它能让我们很容易地处理多于一个单词的城市名,如”San Francisco“(旧金山)。
通过搜索一行输入中的引号,我们能找到每个城市在该行的开始和结束位置。不过查找引号这样的特殊字符可能让人有点困惑,因为引号是C++中用于标识字符串的特殊字符。
要找到引号第一次出现的位置,应该这样写:
~~~
int index = line.find (’\"’);
~~~
参数看起来有点乱,不过它就是表示双引号字符。最外层的单引号依然用于表示这是个字符值。反斜杠(\)说明我们想使用下一个字符的字面意义。 所以序列 \" 表示双引号,而序列 \’表示单引号。有趣的是, 序列\\表示一个反斜杠。第一个反斜杠指示我们要认真对待第二个反斜杠。
解析输入行由这几部分组成:找到每个城市名在该行中的开始和结束位置,使用substr函数提取城市和距离信息。substr是apstring的成员函数之一,它有两个参数,分别是子串的起始位置和长度。
~~~
void processLine (const apstring& line)
{
// 我们要查找的字符是引号
char quote = ’\"’;
// 将引号的索引保存在一个向量中
apvector<int> quoteIndex (4);
// 使用内置的find函数查找到第一个引号
quoteIndex[0] = line.find (quote);
// 使用第7章定义的find函数查找其他引号
for (int i=1; i<4; i++) {
quoteIndex[i] = find (line, quote, quoteIndex[i-1]+1);
}
// 将一行的内容分割成子串
int len1 = quoteIndex[1] - quoteIndex[0] - 1;
apstring city1 = line.substr (quoteIndex[0]+1, len1);
int len2 = quoteIndex[3] - quoteIndex[2] - 1;
apstring city2 = line.substr (quoteIndex[2]+1, len2);
int len3 = line.length() - quoteIndex[2] - 1;
apstring distString = line.substr (quoteIndex[3]+1, len3);
// 输出提取的信息
cout << city1 << "\t" << city2 << "\t" << distString << endl;
}
~~~
当然,我们真正想要的并不仅仅是提取并显示信息,不过这是一个好的开始。
15.3 文件输出
最后更新于:2022-04-01 06:25:01
将输出发送到文件的方法与处理输入类似。例如,我们可以修改前面的程序以实现将一个文件逐行复制到另一个文件的功能。
~~~
ifstream infile ("input-file");
ofstream outfile ("output-file");
if (infile.good() == false || outfile.good() == false) {
cout << "Unable to open one of the files." << endl;
exit (1);
}
while (true) {
getline (infile, line);
if (infile.eof()) break;
outfile << line << endl;
}
~~~
15.2 文件输入
最后更新于:2022-04-01 06:24:58
为了从文件获取数据,必须创建一个从文件到程序的流对象。这点我们可以利用ifstream的构造函数实现:
~~~
ifstream infile ("file-name");
~~~
该构造函数的参数是一个字符串,即你要打开的文件的名字。其结果是创建了infile对象,它支持所有 cin上可以执行的操作,包括>>和getline。
~~~
int x;
apstring line;
infile >> x; // 读取一个整型数并保存到x中
getline (infile, line); // 读取整行并保存到line中
~~~
如果我们提前知道文件中有多少数据,那就可以直接写一个循环来读取整个文件,然后再停止。然而更常见的情况是,我们想读取整个文件,但是不知道其大小。
ifstream有几个用以检查输入流状态的成员函数,它们是good、eof、fail和bad等。我们使用good函数来确保文件成功打开,而使用eof函数来探测”文件尾“。
无论什么时候从输入流读取数据,直到检查时你才能知道尝试是否成功。如果eof函数的返回值为true,那说明已经到达文件尾,我们就知道最后一次读取尝试以失败告终。下面程序代码的功能是:读取一个文件的每一行并将其输出到屏幕上。
~~~
apstring fileName = ...;
ifstream infile (fileName.c_str());
if (infile.good() == false) {
cout << "Unable to open the file named " << fileName;
exit (1);
}
while (true) {
getline (infile, line);
if (infile.eof()) break;
cout << line << endl;
}
~~~
函数c_str把apstring转换为原生C字符串。因为ifstream构造函数期望的参数是C字符串,所以apstring必须转换一下。
我们可以在打开文件之后,立即调用good函数。如果系统无法打开文件,该函数就返回false,原因很可能是文件不存在或者你没有文件读取权限。
while(true)是无穷循环的习惯写法。通常循环中某处会有个break语句,这样程序就不会真的永远运行下去(不过有的程序的确是希望永远执行)。这个例子中,break语句允许只要发现文件尾就退出循环。
退出循环操作放在输入语句和输出语句之间很重要,这样getline在遇到文件尾失败之后,我们就不会在line中输出无效信息。
15.1 流
最后更新于:2022-04-01 06:24:56
为了从文件获取输入,或者将输出发送到文件,你需要创建ifstream对象(对应输入文件)或ofstream对象(对应输出文件)。这些类型在头文件fstream.h中定义,使用时必须包含该文件。
**流**是一个抽象对象,它表示从源(如键盘或文件)到目标(如屏幕或文件)的数据流动。
我们已经用过了两个流:cin和cout,其类型分别是istream和ostream。cin代表从键盘到程序的数据流。每次程序使用>>操作符或getline函数时会将数据从输入流中取走。
类似的,程序在ostream上使用<<操作符时,它将数据添加到输出流。
第15章 文件输入/输出与apmatrix类
最后更新于:2022-04-01 06:24:54
本章我们会开发一个程序,它能读写文件、解析输入并说明apmatrix类的用法。我们还会实现集合数据结构Set,它会随着添加元素自动扩充。除了说明这些特性,程序的真正目标是生成一个表示美国一些主要城市间距离的二维表。输出是这样的一个表格:
~~~
Atlanta 0
Chicago 700 0
Boston 1100 1000 0
Dallas 800 900 1750 0
Denver 1450 1000 2000 800 0
Detroit 750 300 800 1150 1300 0
Orlando 400 1150 1300 1100 1900 1200 0
Phoenix 1850 1750 2650 1000 800 2000 2100 0
Seattle 2650 2000 3000 2150 1350 2300 3100 1450 0
Atlanta Chicago Boston Dallas Denver Detroit Orlando Phoenix Seattle
~~~
因为一个城市到自己的距离是0,所以对角线元素全是0。而且,因为从A到B的距离与从B到A的距离相同,因而矩阵的上半部分没必要打印。
14.11 术语表
最后更新于:2022-04-01 06:24:51
类(class):通常来说,类即带成员函数的用户自定义类型。在C++中一个类即为带私有变量的结构体。
访问函数(accessor function):提供对私有变量的访问(读或写)功能的函数。
不变式(invariant):一个条件,跟一个对象相关,并应该在客户代码中一直为真,该不变性应被成员函数保持。
先决条件(precondition):在某一个函数开始假定为真的条件。如果先决条件为假,函数可能不能正常运行。最好尽可能的检查先决条件。
后置条件(postcondition):在函数执行末尾为真的条件。
14.10 私有函数
最后更新于:2022-04-01 06:24:49
在很多时候,有些成员函数是在一个类内部才会被调用到,他们不应当被使用这个类的客户代码调用。例如,calculatePolar和calculateCartesianare会被访问函数调用到,但客户代码不应该直接调用他们(虽然不会造成伤害)。如果我们想保护这些函数不被调用到,我们就需要把他们声明为private,正如我们处理变量那样。所以一个完整的复数类的定义如下:
~~~
class Complex
{
private:
double real, imag;
double mag, theta;
bool cartesian, polar;
void calculateCartesian ();
void calculatePolar ();
public:
Complex () { cartesian = false; polar = false; }
Complex (double r, double i)
{
real = r; imag = i;
cartesian = true; polar = false;
}
void printCartesian ();
void printPolar ();
double getReal ();
double getImag ();
double getMag ();
double getTheta ();
void setCartesian (double r, double i);
void setPolar (double m, double t);
};
~~~
开头的private标号不是必须的,但它是一个有用的提示符。
14.9 先决条件
最后更新于:2022-04-01 06:24:47
通常当你写函数时会对接收的参数做了隐含的假设。如果这些假设成立,程序没有问题;如果假设不成立,你的程序可能就会崩溃了。
为了让你的程序更为健壮,将你的假设明确,以程序文档的方式写下来或写代码来进行检查。
比如我们观察calculateCartesian方法。是否存在对当前对象进行了假设呢?没错,我们假设极坐标系的标志量已经设置了并且mag和theta的值是有效的。如果假设不成立,那么这个函数的结果无意义。
一种做法是对函数添加注释说明先决条件以警告他人。
~~~
void Complex::calculateCartesian ()
// 先决条件:当前对象包含有效的极坐标值,其极坐标标志量已设定。
// 后置条件:当前对象包含有效的笛卡尔坐标系和极坐标系的值,两个标志量皆已设置。
{
real = mag * cos (theta);
imag = mag * sin (theta);
cartesian = true;
}
~~~
同时,我添加了后置条件,即我们认为函数执行完毕后为真的事情。
这些注释对于阅读你代码的人很有用,但更好的办法是通过代码来检查先决条件,并输出合适的错误信息:
~~~
void Complex::calculateCartesian ()
{
if (polar == false) {
cout << "calculateCartesian failed because the polar representation is invalid" << endl;
exit (1);
}
real = mag * cos (theta);
imag = mag * sin (theta);
cartesian = true;
}
~~~
exit函数会使程序很快的退出执行。返回值是一个错误码以告诉系统(或该程序执行者)某些错误发生。
这种错误检测方式很是常见,于是C++提供了一个内置函数来检查先决条件并打印错误信息。如果你包含了assert.h头文件,你可以使用一个以布尔值或条件表达式为参数的assert函数。只要参数为真,assert函数就啥也不做。如果参数为假,assert打印一个错误信息并退出,用法如下:
~~~
void Complex::calculateCartesian ()
{
assert (polar);
real = mag * cos (theta);
imag = mag * sin (theta);
cartesian = true;
assert (polar && cartesian);
}
~~~
第一句assert检查先决条件(事实上只是一部分先决条件),第二句assert检查后置条件。
在我的开发环境中,当一个断言失败时会得到以下信息:
~~~
Complex.cpp:63: void Complex::calculatePolar(): Assertion ‘cartesian’ failed.
Abort
~~~
信息中会有许多内容可以帮助我跟踪错误,包括文件名和断言失败的出错行,断言语句的内容和所在函数名。
14.8 不变式
最后更新于:2022-04-01 06:24:44
对于一个复数对象,有些条件我们期望是真的。
举例来说,如果笛卡尔坐标系的标志量被设置了,那么我们就期望real和imag的值是有效的,类似地,如果极坐标系的标志量被设置了,我们期望mag和theta也是有效的。最后,如果两个标志位都设置了的话,我们希望四个值是一致的,即他们应该是以不同的表示方式表示相同的一个复数。
这样的条件即为不变式,由于很显而易见的原因他们是不变的——他们总是应该为真。编写几乎没有bug的高质量代码就是要指出你的类中那些是不变式,并让改变他们成为不可能。
数据封装的好处之一就是帮助保证不变式。第一部是通过将变量变为私有从而阻止不受约束的访问。然后更改对象的唯一办法就是通过访问函数和更改器了。如果我们检查所所有的访问函数和更改器并发现他们都能保证不变式不变,那么我们就可以证明一个不变式是不会被篡改的了。
在Complex 类中,我们列出对变量进行赋值的函数:
第二个构造函数
calculateCartesian
calculatePolar
setCartesian
setPolar
在每一个函数中,很明显他们能保持上面提到的不变性。这里我们需要小心一些。注意到我说的是“保持”不变性。这就意味着,如果这个不变式为真,那么当这个函数被调用后仍然为真。
这样的定义允许了两处漏洞。首先在函数执行过程中可能有不变式为假的情况,这是没关系的,有些时候是不可避免的。只要不变式在函数执行之后恢复即可。
另外一处漏洞是如果在函数执行开始时不变式为真我们只需保持该不变性即可。如果开始执行时部位真,所有都是徒劳了。如果不变式在某处被更改了,我们能做的一般来说就是检测到出错,打印错误信息,然后退出。
14.7 复数相关函数(二)
最后更新于:2022-04-01 06:24:42
另外一个我们需要的操作则是乘法。不像加法那样,乘法在极坐标系中容易,在笛卡尔坐标系中麻烦些(是相对有点麻烦而已)。
在极坐标系,我们只需将模相乘,角度相加。像往常那样,我们使用访问函数来实现而不必关心对象的表现形式。
~~~
Complex mult (Complex& a, Complex& b)
{
double mag = a.getMag() * b.getMag()
double theta = a.getTheta() + b.getTheta();
Complex product;
product.setPolar (mag, theta);
return product;
}
~~~
这儿我们遇到一个小问题,即我们没有一个构造函数来接收极坐标系的值。添加这样的一个构造函数也可以,但是要记得只有在参数不同时才能重载一个函数(包括构造函数)。在这个例子中,我们要添加的构造函数仍然是接收两个浮点型的参数,所以没法重载。
另外一个办法是提供一个访问函数来设置变量的值,为使操作正常进行,我们需要确保当mag与theta的值被设定时,极坐标的标志位也要设置为真,同时还要确保笛卡尔坐标系的标志位设置为假。这是因为,如果我们手动设置了极坐标的值,笛卡尔坐标系的值就会失效。
~~~
void Complex::setPolar (double m, double t)
{
mag = m; theta = t;
cartesian = false; polar = true;
}
~~~
作为练习,请写出对应的函数setCartesian。
为测试mult函数,我们可以这样做:
Complex c1 (2.0, 3.0);
Complex c2 (3.0, 4.0);
Complex product = mult (c1, c2);
product.printCartesian(); 该程序的输出结果为:
-6 + 17i
在这个输出的背后进行了很多转换。当我们调用mult时,两个参数为被转换为极坐标系的表示形式。结果也是极坐标形式,当我们调用printCartesian时,就会再转换回笛卡尔坐标系的形式。没错,我们就这样得到了正确结果,很奇妙吧。
14.6 复数相关函数(一)
最后更新于:2022-04-01 06:24:40
对复数做加法是一个很常见的操作。复数在笛卡尔坐标系上的加法是很简单的,只需对实部虚部分别相加即可。如果在极坐标系中进行加法,最简单的方式则是将复数转换到笛卡尔坐标系中再进行相加。
于是,使用访问函数就可以很容易的做到:
~~~
Complex add (Complex& a, Complex& b)
{
double real = a.getReal() + b.getReal();
double imag = a.getImag() + b.getImag();
Complex sum (real, imag);
return sum;
}
~~~
注意add函数的参数不是常量,因为我们在使用访问函数时可能更改他们。调用add函数,需要传递两个参数,如:
Complex c1 (2.0, 3.0);
Complex c2 (3.0, 4.0);
Complex sum = add (c1, c2);
sum.printCartesian();
该程序的输出结果为:
5 + 7i
14.5 输出
最后更新于:2022-04-01 06:24:37
我们定义了一个新的类通常会想将其对象以可读的形式输出出来。对于复数对象,我们使用这样两个函数:
~~~
void Complex::printCartesian ()
{
cout << getReal() << " + " << getImag() << "i" << endl;
}
void Complex::printPolar ()
{
cout << getMag() << " e^ " << getTheta() << "i" << endl;
}
~~~
在此我们不必担心不同象限的表达方式就可以输出任何复数对象。因为两个输出函数使用了访问函数,程序会自动计算需要的值。
以下代码使用第二个构造函数来创建一个复数对象,他只是是以笛卡尔坐标系的形式。 当我们调用到printCartesian时,不必做任何转换即可直接访问real 和imag。
Complex c1 (2.0, 3.0);
c1.printCartesian();
c1.printPolar();
当我们调用到printPolar,时,后者会调用getMag,程序会进行极坐标系转换并将结果保存到变量中。这种转换只需一次。当printPolar调用getTheta时,就会看到极坐标系的数值已经是有效的了,直接返回即可。
以上代码的输出为:
2 + 3i
3.60555 e^ 0.982794i我们定义了一个新的类通常会想将其对象以可读的形式输出出来。对于复数对象,我们使用这样两个函数:
~~~
void Complex::printCartesian ()
{
cout << getReal() << " + " << getImag() << "i" << endl;
}
void Complex::printPolar ()
{
cout << getMag() << " e^ " << getTheta() << "i" << endl;
}
~~~
在此我们不必担心不同象限的表达方式就可以输出任何复数对象。因为两个输出函数使用了访问函数,程序会自动计算需要的值。
以下代码使用第二个构造函数来创建一个复数对象,他只是是以笛卡尔坐标系的形式。 当我们调用到printCartesian时,不必做任何转换即可直接访问real 和imag。
Complex c1 (2.0, 3.0);
c1.printCartesian();
c1.printPolar();
当我们调用到printPolar,时,后者会调用getMag,程序会进行极坐标系转换并将结果保存到变量中。这种转换只需一次。当printPolar调用getTheta时,就会看到极坐标系的数值已经是有效的了,直接返回即可。
以上代码的输出为:
2 + 3i
3.60555 e^ 0.982794i
14.4 访问函数(Accessor functions)
最后更新于:2022-04-01 06:24:35
按照惯例,访问函数以这样的方式命名:get + 变量的名字。返回值类型通常是对应的变量的类型。 在这个例子中,访问函数可以让我们在得到某个值前确保该值是有效的。函数getReal如下:
~~~
double Complex::getReal ()
{
if (cartesian == false) calculateCartesian ();
return real;
}
~~~
如果笛卡尔坐标系的标志位为真,那么real变量中包含着有效的数据,我们在getReal中将其返回即可。为假,我们就需要调用calculateCartesian从极坐标系转化到笛卡尔坐标系。
~~~
void Complex::calculateCartesian ()
{
real = mag * cos (theta);
imag = mag * sin (theta);
cartesian = true;
}
~~~
假设极坐标系的值是有效的,我们就可以使用前一部分提到的公式来转换到笛卡尔坐标系。然后我们设置笛卡尔坐标系的标志位,表明现在的real和imag的值已有效。
作为练习,写一个对应于calculateCartesian的一个calculatePolar和对应的getMag 及 getTheta方法。关于访问函数一个特殊的地方在于他们不是常量,因为调用访问函数可能需要更改对应的变量。
14.3 复数
最后更新于:2022-04-01 06:24:33
本章后面的部分讲述复数这样一个例子。复数在数学和工程领域很有用途,许多计算用到了复数。一个复数是实部和虚部之和,记作x+yi,x为实部,y为虚部,i是-1的平方根。
以下为类Complex的定义:
~~~
class Complex
{
double real, imag;
public:
Complex () { }
Complex (double r, double i) { real = r; imag = i; }
};
~~~
在类的定义中,实部和虚部是私有的,构造函数是公有的,故加上public标号。
一般使用这样两个构造函数:一个没有参数也不做什么工作的构造函数,另一个有两个参数来用来初始化变量。
到现在为止,还看不到将变量私有化的明显优点。让我们把程序复杂一点,就能看到了。
对于复数,通常会有另外一种表达方式叫做基于极坐标系的极坐标表示。跟用复数域上的点的特定位置表示实部虚部不同,极坐标系中用离开原点的距离(或模)和偏离原点的方向(或角度)来表示。
下图表示两个坐标系系统。
在极坐标系中,复数记作reiθ ,其中r是模(半径),θ是用弧度表示的角度。
幸运的是,很容易从两个坐标系中进行转换。
从笛卡尔到极坐标系:
r = x2 + y2
θ = arctan(y/x)
从极坐标系到笛卡尔坐标系:
x = r cos θ
y = r sin θ
那么我们应该使用哪一种表达方式呢?因为有些操作在笛卡尔坐标系中简单些,如加法;而另一些操作在极坐标系中简单些,如乘法。所以一个办法是我们写一个使用两种表达方式的类,让他们根据需要可以自动转换。
~~~
class Complex
{
double real, imag;
double mag, theta;
bool cartesian, polar;
public:
Complex () { cartesian = false; polar = false; }
Complex (double r, double i)
{
real = r; imag = i;
cartesian = true; polar = false;
}
};
~~~
在这个类中有6个变量,这就意味着这样会比之前的任何一种占用的空间都要多。不过我们很快就会看到这样做是很有用的。
其中四个变量可以根据名字判断他们的意思,分别是一个复数的实部,虚部,角度,半径。另外两个变量cartesian和polar则是表示对应坐标系的值是否有效的标志。
举例来说,啥都不做的这个构造函数将两个标志量设置为false表明该对象无论哪种表达方式,都还不是有效的复数。
第二个构造函数使用参数来初始化实部和虚部,但不会计算模或角度。并会把极坐标的标志位置为false来警告其他函数不应当访问模或角度值,直到他们被设置为正确的值。
现在应该清楚为何将变量置为私有了吧。如果一个客户程序被允许不受限制的访问,读取了未初始化的值就很容易导致出错。在下一部分,我们将添加一些访问函数来避免这种错误。