cv.cm

Lập trình · Ngôn ngữ lập trình

Hướng dẫn học lập trình C++ cơ bản cho người mới

8 phút đọc

Trả lời nhanh

C++ là ngôn ngữ biên dịch, kiểu tĩnh, kết hợp tốc độ và khả năng kiểm soát mức thấp của C với lớp, template và một thư viện chuẩn lớn. Biên dịch hello.cpp bằng g++ -std=c++20 hello.cpp -o hello rồi chạy ./hello. Mã C++ hiện đại dùng std::string, std::vector và std::map thay cho mảng thô, con trỏ thông minh thay cho delete thủ công, và RAII để tài nguyên được giải phóng tự động.

Mở Đổi nhị phân

C++ là gì và dùng để làm gì

C++ do Bjarne Stroustrup tạo ra tại Bell Labs, bắt đầu từ năm 1979 với tên "C with Classes". Nó được ISO chuẩn hóa; C++11 mở ra kỷ nguyên "C++ hiện đại", tiếp theo là C++17, C++20 và C++23. C++ được dùng ở nơi hiệu năng và khả năng kiểm soát quan trọng: engine game (Unreal Engine), trình duyệt web (Chrome, Firefox), cơ sở dữ liệu, hệ thống giao dịch, thiết bị nhúng, trình biên dịch và runtime học máy. Ý tưởng chủ đạo của nó là trừu tượng hóa không tốn thêm chi phí (zero-overhead): tính năng bạn không dùng thì không tốn gì.

Cài trình biên dịch và chạy Hello, world

Dùng GCC (g++) hoặc Clang (clang++) trên Linux và macOS, hoặc Microsoft Visual C++ trên Windows (qua Visual Studio). Hãy chọn phiên bản chuẩn một cách tường minh.

#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 là luồng xuất chuẩn và << ghi vào đó. Mọi thứ trong thư viện chuẩn nằm trong không gian tên std.

Biến, kiểu dữ liệu và auto

C++ có các kiểu dựng sẵn giống C, cộng thêm bool là từ khóa, tham chiếu, và các kiểu của thư viện như std::string. auto yêu cầu trình biên dịch suy luận kiểu từ biểu thức khởi tạo. Khởi tạo bằng ngoặc nhọn {} từ chối các chuyển đổi làm hẹp kiểu.

KiểuVí dụGhi chú
int, long longint n{42};Số nguyên có dấu
doubledouble x{3.14};Số thực dấu phẩy động 64 bit
boolbool ok{true};true hoặc false
charchar c{'A'};Một byte
std::stringstd::string s{"hi"};Văn bản co giãn được, từ <string>
autoauto v = 2.5;Được suy luận, ở đây là double
T&int& r = n;Một tham chiếu: tên gọi khác của 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
}

Điều khiển luồng và hàm

Điều khiển luồng giống C, cộng thêm vòng lặp for theo phạm vi. Truyền đối tượng lớn bằng tham chiếu const để tránh sao chép, và bằng tham chiếu không const khi hàm cần thay đổi chúng.

#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!
}

Lớp

Lớp gom các thành viên dữ liệu và hàm thành viên. Constructor thiết lập một đối tượng; thành viên private bị ẩn khỏi mã bên ngoài. struct giống lớp, chỉ khác là thành viên mặc định là 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
}

Kế thừa (class Savings : public Account) và hàm virtual cho tính đa hình lúc chạy. Hãy đánh dấu các hàm ghi đè bằng override để trình biên dịch kiểm tra chúng.

Container và thuật toán của thư viện chuẩn

Thư viện chuẩn (thường gọi là STL) cung cấp các container và thuật toán phối hợp với nhau qua iterator.

ContainerTương đươngDùng cho
std::vector<T>Mảng độngContainer tuần tự mặc định
std::array<T, N>Mảng cố địnhKích thước biết lúc biên dịch
std::map<K, V>Từ điển có thứ tựKhóa có thứ tự, tra cứu O(log n)
std::unordered_map<K, V>Bảng bămTra cứu nhanh, trung bình O(1)
std::set<T>Tập hợp duy nhất có thứ tựKiểm tra thành viên, xuất theo thứ 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) { ... } là lambda, một hàm không tên viết ngay tại chỗ. Template là thứ giúp một std::vector dùng được cho mọi kiểu phần tử; bạn có thể tự viết bằng template <typename T>.

Tính năng đặc trưng: RAII và con trỏ thông minh

RAII (Resource Acquisition Is Initialization) nghĩa là một tài nguyên thuộc sở hữu của một đối tượng: constructor lấy nó và destructor giải phóng nó. Vì destructor chạy tự động khi đối tượng ra khỏi phạm vi, kể cả khi có ngoại lệ được ném, bộ nhớ, tệp và khóa không thể bị rò rỉ. Con trỏ thông minh áp dụng RAII cho bộ nhớ heap.

#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

Kết quả: create 1, create 2, owners: 2, destroy 2, end of main, destroy 1. Trong mã hiện đại, bạn hầu như không bao giờ nên tự viết new và delete.

Xử lý lỗi bằng ngoại lệ

Ném một đối tượng ngoại lệ và bắt nó bằng tham chiếu const. Các kiểu ngoại lệ chuẩn nằm trong <stdexcept>. Hàm không bao giờ ném có thể được đánh dấu 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";
    }
}

Với các lỗi có thể lường trước, nhiều codebase trả về std::optional<T> (C++17) hoặc std::expected<T, E> (C++23) thay vì ném ngoại lệ.

Một chương trình nhỏ hoàn chỉnh: tần suất từ

#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";
}

Biên dịch bằng g++ -std=c++20 -O2 words.cpp -o words và chạy ./words notes.txt. Tệp được đóng tự động khi in ra khỏi phạm vi, đó lại là RAII.

Những lỗi thường gặp của người mới

  • Viết using namespace std; trong tệp header, làm ô nhiễm mọi tệp include chúng.
  • Truyền đối tượng lớn như std::vector theo giá trị trong khi const& là đủ.
  • Trộn kiểu có dấu và không dấu trong so sánh, chẳng hạn int i với v.size(). Hãy bật cảnh báo để thấy những lỗi này.
  • Giữ iterator hoặc tham chiếu vào một std::vector sau push_back, vì các phần tử có thể bị di chuyển.
  • Dùng new và delete thô thay cho std::make_unique, std::vector hoặc các kiểu sở hữu khác.

Học tiếp ở đâu

Hãy dùng cppreference.com làm tài liệu tham khảo ngôn ngữ và thư viện, và C++ Core Guidelines (do Bjarne Stroustrup và Herb Sutter biên tập) cho thực hành tốt. Trang Standard C++ Foundation isocpp.org có trang bắt đầu kèm sách đề xuất. Học CMake để build dự án, rồi chuyển sang move semantics, template, constexpr và lập trình đồng thời với std::thread.

Câu hỏi thường gặp

Có nên học C trước C++ không?

Không bắt buộc. Cách dạy C++ hiện đại bắt đầu bằng std::string, std::vector và lớp, còn con trỏ thô và mảng kiểu C được giới thiệu sau. Học C trước thì có giúp bạn hiểu về bộ nhớ.

Nên dùng chuẩn C++ nào?

C++17 và C++20 được GCC, Clang và MSVC hiện nay hỗ trợ tốt, còn hỗ trợ C++23 đang tăng dần. Hãy chọn một chuẩn tường minh bằng cờ như -std=c++20.

Con trỏ và tham chiếu khác nhau thế nào?

Tham chiếu là một bí danh phải được gắn khi tạo, không thể gắn lại hay bằng null. Con trỏ lưu một địa chỉ, có thể bằng null và có thể đổi để trỏ chỗ khác.

C++ có khó học không?

Ngôn ngữ này rất lớn, nên hãy chuẩn bị cho đường học dài hơn Python hoặc Go. Bám vào các tính năng hiện đại, container chuẩn và con trỏ thông minh sẽ tránh được hầu hết bẫy kinh điển.

Tất cả hướng dẫn lập trình