Wednesday, April 27, 2011

Bí mật switch-case, có phải ai cũng biết?

Tôi tin chắc rằng bạn sẽ có trải nghiệm  thú vị về câu lệnh switch - case thông qua những gì mà tôi sẽ chia sẽ dưới đây ^^
I) Switch - case là gì?
switch - case là cấu trúc cho phép kiểm tra giá trị của 1 biến kiểu nguyên để thực hiện nhiều nhóm câu lệnh tương ứng khác nhau.
Hình 1: cú pháp lệnh switch - case
Hình 1 mô tả một cấu trúc switch - case hoàn chỉnh, biến điều khiển đầu vào là a và có kiểu nguyên, cấu trúc này có 3 khối lệnh, Block 1, Block 2Block 3.
Nếu a có giá trị 0 thì các các câu lệnh trong Block 1 sẽ được thực hiện, nếu a có giá trị 1 thì các câu lệnh trong Block 2 sẽ được thực hiện.
Ngược lại, nếu a khác 0 và khác 1 thì các câu lệnh trong Block 3 sẽ được thực hiện.

II) Switch - case có những bí mật nào?
Secret 1: Biểu thức được sử dụng trong switch phải là kiểu nguyên (integral type) int, char hoặc enum, ngoài ra các kiểu dữ liệu khác đều không hợp lệ khi sử dụng trong switch.
Hình 1: Lỗi khi sử dụng biến kiểu float trong switch.
Secret 2: Nếu một trường hợp "case" thỏa mãn thì chương trình sẽ thực hiện tất cả các câu lệnh bắt đầu từ vị trí "case" đó cho đến khi gặp lệnh break;
Hình 2: Chương trình thực hiện ngoài ý muốn nếu thiếu break;
Trong trường nhợp như Hình 2 vì kết thúc case 0 chương trình không gặp lệnh break; nên nó sẽ tiếp tục thực hiện case 1 cho đến khi gặp lệnh break; do đó kết quả thực hiện chương trình sẽ là:
Secret 3: Câu lệnh default có thể đặt bất cứ chỗ nào trong switch, không nhất thiết phải là cuối cùng trong switch.
Hình 3: bạn có thể đặt khối lệnh default ở bất mọi vị trí bạn muốn trong switch
Secret 4: Đối số theo sau case phải là hằng số:
Hình 4: theo sau case nhất định phải là hằng số
Đoạn chương trình như trong Hình 4 sẽ báo lỗi tại dòng lệnh "case var: ..." vì var là một biến kiểu int, nó không phải là một hằng số nên không thể đặt sau case.
Secret 5Không thể có hai trường hợp case có cùng giá trị tham chiếu giống nhau:
Hình 5: Lỗi vì case 1 xuất hiện 2 lần.

Congratulation! xem đến đây tức là bạn đã được trang bị đầy đủ về cú pháp của lệnh switch - case, tôi hi vọng cách tiếp cận này sẽ cho bạn một chút thú vị và ấn tượng hơn so với sự khô khan của một văn bản khoa học ^^!



Tuesday, April 26, 2011

Pham vi hoat dong cua bien

Giong nhu viec phan quyen trong xa hoi, trong lap trinh C cung vay, de tranh cac ham khong phan su thay doi gia tri cua cac ham khac C/C++ chi cho phep cac ham hoat dong trong mot pham vi nhat dinh (scope), neu muon thay doi gia tri cua ham khac thi phai co giao do lap trinh vien qui dinh.

Truoc het hay cung nhac lai ve bien toan cuc va bien cuc bo:

lam ro, khi truyen 1 doi so vao thi function se copy 1 ban sao tru khi ban truyen vao 1 dia chi

Tham chiếu trong C

Tham chiếu

Trong lập trình C sẽ có nhiều lúc bạn muốn xem địa chỉ của một biến, lúc này bạn chỉ cần thêm ký tự & trước tên biến để đọc địa chỉ nó.
Hình 1: Mô tả bộ nhớ được sử dụng trong chương trình C
Hãy hình dung mộ bộ nhớ có 16 ô nhớ được đánh địa chỉ tương ứng từ 0 đến 15, mỗi ô nhớ đều có giá trị ban đầu khác nhau, tôi biễu diễn bằng 16 màu sắc khác nhau.
Trong một chương trình C, khi tôi khai báo một biến kiểu char như sau:
char mau_sac;
mau_sac = 50;
Hệ điều hành sẽ cấp phát 1 ô nhớ chưa được sử dụng trên RAM cho biến mau_sac, giả sử ô nhớ thứ 5 (Hình 1) được cấp cho biến mau_sac. Việc cấp phát này do hệ điều hành thực hiện và bạn không hề hay biết.
Nếu muốn biết ô nhớ nào được cấp cho biết mau_sac bạn phải sử dụng tham chiếu, xem câu lệnh sau:
printf("o nho tai dia chi %d co gia tri %s\n",&mau_sac,mau_sac);
kết qủa in ra:
o nho tai dia chi 5 co gia tri 50
Vậy sử dụng ký tự & trước tên biến có thể giúp ta đọc được địa chỉ của biến trên bộ nhớ, kỹ thuật này gọi là tham chiếu.
Chú ý: Đối với mảng dạng tham chiếu của mảng là tên mảng mà không có ký tự &

int array[1024];
printf("Dia chi cua array: 0x%x",array);
// tham chiếu của mảng int array[1024] là array chứ không phải &array

Ứng dụng tham chiếu

1/ Tránh bay hơi: bạn đã biết rằng khi truyền một đối số vào Function thì đối số sẽ được copy vào Stack, các tính toán trên đối số sẽ được thực hiện trên stack, vậy nên sẽ xảy ra bay hơi giá trị, tức là giá trị tính toán trên đối số sẽ mất sau khi thoát ra khỏi Function.

Trong trường hợp này thay vì truyền một biến thì ta truyền địa chỉ (tham chiếu) của biến vào hàm, khi đó các tính toán sẽ được thực hiện và lưu lại tại vị trí thực của biến trên RAM chứ không phải tại vị trí copy trên Stack, do đó giá trị sau tính toán không bị bay hơi.

Lưu ý: Để sử truyền được tham chiếu của biến vào hàm ta cần khai báo biến đối số kiểu con trỏ, xem so sánh hai đoạn chương trình sau:


#include "stdio.h"

int my_double (int a)
{
    a = 2*a;
    return a;
}

int main ( void ){
    int val = 10;
    my_double(val);
    printf("val = %d\n\r",val);
return 0;
}
                                                        Source 1: Không dùng tham chiếu


#include "stdio.h"

int my_double (int *a)
{
    *a = 2*(*a);
    return *a;
}

int main ( void ){
    int val = 10;
    my_double(&val);
    printf("val = %d\n\r",val);
return 0;
}
                                                              Source 2: Dùng tham chiếu

Kết qủa in ra của hai đoạn chương trình trên là khác nhau, đoạn chương trình 1 biến val sẽ được copy vào stack và tính toán trên stack do đó giá trị của val không thay đổi sau khi gọi hàm my_double.
ngược lại ở đoạn chương trình 2 các tính toán trong hàm double được thực hiện trực tiếp tại vị trí ô nhớ của biến val trên RAM do đó giá trị val thay đổi sau khi gọi hàm my_double.

// Kết quả in ra của Source 1
val = 10

// Kết quả in ra của Source 2
val = 20
2/ Tránh tràn stack: khi một Funcion cần các biến có kích thước quá lớn
Ta hạn chế khai báo các biến cục bộ có dung lượng lớn (VD: int array[1024]) vì nó có thể làm tràn stack, trường hợp này ta có thể khai báo một biến toàn cục tương đương và truyền nó vào hàm dưới dạng tham chiếu, khi đó sẽ không ảnh hưởng đến dung lượng stack.

#include "stdio.h"

int my_array[1024];
void my_function (int array[])
{
 // code here
}

int main ( void ){
my_function(my_array);
return 0;
}
                                                   Source 3: Sử dụng tham chiếu tránh tràn stack


#include "stdio.h"

void my_function (void)
{
 int my_array[1024];
 // code here
}

int main ( void ){
my_function();
return 0;
}
                                            Source 4: Không sử dụng tham chiếu, có nguy cơ tràn stack

Trong đoạn chương trình 3 và 4 đều cần sử dụng mảng my_array[1024], nhưng viết theo cách của source 3 sẽ an toàn hơn vì nó k gây tràn stack.


So sánh swich - case và if - else?

Đề bài

Để tiếp cận vấn đề hiệu quả nhất, tôi sẽ đưa ra bài toán nhỏ như sau: "Viết chương trình nhập một số nguyên từ bàn phím, nếu nhập số 0 thì in ra màn hình 'mau xanh' nếu 1 thì in ra màn hình 'mau do', nếu khác 0 và 1 thì in ra màn hình 'gia tri nhap vao khong dung' ".

Sử dụng if-else

If - else là công cụ tuyệt vời trong trường hợp này:
Hình 1: Bài giải sử dụng lệnh if - else

Sử dụng switch-case

Nhưng chương trình cũng có thể được viết như sau:
Hình 2: Bài giải sử dụng lệnh switch - case

Tôi đã trình bày hai cách làm phổ biến nhất để giải quyết kiểu bài toán nhiều điều kiện đầu vào như đề bài. Hãy chắc chắn răng bạn đã biết rõ về lệnh switch case nhé: switch - case

So sánh if-else với switch-case

Đến đây sẽ có nhiều bạn tự hỏi tại sao lại có lệnh switch - case trong khi if - else hoàn toàn có thể giải quyết được vấn đề tương tự?

Cũng đề bài trên những bây giờ tôi bổ sung lại điều kiện như sau: "nhập số nguyên có giá trị từ 0 đến 99 và sau đó in ra 100 màu tương ứng khác nhau".
Trong trường hợp này, tôi sẽ giải thích vì sao lại phải có switch - case với 3 lý do chính như sau:
  • Switch - case sẽ giúp cấu trúc chương trình rõ ràng, dễ đọc, dễ hiểu hơn so với if-else.
  • Switch - case sẽ kiểm tra 1 biến để thực hiện 100 trường hợp khác nhau, còn if else phải kiểm tra 100 điều kiện để thực hiện 100 trường hợp khác nhau.
  • Giả sử số điều kiện lên đến 1000, nếu sử dụng if-else thì R.I.P cho anh lập trình viên ^^, vì phải viết rất nhiều biểu thức điều kiện có thể gây ra những nhầm lẫn không đáng có.
Đã có nhiều người muốn so sánh để tìm ra sự khác biệt về hiệu suất chương trình (tốc độ, khả năng tiêu tốn bộ nhớ ...) của hai câu lệnh swich-case và if-else nhưng hầu như không có sự khác biệt nào về hiệu suất giữa chúng.

Lời khuyên của tôi là: Trong trường hợp số điều kiện nhiều bạn nên dùng switch case, còn nếu số điều kiện ít việc dùng if-else hay switch-case đơn giản chỉ là sở thích của bạn. 

Monday, April 25, 2011

Định nghĩa Pointer

Trước khi bắt đầu tôi muốn chắc chắn rằng tất cả các bạn newbie phải thực sự tập trung vì phần này sẽ tương đối phức tạp và ngốn nhiều thời gian của các bạn.

Hinh 1: Going on or not?
Đó có phải là điều bạn thường nghe người ta nói về pointer không? Thôi nào, tôi chỉ đùa thôi, hãy quên tất cả những lời vớ vẩn đó đi. Bạn đang xem blog của tôi, một người chúa ghét và sẽ không bao giờ cho phép những định nghĩa phức tạp được phép xuất hiện trên blog này.

Đầu tiên bạn phải hiểu sơ bộ về bộ nhớ, đó là điều bắt buộc vì bạn sẽ không bao giờ hiểu được pointer nếu không nắm cơ bản về nơi nó tồn tại và làm việc.


Everything done? Okie, Let's go!

Tiếp theo chúng ta phải cùng trả lời một câu hỏi lớn: "Pointer ra đời nhằm mục đích gì?", trả lời câu hỏi này giúp bạn hình dung được vai trò quan trọng và có hứng thú hơn để khám phá Pointer, một công cụ mà giang hồ đồn là "khó".

Định nghĩa pointer

Để dễ hình dung hãy liên tưởng đến việc xem video trên youtube, chỉ có một video duy nhất được lưu trữ tại server của youtube nhưng nhiều người tại nhiều nơi khác nhau có thể xem cùng lúc video đó mà không cần quan tâm video đó ở đâu, do ai lưu giữ, tất cả những gì chúng ta cần là một đường Link "trỏ" tới video đó.

Hãy hình dung, variable chính là video tại server còn pointer chính là đường link trỏ vào video đó. Vậy, Pointer đơn giản chỉ là một địa chỉ để truy cập một variable, thông qua pointer lập trình viên có thể truy xuất và gán giá trị cho variable ở bất cứ nơi đâu họ muốn.

Khai báo Pointer

Khai báo pointer giống như khai báo variable và chỉ cần thêm giấu * phía trước tên của nó để đánh giấu nó là "đặc biệt", nó chưa địa chỉ của một varriable khác chứ không chứa data của người dùng:
<type> *<ptr name>;
V D:  int *p_so_nguyen;
Chú ý: Tên của pointer là "p_so_nguyen" chứ không phải là "*p_so_nguyen". Giấu * là một cú pháp bắt buộc của ngôn ngữ để phân biệt giữa pointer và variable.

Tip: Tôi khuyên bạn nên thêm tiền tố p_ trước tên của một pointer, điều đó sẽ khiến chương trình rõ ràng hơn, khi nhìn vào tên bạn có thể phân biệt ngay đâu là variable và đâu là pointer điều này sẽ giúp ích rất nhiều khi bạn viết một chương trình lớn.


Để hiểu hơn về Pointer bạn cần xem hai ứng dụng quan trọng của nó
Cơ bản, đó là tất cả những gì bạn cần biết về pointer, bạn có thể làm được cả đống chuyện với chừng đó kiến thức mà không cần phải tìm hiểu thêm bất cứ điều gì về pointer nữa. Gút lấc ^^!

Tại sao ngôn ngữ lập trình C lại có con trỏ?

Trước khi con trỏ ra đời, một chương trình C chỉ được phép sử dụng một lượng bộ nhớ cố định được cấp phát ngay từ đầu (Compiler time), trong khi chương trình đang làm việc (Run time) không có cách nào để thay đổi dung lượng bộ nhớ chương trình được phép sử dụng.
Vì vậy, ngay từ đầu, để đảm bảo chương trình luôn làm việc đúng lập trình viên phải cấp phát một lượng bộ nhớ tối đa mà chương trình có thể sử dụng, vấn đề là không phải lúc nào chương trình cũng sử dụng hết dung lượng tối đa đó, dẫn đến lãng phí và không hiệu quả trong sử dụng bộ nhớ.

Thực tế, cách thức tổ chức bộ nhớ của máy tính lại hoàn toàn cho phép lập trình viên chỉ cần cấp phát một dung lượng tối thiểu ngay từ đầu, đủ để chương trình hoạt động đúng, sau đó, trong khi chương trình đang làm việc nếu cần ta có thể cấp phát thêm hoặc giải phóng bớt dung lượng bộ nhớ phục vụ cho chương trình. 

Con trỏ (Pointer) chính là công cụ ra đời để tận dụng thực tế đó, con trỏ ra đời có ý nghĩa rất lớn trong việc sử dụng bộ nhớ, giúp chương trình gọn hơn, nhanh hơn tăng cường sức mạnh của toàn hệ thống.