'new'를 사용하면 왜 메모리 누수가 발생합니까?


131

먼저 C #을 배웠으며 이제 C ++로 시작합니다. 내가 이해하는 것처럼 newC ++의 연산자 는 C #의 연산자 와 비슷하지 않습니다.

이 샘플 코드에서 메모리 누수의 원인을 설명 할 수 있습니까?

class A { ... };
struct B { ... };

A *object1 = new A();
B object2 = *(new B());

답변:


464

무슨 일이야

쓰면 자동 저장 시간을 가진 T t;유형의 객체를 생성합니다 . 범위를 벗어나면 자동으로 정리됩니다.T

쓸 때 동적 저장 시간을 가진 new T()유형의 객체를 생성합니다 . 자동으로 정리되지 않습니다.T

정리하지 않고 새로운

delete정리하려면 포인터를 전달해야 합니다.

삭제로 새로 만들기

그러나 두 번째 예는 더 나쁩니다. 포인터를 역 참조하고 객체를 복사하는 것입니다. 이렇게하면으로 만든 객체에 대한 포인터를 잃을 new수 있으므로 원하는 경우에도 삭제할 수 없습니다!

deref로 새로 만들기

해야 할 일

자동 저장 기간을 선호해야합니다. 새로운 객체가 필요합니다.

A a; // a new object of type A
B b; // a new object of type B

동적 저장 기간이 필요한 경우 할당 된 객체에 대한 포인터를 자동 저장 기간 객체에 저장하면 자동으로 삭제됩니다.

template <typename T>
class automatic_pointer {
public:
    automatic_pointer(T* pointer) : pointer(pointer) {}

    // destructor: gets called upon cleanup
    // in this case, we want to use delete
    ~automatic_pointer() { delete pointer; }

    // emulate pointers!
    // with this we can write *p
    T& operator*() const { return *pointer; }
    // and with this we can write p->f()
    T* operator->() const { return pointer; }

private:
    T* pointer;

    // for this example, I'll just forbid copies
    // a smarter class could deal with this some other way
    automatic_pointer(automatic_pointer const&);
    automatic_pointer& operator=(automatic_pointer const&);
};

automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically

automatic_pointer로 새로 만들기

이것은 잘 설명되지 않은 RAII ( 자원 획득이 초기화 ) 라는 일반적인 관용구입니다 . 정리가 필요한 리소스를 확보하면 자동 저장 기간이 걸리는 리소스에 고정되므로 정리에 대해 걱정할 필요가 없습니다. 이것은 모든 리소스, 메모리, 파일 열기, 네트워크 연결 또는 당신이 좋아하는 모든 것에 적용됩니다.

automatic_pointer것은 이미 다양한 형태로 존재합니다. 예를 들어 제공했습니다. 라는 표준 라이브러리에 매우 유사한 클래스가 있습니다 std::unique_ptr.

오래된 C ++ 11 이전 버전도 auto_ptr있지만 이상한 복사 동작이 있기 때문에 더 이상 사용되지 않습니다.

그리고 std::shared_ptr같은 객체에 대한 여러 포인터를 허용하고 마지막 포인터가 파괴 될 때만 정리하는 더 똑똑한 예제 가 있습니다.


4
@ user1131997 :이 질문을 또하게되어 기쁩니다. 보시다시피 의견에 설명하기가 쉽지 않습니다 :)
R. Martinho Fernandes

@ R.MartinhoFernandes : 훌륭한 답변입니다. 하나의 질문입니다. operator * () 함수에서 참조로 리턴을 사용한 이유는 무엇입니까?
Destructor

@Destructor 늦게 답변 : D. 참조로 리턴하면 포인트를 수정할 수 있으므로 *p += 2일반 포인터를 사용하는 것처럼 예를 들어을 수행 할 수 있습니다 . 참조로 반환되지 않으면 일반적인 포인터의 동작을 모방하지 않을 것입니다. 이것은 의도입니다.
R. Martinho Fernandes

"할당 된 객체에 대한 포인터를 자동으로 삭제하는 자동 저장 기간 객체에 저장"하는 것이 좋습니다. C ++을 컴파일하기 전에 코더가이 패턴을 익히도록 요구할 수있는 방법 만 있다면!
Andy

35

단계별 설명 :

// creates a new object on the heap:
new B()
// dereferences the object
*(new B())
// calls the copy constructor of B on the object
B object2 = *(new B());

따라서 이것의 끝에는 포인터가없는 힙에 객체가 있으므로 삭제할 수 없습니다.

다른 샘플 :

A *object1 = new A();

delete할당 된 메모리 를 잊어 버린 경우에만 메모리 누수입니다 .

delete object1;

C ++에는 자동 스토리지가있는 오브젝트, 스택에서 자동으로 처리 된 오브젝트 및 힙에 동적 스토리지가있는 오브젝트 newdelete있습니다. (이것은 모두 대략 넣어집니다)

delete할당 된 모든 객체에 대해 있어야한다고 생각하십시오 new.

편집하다

생각해 object2보면 메모리 누수가 필요하지 않습니다.

다음 코드는 단지 포인트를 만들기위한 것입니다. 나쁜 생각입니다. 이런 코드를 좋아하지 마십시오.

class B
{
public:
    B() {};   //default constructor
    B(const B& other) //copy constructor, this will be called
                      //on the line B object2 = *(new B())
    {
        delete &other;
    }
}

이 경우 other참조로 전달 되므로 가 가리키는 정확한 객체가됩니다 new B(). 따라서 주소를 가져오고 &other포인터를 삭제하면 메모리가 해제됩니다.

그러나 나는 이것을 충분히 강조 할 수 없습니다. 요점은 바로 여기에 있습니다.


2
나는 똑같은 생각을했습니다 : 우리는 그것을 유출하지 않기 위해 해킹 할 수는 있지만 그렇게하고 싶지 않습니다. object1은 생성자가 어느 시점에서 삭제하는 일종의 데이터 구조에 연결될 수 있기 때문에 누출 할 필요가 없습니다.
CashCow

2
"이것은 가능하지만하지는 않는다"라는 답변을 작성하는 것은 항상 너무 유혹적입니다! :-) 느낌을 알고
코스

11

두 개의 "객체"가 주어지면 :

obj a;
obj b;

메모리에서 동일한 위치를 차지하지 않습니다. 다시 말해,&a != &b

하나의 값을 다른 값으로 지정해도 위치는 변경되지 않지만 내용은 변경됩니다.

obj a;
obj b = a;
//a == b, but &a != &b

직관적으로 포인터 "객체"는 같은 방식으로 작동합니다.

obj *a;
obj *b = a;
//a == b, but &a != &b

이제 예제를 보자.

A *object1 = new A();

이 값을 new A()에 할당하고 object1있습니다. 값은을 의미하는 포인터 object1 == new A()이지만 &object1 != &(new A()). (이 예제는 유효한 코드가 아니며 설명만을위한 것입니다)

포인터의 값이 유지되기 때문에 포인터가 가리키는 메모리를 해제 할 수 있습니다. delete object1;규칙으로 인해 delete (new A());누수가없는 것과 동일하게 동작합니다 .


두 번째 예에서는 뾰족한 개체를 복사합니다. 값은 실제 포인터가 아니라 해당 객체의 내용입니다. 다른 모든 경우와 마찬가지로 &object2 != &*(new A()).

B object2 = *(new B());

할당 된 메모리에 대한 포인터를 잃어 버렸으므로 해제 할 수 없습니다. delete &object2;작동하는 것처럼 보일 수 있지만에 &object2 != &*(new A())해당 delete (new A())하지 않으므로 유효하지 않습니다.


9

C # 및 Java에서는 new를 사용하여 모든 클래스의 인스턴스를 생성 한 다음 나중에 삭제하지 않아도됩니다.

C ++에는 객체를 만드는 키워드 "new"도 있지만 Java 나 C #과 달리 객체를 만드는 유일한 방법은 아닙니다.

C ++에는 객체를 생성하는 두 가지 메커니즘이 있습니다.

  • 자동적 인
  • 동적

자동 작성을 사용하면 범위가 지정된 환경에서-함수에서 또는-클래스 (또는 구조체)의 멤버로 오브젝트를 작성합니다.

함수에서는 다음과 같이 생성합니다.

int func()
{
   A a;
   B b( 1, 2 );
}

클래스 내에서 일반적으로 다음과 같이 생성합니다.

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

첫 번째 경우, 스코프 블록이 종료되면 오브젝트가 자동으로 삭제됩니다. 이것은 함수 또는 함수 내의 스코프 블록 일 수 있습니다.

후자의 경우, 객체 b는 객체 인 A의 인스턴스와 함께 파괴된다.

객체의 수명을 제어해야 할 때 객체에 새로운 객체가 할당 된 다음 객체를 삭제하려면 삭제가 필요합니다. RAII라는 기술을 사용하면 자동 객체 내에 넣어 객체를 생성하는 시점에서 객체 삭제를 처리하고 해당 자동 객체의 소멸자가 적용되기를 기다립니다.

그러한 객체 중 하나는 "삭제 자"로직을 호출하지만 객체를 공유하는 shared_ptr의 모든 인스턴스가 파괴 된 경우에만 shared_ptr입니다.

일반적으로 코드에 새로운 호출이 많이있을 수 있지만 삭제 호출은 제한적이어야하며 항상 스마트 포인터에 넣은 소멸자 또는 "삭제 자"객체에서 호출해야합니다.

소멸자는 절대 예외를 던져서는 안됩니다.

이렇게하면 메모리 누수가 거의 없습니다.


4
보다 더있다 automaticdynamic. 또한 있습니다 static.
Mooing Duck

9
B object2 = *(new B());

이 라인은 누출의 원인입니다. 이것을 조금 골라 보자 ..

object2는 주소 1에 저장된 B 유형의 변수입니다 (예, 임의의 숫자를 선택합니다). 오른쪽에서, 당신은 새로운 B 또는 B 타입의 객체에 대한 포인터를 요구했습니다. 프로그램은 기꺼이 당신에게 그것을주고 주소 2에 새로운 B를 할당하고 주소 3에 포인터를 만듭니다. 주소 2의 데이터에 액세스 할 수있는 유일한 방법은 주소 3의 포인터를 사용 *하는 것입니다. 다음으로 포인터가 가리키는 데이터 (주소 2의 데이터)를 얻기 위해 포인터를 역 참조했습니다 . 이렇게하면 해당 데이터의 복사본이 효과적으로 생성되어 주소 1에 할당 된 object2에 할당됩니다. 원본이 아니라 사본임을 기억하십시오.

자, 여기 문제가 있습니다 :

실제로 사용할 수있는 위치에 포인터를 저장하지 않았습니다! 이 할당이 완료되면 포인터 (주소 2에 액세스하는 데 사용 된 주소 3의 메모리)가 범위를 벗어났습니다. 더 이상 delete를 호출 할 수 없으므로 address2의 메모리를 정리할 수 없습니다. 남아있는 것은 address1의 address2에서 데이터 사본입니다. 같은 것들 중 두 가지는 메모리에 앉아 있습니다. 하나는 액세스 할 수 있고 다른 하나는 액세스 할 수 없습니다 (경로를 잃었 기 때문에). 이것이 메모리 누수 인 이유입니다.

C # 배경에서 C ++의 포인터가 어떻게 작동하는지 많이 읽으라고 제안합니다. 그것들은 고급 주제이며 이해하는 데 시간이 걸릴 수 있지만 그 사용법은 매우 중요합니다.


8

더 쉬워지면 컴퓨터 메모리를 호텔처럼 생각하고 프로그램은 필요할 때 방을 고용하는 고객입니다.

이 호텔이 작동하는 방식은 방을 예약하고 떠날 때 포터에게 알리는 것입니다.

포터에게 알리지 않고 방을 예약하고 떠나면 포터는 그 방이 여전히 사용 중이라고 생각하고 다른 사람이 그 방을 사용하지 못하게합니다. 이 경우 공간 누출이 있습니다.

프로그램이 메모리를 할당하고 삭제하지 않으면 (단지 사용을 중지) 컴퓨터는 메모리가 여전히 사용 중이라고 생각하고 다른 사람이 메모리를 사용할 수 없게합니다. 이것은 메모리 누수입니다.

이것은 정확한 비유는 아니지만 도움이 될 수 있습니다.


5
나는 그 비유가 완벽하지는 않지만 그것을 좋아하지만 확실히 새로운 사람들에게 메모리 누수를 설명하는 좋은 방법입니다!
AdamM

1
나는 이것을 런던 블룸버그의 수석 엔지니어 인터뷰에서 HR 소녀에게 메모리 누수를 설명하기 위해 사용했습니다. 그녀가 이해 한 방식으로 프로그래머가 아닌 사람에게 메모리 누수 (및 스레딩 문제)를 실제로 설명 할 수 있었기 때문에 인터뷰를 마쳤습니다.
Stefan

7

생성 object2할 때 새로 생성 한 객체의 복사본을 생성하지만 할당되지 않은 포인터도 손실되므로 나중에 삭제할 수있는 방법이 없습니다. 이를 피하려면 object2참조를해야합니다.


3
참조 주소를 사용하여 객체를 삭제하는 것은 매우 나쁜 습관입니다. 스마트 포인터를 사용하십시오.
Tom Whittock

3
엄청나게 나쁜 연습? 스마트 포인터가 무대 뒤에서 무엇을 사용한다고 생각하십니까?
Blindy

3
@Blindy 스마트 포인터 (적절하게 구현 된 것)는 포인터를 직접 사용합니다.
Luchian Grigore

2
글쎄, 정직하게 말해서, 전체 아이디어는 그렇게 크지 않습니까? 실제로 OP에서 시도한 패턴이 실제로 유용한 지 확실하지 않습니다.
Mario

7

글쎄, 어떤 시점에서 new연산자를 사용하여 할당 한 메모리를 해당 메모리에 대한 포인터를 연산자에 전달 하여 할당하지 않으면 메모리 누수가 발생합니다 delete.

위의 두 경우 :

A *object1 = new A();

여기서는 delete메모리를 확보 하는 데 사용하지 않으므로 object1포인터가 범위를 벗어나 면 포인터가 손실되어 delete연산자를 사용할 수 없으므로 메모리 누수가 발생 합니다.

그리고 여기

B object2 = *(new B());

에서 반환 한 포인터를 삭제 new B()하므로 delete메모리를 비우기 위해 해당 포인터를 전달할 수 없습니다 . 따라서 다른 메모리 누수가 발생합니다.


7

즉시 누출되는 것은이 줄입니다.

B object2 = *(new B());

여기서는 B힙에 새 객체를 만든 다음 스택에 복사본을 만듭니다. 힙에 할당 된 것은 더 이상 액세스 할 수 없으므로 누출이 발생합니다.

이 줄은 즉시 새지 않습니다.

A *object1 = new A();

당신이 결코 경우 누수가 될 delete거라고하지 object1하지만.


4
동적 / 자동 스토리지를 설명 할 때 힙 / 스택을 사용하지 마십시오.
Pubby

2
@Pubby 왜 사용하지 않습니까? 동적 / 자동 스토리지 때문에 항상 스택이 아닌 힙입니까? 이것이 바로 스택 / 힙에 대해 자세히 설명 할 필요가없는 이유입니다.

4
@ user1131997 힙 / 스택은 구현 세부 사항입니다. 그들은 알고 있어야하지만이 질문과 관련이 없습니다.
Pubby

2
흠 나는 그것에 대한 별도의 답변을 원합니다. 즉 내 것과 동일하지만 힙 / 스택을 가장 잘 생각하는 것으로 바꾸십시오. 나는 당신이 그것을 설명하는 방법을 찾는 것에 관심이 있습니다.
mattjgalloway
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.