Trong phần này, chúng tôi giới thiệu sơ lược về con trỏ trong C và ứng dụng trong truyền tham số cho hàm.
4.2.1 ðịa chỉ
Liên quan ñến một biến ta đã có khái niệm: - Tên biến
- Kiểu biến - Giá trị của biến Ví dụ câu lệnh
float alpha = 30.5;
xác định một biến có tên là alpha có kiểu float và có giá trị 30.5. Ta cũng đã biết, theo khai báo trên, máy sẽ cấp phát cho biến alpha một khoảng nhớ gồm 4 byte liên tiếp.
ðịa chỉ của biến là số thứ tự của byte ñầu tiên của vùng nhớ của biến trong bộ nhớ. Một ñiều cần lưu ý là mặc dù ñịa chỉ của biến là một số ngun nhưng khơng được đánh đồng nó với các số nguyên thông thường dùng trong các phép tính.
ðể lấy địa chỉ của một biến, dùng phép tốn &. Ví dụ để lấy địa chỉ của biến alpha.
&alpha
4.2.2 Con trỏ
Con trỏ là biến dùng ñể chứa ñịa chỉ, có nghĩa là giá trị của con trỏ là ñịa chỉ của một biến nào đó.
Vì có nhiều loại địa chỉ nên cũng có nhiều kiểu con trỏ tương ứng. Con trỏ kiểu int dùng ñể chứa ñịa chỉ các biến kiểu int. Tương tự, ta có con trỏ kiểu float, kiểu double,... . Cũng như ñối với bất kỳ một biến nào khác, một con trỏ cần khai báo trước khi sử dụng.
Việc khai báo biến con trỏ ñược thực hiện theo mẫu sau:
<kiểu> *<tên con trỏ>;
Ví dụ câu lệnh sau khai báo hai biến kiểu int là x, y và hai con trỏ kiểu int là px và py.
int x, y, *px, *py;
Tương tự câu lệnh sau khai báo các con trỏ kiểu float.
float f, *pf;
Khi đã có các khai báo trên thì các câu lệnh sau hồn tồn xác định.
px = &x; py = &y;
Câu lệnh thứ nhất sẽ gán ñịa chỉ của x cho con trỏ px và câu lệnh thứ hai sẽ gán ñịa chỉ của biến y cho con trỏ py.
Khi con trỏ px chứa địa chỉ của biến x thì ta nói px trỏ tới x.
Chúng ta khơng được gán ñịa chỉ của một biến nguyên cho một con trỏ kiểu thực. Ví dụ câu lệnh sau là khơng hợp lệ.
http://www.ebook.edu.vn 74
4.2.3 Qui tắc sử dụng con trỏ trong các biểu thức
Với một biến, có hai đại lượng liên quan, đó là giá trị của biến và địa chỉ của biến. Khi một con trỏ trỏ tới một biến, khi đó cũng có hai đại lượng liên quan, đó là giá trị con trỏ (chính là địa chỉ của biến) và giá trị của biến mà con trỏ ñang trỏ tới.
Trong biểu thức, tên con trỏ ñại diện cho giá trị của con trỏ và dạng khai báo ñại diện cho giá trị biến mà con trỏ ñang trỏ tới.
Sử dụng tên con trỏ
Con trỏ cũng là một biến nên khi tên của nó xuất hiện trọng một biểu thức thì giá trị của nó sẽ được sử dụng trong biểu thức này. Khi tên con trỏ ñứng ở bên trái của một tốn tử gán thì giá trị của biểu thức bên phải (giá trị này phải là ñịa chỉ) ñược gán cho con trỏ.
Ta hãy xem các câu lệnh sau làm gì ?
float a, *p, *q; p = &a;
q = p;
Câu lệnh thứ nhất khai báo một biến kiểu float (biến a) và hai con trỏ p và q kiểu float. Câu lệnh thứ hai sẽ gán ñịa chỉ của biến a cho con trỏ p và câu lệnh thứ ba sẽ gán giá trị của p cho q. Két quả là con trỏ q chứa ñịa chỉ của biến a.
Sử dụng dạng khai báo của con trỏ
Xét ñoạn lệnh sau: float x, y, *px, *py; x = 100.0; y = 215.3; px = &x; py = &y;
Sau các lệnh gán thì px trỏ tới x, py trỏ tới y. Khi đó biến mà px trỏ tới (biến x) được biểu diễn dưới dạng *px.
Nói một cách khác, nếu con trỏ px trỏ tới biến x thì các cách viết x và *px
là tương ñương trong mọi ngữ cảnh.
Theo nguyên lý này thì ba câu lệnh sau đều có hiệu lực như nhau
y = 3*x + 1; *py = 3*x + 1; *py = 3*(*px) + 1;
Từ đây có thể rút ra một kết luận quan trong là: Khi biết được địa chỉ của một biến thì chẳng những chúng ta có thể sử dụng giá trị của nó mà cịn có thể gán cho nó một giá trị mới (làm thay đổi nội dung của nó). ðiều này sẽ ñược áp dụng như một phương pháp chủ yếu ñể nhận kết quả của hàm thông qua ñối.
4.2.4 Mảng và con trỏ
Khi khai báo một mảng, máy cấp phát một vùng nhớ dùng ñể lưu giá trị các phần tử của mảng.
Ví dụ khai báo một mảng số ngun:
http://www.ebook.edu.vn 75
Khi đó máy sẽ cấp phát 100 ô nhớ, mỗi ơ nhớ có kích thước của một số nguyên tương ứng với một phần tử, và các phần tử sẽ sắp xếp liên tiếp nhau trong bộ nhớ, từ phần tử đầu tiên.
Trong C, tên mảng chính là ñịa chỉ của phần tử ñầu tiên của mảng, ñiều có nghĩa là: a bằng &a[0]
Vì tên mảng là một địa chỉ, nên ta có thể gán cho một con trỏ. Ví dụ:
int *p; p = a;
Như vậy giữa mảng con trỏ có sự tương đồng. Do ñó, trong C cho phép sử dụng thay thế giữa mảng và con trỏ trong một số trường hợp.
Ví dụ, sau khi đã gán p bằng a thì:
a[4] và p[4] ñều biểu diễn phần tử thứ 5 của mảng. a[0], *a và *p cùng biểu diễn phần tử ñầu tiên của mảng.
Tuy nhiên giữa mảng và con trỏ có sự khác nhau. Con trỏ là một biến, vì thế giá trị của nó có thể thay đổi ñược và ta có thể gán giá trị cho một con trỏ. Trong khi đó tên mảng là một hằng ñịa chỉ, vì khi mảng ñược cấp phát thì ñịa chỉ của phần tử đầu tiên là xác định. Vì vậy khơng thể thay đổi được giá trị của tên mảng.
4.2.5 Hàm có đối con trỏ
ðối con trỏ thường ñược sử dụng trong các trường hợp sau ñây: - Sử dụng hàm ñể thay ñổi giá trị của một biến.
- Truyền một mảng vào cho hàm. - Trả về nhiều kết quả.
Sử dụng hàm ñể thay ñổi giá trị của một biến
Như chúng ta ñã biết, khi truyền tham số thực cho hàm, thì chỉ có giá trị của các biểu thức là ñược sử dụng. Nếu chúng ta thay ñổi giá trị của đối số bên trong hàm, thì sự thay đổi đó khơng ảnh hưởng ñến giá trị các biến truyền vào cho hàm.
Vì vậy, nếu muốn dùng hàm để thay ñổi giá trị của một biến, thì phải truyền ñịa chỉ của biến thay vì giá trị của biến.
ðể khai báo một tham số là ñịa chỉ của một biến, ta khai báo tham số đó là một con trỏ có kiểu tương ứng. Ví dụ sau định nghĩa một hàm cho phép ñảo giá trị của hai biến.
#include <stdio.h>
void hoan_vi(float *px, float *py) { float z; z = *px; *px = *py; *py = z; } void main() { float a = 7.6, b = 13.5; hoan_vi(&a, &b); printf("\na = %0.2f b = %0.2f", a, b); }
http://www.ebook.edu.vn 76
Kết quả thực hiện chương trình
a = 13.50 b = 7.60
Ta hãy xem hàm hoán_vị làm việc thế nào. Như đã biết, chương trình bắt đầu từ câu lệnh ñầu tiên trong hàm main(). Kết qủa là biến a nhận giá trị 7.6 và biến b nhận giá trị 13.5. Tiếp đó là lời gọi hàm hoan_vi. Máy sẽ gán giá trị của các tham số thực cho ñối tương ứng. Như vậy ñịa chỉ của a ñược gán cho con trỏ px , ñịa chỉ của b ñược gán cho con trỏ py. Sau ñó máy lần lượt xét ñến các câu lệnh trong thân hàm. Câu lệnh thứ nhất sẽ cấp phát cho biến cục bộ z một khoảng nhớ 4 byte. Theo qui tắc về sử dụng con trỏ nêu trong mục 4.2.3 thì ba câu lệnh tiếp theo tương ñương với các câu lệnh:
z = a; a = b; b = z;
Như vậy a sẽ nhận giá trị của b và ngược lại. Tiếp đó, máy trở về hàm main() và in ra những dịng kết qủa như đã chỉ ra ở trên.
Truyền một mảng một chiều vào cho hàm
Khi muốn truyền một mảng vào cho một hàm, ví dụ như hàm in các số hạng của một dãy số, khi đó ta dùng đối con trỏ.
Do mảng là ñịa chỉ của phần tử ñầu nên kiểu của ñối là kiểu con trỏ của kiểu phần tử mảng. Ví dụ, với mảng các phần tử int thì kiểu đối là con trỏ kiểu int.
Khi truyền mảng vào cho hàm, bên trong hàm không rõ số lượng phần tử của mảng, do đó cần thêm đối chỉ ra số lượng phần tử của mảng.
Ví dụ sau định nghĩa hai hàm, một hàm nhập dữ liệu cho một dãy số và một hàm in dãy số đó ra màn hình.
#include <stdio.h> // Nguyen mau ham
void NhapDaySo(int *a, int n); void InDaySo(int *a, int n); void main() {
int a[100]; int n;
printf("Nhap so phan tu: "); scanf("%d", &n);
// Nhap day so NhapDaySo(a, n);
printf("Day so vua nhap:\n"); // In day so
InDaySo(a, n); }
void NhapDaySo(int *a, int n) { int i; for (i = 0; i < n; i++) { printf("a[%d] = ", i + 1); scanf("%d", &a[i]); } }
http://www.ebook.edu.vn 77
void InDaySo(int *a, int n) {
int i;
for (i = 0; i < n; i++) printf("%8d", a[i]); }
Trong ví dụ trên, các tham số của hai hàm NhapDaySo và InDaySo ñược khai báo giống nhau, bao gồm một tham số kiểu con trỏ nguyên và một tham số nguyên.
Tuy nhiên, trong hai trường hợp ý nghĩa tham số là khác nhau. - Tham số a trong hàm NhapDaySo là kết quả ñưa ra. - Tham số a trong hàm InDaySo là dữ liệu ñầu vào.
Về mặt cú pháp, chúng ta không phân biệt ñược sự khác nhau ở trên, mà chỉ phân biệt ñược theo mục đích sử dụng.
Vì vậy, khi truyền một mảng vào cho hàm mà trong hàm khơng có nhu cầu thay ñổi giá trị của các phần tử của mảng, thì có thể thêm từ khố const khi khai báo tham số. Ví dụ, hàm InDaySo có thể được định nghĩa như sau:
void InDaySo(const int *a, int n) {
int i;
for (i = 0; i < n; i++) printf("%8d", a[i]); }
Khi đó thì có thể xác định ngay tham số a là dữ liệu vào cho hàm.
Truyền một mảng nhiều chiều vào cho hàm
Trong một số trường hợp, chúng ta phải truyền một mảng nhiều chiều vào cho hàm, ví dụ như truyền một ma trận vào hàm in ma trận.
Khi khai báo một tham số là một mảng nhiều chiều, cần phải chỉ ra số phần tử tương ứng của các chiều, trừ chiều đầu tiên là khơng cần thiết phải chỉ ra.
Ví dụ sau nhập in một ma trận thực theo dạng bảng, sử dụng cả hai cú pháp khai báo là mảng và con trỏ.
#include <stdio.h> // Nguyen mau ham
void NhapMaTran(float a[20][20], int, int); // Su dung dang khai bao mang
void InMaTran(float a[20][20], int, int); void main()
{
float a[20][20]; int m, n;
printf("Nhap so hang, so cot: "); scanf("%d%d", &m, &n);
// Nhap ma tran NhapMaTran(a, m, n); // In ma tran InMaTran(a, 3, 3); }
http://www.ebook.edu.vn 78
void NhapMaTran(float a[20][20], int m, int n) { int i, j; float f; for (i = 0; i < m; i++) for (j = 0; j < n; j++) { printf("a[%d][%d] = ", i + 1, j + 1); scanf("%f", &f); a[i][j] = f; } }
void InMaTran(float a[20][20], int m, int n) { int i, j; for (i = 0; i < m; i++) { for (j = 0; j < n; j++) printf("%8.2f", a[i][j]); printf("\n"); } }
Chú ý: một số trình dịch khơng cho phép lấy địa chỉ của một phần tử mảng nhiều chiều,
do đó chúng ta phải dùng một biến trung gian khi nhập giá trị các phần tử mảng, như trong hàm NhapMaTran ở trên.
Trả về nhiều kết quả
Một trường hợp mà ñối là con trỏ thường ñược sử dụng đó là khi chúng ta muốn hàm trả về nhiều giá trị. Như chúng ta đã biết, hàm chỉ có thể trả về một giá trị dưới dạng tên hàm, do đó để trả về nhiều giá trị chúng ta phải sử dụng tham số là con trỏ.
Ví dụ: khi viết một hàm giải phương trình bậc hai, thì chúng ta cần phải trả về các đại lượng:
- Hàm đó có bao nhiêu nghiệm (0, 1, 2).
- Nếu có nghiệm, thì giá trị của nghiệm là bao nhiêu.
Như vậy ở ñây chúng ta phải trả về ba ñại lượng: số nghiệm, một hoặc hai nghiệm (trong trường hợp có nghiệm).
Ví dụ sau thể hiện cách chúng ta trả về nhiều kết quả.
/* Chuong trinh giai phuong trinh bac hai */ #include <stdio.h>
#include <math.h> // Nguyen mau ham
int GiaiPTBac2(float, float, float, float*, float*); void main()
{
float a, b, c, x1, x2; int songhiem;
printf("Nhap a, b, c: "); scanf("%f%f%f", &a, &b, &c); // Giai phuong trinh
http://www.ebook.edu.vn 79
songhiem = GiaiPTBac2(a, b, c, &x1, &x2); if (songhiem == 0)
printf("Phuong trinh vo nghiem."); else if (songhiem == 1)
printf("Nghiem kep: %.3f", x1); else
printf("x1: %.3f\nx2: %.3f", x1, x2); }
int GiaiPTBac2(float a, float b, float c, float *px1, float *px2) { float delta; delta = b*b - 4*a*c; if (delta < 0) return 0; else if (delta == 0) { *px1 = -b/(2*a); return 1; } else { *px1 = (-b - sqrt(delta))/(2*a); *px2 = (-b + sqrt(delta))/(2*a); return 2; } } 4.3 ðệ quy
4.3.1 Khái niêm chung về ñệ quy
C không những cho phép từ hàm này gọi tới hàm khác, mà nó cịn cho phép từ một vị trí trong thân của một hàm gọi tới chính hàm đó. Hàm như vậy gọi là hàm ñệ quy. Trong một số trường hợp, sử dụng ñệ quy sẽ làm chương trình ngắn gọn, dễ hiểu hơn.
Khi hàm gọi đệ quy đến chính nó thì mỗi lần gọi, máy sẽ tạo ra một tập các biến cục bộ mới hồn tồn độc lập với các tập biến (cục bộ) ñã ñược tạo ra trong các lần gọi trước.
Ta cũng chú ý: rằng: Có bao nhiêu lần gọi tới hàm thì cũng có bấy nhiêu lần thốt ra khỏi hàm và cứ mỗi lần ra khỏi hàm thì một tập các biến cục bộ bị xóa. Sự tương ứng giữa các lần gọi tới hàm và các lần ra khỏi hàm ñược thực hiện theo thứ tự ngược, nghĩa là: Lần ra ñầu tiên ứng với lần vào cuối cùng và lần ra khỏi hàm cuối cùng ứng với lần ñầu tiên gọi tới hàm.
ðể minh họa những điều nói trên, ta đưa ra một ví dụ ñơn giản. Giả sử ta cần viết một hàm tính n giai thừa. Thơng thường ta sẽ viết như sau:
long giai_thua(int n) { long s; int i; for (s = 1, i = 1; i <= n; i++) s*= i; return s; }
Một cách khác, ta thấy rằng n! có thể tính theo cơng thức truy hồi như sau: n!= 1 nếu n = 0
n!= n*(n-1)! nếu n > 0
http://www.ebook.edu.vn 80 #include <stdio.h> long giai_thua(int n) { return (n > 0)?n*giai_thua(n - 1) : 1; } void main() { printf("%d", giai_thua(5)); }
Cơ chế hoạt ñộng của hàm giai_thua trong chương trình trên có thể giải thích như sau: - Trong hàm main, có một lời gọi hàm giai_thua(5) để tính giai thừa của 5. Khi đó
hàm giai_thua sẽ ñược gọi ra ñể thực hiện với tham số n bằng 5.
- Trong hàm giai_thua, do n lớn hơn 0 nên giá trị trả về là n*giai_thua(n -1), tức là 5*giai_thua(4). Tuy nhiên lúc này giá trị giai_thua(4) chưa xác ñịnh, nên máy lại tiếp tục gọi hàm giai_thua để tính giai thừa của 4.
- Trong lần gọi thứ hai này, do n vẫn lớn hơn 0, nên tiếp tục cần tính giai_thua(3). - Cứ như vậy, cho tới khi n bằng 0, khi đó giá trị trả về của lần gọi giai_thua(0) là 1
và ñược sử dụng để tính biểu thức trả về cho lần gọi giai_thua(1) bằng 1*giai_thua(0), bằng 1*1.
- Giá trị giai_thua(1) bằng 1, lại được sử dụng để tính biểu thức trả về cho lần gọi giai_thua(2), bằng 2*1.
- Cứ như vậy, cuối cùng giá trị trả về của lời gọi giai_thua(5) trong main có giá trị bằng 5*4*3*2*1*1.
4.3.2 Cách dùng đệ quy
Bài tốn nào có thể dùng ñệ quy
Phương pháp ñệ quy thường áp dụng cho các bài tốn phụ thuộc tham số có hai đặc ñiểm sau:
- Bài toán dễ dàng giải quyết trong một số trường hợp riêng ứng với các giá trị ñặc biệt của tham số. Ta gọi ñây là trường hợp suy biến.
- Trong trường hợp tổng qt, bài tốn có thể quy về một bài tốn cùng dạng nhưng giá trị tham số thay ñổi. Và sau một số hữu hạn bước biến ñổi ñệ quy, sẽ dẫn tới trường hợp suy biến.
Ta nhận thấy bài tốn tính n! nêu trên thể hiên rất rõ các ñặc ñiểm này.
Cách xây dựng hàm ñệ quy
Hàm đệ quy thường được viết theo thuật tốn sau:
if (trường hợp suy biến) {
trình bày cách giải bài tốn (giả định đã có cách giải) }
else /* trường hợp tổng quát */