2015-04-26 41 views
5

Tôi đang đọc Lập trình C - Phương pháp tiếp cận hiện đại bởi K.N.King để tìm hiểu ngôn ngữ lập trình C và lưu ý rằng các tuyên bố goto không được bỏ qua các khai báo mảng có độ dài thay đổi.Bỏ qua khai báo biến bằng goto?

Nhưng bây giờ câu hỏi là: Tại sao goto nhảy được phép bỏ qua các khai báo mảng cố định và khai báo thông thường? Và chính xác hơn, hành vi của các ví dụ như thế này, theo tiêu chuẩn C99 là gì? Khi tôi thử nghiệm những trường hợp này, có vẻ như các tuyên bố đã thực sự không nhảy qua, nhưng điều đó có đúng không? Các biến có khai báo có thể đã được nhảy qua an toàn để sử dụng không?

1.

goto later; 
int a = 4; 
later: 
printf("%d", a); 

2.

goto later; 
int a; 
later: 
a = 4; 
printf("%d", a); 

3.

goto later; 
int a[4]; 
a[0] = 1; 
later: 
a[1] = 2; 
for(int i = 0; i < sizeof(a)/sizeof(a[0]); i++) 
    printf("%d\n", a[i]); 
+0

@ Mints97 Ah, vậy nếu các câu lệnh có các khối riêng của chúng ngay cả khi không có các câu lệnh phức hợp thì sao? Tôi cho rằng đó là câu trả lời sau đó :) quá xấu Tôi không thể chấp nhận ý kiến ​​ – MinecraftShamrock

+0

bạn có ý gì? Điều gì sẽ xảy ra nếu các câu lệnh liên quan đến điều này? Và các khối và các câu lệnh phức hợp ít nhiều giống nhau, IIRC – Mints97

+0

@ Mints97 Tôi có nghĩa là các biến được khai báo có điều kiện không được chuyển đến đầu của toàn bộ hàm nhưng chỉ bắt đầu từ "khối" có điều kiện mà chúng tồn tại ở bên phải? Vì vậy, một câu lệnh if không có câu lệnh ghép sẽ đại diện cho một khối như vậy. Tôi hiểu có đúng không? – MinecraftShamrock

Trả lời

9

Tôi đang trong tâm trạng cho việc giải thích này mà không Memory đẫm máu la chi tiết của bạn (tôi tin rằng, họ nhận được rất gory khi VLA được sử dụng; xem câu trả lời của @ Ulfalizer để biết chi tiết).

Vì vậy, ban đầu, trong C89, đó là bắt buộc phải khai báo tất cả các biến vào lúc bắt đầu của một khối, như thế này:

{ 
    int a = 1; 
    a++; 
    /* ... */ 
} 

này trực tiếp ám chỉ một điều rất quan trọng: một khối == một bộ không thay đổi các khai báo biến.

C99 đã thay đổi cài đặt này. Trong đó, bạn có thể khai báo các biến trong bất kỳ phần nào của khối, nhưng các câu lệnh khai báo vẫn khác với các câu lệnh thông thường. Trong thực tế, để hiểu điều này, bạn có thể tưởng tượng rằng tất cả các khai báo biến được chuyển hoàn toàn đến đầu khối, nơi chúng được khai báo và không có sẵn cho tất cả các câu lệnh đặt trước chúng.

Điều đó đơn giản chỉ vì khối một == một bộ khai báo quy tắc vẫn giữ.

Đó là lý do tại sao bạn không thể "nhảy qua tuyên bố". Biến được khai báo sẽ vẫn tồn tại.

Sự cố đang khởi tạo. Nó không nhận được "di chuyển" bất cứ nơi nào. Vì vậy, về mặt kỹ thuật, đối với trường hợp của bạn, các chương trình sau đây có thể được coi là tương đương:

goto later; 
int a = 100; 
later: 
printf("%d", a); 

int a; 
goto later; 
a = 100; 
later: 
printf("%d", a); 

Như bạn có thể thấy, việc kê khai vẫn còn đó, những gì đang được bỏ qua là khởi tạo.

Lý do điều này không hoạt động với VLAs là chúng khác nhau.Tóm lại, đó là vì điều này hợp lệ:

int size = 7; 
int test[size]; 

Tuyên bố VLAs sẽ không giống như tất cả các khai báo khác, hoạt động khác nhau ở các phần khác nhau của khối. Trong thực tế, VLA có thể có các bố trí bộ nhớ hoàn toàn khác nhau tùy thuộc vào vị trí nó được khai báo. Bạn không thể "di chuyển" nó ra khỏi nơi bạn vừa nhảy qua.

Bạn có thể hỏi, "được rồi, vậy tại sao không làm cho nó để tuyên bố sẽ không bị ảnh hưởng bởi goto"? Vâng, bạn vẫn nhận được những trường hợp như thế này:

goto later; 
int size = 7; 
int test[size]; 
later: 

gì bạn thực sự mong đợi điều này để làm ..

Vì vậy, cấm nhảy trên tờ khai VLA là có một lý do - đó là nhiều nhất? quyết định hợp lý để đối phó với các trường hợp như trên bằng cách đơn giản là cấm chúng hoàn toàn.

+3

Ngay cả trong C89/C90, nó là hợp pháp để nhảy vào một khối: 'goto LABEL ; {int n = 42; LABEL: printf ("% d \ n", n); } ' –

+0

@KeithThompson: woah, cảm ơn! Tôi hoàn toàn quên rằng một =) Tôi sẽ chỉnh sửa câu trả lời ngay lập tức! – Mints97

+1

Một ví dụ tuyệt vời/đáng sợ về những gì @KeithThompson đề cập đến được gọi là [thiết bị của Duff] (https://en.wikipedia.org/wiki/Duff%27s_device). Nó sử dụng các nhãn chuyển đổi để nhảy vào giữa vòng lặp while. –

5

Lý do bạn không được phép bỏ qua các lời tuyên bố của một mảng chiều dài thay đổi (VLA) là nó sẽ nhận được lộn xộn với cách Vlas được thực hiện phổ biến, và sẽ làm phức tạp ngữ nghĩa của ngôn ngữ. Cách thức VLAs có khả năng được thực hiện trong thực tế là bằng cách giảm (hoặc tăng dần, trên các kiến ​​trúc mà ngăn xếp tăng lên) con trỏ ngăn xếp theo số động (được tính toán trong thời gian chạy) để nhường chỗ cho VLA trên ngăn xếp. Điều này xảy ra tại thời điểm VLA được khai báo (ít nhất là khái niệm, bỏ qua tối ưu hóa). Điều này là cần thiết để các hoạt động ngăn xếp sau này (ví dụ, đẩy các đối số vào ngăn xếp cho một cuộc gọi hàm) không bước vào bộ nhớ của VLA.

Đối với VLAs lồng trong khối, con trỏ ngăn xếp thường được khôi phục ở cuối khối chứa VLA. Nếu goto được phép nhảy vào khối như vậy và vượt quá khai báo VLA, thì mã để khôi phục con trỏ ngăn xếp sẽ chạy mà không có mã khởi tạo tương ứng đã được chạy, điều này có thể gây ra sự cố. Ví dụ, con trỏ ngăn xếp có thể được tăng lên bởi kích thước của VLA mặc dù nó không bao giờ bị giảm đi, điều này sẽ làm cho địa chỉ trả về được đẩy khi hàm chứa VLA được gọi xuất hiện sai vị trí tương đối vào con trỏ ngăn xếp.

Nó cũng lộn xộn từ góc độ ngữ nghĩa thuần túy. Nếu bạn được phép bỏ qua tuyên bố, thì kích thước của mảng sẽ là bao nhiêu? Điều gì nên sizeof trở lại? Truy cập nó có nghĩa là gì?

Đối với các trường hợp không phải VLA, bạn chỉ đơn giản là bỏ qua việc khởi tạo giá trị (nếu có), điều này không nhất thiết gây ra sự cố trong và của chính nó. Nếu bạn nhảy qua định nghĩa không phải VLA như int x;, thì bộ nhớ sẽ vẫn được dành riêng cho biến số x. VLAs khác nhau ở chỗ kích thước của chúng được tính theo thời gian chạy, điều này làm phức tạp mọi thứ. Một lưu ý phụ, một trong những động lực cho phép các biến được khai báo ở bất kỳ đâu trong một khối trong C99 (C89 yêu cầu khai báo ở đầu khối, mặc dù ít nhất GCC cho phép chúng trong khối như một phần mở rộng) để hỗ trợ VLAs. Có thể thực hiện các phép tính trước đó trong khối trước khi tuyên bố kích thước của VLA là tiện dụng.

Vì một số lý do liên quan, C++ không cho phép goto s bỏ qua các khai báo đối tượng (hoặc khởi tạo cho các loại dữ liệu cũ thuần túy, ví dụ: int). Điều này là bởi vì nó sẽ là không an toàn để nhảy qua mã mà gọi constructor nhưng vẫn chạy destructor ở phần cuối của khối.

3

Sử dụng goto để nhảy qua khai báo biến là gần như chắc chắn là một ý tưởng thực sự tồi tệ, nhưng nó hoàn toàn hợp pháp.

C tạo sự khác biệt giữa thời hạn suốt đời của biến và phạm vi .

Đối với biến được khai báo không có từ khóa static bên trong hàm, phạm vi của nó (vùng văn bản chương trình có tên hiển thị) mở rộng từ định nghĩa đến cuối khối bao quanh gần nhất. Tuổi thọ của nó (thời gian lưu trữ) bắt đầu khi nhập vào khối và kết thúc khi thoát khỏi khối. Nếu nó có bộ khởi tạo, nó được thực hiện khi (và nếu) định nghĩa được đạt tới.

Ví dụ:

{ /* the lifetime of x and y starts here; memory is allocated for both */ 
    int x = 10; /* the name x is visible from here to the "}" */ 
    int y = 20; /* the name y is visible from here to the "}" */ 
    int vla[y]; /* vla is visible, and its lifetime begins here */ 
    /* ... */ 
} 

Đối với mảng chiều dài thay đổi (Vlas), khả năng hiển thị của các định danh là như nhau, nhưng thời gian tồn tại của đối tượng bắt đầu từ định nghĩa. Tại sao? Vì độ dài của mảng không nhất thiết phải được biết trước điểm đó. Trong ví dụ này, không thể cấp phát bộ nhớ cho vla ở đầu khối, vì chúng tôi chưa biết giá trị của y.

A goto bỏ qua định nghĩa đối tượng bỏ qua bất kỳ bộ khởi tạo nào cho đối tượng đó, nhưng bộ nhớ vẫn được phân bổ cho nó. Nếu số goto nhảy vào một khối, bộ nhớ sẽ được phân bổ khi khối được nhập. Nếu không (nếu cả hai goto và nhãn mục tiêu ở cùng một mức trong cùng một khối), thì đối tượng sẽ đã được cấp phát.

... 
goto LABEL; 
{ 
    int x = 10; 
    LABEL: printf("x = %d\n", x); 
} 

Khi tuyên bố printf được thực thi, x tồn tại và tên của nó là có thể nhìn thấy, nhưng khởi của nó đã được bỏ qua, vì vậy nó có giá trị không xác định.

Ngôn ngữ cấm goto bỏ qua định nghĩa của mảng có độ dài thay đổi. Nếu nó được cho phép, nó sẽ bỏ qua việc cấp phát bộ nhớ cho đối tượng và bất kỳ nỗ lực nào để tham chiếu nó sẽ gây ra hành vi không xác định.

goto báo cáo do have their uses. Sử dụng chúng để bỏ qua các khai báo, mặc dù nó được ngôn ngữ cho phép, không phải là một trong số chúng.