Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 20.328 phút đọc

Liên kết trong và liên kết ngoài

Sau bài này bạn sẽ làm được

  • Dùng static để giấu hàm và biến khỏi các tệp khác
  • Khai báo biến toàn cục dùng chung đúng cách bằng extern
  • Giải thích định nghĩa tạm thời và khác biệt giữa fcommon với fno-common
  • Đọc bảng ký hiệu bằng nm và nhận ra ký hiệu trong với ký hiệu ngoài

Trong C, mọi hàm và biến toàn cục mặc định đều nhìn thấy được từ mọi tệp khác trong chương trình. Đó là mặc định sai, và static là một từ khóa duy nhất sửa nó.

#Hai loại liên kết

Liên kết ngoài
Tên được xuất ra bảng ký hiệu, nên tệp khác tham chiếu tới được. Đây là mặc định cho hàm và biến khai báo ở phạm vi tệp.
Liên kết trong
Tên chỉ tồn tại trong đơn vị dịch của nó. Tệp khác không thấy, và hai tệp được phép có hai thứ trùng tên mà không xung đột. Đạt được bằng từ khóa static ở phạm vi tệp.
sinhvien.c
#include "sinhvien.h"

/* --- LIEN KET NGOAI: tep khac dung duoc --- */

int g_so_sv = 0;                              /* bien toan cuc */

MaLoi sv_kiem_mssv(const char *s) { ... }     /* ham cong khai */

/* --- LIEN KET TRONG: chi sinhvien.c thay --- */

static int  s_bo_dem = 0;                     /* bien cap tep */

static void chuan_hoa(char *s) { ... }        /* ham noi bo */
Khai báo ở phạm vi tệpLiên kếtTệp khác thấy không
int x;NgoàiCó
int x = 5;NgoàiCó
static int x;TrongKhông
void f(void) { }NgoàiCó
static void f(void) { }TrongKhông
static inline void f(void) { }TrongKhông
const int x = 5;Ngoài trong CCó, khác C++

#static ở phạm vi tệp

terminal
cat s1.c
static int s_dem = 0;
static void phu(void) { ++s_dem; }
int cong_khai(void) { phu(); return s_dem; }
cat s2.c
static int s_dem = 100;              /* CUNG TEN, khong xung dot */
int cong_khai(void);
int main(void) { return cong_khai() + s_dem - 101; }
gcc -std=c11 -fno-common -o s s1.c s2.c && ./s; echo "exit=$?"
exit=0
terminal
# Hàm static không ai gọi thì được cảnh báo
gcc -Wall -Wextra -c sinhvien.c
sinhvien.c:88:13: warning: 'ham_cu' defined but not used [-Wunused-function]
   88 | static void ham_cu(void) {
      |             ^~~~~~

Biến static cấp tệp thay cho biến toàn cục

Biến toàn cục công khai
/* quanly.h */
extern QuanLy g_quanly;         /* moi tep deu sua duoc */

/* quanly.c */
QuanLy g_quanly;

/* Bat cu tep nao cung viet duoc:
     g_quanly.n = 0;
   va ban khong bao gio biet ai da lam hong no. */
Giấu sau hàm truy cập
/* quanly.h: KHONG lo bien ra ngoai */
MaLoi  ql_them(const SinhVien *sv);
size_t ql_so_luong(void);

/* quanly.c */
static QuanLy s_quanly;         /* chi tep nay cham vao duoc */

MaLoi ql_them(const SinhVien *sv) {
    /* ... sua s_quanly ... */
}

size_t ql_so_luong(void) { return s_quanly.n; }

#extern cho biến dùng chung

Mẫu chuẩn, ba tệp
/* cau-hinh.h */
#ifndef CAU_HINH_H
#define CAU_HINH_H

extern int         g_muc_ghi;      /* KHAI BAO */
extern const char *g_ten_app;

#endif

/* cau-hinh.c */
#include "cau-hinh.h"

int         g_muc_ghi = 2;         /* DINH NGHIA, dung mot lan */
const char *g_ten_app = "Quan ly sinh vien";

/* main.c */
#include "cau-hinh.h"

int main(void) {
    g_muc_ghi = 4;
    printf("%s\n", g_ten_app);

    return 0;
}

extern cho mảng: cẩn thận kích thước

/* bang.c */
const int BANG[] = { 1, 2, 3, 4, 5 };

/* bang.h */
extern const int BANG[];          /* khong co kich thuoc */

/* nguoi dung */
#include "bang.h"

size_t n = sizeof BANG / sizeof BANG[0];    /* LOI:
     error: invalid application of 'sizeof' to incomplete type */

/* Cach chua 1: ghi ro kich thuoc trong header */
extern const int BANG[5];

/* Cach chua 2: xuat them so luong, an toan hon */
extern const int    BANG[];
extern const size_t BANG_SO_LUONG;

/* bang.c */
const size_t BANG_SO_LUONG = sizeof BANG / sizeof BANG[0];

#Định nghĩa tạm thời và fno-common

Định nghĩa tạm thời
Khai báo biến ở phạm vi tệp, không có extern và không có giá trị đầu, ví dụ int x;. Chuẩn C coi đây là một trường hợp riêng: nếu trong cùng tệp có định nghĩa thật thì nó là khai báo, còn không thì nó thành định nghĩa với giá trị không.
terminal
# Header có int g_dem; không extern, hai tệp .c include
gcc -std=c11 -fcommon -o za z1.c z2.c && ./za; echo "exit=$?"
exit=0
gcc -std=c11 -fno-common -o zb z1.c z2.c
z2.o:z2.c:(.bss+0x0): multiple definition of `g_dem'
z1.o:z1.c:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status
Trong headerVới -fcommonVới -fno-common
extern int x;ChạyChạy
int x;Chạy, gộp lại thành mộtLỗi multiple definition
int x = 0;LỗiLỗi

#Đọc bảng ký hiệu bằng nm

Trình liên kết ghép mỗi ký hiệu U của tệp này với một ký hiệu chữ hoa của tệp kia.
terminal
cat nmtest.c
static int  s_dem_tinh = 0;
static int  s_dem_khong;
int         g_co_dau = 5;
int         g_khong_dau;
const int   G_HANG = 7;
static void phu(void) { ++s_dem_tinh; }
int cong_khai(void) { ... }
int can_ngoai(const char *a, const char *b) { return strcmp(a, b); }
gcc -std=c11 -fno-common -c nmtest.c && nm nmtest.o
0000000000000049 T can_ngoai
0000000000000011 T cong_khai
0000000000000000 D g_co_dau
0000000000000000 R G_HANG
0000000000000000 B g_khong_dau
0000000000000000 t phu
0000000000000008 b s_dem_khong
0000000000000004 b s_dem_tinh
                 U strcmp
ChữNghĩaSinh ra từ
TMã lệnh, liên kết ngoàiHàm thường
tMã lệnh, liên kết trongHàm static
DDữ liệu có giá trị đầu, ngoàiint g = 5;
dDữ liệu có giá trị đầu, trongstatic int g = 5;
BDữ liệu bằng không, ngoàiint g;
bDữ liệu bằng không, trongstatic int g;
RChỉ đọc, ngoàiconst int g = 5;
UCHƯA CÓ, tệp này đang cầnGọi hàm ở tệp khác
CCommon, định nghĩa tạm thờiint g; với -fcommon
Quy tắc chung: chữ hoa là liên kết ngoài, chữ thường là liên kết trong.
terminal
# Cùng tệp đó, đổi sang -fcommon
gcc -std=c11 -fcommon -c nmtest.c && nm nmtest.o | grep khong_dau
0000000000000004 C g_khong_dau

Tự làm thử

  1. Viết hai tệp .c cùng có static int s_dem với hai giá trị khác nhau, liên kết lại và xác nhận không xung đột.
  2. Bỏ static đi ở cả hai và xem thông báo lỗi.
  3. Đặt int g; không extern trong header, include từ hai tệp, rồi thử cả -fcommon và -fno-common.
  4. Chạy nm trên một tệp .o của bạn và phân loại từng ký hiệu theo bảng ở trên.
  5. Đếm số ký hiệu module của bạn xuất ra và so với số hàm trong header, rồi thêm static cho phần chênh lệch.
  6. Chạy nm -u trên một tệp .o và giải thích vì sao mỗi ký hiệu U lại có mặt.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Mặc định của C là liên kết ngoài, tức là mọi hàm và biến toàn cục đều lộ ra cho cả chương trình.
  • Nguyên tắc đóng gói: mọi hàm và biến không khai báo trong .h đều phải static.
  • static ở phạm vi tệp nghĩa là liên kết trong, còn static trong hàm nghĩa là thời gian sống tĩnh. Hai chuyện khác nhau.
  • Biến toàn cục dùng chung: extern trong header, định nghĩa trong đúng một tệp .c.
  • int x; trong header chạy được với -fcommon nhưng lỗi với -fno-common, và mặc định đã đổi từ GCC 10.
  • nm cho biết module xuất ra gì và cần gì. Chữ hoa là liên kết ngoài, chữ thường là liên kết trong, U là đang thiếu.