Chuyển tới nội dung chính

Chương 2: Cú pháp lập trình

Mở đầu về lập trình với cú pháp, thao tác cơ bản với dữ liệu trong C++


1. C++ và cú pháp cơ bản của C++​

1.1. Tổng quan về C++​

1.1.1. Nguồn gốc và bản chất​

  • Kế thừa và Mở rộng: C++ được Bjarne Stroustrup phát triển từ năm 1979 như một bản nâng cấp toàn diện của ngôn ngữ C (ngôn ngữ hệ thống do Dennis Ritchie tạo ra năm 1972).

  • Mô hình lập trình: Đột phá lớn nhất của C++ so với C là việc hỗ trợ Lập trình hướng đối tượng (OOP), giúp cấu trúc hóa và quản lý các hệ thống phần mềm quy mô lớn hiệu quả hơn.

Các đặc điểm nổi bật của C++:

  • Hướng đối tượng: Hỗ trợ lập trình theo lớp, đối tượng, kế thừa, đa hình.
  • Hiệu suất cao: Thao tác trực tiếp với bộ nhớ, gần với phần cứng.
  • Tính linh hoạt: Kết hợp lập trình thủ tục và lập trình hướng đối tượng.
  • Thư viện phong phú: STL (Standard Template Library) cung cấp các cấu trúc dữ liệu và thuật toán chuẩn.

1.1.2. Ứng dụng thực tiễn​

  • Nhờ khả năng tối ưu hóa tài nguyên phần cứng và tốc độ thực thi xuất sắc, C++ là lựa chọn hàng đầu cho các lĩnh vực đòi hỏi hiệu năng khắt khe:
    1. Phát triển Game (Game Engines) và các ứng dụng đồ họa nặng.
    2. Hệ thống thời gian thực (Real-time systems) và phần mềm nhúng (Embedded software).
    3. Lõi tính toán cho Trí tuệ nhân tạo (AI) và Mạng nơ-ron (Neural networks).
    4. Hệ thống giao dịch tài chính tần suất cao (HFT).

1.1.3. Phân cấp ngôn ngữ​

Quan trọng

C++ không hoàn toàn được coi là ngôn ngữ bậc cao cũng như ngôn ngữ bậc thấp vì C++ mang theo tính chất của 2 loại ngôn ngữ này:

  • Tính chất của ngôn ngữ bậc cao: C++ cung cấp các cơ chế trừu tượng hóa mạnh mẽ như Lập trình hướng đối tượng (OOP), Template, và thư viện chuẩn (STL), giúp lập trình viên viết code gần với tư duy con người và mô hình hóa các hệ thống phức tạp một cách dễ dàng.

  • Tính chất của ngôn ngữ bậc thấp: Khác với Python hay Java (bậc cao thuần túy), C++ cho phép can thiệp sâu vào kiến trúc máy tính. Thông qua con trỏ (pointer) và quản lý bộ nhớ thủ công, lập trình viên có thể điều khiển trực tiếp việc cấp phát và giải phóng từng byte RAM, giao tiếp trực tiếp với phần cứng.


1.2. Câu chú thích (Comments)​

Dùng để mô tả hoặc ghi chú trong source code, giúp dễ dàng đọc code sau này

Có 2 cách để viết chú thích trong C++

  • Cách 1: Chú thích trên nhiều dòng (Multi-line Comments)

    • Ký tự /**/ là kí tự bắt đầu của chú thích trên nhiều dòng.
    /* Day la chu thich 1
    Day la chu thich 2 */
Loading...

Stdin:

Stdout:

  • Cách 2: Chú thích trên một dòng (Single-line Comments)
    • Kí tự // là ký tự bắt đầu của chú thích trên một dòng
  // Day la chu thich 1
// Day la chu thich 2
Loading...

Stdin:

Stdout:

Khi biên dịch source code thì các phần comments sẽ không được biên dịch

Pro Tip

Trong hầu hết các trình soạn thảo code hiện đại (như VS Code, Visual Studio...), bạn chỉ cần bôi đen các dòng code và nhấn tổ hợp phím Ctrl + / để tạo (hoặc gỡ bỏ) chú thích hàng loạt.

Chú ý quan trọng:

    1. Không được viết chú thích nhiều dòng bên trong một chú thích nhiều dòng khác.
    1. Bạn hoàn toàn có thể viết chú thích một dòng bên trong của một chú thích nhiều dòng nhưng điều này là hoàn toàn không cần thiết.
Loading...

Stdin:

Stdout:


1.3. Cấu trúc chương trình C++ cơ bản​

  • Trong C++, các câu lệnh (statements) thường được gộp lại thành các đơn vị gọi là hàm (functions).

  • Mỗi hàm đều đều là tổ hợp của một hay nhiều câu lệnh và được chạy một cách tuần tự từ trên xuống dưới.

Chú ý: Mọi chương trình C++ bắt buộc phải có hàm main.

  • Hầu hết mọi chương trình đều kết thúc sau khi câu lệnh cuối cùng trong hàm main được thực thi (trừ khi chương trình buộc phải dừng sớm do lỗi hoặc các thao tác đặc biệt).
Loading...

Stdin:

Stdout:

Dòng lệnh 1: Khai báo chỉ thị tiền xử lý #include. Lệnh này yêu cầu trình biên dịch thêm thư viện iostream (hỗ trợ nhập/xuất dữ liệu) vào chương trình.
Dòng lệnh 2: Khoảng trắng. (Nên sử dụng dòng trống để chia các khối code, giúp chương trình trở nên dễ đọc hơn).
Dòng lệnh 3: Khai báo hàm chính của chương trình (bắt buộc có khi viết chương trình C++).
Dòng lệnh 4: Dấu ngoặc nhọn mở { đánh dấu điểm bắt đầu thân hàm main.
Dòng lệnh 5 và 6: Dòng ghi chú (comments). Trình biên dịch sẽ bỏ qua các dòng này, thường dùng để giải thích code cho lập trình viên.
Dòng lệnh 7: Lệnh in ra màn hình dòng chữ "Hello World". (Lưu ý: mọi câu lệnh trong C++ đều phải kết thúc bằng dấu chấm phẩy ;).
Dòng lệnh 8: Lệnh return 0; trả về giá trị 0 cho hệ điều hành, báo hiệu chương trình đã thực thi và kết thúc thành công.
Dòng lệnh 9: Dấu ngoặc nhọn đóng } đánh dấu điểm kết thúc của hàm main.

Cấu trúc của một đoạn mã tương ứng với một chương trình máy tính thường gồm các thành phần sau:

Thành phầnÝ nghĩa
#Tiền xử lýKhai báo thư viện và định nghĩa các marco.
Khai báo biến, hàm (prototype function)Khai báo các biến toàn cục và các hàm được sử dụng trong chương trình chính.
int main() { }Chương trình chính. Nội dung của hàm, thực hiện các chức năng chính của chương trình.
Định nghĩa hàm (define function)Định nghĩa thân hàm của các hàm đã được khai báo trong phần trước đó. Lưu ý: Nếu một hàm f đã được khai báo và có lời gọi hàm đến f thì bắt buộc phải có định nghĩa hàm của f.

Hàm (function) được sử dụng để thực hiện một chức năng cụ thể nào đó (Tìm hiểu thêm)

  • Ví dụ: hàm max có thể chứa khối lệnh nhằm tìm giá trị lớn nhất của 2 số nguyên, 3 số nguyên,...

1.4. Khái niệm cốt lõi và thư viện iostream trong C++​

1.4.1. Giới thiệu biến và kiểu dữ liệu​

Mọi chương trình đều cần xử lý dữ liệu để hoạt động. Trong lập trình, một mảnh dữ liệu đơn lẻ được gọi là một giá trị (value).

Những giá trị được viết trực tiếp vào mã nguồn (source code) được gọi là literals. Đặc điểm của literals là chúng mang tính cố định (read-only) và không thể bị thay đổi trong quá trình chương trình đang chạy.

Một số loại literals cơ bản:

  • Số: 5, -6.7 (không sử dụng dấu nháy).

  • Ký tự: 'H' (được đặt trong dấu nháy đơn, chỉ chứa đúng 1 ký tự).

  • Văn bản: "Hello" (được đặt trong dấu nháy kép, có thể chứa nhiều ký tự).

Loading...

Stdin:

Stdout:

Đoạn code trên là đoạn code in ra màn hình literals cơ bản trong C++ và sẽ được giới thiệu ở chương sau.

Tuy nhiên, vì literals chỉ để đọc (read-only), nếu chúng ta muốn lưu trữ một dữ liệu có thể thay đổi và tính toán, ta cần một cách tiếp cận khác:

  • Khi chương trình chạy, dữ liệu sẽ được đưa vào bộ nhớ chính (RAM). Bạn có thể tưởng tượng RAM giống như một dãy các hộp được đánh số thứ tự liên tiếp nhau. Ở những ngôn ngữ lập trình sơ khai, để lấy một dữ liệu, bạn phải đưa ra chỉ dẫn cho chương trình đến đúng cái ô lưu trữ dữ liệu mà bạn cần rồi lấy chúng ra cho bạn (Ví dụ: Đến ô số 12 và mang ra kết quả 40). Việc phải can thiệp trực tiếp vào địa chỉ bộ nhớ như vậy rất khó khăn và dễ gây lỗi.

  • Đối tượng (Object): Thay vì truy cập trực tiếp vào các hộp lưu trữ dữ liệu, C++ quản lý dữ liệu thông qua các đối tượng. Một đối tượng đại diện cho một vùng không gian trong bộ nhớ (RAM) được dùng để chứa một giá trị.

  • Biến (Variable): Một đối tượng có thể không có tên, nhưng thông thường chúng ta sẽ đặt cho nó một cái tên (identifier) để tiện gọi lại trong mã nguồn. Một đối tượng có tên gọi chính là một biến.

ghi chú
  • Object là nơi lưu trữ giá trị trong bộ nhớ.

  • Biến (Variable) là một Object được cấp cho một cái tên. Nhờ có tên, ta có thể dễ dàng gọi và tái sử dụng giá trị đó ở các dòng code tiếp theo mà không cần quan tâm nó đang nằm ở đâu trong RAM.

Để có thể sử dụng một biến trong chương trình, bạn cần định nghĩa chúng trong chương trình (sẽ được nói chi tiết ở 2.2. Biến)

Loading...

Stdin:

Stdout:

  • Lúc biên dịch (Compile-time): Trình biên dịch đọc code và biết có tồn tại một biến tên x với kiểu dữ liệu là int. Từ thời điểm này trở đi, mỗi khi thấy x trong code, trình biên dịch biết bạn đang nhắc đến biến nào. Nó cũng tự động tính toán xem biến này cần bao nhiêu bộ nhớ và quy trình quản lý nó ra sao.

  • Lúc chạy (Runtime): Khi chương trình thực sự được nạp vào bộ nhớ để hoạt động, biến đó mới chính thức được cấp phát (allocation) một không gian lưu trữ thực tế (ví dụ: một vùng trong RAM). Lúc này, biến mới sẵn sàng để chứa dữ liệu.

Ở trên chúng ta biết đối tượng/biến là nơi chứa dữ liệu. Vậy Kiểu dữ liệu (Data type) chính là quy định xem nơi đó được phép chứa loại giá trị gì (số nguyên, số thực, ký tự...) và nhắc cho Trình biên dịch biết cần phải cấp phát bao nhiêu ô nhớ cho việc lưu trữ dữ liệu này.

Trong ví dụ int x;, từ khóa int (integer) chỉ định rằng biến x là một biến số nguyên. Nó chỉ có thể lưu trữ các số không có phần thập phân (như 4, 27, 0, -12). Nếu bạn muốn lưu các loại giá trị khác (ví dụ số thực), C++ cung cấp các kiểu dữ liệu khác như double hay float.

Để hiểu hơn về Kiểu dữ liệu và Biến, mời bạn đọc 2.1. Kiểu dữ liệu và 2.2. Biến

Khai báo biến trên cùng một dòng:

int a, b; 
// Cách viết trên hoàn toàn tương đương với:
// int a;
// int b;

1.4.2. Thư viện nhập xuất​

Có 2 loại thư viện nhập xuất trong C++

  • Thư viện nhập xuất <stdio.h> (kế thừa từ ngôn ngữ C)
  • Thư viện nhập xuất tiêu chuẩn <iostream> của C++.

Để sử dụng thư viện, các bạn sử dụng cú pháp:

#include <tên_thư_viện>

Ví dụ:

  #include <iostream>
#include <cmath>
#include <iomanip>
#include <limits>
#include <algorithm>
#include <vector>
// ...
// Mỗi thư viện sẽ chứa các hàm/đối tượng khác nhau,
// khi cần sử dụng một hàm/đối tượng nào thì cần khai báo chính xác thư viện chứa nó
Ngôn ngữ C++Ngôn ngữ C
Khai báo thư viện#include <iostream>#include <stdio.h>
Lệnh nhậpcin >>
cin.get, cin.getline,
getline (<string>)
scanf
fgetc, getc, getchar, fgets
Lệnh xuấtcout <<printf
putchar, putc, puts, fputs

Tuy nhiên, khuyến khích các bạn sử dụng thư viện <iostream> đối với ngôn ngữ lập trình C++.

  • Những đối tượng/hàm thao tác (objects/manipulators) cơ bản mà thư viện <iostream> cung cấp bao gồm:
    • std::cout (Character Output): Đây là một đối tượng (thuộc lớp ostream) đại diện cho luồng đầu ra tiêu chuẩn. Nó cho phép gửi dữ liệu và in chúng ra màn hình console dưới dạng text.
    • std::cin (Character Input): Đây là một đối tượng (thuộc lớp istream) đại diện cho luồng đầu vào tiêu chuẩn. Nó được sử dụng để đọc dữ liệu mà người dùng nhập vào từ bàn phím.
    • std::endl (End Line): Đây là một hàm thao tác (manipulator). Khi sử dụng, nó thực hiện hai việc: chèn một ký tự xuống dòng (\n) và ngay lập tức đẩy dữ liệu từ bộ đệm ra ngoài (flush buffer) để đảm bảo nội dung hiện lên màn hình. Chức năng của hàm này là giúp output của chúng ta được in ra trên từng dòng riêng biệt nhau.
Pro Tip

Để các bài toán duyệt dữ liệu khổng lồ không bị lỗi Time Limit Exceeded (TLE), khuyến khích các bạn luôn đặt đoạn code fastIO này ở ngay dòng đầu tiên trong hàm main():

int main()
{
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
// Code something...
return 0;
}

Ví dụ: Để hiểu hơn về các câu lệnh std::cout, std::cin, và std::endl trong ví dụ bên dưới thì mời các bạn đọc các mục 1.3.3, 1.3.4, và 1.3.5

Loading...

Stdin:

Stdout:

1.4.3. Câu lệnh xuất: std::cout​

  • std::cout là một đối tượng giúp bạn có thể hiện thị nội dung như: số nguyên, số thực, giá trị của biến, đoạn text ra màn hình. Để sử dụng đối tượng này bạn cần khai báo thư viện iostream.
  • std::cout được sử dụng đi kèm với toán tử chèn << (Toán tử chèn có vai trò điều hướng, đẩy dữ liệu từ các tham số ở phía bên phải vào dòng luồng đầu ra (output stream) là std::cout để đưa lên màn hình.)
  • Cú pháp:
    #include <iostream> 
    ...
    std:: cout << Tham_số_1 << Tham_số_2 << ... << Tham_số_k;
    hoặc
    #include <iostream> 
    ...
    std::cout << Tham_số_1 << Tham_số_2 << ... << Tham_số_k;
  • Các loại tham số thường gặp:
    • Chuỗi hằng
    • Biến, hằng số, biểu thức, hàm
    • Ký tự điều khiển (escape sequence)

      Ký tự điều khiển (escape sequence): Gồm dấu \ và một ký tự
      Ví dụ \n dùng để xuống dòng, \t cùng để cách vào một khoảng tab

  • Ngoài \n, endl là một đối tượng đặc biệt giúp bạn có thể xuống dòng khi sử dụng cout, bạn sẽ thêm đối tượng này trong câu lệnh cout tại vị trí bạn muốn xuống dòng.

Ví dụ 1:

Loading...

Stdin:

Stdout:

Ví dụ 2:

Loading...

Stdin:

Stdout:

` Lưu ý khi in giá trị của số thực float, double

Mặc định chương trình sẽ in ra 6 chữ số có nghĩa kể cả phần nguyên và phần thập phân.

Loading...

Stdin:

Stdout:

Đối với kiểu dữ liệu số thực, đề bài thường yêu cầu bạn in ra các số này với độ chính xác nhất định, cụ thể k chữ số phần thập phân sau dấu phẩy. Khi đó, bạn hãy thực hiện theo các bước sau:

    1. Thêm thư viện <iomanip> ở đầu chương trình.
    1. Thêm cụm << std::fixed << std::setprecision(k) << số_cần_làm_tròn;

Ví dụ:

Loading...

Stdin:

Stdout:

Xác định độ rộng khi in với setw và setfill

Khi muốn in ra giá trị của 1 số, 1 biến hay đoạn text với độ rộng tương đương bao nhiêu dấu cách bạn có thể sử dụng std::setw(k) với k là độ rộng trước tên biến hay nội dung bạn muốn in.

Trong trường hợp bạn muốn thay thế các dấu cách khi nội dung bạn in ra không đủ độ rộng chỉ định bạn có thể sử dụng std::setfill(c) với c là ký tự bạn muốn thay thế cho các dấu cách trống.

Loading...

Stdin:

Stdout:

Ứng dụng dễ thấy nhất của std::setw và std::setfill chính là in ra dữ liệu dưới dạng bảng:

Loading...

Stdin:

Stdout:

1.4.4. Câu lệnh nhập: std::cin​

  • std::cin là một đối tượng được sử dụng để nhập dữ liệu từ thiết bị nhập chuẩn (thông thường là bàn phím).
  • Tương tự như cout, để sử dụng std::cin bạn cần khai báo thư viện <iostream>.
  • std::cin được sử dụng đi kèm với toán tử trích xuất (extraction operator) >>. Toán tử này sẽ điều hướng dữ liệu từ bàn phím vào biến mà bạn chỉ định.
  • Cú pháp:
Loading...

Stdin:

Stdout:

Để chạy được chương trình thì bạn copy-paste đoạn sau vào ô stdin sau đó ấn Run Code nhé:

Hello UIT
1
2.1

  • Các biến std::cin có thể nhập được các kiểu dữ liệu cơ bản int, long long, short, float, double, char, bool, std::string
    • Khi nhập số (int, double,...) std::cin sẽ tự động bỏ qua các khoảng trắng, dấu tab hoặc dấu xuống dòng ở đầu cho đến khi thấy một con số. Nếu không tìm thấy số trong dữ liệu nhập, chương trình sẽ báo lỗi.
    • Khi nhập ký tự (char) std::cin >> sẽ lấy ký tự không phải khoảng trắng đầu tiên mà nó thấy.
    • Khi nhập bool, theo mặc định phải nhập một giá trị số tương ứng với true (số khác 0) và false (số 0)
      • Nếu không tìm thấy giá trị số trong dữ liệu nhập, chương trình sẽ báo lỗi.
      • Trong nhiều trình biên dịch, chương trình sẽ gán cho biến bool giá trị mặc định là 0 và không báo lỗi. Đó là lý cho khi bạn nhập true (hay false) vào một biến bool, giá trị biến sẽ là 0. Do true và false là một chuỗi không hợp lệ với bool.
      • Nếu muốn chương trình hiểu true và false không phải một dữ liệu kiểu chuỗi, có thể dùng lệnh std::cin >> std::boolalpha >> bien_bool để nhập dữ liệu cho bien_bool dưới dạng true hoặc false. Trường hợp này sẽ ngược lại với trên, kể cả khi bạn nhập giá trị số hợp lệ, giá trị bool vẫn sẽ là 0. std::boolalpha chỉ cho biến bool nhận đúng giá trị khi nhập true hoặc false.
    • Khi nhập std::string, giá trị nhận được sẽ là chuỗi các ký tự cho trước khi gặp khoảng trắng, tab hay xuống dòng. Ta sẽ xem kỹ hơn hướng xử lý khi nhập chuỗi ở bên dưới.
  • Có thể nhập liên tiếp cho nhiều biến trong cùng một câu lệnh, với kiểu dữ liệu không nhất thiết giống nhau
    std::cin >> bien_1 >> bien_2 >> ... >> bien_k;
    Khi đó, các biến sẽ được phân cách bằng một ký tự trắng (white space characters). White space character bao gồm:
    • Ký tự khoảng trắng (ASCII code = 32): Khi nhấn SPACE
    • Ký tự tab \t: Khi nhấn TAB
    • Ký tự xuống dòng \n: Khi nhấn ENTER
Loading...

Stdin:

Stdout:

Copy-Paste đoạn sau vào stdin nhé (bạn có thể tùy chỉnh theo ý thích trên stdin nhé):

19 3.8
A
1
Ban
true
true

Nhập chuỗi (string)​

Vì lý do trên, Khi sử dụng std::cin để nhập một chuỗi ký tự, chương trình sẽ ngừng đọc ngay khi gặp khoảng trắng đầu tiên.
Nếu muốn nhập một chuỗi có dấu cách (ví dụ: Ban học tập Công nghệ Phần mềm), ta cần dùng hàm getline()

Loading...

Stdin:

Stdout:

Copy-Paste đoạn sau vào stdin nhé (bạn có thể tùy chỉnh theo ý thích trên stdin nhé):

Ban học tập
Công nghệ Phần mềm

Giải thích luồng hoạt động:

  • std::getline(std::cin, banHocTap);: Khác với toán tử >>, hàm getline được sinh ra để đọc toàn bộ một dòng dữ liệu, bao gồm cả các dấu khoảng trắng (Space, Tab), cho đến khi nó gặp ký tự xuống dòng (\n - phím Enter).

  • Khi người dùng nhấn Enter, ký tự \n được đẩy vào bộ nhớ đệm (buffer). Hàm getline sẽ đọc tất cả dữ liệu, hút luôn cả ký tự \nnày ra khỏi bộ nhớ đệm và hủy nó đi (không lưu vào biến chuỗi).

  • Khi câu lệnh std::getline(std::cin, tenKhoa); thứ hai được gọi, bộ nhớ đệm hoàn toàn trống. Nó sẽ chờ người dùng nhập một dòng mới một cách bình thường mà không gặp bất kỳ lỗi nào.

Đây là cách chính xác nhất để nhập liên tiếp nhiều chuỗi có chứa khoảng trắng trong C++.

Cùng xem ví dụ quan trong sau đây:

Chuyện gì sẽ xảy ra nếu chúng ta cần nhập một con số (như tuổi, mã sinh viên) trước, sau đó mới nhập một chuỗi có khoảng trắng (như họ tên)?

Loading...

Stdin:

Stdout:

Copy-Paste đoạn sau vào stdin nhé (bạn có thể tùy chỉnh theo ý thích trên stdin nhé):

19
Ban học tập

Màn hình console không in ra được Tên "Ban học tập" mặc dù đã được nhập vào từ trước, lý do đến từ lỗi trôi lệnh trong C++.

Quy trình C++ xử lý và nguyên nhân gây lỗi:

  • Khi bạn nhập 19 và nhấn Enter, luồng dữ liệu thực sự đi vào buffer là: [ '1', '9', '\n' ].

  • Lệnh std::cin >> tuoi; bắt đầu quét buffer. Nó lấy số 19 gán cho biến tuoi.

  • Tuy nhiên, std::cin >> sẽ dừng lại ngay trước các ký tự trắng/xuống dòng và để lại chúng trong buffer. Lúc này, buffer còn sót lại: [ '\n' ].

  • Lệnh std::getline(std::cin, hoTen); được thực thi ngay sau đó. Nó nhìn vào buffer và thấy ký tự \n và hiểu lầm rằng người dùng vừa nhấn Enter để nhập một dòng trống. Lúc này, nó thu dọn \n, gán một chuỗi rỗng "" cho biến hoTen và kết thúc mà không chờ bạn gõ phím.

Giải pháp: Dùng lệnh std::cin.ignore()

  • Để khắc phục hiện tượng này, nguyên tắc là: Phải dọn sạch bộ nhớ đệm trước khi gọi getline nếu trước đó có dùng cin >>.

C++ cung cấp hàm std::cin.ignore() để bỏ qua các ký tự thừa trong buffer.

Loading...

Stdin:

Stdout:

Mở rộng

Lệnh std::cin.ignore(); mặc định chỉ xóa 1 ký tự. Điều này hoạt động tốt nếu người dùng nhập 19 rồi Enter. Nhưng nếu họ lỡ tay nhập 19 rồi thêm vài dấu cách rồi mới Enter (Ví dụ: buffer: [ '1', '9', ' ', ' ', '\n' ]), thì ignore() chỉ xóa được 1 dấu cách, và lỗi trôi lệnh lại tái diễn.

Giải pháp:

#include <limits> // Thêm thư viện này
// ...
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

ĐỌC THÊM

So sánh các lệnh nhập trong C++

Đặc điểmcin >>cin.get()cin.getline()getline(cin, string)
Kiểu dữ liệuĐa dạng (int, float, string...)Ký tự (char)Mảng ký tự (char[])Chuỗi đối tượng (string)
Cơ chếFormatted (Có định dạng)UnformattedUnformattedUnformatted
Xử lý khoảng trắng đầuBỏ quaKhông bỏ quaKhông bỏ quaKhông bỏ qua
Ký tự dừng (Delimiter)Khoảng trắng (space, tab, \n)Không có (đọc 1 char)\n hoặc giới hạn size\n
Ký tự \n sau khi đọcĐể lại trong bộ đệmGiữ nguyên/Lấy ra tùy cách dùng (*)Lấy ra và hủy bỏLấy ra và hủy bỏ

Giải thích thêm về (*):

  • Dùng đọc 1 ký tự char c = cin.get();: Nó sẽ lấy luôn ký tự \n (nếu \n đang ở đầu stream).

  • Dùng đọc mảng ký tự cin.get(char_array, size): Nó sẽ để lại ký tự \ntrong bộ nhớ đệm (giống hệt hiện tượng trôi lệnh).

1.4.5. Câu lệnh xuống dòng std::endl và "\n"​

Ngoài ra, các bạn có thể xuống dòng bằng câu lệnh "\n" thay cho std::endl. Mặc dù chúng cùng chung một chức năng xuống dòng nhưng lại có một số điểm khác biệt sau:

Trong quá trình giải các bài toán yêu cầu thời gian thực thi khắt khe, việc lạm dụng std::endl để xuống dòng có thể làm chương trình chạy chậm đi do thao tác flush buffer liên tục. Để tối ưu hóa và tránh lỗi quá thời gian (Time Limit Exceeded), bạn nên ưu tiên sử dụng ký tự \n thay thế.

Phân biệt giữa std::endl và "\n"

Khi bạn sử dụng std::cout, dữ liệu không được ghi trực tiếp ra màn hình ngay. Thay vào đó, nó được đẩy vào một vùng nhớ tạm trên RAM gọi là Output Buffer (Bộ nhớ đệm xuất). Việc giao tiếp với các thiết bị ngoại vi (như in ra màn hình Console) đòi hỏi các Lời gọi hệ thống - những thao tác rất tốn thời gian thực thi. Do đó, C++ gom nhóm dữ liệu trong buffer và chỉ thực hiện Lời gọi hệ thống khi buffer đã đầy hoặc có tín hiệu yêu cầu.

  • Ký tự "\n" (Newline Escape Sequence):

    • Bản chất: Là một ký tự điều khiển (control character).

    • Cơ chế: Lệnh std::cout << "\n" chỉ đơn thuần chèn thêm ký tự xuống dòng vào Output Buffer. Nó không can thiệp vào quá trình giao tiếp với hệ điều hành. Hệ điều hành sẽ tự động xả bộ đệm (flush) khi vùng nhớ này đầy hoặc khi chương trình kết thúc bình thường.

    • Hiệu năng: Rất cao, vì thao tác ghi vào buffer chỉ diễn ra trên RAM nội bộ.

  • Câu lệnh std::endl (I/O Stream Manipulator):

    • Bản chất: Là một hàm thao tác luồng dữ liệu (manipulator) được định nghĩa trong thư viện <ostream>.

    • Khi gọi std::endl, luồng dữ liệu sẽ thực hiện tuần tự hai tác vụ:

    • Chèn ký tự \n vào luồng.

    • Gọi phương thức flush() (cụ thể là std::cout.flush())). Thao tác này ép buộc (force) chương trình tạm dừng để thực hiện Lời gọi hệ thống, đồng bộ hóa toàn bộ dữ liệu từ RAM đẩy ra thiết bị xuất ngay lập tức, bất kể buffer có đầy hay không.

    • Hiệu năng: Thấp hơn đáng kể nếu bị lạm dụng.

Loading...

Stdin:

Stdout:

Giải thích hiện tượng TLE (Time Limit Exceeded):

  • Trong các bài toán yêu cầu in ra hàng trăm ngàn dòng kết quả, nếu bạn sử dụng std::endl trong vòng lặp, chương trình sẽ phải thực hiện hàng trăm ngàn Lời gọi hệ thống liên tục để đẩy dữ liệu ra Console. Điều này khiến thời gian chạy tăng lên rất nhiều. Thay vào đó, việc dùng \n giúp CPU gom dữ liệu và chỉ thực hiện I/O một vài lần, tối ưu hóa đáng kể tốc độ thực thi.

Bảng tóm tắt:

Đặc điểm\nstd::endl
Vai tròKý tự xuống dòngLệnh xuống dòng + Ép xuất dữ liệu
Hành động thêmKhông làm gì thêmGọi hàm std::flush
Hiệu suấtCao (Tối ưu cho vòng lặp lớn)Thấp (do I/O liên tục)

1.5. Giới thiệu không gian tên (namespace)​

1.5.1. Naming Collision (Naming Conflict)​

Hiện tượng Xung đột tên (Naming Collision) thường xảy ra trong hai trường hợp chính sau:

    1. Lỗi biên dịch (Compiler Error): Khi 2 (hoặc nhiều) hàm, hoặc biến toàn cục trùng tên được định nghĩa bên trong cùng một file. Trình biên dịch (Compiler) sẽ phát hiện lỗi ngay lập tức và dừng quá trình build.
    • Ví dụ:

      Loading...

      Stdin:

      Stdout:

    1. Lỗi liên kết (Linker Error): Khi 2 (hoặc nhiều) hàm, hoặc biến toàn cục trùng tên được định nghĩa ở các file khác nhau nhưng cùng thuộc về một chương trình. Trình biên dịch sẽ cho qua từng file riêng lẻ, nhưng Trình liên kết (Linker) sẽ báo lỗi khi cố gắng gộp chúng lại với nhau vì không biết phải chọn bản định nghĩa nào.
    • Ví dụ:
      • File a.cpp:

        Loading...

        Stdin:

        Stdout:

      • File main.cpp:

        Loading...

        Stdin:

        Stdout:

        • Cả 2 file mã nguồn trên đều thuộc cùng một chương trình nhưng được tách ra để dễ quản lý. Khi build chương trình, Trình biên dịch (Compiler) sẽ hoạt động trước, biên dịch độc lập từng file .cpp thành các file object (thường có đuôi .o hoặc .obj). Sẽ không có lỗi nào bị phát hiện ở bước này.
        • Tuy nhiên, lỗi sẽ xuất hiện ở bước tiếp theo. Trình Linker làm nhiệm vụ liên kết các file object này lại với nhau để tạo thành file thực thi cuối cùng (file .exe). Lúc này, Linker nhìn thấy có đến 2 bản định nghĩa cho hàm myFcn() và không biết phải sử dụng bản nào, dẫn đến lỗi từ chối liên kết (multiple definition).

Toàn bộ các lỗi được liệt kê ở trên được gọi là Naming Colission (hay Naming Conflict)

1.5.2. Namespace​

  • Để khắc phục được lỗi Xung đột tên (Naming Collision), sử dụng Không gian tên (Namespace) là giải pháp tối ưu nhất.

  • Namespace cung cấp cho lập trình viên một Vùng phạm vi độc lập (thường được gọi là phạm vi namespace). Nó cho phép người dùng khai báo hoặc định nghĩa các biến và hàm bên trong vùng này mà không sợ bị trùng lặp với các phần khác của chương trình.

Cú pháp:

namespace NamespaceIdentifier
{
// Nội dung của namespace (khai báo, định nghĩa...) nằm ở đây
}

Ví dụ minh họa chia file:

  • File a.cpp:

    Loading...

    Stdin:

    Stdout:

  • File b.cpp:

    Loading...

    Stdin:

    Stdout:

  • File main.cpp: Khi muốn truy cập vào hàm doSomething() của bất kỳ namespace nào từ một file khác, bạn cần làm hai việc: Khai báo trước (Forward declaration) hàm đó bên trong đúng namespace của nó, và sử dụng Toán tử phân giải phạm vi (Scope Resolution Operator ::) để gọi hàm.

    Loading...

    Stdin:

    Stdout:

File sau khi hoàn thành liên kết:

Loading...

Stdin:

Stdout:

Output:
7
-1

1.5.3. Thư viện chuẩn C++ và namespace std​

Trong giai đoạn đầu phát triển C++, toàn bộ các hàm và đối tượng của thư viện chuẩn (như lệnh nhập/xuất) đều được đặt ở phạm vi toàn cục (global scope). Điều này rất tiện lợi vì lập trình viên có thể gọi trực tiếp chúng mà không cần thêm tiền tố std:: nào.

Tuy nhiên, Thư viện chuẩn chứa hàng ngàn tên định danh (keywords). Nếu bạn vô tình đặt tên một biến hoặc hàm trùng với một tên đã có sẵn trong thư viện chuẩn, lỗi Xung đột tên (Naming Collision) sẽ xảy ra.

Giải pháp của C++: Để giải quyết triệt để vấn đề này, các nhà phát triển C++ đã chuyển toàn bộ nội dung của thư viện chuẩn vào một không gian tên riêng biệt có tên là std (viết tắt của standard - tiêu chuẩn).

Đó là lý do vì sao khi sử dụng các công cụ cơ bản trong C++, bạn luôn phải gọi kèm tiền tố std:: thông qua toán tử phân giải phạm vi (::):

  • std::cin
  • std::cout
  • std::endl
  • std::fixed
  • std::setprecision(k)

Cách rút gọn tiền tố std::

Việc lặp lại std:: liên tục có thể khiến code trở nên dài dòng. C++ cung cấp cho chúng ta hai cách để giải quyết vấn đề này:

Cách 1: Sử dụng using namespace std; (Nhanh gọn nhưng tiềm ẩn rủi ro) Câu lệnh này sẽ yêu cầu trình biên dịch mang toàn bộ các tên định danh trong std ra ngoài sử dụng trực tiếp.

Loading...

Stdin:

Stdout:

LƯU Ý QUAN TRỌNG

Mặc dù using namespace std; rất tiện lợi, nhưng bạn không nên tạo thói quen luôn luôn viết nó ở đầu mỗi file. Việc mở ra toàn bộ thư viện std sẽ mang rủi ro xung đột tên giống như mình có đề cập ở đầu mục. Đặc biệt, không bao giờ sử dụng câu lệnh này ở phạm vi toàn cục bên trong các file header (.h).

Cách 2: Khai báo using tường minh (An toàn, khuyên dùng) Thay vì mang toàn bộ thư viện ra, bạn chỉ khai báo chính xác những gì bạn thực sự cần dùng.

Loading...

Stdin:

Stdout:


1.6. Nhập xuất vào file​

Ngoài tương tác với dữ liệu qua màn hình console, C++ còn cung cấp các thư viện nhận và xuất dữ liệu vào các file trong hệ thống, giúp đơn giản quá trình nhập xuất dữ liệu thủ công.

Khi sử dụng file ta cần phải khai báo thư viện fstream với cú pháp: #include <fstream>

Khi xử lí file trong C++ ta có 3 class là:

  • ifstream là class để đọc dữ liệu đầu vào từ file
    Ví dụ khai báo một biến kiểu ifstream để đọc dữ liệu từ một file có tên là input.txt: ifstream ip("input.txt");
  • ofstream là class để ghi dữ liệu vào (o viết tắt của out, f viết tắt của file) Ví dụ khai báo một biến ofstream để mở file: ofstream op;
  • fstream là class để đọc hoặc ghi dữ liệu. Ta có thể thay thế 2 từ khóa trên bằng từ khóa fstream

Đọc ghi file thường có các chế độ (mode) định dạng đi kèm như sau:

  • ios::in dùng để Mở cho các hoạt động đầu vào. (mode mặc định của ifstream)
  • ios::out dùng để mở cho các hoạt động đầu ra. (mode mặc định của ofstream)
  • ios::binary dùng để mở file nhị phân.
  • ios::ate là đặt vị trí con trỏ ở cuối file khi chúng ta mở file.
  • ios::app là khi file đã có sẵn data thì dữ liệu sẽ được thêm vào cuối file.
  • ios::trunc là khi ta chèn dữ liệu vào file, thì csc dữ liệu cũ sẽ bị xóa hết.

Để ghi dữ liệu vào file ta dùng cú pháp: nameFile<<"Data";

Để đọc dữ liệu từ file ta dùng cú pháp: nameFile>>value;

Để mở file bất kì ta dùng hàm open("nameFile", mode);

Loading...

Stdin:

Stdout:

Khi sử dụng file trong C++ ta cần lưu ý liên kết tới file ta cần sử dụng ở đây bình thường ta copy link sẽ là C:\Users\minhh\OneDrive\Desktop\input.txt sẽ bị lỗi do \ là một kí tự đặc biệt và nó sẽ hiểu \U \m \O \D \i là một lệnh giống như \n (trỏ xuống dòng) vì vậy để ta coi \ là một char bình thường ta cần phải thêm \ trước \ tức là \\ như code mẫu trên.

Bình thường khi chúng ta lấy dữ liệu từ bàn phím ta sẽ sử dụng câu lệnh cin >> và in dữ liệu ra màn hình console ta dùng cout <<, và tương tự với đọc dữ liệu từ file và ghi dữ liệu vào file ta chỉ cần thay cin thành tên file cần đọc dữ liệu, và thay cout thành tên file cần ghi dữ liệu vào.

Nếu chỉ dùng input >> str; thì ta chỉ đọc được chuỗi cho tới khi nó gặp dấu cách. Muốn lấy dữ liệu lấy được cả dấu cách ta dùng hàm getline(input,str);


1.6. Bộ keywords trong C++​

1.6.1. Ký tự (Character)​

C++ sử dụng nhiều loại ký tự khác nhau để viết chương trình:

  • Chữ cái ký tự Latin: A, B, C, ..., Z, a, b, c, ..., z
  • Chữ số thập phân: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
  • Chú thích
    • Chú thích một dòng: //
    • Chú thích nhiều dòng: /* ... */
  • Dấu nháy đơn: Ký tự được đặt trong dấu nháy đơn ' ... '
  • Dấu nháy kép: Chuỗi được đặt trong dấu nháy kép " ... "
  • Ký tự điều khiển: sử dụng ký tự thoát (escape characters) là dấu gạch chéo ngược \ đặt ở đầu: \n (dòng mới), \t (tab ngang), \\ (ký tự gạch chéo ngược), \' (dấu nháy đơn), \" (dấu nháy kép), \0 (null)
  • Toán tử: +, -, *, /, %, ++, --, =, +=, -=, *=, /=, %=, ==, !=, >, <, >=, <=, &&, ||, !, ? ,= (toán tử điều kiện)
  • Ký tự phân tách: ; (dấu chấm phẩy), , (dấu phẩy)
  • Ký tự ngoặc: (, ) , {, } , [, ]
  • Ký tự toán tử bit: &, |, ^, ~, <<, >>
  • Ký tự điều khiển: # (cho các chỉ thị tiền xử lý)
  • Ký tự chấm: . (truy cập thành viên)
  • Ký tự mũi tên: -> (truy cập thành viên qua con trỏ)
  • Ký tự hai chấm: : (sử dụng trong toán tử ba ngôi và khai báo phạm vi), :: (toán tử phạm vi)
  • Ký tự dấu gạch dưới: _ (thường được sử dụng trong tên biến hoặc hàm)

1.6.2. Từ khóa (keyword)​

Không thể sử dụng từ khóa để đặt tên cho biến, hàm, tên chương trình con.

Keyword chỉ chứa ký tự chữ cái thường.
Không có ký hiệu hoặc dấu câu đặc biệt nào được sử dụng trong từ khóa.

Một số từ khóa thông dụng:

  • Nhóm 1: Điều khiển luồng
    if, else, switch, case, default, for, while, do, break, continue, goto, return, try, catch, throw
  • Nhóm 2: Khai báo và định nghĩa
    • int, char, float, double, void, bool, short, long, signed, unsigned, enum
    • class, struct, union, typedef, constexpr, consteval, concept, decltype, inline, static, extern, mutable, thread_local, register
  • Nhóm 3: Truy cập và quản lý bộ nhớ
    new, delete, sizeof, alignas, alignof, reinterpret_cast, static_cast, dynamic_cast, const_cast
  • Nhóm 4: Lập trình hướng đối tượng
    class, public, private, protected, virtual, explicit, friend, this, operator
  • Nhóm 5: Không gian tên và mô-đun
    namespace, using, export, module, import
  • Nhóm 6: Từ khóa không đồng bộ và đồng thời
    co_await, co_return, co_yield, synchronized, atomic_cancel, atomic_commit, atomic_noexcept
  • Nhóm 7: Logic và toán tử
    and, or, not, bitand, bitor, compl, and_eq, or_eq, not_eq
  • Nhóm 8: Các từ khóa khác
    asm, auto, bool, false, true, nullptr, static_assert, noexcept, requires, reflexpr
  • Nhóm 9: Các kiểu dữ liệu và các từ khóa liên quan đến kiểu
    int, char, char8_t, char16_t, char32_t, float, double, void, bool

1.6.3. Dấu chấm phẩy​

  • Dấu chấm phẩy dùng để kết thúc một câu lệnh. Dù trình biên dịch cho phép viết nhiều lệnh trên cùng một dòng (như cout << "Xin chao"; return 0;), tuy nhiên bạn nên viết mỗi câu lệnh trên một dòng riêng biệt để đảm bảo code dễ đọc và dễ bảo trì.

cout << "Xin chao";
return 0;

1.6.4. Tên/ Định danh (Name/Identifier)​

Một dãy ký tự dùng chỉ tên hằng, biến, hàm.

Được tạo thành từ các chữ cái, chữ số, dấu gạch dưới _

Quy ước đặt tên:

  • Không trùng với các từ khóa
  • Ký tự đầu tiên là chữ cái hoặc _
  • Tối đa 255 ký tự
  • Không được sử dụng khoảng trắng ở giữa các ký tự
  • Tên có phân biệt chữ hoa chữ thường (case sensitive).
  • Không sử dụng ký tự đặc biệt như: @, #, !,...
  • Không bắt đầu bằng chữ số: 1H, 2XinChao, 36HelloAE

Lỗi đặt tên biến khi dùng ký tự đặc biệt

Loading...

Stdin:

Stdout:

Giải thích: Các tên biến được viết ở trên (@SoNguyen, Sothuc36@@, 2Hi) đều là những tên biến không hợp lệ vì chúng vi phạm các nguyên tắc đặt tên biến được nêu ở đầu mục.

(Để hiểu hơn về biến và các keywords int, double thì mời bạn đọc 2.1. Kiểu dữ liệu và 2.2. Biến)


2. Dữ liệu trong lập trình​

Về bản chất, lập trình là tập hợp các thao tác điều khiển và xử lý dữ liệu để giải quyết một bài toán cụ thể. Để một chương trình hoạt động hiệu quả, chúng ta không chỉ cần các dòng lệnh logic mà còn phải quản lý tốt "nguyên liệu" đầu vào và đầu ra.
Trong phần này, ta sẽ tìm hiểu các kiểu dữ liệu để phân loại dữ liệu, cách lưu trữ dữ liệu (biến) và các phép toán biến đổi dữ liệu.

2.1. Kiểu dữ liệu​

2.1.1. Khái niệm kiểu dữ liệu​

Trong lập trình, kiểu dữ liệu (data type) là một thuộc tính của dữ liệu, có vai trò báo cho trình biên dịch (compiler) hoặc trình thông dịch (interpreter) biết cách chương trình sẽ sử dụng và lưu trữ dữ liệu đó.

  • Ví dụ: Giá trị 3.2 thuộc kiểu số thực (floating-point) và giá trị 5 thuộc kiểu số nguyên (integer).

Để quản lý vùng nhớ hiệu quả, trình biên dịch quy ước mỗi kiểu dữ liệu sẽ chiếm một dung lượng lưu trữ cố định (tính bằng byte hoặc bit) và có cấu trúc mã hóa nhị phân riêng biệt.

(Để hiểu hơn về cơ chế quản lí biến của bộ nhớ và cách chúng được tạo ra thì mời bạn đọc 2.2. Biến)

Lưu ý:

  • Một biến kiểu số nguyên sẽ luôn chiếm một dung lượng bộ nhớ cố định đã được cấp phát. Do đó, việc lưu trữ giá trị 0 hay 1000 đều tiêu tốn một số lượng bit giống hệt nhau (Điều này cũng xảy ra tương tự với hầu hết các kiểu dữ liệu khác).

  • Kích thước bộ nhớ cấp phát cho cùng một kiểu dữ liệu có thể khác nhau tùy thuộc vào kiến trúc hệ điều hành (32-bit hay 64-bit) và quy chuẩn của trình biên dịch.

Đặc điểm của kiểu dữ liệu: Các kiểu dữ liệu thường có 2 đặc điểm chính:

Bao gồm một tập hợp các giá trị mà biến thuộc kiểu đó có thể nhận
  • Ví dụ: Kiểu dữ liệu short chỉ nhận giá trị tối đa là 32767, nếu bạn cố tình cộng thêm 1 thì kết quả cuối cùng xuất ra màn hình sẽ không phải số 32768 mà là -32768.
Loading...

Stdin:

Stdout:

Giải thích: Vì giá trị 32768 không nằm trong tập giá trị mà một biến kiểu short có thể nhận, các bit trong bộ nhớ bị tràn và quay vòng về giá trị nhỏ nhất là -32768. Điều này chứng minh dữ liệu bị ràng buộc chặt chẽ bởi kích thước vùng nhớ của kiểu dữ liệu.

Các phép toán có thể thực hiện
  • Ví dụ: Xét phép chia lấy phần dư (toán tử %). Phép toán này nằm trong tập phép toán hợp lệ của kiểu số nguyên, nhưng không tồn tại trong tập phép toán của kiểu số thực.
Loading...

Stdin:

Stdout:

Giải thích: Khi thấy 9.2 mang kiểu double, trình biên dịch sẽ tra cứu và thấy toán tử % không nằm trong danh sách phép toán hợp lệ, nên trình biên dịch báo lỗi ở dòng 5.

Các kiểu dữ liệu trong ngôn ngữ lập trình:

  • Kiểu nguyên thủy / Có sẵn: Các kiểu dữ liệu cơ bản (ví dụ: int, float, char, bool).

  • Kiểu do người dùng định nghĩa: Các kiểu dữ liệu phức hợp do lập trình viên tự xây dựng dựa trên sự kết hợp của các kiểu nguyên thủy (ví dụ: struct, class, enum).

2.1.2 Kiểu dữ liệu số nguyên​

Kiểu dữ liệu số nguyên được dùng để biểu diễn các giá trị nguyên (integer). Ký hiệu, kích thước và miền giá trị một số kiểu dữ liệu số nguyên được mô tả trong bảng sau:

Kiểu dữ liệuKích thước lưu trữ (bytes)Miền giá trị (Value range)
signed char1-128 → 127
signed short int2-32,768 → 32,767
signed int4-2,147,483,648 → 2,147,483,647
signed long4-2,147,483,648 → 2,147,483,647
signed long long int8-9,223,372,036,854,775,808 → 9,223,372,036,854,775,807
unsigned char10 → 255
unsigned short int20 → 65,535
unsigned int40 → 4,294,967,295
unsigned long int40 → 4,294,967,295
unsigned long long int80 → 18,446,744,073,709,551,615
ghi chú

Lưu ý: Trong C++ signed có nghĩa là có dấu và unsigned có nghĩa là không dấu.

  • Phạm vi các kiểu số nguyên nn bit có dấu: −2n−1→2n−1−1-2^{n-1} \to 2^{n-1} - 1.
  • Phạm vi các kiểu số nguyên nn bit không dấu: 0→2n−10 \to 2^n - 1
  • Ví dụ:
    • Áp dụng đối với kiểu unsigned int (1 byte = 8 bits -> n = 4 x 8 = 32), phạm vi giá trị thực nhận của kiểu dữ liệu này là [0, 232 - 1], tức là [0, 4,294,967,295].

Chương trình mô phỏng kiểu int:

Loading...

Stdin:

Stdout:

2.1.3 Kiểu dữ liệu số thực​

Kiểu dữ liệu số thực được dùng để biểu diễn các giá trị thực (real). Ký hiệu, kích thước và miền giá trị một số kiểu số thực được mô tả trong bảng sau:

Kiểu dữ liệuKích thước lưu trữ (bytes)Miền giá trị (Value range)Độ chính xác (Precision)
float4≈±3.4×1038\approx \pm 3.4 \times 10^{38}~6 - 9 chữ số
double8≈±1.7×10308\approx \pm 1.7 \times 10^{308}~15 - 17 chữ số
long double8, 12, hoặc 16 (*)≈±1.1×104932\approx \pm 1.1 \times 10^{4932} (**)≥18\ge 18 chữ số

Để thấy rõ sự khác biệt giữa float và double, bạn có thể chạy thử chương trình bên dưới:

Loading...

Stdin:

Stdout:

(Ghi chú: Khi chạy code, bạn sẽ thấy kết quả có chứa chữ e (ví dụ: 3.40282e+38). Đây là ký hiệu khoa học (Scientific notation), chữ e mang ý nghĩa là nhân với 1010 mũ. Tức là 3.40282×10383.40282 \times 10^{38}.)

tip

Thông thường khi lập trình chúng ta thường sử dụng double thay cho float khi thao tác trên kiểu số thực vì double mang lại độ chính xác cao hơn rất nhiều so với float, từ đó giúp bạn tránh được một số lỗi sai số và ảnh hưởng kết quả cuối cùng.

Lưu ý khi làm việc với số thực​

Không bao giờ so sánh bằng (==) hai số thực

Do máy tính sử dụng hệ nhị phân để lưu trữ, nó không thể biểu diễn chính xác hoàn toàn một số giá trị thập phân. Ví dụ: 0.1 + 0.2 == 0.3 trong C++ sẽ trả về kết quả là false.

Cách giải quyết: Chúng ta thường sẽ kiểm tra khoảng cách giữa hai số có nhỏ hơn một mức sai số vô cùng nhỏ (gọi là Epsilon) hay không.

Loading...

Stdin:

Stdout:

2.1.4 Kiểu luận lý/logic​

Kiểu dữ liệu bool (Boolean) trong C++ được sử dụng để biểu diễn các giá trị logic, thường được gọi là "đúng" (true) hoặc "sai" (false).

Kiểu dữ liệuKích thướcGiá trị
bool1 byte- false: giá trị 0
- true: giá trị khác 0

Ví dụ minh họa:

bool isPassed = true;  // Biến lưu giá trị logic đúng
bool hasError = false; // Biến lưu giá trị logic sai
Loading...

Stdin:

Stdout:

Lưu ý: Mặc dù 2 biến isPassed và hasError mang giá trị logic là true và false, nhưng khi sử dụng cout để in ra màn hình, C++ sẽ mặc định hiển thị chúng dưới dạng số nguyên (1 đại diện cho true và 0 đại diện cho false).

2.1.5 Kiểu dữ liệu void​

Kiểu dữ liệu void là một kiểu dữ liệu đặc biệt vì nó được dùng để biểu diễn cho dữ liệu không có giá trị cụ thể.

Kiểu dữ liệuKích thước lưu trữ (bytes)Miền giá trị (Value range)
void1
ghi chú

Trong C++, void là một kiểu dữ liệu không hoàn chỉnh (incomplete type). Điều này có nghĩa là trình biên dịch (compiler) biết về sự tồn tại của nó, nhưng không có thông tin về kích thước để cấp phát bộ nhớ. Do đó, việc khai báo một biến có kiểu void (ví dụ: void value;) là không hợp lệ và sẽ gây lỗi biên dịch.

Loading...

Stdin:

Stdout:

Kiểu dữ liệu void thường được sử dụng trong trường hợp sau:

    1. Một hàm số mà nó không trả về giá trị kết quả nào cả.
    1. Một hàm số mà tham số của nó không chấp nhận giá trị nào.
    1. Con trỏ dùng để lưu trữ địa chỉ của một vùng nhớ mà kiểu dữ liệu của vùng nhớ này chưa xác định.

Lưu ý:

  • Kích thước các kiểu dữ liệu có thể khác nhau tùy theo trình biên dịch.
  • Để biết kích thước của một kiểu dữ liệu (hoặc một biến), ta có thể sử dụng toán tử sizeof(tên_kiểu_dữ_liệu).
Loading...

Stdin:

Stdout:


2.2. Biến​

2.2.1. Khái niệm biến​

Biến là một khái niệm dùng để chỉ một vùng bộ nhớ được cấp phát nhằm mục đích lưu trữ giá trị (giá trị này có thể do người dùng nhập vào hoặc giá trị của một biểu thức; giá trị này có thể thay đổi trong quá trình thực hiện chương trình hoặc trong các lần chương trình được thực thi khác nhau) và được truy xuất thông qua một tên đã được khai báo cho biến đó.
Mỗi biến sẽ có một kiểu dữ liệu tương ứng với kiểu của các giá trị dữ liệu sẽ được lưu trữ.

Máy tính hiểu câu lệnh khai báo biến như nào?
  • Khi gặp lệnh khai báo, máy tính sẽ tìm và cấp phát một vùng nhớ trống có kích thước tương ứng với kiểu dữ liệu, "dán nhãn" vùng nhớ đó bằng tên biến để chương trình quản lý, và tiến hành ghi dữ liệu vào nếu có lệnh khởi tạo.

Ví dụ:

  • Đối với int x; (Khai báo nhưng chưa khởi tạo): Máy tính xin hệ điều hành cấp phát một vùng nhớ 4 bytes và đặt tên nó là x. Lúc này, bên trong ô nhớ x đang chứa một giá trị "rác" ngẫu nhiên.

  • Đối với int x = 2;(Khai báo kèm khởi tạo): Máy tính thực hiện cấp phát 4 bytes bộ nhớ, dán nhãn là x, và ngay lập tức ghi giá trị 2 (dưới dạng nhị phân) vào thẳng vùng nhớ vừa được cấp phát đó.

Loading...

Stdin:

Stdout:

Từ đoạn code trên có thể thấy với các biến không được khởi tạo giá trị thì giá trị in ra màn hình hoàn toàn là ngẫu nhiên và khó kiểm soát

2.2.2. Khai báo biến​

Cú pháp khai báo biến có dạng chung:

<Kiểu_dữ_liệu> tên_biến;

Trong đó:

  • <Kiểu_dữ_liệu> xác định kiểu của dữ liệu được lưu trữ, còn được gọi là kiểu dữ liệu của biến.
  • Tên_biến là một định danh được gán cho một vùng nhớ tương ứng. Ta có thể truy xuất dữ liệu được lưu trữ tại vùng nhớ đó qua tên biến.
  • Dấu ; dùng để xác định sự kết thúc của câu lệnh khai báo biến.

Ta có thể thực hiện khai báo biến và khởi tạo giá trị cho biến theo cú pháp:

<Kiểu_dữ_liệu> tên_biến = giá_trị_khởi_tạo;

Lưu ý: Giá trị của biến chính là giá trị được lưu trữ trong vùng nhớ dành cho biến đó.

Trong một dòng lệnh có thể khai báo nhiều biến có cùng kiểu bằng cách phân tách các biến đó bởi dấu phẩy , theo cú pháp như sau:

<Kiểu_dữ_liệu> tên_biến_1, tên_biến_2, tên_biến_3;

Lưu ý:

  • Luôn luôn khai báo biến trước khi sử dụng biến đó.
  • Nếu một biến X được sử dụng nhưng chưa được khai báo trong các dòng lệnh trước đó thì trình biên dịch thường báo lỗi: "error: 'X' was not declared in this scope"
Loading...

Stdin:

Stdout:

2.2.3. Quy tắc/ cách thức đặt tên biến​

Khi đặt tên biến cần lưu ý những vấn đề sau:

  • Không được sử dụng khoảng trắng (space) ở giữa các ký tự.
  • Không bắt đầu bằng chữ số.
  • Không trùng tên với các từ khóa hoặc tên hàm.
  • Ký tự đầu tiên là các chữ cái hoặc _.
  • Nên sử dụng tất cả chữ thường với dấu _ giữa các từ.

Ví dụ: Giả sử cần khai báo một biến để chứa giá trị diện tích của hình tròn. Khi đó:

  • Khai báo double Dien tich; là khai báo sai vì có khoảng trắng bên trong tên biến.
  • Khai báo double Dientich; hoặc double Dien_tich; là khai báo đúng.

Mở rộng: Bạn có thể tìm hiểu thêm các các quy ước đặt tên biến khác như: camelCase, PascalCase, và snake_case.

2.2.4. Phạm vi của biến​

Mỗi biến sẽ có một phạm vi tồn tại và hoạt động trong chương trình. Dựa trên phạm vi hoạt động, người ta thường chia các biến thành biến toàn cục (global) và biến cục bộ (local).

  • Biến toàn cục (Global Variable): Là biến được khai báo ở bên ngoài tất cả các khối lệnh {} (hoặc bên ngoài các hàm, lớp). Thời gian tồn tại và phạm vi hoạt động của nó tính từ vị trí khai báo cho đến khi kết thúc hoàn toàn chương trình.

  • Biến cục bộ (Local Variable): Là biến được khai báo bên trong một khối lệnh {} (ví dụ: bên trong hàm, hoặc lệnh if, for). Phạm vi sử dụng của biến chỉ nằm gọn bên trong khối lệnh đó. Thời gian tồn tại tính từ lúc biến được khai báo cho đến khi luồng thực thi chạm đến dấu ngoặc nhọn đóng } của khối lệnh.

    Loading...

    Stdin:

    Stdout:

    Lưu ý:

    Trong C++, phạm vi của biến cục bộ được xác định chặt chẽ bởi khối lệnh {} chứa nó. Nếu bạn có các khối lệnh lồng nhau, biến ở khối bên ngoài có thể được nhìn thấy ở khối bên trong, nhưng không có chiều ngược lại.

    Đặc biệt, nếu khối bên trong khai báo một biến trùng tên với biến ở khối bên ngoài, biến bên trong sẽ tạm thời che đi biến bên ngoài nên bạn không thể sử dụng lại các biến bên ngoài có trùng tên được nữa.

    Ví dụ minh họa:

    Loading...

    Stdin:

    Stdout:

    Giải thích: Trong đoạn code trên, mặc dù cả 3 biến đều tên là x, nhưng chúng là 3 vùng nhớ hoàn toàn độc lập, được quản lý dựa trên các phạm vi (khối lệnh) khác nhau:

    • Biến x toàn cục (dòng 4): Được khai báo bên ngoài mọi hàm. Nó có phạm vi toàn cục (Global Scope), tồn tại từ khi chương trình bắt đầu cho đến khi kết thúc. Tuy nhiên, chương trình sẽ không sử dụng biến này nếu bên trong hàm có một biến khác trùng tên.
    • Biến x của hàm main (dòng 6): Là biến cục bộ thuộc khối lệnh của hàm main(), chính vì vậy biến này tồn tại và hoạt động cho đến khi hàm main() kết thúc (chạm đến dấu } cuối cùng) ngoại trừ một số trường hợp đặc biệt.
    • Biến x trong khối lệnh con (dòng 9): Khối lệnh { ... } này nằm lồng bên trong hàm main(). Khi ta khai báo thêm một biến x ở đây, nó lại tiếp tục che khuất biến x của hàm main(). Biến này chỉ tồn tại bên trong cặp ngoặc nhọn này. Ngay khi luồng thực thi chạy đến dấu } đóng của khối lệnh, biến x = 30 này sẽ bị hủy đi và chương trình sẽ thấy trở lại biến x = 20 của hàm main()

    Mở rộng: Dù bị che khuất, bạn vẫn có thể gọi đến biến toàn cục ở dòng 4 bằng cách sử dụng toán tử phân giải phạm vi :: (Scope Resolution Operator) (như ở dòng lệnh số 18).

    Ví dụ:

    ...
    int x;
    {
    int y;
    {
    int z;
    }
    }
    ...

    Nếu x nằm ngoài tất cả các khối lệnh thì x sẽ được gọi là biến toàn cục

    Biến cục bộ có thể trùng tên với biến toàn cục. Khi đó, để truy xuất đến biến toàn cục ta sử dụng dấu 4 chấm :: để truy xuất.

Ví dụ:

Loading...

Stdin:

Stdout:

2.2.5. Một số loại biến thường gặp​

  • Biến thông thường (normal): là biến đơn giản, được dùng để lưu giá trị thông thường.
  • Biến con trỏ (pointer): là biến mà giá trị của nó là địa chỉ. Thông thường ta dùng biến này khi cần lưu trữ địa chỉ của một vùng nhớ.
  • Biến tham chiếu (reference): là biến nhưng nó không được trình biên dịch cấp phát ô nhớ riêng và phải sử dụng chung ô nhớ với biến được tham chiếu.

2.3. Hằng​

2.3.1. Khái niệm hằng​

Hằng là các đại lượng mà giá trị của nó không thay đổi trong quá trình thực hiện chương trình hoặc giữa các lần chương trình được thực thi khác nhau.

2.3.2. Khai báo hằng​

Cách 1: dùng chỉ thị tiền xử lý "preprocessor definition" theo cú pháp sau:

#define tên_hằng giá_trị_hằng 

Lưu ý: Tên hằng thông thường được khai báo bằng chữ in hoa.

Loading...

Stdin:

Stdout:

Cách 2: dùng biến hằng theo cú pháp sau:

const kiểu_dữ_liệu tên_biến = giá_trị_khởi_tạo ; 
Loading...

Stdin:

Stdout:

cảnh báo

Bạn không được thay đổi giá trị của một biến hằng xuyên suốt chương trình. Xét ví dụ sau:

Loading...

Stdin:

Stdout:

Lưu ý

define mang lại nhiều rủi ro tiềm ẩn hơn khai báo biến hằng bằng từ khóa const, sau đây là một vài lý do:

1. Bản chất của #define (Chỉ thị tiền xử lý)​

#define không tạo ra một biến thực sự trong bộ nhớ. Cơ chế hoạt động của define là Tìm và thay thế. Trước khi mã nguồn của bạn được biên dịch, chương trình sẽ quét toàn bộ file, tìm tất cả những chỗ có tên hằng và thay thế nó bằng giá trị tương ứng.

Lấy ví dụ mà chúng ta đã xét ở trên: #define PI 3.14;. Trước khi mã nguồn của các bạn được biên dịch, chương trình sẽ quét qua toàn bộ file và thay thế bất cứ biến nào tên là PI bằng giá trị 3.14.

Sự thay thế này dẫn đến 3 rủi ro lớn:

Rủi ro 1: Phá vỡ quy tắc về phạm vi (Scoping Rules)​

Các biến thông thường trong C++ chỉ tồn tại trong cặp ngoặc nhọn {} chứa nó (gọi là scope). Nhưng #define thì không hoạt động như vậy, một khi đã được khai báo, nó sẽ thay thế mọi từ khóa có trùng tên ở toàn bộ source code phía dưới

Loading...

Stdin:

Stdout:

Rủi ro 2: Gây khó khăn khi sửa lỗi​

Vì #define thực hiện thay thế văn bản trước khi chương trình chạy, nên trình biên dịch và công cụ gỡ lỗi (Debugger) không hề biết đến sự tồn tại của cái tên đó.

Nếu chương trình tính toán sai ở dòng có chứa gravity, debugger sẽ chỉ hiển thị con số 9.8. Bạn sẽ không thể theo dõi hay kiểm tra xem 9.8 đó là đại lượng nào, đến từ đâu trong hàng ngàn dòng code.

Rủi ro 3: Không kiểm tra được kiểu dữ liệu​

#define không quan tâm giá trị của bạn là số nguyên, số thực hay chuỗi. Mà chỉ thị tiền xử lí này chỉ bê nguyên văn bản đắp vào.

Lời khuyên cho bạn

Khuyến khích các bạn sử dụng từ khóa const để khai báo một biến hằng vì chúng an toàn hơn rất nhiều.


2.4. Các phép toán​

Lập trình là quá trình điều khiển dữ liệu thông qua các biểu thức. Một biểu thức biến đổi dữ liệu hoàn chỉnh thường bao gồm:

  • Biến: Nơi lưu trữ dữ liệu cần xử lý.
  • Giá trị: Các dữ liệu trực tiếp dùng trong tính toán (ví dụ: 1, 9, 3.14).
  • Toán tử (Operators): Công cụ thực hiện việc biến đổi dữ liệu. Kết quả của các toán tử là một dữ liệu mới và được gán vào biến.

Như vậy, quy trình xử lý dữ liệu thực chất là việc lấy giá trị từ biến, dùng toán tử để tính toán và gán lại kết quả mới vào biến để điều khiển dữ liệu. Trong phần này, ta sẽ tìm hiểu các loại toán tử phổ biến trong C++ và các ngôn ngữ lập trình khác.

2.4.1. Toán tử gán​

Toán tử gán = cho phép gán trực tiếp một giá trị vào một biến, hoặc gán giá trị từ biến này sang biến.

tên_biến = giá_trị;

Ví dụ:

int a = 5 // Gán biến cho biến a giá trị 5
int b = a // Gán cho biến b giá trị của biến a (5)

Chú ý:

  • Cơ chế ép kiểu ngầm định (Implicit Type Conversion hay Automatic Type Conversion) sẽ diễn ra khi bạn gán giá trị của kiểu dữ liệu này cho biến có kiểu dữ liệu khác.
Loading...

Stdin:

Stdout:

2.4.2. Toán tử số học​

Ký hiệu phép toánPhép toán
+Cộng
-Trừ
*Nhân
/Chia
%Lấy phần dư

Các phép toán tử thực hiện phép toán theo khác quy luật số học trên kiểu số nguyên, số thực

Kiểu trả về của các toán tử trên là kiểu lớn nhất trong các giá trị tham gia nếu không thực hiện ép kiểu.

Ví dụ:

Loading...

Stdin:

Stdout:

Toán tử chia lấy phần dư (%) chỉ được áp dụng trên các toán hạng có kiểu số nguyên. Nếu áp dụng trên kiểu dữ liệu khác thì trình biên dịch sẽ báo lỗi.

2.4.3. Phép toán tăng/ giảm một đơn vị​

Toán tử tăng (++) tăng 1 đơn vị cho toán hạng của nó, toán tử giảm (--) trừ 1 đơn vị cho toán hạng của nó.

Các toán tử tăng, giảm chỉ áp dụng cho toán hạng là các biến dạng số hoặc con trỏ.

Ví dụ:

  • ++x; tương đương x += 1; tương đương x = x+1;
  • Biểu thức (i+j)++ →\rightarrow không hợp lệ

Giả sử x là biến (toán hạng) sử dụng toán tử ++, --, ta có các dạng sau:

  • ++x : Đây được gọi là Prefix increment. Trong biểu thức chứa ++x, x sẽ tăng lên 1 đơn vị trước tiên, sau đó giá trị cập nhật sẽ được sử dụng.
  • x++ : Đây được gọi là Postfix increment. Trong biểu thức chứa x++, biến x sử dụng giá trị hiện tại của x (chưa tăng lên 1 đơn vị), tính xong biểu thức rồi cuối cùng mới tăng x lên 1 đơn vị.
  • --x : Đây được gọi là Prefix decrement. Trong biểu thức chứa --x, biến x sẽ giảm xuống 1 đơn vị trước, sau đó giá trị cập nhật sẽ được sử dụng.
  • x-- : Đây được gọi là Postfix decrement. Trong biểu thức chứa x--, biến x sử dụng giá trị hiện tại của x (chưa giảm 1 đơn vị), tính xong biểu thức rồi cuối cùng mới giảm x 1 đơn vị.

Ví dụ:

Loading...

Stdin:

Stdout:

2.4.4. Toán tử bit​

  • Toán tử bit (bitwise operators) trong C++ là các toán tử thao tác trực tiếp trên các bit của toán hạng (nguyên).
  • Gồm các toán tử: &(and), | (or), ^ (xor), ~ (not hay lấy số bù 1), >> (shift bits right: dịch phải), << (shift bits left: dịch trái)

Bảng chân trị các phép toán logic cơ bản:

pqp & q (AND)p ^ q (XOR)p | q (OR)~p (NOT)
000001
010111
111010
100110

Ví dụ:

  • 5 & 3 Kết quả: 1 (0101 & 0011 = 0001)
  • 5 | 3 Kết quả: 7 (0101 | 0011 = 0111)
  • 5 ^ 3 Kết quả: 6 (0101 ^ 0011 = 0110)
  • ~5 Kết quả: -6 (bit đảo ngược của 0101 là 1010, tương đương với -6)
  • 12 >> 3 = 1100 >> 3 = 0001 = 1 (toán tử dịch phải)
  • 3 << 2 = 0011 << 2 = 1100 = 12 (toán tử dịch trái)

2.4.5. Toán tử kết hợp​

Toán tửVí dụTương đươngÝ nghĩa
+=x += 5x = x + 5Cộng và gán
-=x -= 5x = x - 5Trừ và gán
*=x *= 5x = x * 5Nhân và gán
/=x /= 5x = x / 5Chia và gán
%=x %= 5x = x % 5Chia lấy dư và gán
<<=x <<= 5x = x << 5Dịch trái và gán
>>=x >>= 5x = x >> 5Dịch phải và gán
&=x &= 5x = x & 5Bitwise AND và gán
^=x ^= 5x = x ^ 5Bitwise XOR và gán
|=x |= 5x = x | 5Bitwise OR và gán

Lưu ý:

  • Các toán tử toán học cơ bản (+=, -=, *=, /=) dùng được cho cả số nguyên (int) và số thực (float, double).

  • Tuy nhiên, toán tử chia lấy dư (%=) và toàn bộ các toán tử thao tác bit (Bitwise: <<=, >>=, &=, ^=, |=) chỉ hoạt động với số nguyên. Nếu x là float hoặc double, trình biên dịch sẽ báo lỗi ngay lập tức.

Loading...

Stdin:

Stdout:

Pro Tip
  • Dịch trái (<<) - Nhân nhanh với lũy thừa 2:

    • Công thức: Biểu thức x << n có giá trị tương đương với phép toán x×2nx \times 2^n.
    • Mẹo nhớ: Dịch mũi tên sang trái là tăng giá trị lên (Phép nhân). Mỗi lần dịch 1 bit tương đương với việc nhân số đó cho 2.
    • Ví dụ: 5 << 2 →\rightarrow 5×22=205 \times 2^2 = 20.
  • Dịch phải (>>) - Chia lấy nguyên nhanh cho lũy thừa 2:

    • Công thức: Biểu thức x >> n có giá trị tương đương với phép toán ⌊x/2n⌋\lfloor x / 2^n \rfloor (chia lấy phần nguyên).
    • Mẹo nhớ: Dịch mũi tên sang phải là giảm giá trị xuống (Phép chia). Mỗi lần dịch 1 bit tương đương với việc chia nguyên số đó cho 2.
    • Ví dụ: 20 >> 2 →\rightarrow 20/22=520 / 2^2 = 5.

2.4.6. Toán tử điều kiện - Conditional Ternary Operator​

Toán tử điều kiện (hay toán tử ba ngôi) là một toán tử được áp dụng để xác định biểu thức thứ nhất hay thứ hai được thực hiện tùy theo giá trị đúng hay sai của một điều kiện. Cú pháp:

<điều kiện> ? biểu_thức_1> : <biểu_thức_2> 

Biểu thức 1 được thực thi khi điều kiện đúng và ngược lại, khi điều kiện sai thì biểu thức 2 sẽ được thực thi

Ví dụ: Giữa hai số a,b hãy tìm số nào lớn hơn.

a > b ? max = a : min = b
Loading...

Stdin:

Stdout:

2.4.7. Toán tử quan hệ - Relational operators​

OperatorToán tửKý hiệuVí dụGiải thích
Greater thanLớn hơn>x > yNếu x lớn hơn y → true (1)
Ngược lại → false (0)
Less thanNhỏ hơn<x < yNếu x nhỏ hơn y → true (1)
Ngược lại → false (0)
Greater than or Equal toLớn hơn hoặc bằng>=x >= yNếu x lớn hơn hoặc bằng y → true (1)
Ngược lại → false (0)
Less than or Equal toNhỏ hơn hoặc bằng<=x <= yNếu x nhỏ hơn hoặc bằng y → true (1)
Ngược lại → false (0)
Equal toBằng==x == yNếu x bằng y → true (1)
Ngược lại → false (0)
Not equal toKhác!=x != yNếu x khác y → true (1)
Ngược lại → false (0)

Ví dụ:

(7 == 5) // evaluates to false
(5 > 4) // evaluates to true

2.5.8. Toán tử luận lý - Logical operators​

Toán tửKý hiệuVí dụGiải thích
NOT!!XToán tử một ngôi, thể hiện điều kiện "phủ định (not) X".
AND&&X && YToán tử hai ngôi, thể hiện điều kiện "X và (and) Y".
OR||X || YToán tử hai ngôi, thể hiện điều kiện "X hoặc (or) Y".

Ví dụ:

XY!XX && YX || Y
True (nonzero)False (0)
False (0)True (1)
True (nonzero)True (nonzero)True (1)True (1)
True (nonzero)False (0)False (0)True (1)
False (0)True (nonzero)False (0)True (1)
False (0)False (0)False (0)False (0)

Phân biệt giữa toán tử luận lý và toán tử bit

Đặc điểmToán tử Luận lý (Logical)Toán tử Bit (Bitwise)
Ký hiệu&&, ||, !&, |, ^, ~, <<, >>
Đối tượngBiểu thức Đúng/Sai (Boolean).Các bit nhị phân của số nguyên.
Kết quảLuôn là 0 (False) hoặc 1 (True).Một giá trị số nguyên mới.
Cơ chếKiểm tra toàn bộ giá trị (0 hoặc khác 0).Thao tác trên từng cặp bit tương ứng.

Ví dụ:

Loading...

Stdin:

Stdout:


2.5. Biểu thức và độ ưu tiên toán tử​

2.5.1. Biểu thức​

Định nghĩa: Biểu thức được tạo thành được từ các toán hạng (Operand) (hằng số, biến số và hàm số) liên kết với nhau bằng các toán tử (Operator). Trong đó:

  • Toán tử tác động lên các giá trị của toán hạng và cho giá trị có kiểu nhất định.
  • Toán hạng: hằng, biến, lời gọi hàm...

Giá trị của biểu thức là giá trị kết quả từ các toán tử tác động lên các toán hạng tương ứng.

Loading...

Stdin:

Stdout:

2.5.2. Độ ưu tiên toán tử​

MứcToán tửMô tảThứ tự thực hiện
1::Phân giải phạm vi (Scope resolution)
2++ --
() []
. ->
Hậu tố (Postfix increment/decrement)
Gọi hàm, truy cập mảng
Truy cập thành viên (biến/con trỏ)
Trái sang Phải
3++ --
+ -
! ~
(type)
* &
sizeof
new delete
Tiền tố (Prefix increment/decrement)
Cộng/Trừ nhất phân (Unary plus/minus)
Logic NOT, Bitwise NOT
Ép kiểu (C-style cast)
Giải tham chiếu (Indirection), Lấy địa chỉ
Kích thước kiểu dữ liệu
Cấp phát/Giải phóng bộ nhớ
Phải sang Trái
4.* ->*Con trỏ tới thành viên (Pointer to member)Trái sang Phải
5* / %Nhân, Chia, Chia lấy dưTrái sang Phải
6+ -Cộng, TrừTrái sang Phải
7<< >>Dịch bit trái/phảiTrái sang Phải
8< <= > >=Các toán tử quan hệ (So sánh)Trái sang Phải
9== !=So sánh bằng và khácTrái sang Phải
10&Bitwise ANDTrái sang Phải
11^Bitwise XORTrái sang Phải
12|Bitwise ORTrái sang Phải
13&&Logic ANDTrái sang Phải
14||Logic ORTrái sang Phải
15?:Toán tử điều kiện (Ternary)Phải sang Trái
16=
+= -= *= /= %=
<<= >>=
&= ^= |=
Gán trực tiếp
Gán kết hợp số học
Gán dịch bit
Gán toán tử bit
Phải sang Trái
17throwNém ngoại lệ (Exceptions)Phải sang Trái
18,Toán tử phẩyTrái sang Phải

Một biểu thức có nhiều phép toán, thì quy tắc thực hiện như sau:

  • Thực hiện biểu thức trong ( ) sâu nhất trước.
  • Thực hiện theo thứ tự ưu tiên các toán tử.
  • Khi một biểu thức có hai toán tử có cùng mức độ ưu tiên: thực hiện phép toán từ trái sang phải hoặc từ phải sang trái sẽ theo quy ước chung của nhóm phép toán đó.
  • Việc đặt tất cả các câu lệnh phụ trong dấu ngoặc đơn (ngay cả những câu lệnh không cần thiết vì mức độ ưu tiên của chúng) sẽ cải thiện khả năng đọc mã.

Ví dụ: Tính biểu thức:

int a = 5, b = 10, c = 15;  
bool result = (a + b * c > b) && (c / a == b % c) || (a - b < c);

Ta có:

  • (a + b * c > b) = ((a + (b * c)) > b) = ((5+(10*15)) > 10) = true
  • (c / a == b % c) = ((c / a) == (b % c)) = (15 / 5 == 10 % 15) = (3 == 10) = false
  • (a - b < c) = ((a – b) < c) = (5 - 10 < 15) = true

⇒\Rightarrow result = true && false || true (&& ưu tiên hơn ||)
⇒\Rightarrow result = (true && false) || true
⇒\Rightarrow result = true

Một sai lầm khá phổ biến khi nói về mức độ ưu tiên toán tử đối với toán tử ba ngôi

Các ví dụ

Sự khác biệt giữa hậu tố và tiền tố​

Loading...

Stdin:

Stdout:

Ví dụ ban đầu​

Loading...

Stdin:

Stdout:

Toán tử gán​

Loading...

Stdin:

Stdout:

Các lỗi thường gặp

Ví dụ 1:

Loading...

Stdin:

Stdout:

Giải thích

Trong C++, toán tử xuất << nằm ở Mức 7, tức là ưu tiên cao hơn so với toán tử ba ngôi ?: nằm ở Mức 15.

Thay vì hiểu theo ý người viết là: cout << (toàn_bộ_kết_quả_ba_ngôi) Máy tính sẽ tự động nhóm lệnh lại từ trái sang phải như thế này: (cout << (a < b)) ? (a - b) : (a + b);

Các bước máy tính thực hiện:

Tính biểu thức (a < b): 5 < 10 là đúng, trả về true (dưới dạng số 1).

Thực hiện lệnh cout << 1; Số 1 bị in ngay lập tức ra màn hình.

Sau đó, nó mới chạy nốt phần còn lại của toán tử 3 ngôi. Nó lấy kết quả của (a - b) là -5 làm kết quả cuối cùng. Nhưng vì không có lệnh gán (=) hay lệnh in (cout) nào nhận lấy kết quả này, nên con số -5 này sẽ bị dọn dẹp.

Cách khắc phục
Loading...

Stdin:

Stdout:

Ví dụ 2

Loading...

Stdin:

Stdout:


Tài liệu tham khảo

  1. UIT - Các kiểu dữ liệu cơ bản
  2. UIT - Các phép toán trong C++
  3. UIT - Nhập xuất trong C++
  4. learncpp.com
  5. geeksforgeeks.org
  6. The C++ Programming Language
  7. Introduction to Namespace
  8. Manipulators in C++
  9. The Differences between Float & Double in C/C++
  10. Data Type Void in C++