泛型编程(函数模板/类模板)——c++

25 篇文章 0 订阅
本文详细介绍了C++中的泛型编程,包括函数模板和类模板的使用,以及它们的区别。通过实例展示了如何实现通用的交换函数和数组排序。还探讨了模板的局限性,如不能对自定义类型直接操作,以及如何通过运算符重载和模板特化来解决这些问题。此外,讨论了类模板中的成员函数创建时机、作为函数参数、继承和类外实现,并解释了友元函数在类内和类外的声明与实现。
摘要由CSDN通过智能技术生成

泛型编程


函数模板


c++提供了两种模板机制,一种是函数模板,另一种是类模板。

函数模板:实际上就是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就成为函数模板。凡是函数题体相同的函数都可以用这个模板代替,只需要定义一次即可。在调用函数的时候系统会根据实参的类型来取代模板中的虚拟类型,从而实现不同函数的功能

比如说要实现一个交换函数,但是不但要求int类型数据交换,还要求double等类型数据交换,为了避免写多个函数实现,可以使用这个通用函数。


利用模板实现通用交换函数:

template<typename T>其中,template是模板的关键字,typename是关键字,T代表一个通用的数据类型(告诉下面的编译器如果下面出现了’T’不要报错)

void Swap(int &a,int  &b)//常规的交换代码
{
	int temp = a;
	a = b;
	b = temp;
}

//开始创建通用函数
template<typename T>
void mySwap(T &a,T&b)
{
	T temp = a;
	a = b;
	b = temp;
}

int main()//测试
{
	int a = 10;
	int b = 22;
    double c = 3.14;
    double d = 4.13;
	mySwap(a, b);
    mySwap(c, d);
	cout << "a的值是"<<a << endl;//22
    cout << "c的值是"<<c << endl;//4.13
	return 0;
}

这样只需要声明实现一个函数就可以

上面的写法是自动类型推导,就是编译器自己对写的代码进行推导,将a,b的数据类型都推到出来,必须推导出是一致的T数据类型才可以正常的使用模板。如果一个是char,一个是int就能用,会报错。

试了一下int和double,发现会将 double 强转为 int 类型,丢失精度,所以虽然不报错但是也是不可取的。



还有一种写法:显示指定类型

mySwap<int>(a, b);

仅仅是不需要再进行推导,T还是要设置的。

注意1:char数据可以传换成int数据,但是char数据类型的引用不能转换为int类型的引用。

int main()//测试
{
	int a = 10l;
	char c = 'a';
	a = c;
	cout << a << endl;//a的ascii码:97
	return 0;
}

上面的代码是可以的。

void mySwap( T &a,T &b)
{
	T temp = a;
	a = b;
	b = temp;
}

int main()//测试
{
	int a = 10;
	char c = 'a';
	mySwap<int>(a, c);
	cout << a << endl;
	return 0;
}

这样的代码就会报错,因为引用是不能更换类型的。如果码mySwap的参数去掉&符号,那么可以运行成功,但是没有意义,因为

传的只是值,不会影响到外面了。



注意2:模板不能单独使用,必须指出T的类型才可以用。

template<typename T>这行代码只能保证紧接着的下面这个函数可以使用T,函数下面的另一个函数是不能使用T的。

进而:

template<typename T> 
void mySwap()
{
}

int main()
{
    mySwap();
}

这样是会报错的,因为没有址出T的类型(不指出T的类型怎么分配空间?)

有两种方法指出T的类型,就是上面的两种方法:自动类型检测,将函数设置为有参函数,然后通过对函数传进去的参数类型检测进而得到参数的类型。另一种就是直接设定:mySwap()。不论是什么函数模板,只有像这样确定了T的类型才能调用。



例子:通用函数(所有类型的数组排序)

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<string>


template<class T>
void mySwap(T& a, T& b)
{
	T temp = a;
	a = b;
	b = temp;
}

template<typename T>
void mySort(T* arr,int len)
{
	for (int i = 0; i < len; i++)
	{
		int max = i;
		for (int j = i+1; j < len; j++)//找出这一轮中最大的数,赋值给了max。
		{
			if (arr[j] > arr[max])
			{
				max = j;
			}
		}
		if (i != max)//如果不一样,就换,一样就继续。
		{
			mySwap(arr[i], arr[max]);
		}
	}
}

int main()
{
	int arr[] = { 1,3,2,4 };
	char arr2[] = "abcd";
	int len = sizeof(arr) / sizeof(arr[0]);
	int len2 = strlen(arr2);//不加/0用strlen,加的话就用sizeof
	mySort(arr2, len2);
	for (int i = 0; i < len; i++)
	{
		cout <<" "<< arr2[i];
	}
	
	return 0;
}


模板机制和局限性

1,编译器并不是把函数模板处理成能够处理任何类型的函数(只能处理内置数据类型)。如果处理自定义类型Person的数据,编译器是不理解的。

2,函数通过具体的类型产生不同的函数。(通过函数模板产生的函数称为模板函数。)

template <typename T>
void mtPrint(T a, T b)
{
	cout << "函数模板调用" << endl;
}


void myPrint(int a, int b)
{
	cout << "模板函数调用" << endl;
}

上面的是函数模板,下面的是模板函数。通过模板来生成函数(模板无法调用,但是如果规定了类型,那么就变成了可以调用的函数)

3,编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译(看看有错没),在确定了类型调用的地方对单数替代后的代码进行编译(看看有错没)


局限性:

模板并不是通用的,如果函数的参数是两个数组,而函数体的实现是赋值操作。那么肯定不对,因为数组不能对数组赋值。如果参数是Person类的,函数体是要求比大小,那么也是不成立的。所以说内置的数据类型一般都是可以操作的,但是自定义的数据类型是不可以直接操作的。

那怎么解决局限性问题呢?

在比较Perosn类对象的例子中:

第一种方法:就是对运算符重载,将==重载为分别比较姓名和年龄然后返回结果

第二种方法:利用具体化技术,实现对自定义数据类型提供特殊模板

template<typename T>
bool myCompare(T&a,T&b)
{
	if (a == b)
		return true;
	else
		return false;
}

这样的代码肯定是有局限性的,如果T的数据类型是Person那就不能直接用==比较了

//利用具体化技术,实现对自定义数据类型提供特殊模板。
template<> bool myCompare(Person& a, Person& b)
{
	if (a.m_age == b.m_age && a.m_name == b.m_name)
	{
		return true;
	}
	else
		return false;
}
int main()
{
	Person p1("Tom", 19);
	Person p2("Tom", 20);
	int ret = myCompare(p1, p2);
	cout << ret << endl;
	return 0;
} 

template<>代表这个是要使用具体化技术,然后传进来的对象就可以按照自己规定的方式来返回值了。

上面的代码写不出来了

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<string>

class Person
{
public:
   string m_name;
   int m_age;
   Person(string name,int age)
   {
   	this->m_name = name;
   	this->m_age = age;
   }
};
template<typename T> bool myCompare(T & a, T & b)
{
   if (a.m_age == b.m_age && a.m_name == b.m_name)
   {
   	return true;
   }
   else
   	return false;
}
int main()
{
   string s = "Tom";
   Person p1(s, 19);
   Person p2(s, 19);
   int ret = myCompare(p1, p2);//传进去的是Person类,那么T就是Person。
   cout << ret << endl;
   return 0;
}



类模板


函数模板和类模板的区别
template<class NAMETYPE,class AGETYPE>
class Person
{
public:
	Person(NAMETYPE name, AGETYPE age)
	{
		this m_name = name;
		this m_age = age;
	}
	NAMETYPE m_name;
	AGETYPE m_age;
};

int main()

	//自动类型推导:会报错
	Person p1("悟空", 100);
}

类模板不能使用自动类型推导,如果直接将这两个数据传进去不会自动推导,而是直接报错,在这一点上类模板与函数模板是不同的。(这个也可以理解,string 和char*不好区分等)

Person <string,int> p1 ("悟空", 100);

将指定类型写在对象的前面,对象类型的后面。

类模板和函数模板区别:

1,类模板不可以使用自动类型推导,只能显示指定类型。

2,类模板中的类型可以附上默认值,这样在后面的调用过程中加不加类型都可以。

template<class NAMETYPE,class AGETYPE = int>
class Person
{
public:
	Person(NAMETYPE name, AGETYPE age)
	{
		this->m_name = name;
		this->m_age = age;
	}
	NAMETYPE m_name;
	AGETYPE m_age; 
};

int main()
{
	Person<string>p1("悟空", 100);//下面就只需要一个string即可
	cout << p1.m_age << endl;
	return 0;
}



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


类模板中的成员函数不是一开始就创建好的。

class Person1
{
public:
	void showPerson1()
	{
		cout << "Person1 show的调用" << endl;
	}
};
class Person2
{
public:
	void showPerson2()
	{
		cout << "Person2 show的调用" << endl;
	}
};

template<class T>
class myClass
{
public:
	T obj;
	void func1()
	{
		obj.showPerson1();
	}
	void func2()
	{
		obj.showPerson2();
	}

};
int main()
{
	myClass<Person1> p1;
	p1.func1();//这个能调用
	p1.func2();//这个不能调用
	return 0;
}

在没有创建对象的时候,生成代码编译器不会报错(虽然很明显两个func中的obj不是用一个类型),在程序运行的时候才开始创建成员函数,在确定了T类型以后如果同时调用肯定是错误的,但是只调用一个就不会报错。



类模板做函数参数

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

	T2 m_age;
	T1 m_name;
};

void doWork()//在这个函数中调用对象中的成员函数,要把对象传进去。
{
    
}
int main()
{
	string s("XiaoMing");
	Perosn <string, int> p1("XiaoMing", 18);
	return 0;
}

如果要将对象作为参数传进函数,有三种方法:

第一种:指定传入的类型

void doWork(Person <string,int> &p)

注意中间的类型一定要写,如果不写那么传递的就是一个普通对象引用,而不是函数模板。

void doWork(Person <string,int> &p)//在这个函数中调用对象中的成员函数,要把对象传进去。
{
    p.showPerson();
}

第二种:

这一种和上面的很像,也是直接传进来,不过是将传进来的模板钟的类型进行模板化。

template <typename T1,typename T2>
void doWork(Person<T1,T2>&p)
{
	p.showPerson();
}
//传不传引用都行,只是调用访问,不需要改变。

第三种:

将整个类都进行模板化,(将传进来的对象前面的整个类型都模板化)

template <typename T>
void doWork(T &p)
{
	p.showPerson();
}

因为都是数据类型,所以可以进行模板化。

第一种没有使用模板,直接传入的对象,这个对象的类型是Person<string,int>。

第二种对类中数据类型的模板化,这个对象的类型是Person<T1,T2>.

第三种对整个类的数据类行进行模板化,这个对象的类型是T。

ps:如果第二种中想知道T1,T2的数据类型,可以调用代码:

template <typename T1, typename T2>
void doWork(Person<T1, T2>& p)
{
	cout << typeid(T1).name() << endl;//string,名字很长
	cout << typeid(T2).name() << endl;//int
	p.showPerson();
}

ps:如果想知道第三种中的T的类型,也是相同的方法。结果是class Person<string,int>



类模板中的继承

template<typename T>
class Base
{
public:
	T name;
};

class Son : public Base
{

};

这样写会直接报错,因为没有确定父类中的通用数据类型是什么,如果无法确定是什么类型,编译器怎么为子类分配空间?

解决方法:

template<typename T>
class Base
{
public:
	T m_A;
};

class Son : public Base<int>//直接再后面加上类型即可
{
};

但是如果这样,就相当于将这个通用类型写死了,规定了固定的4个字节的大小。如果用户想字节规定数据类型或者子类中仍然有自己的通用类型的数据,那么就需要进行修改:给子类也规定一下类模板。

template<typename T>
class Base
{
public:
	T m_A;
};

template<typename T1,typename T2>
class Son : public Base<T2>
{
public :
	T1 m_B;
};
//创建对象
int main
{
    Son <int ,double> s;//自己规定类型
    reutnr 0;
}

如果父类是模板,即便没有用规定的T,那么子类继承的时候也需要在继承的时候后面加个类型T1作继承。



类模板中的(成员/构造函数)类外实现

不想在类内实现构造函数和成员函数,想在类外实现。

正常:

template<class T1, class T2>
class Person
{
public:
	Person(T1 name, T2 age)
	{
		this->m_name = name;
		this->m_age = age;
	}

	void showPerson()
	{
		cout << this->m_name << " " << this->m_age << endl;
	}

	T1 m_name;
	T2 m_age;
};

int main()
{
	Person<string, int> p("Tom",19);
	p.showPerson();
	return 0;
}

在类外实现:

template<class T1,class T2>
Person<T1,T2>:: Person(T1 name, T2 age)
{       
        this->m_name = name;
		this->m_age = age;
}
//因为在类外,所以就不认识T1,T2了,应该在加一次T1,T2。然后在规定范围的时候,表明是Person中的,那么也需要在Person的后面加个类模型<T1,T2>.x`

//showPerson也是同样的道理,虽然showPerson中没有用到通用类型,但是它是类模板中的成员函数,所以也需要标注上类模板。
template<class T1,class T2>
void Person<T1,T2>:: showPerson()
{
    cout << this->m_name << " " << this->m_age << endl;
}


分文件编写

头文件

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<string>

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

	void showPerson();

	T1 m_name;
	T2 m_age;
};

函数实现文件

#include"Person.h"

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


template<typename T1,typename T2>
void Person<T1,T2>:: showPerson()
{
	cout << this->m_age << " " << this->m_name << endl;
}

测试文件

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<string>
#include"Person.cpp"


int main()
{
	Person<string, int> p ("xiaoming", 18);
	p.showPerson();
	return 0;
}

如果测试文件中依然是引头文件,那么代码就会报错。必须要引实现文件。

因为.h中的Person类不是一般的类,它是类模板,而类模板中的成员函数并不是生成代码的时候就创建好,而是运行的时候才会创建好函数。
一般的文件运行:先看头文件,然后再看函数实现文件去对应头文件中的函数,最后是测试文件。但是这里的头文件中的函数声明完不去找实现,而是直接进入测试(自己不主动找实现),导致编译器自始至终都没有看见这两个函数的实现,所以也不会去创建.这两个函数,所以链接的时候链接不到函数。

总结:类模板中的成员函数一开始不会创建,导致编译器没见到过cpp文件中的函数实现代码,所以在链接的时候链接不到。

以上方法不推荐,推荐以下:一般遇到这种情况,不把就不会将头文件和实现文件拆开了,可以将实现文件的内容放到头文件中,然后将头文件的名字的后缀改成hpp(类模板专用,说明函数的声明和实现都在这个文件中,但是分开写的),然后在测试文件中引文件就直接引这个hpp文件即可。



友元问题(创建全局函数)

通过全局函数打印访问 类中的(私有)信息。

全局函数分为两种:类内实现和类外实现。

类内实现


template<class T1,class T2>
class Person
{
public:

	Person(T1 name, T2 age)
	{
		this->m_age = age;
		this->m_name = name;
	}

	friend void Print(Person<T1,T2> &p)//案例一
	{
		cout << "调用成功"<<p.m_age << endl;
	}

	void Print2()//案例二
	{
		cout << "成功" << endl;
	}

private:
	T1 m_name;
	T2 m_age;
};

int main()
{
	Person<string, int> p("Tom", 18);
	Print(p);//调用案例一
	//Print2();//调用案例二
	return 0;
}

上方代码中有两个案例,第一个案例就是使用了类模板的全局(友元)函数。案例二就是普通的成员函数。

这个类内的全局函数需要传入一个本类模板的对象,而且还需要加个friend。(调用的时候就像一个全局函数)如果将friend去掉,那么就变成了成员函数,需要对象的调用。

如果将友元(全局)函数写在类内部,那么就可以同时声明和实现。


类外实现

错误案例:

template<class T1,class T2>
class Person
{
public:

	Person(T1 name, T2 age)
	{
		this->m_age = age;
		this->m_name = name;
	}

	friend void Print(Person<T1, T2>& p);//案例一
	
	
private:
	T1 m_name;
	T2 m_age;
};

template<typename T1,typename T2>
void Print(Person<T1, T2>& p)//案例一
{
	cout << "类外实现" << " "<<p.m_name << end;
}

int main()
{
	Person<string,int>p("Tom", 18);
	Print(p);
	return 0;
}

因为有通用数据类型,决定了下面的Person类是一个类模板,其中friend不是函数模板,它只是个普通的函数声明。但是下面的”实现“不是普通函数的实现,而是模板(用了通用数据类型),导致运行的时候想找到普通函数实现没有找到,只找了模板,这样就会报错。这个时候需要告诉编译器,下面的这个”实现“是模板,那么就在类中的声明加个<>(空模板参数列表),说明声明的就是个模板

friend void Print<>(Person<T1, T2>& p);

但是还是报错,继续再改:

一但是模板的声明,那么就需要让编译器提前看到这个模板声明。

下面实现的是函数模板,这个和类中的全局函数不一样(类模板中的friend函数是普通的成员函数,而类模板外实现的函数就是函数模板了(有通用数据类型,类内的不用考虑这个通用数据类型)),所以如果要实现类模板,需要先让编译器看到这个类模板,将声明写到类模板的外面。

void Print(Person<T1, T2>& p);

发现T1,T2未知,所以对这个声明也需要模板化。

template<typename T1,typename T2>
void Print(Person<T1, T2>& p);

然后发现这个Person没有见过(Person类的实现在它的下面),所以对这个类模板声明一下。

template<class T1,class T2>
class Person;

这样,这个类外实现的全局函数就可以成功调用了。

(比较麻烦)

template<class T1,class T2>
class Person;

template<typename T1,typename T2>
void Print(Person<T1, T2>& p);


template<class T1,class T2>
class Person
{
public:
	Person(T1 name, T2 age)
	{
		this->m_age = age;
		this->m_name = name;
	}

	friend void Print<>(Person<T1, T2>& p);//案例一
	
	
private:
	T1 m_name;
	T2 m_age;
};

template<typename T1,typename T2>
void Print(Person<T1, T2>& p)//案例一
{
	cout << "类外实现" << " "<<p.m_name << endl;
}

int main()
{
	Person<string,int>p("Tom", 18);
	Print(p);
	return 0;
}

有一个稍微简单的写法:在类前面声明类模板的时候将实现也写在一块,那么下面就不需要再实现了。

template<class T1,class T2>
class Person;

template<typename T1,typename T2>
void Print(Person<T1, T2>& p)
{
	cout << "类外实现" << " " << p.m_name << endl;
}


template<class T1,class T2>
class Person
{
public:
	Person(T1 name, T2 age)
	{
		this->m_age = age;
		this->m_name = name;
	}

	friend void Print<>(Person<T1, T2>& p);//案例一
	 
private:
	T1 m_name;
	T2 m_age;
};

int main()
{
	Person<string,int>p("Tom", 18);
	Print(p);
	return 0;
}

总之,如果想写全局(友元)函数,类内实现比较容易,类外实现比较复杂。





完整代码:

类内实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<string>

template <typename T1, typename T2>
class Person
{
public:
	
	Person(T1 name, T2 age)
	{
		this->m_name = name;
		this->m_age = age;
	}
	friend void Print(Person<T1, T2>& p)
	{
		cout << "调用成功:" << "姓名:" << p.m_name << "  " << "年龄" << p.m_age << endl;
	}

private:
	T1 m_name;
	T2 m_age;
};

int main()
{
	Person<string, int> p("小明", 18);
	Print(p);
	return 0;
}

类外实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<string>


template<typename T1,typename T2>
class Person;

template<typename T1, typename T2>
 void Print(Person<T1, T2>& p);

template <typename T1,typename T2>
class Person
{
public:
	Person(T1 name, T2 age)
	{
		this->m_name = name;
		this->m_age = age;
	}
	friend void Print<>(Person<T1,T2>&p);

private:
	T1 m_name;
	T2 m_age;
};

template<typename T1,typename T2>
void Print(Person<T1, T2> &p)
{
	cout << "调用成功:" << "姓名:" << p.m_name << "  " << "年龄" << p.m_age << endl;
}

int main()
{
	Person<string, int> p("小明", 18);
	Print(p);
	return 0;
}
  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 18
    评论
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是小明同学啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值