C ++ 프로그램에서 메모리가 누출되지 않도록하는 일반적인 팁은 무엇입니까? 동적으로 할당 된 메모리를 누가 확보해야하는지 어떻게 알 수 있습니까?
C ++ 프로그램에서 메모리가 누출되지 않도록하는 일반적인 팁은 무엇입니까? 동적으로 할당 된 메모리를 누가 확보해야하는지 어떻게 알 수 있습니까?
답변:
메모리를 수동으로 관리하는 대신 해당되는 경우 스마트 포인터를 사용하십시오. Boost lib , TR1 및 스마트 포인터를
살펴보십시오 .
또한 스마트 포인터는 이제 C ++ 11 이라는 C ++ 표준의 일부입니다 .
RAII 및 스마트 포인터에 대한 모든 조언을 철저히 보증하지만 약간 더 높은 수준의 팁을 추가하고 싶습니다. 관리하기 가장 쉬운 메모리는 할당하지 않은 메모리입니다. 거의 모든 것이 참조 인 C # 및 Java와는 달리 C ++에서는 가능할 때마다 객체를 스택에 배치해야합니다. Stroustrup 박사를 포함한 여러 사람들이 지적한 것처럼 C ++에서 가비지 수집이 인기가 없었던 주된 이유는 잘 작성된 C ++이 처음에는 많은 가비지를 생산하지 않기 때문입니다.
쓰지 마
Object* x = new Object;
또는
shared_ptr<Object> x(new Object);
그냥 쓸 수있을 때
Object x;
이 게시물은 반복적 인 것으로 보이지만 C ++에서 알아야 할 가장 기본적인 패턴은 RAII 입니다.
boost, TR1 또는 심지어는 낮지 만 (충분히 효율적인) auto_ptr에서 스마트 포인터를 사용하는 법을 배우십시오 (그러나 그 한계를 알아야합니다).
RAII는 C ++에서 예외 안전 및 리소스 처리의 기초이며, 다른 패턴 (샌드위치 등)은 둘 다를 제공하지 않습니다 (대부분의 경우 아무 것도 제공하지 않음).
RAII와 비 RAII 코드의 비교는 아래를 참조하십시오.
void doSandwich()
{
T * p = new T() ;
// do something with p
delete p ; // leak if the p processing throws or return
}
void doRAIIDynamic()
{
std::auto_ptr<T> p(new T()) ; // you can use other smart pointers, too
// do something with p
// WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}
void doRAIIStatic()
{
T p ;
// do something with p
// WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}
요약하면 ( Ogre Psalm33 의 의견 이후 ) RAII는 다음 세 가지 개념에 의존합니다.
이것은 올바른 C ++ 코드에서 대부분의 객체가로 구성되지 new
않고 대신 스택에서 선언 됨을 의미합니다 . 그리고를 사용하여 생성 된 사람들의 new
경우 모두 범위가 지정됩니다 (예 : 스마트 포인터에 연결).
개발자는 수동 리소스 처리 (C에서 수행 한 것처럼 또는 Java의 경우 try
/ finally
를 집중적으로 사용하는 일부 객체)에 신경 쓸 필요가 없으므로 매우 강력합니다 ...
"범위가 지정된 객체는 ... 종료 여부에 관계없이 파괴됩니다"는 전적으로 사실이 아닙니다. RAII를 속이는 방법이 있습니다. terminate ()의 풍미는 정리를 우회합니다. 이와 관련하여 exit (EXIT_SUCCESS)는 옥시 모론입니다.
– 빌헬름 텔
wilhelmtell 은 그 점에 대해 매우 옳습니다. RAII를 속이는 예외적 인 방법 이 있습니다 .
사람들은 뛰어난 C ++ 코드 등, 종료 종료 뒤덮되지 않기 때문에 방법, 또는 예외의 경우에, 우리는 싶어 처리되지 않은 예외가 그대로 공정 및 코어 덤프 메모리 이미지를 충돌, 그리고 청소 후.
그러나 거의 발생하지 않지만 여전히 발생할 수 있기 때문에 이러한 경우에 대해 알아야합니다.
(누가 호출 terminate
또는 exit
캐주얼 C에서 ++ 코드는 ... 내가 함께 연주 할 때 그 문제를 해결하는 데 기억? GLUT :이 라이브러리는 멀리 적극적으로 개발자가 배려하지 좋아 ++ C에 대한 일을 어렵게 만들 그것을 디자인으로가는 매우 C 지향 에 대해 스택에 할당 된 데이터 , 또는에 대해 "흥미로운"결정을 가진 적이 자신의 메인 루프에서 돌아 없습니다 ... 나는) 그것에 대해 언급하지 않습니다 .
부스트의 스마트 포인터 와 같은 스마트 포인터를보고 싶을 것 입니다.
대신에
int main()
{
Object* obj = new Object();
//...
delete obj;
}
boost :: shared_ptr은 참조 횟수가 0이되면 자동으로 삭제됩니다.
int main()
{
boost::shared_ptr<Object> obj(new Object());
//...
// destructor destroys when reference count is zero
}
마지막 참고 사항 인 "참조 횟수가 0 일 때 가장 멋진 부분입니다. 따라서 개체를 여러 명 사용하는 경우 개체가 아직 사용 중인지 여부를 추적 할 필요가 없습니다. 공유 포인터, 그것은 파괴된다.
그러나 이것은 만병 통치약이 아닙니다. 기본 포인터에 액세스 할 수 있지만 수행중인 작업에 대한 확신이 없으면 타사 API에 전달하지 않습니다. 많은 경우, 작성 범위가 완료된 후 작업을 수행하기 위해 다른 스레드에 "게시"하는 것들. 이것은 Win32의 PostThreadMessage에서 일반적입니다.
void foo()
{
boost::shared_ptr<Object> obj(new Object());
// Simplified here
PostThreadMessage(...., (LPARAM)ob.get());
// Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes!
}
항상 그렇듯이 모든 도구와 함께 사고 뚜껑을 사용하십시오 ...
대부분의 메모리 누수는 개체 소유권과 수명에 대해 명확하지 않은 결과입니다.
가장 먼저 할 일은 가능하면 스택에 할당하는 것입니다. 이것은 어떤 목적으로 단일 객체를 할당해야하는 대부분의 경우를 다룹니다.
객체를 '새로 작성'해야하는 경우 대부분의 수명 동안 나머지 한 명의 명백한 소유자가 있습니다. 이 상황에서는 포인터로 저장된 개체를 '소유'하기 위해 설계된 많은 컬렉션 템플릿을 사용하는 경향이 있습니다. 그것들은 STL 벡터 및 맵 컨테이너로 구현되지만 몇 가지 차이점이 있습니다.
STL에 대한 나의 비애는 그것이 Value 객체에 너무 집중되어 있고 대부분의 응용 프로그램에서 객체는 컨테이너에 사용하기 위해 의미있는 복사 의미가없는 고유 한 엔티티입니다.
바, 당신은 어린 아이들과 새로운 쓰레기 수집가들 ...
"소유권"에 대한 매우 강력한 규칙-개체를 삭제할 권한이있는 소프트웨어의 개체 또는 부분 포인터가 "소유"하거나 "단지 보거나 만지지 마십시오"라는 명확한 설명과 현명한 변수 이름 누가 무엇을 소유하는지 결정하려면 모든 서브 루틴 또는 방법 내에서 가능한 한 "샌드위치"패턴을 따르십시오.
create a thing
use that thing
destroy that thing
때로는 광범위하게 다른 장소에서 창조하고 파괴해야 할 때가 있습니다. 나는 그것을 피하기 어렵다고 생각합니다.
복잡한 데이터 구조가 필요한 모든 프로그램에서 "소유자"포인터를 사용하여 다른 객체를 포함하는 객체의 엄격한 명확한 트리를 만듭니다. 이 트리는 응용 프로그램 도메인 개념의 기본 계층을 모델링합니다. 예를 들어 3D 장면은 객체, 조명, 텍스처를 소유합니다. 프로그램이 종료 될 때 렌더링이 끝나면 모든 것을 파괴 할 수있는 명확한 방법이 있습니다.
하나의 엔티티가 다른 엔티티에 액세스해야 할 때마다, 또는 다른 것들을 스캔하기 위해 필요할 때마다 다른 많은 포인터가 정의됩니다. 이것들은 "그냥보고있는"것입니다. 3D 장면 예제의 경우, 오브젝트는 텍스처를 사용하지만 소유하지는 않습니다. 다른 물체도 같은 질감을 사용할 수 있습니다. 오브젝트의 파괴는 텍스처의 파괴를 유발 하지 않습니다 .
예, 시간이 많이 걸리지 만 그게 내가하는 일입니다. 메모리 누수 나 다른 문제는 거의 없습니다. 그러나 저는 고성능 과학, 데이터 수집 및 그래픽 소프트웨어의 제한된 영역에서 일합니다. 나는 종종 은행 및 전자 상거래, 이벤트 중심 GUI 또는 높은 네트워크 비동기 혼란과 같은 거래를 다루지 않습니다. 어쩌면 새로운 방식이 유리할 수도 있습니다!
좋은 질문입니다!
c ++를 사용하고 있고 게임과 같은 실시간 CPU 및 메모리 보드 응용 프로그램을 개발하는 경우 자체 메모리 관리자를 작성해야합니다.
나는 당신이 더 잘 할 수 있다고 생각합니다. 다양한 작가들의 흥미로운 작품들을 합치면 힌트를 줄 수 있습니다.
고정 크기 할당자는 인터넷 어디에서나 심하게 논의됩니다.
Small Object Allocation은 2001 년 Alexandrescu에 의해 그의 완벽한 저서 "Modern c ++ design"에서 소개되었습니다.
Dimitar Lazarov가 작성한 "고성능 힙 할당 자"라는 Game Programming Gem 7 (2008)의 놀라운 기사에서 소스 코드를 배포하여 크게 발전했습니다.
이 기사 에서 훌륭한 리소스 목록을 찾을 수 있습니다
멍청한 쓸모없는 할당자를 직접 작성하지 마십시오 ... 먼저 자신을 문서화하십시오.
누출되지 않는 방법에 대해서는 이미 많이 있지만 누출을 추적 할 수있는 도구가 필요한 경우 다음을 살펴보십시오.
이러한 버그의 빈번한 원인은 개체에 대한 참조 나 포인터를 허용하지만 소유권을 명확하게하지 않는 방법이있는 경우입니다. 스타일과 주석 규칙은이를 덜 가능하게합니다.
함수가 객체의 소유권을 갖는 경우가 특별한 경우가되게하십시오. 이런 상황이 발생하면이를 나타내는 헤더 파일의 함수 옆에 주석을 작성하십시오. 대부분의 경우 객체를 할당하는 모듈이나 클래스가 객체 할당을 취소해야합니다.
const를 사용하면 어떤 경우에는 많은 도움이 될 수 있습니다. 함수가 오브젝트를 수정하지 않고 리턴 후에도 지속되는 참조를 저장하지 않으면 const 참조를 승인하십시오. 호출자의 코드를 읽음으로써 함수가 객체의 소유권을 수락하지 않았 음을 알 수 있습니다. 동일한 함수가 const가 아닌 포인터를 수락하도록 할 수 있으며 호출자는 수신자가 소유권을 수락했다고 가정 할 수도, 아닐 수도 있지만 const 참조로는 의문의 여지가 없습니다.
인수 목록에 상수가 아닌 참조를 사용하지 마십시오. 호출자 코드를 읽을 때 수신자가 매개 변수에 대한 참조를 유지할 수 있는지는 확실하지 않습니다.
참조 카운트 포인터를 권장하는 의견에 동의하지 않습니다. 이것은 일반적으로 잘 작동하지만 버그가 있고 작동하지 않는 경우, 특히 소멸자가 멀티 스레드 프로그램과 같이 사소한 것을 수행하는 경우에 효과적입니다. 너무 세지 않은 경우 참조 카운트가 필요하지 않도록 설계를 확실히 조정하십시오.
중요도에 따른 팁 :
-Tip # 1 항상 소멸자를 "가상"으로 선언해야합니다.
-팁 # 2 RAII 사용
-Tip # 3 부스트의 스마트 포인터 사용
-팁 # 4 버그가있는 Smartpointer를 작성하지 말고 boost를 사용하십시오 (현재 프로젝트에서 boost를 사용할 수 없으며 자체 스마트 포인터를 디버깅해야하는데 어려움을 겪었습니다. 같은 경로를 다시 사용하지만 지금은 다시 의존성을 높일 수 없습니다.)
-Tip # 5 캐주얼 / 비 성능이 중요하다면 (수천 개의 오브젝트가있는 게임 에서처럼) Thorsten Ottosen의 부스트 포인터 컨테이너를 살펴보십시오
-Tip # 6 Visual Leak Detection의 "vld"헤더와 같이 선택한 플랫폼에 대한 누출 감지 헤더를 찾으십시오.
다른 사람들은 스마트 포인터와 같이 처음에 메모리 누수를 피하는 방법을 언급했습니다. 그러나 프로파일 링 및 메모리 분석 도구는 종종 메모리 문제가 발생하면이를 추적 할 수있는 유일한 방법입니다.
Valgrind memcheck 는 훌륭한 무료 프로그램입니다.
메모리 할당 기능을 가로 챌 수 있으며 프로그램 종료시 해제되지 않은 일부 메모리 영역이 있는지 확인할 수 있습니다 ( 모두에 적합하지는 않지만) 응용 프로그램에 ).
또한 연산자 new 및 delete 및 기타 메모리 할당 기능을 대체하여 컴파일 타임에 수행 할 수도 있습니다.
예를 들어이 사이트 에서 확인 하십시오 [C ++에서 메모리 할당 디버깅] 참고 : delete 연산자에는 다음과 같은 트릭도 있습니다.
#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE
일부 변수에 파일 이름을 저장할 수 있으며 과부하 된 삭제 연산자는 파일이 호출 된 위치를 알 수 있습니다. 이렇게하면 프로그램에서 모든 삭제 및 malloc을 추적 할 수 있습니다. 메모리 검사 순서가 끝나면 할당 된 메모리 블록이 파일 이름과 줄 번호로 식별하여 '삭제되지 않은'메모리 블록을보고 할 수 있어야합니다.
Visual Studio에서 BoundsChecker 와 같은 것을 시도해 볼 수도 있습니다. 매우 흥미롭고 사용하기 쉽습니다.
우리는 모든 할당 함수를 앞에 짧은 문자열과 끝에 센티넬 플래그를 추가하는 레이어로 래핑합니다. 예를 들어 "myalloc (pszSomeString, iSize, iAlignment); 또는 new ("description ", iSize) MyObject ();를 호출하면 지정된 크기와 헤더 및 센티넬에 충분한 공간을 내부적으로 할당합니다. 디버그가 아닌 빌드에 대해서는 이것을 언급하는 것을 잊지 마십시오!이 작업을 수행하는 데 약간의 메모리가 더 필요하지만 이점은 비용보다 훨씬 큽니다.
여기에는 세 가지 이점이 있습니다. 먼저 특정 '영역'에 할당 된 코드를 빠르게 검색하여 해당 영역이 해제되어야 할 때 정리하지 않으면 코드 누출을 쉽고 빠르게 추적 할 수 있습니다. 또한 모든 센티넬이 손상되지 않았는지 확인하여 경계를 덮어 쓴 시점을 감지하는 것이 유용 할 수 있습니다. 이로 인해 숨겨져있는 충돌이나 배열 실수를 찾으려고 할 때 많은 시간이 절약되었습니다. 세 번째 장점은 메모리 사용을 추적하여 빅 플레이어가 누구인지 확인하는 것입니다.
다른 장소에서 할당하고 파괴하는 유일한 예 중 하나는 스레드 생성 (전달하는 매개 변수)입니다. 그러나이 경우에도 쉽습니다. 스레드를 생성하는 함수 / 방법은 다음과 같습니다.
struct myparams {
int x;
std::vector<double> z;
}
std::auto_ptr<myparams> param(new myparams(x, ...));
// Release the ownership in case thread creation is successfull
if (0 == pthread_create(&th, NULL, th_func, param.get()) param.release();
...
대신 스레드 함수
extern "C" void* th_func(void* p) {
try {
std::auto_ptr<myparams> param((myparams*)p);
...
} catch(...) {
}
return 0;
}
꽤 쉽지 않습니까? 스레드 생성이 실패하면 auto_ptr에 의해 리소스가 해제 (삭제)됩니다. 그렇지 않으면 소유권이 스레드로 전달됩니다. 스레드가 너무 빠르면 생성 후 리소스를 해제하기 전에 어떻게해야합니까?
param.release();
주요 기능 / 메소드에서 호출됩니까? 아무것도! 우리는 할당 해제를 무시하기 위해 auto_ptr을 '알아 내기'때문입니다. C ++ 메모리 관리가 쉽지 않습니까? 건배,
에마!
다른 리소스 (핸들, 파일, DB 연결, 소켓 등)를 관리하는 것과 같은 방식으로 메모리를 관리합니다. GC는 그들에게 도움이되지 않습니다.
모든 함수에서 정확히 하나의 리턴. 그렇게하면 할당 해제를 할 수 있고 절대로 놓치지 않을 수 있습니다.
그렇지 않으면 실수하기가 너무 쉽습니다.
new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.