• 为什么模板的声明与定义不能分离?


    目录

    一、模板的好处与注意事项

    二、 声明定义为什么不能不放一起?

    一、模板的好处与注意事项

            函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型的模具,所以其实模板是将本来应该我们做的重复的事情交给了编译器;

            在编译器编译阶段,对应模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。

    模板的好处从下面代码可以体现:

    1. template<typename T>
    2. void Swap(T& left, T& right)
    3. {
    4. T temp = left;
    5. left = right;
    6. right = temp;
    7. }
    8. int main()
    9. {
    10. int a = 3;
    11. int b = 4;
    12. Swap(a, b);
    13. cout << a << " " << b << endl;
    14. double c = 2.4;
    15. double d = 3.5;
    16. Swap(c, d);
    17. cout << c << " " << d << endl;
    18. return 0;
    19. }

     

             以上不用写两遍Swap,Swapint与Swapdouble,只需利用模板,然后由编译器进行转换处理即可。

    再看一段代码:

    1. template<typename T>
    2. T Add(const T& left, const T& right)
    3. {
    4. return left + right;
    5. }
    6. int main()
    7. {
    8. int a1 = 10, a2 = 20;
    9. double d1 = 10.0, d2 = 20.0;
    10. Add(a1, a2);
    11. Add(d1, d2);
    12. Add(a1, d2);
    13. return 0;
    14. }

    注意:

    1. typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)

    2. 对于语句Add(a1, d2);
        该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
        通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
        编译器无法确定此处到底该将T确定为int 或者 double类型而报错
        注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅

        此时有三种处理方式:1. 用户自己来强制转化 2. 使用显式实例化3.使用多个模板参数
        1)   cout << Add(a1, (int)d2) << endl;

         2)  使用显式实例化;cout << Add(a, b) << endl;
         显式实例化:在函数名后的<>中指定模板参数的实际类型

    1. template<typename T>
    2. T Add(const T& left, const T& right)
    3. {
    4. return left + right;
    5. }
    6. int main(void)
    7. {
    8. int a = 10;
    9. double b = 22.4;
    10. // 显式实例化
    11. cout << Add<int>(a, b) << endl;
    12. //显式实例化:在函数名后的<>中指定模板参数的实际类型
    13. return 0;
    14. }

            3)使用多个模板参数

     注意:这样也是无法推导出模板参数类型的,不要想当然的认为能推导出返回值是int*。

    1. template<class T>
    2. T* func(int n)
    3. {
    4. return new T[n];
    5. }
    6. int main()
    7. {
    8. //int* p = func(10);//未能为T推导模板参数
    9. double* p = func<double>(10);
    10. return 0;
    11. }
    12. /*函数模板的类型一般是编译器根据实参传递给形参,推演出来的
    13. 如果不能自动推演,那么我们就需要显示实例化,指定模板参数*/

     再来看一个笔试题:

            模板运行时不检查数据类型,也不保证类型安全,相当于类型的宏替换。        

    1. 下面有关C++中为什么用模板类的原因,描述错误的是? C
    2. A.可用来创建动态增长和减小的数据结构
    3. B.它是类型无关的,因此具有很高的可复用性
    4. C.它运行时检查数据类型,保证了类型安全
    5. D.它是平台无关的,可移植性
    1. A.模板可以具有非类型参数,用于指定大小,可以根据指定的大小创建动态结构
    2. B.模板最重要的一点就是类型无关,提高了代码复用性
    3. C.模板运行时不检查数据类型,也不保证类型安全,相当于类型的宏替换,故错误
    4. D.只要支持模板语法,模板的代码就是可移植的

             模板类是一个家族,编译器的处理会分别进行两次编译,其处理过程跟普通类不一样

    类模板的格式:

    1. template<class T1, class T2, ...... , class Tn>
    2. class 类模板名
    3. {
    4. // 类内成员
    5. };

    类模板的实例化:

            类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。

    1. // Vector类名,Vector才是类型
    2. Vector<int> s1;
    3. Vector<double> s2;

     对于如下代码还需要注意:类模板中函数放在类外进行定义时,需要加模板参数列表

    Vector::~Vector()

    1. //动态顺序表
    2. //注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
    3. template<class T>
    4. class Vector
    5. {
    6. public:
    7. Vector(size_t capacity = 10)
    8. : _pData(new T[capacity])
    9. , _size(0)
    10. , _capacity(capacity)
    11. {}
    12. // 使用析构函数演示:在类中声明,在类外定义。
    13. ~Vector();
    14. private:
    15. T* _pData;
    16. size_t _size;
    17. size_t _capacity;
    18. };
    19. template <class T>
    20. Vector::~Vector()
    21. {
    22. if (_pData)
    23. delete[] _pData;
    24. _size = _capacity = 0;
    25. }

    对应笔试题:类模板中的成员函数全是模板函数

    1. 下列关于模板的说法正确的是( )
    2. A.模板的实参在任何时候都可以省略
    3. B.类模板与模板类所指的是同一概念
    4. C.类模板的参数必须是虚拟类型的
    5. D.类模板中的成员函数全是模板函数
    6. A.不一定,参数类型不同时有时需要显示指定类型参数
    7. B.类模板是一个类家族,模板类是通过类模板实例化的具体类
    8. C.C++中类模板的声明格式为template<模板形参表声明><类声明>,并且类模板的成员函数都是模板函数
    9. D.正确,定义时都必须通过完整的模板语法进行定义

     再来看一段代码,当存在专门处理int的函数时

     从上运行结果可以知道他不会去调用模板。

    如果显示调用,就会去调用模板。

    二、 声明定义为什么不能不放一起?

    test.c

    1. #include "template.h"
    2. int main()
    3. {
    4. Rect<float> rect(1.1f, 2.2f, 3.3f, 4.4f);
    5. rect.display();
    6. return 0;
    7. }

     template.h

    1. #pragma once
    2. #include
    3. using namespace std;
    4. template<typename T>
    5. class Rect
    6. {
    7. public:
    8. Rect(T l = 0.0f, T t = 0.0f, T r = 0.0f, T b = 0.0f) :
    9. left_(l), top_(t), right_(r), bottom_(b) {}
    10. void display();
    11. private:
    12. T left_;
    13. T top_;
    14. T right_;
    15. T bottom_;
    16. };

     template.cpp

    1. #include "template.h"
    2. template<typename T>
    3. void Rect::display()
    4. {
    5. std::cout << left_ << " " << top_ << " " << right_
    6. << " " << bottom_ << std::endl;
    7. }

     编译没有错误:

     但是CTRL+f5出错:

             1. 一个C++项目分为若干个cpp文件和h文件,每个cpp文件单独编译成每个的目标文件,最终将每个cpp文件连接在一起组成最后的单一的可执行文件。这里最重要的点就是:编译是相对于每个cpp文件而言的。

            2. 在分离式编译的环境下,编译器编译某一个cpp文件时并不知道另一个cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于链接器)

            3. 在没有实例化之前,编译器都是不知道T是什么的。

            3. 类Rect, 其类定义式写在tempalte.h,类的实现体写在template.cpp中。由第一点可以知道template.cpp和test.cpp在编译的时候都要展开template.h,于是在test.cpp中出现了声明,在template.cpp中出现了声明与定义,但是两个cpp是相对独立,从第二点可以知道两个cpp是相对独立的,且由第三点,实例化是在test.cpp中的main函数中实现,在test.cpp中只存在声明,编译是可以通过的,但是在链接的过程中,需要找到Rect的实现部分。我们会说不是在template.cpp的编译的时候,展开了头文件,得到了声明和定义吗?为什么不行了?
            是因为在template.cpp中由于编译器不知道T的实参是什么,并没有对其进行处理。因为cpp是独立编译的,因此,Rect的实现自然并没有被编译,链接也就自然而然地因找不到而出错。

            4. 这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来。所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部链接的符号并期待链接器能够将符号的地址决议出来。然而当实现该模板的cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程中就找不到一行模板实例的二进制代码,于是链接器也黔驴技穷了。

            5. 也就是说,模板如果将类声明和类实现进行分离,那么分离式编译模式会导致在链接的时候出现问题。如下代码就运行通过。

  • 相关阅读:
    这些不知道,别说你熟悉 Nacos,深度源码解析!
    「PHP系列」数组详解
    定时任务框架-xxljob
    leetcode 3. 无重复字符的最长子串
    ChatGLM 大模型应用构建 & Prompt 工程
    网站攻击技术,一篇打包带走!
    人工神经网络优化算法,进化算法优化神经网络
    vue3组件外使用route
    【html-CSS布局】简单设计一个静态网页
    【C++】多态学习
  • 原文地址:https://blog.csdn.net/weixin_57604904/article/details/127884920