[Học lập trình C++] Chương 1: 1.7 – tiền khai báo

1.7 – tiền khai báo
Hãy quan sát một chương trình ví dụ gọi là add.cpp:
#include <iostream>

int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is: " << add(3, 4) << endl;
    return 0;
}

int add(int x, int y)
{
    return x + y;
}

Bạn mong đợi chương trình sẽ cho ra kết quả:
The sum of 3 and 4 is: 7

Nhưng sự thật, nó không biên dịch được! Visual Studio 2005 Express sinh ra lỗi biên dịch sau:
add.cpp(6) : error C3861: 'add': identifier not found
add.cpp(10) : error C2365: 'add' : redefinition; previous definition was 'formerly unknown identifier'

Lý do chương trình này không biên dịch là bởi vì trình biên dịch đọc file một cách tuần tự. Khi trình biên dịch gặp lời gọi hàm tên add() trên dòng 6 của hàm main(), nó không biết add là gì, bởi vì chúng ta chưa định nghĩa hàm add() mãi cho tới dòng 10! Điều này sản sinh ra lỗi đầu tiên (“identifier not found”).
Khi Visual Studio 2005 gặp khai báo thực của hàm add() ở dòng 10, nó cũng báo lỗi là hàm add() đã redefined.
Mặc dù lỗi thứ hai bị thừa, nó hữu ích để chú ý rằng một cách phổ biến, lỗi đơn sản sinh ra nhiều lỗi kép khác hoặc cảnh báo khác.
Nguyên tắc: Khi xác định lỗi biên dịch trong chương trình của bạn thì luôn luôn giải quyết lỗi đầu tiên trước.
Để giải quyết vấn đề này, chúng ta cần xác định sự thật rằng trình biên dịch không biết add là gì. Có 2 cách phổ biến để giải quyết vấn đề này.
Lựa chọn 1: Định nghĩa lại hàm trước hàm main()
#include <iostream>

int add(int x, int y)
{
    return x + y;
}

int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is: " << add(3, 4) << endl;
    return 0;
}

Theo cách này, đến thời điểm mà hàm main gọi hàm add(), trình biên dịch đã biết hàm add. Bởi vì đây là một chương trình đơn giản, nên rất dễ thay đổi. Tuy nhiên trong một chương trình lớn, rất chán khi phải cố gắng tìm ra hàm nào gọi hàm nào để mà khai báo một cách tuần tự.
Hơn nữa lựa chọn này không phải lúc nào cũng thực hiện được. Hãy giả sử bạn đang viết một chương trình có hai hàm A và B. Nếu hàm A gọi hàm Bm và hàm gọi hàm A, thì không có cách nào để cả hai có thể thực hiện được. Nếu bạn định nghĩa hàm A trước, trình biên dịch sẽ không biết B là gì. Còn nếu bạn định nghĩa hàm B trước, trình biên dịch sẽ không biết A là gì.
Nguyên mẫu hàm và tiền khai báo của hàm
Lựa chọn 2: Sử dụng tiền khai báo
Một tiền khai báo cho phép chúng ta nói với trình biên dịch về sự tồn tại của một định danh trước khi thực sự định nghĩa nó.
Trong trường hợp của hàm, điều này cho phép chúng ta nói với tình biên dịch về sự tồn tại của một hàm tước khi chúng ta định nghĩa thân hàm. Theo cách này, khi trình biên dịch gặp phải một lời gọi hàm, nó sẽ hiểu rằng chúng ta đang gọi hàm đúng, mặc dù nó chưa biết nó như thế nào hoặc nơi nào hàm được định nghĩa.
Để viết một hàm tiền khai báo cho một hàm, chúng ta sử dụng một câu lệnh khai báo gọi là nguyên mẫu hàm. Nguyên mẫu hàm chứa đụng kiểu trả về của hàm, tên, tham số, nhưng không có thân hàm (Phần nằm giữa dấu ngoặc móc). Và bởi vì nguyên mẫu hàm là một câu lệnh nên nó kết thúc bởi dấu chấm phẩy.
Đây là một nguyên mẫu hàm cho hàm add:
int add(int x, int y); // function prototype includes return type, name, parameters, and semicolon.  No function body!
Chương trình trở thành:
#include <iostream>

int add(int x, int y); // forward declaration of add() (using a function prototype)

int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is: " << add(3, 4) << endl; // this works because we forward declared add() above
    return 0;
}

int add(int x, int y) // even though the body of add() isn't defined until here
{
    return x + y;
}

Bây giờ khi trình biên dịch gặp hàm add() trong hàm main, nó biết hàm add như thế nào (Một hàm mà có hai tham số và trả về kiểu integer), và nó sẽ không báo lỗi.
Điểm đáng chú ý là nguyên mẫu hàm không cần tên cụ thể của tham số. Trong code phía trên, bạn có thể tiền khai báo như sau:

int add(int, int);

Tuy nhiên, chúng tôi nên sử dụng tên cho tham số, bởi vì nó cho phép bạn hiểu những tham số của hàm làm gì khi chỉ nhìn vào nguyên mẫu hàm. Nếu không, bạn sẽ phải tìm đến định nghĩa của hàm thật.
Mẹo: Bạn có thể dễ dàng tạo nguyên mẫu hàm bằng cách sử dụng cắt/dán tại định nghĩa hàm. Đừng quên dấu chấm phẩy ở phần kết thúc.
Quên thân hàm
Một câu hỏi đặt ra của nhiều người mới lập trình đó là: Chuyện gì sẽ xảy ra nếu chúng ta tiền khai báo một hàm nhưng lại không định nghĩa nó?
Câu trả lời là: Còn tùy. Nếu tiền khai báo được tạo, nhưng hàm không bao giờ được gọi, chương trình sẽ biên dịch vào chạy tốt. Tuy nhiên, nếu một tiền khai báo được tạo, khi hàm được gọi, nhưng chương trình chưa định nghĩa nó, chương trình vẫn biên dịch được nhưng linker sẽ báo lỗi vì không thể tìm ra được việc gọi hàm nào.
Xem xét một chương trình sau:
#include <iostream>

int add(int x, int y); // forward declaration of add() using function prototype

int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is: " << add(3, 4) << endl;
    return 0;
}

 Trong chương trình này, chúng ta khai báo hàm add(), và chúng ta gọi hàm add(), nhưng chưa định nghĩa nó. Khi chúng ta thử biên dịch chương trình này. Visual Studio 2005 Express tạo ra thông điệp sau:
Compiling...
add.cpp
Linking...
add.obj : error LNK2001: unresolved external symbol "int __cdecl add(int,int)" (?add@@YAHHH@Z)
add.exe : fatal error LNK1120: 1 unresolved externals

Như bạn thấy, chương trình biên dịch được, nhưng không thể link bởi vì hàm int add(int,int) chưa được định nghĩa.
Kiểu khác của tiền khai báo
Tiền khai báo hầu hết được sử dụng với hàm. Tuy nhiên, tiền khai báo cũng có thể được sử dụng với định danh khác trong C++, ví dụ như biến và kiểu người dùng định nghĩa. Những định danh khác có cú pháp khác cho tiền khai báo.
Chúng ta sẽ nói nhiều hơn về làm thế nào để khai báo trước một kiểu định danh khác trong các bài học kế tiếp.
Khai báo và định nghĩa
Trong C++, bạn thường nghe từ “Khai báo” và “Định nghĩa”. Vậy chúng nghĩa là gì? Bây giờ bạn đã có đủ cơ sở để hiểu sự khác nhau giữa hai cái này.
Một định nghĩa thực ra là một hiện thực hoặc thực thi (Được cấp bộ nhớ). Đây là một ví dụ của định nghĩa.
int add(int x, int y) // defines function add()
{
    return x + y;
}

int x; // instantiates (causes memory to be allocated for) an integer variable named x

Bạn chỉ có thể có một định nghĩa cho mỗi định danh. Một định nghĩa cần phải thỏa mãn linhker.
Một khai báo là một câu lệnh định nghĩa một định danh (biến hoặc tên hàm) và kiểu của nó. Đây là một vài ví dụ về khai báo:
int add(int x, int y); // declares a function named "add" that takes two int parameters and returns an int.  No body!
int x; // declares an integer variable named x

Một khai báo là tất cả mọi thứ để thỏa mãn trình biên dịch. Điều này giải thích tại sao sử dụng tiền khai báo là đáp ứng trình biên dịch. Tuy nhiên, nếu bạn quên định nghĩa định danh đó thì linker sẽ báo lỗi.
Bạn sẽ chú ý rằng “int x” xuất hiện hai lần trong cả hai kiểu khai báo và định nghĩa. Trong C++, tất cả mọi định nghĩa đều cũng là khai báo. Do đó “int x” là một định nghĩa, mặc định nó cũng là khai báo. Đây là trường hợp của hầu hết các khai báo.
Tuy nhiên, có một tập hợp nhỏ các khai báo mà không phải là định nghĩa, ví dụ như nguyên mẫu hàm. Những trường hơp như vậy gọi là pure declarations. Kiểu khác của pure declarations bao gồm tiền khai báo cho biến, khai báo class, và khai báo kiểu (bạn sẽ gặp những trường hợp này trong bài học sau nhưng không cần lo lắng về chúng. Bạn có thể có nhiều pure declarations cho định danh theo mong muốn của bạn (mặc dù có nhiều hơn một thì thông thường là thừa)
Nguồn: learncpp.com

Nhận xét

Bài đăng phổ biến