Ảnh hưởng của từ khóa extern đến chức năng C


171

Trong C, tôi không nhận thấy bất kỳ ảnh hưởng nào của externtừ khóa được sử dụng trước khi khai báo hàm. Lúc đầu, tôi nghĩ rằng khi xác định extern int f();trong một tệp duy nhất buộc bạn phải thực hiện nó bên ngoài phạm vi của tệp. Tuy nhiên tôi phát hiện ra rằng cả hai:

extern int f();
int f() {return 0;}

extern int f() {return 0;}

biên dịch tốt, không có cảnh báo từ gcc. Tôi đã sử dụng gcc -Wall -ansi; nó thậm chí sẽ không chấp nhận //ý kiến.

Có bất kỳ hiệu ứng để sử dụng extern trước khi định nghĩa chức năng ? Hoặc nó chỉ là một từ khóa tùy chọn không có tác dụng phụ cho các chức năng.

Trong trường hợp sau, tôi không hiểu tại sao các nhà thiết kế tiêu chuẩn lại chọn cách sử dụng ngữ pháp với các từ khóa thừa.

EDIT: Để làm rõ, tôi biết có sử dụng cho externcác biến, nhưng tôi chỉ hỏi về externtrong chức năng .


Theo một số nghiên cứu tôi đã làm khi cố gắng sử dụng nó cho một số mục đích tạo khuôn mẫu điên rồ, extern không được hỗ trợ ở dạng mà hầu hết các trình biên dịch dự định, và vì vậy, thực sự không làm gì cả.
Ed James

4
Nó không phải lúc nào cũng thừa, xem câu trả lời của tôi. Bất cứ lúc nào bạn cần chia sẻ một cái gì đó giữa các mô-đun mà bạn KHÔNG muốn trong một tiêu đề công khai, nó rất hữu ích. Tuy nhiên, 'ngoại trừ' mọi chức năng trong một tiêu đề công khai (với trình biên dịch hiện đại) có rất ít lợi ích, vì chúng có thể tự mình tìm ra nó.
Tim Post

@Ed .. nếu volility int foo là toàn cầu trong foo.c và bar.c cần nó, bar.c phải khai báo nó là extern. Nó có lợi thế của nó. Ngoài ra, bạn có thể cần chia sẻ một số chức năng mà bạn KHÔNG muốn tiếp xúc trong một tiêu đề công khai.
Tim Post


2
@Barry Nếu ở tất cả, câu hỏi khác là một bản sao của câu hỏi này. 2009 vs 2012
Elazar Leibovich

Câu trả lời:


138

Chúng tôi có hai tệp, foo.c và bar.c.

Đây là foo.c

#include <stdio.h>

volatile unsigned int stop_now = 0;
extern void bar_function(void);

int main(void)
{
  while (1) {
     bar_function();
     stop_now = 1;
  }
  return 0;
}

Bây giờ, đây là bar.c

#include <stdio.h>

extern volatile unsigned int stop_now;

void bar_function(void)
{
   while (! stop_now) {
      printf("Hello, world!\n");
      sleep(30);
   }
}

Như bạn có thể thấy, chúng tôi không có tiêu đề được chia sẻ giữa foo.c và bar.c, tuy nhiên bar.c cần một cái gì đó được khai báo trong foo.c khi nó được liên kết và foo.c cần một chức năng từ bar.c khi nó được liên kết.

Bằng cách sử dụng 'extern', bạn đang nói với trình biên dịch rằng bất cứ thứ gì theo sau nó sẽ được tìm thấy (không tĩnh) tại thời điểm liên kết; không dự trữ bất cứ điều gì cho nó trong vượt qua hiện tại vì nó sẽ gặp sau này. Các chức năng và các biến được đối xử bình đẳng trong vấn đề này.

Nó rất hữu ích nếu bạn cần chia sẻ một số toàn cầu giữa các mô-đun và không muốn đặt / khởi tạo nó trong một tiêu đề.

Về mặt kỹ thuật, mọi chức năng trong tiêu đề công khai của thư viện là 'extern', tuy nhiên việc gắn nhãn chúng như vậy có rất ít hoặc không có lợi ích, tùy thuộc vào trình biên dịch. Hầu hết các trình biên dịch có thể tự tìm ra điều đó. Như bạn thấy, những chức năng đó thực sự được xác định ở một nơi khác.

Trong ví dụ trên, main () sẽ chỉ in hello world một lần, nhưng tiếp tục nhập bar_feft (). Cũng lưu ý, bar_feft () sẽ không trở lại trong ví dụ này (vì đây chỉ là một ví dụ đơn giản). Chỉ cần tưởng tượng stop_now được sửa đổi khi tín hiệu được phục vụ (do đó, không ổn định) nếu điều này dường như không đủ thực tế.

Externs rất hữu ích cho những thứ như trình xử lý tín hiệu, một mutex mà bạn không muốn đặt trong tiêu đề hoặc cấu trúc, v.v. Hầu hết các trình biên dịch sẽ tối ưu hóa để đảm bảo rằng chúng không dành bất kỳ bộ nhớ nào cho các đối tượng bên ngoài, vì chúng biết 'sẽ đặt nó trong mô-đun nơi đối tượng được xác định. Tuy nhiên, một lần nữa, có rất ít điểm trong việc chỉ định nó với trình biên dịch hiện đại khi tạo nguyên mẫu cho các chức năng công cộng.

Mong rằng sẽ giúp :)


56
Mã của bạn sẽ biên dịch tốt mà không cần extern trước bar_function.
Elazar Leibovich

2
@Tim: Sau đó, bạn không có đặc quyền đáng ngờ khi làm việc với mã tôi làm việc cùng. Nó có thể xảy ra. Đôi khi tiêu đề cũng chứa định nghĩa hàm tĩnh. Nó là xấu xí, và không cần thiết 99,99% thời gian (tôi có thể tắt theo một hoặc hai hoặc độ lớn, nói quá mức độ thường xuyên cần thiết). Nó thường xảy ra khi mọi người hiểu nhầm rằng một tiêu đề chỉ cần thiết khi các tệp nguồn khác sẽ sử dụng thông tin; tiêu đề là (ab) được sử dụng để lưu trữ thông tin khai báo cho một tệp nguồn và không có tệp nào khác dự kiến ​​sẽ bao gồm nó. Đôi khi, nó xảy ra vì lý do méo mó hơn.
Jonathan Leffler

2
@Jonathan Leffler - Thật đáng ngờ! Tôi đã thừa hưởng một số mã khá sơ sài trước đây, nhưng tôi có thể thành thật nói rằng tôi chưa bao giờ thấy ai đó đưa một tuyên bố tĩnh vào một tiêu đề. Tuy nhiên, có vẻ như bạn có một công việc khá thú vị và thú vị :)
Tim Post

1
Nhược điểm của 'nguyên mẫu hàm không có trong tiêu đề' là bạn không có được sự kiểm tra độc lập tự động về tính nhất quán giữa định nghĩa hàm trong bar.cvà khai báo trong foo.c. Nếu hàm được khai báo foo.h cả hai tệp bao gồm foo.h, thì tiêu đề sẽ thực thi tính nhất quán giữa hai tệp nguồn. Nếu không có nó, nếu định nghĩa của bar_functiontrong bar.cnhững thay đổi nhưng việc kê khai trong foo.clà không thay đổi, sau đó mọi thứ đi sai tại thời gian chạy; trình biên dịch không thể phát hiện ra vấn đề. Với một tiêu đề được sử dụng đúng cách, trình biên dịch phát hiện ra vấn đề.
Jonathan Leffler

1
extern trên các khai báo hàm là thừa như 'int' trong 'unsign int'. Cách tốt nhất là sử dụng 'extern' khi nguyên mẫu KHÔNG phải là khai báo chuyển tiếp ... Nhưng nó thực sự nên sống trong một tiêu đề mà không có 'extern' trừ khi tác vụ là trường hợp cạnh. stackoverflow.com/questions/10137037/

82

Theo như tôi nhớ về tiêu chuẩn, tất cả các khai báo hàm được coi là "extern" theo mặc định, do đó không cần phải chỉ định rõ ràng.

Điều đó không làm cho từ khóa này trở nên vô dụng vì nó cũng có thể được sử dụng với các biến (và trong trường hợp đó - đó là giải pháp duy nhất để giải quyết các vấn đề liên kết). Nhưng với các chức năng - có, nó là tùy chọn.


21
Sau đó, với tư cách là nhà thiết kế tiêu chuẩn, tôi sẽ không cho phép sử dụng extern với các hàm, vì nó chỉ thêm tiếng ồn vào ngữ pháp.
Elazar Leibovich

3
Khả năng tương thích ngược có thể là một nỗi đau.
MathuSum Mut

1
@ElazarLeibovich Trên thực tế, trong trường hợp cụ thể này, không cho phép đó là những gì sẽ thêm tiếng ồn cho ngữ pháp.
Các cuộc đua nhẹ nhàng trong quỹ đạo

1
Làm thế nào giới hạn từ khóa thêm tiếng ồn là ngoài tôi, nhưng tôi đoán đó là vấn đề của hương vị.
Elazar Leibovich

Tuy nhiên, thật hữu ích khi cho phép sử dụng "extern" cho các hàm, vì nó chỉ ra cho các lập trình viên khác rằng hàm được định nghĩa trong tệp khác, không phải trong tệp hiện tại và cũng không được khai báo trong một trong các tiêu đề đi kèm.
DimP

23

Bạn cần phân biệt giữa hai khái niệm riêng biệt: định nghĩa hàm và khai báo ký hiệu. "extern" là một công cụ sửa đổi liên kết, một gợi ý cho trình biên dịch về nơi biểu tượng được đề cập sau đó được xác định (gợi ý là "không phải ở đây").

Nếu tôi viết

extern int i;

trong phạm vi tệp (bên ngoài khối chức năng) trong tệp C, khi đó bạn đang nói "biến có thể được xác định ở nơi khác".

extern int f() {return 0;}

vừa là khai báo của hàm f vừa là định nghĩa của hàm f. Các định nghĩa trong trường hợp này vượt qua bên ngoài.

extern int f();
int f() {return 0;}

đầu tiên là một tuyên bố, tiếp theo là định nghĩa.

Sử dụng externlà sai nếu bạn muốn khai báo và xác định đồng thời một biến phạm vi tệp. Ví dụ,

extern int i = 4;

sẽ đưa ra một lỗi hoặc cảnh báo, tùy thuộc vào trình biên dịch.

Cách sử dụng extern là hữu ích nếu bạn rõ ràng muốn tránh định nghĩa của một biến.

Hãy để tôi giải thích:

Giả sử tập tin ac chứa:

#include "a.h"

int i = 2;

int f() { i++; return i;}

Các tập tin ah bao gồm:

extern int i;
int f(void);

và tập tin bc chứa:

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

int main(void){
    printf("%d\n", f());
    return 0;
}

Extern trong tiêu đề là hữu ích, bởi vì nó nói với trình biên dịch trong giai đoạn liên kết, "đây là một khai báo, và không phải là một định nghĩa". Nếu tôi loại bỏ dòng trong ac xác định i, phân bổ không gian cho nó và gán giá trị cho nó, chương trình sẽ không biên dịch được với tham chiếu không xác định. Điều này cho nhà phát triển biết rằng anh ta đã đề cập đến một biến, nhưng chưa xác định nó. Mặt khác, nếu tôi bỏ qua từ khóa "extern" và xóaint i = 2 dòng, chương trình vẫn biên dịch - tôi sẽ được xác định với giá trị mặc định là 0.

Các biến phạm vi tệp được định nghĩa ngầm định với giá trị mặc định là 0 hoặc NULL nếu bạn không gán rõ ràng một giá trị cho chúng - không giống như các biến phạm vi khối mà bạn khai báo ở đầu hàm. Từ khóa extern tránh định nghĩa ngầm này, và do đó giúp tránh sai lầm.

Đối với các hàm, trong khai báo hàm, từ khóa thực sự là dư thừa. Khai báo hàm không có định nghĩa ngầm.


Ý của bạn là xóa int i = 2dòng trong đoạn -3? Và nó có đúng không, khi thấy int i;, trình biên dịch sẽ phân bổ bộ nhớ cho biến đó, nhưng nhìn thấy extern int i;, trình biên dịch sẽ KHÔNG phân bổ bộ nhớ mà tìm kiếm biến ở nơi khác?
Ngọn lửa băng giá

Trên thực tế nếu bạn bỏ qua từ khóa "extern", chương trình sẽ không biên dịch vì định nghĩa lại i trong ac và bc (do ah).
Nixt

15

Các externtừ khóa có các hình thức khác nhau tùy thuộc vào môi trường. Nếu một tuyên bố có sẵn, externtừ khóa sẽ lấy liên kết như được chỉ định trước đó trong đơn vị dịch thuật. Trong trường hợp không có bất kỳ tuyên bố như vậy, externchỉ định liên kết bên ngoài.

static int g();
extern int g(); /* g has internal linkage */

extern int j(); /* j has tentative external linkage */

extern int h();
static int h(); /* error */

Dưới đây là các đoạn có liên quan từ dự thảo C99 (n1256):

6.2.2 Liên kết của định danh

[...]

4 Đối với một mã định danh được khai báo với bên ngoài của trình xác định lớp lưu trữ trong phạm vi mà khai báo trước của mã định danh đó hiển thị, 23) nếu khai báo trước chỉ định liên kết bên trong hoặc bên ngoài, liên kết của mã định danh ở khai báo sau là giống nhau như các liên kết quy định tại khai báo trước. Nếu không có khai báo trước nào được hiển thị hoặc nếu khai báo trước chỉ định không có liên kết, thì định danh có liên kết bên ngoài.

5 Nếu khai báo một mã định danh cho một hàm không có bộ xác định lớp lưu trữ, thì mối liên kết của nó được xác định chính xác như thể nó được khai báo với bên ngoài của bộ xác định lớp lưu trữ. Nếu khai báo một mã định danh cho một đối tượng có phạm vi tệp và không có trình xác định lớp lưu trữ, thì liên kết của nó là bên ngoài.


Đó có phải là tiêu chuẩn hay bạn chỉ nói với tôi một hành vi của nhà soạn nhạc điển hình? Trong trường hợp của tiêu chuẩn, tôi sẽ vui mừng vì một liên kết đến tiêu chuẩn. Nhưng cảm ơn!
Elazar Leibovich

Đây là hành vi tiêu chuẩn. Dự thảo C99 có sẵn tại đây: < open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf >. Tiêu chuẩn thực tế không miễn phí mặc dù (dự thảo đủ tốt cho hầu hết các mục đích).
dirkgently

1
Tôi mới thử nó trong gcc và cả "extern int h (); static int h () {return 0;}" và "int h (); static int h () {return 0;}" đều được chấp nhận như nhau cảnh báo. Có phải C99 chỉ và không ANSI? Bạn có thể giới thiệu cho tôi phần chính xác trong dự thảo không, vì điều này dường như không đúng với gcc.
Elazar Leibovich

Kiểm tra lại. Tôi đã thử tương tự với gcc 4.0.1 và tôi gặp lỗi. Hãy thử trình biên dịch trực tuyến hoặc codepad.org của comeau nếu bạn không có quyền truy cập vào các trình biên dịch khác. Đọc tiêu chuẩn.
dirkgently

2
@dirkgently, Câu hỏi thực sự của tôi là có bất kỳ ảnh hưởng nào đến việc sử dụng exetrn với khai báo hàm không và nếu không có lý do tại sao có thể thêm extern vào khai báo hàm. Và câu trả lời là không, không có hiệu ứng, và đã từng có một hiệu ứng với trình biên dịch không chuẩn.
Elazar Leibovich

11

Hàm nội tuyến có các quy tắc đặc biệt về ý externnghĩa của nó. (Lưu ý rằng các hàm nội tuyến là một phần mở rộng C99 hoặc GNU; chúng không có trong bản gốc C.

Đối với các chức năng phi nội tuyến, externkhông cần thiết vì nó được bật theo mặc định.

Lưu ý rằng các quy tắc cho C ++ là khác nhau. Ví dụ, extern "C"là cần thiết trên khai báo C ++ của các hàm C mà bạn sẽ gọi từ C ++, và có các quy tắc khác nhau về inline.


Đây là câu trả lời duy nhất ở đây là cả hai đều đúng và thực sự trả lời câu hỏi.
robinjam

4

IOW, extern là dư thừa, và không làm gì cả.

Đó là lý do, 10 năm sau:

Xem cam kết ad6dad0 , cam kết b199d71 , cam kết 5545442 (29 tháng 4 năm 2019) của Denton Liu ( Denton-L) .
(Được hợp nhất bởi Junio ​​C Hamano - gitster- trong cam kết 4aeeef3 , ngày 13 tháng 5 năm 2019)

*.[ch]: xóa externkhỏi khai báo hàm bằng cách sử dụngspatch

Đã có một sự thúc đẩy để loại bỏ externkhỏi khai báo chức năng.

Xóa một số trường hợp " extern" để khai báo hàm bị bắt bởi Coccinelle.
Lưu ý rằng Coccinelle có một số khó khăn với các hàm xử lý có __attribute__hoặc varargs nên một số externkhai báo bị bỏ lại để được xử lý trong một bản vá trong tương lai.

Đây là bản vá Coccinelle được sử dụng:

  @@
    type T;
    identifier f;
    @@
    - extern
    T f(...);

và nó đã được chạy với:

  $ git ls-files \*.{c,h} |
    grep -v ^compat/ |
    xargs spatch --sp-file contrib/coccinelle/noextern.cocci --in-place

Điều này không phải lúc nào cũng đơn giản:

Xem cam kết 7027f50 (04 tháng 9 năm 2019) của Denton Liu ( Denton-L) .
(Được hợp nhất bởi Denton Liu - Denton-L- trong cam kết 7027f50 , ngày 5 tháng 9 năm 2019)

compat/*.[ch]: xóa externkhỏi khai báo hàm bằng cách sử dụng spatch

Trong 5545442 ( *.[ch]: xóa externkhỏi khai báo hàm bằng cách sử dụng spatch, 2019-04-29, Git v2.22.0-rc0), chúng tôi đã xóa externs khỏi khai báo hàm bằng cách sử dụng spatchnhưng chúng tôi cố tình loại trừ các tệp theo compat/vì một số được sao chép trực tiếp từ thượng nguồn và chúng tôi nên tránh khuấy chúng để việc hợp nhất các bản cập nhật trong tương lai sẽ đơn giản hơn.

Trong lần xác nhận cuối cùng, chúng tôi đã xác định các tệp được lấy từ thượng nguồn để chúng tôi có thể loại trừ chúng và chạy spatchtrên phần còn lại.

Đây là bản vá Coccinelle được sử dụng:

@@
type T;
identifier f;
@@
- extern
  T f(...);

và nó đã được chạy với:

$ git ls-files compat/\*\*.{c,h} |
    xargs spatch --sp-file contrib/coccinelle/noextern.cocci --in-place
$ git checkout -- \
    compat/regex/ \
    compat/inet_ntop.c \
    compat/inet_pton.c \
    compat/nedmalloc/ \
    compat/obstack.{c,h} \
    compat/poll/

Coccinelle có một số rắc rối khi xử lý __attribute__và varargs vì vậy chúng tôi đã chạy như sau để đảm bảo rằng không có thay đổi nào bị bỏ lại phía sau:

$ git ls-files compat/\*\*.{c,h} |
    xargs sed -i'' -e 's/^\(\s*\)extern \([^(]*([^*]\)/\1\2/'
$ git checkout -- \
    compat/regex/ \
    compat/inet_ntop.c \
    compat/inet_pton.c \
    compat/nedmalloc/ \
    compat/obstack.{c,h} \
    compat/poll/

Lưu ý rằng với Git 2.24 (Q4 2019), bất kỳ giả mạo nào externđều bị loại bỏ.

Xem cam kết 65904b8 (ngày 30 tháng 9 năm 2019) của Emily Shaffer ( nasamuffin) .
Giúp đỡ: Jeff King ( peff) .
Xem cam kết 8464f94 (ngày 21 tháng 9 năm 2019) của Denton Liu ( Denton-L) .
Được giúp đỡ: Jeff King (peff ) .
(Được hợp nhất bởi Junio ​​C Hamano - gitster- trong cam kết 59b19bc , ngày 07 tháng 10 năm 2019)

promisor-remote.h: thả externtừ khai báo hàm

Trong quá trình tạo tệp này, mỗi lần giới thiệu hàm mới được đưa vào, nó bao gồm một extern.
Tuy nhiên, bắt đầu từ 5545442 ( *.[ch]: xóa externkhỏi khai báo hàm bằng cách sử dụng spatch, 2019-04-29, Git v2.22.0-rc0), chúng tôi đã tích cực cố gắng ngăn không cho externs được sử dụng trong khai báo hàm vì chúng không cần thiết.

Loại bỏ những giả mạo này extern s .


3

Các extern thông báo từ khóa trình biên dịch rằng chức năng hoặc biến có liên kết bên ngoài - nói cách khác, nó có thể nhìn thấy từ các tập tin khác so với cái mà nó được xác định. Theo nghĩa này, nó có nghĩa ngược lại với statictừ khóa. Có một chút kỳ lạ khi đặt externtại thời điểm định nghĩa, vì không có tệp nào khác có thể nhìn thấy định nghĩa (hoặc nó sẽ dẫn đến nhiều định nghĩa). Thông thường, bạn đưa ra externmột tuyên bố tại một số điểm với khả năng hiển thị bên ngoài (chẳng hạn như tệp tiêu đề) và đặt định nghĩa ở nơi khác.


2

khai báo một hàm ngoài có nghĩa là định nghĩa của nó sẽ được giải quyết tại thời điểm liên kết, không phải trong quá trình biên dịch.

Không giống như các hàm thông thường, không được khai báo bên ngoài, nó có thể được định nghĩa trong bất kỳ tệp nguồn nào (nhưng không phải trong nhiều tệp nguồn nếu không bạn sẽ gặp lỗi liên kết nói rằng bạn đã đưa ra nhiều định nghĩa của hàm) bao gồm cả định nghĩa trong hàm mà nó được khai báo extern.So, trong trường hợp của bạn, trình liên kết giải quyết định nghĩa hàm trong cùng một tệp.

Tôi không nghĩ rằng làm điều này sẽ hữu ích nhiều tuy nhiên thực hiện các loại thử nghiệm như vậy mang lại cái nhìn sâu sắc hơn về cách trình biên dịch và trình liên kết của ngôn ngữ hoạt động.


2
IOW, extern là dư thừa, và không làm gì cả. Sẽ rõ ràng hơn nhiều nếu bạn đặt nó theo cách đó.
Elazar Leibovich

@ElazarLeibovich Tôi vừa gặp một trường hợp tương tự trong cơ sở mã của chúng tôi và có kết luận tương tự. Tất cả những câu trả lời ở đây có thể được tóm tắt trong một lớp lót của bạn. Nó không có tác dụng thực tế nhưng có thể tốt cho khả năng đọc. Rất vui được gặp bạn trực tuyến và không chỉ trong các cuộc gặp gỡ :)
Aviv

1

Lý do nó không có hiệu lực là vì tại thời điểm liên kết, trình liên kết cố gắng giải quyết định nghĩa bên ngoài (trong trường hợp của bạn extern int f()). Không thành vấn đề nếu nó tìm thấy nó trong cùng một tệp hoặc một tệp khác, miễn là nó được tìm thấy.

Hy vọng điều này trả lời câu hỏi của bạn.


1
Vậy thì tại sao lại cho phép thêm externvào bất kỳ chức năng nào?
Elazar Leibovich

2
Xin vui lòng không đặt thư rác không liên quan trong bài viết của bạn. Cảm ơn!
Mac
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.