Vì sao cần hàm
Đến giờ mọi thứ đều nằm trong main. Cách đó ổn cho tới khi chương trình làm nhiều hơn một việc, và lúc ấy ba vấn đề ập đến cùng lúc: cùng một khối code bị chép ra ba chỗ, main dài quá mức ai cũng có thể nắm trong đầu, và không có cách nào đặt tên cho mục đích của một đoạn code.
Hàm (function) giải quyết cả ba. Nó đặt tên cho một khối code, cho khối đó biến cục bộ riêng, một danh sách đầu vào và một giá trị duy nhất để trả về.
square được viết một lần và gọi ba lần, trong đó có một lần gọi trên chính kết quả của nó. Thật ra bạn đã gọi hàm từ trước: printf là một hàm, và main cũng vậy.
Cấu tạo của một hàm
returnType name(parameterList) {
// body
return value;
}
Bốn phần:
- Kiểu trả về: kiểu của giá trị mà hàm trả về.
int,double,char, một kiểu con trỏ, hoặcvoidcho "không có gì". - Tên: tuân theo quy tắc giống tên biến, và nên nói lên hàm làm gì hoặc tạo ra gì.
- Danh sách tham số: các đầu vào, mỗi cái có kiểu riêng, ngăn cách bằng dấu phẩy.
(void)nghĩa là không nhận gì. - Thân hàm: các câu lệnh trong ngoặc nhọn, kết thúc bằng
returntrừ khi kiểu trả về làvoid.
Lưu ý pi() vẫn cần cặp ngoặc rỗng tại chỗ gọi: chính cặp ngoặc biến nó thành một lời gọi thay vì chỉ nhắc tới tên. Còn printBanner(20); đứng riêng thành một câu lệnh, vì nó không tạo ra giá trị nào để dùng.
Hãy viết (void), không phải (), cho hàm không có tham số. Trong C hai cách viết này mang nghĩa khác nhau, và () là cách lỏng lẻo hơn; ta sẽ quay lại chuyện này ngay sau đây.
Gọi hàm và trả về
Một lời gọi chuyển quyền điều khiển vào hàm, chạy thân hàm, rồi quay lại với một giá trị thế chỗ cho lời gọi trong biểu thức xung quanh.
return làm hai việc cùng lúc: cung cấp giá trị và kết thúc hàm ngay tại chỗ. Mọi thứ sau return trên cùng nhánh chạy sẽ không bao giờ được thực thi, và đó là lý do kiểu return sớm trong larger hoạt động: không cần else, vì đã tới được dòng cuối thì chắc chắn a > b là sai.
Một hàm có thể có nhiều câu lệnh return. Nhưng mỗi lần gọi chỉ thực thi được một trong số đó.
Hàm void
Hàm tồn tại vì tác dụng của nó chứ không phải vì giá trị thì có kiểu trả về void: in ra màn hình, cập nhật thứ gì đó qua con trỏ, vẽ.
Bên trong hàm void, return; không kèm giá trị sẽ thoát sớm. Viết return someValue; ở đó là lỗi biên dịch, và dùng lời gọi trong một biểu thức cũng vậy: int x = printTable(7, 5); sẽ không biên dịch được, đó là hệ thống kiểu đang làm đúng việc của nó.
Chia nhỏ chương trình: ý nghĩa thật sự
Hàm không chủ yếu để tránh lặp code. Hàm giúp biến một quy trình dài thành vài bước có tên, để main đọc như một bản mô tả chương trình thay vì phần cài đặt chi tiết.
main giờ chỉ còn bốn dòng in ra và một phép tính. Mỗi hàm phụ làm một việc, có tên nói rõ việc đó là gì, và có thể kiểm thử riêng. Để ý rằng độ dài mảng được truyền bằng tham số thứ hai: khi mảng đã được truyền vào hàm, mẹo sizeof không còn dùng được ở đó nữa, vì mảng suy biến thành con trỏ.
Ba nguyên tắc kinh nghiệm khi chia việc thành các hàm:
- Mỗi hàm một việc. Nếu tên hàm cần chữ "và", có lẽ đó là hai hàm.
- Nhận thứ cần dùng, trả về thứ tạo ra. Hàm đọc trạng thái toàn cục khó suy luận hơn và không thể kiểm thử riêng lẻ.
- Đủ ngắn để nhìn một lần là thấy hết. Không có con số dòng thần kỳ nào, nhưng một hàm không vừa một màn hình thường đang giấu một hàm nhỏ hơn bên trong.
Hàm gọi hàm
Một hàm có thể gọi bất kỳ hàm nào đã được khai báo, kể cả những hàm bạn tự viết:
main gọi report, report gọi sumEvens, và sumEvens gọi isEven trong một vòng lặp. Mỗi lời gọi có một bộ biến cục bộ mới của riêng nó, xếp chồng lên trên bộ biến của bên gọi, và bị hủy khi hàm trả về.
Các định nghĩa ở trên được xếp theo thứ tự phụ thuộc, isEven trước sumEvens trước report, vì C yêu cầu hàm phải được khai báo trước khi được gọi. Gọi một hàm nằm phía dưới trong tệp là trình biên dịch sẽ báo lỗi implicit declaration. Cách sửa chung là đặt một prototype gần đầu tệp, nhờ đó bạn sắp xếp các định nghĩa theo thứ tự nào cũng được. Hàm gọi chính nó cũng hợp lệ và rất hữu ích; đó là đệ quy.
main cũng là một hàm
main là một hàm bình thường mà runtime gọi giúp bạn. Kiểu trả về int của nó là mã thoát của chương trình: 0 nghĩa là thành công, khác 0 nghĩa là thất bại, và các script shell lẫn công cụ build đều đọc giá trị này.
int main(void) {
/* ... */
return 0;
}
Từ C99, chạy hết main mà không có return sẽ ngầm trả về 0, một quy tắc đặc biệt chỉ áp dụng cho main. Mọi hàm không phải void khác mà kết thúc không trả về giá trị sẽ gây ra undefined behavior khi bên gọi dùng kết quả.
Dạng chuẩn còn lại là int main(int argc, char *argv[]), nhận các đối số dòng lệnh. Cả hai đều đúng; void main() thì không, dù các hướng dẫn cũ có nói gì đi nữa.
Lỗi thường gặp
- Quên
returntrong hàm không phải void. Bên gọi khi đó đọc phải giá trị rác.gcc -Wallsẽ cảnh báo. - Khai báo biến trùng tên với tham số bên trong thân hàm. Biến đó che khuất tham số, và phép gán bạn định thực hiện cho đầu vào chẳng đi đến đâu.
- Mong hàm thay đổi được đối số của nó. C truyền mọi thứ theo giá trị, nên hàm nhận bản sao.
void reset(int x) { x = 0; }không thay đổi gì tại chỗ gọi; đây là chủ đề của trang tiếp theo. - Bỏ cặp ngoặc khi gọi hàm.
printBanner;là một biểu thức hợp lệ, lấy địa chỉ của hàm rồi bỏ đi. Nó biên dịch được, không làm gì cả, và-Wallsẽ đánh dấu nó. - Viết
()thay vì(void). Hợp lệ, nhưng tắt việc kiểm tra đối số trong các phương ngữ C cũ.
Câu hỏi thường gặp
Khai báo hàm trong C như thế nào?
Viết kiểu trả về, tên hàm và danh sách tham số trong ngoặc tròn, rồi đến thân hàm trong ngoặc nhọn: int add(int a, int b) { return a + b; }. Hàm không nhận gì thì dùng (void), còn hàm không trả về gì có kiểu trả về void.
return làm gì trong một hàm C?
Nó kết thúc hàm ngay lập tức và trả một giá trị về cho bên gọi. Kiểu của giá trị phải khớp với kiểu trả về đã khai báo của hàm. Trong hàm void, bạn có thể viết return; không kèm giá trị để thoát sớm, hoặc bỏ hẳn và để hàm kết thúc ở dấu ngoặc nhọn đóng.
Vì sao main trả về int trong C?
Giá trị trả về của main là mã thoát (exit status) của chương trình, mà hệ điều hành và các script shell có thể đọc. return 0; nghĩa là thành công, còn mọi giá trị khác 0 nghĩa là thất bại. Từ C99, chạy tới cuối main mà không có câu lệnh return sẽ ngầm trả về 0.