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.