Bài tập

Tự tạo một hàm printf thay thế cho hàm printf mặc định trong thư viện stdio.h (phần 1)

Huy Erick

Chào bạn! Tôi là Xuân Quỳnh. Bạn vẫn đang háo hức về những kiến thức cơ bản trong lập trình C chứ? Trong bài trước, bạn đã làm quen với lệnh printf, một lệnh cơ...

Chào bạn! Tôi là Xuân Quỳnh. Bạn vẫn đang háo hức về những kiến thức cơ bản trong lập trình C chứ? Trong bài trước, bạn đã làm quen với lệnh printf, một lệnh cơ bản thuộc thư viện stdio.h, được sử dụng để in ra màn hình các đoạn text, số và nhiều thứ khác. Tôi đã hứa sẽ giới thiệu 1 hàm printf tương tự, nhưng nó sẽ được viết bằng tiếng Việt. Đó là lời hứa của tôi và giờ tôi sẽ thực hiện nó. Nhắc lại chương trình cũ của chúng ta như sau:

#include <stdio.h>
void main() {
    printf("Chào em C xinh đẹp");
}

Bây giờ, chúng ta sẽ thay thế hàm printf ở trên bằng hàm inramanhinh("In cái gì đó trong này").

Giới thiệu về thủ tục

Thủ tục là việc nhóm các câu lệnh lại để xử lý một việc nào đó theo ý muốn của lập trình viên. Ở đây, tôi nhóm việc in ra màn hình thành một thủ tục có tên là inramanhinh. Cách định nghĩa thủ tục là sử dụng từ khóa void <Tên thủ tục>. Vậy ở đây là:

void inramanhinh(char* caicaninra)

Bạn có ấn tượng với đoạn này chưa? Đừng lo, tôi sẽ giải thích cho bạn hiểu. Ở trên, tôi có từ khóa void. void là gì vậy? Đó là một từ khóa trong C, quy định cho việc viết thủ tục. Sau đó là một cái tên, bạn có thể đặt tên theo ý muốn, nhưng hãy nhớ một số quy tắc đặt tên như sau:

  • Tên không có dấu cách ở giữa. Ví dụ: void in ra man hinh(char* caicaninra) là sai! Chữa dấu cách là "em C" không chơi đâu, nó cần sự liên tục, anh em mà cứ đứt đoạn là em ấy chê yếu ngay :))
  • Tên không được trùng với các từ khóa trong C. Từ khóa trong C có rất nhiều, tôi copy paste cho bạn đọc nè: auto double int struct break else long switch case enum register typedef char extern return union continue for signed void do if static while default goto sizeof volatile const float short unsigned Đó, bạn có thể đọc nếu thích, không thích thì thôi. Khi cần cái nào thì quay lại đọc, học nó dễ mà không nhức đầu nha.

Bạn đặt tên thủ tục, nhưng tránh các từ này cho tôi là ok:

  • Tên không được bắt đầu bằng ký tự đặc biệt ((, #..) tránh cái này.

Rồi còn nhiều quy tắc khác, nhưng bạn không cần mất quá nhiều thời gian cho việc này. Bạn chỉ cần nhớ đặt tên theo cách của bạn, một cái tên của bạn và tên đó nên gợi nhớ cái thủ tục của bạn làm gì.

Nhắc lại hàm đang nghiên cứu:

void inramanhinh(char* caicaninra)

Ở trong hai cái dấu ngoặc là cái gì mà hoa bay, bướm lượn vậy nhỉ? Bạn kéo lên keyword và xem từ khóa char. Vậy char là gì? Trong tin học, char viết tắt của character, tức là ký tự. Nó chỉ là một kiểu dữ liệu thôi, không có gì đặc biệt. Ngoài char, tôi còn muốn nói thêm:

  • int = integer = số nguyên
  • float = số thực
  • long: số nguyên nhưng phạm vi lớn hơn int
  • double: số thực nhưng phạm vi lớn hơn float. Nó được định nghĩa kiểu dữ liệu có mũ. (cái này sẽ được nghiên cứu sau nhé)

Kiểu dữ liệu là sự lựa chọn của bạn trong chương trình. Ví dụ, nếu chương trình của bạn đếm cây, đếm vịt gà, (đếm được) thì dùng kiểu nguyên là int. Đếm nhiều quá thì cho hẳn cái long để nó máu. Tuy nhiên, mọi thứ đều có giới hạn, máy tính cũng chỉ đếm được tới mức nhất định rồi nó sẽ chịu. Còn giải phương trình bậc 2 thì bạn cần dùng kiểu float hoặc double, vì phương trình bậc 2 giải cho cả số thực. Tôi lấy ví dụ như vậy để nhắc bạn một điều: tùy thuộc vào bài toán mà bạn chọn kiểu dữ liệu, không phải cứ thích int thì cho int, thích float thì cho float. Nhớ rằng, đấy là kỹ năng cơ bản. Đến lúc sai kết quả, đừng trách C ngu ngốc.

Vậy còn sau char, tôi thêm một dấu * để làm cái gì nhỉ? Đúng rồi, nó còn được gọi là con trỏ. Con trỏ là gì thế? Con trỏ chỉ là con trỏ, không phải con trỏ chuột bạn di chuyển trên màn hình đâu nhé. Hồi mới học C, tôi cứ nghĩ vậy mới thú vị. Con trỏ có gì hay vậy? Tôi xin giới thiệu: con trỏ là đặc quyền của C/C++, mà không ngôn ngữ nào khác có được đặc quyền này. Có một thằng objective-c được phát triển từ C cũng có, nhưng tôi chưa biết nó giống hay khác.

Vậy con trỏ đúng nghĩa là gì? Ở đây, nếu tôi không dùng dấu thì nó hiểu đó là kiểu char bình thường, giống như hộp sữa. Bạn gặp dấu thì hiểu rằng đó là kiểu dữ liệu con trỏ. Con trỏ hơn biến thông thường điểm nào? Tôi sẽ làm rõ như sau:

Giả sử bạn cần sử dụng biến để nhập ký tự, bạn dùng char. Một ký tự bạn khai báo như sau:

char <tên biến>

Ví dụ:

char conlon;
char contrau;
char hihi;

Thoải mái, dài bao nhiêu cũng được, nhưng mấy cái tên cần nhớ cho bạn nó để làm gì. Đằng sau, bạn thêm cho tôi một dấu ; để kết thúc quá trình đặt tên biến.

Vì nó lưu một ký tự nên bạn có thể gán giá trị cho biến này. Ví dụ, tôi có đoạn chương trình như sau:

char c;
c = 'm';

Tôi khai báo một biến c, sau đó gán biến c là ký tự m. Bạn để ký tự m trong hai dấu nháy đơn nhé.

Bây giờ để in ra ký tự c này, tôi viết:

printf("c = %c", c);

Chương trình đầy đủ:

#include <stdio.h>
void main() {
    char c;
    c = 'm';
    printf("c = %c", c);
}

Kết quả trên màn hình:

c = m

Bạn đã biết gõ lệnh chưa? Nếu chưa, vui lòng xem bài 1 nhé.

Bạn quay lại câu lệnh này tôi giải thích:

printf("c = %c", c);

%c là cái gì thế? %c là một cái nhãn, bạn hiểu rằng nó sẽ thay thế ký tự vào đó. C = character. Nó sẽ đưa giá trị của biến c vào %c này, nên nó mới in ra như trên. Em C quy định như vậy, thì mình phải tuân theo.

Ngoài %c, ta còn có một số nhãn khác:

  • %d: để in ra số nguyên (d = double)
  • %f: để in ra số thực. (f = float)

Nếu bạn muốn in ra hai số thập phân sau dấu phẩy, bạn thêm:

  • %2.2f: đằng trước bạn đặt bao nhiêu cũng được, đằng sau viết số 2 để in ra hai số sau dấu phẩy. Tương tự %d cũng vậy nhé.

Ví dụ: printf("số thực = %2.2f", biến_số_thực);

%p: in ra địa chỉ con trỏ (p = pointer)

Ví dụ: printf("địa chỉ của p = %p", con_trỏ_p);

Bạn thấy mệt chưa? Tôi có một lời khuyên như này: bạn đọc tới đâu trong bài của tôi thì bạn gõ code tới đó nhé. Học lập trình thì phải lập trình, như vậy mới nhớ được lâu. Gõ lại từng đoạn code của tôi đầy đủ nhé. Đọc qua thì nhanh quên lắm. Ok, hít một hơi và tiếp tục.

Tôi quay lại với vấn đề con trỏ. Vậy là với biến thông thường, bạn khai báo theo kiểu dữ liệu bạn muốn + tên biến bạn nghĩ ra. Lúc đó, máy tính của bạn sẽ cấp cho bạn một vùng nhớ để lưu. Hoàn toàn tự động. Bạn chỉ thấy được biến và giá trị của nó thôi. Thế còn bây giờ, bạn muốn xem biến đó nằm ở đâu trong máy tính của bạn? Bạn dùng cách này. Bạn thêm dấu & trước biến của bạn. Ví dụ &a sẽ lấy địa chỉ của bạn. Chương trình thí dụ:

void main() { char c; c = 'm'; printf("Địa chỉ của c = %p", &c); // bạn viết liền dấu & và c

Ở trên, bạn thay %c bằng %p để in ra địa chỉ. Bạn thêm dấu & trước c để lấy địa chỉ.

Kết quả như sau: Địa chỉ của c = 0028FF2F

Bạn thấy gì không? Kết quả là 0028FF2F. Trông hoa mắt chưa. Vừa số vừa chữ cơ.

Chữ cái F trên nghĩa là gì? Nó đang in ra hệ 16 bạn nhé. Hệ 10 bạn học từ hồi cấp 1 là các số từ 0 tới 9. Lên cấp 2, cấp 3 bạn học thêm hệ 16 với các số từ 0 tới 9 và các chữ cái: A để mô tả số 10, B = 11, C = 12, D = 13, E = 14, F = 15.

Ai quan tâm tới cách đổi cơ số 10 sang cơ số 16 không? Rảnh tôi sẽ hướng dẫn, tạm thời bài này bạn hiểu địa chỉ đó cũng là một cái số, số lớn nên dùng cơ số 16 để in cho tiện.

Vậy ví dụ trên, bạn hiểu biến c lưu giá trị m và nằm tại địa chỉ 0028FF2F. Trên máy bạn, địa chỉ này chắc chắn không giống tôi đâu.

Vậy, bạn đã hiểu rõ hơn về biến mà máy tính cấp cho 1 cái địa chỉ, 1 ô nhớ, 1 con số nằm trên vùng nhớ của máy tính. Và thường thì khi mới học lập trình, không quan tâm mấy tới địa chỉ này. Tuy nhiên, khi học sâu về C, bạn sẽ chơi với các địa chỉ này, nhảy qua địa chỉ này tới địa chỉ khác. Ví dụ:

#include <stdio.h>
void main() {
    char c1;
    char c2;
    char* pc;
    c1 = 'm';
    c2 = 'n';
    printf("Địa chỉ của c1 = %p\n", &c1); // Viết liền dấu & và c1
    printf("Địa chỉ của c2 = %p\n", &c2); // wp ngu, wp ngu :(((
    pc = &c1; // Viết liền dấu & và c1
    printf("Địa chỉ của pc = %p\n", pc);
    pc = &c2;
    printf("Địa chỉ của pc = %p\n", pc);
}

Bạn vui lòng viết từng đoạn lệnh trên vào notepad và build như bài trước. Nhớ là viết từng đoạn 1 cho tôi nhé. Bắt buộc đấy. Để các bạn nhớ lâu hơn.

Ở trên, tôi có thêm \n để xuống dòng in cho dễ đọc.

Kết quả như sau:

Rồi bây giờ ta phân tích từng đoạn một:

char c1;
char c2;
char* pc;

Tôi khai báo 2 biến thường và 1 biến con trỏ để lưu ký tự.

c1 = 'm';
c2 = 'n';

Tôi gán c1 cho ký tự m, c2 cho ký tự n.

printf("Địa chỉ của c1 = %p\n", &c1); // Viết liền dấu & và c1
printf("Địa chỉ của c2 = %p\n", &c2); // wp ngu, wp ngu :(((
pc = &c1; // Viết liền dấu & và c1
printf("Địa chỉ của pc = %p\n", pc);
printf("Giá trị của pc = %c\n", *pc);
pc = &c2;
printf("Địa chỉ của pc = %p\n", pc);
printf("Giá trị của pc = %c\n", *pc);

Tôi in địa chỉ của từng biến ra.

Tương tự, tôi làm với c2. Bây giờ bạn kéo lên xem lại kết quả.

Rõ ràng bạn thấy thằng pc nó là con trỏ và nhảy qua lại giữa 2 địa chỉ biến thông thường c1 và c2. Vậy rõ ràng biến con trỏ rất linh hoạt, nó có thể trỏ tới đâu cũng được. Bạn dùng câu lệnh sau để in ra giá trị của ông pc. Chương trình đầy đủ thêm 2 dòng:

#include <stdio.h>
void main() {
    char c1;
    char c2;
    char* pc;
    c1 = 'm';
    c2 = 'n';
    printf("Địa chỉ của c1 = %p\n", &c1); // Viết liền dấu & và c1
    printf("Địa chỉ của c2 = %p\n", &c2); // wp ngu, wp ngu :(((
    pc = &c1; // Viết liền dấu & và c1
    printf("Địa chỉ của pc = %p\n", pc);
    printf("Giá trị của pc = %c\n", *pc);
    pc = &c2;
    printf("Địa chỉ của pc = %p\n", pc);
    printf("Giá trị của pc = %c\n", *pc);
}

Kết quả như sau: Địa chỉ của c1 = 0028FF2F Địa chỉ của c2 = 0028FF2E Địa chỉ của pc = 0028FF2F Giá trị của pc = m Địa chỉ của pc = 0028FF2E Giá trị của pc = n

Bạn hãy xem kết quả và suy nghĩ xem nào. Suy nào suy nào...

Bạn đã thấy con trỏ mạnh mẽ chưa? Chưa đâu. Bạn còn phải học sâu hơn.

Tôi viết cũng hơi mệt rồi. Thôi ta kết thúc bài học luôn.

Quay lại với chương trình đầu tiên, như cái tiêu đề. Chúng ta sẽ tiếp tục ở phần 2 trong bài tiếp theo nhé các bạn.

Bài viết gốc được đăng tải tại quynhlaptrinhc.wordpress.com

Có thể bạn quan tâm:

Xem thêm Việc làm Developer hấp dẫn trên TopDev

1