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

server/2024/11/29 4:01:44/

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/server/145808.html

相关文章

Docker的save和export命令的区别,load和import的区别 笔记241124

Docker的save和export命令的区别,load和import的区别 解说1: Docker的save和export命令&#xff0c;以及load和import命令&#xff0c;在功能和使用场景上存在显著的区别。以下是对这两组命令的详细对比和解释&#xff1a; Docker save和export命令的区别 使用方式和目的&am…

泛型擦除是什么?

泛型擦除(Type Erasure)是Java编译器在编译泛型代码时的一种机制&#xff0c;它的目的是确保泛型能够与JAVA的旧版本(即不支持泛型的版本)兼容。泛型擦除会在编译时移除泛型类型信息&#xff0c;并将泛型类型替换为其非泛型的上限类型(通常是Object) 详细解释 在Java中&#…

Spring 小案例体验创建对象的快感(Java EE 学习笔记05)

我们了解了Spring的特性及功能后&#xff0c;接下来我们利用下面的小案例来体验以下Spring的使用方式。 首先创建项目 打开IDEA&#xff0c;选择new工程&#xff0c;如下图&#xff1a; ​ 然后&#xff0c;指定工程名字为SpringDemo&#xff0c;并且指定工程目录为D盘。配…

【Python爬虫五十个小案例】爬取豆瓣电影Top250

博客主页&#xff1a;小馒头学python 本文专栏: Python爬虫五十个小案例 专栏简介&#xff1a;分享五十个Python爬虫小案例 &#x1fab2;前言 在这篇博客中&#xff0c;我们将学习如何使用Python爬取豆瓣电影Top250的数据。我们将使用requests库来发送HTTP请求&#xff0c;…

Spring Boot开发实战:从入门到构建高效应用

Spring Boot 是 Java 开发者构建微服务、Web 应用和后端服务的首选框架之一。其凭借开箱即用的特性、大量的自动化配置和灵活的扩展性&#xff0c;极大简化了开发流程。本文将以实战为核心&#xff0c;从基础到高级&#xff0c;全面探讨 Spring Boot 的应用开发。 一、Spring B…

命令行版 postman 之 post 小工具

依赖 curljq post.sh #!/bin/bashBASEhttp://119.119.119.119 METHOD$1 URL$BASE/$2 LOGIN$BASE/login echo $URL token$(curl --silent $LOGIN -H Accept: application/json, text/plain, */* -H Accept-Language: zh-CN,zh;q0.9 -H Connection: keep-alive -H Con…

JSON数据转化为Excel及数据处理分析

在现代数据处理中&#xff0c;JSON&#xff08;JavaScript Object Notation&#xff09;因其轻量级和易于人阅读的特点而被广泛使用。然而&#xff0c;有时我们需要将这些JSON数据转化为Excel格式以便于进一步的分析和处理。本文将介绍如何将JSON数据转化为Excel文件&#xff0…

网络安全笔记

# 网络安全概述 ### 网络安全的特征 - **机密性&#xff1a;信息不泄露给非授权的实体或对象** - **完整性&#xff1a;数据未经授权不能进行改变的特性&#xff0c; 即信息在存储或传输过程中保持不被修改&#xff0c;不被破坏的特性** - **可用性&#xff1a;可被授权实体访…