在C语言的数值计算领域,取整与取余是最基础且高频的操作场景。无论是金融计算中的金额校准、嵌入式开发中的数据精度控制,还是图形学中的坐标转换,都离不开
ceil()、floor()、fmod()和modf()这四个标准库函数的支持。
C语言标准库(C89及后续标准)在math.h头文件中提供了这四个用于数值处理的核心函数,它们虽同属“数值调整”范畴,但分工明确:ceil()与floor()专注于方向型取整,前者向上取整,后者向下取整;fmod()聚焦于浮点数间的取余运算,解决整数取余运算符%的局限性;modf()则擅长“拆分型取整”,将浮点数拆分为整数部分与小数部分。下表先对四个函数进行宏观对比,建立初步认知:
函数名 | 核心功能 | 参数类型 | 返回值类型 | 关键特点 |
|---|---|---|---|---|
ceil() | 向上取整,返回不小于参数的最小整数 | double | double | 对正数“入”,对负数“舍” |
floor() | 向下取整,返回不大于参数的最大整数 | double | double | 对正数“舍”,对负数“入” |
fmod() | 计算两个浮点数的余数 | double, double | double | 余数符号与被除数一致 |
modf() | 拆分浮点数为整数和小数部分 | double, double* | double | 整数部分通过指针传出,小数部分返回 |
①函数原型与核心说明
C标准定义的ceil()原型为:
#include <math.h>
double ceil(double x);其核心作用是返回不小于输入参数x的最小整数,返回值类型为double(这是为了兼容大数值场景,避免整数溢出)。例如:ceil(1.2)返回2.0,ceil(-1.2)返回-1.0,ceil(3.0)返回3.0。
②实现逻辑(伪代码)
ceil()的实现需区分参数的正负性与整数性,核心逻辑是判断参数是否为整数,若不是则向正无穷方向进1。伪代码如下:
function ceil(double x) {
// 处理特殊值:NaN返回NaN,正无穷返回正无穷,负无穷返回负无穷
if (x is NaN or x == +infinity) {
return x;
}
if (x == -infinity) {
return x;
}
// 提取x的整数部分(截断小数)
int integer_part = (int)x;
// 若x为正数且有小数部分,向上取整为integer_part + 1
if (x > 0.0 && x > (double)integer_part) {
return (double)(integer_part + 1);
}
// 若x为负数且有小数部分(此时x < integer_part,因整数部分是截断的),直接返回integer_part
else if (x < 0.0 && x < (double)integer_part) {
return (double)integer_part;
}
// 若x为整数,直接返回自身
else {
return x;
}
}③关键特性与使用场景
核心特性:向正无穷方向取整,与我们日常生活中的“四舍五入”不同,它只关注不小于参数这一规则,不考虑小数部分大小。
典型使用场景:
ceil(100.0 / 8.0)得到13个线程(若用整数除法100/8得到12,会导致4个数据未处理)。
ceil(123.456 * 100) / 100可得到123.46。
①函数原型与核心说明
原型与ceil()对称:
#include <math.h>
double floor(double x);作用是返回不大于输入参数x的最大整数,返回值同样为double。例如:floor(1.8)返回1.0,floor(-1.8)返回-2.0,floor(5.0)返回5.0。
②实现逻辑(伪代码)
与ceil()逻辑对称,核心是向负无穷方向取整:
function floor(double x) {
// 处理特殊值
if (x is NaN or x == -infinity) {
return x;
}
if (x == +infinity) {
return x;
}
// 提取整数部分(截断小数)
int integer_part = (int)x;
// 若x为正数且有小数部分,直接返回integer_part(向下取整)
if (x > 0.0 && x > (double)integer_part) {
return (double)integer_part;
}
// 若x为负数且有小数部分,向下取整为integer_part - 1
else if (x < 0.0 && x < (double)integer_part) {
return (double)(integer_part - 1);
}
// 整数直接返回
else {
return x;
}
}③关键特性与使用场景
核心特性:向负无穷方向取整,与ceil()形成互补。需要注意的是,对于正数,floor()的效果等同于截断小数,但对于负数则完全不同(如floor(-1.1)是-2.0,而截断是-1)。
典型使用场景:
floor(13.9)可得到13,即第13小时。
score用floor(score / 10)可得到段号(如85分对应8号段)。
① 函数原型与核心说明
C语言中的%运算符仅支持整数取余,而fmod()填补了浮点数取余的空白,原型为:
#include <math.h>
double fmod(double x, double y);作用是计算x除以y的余数,满足数学关系:x = n * y + fmod(x, y),其中n是整数,且余数的符号与x一致,绝对值小于y的绝对值。例如:fmod(7.5, 2.0)返回1.5,fmod(-7.5, 2.0)返回-1.5,fmod(7.5, -2.0)仍返回1.5。
②实现逻辑(伪代码)
核心逻辑是通过减法或除法找到满足余数规则的结果,需处理除数为0、特殊值等边界情况:
function fmod(double x, double y) {
// 处理特殊情况:y为0或x为无穷/NaN,返回NaN
if (y == 0.0 || x is NaN || y is NaN || x == +infinity || x == -infinity) {
return NaN;
}
// 若x为0,返回0.0(符号与x一致)
if (x == 0.0) {
return x;
}
// 计算商的整数部分(向零方向取整)
double quotient = x / y;
int n = (int)quotient; // 截断小数,向零取整
// 计算余数:x - n*y
double remainder = x - n * y;
// 确保余数绝对值小于y的绝对值(处理精度误差)
while (fabs(remainder) >= fabs(y)) {
remainder -= (remainder > 0) ? y : -y;
}
while (fabs(remainder) < 0) {
remainder += (remainder > 0) ? y : -y;
}
return remainder;
}③关键特性与使用场景
核心特性:余数符号与被除数一致、支持浮点数运算,这是与%运算符的核心区别(%余数符号与被除数一致,但仅支持整数)。
典型使用场景:
sin(x)周期为2π,对任意x用fmod(x, 2*M_PI)可将其映射到0-2π区间,简化计算。
x是否为y的整数倍,可通过fmod(x, y) == 0.0判断(需注意浮点数精度问题)。
①函数原型与核心说明
与前三个函数不同,modf()的核心是拆分而非取整,原型为:
#include <math.h>
double modf(double x, double *intpart);作用是将浮点数x拆分为整数部分和小数部分,其中:
intpart传出,类型为double,且符号与x一致。
x一致,绝对值范围为[0,1)。
例如:modf(3.14, &intp)会将intp设为3.0,返回0.14;modf(-3.14, &intp)会将intp设为-3.0,返回-0.14。
②实现逻辑(伪代码)
核心逻辑是提取整数部分并计算小数部分,需处理特殊值和精度问题:
function modf(double x, double *intpart) {
// 处理特殊值:NaN返回NaN,无穷大返回0.0并将intpart设为无穷大
if (x is NaN) {
*intpart = NaN;
return NaN;
}
if (x == +infinity) {
*intpart = +infinity;
return 0.0;
}
if (x == -infinity) {
*intpart = -infinity;
return 0.0;
}
// 提取整数部分(向零方向取整)
double integer = (double)(int)x;
// 计算小数部分:x - 整数部分
double fractional = x - integer;
// 处理精度误差:例如0.999999999999可能被识别为1.0
if (fabs(fractional) >= 1.0) {
integer += (fractional > 0) ? 1.0 : -1.0;
fractional = x - integer;
}
// 传出整数部分,返回小数部分
*intpart = integer;
return fractional;
}③关键特性与使用场景
核心特性:整数与小数部分符号一致、拆分后两部分之和等于原数,这是其与手动截断的区别(手动截断可能导致符号不一致)。
典型使用场景:
为了更直观地展示四个函数的差异,下表选取了6个典型输入值,对比其输出结果:
输入值x(y=2.0用于fmod) | ceil(x) | floor(x) | fmod(x, 2.0) | modf(x, &intp):intp/返回值 |
|---|---|---|---|---|
1.2 | 2.0 | 1.0 | 1.2 | 1.0 / 0.2 |
-1.2 | -1.0 | -2.0 | -1.2 | -1.0 / -0.2 |
3.0 | 3.0 | 3.0 | 0.0 | 3.0 / 0.0 |
-3.0 | -3.0 | -3.0 | -0.0(等价于0.0) | -3.0 / -0.0 |
0.0 | 0.0 | 0.0 | 0.0 | 0.0 / 0.0 |
1.999999 | 2.0 | 1.0 | 1.999999 | 1.0 / 0.999999 |
下面通过一个数值处理工具的示例,展示四个函数的综合应用。该程序实现以下功能:1)输入一个浮点数和一个除数;2)分别用四个函数处理数值;3)格式化输出结果。
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
int main() {
double x, y;
// 输入提示与数据读取
printf("请输入一个浮点数x:");
if (scanf("%lf", &x) != 1) {
printf("输入错误!\n");
return 1;
}
printf("请输入除数y(用于fmod计算):");
if (scanf("%lf", &y) != 1) {
printf("输入错误!\n");
return 1;
}
// 函数调用
double ceil_result = ceil(x);
double floor_result = floor(x);
double fmod_result = fmod(x, y);
double intpart, modf_result = modf(x, &intpart);
// 结果输出(格式化)
printf("\n===== 数值处理结果 =====\n");
printf("1. ceil(x) 向上取整:%.6f\n", ceil_result);
printf("2. floor(x) 向下取整:%.6f\n", floor_result);
printf("3. fmod(x, y) 浮点数取余:%.6f\n", fmod_result);
printf("4. modf(x) 拆分结果:整数部分=%.6f,小数部分=%.6f\n", intpart, modf_result);
// 附加:验证modf拆分的正确性(两部分之和是否等于原数)
double sum = intpart + modf_result;
printf("\n附加验证:整数部分 + 小数部分 = %.6f(原数x=%.6f)\n", sum, x);
printf("拆分正确性:%s\n", fabs(sum - x) < 1e-9 ? "正确" : "存在精度误差");
return 0;
}编译与运行说明
由于使用了math.h中的函数,编译时需链接数学库(-lm参数),命令如下:
gcc -o num_process num_process.c -lm
./num_process运行示例与结果分析
输入x=123.456,y=10.0,运行结果如下:
请输入一个浮点数x:123.456
请输入除数y(用于fmod计算):10.0
===== 数值处理结果 =====
1. ceil(x) 向上取整:124.000000
2. floor(x) 向下取整:123.000000
3. fmod(x, y) 浮点数取余:3.456000
4. modf(x) 拆分结果:整数部分=123.000000,小数部分=0.456000
附加验证:整数部分 + 小数部分 = 123.456000(原数x=123.456000)
拆分正确性:正确结果分析:四个函数的输出均符合预期,modf()的拆分结果之和与原数完全一致(无精度误差);fmod(123.456, 10.0)返回3.456,符合“余数绝对值小于除数”的规则。
在实际开发中,若忽视函数的细节特性,容易引发逻辑错误或精度问题。
注意事项1:头文件与编译链接
所有四个函数均定义在
math.h头文件中,必须包含该头文件;同时,编译时需链接数学库(-lm参数),否则会出现“未定义引用”错误。这是初学者最常犯的错误之一。
注意事项2:返回值类型为double
四个函数的返回值均为
double类型,而非整数类型。若需整数结果,需手动强制转换(如int res = (int)ceil(x)),但需注意转换可能导致的溢出(如x超过int的最大值)。
注意事项3:浮点数精度误差问题
由于浮点数存储特性,计算结果可能存在微小误差,导致判断失效。例如
fmod(1.0, 0.1)的结果并非0.0(因0.1无法精确表示),需用fabs(fmod(x,y)) < 1e-9判断是否为0,而非直接用==0.0。
注意事项4:特殊值处理逻辑
当输入为NaN、正无穷(+infinity)或负无穷(-infinity)时,函数返回值有明确规则:
ceil()/floor()对NaN返回NaN,对正无穷返回正无穷,对负无穷返回负无穷;fmod()对这些值均返回NaN;modf()对NaN返回NaN,对无穷大返回0.0并将整数部分设为无穷大。
注意事项5:fmod()与remainder()的区别
C99标准新增
remainder()函数,与fmod()均为浮点数取余,但商的取整方式不同:fmod()商向零取整,remainder()商向最近整数取整(四舍五入)。例如fmod(7.5,2.0)=1.5,而remainder(7.5,2.0)=-0.5。
注意事项6:modf()的指针参数不可为NULL
modf()的第二个参数是指针,用于接收整数部分,必须指向有效的double变量,不可传NULL,否则会触发内存访问错误(段错误)。
进阶使用技巧
除基础用法外,结合函数特性可实现更灵活的数值处理需求,以下为两个高频技巧:
技巧1:实现四舍五入功能 C标准库无直接四舍五入函数,可通过ceil()或floor()实现:对正数x,round(x) = floor(x + 0.5);对负数x,round(x) = ceil(x - 0.5)。封装函数示例:
#include <math.h>
// 四舍五入函数,保留n位小数
double round_n(double x, int n) {
double scale = pow(10, n);
if (x > 0) {
return floor(x * scale + 0.5) / scale;
} else {
return ceil(x * scale - 0.5) / scale;
}
}测试:round_n(123.456,2)=123.46,round_n(-123.456,2)=-123.46,符合四舍五入规则。
技巧2:利用fmod()处理周期性任务 在嵌入式系统中,需周期性执行某任务(如每500ms执行一次),可通过fmod()计算系统运行时间与周期的余数,判断是否到达执行时机:
#include <time.h>
#include <math.h>
// 假设获取系统运行时间(单位:ms)
double get_system_time_ms() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6;
}
// 周期性任务检查:余数小于1ms则认为到达周期
int is_task_time(double period) {
double now = get_system_time_ms();
return fabs(fmod(now, period)) < 1.0;
}面试题1:ceil(1.5)、floor(1.5)、ceil(-1.5)、floor(-1.5)返回值?(百度2023C开发笔试题)
答案:依次为2.0、1.0、-1.0、-2.0。
解析:核心是取整方向——ceil()向正无穷取整,故1.5向上到2.0,-1.5向上到-1.0;floor()向负无穷取整,故1.5向下到1.0,-1.5向下到-2.0。需强调返回值为double类型,非整数。
面试题2:fmod(5.5,2)与5.5%2的区别?(腾讯2024后台开发一面题)
答案:前者返回1.5(有效),后者编译报错(无效)。
解析:1. 运算符支持:%仅支持整数参数,传入浮点数5.5会触发“类型不匹配”编译错误;fmod()专为浮点数设计。2. 结果类型:fmod()返回double类型1.5,%若传入5%2返回int类型1。3. 核心差异:%无浮点数运算能力,fmod()完美适配浮点数场景。
面试题3:用modf()拆分123.456,如何获取整数/小数部分?传NULL会怎样?(字节2023嵌入式二面题)
答案:1. 正确用法:定义double int_part变量,通过double frac_part = modf(123.456, &int_part)调用,int_part接收123.0,frac_part得到0.456。 2. 传NULL后果:触发段错误(内存访问违规)。
解析:modf()第二个参数是输出指针,需指向有效内存,NULL为无效地址,访问时触发操作系统内存保护机制。
博主简介 byte轻骑兵,现就职于国内知名科技企业,专注于嵌入式系统研发,深耕 Android、Linux、RTOS、通信协议、AIoT、物联网及 C/C++ 等领域。乐于技术分享与交流,欢迎关注互动! 📌 主页与联系方式
⚠️ 版权声明 本文为原创内容,未经授权禁止转载。商业合作或内容授权请联系邮箱并备注来意。