本专栏目的
- 更新C/C++的基础语法,包括C++的一些新特性
前言
- 模板与元编程是C++的重要特点,也是难点,本人预计将会更新10期左右进行讲解,这二期是入门,讲神奇的的模板类;
- 考试周,会更新的比较慢😢😢😢😢😢😢;
- C语言后面也会继续更新知识点,如内联汇编;
- 欢迎收藏 + 关注,本人将会持续更新。
文章目录
类模板
🗂 定义
除了函数模板外,C++中还支持类模板。类模板是对成员数据类型不同的类的抽象 ,它说明了类的定义规则,一个类模板可以生成多种具体的类,更具我们上面模板的定义,函数模板也是生成了一个框架,实例化就是填充这个框架。
定义格式
与函数模板的定义形式类似, 类模板也是使用template关键字和尖括号"<>"中的模板形参进行说明,类的定义形式与普通类相同。
cpp
template<typename Type,...>
class className
{
//Code
};
- 类模板中的关键字含义与函数模板相同,
typename
可以换成class
- 类模板中的类型参数可用在类声明和类实现中。一旦声明了类模板就可以用类模板的模板参数声明类中的成员变量和成员函数,即在类中使用内置数据类型的地方都可以使用模板形参名来代替。
案例:
编写一个可以存储任意类型对象的类模板。
cpp
#include <iostream>
template <typename T>
class Warp
{
public:
Warp(T obj) :m_obj(obj) {}
// 输出,二元运算符重载需要用友员
friend std::ostream& operator<<(std::ostream& out, const Warp& other) {
out << other.m_obj;
return out;
}
private:
T m_obj;
};
int main()
{
Warp<int> obj(10);
std::cout << obj << std::endl; // 直接可以输出类
return 0;
}
单个类模板语法
💻 定义一个类模板非常简单,重要的是如何去用类模板定义对象。
- 如果没有指定模板的参数列表,编译器是会报错的
cpp
Wrap num(20); //error C2955: "Wrap": 使用 类 模板 需要 模板 参数列表
- 指定参数列表只需要在类模板名的后面加上<类型>即可
cpp
Wrap<int> num = 20;
Wrap<std::string> str(string("hello"));
👀 从上面可以看出,类模板只是模板,必须 要指定实例化类型,才可以使用。(Wrap只是模板名,Wrap<int>才是类名)。
🎶 注意
- 类模板不代表 一个具体的、实际的类,而代表一类类,只是代表一个代码框架而已,具体类需要实例化;
- 只有那些被调用的成员函数,才会产生这些函数的实例化代码。对于类模板,成员函数只有在被使用的时候才会被实例化 。显然,这样可以节省空间和时间,这个机制也说明了类和函数相分离的思想;
- 如果类模板
中含有静态成员
,那么用来实例化的每种类型
,都会实例化这些静态成员,因为这样代表这不同静态变量。
案例再来一个:
实现简易版Vector,可以存储任意类型的顺序表。
cpp
#include <iostream>
template <typename T>
class Vector
{
public:
Vector() {
m_data = new T[m_capacity]{};
}
void push_back(T data)
{
if (m_size >= m_capacity) {
T* p = new T[m_capacity + 10]{};
std::memcpy(p, m_data, sizeof(T) * m_size);
m_capacity += 10;
delete m_data;
m_data = p;
}
m_data[m_size++] = data;
}
T& operator[](const size_t& index) const
{
return m_data[index];
}
size_t size() const
{
return m_size;
}
public: // 下面案例有用
T* m_data{};
size_t m_size{ 0 };
size_t m_capacity{ 10 };
};
int main()
{
Vector<char> vector;
for (int i = 0; i <= 10; i++) {
vector.push_back(i + 'a');
}
for (int i = 0; i < vector.size(); i++) {
std::cout << vector[i] << " ";
}
return 0;
}
/* 输出:
a b c d e f g h i j k
*/
继承中的类模板
类模板派生普通类
子类是具体类,不是模板类,这个时候从模板类继承的时候,📚 📚📚📚📚 需要让编译器知道,父类的数据类型具体是什么,因为数据类型的本质,内存如何分配内存空间。
案例:
继承上面的Vector
类,实现字符串拼接。
cpp
#include <iostream>
template <typename T>
class Vector
{
public:
Vector() {
m_data = new T[m_capacity]{};
}
void push_back(T data)
{
if (m_size >= m_capacity) {
T* p = new T[m_capacity + 10]{};
std::memcpy(p, m_data, sizeof(T) * m_size);
m_capacity += 10;
delete m_data;
m_data = p;
}
m_data[m_size++] = data;
}
T& operator[](const size_t& index) const
{
return m_data[index];
}
size_t size() const
{
return m_size;
}
public:
T* m_data{};
size_t m_size{ 0 };
size_t m_capacity{ 10 };
};
// 实现字符串连接
class StringJoin : public Vector<std::string> // 显示继承,声明类型
{
public:
// 调用父类构造函数
using Vector<std::string>::Vector;
std::string join(const std::string& split)
{
std::string str;
for (int i = 0; i < size() - 1; i++) {
str.append(m_data[i]);
str.append(split); // 分割
}
str.append(m_data[size() - 1]);
return str;
}
};
int main()
{
StringJoin str;
str.push_back("Hello");
str.push_back("World");
std::cout << str.join(",") << std::endl;
return 0;
}
类模板派生模板类
这个意思就是说继承模板类的时候,子类也为模板类,这个时候声明类型就是T
,这个时候继承语法也是一样的,
cpp
// 模板类继承模板类
template<typename T>
class List : public Vector<T>
{
public:
using Vector<T>::Vector;
// 定义获取数据函数
T* data()const
{
return this->m_data;
}
};
代码看起来没有问题,但是在子类中使用父类的成员,会提示找不到标识符。
解决办法
1,通过this指针访问:this->m_data;
2,通过父类访问: Vector<\T>::m_data;
为什么会这样?
this有类型Vector<\T>,依赖的类型T。所以this有依赖类型。所以需要this->m_data做m_data一个从属名称。
💯 测试
c++
// 模板类继承模板类
template<typename T>
class List : public Vector<T>
{
public:
using Vector<T>::Vector;
// 定义获取数据函数
T* data()const
{
return this->m_data;
}
};
int main()
{
List<char> list;
list.push_back('a');
std::cout << *list.data();
return 0;
}
⛑ 结果
类模板特化
类模板特化分为全特化 和偏特化两种。
- 将模板中的所有参数确定化,叫做全特化。
- 而针对模板参数进一步进行条件限制的特化,叫做偏特化,而函数模板不支持偏特化,类模板才支持偏特化。
全特化
将模板中的所有参数确定化,即,所有类型模板参数都用具体类型代表,特化版本模板参数列表为空 template<>,在特化版本的类名后面加上<type,...>。
💁♂ **注意:**在原有结构上进行全特化,结构不能修改。
cpp
template<typename T,typename U>
struct Test
{
void show()
{
cout<<"非特化版本"<<endl;
}
};
//全特化版本
template<>
struct Test<int,int>
{
void show()
{
cout<<"int,int特化版本"<<endl;
}
};
//特化版本可以有任意多个
template<>
struct Test<double,string>
{
void show()
{
cout<<"double,string特化版本"<<endl;
}
};
//测试
int main()
{
Test<int,int> t;
t.show(); //int,int特化版本
Test<double, string> t1;
t1.show(); //double,string特化版本
Test<char, char> t2;
t2.show(); //非特化版本
return 0;
}
/*输出
int,int特化版本
double,string特化版本
非特化版本
*/
偏特化
而针对模板参数进一步进行条件限制的,即指定一部分模板参数用具体类型代替。
从模板参数数量上
cpp
//从模板参数数量上
template<typename T,typename U>
struct Test
{
void show()
{
cout<<"非特化版本"<<endl;
}
};
//局部特化
template<typename U>
struct Test<int,U>
{
void show()
{
cout<<"非特化版本"<<endl;
}
};
//测试
int main()
{
Test<int,string> tt;
tt.show(); //局部特化版本
return 0;
}、
/* 输出:
非特化版本
*/
从模板参数范围上
int -> int&)
cpp
//从模板参数范围上
template<typename T>
struct Test
{
void show()
{
cout<<"非特化版本"<<endl;
}
};
//const T
template<typename T>
struct Test<T&>
{
void show()
{
cout<<"T&特化版本"<<endl;
}
};
//T*
template<typename T>
struct Test<T*>
{
void show()
{
cout<<"T*特化版本"<<endl;
}
};
//测试
int main()
{
Test<int> t;
t.show(); //非特化版本
Test<int*> t1;
t1.show(); //T*特化版本
Test<int&> t2;
t2.show(); //T&特化版本
return 0;
}
模板声明和实现分离
使用C/C++进行编程时,一般会使用头文件以使定义和声明分离,并使得程序以模块方式组织。将函数声明、类的定义放在头文件中,而将函数实现以及类成员函数的定义放在独立的文件中。
单文件分离
首先,咱们来看一下,模板声明和实现分离在一个文件中。
友元函数
cpp
template<typename _Ty>
class Vector
{
public:
Vector();
Vector(const Vector& other);
void push_back(const _Ty& val);
_Ty& operator[](int index);
size_t size()const;
//友元函数内部声明
template<typename _Ty>
friend std::ostream& operator<<(std::ostream& out, const Vector<_Ty>& other);
protected:
_Ty* _base{ nullptr };
int _size{ 0 };
int _capacity{ 10 };
};
//成员函数外部实现
template<typename _Ty>
Vector<_Ty>::Vector()
{
_base = new _Ty[_capacity]{ _Ty() };
}
template<typename _Ty>
Vector<_Ty>::Vector(const Vector& other);
template<typename _Ty>
void Vector<_Ty>::push_back(const _Ty& val)
{
if (_size >= _capacity)
{
_Ty* p = new _Ty[_capacity + 10]{ _Ty() };
std::memcpy(p, _base, _capacity);
_capacity += 10;
delete _base;
_base = p;
}
_base[_size++] = val;
}
template<typename _Ty>
_Ty& Vector<_Ty>::operator[](int index)
{
return _base[index];
}
template<typename _Ty>
size_t Vector<_Ty>::size()const
{
return _size;
}
//友元函数类外实现
template<typename _Ty>
std::ostream& operator<<(std::ostream& out, const Vector<_Ty>& other)
{
for (int i = 0; i < other.size(); i++)
{
out << other._base[i] << ",";
}
return out;
}
多文件分离
SVector.h
cpp
#pragma once
#include<iostream>
template<typename _Ty>
class Vector
{
public:
Vector();
Vector(const Vector& other);
void push_back(const _Ty& val);
_Ty& operator[](int index);
size_t size()const;
//友元函数内部声明
template<typename _Ty>
friend std::ostream& operator<<(std::ostream& out, const Vector<_Ty>& other);
protected:
_Ty* _base{ nullptr };
int _size{ 0 };
int _capacity{ 10 };
};
SVector.cpp
cpp
#include "SVector.h"
//成员函数外部实现
template<typename _Ty>
Vector<_Ty>::Vector()
{
_base = new _Ty[_capacity]{ _Ty() };
}
template<typename _Ty>
Vector<_Ty>::Vector(const Vector& other);
template<typename _Ty>
void Vector<_Ty>::push_back(const _Ty& val)
{
if (_size >= _capacity)
{
_Ty* p = new _Ty[_capacity + 10]{ _Ty() };
std::memcpy(p, _base, _capacity);
_capacity += 10;
delete _base;
_base = p;
}
_base[_size++] = val;
}
template<typename _Ty>
_Ty& Vector<_Ty>::operator[](int index)
{
return _base[index];
}
template<typename _Ty>
size_t Vector<_Ty>::size()const
{
return _size;
}
//友元函数类外实现
template<typename _Ty>
std::ostream& operator<<(std::ostream& out, const Vector<_Ty>& other)
{
for (int i = 0; i < other.size(); i++)
{
out << other._base[i] << ",";
}
return out;
}
test_SVector.cpp
cpp
#include"SVector.h"
int main()
{
Vector<int> nums;
for (int i = 0; i < 5; i++)
{
nums.push_back(2 + i);
}
std::cout << nums << std::endl;
return 0;
}
运行报错:
cpp
error LNK2019: 无法解析的外部符号 "public: __cdecl 【Vector<int>::Vector<int>(void)】" (??0?$Vector@H@@QEAA@XZ),函数 main 中引用了该符号
error LNK2019: 无法解析的外部符号 "public: void __cdecl 【Vector<int>::push_back(int const &)】" (?push_back@?$Vector@H@@QEAAXAEBH@Z),函数 main 中引用了该符号
error LNK2019: 无法解析的外部符号 "class std::basic_ostream<char,struct std::char_traits<char> > & __cdecl 【operator<<<int>】(class std::basic_ostream<char,struct std::char_traits<char> > &,class Vector<int> const &)" (??$?6H@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@std@@AEAV01@AEBV?$Vector@H@@@Z),函数 main 中引用了该符号
报错原因如下:
编译器也编译了包含模板定义的源码文件test.cpp,但是该文件仅仅是模板的定义,而并没有真正的实例化出具体的函数来。因此在链接阶段,编译器进行符号查找时,发现源码文件中的符号,在所有二进制文件中都找不到相关的定义,因此就报错了。
解决方法
- 使用全特化版本,这个时候就不是模板类,而是一个具体函数;
- 将模板声明和定义放到
.hpp
文件。
编译模型
一,传统的编译模型
使用C/C++进行编程时,一般会使用头文件以使定义和声明分离,并使得程序以模块方式组织。将函数声明、类的定义放在头文件中,而将函数实现以及类成员函数的定义放在独立的文件中。
二,模板的编译模型
编译器并不是把模板编译成一个可以处理任何类型的单一实体;而是如果调用了模板的时候,编译器才产生特定类型的模板实例。
函数:
一般而言,当调用函数的时候,编译器只需要看到函数的声明。类似地,定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的。因此,应该将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。
模板则不同:
要进行实例化,编译器必须能够访问定义模板的源代码 。当调用
函数模板或类模板的成员函数的时候,编译器需要函数定义,需要那些通常放在源文件中的代码,但是模板只是容器,不是具体实现函数/类,这就造成了找不到。