Vì sao cần hàm
Hàm (function) là một khối code có tên mà bạn có thể chạy bất cứ lúc nào bằng cách gọi nó. Thay vì lặp lại cùng một đoạn logic ở nhiều nơi, bạn viết nó một lần, đặt tên, rồi gọi tên đó ở bất cứ đâu cần dùng. Nhờ vậy chương trình ngắn hơn, dễ đọc hơn và dễ sửa hơn: chỉ cần đổi logic ở một chỗ là mọi nơi gọi hàm đều được cập nhật.
Thật ra từ đầu đến giờ bạn đã luôn gọi một hàm: main. Đây là điểm bắt đầu của mọi chương trình C++. Giờ bạn sẽ tự viết hàm của mình. Các vòng lặp bạn đã gặp trước đó, như vòng lặp range-based for, thường nằm bên trong hàm để bạn dùng lại cả một đoạn logic chỉ qua tên của nó.
Cấu trúc của một hàm
Mỗi hàm có bốn phần: kiểu trả về, tên, danh sách tham số trong cặp ngoặc tròn và thân hàm trong cặp ngoặc nhọn.
int add(int a, int b) { // return type | name | parameters
return a + b; // body
}
intlà kiểu trả về, tức loại giá trị mà hàm trả lại.addlà tên bạn dùng để gọi hàm.(int a, int b)là các tham số, tức đầu vào do nơi gọi cung cấp.- Cặp ngoặc nhọn chứa thân hàm, phần code chạy khi bạn gọi hàm.
Dưới đây là hàm đó trong một chương trình hoàn chỉnh. Định nghĩa hàm phía trên main giúp main nhìn thấy hàm khi gọi.
Lời gọi add(2, 3) chạy hàm với a = 2 và b = 3, và cả biểu thức đó trở thành giá trị được trả về. Bạn có thể lưu giá trị này vào một biến hoặc dùng thẳng trong một biểu thức khác, như dòng cout thứ hai.
Trả về giá trị
Câu lệnh return làm hai việc: trả một giá trị về cho nơi gọi, và kết thúc hàm ngay lập tức. Mọi code nằm sau một return đã chạy sẽ không được thực thi; chương trình quay thẳng về chỗ đã gọi hàm.
Kiểu của giá trị trả về phải khớp với (hoặc chuyển đổi được sang) kiểu trả về đã khai báo. Hàm khai báo là int thì phải trả về một int; không trả về gì, hoặc chạy hết hàm mà không có return, là bug với mọi hàm không phải void.
Hàm void
Không phải hàm nào cũng tạo ra giá trị. Khi hàm chỉ làm một việc gì đó, như in ra kết quả hay cập nhật trạng thái, kiểu trả về của nó là void. Hàm void có thể dùng return; không kèm giá trị để thoát sớm, hoặc cứ chạy đến dấu ngoặc nhọn đóng.
Cố dùng kết quả của một hàm void, chẳng hạn int x = greet("Ada");, sẽ gây lỗi biên dịch, vì không có giá trị nào để gán. Một lỗi hay gặp là viết return someValue; bên trong hàm void; trình biên dịch cũng từ chối trường hợp này.
Khai báo và định nghĩa
C++ đọc tệp từ trên xuống dưới, nên mặc định một hàm phải xuất hiện trước đoạn code gọi nó. Khi thứ tự đó gây bất tiện, bạn tách hàm thành khai báo (còn gọi là prototype) và định nghĩa.
Khai báo nêu signature của hàm và kết thúc bằng dấu chấm phẩy, không có thân hàm. Nó hứa với trình biên dịch rằng "hàm này tồn tại; đây là cách gọi nó." Phần định nghĩa đầy đủ có thể đến sau, kể cả sau main.
Nếu không có prototype ở dòng 4, trình biên dịch sẽ gặp square(5) bên trong main trước khi biết square là gì, và quá trình build sẽ thất bại. Prototype cũng là cách các tệp header giúp nhiều tệp mã nguồn dùng chung các hàm. Lưu ý rằng tên tham số trong khai báo là không bắt buộc: int square(int); vẫn chạy tốt, vì trình biên dịch chỉ quan tâm đến kiểu.
Lỗi thường gặp
Vài cái bẫy khiến người mới bắt đầu vấp phải hết lần này đến lần khác:
- Gọi trước khi khai báo. Nếu bạn gặp lỗi "
addwas not declared in this scope", hàm đang được định nghĩa bên dưới lời gọi đầu tiên và không có prototype. Hãy chuyển phần định nghĩa lên trên hoặc thêm prototype. - Quên return. Chạy đến cuối một hàm không phải
voidmà không córeturnlà undefined behavior: nơi gọi nhận về giá trị rác. Hãy biên dịch với cảnh báo được bật (-Wall), trình biên dịch sẽ báo cho bạn. - Nhầm định nghĩa với khai báo. Định nghĩa có thân hàm trong ngoặc nhọn và không có dấu chấm phẩy ở cuối. Khai báo có dấu chấm phẩy và không có thân hàm. Nhầm lẫn hai thứ này, ví dụ đặt dấu chấm phẩy ngay sau danh sách tham số của một hàm bạn định định nghĩa, sẽ sinh ra những lỗi rất khó hiểu.
- Bỏ qua giá trị trả về. Dòng
add(2, 3);đứng riêng vẫn biên dịch được, nhưng tổng tính ra bị âm thầm vứt bỏ. Nhớ dùng thật sự giá trị mà hàm trả về.
// Looks like a definition, but the stray ; turns it into a
// declaration followed by a leftover block - a frequent typo:
int triple(int n); // <- this ; ends the statement
{
return n * 3; // n is undefined here; this block is now orphaned
}
Tiếp theo: tham số của hàm
Bạn đã thấy hàm nhận đầu vào qua danh sách tham số, nhưng chủ đề này còn nhiều điều để nói. Trang tiếp theo đi sâu vào tham số của hàm: truyền tham trị và truyền tham chiếu, đối số mặc định, tham số const, và lựa chọn đó quyết định hàm có thể thay đổi dữ liệu của nơi gọi hay không.
Câu hỏi thường gặp
Viết một hàm trong C++ như thế nào?
Hàm cần có kiểu trả về, tên, cặp ngoặc tròn chứa tham số và thân hàm trong cặp ngoặc nhọn: int add(int a, int b) { return a + b; }. Gọi hàm bằng tên kèm đối số, ví dụ add(2, 3). Nếu hàm không trả về gì, dùng void làm kiểu trả về.
Khai báo hàm và định nghĩa hàm trong C++ khác nhau thế nào?
Khai báo (hay prototype) cho trình biên dịch biết tên, kiểu trả về và các tham số của hàm, kết thúc bằng dấu chấm phẩy: int add(int a, int b);. Định nghĩa thì có thêm thân hàm trong cặp ngoặc nhọn. Bạn có thể khai báo hàm trước main rồi định nghĩa sau; nhờ khai báo, bạn gọi được hàm trước khi phần định nghĩa xuất hiện.
Điều gì xảy ra nếu hàm C++ không trả về giá trị?
Với hàm void thì không sao: hàm chỉ đơn giản kết thúc. Nhưng với hàm không phải void, chạy tới cuối hàm mà không gặp return là undefined behavior: nơi gọi nhận về một giá trị rác và chương trình có thể chạy sai. Hầu hết trình biên dịch đều cảnh báo lỗi này; hãy luôn trả về giá trị trên mọi nhánh của hàm không phải void.