< Lập trình tân binh | 1.7. Chia nhỏ chương trình thành các hàm

1.7. Chia nhỏ chương trình thành các hàm

Trong bài học trước, chúng ta đã nhắc đến các lệnh cấu trúc chia nhánh (lệnh điều kiện) và lặp (vòng lặp) cho phép chúng ta quản lý tiến trình của chương trình. Trước đó, chúng ta cũng đã nói đến các biến. Đấy đều là những khái niệm cơ bản của tin học mà bạn có thể bắt gặp trong bất cứ ngôn ngữ lập trình nào. Khái niệm mà tôi sẽ giới thiệu với các bạn tiếp theo đây cũng vậy. Chủ đề của bài học này sẽ là : hàm.

Tất cả các chương trình C++ đều sử dụng các hàm. Kể cả các bạn cũng đã từng sử dụng chúng, dù có thể là bạn cũng không ý thức được.

Mục đích của việc tạo ra các hàm là chia nhỏ chương trình ra thành nhiều phần mà mỗi phần có thể được sử dụng lại trong những tình huống khác nhau. Hình dung chúng kiểu như những viên gạch vậy, bạn có thể dùng cho những mục đích khác nhau để xây tường, xây nhà, xây kho. Sau khi làm xong những "viên gạch" thì việc các lập trình viên phải làm chỉ là gắn chúng lại với nhau để tạo ra chương trình của mình.

Nào, hãy bắt đầu bằng việc "đúc gạch" !

Tạo ra và sử dụng các hàm

Từ đầu của giáo trình này, chúng ta đã bắt đầu sử dụng các hàm. Đến bây giờ vẫn vậy. Các bạn hẳn sẽ không quên main() chứ ? Điểm khởi đầu của mọi chương trình C++, đó là nơi mà mọi thứ bắt đầu.

#include <iostream>
using namespace std;

int main(){ //Bat dau cua ham main() va cung cua chuong trinh
  cout << "Xin chao, Tan Binh !" << endl;
  return 0;
} //Ket thuc cua ham main() va cung cua chuong trinh

Chương trình bắt đầu ở dòng thứ 4 và kết thúc ở dòng thứ 7. Điều đó có nghĩa là toàn bộ chương trình nằm trong 1 và chi 1 hàm. Chương trình không ra khỏi hàm đó. Trong cả tiến trình chỉ có 1 đoạn mã được chạy tuần tự từ trên xuống dưới.

Nếu tôi nói thế với các bạn thì là vì chúng ta có thể viết thêm những hàm khác nữa và chương trình sẽ được chia nhỏ ra thành các phần độc lập nhau.

? Tại sao lại phải làm như thế ?

Đúng thật là chúng ta có thể nhét tất cả mã của chương trình vào trong hàm main(). Thế nhưng đây không phải 1 thói quen tốt.

Hãy nghĩ về siêu cấp trò chơi 3D mà bạn sẽ tạo ra. Bởi vì nó khá phức tạp, mã nguồn có thể lên tới vài chục nghìn dòng! Nếu xếp tất cả chỗ đó vào cùng 1 hàm thì sẽ rất khó để theo dõi được các thứ. Sẽ đơn giản hơn rất nhiều nếu ở 1 góc chúng ta để 1 đoạn mã chuyên xử lý chuyển động của nhân vật rồi góc khác đoạn xử lý thay đổi cảnh vật mỗi khi nhân vật lên bàn, vv... Chia nhỏ chương trình sẽ giúp chúng ta tổ chức công việc hiệu quả hơn.

Ngoài ra, nếu trong nhóm của bạn có nhiều người cùng tham gia viết mã, sẽ rất tiện để chia công việc theo kiểu mỗi người phụ trách một số hàm khác nhau.

Không chỉ có thể! Lấy ví dụ về hàm lấy căn mà chúng ta đã sử dụng lúc trước. Nếu bạn tạo ra 1 chương trình bao gồm các công thức toán thì lý do là vì bạn cần dùng chúng ở những chỗ khác nhau để thực hiện các phép tính toán. Hàm sqrt() sẽ giúp ta tránh được việc lặp lại 1 đoạn mã nhiều lần ở những vị trí khác nhau. Chúng ta có thể tái sử dụng các hàm, đây mới là lý do chính.

Giới thiệu về các hàm

1 hàm là một đoạn mã thực thi một công việc nhất định. Nó nhận vào những dữ liệu để xử lý, thực hiện 1 số hành động rồi trả về cho chúng ta một giá trị.

Những giá trị mà chúng ta đưa vào được gọi là thông số (argument) và giá trị mà ta nhận ở đầu ra của hàm là giá trị trả về (return value). Một hình vẽ minh họa :D.

Các bạn vẫn còn nhớ hàm pow() chứ ? Đó là hàm cho phép bạn hiện phép lũy thừa. Sử dụng những từ vựng mà chúng ta vừa làm quen thì hàm này :

  1. Nhận vào 2 thông số
  2. Thực hiện 1 phép tính
  3. Trả về kết quả của phép tính

Nếu sử dụng mẫu hình vẽ bên trên

Các bạn đã tự mình thử nghiềm việc có thể dùng lại hàm pow() mỗi khi cần để tính lũy thừa mà không cần chép lại đoạn mã thực thi trong hàm này.

Định nghĩa 1 hàm

Giờ là đến lúc lúc thao tác cụ thể hàm. Tôi đã có thể để các bạn vật lộn với hàm main() và tự đưa ra tổng kết. Thế nhưng tôi lại rất tốt bụng :D, vì thế tôi sẽ đưa ra 1 số chỉ dẫn. Sẵn sàng chưa? ... Chúng ta bắt đầu nhé!

Tất cả các hàm đều có dạng :

Kiểu_dữ_liệu tên_hàm(danh_sách_thông_số){
  //Đoạn lệnh thực thi bên trong hàm
}

Ta có thể xác định được 1 số yếu tố :

  • Kiểu dữ liệu trả về : cung cấp thông tin về kiểu dữ liệu của giá trị trả về. Nếu hàm trả về 1 thông điệp thì kiểu trả về phải là string còn nếu là phép tính thì phải là kiểu số như int, double
  • Tên của hàm : các bạn đã thấy 1 số ví dụ như main(), pow() hay sqrt(). Quan trọng là tên của hảm phải nói lên được nhiệm vụ của hàm đó, giống như tên biến vậy.
  • Danh sách thông số : cung cấp những dữ liệu đầu vào mà hàm sẽ xử lý. Hàm có thể có 0 thông số giống như main(), 1 thông số như sqrt() hay nhiều thông số như pow().
  • Dấu {} : cho biết chỗ bắt đầu và kết thúc của hàm. Tất cả lệnh được thực thi phải nằm trong đấy.

! Chúng ta có quyền tạo ra nhiều hàm trùng tên với nhau nhưng điều kiện bắt buộc là danh sách thông số phải khác nhau (chú ý khi xét sự giống nhau của danh sách thông số, chúng ta chỉ quan tâm đến kiểu dữ liệu và thứ tự của chúng trong đó chứ không quan tâm đến tên biến). Việc này gọi là khai báo chồng. Ví dụ trong cùng 1 chương trình, chúng ta có thể có 2 hàm int cong(int a, int b)double cong(double a, double b). 2 hàm này cùng tên nhưng 1 làm việc với số nguyên còn cái còn lại làm việc với số thực.

Một hàm đơn giản

Hãy bắt đầu với 1 hàm cơ bản : cộng thêm 2 vào giá trị của 1 số và trả về kết quả !

int tangHai(int so){
    int ketQua(so + 2);
    //Ta tao ra 1 o nho
    //Nhan vao thong so va cong vao gia tri do 2 don vi
    //Luu vao trong o nho vua tao bo nho

    return ketQua;
    //Tra ve gia tri duoc luu trong 'ketQua'
}

!Không có dấu ; sau khi khai báo cũng như sau dấu {}

Với tất cả những gì mà tôi đã giải thích với các bạn ở bên trên, tôi cá là các bạn sẽ hiểu dòng đầu tiên. Chúng ta khai báo 1 hàm tên là tangHai. Hàm này nhận vào thông số là một số nguyên và sau khi kết thúc, trả về kết quả là 1 số nguyên.

Tất cả những dòng sau đó đều khá quen thuộc, chỉ trừ dòng return ketQua;. Nếu bạn có đặt câu hỏi về dòng này, xin mời trước hết đọc lại bài học về bộ nhớ. Câu lệnh return gửi trả về kết quả đầu ra của hàm mà trong trường hợp này là giá trị của biến ketQua.

Gọi hàm

Bạn nghĩ sao nếu tôi nói là các bạn đã biết thừa cách gọi hàm rồi? Có nhớ bài học của chúng ta về các hàm trong toán học chứ?

#include <iostream>
using namespace std;

int tangHai(int so){
    int ketQua(so + 2);

    return ketQua;
}

int main(){
    int a(2),b(2);

    cout << "Gia tri cua a : " << a << endl;
    cout << "Gia tri cua b : " << b << endl;
    b = tangHai(a); //Goi ham
    cout << "Gia tri cua a : " << a << endl;
    cout << "Gia tri cua b : " << b << endl;

    return 0;
}

Chúng ta thấy cú pháp <kết_quả> = <tên_hàm>(<danh_sách_thông_số>). Quá dễ đúng không !

Đây là kết quả khi chạy đoạn mã trên.

Sau khi gọi hàm, giá trị của biến b đã thay đổi. Tất cả hoạt động như dự đoán.

Hàm nhiều thông số

 Chúng ta đã thấy là 1 hàm có khả năng nhận vào nhiều thông số như getline() hay pow(). Để truyền nhiều thông số cho hàm, các thông số trong danh sách phải cách nhau bằng dấu phẩy.

int cong(int a, int b){
    return a+b;
}

double nhan(double a, double b, double c){
    return a*b*c;
}

Hàm đầu tiên trả về kết quả là tổng của 2 thông số còn hàm thứ 2 thì kết quả lại là tích của 3 thông số.

!Đương nhiên là bạn cũng có thể khai báo hàm với các biến mang các kiểu dữ liệu khác nhau.

Hàm không có thông số

Trái ngược với những hàm nhiều thông số, có những hàm không cần có dữ liệu đầu vào, đồng nghĩa với không có thông số. Trong trường hợp đó, chúng ta chỉ cần để trống bên trong dấu ngoặc là được

? Những hàm như thế có tác dụng gì?

 Hãy nghĩ đến 1 hàm có chức năng là hỏi tên của người dùng, vây thì nó không cần phải có thông số;

string hoiTen(){    
     cout << "Ten cua ban la : ";
     string ten;
     cin >> ten;
     return ten;
}

Dù là kiểu hàm này cũng khá hiếm nhưng tôi nghĩ các bạn sẽ không khó khăn nếu muốn tìm 1 ví dụ đâu.

Hàm không trả về kết quả

Tất cả những hàm mà tôi giới thiệu nãy giờ đều nhận những thông số và trả về kết quả. Nhưng như thế không có nghĩa là không tồn tại những hàm không trả về gì cả (hoặc nói chính xác hơn là gần như không gì cả).

Không có gì được trả về nhưng khi bạn khai báo hàm, vẫn phải ghi 1 kiểu dữ liệu đầu ra. Đấy là lý do chúng ta dùng kiểu dữ liệu void, từ tiếng Anh mang nghĩa là trống rỗng.

Và vì vậy, nó sẽ mang ý nghĩa là hàm này không trả về gì cả.

void noiXinChao(){
    cout << "Xin chao, Tan Binh!" << endl;
    //Khong tra ve ket qua nao nen khong co lenh return
}

int main(){
    noiXinChao();
    //Vi ham khong tra ve ket qua
    //Nen khong can luu gia tri trả ve trong o nho
    
    return 0;
}

Cần ghi nhớ là hàm không thể trả về nhiều hơn 1 giá trị. 1 hàm chỉ có tối đa 1 kết quả.

Đến đây là chúng ta đã lướt qua hết phần lý thuyết. Phần tiếp theo là 1 số ví dụ và sơ đồ tóm tắt.

1 vài ví dụ
Hàm bình phương

Chúng ta sẽ bắt đầu với 1 ví dụ đơn giản : tính bình phương của 1 số. Hàm này nhận vào thông số là 1 số x và tính giá trị bình phương của x.

#include <iostream>
using namespace std;

double binhPhuong(double x){
    double ketQua;
    ketQua = x*x;
    return ketQua;
}

int main(){
    double so, soBinhPhuong;
    cout << "Hay nhap vao 1 so : ";
    cin >> so;

    soBinhPhuong = binhPhuong(so); //Su dung ham ben tren

    cout << "Binh phuong cua " << so << " la " << soBinhPhuong << endl;
    return 0;
}

Tôi đã vẽ giúp các bạn 1 sơ đồ giải thích tiến trình của chương trình.

  1. Chương trình bắt đầu ở đầu hàm main().
  2. Thực hiện 3 dòng đâu tiên như bình thường.
  3. Gặp 1 lệnh gọi hàm.
  4. Kiểm tra thông số. Giá trị của thông số là giá trị của biến so. Giá trị này sẽ được chép vào ô nhớ x.
  5. Chương trình chuyển lên dòng đầu tiên của hàm binhPhuong(), chạy mã của hàm này giống như bình thường.
  6. Đến cuối hàm binhPhuong, chép giá trị trong biến ketQua vào ô nhớ soBinhPhuong.
  7. Quay tở về hàm main() và thực hiện nốt dòng lệnh cuối.

Một điều tuyệt đối phải nhớ là : giá trị của biến được cung cấp cho hàm được chép vào 1 ô nhớ mới. Những xử lý của hàm binhPhuong() không ảnh hưởng gì tới các biến được khai báo trong hàm main() mà nó chỉ sử dụng những ô nhớ của riêng nó. Chỉ có khi lệnh return được tiến hành thì mới thay đổi biến soBinhPhuong của hàm main(). Biến so không thay đổi trong quá trình gọi hàm.

Sử dụng lại hàm có sẵn

Lợi ích của sử dụng hàm ở đây là giúp chúng ta có thể tính bình phương của nhiều số khác nhau, ví dụ các số từ 1 tới 20.

#include <iostream>
using namespace std;

double binhPhuong(double x){
    double ketQua;
    ketQua = x*x;
    return ketQua;
}

int main(){
    for(int i(1); i <= 20 ; i++){
        cout << "Binh phuong cua " << i << " la : " << binhPhuong(i) << endl;
    }
    return 0;
}

Chúng ta chỉ cần viết 1 lần công thức của hàm tính bình phương và sử dụng 20 lần "viên gạch" này. Ở đây, phép tính khá đơn giản nhưng có những trường hợp, chúng ta sẽ rút ngắng được kha khá đoạn mã.

Các biến cùng tên

Trong bài học trước, chúng ta đã nói là mỗi biến phải có 1 cái tên độc nhất. Điều này hoàn toàn đúng nhưng chỉ được áp dụng trong phạm vi bên trong của cùng 1 hàm. Chúng ta được phép khai báo 2 biến có tên trùng nhau trong 2 hàm khác nhau.

#include <iostream>
using namespace std;

double binhPhuong(double x){
    double so;
    so = x*x;
    return so;
}

int main(){
    double so, soBinhPhuong;
    cout << "Hay nhap vao 1 so : ";
    cin >> so;

    soBinhPhuong = binhPhuong(so); //Su dung ham ben tren

    cout << "Binh phuong cua " << so << " la " << soBinhPhuong << endl;
    return 0;
}

Như các bạn đã thấy, có 2 biến so tồn tại trong 2 hàm là main()binhPhuong(). Trình biên dịch không phàn nàn gì và chương trình thực hiện đúng như đoạn mã trước. Không có sự nhầm lẫn ở đây vì mỗi lần, trình biên dịch chỉ chú ý đên 1 hàm và không nhận ra là có 2 biến trùng tên.

? Nhưng vì sao phải làm thế?

Hãy nhớ lại là tên biến phải đặc trưng cho dữ liệu mà nó lưu trữ. Rất hiển nhiên là có những trường hợp mà những biến khác nhau mang những vai trò tương tự nhau và vì thế có tên giống. Ngoài ra chúng ta cũng tránh đặt những cái tên quá dài và phức tạp chỉ với mục đích để nó trở thành độc nhất trong chương trình. Chắc sẽ cần đến một trí tưởng tượng phi thường nếu phải ngồi nghĩ ra khoảng vài trăm cái tên khác nhau cho một đoạn mã vài nghìn dòng.

Hàm có 2 thông số

Trước khi kết thúc pần này, đây là 1 ví dụ cuối cùng. Lần này, tôi đề nghị là một hàm có 2 thông số. Chúng ta sẽ vẽ 1 hình chữ nhật từ các dấu "*". Hàm này cần 2 thông số là chiều dài và chiều rộng của hình chữ nhật.

#include <iostream>
using namespace std;

void veHinhChuNhat(int dai, int rong){
    for(int dong(0); dong < rong; dong++){
        for(int cot(0); cot < dai; cot++){
            cout << "*";
        }
        cout << endl;
    } 
}

int main(){
    int dai, rong;
    cout << "Chieu dai cua hinh chu nhat : ";
    cin >> dai;
    cout << "Chieu rong cua hinh chu nhat : ";
    cin >> rong;
    
    veHinhChuNhat(dai, rong);
    return 0;
}

Đây là kết quả đạt được.

 Hàm này chỉ gồm lệnh hiển thị thông điệp nên nó không trả về giá trị nào. Vì thế phải khai báo với kiểu void.

Chúng ta có thể dễ dàng thay đổi để nó trả về diện tích hình chữ nhât. Trong trường hợp đó, cần cho hàm trả về kiểu int.

Hãy thử thay đổi 1 số thứ của hàm này xem! Sau đây là vài ý tưởng:

  • Hiển thị thông báo lỗi nếu chiều dài nhỏ hơn 0
  • Thêm vào 1 thông số cho biết ký tự sẽ được dùng thay thế "*" để vẽ hình

Chúc các bạn vui vẻ. Cái chính là cần làm chủ được các khái niệm.

Tham trị và tham chiếu

 Phần cuối bài học dành cho những khái niệm nâng cao. Các bạn có thể xem lại mỗi khi có thắc mắc trong phần sau của giáo trình.

Tham trị

Khái niệm nâng cao đầu tiên là cách mà máy tính quản lý bộ nhớ khi thực thi các hàm.

Quay lại với hàm đơn giản ở đầu bài học : tăng 2 đơn vị vào giá trị thông số. Do các bạn đã bắt đầu hiểu được mọi thứ đang xảy ra, tôi sẽ thay đổi đi 1 tí tẹo.

int tangHai(int a){
    a += 2;
    return a;
}

! Tôi cố tình sử dụng dấu +=. Các bạn sẽ biết ngay thôi.

Hãy thử đoạn mã sau. Hoàn toàn không có gì mới để học trong đoạn mã này.

#include <iostream>
using namespace std;

int tangHai(int a){
    a+=2;
    return a;
}

int main(){
    int so(4), ketQua;
    ketQua = tangHai(so);
    
    cout << "Gia tri goc : " << so << endl;
    cout << "Ket qua : " << ketQua << endl;
    return 0;
}

Kết quả trả về không có gì bất ngờ.

Công đoạn đặc sắc nàm ở chỗ là điều gì đã xảy ra khi dòng lênh ketQua = tangHai(so); được thực thi. Các bạn còn nhớ cái sơ đồ lúc nãy chứ? Hãy lôi ra xem nào.

Khi gọi hàm, rất nhiều thứ đã xảy ra.

  1. Chương trình xem xét giá trị của biến so. Kết quả là 4.
  2. Mượn 1 ô nhớ và lưu vào đó giá trị 4. Ô nhớ đó dán nhãn là a, tên biến của hàm.
  3. Thực thi hàm.
  4. Tăng thêm 2 đơn vị vào biến a.
  5. Giá trị của a sau đó được ghi vào biến ketQua, bây giờ nó có giá trị là 6.
  6. Thoát khỏi hàm

Chuyện quan trọng là giá trị của biến so đã được chép vào trong 1 ô nhớ mới. Ta nói là thông số đã được truyền bằng tham trị. Khi chương trình ở bên trong hàm, mọi thứ diễn ra như trong hình sau.

 

Chúng ta có 3 ô nhớ trong bộ nhớ và quan trọng hơn là giá trok của biển so không hề thay đổi.

Tham chiếu

Các bạn vẫn còn nhớ về các tham chiếu chứ? Đúng thể, chính là cái khái niệm khó hiểu mà tôi nói với các bạn trong bài học trước. Nếu bạn không chắc, đừng ngại đọc để nhớ lại nhé. Giờ là lúc chúng ta xem tham chiếu có tác dụng gì.

Thay vì sao chép giá trị của so vào trong a, chúng ta có thể thêm vao 1 cái nhãn thứ 2 cho ô nhớ so ở bên trong hàm. Và đương nhiên là chúng ta sử dụng tham chiếu làm thông số cho hàm để làm điều này.

int tangHai(int& a){ //Chu y den dau & !!! 
  a += 2;
  return a;
}

Khi chúng ta gọi hàm theo cách này thì không có bản sao nào đước tạo ra cả. Chương trình đơn giản là thêm 1 cách gọi khác cho ô nhớ chứa biến so. Hãy nhìn hình vẽ sau đây.

Lần này thì biến so và biến a trùng nhau. Chúng ta gọi việc này là truyền thông số sử dụng tham chiếu.

? Lợi ích của sử dụng tham chiếu là gì?

Việc này cho phép hàm tangHai() thay đổi giá trị các thông số của nó. Vì thế nó cũng dẫn theo ảnh hưởng ít nhiều đến phần còn lại của chương trình. Vẫn chương trình lúc trước nhưng lần này chúng ta sử dụng tham chiếu thì sẽ nhận được kết quả như sau :

Chuyện gì đã xảy ra ? Để giải thích thì vừa đơn giản vừa phức tạp.

Bởi vì aso chỉ đơn giản là 2 cái nhãn của cùng một ô nhớ, câu lệnh a += 2 đã thay đổi giá trị của biến so.

Vì thế việc sử dụng tham chiếu có thể trở nên rất nguy hiểm. Chỉ làm thế khi nào bạn thật sự cần.

? Hãy cho tôi 1 ví dụ thực tiễn !

Ví dụ điển hình luôn được sử dụng cho khái niệm này chính là hàm doiCho() cho phép tráo đổi giá trị của 2 thông số ta đưa vào trong chương trình.

void doiCho(double& a, double& b){
 double trungGian(a); //Luu gia tri cua 'a' trong bien 'trungGian'
 a = b; //Ghi vao trong 'a' gia tri cua 'b'
 b = trungGian; //Ghi gia tri goc cua 'a' vào 'b'
}

int main(){
 double a(1.2), b(4.5);

 cout << "a = " << a << " va b = " << b << endl;

 doiCho(a,b); //Goi ham

 cout << "a = " << a << " va b = " << b << endl;
 return 0;
}

Kết quả khi chạy chương trình này.

Giá trị của 2 biến đã được tráo đổi cho nhau.

Nếu chúng ta không sử dụng tham chiếu mà dùng tham trị, hàm sẽ tiến hành xử lý việc đổi chỗ của 2 bản sao của các thông số. Các thông số không thay đổi gì cả và việc này trở thành vô dụng.

Tôi đề nghị các bạn chạy thử hàm này dùng 2 cách tham chiếu và tham trị. Bạn sẽ thấy rõ hơn chuyện gì đang diễn ra.

Có thể bây giờ các bạn vẫn hơi mờ mịt và thấy tham chiếu rắc rối. Tuy nhiên trong phần sau của giáo trình, chúng ta sẽ sử dụng nó khá thường xuyên. Bạn luôn có thể quay lại đọc lại bài học này bất cứ lúc nào để có thể hiểu rõ hơn.

Nâng cao : tham chiếu hằng

Bởi vì chúng ta đã nói đến tham chiếu, tôi sẽ nhân tiện nhắc đến một ứng dụng thực tiễn của nó. Khải niệm này đặc biệt hữu dụng trong các chương sau nhưng chúng ta cũng có thể nhắc đến nó sớm 1 chút.

Sử dụng tham chiếu có 1 ưu điểm lớn so với sử dụng tham trị, đó là không cần phải tạo ra thêm 1 bản sao của biến dùng làm thông số. Hãy thử tưởng tượng thông số là một string. Nếu chuỗi ký tự đó của bạn chứa một đoạn văn thật dài (ví dụ nội dung 1 quyển tiểu thuyết!), vậy thì việc tạo ra 1 bản sao của nó trong bộ nhớ sẽ tốn rất nhiều thời gian cũng như ngốn 1 đống bộ nhớ của bạn. Bản sao này hoàn toàn vô dụng và việc loại bỏ được nó sẽ giúp cải thiện hiệu suất của chương trình của bạn.

Và đây là lúc có bạn sẽ nói với tôi là vậy thì chúng ta hãy sử dụng tham chiếu. 1 ý tưởng hay! Nếu dùng tham chiếu thì không có bản sao nào được tạo ra cả. Nhược điểm của việc này là nó cho phép thay đổi thông số của bạn. Đương nhiên rồi, đấy chính là lý do mà tham chiếu tồn tại.

void f1(string vanBan);{ //Dan den viec sao chep 'vanBan' lam giam hieu suat 

}

void f2(string& vanBan);{ //Dan den viec 'vanBan' co the bi thay doi

}

Giải pháp đưa ra là sử dụng tham chiếu hằng. Chúng ta tránh được việc tao ra bản sao vì sử dụng tham chiếu và ngăn cản việc có thể thay đổi thông số đó khi khai báo rằng nó là 1 hằng số.

void f1(string const& vanBan);{ //Khong sao chep va cung khong the thay doi 

}

Lại thêm 1 thứ hơi mờ mịt và vô dụng? Trong chương 2, lúc chúng ta cùng thảo luận về lập trình hướng đối tượng, chúng ta sẽ thường xuyên dùng kỹ năng này. Và nếu cần, quyền tự do khi cho phép ban quay lại để đọc chương này bất cứ khi nào bạn muốn cool.

Sử dụng nhiều tệp mã nguồn

Trong phần giới thiệu, tôi đã nói là các hàm giống như là các viên gạch được tạo ra sẵn sàng để sử dụng cho nhiều mục đích khác nhau.

Cho đến lúc này thì chúng ta mới chỉ tạo ra nó và đặt bên cạnh hàm main(). Chúng ta vẫn chưa thật sự sử dụng lại được nó ở chỗ nào khác.

Ngôn ngữ C++ cho phép ta tách mã nguồn ra và chứa trong nhiều tệp (file) mã nguồn khác nhau. Mỗi tệp có thể chứa 1 hay nhiều hàm. Chúng ta có thể xây dựng 1 tệp bao (include) gồm 1 tệp khác có sẵn qua đó có thể sử dụng lại các hàm trong nhiều dự án khác nhau. Đấy mới chính thức là giống những viên gạch có thể dùng vào nhiều mục đich.

Những tệp cần thiết

Để mọi thứ được rõ ràng, chúng ta không chỉ cần 1 mà đến 2 loại tệp :

  • Tệp nguồn : thường có phần mở rộng là .cpp, chứa mã nguồn thực tế
  • Tệp tiêu đề : thường có phần mở rộng là .h hoặc .hpp, chứa nguyên mẫu của hàm.

Hãy tạo ra 2 tệp cho hàm tangHai() của chúng ta.

int tangHai(int so){
 int ketQua(so + 2);

 return ketQua;
}
Tệp nguồn

Trên thanh công cụ, chọn File > New > File, sau đó chọn C/C++ source như trong hình.

Ấn Go. Giống như khi khởi tạo dự án, máy tính sẽ hỏi bạn muốn làm việc với C hay C++, đương nhiên là chọn C++.

Cuối cùng, máy tính yêu cầu bạn nhập 1 tên cho tệp này. Như mọi khi, hãy chon 1 tên có ý nghĩa cho các tệp để tiện cho việc quản lý. Với hàm tangHai(), tôi chọn tên math.cpp (thú thật là khi không được sử dụng dấu thì tên tiếng Anh khó bị nhầm hơn tên tiếng Việt nên nếu đã học lập trình, bạn cũng nên biết 1 ít tiếng Anh) và để nó trong cùng thư mục với main.cpp.

Chọn tất cả các tùy chọn như trong hình.

Ấn Finish để kết thúc và tệp của chúng ta đã được tạo ra. Hãy tiến hành tạo tệp tiêu đề.

Tệp tiêu đề

Phần đầu khá giống nhau, bạn chũng chọn File > New > File rồi sau đó chọn C/C++ header như trong hình.

Trong cửa sổ tiếp theo, bạn cần cho tệp 1 cái tên. Tốt nhất là hãy đặt cùng tên với tệp nguồn, chỉ khác là phần mở rộng là .h chứ không phải.cpp. Trong trường hợp này, chúng ta có math.h. Đặt nó ở cùng chỗ với 2 tệp trước.

Đừng thay đổi gì phần bên để điền dưới và đừng quên chọn mọi tùy chọn.

Ấn Finish và thế là xong.

Một khi 2 tệp này đã được tạo ra, các bạn sẽ thấy chúng xuất hiện trong ô bên trái của Code::Block như trong hình.

Khai báo hàm trong các tệp

Bây giờ sau khi đã tạo xong các tệp, cần phải thêm nội dung cho chúng

Tệp nguồn

Tôi đã nói với các bạn là trong tệp nguồn chứa nội dung của hàm. Đó chỉ là một bộ phận. Phần còn lại thì có đôi chút khó hiểu hơn. Trình biên dịch cần biết là có 1 sự liên quan giữa 2 tệp .h.cpp. Vậy nên ta cần thêm dòng lệnh sau :

#include "math.h"

Các bạn hẳn là nhận ra dòng này. Nó cho biết là chúng ta muốn sử dụng những thứ có trong tệp math.h.

! Cần chú ý là ở đây phải sử dụng dấu ngoặc kép "" thay vì dấu <> như các bạn vẫn hay dùng.

Tệp math.cpp hoàn chỉnh trông sẽ như sau.

#include "math.h"

int tangHai(int so){
 int ketQua(so + 2);

 return ketQua;
}

Tệp tiêu đề

Nếu các bạn quan sát nội dung của tệp được tạo ra, bạn sẽ thấy nó không hề trống. Có 3 dòng lệnh bí ẩn :

#ifndef MATH_H_INCLUDED
#define MATH_H_INCLUDED

#endif // MATH_H_INCLUDED

Những dòng này để tránh việc trình biên dịch bao gồm nhiều lần tệp này. Trình biên dịch đôi khi không được thông minh lắm và dễ rơi vào vòng luẩn quẩn nếu phải bao gồm nhiều lần 1 tệp. 3 dòng lệnh được thêm vào để tránh cho chúng ta rơi vào tình huống này. Không nên động vào và thay đổi những dòng này và phải viết toàn bộ mã nguồn ở giữa dòng lệnh thứ 2 và thứ 3.

! Phần chữ in hoa sẽ khác nhau tùy theo từng tệp. Đó chính là những chữ đã xuất hiện trong phần để điền mà tôi khuyên các bạn không nên thay đổi khi khởi tạo tệp này. Nếu bạn không sử dụng Code::Block, có thể những dòng này sẽ không được tự động thêm vào các tệp. Trong trường hợp đó, các bạn phải tự tay thêm chúng vào. Phần chữ in hoa phải giống nhau trong cả 3 dòng và mỗi tệp phải dùng những chữ khác nhau.

Trong tệp này chứa cái mà chúng ta gọi là nguyên mẫu của hàm. Trong dòng đầu tiên của hàm, chúng ta chép tất cả những chữ trước dấu { rồi thêm vào cuối dấu .

Rất ngắn gọn, đây là những gì có được sau đó

#ifndef MATH_H_INCLUDED
#define MATH_H_INCLUDED

int tangHai(int so);

#endif // MATH_H_INCLUDED

! Đừng quên dấu

Trong trường hợp đơn giản nhất thì thế là đủ. Nếu các bạn dùng những thông số phức tạp hơn như kiểu string hay kiểu mảng (chúng ta sẽ nói trong bài sau), cần phải thêm dòng lệnh bao gồm kiểu như #include <string> trong nguyên mẫu. Ta sẽ có :

#ifndef MATH_H_INCLUDED
#define MATH_H_INCLUDED

#include <string>

void inThongDiep(std::string thongDiep);

#endif // MATH_H_INCLUDED

 Cần chứ ý 1 yếu tố quan trọng nữa là thêm std:: trước string. Đây là một không gian tên, khái niệm sẽ được nhắc đến trong phần sau của giáo trình. Nếu các bạn để ý kỹ, các bạn sẽ nhận ra là std xuất hiện trong tất cả các tệp nguồn của chúng ta trong dòng lệnh using namespace std. Bởi vì trong trường hợp này không có dòng lệnh đó (và lời khuyên là không nên sử dụng dòng lệnh đó trong tệp tiêu đề), chúng ta phải viết tên đầy đủ của kiểu dữ liệu string đó là std::string. Các bạn sẽ bắt gặp những ví dụ khác với tên phức trong phần sau của giáo trình. Trong lúc này thì std::string trường hợp đặc biệt duy nhất sealed.

Việc cuối cùng chúng tq phải làm là bao gồm tất cả những tệp này bên trong tệp main.cpp. Nếu không làm như vậy, trình biên dịch sẽ không biết là cần phải tìm hàm tangHai() ở đâu. Vậy nên chúng ta cần thêm 1 dòng lệnh

#include "math.h"

vào đầu chương trình. Kết quả nhận được là

#include <iostream>
#include "math.h"
using namespace std;

int main(){
    int a(2),b(2);
    cout << "Gia tri cua a : " << a << endl;
    cout << "Gia tri cua b : " << b << endl;
    b = tangHai(a);      //Goi ham
    cout << "Gia tri cua a : " << a << endl;
    cout << "Gia tri cua b : " << b << endl;

    return 0;
}

!Luôn luôn chỉ bao gồm tệp tiêu đề .h chứ không bao giờ bao gồm tệp nguồn .cpp.

Cuối cùng chúng ta cũng thật sự sử dụng được các hàm giống như những viên gạch. Nếu trong dự án khác mà ta cần dùng hàm tangHai() thì chỉ việc chép 2 tệp math.hmath.cpp vào trong dự án đó.

!Chúng ta cũng có thể định nghĩa nhiều hàm trong cùng 1 tệp. Thường thì các hàm này được phân loại theo chức năng. Ví dụ chúng ta sẽ đưa tất cả các hàm tính toán vào 1 tệp và các hàm dùng để tạo chuyển động cho nhân vật vào 1 tệp khác. Lập trình cũng là học cách tổ chức các thứ.

Giải trình mã nguồn

Trước khi bắt đầu phần này, tôi muốn nhắc lại 1 vấn đề tưởng như là vô ích. Ngay từ đầu, tôi đã nói với các bạn là nên thêm những bình luận vào trong mã nguồn của mình để hiểu chương trình sẽ làm gì. Điều này càng đúng trong tường hợp các hàm bởi vì chúng ta sẽ sử dụng lại các hàm được viết bởi lập trình viên khác cũng như họ sẽ dùng mã của chúng ta. Tất cả chỉ quan tâm đến chức năng của hàm chứ không quan trong việc biết xem hàm đấy có cơ chế hoạt động như thế nào (bởi vì lập trình viên rất lười ;D).

Bởi vì tệp tiêu đề thường có dung lượng rất nhỏ, chúng ta tận dụng để thêm các bình luận giải thích chức năng các hàm vào trong đó. Thường sẽ có 3 yếu tố trong những bình luận đó,

  • Chức năng của hàm
  • Danh sách các thông số nhận vào và ý nghĩa của chúng
  • Kết quả trả về

Thay vì viết 1 bài thuyết trình, trong phần bình luận giải thích chức năng của hàm tangHai(), ta có thể viết

#ifndef MATH_H_INCLUDED
#define MATH_H_INCLUDED

/*
 * Ham tang 2 don vi vao gia tri cua thong so nhan vao
 * so : thong so ma ta muon tang gia tri
 * Ket qua tra ve : so + 2
 */
int tangHai(int so);

#endif // MATH_H_INCLUDED

Trong trường hợp này thì phần miêu tả khá là đơn giản. Cái chính là chúng ta cần tạo được thói quen làm điều này. Các bạn nên nhớ là việc thêm các bình luận vào tệp tiêu đề thông dụng đến mức là có những chương trình được viết ra để đọc chúng để tạo nên các trang web tư liệu về các hàm.

Ví dụ như chương trình doxygen sử dụng cấu trúc sau

/*
 * \brief Ham tang 2 don vi vao gia tri cua thong so nhan vao
 * \param so Thong so ma ta muon tang gia tri
 * \return so + 2
 */
int tangHai(int so);

Bạn sẽ thấy trong chương sau này việc có mã nguồn được giải trình cụ thể là rất quan trọng.

Giá trị mặc định cho các thông số

 Các bạn chắc đã quen dần với các thông số. Từ đầu của bài học, chúng ta đã nói đến chúng, 1 hàm yêu cấu 3 thông số thì cần phải cung cấp cho hàm đó 3 gía trị thì nó mới có thể hoạt động được. Tuy nhiên, trong phần này, bạn sẽ thấy là moi thứ không phải lúc nào cũng như thế.

Hãy xem xét hàm sau

int soGiay(int gio, int phut, int giay)
{
    int tongSo(0);

    tongSo = gio * 60 * 60;
    tongSo += phut * 60;
    tongSo += giay;

    return tongSoo;
}

Hàm này tính tổng số giaay dựa trên số giờ, phút, giây mà chúng ta cung cấp cho nó. Không có gì là khó hiểu ở đây. Các biến gio, phut, giay là thông số mà hàm soGiay() nhận vào để tiến hành xử lý.

Giá trị mặc định

Cái mới ở đây là chúng ta có thể đưa ra các giá trị mặc định cho các thông số và không cần phải cung cấp giá trị của tất cả các thông số cho hàm.

Để bắt đầu, chúng ta sẽ sử dụng 1 đoạn mã hoàn chỉnh mà chúng ta cùng chạy thử trong IDE.

#include <iostream>

using namespace std;

// Nguyen mau cua ham
int soGiay(int gio, int phut, int giay);

// Main
int main(){
    cout << soGiay(1, 10, 25) << endl;

    return 0;
}

// Dinh nghia ham
int soGiay(int gio, int phut, int giay){
    int tongSo(0);

    tongSo = gio * 60 * 60;
    tongSo += phut * 60;
    tongSo += giay;

    return tongSo;
}

Kết quả nhận được là

Nếu bạn vẫn còn nghi ngờ thì 1 giờ gồm 3600 giây, 10 phút là 600 giây, 25 giây là ... 25 giây và 3600 + 600+ 25 = 4225.

Tất cả mọi thứ hoạt động khá ổn.

Bây giờ, chúng ta muốn rằng 1 số thông số là không bắt buộc, ví dụ bởi vì chúng ta thường dùng số giờ hơn là các đại lượng phút và giây.

Chúng ta sẽ phải thay đổi nguyên mẫu của hàm (chú ý, là nguyên mẫu chứ không phải là định nghĩa của hàm). Trong nguyên mẫu, cần thay đổi để thêm vào các giá trị mặc định mà ta muốn đưa cho các thông số không bắt buộc

int soGiiay(int gio, int phut = 0, int giay = 0);

Trong ví dụ này, chỉ có thông số gio là bắt buộc. Nếu người dùng không nhập vào số phút và số giây thì các thông số này sẽ mang giá trị là 0 ở trong xử lý của hàm.

Đây là đoạn mã sau khi đã thay đổi

#include <iostream>

using namespace std;

// Nguyen mau cua ham
int soGiay(int gio, int phut = 0, int giay = 0);

// Main
int main(){
    cout << soGiay(1, 10, 25) << endl;

    return 0;
}

// Dinh nghia ham
int soGiay(int gio, int phut, int giay){
    int tongSo(0);

    tongSo = gio * 60 * 60;
    tongSo += phut * 60;
    tongSo += giay;

    return tongSo;
}

! Nếu các bạn đọc kỹ đoạn mã thì các bạn sẽ thấy là các giá trị mặc định chỉ được nhắc đến ở trong nguyên mẫu mà không phải trong đoạn mã định nghĩa của hàm. Nếu mã nguồn của bạn được chia ra thành nhiều tệp thì nó sẽ nằm trong tệp tiêu đề. Đừng lo, nếu các bạn nhầm lẫn thì trình biên dịch sẽ hiển thị cho các bạn 1 lỗi nằm ở dòng lệnh đầu của định nghĩa hàm.

Đoạn mã không thay đổi gì nhiều lắm và kết quả chúng ta nhận được vẫn giống như lúc trước. Khác biệt chỉ có thể thấy là bây giờ ở trong hàm main(), chúng ta có thể gọi hàm chỉ mà bớt đi 1 hoặc 2 thông số. Ví dụ

cout << soGiay(1) << endl;

Trình biên dịch sẽ xét từ trái qua phải. Bởi vì chỉ có 1 thong số và thông số cho giờ là bắt buộc nên giá trị 1 sẽ được truyền cho số giờ.

Kết quả nhận được là

Bạn cũng có thể thêm giá trị cho số phút nếu bạn muốn

cout << soGiay(1, 10) << endl;

Khi mà các bạn còn nhập đủ những thông số bắt buộc, mọi thứ còn đều hoạt động bình thường.

Các trường hợp đặc biệt, chú ý nguy hiểm

Vẫn có một số chú ý cần biết về việc sử dụng các giá trị mặc định và các thông số không bắt buộc. Chúng ta sẽ làm 1 danh sách hỏi/đáp các trường hợp hay gặp.

? Nếu tôi muốn đưa giá trị số giờ và giây nhưng không đưa ra giá trị số phút thì sao?

Thực ra thì, chuyện này là không thể. Bởi vì trình biên dịch đọc từ trái sang phải, vậy nên giá trị thứ nhất phải là số giờ, giá trị thứ 2 là số phút và cuối cùng là số giây.

Các bạn KHÔNG thể viết :

cout << soGiay(1,,25) << endl;

Trong C++, việc này bị cấm. Điều này có nghĩa là bạn không thể bỏ qua 1 thông số ở giữa nếu bạn muốn đưa ra giá trị cho thông số đầu và cuối. Bạn bắt buộc phải viết 

cout << soGiay(1, 0, 25) << endl;

? Liệu có thể thiết lập cho thông số giờ là không bắt buộc và phút với giây là bắt buộc không?

Nếu nguyên mẫu định nghĩa theo thứ tự chúng ta đã viết bên trên thì không. Tất cả các giá trị không bắt buộc phải nằm ở cuối.

Trình biên dịch sẽ không chấp nhận đoạn mã sau

int soGiay(int gio = 0, int phut, int giay);

Giải pháp cho tình huống này là đổi chỗ các thông số.

int soGiay(int giay, int phut, int gio = 0);

? Tất cả các thông số đều không bắt buộc thì sao?

Không có vấn đề gì với việc này

int soGiay(int gio = 0, int phut = 0, int giay = 0);

Lúc đó, khi gọi hàm bạn có thể viết

cout << soGiay() << endl;

Kết quả trả về sẽ là 0 trong trường hợp thử vừa rồi.

Những quy tắc cần nhớ

Tổng kết lại thì có 2 quy tắc cần nhớ cho các giá trị mặc định:

  • Các giá trị mặc định chỉ được viết trong nguyên mẫu
  • Các thông số không bắt buộc phải nằm ở cuối của danh sách thông số (bên phải)
Tóm tắt bài hoc :
  • Hàm là một đoạn mã thực hiện 1 xử lý công việc nhất định.
  • Tất cả các chương trình đều có hàm main(). Đó là điểm bắt đầu của chương trình.
  • Chia chương trình thành các hàm khác nhau xử lý các công việc khác nhau cho phép việc sắp xếp công việc dễ hơn.
  • 1 hàm có thể được gọi nhiều lần trong chương trình.
  • 1 hàm có thể nhận các dữ liệu đầu vào (thông số) và trả về 1 giá trị nhờ lệnh return.
  • Các hàm nhận vào các thông số qua tham chiếu có thể thay đổi trực tiếp nội dung ô nhớ của thông số đó.
  • Khi mà chương trình lớn, khuyến khích chia nhỏ ra thành các tệp chứa các nhóm hàm. Các tệp .cpp chứa định nghĩa của các hàm và các tệp .h chứa nguyên mẫu của chúng.Các tệp .h cho phép thông báo sự tồn tại của các hàm cho những tệp khác của chương trình.