c++ primer 第五版第七章
7.01 利用2.6.1节所定义的Sales_data类为1.6节的交易处理程序编写一个新的版本。
struct Sales_data
{
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
void test701()
{
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.bookNo == trans.bookNo) {
total.units_sold += trans.units_sold;
total.revenue += trans.revenue;
}
else {
cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
total = trans;
}
}
cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
}
else {
std::cerr << "No data" << endl;
}
}
7.02 为上题中的Sales_data类添加combine和isbn成员。
struct Sales_data
{
std::string bookNo; // 对象的ISBN编号
unsigned units_sold = 0; // 售出的册数
double revenue = 0.0; // 总价格
std::string isbn () const { return bookNo; } // 返回ISBN编号
Sales_data& combine (const Sales_data&); // 将一个Sales_data对象加到另一个上面
};
Sales_data& Sales_data:: combine (const Sales_data& rhs)
{
this.units_sold += rhs.units_sold;
this.revenue += ths.revenue;
return *this;
}
7.03 修改7.1.1节中的交易程序,令其使用这些成员。
void test703()
{
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.bookNo << " " << total.units_sold << " " << total.revenue << endl;
total = trans;
}
}
cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
}
else {
std::cerr << "No data" << endl;
}
}
7.04 编写一个名为Person的类,使其表示人员的姓名和住址。使用string对象存放这些元素,接下来的练习将不断充实这个类的其他特征。
7.05 在你的Person类中提供一些操作使其能够返回姓名和住址。这些函数是否应该是const呢?解释原因。
要使用const,因为这些函数并不改变它调用的对象的内容。
class Person
{
public:
std::string name;
std::string addr;
std::string getName() const { return name; }
std::string getAddr() const { return addr; }
};
7.06 对于函数add、read和print,定义你自己的版本。
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() << "\t" << item.units_sold << "\t" << item.revenue << endl;
return os;
}
Sales_data& add(Sales_data& item1, Sales_data& item2)
{
Sales_data sum = item1;
sum.combine(item2);
return sum;
}
7.07 使用这些新函数重写7.1.2节中的交易处理程序。
void test707()
{
Sales_data total;
if (read(cin, total)) {
Sales_data trans;
while (read(cin, trans)) {
if (total.isbn() == trans.isbn()) {
total.combine (trans);
}
else {
print(cout, total);
total = trans;
}
}
print(cout, total);
}
else {
std::cerr << "No data" << endl;
}
}
7.08 为什么read函数将其Sales_data参数定义成普通的引用,而print函数将其参数定义成常量引用?
因为print函数不会改变对象的值,但是read函数则会改变对象内容。
7.09 对于7.1.2节练习中的代码,添加读取和打印Person对象的操作。
std::istream& readPerson(std::istream& is, Person& item)
{
is >> item.name >> item.addr;
return is;
}
std::ostream& printPerson(std::ostream& os, const Person& item)
{
os << "Name : " << item.getName() << "\t Address: " << item.getAddr() << endl;
return os;
}
7.10 在下面这条if语句中,条件部分的作用是什么?
if (read (read(cin, data), data))
一次连续读入两个data。read(cin, data)返回的还是cin的引用,在该输入流上继续读入数据。
7.11 在你的Sales_data类中添加构造函数,然后编写一段程序令其用到每个构造函数。
// 类定义中
{
Sales_data() = default;
Sales_data(const std::string &s) : bookNo(s){}
Sales_data(const std::string &s, unsigned n, double r) : bookNo(s), units_sold(n), revenue(r) {}
Sales_data(std::istream& is);
}
Sales_data::Sales_data (std::istream& is)
{
read(is, *this);
}
// cpp文件中调用
void test711()
{
Sales_data item1;
print(std::cout, item1) << std::endl;
Sales_data item2("0-201-78345-X");
print(std::cout, item2) << std::endl;
Sales_data item3("0-201-78345-X", 3, 20.00);
print(std::cout, item3) << std::endl;
Sales_data item4(std::cin);
print(std::cout, item4) << std::endl;
}
7.12 把只接受一个istream作为参数的构造函数定义到类的内部。
struct Sales_data;
std::istream& read (std::istream& is, Sales_data& item);
struct Sales_data
{
std::string bookNo; // 对象的ISBN编号
unsigned units_sold = 0; // 售出的册数
double revenue = 0.0; // 总价格
Sales_data() = default;
Sales_data(const std::string &s) : bookNo(s){}
Sales_data(const std::string &s, unsigned n, double r) : bookNo(s), units_sold(n), revenue(r) {}
Sales_data(std::istream& is){ read(is, *this); };
std::string isbn() const { return bookNo; } // 返回ISBN编号
Sales_data& combine (const Sales_data& rhs); // 将一个Sales_data对象加到另一个上面
};
7.13 使用istream构造函数重写229页的程序。
void test712()
{
Sales_data total (cin);
if (cin) {
Sales_data trans(cin);
do{ // 要有do while循环,因为Sales_data构造的时候已经读入一个trans的值。
if (total.isbn() == trans.isbn()) {
total.combine (trans);
}
else {
print(cout, total);
total = trans;
}
} while (read(cin, trans));
print(cout, total);
}
else {
std::cerr << "No data" << endl;
}
}
7.14 编写一个构造函数,令其用我们提供的类内初始值显示地初始化成员。
Sales_data () : bookNo(""), units_sold(0), revenue(0) { }
7.15 为你的Person类添加正确的构造函数。
class Person;
std::istream& readPerson(std::istream& is, Person& item);
class Person
{
public:
std::string name;
std::string addr;
std::string const& getName() const { return name; }
std::string const& getAddr() const { return addr; }
Person() = default;
Person(const std::string n, const std::string a) : name(n), addr(a) { }
Person (std::istream& is) { readPerson(is, *this); }
};
void test715()
{
Person p1("lily", "sanqi");
printPerson(cout, p1);
Person p2(cin);
printPerson(cout, p2);
}
7.16 在类的定义中,对于访问说明符出现的位置和次数有限定吗?如果有,是什么?什么样的成员应该定义在public后?什么样的应该定义在private后?
一个类对访问说明符出现的次数和位置并没有严格的限定。构造函数和接口函数定义在public之后,而数据成员和部分成员函数定义在private后面。
7.17
class和struct唯一的区别就是默认访问权限不同,class默认访问权限为private,而struct则是public。
7.18
封装就是定义一系列的接口,对用户隐藏实现细节,用户在使用时只需要调用接口就可以。
7.19
Person类的构造函数和获取信息等函数应该设置为public,成员数据设置为private。因为构造函数和获取信息的函数需要在类外进行调用,而成员数据可以封装成接口,不需要暴露给用户。
7.20 友元在什么时候有用?请分别列举出使用友元的利弊。
友元是类提供给非成员函数访问类内私有成员的一种机制。优势是:让类外函数也可以像类内成员一样方便的访问私有成员。缺点是:破坏了类的封装,写法较麻烦,必须在类内类外都进行声明。
7.21 修改Sales_data类使其隐藏实现细节,借助类的新定义重新编译程序,使其正常工作。
// 对Sales_data类做如下改动
class Sales_data
{
friend std::istream& read (std::istream& is, Sales_data& item);
friend std::ostream& print (std::ostream& os, const Sales_data& item);
friend Sales_data& add(Sales_data* item1, Sales_data& item2);
public:
Sales_data() = default;
Sales_data(const std::string &s) : bookNo(s){}
Sales_data(const std::string &s, unsigned n, double r) : bookNo(s), units_sold(n), revenue(r) {}
Sales_data(std::istream& is){ read(is, *this); };
std::string isbn() const { return bookNo; } // 返回ISBN编号
Sales_data& combine (const Sales_data& rhs); // 将一个Sales_data对象加到另一个上面
private:
std::string bookNo; // 对象的ISBN编号
unsigned units_sold = 0; // 售出的册数
double revenue = 0.0; // 总价格
};
std::istream& read (std::istream& is, Sales_data& item);
std::ostream& print (std::ostream& os, const Sales_data& item);
Sales_data& add(Sales_data* item1, Sales_data& item2);
7.22 修改你的Person类使其隐藏实现细节。
class Person
{
friend std::istream& readPerson(std::istream& is, Person& item)
public:
Person() = default;
Person(const std::string n, const std::string a) : name(n), addr(a) { }
Person (std::istream& is) { readPerson(is, *this); }
std::string const& getName() const { return name; }
std::string const& getAddr() const { return addr; }
private:
std::string name;
std::string addr;
};
std::istream& readPerson(std::istream& is, Person& item);
std::ostream& printPerson(std::ostream& os, const Person& item);
std::istream& readPerson(std::istream& is, Person& item)
{
is >> item.name >> item.addr;
return is;
}
std::ostream& printPerson(std::ostream& os, const Person& item)
{
os << "Name : " << item.getName() << "\t Address: " << item.getAddr() << endl;
return os;
}
7.23 编写你自己的Screen类。
class Screen
{
public:
using pos = string::size_type;
private:
pos cursor = 0;
pos height = 0, width = 0;
string contents;
};
7.24 给你的Screen类添加三个构造函数:一个默认构造函数;另一个构造函数接受宽和高的值,然后将contents初始化成给定数量的空白;第三个参数接受宽和高的值以及一个字符,该字符作为初始化之后屏幕的内容。
class Screen
{
public:
using pos = string::size_type;
Screen() = default;
Screen(pos wd, pos ht) : width(wh), height(ht), contents(ht*wd, " ") {}
Screen(pos wd, pos ht, char c) : width(wd), height(ht), contents(ht*wd, c) {}
char getChar() const { return contents[cursor]; }
private:
pos cursor = 0;
pos height = 0, width = 0;
string contents;
};
7.25 Screen能安全的依赖拷贝和赋值操作的默认版本吗?如果能,为什么?如果不能,为什么?
可以依赖默认拷贝和赋值操作,因为类中没有分配内存的操作。
7.26 将Sales_data::avg_price定义成内联函数。
在Sales_data类中的public下添加下面语句:
inline double avg_price() const { return units_sold ? revenue/units_sold : 0; }
7.27 给你自己的Screen类添加move、set和display函数,通过执行下面的代码检验你的类是否正确。
// screen.h文件
class Screen
{
public:
using pos = std::string::size_type;
Screen() = default;
Screen(pos wd, pos ht) : width(wd), height(ht), contents(ht*wd, ' ') {}
Screen(pos wd, pos ht, char c) : width(wd), height(ht), contents(ht*wd, c) {}
char get() const { return contents[cursor]; }
char get(pos r, pos c) const;
Screen &set(char);
Screen &set(pos, pos, char);
Screen &move(pos r, pos c);
const Screen& display(ostream& os) const;
Screen& display(ostream& os);
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;
}
inline Screen& Screen::set(char ch)
{
contents[cursor] = ch;
return *this;
}
inline Screen& Screen::set(pos r, pos col, char ch)
{
pos row = r *width;
contents[row+col] = ch;
return *this;
}
Screen& Screen::display(ostream& os)
{
do_display(os);
return *this;
}
const Screen& Screen::display(ostream& os) const
{
do_display(os);
return *this;
}
// cpp文件
void test727()
{
Screen myScreen (5, 5, 'x');
myScreen.move(4, 0).set('#').display(cout);
cout << endl;
myScreen.display(cout);
}
// 打印结果:xxxxxxxxxxxxxxxxxxxx#xxxx
7.28 如果move、set和display函数的返回类型不是Screen&而是Screen,则在上一个练习中会发生什么情况?
如果都返回的是Screen,则move和set都修改的是副本,原始的myScreen并没有被修改,myScreen.display(cout);语句最终打印出来还是(5, 5, ‘X’)的值。
7.29 修改程序,令move、set和display函数的返回类型是Screen,验证上题中你的猜测。
// 有&
xxxxxxxxxxxxxxxxxxxx#xxxx
xxxxxxxxxxxxxxxxxxxx#xxxx
// 没有&
xxxxxxxxxxxxxxxxxxxx#xxxx
xxxxxxxxxxxxxxxxxxxxxxxxx
7.30 通过this指针使用成员的做法虽然合法,但是有点多余。讨论显式地使用指针访问成员的优缺点。
优点:
1、使程序意图明确,更易读;
2、可以使形参名和要赋值的成员名相同。
如:std::string& setName(const string& name) { this->name = name; }
缺点:有些场景下比较多余
std::string const& getName() const
7.31 定义一对类X和Y,其中X包含一个指向Y的指针,而Y包含一个类型位X的对象。
class X;
class Y;
class X
{
private:
Y* y = nullptr;
};
class Y
{
private:
X x;
};
7.32 定义你自己的Screen和Window_mgr,其中clear是Window_mgr的成员,是Screen的友元。
// 要遵守定义友元函数的依赖关系
class Screen;
class Window_mgr
{
public:
using ScreenIndex = std::vector<Screen>::size_type;
void clear(ScreenIndex);
private:
std::vector<Screen> screens; // 这里不能调用Screen构造函数,否则报错引用了不完全类型
};
class Screen
{
friend void Window_mgr::clear(ScreenIndex);
/* Screen类的其他定义 */
};
void Window_mgr::clear(ScreenIndex i)
{
Screen &s = screens[i];
s.contents = string(s.height * s.width, ' ');
}
7.33 如果我们给Screen添加一个如下所示的size成员将会发生什么情况?如果出来问题,请尝试修改它。
pos Screen::size() const
{
return height * width;
}
// 将会发生提示pos是不知道的类型。应做如下修改:
Screen::pos Screen::size() const
7.34 如果我们把Screen类中的pos的typedef放在类的最后一行会发生什么情况?
将会提示pos是不知道的类型。
7.35 介绍下面代码的含义,说明其中的Type和initVal分别使用了哪个定义。如果代码存在错误,尝试修改它。
typedef string Type;
Type initVal();
class Exercise {
public:
typedef double Type;
Type setVal(Type); // double
Type initVal(); // double
private:
int val;
};
Type Exercise::setVal(Type parm) { // 返回的Type是string类型,形参类型是double类型
val = parm + initVal; // initVal调用的类中的
return val;
}
// 会报错Type Exercise::setVal(Type parm)匹配不到类中的函数,因为返回值与类中不一致。改为:
// initVal函数只声明未定义,也会报错。
Exercise::Type Exercise::setVal(Type parm){}
7.36 下面的初始值是错误的,请找出问题并修改。
struct X {
X (int i, int j):base(i), rem(base % j) {}
int rem, base; // 改为int base, rem;
};
// 用一个成员来初始化另一个成员,没有考虑顺序问题
7.37 使用本节提供的Sales_data类,确定初始化下面的变量时分别使用了哪个构造函数,然后罗列出每个对象所有的数据成员的值。
Sales_data first_item(cin); // 使用了 Sales_data(std::istream &is) ; 数据成员值依赖输入
int main() {
Sales_data next; // 使用了Sales_data(std::string s = ""); bookNo = "", cnt = 0, revenue = 0.0
Sales_data last("9-999-99999-9"); // 使用了 Sales_data(std::string s = ""); bookNo = "9-999-99999-9", cnt = 0, revenue = 0.0
}
7.38 有些情况我们希望提供cin作为接受istream&参数的构造函数的默认实参,请声明这样的函数。
Sales_data(std::istream &is = std::cin) { read(is, *this); }
7.39 如果接受string的构造函数和接受istream&的构造函数都使用默认实参,这种行为合法吗?如果不?为什么?
不合法,如果都使用默认值,不提供实参,则编译器就不知道该调用哪个构造函数了。
7.40 从下面的抽象概念中选择一个,思考这样的类需要哪些数据成员,提供一组合理的构造函数并阐明原因。
class Date
{
public:
Date(int y, int m, int d) : year(y), month(m), day(d) { }
void setYear(int y);
void setMonth(int m);
void setDay(int d);
int getYear();
int getMonth();
int getDay();
private:
int year;
int month;
int day;
};
7.41 使用委托构造函数重新编写你的Sales_data类,给每个构造函数体添加一条语句,令其一旦执行就打印一条信息。用各种可能的方式分别创建Sales_data对象,认真研究每次输出的信息直到你确实理解了委托构造函数的执行顺序。
void test741()
{
cout << "----------- 1. default: " << endl;
Sales_data s1();
cout << "----------- 2. init bookNo" << endl;
Sales_data s2("999");
cout << "----------- 3.use cin init" << endl;
Sales_data s3(cin);
cout << "----------- 4. use three parameter init: " << endl;
Sales_data s4("s01-999", 2, 59.8);
}
输出结果如下:
----------- 1. default:
Sales_data(const std::string &s, unsigned n, double r)
default
----------- 2. init bookNo
Sales_data(const std::string &s, unsigned n, double r)
Sales_data(const std::string &s)
----------- 3.use cin init
Sales_data(const std::string &s, unsigned n, double r)
default
iso-99 2 59.2
Sales_data(std::istream& is)
----------- 4. use three parameter init:
Sales_data(const std::string &s, unsigned n, double r)
7.42
7.43 假定有一个类名为NoDefault的类,它有一个接受int的构造函数,但是没有默认构造函数。定义类c,c有一个NoDefault类型的成员,定义c的默认构造函数。
class NoDefault
{
public:
NoDefault(int v) : value(v){}
private:
int value;
};
class C
{
public:
C() : nd(0){}
private:
NoDefault nd;
};
7.44 下面这条声明合理吗?为什么?
vector<NoDefault> vec(10);
非法,因为vector的10个元素没有被初始化,因此需要默认初始化,而NoDefault类型没有提供默认构造函数
7.45 如果上题中定义的vector类型是C,则声明合法吗?
合法,因为C类提供了默认构造函数。
7.46 下面哪种论断是不正确的?为什么?
- 一个类必须至少提供一个构造函数。
- 默认构造函数是参数列表为空的构造函数。
- 如果对于类来说不存在有意义的默认值,则类不应该提供默认构造函数。
- 如果类没有定义默认构造函数,则编译器将为其生成一个并把每个数据成员初始化成相应类型的默认值。
以上论断都不正确:
- 类可以不提供构造函数,编译器会提供一个默认构造函数。
- 默认构造函数为没有初始化列表(而不是参数列表为空)的对象提供默认初始值,为成员提供默认值的构造函数也称为默认构造函数。
- 类应该提供默认构造函数。
- 只有当类没有定义任何构造函数的时候,编译器才会定义默认构造函数。
7.47 说明接受一个string参数的Sales_data构造函数是否应该是explicit的,并解释这样做的优缺点。
Sales_data类的构造函数应该是explicit的。
优点:
- 保证用户能按照类设计者的初衷进行初始化。
缺点:
- 当只有一个参数时,要进行初始化再使用,没有隐式转换的写法简洁。
7.48 假定Sales_data的构造函数不是explicit的,下述定义将会执行什么操作?
string null_isbn("9-99-9999-9");
Sales_data item1(null_isbn); // 用string类型的null_isbn直接初始化item1.
string item2("9-99-9999-9"); // 用字符串初始化item2.
7.49 对于combine函数的三种不同声明,当我们调用i.combine(s)时分别发生什么情况?其中i是一个Sales_data,而s是一个string对象。
- Sales_data &combine(Sales_data); // 正常初始化,将s转化成Sales_data类型。
- Sales_data &combine(Sales_data&);
// 报错:error: invalid initialization of non-const reference of type 'Sales_data&' from an rvalue of type 'Sales_data',string不能转化为Sales_data类型的引用。
- Sales_data &combine(const Sales_data&) const;
// 报错:error: assignment of member 'Sales_data::units_sold' in read-only object,声明最后的const会禁止函数对值做出改变。
7.50 确定在你的person类中是否有一些构造函数应该是explicit的。
explicit Person (std::istream& is) { readPerson(is, *this); }
// 只接受一个参数的构造函数应该是explicit
7.51 vector将其单参数的构造函数定义成explicit的,而string则不是,你觉得原因何在?
引用别人对这个问题的阐述如下:
Such as a function like that:
int getSize(const std::vector<int>&);
if vector has not defined its single-argument constructor as explicit. we can use the function like:
getSize(34);
What is this mean? It's very confused.
But the
std::string
is different. In ordinary, we usestd::string
to replaceconst char *
(the C language). so when we call a function like that:void setYourName(std::string); // declaration. setYourName("pezy"); // just fine.
it is very natural.
7.52 使用2.6.1节的Sales_data类,解释下面的初始化过程。如果存在问题,尝试修改它。
Sales_data item = {"978-059035", 15, 29.98};
// 要使用这种初始化方式,要求类必须是聚合类。因此Sales_data类需要改成如下形式:
struct Sales_data
{
std::string bookNo;
unsigned int unit_sold;
double revenue;
};
7.53 定义你自己的Debug。
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 bool any() { return hw || io || other; }
void set_hw (bool b) { hw = b; }
void set_io (bool b) { io = b; }
void set_other (bool b) { other = b; }
private:
bool hw; // 硬件错误
bool io; // io错误
bool other; // 其它错误
};
7.54 Debug中以set_开头的成员应该被声明为constexpr吗?如果不,为什么?
在c++11中声明函数是constexpr必须满足以下条件:
返回值和参数必须是Literal类型
函数体必须只包含一个return语句
函数提可以包含其他的语句,但是这些语句不能在运行期起作用
函数可以不返回常量,但是在调用的时候实参必须传入常量表达式
因此,如果按照c++11的标准,set_开头的成员函数不能被声明为constexpr。
报错信息:error: assignment of member 'Debug::hw' in read-only object
error: invalid return type 'void' of constexpr function 'constexpr void Debug::set_hw(bool) const'
但c++14好像取消了一些限制,因此c++14编译不报错。
7.55 7.5.5节的Data类是字面值常量类吗?为什么?
数据成员都是字面值类型的聚合类是字面值常量类。 但Data类的数据成员不一定是字面值类型,使用变量或表达式也可以进行初始化。
7.56 什么是类的静态成员?它有何优点?静态成员与普通成员有何区别?
类的静态成员与类本身直接相关,而不是与类的各个对象关联。
优点:每个对象都不需要单独存储静态成员变量,一旦静态成员改变了,则每个对象都可以使用新的值。
区别:类的静态成员属于类本身,在类加载时就会分配内存,可以通过类名直接进行访问。
普通成员属于类的对象,只有在类对象产生时才会分配内存。只能通过对象去访问。
7.57 编写你自己的Account类。
class Account
{
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();
};
7.58 下面的静态数据成员的声明和定义有错吗?请解释原因。
// example.h
class Example
{
public:
static double rate = 6.5; // 错误,静态成员类内初始化应该是一个const表达式。
// 改为 static constexpr double rete = 6.5;
static const int vecSize = 20;
static vector<double> vec(vecSize); // vector不需要在类内就定义大小
// 改为static vector<double> vec;
}
// examplec.cpp
#include "example.h"
double Example::rate;
vector<double> Example::vec;