Cpp学习19 第 15 章 类的进阶

铁名_IronName Lv5

15.1 — 隐藏的“this”指针和成员函数链

在每个成员函数内部,关键字 this 是一个常量指针,它保存当前隐式对象的地址。

1
2
void print() const { std::cout << m_id; }       // implicit use of this
void print() const { std::cout << this->m_id; } // explicit use of this

 this 只是函数参数(而不是成员),所以不会增加类的实例的内存占用。

显式引用 this

如果一个成员函数的某个参数与数据成员的参数同名,可以使用 this 消除歧义:

1
2
3
4
5
6
7
8
9
struct Something
{
int data{}; // not using m_ prefix because this is a struct

void setData(int data)
{
this->data = data; // this->data is the member, data is the local parameter
}
};

有些开发者喜欢在所有类成员变量名前显式添加 this-> 以明确表明他们引用的是成员变量。我们建议您避免这样做,因为它往往会降低代码的可读性,而收益甚微。使用 m_ 前缀是区分私有成员变量和非成员(局部)变量的更简洁的方法。

返回 *this

有时让成员函数返回隐式对象作为返回值会很有用。这样做的主要目的是为了允许成员函数“链式调用”,这样就可以在单个表达式中对同一个对象调用多个成员函数!这称为函数链式调用 (或方法链式调用 )。

但是,如果我们让每个函数都通过引用返回 *this ,我们就可以将函数调用链接起来。以下是带有“可链接”函数的新版 Calc :

1
2
3
4
5
6
7
8
9
10
11
12
class Calc
{
private:
int m_value{};

public:
Calc& add(int value) { m_value += value; return *this; }
Calc& sub(int value) { m_value -= value; return *this; }
Calc& mult(int value) { m_value *= value; return *this; }

int getValue() const { return m_value; }
};

请注意, add() 、 sub() 和 mult() 现在通过引用返回 *this 。因此,这允许我们执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

int main()
{
Calc calc{};
calc.add(5).sub(3).mult(4); // method chaining

std::cout << calc.getValue() << '\n';

return 0;
}

将类重置为默认状态

构造函数仅用于初始化新对象,不应直接调用。直接调用会导致意外行为。

将类重置回默认状态的最佳方法是创建一个 reset() 成员函数,让该函数创建一个新对象(使用默认构造函数),然后将该新对象分配给当前的隐式对象,如下所示:

1
2
3
4
void reset()
{
*this = {}; // value initialize a new object and overwrite the implicit object
}

15.2 — 类和头文件

成员函数可以像非成员函数一样定义在类定义之外。唯一的区别在于,成员函数名必须以类类型名称(在本例中为 Date:: :)作为前缀,以便编译器知道我们定义的是该类类型的成员函数,而不是非成员函数。
我们将访问函数定义在了类定义内部。由于访问函数通常只有一行代码,因此在类定义内部定义这些函数可以最大限度地减少代码冗余,而将它们移到类定义外部则会导致代码量大幅增加。因此,访问函数(以及其他简单的单行函数)的定义通常都保留在类定义内部。

将类定义放在头文件中

编译器通常需要看到类(或任何程序自定义类型)的完整定义才能使用该类型。这是因为编译器需要了解成员的声明方式,以确保它们被正确使用,并且需要能够计算该类型对象的大小才能实例化它们。因此,我们的 头文件通常包含类的完整定义 ,而不仅仅是类的前向声明。

命名类头文件和代码文件

以下是我们的 Date 类,它被拆分成 .cpp 文件和 .h 文件:

Date.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef DATE_H
#define DATE_H

class Date
{
private:
int m_year{};
int m_month{};
int m_day{};

public:
Date(int year, int month, int day);

void print() const;

int getYear() const { return m_year; }
int getMonth() const { return m_month; }
int getDay() const { return m_day; }
};

#endif

Date.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "Date.h"

Date::Date(int year, int month, int day) // constructor definition
: m_year{ year }
, m_month{ month }
, m_day{ day }
{
}

void Date::print() const // print function definition
{
std::cout << "Date(" << m_year << ", " << m_month << ", " << m_day << ")\n";
};

建议将类定义放在与类同名的头文件中。简单的成员函数(例如访问函数、空构造函数等)可以定义在类定义内部。
建议将非平凡的成员函数定义在与类同名的源文件中。

内联成员函数

在类定义内部 定义的成员函数默认是内联函数。内联函数不受“每个程序只能定义一个函数”规则的限制。
在类定义 之外 定义的成员函数并非隐式内联函数(因此受“每个程序只能定义一个函数”规则的约束)。这就是为什么这类函数通常定义在代码文件中(这样它们在整个程序中就只有一个定义)。
或者,如果将类定义之外的成员函数标记为内联函数(使用 inline 关键字),则可以将其保留在头文件中。

为什么不把所有内容都放在一个头文件中呢?

如果您更改了头文件中的任何代码,则需要重新编译所有包含该头文件的文件。这可能会产生连锁反应,一个微小的更改就可能导致整个程序需要重新编译。重新编译的成本可能差异很大:小型项目可能只需一分钟或更短时间即可完成,而大型商业项目则可能需要数小时。
反之,如果您更改了 .cpp 文件中的代码,则只需要重新编译该 .cpp 文件。因此,如果可以选择,通常最好将复杂的代码放在 .cpp 文件中。

最佳实践

将成员函数的默认参数放在类定义内部。

15.3 — 嵌套类型(成员类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Fruit
{
public:
// FruitType has been moved inside the class, under the public access specifier
// We've also renamed it Type and made it an enum rather than an enum class
enum Type
{
apple,
banana,
cherry
};

private:
Type m_type {};
int m_percentageEaten { 0 };

public:
Fruit(Type type) :
m_type { type }
{
}

Type getType() { return m_type; }
int getPercentageEaten() { return m_percentageEaten; }

bool isCherry() { return m_type == cherry; } // Inside members of Fruit, we no longer need to prefix enumerators with FruitType::
};

嵌套类型 Type 已在类的顶部定义。嵌套类型名称必须先完全定义才能使用,因此通常先定义它们。
 Type 的完全限定名称是 Fruit::Type ,而 apple 枚举器的完全限定名称是 Fruit::apple 。

在类的成员内部,我们不需要使用完全限定名。例如,在成员函数 isCherry() 中,我们可以直接访问 cherry 枚举器,而无需使用 Fruit:: scope 限定符。

将枚举类型从作用域化改为非作用域化。由于类本身现在充当了作用域区域,因此使用作用域化枚举器就显得有些多余了。改为非作用域化枚举意味着我们可以像 Fruit::apple 一样访问枚举器,而无需像使用作用域化枚举器时那样使用更长的 Fruit::Type::apple 。

嵌套类型定义和类型别名

类类型还可以包含嵌套的类型定义或类型别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <string>
#include <string_view>

class Employee
{
public:
using IDType = int;

private:
std::string m_name{};
IDType m_id{};
double m_wage{};

public:
Employee(std::string_view name, IDType id, double wage)
: m_name { name }
, m_id { id }
, m_wage { wage }
{
}

const std::string& getName() { return m_name; }
IDType getId() { return m_id; } // can use unqualified name within class
};

int main()
{
Employee john { "John", 1, 45000 };
Employee::IDType id { john.getId() }; // must use fully qualified name outside class

std::cout << john.getName() << " has id: " << id << '\n';

return 0;
}

嵌套类以及对外部类成员的访问

在 C++ 中,嵌套类无法访问外部(包含)类的 this 指针,因此嵌套类无法直接访问外部类的成员。这是因为嵌套类可以独立于外部类进行实例化(在这种情况下,也就不存在外部类的成员可供访问!)。
嵌套类是外部类的成员,因此它们可以访问外部类中任何在作用域内的私有成员。
嵌套类“可以访问”外部类的所有私有成员(权限足够),但“无法直接使用”外部类的非静态成员(因为没有默认的外部类对象 this

嵌套类型和前向声明

嵌套类型可以在包含它的类中预先声明。然后,嵌套类型可以稍后定义,既可以在包含它的类中定义,也可以在类外定义。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

class outer
{
public:
class inner1; // okay: forward declaration inside the enclosing class okay
class inner1{}; // okay: definition of forward declared type inside the enclosing class
class inner2; // okay: forward declaration inside the enclosing class okay
};

class inner2 // okay: definition of forward declared type outside the enclosing class
{
};

int main()
{
return 0;
}

但是,嵌套类型不能在定义封闭类之前进行前向声明。

15.4 — 析构函数简介

类似地,类还有另一种特殊的成员函数,当非聚合类类型的对象被销毁时,该函数会自动调用。这个函数称为析构函数 Destructors 。析构函数的作用是允许类在销毁对象之前执行任何必要的清理工作。

命名规则

  1. 析构函数必须与类名相同,前面加上波浪号(~)。
  2. 析构函数不能接受参数。
  3. 析构函数没有返回类型。

一个类只能有一个析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>

class Simple
{
private:
int m_id {};

public:
Simple(int id)
: m_id { id }
{
std::cout << "Constructing Simple " << m_id << '\n';
}

~Simple() // here's our destructor
{
std::cout << "Destructing Simple " << m_id << '\n';
}

int getID() const { return m_id; }
};

int main()
{
// Allocate a Simple
Simple simple1{ 1 };
{
Simple simple2{ 2 };
} // simple2 dies here

return 0;
} // simple1 dies here

隐式析构函数

如果你的类在析构时不需要进行任何清理工作,那么完全可以不定义析构函数,让编译器为你的类生成一个隐式析构函数(函数体是空的)。

关于 std::exit() 函数的警告

它可以用来立即终止程序。当程序立即终止时,程序会直接结束。局部变量不会被销毁,因此也不会调用析构函数。在这种情况下,如果您依赖析构函数来执行必要的清理工作,请务必谨慎。

15.5 — 带有成员函数的类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <ios>       // for std::boolalpha
#include <iostream>

template <typename T>
class Pair
{
private:
T m_first{};
T m_second{};

public:
// When we define a member function inside the class definition,
// the template parameter declaration belonging to the class applies
Pair(const T& first, const T& second)
: m_first{ first }
, m_second{ second }
{
}

bool isEqual(const Pair<T>& pair);
};

// When we define a member function outside the class definition,
// we need to resupply a template parameter declaration
template <typename T>
bool Pair<T>::isEqual(const Pair<T>& pair)
{
return m_first == pair.m_first && m_second == pair.m_second;
}

int main()
{
Pair p1{ 5, 6 }; // uses CTAD to infer type Pair<int>
std::cout << std::boolalpha << "isEqual(5, 6): " << p1.isEqual( Pair{5, 6} ) << '\n';
std::cout << std::boolalpha << "isEqual(5, 7): " << p1.isEqual( Pair{5, 7} ) << '\n';

return 0;
}

注入类名

在类的作用域内,类的非限定名称称为注入类名 。在类模板中,注入类名用作完整模板化名称的简写形式。
因为 Pair 是注入的类名 Pair<T> ,所以在我们的 Pair<T> 类模板的作用域内,任何对 Pair 的使用都会被视为我们写的是 Pair<T> 。因此,尽管我们将构造函数命名为 Pair ,编译器仍然会将其视为我们写的是 Pair<T> 。(构造函数命名为 Pair 仍有效的原因)

我们也可以像这样定义 isEqual() 成员函数:

1
2
3
4
5
template <typename T>
bool Pair<T>::isEqual(const Pair& pair) // note the parameter has type Pair, not Pair<T>
{
return m_first == pair.m_first && m_second == pair.m_second;
}

因为这是 Pair<T> 的一个成员函数的定义,所以我们处于 Pair<T> 类模板的作用域内。因此,任何对 Pair 的使用都是 Pair<T> 简写!

最佳实践

任何在类定义之外定义的成员函数模板都应该在类定义下方(在同一个文件中)定义。

Q1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template <typename T, typename U, typename R>
class Triad
{
public:
Triad(const T& first, const U& second, const R& third)
: m_first{ first }, m_second{ second }, m_third{ third }
{

}
void print() const;
const T& first() const { return m_first; }
const U& second() const { return m_second; }
const R& third() const { return m_third; }
private:
T m_first{};
U m_second{};
R m_third{};
};
template <typename T, typename U, typename R>
void Triad<T, U, R>::print() const
{
std::cout << '[' << m_first << ", " << m_second << ", " << m_third << ']';
}

还没养成使用 const 和 引用 的习惯..

Q2

从 print() 函数的声明和定义中移除 const ,程序将无法编译。为什么呢?
A: t2 是一个 const 对象,因此只能对 t2 调用 const 成员函数。如果我们把 print() 函数设为非常量成员函数,那么 t2 就不能调用它了。这是因为非常量成员函数可能会修改隐式对象,从而违反 const 对象(本例中为 t2 )的常量性。

15.6 — 静态成员变量

与普通成员变量不同, 静态成员变量由类的所有对象共享。
静态成员是存在于类的作用域内的全局变量。
使用类名和作用域解析运算符(::)访问静态成员。

定义和初始化静态成员变量

由于静态成员变量本质上是全局变量,因此必须在类外部的全局作用域中显式定义(并可选择初始化)该静态成员。

当静态成员是常量整型(包括 char 和 bool )或 const 枚举时,可以在类定义内部初始化该静态成员:

1
2
3
4
5
class Whatever
{
public:
static const int s_value{ 4 }; // a static const int can be defined and initialized directly
};

将静态成员设为 inline 或 constexpr ,以便在类定义内部进行初始化。

解释:

  1. “必须在类外部显式定义”:指的是在没有 inline 的老旧模式下,为了给静态变量分配内存地址,必须去 .cpp 定义。
  2. “常量整型可以在类内部初始化”:指的是在类内部可以写一个初始值,但这只是给了编译器一个值用于编译期计算(比如数组大小),并不代表它不需要外部定义(除非满足 C++17 的 inline/constexpr 规则)。

为什么要在类中使用静态变量?一个用途是为类的每个实例分配一个唯一的 ID。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

class Something
{
private:
static inline int s_idGenerator { 1 };
int m_id {};

public:
// grab the next value from the id generator
Something() : m_id { s_idGenerator++ }
{
}

int getID() const { return m_id; }
};

int main()
{
Something first{};
Something second{};
Something third{};

std::cout << first.getID() << '\n';
std::cout << second.getID() << '\n';
std::cout << third.getID() << '\n';
return 0;
}

只有静态成员才能使用类型推导( auto 和 CTAD)。

15.7 — 静态成员函数

成员函数也可以设为静态。
由于静态成员函数不与特定对象关联,因此可以直接使用类名和作用域解析运算符来调用它们(例如 Something::getValue() )。静态成员函数没有 this 指针。
静态成员函数可以直接访问其他静态成员(变量或函数),但不能访问非静态成员。

静态成员函数也可以定义在类声明之外。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

class IDGenerator
{
private:
static inline int s_nextID { 1 };

public:
static int getNextID(); // Here's the declaration for a static function
};

// Here's the definition of the static function outside of the class. Note we don't use the static keyword here.
int IDGenerator::getNextID() { return s_nextID++; }

int main()
{
for (int count{ 0 }; count < 5; ++count)
std::cout << "The next ID is: " << IDGenerator::getNextID() << '\n';

return 0;
}

C++ 不支持静态构造函数。

如果初始化静态成员变量需要执行代码(例如循环),则有很多不同的、略显晦涩的方法可以实现。一种适用于所有变量(无论是否为静态变量)的方法是使用函数创建一个对象,填充数据,然后将其返回给调用者。返回值可以复制到正在初始化的对象中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>

struct Chars
{
char first{};
char second{};
char third{};
char fourth{};
char fifth{};
};

class MyClass
{
private:
static Chars generate()
{
Chars c{}; // create an object
c.first = 'a'; // fill it with values however you like
c.second = 'e';
c.third = 'i';
c.fourth = 'o';
c.fifth = 'u';

return c; // return the object
}

public:
static inline Chars s_mychars { generate() }; // copy the returned object into s_mychars
};

int main()
{
std::cout << MyClass::s_mychars.third; // print i

return 0;
}

15.8 — 友元非成员函数

在类体内部,可以使用友元声明 (使用 friend 关键字)来告诉编译器,某个类或函数现在是它的友元。在 C++ 中, 友元是指被授予对另一个类的私有成员和受保护成员的完全访问权限的类或函数(成员或非成员)。这样,一个类就可以选择性地授予其他类或函数对其成员的完全访问权限,而不会影响其他任何内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Accumulator
{
private:
int m_value { 0 };

public:
void add(int value) { m_value += value; }

// Here is the friend declaration that makes non-member function void print(const Accumulator& accumulator) a friend of Accumulator
friend void print(const Accumulator& accumulator);
};

void print(const Accumulator& accumulator)
{
// Because print() is a friend of Accumulator
// it can access the private members of Accumulator
std::cout << accumulator.m_value;
}

就像可以在类内部定义成员函数一样,也可以在类内部定义友元非成员函数。(?这看起来很怪)

一个函数可以同时是多个类的友元。

在实现友元函数时,尽可能优先使用公共接口而非直接访问成员。这有助于保护友元函数免受未来实现变更的影响,并减少后续需要修改和/或重新测试的代码量。

尽可能合理地选择 非友元函数。

15.9 — 友元类和友元成员函数

友元类是指可以访问另一个类的私有成员和受保护成员的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Storage
{
private:
int m_nValue {};
double m_dValue {};
public:
Storage(int nValue, double dValue)
: m_nValue { nValue }, m_dValue { dValue }
{ }

// Make the Display class a friend of Storage
friend class Display;
};

class Display
{

友关系并非双向的。Display 是 Storage 的友元,并不意味着 Storage 也是 Display 的友元。如果要让两个类互为友元,它们都必须声明对方为友元。

除了将整个类设为友元之外,你还可以将单个成员函数设为友元。这与将非成员函数设为友元类似,只是这里使用的是成员函数的名称。

要将单个成员函数设为友元,编译器必须已经看到该友元成员函数所属类的完整定义(而不仅仅是前向声明)。

更好的解决方案是将每个类定义放在单独的头文件中,成员函数定义放在相应的 .cpp 文件中。这样,所有类定义都可以在 .cpp 文件中使用,无需重新排列类或函数!

15.10 — Ref qualifiers 引用限定词

上述挑战的根源在于,我们只想用一个函数来处理两种不同的情况(一种是隐式对象为左值,另一种是隐式对象为右值)。对一种情况最优的方案,对另一种情况可能并不理想。
为了解决这类问题,C++11 引入了一个鲜为人知的特性,称为引用限定符(ref-qualifier) ,它允许我们根据隐式对象是左值还是右值来重载成员函数。利用这一特性,我们可以创建两个版本的 getName() 函数——一个用于隐式对象为左值的情况,另一个用于隐式对象为右值的情况。

1
2
const std::string& getName() const &  { return m_name; } //  & qualifier overloads function to match only lvalue implicit objects, returns by reference
std::string getName() const && { return m_name; } // && qualifier overloads function to match only rvalue implicit objects, returns by value

为什么我们不建议使用引用限定符呢?

略。

我们建议始终立即使用访问函数的结果,不要保存返回的引用以供后续使用。

Quiz

由于 MonsterGenerator 没有状态,因此它非常适合使用命名空间。
下面是我的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <iostream>
#include <string>
#include "Random.h"

class Monster
{
public:
enum Type
{
dragon,
goblin,
ogre,
orc,
skeleton,
troll,
vampire,
zombie,
maxMonsterTypes,
};
Monster(Type type, std::string_view name, std::string_view roar, int hitPoints)
: m_type{type}, m_name{name}, m_roar{roar}, m_hitPoints{hitPoints}
{

}
constexpr std::string_view getTypeString() const
{
switch (m_type)
{
case dragon: return "Dragon";
case goblin: return "Goblin";
case ogre: return "Ogre";
case orc: return "Orc";
case skeleton: return "Skeleton";
case troll: return "Troll";
case vampire: return "Vampire";
case zombie: return "Zombie";
default: return "???";
}
}
void print() const
{
if (m_hitPoints > 0)
std::cout << m_name << " the " << getTypeString() << " has " << m_hitPoints << " hit points and says " << m_roar << ".\n";
else
std::cout << m_name << " the " << getTypeString() << "is dead.\n";
}
private:
Type m_type{};
std::string m_name{ "???" };
std::string m_roar{ "???" };
int m_hitPoints{};
};

namespace MonsterGenerator
{
constexpr std::string_view getName(int number)
{
switch (number)
{
case 0: return "Alice";
case 1: return "Bob";
case 2: return "Colin";
case 3: return "Duck";
case 4: return "Elin";
case 5: return "Frank";
default: return "???";
}
}

constexpr std::string_view getRoar(int number)
{
switch (number)
{
case 0: return "*Alice";
case 1: return "*Bob";
case 2: return "*Colin";
case 3: return "*Duck";
case 4: return "*Elin";
case 5: return "*Frank";
default: return "*???";
}
}

Monster generate()
{
return Monster{ static_cast<Monster::Type>(Random::get(0,7)), getName(Random::get(0,5)), getRoar(Random::get(0,5)), Random::get(1,100) };
}

}
int main()
{
Monster m{ MonsterGenerator::generate() };
m.print();

return 0;
}
  • 标题: Cpp学习19 第 15 章 类的进阶
  • 作者: 铁名_IronName
  • 创建于 : 2026-08-12 09:20:41
  • 更新于 : 2026-08-12 12:29:01
  • 链接: https://blog.ironname.top/2026/Cpp/Cpp学习19/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论