Cpp学习27 虚函数;模板与类

铁名_IronName Lv5

指向派生对象基类的指针和引用

由于 rBase 和 pBase 是 Base 的引用和指针,它们只能看到 Base 的成员(或者 Base 继承的任何类的成员)。因此,即使 Derived::getName() 会屏蔽(隐藏)Derived 对象的 Base::getName(),Base 的指针/引用也无法看到 Derived::getName()。因此,它们会调用 Base::getName()。

为了能让 指向派生对象基类的指针和引用 调用派生类的函数,我们需要虚函数

虚函数和多态性

虚函数 virtual function是一种特殊的成员函数,当被调用时,它会解析为被引用或指向的对象的实际类型的最派生版本函数。
如果派生函数与基函数具有相同的签名(名称、参数类型以及是否为常量)和返回类型,则认为该派生函数匹配。这类函数称为重写函数 overrides 。

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

class Base
{
public:
virtual std::string_view getName() const { return "Base"; } // note addition of virtual keyword
};

class Derived: public Base
{
public:
virtual std::string_view getName() const { return "Derived"; }
};

int main()
{
Derived derived {};
Base& rBase{ derived };
std::cout << "rBase is a " << rBase.getName() << '\n';

return 0;
}

Polymorphism  多态性

 多态性指的是一个实体拥有多种形式的能力(“多态性”一词的字面意思就是“多种形式”)。
编译时多态是指由编译器解析的多态形式。这些形式包括函数重载解析和模板解析。
运行时多态是指在运行时解析的多态形式,包括虚函数解析。

如果一个函数被标记为虚函数,则派生类中所有匹配的重写函数也会被隐式地视为虚函数,即使它们没有被显式地标记为虚函数。

不要在构造函数或析构函数中调用虚函数。

创建派生类时,基类部分会先被构造。如果您从基类构造函数中调用一个虚函数,而派生类部分尚未创建,则无法调用该函数的派生版本,因为没有派生对象可供该函数使用。在 C++ 中,它会改为调用基类版本。
如果在基类析构函数中调用虚函数,它总是会解析为基类版本的函数,因为派生类的部分已经被销毁了。

override 和 final 说明符,以及协变返回类型

如果标记为 override 函数没有覆盖基类函数(或者应用于非虚函数),编译器会将该函数标记为错误。

最佳实践

在基类中,对虚函数使用 virtual 关键字。
在派生类中,对重写函数使用 override 说明符(但不要使用 virtual 关键字)。这包括虚析构函数。

有些情况下,您可能不希望用户能够重写虚函数或继承某个类。可以使用 final 限定符来告诉编译器强制执行此操作。如果用户尝试重写已指定为 final 的函数或继承已指定为 final 的类,编译器将报错。

override 和 final 的使用位置是:

1
std::string_view getName() const override final { return "B"; }

协变返回类型

有一种特殊情况,派生类的虚函数重写可以具有与基类不同的返回类型,并且仍然被视为匹配的重写。如果虚函数的返回类型是指向某个类的指针或引用,则重写函数可以返回指向派生类的指针或引用。这些被称为协变返回类型 。

我们调用 b->getThis()。变量 b 是一个指向派生对象的 Base 指针。Base::getThis() 是一个虚函数,因此它会调用 Derived::getThis()。虽然 Derived::getThis() 返回的是 Derived* 类型,但由于该函数的 Base 版本返回的是 Base* 类型,所以返回的 Derived* 会被向上转型为 Base* 类型。

虚析构函数和重写虚拟化

如果涉及到继承,你应该 始终 将析构函数声明为虚函数。

如果您希望基类拥有一个空的虚析构函数,您可以按如下方式定义析构函数:

1
virtual ~Base() = default; // generate a virtual default destructor
1
2
3
4
5
Derived derived {};
const Base& base { derived };

// Calls Base::getName() instead of the virtualized Derived::getName()
std::cout << base.Base::getName() << '\n';
  • 如果你希望你的类被继承,请确保你的析构函数是虚函数且是公共的。
  • 如果您不希望其他类继承您的类,请将其标记为 final。这将从一开始就阻止其他类继承它,而不会对类本身施加任何其他使用限制。
1
2
3
class MyClass final {
// ...
};

如果编译器知道一个类没有子类,那么当你通过基类指针调用虚函数时,编译器可以直接确定该调用最终一定指向哪个实现,从而跳过“虚函数表(vtable)”查询,直接进行静态绑定(即“去虚拟化”),提升运行效率。

早期绑定和晚期绑定

在一般编程中, 绑定是指将名称与属性关联起来的过程。 函数绑定 (或方法绑定 )是指确定哪个函数定义与函数调用关联的过程。实际调用已绑定函数的过程称为分派 。

当直接调用非成员函数或非虚成员函数时,编译器可以确定应该将哪个函数定义与该调用匹配。这有时被称为早期绑定 (或静态绑定 ),因为它可以在编译时执行。

在某些情况下,函数调用必须等到运行时才能解析。在 C++ 中,这有时被称为后期绑定 (或者在虚函数解析的情况下,称为动态分派 )。

The virtual table 虚函数表

虚函数表是一个函数查找表,用于以动态/后期绑定的方式解析函数调用。虚函数表有时也称为“vtable”、“虚函数表”、“虚方法表”或“分派表”。在 C++ 中,虚函数解析有时被称为动态分派 。

通过使用这些表格,编译器和程序能够确保函数调用解析为适当的虚函数,即使您只使用指向基类的指针或引用!

任何使用虚函数的类都有一个 *__vptr ,因此该类的每​​个对象都会大一个指针值。

纯虚函数、抽象基类和接口类

C++ 允许你创建一种特殊的虚函数,称为纯虚函数 (或抽象函数 ),它完全没有函数体!纯虚函数仅仅充当一个占位符,供派生类重新定义。

1
virtual int getValue() const = 0; // a pure virtual function

任何包含一个或多个纯虚函数的类都会变成抽象基类 ,这意味着它不能被实例化!

Interface classes  接口类

接口类是一种没有成员变量的类,其中的_所有_函数都是纯虚函数!当你想定义派生类必须实现的功能,但将派生类如何实现这些功能的具体细节完全留给派生类自己决定时,接口就非常有用。

别忘了为接口类添加虚析构函数,这样当指向该接口的指针被删除时,就会调用相应的派生析构函数。

虚拟基类

要共享基类,只需在派生类的继承列表中插入“virtual”关键字即可。这将创建一个所谓的虚拟基类 ,这意味着只有一个基对象。该基对象在继承树中的所有对象之间共享,并且只被构造一次。(解决菱形继承的问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PoweredDevice
{
};

class Scanner: virtual public PoweredDevice
{
};

class Printer: virtual public PoweredDevice
{
};

class Copier: public Scanner, public Printer
{
};

对象切片 object slicing

直接将派生对象 赋值 给基类对象,会发生什么情况呢?
派生类包含基类和派生类两部分。当我们将一个派生类对象赋值给一个基类对象时,只会复制派生类对象的基类部分,而不会复制派生类部分。基类会收到派生类对象的基类部分的副本,但不会收到派生类部分的副本。派生类部分实际上已经被“切片”掉了。因此,将派生类对象赋值给基类对象的过程称为对象切片 (简称切片)。

如果基类并非设计为由自身实例化(例如,它只是一个接口类),则可以通过使基类不可复制(通过 =delete 删除基类复制构造函数和基类赋值运算符)来避免切片。

Dynamic casting

C++ 提供了一个名为 dynamic_cast 的类型转换运算符,最常见的用途是将基类指针转换为派生类指针。这个过程称为向下转型 downcasting 。

1
2
3
Base* b{ getObject(true) };

Derived* d{ dynamic_cast<Derived*>(b) }; // use dynamic cast to convert Base pointer into Derived pointer

如果 dynamic_cast 失败,转换结果将为空指针。
 dynamic_cast 也可以用于引用。

除非需要向下转型,否则请使用 static_cast;如果需要向下转型,dynamic_cast 通常是更好的选择。

如果禁用 RTTI (运行时类型信息),dynamic_cast 函数将无法正常工作。

使用 operator<< 打印继承的类

(目的是 用基类指针指向派生类的时候,使用std::cout能打出派生类的信息)

像往常一样在基类中将 operator<< 设置为友元。但是,我们不会让 operator<< 决定打印什么内容,而是让它调用一个_可以_虚拟化的普通成员函数!这个虚函数将负责确定每个类需要打印的内容。

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
class Base
{
public:
// Here's our overloaded operator<<
friend std::ostream& operator<<(std::ostream& out, const Base& b)
{
// Delegate printing responsibility for printing to virtual member function print()
return b.print(out);
}

// We'll rely on member function print() to do the actual printing
// Because print() is a normal member function, it can be virtualized
virtual std::ostream& print(std::ostream& out) const
{
out << "Base";
return out;
}
};

// Some class or struct with an overloaded operator<<
struct Employee
{
std::string name{};
int id{};

friend std::ostream& operator<<(std::ostream& out, const Employee& e)
{
out << "Employee(" << e.name << ", " << e.id << ")";
return out;
}
};

class Derived : public Base
{
private:
Employee m_e{}; // Derived now has an Employee member

public:
Derived(const Employee& e)
: m_e{ e }
{
}

// Here's our override print() function to handle the Derived case
std::ostream& print(std::ostream& out) const override
{
out << "Derived: ";

// Print the Employee member using the stream object
out << m_e;

return out;
}
};

模板类

最简单的方法是将所有模板类代码直接放在头文件中(在本例中,将 Array.cpp 的内容放在 Array.h 中,位于类定义下方)。这样,当你使用 #include 头文件时,所有模板代码都会集中在一个地方。这种方法的优点是简单易行。缺点是,如果模板类在多个文件中使用,最终会生成许多模板类的本地实例,这可能会增加编译和链接时间(链接器应该会移除重复的定义,因此不会使可执行文件体积过大)。除非编译或链接时间过长成为问题,否则我们推荐使用这种方法。

另一种方法是使用三文件架构。模板类定义放在头文件中。模板类成员函数放在代码文件中。然后添加第三个文件,其中包含_所有_需要实例化的类:

templates.cpp:

1
2
3
4
5
6
7
8
9
10
// Ensure the full Array template definition can be seen
#include "Array.h"
#include "Array.cpp" // we're breaking best practices here, but only in this one place

// #include other .h and .cpp template definitions you need here

template class Array<int>; // Explicitly instantiate template Array<int>
template class Array<double>; // Explicitly instantiate template Array<double>

// instantiate other templates here

这种方法可能更高效(取决于编译器和链接器如何处理模板和重复定义),但需要为每个程序维护 templates.cpp 文件。

模板非类型参数

函数模板特化

显式模板特化 (通常简称为模板特化 )允许我们为特定类型或值显式定义模板的不同实现。当所有模板参数都被特化时,称为完全特化 ;当只有部分模板参数被特化时,称为部分特化 。

编译器首先必须看到主模板的声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Here's our primary template (must come first)
template <typename T>
void print(const T& t)
{
std::cout << t << '\n';
}

// A full specialization of primary template print<T> for type double
// Full specializations are not implicitly inline, so make this inline if put in header file
template<> // template parameter declaration containing no template parameters
void print<double>(const double& d) // specialized for type double
{
std::cout << std::scientific << d << '\n';
}

如果同时存在匹配的非模板函数和匹配的模板函数特化版本,则非模板函数优先。
完整的特化版本不会隐式内联,因此如果您在头文件中定义了特化版本,请确保将其 inline 以避免违反 ODR 规则。

类模板特化

和上面的差不多。

只特化成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename T>
class Storage
{
private:
T m_value {};
public:
Storage(T value)
: m_value { value }
{
}

void print()
{
std::cout << m_value << '\n';
}
};

// This is a specialized member function definition
// Explicit function specializations are not implicitly inline, so make this inline if put in header file
template<>
void Storage<double>::print()
{
std::cout << std::scientific << m_value << '\n';
}

模版部分特化

以下是一个重载打印函数的示例,该函数接受一个部分特化的静态数组:

1
2
3
4
5
6
7
// overload of print() function for partially specialized StaticArray<char, size>
template <int size> // size is still a template non-type parameter
void print(const StaticArray<char, size>& array) // we're explicitly defining type char here
{
for (int count{ 0 }; count < size; ++count)
std::cout << array[count];
}

函数不能模板部分特化。

对于成员函数:

部分特化整个类,然后在特化的类里重新实现你需要的那个成员函数

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
#include <iostream>

template <typename T, int size>
class StaticArray_Base
{
protected:
T m_array[size]{};

public:
T* getArray() { return m_array; }

const T& operator[](int index) const { return m_array[index]; }
T& operator[](int index) { return m_array[index]; }

void print() const
{
for (int i{ 0 }; i < size; ++i)
std::cout << m_array[i] << ' ';
std::cout << '\n';
}

// Don't forget a virtual destructor if you're going to use virtual function resolution
};

template <typename T, int size>
class StaticArray: public StaticArray_Base<T, size>
{
};

template <int size>
class StaticArray<double, size>: public StaticArray_Base<double, size>
{
public:

void print() const
{
for (int i{ 0 }; i < size; ++i)
std::cout << std::scientific << this->m_array[i] << ' ';
// note: The this-> prefix in the above line is needed.
// See https://stackoverflow.com/a/6592617 or https://isocpp.org/wiki/faq/templates#nondependent-name-lookup-members for more info on why.
std::cout << '\n';
}
};

int main()
{
// declare an integer array with room for 6 integers
StaticArray<int, 6> intArray{};

// Fill it up in order, then print it
for (int count{ 0 }; count < 6; ++count)
intArray[count] = count;

intArray.print();

// declare a double buffer with room for 4 doubles
StaticArray<double, 4> doubleArray{};

for (int count{ 0 }; count < 4; ++count)
doubleArray[count] = (4.0 + 0.1 * count);

doubleArray.print();

return 0;
}

指针的部分模板特化

对 Storage 类进行部分特化:

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
#include <iostream>

template <typename T>
class Storage // This is our primary template class (same as previous)
{
private:
T m_value {};
public:
Storage(T value)
: m_value { value }
{
}

void print()
{
std::cout << m_value << '\n';
}
};

template <typename T> // we still have a type template parameter
class Storage<T*> // This is partially specialized for T*
{
private:
T* m_value {};
public:
Storage(T* value)
: m_value { value }
{
}

void print();
};

template <typename T>
void Storage<T*>::print() // This is a non-specialized function of partially specialized class Storage<T*>
{
if (m_value)
std::cout << std::scientific << *m_value << '\n';
}

使用部分模板类特化来创建类的指针和非指针实现非常有用,尤其是在您希望一个类以不同的方式处理指针和非指针,但又希望这种处理方式对最终用户完全透明时。
(感觉和引擎相关一些,客户端不太会遇到)

  • 标题: Cpp学习27 虚函数;模板与类
  • 作者: 铁名_IronName
  • 创建于 : 2026-08-15 09:18:50
  • 更新于 : 2026-08-15 10:57:02
  • 链接: https://blog.ironname.top/2026/Cpp/Cpp学习27/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论