C++:模板——详解函数模板与类模板

1. 模板的概念

C++的模板(Templates)是泛型编程的基础,它允许编写与类型无关的代码,从而提高代码的复用性和灵活性。通过模板,你可以编写一种通用的函数或类,而不需要为每种特定的数据类型单独定义多个函数或类。这种机制主要分为函数模板类模板

模板就是建立通用的模具,大大提高复用性

2. 函数模板

函数模板可以用于编写可以接受不同类型参数的函数。你定义一次模板,编译器会根据你调用时的参数类型自动生成相应的函数版本。其基本语法如下:

template <typename T>
T add(T a, T b) {
    return a + b;
}

解释

  • template – 声明创建模板
  • typename – 表明其后面的符号是一种数据类型,可以用class代替
  • T – 通用的数据类型,名称可以替换,通常为大写字母

这里的template <typename T>声明了一个模板,T是模板参数,表示一种泛型类型。函数add会根据实际调用时的参数类型推导出T,比如当你传递两个整数时,Tint类型;当你传递两个浮点数时,Tfloat类型:

int x = add(5, 3);       // T是int
double y = add(5.2, 3.8); // T是double

2.1 两种方式使用函数模板

  1. 自动类型推导myswap(a,b);
  2. 显示指定类型myswap<int>(a,b);

注意事项

  • 自动类型推导,必须推导出一致的数据类型T,才可以使用
  • 模板必须要确定T的数据类型,才可以使用

2.2 普通函数与函数模板区别

  • 普通函数调用时可以发生自动类型转换(隐式类型转换)
  • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  • 如果利用显示指定类型的方式,可以发生隐式类型转换

2.3 普通函数与函数模板的调用规则

  • 如果函数模板和普通函数都可以实现(同名且参数个数相同),优先调用普通函数

  • 可以通过空模板参数列表来强制调用函数模板

    void myPrint(int a,int b){
    	cout<<"调用普通函数"<<endl;
    }
    
    template<typename T>
    void myPrint(T a,T b){
    	cout<<"调用模板函数"<<endl;
    }
    
    int main(){
        myPrint(a,b);  //优先调用普通函数
        
        myPrint<>(a,b);  //通过空模板参数列表强制调用函数模板
    }
    
  • 函数模板也可以发生重载

  • 如果函数模板可以产生更好的匹配,优先调用函数模板

2.4 模板的局限性

模板并不是万能的,有些特定数据类型,需要用具体化方式做特殊实现

3. 类模板

3.1 基本概念

类模板与函数模板类似,但用于类的定义。使用类模板,可以创建一个通用的类,允许在实例化时为类指定不同的数据类型。其基本语法如下:

template <typename T>
class Box {
private:
    T value;
public:
    Box(T v) : value(v) {}
    T getValue() { return value; }
};

在上面的示例中,Box是一个类模板,T是一个泛型类型。你可以在创建Box对象时为T指定不同的类型:

Box<int> intBox(123);      // T是int
Box<double> doubleBox(45.6); // T是double

3.2 类模板与函数模板区别

  1. 类模板没有自动类型推导的使用方式
  2. 类模板在模板参数列表中可以有默认参数
template<class NameType,class AgeType = int> //模板参数列表有默认参数
class Person {
public:
	Person(NameType name, AgeType age) {
		this->name = name;
		this->age = age;
	}
	void showPerson() {
		cout << "姓名:" << name << ",年龄:" << age << endl;
	}
	NameType name;
	AgeType age;
};
int main()
{
	//Person p("孙悟空",1000);  //错误,无法用自动类型推导
	//Person<string, int> p("孙悟空",1000);  //正确,只能用显示指定类型

	Person<string> p("猪八戒",999);  //由于模板参数列表有默认参数,所以只需要指出string即可,函数模板不能这么用
	p.showPerson();
	return 0;
}

3.3 类模板中成员函数创建时机

  • 普通类中的成员函数一开始就可以创建
  • 类模板中的成员函数在调用时才创建

3.4 类模板对象做函数参数

template<class T1,class T2>
class Person{
public:
    T1 name;
    T2 age;
    Person(T1 name,T2 age){
        this->name = name;
        this->age = age;
    }
    void showPerson(){
		cout << "name:"<<this->name<<"  age:"<<this->age<<endl;
    }
};

类模板实例化出的对象,向函数传参的方式

  1. 指定传入的类型 – 直接显示对象的数据类型(使用广泛

    void printPerson1(Person<string,int>&p){  //指定传入的类型
        p.showPerson();
    }
    int main()
    {
        Person<string,int> p("孙悟空",18);
        printPerson1(p);
    }
    
  2. 参数模板化 - 将对象中的参数变为模板进行传递

    template<class T1,class T2>
    void printPerson2(Person<T1,T2>&p){  //参数模板化
        p.showPerson();
        cout << "T1 的类型为:" << typeid(T1).name() << endl;  //得到T1的类型
    }
    int main()
    {
        Person<string,int> p("白骨精",18);
        printPerson2(p);
    }
    
  3. 整个类模板化 - 将这个对象类型模板化进行传递

    template<class T>
    void printPerson3( T &p ){    //整个类模板化
        p.showPerson();
    }
    int main()
    {
        Person<string,int> p("牛魔王",18);
        printPerson3(p);
    }
    

3.5 类模板与继承

注意

  • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型

    template<class T>
    class Base {
    	T m;
    };
    //class Son : public Base { };//报错:必须要知道父类中的T类型才能继承给子类
    class Son : public Base<int> {
    
    };
    int main() {
    	Son s1;
    }
    
  • 如果不指定,编译器无法给子类分配内存

  • 如果想灵活指定出父类中T的类型,子类也需变为类模板

    template<class T>
    class Base {
    	T m;
    };
    
    template<class T1,class T2>
    class Son2 : public Base<T1> {
    	T2 obj;
    };
    
    int main()
    {
        Son2<int,char> S2;
    }
    

3.6 类模板成员函数类外实现

template<class T1,class T2>
class Person {
public:
	Person(T1 name, T2 age);
	void showPerson();

	T1 name;
	T2 age;
};

template<class T1,class T2>
Person<T1,T2>::Person(T1 name, T2 age)   //注意Person后必须写一个模板参数列表
{
		this->name = name;
		this->age = age;
}

template<class T1, class T2>
void Person<T1, T2>::showPerson()  //   //注意Person后必须写一个模板参数列表
{
		cout << "name:" << this->name << " age:" << this->age << endl;
}

int main() {
	Person<string,int> p("沙和尚",18);
	p.showPerson();
}

3.7 类模板分文件编写

类模板成员函数创建时机是在调用阶段,导致分文件编写时链接不到

解决方法:

  • 方法一:直接包含.cpp源文件
  • 方法二:将函数声明和实现写到同一个文件中,并更改后缀名为.hpphpp是约定的名称,并不是强制

3.8 类模板与友元

全局函数类内实现:直接在类内声明友元即可

全局函数类外实现:需要提前让编译器知道全局函数的存在

4. class和typename

在C++模板的声明中,classtypename都是用于定义类型参数的关键字,它们的功能几乎完全相同。也就是说,在模板的上下文中,使用classtypename来表示模板参数时,它们是可以互换的。

template <class T>
T add(T a, T b) {
    return a + b;
}

template <typename T>
T multiply(T a, T b) {
    return a * b;
}

上面两个模板函数,一个使用class声明类型参数,另一个使用typename,但它们的效果完全相同。T在这两种情况下都表示一个类型参数,编译器会根据你传递的参数类型来推导T的具体类型。

区别

尽管classtypename在大多数情况下是可以互换的,但它们之间有一些细微的区别和约定:

  • 历史原因:在最早的C++标准中,class是最早引入模板时用来定义类型参数的关键字。后来,C++标准委员会为了更加语义化和明确,才引入了typename。因此,class作为模板类型参数的关键字更多是历史遗留,而typename则从语义上更符合“表示类型”的含义。

  • 在嵌套依赖类型中,必须使用typename:在某些复杂的模板表达式中,特别是涉及到嵌套类型时,typename是必须的。例如,处理模板参数中包含的嵌套类型时,你必须用typename来明确告知编译器该嵌套依赖的标识符是一个类型,而不是一个变量或其他符号。

    例如

    template <typename T>
    void func() {
        typename T::value_type val;  // 必须用typename,表明T::value_type是一个类型
    }
    

    在这里,typename是必需的,因为T::value_type可能是一个类型名称。编译器需要通过typename来确认这个符号确实是一个类型,而不是某个成员变量。

总结

虽然classtypename可以在模板参数中互换,但很多C++开发者倾向于使用typename,因为它更能直观地表达模板参数是一个“类型”的含义。class这个关键字容易让人联想到类,而不是类型(尽管类也是一种类型)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值