Trong bài này chúng ta sẽ tìm hiểu cú pháp C++ cơ bản: một chương trình C++ gồm những phần nào, quy tắc viết câu lệnh, comment, đặt tên, cách nhập xuất với std::cout và std::cin. Cuối bài chúng ta mở “hộp đen” g++ ra xem file .cpp được biến thành chương trình chạy được qua những bước nào, để đọc hiểu được mọi thông báo lỗi sau này.
Bài 2 trong Serie Lập trình C++ từ A tới Z
Cấu trúc một chương trình C++
Chúng ta bắt đầu bằng một chương trình nhỏ tính diện tích hình chữ nhật. Các bạn tạo file dien_tich.cpp trong thư mục hoc-cpp đã tạo ở bài cài đặt C++ trên VS Code nhé:
// Chuong trinh tinh dien tich hinh chu nhat
#include <iostream>
int main()
{
int dai = 5; // chieu dai
int rong = 3; /* chieu rong */
int dienTich = dai * rong;
std::cout << "Dien tich: " << dienTich << std::endl;
return 0;
}
Biên dịch và chạy, chương trình in ra diện tích bằng 15:
Chương trình ngắn vậy thôi nhưng đã có đủ các thành phần mà chương trình C++ nào cũng có:
- Comment (chú thích): dòng bắt đầu bằng
//hoặc nằm giữa/*và*/. Trình biên dịch bỏ qua phần này, nó chỉ để người đọc code hiểu. - Chỉ thị tiền xử lý (preprocessor directive): dòng bắt đầu bằng
#, ở đây là#include <iostream>để dùng đượcstd::cout. - Hàm
main: điểm bắt đầu của chương trình. Chạy chương trình là chạy từ dòng đầu tiên trongmain. - Khối lệnh (block): phần nằm giữa cặp ngoặc nhọn
{ }. - Câu lệnh (statement): mỗi dòng kết thúc bằng dấu chấm phẩy
;.
Các bạn có thể hình dung chương trình như một công thức nấu ăn. #include là phần “chuẩn bị dụng cụ”, main là phần “các bước làm”, còn mỗi câu lệnh là một bước, làm lần lượt từ trên xuống dưới.
Câu lệnh, khối lệnh và dấu chấm phẩy
Trong C++, dấu chấm phẩy ; báo cho trình biên dịch biết câu lệnh kết thúc ở đâu. Xuống dòng không có ý nghĩa gì, nên 2 cách viết dưới đây là như nhau với g++:
int dai = 5; int rong = 3; int dai = 5; int rong = 3;
Tất nhiên cách thứ hai dễ đọc hơn nhiều, nên mỗi dòng chỉ nên có một câu lệnh.
Khối lệnh { } gom nhiều câu lệnh thành một nhóm. Thân hàm main, thân của if, của vòng lặp đều là khối lệnh. Biến khai báo trong khối nào thì chỉ dùng được trong khối đó, phần này mình sẽ nói kỹ ở bài biến và hằng.
Còn một chỗ đặt dấu ; sai mà người mới hay mắc, mà g++ không báo lỗi. Chương trình dưới đây kiểm tra tuổi lái xe:
#include <iostream>
int main()
{
int tuoi = 15;
if (tuoi >= 18);
{
std::cout << "Ban du tuoi lai xe" << std::endl;
}
return 0;
}
Mới 15 tuổi mà chương trình vẫn in “Ban du tuoi lai xe”:
Lý do là dấu ; ngay sau if (tuoi >= 18). Với C++, dấu ; đứng một mình là một câu lệnh rỗng, nên if chỉ điều khiển câu lệnh rỗng đó. Khối { } phía dưới trở thành khối bình thường và luôn được chạy.
Lưu ý: Đây là lý do mình luôn biên dịch với -Wall -Wextra. Lỗi này đúng cú pháp nên g++ không báo error, nhưng có bật cảnh báo thì g++ chỉ ra ngay dòng 7. Thấy warning thì đừng bỏ qua, phần lớn đều là lỗi logic thật.
Comment trong C++
C++ có 2 kiểu comment:
// Comment mot dong: tu dau // toi het dong /* Comment nhieu dong: nam giua dau mo va dau dong */
Comment nên giải thích tại sao viết như vậy, chứ không lặp lại code làm gì. Ví dụ i++; // tang i len 1 là comment thừa, ai đọc code cũng thấy rồi. Còn delay(20); // cho nut nhan het rung thì có ích, vì người đọc không tự đoán ra được.
Comment kiểu /* */ không lồng nhau được: dấu */ đầu tiên gặp phải sẽ đóng comment. Muốn tạm tắt một đoạn code dài thì các bạn bôi đen rồi nhấn Ctrl + / trong VS Code, nó sẽ thêm // vào từng dòng.
Định danh: quy tắc đặt tên trong C++
Định danh (identifier) là tên các bạn đặt cho biến, hàm, lớp… Ví dụ ở trên có dai, rong, dienTich. Quy tắc đặt tên trong C++:
- Chỉ gồm chữ cái tiếng Anh, chữ số và dấu gạch dưới
_. - Không bắt đầu bằng chữ số.
- Không trùng với từ khóa của C++.
- Phân biệt chữ hoa, chữ thường:
dienTichvàdientichlà 2 tên khác nhau.
| Tên | Hợp lệ? | Lý do |
|---|---|---|
dienTich |
Có | |
so_luong_2 |
Có | |
_tam |
Có, nhưng nên tránh | Tên bắt đầu bằng _ hay dùng cho thư viện chuẩn |
2dai |
Không | Bắt đầu bằng chữ số |
chieu dai |
Không | Có dấu cách |
diện_tích |
Không nên | Chữ có dấu không chạy được trên mọi trình biên dịch |
int |
Không | Trùng từ khóa |
Ngoài quy tắc bắt buộc, các bạn nên chọn một kiểu đặt tên và dùng thống nhất. Hai kiểu phổ biến là camelCase (dienTich, soLuongSach) và snake_case (dien_tich, so_luong_sach). Trong serie này mình dùng camelCase cho biến và hàm.
Tên tốt là tên đọc lên hiểu ngay: soLuongSach tốt hơn hẳn n hay x1. Code viết một lần nhưng đọc lại rất nhiều lần, kể cả chính các bạn sau 2 tháng nhé.
Từ khóa trong C++
Từ khóa (keyword) là những từ C++ dành riêng, mang ý nghĩa cố định, nên không dùng làm tên được. Một số từ khóa các bạn sẽ gặp trong serie:
| Nhóm | Từ khóa |
|---|---|
| Kiểu dữ liệu | int, double, char, bool, void, auto |
| Rẽ nhánh, vòng lặp | if, else, switch, case, for, while, do, break, continue, return |
| Hằng, giá trị | const, constexpr, true, false, nullptr |
| Hướng đối tượng | class, struct, public, private, protected, virtual, this |
| Bộ nhớ, ngoại lệ | new, delete, try, catch, throw |
| Khác | namespace, using, template, typename |
Danh sách đầy đủ có hơn 90 từ khóa, các bạn xem ở trang C++ keywords trên cppreference. Không cần học thuộc, VS Code tô màu khác cho từ khóa nên nhìn là nhận ra.
Nếu đã học C, các bạn sẽ thấy C++ giữ lại toàn bộ từ khóa của C và thêm nhiều từ mới như class, new, namespace, bool. Vì vậy code C viết tên biến là new hay class sẽ không biên dịch được bằng g++.
Khoảng trắng trong C++
Khoảng trắng gồm dấu cách, tab và xuống dòng. Trình biên dịch gần như bỏ qua chúng, chỉ cần để tách các từ ra. Nên 2 dòng sau là như nhau:
int dienTich=dai*rong; int dienTich = dai * rong;
Có 2 chỗ khoảng trắng mang ý nghĩa: bên trong chuỗi "..." thì dấu cách được giữ nguyên khi in ra, còn chỉ thị #include phải nằm trên một dòng riêng.
Thụt đầu dòng không ảnh hưởng tới kết quả, nhưng giúp nhìn ra ngay câu lệnh nào thuộc khối nào. Trong VS Code nhấn Shift + Alt + F để tự căn lề cả file.
Nhập xuất với std::cout và std::cin
std::cout dùng để in ra màn hình, đi kèm toán tử <<. std::cin dùng để đọc từ bàn phím, đi kèm toán tử >>. Cách nhớ: mũi tên chỉ hướng dữ liệu chạy. In ra thì dữ liệu chạy về phía cout, nhập vào thì dữ liệu chạy từ cin sang biến.
#include <iostream>
int main()
{
int a, b;
std::cout << "Nhap 2 so nguyen: ";
std::cin >> a >> b;
std::cout << a << " + " << b << " = " << a + b << '\n';
return 0;
}
Chạy thử, nhập 2 số cách nhau bởi dấu cách:
Có 2 điểm cần để ý ở đây:
- Một lệnh
std::coutnối được nhiều<<, in lần lượt từ trái qua phải.std::cin >> a >> bcũng đọc lần lượtarồi tớib. - Bài này dùng
'\n'để xuống dòng, bài trước dùngstd::endl. Cả 2 đều xuống dòng, nhưngstd::endlcòn ép đẩy dữ liệu ra màn hình ngay (flush), nên chậm hơn khi in nhiều. Thông thường dùng'\n'là đủ.
namespace std và vì sao không nên dùng using namespace std
Chữ std:: phía trước cout cho biết cout nằm trong namespace (không gian tên) std, nơi chứa toàn bộ thư viện chuẩn của C++.
Namespace giống như họ của một người. Lớp có 2 bạn tên An thì gọi “An” sẽ không biết là ai, nhưng gọi “Nguyễn An” và “Trần An” thì rõ ngay. Thư viện chuẩn có hàng nghìn cái tên, gom hết vào họ std để không trùng với tên các bạn tự đặt.
Nhiều bài hướng dẫn trên mạng viết using namespace std; ở đầu file để khỏi phải gõ std::. Viết vậy tiện thật, nhưng là “bỏ họ” của toàn bộ thư viện chuẩn. Thử chương trình đếm sau:
#include <iostream>
#include <algorithm>
using namespace std;
int count = 0;
int main()
{
count++;
cout << count << endl;
return 0;
}
Code trông không có gì sai, nhưng g++ báo lỗi:
Thư viện <algorithm> có sẵn một hàm tên std::count. Sau khi using namespace std, g++ thấy 2 cái tên count và không biết các bạn muốn dùng cái nào. Bỏ dòng using namespace std;, thêm std:: trước cout và endl là chương trình chạy bình thường.
Lưu ý: Chương trình nhỏ dùng using namespace std thì ít khi gặp lỗi, nhưng khi chương trình lớn dần và include nhiều thư viện thì lỗi trùng tên kiểu này rất khó tìm. Trong serie này mình luôn viết std:: cho quen tay ngay từ đầu, và các bạn tuyệt đối không đặt using namespace std trong file header .h.
Chương trình C++ được biên dịch như thế nào?
Ở bài trước chúng ta chỉ gõ 1 lệnh g++ là ra file chạy được. Thực ra g++ đã làm 4 bước liên tiếp:
- Tiền xử lý (preprocessing): xử lý các dòng bắt đầu bằng
#. Gặp#include <iostream>thì chép nguyên nội dung file đó vào, đồng thời bỏ hết comment. - Biên dịch (compiling): kiểm tra cú pháp C++ rồi dịch sang hợp ngữ (assembly). Lỗi cú pháp như thiếu
;được báo ở bước này. - Hợp dịch (assembling): dịch hợp ngữ sang mã máy, tạo ra file object
.o. - Liên kết (linking): ghép file
.ovới code của thư viện chuẩn, ví dụ phần code thật sự in chữ ra màn hình củastd::cout, để tạo ra file chạy được.
Chúng ta có thể bảo g++ dừng lại sau từng bước để xem kết quả trung gian:
g++ -std=c++17 -E hello.cpp -o hello.ii g++ -std=c++17 -S hello.cpp -o hello.s g++ -std=c++17 -c hello.cpp -o hello.o g++ hello.o -o hello
Mình chạy thử với file hello.cpp của bài trước:
File hello.cpp chỉ có 7 dòng, nhưng sau bước tiền xử lý thành hơn 32 nghìn dòng. Đó chính là nội dung của <iostream> và các file mà nó include tiếp. Các bạn mở thử hello.ii trong VS Code sẽ thấy 7 dòng code của mình nằm ở tận cuối file.
Lỗi biên dịch và lỗi liên kết
Biết 4 bước này giúp các bạn đọc lỗi nhanh hơn. Lỗi thiếu ; ở bài trước là lỗi biên dịch, g++ chỉ đúng dòng, đúng cột. Còn lỗi liên kết trông khác hẳn. Thử chương trình gọi một hàm mới khai báo mà quên viết phần thân:
#include <iostream>
int tinhTong(int a, int b); // chi khai bao, chua viet than ham
int main()
{
std::cout << tinhTong(2, 3) << std::endl;
return 0;
}
Bước biên dịch vẫn qua, vì khai báo int tinhTong(int a, int b); là đủ để g++ biết cách gọi hàm. Tới bước liên kết, ld (chương trình liên kết) đi tìm phần thân của tinhTong và không thấy, nên báo undefined reference. Lỗi này không chỉ ra dòng nào trong code.
Lưu ý: Gặp undefined reference thì đừng dò cú pháp. Hãy kiểm tra hàm đó đã viết phần thân chưa, tên và kiểu tham số ở chỗ khai báo và chỗ định nghĩa có khớp nhau không, và file .cpp chứa hàm đó đã được đưa vào lệnh biên dịch chưa.
Phần khai báo hàm, định nghĩa hàm và tách code ra nhiều file chúng ta sẽ học kỹ ở bài về hàm. Các bạn đã học C thì quy trình này giống hệt bài cấu trúc và cách biên dịch chương trình C, chỉ khác là dùng g++ thay cho gcc.
Bài tập
- Viết chương trình nhập chiều dài, chiều rộng từ bàn phím rồi in ra chu vi và diện tích hình chữ nhật.
- Trong các tên sau, tên nào hợp lệ:
soSach,3sach,so-sach,so_sach,return,Return? - Xóa dòng
#include <algorithm>trong chương trình đếm ở mục namespace rồi biên dịch lại. Còn lỗi không? Vì sao kết quả này lại cho thấyusing namespace stdnguy hiểm hơn ta tưởng? - Chạy
g++ -Evới chương trình diện tích, mở file.iivà tìm xem comment của các bạn còn trong đó không.
Lời giải sẽ có trong bài tổng hợp bài tập C++ cơ bản nhé.
Kết
Cú pháp C++ cơ bản không có nhiều quy tắc, nhưng nắm chắc từ đầu sẽ giúp các bạn đọc code của người khác và đọc thông báo lỗi dễ hơn rất nhiều. Hiểu 4 bước biên dịch cũng là nền tảng để sau này làm việc với project nhiều file, thư viện ngoài, hay lập trình nhúng.
Thói quen nên giữ từ bài này: luôn bật -Wall -Wextra, viết std:: thay vì using namespace std, và đặt tên biến đọc lên là hiểu.
Bài tiếp theo chúng ta sẽ tìm hiểu các kiểu dữ liệu trong C++. Nếu thấy bài viết có ích, hãy chia sẻ cho bạn bè đang học C++ cùng nhé.








