• 【C++】泛型编程 ⑩ ( 类模板的运算符重载 - 函数实现 写在类外部的同一个 cpp 代码中 | 类模板 的 外部友元函数二次编译问题 )



    将 类模板 函数声明 与 函数实现 分开进行编码 , 有 三种 方式 :

    • 类模板 的 函数声明 与 函数实现 都写在同一个类中 , 也就是没有分开进行编码 ;
    • 类模板 的 函数实现 在 类外部进行 , 函数声明 和 实现 写在相同的 .cpp 源码文件中 ;
    • 类模板 的 函数实现 在 类外部进行 , 函数声明 和 实现 写在不同的 .h 和 .cpp 源码文件中 ;

    上一篇博客 【C++】泛型编程 ⑨ ( 类模板的运算符重载 - 函数声明 和 函数实现 写在同一个类中 | 类模板 的 外部友元函数问题 ) 实现了第一种情况 , 类模板 的 函数声明 与 函数实现 都写在同一个类中 , 也就是没有分开进行编码 ;

    本篇博客 , 开始分析 第二种情况 , 类模板 的 函数实现 在 类外部进行 , 写在相同的 .h 和 .cpp 源码文件中 ;





    一、类模板 - 函数声明与函数实现分离




    1、类模板 外部 实现 构造函数


    原来的构造函数是 :

    template <typename T>
    class Student
    {
    public:
    	Student(T x, T y)
    	{
    		this->a = x;
    		this->b = y;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    如果将 构造函数 实现 , 写在类外部的 .cpp 源码中 ,

    • 首先 , 需要 声明 模板类型 , template ;
    • 然后 , 通过 域操作符 访问 构造函数 , 并实现该函数 , 使用域操作符 时 , 前面的类 需要指定 具体的泛型类型 , 这里使用 声明的 T 模板类型 作为 具体的 泛型类型 ;
    template <typename T>
    Student<T>::Student(T x, T y)
    {
    	this->a = x;
    	this->b = y;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在 类模板 内部 , 只需要声明该 构造函数 :

    template <typename T>
    class Student
    {
    public:
    	Student(T x, T y);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2、类模板 外部 实现 普通函数


    将 类内部的 普通函数 实现 加法运算符重载 的函数 , 提取到 类模板 外部进行定义 ;

    该函数的 返回值 和 参数 都涉及到 类模板 类型 ;

    template <typename T>
    class Student
    {
    public:
    
    	// 重载 + 运算符
    	Student operator+(Student& s)
    	{
    		Student student(this->a + s.a, this->b + s.b);
    		return student;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在类外部 实现 该 加号运算符重载 需要注意以下几点 :

    • 首先 , 需要 声明 模板类型 , template ;
    • 然后 , 通过 域操作符 访问 构造函数 , Student:: 后面跟上要访问的成员 ;
    • 最后 , 返回值和参数类型 , 如果是 类模板类型 Student , 需要在后面使用尖括号 指明具体的类型 , 这里具体的类型就是泛型 T ;

    函数内部 Student 类型 , 可以加 也可不加 , 不加 也可以使用 , 加了也不会报错 ;

    // 重载 + 运算符
    // 使用  Student:: 域操作符访问函数
    template <typename T>
    Student<T> Student<T>::operator+(Student<T>& s)
    {
    	// 函数内部的类的  模板类型 , 可加  可不加  
    	// 不加  也可以使用 , 加了也不会报错
    	Student student(this->a + s.a, this->b + s.b);
    	return student;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    类模板内部 , 需要声明该 重载函数 ;

    template <typename T>
    class Student
    {
    public:
    	// 重载 + 运算符
    	Student operator+(Student& s);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3、类模板 外部 实现 友元函数


    友元函数 不是 类中的函数 , 是 类外部的函数 , 友元函数 中又用到了 泛型 T , 说明这是一个 模板函数 ;

    友元函数 是 全局函数 , 不属于 类模板 , 不要使用 域操作符 访问友元函数 ;

    友元函数 中的 泛型类型 , 要当做 函数模板 对待 ;

    模板函数就涉及到 二次编译 问题 , 下面先分析一下 模板函数 二次编译 导致的 类模板的友元函数 问题 ;

    友元函数 不要乱用 , 只有在 重载 左移 右移 操作符时 , 才使用 友元函数 ;



    ( 1 ) 错误示例及分析 - 类模板 的 外部友元函数 二次编译 问题

    在 类模板 内部声明 友元函数 ,

    template <typename T>
    class Student
    {
    	// 左移运算符重载
    	friend ostream& operator<<(ostream& out, Student& s);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在 类外部 实现 友元函数 ,

    // Student 类的友元函数
    // 左移运算符重载 函数
    template <typename T>
    ostream& operator<<(ostream& out, Student<T>& s)
    {
    	out << "a:" << s.a << " b: " << s.b << endl;
    	return out;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    运行时会报如下错误 :

    已启动生成…
    1>------ 已启动生成: 项目: HelloWorld, 配置: Debug Win32 ------
    1>Test.obj : error LNK2019: 无法解析的外部符号 "class std::basic_ostream > & __cdecl operator<<(class std::basic_ostream > &,class Student &)" (??6@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@std@@AAV01@AAV?$Student@H@@@Z),函数 _main 中引用了该符号
    1>D:\002_Project\006_Visual_Studio\HelloWorld\HelloWorld\Debug\HelloWorld.exe : fatal error LNK1120: 1 个无法解析的外部命令
    1>已完成生成项目“HelloWorld.vcxproj”的操作 - 失败。
    ========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0==========
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    造成上述错误的原因 就是 函数模板 的实现机制 中的 二次编译 有关 ,

    • 第一次编译 函数模板 时 , 只进行 简单的 语法分析 , 词法分析 , 生成一个函数头 ;
    • 第二次编译 函数模板 时 , 又生成一个 函数头 ;

    这两次编译生成的 函数头 不一致 , 导致 无法找到 相应的 函数实现 ;


    ( 2 ) 正确写法

    友元函数 不要乱用 , 只有在 重载 左移 右移 操作符时 , 才使用 友元函数 ;


    这是 函数模板 二次编译 问题 ,

    一般情况下 , 函数模板 只有在 调用时 , 才需要将 泛型类型 指明 , 在 函数名称后面 , 使用 <> 注明泛型类型 ,

    但是在 类模板 声明 友元函数 时 , 就需要指定 泛型类型 ;

    这样才能将 类模板中的 泛型 T , 与 友元函数在 外部实现时 声明的 template 关联起来 ;


    在 类模板 内部声明 友元函数时 , 在函数名 operator<< 后面 加上 ;

    template <typename T>
    class Student
    {
    	// 左移运算符重载
    	friend ostream& operator<< <T> (ostream& out, Student& s);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在 类外部 实现 友元函数 保持不变 ;

    // Student 类的友元函数
    // 左移运算符重载 函数
    template <typename T>
    ostream& operator<<(ostream& out, Student<T>& s)
    {
    	out << "a:" << s.a << " b: " << s.b << endl;
    	return out;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8




    二、代码示例 - 函数声明与函数实现分离




    1、代码示例


    #include "iostream"
    using namespace std; 
    
    template <typename T>
    class Student
    {
    	// 左移运算符重载
    	friend ostream& operator<< <T> (ostream& out, Student& s);
    
    public:
    	// 构造函数
    	Student(T x, T y);
    
    	// 重载 + 运算符
    	Student operator+(Student& s);
    
    public:
    	T a, b;
    };
    
    // 类模板构造函数
    // 使用  Student:: 域操作符访问函数
    template <typename T>
    Student<T>::Student(T x, T y)
    {
    	this->a = x;
    	this->b = y;
    }
    
    // 重载 + 运算符
    // 使用  Student:: 域操作符访问函数
    template <typename T>
    Student<T> Student<T>::operator+(Student<T>& s)
    {
    	// 函数内部的类的  模板类型 , 可加 Student 可不加 Student
    	// 不加  也可以使用 , 加了也不会报错
    	Student student(this->a + s.a, this->b + s.b);
    	return student;
    }
    
    // Student 类的友元函数
    // 左移运算符重载 函数
    template <typename T>
    ostream& operator<<(ostream& out, Student<T>& s)
    {
    	out << "a:" << s.a << " b: " << s.b << endl;
    	return out;
    }
    
    int main() {
    	// 模板类不能直接定义变量
    	// 需要将 模板类 具体化之后才能定义变量
    	Student<int> s(666, 888);
    	cout << s << endl;
    
    	Student<int> s2(222, 111);
    	cout << s2 << endl;
    
    	// 验证 加法运算符 + 重载
    	Student<int> s3 = s + s2;
    
    	// 验证 左移运算符 << 重载
    	cout << s3 << endl;
    	
    	// 控制台暂停 , 按任意键继续向后执行
    	system("pause");
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    2、执行结果


    执行结果 :

    a:666 b: 888
    
    a:222 b: 111
    
    a:888 b: 999
    
    Press any key to continue . . .
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

  • 相关阅读:
    Vue2+ElementUI的el-table实现新增数据行与删除的功能
    每日一题 213打家劫舍||(动态规划)
    基于QT实现发送http的get和post请求(post还可以实现上传文件),同时实现接收返回json数据,并对其进行解析
    吐血推荐 | 珍藏多年的 Python 奇淫技巧,务必收藏
    ios app安装的多种方式
    电脑重装系统后如何在防火墙设置允许浏览器访问网络
    力扣55. 跳跃游戏(动态规划)
    CentOS7使用yum安装MySQL8.0教程
    玉米社:百度SEM竞价推广策略有哪些?
    期货量化交易客户端开源教学第三节——键盘通信协议
  • 原文地址:https://blog.csdn.net/han1202012/article/details/134528915