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;
}
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};
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;
}
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;
}
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;
}
Có 3 tầng phạm vi trong chương trình này:
- Biến toàn cục
tongSoSachkhai báo ngoài mọi hàm, dùng được ở mọi nơi trong file. - Biến cục bộ
soSachMuoncủamainchỉ dùng được trongmain. - Khối
{ }bên trong khai báo một biến mới cũng tênsoSachMuon. 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;
}
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;
}
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:
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
- 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. - 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. - 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? - Biến
const int x = 10;vàconstexpr int y = x * 2;có hợp lệ không? Còn nếuxđượ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é.










