왜 객체 자체보다는 포인터를 사용해야합니까?


1602

Java 배경에서 왔으며 C ++에서 객체 작업을 시작했습니다. 그러나 나에게 일어난 한 가지는 사람들이 종종 객체 자체가 아닌 객체에 대한 포인터를 사용한다는 것입니다.

Object *myObject = new Object;

오히려

Object myObject;

또는 함수를 사용하는 대신 testFunc()다음과 같이 말합니다 .

myObject.testFunc();

우리는 다음과 같이 작성해야합니다.

myObject->testFunc();

그러나 왜 우리가 이런 식으로 해야하는지 알 수 없습니다. 메모리 주소에 직접 액세스 할 수 있기 때문에 효율성과 속도와 관련이 있다고 가정합니다. 내가 맞아?


405
이 관행을 따르기보다는 질문하는 것에 대한 조언. 대부분의 경우 포인터가 과도하게 사용됩니다.
Luchian Grigore

120
포인터를 사용해야 할 이유가 없다면,하지 마십시오. 개체를 선호하십시오. 원시 포인터보다 shared_ptr 이전의 unique_ptr 이전의 오브젝트를 선호하십시오.
stefan

113
참고 : 자바에서 모든 것 (기본 유형 제외)은 포인터입니다. 오히려 반대의 질문을해야합니다. 왜 간단한 물체가 필요한가요?
Karoly Horvath 2014 년

119
Java에서 포인터는 구문에 의해 숨겨집니다. C ++에서 포인터와 비 포인터의 차이점은 코드에서 명시 적으로 나타납니다. Java는 어디에서나 포인터를 사용합니다.
Daniel Martín

216
너무 넓게 닫 습니까? 진심이야? 이 Java ++ 프로그래밍 방식은 매우 일반적이며 C ++ 커뮤니티에서 가장 중요한 문제 중 하나입니다 . 진지하게 다루어야합니다.
Manu343726

답변:


1573

동적 할당이 너무 자주 나타나는 것은 매우 불행한 일입니다. 그것은 단지 얼마나 나쁜 C ++ 프로그래머가 있는지 보여줍니다.

어떤 의미에서, 당신은 두 개의 질문을 하나로 묶습니다. 첫 번째는 언제 동적 할당을 사용해야 new합니까? 두 번째는 언제 포인터를 사용해야합니까?

중요한 가정 메시지는 항상 해당 작업에 적합한 도구를 사용해야 한다는 입니다. 거의 모든 상황에서 수동 동적 할당을 수행하거나 원시 포인터를 사용하는 것보다 더 적절하고 안전한 것이 있습니다.

동적 할당

귀하의 질문에, 당신은 객체를 생성하는 두 가지 방법을 보여주었습니다. 주요 차이점은 객체의 저장 기간입니다. Object myObject;블록 내에서 작업 할 때 개체는 자동 저장 기간으로 생성되므로 범위를 벗어나면 자동으로 삭제됩니다. 그렇게 new Object()하면 객체의 동적 저장 기간이 유지되므로 명시 적으로 객체가 유지 될 때까지 활성 상태를 유지 delete합니다. 필요할 때만 동적 스토리지 기간을 사용해야합니다. 즉, 가능한 경우 자동 저장 시간으로 객체를 생성하는 것을 항상 선호 해야합니다 .

동적 할당이 필요할 수있는 주요 두 상황 :

  1. 현재 범위보다 오래 지속되는 개체가 필요합니다 ( 복사본이 아닌 특정 메모리 위치에있는 특정 개체). 개체를 복사 / 이동해도 (대부분의 경우) 자동 개체를 선호해야합니다.
  2. 스택을 쉽게 채울 수 있는 많은 메모리를 할당해야합니다 . 실제로 C ++의 범위를 벗어 났기 때문에이 문제에 대해 걱정할 필요가 없다면 좋을 것입니다. 그러나 불행히도 우리는 시스템의 현실을 다루어야합니다. 우리는 개발 중입니다.

동적 할당이 절대적으로 필요한 경우 스마트 포인터 또는 RAII 를 수행하는 다른 유형 (예 : 표준 컨테이너)으로 캡슐화해야합니다 . 스마트 포인터는 동적으로 할당 된 객체의 소유권 의미를 제공합니다. 한 번 봐 std::unique_ptrstd::shared_ptr예를 들어,. 적절하게 사용하면 거의 모든 메모리 관리를 수행하지 않아도됩니다 ( 0규칙 참조 ).

포인터

그러나 동적 할당 이외의 원시 포인터에는 다른 일반적인 용도가 있지만 대부분 선호하는 대안이 있습니다. 이전과 마찬가지로 포인터가 실제로 필요한 경우가 아니면 항상 대안을 선호하십시오 .

  1. 참조 의미론이 필요합니다 . 때로는 전달 된 함수가 특정 객체 (복사본이 아닌)에 액세스 할 수 있기를 원하기 때문에 포인터가 할당 된 방법에 관계없이 객체를 전달하려고 할 때가 있습니다. 그러나 대부분의 경우 포인터에 대한 참조 유형을 선호해야합니다. 이는 포인터가 특별히 설계된 것이므로 포인터에 대한 것입니다. 위의 상황 1에서와 같이 반드시 개체의 수명을 현재 범위를 넘어 연장하는 것은 아닙니다. 이전과 같이 객체 사본을 전달해도 괜찮다면 참조 의미론이 필요하지 않습니다.

  2. 다형성이 필요합니다 . 객체에 대한 포인터 나 참조를 통해서만 다형성으로 (즉, 객체의 동적 유형에 따라) 함수를 호출 할 수 있습니다. 이것이 필요한 동작이면 포인터 또는 참조를 사용해야합니다. 다시, 참조가 바람직하다.

  3. nullptr객체를 생략 할 때 a 를 전달 하여 객체가 선택 사항임을 나타내 려고합니다. 인수 인 경우 기본 인수 또는 함수 오버로드를 사용하는 것이 좋습니다. 그렇지 않으면이 동작을 캡슐화하는 유형을 사용하는 것이 좋습니다 std::optional( 예 : C ++ 17에서 도입 됨-이전 C ++ 표준에서는 boost::optional).

  4. 컴파일 시간을 향상시키기 위해 컴파일 단위를 분리하려고합니다 . 포인터의 유용한 속성은 뾰족한 유형의 전달 선언 만 필요하다는 것입니다 (실제로 객체를 사용하려면 정의가 필요합니다). 이를 통해 컴파일 프로세스의 일부를 분리하여 컴파일 시간을 크게 향상시킬 수 있습니다. Pimpl 관용구를 참조하십시오 .

  5. C 라이브러리 또는 C 스타일 라이브러리 와 인터페이스해야합니다 . 이 시점에서 원시 포인터를 사용해야합니다. 가장 좋은 방법은 마지막 순간에 원시 포인터를 느슨하게 놓는 것입니다. 예를 들어 get멤버 함수 를 사용하여 스마트 포인터에서 원시 포인터를 얻을 수 있습니다 . 라이브러리가 핸들을 통해 할당 해제 할 것으로 예상되는 일부 할당을 수행하는 경우, 오브젝트를 적절하게 할당 해제하는 사용자 정의 삭제 도구를 사용하여 스마트 포인터로 핸들을 랩핑 할 수 있습니다.


83
"현재 범위보다 오래 지속하려면 개체가 필요합니다." -이것에 대한 추가 참고 사항 : 현재 범위보다 오래 유지하기 위해 객체가 필요한 것처럼 보이지만 실제로는 그렇지 않습니다. 예를 들어, 객체를 벡터 안에 넣으면 객체가 벡터로 복사 (또는 이동)되며 범위가 끝나면 원래 객체는 파기해도 안전합니다.

25
많은 곳에서 s / copy / move /를 기억하십시오. 객체를 반환한다고해서 반드시 이동을 의미하는 것은 아닙니다. 포인터를 통해 객체에 액세스하는 것은 객체의 생성 방식과 직교한다는 점에 유의해야합니다.
강아지

15
이 답변에 대한 RAII에 대한 명시 적 참조가 누락되었습니다. C ++는 자원 관리에 관한 모든 것 (거의 모든 것)이며 RAII는 C ++에서 수행하는 방법입니다 (그리고 원시 포인터가 생성하는 주요 문제 : Breaking RAII)
Manu343726

11
C ++ 11 이전에는 스마트 포인터가있었습니다 (예 : boost :: shared_ptr 및 boost :: scoped_ptr). 다른 프로젝트는 자체적으로 동등합니다. 이동 의미론을 얻을 수 없으며 std :: auto_ptr의 assign에 결함이 있으므로 C ++ 11이 개선하지만 조언은 여전히 ​​좋습니다. (그리고 슬픈 nitpick, 그것은에 액세스하도록 충분하지 11 컴파일러 ++ C를, 모든 컴파일러가 당신이 가능하게 지원 C ++ 11 일에 코드를 할 수 있습니다 필요합니다. 예, 오라클 솔라리스 스튜디오, 난
armb

7
@ MDMoore313 글을 쓸 수 있습니다Object myObject(param1, etc...)
user000001

173

포인터에 대한 많은 사용 사례가 있습니다.

다형성 행동 . 다형성 유형의 경우 슬라이싱을 피하기 위해 포인터 (또는 참조)가 사용됩니다.

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

참조 의미론 및 복사 방지 . 다형성이 아닌 유형의 경우 포인터 (또는 참조)는 값 비싼 객체를 복사하지 않도록합니다.

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

C ++ 11에는 값 비싼 객체의 많은 복사본을 함수 인수 및 반환 값으로 피할 수있는 의미 체계가 이동했습니다. 그러나 포인터를 사용하면 해당 객체를 피할 수 있으며 동일한 객체에 여러 개의 포인터를 사용할 수 있습니다 (객체는 한 번만 이동할 수 있음).

자원 확보 . new연산자를 사용하여 리소스에 대한 포인터를 만드는 것은 최신 C ++ 의 안티 패턴 입니다. 특수 자원 클래스 (표준 컨테이너 중 하나) 또는 스마트 포인터 ( std::unique_ptr<>또는 std::shared_ptr<>)를 사용하십시오. 치다:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

vs.

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

원시 포인터는 "보기"로만 사용해야하며 소유권에 관여하지 않아야합니다 (직접 생성 또는 반환 값을 통한 암시 적). C ++ FAQ의이 Q & A 도 참조하십시오 .

보다 세분화 된 수명 제어 공유 포인터를 복사 때마다 (예 : 함수 인수로) 가리키는 자원이 활성 상태로 유지됩니다. new범위를 벗어나면 일반 객체 ( 사용자가 직접 또는 리소스 클래스 내부에서 직접 생성하지 않음 )가 소멸됩니다.


17
"새로운 연산자를 사용하여 리소스에 대한 포인터를 만드는 것은 안티 패턴입니다." 나는 원시 포인터를 소유하는 것이 안티 패턴이라는 것을 향상시킬 수 있다고 생각합니다 . 창조하지만, 소유권 이전 이럴을 의미 인수 또는 반환 값으로 원시 포인터를 전달뿐만 아니라 이후 사용되지 않습니다 unique_ptr/ 이동 의미를
dyp

1
@dyp tnx,이 주제에 대한 C ++ FAQ Q & A에 대한 업데이트 및 참조.
TemplateRex

4
어디에서나 스마트 포인터를 사용하는 것은 안티 패턴입니다. 적용 가능한 몇 가지 특별한 경우가 있지만 대부분의 경우 동적 할당 (임의의 수명)이 일반적인 스마트 포인터와 반대되는 이유와 같습니다.
James Kanze

2
@JamesKanze 나는 스마트 포인터가 모든 곳에서 소유권을 위해 사용해야한다는 것을 의미하지 않았으며 원시 포인터는 소유권을 위해서가 아니라 뷰에만 사용해야한다는 것을 의미하지는 않았습니다.
TemplateRex

2
@TemplateRex hun(b)컴파일 할 때까지 잘못된 유형을 제공 했는지 알지 못하는 경우가 아니라면 서명에 대한 지식이 필요합니다. 참조 문제는 일반적으로 컴파일 타임에 잡히지 않으며 디버그하는 데 더 많은 노력이 필요하지만 인수가 올바른지 확인하기 위해 서명을 확인하는 경우 인수가 참조인지 여부도 확인할 수 있습니다 따라서 참조 비트는 문제가되지 않습니다 (특히 선택한 함수의 서명을 표시하는 IDE 또는 텍스트 편집기를 사용하는 경우). 또한 const&.
JAB

130

앞으로의 선언, 다형성 등의 중요한 유스 케이스를 포함 하여이 질문에 대한 훌륭한 답변이 많이 있지만 질문의 "영혼"의 일부에는 대답이 없다고 느낍니다.

두 언어를 비교하는 상황을 살펴 보자.

자바:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

이에 가장 가까운 것은 다음과 같습니다.

C ++ :

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

대안적인 C ++ 방식을 보자 :

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

그것을 생각하는 가장 좋은 방법은 Java가 객체에 대한 포인터를 암시 적으로 처리하는 반면 C ++은 객체에 대한 포인터 또는 객체 자체를 처리 할 수 ​​있다는 것입니다. 여기에는 예외가 있습니다. 예를 들어 Java "primitive"유형을 선언하는 경우 포인터가 아니라 복사 된 실제 값입니다. 그래서,

자바:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

즉, 포인터를 사용하는 것이 반드시 물건을 처리하는 올바른 방법이거나 잘못된 방법은 아닙니다. 그러나 다른 답변은 만족스럽게 다루었습니다. 그러나 일반적인 아이디어는 C ++에서 객체의 수명과 객체의 위치를 ​​훨씬 더 많이 제어 할 수 있다는 것입니다.

요점 Object * object = new Object()은 실제로 일반적인 Java (또는 해당 C #) 의미에 가장 가까운 구문입니다.


7
Object2 is now "dead"나는 당신이 myObject1더 정확하게 생각합니다 the object pointed to by myObject1.
Clément

2
과연! 약간의 표현.
Gerasimos R

2
Object object1 = new Object(); Object object2 = new Object();매우 나쁜 코드입니다. 두 번째 new 또는 두 번째 Object 생성자가 throw 될 수 있으며 이제 object1이 누출됩니다. raw를 사용하는 경우 RAII 랩퍼로 가능한 빨리 new랩핑 된 new오브젝트를 랩핑 해야합니다.
PSkocik

8
실제로 이것이 프로그램이라면 다른 일도 일어나지 않을 것입니다. 고맙게도, 이것은 C ++의 포인터가 동작하는 방법을 보여주는 설명 스 니펫입니다. RAII 객체를 원시 포인터로 대체 할 수없고 원시 포인터를 연구하고 배우는 몇 안되는 장소 중 하나입니다.
Gerasimos R

80

포인터를 사용하는 또 다른 좋은 이유는 앞으로 선언하는 것 입니다. 충분히 큰 프로젝트에서 실제로 컴파일 시간을 단축시킬 수 있습니다.


7
이것은 유용한 정보의 혼합에 정말로 추가되고 있습니다.
TemplateRex

3
std :: shared_ptr <T>는 T의 forward 선언과도 작동합니다. (std :: unique_ptr <T> does not )
berkus

13
@berkus :의 std::unique_ptr<T>앞으로 선언과 함께 작동 T합니다. 의 소멸자 std::unique_ptr<T>가 호출 될 때 T완전한 유형 인지 확인해야합니다 . 이것은 일반적으로 std::unique_ptr<T>선언 을 포함하는 클래스가 헤더 파일에 소멸자를 선언하고 cpp 파일에서 구현을 비어 있음을 의미합니다.
David Stone

모듈이이 문제를 해결합니까?
Trevor Hickey

@TrevorHickey 내가 알고있는 오래된 의견이지만 어쨌든 대답합니다. 모듈은 종속성을 제거하지 않지만 성능 비용 측면에서 종속성을 매우 저렴하고 거의 무료로 포함시켜야합니다. 또한 모듈의 일반적인 속도 향상으로 인해 컴파일 시간이 허용 범위 내로 충분 해지면 더 이상 문제가되지 않습니다.
Aidiakapi

79

머리말

자바는 과대 광고와 달리 C ++과는 다릅니다. Java 과장 기계는 Java가 C ++과 같은 구문을 가지고 있기 때문에 언어가 유사하다고 생각합니다. 진실에서 더 이상 갈 수있는 것은 없습니다. 이 잘못된 정보는 Java 프로그래머가 코드의 의미를 이해하지 않고 C ++로 이동하여 Java와 유사한 구문을 사용하는 이유의 일부입니다.

앞으로 우리는 간다

그러나 왜 우리가 이런 식으로 해야하는지 알 수 없습니다. 메모리 주소에 직접 액세스 할 수 있기 때문에 효율성과 속도와 관련이 있다고 가정합니다. 내가 맞아?

반대로, 실제로. 스택은 힙에 비해 매우 단순하므로 힙이 스택보다 훨씬 느립니다 . 자동 저장 변수 (일명 스택 변수)는 소멸자가 범위를 벗어나면 호출됩니다. 예를 들면 다음과 같습니다.

{
    std::string s;
}
// s is destroyed here

반면 동적으로 할당 된 포인터를 사용하는 경우 소멸자를 수동으로 호출해야합니다. delete이 소멸자를 호출합니다.

{
    std::string* s = new std::string;
}
delete s; // destructor called

이것은 newC # 및 Java에서 널리 사용되는 구문 과 관련이 없습니다 . 그것들은 완전히 다른 목적으로 사용됩니다.

동적 할당의 이점

1. 사전에 배열의 크기를 알 필요가 없습니다

많은 C ++ 프로그래머가 겪는 첫 번째 문제 중 하나는 사용자의 임의 입력을 받아 들일 때 스택 변수에 고정 된 크기 만 할당 할 수 있다는 것입니다. 배열의 크기도 변경할 수 없습니다. 예를 들면 다음과 같습니다.

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

물론, std::string대신 사용하면 std::string내부적으로 크기가 조정되므로 문제가되지 않습니다. 그러나 본질적으로이 문제에 대한 해결책은 동적 할당입니다. 예를 들어, 사용자의 입력을 기반으로 동적 메모리를 할당 할 수 있습니다.

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

참고 : 많은 초보자들이 실수로하는 실수 중 하나는 가변 길이 배열의 사용입니다. 이것은 GNU 확장이며 Clang의 확장이기도합니다. 많은 GCC 확장을 반영하기 때문입니다. 따라서 다음에 int arr[n]의존해서는 안됩니다.

힙이 스택보다 훨씬 크기 때문에 필요한만큼의 메모리를 임의로 할당 / 재 할당 할 수 있지만 스택에는 제한이 있습니다.

2. 배열은 포인터가 아니다

이것이 당신이 요구하는 이점은 무엇입니까? 배열과 포인터의 혼란과 신화를 이해하면 대답이 명확해질 것입니다. 일반적으로 동일한 것으로 가정하지만 그렇지 않습니다. 이 신화는 포인터가 배열처럼 첨자 화 될 수 있고 함수 선언에서 최상위 레벨의 포인터로 배열이 소멸 될 수 있다는 사실에서 비롯됩니다. 그러나 배열이 포인터로 붕괴되면 포인터가 sizeof정보를 잃게 됩니다. 따라서 sizeof(pointer)64 비트 시스템에서 일반적으로 8 바이트 인 포인터 크기를 바이트 단위로 제공합니다.

배열은 할당 할 수 없으며 초기화 만합니다. 예를 들면 다음과 같습니다.

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

반면에 포인터로 원하는 것을 할 수 있습니다. 불행히도, 포인터와 배열의 구별은 Java와 C #에서 수동으로 이루어지기 때문에 초보자는 그 차이를 이해하지 못합니다.

3. 다형성

Java 및 C #에는 as키워드 를 사용하여 객체를 다른 객체로 취급 할 수있는 기능이 있습니다 . 누군가가 치료 싶어한다면 EntityA와 객체를 Player객체, 사람은 할 수있는 Player player = Entity as Player;경우에만 특정 유형에 적용해야 균일 한 용기에 함수를 호출하려는 경우에 매우 유용합니다. 기능은 아래와 비슷한 방식으로 달성 될 수 있습니다.

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

따라서 Triangles에만 Rotate 함수가 있으면 클래스의 모든 객체에서 호출하려고하면 컴파일러 오류가 발생합니다. 를 사용 dynamic_cast하면 as키워드를 시뮬레이션 할 수 있습니다 . 명확하게 말하면, 캐스트가 실패하면 유효하지 않은 포인터를 반환합니다. 따라서 !test본질적으로 testNULL 또는 유효하지 않은 포인터인지 확인하기위한 약칭으로 , 캐스트가 실패했음을 의미합니다.

자동 변수의 장점

동적 할당이 할 수있는 모든 위대한 일을보고 나면 왜 동적 할당을 항상 사용하지 않는 사람이 있는지 궁금 할 것입니다. 나는 이미 한 가지 이유를 말했지만 힙이 느립니다. 모든 메모리가 필요하지 않은 경우 남용해서는 안됩니다. 따라서 특별한 순서가없는 몇 가지 단점이 있습니다.

  • 오류가 발생하기 쉽습니다. 수동 메모리 할당은 위험하며 누수가 발생하기 쉽습니다. 디버거 나 valgrind(메모리 누수 도구) 사용에 능숙하지 않으면 머리카락을 머리에서 빼낼 수 있습니다. 운 좋게도 RAII 관용구와 똑똑한 포인터는 이것을 조금 완화하지만, Rule of Three와 The Rule Of Five와 같은 관행에 익숙해야합니다. 많은 정보가 필요하며, 모르거나 신경 쓰지 않는 초보자는이 함정에 빠질 것입니다.

  • 필요하지 않습니다. new모든 곳 에서 키워드 를 사용하는 것이 관용 인 Java 및 C #과 달리 C ++에서는 필요한 경우에만 사용해야합니다. 일반적인 문구는 망치가 있으면 모든 것이 못처럼 보입니다. C ++로 시작하는 초보자는 포인터가 무섭고 습관적으로 스택 변수를 사용하는 법을 배우는 반면 Java 및 C # 프로그래머는 포인터를 이해하지 않고 포인터를 사용하여 시작 합니다! 말 그대로 잘못된 발을 밟고 있습니다. 구문은 하나이고 언어를 배우는 것은 다른 것이므로 아는 모든 것을 버려야합니다.

1. (N) RVO-일명, (명명 된) 반환 값 최적화

많은 컴파일러가 만드는 최적화 중 하나는 elisionreturn value optimization 입니다. 이러한 것들은 많은 요소를 포함하는 벡터와 같이 매우 큰 객체에 유용한 불필요한 사본을 피할 수 있습니다. 일반적으로 큰 개체를 복사하여 이동 하지 않고 포인터를 사용하여 소유권이전 하는 것이 일반적 입니다. 이것은 이동 의미론스마트 포인터 의 시작으로 이어졌다 .

포인터를 사용하는 경우 (N) RVO가 발생 하지 않습니다 . 최적화가 걱정되면 포인터를 반환하거나 전달하는 것보다 (N) RVO를 이용하는 것이 더 유리하고 오류가 적은 경향이 있습니다. 함수의 호출자가 delete동적으로 할당 된 객체 등을 담당하는 경우 오류가 발생할 수 있습니다 . 포인터가 핫 포테이토처럼지나 가면 객체의 소유권을 추적하기가 어려울 수 있습니다. 스택 변수는 더 단순하고 우수하므로 사용하십시오.


"그래서! test는 본질적으로 test가 NULL인지 또는 유효하지 않은 포인터인지 확인하기위한 약식입니다. 이는 캐스트가 실패했음을 의미합니다." 명확성을 위해이 문장을 다시 작성해야한다고 생각합니다.
berkus

4
"Java 과대 광고 머신은 여러분이 믿기를 바랍니다."– 아마도 1997 년이되었지만 이제는 시대
Matt R

15
오래된 질문이지만 코드 세그먼트에서 { std::string* s = new std::string; } delete s; // destructor called.... delete컴파일러 s가 더 이상 무엇인지 알지 못하므로 작동하지 않습니다.
badger5000

2
나는 -1을주지 않지만, 작성된 진술에 동의하지 않습니다. 먼저, "hype"에 동의하지 않습니다. Y2K와 관련이 있었을 지 모르지만 이제는 두 언어가 모두 잘 이해되고 있습니다. 둘째, C ++은 Simula와 결혼 한 C의 자식이며 Java는 Virtual Machine, Garbage Collector를 추가하고 HEAVILY는 기능을 줄이고 C #을 간소화하고 누락 된 기능을 Java에 다시 도입합니다. 예, 이로 인해 패턴과 유효 사용이 크게 달라 지지만 공통 인프라 / 사망을 이해하면 차이점을 알 수 있습니다.
Gerasimos R

1
@James Matta : 물론 메모리는 메모리이며 모두 동일한 물리적 메모리에서 할당되지만 올바른 고려 사항은 스택 할당 된 객체로 작업하는 더 나은 성능 특성을 얻는 것이 매우 일반적이라는 것입니다. 또는 적어도 가장 높은 수준-함수가 들어오고 나갈 때 캐시에서 "핫"할 가능성이 매우 높지만 힙에는 그러한 이점이 없으므로 힙에서 포인터를 쫓는 경우 여러 캐시 누락이 발생할있습니다. 당신은 아마 스택에 없을 입니다. 그러나이 모든 "무작위"는 일반적으로 스택을 선호합니다.
Gerasimos R

23

C ++은 포인터, 참조 및 값으로 객체를 전달하는 세 가지 방법을 제공합니다. Java는 후자를 제한합니다 (int, boolean 등의 기본 유형은 예외입니다). 이상한 장난감이 아닌 C ++을 사용하려면이 세 가지 방법의 차이점을 이해하는 것이 좋습니다.

자바는 '누가 언제 언제 이것을 파괴해야 하는가?'와 같은 문제가 없다고 주장한다. 대답은 가비지 컬렉터, 위대하고 끔찍합니다. 그럼에도 불구하고 메모리 누수에 대해 100 % 보호 기능을 제공 할 수 없습니다 (예, java 메모리 누수 가능 ). 실제로 GC는 잘못된 안전 감각을 제공합니다. SUV가 클수록 진공 청소기로가는 길이 길어집니다.

C ++는 객체의 수명주기 관리와 직접 대면합니다. 스마트 포인터 제품군, Qt의 QObject 등 을 다루는 방법이 있지만 GC와 같은 '화재 및 잊어 버림'방식으로 사용할 수는 없습니다. 항상 메모리 처리를 명심 해야합니다 . 물체를 파괴하는 것에주의해야 할뿐만 아니라 같은 물체를 두 번 이상 파괴하지 않아야합니다.

아직 두렵지 않습니까? Ok : 순환 참조-스스로 처리하십시오. 그리고 기억하십시오 : 각 객체를 정확히 한 번 죽이면 C ++ 런타임은 시체를 엉망으로 만드는 사람들을 좋아하지 않으며 죽은 것을 혼자 남겨 둡니다.

다시 질문으로 돌아가십시오.

포인터 나 참조가 아닌 값으로 객체를 전달할 때 객체를 복사합니다 (전체 객체는 몇 바이트이든 큰 데이터베이스 덤프이든 관계없이 후자를 피할만큼 똑똑합니다). 당신은?) 할 때마다 '='. 그리고 객체의 멤버에 액세스하려면 '.' (점).

포인터로 객체를 전달하면 몇 바이트 (32 비트 시스템의 경우 4, 64 비트 시스템의 경우 8), 즉이 객체의 주소 만 복사합니다. 그리고 이것을 모든 사람에게 보여주기 위해 회원에게 접근 할 때이 멋진 '->'연산자를 사용합니다. 또는 '*'와 '.'의 조합을 사용할 수 있습니다.

참조를 사용하면 값을 가장하는 포인터가 나타납니다. 포인터이지만 '.'을 통해 멤버에 액세스합니다.

그리고 한 번 더 마음을 불어 넣으려면 쉼표로 구분 된 여러 변수를 선언하면 (손을보십시오) :

  • 유형은 모든 사람에게 주어집니다
  • 값 / 포인터 / 참조 수정자는 개별적입니다

예:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one

1
std::auto_ptr더 이상 사용되지 않습니다. 사용하지 마십시오.
Neil

2
참조 변수를 포함하는 초기화 목록을 생성자에 제공하지 않으면 멤버로 참조를 가질 수 없습니다. (참조는 즉시 초기화되어야한다. 심지어 생성자 본문조차 그것을 설정하기에는 너무 늦다, IIRC.)
cHao

20

C ++에서 스택에 할당 된 객체 ( Object object;블록 내 명령문 사용 )는 선언 된 범위 내에서만 존재합니다. 코드 블록이 실행을 마치면 선언 된 객체가 파괴됩니다. 을 사용하여 힙에 메모리를 할당하는 경우을 Object* obj = new Object()호출 할 때까지 메모리는 계속 힙에 유지 delete obj됩니다.

선언 / 할당 된 코드 블록뿐만 아니라 객체를 사용하고 싶을 때 힙에 객체를 만듭니다.


6
Object obj전역 또는 멤버 변수와 같이 항상 스택에있는 것은 아닙니다.
12시

2
@LightnessRacesinOrbit 전역 변수와 멤버 변수가 아니라 블록에 할당 된 객체에 대해서만 언급했습니다. 그것은 명확하지 않았으며, 이제 수정되었습니다-답변에 "블록 내"가 추가되었습니다. 거짓 정보가
아니길 바랍니다

20

그러나 왜 우리가 이것을 사용해야하는지 알 수 없습니다.

다음을 사용하면 함수 본문 내에서 어떻게 작동하는지 비교합니다.

Object myObject;

함수 내부 myObject에서이 함수가 반환되면 파괴됩니다. 따라서 함수 외부에서 객체가 필요하지 않은 경우에 유용합니다. 이 객체는 현재 스레드 스택에 배치됩니다.

함수 본문 안에 쓰는 경우 :

 Object *myObject = new Object;

myObject함수가 종료되고 할당이 힙에 있으면 객체가 가리키는 객체 클래스 인스턴스 가 소멸되지 않습니다.

Java 프로그래머라면 두 번째 예는 Java에서 객체 할당이 작동하는 방식에 더 가깝습니다. 이 줄 Object *myObject = new Object;은 java :와 같습니다 Object myObject = new Object();. 차이점은 java myObject에서는 가비지 수집이 이루어지고 c ++에서는 해제되지 않으므로 명시 적으로`delete myObject; 그렇지 않으면 메모리 누수가 발생합니다.

c ++ 11부터는 new Objectshared_ptr / unique_ptr에 값을 저장 하여 안전한 동적 할당 방법을 사용할 수 있습니다 .

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

또한 객체는 맵 또는 벡터와 같은 컨테이너에 저장되는 경우가 많으며 객체의 수명을 자동으로 관리합니다.


1
then myObject will not get destroyed once function ends절대적으로 그렇습니다.
궤도에서 가벼움 레이스

6
포인터의 경우 myObject다른 로컬 변수와 마찬가지로 여전히 파괴됩니다. 차이점은 그 값이 객체 자체가 아니라 객체에 대한 포인터 이며 벙어리 포인터의 파괴는 그 포인트에 영향을 미치지 않습니다. 따라서 그 물체 는 살아남을 것이라고 말했다.
cHao

물론 지역 변수 (포인터 포함)가 해제됩니다-스택에 있습니다.
marcinj

13

기술적으로 이것은 메모리 할당 문제이지만 여기에 두 가지 더 실용적인 측면이 있습니다. 1) 범위, 포인터없이 객체를 정의하면 코드 블록이 정의 된 후에는 더 이상 액세스 할 수 없지만 "new"로 포인터를 정의하면 같은 포인터에서 "삭제"를 호출 할 때까지이 메모리에 대한 포인터가있는 어느 곳에서나 액세스 할 수 있습니다. 2) 함수에 인수를 전달하려면보다 효율적으로 포인터 또는 참조를 전달하려고합니다. 객체를 전달하면 객체가 복사됩니다. 많은 메모리를 사용하는 객체 인 경우 CPU를 소모 할 수 있습니다 (예 : 벡터로 데이터를 복사). 포인터를 전달하면 전달하는 것은 1 int입니다 (구현에 따라 다르지만 대부분 int입니다).

그 외에는 "new"가 특정 시점에 해제해야하는 힙에 메모리를 할당한다는 것을 이해해야합니다. "새"를 사용할 필요가없는 경우 "스택에서"일반 개체 정의를 사용하는 것이 좋습니다.


6

주된 질문은 왜 객체 자체보다는 포인터를 사용해야합니까? 그리고 내 대답은 객체 대신 포인터를 사용하지 않아야합니다 .C ++에는 참조 가 있기 때문에 포인터보다 안전하고 포인터와 동일한 성능을 보장합니다.

당신이 당신의 질문에 언급 한 또 다른 것 :

Object *myObject = new Object;

어떻게 작동합니까? 그것은 Object유형의 포인터를 만들고 , 하나의 객체에 맞게 메모리를 할당하고 기본 생성자를 호출합니다. 그러나 실제로 메모리가 좋지 않은 경우 (키워드 사용 new) 메모리를 수동으로 해제해야합니다. 즉, 코드에서 다음을 가져야합니다.

delete myObject;

이것은 소멸자를 호출하고 메모리를 해제하고 쉽게 보입니다. 그러나 큰 프로젝트에서는 하나의 스레드가 메모리를 해제했는지 여부를 감지하기 어려울 수 있지만 공유 목적을 시도 하면 성능이 약간 저하되지만 작업하기가 훨씬 쉽습니다. 그들.


이제 소개가 끝났고 다시 질문으로 돌아갑니다.

함수 대신 데이터를 전송하는 동안 성능을 높이기 위해 객체 대신 포인터를 사용할 수 있습니다.

살펴보면 std::string(객체이기도 함) 큰 XML과 같은 많은 양의 데이터가 포함되어 있으므로 구문 분석해야하지만 void foo(...)다른 방법으로 선언 할 수있는 기능 이 있습니다.

  1. void foo(std::string xml); 이 경우 변수의 모든 데이터를 함수 스택으로 복사하면 시간이 걸리므로 성능이 저하됩니다.
  2. void foo(std::string* xml); 이 경우 size_t변수 전달과 동일한 속도로 객체에 포인터를 전달 하지만 NULL포인터 또는 유효하지 않은 포인터를 전달할 수 있기 때문에이 선언에 오류가 발생하기 쉽습니다 . C참조가 없기 때문에 일반적으로 사용되는 포인터 .
  3. void foo(std::string& xml); 여기에서는 참조를 전달합니다. 기본적으로 포인터를 전달하는 것과 동일하지만 컴파일러는 일부 작업을 수행하고 유효하지 않은 참조를 전달할 수 없습니다 (실제로 유효하지 않은 참조로 상황을 만들 수는 있지만 컴파일러를 속이는 것입니다).
  4. void foo(const std::string* xml); 두 번째와 동일하며 포인터 값만 변경할 수 없습니다.
  5. void foo(const std::string& xml); 여기는 세 번째와 동일하지만 객체 값을 변경할 수 없습니다.

더 언급하고 싶은 것은, 당신이 선택한 할당 방법 (with new또는 regular )에 관계없이이 5 가지 방법을 사용하여 데이터를 전달할 수 있습니다 .


언급해야 할 또 다른 사항은 정기적 으로 객체를 만들 때 스택에 메모리를 할당하지만 new힙을 할당 하면서 메모리 를 할당하는 것입니다. 스택을 할당하는 것이 훨씬 빠르지 만 실제로 큰 데이터 배열의 경우 크기가 작으므로 큰 객체가 필요한 경우 스택 오버플로가 발생할 수 있으므로 힙을 사용해야하지만 일반적 으로이 문제는 STL 컨테이너를 사용하여 해결 되고 기억하십시오. std::string또한 컨테이너이며 일부 사람들은 그것을 잊었습니다 :)


5

당신이 class A포함하고 있다고 가정 해 봅시다 외부의 class B함수를 호출하고 싶을 때이 클래스에 대한 포인터를 얻으면 원하는대로 무엇이든 할 수 있으며 컨텍스트의 컨텍스트가 변경 됩니다.class Bclass Aclass Bclass A

그러나 동적 객체에주의하십시오.


5

객체에 대한 포인터를 사용하면 많은 이점이 있습니다.

  1. 효율성 (이미 지적한대로). 객체를 함수에 전달한다는 것은 객체의 새로운 사본을 만드는 것을 의미합니다.
  2. 타사 라이브러리의 객체 작업 객체가 타사 코드에 속하고 작성자가 포인터 만 사용하여 (복사 생성자 등이 아닌) 객체를 사용하려는 경우이 객체를 전달할 수있는 유일한 방법은 포인터를 사용하는 것입니다. 값으로 전달하면 문제가 발생할 수 있습니다. (깊은 복사 / 얕은 복사 문제).
  3. 개체가 리소스를 소유하고 있고 다른 개체와 소유권을 공유해서는 안되는 경우

3

이것은 오랫동안 논의되었지만 Java에서는 모든 것이 포인터입니다. 스택 할당과 힙 할당 (모든 객체가 힙에 할당 됨)을 구분하지 않으므로 포인터를 사용하고 있다는 사실을 모릅니다. C ++에서는 메모리 요구 사항에 따라 두 가지를 혼합 할 수 있습니다. C ++ (duh)에서 성능 및 메모리 사용량이보다 결정적입니다.


3
Object *myObject = new Object;

이렇게하면 메모리 누수 를 피하기 위해 명시 적으로 삭제해야하는 객체 (힙에서)에 대한 참조가 생성됩니다 .

Object myObject;

이렇게하면 자동 유형 (스택)의 객체 (myObject)가 생성되어 객체 (myObject)가 범위를 벗어날 때 자동으로 삭제됩니다.


1

포인터는 객체의 메모리 위치를 직접 참조합니다. 자바에는 이런 것이 없습니다. Java에는 해시 테이블을 통해 객체의 위치를 ​​참조하는 참조가 있습니다. 이러한 참조를 사용하여 Java에서 포인터 산술과 같은 작업을 수행 할 수 없습니다.

귀하의 질문에 대답하기 위해서는 귀하가 선호하는 것입니다. Java와 같은 구문을 선호합니다.


해시 테이블? 일부 JVM에서는 가능하지만 신뢰할 수는 없습니다.
Zan Lynx

Java와 함께 제공되는 JVM은 어떻습니까? 물론 포인터를 직접 사용하는 JVM 또는 포인터 수학을 수행하는 메소드처럼 생각할 수있는 모든 것을 구현할 수 있습니다. 그것은 "사람들이 감기에 걸리지 않는다"고 말하고 "대부분의 사람들은 아마 그것을 믿지 않을 것입니다!"라고 대답하는 것과 같습니다. ㅋ.
RioRicoRick

2
@RioRicoRick HotSpot은 Java 참조를 기본 포인터로 구현합니다 ( docs.oracle.com/javase/7/docs/technotes/guides/vm/ 참조). JRockit도 마찬가지입니다. 둘 다 OOP 압축을 지원하지만 해시 테이블을 사용하지는 않습니다. 성능 결과는 아마도 비참 할 것입니다. 또한 "그것은 단지 당신의 취향에 불과하다"는 것은 두 행동이 동등한 행동에 대한 다른 문법 일 뿐이며 물론 그렇지 않다는 것을 암시하는 것으로 보인다.
Max Barraclough


0

포인터

  • 직접 메모리와 대화 할 수 있습니다.

  • 포인터를 조작하여 프로그램의 많은 메모리 누수를 방지 할 수 있습니다.


4
" C ++에서는 포인터를 사용하여 자신의 프로그램에 맞는 사용자 정의 가비지 수집기를 만들 수 있습니다 ."
quant

0

포인터를 사용하는 한 가지 이유는 C 함수와 인터페이스하기위한 것입니다. 또 다른 이유는 메모리를 절약하는 것입니다. 예를 들어, 많은 데이터를 포함하고 프로세서 중심의 복사 생성자를 가진 객체를 함수에 전달하는 대신 객체에 포인터를 전달하면 특히 루프에있는 경우 메모리와 속도를 절약 할 수 있습니다. 이 경우 C 스타일 배열을 사용하지 않는 한 참조가 더 좋습니다.


0

메모리 사용률이 가장 높은 영역에서는 포인터가 편리합니다. 예를 들어, 재귀적인 루틴을 사용하여 수천 개의 노드가 생성되는 미니 맥스 알고리즘을 고려한 다음,이 노드를 사용하여 게임에서 다음 최고 이동, 스마트 포인터에서와 같이 할당 해제 또는 재설정 기능을 사용하여 메모리 소비를 크게 줄입니다. 비 포인터 변수는 재귀 호출이 값을 반환 할 때까지 공간을 계속 차지합니다.


0

포인터의 중요한 사용 사례 하나를 포함하겠습니다. 기본 클래스에 일부 객체를 저장하고 있지만 다형성 일 수 있습니다.

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

따라서이 경우 bObj를 직접 객체로 선언 할 수 없으므로 포인터가 있어야합니다.


-5

"필요는 발명의 어머니이다." 내가 지적하고 싶은 가장 중요한 차이점은 코딩 경험의 결과입니다. 때로는 객체를 함수에 전달해야합니다. 이 경우 객체가 매우 큰 클래스 인 경우 객체로 전달하면 객체의 상태가 복사됩니다 (원치 않을 수 있습니다. .. 그리고 너무 큼직 할 수 있음). 복사 객체의 오버 헤드가 발생합니다. 4 바이트 크기 (32 비트로 가정). 다른 이유는 이미 위에서 언급했습니다 ...


14
참조로 전달하는 것이 좋습니다
bolov

2
나는 변수와 같은 일정 참조로 전달하는 것이 좋습니다 std::string test;우리가 가지고 void func(const std::string &) {}있지만 기능은 내가 포인터를 사용하는 것이 좋습니다 경우에 입력을 변경해야하지 않는 한 (코드를 읽고 그 사람이 통지를하지 않도록 &하고, 입력을 변경할 수있는 기능을 이해)
한 탑을 마스터

-7

이미 훌륭한 답변이 많이 있지만 한 가지 예를 들어 보겠습니다.

간단한 Item 클래스가 있습니다.

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

나는 그것들을 잔뜩 담을 벡터를 만듭니다.

std::vector<Item> inventory;

백만 개의 Item 객체를 만들고 벡터로 다시 밀어 넣습니다. 벡터를 이름별로 정렬 한 다음 특정 항목 이름에 대한 간단한 반복 이진 검색을 수행합니다. 프로그램을 테스트했는데 실행을 마치는 데 8 분이 걸립니다. 그런 다음 인벤토리 벡터를 다음과 같이 변경합니다.

std::vector<Item *> inventory;

... 새로운 것을 통해 백만 개의 Item 객체를 만듭니다. 내가 코드를 변경하는 유일한 것은 마지막에 메모리 정리를 위해 추가 한 루프를 제외하고 항목에 대한 포인터를 사용하는 것입니다. 이 프로그램은 40 초 이내에 실행되거나 10 배 속도 증가보다 우수합니다. 편집 : 코드는 http://pastebin.com/DK24SPeW에 있습니다 . 컴파일러 최적화를 사용하면 방금 테스트 한 컴퓨터에서 3.4 배 증가한 것으로 나타났습니다.


2
그렇다면 포인터를 비교하고 있습니까 아니면 실제 객체를 여전히 비교합니까? 나는 다른 수준의 간접 지향성이 성능을 향상시킬 수 있다는 것을 의심한다. 코드를 입력하십시오! 나중에 제대로 청소합니까?
stefan

1
@ stefan 정렬과 검색을 위해 객체의 데이터 (특히 이름 필드)를 비교합니다. 게시물에서 이미 언급했듯이 올바르게 정리합니다. 속도 향상은 아마도 두 가지 요인 때문일 것입니다 : 1) std :: vector push_back ()은 객체를 복사하므로 포인터 버전은 객체 당 단일 포인터 만 복사하면됩니다. 데이터 복사가 적을뿐만 아니라 벡터 클래스 메모리 할당자가 덜 쓰러지기 때문에 성능에 여러 영향을 미칩니다.
Darren

2
다음은 예제와 실질적으로 차이가없는 코드입니다. 정렬. 포인터 코드는 정렬 방식만으로 비 포인터 코드보다 6 % 빠르지 만 전반적으로 비 포인터 코드보다 10 % 느립니다. ideone.com/G0c7zw
stefan

3
핵심어 : push_back. 물론 이것은 복사합니다. emplace다른 곳에서 캐시해야하는 경우가 아니라면 객체를 만들 때 제대로 설치되어 있어야합니다.
underscore_d

1
포인터의 벡터는 거의 항상 잘못되었습니다. 주의 사항과 장단점을 자세히 설명하지 않고는 권장하지 마십시오. 당신은 잘못 코딩 카운터 - 예를의 결과 인 한 프로 발견하는 것, 그리고 잘못
궤도의 밝기 경주
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.