C ++ 개체 인스턴스화


113

저는 C ++를 이해하려는 C 프로그래머입니다. 많은 튜토리얼은 다음과 같은 스 니펫을 사용하여 객체 인스턴스화를 보여줍니다.

Dog* sparky = new Dog();

이는 나중에 다음을 수행 할 것임을 의미합니다.

delete sparky;

말이 되네요. 이제 동적 메모리 할당이 필요하지 않은 경우 대신 위를 사용하는 이유가 있습니까?

Dog sparky;

Sparky가 범위를 벗어나면 소멸자가 호출되도록할까요?

감사!

답변:


166

반대로, 경험상 사용자 코드에 새 / 삭제가 없어야하는 한 항상 스택 할당을 선호해야합니다.

말했듯이, 변수가 스택에서 선언되면 해당 소멸자가 범위를 벗어날 때 자동으로 호출되며, 이는 리소스 수명을 추적하고 누수를 방지하기위한 주요 도구입니다.

따라서 일반적으로 메모리 (new 호출), 파일 핸들, 소켓 또는 기타 모든 리소스를 할당해야 할 때마다 생성자가 리소스를 획득하고 소멸자가이를 해제하는 클래스로 래핑합니다. 그런 다음 스택에 해당 유형의 개체를 만들 수 있으며 리소스가 범위를 벗어날 때 리소스가 해제되도록 보장됩니다. 이렇게하면 메모리 누수를 방지하기 위해 모든 곳에서 새 / 삭제 쌍을 추적 할 필요가 없습니다.

이 관용구의 가장 일반적인 이름은 RAII입니다.

또한 전용 RAII 개체 외부에 새로운 항목을 할당해야하는 드문 경우에 결과 포인터를 래핑하는 데 사용되는 스마트 포인터 클래스를 살펴보십시오. 대신 포인터를 스마트 포인터에 전달하면, 예를 들어 참조 카운트를 통해 수명을 추적하고 마지막 참조가 범위를 벗어날 때 소멸자를 호출합니다. 표준 라이브러리는 std::unique_ptr단순한 범위 기반 관리를 위해 있으며 std::shared_ptr공유 소유권을 구현하기 위해 참조 계산을 수행합니다.

많은 튜토리얼은 다음과 같은 스 니펫을 사용하여 객체 인스턴스화를 보여줍니다.

그래서 당신이 발견 한 것은 대부분의 튜토리얼이 형편 없다는 것입니다. ;) 대부분의 튜토리얼은 필요하지 않을 때 변수를 생성하기 위해 new / delete를 호출하고 할당 수명을 추적하는 데 어려움을주는 것을 포함하여 형편없는 C ++ 사례를 가르칩니다.


원시 포인터는 auto_ptr과 유사한 의미 (소유권 이전)를 원하지만 던지지 않는 스왑 작업을 유지하고 참조 계산의 오버 헤드를 원하지 않을 때 유용합니다. 가장자리 케이스 일 수도 있지만 유용합니다.
— Greg Rogers

2
이것은 정답이지만 스택에 객체를 생성하는 습관을 갖지 않는 이유는 그 객체가 얼마나 클지 완전히 분명하지 않기 때문입니다. 스택 오버플로 예외를 요청하는 것입니다.
— dviljoen

3
Greg : 물론입니다. 그러나 당신이 말했듯이, 엣지 케이스. 일반적으로 포인터는 피하는 것이 가장 좋습니다. 그러나 그들은 이유 때문에 언어로되어 있습니다. :) dviljoen : 객체가 크면 스택에 할당 할 수있는 RAII 객체로 래핑하고 힙 할당 데이터에 대한 포인터를 포함합니다.
— jalf

3
@dviljoen : 아니요. C ++ 컴파일러는 불필요하게 개체를 부 풀리지 않습니다. 최악의 경우 일반적으로 가장 가까운 4 바이트의 배수로 반올림됩니다. 일반적으로 포인터를 포함하는 클래스는 포인터 자체만큼 많은 공간을 차지하므로 스택 사용에 비용이 들지 않습니다.
— jalf

20

스택에있는 것이 할당 및 자동 해제 측면에서 이점이 될 수 있지만 몇 가지 단점이 있습니다.

  1. 스택에 거대한 개체를 할당하고 싶지 않을 수 있습니다.

  2. 다이나믹 파견! 이 코드를 고려하십시오.

#include <iostream>

class A {
public:
  virtual void f();
  virtual ~A() {}
};

class B : public A {
public:
  virtual void f();
};

void A::f() {cout << "A";}
void B::f() {cout << "B";}

int main(void) {
  A *a = new B();
  a->f();
  delete a;
  return 0;
}

그러면 "B"가 인쇄됩니다. 이제 Stack을 사용할 때 어떤 일이 발생하는지 살펴 보겠습니다.

int main(void) {
  A a = B();
  a.f();
  return 0;
}

이렇게하면 "A"가 인쇄되며 Java 또는 기타 객체 지향 언어에 익숙한 사용자에게는 직관적이지 않을 수 있습니다. 그 이유는 B더 이상 인스턴스에 대한 포인터가 없기 때문입니다 . 대신의 인스턴스 B가 생성되고 a유형의 변수에 복사됩니다 A.

특히 C ++를 처음 접할 때 어떤 일이 직관적이지 않게 발생할 수 있습니다. C에서는 포인터가 있고 그게 전부입니다. 당신은 그것들을 사용하는 방법을 알고 있으며 그들은 항상 동일합니다. C ++에서는 그렇지 않습니다. 그냥 당신이하는 방법에 대한 인수로이 예에서 사용할 때, 무슨 상상 - 일을 더 복잡하게 얻을 경우는 큰 차이를 만들 않습니다 a유형 인 A또는 A*심지어 A&(통화 별 참조). 많은 조합이 가능하며 모두 다르게 작동합니다.


2
-1 : 가치 의미론을 이해할 수없는 사람들과 다형성에 참조 / 포인터가 필요하다는 단순한 사실 (객체 슬라이싱을 피하기 위해)은 언어에 어떤 종류의 문제도 구성하지 않습니다. 일부 사람들이 기본 규칙을 배울 수 없다고해서 C ++의 힘을 단점으로 간주해서는 안됩니다.
— underscore_d

알림 주셔서 감사합니다. 포인터 또는 참조 대신 객체를 취하는 방법과 비슷한 문제가 있었고 엉망이었습니다. 객체에는 내부에 포인터가 있으며 이러한 대처로 인해 너무 빨리 삭제되었습니다.
— BoBoDev 2011

@underscore_d 동의합니다. 이 답변의 마지막 문단을 제거해야합니다. 내가이 답변에 동의한다는 의미로 편집했다는 사실을 받아들이지 마십시오. 나는 단지 그 안에있는 실수를 읽는 것을 원하지 않았습니다.
— TamaMcGlinn

@TamaMcGlinn이 동의했습니다. 좋은 편집에 감사드립니다. 의견 부분을 삭제했습니다.
— UniversE

13

글쎄요, 포인터를 사용하는 이유는 malloc으로 할당 된 C에서 포인터를 사용하는 이유와 똑같을 것입니다 : 객체가 변수보다 오래 살기를 원한다면!

피할 수 있다면 new 연산자를 사용하지 않는 것이 좋습니다. 특히 예외를 사용하는 경우. 일반적으로 컴파일러가 개체를 해제하도록하는 것이 훨씬 안전합니다.


13

나는 운영자의 & 주소를 알지 못하는 사람들로부터이 안티 패턴을 보았다. 포인터가있는 함수를 호출해야하는 경우 항상 힙에 할당하여 포인터를 얻습니다.

void FeedTheDog(Dog* hungryDog);

Dog* badDog = new Dog;
FeedTheDog(badDog);
delete badDog;

Dog goodDog;
FeedTheDog(&goodDog);

또는 : void FeedTheDog (Dog & hungryDog); 개 개; FeedTheDog (개);
— Scott Langham 2015

1
@ScottLangham 사실이지만 그게 내가 만들려고 한 요점이 아닙니다. 예를 들어 선택적 NULL 포인터를 사용하는 함수를 호출하거나 변경할 수없는 기존 API 일 수 있습니다.
— Mark Ransom 2015

7

힙을 매우 중요한 부동산으로 취급하고 매우 신중하게 사용하십시오. 기본 규칙은 가능할 때마다 스택을 사용 하고 다른 방법이 없을 때마다 힙을 사용하는 것입니다. 스택에 객체를 할당하면 다음과 같은 많은 이점을 얻을 수 있습니다.

(1). 예외의 경우 스택 해제에 대해 걱정할 필요가 없습니다.

(2). 힙 관리자가 필요로하는 것보다 더 많은 공간을 할당하여 발생하는 메모리 조각화에 대해 걱정할 필요가 없습니다.


1
물체의 크기에 대해 약간의 고려가 있어야합니다. 스택은 크기가 제한되어 있으므로 매우 큰 개체에 힙을 할당해야합니다.
— 아담

1
@Adam 나는 Naveen이 여전히 옳다고 생각합니다. 스택에 큰 물체를 놓을 수 있다면 더 낫기 때문에 그렇게하십시오. 당신이 아마 할 수없는 많은 이유가 있습니다. 그러나 나는 그가 옳다고 생각합니다.
— McKay

5

내가 걱정할 유일한 이유는 Dog가 이제 힙이 아닌 스택에 할당된다는 것입니다. 따라서 Dog의 크기가 메가 바이트라면 문제가있을 수 있습니다.

새 / 삭제 경로로 이동해야하는 경우 예외에주의하십시오. 이 때문에 auto_ptr 또는 boost 스마트 포인터 유형 중 하나를 사용하여 개체 수명을 관리해야합니다.


1

스택에 할당 할 수있을 때 (힙에) 새로 만들 이유가 없습니다 (어떤 이유로 든 작은 스택이 있고 힙을 사용하려는 경우가 아니라면).

힙에 할당하려는 경우 표준 라이브러리에서 shared_ptr (또는 그 변형 중 하나) 사용을 고려할 수 있습니다. shared_ptr에 대한 모든 참조가 존재하지 않으면 삭제 작업을 처리합니다.


예를 들어 많은 이유가 있습니다.
— dangerousdave

나는 당신이 객체가 함수의 범위보다 오래 살아남기를 원할 것이라고 생각하지만 OP는 그것이 그들이하려는 일을 제안하지 않는 것 같습니다.
— Scott Langham 2015

0

다른 누구도 언급하지 않은 추가 이유가 있는데, 왜 동적으로 개체를 만들도록 선택할 수 있는지에 대한 것입니다. 동적 힙 기반 객체를 사용하면 다형성 을 사용할 수 있습니다 .


2
스택 기반 객체도 다형성으로 작업 할 수 있지만, 이와 관련하여 스택 / 힙 할당 객체 간의 차이는 보이지 않습니다. 예 : void MyFunc () {Dog dog; 개에게 먹이를 주다); } void Feed (Animal & animal) {auto food = animal-> GetFavouriteFood (); PutFoodInBowl (음식); } //이 예제에서 GetFavouriteFood는 각 동물이 자체 구현으로 재정의하는 가상 함수가 될 수 있습니다. 이것은 힙을 사용하지 않고 다형 적으로 작동합니다.
— Scott Langham 2015

-1 : 다형성에는 참조 또는 포인터 만 필요합니다. 기본 인스턴스의 저장 기간과는 완전히 무관합니다.
— underscore_d

@underscore_d "유니버설베이스 클래스를 사용하는 것은 비용을 의미합니다. 객체는 다형성이되도록 힙 할당되어야합니다." - 비얀 스트로브 스트 룹 stroustrup.com/bs_faq2.html#object
— 위험한 데이브

LOL, Stroustrup 자신이 말했더라도 상관 없지만, 그가 틀 렸기 때문에 그것은 그에게 엄청나게 창피한 말입니다. 직접 테스트하는 것은 어렵지 않습니다. 스택에서 DerivedA, DerivedB 및 DerivedC를 인스턴스화합니다. Base에 대한 스택 할당 포인터도 인스턴스화하고 A / B / C로 장착 할 수 있는지 확인하고 다형성으로 사용할 수 있습니다. 주장에 비판적 사고와 표준 참조를 적용합니다. 주장이 언어의 원저자 인 경우에도 마찬가지입니다. 여기 : stackoverflow.com/q/5894775/2757035
— underscore_d

이렇게 말하면 자동 저장 기간과 함께 거의 1000 개의 다형성 객체로 구성된 2 개의 개별 패밀리가 포함 된 객체가 있습니다. 스택에서이 객체를 인스턴스화하고 참조 또는 포인터를 통해 해당 멤버를 참조하여 다형성의 모든 기능을 얻습니다. 내 프로그램의 어떤 것도 동적 / 힙 할당되지 않으며 (스택 할당 클래스가 소유 한 리소스를 제외하고) iota가 내 개체의 기능을 변경하지 않습니다.
— underscore_d

-4

Visual Studio에서 동일한 문제가 발생했습니다. 다음을 사용해야합니다.

yourClass-> classMethod ();

보다는 :

yourClass.classMethod ();


3
이것은 질문에 대한 답이 아닙니다. 오히려 힙에 할당 된 개체 (개체에 대한 포인터를 통해)와 스택에 할당 된 개체에 액세스하려면 다른 구문을 사용해야합니다.
— Alexey Ivanov
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.