C++Primer第五版 第7章 类 7.5构造函数再探 练习
7.5 构造函数再探
7.5.1 构造函数初始值列表
练习7.36 下面的初始值是错误的,请找出问题所在并尝试修改它。
struct X {
X (int i, int j): base(i), rem(base % j) { }
int rem, base;
};
答:成员的初始化顺序与他们在类定义中的出现顺序一致;在此类中rem在定义中先出现,则先初始化rem,因此rem的初始值的效果是使用未定义值的base去初始化rem,程序会出错。
修改:
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");
}
答:first_item使用了
Sales_data::Sales_data(istream &is){read(is, *this);}
构造函数;数据成员的值为用户输入的自定义值;
next使用了默认构造函数Sales_data() = default
;或Sales_data(const string &s= ""): bookNo(s) { }
,数据成员的值bookNo为"";units_sold = 0;revenue = 0
;
last使用了Sales_data(const string &s): bookNo(s) { }
构造函数;数据成员的值bookNo为"9-999-99999-9";units_sold = 0;revenue = 0
。
练习7.38 有些情况下我们希望提供cin作为接受istream&参数的构造函数的默认实参,请声明这样的构造函数。
Sales_data::Sales_data(istream &is = cin){
read(is, *this);
}
练习7.39 如果接受string的构造函数和接受istream&的构造函数都使用默认实参,这种行为合法吗?如果不,为什么?
答:不合法,如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。本题两个构造函数都使用默认实参,即都具有了默认构造函数的作用,当我们不提供任何实参创建类的对象时,编译器无法确定使用哪一个构造函数,出现二义性错误。
练习7.40 从下面的抽象概念中选择一个(或者你自己指定一个),思考这样的类需要哪些数据成员,提供一组合理的构造函数并阐明这样做的原因。
( a )Book
class Book{
public:
Book() = default; // 默认构造函数
Book(const string &s, const string &n, const string &a, double p): bookNo(s), bookName(n), author(a), price(p) { }
Book(istream &is){
read(is,*this);
}
istream &read(istream& is, Book& item){
is >> item.bookNo >> item.bookName >> item.author>> item.price;
return is;
}
private:
string bookNo; // 书isbn编号
string bookName; // 书名
string author; // 书的作者
double price = 0.0; // 单价
};
7.5.2 委托构造函数
练习7.41 使用委托构造函数重新编写你的Sales_data类,给每个构造函数体添加依据语句,令其一旦执行就打印一条信息。用各种可能的方式分别创建Sales_data对象,认真研究每次输出的信息直到你确实理解了委托构造函数的执行顺序位置。
Sales_data.h
#include <iostream>
#include <string>
using namespace std;
class Sales_data{
//Sales_data的非成员接口函数
friend Sales_data add(const Sales_data&, const Sales_data&);
friend ostream &print(ostream&, const Sales_data&);
friend istream &read(istream&, Sales_data&);
public:
//委托构造函数7.5.2
//非委托构造函数使用对应的实参初始化成员
Sales_data(const string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n)
{
cout << "非委托构造函数使用对应的实参初始化成员" << endl;
}
//其余构造函数全部委托给另一个构造函数
Sales_data():Sales_data("", 0, 0)
{
cout << "默认构造函数委托3参数" << endl;
}
Sales_data(string s):Sales_data(s,0,0)
{
cout << "string构造函数委托3参数" << endl;
}
Sales_data(istream &is):Sales_data()
{
read(is, *this);
cout << "istream&构造函数委托默认,又委托3参数" << endl;
}
//关于Sales_data对象的操作
string isbn() const {return bookNo;}//返回对象的isbn编号
Sales_data& combine(const Sales_data&);//将一个Sales_data对象加到另一个对象上
private:
double avg_price() const;//返回售出书籍的平均价格
string bookNo; //表示isbn编号
unsigned units_sold = 0; //书的销量
double revenue = 0.0; //书的总 销售收入
};
//Sales_data的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
ostream &print(ostream&, const Sales_data&);
istream &read(istream&, Sales_data&);
Sales_data.cpp
#include <iostream>
#include <string>
#include "Sales_data.h"
using namespace std;
inline
double Sales_data::avg_price()const{//常量成员函数,不会改变Sales_data对象的值
if(units_sold)
return revenue/units_sold;
else
return 0;
}
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold; //把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this; //返回调用该函数的对象
}
Sales_data add(const Sales_data& lhs, const Sales_data& rhs){
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
ostream &print(ostream& os, const Sales_data& item){
//将给定对象的内容打印到给定的流当中
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
istream &read(istream& is, Sales_data& item){
// 从给定流中将数据读到给定对象中
double price = 0.0;//每本书的单价
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
void test(){
Sales_data total01;
print(cout,total01);
cout << endl << endl;
Sales_data total02("0-201-78345-X");
print(cout,total02);
cout << endl << endl;
Sales_data total03("0-201-78345-X",3,3.59);
print(cout,total03);
cout << endl << endl;
Sales_data total04(cin);
print(cout,total04);
}
int main(){
test();
system("pause");
return 0;
}
练习7.42 对于你在练习7.40(参见7.5.1节,第261页)中编写的类,确定哪些构造函数可疑使用委托。如果可以的话,编写委托构造函数。如果不可以,从抽象概念列表中重新选择一个你认为可以使用委托构造函数的,为挑选出的这个概念编写类定义。
class Book{
public:
Book(const string &s, const string &n, const string &a, double p): bookNo(s), bookName(n), author(a), price(p) { }
Book():Book("", "", "", 0){}
Book(string s):Book(s,"","",0){}
Book(istream &is):Book(){
read(is,*this);
}
istream &read(istream& is, Book& item){
is >> item.bookNo >> item.bookName >> item.author>> item.price;
return is;
}
private:
string bookNo; // 书isbn编号
string bookName; // 书名
string author; // 书的作者
double price = 0.0; // 单价
};
7.5.3 默认构造函数的作用
练习7.43 假定有一名为NoDefault的类,它有一个接受int的构造函数,但是没有默认构造函数。定义类C,C有一个NoDefault类型的成员,定义C的默认构造函数。
class NoDefault{
public:
NoDefault(int i){val = i;}
int val;
};
class C{
public:
C(int i = 0):nd(i){}
NoDefault nd;
};
练习7.44 下面这条声明合法吗?如果不,为什么?
vector<NoDefault> vec(10);
答:该语句创建了一个vector对象vec,包含10个NoDefault类型的元素,对vec中的元素进行默认初始化;前文中我们没有为NoDefault定义默认构造函数,所以不合法;(如果NoDefault中定义了默认构造函数,那么是合法的;)
练习7.45 如果在上一个练习中定义的vector的元素类型是C,则声明合法吗?为什么?
答:合法;我们在练习7.43中为类C定义了带参数的默认构造函数
练习7.46 下面那些论断是不正确的?为什么?
(a)一个类必须至少提供一个构造函数
(b)默认构造函数是参数列表为空的构造函数
(c)如果对于类来说不存在有意义的默认值,则类不应该提供默认构造函数。
(d)如果类没有定义默认构造函数,则编译器将为其生成一个并把每个数据成员初始化成相应类型的默认值。
答:(a)不正确;类可以不提供任何构造函数,编译器会为其自动生成一个默认构造函数;
(b)不正确; 如果某个构造函数包含若干形参,但是同时为这些形参都提供了默认实参,则该构造函数也具有默认构造函数的功能。
(c)不正确; 以防万一,应该为类提供一个默认构造函数;
(d)不正确,对于编译器合成的默认构造函数来说,类类型的成员执行各自所属类的默认构造函数,内置类型和符合类型的成员支队定义在全局作用域中的对象执行初始化。
7.5.4 隐式的类类型转换
练习7.47 说明接受一个string参数的Sales_data构造函数是否应该是explicit的,并解释这样做的优缺点。
答:应声明时explicit的;
优点:防止编译器自动将string对象隐式转换成Sales_data对象,改变程序初衷或带来其他程序错误;
缺点:当我们想要实现这种类类型转换效果时,需要显示地强制进行转换。
练习7.48 假定Sales_data的构造函数不是explicit的,则下述定义将执行什么样的操作?
如果Sales_data的构造函数是explicit的,又会发生什么呢?
string null_isbn("9-999-99999-9");
Sales_data item1(null_isbn);
Sales_data item2("9-999-99999-9");
答:
item1
和item2
都是使用了string的构造函数创建的对象;无论是否声明explicit,都不影响对象的创建。
练习7.49 对于combine函数的三种不同声明,当我们调用i.combine(s)时分别发生了什么情况?其中i是一个Sales_data,而s是一个string对象。
(a) Sales_data &combine(Sales_data);
(b) Sales_data &combine(Sales_data&);
(c) Sales_data &combine(const Sales_data&) const;
答:在使用string的构造函数没有声明explicit的情况下:
(a) 正确;编译器使用给定的string对象s自动创建一个Sales_data对象,然后将这个临时对象传递给combine的形参,函数正确执行并返回结果。
(b) 错误,用类型Sales_data的右值初始化类型为Sales_data&的非常量引用无效;同a生成了一个Sales_data临时对象,但是combine函数的参数是一个非常量引用,临时对象无法传递给combine;修改为Sales_data &combine(const Sales_data&);
即可
© 错误,combine是常量成员函数,无法修改数据成员的值;
练习7.50 确定在你的Person类中是否有一些构造函数应该是explicit的。
答:Person(istream& is)
可以声明为explicit;一共三个构造函数,其余两个分别是无参和我包括两个参数的构造函数,不存在隐式转换的问题。
练习7.51 vector将其单参数的构造函数定义成explicit的,而string不是,你觉得原因何在?
答:string构造函数是接受一个单参数的const char*类型,如果我们得到了一个常量字符指针(字符数组),隐式转换机制将其转换成string类型,是符合程序本意的;
vector构造函数接受一个容量参数,即int类型,程序本意是指定了vector的容量,隐式转换机制将int类型转换成vector类型,不符合程序本意,所以定义成explicit更为合理。
7.5.5 聚合类
练习7.52 使用2.6.1节(第64页)的Sales_data类,解释下面的初始化过程。如果存在问题,尝试修改它。
struct Sales_data {
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data item = {"978-0590353403", 25, 15.99};
答:有问题;执行聚合类初始化操作不能有类内初始值;
修改:去掉类内初始值就满足了聚合类操作的条件。
7.5.6 字面值常量类
练习7.53 定义你自己的Debug。
class Debug{
public:
constexpr Debug(bool b = true): rt(b), io(b), other(b) {}
constexpr Debug(bool r, bool i, bool o): rt(r), io(i), other(o) {}
constexpr bool any() { return rt || io || other; }
void set_io(bool b) { io = b; }
void set_rt(bool b) { rt = b; }
void set_other(bool b) { other = b; }
private:
bool rt;//运行错误
bool io;//IO错误
bool other;//其他错误
};
练习7.54 Debug中以set_开头的成员应该被声明成constexpr吗?如果不,为什么?
答:不能,set_开头的函数作用是设置数据成员的值,而constexpr函数要求它能拥有的唯一可执行语句就是返回语句,不能执行其他语句。
练习7.55 7.5.5节(第266页)的Data类是字面值常量类,请解释原因。
答: 是,因为Data是聚合类,所以他也是一个字面值常量。