Hằng và biến C++: cách khai báo, auto, const và constexpr

hằng và biến C++

Trong bài này chúng ta sẽ học cách khai báo hằng và biến C++: các cách khởi tạo biến, từ khóa auto, phạm vi của biến, và 2 cách tạo hằng là const và constexpr. Khai báo biến thì ai cũng làm được, nhưng khởi tạo sai cách hay quên khởi tạo là nguồn gốc của những lỗi “lúc chạy lúc không” rất khó tìm, nên các bạn đọc kỹ phần lưu ý nhé.

Bài 4 trong Serie Lập trình C++ từ A tới Z

Biến là gì?

Biến (variable) là một vùng nhớ có tên, dùng để lưu dữ liệu mà chương trình có thể đọc và thay đổi. Mỗi biến có 3 thứ: tên, kiểu dữ liệu và giá trị.

Các bạn hình dung biến như một chiếc hộp có dán nhãn. Nhãn là tên biến, kích thước hộp do kiểu dữ liệu quyết định, còn món đồ bên trong là giá trị. Đồ trong hộp có thể thay bất cứ lúc nào, nhưng nhãn và kích thước hộp thì cố định từ lúc tạo.

Cú pháp khai báo biến:

kieu_du_lieu ten_bien = gia_tri_ban_dau;

Các kiểu dữ liệu như int, double, bool chúng ta đã học ở bài các kiểu dữ liệu trong C++, còn quy tắc đặt tên biến xem lại ở bài cú pháp C++ cơ bản nhé.

3 cách khởi tạo biến trong C++

Khác với C chỉ có một cách, C++ cho phép khởi tạo biến bằng 3 kiểu cú pháp:

#include <iostream>

int main()
{
    int a = 5;      // khoi tao bang dau =
    int b(5);       // khoi tao bang ngoac tron
    int c{5};       // khoi tao bang ngoac nhon (C++11)
    int d{};        // ngoac nhon rong: gia tri 0

    int giaSach = 3.7;   // bi cat con 3, khong bao loi

    std::cout << a << ' ' << b << ' ' << c << ' ' << d << '\n';
    std::cout << "giaSach = " << giaSach << '\n';
    return 0;
}

3 cách khởi tạo biến trong C++ bằng dấu bằng, ngoặc tròn, ngoặc nhọn, gán 3.7 vào int thành 3

Với số nguyên bình thường thì cả 3 cách cho kết quả như nhau. Điểm khác nằm ở dòng giaSach: gán 3.7 vào biến int thì phần thập phân bị cắt mất, thành 3, mà g++ không hề cảnh báo.

Vì sao nên dùng ngoặc nhọn {}

Thử viết lại dòng đó bằng ngoặc nhọn:

int giaSach{3.7};

g++ báo lỗi narrowing conversion khi khởi tạo int bằng ngoặc nhọn với giá trị 3.7

Lần này g++ báo lỗi narrowing conversion (chuyển đổi thu hẹp): đổi từ double sang int sẽ làm mất dữ liệu, nên khởi tạo bằng ngoặc nhọn không cho phép. Lỗi được bắt ngay lúc biên dịch, thay vì âm thầm thành 3 rồi các bạn mất cả buổi đi tìm vì sao tiền sách bị tính sai.

Ngoặc nhọn rỗng int d{}; còn đảm bảo biến có giá trị 0. Vì vậy trong serie này mình ưu tiên dùng ngoặc nhọn, và dùng dấu = ở những chỗ đơn giản, không có nguy cơ mất dữ liệu.

Biến chưa khởi tạo

Khai báo biến mà không gán giá trị ban đầu là hợp lệ, nhưng rất nguy hiểm. Chương trình dưới đây tính tổng 1 + 2 + 3 mà quên gán tong = 0:

#include <iostream>

int main()
{
    int tong;
    for (int i = 1; i <= 3; i++)
        tong += i;
    std::cout << "Tong = " << tong << '\n';
    return 0;
}

Biến tong chưa khởi tạo, g++ chỉ cảnh báo used uninitialized khi bật -O2

Chương trình vẫn in ra 6, trông như đúng. Nhưng đó chỉ là may mắn: biến cục bộ chưa khởi tạo chứa giá trị rác còn sót lại trong bộ nhớ, lần này tình cờ là 0. Đổi máy, đổi trình biên dịch hay thêm vài dòng code khác là kết quả có thể thành số bất kỳ.

Để ý là khi biên dịch bình thường, g++ không cảnh báo gì cả. Chỉ khi bật tối ưu -O2, g++ phân tích kỹ hơn mới báo 'tong' is used uninitialized.

Lưu ý: Lỗi biến chưa khởi tạo rất khó tìm vì chương trình thường vẫn chạy đúng trên máy của bạn, rồi sai trên máy người khác. Cách phòng đơn giản nhất: luôn khởi tạo biến ngay khi khai báo, kể cả khi chưa biết giá trị thì cứ viết int tong{}; cho chắc.

Từ khóa auto

Từ C++11, các bạn có thể để trình biên dịch tự suy ra kiểu từ giá trị khởi tạo bằng từ khóa auto:

#include <iostream>
#include <string>

int main()
{
    auto soTrang = 356;                       // int
    auto giaBan = 125000.0;                   // double
    auto tenSach = std::string("C++ co ban"); // std::string

    std::cout << tenSach << ", " << soTrang << " trang, " << giaBan << " dong\n";
    std::cout << "sizeof(soTrang) = " << sizeof(soTrang) << '\n';
    std::cout << "sizeof(giaBan)  = " << sizeof(giaBan) << '\n';
    return 0;
}

Từ khóa auto tự suy ra kiểu int 4 byte và double 8 byte

356 là số nguyên nên soTrang có kiểu int (4 byte), 125000.0 có dấu chấm nên giaBan là double (8 byte). Kiểu được chốt lúc biên dịch, sau đó không đổi được nữa. auto không biến C++ thành ngôn ngữ kiểu động như Python đâu nhé.

auto phát huy tác dụng với những kiểu có tên rất dài, ví dụ khi làm việc với thư viện STL sau này. Còn với int, double đơn giản thì viết rõ kiểu ra sẽ dễ đọc hơn.

Lưu ý: Viết auto tenSach = "C++ co ban"; (không có std::string) thì tenSach có kiểu const char*, là con trỏ tới chuỗi kiểu C, không phải std::string. Biến kiểu này không có các hàm tiện lợi như .size() hay nối chuỗi bằng dấu +.

Phạm vi của biến

Phạm vi (scope) là vùng code mà một biến dùng được. Quy tắc trong C++: biến khai báo trong khối { } nào thì chỉ sống trong khối đó, ra khỏi khối là bị hủy.

#include <iostream>

int tongSoSach = 100;   // bien toan cuc

int main()
{
    int soSachMuon = 3;   // bien cuc bo cua main

    {
        int soSachMuon = 10;   // bien moi, che bien ben ngoai
        std::cout << "Trong khoi: " << soSachMuon << '\n';
    }

    std::cout << "Ngoai khoi: " << soSachMuon << '\n';
    std::cout << "Toan cuc:   " << tongSoSach << '\n';
    return 0;
}

Kết quả chương trình phạm vi biến: trong khối in 10, ngoài khối in 3, biến toàn cục in 100

Có 3 tầng phạm vi trong chương trình này:

Sơ đồ phạm vi biến trong C++: toàn cục, khối hàm main và khối con che khuất biến cùng tên

  • Biến toàn cục tongSoSach khai báo ngoài mọi hàm, dùng được ở mọi nơi trong file.
  • Biến cục bộ soSachMuon của main chỉ dùng được trong main.
  • Khối { } bên trong khai báo một biến mới cũng tên soSachMuon. Trong khối đó, tên này chỉ tới biến mới, gọi là che khuất (shadowing). Ra khỏi khối, biến mới bị hủy và biến cũ vẫn giữ nguyên giá trị 3.

Đặt trùng tên như vậy là hợp lệ nhưng rất dễ nhầm, các bạn nên tránh. Biến toàn cục cũng nên hạn chế: chỗ nào trong chương trình cũng sửa được nó, nên khi giá trị bị sai rất khó biết ai sửa. Phần biến toàn cục, static và extern mình đã viết kỹ trong bài biến cục bộ và biến toàn cục trong C, C++ dùng y hệt.

Hằng số với const

Hằng (constant) là biến mà giá trị không được thay đổi sau khi khởi tạo. Thêm từ khóa const phía trước là xong:

#include <iostream>

int main()
{
    const int soNgayMuonToiDa = 14;
    soNgayMuonToiDa = 30;
    std::cout << soNgayMuonToiDa << '\n';
    return 0;
}

g++ báo lỗi assignment of read-only variable khi gán giá trị mới cho hằng const

Cố gán giá trị mới cho hằng thì g++ báo lỗi assignment of read-only variable ngay lúc biên dịch.

Dùng hằng có 2 lợi ích. Thứ nhất, con số có tên nên đọc code hiểu ngay: soNgay > soNgayMuonToiDa dễ hiểu hơn nhiều so với soNgay > 14. Thứ hai, khi thư viện đổi quy định thành 21 ngày, các bạn chỉ sửa đúng một chỗ thay vì đi tìm mọi con số 14 trong code.

Nếu đã học C, chắc các bạn quen dùng #define SO_NGAY 14. Trong C++ nên dùng const hoặc constexpr thay cho #define, vì hằng có kiểu dữ liệu, có phạm vi, và hiện tên rõ ràng trong thông báo lỗi. Còn #define chỉ là phép thay chữ ở bước tiền xử lý.

constexpr: hằng số lúc biên dịch

const chỉ hứa là “không sửa giá trị”, còn giá trị đó có thể chỉ biết lúc chương trình chạy. constexpr chặt hơn: giá trị phải tính được ngay lúc biên dịch.

#include <iostream>

int main()
{
    constexpr int SO_SACH_TOI_DA = 5;     // biet gia tri ngay luc bien dich
    int danhSachMaSach[SO_SACH_TOI_DA] = {101, 102, 103, 104, 105};

    int soNgay;
    std::cout << "Nhap so ngay muon: ";
    std::cin >> soNgay;
    const int soNgayMuon = soNgay;        // chi biet luc chay, khong dung constexpr duoc

    std::cout << "Muon toi da " << SO_SACH_TOI_DA << " cuon, ma cuon dau: "
              << danhSachMaSach[0] << '\n';
    std::cout << "So ngay muon: " << soNgayMuon << '\n';
    return 0;
}

Dùng hằng constexpr làm kích thước mảng, hằng const nhận giá trị nhập từ bàn phím

SO_SACH_TOI_DA là constexpr nên dùng được làm kích thước mảng. Còn soNgayMuon phụ thuộc vào số người dùng nhập, chỉ biết lúc chạy, nên chỉ khai báo được là const. Thử đổi nó thành constexpr thì g++ sẽ báo lỗi ngay.

Quy tắc chọn: giá trị viết sẵn trong code (số ngày tối đa, số pi, kích thước mảng) thì dùng constexpr. Giá trị tính lúc chạy nhưng sau đó không đổi nữa thì dùng const.

Hằng constexpr thường được đặt tên bằng chữ in hoa nối gạch dưới như SO_SACH_TOI_DA để nhìn là biết đây là hằng. Đây là thói quen, không phải quy tắc bắt buộc.

Áp dụng vào chương trình thư viện

Chương trình thư viện của chúng ta có quy định: mỗi lần mượn tối đa 14 ngày, quá hạn phạt 2000 đồng mỗi ngày. Hai con số này viết sẵn trong code nên dùng constexpr:

#include <iostream>
#include <string>

int main()
{
    constexpr int SO_NGAY_MUON_TOI_DA = 14;
    constexpr int TIEN_PHAT_MOI_NGAY = 2000;   // dong

    std::string tenSach = "Lap trinh C++ co ban";
    int soNgayDaMuon;

    std::cout << "Sach: " << tenSach << '\n';
    std::cout << "Ban da muon bao nhieu ngay: ";
    std::cin >> soNgayDaMuon;

    int soNgayQuaHan = soNgayDaMuon - SO_NGAY_MUON_TOI_DA;
    if (soNgayQuaHan > 0)
        std::cout << "Qua han " << soNgayQuaHan << " ngay, tien phat: "
                  << soNgayQuaHan * TIEN_PHAT_MOI_NGAY << " dong\n";
    else
        std::cout << "Con " << -soNgayQuaHan << " ngay nua la den han tra\n";
    return 0;
}

Chạy thử 2 lần, một lần mượn 17 ngày và một lần mới mượn 10 ngày:

Chương trình tính hạn trả sách: mượn 17 ngày bị phạt 6000 đồng, mượn 10 ngày còn 4 ngày

Lệnh if ở đây chúng ta sẽ học kỹ ở bài rẽ nhánh. Giờ nếu thư viện tăng tiền phạt lên 3000 đồng, các bạn chỉ phải sửa đúng một dòng.

Bài tập

  1. Khai báo biến lưu số lượng sách trong kho bằng cả 3 cách khởi tạo. Thử khởi tạo bằng {2.5} và đọc thông báo lỗi.
  2. Viết chương trình nhập bán kính, in ra chu vi và diện tích hình tròn. Số pi khai báo bằng constexpr.
  3. Trong chương trình phạm vi biến ở trên, bỏ cặp ngoặc { } của khối bên trong rồi biên dịch lại. g++ báo lỗi gì, vì sao?
  4. Biến const int x = 10; và constexpr int y = x * 2; có hợp lệ không? Còn nếu x được nhập từ bàn phím thì sao?

Lời giải sẽ có trong bài tổng hợp bài tập C++ cơ bản nhé.

Kết

Khai báo hằng và biến C++ đúng cách giúp trình biên dịch bắt lỗi thay cho các bạn: ngoặc nhọn chặn mất dữ liệu khi khởi tạo, const chặn sửa nhầm giá trị, constexpr đảm bảo hằng số có ngay lúc biên dịch. Ghi nhớ thêm thói quen quan trọng nhất của bài: luôn khởi tạo biến ngay khi khai báo.

Bài tiếp theo chúng ta sẽ tìm hiểu toán tử trong C++ để tính toán với các biến này. Nếu thấy bài viết có ích, hãy chia sẻ và đừng quên ghé hội Nghiện lập trình để cùng trao đổi nhé.

Rate this post

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *