Cú pháp C++ cơ bản và cách biên dịch chương trình

Cú pháp C++

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:

Biên dịch và chạy chương trình dien_tich.cpp, in ra Dien tich: 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 được std::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 trong main.
  • 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”:

g++ cảnh báo empty body khi đặt dấu chấm phẩy ngay sau if, 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: dienTich và dientich là 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:

Chạy chương trình nhập 2 số 7 và 5 bằng std::cin, in ra 7 + 5 = 12

Có 2 điểm cần để ý ở đây:

  • Một lệnh std::cout nối được nhiều <<, in lần lượt từ trái qua phải. std::cin >> a >> b cũng đọc lần lượt a rồi tới b.
  • Bài này dùng '\n' để xuống dòng, bài trước dùng std::endl. Cả 2 đều xuống dòng, nhưng std::endl cò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:

Lỗi reference to count is ambiguous khi dùng using namespace std và đặt biến tên count

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:

Sơ đồ 4 bước biên dịch C++: tiền xử lý, biên dịch, hợp dịch, liên kết, từ hello.cpp tới file chạy được

  1. 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.
  2. 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.
  3. Hợp dịch (assembling): dịch hợp ngữ sang mã máy, tạo ra file object .o.
  4. Liên kết (linking): ghép file .o vớ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ủa std::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:

Chạy g++ -E, -S, -c và liên kết từng bước, file hello.ii có 32211 dòng

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

Lỗi liên kết undefined reference to tinhTong khi hàm chỉ khai báo mà chưa viết phần thân

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

  1. 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.
  2. Trong các tên sau, tên nào hợp lệ: soSach, 3sach, so-sach, so_sach, return, Return?
  3. 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ấy using namespace std nguy hiểm hơn ta tưởng?
  4. Chạy g++ -E với chương trình diện tích, mở file .ii và 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é.

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 *