貨幣兌換

貨幣兌換

學習練習

簡介

在前面的概念中提過,global指示詞會讓定義在某個原始碼檔案中的函式或變數,能被其他檔案看見。 同樣地,extern指示詞會告訴組譯器,目前這個原始碼檔案裡用到的函式或變數是定義在另一個檔案中。

這兩個指示詞就是組合語言程式碼與其他原始碼檔案互動、實現模組化的主要方式。

對這兩個指示詞而言,外部程式碼是用組合語言還是高階語言定義或使用,並沒有差別。 所以舉例來說,即使是高階語言定義的函式,只要宣告成extern,就能在 x86-64 程式碼中使用。

同樣地,呼叫慣例是所有這些函式共通的,無論它們是用組合語言還是用高階語言定義,甚至連作業系統(OS)也一樣。

不過,由於在組合語言中資料只是一連串的位元組,因此了解與之介接的高階語言如何管理記憶體就很重要。

C 型別

C 語言是搭配組合語言程式碼時很常見的高階語言選擇。 它讓與作業系統互動的工作變得更容易,為許多重要任務提供高階的包裝,例如輸出到螢幕、從鍵盤讀取輸入,或配置動態記憶體。

在這個軌道中,測試是用 C 撰寫的,因此有必要對這個語言做個概括介紹。

Note

下面提到的任何型別都可以加上const修飾。 這會讓它們變成唯讀。

基本型別

C 有許多基本型別,它們的大小(以位元組計)可能各不相同。 下表整理了其中一些,以及它們在 x86-64 系統上常見的大小:

型別 位元組數 整數/浮點數
_Bool 1 整數
char 1 整數
short 2 整數
int 4 整數
long 4 或 8 整數
long long 8 整數
float 4 浮點數
double 8 浮點數

這些整數型別每一個都可以是有號(預設)或無號。 如果是無號,就必須明確指定,例如:unsigned int。

有些整數型別的別名,其定義是為了能精確控制它們的大小:

別名 位元組數
int8_t 1
int16_t 2
int32_t 4
int64_t 8

這些別名可以加上u前綴來表示無號型別,例如:uint64_t。

其他值得注意的別名還有:

別名 型別
bool _Bool
size_t 通常是無號的 8 位元組整數

_Bool(或其別名bool)特別的地方在於,雖然它佔了整整 1 個位元組,卻只能取兩個值的其中一個:true(1)和false(0)。

這些基本型別的值在傳入函式和從函式回傳時,都遵循整數和浮點數的一般規則。

列舉

列舉是一種型別,可以取數個明確指定元素中的其中一個:

enum example {
    example_1,
    example_2,
    example_3,
    example_4
};

在 C 中,列舉會隱式轉換成整數型別,通常是int,也就是 4 位元組的整數。

預設情況下,列舉中第一個可能元素會轉換成0,之後每個元素則轉換成下一個整數。 所以在上面的enum example中,example_1的值會是0,example_2的值會是1,依此類推。

可以為列舉中的任何元素指定值,接著預設情況下,下一個元素的值就會是下一個整數:

enum example_2 {
    example_1 = 4,
    example_2 = 8,
    example_3
}; // example_3 is equal to 9

記憶體位址

在 C 中,記憶體位址是以指向某個型別的指標來表示,並用*運算子標記。 所以舉例來說,int64_t *指的是存放一個或多個int64_t的記憶體位置。

位址照慣例會被當成 8 位元組的整數。

陣列

陣列在傳入函式和從函式回傳時,都是以指向其第一個元素的指標形式。

int64_t example_arr[] = {1, 2, 3}; // this is an array of 3 signed 8-byte integers
fn(example_arr); // this is a function that passes a pointer (int64_t *) to the beginning of the array as an argument

陣列中每個元素的大小,就是該陣列元素型別的大小。 例如上面定義的example_arr有 3 個元素,每個 8 位元組,總共 24 位元組。

由於陣列不會儲存自己的長度,通常需要另外用一個值來表示元素數量,除非是以哨兵值標示結尾。

字串

C 的字串是由char組成的陣列,而char是 1 位元組的型別。 大多數字串由 ASCII 字元組成並以 NUL 結尾,也就是說,當遇到值為0的位元組時就結束。 這表示字串的長度通常不需要另外當作引數傳遞。

說明

你的朋友 Chandler 打算走訪世界各地充滿異國風情的國家。 可惜的是,Chandler 的數學不太好。 他很擔心旅途中會被兌換所坑騙,所以希望你幫他做一個貨幣計算機。

以下是他對這個應用程式的規格要求。

Note

這個練習中的函式會宣告在 C 原始檔裡,並附有已提供的簽章。 你的 x86-64-assembly 程式碼必須遵循這些規格。

1. 追蹤不同的貨幣

你已經建立了一個 enum currency_t,用來記錄 Chandler 旅途中會用到的各種貨幣:

enum currency_t {
    GBP,
    EUR,
    JPY,
    AUD,
    BRL,
    CNY,
    CAD,
    INR
};

有時你會需要把這些貨幣列印到螢幕上,好確認它們是否正確。

建立 stringify_currency 函式:

void stringify_currency(char *buffer, enum currency_t currency);

這個函式沒有回傳值,應該把一個 C 風格的字串存到 buffer 所指的位置。 這個字串應該以視覺化的方式呈現傳入函式的是哪一種貨幣:

char buffer[4];
stringify_currency(buffer, CAD);
// => "CAD"

2. 檢查匯率

建立 exchange_rate 函式:

double exchange_rate(enum currency_t domestic_currency, enum currency_t foreign_currency, const double *value_in_US_dollars);

domestic_currency 引數代表 Chandler 要兌換成 foreign_currency 的當地貨幣。

value_in_US_dollars 是一個指向 double 陣列的指標,內容是每一種貨幣一單位以美元計的價值。 貨幣是這個陣列中的索引。 所以,如果 EUR 是 enum currency_t 的有效元素,那麼 value_in_US_dollars[EUR] 就會得出 1 EUR 以美元計的價值。

這個函式應該回傳一單位外幣以本國貨幣計的價值。

const double value_in_US_dollars[8] = {
    1.33329, 1.16443, 0.00654374, 0.650888,
    0.185577, 0.140420, 0.714142, 0.0113863
};

exchange_rate(JPY, GBP, value_in_US_dollars);
// => 203.750454633

3. 計算鈔票的價值

建立 get_value_of_bills 函式:

uint64_t get_value_of_bills(unsigned long long denomination, unsigned short number_of_bills);

denomination 引數是單張鈔票的面額,而 number_of_bills 是鈔票的總張數。

這個兌換亭只處理特定面額單位的現金。 你收到的總額必須能被一張「鈔票」或單位的價值整除,除不盡的部分會留下小數或餘數。 你的函式應該只回傳兌換亭會退還的鈔票總價值(不包含小數部分)。 可惜的是,兌換亭會把剩下的餘額或零錢當作額外的好處留下來。

get_value_of_bills(5, 128);
// => 640

4. 計算鈔票的張數

建立 get_number_of_bills 函式:

unsigned int get_number_of_bills(float amount, unsigned long long denomination);

這個函式應該回傳在指定 金額 之內可以拿到的_鈔票張數_。 換句話說:起始金額可以換成多少_整張鈔票_? 記住,你只能收到_整張鈔票_,不能收到不滿一張的鈔票,所以要記得依此做除法。 實際上,你就是把金額_向下_取整到最接近的整張鈔票或面額。

get_number_of_bills(127.5, 5);
// => 25

5. 計算兌換後的價值

建立 exchangeable_value 函式:

uint32_t exchangeable_value(float budget, double exchange_rate, uint8_t spread, unsigned long long denomination);

參數 spread 是以整數表示、作為兌換手續費_收取的百分比_。 它需要除以 100 換算成小數。 注意,價差會加到匯率上,所以價差 10 會讓匯率提高 10%。

這個函式應該計算由價差調整後的匯率,然後回傳新貨幣的最大價值。 記住,貨幣的面額是整數,不能再細分。

exchangeable_value(127.25, 0,873350884, 10, 20);
// => 120
exchangeable_value(127.25, 0,873350884, 10, 5);
// => 130
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
x86-64 Assembly Exercism

準備好開始 貨幣兌換 了嗎?

註冊 Exercism,透過 22 個概念130 個練習 和真人引導來學習並精通 x86-64 Assembly,全部免費。