c语言实现pow(x,y)函数

0.math库里的实现(库里?)

在写这个函数之前,我想先查查c语言math库里自带的pow是怎么实现的
我先是在math.h里找,结果只找到了这个

_CRTIMP	double __cdecl pow (double, double);

和这个

/* 7.12.7.4 The pow functions. Double in C89 */
extern float __cdecl powf (float, float);
#ifndef __NO_INLINE__
__CRT_INLINE float __cdecl powf (float x, float y)
  {return (float) pow (x, y);}
#endif
extern long double __cdecl powl (long double, long double);

您这只给个声明不给我函数定义是几个意思啊
既然库文件查不到,我就上谷歌里搜搜吧
突然,不知怎的,我有一种不好的预感
果然,我啥也没查到,倒是有一大堆人在那里研究pow是怎么实现的
更有甚者,只写了一个for循环,然后给出示例

double value3 = mypow(5.21,4.11);
printf("value3 = %f\n", value3);

在这里插入图片描述
我还特意用手机算了一下5.21的4.11次幂
在这里插入图片描述
您说您这不是自欺欺人吗?
但我就好奇了,math.h的源代码到底定义在哪里呢?
终于在知乎上找到了答案,感谢Belleve大佬,点击这里查看

math.h 里的函数都是定义在 libm 里,而每个 libm 实现都不同
gcc 的 glibm 中数学函数的实现完全是平台依存的,在 x86 机器上,能调用 FPU 指令的就用 FPU(比如 sqrt() 就实际上调用 FSQRT,log() 调用的是 FYL2X),否则再自己实现
如果需要软件实现,方法基本上是泰勒级数。当然对于 sin 这类,可以用专门的优化算法,比如 CORDIC
CPU 中的电路基本上也就是泰勒级数。

原来是个叫libm的东西,gcc的就叫glibm
那glibm在哪里呢?在glibc,也就是c标准库
是在是不想再在mac里找各种文件了,我再重新下载一个吧
libm.so位于 libc6-dev 这个包里,那就到pkgs.org上找找
结果网速感人,5MB的文件可能要下十分钟,在这个空档,我又搜了一下x86浮点运算指令集
果然不好好学汇编是要付出代价的,这些用软件实现费劲的函数,几条汇编指令就搞定了,比如说以下几条

指令 说明
f2xm1 计算2的乘方(次数为st0中的值),减去1
fyl2x 计算st1*log st0 以2为底
fyl2xp1 计算st1*log (st0 + 1) 以2为底

我又想到存储浮点数的IEEE标准,这个计算乘方和log以2为底真是引人深思啊
既然浮点数在寄存器里就是指数形式储存的,直接在[1:8]位操作不就行了?再加上上面的浮点运算指令,搞定!
这时候文件也下载好了,开始念咒语(Windows的同学别学我哈,容易被室友当成疯子)

ar -vx libc6-dev_2.28-2_i386.deb   
tar -xzvf data.tar.gz  

然后肉眼搜索(一个一个文件夹点开……)
终于在usr/lib/i386-linux-gnu里找到了libm.so
干啊,.so的文件我还打不开,.so惹不起.a总能打开吧
敲代码!

#include <stdio.h>
int main() {
    FILE *fp = fopen("libm.a","rb");
    char element;
    
    while(!feof(fp)){
        element = getc(fp);
        printf("%2x",element);
    }
    
    fclose(fp);
    
    return 0;
}

然而也仅仅是打开了而已,有啥用呢,读起来要累死人
还是用synalysis吧,Synalyze It!
结果对着一大堆二进制文件synalyze半个小时,体验极其糟糕
看到了一大堆ieee754,和各种powf32,powf64
因为最后实在读不下去了,就姑且认为我的猜测就是正确的吧(我咋怎么懒)
要是有哪位大佬看到了pow在libm.a里的实现,请务必告诉我!!!
其实我还想知道像f2xm1,fyl2x这样的指令在底层(门电路层次)是如何实现的,计算系统基础扎实的同学请务必带带我!!!

不扯淡了,我们现在切入正题,尝试用不同的方法实现pow函数

1.牛顿迭代法实现pow

其实这个方法是很容易想到的,特别是在尝试用牛顿迭代法实现sqrt函数之后,很自然的联想到也可以用这个方法解决pow函数的实现
首先考虑计算pow(2.17,3.14)
我们可以用for循环实现pow(2.17,3)
接下来只要把pow(2.17,0.14)也计算出来,再把两部分乘到一起,问题就解决了
后面的那部分,也就是一个数的小数次幂怎么算呢?
我再分
将0.14分成0.1和0.04,0.04就是0.01*4
由牛顿迭代法,我们就可以求出pow(2.17,0.1)和pow(2.17,0.01),以及整数次幂pow(2.17,3),这样我们就可以把这个值算出来
还是稍微写一下原理吧
计算2.170.1,即解出方程x10=2.17的根,然后百度百科吧……

用牛顿迭代法解非线性方程,是把非线性方程 f(x)=0线性化的一种近似方法。把 f(x)在点x0的某邻域内展开成泰勒级数
>
取其线性部分(即泰勒展开的前两项),并令其等于0,即 f(x0)+f’(x0)(x-x0)=0,以此作为非线性方程f(x)=0的近似方程,若f’(x0)!=0,则其解为
在这里插入图片描述
这样,得到牛顿迭代法的一个迭代关系式
在这里插入图片描述

下面还有一些细节问题,例如当pow(x,y)中的x为负数,y为整数时,函数会正常工作,但y若是小数,就会返回nan
那我们根据x和y的取值列一个表格

x,y情况 返回值
x>0&&y>0 计算,存储为answer,return answer
x>0&&y=0 return 1
x>0&&y<0 将y取反,当作第一种情形运算,return 1/answer
x=0&&y>0 return 0
x=0&&y<=0 return 0/0(即NaN)
x<0&&y为整数 计算
x<0&&y不为整数 return 0/0(即NaN)

但是实际上我们不能制造出nan,于是可以将0.000000作为nan输出
以及pow函数的精度问题,我们可以通过控制y具体计算到小数点后几位来决定pow保留的精度
先写个小函数求绝对值

double Absolute(double a){
	if(a<0){
		a=-1*a;	
	}
	return a;
}

再写个函数,计算x的y次幂,这个y是像0.1,0.01,0.001这样的小数,实际上,这样的小数可以写成10^-n的形式,我们只需要传整数n到函数中

double func1(double x, int n){
    int number=1;
    int i,j;
    double x1n=1,x1=1;                              	//x1n表示x1的number次幂
    
    for(i=0;i<n;i++){                           		//number即10^n
        number=number*10;
    }
    
    while(Absolute(x1n-x)>0.0001){           	//通过牛顿迭代法计算t^number=x(t为未知数)的根
        x1n=1;
        for(j=0;j<number;j++){
            x1n=x1n*x1;
        }

        x1=((number-1)*x1n+x)/(number*x1n/x1);
   
        x1n=1;
        for(j=0;j<number;j++){
            x1n=x1n*x1;
        }                                          		 	//经过多次迭代,x1n的值逼近方程的根t
    }
    
    return x1;
}

接下来只要再写一个double pow1(double x, double y)函数,对x和y对取值各种if,然后用两个for循环分别处理y的整数部分和小数部分,其余的工作交给我们之前写的func1
下面是代码

double pow1(double x, double y){
    int positive = 1;                                   //用来储存y的正负
    double answer=1;                                  //即结果
    
    if(x>0&&y>0){
        positive=1;
    }
    else if(x!=0&&y==0){
        return 1;
    }
    else if(x>0&&y<0){
        y=-y;
        positive=0;
    }
    else if(x==0&&y>0){
        return 0;
    }
    else if(x==0&&y<=0){
        return 1/0;
    }
    else if(x<0&&(y-(int)y<0.0001||(y-(int)y>0.999))){
        if(y>0){
            positive=1;
        }
        else{
            y=-y;
            positive=0;
        }
    }
    else {
        return 1/0;
    }
    
    int integer=(int)y;
    int i,j;
    
    for(i=0;i<integer;i++){
        answer=answer*x;
    }
    
    if((y-(int)y<0.0001||y-(int)y>0.999)&&positive==1){                   //如果y为整数,跳过小数运算部分
        return answer;
    }
    if((y-(int)y<0.0001||y-(int)y>0.999)&&positive==0){
        return 1/answer;
    }
    
    double decimal=y-(int)y;
    
    for(i=1;i<=ACCURACY;i++){      							//ACCURACY表示计算精度
        decimal=decimal*10;
   
        for(j=0;j<(int)decimal;j++){
            answer=answer*func1(x, i);
        }
        decimal=decimal-(int)decimal;
    }
    
    if(positive==1){
        return answer;
    }
    else {
        return 1/answer;
    }
}

把这三个函数拼起来就完工了!
这里还有一个小细节是

x<0&&(y-(int)y<0.0001||(y-(int)y>0.999))

这是因为double型存储形式与int有本质上的区别(ieee754)
比如说1这个double,在内存中以二进制形式存储,转化为十进制可能是1.000000001也可能是0.99999999
而强制类型转换是只保留double型小数点前的部分,不是四舍五入
因此很可能(int)1的结果是0
所以我们要用(y-(int)y<0.0001||(y-(int)y>0.999)让两种可能都通过

下面让我们计算以下正版的5.21的4.11次幂

printf("%lf",pow1(5.21,4.11));

快看控制台!

883.492894Program ended with exit code: 0

和手机算出来的数据基本吻合,增大ACCURACY的值还会提高精度
(我还看到有的博主计算y为整数的情形时用了分治的方法,我懒得写了)

2.幂级数展开实现pow

在这里插入图片描述
为了避免歧义,这次pow接受的参数改为a,b
即double pow2(double a ,double b)
我采用麦克劳林展开的方法,用下面的公式
在这里插入图片描述
在pow(a,b)中,令t=a-1,设f(t)=(t+1)b
就可以得到
f(t)=(t+1)b=1+bt+b(b-1)t2/2+…+b(b-1)(b-2)…(b-n+1)tn/n!+…
这就把小数次幂的问题转化成了计算整数次幂和计算阶乘
然而,还存在一个问题,那就是当|t|>1时幂级数是发散的,随着我们设置的ACCURACY的增大,计算结果反而更加不精确
可以这样,当a>2时,对a取倒数,对应的t=a-1就满足|t|<=1了
然而,直觉告诉我这事不靠谱
其实我们可以对a不停的除2,直到0<a<2为止,再把相应的指数乘到b上,这样算精度应该会高一些
最后就是针对a,b的一些极端情况用if else缝缝补补
直接上代码!

#include <stdio.h>
#define ACCURACY 100
double func1(double t,int n);
double func2(double b,int n);
double pow2(double a,double b);
int main() {
    printf("%lf",pow2(5.21,4.11));
    return 0;
}

double pow2(double a,double b){
    if(a==0&&b>0){
        return 0;
    }
    else if(a==0&&b<=0){
        return 1/0;
    }
    else if(a<0&&!(b-(int)b<0.0001||(b-(int)b>0.999))){
        return 1/0;
    }

    if(a<=2&&a>=0){
        double t=a-1;
        double answer=1;
        for(int i=1;i<ACCURACY;i++){
            answer=answer+func1(t,i)*func2(b,i);
        }
        return answer;
    }
    
    else if(a>2){
        int time=0;
        
        while(a>2){
            a=a/2;
            time++;
        }
        
        return pow2(a,b)*pow2(2,b*time);
    }
    
    else{
        if((int)b%2==0){
            return pow2(-a,b);
        }
        else {
            return -pow2(-a,b);
        }
    }
}
double func1(double t,int n){
    double answer=1;
    for(int i=0;i<n;i++){
        answer=answer*t;
    }
    
    return answer;
}
double func2(double b,int n){
    double answer=1;
    for(int i=1;i<=n;i++){
        answer=answer*(b-i+1)/i;
    }
    
    return answer;
}

最后再试一下5.21的4.11次幂

printf("%lf",pow2(5.21,4.11));

见证奇迹的时刻

883.492857Program ended with exit code: 0

嗷嗷,终于完成了,这个的精确度更高,原因是ACCURACY取了100

猜你喜欢

转载自blog.csdn.net/weixin_43263346/article/details/85146301