【C++模版初阶】告别重复造轮子!让代码“活”起来~
C++模版初阶
每次写相同逻辑、只换类型的代码,真的又累又啰嗦。而 C++ 模板,就是来帮我们偷懒的——一套代码,通吃所有类型,从此告别重复CV。今天就从零基础,轻松搞定模板初阶。ദ്ദി˶ー̀֊ー́ )✧
文章目录
- C++模版初阶
- 1. 泛型编程
- 2. 函数模版
- 2.1 函数模版概念
- 2.2 函数模版格式
- 2.3 函数模版的原理
- 2.4 函数模版的实例化
- 2.4.1 隐式实例化
- 2.4.2 显示实例化
- 2.5 模版参数的匹配原则
- 3. 类模版
- 3.1 类模版的定义格式
- 3.2 成员函数的定义:放在类里还是类外?
- 3.2 1 类内定义(简单直接)
- 3.2.2 类外定义(需要一点小技巧)
- 3.3 一个大坑:声明和定义不要分开放两个文件
- 3.4 怎么使用类模板?—— 实例化
- 结语:
1. 泛型编程
你是否也曾陷入过这样的困境:为了实现一个功能,只是参数类型不同,就要把同样的逻辑反复写好几遍?比如交换两个变量的值,int写一遍,double写一遍,char再写一遍……
voidSwap(int&left,int&right){inttemp=left;left=right;right=temp;}voidSwap(double&left,double&right){doubletemp=left;left=right;right=temp;}voidSwap(char&left,char&right){chartemp=left;left=right;right=temp;}看着是不是很眼熟?没错,这就是函数重载。虽然它能解决问题,但缺点也显而易见:
- 代码臃肿:每增加一种类型,就得手动新增一个函数,复用率极低。
- 维护噩梦:如果交换逻辑需要修改,你得同时改好几个地方,漏一个就是 bug!
那有没有一个“万能模具”,告诉编译器一个模子,让它根据不同的类型自动生成对应的代码呢?
有的兄弟,有的!ƪ(˘⌣˘)ʃ
那就是 C++ 的模板,泛型编程的核心工具。
2. 函数模版
2.1 函数模版概念
函数模板代表了一个函数家族,该函数模板与类型无关,它本身不是一个真正的函数, 而是告诉编译器如何根据你传入的参数类型产生函数的特定类型版本。
2.2 函数模版格式
template<typename T1,typename T2,......,typename Tn> 返回值类型 函数名(参数列表){ }例如:
template<typenameT>voidSwap(T&left,T&right){T temp=left;left=right;right=temp;}敲黑板:
template关键字:表明这是一个模板。<typename T>:T 是一个类型参数,你可以把它想象成一个占位符,将来会被int、double等真实类型替代。typename也可以用class,效果完全一样。
现在,你只需要维护这一份代码,编译器会在你调用Swap(a, b)时,根据a和b的实际类型,自动生成对应版本的Swap函数。
2.3 函数模版的原理
当你写下
Swap(x, y)时,编译器会:
- 查看
x和y的类型(比如都是double)。- 将模板中的
T全部替换成double。- 生成一份处理
double类型交换的完整代码。整个过程不需要你动手,全自动完成。
2.4 函数模版的实例化
模板写好了,怎么用?有两种方式让编译器真正生成代码:隐式实例化和显式实例化。
2.4.1 隐式实例化
让编译器自己根据实参的类型去猜T是什么。
template<classT>TAdd(constT&left,constT&right){returnleft+right;}intmain(){inta=3,b=5;doublec=2.5,d=1.2;Add(a,b);// 编译器推导 T 为 int,生成 int 版本Add(c,d);// 编译器推导 T 为 double,生成 double 版本// 下面的调用会出问题:一个 int,一个 double,T 该是什么?// Add(a, c); // 编译错误!类型不明确return0;}如果参数类型不统一怎么办?你可以手动“协调”一下:
Add(a,(int)c);// 把 double 强转成 intAdd((double)a,c);// 把 int 强转成 double2.4.2 显示实例化
用尖括号<类型>直接告诉编译器你要用哪种类型,不用它猜。
Add<int>(a,c);// 强制 T 为 int,c 会被隐式转换为 intAdd<double>(a,c);// 强制 T 为 double,a 会被隐式转换为 double显式实例化尤其适用于模板参数无法从函数参数中推导的场景(比如返回值类型与参数无关):
template<classT>T*CreateArray(intn){returnnewT[n];}intmain(){// 无法从参数推导 T,必须显式指定double*arr=CreateArray<double>(10);return0;}2.5 模版参数的匹配原则
有时候,你既写了模板,又写了一个同名的普通函数,编译器会怎么选?
敲黑板:
- 如果普通函数完全匹配,优先调用普通函数——编译器觉得你手动写的肯定有特殊意图。ᴖᗜᴖ
- 如果模板能生成一个更匹配的版本,那就调用模板生成的版本——模板更灵活。
- 普通函数支持隐式类型转换,模板函数一般不做转换(因为模板匹配要求严格)。
来看个例子:
// 普通函数intAdd(intx,inty){return(x+y)*10;// 故意乘 10,便于区分}// 函数模板template<classT>TAdd(T x,T y){returnx+y;}intmain(){inta=1,b=2;doublec=1.5,d=2.5;cout<<Add(a,b)<<endl;// 调用普通函数,输出 (1+2)*10 = 30cout<<Add(c,d)<<endl;// 模板生成 double 版本,输出 4.0cout<<Add(a,c)<<endl;// 模板生成更匹配的 double 版本?不,这里会报错或需要显式实例化// 若改为 Add<double>(a, c),则强制使用模板,输出 2.5return0;}小提示:如果希望总是优先使用模板,可以写成
Add<>(a, b),空尖括号表示“就要模板,不要普通函数”。
3. 类模版
3.1 类模版的定义格式
template<class T1,class T2,......,class Tn> class 类模版名 { //类内成员定义 };例如:
template<typenameT>classStack{public:Stack(size_t capacity=4);voidPush(constT&data);// 其他成员函数...private:T*_array;// 指向动态数组的指针,数组元素类型为 Tsize_t _capacity;size_t _size;};这里的T还是一个类型占位符。你可以叫它T,也可以叫Type, 只要统一就行。编译器会在你真正使用这个类的时候,把T替换成真实的类型(比如int、double)。
注意:
typename和class在这里完全等价,用哪个看个人习惯。不过,老派程序员好像更偏爱class(¯▽¯)ゞ
3.2 成员函数的定义:放在类里还是类外?
3.2 1 类内定义(简单直接)
直接把函数体写在类里面,和普通类一模一样:
template<typenameT>classStack{public:voidPush(constT&data){// 直接写扩容和赋值逻辑if(_size==_capacity){// 扩容...}_array[_size++]=data;}// ...};这种方式最省心,不会有额外的语法烦恼。
3.2.2 类外定义(需要一点小技巧)
有时候你想把声明和定义分开,让类的“接口”看起来更清晰。这时每个成员函数在类外定义时,都必须再次声明模板参数,并且用类名<T>::来限定作用域。
template<typenameT>voidStack<T>::Push(constT&data){if(_size==_capacity){// 扩容:新开一块空间,复制过去,释放旧空间T*tmp=newT[_capacity*2];for(size_t i=0;i<_size;++i)tmp[i]=_array[i];delete[]_array;_array=tmp;_capacity*=2;}_array[_size++]=data;}看到那个Stack<T>::了吗?因为Stack现在是一个模板名,而不是一个具体的类。只有Stack<int>、Stack<double>才是真正的类。所以定义成员函数时必须告诉编译器:这个函数属于Stack<T>这个家族。
3.3 一个大坑:声明和定义不要分开放两个文件
写普通类时,我们习惯把声明放.h,定义放.cpp。但是对模板类,这样做几乎必然导致链接错误。
原因很简单:编译器在编译.cpp文件时,如果看不到模板的完整定义(比如Push的实现),就无法为Stack<int>生成真正的代码。等链接时,就会报“找不到Stack<int>::Push的符号”。
解决方案:把类模板的声明和所有成员函数的定义都放在同一个头文件里(通常就叫
Stack.h)。
3.4 怎么使用类模板?—— 实例化
类模板和函数模板的一个显著区别是:类模板不支持隐式实例化。你必须显式地告诉编译器,你要用哪种类型。
intmain(){// 创建一个存放 int 的栈Stack<int>st1;st1.Push(10);st1.Push(20);// 创建一个存放 double 的栈Stack<double>st2;st2.Push(1.1);st2.Push(2.2);// 也可以用在堆上Stack<char>*pst=newStack<char>(100);pst->Push('A');// ...deletepst;return0;}Stack<int>就是类模板的一个实例化结果,它是一个实实在在的类。Stack<double>是另一个完全不同的类。它们各自拥有独立的代码,互不干扰。
结语:
今天的内容到这里就结束了,希望你能有所收获~
代码无bug,学习不迷路,我们下篇再见!(•̀ᴗ•́)و
