[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
Đăng nhận xét