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

C++模板类的派生机制及其应用

2021-08-204.8k 阅读

C++模板类的派生机制基础

在C++中,模板类为代码的复用提供了强大的手段。而模板类的派生机制则进一步拓展了这种复用性,允许我们基于已有的模板类创建新的模板类,从而构建出层次化的模板类体系。

当我们从一个模板类派生出另一个模板类时,需要遵循一定的语法规则。例如,假设有一个基础的模板类 BaseTemplate

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

现在我们要从 BaseTemplate 派生出一个新的模板类 DerivedTemplate

template <typename T>
class DerivedTemplate : public BaseTemplate<T> {
public:
    DerivedTemplate(T value) : BaseTemplate<T>(value) {}
    void printData() {
        std::cout << "Data in DerivedTemplate: " << this->data << std::endl;
    }
};

在上述代码中,DerivedTemplate 继承自 BaseTemplate<T>,并且在其构造函数中调用了 BaseTemplate<T> 的构造函数来初始化从基类继承的成员 data。同时,DerivedTemplate 还添加了一个新的成员函数 printData 用于打印数据。

模板类派生中的类型参数传递

在模板类的派生过程中,基类的类型参数会被传递到派生类。这意味着派生类会继承基类基于特定类型参数实例化后的行为和属性。例如,如果我们实例化 DerivedTemplate<int>

int main() {
    DerivedTemplate<int> obj(10);
    obj.printData();
    return 0;
}

在这个例子中,BaseTemplate<int> 的实例化结果为 DerivedTemplate<int> 提供了基础,DerivedTemplate<int> 不仅拥有 BaseTemplate<int> 的成员 data,还可以使用自己定义的 printData 函数。这种类型参数的传递使得我们可以通过不同的类型参数来创建具有相似结构但针对不同数据类型的类层次结构。

多重继承与模板类

C++ 支持从多个模板类进行多重继承。例如,我们有两个模板类 TemplateATemplateB

template <typename T>
class TemplateA {
public:
    T a;
    TemplateA(T value) : a(value) {}
};

template <typename T>
class TemplateB {
public:
    T b;
    TemplateB(T value) : b(value) {}
};

现在我们可以创建一个多重继承自这两个模板类的 TemplateC

template <typename T>
class TemplateC : public TemplateA<T>, public TemplateB<T> {
public:
    TemplateC(T aValue, T bValue) : TemplateA<T>(aValue), TemplateB<T>(bValue) {}
    void printValues() {
        std::cout << "Value from TemplateA: " << this->a << ", Value from TemplateB: " << this->b << std::endl;
    }
};

main 函数中使用 TemplateC

int main() {
    TemplateC<int> cObj(5, 10);
    cObj.printValues();
    return 0;
}

在这个例子中,TemplateC 同时继承了 TemplateATemplateB 的成员,通过构造函数分别初始化来自两个基类的成员。多重继承模板类可以有效地整合多个模板类的功能,但也需要注意避免命名冲突和菱形继承等问题。

模板类派生的访问控制

与普通类的继承类似,模板类派生中的访问控制决定了派生类对基类成员的访问权限。在C++ 中,有三种主要的访问修饰符:publicprotectedprivate

public 继承

在前面的例子中,我们使用了 public 继承,如 class DerivedTemplate : public BaseTemplate<T>。在 public 继承中,基类的 public 成员在派生类中仍然是 public,基类的 protected 成员在派生类中仍然是 protected,而基类的 private 成员在派生类中是不可访问的。例如:

template <typename T>
class Base {
public:
    T publicData;
protected:
    T protectedData;
private:
    T privateData;
public:
    Base(T pData, T prData, T pvData) : publicData(pData), protectedData(prData), privateData(pvData) {}
};

template <typename T>
class Derived : public Base<T> {
public:
    void accessMembers() {
        std::cout << "Public data in derived: " << this->publicData << std::endl;
        std::cout << "Protected data in derived: " << this->protectedData << std::endl;
        // 以下代码会报错,因为 privateData 不可访问
        // std::cout << "Private data in derived: " << this->privateData << std::endl;
    }
};

Derived 类的 accessMembers 函数中,可以访问 publicDataprotectedData,但不能访问 privateData

protected 继承

当使用 protected 继承时,如 class Derived : protected Base<T>,基类的 publicprotected 成员在派生类中都变为 protected,而 private 成员仍然不可访问。例如:

template <typename T>
class Base {
public:
    T publicData;
protected:
    T protectedData;
private:
    T privateData;
public:
    Base(T pData, T prData, T pvData) : publicData(pData), protectedData(prData), privateData(pvData) {}
};

template <typename T>
class Derived : protected Base<T> {
public:
    void accessMembers() {
        std::cout << "Protected access to public data in derived: " << this->publicData << std::endl;
        std::cout << "Protected access to protected data in derived: " << this->protectedData << std::endl;
        // 以下代码会报错,因为 privateData 不可访问
        // std::cout << "Private data in derived: " << this->privateData << std::endl;
    }
};

template <typename T>
class FurtherDerived : public Derived<T> {
public:
    void accessMembers() {
        std::cout << "Accessing data from FurtherDerived: " << this->publicData << std::endl;
        std::cout << "Accessing data from FurtherDerived: " << this->protectedData << std::endl;
    }
};

Derived 类中,publicDataprotectedData 变为 protected 访问权限。而在 FurtherDerived 类中,由于 Derived 类使用了 protected 继承,FurtherDerived 类可以访问 publicDataprotectedData,因为它们在 Derived 类中是 protected 的。

private 继承

private 继承时,如 class Derived : private Base<T>,基类的 publicprotected 成员在派生类中都变为 privateprivate 成员同样不可访问。这意味着派生类的子类将无法访问从基类继承的任何成员(除了通过基类提供的接口)。例如:

template <typename T>
class Base {
public:
    T publicData;
protected:
    T protectedData;
private:
    T privateData;
public:
    Base(T pData, T prData, T pvData) : publicData(pData), protectedData(prData), privateData(pvData) {}
};

template <typename T>
class Derived : private Base<T> {
public:
    void accessMembers() {
        std::cout << "Private access to public data in derived: " << this->publicData << std::endl;
        std::cout << "Private access to protected data in derived: " << this->protectedData << std::endl;
        // 以下代码会报错,因为 privateData 不可访问
        // std::cout << "Private data in derived: " << this->privateData << std::endl;
    }
};

template <typename T>
class FurtherDerived : public Derived<T> {
public:
    void accessMembers() {
        // 以下代码会报错,因为 publicData 和 protectedData 在 Derived 类中是 private 的
        // std::cout << "Accessing data from FurtherDerived: " << this->publicData << std::endl;
        // std::cout << "Accessing data from FurtherDerived: " << this->protectedData << std::endl;
    }
};

FurtherDerived 类中,无法访问从 Base 类继承的 publicDataprotectedData,因为它们在 Derived 类中变为 private

模板类派生中的虚函数与多态

多态是面向对象编程的重要特性之一,在模板类派生中同样可以实现多态。通过在基类模板中定义虚函数,并在派生类模板中重写这些虚函数,我们可以实现运行时的多态行为。

虚函数的定义与重写

假设有一个基类模板 Shape,它定义了一个虚函数 draw

template <typename T>
class Shape {
public:
    virtual void draw() const = 0;
};

这里 draw 函数被声明为纯虚函数,意味着 Shape 是一个抽象类,不能被实例化。现在我们从 Shape 派生出 CircleRectangle 模板类:

template <typename T>
class Circle : public Shape<T> {
public:
    T radius;
    Circle(T r) : radius(r) {}
    void draw() const override {
        std::cout << "Drawing a circle with radius " << radius << std::endl;
    }
};

template <typename T>
class Rectangle : public Shape<T> {
public:
    T width;
    T height;
    Rectangle(T w, T h) : width(w), height(h) {}
    void draw() const override {
        std::cout << "Drawing a rectangle with width " << width << " and height " << height << std::endl;
    }
};

CircleRectangle 类中,重写了 draw 函数。注意在C++ 11及以后版本,使用 override 关键字来明确表示重写基类的虚函数,这有助于编译器检测错误。

多态的实现

我们可以通过指针或引用来实现多态。例如:

int main() {
    Shape<int>* shapes[2];
    shapes[0] = new Circle<int>(5);
    shapes[1] = new Rectangle<int>(10, 20);

    for (int i = 0; i < 2; ++i) {
        shapes[i]->draw();
    }

    for (int i = 0; i < 2; ++i) {
        delete shapes[i];
    }

    return 0;
}

在这个例子中,shapes 数组包含指向不同派生类对象的指针。通过 shapes[i]->draw() 调用 draw 函数时,会根据指针实际指向的对象类型来调用相应的 draw 函数,从而实现多态。需要注意的是,在使用完动态分配的对象后,要记得释放内存,以避免内存泄漏。

模板类派生中的模板参数特化

模板参数特化是C++模板机制的重要组成部分,在模板类派生中也有着重要的应用。模板参数特化允许我们为特定的模板参数提供专门的实现。

全特化

假设我们有一个模板类 PrintValue

template <typename T>
class PrintValue {
public:
    void print(T value) {
        std::cout << "General print: " << value << std::endl;
    }
};

现在我们对 PrintValue 进行全特化,针对 int 类型:

template <>
class PrintValue<int> {
public:
    void print(int value) {
        std::cout << "Special print for int: " << value << std::endl;
    }
};

在全特化版本中,模板参数列表为空,并且类名后的尖括号中明确指定了特化的类型。当我们使用 PrintValue<int> 时,会调用全特化版本的 print 函数。

偏特化

偏特化允许我们对模板参数的一部分进行特化。例如,有一个二维数组模板类 Array2D

template <typename T, int rows, int cols>
class Array2D {
public:
    T data[rows][cols];
    Array2D() {
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < cols; ++j) {
                data[i][j] = T();
            }
        }
    }
};

现在我们对 Array2D 进行偏特化,固定列数为10:

template <typename T, int rows>
class Array2D<T, rows, 10> {
public:
    T data[rows][10];
    Array2D() {
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < 10; ++j) {
                data[i][j] = T();
            }
        }
    }
};

在偏特化版本中,只对列数进行了特化。当我们使用 Array2D<int, 5, 10> 时,会调用偏特化版本的 Array2D

模板类派生中的特化应用

在模板类派生中,特化可以用于为特定类型的派生类提供优化的实现。例如,我们有一个基础模板类 DataProcessor

template <typename T>
class DataProcessor {
public:
    T process(T data) {
        return data;
    }
};

现在我们从 DataProcessor 派生出 IntDataProcessor,并对其进行特化:

template <>
class IntDataProcessor<int> : public DataProcessor<int> {
public:
    int process(int data) override {
        return data * 2;
    }
};

在这个例子中,IntDataProcessor<int> 针对 int 类型提供了一个优化的 process 函数实现,它将输入的整数翻倍。

模板类派生在实际项目中的应用案例

容器类的派生

在C++ 标准库中,容器类的设计就大量运用了模板类派生机制。例如,std::vector 是一个动态数组容器,而 std::stackstd::queue 可以基于 std::vector 或其他序列容器进行派生实现。

std::stack 通常是通过组合或继承的方式基于其他容器实现的。假设有一个简化版的 MyStack 基于 MyVector(一个自定义的类似 std::vector 的模板类)派生:

template <typename T>
class MyVector {
private:
    T* data;
    int size;
    int capacity;
public:
    MyVector() : size(0), capacity(10) {
        data = new T[capacity];
    }
    ~MyVector() {
        delete[] data;
    }
    void push_back(T value) {
        if (size == capacity) {
            capacity *= 2;
            T* newData = new T[capacity];
            for (int i = 0; i < size; ++i) {
                newData[i] = data[i];
            }
            delete[] data;
            data = newData;
        }
        data[size++] = value;
    }
    T pop_back() {
        if (size > 0) {
            return data[--size];
        }
        return T();
    }
    T& operator[](int index) {
        return data[index];
    }
};

template <typename T>
class MyStack : public MyVector<T> {
public:
    void push(T value) {
        this->push_back(value);
    }
    T pop() {
        return this->pop_back();
    }
    T top() {
        return (*this)[this->size - 1];
    }
};

在这个例子中,MyStack 继承自 MyVector,并利用 MyVector 的功能实现了栈的基本操作。通过这种方式,代码的复用性得到了提高,同时也减少了重复代码的编写。

图形绘制框架

在图形绘制框架中,模板类派生可以用于构建不同类型图形的层次结构。例如,我们可以有一个基础的 GraphicObject 模板类,然后派生出 CircleRectangle 等具体图形类。

template <typename T>
class GraphicObject {
public:
    virtual void draw() const = 0;
};

template <typename T>
class Circle : public GraphicObject<T> {
public:
    T radius;
    Circle(T r) : radius(r) {}
    void draw() const override {
        std::cout << "Drawing a circle with radius " << radius << std::endl;
    }
};

template <typename T>
class Rectangle : public GraphicObject<T> {
public:
    T width;
    T height;
    Rectangle(T w, T h) : width(w), height(h) {}
    void draw() const override {
        std::cout << "Drawing a rectangle with width " << width << " and height " << height << std::endl;
    }
};

然后我们可以通过模板类派生进一步扩展图形类,比如派生出 FilledCircle 类:

template <typename T>
class FilledCircle : public Circle<T> {
public:
    T fillColor;
    FilledCircle(T r, T color) : Circle<T>(r), fillColor(color) {}
    void draw() const override {
        std::cout << "Drawing a filled circle with radius " << this->radius << " and color " << fillColor << std::endl;
    }
};

通过这种层次化的模板类派生,我们可以方便地管理和扩展图形绘制的功能,不同类型的图形可以共享一些基础的属性和操作,同时又有各自独特的行为。

数据库访问层

在数据库访问层的设计中,模板类派生可以用于抽象不同数据库操作。例如,我们可以有一个基础的 DatabaseOperation 模板类,它定义了一些基本的数据库操作接口,如连接数据库、执行查询等。

template <typename ConnectionType, typename QueryType>
class DatabaseOperation {
public:
    virtual void connect() = 0;
    virtual QueryType executeQuery(const std::string& query) = 0;
};

然后我们可以派生出针对不同数据库的具体操作类,如 MySqlOperationOracleOperation

template <typename QueryType>
class MySqlOperation : public DatabaseOperation<MYSQL*, QueryType> {
public:
    void connect() override {
        // 实现连接 MySQL 数据库的代码
    }
    QueryType executeQuery(const std::string& query) override {
        // 实现执行 MySQL 查询的代码
    }
};

template <typename QueryType>
class OracleOperation : public DatabaseOperation<OCIEnv*, QueryType> {
public:
    void connect() override {
        // 实现连接 Oracle 数据库的代码
    }
    QueryType executeQuery(const std::string& query) override {
        // 实现执行 Oracle 查询的代码
    }
};

通过这种方式,我们可以利用模板类派生机制来创建一个统一的数据库访问接口,同时针对不同的数据库类型提供具体的实现,使得代码具有更好的可维护性和扩展性。

模板类派生中的常见问题与解决方案

命名冲突

在模板类派生中,命名冲突是一个常见的问题。当多个模板类或派生类中有相同名称的成员时,可能会导致编译错误。例如:

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

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

在这个例子中,BaseDerived 类都有一个名为 print 的函数。如果在 Derived 类中调用 print,编译器会优先选择 Derived 类的 print 函数。如果要调用 Base 类的 print 函数,可以使用作用域解析运算符 Base<T>::print()

菱形继承问题

菱形继承是多重继承中可能出现的问题,在模板类派生的多重继承中同样可能发生。例如:

template <typename T>
class A {
public:
    T data;
    A(T value) : data(value) {}
};

template <typename T>
class B : public A<T> {
public:
    B(T value) : A<T>(value) {}
};

template <typename T>
class C : public A<T> {
public:
    C(T value) : A<T>(value) {}
};

template <typename T>
class D : public B<T>, public C<T> {
public:
    D(T value) : B<T>(value), C<T>(value) {}
    void accessData() {
        // 以下代码会报错,因为 data 有歧义
        // std::cout << data << std::endl;
    }
};

在这个例子中,D 类从 BC 类多重继承,而 BC 又都继承自 A 类,导致 D 类中 data 成员有歧义。为了解决这个问题,可以使用虚拟继承:

template <typename T>
class A {
public:
    T data;
    A(T value) : data(value) {}
};

template <typename T>
class B : virtual public A<T> {
public:
    B(T value) : A<T>(value) {}
};

template <typename T>
class C : virtual public A<T> {
public:
    C(T value) : A<T>(value) {}
};

template <typename T>
class D : public B<T>, public C<T> {
public:
    D(T value) : B<T>(value), C<T>(value) {}
    void accessData() {
        std::cout << data << std::endl;
    }
};

通过虚拟继承,BC 类共享 A 类的同一个实例,从而避免了数据成员的歧义。

模板实例化问题

在模板类派生中,模板的实例化也可能出现问题。例如,如果一个模板类的派生依赖于一些复杂的模板参数推导,可能会导致编译器无法正确实例化模板。为了解决这个问题,可以明确指定模板参数,或者使用模板别名来简化模板参数的书写。例如:

template <typename T1, typename T2>
class ComplexTemplate {
public:
    T1 data1;
    T2 data2;
    ComplexTemplate(T1 d1, T2 d2) : data1(d1), data2(d2) {}
};

template <typename T>
using SimplifiedTemplate = ComplexTemplate<T, int>;

template <typename T>
class DerivedFromComplex : public SimplifiedTemplate<T> {
public:
    DerivedFromComplex(T d1, int d2) : SimplifiedTemplate<T>(d1, d2) {}
};

在这个例子中,通过使用模板别名 SimplifiedTemplate,简化了 DerivedFromComplex 类对 ComplexTemplate 的继承,使得模板参数的使用更加清晰,减少了实例化错误的可能性。

通过对模板类派生机制及其应用的深入了解,我们可以更好地利用C++的模板特性,编写出更高效、可复用且易于维护的代码。无论是在大型项目开发还是小型程序设计中,模板类派生都为我们提供了强大的工具来构建灵活的软件架构。