【C++】类(三):类的其它特性

ops/2024/11/25 10:04:08/

7.3 类的其它特性

本节将继续介绍之前章节当中 Sales_data 没有体现出来的类的特性,包括:类型成员、类的成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回*this、如何定义并使用类类型及友元类等。

7.3.1 类成员再探

这部分定义了一对相互关联的类,分别是 Screen 和 Window_mgr。

定义一个类型成员

Screen 表示显示器的一个窗口。每个 Screen 包含一个用于保存 Screen 内容的 string 成员和三个 string::size_type 类型的成员,分别表示光标的位置以及屏幕的高和宽。

除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名。由类定义的类型名字和其它成员一样存在访问限制,可以是 public 或 private 当中的一种:

class Screen {public: typedef std::string::size_type pos;private:pos cursor = 0;pos height = 0, width = 0;std::string contents;
};

我们在 Screen 的 public 部分定义了 pos。Screen 的用户不应该知道 Screen 使用一个 string 对象存放它的数据,因此通过把 pos 定义为 public 成员可以隐藏 Screen 实现的细节(这段话全部引用自原书对应部分,但这一部分没太看懂???为什么将 pos 定义为 public 成员就可以隐藏 Screen 的实现细节?)。

Screen 类成员函数

为了使 Screen 类更加实用,含需要添加一个构造函数令用户能够定义屏幕的尺寸和内容,以及其它两个成员,负责移动光标和读取给定位置的字符。

class Screen {public:typedef std::string::size_type pos;Screen() = default; // 由于 Screen 类存在其它构造函数// 因此必须显式地定义默认构造函数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;
};

回忆之前学习过的内容,由于我们显式地给出了一个含有形参列表的构造函数的定义,在这种情况下编译器不会自动生成合成的默认构造函数。因此,如果我们在该情况下仍然需要编译器为我们生成默认的构造函数,则应该使用= default告诉编译器我们的这一需求。

需要注意的是,第二个构造函数(即含有形参列表的那个构造函数)在执行时将会为私有成员 cursor 隐式地指定类内初始值。如果类内不存在 cursor 的初始值,那么我们需要像其他成员那样对 cursor 进行显式初始化。

令成员作为内联函数

定义在类内部的成员函数是自动 inline 的(即无论是否显式地声明 inline,类内的成员函数都将会是默认 inline 的)。因此,Screen 类的构造函数和 get 函数默认是 inline 的。

可以在类的内部将 inline 作为声明的一部分显式地声明成员函数,如果类的定义是在类外,同样也应该用 inline 来对外部定义的函数进行修饰。

需要注意的是,我们不是必须显式地声明 inline,但这样做是一个合法的好习惯。最好只在类外定义类函数的地方显式地声明 inline,有助于类的理解。

重载成员函数

和非成员函数一样,成员函数也可以被重载。

可变数据成员

有时会出现如下情况,即我们希望修改类的某个数据成员,即使是在一个 const 成员函数当中。可以通过在变量的声明中加入 mutable 关键字来完成。

一个可变数据成员永远不会是 const,即使它是 const 对象的成员。因此,一个 const 成员函数可以改变一个可变成员的值。例如,给 Screen 类添加一个名为 access_ctr 的可变成员,通过它我们可以追踪每个 Screen 的成员函数被调用多少次:

class Screen {public:void some_member() const;private:mutable size_t access_ctr;	// 声明为 mutable 使得 access_ctr 即使在// 权限为 const 的成员函数当中时, 其值仍然可以被修改
};void Screen::some_member() const {++ access_ctr;
}

(个人认为 mutable 关键字应该不是特别常用,因为它打破了 const 的规则)

类数据成员的初始值

定义好 Screen 类之后,将继续定义一个窗口管理类并使用它表示显示器上的一组 Screen。这个类将包含一个 Screen 类型的 vector,每个元素表示一个特定的 Screen。默认情况下,我们希望 Window_mgr 类开始时总是有一个默认初始化的 Screen。在 C++ 11 标准下,最好的方式就是把这个默认追声明为类内初始值:

class Window_mgr {private:std::vector<Screen> screens{Screen(24, 80, ' ')};
};

7.3.2 返回 *this 的成员函数

接下来我们添加一些函数,它们负责设置光标所在位置的字符或其它任一给定位置的字符:

class Screen {public:Screen &set(char);Screen &set(pos, pos, char);
};
inline Screen &Screen::set(char c) {contents[cursor] = c;	// 设置当前光标所在位置的新值return *this;			// 将 this 对象作为左值返回
}
inline Screen &Screen::set(pos r, pos col, char ch) {contents[r * width + col] = ch;	// 同上return *this;					// 同上
}

返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本(对左值和右值又有了一些直观的新理解,可以简单地将返回左值理解为返回对象本身,将返回右值理解为返回对象的副本)。可以将一系列操作连接成下述表达式:

myScreen.move(4, 0).set('#'); // 移动光标到指定位置(move), 并设置该位置的字符(set)

如果我们将 move 和 set 返回值设置为 Screen 而非 Screen&,则上述语句的行为将大不相同。在此例中等价为:

Screen temp = myScreen.move(4, 0);	// 拷贝返回值
temp.set('#');						// 不会改变 myScreen 的 contents, 除非将 temp 赋值给 myScreen

从 const 成员函数返回 *this

加下来添加一个名为 display 的操作,负责打印 Screen 的内容。

从逻辑上来说,返回 Screen 的内容不需要修改 Screen 的内容,因此可以将 display 成员函数声明为 const 成员,此时 this 将是一个指向 const 的指针,而 *this 是 const 对象(回忆之前的内容,this 指针本身必然是一个指向 const 的指针,成员函数定义的形参列表之后的 const 关键字指的是将 this 指针指向的对象声明为 const 类型,意味着不可以使用 this 指针对 *this进行修改)。由此推断,display 的返回类型应该是 const Sales_data&。然而,如果真的令 display 返回一个 const 引用,则我们将不能把 display 嵌套到一组动作的序列当中:

Screen myScreen;
// 如果 display 返回常量引用, 则调用 set 将引发错误, 因为 set 试图改变一个常量的值
myScreen.display(cout).set('*'); // 错误❌

即使 myScreen 本身是一个非常量对象,对 set 的调用也无法通过编译。问题在于 display 的 const 版本返回的是常量引用,而 set 无权对常量进行修改。

基于 const 的重载

通过区分成员函数是否为 const 的,我们可以对其进行重载,其原因与之前所说的根据指针参数是否指向 const 而对函数进行重载的原因类似。具体来说,非常量版本的函数对于常量对象是不可用的,我们只能在常量对象上调用 const 成员函数。现在,在非常量对象上调用非常量版本是一个更好的匹配。

在下例当中,我们定义一个名为 do_display 的私有成员,负责打印 Screen 的实际工作。所有的 display 操作都将调用这个函数,返回执行操作的对象:

class Screen {public: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)调用另一个成员函数(do_display)时,this 指针在其中隐式地传递,因此在调用 display 时,this 隐式地传递给了 do_display。当 display 的非常量版本调用 do_display 时,它的 this 指针将隐式地从指向非常量的指针转换成指向常量的指针。

当 do_display 完成后,display 函数各自返回解引用 this 所得的对象。在非常量版本中,this 指向一个非常量对象,因此 display 返回一个普通的引用,而 const 成员则返回一个常量引用。

通过对 display 进行重载,并引入 do_display,当我们在某个对象上调用 display 时,该对象是否为 const 决定了应该调用 display 的哪个版本。

在实践中,设计良好的 C++ 代码常常包含大量的类似于 do_display 的小函数。通过调用这些函数,可以完成一组其它函数的 “实际” 工作

7.3.3 类类型

我们可以把类的名字作为类型的名字使用,从而直接指向类类型。

类的声明

就像我们可以把函数的声明和定义分开一样,我们可以只声明类但暂时不定义它:

class Screen;	// Screen 类的声明

这种声明有时称为前向声明(forward declaration),它向程序引入了名字 Screen 并指明它是类类型。对于此时的 Screen 类而言,在声明之后定义之前它是一个不完全类型,即我们只知道它是一个类类型,但是不清楚它包含哪些成员。

7.3.4 友元再探

上一章中所提到的 Sales_data 类将三个普通的非成员函数定义为友元。类还可以把其它类定义为友元,同时也可以将其他类的成员函数定义为友元。此外,友元函数可以直接定义在类的内部,这种函数是隐式内联的。

类之间的友元关系

举一个友元的例子,我们的 Window_mgr 类的某些成员可能需要访问它管理的 Screen 类的内部数据。例如,假设我们需要为 Window_mgr 添加一个名为 clear 的成员,它负责把一个指定的 Screen 的内容置为空白。为了完成上述任务,clear 需要访问 Screen 的私有成员。如果想要使上述访问合法,Screen 需要把 Window_mgr 指定为它的友元:

class Screen {friend class Window_mgr;
};

如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员:

class Window_mgr {public:using ScreenIndex = std::vector<Screen>::size_type;void clear(ScreenIndex);private:std::vector<Screen> screens{Screen(24, 80, ' ')};
};void Window_mgr::clear(ScreenIndex i) {Screen &s = screens[i];s.contents = string(s.height * s.width, ' ');
}

需要注意的是,友元关系不存在传递性。如果 Window_mgr 有它自己的友元,则这些友元不能理所当然地具有访问 Screen 的特权。

令成员函数作为友元

除了令整个 Window_mgr 类称为 Screen 类的友元之外,Screen 还可以只为 clear 提供访问权限。

当把一个成员函数声明为类的友元时,必须指明该成员函数属于哪个类:

class Screen {friend void Window::clear(ScreenIndex);
};

函数重载和友元

重载的函数尽管名字相同,但是它们仍然是不同的函数,因为它们的形参列表不同。对于不同的重载函数,如果想要将它们声明为某个类的友元,则需要全部进行新的声明。


http://www.ppmy.cn/ops/136533.html

相关文章

机器学习入门-Scikit-learn

目录 一.Sklearn基本介绍 二.以鸢尾花数据集为例&#xff0c;理解基础运用 1.导入包 2.加载数据集 3.数据预处理 4.数据集拆分 5.模型训练 6.模型评估 7.模型保存和加载 三.碎碎念 一.Sklearn基本介绍 scikit-learn是一个开源的Python机器学习库&#xff0c;提供了大…

无Linux管理员权限,照样可以安装CUDA

以下演示内容使用CUDA版本为 CUDA11.7 1、官网 官网:CUDA官网下载地址 这里会列出所有的CUDA版本,选择需要的版本即可。 2、查看系统信息 这里分享三个命令,可以查看Linux系统的配置信息,方便下一步下载合适的CUDA版本。 可以根据这些命令输出的系统配置信息选择相应的…

阿里云IIS虚拟主机部署ssl证书

宝塔配置SSL证书用起来是很方便的&#xff0c;只需要在站点里就可以配置好&#xff0c;但是云虚拟主机在管理的时候是没有这个权限的&#xff0c;只提供了简单的域名管理等信息。 此处记录下阿里云&#xff08;原万网&#xff09;的IIS虚拟主机如何配置部署SSL证书。 进入虚拟…

IntelliJ IDEA常用快捷键

文章目录 环境快捷键外观编辑移动光标提示查找Live Templates列操作调试运行 环境 Ubuntu 24.04.1IntelliJ IDEA 2024.1.6 快捷键 外观 Alt 1&#xff1a;打开/关闭“项目”窗口&#xff08;即左边的导航窗口&#xff09; Alt 4&#xff1a;打开/关闭“运行”窗口 Alt …

HarmonyOS 应用中复杂业务场景下的接口设计

文章目录 前言设计理念与原则模块化设计动态可扩展性支持多种查询模式统一响应格式实现复杂业务接口示例后端接口代码&#xff08;Node.js Express&#xff09;前端调用代码&#xff08;ArkTS&#xff09;前端界面&#xff08;ArkUI&#xff09; 代码详解后端代码详解前端代码…

数据结构:链表进阶

链表进阶 1. ArrayList的缺陷2. 链表2.1 链表的概念及结构2.2 链表的实现 3.链表面试题4.LinkedList的使用5.1 什么是LinkedList4.2 LinkedList的使用 5. ArrayList和LinkedList的区别 1. ArrayList的缺陷 通过源码知道&#xff0c;ArrayList底层使用数组来存储元素&#xff1…

Python3 Flask 应用中使用阿里短信发送

代码大部分都是官网提供的&#xff0c;稍做了一点修改。 需要申请 access_key_id 和 access_key_secret 很简单这里就不絮叨了&#xff0c;如果你不会私信我&#xff0c;我教你。 安装命令 pip install alibabacloud_dysmsapi201705253.1.0 # -*- coding: utf-8 -*- from…

HARCT 2025 新增分论坛7:机器人和自动化的新趋势

会议名称&#xff1a;机电液一体化与先进机器人控制技术国际会议 会议简称&#xff1a;HARCT 2025 大会时间&#xff1a;2025年1月3日-6日 大会地点&#xff1a;中国桂林 主办单位&#xff1a;桂林航天工业学院、广西大学、桂林电子科技大学、桂林理工大学 协办单位&#…