首页 > 编程语言 >将C++模板应用于多文件编程

将C++模板应用于多文件编程

时间:2024-03-16 14:12:10浏览次数:31  
标签:定义 int 编程 C++ Swap cpp 模板 函数

C++ 模板

在将函数应用于多文件编程时,我们通常是将函数定义放在源文件(.cpp文件)中,将函数声明放在头文件(.h文件)中,使用函数时引入(#include命令)对应的头文件即可。

编译是针对单个源文件的,只要有函数声明,编译器就能知道函数调用是否正确;而将函数调用和函数定义对应起来的过程,可以延迟到链接时期。正是有了链接器的存在,函数声明和函数定义的分离才得以实现。

将类应用于多文件编程也是类似的道理,我们可以将类的声明和类的实现分别放到头文件和源文件中。类的声明已经包含了所有成员变量的定义和所有成员函数的声明(也可以是 inline 形式的定义),这样就知道如何创建对象了,也知道如何调用成员函数了,只是还不能将函数调用与函数实现对应起来,但是这又有什么关系呢,反正链接器可以帮助我们完成这项工作。

总起来说,不管是函数还是类,声明和定义(实现)的分离其实是一回事,都是将函数定义放到其他文件中,最终要解决的问题也只有一个,就是把函数调用和函数定义对应起来(找到函数定义的地址,并填充到函数调用处),而保证完成这项工作的就是链接器。

基于传统的编程思维,初学者往往也会将模板(函数模板和类模板)的声明和定义分散到不同的文件中,以期达到「模块化编程」的目的。但事实证明这种做法是不对的,程序员惯用的做法是将模板的声明和定义都放到头文件中。

模板并不是真正的函数或类,它仅仅是用来生成函数或类的一张“图纸”,在这个生成过程中有三点需要明确:

1、模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码;

2、模板的实例化是由编译器完成的,而不是由链接器完成的;

3、在实例化过程中需要知道模板的所有细节,包含声明和定义。

将函数模板的声明和定义分散到不同的文件

为了更加深入地说明问题,现在有一个反面教材,它将函数模板的声明和实现分别放到了头文件和源文件。
func.cpp 源码:

//交换两个数的值
template<typename T> void Swap(T &a, T &b){
    T temp = a;
    a = b;
    b = temp;
}
//冒泡排序算法
void bubble_sort(int arr[], int n){
    for(int i=0; i<n-1; i++){
        bool isSorted = true;
        for(int j=0; j<n-1-i; j++){
            if(arr[j] > arr[j+1]){
                isSorted = false;
                Swap(arr[j], arr[j+1]);  //调用Swap()函数
            }
        }
        if(isSorted) break;
    }
}

func.h 源码:

#ifndef _FUNC_H
#define _FUNC_H

template<typename T> void Swap(T &a, T &b);
void bubble_sort(int arr[], int n);

#endif

main.cpp 源码

#include <iostream>
#include "func.h"
using namespace std;
int main(){
    int n1 = 10, n2 = 20;
    Swap(n1, n2);
    double f1 = 23.8, f2 = 92.6;
    Swap(f1, f2);
    return 0;
}

该工程包含了两个源文件和一个头文件,func.cpp中定义了两个函数,func.h中对函数进行了声明,main.cpp中对函数进行了调用,这是典型的将函数的声明和实现分离的编程模式。

运行上面的程序,会产生一个链接错误,意思是无法找到void Swap<double>(double &, double &)这个函数。主函数 main() 中共调用了两个版本的 Swap() 函数,它们的原型分别是:

void Swap<double>(int &, int &);
void Swap<double>(double &, double &);

为什么针对 int 的版本能够找到定义,而针对 double 的版本就找不到呢?

我们先来说针对 double 的版本为什么找不到定义。当编译器编译main.cpp时,发现使用到了 double 版本的 Swap() 函数,于是尝试生成一个 double 版本的实例,但是由于只有声明没有定义,所以生成失败。不过这个时候编译器不会报错,而是对该函数的调用做一个记录,希望等到链接程序时在其他目标文件(.obj 文件或 .o 文件)中找到该函数的定义。很明显,本例需要到func.obj中寻找。但是遗憾的是,func.cpp中没有调用 double 版本的 Swap() 函数,编译器不会生成 double 版本的实例,所以链接器最终也找不到 double 版本的函数定义,只能抛出一个链接错误,让程序员修改代码。

那么,针对 int 的版本为什么能够找到定义呢?请读者注意bubble_sort()函数,该函数用来对数组元素进行排序,在排序过程中需要频繁的交换两个元素的值,所以调用了 Swap() 函数,这样做的结果是:编译生成的func.obj中会有一个 int 版本的 Swap() 函数定义。编译器在编译main.cpp时虽然找不到 int 版本的实例,但是等到链接程序时,链接器在func.obj中找到了,所以针对 int 版本的调用就不会出错。

将类模板的声明和实现分散到不同的文件

我们再看一个类模板的反面教材,它将类模板的声明和实现分别放到了头文件和源文件。
point.h 源码:

#ifndef _POINT_H
#define _POINT_H

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;
};

#endif

point.cpp 源码:

#include <iostream>
#include "point.h"
using namespace std;

template<class T1, class T2>
void Point<T1, T2>::display() const{
    cout<<"x="<<m_x<<", y="<<m_y<<endl;
}

main.cpp 源码:

#include <iostream>
#include "point.h"
using namespace std;

int main(){
    Point<int, int> p1(10, 20);
    p1.setX(40);
    p1.setY(50);
    cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;

    Point<char*, char*> p2("东京180度", "北纬210度");
    p2.display();

    return 0;
}

该工程包含了两个源文件和一个头文件,point.h中声明了类模板,point.cpp中对类模板进行了实现,main.cpp中通过类模板创建了对象,并调用了成员函数,这是典型的将类的声明和实现分离的编程模式。

运行上面的程序,会产生一个链接错误,意思是无法通过 p2 调用Point<char*, char*>::display() const这个函数。

类模板声明位于point.h中,它包含了所有成员变量的定义以及构造函数、get 函数、set 函数的定义,这些信息足够创建出一个完整的对象了,并且可以通过对象调用 get 函数和 set 函数,所以main.cpp的前 11 行代码都不会报错。而第 12 行代码调用了 display() 函数,该函数的定义位于point.cpp文件中,并且point.cpp中也没有生成对应的实例,所以会在链接期间抛出错误。

总结

通过上面的两个反面教材可以总结出,「不能将模板的声明和定义分散到多个文件中」的根本原因是:模板的实例化是由编译器完成的,而不是由链接器完成的,这可能会导致在链接期间找不到对应的实例。

 

标签:定义,int,编程,C++,Swap,cpp,模板,函数
From: https://www.cnblogs.com/uacs2024/p/18077022

相关文章

  • 邻接表存储带权的无向图(c++题解)
    题目描述给出一个无向带权图,顶点数为n,边数为m。输入格式第一行两个整数n,m,接下来有m行,每行3个整数u,v,w,表示点u到点v有一条边,边权为w。输出格式第i行输出第点i的所有邻接点,按照点i到该点的边权由小到大输出,如果边权相等,则按照点的编号有小到大输出。样例样例输入复......
  • 有向图的DFS(c++题解)
    题目描述给定一个有向图(不一定连通),有N个顶点,M条边,顶点从1..N依次编号,求出字典序最小的深度优先搜索顺序。输入格式第1行:2个整数,N(1≤N≤200)和M(2≤M≤5000)接下来M行,每行2个整数I,J,描述一条边从顶点I指向顶点J输出格式仅一行,一个顶点编号序列,表示字典序最小的深度优先搜索......
  • C++模板的实例化
    C++模板模板并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一张“图纸”。模板不会占用内存,最终生成的函数或者类才会占用内存。由模板生成函数或类的过程叫做模板的实例化,相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例。在学习模板以前,如果想针对不同......
  • C++模板中的非类型参数
    C++模板模板是一种泛型技术,目的是将数据的类型参数化,以增强C++语言(强类型语言)的灵活性。C++对模板的支持非常自由,模板中除了可以包含类型参数,还可以包含非类型参数,例如:template<typename T, int N> class Demo{ };template<class T, int N> void func(T (&arr)......
  • C++示例:学习C++标准库,std::unordered_map无序关联容器的使用
    01std::unordered_map介绍std::unordered_map是C++标准库中的一种无序关联容器模板类,它提供了一种将键映射到值的方法。它的底层基于哈希表实现,内容是无序的,可以在平均情况下在O(1)的时间复杂度内完成插入、查找和删除操作。值得注意的是,哈希表可能存在冲突,即不同的键值......
  • 滴水逆向笔记系列-c++总结2-36.权限控制-37.虚函数-38.多态_绑定
    第三十六课c++3权限控制1.定义和实现分开写2.private和publicprivate权限说明私有变量在类外是无法访问的,只有在类内或者使用类内函数访问类内函数访问3.private真的不能访问吗反汇编看看t对象在初始化public和private成员时都是一视同仁的,在底层还是没区别,都是编......
  • 滴水逆向笔记系列-c++总结3-39.模板-40.引用_友元_运算符重载
    第三十八课c++6模板1.冒泡排序和折半查找voidSort(int*arr,intnLength) { inti; intk; for(i=0;i<nLength-1;i++) { for(k=0;k<nLength-1-i;k++) { if(arr[k]>arr[k+1]) { inttemp=arr[k]; a......
  • 滴水逆向笔记系列-c++总结4-41.new-delete-vector-42.链表
    第四十课c++8new-delete-vector1.内存空间复习在类外函数外的变量就是全局变量,程序一编译地址就已经确定了的临时数据,参数和局部变量就是在堆栈里而使用malloc函数动态申请的则是在堆里2.跟踪调试反汇编函数我们调用malloc函数申请内存,但是不是malloc一个函数完成整个......
  • 【Linux系统编程】静态库与动态库
    静态库与动态库静态库的制作和使用编写库文件源代码和头文件。将所有需要做成库的源文件生成目标文件:gcc-c{filename}.c-o{filename}.o用ar工具将要做成库的目标文件打包:arrcslib{库名}.a{filename-1}.o...{filename-n}.o将使用库的源文件与库文件一起编译:gcc{......
  • C++性能分析工具
    gprof:这是一个GNU的性能分析工具,主要用于分析程序的函数调用关系,以及每个函数的运行时间等。Valgrind:这是一个用于内存调试、内存泄漏检测以及性能分析的开源工具集。其中,Valgrind的Callgrind工具可以收集程序运行时的函数调用信息,用于性能分析。perf:这是Linux下的一个性能分析......