文章目录
类的其他特性
这里主要特性包括:类型成员、类的成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回 *this、如何定义并使用类类型以及友元的更多知识。
类成员再探
这里将用一对相互关联的类来讲解。分别是 Screen 和 Window_mgr
定义一个类型成员
这里是成员部分代码:
class Screen {
public:
typedef std::string::size_type pos; // 当然,也可以用 using 声明类型别名
private:
pos cursor = 0; // 表示光标位置
pos height = 0, width = 0; // 高 和 宽
std::string contents; // 存放内容
};
在 public 部分定义 pos 使用户能够使用这个名字。其他的放入 private 部分隐藏具体细节。有一点需要注意的,和普通成员不同(普通成员无视定义顺序,可直接使用),类型成员需要先定义后使用。
Screen 类的成员函数
一个类,当然不能够缺少构造函数。这里添加一个构造函数令用户能够定义屏幕尺寸和内容,以及其他两个成员,分别负责移动光标和读取给定位置的字符:
class Screen {
public:
typedef std::string::size_type pos; // 当然,也可以用 using 声明类型别名
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);
private:
pos cursor = 0; // 表示光标位置
pos height = 0, width = 0; // 高 和 宽
std::string contents; // 存放内容
};
由于提供了其他的构造函数,编译器将不会自动生成默认的构造函数,所以 Scree() = default; 是必须的。
可以看到,提供了三个参数的构造函数中并没有初始化 cursor,所以 cursor 隐式的使用了类内初始值进行初始化。如果 cursor 没有类内初始值,就需要向其他的成员一样显示地初始化 cursor。
令成员作为内联函数
在类中,一般规模较小的函数适合于被声明为内联函数。定义在类内部的成员函数是自动 inline 的。如上 Screen 的构造函数 和 返回光标所指字符的 get 函数默认是内联函数。
可以在类的内部吧 inline 作为声明的一部分来显示声明为内联的成员函数,也可以在类外部用 inline 关键字修饰函数的定义:
inline Screen& Screen::move(pos r,pos c) { // 在定义处指定 inline
pos row = r * width; // 计算行的位置
cursor = row + c; // 在行内将光标移动到指定列
return *this; // 以左值的形式返回对象
}
char Screen::get(pos r,pos c) const { // 在类内部声明成 inline
pos row = r * width;
return contents[row + c];
}
// !! 为什么是 row = r * width,因为存放数据的时候用的一个 字符串存放二维的数组,
// 相当于使用的一维数组存放来二维数据
虽然不需要在声明处和定义处同时说明 inline,但是最好在类外部定义的地方说明 inline,这样使类更容易理解。
重载成员函数
成员函数同样可以重载,同样需要函数之间在参数数量或者类型上有所区别。
如以上类定义中的两个 get 函数,分别返回的是光标当前位置 与 有行号和列号确定的位置的字符。编译器同样根据实参来决定运行哪一个函数。
可变数据成员
有时(但不频繁)会发生这样一种情况,我们希望能修改类的某个数据成员,即使是在一个 const 成员函数内。可以通过在变量声明中假如 mutable 关键字实现。
一个可变数据成员永远不会是 const,即使它是 const 对象的成员。因此一个 const 成员函数可以改变一个可变成员的值。如:在 Screen 中添加一个名为 access_ctr 的可变成员,通过它追踪每个 Screen 成员函数被调用的次数:
class Screes {
public:
void some_member() const;
private:
mutable size_t access_ctr; // 即使在一个 const 对象内仍能被修改
// 其他成员与之前一致
};
void Screen::some_member() const {
++ access_ctr; //保存一个计数值,用于记录成员函数被调用次数
// 其他操作
}
注意 mutable 定义的变量的初始化
类数据成员的初始值
在定义好 Screen 类后,还将定义一个窗口管理类并用它表示显示器上的一组 Screen。这个类将包含一个 Screen 类型的 vector,每个元素表示一个特定的 Scree。默认情况下,我们希望 Window_mgr 类开始时总是拥有一个默认初始化的 Screen。在 C++11 中,最好方式就是把这个默认值声明成一个类内初始值。
class Window_mgr {
private:
// 这个 Window_mgr 追踪的 Screen
// 默认情况下,一个 Window_mgr 包含一个标准尺寸的空白 Screen
std::vector<Screen> screens { Screen(24,80,' ') };
};
当提供一个类内初始值时,必须以符号 = 或者花括号表示.>
返回 *this 的成员函数
现在将继续添加一些函数,负责设置光标所在位置或者其他任一给定位置的字符。
class Screen {
public:
Screen &set(char);
Screen &set(pos, pos, char);
// 其他的和之前一致
};
inline Screen& Screen::set(char ch) {
contents[cursor] = ch;
return *this; // 将 this 对象作为左值返回
}
inline Screen& Screen::set(pos r,pos c,char ch) {
contents[r * width + c] = ch;
return *this; // 将 this 对象作为左值返回
}
和 move 函数相同,set 成员函数的返回值是调用 set 对象的引用。返回引用的函数是左值的,意味着函数返回的是对象本身而非对象的副本。所以可以吧一系列操作连接在一条表达式中。
myScreen.move(4,0).set('#');
// 等价于
myScreen.move(4,0);
myScreen.set('#');
如果我们 move 函数返回的是 Screen 而不是 Screen&
那么以上语句就等价于:
Screen tmp = myScreen.move(4,0);
tmp.set('#');
即相当于改变的是 临时对象 tmp,而不是 原对象 myScreen
从 const 成员函数返回 *this
紧接着,我们将添加一个名为 display 的操作,负责打印 Screen 的内容。同样,我们希望这个函数能够与 move 和 set 出现在同一序列中,所以函数的返回值仍然是 Screen&
逻辑上,我们是显示 Screen 的内容,并没有改变,因此我们令 display 为一个 const 成员函数。但是此时,this 指针将是指向 const 的指针而 *this 是 const 对象。所以可知 display 的返回类型便成了 const Screen&。如果 display 返回一个 const 引用,就不能把 display 嵌入到一组动作中去:
Screen myScreen;
// display 如果返回常量引用,则调用 set 将发生错误
myScreen.display(cout).set('*');
// 即使 myScreen 是一个非常量对象,对 set 调用仍将无法通过编译
即:一个 const 成员函数如果以引用的形式返回 *this,那么他的返回类型将是常量引用
基于 const 的重载
通过区分成员函数是否是 const 的,我们可以对其进行重载,原因与我们之前根据指针参数是否指向 const 而重载函数的原因差不多。具体来说,因为非常量的版本对于常量对象是不可用的,所以只能在一个常量对象时调用 const 成员函数。另一方面,虽然可以在非常量对象上调用常量版本或者非常量版本,但此时显然非常量版本更好。
这里将定义一个名为 do_display 的私有函数,有它负责实际的打印工作。
class Screen {
public:
// 根据对象是否是 const 重载了 display函数
Screen &display(std::ostream &os){
do_display(os);
return *this;
}
const Screen &display(std::ostream &os) const {
do_display(os);
return *this;
}
private:
void do_display(std::ostream &os) const { os << contents; }
// 其他成员和之前一致
};
这样,我们就有了 常量版本与非常量版本的 display 函数,如此 我们便可以解决 myScreen.display(cout).set(’*’) 的问题,专门对 非常量对象进行调用。
Screen myScreen(5,3);
const Screen blank(5,3);
myScreen.set('#').dispaly(cout); // 调用非常量版本
blank.display(cout); // 调用常量版本
-----3月20日更-----(大家可以当我傻 X 了,突然写了这个问题,实际可能大家都知道)
今天回顾了一下这篇博客,看到 display 重载了 const 版本,脑中突然引发了一个问题:
为什么如同 get 函数,我们不需要重载 const 版本,而是直接 char get() const { return contents[cursor]; } (假设叫函数①),
而没有 char get() { return contents[cursor]; } 然后重载:const char get() const { return contents[cursor]; } ?
解决:(当然了,这是我的想法,不知道对不对),虽然函数①中的 this 指针的类型是 const Screen* const,所以 contents[cursor] 也应当是 const 的,然是我们返回的并不是引用,我们返回的是该对象的拷贝,所以根本无须关心这里的 this 指针类型到底是不是 const 的。所以无论对象是否是 const,我们都能够使用这个函数。
而 display 函数,我们返回的是当前对象的引用,如果当前对象是 const 的,那么函数返回值就必须是 const 的,所以必须重载。