C++ Primer 5th 第7章 类

类的基本思想是数据抽象和封装,定义类就是定义一个抽象数据类型。

前面是一句很专业抽象的行话,到目前为止,我们已经接触到了C++支持的四种程序设计思维模式的两种:面向过程和基于对象。

什么是面向过程?很明显,前面的学习中,我们要解决一件事,思路都是先做什么,再做什么,比如前面中遇到的练习题:

练习6.5:编写一个函数输出其实参的绝对值。

这个问题我们的解决思路时:第一步定义一个变量,第二步对变量用实参初始化后进行判断,如果是0或者正数则其绝对值是自身,如果是负数则取相反值,第三步返回结果。这里就是典型的面向过程,开始到最后每步做什么,我们都有序规划好,不可能第二步在第一步之前,第一步后面也不是第三步。

什么是基于对象?回想书本最开头的关于书店的程序,书店有个程序能够记录ISBN、价格、数量等信息,这些信息是用一个变量来存储的,而C++语言自带的类型并不能存储这些信息,比如int、double都无法存储,我们是通过自定义了一个自己的类型,然后定义该类型的变量来存储。这里我们仅仅定义了一个新的类型来存储这些信息,并且额外定义了一些使用这种类型的操作,除此之外,没有更多了。这就是基于对象,我们实际上只是使用了类的封装这一特点,也就是把内部的细节隐藏起来了,告诉别人我们的这个类型有什么功能(操作),你按照那样去使用就可以了。以上是我对基于对象的简单理解。

那么还有两种是什么?一个是面向对象,一个是泛型编程。所谓泛型编程就是模板的那一套,后续将会学习。那么是什么面向对象呢?由于我的认识不够深刻,我在这里就不误人子弟,擅自给出理解了。但是面向对象编程同时具有三个很明显的特点:封装、多态、继承,这些后续也将陆续学习。

类中的所有成员必须在类中声明,也即默认定义在类中的成员全部为声明,除非显式的定义成员函数的函数体。成员函数是在类中声明的,定义可以在类内,也可以在类外。在类内定义时,为隐式内联,对于内联函数允许放在头文件,被多次定义。

在类的普通成员函数体内,成员函数是使用隐式的this指针来识别具体对象的。this指针是隐式定义的,在类内任何地方都不允许再次定义名为this的变量。

对于一个具体对象来说,它内部的this指针是const的,即是个顶层const指针,不允许改变this指针所指的对象。

类的非static成员函数可以被定义为const函数,const成员函数表示传入的this指针是个底层const指针,非const成员函数表示传入的this指针不是底层const指针,因此const对象不允许调用非const成员函数。

类定义时,是用一对花括号括起来类体的。花括号就表示一个作用域,因此一个类就是一个作用域。类的成员在类的作用域可见。当在类外定义成员函数时,必须在成员函数名前加上类名和作用域运算符,以指明生效作用域。

在类的作用域之外,普通数据和函数成员只能通过对象、引用、指针来访问。

一个类就是一个作用域的事实表明了在类外定义成员函数时必须同时提供类名和函数名,在类的外部,成员的名字被隐藏起来了。

当编译器解析的时候,一旦遇到类名,剩下的部分就认为全部在类的作用域内进行,结果就是后面不再需要类名来指明作用域,但是有一点特殊,就是函数的返回类型,因为先遇到返回类型,再遇到类名和函数名,所以返回类型中使用的名字都位于类的作用域之外。

由于类是一个作用域,因为类中涉及的名字查找就有一些规定。具体如下:

1.编译成员的声明

2.处理完全部声明后编译函数体

这样处理的好处是,在成员函数体中可以任意使用类中的名字,而无需在意出现的先后次序。

类名字的查找仅仅对成员函数体适用,声明中的名字则是在使用前必须可见。即使一个定义在类内部的函数,它的函数头既是声明,又是定义,也要按照声明来处理。

可以直接在成员函数内返回this指针所指的对象,当然,函数返回类型得是一个引用,以避免拷贝。

构造函数

构造函数决定着类对象的初始化方式,构造函数的任务是初始化类的对象的数据成员,无论何时,只要类的对象被创建,就会执行构造函数,需要说明的是,构造函数是可以重载的,通常对不同特征的构造函数有以下三种称呼,如下图:

C++ Primer 5th 第7章 类

从上图可以看到,复制构造函数,合成/非合成的默认构造函数以及自定义的构造函数都是构造函数。

对于合成和非合成的默认构造函数的区别是:合成默认构造函数是编译器提供的,非合成默认构造函数是属于用于自定义的。非合成默认构造函数有两者:一种是不带参数的构造函数,另一种是所有参数都带有默认实参的构造函数。

一旦我们定义了任意一个构造函数,类都将不再生成默认构造函数,也就是说,假如你定义了复制构造函数,它也属于构造函数,将不会再生成默认构造函数,会导致类无法默认初始化,因此如果你定义了复制构造函数,你必须再定义一个默认构造函数或者其他的构造函数。

构造函数的重载和普通函数重载的要求是一致的,只是构造函数并没有返回值。另外,构造函数也不能声明成const的,因为对象在初始化完成之前是不具有const属性的。

默认构造函数是没有参数的,默认构造函数分为自动合成和非合成手工定义的,默认构造函数符合重载要求,也即一个类只能有合成或非合成的默认构造函数中的任意一个。

默认构造函数是用于控制默认初始化的,如果我们可以不定义,那么该类不允许默认初始化。

合成的默认构造函数是没有定义任何构造函数时,编译器自动生成的,非合成的默认构造函数是我们手工定义的空参数类型的用户自定义默认构造函数

C++11新标准中,可以在形参列表后使用=default来显式的要求编译器为我们生成一个合成的默认构造函数。

=default既可以在类的内部,又可以在类的外部,两者之间的区别不仅仅是内联,还有编译器合成和用户自定义之分。出现在类的内部声明处,则是默认合成构造函数,出现在外部定义处,则是用户自定义默认构造函数。

编译器合成和用户自定义之间的一个区别是:在定义const类对象时,如果类只有编译器合成的构造函数,那么不允许定义该类的const对象。例如:

struct synthesis
{
int i;
}; struct user_provided
{
user_provided();
int i;
}; user_provided::user_provided() = default; //user_provided constructor
const synthesis s; //错误,缺少用户自定义构造函数,不允许定义
const user_provided u;  //正确,拥有用户自定义构造函数,运行定义

构造函数初始值列表,即在构造函数的形参列表后冒号后跟的一些值的初始化列表。

初始化列表负责初始化每个数据成员,没有出现在初始化列表中的数据成员,如果有类内初始化,则用类内初始值初始化,没有则默认初始化。

构造函数的初始化顺序与初始值列表无关,与在类中声明的先后顺序有关。

访问控制与封装是通过访问说明符:public、private来控制的。

在public说明符后面的可以被整个程序访问。

在private说明符后面的可以被类的成员函数访问。注意:这里是类的成员函数,而不是对象的成员函数,因此同一个类的不同对象可以相互访问对方的private部分。

对于类中private、public说明符的数量没有限制。

在C++中,class和struct的唯一区别就是默认访问权限。

当一个函数在逻辑上是类的接口部分,但实现上不属于类时,可以将其定义为友元。友元允许非类成员访问类的private部分。如果类想把一个函数作为类的友元,只需要在类的内部增加一条用friend关键词修饰的函数声明即可。

使用友元有几个注意:

1.友元只是个类内声明

2.友元只能出现在类定义的内部,但位置不限,也不受访问说明符的限制。

3.友元只是指出了访问权限,而不是一个普通意义的函数声明。

4.友元不具有传递性

对于类来说,我们也可以在类内部定义别名,这个别名叫做类型成员(type member),类型成员的访问不同于数据成员,类型成员访问直接通过类名来访问。类型成员受到访问说明符public、private的限制。

同构造函数一样,成员函数也可以被重载,满足重载的条件即可。

mutable数据成员可以被const类对象修改。

隐式类类型转换

字面值常量类

数据成员必须都是字面值类型且是内置的,函数成员可以允许有非constexpr的,字面值常量类必须初始化,一旦初始化,数据成员值确定,对于constexpr函数和非constexpr函数都可以在编译期间得到具体值?

类的静态成员

explicit函数只允许被显式调用,而不允许隐式调用导致间接转换。explicit关键词只允许用于构造函数/拷贝构造函数/类型转换函数。

练习7.1:使用2.6.1节练习定义的Sales_data类为1.6节(第21页)的交易处理程序编写一个新版本。

#include <iostream>

using namespace std;

struct Sales_data
{
string bookNo;
unsigned units_sold = ;
double revenue = 0.0;
}; int main(int argc, char const *argv[])
{
Sales_data total; if (cin >> total.bookNo >> total.units_sold >> total.revenue)
{
Sales_data trans;
while (cin >> trans.bookNo >> trans.units_sold >> trans.revenue)
{
if (total.bookNo == trans.bookNo)
{
total.units_sold += trans.units_sold;
total.revenue += trans.revenue;
}
else
{
cout << total.bookNo << "\t" << total.units_sold << "\t" << total.revenue << endl;
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;
}
}
cout << total.bookNo << "\t" << total.units_sold << "\t" << total.revenue << endl;
}
else
{
cerr << "No data?!" << endl;
return -;
}
return ;
}

练习7.2:曾在2.6.2节的练习(第76页)中编写了一个Sales_data类,请向这个类添加combine和isbn成员。

struct Sales_data
{
string bookNo;
unsigned units_sold = ;
double revenue = 0.0; Sales_data combine(const Sales_data& s1, const Sales_data& s2);
Sales_data isbn(const Sales_data& s) const;
}; Sales_data& Sales_data::combine(const Sales_data& s1)
{
units_sold += s1.units_sold;
revenue += s1.revenue;
return *this;
} string Sales_data::isbn(const Sales_data& s) const
{
return s.bookNo;
}

练习7.3:修改7.1.1节(第229页)的交易处理程序,令其使用这些成员。

#include <iostream>

using namespace std;

struct Sales_data
{
string bookNo;
unsigned units_sold = ;
double revenue = 0.0; Sales_data& combine(const Sales_data& s);
string isbn() const;
}; Sales_data& Sales_data::combine(const Sales_data& s)
{
units_sold += s.units_sold;
revenue += s.revenue;
return *this;
} string Sales_data::isbn() const
{
return bookNo;
} int main(int argc, char const *argv[])
{
Sales_data total; if (cin >> total.bookNo >> total.units_sold >> total.revenue)
{
Sales_data trans;
while (cin >> trans.bookNo >> trans.units_sold >> trans.revenue)
{
if (total.isbn() == trans.isbn())
{
total.combine(trans);
}
else
{
cout << total.isbn() << "\t" << total.units_sold << "\t" << total.revenue << endl;
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;
}
}
cout << total.isbn() << "\t" << total.units_sold << "\t" << total.revenue << endl;
}
else
{
cerr << "No data?!" << endl;
return -;
}
return ;
}

练习7.4:编写一个名为Person的类,使其表示人员的姓名和住址。使用string对象存放这些元素,接下来的练习将不断充实这个类的其他特征。

struct Person
{
string Name;
string Address;
};

练习7.5:在你的Person类中提供一些操作使其能够返回姓名和地址。这些函数是否应该是const的呢?解释原因。

struct Person
{
string Name_d;
string Address_d; string Name() const;
string Address();
}; string Person::Name() const
{
return Name_d;
} string Person::Address()
{
return Address_d;
}

姓名应该是const的,因为姓名不会改变。地址是非const的,因为地址可能改变

练习7.6:对于函数add、read和print,定义你自己的版本。

struct Sales_data
{
string bookNo;
unsigned units_sold = ;
double revenue = 0.0; Sales_data& combine(const Sales_data& s);
string isbn() const; }; Sales_data& Sales_data::combine(const Sales_data& s)
{
units_sold += s.units_sold;
revenue += s.revenue;
return *this;
} string Sales_data::isbn() const
{
return bookNo;
} Sales_data add(const Sales_data& s1, const Sales_data& s2)
{
Sales_data temp = s1;
temp.combine(s2);
return temp;
} istream& read(istream& in, Sales_data& s)
{
in >> s.bookNo >> s.units_sold >> s.revenue;
return in;
} ostream& print(ostream& out, const Sales_data& s)
{
out << s.isbn() << s.units_sold << s.revenue;
return out;
}

练习7.7:使用这些新函数重写7.1.2节(第233页)练习中的交易处理程序。

int main(int argc, char const *argv[])
{
Sales_data total; if (read(cin, total))
{
Sales_data trans;
while (read(cin, trans))
{
if (total.isbn() == trans.isbn())
{
total.combine(trans);
}
else
{
print(cout, total) << endl;
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;
}
}
print(cout, total) << endl;
}
else
{
cerr << "No data?!" << endl;
return -;
}
return ;
}

练习7.8:为什么read函数将其Sales_data参数定义成普通的引用,而print函数将其参数定义成常量引用?

因为read函数要对形参进行写入操作,而print只需要读取操作。

练习7.9:对于7.1.2节(第233页)练习中的代码,添加读取和打印Person对象的操作。

istream& read(istream& in, Person& s)
{
in >> s.Name_d >> s.Address_d;
return in;
} ostream& print(ostream& out, const Person& s)
{
out << s.Name_d << "\t" << s.Address_d;
return out;
}

练习7.10:在下面这条if语句中,条件部分的作用是什么?

if (read(read(cin, data1), data2))

从cin流连续读取data1、data2两个对象

练习7.11:在你的Sales_data类中添加构造函数,然后编写一段程序令其用到每个构造函数。

#include <iostream>

using namespace std;

struct Sales_data
{
Sales_data() = default;
Sales_data(string s, unsigned u, double d);
string bookNo;
unsigned units_sold = ;
double revenue = 0.0; Sales_data& combine(const Sales_data& s);
string isbn() const; }; Sales_data::Sales_data(string s, unsigned u, double d): bookNo(s), units_sold(u), revenue(d)
{ } Sales_data& Sales_data::combine(const Sales_data& s)
{
units_sold += s.units_sold;
revenue += s.revenue;
return *this;
} string Sales_data::isbn() const
{
return bookNo;
} Sales_data add(const Sales_data& s1, const Sales_data& s2)
{
Sales_data temp = s1;
temp.combine(s2);
return temp;
} istream& read(istream& in, Sales_data& s)
{
in >> s.bookNo >> s.units_sold >> s.revenue;
return in;
} ostream& print(ostream& out, const Sales_data& s)
{
out << s.isbn() << s.units_sold << s.revenue;
return out;
} int main(int argc, char const *argv[])
{
Sales_data s1;
Sales_data s2("", , 2.3);
return ;
}

练习7.12:把只接受一个istream 作为参数的构造定义函数移到类的内部。

struct Sales_data
{
Sales_data(istream& is)
{
read(is, *this);
}
};

练习7.13:使用istream构造函数重写第229页的程序。

#include <iostream>

using namespace std;

struct Sales_data;
istream& read(istream& in, Sales_data& s); struct Sales_data
{
Sales_data() = default;
Sales_data(istream& is)
{
read(is, *this);
}
Sales_data(string s, unsigned u, double d);
string bookNo;
unsigned units_sold = ;
double revenue = 0.0; Sales_data& combine(const Sales_data& s);
string isbn() const; }; Sales_data::Sales_data(string s, unsigned u, double d): bookNo(s), units_sold(u), revenue(d)
{ } Sales_data& Sales_data::combine(const Sales_data& s)
{
units_sold += s.units_sold;
revenue += s.revenue;
return *this;
} string Sales_data::isbn() const
{
return bookNo;
} Sales_data add(const Sales_data& s1, const Sales_data& s2)
{
Sales_data temp = s1;
temp.combine(s2);
return temp;
} istream& read(istream& in, Sales_data& s)
{
in >> s.bookNo >> s.units_sold >> s.revenue;
return in;
} ostream& print(ostream& out, const Sales_data& s)
{
out << s.isbn() << s.units_sold << s.revenue;
return out;
} int main(int argc, char const *argv[])
{
Sales_data total(std::cin); if (!total.isbn().empty())
{
Sales_data trans;
while (read(cin, trans))
{
if (total.isbn() == trans.isbn())
{
total.combine(trans);
}
else
{
print(cout, total) << endl;
total = trans;
}
}
print(cout, total) << endl;
}
else
{
cerr << "No data?!" << endl;
return -;
}
return ;
}

练习7.14:编写一个构造函数,令其用我们提供的类内初始值显式地初始化成员。

Sales_data() : units_sold() , revenue(0.0) { }

练习7.15:为你的 Person 类添加正确的构造函数。

Person() = default;
Person(): Name_d(n), Address_d(a) { }

练习7.16:在类的定义中对于访问说明符出现的位置和次数有限定吗?如果有,是什么?什么样的成员应该定义在public说明符之后?什么样的成员应该定义在private说明符之后?

没有限制。对于那些属于类的接口的部分,应当出现在public后面,而那些具体实现或者数据成员则应该在private后面

练习7.17:使用class 和 struct 时有区别吗?如果有,是什么?

有,在没有访问说明符时,默认的访问权限不同。

练习7.18:封装是何含义?它有什么用处?

封装是把过程和数据包围起来,对数据的访问只能通过已定义的接口。

练习7.19:在你的Person 类中,你将把哪些成员声明成public的?哪些声明成private的?解释你这样做的原因。

将数据成员声明成private的,函数成员声明成public的。因为Person类不需要直接提供数据操作给外部,只需要相应的功能接口。

练习7.20:友元在什么时候有用?请分别列举出使用友元的利弊。

在需要直接访问类的private部分的内容时有用,友元有时候能够避免开销,但也破坏了封装。

练习7.21:修改你的Sales_data类使其隐藏实现的细节。你之前编写的关于Sales_data操作的程序应该继续使用,借助类的新定义重新编译该程序,确保其正常工作。

#include <iostream>

using namespace std;

class Sales_data;
istream& read(istream& in, Sales_data& s); class Sales_data
{
friend Sales_data add(const Sales_data& s1, const Sales_data& s2);
friend istream& read(istream& in, Sales_data& s);
friend ostream& print(ostream& out, const Sales_data& s);
public:
Sales_data() = default;
Sales_data(istream& is)
{
read(is, *this);
}
Sales_data(string s, unsigned u, double d);
Sales_data& combine(const Sales_data& s);
string isbn() const; private:
string bookNo;
unsigned units_sold = ;
double revenue = 0.0; }; Sales_data::Sales_data(string s, unsigned u, double d): bookNo(s), units_sold(u), revenue(d)
{ } Sales_data& Sales_data::combine(const Sales_data& s)
{
units_sold += s.units_sold;
revenue += s.revenue;
return *this;
} string Sales_data::isbn() const
{
return bookNo;
} Sales_data add(const Sales_data& s1, const Sales_data& s2)
{
Sales_data temp = s1;
temp.combine(s2);
return temp;
} istream& read(istream& in, Sales_data& s)
{
in >> s.bookNo >> s.units_sold >> s.revenue;
return in;
} ostream& print(ostream& out, const Sales_data& s)
{
out << s.isbn() << s.units_sold << s.revenue;
return out;
} int main(int argc, char const *argv[])
{
Sales_data total(std::cin); if (!total.isbn().empty())
{
Sales_data trans;
while (read(cin, trans))
{
if (total.isbn() == trans.isbn())
{
total.combine(trans);
}
else
{
print(cout, total) << endl;
total = trans;
}
}
print(cout, total) << endl;
}
else
{
cerr << "No data?!" << endl;
return -;
}
return ;
}

练习7.22:修改你的Person类使其隐藏实现的细节。

class Person
{
public:
string Name() const;
string Address(); private:
string Name_d;
string Address_d; };

练习7.23:编写你自己的Screen类。

class Screen
{
public:
using unsign = unsigned int; Screen() = default;
Screen(unsign cursor_p): cursor(cursor_p)
{ }
inline Screen(unsign h_p, unsign w_p, unsign cursor_p);
char get() const
{
return s[cursor];
}
char get(unsign x, unsign y) const; private:
unsign h = ;
unsign w = ;
mutable unsign cursor;
string s;
}; Screen::Screen(unsign h_p, unsign w_p, unsign cursor_p): h(h_p), w(w_p), cursor(cursor_p)
{ } char Screen::get(unsign x, unsign y) const
{
cursor = x * y;
return s[cursor];
}

练习7.24:给你的Screen类添加三个构造函数:一个默认构造函数;另一个构造函数接受宽和高的值,然后将contents初始化成给定数量的空白;第三个构造函数接受宽和高的值以及一个字符,该字符作为初始化后屏幕的内容。

Screen() = default;
Screen(unsign x, unsign y): h(x), w(y), contents(, ' ')
{ }
Screen(unsign h_p, unsign w_p, char c): h(h_p), w(w_p), contents(, c)
{ }

练习7.25:Screen 能安全地依赖于拷贝和赋值操作的默认版本吗?如果能,为什么?如果不能?为什么?

可以依赖,因为类不需要额外分配对象之外的资源,且string类在合成的构造函数中能正常工作。

练习7.26:将Sales_data::avg_price 定义成内联函数。

inline Sales_data::avg_price() {  }

练习7.27:给你自己的Screen 类添加move、set 和display 函数,通过执行下面的代码检验你的类是否正确。

Screen myScreen(, , 'X');
myScreen.move(, ).set('#').display(cout);
cout << "\n";
myScreen.display(cout);
cout << "\n";
#include <iostream>

using namespace std;

class Screen
{
public:
typedef std::string::size_type pos;
Screen() = default;
Screen(pos ht, pos wd, char c): height(ht), width(wd), contents(ht * wd, c)
{ }
char get() const
{
return contents[cursor];
}
inline char get(pos ht, pos wd) const;
Screen &move(pos r, pos c);
Screen &set(char);
Screen &set(pos, pos, char);
Screen &display(std::ostream &os)
{
do_display(os); return *this;
}
const Screen &display(std::ostream &os) const
{
do_display(os); return *this;
} private:
pos cursor = ;
pos height = , width = ;
std::string contents;
void do_display(std::ostream &os) const
{
os << contents;
}
}; inline Screen &Screen::move(pos r, pos c)
{
pos row = r * width;
cursor = row + c ;
return *this;
} inline Screen &Screen::set(char c)
{
contents[cursor] = c;
return *this;
} inline Screen &Screen::set(pos r, pos col, char ch)
{
contents[r * width + col] = ch;
return *this;
} int main(int argc, char const *argv[])
{
Screen myScreen(, , 'X');
myScreen.move(, ).set('#').display(cout);
cout << "\n";
myScreen.display(cout);
std::cout << "\n";
return ;
}

练习7.28:如果move、set和display函数的返回类型不是Screen&而是Screen,则在上一个练习中将会发生什么?

对myScreen对象的操作只能成功一次,链式表达式之后的都无法成功。

练习7.29:修改你的Screen 类,令move、set和display函数返回Screen并检查程序的运行结果,在上一个练习中你的推测正确吗?

正确

练习7.30:通过this指针使用成员的做法虽然合法,但是有点多余。讨论显示地使用指针访问成员的优缺点。

并没有感觉到有什么优缺点...

练习7.31:定义一对类X和Y,其中X 包含一个指向 Y 的指针,而Y包含一个类型为 X 的对象。

class Y;

class X
{
Y *py;
}; class Y
{
X x;
};

练习7.32:定义你自己的Screen 和 Window_mgr,其中clear是Window_mgr的成员,是Screen的友元。

class Screen
{
public:
Screen() = default;
Screen(int a, int b, string st): i(a), j(b), s(st) { }
friend class Window_mgr; private:
int i = , j = ;
string s;
}; class Window_mgr
{
public:
Window_mgr();
void clear(size_t); private:
vector<Screen> v{Screen(, , "")};
}; void Window_mgr::clear(size_t i)
{
v[i].s = string(v[i].i * v[i].j, ' ');
}

练习7.33:如果我们给Screen 添加一个如下所示的size成员将发生什么情况?如果出现了问题,请尝试修改它。

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

编译将发生错误,因为pos的类型未知。在pos前添加类名和作用域运算符即可。

练习7.34:如果我们把第256页Screen类的pos的typedef放在类的最后一行会发生什么情况?

将会找不到类型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;                //定义string类型别名Type
Type initVal(); //全局函数声明,返回类型为Type(string) class Exercise
{
public:
typedef double Type; //定义double类型别名Type
Type setVal(Type); //成员函数声明,返回类型、形参为Type(double)
Type initVal(); //成员函数声明,返回类型为Type,函数名隐藏全局作用域的initVal()
private:
int val;
}; Type Exercise::setVal(Type parm) //类外定义成员函数,返回类型、形参为Type(double)
{
val = parm + initVal(); //initVal()为类Exercise()的成员函数
return val;
}

练习7.36下面的初始值是错误的,请找出问题所在并尝试修改它。

struct X
{
X (int i, int j): base(i), rem(base % j) {}
int rem, base;
};

类数据成员初始化的顺序依赖于数据成员的定义次序,而非初始化列表的位置顺序。

struct X
{
X(int i, int j) : base(i), rem(i % j)
{ } int rem, base;
};

练习7.37:使用本节提供的Sales_data类,确定初始化下面的变量时分别使用了哪个构造函数,然后罗列出每个对象所有的数据成员的值。

Sales_data first_item(cin);            //Sales_data(std::istream &is)    用户输入的值

int main()
{
Sales_data next; //Sales_data(std::string s = "") bookNo="",units_sold=0,revenue=0.0
Sales_data last("9-999-99999-9"); //Sales_data(std::string s = "") bookNo="9-999-99999-9",units_sold=0,revenue=0.0
}

练习7.38:有些情况下我们希望提供cin作为接受istream& 参数的构造函数的默认实参,请声明这样的构造函数。

Sales_data(std::istream &is = cin);

练习7.39:如果接受string的构造函数和接受 istream&的构造函数都使用默认实参,这种行为合法吗?如果不,为什么?

不合法,因为这样无法确定具体调用哪个构造函数。

练习7.40:从下面的抽象概念中选择一个(或者你自己指定一个),思考这样的类需要哪些数据成员,提供一组合理的构造函数并阐明这样做的原因。
(a) Book           (b) Data           (c) Employee
(d) Vehicle        (e) Object        (f) Tree
对于(a)Book来说,数据成员要有书名,书的数量,书的价格,书的刊号,书的作者,出版日期等。

class Book
{
public:
Book();
Book(string n1, string n2, double p1, string a1, string d1): bookName(n1), book_num(n2), book_price(p1), book_author(a1), book_date(d1) { } private:
string bookName;
string book_num;
double book_price;
string book_isbn;
string book_author;
string book_date;
};

练习7.41:使用委托构造函数重新编写你的Sales_data 类,给每个构造函数体添加一条语句,令其一旦执行就打印一条信息。用各种可能的方式分别创建 Sales_data 对象,认真研究每次输出的信息直到你确实理解了委托构造函数的执行顺序。

#include <iostream>

using namespace std;

class Sales_data;
istream& read(istream& in, Sales_data& s); class Sales_data
{
friend istream& read(istream& in, Sales_data& s);
public:
Sales_data(): Sales_data("", , 0.0)
{
cout << "default constructor.\n";
} Sales_data(istream& is): Sales_data("", , 0.0)
{
cout << "istream constructor.\n";
read(is, *this);
} Sales_data(string s, unsigned u, double d)
{
cout << "3 parameter constructor.\n";
} private:
string bookNo;
unsigned units_sold = ;
double revenue = 0.0; }; istream& read(istream& in, Sales_data& s)
{
in >> s.bookNo >> s.units_sold >> s.revenue;
return in;
} int main(int argc, char const * argv[])
{
Sales_data s1;
Sales_data s2(cin);
return ;
}

练习7.42:对于你在练习7.40(参见7.5.1节,第261页)中编写的类,确定哪些构造函数可以使用委托。如果可以的话,编写委托构造函数。如果不可以,从抽象概念列表中重新选择一个你认为可以使用委托构造函数的,为挑选出的这个概念编写类定义。

#include <iostream>

using namespace std;

class Book
{
public:
Book();
Book(string n1, string n2, double p1, string a1, string d1): bookName(n1), book_num(n2), book_price(p1), book_author(a1), book_date(d1)
{
cout << "6 parameter constructor.\n";
} private:
string bookName;
string book_num;
double book_price;
string book_isbn;
string book_author;
string book_date;
}; Book::Book(): Book("", "", 0.0, "", "")
{
cout << "default constructor.\n";
} int main(int argc, char const * argv[])
{
Book b1;
return ;
}

练习7.43:假定有一个名为 NoDefault 的类,它有一个接受 int 的构造函数,但是没有默认构造函数。定义类 C,C 有一个 NoDefault 类型的成员,定义C 的默认构造函数。

#include <iostream>

using namespace std;

class NoDefault
{
public:
NoDefault(int i); private:
int n;
}; NoDefault::NoDefault(int i): n(i)
{
cout << "NoDefault constructor.\n";
} class C
{
public:
C(); private:
NoDefault cN;
}; C::C(): cN()
{ } int main(int argc, char const * argv[])
{
C c1;
return ;
}

练习7.44:下面这条声明合法吗?如果不,为什么?

vector<NoDefault> vec();

不合法,因为NoDefault既没有默认构造函数又不能值初始化。、

练习7.45:如果在上一个练习中定义的vector的元素类型是C,则声明合法吗?为什么?

合法,因为C有默认构造函数。

练习7.46:下面哪些论断是不正确的?为什么?

(a) 一个类必须至少提供一个构造函数。
(b) 默认构造函数是参数列表为空的构造函数。
(c) 如果对于类来说不存在有意义的默认值,则类不应该提供默认构造函数。
(d) 如果类没有定义默认构造函数,则编译器将为其生成一个并把每个数据成员初始化成相应类型的默认值。

(a)不正确,简单的类可以依靠编译器自动合成
(b)正确
(c)正确,<<More Effective C++>>中说到:非必要不提供default constructor,添加无意义的default constructor(提供了无意义的默认值)会影响效率,因为成员函数必须测试字段是否被初始化。
(d)当类没有定义任何构造函数时,才会自动合成构造函数。

练习7.47:说明接受一个string 参数的Sales_data构造函数是否应该是explicit的,并解释这样做的优缺点。

是否需要从string到Sales_data的转换依赖于我们对用户使用该转换的看法,也就是说要视具体情况而定。

练习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 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);    //正确,string隐式转换到Sales_data
(b) Sales_data &combine(Sales_data&); //错误,不能从string转换到Sales_data &
(c) Sales_data &combine(const Sales_data&) const; //正确,string隐式转换为一个临时Sales_data,该Sales_data被绑定到const Sales_data &

练习7.50:确定在你的Person类中是否有一些构造函数应该是 explicit 的。

是否需要explicit是根据具体情况而定的。

练习7.51:vector 将其单参数的构造函数定义成 explicit 的,而string则不是,你觉得原因何在?

如果vector单参数构造函数不是explicit的,那么对于这样的一个函数void fun(vector<int> v)来说,可以直接以这样的形式进行调用fun(5),这种调用容易引起歧义,无法得知实参5指的是vector的元素个数还是只有一个值为5的元素。而string类型不是一个容器,不存在这样的歧义问题。

练习7.52:使用2.6.1节(第64页)的 Sales_data 类,解释下面的初始化过程。如果存在问题,尝试修改它。

Sales_data item = {"987-0590353403", , 15.99};

将Sales_data的bookNo成员初始化为"978-0590353403",将units_sold初始化为25,将revenue初始化为15.99

练习7.53:定义你自己的 Debug。

class Debug
{
public:
constexpr Debug(int ii, double dd): i(ii), d(dd)
{ }
~Debug() = default; private:
int i;
double d;
};

练习7.54:Debug中以 set_ 开头的成员应该被声明成 constexpr 吗?如果不,为什么?

不应该,因为set_开头的成员不含有return 语句。

练习7.55:7.5.5节(第266页)的 Data 类是字面值常量类吗?请解释原因。

不是,因为数据成员s是string类型,而string类型不是字面值类型。

练习7.56:什么是类的静态成员?它有何优点?静态成员与普通成员有何区别?

在类中使用static关键词修饰的成员。优点是能适用于某些普通成员无法使用的场合。静态成员与普通成员的区别是静态成员属于整个类,而不是属于一个对象,也即一个类只有一份静态成员,而有多份对象。

练习7.57:编写你自己的 Account 类。

class Account
{
public:
int i;
static char c; }; char Account::c = 'A';

练习7.58:下面的静态数据成员的声明和定义有错误吗?请解释原因。

// example.h
class Example
{
public:
static double rate = 6.5; //错误,非constexpr static数据成员不允许在类内初始化
static const int vecSize = ; //正确 //另外需要防止下面的声明被解析成函数成员的声明
static vector<double> vec(vecSize); //错误,constexpr static数据成员必须是字面值类型,vector非字面值类型,不允许类内初始化
};
// example.C
#include "example.h"
double Example::rate;
vector<double> Example::vec;
上一篇:Kubernetes介绍及基本概念


下一篇:Python中根据提供的日期,返回是一年中的第几天