【问题标题】:Making the leap from Java to C++实现从 Java 到 C++ 的飞跃
【发布时间】:2010-10-08 01:59:55
【问题描述】:

正如主题所说,我对 c++ 很陌生,但我对 java 有一些经验。 为了开始学习 c++,我有制作一个简单的命令行计算器的(不是很原始的)想法。 我要做的是将数字和运算符存储在二叉树中。

#include <iostream>
using namespace std;

class Node
{
  bool leaf;
  double num;
  char oper;
  Node* pLNode;
  Node* pRNode;

public:

  Node(double n)
  {
    num = n;
    leaf = true;
    pLNode = 0;
    pRNode = 0;
  }

  Node(char o, Node lNode, Node rNode)
  {
    oper = o;
    pLNode = &lNode;
    pRNode = &rNode;
    leaf = false;
  }

  bool isLeaf()
  {
    return leaf;
  }

  double getNumber()
  {
    return num;
  }

  char getOperator()
  {
    return oper;
  }

  Node* getLeftNodePointer()
  {
    return pLNode;
  }

  Node* getRightNodePointer()
  {
    return pRNode;
  }

  //debug function
  void dump()
  {
    cout << endl << "**** Node Dump ****" << endl;
    cout << "oper: " << oper << endl;
    cout << "num: " << num << endl;
    cout << "leaf: " << leaf << endl;
    cout << "*******************" << endl << endl;
  }

};

class CalcTree
{
  Node* pRootNode;
  Node* pCurrentNode;
public:

  Node* getRootNodePointer()
  {
    return pRootNode;
  }

  Node* getCurrentNodePointer()
  {
    return pCurrentNode;
  }

  void setRootNode(Node node)
  {
    pRootNode = &node;
  }

  void setCurrentNode(Node node)
  {
    pCurrentNode = &node;
  }

  double calculateTree()
  {
    return calculateTree(pRootNode);
  }

private:

  double calculateTree(Node* nodePointer)
  {
    if(nodePointer->isLeaf())
    {
      return nodePointer->getNumber();
    }
    else
    {
      Node* leftNodePointer = nodePointer->getLeftNodePointer();
      Node* rightNodePointer = nodePointer->getRightNodePointer();
      char oper = nodePointer->getOperator();

      if(oper == '+')
      {
    return calculateTree(leftNodePointer) + calculateTree(rightNodePointer);
      }
      else if(oper == '-')
      {
    return calculateTree(leftNodePointer) - calculateTree(rightNodePointer);
      } 
      else if(oper == '*')
      {
    return calculateTree(leftNodePointer) * calculateTree(rightNodePointer);
      }
      else if(oper == '/')
      {
    return calculateTree(leftNodePointer) / calculateTree(rightNodePointer);
      }
    }
  }
};

int main(int argc, char* argv[])
{
  CalcTree tree;
  tree.setRootNode(Node('+', Node(1), Node(534)));
  cout << tree.calculateTree() << endl;
  return 0;
}

我有几个关于这段代码的问题:

  1. 这会编译,但不会执行预期的操作。似乎在 tree.setRootNode(Node('+', Node(1), Node(534))); 之后在 main 中,rightnode 已正确初始化,但 leftnode 未正确初始化。编译并运行它会为我打印出 534(gcc,freebsd)。这里有什么问题?

  2. 在 c++ 中,人们似乎更喜欢在类之外定义类的成员,例如

    A 级 { 民众: 无效成员(); };

    A :: member(){std::cout

    这是为什么呢?

  3. 我非常想要一些关于 c++ 约定(命名、缩进等)的指针

  4. 我习惯用 eclipse 编写 java 代码。 Atm 我正在使用 emacs 来学习 c++。有人可以就一个好的(免费)c++ ide给我建议,还是我应该像一个真正的男人一样坚持使用emacs? :)

【问题讨论】:

  • 为什么要将数字存储在二叉树中? :|
  • 这不仅仅是将数字存储在二叉树中!

标签: c++


【解决方案1】:

C++ 有非常不同的约定。谷歌一些。没有“官方”约定。 ;)

对于您的 IDE 问题:我正在使用 CDT(C[/C++] 开发工具,Eclipse)。另一个基于 Eclipse 的 C++ IDE 已经发布了第一个版本:http://www.eclipse.org/linuxtools/ 这些天我将对其进行测试。听起来很不错。

如果您使用 KDE,也可以试试 KDevelop。

【讨论】:

    【解决方案2】:
    2. It seems in c++ people prefer to define members of a class outside the 
    class[..]
    

    这是风格问题。大多。我们将大的方法分组到一个单独的cpp 文件中,并将小的方法与头文件一起保存。在类声明中声明方法使函数内联(这是编译器的提示,你猜对了——内联)。这取决于您要解决的问题,您可能想要也可能不想要。

    3. I'd very much like some pointers on c++ conventions (naming, indenting etc.)
    

    标准库是一个很好的参考。开始查看标题。

    4. I'm used to coding java with eclipse. 
    

    Eclipse 可以配置为使用 C++ 编译器。去(ogle)为它,请。既然你已经学会了 C++,为什么还要惩罚自己两次 ;-)

    【讨论】:

      【解决方案3】:
      • 首先,我只能推荐看看《Accelerated C++》这本书。它将让您快速进入 STL 和 C++ style,并且可以为您节省一年的糟糕体验。 (如果您决定深入了解,那里有大量关于 C++ 的优秀文献)。

      • C++ 不在堆上使用自动内存管理。您正在存储指向临时对象的指针。您的程序不正确,因为它试图访问被破坏的对象。不管你喜不喜欢,你必须先了解对象的生命周期。在简单的情况下,您可以通过使用值语义(不是存储指针,而是存储对象的副本)来简化其中的大部分内容。

      • 据报道 Eclipse/CDT 在 linux 上运行良好。在 Windows 上,使用 Microsoft Visual C++ Express Edition 会更轻松。当您掌握了基础知识后,以后切换对您来说没有问题。

      • 在类本身之外定义成员是首选,因为我们通常拆分头文件和实现文件。在头文件中尽量不暴露任何不必要的信息,同时减少代码大小,这是一个简单的编译时间问题。但是对于许多现代编程技术(例如使用模板元编程)来说,这是无法使用的,因此相当多的 C++ 代码朝着内联定义的方向发展。

      【讨论】:

        【解决方案4】:
        1. 你说的是价值,而不是引用

          节点(字符 o,节点 lNode,节点 rNode)

        应该是

        Node(char o, Node &lNode, Node &rNode)
        

        或更好(为了与您的其余代码保持一致),

        Node(char o, Node *lNode, Node *rNode)
        
        1. 编译速度: 标头(.h 文件)包含未嵌入 .o 文件中的额外信息,因此必须由包含它的所有其他 C++ 文件重新编译。 空格:如果包含方法主体,它们会在包含 .h 文件的每个文件中重复。这与 Java 不同,Java 中所有相关信息都嵌入在 .class 文件中。 C++ 无法做到这一点,因为它是一种更丰富的语言。特别是,宏使得 C++ 永远无法将所有信息嵌入到 .o 文件中。此外,图灵完备的模板使摆脱 .h / .cpp 区别变得具有挑战性。

        2. 有很多约定。标准 C++ 库有一套,标准 C 库有另一套,BSD 和 GNU 各有一套,微软使用另一套。在命名标识符和缩进方面,我个人喜欢非常接近 Java。

        3. Eclipse、NetBeans、KDevelop、vim 和 emacs 都是不错的选择。如果您使用的是 Windows,Visual Studio 真的很棒。

        【讨论】:

          【解决方案5】:

          首先,我也向您推荐一本好书。有关于 C++ 书籍的非常好的 SO 线程,例如 The definitive C++ book guide and List。在下文中,你会发现我会告诉你如何解决一些问题,但我不会深入细节,因为我认为这是一本书可以做得更好的。

          我浏览了代码,这是我的想法:

            Node(char o, Node lNode, Node rNode)
            {
              oper = o;
              pLNode = &lNode;
              pRNode = &rNode;
              leaf = false;
            }
          

          该构造函数有 3 个参数,所有这些参数都是函数的本地参数。当函数返回时,参数不再存在,它们占用的内存被自动清理。但是您将它们的地址存储到指针中。那将失败。你想要的是传递指针。顺便说一句,总是使用构造函数初始化列表:

            Node(char o, Node *lNode, Node *rNode)
                :oper(o), pLNode(lNode), pRNode(rNode), leaf(false)
            { }
          

          现在创建树看起来确实有所不同:

            CalcTree tree;
            tree.setRootNode(new Node('+', new Node(1), new Node(534)));
            cout << tree.calculateTree() << endl;
          

          New 创建一个动态对象并返回一个指向它的指针。指针不能丢失 - 否则你有内存泄漏,因为你不能再删除对象了。现在,通过为 Node 创建析构函数来确保删除子节点(就像放置任何其他成员函数一样):

            ~Node() {
                delete pLNode;
                delete pRNode;
            }
          

          在这里您看到始终将指针设为空是很重要的:空指针上的删除将无济于事。要开始清理,您还要创建一个 CalcTree 析构函数,它会启动删除链:

            ~CalcTree() {
                delete pRootNode;
            }
          

          不要忘记为 CalcTree 创建一个将指针初始化为 0 的默认构造函数!现在,您还有一个问题:如果您复制对象,原始对象和副本共享相同的指针,并且当第二个对象(副本)超出范围时,它将再次调用指针上的 delete,从而两次删除同一个对象。那真不幸。它可以通过禁止复制你的类来解决 - 或者通过使用具有共享所有权语义的智能指针(查看 shared_ptr)。第三种变体是编写自己的复制构造函数和复制赋值运算符。好吧,这里是你如何禁用复制构造函数和复制赋值运算符。一旦你尝试复制,你会得到一个编译错误。把它放到Node中:

            private:
                Node(Node const&);
                Node& operator=(Node const&);
          

          对于 CalcTree 也是如此,您现在可以免受该微妙错误的影响。

          现在,关于您的其他问题:

          似乎在 c++ 中人们更喜欢在类之外定义类的成员

          这是因为您添加到标头的代码越多,您必须从其他文件中将更多的代码包含到您的标头中(因为您的代码取决于其中定义的内容)。请注意,包含该标头的所有其他文件随后将传递地包含一个包含的标头。因此,当您将代码放入单独编译的文件中时,最终会减少间接包含的标头。另一个解决的问题是循环引用。有时,您必须编写一个需要访问稍后在标头中定义的内容的方法。由于 C++ 是单通道语言,因此编译器无法解析对在使用点之后声明的符号的引用 - 通常。

          我非常想要一些关于 c++ 约定的指针

          这是非常主观的,但我喜欢这样的约定:

          • 类数据成员的写法类似于 mDataMember
          • 函数写成getDataMember
          • 局部变量写成localVariable
          • 用空格缩进,每个4个空格(呃,哦,这个一个是有争议的。有很多可能的缩进方式,请不要要求最好的!)

          我习惯用 eclipse 编写 java 代码。 Atm 我正在使用 emacs 学习 c++。

          我在 C++ 开发中使用 emacs,在 Java 中使用 eclipse。有些人也使用 eclipse for Java(有一个名为 CDT 的包用于 eclipse C++ 开发)。似乎很常见的要点(我同意)Windows 上的 Visual C++ 是您为 Windows 获得的最佳 IDE。对此也有一些关于 SO 的答案:Best IDE for C++(在 stackoverflow 中使用 google 进行最佳搜索,它将为您提供比内置搜索更多的结果)。

          【讨论】:

          • +1,不仅用于代码审查,还特别用于本书指南的链接。我不知道 SO 上的那个页面,它真的是一个很好的集合。
          猜你喜欢
          • 2012-12-10
          • 2022-12-10
          • 2014-08-12
          • 2014-02-26
          • 2011-09-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多