C++类模板

类模板 基本概念

类模板允许我们创建一个可以在工作在多种类型上的类,比如数组,链表,队列,栈等等。其工作原理就是用占位符代替实际的类型,当我们使用这个类模板时,再指定具体的类型。

语法:

template <class T>
class 类模板名
{
	类的定义;
};

函数模板建议用typename描述通用数据类型,类模板建议用class。

template <class T>
class MyArray {
    private:
        T* array;
        int size;
    public:
        MyArray(int size) {
            this->size = size;
            array = new T[size];
        }
        ~MyArray() {
            delete[] array;
        }
        void set(int index, T value) {
            array[index] = value;
        }
        T get(int index) {
            return array[index];
        }
};

上述类模板例子中,MyArray类可以用于创建各种类型的数组。包括MyArray, MyArray, 甚至是MyArray等。

**需要注意的是,在创建模板类的实例时,我们必须指定模板参数。**例如,我们需要创建一个存储整数的MyArray,我们可以这样写MyArray intArray(10)。这里,模板参数时int,表示我们创建的MyArray将存储int类型的元素。

需要确保使用的类型T能够适应类模板中的所有操作,例如,如果模板类中有比较操作,那么T就必须支持比较操作。

类模板可以为通用数据类型指定缺省的数据类型(C++11标准的函数模板也可以)。例如,你可以这样定义一个模板类:

template <class T = int>
class MyDefaultArray {
    // ...
};

这种情况下,如果在创建对象时没有提供模板参数,那么编译器就会使用默认的int类型。

如果我们需要将模板类的成员函数在类外实现,我们必须在函数声明前加上template ,并在函数名前加上类名和模板参数。例如:

template <class T>
void MyArray<T>::set(int index, T value) {
    // ...
}

另外我们也可以**用new来创建模板类对象。**例如:MyArray* p = new MyArray(5); 创建了一个可以存储int类型的MyArray对象,并返回了一个指向该对象的指针。

最后,类模板中的函数只有在程序中实际被使用时才会被实例化,这是由模板的特性决定的,可以有效的节省程序的运行资源。

创建模板类的方法

  1. 先写一个普通类,用具体的数据类型:开始时,可以先用一个具体的数据类型(如int、double或string等)来定义一个普通类。例如,你可以创建一个名为IntArray的类,用于存储int类型的数组。
class IntArray {
private:
    int* items;
    int size;
public:
    IntArray(int size) :size(size) {
        items = new int[size];
    }
    // ...其他函数...
};

在这个阶段,你可以专注于类的功能实现,而不用考虑模板类的复杂性。

  1. 调试普通类:一旦你创建了这个普通类,你可以对它进行测试,确保它的功能是正确的。你可以创建一些IntArray对象,然后尝试对它们进行各种操作,比如添加元素、删除元素、访问元素等。
  2. 把普通类改为模板类:当你确认这个普通类工作正常后,你就可以把它改为模板类。这主要涉及两个步骤:首先,在类的定义前添加template ,然后,把类中所有的具体数据类型(如int)都替换为模板参数T。
template <typename T>
class MyArray {
private:
    T* items;
    int size;
public:
    MyArray(int size) :size(size) {
        items = new T[size];
    }
    // ...其他函数...
};

类模板 示例——栈

简单回忆一下栈(Stack)数据结构。栈是一种“后进先出”(LIFO, Last In First Out)的数据结构,也就是说,最后进入栈的元素会先被取出。
在定义栈时,我们首先定义了一个指针“items”,它将指向我们创建的动态数组,这个数组将存储栈中的元素。然后,我们定义两个整数变量stacksizetop,分别表示栈的大小和栈顶的位置。

对于栈的两个主要操作,push 和 pop:

  • push 操作将新的元素添加到栈顶。在添加元素之前,需要先检查栈是否已满。如果栈已满,就不能再添加新的元素,push 操作将返回 false。否则,将新的元素添加到栈顶,然后将栈顶的位置向上移动一位,push 操作返回 true。
  • pop 操作从栈顶移除一个元素。在移除元素之前,需要先检查栈是否为空。如果栈为空,就不能移除元素,pop 操作将返回 false。否则,将栈顶的位置向下移动一位,移除栈顶的元素,pop 操作返回 true。

在实际应用中,栈是非常有用的数据结构。例如,当你在浏览网页时,点击浏览器的 “后退” 按钮,就是使用了一个栈。浏览器会记录你访问过的每个网页,将新访问的网页压入栈顶,点击 “后退” 按钮时,就从栈顶弹出最近访问的网页。

#include 

// 声明模板类 "Stack",模板参数 "DataType" 可以是任何类型。
template <class DataType>
class Stack {
private:
    DataType* items;  // 动态数组,用于存储栈的元素。
    int stacksize;    // 栈的大小。
    int top;          // 栈顶的位置。

public:
    // 构造函数,参数 "size" 指定了栈的大小。
    Stack(int size) : stacksize(size), top(-1) {
        // 分配一段内存,大小为 "size",用于存储栈的元素。
        items = new DataType[stacksize];
    }

    // push 操作,将新的元素添加到栈顶。
    bool push(const DataType& item) {
        // 如果栈已满,返回 false。
        if (top >= stacksize - 1) return false;
        // 否则,将新的元素添加到栈顶,栈顶位置加一。
        items[++top] = item;
        return true;
    }

    // pop 操作,从栈顶移除一个元素。
    bool pop(DataType& item) {
        // 如果栈为空,返回 false。
        if (top < 0) return false;
        // 否则,从栈顶移除一个元素,栈顶位置减一。
        item = items[top--];
        return true;
    }

    // 析构函数,释放动态数组占用的内存。
    ~Stack() {
        delete [] items;
    }
};

int main() {
    // 创建一个能存储 int 类型数据的栈,大小为 10。
    Stack<int> intStack(10);

    // 尝试将 5 压入栈。
    if (!intStack.push(5)) {
        std::cout << "Push failed." << std::endl;
    }

    // 创建一个变量,用于存储 pop 出的元素。
    int poppedItem;
    // 尝试从栈顶弹出一个元素。
    if (!intStack.pop(poppedItem)) {
        std::cout << "Pop failed." << std::endl;
    } else {
        std::cout << "Popped item: " << poppedItem << std::endl;
    }

    return 0;
}

类模板 示例—— 数组

Array 模板类

Array模板类是一个静态数组。在这个类模板中,有两个模板参数:T表示数组中元素的类型,len表示数组的长度。静态数组的大小在编译时就确定了,你不能在运行时改变它的大小。这是一个静态数组的主要限制。

#include
#include

template <class T, int len>
class Array{
private:
    T items[len];
public:
    Array(){}
    ~Array(){}
    T& operator[](int ii){return items[ii];}
    const T& operator[](int ii) const{return items[ii];}
};

int main()
{
    Array <int, 5> intArray;
    for( int i = 0; i < 5 ; ++i)
    {
        intArray[i] = i * 2;
    }
    for(int i = 0; i< 5; ++i)
    {
        std::cout << intArray[i] << std::endl;
    }

    Array<std::string, 3> strArray;  // 创建一个长度为3的字符串数组
    strArray[0] = "Apple";
    strArray[1] = "Banana";
    strArray[2] = "Cherry";

    for (int i = 0; i < 3; ++i) {
        std::cout << "strArray[" << i << "] = " << strArray[i] << std::endl;
    }

    return 0;
}

Vector 模板类

Vector模板类是一个动态数组。在这个类模板中,只有一个模板参数T,表示数组中元素的类型。动态数组的大小可以在运行时改变,你可以随时添加或删除元素。

#include
#include

// 你给出的Vector模板类定义
template <class T>
class Vector {
private:
    int len;
    T* items;
public:
    Vector(int size = 10):len(size) {
        items = new T[len];
    }
    ~Vector() {
        delete[] items; 
        items = nullptr;
    }
    void resize(int size) {
        if (size <= len) return;
        T* tmp = new T[size];
        for (int ii = 0; ii < len; ii++) tmp[ii] = items[ii];
        delete[] items;
        items = tmp;
        len = size;
    }
    int size() const { return len; }
    T& operator[](int ii) { 
        if (ii >= len) resize(ii + 1);
        return items[ii]; 
    }
    const T& operator[](int ii) const { return items[ii]; }
};

int main() {
    Vector<int> intVector(3);  // 创建一个初始长度为3的整数数组
    intVector[0] = 5;
    intVector[1] = 10;
    intVector[2] = 15;

    // 使用resize函数扩大数组
    intVector.resize(5);
    intVector[3] = 20;
    intVector[4] = 25;

    for (int i = 0; i < intVector.size(); ++i) {
        std::cout << "intVector[" << i << "] = " << intVector[i] << std::endl;
    }

    Vector<std::string> strVector(2);  // 创建一个初始长度为2的字符串数组
    strVector[0] = "Hello";
    strVector[1] = "World";

    // 使用resize函数扩大数组
    strVector.resize(3);
    strVector[2] = "!";

    for (int i = 0; i < strVector.size(); ++i) {
        std::cout << "strVector[" << i << "] = " << strVector[i] << std::endl;
    }

    return 0;
}

静态&动态需求分析

静态数组具体固定的大小,不能在运行时改变。动态数组,可以在运行时调整大小。如果你知道需要存储的数据量,并且这个数据量在程序运行期间不会改变,那么静态数组是一个很好的选择。反之,动态数组更好。

静态数组一般在栈上分配内存,这就意味着超出作用域时,它的内存将自动被释放,不需要手动管理。而动态数组需要手动管理内存,所以你使用动态数组时需要更多的注意力。在vector类中,这主要体现在resize函数的使用上,这个函数用于调整数组大小并在需要时分配更多的内存。

嵌套和递归使用模板类

嵌套模板类就是指一个模板类的参数又是另一个模板类。

递归嵌套是指模板类的参数还是该模板类本身。

#include
#include

using namespace std;

template<typename T>
class Stack {
private:
    T *data;
    int top;
    int capacity;

    void resize(int newCapacity) {
        T *newData = new T[newCapacity];
        for (int i = 0; i <= top; ++i) {
            newData[i] = data[i];
        }

        delete[] data;
        data = newData;
        capacity = newCapacity;
    }

public:
    Stack(int initCapacity = 4) : data(new T[initCapacity]), top(-1), capacity(initCapacity) {}

    ~Stack() { delete[] data; }


    void push(const T &item) {
        if (top + 1 >= capacity)
        {
            resize(capacity*2);
        }
        data[++top] = item;
    }

    T pop(){
        if(top == -1)
        {
            throw runtime_error("Stack is empty");
        }
        T item = data[top--];
        if(top+1 <= capacity/4 && capacity/2 != 0)
        {
            resize(capacity / 2);
        }
        return item;
    }

    bool isEmpty () const {
        return top == -1;
    }
};

template<typename T>
class Vector{
private:
    T* data;
    int size;
    int capacity;

    void resize(int newcapacity)
    {
        T* newData = new T[newcapacity];
        for(int i = 0; i < size; ++i)
        {
            newData[i] = data[i];
        }
        delete[] data;
        data = newData;
    }

public:
    Vector(int capacity = 4) : data(new T[capacity]), size(0), capacity(capacity) {}
    ~Vector(){delete[] data;}

    void push_back(const T& item)
    {
        if(size >= capacity)
        {
            resize(capacity*2);
        }
        data[size++] = item;
    }

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

    int get_size() const{
        return size;
    }

};

int main()
{
    //创建一个动态数组来存储用户
    Vector<Stack<string>> users(4);

    //为第一个用户创建一个栈
    Stack<string> user1;
    user1.push("hello,");
    user1.push("world.");

    // 为第二个用户创建一个栈
    Stack<std::string> user2;
    user2.push("Hello, C++!");
    user2.push("Goodbye, C++!");

    // 将两个用户添加到数组中
    users.push_back(user1);
    users.push_back(user2);

    // 打印两个用户所有数据
    cout<<users[0].pop()<<endl;
    cout<<users[0].pop()<<endl;
    cout<<users[1].pop()<<endl;
    cout<<users[1].pop()<<endl;
    
    return 0;
}

我们用Stack模板类来存储每个用户的数组(string类型),然后我们定义了一个Vector模板类,其元素是Stack,即Vector>,来保存所有用户的数据。

这部分内容需要注意的是:

  1. 类型兼容性:因为模板类可以接受任何类型的参数,但是这并不意味着我们可以随意传递任何类型的参数。
  2. **编译器兼容性:**在C++11之前,嵌套模板类的右箭头(>>)之间需要加一个空格,否则编译器会把它识别为右移运算符,导致编译错误。但是在C++11及以后的版本中,这个空格已经不再必要了。
  3. **内存管理:**在设计模板类的时候,一定要注意内存管理,特别是在析构函数、赋值运算符等函数中。在这些函数中,我们需要正确地释放动态分配的内存,否则会造成内存泄露。

模板类具体化

类模板的具体化和函数模板的具体化有许多相似之处,但还是有些不同,如果函数模板具体化搞明白的话,这部分内容会很轻松。

模板类具体化(特化,特例化)有两种:完全具体化和部分具体化。

具体化程度高的类优先于具体化程度低的类,具体化的类优先于没有具体化的类。

具体化的模板类,成员函数类外实现的代码应该放在源文件中。

#include          
using namespace std;        

template<typename T1, typename T2>
class AA {
public:
	T1 m_x;
	T2 m_y;

	AA(const T1& x, const T2& y) : m_x(x), m_y(y) { 
        cout << "Generic class: constructor\n"; 
    }
	
    void show() const {
		cout << "Generic class: x = " << m_x << ", y = " << m_y << "\n";
	}
};

template<>
class AA<int, string> {
public:
	int      m_x;
	string   m_y;

	AA(const int x, const string& y) : m_x(x), m_y(y) { 
        cout << "Fully specialized: constructor\n"; 
    }
	
    void show() const {
		cout << "Fully specialized: x = " << m_x << ", y = " << m_y << "\n";
	}
};

template<typename T1>
class AA<T1, string> {
public:
	T1      m_x;
	string  m_y;

	AA(const T1& x, const string& y) : m_x(x), m_y(y) { 
        cout << "Partially specialized: constructor\n"; 
    }
	
    void show() const {
		cout << "Partially specialized: x = " << m_x << ", y = " << m_y << "\n";
	}
};

int main()
{
	AA<int, string>    aa1(8, "Bird");  // Will use the fully specialized class.
	AA<char, string>   aa2('A', "Bird");  // Will use the partially specialized class.
	AA<int, double>    aa3(8, 999999); // Will use the generic class.

    aa1.show();
    aa2.show();
    aa3.show();

    return 0;
}

模板类与继承

1. 模板类继承普通类

模板类继承普通类的一个主要应用场景就是当你希望扩展一个普通类的功能,但同时希望扩展的功能能适应多种数据类型。例如:你可能有一个基本的Array类,该类处理整数数组,但你想创建一个Array类的派生类,可以处理整数、浮点数、字符串等任何类型的数组。

需要注意的是,在这种情况下,基类是一个常规类,不含模板。派生类是模板类,可以处理不同的数据类型。

#include 

// 基类(普通类)
class Array {
protected: // 修改访问权限为protected
    int* array_;
    int size_;
public:
    Array(int size) : size_(size) {
        array_ = new int[size_];
    }

    ~Array() {
        delete[] array_;
    }

    void setValue(int index, int value) {
        if(index < size_)
            array_[index] = value;
    }

    int getValue(int index) {
        if(index < size_)
            return array_[index];
        else
            return -1; // 对于超出数组范围的情况,简单返回-1
    }


};

// 派生类(模板类)
template<typename T>
class ArrayEx : public Array {
private:
    T* arrayEx_;
public:
    ArrayEx(int size) : Array(size) {
        arrayEx_ = new T[size];
    }

    ~ArrayEx() {
        delete[] arrayEx_;
    }

    void setValueEx(int index, T value) {
        if(index < size_)
            arrayEx_[index] = value;
    }

    T getValueEx(int index) {
        if(index < size_)
            return arrayEx_[index];
        else
            return T(); // 对于超出数组范围的情况,简单返回类型T的默认值
    }

};

int main() {
    // 创建一个能存储整数的ArrayEx对象
    ArrayEx<int> array1(10);
    array1.setValue(0, 5);
    array1.setValueEx(0, 10);
    std::cout << "ArrayEx value: " << array1.getValue(0) << ", " << array1.getValueEx(0) << std::endl;

    // 创建一个能存储字符串的ArrayEx对象
    ArrayEx<std::string> array2(10);
    array2.setValue(0, 5);
    array2.setValueEx(0, "Hello");
    std::cout << "ArrayEx value: " << array2.getValue(0) << ", " << array2.getValueEx(0) << std::endl;

    return 0;
}

2. 普通类继承模板类的实例化版本

这通常发生在你有一个模板类,并且希望创建一个具有额外功能的特定版本。例如,你有一个Array模板类,该类可以处理任何类型的数组。然后,你希望创建一个StringArray类,该类继承Array,并添加一些专门用于处理字符串数组的方法。

这种情况下你需要注意的是,派生类是一个特定的类,只能处理特定类型的数据(在这个例子中是字符串)。因此,尽管你从一个模板类派生,但你失去了那种可以处理任何类型的灵活性。

#include 
#include 

// 基类(模板类)
template<typename T>
class Array {
public:
    Array(int size) : size_(size) {
        array_ = new T[size_];
    }

    virtual ~Array() {
        delete[] array_;
    }

    void setValue(int index, T value) {
        if(index < size_)
            array_[index] = value;
    }

    T getValue(int index) {
        if(index < size_)
            return array_[index];
        else
            return T(); // 对于超出数组范围的情况,简单返回类型T的默认值
    }

protected:
    T* array_;
    int size_;
};

// 派生类(特定类,只处理字符串类型)
class StringArray : public Array<std::string> {
public:
    StringArray(int size) : Array<std::string>(size) {}

    // 添加的专门用于处理字符串数组的方法
    std::string concatenate() {
        std::string result;
        for(int i = 0; i < size_; ++i) {
            result += array_[i];
        }
        return result;
    }
};

int main() {
    // 创建一个StringArray对象
    StringArray array(2);
    array.setValue(0, "Hello");
    array.setValue(1, "World");
    std::cout << "Concatenated string: " << array.concatenate() << std::endl; // 输出"HelloWorld"
    return 0;
}

普通类继承模板类

普通类不能直接继承一个模板类。通常普通类继承的是某个模板类的实例化版本。

template <typename T>
class TemplateBase {
    T data;
public:
    void setData(T value) { data = value; }
    T getData() const { return data; }
};

class DerivedFromTemplate : public TemplateBase<int> {
    //...
};

模板类继承模板类

模板类可以继承另一个模板类,而不管它们的模板参数是否相同。

template<typename T>
class G {
  //...
};

template<typename T>  
class H : public G<T> {
  // H 模板类继承 G 模板类
};

模板类继承模板类参数给出的基类

在这种设计中,基类是由模板参数来决定的,这为设计提供了极大的灵活性。

template <typename BaseClass>
class Adapter : public BaseClass {
    //...
};

模板类与函数

模板类可以用于函数的参数和返回值,有三种形式:

  1. 普通函数,参数和返回值是模板类的实例化版本

在这种情况下,我们定义的函数使用了特定的、已经实例化的模板类版本作为参数或返回值。

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

void displayBox(Box<int> b) {
    std::cout << "Box contains: " << b.getData() << std::endl;
}

int main() {
    Box<int> myBox(10);
    displayBox(myBox);
}
  1. 函数模板,参数和返回值是某种的模板类

这里的关键是函数本身也是模板化的,这就意味着它可以接受多种版本的模板类。

template <typename T>
T extractData(Box<T> b) {
    return b.getData();
}

int main() {
    Box<int> intBox(10);
    Box<double> doubleBox(10.5);
    
    std::cout << "Integer Box: " << extractData(intBox) << std::endl;
    std::cout << "Double Box: " << extractData(doubleBox) << std::endl;
}
  1. 函数模板,参数和返回值是任意类型

这是模板编程中最通用的形式,因为他允许函数处理任意类型,无论是普通的类型还是模板类

template <typename T>
void display(T item) {
    std::cout << "Item: " << item << std::endl;
}

int main() {
    display<int>(10);
    display<double>(10.5);
    display<std::string>("Hello");
    
    Box<char> charBox('A');
    display<Box<char>>(charBox);
}

在这里,display函数可以接受任何类型作为参数。

模板类与友元

模板类的友元函数有三类:

1)非模板友元:友元函数不是模板函数,而是利用模板类参数生成的函数。

2)约束模板友元:模板类实例化时,每个实例化的类对应一个友元函数。

3)非约束模板友元:模板类实例化时,如果实例化了n个类,也会实例化n个友元函数,每个实例化的类都拥有n个友元函数。

非模板友元

非模板友元函数不是模板函数,它是基于特定模板示例参数创建的。

template<class T>
class AA{
public:
    T a;
    AA(T a):a(a){}
    friend void show(const AA<int>& a);
};

void show(const AA<int>& a)
{
    cout << a.a << endl;
}

int main()
{
    AA<int> a(1);
    show(a);
    return 0;
}

约束模板友元

此类型表示模板类实例化时,每个实例化的类都有一个对应的友元函数模板。(常用)

template<class T>
class AA{
    T a;
public:
    AA(T a): a(a){}

    template<typename U>
    friend void show(const AA<U>& a);
};

template<typename U>
void show(const AA<U>& a){
    cout<< a.a << endl;
}

int main(){
    AA<int> a(1);
    show(a);
    return 0;
}

非约束模板友元

对于非约束模板友元,如果模板类实例化了n个类,则每个类都有n个友元函数。(基本不用)

假设你正在制作一个计算器,这个计算器可以执行各种操作,如加法、乘法等。每种操作都有自己的模板函数,但计算器类应该能够访问所有这些操作。

template<class T1, class T2>
class Calculator {
    template <typename Operation> friend void executeOperation(T1, T2);
    T1 value1;
    T2 value2;
public:
    Calculator(const T1 &v1, const T2 &v2) : value1(v1), value2(v2) { }
};

template <typename Operation>
void executeOperation(Operation op, Calculator<int, int>& calc) {
    cout << "Result: " << op(calc.value1, calc.value2) << endl;
}

int add(int a, int b) {
    return a + b;
}

int main() {
    Calculator<int, int> calc(3, 4);
    executeOperation(add, calc); // 输出: Result: 7
}

你可能感兴趣的:(C++学习/笔记,c++,java,算法)