1. Vấn đề định nghĩa trùng lặp

Trong bài – Thứ tự Khai báo và định nghĩa chuyển tiếp, chúng ta lưu ý rằng một biến định danh hoặc hàm chỉ có thể có một định nghĩa (quy tắc một định nghĩa). Do đó, một chương trình khai báo một ký hiệu nhận dạng biến nhiều lần sẽ gây ra lỗi biên dịch:

int main()
{
    int x; // this is a definition for variable x
    int x; // compile error: duplicate definition
 
    return 0;
}

Tương tự, các chương trình khai báo một hàm nhiều lần cũng sẽ gây ra lỗi biên dịch:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

#include <iostream>
 
int foo() // this is a definition for function foo
{
    return 5;
}
 
int foo() // compile error: duplicate definition
{
    return 5;
}
 
int main()
{
    std::cout << foo();
    return 0;
}

Mặc dù các chương trình này dễ sửa (loại bỏ định nghĩa trùng lặp), với các file header, khá dễ dẫn đến tình huống một định nghĩa trong file header được đưa vào nhiều lần. Điều này có thể xảy ra khi file header #include file header khác (điều này thường xảy ra).

Hãy xem xét ví dụ học thuật sau:

square.h:

// We shouldn't be including function definitions in header files
// But for the sake of this example, we will
int getSquareSides()
{
    return 4;
}

geometry.h:

#include "square.h"

main.cpp:

#include "square.h"
#include "geometry.h"
 
int main()
{
    return 0;
}

Chương trình trông có vẻ đơn giản này sẽ không biên dịch! Đây là những gì đang xảy ra. Đầu tiên, main.cpp #includes square.h, sao chép định nghĩa cho hàm getSquareSides vào main.cpp. Sau đó, main.cpp #include geometry.h, chính là geometry file #include square.h. Thao tác này sao chép nội dung của square.h (bao gồm cả định nghĩa cho hàm getSquareSides) vào geometry.h, sau đó được sao chép vào main.cpp.

Do đó, sau khi giải quyết tất cả #includes, main.cpp sẽ trông như thế này:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

int getSquareSides()  // from square.h
{
    return 4;
}
 
int getSquareSides() // from geometry.h (via square.h)
{
    return 4;
}
 
int main()
{
    return 0;
}

Định nghĩa trùng lặp và lỗi biên dịch. Mỗi file, riêng lẻ, đều tốt. Tuy nhiên, vì main.cpp kết thúc #including nội dung của square.h hai lần, chúng ta đã gặp sự cố. Nếu geometry.h cần getSquareSides() và main.cpp cần cả geometry.h và square.h, bạn sẽ giải quyết vấn đề này như thế nào?

2. Bảo vệ header

Tin tốt là chúng ta có thể tránh được vấn đề trên thông qua một cơ chế được gọi là bộ bảo vệ header (còn gọi là bộ bảo vệ include). Bảo vệ Header là các lệnh biên dịch có điều kiện có dạng sau:

#ifndef SOME_UNIQUE_NAME_HERE
#define SOME_UNIQUE_NAME_HERE
 
// your declarations (and certain types of definitions) here
 
#endif

E_UNIQUE_NAME_HERE sẽ đã được khai báo ngay từ lần đầu tiên nội dung của header được đưa vào và nội dung của header sẽ bị bỏ qua (nhờ #ifndef).

Tất cả các file header của bạn phải có bảo vệ header trên chúng. SOME_UNIQUE_NAME_HERE có thể là bất kỳ tên nào bạn muốn, nhưng theo quy ước được đặt thành tên file đầy đủ của file header, được nhập toàn bộ bằng chữ hoa, sử dụng dấu gạch dưới cho khoảng trắng hoặc dấu câu. Ví dụ: square.h sẽ có phần bảo vệ header:

square.h:

#ifndef SQUARE_H
#define SQUARE_H
 
int getSquareSides()
{
    return 4;
}
 
#endif

Ngay cả các header của thư viện chuẩn cũng sử dụng bảo vệ header . Nếu bạn xem file header iostream từ Visual Studio, bạn sẽ thấy:

#ifndef _IOSTREAM_
#define _IOSTREAM_
 
// content here
 
#endif

Cập nhật ví dụ trước của chúng ta với bộ bảo vệ header:

Hãy quay lại ví dụ square.h, sử dụng square.h với các bộ bảo vệ header. Để có hình thức tốt, chúng ta cũng sẽ thêm các phần bảo vệ header cho geometry.h.

#ifndef SQUARE_H
#define SQUARE_H
 
int getSquareSides()
{
    return 4;
}
 
#endif

geometry.h:

#ifndef GEOMETRY_H
#define GEOMETRY_H
 
#include "square.h"
 
#endif

main.cpp:

#include "square.h"
#include "geometry.h"
 
int main()
{
    return 0;
}

Sau khi bộ xử lý tiền xử lý giải quyết tất cả các include, chương trình này trông giống như sau:

main.cpp

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

#ifndef SQUARE_H // square.h included from main.cpp,
#define SQUARE_H // SQUARE_H gets defined here
 
// and all this content gets included
int getSquareSides()
{
    return 4;
}
 
#endif // SQUARE_H
 
#ifndef GEOMETRY_H // geometry.h included from main.cpp
#define GEOMETRY_H
#ifndef SQUARE_H // square.h included from geometry.h, SQUARE_H is already defined from above
#define SQUARE_H // so none of this content gets included
 
int getSquareSides()
{
    return 4;
}
 
#endif // SQUARE_H
#endif // GEOMETRY_H
 
int main()
{
    return 0;
}

Như bạn có thể thấy từ ví dụ, phần bao gồm nội dung thứ hai của square.h (từ geometry.h) bị bỏ qua vì SQUARE_H đã được khai báo từ phần include đầu tiên. Do đó, hàm getSquareSides chỉ được đưa vào một lần.

Trình bảo vệ header không ngăn header được include một lần vào các file code khác nhau

Lưu ý rằng mục tiêu của trình bảo vệ header là ngăn một file code nhận nhiều hơn một bản sao của header file được bảo vệ. Theo thiết kế, bộ bảo vệ header không ngăn một file header nhất định được đưa vào (một lần) vào các file code riêng biệt. Điều này cũng có thể gây ra sự cố không mong muốn. Xem xét:

square.h:

#ifndef SQUARE_H
#define SQUARE_H
 
int getSquareSides()
{
    return 4;
}
 
int getSquarePerimeter(int sideLength); // forward declaration for getSquarePerimeter
 
#endif

square.cpp:

#include "square.h"  // square.h is included once here
 
int getSquarePerimeter(int sideLength)
{
    return sideLength * getSquareSides();
}

main.cpp:

#include "square.h" // square.h is also included once here
#include <iostream>
 
int main()
{
    std::cout << "a square has " << getSquareSides() << " sides\n";
    std::cout << "a square of length 5 has perimeter length " << getSquarePerimeter(5) << '\n';
 
    return 0;
}

Lưu ý rằng square.h được include từ cả main.cpp và square.cpp. Điều này có nghĩa là nội dung của square.h sẽ được đưa một lần vào square.cpp và một lần vào main.cpp.

Hãy xem xét lý do tại sao điều này xảy ra chi tiết hơn. Khi square.h được include từ square.cpp, SQUARE_H được khai báo cho đến cuối file square.cpp. Định nghĩa này ngăn không square.h được đưa vào square.cpp lần thứ hai (là điểm của các trình bảo vệ header). Tuy nhiên, khi square.cpp kết thúc, SQUARE_H không còn được coi là đã định nghĩa. Điều này có nghĩa là khi bộ tiền xử lý chạy trên main.cpp, SQUARE_H ban đầu không được định nghĩa trong main.cpp.

Kết quả cuối cùng là cả square.cpp và main.cpp đều nhận được bản sao định nghĩa của getSquareSides. Chương trình này sẽ biên dịch, nhưng trình liên kết sẽ phàn nàn về việc chương trình của bạn có nhiều định nghĩa cho getSquareSides được nhận dạng!

Cách tốt nhất để giải quyết vấn đề này chỉ đơn giản là đặt định nghĩa hàm vào một trong các file .cpp để header chỉ chứa một khai báo chuyển tiếp:

square.h:

#ifndef SQUARE_H
#define SQUARE_H
 
int getSquareSides(); // forward declaration for getSquareSides
int getSquarePerimeter(int sideLength); // forward declaration for getSquarePerimeter
 
#endif

square.cpp:

#include "square.h"
 
int getSquareSides() // actual definition for getSquareSides
{
    return 4;
}
 
int getSquarePerimeter(int sideLength)
{
    return sideLength * getSquareSides();
}

main.cpp:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

#include "square.h" // square.h is also included once here
#include <iostream>
 
int main()
{
    std::cout << "a square has " << getSquareSides() << "sides\n";
    std::cout << "a square of length 5 has perimeter length " << getSquarePerimeter(5) << '\n';
 
    return 0;
}

Bây giờ khi chương trình được biên dịch, hàm getSquareSides sẽ chỉ có một định nghĩa (thông qua square.cpp), vì vậy trình liên kết rất vui. file main.cpp có thể gọi hàm này (mặc dù nó sống trong square.cpp) vì nó bao gồm square.h, có khai báo chuyển tiếp cho hàm (trình liên kết sẽ kết nối lệnh gọi getSquareSides từ main.cpp với định nghĩa của getSquareSides trong square.cpp).

3. Chúng ta có thể tránh các định nghĩa trong file header không?

Nói chung, chúng ta đã nói với bạn rằng không bao gồm các định nghĩa hàm trong header file của bạn. Vì vậy, bạn có thể tự hỏi tại sao bạn nên include các bảo vệ header nếu chúng bảo vệ bạn khỏi những điều bạn không nên làm.

Trong tương lai, chúng ta sẽ chỉ cho bạn khá nhiều trường hợp cần phải đưa các định nghĩa hàm vào file header. Ví dụ, C ++ sẽ cho phép bạn tạo các kiểu của riêng mình. Các kiểu do người dùng định nghĩa này thường được khai báo trong các file header, vì vậy định nghĩa có thể được truyền ra các file code cần sử dụng chúng. Nếu không có bộ bảo vệ header, các file code của bạn có thể có nhiều bản sao giống hệt nhau của các định nghĩa này, điều này sẽ gây ra lỗi biên dịch định nghĩa trùng lặp.

Vì vậy, mặc dù không nhất thiết phải có bảo vệ header vào thời điểm này trong loạt bài hướng dẫn, nhưng chúng ta đang thiết lập những thói quen tốt ngay bây giờ, vì vậy bạn không phải bỏ học những thói quen xấu sau này.

4. #pragma một lần

Nhiều trình biên dịch hỗ trợ một dạng bảo vệ header thay thế, đơn giản hơn bằng cách sử dụng chỉ thị #pragma:

#pragma once
 
// your code here

#pragma từng phục vụ cùng mục đích như trình bảo vệ header và có thêm lợi ích là ngắn hơn và ít mắc lỗi hơn.

Tuy nhiên, #pragma một lần không phải là một phần chính thức của ngôn ngữ C ++ và không phải tất cả các trình biên dịch đều hỗ trợ nó (mặc dù hầu hết các trình biên dịch hiện đại đều có).

Vì mục đích tương thích, chúng ta khuyên bạn nên sử dụng các trình bảo header truyền thống. Chúng không còn nhiều công việc nữa và chúng được đảm bảo sẽ được hỗ trợ trên tất cả các trình biên dịch tuân thủ.

5. Tóm lược

Trình bảo vệ header được thiết kế để đảm bảo rằng nội dung của một file header nhất định không được sao chép nhiều lần vào bất kỳ file đơn lẻ nào, để ngăn các định nghĩa trùng lặp.

Lưu ý rằng các khai báo trùng lặp là tốt, vì một khai báo có thể được khai báo nhiều lần mà không xảy ra sự cố – nhưng ngay cả khi file header của bạn bao gồm tất cả các khai báo (không có định nghĩa) thì cách tốt nhất vẫn là bao gồm trình bảo vệ header.

Lưu ý rằng bộ bảo vệ header không ngăn nội dung của file header được sao chép (một lần) vào các file dự án riêng biệt. Đây là một điều tốt, vì chúng ta thường cần tham chiếu nội dung của một header nhất định từ các file dự án khác nhau.

Cài ứng dụng cafedev để dễ dàng cập nhật tin và học lập trình mọi lúc mọi nơi tại đây.

Nguồn và Tài liệu tiếng anh tham khảo:

Tài liệu từ cafedev:

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!