C ++의 스택, 정적 및 힙


160

검색했지만이 세 가지 개념을 잘 이해하지 못했습니다. 힙에서 동적 할당을 언제 사용해야합니까? 그리고 실제 장점은 무엇입니까? 정적 및 스택의 문제점은 무엇입니까? 힙에 변수를 할당하지 않고 전체 응용 프로그램을 작성할 수 있습니까?

다른 언어에는 "가비지 수집기"가 포함되어 있으므로 메모리에 대해 걱정할 필요가 없습니다. 가비지 수집기는 무엇을합니까?

이 가비지 수집기를 사용하여 수행 할 수없는 메모리를 직접 조작 할 수 있습니까?

누군가가이 선언으로 저에게 말했습니다 :

int * asafe=new int;

"포인터에 대한 포인터"가 있습니다. 무슨 뜻인가요? 다음과 다릅니다.

asafe=new int;

?


얼마 전에 비슷한 질문이있었습니다. 스택과 힙은 무엇이며 어디에 있습니까? 그 질문에 대한 몇 가지 좋은 답변이 있습니다.
Scott Saad

답변:


223

비슷한 질문 이 있었지만 정적에 대해서는 묻지 않았습니다.

정적, 힙 및 스택 메모리의 요약 :

  • 정적 변수는 전역 적으로 액세스 할 수없는 경우에도 기본적으로 전역 변수입니다. 일반적으로 실행 파일 자체에 주소가 있습니다. 전체 프로그램에 대해 하나의 사본 만 있습니다. 함수 호출 (또는 클래스) (및 스레드 수)에 몇 번이나 상관없이 변수는 동일한 메모리 위치를 참조합니다.

  • 힙은 동적으로 사용할 수있는 메모리입니다. 객체에 4kb를 원하면 동적 할당자는 힙에서 사용 가능한 공간 목록을 살펴보고 4kb 청크를 선택하여 사용자에게 제공합니다. 일반적으로 동적 메모리 할당 자 (malloc, new 등)는 메모리 끝에서 시작하여 뒤로 작동합니다.

  • 스택이 어떻게 늘어나고 줄어드는 지 설명하는 것은이 답변의 범위를 벗어나지 만, 항상 끝에서만 추가하고 제거한다고 말하면 충분합니다. 스택은 일반적으로 시작하여 더 낮은 주소로 증가합니다. 스택이 중간 어딘가에서 동적 할당자를 만나면 메모리가 부족합니다 (물리적 메모리와 가상 메모리 및 조각화 참조). 다중 스레드에는 다중 스택이 필요합니다 (프로세스는 일반적으로 스택의 최소 크기를 예약합니다).

각각을 사용하고 싶을 때 :

  • 스태틱 / 글로벌은 항상 필요하고 할당 해제를 원하지 않는 메모리에 유용합니다. (이러한 방식으로, 임베디드 환경은 정적 메모리 만있는 것으로 생각할 수 있습니다. 스택 및 힙은 세 번째 메모리 유형 (프로그램 코드)이 공유하는 알려진 주소 공간의 일부입니다. 프로그램 코드는 종종 동적 할당을 수행합니다. 링크 된리스트와 같은 것들이 필요할 때 정적 메모리 그러나, 정적 메모리 자체 (버퍼) 자체는 "할당"되지 않고 오히려 다른 목적은이 목적을 위해 버퍼가 보유한 메모리에서 할당됩니다. 콘솔 게임은 내장 된 동적 메모리 메커니즘을 자주 배제하여 모든 할당에 대해 사전 설정된 크기의 버퍼를 사용하여 할당 프로세스를 엄격하게 제어합니다.)

  • 스택 변수는 함수가 범위 내 (스택 어딘가)에있는 한 변수가 유지되기를 원할 때 유용합니다. 스택은 코드가있는 코드에 필요하지만 해당 코드 외부에서는 필요하지 않은 변수에 좋습니다. 또한 파일과 같은 리소스에 액세스 할 때 유용하며 해당 코드를 떠날 때 리소스가 자동으로 사라지기를 원합니다.

  • 힙 할당 (동적 할당 메모리)은 위의 것보다 더 유연하게하고 싶을 때 유용합니다. 종종 이벤트에 응답하기 위해 함수가 호출됩니다 (사용자가 "상자 만들기"버튼을 클릭 함). 올바른 응답을 위해서는 함수가 종료 된 후 오래 지속되어야하는 새 개체 (새 Box 개체)를 할당해야하므로 스택에있을 수 없습니다. 그러나 프로그램을 시작할 때 얼마나 많은 상자를 원하는지 알 수 없으므로 정적 일 수 없습니다.

가비지 콜렉션

최근 가비지 콜렉터가 얼마나 훌륭한 지에 대해 많이 들었으므로 약간의 반대 의견이 도움이 될 것입니다.

가비지 콜렉션은 성능이 큰 문제가 아닐 때 훌륭한 메커니즘입니다. GC가 점점 더 정교 해지고 있다고 들었지만 사실 (사용 사례에 따라) 성능 저하를 감수해야 할 수도 있습니다. 게 으르더라도 여전히 제대로 작동하지 않을 수 있습니다. 가비지 콜렉터는 최상의 경우 참조가 더 이상 없다는 것을 알게되면 메모리가 사라진다는 것을 알게됩니다 ( 참조 횟수 계산 참조)). 그러나 자신을 참조하는 오브젝트가있는 경우 (아마도 다른 오브젝트를 참조하여 참조하는 경우) 참조 카운트만으로는 메모리를 삭제할 수 있음을 나타내지 않습니다. 이 경우 GC는 전체 참조 수프를보고 자신 만 참조하는 섬이 있는지 확인해야합니다. 오프 핸드, 나는 O (n ^ 2) 연산이라고 생각하지만, 그것이 무엇이든간에 성능에 관심이 있다면 나빠질 수 있습니다. (편집 : Martin B 는 합리적으로 효율적인 알고리즘의 경우 O (n)임을 지적 합니다. 성능에 관심이 있고 가비지 수집없이 일정한 시간에 할당을 취소 할 수 있으면 여전히 O (n)입니다.)

개인적으로 사람들이 C ++에 가비지 수집이 없다고 말하면 내 마음에 C ++의 기능으로 태그가 지정되지만 소수에 해당합니다. 아마도 사람들이 C와 C ++에서의 프로그래밍에 대해 배우기 가장 어려운 것은 포인터와 동적 메모리 할당을 올바르게 처리하는 방법 일 것입니다. 파이썬과 같은 다른 언어는 GC가 없으면 끔찍할 것이므로 언어에서 원하는 것에 달려 있다고 생각합니다. 신뢰할 수있는 성능을 원한다면 가비지 수집이없는 C ++이 Fortran의 유일한 측면입니다. 사용하기 쉽고 훈련 휠을 원한다면 ( "적절한"메모리 관리를 배우지 않고도 충돌을 피하기 위해) GC로 무언가를 선택하십시오. 메모리를 잘 관리하는 방법을 알고 있더라도 다른 코드를 최적화하는 데 시간을 절약 할 수 있습니다. 실제로 더 이상 성능 저하가 발생하지 않지만 신뢰할 수있는 성능 (및 커버 아래에서 진행 상황을 정확하게 알 수있는 기능)이 정말로 필요한 경우 C ++을 고수합니다. 내가 들어 본 모든 주요 게임 엔진이 C ++ (C 또는 어셈블리가 아닌 경우)에있는 이유가 있습니다. Python 등은 스크립팅에는 적합하지만 주요 게임 엔진에는 적합하지 않습니다.


원래 질문과 관련이 없지만 실제로는 스택의 위치와 힙이 거꾸로 있습니다. 일반적으로 스택은 커지고 힙은 커집니다 (힙이 실제로 "성장하지는 않지만 막대한 단순화").
P Daddy

나는이 질문이 다른 질문과 비슷하거나 심지어 복제품이라고 생각하지 않습니다. 이것은 특히 C ++에 관한 것이며 C ++에 존재하는 세 가지 저장 기간입니다. 정적 객체에 동적 객체를 할당 할 수 있습니다 (예 : 과부하 새 기능).
Johannes Schaub-litb

7
가비지 수집에 대한 당신의 거만한 취급은 도움이되지 못했습니다.
P Daddy

9
가비지 수집은 성능이 다른 방식으로 사용될 수있을 때 바로 발생할 수있는 메모리를 확보하는 것과는 달리 할 일이 거의 없을 때 발생하기 때문에 메모리를 수동으로 해제하는 것보다 오늘날 더 낫습니다.
Georg Schölly

3
간단한 설명-가비지 수집에는 O (n ^ 2) 복잡도가 없습니다 (실제로 성능이 저하 될 수 있음). 한 가비지 수집주기에 걸리는 시간은 힙 크기에 비례합니다 ( hpl.hp.com/personal/Hans_Boehm/gc/complexity.html 참조) .
Martin B

54

다음은 물론 모든 것이 정확하지는 않습니다. 읽을 때 소금 한알과 함께 가져 가십시오. :)

글쎄, 당신이 말하는 세 가지 것은 자동, 정적 및 동적 저장 기간 이며, 이것은 객체의 수명과 수명이 시작되는 시간 과 관련이 있습니다.


자동 저장 기간

당신은 자동 저장 기간 사용 짧은 수명작은 에만 필요 데이터를 로컬로 일부 블록 내를 :

if(some condition) {
    int a[3]; // array a has automatic storage duration
    fill_it(a);
    print_it(a);
}

수명은 블록을 종료하자마자 끝나고 객체가 정의 되 자마자 시작됩니다. 이들은 가장 간단한 종류의 스토리지 기간이며 특히 동적 스토리지 기간보다 훨씬 빠릅니다.


정적 저장 기간

사용 가능한 변수 (네임 스페이스 범위) 및 해당 범위의 종료 (로컬 범위)를 통해 수명을 연장해야하는 로컬 변수에 대해 모든 코드에서 항상 액세스 할 수있는 빈 변수에 대해 정적 저장 기간을 사용합니다. 클래스 (클래스 범위)의 모든 객체가 공유해야하는 멤버 변수 수명은 범위에 따라 다릅니다. 네임 스페이스 범위로컬 범위클래스 범위를 가질 수 있습니다 . 둘 다에 대한 진실은, 일단 그들의 삶이 시작되면 평생 은 프로그램의 끝에서 끝납니다 . 다음은 두 가지 예입니다.

// static storage duration. in global namespace scope
string globalA; 
int main() {
    foo();
    foo();
}

void foo() {
    // static storage duration. in local scope
    static string localA;
    localA += "ab"
    cout << localA;
}

이 프로그램의 인쇄는 ababab, 때문에 localA그 블록의 종료에 파괴되지 않는다. 제어 범위가 정의에 도달하면 로컬 범위를 가진 개체의 수명이 시작된다고 말할 수 있습니다 . 의 경우 localA함수 본문이 입력 될 때 발생합니다. 네임 스페이스 범위에있는 개체의 경우 수명은 프로그램 시작시 시작 됩니다. 클래스 범위의 정적 객체에 대해서도 마찬가지입니다.

class A {
    static string classScopeA;
};

string A::classScopeA;

A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;

보시다시피, classScopeA클래스의 특정 객체가 아니라 클래스 자체에 바인딩됩니다. 위의 세 이름 모두의 주소는 동일하며 모두 동일한 개체를 나타냅니다. 정적 객체가 언제 어떻게 초기화되는지에 대한 특별한 규칙이 있지만 지금은 걱정하지 마십시오. 이는 정적 초기화 순서 fiasco 라는 용어를 의미합니다 .


동적 저장 기간

마지막 저장 기간은 동적입니다. 다른 섬에 개체를 배치하고 해당 참조 주위에 포인터를 놓으려는 경우 사용합니다. 객체가 거나 런타임 에만 알려진 크기의 배열을 만들려 는 경우에도 사용합니다 . 이러한 유연성으로 인해 동적 스토리지 기간을 갖는 객체는 복잡하고 관리가 느립니다. 해당 동적 지속 시간을 가진 객체는 적절한 연산자 호출이 발생할 때 수명을 시작 합니다.

int main() {
    // the object that s points to has dynamic storage 
    // duration
    string *s = new string;
    // pass a pointer pointing to the object around. 
    // the object itself isn't touched
    foo(s);
    delete s;
}

void foo(string *s) {
    cout << s->size();
}

수명은 delete 를 호출 할 때만 종료됩니다 . 잊어 버린 경우 해당 개체의 수명이 끝나지 않습니다. 그리고 사용자 선언 생성자를 정의하는 클래스 객체에는 소멸자가 호출되지 않습니다. 동적 저장 시간을 갖는 객체는 수명 및 관련 메모리 리소스를 수동으로 처리해야합니다. 라이브러리는 쉽게 사용할 수 있도록 존재합니다. 스마트 포인터를 사용하여 특정 객체에 대한 명시 적 가비지 수집 을 설정할 수 있습니다.

int main() {
    shared_ptr<string> s(new string);
    foo(s);
}

void foo(shared_ptr<string> s) {
    cout << s->size();
}

객체를 참조하는 마지막 포인터가 범위를 벗어나면 delete 호출에 신경 쓸 필요가 없습니다. 공유 ptr이 자동으로 처리합니다. 공유 ptr 자체에는 자동 저장 기간이 있습니다. 그래서 수명이 자동으로 관리되어, 그것은 소멸자에서 동적 객체에 지적을 삭제할지 여부를 확인 할 수 있도록. shared_ptr 참조는 부스트 문서를 참조하십시오. http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm


39

"짧은 대답"과 같이 정교하게 언급되었습니다.

  • 정적 변수 (클래스)
    수명 = 프로그램 런타임 (1)
    가시성 = 액세스 수정 자에 의해 결정 (개인 / 보호 / 공용)

  • 정적 변수 (전역 범위)
    수명 = 프로그램 런타임 (1)
    가시성 = 인스턴스화되는 컴파일 단위 (2)

  • 힙 변수
    수명 = 사용자에 의해 정의 됨 (새로 삭제)
    가시성 = 사용자에 의해 정의 됨 (포인터를 할당 한 대상)

  • 스택 변수
    가시성 = 선언에서 범위가 종료 될 때까지
    수명 = 선언에서 선언 범위가 끝날 때까지


(1) 더 정확하게 : 초기화에서 컴파일 단위의 초기화 해제 (예 : C / C ++ 파일)까지. 컴파일 단위의 초기화 순서는 표준에 의해 정의되지 않습니다.

(2)주의 : 헤더에서 정적 변수를 인스턴스화하면 각 컴파일 단위가 자체 사본을 가져옵니다.


5

나는 pedants 중 하나가 곧 더 나은 답변을 얻을 것이라고 확신하지만 주요 차이점은 속도와 크기입니다.

스택

할당 속도가 훨씬 빠릅니다. 스택 프레임을 설정할 때 할당되므로 본질적으로 자유 로워지기 때문에 O (1)에서 수행됩니다. 단점은 스택 공간이 부족하면 뼈가 생깁니다. 스택 크기를 조정할 수 있지만 IIRC는 ~ 2MB를 가지고 있습니다. 또한 함수를 종료하자마자 스택의 모든 내용이 지워집니다. 따라서 나중에 참조하는 것이 문제가 될 수 있습니다. (할당 된 객체를 쌓는 포인터는 버그로 이어집니다.)

더미

할당 속도가 크게 느려집니다. 그러나 당신은 GB를 가지고 놀았습니다.

가비지 콜렉터

가비지 수집기는 백그라운드에서 실행되고 메모리를 비우는 일부 코드입니다. 힙에 메모리를 할당 할 때는 메모리 누수라고하는 메모리를 잊어 버리는 것이 매우 쉽습니다. 시간이 지남에 따라 응용 프로그램이 사용하는 메모리는 충돌 할 때까지 증가하고 증가합니다. 가비지 수집기가 주기적으로 더 이상 필요없는 메모리를 확보하면이 종류의 버그를 제거하는 데 도움이됩니다. 물론 이것은 가비지 수집기가 속도를 늦추기 때문에 가격이 책정됩니다.


3

정적 및 스택의 문제점은 무엇입니까?

"정적"할당의 문제점은 컴파일 타임에 할당이 수행된다는 것입니다.이 변수를 사용하여 가변 수의 데이터를 할당 할 수 없습니다.이 수는 런타임까지 알려지지 않았습니다.

"스택"에 할당 할 때의 문제점은 할당이 수행되는 서브 루틴이 반환되는 즉시 할당이 삭제된다는 것입니다.

힙에 변수를 할당하지 않고 전체 응용 프로그램을 작성할 수 있습니까?

아마도 사소하지 않고 일반적이며 큰 응용 프로그램은 아니지만 (소위 "임베디드"프로그램은 C ++의 하위 집합을 사용하여 힙없이 작성 될 수 있습니다).

가비지 수집기는 어떤 기능을 수행합니까?

응용 프로그램이 더 이상 참조하지 않을 때를 감지하기 위해 데이터 ( "표시 및 스윕")를 계속 감시합니다. 응용 프로그램이 데이터 할당을 해제 할 필요가 없기 때문에 이것은 응용 프로그램에 편리하지만 가비지 수집기는 계산 비용이 많이들 수 있습니다.

가비지 콜렉터는 일반적인 C ++ 프로그래밍 기능이 아닙니다.

이 가비지 수집기를 사용하여 수행 할 수없는 메모리를 직접 조작 할 수 있습니까?

결정 론적 메모리 할당 해제를위한 C ++ 메커니즘을 배우십시오.

  • '정적': 할당 해제되지 않음
  • 'stack': 변수가 "범위를 벗어남"하자마자
  • 'heap': 포인터가 삭제 될 때 (어플리케이션에 의해 명시 적으로 삭제되거나 일부 서브 루틴 내에서 암시 적으로 삭제됨)

1

스택이 너무 깊고 스택 할당에 사용 가능한 메모리가 오버플로되면 스택 메모리 할당 (함수 변수, 로컬 변수)이 문제가 될 수 있습니다. 힙은 여러 스레드에서 또는 프로그램 수명주기 내내 액세스해야하는 객체를위한 것입니다. 힙을 사용하지 않고 전체 프로그램을 작성할 수 있습니다.

가비지 수집기없이 메모리를 매우 쉽게 누출 할 수 있지만 개체와 메모리가 해제 될 때 지시 할 수도 있습니다. Java가 GC를 실행할 때 문제가 발생했으며 GC가 독점 스레드이므로 다른 프로세스를 실행할 수 없기 때문에 실시간 프로세스가 있습니다. 따라서 성능이 중요하고 누출 된 객체가 없음을 보장 할 수 있다면 GC를 사용하지 않는 것이 매우 도움이됩니다. 그렇지 않으면 응용 프로그램이 메모리를 소비하고 누출의 원인을 추적해야 할 때 생명을 싫어합니다.


1

프로그램이 할당 할 메모리 양을 미리 알지 못하는 경우 (따라서 스택 변수를 사용할 수 없음) 링크 된 목록이라고하면 목록의 크기를 미리 몰라도 목록이 커질 수 있습니다. 따라서 힙에 할당하는 것은 얼마나 많은 요소가 삽입되는지 알지 못하는 경우 연결된 목록에 적합합니다.


0

어떤 상황에서 GC의 장점은 다른 상황에서 성가신 것입니다. GC에 대한 의존은 그것에 대해 많이 생각하지 않도록 권장합니다. 이론적으로 '유휴'기간까지 또는 앱이 대역폭을 훔쳐 응답 지연을 유발할 때까지 반드시 대기해야합니다.

그러나 '생각하지 않아도됩니다'. 멀티 스레드 앱의 다른 모든 것과 마찬가지로 양보 할 수 있으면 양보 할 수 있습니다. 예를 들어, .Net에서는 GC를 요청할 수 있습니다. 이렇게하면 GC를 덜 자주 실행하는 대신 GC를 더 자주 실행하고이 오버 헤드와 관련된 대기 시간을 분산시킬 수 있습니다.

그러나 이것은 "자동 매트이기 때문에 그것에 대해 많이 생각할 필요가없는 것으로 보이는"GC의 주요 매력을 물리 친다.

GC가 널리 퍼지기 전에 프로그래밍에 처음 노출되었고 malloc / free 및 new / delete에 익숙한 경우 GC가 약간 성가 시거나 불신 할 수 있습니다 ( ' 많은 앱이 임의의 대기 시간을 허용합니다. 그러나 무작위 대기 시간이 허용되지 않는 응용 프로그램의 경우 GC 환경을 피하고 순전히 관리되지 않는 코드 (또는 신의 금지, 오랜 죽음의 예술, 어셈블리 언어)의 방향으로 이동하는 것이 일반적인 반응입니다.

나는 여름 방학을 보냈고, GC에 젖어 있던 인턴, 똑똑한 아이가 있었다. 그는 관리되지 않는 C / C ++로 프로그래밍 할 때도 "현대 프로그래밍 언어에서는이 작업을 수행 할 필요가 없기 때문에 malloc / free new / delete 모델을 따르기를 거부 한 GC의 우수성에 대해 매우 단호했다. 그리고 당신은 알고 있습니까? 작고 짧은 실행 앱의 경우 실제로 그 거리를 벗어날 수 있지만 오래 실행되는 실행 앱은 아닙니다.


0

스택은 컴파일러가 프로그램을 컴파일 할 때마다 컴파일러가 할당 한 메모리이며 기본 컴파일러는 OS에서 일부 메모리를 할당합니다 (IDE의 컴파일러 설정에서 설정을 변경할 수 있음) .OS는 메모리를 제공하는 메모리입니다. 시스템에서 사용 가능한 많은 메모리와 다른 많은 것들에서 스택 메모리에 오는 것은 복사 할 변수를 선언 할 때 할당됩니다 (공식으로 참조) 해당 변수는 스택에 푸시되어 Visual Studio에서 기본적으로 CDECL에 따라 일부 명명 규칙을 따릅니다. 예 : 접두사 표기법 : c = a + b; 스택 푸시는 오른쪽에서 왼쪽으로 밀어 넣기, b에서 스택, 연산자, a에서 스택 및 i, ec의 스택 결과입니다. 사전 수정 표기법 : = + cab 여기에서 모든 변수가 1st (오른쪽에서 왼쪽으로) 쌓 이도록 푸시 된 다음 작업이 수행됩니다. 컴파일러가 할당 한이 메모리는 고정되어 있습니다. 따라서 1MB의 메모리가 응용 프로그램에 할당되었다고 가정하고 700kb의 메모리를 사용하는 변수 (동적 할당되지 않은 한 모든 로컬 변수가 스택으로 푸시 됨)를 말하면 나머지 324kb 메모리가 힙에 할당됩니다. 그리고이 스택은 수명이 짧아 함수의 범위가 끝나면이 스택이 지워집니다.

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