最近更新于 2023-02-19 00:41

环境

Debian 11 (arm64)

编译器 g++ 10.2.1;编译标准 C++20;参数:-std=c++20 -no-pie -Wall -Werror=return-type -Werror=non-virtual-dtor -Werror=address -Werror=sequence-point -Werror=format-security -Wextra -pedantic -Wimplicit-fallthrough -Wsequence-point -Wswitch-unreachable -Wswitch-enum -Wstringop-truncation -Wbool-compare -Wtautological-compare -Wfloat-equal -Wshadow=global -Wpointer-arith -Wpointer-compare -Wcast-align -Wcast-qual -Wwrite-strings -Wdangling-else -Wlogical-op -Wconversion -g -O0

函数模板

模板的作用,这里举一个例子来说明。假如这里我要写数据交换的函数,那么针对不同数据类型都要去写一个实现,而使用模板,则只用一个函数可以做到所有数据类型的交换。

#include <iostream>

template<typename T>
void swap(T &data1, T &data2)
{
    T tmp = data1;
    data1 = data2;
    data2 = tmp;
}

int main()
{
    int num1 = 8;
    int num2 = 9;
    swap(num1, num2);
    std::cout << num1 << " " << num2 << std::endl;

    std::string s1 = "AB";
    std::string s2 = "ba";
    swap(s1, s2);
    std::cout << s1 << " " << s2 << std::endl;

    double f1 = 2.1;
    double f2 = 3.4;
    swap(f1, f2);
    std::cout << f1 << " " << f2 << std::endl;

    char c1 = 'A';
    char c2 = 'B';
    swap(c1, c2);
    std::cout << c1 << " " << c2 << std::endl;
}

file

代码示例中写的 template <typename T>,T 就用于代表任意数据类型,可以自命名,如下图,只是一般习惯写 T
file

在使用模板时,T 必须是确定的类型,比如下面的例子,如果参数没有使用 T,就不能通过传入参数来推导出 T 的类型,此时就必须在调用时指定 T 的类型。

#include <iostream>

template<typename T>
void func()
{
    std::cout << sizeof(T) << std::endl;
}

int main()
{
    func<char>();
    func<int>();
    func<double>();
    func<std::string>();
}

file

隐式转换

普通函数可以发生隐式转换,比如

函数的参数都是 double,我传入的一个是 int,另一个是 float,实际都会发生隐式转换,变为 double 再进行计算

#include <iostream>

double add(double num1, double num2)
{
    return num1 + num2;
}

int main()
{
    int a = 8;
    float b = 2.4f;
    std::cout << add(a, b) << std::endl;
}

而函数模板则不行

#include <iostream>

template<typename T>
double add(T num1, T num2)
{
    return num1 + num2;
}

int main()
{
    int a = 8;
    float b = 2.4f;
    std::cout << add(a, b) << std::endl;
}

file

除非调用时指定 T 的类型
file

优先顺序

当同时存在普通函数和函数模板匹配时,会优先调用普通函数

#include <iostream>

template<typename T>
T add(T num1, T num2)
{
    std::cout << "函数模板" << std::endl;
    return num1 + num2;
}

int add(int num1, int num2)
{
    std::cout << "普通函数" << std::endl;
    return num1 + num2;
}

int main()
{
    add(8, 9);
}

file

如果要强制调用函数模板,则需要使用空模板参数列表
file

类模板

下面的例子定义了一个 Person 类型,属性有名字和年龄,变量不指定类型,由模板推导

#include <iostream>

// template <typename nameType, typename ageType> // 用 typename 或 class 都可以
template<class nameType, class ageType>
class Person
{
    private:
        nameType m_name;
        ageType m_age;

    public:
        Person(nameType name, ageType age)
        {
            m_name = name;
            m_age = age;
        }

        nameType getName()
        {
            return m_name;
        }

        ageType getAge()
        {
            return m_age;
        }
};

int main()
{
    Person p1("小明", 20);
    std::cout << p1.getName() << " " << p1.getAge() << std::endl;
    std::cout << typeid(p1.getName()).name() << std::endl;
    std::cout << typeid(const char *).name() << std::endl;
    std::cout << typeid(p1.getAge()).name() << std::endl;
    std::cout << typeid(int).name() << std::endl;
}

PKc 就是 “const char *”
file

类模板对象做函数参数

#include <iostream>

template<typename nameType, typename ageType>
class Person
{
    private:
        nameType m_name;
        ageType m_age;

    public:
        Person(nameType name, ageType age)
        {
            m_name = name;
            m_age = age;
        }

        void setName(nameType name)
        {
            m_name = name;
        }

        void setAge(ageType age)
        {
            m_age = age;
        }

        nameType getName()
        {
            return m_name;
        }

        ageType getAge()
        {
            return m_age;
        }
};

template<typename T1, typename T2>
void func(Person<T1, T2> &p)
{
    p.setName("小明");
    p.setAge(20);
}

int main()
{
    Person<std::string, int> p("", -1);
    func(p);
    std::cout << p.getName() << " " << p.getAge() << std::endl;
    std::cout << typeid(p.getName()).name() << std::endl;
    std::cout << typeid(p.getAge()).name() << std::endl;
}

file

或者将上面的 func 函数改为如下图,让它自己推导类类型
file

类模板继承

这里写了一个例子,Son 类从 Base 类继承,需要指定 Base 类模板的 myType 类型,在派生类 Son 中也可以写成类模板,让它自动推导类型。在实例化 Son 类时,推导出 Son 中 myType1 的类型,再可以推出基类 Base 中 myType 的类型。

#include <iostream>

template<typename myType>
class Base
{
    public:
        myType m_data;

        ~Base()
        {
            std::cout << typeid(m_data).name() << std::endl;
        }
};

template<typename myType1>
class Son : public Base<myType1>
{
    public:
        myType1 m_data1;
        Son(myType1 data)
        {
            m_data1 = data;
        }
};

int main()
{
    Son s("hello");
    std::cout << s.m_data1 << " " << typeid(s.m_data1).name() << std::endl;
}

类模板成员函数类外实现

在一般开发的时候,会把声明放到头文件,定义写到源文件,即声明和定义分离。不过类模板成员函数比较特殊,这是一个要注意的点,不能像平时那样,声明放在 .hpp,定义放在 .cpp,否则编译的时候会报错说找不到定义。因为类模板定义的东西在编译时不会为它分配内存,只有实例化的时候才会分配。
所以类模板的声明和定义必须写在同一个文件里,但是可以将声明和定义分离,这里就写了一个例子,在类外定义。

#include <iostream>

template<typename T1, typename T2>
class Person
{
    private:
        T1 m_name;
        T2 m_age;

    public:
        Person(T1 name, T2 age);

        T1 getName();

        T2 getAge();
};

template<typename T1, typename T2>
Person<T1, T2>::Person(T1 name, T2 age)
{
    m_name = name;
    m_age = age;
}

template<typename T1, typename T2>
T1 Person<T1, T2>::getName()
{
    return m_name;
}

template<typename T1, typename T2>
T2 Person<T1, T2>::getAge()
{
    return m_age;
}

int main()
{
    Person p("小强", 19);
    std::cout << p.getName() << " " << p.getAge() << std::endl;
}

file

类模板友元

第一个例子友元函数的实现是在类内的,只要加上了 friend,就代表它是一个全局函数,但是作为友元可以访问私有成员

#include <iostream>

template<typename T>
class Person
{
    private:
        T m_name;
        friend T get(Person &p)
        {
            return p.m_name;
        }

    public:
        Person(T name)
        {
            m_name = name;
        }
};

int main()
{
    Person p("小强");
    std::cout << get(p) << std::endl;
}

而友元函数的实现写在类外的就要麻烦一点了
在类中声明友元函数的地方,函数名后要加上尖括号
在友元函数实现处,函数参数中的类模板要写上参数列表

#include <iostream>

template<typename T>
class Person
{
    private:
        T m_name;
        friend T get<>(Person &p);

    public:
        Person(T name)
        {
            m_name = name;
        }
};

template<typename T>
T get(Person<T> &p)
{
    return p.m_name;
}

int main()
{
    Person p("小强");
    std::cout << get(p) << std::endl;
}

仿 vector

vector 是 C++ 中的一种容器,简单点说就是一个动态数组。这里就应用模板技术模拟几个 vector 的基础接口,以熟悉模板的使用。

MyVector.hpp

#ifndef MYVECTOR_HPP
#define MYVECTOR_HPP

#define INIT_SIZE 16 // 初始数组大小
#define EXTEND_SIZE 16 // 每次扩容大小

template<typename T>
class MyVector
{
    private:
        T *m_pData = nullptr; // 指向存储数据的指针
        int m_capacity; // 数组容量
        int m_size; // 数组实际大小

    public:
        /**
         * @brief 默认构造函数
         */
        MyVector();

        /**
         * @brief 拷贝构造函数 - 深拷贝实现
         * @param mv 拷贝对象
         */
        MyVector(const MyVector &mv);

        /**
         * @brief 赋值重载 - 深拷贝实现
         * @param mv 拷贝对象
         */
        MyVector &operator=(const MyVector &mv);

        /**
         * @brief 析构函数 - 释放资源
         */
        ~MyVector();

        /**
         * @brief 尾插实现
         * @param data 要插入的数据
         */
        void push_back(const T &data);

        /**
         * @brief 尾删实现
        */
        void pop_back();

        /**
         * @brief 重载 [] 实现下标索引
         * @param idx 下标
         * @return 返回数据
        */
        T &operator[](int idx);

        /**
         * @brief 获取数组的大小
         * @return 数组的大小
        */
        int size();
};

template<typename T>
MyVector<T>::MyVector()
{
    m_pData = new T[INIT_SIZE];
    m_capacity = INIT_SIZE;
    m_size = 0;
}

template<typename T>
MyVector<T>::MyVector(const MyVector &mv)
{
    if (m_pData != nullptr)
    {
        delete[] m_pData;
    }

    m_capacity = mv.m_capacity;
    m_size = mv.m_size;

    m_pData = new T[m_capacity];
    for (int i = 0; i < m_size; ++i)
    {
        m_pData[i] = mv.m_pData[i];
    }
}

template<typename T>
MyVector<T> &MyVector<T>::operator=(const MyVector &mv)
{
    if (m_pData != nullptr)
    {
        delete[] m_pData;
    }

    m_capacity = mv.m_capacity;
    m_size = mv.m_size;

    m_pData = new T[m_capacity];
    for (int i = 0; i < m_size; ++i)
    {
        m_pData[i] = mv.m_pData[i];
    }

    return *this;
}

template<typename T>
MyVector<T>::~MyVector()
{
    if (m_pData != nullptr)
    {
        delete[] m_pData;
        m_pData = nullptr;
        m_capacity = 0;
        m_size = 0;
    }
}

template<typename T>
void MyVector<T>::push_back(const T &data)
{
    if (m_size + 1 > m_capacity) // 扩容
    {
        T *tmp = new T[m_capacity + EXTEND_SIZE];
        for (int i = 0; i < m_size; ++i)
        {
            tmp[i] = m_pData[i];
        }
        delete[] m_pData;
        m_pData = tmp;
        m_capacity += EXTEND_SIZE;
    }
    m_pData[m_size] = data;
    ++m_size;
}

template<typename T>
void MyVector<T>::pop_back()
{
    if (m_size == 0)
    {
        return;
    }
    --m_size;
}

template<typename T>
T &MyVector<T>::operator[](int idx)
{
    return m_pData[idx];
}

template<typename T>
int MyVector<T>::size()
{
    return m_size;
}

#endif

testMain.cpp

#include "MyVector.hpp"
#include <iostream>

template<typename T>
void print(MyVector<T> &mv)
{
    for (int i = 0; i < mv.size(); ++i)
    {
        std::cout << mv[i] << " ";
    }
    std::cout << std::endl;
}

int main()
{
    MyVector<int> mv1; // 默认构造
    for (int i = 0; i < 100; ++i)
    {
        mv1.push_back(i * 3); // 尾插
    }
    print(mv1);

    int mv1Size = mv1.size();
    for (int i = 0; i < mv1Size / 2; ++i)
    {
        mv1.pop_back(); // 尾删
    }
    print(mv1);

    std::string s1 = "hello";
    std::string s2 = "12345";
    std::string s3 = "abcdef";
    MyVector<std::string> mv2;
    mv2.push_back(s1);
    mv2.push_back(s2);
    mv2.push_back(s3);
    print(mv2);

    MyVector mv3(mv2); // 拷贝构造
    print(mv3);

    MyVector mv4 = mv3; // 赋值重载
    print(mv4);
}

file
右键在新标签页中打开图像即为原图