编译软件为 vs2015。
第七章
练习7.1:
使用 2.6.1 节练习定义的 Sales_data 类为 1.6 节(第21页)的交易处理程序编写一个新的版本。
解答:
(1)main.cpp:
#include <iostream>
#include "Sales_data.h"
int main()
{
Sales_data total;
if (std::cin >> total.Isbn >> total.units_sold >> total.price)
{
total.read(total);
Sales_data trans;
while (std::cin >> trans.Isbn >> trans.units_sold >> trans.price)
{
trans.read(trans);
if (total.Isbn == trans.Isbn)
{
total.combine(trans);
}
else
{
total.print();
total = trans;
}
}
total.print();
}
else
{
std::cerr << "No data?!" << std::endl;
return -1;
}
system("pause");
return 0;
}
(2)自定义的 Sales_data.h:
#ifndef _SALES_DATA_H_
#define _SALES_DATA_H_
#include <iostream>
#include <string>
using std::string;
class Sales_data
{
public:
string Isbn; //书号
unsigned units_sold = 0; //销售量
double revenue = 0.0; //总销售额
double price = 0.0; //销售单价
double aver_price = 0.0; //平均销售单价
void read(Sales_data item)
{
Isbn = item.Isbn;
units_sold = item.units_sold;
price = item.price;
aver_price = item.aver_price;
revenue = units_sold*price;
}
void combine(Sales_data item)
{
if (Isbn != item.Isbn)
{
std::cout << "the book are different!" << std::endl;
return;
}
else
{
units_sold += item.units_sold;
revenue += item.revenue;
}
}
void print()
{
aver_price = revenue / units_sold;
std::cout << "information of: " << Isbn << std::endl;
std::cout << "total units_sold: " << units_sold << std::endl;
std::cout << "total revenue: " << revenue << std::endl;
std::cout << "average price: " << aver_price << std::endl;
}
};
#endif //_SALES_DATA_H_
练习7.2:
曾在 2.6.2 节的练习(第 67 页)中编写了一个 Sales_data 类,请向这个类添加 combine 和 isbn 成员。
解答:
#ifndef _SALES_DATA_H_
#define _SALES_DATA_H_
#include <string>
using std::string;
struct Sales_data {
string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
#endif //_SALES_DATA_H_
练习7.3:
修改 7.1.1 节(第 229 页)的交易处理程序,令其使用这些成员。
解答:
Sales_data.h 文件见练习 7.2
#include "Sales_data.h"
#include <iostream>
using namespace std;
int main()
{
Sales_data total;
if (cin >> total.bookNo >> total.units_sold >> total.revenue)
{
Sales_data trans;
while (cin >> trans.bookNo >> trans.units_sold >> trans.revenue)
{
if (total.isbn() == trans.isbn())
{
total.combine(trans);
}
else
{
cout << "total: " << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
total = trans;
}
}
cout << "total: " << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
}
else
{
cerr << " No data?!" << endl;
return -1;
}
system("pause");
return 0;
}
练习7.4:
编写一个名为 person 的类,使其表示人员的姓名和住址。使用string对象存放这些元素,接下来的练习将不断充实这个类的其他特征。
解答:
#ifndef PERSON_H
#define PERSON_H
#include <string>
using std::string;
class Person
{
string name;
string address;
};
#endif // PERSON_H
练习7.5:
在你的 Person 类中提供一些操作使其能够返回姓名和住址。这些函数是否应该是 const 的呢?解释原因。
解答:
返回姓名和住址的函数应该是 const,首先只需要对内部的成员进行读取个调用,不需要写入,可以声明为 const 类型,其次,声明为 const 类型避免了通过函数修改相关参数的风险,更加安全。
#ifndef PERSON_H
#define PERSON_H
#include <string>
using std::string;
class Person
{
string name;
string address;
public:
const string& getName() const { return name; }
const string& getAddress() const { return address; }
};
#endif // PERSON_H
练习7.6:
对于函数 add、read 和 print,定义你自己的版本。
解答:
// Sales_data.h
#ifndef _SALES_DATA_H_
#define _SALES_DATA_H_
#include <string>
#include <iostream>
using namespace std;
struct Sales_data {
string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
double aver_price = 0.0;
};
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
aver_price = revenue / units_sold;
return *this;
}
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price*item.units_sold;
return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.bookNo << " " << item.units_sold << " "
<< item.revenue << " " << item.aver_price;
return os;
}
Sales_data &add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
#endif //_SALES_DATA_H_
练习7.7:
使用这些新函数重写 7.1.2 节(第 233页)练习中的交易处理程序。
解答:
Sales_data.h 将上一练习7.6的答案
main.cpp 如下:
#include "Sales_data.h"
int main()
{
Sales_data total;
if (read(cin, total))
{
Sales_data trans;
while (read(cin, trans))
{
if (total.isbn() == trans.isbn())
//total.combine(trans);
total = add(total, trans);
else
{
print(cout, total);
total = trans;
}
}
print(cout, total);
}
else
{
cerr << "No data?!" << endl;
}
}
练习7.8:
为什么 read 函数将其 Sales_data 参数定义成普通的引用,而 print 函数将其参数定义成常量引用?
解答:
因为 read 函数需要向 Sales_data 的参数中写入数据,所以不能定义为常量引用的形式;而 print 函数只需要读取 Sales_data 中的参数而不需要改变其值,定义为常量引用更安全。
练习7.9:
对于 7.1.2 节(第 233 页)练习中的代码,添加读取和打印 person 对象的操作。
解答:
#ifndef PERSON_H
#define PERSON_H
#include <string>
#include <iostream>
using namespace std;
class Person
{
public:
string name;
string address;
const string& getName() const { return name; }
const string& getAddress() const { return address; }
};
istream &read(istream &is, Person &per)
{
is>>per.name>>per.address;
return is;
}
ostream &print(ostream &os, Person &per)
{
os << per.name << " " << per.address;
return os;
}
#endif // PERSON_H
练习7.10:
在下面这条 if 语句中,条件部分的作用是什么?
if (read (read (cin, data1), data2))
解答:
if 语句的条件部分决定了其一次读取两个对象。
练习7.11:
在你的 Sales_data 类中添加构造函数,然后编写一段程序令其用到每个构造函数。
解答:
// Sales_data.h
#ifndef _SALES_DATA_H_
#define _SALES_DATA_H_
#include <string>
#include <iostream>
using namespace std;
struct Sales_data {
Sales_data() = default; //默认的构造函数
Sales_data(const string &s) :
bookNo(s)
{
}
Sales_data(const string &s, unsigned n, double p) :
bookNo(s),
units_sold(n),
revenue(p*n)
{
}
Sales_data(istream &);
string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
double aver_price = 0.0;
};
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
aver_price = revenue / units_sold;
return *this;
}
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price*item.units_sold;
return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.bookNo << " " << item.units_sold << " "
<< item.revenue << " " << item.aver_price;
return os;
}
Sales_data &add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
#endif //_SALES_DATA_H_
// main.cpp 调用 Sales_data 里的每个构造函数
#include "Sales_data.h"
int main()
{
Sales_data item1;
read(cin, item1);
print(cout, item1);
Sales_data item2("00-x-787345");
print(cout, item2);
Sales_data item3("00-x-78345", 4, 25.0);
print(cout, item3);
Sales_data item4(cin);
print(cout, item4);
}
练习7.12:
把只接受一个 istream 作为参数的构造函数定义转移到类的内部。
解答:
将构造函数的定义移到类的内部,最简答的方式就是在声明的后面加上大括号。
// Sales_data.h
#ifndef _SALES_DATA_H_
#define _SALES_DATA_H_
#include <string>
#include <iostream>
using namespace std;
struct Sales_data {
Sales_data() = default; //默认的构造函数
Sales_data(const string &s) :
bookNo(s)
{
}
Sales_data(const string &s, unsigned n, double p) :
bookNo(s),
units_sold(n),
revenue(p*n)
{
}
Sales_data(istream &is) { read(is, *this); };
string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
double aver_price = 0.0;
};
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
aver_price = revenue / units_sold;
return *this;
}
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price*item.units_sold;
return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.bookNo << " " << item.units_sold << " "
<< item.revenue << " " << item.aver_price;
return os;
}
Sales_data &add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
#endif //_SALES_DATA_H_
练习7.13:
使用 istream 构造函数重写第 229 页的程序。
解答:
其中 Sales_data.h 见上一练习7.12 的答案。
main.cpp 文件如下:
#include "Sales_data.h"
int main()
{
Sales_data total(cin);
if (!total.isbn().empty)
{
istream &is = cin;
while (is)
{
Sales_data trans(is);
if (total.isbn() == trans.isbn())
//total.combine(trans);
total = add(total, trans);
else
{
print(cout, total);
total = trans;
}
}
print(cout, total);
}
else
{
cerr << "No data?!" << endl;
}
}
练习7.14:
编写一个构造函数,令其使用我们提供的类内初始值显示地初始化成员。
解答:
// Sales_data.h
#ifndef _SALES_DATA_H_
#define _SALES_DATA_H_
#include <string>
#include <iostream>
using namespace std;
struct Sales_data {
Sales_data() = default; //默认的构造函数
Sales_data(const string &s) :
bookNo(s)
{
}
Sales_data(const string &s, unsigned n, double p) :
bookNo(s),
units_sold(n),
revenue(p*n)
{
}
Sales_data(istream &is) { read(is, *this); };
Sales_data() :
units_sold(0),
revenue(0) // 使用类内提供的初始值显示的初始化
{
}
string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
double aver_price = 0.0;
};
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
aver_price = revenue / units_sold;
return *this;
}
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price*item.units_sold;
return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.bookNo << " " << item.units_sold << " "
<< item.revenue << " " << item.aver_price;
return os;
}
Sales_data &add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
#endif //_SALES_DATA_H_
练习7.15:
为你的 Person 类添加正确的构造函数。
解答:
#ifndef PERSON_H
#define PERSON_H
#include <string>
#include <iostream>
using namespace std;
class Person
{
public:
Person() = default;
Person(const string on_name, const string on_address) :
name(on_name),
address(on_address)
{
}
Person(istream &is) { read(is, *this); };
string name;
string address;
const string& getName() const { return name; }
const string& getAddress() const { return address; }
};
istream &read(istream &is, Person &per)
{
is >> per.name >> per.address;
return is;
}
ostream &print(ostream &os, Person &per)
{
os << per.name << " " << per.address;
return os;
}
#endif // PERSON_H
练习7.16:
在类的定义中对于访问说明符出现的位置和次数有限定吗?如果有,是什么?什么样的成员应该定义在 public 说明符之后?什么样的成员应该定义在 private 说明符之后?
解答:
一个类可以包含 0 个或多个访问说明符,而且对于某个访问说明符能出现多少次也没有严格限定。每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或直到达到类的结尾处为止。
定义在 public 说明符之后的成员在整个程序内可被访问,public 成员定义类的接口。
定义在 private 说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private 部分封装了(既隐藏了)类的实现细节。
练习7.17:
使用 class 和 struct 时有区别吗?如果有,是什么?
解答:
使用 struct 和 class 定义类的唯一区别就是默认的访问权限。class 默认的是 private,struct 默认是 public。
练习7.18:
封装是何含义?它有什么用处?
解答:
封装就是实现隐藏了类的实现细节。
优点:
1. 确保用户代码不会无意间破坏封装对象的状态。
2. 被封装的具体实现细节可以随时改变,而无须调整用户级别的代码。
练习7.19:
在你的 Person 类中,你将把哪些成员声明为 public 的?哪些声明成 private 的?解释你这样做的原因。
解答:
由于就目前的 Person 的要求,类中的成员变量及函数都需要是 public 的,首先构造函数及 getnName() 和 getAddress() 是需要提供给用户使用的接口函数。其次 非成员函数 read 和 print 中需要用到 Person 类内的成员变量 name 和 address,所以需要声明成 public,后续会根据 Person 类的其他需求进行修改。
#ifndef PERSON_H
#define PERSON_H
#include <string>
#include <iostream>
using namespace std;
class Person
{
public:
Person() = default;
Person(const string on_name, const string on_address) :
name(on_name),
address(on_address)
{
}
Person(istream &is) { read(is, *this); };
const string& getName() const { return name; }
const string& getAddress() const { return address; }
string name;
string address;
};
istream &read(istream &is, Person &per)
{
is >> per.name >> per.address;
return is;
}
ostream &print(ostream &os, Person &per)
{
os << per.name << " " << per.address;
return os;
}
#endif // PERSON_H
练习7.20:
友元在什么时候有用?请分别列举出使用友元的利弊。
解答:
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend)。
优点:
可以访问类内的 private 成员。
可以在类的作用域内引用类成员而不需要显示地增加类名。
类的用户更容易阅读。
缺点:
减少了封装的内容,降低了可维护性。
减少的代码的简洁性,友元函数需要分别在类内类外单独声明。
练习7.21:
修改你的 Sales_data 类使其隐藏实现的细节。你之前编写的关于 Sales_data 操作的程序应该继续使用,借助类的新定义重新编译该程序,确保其工作正常。
解答:
// Sales_data.h
#ifndef _SALES_DATA_H_
#define _SALES_DATA_H_
#include <string>
#include <iostream>
using namespace std;
class Sales_data
{
friend istream &read(istream &is, Sales_data &item);
friend ostream &print(ostream &os, const Sales_data &item);
friend Sales_data add(const Sales_data &lhs, const Sales_data &rhs);
public:
Sales_data() = default; //默认的构造函数
Sales_data(const string &s) :
bookNo(s)
{
}
Sales_data(const string &s, unsigned n, double p) :
bookNo(s),
units_sold(n),
revenue(p*n)
{
}
Sales_data(istream &is) { read(is, *this); };
string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);
private:
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
double aver_price = 0.0;
};
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
aver_price = revenue / units_sold;
return *this;
}
static istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price*item.units_sold;
return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.bookNo << " " << item.units_sold << " "
<< item.revenue << " " << item.aver_price;
return os;
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
#endif //_SALES_DATA_H_
练习7.22:
修饰你的 Person 类使其隐藏实现的细节。
解答:
#ifndef PERSON_H
#define PERSON_H
#include <string>
#include <iostream>
using namespace std;
class Person
{
friend istream &read(istream &is, Person &per);
friend ostream &print(ostream &os, Person &per);
public:
Person() = default;
Person(const string on_name, const string on_address) :
name(on_name),
address(on_address)
{
}
Person(istream &is) { read(is, *this); };
const string& getName() const { return name; }
const string& getAddress() const { return address; }
private:
string name;
string address;
};
istream &read(istream &is, Person &per)
{
is >> per.name >> per.address;
return is;
}
ostream &print(ostream &os, Person &per)
{
os << per.name << " " << per.address;
return os;
}
#endif // PERSON_H
练习7.23:
编写你自己的 Screen 类。
解答:
#ifndef SCREEN_H
#define SCREEN_H
#include <string>
using namespace std;
class Screen
{
public:
typedef string::size_type pos;
Screen() = default;
Screen(pos ht, pos wd) :
height(ht),
width(wd),
contents(ht * wd, ' ')
{
}
Screen(pos ht, pos wd, char c) :
height(ht),
width(wd),
contents(ht * wd, c)
{
}
char get() const // 读取光标处的字符
{
return contents[cursor];
}
inline char get(pos ht, pos wd) const;
Screen &move(pos r, pos c);
private:
pos cursor = 0;
pos height = 0, width = 0;
string contents;
};
#endif //SCREEN_H
练习7.24:
给你的 Screen 类添加三个构造函数:一个默认构造函数;另一个构造函数接受宽和高的值。然后将 contents 初始化成给定数量的空白;第三个构造函数接受宽和高的值以及一个字符,该字符作为初始化之后屏幕的内容。
解答:
#ifndef SCREEN_H
#define SCREEN_H
#include <string>
using namespace std;
class Screen
{
public:
typedef string::size_type pos;
Screen() = default;
Screen(pos ht, pos wd) :
height(ht),
width(wd),
contents(ht * wd, ' ')
{
}
Screen(pos ht, pos wd, char c) :
height(ht),
width(wd),
contents(ht * wd, c)
{
}
char get() const // 读取光标处的字符
{
return contents[cursor];
}
inline char get(pos ht, pos wd) const;
Screen &move(pos r, pos c);
private:
pos cursor = 0;
pos height = 0, width = 0;
string contents;
};
#endif //SCREEN_H
练习7.25:
Screen 能安全地依赖于拷贝和赋值操作的默认版本吗?如果能,为什么?如果不能,为什么?
解答:
当类需要分配类对象之外的资源时,合成的版本通常会失效; 如果类包含 vector 或 string 成员,则其拷贝、赋值和销毁的合成版本才能够正常工作,因此 Screen 可以安全的依赖于拷贝和赋值的默认操作。
练习7.26:
将 Sales_data::avg_price 定义成内联函数。
解答:
// Sales_data.h
#ifndef _SALES_DATA_H_
#define _SALES_DATA_H_
#include <string>
#include <iostream>
using namespace std;
class Sales_data
{
friend istream &read(istream &is, Sales_data &item);
friend ostream &print(ostream &os, const Sales_data &item);
friend Sales_data add(const Sales_data &lhs, const Sales_data &rhs);
public:
Sales_data() = default; //默认的构造函数
Sales_data(const string &s) :
bookNo(s)
{
}
Sales_data(const string &s, unsigned n, double p) :
bookNo(s),
units_sold(n),
revenue(p*n)
{
}
Sales_data(istream &is) { read(is, *this); };
string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);
private:
inline double avg_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
double aver_price = 0.0;
};
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
aver_price = revenue / units_sold;
return *this;
}
inline double Sales_data::avg_price() const
{
return units_sold ? revenue / units_sold : 0;
}
static istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price*item.units_sold;
return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.bookNo << " " << item.units_sold << " "
<< item.revenue << " " << item.aver_price;
return os;
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
#endif //_SALES_DATA_H_
练习7.27:
给你自己的 Screen 类添加 move、set 和 display 函数,通过执行下面的代码检验你的类是否正确。
Screen myScreen(5,5,'x');
myScreen.move(4,0).set('#').display(cout);
cout<<"\n";
myScreen.display(cout);
cout<<"\n";
解答:
#ifndef SCREEN_H
#define SCREEN_H
#include <string>
#include <iostream>
using namespace std;
class Screen
{
public:
typedef string::size_type pos;
Screen() = default;
Screen(pos ht, pos wd) :
height(ht),
width(wd),
contents(ht * wd, ' ')
{
}
Screen(pos ht, pos wd, char c) :
height(ht),
width(wd),
contents(ht * wd, c)
{
}
char get() const // 读取光标处的字符
{
return contents[cursor];
}
inline char get(pos ht, pos wd) const;
inline Screen &move(pos r, pos c);
inline Screen &set(char);
inline Screen &set(pos, pos, char);
Screen &display(ostream &os)
{
do_display(os);
return *this;
}
const Screen &display(ostream &os) const
{
do_display(os);
return *this;
}
private:
pos cursor = 0;
pos height = 0, width = 0;
string contents;
void do_display(ostream &os) const { os << contents; }
};
inline
Screen &Screen::move(pos r, pos c)
{
pos row = r * width;
cursor = row + c;
return *this;
}
char Screen::get(pos r, pos c) const
{
pos row = r * width;
return contents[row + c];
}
inline Screen &Screen::set(char c)
{
contents[cursor] = c;
return *this;
}
inline Screen &Screen::set(pos r, pos col, char ch)
{
contents[r*width + col] = ch;
return *this;
}
#endif //SCREEN_H
运行结果如下:
练习7.28:
如果 move、set 和 display 函数的返回类型不是 Screen& 而是 Screen,则在上一个练习中将会发生什么情况?
解答:
如果函数的返回类型不是引用,则返回值是 *this 的副本,是临时副本,而不能改变 myScreen 的值。
练习7.29:
修改你的 Screen 类,令 move、set 和 display 函数返回 Screen 并检查程序的运行结果,在上一个练习中你的推测正确吗?
解答:
运行结果如下:
在第二个 display 中并没有把 ‘#’ 设置进去。
练习7.30:
通过 this 指针使用成员的做法虽然合法,但是有点多余。讨论显示地使用指针访问成员的优缺点。
解答:
优点:可以明确地指出访问的是调用该函数的对象的成员,且可以在成员函数中使用与数据成员同名的形参。
缺点:容易出现定义的返回类型不是引用时,函数改变的是 *this 的副本,从而无法实现函数的目的。
练习7.31:
定义一对类 X 和 Y ,其中 X 包含一个指向 Y 的指针,而 Y 包含一个类型为 X 的对象。
解答:
#ifndef X_Y_H
#define X_Y_H
class Y;
class X
{
Y *temp;
};
class Y
{
X item;
};
#endif // X_Y_H
练习7.32:
定义你自己的 Screen 和 Window_mgr,其中 clear 是 Window_mgr 的成员,是 Screen 的友元。
解答:
#ifndef SCREEN_H
#define SCREEN_H
#include <string>
#include <iostream>
#include <vector>
using namespace std;
class Screen;
class Window_mgr
{
public:
using ScreenIndex = vector<Screen>::size_type;
void clear(ScreenIndex);
private:
vector<Screen> screens{ Screen(24,80,' ') };
};
class Screen
{
friend void Window_mgr::clear(ScreenIndex);
public:
typedef string::size_type pos;
Screen() = default;
Screen(pos ht, pos wd) :
height(ht),
width(wd),
contents(ht * wd, ' ')
{
}
Screen(pos ht, pos wd, char c) :
height(ht),
width(wd),
contents(ht * wd, c)
{
}
char get() const // 读取光标处的字符
{
return contents[cursor];
}
inline char get(pos ht, pos wd) const;
inline Screen &move(pos r, pos c);
inline Screen &set(char);
inline Screen &set(pos, pos, char);
Screen &display(ostream &os)
{
do_display(os);
return *this;
}
const Screen &display(ostream &os) const
{
do_display(os);
return *this;
}
private:
pos cursor = 0;
pos height = 0, width = 0;
string contents;
void do_display(ostream &os) const { os << contents; }
};
inline void Window_mgr::clear(ScreenIndex i)
{
Screen &s = screens[i];
s.contents = string(s.height * s.width, ' ');
}
inline
Screen &Screen::move(pos r, pos c)
{
pos row = r * width;
cursor = row + c;
return *this;
}
char Screen::get(pos r, pos c) const
{
pos row = r * width;
return contents[row + c];
}
inline Screen &Screen::set(char c)
{
contents[cursor] = c;
return *this;
}
inline Screen &Screen::set(pos r, pos col, char ch)
{
contents[r*width + col] = ch;
return *this;
}
练习7.33:
如果我们给 Screen 添加一个如下所示的 size 成员将发生什么情况?如果出现了问题,请尝试修改它。
pos Screen::size () const
{
return height * width;
}
解答:
在类的外部定义函数体时,由于 pos 类型是在类内声明的,所以当出了类的作用域后,编译器无法识别 pos,正确的表达方式如下:
Screen::pos Screen::size() const
{
return height*width;
}
练习7.34:
如果我们把第 256 页的 Screen 类的 pos 的 typedef 防在类的最后一行会发生什么情况?
解答:
会报错 pos 未定义的符号。
用来定义类型的成员必须先定义后使用。声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。如果某个成员的生命使用了类中尚未出现的名字,则编译器将会在定义该类的作用于中继续查找。编译器只考虑就在使用类型之前出现的声明。
练习7.35:
解释下面代码的含义,说明其中的 Type 和 initVal 分别使用了哪个定义。如果代码存在错误,尝试修改它。
typedef string Type;
Type initVal ();
class Exercise
{
public:
typedef double Type;
Type setVal(Type);
Type initVal();
private:
int val;
};
Type Exercise::setVal(Type parm)
{
val = parm + initVal();
return val;
}
解答:
typedef string Type;
Type initVal (); // 此处 Type 表示的是 string 类型,使用的是上一条定义 typedef string Type。
class Exercise
{
public:
typedef double Type; // 此处的 Type 将外层作用域的 Type 隐藏了
Type setVal(Type); // 此处的 Type 使用的是 typedef double Type 这一定义,表示 double 类型
Type initVal(); // 此处的 Type 使用的是 typedef double Type 这一定义,表示 double 类型
private:
int val;
};
Type Exercise::setVal(Type parm) // 第一个属于外层作用域,其代表 string 类型,第二个在类内作用域,表示 double 类型
{
val = parm + initVal(); // 此处函数 initVal 在类的内部定义,在类外使用时必须制定是哪个类的 Exercise::initVal();
return val;
}
当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。这时,返回类型必须指明它是哪个类的成员。如本例中在类外定义的函数设定其返回类型时,若想使用类内定义的返回类型,需要指定是哪个类,正确的表达为:Exercise::Type Exercise::setVal(Type parm)。
练习7.36:
下面的初始值是错误的,请找出问题所在并尝试修改它。
struct X
{
X(int i, int j) : base(i), rem(base%j) { }
int rem, base;
}
解答:
由于源程序先定义了 rem, 在定义了 base, 所以在构造函数中 rem 先被初始化,而 rem 初始化需要用到 base ,此时 base 还没有被定义。
修改:
struct X
{
X(int i, int j) : base(i), rem(base%j) { }
int base, rem;
}
练习7.37:
使用本节提供的 Sales_data 类,确定初始化下面的变量时分别使用了哪个构造函数,然后罗列出每个对象所有数据成员的值。
Sales_data first_item (cin)
int main()
{
Sales_data next;
Sales_data last("9-999-99999-9");
}
解答:
对于 cin 使用的构造函数: Sales_data(std::istream &is), 数据成员的值为用户输入的数据。
对于 next 使用的构造函数:Sales_data(std::string s = ""), 数据成员的值:bookNo = "", cnt = 0, revenue = 0.0。
对于 last 使用的构造函数:Sales_data(std::string s = ""), 数据成员的值:bookNo = "9-999-99999-9", cnt = 0, revenue = 0.0。
练习7.38:
有些情况下我们希望提供 cin 作为接受 istream& 参数的构造函数的默认实参,请声明这样的构造函数。
解答:
#ifndef STREAM_H
#define STREAM_H
#include <iostream>
using namespace std;
class stream
{
stream(istream &is = cin) { /* */ };
};
#endif //STREAM_H
练习7.39:
如果接受 string 的构造函数和接受 istream& 的构造函数都使用默认实参,这种行为合法吗?如果不,为什么?
解答:
不合法,会造成重载函数的调用不明确。
练习7.40:
从下面的抽象概念中选择一个(或者你自己指定一个),思考这样的类需要哪些数据成员,提供一组合理的构造函数并阐明这样的原因。
(a)Book (b)Date (c)Employee
(d)vehicle (e)Object (d)Tree
解答:
#ifndef EMPLOYEE_H
#define EMPLOYEE_H
#include <iostream>
#include <string>
using namespace std;
class Employee
{
public:
Employee() = default;
Employee(string name, int age, unsigned dayOfWork, double daySalary) {};
Employee(istream &is,const Employee &item) { };
private:
string name;
int age = 0;
unsigned dayOfWork;
double salary;
double bonus;
void read(istream &is, Employee &item);
void print(ostream &is, Employee &item);
void finalSalary(Employee &item);
};
#endif //EMPLOYEE_H
以上是一个简单的雇员工资结算系统的类,提供了最基础的构造函数,包括默认构造函数、以所有成员变量类型为形参的构造函数, 以及用户输入的接口,接受输入的构造函数。
练习7.41:
使用委托构造函数重新编写你的 Sales_data 类,给每个构造函数体添加一条语句,空气一旦执行就打印一条信息。用各种可能的方式分别创建 Sales_data 对象,认真研究每次输出的信息直到你确实理解了委托构造函数的执行顺序。
解答:
Sales_data.h 如下:
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <iostream>
#include <string>
using namespace std;
class Sales_data {
friend istream& read(istream& is, Sales_data& item);
friend ostream& print(ostream& os, const Sales_data& item);
friend Sales_data add(const Sales_data& lhs, const Sales_data& rhs);
public:
Sales_data(const string& s, unsigned n, double p)
: bookNo(s), units_sold(n), revenue(n * p)
{
cout << "Sales_data(const string&, unsigned, double)"
<< endl;
}
Sales_data() : Sales_data("", 0, 0.0f) //委托构造函数
{
cout << "Sales_data()" << endl;
}
Sales_data(const string &s) : Sales_data(s, 0, 0.0f) //委托构造函数
{
cout << "Sales_data(const string&)" << endl;
}
Sales_data(istream &is) : Sales_data() // 委托构造函数
{
read(is, *this);
cout << "Sales_data(istream &is)" << endl;
}
string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
private:
inline double avg_price() const;
private:
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
inline double Sales_data::avg_price() const
{
return units_sold ? revenue / units_sold : 0;
}
istream& read(istream& is, Sales_data& item);
ostream& print(ostream& os, const Sales_data& item);
Sales_data add(const Sales_data& lhs, const Sales_data& rhs);
#endif // SALES_DATA_H
Sales_data& Sales_data::combine(const Sales_data& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " " << item.revenue;
return os;
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
main.cpp 如下:
#include "Sales_data.h"
int main()
{
Sales_data item1;
cout << endl;
Sales_data item2("00-x-78345");
cout << endl;
Sales_data item3("00-x-78345", 5, 10.0);
cout << endl;
Sales_data item4(cin);
cout << endl;
system("pause");
return 0;
}
运行结果:
根据运行结果可以看出调用顺序是受委托构造函数初始值列表及函数体先运行,接着是委托者。
练习7.42:
对于你在练习7.40(参见7.5.1节,第261页)中编写的类,确定哪些构造函数可以使用委托。如果可以的话,编写委托构造函数。如果不可以,从抽象概念列表中选择一个你认为可以使用委托构造函数的,为挑选出的这个概念编写类定义。
解答:
#ifndef EMPLOYEE_H
#define EMPLOYEE_H
#include <iostream>
#include <string>
using namespace std;
class Employee
{
public:
Employee(const string &n, int a, unsigned d, double dS) :
name(n), age(a), dayOfWork(d), salary(dS) { }
//委托构造函数
Employee() : Employee("", 0, 0, 0.0) { }
Employee(const string &n) : Employee(n, 0, 0, 0.0) { }
Employee(istream &is, const Employee &item) : Employee() { };
private:
string name;
int age = 0;
unsigned dayOfWork;
double salary;
double bonus;
void read(istream &is, Employee &item);
void print(ostream &is, Employee &item);
void finalSalary(Employee &item);
};
#endif //EMPLOYEE_H
练习7.43:
假定有一个名为 NoDefault 的类, 它有一个接受 int 的构造函数,但是没有默认构造函数。定义类 C, C 有一个 NoDefault 类型的成员,定义 C 的默认构造函数。
解答:
#ifndef C_H
#define C_H
class NoDefault
{
NoDefault(int &) {}
};
class C
{
C() : def(0) {}
private:
NoDefault def;
};
#endif //C_H
练习7.44:
下面这条声明合法吗?如果不,为什么?
vector<NoDefault> vec(10);
解答:
此条声明是非法的,因为没有显示初始化,此时 vector 中的十个元素都需要默认初始化,但是类NoDefault没有默认的构造函数,无法默认初始化。
练习7.45:
如果在上一个练习中定义的 vector 的元素类型是 C, 则声明合法吗?为什么?
解答:
合法,因为类 C 中定义了默认的构造函数。
练习7.46:
下面那些论断是不正确的?为什么?
(a)一个类必须至少提供一个构造函数。
(b)默认构造函数函数是参数列表为空的构造函数。
(c)如果对于类来说不存在有意义的默认值,则类不应该提供默认构造函数。
(d)如果类没有定义默认构造函数,则编译器将为其生成一个并把每个数据成员初始化成相应类型的默认值。
解答:
(a) 错误。类可以不提供构造函数,编译器会自动生成一个合成的默认的构造函数。
(b)错误。默认构造函数是在未提供初始值设定项时使用的构造函数。此外,若构造函数包含若干实参且为其所有形参提供默认实参的构造函数也可以实现默认构造函数的功能。
(c)错误。如果一个类没有默认构造函数,当编译器确实需要隐式地使用默认构造函数时,该类则会无法使用。所以一般情况下,都应该为类构建一个默认构造函数。
(d)错误。只有当类没有显示地定义任何构造函数时,编译器才会隐式地自动生成默认构造函数。
练习7.47:
说明接受一个 string 参数的 Sales_data 构造函数是否应该是 explicit 的,并解释这样做的优缺点。
解答:
Sales_data 类中的只接受一个实参的构造函数应该是 explicit 的,编译器只会自动地执行一步类型转换,类类型的转换不是总有效。例如,item.combine("9-999-99-9"),需要先将“9-999-99-9”转换为一个临时的 string,再将这个临时的 string 转换为 Sales_data 类类型。将其声明为 explicit 避免用户输入出现此类情况。
优点:
1. 抑制构造函数定义的隐式转换;
2. 可以定义一个只能使用直接初始化的构造函数(explicit 函数只能用于直接初始化);
缺点:
1. explicit 构造函数只能用于直接初始化;
2. explicit 关键字只能用于接收一个实参的构造函数;
练习7.48:
假定 Sales_data 的构造函数不是 explicit 的,则下述定义将执行什么样的操作?
string null_isbn("9-999-99999-9");
Sales_data item1(null_isbn);
Sales_data item2("9-999-99999-9");
如果构造函数是 explicit 的,又会发生什么呢?
解答:
全部调用了相关的构造函数,且编译器不会报错。
练习7.49:
对于 combine 函数的三种不同声明,当我们调用 i.combine(s) 时分别发生什么情况?其中 i 是一个 Sales_data, 而 s 是一个 string 对象。
(1)Sales_data &combine (Sales_data);
(2)Sales_data &combine (Sales_datat&);
(3)Sales_data &combine (const Sales_data&) const;
解答:
(1)合法,string 类型的 s 会自动创建一个临时的 Sales_data,并通过其传递给 combine 函数;
(2)不合法,不可以把创建的临时量绑定在普通引用上;
(3)不合法,尾随的 const 标记禁止对数据成员进行修改,与 combine 函数的目的冲突;
练习7.50:
确定在你的 Person 类中是否含有一些构造函数应该是 explicit 的。
解答:
explicit Person(std::istream& is) { read(is, *this); }
练习7.51:
vector 将其但参数的构造函数定义成 explicit 的,而 string 则不是,你觉得原因何在?
解答:
因为 vector 中定义数据类型,如果某个函数接受的实参输入为 vector<int>&,当输入某一整数时,例如 15, 会造成混乱,而string 则不会出现这种情况。
练习7.52:
使用 2.6.1 节(第 64 页)的 Sales_data 类,解释下面的初始化过程。如果存在问题,请尝试修改它。
Sales_data item = {“978-0590353403”,25,15.99};
解答:
首先使用此方式初始化聚合类的前提条件包括“没有类内初始值”,所以应将其修改为:
struct Sales_data
{
std::string bookNo;
unsigned units_sold;
double revenue;
}
含义是 bookNo = “978-0890353403”,units_sold = 25,revenue = 15.99;
练习7.53:
定义你自己的 Debug。
解答:
#ifndef DEBUG_H
#define DEBUG_H
class Debug
{
public:
constexpr Debug(bool b = true) :
hw(b),
io(b),
other(b)
{ }
constexpr Debug(bool h, bool i, bool o):
hw(h),
io(i),
other(o)
{ }
constexpr Debug()
{
return hw || io || other;
}
void set_hw(bool b) { hw = b; }
void set_io(bool b) { io = b; }
void set_other(bool b) { other = o; }
private:
bool hw;
bool io;
bool other;
};
#endif //DEBUG_H
练习7.54:
Debug 中以 set_开头的成员应该被声明成 constexpr 吗?如果不,为什么?
解答:
不应该,因为 constexpr 函数必须包含且只能包含一个 return 语句。
练习7.55:
7.5.5 节(第 266 页)的 Data 类是字面值常量吗?请解释原因。
解答:
不是,Data 是一个聚合类但其数据成员不是字面值类型。
练习7.56:
什么是类的静态成员?它有何优点?静态成员与普通成员有何区别?
解答:
类的静态成员与类本身直接相关,而不是与类的各个对象保持联系。
优点:
与类直接关联,不需要为每个类的对象存储静态成员的信息,若数据被更改则每个类的对象都可以使用更改之后的值。
静态成员与普通成员的区别:
1. 可以使用作用域运算符直接访问静态成员。
2. 静态数据成员可以使不完全类型,静态成员可以使它所属的类类型,而普通成员则受到限制,只能声明为它所属的类的指针或引用。
3. 可以使用静态成员作为默认实参,非静态数据成员不能作为默认实参。
练习7.57:
编写你自己的 Account 类。
解答:
#ifndef ACCOUNT_H
#define ACCOUNT_H
#include<string>
using namespace std;
class Acount
{
public:
void calculate() { amount += amount *interestRate; }
static double rate() { return interestRate; }
static void rate(double);
private:
string owner;
double amount;
static double interestRate;
static double initRate();
};
#endif //ACCOUNT_H
练习7.58:
下面的静态数据成员的声明和定义有错误吗?请解释原因。
// example.h
class Example
{
public:
static double rate = 6.5;
static const int vecSize = 20;
static vector<double> vec(vecSize);
};
// example.C
# include "example.h"
double Example::rate;
vector<double> Example::vec;
解答:
静态成员在类的内部初始化时,可以为静态成员提供 const 整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的 constexpr ,即初始值必须是常量表达式。所以应该做如下修改:
// example.h
class Example {
public:
static constexpr double rate = 6.5; //初始值必须为常量表达式
static const int vecSize = 20; //初始值可以是 const 整数类型
static vector<double> vec; // vector 类型不是字面值类型,更不是字面值常量
};
// example.C
#include "example.h"
constexpr double Example::rate;
vector<double> Example::vec(Example::vecSize);