C++ Primer 第七章 7.3 练习和总结

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的环境下编译的。

发布了54 篇原创文章 · 获赞 6 · 访问量 3342

猜你喜欢

转载自blog.csdn.net/zengqi12138/article/details/104131883