类的基本思想是:数据抽象和封装。
成员函数的声明在类的内部,定义则既可以在类的内部也可以在类的外部。
class定义的类,是希望所有成员是public,struct定义的类,是希望所有成员是private。
使用explicit的优点是避免因隐式转换而带来的意想不到的错误,缺点是用户的确需要这样的类类型转换时,不得不使用略显繁琐的方式来实现。
练习 7.1:使用2.6.1节练习定义的Sales data 类为1.6 节(第21 页)的交易处理程序编写一个新版本。
// 练习 7.1
// 现在只考虑使用,还没有考虑实现
#include
#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;
}
}
cout << total << endl;
}
else
{
cerr << "No data?" << endl;
return -1;
}
system("puase");
return 0;
}
练习 7.2:曾在2.6.2 节的练习(第67 页)中编写了一个Sales data 类,请向这个类添加combine 和isbn 成员。
// 练习 7.2
class Sales_data
{
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)
{
discout = saleprice / sellingprice; //重新计算实际折扣
}
return *this; //返回合并后的结果
}
private: //定义私有数据成员
string bookNo; //书籍编号,隐式初始化为空串
unsigned units_sold = 0; //销售量,显示初始化为0
double sellingprice = 0.0; //原始价格,显示初始化为0.0
double saleprice = 0.0; //实售价格,显示初始化为0.0
double discout = 0.0; //折扣,显示初始化为0.0
};
练习 7.3:修改7.1.1节(第229页)的交易处理程序,令其使用这些成员。
// 练习 7.3
//程序里关于输入输出流存在一些问题,不存在操作类型为std::iostream >> Sales_data
#include
#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.combine(trans);
}
else
{
cout << total << endl;
total = trans;
}
}
cout << total << endl;
}
else
{
cerr << "No data?" << endl;
return -1;
}
system("puase");
return 0;
}
练习 7.4:编写一个名为Person 的类, 使其表示人员的姓名和住址。使用string 对象存放这些元素,接下来的练习将不断充实这个类的其他特征。
// 练习 7.4
struct Person
{
//添加数据成员
string strName; //姓名
string strAddress; //住址
};
练习 7.5:在你的Person 类中提供一些操作使其能够返回姓名和住址。这些函数是否应该是const 的呢?解释原因。
// 练习 7.5
struct Person
{
//添加数据成员
string strName; //姓名
string strAddress; //住址
string getName() const { return strName; }
string getAddress() const { return getAddress; }
};
这些函数应该是const函数,因为在这些函数中,并没有改变调用该函数对象的数据成员的值,
练习 7.6:对于函数add、read和print,定义你自己的版本。
// 练习 7.6
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
istream &read(istream &is, Sales_data &item)
{
is >> item.bookNo >> item.units_sold >> item.sellingprice >> item.saleprice;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " " << item.sellingprice
<< " " << item.saleprice << " " << item.discout;
}
练习 7.7:使用这些新函数重写7.1.2节(第233 页)练习中的交易处理程序。
// 练习 7.3
//程序里关于输入输出流存在一些问题,不存在操作类型为std::iostream >> Sales_data
#include
#include "Sales_data.h"
using namespace std;
int main()
{
cout << "请输入交易记录(ISBN、销售量、原价、实际售价):" << endl;
Sales_data total; //保存下一条交易记录的变量
// 读入第一条交易记录,并确保有数据可以处理
if (read(cin, total))
{
Sales_data trans; //保存和的变量
//读入并处理剩余交易记录
while (read(cin, total))
{
//如果我们仍在处理相同的书
if (total.isbn() == trans.isbn())
{
total = add(total, trans); //更新销售zonge
}
else
{
//打印前一本的结果
print(cout, total) << endl;
cout << endl;
total = trans;
}
}
print(cout, total);
cout << endl;
}
else
{
cerr << "No data?" << endl;
return -1;
}
system("puase");
return 0;
}
练习 7.8:为什么read 函数将其Sales data 参数定义成普通的引用,而print 将其参数定义成常量引用?
答:因为在read函数中,我们需要从标准输入流中读取数据并将其写入到给定的Sales_data对象,因此需要有修改对象的权限。而print函数将其参数定义成常量引用是因为它只负责数据的输出,不对其做任何的更改。
练习 7.9:对于7.1.2节(第233 页)练习中的代码,添加读取和打印Person 对象的操作。
#include
#include
using namespace std;
struct Person
{
//添加数据成员
string strName; //姓名
string strAddress; //住址
string getName() const { return strName; } //返回姓名
string getAddress() const { return strAddress; } //返回住址
};
//从标准输入中读取姓名和住址
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.strName << per.strAddress;
return os;
}
练习 7.10:在下面这条if 语句中,条件部分的作用是什么?
if(read(read(cin, data1), data2))
这里其实是引用的引用,read函数返回的类型是istream &,因此read(cin, data1)返回的istream &又可以作为外层read的shican使用。该条件检验读入data1和data2的过程是否正确,如果正确,条件满足,否则,条件不满足。
练习 7.11:在你的Sales data类中添加构造函数,然后编写一段程序令其用到每个构造函数。
// 练习 7.11
class Sales_data
{
public:
Sales_data() = default;
Sales_data(const std::string &book): bookNo(book) { }
Sales_data(const std::string &book, const unsigned num, const double sellp, const double salep);
Sales_data(std::istream &is);
private:
std::string bookNo; //书籍编号,隐式初始化为空串
unsigned units_sold = 0; //销售量,显示初始化为0
double sellingprice = 0.0; //原始价格,显示初始化为0.0
double salesprice = 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)
{
is >> *this;
}
// 练习 7.11
#include
#include "Sales_data.h"
using namespace std;
int main()
{
Sales_data data1;
Sales_data data2("978-7-121-15535-2");
Sales_data data3("978-7-121-15535-2", 100, 128, 109);
Sales_data data4(cin);
cout << "书籍的销售情况是:" << endl;
cout << data1 << "\n" << data2 << "\n" << data3 << "\n" << data4 << "\n";
system("pause");
return 0;
}
练习 7.12:把只接受一个istream 作为参数的构造函数定义移到类的内部。
// 练习 7.12
class Sales_data
{
public:
Sales_data() = default;
Sales_data(const std::string &book) : bookNo(book) { }
Sales_data(const std::string &book, const unsigned num, const double sellp, const double salep);
Sales_data::Sales_data(std::istream &is){ is >> *this; }
private:
std::string bookNo; //书籍编号,隐式初始化为空串
unsigned units_sold = 0; //销售量,显示初始化为0
double sellingprice = 0.0; //原始价格,显示初始化为0.0
double salesprice = 0.0; //实售价格,显示初始化为0.0
double discount = 0.0; //折扣,显示初始化为0.0
};
练习 7.13:使用istream 构造函数重写第229 页的程序。
// 练习 7.13
#include
#include "Sales_data.h"
using namespace std;
int main()
{
Sales_data total(cin); //保存当前求和结果的变量
if (cin)
{ //读入第一笔交易
Sales_data trans(cin); //保存下一笔交易
do
{ //读入剩余的交易
if (total.isbn() == trans.isbn()) //检查isbn
total.combine(trans); //更新变量total当前的值
else
{
print(cout, total) << endl; //输出结果
total = trans; //处理下一本书
}
} while (read(cin, trans));
print(cout, total) << endl; //输出最后一条交易
}
else
{
cerr << "No data?" << endl;
}
system("puase");
return 0;
}
练习 7.14:编写一个构造函数, 令其用我们提供的类内初始值显式地初始化成员。
// 练习 7.14
Sales_data(const std::string &book)
: bookNo(book), units_sold(0), sellingprice(0.0), saleprice(0.0), discout(0.0) { }
练习 7.15:为你的Person类添加正确的构造函数。
//练习 7.15
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,我们就能完成类的封装。封装实现了类的接口的实现的分离。
封装有两个重要的优点:一是确保用户代码不会无意间破坏封装对象的状态;二是被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
练习 7.19:在你的Person 类中, 你将把哪些成员声明成public 的?哪些声明成private 的?解释你这样做的原因。
答:我们将数据成员strName和strAddress设置为private,这样可以避免用户程序不经意间修改和破坏它们,同时把构造函数和两个获取数据成员的接口函数设置为public,以便于我们在类的外部访问。
练习 7.20:友元在什么时候有用?请分别列举出使用友元的利弊。
答:友元为类的非成员接口函数提供了访问其私有成员的能力,当非成员函数确实需要访问类的私有成员时,我们可以把它声明成该类的友元,此时友元可以访问类的所有数据和函数。但是一旦友元使用不慎,就有可能破坏类的封装性。
练习 7.21:修改你的Sales data 类使其隐藏实现的细节。你之前编写的关于Sales data 操作的程序应该继续使用, 借助类的新定义重新编译该程序, 确保其工作正常。
// 练习 7.21
class Sales_data
{
//友元的声明,友元最好在类初或者类尾集中声明。
friend Sales_data add(const Sales_data &, const Sales_data &);
friend Sales_data read(std::istream &, Sales_data &);
friend Sales_data print(std::ostream &, const Sales_data &);
public:
Sales_data() = default;
Sales_data(const std::string &book) : bookNo(book) { }
Sales_data(const std::string &book, const unsigned num, const double sellp, const double salep);
Sales_data::Sales_data(std::istream &is){ is >> *this; }
private:
std::string bookNo; //书籍编号,隐式初始化为空串
unsigned units_sold = 0; //销售量,显示初始化为0
double sellingprice = 0.0; //原始价格,显示初始化为0.0
double salesprice = 0.0; //实售价格,显示初始化为0.0
double discount = 0.0; //折扣,显示初始化为0.0
};
//三个非成员函数的声明和定义,和之前类似,记得要重新编译
练习 7.22:修改你的Person 类使其隐藏实现的细节。
// 练习 7.22
//通过把数据成员设置为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 类。
// 练习 7.23
class Screen
{
private:
unsigned height = 0, width = 0;
unsigned cursor = 0;
string contets;
};
练习 7.24:给你的Screen 类添加三个构造函数: 一个默认构造函数;另一个构造函数接受宽和高的值,然后将content初始化成给定数量的空白: 第三个构造函数接受宽和高的值以及一个字符, 该字符作为初始化之后屏幕的内容。
// 练习 7.24
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 能安全地依赖于拷贝和赋值操作的默认版本吗?如果能,为什么?如果不能, 为什么?
答:含有指针数据成员一般不宜使用默认的拷贝和复制操作,如果类的数据成员都是内置类型的,则不受干扰。
Screen的4个数据成员都是内置类型(string类定义了拷贝和赋值运算符),因此可以直接使用类对象执行拷贝和赋值操作。
练习 7.26:将Sales_data : : avg_price 定义成内联函数。
// 练习 7.26
//显式内联
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";
// 练习 7.27
#include
#include
using namespace std;
class Screen
{
private:
unsigned height = 0, width = 0;
unsigned cursor = 0;
string contents;
public:
Screen() = default; //默认构造函数
//构造函数初始值列表
Screen(unsigned ht, unsigned wd ) : height(ht), width(wd),
contents(ht * wd, ' ') { }
Screen(unsigned ht, unsigned wd, char c)
: height(ht), width(wd), contents(ht * wd, c) { }
public:
Screen & move(unsigned r, unsigned c)
{
cursor = r * width + c;
return *this;
}
Screen & set(char ch)
{
contents[cursor] = ch;
return *this;
}
Screen & set(unsigned r, unsigned c, char ch)
{
contents[r * width + c] = ch;
return *this;
}
Screen & display(ostream & os)
{
os << contents;
return *this;
}
};
int main()
{
Screen myScreen(5, 5, 'X');
myScreen.move(4, 0).set('#').display(cout);
cout << "\n";
myScreen.display(cout);
cout << "\n";
system("pause");
return 0;
}
练习 7.28:如果move 、set 和l display 函数的返回类型不是Screen &而是Screen,则在上一个练习中将会发生什么情况?
答:第一行X中有一个#,第二行全是X,没有#。因为如果返回的不是引用的话,意味着这些函数只返回一个临时副本,并不会改变对象本身的值。但是如果返回的是引用的话,这些函数返回的是对象本身而非对象的副本。
练习 7.29:修改你的Screen 类,令move、set 和display 函数返回Screen 并检程序的运行结果, 在上一个练习中你的推测正确吗?
答:正确。
练习 7.30:通过this 指针使用成员的做法虽然合法, 但是有点多余。讨论显式地使用指针访问成员的优缺点。
答:通过this指针访问成员的优点是,可以非常明确地指出访问的是对象的成员,并且可以在成员函数中使用与数据成员同名的形参,缺点是显得多余,代码不够简洁。
练习 7.31:定义一对类X 和Y,其中X 包含一个指向Y 的指针,而Y 包含一个类型为X 的对象。
// 练习 7.31
//先声明class Y,此时可以定义指向不完全类型的指针,但是不能创建不完全类型的对象
class Y;
class X
{
Y *y;
};
class Y
{
X x;
};
练习 7.32:定义你自己的Screen 和Window_mgr ,其中clear 是Window_mgr 的成员,是Screen 的友元。
有问题!
练习 7.33:如果我们给Screen 添加一个如下所示的size 成员将发生什么情况?如果出现了问题,请尝试修改它。
pos Screen:size() const
{
return height * width;
}
因为pos是定义在Screen类的内部的,而返回类型是在类的作用域之外的,并且在类的外部是无法直接访问pos的,所以会报错。改成如下形式:
Screen::pos Screen:size() const
{
return height * width;
}
练习 7.34:如果我们把第256 页Screen 类的pos 的typedef 放在类的最后一行会发生什么情况?
答:会导致编译出错,因为对pos的使用出现在它的声明之前,此时编译器不知道pos是什么含义。
练习 7.35:解释下面代码的含义,说明其中的Type 和linitVal 分别使用了哪个定义。如果代码存在错误, 尝试修改它。
// 练习 7.35
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;
}
要将函数setVal的定义中的返回类型修改一下,因为返回类型不属于类的作用域,说明此时Type代表的是外层作用域当中的string,而这与类内setVal函数的声明是不匹配的,因此要修改成如下形式:
Exercise::Type Exercise::setVal(Type parm)
练习 7.36:下面的初始值是错误的,请找出问题所在并尝试修改它。
struct X
{
X(int i, int j) : base(i), rem(base % j) { }
int rem, base;
};
// 构造函数初始值列表成员的初始化顺序与初始值列表顺序无关,只与数据成员在类中出现的次序有关。
//因此在上面的程序中,应该是先用base初始化rem,但是此时base的值未定义,因此会报错。
练习 7.37:使用本节提供的Sales data类,确定初始化下面的变量时分别使用了哪个构造函数,然后罗列出每个对象所有数据成员的值。
Sales_data first_item(cin);
int main()
{
Sales_data next;
Sales_data last("9-999-9999-9");
}
//first_item使用了接收std::istream&参数的构造函数,该对象的成员依赖于用户的输入。
//next使用了默认构造函数,即将string类型的成员bookNo默认初始化为空字符串,其他几个成员使用类内初始值初始化为0。
//last使用了const string &参数的构造函数,bookNo初始化为"9-999-9999-9",其他成员初始化为0。
练习 7.38:有些情况下我们希望提供cin 作为接受istream &参数的构造函数的默认实参, 请声明这样的构造函数。
Slaes_data(std::istream & is = std::cin) { is >> *this; }
//该函数具有了默认构造函数的功能,原来的默认构造函数应该去掉,否则会引起二义性调用
练习 7.39:如果接受string 的构造函数和接受istream &的构造函数都使用默认实参,这种行为合法吗?如果不, 为什么?
答:我们为两个构造函数同样都赋予了默认实参,则这两个构造函数都具有了默认构造函数的作用。一旦我们不提供任何实参创建类的对象,会引起二义性调用。
练习 7.40:从下面的抽象概念中选择一个(或者你自己指定一个 ,思考这样的类需要哪些数据成员, 提供一组合理的构造函数并阐明这样做的原因。
(a)Book (b)Data (c)Employee
(d)Vehicle (e)Object (f)Tree
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 类,给每个构造函数体添加一条语句,令其一旦执行就打印一条信息。用各种可能的方式分别创建Sales_data对象,认真研究每次输出的信息直到你确实理解了委托构造函数的执行顺序。
// 练习 7.41
#include
#include
using namespace std;
class Sales_data
{
//友元的声明,友元最好在类初或者类尾集中声明。
friend istream & read(std::istream & is, Sales_data & item);
friend 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), salesprice(salep)
{
if (sellingprice)
{
discount = salesprice / 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 salesprice = 0.0; //实售价格,显示初始化为0.0
double discount = 0.0; //折扣,显示初始化为0.0
};
istream &read(std::istream &is, Sales_data &item)
{
is >> item.bookNo >> item.units_sold >> item.sellingprice >> item.salesprice;
return is;
}
ostream &print(std::ostream &os, const Sales_data &item)
{
os << item.bookNo << " " << item.units_sold << " " << item.sellingprice
<< " " << item.salesprice << " " << item.discount;
return os;
}
int main()
{
Sales_data first("978-121-15535-2", 85, 128, 109);
Sales_data second;
Sales_data third("978-121-15535-2");
Sales_data last(cin);
system("pause");
return 0;
}
练习 7.42:对于你在练习7.40 (参见7.5.1节,第261 页)中编写的类,确定哪些构造函数可以使用委托。如果可以的话,编写委托构造函数。如果不可以,从抽象概念列表中重新选择一个你认为可以使用委托构造函数的,为挑选出的这个概念编写类定义。
// 练习 7.42
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 的类,它有一个接受int的构造函数,但是没有默认构造函数。定义类C, C 有一个NoDefault 类型的成员,定义C 的默认构造函数。
#include
#include
using namespace std;
//该类型没有显示定义默认构造函数,编译器也不会为它合成一个
class NoDefault
{
public:
NoDefault(int i)
{
val = i;
}
int val;
};
class C
{
public:
NoDefault nd;
//必须显示调用NoDefault的带参构造函数初始化nd,并且这个是C的默认构造函数
C(int i = 0) : nd(i) { }
};
int main()
{
C c; //使用了类型C的默认构造函数
cout << c.nd.val << endl;
system("pause");
return 0;
}
练习 7.44:下面这条声明合法吗?如果不,为什么?
vector vec(10);
//上述语句的想法是:创建一个vector对象vec,该对象包含10个元素,
//每个元素的类型都是NoDefault且执行默认初始化,但是在类NoDefault中并没有设计默认构造函数,
//所以该初始化过程无法执行。
练习 7.45:如果在上一个练习中定义的vector 的元素类型是C,则声明合法吗?为什么?
答:合法,因为类C中定义了带参数的默认构造函数。
练习 7.46:下面哪些论断是不正确的?为什么?
( a ) 一个类必须至少提供一个构造函数。
( b )默认构造函数是参数列表为空的构造函数。
( c ) 如果对于类来说不存在有意义的默认值,则类不应该提供默认构造函数。
( d )如果类没有定义默认构造函数,则编译器将为其生成一个并把每个数据成员初始化成相应类型的默认值。
答:(a)是错误的,类可以不提供任何构造函数,这是编译器自动实现一个合成的默认构造函数。
(b)是错误的,如果某个构造函数包含若干形参,但是同时为这些形参都提供了默认实参,则该构造函数也是具备默认构造函数的功能。
(c)是错误的,一般情况下,都应该为类构建一个默认构造函数。
(d)是错误的,类类型的成员执行各自所属类型的默认构造函数,内置类型和复合类型的成员只对定义在全局作用域中的对象执行初始化。
练习 7.47:说明接受一个string 参数的Sales_data 构造函数是否应该是explicit的,并解释这样做的优缺点。
答:应该是explicit的,这样可以防止编译器自动的把一个string对象转换成Sales_data对象,这样可能会导致意想不到的后果。
使用explicit的优点是避免因隐式转换而带来的意想不到的错误,缺点是用户的确需要这样的类类型转换时,不得不使用略显繁琐的方式来实现。
练习 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");
//
如果不是explicit的:
string null_isbn("9-999-99999-9");
Sales_data item1(null_isbn); //定义了一个Sales_data对象,该对象使用null_isbn转换得到的临时对象进行初始化
Sales_data item2("9-999-99999-9");//定义了一个Sales_data对象,该对象使用字符串字面值转换得到的临时对象进行初始化
//
如果是explicit的:
string null_isbn("9-999-99999-9");
Sales_data item1(null_isbn); //定义了一个Sales_data对象,该对象使用null_isbn转换得到的临时对象进行初始化
Sales_data item2("9-999-99999-9");//定义了一个Sales_data对象,该对象使用字符串字面值转换得到的临时对象进行初始化
//构造函数是否是explicit的与直接显式定义对象调用特定的构造函数无关,
//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;
如果我们试图在一行代码中使用两种转换规则,编译器将报错。
(a)是正确的,编译器隐式地调用Sales_data的构造函数,生成一个临时的Sales_data对象,然后传递给combine的形参。
(b)是错误的,编译无法通过。因为combine成员函数的形参是非常量引用,但是s自动创建的Sales_data临时对象无法传递给combine所需的非常量引用。(PS:隐式转换生成的无名的临时对象是const的)
修改为:Sales_data &combine( const Sales_data& ) 就可以了。
(c)是错误的,编译无法通过。因为我们把combine成员函数声明成了常量成员函数,所以该函数无法修改数据成员的值。
练习 7.50:确定在你的Person 类中是否有一些构造函数应该是explicit 的。
将构造函数Person(std::istream & is)定义为explicit。
练习 7.51:vector 将其单参数的构造函数定义成explicit 的,而string 则不是,你觉得原因何在?
答:从参数类型到类类型的自动转换是否有意义依赖于程序员的看法,如果这种转换是自然而然的,则不应该把它定义成explicit,如果二者语义相差甚远,则为了不必要的转换,应该指定对应的构造函数是explicit的。
string接受的单参数类型是const char*类型,如果我们得到了一个常量字符串指针(字符数组),则把它看做string对象是自然而然的过程。无须指定为explicit的。
但是vector接受的单参数类型是int类型,指定vector的容量。如果在本来需要vector的地方提供一个int值并且希望这个int值自动转换成vector,这个过程比较牵强。
如果vector单参数构造函数不是explicit的,那么对于这样的一个函数void fun(vector
练习 7.52:使用2.6.1 节(第64 页) 的Sales_data 类,解释下面的初始化过程。如果存在问题,尝试修改它。
Sales_data item = {"978-0590353403", 25, 15.99};
Slaes_data类不能有类内初始值,所以要将Sales_data类的两个类内初始值去掉。
练习 7.53:定义你自己的Debug。
略过。
练习 7.54:Debug 中以set_开头的成员应该被声明成constexpr 吗?如果不,为什么?
答:不能,constexpr函数只能包含return语句,不允许执行其他任务。
练习 7.55:7.5.5 节(第266 页)的Data 类是字面值常量类吗?请解释原因。
答:是字面值常量类,数据成员都是字面值类型的聚合类都是字面值常量类。
练习 7.56:什么是类的静态成员?它有何优点? 静态成员与普通成员有何区别?
答:静态成员是指声明语句之前带有关键字static的类成员,静态成员不是任意单独对象的组成部分,而是由该类的全体对象所共享。
静态成员的优点包括:作用域位于类的范围之内,避免与其他类的成员或者全局作用域的名字冲突;可以是私有成员,而全局对象不可以;通过阅读程序可以非常容易地看出静态成员与特定类关联,使得程序的含义清晰明了。
静态成员与普通成员的区别主要体现在普通成员与类的对象关联,是某个具体对象的组成部分;而静态成员不从属于任何具体的对象,它由该类的所有对象共享。另外,还有一个细微的区别,静态成员可以作为默认参数,而普通成员不能作为默认参数。
练习 7.57:编写你自己的Account 类。
//练习 7.57
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 vetSize = 20;
//正确,但是在类外应该在定义一下,比如:
//const int Example::vetSize;
static vector vec(vetSize);
//错误,必须要是常量字面值类型,vector不是字面值类型,不允许类内初始化
};
//example.C
#include "example.h"
//因为上面两个初始化都是错误的,因此下面两个static数据成员必须给出初始值。
double Example::rate;
vector Example::vec;