1 模板编程的基本概念
C++ 的模板编程是一种编程技术,它允许程序员编写处理不同类型数据的通用代码。通过使用模板,可以创建与特定数据类型无关的函数或类,这些函数或类在编译时可以根据需要生成特定数据类型的版本。这增加了代码的复用性、灵活性和类型安全性。
1.1 模板编程的本质
从本质上来说, C++ 的模板编程是一种编译时多态性( compile-time polymorphism )的机制。在 C++ 中,多态性通常指的是以统一的方式处理不同类型的数据或对象的能力。运行时多态性( runtime polymorphism )通常通过虚函数实现,它允许在运行时根据对象的实际类型来调用不同的函数。而编译时多态性则通过模板实现,它允许在编译时根据所使用的数据类型来生成不同的代码。
模板编程的本质可以归结为以下几点:
类型参数化
模板允许程序员定义接受类型作为参数的函数或类。这些类型参数在模板实例化时被具体的数据类型所替换,从而生成特定类型的代码。这种类型参数化使得代码能够以一种类型无关的方式编写,提高了代码的复用性和灵活性。
编译时生成
模板实例化发生在编译时,编译器根据提供的类型参数生成特定类型的函数或类的代码。这意味着在编译时就确定了函数或类的具体实现,而不是在运行时。这种编译时生成的方式使得模板编程具有很高的性能优势。
类型安全
由于模板实例化是在编译时进行的,编译器能够对类型进行严格的检查,确保类型安全。这意味着在编译时就能够发现类型错误,而不是等到运行时才出现错误。
泛型编程
模板编程是一种泛型编程的形式,它允许程序员编写与具体数据类型无关的通用代码。这种泛型编程的能力使得代码更加简洁、易读和可维护。
元编程能力
模板编程还具有元编程的能力,即利用模板在编译时进行编程。这包括模板特化、模板偏特化、递归模板等技术,它们允许程序员在编译时进行复杂的逻辑判断和代码生成。
综上所述, C++ 的模板编程的本质是一种编译时多态性的机制,它允许程序员以类型无关的方式编写通用代码,并在编译时生成特定类型的代码。这种机制提高了代码的复用性、灵活性和性能,同时保证了类型安全。通过模板编程,程序员能够编写出更加优雅、高效和可维护的代码。
1.2 模板编程的应用场景
C++ 模板编程支持使用通用代码处理多种数据类型。这种编程模式在多个应用场景中都非常有用,以下是 C++ 模板编程的一些典型应用场景:
数据类型与算法相分离的泛型编程
这是模板编程最常见的应用之一。通过将数据类型与算法分离,我们可以实现泛型编程,使代码能够处理多种数据类型而无需重复编写。例如,在STL(标准模板库)中,容器(如std::vector
、std::list
)和算法(如std::sort
、std::find
)都是通过模板实现的,这使得它们可以与任何数据类型一起使用,从而大大提高了代码的重用性。
类型适配( Traits )
类型适配是模板编程中的另一个重要概念。通过使用特性( Traits )模板,我们可以根据数据类型提供特定的类型信息或行为。这在需要根据数据类型执行不同操作时非常有用。例如,根据数据类型是否为指针或引用,可以提供不同的类型适配实现。
函数转发
函数转发是 C++11 引入的一种新特性,它允许我们编写能够转发其参数给另一个函数的模板函数。这在创建通用函数包装器或代理时非常有用。通过使用函数转发,我们可以编写一个模板函数,该函数能够将其参数以正确的方式转发给另一个函数,而无需知道目标函数的具体签名。
元编程
模板元编程是 C++ 模板编程的高级应用之一。它允许我们在编译时执行一系列的逻辑判断和计算。元编程通常用于常量计算、类型操作和策略模式等场景。例如,可以使用递归模板来实现编译时的阶乘计算,或者使用模板特化来实现不同类型的特定行为。
编译时容器与算法
通过模板元编程,我们还可以实现编译时的容器和算法。这意味着在编译阶段就可以生成针对特定数据类型的容器和算法实现,从而在运行时获得更高的性能。例如,我们可以使用模板元编程实现编译时的静态数组和排序算法。
策略模式
策略模式是一种设计模式,它允许程序在运行时根据需要选择不同的算法实现。通过模板编程,可以实现类似的功能,但在编译时进行选择。这有助于提高代码的灵活性和可扩展性。
跨平台编程
在跨平台编程中,由于不同平台可能使用不同的数据类型和大小,使用模板可以确保代码在不同平台上的兼容性和可移植性。通过编写与平台无关的模板代码,我们可以更容易地在多个平台上编译和运行程序。
综上所述, C++ 模板编程在多个应用场景中都发挥着重要作用,包括数据类型与算法相分离的泛型编程、类型适配、函数转发、元编程、编译时容器与算法、策略模式以及跨平台编程等。这些应用场景共同展示了模板编程在提高代码重用性、类型安全性、性能和灵活性方面的优势。
2 函数模板
函数模板是一种特殊的函数定义,它使用模板参数来指定函数可以处理的数据类型。模板参数在函数定义中以类型参数的形式出现,它们可以是任何有效的 C++ 数据类型,包括内置类型和用户定义的类型。
2.1 函数模板的定义和实例化
函数模板的定义包含了一个或多个类型参数,这些类型参数在函数定义中以 typename 或 class 关键字进行声明。这些类型参数在函数体内部被当作普通的数据类型来使用。
函数模板的定义
函数模板的一般定义形式如下:
cpp
template <typename T1, typename T2, ..., typename Tn>
return_type functionName(parameterList)
{
// 函数体
}
其中 T1, T2, ..., Tn 是类型参数,代表可以是任何数据类型的占位符。return_type 是函数的返回类型, functionName 是函数的名称, parameterList 是函数的参数列表。
如下是一个简单的函数模板示例,该模板定义了一个加法函数:
cpp
template <typename T>
T add(const T& a, const T& b)
{
return a+b;
}
在上面代码中, T 是一个类型参数,它可以是任何数据类型。 add 函数接受两个类型为 T 的引用参数,并返回它们相加的值。
函数模板的实例化
当函数模板被调用时,编译器会根据提供的实际参数类型来推断类型参数的具体类型,并生成一个具体的函数实例,这个过程称为函数模板的实例化。
例如,如果使用 int 类型来调用上面的 add 函数模板:
cpp
int a = 1;
int b = 2;
int sum = add(a, b); // 这里会实例化一个 int 类型的 add 函数
编译器会生成一个专门处理 int 类型的 add 函数实例:
cpp
int add(const int& a, const int& b)
{
return a+b;
}
同样地,如果使用 double 类型来调用 add 函数模板,编译器会生成一个处理 double 类型的函数实例。
显式实例化
除了隐式地通过函数调用进行实例化外,我们还可以显式地要求编译器为特定的类型生成函数模板的实例。这通常在编译时性能优化或者某些特殊场景下是有用的。
显式实例化的语法如下:
cpp
template void functionName<T>(parameterList);
对于上面的 add 函数模板,如果想要显式地实例化一个处理 int 类型的版本,可以这样做:
cpp
template int add<int>(const int&, const int&);
显式实例化通常是在头文件中完成的,以确保在多个源文件中使用时链接器可以找到正确的实例。
2.2 函数模板的自动类型推导
函数模板的自动类型推导是一种编译器特性,它允许在调用函数模板时自动确定模板参数的类型。这种自动类型推导机制极大地简化了代码,并提高了代码的可读性和可维护性。
在调用一个函数模板时,编译器会根据传递给函数的实际参数来推导模板参数的类型。这个过程通常被称为模板参数的类型推导或类型推断。
2.2.1 自动类型推导的基本使用
如下为样例代码:
cpp
#include <iostream>
template <typename T>
T add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int sum1 = add(1, 2);
double sum2 = add(1.1, 2.1);
return 0;
}
在上面代码中,add 是一个函数模板,它接受两个类型为 T 的参数,并返回它们的和。类型 T 是一个模板参数,代表可以是任何数据类型的占位符。
当调用这个函数模板时,编译器会自动推导类型 T 。在第一个调用 add(1, 2) 中,编译器能够推导出 T 的类型为 int,因为两个参数都是整数。类似地,在第二个调用 add(1.1, 2.1) 中,编译器推导出 T 的类型为 double ,因为两个参数都是浮点数。类型推导的规则通常是针对直观的数据类型,但是当涉及到引用、指针、模板中包含多个类型时,上面代码中的简单自动类型推导有可能会无法满足。此时就需要使用 C++11 的新特性来处理。
2.2.2 使用 decltype 与 auto
C++11 及其之后的版本引入了更强大的类型推导机制,即 decltype 和 auto 的联合使用,可以进一步简化代码并提高类型推导的灵活性。
如下为样例代码:
cpp
#include <iostream>
template <class A, class B>
auto add(A a, B b) -> decltype(a + b)
{
return a + b;
}
int main()
{
auto sum1 = add(1, 2);
auto sum2 = add(1.1, 2.1);
return 0;
}
在上面中,decltype(a + b) 允许编译器推导返回值的类型,而不仅仅是函数参数的类型。在这个场景下,sum1 与 sum2 的类型分别被推导为 int 与 double 。
C++14 支持的语法 decltype(auto) 更为简洁,可以将上面代码中的函数模板修改为:
cpp
template <class A, class B>
decltype(auto) add(A a, B b)
{
return a + b;
}
使用 decltype 与 auto 尤其对模板函数中引用与 const 限定符的处理更为有效,如下为样例代码:
cpp
#include <iostream>
template <typename T>
void func(T&& t)
{
decltype(t) val = t; // a 的类型与 t 相同,包括引用和 const 限定符
// ...
}
int main()
{
int a = 1;
const int& b = a;
func(a); // t 的类型为 int& , val 的类型也为 int&
func(b); // t 的类型为 const int& , val 的类型也为 const int&
return 0;
}
在这个例子中,函数模板 func 接受一个右值引用参数 t ,并使用 decltype(t) 来声明一个局部变量 val ,其类型与 t 完全相同,包括引用和 const 限定符。这允许函数模板在处理不同类型的参数时保持更高的灵活性。
2.3 函数模板的显式类型指定
函数模板的显式类型指定是指在调用函数模板时明确指定模板参数的类型。这通常在希望消除类型推导的歧义或明确指定一个类型而不是让编译器自动推导时非常有用。
使用显式类型指定的语法是在函数名后面的尖括号 < > 中直接列出模板参数类型。这允许你在调用模板函数时提供具体的类型,即使这些类型可以从传递给函数的参数中推导出来。
如下为样例代码:
cpp
#include <iostream>
#include <typeinfo>
template <typename T1, typename T2>
decltype(auto) add(T1 t1, T2 t2)
{
return t1 + t2;
}
int main()
{
auto sum1 = add(1, 2);
printf("type of sum1 is %s\n", typeid(sum1).name());
auto sum2 = add<double, double>(1, 2);
printf("type of sum2 is %s\n", typeid(sum2).name());
return 0;
}
上面代码的输出为:
type of sum1 is int
type of sum2 is double
在这个例子中,add 是一个函数模板,它接受类型为 T1 、 T2 的参数。在 main 函数中,首先以正常方式调用 add 函数,让编译器从传递给函数的参数中推导出 T 的类型,最后的返回类型为 int 。然后,显式地指定了 T 的类型,最后的返回类型为 double 。
2.4 函数模板的特化
函数模板的特化(Function Template Specialization)是C++模板编程中的一个重要概念,它支持为特定的类型或一组类型提供定制的模板实现。特化版本的函数模板会覆盖通用模板的版本,当使用特化类型调用函数模板时,将使用特化版本的实现。
函数模板的特化通常指的是完全特化(类模板同时支持完全特化与部分特化),但是可以通过为特定的类型组合创建新的函数模板或重载现有的函数来实现部分特化的效果。
完全特化是指为函数模板提供一个完全限定的类型实现。这意味着为特定的类型提供了一个独立的实现,这个实现将仅适用于该类型。完全特化的语法与通用模板的语法类似,但在模板参数列表中使用具体的类型替代类型参数。
如下为样例代码:
cpp
#include <iostream>
// 通用模板
template <typename T1, typename T2>
decltype(auto) add(T1 t1, T2 t2)
{
printf("call general template\n");
return t1 + t2;
}
// 特化模板,仅适用于 int , int 类型
template <>
decltype(auto) add<int>(int t1, int t2)
{
printf("call specialized template for int , int\n");
return t1 + t2;
}
int main()
{
// 调用特化模板
auto sum1 = add(1, 2);
// 调用通用模板
auto sum2 = add(1.2, 2.3);
return 0;
}
上面代码的输出为:
call specialized template for int , int
call general template
在这个例子中,当 add 函数以 int 类型调用时,将使用完全特化版本的实现。对于其他类型,如 double 或 std::string ,将使用通用模板的实现。
由于函数模板不支持部分特化,如果需要为一组类型提供特定的实现,通常需要通过重载函数来实现类似的效果。这些函数具有与通用模板相同的名称,但参数类型不同。
如下为样例代码:
cpp
#include <iostream>
// 通用模板
template <typename T1, typename T2>
decltype(auto) add(T1 t1, T2 t2)
{
printf("call general template\n");
return t1 + t2;
}
// 重载函数,仅适用于 int ,int 类型
decltype(auto) add(int t1, int t2)
{
printf("call overloaded function for int , int\n");
return t1 + t2;
}
// 重载函数,仅适用于 double ,int 类型
decltype(auto) add(double t1, int t2)
{
printf("call overloaded function for double , int\n");
return t1 + t2;
}
int main()
{
// 调用重载函数:int ,int
auto sum1 = add(1, 2);
// 调用重载函数:double ,int
auto sum2 = add(1.2, 2);
// 调用通用模板
auto sum3 = add(1.2, 2.3);
return 0;
}
上面代码的输出为:
call overloaded function for int , int
call overloaded function for double , int
call general template
3 类模板
类模板允许定义可以在实例化时进行指定数据类型的类。换句话说,类模板是一个参数化类型,它使用一个或多个参数来创建一系列类。
类模板的主要优势在于,它可以减少代码重复,提高编程效率。通过为一系列仅成员数据类型不同的类创建一个类模板,程序员只需提供一套程序代码,就可以生成多种具体的类,这些类可以看作是类模板的实例。
3.1 类模板的定义和实例化
类模板的定义类似于函数模板的定义。在类模板中,可以指定一个或多个类型参数,这些类型参数在实例化时将被实际的数据类型替代。
类模板的一般定义形式如下:
cpp
template <typename T> // T 是一个类型参数
class MyClass
{
public:
MyClass(T val) : m_val(val) {} // 构造函数也使用类型参数 T
void printVal()
{
std::cout << "value: " << m_val << std::endl;
}
private:
T m_val; // 成员变量使用类型参数 T
};
在这个例子中,MyClass 是一个类模板,它有一个类型参数 T 。T 是一个占位符,代表一种未指定的数据类型。在类模板的定义中,可以像使用普通数据类型一样使用 T 。
要使用类模板,需要创建一个或多个该模板的实例。实例化类模板时,需要为模板参数提供具体的数据类型。这可以通过在类模板名称后的尖括号 < > 中指定类型来完成。针对上面定义的模板类,可以做如下实例化:
cpp
int main()
{
// 实例化 MyClass 模板,T 被替换为 int
MyClass<int> obj1(1);
obj1.printVal(); // 输出: value: 1
// 实例化 MyClass 模板,T 被替换为 string
MyClass<std::string> obj2("hello");
obj1.printVal(); // 输出: value: hello
return 0;
}
在上面代码中,为 MyClass 模板提供了两种数据类型:int 和 std::string。每次提供一个新的数据类型,编译器都会创建一个新的类类型。因此,MyClass<int> 和 MyClass<std::string> 是两种不同的类类型,它们有各自的对象实例和方法。
注意事项
(1)类模板的实例化是隐式的,也就是说,当创建一个对象时,编译器会自动处理模板的实例化。
(2)模板参数 T 在类模板的实例化时被实际类型替换,这种替换是在编译时完成的,因此不会增加运行时开销。
(3)可以为类模板定义多个类型参数,例如 template <typename T1, typename T2> ,这样就可以在类中使用两种不同类型的数据。
(4)类模板的实例化会产生新的类类型,这些类型之间是相互独立的,除了它们共享相同的模板定义外。
3.2 类模板的构造函数和析构函数
在类模板中,构造函数和析构函数的定义与处理常规类的方式类似。类模板的构造函数用于初始化模板类的对象,而析构函数用于在对象生命周期结束时释放资源。
构造函数
构造函数是特殊类型的成员函数,它在创建类的新对象时自动调用。类模板的构造函数在实例化时会用实际的类型参数替换模板参数,以便正确地进行初始化。
下面是一个类模板的例子,其中包含了构造函数:
cpp
template <typename T>
class MyClass {
public:
// 构造函数
MyClass(T val) : m_val(val) {}
// 其他成员函数...
private:
T m_val;
};
在这个例子中,MyClass 的构造函数接受一个类型为 T 的参数 val ,并用它来初始化私有成员变量 m_val 。
析构函数
析构函数是当对象的生命周期结束时自动调用的特殊成员函数。在类模板中,析构函数通常用于释放对象可能拥有的任何资源,如动态分配的内存。
下面是一个类模板的例子,其中包含了析构函数:
cpp
template <typename T>
class MyClass
{
public:
// 构造函数
MyClass(T val) : m_val(new T(val)) { }
// 析构函数
~MyClass()
{
delete m_val;
}
// 其他成员函数...
private:
T* m_val;
};
在这个例子中,MyClass 的析构函数使用 delete 释放了动态分配的内存。
实例化时的构造函数和析构函数调用
当类模板被实例化并创建对象时,相应的构造函数会被调用。同样,当对象离开其作用域或被显式删除时,析构函数会被调用。针对上面定义的模板类,可以做如下实例化的调用:
cpp
int main()
{
// 实例化 MyClass 模板,T 被替换为 int
MyClass<int> obj(1); // 调用 MyClass<int> 的构造函数
// ... obj 被使用 ...
// 当 obj 离开作用域时,MyClass<int> 的析构函数会被调用
return 0;
}
在上面代码中,obj 是 MyClass 类型的一个对象。当 obj1 被创建时,MyClass 的构造函数会被调用,而当 obj 离开其作用域时,MyClass 的析构函数会被调用。
注意:类模板的构造函数和析构函数在处理资源管理和初始化/清理工作时与常规类相同,只是它们需要能够处理模板参数 T 所代表的不同数据类型。
3.3 类模板的成员变量和成员函数
在类模板中,成员变量和成员函数的概念与常规类中的相同。成员变量用于存储类的实例的状态,而成员函数则定义了可以对这些状态执行的操作。
成员变量
类模板的成员变量通常使用模板参数 T(或其他模板参数)作为它们的类型。这些变量在类的所有实例化中都是私有的、受保护的或公开的,具体取决于它们的访问修饰符。
下面是一个类模板的例子,其中包含了由不同访问修饰符修饰的成员变量:
cpp
template <typename T>
class MyClass
{
// 构造函数和其他成员函数...
// 成员变量
private:
// 私有成员变量
T m_privateVar;
protected:
// 受保护成员变量
T m_protectedVar;
public:
// 公开成员变量
T m_publicVar;
};
在上面代码中,m_privateVar、m_protectedVar 和 m_publicVar 都是使用模板参数 T 类型的成员变量。它们的访问级别分别是私有、受保护和公开。
成员函数
类模板的成员函数定义了可以在类的实例上执行的操作。这些函数可以访问类的成员变量,并且可以使用模板参数 T(或其他模板参数)来执行类型无关的操作。
如下为样例代码:
cpp
template <typename T>
class MyClass
{
public:
// 构造函数
MyClass(T val) : m_val(val) {}
// 成员函数
void setVal(T val) { m_val = val; }
T getVal() const { return m_val; }
// 其他成员函数...
private:
T m_val;
};
在上面代码中,setVal 和 getVal 都是成员函数。setVal 接受一个类型为 T 的参数,并设置 value 成员变量的值。getVal 则返回 value 的当前值,并且由于它被声明为 const,所以不能修改类的状态。
成员函数的重载
与常规类一样,也可以在类模板中重载成员函数。这意味着可以定义多个具有相同名称但参数不同的成员函数。
如下为样例代码:
cpp
template <typename T1, typename T2>
class MyClass
{
public:
// 重载的成员函数
void setVal(T1 val) { m_val1 = val; }
// 重载的成员函数
void setVal(T2 val) { m_val2 = val; }
// 其他成员函数...
private:
T1 m_val1;
T2 m_val2;
};
在上面代码中,setVal 被重载了两次:一次接受 T1 类型的参数,另一次接受 T2 类型的参数。根据传递给函数的参数类型,编译器会选择适当的函数版本进行调用。
3.4 类模板的特化和偏特化
类模板的特化和偏特化是 C++ 模板编程中的两个重要概念,它们允许为特定的类型或一组类型提供定制的模板实现。
特化
特化是指为模板提供一个完整的替代实现,该实现仅适用于一个特定的类型。当需要要改变某个类型在模板中的行为时,则可以为这个类型创建一个特化版本。特化版本会覆盖模板的通用实现。
如下为样例代码:
cpp
template <typename T>
class MyClass
{
// 通用实现
};
// 特化版本,仅适用于int类型
template <>
class MyClass<int>
{
// int类型的特化实现
};
在上面代码中,MyClass 是一个模板类,它有一个通用实现。然后为 int 类型创建了一个特化版本,该版本将替代通用实现。
偏特化
偏特化允许为模板提供一个定制的实现,该实现适用于一组特定的类型。与特化不同,偏特化不需要指定所有的模板参数。
如下为样例代码:
cpp
template <typename T1, typename T2>
class MyClass
{
// 通用实现
};
// 偏特化版本,仅适用于T1为int,T2为任意类型的情况
template <typename T2>
class MyClass<int, T2>
{
// int, T2类型的偏特化实现
};
在上面代码中,为 MyClass 创建了一个偏特化版本,该版本仅当第一个模板参数 T1 是 int 类型时适用,而第二个模板参数 T2 可以是任意类型。
注意事项
(1)特化和偏特化必须在模板定义之后声明。
(2)特化和偏特化不能与原始模板在同一个头文件中定义。
(3)偏特化不能比原始模板更加通用。例如,如果原始模板接受两个类型参数,偏特化就不能只接受一个。
(4)偏特化在编译时的优先级高于特化,也高于原始模板。
使用特化和偏特化可以显著提高模板的灵活性和效率,但也需要谨慎使用,以避免产生复杂性和维护问题。
3.5 类模板与继承
模板类的继承与常规类的继承非常相似。你可以定义一个模板类作为基类,然后创建另一个模板类或非模板类来继承它。这种继承允许派生类继承基类的成员变量和成员函数,同时还可以添加或覆盖成员。
模板类继承非模板类
首先,一个模板类可以继承一个非模板类。这种情况下,模板类将继承非模板类的所有成员。
如下为样例代码:
cpp
#include <iostream>
class MyClassWithNonTemplate
{
public:
void withoutTemplateFunc()
{
printf("function without template\n");
}
};
template <typename T>
class MyClassWithTemplateDerived : public MyClassWithNonTemplate
{
public:
void withTemplateFunc()
{
printf("function with template for type : %s\n", typeid(T).name());
}
};
int main() {
MyClassWithTemplateDerived<int> obj;
obj.withoutTemplateFunc(); // 调用继承自非模板基类的成员函数
obj.withTemplateFunc(); // 调用模板派生类的成员函数
return 0;
}
上面代码的输出为:
function without template
function with template for type : int
模板类继承模板类
更常见的是,一个模板类可以继承另一个模板类。在这种情况下,派生类模板可以添加、覆盖或使用基类模板的成员。
如下为样例代码:
cpp
#include <iostream>
template <typename T>
class MyClassBase
{
public:
void baseFunction()
{
printf("base type : %s\n", typeid(T).name());
}
};
template <typename T1, typename T2=int>
class MyClassDerived : public MyClassBase<T2>
{
public:
void derivedFunction()
{
printf("derived type : %s, base type : %s\n", typeid(T1).name(), typeid(T2).name());
}
};
int main() {
MyClassDerived<double> obj;
obj.baseFunction(); // 调用模板基类的成员函数
obj.derivedFunction(); // 调用模板派生类的成员函数
return 0;
}
上面代码的输出为:
base type : int
derived type : double, base type : int
在这个例子中,MyClassDerived 继承自 MyClassBase,并且 MyClassBase 是一个模板类。MyClassDerived 可以选择性地提供模板参数 T2,如果没有提供,则默认为 int。
注意事项
(1)模板参数传递:当模板类继承另一个模板类时,可以选择传递或省略模板参数。在上面的例子中,MyClassDerived 选择了传递一个模板参数 T1,并且为 T2 提供了一个默认值 int。
(2)访问控制:继承的规则(公有继承、保护继承、私有继承)同样适用于模板类。公有继承允许派生类访问基类的公有和保护成员;保护继承允许派生类访问基类的公有和保护成员,但将这些成员视为保护成员;私有继承允许派生类访问基类的公有和保护成员,但将这些成员视为私有成员。
(3)特化和偏特化:当涉及到模板类的继承时,特化和偏特化的规则同样适用。可以为基类模板或派生类模板提供特化或偏特化版本。
(4)虚函数和纯虚函数:如果基类模板包含虚函数或纯虚函数,派生类可以选择覆盖这些函数或提供自己的实现。这对于创建模板类和派生类的抽象基类尤其有用。