c++STL vector<> 에 추가할때 클래스 소멸질문입니다.
글쓴이: hanty11 / 작성시간: 월, 2016/02/29 - 5:15오후
실험(?)을 위해 Integer클래스를 만들었습니다.
class Integer { int n; public : Integer(int _n = 0): n(_n){} ~Integer() { cout << "del : " << n << endl; } operator int () const{ return n; } };
소멸할때마다 n을 외치고 죽습니다.
그리고 vector에 5까지 추가해서 출력해봤더니
void main(){ vector<Integer> v; cout << "insert 1" << endl; v.push_back(Integer(1)); cout << "insert 2" << endl; v.push_back(Integer(2)); cout << "insert 3" << endl; v.push_back(Integer(3)); cout << "insert 4" << endl; v.push_back(Integer(4)); cout << "insert 5" << endl; v.push_back(Integer(5)); cout << endl << "======================" << endl; }
결과는 이렇게 나왔습니다.
insert 1
del : 1
insert 2
del : 1
del : 2
insert 3
del : 1
del : 2
del : 3
insert 4
del : 1
del : 2
del : 3
del : 4
insert 5
del : 1
del : 2
del : 3
del : 4
del : 5
======================
del : 1
del : 2
del : 3
del : 4
del : 5
계속하려면 아무 키나 누르십시오 . . .
질문은 왜 추가할때마다 vector에 들어있는 원소들이 한번씩 차례로 다 소멸되는건가요?
Forums:
Integer(1)로 생성된 임시 객체가 백터 안으로
Integer(1)로 생성된 임시 객체가 백터 안으로 push된 후에 해당 임시 객체가 소멸됩니다.
vector에 push할때 할당해둔 메모리 공간이 부족할 경우 더 큰 공간을 할당한 후 기존에 저장된 원소들을 새로 할당한 공간에 복사하고 기존의 메모리 공간에 존재하던 원소들을 소멸시킨 후 기존의 메모리 공간을 해제합니다.
보통은 2*n + 1같이 점점 더 큰 공간을 할당하는데 어떤 구현체를 사용중이신지 몰라도 해당 구현체가 기존의 공간보다 원소 1개만큼만 더 큰 공간을 매번 할당하나보네요.
예상하자면 다음과 같습니다.
insert 1
del : 1 (임시 객체 소멸)
insert 2
del : 1 (재할당되면서 기존 객체 소멸)
del : 2 (임시 객체 소멸)
insert 3
del : 1 (재할당되면서 기존 객체 소멸)
del : 2 (재할당되면서 기존 객체 소멸)
del : 3 (임시 객체 소멸)
insert 4
del : 1 (재할당되면서 기존 객체 소멸)
del : 2 (재할당되면서 기존 객체 소멸)
del : 3 (재할당되면서 기존 객체 소멸)
del : 4 (임시 객체 소멸)
insert 5
del : 1 (재할당되면서 기존 객체 소멸)
del : 2 (재할당되면서 기존 객체 소멸)
del : 3 (재할당되면서 기존 객체 소멸)
del : 4 (재할당되면서 기존 객체 소멸)
del : 5 (임시 객체 소멸)
======================
del : 1 (vector가 소멸되면서 내부 원소 소멸)
del : 2 (vector가 소멸되면서 내부 원소 소멸)
del : 3 (vector가 소멸되면서 내부 원소 소멸)
del : 4 (vector가 소멸되면서 내부 원소 소멸)
del : 5 (vector가 소멸되면서 내부 원소 소멸)
답변감사합니다.
vector가 용량을 키울때 기존객체를 복사하느라그런거였군요
알려주셔서 감사합니다!!
+1
+1
저는 이렇게 생각했습니다.
두 가지 이유가 있습니다. 1. push_back에
두 가지 이유가 있습니다.
1.
push_back
에 넘겨주는 temporary의 소멸.예컨대
v.push_back(Integer(3));
를 호출했을 때, temporary인Integer
객체가 생성자 인수 3을 받아서 생성됩니다.temporary는 rvalue인데,
push_back
의 인수는 const reference이죠.따라서 temporary가 const reference에 bound될 때의 규칙에 따르게 됩니다.
이 규칙에 대한 얘기는 https://kldp.org/node/154710 에서 제가 한 번 길게 설명했었는데 관심 있으시면 가서 읽어보시고요.
여기서 필요한 부분은 "function call 과정에서 reference parameter에 bound된 temporary는 함수 호출을 포함하는 full expression의 completion까지 살아있게 된다." 입니다.
push_back
은 매개변수로 넘어온 객체를,vector
가 미리 할당해 놓은 공간에 복사하는데요.그러고 나면 원본이었던 temporary는
push_back
함수 호출을 포함하는 full expression의 completion까지만 살아 있다가 소멸하게 됩니다.full expression이니 뭐니 하는 자세한 설명은 필요 없으니 생략하고요. 짧게 줄이면, 위의 예에서는
v.push_back(Integer(3));
의 실행이 끝난 뒤에,Integer(3)
로 인해 생성된 temporary가 소멸하는 겁니다.따라서
v.push_back(Integer(3));
뒤에는 반드시Integer(3)
의 소멸자가 뒤따르는 겁니다.2.
vector
의 reallocation에 의한 앞선 객체의 소멸근데 그러면
Integer(1)
,Integer(2)
의 소멸자는 왜 호출된 걸까요?이는
vector
의 동작 원리에 의한 것입니다.vector
는 반드시 자신이 포함하는 객체들을 연속된 메모리 영역에 배치해야 합니다.하지만 연속된 메모리 영역은 무한히 확장해갈 수 없지요. 그래서 처음에는 작은 크기만을 할당해 놓습니다.
그러다가 할당된 영역을 모두 쓰고 나면, 더 큰 메모리 영역을 할당하고, 모든 객체를 복사한 뒤, 이전에 쓰던 메모리 영역을 할당 해제합니다.
그 과정에서 복사 생성자(이전 영역의 객체로부터 새 영역으로 복사 생성)와 소멸자(이전 영역에 있던 객체를 소멸시킴)가 필요하지요.
(C++11에서는 복사 생성이 아니라 이동 생성될 수도 있습니다. 그래도 소멸자는 호출됩니다.)
vector<..> v
에 대해서,v
에 현재 할당된 메모리 영역에 객체가 최대 몇 개까지 들어갈 수 있는지를 반환하는 멤버 함수는capacity
입니다. 반면 실제로 들어가 있는 객체의 수는size
멤버 함수를 호출하면 알 수 있습니다.그러니
size
=capacity
일 때 (즉vector
가 꽉 찼을 때)push_back
을 호출하면,vector
에서 reallocation이 일어나면서 이전 메모리 영역에 있었던 모든 객체에 대해 소멸자가 호출되는 것이죠.한 가지 더 짚고 넘어가죠. reallocation이 일어났을 때
vector
가 할당하는 새 메모리 영역의 크기가 이전에 비해 얼마나 커지느냐 하는 건 라이브러리 개발자가 정하기 나름입니다. 보통 2배로 커집니다.아래 코드를 보세요.
실행 결과는 아래와 같습니다.
push_back
으로 삽입하는 temporary의 소멸은 항상 관찰되는 반면, 이전 객체들의 소멸은size
=capacity
일 때 (즉vector
가 꽉 찼을 때)push_back
을 호출했을 때만 발생하죠? 또한 그 이후에capacity
가 어떻게든 이전 값보다 커져 있다는 것도 주목할 만합니다. 구체적으로 얼마나 늘었는지는 구현환경마다 다를 수 있어요. 저는 Ubuntu 14.04, x86_64, g++ 4.8.2로 돌렸습니다.3. 그래서, 이런 소멸자 호출을 없앨 수 있을까요?
1) push_back에 넘기는 temporary의 소멸은 피할 수 없었습니다. C++03까지는 말이죠.
C++11부터는 다른 방법이 하나 생겼는데요. 차근차근 설명 드리죠.
우선 C++11 컴파일러의 눈에는 위 코드가 C++03 컴파일러가 봤을 때와 좀 다르게 보입니다.
C++11의 관점에서 볼 때,
push_back
에 넘겨지는 temporary는 rvalue이고, 분명히push_back
에 넘겨지는 용도로밖에 안 쓰일 거란 말이죠.그래서 C++11의
push_back
는 temporary를vector
로 "복사"하는 게 아니라 "이동"시킵니다. 대부분의 상황에서, 객체를 이동시키는 건 복사하는 것보다 더 저렴하기 마련이죠.하지만 "이동"될 때에도 원본 temporary에는 여전히 소멸자가 호출되어야 합니다. 하지만 그것조차도 막을 수 있는 방법이 C++11에는 있는데, 그게 바로 Perfect forward(완벽 전달)을 기반으로 한 새 멤버 함수,
emplace_back
이죠.요점은 이렇습니다. 괜히 temporary를 만들어서
push_back
를 통해vector
내부로 이동시키지 말고, 그냥 생성자 인수들을 건네 주면vector
가 스스로 필요한 위치에 객체를 생성하겠다는 거에요.직접 한 번 보세요. 컴파일하려면 C++11을 지원하는 컴파일러에 C++11 활성화 옵션을 줘야 합니다.
실행 결과는 아래와 같습니다.
짠. 이제
Integer(3)
를 넣을 때 3에 대한 소멸자가 호출되지 않습니다. 1, 2, 4, 5에 대해서도 마찬가지고요.vector
가 생성자 인수(int
)를 받아 필요한 자리에 곧바로 생성하니까요.2) 근데 여전히
vector
의 reallocation에 의한 소멸자 호출은 일어나는군요.vector
reallocation은 가변 길이의 연속된 메모리 영역을 쓰는 데 있어서 거의 어쩔 수 없는 비용입니다.하지만 꽤 비싼 비용이죠. 사실 소멸자 호출보다도, 한 번 일어날 때마다
vector
안의 모든 객체를 복사(C++11에서는 이동일 수도 있음)해야 하는 부담이 훨씬 더 커요.그러니, 막을 수 있다면 막는 것이 좋습니다.
vector
의 reallocation을 예방하는 방법은,vector
의 내부 메모리 영역을 일찌감치 충분히 크게 잡아놓는 겁니다.때로는
vector
안에 들어갈 객체의 최대 수를 어림잡을 수 있잖아요. 그 크기로vector
를 미리 예약(reserve)하면 됩니다. 마침 멤버 함수 이름도 그대로reserve
입니다.아래 코드를 보세요. 여전히 C++11 컴파일이 필요합니다.
reserve
는 C++03때도 있었던 멤버 함수이지만, 이 예제도emplace_back
을 쓰거든요.vector
에 객체를 다섯 개까지만 넣을 것을 미리 알고 있었으므로, 미리 크기를 5로 예약했습니다.실행 결과는 아래와 같습니다.
reserve
멤버 함수의 호출로 인해vector
안의 객체 숫자는 변하지 않았지만(아무 것도 안 넣었으니까요) 용량은 5로 미리 예약된 것을 확인할 수 있습니다. 이후 reallocation은 전혀 일어나지 않았고, 소멸자도 (vector
자체가 소멸되기 전까지) 한 번도 안 불렸습니다. 해결 됐죠. 그렇죠?3) 덤.
reserve
는vector
를 효율적으로 사용하는 데 매우 중요한 멤버 함수입니다. 알아 두세요.하지만
vector
를 필요 이상으로 크게 예약해 두면 그만큼의 메모리가 묶여 있게 됩니다. 필요가 없어지면 풀어 줘야죠.하지만
vector
의 용량을 줄이기 위해reserve
에 현재 용량보다 작은 값을 넘겨서 호출해 봤자 아무 의미도 없습니다.reserve
는vector
의 용량을 키우는 역할만 하거든요.vector
의 용량을 줄이는 방법은 C++03까지는 간단한 방법이 없어서,swap
을 이용한 약간 복잡한 편법을 동원해야만 했죠. C++11부터는 간단한 방법이 생겼습니다.shrink_to_fit
이라는 멤버 함수입니다.자세한설명 진짜감사드립니다.
사실 학생인데 성능부분에대한 관심이 많아서 좀 공부하고있다가
소멸자가 계속호출되는걸 알게되고 궁금했던거였거든요
자세한설명 정말 감사드립니다.
좋은 기법들 배워갑니다!
좋은 설명 감사합니다
이동 생성자에 대해 공부하고 있었는데 다른 방법도 알게 되었네요
감사합니다 !
?
gcc(libstdc++) 쪽에서 테스트해보면 2 4 8 16 32 ... 단위로 reserve 하는 걸로 보입니다.
vs2015 에서 테스트해보면 글쓴 분과 같은 결과가 나옵니다.
vs2015 에서는 더 크게 테스트해보면 5 7 10 14 20 29 43 64 ... (!?) 단위로 reserve 하네요.
늘리는 부분 소스는 대략 아래와 같습니다.
size_type _Grow_to(size_type _Count) const
{ // grow by 50% or at least to _Count
size_type _Capacity = capacity();
_Capacity = max_size() - _Capacity / 2 < _Capacity
? 0 : _Capacity + _Capacity / 2; // try to grow by 50%
if (_Capacity < _Count)
_Capacity = _Count;
return (_Capacity);
}
희한하네요.
g++ -std=c++11 로 컴파일 했는데 저도
g++ -std=c++11 로 컴파일 했는데 저도 1개씩 증가시키네요.
저도 2배씩 메모리 할당 받는 것으로 알고 있었는데데
네..뭔가 규칙이있을거같긴한데
VC2015기준으론
1 - 2 - 3 - 4 - 6 - 9 - 13 - 19 - 28 -42
이러네요...
덧붙이자면,vector의 메모리를 미리 잡아두는
덧붙이자면,
vector의 메모리를 미리 잡아두는 메소드가 존재합니다.
그리고 생성자는 존재하는데 복사 생성자가 없네요.
STL 컨테이너들은 기본적으로 Call by Value로 동작하기 때문에 복사 생성자가 꼭 필요합니다.
이 경우에는 동적할당된 멤버변수가없어서 문제 없지만
그렇지 않는 경우 심각한 메모리 참조 오류가 발생합니다.
C++11을 오랫동안 사용안했더니
C++11을 오랫동안 사용안했더니 emplace_back 메소드를 완전 잊고 있었네요.
작성한 코드는 너무 옛날 방식이라 삭제합니다. 익명분 코드 보시면 도움이 많이 될 듯 하네요.
감사합니다
STL쓸때 복사생성자없을때 메모리문제는 생각못했네요
팁주셔서 감사합니다.
몇 가지 더...
STL 컨테이너가 Call by Value로 동작한다는 말씀은 무슨 의미인지 잘 이해되지 않는군요.
의도하셨던 바는 아마도 C++03까지의 STL 컨테이너들이 객체의 복사 기능에 의존하여 동작한다는 말씀이시겠죠?
C++11부터는 사실 객체가 복사 가능하지 않아도 이동 가능하기만 하면(즉 이동 생성/대입이 가능하면) 괜찮은 경우가 많습니다.
사실 STL이 객체를 복사하려던 이유도, 정말 복제본을 만들기 위해서라기보다는 이동을 위해서였으니까요.
그리고 복사 생성자와 복사 대입 연산자는 C++03의 특수 멤버 함수에 속하며, 직접 정의하지 않아도 컴파일러가 암시적으로 적절히 trivial한 꼴으로 만들어 줍니다. 사실 C++03에서는 이들 함수를 정의하지 않으면서 컴파일러가 대신 작성해주는 걸 금지하는 편이 좀 더 까다롭습니다.
그러나 소위 3의 법칙이라고 하여, 객체의 소멸자/복사 생성자/복사 대입 연산자 중 하나라도 직접 작성하였다면(그럴 필요가 있었다면) 나머지 둘도 직접 작성하는 것이 바람직합니다. 논리적 근거가 있는데, 자세한 내용은 생략하도록 하죠.
이 질문 예제의 경우엔 애초에 짧은 예제이기도 하고, 소멸자를 직접 정의한 이유도 다른 게 아니라 그냥 로그를 남기기 위한 목적이므로, 컴파일러가 복사 생성자를 작성하도록 하는 걸 그다지 거리낄 필요는 없을 것 같다고 생각합니다.
3의 법칙은 C++11에서도 여전히 유의미한 지침이며, C++11 컴파일러는 여전히 사용자 정의 복사 생성자가 없을 때 자동으로 작성해 주기는 하지만, "사용자가 정의한 소멸자/복사 대입 연산자가 있을 때 컴파일러가 복사 생성자를 암시적으로 대신 작성해 주는 것"은 비권장 기능으로 분류되어 있습니다. 즉 가급적이면 복사 생성자와 복사 대입 연산자도 같이 정의해 주시는 편이 좋습니다.
사실 C++11에선 명시적으로 컴파일러한테 대신 작성해 달라고 하거나, 컴파일러의 작성을 금지해버리는 방법도 있습니다.
마찬가지로 C++11 컴파일이 필요한 코드 제시해 드리겠습니다.
gilgil.net
좋은 정보 감사합니다.
www.gilgil.net
user define class를 가지고 테스트를
user define class를 가지고 테스트를 하는 방법도 있지만,
malloc, free, new, delete 등을 재정의해서
primitive type(int)을 가지고도 메모리의 작동을 확인할 수 있습니다.
참고해 보시기 바랍니다.
http://www.gilgil.net/827397
www.gilgil.net
오이렇게보는방법도있네요
잘만활용하면 엄청편할것같습니다 감사합니다
gilgil.net
굳이 qt 사용할 필요도 없구요, 그냥 src/mem 폴더에 있는 소스 코드 그냥 붙여 넣기만 하셔도 됩니다. ^^
www.gilgil.net
댓글 달기