Câu trả lời:
Việc sử dụng extern
chỉ 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.c
cầ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 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.c
và file2.c
:
extern int global_variable; /* Declaration of the variable */
#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++; }
#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 extern
trước các khai báo hàm trong các tiêu đề để thống nhất - để khớp với các extern
khai báo trước trong các tiêu đề. Nhiều người không thích sử dụng extern
trướ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.
extern void use_it(void);
extern int increment(void);
#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;
}
prog1
sử dụng prog1.c
, file1.c
, file2.c
, file3.h
và prog1.h
.Các tập tin prog1.mk
là một makefile prog1
chỉ. 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.
# 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}
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 extern
khai 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 extern
khai 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.
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:
#include "prog2.h"
long l; /* Do not do this in portable code */
void inc(void) { l++; }
#include "prog2.h"
long l; /* Do not do this in portable code */
void dec(void) { l--; }
#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:
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).
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ử
sizeof
hoặc_Alignof
toá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 l
như một double
thay 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 long
và double
thậm chí bạn sẽ không nhận được cảnh báo; trên máy có 32 bit long
và 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
:
extern void dec(void);
extern void put(void);
extern void inc(void);
#include "prog2.h"
#include <stdio.h>
int main(void)
{
inc();
put();
dec();
put();
dec();
put();
}
prog2
sử dụng prog2.c
, file10.c
, file11.c
, file12.c
, prog2.h
.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ị.
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.
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ó extern
từ 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.
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.
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.
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.
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
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
:
#ifdef DEFINE_VARIABLES
#define EXTERN /* nothing */
#else
#define EXTERN extern
#endif /* DEFINE_VARIABLES */
EXTERN int global_variable;
#define DEFINE_VARIABLES
#include "file3a.h" /* Variable defined - but not initialized */
#include "prog3.h"
int increment(void) { return global_variable++; }
#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
:
extern void use_it(void);
extern int increment(void);
#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;
}
prog3
sử dụng prog3.c
, file1a.c
, file2a.c
, file3a.h
, prog3.h
.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 ý.)
#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
và #else
khối, sửa lỗi được xác định bởi
Denis Kniazhev
#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; }
#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
là { 41
và đố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.h
bao 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
:
extern int increment(void);
extern int oddball_value(void);
extern void use_them(void);
#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;
}
prog4
sử dụng prog4.c
, file1b.c
, file2b.c
, prog4.h
, file3b.h
.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.h
bao gồm file3b.h
một định nghĩa loại không được hiển thị và file1b.c
cần sử dụng cả tiêu đề file4b.h
và file3b.h
thì 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.h
trước khi đưa file3b.h
vào các định nghĩa, nhưng các trình bảo vệ tiêu đề bình thường trên file3b.h
sẽ 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.h
nhiề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).
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 oddball
loạ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.c
và file6c.c
trực tiếp bao gồm tiêu đề file2c.h
nhiề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à:
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.
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.
Tiêu đề xác định hoặc khai báo các biến có nội dung cách điệu.
#ifdef DEFINE_VARIABLES
#define EXTERN /* nothing */
#define INITIALIZE(...) = __VA_ARGS__
#else
#define EXTERN extern
#define INITIALIZE(...) /* nothing */
#endif /* DEFINE_VARIABLES */
#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 */
/* 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 */
#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; }
#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;
}
#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; }
#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
, prog6
và prog7
:
#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;
}
prog5
sử dụng prog5.c
, file3c.c
, file4c.c
, file1c.h
, file2c.h
, external.h
.
prog6
sử dụng prog5.c
, file5c.c
, file4c.c
, file1c.h
, file2c.h
, external.h
.
prog7
sử 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.h
thành 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
và #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"
#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_VARIABLES
trong file2d.h
).
/* 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; }
/* 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 */
/* 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 prog8
và prog9
:
#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;
}
#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;
}
prog8
sử dụng prog8.c
, file7c.c
, file9c.c
.
prog9
sử 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 để
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.h
và externdef.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.c
và prog8.c
là 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ộ.)
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
.
Một extern
biế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.c
và test2.c
. Nếu bạn định nghĩa một biến toàn cầu int test1_var;
trong test1.c
và bạn muốn truy cập vào biến này trong test2.c
bạ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
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ó extern
thực sự cần thiết trong trường hợp này?
extern
phầ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 - đó là 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).
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.
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."
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.
Thêm extern
mộ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.
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.
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.
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>
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 extern
cá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 extern
thô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 ++?
readelf
hoặc nm
có 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;
!).
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.
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.
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 static
cho 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.
Trước hết, extern
từ 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 extern
là 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.
extern
được sử dụng để một first.c
tệp có thể có toàn quyền truy cập vào một tham số toàn cục trong second.c
tệp khác .
Có extern
thể được khai báo trong first.c
tệp hoặc trong bất kỳ tệp tiêu đề nào first.c
.
extern
khai 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.
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ì đó int
trong một tệp và char
nó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ự gotcha
khi 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!
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