Trong C, niềng răng có hoạt động như một khung stack không?


153

Nếu tôi tạo một biến trong một tập hợp các dấu ngoặc nhọn mới, thì biến đó có bật ra khỏi ngăn xếp trên dấu ngoặc đóng hay nó có bị treo cho đến khi kết thúc hàm không? Ví dụ:

void foo() {
   int c[100];
   {
       int d[200];
   }
   //code that takes a while
   return;
}

Sẽ dchiếm bộ nhớ trong code that takes a whilephần?


8
Bạn có nghĩa là (1) theo Tiêu chuẩn, (2) thực tiễn phổ biến trong số các triển khai, hoặc (3) thực tiễn chung giữa các triển khai?
David Thornley

Câu trả lời:


83

Không, niềng răng không hoạt động như một khung ngăn xếp. Trong C, dấu ngoặc chỉ biểu thị một phạm vi đặt tên, nhưng không có gì bị phá hủy cũng như không có gì bật ra khỏi ngăn xếp khi điều khiển vượt qua nó.

Là một lập trình viên viết mã, bạn thường có thể nghĩ về nó như thể nó là một khung stack. Các mã định danh được khai báo trong dấu ngoặc nhọn chỉ có thể truy cập được trong dấu ngoặc nhọn, vì vậy theo quan điểm của lập trình viên, nó giống như chúng được đẩy lên ngăn xếp khi chúng được khai báo và sau đó bật ra khi phạm vi được thoát ra. Tuy nhiên, trình biên dịch không phải tạo mã đẩy / bật bất cứ thứ gì khi vào / ra (và nói chung, chúng không).

Cũng lưu ý rằng các biến cục bộ hoàn toàn không thể sử dụng bất kỳ không gian ngăn xếp nào: chúng có thể được giữ trong các thanh ghi CPU hoặc trong một số vị trí lưu trữ phụ trợ khác hoặc được tối ưu hóa hoàn toàn.

Vì vậy, dtheo lý thuyết, mảng có thể tiêu thụ bộ nhớ cho toàn bộ hàm. Tuy nhiên, trình biên dịch có thể tối ưu hóa nó đi hoặc chia sẻ bộ nhớ của nó với các biến cục bộ khác mà tuổi thọ sử dụng không trùng nhau.


9
Đó không phải là thực hiện cụ thể?
avakar

54
Trong C ++, hàm hủy của đối tượng được gọi ở cuối phạm vi của nó. Liệu bộ nhớ có được thu hồi hay không là một vấn đề cụ thể khi thực hiện.
Kristopher Johnson

8
@ pm100: Các hàm hủy sẽ được gọi. Điều đó không nói gì về bộ nhớ mà những đối tượng đó chiếm giữ.
Donal Fellows

9
Tiêu chuẩn C chỉ định rằng thời gian tồn tại của các biến tự động được khai báo trong khối chỉ kéo dài cho đến khi việc thực hiện khối kết thúc. Vì vậy, về cơ bản các biến tự động đó sẽ bị "phá hủy" ở cuối khối.
phê

3
@KristopherJohnson: Nếu một phương thức có hai khối riêng biệt, mỗi khối khai báo mảng 1Kbyte và khối thứ ba gọi là phương thức lồng nhau, trình biên dịch sẽ tự do sử dụng cùng một bộ nhớ cho cả hai mảng và / hoặc để đặt mảng ở phần nông nhất của ngăn xếp và di chuyển con trỏ ngăn xếp lên trên nó gọi phương thức lồng nhau. Hành vi như vậy có thể giảm 2K độ sâu ngăn xếp cần thiết cho lệnh gọi hàm.
supercat

39

Thời gian mà biến thực sự chiếm bộ nhớ rõ ràng phụ thuộc vào trình biên dịch (và nhiều trình biên dịch không điều chỉnh con trỏ ngăn xếp khi các khối bên trong được nhập và thoát trong các hàm).

Tuy nhiên, một câu hỏi liên quan chặt chẽ nhưng có thể thú vị hơn là liệu chương trình có được phép truy cập vào đối tượng bên trong đó bên ngoài phạm vi bên trong (nhưng bên trong hàm chứa), tức là:

void foo() {
   int c[100];
   int *p;

   {
       int d[200];
       p = d;
   }

   /* Can I access p[0] here? */

   return;
}

(Nói cách khác: trình biên dịch có được phép phân bổ d, ngay cả trong thực tế hầu hết không?).

Câu trả lời là trình biên dịch được phép phân bổ dvà truy cập vào p[0]nơi nhận xét chỉ ra là hành vi không xác định (chương trình không được phép truy cập vào đối tượng bên ngoài bên ngoài phạm vi bên trong). Phần có liên quan của tiêu chuẩn C là 6.2.4p5:

Đối với một đối tượng như vậy [một đối tượng có thời lượng lưu trữ tự động] không có kiểu mảng có độ dài thay đổi, thời gian tồn tại của nó kéo dài từ mục nhập vào khối mà nó được liên kết cho đến khi việc thực hiện khối đó kết thúc theo bất kỳ cách nào . (Nhập một khối kèm theo hoặc gọi một hàm tạm dừng, nhưng không kết thúc, thực thi khối hiện tại.) Nếu khối được nhập theo cách đệ quy, một phiên bản mới của đối tượng được tạo mỗi lần. Giá trị ban đầu của đối tượng là không xác định. Nếu một khởi tạo được chỉ định cho đối tượng, nó được thực hiện mỗi khi đạt được khai báo trong quá trình thực thi khối; mặt khác, giá trị trở nên không xác định mỗi lần đạt được khai báo.


Khi ai đó học cách phạm vi và bộ nhớ hoạt động trong C và C ++ sau nhiều năm sử dụng các ngôn ngữ cấp cao hơn, tôi thấy câu trả lời này chính xác và hữu ích hơn so với ngôn ngữ được chấp nhận.
Chris

20

Câu hỏi của bạn không đủ rõ ràng để được trả lời rõ ràng.

Một mặt, trình biên dịch thường không thực hiện bất kỳ phân bổ bộ nhớ cục bộ nào - phân bổ cho phạm vi khối lồng nhau. Bộ nhớ cục bộ thường chỉ được cấp phát một lần tại mục nhập chức năng và được giải phóng khi thoát chức năng.

Mặt khác, khi thời gian tồn tại của một đối tượng cục bộ kết thúc, bộ nhớ bị chiếm bởi đối tượng đó có thể được sử dụng lại cho một đối tượng cục bộ khác sau đó. Ví dụ, trong mã này

void foo()
{
  {
    int d[100];
  }
  {
    double e[20];
  }
}

cả hai mảng thường sẽ chiếm cùng một vùng nhớ, có nghĩa là tổng dung lượng lưu trữ cục bộ cần theo chức năng foolà bất cứ điều gì cần thiết cho phần lớn nhất của hai mảng, không phải cho cả hai mảng cùng một lúc.

Liệu cái sau có đủ điều kiện là dtiếp tục chiếm bộ nhớ cho đến khi kết thúc chức năng trong bối cảnh câu hỏi của bạn hay không là do bạn quyết định.


6

Đó là phụ thuộc thực hiện. Tôi đã viết một chương trình ngắn để kiểm tra gcc 4.3.4 làm gì và nó phân bổ tất cả không gian ngăn xếp cùng một lúc khi bắt đầu hàm. Bạn có thể kiểm tra lắp ráp mà gcc tạo ra bằng cờ -S.


3

Không, d [] sẽ không ở trong ngăn xếp trong phần còn lại của thói quen. Nhưng alloca () thì khác.

Chỉnh sửa: Kristopher Johnson (và simon và Daniel) là đúng , và phản ứng ban đầu của tôi là sai . Với gcc 4.3.4.on CYGWIN, mã:

void foo(int[]);
void bar(void);
void foobar(int); 

void foobar(int flag) {
    if (flag) {
        int big[100000000];
        foo(big);
    }
    bar();
}

cho:

_foobar:
    pushl   %ebp
    movl    %esp, %ebp
    movl    $400000008, %eax
    call    __alloca
    cmpl    $0, 8(%ebp)
    je      L2
    leal    -400000000(%ebp), %eax
    movl    %eax, (%esp)
    call    _foo
L2:
    call    _bar
    leave
    ret

Sống và học hỏi! Và một bài kiểm tra nhanh dường như cho thấy rằng AndreyT cũng đúng về nhiều phân bổ.

Đã thêm nhiều sau : Thử nghiệm trên cho thấy tài liệu gcc không hoàn toàn đúng. Trong nhiều năm, nó đã nói (nhấn mạnh thêm):

"Không gian cho một mảng chiều dài thay đổi được deallocated càng sớm càng tên mảng của phạm vi mục đích ."


Biên dịch với tối ưu hóa bị vô hiệu hóa không nhất thiết phải cho bạn thấy những gì bạn sẽ nhận được trong mã được tối ưu hóa. Trong trường hợp này, hành vi là như nhau (phân bổ khi bắt đầu chức năng và chỉ miễn phí khi rời khỏi chức năng): godbolt.org/g/M112AQ . Nhưng gcc không phải cygwin không gọi một allocachức năng. Tôi thực sự ngạc nhiên khi Cygwin gcc sẽ làm điều đó. Nó thậm chí không phải là một mảng có độ dài thay đổi, vì vậy IDK tại sao bạn đưa nó lên.
Peter Cordes

2

Họ có thể. Họ có thể không. Câu trả lời tôi nghĩ bạn thực sự cần là: Đừng bao giờ thừa nhận bất cứ điều gì. Trình biên dịch hiện đại làm tất cả các loại kiến ​​trúc và ma thuật cụ thể thực hiện. Viết mã của bạn một cách đơn giản và dễ đọc cho con người và để trình biên dịch làm những thứ tốt. Nếu bạn cố gắng viết mã xung quanh trình biên dịch bạn đang gặp rắc rối - và rắc rối bạn thường gặp phải trong những tình huống này thường rất tinh vi và khó chẩn đoán.


1

Biến của bạn dthường không xuất hiện trong ngăn xếp. Niềng răng xoăn không biểu thị một khung ngăn xếp. Nếu không, bạn sẽ không thể làm một cái gì đó như thế này:

char var = getch();
    {
        char next_var = var + 1;
        use_variable(next_char);
    }

Nếu các dấu ngoặc nhọn gây ra một lần đẩy / bật ngăn xếp thực sự (giống như một lệnh gọi hàm), thì đoạn mã trên sẽ không biên dịch được vì mã bên trong dấu ngoặc nhọn sẽ không thể truy cập vào biến varnằm ngoài dấu ngoặc (giống như một hàm con chức năng không thể truy cập trực tiếp các biến trong chức năng gọi). Chúng tôi biết rằng đây không phải là trường hợp.

Niềng răng xoăn đơn giản được sử dụng cho phạm vi. Trình biên dịch sẽ coi mọi quyền truy cập vào biến "bên trong" từ bên ngoài dấu ngoặc nhọn là không hợp lệ và nó có thể sử dụng lại bộ nhớ đó cho mục đích khác (điều này phụ thuộc vào việc triển khai). Tuy nhiên, nó có thể không được bật ra khỏi ngăn xếp cho đến khi hàm kèm theo trở lại.

Cập nhật: Đây là những gì thông số kỹ thuật C đã nói. Về các đối tượng có thời gian lưu trữ tự động (mục 6.4.2):

Đối với một đối tượng không có kiểu mảng có chiều dài thay đổi, thời gian tồn tại của nó kéo dài từ mục nhập vào khối mà nó được liên kết cho đến khi việc thực hiện khối đó kết thúc bằng mọi cách.

Phần tương tự định nghĩa thuật ngữ "trọn đời" là (nhấn mạnh của tôi):

Thời gian tồn tại của một đối tượng là một phần của việc thực hiện chương trình trong đó lưu trữ được đảm bảo được dành riêng cho nó. Một đối tượng tồn tại, có một địa chỉ không đổi và giữ lại giá trị được lưu trữ cuối cùng trong suốt vòng đời của nó. Nếu một đối tượng được đề cập đến bên ngoài vòng đời của nó, hành vi không được xác định.

Từ khóa ở đây, tất nhiên, là 'được bảo đảm'. Khi bạn rời khỏi phạm vi của bộ dấu ngoặc trong, thời gian tồn tại của mảng sẽ kết thúc. Bộ nhớ có thể vẫn có thể được phân bổ cho nó (trình biên dịch của bạn có thể sử dụng lại không gian cho thứ khác), nhưng mọi nỗ lực truy cập vào mảng đều gọi hành vi không xác định và mang lại kết quả không thể đoán trước.

Thông số C không có khái niệm về khung stack. Nó chỉ nói về cách chương trình kết quả sẽ hoạt động và để lại các chi tiết thực hiện cho trình biên dịch (xét cho cùng, việc triển khai sẽ trông khá khác biệt trên CPU không có ngăn xếp so với CPU có ngăn xếp phần cứng). Không có gì trong thông số C bắt buộc trong đó khung stack sẽ hoặc không kết thúc. Cách thực sự duy nhất để biết là biên dịch mã trên trình biên dịch / nền tảng cụ thể của bạn và kiểm tra tập hợp kết quả. Bộ tùy chọn tối ưu hóa hiện tại của nhà biên dịch của bạn cũng có thể đóng vai trò trong việc này.

Nếu bạn muốn đảm bảo rằng mảng dkhông còn chiếm bộ nhớ trong khi mã của bạn đang chạy, bạn có thể chuyển đổi mã trong dấu ngoặc nhọn thành một chức năng riêng biệt hoặc rõ ràng mallocfreebộ nhớ thay vì sử dụng lưu trữ tự động.


1
"Nếu các dấu ngoặc nhọn gây ra một lần đẩy / bật ngăn xếp, thì đoạn mã trên sẽ không được biên dịch bởi vì mã bên trong dấu ngoặc nhọn sẽ không thể truy cập vào biến var sống bên ngoài dấu ngoặc" - điều này đơn giản là không đúng. Trình biên dịch luôn có thể nhớ khoảng cách từ con trỏ stack / frame và sử dụng nó để tham chiếu các biến ngoài. Ngoài ra, xem câu trả lời Joseph cho một ví dụ về dấu ngoặc nhọn mà làm nguyên một chồng push / pop.
george

@ george- Hành vi bạn mô tả, cũng như ví dụ của Joseph, phụ thuộc vào trình biên dịch và nền tảng mà bạn đang sử dụng. Ví dụ: biên dịch cùng một mã cho mục tiêu MIPS mang lại kết quả hoàn toàn khác nhau. Tôi đã nói hoàn toàn từ quan điểm của thông số C (vì OP không chỉ định trình biên dịch hoặc mục tiêu). Tôi sẽ chỉnh sửa câu trả lời và thêm chi tiết cụ thể.
bta

0

Tôi tin rằng nó không vượt quá phạm vi, nhưng không bật ra khỏi ngăn xếp cho đến khi hàm trả về. Vì vậy, nó vẫn sẽ chiếm bộ nhớ trên ngăn xếp cho đến khi chức năng được hoàn thành, nhưng không thể truy cập được vào hạ lưu của dấu ngoặc nhọn đóng đầu tiên.


3
Không đảm bảo. Khi phạm vi đóng trình biên dịch sẽ không theo dõi bộ nhớ đó nữa (hoặc ít nhất là không bắt buộc phải ...) và cũng có thể sử dụng lại nó. Đây là lý do tại sao việc chạm vào bộ nhớ trước đây bị chiếm bởi một biến ngoài phạm vi là hành vi không xác định. Cẩn thận với quỷ mũi và cảnh báo tương tự.
dmckee --- ex-moderator mèo con

0

Hiện đã có nhiều thông tin về tiêu chuẩn chỉ ra rằng nó thực sự cụ thể.

Vì vậy, một thí nghiệm có thể được quan tâm. Nếu chúng tôi thử đoạn mã sau:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
        printf("%p\n", (void*) x);
    }
    {
        int b;
        y = &b;
        printf("%p\n", (void*) y);
    }
}

Sử dụng gcc, chúng tôi nhận được ở đây hai lần cùng một địa chỉ: Coliro

Nhưng nếu chúng ta thử đoạn mã sau:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
    }
    {
        int b;
        y = &b;
    }
    printf("%p\n", (void*) x);
    printf("%p\n", (void*) y);
}

Sử dụng gcc chúng tôi có được ở đây hai địa chỉ khác nhau: Coliro

Vì vậy, bạn không thể thực sự chắc chắn những gì đang xảy ra.

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.