《C++ Primer》第7章 类
7.3节 类的其他特性
练习7.23:编写你自已的Screen类。
【出题思路】
思考Screen类应该包含哪些数据成员和函数成员,设置适当的访问权限。
【解答】
对于Screen类来说,必不可少的数据成员有:屏幕的宽度和高度、屏幕的内容以及光标的当前位置,这与书中的示例是一致的。因此,仅包含数据成员的Screen类是:
class Screen
{
private:
unsigned height = 0; //屏幕的高
unsigned width = 0; //屏幕的宽
unsigned cursor = 0; //光标的当前位置
unsigned contents; //屏幕的内容
};
练习7.24:给你的Screen类添加三个构造函数:一个默认构造函数;另一个构造函数接受宽和高的值,然后将contents初始化成给定数量的空白;第三个构造函数接受宽和高的值以及一个字符,该字符作为初始化之后屏幕的内容。
【出题思路】
同一个类可以包含多个构造函数,构造函数的定义可以在类的内部也可以在类的外部。
【解答】
使用构造函数的列表初始值执行初始化操作,添加构造函数之后的Screen类是:
#include <iostream>
#include <string>
using namespace std;
class Screen
{
private:
unsigned height = 0;
unsigned 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)
{
}
void print()
{
cout << "width = " << width << "\theight = " << height << "\tcursor = " << cursor << "\tcontents = " << contents << endl;
}
};
int main()
{
cout << "打印屏幕信息:" << endl;
Screen screen1;
screen1.print();
Screen screen2(50, 10);
screen2.print();
Screen screen3(5, 2, 'A'); // 5 * 2 个'A'
screen3.print();
return 0;
}
运行结果:
练习7.25:Screen能安全地依赖于拷贝和赋值操作的默认版本吗?如果能,为什么?如果不能,为什么?
【出题思路】
含有指针数据成员的类一般不宜使用默认的拷贝和赋值操作,如果类的数据成员都是内置类型的,则不受干拢。
【解答】
Screen的4个数据成员都是内置类型(string类定义了拷贝和赋值运算符),因此直接使用类对象执行拷贝和赋值操作是可以的。
练习7.26:将Sales_data::avg_price定义成内联函数。
【出题思路】
要想把类的成员函数定义成内联函数,有几种不同的途径。第一种是直接把函数定义放在类的内部,第二种是把函数定义放在类的外部,并且在定义之前显式地指定inline。
【解答】
隐式内联,把avg_price函数的定义放在类的内部:
//隐式内联,把avg_price函数的定义放在类的内部
class Sales_data
{
public:
double avg_privce() 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_privce() 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";
【出题思路】
添加3个成员函数,注意函数的返回值类型应该是引用类型,在成员函数内部可以直接使用类的数据成员。
【解答】
添加move、set和display函数之后,新的Screen类是:
#include <iostream>
#include <string>
using namespace std;
class Screen
{
public:
Screen() = default;
Screen(unsigned width, unsigned height):
m_width(width), m_height(height), contents(width * height, ' ')
{
}
Screen(unsigned width, unsigned height, char c):
m_width(width), m_height(height), contents(width * height, c)
{
}
Screen& move(unsigned int row, unsigned int col)
{
m_cursor = row * m_width + col;
return *this;
}
Screen& set(char c)
{
contents[m_cursor] = c;
return *this;
}
Screen& set(unsigned int row, unsigned int col, char c)
{
contents[row * m_width + col] = c;
return *this;
}
Screen& display(std::ostream &os)
{
cout << "Screen& display======================" << endl;
do_display(os);
return *this;
}
void do_display(std::ostream &os)
{
os << contents;
}
private:
unsigned int m_width = 0;
unsigned int m_height = 0;
unsigned int m_cursor = 0;
string contents;
};
int main()
{
cout << "Screen测试:" << endl;
Screen myScreen(5, 5, 'X');
myScreen.move(4, 0).set('#').display(cout);
cout << endl;
myScreen.display(cout);
cout << "\n";
return 0;
}
运行结果:
练习7.29:修改你的Screen类,令move、set和display函数返回Screen并检查程序的运行结果,在上一个练习中你的推测正确吗?
【出题思路】
在编程环境中验证修改前后的程序差别,注意对比运行结果的变化并思考原因。
【解答】
修改move、set和display函数的返回类型后运行程序,得到的结果是:
#include <iostream>
#include <string>
using namespace std;
class Screen
{
public:
Screen() = default;
Screen(unsigned width, unsigned height):
m_width(width), m_height(height), contents(width * height, ' ')
{
}
Screen(unsigned width, unsigned height, char c):
m_width(width), m_height(height), contents(width * height, c)
{
}
Screen move(unsigned int row, unsigned int col)
{
m_cursor = row * m_width + col;
return *this;
}
Screen set(char c)
{
contents[m_cursor] = c;
return *this;
}
Screen set(unsigned int row, unsigned int col, char c)
{
contents[row * m_width + col] = c;
return *this;
}
Screen display(std::ostream &os)
{
cout << "Screen display======================" << endl;
do_display(os);
return *this;
}
void do_display(std::ostream &os)
{
os << contents;
}
private:
unsigned int m_width = 0;
unsigned int m_height = 0;
unsigned int m_cursor = 0;
string contents;
};
int main()
{
cout << "Screen测试:" << endl;
Screen myScreen(5, 5, 'X');
myScreen.move(4, 0).set('#').display(cout);
cout << endl;
myScreen.display(cout);
cout << "\n";
return 0;
}
运行结果:
练习7.30:通过this指针使用成员的做法虽然合法,但是有点多余。讨论显示地使用指针访问成员的优缺点。
【出题思路】
对比使用this指针访问成员的利弊。
【解答】
通过this指针访问成员的优点是,可以非常明确地指出访问的是对象的成员,并且可以在成员函数中使用与数据成员同名的形参;缺点是显得多余,代码不够简洁。
练习7.31:定义一对类X和Y,其中X包含一个指向Y的指针,而Y包含一个类型为X的对象。
【出题思路】
理解类的声明和定义。声明的作用是告知程序类的名字合法可用;定义的作用是规定类的细节。
【解答】
满足题意的程序如下所示:
class X;
class Y
{
X* x;//可以定义不完全类型的指针,但是无法创建不完全类型的对象。
};
class X
{
Y y;
};
类X的声明称为前向声明,它向程序中引入了名字X并且指明X是一种类类型。对于类型X来说,此时我们已知它是一个类类型,但是不清楚它到底包含哪些成员,所以它是一个不完全类型。我们可以定义指向不完全类型的指针,但是无法创建不完全类型的对象。
如果试图写成下面的形式,将引发编译器错误。
class Y;
class X
{
Y y; //错误,创建不完全类型Y的对象
};
class Y
{
X* x;
};
练习7.32:定义你自己的Screen和Window_mgr,其中clear是Window_mgr的成员,是Screen的友元。
【出题思路】
类可以把其他类定义成友元,也可以把其他类的成员函数定义成友元。当把成员函数定义成友元时,要特别注意程序的组织结构。
【解答】
要想让clear函数作为Screen的友元,只需要在Screen类中做出友元声明即可。本题的真正关键之处是程序的组织结构,我们必须首先定义Window_mgr类,其中声明clear函数,但是不能定义它;接下来定义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;
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;
}
运行结果: