【问题标题】:"Proper C++ solution"-- is c-style logic 'bad' when using C++?“正确的 C++ 解决方案”——使用 C++ 时,c 风格的逻辑是否“不好”?
【发布时间】:2011-08-23 09:36:35
【问题描述】:

所以最近我开始讨论如何解决一个问题,具体问题是:我如何找到 1 到 100 万之间的所有回文。我说,“用atoi做一个字符串,用for循环反转字符串,用strcmp比较有问题的字符串。

几分钟后,有人问“为什么要在 C++ 中使用 C 风格的解决方案”。我发现自己对使用直接且易于理解的代码解决这个问题的简单、更“C++”的方法感到困惑。有人愿意在这个上照亮我吗?

编辑:itoa 不是 atoi

【问题讨论】:

  • atoi 做一个字符串,不是反过来吗?你不是说sprint之类的吗?
  • 顺便说一句,创建字符串的反向副本并比较它们是一种非常检查单词是否为回文的低效方法
  • 把字符串倒过来很浪费,就地检查一下就可以了
  • 不是检查回文,为什么不直接generate所有回文呢?这可以通过“镜像”0 到 999 之间的所有值来轻松完成,例如。
  • 检查回文是非常低效的解决问题的方法。 1000000以下的范围内只有1800个,很容易全部生成。

标签: c++ c


【解决方案1】:

很简单,C++ 流保证是内存安全和异常安全的,失败不同于任何返回值,C++ 字符串是内存安全和异常安全的。 C-strings 和atoi 在人类已知的几乎所有方面都非常不安全。以这种方式编写的代码更容易出错。

【讨论】:

  • 您能否详细说明它们在哪些方面不安全以及为什么?
  • @HunderingThooves:C 字符串是 \0 终止的,如果它们被终止以防你忘记的话。通过忘记一些极端情况(-> 超出范围的索引),您可能会读/写应该读/写的内存区域。就像在每个 STL 东西中一样,错误的使用通常会导致抛出异常而不是段错误。无论如何,在某些情况下,当您需要更高的性能并且您知道自己在做什么时,“C 风格”是有意义的。
  • @user694971:同意你的主要分析。但声称 C 具有更好的性能是不正确的。 C++ 字符串的使用通常与 C-Strings 一样好或更好(C-Strings 的问题是如果不遍历它们就永远不知道大小,这是一整套性能问题)。由于 STL 都是模板(d),因此编译器通常会消除任何低效率,并且生成的代码将与其 C 对应物一样高效(只是更安全)。
  • @Martin - 我很高兴有人质疑“C 风格更快”的假设。在我看来,如果 C++ 风格较慢(这实际上是一个问题),那么它就是一个编译器缺陷 :)
  • @awoodland:假设我们有完美的编译器,我想知道为什么(提前)编译的语言之间有任何区别:P
【解决方案2】:

示例 C++ 解决方案:

#include <algorithm>
#include <iterator>
#include <iostream>
#include <boost/iterator/counting_iterator.hpp>
#include <boost/lexical_cast.hpp>

namespace {
   bool is_palindrome(unsigned int i) {
     const std::string& s = boost::lexical_cast<std::string>(i);
     return std::equal(s.begin(), s.end(), s.rbegin());
   }

   const unsigned int stop = 1000000;
}

int main() {
   std::remove_copy_if(boost::counting_iterator<unsigned int>(1),
                       boost::counting_iterator<unsigned int>(stop),
                       std::ostream_iterator<unsigned int>(std::cout, "\n"),
                       std::not1(std::ptr_fun(is_palindrome)));
}

(我用std::remove_copy_if来弥补C++0x中std::copy_if的不足)

为了完整起见,我实现了一个生成回文的版本,而不是针对谓词测试候选者:

#include <boost/lexical_cast.hpp>
#include <boost/iterator/counting_iterator.hpp>
#include <boost/iterator/transform_iterator.hpp>
#include <algorithm>
#include <iterator>
#include <iostream>
#include <cassert>

namespace {
  template <int ver>
  unsigned int make_palindrome(unsigned int i) {
     std::string s = boost::lexical_cast<std::string>(i);
     assert(s.size());
     s.reserve(s.size()*2);
     std::reverse_copy(s.begin(), s.end()-ver, std::back_inserter(s));
     return boost::lexical_cast<unsigned int>(s);
  }
}

int main() {
   typedef boost::counting_iterator<unsigned int> counter;
   std::merge(boost::make_transform_iterator(counter(1), make_palindrome<1>),
              boost::make_transform_iterator(counter(999), make_palindrome<1>),
              boost::make_transform_iterator(counter(1), make_palindrome<0>),
              boost::make_transform_iterator(counter(999), make_palindrome<0>),
              std::ostream_iterator<unsigned int>(std::cout, "\n"));
}

(我认为我可以使用 boost.range 而不是 std::merge

我想讨论的重点是“这是一种更好的*方式来编写它吗?”。我喜欢以这种风格编写诸如回文之类的问题的一点是,您会得到“如果它编译它可能是正确的”启发式。即使存在错误,它仍然会在运行时得到合理处理(例如来自lexical_cast 的异常)。

这是一种与 C 编程明显不同的思维方式(但在某些方面与 Haskell 奇怪地相似)。它带来了许多额外安全方面的好处,但编译器错误消息可能很糟糕,并且很难改变您对问题的看法。

归根结底,重要的是“工作量越少,错误越少?”。如果没有一些指标提供帮助,我无法回答这个问题。

*对于更好的一些定义。

【讨论】:

  • 老实说,我认为最后一行与for (unsigned int i = 1; i &lt; stop; ++i) { if (is_palendrome(i)) { std::cout &lt;&lt; i &lt;&lt; '\n'; } } 相比有点过分。但它表明这些工具可用于改进代码的情况。
  • 这里的ostream_iterator 可能是正确的,但如果它插入到列表中,我可能更可能在这里使用“stlified”形式。缺少std::copy_if 也使示例不那么直观也无济于事。
  • 这是一个很好的说明,我认为,以“C++ 方式”进行操作是需要考虑的事情,但不一定总是 :-) 缺少copy_if 是标准中的一个特定缺陷,这意味着“C++03 方式”比它应有的更糟糕,因此比存在它时更有可能输给 for 循环。
  • 在您的生成器代码中,也许使用ver 作为模板参数的模板函数会比boost::bind 更好地共享代码。当然不那么冗长。
  • 我也不认为您使用 std::reverse_copy 是安全的,因为插入 s 会使迭代器无效(实际上至少在 VC++ 上不会,但不能保证行为),除非您先预订尺寸。
【解决方案3】:

与您的解决方案等效的 C++ 是:

  1. 使用 stringstream 将数字转换为 std::string。
  2. 使用std::reverse_copy 反转字符串。
  3. 使用 == 比较字符串。

1 比使用itoa(您可能是这个意思)要好,因为您不必自己为创建的字符串分配内存,也不会出现缓冲区溢出。

2 更好,因为您不必再​​担心为反转字符串分配内存,也不会复制现有功能。

3 更好,因为string1 == string2 比使用 strcmp 读取更好。

【讨论】:

  • 我将步骤 2 替换为“使用 std::reverse_copy 反转字符串”。如果你打算使用 C++ 还不如一路走下去。 :) 如果可以的话,我也更喜欢使用 boost::lexical_cast 进行转换。
  • 而不是 2 和 3 执行一个循环到 length / 2 并比较 string[i]string[length - i - 1]效率更高
  • @Andreas Brinck:同意,我的建议坚持问题的原始参数。 :)
  • @Andreas:但不等同于 OP 的算法。重点是比较同一算法的 C 和 C++ 版本。
  • @Andreas Brinck, @Skizz:C++ 版本的回文检查怎么样:std::equal(s.begin(), s.begin() + s.length() / 2, s.rbegin()); :)
【解决方案4】:

atoi 使得无法检测输入错误。而stringstream 可以完成相同的工作,并且可以轻松检测到错误。

【讨论】:

    【解决方案5】:

    我认为一个更合适的问题是为什么不应该在 C++ 中使用基于 C 的解决方案?如果它比“纯”C++ 解决方案更简单、更易读,我知道我会选择 C ​​风格的解决方案。鉴于您想出的解决方案既聪明又简单,相应的 C++ 解决方案可能有点矫枉过正,而且考虑到问题的简单性,我不确定 C++ 可以为解决方案带来什么建议。

    【讨论】:

    • 这个答案中有很多假设“如果它[C解决方案]更简单......相应的C++解决方案可能是矫枉过正”,没有真正的理由。
    • 也正如其他答案所指出的,原来的解决方案并不是特别聪明,C++ 会带来很多额外的安全性,如果你在 C 版本中添加代码,它的可读性会大大降低
    猜你喜欢
    • 2013-10-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-15
    • 1970-01-01
    • 2016-03-27
    • 2015-04-01
    • 2010-09-21
    相关资源
    最近更新 更多