9.8 內建數學函式

處理浮點數的常用數學函式都住在 <cmath> 函式庫。先列總表:

函式 功能 例子 long double
sqrt(x) \sqrt{x}(開根號) sqrt(2.0)1.41421\ldots sqrtl
cbrt(x) \sqrt[3]{x}(開立方根) cbrt(27.0)3 cbrtl
floor(x) 向下取整(往小的方向) floor(-1.2)-2 floorl
ceil(x) 向上取整(往大的方向) ceil(-1.2)-1 ceill
round(x) 四捨五入到整數 round(2.5)3 roundl
abs(x) 絕對值(浮點數版) abs(-1.2)1.2 fabsl
pow(a, b) a^b(次方) 陷阱見下方,整數次方別用它 powl
log(x)log2(x)log10(x) 對數(底 e210 進階函式,先知道存在 logl
sin(x)cos(x)tan(x) 三角函數 進階函式,先知道存在 sinl
exp(x) e^x 進階函式,先知道存在 expl

數學符號小抄:向下取整寫作 \lfloor x \rfloor(例如 \lfloor 3.8 \rfloor = 3\lfloor -1.2 \rfloor = -2),向上取整寫作 \lceil x \rceil——題目敘述常直接用這兩個符號,看到就想到 floorceil

參數和回傳都是浮點數

這些函式吃的是浮點數、吐出來的也是浮點數(總表最後一欄以外的名字,都是 double 版)。這件事有兩個實務後果,都要放在心上:

後果一:丟整數進去,會被悄悄轉成 double 例如寫 sqrt(2),那個整數 2 會先隱性轉型成 double2.0 再開根號。小整數這樣沒問題;但大整數在「轉成 double」這一步就會失真——double 只有約 15 \sim 16 位有效數字,塞不下十幾位的大整數(原理見 9.5)。本節後面 sqrt 的陷阱就是栽在這裡。

後果二:回傳值是 double,不是 int floor(3.8)round(2.5) 算出來的是 double3.0),不是整數。如果你寫 int k = floor(3.8);,那個 double 還會被截斷一次才存進 int——多數時候剛好對,但心裡要清楚「進去、出來都是浮點數」這回事。

範例程式碼

#include <iostream>
#include <cmath>
using namespace std;

int main() {
    // 這些函式吃浮點數、回傳的也是浮點數
    cout << sqrt(8.0) << '\n';        // 2.82843(預設 6 位有效數字)
    cout << floor(3.8) << '\n';       // 3
    cout << floor(-1.2) << '\n';      // -2:向下是「往負的方向」!
    cout << ceil(-1.2) << '\n';       // -1
    cout << round(2.5) << '\n';       // 3
    cout << round(2.49999) << '\n';   // 2
    cout << round(-2.5) << '\n';      // -3:四捨五入是「往遠離 0 的方向進位」
    cout << abs(-1.2) << '\n';        // 1.2
    return 0;
}

執行結果:

2.82843
3
-2
-1
3
2
-3
1.2

注意負數的行為:floor(-1.2)-2(不是 -1!),round(-2.5)-3。「向下」永遠指數線上往左,跟直覺的「去掉小數」不同——去掉小數的那個操作是 9.9 的轉型。

pow 的兩宗罪:慢,而且不準

同樣是「大整數丟進浮點函式」的坑,pow 還要再加一條罪。pow(a, b) 是用對數指數硬算出來的——就算 ab 都是整數,它也走浮點數路線。代價有二:速度慢(實測比一次乘法慢了將近十倍),結果帶誤差(大數字時捨入後直接錯給你看)。

實驗:把 1^2, 2^2, 3^2, \ldots 一路算到 (10^8)^2,奇數項用減的、偶數項用加的,全部加總起來。用 pow(i, 2) 的版本:

long long an = 0;
for (long long i = 1; i <= 100000000; i++) {   // i*i 會到 10^16:i 用 long long(上冊 2.9)
    if (i % 2 != 0) an -= pow(i, 2);
    else            an += pow(i, 2);
}

在本站評測環境實測:跑了約 0.5 秒,而且答案是錯的——印出 5000000052546867i^2 接近 10^{16} 時已超過 2^{53}pow 回傳的浮點數存不精確,加總後整個走樣)。改用一次乘法 i * i

long long an = 0;
for (long long i = 1; i <= 100000000; i++) {
    if (i % 2 != 0) an -= i * i;
    else            an += i * i;
}

跑約 0.06 秒,答案正確5000000050000000)。

動手試試看:解掉地平線(練 sqrt)。記得先把 9.3 的輸出開場白寫好。