练习7.1:使用2.6.1节定义的Sales_data类为1.6节的交易处理程序编写一个新版本。
#include
#include "Sales_data.h"
using std::cin;
using std::cout;
using std::endl;
using std::cerr;
int main()
{
cout<<"请输入交易记录(ISBN、销售量、原价、实际售价):"<> total)
{
Sales_data trans;
//读入剩余交易记录
while (cin >> trans)
{
//如果我们仍在处理相同的书
if (total.isbn() == trans.isbn())
total += trans;
else
{
cout<
class Sales_data
{
private:
string bookNo;
unsigned units_sold = 0;
double sellingprice = 0.0;
double salesprice = 0.0;
double discount = 0.0;
public:
string isbn() const { return bookNo; }
Sales_data& combine( const Sales_data &rhs)
{
units_sold += rhs.units_sold;
salesprice = (rhs.salesprice * rhs.units_sold + salesprice * units_sold) / (rhs.units_sold + units_sold);
if (sellingprice != 0)
discount = salesprice / sellingprice;
return *this;
}
};
#include
#include "Sales_data.h"
using std::cin;
using std::cout;
using std::endl;
using std::cerr;
int main()
{
cout<<"请输入交易记录(ISBN、销售量、原价、实际售价):"<> total)
{
Sales_data trans;
//读入剩余交易记录
while (cin >> trans)
{
//如果我们仍在处理相同的书
if (total.isbn() == trans.isbn())
total.combine(trans);
else
{
cout<
练习7.4:编写一个名为Person的类,使其表示人员的姓名和住址。使用string对象存放这些元素,接下来的练习将不断重试这个类的其他特征。
class Person
{
private:
string strName;
string strAddress;
};
class Person
{
private:
string strName;
string strAddress;
public:
string getName() const { return strName; }
string getAddress() const { return strAddress; }
};
这两个函数都应该被定义为常量成员函数,因为不论返回姓名还是返回地址,在函数体内都只是读取数据成员的值,而不会做任何改变。
练习7.6:对于函数add、read、和print,定义你自己的版本。
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sun.combine(rhs);
return sum;
}
std::istream &read(std::istream &is, Sales_data &item)
{
is >> item.bookNo >> item.units_sold >> item.sellingprice >> item.salesprice;
return is;
}
std::ostream &print(std::ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " " <
#include
#include "Sales_data.h"
using std::cin;
using std::cout;
using std::endl;
using std::cerr;
int main()
{
cout<<"请输入交易记录(ISBN、销售量、原价、实际售价):"<
read函数将其Sales_data参数定义成普通的引用是因为我们需要从标准输入流中读取数据并将其写入到给定的Sales_data对象,因此需要有修改的权限。而print将其参数定义成常量引用是因为它只负责数据的输出,不对其做任何更改。
练习7.9:对于7.1.2节练习中的代码,添加读取和打印Person对象的操作。
std::istream &read(std::istream &is, Person &per)
{
is >> per.strName >> per.strAddress;
return is;
}
std::ostream &print(std::ostream &os, const Person &per)
{
os << per.getName() << per.getAddress();
return os;
}
if ( read(read(cin, data1), data2))
因为read函数的返回类型是引用,所以read(cin, data1)的返回值可以继续作为外层read函数的实参使用。该条件检验读入data1和data2的过程是否正确,如果正确,条件满足
练习7.11:在你的Sales_data类中添加构造函数,然后编写一段程序令其用到每个构造函数。
class Sales_data
{
public:
Sales_data() = default;
Sales_data(const std::string &book): bookNo(book) { }
Sales_data(const std::string &book, const unsigned num, const double sellp, const double salep);
Sales_data(std::istream &is);
private:
std::string bookNo;
unsigned units_sold = 0;
double sellingprice = 0.0;
double salesprice = 0.0;
double discount = 0.0;
};
Sales_data::Sales_data(const std::string &book, const unsigned num, const double sellp, const double salep)
{
bookNo = book;
units_sold = num;
sellingprice = sellp;
saleprice = salep;
if (sellingprice != 0)
discount = saleprice / sellingprice;
}
Sales_data::Sales_data(std::istream &is)
{
is >> *this;
}
#include
#include "Sales_data.h"
using std::cout;
using std::endl;
int main()
{
Sales_data data1;
Sales_data data2("978-7-121-15535-2");
Sales_data data3("978-7-121-15535-2", 100, 128, 109);
Sales_data data4(cin);
cout<<"书籍的销售情况是: "<
class Sales_data
{
public:
Sales_data() = default;
Sales_data(const std::string &book): bookNo(book) { }
Sales_data(const std::string &book, const unsigned num, const double sellp, const double salep);
Sales_data(std::istream &is) {is >>*this; }
private:
std::string bookNo;
unsigned units_sold = 0;
double sellingprice = 0.0;
double salesprice = 0.0;
double discount = 0.0;
};
练习7.13:使用istream构造函数重写第229页的程序。
#include
#include "Sales_data.h"
using namespace std;
int main()
{
Sales_data total(cin);
if (cin)
{
Sales_data trans(cin);
do
{
if (total.isbn() == trans.isbn())
total.combine(trans);
else
{
print(cout, total) << endl;
total = trans;
}
}while (read(cin, trans));
print(cout, total)<
Sales_data(const std::string &book): bookNo(book), units_sold(0), sellingprice(0), salesprice(0), discount(0) { }
class Person
{
private:
string strName;
string strAddress;
public:
Person() = default;
Person(const string &name, const string &add)
{
strName = name;
strAddress = add;
}
Person(std::istream &is) {is >> *this;}
string getName() const { return strName; }
string getAddress() const { return strAddress; }
};
在类的定义中,可以包含0个或者多个访问说明符,并且对于某个访问说明符能出现多少次以及可能出现在哪里都没有严格规定。每个访问说明符指定接下来的成员的访问级别,有效范围直到出现下一个访问说明符或者到达类的结尾为止。
一般来说,作为接口的一部分,构造函数和一部分成员函数应该定义在public说明符之后,而数据成员作为实现部分的函数则应该跟在private说明符之后。
练习7.17:使用class和struct时有区别吗?如果有,是什么?
有区别。类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类的定义方式。如果使用struct关键字,则定义在第一个访问说明符之前的成员是public的;相反,如果使用的是class关键字,则这些成员是private的。
练习7.18:封装是何含义?它有什么用处?
封装是指保护类的成员不被随意访问的能力。通过把类的实现细节设置为private,我们就能完成类的封装。封装实现了类的接口和实现的分离。
封装有两个重要的优点:一是确保用户代码不会无意间破坏封装对象的状态;二是被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
练习7.19:在你的Person类中,你将把哪些成员声明成public的?哪些声明成private的?解释你这样做的原因。
把数据成员strName和strAddress设置为private,这样可以避免用户程序不经意间修改和破坏它们;同时把构造函数和两个获取成员的接口函数设置为public,以便于我们在类的外部访问。
练习7.20:友元在什么时候有用?请分别列举出使用友元的利弊。
当非成员函数确实需要访问类的私有成员时,我们可以把它声明成该类的友元。此时,友元可以工作在类的内部,像类的成员一样访问类的所有数据和函数。但是一旦使用不慎,就有可能破坏类的封装性。
练习7.21:修改你的Sales_data类使其隐藏实现的细节。你之前编写的关于Sales_data操作的程序应该继续使用,借助类的新定义重新编译该程序,确保其工作正常。
class Sales_data
{
friend Sales_data add(const Sales_data &lhs, const Sales_data &rhs);
friend std::istream &read(std::istream &is, Sales_data &item);
friend std::ostream &print(std::ostream &os, const Sales_data &item);
public:
Sales_data() = default;
Sales_data(const std::string &book): bookNo(book) { }
Sales_data(const std::string &book, const unsigned num, const double sellp, const double salep);
Sales_data(std::istream &is) {is >>*this; }
private:
std::string bookNo;
unsigned units_sold = 0;
double sellingprice = 0.0;
double salesprice = 0.0;
double discount = 0.0;
};
class Person
{
private:
string strName;
string strAddress;
public:
Person() = default;
Person(const string &name, const string &add)
{
strName = name;
strAddress = add;
}
Person(std::istream &is) {is >> *this;}
public:
string getName() const { return strName; }
string getAddress() const { return strAddress; }
};
class Screen
{
private:
unsigned height = 0, width = 0;
unsigned cursor = 0;
string contents;
};
class Screen
{
private:
unsigned height = 0, width = 0;
unsigned cursor = 0;
string contents;
public:
Screen() = default;
Screen(unsigned ht, unsigned wd) : height(ht), width(wd), contents(ht * wd, ' ') { }
Screen(unsigned ht, unsigned wd, char c) : height(ht), width(wd), contents(ht * wd, c) { }
};
Screen的4个数据成员都是内置类型(string类定义了拷贝和赋值运算符),因此可以直接使用类对象执行拷贝和赋值操作。
练习7.26:将Sales_data::avg_price定义成内联函数。
隐式内联:
class Sales_data
{
public:
double avg_price() const
{
if (units_sold)
return revenue / units_sold;
else
return 0;
}
};
限时内联:
class Sales_data
{
double avg_price() const;
};
inline double Sales_data::avg_price() const
{
if (units_sold)
return revenue / units_sold;
else
return 0;
}
Screen myScreen(5, 5, 'X');
myScreen.move(4, 0).set('#').display(cout);
cout << "\n";
myScreen.display(cout);
cout<< "\n";
class Screen
{
private:
unsigned height = 0, width = 0;
unsigned cursor = 0;
string contents;
public:
Screen() = default;
Screen(unsigned ht, unsigned wd) : height(ht), width(wd), contents(ht * wd, ' ') { }
Screen(unsigned ht, unsigned wd, char c) : height(ht), width(wd), contents(ht * wd, c) { }
public:
Screen& move(unsigned r, unsigned c)
{
cursor = r * width + c;
return *this;
}
Screen& set(char ch)
{
contents[cursor] = ch;
return *this;
}
Screen& set(unsigned r, unsigned c, char c)
{
contents[r * width + c] = ch;
return *this;
}
Screen& display()
{
cout << contents;
return *this;
}
};
如果返回类型为Screen,则上述函数各自只返回一个临时副本,不会改变myScreen的值。
练习7.30:通过this指针使用成员的做法虽然合法,但是有点多余。讨论显式地使用指针访问成员的优缺点。
通过this指针访问成员的优点是可以非常明确地指出访问的是对象的成员,并且可以在成员函数中使用与数据成员同名的形参;缺点是显得多余,代码不够简洁。
练习7.31:定义一对类X和Y,其中X包含一个指向Y的指针,而Y包含一个类型为X的对象。
class X; class Y { X x; }; class X { Y* y; };
#include
#include
using std::cout;
using std::endl;
using std::string;
class Window_mgr
{
public:
void clear();
};
class Screen
{
friend void Window_mgr::clear();
private:
unsigned height = 0, width = 0;
unsigned cursor = 0;
string contents;
public:
Screen() = default;
Screen(unsigned ht, unsigned wd, char c) : height(ht), width(wd), contents(ht * wd, c) { }
};
void Window_mgr::clear()
{
Screen myScreen(10, 20, 'X');
cout<<"Before clearing, th contents of myScreen is "<
pos Screen::size() const
{
return height * width;
}
如果添加如题目所示的size函数将会出现编译错误。因为该函数的返回类型pos本身定义在Screen类的内部,所以在类的外部无法直接使用pos。要想使用pos,需要在它的前面加上作用域Screen:: 修改后的程序是:
Screen::pos Screen::size() const
{
return height * width;
}
这样会导致编译出错,因为对pos的使用出现在它的声明之前,此时编译器并不知道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;
}
在Exercise类的内部,函数setVal和initVal用到的是Type都是Exercise内部声明的别名,对应的实际类型是double。在Exercise类的外部,定义Exercise::setVal函数时形参类型Type用的是Exercise内部定义的别名,对应double;返回类型Type用的是全局作用域的别名,对应string。使用的initVal函数是Exercise类内定义的版本。
编译程序在setVal处出现错误,此处定义的函数形参类型是double、返回类型是string,二者无法匹配。修改如下:
Exercise::Type Exercise::setVal(Type parm)
{
val = parm + initVal();
return val;
}
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");
}
Sales_data first_item(cin);使用了接受std::istream&参数的构造函数,该对象的成员值依赖于用户的输入。
Sales_data nest;使用了Sales_data的默认构造函数,其中string类型的成员bookNo默认初始化为空字符串,其他几个成员使用类内初始值初始化为0.
Sales_data last("9-999-99999-9");使用了接受const string&参数的构造函数,其中bookNo使用实参初始化为"9-999-99999-9",其他几个成员使用类内初始值初始化为0.
练习7.38:有些情况下我们希望提供cin作为接受istream参数的构造函数的默认实参,请声明这样的构造函数。
Sales_data(std::istream &is = std::cin) { is >> *this; }
这种行为不合法,如果为两个构造函数都赋予默认实参,则这两个构造函数都具有了默认构造函数的作用。一旦不提供任何实参地创建类的对象,则编译器无法判断这两个构造函数哪个更好,从而出现二义性错误。
练习7.40:从下面的抽象概念中选择一个,思考这样的类需要哪些数据成员,提供一组合理的构造函数并阐明这样做的原因。
(a) Book (b)Date (c)Employee (d)Vehicle (e)Object (f)Tree
选择Book,一本书通常包含书名、ISBN编号、定价、作者、出版社等信息,因此令其数据成员为:Name、ISBN、Price、Author、Publisher,其中Price是double类型,其他都是string类型。Book的构造函数有三个,一个默认构造函数,一个包含完整书籍信息的构造函数和一个接受用户输入的构造函数。其定义如下:
class Book
{
private:
string Name, ISBN, Author, Publisher;
double Price = 0;
public:
Book() = default;
Book(const string &n, const string &I, double pr, const string &a, const string &p)
{
Name = n;
ISBN = I;
Price = pr;
Author = a;
Publisher = p;
}
Book(std::istream &is) {is >> *this;}
};
#include
#include
using std::cin;
using std::cout;
using std::endl;
using std::istream;
using std::ostream;
using std::string;
class Sales_data
{
friend istream &read(istream &is, Sales_data &item);
friend ostream &print(ostream &os, const Sales_data &item);
public:
Sales_data(const string &book, unsigned num, double sellp, double salep) : bookNo(book), units_sold(num), sellingprice(sellp), saleprice(salep)
{
if(sellingprice)
discount = saleprice / sellingprice;
cout<<"The constructor can receive booNo, units_sold, sellingprice, saleprice."<> item.bookNo >> item.units_sold >> item.sellingprice >> item.saleprice;
return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.bookNo <<" "<< item.units_sold <<" "<< item.sellingprice <<" "<< item.saleprice <<" "<< item.discount;
return os;
}
int main()
{
Sales_data first("978-121-15535-2", 85, 128, 109);
Sales_data second;
Sales_data third("978-121-15535-2");
Sales_data last(cin);
return 0;
}
class Book
{
private:
string Name, ISBN, Author, Publisher;
double Price = 0.0;
public:
Book(const string &n, const string &I, double pr, const string &a, const string &p) : Name(n), ISBN(I), Price(pr), Author(a), Publisher(p) { }
Book() : Book("", "", 0, "", "") { }
Book(std::istream &is) : Book() {is >> *this;}
};
定义及验证程序如下:
#include
using std::cout;
using std::endl;
class Nodefault
{
public:
Nodefault(int i)
{
val = i;
}
int val;
};
class C
{
public:
Nodefault nd;
C(int i = 0) : nd(i) { }
};
int main()
{
C c;
cout<
vector
不合法。这条语句的含义是创建一个vector对象vect,该对象包含10个元素,每个元素都是NoDefault类且执行默认初始化。然而,在类NoDefault的定义中没有涉及默认构造函数,所以所需的默认初始化过程无法执行,编译器会报错。
练习7.45:如果在上一个练习中定义的vector的元素类型是C,则声明合法吗?为什么?
合法,类型C定义了带参数的默认构造函数,它可以完成声明语句所需的默认初始化操作。
练习7.46:下面那些论断是不正确的?为什么?
(a) 一个类必须至少提供一个构造函数。
(b)默认构造函数是参数列表为空的构造函数。
(c)如果对于类来说不存在有意义的默认值,则类不应该提供默认构造函数。
(d)如果类没有定义默认构造函数,则编译器将为其生成一个并把每个数据成员初始化相应类型的默认值。
(a)错误,类可以不提供任何构造函数,这时编译器自动实现一个合成的默认构造函数。
(b)错误,如果某个构造函数包含若干实参,但是同时为这些形参都提供了默认实参,则该构造函数也具备默认构造函数的功能。
(c)错误,如果一个类没有默认构造函数,也就是说我们定义了该类的某些构造函数但是没有为其设计默认构造函数,则当编译器确实需要隐式地使用默认构造函数时,该类无法使用。所以一般情况下,都应该为类构建一个默认构造函数。
(d)错误,对于编译器合成的默认构造函数来说,类类型的成员执行各自所属类的默认构造函数,内置类型和符合类型的成员只对定义在全局作用域中的对象执行初始化。
练习7.47:说明接受一个string参数的Sales_data构造函数是否应该是explicit的,并解释这样做的优缺点。
接受一个string参数的Sales_data构造函数应该是explicit的,否则,编译器就有可能自动把一个string对象转换成Sales_data对象,这种做法显得有些随意,某些时候会违背程序员的初衷。
练习7.48:假定Sales_data的构造函数不是explicit的,则下述定义将执行什么样的操作?如果Sales_data的构造函数是explicit的,又会发生什么呢?
string null_isbn("9-999-99999-9");
Sales_data item1(null_isbn);
Sales_data item2("9-999-99999-9");
第一行创建了一个string对象,第二行和第三行都是调用Sales_data的构造函数,所以不论Sales_data的构造函数是不是explicit的,item1和item2都能被正确的创建,它们的bookNo成员都是9-999-99999-9000000000000000000000000000000000,其他成员都是0。
练习7.49:对于combine函数的三种不同声明,当我们调用i.combine(s)时分别发生什么情况?其中i是一个Sales_data,而s是一个string对象?
(a) Sales_data &combine(Sales_data);
(b)Sales_data &combine(Sales_data&);
(c)Sales_data &combine(const Sales_data&) const;
第一个正确,编译器首先用给定的string对象s自动创建一个Sales_data对象,然后这个新生成的临时对象传给combine的形参,函数正确执行并返回结果。
第二个错误。无法编译通过,因为combine函数的参数是一个非常量引用,而s是一个string对象,编译器用s自动创建一个Sales_data临时对象,但是这个新生成的临时对象无法传递给combine所需的非常量引用。
第三个错误,无法编译通过,因为把combine声明成了常量成员函数,所以该函数无法修改数据成员的值。
练习7.50:确定在你的Person类中是否有一些构造函数应该是explicit的。
Person类有3个构造函数,前两个构造函数接收的参数个数都不是1,所以他们不存在隐式转换的问题,当然也不必指定explicit.
Person类的最后一个构造函数Person(std::istream &is);只接受一个参数,默认情况下它会把读入的数据自动转换成Person对象,可以明确指定。在其他情况下则不希望自动类型转换的发生。所以应该把这个构造函数指定为explicit的。
练习7.51:vector将其单参数构造函数定义成explicit的,而string则不是,你觉得原因何在?
string接受的单参数是const char*类型,如果我们得到了一个常量指针,则把它看做string对象是自然而然的过程,编译器自动把参数类型转换成类类型也非常符合逻辑,因此我们无须指定为explicit。
与string相反,vector接受的单参数是int类型,这个参数的原意是指定vector的容量。如果我们在本来需要vector的地方提供一个int值并且希望这个int值自动转换成vector,则这个过程显得比较牵强,因此把vector的单参数构造函数定义成explicit的更加合理。
练习7.52:使用2.6.1节的sales_data类,解释下面的初始化过程。如果存在任何问题,尝试修改它。
slaes_data item = {"978-0590353403", 25, 25.99};
这个程序相对item进行聚合类初始化操作,用花括号内的值初始化item的数据成员。然而实际过程与程序的愿意不符合,编译器会出错。聚合类要满足没有类内初始值这一条件,因此去掉类内初始值,就可以正常运行了。
练习7.54:Debug中以set_开头的成员应该被声明成constexpr吗?如果不,为什么?
这些以set_开头的成员不能声明成constexpr,这些函数的作用是设置数据成员的值,而constexpr函数只能包含return语句,不允许执行其他任务。
练习7.55:7.5.5节的Data类是字面值常量吗?请解释原因。
因为Data类是聚合类,所以它也是一个字面值常量。
练习7.56:什么是类的静态成员?它有何优点?静态成员与普通成员有何区别?
静态成员是指声明语句之前带有关键字static的类成员,静态成员不是任意单独对象的组成部分,而是由该类的全体对象所共享。
静态成员的优点包括:可以是私有成员,而全局对象不可以;通过阅读程序可以非常容易地看出静态成员与特定类关联,使得程序的含义清晰明了。
静态成员与普通成员的区别主要体现在普通成员与类的对象关联,是某个具体对象的组成部分;而静态成员从不属于任何具体的对象,它由该类的所有对象共享。静态成员可以作为默认实参,而普通数据成员不可以。
练习7.58:下面的静态数据成员的声明和定义有错误吗?请解释原因。
//example.h
class Example
{
public:
static double rate = 6.5;
static const int vecSize = 20;
static vector vec(vecSzie);
};
//example.c
#include "example.h"
double Example::rate;
vector Example::vec;
在类的内部,rate和vec的初始化是错误的,因为除了静态常量成员之外,其他静态成员不能在类的内部初始化。另外,example.c文件的两条语句也是错误的,在这里必须给出静态成员的初始值。