ฉันจะใช้ extern เพื่อแชร์ตัวแปรระหว่างไฟล์ต้นฉบับได้อย่างไร


987

ฉันรู้ว่าตัวแปรทั่วโลกใน C บางครั้งมีexternคำหลัก คืออะไรexternตัวแปร? การประกาศเป็นอย่างไร ขอบเขตคืออะไร

สิ่งนี้เกี่ยวข้องกับการแชร์ตัวแปรข้ามไฟล์ต้นฉบับ แต่มันทำงานอย่างไรอย่างแม่นยำ? ฉันจะใช้externที่ไหน

คำตอบ:


1751

ใช้externเป็นเพียงความเกี่ยวข้องเมื่อโปรแกรมที่คุณกำลังอาคารประกอบด้วยไฟล์หลายแหล่งที่มาเชื่อมโยงกันที่บางส่วนของตัวแปรที่กำหนดไว้สำหรับตัวอย่างเช่นในแฟ้มแหล่งที่มาfile1.cจะต้องมีการอ้างอิงในแฟ้มแหล่งอื่น ๆ file2.cเช่น

มันเป็นสิ่งสำคัญที่จะเข้าใจความแตกต่างระหว่างการกำหนดตัวแปรและการประกาศตัวแปร :

  • ตัวแปรถูกประกาศเมื่อคอมไพเลอร์ได้รับแจ้งว่ามีตัวแปรอยู่ (และนี่คือประเภทของมัน); มันไม่ได้จัดสรรที่เก็บข้อมูลสำหรับตัวแปร ณ จุดนั้น

  • ตัวแปรถูกกำหนดเมื่อคอมไพเลอร์จัดสรรหน่วยเก็บสำหรับตัวแปร

คุณอาจประกาศตัวแปรหลาย ๆ ครั้ง (แต่ครั้งเดียวก็เพียงพอแล้ว); คุณสามารถกำหนดได้เพียงครั้งเดียวภายในขอบเขตที่กำหนด นิยามตัวแปรยังเป็นการประกาศ แต่ไม่ใช่การประกาศตัวแปรทั้งหมดเป็นคำจำกัดความ

วิธีที่ดีที่สุดในการประกาศและกำหนดตัวแปรส่วนกลาง

วิธีที่สะอาดและเชื่อถือได้ในการประกาศและกำหนดตัวแปรส่วนกลางคือการใช้ไฟล์ส่วนหัวเพื่อให้มีการextern ประกาศตัวแปร

ส่วนหัวถูกรวมโดยไฟล์ต้นฉบับหนึ่งไฟล์ที่กำหนดตัวแปรและโดยไฟล์ต้นฉบับทั้งหมดที่อ้างอิงตัวแปร สำหรับแต่ละโปรแกรมไฟล์ต้นฉบับหนึ่งไฟล์ (และไฟล์เดียวเท่านั้น) จะกำหนดตัวแปร ในทำนองเดียวกันไฟล์ส่วนหัวหนึ่งไฟล์ (และไฟล์ส่วนหัวเดียวเท่านั้น) ควรประกาศตัวแปร ไฟล์ส่วนหัวมีความสำคัญ มันช่วยให้การตรวจสอบข้ามระหว่าง TU อิสระ (หน่วยการแปล - คิดว่าไฟล์ต้นฉบับ) และสร้างความมั่นคง

แม้ว่าจะมีวิธีอื่นในการทำวิธีนี้ง่ายและน่าเชื่อถือ มันคือการแสดงfile3.h, file1.cและfile2.c:

file3.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++);
}

นั่นเป็นวิธีที่ดีที่สุดในการประกาศและกำหนดตัวแปรทั่วโลก


ไฟล์สองไฟล์ถัดไปทำให้สมบูรณ์สำหรับprog1:

โปรแกรมที่สมบูรณ์แสดงการใช้งานฟังก์ชั่นดังนั้นการประกาศฟังก์ชั่นได้พุ่งเข้ามาทั้ง C99 และ C11 ต้องการฟังก์ชั่นที่จะประกาศหรือกำหนดก่อนที่จะใช้ (ในขณะที่ C90 ไม่ได้สำหรับเหตุผลที่ดี) ฉันใช้คำสำคัญที่externด้านหน้าของการประกาศฟังก์ชั่นในส่วนหัวเพื่อความสอดคล้อง - เพื่อให้ตรงกับexternด้านหน้าของการประกาศตัวแปรในส่วนหัว หลายคนไม่ต้องการใช้externหน้าการประกาศฟังก์ชัน คอมไพเลอร์ไม่สนใจ - และในที่สุดฉันก็ไม่ได้ตราบใดที่คุณมีความสอดคล้องอย่างน้อยก็ในไฟล์ต้นฉบับ

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;
}
  • prog1การใช้งานprog1.c, file1.c, file2.c, และfile3.hprog1.h

ไฟล์prog1.mkนี้เป็น makefile สำหรับprog1เท่านั้น มันจะทำงานร่วมกับรุ่นที่makeผลิตส่วนใหญ่ตั้งแต่ประมาณเปลี่ยนสหัสวรรษ ไม่ได้เชื่อมโยงกับ 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}

แนวทาง

กฎจะถูกทำลายโดยผู้เชี่ยวชาญเท่านั้นและด้วยเหตุผลที่ดีเท่านั้น:

  • ไฟล์ส่วนหัวมีเพียงexternการประกาศของตัวแปร - staticคำจำกัดความของตัวแปรไม่เคย หรือไม่มีเงื่อนไข

  • สำหรับตัวแปรใด ๆ ที่กำหนดให้มีไฟล์ส่วนหัวเพียงหนึ่งไฟล์เท่านั้นที่ประกาศ (SPOT - Single Point of Truth)

  • ไฟล์ต้นฉบับไม่เคยมีexternการประกาศของตัวแปร - ไฟล์ต้นฉบับจะรวมส่วนหัว (แต่เพียงผู้เดียว) ที่ประกาศไว้เสมอ

  • สำหรับตัวแปรที่กำหนดใด ๆ ไฟล์ต้นฉบับหนึ่งไฟล์จะกำหนดตัวแปรโดยควรกำหนดค่าเริ่มต้นด้วยเช่นกัน (แม้ว่าจะไม่จำเป็นต้องกำหนดค่าเริ่มต้นเป็นศูนย์อย่างชัดเจน แต่ก็ไม่เป็นอันตรายและสามารถทำได้ดีเนื่องจากมีคำจำกัดความเริ่มต้นเพียงหนึ่งเดียวของตัวแปรโกลบอลเฉพาะในโปรแกรม)

  • ไฟล์ต้นฉบับที่กำหนดตัวแปรรวมถึงส่วนหัวเพื่อให้แน่ใจว่าคำจำกัดความและการประกาศสอดคล้องกัน

  • externฟังก์ชั่นไม่ควรต้องประกาศตัวแปรที่ใช้

  • หลีกเลี่ยงตัวแปรโกลบอลเมื่อทำได้ - ใช้ฟังก์ชันแทน

ซอร์สโค้ดและข้อความของคำตอบนี้มีอยู่ในที่เก็บ SOQ (คำถามซ้อนมากเกินไป) ของฉันบน GitHub ในไดเรกทอรีย่อยsrc / so-0143-3204

หากคุณไม่ใช่โปรแกรมเมอร์ C ที่มีประสบการณ์คุณสามารถ (และควรจะ) หยุดอ่านที่นี่

ไม่ใช่วิธีที่ดีในการกำหนดตัวแปรส่วนกลาง

ด้วยคอมไพเลอร์ C (แน่นอนมาก) คุณสามารถหลีกเลี่ยงสิ่งที่เรียกว่านิยาม 'ทั่วไป' ของตัวแปรได้เช่นกัน 'Common' ที่นี่หมายถึงเทคนิคที่ใช้ใน Fortran สำหรับการแชร์ตัวแปรระหว่างไฟล์ต้นฉบับโดยใช้บล็อกสามัญ (อาจมีชื่อ) สิ่งที่เกิดขึ้นที่นี่คือไฟล์แต่ละไฟล์มีคำนิยามที่ไม่แน่นอนของตัวแปร ตราบใดที่ไม่มีไฟล์มากกว่าหนึ่งไฟล์ที่ให้คำจำกัดความที่ถูกกำหนดค่าเริ่มต้นแล้วไฟล์ต่างๆจะจบลงด้วยการแชร์คำจำกัดความทั่วไปของตัวแปร:

file10.c

#include "prog2.h"

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

void inc(void) { l++; }

file11.c

#include "prog2.h"

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

void dec(void) { l--; }

file12.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); }

เทคนิคนี้ไม่เป็นไปตามตัวอักษรของมาตรฐาน C และ 'กฎนิยามเดียว' - มันเป็นพฤติกรรมที่ไม่ได้กำหนดอย่างเป็นทางการ:

J.2 พฤติกรรมที่ไม่ได้กำหนด

มีการใช้ตัวระบุที่มีการเชื่อมโยงภายนอก แต่ในโปรแกรมไม่มีคำจำกัดความภายนอกหนึ่งคำสำหรับตัวระบุหรือไม่ได้ใช้ตัวระบุและมีคำจำกัดความภายนอกหลายรายการสำหรับตัวระบุ (6.9)

.96.9 คำจำกัดความภายนอก¶5

นิยามภายนอกคือการประกาศภายนอกที่ยังเป็นความหมายของฟังก์ชั่น (นอกเหนือจากคำนิยามแบบอินไลน์) หรือวัตถุ หากตัวบ่งชี้ที่ประกาศพร้อมลิงก์ภายนอกถูกใช้ในนิพจน์ (นอกเหนือจากส่วนหนึ่งของตัวถูกดำเนินการของsizeofหรือ_Alignofโอเปอเรเตอร์ที่ผลลัพธ์เป็นค่าคงที่จำนวนเต็ม) ที่ใดที่หนึ่งในโปรแกรมทั้งหมดจะมีคำจำกัดความภายนอกอย่างแน่นอนสำหรับตัวระบุ มิฉะนั้นจะต้องมีมากกว่าหนึ่ง 161)

161)ดังนั้นหากตัวบ่งชี้ที่ประกาศด้วยการเชื่อมโยงภายนอกไม่ได้ใช้ในการแสดงออกก็ไม่จำเป็นต้องมีคำจำกัดความภายนอก

อย่างไรก็ตามมาตรฐานซียังแสดงไว้ในภาคผนวกข้อมูล J เป็นหนึ่งในส่วนขยายที่พบบ่อย

J.5.11 หลายคำจำกัดความภายนอก

อาจมีมากกว่าหนึ่งคำจำกัดความภายนอกสำหรับตัวระบุของวัตถุที่มีหรือไม่มีการใช้คำหลักภายนอกอย่างชัดเจน หากคำจำกัดความไม่เห็นด้วยหรือมากกว่าหนึ่งค่าเริ่มต้นพฤติกรรมที่ไม่ได้กำหนด (6.9.2)

เนื่องจากเทคนิคนี้ไม่ได้รับการสนับสนุนเสมอไปจึงเป็นการดีที่สุดที่จะหลีกเลี่ยงการใช้งานโดยเฉพาะอย่างยิ่งหากรหัสของคุณต้องพกพาได้ การใช้เทคนิคนี้คุณยังสามารถจบด้วยการสะกดคำแบบไม่ได้ตั้งใจ

หากไฟล์ใดไฟล์หนึ่งข้างต้นถูกประกาศว่าlเป็น a doubleแทนที่จะเป็น a longตัวลิงก์ประเภทที่ไม่ปลอดภัยของ C อาจไม่พบจุดที่ไม่ตรงกัน หากคุณอยู่ในเครื่องที่มี 64- บิตlongและdoubleคุณจะไม่ได้รับคำเตือน บนเครื่องที่มีขนาด 32- บิตlongและ 64- บิตdoubleคุณอาจได้รับคำเตือนเกี่ยวกับขนาดต่าง ๆ - ตัวเชื่อมโยงจะใช้ขนาดที่ใหญ่ที่สุดเหมือนกับที่โปรแกรม Fortran ใช้ขนาดใหญ่ที่สุดของบล็อกทั่วไป

โปรดทราบว่า GCC 10.1.0 ซึ่งเปิดตัวใน 2020-05-07 เปลี่ยนตัวเลือกการคอมไพล์เริ่มต้นที่ใช้-fno-commonซึ่งหมายความว่าโดยค่าเริ่มต้นโค้ดด้านบนจะไม่เชื่อมโยงอีกต่อไปเว้นแต่คุณจะแทนที่ค่าเริ่มต้นด้วย-fcommon(หรือใช้แอททริบิว ฯลฯ ) ดูลิงค์)


ไฟล์สองไฟล์ถัดไปทำให้สมบูรณ์สำหรับ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();
}
  • prog2การใช้งานprog2.c, file10.c, file11.c, ,file12.cprog2.h

คำเตือน

ดังที่ระบุไว้ในความคิดเห็นที่นี่และตามที่ระบุไว้ในคำตอบของฉันสำหรับคำถามที่คล้ายกันการใช้คำจำกัดความหลายตัวแปรทั่วโลกทำให้เกิดพฤติกรรมที่ไม่ได้กำหนด (J.2; .9.9.9) ซึ่งเป็นวิธีมาตรฐานในการพูดว่า สิ่งหนึ่งที่สามารถเกิดขึ้นได้คือโปรแกรมทำงานตามที่คุณคาดไว้ และ J.5.11 พูดว่า "คุณอาจโชคดีบ่อยกว่าที่คุณสมควรได้รับ" โดยประมาณ แต่โปรแกรมที่ต้องอาศัยคำจำกัดความหลายตัวของตัวแปร extern ไม่ว่าจะมีหรือไม่มีคีย์เวิร์ด 'extern' อย่างชัดเจนไม่ใช่โปรแกรมที่สอดคล้องอย่างเคร่งครัดและไม่รับประกันว่าจะทำงานได้ทุกที่ อย่างเท่าเทียมกัน: มันมีข้อผิดพลาดที่อาจหรืออาจไม่แสดงตัวเอง

ละเมิดหลักเกณฑ์

แน่นอนว่ามีหลายวิธีที่แนวทางเหล่านี้อาจเสียหายได้ บางครั้งอาจมีเหตุผลที่ดีในการทำลายแนวทาง แต่โอกาสดังกล่าวผิดปกติอย่างยิ่ง

faulty_header.h

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

หมายเหตุ 1: ถ้าส่วนหัวกำหนดตัวแปรโดยไม่มีexternคีย์เวิร์ดดังนั้นแต่ละไฟล์ที่มีส่วนหัวจะสร้างนิยามที่แน่นอนของตัวแปร ดังที่ระบุไว้ก่อนหน้านี้มักจะใช้งานได้ แต่มาตรฐาน C ไม่รับประกันว่าจะทำงานได้

broken_header.h

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

หมายเหตุ 2: หากส่วนหัวกำหนดและเริ่มต้นตัวแปรดังนั้นไฟล์ต้นฉบับเพียงไฟล์เดียวในโปรแกรมที่กำหนดสามารถใช้ส่วนหัวได้ เนื่องจากส่วนหัวมีไว้สำหรับการแบ่งปันข้อมูลเป็นหลักมันค่อนข้างโง่ที่จะสร้างขึ้นมาซึ่งสามารถใช้ได้เพียงครั้งเดียว

seldom_correct.h

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

หมายเหตุ 3: หากส่วนหัวกำหนดตัวแปรคงที่ (มีหรือไม่มีการเริ่มต้น) จากนั้นแต่ละไฟล์ต้นฉบับจะจบลงด้วยรุ่นส่วนตัวของตัวแปร 'ทั่วโลก'

ตัวอย่างเช่นถ้าตัวแปรเป็นอาร์เรย์แบบซับซ้อนตัวอย่างเช่นอาจทำให้เกิดการทำซ้ำรหัสอย่างรุนแรง บางครั้งมันอาจเป็นวิธีที่สมเหตุสมผลในการทำให้เกิดผลกระทบบางอย่าง แต่นั่นก็ผิดปกติมาก


สรุป

ใช้เทคนิคหัวข้อที่ฉันแสดงก่อน มันทำงานได้อย่างน่าเชื่อถือและทุกที่ โดยเฉพาะอย่างยิ่งโปรดทราบว่าส่วนหัวประกาศglobal_variableรวมอยู่ในทุกไฟล์ที่ใช้งาน - รวมถึงส่วนที่กำหนดไว้ สิ่งนี้ทำให้มั่นใจได้ว่าทุกอย่างสอดคล้องกัน

ความกังวลที่คล้ายกันเกิดขึ้นกับการประกาศและการกำหนดฟังก์ชั่น - ใช้กฎแบบอะนาล็อก แต่คำถามนั้นเกี่ยวกับตัวแปรโดยเฉพาะดังนั้นฉันจึงเก็บคำตอบไว้กับตัวแปรเท่านั้น

จบคำตอบเดิม

หากคุณไม่ใช่โปรแกรมเมอร์ C ที่มีประสบการณ์คุณควรหยุดอ่านที่นี่


ปลายสายสำคัญ

หลีกเลี่ยงการทำซ้ำรหัส

ข้อกังวลหนึ่งที่บางครั้ง (และถูกต้องตามกฎหมาย) เกิดขึ้นเกี่ยวกับกลไก 'การประกาศในส่วนหัวคำจำกัดความในแหล่งที่มา' อธิบายไว้ที่นี่คือมีไฟล์สองไฟล์ที่จะซิงโครไนซ์ - ส่วนหัวและแหล่งที่มา โดยทั่วไปแล้วจะมีการติดตามด้วยการสังเกตว่าสามารถใช้แมโครเพื่อให้ส่วนหัวทำหน้าที่สองหน้าที่ - โดยปกติแล้วจะประกาศตัวแปร แต่เมื่อมีการตั้งค่าแมโครเฉพาะก่อนที่จะรวมส่วนหัวมันจะกำหนดตัวแปรแทน

ข้อกังวลอีกประการหนึ่งคือตัวแปรที่จำเป็นต้องกำหนดใน 'โปรแกรมหลัก' แต่ละรายการ นี่เป็นเรื่องจริงที่น่ากังวล คุณสามารถแนะนำไฟล์ต้นฉบับ C เพื่อกำหนดตัวแปรและลิงก์ไฟล์อ็อบเจ็กต์ที่สร้างกับแต่ละโปรแกรม

แบบแผนทั่วไปทำงานเช่นนี้โดยใช้ตัวแปรโกลบอลดั้งเดิมที่แสดงใน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++);
}

ไฟล์สองไฟล์ถัดไปทำให้สมบูรณ์สำหรับ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;
}
  • prog3การใช้งานprog3.c, file1a.c, file2a.c, ,file3a.hprog3.h

การเริ่มต้นตัวแปร

ปัญหาของโครงร่างนี้ดังที่แสดงคือมันไม่ได้เตรียมไว้สำหรับการเริ่มต้นของตัวแปรโกลบอล ด้วย C99 หรือ C11 และรายการอาร์กิวเมนต์ตัวแปรสำหรับแมโครคุณสามารถกำหนดแมโครเพื่อสนับสนุนการเริ่มต้นได้เช่นกัน (ด้วย C89 และไม่รองรับรายการอาร์กิวเมนต์ตัวแปรในมาโครจึงไม่มีวิธีที่ง่ายในการจัดการกับ initializers ที่ยาวโดยพลการ)

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 });

ย้อนกลับเนื้อหาของ#ifและ#elseบล็อกแก้ไขข้อผิดพลาดที่ระบุโดย 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;
}

เห็นได้ชัดว่ารหัสสำหรับโครงสร้างคี่บอลนั้นไม่ใช่สิ่งที่คุณเขียนตามปกติ แต่มันแสดงให้เห็นถึงจุด อาร์กิวเมนต์แรกไปภาวนาที่สองของการINITIALIZERมี{ 41และการโต้แย้งที่เหลือ (เอกพจน์ในตัวอย่างนี้) 43 }คือ หากไม่มี C99 หรือการสนับสนุนที่คล้ายกันสำหรับรายการอาร์กิวเมนต์ตัวแปรสำหรับแมโครค่าเริ่มต้นที่จำเป็นต้องมีเครื่องหมายจุลภาคนั้นเป็นปัญหามาก

file3b.hรวมส่วนหัวที่ถูกต้อง(แทนfileba.h) ต่อ Denis Kniazhev


ไฟล์สองไฟล์ถัดไปทำให้สมบูรณ์สำหรับ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;
}
  • prog4การใช้งานprog4.c, file1b.c, file2b.c, ,prog4.hfile3b.h

Header Guards

ส่วนหัวใด ๆ ควรได้รับการปกป้องจากการรวมใหม่ดังนั้นคำจำกัดความของประเภท (enum, struct หรือ union type หรือโดยทั่วไปพิมพ์ดีด) จะไม่ทำให้เกิดปัญหา เทคนิคมาตรฐานคือการห่อเนื้อหาของส่วนหัวในการ์ดส่วนหัวเช่น:

#ifndef FILE3B_H_INCLUDED
#define FILE3B_H_INCLUDED

...contents of header...

#endif /* FILE3B_H_INCLUDED */

ส่วนหัวอาจรวมสองครั้งทางอ้อม ตัวอย่างเช่นหากfile4b.hรวมถึงfile3b.hคำจำกัดความประเภทที่ไม่ได้แสดงและfile1b.cจำเป็นต้องใช้ทั้งส่วนหัวfile4b.hและfile3b.hจากนั้นคุณมีปัญหายุ่งยากในการแก้ไขเพิ่มเติม file4b.hเห็นได้ชัดว่าคุณอาจแก้ไขรายการส่วนหัวที่จะรวมเพียง อย่างไรก็ตามคุณอาจไม่ทราบถึงการพึ่งพาภายใน - และรหัสควรจะยังคงทำงานต่อไป

ยิ่งไปกว่านั้นมันเริ่มมีเล่ห์เหลี่ยมเนื่องจากคุณอาจรวมfile4b.hก่อนหน้านี้file3b.hเพื่อรวมการสร้างคำจำกัดความ แต่ตัวป้องกันส่วนหัวปกติบนfile3b.hจะป้องกันไม่ให้ส่วนหัวถูกรวมใหม่

ดังนั้นคุณต้องรวมเนื้อหาอย่างfile3b.hน้อยหนึ่งครั้งสำหรับการประกาศและอย่างน้อยหนึ่งครั้งสำหรับคำจำกัดความ แต่คุณอาจต้องการทั้งสองอย่างในหน่วยการแปลเดียว (TU - การรวมกันของไฟล์ต้นฉบับและส่วนหัวที่ใช้)

การรวมหลายรายการพร้อมกับนิยามตัวแปร

อย่างไรก็ตามสามารถทำได้ภายใต้ข้อ จำกัด ที่ไม่สมเหตุสมผลเกินไป มาแนะนำชื่อไฟล์ชุดใหม่:

  • external.h สำหรับนิยามแมโคร EXTERN ฯลฯ

  • file1c.hเพื่อกำหนดประเภท (โดยเฉพาะstruct oddballประเภทของoddball_struct)

  • file2c.h เพื่อกำหนดหรือประกาศตัวแปรโกลบอล

  • file3c.c ซึ่งกำหนดตัวแปรทั่วโลก

  • file4c.c ซึ่งเพียงแค่ใช้ตัวแปรทั่วโลก

  • file5c.c ซึ่งแสดงให้เห็นว่าคุณสามารถประกาศและจากนั้นกำหนดตัวแปรทั่วโลก

  • file6c.c ซึ่งแสดงให้เห็นว่าคุณสามารถกำหนดและจากนั้น (พยายาม) ประกาศตัวแปรทั่วโลก

ในตัวอย่างเหล่านี้file5c.cและfile6c.cรวมส่วนหัวโดยตรงfile2c.hหลายครั้ง แต่นั่นเป็นวิธีที่ง่ายที่สุดในการแสดงให้เห็นว่ากลไกทำงาน หมายความว่าหากส่วนหัวนั้นถูกรวมทางอ้อมสองครั้งก็จะปลอดภัยเช่นกัน

ข้อ จำกัด สำหรับการทำงานนี้คือ:

  1. ส่วนหัวที่กำหนดหรือประกาศตัวแปรทั่วโลกอาจไม่ได้กำหนดประเภทใด ๆ

  2. ทันทีก่อนที่คุณจะมีส่วนหัวที่ควรกำหนดตัวแปรคุณจะต้องกำหนดแมโคร DEFINE_VARIABLES

  3. ส่วนหัวที่กำหนดหรือประกาศตัวแปรมีเนื้อหาที่มีสไตล์

external.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; }

แฟ้มแหล่งที่มาต่อไปเสร็จสมบูรณ์แหล่งที่มา (มีโปรแกรมหลัก) สำหรับprog5, prog6และprog7:

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;
}
  • prog5การใช้งานprog5.c, file3c.c, file4c.c, file1c.h, ,file2c.hexternal.h

  • prog6การใช้งานprog5.c, file5c.c, file4c.c, file1c.h, ,file2c.hexternal.h

  • prog7การใช้งานprog5.c, file6c.c, file4c.c, file1c.h, ,file2c.hexternal.h


โครงการนี้หลีกเลี่ยงปัญหาส่วนใหญ่ คุณพบปัญหาเฉพาะในกรณีที่ส่วนหัวที่กำหนดตัวแปร (เช่นfile2c.h) รวมอยู่ในส่วนหัวอื่น (พูดfile7c.h) ที่กำหนดตัวแปร ไม่มีวิธีง่าย ๆ ในการอื่นนอกเหนือจาก "อย่าทำ"

คุณสามารถแก้ไขปัญหาบางส่วนได้โดยแก้ไขfile2c.hเป็น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 */

ปัญหากลายเป็น 'ส่วนหัวควรรวม#undef DEFINE_VARIABLESหรือไม่' หากคุณไม่ใช้สิ่งนั้นจากส่วนหัวและตัดคำเชิญที่กำหนดด้วย#defineและ#undef:

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

ในซอร์สโค้ด (ดังนั้นส่วนหัวไม่เคยเปลี่ยนแปลงค่าของDEFINE_VARIABLES) จากนั้นคุณควรจะสะอาด มันเป็นเพียงความรำคาญที่ต้องจำไว้ว่าให้เขียนบรรทัดพิเศษ ทางเลือกอาจเป็น:

#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 */

สิ่งนี้กำลังได้รับความสับสนเล็กน้อย แต่ดูเหมือนว่าจะปลอดภัย (ใช้โดยfile2d.hไม่มี#undef DEFINE_VARIABLESใน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; }

file8c.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; }

ไฟล์สองไฟล์ถัดไปทำให้สมบูรณ์สำหรับprog8และprog9:

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;
}
  • prog8การใช้งานprog8.c, ,file7c.cfile9c.c

  • prog9การใช้งานprog8.c, ,file8c.cfile9c.c


อย่างไรก็ตามปัญหาดังกล่าวไม่น่าเป็นไปได้ที่จะเกิดขึ้นในทางปฏิบัติโดยเฉพาะหากคุณใช้คำแนะนำมาตรฐาน

หลีกเลี่ยงตัวแปรส่วนกลาง


นิทรรศการนี้พลาดอะไรหรือเปล่า?

คำสารภาพ : โครงร่าง 'การหลีกเลี่ยงรหัสที่ซ้ำกัน' ที่ระบุไว้ที่นี่ได้รับการพัฒนาเนื่องจากปัญหานี้ส่งผลกระทบต่อรหัสบางอย่างที่ฉันทำงาน (แต่ไม่ได้เป็นเจ้าของ) และเป็นปัญหาที่น่ากังวลสำหรับโครงการที่ระบุไว้ในส่วนแรกของคำตอบ อย่างไรก็ตามรูปแบบดั้งเดิมทำให้คุณมีเพียงสองสถานที่ที่จะปรับเปลี่ยนเพื่อให้คำจำกัดความของตัวแปรและการประกาศตรงกันซึ่งเป็นขั้นตอนใหญ่ไปข้างหน้ามีการประกาศตัวแปร exernal กระจัดกระจายไปทั่วฐานรหัส (ซึ่งจริงๆสำคัญเมื่อมีไฟล์นับพันทั้งหมด) . อย่างไรก็ตามรหัสในไฟล์ที่มีชื่อfileNc.[ch](บวกexternal.hและexterndef.h) แสดงว่าสามารถทำงานได้ เห็นได้ชัดว่าคงไม่ยากที่จะสร้างสคริปต์ตัวสร้างส่วนหัวเพื่อให้แม่แบบมาตรฐานสำหรับตัวแปรที่กำหนดและประกาศไฟล์ส่วนหัว

NBโปรแกรมเหล่านี้เป็นของเล่นที่มีรหัสเพียงพอที่จะทำให้พวกเขาน่าสนใจเล็กน้อย มีการทำซ้ำภายในตัวอย่างที่สามารถลบออกได้ แต่ไม่ใช่เพื่อทำให้คำอธิบายการสอนง่ายขึ้น (ตัวอย่างเช่น: ความแตกต่างระหว่างprog5.cและprog8.cเป็นชื่อของหนึ่งในส่วนหัวที่รวมไว้มันเป็นไปได้ที่จะจัดระเบียบรหัสใหม่เพื่อให้main()ฟังก์ชั่นไม่ได้ทำซ้ำ แต่มันจะปกปิดมากกว่าที่เปิดเผย)


3
@litb: ดูภาคผนวก J.5.11 สำหรับคำจำกัดความทั่วไป - เป็นส่วนขยายทั่วไป
Jonathan Leffler

3
@litb: และฉันเห็นด้วยควรหลีกเลี่ยง - นั่นเป็นสาเหตุที่อยู่ในส่วนของ 'วิธีที่ไม่ดีในการกำหนดตัวแปรทั่วโลก'
Jonathan Leffler

3
อันที่จริงมันเป็นส่วนขยายทั่วไป แต่เป็นพฤติกรรมที่ไม่ได้กำหนดไว้สำหรับโปรแกรมที่จะใช้มัน ฉันแค่ไม่ชัดเจนว่าคุณกำลังบอกหรือไม่ว่านี่เป็นการอนุญาตตามกฎของ C ตอนนี้ฉันเห็นคุณกำลังบอกว่ามันเป็นเพียงส่วนขยายทั่วไปและเพื่อหลีกเลี่ยงหากคุณต้องการรหัสของคุณเพื่อพกพา ดังนั้นฉันสามารถโหวตคุณได้โดยไม่ต้องสงสัย จริงๆคำตอบที่ดี IMHO :)
โยฮันเน Schaub - litb

19
หากคุณหยุดที่ด้านบนมันช่วยให้สิ่งที่ง่ายง่าย เมื่อคุณอ่านเพิ่มเติมลงไปมันเกี่ยวข้องกับความแตกต่างความยุ่งยากและรายละเอียดเพิ่มเติม ฉันเพิ่งเพิ่ม 'จุดหยุดต้น' สองจุดสำหรับโปรแกรมเมอร์ C ที่มีประสบการณ์น้อยกว่าหรือโปรแกรมเมอร์ C ที่รู้จักเรื่องนี้อยู่แล้ว ไม่จำเป็นต้องอ่านทุกอย่างถ้าคุณรู้คำตอบแล้ว (แต่ให้ฉันรู้ถ้าคุณพบข้อผิดพลาดทางเทคนิค)
Jonathan Leffler

4
@supercat: มันเกิดขึ้นกับฉันว่าคุณสามารถใช้ตัวอักษรอาร์เรย์ C99 เพื่อรับค่าการแจงนับสำหรับขนาดอาร์เรย์, exemplified by ( foo.h): #define FOO_INITIALIZER { 1, 2, 3, 4, 5 }เพื่อกำหนด initializer สำหรับอาร์เรย์, enum { FOO_SIZE = sizeof((int [])FOO_INITIALIZER) / sizeof(((int [])FOO_INITIALIZER)[0]) };เพื่อให้ได้ขนาดของอาร์เรย์, และextern int foo[];ประกาศอาร์เรย์ . เห็นได้ชัดว่าคำจำกัดความควรจะเป็นเพียงint foo[FOO_SIZE] = FOO_INITIALIZER;แต่ขนาดไม่จำเป็นต้องรวมอยู่ในคำนิยาม นี่ทำให้คุณมีค่าคงที่จำนวนเต็ม, FOO_SIZE.
Jonathan Leffler

125

externตัวแปรคือการประกาศ (ขอบคุณ SBI สำหรับการแก้ไข) ของตัวแปรซึ่งกำหนดไว้ในหน่วยการแปลอีก นั่นหมายถึงหน่วยเก็บสำหรับตัวแปรถูกจัดสรรในไฟล์อื่น

สมมติว่าคุณมีสอง.c-Files และtest1.c test2.cหากคุณกำหนดตัวแปรทั่วโลกint test1_var;ในtest1.cและคุณต้องการเข้าถึงตัวแปรนี้test2.cคุณต้องใช้ในextern int test1_var;test2.c

ตัวอย่างที่สมบูรณ์:

$ 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
ไม่มี "คำจำกัดความหลอก" มันเป็นการประกาศ
sbi

3
ในตัวอย่างข้างต้นถ้าผมเปลี่ยนextern int test1_var;ไปint test1_var;, ลิงเกอร์ (GCC 5.4.0) ยังคงผ่านไป ดังนั้นexternจำเป็นจริง ๆ ในกรณีนี้
Radiohead

2
@radiohead: ในคำตอบของฉันคุณจะพบข้อมูลที่ปล่อยexternส่วนขยายที่มักใช้งานได้ - และใช้งานได้เฉพาะกับ GCC (แต่ GCC นั้นไกลเกินกว่าจะเป็นคอมไพเลอร์เพียงตัวเดียวที่รองรับมัน; คุณสามารถมองหา "J.5.11" หรือส่วน "วิธีที่ไม่ดีดังนั้น" ในคำตอบของฉัน (ฉันรู้ - มันคือยาว) และข้อความที่อยู่ใกล้ที่อธิบายมัน (หรือพยายามที่จะทำเช่นนั้น)
Jonathan Leffler

การประกาศภายนอกไม่จำเป็นต้องกำหนดไว้ในหน่วยการแปลอื่น (และโดยทั่วไปจะไม่ใช่) ในความเป็นจริงการประกาศและคำนิยามสามารถเป็นหนึ่งเดียวกัน
จำโมนิก้า

40

ภายนอกคือคำหลักที่คุณใช้เพื่อประกาศว่าตัวแปรนั้นอยู่ในหน่วยการแปลอื่น

ดังนั้นคุณสามารถตัดสินใจที่จะใช้ตัวแปรในหน่วยการแปลแล้วเข้าถึงจากตัวแปรอื่นจากนั้นในอันที่สองคุณประกาศว่าเป็นภายนอกและสัญลักษณ์จะได้รับการแก้ไขโดย linker

หากคุณไม่ประกาศว่าเป็นภายนอกคุณจะได้รับ 2 ตัวแปรชื่อเดียวกัน แต่ไม่เกี่ยวข้องเลยและข้อผิดพลาดของคำจำกัดความหลายตัวแปร


5
กล่าวอีกนัยหนึ่งหน่วยการแปลที่ใช้ extern รู้เกี่ยวกับตัวแปรนี้ชนิดของมันและดังนั้นจึงอนุญาตให้ซอร์สโค้ดในลอจิกพื้นฐานที่จะใช้มัน แต่ไม่ได้จัดสรรตัวแปรหน่วยการแปลอื่นจะทำเช่นนั้น หากหน่วยการแปลทั้งสองต้องประกาศตัวแปรตามปกติจะมีตำแหน่งทางกายภาพสองตำแหน่งสำหรับตัวแปรพร้อมกับการอ้างอิง "ผิด" ที่เกี่ยวข้องภายในโค้ดที่คอมไพล์และมีความคลุมเครือที่เกิดขึ้นกับลิงเกอร์
mjv

26

ฉันชอบคิดว่าตัวแปรภายนอกเป็นสัญญาที่คุณทำกับคอมไพเลอร์

เมื่อเจอกับ extern คอมไพเลอร์สามารถค้นหาประเภทของมันไม่ใช่ตำแหน่งที่ "อยู่" ดังนั้นจึงไม่สามารถแก้ไขการอ้างอิงได้

คุณกำลังบอกว่า "เชื่อฉันในเวลาลิงก์การอ้างอิงนี้จะแก้ไขได้"


โดยทั่วไปการประกาศเป็นสัญญาว่าชื่อจะสามารถแก้ไขได้กับคำจำกัดความหนึ่งคำในเวลาลิงก์ extern ประกาศตัวแปรโดยไม่กำหนด
Lie Ryan

18

extern บอกให้คอมไพเลอร์เชื่อใจคุณว่าหน่วยความจำสำหรับตัวแปรนี้ถูกประกาศไว้ที่อื่นดังนั้นจึงไม่ลองจัดสรร / ตรวจสอบหน่วยความจำ

ดังนั้นคุณสามารถรวบรวมไฟล์ที่มีการอ้างอิงไปยัง extern แต่คุณไม่สามารถเชื่อมโยงได้หากหน่วยความจำนั้นไม่ได้ถูกประกาศที่ไหนสักแห่ง

มีประโยชน์สำหรับตัวแปรและไลบรารีระดับโลก แต่มีอันตรายเนื่องจากตัวลิงก์ไม่ได้ตรวจสอบ


หน่วยความจำไม่ได้ประกาศ ดูคำตอบสำหรับคำถามนี้: stackoverflow.com/questions/1410563สำหรับรายละเอียดเพิ่มเติม
sbi

15

เพิ่มexternเปลี่ยนตัวแปรนิยามเป็นตัวแปรประกาศ ดูกระทู้นี้ว่าอะไรคือความแตกต่างระหว่างการประกาศและคำจำกัดความ


ความแตกต่างระหว่างint fooและextern int foo(ขอบเขตไฟล์) คืออะไร? ทั้งคู่เป็นการประกาศใช่มั้ย

@ user14284: ทั้งคู่เป็นเพียงการประกาศในแง่ที่ว่าทุกคำจำกัดความเป็นการประกาศเช่นกัน แต่ฉันเชื่อมโยงกับคำอธิบายของสิ่งนี้ ("ดูกระทู้นี้ว่าอะไรคือความแตกต่างระหว่างการประกาศและคำจำกัดความ") ทำไมคุณไม่ง่ายไปตามลิงค์และอ่าน?
sbi

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

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

การประกาศจะไม่จัดสรรหน่วยความจำ (ต้องกำหนดตัวแปรสำหรับการจัดสรรหน่วยความจำ) แต่จะมีการกำหนด นี่เป็นเพียงมุมมองง่ายๆเกี่ยวกับคำหลักภายนอกเนื่องจากคำตอบอื่น ๆ นั้นยอดเยี่ยมมาก


11

การตีความที่ถูกต้องของภายนอกคือคุณบอกบางสิ่งกับคอมไพเลอร์ คุณบอกคอมไพเลอร์ว่าแม้จะไม่มีอยู่ในปัจจุบันตัวแปรที่ประกาศจะพบได้โดย linker (โดยทั่วไปแล้วจะอยู่ในวัตถุอื่น (ไฟล์)) ลิงเกอร์จะเป็นคนโชคดีที่พบทุกสิ่งและรวบรวมมันเข้าด้วยกันไม่ว่าคุณจะมีการประกาศจากภายนอกหรือไม่ก็ตาม


8

ใน C ตัวแปรภายในไฟล์บอกว่า example.c ได้รับขอบเขตในตัวเครื่อง คอมไพเลอร์คาดหวังว่าตัวแปรจะมีคำจำกัดความของมันอยู่ในไฟล์เดียวกัน example.c และเมื่อมันไม่พบเหมือนกันมันก็จะโยนข้อผิดพลาดฟังก์ชั่นในมืออื่น ๆ ที่มีตามขอบเขตทั่วโลกเริ่มต้น ดังนั้นคุณไม่ต้องพูดถึงคอมไพเลอร์ "look dude ... คุณอาจพบคำจำกัดความของฟังก์ชันนี้ที่นี่" สำหรับฟังก์ชั่นรวมถึงไฟล์ที่มีการประกาศก็เพียงพอแล้ว (ไฟล์ที่คุณเรียกไฟล์ส่วนหัว) ตัวอย่างเช่นพิจารณาไฟล์ 2 ไฟล์ต่อไปนี้:
example.c

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

example1.c

int a = 5;

ตอนนี้เมื่อคุณคอมไพล์ไฟล์ทั้งสองเข้าด้วยกันโดยใช้คำสั่งต่อไปนี้:

ขั้นตอนที่ 1) cc -o ex example.c example1.c ขั้นตอนที่ 2) ./ ex

คุณได้ผลลัพธ์ต่อไปนี้: ค่าของ a คือ <5>


8

การใช้งาน GCC ELF Linux

คำตอบอื่น ๆ ได้กล่าวถึงมุมมองด้านการใช้ภาษาดังนั้นตอนนี้เรามาดูวิธีการนำไปใช้ในการใช้งานนี้

main.c

#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);
}

รวบรวมและถอดรหัส:

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

เอาท์พุทประกอบด้วย:

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

บทที่System V ABI Update ELF สเปค "ตารางสัญลักษณ์" อธิบาย:

SHN_UNDEF ดัชนีตารางส่วนนี้หมายความว่าไม่มีการกำหนดสัญลักษณ์ เมื่อตัวแก้ไขลิงก์รวมอ็อบเจ็กต์ไฟล์นี้เข้ากับอีกอันที่กำหนดสัญลักษณ์ที่ระบุการอ้างอิงของสัญลักษณ์นี้กับไฟล์จะถูกลิงก์กับนิยามจริง

ซึ่งโดยทั่วไปพฤติกรรมที่มาตรฐาน C ให้กับexternตัวแปร

จากนี้ไปมันเป็นหน้าที่ของ linker ในการสร้างโปรแกรมขั้นสุดท้าย แต่externข้อมูลได้ถูกดึงออกมาจากซอร์สโค้ดไปยังไฟล์ออบเจ็กต์แล้ว

ทดสอบกับ GCC 4.8

ตัวแปรอินไลน์ C ++ 17

ใน C ++ 17 คุณอาจต้องการใช้ตัวแปรอินไลน์แทนตัวแปร extern เนื่องจากมันใช้ง่าย (สามารถกำหนดได้เพียงครั้งเดียวที่ส่วนหัว) และมีประสิทธิภาพมากกว่า (รองรับ constexpr) ดู: 'const static' หมายถึงอะไรใน C และ C ++


3
มันไม่ใช่การลงคะแนนเสียงของฉันดังนั้นฉันไม่รู้ อย่างไรก็ตามฉันจะเสนอความคิดเห็น แม้ว่าการดูผลลัพธ์readelfหรือnmอาจมีประโยชน์คุณยังไม่ได้อธิบายพื้นฐานของวิธีการใช้ประโยชน์externหรือทำให้โปรแกรมแรกเสร็จสมบูรณ์พร้อมคำจำกัดความที่แท้จริง notExternรหัสของคุณไม่ได้ใช้ มีปัญหาเกี่ยวกับระบบการตั้งชื่อด้วยเช่นกัน: แม้ว่าจะnotExternมีการกำหนดไว้ที่นี่แทนที่จะประกาศด้วยexternแต่มันเป็นตัวแปรภายนอกที่สามารถเข้าถึงได้โดยไฟล์ต้นฉบับอื่นหากหน่วยการแปลเหล่านั้นมีการประกาศที่เหมาะสม (ซึ่งต้องextern int notExtern;!
Jonathan Leffler

1
@ JonathanLeffler ขอบคุณสำหรับความคิดเห็น! พฤติกรรมมาตรฐานและคำแนะนำการใช้งานได้ทำไปแล้วในคำตอบอื่น ๆ ดังนั้นฉันจึงตัดสินใจที่จะแสดงการใช้งานเล็กน้อยเพราะนั่นช่วยให้ฉันเข้าใจว่าเกิดอะไรขึ้นจริง ๆ ไม่ได้ใช้notExternน่าเกลียดแก้ไขมัน เกี่ยวกับระบบการตั้งชื่อแจ้งให้เราทราบหากคุณมีชื่อที่ดีขึ้น แน่นอนว่าไม่ใช่ชื่อที่ดีสำหรับโปรแกรมจริง แต่ฉันคิดว่ามันเหมาะกับบทบาทการสอนที่นี่
Ciro Santilli 法轮功病毒审查六四事件法轮功

สำหรับชื่อแล้วglobal_defตัวแปรที่กำหนดไว้ที่นี่และextern_refตัวแปรที่กำหนดในโมดูลอื่น พวกเขาจะมีความสมดุลที่ชัดเจนอย่างเหมาะสมหรือไม่ คุณยังท้ายint extern_ref = 57;หรืออะไรทำนองนั้นในไฟล์ที่มีการกำหนดดังนั้นชื่อจึงไม่เหมาะ แต่ในบริบทของไฟล์ต้นฉบับมันเป็นตัวเลือกที่สมเหตุสมผล การมีextern int global_def;ส่วนหัวไม่ได้เป็นปัญหามากนัก ขึ้นอยู่กับคุณทั้งหมดแน่นอน
Jonathan Leffler

7

คำหลักภายนอกจะใช้กับตัวแปรสำหรับการระบุว่าเป็นตัวแปรทั่วโลก

นอกจากนี้ยังแสดงให้เห็นว่าคุณสามารถใช้ตัวแปรประกาศโดยใช้คำหลัก extern ในไฟล์ใด ๆ แม้ว่ามันจะมีการประกาศ / กำหนดในไฟล์อื่น ๆ


5

extern อนุญาตให้โมดูลหนึ่งของโปรแกรมของคุณเข้าถึงตัวแปรหรือฟังก์ชันส่วนกลางที่ประกาศในโมดูลอื่นของโปรแกรมของคุณ คุณมักจะประกาศตัวแปร extern ในไฟล์ส่วนหัว

หากคุณไม่ต้องการให้โปรแกรมเข้าถึงตัวแปรหรือฟังก์ชั่นของคุณคุณใช้staticซึ่งบอกคอมไพเลอร์ว่าตัวแปรหรือฟังก์ชั่นนี้ไม่สามารถใช้งานได้นอกโมดูลนี้



4

ก่อนอื่นexternคำสำคัญไม่ได้ใช้สำหรับการกำหนดตัวแปร ค่อนข้างจะใช้สำหรับการประกาศตัวแปร ฉันสามารถพูดได้ว่าexternเป็นคลาสหน่วยเก็บข้อมูลไม่ใช่ประเภทข้อมูล

externใช้เพื่อให้ไฟล์ C อื่น ๆ หรือส่วนประกอบภายนอกรู้ว่าตัวแปรนี้ถูกกำหนดไว้แล้วที่ไหนสักแห่ง ตัวอย่าง: หากคุณกำลังสร้างห้องสมุดไม่จำเป็นต้องกำหนดตัวแปรส่วนกลางโดยไม่ต้องมีคำสั่งอยู่ในห้องสมุด ไลบรารีจะถูกคอมไพล์โดยตรง แต่ในขณะที่เชื่อมโยงไฟล์มันจะตรวจสอบข้อกำหนด


3

externถูกใช้เพื่อให้first.cไฟล์หนึ่งไฟล์สามารถเข้าถึงพารามิเตอร์โกลบอลได้อย่างสมบูรณ์ในsecond.cไฟล์อื่น

externสามารถประกาศในfirst.cไฟล์หรือในใด ๆ ของส่วนหัวของไฟล์first.cรวมถึง


3
โปรดทราบว่าการexternประกาศควรอยู่ในส่วนหัวไม่ใช่ในfirst.cเพื่อที่ว่าหากการเปลี่ยนแปลงประเภทการประกาศจะเปลี่ยนแปลงเช่นกัน นอกจากนี้ส่วนหัวที่ประกาศตัวแปรควรรวมอยู่ด้วยsecond.cเพื่อให้แน่ใจว่าคำจำกัดความสอดคล้องกับการประกาศ การประกาศในส่วนหัวเป็นกาวที่ยึดมันเข้าด้วยกัน มันช่วยให้ไฟล์ที่จะรวบรวมแยกต่างหาก แต่มั่นใจว่าพวกเขามีมุมมองที่สอดคล้องของประเภทของตัวแปรทั่วโลก
Jonathan Leffler

2

ด้วย xc8 คุณต้องระวังเกี่ยวกับการประกาศตัวแปรชนิดเดียวกันในแต่ละไฟล์เท่าที่จะทำได้ประกาศอย่างผิดพลาดบางสิ่งบางอย่างintในไฟล์เดียวและcharพูดอีกอย่างหนึ่ง สิ่งนี้อาจนำไปสู่ความเสียหายของตัวแปร

ปัญหานี้ได้รับการแก้ไขอย่างหรูหราในฟอรัม microchip เมื่อ 15 ปีก่อน / * โปรดดู "http: www.htsoft.com" / / "ฟอรัม / all / showflat.php / Cat / 0 / หมายเลข / 18766 / an / 0 / หน้า / 0 # 18766"

แต่ดูเหมือนว่าลิงค์นี้จะไม่ทำงานอีกต่อไป ...

ดังนั้นฉันจะพยายามอธิบายอย่างรวดเร็ว ทำไฟล์ที่เรียกว่า global.h

ในการประกาศดังต่อไปนี้

#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

ตอนนี้อยู่ในไฟล์ main.c

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

ซึ่งหมายความว่าใน main.c unsigned charตัวแปรจะได้รับการประกาศเป็น

ตอนนี้ในไฟล์อื่น ๆ รวมถึง global.h จะมีการประกาศเป็น extern สำหรับไฟล์นั้น

extern unsigned char testing_mode;

unsigned charแต่มันจะได้รับการประกาศอย่างถูกต้องเป็น

โพสต์ฟอรัมเก่าอาจอธิบายสิ่งนี้ได้ชัดเจนยิ่งขึ้น แต่นี่เป็นศักยภาพที่แท้จริงgotchaเมื่อใช้คอมไพเลอร์ที่ช่วยให้คุณสามารถประกาศตัวแปรในไฟล์หนึ่งแล้วประกาศว่าภายนอกเป็นประเภทอื่นในอีกไฟล์หนึ่ง ปัญหาที่เกี่ยวข้องกับการที่ถ้าคุณพูดประกาศ test_mode เป็น int ในไฟล์อื่นก็จะคิดว่ามันเป็น 16 บิต var และเขียนทับส่วนอื่น ๆ ของ ram, อาจทำให้เกิดความเสียหายตัวแปรอื่น การดีบักยาก!


0

โซลูชันสั้น ๆ ที่ฉันใช้เพื่ออนุญาตให้ไฟล์ส่วนหัวมีการอ้างอิงภายนอกหรือการใช้งานจริงของวัตถุ #define GLOBAL_FOO_IMPLEMENTATIONแฟ้มที่จริงมีวัตถุเพียงแค่ไม่ จากนั้นเมื่อฉันเพิ่มวัตถุใหม่ลงในไฟล์นี้มันจะปรากฏขึ้นในไฟล์นั้นโดยที่ฉันไม่ต้องคัดลอกและวางคำจำกัดความ

ฉันใช้รูปแบบนี้ในหลายไฟล์ ดังนั้นเพื่อที่จะทำให้สิ่งต่าง ๆ เป็นของตัวเองให้ได้มากที่สุดฉันแค่ใช้แมโคร GLOBAL เดี่ยวในแต่ละหัวข้อ ส่วนหัวของฉันมีลักษณะเช่นนี้:

//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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.