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:
- Phát triển Game (Game Engines) và các ứng dụng đồ họa nặng.
- Hệ thống thời gian thực (Real-time systems) và phần mềm nhúng (Embedded software).
- Lõi tính toán cho Trí tuệ nhân tạo (AI) và Mạng nơ-ron (Neural networks).
- 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ữ
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 */ - Ký tự
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
- Kí tự
// Day la chu thich 1
// Day la chu thich 2
Stdin:
Stdout:
Khi biên dịch source code thì các phần comments sẽ không được biên dịch
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:
-
- 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.
-
- 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.
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).
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àmmain.
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ệnhreturn 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àmmain.
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
maxcó 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ự).
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.
-
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)
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
xvới kiểu dữ liệu làint. Từ thời điểm này trở đi, mỗi khi thấyxtrong 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ập | cin >>cin.get, cin.getline,getline (<string>) | scanffgetc, getc, getchar, fgets |
| Lệnh xuất | cout << | printfputchar, 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.
Để 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
Stdin:
Stdout:
1.4.3. Câu lệnh xuất: std::cout
std::coutlà 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ệniostream.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:
hoặc
#include <iostream>
...
std:: cout << Tham_số_1 << Tham_số_2 << ... << Tham_số_k;#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ụ\ndùng để xuống dòng,\tcùng để cách vào một khoảng tab
- Ngoài
\n,endllà một đối tượng đặc biệt giúp bạn có thể xuống dòng khi sử dụngcout, bạn sẽ thêm đối tượng này trong câu lệnhcouttại vị trí bạn muốn xuống dòng.
Ví dụ 1:
Stdin:
Stdout:
Ví dụ 2:
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:
-
- Thêm thư viện
<iomanip>ở đầu chương trình.
- Thêm thư viện
-
- Thêm cụm
<< std::fixed << std::setprecision(k) << số_cần_làm_tròn;
- Thêm cụm
Ví dụ:
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.
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:
Stdin:
Stdout:
1.4.4. Câu lệnh nhập: std::cin
std::cinlà 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::cinbạ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:
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::cincó thể nhập được các kiểu dữ liệu cơ bảnint,long long,short,float,double,char,bool,std::string- Khi nhập số (
int,double,...)std::cinsẽ 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ớitrue(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
boolgiá trị mặc định là 0 và không báo lỗi. Đó là lý cho khi bạn nhậptrue(hayfalse) vào một biếnbool, giá trị biến sẽ là 0. Dotruevàfalselà một chuỗi không hợp lệ vớibool. - Nếu muốn chương trình hiểu
truevàfalsekhông phải một dữ liệu kiểu chuỗi, có thể dùng lệnhstd::cin >> std::boolalpha >> bien_boolđể nhập dữ liệu chobien_booldưới dạngtruehoặcfalse. 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ịboolvẫn sẽ là0.std::boolalphachỉ cho biếnboolnhận đúng giá trị khi nhậptruehoặcfalse.
- 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.
- Khi nhập số (
- 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
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:
std::cin >> bien_1 >> bien_2 >> ... >> bien_k;- 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
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()
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àmgetlineđượ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àmgetlinesẽ đọ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)?
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
19và 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ố19gán cho biếntuoi. -
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ự\nvà 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ếnhoTenvà 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.
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ểm | cin >> | 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) | Unformatted | Unformatted | Unformatted |
| Xử lý khoảng trắng đầu | Bỏ qua | Không bỏ qua | Không bỏ qua | Khô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ộ đệm | Giữ 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ự
\nvà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.
-
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::endltrong 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\ngiú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 | \n | std::endl |
|---|---|---|
| Vai trò | Ký tự xuống dòng | Lệnh xuống dòng + Ép xuất dữ liệu |
| Hành động thêm | Không làm gì thêm | Gọi hàm std::flush |
| Hiệu suất | Cao (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:
-
- 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:
-
- 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
.cppthành các file object (thường có đuôi.ohoặ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àmmyFcn()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).
- 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
-
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:
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::cinstd::coutstd::endlstd::fixedstd::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.
Stdin:
Stdout:
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.
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à:
ifstreamlà class để đọc dữ liệu đầu vào từ file
Ví dụ khai báo một biến kiểuifstreamđể đọc dữ liệu từ một file có tên là input.txt:ifstream ip("input.txt");ofstreamlà 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ếnofstreamđể mở file:ofstream op;fstreamlà class để đọc hoặc ghi dữ liệu. Ta có thể thay thế 2 từ khóa trên bằng từ khóafstream
Đọc ghi file thường có các chế độ (mode) định dạng đi kèm như sau:
ios::indùng để Mở cho các hoạt động đầu vào. (mode mặc định củaifstream)ios::outdùng để mở cho các hoạt động đầu ra. (mode mặc định củaofstream)ios::binarydùng để mở file nhị phân.ios::atelà đặt vị trí con trỏ ở cuối file khi chúng ta mở file.ios::applà khi file đã có sẵn data thì dữ liệu sẽ được thêm vào cuối file.ios::trunclà 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);
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:
/*...*/
- Chú thích một 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,enumclass,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
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.2thuộc kiểu số thực (floating-point) và giá trị5thuộ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
shortchỉ 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.
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.
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ệu | Kích thước lưu trữ (bytes) | Miền giá trị (Value range) |
|---|---|---|
signed char | 1 | -128 → 127 |
signed short int | 2 | -32,768 → 32,767 |
signed int | 4 | -2,147,483,648 → 2,147,483,647 |
signed long | 4 | -2,147,483,648 → 2,147,483,647 |
signed long long int | 8 | -9,223,372,036,854,775,808 → 9,223,372,036,854,775,807 |
unsigned char | 1 | 0 → 255 |
unsigned short int | 2 | 0 → 65,535 |
unsigned int | 4 | 0 → 4,294,967,295 |
unsigned long int | 4 | 0 → 4,294,967,295 |
unsigned long long int | 8 | 0 → 18,446,744,073,709,551,615 |
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 bit có dấu: .
- Phạm vi các kiểu số nguyên bit không dấu:
- 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].
- Áp dụng đối với kiểu
Chương trình mô phỏng kiểu int:
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ệu | Kích thước lưu trữ (bytes) | Miền giá trị (Value range) | Độ chính xác (Precision) |
|---|---|---|---|
float | 4 | ~6 - 9 chữ số | |
double | 8 | ~15 - 17 chữ số | |
long double | 8, 12, hoặc 16 (*) | (**) | 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:
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 mũ. Tức là .)
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
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.
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ệu | Kích thước | Giá trị |
|---|---|---|
bool | 1 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
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ệu | Kích thước lưu trữ (bytes) | Miền giá trị (Value range) |
|---|---|---|
void | 1 |
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.
Stdin:
Stdout:
Kiểu dữ liệu void thường được sử dụng trong trường hợp sau:
-
- Một hàm số mà nó không trả về giá trị kết quả nào cả.
-
- Một hàm số mà tham số của nó không chấp nhận giá trị nào.
-
- 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).
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 đó.
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ếnlà 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"
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ặcdouble 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ệnhif,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
xtoà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
xcủa hàmmain(dòng 6): Là biến cục bộ thuộc khối lệnh của hàmmain(), chính vì vậy biến này tồn tại và hoạt động cho đến khi hàmmain()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
xtrong khối lệnh con (dòng 9): Khối lệnh{ ... }này nằm lồng bên trong hàmmain(). Khi ta khai báo thêm một biếnxở đây, nó lại tiếp tục che khuất biếnxcủa hàmmain(). 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ếnx = 30này sẽ bị hủy đi và chương trình sẽ thấy trở lại biếnx = 20của hàmmain()
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
xnằm ngoài tất cả các khối lệnh thìxsẽ được gọi là biến toàn cụcBiế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. - Biến
Ví dụ:
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ằngLưu ý: Tên hằng thông thường được khai báo bằng chữ in hoa.
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 ;
Stdin:
Stdout:
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:
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
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.
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.
Stdin:
Stdout:
2.4.2. Toán tử số học
| Ký hiệu phép toán | Phé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ụ:
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 đươngx += 1;tương đươngx = x+1;- Biểu thức
(i+j)++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,xsẽ 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ứax++, biếnxsử dụng giá trị hiện tại củax(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ăngxlên 1 đơn vị.--x: Đây được gọi là Prefix decrement. Trong biểu thức chứa--x, biếnxsẽ 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ứax--, biếnxsử dụng giá trị hiện tại củax(chưa giảm 1 đơn vị), tính xong biểu thức rồi cuối cùng mới giảmx1 đơn vị.
Ví dụ:
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:
| p | q | p & q (AND) | p ^ q (XOR) | p | q (OR) | ~p (NOT) |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 1 |
| 0 | 1 | 0 | 1 | 1 | 1 |
| 1 | 1 | 1 | 0 | 1 | 0 |
| 1 | 0 | 0 | 1 | 1 | 0 |
Ví dụ:
5 & 3Kết quả: 1 (0101 & 0011 = 0001)5 | 3Kết quả: 7 (0101 | 0011 = 0111)5 ^ 3Kết quả: 6 (0101 ^ 0011 = 0110)~5Kế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 += 5 | x = x + 5 | Cộng và gán |
-= | x -= 5 | x = x - 5 | Trừ và gán |
*= | x *= 5 | x = x * 5 | Nhân và gán |
/= | x /= 5 | x = x / 5 | Chia và gán |
%= | x %= 5 | x = x % 5 | Chia lấy dư và gán |
<<= | x <<= 5 | x = x << 5 | Dịch trái và gán |
>>= | x >>= 5 | x = x >> 5 | Dịch phải và gán |
&= | x &= 5 | x = x & 5 | Bitwise AND và gán |
^= | x ^= 5 | x = x ^ 5 | Bitwise XOR và gán |
|= | x |= 5 | x = x | 5 | Bitwise 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àfloathoặcdouble, trình biên dịch sẽ báo lỗi ngay lập tức.
Stdin:
Stdout:
-
Dịch trái (
<<) - Nhân nhanh với lũy thừa 2:- Công thức: Biểu thức
x << ncó giá trị tương đương với phép toá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.
- Công thức: Biểu thức
-
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 >> ncó giá trị tương đương với phép toán (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.
- Công thức: Biểu thức
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
Stdin:
Stdout:
2.4.7. Toán tử quan hệ - Relational operators
| Operator | Toán tử | Ký hiệu | Ví dụ | Giải thích |
|---|---|---|---|---|
| Greater than | Lớn hơn | > | x > y | Nếu x lớn hơn y → true (1)Ngược lại → false (0) |
| Less than | Nhỏ hơn | < | x < y | Nếu x nhỏ hơn y → true (1)Ngược lại → false (0) |
| Greater than or Equal to | Lớn hơn hoặc bằng | >= | x >= y | Nếu x lớn hơn hoặc bằng y → true (1)Ngược lại → false (0) |
| Less than or Equal to | Nhỏ hơn hoặc bằng | <= | x <= y | Nếu x nhỏ hơn hoặc bằng y → true (1)Ngược lại → false (0) |
| Equal to | Bằng | == | x == y | Nếu x bằng y → true (1)Ngược lại → false (0) |
| Not equal to | Khác | != | x != y | Nế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ệu | Ví dụ | Giải thích |
|---|---|---|---|
| NOT | ! | !X | Toán tử một ngôi, thể hiện điều kiện "phủ định (not) X". |
| AND | && | X && Y | Toán tử hai ngôi, thể hiện điều kiện "X và (and) Y". |
| OR | || | X || Y | Toán tử hai ngôi, thể hiện điều kiện "X hoặc (or) Y". |
Ví dụ:
X Y !X X && Y X || 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ểm | Toán tử Luận lý (Logical) | Toán tử Bit (Bitwise) |
|---|---|---|
| Ký hiệu | &&, ||, ! | &, |, ^, ~, <<, >> |
| Đối tượng | Biể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ụ:
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.
Stdin:
Stdout:
2.5.2. Độ ưu tiên toán tử
| Mức | Toá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)* &sizeofnew 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ải | Trá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ác | Trái sang Phải |
| 10 | & | Bitwise AND | Trái sang Phải |
| 11 | ^ | Bitwise XOR | Trái sang Phải |
| 12 | | | Bitwise OR | Trái sang Phải |
| 13 | && | Logic AND | Trái sang Phải |
| 14 | || | Logic OR | Trá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 |
| 17 | throw | Ném ngoại lệ (Exceptions) | Phải sang Trái |
| 18 | , | Toán tử phẩy | Trá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
result = true && false || true(&&ưu tiên hơn||)
result = (true && false) || true
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ụ
Các lỗi thường gặp
Ví dụ 1:
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
Stdin:
Stdout:
Ví dụ 2
Stdin:
Stdout: