Vị trí khai báo biến trong C


129

Tôi từ lâu đã nghĩ rằng trong C, tất cả các biến phải được khai báo ở đầu hàm. Tôi biết rằng trong C99, các quy tắc giống như trong C ++, nhưng các quy tắc vị trí khai báo biến cho C89 / ANSI C là gì?

Đoạn mã sau biên dịch thành công với gcc -std=c89gcc -ansi:

#include <stdio.h>
int main() {
    int i;
    for (i = 0; i < 10; i++) {
        char c = (i % 95) + 32;
        printf("%i: %c\n", i, c);
        char *s;
        s = "some string";
        puts(s);
    }
    return 0;
}

Không nên khai báo csgây ra lỗi trong chế độ C89 / ANSI?


54
Chỉ cần lưu ý: các biến trong ansi C không phải khai báo khi bắt đầu hàm mà thay vào đó là bắt đầu một khối. Vì vậy, char c = ... ở đầu vòng lặp for của bạn là hoàn toàn hợp pháp trong ansi C. Tuy nhiên, char * s sẽ không như vậy.
Jason Coco

Câu trả lời:


149

Nó biên dịch thành công vì GCC cho phép khai báo sdưới dạng phần mở rộng GNU, mặc dù nó không phải là một phần của tiêu chuẩn C89 hoặc ANSI. Nếu bạn muốn tuân thủ nghiêm ngặt các tiêu chuẩn đó, bạn phải vượt qua -pedanticcờ.

Tuyên bố ckhi bắt đầu một { }khối là một phần của tiêu chuẩn C89; khối không phải là một chức năng.


41
Có lẽ đáng lưu ý rằng chỉ có tuyên bố slà một phần mở rộng (từ quan điểm C89). Tuyên bố clà hoàn toàn hợp pháp trong C89, không cần gia hạn.
AnT

7
@AndreyT: Vâng, trong C, các khai báo biến phải là @ phần đầu của một khối và không phải là hàm mỗi se; nhưng mọi người nhầm lẫn khối với chức năng vì đây là ví dụ chính của khối.
huyền thoại2k

1
Tôi chuyển bình luận với +39 phiếu vào câu trả lời.
MarcH

78

Đối với C89, bạn phải khai báo tất cả các biến của mình khi bắt đầu khối phạm vi .

Vì vậy, char ckhai báo của bạn là hợp lệ vì nó nằm ở đầu khối phạm vi vòng lặp for. Nhưng, char *stuyên bố nên là một lỗi.


2
Hoàn toàn chính xác. Bạn có thể khai báo các biến ở đầu bất kỳ {...}.
Artelius

5
@Artelius Không hoàn toàn chính xác. Chỉ khi các curlies là một phần của một khối (không phải nếu chúng là một phần của tuyên bố cấu trúc hoặc kết hợp hoặc bộ khởi tạo giằng.)
Jens

Chỉ cần là phạm vi, tuyên bố sai lầm ít nhất phải được thông báo theo tiêu chuẩn C. Vì vậy, nó phải là một lỗi hoặc một cảnh báo trong gcc. Đó là, đừng tin rằng một chương trình có thể được biên dịch có nghĩa là nó tuân thủ.
jinawee

35

Nhóm các khai báo biến ở đầu khối là một di sản có khả năng do các hạn chế của trình biên dịch C cũ, nguyên thủy. Tất cả các ngôn ngữ hiện đại đều khuyến nghị và đôi khi thậm chí thực thi khai báo các biến cục bộ tại điểm mới nhất: nơi chúng được khởi tạo lần đầu tiên. Bởi vì điều này thoát khỏi rủi ro sử dụng một giá trị ngẫu nhiên do nhầm lẫn. Tách khai báo và khởi tạo cũng ngăn bạn sử dụng "const" (hoặc "cuối cùng") khi bạn có thể.

C ++ không may tiếp tục chấp nhận cách khai báo cũ, hàng đầu để tương thích ngược với C (một khả năng tương thích C kéo ra khỏi nhiều người khác ...) Nhưng C ++ cố gắng tránh xa nó:

  • Thiết kế của các tham chiếu C ++ thậm chí không cho phép nhóm đầu khối như vậy.
  • Nếu bạn tách riêng khai báo và khởi tạo một đối tượng cục bộ C ++ thì bạn phải trả chi phí cho một hàm tạo bổ sung. Nếu hàm tạo không có đối số không tồn tại thì một lần nữa bạn thậm chí không được phép tách cả hai!

C99 bắt đầu di chuyển C theo cùng hướng này.

Nếu bạn lo lắng về việc không tìm thấy nơi các biến cục bộ được khai báo thì điều đó có nghĩa là bạn có một vấn đề lớn hơn nhiều: khối kèm theo quá dài và nên được chia.

https://wiki.sei.cmu.edu/confluence/display/c/DCL19-C.+Minizes+the+scope+of+variables+and+fifts



Xem thêm cách buộc khai báo biến ở đầu khối có thể tạo lỗ hổng bảo mật: lwn.net/Articles/443037
MarcH

"C ++ không may tiếp tục chấp nhận cách khai báo cũ, hàng đầu để tương thích ngược với C": IMHO, đó chỉ là cách sạch để thực hiện. Ngôn ngữ khác "giải quyết" vấn đề này bằng cách luôn khởi tạo bằng 0. Bzzt, chỉ che giấu các lỗi logic nếu bạn hỏi tôi. Và có khá nhiều trường hợp bạn CẦN khai báo mà không khởi tạo vì có nhiều vị trí có thể khởi tạo. Và đó là lý do tại sao RAII của C ++ thực sự là một nỗi đau rất lớn ở mông - Bây giờ bạn cần bao gồm trạng thái chưa được xác thực "hợp lệ" trong mỗi đối tượng để cho phép những trường hợp này.
Jo So

1
@JoSo: Tôi bối rối tại sao bạn nghĩ rằng việc đọc các biến chưa được khởi tạo mang lại hiệu ứng tùy ý sẽ làm cho các lỗi lập trình dễ phát hiện hơn so với việc chúng mang lại giá trị nhất quán hoặc lỗi xác định? Lưu ý rằng không có gì đảm bảo rằng việc đọc lưu trữ không xác định sẽ hoạt động theo kiểu phù hợp với bất kỳ mô hình bit nào mà biến có thể có, thậm chí một chương trình như vậy sẽ hành xử theo kiểu thời gian và nguyên nhân thông thường. Đưa ra một cái gì đó như int y; ... if (x) { printf("X was true"); y=23;} return y;...
supercat

1
@JoSo: Đối với các con trỏ, đặc biệt là các triển khai bẫy hoạt động trên null, all-bit-zero thường là một giá trị bẫy hữu ích. Hơn nữa, trong các ngôn ngữ chỉ định rõ ràng các biến mặc định cho all-bit-zero, việc phụ thuộc vào giá trị đó không phải là lỗi . Trình biên dịch chưa có xu hướng trở nên quá kỳ quặc với "tối ưu hóa" của chúng, nhưng các nhà văn trình biên dịch tiếp tục cố gắng để ngày càng thông minh hơn. Một tùy chọn trình biên dịch để khởi tạo các biến với các biến giả ngẫu nhiên có chủ ý có thể hữu ích để xác định lỗi, nhưng chỉ để lại lưu trữ giữ giá trị cuối cùng của nó đôi khi có thể che dấu lỗi.
supercat

22

Từ một khả năng duy trì, thay vì cú pháp, quan điểm, có ít nhất ba chuyến tàu tư tưởng:

  1. Khai báo tất cả các biến ở đầu hàm để chúng ở một nơi và bạn sẽ có thể xem danh sách toàn diện trong nháy mắt.

  2. Khai báo tất cả các biến càng gần càng tốt với địa điểm chúng được sử dụng lần đầu tiên, vì vậy bạn sẽ biết tại sao mỗi biến cần thiết.

  3. Khai báo tất cả các biến ở đầu khối phạm vi trong cùng, vì vậy chúng sẽ ra khỏi phạm vi càng sớm càng tốt và cho phép trình biên dịch tối ưu hóa bộ nhớ và cho bạn biết nếu bạn vô tình sử dụng chúng ở nơi bạn dự định.

Tôi thường thích tùy chọn đầu tiên, vì tôi thấy những người khác thường buộc tôi phải tìm kiếm mã cho các khai báo. Xác định tất cả các biến lên phía trước cũng giúp dễ dàng khởi tạo và xem chúng từ trình gỡ lỗi.

Đôi khi tôi sẽ khai báo các biến trong một khối phạm vi nhỏ hơn, nhưng chỉ vì một Lý do chính đáng, trong đó tôi có rất ít. Một ví dụ có thể là sau a fork(), để khai báo các biến chỉ cần cho quá trình con. Đối với tôi, chỉ số trực quan này là một lời nhắc nhở hữu ích về mục đích của họ.


27
Tôi sử dụng tùy chọn 2 hoặc 3 để dễ dàng tìm thấy các biến hơn - bởi vì các hàm không nên lớn đến mức bạn không thể thấy các khai báo biến.
Jonathan Leffler

8
Tùy chọn 3 không phải là vấn đề, trừ khi bạn sử dụng trình biên dịch từ những năm 70.
edgar.holleis

15
Nếu bạn đã sử dụng một IDE tốt, bạn sẽ không cần phải đi tìm mã, bởi vì cần có một lệnh IDE để tìm khai báo cho bạn. (F3 trong Eclipse)
edgar.holleis

4
Tôi không hiểu làm thế nào bạn có thể đảm bảo khởi tạo trong tùy chọn 1, có thể đôi khi bạn chỉ có thể nhận được giá trị ban đầu trong khối, bằng cách gọi một hàm khác hoặc thực hiện phép tính toán, có thể.
Plumator

4
@Plumenator: tùy chọn 1 không đảm bảo khởi tạo; Tôi đã chọn khởi tạo chúng khi khai báo, theo giá trị "chính xác" của chúng hoặc theo thứ gì đó sẽ đảm bảo mã tiếp theo sẽ bị hỏng nếu chúng không được đặt một cách thích hợp. Tôi nói "đã chọn" vì sở thích của tôi đã thay đổi thành # 2 kể từ khi tôi viết bài này, có lẽ vì hiện tại tôi đang sử dụng Java nhiều hơn C và vì tôi có các công cụ phát triển tốt hơn.
Adam Liss

6

Như những người khác lưu ý, GCC được cho phép trong vấn đề này (và có thể cả các trình biên dịch khác, tùy thuộc vào các đối số mà họ được gọi) ngay cả khi ở chế độ 'C89', trừ khi bạn sử dụng kiểm tra 'pedantic'. Thành thật mà nói, không có nhiều lý do chính đáng để không có phạm vi; Mã hiện đại chất lượng phải luôn được biên dịch mà không có cảnh báo (hoặc rất ít nơi bạn biết bạn đang làm điều gì đó đáng ngờ đối với trình biên dịch là một lỗi có thể xảy ra), vì vậy nếu bạn không thể biên dịch mã của mình bằng một thiết lập mô phạm thì có lẽ cần phải chú ý.

C89 yêu cầu các biến được khai báo trước bất kỳ câu lệnh nào khác trong mỗi phạm vi, các tiêu chuẩn sau này cho phép khai báo gần hơn để sử dụng (có thể vừa trực quan hơn và hiệu quả hơn), đặc biệt là khai báo đồng thời và khởi tạo biến điều khiển vòng lặp trong các vòng lặp 'cho'.


0

Như đã lưu ý, có hai trường phái suy nghĩ về điều này.

1) Khai báo mọi thứ ở đầu các chức năng vì năm là 1987.

2) Khai báo gần nhất với lần sử dụng đầu tiên và trong phạm vi nhỏ nhất có thể.

Câu trả lời của tôi cho điều này là LÀM CẢ! Hãy để tôi giải thích:

Đối với các hàm dài, 1) làm cho việc tái cấu trúc rất khó khăn. Nếu bạn làm việc trong một cơ sở mã nơi các nhà phát triển chống lại ý tưởng về chương trình con, thì bạn sẽ có 50 khai báo biến khi bắt đầu hàm và một số trong số chúng có thể chỉ là "i" cho một vòng lặp for đáy của hàm.

Do đó, tôi đã phát triển khai báo PTSD từ đầu và cố gắng thực hiện tùy chọn 2) một cách tôn giáo.

Tôi quay lại để lựa chọn một vì một điều: các chức năng ngắn. Nếu các hàm của bạn đủ ngắn, thì bạn sẽ có một vài biến cục bộ và vì hàm này ngắn, nếu bạn đặt chúng ở đầu hàm, chúng sẽ vẫn gần với lần sử dụng đầu tiên.

Ngoài ra, mô hình chống "khai báo và đặt thành NULL" khi bạn muốn khai báo ở trên cùng nhưng bạn chưa thực hiện một số tính toán cần thiết cho việc khởi tạo vì những điều bạn cần khởi tạo sẽ có thể được nhận làm đối số.

Vì vậy, bây giờ suy nghĩ của tôi là bạn nên khai báo ở đầu các chức năng và càng gần càng tốt để sử dụng lần đầu tiên. Cả hai! Và cách để làm điều đó là với các chương trình con được phân chia tốt.

Nhưng nếu bạn đang làm việc trên một hàm dài, thì hãy đặt những thứ gần nhất với lần sử dụng đầu tiên bởi vì cách đó sẽ dễ dàng hơn để trích xuất các phương thức.

Công thức của tôi là thế này Đối với tất cả các biến cục bộ, lấy biến đó và di chuyển khai báo xuống dưới cùng, biên dịch, sau đó di chuyển khai báo sang ngay trước lỗi biên dịch. Đó là lần đầu tiên sử dụng. Làm điều này cho tất cả các biến địa phương.

int foo = 0;
<code that uses foo>

int bar = 1;
<code that uses bar>

<code that uses foo>

Bây giờ, xác định một khối phạm vi bắt đầu trước khi khai báo và di chuyển kết thúc cho đến khi chương trình biên dịch

{
    int foo = 0;
    <code that uses foo>
}

int bar = 1;
<code that uses bar>

>>> First compilation error here
<code that uses foo>

Điều này không biên dịch vì có thêm một số mã sử dụng foo. Chúng ta có thể nhận thấy rằng trình biên dịch đã có thể đi qua mã sử dụng thanh vì nó không sử dụng foo. Tại thời điểm này, có hai sự lựa chọn. Cơ chế là chỉ cần di chuyển "}" xuống dưới cho đến khi nó biên dịch, và lựa chọn khác là kiểm tra mã và xác định xem thứ tự có thể được thay đổi thành:

{
    int foo = 0;
    <code that uses foo>
}

<code that uses foo>

int bar = 1;
<code that uses bar>

Nếu thứ tự có thể được chuyển đổi, đó có thể là những gì bạn muốn bởi vì nó rút ngắn tuổi thọ của các giá trị tạm thời.

Một điều cần lưu ý, giá trị của foo cần được bảo tồn giữa các khối mã sử dụng nó, hoặc có thể chỉ là một foo khác nhau trong cả hai. Ví dụ

int i;

for(i = 0; i < 8; ++i){
    ...
}

<some stuff>

for(i = 3; i < 32; ++i){
    ...
}

Những tình huống này cần nhiều hơn thủ tục của tôi. Nhà phát triển sẽ phải phân tích mã để xác định những việc cần làm.

Nhưng bước đầu tiên là tìm kiếm sử dụng đầu tiên. Bạn có thể thực hiện nó một cách trực quan nhưng đôi khi, việc xóa khai báo sẽ dễ dàng hơn, cố gắng biên dịch và chỉ đưa nó trở lại trên lần sử dụng đầu tiên. Nếu lần sử dụng đầu tiên đó nằm trong câu lệnh if, hãy đặt nó ở đó và kiểm tra xem nó có biên dịch không. Trình biên dịch sau đó sẽ xác định các sử dụng khác. Cố gắng tạo một khối phạm vi bao gồm cả hai cách sử dụng.

Sau khi phần cơ học này được thực hiện, nó sẽ trở nên dễ dàng hơn để phân tích dữ liệu ở đâu. Nếu một biến được sử dụng trong một khối phạm vi lớn, hãy phân tích tình huống và xem liệu bạn có đang sử dụng cùng một biến cho hai thứ khác nhau hay không (như "i" được sử dụng cho hai vòng lặp). Nếu việc sử dụng không liên quan, hãy tạo các biến mới cho mỗi lần sử dụng không liên quan này.


0

Bạn nên khai báo tất cả các biến ở đầu hoặc "cục bộ" trong hàm. Câu trả lời là:

Nó phụ thuộc vào loại hệ thống bạn đang sử dụng:

1 / Hệ thống nhúng (đặc biệt liên quan đến các cuộc sống như Máy bay hoặc Xe hơi): Nó cho phép bạn sử dụng bộ nhớ động (ví dụ: calloc, malloc, new ...). Hãy tưởng tượng bạn đang làm việc trong một dự án rất lớn, với 1000 kỹ sư. Điều gì sẽ xảy ra nếu họ phân bổ bộ nhớ động mới và quên xóa nó (khi nó không sử dụng nữa)? Nếu hệ thống nhúng chạy trong một thời gian dài, nó sẽ dẫn đến chồng tràn và phần mềm sẽ bị hỏng. Không dễ để đảm bảo chất lượng (cách tốt nhất là cấm bộ nhớ động).

Nếu một Máy bay chạy trong 30 ngày và không tắt, điều gì xảy ra nếu phần mềm bị hỏng (khi máy bay vẫn ở trên không)?

2 / Các hệ thống khác như web, PC (có không gian bộ nhớ lớn):

Bạn nên khai báo biến "cục bộ" để tối ưu hóa bộ nhớ bằng cách sử dụng. Nếu các hệ thống này chạy trong một thời gian dài và xảy ra tràn ngăn xếp (vì ai đó quên xóa bộ nhớ động). Chỉ cần làm điều đơn giản để thiết lập lại PC: P Nó không ảnh hưởng đến cuộc sống


Tôi không chắc điều này là chính xác. Tôi đoán bạn đang nói rằng việc kiểm tra rò rỉ bộ nhớ sẽ dễ dàng hơn nếu bạn khai báo tất cả các biến cục bộ của mình ở một nơi? Điều đó thể đúng, nhưng tôi không chắc là tôi mua nó. Đối với điểm (2), bạn nói khai báo biến cục bộ sẽ "tối ưu hóa việc sử dụng bộ nhớ"? Đây là lý thuyết có thể. Trình biên dịch có thể chọn thay đổi kích thước khung ngăn xếp trong suốt quá trình của hàm để giảm thiểu việc sử dụng bộ nhớ, nhưng tôi không biết bất kỳ điều gì làm điều này. Trong thực tế, trình biên dịch sẽ chỉ chuyển đổi tất cả các khai báo "cục bộ" thành "hàm bắt đầu đằng sau hậu trường".
QuinnFreedman

1 / Hệ thống nhúng đôi khi không cho phép bộ nhớ động, vì vậy nếu bạn khai báo tất cả các biến ở trên cùng của chức năng. Khi mã nguồn được xây dựng, nó có thể tính toán số byte họ cần trong ngăn xếp để chạy chương trình. Nhưng với bộ nhớ động, trình biên dịch không thể làm như vậy.
Dang_Ho

2 / Nếu bạn khai báo một biến cục bộ, biến đó chỉ tồn tại trong dấu ngoặc đóng / mở "{}". Vì vậy, trình biên dịch có thể giải phóng không gian của biến nếu biến đó "nằm ngoài phạm vi". Điều đó có thể tốt hơn so với khai báo mọi thứ ở đầu chức năng.
Dang_Ho

Tôi nghĩ rằng bạn đang nhầm lẫn về bộ nhớ tĩnh và động. Bộ nhớ tĩnh được phân bổ trên ngăn xếp. Tất cả các biến được khai báo trong một hàm, bất kể chúng được khai báo ở đâu, đều được phân bổ tĩnh. Bộ nhớ động được phân bổ trên heap với một cái gì đó như malloc(). Mặc dù tôi chưa bao giờ thấy một thiết bị không có khả năng của thiết bị, nhưng cách tốt nhất là tránh phân bổ động trên các hệ thống nhúng ( xem tại đây ). Nhưng điều đó không liên quan gì đến việc bạn khai báo các biến của mình trong một hàm.
QuinnFreedman

1
Mặc dù tôi đồng ý rằng đây sẽ là một cách hợp lý để vận hành, nhưng đó không phải là điều xảy ra trong thực tế. Đây là hội nghị thực tế cho một cái gì đó rất giống ví dụ của bạn: godbolt.org/z/mLhE9a . Như bạn có thể thấy, trên dòng 11, sub rsp, 1008đang phân bổ không gian cho toàn bộ mảng bên ngoài câu lệnh if. Điều này đúng cho clanggccở mọi phiên bản và mức độ tối ưu hóa tôi đã thử.
QuinnFreedman

-1

Tôi sẽ trích dẫn một số tuyên bố từ hướng dẫn sử dụng cho phiên bản gcc 4.7.0 để được giải thích rõ ràng.

"Trình biên dịch có thể chấp nhận một số tiêu chuẩn cơ bản, chẳng hạn như 'c90' hoặc 'c ++ 98' và phương ngữ GNU của các tiêu chuẩn đó, chẳng hạn như 'gnu90' hoặc 'gnu ++ 98'. Bằng cách chỉ định một tiêu chuẩn cơ sở, trình biên dịch sẽ chấp nhận tất cả các chương trình tuân theo tiêu chuẩn đó và các chương trình sử dụng các phần mở rộng GNU không mâu thuẫn với nó. Ví dụ: '-std = c90' sẽ tắt một số tính năng nhất định của GCC không tương thích với ISO C90, chẳng hạn như từ khóa asm và typeof, nhưng không các phần mở rộng GNU khác không có ý nghĩa trong ISO C90, chẳng hạn như bỏ qua thuật ngữ giữa của biểu thức ?: "

Tôi nghĩ rằng điểm chính của câu hỏi của bạn là tại sao gcc không phù hợp với C89 ngay cả khi tùy chọn "-std = c89" được sử dụng. Tôi không biết phiên bản gcc của bạn, nhưng tôi nghĩ rằng sẽ không có sự khác biệt lớn. Nhà phát triển của gcc đã nói với chúng tôi rằng tùy chọn "-std = c89" chỉ có nghĩa là các tiện ích mở rộng mâu thuẫn với C89 bị tắt. Vì vậy, nó không liên quan gì đến một số tiện ích mở rộng không có ý nghĩa trong C89. Và tiện ích mở rộng không hạn chế vị trí khai báo biến thuộc về các tiện ích mở rộng không mâu thuẫn với C89.

Thành thật mà nói, mọi người sẽ nghĩ rằng nó nên tuân thủ C89 hoàn toàn ngay từ cái nhìn đầu tiên của tùy chọn "-std = c89". Nhưng nó không. Đối với vấn đề khai báo tất cả các biến ở đầu là tốt hơn hay tồi tệ hơn chỉ là vấn đề của thói quen.


tuân thủ không có nghĩa là không chấp nhận các phần mở rộng: miễn là trình biên dịch biên dịch các chương trình hợp lệ và tạo ra bất kỳ chẩn đoán cần thiết nào cho các phần mở rộng khác, nó tuân thủ.
Hãy nhớ đến

@Marc Lehmann, vâng, bạn đã đúng khi từ "tuân thủ" được sử dụng để phân biệt trình biên dịch. Nhưng khi từ "tuân thủ" được sử dụng để mô tả một số cách sử dụng, bạn có thể nói "Việc sử dụng không tuân thủ tiêu chuẩn". Và tất cả những người mới bắt đầu đều có ý kiến ​​rằng việc sử dụng không tuân thủ tiêu chuẩn sẽ gây ra lỗi.
Junwanghe

@Marc Lehmann, nhân tiện, không có chẩn đoán khi gcc thấy việc sử dụng không phù hợp với tiêu chuẩn C89.
Junwanghe

Câu trả lời của bạn vẫn sai, bởi vì tuyên bố "gcc không tuân thủ" không giống với "một số chương trình người dùng không tuân thủ". Việc sử dụng tuân thủ của bạn chỉ đơn giản là không chính xác. Ngoài ra, khi tôi là người mới bắt đầu, tôi không có ý kiến ​​gì về tình trạng của bạn, vì vậy điều đó cũng sai. Cuối cùng, không có yêu cầu đối với trình biên dịch tuân thủ để chẩn đoán mã không tuân thủ và trên thực tế, điều này là không thể thực hiện được.
Hãy nhớ đến
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.