CHƯƠNG 3: LẬP TRÌNH CẤU TRÚC TRONG C++
Tổng quan về hàm (functions overview)
Giả sử có một chương trình yêu cầu tính tuổi của người dùng với năm sinh được nhập từ bàn phím.
#include <iostream>
using namespace std;
int main()
{
int year;
cout << "Input your year of birth: ";
cin >> year;
int age = 2016 - year;
cout << "Your ages: " << age << endl;
return 0;
}
1234567891011121314
Outputs:
Chương trình bên trên dùng để tính tuổi của 1 người. Giả sử bây giờ bài toán cần được mở rộng thêm, yêu cầu tính tuổi của 3 người.
Vấn đề phát sinh từ đây, bạn phát hiện ra mình phải lặp lại những dòng code tương tự bên trên để tính tuổi cho 2 người tiếp theo. Dẫn đến tình trạng trùng lắp code và mất nhiều thời gian xây dựng chương trình. Để khắc phục vấn đề đó, khái niệm Hàm (Function) trong lập trình được ra đời.
Hàm (function) là một dãy các câu lệnh có thể tái sử dụng, được thiết kế để thực hiện một công việc cụ thể trong chương trình.
Cú pháp của hàm trong C++:
<kiểu trả về> <tên hàm>([<danh sách tham số>])
{
<các câu lệnh>
[return <giá trị>;]
}
Trong đó:
- <kiểu trả về>: kiểu bất kỳ của C++ (bool, char, int, double,…). Nếu không trả về thì là void.
- <tên hàm>: theo quy tắc đặt tên định danh.
- <danh sách tham số>: tham số hình thức đầu vào giống khai báo biến, cách nhau bằng dấu phẩy “,”. (Có thể không có)
- <giá trị>: trả về cho hàm qua lệnh return. (Có thể không có)
Ở bài CẤU TRÚC MỘT CHƯƠNG TRÌNH C++ (Structure of a program), bạn đã biết mỗi chương trình C++ đều có một hàm tên là main(), hàm này là nơi bắt đầu của chương trình. Trong thực tế, một chương trình thường sẽ có rất nhiều hàm bên trong.
Ví dụ về chương trình đơn giản có 2 hàm: main() và sayHello()
#include <iostream>
using namespace std;
// Definition of function sayHello()
void sayHello() // sayHello() is the called function in this example
{
cout << "Hello Howkteam.com!" << endl;
}
// Definition of function main()
int main()
{
cout << "Starting main()" << endl;
// Interrupt main() by making a function call to sayHello(). main() is the caller.
sayHello();
cout << "Ending main()" << endl;
return 0;
}
12345678910111213141516171819202122
Outputs:
Chương trình sẽ thực thi các câu lệnh một cách tuần tự bên trong một hàm. Khi gặp một lời gọi hàm, CPU sẽ gián đoạn hàm hiện tại để thực thi các câu lệnh bên trong hàm được gọi. Khi hàm được gọi kết thúc, CPU sẽ lại tiếp tục thực thi hàm hiện tại.
Chú ý: Hàm có thể được gọi nhiều lần trong một chương trình (tính tái sử dụng), và bất kỳ hàm nào cũng đều có thể gọi hàm khác.
Hiện tại, bạn nên đặt hàm main() ở vị trí cuối cùng trong file code của chương trình. Lý do tại sao sẽ được đề cập cụ thể trong bài TIỀN KHAI BÁO & ĐỊNH NGHĨA HÀM (Forward declarations and Definitions of Functions).
Giá trị trả về (return values)
Ở bài CẤU TRÚC MỘT CHƯƠNG TRÌNH C++ (Structure of a program), bạn đã biết hàm main() có kiểu int nên bắt buộc phải có một câu lệnh return giá trị kiểu int. Khi chương trình thực thi kết thúc, hàm main() sẽ return một giá trị cho hệ điều hành, để thông báo là nó chạy thành công hay không.
Khi tạo ra một hàm mới, tùy vào mục đích của hàm mà bạn có thể quyết định hàm đó có trả về một giá trị nào đó hay không.
Để tạo ra một hàm có giá trị trả về, bạn cần:
- Thiết lập kiểu trả về trong định nghĩa của hàm
- Sử dụng câu lệnh return để trả về một giá trị.
Chú ý: Khi gặp câu lệnh return, hàm sẽ trả về giá trị ngay tại thời điểm đó. Tất cả câu lệnh trong hàm, sau dòng lệnh return sẽ được bỏ qua.
Ví dụ về chương trình có hàm trả về một số nguyên:
#include <iostream>
using namespace std;
// int means the function returns an integer value to the caller
int return69()
{
// this function returns an integer, so a return statement is needed
return 69; // we're going to return integer value 69 back to the caller of this function
}
int main()
{
cout << return69() << endl; // prints 69
int sum = return69() + 1;
cout << sum << endl; // prints 70
return69(); // okay: the value 69 is returned, but is discarded
return 0;
}
12345678910111213141516171819202122
Outputs:
Hàm có giá trị trả về có thể đặt riêng biệt, hoặc bên trong một biểu thức như ở ví dụ trên.
Một câu hỏi thường được hỏi là: "Hàm có thể trả về nhiều giá trị thông qua câu lệnh return?". Câu trả lời là không. Khi sử dụng câu lệnh return, hàm chỉ có thể trả về một giá trị duy nhất.
Tuy nhiên, bạn có thể sử dụng phương pháp truyền tham chiếu hoặc truyền địa chỉ cho hàm để có thể lấy được nhiều giá trị:
- Phương pháp truyền tham chiếu sẽ được hướng dẫn ở bài: TRUYỀN THAM CHIẾU CHO HÀM (Passing Arguments by Reference).
- Phương pháp truyền địa chỉ (con trỏ) sẽ được hướng dẫn trong bài: TRUYỀN ĐỊA CHỈ CHO HÀM (Passing arguments by address).
Giá trị trả về của kiểu void (return values of type void)
Những hàm có mục đích tính toán thường sẽ return một giá trị khi kết thúc hàm. Đối với những hàm không có mục đích tính toán (Vd: hàm setter, hàm print, …), C++ hỗ trợ sử dụng kiểu dữ liệu void cho những hàm không có giá trị trả về.
#include <iostream>
using namespace std;
// void means the function does not return a value to the caller
void sayHello()
{
cout << "Hello Duy Tan University!" << endl;
cout << "PNU CS211" << endl;
// This function does not return a value so no return statement is needed
}
int main()
{
sayHello(); // okay: function sayHello() is called, no value is returned
cout << sayHello(); // error: this line will not compile. You'll need to comment it out to continue.
return 0;
}
123456789101112131415161718192021
Outputs: “binary '<<': no operator found which takes a right-hand operand of type 'void' (or there is no acceptable conversion)”
Trong chương trình trên, hàm sayHello() có kiểu void nên sẽ không trả về giá trị. Nên compiler sẽ thông báo lỗi không thể in giá trị của hàm sayHello() ra màn hình trong lần gọi hàm thứ 2.
Chú ý:
- Hàm có kiểu void sẽ không có giá trị trả về.
- Có thể sử dụng câu lệnh return trong hàm void để kết thúc hàm ngay lập tức.
Tham số và đối số của hàm (Function parameters and arguments)
Để chuyển thông tin vào một hàm để tính toán, bạn cần biết đến khái niệm tham số và đối số của hàm (function parameters and arguments):
- Tham số (parameters): là các biến được sử dụng trong một hàm mà giá trị của biến đó được cung cấp bởi lời gọi hàm. Các tham số được đặt bên trong dấu ngoặc đơn, cú pháp giống khai báo biến, cách nhau bằng dấu phẩy “,”.
- Đối số (arguments): là các giá trị truyền vào hàm qua lời gọi hàm, cách nhau bởi dấu phẩy “,”. Số lượng đối số tương ứng với số lượng tham số của hàm.
Ví dụ: về 3 hàm có số tham số và đối số khác nhau:
#include <iostream>
using namespace std;
// This function takes no parameters
// It does not rely on the caller for anything
void sayHello()
{
cout << "Hello Howkteam.com!" << endl;
}
// This function takes one integer parameter named x
// The caller will supply the value of x
void printValue(int x)
{
cout << x << endl;
}
// This function has two integer parameters, one named x, and one named y
// The caller will supply the value of both x and y
int add(int x, int y)
{
return x + y;
}
int main()
{
sayHello();
printValue(69); // 69 is the argument passed to function printValue()
cout << add(6, 9) << endl; // 6 and 9 are the arguments passed to function add()
return 0;
}
123456789101112131415161718192021222324252627282930313233
Outputs:
Trong C++, có 3 cách truyền đối số (arguments) cho một hàm:
- Truyền giá trị (Call by value)
- Truyền tham chiếu (Call by reference) (Chỉ có trong C++): Cách này sẽ được hướng dẫn trong bài sau: TRUYỀN THAM CHIẾU CHO HÀM (Passing Arguments by Reference)
- Truyền địa chỉ (Call by address): Cách này sẽ được hướng dẫn trong bài TRUYỀN ĐỊA CHỈ CHO HÀM (Passing Arguments by Address), sau khi bạn đã được học về con trỏ.
Trong bài học này, mình sẽ chia sẻ về 2 cách đầu tiên.
Truyền giá trị cho hàm (Passing arguments by value)
Trong C++, mặc định đối số được truyền cho hàm ở dạng giá trị.
Khi truyền đối số cho hàm ở dạng giá trị, giá trị của đối số được sao chép vào tham số của hàm. Và đối số sẽ không bị thay đổi sau lời gọi hàm.
Ví dụ:
#include <iostream>
using namespace std;
void callByValue(int y)
{
cout << "y = " << y << endl;
y = 69;
cout << "y = " << y << endl;
} // y is destroyed here
int main()
{
int x(1);
cout << "x = " << x << endl;
callByValue(x);
cout << "x = " << x << endl;
return 0;
}
123456789101112131415161718192021222324
Outputs:
Trong chương trình trên, biến x truyền vào hàm callByValue(int y) ở dạng giá trị, nên nó không bị thay đổi sau lời gọi hàm. Kết quả cuối cùng của biến x vẫn là 1.
Tổng kết về phương pháp truyền giá trị cho hàm (Passing argument by value)
Ưu điểm:
- Đối số có thể là biến (Vd: x, y), hằng (Vd: 1, 2), biểu thức (Vd: x + 1), structs, classes, hoặc enumerators.
- Đối số không bị thay đổi sau lời gọi hàm, hạn chế tác động không mong muốn của hàm lên đối số.
Nhược điểm:
- Gây tốn thêm vùng nhớ do hàm phải tạo các tham số là bản sao của các đối số.
- Gây giảm hiệu suất trong trường hợp đối số là kiểu cấu trúc (structs) hoặc các lớp (classes), đặc biệt là nếu hàm đó được gọi nhiều lần. Vì mỗi lần gọi hàm đều phải sao chép giá trị của đối số vào tham số của hàm.
- Hàm chỉ có thể trả về một giá trị duy nhất bằng câu lệnh return.
Khi nào nên sử dụng:
- Khi đối số là các kiểu dữ liệu cơ bản.
- Khi không có nhu cầu thay đổi giá trị của đối số sau khi thực hiện hàm.
Khi nào không nên sử dụng:
- Khi đối số là các mảng (arrays), kiểu cấu trúc (structs), hoặc các lớp (classes)
Truyền tham chiếu cho hàm (Passing arguments by reference)
Mặc dù truyền giá trị cho hàm (Passing arguments by value) là phương pháp thường được sử dụng nhất, vì tính linh hoạt và an toàn. Nhưng nó vẫn có 2 hạn chế:
- Gây giảm hiệu suất trong trường hợp đối số là kiểu cấu trúc (structs) hoặc các lớp (classes), đặc biệt là nếu hàm đó được gọi nhiều lần. Vì mỗi lần gọi hàm đều phải sao chép giá trị của đối số vào tham số của hàm.
- Hàm chỉ có thể trả về một giá trị duy nhất bằng câu lệnh return. Trong nhiều trường hợp, hàm cần trả về nhiều thông tin hơn, cách này không đáp ứng được.
Phương pháp Truyền tham chiếu cho hàm (Passing arguments by reference) ra đời để khắc phục 2 nhược điểm đó.
Trước tiên, bạn cần biết cơ bản về biến tham chiếu:
- Trong C++, tham chiếu (reference) là một loại biến hoạt động như một bí danh của biến khác.
- Khai báo bằng cách sử dụng ký hiệu “&” giữa kiểu dữ liệu và tên biến.
- Mọi thay đổi trên biến tham chiếu cũng chính là thay đổi trên biến được tham chiếu.
Chi tiết về biến tham chiếu sẽ được hướng dẫn chi tiết trong bài BIẾN THAM CHIẾU (Reference variables).
Để truyền tham chiếu cho hàm (Passing arguments by reference), bạn chỉ cần khai báo các tham số (parameters) của hàm dưới dạng tham chiếu (references):
#include <iostream>
using namespace std;
void callByReferences(int &y) // y is a reference variable
{
cout << "y = " << y << endl;
y = 69;
cout << "y = " << y << endl;
} // y is destroyed here
int main()
{
int x(1);
cout << "x = " << x << endl;
callByReferences(x);
cout << "x = " << x << endl;
return 0;
}
123456789101112131415161718192021222324
Outputs:
Trong chương trình trên, khi hàm callByReferences(int &y) được gọi, y sẽ trở thành một tham chiếu đến đối số x. Mọi thay đổi của biến y bên trong hàm callByReferences(int &y) cũng chính là thay đổi trên biến x.
Chú ý: Khi truyền tham chiếu cho hàm, đối số chỉ có thể là biến (variables).
Trả về nhiều giá trị thông qua tham số đầu ra (Returning multiple values via out parameters)
Đôi khi bạn cần một hàm trả về nhiều giá trị. Tuy nhiên, hàm chỉ có một giá trị trả về. Một trong những cách để hàm trả về nhiều giá trị là sử dụng tham số tham chiếu:
Ví dụ:
#include <iostream>
using namespace std;
void calculator(int x, int y, int &addOut, int &subOut)
{
addOut = x + y;
subOut = x - y;
}
int main()
{
int a(6), b(9);
int add, sub;
// calculator will return the addOut and subOut in variables add and sub
calculator(a, b, add, sub);
cout << " a + b = " << add << endl;
cout << " a - b = " << sub << endl;
return 0;
}
1234567891011121314151617181920212223
Outputs:
Trong chương trình trên, biến add và sub truyền vào hàm calculator ở dạng tham chiếu, nên giá trị của nó đã thay đổi sau lời gọi hàm.
Chú ý: Trong một hàm, các tham số có thể truyền theo nhiều cách.
Truyền tham chiếu hằng (Pass by const reference)
Truyền tham chiếu cho hàm đã giải quyết được vấn đề hiệu suất của phương pháp Truyền giá trị (Pass by value). Nhưng truyền tham chiếu cho phép hàm thay đổi giá trị của các đối số (arguments), điều này sẽ là nguy hiểm tiềm ẩn nếu bạn chỉ muốn đọc các đối số đó (read only).
Nếu bạn biết rằng một hàm sẽ không thay đổi giá trị của đối số, nhưng không muốn truyền giá trị (pass by value) vì vấn đề hiệu suất, giải pháp tốt nhất là truyền tham chiếu hằng (Pass by const reference).
Tham chiếu hằng (const reference) là một tham chiếu mà không cho phép biến được tham chiếu thay đổi thông qua biến tham chiếu. Đối số của tham chiếu hằng có thể là biến số, hằng số hoặc biểu thức.
Ví dụ:
#include <iostream>
using namespace std;
void printValue(const int &value) // value is a const reference
{
// compile error: a const reference cannot have its value changed!
value = 69;
cout << value << endl;
}
int main()
{
int x(1);
printValue(x); // argument is a variable
//printValue(5); // argument is a const
//printValue(x + 5); // argument is a expression
return 0;
}
12345678910111213141516171819202122
Trong chương trình trên, vì value là tham chiếu hằng, giá trị của nó không thể thay đổi. Nên dòng lệnh value = 69; bên trong hàm printValue(const int &value) đã tạo ra một lỗi biên dịch.
Ưu điểm khi truyền tham chiếu hằng:
- Đảm bảo các đối số sẽ không bị thay đổi ngoài ý muốn. Compiler sẽ thông báo lỗi nếu bạn cố thay đổi một tham chiếu hằng.
- Giúp lập trình viên biết hàm đó sẽ không làm thay đổi giá trị của đối số.
- Bạn không thể truyền đối số là một hằng số (const argument) cho tham số của hàm là một biến tham chiếu (non-const reference parameter). Nhưng khi tham số của hàm là một tham chiếu hằng, bạn có thể truyền đối số là một biến số hoặc một hằng số cho nó.
Chú ý: Khi truyền tham chiếu cho hàm, luôn sử dụng một tham chiếu hằng (const reference), trừ khi bạn cần thay đổi giá trị của các đối số
Tổng kết về phương pháp truyền tham chiếu cho hàm (Passing arguments by reference)
Ưu điểm:
- Hàm có thể thay đổi giá trị của các đối số. Ngược lại, bạn có thể sử dụng tham chiếu hằng (const reference) nếu không muốn hàm thay đổi giá trị đối số.
- Không mất thời gian và bộ nhớ để sao chép giá trị của đối số vào tham số của hàm.
- Hàm có thể trả về nhiều giá trị thông qua tham chiếu.
Nhược điểm:
- Đối số khi truyền tham chiếu non-const bắt buộc phải là biến số (variables).
- Khó phân biệt được tham số khi truyền tham chiếu non-const là tham số input, output hay cả 2.
Khi nào nên sử dụng:
- Khi đối số là kiểu cấu trúc (structs) hoặc các lớp (classes). Sử dụng tham chiếu hằng (const reference) nếu không muốn hàm thay đổi giá trị của đối số.
- Khi có nhu cầu thay đổi giá trị của đối số sau khi thực hiện hàm.
Khi nào không nên sử dụng:
- Khi đối số có kiểu dữ liệu cơ bản (sử dụng truyền giá trị).
Không có nhận xét nào:
Đăng nhận xét