第四个项目,用可怕的帖子名继续了我的C ++传奇。 :P


pi的近似值可以使用下面给出的序列
计算:

$$ \ pi \ approx 4 \ left [1 -\ dfrac {1} {3} + \ dfrac {1} {5}-\ dfrac {1} {7} +
\ dfrac {1} {9}-\ cdots + \ dfrac {\ left( -1 \ right)^ n} {2n + 1} \ right] $$

编写一个C ++程序,使用该系列计算pi的近似值。程序采用输入\ $ n \ $来确定pi值近似值中的项数,然后输出近似值。包括一个循环,该循环允许用户对新值\ $ n \ $重复此计算,直到用户说出要结束程序为止。


另外,我还必须在我的代码中至少使用一次for循环。

pi.cpp

/**
 * @file pi.cpp
 * @brief Calculates pi for the given number of terms
 * @author syb0rg
 * @date 10/3/14
 */

#include <iostream>
#include <limits>
#include <cctype>
#include <cmath>

/**
 * Makes sure data isn't malicious, and signals user to re-enter proper data if invalid
 */
void getSanitizedDouble(long double &input)
{
    while (!(input = std::cin.peek()) && input != '\n')
    {
        if (std::isalpha(input) || std::isspace(input)) std::cin.ignore(); // ignore alphabetic and space characters from input
    }
    while(!(std::cin >> input) || input < 0)
    {
        std::cin.clear(); // clear the error flag that was set so that future I/O operations will work correctly
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // skips to the next newline
        std::cout << "Invalid input.  Please enter a positive number: ";
    }
}

int main()
{
    long double num = 0.;
    char again = 'q4312078q';
    do
    {
        long double pi = 0.;

        // get input for height, re-read input if not a positive number
        std::cout << "Enter the number of terms to approximate π: ";
        getSanitizedDouble(num);

        for(auto i = 0; i < num; i++)
        {
            pi += std::pow(-1, i) / (2 * i + 1);
        }
        pi *= 4;

        std::fprintf(stdout, "Approximated value of π for %Lg terms: %Lg\n", num, pi);

        std::cout << "Run the program again (y/N): ";  // signify n as default with capital letter
        std::cin.get();  // absorb newline character from previous input
        std::cin.get(again);
        again = std::tolower(again);
    } while (again == 'y');
}


#1 楼

C ++中的Out参数

在C ++中,您几乎永远都不要使用out参数(通过引用获取并用于从函数返回值的变量),您可以阅读Eric Niebler的精彩文章。在少数情况下,使用out参数可能有意义:


当您希望快速返回时。即使这样,返回值优化和移动语义仍然可能更快。

当您有多个输出值时。例如,您想为一个函数赋值并返回它是否成功:

bool assign(int from, int& to)
{
    if (from != 0)
    {
        to = from;
        return true;
    }
    return false;
}


但是即使在这种情况下,也有更好的解决方案,例如boost:optional返回无论是价值还是成功与否。而且,如果您需要多个错误值,请使用异常。而且,如果您实际上需要返回几个值,则通常需要将它们打包到专用的structstd::tuple中。

输入输出参数:有时,您希望获取一个参数,从中读取,然后再写一次。那仍然是一个有效的用例(但是这些不再是严格的out参数)。

在您的情况下,似乎getSanitizedDouble可以简单地return读取long double而不使用out参数。 >
类型正确性

您可以改善类型正确性:


这也是一个命名问题:我不希望将名为getSanaitzeDouble的函数用于返回long double,但返回double
表达式long double pi = 0.;中使用的文字不正确:0.double。正确的long double文字应为0.0L。我们也可以使用0.0l,但是后缀l确实太接近1了,不会造成危险。您的标题(至少在逻辑的标题组中)。这样可以更快地进行搜索,以检查是否已包含一些标头:

#include <cctype>
#include <cmath>
#include <iostream>
#include <limits>


评论


\ $ \ begingroup \ $
\ $ \ endgroup \ $
–Memleak
14-10-3在11:56



\ $ \ begingroup \ $
@Memleak我提到了逻辑分组。但是在逻辑组内部,您并不总是具有明显的逻辑结构,并且按字母顺序排序是可以的。老实说,从一开始就不需要花费任何时间(除非您不记得字母),并且它使您不必在每次添加内容时都必须运行静态分析器。
\ $ \ endgroup \ $
–莫文
2014年10月4日在18:28

#2 楼

简化输入解析

我没有看到这部分的目的:


while (!(input = std::cin.peek()) && input != '\n')
{
    if (std::isalpha(input) || std::isspace(input)) std::cin.ignore();
}



我想您的意图是在输入的开头忽略字母字符,但这对我不起作用。而且我认为也没有必要。如果我输入blah333作为输入,我不会期望任何程序将其视为333。关于跳过空格,std::cin >> input可以正常工作,对于333,您将正确获得333

为什么用long double代替long long


在问题描述中,它看起来像n是整数。并且在代码中,没有任何内容需要该数字成为double。那么,为什么不使用long long呢?

如果更改为long long,记得要重命名的方法从getSanitizedDoublegetSanitizedNum什么的,并更改%Lgfprintf%lld

不必要的初始化

和以前的程序一样,这些初始化是没有意义的,因为您不可避免地会在之后不久重新分配这些变量:


long double num = 0.;
char again = 'q4312078q';

<正如@ syb0rg所指出的那样,在声明中进行初始化是一种已知的最佳实践:


始终在声明变量时对其进行初始化。这样一来,如果我事先没有给变量赋值,就不会意外地访问这些变量中的值。


编码样式

我会建议不要在语句后的行末添加注释,
但要在前一行添加注释。特别是评论过长,迫使读者向右滚动。

评论


\ $ \ begingroup \ $
关于您的“不必要的初始化”,我学会了始终在声明变量时进行初始化。这样一来,如果我事先没有给变量赋值,我就不会偶然尝试访问这些变量中的值。
\ $ \ endgroup \ $
–syb0rg
2014-09-30 18:59

#3 楼

该代码与p无关,而是与用户交互。这篇评论着重于数学。

numdouble,所以我想可能是i。将负数提高为非整数幂可能会给您带来问题:


如果base为有限且为负,而exp为有限且非整数,则将发生域错误并且范围可能会发生错误。


完全避免pow,而仅在1到-1之间切换是更安全(且更快)的方法。

当然main做得太多。至少您需要将实际的pi计算提取到函数中。

#4 楼

让我们再次读取任务


程序采用输入\ $ n \ $来确定\ $ \ pi \ $值的近似值中的项数,并输出近似值。包括一个循环,使用户可以对新值\ $ n \ $重复此计算,直到用户说她想结束程序。


我认为这意味着遵循程序流程:重复获取\ $ n \ $输入值的升序(因为它是近似的项,而不是其他迭代的数目)。

在开始之前

让我们批准程序流程。


首先我们需要确定:它会继续还是中断主循环。
然后我们需要一个函数,该函数根据\ $ \ pi_ {返回\ $ \ pi_i \ $ i-1},n_ {i-1},n_i \ $,其中\ $ i \ $是项的数量近似输入序列中的当前索引。可以用以下方式表示:

$$ \ pi_i = \ pi_ {i-1} + 4 \ left [\ dfrac {(-1)^ {n_ {i-1} + 1}} {2n_ {i-1} +3} + \ cdots + \ dfrac {\ left(-1 \ right)^ {n_i}} {2n_i + 1} \ right],\ pi _ {-1} = 0 $$

换句话说,它以间隔\ $(n_ {i-1},n_i] \ $返回函数\ $ \ pi \ $的增量。

结果代码

#include <iostream>
#include <algorithm>

double pi_increment(const int &from, const int& to) {
    double result = 0;
    for (int n = from + 1; n <= to; ++n) {
        result += (n & 1 ? -1.0 : 1.0) / (2 * n + 1);
    }
    return 4 * result;
}

inline bool check_choice(const char &choice) {
    return choice != 'n' && choice != 'N';
}

int main(int argc, char *argv[]) {
    int previous_n = -1,
        current_n = -1;
    double pi = 0;
    std::cout.precision(15);
    do {
        std::cout << "Write number of terms: ";
        while (previous_n >= current_n) {
            std::cin >> current_n;
            if (previous_n >= current_n) {
                std::cout << "Number of terms should be greater than "
                          << previous_n << ". Enter new value " << std::endl;
            }
        }

        pi += pi_increment(previous_n, current_n);
        std::cout << "Pi(" << current_n << ") equals to " << pi << std::endl;
        std::swap(previous_n, current_n);

        std::cout << "Would you like to continue (Y/n)? ";
        std::cin.ignore();
    }
    while (check_choice(std::cin.get()));
}


评论


\ $ \ begingroup \ $
我认为您在错误理解需求:OP的方式就是需求所要求的。
\ $ \ endgroup \ $
– GreenAsJade
2014年10月1日在7:12



\ $ \ begingroup \ $
@GreenAsJade,好吧,你可以问问TC。
\ $ \ endgroup \ $
–超时
2014年10月1日在7:15

#5 楼

结构

尽管您已经分离出了读取用户输入的功能,但是您的算法在main()中,与打印和交互式循环混合在一起。我会将算术运算符添加到具有此签名的函数中:

long double compute_pi(unsigned int num_places);


这具有两个优点:


您可以使用不同的main()实现(例如,由于要在下面说明的原因,我宁愿指定位数作为参数)。
您可以使用不同的compute_pi()实现(因此您可以比较此算法的收敛性

还要注意num_places的类型-将其指定为浮点数是没有意义的。

如果我更改从num_placesunsigned long,我可以制作一个非常简单的测试程序(只需很少的检查):

#include <iostream>
#include <string>

int main(int, char **argv)
{
    while (*++argv) {
        auto n = std::stoul(*argv);
        std::cout << n << " terms: " << compute_pi(n);
    }
}


从命令行参数读取n的优点包括:


可以轻松编写脚本-例如在bash中:for i in {1..15}; do ./64297 $i; done显示总和如何收敛。
可以在time中运行该程序,以查看代码更改如何影响执行速度。

算术>现在开始实际执行。我可以将您的代码提取到函数中:

#include <cmath>
long double compute_pi(unsigned long num_places)
{
    long double pi = 0.;
    for (auto i = 0u; i < num_places; i++) {
        pi += std::pow(-1, i) / (2 * i + 1);
    }
    pi *= 4;
    return pi;
}


我将i的初始化器更改为0ul,以与num_places一致。这里突出的一件事是昂贵的std::pow电话。因为我们只是用它来获得1或-1,所以我们可以检查i是偶数还是奇数:i % 2 ? -1 : 1
保持某种状态,并交换符号每次循环时。

我将在此处采用第二种选择:

long double compute_pi(unsigned long num_places)
{
    long double pi = 0.;
    long double numerator = 1;
    for (auto i = 0ul;  i < num_places;  i++) {
        pi += numerator / (2 * i + 1);
        numerator = -numerator;
    }
    pi *= 4;
    return pi;
}


类似地,我们不乘以分母,而是可以从1开始并在循环中每次加2:

long double compute_pi(unsigned long num_places)
{
    long double pi = 0.;
    long double numerator = 1;
    unsigned long denominator = 1;
    for (auto i = 0ul;  i < num_places;  i++) {
        pi += numerator / denominator;
        numerator = -numerator;
        denominator += 2;
    }
    pi *= 4;
    return pi;
}

在计算几千项时,这两项改进都具有可衡量的性能改进。重写循环条件的好处要小得多:

while (num_places--) {
    ...
}



完成程序

这是我的最终版本。如果您仍然喜欢交互式循环,当然可以使用自己的main()

long double compute_pi(unsigned long num_places)
{
    long double pi = 0.;
    long double numerator = 1;
    unsigned long denominator = 1;
    while (num_places--) {
        pi += numerator / denominator;
        numerator = -numerator;
        denominator += 2;
    }
    return pi * 4;
}

#include <iostream>
#include <iomanip>
#include <limits>
#include <string>
int main(int, char **argv)
{
    std::cout << std::setprecision(1 + std::numeric_limits<long double>::digits10);
    while (*++argv) {
        auto n = std::stoul(*argv);
        std::cout << compute_pi(n) << " (to " << n << " terms)" << std::endl;
    }
}


程序输出

 g++ -std=c++17 -fPIC -g -Wall -pedantic -Wextra -Wwrite-strings -Wno-parentheses -Weffc++ -O3 64297.cpp -o 64297
./64297 0 1 10 100 1000 10000 100000 1000000 10000000 100000000 1000000000
0 (to 0 terms)
4 (to 1 terms)
3.041839618929402211 (to 10 terms)
3.131592903558552764 (to 100 terms)
3.140592653839792927 (to 1000 terms)
3.141492653590043241 (to 10000 terms)
3.141582653589793492 (to 100000 terms)
3.141591653589793185 (to 1000000 terms)
3.141592553589793097 (to 10000000 terms)
3.141592643589794078 (to 100000000 terms)
3.141592652589795228 (to 1000000000 terms)
 


您可以看到该算法收敛非常慢。但是,现在您有了结构,就可以轻松使用替代方案。