How do I restrict a float value to only two places after the decimal point in C?

前端 未结 17 3055
孤城傲影
孤城傲影 2020-11-22 03:22

How can I round a float value (such as 37.777779) to two decimal places (37.78) in C?

17条回答
  •  余生分开走
    2020-11-22 03:50

    Always use the printf family of functions for this. Even if you want to get the value as a float, you're best off using snprintf to get the rounded value as a string and then parsing it back with atof:

    #include 
    #include 
    #include 
    #include 
    
    double dround(double val, int dp) {
        int charsNeeded = 1 + snprintf(NULL, 0, "%.*f", dp, val);
        char *buffer = malloc(charsNeeded);
        snprintf(buffer, charsNeeded, "%.*f", dp, val);
        double result = atof(buffer);
        free(buffer);
        return result;
    }
    

    I say this because the approach shown by the currently top-voted answer and several others here - multiplying by 100, rounding to the nearest integer, and then dividing by 100 again - is flawed in two ways:

    • For some values, it will round in the wrong direction because the multiplication by 100 changes the decimal digit determining the rounding direction from a 4 to a 5 or vice versa, due to the imprecision of floating point numbers
    • For some values, multiplying and then dividing by 100 doesn't round-trip, meaning that even if no rounding takes place the end result will be wrong

    To illustrate the first kind of error - the rounding direction sometimes being wrong - try running this program:

    int main(void) {
        // This number is EXACTLY representable as a double
        double x = 0.01499999999999999944488848768742172978818416595458984375;
    
        printf("x: %.50f\n", x);
    
        double res1 = dround(x, 2);
        double res2 = round(100 * x) / 100;
    
        printf("Rounded with snprintf: %.50f\n", res1);
        printf("Rounded with round, then divided: %.50f\n", res2);
    }
    

    You'll see this output:

    x: 0.01499999999999999944488848768742172978818416595459
    Rounded with snprintf: 0.01000000000000000020816681711721685132943093776703
    Rounded with round, then divided: 0.02000000000000000041633363423443370265886187553406
    

    Note that the value we started with was less than 0.015, and so the mathematically correct answer when rounding it to 2 decimal places is 0.01. Of course, 0.01 is not exactly representable as a double, but we expect our result to be the double nearest to 0.01. Using snprintf gives us that result, but using round(100 * x) / 100 gives us 0.02, which is wrong. Why? Because 100 * x gives us exactly 1.5 as the result. Multiplying by 100 thus changes the correct direction to round in.

    To illustrate the second kind of error - the result sometimes being wrong due to * 100 and / 100 not truly being inverses of each other - we can do a similar exercise with a very big number:

    int main(void) {
        double x = 8631192423766613.0;
    
        printf("x: %.1f\n", x);
    
        double res1 = dround(x, 2);
        double res2 = round(100 * x) / 100;
    
        printf("Rounded with snprintf: %.1f\n", res1);
        printf("Rounded with round, then divided: %.1f\n", res2);
    }
    

    Our number now doesn't even have a fractional part; it's an integer value, just stored with type double. So the result after rounding it should be the same number we started with, right?

    If you run the program above, you'll see:

    x: 8631192423766613.0
    Rounded with snprintf: 8631192423766613.0
    Rounded with round, then divided: 8631192423766612.0
    

    Oops. Our snprintf method returns the right result again, but the multiply-then-round-then-divide approach fails. That's because the mathematically correct value of 8631192423766613.0 * 100, 863119242376661300.0, is not exactly representable as a double; the closest value is 863119242376661248.0. When you divide that back by 100, you get 8631192423766612.0 - a different number to the one you started with.

    Hopefully that's a sufficient demonstration that using roundf for rounding to a number of decimal places is broken, and that you should use snprintf instead. If that feels like a horrible hack to you, perhaps you'll be reassured by the knowledge that it's basically what CPython does.

提交回复
热议问题