C/C++ 保留小数全攻略:从printf到setprecision
C/C++ 保留小数全攻略:从printf到setprecision
前言
写代码的时候经常遇到保留小数的问题——输出要保留两位小数、计算要四舍五入、有时候还要控制有效数字位数。C和C++在这方面的处理方式不太一样,C有printf那一套格式控制,C++则有cout配合iomanip。本文把这些方法整理了一下,希望能帮你一次性搞清楚。
一、C语言的方法
1. printf 格式化输出
这是C语言最常用的方式,通过%f格式符配合精度修饰符来实现。
基本语法:%.nf
其中n表示要保留的小数位数。例如:
printf("%.2f",3.14159);// 输出 3.14如果不指定精度,%f默认保留6位小数。
进阶用法:%m.nf
m表示整个输出占用的最小宽度(包括整数部分、小数点和小数部分),n表示小数位数。如果实际宽度小于m,左边会补空格。
printf("%8.3f",3.14159);// 输出 " 3.142"(共8位,前面3个空格)注意事项:
- 单精度float的有效位数大约是7位,双精度double大约是16位。如果要求输出的小数位数超出有效位数,结果可能会出错。
printf的%.nf是自带四舍五入的。不过有个小细节:保留0位小数时,只要小数点后第一位 >= 5 就进位;但保留1位或更多位时,需要保留位的下一位 > 5 才会进位。这其实是浮点数二进制表示导致的,后面会细说。
其他格式符:
| 格式符 | 说明 |
|---|---|
%f | 十进制小数形式,默认6位小数 |
%e/%E | 指数形式,如1.23e+02 |
%g/%G | 自动选择%f或%e,去掉多余的0 |
2. sprintf 先格式化再使用
有时候不只是输出,还需要把格式化后的结果存起来供后续使用。这时候可以用sprintf。
charbuffer[50];sprintf(buffer,"%.2f",3.14159);// buffer 中存的是 "3.14"3. round 函数进行四舍五入
printf只是控制显示,并不会改变变量本身的值。如果需要在计算中真正得到一个四舍五入后的数值,可以用round函数。
round函数定义在<math.h>中,C99标准后可用:
doubleround(doublex);floatroundf(floatx);longdoubleroundl(longdoublex);round(3.623)返回4.0。但round只能四舍五入到整数,如果要保留n位小数,需要先乘再除:
doubleroundToNDecimalPlaces(doublenum,intn){doublefactor=pow(10,n);returnround(num*factor)/factor;}编译时需要链接数学库:gcc -lm。
手动实现四舍五入(不依赖math库):
// 保留2位小数doubleresult=(int)(num*100+0.5)/100.0;加0.5再强转取整,本质就是四舍五入。不过要注意浮点数精度问题,这种方法在某些边界情况下可能不准确。
二、C++的方法
C++ 继承了一部分C的方式(printf仍然可用),但更推荐用 cout 配合iomanip头文件中的操纵符。
1. setprecision + fixed(最常用)
需要包含<iomanip>头文件。
#include<iostream>#include<iomanip>usingnamespacestd;intmain(){doublenum=3.1415926;cout<<fixed<<setprecision(2)<<num<<endl;// 输出 3.14return0;}fixed:固定小数点格式,不用科学计数法setprecision(n):设置小数点后的位数为 n- 两者结合使用,自动补0且四舍五入
三种写法:
// 写法一cout<<setiosflags(ios::fixed)<<setprecision(2);// 写法二cout.setf(ios::fixed);cout<<setprecision(2);// 写法三(最简洁)cout<<fixed<<setprecision(2);三种写法效果一样。
注意:setprecision设置一次后,对后续所有浮点数输出都有效,直到被重新设置。如果需要恢复默认,可以cout << setprecision(6)。
2. 只用 setprecision(保留有效数字)
如果不加fixed,setprecision(n)控制的是有效数字的总位数(整数部分 + 小数部分)。
doublenum=1234.56789;cout<<setprecision(5)<<num<<endl;// 输出 1234.6(5位有效数字)- 同样会四舍五入
- 不会自动补0
- 如果整数部分位数超过 n,会改用科学计数法
3. scientific(科学计数法)
cout<<scientific<<setprecision(3)<<num<<endl;// 输出 1.235e+03setprecision(3)控制的是小数点后的位数。
4. C++ 中使用 printf
C++ 完全兼容 C 的printf,用法和C语言一样:
printf("%.2f\n",3.14159);// 输出 3.14printf("%.2lf\n",3.14159);// double 用 %lf5. stringstream 格式化
需要把格式化结果存成字符串时,可以用stringstream:
#include<sstream>#include<iomanip>stringstream ss;ss<<fixed<<setprecision(2)<<3.14159;string result=ss.str();// result = "3.14"6. C++ 中的 round
C++ 同样可以使用round函数,包含<cmath>即可。用法和C完全一样。
#include<cmath>doubleresult=round(3.14159*100)/100.0;// 3.14三、实际开发中的坑
坑1:浮点数精度问题
浮点数在计算机里是二进制表示的,很多十进制小数无法精确表示。比如:
printf("%.2f",2.675);// 可能输出 2.67 而不是 2.68这不是printf的 bug,而是2.675在二进制中实际存储的值略小于2.675。如果对精度要求极高(比如金融计算),建议用整数(分)或者专门的十进制库。
坑2:float 和 double 的区别
用printf时,float用%f,double用%lf。但在printf中,float会自动提升为double,所以用%f也能打印double。不过为了代码清晰,建议该用什么就用什么。
C++ 的cout不区分float和double,统一处理。
坑3:setprecision 的持久性
setprecision设置后会一直生效,这在有些场景下是好事(不用重复写),但也可能造成意料之外的输出:
cout<<fixed<<setprecision(2)<<3.14159<<endl;// 3.14cout<<2.71828<<endl;// 2.72(仍然保留2位小数)如果想恢复,需要重新设置。
坑4:round 的"银行家舍入"
C++11 起,round遵循"远离零"的舍入规则。但某些语言的round是"银行家舍入"(四舍六入五成双),跨语言开发时要注意。
坑5:VS2022 的"不安全函数"警告(C4996)
如果你在 Visual Studio 2022 里写 C/C++ 代码,大概率遇到过这样的报错:
error C4996: 'sprintf': This function or variable may be unsafe. Consider using sprintf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS.不只sprintf,scanf、strcpy、gets这些函数都会触发同样的警告。VS2022 默认启用了一套安全检查机制(Security Development Lifecycle,SDL),把这类传统 C 库函数标记为"不安全"或"已弃用"。
为什么这些函数"不安全"?
根本原因是它们不检查目标缓冲区的大小。比如scanf("%s", arr)读字符串时,如果用户输入了超过arr长度的内容,数据就会越界写入,可能导致程序崩溃甚至被利用执行恶意代码。sprintf也是类似的问题——它只管往缓冲区里写,不管缓冲区够不够大。
三种解决方式
| 方式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 使用安全版本函数 | 把sprintf换成sprintf_s,scanf换成scanf_s | 最安全,符合微软推荐 | 这些函数是 VS 特有的(C11 Annex K 可选规范),代码移植到 Linux 或 GCC 环境可能无法编译 |
定义_CRT_SECURE_NO_WARNINGS | 在文件最开头(所有#include之前)加一行#define _CRT_SECURE_NO_WARNINGS | 代码保持标准 C/C++ 写法,可移植性好 | 禁用了警告,需要自己确保输入安全 |
| 项目属性中关闭 | 项目属性 → C/C++ → 预处理器 → 预处理器定义中添加_CRT_SECURE_NO_WARNINGS | 整个项目统一配置,不用每个文件都写 | 同上 |
一点建议
- 写作业/学习算法:用
#define _CRT_SECURE_NO_WARNINGS最简单省事,把精力放在逻辑上。 - 写正式项目/跨平台代码:优先考虑用
snprintf代替sprintf——它是 C99 标准函数,既安全又跨平台。或者干脆不用scanf,改用fgets+sscanf组合,更可控。 - 初学阶段不建议一上来就用
_s版本:因为换了个编译器可能就不认识了,容易产生困惑。
另外还有一个细节:如果不想每次新建.c文件都手动加一行宏定义,可以修改 VS2022 的默认模板文件newc++file.cpp,把#define _CRT_SECURE_NO_WARNINGS写进去,以后新建文件就自动带上了。
坑6:宽字节环境(wchar_t)下的格式化输出
如果只是写控制台算法题,基本碰不到宽字节。但要是搞 Windows 开发(MFC、COM、Win32 API 或者用wchar_t做国际化),字符串经常是宽字符类型的,保留小数的写法就有变化了。
C 风格宽字节输出
用wprintf替代printf,格式字符串也要加L前缀:
doublenum=3.14159;wprintf(L"%.2f\n",num);// 输出 3.14wprintf对%f、%lf的支持和printf一样,但有个坑:wprintf默认可能无法正确输出非 ASCII 字符,比如中文单位或提示语,这个和终端编码有关(Windows 下是 UTF-16 但控制台默认是 GBK),一般不推荐在 Windows 控制台里用wprintf混着打印中文和数字。
C++ 风格宽字节输出
用wcout替代cout,操纵符用法完全一致:
doublenum=3.14159;wcout<<fixed<<setprecision(2)<<num<<endl;// 输出 3.14转字符串时的"参数顺序陷阱"
printf对应的宽字节版本是wprintf,sprintf对应的是swprintf。但这里有一个跨平台的大坑:
- Linux / 标准 C(C99):
swprintf的签名是int swprintf(wchar_t *wcs, size_t maxlen, const wchar_t *format, ...);,必须传缓冲区长度。 - Windows(旧版 CRT):
swprintf的签名是int swprintf(wchar_t *buffer, const wchar_t *format, ...);,跟sprintf一样不检查长度!直到 VS2015 后微软才改了实现,让它兼容标准行为。
这就导致同样一行swprintf(buf, L"%.2f", num),在 Linux 上能编译,在旧版 VS 上也能编译,但参数含义完全不同——Linux 把L"%.2f"当长度参数解析,直接报错或崩溃。好在 VS2022 里swprintf已经按标准实现了,但为了保险,微软更推荐直接用swprintf_s:
swprintf_s(buf,sizeof(buf)/sizeof(wchar_t),L"%.2f",num);这就又回到坑5里说的问题了——_s版本是 VS 特有的,换到 GCC / Clang 环境就编译不过。
区域设置导致小数点变成逗号
这算是宽字节环境下最隐蔽的坑。如果代码里为了输出中文提示语而调用了setlocale(LC_ALL, "zh_CN.UTF-8")或类似的操作,浮点数输出的分隔符可能从.变成,——欧洲很多国家用逗号当小数点。
实测例子:
#include<iostream>#include<iomanip>#include<clocale>usingnamespacestd;intmain(){doublenum=3.14;setlocale(LC_ALL,"zh_CN.UTF-8");cout<<fixed<<setprecision(2)<<num<<endl;// 可能输出 3,14return0;}解决办法就两种:
- 保持区域设置为
"C"或"en_US.UTF-8",中文输出单独处理(比如用MultiByteToWideChar手动转,或者分开不同流)。 - 如果必须用中文区域,输出数字时临时切回去:
char*old_locale=setlocale(LC_ALL,NULL);setlocale(LC_ALL,"C");cout<<fixed<<setprecision(2)<<num;// 用 C 区域输出数字setlocale(LC_ALL,old_locale);// 切回去输出中文不过这种做法比较繁琐,实际开发中更推荐用printf/wprintf配合格式字符串,不受区域设置影响。
VS2022 对宽字节_s函数的额外检查
和sprintf_s一样,swprintf_s对缓冲区大小检查极其严格。如果数值很大(比如1e20),保留 2 位小数后要占用几十个字符,超过了buf长度,swprintf_s不会像旧版函数那样悄悄截断,而是直接触发invalid parameter异常(调用invalid_parameter_handler)。所以用的时候缓冲区宁可开大一点(比如wchar_t buf[512]),或者先用_scwprintf计算所需长度:
intlen=_scwprintf(L"%.2f",num);// 计算格式化后需要的字符数wchar_t*buf=(wchar_t*)malloc((len+1)*sizeof(wchar_t));swprintf_s(buf,len+1,L"%.2f",num);这样就不会因为长度不够而踩坑了。
宽字节场景实用建议:
| 场景 | 推荐做法 |
|---|---|
| 只是输出到控制台 | 直接用printf/cout,省心。真遇到宽字符串需要打印,转成char*再输出 |
需要把格式化结果存进wchar_t* | 用swprintf_s(VS)或snprintf转成char再转宽字节,跨平台最稳妥 |
| 跨平台项目(Windows + Linux) | 尽量避免用swprintf,统一用snprintf处理成char字符串,最后再转wchar_t,参数顺序不会打架 |
| 区域设置影响小数分隔符 | 数字输出不受区域影响的方法:用printf/sprintf系列,它们不依赖setlocale的LC_NUMERIC(除非显式设置);cout/wcout则受std::locale影响,要格外留意 |
这个坑虽然不常遇到,但一旦碰到了,查错成本特别高——因为编译能通过,运行结果却莫名其妙多出来一个逗号。希望你能少走弯路。
四、方法对比表格
| 方法 | 语言 | 头文件 | 是否四舍五入 | 是否补0 | 是否改变原值 | 适用场景 |
|---|---|---|---|---|---|---|
printf("%.nf") | C / C++ | <stdio.h> | ✅ | ✅ | ❌ | 直接输出 |
sprintf | C / C++ | <stdio.h> | ✅ | ✅ | ❌ | 转字符串 |
fixed+setprecision | C++ | <iomanip> | ✅ | ✅ | ❌ | cout输出 |
仅setprecision | C++ | <iomanip> | ✅ | ❌ | ❌ | 控制有效数字 |
scientific+setprecision | C++ | <iomanip> | ✅ | ✅ | ❌ | 科学计数法 |
stringstream+setprecision | C++ | <sstream><iomanip> | ✅ | ✅ | ❌ | 转字符串 |
round(num * factor) / factor | C / C++ | <math.h><cmath> | ✅ | 取决于输出格式 | ✅ | 计算中需要精确值 |
手动(int)(num*100+0.5)/100.0 | C / C++ | 无 | ✅ | 取决于输出格式 | ✅ | 不想引入math库 |
五、结语
总结一下个人经验:
- 只是输出:C用
printf("%.nf"),C++用cout << fixed << setprecision(n),简单直接。 - 需要字符串:C用
sprintf,C++用stringstream。 - 计算中需要四舍五入的值:用
round(num * pow(10,n)) / pow(10,n)。 - 控制有效数字:C++用单独的
setprecision(n),C用%g。 - VS2022用户:要么在文件开头加
#define _CRT_SECURE_NO_WARNINGS,要么老老实实用_s版本。 - 宽字节场景:能用
printf就别用wprintf,能用char*就别碰wchar_t*,实在绕不开就用snprintf打好再转,参数顺序不会出幺蛾子。
没有哪种方法是万能的,根据场景选合适的就行。希望这篇文章对你有帮助。
