Làm cách nào để sử dụng extern để chia sẻ các biến giữa các tệp nguồn?


987

Tôi biết rằng các biến toàn cục trong C đôi khi có externtừ khóa. Một externbiến là gì? Tuyên bố như thế nào? Phạm vi của nó là gì?

Điều này có liên quan đến việc chia sẻ các biến trên các tệp nguồn, nhưng làm thế nào để nó hoạt động chính xác? Tôi sử dụng ở externđâu?

Câu trả lời:


1751

Việc sử dụng externchỉ liên quan khi chương trình bạn đang xây dựng bao gồm nhiều tệp nguồn được liên kết với nhau, trong đó một số biến được xác định, ví dụ, trong tệp nguồn file1.ccần được tham chiếu trong các tệp nguồn khác, chẳng hạn như file2.c.

Điều quan trọng là phải hiểu sự khác biệt giữa việc xác định một biến và khai báo một biến :

  • Một biến được khai báo khi trình biên dịch được thông báo rằng một biến tồn tại (và đây là kiểu của nó); nó không phân bổ lưu trữ cho biến tại thời điểm đó.

  • Một biến được định nghĩa khi trình biên dịch phân bổ lưu trữ cho biến.

Bạn có thể khai báo một biến nhiều lần (mặc dù một lần là đủ); bạn chỉ có thể xác định nó một lần trong một phạm vi nhất định. Một định nghĩa biến cũng là một khai báo, nhưng không phải tất cả các khai báo biến là định nghĩa.

Cách tốt nhất để khai báo và định nghĩa các biến toàn cục

Cách rõ ràng, đáng tin cậy để khai báo và xác định các biến toàn cục là sử dụng tệp tiêu đề để chứa extern khai báo biến.

Tiêu đề được bao gồm bởi một tệp nguồn xác định biến và bởi tất cả các tệp nguồn tham chiếu biến đó. Đối với mỗi chương trình, một tệp nguồn (và chỉ một tệp nguồn) xác định biến. Tương tự, một tệp tiêu đề (và chỉ một tệp tiêu đề) sẽ khai báo biến. Các tập tin tiêu đề là rất quan trọng; nó cho phép kiểm tra chéo giữa các TU độc lập (đơn vị dịch thuật - nghĩ các tệp nguồn) và đảm bảo tính nhất quán.

Mặc dù có nhiều cách khác để làm điều đó, phương pháp này đơn giản và đáng tin cậy. Nó được thể hiện bởi file3.h, file1.cfile2.c:

tập tin3.h

extern int global_variable;  /* Declaration of the variable */

file1.c

#include "file3.h"  /* Declaration made available here */
#include "prog1.h"  /* Function declarations */

/* Variable defined here */
int global_variable = 37;    /* Definition checked against declaration */

int increment(void) { return global_variable++; }

file2.c

#include "file3.h"
#include "prog1.h"
#include <stdio.h>

void use_it(void)
{
    printf("Global variable: %d\n", global_variable++);
}

Đó là cách tốt nhất để khai báo và định nghĩa các biến toàn cục.


Hai tệp tiếp theo hoàn thành nguồn cho prog1:

Các chương trình hoàn chỉnh hiển thị các hàm sử dụng, do đó, khai báo hàm đã xuất hiện. Cả C99 và C11 đều yêu cầu các hàm phải được khai báo hoặc định nghĩa trước khi chúng được sử dụng (trong khi C90 thì không, vì lý do chính đáng). Tôi sử dụng từ khóa externtrước các khai báo hàm trong các tiêu đề để thống nhất - để khớp với các externkhai báo trước trong các tiêu đề. Nhiều người không thích sử dụng externtrước các khai báo hàm; trình biên dịch không quan tâm - và cuối cùng, tôi cũng không miễn là bạn nhất quán, ít nhất là trong một tệp nguồn.

prog1.h

extern void use_it(void);
extern int increment(void);

prog1.c

#include "file3.h"
#include "prog1.h"
#include <stdio.h>

int main(void)
{
    use_it();
    global_variable += 19;
    use_it();
    printf("Increment: %d\n", increment());
    return 0;
}
  • prog1sử dụng prog1.c, file1.c, file2.c, file3.hprog1.h.

Các tập tin prog1.mklà một makefile prog1chỉ. Nó sẽ hoạt động với hầu hết các phiên bản makeđược sản xuất kể từ khoảng đầu thiên niên kỷ. Nó không được liên kết cụ thể với GNU Make.

prog1.mk

# Minimal makefile for prog1

PROGRAM = prog1
FILES.c = prog1.c file1.c file2.c
FILES.h = prog1.h file3.h
FILES.o = ${FILES.c:.c=.o}

CC      = gcc
SFLAGS  = -std=c11
GFLAGS  = -g
OFLAGS  = -O3
WFLAG1  = -Wall
WFLAG2  = -Wextra
WFLAG3  = -Werror
WFLAG4  = -Wstrict-prototypes
WFLAG5  = -Wmissing-prototypes
WFLAGS  = ${WFLAG1} ${WFLAG2} ${WFLAG3} ${WFLAG4} ${WFLAG5}
UFLAGS  = # Set on command line only

CFLAGS  = ${SFLAGS} ${GFLAGS} ${OFLAGS} ${WFLAGS} ${UFLAGS}
LDFLAGS =
LDLIBS  =

all:    ${PROGRAM}

${PROGRAM}: ${FILES.o}
    ${CC} -o $@ ${CFLAGS} ${FILES.o} ${LDFLAGS} ${LDLIBS}

prog1.o: ${FILES.h}
file1.o: ${FILES.h}
file2.o: ${FILES.h}

# If it exists, prog1.dSYM is a directory on macOS DEBRIS = a.out core *~ *.dSYM RM_FR = rm -fr 

clean:
    ${RM_FR} ${FILES.o} ${PROGRAM} ${DEBRIS}


Hướng dẫn

Các quy tắc chỉ bị phá vỡ bởi các chuyên gia và chỉ với lý do chính đáng:

  • Tệp tiêu đề chỉ chứa externkhai báo các biến - staticđịnh nghĩa biến không bao giờ hoặc không đủ tiêu chuẩn.

  • Đối với bất kỳ biến nào, chỉ một tệp tiêu đề khai báo nó (SPOT - Điểm duy nhất của sự thật).

  • Tệp nguồn không bao giờ chứa externkhai báo biến - tệp nguồn luôn bao gồm tiêu đề (duy nhất) khai báo chúng.

  • Đối với bất kỳ biến đã cho, chính xác một tệp nguồn xác định biến đó, tốt nhất là khởi tạo nó. (Mặc dù không cần phải khởi tạo rõ ràng bằng không, nhưng nó không có hại và có thể làm một số điều tốt, bởi vì chỉ có thể có một định nghĩa khởi tạo của một biến toàn cục cụ thể trong một chương trình).

  • Tệp nguồn xác định biến cũng bao gồm tiêu đề để đảm bảo rằng định nghĩa và khai báo là nhất quán.

  • Một hàm không bao giờ cần phải khai báo một biến bằng cách sử dụng extern.

  • Tránh các biến toàn cục bất cứ khi nào có thể - thay vào đó hãy sử dụng các hàm.

Mã nguồn và văn bản của câu trả lời này có sẵn trong kho SOQ (Câu hỏi về chồng chéo) của tôi trên GitHub trong thư mục con src / so-0143-3204 .

Nếu bạn không phải là một lập trình viên C có kinh nghiệm, bạn có thể (và có lẽ nên) dừng đọc ở đây.

Cách không tốt để xác định các biến toàn cục

Với một số trình biên dịch C (thực sự là nhiều), bạn cũng có thể thoát khỏi định nghĩa 'chung' của một biến. 'Chung', ở đây, đề cập đến một kỹ thuật được sử dụng trong Fortran để chia sẻ các biến giữa các tệp nguồn, sử dụng khối CommON (có thể được đặt tên). Điều xảy ra ở đây là mỗi một số tệp cung cấp một định nghĩa dự kiến ​​của biến. Miễn là không có nhiều hơn một tệp cung cấp một định nghĩa được khởi tạo, thì các tệp khác nhau sẽ chia sẻ một định nghĩa chung về biến:

tập tin 10.c

#include "prog2.h"

long l; /* Do not do this in portable code */ 

void inc(void) { l++; }

tập tin11.c

#include "prog2.h"

long l; /* Do not do this in portable code */ 

void dec(void) { l--; }

tập tin 12.c

#include "prog2.h"
#include <stdio.h>

long l = 9; /* Do not do this in portable code */ 

void put(void) { printf("l = %ld\n", l); }

Kỹ thuật này không phù hợp với chữ cái của tiêu chuẩn C và 'quy tắc một định nghĩa' - đó là hành vi không được xác định chính thức:

J.2 Hành vi không xác định

Một định danh có liên kết ngoài được sử dụng, nhưng trong chương trình không tồn tại chính xác một định nghĩa bên ngoài cho định danh hoặc định danh không được sử dụng và tồn tại nhiều định nghĩa bên ngoài cho định danh (6.9).

§6.9 Định nghĩa bên ngoài ¶5

Một định nghĩa bên ngoài là một khai báo bên ngoài cũng là một định nghĩa của hàm (ngoài định nghĩa nội tuyến) hoặc một đối tượng. Nếu một mã định danh được khai báo với liên kết ngoài được sử dụng trong một biểu thức (không phải là một phần của toán hạng của toán tử sizeofhoặc _Alignoftoán tử có kết quả là hằng số nguyên), thì ở đâu đó trong toàn bộ chương trình sẽ có chính xác một định nghĩa bên ngoài cho mã định danh; nếu không, sẽ không có nhiều hơn một. 161)

161) Do đó, nếu một định danh được khai báo với liên kết ngoài không được sử dụng trong một biểu thức, thì không cần có định nghĩa bên ngoài cho nó.

Tuy nhiên, tiêu chuẩn C cũng liệt kê nó trong Phụ lục J thông tin là một trong những phần mở rộng phổ biến .

J.5.11 Nhiều định nghĩa bên ngoài

Có thể có nhiều hơn một định nghĩa bên ngoài cho định danh của một đối tượng, có hoặc không có việc sử dụng rõ ràng từ khóa bên ngoài; nếu các định nghĩa không đồng ý hoặc nhiều hơn một định nghĩa được khởi tạo, hành vi không được xác định (6.9.2).

Bởi vì kỹ thuật này không phải lúc nào cũng được hỗ trợ, tốt nhất nên tránh sử dụng nó, đặc biệt nếu mã của bạn cần phải di động . Sử dụng kỹ thuật này, bạn cũng có thể kết thúc với kiểu nhìn trộm không chủ ý.

Nếu một trong các tập tin khai báo bên trên lnhư một doublethay vì như một long, linkers kiểu không an toàn C có lẽ sẽ không nhận ra sự không phù hợp. Nếu bạn đang sử dụng máy có 64 bit longdoublethậm chí bạn sẽ không nhận được cảnh báo; trên máy có 32 bit longvà 64 bit double, bạn có thể nhận được cảnh báo về các kích thước khác nhau - trình liên kết sẽ sử dụng kích thước lớn nhất, chính xác như một chương trình Fortran sẽ có kích thước lớn nhất trong mọi khối chung.

Lưu ý rằng GCC 10.1.0, được phát hành vào ngày 2020-05-07, thay đổi các tùy chọn biên dịch mặc định sẽ sử dụng -fno-common, điều đó có nghĩa là theo mặc định, mã ở trên không còn liên kết trừ khi bạn ghi đè mặc định bằng -fcommon(hoặc sử dụng thuộc tính, v.v. - xem liên kết).


Hai tệp tiếp theo hoàn thành nguồn cho prog2:

prog2.h

extern void dec(void);
extern void put(void);
extern void inc(void);

prog2.c

#include "prog2.h"
#include <stdio.h>

int main(void)
{
    inc();
    put();
    dec();
    put();
    dec();
    put();
}
  • prog2sử dụng prog2.c, file10.c, file11.c, file12.c, prog2.h.

Cảnh báo

Như đã lưu ý trong các nhận xét ở đây và như đã nêu trong câu trả lời của tôi cho một câu hỏi tương tự , sử dụng nhiều định nghĩa cho một biến toàn cục dẫn đến hành vi không xác định (J.2; §6.9), đó là cách nói tiêu chuẩn "bất cứ điều gì cũng có thể xảy ra". Một trong những điều có thể xảy ra là chương trình ứng xử như bạn mong đợi; và J.5.11 nói, xấp xỉ, "bạn có thể may mắn thường xuyên hơn bạn xứng đáng". Nhưng một chương trình dựa trên nhiều định nghĩa của biến extern - có hoặc không có từ khóa 'extern' rõ ràng - không phải là một chương trình tuân thủ nghiêm ngặt và không được bảo đảm để hoạt động ở mọi nơi. Tương đương: nó chứa một lỗi có thể hoặc không thể hiển thị.

Vi phạm các hướng dẫn

Tất nhiên, có nhiều cách mà những hướng dẫn này có thể bị phá vỡ. Đôi khi, có thể có một lý do tốt để phá vỡ các hướng dẫn, nhưng những dịp như vậy là vô cùng bất thường.

bị lỗi_header.h

c int some_var; /* Do not do this in a header!!! */

Lưu ý 1: nếu tiêu đề xác định biến mà không có externtừ khóa, thì mỗi tệp bao gồm tiêu đề sẽ tạo ra một định nghĩa dự kiến ​​của biến. Như đã lưu ý trước đây, điều này thường sẽ hoạt động, nhưng tiêu chuẩn C không đảm bảo rằng nó sẽ hoạt động.

hỏng_header.h

c int some_var = 13; /* Only one source file in a program can use this */

Lưu ý 2: nếu tiêu đề xác định và khởi tạo biến, thì chỉ một tệp nguồn trong một chương trình đã cho có thể sử dụng tiêu đề. Vì các tiêu đề chủ yếu để chia sẻ thông tin, nên hơi ngớ ngẩn khi tạo một thông tin chỉ có thể được sử dụng một lần.

hiếm khi chính xác.h

c static int hidden_global = 3; /* Each source file gets its own copy */

Lưu ý 3: nếu tiêu đề xác định một biến tĩnh (có hoặc không khởi tạo), thì mỗi tệp nguồn kết thúc bằng phiên bản riêng của biến 'toàn cầu'.

Nếu biến thực sự là một mảng phức tạp, chẳng hạn, điều này có thể dẫn đến sự trùng lặp mã cực kỳ. Nó có thể, rất thỉnh thoảng, là một cách hợp lý để đạt được một số hiệu quả, nhưng điều đó rất bất thường.


Tóm lược

Sử dụng kỹ thuật tiêu đề tôi đã hiển thị đầu tiên. Nó hoạt động đáng tin cậy và ở khắp mọi nơi. Đặc biệt lưu ý rằng tiêu đề khai báo global_variableđược bao gồm trong mọi tệp sử dụng nó - bao gồm cả tệp định nghĩa nó. Điều này đảm bảo rằng mọi thứ đều tự ổn định.

Mối quan tâm tương tự phát sinh với việc khai báo và xác định hàm - áp dụng quy tắc tương tự. Nhưng câu hỏi là về các biến cụ thể, vì vậy tôi chỉ giữ câu trả lời cho các biến.

Kết thúc câu trả lời gốc

Nếu bạn không phải là một lập trình viên C có kinh nghiệm, có lẽ bạn nên dừng đọc ở đây.


Bổ sung chính muộn

Tránh sao chép mã

Một mối quan tâm đôi khi (và hợp pháp) được nêu ra về 'khai báo trong các tiêu đề, định nghĩa trong cơ chế nguồn' được mô tả ở đây là có hai tệp được giữ đồng bộ - tiêu đề và nguồn. Điều này thường được theo dõi với một quan sát rằng một macro có thể được sử dụng để tiêu đề phục vụ nhiệm vụ kép - thường khai báo các biến, nhưng khi một macro cụ thể được đặt trước khi bao gồm tiêu đề, nó sẽ xác định các biến thay thế.

Một mối quan tâm khác có thể là các biến cần được xác định trong mỗi một số 'chương trình chính'. Đây thường là một mối quan tâm giả; bạn có thể chỉ cần giới thiệu tệp nguồn C để xác định các biến và liên kết tệp đối tượng được tạo với mỗi chương trình.

Một lược đồ điển hình hoạt động như thế này, sử dụng biến toàn cục ban đầu được minh họa trong file3.h:

file3a.h

#ifdef DEFINE_VARIABLES
#define EXTERN /* nothing */
#else
#define EXTERN extern
#endif /* DEFINE_VARIABLES */

EXTERN int global_variable;

file1a.c

#define DEFINE_VARIABLES
#include "file3a.h"  /* Variable defined - but not initialized */
#include "prog3.h"

int increment(void) { return global_variable++; }

file2a.c

#include "file3a.h"
#include "prog3.h"
#include <stdio.h>

void use_it(void)
{
    printf("Global variable: %d\n", global_variable++);
}

Hai tệp tiếp theo hoàn thành nguồn cho prog3:

prog3.h

extern void use_it(void);
extern int increment(void);

prog3.c

#include "file3a.h"
#include "prog3.h"
#include <stdio.h>

int main(void)
{
    use_it();
    global_variable += 19;
    use_it();
    printf("Increment: %d\n", increment());
    return 0;
}
  • prog3sử dụng prog3.c, file1a.c, file2a.c, file3a.h, prog3.h.

Biến khởi tạo

Vấn đề với sơ đồ này như được hiển thị là nó không cung cấp cho việc khởi tạo biến toàn cục. Với C99 hoặc C11 và danh sách đối số biến cho các macro, bạn cũng có thể xác định một macro để hỗ trợ khởi tạo. (Với C89 và không hỗ trợ cho danh sách đối số biến trong macro, không có cách nào dễ dàng để xử lý các trình khởi tạo dài tùy ý.)

file3b.h

#ifdef DEFINE_VARIABLES
#define EXTERN                  /* nothing */
#define INITIALIZER(...)        = __VA_ARGS__
#else
#define EXTERN                  extern
#define INITIALIZER(...)        /* nothing */
#endif /* DEFINE_VARIABLES */

EXTERN int global_variable INITIALIZER(37);
EXTERN struct { int a; int b; } oddball_struct INITIALIZER({ 41, 43 });

Đảo ngược nội dung #if#elsekhối, sửa lỗi được xác định bởi Denis Kniazhev

file1b.c

#define DEFINE_VARIABLES
#include "file3b.h"  /* Variables now defined and initialized */
#include "prog4.h"

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file2b.c

#include "file3b.h"
#include "prog4.h"
#include <stdio.h>

void use_them(void)
{
    printf("Global variable: %d\n", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}

Rõ ràng, mã cho cấu trúc lẻ bóng không phải là những gì bạn thường viết, nhưng nó minh họa điểm. Đối số đầu tiên cho lần gọi thứ hai INITIALIZER{ 41và đối số còn lại (số ít trong ví dụ này) là 43 }. Không có C99 hoặc hỗ trợ tương tự cho danh sách đối số biến cho macro, trình khởi tạo cần chứa dấu phẩy rất có vấn đề.

Tiêu đề chính xác file3b.hbao gồm (thay vì fileba.h) cho mỗi Denis Kniazhev


Hai tệp tiếp theo hoàn thành nguồn cho prog4:

prog4.h

extern int increment(void);
extern int oddball_value(void);
extern void use_them(void);

prog4.c

#include "file3b.h"
#include "prog4.h"
#include <stdio.h>

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d\n", increment());
    printf("Oddball:   %d\n", oddball_value());
    return 0;
}
  • prog4sử dụng prog4.c, file1b.c, file2b.c, prog4.h, file3b.h.

Bộ đội trưởng

Bất kỳ tiêu đề nào cũng cần được bảo vệ chống lại sự tái hợp, để các định nghĩa kiểu (enum, struct hoặc union type hoặc typedefs nói chung) không gây ra vấn đề. Kỹ thuật tiêu chuẩn là bọc phần thân của tiêu đề trong bộ bảo vệ tiêu đề, chẳng hạn như:

#ifndef FILE3B_H_INCLUDED
#define FILE3B_H_INCLUDED

...contents of header...

#endif /* FILE3B_H_INCLUDED */

Tiêu đề có thể được bao gồm hai lần gián tiếp. Ví dụ: nếu file4b.hbao gồm file3b.hmột định nghĩa loại không được hiển thị và file1b.ccần sử dụng cả tiêu đề file4b.hfile3b.hthì bạn có một số vấn đề khó giải quyết hơn. Rõ ràng, bạn có thể sửa đổi danh sách tiêu đề để bao gồm chỉ file4b.h. Tuy nhiên, bạn có thể không nhận thức được các phụ thuộc bên trong - và mã, lý tưởng nhất là tiếp tục hoạt động.

Hơn nữa, nó bắt đầu trở nên khó khăn bởi vì bạn có thể bao gồm file4b.htrước khi đưa file3b.hvào các định nghĩa, nhưng các trình bảo vệ tiêu đề bình thường trên file3b.hsẽ ngăn tiêu đề được bao gồm lại.

Vì vậy, bạn cần bao gồm phần thân của file3b.hnhiều nhất một lần cho các khai báo và nhiều nhất một lần cho các định nghĩa, nhưng bạn có thể cần cả hai trong một đơn vị dịch thuật (TU - sự kết hợp của một tệp nguồn và các tiêu đề mà nó sử dụng).

Nhiều sự bao gồm với các định nghĩa biến

Tuy nhiên, nó có thể được thực hiện theo một ràng buộc không quá vô lý. Hãy giới thiệu một bộ tên tệp mới:

  • external.h cho các định nghĩa vĩ mô EXTERN, v.v.

  • file1c.hđể xác định loại (đáng chú ý là, struct oddballloại oddball_struct).

  • file2c.h để xác định hoặc khai báo các biến toàn cục.

  • file3c.c trong đó xác định các biến toàn cục.

  • file4c.c mà chỉ đơn giản là sử dụng các biến toàn cầu.

  • file5c.c cho thấy bạn có thể khai báo và sau đó xác định các biến toàn cục.

  • file6c.c cho thấy bạn có thể định nghĩa và sau đó (cố gắng) khai báo các biến toàn cục.

Trong các ví dụ này, file5c.cfile6c.ctrực tiếp bao gồm tiêu đề file2c.hnhiều lần, nhưng đó là cách đơn giản nhất để chỉ ra rằng cơ chế hoạt động. Điều đó có nghĩa là nếu tiêu đề được gián tiếp bao gồm hai lần, nó cũng sẽ an toàn.

Các hạn chế để làm việc này là:

  1. Tiêu đề xác định hoặc khai báo các biến toàn cục có thể không tự xác định bất kỳ loại nào.

  2. Ngay trước khi bạn bao gồm một tiêu đề sẽ xác định các biến, bạn xác định macro DEFINE_VARIABLES.

  3. Tiêu đề xác định hoặc khai báo các biến có nội dung cách điệu.

bên ngoài.h


#ifdef DEFINE_VARIABLES
#define EXTERN              /* nothing */
#define INITIALIZE(...)     = __VA_ARGS__
#else
#define EXTERN              extern
#define INITIALIZE(...)     /* nothing */
#endif /* DEFINE_VARIABLES */

file1c.h

#ifndef FILE1C_H_INCLUDED
#define FILE1C_H_INCLUDED

struct oddball
{
    int a;
    int b;
};

extern void use_them(void);
extern int increment(void);
extern int oddball_value(void);

#endif /* FILE1C_H_INCLUDED */

file2c.h


/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE2C_H_DEFINITIONS)
#undef FILE2C_H_INCLUDED
#endif

#ifndef FILE2C_H_INCLUDED
#define FILE2C_H_INCLUDED

#include "external.h"   /* Support macros EXTERN, INITIALIZE */
#include "file1c.h"     /* Type definition for struct oddball */

#if !defined(DEFINE_VARIABLES) || !defined(FILE2C_H_DEFINITIONS)

/* Global variable declarations / definitions */
EXTERN int global_variable INITIALIZE(37);
EXTERN struct oddball oddball_struct INITIALIZE({ 41, 43 });

#endif /* !DEFINE_VARIABLES || !FILE2C_H_DEFINITIONS */

/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE2C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */

#endif /* FILE2C_H_INCLUDED */

file3c.c

#define DEFINE_VARIABLES
#include "file2c.h"  /* Variables now defined and initialized */

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file4c.c

#include "file2c.h"
#include <stdio.h>

void use_them(void)
{
    printf("Global variable: %d\n", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}

file5c.c


#include "file2c.h"     /* Declare variables */

#define DEFINE_VARIABLES
#include "file2c.h"  /* Variables now defined and initialized */

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file6c.c


#define DEFINE_VARIABLES
#include "file2c.h"     /* Variables now defined and initialized */

#include "file2c.h"     /* Declare variables */

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }


Các tập tin nguồn tiếp theo hoàn thành nguồn (cung cấp một chương trình chính) cho prog5, prog6prog7:

prog5.c

#include "file2c.h"
#include <stdio.h>

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d\n", increment());
    printf("Oddball:   %d\n", oddball_value());
    return 0;
}
  • prog5sử dụng prog5.c, file3c.c, file4c.c, file1c.h, file2c.h, external.h.

  • prog6sử dụng prog5.c, file5c.c, file4c.c, file1c.h, file2c.h, external.h.

  • prog7sử dụng prog5.c, file6c.c, file4c.c, file1c.h, file2c.h, external.h.


Đề án này tránh được hầu hết các vấn đề. Bạn chỉ gặp vấn đề nếu một tiêu đề xác định các biến (chẳng hạn file2c.h) được bao gồm bởi một tiêu đề khác (giả sử file7c.h) xác định các biến. Không có cách nào dễ dàng ngoài việc "không làm".

Bạn có thể giải quyết một phần vấn đề bằng cách sửa đổi file2c.hthành file2d.h:

file2d.h

/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE2D_H_DEFINITIONS)
#undef FILE2D_H_INCLUDED
#endif

#ifndef FILE2D_H_INCLUDED
#define FILE2D_H_INCLUDED

#include "external.h"   /* Support macros EXTERN, INITIALIZE */
#include "file1c.h"     /* Type definition for struct oddball */

#if !defined(DEFINE_VARIABLES) || !defined(FILE2D_H_DEFINITIONS)

/* Global variable declarations / definitions */
EXTERN int global_variable INITIALIZE(37);
EXTERN struct oddball oddball_struct INITIALIZE({ 41, 43 });

#endif /* !DEFINE_VARIABLES || !FILE2D_H_DEFINITIONS */

/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE2D_H_DEFINITIONS
#undef DEFINE_VARIABLES
#endif /* DEFINE_VARIABLES */

#endif /* FILE2D_H_INCLUDED */

Vấn đề trở thành 'tiêu đề nên bao gồm #undef DEFINE_VARIABLES?' Nếu bạn bỏ qua phần đó từ tiêu đề và gói bất kỳ lời gọi xác định nào với #define#undef:

#define DEFINE_VARIABLES
#include "file2c.h"
#undef DEFINE_VARIABLES

trong mã nguồn (để các tiêu đề không bao giờ thay đổi giá trị của DEFINE_VARIABLES), thì bạn nên sạch sẽ. Nó chỉ là một phiền toái phải nhớ để viết các dòng thêm. Một sự thay thế có thể là:

#define HEADER_DEFINING_VARIABLES "file2c.h"
#include "externdef.h"

externdef.h


#if defined(HEADER_DEFINING_VARIABLES)
#define DEFINE_VARIABLES
#include HEADER_DEFINING_VARIABLES
#undef DEFINE_VARIABLES
#undef HEADER_DEFINING_VARIABLES
#endif /* HEADER_DEFINING_VARIABLES */

Điều này đang nhận được một chút hỗn độn, nhưng dường như là an toàn (sử dụng file2d.h, không có #undef DEFINE_VARIABLEStrong file2d.h).

file7c.c

/* Declare variables */
#include "file2d.h"

/* Define variables */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"

/* Declare variables - again */
#include "file2d.h"

/* Define variables - again */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

tập tin8c.h

/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE8C_H_DEFINITIONS)
#undef FILE8C_H_INCLUDED
#endif

#ifndef FILE8C_H_INCLUDED
#define FILE8C_H_INCLUDED

#include "external.h"   /* Support macros EXTERN, INITIALIZE */
#include "file2d.h"     /* struct oddball */

#if !defined(DEFINE_VARIABLES) || !defined(FILE8C_H_DEFINITIONS)

/* Global variable declarations / definitions */
EXTERN struct oddball another INITIALIZE({ 14, 34 });

#endif /* !DEFINE_VARIABLES || !FILE8C_H_DEFINITIONS */

/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE8C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */

#endif /* FILE8C_H_INCLUDED */

file8c.c

/* Define variables */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"

/* Define variables */
#define HEADER_DEFINING_VARIABLES "file8c.h"
#include "externdef.h"

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }


Hai tệp tiếp theo hoàn thành nguồn cho prog8prog9:

prog8.c

#include "file2d.h"
#include <stdio.h>

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d\n", increment());
    printf("Oddball:   %d\n", oddball_value());
    return 0;
}

file9c.c

#include "file2d.h"
#include <stdio.h>

void use_them(void)
{
    printf("Global variable: %d\n", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}
  • prog8sử dụng prog8.c, file7c.c, file9c.c.

  • prog9sử dụng prog8.c, file8c.c, file9c.c.


Tuy nhiên, các vấn đề tương đối khó xảy ra trong thực tế, đặc biệt nếu bạn thực hiện lời khuyên tiêu chuẩn để

Tránh các biến toàn cục


Liệu giải trình này bỏ lỡ bất cứ điều gì?

Thú nhận : Lược đồ 'tránh mã trùng lặp' được nêu ở đây đã được phát triển vì vấn đề này ảnh hưởng đến một số mã tôi làm việc (nhưng không sở hữu) và là mối quan tâm rắc rối với lược đồ được nêu trong phần đầu của câu trả lời. Tuy nhiên, lược đồ ban đầu khiến bạn chỉ có hai nơi để sửa đổi để giữ cho các định nghĩa và khai báo biến được đồng bộ hóa, đây là một bước tiến lớn so với việc khai báo biến bên ngoài nằm rải rác trong cơ sở mã (thực sự quan trọng khi có tổng số hàng ngàn tệp) . Tuy nhiên, mã trong các tệp có tên fileNc.[ch](cộng external.hexterndef.h) cho thấy rằng nó có thể được thực hiện để hoạt động. Rõ ràng, sẽ không khó để tạo một tập lệnh trình tạo tiêu đề để cung cấp cho bạn mẫu được chuẩn hóa cho một biến định nghĩa và khai báo tệp tiêu đề.

NB Đây là những chương trình đồ chơi chỉ có mã vừa đủ để làm cho chúng thú vị hơn một chút. Có sự lặp lại trong các ví dụ có thể được loại bỏ, nhưng không phải để đơn giản hóa lời giải thích sư phạm. (Ví dụ: sự khác biệt giữa prog5.cprog8.clà tên của một trong các tiêu đề được bao gồm. Có thể sắp xếp lại mã để main()chức năng không bị lặp lại, nhưng nó sẽ che giấu nhiều hơn so với tiết lộ.)


3
@litb: xem Phụ lục J.5.11 để biết định nghĩa chung - đó là phần mở rộng chung.
Jonathan Leffler

3
@litb: và tôi đồng ý rằng nên tránh - đó là lý do tại sao nó nằm trong phần 'Cách không tốt để xác định các biến toàn cục'.
Jonathan Leffler

3
Quả thực đó là một phần mở rộng phổ biến, nhưng đó là hành vi không xác định đối với một chương trình dựa vào nó. Tôi chỉ không rõ liệu bạn có nói rằng điều này được cho phép theo quy tắc riêng của C. Bây giờ tôi thấy bạn đang nói nó chỉ là một phần mở rộng phổ biến và để tránh nó nếu bạn cần mã của mình để có thể mang theo. Vì vậy, tôi có thể nâng cao bạn mà không nghi ngờ. Câu trả lời thực sự tuyệt vời IMHO :)
Johannes Schaub - litb

19
Nếu bạn dừng lại ở đầu, nó giữ những điều đơn giản đơn giản. Khi bạn đọc thêm, nó liên quan đến nhiều sắc thái, biến chứng và chi tiết hơn. Tôi vừa mới thêm hai "điểm dừng sớm" cho các lập trình viên C ít kinh nghiệm - hoặc lập trình viên C đã biết về chủ đề này. Không cần phải đọc tất cả nếu bạn đã biết câu trả lời (nhưng hãy cho tôi biết nếu bạn tìm thấy lỗi kỹ thuật).
Jonathan Leffler

4
@supercat: Tôi nhận thấy rằng bạn có thể sử dụng các ký tự mảng C99 để lấy giá trị liệt kê cho kích thước mảng, được minh họa bởi ( foo.h): #define FOO_INITIALIZER { 1, 2, 3, 4, 5 }để xác định trình khởi tạo cho mảng, enum { FOO_SIZE = sizeof((int [])FOO_INITIALIZER) / sizeof(((int [])FOO_INITIALIZER)[0]) };để lấy kích thước của mảng và extern int foo[];khai báo mảng . Rõ ràng, định nghĩa chỉ nên int foo[FOO_SIZE] = FOO_INITIALIZER;, mặc dù kích thước không thực sự phải được đưa vào định nghĩa. Điều này giúp bạn có một hằng số nguyên , FOO_SIZE.
Jonathan Leffler

125

Một externbiến là một khai báo (nhờ sbi cho việc hiệu chỉnh) của một biến được định nghĩa trong một đơn vị dịch thuật khác. Điều đó có nghĩa là việc lưu trữ cho biến được phân bổ trong một tệp khác.

Nói rằng bạn có hai .c-files test1.ctest2.c. Nếu bạn định nghĩa một biến toàn cầu int test1_var;trong test1.cvà bạn muốn truy cập vào biến này trong test2.cbạn phải sử dụng extern int test1_var;trong test2.c.

Mẫu hoàn chỉnh:

$ cat test1.c 
int test1_var = 5;
$ cat test2.c
#include <stdio.h>

extern int test1_var;

int main(void) {
    printf("test1_var = %d\n", test1_var);
    return 0;
}
$ gcc test1.c test2.c -o test
$ ./test
test1_var = 5

21
Không có "định nghĩa giả". Đó là một tuyên bố.
sbi

3
Trong ví dụ trên, nếu tôi thay đổi extern int test1_var;thành int test1_var;, trình liên kết (gcc 5.4.0) vẫn vượt qua. Vì vậy, có externthực sự cần thiết trong trường hợp này?
Radiohead

2
@radiohead: Trong câu trả lời của tôi , bạn sẽ tìm thấy thông tin bỏ externphần mở rộng phổ biến thường hoạt động - và đặc biệt hoạt động với GCC (nhưng GCC không phải là trình biên dịch duy nhất hỗ trợ nó; nó phổ biến trên các hệ thống Unix). Bạn có thể tìm kiếm "J.5.11" hay phần "cách Không phải như vậy tốt" trong câu trả lời của tôi (Tôi biết - đó dài) và các văn bản gần đó giải thích nó (hoặc cố gắng làm như vậy).
Jonathan Leffler

Một tuyên bố bên ngoài chắc chắn không phải được xác định trong một đơn vị dịch thuật khác (và thông thường là không). Trong thực tế, khai báo và định nghĩa có thể là một và giống nhau.
Hãy nhớ đến Monica

40

Extern là từ khóa bạn sử dụng để khai báo rằng chính biến đó nằm trong một đơn vị dịch thuật khác.

Vì vậy, bạn có thể quyết định sử dụng một biến trong một đơn vị dịch thuật và sau đó truy cập nó từ một đơn vị dịch thuật khác, sau đó trong lần thứ hai bạn khai báo nó là extern và biểu tượng sẽ được giải quyết bởi trình liên kết.

Nếu bạn không khai báo nó là extern, bạn sẽ nhận được 2 biến có cùng tên nhưng hoàn toàn không liên quan và lỗi nhiều định nghĩa của biến.


5
Nói cách khác, đơn vị dịch thuật sử dụng extern biết về biến này, loại của nó, v.v. và do đó cho phép mã nguồn trong logic cơ bản sử dụng nó, nhưng nó không phân bổ biến, một đơn vị dịch thuật khác sẽ làm điều đó. Nếu cả hai đơn vị dịch đều khai báo biến một cách bình thường, thì sẽ có hai vị trí vật lý cho biến đó, với các tham chiếu "sai" liên quan trong mã được biên dịch và dẫn đến sự mơ hồ cho trình liên kết.
mjv

26

Tôi thích nghĩ về một biến extern như một lời hứa mà bạn thực hiện với trình biên dịch.

Khi gặp một extern, trình biên dịch chỉ có thể tìm ra kiểu của nó, không phải nơi nó "sống", vì vậy nó không thể giải quyết tham chiếu.

Bạn đang nói với nó, "Hãy tin tôi. Tại thời điểm liên kết, tài liệu tham khảo này sẽ được giải quyết."


Tổng quát hơn, một tuyên bố là một lời hứa rằng tên sẽ có thể phân giải thành một định nghĩa chính xác tại thời điểm liên kết. Một extern khai báo một biến mà không xác định.
Lie Ryan

18

extern nói với trình biên dịch tin tưởng bạn rằng bộ nhớ cho biến này được khai báo ở nơi khác, vì vậy nó không cố phân bổ / kiểm tra bộ nhớ.

Do đó, bạn có thể biên dịch một tệp có tham chiếu đến extern, nhưng bạn không thể liên kết nếu bộ nhớ đó không được khai báo ở đâu đó.

Hữu ích cho các biến và thư viện toàn cầu, nhưng nguy hiểm vì trình liên kết không gõ kiểm tra.


Bộ nhớ không được khai báo. Xem câu trả lời cho câu hỏi này: stackoverflow.com/questions/1410563 để biết thêm chi tiết.
sbi

15

Thêm externmột biến định nghĩa biến thành một khai báo biến . Xem chủ đề này là những gì sự khác biệt giữa một tuyên bố và một định nghĩa.


Sự khác biệt giữa int fooextern int foo(phạm vi tập tin)? Cả hai đều là tuyên bố, phải không?

@ user14284: Cả hai đều chỉ khai báo theo nghĩa là mọi định nghĩa cũng là một khai báo. Nhưng tôi liên kết với một lời giải thích về điều này. ("Xem chủ đề này để biết sự khác biệt giữa khai báo và định nghĩa.") Tại sao bạn không đơn giản theo liên kết và đọc?
sbi

14
                 declare | define   | initialize |
                ----------------------------------

extern int a;    yes          no           no
-------------
int a = 2019;    yes          yes          yes
-------------
int a;           yes          yes          no
-------------

Khai báo sẽ không cấp phát bộ nhớ (biến phải được xác định để cấp phát bộ nhớ) nhưng định nghĩa sẽ. Đây chỉ là một cái nhìn đơn giản khác về từ khóa extern vì các câu trả lời khác thực sự tuyệt vời.


11

Giải thích chính xác của extern là bạn nói điều gì đó với trình biên dịch. Bạn nói với trình biên dịch rằng, mặc dù không có mặt ngay bây giờ, biến được khai báo bằng cách nào đó sẽ được tìm thấy bởi trình liên kết (thường là trong một đối tượng khác (tệp)). Trình liên kết sau đó sẽ là người may mắn tìm thấy mọi thứ và đặt nó lại với nhau, cho dù bạn có một số tuyên bố bên ngoài hay không.


8

Trong C, một biến trong tệp nói example.c được đưa ra phạm vi cục bộ. Trình biên dịch hy vọng rằng biến đó sẽ có định nghĩa của nó trong cùng một tập tin example.c và khi nó không tìm thấy giống nhau, nó sẽ đưa ra một lỗi. Mặt khác, theo phạm vi toàn cầu mặc định. Do đó, bạn không cần phải đề cập rõ ràng đến trình biên dịch "nhìn anh bạn ... bạn có thể tìm thấy định nghĩa của hàm này ở đây". Đối với một hàm bao gồm tệp chứa khai báo của nó là đủ. (Tệp mà bạn thực sự gọi là tệp tiêu đề). Ví dụ, hãy xem xét 2 tệp sau:
example.c

#include<stdio.h>
extern int a;
main(){
       printf("The value of a is <%d>\n",a);
}

ví dụ1.c

int a = 5;

Bây giờ khi bạn biên dịch hai tệp lại với nhau, sử dụng các lệnh sau:

bước 1) cc -o ex example.c example1.c bước 2) ./ ex

Bạn nhận được đầu ra sau: Giá trị của a là <5>


8

Triển khai GCC ELF Linux

Các câu trả lời khác đã đề cập đến khía cạnh sử dụng ngôn ngữ, vì vậy bây giờ chúng ta hãy xem cách nó được thực hiện trong triển khai này.

C chính

#include <stdio.h>

int not_extern_int = 1;
extern int extern_int;

void main() {
    printf("%d\n", not_extern_int);
    printf("%d\n", extern_int);
}

Biên dịch và dịch ngược:

gcc -c main.c
readelf -s main.o

Đầu ra chứa:

Num:    Value          Size Type    Bind   Vis      Ndx Name
 9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 not_extern_int
12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND extern_int

Chương "Bảng biểu tượng" ELF Cập nhật hệ thống V ABI giải thích:

SHN_UNDEF Chỉ mục bảng phần này có nghĩa là biểu tượng không được xác định. Khi trình soạn thảo liên kết kết hợp tệp đối tượng này với tệp khác xác định ký hiệu được chỉ định, các tham chiếu của tệp này đến ký hiệu sẽ được liên kết với định nghĩa thực tế.

về cơ bản là hành vi mà tiêu chuẩn C đưa ra cho externcác biến.

Từ giờ trở đi, công việc của trình liên kết là tạo chương trình cuối cùng, nhưng externthông tin đã được trích xuất từ ​​mã nguồn vào tệp đối tượng.

Đã thử nghiệm trên GCC 4.8.

C ++ 17 biến nội tuyến

Trong C ++ 17, bạn có thể muốn sử dụng các biến nội tuyến thay vì các biến ngoài, vì chúng đơn giản để sử dụng (có thể được xác định chỉ một lần trên tiêu đề) và mạnh hơn (hỗ trợ constexpr). Xem: 'const static' có nghĩa là gì trong C và C ++?


3
Đó không phải là phiếu bầu của tôi, vì vậy tôi không biết. Tuy nhiên, tôi sẽ đưa ra ý kiến. Mặc dù nhìn vào đầu ra của readelfhoặc nmcó thể hữu ích, bạn đã không giải thích các nguyên tắc cơ bản về cách sử dụng extern, cũng như không hoàn thành chương trình đầu tiên với định nghĩa thực tế. Mã của bạn thậm chí không sử dụng notExtern. Cũng có một vấn đề về danh pháp: mặc dù notExternđược định nghĩa ở đây thay vì được khai báo extern, đó là một biến ngoài có thể được truy cập bởi các tệp nguồn khác nếu các đơn vị dịch thuật đó chứa một khai báo phù hợp (cần có extern int notExtern;!).
Jonathan Leffler

1
@JonathanLeffler cảm ơn vì đã phản hồi! Các khuyến nghị về hành vi và sử dụng tiêu chuẩn đã được thực hiện trong các câu trả lời khác, vì vậy tôi quyết định chỉ ra cách thực hiện một chút vì điều đó thực sự giúp tôi nắm bắt được những gì đang diễn ra. Không sử dụng notExternđã xấu, sửa nó. Về danh pháp, hãy cho tôi biết nếu bạn có một cái tên tốt hơn. Tất nhiên đó không phải là một cái tên hay cho một chương trình thực tế, nhưng tôi nghĩ nó phù hợp với vai trò mô phạm ở đây.
Ciro Santilli 冠状 病毒 审查 事件

Về tên, còn global_defđối với biến được định nghĩa ở đây và extern_refđối với biến được định nghĩa trong một số mô-đun khác thì sao? Họ sẽ có sự đối xứng rõ ràng phù hợp? Bạn vẫn kết thúc với int extern_ref = 57;hoặc một cái gì đó tương tự trong tệp được xác định, vì vậy tên không hoàn toàn lý tưởng, nhưng trong ngữ cảnh của tệp nguồn đơn, đó là một lựa chọn hợp lý. Có extern int global_def;một tiêu đề không phải là vấn đề nhiều, nó dường như đối với tôi. Hoàn toàn phụ thuộc vào bạn, tất nhiên.
Jonathan Leffler

7

từ khóa extern được sử dụng với biến để xác định nó là biến toàn cục.

Nó cũng thể hiện rằng bạn có thể sử dụng biến được khai báo bằng từ khóa extern trong bất kỳ tệp nào mặc dù nó được khai báo / định nghĩa trong tệp khác.


5

extern cho phép một mô-đun của chương trình của bạn truy cập vào một biến hoặc hàm toàn cục được khai báo trong một mô-đun khác của chương trình. Bạn thường có các biến ngoài được khai báo trong tệp tiêu đề.

Nếu bạn không muốn một chương trình truy cập vào các biến hoặc hàm của mình, bạn sử dụng thông báo staticcho trình biên dịch rằng biến hoặc hàm này không thể được sử dụng bên ngoài mô-đun này.


5

extern chỉ đơn giản có nghĩa là một biến được định nghĩa ở nơi khác (ví dụ, trong một tệp khác).


4

Trước hết, externtừ khóa không được sử dụng để xác định một biến; thay vì nó được sử dụng để khai báo một biến. Tôi có thể nói externlà một lớp lưu trữ, không phải là một loại dữ liệu.

externđược sử dụng để cho các tệp C khác hoặc các thành phần bên ngoài biết biến này đã được xác định ở đâu đó. Ví dụ: nếu bạn đang xây dựng một thư viện, không cần xác định biến toàn cục bắt buộc ở đâu đó trong chính thư viện. Thư viện sẽ được biên dịch trực tiếp, nhưng trong khi liên kết tệp, nó sẽ kiểm tra định nghĩa.


3

externđược sử dụng để một first.ctệp có thể có toàn quyền truy cập vào một tham số toàn cục trong second.ctệp khác .

externthể được khai báo trong first.ctệp hoặc trong bất kỳ tệp tiêu đề nào first.c.


3
Lưu ý rằng externkhai báo phải ở trong một tiêu đề, không phải trong first.c, để nếu loại thay đổi, khai báo cũng sẽ thay đổi. Ngoài ra, tiêu đề khai báo biến phải được đưa vào second.cđể đảm bảo rằng định nghĩa phù hợp với khai báo. Tuyên bố trong tiêu đề là chất keo giữ tất cả lại với nhau; nó cho phép các tệp được biên dịch riêng nhưng đảm bảo chúng có một cái nhìn nhất quán về loại biến toàn cục.
Jonathan Leffler

2

Với xc8, bạn phải cẩn thận khi khai báo một biến là cùng loại trong mỗi tệp nếu bạn có thể, khai báo một cái gì đó inttrong một tệp và charnói trong một tệp khác. Điều này có thể dẫn đến tham nhũng của các biến.

Vấn đề này đã được giải quyết một cách tao nhã trong một diễn đàn vi mạch khoảng 15 năm trước / * Xem "http: www.htsoft.com" / / "forum / all / showflat.php / Cat / 0 / Number / 18766 / an / 0 / page / 0 # 18766 "

Nhưng liên kết này dường như không còn hoạt động ...

Vì vậy, tôi sẽ nhanh chóng cố gắng giải thích nó; tạo một tập tin gọi là global.h.

Trong đó khai báo như sau

#ifdef MAIN_C
#define GLOBAL
 /* #warning COMPILING MAIN.C */
#else
#define GLOBAL extern
#endif
GLOBAL unsigned char testing_mode; // example var used in several C files

Bây giờ trong tập tin main.c

#define MAIN_C 1
#include "global.h"
#undef MAIN_C

Điều này có nghĩa là trong main.c, biến sẽ được khai báo là an unsigned char.

Bây giờ trong các tệp khác chỉ đơn giản bao gồm global.h sẽ có nó được khai báo là extern cho tệp đó .

extern unsigned char testing_mode;

Nhưng nó sẽ được khai báo chính xác như một unsigned char.

Các bài viết diễn đàn cũ có lẽ giải thích điều này rõ ràng hơn một chút. Nhưng đây là một tiềm năng thực sự gotchakhi sử dụng trình biên dịch cho phép bạn khai báo một biến trong một tệp và sau đó khai báo nó là một loại khác trong một loại khác. Các vấn đề liên quan đến điều đó là nếu bạn nói đã khai báo tests_mode như một int trong một tệp khác, nó sẽ nghĩ rằng đó là một var 16 bit và ghi đè lên một phần khác của ram, có khả năng làm hỏng một biến khác. Khó gỡ lỗi!


0

Một giải pháp rất ngắn tôi sử dụng để cho phép một tệp tiêu đề chứa tham chiếu bên ngoài hoặc thực hiện thực tế của một đối tượng. Các tập tin thực sự có chứa các đối tượng chỉ cần làm #define GLOBAL_FOO_IMPLEMENTATION. Sau đó, khi tôi thêm một đối tượng mới vào tệp này, nó cũng xuất hiện trong tệp đó mà không cần tôi phải sao chép và dán định nghĩa.

Tôi sử dụng mô hình này trên nhiều tập tin. Vì vậy, để giữ mọi thứ khép kín nhất có thể, tôi chỉ sử dụng lại macro GLOBAL duy nhất trong mỗi tiêu đề. Tiêu đề của tôi trông như thế này:

//file foo_globals.h
#pragma once  
#include "foo.h"  //contains definition of foo

#ifdef GLOBAL  
#undef GLOBAL  
#endif  

#ifdef GLOBAL_FOO_IMPLEMENTATION  
#define GLOBAL  
#else  
#define GLOBAL extern  
#endif  

GLOBAL Foo foo1;  
GLOBAL Foo foo2;


//file main.cpp
#define GLOBAL_FOO_IMPLEMENTATION
#include "foo_globals.h"

//file uses_extern_foo.cpp
#include "foo_globals.h
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.