C ++에서 쓰레기는 어떻게됩니까?


52

Java에는 자동 중지 기능이있어 가끔 세상을 멈추지 만 힙의 가비지를 처리합니다. 이제 C / C ++ 애플리케이션에는 이러한 STW 동결이 없으며 메모리 사용량도 무한대로 증가하지 않습니다. 이 동작은 어떻게 이루어 집니까? 죽은 물체는 어떻게 관리됩니까?


38
참고 : stop-the-world는 일부 가비지 수집기의 구현 선택이지만 모든 것은 아닙니다. 예를 들어 뮤 테이터와 동시에 실행되는 동시 GC가 있습니다 (GC 개발자가 실제 프로그램이라고 함). 동시 일시 정지 콜렉터가있는 상용 버전의 IBM 오픈 소스 JVM J9를 구입할 수 있다고 생각합니다. Azul Zing에는 " 일시 정지 되지 않는"수집기가있어 실제로 일시 정지 되지는 않지만 매우 빠르기 때문에 눈에 띄는 일시 정지 가 없습니다 (GC 일시 정지는 운영 체제 스레드 컨텍스트 스위치와 같은 순서이며 일반적으로 일시 정지로 표시되지 않음) .
Jörg W Mittag 2016 년

14
내가 사용하는 (장시간 실행) C ++ 프로그램의 대부분은 시간이 지남에 제한없이 증가 메모리 사용량이있다. 한 번에 며칠 이상 프로그램을 열어 두는 습관이 없습니까?
Jonathan Cast

12
스마트 포인터를 통해 동적 메모리를 관리 할 수 ​​있으므로 최신 C ++ 및 해당 구문을 사용하면 더 이상 메모리를 수동으로 삭제할 필요가 없습니다 (특별 최적화를 수행하지 않은 경우 제외). 분명히, 그것은 C ++ 개발에 약간의 오버 헤드를 추가하고 조금 더 조심해야하지만 완전히 다른 것은 아닙니다 new. 매뉴얼을 호출하는 대신 스마트 포인터 구문을 사용해야한다는 것을 기억해야합니다 .
Andy

9
가비지 수집 언어로 메모리 누수가 여전히 발생할 수 있습니다. Java에 익숙하지 않지만 불행히도 관리되는 GC .NET 세계에서 메모리 누수가 매우 일반적입니다. 정적 필드에서 간접적으로 참조하는 객체는 자동으로 수집되지 않으며, 이벤트 처리기는 매우 일반적인 누수 원인이며 가비지 수집의 비 결정적 특성으로 인해 리소스를 수동으로 해제 할 필요가 없습니다 (IDisposable로 이어짐) 무늬). 적절하게 사용 된 C ++ 메모리 관리 모델은 가비지 수집보다 훨씬 우수합니다.
코디 그레이

26
What happens to garbage in C++? 일반적으로 실행 파일로 컴파일되지 않습니까?
BJ Myers

답변:


101

프로그래머 new는를 통해 생성 된 객체가를 통해 삭제 되도록해야합니다 delete. 객체가 생성되었지만 마지막 포인터 또는 객체에 대한 참조가 범위를 벗어나기 전에 파괴되지 않으면 객체는 균열을 통해 떨어져 메모리 누수가 됩니다.

불행히도 C, C ++ 및 GC를 포함하지 않는 다른 언어의 경우 시간이 지남에 따라 쌓입니다. 응용 프로그램이나 시스템의 메모리가 부족하여 새 메모리 블록을 할당 할 수 없습니다. 이 시점에서 사용자는 운영 체제가 사용한 메모리를 회수 할 수 있도록 응용 프로그램을 종료해야합니다.

이 문제를 해결하는 한, 프로그래머의 삶을 훨씬 쉽게 만들어주는 몇 가지가 있습니다. 이것들은 주로 범위 의 특성에 의해 지원됩니다 .

int main()
{
    int* variableThatIsAPointer = new int;
    int variableInt = 0;

    delete variableThatIsAPointer;
}

여기서 우리는 두 개의 변수를 만들었습니다. 중괄호 로 정의 된대로 블록 범위에 존재합니다 {}. 실행이이 범위를 벗어나면 이러한 개체가 자동으로 삭제됩니다. 이 경우, variableThatIsAPointer이름에서 알 수 있듯이 메모리의 객체에 대한 포인터입니다. 범위를 벗어나면 포인터가 삭제되지만 가리키는 객체는 그대로 유지됩니다. 여기서는 delete메모리 누수가 없는지 확인하기 위해 범위를 벗어나기 전에이 개체를 사용합니다. 그러나이 포인터를 다른 곳으로 전달하여 나중에 삭제할 것으로 예상했습니다.

이러한 범위의 특성은 클래스로 확장됩니다.

class Foo
{
public:
    int bar; // Will be deleted when Foo is deleted
    int* otherBar; // Still need to call delete
}

여기에도 같은 원칙이 적용됩니다. bar언제 Foo삭제 되는지 걱정할 필요가 없습니다 . 그러나의 otherBar경우 포인터 만 삭제됩니다. otherBar그것이 가리키는 객체에 대한 유일한 유효한 포인터 라면 , 아마도 소멸자 delete에 있어야합니다 Foo. 이것이 RAII 의 원동력입니다

자원 할당 (획득)은 생성자에 의해 객체 생성 (구체적으로 초기화) 동안 수행되는 반면, 자원 할당 해제 (해제)는 소멸자에 의해 객체 파괴 (구체적으로 최종화) 동안 수행됩니다. 따라서 초기화가 완료되고 종료가 시작될 때 (리소스는 클래스가 변하지 않음) 사이에 리소스가 유지되고 개체가 살아있을 때만 유지됩니다. 따라서 객체 누출이 없으면 리소스 누출이 없습니다.

RAII는 또한 Smart Pointers 의 전형적인 원동력 입니다. C ++ 표준 라이브러리에서 이들은 std::shared_ptr, std::unique_ptr그리고 std::weak_ptr; 비록 같은 개념을 따르는 다른 shared_ptr/ weak_ptr구현을 보고 사용했지만 . 이를 위해, 참조 카운터는 주어진 객체에 대한 포인터의 수를 추적하고 delete더 이상 참조가 없으면 객체를 자동으로 추적합니다 .

그 외에도 프로그래머가 코드를 사용하여 객체를 올바르게 처리하도록하는 것은 올바른 관행과 규율에 달려 있습니다.


4
통해 삭제 delete-그게 내가 찾던 것입니다. 대박.
Ju Shua

3
c ++에서 제공하는 범위 지정 메커니즘에 대해 추가하여 새로운 기능과 삭제 기능을 대부분 자동으로 수행 할 수 있습니다.
whatsisname

9
@whatsisname 새롭고 삭제되는 것이 자동적이지는 않습니다. 많은 경우에 전혀 발생하지 않습니다.
Caleth

10
delete자동으로 당신을 위해 호출 스마트 포인터 당신이 그들을 사용하는 경우 자동 스토리지를 사용할 수 없을 때 당신은 그들에게 모든 시간을 사용하는 것이 좋습니다 있도록.
Marian Spanik 2016 년

11
@JuShua 현대 C ++을 작성할 때 실제로 delete응용 프로그램 코드 (및 C ++ 14 이상과 동일 new)를 가질 필요는 없지만 대신 스마트 포인터와 RAII를 사용하여 힙 객체를 삭제하십시오. std::unique_ptr형태와 std::make_unique기능의 직접, 간단한 교환입니다 newdelete응용 프로그램 코드 수준에서.
hyde

83

C ++에는 가비지 콜렉션이 없습니다.

C ++ 응용 프로그램은 자체 쓰레기를 처리해야합니다.

이를 이해하려면 C ++ 애플리케이션 프로그래머가 필요합니다.

그들이 잊었을 때, 결과를 "메모리 누수"라고합니다.


22
당신은 분명히 당신의 대답에 쓰레기 나 보일러 플레이트가 포함되어 있지 않은지 확인했습니다 ...
leftaroundabout

15
@leftaroundabout : 감사합니다. 칭찬이라고 생각합니다.
John R. Strohm 2016 년

1
OK이 가비가없는 답변에는 키워드 : 메모리 누수를 검색해야합니다. 또한 어떻게 든 언급 new하고 좋을 것 delete입니다.
Ruslan

4
같은도 적용 @Ruslan malloc하고 free, 또는 new[]delete[](윈도우의 같은 또는 다른 할당 자 GlobalAlloc, LocalAlloc, SHAlloc, CoTaskMemAlloc, VirtualAlloc, HeapAlloc, 및 메모리 당신을 위해 할당, ...) (예를 통해 fopen).
user253751

43

가비지 콜렉터가없는 C, C ++ 및 기타 시스템에서 개발자는 메모리를 확보 할 수있는시기를 표시하기 위해 언어 및 라이브러리별로 기능을 제공합니다.

가장 기본적인 기능은 자동 스토리지 입니다. 언어 자체는 여러 번 다음 항목을 처리합니다.

int global = 0; // automatic storage

int foo(int a, int b) {
    static int local = 1; // automatic storage

    int c = a + b; // automatic storage

    return c;
}

이 경우 컴파일러는 해당 값이 사용되지 않는시기를 알고 해당 값과 연관된 스토리지를 회수해야합니다.

C에서 동적 저장소를 사용할 때는 일반적으로 메모리가 할당되고 메모리가 malloc회수됩니다 free. C ++에서, 메모리는 전통적으로로 할당 new되고 회수됩니다 delete.

C 그러나 현대의 C ++은 피하고, 많은 년간 변경되지 않은 newdelete완전하고 (자신이 사용하는 대신 라이브러리 시설에 의존 new하고 delete적절하게) :

  • 스마트 포인터는 가장 유명한 : std::unique_ptrstd::shared_ptr
  • :하지만 용기는 실제로는 훨씬 더 널리 퍼져 std::string, std::vector, std::map투명, ... 모든 내부적으로 동적으로 할당 된 메모리 관리

에 대해 말하면 shared_ptr, 위험이 있습니다. 참조주기가 형성되고 끊어지지 않으면 메모리 누수가 발생할 수 있습니다. 이 상황을 피하는 것은 개발자의 몫입니다. 가장 간단한 방법은 shared_ptr완전히 피하는 것이고 두 번째는 유형 수준에서주기를 피하는 것입니다.

결과적으로 , 또는 을 사용하지 않는 한 새로운 사용자에게도 메모리 누수가 C ++에서 문제가되지 않습니다 . 이것은 철저한 훈련이 필요하고 일반적으로 불충분 한 C와는 다릅니다.newdeletestd::shared_ptr


그러나이 누수는 메모리 누수의 쌍둥이 자매 인 dangling pointers 를 언급하지 않으면 완료되지 않습니다 .

매달려있는 포인터 (또는 매달려있는 참조)는 포인터 나 죽은 개체에 대한 참조를 유지함으로써 생성되는 위험입니다. 예를 들면 다음과 같습니다.

int main() {
    std::vector<int> vec;
    vec.push_back(1);     // vec: [1]

    int& a = vec.back();

    vec.pop_back();       // vec: [], "a" is now dangling

    std::cout << a << "\n";
}

매달려있는 포인터 또는 참조를 사용하는 것은 정의되지 않은 동작 입니다. 운 좋게도 이것은 즉각적인 충돌입니다. 불행히도, 이로 인해 메모리가 먼저 손상되고 때때로 컴파일러가 이상한 코드를 생성하기 때문에 이상한 동작이 발생합니다.

정의되지 않은 동작 은 현재 C / C ++에서 보안 / 프로그램의 정확성 측면에서 가장 큰 문제입니다. 가비지 콜렉터가없고 정의되지 않은 동작이없는 언어에 대해 Rust를 체크 아웃 할 수 있습니다.


17
다시 : "매달려 포인터 또는 참조를 사용하는 것은 정의되지 않은 동작 입니다. 일반적으로 운 좋게도 이것은 즉시 충돌입니다": 정말? 그것은 내 경험과 전혀 일치하지 않습니다. 반대로, 내 경험에 따르면 매달려있는 포인터를 사용 하면 즉각적인 충돌이 거의 발생 하지 않습니다 . . .
ruakh

9
예, "매달리기"때문에 포인터는 한 시점에서 이전에 할당 된 메모리를 대상으로해야했으며, 해당 메모리는 더 이상 액세스 할 수 없도록 프로세스에서 완전히 매핑 해제되지 않았을 가능성이 높습니다. 실제로 재사용 할 수있는 좋은 후보입니다 ... 실제로 매달린 포인터는 충돌을 일으키지 않으며 혼란을 유발합니다.
Leushenko 2016 년

2
"결과적으로 메모리 누수가 C ++에서는 문제가되지 않습니다."물론, 재귀 적 shared_ptrs 또는 재귀 적 unique_ptrs 및 기타 상황뿐만 아니라 라이브러리에 대한 C 바인딩도 항상 존재합니다.
Mooing Duck

3
“C ++에서는 새로운 사용자에게도 문제가되지 않습니다.”–“ Java와 같은 언어 나 C를 사용하지 않는 새로운 사용자 ”에 해당됩니다.
leftaroundabout

3
@leftaroundabout : 그것은 "그들이 사용을 자제만큼 자격의 new, delete그리고 shared_ptr"; 없이 new그리고 shared_ptr당신은 아무 누수 때문에 직접 소유권을 가지고있다. 물론, 당신은 매달려있는 포인터 등을 가지고있을 것입니다 ...하지만 그것들을 제거하기 위해 C ++을 떠나야 할 것을 두려워합니다.
Matthieu M.

27

C ++에는 RAII 라는 것이 있습니다. 기본적으로 쓰레기를 쌓아 두지 않고 청소할 때 쓰레기를 깨끗이 정리하는 것이 중요합니다. (내 방에서 축구를보고 나를 상상하십시오-맥주 캔을 마시고 새로운 맥주를 필요로 할 때 C ++ 방법은 빈 캔을 냉장고로가는 빈으로 가져가는 것입니다 .C # 방법은 바닥에 쥐어 놓는 것입니다 하녀가 청소를 할 때 데리러 올 때까지 기다리십시오.

이제 C ++에서 메모리를 누출 할 수는 있지만 일반적인 구문을 그대로두고 C 블록 방식으로 되돌려 야합니다. 메모리 블록을 할당하고 언어 지원이없는 블록을 추적합니다. 어떤 사람들은이 포인터를 잊어 버려 블록을 제거 할 수 없습니다.


9
RAII를 사용하는 공유 포인터는 누출을 생성하는 현대적인 방법을 제공합니다. 객체 A와 B가 공유 포인터를 통해 서로를 참조하고 객체 A 나 객체 B를 참조하는 것이 없다고 가정합니다. 결과적으로 누출이 발생합니다. 이 상호 참조는 가비지 콜렉션이있는 언어에서는 문제가되지 않습니다.
David Hammen 2016 년

@DavidHammen은 확실하지만 거의 모든 객체를 통과해야합니다. 스마트 포인터의 예는 스마트 포인터 자체가 범위를 벗어난 다음 개체가 해제된다는 사실을 무시합니다. 스마트 포인터는 대부분의 매개 변수처럼 스택에 전달되는 객체가 아니라 포인터와 같다고 가정합니다. 이것은 GC 언어에서 발생하는 메모리 누수와 크게 다르지 않습니다. 예를 들어 UI 클래스에서 이벤트 핸들러를 제거하면 자동으로 참조되어 누출되는 경우가 있습니다.
gbjbaanb 2016 년

1
스마트 포인터의 예에서 @gbjbaanb 아니하며 스마트 포인터가 지금까지 누출이 왜 범위를 벗어나, 그건. 두 스마트 포인터 객체 모두 어휘가 아닌 동적 범위에 할당 되므로 각각 파괴하기 전에 다른 것을 기다립니다. 스마트 포인터가 포인터가 아닌 C ++의 실제 객체라는 사실은 여기서 누출을 일으키는 원인입니다 . 컨테이너 객체를 가리키는 스택 범위 의 추가 스마트 포인터 객체는 참조 횟수가 커서 스스로 파괴 할 때 할당 할 수 없습니다 0이 아닙니다.
Leushenko 2016 년

2
.NET 방식은 바닥에 하지 않습니다 . 하녀가 돌아올 때까지 그대로 유지합니다. 그리고 .NET이 실제로 메모리를 할당하는 방식 (계약이 아님)으로 인해 힙은 랜덤 액세스 스택과 비슷합니다. 그것은 일종의 계약서와 서류를 가지고 더 이상 유효하지 않은 것들을 폐기하기 위해 한 번에 통과하는 것과 같습니다. 그리고 이것을 더 쉽게하기 위해, 각 버림에서 살아남은 것들은 다른 스택으로 승격되어, 대부분의 스택을 순회하지 않도록 할 수 있습니다-첫 번째 스택이 충분히 커지지 않으면 하녀는 다른 스택을 건드리지 않습니다.
Luaan

@Luaan 그것은 비유였습니다 ... 하녀가 청소 할 때까지 테이블에 누워있는 캔을 남겨두면 더 행복 할 것 같아요.
gbjbaanb

26

C ++의 경우 "수동 메모리 관리를 수행해야합니다"라는 일반적인 오해라는 점에 유의해야합니다. 사실, 코드에서는 일반적으로 메모리 관리를 수행하지 않습니다.

고정 크기 객체 (범위 수명)

객체가 필요한 대부분의 경우 객체는 프로그램에서 정의 된 수명을 가지며 스택에 생성됩니다. 이것은 모든 내장 기본 데이터 유형뿐만 아니라 클래스 및 구조체의 인스턴스에서도 작동합니다.

class MyObject {
    public: int x;
};

int objTest()
{
    MyObject obj;
    obj.x = 5;
    return obj.x;
}

함수가 끝나면 스택 객체가 자동으로 제거됩니다. Java에서 오브젝트는 항상 힙에 작성되므로 가비지 콜렉션과 같은 메커니즘으로 제거해야합니다. 이것은 스택 객체의 문제가 아닙니다.

동적 데이터를 관리하는 객체 (범위 수명)

스택에 공간을 사용하면 고정 된 크기의 객체에 적합합니다. 배열과 같이 가변적 인 공간이 필요한 경우 다른 접근 방식이 사용됩니다. 목록은 동적 메모리를 관리하는 고정 크기의 개체로 캡슐화됩니다. 이것은 객체가 특별한 정리 기능인 소멸자를 가질 수 있기 때문에 작동합니다. 객체가 범위를 벗어나고 생성자의 반대 작업을 수행하면 호출됩니다.

class MyList {        
public:
    // a fixed-size pointer to the actual memory.
    int* listOfInts; 
    // constructor: get memory
    MyList(size_t numElements) { listOfInts = new int[numElements]; }
    // destructor: free memory
    ~MyList() { delete[] listOfInts; }
};

int listTest()
{
    MyList list(1024);
    list.listOfInts[200] = 5;
    return list.listOfInts[200];
    // When MyList goes off stack here, its destructor is called and frees the memory.
}

메모리가 사용되는 코드에는 메모리 관리가 전혀 없습니다. 우리가 확인해야 할 유일한 것은 우리가 작성한 객체가 적절한 소멸자를 가지고 있다는 것입니다. 우리가 범위를 벗어나는 방식에 관계없이 listTest예외를 통해 또는 단순히 반환하여 소멸자 ~MyList()가 호출되며 메모리를 관리 할 필요가 없습니다.

( 이진 NOT 연산자 를 사용하여 ~소멸자를 표시하는 것은 재미있는 디자인 결정이라고 생각합니다 . 숫자에 사용하면 비트가 반전됩니다. 유사하게는 생성자가 수행 한 작업이 반전되었음을 나타냅니다.)

기본적으로 동적 메모리가 필요한 모든 C ++ 객체는이 캡슐화를 사용합니다. RAII ( "자원 획득은 초기화")라고 불리며, 이는 객체가 자신의 컨텐츠에 관심을 갖고 있다는 간단한 아이디어를 표현하는 아주 이상한 방법입니다. 그들이 얻는 것은 청소하는 것입니다.

다형성 개체 및 범위를 벗어난 수명

이제이 두 경우 모두 수명이 명확하게 정의 된 메모리에 대한 것입니다. 수명은 범위와 같습니다. 스코프를 떠날 때 객체가 만료되지 않게하려면 메모리를 관리 할 수있는 세 번째 메커니즘 인 스마트 포인터가 있습니다. 스마트 포인터는 런타임에 유형이 다르지만 공통 인터페이스 또는 기본 클래스가있는 객체 인스턴스가있는 경우에도 사용됩니다.

class MyDerivedObject : public MyObject {
    public: int y;
};
std::unique_ptr<MyObject> createObject()
{
    // actually creates an object of a derived class,
    // but the user doesn't need to know this.
    return std::make_unique<MyDerivedObject>();
}

int dynamicObjTest()
{
    std::unique_ptr<MyObject> obj = createObject();
    obj->x = 5;
    return obj->x;
    // At scope end, the unique_ptr automatically removes the object it contains,
    // calling its destructor if it has one.
}

std::shared_ptr여러 클라이언트간에 객체를 공유하기위한 또 다른 종류의 스마트 포인터가 있습니다. 마지막 클라이언트가 범위를 벗어날 때만 포함 된 개체를 삭제하므로 클라이언트가 몇 개인 지 얼마나 오래 사용할지 완전히 알 수없는 상황에서 사용할 수 있습니다.

요약하면, 실제로 수동 메모리 관리를 수행하지 않는 것으로 나타났습니다. 모든 것이 캡슐화되고 완전히 자동적 인 스코프 기반 메모리 관리를 통해 관리됩니다. 이것으로 충분하지 않은 경우, 원시 메모리를 캡슐화하는 스마트 포인터가 사용됩니다.

delete예외가 발생했을 때 관리하기가 거의 불가능하고 일반적으로 안전하게 사용하기 어렵 기 때문에 원시 포인터를 C ++ 코드의 어디에서나 리소스 소유자로 사용하는 것, 생성자 외부의 원시 할당 및 소멸자 외부의 원시 호출 을 사용하는 것은 매우 나쁜 습관으로 간주 됩니다.

최고 : 모든 유형의 리소스에 적용

RAII의 가장 큰 장점 중 하나는 메모리에만 국한되지 않는다는 것입니다. 실제로 파일과 소켓 (열기 / 닫기)과 같은 리소스와 뮤텍스 (잠금 / 잠금 해제)와 같은 동기화 메커니즘을 관리하는 매우 자연스러운 방법을 제공합니다. 기본적으로 수집 및 해제해야하는 모든 리소스는 C ++에서 정확히 동일한 방식으로 관리되며이 관리는 사용자에게 맡겨지지 않습니다. 생성자에서 획득하고 소멸자에서 해제하는 클래스로 모두 캡슐화됩니다.

예를 들어, 뮤텍스를 잠그는 함수는 일반적으로 C ++에서 다음과 같이 작성됩니다.

void criticalSection() {
    std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
    doSynchronizedStuff();
} // myMutex is released here automatically

다른 언어는 수동으로 (예를 들어 finally절에서) 요구 하거나이 문제를 해결하는 특수 메커니즘을 생성하지만 특히 우아한 방법으로 (일반적으로 인생의 후반에 충분한 사람들이 단점으로 고통 받았다). 이러한 메커니즘은 Java에서 리소스사용하여 시도 하고 C #에서 using 문을 사용 하며 둘 다 C ++의 RAII와 비슷합니다.

요약하자면,이 모든 것이 C ++에서 RAII에 대한 매우 피상적 ​​인 설명 이었지만, 독자들이 C ++에서 메모리와 리소스 관리가 일반적으로 "수동"이 아니라 실제로 대부분 자동이라는 것을 이해하는 데 도움이되기를 바랍니다.


7
이것은 사람들에게 오해를주지 않거나 C ++을 실제보다 더 어렵거나 위험하게 칠하지 않는 유일한 대답입니다.
Alexander Revo 2016 년

6
BTW, 원시 포인터를 리소스 소유자로 사용하는 것은 나쁜 습관으로 간주됩니다. 포인터 자체보다 오래 지속될 수있는 것을 가리키면 사용하는 데 아무런 문제가 없습니다.
Alexander Revo 2016 년

8
나는 두 번째 알렉산더입니다. "C ++에는 자동 메모리 관리 기능이없고, 잊어 버리고 delete죽었다"라는 대답이 30 점 이상으로 급등하여 수용되는 것을보고 당황했습니다 . 누구든지 실제로 C ++을 사용합니까?
Quentin

8

특히 C와 관련하여이 언어는 동적으로 할당 된 메모리를 관리하는 도구를 제공하지 않습니다. 모든 사람 *allocfree어딘가에 해당 하는지 확인하는 것은 전적으로 귀하의 책임입니다 .

자원 할당이 실제로 실패하는 경우는 자원 할당이 중간에 실패 할 때입니다. 다시 시도합니까, 롤백하고 처음부터 다시 시작합니까, 롤백하고 오류가 발생했는지, 그냥 구제하고 OS가 처리하도록 하시겠습니까?

예를 들어, 인접하지 않은 2D 배열을 할당하는 함수가 있습니다. 여기서 동작은 프로세스 중간에 할당 실패가 발생하면 모든 것을 롤백하고 NULL 포인터를 사용하여 오류 표시를 반환한다는 것입니다.

/**
 * Allocate space for an array of arrays; returns NULL
 * on error.
 */
int **newArr( size_t rows, size_t cols )
{
  int **arr = malloc( sizeof *arr * rows );
  size_t i;

  if ( arr ) // malloc returns NULL on failure
  {
    for ( i = 0; i < rows; i++ )
    {
      arr[i] = malloc( sizeof *arr[i] * cols );
      if ( !arr[i] )
      {
        /**
         * Whoopsie; we can't allocate any more memory for some reason.
         * We can't just return NULL at this point since we'll lose access
         * to the previously allocated memory, so we branch to some cleanup
         * code to undo the allocations made so far.  
         */
        goto cleanup;
      }
    }
  }
  goto done;

/**
 * We encountered a failure midway through memory allocation,
 * so we roll back all previous allocations and return NULL.
 */
cleanup:
  while ( i )         // this is why we didn't limit the scope of i to the for loop
    free( arr[--i] ); // delete previously allocated rows
  free( arr );        // delete arr object
  arr = NULL;

done:
  return arr;
}

이 코드는 엉덩이 못생긴 사람들과 goto이 거의 그냥 완전히 구제하지 않고 문제를 해결하는 유일한 방법입니다,없는, 구조화 된 예외 처리 메커니즘의 어떤 종류의, 그러나 특히 자원 할당 코드가 중첩 된 경우 더 하나의 루프 깊이보다. 이것은 goto실제로 매력적인 옵션 인 몇 안되는 시간 중 하나입니다 . 그렇지 않으면 많은 플래그와 추가 if명령문을 사용하고 있습니다.

각 자원에 대해 전용 할당 자 / 할당 자 기능을 작성하여보다 쉽게 ​​생활을 할 수 있습니다.

Foo *newFoo( void )
{
  Foo *foo = malloc( sizeof *foo );
  if ( foo )
  {
    foo->bar = newBar();
    if ( !foo->bar ) goto cleanupBar;
    foo->bletch = newBletch(); 
    if ( !foo->bletch ) goto cleanupBletch;
    ...
  }
  goto done;

cleanupBletch:
  deleteBar( foo->bar );
  // fall through to clean up the rest

cleanupBar:
  free( foo );
  foo = NULL;

done:
  return foo;
}

void deleteFoo( Foo *f )
{
  deleteBar( f->bar );
  deleteBletch( f->bletch );
  free( f );
}

1
이것은 goto진술 에도 불구하고 좋은 대답 입니다. 일부 지역에서는 권장되는 방법입니다. C에서 예외와 동등한 것을 방지하기 위해 일반적으로 사용되는 체계입니다. goto문장 이 가득 하고 유출되지 않는 Linux 커널 코드를 살펴보십시오 .
David Hammen 2016 년

공정하게 말해서 C에 대해 이야기하고 싶다면 이것은 아마도 좋은 습관 일 것입니다. C는 다른 곳에서 온 메모리 블록을 처리 하거나 작은 메모리 청크를 다른 프로 시저로 파싱하지만 인터리브 방식으로 동시에 두 가지를 수행하지 않는 데 가장 적합한 언어 입니다. C에서 고전적인 "객체"를 사용하는 경우 언어를 강점으로 사용하지 않을 수 있습니다.
Leushenko 2016 년

두 번째 goto는 외부입니다. 당신이 변경되었을 경우 그것은 더 읽을 것 goto done;return arr;arr=NULL;done:return arr;return NULL;. 더 복잡한 경우에는 실제로 여러 단계가있을 수 있지만 goto다른 수준의 준비 상태에서 풀기 시작합니다 (C ++에서 예외 스택 해제로 수행되는 작업).
Ruslan

2

메모리 문제를 여러 가지 범주로 분류하는 법을 배웠습니다.

  • 한 번 물이 뚝뚝 떨어진다 시작시 프로그램이 100 바이트를 누설한다고 가정하고 다시는 누설하지 마십시오. 이러한 일회성 누출을 추적하고 제거하는 것은 좋지만 (누출 탐지 기능으로 깨끗한 보고서를 작성하는 것을 좋아합니다) 필수는 아닙니다. 때로는 공격을 받아야하는 더 큰 문제가 있습니다.

  • 반복되는 누출. 프로그램 수명 동안 반복적으로 호출되는 기능으로, 정기적으로 메모리가 큰 문제가됩니다. 이 드립은 프로그램과 OS를 고문하여 죽게 할 것입니다.

  • 상호 참조. 객체 A와 B가 공유 포인터를 통해 서로 참조하는 경우 해당 클래스의 디자인 또는 해당 클래스를 구현 / 사용하여 순환 성을 깨뜨리는 코드에서 특별한 작업을 수행해야합니다. (가비지 수집 언어에는 문제가되지 않습니다.)

  • 너무 많이 기억하고 있습니다. 이것은 쓰레기 / 메모리 누수의 사촌입니다. RAII는 여기서 도움이되지 않으며 가비지 수집도하지 않습니다. 이것은 모든 언어에서 문제입니다. 일부 활성 변수에 임의의 메모리 청크에 연결하는 경로가있는 경우 해당 임의의 메모리 청크는 가비지가 아닙니다. 프로그램을 잊어 버려 며칠 동안 실행할 수있게 만드는 것은 까다 롭습니다. 디스크가 고장날 때까지 몇 달 동안 실행할 수있는 프로그램을 만드는 것은 매우 까다로운 작업입니다.

오랫동안 누출에 심각한 문제가 없었습니다. C ++에서 RAII를 사용하면 이러한 누수 및 누출 문제를 해결하는 데 크게 도움이됩니다. 더 중요한 것은 더 이상 사용하지 않는 메모리에 대한 끊어지지 않은 연결로 인해 메모리 사용이 계속 증가하고 증가하는 응용 프로그램에 문제가 있다는 것입니다.


-6

필요한 경우 자신의 가비지 수집 양식을 구현하는 것은 C ++ 프로그래머의 책임입니다. 그렇지 않으면 '메모리 누수'가 발생합니다. Java와 같은 '고수준'언어가 가비지 콜렉션을 내장하는 것이 일반적이지만 C 및 C ++와 같은 '저수준'언어는 그렇지 않습니다.

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