7.3 修改7.1.1节交易程序,令其使用这些成员
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
};
void p7_3()
{
Sales_data total;//保存下一条交易记录变量
//读入第一条交易记录,确保有数据可以处理
double price = 0;
if (cin >> total.bookNo >> total.units_sold >> price)
{
total.revenue = total.units_sold * price;
Sales_data trans; // 保存随后输入的记录
while (cin >> trans.bookNo >> trans.units_sold >> price)
{
trans.revenue = trans.units_sold * price;
if (total.isbn() == trans.isbn())
{
total.combine(trans);
}
else {
//打印前一本书结果
cout << total.bookNo << " "
<< total.units_sold << " "
<< total.revenue << " ";
if (total.units_sold)
cout << (total.revenue / total.units_sold) << endl;
total = trans;
}
}
//打印最后一本书的结果
cout << total.bookNo << " "
<< total.units_sold << " "
<< total.revenue << " ";
if (total.units_sold)
cout << (total.revenue / total.units_sold) << endl;
}
else
{
cerr << "No data?!" << endl;
return;
}
}
7.10 在下面这条if语句中,条件部分的作用是什么
if(read(read(cin, data1), data2))
先从cin获取输入数据写入到对象data1,返回一个istream对象引用若输入正确,再一次获取输入数据写入到对象data2中。
7.12 把只接受一个istream作为参数的构造函数定义移到类的内部
Sales_data.h
#pragma once
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include<iostream>
#include<string>
using std::ostream;
using std::istream;
using std::string;
using std::cin;
using std::cout;
using std::cerr;
using std::endl;
struct Sales_data; //必须要提前声明,告诉编译器,后面有出现Sales_data是合法的。
//可以类比cin返回的类型理解
istream& read(istream& is, Sales_data& item);
ostream& print(ostream& os, const Sales_data& item);
Sales_data add(const Sales_data& lhs, const Sales_data& rhs);
struct Sales_data {
Sales_data() = default;
Sales_data(const string& s) :bookNo(s) {}
Sales_data(const string& s, unsigned n, double p) :
bookNo(s), units_sold(n), revenue(p* n) {}
Sales_data(istream& is)
{
//编译器跑到Sales_data.cpp文件中找函数的实现(定义)
read(is, *this); //从键盘上输入的数据输入到调用该函数的对象中
}
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
string isbn() const;
double avg_price() const;
Sales_data& combine(const Sales_data& rhs);
};
#endif
Sales_data.cpp
#include "Sales_data.h" // 通过这个包含就带有一系列的#include<iostream>、using std::....;
string Sales_data::isbn() const
{
return bookNo;
}
double Sales_data::avg_price() const
{
if (units_sold)
return revenue / units_sold;
return 0;
}
Sales_data& Sales_data::combine(const Sales_data& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
Sales_data add(const Sales_data& lhs, const Sales_data& rhs)
{
Sales_data sum = lhs;//对类对象的拷贝,实际上是将lhs成员变量bookNo、units_sold、revenue拷贝到sum的对应成员上
sum.combine(rhs);
return sum;
}
//可以类比cin返回的类型理解这里read返回类型
istream& read(istream& is, Sales_data& item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = item.units_sold * price;
return is;
}
ostream& print(ostream& os, const Sales_data& item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();//尽量不要在这里换行
return os;
}
调用的部分
#include<iostream>
#include<string>
#include"Sales_data.h"
#include"Person.h"
void p7_11()
{
Sales_data item1;
print(cout, item1) << endl;
Sales_data item2("0-201-88728");
print(cout, item2) << endl; //print返回的是标准输出流对象ostream
Sales_data item3("0-203-99283", 10, 4);
print(cout, item3) << endl;
Sales_data item4(cin);
print(cout, item4) << endl;
}
int main()
{
p7_11();
return 0;
}
7.14 编写一个构造函数,令其用我们提供的类内初始值显式的初始化成员
Sales_data():bookNo(""), units_sold(0), revenue(0) {}
7.16 在类定义中对访问说明符出现的位置和次数有限定吗?如果有,是什么?什么样的成员应该定义在public说明符之后?什么样的成员应该定义在private说明符后?
说明符出现的位置和次数没有限定。
构造函数和部分成员函数(应该提供给使用者的)即希望外部使用的接口应该定义在public说明符后。
数据成员和作为实现部分的函数即 不希望外部访问的成员则跟在private说明符后面。
7.17 使用class和struct 时有什么区别吗?如果有,是什么?
有区别,class的默认访问权限是private,而struct的默认访问权限是public
7.18 封装的含义和用处?
封装有两个重要的优点:
- 确保用户代码不会无意间破坏封装对象的状态。
- 被封装的类具体实现可以随时改变,而无需调整用户级别的代码
- 隐藏了实现的细节,只保留了接口给用户调用
7.19 在你的Person类中,你将把哪些成员声明成public?哪些声明成private,解释这么做的原因
构造函数和getName()以及getAddress()声明成public,因为这些都是要提供给类对象使用者进行使用的。
数据成员name和address声明成private,因为不让用户进行修改这些成员变量从而提高对象的安全性。
7.20 友元在什么时候有用?请列举使用友元的利弊
优点是类可以允许其他类或者函数访问它的非公有成员,可以令其他类或者函数成为它的友元。
缺点是友元如果设计的不好使得其他类或者函数能随意访问修改数据成员,导致数据的破坏,提高了维护代码及修正程序错误的难度。
牺牲了封装性与可维护性
7.25 Screen类能安全地依赖拷贝和赋值操作的默认版本吗?为什么?
Screen类可以依赖于默认版本的复制和拷贝操作。因为没有分配类对象以外的资源,且内部成员都支持默认复制和拷贝操作。若是管理动态内存的类则不能依赖于拷贝和赋值操作的默认版本,而且也应该尽量使用string和vector来避免动态管理内存的复杂性
7.30 通过this指针使用成员的做法虽然合法,但是有点多余。讨论显式地使用指针访问成员的优缺点。
优点:如果出现成员函数形参和成员变量有同名的时候,用this指针显示访问成员,能够明确访问的是对象的成员而不是函数的形参。
缺点:函数参数和成员变量没有重名时候,使用this 优点多余
7.36 下面的初始值是错误的,请找出问题所在并尝试修改它
struct X{
X(int i, int j):base(i), rem(base % j){}
int rem, base;
}
编译器先对rem进行初始化,base尚未定义,利用base %j则也是得到未定义的数,故错误
应修改为:
struct X{
X(int i, int j):rem(i % j), base(i){}
int rem, base;
}
7.38 有些情况下我们希望提供cin作为接受istream&参数的构造函数的默认实参,请声明这样的构造函数
Sales_data(istream& is = cin);
7.39 如果接受string的构造函数和接受istream&的构造函数都使用默认实参,这种行为合法吗?如果不,为什么
不合法,因为这样当不给任何参数时可选的构造函数不止一个。导致无法匹配到正确的重载函数
7.40 请为Tree这个抽象概念添加一些合理的数据成员和构造函数
class Tree
{
public:
Tree() = default;
Tree(int d):date(d){}
Tree(int d, Tree *l, Tree *r):data(d), left(l), right(r){}
private:
int data = 0;
Tree *left = nullptr;
Tree *right = nullptr;
}
7.43 假定有一个名为NoDefault的类,它有一个接受int的构造函数,但是没有默认构造函数。定义类C,C有一个NoDefault类型的成员,定义C的默认构造函数
class NoDefault {
public:
NoDefault(int n){}
};
class C {
public:
//用0去初始化NoDefault类型的成员对象
C():def(0){}
private:
NoDefault def;
};
7.44 下面这条声明合法吗?如果不,为什么
vector<NoDefault> vec(10);
不合法,因为NoDefault没有默认构造函数,无法对10个NoDefault对象进行默认初始化
7.45 如果在上一个练习中定义的vector的元素类型是C,则声明合法吗?为什么?
合法,因为C有默认构造函数,能够对C进行默认初始化
7.46 下面哪些论断是不正确的?为什么?
(a)一个类必须至少提供一个构造函数
(b)默认构造函数是参数列表为空的构造函数。
(c)如果对于类来说不存在有意义的默认值,则类不应该提供默认构造函数
(d)如果类没有定义默认构造函数,则编译器将为其生成一个并把每个数据成员初始化成相应的类型
(a) 不正确,如果我们的类没有没有显式地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数,称为合成的默认构造函数。
(b)不完全正确,因为只要有构造函数为所有成员变量提供了默认实参,就可以是默认构造函数,不一定要求默认构造函数列表为空。
©不正确,哪怕没有意义的值也需要初始化。
(d)不正确,只有当一个类没有定义任何构造函数的时候,编译器才会生成一个默认构造函数。
7.47 说明接受一个string参数的Sales_data构造函数是否应该是explicit的,并解释这样做的优缺点
是否需要从string 到 Sales_data的转换依赖于我们对用户使用该转换的看法,此例中,这种转换可能是对的null_book中的 string可能表示了一个不存在的ISBN号
优点:可以抑制构造函数定义的隐式转换
缺点:为了转换要显式的使用构造函数
7.48 假定Sales_data的构造函数不是explicit的,则下述定义将执行什么样的操作
string null_isbn("9-999-99999-9");
Sales_data item1(null_isbn);
Sales_data item2("9-999-99999-9");
如果Sales_data的构造函数是explicit的,又会发生什么呢?
答:两行代码都是直接初始化
1.Sales_data构造函数不是explicit时,
//下面语句调用Sales_data接受string实参的构造函数初始化对象item1
Sales_data item1(null_isbn);
//编译器将将字符串字面值转换为string,然后调用Sales_data接受string实参的构造函数初始化对象item2
Sales_data item2("9-999-99999-9");
2.Sales_data构造函数为explicit,只是参数不能隐式转换为类类型。
//下面语句调用Sales_data接受string实参的构造函数初始化对象item1
Sales_data item1(null_isbn);
//先将字符串转隐式换为string类型,然后调用Sales_data的接受string实参的构造函数初始化对象item2
Sales_data item2("9-999-99999-9");
对以上代码,是不是explicit都不影响
7.49 对于combine 函数的三种不同声明,当我们调用i.combine(s)
时分别发生什么情况?其中i是一个Sales_data,而s是一个string对象
(a)Sales_data &combine(Sales_data);
(b)Sales_data &combine(Sales_data&);
(a)Sales_data &combine(const Sales_data&) const;
(a)会将string对象s隐式转换成Sales_data,这个转换执行了一个接受string对象的Sales_data的构造函数,创建出一个临时对象拷贝给combine形参,然后执行函数体
(b)会出错,因为string对象s隐式转换成Sales_data对象,这是一个临时Sales_data对象,不可以用于初始化一个普通引用。
©不正确,因为const成员函数就是相当于隐式传入了一个const Sales_data * const 类型的指针,不可以修改成员变量,但是如果函数体空的时候,实参string对象s转换隐式转换为Sales_data临时对象,然后用于初始化常量引用。
7.50 确定在你的Person类中是否 有一些构造函数应该是explicit的
explicit Person(std::istream &is){read(is, *this);}
原因是允许一个istream& 类型对象可以隐式转换为一个Person对象不大合理
7.51 vector将其单参数构造函数定义成explicit的而string不是,原因何在?
因为vector的单参数构造函数若不定义为explicit,那么支持从int类型参数隐式转换为vector对象时则
vector<int > ivec = 10; //拷贝初始化
上方语句成立,则无法清楚是初始化10个int对象,还是初始化一个值为10的int对象。
而对于string对象,单参数构造函数,参数一般为string,下方语句很自然的应该是用"haha"来初始化一个string对象,允许将const char*对象隐式转换成string对象,其中执行了接受一个const char *参数的构造函数创建出 一个临时对象,然后拷贝初始化s。
string s = "haha"; //通过调用对应构造函数,隐式将参数转换为类类型对象。
凡是在需要用到string的地方都可以用const char * 代替,这是很自然的。
7.53 定义你自己的Debug
class Debug {
public:
constexpr Debug(bool b = true) :hw(b), io(b), other(b) {}
constexpr Debug(bool h, bool i, bool o) : hw(h), io(i), other(o) {}
constexpr bool any() const { return hw || io || other; }
void set_io(bool b) { io = b; }
void set_hw(bool b) { hw = b; }
void set_other(bool b) { other = b; }
private:
bool hw; //硬件错误
bool io; //io错误
bool other; //其他错误
};
7.54 Debug中以set_开头的成员应该被声明成constexpr吗?若不,为什么?
不应该,因为constexpr函数必须包含一个返回语句(constexpr构造函数除外)
7.55 如下Data类是字面值常量类吗,请解释原因
struct Data{
int ival;
string s;
};
不是,因为string不是字面值类型。
7.56 什么是类的静态成员?它有什么优点?静态成员与普通成员有何区别?
-
一些与类本身直接相关而不是与类的各个具体对象保持关联的成员
-
一些类的通用的成员可以不用每个对象重复存储,能够被该类所有的对象所共享
-
区别有如下:
-
静态成员声明之前要加上关键字static,而普通成员不需要
-
静态成员可以使用类名通过作用域运算符直接访问,也可以通过类对象和引用以及指向对象的指针来访问;而普通成员必须通过类对象来访问。
-
静态成员可以作为成员函数的默认实参,而普通成员不行
-
静态数据成员的类型可以就是它所属的类类型,而普通成员只能声明成它所属的类的指针或引用
-
7.57 编写你自己的Account类
class Account {
public:
void calculate() { amount += amount * interestRate; }
//求当前利率
static double rate() { return interestRate; }
//修改利率
static void rate(double);
private:
std::string owner;
double amount;
//静态成员想要在类内进行初始化必须要是constexpr
static constexpr double todayRate = 4.44;
static double interestRate; //如果在类外定义时不要再加static了
//初始化利率
static double initRate() { return todayRate; } //静态成员函数只能使用静态数据成员
};
double Account::interestRate = initRate();
//编译器读到Account时就进入到类的作用域之内了
void Account::rate(double newRate)
{
interestRate = newRate;//调用的是类的静态成员interestRate
}
7.58 下面的静态数据成员的声明和定义有错误吗?请解释原因
//example.h
class Example {
public:
static double rate = 6.5;
static const int VecSize = 20;
static vector<double> vec(vecSize);
};
//example.cpp
#include"example.h"
double Example::rate;
vector<double> Example::vec;
有错误,rate必须是一个常量表达式(constexpr)才可以再类内进行初始化。类内只能初始化整型静态常量,所以不能在类内初始化vec。修改后为:
//example.h
class Example {
public:
static constexpr double rate = 6.5;
static const int VecSize = 20;
static vector<double> vec;
};
//example.cpp
#include"example.h"
constexpr double Example::rate;
vector<double> Example::vec(Example::vecSize);