Trường hợp bạn có thể và không thể khai báo các biến mới trong C?


77

Tôi đã nghe (có thể là từ một giáo viên) rằng người ta nên khai báo tất cả các biến ở đầu chương trình / hàm và việc khai báo các biến mới trong số các câu lệnh có thể gây ra vấn đề.

Nhưng sau đó tôi đang đọc K&R và tôi bắt gặp câu này: "Các khai báo của các biến (bao gồm cả khởi tạo) có thể theo sau dấu ngoặc nhọn bên trái giới thiệu bất kỳ câu lệnh ghép nào, không chỉ câu lệnh bắt đầu một hàm". Anh ấy làm theo một ví dụ:

Tôi đã chơi một chút với khái niệm này, và nó hoạt động ngay cả với các mảng. Ví dụ:

Vậy chính xác khi nào tôi không được phép khai báo biến? Ví dụ, nếu khai báo biến của tôi không nằm ngay sau dấu ngoặc nhọn mở thì sao? Như đây:

Điều này có thể gây ra sự cố tùy thuộc vào chương trình / máy không?


5
gcclà khá lỏng lẻo. Bạn đang sử dụng mảng và khai báo có độ dài thay đổi c99. Biên dịch với gcc -std=c89 -pedanticvà bạn sẽ bị la. Tuy nhiên, theo c99, tất cả đó là kosher.
Dave

6
Vấn đề là bạn đang đọc K&R, đã lỗi thời.
Lundin

1
@Lundin Có sự thay thế thích hợp cho K&R không ?? Không có gì sau ấn bản ANSI C và người đọc cuốn sách này có thể đọc rõ ràng nó đề cập đến tiêu chuẩn nào
Brandin

Câu trả lời:


126

Tôi cũng thường nghe nói rằng đặt các biến ở đầu hàm là cách tốt nhất để làm mọi thứ, nhưng tôi rất không đồng ý. Tôi thích giới hạn các biến trong phạm vi nhỏ nhất có thể để chúng ít có cơ hội bị sử dụng sai hơn và do đó, tôi có ít thứ lấp đầy không gian tinh thần của mình trong mỗi dòng trên chương trình.

Trong khi tất cả các phiên bản của C đều cho phép phạm vi khối từ vựng, nơi bạn có thể khai báo các biến phụ thuộc vào phiên bản của tiêu chuẩn C mà bạn đang nhắm mục tiêu:

C99 trở đi hoặc C ++

Các trình biên dịch C hiện đại như gcc và clang hỗ trợ các tiêu chuẩn C99C11 , cho phép bạn khai báo một biến ở bất kỳ đâu mà một câu lệnh có thể sử dụng. Phạm vi của biến bắt đầu từ điểm khai báo đến cuối khối (dấu ngoặc nhọn đóng tiếp theo).

Bạn cũng có thể khai báo các biến bên trong cho bộ khởi tạo vòng lặp. Biến sẽ chỉ tồn tại bên trong vòng lặp.

ANSI C (C90)

Nếu bạn đang nhắm mục tiêu tiêu chuẩn ANSI C cũ hơn , thì bạn bị hạn chế khai báo các biến ngay sau dấu ngoặc nhọn 1 .

Mặc dù vậy, điều này không có nghĩa là bạn phải khai báo tất cả các biến ở đầu các hàm. Trong C, bạn có thể đặt một khối được phân cách bằng dấu ngoặc nhọn ở bất kỳ đâu mà một câu lệnh có thể đi đến (không chỉ sau những thứ như ifhoặc for) và bạn có thể sử dụng khối này để giới thiệu phạm vi biến mới. Sau đây là phiên bản ANSI C của các ví dụ C99 trước đó:


1 Lưu ý rằng nếu bạn đang sử dụng gcc, bạn cần chuyển --pedanticcờ để nó thực sự thực thi tiêu chuẩn C90 và phàn nàn rằng các biến được khai báo không đúng chỗ. Nếu bạn chỉ sử dụng -std=c90nó, gcc sẽ chấp nhận một tập hợp siêu C90, điều này cũng cho phép khai báo biến C99 linh hoạt hơn.


1
"Phạm vi của biến bắt đầu từ điểm khai báo đến cuối khối" - điều này, trong trường hợp có ai thắc mắc, không có nghĩa là việc tạo một khối hẹp hơn theo cách thủ công là hữu ích / cần thiết để làm cho trình biên dịch sử dụng không gian ngăn xếp một cách hiệu quả. Tôi đã thấy điều này một vài lần, và đó là một suy luận sai từ điệp khúc sai rằng C là 'trình lắp ráp di động'. Bởi vì (A) biến có thể được cấp phát trong một thanh ghi, không phải trên ngăn xếp, & (B) nếu một biến nằm trên ngăn xếp nhưng trình biên dịch có thể thấy rằng bạn ngừng sử dụng nó, ví dụ: 10% chặng đường thông qua một khối, nó có thể dễ dàng tái chế không gian đó cho một thứ khác.
underscore_d

3
@underscore_d Hãy nhớ rằng những người muốn tiết kiệm bộ nhớ thường làm việc với các hệ thống nhúng, trong đó người ta buộc phải tuân theo mức tối ưu hóa thấp hơn và / hoặc các phiên bản trình biên dịch cũ hơn do các khía cạnh chứng nhận và / hoặc chuỗi công cụ.
lớp stacker

chỉ vì bạn khai báo một biến ở giữa phạm vi không làm cho phạm vi của nó ngắn hơn. nó chỉ làm cho việc xem biến nào nằm trong phạm vi và biến nào không thuộc phạm vi khó hơn. Điều làm cho phạm vi ngắn hơn là tạo phạm vi ẩn danh, không khai báo ở giữa phạm vi (đó chỉ là một cách hack có hiệu quả di chuyển khai báo lên đầu và giữ nguyên chỉ định, chỉ khiến cho việc suy luận về môi trường của phạm vi, có hiệu quả là đẳng cấu để có một cấu trúc ẩn danh trong mỗi phạm vi là tích của tất cả các biến được khai báo).
Dmitry

2
Tôi không biết bạn lấy ý tưởng từ đâu rằng việc khai báo các biến ở giữa một phạm vi chỉ là một cách "hack có hiệu quả di chuyển khai báo lên đầu". Điều này không đúng và nếu bạn cố gắng sử dụng một biến trong một dòng và khai báo nó ở dòng tiếp theo, bạn sẽ nhận được lỗi biên dịch "biến không được khai báo".
ômomg

3

Thiếu không bao gồm những gì ANSI C cho phép, nhưng anh ấy không giải thích lý do tại sao giáo viên của bạn yêu cầu bạn khai báo các biến của bạn ở đầu các hàm của bạn. Khai báo các biến ở những vị trí kỳ lạ có thể khiến mã của bạn khó đọc hơn và điều đó có thể gây ra lỗi.

Lấy đoạn mã sau làm ví dụ.

Như bạn có thể thấy, tôi đã khai báo ihai lần. Nói chính xác hơn, tôi đã khai báo hai biến, cả hai đều có tên i. Bạn có thể nghĩ rằng điều này sẽ gây ra lỗi, nhưng không phải vậy, vì hai ibiến nằm trong các phạm vi khác nhau. Bạn có thể thấy điều này rõ ràng hơn khi bạn nhìn vào đầu ra của hàm này.

Đầu tiên, chúng tôi gán 20 và 30 cho ijtương ứng. Sau đó, bên trong dấu ngoặc nhọn, chúng ta gán 88 và 99. Vì vậy, tại sao sau đó jgiá trị của nó vẫn giữ nguyên, nhưng ilại quay trở lại là 20? Đó là do hai ibiến số khác nhau .

Giữa tập hợp bên trong của dấu ngoặc nhọn, ibiến có giá trị 20 bị ẩn và không thể truy cập được, nhưng vì chúng tôi chưa khai báo mới jnên chúng tôi vẫn đang sử dụng jtừ phạm vi bên ngoài. Khi chúng ta rời khỏi tập hợp các dấu ngoặc nhọn bên trong, việc igiữ giá trị 88 sẽ biến mất và chúng ta lại có quyền truy cập vào ivới giá trị 20.

Đôi khi hành vi này là một điều tốt, những lần khác, có thể không, nhưng cần rõ ràng rằng nếu bạn sử dụng tính năng này của C một cách bừa bãi, bạn thực sự có thể làm cho mã của bạn trở nên khó hiểu và khó hiểu.


30
Bạn đã làm cho mã của mình khó đọc vì bạn đã sử dụng cùng một tên cho hai biến chứ không phải vì bạn đã khai báo các biến không phải ở đầu hàm. Đó là hai vấn đề khác nhau. Tôi hoàn toàn không đồng ý với tuyên bố rằng việc khai báo các biến ở những nơi khác làm cho mã của bạn khó đọc, tôi nghĩ điều ngược lại là đúng. Khi viết mã, nếu bạn khai báo biến gần khi nó được sử dụng, tuân theo nguyên tắc cục bộ thời gian và không gian, khi đọc, bạn sẽ xác định được nó làm gì, tại sao ở đó và nó được sử dụng như thế nào rất dễ dàng.
Havok

3
Theo nguyên tắc chung, tôi khai báo tất cả các biến được sử dụng nhiều lần trong khối ở đầu khối. Một số biến tạm thời chỉ dành cho một phép tính cục bộ ở đâu đó, tôi có xu hướng khai báo nơi nó được sử dụng, vì nó không được quan tâm bên ngoài đoạn mã đó.
Lundin

5
Khai báo một biến khi nó cần, không nhất thiết phải ở đầu khối, thường cho phép bạn khởi tạo nó. Thay vì { int n; /* computations ... */ n = some_value; }bạn có thể viết { /* computations ... */ const int n = some_value; }.
Keith Thompson,

@Havok "bạn đã sử dụng cùng một tên cho hai biến", còn được gọi là "biến ẩn" ( man gccsau đó tìm kiếm -Wshadow). Vì vậy, tôi đồng ý Các biến bị che khuất được trình bày ở đây.
Trevor Boyd Smith

1

Nếu trình biên dịch của bạn cho phép thì việc khai báo ở bất cứ đâu bạn muốn là điều tốt. Trên thực tế, mã dễ đọc hơn (IMHO) khi bạn khai báo biến ở nơi bạn sử dụng thay vì ở đầu hàm vì nó dễ phát hiện lỗi hơn, ví dụ như quên khởi tạo biến hoặc vô tình ẩn biến.


0

Một bài đăng hiển thị mã sau:

và tôi nghĩ rằng hàm ý là chúng tương đương nhau. Họ không phải. Nếu int z được đặt ở cuối đoạn mã này, nó gây ra lỗi định nghĩa lại so với định nghĩa z đầu tiên nhưng không so với định nghĩa thứ hai.

Tuy nhiên, nhiều dòng:

làm việc. Cho thấy sự tinh tế của quy tắc C99 này.

Cá nhân tôi rất xa lánh tính năng này của C99.

Đối số rằng nó thu hẹp phạm vi của một biến là sai, như thể hiện trong các ví dụ này. Theo quy tắc mới, bạn không thể khai báo một biến một cách an toàn cho đến khi bạn đã quét toàn bộ khối, trong khi trước đây bạn chỉ cần hiểu những gì đang xảy ra ở đầu mỗi khối.


1
Hầu hết những người sẵn sàng chịu trách nhiệm theo dõi mã của họ được chào đón 'khai báo ở bất cứ đâu' với vòng tay rộng mở do nhiều lợi ích mà nó mang lại cho khả năng đọc. Và forlà một so sánh không liên quan
underscore_d

Nó không phức tạp như bạn tạo ra âm thanh. Phạm vi của một biến bắt đầu từ phần khai báo của nó và kết thúc ở phần tiếp theo }. Đó là nó! Trong ví dụ đầu tiên, nếu bạn muốn thêm nhiều dòng sử dụng zsau printf, bạn sẽ thực hiện bên trong khối mã chứ không phải bên ngoài nó. Bạn chắc chắn không cần phải "quét toàn bộ khối" để xem liệu nó có OK để xác định một biến mới hay không. Tôi phải thú nhận rằng đoạn mã đầu tiên là một ví dụ nhân tạo và tôi có xu hướng tránh nó vì có thêm thụt lề mà nó tạo ra. Tuy nhiên, {int i; for(..){ ... }}khuôn mẫu là điều tôi luôn làm.
ômomg

Tuyên bố của bạn không chính xác vì trong đoạn mã thứ hai (ANSI C), bạn thậm chí không thể đặt khai báo thứ hai của int z ở cuối khối ANSI C vì ANSI C chỉ cho phép bạn đặt khai báo biến ở trên cùng. Vì vậy, sai số là khác nhau, nhưng kết quả là như nhau. Bạn không thể đặt int z ở cuối một trong hai đoạn mã đó.
RTHarston

Ngoài ra, vấn đề với việc có nhiều dòng của vòng lặp for đó là gì? Int i chỉ sống trong khối của vòng lặp for đó, vì vậy không có rò rỉ và không có định nghĩa lặp lại nào về int i.
RTHarston

0

Theo Ngôn ngữ lập trình C của K&R -

Trong C, tất cả các biến phải được khai báo trước khi chúng được sử dụng, thường là ở đầu hàm trước bất kỳ câu lệnh thực thi nào.

Ở đây bạn có thể thấy từ thường không phải ..


Ngày nay, không phải tất cả C đều là K&R - rất ít mã hiện tại được biên dịch bằng các trình biên dịch K&R cổ, vậy tại sao bạn lại sử dụng mã đó làm tài liệu tham khảo?
Toby Speight

Sự rõ ràng và khả năng giải thích của nó thật tuyệt vời. Tôi nghĩ rằng việc học hỏi từ những nhà phát triển ban đầu là rất tốt, có cổ nhưng nó tốt cho người mới bắt đầu.
Gagandeep kaur

0

Với clang và gcc, tôi gặp phải các vấn đề lớn sau đây. gcc phiên bản 8.2.1 20181011 clang phiên bản 6.0.1

không có trình biên dịch nào thích f1, f2 hoặc f3, nằm trong khối. Tôi phải chuyển f1, f2, f3 đến vùng xác định hàm. trình biên dịch đã không bận tâm đến định nghĩa của một số nguyên với khối.


0

Bên trong tất cả các biến cục bộ cho một hàm được cấp phát trên một ngăn xếp hoặc bên trong các thanh ghi CPU, sau đó mã máy được tạo sẽ hoán đổi giữa các thanh ghi và ngăn xếp (được gọi là tràn thanh ghi), nếu trình biên dịch bị lỗi hoặc nếu CPU không có đủ thanh ghi để giữ cho tất cả các quả bóng tung hứng trong không khí.

Để cấp phát nội dung trên ngăn xếp, CPU có hai thanh ghi đặc biệt, một được gọi là Con trỏ ngăn xếp (SP) và một thanh ghi khác - Con trỏ cơ sở (BP) hoặc con trỏ khung (có nghĩa là khung ngăn xếp cục bộ cho phạm vi chức năng hiện tại). SP trỏ vào bên trong vị trí hiện tại trên một ngăn xếp, trong khi BP trỏ đến tập dữ liệu đang hoạt động (bên trên nó) và các đối số hàm (bên dưới nó). Khi hàm được gọi, nó đẩy BP của hàm gọi / hàm cha lên ngăn xếp (được trỏ bởi SP) và đặt SP hiện tại làm BP mới, sau đó tăng SP bằng số byte tràn từ các thanh ghi vào ngăn xếp, thực hiện tính toán và ngược lại, nó khôi phục BP của cha mẹ nó, bằng cách đưa nó ra khỏi ngăn xếp.

Nói chung, việc giữ các biến của bạn bên trong {}-scope của riêng chúng có thể tăng tốc độ biên dịch và cải thiện mã được tạo bằng cách giảm kích thước của biểu đồ mà trình biên dịch phải đi để xác định biến nào được sử dụng ở đâu và như thế nào. Trong một số trường hợp (đặc biệt khi có sự tham gia của goto) trình biên dịch có thể bỏ lỡ thực tế là biến sẽ không được sử dụng nữa, trừ khi bạn nói rõ ràng với trình biên dịch phạm vi sử dụng của nó. Trình biên dịch có thể có giới hạn thời gian / độ sâu để tìm kiếm đồ thị chương trình.

Trình biên dịch có thể đặt các biến được khai báo gần nhau vào cùng một khu vực ngăn xếp, có nghĩa là tải một biến sẽ tải trước tất cả các biến khác vào bộ đệm. Tương tự như vậy, khai báo biến register, có thể cung cấp cho trình biên dịch một gợi ý rằng bạn muốn tránh biến đã nói bị tràn vào ngăn xếp bằng mọi giá.

Tiêu chuẩn C99 nghiêm ngặt yêu cầu rõ ràng { trước khi khai báo, trong khi các phần mở rộng được giới thiệu bởi C ++ và GCC cho phép khai báo các vars sâu hơn vào nội dung, điều này làm phức tạp gotovà các casecâu lệnh. C ++ còn cho phép khai báo nội dung bên trong để khởi tạo vòng lặp, điều này được giới hạn trong phạm vi của vòng lặp.

Cuối cùng nhưng không kém phần quan trọng, đối với một người khác đang đọc mã của bạn, sẽ rất choáng ngợp khi anh ta nhìn thấy phần trên cùng của một hàm với hàng trăm khai báo biến, thay vì chúng được bản địa hóa tại nơi sử dụng. Nó cũng làm cho việc bình luận về việc sử dụng chúng dễ dàng hơn.

TLDR: sử dụng {}để trình bày rõ ràng phạm vi biến có thể giúp cả trình biên dịch và trình đọc của con người.


"Tiêu chuẩn C99 nghiêm ngặt yêu cầu rõ ràng {" là không đúng. Tôi đoán bạn có nghĩa là C89 ở đó. C99 cho phép khai báo sau câu lệnh.
Mecki
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.