2024年最新【C++学习笔记】C+(4),看这篇文章就行了

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

//赋值号两边的数据类型不一致
Point<float, float> *p = new Point<float, int>(10.6, 109);
//赋值号右边没有指明数据类型
Point<float, float> *p = new Point(10.6, 109);

综合示例

【实例1】将上面的类定义和类实例化的代码整合起来,构成一个完整的示例,如下所示:

#include <iostream>
using namespace std;
template<class T1, class T2>  //这里不能有分号
class Point{
public:
    Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
    T1 getX() const;  //获取x坐标
    void setX(T1 x);  //设置x坐标
    T2 getY() const;  //获取y坐标
    void setY(T2 y);  //设置y坐标
private:
    T1 m_x;  //x坐标
    T2 m_y;  //y坐标
};
template<class T1, class T2>  //模板头
T1 Point<T1, T2>::getX() const /*函数头*/ {
    return m_x;
}
template<class T1, class T2>
void Point<T1, T2>::setX(T1 x){
    m_x = x;
}
template<class T1, class T2>
T2 Point<T1, T2>::getY() const{
    return m_y;
}
template<class T1, class T2>
void Point<T1, T2>::setY(T2 y){
    m_y = y;
}
int main(){
    Point<int, int> p1(10, 20);
    cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;
    Point<int, char*> p2(10, "东经180度");
    cout<<"x="<<p2.getX()<<", y="<<p2.getY()<<endl;
    Point<char*, char*> *p3 = new Point<char*, char*>("东经180度", "北纬210度");
    cout<<"x="<<p3->getX()<<", y="<<p3->getY()<<endl;
    return 0;
}

运行结果:
x=10, y=20
x=10, y=东经180度
x=东经180度, y=北纬210度

在定义类型参数时我们使用了 class,而不是 typename,这样做的目的是让读者对两种写法都熟悉。

【实例2】用类模板实现可变长数组。

#include <iostream>
#include <cstring>
using namespace std;
template <class T>
class CArray
{
    int size; //数组元素的个数
    T *ptr; //指向动态分配的数组
public:
    CArray(int s = 0);  //s代表数组元素的个数
    CArray(CArray & a);
    ~CArray();
    void push_back(const T & v); //用于在数组尾部添加一个元素v
    CArray & operator=(const CArray & a); //用于数组对象间的赋值
    T length() { return size; }
    T & operator[](int i)
    {//用以支持根据下标访问数组元素,如a[i] = 4;和n = a[i]这样的语句
        return ptr[i];
    }
};
template<class T>
CArray<T>::CArray(int s):size(s)
{
     if(s == 0)
         ptr = NULL;
    else
        ptr = new T[s];
}
template<class T>
CArray<T>::CArray(CArray & a)
{
    if(!a.ptr) {
        ptr = NULL;
        size = 0;
        return;
    }
    ptr = new T[a.size];
    memcpy(ptr, a.ptr, sizeof(T ) * a.size);
    size = a.size;
}
template <class T>
CArray<T>::~CArray()
{
     if(ptr) delete [] ptr;
}
template <class T>
CArray<T> & CArray<T>::operator=(const CArray & a)
{ //赋值号的作用是使"="左边对象里存放的数组,大小和内容都和右边的对象一样
    if(this == & a) //防止a=a这样的赋值导致出错
    return * this;
    if(a.ptr == NULL) {  //如果a里面的数组是空的
        if( ptr )
            delete [] ptr;
        ptr = NULL;
        size = 0;
        return * this;
    }
     if(size < a.size) { //如果原有空间够大,就不用分配新的空间
         if(ptr)
            delete [] ptr;
        ptr = new T[a.size];
    }
    memcpy(ptr,a.ptr,sizeof(T)*a.size);   
    size = a.size;
     return *this;
}
template <class T>
void CArray<T>::push_back(const T & v)
{  //在数组尾部添加一个元素
    if(ptr) {
        T *tmpPtr = new T[size+1]; //重新分配空间
    memcpy(tmpPtr,ptr,sizeof(T)*size); //拷贝原数组内容
    delete []ptr;
    ptr = tmpPtr;
}
    else  //数组本来是空的
        ptr = new T[1];
    ptr[size++] = v; //加入新的数组元素
}
int main()
{
    CArray<int> a;
    for(int i = 0;i < 5;++i)
        a.push_back(i);
    for(int i = 0; i < a.length(); ++i)
        cout << a[i] << " ";   
    return 0;
}

大话C++模板编程的来龙去脉

计算机编程语言种类繁多,目前能够查询到的有 600 多种,常用的不超过 20 种,TIOBE 每个月都会发布世界编程语言排行榜,统计前 50 名编程语言的市场份额以及它们的变动趋势。该榜单反映了编程语言的热门程度,程序员可以据此来检查自己的开发技能是否跟得上趋势,公司或机构也可以据此做出战略调整。

这些编程语言根据不同的标准可以分为不同的种类,根据“在定义变量时是否需要显式地指明数据类型”可以分为强类型语言和弱类型语言。

1) 强类型语言

强类型语言在定义变量时需要显式地指明数据类型,并且一旦为变量指明了某种数据类型,该变量以后就不能赋予其他类型的数据了,除非经过强制类型转换或隐式类型转换。典型的强类型语言有 C/C++JavaC# 等。

下面的代码演示了如何在 C/C++ 中使用变量:

int a = 100;  //不转换
a = 12.34;  //隐式转换(直接舍去小数部分,得到12)
a = (int)"http://c.biancheng.net";  //强制转换(得到字符串的地址) 

下面的代码演示了如何在 Java 中使用变量:

int a = 100;  //不转换
a = (int)12.34;  //强制转换(直接舍去小数部分,得到12)

Java 对类型转换的要求比 C/C++ 更为严格,隐式转换只允许由低向高转,由高向低转必须强制转换。

2) 弱类型语言

弱类型语言在定义变量时不需要显式地指明数据类型,编译器(解释器)会根据赋给变量的数据自动推导出类型,并且可以赋给变量不同类型的数据。典型的弱类型语言有 JavaScriptPythonPHP、Ruby、Shell、Perl 等。

下面的代码演示了如何在 JavaScript 中使用变量:

var a = 100;  //赋给整数
a = 12.34;  //赋给小数
a = "http://c.biancheng.net";  //赋给字符串
a = new Array("JavaScript","React","JSON");  //赋给数组

var 是 JavaScript 中的一个关键字,表示定义一个新的变量,而不是数据类型。

下面的代码演示了如何在 PHP 中使用变量:

$a = 100;  //赋给整数
$a = 12.34;  //赋给小数
$a = "http://c.biancheng.net";  //赋给字符串
$a = array("JavaScript","React","JSON");  //赋给数组

$ 是一个特殊符号,所有的变量名都要以 $ 开头。PHP 中的变量不用特别地定义,变量名首次出现即视为定义。

这里的强类型和弱类型是站在变量定义和类型转换的角度讲的,并把 C/C++ 归为强类型语言。另外还有一种说法是站在编译和运行的角度,并把 C/C++ 归为弱类型语言。本节我们只关注第一种说法。

类型对于编程语言来说非常重要,不同的类型支持不同的操作,例如class Student类型的变量可以调用 display() 方法,int类型的变量就不行。不管是强类型语言还是弱类型语言,在编译器(解释器)内部都有一个类型系统来维护变量的各种信息。

对于强类型的语言,变量的类型从始至终都是确定的、不变的,编译器在编译期间就能检测某个变量的操作是否正确,这样最终生成的程序中就不用再维护一套类型信息了,从而减少了内存的使用,加快了程序的运行。

不过这种说法也不是绝对的,有些特殊情况还是要等到运行阶段才能确定变量的类型信息。比如 C++ 中的多态,编译器在编译阶段会在对象内存模型中增加虚函数表、type_info 对象等辅助信息,以维护一个完整的继承链,等到程序运行后再执行一段代码才能确定调用哪个函数,这在《C++多态与虚函数》一章中进行了详细讲解。

对于弱类型的语言,变量的类型可以随时改变,赋予它什么类型的数据它就是什么类型,编译器在编译期间不好确定变量的类型,只有等到程序运行后、真的赋给变量一个值了,才能确定变量当前是什么类型,所以传统的编译对弱类型语言意义不大,因为即使编译了也有很多东西确定不下来。

弱类型语言往往是一边执行一边编译,这样可以根据上下文(可以理解为当前的执行环境)推导出很多有用信息,让编译更加高效。我们将这种一边执行一边编译的语言称为解释型语言,而将传统的先编译后执行的语言称为编译型语言。

强类型语言较为严谨,在编译时就能发现很多错误,适合开发大型的、系统级的、工业级的项目;而弱类型语言较为灵活,编码效率高,部署容易,学习成本低,在 Web 开发中大显身手。另外,强类型语言的 IDE 一般都比较强大,代码感知能力好,提示信息丰富;而弱类型语言一般都是在编辑器中直接书写代码。

为了展示弱类型语言的灵活,我们以 PHP 为例来实现上节中的 Point 类,让它可以处理整数、小数以及字符串:

class Point{
    public function Point($x, $y){  //构造函数
        $this -> m_x = $x;
        $this -> m_y = $y;
    }
    public function getX(){
        return $this -> m_x;
    }
    public function getY(){
        return $this -> m_y;
    }
    public function setX($x){
        $this -> m_x = $x;
    }
    public function setY($y){
        $this -> m_y = $y;
    }
    private $m_x;
    private $m_y;
}
$p = new Point(10, 20);  //处理整数
echo $p->getX() . ", " . $p->getY() . "<br />";
$p = new Point(24.56, "东京180度");  //处理小数和字符串
echo $p->getX() . ", " . $p->getY() . "<br />";
$p = new Point("东京180度", "北纬210度");  //处理字符串
echo $p->getX() . ", " . $p->getY() . "<br />";

运行结果:
10, 20
24.56, 东京180度
东京180度, 北纬210度

看,PHP 不需要使用模板就可以处理多种类型的数据,它天生对类型就不敏感。C++ 就不一样了,它是强类型的,比较“死板”,所以后来 C++ 开始支持模板了,主要就是为了弥补强类型语言“不够灵活”的缺点。

模板所支持的类型是宽泛的,没有限制的,我们可以使用任意类型来替换,这种编程方式称为泛型编程(Generic Programming)。相应地,可以将参数 T 看做是一个泛型,而将 int、float、string 等看做是一种具体的类型。除了 C++,Java、C#、Pascal(Delphi)也都支持泛型编程。

C++ 模板也是被迫推出的,最直接的动力来源于对数据结构的封装。数据结构关注的是数据的存储,以及存储后如何进行增加、删除、修改和查询操作,它是一门基础性的学科,在实际开发中有着非常广泛的应用。C++ 开发者们希望为线性表、链表、图、树等常见的数据结构都定义一个类,并把它们加入到标准库中,这样以后程序员就不用重复造轮子了,直接拿来使用即可。

但是这个时候遇到了一个无法解决的问题,就是数据结构中每份数据的类型无法提前预测。以链表为例,它的每个节点可以用来存储小数、整数、字符串等,也可以用来存储一名学生、教师、司机等,还可以直接存储二进制数据,这些都是可以的,没有任何限制。而 C++ 又是强类型的,数据的种类受到了严格的限制,这种矛盾是无法调和的。

要想解决这个问题,C++ 必须推陈出新,跳出现有规则的限制,开发新的技术,于是模板就诞生了。模板虽然不是 C++ 的首创,但是却在 C++ 中大放异彩,后来也被 Java、C# 等其他强类型语言采用。

C++ 模板有着复杂的语法,可不仅仅是前面两节讲到的那么简单,它的话题可以写一本书。C++ 模板也非常重要,整个标准库几乎都是使用模板来开发的,STL 更是经典之作。

STL(Standard Template Library,标准模板库)就是 C++ 对数据结构进行封装后的称呼。

C++函数模板的重载

当需要对不同的类型使用同一种算法(同一个函数体)时,为了避免定义多个功能重复的函数,可以使用模板。然而,并非所有的类型都使用同一种算法,有些特定的类型需要单独处理,为了满足这种需求,C++ 允许对函数模板进行重载,程序员可以像重载常规函数那样重载模板定义。

在《C++函数模板》一节中我们定义了 Swap() 函数用来交换两个变量的值,一种方案是使用指针,另外一种方案是使用引用,请看下面的代码:

//方案①:使用指针
template<typename T> void Swap(T *a, T *b){
    T temp = *a;
    *a = *b;
    *b = temp;
}
//方案②:使用引用
template<class T> void Swap(T &a, T &b){
    T temp = a;
    a = b;
    b = temp;
}

这两种方案都可以交换 int、float、char、bool 等基本类型变量的值,但是却不能交换两个数组。

对于方案①,调用函数时传入的是数组指针,或者说是指向第 0 个元素的指针,这样交换的仅仅是数组的第 0 个元素,而不是整个数组。数组和指针本来是不等价的,只是当函数的形参为指针时,传递的数组也会隐式地转换为指针,这在《C语言入门教程》中的《数组和指针绝不等价,数组是另外一种类型》《数组到底在什么时候会转换为指针》两节中做了详细讲解,不了解的读者请猛击链接深入学习。

对于方案②,假设传入的是一个长度为 5 的 int 类型数组(该数组的类型是 int [5]),那么 T 的真实类型为int [5]T temp会被替换为int [5] temp,这显然是错误的。另外一方面,语句a=b;尝试对数组 a 赋值,而数组名是常量,它的值不允许被修改,所以也会产生错误。总起来说,方案②会有两处语法错误。

交换两个数组唯一的办法就是逐个交换所有的数组元素,请看下面的代码:

template<typename T> void Swap(T a[], T b[], int len){
    T temp;
    for(int i=0; i<len; i++){
        temp = a[i];
        a[i] = b[i];
        b[i] = temp;
    }
}

在该函数模板中,最后一个参数的类型为具体类型(int),而不是泛型。并不是所有的模板参数都必须被泛型化。

下面是一个重载函数模板的完整示例:

#include <iostream>
using namespace std;
template<class T> void Swap(T &a, T &b);  //模板①:交换基本类型的值
template<typename T> void Swap(T a[], T b[], int len);  //模板②:交换两个数组
void printArray(int arr[], int len);  //打印数组元素
int main(){
    //交换基本类型的值
    int m = 10, n = 99;
    Swap(m, n);  //匹配模板①
    cout<<m<<", "<<n<<endl;
    //交换两个数组
    int a[5] = { 1, 2, 3, 4, 5 };
    int b[5] = { 10, 20, 30, 40, 50 };
    int len = sizeof(a) / sizeof(int);  //数组长度
    Swap(a, b, len);  //匹配模板②
    printArray(a, len);
    printArray(b, len);
    return 0;
}
template<class T> void Swap(T &a, T &b){
    T temp = a;
    a = b;
    b = temp;
}
template<typename T> void Swap(T a[], T b[], int len){
    T temp;
    for(int i=0; i<len; i++){
        temp = a[i];
        a[i] = b[i];
        b[i] = temp;
    }
}
void printArray(int arr[], int len){
    for(int i=0; i<len; i++){
        if(i == len-1){
            cout<<arr[i]<<endl;
        }else{
            cout<<arr[i]<<", ";
        }
    }
}

运行结果:
99, 10
10, 20, 30, 40, 50
1, 2, 3, 4, 5

C++函数模板的实参推断

在使用类模板创建对象时,程序员需要显式的指明实参(也就是具体的类型)。例如对于下面的 Point 类:

template<typename T1, typename T2> class Point;

我们可以在栈上创建对象,也可以在堆上创建对象:

Point<int, int> p1(10, 20);  //在栈上创建对象
Point<char*, char*> *p = new Point<char*, char*>("东京180度", "北纬210度");  //在堆上创建对象

因为已经显式地指明了 T1、T2 的具体类型,所以编译器就不用再自己推断了,直接拿来使用即可。

而对于函数模板,调用函数时可以不显式地指明实参(也就是具体的类型)。请看下面的例子:

//函数声明
template<typename T> void Swap(T &a, T &b);
//函数调用
int n1 = 100, n2 = 200;
Swap(n1, n2);
float f1 = 12.5, f2 = 56.93;
Swap(f1, f2);

虽然没有显式地指明 T 的具体类型,但是编译器会根据 n1 和 n2、f1 和 f2 的类型自动推断出 T 的类型。这种通过函数实参来确定模板实参(也就是类型参数的具体类型)的过程称为模板实参推断。

在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找类型参数的具体类型。

模板实参推断过程中的类型转换

对于普通函数(非模板函数),发生函数调用时会对实参的类型进行适当的转换,以适应形参的类型。这些转换包括:

  • 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
  • 派生类向基类的转换:也就是向上转型,请猛击《C++向上转型(将派生类赋值给基类)》了解详情。
  • const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
  • 数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
  • 用户自定的类型转换。

例如有下面两个函数原型:

void func1(int n, float f);
void func2(int *arr, const char *str);

它们具体的调用形式为:

int nums[5];
char *url = "http://c.biancheng.net";
func1(12.5, 45);
func2(nums, url);

对于 func1(),12.5 会从double转换为int,45 会从int转换为float;对于 func2(),nums 会从int [5]转换为int *,url 会从char *转换为const char *

而对于函数模板,类型转换则受到了更多的限制,仅能进行「const 转换」和「数组或函数指针转换」,其他的都不能应用于函数模板。例如有下面几个函数模板:

template<typename T> void func1(T a, T b);
template<typename T> void func2(T *buffer);
template<typename T> void func3(const T &stu);
template<typename T> void func4(T a);
template<typename T> void func5(T &a);

它们具体的调用形式为:

int name[20];
Student stu1("张华", 20, 96.5);  //创建一个Student类型的对象
func1(12.5, 30);  //Error
func2(name);  //name的类型从 int [20] 换转换为 int *,所以 T 的真实类型为 int
func3(stu1);  //非const转换为const,T 的真实类型为 Student
func4(name);  //name的类型从 int [20] 换转换为 int *,所以 T 的真实类型为 int *
func5(name);  //name的类型依然为 int [20],不会转换为 int *,所以 T 的真实类型为 int [20]

对于func1(12.5, 30),12.5 的类型为 double,30 的类型为 int,编译器不知道该将 T 实例化为 double 还是 int,也不会尝试将 int 转换为 double,或者将 double 转换为 int,所以调用失败。

请读者注意 name,它本来的类型是int [20]

  • 对于func2(name)func4(name),name 的类型会从 int [20] 转换为 int *,也即将数组转换为指针,所以 T 的类型分别为 int * 和 int。
  • 对于func5(name),name 的类型依然为 int [20],不会转换为 int *,所以 T 的类型为 int [20]。

可以发现,当函数形参是引用类型时,数组不会转换为指针。这个时候读者要注意下面这样的函数模板:

template<typename T> void func(T &a, T &b);

如果它的具体调用形式为:

int str1[20];
int str2[10];
func(str1, str2);

由于 str1、str2 的类型分别为 int [20] 和 int [10],在函数调用过程中又不会转换为指针,所以编译器不知道应该将 T 实例化为 int [20] 还是 int [10],导致调用失败。

为函数模板显式地指明实参(也就是具体的类型)

函数模板的实参推断是指「在函数调用过程中根据实参的类型来寻找类型参数的具体类型」的过程,这在大部分情况下是奏效的,但是当类型参数的个数较多时,就会有个别的类型无法推断出来,这个时候就必须显式地指明实参。

下面是一个实参推断失败的例子:

template<typename T1, typename T2> void func(T1 a){
    T2 b;
}
func(10);  //函数调用

func() 有两个类型参数,分别是 T1 和 T2,但是编译器只能从函数调用中推断出 T1 的类型来,不能推断出 T2 的类型来,所以这种调用是失败的,这个时候就必须显式地指明 T1、T2 的具体类型。

「为函数模板显式地指明实参」和「为类模板显式地指明实参」的形式是类似的,就是在函数名后面添加尖括号< >,里面包含具体的类型。例如对于上面的 func(),我们要这样来指明实参:

func<int, int>(10);

显式指明的模板实参会按照从左到右的顺序与对应的模板参数匹配:第一个实参与第一个模板参数匹配,第二个实参与第二个模板参数匹配,以此类推。只有尾部(最右)的类型参数的实参可以省略,而且前提是它们可以从传递给函数的实参中推断出来。

对于上面的 func(),虽然只有 T2 的类型不能自动推断出来,但是由于它位于类型参数列表的尾部(最右),所以必须同时指明 T1 和 T2 的类型。对代码稍微做出修改:

template<typename T1, typename T2> void func(T2 a){
    T1 b;
}
//函数调用
func<int>(10);  //省略 T2 的类型
func<int, int>(20);  //指明 T1、T2 的类型

由于 T2 的类型能够自动推断出来,并且它位于参数列表的尾部(最右),所以可以省略。

显式地指明实参时可以应用正常的类型转换

上面我们提到,函数模板仅能进行「const 转换」和「数组或函数指针转换」两种形式的类型转换,但是当我们显式地指明类型参数的实参(具体类型)时,就可以使用正常的类型转换(非模板函数可以使用的类型转换)了。

例如对于下面的函数模板:

template<typename T> void func(T a, T b);

它的具体调用形式如下:

func(10, 23.5);  //Error
func<float>(20, 93.7);  //Correct

在第二种调用形式中,我们已经显式地指明了 T 的类型为 float,编译器不会再为「T 的类型到底是 int 还是 double」而纠结了,所以可以从容地使用正常的类型转换了。

C++模板的显式具体化

C++ 没有办法限制类型参数的范围,我们可以使用任意一种类型来实例化模板。但是模板中的语句(函数体或者类体)不一定就能适应所有的类型,可能会有个别的类型没有意义,或者会导致语法错误。

例如有下面的函数模板,它用来获取两个变量中较大的一个:

template<class T> const T& Max(const T& a, const T& b){
    return a > b ? a : b;
}

请读者注意a > b这条语句,>能够用来比较 int、float、char 等基本类型数据的大小,但是却不能用来比较结构体变量、对象以及数组的大小,因为我们并没有针对结构体、类和数组重载>

另外,该函数模板虽然可以用于指针,但比较的是地址大小,而不是指针指向的数据,所以也没有现实的意义。

除了>+``-``*``/``==``<等运算符也只能用于基本类型,不能用于结构体、类、数组等复杂类型。总之,编写的函数模板很可能无法处理某些类型,我们必须对这些类型进行单独处理。

模板是一种泛型技术,它能接受的类型是宽泛的、没有限制的,并且对这些类型使用的算法都是一样的(函数体或类体一样)。但是现在我们希望改变这种“游戏规则”,让模板能够针对某种具体的类型使用不同的算法(函数体或类体不同),这在 C++ 中是可以做到的,这种技术称为模板的显示具体化(Explicit Specialization)

函数模板和类模板都可以显示具体化,下面我们先讲解函数模板的显示具体化,再讲解类模板的显示具体化。

函数模板的显式具体化

在讲解函数模板的显示具体化语法之前,我们先来看一个显示具体化的例子:

#include <iostream>
#include <string>
using namespace std;
typedef struct{
    string name;
    int age;
    float score;
} STU;
//函数模板
template<class T> const T& Max(const T& a, const T& b);
//函数模板的显示具体化(针对STU类型的显示具体化)
template<> const STU& Max<STU>(const STU& a, const STU& b);
//重载<<
ostream & operator<<(ostream &out, const STU &stu);
int main(){
    int a = 10;
    int b = 20;
    cout<<Max(a, b)<<endl;
   
    STU stu1 = { "王明", 16, 95.5};
    STU stu2 = { "徐亮", 17, 90.0};
    cout<<Max(stu1, stu2)<<endl;
    return 0;
}
template<class T> const T& Max(const T& a, const T& b){
    return a > b ? a : b;
}
template<> const STU& Max<STU>(const STU& a, const STU& b){
    return a.score > b.score ? a : b;
}
ostream & operator<<(ostream &out, const STU &stu){
    out<<stu.name<<" , "<<stu.age <<" , "<<stu.score;
    return out;
}

运行结果:
20
王明 , 16 , 95.5

本例中,STU 结构体用来表示一名学生(Student),它有三个成员,分别是姓名(name)、年龄(age)、成绩(score);Max() 函数用来获取两份数据中较大的一份。

要想获取两份数据中较大的一份,必然会涉及到对两份数据的比较。对于 int、float、char 等基本类型的数据,直接比较它们本身的值即可,而对于 STU 类型的数据,直接比较它们本身的值不但会有语法错误,而且毫无意义,这就要求我们设计一套不同的比较方案,从语法和逻辑上都能行得通,所以本例中我们比较的是两名学生的成绩(score)。

不同的比较方案最终导致了算法(函数体)的不同,我们不得不借助模板的显示具体化技术对 STU 类型进行单独处理。第 14 行代码就是显示具体化的声明,第 34 行代码进行了定义。

请读者注意第 34 行代码,Max<STU>中的STU表明了要将类型参数 T 具体化为 STU 类型,原来使用 T 的位置都应该使用 STU 替换,包括返回值类型、形参类型、局部变量的类型。

Max 只有一个类型参数 T,并且已经被具体化为 STU 了,这样整个模板就不再有类型参数了,类型参数列表也就为空了,所以模板头应该写作template<>

另外,Max<STU>中的STU是可选的,因为函数的形参已经表明,这是 STU 类型的一个具体化,编译器能够逆推出 T 的具体类型。简写后的函数声明为:

template<> const STU& Max(const STU& a, const STU& b);

函数的调用规则

回顾一下前面学习到的知识,在 C++ 中,对于给定的函数名,可以有非模板函数、模板函数、显示具体化模板函数以及它们的重载版本,在调用函数时,显示具体化优先于常规模板,而非模板函数优先于显示具体化和常规模板。

类模板的显式具体化

除了函数模板,类模板也可以显示具体化,并且它们的语法是类似的。

在《C++类模板》一节中我们定义了一个 Point 类,用来输出不同类型的坐标。在输出结果中,横坐标 x 和纵坐标 y 是以逗号,为分隔的,但是由于个人审美的不同,我希望当 x 和 y 都是字符串时以|为分隔,是数字或者其中一个是数字时才以逗号,为分隔。为了满足我这种奇葩的要求,可以使用显示具体化技术对字符串类型的坐标做特殊处理。

下面的例子演示了如何对 Point 类进行显示具体化:

#include <iostream>
using namespace std;
//类模板
template<class T1, class T2> class Point{
public:
    Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
    T1 getX() const{ return m_x; }
    void setX(T1 x){ m_x = x; }
    T2 getY() const{ return m_y; }
    void setY(T2 y){ m_y = y; }
    void display() const;
private:
    T1 m_x;
    T2 m_y;
};
template<class T1, class T2>  //这里要带上模板头
void Point<T1, T2>::display() const{
    cout<<"x="<<m_x<<", y="<<m_y<<endl;
}
//类模板的显示具体化(针对字符串类型的显示具体化)
template<> class Point<char*, char*>{
public:
    Point(char *x, char *y): m_x(x), m_y(y){ }
public:
    char *getX() const{ return m_x; }
    void setX(char *x){ m_x = x; }
    char *getY() const{ return m_y; }
    void setY(char *y){ m_y = y; }
    void display() const;
private:
    char *m_x;  //x坐标
    char *m_y;  //y坐标
};
//这里不能带模板头template<>
void Point<char*, char*>::display() const{
    cout<<"x="<<m_x<<" | y="<<m_y<<endl;
}
int main(){
    ( new Point<int, int>(10, 20) ) -> display();
    ( new Point<int, char*>(10, "东京180度") ) -> display();
    ( new Point<char*, char*>("东京180度", "北纬210度") ) -> display();
    return 0;
}

运行结果:
x=10, y=20
x=10, y=东京180度
x=东京180度 | y=北纬210度

请读者注意第 25 行代码,Point<char*, char*>表明了要将类型参数 T1、T2 都具体化为char*类型,原来使用 T1、T2 的位置都应该使用char*替换。Point 类有两个类型参数 T1、T2,并且都已经被具体化了,所以整个类模板就不再有类型参数了,模板头应该写作template<>

再来对比第 19、40 行代码,可以发现,当在类的外部定义成员函数时,普通类模板的成员函数前面要带上模板头,而具体化的类模板的成员函数前面不能带模板头。

部分显式具体化

在上面的显式具体化例子中,我们为所有的类型参数都提供了实参,所以最后的模板头为空,也即template<>。另外 C++ 还允许只为一部分类型参数提供实参,这称为部分显式具体化。

部分显式具体化只能用于类模板,不能用于函数模板。

仍然以 Point 为例,假设我现在希望“只要横坐标 x 是字符串类型”就以|来分隔输出结果,而不管纵坐标 y 是什么类型,这种要求就可以使用部分显式具体化技术来满足。请看下面的代码:

#include <iostream>
using namespace std;
//类模板
template<class T1, class T2> class Point{
public:
    Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
    T1 getX() const{ return m_x; }
    void setX(T1 x){ m_x = x; }
    T2 getY() const{ return m_y; }
    void setY(T2 y){ m_y = y; }
    void display() const;
private:
    T1 m_x;
    T2 m_y;
};
template<class T1, class T2>  //这里需要带上模板头
void Point<T1, T2>::display() const{
    cout<<"x="<<m_x<<", y="<<m_y<<endl;
}
//类模板的部分显示具体化
template<typename T2> class Point<char*, T2>{
public:
    Point(char *x, T2 y): m_x(x), m_y(y){ }
public:
    char *getX() const{ return m_x; }
    void setX(char *x){ m_x = x; }
    T2 getY() const{ return m_y; }
    void setY(T2 y){ m_y = y; }
    void display() const;
private:
    char *m_x;  //x坐标
    T2 m_y;  //y坐标
};
template<typename T2>  //这里需要带上模板头
void Point<char*, T2>::display() const{
    cout<<"x="<<m_x<<" | y="<<m_y<<endl;
}
int main(){
    ( new Point<int, int>(10, 20) ) -> display();
    ( new Point<char*, int>("东京180度", 10) ) -> display();
    ( new Point<char*, char*>("东京180度", "北纬210度") ) -> display();
    return 0;
}

运行结果:
x=10, y=20
x=东京180度 | y=10
x=东京180度 | y=北纬210度

本例中,T1 对应横坐标 x 的类型,我们将 T1 具体化为char*,第 25 行代码就是类模板的部分显示具体化。

模板头template<typename T2>中声明的是没有被具体化的类型参数;类名Point<char*, T2>列出了所有类型参数,包括未被具体化的和已经被具体化的。

类名后面之所以要列出所有的类型参数,是为了让编译器确认“到底是第几个类型参数被具体化了”,如果写作template<typename T2> class Point<char*>,编译器就不知道char*代表的是第一个类型参数,还是第二个类型参数。

C++模板中的非类型参数

模板是一种泛型技术,目的是将数据的类型参数化,以增强 C++ 语言(强类型语言)的灵活性。C++ 对模板的支持非常自由,模板中除了可以包含类型参数,还可以包含非类型参数,例如:

template<typename T, int N> class Demo{ };template<class T, int N> void func(T (&arr)[N]);

T 是一个类型参数,它通过classtypename关键字指定。N 是一个非类型参数,用来传递数据的值,而不是类型,它和普通函数的形参一样,都需要指明具体的类型。类型参数和非类型参数都可以用在函数体或者类体中。

当调用一个函数模板或者通过一个类模板创建对象时,非类型参数会被用户提供的、或者编译器推断出的值所取代。

在函数模板中使用非类型参数

在《C++函数模板的重载》一节中,我们通过 Swap() 函数来交换两个数组的值,其原型为:

template void Swap(T a[], T b[], int len);

形参 len 用来指明要交换的数组的长度,调用 Swap() 函数之前必须先通过sizeof求得数组长度再传递给它。

有读者可能会疑惑,为什么在函数内部不能求得数组长度,一定要通过形参把数组长度传递进去呢?这是因为数组在作为函数参数时会自动转换为数组指针,而sizeof只能通过数组名求得数组长度,不能通过数组指针求得数组长度,这在《数组和指针绝不等价,数组是另外一种类型》《数组到底在什么时候会转换为指针》两节中已经作了详细讲解。

多出来的形参 len 给编码带来了不便,我们可以借助模板中的非类型参数将它消除,请看下面的代码:

template<typename T, unsigned N> void Swap(T (&a)[N], T (&b)[N]){
    T temp;
    for(int i=0; i<N; i++){
        temp = a[i];
        a[i] = b[i];
        b[i] = temp;
    }
}

T (&a)[N]表明 a 是一个引用,它引用的数据的类型是T [N],也即一个数组;T (&b)[N]也是类似的道理。分析一个引用和分析一个指针的方法类似,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析,这一点已在《只需一招,彻底攻克C语言指针》中进行了讲解。

调用 Swap() 函数时,需要将数组名字传递给它:

int a[5] = { 1, 2, 3, 4, 5 };
int b[5] = { 10, 20, 30, 40, 50 };
Swap(a, b);

编译器会使用数组类型int来代替类型参数T,使用数组长度5来代替非类型参数N

下面是一个完整的示例:

#include <iostream>
using namespace std;
template<class T> void Swap(T &a, T &b);  //模板①:交换基本类型的值
template<typename T, unsigned N> void Swap(T (&a)[N], T (&b)[N]);  //模板②:交换两个数组
template<typename T, unsigned N> void printArray(T (&arr)[N]);  //打印数组元素
int main(){
    //交换基本类型的值
    int m = 10, n = 99;
    Swap(m, n);  //匹配模板①
    cout<<m<<", "<<n<<endl;
    //交换两个数组
    int a[5] = { 1, 2, 3, 4, 5 };
    int b[5] = { 10, 20, 30, 40, 50 };
    Swap(a, b);  //匹配模板②
    printArray(a);
    printArray(b);
    return 0;
}
template<class T> void Swap(T &a, T &b){
    T temp = a;
    a = b;
    b = temp;
}
template<typename T, unsigned N> void Swap(T (&a)[N], T (&b)[N]){
    T temp;
    for(int i=0; i<N; i++){
        temp = a[i];
        a[i] = b[i];
        b[i] = temp;
    }
}
template<typename T, unsigned N> void printArray(T (&arr)[N]){
    for(int i=0; i<N; i++){
        if(i == N-1){
            cout<<arr[i]<<endl;
        }else{
            cout<<arr[i]<<", ";
        }
    }
}

运行结果:
99, 10
10, 20, 30, 40, 50
1, 2, 3, 4, 5

printArray() 也使用了非类型参数,这样只传递数组名字就能够打印数组元素了。

在类模板中使用非类型参数

C/C++ 规定,数组一旦定义后,它的长度就不能改变了;换句话说,数组容量不能动态地增大或者减小。这样的数组称为静态数组(Static array)。静态数组有时候会给编码代码不便,我们可以通过自定义的 Array 类来实现动态数组(Dynamic array)。所谓动态数组,是指数组容量能够在使用的过程中随时增大或减小。

动态数组的完整实现代码如下:

#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
template<typename T, int N>
class Array{
public:
    Array();
    ~Array();
public:
    T & operator[](int i);  //重载下标运算符[]
    int length() const { return m_length; }  //获取数组长度
    bool capacity(int n);  //改变数组容量
private:
    int m_length;  //数组的当前长度
    int m_capacity;  //当前内存的容量(能容乃的元素的个数)
    T *m_p;  //指向数组内存的指针
};
template<typename T, int N>
Array<T, N>::Array(){
    m_p = new T[N];
    m_capacity = m_length = N;
}
template<typename T, int N>
Array<T, N>::~Array(){
    delete[] m_p;
}
template<typename T, int N>
T & Array<T, N>::operator[](int i){
    if(i<0 || i>=m_length){
        cout<<"Exception: Array index out of bounds!"<<endl;
    }
    return m_p[i];
}
template<typename T, int N>
bool Array<T, N>::capacity(int n){
    if(n > 0){  //增大数组
        int len = m_length + n;  //增大后的数组长度
        if(len <= m_capacity){  //现有内存足以容纳增大后的数组
            m_length = len;
            return true;
        }else{  //现有内存不能容纳增大后的数组
            T *pTemp = new T[m_length + 2 * n * sizeof(T)];  //增加的内存足以容纳 2*n 个元素
            if(pTemp == NULL){  //内存分配失败
                cout<<"Exception: Failed to allocate memory!"<<endl;
                return false;
            }else{  //内存分配成功
                memcpy( pTemp, m_p, m_length*sizeof(T) );
                delete[] m_p;
                m_p = pTemp;
                m_capacity = m_length = len;
            }
        }
    }else{  //收缩数组
        int len = m_length - abs(n);  //收缩后的数组长度
        if(len < 0){
            cout<<"Exception: Array length is too small!"<<endl;
            return false;
        }else{
            m_length = len;
            return true;
        }
    }
}
int main(){
    Array<int, 5> arr;
    //为数组元素赋值
    for(int i=0, len=arr.length(); i<len; i++){
        arr[i] = 2*i;
    }
   
    //第一次打印数组
    for(int i=0, len=arr.length(); i<len; i++){
        cout<<arr[i]<<" ";
    }
    cout<<endl;
   
    //扩大容量并为增加的元素赋值
    arr.capacity(8);
    for(int i=5, len=arr.length(); i<len; i++){
        arr[i] = 2*i;
    }
    //第二次打印数组
    for(int i=0, len=arr.length(); i<len; i++){
        cout<<arr[i]<<" ";
    }
    cout<<endl;
    //收缩容量
    arr.capacity(-4);
    //第三次打印数组
    for(int i=0, len=arr.length(); i<len; i++){
        cout<<arr[i]<<" ";
    }
    cout<<endl;
    return 0;
}

运行结果:
0 2 4 6 8
0 2 4 6 8 10 12 14 16 18 20 22 24
0 2 4 6 8 10 12 14 16

Array 是一个类模板,它有一个类型参数T和一个非类型参数N,T 指明了数组元素的类型,N 指明了数组长度。

capacity() 成员函数是 Array 类的关键,它使得数组容量可以动态地增加或者减小。传递给它一个正数时,数组容量增大;传递给它一个负数时,数组容量减小。

之所以能通过[ ]来访问数组元素,是因为在 Array 类中以成员函数的形式重载了[ ]运算符,并且返回值是数组元素的引用。如果直接返回数组元素的值,那么将无法给数组元素赋值。

非类型参数的限制

非类型参数的类型不能随意指定,它受到了严格的限制,只能是一个整数,或者是一个指向对象或函数的指针(也可以是引用)。引用和指针在本质上是一样的,我们已在《引用在本质上是什么,它和指针到底有什么区别》中讲到。

  1. 当非类型参数是一个整数时,传递给它的实参,或者由编译器推导出的实参必须是一个常量表达式,例如102 * 3018 + 23 - 4等,但不能是nn + 10n + m等(n 和 m 都是变量)。

对于上面的 Swap() 函数,下面的调用就是错误的:

int len;
cin>>len;
int a[len];
int b[len];
Swap(a, b);

对上面的 Array 类,以下创建对象的方式是错误的:

int len;
cin>>len;
Array<int, len> arr;

这两种情况,编译器推导出来的实参是 len,是一个变量,而不是常量。

  1. 当非类型参数是一个指针(引用)时,绑定到该指针的实参必须具有静态的生存期;换句话说,实参必须存储在虚拟地址空间中的静态数据区。局部变量位于栈区,动态创建的对象位于堆区,它们都不能用作实参。

C++模板的实例化

模板(Templet)并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一张“图纸”。模板不会占用内存,最终生成的函数或者类才会占用内存。由模板生成函数或类的过程叫做模板的实例化(Instantiate),相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例(Instantiation)。

在学习模板以前,如果想针对不同的类型使用相同的算法,就必须定义多个极其相似的函数或类,这样不但做了很多重复性的工作,还导致代码维护困难,用于交换两个变量的值的 Swap() 函数就是一个典型的代表。而有了模板后,这些工作都可以交给编译器了,编译器会帮助我们自动地生成这些代码。从这个角度理解,模板也可以看做是编译器的一组指令,它命令编译器生成我们想要的代码。

模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码。也就是说,编译器会根据传递给类型参数的实参(也可以是编译器自己推演出来的实参)来生成一个特定版本的函数或类,并且相同的类型只生成一次。实例化的过程也很简单,就是将所有的类型参数用实参代替。

例如,给定下面的函数模板和函数调用:

template<typename T> void Swap(T &a, T &b){
    T temp = a;
    a = b;
    b = temp;
}
int main(){
    int n1 = 100, n2 = 200, n3 = 300, n4 = 400;
    float f1 = 12.5, f2 = 56.93;
   
    Swap(n1, n2);  //T为int,实例化出 void Swap(int &a, int &b);
    Swap(f1, f2);  //T为float,实例化出 void Swap(float &a, float &b);
    Swap(n3, n4);  //T为int,调用刚才生成的 void Swap(int &a, int &b);
    return 0;
}

编译器会根据不同的实参实例化出不同版本的 Swap() 函数。对于Swap(n1, n2)调用,编译器会生成并编译一个 Swap() 版本,其中 T 被替换为 int:

void Swap(int &a, int &b){
    int temp = a;
    a = b;
    b = temp;
}

对于Swap(f1, f2)调用,编译器会生成另一个 Swap() 版本,其中 T 被替换为 float。对于Swap(n3, n4)调用,编译器不会再生成新版本的 Swap() 了,因为刚才已经针对 int 生成了一个版本,直接拿来使用即可。

另外需要注意的是类模板的实例化,通过类模板创建对象时并不会实例化所有的成员函数,只有等到真正调用它们时才会被实例化;如果一个成员函数永远不会被调用,那它就永远不会被实例化。这说明类的实例化是延迟的、局部的,编译器并不着急生成所有的代码。

通过类模板创建对象时,一般只需要实例化成员变量和构造函数。成员变量被实例化后就能够知道对象的大小了(占用的字节数),构造函数被实例化后就能够知道如何初始化了;对象的创建过程就是分配一块大小已知的内存,并对这块内存进行初始化。

请看下面的例子:

#include <iostream>
using namespace std;
template<class T1, class T2>
class Point{
public:
    Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
    T1 getX() const{ return m_x; }
    void setX(T1 x){ m_x = x; }
    T2 getY() const{ return m_y; };
    void setY(T2 y){ m_y = y; };
    void display() const;
private:
    T1 m_x;
    T2 m_y;
};
template<class T1, class T2>
void Point<T1, T2>::display() const{
    cout<<"x="<<m_x<<", y="<<m_y<<endl;
}
int main(){
    Point<int, int> p1(10, 20);
    p1.setX(40);
    p1.setY(50);
    cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;
    Point<char*, char*> p2("东京180度", "北纬210度");
    p2.display();
    return 0;
}

运行结果:
x=40, y=50
x=东京180度, y=北纬210度

p1 调用了所有的成员函数,整个类会被完整地实例化。p2 只调用了构造函数和 display() 函数,剩下的 get 函数和 set 函数不会被实例化。

值得提醒的是,Point<int, int>Point<char*, char*>是两个相互独立的类,它们的类型是不同的,不能相互兼容,也不能自动地转换类型,所以诸如p1 = p2;这样的语句是错误的,除非重载了=运算符。

将C++模板应用于多文件编程

在将函数应用于多文件编程时,我们通常是将函数定义放在源文件(.cpp文件)中,将函数声明放在头文件(.h文件)中,使用函数时引入(#include命令)对应的头文件即可。

编译是针对单个源文件的,只要有函数声明,编译器就能知道函数调用是否正确;而将函数调用和函数定义对应起来的过程,可以延迟到链接时期。正是有了链接器的存在,函数声明和函数定义的分离才得以实现。

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

const{ return m_x; }
void setX(T1 x){ m_x = x; }
T2 getY() const{ return m_y; };
void setY(T2 y){ m_y = y; };
void display() const;
private:
T1 m_x;
T2 m_y;
};
template<class T1, class T2>
void Point<T1, T2>::display() const{
cout<<“x=”<<m_x<<“, y=”<<m_y<<endl;
}
int main(){
Point<int, int> p1(10, 20);
p1.setX(40);
p1.setY(50);
cout<<“x=”<<p1.getX()<<“, y=”<<p1.getY()<<endl;
Point<char*, char*> p2(“东京180度”, “北纬210度”);
p2.display();
return 0;
}


运行结果:  
 x=40, y=50  
 x=东京180度, y=北纬210度


p1 调用了所有的成员函数,整个类会被完整地实例化。p2 只调用了构造函数和 display() 函数,剩下的 get 函数和 set 函数不会被实例化。


值得提醒的是,`Point<int, int>`和`Point<char*, char*>`是两个相互独立的类,它们的类型是不同的,不能相互兼容,也不能自动地转换类型,所以诸如`p1 = p2;`这样的语句是错误的,除非重载了`=`运算符。


## 将C++模板应用于多文件编程


在将函数应用于多文件编程时,我们通常是将函数定义放在源文件(`.cpp`文件)中,将函数声明放在头文件(`.h`文件)中,使用函数时引入(`#include`命令)对应的头文件即可。


编译是针对单个源文件的,只要有函数声明,编译器就能知道函数调用是否正确;而将函数调用和函数定义对应起来的过程,可以延迟到链接时期。正是有了链接器的存在,函数声明和函数定义的分离才得以实现。



[外链图片转存中...(img-HZuhvhhc-1715664845214)]
[外链图片转存中...(img-d3BnCvAu-1715664845215)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值