类模板的简单实例
author: hjjdebug
date: 2025年 05月 16日 星期五 15:06:00 CST
description: 类模板的简单实例
文章目录
- 1.实例代码:
- 2. 模板类写法
- 2.1 模板类的构造函数.
- 2.2 模板类中的语句
- 3. 模板类的实例化过程.
- 3.1 实例化的进一步试验.
- 4. 怎样调试constexpr 修饰的函数?
类模板的概念可能很复杂,假定你已经理解了一些基本概念.
这里用简单实例来巩固一些基本概念.
1.实例代码:
$ cat main.cpp
#include <stdio.h>
//定义一个类模板,带类型参数和非类型参数
//将来可以生成很多类
template <typename Func, int N>
struct ConstArray
{constexpr ConstArray(Func f) : data{} {for (int i = 0; i < N; ++i)data[i] = f(i);}int data[N];
};
//想调试,你可以去掉constexpr, 编译出debug版本调试
constexpr int myFunc(int i) { return i * 2; }/* 定义一个类模板实例, 实例化类模板, 型实结合的过程* 模板参数1,类型 ,传递实参deltype(&myFunc)* 模板参数2,整数 ,传递整数3** 定义类的对象名称arr,传递对象的构造参数&myFunc*/
//constexpr ConstArray<decltype(&myFunc),5> arr(&myFunc); // 放开该语句,编译时就能生成数组 {0, 2, 4, 6,8}:int main()
{int(*pTest1)(int) = myFunc;int(*pTest2)(int) = &myFunc;printf("pTest1:%p,pTest2:%p\n",pTest1,pTest2);//两种赋值都能工作,随便选一种吧,都各有各的道理,其实是一样的,选第二种吧.ConstArray<decltype(myFunc),5> arr(myFunc); //该对象执行时会被直接付给初值,数值是编译期调用构造计算好的.ConstArray<decltype(&myFunc),5> arr2(&myFunc); return 0;
}
运行:
./tt
pTest1:0x401136,pTest2:0x401136
2. 模板类写法
template <typename Func, int N>
struct ConstArray
template 是关键字, 表示模板, 模板就是模子, 就是由它可造出一堆东西来.
typename 是关键字, 表示模板类型, 也可以用class 关键字替换.
Func 是模板类型名称,是一个标识符, 你可以改成你喜欢的名字. 它代表了可变的类型.
int 是关键字, 是确定的类型为int
N 是标识符,可改为你喜欢的名字. 是非类型参数, 将来由固定的数值替换.
struct 是关键字, 代表定义的是类.
ConstArray 是标识符, 代表类模板的名称
与普通类定义比较,多了模板参数声明部分,这正式模板类的精华所在, 类型是不固定的,由参数确定类型.
模板类只注重算法,不注重类型,进一步把算法分离.
模板参数还可以传递固定类型的参数,例如这里的N,它是一个整形数, 叫非类型模板参数. 通常用来处理编译期常量
如果按定义读下来, 似乎模板类更自然, 但从意义而言,类模板更贴切,
类模板,模板类含义差不多,也就混用了.
2.1 模板类的构造函数.
constexpr ConstArray(Func f) : data{}
constexpr 是关键字, 表示函数可以在编译器调用.
ConstArray 与类同名,表示是构成函数
Func 是模板类传来的类型
f 是类型的实例,是对象, 其名称是标识符,可以随便起名.
data 是该类定义的 int 型数据成员. 是一个数组名称,数组大小为模板参数N
2.2 模板类中的语句
分析一条函数语句:
for (int i = 0; i < N; ++i) data[i] = f(i);
for循环意义比较明确,仅有f(i) 看着别扭, f 是我们传来的类型的实例, f(i)是什么意思?
如果f是一个函数指针,那f(i) 就是函数调用. 所以在实例化时要传递函数地址.
3. 模板类的实例化过程.
constexpr ConstArray<decltype(&myFunc),5> arr(&myFunc);
/* 定义一个类模板实例, 实现型实结合的过程
- 模板参数1,类型 ,类型是gcc 从传递的构造参数推导出来的deltype(&myFunc)
- 模板参数2,整数 ,传递整数5 , 表示只运算5次
- 定义类的对象名称arr,传递对象的构造参数&myFunc
我们看看gdb中打印的arr, 这就是gcc 我们编译的计算机认识的对象. 一目了然.
(gdb) p arr
$3 = {data = {0, 2, 4, 6, 8}
}
(gdb) ptype arr
type = const struct ConstArray<int (*)(int), 5> [with Func = int (*)(int)] {int data[5];public:ConstArray(Func);
}
类型的名称就有点长了,是模板类型 struct ConstArray<int (*)int,5> 类型
3.1 实例化的进一步试验.
我们把实例化改一改,如下:
constexpr ConstArray<decltype(&myFunc),5> arr(1);
编译出错:
error: invalid conversion from ‘int’ to ‘int ()(int)’ [-fpermissive]
22 | constexpr ConstArray<decltype(&myFunc),5> arr(1); // 编译时生成数组 {0, 2, 4, 6,8}:
| ^
| |
| int
咿! 它怎么知道我传1不对,要求传int()(int) 函数呢? 噢! 是 decltype(&myFunc)推导出来的.
重新实例化:
constexpr ConstArray<int,5> arr(1);
编译报错:
main.cpp:9:42: error: ‘f’ cannot be used as a function
9 | for (int i = 0; i < N; ++i) data[i] = f(i);
| ^~
gcc 抱怨我们传入的类型对象没法当函数来调用, 是的,我们传入的是整数1,当然不能当函数.
这样对传递的类型要求有了更清晰的认识.
可见模板类会对传递的类型进行检查, 对i语句语法进行检查, 如果gcc不理解,就会报错,
可见其使用还是很安全的. 是啊,生不成执行代码,gcc 肯定是不干的.
如果我不用编译期生成数据,而在运行期再生成数据,是怎样的过程呢?
反编译发现,竟是直接把数值付给类成员, 调用构造函数及回调函数根本就没有执行.
26 ConstArray<decltype(&myFunc),5> arr(&myFunc);
0x0000000000401151 <+27>: movl $0x0,-0x20(%rbp)
0x0000000000401158 <+34>: movl $0x2,-0x1c(%rbp)
0x000000000040115f <+41>: movl $0x4,-0x18(%rbp)
0x0000000000401166 <+48>: movl $0x6,-0x14(%rbp)
0x000000000040116d <+55>: movl $0x8,-0x10(%rbp)
可见gcc 在编译期,根据用户提供的线索,构造函数,回调函数,按解释性语句已经帮我们计算好了数据,
直接来构建了对象, gcc 很强大!
4. 怎样调试constexpr 修饰的函数?
此例我想看看构造过程,因为如果函数比较复杂,运行出错怎么办?
你只需要去掉constexpr 就可以了. debug版还可以跟入函数内部.
否则的话, 静态编译,你只能看到编译的结果而看不到具体的过程.
调试的时候,myFunc 的地址也可以看见了.
(gdb) p myFunc
$6 = {int (int)} 0x401136 <myFunc(int)>
它说myFunc 地址是0x401136
类型: 输入参数是int,输出参数是int的函数类型
(gdb) p &myFunc
函数名称本来就是地址0x401136, 函数名称再取地址是什么意思?
由调试知道, 它认为还是地址,地址没有变,但类型变了,是函数指针类型的地址,
$7 = (int (*)(int)) 0x401136 <myFunc(int)>
这与我们通常意义上的变量取地址是不一样的内涵!
变量取地址是变量所处的内存地址. 函数名称取地址其值还是函数的地址,这只能算是和gcc的约定了.
但这就要小心了,函数指针解引用,其值还是函数地址.
即从0x401136函数指针解引用还是得0x401136函数地址, 效果上好象调不调解引用都一样
(gdb) ptype myFunc
type = int (int)
(gdb) ptype &myFunc
type = int (*)(int)
int(int) 与 int(*) int 有什么区别呢?
前者为函数类型,后者为函数指针类型.如果一定要区分的话.
我们定义一个函数指针pTest,为其赋值即可调用函数.
int (*pTest1) (int) = myFunc;
int (*pTest2) (int) = &myFunc;
两种赋值方式都是可以通过编译的,且pTest1 完全等价于pTest2
c++中允许写为 pTest(5); 这种形式来调用函数
实例化类时我传myFunc,或传&myFunc 结果都是对的, 因为它们传的都是0x401136
而且其构造函数的参数类型f 都是
(gdb) p f
4 = (int (*)(int)) 0x401136 <myFunc(int)>
在这里得到了统一.
int (int) 类型的Func类型声明一个变量f, 居然是int (*) (int) 类型的, 看起来还是有一点点不爽,
所以正宗的还是传递&myFunc 吧. 这样它们的类型一致性更好.
而且了解到myFunc与&myFunc它们的值是一样的. 用着也就放心了.
按说gcc 应该加一种限制,把函数地址限定为一种会更好,
或者让函数名称代表有效地址,
或者让函数名称取地址代表有效地址,
让2者都有效,总有一种混乱的感觉. 但如果限制某一种,也有管得太宽的嫌疑.
算了,现实是允许这种有点混乱的赋值,知道了其内在实现,心理用着也就踏实了.