在模板类中重载 << 运算符

8

我已经为链表中的节点制作了一个模板类,并尝试通过重载<<将其内容输出到输出流中。然而,我的当前代码:

#include <iostream>
using namespace std;
template<class NType> class Node;

template<class NType>
class Node {
private:
        void deletePointer(NType* p);
public:
        NType data;
        Node *prev, *next;

        template<typename T>
        struct is_pointer { static const bool value = false; };

        template<typename T>
        struct is_pointer<T*> { static const bool value = true; };

        Node();
        Node(NType data);
        ~Node();
};  

int main() {
        Node<int> *n1 = new Node<int>();
        Node<int> *n2 = new Node<int>(10);

        std::cout << "Node 1: " << n1 << std::endl;
        std::cout << "Node 2: " << n2 << std::endl;
}

template<class NType> inline std::ostream & operator << (std::ostream& out, const Node<NType> &node){
        out << node.data;
        return out;
}

template<class NType> inline Node<NType>::Node()
            :data(NULL), prev(NULL), next(NULL)
{
}

template<class NType> inline Node<NType>::Node(NType data)
            :data(data), prev(NULL), next(NULL)
{
}

template<class NType> inline Node<NType>::~Node(){
        if(is_pointer<NType>::value){
                deletePointer(&data);
        } else {
                return;
        }
}

template<class NType> inline void Node<NType>::deletePointer(NType* p){
    delete p;
}

输出节点中的内存位置,而不是数据。这种情况发生在原始类型,如int等,就好像它不知道NType容器中有什么样的数据。

Node 1: 0x741010
Node 2: 0x741030
Node 3: 0x741070
Node 4: 0x741090

我尝试使用 typename 而不是 class,但仍然无法成功...是否有办法在插入之前动态查找模板正在使用的类型并进行转换或其他操作?我知道我可以为所有基本类型编写大量冗余代码,但这似乎是浪费和不必要的。
如果有帮助的话,我正在 Arch Linux x64 上使用 GCC v4.6.2 20111223 进行编译。
编辑:由于很多人都提到了它。我还尝试将类放在外部作为友元和独立函数,但无论我把它放在哪里,流输出的都是地址而不是数据本身。没有私有数据值需要访问,因此不需要成为友元。
编辑: 测试用例:http://ideone.com/a99u5 同时更新了上面的源代码。
编辑: 添加了我的代码剩余部分,以帮助 Aaron 理解代码。

1
你的做法需要使用 SomeNode << cout,这显然不是你想要的。ostream& operator<< 应该通常作为一个自由函数,因为你希望 ostream 在左侧。修复这个问题不一定能解决你的问题,但是... - Lightness Races in Orbit
请生成一个测试用例。http://ideone.com - Lightness Races in Orbit
5个回答

12

您的代码将operator<<声明为成员函数,因此它实际上会将this指针作为第一个参数和ostream作为第二个参数。而应该是一个自由函数:

template<class NType> class Node {
public:
    NType data;
    Node *prev, *next;
};
//Note how this is declared outside of the class body, so it is a free function instead of a memberfunction
template<class NType> inline std::ostream& operator<<(std::ostream& out, const Node<NType>& val){
    out << val.data;
    return out;
}

然而,如果你的 operator<< 需要访问私有数据,你需要将其声明为友元函数:

template<class NType> class Node {
public:
    NType data;
    Node *prev, *next;
    friend std::ostream& operator<<(std::ostream& out, const Node& val){
        out << val.data;
        return out;
    }
};

现在看一下你的输出:如果调用了你的 operator<<,编译器会知道 NType 的类型,并在流式传输 data 成员时做正确的事情。然而,由于你的 operator<< 不应该工作(如写作),并且似乎将内存地址作为输出,我会认为你有类似以下代码的问题:
Node* n = new Node();
std::cout<<n;
//When it should be:
std::cout<<*n;

现在只是出于好奇,为什么你要自己实现类似于链表的东西,而不是简单地使用 std::list

编辑: 现在我们可以看到测试用例,似乎关于如何调用operator<<的假设是正确的。输出需要更改为:

std::cout << "Node 1: " << *n1 << std::endl;
std::cout << "Node 2: " << *n2 << std::endl;

实际调用Nodeoperator<<,而不是T*的通用运算符。


我知道在互联网和标准库中有多种链表的实现方式,但我只是想自己实现这个数据结构以便于我更好地理解和控制它的功能。 - TechWiz
哇,多么愚蠢的错误...感觉自己像个新手哈哈。非常感谢。 - TechWiz
2
@TechWiz:不要太在意那种感觉,它很快就会再次回到你身边的。 ;) - Xeo

3

为什么您要在已经模板化的类方法前添加template<class NType>?这样做的目的是什么呢?

通常,重载operator<<的最佳方法是将其定义为友元函数:

template<typename NType>
friend std::ostream& operator<<(std::ostream& out, const Node<NType>& node)
{
    out << node.data;
    return out; 
}

编辑:针对下面的评论,这就是我的意思:

当您在头文件中定义类模板时,不需要为类的成员函数重新声明该模板:

template<typename T>
class Foo
{
    T some_data;

    void member_fn();
}

当你声明一个非成员友元函数时,这是必需的:

template<typename NType>
class Node 
{
public:

    NType data;
    Node<NType> *prev, *next;

    //Note the different template parameter!
    template<typename NType1>
    friend std::ostream& operator<<(std::ostream& out, const Node<NType1>& node);
};

实现方法如下:template<typename NType> std::ostream& operator<<(std::ostream& out, const Node<NType>& node)


我不明白。你也在你的类前面加上了 template<typename NType> 吗?此外,这也返回地址。我还漏掉了其他什么吗? - TechWiz
抱歉,我在复制和粘贴时有些马虎。该函数在类定义中被声明,但在类外部被定义。但出于简洁起见,我将其移入了类中。显然这是一个愚蠢的想法,它造成了比应该更多的混乱。 - TechWiz

1
template <class NType>
class Node
{
  public:
    NType data;
    Node *prev, *next;
};

template <class NType>
inline std::ostream& operator<<(std::ostream& os, const Node<NType>& n)
{
    return out << n.data;
}

我之前尝试过(而且当你再次提到它时我又试了一遍),但情况依然如故。 - TechWiz

0
您正在打印节点地址,而不是节点本身:
int main() {
        Node<int> *n1 = new Node<int>();
        Node<int> *n2 = new Node<int>(10);

        std::cout << "Node 1: " << n1 << std::endl;
        std::cout << "Node 2: " << n2 << std::endl;
}

n1n2 都是指针,而不是对象。你应该写成:

int main() {
        Node<int> *n1 = new Node<int>();
        Node<int> *n2 = new Node<int>(10);

        std::cout << "Node 1: " << *n1 << std::endl;
        std::cout << "Node 2: " << *n2 << std::endl;
}

或者:

int main() {
        Node<int> n1();
        Node<int> n2(10);

        std::cout << "Node 1: " << n1 << std::endl;
        std::cout << "Node 2: " << n2 << std::endl;
}

是的,那就是问题所在,谢谢你。该死的Java试图给我灌输可怕的习惯。 - TechWiz

0

其他人已经指出,你需要使用cout << *n1而不是cout << n1,但是你的代码中还有另一个重要的错误。

你的析构函数包括调用deletePointer(&data);,其中data是类的成员。这是很危险的。如果执行了这行代码,程序可能会崩溃。data只是由this指向的对象的一小部分,试图删除它就像试图删除int数组中的单个元素。

Node<int> *n1 = new Node<int>();
delete n1; // this makes sense. deleting the same thing that was new'ed

Node<int> *n1 = new Node<int>();
delete &(n1->data); // BUG

你可能需要大幅度改变你的设计,简单地删除指针的特殊代码。你期望编写这样的代码吗:Node<int*> *n2 = new Node<int*>( new int );?如果是这样,你希望它如何运作?

如果NType实际上是一个指针类型,比如int*,那么做deletePointer(data)可能是有意义的,但做deletePointer(&data)永远不会有意义。


还有@TechWiz,这是C++,不是Java。你说:“...但应该是垃圾回收的一部分”。 - Aaron McDaid
很抱歉,你错了。它完全正常,请自行尝试。程序既不会崩溃也不会创建内存泄漏。我知道这不是Java,但默认情况下,在程序执行完成时,所有非指针值都会调用析构函数,只要有一个已定义。请仔细查看代码,静态的“struct is_pointer”在非指针类型上始终为false,因为除非它是指针类型,否则“NType”不会是指针。此外,“deletePointer()”的代码在我的帖子中缺失,但我将很乐意编辑我的问题以包含它。 - TechWiz
它已经在我的问题中发布,位于代码片段的末尾。这是你提供的两种方法中的后者。 - TechWiz
啊,我明白你的意思了。但问题不在于我删除指针的方式,而在于我已经删除了指针这一事实导致了问题。数据变量存在“双重删除”的情况,这很奇怪,因为早些时候它会因为GCC无法正确清理而导致泄漏。感谢你发现了这个问题,否则稍后在这里发布这个错误将会非常尴尬=P - TechWiz
这不是双重删除,而是内存损坏。以下是一个更简单的示例,演示了在ideone上出现的问题。 - Aaron McDaid
显示剩余23条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接