Nội dung bài viết
Trần Thị Thu Hiền Lỗi “identifier not found” là một lỗi thường thấy khi build chương trình, đặc biệt là với những chương trình lớn khi mà việc sắp xếp các hàm hoặc trình tự #include các file header trở nên phức tạp. Khi đó việc sử dụng forward declaration là một giải pháp hữu hiệu.

Giới thiệu

Lỗi “identifier not found” là một lỗi thường thấy khi build chương trình, đặc biệt là với những chương trình lớn khi mà việc sắp xếp các hàm hoặc trình tự #include các file header trở nên phức tạp. Khi đó việc sử dụng forward declaration là một giải pháp hữu hiệu.

Tiền đề bài viết

Trong khi theo học tại STDIO Training, tôi gặp phải một số vấn đề khi phải #include quá nhiều file header. Nhờ sự hướng dẫn của các giảng viên tôi đã biết thêm về forward declaration và mong muốn có thể chia sẻ lại kiến thức này. Hy vọng bài viết sẽ giúp ích cho các bạn.

Đối tượng hướng đến

Những bạn đang học lập trình C/C++ đang bị vướng phải lỗi “identifier not found” hay có mong muốn tìm hiểu về forward declaration.

Lỗi “identifier not found”

#include <stdio.h>

int main()
{
	printf("The sum of 1 and 2 is: ", add(1, 2));
	return 0;
}

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

Khi thực hiện đoạn code trên, chúng ta hy vọng kết quả hiện lên màn hình sẽ là “The sum of 1 and 2 is: 3”, nhưng thực tế khi compile chương trình sẽ báo lỗi:

Example2

Lí do xuất hiện lỗi trên là vì compiler compile từng dòng liên tục, nên khi compiler đi đến lời gọi hàm add() ở dòng thứ 5 nó sẽ không biết add() là gì, vì cho đến dòng thứ 9 chương trình mới định nghĩa add(). Điều này gây ra lỗi “identifier not found”.

Giải quyết vấn đề

Có hai cách để giải quyết vấn đề này:

Cách 1: Sắp xếp lại, đưa phần định nghĩa hàm add() lên trước hàm main() 

#include <stdio.h>

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

int main()
{
	printf("The sum of 1 and 2 is: ", add(1, 2));
	return 0;
}

Bằng cách đó, khi hàm main() gọi hàm add(), trình biên dịch sẽ biết gì add() là gì. 

Bởi vì đây là một chương trình đơn giản, nên cách này tương đối dễ thực hiện. Tuy nhiên, trong một chương trình lớn hơn, cách này có thể làm chúng ta mệt mỏi khi phải cố gắng mường tượng hàm nào sẽ gọi hàm nào (và trật tự của nó) để chúng có thể được khai báo theo tuần tự.

Hơn nữa, cách này không phải lúc nào cũng khả thi. Với trường hợp chúng ta đ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 B, và hàm B gọi hàm A, khi đó không có cách nào để sắp xếp các hàm mà cả hai sẽ được định nghĩa trước khi gọi. Nếu bạn định nghĩa A trước B, trình biên dịch sẽ bảo là nó không biết B là gì. Nếu bạn định nghĩa B trước A, trình biên dịch sẽ bảo rằng nó không biết A là gì.

Cách 2: Sử dụng forward decleration

Một forward declaration cho phép chúng ta hẹn với compiler về sự tồn tại của một ký hiệu nhận dạng (một type, function hay class nào đó) trước khi thực sự định nghĩa nó.

Sau đây tôi xin giới thiệu một số loại forward declaration thường sử dụng.

Forward declaration sử dụng prototype cho function

Trong trường hợp của các function, khi sử dụng forward declaration tức là chúng ta báo với compiler rằng có tồn tại của hàm đó ngay cả khi ta chưa định nghĩa nó. Bằng cách này khi compiler gặp phải một lời gọi của một hàm đã được hẹn trước bằng forward declaration, nó sẽ biết rằng chúng ta đang thực hiện một lời gọi hàm và kiểm tra xem chúng ta đã gọi hàm chính xác chưa (các đầu vào đầu ra có hợp lệ không) mặc dù compiler vẫn chưa biết hàm đó thực hiện những công việc gì hay là nó được định nghĩa ở đâu.

Để sử dụng forward declaration cho hàm, chúng ta sử dụng một câu lệnh declaration được gọi là function prototype (nguyên mẫu hàm). Một function prototype gồm kiểu trả về của hàm, tên hàm, các tham số, tham biến, không bao gồm phần định nghĩa hàm, và được kết thúc bằng dấu chấm phẩy. 

Dưới đây là function prototype cho hàm add():

int add(int a, int b);

Và đây là chương trình đầy đủ:

#include <stdio.h>

int add(int a, int b);

int main()
{
	printf("The sum of 1 and 2 is: ", add(1, 2));
	return 0;
}

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

Trong đoạn code trên bạn cũng có thể đặt forward declaration như thế này:

int add(int , int );

Tuy nhiên, tôi thích đặt tên cho các tham số hơn, bởi vì nó giúp bạn hiểu những tham số của hàm khi chỉ nhìn vào prototype, nếu không bạn phải xác định vị trí thực tế định nghĩa hàm.

Forward declaration dành cho class

Đối với những chương trình phức tạp hơn, gồm nhiều class mỗi class được định nghĩa trong  file hearder và phần mô tả nằm trong file cpp, khi bạn muốn sử dụng một class A được định nghĩa trong file A.h trong một class B được định nghĩa trong file B.h, thì chúng ta có thể #include file A.h vào trong file B.h. 

Tuy nhiên, C++ còn cho phép chúng ta cũng đặt một forward class declaration cho class A trong class B nếu như phần định nghĩa của class B không đề cập gì tới những nội dụng được định nghĩa trong class A, và nó có dạng như sau:

class A;

Bằng cách này, chúng ta có thể giảm một số lượng các file header cần #include, điều này có thể làm tăng tốc độ compile chương trình.

Bàn luận thêm

Những trường hợp có thể dùng forward class declaration

Đặt mình vào vị trí của compiler, khi bạn đặt một forward declaration cho class A, compiler biết là class này có tồn tại. Nó không biết gì về kích thước, các thuộc tính hoặc các phương thức của class A. Do đó, bạn không thể sử dụng class A một cách trọn vẹn ở trong class B vì khi đó nó cần biết đầy đủ thiết kế của class A. Điều này dẫn đến chúng ta chỉ nên dùng forward declaration class A khi phần định nghĩa của class B đạt những yếu tố sau:

-    Chỉ chứa dữ liệu kiểu con trỏ A chứ không chứa dữ liệu kiểu A.

-    Không truy cập bất kỳ phương thức hay thuộc tính nào của A, nếu có nhu cầu muốn sử dụng thì nên để nó trong file B.cpp và #include file A.h vào file B.cpp.

Một ví dụ đơn giản:

class A;

class B
{
	private:
		A* m_A;
};

Làm thế nào để forward declaration có thể giảm thời gian compile chương trình?

Như các bạn đã biết, khi thực hiện compile, đầu tiên compiler sẽ copy toàn bộ nội dung của file.h vào trong những file có #include nó. Nếu như B rất lớn và phức tạp, như vậy bạn nên #include file B.h càng ít càng tốt như vậy sẽ giảm thời gian compile đi. 

Trường hợp khác, giả sử bạn đã compile thành công chương trình rồi, sau đó bạn muốn thay đổi nội dung trong B.h, và recompile chương trình, khi đó tất cả những file #include file B.h đều phải recompile, ví dụ như file A.h, khi đó file A.h lại bị thay đổi, vậy những file #include file A.h cũng phải recompile,… đối với những chương trình nhỏ, điều này có thể không gây ảnh hưởng mấy, những với chương trình lớn và phức tạp thì đây lại là một vấn đề lớn.

Do đó chúng ta nên sử dụng forward declaration và đặt #include file header ở file .cpp để giảm thời gian compile. 

Chuyện gì xảy ra khi dùng forward declaration nhưng quên định nghĩa hàm / class?

Nếu như forward declaration được tạo và bạn không sử dụng tới nó trong chương trình, thì chương trình vẫn compile thành công và chạy tốt, nhưng nếu như bạn có sử dụng tới nó nhưng lại quên định nghĩa thì chương trình vẫn compile tạo được file .obj thành công nhưng linker sẽ báo lỗi vì không giải quyết được lời gọi hàm, class.

Example7

Lời cảm ơn

Cám ơn anh La Kiến Vinh và STDIO Training đã cho tôi những kiến thức nền tảng sâu sắc nhất mà tôi không thể tìm được ở đâu, các giải đáp thắc mắc về lập trình và đa nền tảng và Khoa học máy tính. Cảm ơn anh đã thắp lại cho tôi ngọn lửa đam mê lập trình.  Cám ơn các bạn tại STDIO Training đã giúp đỡ tôi thời gian qua.

THẢO LUẬN
ĐÓNG