You are on page 1of 32

CHINH PHỤC

LẬP TRÌNH C
LÊ NHẬT THANH

First editor 2019

Bản quyền thuộc về lenhatthanh.com và coderdocs.info


Lời mở đầu
Chào mừng các bạn đến với thế giới lập trình viên. Mình là Thanh, hiện tại đang là một software
engineer. Ngày trước mình từng là một embedded developer, và bây giờ mình đang code PHP và JS.
Sau nhiều năm học tập và làm việc, mình nhận ra một điều, nền tảng lập trình và những kiến thức cơ bản
trong trường đại học dạy bạn, là những thứ tối quan trọng, là bước đệm trong con đường sự nghiệp.
Mình chọn ngôn ngữ C làm ngôn ngữ bắt đầu, vì C là một trong những ngôn ngữ cực kì hay, bạn sẽ hiểu
cách quản lý bộ nhớ, cách phần cứng hoạt động như thế nào, kiến trúc máy tính, hệ điều hành, ... . Bạn
hiểu được làm thế nào để xữ lý dữ liệu ở mức độ binary, một trong những kiến thực cực kì hay và nền
tảng cho lập trình viên.
Và nếu bạn là người đã chinh phục được C (và C++) thì những ngôn ngữ cấp cao, PHP, JS, Java, C#, ... sẽ
rất dễ dàng để tiếp cận và học sau này.
Trong ebook này, mình muốn share cho các bạn 2 phần là pointer và digital C và C++. Mình cực kì tâm
đắc với 2 phần này. Và yêu cầu để học ebook này chính là bạn phải có học biết qua lập trình C từ trước,
các kiến thức cơ bản về biến, vòng lặp, câu điều kiện, mảng, cấp phát động, static, .... Hãy chuẩn bị cho
mình những kiến thức trước khi bước vào ebook này!
Trong quá trình đọc ebook, nếu bạn gặp những khó khăn hay điều gì, bạn có thể để liên lạc mình thông
qua: facebook.com/coderdocs hoặc website: lenhatthanh.com.
Nếu có bất cứ điều gì sai sót, hay bạn muốn góp ý cũng hãy liên lạc với mình. Vì đây là phiên bản đầu
tiên nên không thể tránh sai sót.
Trân trọng,
Lê Nhật Thanh
NỘI DUNG EBOOK

CHƯƠNG 1 - NỀN TẢNG LÀ QUAN TRỌNG NHẤT ......................................................................... 1


CHƯƠNG 2 - TẠI SAO LẠI LÀ C.......................................................................................................... 2
CHƯƠNG 3 - CON TRỎ (POINTER) ..................................................................................................... 3
1. Con trỏ là gì?................................................................................................................................. 3
2. Chúng ta đi sâu hơn về bộ nhớ máy tính (memory) để hiểu khái niệm con trỏ. ........................... 3
3. Khai báo con trỏ ............................................................................................................................ 5
4. Khởi tạo giá trị cho biến con trỏ ................................................................................................... 5
5. Kiểu dữ liệu của con trỏ ................................................................................................................ 6
6. Các phép toán trên con trỏ ............................................................................................................ 7
6.1 Phép gán ................................................................................................................................ 7
6.2 Phép so sánh.......................................................................................................................... 8
6.3 Phép cộng trừ và phép tăng giảm .......................................................................................... 8
7. Một số ví dụ hay và đương nhiên, khó! ........................................................................................ 9
8. Constant pointer .......................................................................................................................... 10
9. Pointer to constant....................................................................................................................... 10
10. Mảng các con trỏ (Array of pointers)...................................................................................... 11
11. Con trỏ trỏ tới một con trỏ (pointer to pointer) ....................................................................... 12
12. Function trả về kiểu con trỏ .................................................................................................... 13
13. Con trỏ trỏ tới hàm (function pointer)..................................................................................... 15
CHƯƠNG 4 - DIGITAL ......................................................................................................................... 17
1. Các hệ thống số đếm ................................................................................................................... 17
1.1 Cách máy tính lưu mã nhị phân. ......................................................................................... 17
1.2 Số bù một (one’s complement) ........................................................................................... 17
1.3 Số bù 2 (two’s complement) ............................................................................................... 17
1.4 signed và unsigned trong C/C++......................................................................................... 17
2. Các phép toán xử lý bit ............................................................................................................... 18
2.1 Phép OR bit, ký hiệu “|” ..................................................................................................... 18
2.2 Phép toán AND, ký hiệu “&” ............................................................................................ 19
2.3 Phép XOR bit, ký hiệu “^”................................................................................................. 19
2.4 Phép NOT bit, ký hiệu “~” ................................................................................................ 20
3. Các phép toán logic..................................................................................................................... 20
3.1 Phép logic OR, ký hiệu “||” ................................................................................................ 20
3.2 Phép logic AND, ký hiệu “&&” ........................................................................................ 21
3.3 Toán tử logic NOT, ký hiệu “!” ......................................................................................... 22
4. Toán tử dịch bit ........................................................................................................................... 22
4.1 Toán tử dịch trái, ký hiệu “<<“ .......................................................................................... 22
4.2 Toán tử dịch phải, ký hiệu “>>” ......................................................................................... 23
5. Các phép toán phối với nhau....................................................................................................... 24
6. Dấu ngoặc chết chóc và “đừng bao giờ thử thách trí nhớ của mình với ngôn ngữ C”................ 25
CHƯƠNG 5 – KẾT ................................................................................................................................ 27
TÀI LIỆU THAM KHẢO ...................................................................................................................... 28
Chương 1 – Nền tảng là quan trọng nhất Chinh phục ngôn ngữ C

CHƯƠNG 1 - NỀN TẢNG LÀ QUAN TRỌNG NHẤT


Chúng ta là một con người, bất cứ thứ gì từ lúc sinh ra cho tới lúc trưởng thành như bây giờ đều là quá
trình đi từng bước (step by step). Ví dụ như em bé muốn tập đi thì ban đầu phải tập ngồi, tập bò, tập đứng,
rồi sau đó mới tập đi được. Người xưa cũng luôn có câu “dục tốc bất đạt”. Chính xác là như vậy, trong thế
giới lập trình, cái quan trọng nhất của một người muốn theo con đường này lâu dài, hay muốn trở thành
một lập trình viên tốt, là nền tảng. Khi bạn xây nhà, bạn cần phải làm móng thật vững chắc, thì ngôi nhà
bạn được xây lên cũng sẽ vững chắc theo. Lập trình hay bất kì ngành nghề nào cũng vậy, cái quan trọng
nhất là nền tảng.
Vậy nền tảng ở trong thế giới IT này là gì? Câu trả lời chính là, tư duy lập trình và những kiến thức căn
bản về IT.
Tư duy lập trình được hiểu rất đơn giản, cực kì đơn giản như sau: Khi cho bạn một bài toán (sau này khi
đi làm, chúng ta sẽ gọi nó là task, hay lớn hơn là project), bạn có thể tìm ra solution và giải quyết nó trên
những dòng code. Cách tìm solution ở đây chính là phân tích bài toán, chia nhỏ bài toán ra và xem xét tìm
những phương án thích hợp để giải quyết nó. Bạn có thể rèn luyện tư duy lập trình từ các bài rất rất nhỏ
mà bạn được học trong trường, ví dụ giải phương trình, tìm số nguyên số, tính tổng các số từ 1 đến N
chẳng hạn. Từ từ bạn sẽ rèn luyện được tư duy lập trình, tư duy logic với các bài thực hành từ nhỏ tới lớn.
Lớn hơn bạn có thể viết cái chương trình, application, hay project nào đó. Bạn sẽ học được rất nhiều khi
làm một project thực tế, nhỏ cũng được. Và cuối cùng, chìa khóa ở đây chính là học và luyện tập.
Còn những kiến thức căn bản ở đây là gì? Không biết bạn sẽ chọn đi hướng nào để đi trong thế giới IT,
nhưng những kiến thức sau bạn bắt buộc phải có: cơ sở dữ liệu, cấu trúc dữ liệu và giải thuật, lập trình
hướng đối tượng, mạng máy tính, kiến trúc máy tính. Và những kiến thức này bạn sẽ được học trong
trường đại học. Những kiến thức này rất quan trọng, bạn cần tập trung cho những môn này khi vào đại
học: ví dụ như nhập môn lập trình, lập trình C/C++, cấu trúc dữ liệu và giải thuật, lập trình hướng đối
tượng, ... Còn nếu bạn đang không học đại học, bạn có thể học các khóa học online trên internet, có rất
nhiều khóa học như vậy.
Điểm quan trọng cuối cùng, tập trung học ngay từ khi bạn đang còn ở trong trường đại học. Và một lời
khuyên khác, nếu bạn không bận tâm nhiều đến chuyện tiền bạc, bạn không nên đi làm thêm, thay vì đó
hãy tập trung học hoặc kiếm một công ty để thực tập và làm part-time đúng chuyên ngành. Đó là một lời
khuyên chân thành của một người từng đi làm thêm rất nhiều và cảm thấy tiếc rất nhiều về khoảng thời
gian đó thay vì dùng để luyện tập kĩ năng lập trình.

1
Chương 2 – Tại sao lại là C Chinh phục ngôn ngữ C

CHƯƠNG 2 - TẠI SAO LẠI LÀ C


Bạn có nhận ra một điều, dường như trong tất cả các trường đại học, C (và C++) là thứ bạn sẽ luôn tiếp
cận đầu tiên khi bước vào thế giới lập trình. Bạn có bao giờ tự hỏi là tại sao như vậy?
C là ngôn ngữ hướng cấu trúc gần với phần cứng nhất, giao tiếp trực tiếp với phần cứng. Khi bạn học C
và chinh phục nó, bạn sẽ hiểu rõ hơn về phần cứng, cách phần cứng hoạt động, giao tiếp lẫn nhau, cách
quản lý bộ nhớ, tóm lại là bạn sẽ hiểu rõ hơn về phần hệ thống và kiến trúc máy tính.
Hiện nay các ngôn ngữ cấp cao, hay các thư viện, thường được xây dựng dựa trên ngôn ngữ C, hay ngay
cả hệ điều hành Linux cũng được xây dựng bằng ngôn ngữ C. Khi bạn nắm được ngôn ngữ C, sẽ là một
lợi thế cực kì lớn, một nền tảng rất vững chắc cho bạn khi học các ngôn ngữ cấp cao khác. Mình luôn
quan niệm một điều: “Nắm chắc được C/C++ và OOP, việc học các ngôn ngữ khác là cực kì đơn giản”.
Nhưng để chinh phục được C (hay C++) không phải là chuyện đơn giản và một sớm một chiều. Bạn cần
phải học tập và rèn luyện nhiều, bởi vì C/C++ là 2 ngôn ngữ thuộc top những ngôn ngữ phổ biến khó học
nhất trên thế giới.
Yêu cầu của ebook này:
Bạn phải là người đã biết về C hoặc C++ ở mức cơ bản. Nghĩa là các kiến thức như variable,
function, struct, if, for, while, ... Và đặc biệt là đã từng nghe, tiếp xúc hay học về con trỏ
(pointer ) hay digital .

2
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

CHƯƠNG 3 - CON TRỎ (POINTER)


Chắc hẳn nếu ai đã từng lập trình C/C++ thì đã từng nghe qua về con trỏ (hay pointer). Đây có thể nói là
một trong những phần khó nhất trong C/C++. Đại khái là nó khó hiểu và dễ quên nữa. Vậy làm cách nào
để bạn biết và hiểu về con trỏ, làm thế nào để bạn có thể hoàn thành tốt các kì phỏng vấn cho một kĩ sư
lập trình nhúng (hay bất kì chuyên ngành nào đụng tới C/C++), mà khi người ta hỏi tới C/C++, thì 90% là
hỏi về con trỏ.

1. Con trỏ là gì?


Câu trả lời, con trỏ là một biến bình thường, nó chứa một giá trị trong đó. Vậy giá trị nó lưu trong đó là
gì? Câu trả lời là địa chỉ của một ô nhớ. Ta lập trình trên ARM 32 bit, thì con trỏ chứa giá trị 32 bit (8 số
hex).
Ủa, vậy tại sao lại có char* , int* , void* , double* , vv? Câu trả lời là: con trỏ chứa địa chỉ, con trỏ
kiểu char trỏ tới “vùng nhớ” kiểu char (nghĩa là các ô nhớ ở đây chứa dữ liệu kiểu char ), con trỏ kiểu int
thì trỏ tới vùng nhớ kiểu int ….
2. Chúng ta đi sâu hơn về bộ nhớ máy tính (memory) để hiểu khái niệm con trỏ.
Trước khi vào bài, bạn cần phải biết về cấu tạo bộ nhớ máy tính là như thế nào. Bạn hãy nhìn vào hình 1
bên dưới.

Hình 1 – Biếm họa về bộ nhớ máy tính (nguồn ảnh – internet)


Bộ nhớ bao gồm những ô nhớ được sắp xếp từ trên xuống dưới. Một ô nhớ sẽ có 2 phần: địa chỉ và nội
dung ô nhớ.
Bạn hãy nhìn vào biết pAge trong hình 1. Biến này chiếm 1 ô nhớ trong bộ nhớ, địa chỉ của biến này là
20886 , và nội dung của biến này chính là 18826 .

Và vì nội dung của biến pAge lại chính là địa chỉ của biến age . Nên ta gọi pAge là một pointer hay con
trỏ. Chúng ta có thể nói rằng: aAge đang trỏ tới age . Bạn đã hiểu pointer là gì chưa?

3
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

Bây giờ chúng ta sẽ tiếp tục khám phá thêm về bộ nhớ thông qua hình 2.

Hình 2 – Biếm họa về bộ nhớ máy tính 2 (nguồn ảnh – head first C)
Nhìn vào hình 2 và ta sẽ giải thích như sau:
Khi chúng ta khai báo một biến, máy tính sẽ tạo ra một vùng nhớ để lưu giá trị cho biến đó
(hay còn gọi là cấp phát bộ nhớ).
Nếu biến được khai báo biến bình thường (không phải cấp phát động) trong hàm main() , thì
những ô nhớ được cấp phát sẽ nằm trong vùng stack (như trong hình là biến x ). Lưu ý, chúng
ta chưa bàn tới khái niệm vùng heap trong phần này.
Nếu biến được khai báo ngoài hàm main() và ngoài tất cả các hàm, thì những ô nhớ được
cấp phát sẽ nằm trong vùng global của bộ nhớ (trong hình là biến y ).
Trong ví dụ trên, máy tính cấp phát cho biến x một “vị trí” (vị trí này chính là địa chỉ – địa chỉ của vùng
nhớ, tại sao mình không nói là ô nhớ, bởi vì tùy vào CPU mà kiểu int có thể được chứa ở nhiều ô nhớ) là
4,100,000 . Chúng ta đã gán 4 vào x thì “vị trí” 4,100,000 sẽ lưu giá trị là 4 .

Và…con trỏ là biến chứ địa chỉ 4,100,000 như ví dụ ở trên! Nếu muốn trỏ tới x , thì phải dùng con trỏ
kiểu int , bởi vì giá trị của x là integer.
Qua 2 ví dụ, bạn đã hiểu được khái niệm về con trỏ, hình dung là nó đã hoạt động như thế nào. Chúng ta
tiếp tục các phần tiếp theo. Nên nhớ, nếu đi tới đây, bạn vẫn chưa hiểu về con trỏ, thì bạn hãy tiếp tục đọc
lại hoặc nghiền ngẫm cho đến khi nào hiểu thì mới đi tiếp.

4
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

3. Khai báo con trỏ


Con trỏ được khai báo với cú pháp như sau:

Kiểu dữ liệu *tên con trỏ;

Kiểu dữ liệu ở đây bao gồm:


Kiểu dữ liệu có sẵn: int , char , void , long , ….
Kiểu dữ liệu do chúng ta tự định nghĩa: struct, union.
Kiểu dữ liệu là lớp (class) do chúng ta định nghĩa (c++).
Kiểu dữ liệu dẫn xuất, kiểu con trỏ hàm (nâng cao).
Ví dụ:

int *p;

p ở đây là con trỏ, và con trỏ p này trỏ đến vùng nhớ kiểu int . Lưu ý *p không phải là con trỏ
mà là giá trị của vùng nhớ mà con trỏ p trỏ tới (giả sử p trỏ tới x trong hình 2 thì *p = 4 ).
Tiếp ví dụ:

int *a, *b; // a và b đều là con trỏ


int *a, b; // a là con trỏ, b là biến integer
int* a, b; // a là con trỏ, b là biến integer,
// khai báo này đúng, nhưng dễ gây nhầm lẫn
void *a; // Đúng! Có con trỏ void kiểu này

4. Khởi tạo giá trị cho biến con trỏ


Khởi tạo (initialize) khác với khai báo (declare) các bạn! Sau khi khai báo 1 biến, bạn gán giá trị lần đầu
cho biến đó thì gọi là khởi tạo! Lời khuyên cho các bạn là, nên khởi tạo giá trị biến mỗi khi khai báo, vì
khi không khởi tạo giá trị, nhiều trình biên dịch sẽ tự khởi tạo một giá trị rác cho biến đó, hoặc nhiều khi
gây lỗi.
Khởi tạo cho con trỏ:

tên con trỏ = địa chỉ vùng nhớ;

Toán tử lấy địa chỉ: toán tử & 1 ngôi (unary operator), toán tử này hoàn toàn khác với toán tử & 2
ngôi (bitwise).
Ví dụ:

int bienInteger = 100; // khai báo và khởi tạo biến integer


int *conTro; // khai báo con trỏ kiểu int
conTro = &bienInteger; // con trỏ conTro trỏ tới biến bienInteger

5
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

Lúc này *conTro tương đương với bienInteger , mọi thao tác với *conTro cũng chính là
thao tác với bienInteger , hay mọi thao tác với bienInteger cũng chính là thao tác với
*conTro .
Câu lệnh bienInteger = 5; hoàn toàn tương đương với *conTro = 5; .
bienInteger++; hoàn toàn tương đương với (*conTro)++; (Khác với *conTro++ nhé!).

Bạn hãy thử test các ví dụ này xem. Test bằng cách dùng lệnh printf để in giá trị ra và chúng ta sẽ kiểm
tra được tính đúng đắn của nó.
Thêm một ví dụ nữa là bạn sẽ hiểu tường tận từ con trỏ, biến, địa chỉ.

#include <stdio.h>

int main () {

int thanhVar = 20; /* khai báo và khởi tạo biến thanhVar */


int *pTh; /* khai báo con trỏ pTh */

pTh = &thanhVar; /* gán địa chỉ của biến thanhVar vào pTh */
/* hay pTh đang trỏ vào biến thanhVar */

printf("Địa chỉ của biến thanhVar: %x\n", &thanhVar );

/* giá trị của biến pTh đang lưu là */


printf("Giá trị được lưu trong pTh: %x\n", pTh );

/* truy cập giá trị biến thanhVar thông qua con trỏ pTh */
printf("Value of *ip variable: %d\n", *ip );

return 0;
}

5. Kiểu dữ liệu của con trỏ


Lấy một ví dụ đơn giản thế này:

int *p; // kiểu dữ liệu của con trỏ p này là int*

Hiểu kiểu dữ liệu của con trỏ chưa nào? quá dễ hiểu! Vậy xem ví dụ sau:

void *p; // biến p này kiểu gì?

Ngôn ngữ c có rất nhiều kiểu dữ liệu dành cho con trỏ, phần này đã được nói phần 3. trong chương này.
Trong phần này, mình muốn giới thiệu một chút về con trỏ kiểu void và con trỏ trỏ tới NULL .
Chúng ta đã có con trỏ kiểu int, nghĩa là con trỏ này sẽ trỏ tới vùng nhớ kiều int, hay tương tự cho doube,
float,... Vậy con trỏ kiểu void nghĩa là con trỏ chưa xác định được kiểu dữ liệu nó sẽ trỏ vào. Và điều
quan trọng, nó có thể trỏ tất bất kì vùng nhớ có kiểu dữ liệu gì.

6
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

Ví dụ:

int a = 10;
char b = 'x';

void *p = &a; // void pointer đang trỏ tới 'a'


p = &b; // bây giờ thì lại trỏ tới 'b'. Mặc dù 'a' và 'b' khác kiểu dữ liệu

Bây giờ chúng ta tìm hiểu tiếp về con trỏ mà trỏ tới NULL . Chúng ta hay gọi nó là NULL pointer.
Chúng ta cùng xem ví dụ:
#include <stdio.h>

int main () {

int *ptr = NULL;


printf("Giá trị của ptr là : %x\n", ptr );

return 0;
}

Bạn dễ dàng nhìn thấy được kết quả in ra: Giá trị của ptr là : 0 . Null pointer chính là một
hằng số 0 , và điều này được quy định ở các thư viện chuẩn của C. Và bạn có thể hiểu một cách đơn giản,
NULL pointer là một con trỏ chưa trỏ đến vùng nhớ nào hết. Giá trị của nó là 0 , không có nghĩa là nó trỏ
tới địa chỉ 0 (các chương trình sẽ bị cấm trỏ vào địa chỉ 0, vì đây là vùng nhớ của hệ thống).

6. Các phép toán trên con trỏ


6.1 Phép gán
Một số điều sau đây các bạn phải ghi nhớ.
Tất cả con trỏ đều có phép gán.
Như phần khởi tạo, phép gán yêu cầu vế trái là một con trỏ, và vế phải là một địa chỉ.
Phép gán yêu cầu sự tương xứng về kiểu dữ liệu, nếu không chúng ta phải ép kiểu, các bạn biết ép
kiểu không?
Phép gán với con trỏ kiểu void không cần phải tương xứng kiểu dữ liệu. Con trỏ kiểu void là con
trỏ cuối phần 5. hồi nảy.
Xem ví dụ:

int a = 100;
int *p;
p = (int*)0x0060FF08; // 0x0060FF08 giả sử là địa chỉ biến a,
// đây là ép kiểu,
// vì 0x0060FF08 chỉ là một số nguyên,
// phải ép sang kiểu int* mới gán được cho con trỏ
printf("*p: %d\n", *p); // kết quả của *p là 100, vì p trỏ tới a

7
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

6.2 Phép so sánh


Và chúng ta cũng có một vài tính chất sau của phép so sánh. Lưu ý là các bạn không phải học thuộc lòng,
mà phải đọc hiểu từng phần.
Phép so sánh ngang bằng dùng để kiểm tra 2 con trỏ có trỏ vào cùng 1 vùng nhớ hay không, hoặc
kiểm tra 1 con trỏ có phải là đang trỏ vào NULL hay không (trong trường hợp cấp phát động, mở
file, mở resource, vv).
Phép so sánh lớn hơn nhỏ hơn: > , = , <= sử dụng để kiểm tra về độ thấp cao giữa 2 địa chỉ. Con
trỏ nào nhỏ hơn thì trỏ vào địa chỉ thấp hơn.
Được quyền so sánh mọi con trỏ với 0 , vì 0 chính là NULL .

int a=100,*p=&a;
double *x;
printf(p == &a); // kết quả bằng 1 (true)
printf(main == 0); // kết quả bằng 0 (false)
printf(p == 0); // kết quả bằng 0 (false)
printf(x == 0); // kết quả bằng 0 (false)

Lưu ý: chúng ta sẽ đi sâu về phép so sáng này sau, vì nâng cao khá khó!
6.3 Phép cộng trừ và phép tăng giảm
Bản chất của việc tăng giảm con trỏ là di chuyển con trỏ đi lên hoặc đi xuống. Hãy nhớ lại hình 1, khi
chúng ta tăng giảm nội dung của con trỏ, thì con trỏ sẽ trỏ tới vùng nhớ khác.
Đương nhiên không phải di chuyển sang ô nhớ kế tiếp (byte kế tiếp), mà phụ thuộc vào kiểu dữ liệu của
vùng nhớ con trỏ trỏ tới.
Ví dụ:

int a = 100, *p = &a; // giả sử lúc này p đang trỏ tới địa chỉ 0x0060FF08 (địa
chỉ của a)
p++; // p lúc này sẽ trỏ tới địa chỉ 0x0060FF0C (tăng lên 4 ô nhớ)
// bởi vì kiểu int lúc này có 4 byte
p -= 2; // p sẽ trỏ tới địa chỉ 0x0060FF04

Chú ý:
Không có phép tăng giảm con trỏ void và con trỏ hàm.
Không có phép cộng 2 con trỏ với nhau.
Phép trừ 2 con trỏ trả về độ lệch giữa 2 con trỏ

8
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

7. Một số ví dụ hay và đương nhiên, khó!


Chương trình viết hoa một chuỗi được nhập vào từ bàn phím

#include <stdio.h>
#include <conio.h>
#include <ctype.h>

int main()
{
char xau[100];
char *p = &xau;

printf("Nhap xau: ");


scanf("%[a-zA-Z]", xau); // lệnh này giới hạn kí tự nhập từ a-z và A-Z thôi

/* while-loop style */
while(*p)
{
printf("%c",toupper(*p));
p++;
}

return 0;
}

Chương trình in ra chuỗi bị đảo ngược

#include <stdio.h>
#include <conio.h>
#include <ctype.h>

int main()
{
char xau[100];
char *p;
printf("Nhap xau: ");
scanf("%[a-zA-Z]",xau);

p = xau + strlen(xau) -1; // đưa con trỏ về cuối xâu

/* for-loop style */
for(; p >= xau; p--)
printf("%c", *p);

return 0;
}

Chương trình con lấy độ dài một chuỗi (khi học nhúng thì bạn nên viết chương trình con ra như vậy thay
vì dùng hàm có sẵn, bạn phải khai báo thư viện, tốn bộ nhớ).

9
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

int strlen(char *p)


{
int temp = 0; // biến đếm độ dài

while(*p) // chừng nào gặp kí tự kết thúc chuỗi thì dừng (mã ASCII = 0)
{
temp++;
p++;
}

return temp;
}

8. Constant pointer
Constant pointer tạm dịch là hằng con trỏ. Hằng con trỏ là một con trỏ, nhưng khi đã trỏ tới một vùng
nhớ nào rồi, thì chúng ta không thể cho nó trỏ tới vùng nhớ khác được. Nhưng chúng ta vẫn có thể thay
đổi data của vùng nhớ đó. Xem ví dụ sau nhé:

#include <stdio.h>

int main()
{
int a = 10;
int *const ptr = &a;
*ptr = 5; // right
ptr++; // báo lỗi

return 0;
}

Vây cú pháp khai báo hằng con trỏ như sau:

[kiểu dữ liệu] *const [tên con trỏ];


// ở ví dụ trên là:
int *const ptr;

9. Pointer to constant
Pointer to constant tạm dịch là con trỏ trỏ tới một vùng nhớ hằng. Nghĩa là vùng nhớ con trỏ trỏ tới sẽ
không thể nào thay đổi được, còn con trỏ thì muốn trỏ đi đâu thì tùy.
Xém ví dụ sau:

10
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

#include <stdio.h>

int main()
{
const int a = 10; // hoặc int a = 10;
const int *ptr = &a;
*ptr = 5; // báo lỗi
ptr++; // right
}

Cú pháp khai báo pointer to constant như sau:

const [kiểu dữ liệu] *[tên con trỏ];


// ở ví dụ trên là:
const int* ptr;

Rất khó nhớ phải không? Bây giờ chúng ta sẽ có một cách rất dễ nhớ:

int const *ptr; // ptr is a pointer to constant int


int *const ptr; // ptr is a constant pointer to int

10.Mảng các con trỏ (Array of pointers)


Trước khi đọc ebook này, mình đã yêu cầu bạn phải biết trước về các kiến thức cơ bản của C, và trong đó
có kiến thức về mảng (array). Trong các phần trước, chúng ta đã học về con trỏ đơn bình thường. Bây giờ
chúng ta sẽ học về mảng các con trỏ.
Phần này cũng đơn giản và dễ hiểu. Array of pointers là một mảng chứa các phần tử là con trỏ.
Cú pháp của khai báo này như sau:

Kiểu dữ liệu *ptr[số lượng phần tử];

11
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

Xem ví dụ để hiểu:

#include <stdio.h>
const int MAX = 3;

int main () {

int var[] = {10, 100, 200}; /* khai báo một mảng có 3 phần tử */
int i, *ptr[MAX]; /* khai báo một mảng 3 con trỏ */

for ( i = 0; i < MAX; i++) {


/* 3 con trỏ trong mảng lần lượt trỏ tới 3 biến của var */
ptr[i] = &var[i];
}

for ( i = 0; i < MAX; i++) {


printf("Giá trị của var[%d] = %d\n", i, *ptr[i] );
}

return 0;
}

Bạn sẽ thấy chương trình in ra kết quả sau:

Giá trị của var[0] = 10


Giá trị của var[1] = 100
Giá trị của var[2] = 200

Bạn đã hiểu về mảng rồi thì mảng các con trỏ cũng tương tự như vậy – mỗi phần tử trong mảng là một
con trỏ.

11.Con trỏ trỏ tới một con trỏ (pointer to pointer)


Đã đi được tới đây rồi thì kiến thức của bạn thuộc dạng rất tốt rồi. Bây giờ chúng ta sẽ học phần khó hơn.
Bạn hãy nhìn hình bên dưới để hiểu về khái niệm này.

Theo hình trên, con trỏ đầu tiên sẽ chứa địa chỉ của con trỏ thứ 2, và con trỏ thứ 2 lại chứa địa chỉ của
biến Variable .

12
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

Theo hình trên, con trỏ ptr2 sẽ chứ địa chỉ của con trỏ ptr1 , và con trỏ ptr1 sẽ chứa địa chỉ của biến
num . Ta gọi ptr2 là con trỏ trỏ tới con trỏ (pointer to pointer). Đây là cách hiểu đơn giản của pointer to
pointer.
Chúng ta cùng xem một ví dụ về code:

#include <stdio.h>

int main () {

int var;
int *ptr;
int **pptr;

var = 3000;

/* ptr lấy địa chỉ của biến var */


ptr = &var;

/* pptr lấy địa chỉ của biến ptr */


pptr = &ptr;

/* in giá trị sử dụng con trỏ pptr */


printf("Giá trị của var = %d\n", var );
printf("Giá trị của *ptr = %d\n", *ptr );
printf("Giá trị của **pptr = %d\n", **pptr);

return 0;
}

Bạn sẽ nhận được 3 kết quả in ra giống nhau: 3000 . Hãy ráng tập trung và hiểu ví dụ này. Nếu bạn thật
sự hiểu được ví dụ này, thì kiến thức về con trỏ của bạn đã cực kì tốt rồi.

12. Function trả về kiểu con trỏ


Sở dĩ phần này được đề cập đến để phân biệt với phần 13 bạn sẽ học ngay sau đây.
Bình thường khi bạn viết một function, bạn phải có kiểu trả về cho function đó, điều này quá đỗi bình
thường trong C. Và trong phần này, bạn sẽ học một kiểu trả về mới của một hàm, đó chính là pointer.
Đúng vậy, một hàm có thể trả về một pointer.

13
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

Hãy xem ví dụ đầu tiên

#include <stdio.h>

int* doubleValue(int x)
{
int value = x * 2;
return &value; // hàm này trả về một địa chỉ
} // giá trị value sẽ bị phá hủy khi ra khỏi hàm này. Hãy xem lại phần scope

int main () {

int *ptr = doubleValue(2);


printf("Giá trị con trỏ đang trỏ tới là : %x\n", *ptr);

return 0;
}

Ví dụ trên là một function trả về một con trỏ. Điều này hoàn toàn không có gì phức tạp, con trỏ thì chứa
địa chỉ, cho nên return một con trỏ đồng nghĩa với return một địa chỉ. Bạn hãy để ý là biến value (biến
local trong hàm) sẽ bị phá hủy (destroy) ngay khi ra khỏi hàm, cho nên khi con trỏ ptr “lấy” địa chỉ trả về
của hàm doubleValue, địa chỉ này đã bị phá hủy rồi, nên chương trình sẽ gây ra lỗi vì con trỏ không biết
đang trỏ đến đâu, có thể là một địa chỉ rác.
Cho nên khi chúng ta sử dụng hàm để trả về con trỏ thì phải đặc biệt lưu ý điều này. Thông thường chúng
ta sử dụng hàm để trả về con trỏ trong 2 trường hợp là biến static hoặc cấp phát động.
Ví dụ về static :

#include <stdio.h>

int* doubleValue(int x)
{
static int;
value = x * 2;
return &value; // hàm này trả về một địa chỉ
}

int main () {

int *ptr = doubleValue(2);


printf("Giá trị con trỏ đang trỏ tới là : %x\n", *ptr);

return 0;
}

14
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

Ví dụ về cấp phát động:

#include <stdio.h>

int* doubleValue(int x)
{
int *value = (int *) malloc(sizeof( int ));
*value = x * 2;
return value; // hàm này vẫn trả về địa chỉ
}

int main () {

int *ptr = doubleValue(2);

printf("Giá trị con trỏ đang trỏ tới là : %x\n", *ptr);

return 0;
}

13. Con trỏ trỏ tới hàm (function pointer)


Bạn đã biết, một con trỏ sẽ chứa địa chỉ của một biến nào đó. Trong C (và C++), con trỏ còn có thể chứa
địa chỉ của một hàm (function). Khi một con trỏ chứa địa chỉ của một hàm, hay ta gọi đó là con trỏ đang
trỏ tới hàm. Đây là khái niệm rất dễ hiểu của function pointer.
Chúng ta đi tới xem ví dụ:

#include <stdio.h>
// đây là một hàm rất bình thường
// và có kiểu trả về là void
void fun(int a)
{
printf("Value of a is %d\n", a);
}

int main()
{
// fun_ptr là một con trỏ trỏ tới hàm fun()
void (*fun_ptr)(int) = &fun;

/* dòng phía trên thì hoàn toàn tương đương với hai dòng dưới:
void (*fun_ptr)(int);
fun_ptr = &fun;
*/

// truy cập hàm fun thông qua con trỏ hàm


(*fun_ptr)(10);

return 0;
}

Giải thích một chút nếu các bạn thấy hơi khó hiểu: Bạn hãy nhìn hàm fun(), hàm này có kiểu trả về là
void, hàm này cũng có một tham số đầu vào là int.

15
Chương 3 – Con trỏ (pointer) Chinh phục ngôn ngữ C

Đó là lý do tại sao con trỏ hàm lại được khai báo void (*fun_ptr)(int) . Hãy ghi nhớ cách khai báo
này và đừng quên dấu ngoặc nào hết. Có thể cú pháp nó hơi khác so với những gì bạn đã học về con trỏ.

16
Chương 4 – Digital Chinh phục ngôn ngữ C

CHƯƠNG 4 - DIGITAL
Một chương rất hay, giúp cho các bạn một cái nhìn tổng quan về digital trong C/C++. Bởi vì khi các bạn
đã học C rồi, thông thạo con trỏ (pointer) rồi, thì vấn đề về digital hay quản lý bộ nhớ (memory
management) các bạn phải biết thì mới thấy “vui” khi học C/C++ được, mới trở thành một dev chất
được.

1. Các hệ thống số đếm


Cái này thì đa số mọi người học lập trình thì biết rồi. Chúng ta có các hệ đếm cơ bản là hệ mười (decimal
system), hệ nhị phân (binary system), hệ 16 (hexa decimal system) và hệ 8 (octal system). Đối với máy
tính thì chúng chỉ hiểu duy nhất hệ nhị phân và mọi loại dữ liệu đểu được mã hóa về mã nhị phân để máy
tính hiểu, và các hệ còn lại giúp cho con người và compiler hiểu.
Ở phần này, chúng ta sẽ tìm hiểu một số vấn đề liên quan, còn phần chuyển đổi giữa các hệ số thì các bạn
tự tìm hiểu thêm nhé.
1.1 Cách máy tính lưu mã nhị phân.
Máy tính chỉ hiểu nhị phân, còn lại không hiểu một cái gì hết. Ta có ví dụ sau:

char a = 0x80 // = (MSB)1000 0000(LSB)

Tất cả các mã nhị phân đều được lưu vào các ô nhớ máy tính. Bit bên trái ngoài cùng chính là bit cao
nhất (MSB) và bit bên phải ngoài cùng là bit có trọng số thấp nhất (LSB).
1.2 Số bù một (one’s complement)
Là một số trong hệ nhị phân mà nó chính là bù cơ số trừ 1 (radix-minus-1 complement) của một số khác.
Khái niệm này có thể khó hiểu cho một số bạn. Nhưng bạn hãy xem ví dụ để hiểu về số bù 1.
Một số bù 1 có thể có được do đảo tất cả các bit có trong số nhị phân (đổi 1 thành 0 và ngược lại).

char a = 0xC0 // 0xC0 = (MSB)1100 0000(LSB)

Thì số bù 1 của a là: 0011 1111


1.3 Số bù 2 (two’s complement)
Chúng ta có thể hiểu đơn giản số bù 2 là số bù 1 cộng thêm 1. Và số bù 2 dùng để biểu diễn số âm khi
chúng ta khai báo signed .
Vẫn lấy ví dụ trên, 0011 1111 là số bù một của a .
Và số bù 2 của a là 0011 1111 + 1 = 0100 0000
1.4 signed và unsigned trong C/C++
signed là kiểu dữ liệu có dấu. Bit dấu chính là bit MSB. MSB = 1 thì là số âm, và MSB = 0 thì là số
dương.
unsigned là kiểu dữ liệu không dấu. Chúng ta chỉ quan tâm nhiều về số có dấu thôi nhé.

17
Chương 4 – Digital Chinh phục ngôn ngữ C

Ví dụ về những kiến thức phía trên.

#include <stdio.h>

int main()
{
unsigned char a = 0xC0; // 1100 000
signed char b = 0xC0;

printf("%d\n", a); // in ra 192


printf("%d", b); // in ra -64

return 0;
}

Giải thích kết quả in ra màn hình:


Dòng số 7 in ra một số không âm nên sẽ in ra 192 <=> 1100 000 <=> 0xC0
Dòng số 8 in ra một số âm (-64), lý dó số âm là vì MSB = 1 . Máy tính sẽ in ra số -c với c là số bù 2
của b . Đây là nguyên tắc của kiểu có dấu – signed. Kết quả cho thấy c = 64 . Chúng ta sẽ tính tại sao lại
như vậy.
c là số bù 2 của b . Vậy chúng ta sẽ tính số bù 2 của b bằng cách lấy số bù 1 của b rồi cộng với 1 .
c = 0011 1111 + 1 = 0100 0000 . Số này tương đương với 64 . Vậy c = 64 , máy tính in ra kết
quả -c là -64 .
Đó là lý do giải thích cho bài toán.

2. Các phép toán xử lý bit


Sau đây là các phép toán xử lý trực tiếp trên bit. Các bạn có thể search bảng chân trị (true table) của các
phép toán này nếu các bạn chưa biết gì về nó.
2.1 Phép OR bit, ký hiệu “| ”
Đây là phép toàn OR các bit lại với nhau, và nó OR trực tiếp các bit luôn nhé. Cái gì OR với 1 thì luôn
bằng 1 . Lấy ví dụ thế này

18
Chương 4 – Digital Chinh phục ngôn ngữ C

/* Đây là ví dụ thực hiện phép OR giữa 2 số 0101 1000 và 1010 0100 */


#include <stdio.h>

int main()
{
unsigned char a = 0x58; // 0101 1000
unsigned char b = 0xA4; // 1010 0100

printf("%x", a | b);
/* Kết quả sẽ in ra:
0101 1000
OR 1010 0100
= 1111 1100 <=> 0xFC
*/

return 0;
}

2.2 Phép toán AND , ký hiệu “& ”


Phép này cũng giống như phép OR và cũng khá đơn giản. Cái gì AND với 0 thì luôn bằng 0 .

/* Đây là ví dụ thực hiện phép AND giữa 2 số 0101 1000 và 1010 0100 */
#include <stdio.h>
int main()
{
unsigned char a = 0x58; // 0101 1000
unsigned char b = 0xA4; // 1010 0100

printf("%x", a & b);


/* Kết quả sẽ in ra:
0101 1000
AND 1010 0100
= 0000 0000 <=> 0x00
*/

return 0;
}

2.3 Phép XOR bit, ký hiệu “^ ”


Đây là một phép toán cũng ít người nhớ. Nhưng nó cũng dễ nhớ cực kì nếu bạn chú ý. Hai số giống nhau
XOR thì sẽ ra 0 , và khác nhau XOR sẽ ra 1 (từ nảy tới giờ mình nói số số thì nó là nhị phân nhé)

19
Chương 4 – Digital Chinh phục ngôn ngữ C

/* Đây là ví dụ thực hiện phép XOR giữa 2 số 0101 1001 và 1010 0101 */
#include <stdio.h>

int main()
{
unsigned char a = 0x59; // 0101 1001
unsigned char b = 0xA5; // 1010 0101

printf("%x", a ^ b);
/* Kết quả sẽ in ra:
0101 1001
XOR 1010 0101
= 1111 1100 <=> 0xFC
*/

return 0;
}

2.4 Phép NOT bit, ký hiệu “~ ”


Phép này là dễ nhất, 0 thì đảo thành 1 , 1 đảo thành 0 .

/* Đây là ví dụ thực hiện phép NOT số 0101 1001 */


#include <stdio.h>

int main()
{
unsigned char a = 0x59; // 0101 1001

printf("%x", ~a);
/* Kết quả sẽ in ra:
NOT 0101 1001
= 1010 0110 <=> 0xA6 (chính xác sẽ là 0xFFFFFFA6 nhé)
Nếu các bạn thắc mắc về chuyện này thì để lại comment bên dưới nhé.
*/

return 0;
}

3. Các phép toán logic


Sau khi đã có hiểu biết về các phép toán xử lý bit, chúng ta sẽ qua các phép logic trong C/C++. Đây là các
phép toán chúng ta hay gặp nhất trong khi code. Nhất là khi các bạn dùng lệnh if hay while thì các bạn
sẽ đặt điều kiện trong đó. Chính các phép toán logic này sẽ tạo ra điều kiện đó đó. Mà các bạn có thực sự
hiểu rõ các phép toàn. Và có hay nhầm lẫn với các phép toán xử lý bit?
Phép toán logic chỉ trả về 2 kết quả đó là 0 (false ) và 1 (true ).
3.1 Phép logic OR , ký hiệu “|| ”
Cùng xem ví dụ để hiểu phép toàn này nhé:
True || True = True

20
Chương 4 – Digital Chinh phục ngôn ngữ C

True || False = True

False || False = False

Chúng ta đi vào một đoạn code để chúng minh cho điều vừa nói.

/* Đây là ví dụ thực hiện phép logic OR */


#include <stdio.h>

int main()
{
int a = (9 > 5) || (1 == 0);
/*
(9 > 5), cái này đúng sẽ cho ra True
(1 == 0), cái này bậy nên sẽ cho ra False
True || False = True
Nên kết quả sẻ in ra 1.
*/

printf("%d", a); // kết quả in ra 1 (true)

return 0;
}

3.2 Phép logic AND , ký hiệu “&& ”


Tương tự như phép logic OR , chúng ta có các kết quả sau:
True && True = True

True && False = False

False && False = False

Và đây là một đoạn code

/* Đây là ví dụ thực hiện phép logic AND */


#include <stdio.h>

int main()
{
int a = (9 > 5) && (1 == 0);
/*
(9 > 5), cái này đúng sẽ cho ra True
(1 == 0), cái này bậy nên sẽ cho ra False
True && False = False
Nên kết quả sẻ in ra 0.
*/

printf("%d", a); // kết quả in ra 0 (false)

return 0;
}

21
Chương 4 – Digital Chinh phục ngôn ngữ C

3.3 Toán tử logic NOT , ký hiệu “! ”


Và toán tử này cũng thuộc loại dễ … NHẦM với toán tử NOT trên kia.
!True = Flase

!False = True

Xem đoạn code sau

/* Đây là ví dụ thực hiện phép logic NOT */


#include <stdio.h>

int main()
{
int a = (9 > 5) && (1 == 0);
int b = 100;
/*
(9 > 5), cái này đúng sẽ cho ra True
(1 == 0), cái này bậy nên sẽ cho ra False
True && False = False
*/

printf("%d", a); // kết quả in ra 0 (false)


printf("%d", !a); // kết quả in ra 1 (true)
printf("%d", !b); // kết quả in ra 0 (false)
printf("%d", !(!b)); // kết quả in ra 1 (true)

return 0;
}

Để ý thằng b trên đoạn code, nó là một số integer ban đầu được gán 100 , vậy mà đưa cho nó 2 cái dấu
!! nó câm lặng về 1 liền. :)))

Điều này giải thích khá dễ, ngoài 1 là true ra (đương nhiên 0 là false rồi) thì tất cả các số còn lại máy
tính hiểu nó là true hết.
Cho nên !b = !True = False
!(!b) = !False = True . Thì sẽ in ra một. True thì máy tính sẽ in ra 1 , tất cả các số còn lại trừ số 0
máy tính sẽ hiểu là True .

4. Toán tử dịch bit


Sở dĩ mình không để toán tử dịch bit này cùng với các toán tử xử lý bit vì mình thấy toán tử dịch bit này
khá hay và khó hơn những toán tử kia. Toán tử này là một trong những toán tử quan trong nhất trong lập
trình nhúng với kĩ thuật mặt nạ (mask).
4.1 Toán tử dịch trái, ký hiệu “<< “
Khi toán tử dịch trái được thực hiện trên một toán hạng, những bit của toán hạng được dịch về bên trái.
Các bít bị chuyển sang trái bị mất và 0 thay vào phía bên phải của toán hạng.

22
Chương 4 – Digital Chinh phục ngôn ngữ C

Ví dụ: Cho A = 0000 0010


A << 1 = 0000 0100

A << 7 = 0000 0000

Một ví dụ trong C:

#include <stdio.h>

int main()
{
int a = 2;
printf("%d", a << 1); // Kết quả in ra 4
// 2 = ... 0000 0010
// 2 << 1 = ... 0000 0100 <=> 0x4 = 4

return 0;
}

4.2 Toán tử dịch phải, ký hiệu “>> ”


Đây là toán tử khá khó chịu, nhưng một khi bạn hiểu rồi thì mọi chuyện khá dễ dàng. Nó cũng tương tự
như toán tử trên, chỉ là nó ngược lại thôi. Nhưng….
Các bạn còn nhớ signed với unsigned chứ. Nếu không nhớ thì quay lại phần 1 xem nhé.
Khi chúng ta khai báo unsigned và chúng ta dùng phép toán dịch phải, mọi chuyện bình thường:
Cho A = 1000 0000 (với khai báo: unsigned char a = 0x80; )
A >> 1 = 0100 0000

A >> 7 = 0000 0001

Nhưng khi chúng ta khai báo signed (nghĩa là số có dấu) thì mọi chuyện sẽ khác.
Cho A = 1000 0000 (với khai báo: signed char a = 0x80; )
A >> 1 = 1100 0000

A >> 7 = 1111 1111

Các bạn có hiểu vấn đề chưa? Khi một số có dấu thì cái bit được thêm vào trong khi dịch chính là bit dấu
(MSB ).
Lấy ví dụ thực tế

23
Chương 4 – Digital Chinh phục ngôn ngữ C

#include <stdio.h>

int main()
{
unsigned char a = -128; // 0x80
char b = -128; // 0x80 <=> 1000 0000

printf("%d\n", a >> 1); // 0100 0000 <=> 64


printf("%d", b >> 1); // 1100 0000 <=> -64

return 0;
}

Tại sao kết quả in ra -64 thì mời các bạn đọc lại bài trước ở phần số bù 2.
Chúng ta có một số công thức về phép dịch: a << n = a * 2^N và a >> n = a / 2^N
Các bạn không cần phải nhớ mấy cái công thức này (mình chẳng bao giờ nhớ), mà các bạn cần phải hiểu
rõ bản chất thật sự của vấn đề. Vì mục đích học C là để thấu hiểu hơn về phần cứng.

5. Các phép toán phối với nhau


Mình đề cập tới vấn đề này bởi vì đây là thực tế của các buổi phỏng vấn. Không lẽ họ đưa cho bạn một
bài thiệt khó đòi hỏi giải thuật cao siêu để giải để đánh giá bạn? Không! Họ đưa cho bạn những bài ngắn
nhưng rất chất kiểu….

// Kết quả in ra trên màn hình là gì?


#include <stdio.h>

int main()
{
int a = 10, b = 10;
if (a = 5)
b--;
printf("%d, %d", a, b--);

return 0;
}

Bạn sẽ trả lời như thế nào? Dừng lại và thử suy nghĩ để trả lời cho mình trước khi đọc tiếp!
Câu trả lời: 5, 9
Giải thích cho bài toán trên: trong dòng if ở phía trên, có vẻ như chúng ta đã thiếu 1 dấu = .
Thay vì if (a == 5) thì đây lại là if (a = 5) . Tình cờ đây là một phép gán, và trong C chấp nhận
điều này. Và lúc này biến a đã mang giá trị 5 . Bởi vì điều kiện trong if là một phép gán (thành công),
cho nên nó luôn luôn trả về true , và lệnh b--; trong if sẽ được thực hiện. Suy ra b sẽ là 9 trước khi in
ra màn hình.
Một bài toán khác, đây là bài mình đánh giá khá hay và rất dễ xuất hiện trong các bài toán tuyển dụng.

24
Chương 4 – Digital Chinh phục ngôn ngữ C

// Kết quả in ra màn hình là gì?


#include <stdio.h>

int main()
{
int i = 10, j = 0;
if (i || (j = i + 10))
printf("%d", j);

return 0;
}

Các bạn dừng lại và suy nghĩ một chút về câu trả lời nhé. Đáp án rất bất ngờ…
Câu trả lời: 0
Giải thích: Trong câu điều kiện if , vế bên trái của phép “|| ” là i (mà i ở đây đang = 10 nghĩa là
true ). True nó có OR với thằng nào thì cũng là true cả, nên vế bên phải sẽ không được thực hiện, và
đương nhiên j vẫn dữ giá trị 0 . Hay đúng không?

6. Dấu ngoặc chết chóc và “đừng bao giờ thử thách trí nhớ của mình với ngôn
ngữ C”
Trong ngôn ngữ C/C++, có một cái bảng gọi là… thứ tự ưu tiên của các phép toán. Mà đa số lập trình
viên C/C++ đều không thể nhớ nó (kể cả mình), nhưng oái ăm một điều, mấy anh phỏng vấn thì lại thích
mấy cái đó mới hay.
Đại loại như ví dụ sau:

#include <stdio.h>
int main()
{
int a = 10, b = 5, c = 5;
int d;
d = b + c == a;
printf("%d", d); // Kết quả in ra 1

return 0;
}

Kết quả in ra d = 1 . Vì phép “+ ” ưu tiên đầu tiên, nên b + c thực hiện trước cho ra 10 . Sau đó phép
“== ” ưu tiên tiếp theo (10 == 10 đúng (true )) nên cuối cùng d = 1 .
Khổ nổi là mấy người trong anh em chúng ta nhớ nó. Vậy mà đi phỏng vấn người tahỏi vậy để thử trí nhớ
siêu phàm của ứng viên.
Lời khuyên chân thành: Nếu đi phỏng vấn mình khuyến khích các bạn học những thứ này để hoàn thành
được bài phỏng vấn tốt và ghi điểm trong mắt người tuyền dụng. Còn khi đã trở thành developer, tuyệt
đối không được thử trí thông minh của bản thân bằng những dòng code như thế này. Vì nó chỉ làm cho
bạn và đồng nghiệp thêm nhức đầu thôi, khó hiểu và đặc biệt là khó bảo trì (maintenance). Đó là ý kiến
của mình, các bạn ai có ý kiến khác thì hãy để lại comment bên dưới.
Các bạn dev chất hãy sửa lại đoạn code như sau để dễ đọc và dễ hiểu:
25
Chương 4 – Digital Chinh phục ngôn ngữ C

#include <stdio.h>

int main()
{
int a = 10, b = 5, c = 5;
int d;
d = ((b + c) == a);
printf("%d", d); // Kết quả in ra 1
}

Có một bài toán rất hay, đề bài thì mình đã ghi trong phần comment rồi nha.

/*
Bài toán hoán đổi các bit trong một biến 8 bit
a7a6a5a4 a3a2a1a0 =====> a0a1a2a3 a4a5a6a7

Ví dụ: 1010 0011 =====> 1100 0101


*/
#include <stdio.h>

int main()
{
unsigned char a = 0xA3; // <=> 1010 0011
unsigned char result = 0;

int i = 0;
for(i; i < 8; ++i)
{
result = result | (((a >> (7 - i)) & 0x01) << i);
}

printf("%x", result); // 0xC5 <=> 1100 0101


}

26
Chương 5 – Kết Chinh phục ngôn ngữ C

CHƯƠNG 5 – KẾT
Chúng ta đã đi qua hai phần thuộc dạng khó nhất của lập trình C: pointer và digital. Khi bạn đã thật sự
nắm được 2 phần này, bạn gần như đã chinh phục được lập trình C rồi. Mặc dù vẫn còn nhiều phần khó
khác trong C như: struct, union, debug, ... nhưng sẽ dễ dàng cho bạn khi đã chinh phục được pointer và
digital.
Trong giới hạn ebook này, mình khổng thể đi hết được toàn bộ những thứ có trong pointer hay digital,
nhưng có thể nói mình đã đi được gần như 80, 90% rồi. Có chăng là những thứ phối hợp phức tạp với
nhau khi bạn thực hiện một project lớn nào đó. Khi đó bạn sẽ phối hợp rất nhiều thứ phức tạp với nhau.
Đòi hỏi kĩ năng lập trình bạn phải rất cao.
Thật sự để chinh phục 2 phần này không phải là chuyện dễ dàng, có nhiều bạn mặc dù đã học pointer
nhiều lần, nhưng vẫn không thể nào hiểu vào chinh phục được nó. Mình cũng xin nhắc lại, hãy kiên trì
theo đuổi đam mê, thành công sẽ đến với bạn.
Nếu bạn cảm thấy ebook này đem lại giá trị cho bạn, bạn có thể cám ơn mình bằng cách chia sẻ ebook
này đến nhiều người hơn nhé.

27
Tài liệu tham khảo Chinh phục ngôn ngữ C

TÀI LIỆU THAM KHẢO


Head first C
Congdongcviet
https://www.learncpp.com/
https://www.tutorialspoint.com/

28

You might also like