数学库中包含许多有用的数学函数。math.h头文件提供这些函数的原型。表16.2中列出了一些声明在math.h中的函数。注意,函数中涉及的角度都以弧度为单位(1弧度=180/π=57.296度)。参考资料V“新增C99和C11标准的ANSI C库”列出了C99和C11标准的所有函数。

c语言最基本的类型(C语言数学库的3种类型)(1)

Some ANSI C Standard Math Functions

1 三角问题

我们可以使用数学库解决一些常见的问题:把x/y坐标转换为长度和角度。例如,在网格上画了一条线,该线条水平穿过了4个单元(x的值),垂直穿过了3个单元(y的值)。那么,该线的长度(量)和方向是什么?根据数学的三角公式可知:

magnitude = square root (x^2 y^2)

and

angle = arctangent (y/x)

数学库提供平方根函数和一对反正切函数,所以可以用C程序表示这个问题。平方根函数是sqrt(),接受一个double类型的参数,并返回参数的平方根,也是double类型。 atan()函数接受一个double类型的参数(即正切值),并返回一个角度(该角度的正切值就是参数值)。但是,当线的x值和y值均为-5时,atan()函数产生混乱。因为(-5)/(-5)得1,所以atan()返回45°,该值与x和y均为5时的返回值相同。也就是说,atan()无法区分角度相同但反向相反的线(实际上,atan()返回值的单位是弧度而不是度,稍后介绍两者的转换)。 当然,C库还提供了atan2()函数。它接受两个参数:x的值和y的值。这样,通过检查x和y的正负号就可以得出正确的角度值。atan2()和atan()均返回弧度值。把弧度转换为度,只需将弧度值乘以180,再除以pi即可。pi的值通过计算表达式4*atan(1)得到。程序rectpol.c演示了这些步骤。另外,学习该程序还复习了结构和typedef相关的知识。

The rectpol.c Program

/* rect_pol.c -- converts rectangular coordinates to polar */ #include <stdio.h> #include <math.h> ​ #define RAD_TO_DEG (180/(4 * atan(1))) ​ typedef struct polar_v { double magnitude; double angle; } Polar_V; ​ typedef struct rect_v { double x; double y; } Rect_V; ​ Polar_V rect_to_polar(Rect_V); ​ int main(void) { Rect_V input; Polar_V result; ​ puts("Enter x and y coordinates; enter q to quit:"); while (scanf("%lf %lf", &input.x, &input.y) == 2) { result = rect_to_polar(input); printf("magnitude = %0.2f, angle = %0.2fn", result.magnitude, result.angle); } puts("Bye."); ​ return 0; } ​ Polar_V rect_to_polar(Rect_V rv) { Polar_V pv; ​ pv.magnitude = sqrt(rv.x * rv.x rv.y * rv.y); if (pv.magnitude == 0) pv.angle = 0.0; else pv.angle = RAD_TO_DEG * atan2(rv.y, rv.x); ​ return pv; }

下面是运行该程序后的一个输出示例:

Enter x and y coordinates; enter q to quit: 10 10 magnitude = 14.14, angle = 45.00 -12 -5 magnitude = 13.00, angle = -157.38 q Bye.

如果编译时出现下面的消息:

Undefined:_sqrt

或者

'sqrt': unresolved external

或者其他类似的消息,表明编译器链接器没有找到数学库。UNIX系统会要求使用-lm标记(flag)指示链接器搜索数学库:

cc rect_pol.c --lm

注意,-lm标记在命令行的末尾。因为链接器在编译器编译C文件后才开始处理。在Linux中使用GCC编译器可能要这样写:

gcc rect_pol.c -lm

2 类型变体

基本的浮点型数学函数接受double类型的参数,并返回double类型的值。当然,也可以把float或long double类型的参数传递给这些函数,它们仍然能正常工作,因为这些类型的参数会被转换成double类型。这样做很方便,但并不是最好的处理方式。如果不需要双精度,那么用float类型的单精度值来计算会更快些。而且把long double类型的值传递给double类型的形参会损失精度,形参获得的值可能不是原来的值。为了解决这些潜在的问题,C标准专门为float类型和long double类型提供了标准函数,即在原函数名后加上f或l后缀。因此,sqrtf()是sqrt()的float版本,sqrtl()是sqrt()的longdouble版本。 利用C11新增的泛型选择表达式定义一个泛型宏,根据参数类型选择最合适的数学函数版本。程序清单16.15演示了两种方法。

Listing 16.15 The generic.c Program

​ //generic.c-- defining generic macros ​ #include <stdio.h> #include <math.h> #define RAD_TO_DEG (180/(4 * atanl(1))) ​ // generic square root function #define SQRT(X) _Generic((X), long double: sqrtl, default: sqrt, float: sqrtf)(X) ​ // generic sine function, angle in degrees #define SIN(X) _Generic((X), long double: sinl((X)/RAD_TO_DEG), default:sin((X)/RAD_TO_DEG), float:sinf((X)/RAD_TO_DEG) ) ​ int main(void) { float x = 45.0f; double xx = 45.0; long double xxx =45.0L; ​ long double y = SQRT(x); long double yy= SQRT(xx); long double yyy = SQRT(xxx); printf("%.17Lfn", y);// matches float printf("%.17Lfn", yy);// matches default printf("%.17Lfn", yyy); // matches long double int i = 45; yy = SQRT(i);// matches default printf("%.17Lfn", yy); yyy= SIN(xxx);// matches long double printf("%.17Lfn", yyy); ​ return 0; }

下面是该程序的输出:

6.70820379257202148 6.70820393249936942 6.70820393249936909 6.70820393249936942 0.70710678118654752

如上所示,SQRT(i)和SQRT(xx)的返回值相同,因为它们的参数类型分别是int和double,所以只能与default标签对应。 有趣的一点是,如何让Generic宏的行为像一个函数。SIN()的定义也许提供了一个方法:每个带标号的值都是函数调用,所以Generic表达式的值是一个特定的函数调用,如sinf((X)/RADTODEG),用传入SIN()的参数替换X。 SQRT()的定义也许更简洁。Generic表达式的值就是函数名,如sinf。函数的地址可以代替该函数名,所以Generic表达式的值是一个指向函数的指针。然而,紧随整个Generic表达式之后的是(X),函数指针(参数)表示函数指针。因此,这是一个带指定的参数的函数指针。 简而言之,对于SIN(),函数调用在泛型选择表达式内部;而对于SQRT(),先对泛型选择表达式求值得一个指针,然后通过该指针调用它所指向的函数。

3 tgmath.h库(C99)

C99标准提供的tgmath.h头文件中定义了泛型类型宏,其效果与程序清单16.15类似。如果在math.h中为一个函数定义了3种类型(float、double和long double)的版本,那么tgmath.h文件就创建一个泛型类型宏,与原来double版本的函数名同名。例如,根据提供的参数类型,定义sqrt()宏展开为sqrtf()、sqrt()或sqrtl()函数。换言之,sqrt()宏的行为和程序清单16.15中的SQRT()宏类似。 如果编译器支持复数运算,就会支持complex.h头文件,其中声明了与复数运算相关的函数。例如,声明有csqrtf()、csqrt()和csqrtl(),这些函数分别返回float complex、double complex和long double complex类型的复数平方根。如果提供这些支持,那么tgmath.h中的sqrt()宏也能展开为相应的复数平方根函数。 如果包含了tgmath.h,要调用sqrt()函数而不是sqrt()宏,可以用圆括号把被调用的函数名括起来:

#include <tgmath.h> ... float x = 44.0; double y; y = sqrt(x);// invoke macro, hence sqrtf(x) y = (sqrt)(x); // invoke function sqrt()

这样做没问题,因为类函数宏的名称必须用圆括号括起来。圆括号只会影响操作顺序,不会影响括起来的表达式,所以这样做得到的仍然是函数调用的结果。实际上,在讨论函数指针时提到过,由于C语言奇怪而矛盾的函数指针规则,还可以使用(*sqrt)()的形式来调用sqrt()函数。不借助C标准以外的机制,C11新增的Generic表达式是实现tgmath.h最简单的方式。

,