Trong bài học trước (Liên kết nội bộ), Cafedev đã thảo luận về cách liên kết nội bộ giới hạn việc sử dụng code định danh cho một file duy nhất. Trong bài học này, chúng ta sẽ khám phá khái niệm liên kết bên ngoài.

Một code định danh có liên kết bên ngoài có thể được nhìn thấy và sử dụng cả từ file mà nó được khai báo và từ các file code khác (thông qua khai báo chuyển tiếp). Theo nghĩa này, các ký hiệu nhận dạng có liên kết bên ngoài thực sự “toàn cầu” ở chỗ chúng có thể được sử dụng ở bất kỳ đâu trong chương trình của bạn!

1. Các hàm có liên kết bên ngoài theo mặc định

Trong bài  – Chương trình có nhiều file code, bạn đã biết rằng bạn có thể gọi một hàm được định nghĩa trong một file từ file khác. Điều này là do các hàm có liên kết bên ngoài theo mặc định.

Để gọi một hàm được định nghĩa trong một file khác, bạn phải đặt một khai báo chuyển tiếp cho hàm trong bất kỳ file nào khác muốn sử dụng hàm. Khai báo chuyển tiếp cho trình biên dịch biết về sự tồn tại của hàm và trình liên kết kết nối các lệnh gọi hàm với định nghĩa hàm thực.

Đây là một ví dụ:

#include <iostream>
 
void sayHi() // this function has external linkage, and can be seen by other files
{
    std::cout << "Hi!";
}

main.cpp

void sayHi(); // forward declaration for function sayHi, makes sayHi accessible in this file
 
int main()
{
    sayHi(); // call to function defined in another file, linker will connect this call to the function definition
 
    return 0;
}

Kết quả

Hi!

Trong ví dụ trên, khai báo phía trước của hàm sayHi() trong main.cpp cho phép main.cpp truy cập vào hàm sayHi() được định nghĩa trong a.cpp. Khai báo chuyển tiếp thỏa mãn trình biên dịch và trình liên kết có thể liên kết lệnh gọi hàm với định nghĩa hàm.

Nếu hàm sayHi () có liên kết nội bộ thay vào đó, trình liên kết sẽ không thể kết nối lệnh gọi hàm với định nghĩa hàm và sẽ xảy ra lỗi trình liên kết.

2. Các biến toàn cục có liên kết bên ngoài

Các biến toàn cục có liên kết bên ngoài đôi khi được gọi là các biến bên ngoài. Để tạo một biến toàn cục bên ngoài (và do đó các file khác có thể truy cập được), chúng ta có thể sử dụng từ khóa extern để làm như vậy:

int g_x { 2 }; // non-constant globals are external by default
 
extern const int g_y { 3 }; // const globals can be defined as extern, making them external
extern constexpr int g_z { 3 }; // constexpr globals can be defined as extern, making them external (but this is useless, see the note in the next section)
 
int main()
{
    return 0;
}

Các biến toàn cục không phải const là bên ngoài theo mặc định (nếu được sử dụng, từ khóa extern sẽ bị bỏ qua).

3. Khai báo biến chuyển tiếp thông qua từ khóa extern

Để thực sự sử dụng một biến toàn cục bên ngoài đã được khai báo trong một file khác, bạn cũng phải đặt một khai báo chuyển tiếp cho biến toàn cục trong bất kỳ file nào khác muốn sử dụng biến. Đối với các biến, việc tạo khai báo chuyển tiếp cũng được thực hiện thông qua từ khóa extern (không có giá trị khởi tạo).

Đây là một ví dụ về việc sử dụng khai báo chuyển tiếp biến:

a.cpp

// global variable definitions
int g_x { 2 }; // non-constant globals have external linkage by default
extern const int g_y { 3 }; // this extern gives g_y external linkage

main.cpp:

#include <iostream>
 
extern int g_x; // this extern is a forward declaration of a variable named g_x that is defined somewhere else
extern const int g_y; // this extern is a forward declaration of a const variable named g_y that is defined somewhere else
 
int main()
{
    std::cout << g_x; // prints 2
 
    return 0;
}

Trong ví dụ trên, a.cpp và main.cpp đều tham chiếu đến cùng một biến toàn cục có tên là g_x. Vì vậy, mặc dù g_x được định nghĩa và khởi tạo trong a.cpp, chúng ta vẫn có thể sử dụng giá trị của nó trong main.cpp thông qua khai báo chuyển tiếp của g_x.

Lưu ý rằng từ khóa extern có các ý nghĩa khác nhau trong các ngữ cảnh khác nhau. Trong một số ngữ cảnh, extern có nghĩa là “cung cấp cho mối liên kết bên ngoài có thể thay đổi này”. Trong các ngữ cảnh khác, extern có nghĩa là “đây là một khai báo chuyển tiếp cho một biến bên ngoài được định nghĩa ở một nơi khác”. Vâng, điều này thật khó hiểu, vì vậy chúng tôi tóm tắt tất cả những cách sử dụng này trong bài  – Tóm tắt phạm vi, thời lượng và liên kết.

Lưu ý – Nếu bạn muốn khai báo một biến toàn cục không phải const chưa khởi tạo, không sử dụng từ khóa extern, nếu không C ++ sẽ nghĩ rằng bạn đang cố gắng thực hiện khai báo chuyển tiếp cho biến.

Lưu ýMặc dù các biến constexpr có thể được cung cấp liên kết bên ngoài thông qua từ khóa extern, nhưng chúng không thể được khai báo phía trước, vì vậy không có giá trị nào trong việc cung cấp cho chúng liên kết bên ngoài.

Lưu ý rằng khai báo chuyển tiếp hàm không cần từ khóa extern – trình biên dịch có thể cho biết bạn đang xác định một hàm mới hay khai báo chuyển tiếp dựa trên việc bạn có cung cấp nội dung hàm hay không. Khai báo chuyển tiếp biến cần từ khóa extern để giúp phân biệt các định nghĩa biến với khai báo chuyển tiếp biến (chúng trông giống hệt nhau):

// non-constant 
int g_x; // variable definition (can have initializer if desired)
extern int g_x; // forward declaration (no initializer)
 
// constant
extern const int g_y { 1 }; // variable definition (const requires initializers)
extern const int g_y; // forward declaration (no initializer)

4. Phạm vi file so với phạm vi toàn cầu

Các thuật ngữ “phạm vi file” và “phạm vi toàn cầu” có xu hướng gây nhầm lẫn và điều này một phần là do cách chúng được sử dụng không chính thức. Về mặt kỹ thuật, trong C ++, tất cả các biến toàn cục đều có “phạm vi file” và thuộc tính liên kết kiểm soát việc chúng có thể được sử dụng trong các file khác hay không.

Hãy xem xét chương trình sau:

global.cpp:

int g_x { 2 }; // external linkage by default
// g_x goes out of scope here

main.cpp:

extern int g_x; // forward declaration for g_x -- g_x can be used beyond this point in this file
 
int main()
{
    std::cout << g_x; // should print 2
 
    return 0;
}
// the forward declaration for g_x goes out of scope here

Biến g_x có phạm vi file trong global.cpp – nó có thể được sử dụng từ điểm định nghĩa đến cuối file, nhưng không thể nhìn thấy trực tiếp bên ngoài global.cpp.

Bên trong main.cpp, khai báo chuyển tiếp của g_x cũng có phạm vi file – nó có thể được sử dụng từ điểm khai báo đến cuối file.

Tuy nhiên, một cách không chính thức, thuật ngữ “phạm vi file” thường được áp dụng cho các biến toàn cục có liên kết nội bộ và “phạm vi toàn cầu” cho các biến toàn cầu có liên kết bên ngoài (vì chúng có thể được sử dụng trong toàn bộ chương trình, với các khai báo chuyển tiếp thích hợp).

5. Vấn đề thứ tự khởi tạo của các biến toàn cục

Khởi tạo các biến toàn cục xảy ra như một phần của quá trình khởi động chương trình, trước khi thực thi hàm chính. Quá trình này diễn ra trong hai giai đoạn.

Giai đoạn đầu tiên được gọi là khởi tạo tĩnh. Trong giai đoạn khởi tạo tĩnh, các biến toàn cục có bộ khởi tạo constexpr (bao gồm cả các chữ) được khởi tạo cho các giá trị đó. Ngoài ra, các biến toàn cục không có bộ khởi tạo được khởi tạo bằng 0.

Giai đoạn thứ hai được gọi là khởi tạo động. Giai đoạn này phức tạp hơn và nhiều sắc thái hơn, nhưng ý chính của nó là các biến toàn cục có bộ khởi tạo không phải constexpr được khởi tạo.

Dưới đây là một ví dụ về trình khởi tạo không phải constexpr:

int init()
{
    return 5;
}
 
int g_something{ init() }; // non-constexpr initialization

Trong một file duy nhất, các biến toàn cục thường được khởi tạo theo thứ tự định nghĩa (có một vài ngoại lệ đối với quy tắc này). Do đó, bạn cần phải cẩn thận để không có các biến phụ thuộc vào giá trị khởi tạo của các biến khác sẽ không được khởi tạo cho đến sau này. Ví dụ:

#include <iostream>
 
int initx();  // forward declaration
int inity();  // forward declaration
 
int g_x{ initx() }; // g_x is initialized first
int g_y{ inity() };
 
int initx()
{
    return g_y; // g_y isn't initialized when this is called
}
 
int inity()
{
  return 5;
}
 
int main()
{
    std::cout << g_x << ' ' << g_y << '\n';
}

Kết quả:

0 5

Nhiều vấn đề hơn nữa, thứ tự khởi tạo trên các file khác nhau không được xác định. Với hai file, a.cpp và b.cpp, hoặc có thể khởi tạo các biến toàn cục trước. Điều này có nghĩa là nếu các biến trong a.cpp phụ thuộc vào các giá trị trong b.cpp, thì có 50% khả năng các biến đó chưa được khởi chạy.

Lưu ý:

Khởi tạo động của các biến toàn cục gây ra rất nhiều vấn đề trong C ++. Tránh nó bất cứ khi nào có thể.

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!