把文字當成 `char` 與 C 相容緩衝區處理
繁中(台灣)

把文字當成 char 與 C 相容緩衝區處理

第 8.1 節先把「固定大小陣列」這件事本身講清楚了。這一節把同樣的思路收窄到 char:字串字面量、原始 C 字串,以及把位元組直接存在自己內部的可寫緩衝區。

這種底層表示在今天的 scpp 裡依然很重要。std::string 已經存在,而且在你需要一 個可增長、自己擁有的文字值時很好用;但很多 API – 尤其是 extern "C" 邊界 – 今天仍然直接使用 const char*char[N]

對下面每個可以執行的例子,把檔案存成 char-buffers.scpp,然後這樣編譯執行:

scpp char-buffers.scpp -o char-buffers
./char-buffers

對於那些本來就應該被拒絕的例子,如果你想讓編譯器輸出逐字匹配,就按診斷區塊裡 給出的描述性檔名保存。

字串字面量是唯讀的 const char*

字串字面量給你的是一段借用來的唯讀文字。

import std;

int main() {
    const char* greeting = "hello";
    std::println("{} {}", greeting, greeting[1]);
    return 0;
}

輸出:

hello e

greeting 並不擁有一塊由 main 自己配置出來的緩衝區。它只是一個指向現成唯讀 位元組的指標,而對這個指標做索引存取,讀到的就是其中單個字元。

char[N] 陣列會把可寫文字緩衝區直接擁有在自己內部

當你需要一塊可以由目前函式自己修改的位元組儲存時,固定大小 char 陣列就跟第 8.1 節裡的其他陣列一樣運作。額外要記住的一條規則是:C 風格文字必須以 \0 結 尾,這樣另一個 API 才知道文字在哪裡結束。

extern "C" int puts(const char* s);

int main() {
    char word[6]{};
    word[0] = 's';
    word[1] = 'c';
    word[2] = 'p';
    word[3] = 'p';
    word[4] = '!';
    word[5] = '\0';

    [[scpp::unsafe]] {
        puts(word);
    }
    return 0;
}

輸出:

scpp!

word 直接擁有六個 char 元素。對 puts 的呼叫要放進 [[scpp::unsafe]] 裡, 原因跟本書裡其他 extern "C" 呼叫一樣:編譯器看不到對面真正的實作。

最後那個 \0 和前面的可見字元一樣重要。puts 會一直往後讀,直到碰到這個終止 符為止。

char[N] 緩衝區傳給函式時,改到的仍然是同一塊儲存

在呼叫邊界上,陣列緩衝區可以傳給一個寫成陣列語法的參數,而被呼叫函式會直接原地 改寫呼叫方那塊緩衝區。

import std;

void make_excited(char text[6]) {
    text[4] = '!';
    return;
}

int main() {
    char word[6]{};
    word[0] = 's';
    word[1] = 'c';
    word[2] = 'p';
    word[3] = 'p';
    word[4] = '?';
    word[5] = '\0';

    make_excited(word);
    const char* text = word;
    std::println("{}", text);
    return 0;
}

輸出:

scpp!

make_excited 並沒有收到一個新複製出來的陣列。它寫到的仍然是同一塊底層緩衝 區,所以呼叫方裡的 word[4] 也跟著變了。

std::string 負責擁有可增長文字,c_str() 負責接到 C API 上

當大小一開始就確定時,固定大小 char[N] 緩衝區很好用。要是你需要一個自己擁 有、而且還能增長的文字值,就用 std::string,只在真正需要的邊界上借出 C 相容視 圖。

import std;

extern "C" int puts(const char* s);

int main() {
    std::string name{"scpp"};
    name.append(" book");

    [[scpp::unsafe]] {
        puts(name.c_str());
    }

    int letters = static_cast<int>(name.size());
    std::println("{}", letters);
    return 0;
}

輸出:

scpp book
9

這裡文字的所有權一直都在 name 手裡,不在那個 C API 手裡。c_str() 給出的就 是 puts 需要的、借用來的 nul 結尾指標,而在普通 scpp 程式碼裡,std::string 仍然是處理動態長度文字時更合適的工具。

字串字面量不能直接初始化可變 char*

字面量本來就是唯讀文字,所以 scpp 不允許你把它當成可寫緩衝區。

int main() {
    char* text = "hi";
    return 0;
}

編譯器輸出:

string_literal_mutable_pointer_fail.scpp:2:18: error: cannot initialize or assign raw pointer 'text' from an incompatible pointer type without an explicit cast

如果一段程式碼打算透過這個指標寫資料,那它就必須真的拿到可寫儲存 – 比如它自己 擁有的 char[N] 陣列。如果只是讀取字面量,那就改用 const char*

到目前為止,文字緩衝區的規則是

下一節會回到「借用視圖」這個主題,用 std::span 來表達「看著一塊連續緩衝區,但 並不接管它的所有權」。


← 上一章:固定大小陣列 · 目錄 · 下一章:用 std::span 借用視圖 →