C에서 괄호는 스택 프레임 역할을합니까?


153

새 중괄호 세트 내에 변수를 작성하면 해당 변수가 닫는 중괄호의 스택에서 튀어 나왔습니까? 아니면 함수가 끝날 때까지 중단됩니까? 예를 들면 다음과 같습니다.

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

d동안 메모리를 복용 할 code that takes a while부분?


8
(1) 표준에 따라, (2) 구현 간 보편적 관행 또는 (3) 구현 간 공통 관행을 의미합니까?
David Thornley

답변:


83

아니요, 괄호는 스택 프레임으로 작동하지 않습니다. C에서 중괄호는 이름 지정 범위 만 나타내지 만 제어가 범위를 벗어나면 아무것도 파괴되거나 스택에서 튀어 나오는 것이 없습니다.

프로그래머가 코드를 작성하면 종종 스택 프레임 인 것처럼 생각할 수 있습니다. 중괄호 안에 선언 된 식별자는 중괄호 내에서만 액세스 할 수 있으므로 프로그래머의 관점에서 볼 때, 선언 될 때 스택으로 푸시 된 다음 범위가 종료 될 때 팝업되는 것과 같습니다. 그러나 컴파일러는 진입 / 종료시 어떤 것도 푸시 / 팝핑하는 코드를 생성 할 필요가 없으며 일반적으로 그렇지 않습니다.

또한 지역 변수는 스택 공간을 전혀 사용하지 않을 수 있습니다. CPU 레지스터 나 다른 보조 기억 장치 위치에 유지되거나 완전히 최적화 될 수 있습니다.

따라서 d이론적으로 배열은 전체 기능을 위해 메모리를 소비 할 수 있습니다. 그러나 컴파일러는이를 최적화하거나 사용 수명이 겹치지 않는 다른 로컬 변수와 메모리를 공유 할 수 있습니다.


9
구현별로 다르지 않습니까?
avakar

54
C ++에서 객체의 소멸자는 범위 끝에서 호출됩니다. 메모리 회수 여부는 구현 관련 문제입니다.
Kristopher Johnson

8
@ pm100 : 소멸자가 호출됩니다. 그것은 그 물체가 차지한 메모리에 대해서는 아무 것도 말하지 않습니다.
Donal Fellows

9
C 표준은 블록에 선언 된 자동 변수의 수명이 블록 실행이 끝날 때까지만 연장되도록 지정합니다. 따라서 기본적으로 이러한 자동 변수 블록 끝에서 "파기"됩니다.
caf

3
@ 크리스토퍼 존슨 (KristopherJohnson) : 메소드에 각각 1Kbyte 배열을 선언 한 두 개의 개별 블록과 중첩 된 메소드를 호출하는 세 번째 블록이있는 경우 컴파일러는 두 배열 모두에 동일한 메모리를 사용하거나 배열을 배치 할 수 있습니다 스택의 가장 얕은 부분에서 중첩 포인터를 스택 메소드 위로 이동하여 중첩 된 메소드를 호출하십시오. 이러한 동작은 함수 호출에 필요한 스택 깊이를 2K 줄일 수 있습니다.
supercat

39

변수가 실제로 메모리를 차지하는 시간 은 분명히 컴파일러에 따라 다릅니다 (그리고 많은 컴파일러는 내부 블록이 함수 내에서 들어오고 나올 때 스택 포인터를 조정하지 않습니다).

그러나 밀접하게 관련되어 있지만 더 흥미로운 질문은 프로그램이 내부 범위 외부 (그러나 포함 함수 내)의 내부 객체에 액세스 할 수 있는지 여부입니다.

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

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

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

   return;
}

(즉 , 실제로는 그렇지 않은 경우에도 컴파일러 가 할당을 해제 할 수d 있습니까?).

대답은 컴파일러 할당 해제 할 수 있고 주석이 나타내는 위치에 d액세스 p[0]하는 것이 정의되지 않은 동작이라는 것입니다 (프로그램은 내부 범위 외부의 내부 객체에 액세스 할 수 없음 ). C 표준의 관련 부분은 6.2.4p5입니다.

가변 길이 배열 유형이없는 객체 (자동 저장 기간이있는 객체) 의 경우 수명은 해당 블록의 실행이 어떤 식 으로든 끝날 때까지 연관된 블록으로 시작됩니다 . (동봉 된 블록을 입력하거나 함수를 호출하면 현재 블록의 실행이 일시 중단되지만 종료되지는 않습니다.) 블록이 재귀 적으로 입력되면 매번 객체의 새 인스턴스가 생성됩니다. 개체의 초기 값은 불확실합니다. 객체에 대해 초기화가 지정되면 블록 실행시 선언에 도달 할 때마다 수행됩니다. 그렇지 않으면 선언에 도달 할 때마다 값이 결정되지 않습니다.


고급 언어를 사용한 지 몇 년이 지난 후에 C와 C ++에서 범위와 메모리가 작동하는 방식을 배우는 사람은이 대답이 허용되는 것보다 더 정확하고 유용하다는 것을 알게되었습니다.
Chris

20

귀하의 질문은 명확하게 답변 될만큼 명확하지 않습니다.

한편으로, 컴파일러는 일반적으로 중첩 된 블록 범위에 대해 로컬 메모리 할당 할당 해제를 수행하지 않습니다. 로컬 메모리는 일반적으로 기능 입력시 한 번만 할당되고 기능 종료시 해제됩니다.

반면, 로컬 객체의 수명이 끝나면 해당 객체가 점유 한 메모리를 나중에 다른 로컬 객체에 재사용 할 수 있습니다. 예를 들어이 코드에서

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

두 어레이는 일반적으로 동일한 메모리 영역을 차지하므로 기능에 필요한 로컬 스토리지의 총량 은 두 어레이 중 가장 큰 어레이에 필요한 양이며 동시에 두 개의 어레이에 foo필요한 것은 아닙니다.

후자 d가 질문의 맥락에서 기능이 끝날 때까지 메모리 를 계속 차지 하는지 자격이 있는지 여부는 결정하는 것입니다.


6

구현에 따라 다릅니다. gcc 4.3.4의 기능을 테스트하는 간단한 프로그램을 작성했으며 함수 시작시 모든 스택 공간을 한 번에 할당합니다. -S 플래그를 사용하여 gcc가 생성 한 어셈블리를 검사 할 수 있습니다.


3

아니오, d []는 나머지 루틴 동안 스택에 없습니다 . 그러나 alloca ()는 다릅니다.

편집 : Kristopher Johnson (및 simon 및 Daniel)이 옳았 으며 초기 응답이 잘못되었습니다 . CYGWIN에서 gcc 4.3.4를 사용하면 코드는 다음과 같습니다.

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

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

제공합니다 :

_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

살고 배우십시오! 그리고 빠른 테스트를 통해 AndreyT도 여러 할당에 대해 올바른 것으로 보입니다.

훨씬 나중에 추가 : 위의 테스트는 gcc 문서 가 옳지 않다는 것을 보여줍니다 . 수년 동안 그것은 (강조 추가) 말했다 :

" 배열 이름의 범위가 끝나 자마자 가변 길이 배열의 공간이 할당 해제 됩니다 ."


최적화가 비활성화 된 상태로 컴파일한다고해서 최적화 된 코드에서 얻을 수있는 것을 반드시 보여주지는 않습니다. 이 경우 동작은 동일합니다 (함수 시작시 할당하고 함수를 떠날 때만 무료) : godbolt.org/g/M112AQ . 그러나 Cygwin이 아닌 gcc는 alloca함수를 호출하지 않습니다 . cygwin gcc가 그렇게 할 것이라고 정말 놀랐습니다. 가변 길이 배열조차 아니기 때문에 IDK를 사용하는 이유는 무엇입니까?
Peter Cordes

2

그들은 그렇습니다. 그렇지 않을 수도 있습니다. 내가 정말로 필요하다고 생각하는 답변은 다음과 같습니다 . 최신 컴파일러는 모든 종류의 아키텍처 및 구현 별 마술을 수행합니다. 인간에게 간단하고 읽기 쉬운 코드를 작성하고 컴파일러가 좋은 일을하도록하십시오. 컴파일러 주변에서 코드를 작성하려고하면 문제가 발생합니다. 일반적으로 이러한 상황에서 발생하는 문제는 매우 미묘하고 진단하기 어렵습니다.


1

변수 d는 일반적으로 스택에서 튀어 나오지 않습니다. 중괄호는 스택 프레임을 나타내지 않습니다. 그렇지 않으면 다음과 같은 작업을 수행 할 수 없습니다.

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

중괄호로 인해 함수 호출과 같이 실제 스택 푸시 / 팝이 발생하는 경우 위의 코드는 중괄호 안의 코드가 중괄호 var밖에있는 변수에 액세스 할 수 없기 때문에 컴파일되지 않습니다. 함수는 호출 함수의 변수에 직접 액세스 할 수 없습니다). 우리는 이것이 사실이 아니라는 것을 알고 있습니다.

중괄호는 단순히 범위 지정에 사용됩니다. 컴파일러는 둘러싸는 중괄호 외부에서 "내부"변수에 대한 액세스를 유효하지 않은 것으로 간주하고 해당 메모리를 다른 용도로 재사용 할 수 있습니다 (구현에 따라 다름). 그러나 둘러싸는 함수가 반환 될 때까지 스택에서 튀어 나오지 않을 수 있습니다.

업데이트 : C 스펙 의 내용은 다음과 같습니다 . 자동 보관 기간이있는 객체 (섹션 6.4.2) :

가변 길이 배열 유형이없는 객체의 경우 수명은 해당 블록의 실행이 어쨌든 종료 될 때까지 연관된 블록으로의 진입에서 연장됩니다.

동일한 섹션에서는 "평생"이라는 용어를 (강조 광산)으로 정의합니다.

객체 의 수명 은 스토리지가 예약되도록 보장 되는 동안 프로그램 실행의 일부입니다 . 개체가 존재하고 주소가 일정하며 수명 기간 동안 마지막으로 저장된 값을 유지합니다. 수명이 지난 개체를 참조하면 동작이 정의되지 않습니다.

여기서 핵심 단어는 물론 '보장'입니다. 내부 중괄호 세트의 범위를 벗어나면 어레이 수명이 끝납니다. 스토리지가 여전히 스토리지에 할당되었거나 할당되지 않았을 수 있지만 (컴파일러가 다른 공간을 재사용 할 수 있음) 어레이에 액세스하려고하면 정의되지 않은 동작이 발생하여 예측할 수없는 결과가 발생합니다.

C 스펙에는 스택 프레임에 대한 개념이 없습니다. 결과 프로그램의 작동 방식에 대해서만 이야기하고 구현 세부 사항을 컴파일러에 남겨 둡니다 (결국 하드웨어 스택이있는 CPU의 경우와 스택리스 CPU의 경우 구현이 상당히 다르게 보입니다). C 스펙에는 스택 프레임이 끝나거나 끝나지 않는 위치를 지정하는 것이 없습니다. 유일한 진짜 알 방법은 특정 컴파일러 / 플랫폼에서 코드를 컴파일하고 어셈블리 결과를 검토하는 것입니다. 컴파일러의 현재 최적화 옵션 세트도 이와 같은 역할을합니다.

당신이 배열하지 않은지 확인하려면 d더 이상 코드가 실행되는 동안 메모리를 먹고, 당신도 별도의 기능 또는 명시 적으로 중괄호의 코드를 변환 할 수 있습니다 mallocfree메모리 대신 자동 스토리지를 사용하여.


1
"중괄호가 스택 푸시 / 팝을 일으킨 경우, 중괄호 안의 코드가 중괄호 밖에있는 변수 var에 액세스 할 수 없기 때문에 위 코드는 컴파일 되지 않습니다. " -이것은 사실이 아닙니다. 컴파일러는 항상 스택 / 프레임 포인터와의 거리를 기억하여 외부 변수를 참조하는 데 사용할 수 있습니다. 또한, 중괄호의 예는 요셉의 답변을 참조 스택 푸시 / 팝 원인.
조지

@ george- Joseph의 예제뿐만 아니라 설명하는 동작은 사용중인 컴파일러와 플랫폼에 따라 다릅니다. 예를 들어, MIPS 대상에 대해 동일한 코드를 컴파일하면 완전히 다른 결과가 생성됩니다. 나는 OP가 컴파일러 또는 대상을 지정하지 않았기 때문에 C 스펙의 관점에서 순수하게 말하고있었습니다. 답변을 수정하고 더 구체적인 내용을 추가하겠습니다.
bta

0

나는 그것이 범위를 벗어났다고 생각하지만 함수가 반환 될 때까지 스택에서 팝되지 않습니다. 따라서 함수가 완료 될 때까지 스택에서 메모리를 차지하지만 첫 번째 닫는 중괄호의 다운 스트림에는 액세스 할 수 없습니다.


3
보장하지 않습니다. 스코프가 닫히면 컴파일러는 더 이상 해당 메모리를 추적하지 않으며 (또는 적어도 필요하지는 않습니다 ...) 재사용 할 수 있습니다. 이것이 범위를 벗어난 변수에 의해 점유되었던 메모리를 건 드리는 것이 정의되지 않은 동작 인 이유입니다. 코 악마 및 이와 유사한 경고에주의하십시오.
dmckee --- 전 운영자 고양이

0

표준에 대해서는 실제로 구현에 따라 다름을 나타내는 많은 정보가 이미 제공되어 있습니다 .

따라서 하나의 실험이 흥미로울 수 있습니다. 다음 코드를 시도하면

#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);
    }
}

gcc를 사용하여 동일한 주소의 두 배를 얻습니다 : Coliro

그러나 다음 코드를 시도하면

#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);
}

우리는 여기에 두 개의 서로 다른 주소를 가져 GCC를 사용 : Coliro를

그래서, 당신은 정말로 무슨 일이 일어나고 있는지 확신 할 수 없습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.