MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++模板类派生新模板类的技巧

2022-10-066.8k 阅读

C++ 模板类派生新模板类的基础概念

在 C++ 编程中,模板类提供了一种强大的机制,允许我们编写通用的代码,以适应不同的数据类型。当从一个模板类派生出新的模板类时,我们可以继承基模板类的特性,并在此基础上添加新的功能或修改现有功能。

模板类的基本定义

首先回顾一下模板类的基本定义。模板类使用 template 关键字来定义一个类型参数化的类。例如,下面是一个简单的模板类 MyClass

template <typename T>
class MyClass {
private:
    T data;
public:
    MyClass(T value) : data(value) {}
    T getData() const {
        return data;
    }
};

在上述代码中,typename T 定义了一个类型参数 T,它可以在类的定义中被替换为任何实际的类型。

从模板类派生新模板类的语法

从模板类派生新模板类的语法与普通类的继承类似,但需要注意模板参数的传递。例如,假设我们要从 MyClass 派生出一个新的模板类 MyDerivedClass

template <typename T>
class MyDerivedClass : public MyClass<T> {
public:
    MyDerivedClass(T value) : MyClass<T>(value) {}
    T doubleData() const {
        return MyClass<T>::getData() * 2;
    }
};

在上述代码中,MyDerivedClass 继承自 MyClass<T>,并且通过构造函数初始化基类。同时,它还添加了一个新的成员函数 doubleData,该函数返回基类中数据的两倍。

模板类派生中的模板参数处理

继承基类的模板参数

在从模板类派生新模板类时,新类通常会继承基类的模板参数。这意味着我们可以在派生类中直接使用基类的模板参数。例如:

template <typename T>
class Base {
public:
    T value;
    Base(T v) : value(v) {}
};

template <typename T>
class Derived : public Base<T> {
public:
    Derived(T v) : Base<T>(v) {}
    T getDoubleValue() {
        return this->value * 2;
    }
};

在这个例子中,Derived 类继承了 Base 类的模板参数 T,并使用它来定义自己的成员函数 getDoubleValue

引入新的模板参数

除了继承基类的模板参数,派生类还可以引入新的模板参数。这为派生类提供了更多的灵活性。例如:

template <typename T>
class BaseWithOneParam {
public:
    T data;
    BaseWithOneParam(T d) : data(d) {}
};

template <typename T, typename U>
class DerivedWithTwoParams : public BaseWithOneParam<T> {
private:
    U additionalData;
public:
    DerivedWithTwoParams(T t, U u) : BaseWithOneParam<T>(t), additionalData(u) {}
    U getAdditionalData() const {
        return additionalData;
    }
};

在上述代码中,DerivedWithTwoParams 类继承自 BaseWithOneParam<T>,并引入了一个新的模板参数 U。新参数 U 用于定义 additionalData 成员变量和 getAdditionalData 成员函数。

模板类派生中的成员访问控制

公有继承

当使用公有继承从模板类派生新模板类时,基类的公有成员在派生类中仍然是公有的,保护成员在派生类中仍然是保护的。例如:

template <typename T>
class PublicBase {
public:
    T publicData;
protected:
    T protectedData;
private:
    T privateData;
public:
    PublicBase(T p, T pr, T prvt) : publicData(p), protectedData(pr), privateData(prvt) {}
};

template <typename T>
class PublicDerived : public PublicBase<T> {
public:
    T getProtectedData() const {
        return this->protectedData;
    }
};

在上述代码中,PublicDerived 类通过公有继承从 PublicBase 类派生。PublicDerived 类可以访问 PublicBase 类的保护成员 protectedData,但不能访问私有成员 privateData

保护继承

使用保护继承时,基类的公有成员和保护成员在派生类中都变为保护成员。例如:

template <typename T>
class ProtectedBase {
public:
    T publicData;
protected:
    T protectedData;
private:
    T privateData;
public:
    ProtectedBase(T p, T pr, T prvt) : publicData(p), protectedData(pr), privateData(prvt) {}
};

template <typename T>
class ProtectedDerived : protected ProtectedBase<T> {
public:
    T getProtectedData() const {
        return this->protectedData;
    }
};

在这个例子中,ProtectedDerived 类通过保护继承从 ProtectedBase 类派生。ProtectedDerived 类可以访问 ProtectedBase 类的公有和保护成员,但这些成员在 ProtectedDerived 类的外部(除了派生类)是不可访问的。

私有继承

私有继承会使基类的公有成员和保护成员在派生类中都变为私有成员。例如:

template <typename T>
class PrivateBase {
public:
    T publicData;
protected:
    T protectedData;
private:
    T privateData;
public:
    PrivateBase(T p, T pr, T prvt) : publicData(p), protectedData(pr), privateData(prvt) {}
};

template <typename T>
class PrivateDerived : private PrivateBase<T> {
public:
    T getProtectedData() const {
        return this->protectedData;
    }
};

在上述代码中,PrivateDerived 类通过私有继承从 PrivateBase 类派生。PrivateDerived 类可以访问 PrivateBase 类的公有和保护成员,但这些成员在 PrivateDerived 类的外部(包括进一步的派生类)是不可访问的。

模板类派生中的特殊情况处理

模板类与非模板类的继承关系

C++ 允许从模板类派生出非模板类,也允许从非模板类派生出模板类。

从模板类派生出非模板类的例子:

template <typename T>
class TemplateBase {
public:
    T data;
    TemplateBase(T d) : data(d) {}
};

class NonTemplateDerived : public TemplateBase<int> {
public:
    NonTemplateDerived(int d) : TemplateBase<int>(d) {}
    int getDataPlusOne() {
        return this->data + 1;
    }
};

在上述代码中,NonTemplateDerived 类从 TemplateBase<int> 派生,它将模板类实例化为特定类型 int,并添加了自己的成员函数 getDataPlusOne

从非模板类派生出模板类的例子:

class NonTemplateBase {
public:
    int baseData;
    NonTemplateBase(int d) : baseData(d) {}
};

template <typename T>
class TemplateDerived : public NonTemplateBase {
private:
    T additionalData;
public:
    TemplateDerived(int base, T add) : NonTemplateBase(base), additionalData(add) {}
    T getAdditionalData() const {
        return additionalData;
    }
};

在这个例子中,TemplateDerived 类从 NonTemplateBase 派生,并引入了自己的模板参数 T 来定义 additionalData 成员变量和 getAdditionalData 成员函数。

多重继承与模板类

C++ 支持模板类的多重继承。例如,假设我们有两个模板类 Base1Base2,我们可以从它们派生出一个新的模板类 DerivedMulti

template <typename T>
class Base1 {
public:
    T data1;
    Base1(T d1) : data1(d1) {}
};

template <typename U>
class Base2 {
public:
    U data2;
    Base2(U d2) : data2(d2) {}
};

template <typename T, typename U>
class DerivedMulti : public Base1<T>, public Base2<U> {
public:
    DerivedMulti(T d1, U d2) : Base1<T>(d1), Base2<U>(d2) {}
    T getBase1Data() const {
        return this->data1;
    }
    U getBase2Data() const {
        return this->data2;
    }
};

在上述代码中,DerivedMulti 类从 Base1<T>Base2<U> 多重继承,它可以访问两个基类的成员变量,并提供了相应的访问函数。

模板类派生中的编译与链接问题

模板类的实例化

当我们使用模板类时,编译器会根据实际使用的类型参数实例化模板类。在模板类派生的情况下,编译器同样会实例化基类和派生类。例如:

template <typename T>
class BaseTemplate {
public:
    T value;
    BaseTemplate(T v) : value(v) {}
};

template <typename T>
class DerivedTemplate : public BaseTemplate<T> {
public:
    DerivedTemplate(T v) : BaseTemplate<T>(v) {}
    T getDoubleValue() {
        return this->value * 2;
    }
};

int main() {
    DerivedTemplate<int> obj(5);
    int result = obj.getDoubleValue();
    return 0;
}

在上述代码中,当 DerivedTemplate<int> 被实例化时,BaseTemplate<int> 也会被实例化。编译器会为 DerivedTemplate<int>BaseTemplate<int> 生成相应的代码。

模板类定义与声明的分离

在实际项目中,我们通常会将类的声明和定义分离到不同的文件中。对于模板类,这种分离需要特别注意。因为模板类的实例化是在使用时进行的,所以模板类的定义必须在使用它的地方可见。

一种常见的做法是将模板类的声明放在头文件(.h.hpp)中,将定义放在包含头文件的源文件(.cpp)中。例如: // BaseTemplate.hpp

template <typename T>
class BaseTemplate {
public:
    T value;
    BaseTemplate(T v);
};

// BaseTemplate.cpp

#include "BaseTemplate.hpp"

template <typename T>
BaseTemplate<T>::BaseTemplate(T v) : value(v) {}

// DerivedTemplate.hpp

#include "BaseTemplate.hpp"

template <typename T>
class DerivedTemplate : public BaseTemplate<T> {
public:
    DerivedTemplate(T v);
    T getDoubleValue();
};

// DerivedTemplate.cpp

#include "DerivedTemplate.hpp"

template <typename T>
DerivedTemplate<T>::DerivedTemplate(T v) : BaseTemplate<T>(v) {}

template <typename T>
T DerivedTemplate<T>::getDoubleValue() {
    return this->value * 2;
}

// main.cpp

#include "DerivedTemplate.hpp"

int main() {
    DerivedTemplate<int> obj(5);
    int result = obj.getDoubleValue();
    return 0;
}

在上述代码中,虽然模板类的声明和定义分离了,但通过在头文件中包含必要的定义,确保了编译器在实例化模板类时能够找到完整的定义。

模板类派生的应用场景

代码复用与扩展

模板类派生最常见的应用场景是代码复用和扩展。通过从现有模板类派生出新模板类,我们可以复用基类的代码,并根据具体需求添加新的功能。例如,在一个图形库中,可能有一个基模板类 Shape 定义了通用的图形属性和操作,然后从 Shape 派生出 CircleRectangle 等具体的图形模板类,每个派生类可以添加自己特有的属性和操作。

template <typename T>
class Shape {
public:
    T x;
    T y;
    Shape(T a, T b) : x(a), y(b) {}
};

template <typename T>
class Circle : public Shape<T> {
private:
    T radius;
public:
    Circle(T a, T b, T r) : Shape<T>(a, b), radius(r) {}
    T getArea() {
        return 3.14 * radius * radius;
    }
};

在上述代码中,Circle 类从 Shape 类派生,复用了 Shape 类的位置属性 xy,并添加了自己的半径属性 radius 和计算面积的函数 getArea

实现通用算法与数据结构

模板类派生在实现通用算法和数据结构时也非常有用。例如,我们可以从一个通用的链表模板类派生出栈和队列的模板类。

template <typename T>
class Node {
public:
    T data;
    Node* next;
    Node(T d) : data(d), next(nullptr) {}
};

template <typename T>
class LinkedList {
protected:
    Node<T>* head;
public:
    LinkedList() : head(nullptr) {}
    void add(T data) {
        Node<T>* newNode = new Node<T>(data);
        newNode->next = head;
        head = newNode;
    }
};

template <typename T>
class Stack : public LinkedList<T> {
public:
    void push(T data) {
        this->add(data);
    }
    T pop() {
        if (this->head == nullptr) {
            throw std::runtime_error("Stack is empty");
        }
        T data = this->head->data;
        Node<T>* temp = this->head;
        this->head = this->head->next;
        delete temp;
        return data;
    }
};

在上述代码中,Stack 类从 LinkedList 类派生,复用了 LinkedList 类的添加节点的功能,并在此基础上实现了栈的 pushpop 操作。

实现类型安全的接口

模板类派生可以用于实现类型安全的接口。例如,假设我们有一个通用的数据库访问模板类,我们可以从它派生出针对不同数据库类型(如 MySQL、Oracle 等)的模板类,每个派生类可以提供类型安全的特定数据库操作。

template <typename T>
class Database {
public:
    virtual void connect() = 0;
    virtual void query(T query) = 0;
    virtual ~Database() {}
};

template <typename T>
class MySQLDatabase : public Database<T> {
public:
    void connect() override {
        std::cout << "Connecting to MySQL database" << std::endl;
    }
    void query(T query) override {
        std::cout << "Executing MySQL query: " << query << std::endl;
    }
};

在上述代码中,MySQLDatabase 类从 Database 类派生,并实现了 connectquery 虚函数,提供了针对 MySQL 数据库的类型安全的操作。

模板类派生中的注意事项

避免模板参数冲突

在模板类派生中,要注意避免模板参数冲突。当派生类引入新的模板参数时,确保这些参数不会与基类的参数或其他作用域中的参数冲突。例如:

template <typename T>
class BaseWithParam {
public:
    T value;
    BaseWithParam(T v) : value(v) {}
};

// 错误示例,新参数 T 与基类参数冲突
// template <typename T>
// class DerivedWithConflict : public BaseWithParam<T> {
// public:
//     T newData;
//     DerivedWithConflict(T v, T newV) : BaseWithParam<T>(v), newData(newV) {}
// };

// 正确示例,使用不同的参数名
template <typename U>
class DerivedWithoutConflict : public BaseWithParam<int> {
public:
    U newData;
    DerivedWithoutConflict(int v, U newV) : BaseWithParam<int>(v), newData(newV) {}
};

在上述代码中,第一个 DerivedWithConflict 类的定义会导致模板参数冲突,而 DerivedWithoutConflict 类通过使用不同的参数名 U 避免了冲突。

理解模板实例化顺序

在模板类派生中,要理解模板实例化的顺序。首先,基类会被实例化,然后派生类才会被实例化。这意味着派生类依赖的基类的成员必须在实例化派生类之前已经定义。例如:

template <typename T>
class BaseForInstance {
public:
    T data;
    BaseForInstance(T d) : data(d) {}
    void printData();
};

template <typename T>
class DerivedForInstance : public BaseForInstance<T> {
public:
    DerivedForInstance(T d) : BaseForInstance<T>(d) {}
    void printDataAndDouble() {
        this->printData();
        std::cout << " Double: " << this->data * 2 << std::endl;
    }
};

// 错误示例,BaseForInstance 的 printData 定义在 DerivedForInstance 之后
// template <typename T>
// void BaseForInstance<T>::printData() {
//     std::cout << "Data: " << this->data << std::endl;
// }

// 正确示例,BaseForInstance 的 printData 定义在 DerivedForInstance 之前
template <typename T>
void BaseForInstance<T>::printData() {
    std::cout << "Data: " << this->data << std::endl;
}

template <typename T>
class DerivedForInstance : public BaseForInstance<T> {
public:
    DerivedForInstance(T d) : BaseForInstance<T>(d) {}
    void printDataAndDouble() {
        this->printData();
        std::cout << " Double: " << this->data * 2 << std::endl;
    }
};

在上述代码中,如果 BaseForInstanceprintData 函数定义在 DerivedForInstance 之后,编译器在实例化 DerivedForInstance 时会找不到 printData 函数的定义,从而导致错误。

处理模板类中的虚函数

当模板类中包含虚函数时,在派生类中重写这些虚函数需要特别注意。确保派生类中的虚函数声明与基类中的虚函数声明完全一致,包括参数类型和 const 修饰符等。例如:

template <typename T>
class BaseWithVirtual {
public:
    virtual void print(T data) const {
        std::cout << "Base: " << data << std::endl;
    }
};

template <typename T>
class DerivedWithVirtual : public BaseWithVirtual<T> {
public:
    // 正确示例,函数声明与基类完全一致
    void print(T data) const override {
        std::cout << "Derived: " << data << std::endl;
    }
};

在上述代码中,DerivedWithVirtual 类重写了 BaseWithVirtual 类的 print 虚函数,并且使用了 override 关键字来确保函数声明的一致性。如果函数声明不一致,编译器会将其视为新的函数,而不是重写基类的虚函数。

通过深入理解和掌握 C++ 模板类派生新模板类的技巧,开发者可以编写出更加通用、灵活和高效的代码,充分发挥 C++ 模板机制的强大功能。无论是在实现通用数据结构、算法,还是在构建类型安全的接口等方面,模板类派生都为我们提供了有力的工具。在实际编程中,需要注意模板参数的处理、成员访问控制、编译链接问题以及各种特殊情况,以确保代码的正确性和可读性。