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

C++模板类派生新类的不同形式

2024-05-293.7k 阅读

C++模板类派生新类的不同形式

在C++编程中,模板类为我们提供了一种强大的代码复用机制。当基于模板类派生新类时,存在多种不同的形式,每种形式都有其独特的特性和应用场景。深入理解这些形式对于编写高效、灵活且可维护的代码至关重要。

从非模板类派生模板类

这种形式是指以一个普通的非模板类作为基类,派生出一个模板类。这种方式在很多场景下都非常有用,比如当我们有一个基础的、已经成熟且稳定的非模板类,而我们希望在此基础上通过模板参数化某些特性,以适应不同的数据类型或其他变化的情况。

// 非模板基类
class Base {
public:
    void baseFunction() {
        std::cout << "This is a base function from Base class." << std::endl;
    }
};

// 从非模板类Base派生的模板类
template <typename T>
class Derived : public Base {
private:
    T data;
public:
    Derived(T value) : data(value) {}
    void printData() {
        std::cout << "Data: " << data << std::endl;
    }
};

int main() {
    Derived<int> derivedInt(10);
    derivedInt.baseFunction();
    derivedInt.printData();

    Derived<double> derivedDouble(3.14);
    derivedDouble.baseFunction();
    derivedDouble.printData();

    return 0;
}

在上述代码中,Base 是一个普通的非模板类,它有一个 baseFunction 方法。Derived 是一个模板类,它从 Base 派生而来。Derived 类模板有一个模板参数 T,用于表示存储的数据类型。通过这种方式,我们既可以复用 Base 类的功能,又能根据不同的数据类型来定制 Derived 类的行为。

从非模板类派生模板类的优点在于可以充分利用已有的非模板类的成熟代码和功能,同时通过模板参数化扩展其适用性。在大型项目中,这有助于在保持代码稳定性的同时,提高代码的复用性和灵活性。例如,在一个图形绘制库中,可能有一个非模板的 Shape 基类,包含了一些通用的绘制属性和方法。然后可以派生出模板类 Circle<T>Rectangle<T>,其中 T 可以表示坐标的数据类型,如 intfloat,这样就可以根据不同的需求使用不同的数据类型来绘制图形。

从模板类派生非模板类

与上述情况相反,我们也可以从一个模板类派生出一个非模板类。这种方式适用于当我们希望对模板类进行特定化,固定某些模板参数,得到一个具体的、不再具有模板参数灵活性的类。

// 模板基类
template <typename T>
class TemplateBase {
private:
    T value;
public:
    TemplateBase(T v) : value(v) {}
    T getValue() {
        return value;
    }
};

// 从模板类TemplateBase派生的非模板类
class NonTemplateDerived : public TemplateBase<int> {
public:
    NonTemplateDerived(int v) : TemplateBase<int>(v) {}
    void printValue() {
        std::cout << "Value in NonTemplateDerived: " << getValue() << std::endl;
    }
};

int main() {
    NonTemplateDerived derived(20);
    derived.printValue();

    return 0;
}

在这段代码中,TemplateBase 是一个模板类,它有一个模板参数 T 用于表示存储的值的类型。NonTemplateDerived 是一个非模板类,它从 TemplateBase<int> 派生而来,也就是说,它固定了模板参数 Tint 类型。这样,NonTemplateDerived 类就具有了 TemplateBase<int> 的所有功能,并且可以添加自己特定的方法,如 printValue

从模板类派生非模板类的好处在于可以针对特定的模板参数值创建一个专门的类,这个类的行为更加具体和可预测。在一些性能敏感的场景中,这种特定化可以避免模板实例化带来的一些开销。例如,在一个数值计算库中,可能有一个通用的模板类 Matrix<T> 用于表示矩阵。对于一些经常使用的特定数据类型,如 float,可以派生出非模板类 FloatMatrix,对其进行针对性的优化,如特定的内存对齐方式或更高效的计算算法,以提高性能。

从模板类派生模板类

这是一种更为复杂但也更具灵活性的派生方式。当从一个模板类派生出另一个模板类时,新的模板类可以继承基类的模板参数,也可以定义自己新的模板参数,或者两者兼而有之。

继承基类模板参数

// 模板基类
template <typename T>
class BaseTemplate {
protected:
    T data;
public:
    BaseTemplate(T value) : data(value) {}
    T getData() {
        return data;
    }
};

// 派生模板类,继承基类模板参数
template <typename T>
class DerivedTemplate1 : public BaseTemplate<T> {
public:
    DerivedTemplate1(T value) : BaseTemplate<T>(value) {}
    void printData() {
        std::cout << "Data in DerivedTemplate1: " << this->getData() << std::endl;
    }
};

int main() {
    DerivedTemplate1<int> derived1(30);
    derived1.printData();

    return 0;
}

在上述代码中,BaseTemplate 是一个模板类,有模板参数 TDerivedTemplate1 也是一个模板类,它继承了 BaseTemplate 的模板参数 TDerivedTemplate1 可以直接使用 BaseTemplate 中基于 T 的功能,并在此基础上添加自己的方法,如 printData。这种方式适用于当派生类需要与基类保持相同的数据类型灵活性,同时又要添加一些额外的行为。

定义新的模板参数

// 模板基类
template <typename T>
class BaseTemplate2 {
protected:
    T data;
public:
    BaseTemplate2(T value) : data(value) {}
    T getData() {
        return data;
    }
};

// 派生模板类,定义新的模板参数
template <typename T, typename U>
class DerivedTemplate2 : public BaseTemplate2<T> {
private:
    U extraData;
public:
    DerivedTemplate2(T value, U extra) : BaseTemplate2<T>(value), extraData(extra) {}
    void printExtraData() {
        std::cout << "Extra Data in DerivedTemplate2: " << extraData << std::endl;
    }
};

int main() {
    DerivedTemplate2<int, double> derived2(40, 5.5);
    std::cout << "Data from Base: " << derived2.getData() << std::endl;
    derived2.printExtraData();

    return 0;
}

这里,BaseTemplate2 是模板基类,有模板参数 TDerivedTemplate2 是派生模板类,它不仅继承了 BaseTemplate2 的模板参数 T,还定义了自己新的模板参数 UDerivedTemplate2 可以利用 BaseTemplate2 中基于 T 的功能,同时使用 U 来表示和操作额外的数据。这种方式在需要扩展功能并且引入新的数据类型灵活性时非常有用。

同时继承和定义模板参数

// 模板基类
template <typename T1, typename T2>
class BaseTemplate3 {
protected:
    T1 data1;
    T2 data2;
public:
    BaseTemplate3(T1 value1, T2 value2) : data1(value1), data2(value2) {}
    T1 getFirstData() {
        return data1;
    }
    T2 getSecondData() {
        return data2;
    }
};

// 派生模板类,同时继承和定义模板参数
template <typename T1, typename T2, typename T3>
class DerivedTemplate3 : public BaseTemplate3<T1, T2> {
private:
    T3 newData;
public:
    DerivedTemplate3(T1 value1, T2 value2, T3 value3) : BaseTemplate3<T1, T2>(value1, value2), newData(value3) {}
    void printNewData() {
        std::cout << "New Data in DerivedTemplate3: " << newData << std::endl;
    }
};

int main() {
    DerivedTemplate3<int, double, char> derived3(60, 7.7, 'A');
    std::cout << "First Data from Base: " << derived3.getFirstData() << std::endl;
    std::cout << "Second Data from Base: " << derived3.getSecondData() << std::endl;
    derived3.printNewData();

    return 0;
}

在这个例子中,BaseTemplate3 有两个模板参数 T1T2DerivedTemplate3 继承了这两个模板参数,并且又定义了一个新的模板参数 T3。通过这种方式,DerivedTemplate3 既可以复用 BaseTemplate3 中基于 T1T2 的功能,又能利用 T3 来实现新的功能。这种形式在需要高度定制化和灵活性的场景中非常适用,例如在一个通用的数据处理框架中,可能有一个基类模板用于处理两种不同类型的数据,而派生类模板需要引入第三种数据类型来进行更复杂的处理。

模板类派生中的访问控制

在模板类派生过程中,访问控制规则与普通类派生基本一致,但需要注意模板参数的影响。

公有派生

当使用公有派生时,基类的公有成员在派生类中仍然是公有的,基类的保护成员在派生类中仍然是保护的。

// 模板基类
template <typename T>
class PublicBase {
public:
    T publicData;
protected:
    T protectedData;
private:
    T privateData;
public:
    PublicBase(T pub, T prot, T priv) : publicData(pub), protectedData(prot), privateData(priv) {}
};

// 公有派生模板类
template <typename T>
class PublicDerived : public PublicBase<T> {
public:
    void accessBaseData() {
        std::cout << "Public data from base: " << this->publicData << std::endl;
        std::cout << "Protected data from base: " << this->protectedData << std::endl;
        // 以下代码会报错,无法访问基类的私有成员
        // std::cout << "Private data from base: " << this->privateData << std::endl;
    }
};

int main() {
    PublicDerived<int> publicDerived(1, 2, 3);
    publicDerived.accessBaseData();
    std::cout << "Public data accessible outside: " << publicDerived.publicData << std::endl;

    return 0;
}

在上述代码中,PublicDerivedPublicBase 公有派生。在 PublicDerived 中,可以访问 PublicBase 的公有和保护成员,但不能访问私有成员。在 main 函数中,由于 publicData 是公有的,所以可以在类外部访问。

私有派生

当使用私有派生时,基类的公有和保护成员在派生类中都变为私有的。

// 模板基类
template <typename T>
class PrivateBase {
public:
    T publicData;
protected:
    T protectedData;
private:
    T privateData;
public:
    PrivateBase(T pub, T prot, T priv) : publicData(pub), protectedData(prot), privateData(priv) {}
};

// 私有派生模板类
template <typename T>
class PrivateDerived : private PrivateBase<T> {
public:
    void accessBaseData() {
        std::cout << "Public data from base (now private in derived): " << this->publicData << std::endl;
        std::cout << "Protected data from base (now private in derived): " << this->protectedData << std::endl;
        // 以下代码会报错,无法访问基类的私有成员
        // std::cout << "Private data from base: " << this->privateData << std::endl;
    }
};

int main() {
    PrivateDerived<int> privateDerived(4, 5, 6);
    privateDerived.accessBaseData();
    // 以下代码会报错,publicData在PrivateDerived中变为私有
    // std::cout << "Public data accessible outside: " << privateDerived.publicData << std::endl;

    return 0;
}

在这个例子中,PrivateDerivedPrivateBase 私有派生。在 PrivateDerived 中,PrivateBase 的公有和保护成员都变为私有的,可以在 PrivateDerived 内部访问,但在类外部无法访问。

保护派生

当使用保护派生时,基类的公有成员在派生类中变为保护的,基类的保护成员在派生类中仍然是保护的。

// 模板基类
template <typename T>
class ProtectedBase {
public:
    T publicData;
protected:
    T protectedData;
private:
    T privateData;
public:
    ProtectedBase(T pub, T prot, T priv) : publicData(pub), protectedData(prot), privateData(priv) {}
};

// 保护派生模板类
template <typename T>
class ProtectedDerived : protected ProtectedBase<T> {
public:
    void accessBaseData() {
        std::cout << "Public data from base (now protected in derived): " << this->publicData << std::endl;
        std::cout << "Protected data from base: " << this->protectedData << std::endl;
        // 以下代码会报错,无法访问基类的私有成员
        // std::cout << "Private data from base: " << this->privateData << std::endl;
    }
};

class FurtherDerived : public ProtectedDerived<int> {
public:
    void accessData() {
        std::cout << "Public data from base (now protected in further derived): " << this->publicData << std::endl;
    }
};

int main() {
    ProtectedDerived<int> protectedDerived(7, 8, 9);
    protectedDerived.accessBaseData();
    // 以下代码会报错,publicData在ProtectedDerived中变为保护的
    // std::cout << "Public data accessible outside: " << protectedDerived.publicData << std::endl;

    FurtherDerived furtherDerived;
    furtherDerived.accessData();

    return 0;
}

在这段代码中,ProtectedDerivedProtectedBase 保护派生。ProtectedBase 的公有成员在 ProtectedDerived 中变为保护的,保护成员仍然是保护的。FurtherDerivedProtectedDerived 公有派生,在 FurtherDerived 中可以访问 ProtectedDerived 中从 ProtectedBase 继承过来的保护成员。

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

在模板类派生中,虚函数和多态的概念同样适用,但由于模板的特性,需要特别注意一些细节。

模板类中的虚函数

// 模板基类
template <typename T>
class VirtualBase {
public:
    virtual void virtualFunction(T value) {
        std::cout << "Virtual function in VirtualBase with value: " << value << std::endl;
    }
};

// 派生模板类
template <typename T>
class VirtualDerived : public VirtualBase<T> {
public:
    void virtualFunction(T value) override {
        std::cout << "Overridden virtual function in VirtualDerived with value: " << value << std::endl;
    }
};

int main() {
    VirtualBase<int>* basePtr1 = new VirtualBase<int>();
    basePtr1->virtualFunction(10);

    VirtualBase<int>* basePtr2 = new VirtualDerived<int>();
    basePtr2->virtualFunction(20);

    delete basePtr1;
    delete basePtr2;

    return 0;
}

在上述代码中,VirtualBase 是一个模板类,其中定义了一个虚函数 virtualFunctionVirtualDerivedVirtualBase 派生,并覆盖了 virtualFunction。通过基类指针指向不同的派生类对象,可以实现多态行为。在 main 函数中,basePtr1 指向 VirtualBase 对象,调用的是 VirtualBase 中的 virtualFunctionbasePtr2 指向 VirtualDerived 对象,调用的是 VirtualDerived 中覆盖后的 virtualFunction

模板类派生中的运行时多态

运行时多态在模板类派生中同样有效,但需要注意对象的生命周期管理。

// 模板基类
template <typename T>
class RuntimeBase {
public:
    virtual void runtimeFunction(T value) = 0;
};

// 派生模板类1
template <typename T>
class RuntimeDerived1 : public RuntimeBase<T> {
public:
    void runtimeFunction(T value) override {
        std::cout << "Runtime function in RuntimeDerived1 with value: " << value << std::endl;
    }
};

// 派生模板类2
template <typename T>
class RuntimeDerived2 : public RuntimeBase<T> {
public:
    void runtimeFunction(T value) override {
        std::cout << "Runtime function in RuntimeDerived2 with value: " << value << std::endl;
    }
};

int main() {
    std::vector<RuntimeBase<int>*> basePtrs;
    basePtrs.push_back(new RuntimeDerived1<int>());
    basePtrs.push_back(new RuntimeDerived2<int>());

    for (auto ptr : basePtrs) {
        ptr->runtimeFunction(30);
    }

    for (auto ptr : basePtrs) {
        delete ptr;
    }

    return 0;
}

在这段代码中,RuntimeBase 是一个抽象模板类,包含一个纯虚函数 runtimeFunctionRuntimeDerived1RuntimeDerived2RuntimeBase 派生,并实现了 runtimeFunction。在 main 函数中,通过一个 std::vector 存储指向不同派生类对象的基类指针,然后遍历这个 vector 调用 runtimeFunction,实现了运行时多态。同时,要注意手动释放动态分配的对象内存,以避免内存泄漏。

模板类派生中的模板特化

在模板类派生过程中,模板特化也是一个重要的概念。模板特化允许我们针对特定的模板参数值提供专门的实现。

全特化

// 模板基类
template <typename T>
class GeneralBase {
public:
    void generalFunction(T value) {
        std::cout << "General function in GeneralBase with value: " << value << std::endl;
    }
};

// 模板类全特化
template <>
class GeneralBase<int> {
public:
    void generalFunction(int value) {
        std::cout << "Specialized function in GeneralBase<int> with value: " << value << std::endl;
    }
};

int main() {
    GeneralBase<double> baseDouble;
    baseDouble.generalFunction(3.14);

    GeneralBase<int> baseInt;
    baseInt.generalFunction(42);

    return 0;
}

在上述代码中,GeneralBase 是一个模板类。然后对 GeneralBase<int> 进行了全特化,即针对 Tint 的情况提供了专门的实现。在 main 函数中,分别创建了 GeneralBase<double>GeneralBase<int> 的对象,并调用 generalFunction,可以看到不同的输出,体现了全特化的效果。

偏特化

// 模板基类
template <typename T1, typename T2>
class PartialBase {
public:
    void partialFunction(T1 value1, T2 value2) {
        std::cout << "General partial function with values: " << value1 << ", " << value2 << std::endl;
    }
};

// 模板类偏特化
template <typename T>
class PartialBase<T, int> {
public:
    void partialFunction(T value1, int value2) {
        std::cout << "Specialized partial function with values: " << value1 << ", " << value2 << std::endl;
    }
};

int main() {
    PartialBase<double, float> base1;
    base1.partialFunction(3.14, 2.71);

    PartialBase<double, int> base2;
    base2.partialFunction(4.5, 10);

    return 0;
}

这里,PartialBase 是一个有两个模板参数的模板类。对 PartialBase<T, int> 进行了偏特化,即针对第二个模板参数为 int 的情况提供了专门的实现。在 main 函数中,创建了不同的对象并调用 partialFunction,展示了偏特化的作用。

在模板类派生中,模板特化可以用于优化特定类型的性能,或者提供与通用实现不同的行为。例如,在一个通用的排序模板类中,可以对 int 类型进行全特化,使用更高效的整数排序算法;对于特定的数据结构和数据类型组合,可以进行偏特化,以满足特定的需求。

模板类派生中的注意事项

  1. 模板实例化:在模板类派生中,要注意模板的实例化过程。模板只有在被使用时才会实例化,如果派生类没有使用基类模板的某些成员,这些成员可能不会被实例化。这可能会导致一些隐藏的问题,例如如果基类模板中的某个成员函数依赖于特定的模板参数类型的操作,而派生类没有调用该函数,那么在编译时可能不会发现该类型不支持这些操作的错误。
  2. 命名冲突:随着模板类派生层次的增加,命名冲突的可能性也会增加。要注意合理命名类、成员函数和变量,避免在不同层次的模板类中出现命名冲突。可以使用命名空间来组织代码,减少命名冲突的风险。
  3. 代码可读性和维护性:复杂的模板类派生结构可能会导致代码可读性和维护性下降。在设计模板类派生体系时,要尽量保持结构清晰,合理划分功能,添加足够的注释,以便其他开发者能够理解和维护代码。

综上所述,C++ 中模板类派生新类的不同形式为我们提供了丰富的编程手段。从非模板类派生模板类、从模板类派生非模板类以及从模板类派生模板类,每种形式都有其独特的应用场景和优势。同时,在派生过程中要注意访问控制、虚函数和多态、模板特化等方面的问题,以及一些常见的注意事项,这样才能编写出高效、灵活且可维护的 C++ 代码。