侧边栏壁纸
  • 累计撰写 47 篇文章
  • 累计创建 0 个标签
  • 累计收到 38 条评论

目 录CONTENT

文章目录

第 7 章

7.1

【出题思路】

程序的思路是:只要 ISBN 相同,就不断累加销量并重新计算平均售价,直至输入新的书籍为止。

【解答】

满足题意的程序如下所示:

Sale_data.h

#ifndef TEST_SALES_DATA_H
#define TEST_SALES_DATA_H

// Definition of Sales_data class and related functions goes here
#include <iostream>
#include <string>

// 头文件不应包含 using 声明
// 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; }

    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 //TEST_SALES_DATA_H

main.cpp

#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())  // 检查 isbn
                 total += trans;    // 更新变量 total 当前的值
             else {
                 cout << total << endl;     // 输出结果
                 total = trans;             // 处理下一本
             }
         }
         cout << total << endl;     // 输出最后一条交易
    }
    else {                              // 没有输入任何信息
        cerr << "No data?!" << endl;    // 通知用户
        return -1;
    }
    return 0;
}
// 运行结果
// 0-201-78345-X 7 20 19 0.95
// 0-202-78345-X 4 30 29 0.966667
// 上面这两行为控制台的输出结果,其余为从控制台输入的测试数据
// 控制台输出的数据依次为:isbn 销售量 原价 平均实际零售价 折扣
请输入交易记录(ISBN、销售量、原价、实际售价):
0-201-78345-X 3 20.00 19.00
0-201-78345-X 4 20.00 19.00
0-202-78345-X 4 30.00 29.00
0-201-78345-X 7 20 19 0.95
0-202-78345-Y 2 200.00 199.00
0-202-78345-X 4 30 29 0.966667

7.2

【解答】

添加 combine 和 isbn 成员后的 Sales_data 类是:

class Sales_data {
private:                            // 定义私有数据成员
    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;   // 把 rhs 的成员加到 this 对象的成员上
        saleprice = (rhs.saleprice * rhs.units_sold + saleprice * units_sold)
                / (rhs.units_sold + units_sold);    // 计算平均实际售价
        if (sellingprice != 0)
            discount = saleprice / sellingprice;    // 计算 this 对象的折扣
        return *this;               // 返回调用该函数的对象    
    }
};

7.3

参考练习 7.1

7.4

【出题思路】

练习定义类并添加必要的数据成员。

【解答】

满足题意的 Person 类是:

class Person {
private:
    string strName;         // 姓名
    string strAddress;      // 地址
};

7.5

【出题思路】

练习向类添加函数成员的方法,理解常量成员函数。

【解答】

修改后的 Person 类是:

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;
    sum.combine(rhs);
    return sum;
}

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.isbn() << " " << item.units_sold << " " << item.sellingprice
    << " " << item.saleprice << " " << item.discount;
    return os;
}

7.7

【出题思路】

基于 7.1 和 7.6 书写本题。

头文件中,print 函数不负责换行。一般来说,执行输出任务的函数应该尽量减少对格式的控制,这样可以确保由用户代码来决定是否换行。

【解答】

满足题意的程序如下所示:

Sales_data.h

#ifndef TEST_SALES_DATA_H
#define TEST_SALES_DATA_H

// Definition of Sales_data class and related functions goes here
#include <iostream>
#include <string>

// 头文件不应包含 using 声明
// using namespace std;

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
    std::string isbn() const { return bookNo; }
    // combine 函数用于把两个 ISBN 相同的销售记录合并在一起
    Sales_data& combine(const Sales_data &rhs) {
        units_sold += rhs.units_sold;   // 把 rhs 的成员加到 this 对象的成员上
        saleprice = (rhs.saleprice * rhs.units_sold + saleprice * units_sold)
                    / (rhs.units_sold + units_sold);    // 计算平均实际售价
        if (sellingprice != 0)
            discount = saleprice / sellingprice;    // 计算 this 对象的折扣
        return *this;               // 返回调用该函数的对象
    }
    Sales_data add(const Sales_data &lhs, const Sales_data &rhs);
    std::istream &read(std::istream &is, Sales_data &item);
    std::ostream &print(std::ostream &os, const Sales_data &item);
};

Sales_data Sales_data::add(const Sales_data &lhs, const Sales_data &rhs) {
    Sales_data sum = lhs;
    sum.combine(rhs);
    return sum;
}

std::istream &Sales_data::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 &Sales_data::print(std::ostream &os, const Sales_data &item) {
    os << item.isbn() << " " << item.units_sold << " " << item.sellingprice
       << " " << item.saleprice << " " << item.discount;
    return os;
}

#endif //TEST_SALES_DATA_H

main.cpp

#include "Sales_data.h"
using namespace std;

int main() {
    cout << "请输入交易记录(ISBN、销售量、原价、实际售价):" << endl;
    Sales_data total;               // 保存当前求和结果的变量
    if (total.read(cin, total)) {             // 读入第一笔交易记录
        Sales_data trans;          // 保存下一条交易数据的变量
        while (trans.read(cin, trans)) {     // 读入剩余的交易
            if (total.isbn() == trans.isbn())  // 检查 isbn
                total = total.add(total, trans);     // 更新变量 total 当前的值
            else {
                total.print(cout, total) << endl;    // 输出结果
                total = trans;                 // 处理下一本
            }
        }
        total.print(cout, total) << endl;            // 输出最后一条交易
    }
    else {                              // 没有输入任何信息
        cerr << "No data?!" << endl;    // 通知用户
        return -1;
    }
    return 0;
}
// 运行结果
// 0-201-78345-X 7 20 19 0.95
// 0-202-78345-X 4 30 29 0.966667
// 上面这两行为控制台的输出结果,其余为从控制台输入的测试数据
// 控制台输出的数据依次为:isbn 销售量 原价 平均实际零售价 折扣
请输入交易记录(ISBN、销售量、原价、实际售价):
0-201-78345-X 3 20.00 19.00
0-201-78345-X 4 20.00 19.00
0-202-78345-X 4 30.00 29.00
0-201-78345-X 7 20 19 0.95
0-202-78345-Y 2 200.00 199.00
0-202-78345-X 4 30 29 0.966667


7.8

【出题思路】

本题考查理解 Sales_data 类中输入输出函数的原理。

【解答】

read 函数将其 Sales_data 参数定义成普通的引用是因为我们需要从标准输入流中读取数据并将其写入到给定的 Sales_data 对象,因此需要有修改对象的权限。而 print 将其参数定义成常量引用是因为它只负责数据的输出,不对其做任何更改。

7.9

【出题思路】

仿照 Sales_data 类,为 Person 类添加相应的 read 和 print 函数。

基于练习 7.5,继续扩充 Person 类。

【解答】

满足题意的 read 和 print 函数如下所示:

Person.h

#ifndef TEST_PERSON_H
#define TEST_PERSON_H

// Definition of Person class and related functions goes here
// 头文件不应包含 using 声明
// using namespace std;

class Person {
private:
    string strName;         // 姓名
    string strAddress;      // 地址
public:
    string getName() const { return strName; }          // 返回姓名
    string getAddress() const { return strAddress; }    // 返回地址
    std::istream &read(std::istream &is, Person &per);
    std::ostream &print(std::ostream &os, const Persong &per);
};

std::istream & Person::read(std::istream &is, Person &per) {
    is >> per.strName >> per.strAddress;
    return is;
}

std::ostream & Person::print(std::ostream &os, const Persong &per) {
    os << per.getName() << per.getAddress();
    return os;
}

#endif //TEST_PERSON_H

7.10

【出题思路】

read 函数的返回类型是 std::istream &,体会这里使用引用的作用。

【解答】

因为 read 函数的返回类型是引用(左值),所以 read(cin, data1) 的返回值可以继续作为外层 read 函数的实参使用。该条件检验读入 data1 和 data2 的过程是否正确,如果正确,条件满足;否则条件不满足。

我们可以这样拆分理解:

std::istream &firstStep = read(cin, data1);
std::istream &secondStep = read(firstStep, data2);
if (secondStep)

the condition of the if statement would read two Sales_data object at one time.

7.11

【出题思路】

在不同情况下,初始化 Sales_data 对象所需的数据有所不同,分别为其设计构造函数,同时也利用 C++11 新标准提供的 = default 定义默认构造函数。

【解答】

满足题意的 4 个构造函数分别如下所示:

Sales_data.h

#ifndef TEST_SALES_DATA_H
#define TEST_SALES_DATA_H

// Definition of Sales_data class and related functions goes here
#include <iostream>

// 头文件不应包含 using 声明
// using namespace std;

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);

   std::istream &read(std::istream &is, 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
};

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) {
   read(is, *this);
}

std::istream& Sales_data::read(std::istream &is, Sales_data &item) {
   is >> item.bookNo >> item.units_sold >> item.sellingprice
      >> item.saleprice;
   return is;
}

#endif //TEST_SALES_DATA_H

main.cpp

#include "Sales_data.h"

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(std::cin);

    return 0;
}

注:可以下断点观察程序执行流程。

在类的定义中,我们设计了 4 个构造函数。

第一个构造函数是默认构造函数,它使用了 C++11 新标准提供的 = default 。它的参数列表为空,即不需要我们提供任何数据也能构造一个对象。

第二个构造函数只接受一个 const string & ,表示书籍的 ISBN 编号,编译器赋予其他数据成员类内初始值。

第三个构造函数接受完整的销售记录信息, const string & 表示书籍的 ISBN 编号, const unsigned 表示销售量,后面两个 const double 分别表示书籍的原价和实际售价。

最后一个构造函数接受 istream & 并从中读取书籍的销售信息。

7.12

【出题思路】

构造函数既可以定义在类的外部,也可以定义在类的内部。

【解答】

按照题目要求,把只接受一个 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) { read(is, *this); }

   std::istream &read(std::istream &is, 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.13

【出题思路】

原来的程序使用 Sales_data 类的默认构造函数,本题改为使用接受 istream 的构造函数。

【解答】

改写后的程序如下所示:

Sales_data.h

#ifndef TEST_SALES_DATA_H
#define TEST_SALES_DATA_H

// Definition of Sales_data class and related functions goes here
#include <iostream>
#include <string>

// 头文件不应包含 using 声明
// using namespace std;

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:                             // 定义公有函数成员
    // 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);

    // isbn 函数只有一条语句,返回 bookNo
    std::string isbn() const { return bookNo; }
    // combine 函数用于把两个 ISBN 相同的销售记录合并在一起
    Sales_data& combine(const Sales_data &rhs) {
        units_sold += rhs.units_sold;   // 把 rhs 的成员加到 this 对象的成员上
        saleprice = (rhs.saleprice * rhs.units_sold + saleprice * units_sold)
                    / (rhs.units_sold + units_sold);    // 计算平均实际售价
        if (sellingprice != 0)
            discount = saleprice / sellingprice;    // 计算 this 对象的折扣
        return *this;               // 返回调用该函数的对象
    }
    Sales_data add(const Sales_data &lhs, const Sales_data &rhs);
    std::istream &read(std::istream &is, Sales_data &item);
    std::ostream &print(std::ostream &os, const Sales_data &item);
};

Sales_data Sales_data::add(const Sales_data &lhs, const Sales_data &rhs) {
    Sales_data sum = lhs;
    sum.combine(rhs);
    return sum;
}

std::istream &Sales_data::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 &Sales_data::print(std::ostream &os, const Sales_data &item) {
    os << item.isbn() << " " << item.units_sold << " " << item.sellingprice
       << " " << item.saleprice << " " << item.discount;
    return os;
}

#endif //TEST_SALES_DATA_H

main.cpp

#include "Sales_data.h"
using namespace std;

int main() {
    cout << "请输入交易记录(ISBN、销售量、原价、实际售价):" << endl;
    Sales_data total;               // 保存当前求和结果的变量
    if (total.read(cin, total)) {             // 读入第一笔交易记录
        Sales_data trans;          // 保存下一条交易数据的变量
        while (trans.read(cin, trans)) {     // 读入剩余的交易
            if (total.isbn() == trans.isbn())  // 检查 isbn
                total = total.add(total, trans);     // 更新变量 total 当前的值
            else {
                total.print(cout, total) << endl;    // 输出结果
                total = trans;                 // 处理下一本
            }
        }
        total.print(cout, total) << endl;            // 输出最后一条交易
    }
    else {                              // 没有输入任何信息
        cerr << "No data?!" << endl;    // 通知用户
        return -1;
    }
    return 0;
}
// 运行结果
// 0-201-78345-X 7 20 19 0.95
// 0-202-78345-X 4 30 29 0.966667
// 上面这两行为控制台的输出结果,其余为从控制台输入的测试数据
// 控制台输出的数据依次为:isbn 销售量 原价 平均实际零售价 折扣
请输入交易记录(ISBN、销售量、原价、实际售价):
0-201-78345-X 3 20.00 19.00
0-201-78345-X 4 20.00 19.00
0-202-78345-X 4 30.00 29.00
0-201-78345-X 7 20 19 0.95
0-202-78345-Y 2 200.00 199.00
0-202-78345-X 4 30 29 0.966667

7.14

【出题思路】

构造函数初始值列表负责为新创建的对象的一个或几个数据成员赋初值。构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来的成员初始值,不同成员的初始化通过逗号分隔开。

【解答】

使用初始值列表的构造函数是:

Sales_data(const std::string &book) : bookNo(book), units_sold(0), sellingprice(0), saleprice(0), discount(0) { }

7.15

【出题思路】

仿照 Sales_data 类,为 Person 类添加默认构造函数、接受两个实参的构造函数和从标准输入流中读取数据的构造函数。

【解答】

添加上述 3 个构造函数之后,新的 Person 类如下所示:

Person.h

#ifndef TEST_PERSON_H
#define TEST_PERSON_H

// Definition of Person class and related functions goes here
// 头文件不应包含 using 声明
// using namespace std;

class Person {
private:
    string strName;         // 姓名
    string strAddress;      // 地址
public:
    Person() = default;
    Person(const string &name, const string &addr) {
        strName = name;
        strAddress = addr;
    }
    Person(std::istream &is) { read(is, *this); }
    
    string getName() const { return strName; }          // 返回姓名
    string getAddress() const { return strAddress; }    // 返回地址
    std::istream &read(std::istream &is, Person &per);
    std::ostream &print(std::ostream &os, const Persong &per);
};

std::istream & Person::read(std::istream &is, Person &per) {
    is >> per.strName >> per.strAddress;
    return is;
}

std::ostream & Person::print(std::ostream &os, const Persong &per) {
    os << per.getName() << per.getAddress();
    return os;
}

#endif //TEST_PERSON_H

7.16

【出题思路】

考查访问说明符的用法。

【解答】

在类的定义中,可以包含 0 个或者多个访问说明符,并且对于某个访问说明符能出现多少次以及能出现在哪里都没有严格规定。每个访问说明符指定接下来的成员的访问级别,有效范围直到出现下一个访问说明符或者到达类的结尾为止。

一般来说,作为接口的一部分,构造函数和一部分成员函数应该定义在 public 说明符之后,而数据成员和作为实现部分的函数则应该跟在 private 说明符之后。

7.17

【出题思路】

class 和 struct 都可以用来声明类,它们的大多数功能都类似,唯一的区别是默认访问权限不同。

【解答】

类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式。如果使用 struct 关键字,则定义在第一个说明符之前的成员是 public 的;相反,如果使用 class 关键字,则这些成员是 private 的。

7.18

【出题思路】

封装、继承、多态是类的三个特性,本题首先考查封装的含义。

【解答】

封装是指保护类的成员不被随意访问的能力。通过把类的实现细节设置为 private,我们就能完成类的封装。封装实现了类的接口和实现的分离。

如书中所述,封装有两个重要的优点:

  1. 确保用户代码不会无意间破坏封装对象的状态;
  2. 被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。

一旦把数据成员定义成 private 的,类的作者就可以比较自由地修改数据了。当实现部分发生改变时,只需要检查类的代码本身以确认这次改变有什么影响;换句话说,只要类的接口不变,用户代码就无须改变。如果数据是 public 的,则所有使用了原来数据成员的代码都可能失效,这时我们必须定位并重写所有依赖于老版本实现的代码,之后才能重新使用该程序。

把数据成员的访问权限设成 private 还有另外一个好处,这么做能防止由于用户的原因造成数据被破坏。如果我们发现有程序缺陷破坏了对象的状态,则可以在有限的范围内定位缺陷:因为只有实现部分的代码可能产生这样的错误。因此,将错误的搜索限制在有限范围内将能极大地简化更改问题及修正程序等工作。

7.19

【出题思路】

根据封装的含义我们知道,作为接口的一部分,构造函数和一部分成员函数应该定义在 public 说明符之后,而数据成员和作为实现部分的函数则应该跟在 private 说明符之后。

【解答】

根据上述分析,我们把数据成员 strName 和 strAddress 设置为 private,这样可以避免用户程序不经意间修改和破坏它们;同时把构造函数和两个获取数据成员的接口函数设置为 public,以便于我们在类的外部访问。

7.20

【出题思路】

友元为类的非成员接口函数提供了访问类私有成员的能力,这种能力的提升利弊共存。

【解答】

当非成员函数确实需要访问类的私有成员时,我们可以把它声明成该类的友元。此时,友元可以“工作在类的内部”,像类的成员一样访问类的所有数据和函数。但是一旦使用不慎(比如随意设定友元),就有可能破坏类的封装性。

7.21

【出题思路】

本题主要涉及 read、print 和 add 三个函数,在修改之前由于它们不属于类的成员函数,所以一旦把数据成员设置为 private,则这三个函数无法访问它们,因而编译不能通过。

【解答】

解决方法是把函数 read、print 和 add 设置为 Sales_data 类的友元,其形式如下所示,此时即使数据成员的访问说明符是 private 也能编译通过。

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 类的数据成员以及不应该被外部访问的函数成员设置成 private。

【解答】

到目前为止,我们设计的 Person 类包含两个数据成员、三个构造函数和四个用来获取数据的接口函数。显然,除了数据成员之外其他几个函数都有权被外部程序访问,所以我们通过把数据成员设置为 private 来确保类的封装性。

class Person {
private:
    string strName;         // 姓名
    string strAddress;      // 地址
public:
    Person() = default;
    Person(const string &name, const string &addr) {
        strName = name;
        strAddress = addr;
    }
    Person(std::istream &is) { read(is, *this); }

    string getName() const { return strName; }          // 返回姓名
    string getAddress() const { return strAddress; }    // 返回地址
    std::istream &read(std::istream &is, Person &per);
    std::ostream &print(std::ostream &os, const Persong &per);
};

7.23

【出题思路】

思考 Screen 类应该包含哪些数据成员和函数成员,设置适当的访问权限。

【解答】

对于 Screen 类来说,必不可少的数据成员有:屏幕的宽度和高度、屏幕的内容以及光标的当前位置,这与书中的示例是一致的。因此,仅包含数据成员的 Screen 类是:

class Screen {
private:
    unsigned height = 0, width = 0;
    unsigned cursor = 0;
    string contents;
};

7.24

【出题思路】

同一个类可以包含多个构造函数,构造函数的定义可以在类的内部也可以在类的外部。

【解答】

使用构造函数的列表初始值初始化操作,添加构造函数之后的 Screen 类是:

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) { }
};

7.25

【出题思路】

含有指针数据成员的类一般不宜使用默认的拷贝和赋值操作,如果类的数据成员都是内置类型的,则不受干扰。

【解答】

Screen 的 4 个数据成员都是内置类型(string 类定义了拷贝和赋值运算符),因此直接使用类对象执行拷贝和赋值操作是可以的。

7.26

【出题思路】

要想把类的成员函数定义成内联函数,有两种途径。

  • 第一种是直接把函数定义放在类的内部;
  • 第二种是把函数定义放在类的外部,并且在定义前显式地指定 inline。

【解答】

隐式内联,把 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

【出题思路】

添加 3 个成员函数,注意函数的返回值类型应该是引用类型,在成员函数内部可以直接使用类的数据成员。

【解答】

添加 mov、set 和 display 函数之后,满足题意的程序如下所示:

Screen.h

#ifndef TEST_SCREEN_H
#define TEST_SCREEN_H

#include <iostream>

class Screen {
private:
    unsigned height = 0, width = 0;
    unsigned cursor = 0;
    std::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 ch) {
        contents[r * width + c] = ch;
        return *this;
    }
    Screen &display(std::ostream &os) {
        os << contents;
        return *this;
    }
};

#endif //TEST_SCREEN_H

main.cpp

#include <iostream>
#include "Screen.h"

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";

    return 0;
}
XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXX#XXXX

Process finished with exit code 0

7.28

【出题思路】

函数的返回值如果是引用,则表明函数返回的是对象本身;函数的返回值如果不是引用,则表明函数返回的是对象的副本。

【解答】

返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本。如果我们把一系列这样的操作连接在一起的话,所有这些操作将在同一个对象上执行。

在上一个练习中,mov、set 和 display 函数的返回类型都是 Screen & ,表示我们首先移动光标至 (4, 0) 位置,然后将该位置的字符修改为 # ,最后输出 myScreen 的内容。

相反,如果我们把 mov、set 和 display 函数的返回类型改成 Screen,则上述函数各自只返回一个临时副本,不会改变 myScreen 的值。

7.29

【出题思路】

在编程环境中验证修改前后的程序差别,注意对比运行结果的变化并思考原因(下断点观察程序执行流程,注意观察 this 指针(地址)是否一直指向同一个对象)。

【解答】

验证程序如下所示:

Screen.h

#ifndef TEST_SCREEN_H
#define TEST_SCREEN_H

#include <iostream>

class Screen {
private:
    unsigned height = 0, width = 0;
    unsigned cursor = 0;
    std::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 ch) {
        contents[r * width + c] = ch;
        return *this;
    }
    Screen display(std::ostream &os) {
        os << contents;
        return *this;
    }
};

#endif //TEST_SCREEN_H

main.cpp

#include <iostream>
#include "Screen.h"

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";

    return 0;
}

修改 mov、set 和 display 函数的返回类型后运行程序,得到的结果是:

XXXXXXXXXXXXXXXXXXXX#XXXX
XXXXXXXXXXXXXXXXXXXXXXXXX

Process finished with exit code 0

与练习 7.27 的结果有细微的差别。

7.30

【出题思路】

对比使用 this 指针访问成员的利弊。

【解答】

通过 this 指针访问成员的优点是,可以非常明确地指出访问的是对象的成员,并且可以在成员函数中使用与数据成员同名的形参;缺点是显得多余,代码不够简洁。

7.31

【出题思路】

理解类的声明和定义。声明的作用是告知程序类的名字合法可用;定义的作用是规定类的细节。

【解答】

满足题意的程序如下所示:

class X;        // 声明类型 X
class Y {       // 定义类型 Y
    X *x;
};
class X {       // 定义类型 X
    Y y;
};

类 X 的声明称为**前向(forward declaration)**声明,它向程序中引入了名字 X 并且指明 X 是一种类类型。对于类型 X 来说,此时我们已知它是一个类类型,但是不清楚它到底包含哪些成员,所以它是一个不完全类型。我们可以定义指向不完全类型的指针,但是无法创建不完全类型的对象。

如果试图写成下面的形式,将引发编译器错误。

class Y;        // 声明类型 Y
class X {       // 定义类型 X
    Y y;
};
class Y {       // 定义类型 Y
    X *x;
};

此时我们试图在类 X 中创建不完全类型 Y 的对象,编译器给出报错信息:

error: field has incomplete type 'Y'

7.32

【出题思路】

类可以把其他类定义成友元,也可以把其他类的成员函数定义成友元。当把成员函数定义成友元时,要特别注意程序的组织结构。

【解答】

要想让 clear 函数作为 Screen 的友元,只需要在 Screen 类中做出友元声明即可。本题的真正关键之处是程序的组织结构,我们必须首先定义 Window_mgr 类,其中声明 clear 函数,但是不能定义它(因为 clear 函数的定义会使用 Screen 类的成员,而此时 Screen 类尚未定义,所以在定义 clear 函数前,需要先定义 Screen 类);接下来定义 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 height = 0, width = 0;
    unsigned cursor = 0;
    std::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 << "清理之前 myScreen 的内容是:" << endl;
    cout << myScreen.contents << endl;
    myScreen.contents = "";
    cout << "清理之后 myScreen 的内容是:" << endl;
    cout << myScreen.contents << endl;
}

int main() {
    Window_mgr w;
    w.clear();
    return 0;
}
// 运行结果
清理之前 myScreen 的内容是:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
清理之后 myScreen 的内容是:


Process finished with exit code 0

7.33

【出题思路】

考查类的作用域。

【解答】

如果添加如题目所示的 size 函数将会出现编译错误。因为该函数的返回类型 pos 本身定义在 Screen 类的内部,所以在类的外部无法直接使用 pos。要想使用 pos,需要在它的前面加上作用域 Screen:: 。修改后的程序是:

Screen::pos Screen::size() const {
    return height * width;
}

7.34

【出题思路】

本题考查用于类成员声明的名字查找。

【解答】

这样会导致编译错误,因为对 pos 的使用出现在它的声明之前,此时编译器并不知道 pos 到底是什么含义。

7.35

【出题思路】

理解名字查找与类的作用域的关系,包括用于类成员声明的名字查找和成员定义中的名字查找。

【解答】

编译书中的程序如下所示:

#include <string>

using namespace std;

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;
}

产生如下报错信息:

error: return type of out-of-line definition of 'Exercise::setVal' differs from that in the declaration
Type Exercise::setVal(Type parm) {

代码的含义及 Type 和 initVal 的使用请参考注释。

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

【出题思路】

本题旨在考查使用构造函数初始值列表时成员的初始化顺序,初始化顺序只与数据成员在类中声明的次序有关,而与构造函数初始值列表的顺序无关。

【解答】

在类 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 first_item(cin); 使用了接受 std::istream & 参数的构造函数,该对象的成员值依赖于用户的输入。

Sales_data next; 使用了 Sales_data 的默认构造函数,其中 string 类型的成员 bookNo 默认初始化为空字符串,其他几个成员使用类内初始值初始化为 0.

Sales_data last("9-999-99999-9"); 使用了接受 const string & 参数的构造函数,其中 bookNo 使用实参初始化为 “9-999-99999-9”,其他几个成员使用类内初始值初始化为 0.

7.38

【出题思路】

可以直接在函数声明的地方为 istream & 类型的参数设置默认实参 cin。

【解答】

满足题意的构造函数如下所示:

Sales_data(std::istream &is = std::cin) { is >> *this; }

此时该函数具有了默认构造函数的作用,因此我们原来声明的默认构造函数

Sales_data() = default;

应该去掉,否则会引起调用的二义性。

7.39

【出题思路】

本题考查使用默认实参对构造函数的影响。

【解答】

如果我们为构造函数的全部形参都提供了默认实参(包括为只接受一个形参的构造函数提供默认实参),则该构造函数同时具备了默认构造函数的作用。此时即使我们不提供任何实参地创建类的对象,也可以找到可用的构造函数。

然而,如果按照本题的叙述,我们为两个构造函数同样都赋予了默认实参,则这两个构造函数都具有了默认构造函数的作用。一旦我们不提供任何实参地创建类的对象,则编译器无法判断这两个(重载的)构造函数哪个更好,从而出现了二义性错误。

7.40

【出题思路】

掌握创建类类型的方法,理解不同构造函数的区别。

【解答】

首先选择(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(n), Age(a), Height(h);
};

7.41

【出题思路】

委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些或全部职责委托给了其他构造函数。程序先执行受委托构造函数,然后才执行委托构造函数本身的语句。

【解答】

改写后的 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 first("978-7-121-15535-2", 85, 128, 109);
    Sales_data second;
    Sales_data third("978-7-121-15535-2");
    Sales_data last(cin);
    return 0;
}
// 运行结果
// 977-7-121-16636-1 10 100 85 此行为用户输入
该构造函数接受书号、销售量、原价、实际售价四个信息
该构造函数接受书号、销售量、原价、实际售价四个信息
该构造函数无须接受任何信息
该构造函数接受书号、销售量、原价、实际售价四个信息
该构造函数接受书号信息
该构造函数接受书号、销售量、原价、实际售价四个信息
该构造函数无须接受任何信息
977-7-121-16636-1 10 100 85
该构造函数接受用户输入的信息

Process finished with exit code 0

注:可自己下断点跟踪程序执行过程。

7.42

【出题思路】

委托构造函数是指使用它所属类的其他构造函数执行它自己的初始化过程,因此在类中应该设计一些构造函数使其具备自主的构造函数功能,而把另外一些设计成委托构造函数。

【解答】

以练习 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() : Book("", "", 0, "", "") {}
    Book(std::istream &is) : Book() { is >> *this; }
};

7.43

【出题思路】

因为 NoDefault 仅有的一个构造函数并不是默认构造函数,所以在类 C 中,不能使用无参数的默认构造函数,那样的话,类 C 的 NoDefault 成员无法正确初始化。

【解答】

我们需要为类 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;
    return 0;
}
// 运行结果
0

Process finished with exit code 0

7.44

【出题思路】

理解默认构造函数的用法,理解 vector 对象是如何定义和初始化的。

【解答】

上述语句的含义是创建一个 vector 对象 vec,该对象包含 10 个元素,每个元素的类型都是 NoDefault 且执行默认初始化。然而,因为我们在类 NoDefault 的定义中没有设计默认构造函数,所以所需的默认初始化过程无法执行。编译器会报告这一错误。

7.45

【出题思路】

理解默认构造函数的用法,理解 vector 对象是如何定义和初始化的。

【解答】

与上一个练习相比,如果把 vector 的元素类型更改为 C,则该声明是合法的,这是因为我么给类型 C 定义了带参数的默认构造函数,它可以完成声明语句所需的默认初始化操作。

7.46

【出题思路】

本题旨在考查读者对默认构造函数原理的熟悉程度。

【解答】

(a)是错误的,类可以不提供任何构造函数,这时编译器自动实现一个合成的默认构造函数。

(b)是错误的,如果某个构造函数包含若干形参,但是同时为这些形参都提供了默认实参,则该构造函数也具备默认构造函数的功能。

(c)是错误的,因为如果一个类没有默认构造函数,也就是说我们定义了该类的某些构造函数但是没有为其设计默认构造函数,则当编译器确实需要隐式地使用默认构造函数时,该类无法使用。所以一般情况下,都应该为类构建一个默认构造函数。

(d)是错误的,对于编译器合成的默认构造函数来说,类类型的成员执行各自所属类的默认构造函数,内置类型和复合类型的成员只对定义在全局作用域中的对象执行初始化。

7.47

【出题思路】

explicit 用于抑制类类型的隐式转换,读者需要知道 explicit 的长处和不足。

【解答】

接受一个 string 参数的 Sales_data 构造函数应该是 explicit 的,否则,编译器就有可能自动把一个 string 对象转换成 Sales_data 对象,这种做法显得有些随意,某些时候会与程序员的初衷相违背。

使用 explicit 的优点是避免因隐式类类型转换而带来意想不到的错误,缺点是当用户的确需要这样的类类型转换时,不得不使用略显繁琐的方式来实现。

7.48

【出题思路】

构造函数如果不是 explicit 的,则 string 对象隐式地转换成 Sales_data 对象;相反,构造函数如果是 explicit 的,则隐式类类型转换不会发生。

【解答】

在本题给出的代码中,第一行创建了一个 string 对象,第二行和第三行都是调用 Sales_data 的构造函数(该构造函数接受一个 string)创建它的对象。此处无须任何类类型转换,所以不论 Sales_data 的构造函数是不是 explicit 的,item1 和 item2 都能被正确地创建,它们的 bookNo 成员都是 9-999-99999-9,其他成员都是 0.

7.49

【出题思路】

要想使用隐式的类类型转换,必须遵循一系列规定。如果我们试图在一行代码中使用两种转换规则,编译器将报错。

【解答】

(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

【出题思路】

explicit 的优点是可以避免程序员不期望的隐式类类型转换。

【解答】

class Person {
private:
    string strName;         // 姓名
    string strAddress;      // 地址
public:
    Person() = default;
    Person(const string &name, const string &addr) {
        strName = name;
        strAddress = addr;
    }
    Person(std::istream &is) { read(is, *this); }

    string getName() const { return strName; }          // 返回姓名
    string getAddress() const { return strAddress; }    // 返回地址
    std::istream &read(std::istream &is, Person &per);
    std::ostream &print(std::ostream &os, const Persong &per);
};

我们之前定义的 Person 类(代码如上所示)含有 3 个构造函数,因为前两个构造函数接受的参数个数都不是 1,所以它们不存在隐式转换的问题,当然也不必指定 explicit。

Person 类的最后一个构造函数 Person(std::stream &is); 只接受一个参数,默认情况下它会把读入的数据自动转换成 Person 对象。我们更倾向于严格控制 Person 对象的生成过程,如果确实需要使用 Person 对象,可以明确指定;在其他情况下则不希望自动类型转换的发生。所以,应该把这个构造函数指定为 explicit 的(代码如下所示)。

class Person {
private:
    string strName;         // 姓名
    string strAddress;      // 地址
public:
    Person() = default;
    Person(const string &name, const string &addr) {
        strName = name;
        strAddress = addr;
    }
    explicit Person(std::istream &is) { read(is, *this); }

    string getName() const { return strName; }          // 返回姓名
    string getAddress() const { return strAddress; }    // 返回地址
    std::istream &read(std::istream &is, Person &per);
    std::ostream &print(std::ostream &os, const Persong &per);
};

7.51

【出题思路】

从参数类型到类类型的自动转换是否有意义依赖于程序员的看法,如果这种转换是自然而然的,则不应该把它定义成 explicit 的;如果二者的语义相差甚远,则为了避免不必要的转换,应该指定对应的构造函数是 explicit 的。

【解答】

string 接受的单参数是 const char * 类型,如果我们得到了一个常量字符指针(字符数组),则把它看作 string 对象是自然而然的过程,编译器自动把参数类型转换成类类型也非常符合逻辑,因此我们无须指定为 explicit 的。

与 string 相反,vector 接受的单参数是 int 类型,这个参数的原意是指定 vector 的容量。如果我们在本来需要 vector 的地方提供一个 int 值并且希望这个 int 值自动转换成 vector,则这个过程显得比较牵强,因此把 vector 的单参数构造函数定义成 explicit 的更加合理。

7.52

【出题思路】

熟悉聚合类的概念,理解聚合类初始化的过程及对数据成员的要求。

2.6.1 节(第 64 页)的 Sales_data 类如下所示:

class Sales_data {
    string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

一个类若是聚合类,必须满足的如下 4 个条件:

  • 所有成员都是 public 的。
  • 没有定义任何构造函数。
  • 没有类内初始值。
  • 没有基类,也没有 virtual 函数。

【解答】

程序的意图是对 item 执行聚合类初始化操作,用花括号内的值初始化 item 的数据成员。然而实际过程与程序的原意不符合,编译器会报错。

这是因为聚合类必须满足一些非常苛刻的条件,其中一项就是没有类内初始值,而在 2.6.1 节给出的定义中,数据成员 units_sold 和 revenue 都包含类内初始值。

只要去掉这两个类内初始值,程序就可以正常运行了。

class Sales_data {
    string bookNo;
    unsigned units_sold;
    double revenue;
};

7.53

【出题思路】

本题旨在考查字面值常量类的用法。

【解答】

字面值常量类是一种非常特殊的类类型,聚合类是字面值常量类,某些类虽然不是聚合类但在满足书中所提要求的情况下也是字面值常量类。字面值常量类必须至少提供一个 constexpr 构造函数。

读者参考书中的例子定义 Debug 类即可,这里不再赘述。

7.54

【出题思路】

理解 constexpr 函数的用法。

【解答】

这些以 set_ 开头的成员不能声明成 constexpr,这些函数的作用是设置数据成员的值,而 constexpr 函数只能包含 return 语句,不允许执行其他任务。

7.55

【出题思路】

读者需要掌握字面值常量类的判断方法。

【解答】

因为 Data 类是聚合类,所以它也是一个字面值常量类。

7.56

【出题思路】

本题考查静态成员的含义及用法。

【解答】

静态成员是指声明语句之前带有关键字 static 的类成员,静态成员不是任意单独对象的组成部分,而是由该类的全体对象所共享。

静态成员的优点包括:作用域位于类的范围之内,避免与其他类的成员或者全局作用域的名字冲突;可以是私有成员;通过阅读程序可以非常容易地看出静态成员与特定类关联,使得程序的含义清晰明了。

静态成员与普通成员的区别主要体现在:普通成员与类的对象关联,是某个具体对象的组成部分;而静态成员不从属于任何具体的对象,它由该类的所有对象共享。另外,还有一个细微的区别,静态成员可以作为默认实参,而普通数据成员不能作为默认实参。

7.57

【出题思路】

本题练习在自定义的类中使用静态成员。

【解答】

读者参考书中的例子定义 Account 类即可,这里不再赘述。

7.58

【出题思路】

本题旨在考查静态成员的用法。

【解答】

本题的程序存在如下几处错误:

在类的内部,rate 和 vec 的初始化是错误的,因为除了静态常量成员之外,其他静态成员不能在类的内部初始化。另外,example.C 文件的两条语句也是错误的,因为在这里我们必须给出静态成员的初始值(类外初始化静态成员)。

正确的版本如下所示:

// example.h
class Example {
public:
  static double rate; // = 6.5;
  // static member should be initialize ouside class
  static const int vecSize = 20;
  static vector<double> vec; //(vecSize);
  // 1. cannot use parentheses as in-class initializer
  // 2. static member should be initialize ouside class
};

// example.C
#include "example.h"
double Example::rate = 6.5;
// should initialize static data member
vector<double> Example::vec(vecSize);
// should initialize static data member
34

评论区