cv.cm

編碼 · 程式語言

C++ 入門教學

閱讀約 10 分鐘

簡答

C++ 是編譯式的靜態型別語言,結合了 C 的速度與底層控制能力,並加入類別、樣板與龐大的標準函式庫。用 g++ -std=c++20 hello.cpp -o hello 編譯 hello.cpp,再執行 ./hello。現代 C++ 程式碼以 std::string、std::vector 和 std::map 取代原始陣列,以智慧指標取代手動 delete,並利用 RAII 讓資源自動釋放。

開啟二進位轉換

C++ 是什麼,用來做什麼

C++ 由 Bjarne Stroustrup 於 1979 年在貝爾實驗室開始開發,最初名為「C with Classes」。它由 ISO 標準化;C++11 開啟了「現代 C++」時代,之後有 C++17、C++20 與 C++23。C++ 用在重視效能與控制力的地方:遊戲引擎(Unreal Engine)、網頁瀏覽器(Chrome、Firefox)、資料庫、交易系統、嵌入式裝置、編譯器與機器學習執行環境。它的核心理念是零負擔抽象:你沒用到的功能不會有任何代價。

安裝編譯器並執行 Hello, world

Linux 與 macOS 上使用 GCC(g++)或 Clang(clang++),Windows 上則透過 Visual Studio 使用 Microsoft Visual C++。請明確指定標準版本。

#include <iostream>

int main() {
    std::cout << "Hello, world\n";
    return 0;
}
g++ -std=c++20 -Wall -Wextra hello.cpp -o hello
./hello
# Hello, world

std::cout 是標準輸出串流,<< 用來寫入它。標準函式庫中的所有內容都位於命名空間 std 之下。

變數、型別與 auto

C++ 擁有與 C 相同的內建型別,另外還有作為關鍵字的 bool、參考(reference),以及 std::string 等函式庫型別。auto 讓編譯器根據初始值推導型別。大括號初始化 {} 不允許窄化轉換。

型別範例說明
int, long longint n{42};有號整數
doubledouble x{3.14};64 位元浮點數
boolbool ok{true};true 或 false
charchar c{'A'};一個位元組
std::stringstd::string s{"hi"};可增長的文字,來自 <string>
autoauto v = 2.5;自動推導,這裡是 double
T&int& r = n;參考:n 的另一個名稱
#include <iostream>
#include <string>

int main() {
    int count{7};
    auto avg = count / 2.0;          // double, 3.5
    std::string name{"Ada"};
    name += " Lovelace";
    const int& ref = count;          // read-only alias of count
    std::cout << name << " " << name.size() << " " << avg << " " << ref << "\n";
    // Ada Lovelace 12 3.5 7
}

流程控制與函式

流程控制與 C 相同,另外還有範圍式 for 迴圈。傳遞大型物件時請用 const 參考以避免複製;若函式需要修改它們,則使用非 const 參考。

#include <iostream>
#include <string>
#include <vector>

int square(int x) { return x * x; }

void addSuffix(std::string& s, const std::string& suffix = "!") {
    s += suffix;
}

int main() {
    int score = 72;
    if (score >= 90) std::cout << "A\n";
    else if (score >= 70) std::cout << "B\n";   // B
    else std::cout << "C\n";

    std::vector<int> nums{1, 2, 3};
    for (int n : nums) std::cout << square(n) << " ";  // 1 4 9
    std::cout << "\n";

    std::string word = "hi";
    addSuffix(word);
    std::cout << word << "\n";                  // hi!
}

類別

類別把資料成員與成員函式組合在一起。建構子負責建立物件的初始狀態;private 成員對外部程式碼是隱藏的。struct 與 class 相同,只是成員預設為 public。

#include <iostream>
#include <string>
#include <utility>

class Account {
public:
    explicit Account(std::string owner) : owner_(std::move(owner)) {}

    void deposit(double amount) {
        if (amount > 0) balance_ += amount;
    }
    double balance() const { return balance_; }
    const std::string& owner() const { return owner_; }

private:
    std::string owner_;
    double balance_ = 0.0;
};

int main() {
    Account acc{"Ann"};
    acc.deposit(50);
    acc.deposit(-5);   // ignored
    std::cout << acc.owner() << ": " << acc.balance() << "\n";  // Ann: 50
}

繼承(class Savings : public Account)與 virtual 函式提供執行階段的多型。請在覆寫的函式上標示 override,讓編譯器幫你檢查。

標準函式庫容器與演算法

標準函式庫(常稱為 STL)提供容器與演算法,它們透過迭代器彼此搭配運作。

容器類似適用情境
std::vector<T>動態陣列預設的序列容器
std::array<T, N>固定大小陣列大小在編譯期已知
std::map<K, V>已排序的字典鍵有序,查找為 O(log n)
std::unordered_map<K, V>雜湊表平均查找速度快,為 O(1)
std::set<T>已排序且不重複的集合成員檢查、有序輸出
#include <algorithm>
#include <iostream>
#include <map>
#include <string>
#include <vector>

int main() {
    std::vector<int> nums{3, 1, 2, 5};
    std::sort(nums.begin(), nums.end());
    auto evens = std::count_if(nums.begin(), nums.end(),
                               [](int n) { return n % 2 == 0; });
    std::cout << nums.front() << " " << nums.back() << " " << evens << "\n";  // 1 5 1

    std::map<std::string, int> ages{{"bob", 25}, {"ann", 31}};
    ages["cy"] = 40;
    for (const auto& [name, age] : ages) {     // structured bindings
        std::cout << name << "=" << age << " ";  // ann=31 bob=25 cy=40
    }
    std::cout << "\n";
}

[](int n) { ... } 是 lambda,一種直接寫在原地的匿名函式。樣板(template)讓同一個 std::vector 能用於任何元素型別;你也可以用 template <typename T> 自己撰寫。

招牌特色:RAII 與智慧指標

RAII(Resource Acquisition Is Initialization,資源取得即初始化)表示資源由物件擁有:建構子取得資源,解構子釋放資源。由於物件離開作用域時解構子會自動執行,即使拋出例外也一樣,因此記憶體、檔案與鎖都不會洩漏。智慧指標就是把 RAII 應用在堆積記憶體上。

#include <iostream>
#include <memory>

struct Noisy {
    int id;
    explicit Noisy(int i) : id(i) { std::cout << "create " << id << "\n"; }
    ~Noisy() { std::cout << "destroy " << id << "\n"; }
};

int main() {
    auto a = std::make_unique<Noisy>(1);       // sole owner
    {
        auto b = std::make_shared<Noisy>(2);   // shared, reference-counted
        auto c = b;                            // two owners now
        std::cout << "owners: " << b.use_count() << "\n";
    }                                          // destroy 2 happens here
    std::cout << "end of main\n";
}                                              // destroy 1 happens here

輸出:create 1、create 2、owners: 2、destroy 2、end of main、destroy 1。在現代程式碼中,你幾乎不該自己寫 new 和 delete。

以例外處理錯誤

拋出例外物件,並以 const 參考捕捉。標準例外型別位於 <stdexcept>。不會拋出例外的函式可標示為 noexcept。

#include <iostream>
#include <stdexcept>
#include <string>

int parseAge(const std::string& text) {
    int age = std::stoi(text);   // throws std::invalid_argument on "abc"
    if (age < 0) throw std::out_of_range("negative age");
    return age;
}

int main() {
    try {
        std::cout << parseAge("41") << "\n";   // 41
        std::cout << parseAge("abc") << "\n";
    } catch (const std::invalid_argument&) {
        std::cout << "not a number\n";         // not a number
    } catch (const std::exception& e) {
        std::cout << "error: " << e.what() << "\n";
    }
}

對於可預期的失敗,許多程式碼庫會回傳 std::optional<T>(C++17)或 std::expected<T, E>(C++23),而不是拋出例外。

完整的小程式:單字頻率統計

#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cerr << "usage: " << argv[0] << " FILE\n";
        return 1;
    }
    std::ifstream in(argv[1]);
    if (!in) {
        std::cerr << "cannot open " << argv[1] << "\n";
        return 1;
    }
    std::unordered_map<std::string, int> counts;
    std::string word;
    while (in >> word) {
        std::string clean;
        for (unsigned char ch : word)
            if (std::isalpha(ch)) clean += static_cast<char>(std::tolower(ch));
        if (!clean.empty()) ++counts[clean];
    }
    std::vector<std::pair<std::string, int>> top(counts.begin(), counts.end());
    std::sort(top.begin(), top.end(),
              [](const auto& a, const auto& b) { return a.second > b.second; });
    for (size_t i = 0; i < top.size() && i < 5; ++i)
        std::cout << top[i].second << "  " << top[i].first << "\n";
}

以 g++ -std=c++20 -O2 words.cpp -o words 編譯,再執行 ./words notes.txt。in 離開作用域時檔案會自動關閉,這又是 RAII 的應用。

初學者常見錯誤

  • 在標頭檔中寫 using namespace std;,這會污染所有引入該標頭檔的檔案。
  • 明明用 const& 就夠了,卻以傳值方式傳遞 std::vector 這類大型物件。
  • 在比較中混用有號與無號數,例如 int i 與 v.size()。開啟警告就能發現這類問題。
  • 在 push_back 之後仍保留指向 std::vector 的迭代器或參考,元素可能已被搬移。
  • 使用原始的 new 和 delete,而不是 std::make_unique、std::vector 或其他具擁有權的型別。

接下來學什麼

把 cppreference.com 當作語言與函式庫的參考,並參考 C++ Core Guidelines(由 Bjarne Stroustrup 與 Herb Sutter 編輯)學習良好實務。Standard C++ Foundation 的網站 isocpp.org 有入門頁面與推薦書籍。學習用 CMake 建置專案,再進一步學習移動語意、樣板、constexpr,以及使用 std::thread 的並行處理。

常見問題

學 C++ 之前要先學 C 嗎?

不是必要的。現代 C++ 教學從 std::string、std::vector 與類別開始,之後才介紹原始指標與 C 風格陣列。先學 C 確實有助於理解記憶體。

該使用哪個 C++ 標準?

目前的 GCC、Clang 與 MSVC 對 C++17 和 C++20 支援良好,C++23 的支援也在增加。請用 -std=c++20 之類的旗標明確指定。

指標和參考有什麼差別?

參考是別名,建立時就必須綁定,之後不能改綁,也不能是 null。指標儲存位址,可以是 null,也可以改指向其他位置。

C++ 很難學嗎?

這門語言相當龐大,學習曲線會比 Python 或 Go 更長。堅持使用現代功能、標準容器與智慧指標,就能避開大部分經典陷阱。

全部編碼教學