需要将声明和定义放在同一个头文件

1 内联函数

2 函数模板

3 类模板

 

包含模型

链接器错误:

大多数C和C++程序员会这样组织他们的非模板代码:

  • 类和其他类型都被放在一个头文件中。通常而言,头文件是一个扩展名为.hpp(.h、.hh)的文件。
  • 对于全局变量和(非内联)函数,只有声明放在头文件中,定义则位于.c文件,通常而言,.c文件是指扩展名为.cpp(或者.C、.c、.cc、.cxx)的文件。

这样一切都可以正常运作了。所需的类型定义在整个程序中都是可见的;并且对于变量和函数而言,链接器也不会给出重复定义的错误。

 1 如何处理函数模板中的函数体

既然编译器是在需要生成模板实例时自动生成,这就带来一个与传统C/C++ 编程习惯的冲突,即函数模板中的函数体应该放在哪里。

HPP 文件还是CPP 文件

按照C++ 语言习惯,普通函数及类的声明应该放在一个头文件(通常以h、hpp 或者hh 为扩展名)里,而将其实现放在一个主代码文件(通常以c、cpp 或者cc 为扩展名)里,这样便于将代码分散编译到多个目标文件中,最后通过链接形成一个完整的目标文件。但是由于模板的实现是随用随生成,并不存在真实的函数实现代码,如果还是按照“头文件放声明,主文件放实现”的做法,则会导致编译失败。

例如一个最简单的模板函数声明如下所示:

// 文件名func.hpp
template<typename T>
T const& func(T const &v);

  这个声明放在一个名为“func.hpp”的头文件中,其实现放在名为“func.cpp”的文件中,代码如下所示:

// 文件名func.cpp
template<typename T>
T const& func(T const &v) {return v;}

  

在一个名为“main.hpp”的文件中定义一个main 函数用来调用func 函数模板,代码如下所示:

// 文件名main.cpp
#include "func.hpp"
int main() {func(0);}

  正如我们通常安排普通函数的代码那样,如果单独编译这两个CPP 文件都没有问题,但是在链接两个目标文件时链接器就会报错。在笔者的编译环境下出现的错误如下:

$ g++ func.o main.o
main.o: In function 'main':
main.cpp:(.text+0x17): undefined reference to `int const& func<int>(int
const&)'
collect2: ld 返回 1

  链接器报的错误是func<int> 即func 函数模板的某个实例未定义。按常理,这样一个函数的实现应该是在func.cpp 编译出的目标文件中定义。但如果查看该目标文件(笔者用的是func.o)就会发现其中空空如也,并无任何函数定义。

  回想一下模板的工作原理就不难理解这一现象。编译器在编译func.cpp 时,只是读到了func 函数模板的实现,并没有读到任何需要生成函数模板实例的语句,所以不会生成任何func 函数的实例。而在编译main.cpp 时,虽然用到了一个函数模板实例,但因为main.cpp 只是将func.hpp 头文件包含进来,而后者只有一个func 函数模板的声明,并无具体函数体实现,此时编译器也无法生成func 函数模板实例,只好预留一个调用链接,期望在最后的链接过程中可以找到函数实现。但很遗憾这样的实现并不存在,于是最后链接时出错。

  稍微修改func.cpp 中的代码,使其生成一个func<int> 的函数实现,如例1.6 所示。

// 文件名func2.cpp
template<typename T>
T const& func(T const &v) {return v;}
template<> int const& func(int const &v);

  例1.6 中用到一种尚未介绍过的语法——明确生成模板实例。当关键字template 后没有模板参数列表,而是一个函数声明时,意味着指示编译器根据此函数声明寻找合适的模板实现。当然,所声明函数必须与某一已知模板函数同名,并且其参数可用模板匹配。

  例1.6 中将函数声明为T=int, 从而在编译func2.cpp 时, 会在目标文件中生成func<int> 的代码而不会在链接时产生错误。但这只是权宜之计,倘若还需要func<float> 或者func<char>,那么在代码文件中还得增加相应的语句,以促使编译器生成相应函数模板实例。如此一来,又变成由人工生成模板实例,违背了当初由编译器随用随生成的初衷。

  可见,虽然模板中的函数也可以有自己的声明和实现,但编译器不会在读到模板实现时立刻生成实际代码,因为具体的模板参数类型还未知,无法进行编译。对于编译器来说,模板实现也是一种声明,声明如何自动生成代码。所以模板的实现也应该放在头文件内,这样,在其他代码文件中可以直接将模板的实现也包含进来,当需要生成模板实例时,编译器可根据已知模板实现当场生成,而无需依赖在别的目标文件中生成的模板实例。

  但这样会带来另一个问题,即重复模板实例。

链接器如何识别重复模板实例

  假设将例1.6 中函数模板func 的实现也放在头文件func.hpp 中,并且文件caller.cpp 及main.cpp 中各有函数caller 及main 都调用func 生成实例func<int>,易知编译后的目标文件caller.o 及main.o 中各自都有func<int> 实例。两个函数由同一模板生成,完全等价,则这两个函数为重复模板实例。

  如果在最后链接步骤中不做特殊处理,则会在最终目标代码中存在多个等价的模板实例,造成目标文件尺寸的增加,尤其是在大量用到模板库时,这种情况会愈发严重。对此问题,C++ 标准中给出的解决方案是:在链接时识别及合并等价的模板实例。

  那么,链接器如何识别等价的模板实例呢?答案见例1.7。

// 文件名caller1.cpp
#include <iostream>
template<typename T>
void func(T const &v)
{
std::cout << "func1: " << v << std::endl;
}
void caller1() {
func(1);
func(0.1);
}

// ======================================
// 文件名caller2.cpp
#include <iostream>
template<typename T>
void func(T const &v)
{
std::cout << "func2: " << v << std::endl;
}
void caller2() {
func(2);
func(0.2f);
}
// ======================================
// 文件名main.cpp
void caller1();
void caller2();

int main()
{
caller1();
caller2();
return 0;
}

  例1.7 中用到3 个代码文件。其中caller1.cpp 和caller2.cpp 中都有一个名为func 的函数模板,且两个同名模板的模板参数也相同,都只有一个类型模板参数。但两个函数模板内容不同,区别在于打印出的前导字符串。此外,caller1.cpp 和caller2.cpp 中还分别声明两个函数caller1 及caller2,其中都用到各自文件的func 模板生成函数实例并调用。

  细看代码便知,caller1.cpp 编译所得目标文件中有func<int> 及func<double> 两个函数模板实例,而caller2.cpp 编译所得目标文件中有func<int> 及func<float> 两个函数模板实例。这两个目标文件再与main.cpp 编译所得目标文件共同链接成可执行文件后会出现什么情况呢?还是以笔者所用GCC 编译器为例,如果用以下命令行编译:

$ g++ caller1.o caller2.o main.o -o a.out

  执行程序的输出如下:

$ ./a.out
func1: 1
func1: 0.1
func1: 2
func2: 0.2

  很有趣, 在函数caller2() 中本意是调用caller2.cpp 中的func<int>, 所以应该输出“func2: 2”。但是由于caller1.cpp 与caller2.cpp 中均有func<int> 实例,并且函数参数列表也相同(都为空),那么在链接时链接器基于函数名、模板实参列表以及参数列表判断两个函数模板实例等价,而将caller2.cpp 中的func<int> 除名。所有func<int> 的调用都被链接到caller1.cpp 中的func<int> 实例。所以在以上程序输出第三行才会打印“func1: 2”。而caller1() 和caller2() 中还分别调用了func<double>(无修饰浮点常数默认是double 型)及func<int>。由于模板参数类型不同,这是两个不同的函数。链接器在链接时可以区分二者而做出如我们所想的链接。由此例的运行结果可以推知,链接器不考虑函数具体内容,仅仅通过函数名、模板实参列表以及参数列表等“接口”信息来判断两个函数是否等价。

  实际上,编译器在编译函数模板实例时,将根据函数名、函数参数类型以及模板参数值等信息来重命名编译所生成的目标函数名,这一处理方式称为Name-Mangling。如果发现“接口”等价的函数(即编译后的函数名相同),则在最终可执行代码中只保留等价函数之一作为链接候选,而放弃其他等价函数。具体保留哪个函数是随机的,可能与用户输入有关。

  比如在写链接命令时,将caller2.o 放在caller1.o 之前,如下所示:

$ g++ caller2.o caller1.o main.o -o a.out.2

  程序运行结果会变为如下所示:

$ ./a.out.2
func2: 1
func1: 0.1
func2: 2
func2: 0.2

  显然,因为命令行中文件顺序的关系,导致caller2.o 中的func<int> 先出现,而使得caller1.o 中的func<int> 实例被编译器放弃。

  

  通常情况下,根据函数接口判断等价函数实例并在链接时合并的简单方法,可以有效解决重复模板实例的问题。但正如例1.7 中所演示那样,使用这种方法也有弊端。倘若有不同的作者在写不同的模板库时,碰巧用到同一函数名以及相同的模板参数列表和函数形参列表,对于一些简单函数,这也是非常有可能的。又碰巧两个模板库用在同一项目的不同代码文件之中,则在最终链接时,有可能因为链接器的去重复功能而导致意外的链接结果,使得最终程序工作异常。降低落入这一陷阱的可能性,最好的方法就是避免使用相同的函数名。此时,C++ 中的命名空间(namespace)机制就显得异常重要。

  模板库作者最好为自己的作品起一个独特的名字,并将所有模板库代码放在此命名空间内,例如所有的C++ 标准模板库代码都放在std 命名空间内。即使名字很长,库的用户也可以通过为空间改名或者利用using 语句显示引用所需函数等办法来降低代码量。只要两个库的命名空间不一样,库中的函数名就不会重复。除非用户采用以下方式强行将两库命名空间内的所有元素引入自己的空间,人为地制造命名冲突:

using namespace libA;
using namespace libB;

因此,无论是库开发者还是用户,管理命名的习惯至关重要。这不仅为了提高代码可读性,更是关系到编译结果是否正确。

时间: 2024-11-09 02:18:39

需要将声明和定义放在同一个头文件的相关文章

基于C++全局变量的声明与定义的详解_C 语言

(1)编译单元(模块)在VC或VS上编写完代码,点击编译按钮准备生成exe文件时,编译器做了两步工作:第一步,将每个.cpp(.c)和相应的.h文件编译成obj文件:第二步,将工程中所有的obj文件进行LINK,生成最终.exe文件.那么,错误可能在两个地方产生:一个,编译时的错误,这个主要是语法错误:一个,链接时的错误,主要是重复定义变量等.编译单元指在编译阶段生成的每个obj文件.一个obj文件就是一个编译单元.一个.cpp(.c)和它相应的.h文件共同组成了一个编译单元.一个工程由很多编译

vc-VC 两个文件包涵同一个头文件 提示变量重定义

问题描述 VC 两个文件包涵同一个头文件 提示变量重定义 总共四个文件.分别是定义main函数的text.cpp定义了一个类 的 a.cpp和a.h还有一个b.h text.cpp代码 #include ""a.h"" #include ""b.h""int main(){ //some code} a.h代码 #pragma onceclass Ca{ //some code}; a.cpp代码 #include "

C语言中声明和定义详解

变量声明和变量定义 变量定义:用于为变量分配存储空间,还可为变量指定初始值.程序中,变量有且仅有一个定义. 变量声明:用于向程序表明变量的类型和名字. 定义也是声明,extern声明不是定义 定义也是声明:当定义变量时我们声明了它的类型和名字. extern声明不是定义:通过使用extern关键字声明变量名而不定义它. [注意] 变量在使用前就要被定义或者声明. 在一个程序中,变量只能定义一次,却可以声明多次. 定义分配存储空间,而声明不会. C++程序通常由许多文件组成,为了让多个文件访问相同

如何把一个头文件中定义的结构体用到另一个头文件中,另一个头文件中定义的结构体又用到改头文件中?

问题描述 如何把一个头文件中定义的结构体用到另一个头文件中,另一个头文件中定义的结构体又用到改头文件中? //栈的头文件声明 #include #include #include "Tree" #ifndef STACK_H #define STACK_H typedef union typelemt{ char ch; Tree t; }typelemt; typedef struct stack{ typelemt* type_stack; int max; }stack; typ

C++编程中变量的声明和定义以及预处理命令解析_C 语言

关于C++变量的声明和定义 我们已经知道,一个函数一般由两部分组成:声明部分和执行语句. 声明部分的作用是对有关的标识符(如变量?函数?结构体?共用体等)的属性进行说明.对于函数,声明和定义的区别是明显的,前边已说明,函数的声明是函数的原型,而函数的定义是函数功能的确立.对函数的声明是可以放在声明部分中的,而函数的定义显然不在函数的声明部分范围内,它是一个文件中的独立模块. 对变量而言,声明与定义的关系稍微复杂一些.在声明部分出现的变量有两种情况:一种是需要建立存储空间的(如int a;):另一

深入分析C++中声明与定义的区别_C 语言

        首先谈下声明与定义的区别.         声明是将一个名称引入程序.定义提供了一个实体在程序中的唯一描述.声明和定义有时是同时存在的. 如int a; extern int b=1;     只有当extern中不存在初始化式是才是声明.其他情况既是定义也是声明.      但是在下列情况下,声明仅仅是声明: 1:仅仅提供函数原型.如void func(int,int); 2: extern int a; 3:class A: 4:typedef声明 5:在类中定义的静态数据成

C语言中函数的声明、定义及使用的入门教程_C 语言

对函数的"定义"和"声明"不是一回事.函数的定义是指对函数功能的确立,包括指定函数名,函数值类型.形参及其类型以及函数体等,它是一个完整的.独立的函数单位.而函数的声明的作用则是把函数的名字,函数类型以及形参的类型.个数和顺序通知编译系统,以便在调用该函数时进行对照检查(例如,函数名是否正确,实参与形参的类型和个数是否一致),它不包括函数体.--谭浩强 ,<C程序设计>(第四版),清华大学出版社,2010年6月,p182 这段论述包含了许多概念性错误,这

c/c++全局变量的声明和定义

问题描述 c/c++全局变量的声明和定义 1.cpp const int point=50; //point 里放的是特征点的数量 fun1(){...} 2.h extern const int point; 3.cpp #include"2.h" fun2(){ float a[point]={0,} }//在cpp里定义一个point维的数组 4.h #include"2.h" class ObsFun { ObsFun(); ..... } ObsFun()

C++可以在多个头文件中重复定义同一名称的命名空间吗?

问题描述 C++可以在多个头文件中重复定义同一名称的命名空间吗? test_a.h中有定义: namespace ns { class a {...}; } test_b.h中有定义: namespace ns { class b {...}; } test_c.h中有定义: namespace ns { class c {...}; } 然后,在test_c.cpp中有如下代码片段: #include "test_a.h" #include "test_b.h"