【C++学习笔记】C+(3),2024年最新算法题+Golang+自定义View

x 坐标和 y 坐标的数据类型不确定,借助类模板可以将数据类型参数化,这样就不必定义多个类了。

注意:模板头和类头是一个整体,可以换行,但是中间不能有分号。

上面的代码仅仅是类的声明,我们还需要在类外定义成员函数。在类外定义成员函数时仍然需要带上模板头,格式为:

template<typename 类型参数1 , typename 类型参数2 , …>
返回值类型 类名<类型参数1 , 类型参数2, …>::函数名(形参列表){
//TODO:
}

第一行是模板头,第二行是函数头,它们可以合并到一行,不过为了让代码格式更加清晰,一般是将它们分成两行。

下面就对 Point 类的成员函数进行定义:

template<typename T1, typename T2> //模板头
T1 Point<T1, T2>::getX() const /函数头/ {
return m_x;
}
template<typename T1, typename T2>
void Point<T1, T2>::setX(T1 x){
m_x = x;
}
template<typename T1, typename T2>
T2 Point<T1, T2>::getY() const{
return m_y;
}
template<typename T1, typename T2>
void Point<T1, T2>::setY(T2 y){
m_y = y;
}

请读者仔细观察代码,除了 template 关键字后面要指明类型参数,类名 Point 后面也要带上类型参数,只是不加 typename 关键字了。另外需要注意的是,在类外定义成员函数时,template 后面的类型参数要和类声明时的一致。

使用类模板创建对象

上面的两段代码完成了类的定义,接下来就可以使用该类创建对象了。使用类模板创建对象时,需要指明具体的数据类型。请看下面的代码:

Point<int, int> p1(10, 20);
Point<int, float> p2(10, 15.5);
Point<float, char*> p3(12.4, “东经180度”);

与函数模板不同的是,类模板在实例化时必须显式地指明数据类型,编译器不能根据给定的数据推演出数据类型。

除了对象变量,我们也可以使用对象指针的方式来实例化:

Point<float, float> p1 = new Point<float, float>(10.6, 109.3);
Point<char
, char*> p = new Point<char, char*>(“东经180度”, “北纬210度”);

需要注意的是,赋值号两边都要指明具体的数据类型,且要保持一致。下面的写法是错误的:

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

综合示例

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

#include
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=”<getX()<<“, y=”<getY()<<endl;
return 0;
}

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

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

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

#include
#include
using namespace std;
template
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
CArray::CArray(int s):size(s)
{
if(s == 0)
ptr = NULL;
else
ptr = new T[s];
}
template
CArray::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
CArray::~CArray()
{
if(ptr) delete [] ptr;
}
template
CArray & CArray::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
void CArray::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 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 KaTeX parse error: Expected 'EOF', got '}' at position 14: this -> m_y; }̲ public functio…x){
$this -> m_x = KaTeX parse error: Expected 'EOF', got '}' at position 4: x; }̲ public functio…y){
$this -> m_y = $y;
}
private $m_x;
private $m_y;
}
$p = new Point(10, 20); //处理整数
echo $p->getX() . ", " . $p->getY() . “
”;
$p = new Point(24.56, “东京180度”); //处理小数和字符串
echo $p->getX() . ", " . $p->getY() . “
”;
$p = new Point(“东京180度”, “北纬210度”); //处理字符串
echo $p->getX() . ", " . $p->getY() . “
”;

运行结果:
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 void Swap(T *a, T *b){
T temp = *a;
*a = *b;
*b = temp;
}
//方案②:使用引用
template 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 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
using namespace std;
template void Swap(T &a, T &b); //模板①:交换基本类型的值
template 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 void Swap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}
template 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 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 void func1(T a, T b);
template void func2(T *buffer);
template void func3(const T &stu);
template void func4(T a);
template 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 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(10); //省略 T2 的类型
func<int, int>(20); //指明 T1、T2 的类型

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

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

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

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

template void func(T a, T b);

它的具体调用形式如下:

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

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

C++模板的显式具体化

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

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

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

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

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

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

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

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

函数模板的显式具体化

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

#include
#include
using namespace std;
typedef struct{
string name;
int age;
float score;
} STU;
//函数模板
template const T& Max(const T& a, const T& b);
//函数模板的显示具体化(针对STU类型的显示具体化)
template<> const STU& Max(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 const T& Max(const T& a, const T& b){
return a > b ? a : b;
}
template<> const STU& Max(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
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
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*, 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 //这里需要带上模板头
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
using namespace std;
template 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 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
#include
#include
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)]; //增加的内存足以容纳 2n 个元素
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 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);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
img

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

它命令编译器生成我们想要的代码。

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

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

template 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);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-DlfQ2s23-1712997865867)]
[外链图片转存中…(img-LQ7jNIAt-1712997865869)]
[外链图片转存中…(img-9Hx5qv22-1712997865870)]
[外链图片转存中…(img-ywZW8hS3-1712997865870)]
[外链图片转存中…(img-oGTYuVvr-1712997865871)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-TKa6o8lJ-1712997865871)]

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值