在我学习数据结构的时候,选用了《数据结构(用面向对象方法与C++语言描述)》殷人昆 编著 这本教材。这本书代码较老有一些错误,好多是无法直接在新版本编译器下实现的。
当时我C++模板学的不是很好,比葫芦画瓢照着书上的代码敲时遇到了关于函数模板的问题。以下代码为仿照书上的代码写的,类似这种问题在书上还有很多。
问题引入:
#include<iostream>
#include<cassert>
using namespace std;
template <class T>
struct stackNode//链式栈结点
{
T date;
stackNode<T>*link;
stackNode() {}
stackNode(const T&x, stackNode<T>*next = NULL) :date(x), link(next) {}
};
template <class T>
class linkStack//链式栈
{
//重载<<运算符输出栈中元素
friend ostream& operator<<(ostream&os, linkStack<T>&st);//此处出现问题
private:
stackNode<T> *top;
public:
linkStack() :top(NULL) {}
~linkStack() { clear(); }//清空栈
bool empty()const { return (top == NULL) ? true : false; }
void push(const T&x) { top = new stackNode<T>(x, top); assert(top); }
bool pop(T&x)
{
if (empty())return false;
x = top->date;
stackNode<T>*p = top;
top = top->link;
delete p;
return true;
}
int size()const
{
int n = 0;
for (stackNode<T>*k = top; k != NULL; n++, k = k->link);
return n;
}
void clear() { stackNode<T>*p; while (top != NULL) { p = top; top = top->link; delete p; } }
};
template<class T>
ostream& operator<< (ostream&os, linkStack<T>&st)
{
if (st.top == NULL)cout << "栈中没有元素" << endl;
else
{
int i; stackNode<T>*p;
for (p = st.top, i = 0; p != NULL; p = p->link, i++)
os << i << ":" << p->date << endl;
}
return os;
}
int main()
{
linkStack<int> lin;
int i;
int tmp;
for (i = 0; i<10; i++)lin.push(i);
cout << "栈中有元素:" << lin.size() << endl;
for (i = 0; i<5; i++)
{
lin.pop(tmp);
cout << i << ":" << tmp << " ";
}
cout << endl;
cout << "栈中有元素:" <<lin.size() << endl;
cout << lin << endl;
system("pause");
return 0;
}
在vs2017上运行报错
需要注意的是大多数模板编译错误在实例化期间报告,也就是好多错误可能在链接的时候才报,这给了我们调试程序一定难度。
在带有gcc编译器的IDE上运行同样会报错,但给出代码的警告提示,可以帮助我们知道错在哪里
||=== Build: Debug in temp (compiler: GNU GCC Compiler) ===|
|17|warning: friend declaration 'std::ostream& operator<<(std::ostream&, linkStack<T>&)' declares a non-template function [-Wnon-template-friend]|
|17|note: (if this is not what you intended, make sure the function template has already been declared and add <> after the function name here)
它的意思是说
friend ostream& operator<<(ostream&os, linkStack<T>&st);
这里声明了一个非模板友元函数,如果这不是你想要的,确保函数模板已经声明了,并且在函数名后面加<>。
为了弄清这个问题,我们需要知道,模板的友元分三类:非模板友元、约束模板友元、非约束模板友元。(这里模板既可指函数模板也可指类模板,以下以函数模板为例)
1.非模板友元:
在模板类中将一个常规函数(类)声明为友元。
#include<iostream>
#include<string>
using namespace std;
template<typename T>
class HasFriend
{
public:
friend void counts();//counts() reports(HasFriend<T>&)一定是非模板函数
friend void reports(HasFriend<T>&);
HasFriend(const T&i) :item(i) { ct++; }
private:
T item;
static int ct;
};
template<typename T>
int HasFriend<T>::ct = 0;//静态成员变量类外初始化
void counts()
{
cout << "int count:" << HasFriend<int>::ct << endl;
cout << "double count:" << HasFriend<double>::ct << endl;
}
//HasFriend<int>类的非模板友元函数
void reports(HasFriend<int>&a)
{
cout << "HasFriend<int>:" << a.item << endl;
}
//HasFriend<double>类的非模板友元函数
void reports(HasFriend<double>&a)
{
cout << "HasFriend<double>:" << a.item << endl;
}
int main()
{
HasFriend<string>de("0");
cout << "No object ";
counts();
HasFriend<double>d(3.1);
cout << "After d declared ";
counts();
HasFriend<int>i(4);
cout << "After i declared ";
counts();
reports(d);
reports(i);
return 0;
}
这里每声明一个实例化对象,编译器就生成对应的类,类中包含对应的友元函数声明,但是不会生成对应的友元函数。本段代码中带HasFriend<int>参数的report()将成为HasFriend<int>类的友元。同样,带HasFriend<double>参数的report()将是HasFriend<int>参数的report()的一个重载函数,是Hasfriend<double>类的友元。
注意,这里类中friend void counts()友元函数声明并没有把它声明为模板函数,而只是使用了一个模板做参数,这意味着必须为要使用的友元定义显示具体化,同时也指出了void counts()为非模板函数,后面不可以把void counts()定义成模板函数。因此,问题引入中,类中已经把重载<<运算符的函数声明为非模板友元函数,后面定义时有把它定义成模板函数显然是错误的。
这段程序中,该模板有一个静态成员ct,这意味着这个类的每一个特定的具体会都有自己的静态成员,这个从程序执行结果中可以看出。
2.模板类的约束模板友元:(一对一友好关系)(VC++ 6.0不支持)
可以修改前一个程序,使友元函数本身成为模板,也就是使类的每一个具体会都获得与友元匹配的具体化。它以下包含三步。
(1)在类定义的前面声明每个约束模板友元函数。
(注意:网上相关的教程好多没有此步骤,不加这个有的编译器能通过,有的不能 )C++ Primer上指出:想要限制对特定实例化的友元关系时,必须在可以用于友元声明之前声明类或函数(When we want to restrict friendship to a specific instantiation, then the class or function must have been declared before it can be used in a friend declaration:):想要将友元关系限定在特定的实例化上,则相关的类或函数必须在其友元声明之前进行声明。
例如:template<typename T> void counts();
template<typename T> class HasFriend;
template<typename T> void reports(HasFriend<T>&);
(2)在类中将模板声明为友元。例如:
template<typename T>
class HasFriend
{
public:
friend void counts<T>();//counts()函数没有参数,因此必须使用模板参数语法<T>来指明具体化。
friend void reports<>(HasFriend<T>&);/*声明中<>指出这是模板具体化,对于report(),<>中T可以省略,因为可以从函数参数推断出模板类型参数*/
.......
};
(3)为友元提供模板定义。例如:
template<typename T>
void counts(){cout << "template size:" << sizeof(HasFriend<T>) << endl;}
template<typename T>void reports(HasFriend<T>&a){ cout << "template:" << a.item << endl;}
注意:count()函数调用没有可被编译器用来推断出所需具体化的函数参数,所以这些调用需要使用count<T>指明具体化,例如count<int>()。但对于report()调用,编译器可以从参数类型推断出要使用的具体化,不必使用<>格式指明。例如:report(hfi2)或者report<int>(hfi2)。
3.模板类的非约束模板友元(通用和特定的模板友好关系)
模板类的非约束模板友元函数(类)通过在类内部声明模板,可以创建非约束友元函数(类),即每个模板函数(类)具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类型参数是不同的。例如以下程序就使用了非约束模板友元函数。
#include<iostream>
using namespace std;
template<typename T>
class ManyFriends
{
private:
T item;
public:
ManyFriends(const T&i):item(i){}
template<typename C, typename D>friend void show(C&c, D&d);
};
template<typename C, typename D>
void show(C&c, D&d)
{
cout << c.item << ", " << d.item << endl;
}
int main()
{
ManyFriends<int>hfi1(10);
ManyFriends<int>hfi2(20);
ManyFriends<double>hfdb(10.5);
cout << "hfi1,hfi2: ";
show(hfi1, hfi2);
cout << "hfdb,hfi2: ";
show(hfdb, hfi2);
//system("pause");
return 0;
}
需要注意的是 void show(C&c, D&d)函数是所有ManyFriends具体化的友元,这个函数可以访问所有ManyFriends具体化产生的类。
对于问题引入中的程序,可以使用模板类的约束模板友元函数,把友元函数声明改为如下声明,并在类前增加前置声明。
template<class T>class linkStack;
template<class T>ostream& operator<<(ostream&os, linkStack<T>&st);
friend ostream& operator<<<>(ostream&os, linkStack<T>&st);
或者也可把友元函数定义为模板类的非约束模板友元函数。对友元的定义做如下修改。
template<class T> friend ostream& operator<<(ostream&os, linkStack<T>&st);
到这里,这个问题就成功解决了。我又想,为何这本书以及其他参考资料上都有好多现在看起来类似错误的代码。于是我便把那段gcc和vc++2017都无法编译通过的代码放到VC++ 6.0上编译运行,还是失败。最后把头文件引用改为#include<iostream.h>发现竟然编译成功,这完全出乎我意料。原来这本书和网上的错误代码好多是VC++ 6.0时期的代码,VC++ 6.0对C++标准库支持不是很好。
参考资料:Stanley B. Lippman C++ Primer 中文版(第五版)北京:电子工业出版社
Stephen Prata C++ Primer Plus 中文版(第六版)北京:人民邮电出版社
https://blog.csdn.net/lychee007/article/details/4428161