7.3 类的其他特性
7.3.1类成员再探
在类中,我们也可以使用为数据类型启类型别名。
class Screen{
public:
using pos = string::size_type;
private:
pos width=0,height=0;//类内初始值初始化
pos cursor {0};//初始化列表初始化
string contents;
};
至于为什么这个类型别名,要使用public访问限定符来修饰,我认为这是由于类的使用者,有可能用到这个数据结构。所以需要设置为public。
类型别名也可以使用typedef来定义。
成员函数作为内联函数
直接在类中声明和定义的函数,会被默认修饰为内联函数。
我们也可以显式的使用inline关键字修饰成员函数,表示该函数时内联的。我们可以在类中声明的时候就将函数声明为inline,也可以在定义的时候声明为inline。
Class_A{
public:
void func_1(){};//在类中定义为inline
inline void func_2();//显式的定义为inline
void func_3();//在外部定义的时候,修饰为inline
};
void Class_A::func_2(){
//todo
}
inline void Class_A::func_3(){
}
注意,和之前定义内联函数一样,我们一般在头文件中声明和定义内联函数。
类的成员函数同样可以重载,根据形参列表中的形参数量和类型进行区别。
可变数据成员
之前说过,如果一个成员函数的内部没有修改数据成员的值,或者说,不允许修改数据成员的值,我们可以将这个函数声明为常量函数。
但是C++提供了mutable关键字,用该关键字修饰的数据成员,即使是在常量函数中,也可以修改其值。
Class Class_A{
private:
mutable int count;
public:
void do_something() const {
++count;
}
};
从const变量可以使用const_cast,修改为非const,和mutable修饰的数据成员可以在常量函数中修改,可以看出C++语言确实灵活,有些规则并不是死的,但是我觉得这种灵活也导致了混乱= =
类数据成员的初始值
在这篇总结的最前面我就写了,类数据成员的初始值可以使用类内初始值,和列表初始化。
类内初始值使用=进行赋值,而列表初始化,则使用一对花括号。
练习
7.23
7.24
class Screen {
public:
using pos = string::size_type;
Screen() = default;
Screen(pos w, pos h) :width(w), height(h), contents(w*h,' ') {};
Screen(pos w, pos h, char c) :width(w), height(h), contents(w*h, c) {};
private:
pos width = 0, height = 0;//类内初始值初始化
pos cursor{ 0 };//初始化列表初始化
string contents;
};
注意构造函数需要是public的,因为如果为private,则在类的外部没法访问。
7.25
可以依赖默认的版本,因为Screen类中,没有涉及动态分配内存的变量。
7.26
也可以试一下,上文总结的三种将函数定义为inline的方法。
inline double avg_price()const;
7.3.2 返回*this的成员函数
如果成员函数返回*this,那么我们就可以使用链式调用。
如果我没有记错的话,应该叫链式调用
即:
class Class_A{
public:
Class_A& move() {
//todo
return *this;
}
const Class_A & move() const {
return *this;
}
const Class_A& set() const {
//todo
return *this;
}
};
这样在调用Class_A的对象的成员函数时,我们可以这么写
Class_A a;
a.move().move().move()
需要注意的是,如果调用的成员函数常量成员函数,返回的类型则必须为常量引用,如果不这么写,函数是会报错的。
上面代码的set函数,返回的就是常量引用。
常量成员函数和非常量成员函数是可以重载的。
上面的代码中两个move可以实现重载,调用move时,根据类的对象是否是常量,调用相应的类型。
如果对象是常量,则调用常量版本的move,如果不是常量则调用非常量版本的move。
练习
7.27
class Screen {
public:
using pos = string::size_type;
Screen() = default;
Screen(pos w, pos h) :width(w), height(h), contents(w*h, ' ') {};
Screen(pos w, pos h, char c) :width(w), height(h), contents(w*h, c) {};
//将光标移动到第几行的第几个
inline Screen& move(pos r, pos c) {
//假定第一行从0开始计数,如果从1开始计数从row中,r需要减一
pos row = r * width;
cursor = row + c;
return *this;
}
inline Screen& set(char c) {
contents[cursor] = c;
return *this;
}
inline Screen& set(pos row,pos col,char ch) {
contents[row*width + col ]= ch;
return *this;
}
inline Screen& display(ostream& temp_cout) {
//temp_cout << contents;
do_display(temp_cout);
return *this;
}
inline const Screen& display(ostream& temp_cout) const {
//temp_cout << contents;
do_display(temp_cout);
return *this;
}
inline void do_display(ostream& temp_cout) const {
temp_cout << contents;
}
//intline Screen& set(cah)
private:
//屏幕的宽高
pos width = 20, height = 80;//类内初始值初始化
//光标的位置
pos cursor{ 0 };//初始化列表初始化
//屏幕内容
string contents;
};
为什么重载的display中,写的逻辑只有那么一丢丢,却还是要调用一个do_display,总结书上的内容就是一句话,因为这些代码往往是重复的,通过放入另一个函数中,可以有效的减少重复代码。也让我们在修改代码的时候,减轻工作量。
7.28
第一次打印会照常输出,但是第二次打印只会输出X。
7.30
优点:
显式调用可读性更高,尤其是数据成员的变量名字和形参的变量名字一样时
缺点:
很枯燥,每个数据成员前面都要写this->,这增加的程序员的工作量。
7.3.3 类类型
两个类名不同,但是数据成员和成员函数一摸一样的类,它们不是同一个类。
我们可以认为类名就是类类型。
Class Class_A{
//todo
};
Class_A a;
class Class_A a;
这两个定义变量的方式是等价的。
我们可以在文件的最前面声明一个类,然后再后面定义它。
class Class_A;
这样的声明叫做前向声明,此时它是一个不完整类型,即使没有对该类进行定义,我们仍然可以用该类作一些有限的操作,比如,定义Class_A的引用或者指针。
对于其他的操作,由于Class_A没有定义,所以无法操作。
只有我们定义完一个类,编译器才能够知道一个类所需的存储空间大小,所以我们无法在一个类中定义该类的变量(引用和指针除外);
练习
7.31
struct X {
Y* y;
};
class Y {
X x;
};
7.3.4 友元再探
之前我们只是将外部函数声明为某一个类的友元。其实我们可以将另一个类,或者另一个类的成员函数声明为其他类的友元。
需要注意的是,如果把一个类或者一个类的成员函数声明为另外一个类的友元。
这种情况下,需要记住。
1.如果B中使用A类作为友元,则需要在B类之前声明A类。
class A
class B{
friend class A;
public:
void f_b_1();
};
class A{
public:
void f_a_1();
};
B可以访问A中的所有非公开成员。
2.如果B类,仅声明类A的成员函数,为类B的友元, 则需要在类B之前,声明类A的成员函数。
class A{
void f_a_1();//声明
};
class B{
friend void A::f_a_1();
void f_b_1();
};
void A::f_a_1(){};
对于重载函数,每个都要声明为友元,否则只有对对应的函数才是友元函数。
通常对于类和非成员函数,都需要在声明为友元之前声明它们。但是有些编译器并不强制这一行为,总的来说为了统一还是这样作好点。
需要注意的是,友元修饰非成员函数,只是表明它的访问状态,并不是声明。
就算我们在类的内部定义友元类,在外部使用的时候,还是需要声明。
我觉得已经没有人这么做。。这纯属给自己添乱。
练习
7.32
这个题我认为是有错误的。无法将clear声明为友元,只能将Window_mgr声明为友元
class Window_mgr;
class Screen {
friend Window_mgr;
public:
using pos = string::size_type;
Screen() = default;
Screen(pos w, pos h) :width(w), height(h), contents(w*h, ' ') {};
Screen(pos w, pos h, char c) :width(w), height(h), contents(w*h, c) {};
//将光标移动到第几行的第几个
inline Screen& move(pos r, pos c) {
//假定第一行从0开始计数,如果从1开始计数从row中,r需要减一
pos row = r * width;
cursor = row + c;
return *this;
}
inline Screen& set(char c) {
contents[cursor] = c;
return *this;
}
inline Screen& set(pos row,pos col,char ch) {
contents[row*width + col ]= ch;
return *this;
}
inline Screen& display(ostream& temp_cout) {
//temp_cout << contents;
do_display(temp_cout);
return *this;
}
inline const Screen& display(ostream& temp_cout) const {
//temp_cout << contents;
do_display(temp_cout);
return *this;
}
inline void do_display(ostream& temp_cout) const {
temp_cout << contents;
}
//intline Screen& set(cah)
private:
//屏幕的宽高
pos width = 20, height = 80;//类内初始值初始化
//光标的位置
pos cursor{ 0 };//初始化列表初始化
//屏幕内容
string contents;
};
class Window_mgr {
public:
void clear(std::vector<int>::size_type index);
private:
std::vector<Screen> screen_list{ Screen(20,20,' ') };
};
void Window_mgr::clear(std::vector<int>::size_type index) {
//screen_list[index].contents =
Screen &s = screen_list[index];
s.contents=string(s.width*s.height,' ');
}
这里是把Window_mgr声明为友元类
如果要把clear声明为友元函数, 需要将Window_mgr的定义写在Screen前面,而Window_mgr中有Screen的变量,所以把类Window_mgr的定义写在Screen前面,则会造成Screen未定义,而写在后面则会造成函数clear未定义。
也就是说,按照书上的这个步骤是没法进行的
注:我是在vs2017的环境下编译的。