在结构体、成员和成员选择的介绍中,我们介绍了结构体这种程序定义类型,它可以包含成员变量。以下是一个用于存储日期的结构体示例:
struct Date
{
int year {}; // 年
int month {}; // 月
int day {}; // 日
};
现在,如果我们想将日期打印到屏幕上(这可能是一个经常需要执行的操作),编写一个函数来完成这个任务是有意义的。以下是一个完整的程序:
#include <iostream>
struct Date
{
// 这里是我们的成员变量
int year {};
int month {};
int day {};
};
void print(const Date& date)
{
// 使用成员选择运算符(.)访问成员变量
std::cout << date.year << '/' << date.month << '/' << date.day;
}
int main()
{
Date today { 2020, 10, 14 }; // 使用聚合初始化结构体
today.day = 16; // 使用成员选择运算符(.)访问成员变量
print(today); // 使用普通调用约定调用非成员函数
return 0;
}
该程序输出:
2020/10/16
属性与行为的分离
环顾四周,你看到的到处都是对象:书籍、建筑物、食物,甚至是你自己。现实中的对象有两个主要组成部分:1)一定数量的可观察属性(例如重量、颜色、大小、坚固性、形状等),2)一定数量的基于这些属性可以执行或被施加的行为(例如被打开、损坏其他东西等)。这些属性和行为是不可分割的。
在编程中,我们用变量表示属性,用函数表示行为。
在上面的Date
示例中,我们定义了属性(Date
的成员变量)和使用这些属性执行行为(print()
函数)是分开的。我们只能根据print()
函数的const Date&
参数来推断Date
和print()
之间的联系。
虽然我们可以将Date
和print()
放入一个命名空间(使它们作为一组打包在一起的意图更加明确),但这会向程序中添加更多的名称和更多的命名空间前缀,从而使代码变得杂乱。
如果有一种方法可以将属性和行为一起定义为一个单独的包,那该多好啊。
成员函数
除了可以包含成员变量外,类类型(包括结构体、类和联合体)还可以拥有自己的函数!属于类类型的函数称为成员函数。
顺便说一下:
在其他面向对象的语言(如Java和C#)中,这些被称为方法。尽管C++中不使用“方法”一词,但那些首先学习了其他语言的程序员可能仍然会使用这个术语。
不是成员函数的函数称为非成员函数(或偶尔称为自由函数),以区别于成员函数。上面的print()
函数就是一个非成员函数。
作者注:
在本课中,我们将使用结构体来展示成员函数的示例——但这里展示的所有内容同样适用于类。当我们讲到相关内容时,原因会变得显而易见,我们将在后续课程(14.5——公共和私有成员以及访问说明符)中展示带有成员函数的类的示例。
成员函数必须在类类型定义内部声明,并且可以在类类型定义的内部或外部定义。提醒一下,定义也是一种声明,因此如果我们在类内部定义了一个成员函数,这也算作一种声明。
为了保持简单,我们现在将成员函数定义在类类型定义的内部。
相关内容:
我们在第15.2课——类和头文件中展示如何在类类型定义的外部定义成员函数。
成员函数示例
让我们重写本课开头的Date
示例,将print()
从非成员函数转换为成员函数:
// 成员函数版本
#include <iostream>
struct Date
{
int year {};
int month {};
int day {};
void print() // 定义一个名为print的成员函数
{
std::cout << year << '/' << month << '/' << day;
}
};
int main()
{
Date today { 2020, 10, 14 }; // 使用聚合初始化结构体
today.day = 16; // 使用成员选择运算符(.)访问成员变量
today.print(); // 使用成员选择运算符(.)调用成员函数
return 0;
}
该程序编译并产生与上面相同的结果:
2020/10/16
非成员和成员示例之间有三个关键区别:
print()
函数的声明(和定义)位置- 调用
print()
函数的方式 - 在
print()
函数内部访问成员的方式
让我们逐一探讨这些区别。
成员函数在类类型定义内部声明
在非成员示例中,print()
非成员函数在Date
结构体的外部定义,位于全局命名空间中。默认情况下,它具有外部链接,因此可以从其他源文件中调用它(在适当的前置声明的情况下)。
在成员示例中,print()
成员函数在Date
结构体定义的内部声明(并且在这个例子中,也定义了)。由于print()
是作为Date
的一部分声明的,这告诉编译器print()
是一个成员函数。
在类类型定义内部定义的成员函数隐式地是内联的,因此如果类类型定义被包含在多个代码文件中,它们不会违反单一定义规则。
相关内容:
成员函数也可以在类定义内部(前置)声明,并在类定义之后定义。我们在第15.2课——类和头文件中讨论了这一点。
调用成员函数(以及隐式对象)
在非成员示例中,我们调用print(today)
,其中today
被显式地作为参数传递。
在成员示例中,我们调用today.print()
。这种语法使用成员选择运算符(.)来选择要调用的成员函数,这与我们访问成员变量的方式一致(例如today.day = 16;
)。
所有(非静态)成员函数都必须使用该类类型的对象来调用。在这个例子中,today
是调用print()
的today
对象。
请注意,在成员函数的情况下,我们不需要将today
作为参数传递。调用成员函数的对象被隐式地传递给成员函数。因此,调用成员函数的对象通常被称为隐式对象。
换句话说,当我们调用today.print()
时,today
是隐式对象,并且它被隐式地传递给print()
成员函数。
相关内容:
我们在第15.1课——隐藏的“this”指针和成员函数链式调用中讨论了关联对象是如何实际传递给成员函数的。
在成员函数内部访问成员时使用隐式对象
再次回顾非成员版本的print()
:
// 非成员版本的print
void print(const Date& date)
{
// 使用成员选择运算符(.)访问成员变量
std::cout << date.year << '/' << date.month << '/' << date.day;
}
这个版本的print()
有一个引用参数const Date& date
。在函数内部,我们通过这个引用参数访问成员,即date.year
、date.month
和date.day
。当调用print(today)
时,date
引用参数绑定到参数today
,date.year
、date.month
和date.day
分别解析为today.year
、today.month
和today.day
。
现在,让我们再次查看print()
成员函数的定义:
void print() // 定义一个名为print的成员函数
{
std::cout << year << '/' << month << '/' << day;
}
在成员示例中,我们直接访问成员year
、month
和day
。
在成员函数内部,任何未使用成员选择运算符(.)前缀的成员标识符都与隐式对象相关联。
换句话说,当调用today.print()
时,today
是我们的隐式对象,year
、month
和day
(未加前缀)分别解析为today.year
、today.month
和today.day
的值。
关键洞察:
- 对于非成员函数,我们需要显式地将对象传递给函数以进行操作,并且通过该对象显式地访问成员。
- 对于成员函数,我们将对象隐式地传递给函数以进行操作,并且通过该对象隐式地访问成员。
另一个成员函数示例
以下是一个包含稍微复杂一些的成员函数的示例:
#include <iostream>
#include <string>
struct Person
{
std::string name {}; // 姓名
int age {}; // 年龄
void kisses(const Person& person)
{
std