14-3 成员函数
在第13.7节——结构体、成员及成员选择简介,我们介绍了结构体这种程序定义的类型,它可以包含成员变量。以下是一个用于存储日期的结构体示例:
struct Date
{
int year {};
int month {};
int day {};
};
现在,如果我们想将日期打印到屏幕上(这可能是我们经常需要做的事情),编写一个函数来实现这个功能是很有意义的。以下是一个完整的程序:
#include <iostream>
struct Date
{
// here are our member variables
int year {};
int month {};
int day {};
};
void print(const Date& date)
{
// member variables accessed using member selection operator (.)
std::cout << date.year << '/' << date.month << '/' << date.day;
}
int main()
{
Date today { 2020, 10, 14 }; // aggregate initialize our struct
today.day = 16; // member variables accessed using member selection operator (.)
print(today); // non-member function accessed using normal calling convention
return 0;
}
该程序输出:

属性与动作的分离
环顾四周——所见皆是物体:书籍、建筑、食物,甚至你自己。现实物体主要由两部分构成:1)若干可观察属性(如重量、颜色、尺寸、坚固度、形状等);2) 基于这些属性可执行或被执行的若干动作(如被打开、损坏其他物品等)。这些属性和动作密不可分。
在编程中,我们用变量表示属性,用函数表示动作。
在上文的Date示例中,请注意我们已将属性(Date的成员变量)与基于这些属性执行的动作(print()函数)分开定义。我们只能通过print()函数的const Date&参数来推断Date与print()之间的关联。
虽然可将Date和print()置于同一命名空间(以明确二者应打包使用),但这会增加程序中的名称数量及命名空间前缀,导致代码冗余。
若能将属性与动作作为单一包共同定义,无疑将极大提升代码优雅性。
成员函数
除了拥有成员变量外,类类型(包括结构体、类和联合体)还可以拥有自己的函数!属于类类型的函数被称为成员函数。
顺带一提…
在其他面向对象语言(如Java和C#)中,这类函数被称为方法methods。尽管C++未使用“方法”一词,但先接触过这些语言的程序员仍可能沿用该术语。
非成员函数的函数称为非成员函数non-member functions(有时也称为自由函数free functions)用于区分于成员函数。上文的print()函数即为非成员函数。
作者注:
本节将使用结构体演示成员函数示例——但所有内容同样适用于类。出于后续章节将阐明的理由,类成员函数的示例将在后续课程(14.5节——公共与私有成员及访问限定符)中展示。
成员函数必须在类类型定义内部声明,其定义可位于类类型定义内部或外部。需注意:定义本身即构成声明,因此在类内部定义成员函数即视为声明。
为保持简洁,当前我们将成员函数定义置于类类型定义内部。
相关内容:
我们在第15.2课——类与头文件中展示了如何在类类型定义之外定义成员函数。
成员函数示例
让我们重写本节开头的 Date 示例,将 print() 从非成员函数转换为成员函数:
// Member function version
#include <iostream>
struct Date
{
int year {};
int month {};
int day {};
void print() // defines a member function named print
{
std::cout << year << '/' << month << '/' << day;
}
};
int main()
{
Date today { 2020, 10, 14 }; // aggregate initialize our struct
today.day = 16; // member variables accessed using member selection operator (.)
today.print(); // member functions also accessed using member selection operator (.)
return 0;
}
该程序编译后产生的结果与上述相同:

非成员示例与成员示例之间存在三个关键差异:
-
声明(并定义)print()函数的位置
-
调用print()函数的方式
-
在print()函数内部访问成员的方式
让我们依次探讨这些差异。
成员函数在类类型定义中声明
在非成员示例中, print() 非成员函数是在 Date 结构之外的全局命名空间中定义的。默认情况下,它具有外部链接,因此可以从其他源文件调用它(使用适当的前向声明)。
在成员示例中,print() 成员函数是在 Date 结构定义内声明(在本例中是定义)的。因为 print() 被声明为 Date 的一部分,所以这告诉编译器 print() 是一个成员函数。
类类型定义内部定义的成员函数是隐式内联的,因此如果类类型定义包含在多个代码文件中,它们不会导致违反单一定义规则。
相关内容:
成员函数也可以在类定义内部(前向)声明,并在类定义之后定义。我们在第 15.2 课——类和头文件中介绍了这一点。
调用成员函数(和隐式对象)
在非成员示例中,我们调用 print(today),其中today(显式)作为参数传递。
在成员示例中,我们调用today.print()。这种语法使用成员选择运算符 (.) 来选择要调用的成员函数,这与我们访问成员变量的方式一致(例如 today.day = 16;)。
所有(非静态)成员函数都必须使用该类类型的对象来调用。在本例中,today 是调用 print() 的对象。
请注意,在成员函数的情况下,我们不需要将today作为参数传递。调用成员函数的对象被隐式传递给成员函数。因此,调用成员函数的对象通常称为隐式对象。
换句话说,当我们调用today.print()时,today是隐式对象,并且它被隐式传递给print()成员函数。
相关内容:
我们将介绍了关联对象如何实际传递给成员函数的机制,在第 15.1 课——隐藏的“this”指针和成员函数链接。
在成员函数内部访问成员时使用隐式对象
以下是 print() 的非成员版本:
// non-member version of print
void print(const Date& date)
{
// member variables accessed using member selection operator (.)
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() // defines a member function named 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::cout << name << " kisses " << person.name << '\n';
}
};
int main()
{
Person joe{ "Joe", 29 };
Person kate{ "Kate", 27 };
joe.kisses(kate);
return 0;
}
这将产生以下输出:

让我们来分析其工作原理。首先,我们定义两个Person结构体joe和kate。接着调用joe.kisses(kate)。此时joe是隐式对象,而kate作为显式参数传递。
当kisses()成员函数执行时,名称name未使用成员选择运算符(.),因此指向隐式对象joe。故解析为joe.name。而person.name使用了成员选择运算符,故不指向隐式对象。由于person是kate的引用,此处解析为kate.name。
关键要点:
若无成员函数,我们需编写 kisses(joe, kate)。而使用成员函数时,只需写 joe.kisses(kate)。请注意后者不仅可读性更佳,更能清晰区分哪个对象是动作发起者,哪个是辅助对象。
成员变量和函数可以任意顺序定义
C++编译器通常从上到下编译代码。遇到每个名称时,编译器会判断是否已声明该名称,以便进行正确的类型检查。
非成员函数必须在使用前声明,否则编译器会报错:
int x()
{
return y(); // error: y not declared yet, so compiler doesn't know what it is
}
int y()
{
return 5;
}
为解决此问题,我们通常要么按大致使用顺序定义非成员变量(每次更改顺序都需要额外工作),要么使用前向声明(添加时需要额外工作)。
然而在类定义内部,此限制不适用:你可以在成员变量和成员函数声明之前访问它们。这意味着你可以按任意顺序定义成员变量和成员函数!
例如:
struct Foo
{
int z() { return m_data; } // We can access data members before they are defined
int x() { return y(); } // We can access member functions before they are defined
int m_data { y() }; // This even works in default member initializers (see warning below)
int y() { return 5; }
};
在即将进行的第14.8节课程中,我们将讨论成员定义的推荐顺序——数据隐藏(封装)的优势。
警告:
数据成员按声明顺序初始化。若某数据成员的初始化操作涉及另一个尚未声明(因此尚未初始化)的数据成员,则该初始化操作将导致未定义行为。
struct Bad
{
int m_bad1 { m_data }; // undefined behavior: m_bad1 initialized before m_data
int m_bad2 { fcn() }; // undefined behavior: m_bad2 initialized before m_data (accessed through fcn())
int m_data { 5 };
int fcn() { return m_data; }
};
因此,在默认成员初始化器中避免使用其他成员通常是个好主意。
面向高级读者:
为允许以任意顺序定义数据成员和成员函数,编译器采用了一个巧妙技巧。当编译器在类定义内部遇到成员函数定义时:
- 该成员函数会被隐式前向声明。
- 该成员函数的定义会被移至类定义结尾之后。
这样,当编译器实际编译成员函数定义时,它已经看到了完整的类定义(包含所有成员的声明!)
例如,当编译器遇到以下代码时:
struct Foo
{
int z() { return m_data; } // m_data not declared yet
int x() { return y(); } // y not declared yet
int y() { return 5; }
int m_data{};
};
它将编译出相当于以下内容:
struct Foo
{
int z(); // forward declaration of Foo::z()
int x(); // forward declaration of Foo::x()
int y(); // forward declaration of Foo::y()
int m_data{};
};
int Foo::z() { return m_data; } // m_data already declared above
int Foo::x() { return y(); } // y already declared above
int Foo::y() { return 5; }
成员函数可以重载
与非成员函数类似,只要每个成员函数都能被区分开来,成员函数也可以重载。
相关内容:
我们在第11.2课——函数重载区分中介绍了函数重载的区分方法。
以下是一个带有重载print()成员函数的Date结构体示例:
#include <iostream>
#include <string_view>
struct Date
{
int year {};
int month {};
int day {};
void print()
{
std::cout << year << '/' << month << '/' << day;
}
void print(std::string_view prefix)
{
std::cout << prefix << year << '/' << month << '/' << day;
}
};
int main()
{
Date today { 2020, 10, 14 };
today.print(); // calls Date::print()
std::cout << '\n';
today.print("The date is: "); // calls Date::print(std::string_view)
std::cout << '\n';
return 0;
}
这将输出:

结构体与成员函数
在C语言中,结构体仅包含数据成员,不支持成员函数。
在设计C++类时,Bjarne Stroustrup曾深入探讨是否应赋予结构体(继承自C语言)成员函数能力。经过审慎考量,他最终决定予以支持。
顺带一提……:
这一决策引发了关于结构体还应获得哪些新C++功能的连锁思考。Bjarne担心若仅赋予结构体有限功能子集,反而会增加语言复杂性与边界情况。为保持简洁,他最终决定采用统一规则集(即结构体能做的一切类也能做到,反之亦然),并通过约定规范结构体的实际使用方式。
在现代C++中,结构体完全可以拥有成员函数。但构造函数除外——这种特殊类型的成员函数将在后续第14.9节---构造函数导论 中详述。带构造函数的类类型不再属于聚合体,而我们希望结构体保持聚合体特性。
最佳实践:
成员函数可同时用于结构体和类。
但结构体应避免定义构造函数成员,否则将导致其失去聚合属性。
无数据成员的类类型
可以创建不包含数据成员的类类型(例如仅包含成员函数的类类型)。此类类型的对象同样可以被实例化:
#include <iostream>
struct Foo
{
void printHi() { std::cout << "Hi!\n"; }
};
int main()
{
Foo f{};
f.printHi(); // requires object to call
return 0;
}
然而,如果类类型不包含任何数据成员,那么使用类类型可能有些过度设计。在这种情况下,建议改用命名空间(包含非成员函数)。这能更清晰地向读者表明该类不管理任何数据(且调用函数时无需实例化对象)。
#include <iostream>
namespace Foo
{
void printHi() { std::cout << "Hi!\n"; }
};
int main()
{
Foo::printHi(); // no object needed
return 0;
}
** 最佳实践**:
如果类类型没有数据成员,建议使用命名空间。
测验时间
问题 #1
创建一个名为 IntPair 的结构体,用于存储两个整数。添加一个名为 print 的成员函数,用于打印这两个整数的值。
以下程序函数应能编译通过:
#include <iostream>
// Provide the definition for IntPair and the print() member function here
int main()
{
IntPair p1 {1, 2};
IntPair p2 {3, 4};
std::cout << "p1: ";
p1.print();
std::cout << "p2: ";
p2.print();
return 0;
}
并输出结果:
p1: Pair(1, 2)
p2: Pair(3, 4)
显示解决方案
#include <iostream>
struct IntPair
{
int first{};
int second{};
void print()
{
std::cout << "Pair(" << first << ", " << second << ")\n";
}
};
int main()
{
IntPair p1 {1, 2};
IntPair p2 {3, 4};
std::cout << "p1: ";
p1.print();
std::cout << "p2: ";
p2.print();
return 0;
}

问题#2
向 IntPair 添加一个名为 isEqual 的新成员函数,该函数返回一个布尔值,指示一个 IntPair 是否等于另一个。
应编译以下程序函数:
#include <iostream>
// Provide the definition for IntPair and the member functions here
int main()
{
IntPair p1 {1, 2};
IntPair p2 {3, 4};
std::cout << "p1: ";
p1.print();
std::cout << "p2: ";
p2.print();
std::cout << "p1 and p1 " << (p1.isEqual(p1) ? "are equal\n" : "are not equal\n");
std::cout << "p1 and p2 " << (p1.isEqual(p2) ? "are equal\n" : "are not equal\n");
return 0;
}
and produce the output:
p1: Pair(1, 2)
p2: Pair(3, 4)
p1 and p1 are equal
p1 and p2 are not equal
显示解决方案
#include <iostream>
struct IntPair
{
int first{};
int second{};
void print()
{
std::cout << "Pair(" << first << ", " << second << ")\n";
}
bool isEqual(IntPair a)
{
return (first == a.first) && (second == a.second);
}
};
int main()
{
IntPair p1 {1, 2};
IntPair p2 {3, 4};
std::cout << "p1: ";
p1.print();
std::cout << "p2: ";
p2.print();
std::cout << "p1 and p1 " << (p1.isEqual(p1) ? "are equal\n" : "are not equal\n");
std::cout << "p1 and p2 " << (p1.isEqual(p2) ? "are equal\n" : "are not equal\n");
return 0;
}


浙公网安备 33010602011771号