专栏C++学习笔记
《C++ Primer》学习笔记/习题答案 总目录
——————————————————————————————————————————————————————
📚💻 Cpp-Prime5 + Cpp-Primer-Plus6 源代码和课后题
第7章 - 对象和类
练习1.20
在网站 http://www.informit.com/title/032174113 上,第1章的代码目录包含了头文件 Sales_item.h
。将它拷贝到你自己的工作目录中。用它编写一个程序,读取一组书籍销售记录,将每条记录打印到标准输出上。
解:
Sales_item.h
:
/*
* This file contains code from "C++ Primer, Fifth Edition", by Stanley B.
* Lippman, Josee Lajoie, and Barbara E. Moo, and is covered under the
* copyright and warranty notices given in that book:
*
* "Copyright (c) 2013 by Objectwrite, Inc., Josee Lajoie, and Barbara E. Moo."
*
*
* "The authors and publisher have taken care in the preparation of this book,
* but make no expressed or implied warranty of any kind and assume no
* responsibility for errors or omissions. No liability is assumed for
* incidental or consequential damages in connection with or arising out of the
* use of the information or programs contained herein."
*
* Permission is granted for this code to be used for educational purposes in
* association with the book, given proper citation if and when posted or
* reproduced.Any commercial use of this code requires the explicit written
* permission of the publisher, Addison-Wesley Professional, a division of
* Pearson Education, Inc. Send your request for permission, stating clearly
* what code you would like to use, and in what specific way, to the following
* address:
*
* Pearson Education, Inc.
* Rights and Permissions Department
* One Lake Street
* Upper Saddle River, NJ 07458
* Fax: (201) 236-3290
*/
/* This file defines the Sales_item class used in chapter 1.
* The code used in this file will be explained in
* Chapter 7 (Classes) and Chapter 14 (Overloaded Operators)
* Readers shouldn't try to understand the code in this file
* until they have read those chapters.
*/
#ifndef SALESITEM_H
// we're here only if SALESITEM_H has not yet been defined
#define SALESITEM_H
#include "Version_test.h"
// Definition of Sales_item class and related functions goes here
#include <iostream>
#include <string>
class Sales_item {
// these declarations are explained section 7.2.1, p. 270
// and in chapter 14, pages 557, 558, 561
friend std::istream& operator>>(std::istream&, Sales_item&);
friend std::ostream& operator<<(std::ostream&, const Sales_item&);
friend bool operator<(const Sales_item&, const Sales_item&);
friend bool
operator==(const Sales_item&, const Sales_item&);
public:
// constructors are explained in section 7.1.4, pages 262 - 265
// default constructor needed to initialize members of built-in type
#if defined(IN_CLASS_INITS) && defined(DEFAULT_FCNS)
Sales_item() = default;
#else
Sales_item() : units_sold(0), revenue(0.0) { }
#endif
Sales_item(const std::string &book) :
bookNo(book), units_sold(0), revenue(0.0) { }
Sales_item(std::istream &is) { is >> *this; }
public:
// operations on Sales_item objects
// member binary operator: left-hand operand bound to implicit this pointer
Sales_item& operator+=(const Sales_item&);
// operations on Sales_item objects
std::string isbn() const { return bookNo; }
double avg_price() const;
// private members as before
private:
std::string bookNo; // implicitly initialized to the empty string
#ifdef IN_CLASS_INITS
unsigned units_sold = 0; // explicitly initialized
double revenue = 0.0;
#else
unsigned units_sold;
double revenue;
#endif
};
// used in chapter 10
inline
bool compareIsbn(const Sales_item &lhs, const Sales_item &rhs)
{
return lhs.isbn() == rhs.isbn();
}
// nonmember binary operator: must declare a parameter for each operand
Sales_item operator+(const Sales_item&, const Sales_item&);
inline bool
operator==(const Sales_item &lhs, const Sales_item &rhs)
{
// must be made a friend of Sales_item
return lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue &&
lhs.isbn() == rhs.isbn();
}
inline bool
operator!=(const Sales_item &lhs, const Sales_item &rhs)
{
return !(lhs == rhs); // != defined in terms of operator==
}
// assumes that both objects refer to the same ISBN
Sales_item& Sales_item::operator+=(const Sales_item& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
// assumes that both objects refer to the same ISBN
Sales_item
operator+(const Sales_item& lhs, const Sales_item& rhs)
{
Sales_item ret(lhs); // copy (|lhs|) into a local object that we'll return
ret += rhs; // add in the contents of (|rhs|)
return ret; // return (|ret|) by value
}
std::istream&
operator>>(std::istream& in, Sales_item& s)
{
double price;
in >> s.bookNo >> s.units_sold >> price;
// check that the inputs succeeded
if (in)
s.revenue = s.units_sold * price;
else
s = Sales_item(); // input failed: reset object to default state
return in;
}
std::ostream&
operator<<(std::ostream& out, const Sales_item& s)
{
out << s.isbn() << " " << s.units_sold << " "
<< s.revenue << " " << s.avg_price();
return out;
}
double Sales_item::avg_price() const
{
if (units_sold)
return revenue / units_sold;
else
return 0;
}
#endif
Version_test.h
:
/*
* This file contains code from "C++ Primer, Fifth Edition", by Stanley B.
* Lippman, Josee Lajoie, and Barbara E. Moo, and is covered under the
* copyright and warranty notices given in that book:
*
* "Copyright (c) 2013 by Objectwrite, Inc., Josee Lajoie, and Barbara E. Moo."
*
*
* "The authors and publisher have taken care in the preparation of this book,
* but make no expressed or implied warranty of any kind and assume no
* responsibility for errors or omissions. No liability is assumed for
* incidental or consequential damages in connection with or arising out of the
* use of the information or programs contained herein."
*
* Permission is granted for this code to be used for educational purposes in
* association with the book, given proper citation if and when posted or
* reproduced. Any commercial use of this code requires the explicit written
* permission of the publisher, Addison-Wesley Professional, a division of
* Pearson Education, Inc. Send your request for permission, stating clearly
* what code you would like to use, and in what specific way, to the following
* address:
*
* Pearson Education, Inc.
* Rights and Permissions Department
* One Lake Street
* Upper Saddle River, NJ 07458
* Fax: (201) 236-3290
*/
#ifndef VERSION_TEST_H
#define VERSION_TEST_H
/* As of the first printing of C++ Primer, 5th Edition (July 2012),
* the Microsoft Complier did not yet support a number of C++ 11 features.
*
* The code we distribute contains both normal C++ code and
* workarounds for missing features. We use a series of CPP variables to
* determine whether a given features is implemented in a given release
* of the MS compiler. The base version we used to test the code in the book
* is Compiler Version 17.00.50522.1 for x86.
*
* When new releases are available we will update this file which will
* #define the features implmented in that release.
*/
#if _MSC_FULL_VER == 170050522 || _MSC_FULL_VER == 170050727
// base version, future releases will #define those features as they are
// implemented by Microsoft
/* Code in this delivery use the following variables to control compilation
Variable tests C++ 11 Feature
CONSTEXPR_VARS constexpr variables
CONSTEXPR_FCNS constexpr functions
CONSTEXPR_CTORS constexpr constructors and other member functions
DEFAULT_FCNS = default
DELETED_FCNS = delete
FUNC_CPP __func__ local static
FUNCTION_PTRMEM function template with pointer to member function
IN_CLASS_INITS in class initializers
INITIALIZER_LIST library initializer_list<T> template
LIST_INIT list initialization of ordinary variables
LROUND lround function in cmath
NOEXCEPT noexcept specifier and noexcept operator
SIZEOF_MEMBER sizeof class_name::member_name
TEMPLATE_FCN_DEFAULT_ARGS default template arguments for function templates
TYPE_ALIAS_DECLS type alias declarations
UNION_CLASS_MEMS unions members that have constructors or copy control
VARIADICS variadic templates
*/
#endif // ends compiler version check
#ifndef LROUND
inline long lround(double d)
{
return (d >= 0) ? long(d + 0.5) : long(d - 0.5);
}
#endif
#endif // ends header guard
#include<iostream>
#include"Sales_item.h"
int main()
{
Sales_item book;
std::cout << "请输入销售记录:" << std::endl;
while (std::cin >> book){
std::cout << "ISBN、售出本数、销售额和平均售价为"
<< book << std::endl;
}
system("pause");
return 0;
}
练习1.21
编写程序,读取两个 ISBN
相同的 Sales_item
对象,输出他们的和。
解:
#include<iostream>
#include"Sales_item.h"
int main()
{
Sales_item trans1, trans2;
std::cout << "请输入两条ISBN相同的销售记录:" << std::endl;
std::cin >> trans1 >> trans2;
if (compareIsbn(trans1, trans2))
std::cout << "汇总信息:ISBN、售出本数、销售额和平均售价为"
<< trans1 + trans2 << std::endl;
else
std::cout << "两条销售记录的ISBN不同" << std::endl;
system("pause");
return 0;
}
练习1.22
编写程序,读取多个具有相同 ISBN
的销售记录,输出所有记录的和。
解:
#include<iostream>
#include"Sales_item.h"
int main(){
Sales_item total, trans;
std::cout << "请输入几条ISBN相同的销售记录:" << std::endl;
if (std::cin >> total){
while (std::cin >> trans)
if (compareIsbn(total, trans)){
total = total + trans;
}
else{
std::cout << "ISBN不同" << std::endl;
return -1;
}
std::cout << "汇总信息:ISBN、售出本数、销售额和平均售价为"
<< total << std::endl;
}
else{
std::cout << "没有数据" << std::endl;
return -1;
}
system("pause");
return 0;
}
练习1.23
编写程序,读取多条销售记录,并统计每个 ISBN(每本书)有几条销售记录。
解:
#include <iostream>
#include"Sales_item.h"
#include <fstream> // file I/O support
#include <cstdlib> // support for exit()
const int SIZE = 60;
int main()
{
using namespace std;
Sales_item trans1, trans2;
int num = 1;
char filename[SIZE];
ifstream inFile; // object for handling file input
cout << "Enter name of data file: ";
cin.getline(filename, SIZE);
inFile.open(filename); // associate inFile with a file
if (!inFile.is_open()) // failed to open file
{
cout << "Could not open the file " << filename << endl;
cout << "Program terminating.\n";
// cin.get(); // keep window open
exit(EXIT_FAILURE);
}
cout << "请输入几条ISBN相同的销售记录:" << endl;
if (inFile >> trans1){
while (inFile >> trans2)
if (compareIsbn(trans1, trans2))
num++;
else{
cout << trans1.isbn() << "共有"
<< num << "条销售记录" << endl;
trans1 = trans2;
num = 1;
}
cout << trans1.isbn() << "共有"
<< num << "条记录" << endl;
}
else{
cout << "没有数据" << endl;
return -1;
}
inFile.close(); // finished with the file
system("pause");
return 0;
}
练习1.24
输入表示多个 ISBN
的多条销售记录来测试上一个程序,每个 ISBN 的记录应该聚在一起。
解:
#include<iostream>
#include"Sales_item.h"
int main(){
Sales_item trans1, trans2;
int num = 1;
std::cout << "请输入几条ISBN相同的销售记录:" << std::endl;
if (std::cin >> trans1){
while (std::cin >> trans2)
if (compareIsbn(trans1, trans2))
num++;
else{
std::cout << trans1.isbn() << "共有"
<< num << "条销售记录" << std::endl;
trans1 = trans2;
num = 1;
}
std::cout << trans1.isbn() << "共有"
<< num << "条记录" << std::endl;
}
else{
std::cout << "没有数据" << std::endl;
return -1;
}
system("pause");
return 0;
}
练习1.25
借助网站上的Sales_item.h
头文件,编译并运行本节给出的书店程序。
解:
#include<iostream>
#include"Sales_item.h"
int main(){
Sales_item total, trans;
std::cout << "请输入几条ISBN相同的销售记录:" << std::endl;
if (std::cin >> total){
while (std::cin >> trans)
if (compareIsbn(total, trans)){
total = total + trans;
}
else{
std::cout << total << std::endl;
total = trans;
}
std::cout << total << std::endl;
}
else{
std::cerr << "没有数据" << std::endl;
return -1;
}
system("pause");
return 0;
}
第二章 变量和基本类型
练习2.39
编译下面的程序观察其运行结果,注意,如果忘记写类定义体后面的分号会发生什么情况?记录下相关的信息,以后可能会有用。
struct Foo { /* 此处为空 */ } // 注意:没有分号
int main()
{
return 0;
}
解:
运行结果:
该程序无法编译通过,原因是缺少了一个分号。因为类体后面可以紧跟变量名以示对该类型对象的定义,所以在类体右侧表示结束的花括号之后必须写一个分号。
稍作修改,该程序就可以编译通过了。
struct Foo { /* 此处为空 */ };
int main(){
return 0;
}
练习2.40
根据自己的理解写出 Sales_data
类,最好与书中的例子有所区别。
解:
原书中的程序包含3个数据成员,分别是 bookNo
(书籍编号)、units sold
(销售量)、revenue
(销售收入);
新设计的 Sales data
类细化了销售收入的计算方式,在保留 bookNo
和 units sold
的基础上,新增了 sellingprice
(零售价、原价)、saleprice
(实售价、折扣价)、discount
(折扣),其中 discount=saleprice/sellingprice
。
struct Sales_data{
std::string bookNo; //书籍编号
unsigned units_sold = 0; //销售量
double sellingprice = 0.0; //零售价
double saleprice = 0.0; //实售价
double discount = 0.0; //折扣
};
练习2.41
使用你自己的 Sale_data
类重写1.5.1节(第20页)、1.5.2节(第21页)和1.6节(第22页)的练习。眼下先把 Sales_data
类的定义和 main
函数放在一个文件里。
解:
#include<iostream>
#include<string>
using namespace std;
class Sales_data{
//友元函数
friend std::istream& operator >> (std::istream&, Sales_data&);
//友元函数
friend std::ostream& operator << (std::ostream&, const Sales_data&);
//友元函数
friend bool operator < (const Sales_data&, const Sales_data&);
//友元函数
friend bool operator == (const Sales_data&, const Sales_data&);
public://构造函数的3种形式
Sales_data() = default;
Sales_data(const std::string &book) : bookNo(book){}
Sales_data(std::istream &is){ is >> *this; }
public:
Sales_data& operator += (const Sales_data&);
std::string isbn() const {
return bookNo;
}
private:
std::string bookNo; //书籍编号,隐式初始化为空串
unsigned units_sold = 0; //销售量,显式初始化为0
double sellingprice = 0.0; //原始价格,显式初始化为0.0
double saleprice = 0.0; //实售价格,显式初始化为0.0
double discount = 0.0; //折扣,显式初始化为0.0
};
inline bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn();
}
Sales_data operator+(const Sales_data&, const Sales_data&);
inline bool operator == (const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.units_sold == rhs.units_sold &&
lhs.sellingprice == rhs.sellingprice &&
lhs.saleprice == rhs.saleprice &&
lhs.isbn() == rhs.isbn();
}
inline bool operator != (const Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs); //基于运算符==给出!=的定义
}
Sales_data& Sales_data::operator += (const Sales_data& rhs)
{
units_sold += rhs.units_sold;
saleprice = (rhs.saleprice*rhs.units_sold + saleprice*units_sold)
/ (rhs.units_sold + units_sold);
if (sellingprice != 0)
discount = saleprice / sellingprice;
return *this;
}
Sales_data operator + (const Sales_data& lhs, const Sales_data& rhs)
{
Sales_data ret(lhs); //把lhs的内容拷贝到临时变量ret中,这种做法便于运算
ret += rhs; //把rhs的内容加入其中
return ret; //返回ret
}
std::istream& operator >> (std::istream& in, Sales_data& s)
{
in >> s.bookNo >> s.units_sold >> s.sellingprice >> s.saleprice;
if (in && s.sellingprice != 0)
s.discount = s.saleprice / s.sellingprice;
else
s = Sales_data(); //输入错误,重置输入的数据
return in;
}
std::ostream& operator << (std::ostream& out, const Sales_data& s)
{
out << s.isbn() << " " << s.units_sold << " "
<< s.sellingprice << " " << s.saleprice << " " << s.discount;
return out;
}
int main(){
Sales_data book;
std::cout << "请输入销售记录:" << std::endl;
while (std::cin >> book){
std::cout << "ISBN、售出本数、原始价格、实售价格、折扣为" << book << std::endl;
}
Sales_data trans1, trans2;
std::cout << "请输入两条ISBN相同的销售记录:" << std::endl;
std::cin >> trans1 >> trans2;
if (compareIsbn(trans1, trans2))
std::cout << "汇总信息:ISBN、售出本数、原始价格、实售价格、折扣为"
<< trans1 + trans2 << std::endl;
else
std::cout << "两条销售记录的ISBN不同" << std::endl;
Sales_data total, trans;
std::cout << "请输入几条ISBN相同的销售记录:" << std::endl;
if (std::cin >> total){
while (std::cin >> trans)
if (compareIsbn(total, trans)) //ISBN相同
total = total + trans;
else{ //ISBN不同
std::cout << "当前书籍ISBN不同" << std::endl;
break;
}
std::cout << "有效汇总信息:ISBN、售出本数、原始价格、实售价格、折扣为"
<< total << std::endl;
}
else{
std::cout << "没有数据" << std::endl;
return -1;
}
int num = 1; //记录当前书籍的销售记录总数
std::cout << "请输入若干销售记录:" << std::endl;
if (std::cin >> trans1){
while (std::cin >> trans2)
if (compareIsbn(trans1, trans2)) //ISBN相同
num++;
else{ //ISBN不同
std::cout << trans1.isbn() << "共有"
<< num << "条销售记录" << std::endl;
trans1 = trans2;
num = 1;
}
std::cout << trans1.isbn() << "共有"
<< num << "条销售记录" << std::endl;
}
else{
std::cout << "没有数据" << std::endl;
return -1;
}
system("pause");
return 0;
}
练习2.42
根据你自己的理解重写一个 Sales_data.h
头文件,并以此为基础重做2.6.2节(第67页)的练习。
解:
Sales_data.h
:
#ifndef SALES_DATA_H_INCLUDED
#define SALES_DATA_H_INCLUDED
#include<iostream>
#include<string>
class Sales_data{
//友元函数
friend std::istream& operator >> (std::istream&, Sales_data&);
//友元函数
friend std::ostream& operator << (std::ostream&, const Sales_data&);
//友元函数
friend bool operator < (const Sales_data&, const Sales_data&);
//友元函数
friend bool operator == (const Sales_data&, const Sales_data&);
public://构造函数的3种形式
Sales_data() = default;
Sales_data(const std::string &book) : bookNo(book){}
Sales_data(std::istream &is){ is >> *this; }
public:
Sales_data& operator += (const Sales_data&);
std::string isbn() const {
return bookNo;
}
private:
std::string bookNo; //书籍编号,隐式初始化为空串
unsigned units_sold = 0; //销售量,显式初始化为0
double sellingprice = 0.0; //原始价格,显式初始化为0.0
double saleprice = 0.0; //实售价格,显式初始化为0.0
double discount = 0.0; //折扣,显式初始化为0.0
};
inline bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn();
}
Sales_data operator+(const Sales_data&, const Sales_data&);
inline bool operator == (const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.units_sold == rhs.units_sold &&
lhs.sellingprice == rhs.sellingprice &&
lhs.saleprice == rhs.saleprice &&
lhs.isbn() == rhs.isbn();
}
inline bool operator != (const Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs); //基于运算符==给出!=的定义
}
Sales_data& Sales_data::operator += (const Sales_data& rhs)
{
units_sold += rhs.units_sold;
saleprice = (rhs.saleprice*rhs.units_sold + saleprice*units_sold)
/ (rhs.units_sold + units_sold);
if (sellingprice != 0)
discount = saleprice / sellingprice;
return *this;
}
Sales_data operator + (const Sales_data& lhs, const Sales_data& rhs)
{
Sales_data ret(lhs); //把lhs的内容拷贝到临时变量ret中,这种做法便于运算
ret += rhs; //把rhs的内容加入其中
return ret; //返回ret
}
std::istream& operator >> (std::istream& in, Sales_data& s)
{
in >> s.bookNo >> s.units_sold >> s.sellingprice >> s.saleprice;
if (in && s.sellingprice != 0)
s.discount = s.saleprice / s.sellingprice;
else
s = Sales_data(); //输入错误,重置输入的数据
return in;
}
std::ostream& operator << (std::ostream& out, const Sales_data& s)
{
out << s.isbn() << " " << s.units_sold << " "
<< s.sellingprice << " " << s.saleprice << " " << s.discount;
return out;
}
#endif // SALES_DATA_H_INCLUDED
main.cpp
:
#include<iostream>
#include"Sales_data.h"
int main(){
Sales_data book;
std::cout << "请输入销售记录:" << std::endl;
while (std::cin >> book){
std::cout << "ISBN、售出本数、原始价格、实售价格、折扣为" << book << std::endl;
}
Sales_data trans1, trans2;
std::cout << "请输入两条ISBN相同的销售记录:" << std::endl;
std::cin >> trans1 >> trans2;
if (compareIsbn(trans1, trans2))
std::cout << "汇总信息:ISBN、售出本数、原始价格、实售价格、折扣为"
<< trans1 + trans2 << std::endl;
else
std::cout << "两条销售记录的ISBN不同" << std::endl;
Sales_data total, trans;
std::cout << "请输入几条ISBN相同的销售记录:" << std::endl;
if (std::cin >> total){
while (std::cin >> trans)
if (compareIsbn(total, trans)) //ISBN相同
total = total + trans;
else{ //ISBN不同
std::cout << "当前书籍ISBN不同" << std::endl;
break;
}
std::cout << "有效汇总信息:ISBN、售出本数、原始价格、实售价格、折扣为"
<< total << std::endl;
}
else{
std::cout << "没有数据" << std::endl;
return -1;
}
int num = 1; //记录当前书籍的销售记录总数
std::cout << "请输入若干销售记录:" << std::endl;
if (std::cin >> trans1){
while (std::cin >> trans2)
if (compareIsbn(trans1, trans2)) //ISBN相同
num++;
else{ //ISBN不同
std::cout << trans1.isbn() << "共有"
<< num << "条销售记录" << std::endl;
trans1 = trans2;
num = 1;
}
std::cout << trans1.isbn() << "共有"
<< num << "条销售记录" << std::endl;
}
else{
std::cout << "没有数据" << std::endl;
return -1;
}
system("pause");
return 0;
}
第七章 类
练习7.1
使用2.6.1节定义的 Sales_data
类为1.6节的交易处理程序编写一个新版本。
解:
#include<iostream>
#include"Sales_data.h"
using namespace std;
int main(){
cout << "请输入交易记录(ISBN、销售量、原价、实际售价):" << endl;
Sales_data total; //保存下一条交易记录的变量
//读入第一条交易记录,并确保有数据可以处理
if (cin >> total){
Sales_data trans; //保存和的变量
//读入并处理剩余交易记录
while(cin >> trans){
//如果我们仍在处理相同的书
if (total.isbn() == trans.isbn())
total += trans; //更新总销售额
else{
//打印前一本书的结果
cout << total << endl;
total = trans; //total现在表示下一本书的销售额
}
}
cout << total << endl; //打印最后一本书的结果
}
else{
//没有输入!警告读者
cerr << "No data?!" << endl;
return -1; // 表示失败
}
system("pause");
return 0;
}
练习7.2
曾在2.6.2节的练习中编写了一个 Sales_data
类,请向这个类添加 combine
函数和 isbn
成员。
解:
添加 combine
和 isbn
成员后的 sales data
类是:
class Sales_data{
private:
std::string bookNo; //书籍编号,隐式初始化为空串
unsigned units_sold = 0; //销售量,显式初始化为0
double sellingprice = 0.0; //原始价格,显式初始化为0.0
double saleprice = 0.0; //实售价格,显式初始化为0.0
double discount = 0.0; //折扣,显式初始化为0.0
public: //定义公有函数成员
//isbn函数只有一条语句,返回bookNo
string isbn() const { return bookNo; }
//combine函数用于把两个ISBN相同的销售记录合并在一起
Sales_data& combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold; //累加书籍的销售量
saleprice = (rhs.saleprice*rhs.units_sold + saleprice*units_sold)
/ (rhs.units_sold + units_sold); //重新计算实际销售价格
if (sellingprice != 0)
discount = saleprice / sellingprice;//重新计算实际折扣
return *this; //返回合并后的结果
}
}
练习7.3
修改7.1.1节的交易处理程序,令其使用这些成员。
解:
#include<iostream>
#include"Sales_data.h"
using namespace std;
int main(){
Sales_data total, trans;
cout << "请输入交易记录(ISBN、销售量、原价、实际售价):" << endl;
if (cin >> total){
while (cin >> trans)
if (total.isbn() == trans.isbn()){
total.combine(trans);
}
else{
cout << total << endl;
total = trans;
}
cout << total << endl;
}
else{
cerr << "没有数据" << endl;
return -1;
}
system("pause");
return 0;
}
练习7.4
编写一个名为 Person
的类,使其表示人员的姓名和地址。使用 string
对象存放这些元素,接下来的练习将不断充实这个类的其他特征。
解:
满足题意的 Person
类是:
#include <string>
class Person {
private:
string strName; //姓名
string strAddress; //地址
};
练习7.5
在你的Person
类中提供一些操作使其能够返回姓名和地址。
这些函数是否应该是const
的呢?解释原因。
解:
修改后的 Person
类是:
#include <string>
class Person {
private:
string strName; //姓名
string strAddress; //地址
public:
string getName() const { return strName; } //返回姓名
string getAddress() const { return strAddress; } //返回地址
};
上述两个函数应该被定义成常量成员函数,因为不论返回姓名还是返回地址,在函数体内都只是读取数据成员的值,而不会做任何改变。
练习7.6
对于函数 add
、read
和 print
,定义你自己的版本。
解:
满足题意的 add
、read
和 print
函数分别如下所示:
#include <string>
#include <iostream>
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
std::istream &read(std::istream &is, Sales_data &item)
{
is >> item.bookNo >> item.units_sold >> item.sellingprice
>> item.saleprice;
if (is && item.sellingprice != 0)
item.discount = item.saleprice / item.sellingprice;
else
item = Sales_data(); //输入错误,重置输入的数据
return is;
}
std::ostream &print(std::ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " " << item.sellingprice
<< " " << item.saleprice << "" << item.discount;
return os;
}
练习7.7
使用这些新函数重写7.1.2节练习中的程序。
解:
用 read
函数替代 >>
,print
函数替代 <<
,add
函数替代 combine
函数。
#include<iostream>
#include"Sales_data.h"
using namespace std;
int main(){
Sales_data total, trans;
cout << "请输入交易记录(ISBN、销售量、原价、实际售价):" << endl;
if (read(cin, total)){
while (read(cin, trans))
if (total.isbn() == trans.isbn()){
total = add(total, trans);
}
else{
print(cout, total);
cout << endl;
total = trans;
}
print(cout, total);
cout << endl;
}
else{
cerr << "没有数据" << endl;
return -1;
}
system("pause");
return 0;
}
练习7.8
为什么 read
函数将其 Sales_data
参数定义成普通的引用,而 print
函数将其参数定义成常量引用?
解:
read
函数将其Sales_data
参数定义成普通的引用,是因为我们需要从标准输入流中读取数据并将其写入到给定的Sales_data
对象,因此需要有修改对象的权限。- 而
print
将其参数定义成常量引用是因为它只负责数据的输出,不对其做任何更改。
练习7.9
对于7.1.2节练习中代码,添加读取和打印 Person
对象的操作。
解:
满足题意的 read
和 print
函数如下所示:
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;
}
练习7.10
在下面这条 if
语句中,条件部分的作用是什么?
if (read(read(cin, data1), data2))
解:
read
函数的返回类型是 std::istream&
,是引用,所以 read(cin, data1)
的返回值可以继续作为外层 read
函数的实参使用。该条件检验读入 data1
和 data2
的过程是否正确,如果正确,条件满足;否则条件不满足。
练习7.11 :
在你的 Sales_data
类中添加构造函数,
然后编写一段程序令其用到每个构造函数。
解:
头文件:
#ifndef SALES_DATA_H_INCLUDED
#define SALES_DATA_H_INCLUDED
#include<iostream>
#include<string>
class Sales_data{
//友元函数
friend std::istream& operator >> (std::istream&, Sales_data&);
//友元函数
friend std::ostream& operator << (std::ostream&, const Sales_data&);
//友元函数
friend bool operator < (const Sales_data&, const Sales_data&);
//友元函数
friend bool operator == (const Sales_data&, const Sales_data&);
public://构造函数的3种形式
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; }
public:
Sales_data& operator += (const Sales_data&);
std::string isbn() const {
return bookNo;
}
//combine函数用于把两个ISBN相同的销售记录合并在一起
Sales_data& combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold; //累加书籍的销售量
saleprice = (rhs.saleprice*rhs.units_sold + saleprice*units_sold)
/ (rhs.units_sold + units_sold); //重新计算实际销售价格
if (sellingprice != 0)
discount = saleprice / sellingprice;//重新计算实际折扣
return *this; //返回合并后的结果
}
//private:
public:
std::string bookNo; //书籍编号,隐式初始化为空串
unsigned units_sold = 0; //销售量,显式初始化为0
double sellingprice = 0.0; //原始价格,显式初始化为0.0
double saleprice = 0.0; //实售价格,显式初始化为0.0
double discount = 0.0; //折扣,显式初始化为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;
}
inline bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn();
}
Sales_data operator+(const Sales_data&, const Sales_data&);
inline bool operator == (const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.units_sold == rhs.units_sold &&
lhs.sellingprice == rhs.sellingprice &&
lhs.saleprice == rhs.saleprice &&
lhs.isbn() == rhs.isbn();
}
inline bool operator != (const Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs); //基于运算符==给出!=的定义
}
Sales_data& Sales_data::operator += (const Sales_data& rhs)
{
units_sold += rhs.units_sold;
saleprice = (rhs.saleprice*rhs.units_sold + saleprice*units_sold)
/ (rhs.units_sold + units_sold);
if (sellingprice != 0)
discount = saleprice / sellingprice;
return *this;
}
Sales_data operator + (const Sales_data& lhs, const Sales_data& rhs)
{
Sales_data ret(lhs); //把lhs的内容拷贝到临时变量ret中,这种做法便于运算
ret += rhs; //把rhs的内容加入其中
return ret; //返回ret
}
std::istream& operator >> (std::istream& in, Sales_data& s)
{
in >> s.bookNo >> s.units_sold >> s.sellingprice >> s.saleprice;
if (in && s.sellingprice != 0)
s.discount = s.saleprice / s.sellingprice;
else
s = Sales_data(); //输入错误,重置输入的数据
return in;
}
std::ostream& operator << (std::ostream& out, const Sales_data& s)
{
out << s.isbn() << " " << s.units_sold << " "
<< s.sellingprice << " " << s.saleprice << " " << s.discount;
return out;
}
#endif // SALES_DATA_H_INCLUDED
在类的定义中,我们设计了4个构造函数:
- 第一个构造函数是默认构造函数,它使用了C++11新标准提供的
=default
。它的参数列表为空,即不需要我们提供任何数据也能构造一个对象。 - 第二个构造函数只接受一个
const strings
,表示书籍的ISBN
编号,编译器赋予其他数据成员类内初始值。 - 第三个构造函数接受完整的销售记录信息,
const strings
表示书籍的ISBN
编号,const unsigned
表示销售量,后面两个const double
分别表示书籍的原价和实际售价。 - 最后一个构造函数接受
istreams
并从中读取书籍的销售信息。
我们在 main
函数中创建4个 sales_data
对象并依次输出其内容,上面定义的构造函数各被用到了一次。
主函数:
#include <iostream>
#include "Sales_data.h"
int main()
{
using namespace std;
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 << "书籍的销售情况是:" << endl;
cout << data1 << "\n" << data2 << "\n" << data3 << "\n" << data4 << endl;
system("pause");
return 0;
}
练习7.12
把只接受一个 istream
作为参数的构造函数移到类的内部。
解:
按照题目要求,把只接受一个 istream
作为参数的构造函数定义到类的内部之后,类的形式如下所示:
class Sales_data
{
public: //构造函数的4种形式
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; }
public:
std::string bookNo; //书籍编号,隐式初始化为空串
unsigned units_sold = 0; //销售量,显式初始化为0
double sellingprice = 0.0; //原始价格,显式初始化为0.0
double saleprice = 0.0; //实售价格,显式初始化为0.0
double discount = 0.0; //折扣,显式初始化为0.0
};
练习7.13
使用 istream
构造函数重写第229页的程序。
解:
#include<iostream>
#include"Sales_data.h"
using namespace std;
int main(){
Sales_data total(cin);
if (cin){
Sales_data trans(cin);
while (read(cin, trans))
if (total.isbn() == trans.isbn()){
total = add(total, trans);
}
else{
print(cout, total);
cout << endl;
total = trans;
}
print(cout, total);
cout << endl;
}
else{
cerr << "没有数据" << endl;
return -1;
}
system("pause");
return 0;
}
练习7.14
编写一个构造函数,令其用我们提供的类内初始值显式地初始化成员。
解:
使用初始值列表的构造函数是:
Sales_data(const std::string &book)
:bookNo(book), units_sold(0), sellingprice(0), saleprice(0), discount(0) { }
练习7.15
为你的 Person
类添加正确的构造函数。
解:
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; } //返回地址
};
练习7.16
在类的定义中对于访问说明符出现的位置和次数有限定吗?
如果有,是什么?什么样的成员应该定义在 public
说明符之后?
什么样的成员应该定义在 private
说明符之后?
解:
在类的定义中,可以包含0个或者多个访问说明符,并且对于某个访问说明符能出现多少次以及能出现在哪里都没有严格规定。每个访问说明符指定接下来的成员的访问级别,有效范围直到出现下一个访问说明符或者到达类的结尾为止。
一般来说,作为接口的一部分,
- 构造函数和一部分成员函数应该定义在
public
说明符之后, - 而数据成员和作为实现部分的函数则应该跟在
private
说明符之后。
练习7.17
使用 class
和 struct
时有区别吗?如果有,是什么?
解:
class
和 struct
都可以用来声明类,它们的大多数功能都类似,唯一的区别是 默认访问权限不同。
类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式。
- 如果使用
struct
关键字,则定义在第一个访问说明符之前的成员是public
的; - 相反,如果使用
class
关键字,则这些成员是private
的。
练习7.18
封装是何含义?它有什么用处?
解:
封装是指保护类的成员不被随意访问的能力。通过把类的实现细节设置为 private
,我们就能完成类的封装。封装实现了类的接口和实现的分离。
如书中所述,封装有两个重要的优点:
- 一是确保用户代码不会无意间破坏封装对象的状态;
- 二是被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。
一旦把数据成员定义成 private
的,类的作者就可以比较自由地修改数据了。当实现部分发生改变时,只需要检查类的代码本身以确认这次改变有什么影响;换句话说,只要类的接口不变,用户代码就无须改变。
如果数据是 public
的,则所有使用了原来数据成员的代码都可能失效,这时我们必须定位并重写所有依赖于老版本实现的代码,之后才能重新使用该程序。
把数据成员的访问权限设成 private
还有另外一个好处,这么做能防止由于用户的原因造成数据被破坏。如果我们发现有程序缺陷破坏了对象的状态,则可以在有限的范围内定位缺陷:因为只有实现部分的代码可能产生这样的错误。因此,将错误的搜索限制在有限范围内将能极大地简化更改问题及修正程序等工作。
练习7.19
在你的 Person
类中,你将把哪些成员声明成 public
的?
哪些声明成 private
的?
解释你这样做的原因。
解:
根据封装的含义我们知道,作为接口的一部分,
- 构造函数和一部分成员函数应该定义在
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);
private:
std::string bookNo; //书籍编号,隐式初始化为空串
unsigned units_sold = 0; //销售量,显式初始化为0
double sellingprice = 0.0; //原始价格,显式初始化为0.0
double saleprice = 0.0; //实售价格,显式初始化为0.0
double discount = 0.0; //折扣,显式初始化为0.0
};
练习7.22
修改你的 Person
类使其隐藏实现的细节。
解:
到目前为止,我们设计的 Person
类包含两个数据成员、三个构造函数和两个用来获取数据的接口函数。显然,除了数据成员之外其他几个函数都有权被外部程序访问,所以我们通过把数据成员设置为 private
来确保类的封装性。
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; } //返回地址
};
练习7.23
编写你自己的 Screen
类型。
解:
对于 Screen
类来说,必不可少的数据成员有:屏幕的宽度和高度、屏幕的内容以及光标的当前位置,这与书中的示例是一致的。因此,仅包含数据成员的 Screen
类是:
class Screen {
private:
unsigned height=0, width=0;
unsigned cursor=0;
string contents;
}
练习7.24
给你的 Screen
类添加三个构造函数:一个默认构造函数;另一个构造函数接受宽和高的值,然后将 contents
初始化成给定数量的空白;第三个构造函数接受宽和高的值以及一个字符,该字符作为初始化后屏幕的内容。
解:
使用构造函数的列表初始值执行初始化操作,添加构造函数之后的 Screen
类是:
class Screen {
private:
unsigned height = 0, width = 0;
unsigned cursor = 0;
string contents;
public:
Screen() = default; // 1
Screen(unsigned ht, unsigned wd) :
height(ht), width(wd), contents(ht*wd, ' '){ } // 2
Screen(unsigned ht, unsigned wd, char c) :
height(ht), width(wd), contents(ht*wd, c){ } // 3
};
练习7.25
Screen
能安全地依赖于拷贝和赋值操作的默认版本吗?
如果能,为什么?如果不能?为什么?
解:
含有指针数据成员的类一般不宜使用默认的拷贝和赋值操作,如果类的数据成员都是内置类型的,则不受干扰。
Screen
的4个数据成员都是内置类型(string
类定义了拷贝和赋值运算符),因此直接使用类对象执行拷贝和赋值操作是可以的。
练习7.26
将 Sales_data::avg_price
定义成内联函数。
解:
- 隐式内联,把
avg_price
函数的定义放在类的内部:
class Sales_data
{
public:
double avg_price() const
{
if(units_sold)
return revenue/units_sold;
else
return 0;
}
}
- 显式内联,把
avg_price
函数的定义放在类的外部,并且指定inline:
class Sales_data
{
double avg_price() const;
}
inline double Sales_data::avg_price() const
{
if(units_sold)
return revenue/units_sold;
else
return 0;
}
练习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";
解:
添加 move
、set
和 display
函数之后,新的 Screen
类是:
#include "Version_test.h"
#include <string>
#include <iostream>
class Screen {
public:
typedef std::string::size_type pos;
#if defined(IN_CLASS_INITS) && defined(DEFAULT_FCNS)
Screen() = default; // needed because Screen has another constructor
#else
Screen() : cursor(0), height(0), width(0) { }
#endif
// cursor initialized to 0 by its in-class initializer
Screen(pos ht, pos wd, char c) : height(ht), width(wd),
contents(ht * wd, c) { }
friend class Window_mgr;
Screen(pos ht = 0, pos wd = 0) :
cursor(0), height(ht), width(wd), contents(ht * wd, ' ') { }
char get() const // get the character at the cursor
{
return contents[cursor];
} // implicitly inline
inline char get(pos ht, pos wd) const; // explicitly inline
Screen &clear(char = bkground);
private:
static const char bkground = ' ';
public:
Screen &move(pos r, pos c); // can be made inline later
Screen &set(char);
Screen &set(pos, pos, char);
// other members as before
// display overloaded on whether the object is const or not
Screen &display(std::ostream &os)
{
do_display(os); return *this;
}
const Screen &display(std::ostream &os) const
{
do_display(os); return *this;
}
private:
// function to do the work of displaying a Screen
void do_display(std::ostream &os) const { os << contents; }
// other members as before
private:
#ifdef IN_CLASS_INITS
pos cursor = 0;
pos height = 0, width = 0;
#else
pos cursor;
pos height, width;
#endif
std::string contents;
};
Screen &Screen::clear(char c)
{
contents = std::string(height*width, c);
return *this;
}
inline // we can specify inline on the definition
Screen &Screen::move(pos r, pos c)
{
pos row = r * width; // compute the row location
cursor = row + c; // move cursor to the column within that row
return *this; // return this object as an lvalue
}
char Screen::get(pos r, pos c) const // declared as inline in the class
{
pos row = r * width; // compute row location
return contents[row + c]; // return character at the given column
}
inline Screen &Screen::set(char c)
{
contents[cursor] = c; // set the new value at the current cursor location
return *this; // return this object as an lvalue
}
inline Screen &Screen::set(pos r, pos col, char ch)
{
contents[r*width + col] = ch; // set specified location to given value
return *this; // return this object as an lvalue
}
测试代码:
#include<iostream>
#include"Screen.h"
using namespace std;
int main()
{
Screen myScreen(5, 5, 'X');
myScreen.move(4, 0).set('#').display(std::cout);
std::cout << "\n";
myScreen.display(std::cout);
std::cout << "\n";
system("pause");
return 0;
}
练习7.28
如果 move
、set
和 display
函数的返回类型不是 Screen&
而是 Screen
,则在上一个练习中将会发生什么?
解:
- 函数的返回值如果是引用,则表明函数返回的是对象本身;
- 函数的返回值如果不是引用,则表明函数返回的是对象的副本。
返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本。如果我们把一系列这样的操作连接在一起的话,所有这些操作将在同一个对象上执行。
在上一个练习中,move
、set
和 display
函数的返回类型都是 Screen&
,表示我们首先移动光标至(4,0)位置,然后将该位置的字符修改为 #
,最后输出 myscreen
的内容。
相反,如果我们把 move
、set
和 display
函数的返回类型改成 Screen
,则上述函数各自只返回一个临时副本,不会改变 myScreen
的值。
练习7.29
修改你的 Screen
类,令 move
、set
和 display
函数返回 Screen
并检查程序的运行结果,在上一个练习中你的推测正确吗?
解:
推测正确。
# with '&'
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXX#XXXX
^
# without '&'
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXXXXXXX
^
练习7.30
通过 this
指针使用成员的做法虽然合法,但是有点多余。讨论显示使用指针访问成员的优缺点。
解:
通过 this
指针访问成员的
- 优点是可以非常明确地指出访问的是对象的成员,并且可以在成员函数中使用与数据成员同名的形参;
- 缺点是显得多余,代码不够简洁。
练习7.31
定义一对类 X
和 Y
,其中 X
包含一个指向 Y
的指针,而 Y
包含一个类型为 X
的对象。
解:
class X;
class Y{
X* x;
};
class X{
Y y;
};
类 X
的声明称为前向声明,它向程序中引入了名字 X
并且指明 X
是一种类类型。对于类型 X
来说,此时我们已知它是一个类类型,但是不清楚它到底包含哪些成员,所以它是一个不完全类型。我们可以定义指向不完全类型的指针,但是无法创建不完全类型的对象。
如果试图写成下面的形式,将引发编译器错误。
class Y;
class X{
Y y;
};
class Y{
X* x;
};
此时我们试图在类 X
中创建不完全类型 Y
的对象,编译器给出报错信息:
error:field 'y' has incomplete type
练习7.32
定义你自己的 Screen
和 Window_mgr
,其中 clear
是 Window_mgr
的成员,是 Screen
的友元。
解:
类可以把其他类定义成友元,也可以把其他类的成员函数定义成友元。当把成员函数定义成友元时,要特别注意程序的组织结构。
要想让 clear
函数作为 screen
的友元,只需要在 Screen
类中做出友元声明即可。本题的真正关键之处是程序的组织结构,我们必须
- 首先定义
Window mgr
类,其中声明clear
函数,但是不能定义它; - 接下来定义
Screen
类,并且在其中指明clear
函数是其友元; - 最后定义
clear
函数。
满足题意的程序如下所示:
#include <iostream>
#include <string>
using namespace std;
class Window_mgr
{
public:
void clear();
};
class Screen
{
friend void Window_mgr::clear();
private:
unsigned width = 0, height = 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 << "清理之前myScrean的内容是:" << endl;
cout << myScreen.contents << endl;
myScreen.contents = "";
cout << "清理之后myScrean的内容是:" << endl;
cout << myScreen.contents << endl;
}
int main()
{
Window_mgr w;
w.clear();
system("pause");
return 0;
}
练习7.33
如果我们给Screen
添加一个如下所示的size
成员将发生什么情况?如果出现了问题,请尝试修改它。
pos Screen::size() const
{
return height * width;
}
解:
如果添加如题目所示的 size
函数将会出现编译错误。因为该函数的返回类型 pos
本身定义在 Screen
类的内部,所以在类的外部无法直接使用 pos
。要想使用 pos
,需要在它的前面加上作用域 Screen::
。
修改后的程序是:
Screen::pos Screen::size() const
{
return height * width;
}
练习7.34
如果我们把第256页 Screen
类的 pos
的 typedef
放在类的最后一行会发生什么情况?
解:
这样做会导致编译出错,因为对 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;
}
解:
typedef string Type; //声明类型别名Type表示string
Type initVal(); //声明函数initVal,返回类型是Type
class Exercise { //定义一个新类Exercise
public:
typedef double Type; //在内层作用域重新声明类型别名Type表示double
Type setVal(Type); //声明函数setVal,参数和返回值的类型都是Type
Type initVal(); //在内层作用域重新声明函数initVal,返回类型是Type
private:
int val; //声明私有数据成员val
};
//定义函数setVal,此时的Type显然是外层作用域的
Type Exercise::setVal(Type parm) {
val = parm + initVal(); //此处使用的是类内的initVal函数
return val;
}
其中,
- 在
Exercise
类的内部,函数setval
和initval
用到的Type
都是Exercise
内部声明的类型别名,对应的实际类型是double
。 - 在
Exercise
类的外部,定义Exercise::setVal
函数时形参类型Type
用的是Exercise
内部定义的别名,对应double
;返回类型Type
用的是全局作用域的别名,对应string
。使用的initVal
函数是Exercise
类内定义的版本。
编译上述程序时在 setVal
的定义处发生错误,此处定义的函数形参类型是 double
、返回值类型是 string
,而类内声明的同名函数形参类型是 double
、返回值类型也是 double
,二者无法匹配。修改的措施是在定义 setVal
函数时使用作用域运算符强制指定函数的返回值类型。
Exercise::Type Exercise::setVal(Type parm){
val=parm+initVal(); //此处使用的是类内的initVal函数
return val;
}
练习7.36
下面的初始值是错误的,请找出问题所在并尝试修改它。
struct X {
X (int i, int j): base(i), rem(base % j) {}
int rem, base;
};
解:
本题旨在考查使用构造函数初始值列表时成员的初始化顺序,初始化顺序只与数据成员在类中出现的次序有关,而与初始值列表的顺序无关。
在类 X
中,两个数据成员出现的顺序是 rem
在前,base
在后,所以当执行 X
对象的初始化操作时先初始化 rem
。如上述代码所示,初始化 rem
要用到 base
的值,而此时 base
尚未被初始化,因此会出现错误。该过程与构造函数初始值列表中谁出现在前面谁出现在后面没有任何关系。
修改的方法很简单,只需要把变量 rem
和 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");
}
解:
根据实参的不同调用实现了最佳匹配的构造函数,对于没有提供实参的成员使用其类内初始值进行初始化。
-
Sales_data first_item(cin);
使用了接受std::istreams
参数的构造函数,该对象的成员值依赖于用户的输入。 -
Sales_data next;
使用了Sales data
的默认构造函数,其中string
类型的成员bookNo
默认初始化为空字符串,其他几个成员使用类内初始值初始化为0。 -
Sales_data last("9-999-99999-9");
使用了接受const strings
参数的构造函数,其中bookNo
使用实参初始化为"9-999-99999-9"
,其他几个成员使用类内初始值初始化为0。
练习7.38
有些情况下我们希望提供 cin
作为接受 istream&
参数的构造函数的默认实参,请声明这样的构造函数。
解:
满足题意的构造函数如下所示:
Sales_data(std::istream &is = std::cin) { is >> *this; }
此时该函数具有了默认构造函数的作用,因此原来声明的默认构造函数 Sales data()=default;
应该去掉,否则会引起调用的二义性。
练习7.39
如果接受 string
的构造函数和接受 istream&
的构造函数都使用默认实参,这种行为合法吗?如果不,为什么?
解:
如果我们为构造函数的全部形参都提供了默认实参(包括为只接受一个形参的构造函数提供默认实参),则该构造函数同时具备了默认构造函数的作用。此时即使我们不提供任何实参地创建类的对象,也可以找到可用的构造函数。
然而,如果按照本题的叙述,我们为两个构造函数同样都赋予了默认实参,则这两个构造函数都具有了默认构造函数的作用。一旦我们不提供任何实参地创建类的对象,则编译器无法判断这两个(重载的)构造函数哪个更好,从而出现了二义性错误。
练习7.40
从下面的抽象概念中选择一个(或者你自己指定一个),思考这样的类需要哪些数据成员,提供一组合理的构造函数并阐明这样做的原因。
(a) Book
(b) Data
(c) Employee
(d) Vehicle
(e) Object
(f) Tree
解:
首先选择(a)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; }
};
也可以选择(f)Tree,
- 一棵树通常包含树的名称、存活年份、树高等信息,因此令其数据成员为:
Name、Age、Height
,其中Name
是string
类型,Age
是unsigned
类型,Height
是double
类型。 - 假如我们不希望由用户输入
Tree
的信息,则可以去掉接受std::istream&
形参的构造函数,只保留默认构造函数和接受全部信息的构造函数。
其定义如下:
class Tree
{
private:
string Name;
unsigned Age = 0;
double Height = 0;
public:
Tree() = default;
Tree(const string &n, unsigned a, double h):Name(h), Age(a), Height(h);
};
开放题。
练习7.41
使用委托构造函数重新编写你的Sales_data
类,给每个构造函数体添加一条语句,令其一旦执行就打印一条信息。用各种可能的方式分别创建Sales_data
对象,认真研究每次输出的信息直到你确实理解了委托构造函数的执行顺序。
解:
#include <iostream>
#include <string>
using namespace std;
class Sales_data{
friend std::istream &read(std::istream &is, Sales_data &item);
friend std::ostream &print(std::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 << "该构造函数接受书号、销售量、原价、实际售价四个信息" << endl;
}
Sales_data() :Sales_data("", 0, 0, 0)
{
cout << "该构造函数无须接受任何信息" << endl;
}
Sales_data(const string &book) :Sales_data(book, 0, 0, 0)
{
cout << "该构造函数接受书号信息" << endl;
}
Sales_data(std::istream &is) :Sales_data()
{
read(is, *this);
cout << "该构造函数接受用户输入的信息" << endl;
}
private:
std::string bookNo; //书籍编号,隐式初始化为空串
unsigned units_sold = 0; //销售量,显式初始化为0
double sellingprice = 0.0; //原始价格,显式初始化为0.0
double saleprice = 0.0; //实售价格,显式初始化为0.0
double discount = 0.0; //折扣,显式初始化为0.0
};
std::istream &read(std::istream &is, Sales_data &item)
{
is >> item.bookNo >> item.units_sold >> item.sellingprice >>
item.saleprice;
return is;
}
std::ostream &print(std::ostream &os, const Sales_data &item)
{
os << item.bookNo << "" << item.units_sold << "" << item.sellingprice
<< "" << item.saleprice << "" << item.discount;
return os;
}
int main(){
Sales_data fist("978-7-121-15535-2", 85, 128, 109);
cout << "*************" << endl;
Sales_data second;
cout << "*************" << endl;
Sales_data third("978-7-121-15535-2");
cout << "*************" << endl;
Sales_data last(cin);
system("pause");
return 0;
}
练习7.42
对于你在练习7.40中编写的类,确定哪些构造函数可以使用委托。如果可以的话,编写委托构造函数。如果不可以,从抽象概念列表中重新选择一个你认为可以使用委托构造函数的,为挑选出的这个概念编写类定义。
解:
以练习7.40构建的 Book
类为例,我们令其中的构造函数 Book(const string &n, const string&I, double pr, const string &a, const string &p)
为普通构造函数,而令另外两个作为委托构造函数。
其具体形式如下所示:
class Book
{
private:
string Name, ISBN, Author, Publisher;
double Price = 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(std::istream &is):Book() { is >> *this; }
};
练习7.43
假定有一个名为 NoDefault
的类,它有一个接受 int
的构造函数,但是没有默认构造函数。定义类 C
,C
有一个 NoDefault
类型的成员,定义 C
的默认构造函数。
解:
我们需要为类 C
的构造函数提供一个默认的 int
值作为参数,满足题意的类定义及验证程序如下所示:
#include<iostream>
#include<string>
using namespace std;
// 该类型没有显式定义默认构造函数,编译器也不会为它合成一个
class NoDefault
{
public:
NoDefault(int i)
{
val = i;
}
int val;
};
class C
{
public:
NoDefault nd;
// 必须显式调用 NoDefault 的带参构造函数初始化 nd
C(int i = 0) :nd(i) { }
};
int main()
{
C c; // 使用了类型C的默认构造函数
cout << c.nd.val << endl;
system("pause");
return 0;
}
练习7.44
下面这条声明合法吗?如果不,为什么?
vector<NoDefault> vec(10);
解:
上述语句的含义是创建一个 vector
对象 vec
,该对象包含10个元素,每个元素的类型都是 NoDefault
且执行默认初始化。
然而,因为我们在类 NoDefault
的定义中没有设计默认构造函数,所以所需的默认初始化过程无法执行。编译器会报告这一错误。
练习7.45
如果在上一个练习中定义的 vector
的元素类型是 C
,则声明合法吗?为什么?
解:
与上一个练习相比,如果把 vector
的元素类型更改为 C
,则该声明是合法的,这是因为我们给类型 C
定义了带参数的默认构造函数,它可以完成声明语句所需的默认初始化操作。
练习7.46
下面哪些论断是不正确的?为什么?
- (a) 一个类必须至少提供一个构造函数。
- (b) 默认构造函数是参数列表为空的构造函数。
- © 如果对于类来说,不存在有意义的默认值,则类不应该提供默认构造函数。
- (d) 如果类没有定义默认构造函数,则编译器将为其生成一个并把每个数据成员初始化成相应类型的默认值。
解:
(a)是错误的,类可以不提供任何构造函数,这时编译器自动实现一个合成的默认构造函数。
(b)是错误的,如果某个构造函数包含若干形参,但是同时为这些形参都提供了默认实参,则该构造函数也具备默认构造函数的功能。
(c)是错误的,因为如果一个类没有默认构造函数,也就是说我们定义了该类的某些构造函数但是没有为其设计默认构造函数,则当编译器确实需要隐式地使用默认构造函数时,该类无法使用。所以一般情况下,都应该为类构建一个默认构造函数。
(d)是错误的,对于编译器合成的默认构造函数来说,类类型的成员执行各自所属类的默认构造函数,内置类型和复合类型的成员只对定义在全局作用域中的对象执行初始化。
练习7.47
说明接受一个 string
参数的 Sales_data
构造函数是否应该是 explicit
的,并解释这样做的优缺点。
解:
接受一个 string
参数的 Sales_data
构造函数应该是 explicit
的,否则,编译器就有可能自动把一个 string
对象转换成 Sales_data
对象,这种做法显得有些随意,某些时候会与程序员的初衷相违背。
使用 explicit
的优点是避免因隐式类类型转换而带来意想不到的错误,缺点是当用户的确需要这样的类类型转换时,不得不使用略显烦琐的方式来实现。
练习7.48
假定 Sales_data
的构造函数不是 explicit
的,则下述定义将执行什么样的操作?
string null_isbn("9-999-9999-9");
Sales_data item1(null_isbn);
Sales_data item2("9-999-99999-9");
解:
- 构造函数如果不是
explicit
的,则string
对象隐式地转换成Sales_data
对象; - 相反,构造函数如果是
explicit
的,则隐式类类型转换不会发生。
在本题给出的代码中,第一行创建了一个 string
对象,第二行和第三行都是调用 Sales_data
的构造函数(该构造函数接受一个 string
)创建它的对象。此处无须任何类类型转换,所以不论 Sales_data
的构造函数是不是 explicit
的,item1
和 item2
都能被正确地创建,它们的 bookNo
成员都是 9-999-99999-9
,其他成员都是 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;
解:
(a)是正确的,编译器首先用给定的 string
对象 s
自动创建一个 Sales_data
对象,然后这个新生成的临时对象传给 combine
的形参(类型是 Sales_data
),函数正确执行并返回结果。
(b)无法编译通过,因为 combine
函数的参数是一个非常量引用,而 s
是一个 string
对象,编译器用 s
自动创建一个 Sales_data
临时对象,但是这个新生成的临时对象无法传递给 combine
所需的非常量引用。如果我们把函数声明修改为 Sales_data &combine(const Sales_data&);
就可以了。
(c)无法编译通过,因为我们把 combine
声明成了常量成员函数,所以该函数无法修改数据成员的值。
练习7.50
确定在你的 Person
类中是否有一些构造函数应该是 explicit
的。
解:
我们之前定义的 Person
类含有3个构造函数,因为前两个构造函数接受的参数个数都不是1,所以它们不存在隐式转换的问题,当然也不必指定 explicit
。
Person
类的最后一个构造函数 Person(std::istream &is);
只接受一个参数,默认情况下它会把读入的数据自动转换成 Person
对象。我们更倾向于严格控制 Person
对象的生成过程,
- 如果确实需要使用
Person
对象,可以明确指定; - 在其他情况下则不希望自动类型转换的发生。所以应该把这个构造函数指定为
explicit
的。
练习7.51
vector
将其单参数的构造函数定义成 explicit
的,而 string
则不是,你觉得原因何在?
解:
从参数类型到类类型的自动转换是否有意义依赖于程序员的看法,
- 如果这种转换是自然而然的,则不应该把它定义成
explicit
的; - 如果二者的语义距离较远,则为了避免不必要的转换,应该指定对应的构造函数是
explicit
的。
string
接受的单参数是 const char*
类型,如果我们得到了一个常量字符指针(字符数组),则把它看作 string
对象是自然而然的过程,编译器自动把参数类型转换成类类型也非常符合逻辑,因此我们无须指定为 explicit
的。
与 string
相反,vector
接受的单参数是 int
类型,这个参数的原意是指定 vector
的容量。如果我们在本来需要 vector
的地方提供一个 int
值并且希望这个 int
值自动转换成 vector
,则这个过程显得比较牵强,因此把 vector
的单参数构造函数定义成 explicit
的更加合理。
练习7.52
使用2.6.1节的 Sales_data
类,解释下面的初始化过程。如果存在问题,尝试修改它。
Sales_data item = {"987-0590353403", 25, 15.99};
解:
程序的意图是对 item
执行聚合类初始化操作,用花括号内的值初始化 item
的数据成员。然而实际过程与程序的原意不符合,编译器会报错。
这是因为聚合类必须满足一些非常苛刻的条件,其中一项就是没有类内初始值,而在2.6.1节给出的定义中,数据成员 units_sold
和 revenue
都包含类内初始值。只要去掉这两个类内初始值,程序就可以正常运行了。
struct Sales_data {
string bookNo;
unsigned units_sold;
double revenue;
};
练习7.53
定义你自己的 Debug
。
解:
字面值常量类是一种非常特殊的类类型,聚合类是字面值常量类,某些类虽然不是聚合类,但在满足书中所提要求的情况下也是字面值常量类。字面值常量类必须至少提供一个 constexpr
构造函数。
参考书中的例子定义 Debug
类即可。
class Debug {
public:
constexpr Debug(bool b = true) : hw(b), io(b), other(b) { }
constexpr Debug(bool h, bool i, bool o) : hw(r), io(i), other(0) { }
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; // 硬件错误,而非IO错误
bool io; // IO错误
bool other; // 其他错误
};
练习7.54
Debug
中以 set_
开头的成员应该被声明成 constexpr
吗?如果不,为什么?
解:
这些以 set_
开头的成员不能声明成 constexpr
,这些函数的作用是设置数据成员的值,而 constexpr
函数只能包含 return
语句,不允许执行其他任务。
练习7.55
7.5.5节的 Data
类是字面值常量类吗?请解释原因。
解:
因为 Data
类是聚合类,所以它也是一个字面值常量类。
练习7.56
什么是类的静态成员?它有何优点?静态成员与普通成员有何区别?
解:
静态成员是指声明语句之前带有关键字 static
的类成员,静态成员不是任意单独对象的组成部分,而是由该类的全体对象所共享。
静态成员的优点包括:
- 作用域位于类的范围之内,避免与其他类的成员或者全局作用域的名字冲突;
- 可以是私有成员,而全局对象不可以;
- 通过阅读程序可以非常容易地看出静态成员与特定类关联,使得程序的含义清晰明了。
静态成员与普通成员的区别主要体现在普通成员与类的对象关联,是某个具体对象的组成部分;而静态成员不从属于任何具体的对象,它由该类的所有对象共享。另外,还有一个细微的区别,静态成员可以作为默认实参,而普通数据成员不能作为默认实参。
练习7.57
编写你自己的 Account
类。
解:
如果类的某些(某个)成员从逻辑上来说更应该与类本身关联,而不是与类的具体对象关联,则我们应该把这种成员声明成静态的。在 Account
类中,很明显利率是相对稳定和统一的,应该是静态成员;而开户人以及它的储蓄额则与对象息息相关,不能是静态的。
为了简便起见,我们只给出 Account
类的声明:
class Account
{
private:
string strName;
double dAmount=0.0;
static double dRate;
};
练习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;
解:
本题的程序存在以下几处错误:
在类的内部,rate
和 vec
的初始化是错误的,因为除了静态常量成员之外,其他静态成员不能在类的内部初始化。
另外,example.c
文件的两条语句也是错误的,因为在这里我们必须给出静态成员的初始值。
如果想要更多的资源,欢迎关注 @我是管小亮,文字强迫症MAX~
回复【福利】即可获取我为你准备的大礼,包括C++,编程四大件,NLP,深度学习等等的资料。
想看更多文(段)章(子),欢迎关注微信公众号「程序员管小亮」~