집 >백엔드 개발 >C#.Net 튜토리얼 >C++ 메모리 관리에 대한 자세한 설명
1. 해당 new와 delete는 같은 형식이어야 합니다. 다음 문장에서 잘못된 점은 무엇인가요?
string *stringarray = new string[100];
...
delete stringarray;
모든 것이 정상인 것 같습니다. 새 문자열은 삭제에 해당하지만 큰 버그가 숨겨져 있습니다. : 프로그램의 동작을 추측할 수 없습니다. stringarray가 가리키는 100개의 문자열 개체 중 최소 99개는 해당 소멸자가 호출되지 않기 때문에 제대로 삭제되지 않습니다.
new를 사용하면 두 가지 일이 발생합니다. 먼저 메모리가 할당되고(Operator new 함수를 통해, 자세한 내용은 항목 7-10 및 항목 m8 참조) 할당된 메모리에 대해 하나 이상의 생성자가 호출됩니다. delete를 사용하면 두 가지 일도 발생합니다. 먼저 메모리를 해제하기 위해 하나 이상의 소멸자가 호출된 다음 메모리가 해제됩니다(연산자 delete 함수를 통해 자세한 내용은 항목 8 및 m8 참조). 삭제에는 다음과 같은 중요한 질문이 있습니다. 메모리에서 얼마나 많은 객체를 삭제해야 합니까? 대답에 따라 호출될 소멸자 수가 결정됩니다.
이 질문은 간단합니다. 삭제할 포인터가 단일 객체를 가리키는가요, 아니면 객체 배열을 가리키는가요? 오직 당신만이 삭제를 알 수 있습니다. 괄호 없이 delete를 사용하면 delete는 단일 객체를 가리키는 것으로 간주합니다. 그렇지 않으면 배열을 가리키는 것으로 간주합니다.
string *stringptr1 = new string;
string * stringptr2 = new string [100];
...
delete stringptr1;//객체 삭제
delete [] stringptr2;//객체 배열 삭제
당신이 무엇을 할 것인가? stringptr1 앞에 "[]"를 추가하면 어떻게 되나요? 대답은 다음과 같습니다. stringptr2 앞에 "[]"를 추가하지 않으면 어떻게 될까요? 대답은 또한 추측할 수 없다는 것입니다. 그리고 int와 같은 고정 유형의 경우 해당 유형에 소멸자가 없더라도 결과를 예측할 수 없습니다. 따라서 이러한 유형의 문제를 해결하는 규칙은 간단합니다. new를 호출할 때 []를 사용하면 delete를 호출할 때도 []를 사용해야 합니다. new를 호출할 때 []를 사용하지 않으면 delete를 호출할 때 []를 사용하지 마세요.
포인터 데이터 멤버를 포함하고 여러 생성자를 제공하는 클래스를 작성할 때 이 규칙을 염두에 두는 것이 특히 중요합니다. 이 경우 포인터 멤버를 초기화하는 모든 생성자에서 동일한 새 형식을 사용해야 하기 때문입니다. 그렇지 않으면 소멸자에서 어떤 형태의 삭제가 사용됩니까? 이 주제에 대한 자세한 내용은 항목 11을 참조하세요.
이 규칙은 typedef를 사용하는 사람들에게도 매우 중요합니다. 왜냐하면 typedef를 작성하는 프로그래머는 typedef delete로 정의된 유형의 개체를 생성하기 위해 new를 사용한 후에 어떤 형식의 삭제를 사용해야 하는지 알려야 하기 때문입니다. . 예:
typedef string addresslines[4]; //사람의 주소, 총 4줄, 각 줄에 하나의 문자열
//주소줄은 배열이므로 new:
string을 사용하세요. *pal = new addresslines; // "new addresslines"는 string*을 반환하며 이는
// "new string[4]"이 반환하는 것과 동일합니다.
Delete는 해당하는 배열 형식이어야 합니다. to it:
delete pal; // 오류!
delete [] pal; // 정확함
혼란을 피하기 위해 배열 유형에 typedef를 사용하지 않는 것이 가장 좋습니다. 표준 C++ 라이브러리(항목 49 참조)에는 문자열 및 벡터 템플릿이 포함되어 있고 이를 사용하면 배열의 필요성이 거의 0으로 줄어들기 때문에 이는 실제로 쉽습니다. 예를 들어, 주소 라인은 문자열의 벡터로 정의될 수 있습니다. 즉, 주소 라인은 벡터 유형으로 정의될 수 있습니다.
2. 소멸자의 포인터 멤버에 대해 delete를 호출합니다
대부분의 경우 동적 메모리 할당을 수행하는 클래스는 생성자에서 메모리를 할당하기 위해 new를 사용한 다음 소멸자에서 delete를 사용하여 메모리를 해제합니다. 이 클래스를 처음 작성할 때 수행하는 것은 확실히 어렵지 않으며 모든 생성자에 메모리가 할당된 모든 멤버에 대해 마지막에 delete를 사용하는 것을 기억할 것입니다.
그러나 이 클래스가 유지되고 업그레이드된 후에는 상황이 어려워질 것입니다. 클래스의 코드를 수정한 프로그래머가 반드시 이 클래스를 처음 작성한 사람은 아니기 때문입니다. 포인터 멤버를 추가한다는 것은 거의 다음 작업을 의미합니다.
·각 생성자에서 포인터를 초기화합니다. 일부 생성자의 경우 포인터에 메모리가 할당되지 않으면 포인터가 0(즉, 널 포인터)으로 초기화됩니다.
·기존 메모리를 삭제하고 할당 연산자를 통해 포인터에 새 메모리를 할당합니다.
·소멸자에서 포인터를 삭제합니다.
생성자에서 포인터를 초기화하는 것을 잊어버리거나 할당 작업 중에 포인터를 처리하는 것을 잊어버린 경우 문제가 빠르고 명확하게 나타나므로 실제로는 이 두 가지 문제가 발생하지 않습니다. 그러나 소멸자에서 포인터가 삭제되지 않으면 뚜렷한 외부 증상이 나타나지 않습니다. 대신 주소 공간을 차지하고 프로그램이 종료될 때까지 계속 증가하는 작은 메모리 누수로 나타날 수 있습니다. 이 상황은 눈에 띄지 않는 경우가 많기 때문에 클래스에 포인터 멤버를 추가할 때마다 이를 명확하게 기억해야 합니다.
또한 널 포인터를 삭제하는 것은 안전합니다(아무 작업도 수행하지 않으므로). 따라서 생성자, 할당 연산자 또는 기타 멤버 함수를 작성할 때 클래스의 각 포인터 멤버는 유효한 메모리를 가리키거나 null을 가리킵니다. 그런 다음 소멸자에서 새 항목인지 여부에 대해 걱정하지 않고 삭제할 수 있습니다.
물론 이러한 용어의 사용이 절대적이어서는 안 됩니다. 예를 들어, new로 초기화되지 않은 포인터를 삭제하기 위해 delete를 사용하지 않을 것이며 스마트 포인터 개체를 삭제할 필요 없이 사용하는 것과 마찬가지로 전달된 포인터도 삭제하지 않을 것입니다. 즉, 클래스 멤버가 원래 new를 사용하지 않는 한 소멸자에서 delete를 사용할 필요가 없습니다.
스마트 포인터에 관해 말하자면, 포인터 멤버를 삭제하지 않아도 되는 방법이 있습니다. 즉, 이러한 멤버를 C++ 표준 라이브러리의 auto_ptr과 같은 스마트 포인터 개체로 바꾸는 것입니다. 그것이 어떻게 작동하는지 알려면 m9절과 m10절을 살펴보십시오.
3. 메모리 부족을 미리 대비하세요
new 연산자는 메모리 할당 요청을 완료할 수 없을 때 예외를 발생시킵니다. (이전 방법은 0을 반환하는 것이었지만 일부 이전 컴파일러에서는 여전히 이 작업을 수행합니다. 원한다면 이 주제에 대한 논의를 이 기사 끝까지 연기하겠습니다. 기억력 부족으로 인한 예외 처리는 도덕적인 행위로 볼 수 있다는 것은 누구나 알지만 실제로는 목에 칼을 대는 것만큼이나 고통스러운 일이다. 그래서 때로는 그것을 내버려두기도 하고, 어쩌면 전혀 신경 쓰지 않을 수도 있습니다. 하지만 마음속에는 여전히 깊은 죄책감이 숨겨져 있을 것입니다. 새것이 정말 잘못되면 어쩌지?
이러한 상황을 해결하는 방법은 자연스럽게 예전 방식으로 돌아가서 전처리를 활용하는 방법이 생각나실 겁니다. 예를 들어, C의 일반적인 관행은 유형 독립적인 매크로를 정의하여 메모리를 할당하고 할당이 성공했는지 확인하는 것입니다. C++의 경우 이 매크로는 다음과 같습니다.
#define new(ptr, type)
try { (ptr) = new type }
catch (std: :bad_alloc&) { 주장(0); }
("느려요! std::bad_alloc은 무엇을 하나요?"라고 물을 것입니다. bad_alloc은 연산자 new가 메모리 할당 요청을 충족할 수 없을 때 발생하는 예외 유형입니다. std는 이름입니다. bad_alloc이 위치한 네임스페이스(항목 28 참조) "알겠습니다!" 표준 C 헤더 파일(또는 이에 상응하는 공간 버전)을 보면 "assert의 용도는 무엇입니까?"라고 물을 수 있습니다. ), Assert가 매크로라는 것을 알 수 있습니다. 이 매크로는 전달된 표현식이 0이 아닌지 확인하고, 그렇지 않은 경우 오류 메시지를 발행하고 정의되지 않은 경우에만 중단을 호출합니다. 표준 매크로 ndebug를 사용할 때, 즉 디버깅 상태일 때, 제품 출시 상태에서, 즉 ndebug가 정의되었을 때, Assert는 아무것도 하지 않으며, 이는 빈 문장과 동일하므로, Assertion을 확인하는 동안에만 확인할 수 있습니다. (어설션)).
새 매크로는 위에서 언급한 일반적인 문제, 즉 게시된 프로그램에서 발생할 수 있는 상태를 확인하기 위해 Assert를 사용하는 것(단, 언제든지 메모리 부족이 발생할 수 있음)을 동시에 가지고 있습니다. , 이는 c +에서도 사용됩니다. +에는 또 다른 결함이 있습니다. 즉, new를 사용할 수 있는 다양한 방법을 고려하지 않습니다. 예를 들어 t 유형의 객체를 생성하려는 경우 일반적으로 세 가지 일반적인 구문 형식이 있습니다. 각 형식에서 발생할 수 있는 예외를 처리해야 합니다.
new t;
new t(constrUCtor 인수);
new t[size];
일부 사람들은 연산자 new를 사용자 정의(오버로드)하여 프로그램에 새 형식을 사용하는 구문이 포함되기 때문에 여기서 문제는 크게 단순화됩니다. .
그럼 어떻게 해야 할까요? 매우 간단한 오류 처리 방법을 사용하려는 경우 다음과 같이 할 수 있습니다. 메모리 할당 요청을 만족할 수 없는 경우 미리 지정한 오류 처리 함수를 호출합니다. 이 방법은 규칙을 기반으로 합니다. 즉, new 연산자가 요청을 충족할 수 없는 경우 예외를 발생시키기 전에 고객이 지정한 오류 처리 함수(일반적으로 new-handler 함수라고 함)를 호출합니다. (new 연산자의 실제 작업은 더 복잡합니다. 자세한 내용은 8절을 참조하세요.)
오류 처리 함수를 지정할 때 set_new_handler 함수가 사용됩니다. 헤더 파일에는 대략 다음과 같이 정의되어 있습니다.
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
보시다시피 new_handler는 다음을 가리키는 사용자 정의 함수 포인터 유형입니다. a 입력 매개변수를 취하고 값을 반환하지 않는 함수입니다. set_new_handler는 new_handler 타입을 입력받아 반환하는 함수입니다.
set_new_handler의 입력 매개변수는 new 연산자가 메모리 할당에 실패할 때 호출되는 오류 처리 함수의 포인터이고, 반환 값은 set_new_handler 이전에 이미 적용되었던 이전 오류 처리 함수의 포인터입니다. 불렀다.
다음과 같이 set_new_handler를 사용할 수 있습니다.
// 연산자 new가 충분한 메모리를 할당할 수 없는 경우 호출하는 함수
void nomorememory()
{
cerr << "메모리 요청을 충족할 수 없습니다
";
abort();
}
int main()
{
set_new_handler(nomorememory);
int *pbigdataarray = 새로운 int[100000000];
...
}
new 연산자가 100,000,000개의 정수에 대한 공간을 할당할 수 없으면 nomorememory가 호출되고 프로그램은 오류 메시지와 함께 종료됩니다. 이는 단순히 시스템 커널이 오류 메시지를 생성하고 프로그램을 종료하도록 하는 것보다 낫습니다. (그런데, 오류 메시지를 작성하는 동안 cerr이 동적으로 메모리를 할당해야 한다면 어떻게 될지 생각해 보세요...)
new 연산자가 메모리 할당 요청을 만족할 수 없을 때 new-handler 함수는 다음과 같습니다. 두 번 이상 호출되는 대신 충분한 메모리가 발견될 때까지 반복됩니다. 반복 호출을 구현하는 코드는 항목 8에서 볼 수 있습니다. 여기서는 설명 언어를 사용하여 설명합니다. 잘 설계된 새 처리기 함수는 다음 함수 중 하나를 구현해야 합니다.
·사용 가능한 메모리를 더 많이 생성합니다. 이렇게 하면 new 연산자의 다음 번 메모리 할당 시도가 성공할 가능성이 높아집니다. 이 전략을 구현하는 한 가지 방법은 프로그램이 시작될 때 큰 메모리 블록을 할당한 다음 new-handler가 처음 호출될 때 이를 해제하는 것입니다. 릴리스에는 사용자에게 경고 메시지가 표시됩니다. 예를 들어 메모리 양이 너무 적으면 여유 공간이 더 없으면 다음 요청이 실패할 수 있습니다.
·다른 new-handler 기능을 설치합니다. 현재 new-handler 함수가 더 많은 사용 가능한 메모리를 생성할 수 없다면 아마도 다른 new-handler 함수가 더 많은 리소스를 제공할 수 있다는 것을 알게 될 것입니다. 이 경우 현재 new-handler는 이를 대체하기 위해 다른 new-handler를 설치할 수 있습니다(set_new_handler를 호출하여). 다음번에 Operator new가 new-handler를 호출할 때 가장 최근에 설치된 것이 사용됩니다. (이 전략의 또 다른 대안은 new-handler가 자신의 실행 동작을 변경하여 다음에 호출될 때 다른 작업을 수행하도록 허용하는 것입니다. 이는 new-handler가 자신에게 영향을 미치는 정적 변수를 수정할 수 있도록 허용하여 수행됩니다. 또는 전역 데이터. )
·new-handler를 제거합니다. 즉, set_new_handler에 널 포인터를 전달하십시오. new-handler가 설치되지 않은 경우 new 연산자는 메모리 할당에 실패할 때 표준 std::bad_alloc 유형 예외를 발생시킵니다.
·std::bad_alloc 또는 std::bad_alloc에서 계속되는 다른 유형의 예외를 발생시킵니다. 이러한 예외는 new 연산자에 의해 포착되지 않으므로 원래 메모리 요청이 이루어진 위치로 전송됩니다. (다른 유형의 예외를 발생시키는 것은 연산자의 새로운 예외 사양을 위반하게 됩니다. 사양의 기본 동작은 중단을 호출하는 것이므로 new-handler가 예외를 발생시키려는 경우 std::bad_alloc에서 계속되는지 확인해야 합니다. 예외 사양에 대한 자세한 내용은 항목 m14를 참조하세요. )
·반품 불가. 일반적인 접근 방식은 중단 또는 종료를 호출하는 것입니다. 중단/종료는 표준 C 라이브러리(및 표준 C++ 라이브러리, 항목 49 참조)에서 찾을 수 있습니다.
위 옵션을 사용하면 new-handler 기능을 구현할 때 뛰어난 유연성을 얻을 수 있습니다.
메모리 할당 실패 시 수행할 작업은 할당되는 개체의 클래스에 따라 다릅니다.
class x {
public:
static void
outofmemory();
...
};
class y {
public:
static void outofmemory();
...
};
x* p1 = new x; // 할당이 성공하면 x::outofmemory를 호출합니다.
y* p2 = new y; / 할당에 실패하면 y::outofmemory를 호출하세요
C++에서는 클래스에 대해 특별히 new-handler 함수를 지원하지 않으며 필요하지도 않습니다. 각 클래스에 자신만의 set_new_handler 버전과 new 연산자를 제공하여 직접 구현할 수 있습니다. 클래스의 set_new_handler는 클래스에 대한 new-handler를 지정할 수 있습니다(표준 set_new_handler가 전역 new-handler를 지정하는 것과 마찬가지로). 클래스의 new 연산자는 클래스의 객체에 메모리를 할당할 때 전역 new-handler 대신 클래스의 new-handler가 사용되도록 보장합니다.
클래스 x의 메모리 할당 실패가 처리되었다고 가정합니다. new 연산자는 x 유형의 객체에 대한 메모리 할당에 실패하므로 오류 처리 함수를 매번 호출해야 하므로 클래스에서 new_handler 유형의 정적 멤버를 선언해야 합니다. 그러면 클래스 x는 다음과 같습니다:
class x {
public:
static new_handler set_new_handler(new_handler p);
static void * Operator new(size_t size); 🎜>
PRivate:
static new_handler currenthandler;
};
클래스의 정적 멤버는 클래스 외부에서 정의되어야 합니다. 정적 객체의 기본 초기화 값인 0을 빌려오고 싶어서 x::currenthandler를 정의할 때 초기화하지 않았습니다.
new_handler x::currenthandler; //기본 currenthandler는 0(예: null)으로 설정됩니다.
클래스 x의 set_new_handler 함수는 전달된 모든 포인터를 저장하고 호출 시 반환합니다. 이전에 저장된 모든 포인터. 이것이 바로 set_new_handler의 표준 버전이 수행하는 작업입니다.
new_handler x::set_new_handler(new_handler p)
{
new_handler oldhandler = currenthandler;
currenthandler = p;
return oldhandler;
}
마지막으로 x의 new 연산자가 수행하는 작업을 살펴보세요.
1. 표준 set_new_handler 함수를 호출하고 입력 매개변수는 x의 오류 처리 함수입니다. 이는 x의 new-handler 함수를 전역 new-handler 함수로 만듭니다. 다음 코드에서 "::" 기호는 std 공간을 명시적으로 참조하는 데 사용됩니다(표준 set_new_handler 함수는 std 공간에 존재합니다).
2. 전역 연산자 new를 호출하여 메모리를 할당합니다. 첫 번째 할당이 실패하면 전역 연산자 new는 x의 새 핸들러를 호출합니다. x가 전역 새 핸들러로 방금 설치되었기 때문입니다(1 참조). 전역 연산자 new가 마침내 메모리 할당에 실패하면 std::bad_alloc 예외가 발생하고 x의 연산자 new가 이를 포착합니다. x의 new 연산자는 원래 대체된 전역 new-handler 함수를 복원하고 마지막으로 예외를 발생시켜 반환합니다.
3. 전역 연산자 new가 x 유형의 객체에 메모리를 성공적으로 할당했다고 가정하면 x의 연산자 new는 표준 set_new_handler를 다시 호출하여 원래 전역 오류 처리 함수를 복원합니다. 마지막으로 성공적으로 할당된 메모리에 대한 포인터가 반환됩니다.
C++에서는 다음을 수행합니다.
void * x::operator new(size_t size)
{
new_handler globalhandler = // x의 new_handler 설치
std: :set_new_handler (currenthandler);
void *memory;
try { // 메모리 할당 시도
memory = ::operator new(size);
}
catch (std::bad_alloc&) { // 이전 new_handler 복원
std::set_new_handler(globalhandler);
throw; // 예외 발생
}
std::set_new_handler(globalhandler ); // 이전 new_handler 복원
return memory;
}
위의 std::set_new_handler에 대한 반복 호출이 마음에 들지 않으면 항목 m9를 참조하여 제거할 수 있습니다.
클래스
x::set_new_handler(nomorememory);
// nomorememory를 x의
에 설정// new-handling 함수
x *px1의 메모리 할당 처리 함수를 사용할 경우 = new x;
// 메모리 할당에 실패하면
// nomorememory 호출
string *ps = new string;
// 메모리 할당에 실패하면 전역 new 처리 함수 호출
x::set_new_handler(0);
//처리 함수의 새로운 처리 함수를 가정)
위와 비슷한 상황을 처리할 때 클래스가 고려되지 않으면 알 수 있습니다. , 구현 코드는 동일하므로 다른 곳에서 재사용하는 것을 생각하는 것이 당연합니다. 항목 41에서 설명한 것처럼 연속과 템플릿을 사용하여 재사용 가능한 코드를 디자인할 수 있습니다. 여기서는 귀하의 요구 사항을 충족하기 위해 두 가지 방법을 결합합니다.
하위 클래스가 특정 기능을 계속할 수 있도록 허용하는 "믹스인 스타일" 기본 클래스만 생성하면 됩니다. 여기서는 클래스 함수의 새 핸들러를 생성하는 것을 의미합니다. 기본 클래스를 설계한 이유는 모든 하위 클래스가 set_new_handler 및 연산자 new 함수를 계속할 수 있도록 하기 위함이며, 템플릿은 각 하위 클래스가 서로 다른 currenthandler 데이터 멤버를 갖도록 설계되었습니다. 복잡해 보이지만 코드가 실제로는 매우 친숙하다는 것을 알 수 있습니다. 유일한 차이점은 이제 모든 클래스에서 재사용할 수 있다는 것입니다.
template // set_new_handler 클래스에 대한 지원 제공
class newhandlersupport { // "혼합 스타일"을 위한 기본 클래스
public:
static new_handler set_new_handler(new_handler p);
static void * 연산자 new(size_t size);
private:
static new_handler currenthandler;
};
template
new_handler newhandlersupport::set_new_handler( new_handler p)
{
new_handler oldhandler = currenthandler;
currenthandler = p;
return oldhandler;
}
template
void * newhandlersupport::operator new( size_t size)
{
new_handler globalhandler =
std::set_new_handler(currenthandler);
void *memory;
try {
memory = ::operator new(size);
}
catch(std::bad_alloc&) {
std::set_new_handler(globalhandler);
throw;
}
std::set_new_handler(globalhandler);
return memory;
}
// 이는 각 현재 핸들러를 0으로 설정합니다
template
new_handler newhandlersupport::currenthandler;
이 템플릿 클래스를 사용하여 set_new_handler 함수를 클래스 x에 추가합니다. 쉽습니다: 여기서는 선호도를 지정합니다.)
class x: public newhandlersupport {
... // 이전과 같지만
에 대한 선언은 없습니다.} // set_new_handler 또는 연산자 new
x를 사용할 때 이전 코드가 뒤에서 무엇을 하는지 걱정할 필요가 없습니다. 정말 좋아요! 당신이 자주 무시하는 것들이 가장 신뢰할 만한 것일 때가 많습니다.
set_new_handler를 사용하는 것은 부족한 메모리를 처리하는 편리하고 간단한 방법입니다. 이는 try 모듈에 각각의 새로운 항목을 래핑하는 것보다 확실히 훨씬 낫습니다. 게다가 newhandlersupport와 같은 템플릿을 사용하면 모든 클래스에 특정 new-handler를 더 쉽게 추가할 수 있습니다. "혼합 스타일" 연속은 필연적으로 주제를 여러 연속으로 이끌어냅니다. 이 주제로 이동하기 전에 항목 43을 읽어야 합니다.
1993년 이전에는 C++에서는 메모리 할당이 실패할 때 new 연산자가 항상 0을 반환하도록 요구했습니다. 이제 std::bad_alloc 예외를 발생시키려면 new 연산자가 필요합니다. 컴파일러가 새 사양을 지원하기 전에 많은 C++ 프로그램이 작성되었습니다. C++ 표준 위원회는 return-0 사양을 따르는 기존 코드를 포기하고 싶지 않았기 때문에 return-0 기능을 계속 제공하는 new 연산자(및 new[] 연산자 - 항목 8 참조)의 대체 형식을 제공했습니다. 이러한 형식은 throw를 사용하지 않지만 new를 사용하여 진입점에서 nothrow 객체를 사용하기 때문에 "throwless"라고 합니다.
class widget { ... };
widget *pw1 = new widget; // 할당 실패로 인해 std::bad_alloc if
if (pw1 == 0) ... // 이 검사는 실패해야 합니다
widget * pw2 = new (nothrow) widget; // 할당이 실패하면 0을 반환합니다.
if (pw2 == 0) ... // 이 검사는 "normal" 사용 여부에 관계없이
성공할 수 있습니다. 예외 발생) 형태의 new 또는 "throwing 없음" 형태의 new에서 중요한 것은 메모리 할당 실패에 대비해야 한다는 것입니다. 가장 쉬운 방법은 두 가지 형식 모두에서 작동하는 set_new_handler를 사용하는 것입니다.
4. 연산자 new 및 연산자 delete를 작성할 때 규칙을 따르십시오.
연산자 new를 직접 다시 작성할 때(항목 10에서는 때때로 다시 작성해야 하는 이유를 설명함) 다음을 제공하는 것이 중요합니다. 동작은 시스템 기본 연산자 new와 일치해야 합니다. 실제 구현은 다음과 같습니다. 올바른 반환 값을 갖고, 사용 가능한 메모리가 충분하지 않을 때 오류 처리 함수를 호출합니다(항목 7 참조). 또한 실수로 new의 표준형을 숨기지 않도록 하세요. 이것이 항목 9의 주제입니다.
반환값에 관한 부분은 간단합니다. 메모리 할당 요청이 성공하면 메모리에 대한 포인터가 반환되고, 실패하면 항목 7에 지정된 대로 std::bad_alloc 유형의 예외가 발생합니다.
하지만 상황은 그렇게 간단하지 않습니다. new 연산자는 실제로 메모리 할당을 두 번 이상 시도하기 때문에 각 실패 후에 오류 처리 함수를 호출해야 하며 오류 처리 함수가 다른 곳에서 메모리를 해제할 방법을 찾을 것으로 기대합니다. new 연산자는 오류 처리 함수에 대한 포인터가 null인 경우에만 예외를 발생시킵니다.
또한 C++ 표준에서는 new 연산자가 0바이트의 메모리 할당을 요청하는 경우에도 유효한 포인터를 반환해야 한다고 요구합니다. (사실 이상하게 들리는 이 요구 사항은 C++ 언어의 다른 부분을 단순하게 만듭니다.)
이런 식으로 클래스 멤버가 아닌 형태의 new 연산자의 의사 코드는 다음과 같습니다.
void * 연산자 new(size_t size) // 연산자 new에는 다른 매개변수도 있을 수 있습니다.
{
if (size == 0) { // 0바이트 요청을 처리할 때
size = 1 ; // 1바이트 요청으로 처리
}
while (1) {
size 바이트의 메모리 할당;
if(할당 성공)
return (메모리에 대한 포인터);
// 할당에 실패하면 현재 오류 처리 함수를 찾습니다.
new_handler globalhandler = set_new_handler(0);
set_new_handler(globalhandler);
if (globalhandler) (*globalhandler)();
else throw std::bad_alloc();
}
}
0바이트 요청을 처리하는 요령은 다음과 같습니다. 요청으로 처리하려면 바이트가 처리됩니다. 이상하게 보일 수도 있지만 간단하고 합법적이며 효과적입니다. 그리고 0바이트 요청이 얼마나 자주 발생합니까?
위 의사 코드에서 왜 오류 처리 기능이 0으로 설정되었다가 즉시 복원되는지 궁금하실 것입니다. 오류 처리 함수에 대한 포인터를 직접 얻을 수 있는 방법이 없기 때문에 set_new_handler를 호출하여 찾아야 한다. 이 방법은 어리석지만 효과적이다.
항목 7에서는 new 연산자가 내부적으로 무한 루프를 포함하고 있음을 언급합니다. 위 코드는 이를 명확하게 보여줍니다. 반면 (1)은 무한 루프를 발생시킵니다. 루프를 벗어나는 유일한 방법은 메모리 할당이 성공하거나 오류 처리기가 항목 7에 설명된 이벤트 중 하나를 완료하는 것입니다. 더 많은 사용 가능한 메모리를 얻거나 새로운 -handler(오류 처리기)가 설치됩니다. 오류 처리기가 제거되었습니다. new-handler; std::bad_alloc 또는 해당 파생 유형의 예외가 발생했거나 실패를 반환했습니다. 이제 우리는 new-handler가 이러한 작업 중 하나를 수행해야 하는 이유를 이해합니다. 이렇게 하지 않으면 new 연산자의 루프가 끝나지 않습니다.
많은 사람들이 깨닫지 못하는 한 가지는 new 연산자가 서브클래스에 의해 상속되는 경우가 많다는 것입니다. 이로 인해 몇 가지 합병증이 발생합니다. 위의 의사코드에서 함수는 size 바이트의 메모리를 할당합니다(size가 0이 아닌 경우). 크기는 함수에 전달되는 매개변수이기 때문에 중요합니다. 그러나 클래스용으로 작성된 대부분의 연산자 new(항목 10의 항목 포함)는 모든 클래스나 모든 하위 클래스가 아닌 특정 클래스용으로만 설계되었습니다. 이는 새로운 클래스의 연산자에 대해 그러나 연속이 존재하기 때문에 기본 클래스의 new 연산자가 하위 클래스 객체에 대한 메모리를 할당하기 위해 호출될 수 있습니다.
class base {
public:
static void * Operator new(size_t size);
...
};
클래스 파생: 공개 베이스 // 파생 클래스는 연산자 new
{ ... }를 선언하지 않습니다. //
파생 *p = new 파생; // base::operator new
호출기본 클래스의 new 연산자가 이 상황을 구체적으로 처리하는 데 어려움을 겪고 싶지 않은 경우(발생 가능성이 낮음) 가장 쉬운 방법은 이 "잘못된" 메모리 할당 요청 수를 표준 연산자 new로 전송하는 것입니다. , 다음과 같습니다:
void * base::operator new(size_t size)
{
if (size != sizeof(base)) // 수량이 "틀리면" 표준 연산자 new를 사용합니다.
return ::operator new(size); // 이 요청을 처리합니다
//
... // 그렇지 않으면 이 요청을 처리합니다
}
"중지! "불합리한데 가능한 상황을 확인하는 걸 깜빡했군요. 크기가 0일 수도 있습니다!" 네 확인은 안 했지만 다음에는 큰 소리로 외쳐주세요. 너무 격식을 차리지 마세요. 시간. :) 그러나 실제로 검사는 여전히 수행되지만 size != sizeof(base) 문에 통합되어 있습니다. C++ 표준은 이상합니다. 그 중 하나는 모든 독립형 클래스의 크기가 0이 아니라는 것입니다. 따라서 sizeof(base)는 0이 될 수 없습니다(기본 클래스에 멤버가 없더라도). size가 0이면 요청은 합리적인 방식으로 요청을 처리하는 ::operator new로 이동합니다. (흥미롭게도 base가 독립 클래스가 아닌 경우 sizeof(base)는 0이 될 수 있습니다. 자세한 내용은 "객체 계산에 대한 내 기사"를 참조하세요.
클래스 기반 배열의 메모리 할당을 제어하려면 new 연산자의 배열 형식인 new[] 연산자를 구현해야 합니다. "operator new[" ]"를 생각해보세요)발음 방법). new[] 연산자를 작성할 때 "원시" 메모리를 다루고 배열에 아직 존재하지 않는 개체에 대해서는 어떤 작업도 수행할 수 없다는 점을 기억하세요. 실제로 각 개체의 크기를 모르기 때문에 배열에 개체가 몇 개 있는지조차 알 수 없습니다. 기본 클래스의 new[] 연산자는 연속을 통해 하위 클래스 객체의 배열에 메모리를 할당하는 데 사용되며 하위 클래스 객체는 종종 기본 클래스보다 큽니다. 따라서 base::operator new[]에 있는 각 개체의 크기가 sizeof(base)라는 것을 당연하게 생각할 수 없습니다. 즉, 배열의 개체 수가 반드시 (요청된 바이트 수)/sizeof일 필요는 없습니다. (베이스). new[] 연산자에 대한 자세한 소개는 m8 절을 참조하세요.
이것이 new 연산자(및 new[] 연산자)를 재정의할 때 따라야 하는 모든 규칙입니다. 연산자 삭제(및 해당 메이트 연산자 delete[])의 경우 상황이 더 간단합니다. 기억해야 할 것은 C++에서는 널 포인터를 삭제해도 항상 안전하다는 것을 보장하므로 이 보장을 최대한 활용해야 한다는 것입니다. 다음은 클래스가 아닌 멤버 형태의 연산자 삭제 의사코드입니다.
void Operator delete(void *rawmemory)
{
if (rawmemory == 0) return; 포인터가 null이면 Return
//
rawmemory가 가리키는 메모리를 해제합니다.
return;
}
이 클래스 멤버 버전 함수도 간단하지만 삭제된 개체의 크기도 확인해야 합니다. 클래스의 new 연산자가 "잘못된" 크기의 할당 요청을::operator new로 전달한다고 가정하면 "잘못된" 크기의 삭제 요청도 ::operator delete:
로 전달되어야 합니다. class base { // 그리고
public이 여기서 선언된다는 점을 제외하면 이전과 같습니다: // 연산자 delete
static void * 연산자 new(size_t size);
static void 연산자 delete(void *rawmemory, size_t size);
.. .
};
void base::operator delete(void *rawmemory, size_t size)
{
if (rawmemory == 0) return; // 널 포인터 확인
if (size != sizeof(base)) { // 크기가 "잘못"인 경우
::operator delete(rawmemory) // 표준 연산자가 처리하도록 합니다. 요청
return;
}
rawmemory를 가리키는 메모리를 해제합니다.
return;
}
에 대한 규정을 볼 수 있습니다. 연산자 new 및 연산자 삭제(및 해당 배열 형식)는 그다지 번거롭지 않으므로 이를 준수하는 것이 중요합니다. 메모리 할당자가 new-handler 함수를 지원하고 제로 메모리 요청을 올바르게 처리하는 한, 메모리 할당 해제자가 널 포인터를 처리하면 더 이상 할 일이 없습니다. 함수의 클래스 멤버 버전에 연속 지원을 추가하는 작업은 곧 완료될 예정입니다.
5. new
내부 범위에 선언된 이름은 외부 범위에서 동일한 이름을 숨기므로 클래스 내부에 선언된 동일한 이름을 가진 두 함수에 대해
f의 경우 클래스의 멤버 함수는 전역 함수를 숨깁니다.
void f(); // 전역 함수
class x {
public:
void f() // 멤버 함수
};
x x;
f(); // f 호출
x.f(); // x 호출::f
전역 함수와 멤버 함수 호출은 항상 다른
구문을 사용하기 때문에 놀랍거나 혼란스럽지 않습니다. 그러나 여러 매개변수가 있는 연산자 new 함수를 클래스에 추가하면 결과는
이 되어 놀랄 수 있습니다.
class x {
public:
void f();
// new 연산자의 매개변수는
// new-hander(new의 오류 처리)를 지정합니다. 함수
static void * 연산자 new(size_t size, new_handler p);
};
void Specialerrorhandler(); // 다른 곳에 정의됨
x *px1 =
new (specialerrorhandler) x; // x::operator 호출 new
x *px2 = new x; // 오류!
클래스에 "operator new"라는 함수를 정의한 후 표준 new에 대한 액세스가 실수로 차단됩니다. 항목 50에서는 이것이 왜 그런지 설명하지만 여기서 더 관심을 갖는 것은 이 문제를 피할 수 있는 방법을 찾는 것입니다.
한 가지 방법은 표준 new 호출 방법을 지원하는 클래스에 new 연산자를 작성하는 것입니다. 이는 표준 new와 동일한
작업을 수행합니다. 이는 효율적인 인라인 함수를 사용하여 구현할 수 있습니다.
class x {
public:
void f();
static void * 연산자 new(size_t size, new_handler p);
static void * 연산자 new(size_t size)
{ return ::operator new(size);
x *px1 =
new (specialerrorhandler) x; 연산자
// new(size_t, new_handler)
x* px2 = new x; // x::operator 호출
// new(size_t)
다른 방법 new 연산자에 추가된 각 매개변수에 대해 기본값을 제공하는 것입니다(항목 24 참조):
class x {
public:
void f();
static
void * 연산자 new(size_t size, // p의 기본값은 0
new_handler p = 0) //
};
x *px1 = new(specialerrorhandler) x ; // 정확함
x* px2 = new x; // 또한 정확함
어떤 방법을 사용하든지 new의 "표준" 형식에 대해 새 함수를 사용자 정의하려는 경우 앞으로는 이 함수를 다시 작성하기만 하면 됩니다.
호출자는 다시 컴파일하고 링크한 후 새로운 기능을 사용할 수 있습니다.
6. 연산자 new를 작성하는 경우 연산자 삭제도 작성해야 합니다
다시 돌아가서 이 기본 질문을 살펴보겠습니다. 왜 자체 연산자 new 및 연산자 삭제를 작성해야 합니까? ?
일반적으로 대답은 효율성입니다. 기본 연산자 new 및 연산자 delete는 매우 다양하며 유연성을 통해 특정 상황에서 성능을 더욱 향상시킬 수도 있습니다. 이는 많은 수의 작은 개체를 동적으로 할당해야 하는 응용 프로그램에서 특히 그렇습니다.
예를 들어 비행기를 나타내는 클래스가 있습니다. 비행기 클래스에는 비행기 객체의 실제 설명을 가리키는 포인터만 포함되어 있습니다(이 기술은 항목 34에 설명되어 있습니다).
classplanerep { ... } // 비행기 객체를 나타냅니다
//
classplane {
public:
...
private:
airplanerep *rep; 실제 설명을 가리킵니다
};
항공기 객체는 크지 않고 포인터만 포함합니다(항목 14 및 m24에 설명된 대로 항공기 클래스가 가상 기능을 선언하면 암시적으로 포인터가 포함됩니다). 두 번째 포인터) . 그러나 항공기 개체를 할당하기 위해 연산자 new를 호출하면 포인터(또는 포인터 쌍)를 저장하는 데 필요한 것보다 더 많은 메모리를 얻을 수 있습니다. 이렇게 이상해 보이는 동작이 발생하는 이유는 Operator new와 Operator delete가 서로 정보를 전달해야 하기 때문입니다.
new 연산자의 기본 버전은 범용 메모리 할당자이므로 모든 크기의 메모리 블록을 할당할 수 있어야 합니다. 마찬가지로, 연산자 삭제도 모든 크기의 메모리 블록을 해제할 수 있어야 합니다. delete 연산자가 해제하려는 메모리 양을 확인하려면 new 연산자가 원래 할당한 메모리 양을 알아야 합니다. 연산자 new가 원래 할당한 메모리의 크기를 연산자 삭제에게 알려주는 일반적인 방법은 할당된 메모리 블록의 크기를 나타내기 위해 반환하는 메모리에 몇 가지 추가 정보를 미리 포함하는 것입니다. 즉,
airplane *pa = new Airplane;
명령문을 작성하면 다음과 같은 메모리 블록을 얻지 못할 것입니다.
pa—— > 비행기 객체의 메모리
대신 다음과 같은 메모리 블록을 얻습니다.
pa---> 메모리 블록 크기 데이터 + 비행기 객체의 메모리
비행기와 같은 매우 작은 개체의 경우 이 추가 데이터 정보는 개체를 동적으로 할당할 때 필요한 메모리 크기를 두 배로 늘립니다(특히 클래스에 가상 함수가 없는 경우).
메모리가 가장 중요한 환경에서 소프트웨어를 실행하는 경우 이러한 고급스러운 메모리 할당 방식을 감당할 수 없습니다. 항공기 클래스에 대해 특별히 new 연산자를 작성하면 할당된 각 메모리 블록에 추가 정보를 추가하지 않고도 각 항공기의 크기가 동일하다는 사실을 활용할 수 있습니다.
구체적으로 사용자 정의 연산자 new를 구현하는 방법이 있습니다. 먼저 기본 연산자 new가 원시 메모리의 큰 블록을 할당하도록 합니다. 각 블록은 많은 항공기 개체를 수용할 수 있을 만큼 충분히 큽니다. 비행기 객체의 메모리 블록은 이러한 대형 메모리 블록에서 가져옵니다. 현재 사용되지 않는 메모리 블록은 나중에 항공기에서 사용할 수 있도록 연결 목록(자유 연결 목록이라고 함)으로 구성됩니다. 각 객체가 (연결된 목록을 지원하기 위해) 다음 필드의 오버헤드를 감당해야 하는 것처럼 들리지만, 그렇지 않습니다. Rep 필드의 공간은 다음 포인터를 저장하는 데에도 사용됩니다(사용된 메모리 블록에만 필요하기 때문입니다) 마찬가지로 항공기 객체 대표 포인터로서 항공기 객체로 사용되지 않는 메모리 블록에만 다음 포인터가 필요합니다. 이는 결합을 사용하여 달성할 수 있습니다.
특정 구현 중에는 맞춤형 메모리 관리를 지원하도록 항공기 정의를 수정해야 합니다. 다음과 같이 할 수 있습니다:
class Airplane { // 수정된 클래스 - 맞춤형 메모리 관리 지원
public: //
static void * Operator new(size_t size);
...
비공개:
연합 {
Airplanerep *rep; // 사용된 객체의 경우
airplane *next; // 사용되지 않은 객체의 경우(무료 연결 목록에서)
};
// 클래스 상수, 개수 지정
/ / 큰 메모리 블록에 넣고 나중에 초기화하는 비행기 개체
static const int block_size;
staticplane *headoffreelist;
};
위 내용 코드는 연산자 new 함수, 공용체(rep 및 next 필드가 동일한 공간을 차지하도록 함), 상수(대형 메모리 블록의 크기 지정) 및 정적 포인터(자유 연결 목록의 헤더 추적) 등 여러 선언을 추가합니다. ). 각 항공기 개체가 아닌 전체 클래스에 대해 단 하나의 자유 연결 목록이 있으므로 헤더 포인터를 정적 멤버로 선언하는 것이 중요합니다.
이제 연산자 새 함수를 작성할 시간입니다.
void *plane::operator new(size_t size)
{
// "잘못된" 크기 요청을 다음으로 전달합니다. :operator new() 처리;
// 자세한 내용은 8절을 참조하세요
if (size != sizeof(airplane))
return ::operator new(size);
airplane * p = // p는 자유 연결 리스트의 헤드를 가리킵니다.
headoffreelist; //
// p가 유효하면 헤드를 다음 요소로 이동합니다.
//
if ( p)
headoffreelist = p->next;
else {
// free linked list가 비어 있으면 큰 메모리 블록을 할당하고,
// block_size 항공기를 수용할 수 있습니다. object
airplane *newblock =
static_cast(::operator new(block_size *
sizeof(airplane)));
// 각각의 작은 메모리 블록을 연결하여 새로운 무료 연결 목록을 형성합니다.
//0번째 요소는 new 연산자의 호출자에게 반환되므로 건너뜁니다.
//
for (int i = 1; i < block_size-1; ++i)
newblock [i].next = &newblock[i+1];
// 널 포인터로 연결 목록 종료
newblock[block_size-1].next = 0;
/ / p는 테이블의 헤드로 설정되고, headoffreelist가 가리키는 //
뒤에는 메모리 블록
p = newblock;
headoffreelist = &newblock[1];
}<이 옵니다. 🎜>
return p;
}
항목 8을 읽으면 new 연산자가 메모리 할당 요청을 충족할 수 없을 때 new 핸들러 함수 및 예외와 관련된 일련의 루틴이 있다는 것을 알 수 있습니다. 조치가 실행됩니다. 위의 코드에는 이러한 단계가 없습니다. 이는 연산자 new가 관리하는 메모리가::operator new에서 할당되기 때문입니다. 즉, 연산자 new는 ::operator new가 실패하는 경우에만 실패한다는 의미입니다. 그리고 ::operator new가 실패하면 new-handler의 작업을 수행하므로(예외 발생으로 종료될 수 있음) 항공기 Operator new가 이를 처리할 필요가 없습니다. 즉, new-handler의 작업은 실제로 여전히 존재하며, 표시되지 않고 ::operator new에 숨겨져 있습니다.
연산자 new를 사용하여 다음으로 해야 할 일은 항공기의 정적 데이터 멤버를 정의하는 것입니다.
airplane *airplane::headoffreelist;
const int Airplane:: block_size = 512;
정적 멤버의 초기 값은 기본적으로 0으로 설정되어 있으므로 headoffreelist를 null 포인터로 명시적으로 설정할 필요가 없습니다. block_size는 ::operator new에서 가져올 메모리 블록의 크기를 결정합니다.
이 버전의 Operator new는 매우 잘 작동합니다. 기본 연산자인 new보다 항공기 개체에 더 적은 메모리를 할당하고 더 빠르게 실행됩니다. 아마도 두 배 더 빠르게 실행될 수 있습니다. 이는 놀라운 일이 아닙니다. 범용 기본 연산자 new는 다양한 크기의 메모리 요청과 내부 및 외부 조각화를 처리해야 하지만 new 연산자는 연결된 목록의 포인터 쌍에서만 작동합니다. 속도를 위해 유연성을 교환하는 것은 종종 쉽습니다.
아래에서는 연산자 삭제에 대해 설명하겠습니다. 운영자 삭제를 기억하시나요? 이 문서는 운영자 삭제에 관한 것입니다. 그러나 지금까지 항공기 클래스는 운영자 신규만 선언했을 뿐 운영자 삭제는 선언하지 않았습니다. 다음 코드를 작성하면 어떤 일이 일어날지 생각해 보세요.
airplane *pa = new Airplane; // Call
// Airplane::operator new
...
delete pa; // Call::operator delete
이 코드를 읽을 때 귀를 쫑긋 세우면 비행기가 부서지고 타는 소리, 프로그래머가 우는 소리가 들립니다. 문제는 new 연산자(비행기에 정의된 것)가 헤더 정보 없이 메모리에 대한 포인터를 반환하는 반면, delete 연산자(기본값)는 전달된 메모리에 헤더 정보가 포함되어 있다고 가정한다는 것입니다. 이것이 비극의 원인입니다.
이 예는 일반적인 원칙을 보여줍니다. 즉, 다른 가정이 발생하지 않도록 연산자 new와 연산자 삭제를 동시에 작성해야 합니다. 메모리 할당 프로그램을 직접 작성하는 경우 릴리스 프로그램도 작성해야 합니다. (이 규칙을 따라야 하는 또 다른 이유는 물체 계산에 관한 기사의 배치 섹션에 있는 사이드바를 참조하십시오.)
따라서 비행기 클래스를 다음과 같이 계속 디자인하십시오.
비행기 클래스 { / / 이전과 동일하지만
public: // 연산자 삭제 선언
...
static void 연산자 delete(void *deadobject,
size_t size)를 추가했습니다. ;
};
// delete 연산자에 전달된 것은 메모리 블록입니다.
// 크기가 정확하면 여유 메모리 앞에 추가됩니다. 차단 목록
/ /
voidplane::operator delete(void *deadobject,
size_t size)
{
if (deadobject == 0) return; 8
if (size != sizeof(airplane)) { // 항목 8 참조
::operator delete(deadobject);
return;
}
airplane *carcass =
static_cast (deadobject);
carcass->next = headoffreelist;
headoffreelist = carcass;
}
"잘못된" 크기 요청이 이전에 new 연산자로 전송되었기 때문입니다. 전역 연산자 new가 사용된 경우(항목 8 참조) "잘못된" 크기 객체도 처리를 위해 전역 연산자 delete에 넘겨져야 합니다. 이렇게 하지 않으면 이전에 피하려고 애썼던 문제, 즉 new와 delete 사이의 구문 불일치 문제가 다시 발생하게 됩니다.
흥미롭게도 삭제하려는 객체가 가상 소멸자 없이 클래스에서 상속된 경우 delete 연산자에 전달된 size_t 값이 올바르지 않을 수 있습니다. 이것이 기본 클래스에 가상 소멸자가 있어야 하는 이유이며 항목 14에는 두 번째로 더 설득력 있는 이유가 나열되어 있습니다. 여기서 기본 클래스가 가상 생성자를 생략하면 연산자 삭제가 올바르게 작동하지 않을 수 있다는 점을 기억하세요.
다 잘되고 좋은데, 눈살을 찌푸리는 모습을 보면 메모리 누수를 걱정하고 있음을 알 수 있습니다. 개발 경험이 많다면 항공기의 Operator new가 큰 메모리 덩어리를 얻기 위해::operator new를 호출하지만 항공기의 Operator delete가 이를 해제하지 않는다는 사실을 놓치지 않을 것입니다. 메모리 누수! 메모리 누수! 당신의 머릿속에서 울리는 알람벨 소리가 선명하게 들립니다.
하지만 제 답변을 잘 들어주세요. 여기에는 메모리 누수가 없습니다!
메모리 누수가 발생하는 이유는 메모리 할당 후 메모리에 대한 포인터가 손실되기 때문입니다. 가비지 처리나 언어 외부의 기타 메커니즘이 없으면 이 메모리는 회수되지 않습니다. 그러나 위 디자인에서는 메모리 포인터가 절대 손실되지 않으므로 메모리 누수가 발생하지 않습니다. 각각의 대형 메모리 블록은 먼저 항공기 크기의 청크로 분할된 다음 이 청크를 자유 연결 리스트에 배치합니다. 클라이언트가 Airplane::operator new를 호출하면 작은 블록이 자유 연결 목록에서 제거되고 클라이언트는 작은 블록에 대한 포인터를 얻습니다. 클라이언트가 운영자 삭제를 호출하면 작은 블록이 사용 가능 목록에 다시 배치됩니다. 이 설계에서는 모든 메모리 블록이 항공기 개체에 의해 사용되거나(이 경우 메모리 누수를 방지하는 것은 클라이언트의 책임임) 자유 연결 목록에 있습니다(이 경우 메모리 블록에 포인터가 있음). 따라서 여기에는 메모리 누수가 없습니다.
그러나 ::operator new가 반환한 메모리 블록은 Aircraft::operator delete에 의해 해제된 적이 없는 것이 사실입니다. 이 메모리 블록에는 메모리 풀이라는 이름이 있습니다. 그러나 메모리 누수와 메모리 풀 사이에는 중요한 차이점이 있습니다. 클라이언트가 제대로 작동하더라도 메모리 누수는 무한정 커질 수 있습니다. 메모리 풀의 크기는 클라이언트가 요청한 최대 메모리 양을 초과하지 않습니다.
:operator new가 반환한 메모리 블록이 사용되지 않을 때 자동으로 해제되도록 항공기의 메모리 관리 프로그램을 수정하는 것은 어렵지 않지만 여기서는 두 가지 이유가 있습니다. : 첫 번째 이유는 메모리 관리를 사용자 정의하려는 원래 의도와 관련이 있습니다. 메모리 관리를 사용자 정의해야 하는 많은 이유가 있습니다. 그 중 가장 기본적인 이유는 기본 연산자 new 및 연산자 delete가 너무 많은 메모리를 사용하거나 느리게 실행된다는 것입니다. 이러한 대형 메모리 블록을 추적하고 해제하기 위해 작성된 모든 추가 바이트와 추가 명령문은 메모리 풀 전략을 사용할 때보다 소프트웨어 실행 속도를 늦추고 더 많은 메모리를 사용하게 됩니다. 높은 성능을 요구하는 라이브러리나 프로그램을 설계할 때 메모리 풀의 크기가 합리적인 범위 내에 있을 것으로 예상된다면 메모리 풀 방식을 사용하는 것이 가장 좋습니다.
두 번째 이유는 일부 불합리한 프로그램 행위에 대한 처리와 관련이 있습니다. 항공기의 메모리 관리 프로그램이 수정되었다고 가정하면 항공기 운영자 삭제는 객체가 존재하지 않는 메모리의 큰 덩어리를 해제할 수 있습니다. 그런 다음 다음 프로그램을 살펴보세요: int main()
{
airplane *pa = new Airplane; // 첫 번째 할당: 큰 메모리 블록 가져오기,
// 사용 가능한 연결 목록 생성 등
delete pa; //메모리 블록이 비어 있습니다.
//해제
pa = new Airplane; //큰 메모리 블록을 다시 가져옵니다.
// 무료 연결 리스트 등을 생성합니다.
delete pa; // 메모리 블록이 다시 비어 있습니다.
// 해제
... // 아이디어가 있습니다...
return 0;
}
이 끔찍하고 작은 프로그램은 기본 연산자인 new 및 연산자 delete로 작성된 프로그램보다 느리게 실행되고 더 많은 메모리를 차지합니다. 메모리 풀.
물론 이러한 불합리한 상황을 처리할 수 있는 방법이 있지만, 더 특별한 상황을 고려할수록 메모리 관리 기능을 다시 구현할 가능성이 높아지고 결국 무엇을 얻게 될까요? 메모리 풀은 모든 메모리 관리 문제를 해결할 수 없으며 많은 상황에 적합합니다.
실제 개발에서는 다양한 클래스에 대해 메모리 풀 기반 기능을 구현해야 하는 경우가 많습니다. '이 고정 크기 메모리 할당자를 캡슐화하여 편리하게 사용할 수 있는 방법이 있어야 한다'고 생각하실 것입니다. 예, 방법이 있습니다. 비록 오랫동안 이 조항에 대해 잔소리를 해왔지만, 그래도 간단히 소개하고 구체적인 구현은 독자들의 연습 문제로 남겨두고 싶습니다.
다음은 단순히 풀 클래스의 최소 인터페이스를 제공합니다(항목 18 참조). 풀 클래스의 각 객체는 특정 유형의 객체에 대한 메모리 할당자입니다(크기는 의 생성자에 지정됨). 수영장).
class pool {
public:
pool(size_t n) // 크기 n 객체에 대한
생성// 할당자
void * alloc(size_t n); // 객체에 충분한 메모리를 할당합니다
// 항목 8의 연산자 새 규칙을 따릅니다.
void free (void *p, size_t n); // p가 가리키는 메모리를 메모리 풀로 반환합니다.
// 8절의 연산자 삭제 규칙을 따릅니다.
~pool(); 메모리 풀
};
의 모든 메모리는 풀 개체 생성을 지원하고 할당 및 해제 작업을 수행하며 소멸됩니다. 풀 개체가 삭제되면 해당 개체에 할당된 모든 메모리가 해제됩니다. 이는 이제 비행기 기능에서 나타나는 메모리 누수와 같은 동작을 방지할 수 있는 방법이 있음을 의미합니다. 그러나 이는 풀의 소멸자가 너무 빨리 호출되면(메모리 풀을 사용하는 모든 객체가 파괴되지는 않음) 일부 객체에서 사용 중인 메모리가 갑자기 사라진 것을 발견할 수도 있음을 의미합니다. 결과는 예측할 수 없는 경우가 많습니다.
이 풀 클래스를 사용하면 Java 프로그래머라도 자신의 메모리 관리 기능을 비행기 클래스에 쉽게 추가할 수 있습니다:
class Airplane {
public:
... // 일반 항공기 기능
static void * 연산자 new(size_t size);
static void 연산자 delete(void *p, size_t size);
private:
airplanerep *rep; // 실제 설명에 대한 포인터
static pool mempool; //비행기용 메모리 풀
};
inline void *plane: :operator new(size_t size )
{ return mempool.alloc(size); }
inline voidplane::operator delete(void *p,
size_t size)
{ mempool. ; }
// 항공기 객체에 대한 메모리 풀을 생성합니다.
// 클래스 구현 파일에
poolplane::mempool(sizeof(airplane))을 구현합니다 ;
이번 디자인은 항공기 클래스가 더 이상 비비행기 코드와 혼합되지 않기 때문에 이전 디자인보다 훨씬 더 명확하고 깔끔해졌습니다. 공용체, 자유 연결 목록 헤드 포인터 및 원래 메모리 블록의 크기를 정의하는 상수는 모두 풀 클래스에 숨겨져 있습니다. 풀을 작성하는 프로그래머가 메모리 관리의 세부 사항에 대해 걱정하도록 하십시오. 귀하의 임무는 항공기 클래스가 제대로 작동하도록 만드는 것입니다.
이제 사용자 정의 메모리 관리 프로그램이 프로그램 성능을 크게 향상할 수 있으며 풀과 같은 클래스에 캡슐화될 수 있다는 점을 이해해야 합니다. 그러나 요점을 잊지 마십시오. Operator new와 Operator delete는 동시에 작동해야 합니다. 따라서 Operator new를 작성하는 경우에는 Operator delete도 작성해야 합니다.
이상은 C++ 메모리 관리에 대한 자세한 설명입니다. 더 많은 관련 글은 PHP 중국어 홈페이지(www.php.cn)를 참고해주세요!