Biến là gì
Biến (variable) là một vùng nhớ có tên, dùng để chứa một giá trị. Trong C++, mỗi biến có một kiểu cố định, được chọn khi bạn khai báo, và kiểu đó không bao giờ thay đổi. Đây là điều khiến C++ trở thành ngôn ngữ kiểu tĩnh (statically typed): trình biên dịch biết kiểu của mọi biến trước khi chương trình chạy và từ chối biên dịch code lưu sai loại giá trị.
Một câu lệnh khai báo có ba phần: kiểu, tên và (gần như luôn có) giá trị ban đầu.
Hãy đọc int age = 30; là "tạo một biến int tên age và đặt 30 vào đó". Dấu chấm phẩy kết thúc câu lệnh, giống như trang về chú thích đã cho thấy mọi câu lệnh đều vậy. Để ý isActive được in ra là 1: mặc định bool hiển thị dưới dạng 1/0, điều mà trang kiểu dữ liệu sẽ nói thêm.
Khởi tạo và gán
Hai thao tác này trông giống nhau nhưng thực ra khác nhau, và sự khác biệt đó là một trong những ý quan trọng nhất của trang này.
Khởi tạo (initialization) cho biến giá trị đầu tiên ngay trong câu lệnh khai báo. Gán (assignment) thay đổi giá trị của một biến đã tồn tại.
Bạn có thể khai báo mà không khởi tạo, rồi gán sau, nhưng hãy cẩn thận:
Đoạn code trên chạy được vì ta đã gán score trước khi đọc nó. Phiên bản nguy hiểm là đọc trước, sẽ được nói ngay sau đây.
Cái bẫy biến chưa khởi tạo
Đây là cái bẫy kinh điển của C++ mà nhiều ngôn ngữ khác không có. Đọc một biến cục bộ đã khai báo nhưng chưa từng được gán giá trị là hành vi không xác định: biến chứa bất kỳ byte nào tình cờ nằm trong vùng nhớ đó.
int score; // not initialized
std::cout << score; // undefined behavior - prints garbage, or "works", or crashes
Trình biên dịch vẫn vui vẻ biên dịch đoạn này. Nó có thể in ra 0, có thể in ra 32766, và có thể cư xử khác nhau ở mỗi lần chạy hay trên mỗi máy, khiến những bug này cực kỳ khó truy tìm. Hai cách phòng thủ:
- Luôn khởi tạo khi khai báo.
int score = 0;không tốn gì mà loại bỏ hoàn toàn vấn đề. - Bật cảnh báo. Biên dịch với
-Wall -Wextrakhiến trình biên dịch đánh dấu nhiều lần đọc biến chưa khởi tạo trước khi chúng gây hại.
Hãy ưu tiên cách thứ nhất cho mọi biến cục bộ: cho nó một giá trị khởi đầu hợp lý ngay khi tạo ra.
Các kiểu khởi tạo: =, () và {}
C++ cho bạn vài cách viết giá trị ban đầu. Phần lớn chúng làm cùng một việc, nhưng khởi tạo bằng ngoặc nhọn có thêm một tính năng an toàn đáng biết.
Lý do nên dùng {} là nó từ chối chuyển đổi thu hẹp, tức những phép gán âm thầm làm mất dữ liệu. Với =, một giá trị có phần thập phân bị cắt bỏ lặng lẽ; với {}, trình biên dịch sẽ chặn bạn lại:
int x = 3.9; // compiles - x silently becomes 3 (the .9 is thrown away)
int y{3.9}; // compile error - narrowing from double to int is not allowed
Bắt được lỗi đó ngay lúc biên dịch chính là điều bạn muốn. Một thói quen phổ biến trong C++ hiện đại là mặc định dùng {} để có thêm lớp kiểm tra này, chỉ quay về = khi khởi tạo kiểu sao chép đọc tự nhiên hơn.
Quy tắc và quy ước đặt tên
C++ áp đặt vài quy tắc bắt buộc, rồi ai cũng thêm các quy ước lên trên. Các quy tắc: tên có thể chứa chữ cái, chữ số và _; không được bắt đầu bằng chữ số; không được trùng từ khóa dành riêng (như int hay return); và phân biệt chữ hoa chữ thường (age và Age là hai biến khác nhau). Tránh những tên bắt đầu bằng dấu gạch dưới theo sau là chữ hoa, hoặc chứa hai dấu gạch dưới liền nhau, vì chúng được dành riêng cho trình biên dịch và thư viện chuẩn.
Các quy ước mà phần lớn code C++ tuân theo:
- Biến dùng
snake_casehoặccamelCase, chọn một kiểu và dùng nhất quán:item_counthoặcitemCount. - Tên nên mô tả giá trị:
countchứ không phảic;user_emailchứ không phảix.
Tên rõ ràng không phải để trang trí: đó là cách chính bạn trong tương lai đọc lại code. total_price = item_count * price_per_item tự giải thích theo cách mà t = c * p không bao giờ làm được.
Phạm vi của biến
Một biến chỉ tồn tại bên trong khối lệnh, tức cặp { ... }, nơi nó được khai báo, và bị hủy tại dấu ngoặc đóng. Đó là phạm vi (scope) của nó. Một biến khai báo bên trong vòng lặp hay khối if sẽ không nhìn thấy được từ bên ngoài:
Cả i và square thuộc về vòng lặp và biến mất khi vòng lặp kết thúc; total được khai báo ở khối ngoài nên vẫn tồn tại. Một khối bên trong cũng có thể che khuất (shadow) một tên ở khối bên ngoài: một biến mới cùng tên tạm thời che đi biến bên ngoài cho đến khi khối bên trong đóng lại. Đây là nguồn gây nhầm lẫn thường gặp, nên hãy tránh dùng lại tên giữa các phạm vi lồng nhau.
Điều cần nhớ: khai báo mỗi biến trong khối nhỏ nhất cần đến nó, và khởi tạo ngay tại đó. Phạm vi hẹp nghĩa là ít tên tranh giành sự chú ý của bạn hơn, và ít khả năng đọc một giá trị ở xa nơi nó được gán.
Tiếp theo: kiểu dữ liệu
Mọi biến trong trang này đều bắt đầu bằng một kiểu: int, double, string, bool. Trang tiếp theo đi sâu vào các kiểu dữ liệu của C++: các kiểu cơ bản và kích thước của chúng, số nguyên có dấu và không dấu, float và double, char, và cách chọn đúng kiểu cho từng việc.
Câu hỏi thường gặp
Khai báo biến trong C++ như thế nào?
Viết kiểu, rồi đến tên, rồi tùy chọn gán giá trị: int age = 30;. Kiểu (int) cố định trong suốt vòng đời của biến; tên (age) là cách bạn gọi đến nó. Bạn có thể khai báo mà không có giá trị, int age;, nhưng với biến cục bộ, điều đó khiến biến chứa giá trị rác cho đến khi bạn gán, vì vậy hãy luôn khởi tạo ngay tại chỗ khai báo.
Khởi tạo và gán trong C++ khác nhau thế nào?
Khởi tạo cho biến giá trị đầu tiên ngay trong câu lệnh khai báo: int x = 5; hoặc int x{5};. Gán thì thay đổi giá trị của một biến đã tồn tại: x = 7;. Sự phân biệt này quan trọng vì đọc một biến cục bộ đã khai báo nhưng chưa từng khởi tạo là hành vi không xác định (undefined behavior).
Điều gì xảy ra nếu dùng biến chưa khởi tạo trong C++?
Đọc giá trị của một biến cục bộ chưa khởi tạo là hành vi không xác định: biến chứa bất kỳ byte nào đã nằm sẵn trong vùng nhớ đó, nên chương trình có thể in ra một số ngẫu nhiên, chạy đúng nhờ may mắn, hoặc sập. Trình biên dịch sẽ không ngăn bạn (dù nhiều trình biên dịch cảnh báo với -Wall), nên cách sửa là luôn gán giá trị cho biến cục bộ khi khai báo.