동적 메모리 할당. C에서의 메모리 할당(malloc 함수) 동적 메모리 할당을 위한 예약어

프로그램은 두 가지 주요 방법으로 컴퓨터의 주 메모리에 정보를 저장할 수 있습니다. 첫 번째는 배열, 구조 및 클래스를 포함하여 전역 및 지역 변수를 사용합니다. 전역 및 정적 지역 변수의 경우 정보의 저장 위치는 프로그램 실행 기간 동안 고정됩니다. 지역 변수의 경우 스택에 메모리가 할당됩니다. Borland C++는 이러한 변수를 매우 효율적으로 처리하지만 이를 사용하려면 프로그래머가 프로그램 실행 중에 필요한 메모리 양을 미리 알아야 합니다.

정보를 저장하는 두 번째 방법은 Borland C++ 동적 메모리 할당 시스템을 사용하는 것입니다. 이 방법은 정보를 저장하기 위한 메모리를 여유 메모리 영역에서 필요에 따라 할당하고 다시 반환합니다. 더 이상 필요하지 않을 때 해제됩니다. 여유 메모리 영역은 프로그램이 상주하는 메모리 영역과 스택 사이에 있습니다. 이 영역을 힙이라고 하며 동적 메모리 할당 요청에 사용됩니다.

동적 메모리를 사용하는 이점은 프로그램 실행 중에 다른 정보를 저장하는 데 동일한 메모리를 사용할 수 있다는 것입니다. 메모리는 특정 목적을 위해 할당되고 사용이 완료되면 해제되므로 프로그램의 다른 부분에서 다른 용도로 동일한 메모리를 다른 시간에 사용할 수 있습니다. 동적 메모리 할당의 또 다른 장점은 연결된 목록, 이진 트리 및 기타 동적 데이터 구조를 생성할 수 있다는 것입니다.

C의 동적 메모리 할당의 핵심은 표준 라이브러리의 일부인 malloc() 및 free() 함수입니다. malloc() 함수에 의해 메모리 할당 요청이 이루어질 때마다 사용 가능한 여유 메모리의 일부가 할당됩니다. free() 함수를 사용하여 이 메모리를 해제할 때마다 이 메모리는 시스템으로 다시 반환됩니다.

C++ 언어는 두 개의 동적 메모리 할당 연산자인 new 및 delete를 정의합니다.

ANSI C 표준은 calloc(), malloc(), free() 및 realloc()의 네 가지 동적 메모리 할당 함수만 정의합니다. 그러나 Borland C++에는 몇 가지 다른 동적 메모리 할당 기능이 포함되어 있습니다. 최신 32비트 메모리 모델에 대한 코드를 컴파일할 때 메모리는 평평하며 일반적으로 4개의 표준 메모리 할당 함수만 사용됩니다.

ANSI C 표준은 동적 메모리 할당에 필요한 헤더 정보가 stdlib.h 파일에 포함되도록 지정합니다. 그러나 Borland C++에서는 stdlib.h 또는 alloc.h 헤더 파일을 사용할 수 있습니다. 여기서는 이식성을 제공하는 stdlib.h 헤더 파일을 사용합니다. 일부 다른 동적 메모리 할당 기능에는 alloc.h, malloc.h 또는 dos.h 헤더 파일이 필요합니다. 각 기능을 사용하기 위해 어떤 헤더 파일이 필요한지 특별히 주의하십시오.

그래서. 이 주제에서 가장 흥미로운 세 번째 유형은 동적 유형의 메모리입니다.

이전에는 어레이 작업을 어떻게 했습니까? int a 지금 우리는 어떻게 일합니까? 필요한 만큼 선택:

#포함하다 < stdio.h> #포함하다 < stdlib.h> int main() ( size_t 크기; // int에 대한 포인터 생성 // 본질적으로 빈 배열입니다.정수 *목록; 스캔프( "%lu", 크기); // int 크기의 크기 요소에 대한 메모리 할당 // 그리고 "빈 배열"은 이제 이 메모리를 참조합니다. list = (int *)malloc (크기 * sizeof (int )); for (int i = 0; i< size; ++i) { scanf ("%디" < size; ++i) { printf ("%디", *(리스트 + i)); ) // 자신을 정리하는 것을 잊지 마세요!무료(목록); ) // *

무효 * malloc(size_t size);

그러나 일반적으로 이것은 초기화되지 않은 메모리(0이 아니라 쓰레기)의 크기 바이트를 할당하는 함수입니다.

할당이 성공하면 할당된 메모리의 첫 번째 바이트에 대한 포인터가 반환됩니다.

실패하면 NULL입니다. 또한 errno는 ENOMEM과 동일합니다(이 놀라운 변수는 나중에 살펴보겠습니다). 즉, 다음과 같이 작성하는 것이 더 정확할 것입니다.

#포함하다 < stdio.h> #포함하다 < stdlib.h> int main () ( size_t size; int *list; scanf ( "%lu", 크기); list = (int *)malloc (크기 * sizeof (int )); if (list == NULL ) ( goto error; ) for (int i = 0 ; i< size; ++i) { scanf ("%디", 목록 + i); ) for (int i = 0; i< size; ++i) { printf ("%디", *(리스트 + i)); ) 무료(목록); 반환 0 ; 오류: 반환 1 ; ) // *

NULL 포인터를 지울 필요가 없습니다.

#포함하다 < stdlib.h> int main() (무료(NULL); )

- 같은 clang에서는 모든 것이 잘 진행되지만(아무것도 하지 않음) 더 특이한 경우에는 프로그램이 충돌할 수 있습니다.

mana에서 malloc 및 free 옆에 다음도 표시됩니다.

    void * calloc(size_t count, size_t size);

    뿐만 아니라 malloc은 size 바이트 크기의 카운트 개체에 대한 메모리를 할당합니다. 할당된 메모리는 0으로 초기화됩니다.

    void * realloc(void *ptr, size_t 크기);

    (가능한 경우) ptr이 가리키는 메모리를 크기 바이트로 재할당합니다. ptr이 가리키는 할당된 메모리를 늘릴 공간이 충분하지 않으면 realloc은 새 할당(할당)을 만들고 ptr이 가리키는 이전 데이터를 복사하고 이전 할당을 해제하고 할당된 메모리에 대한 포인터를 반환합니다.

    ptr이 NULL이면 realloc은 malloc을 호출하는 것과 동일합니다.

    size가 0이고 ptr이 NULL이 아니면 최소 크기 청크가 할당되고 원래 청크가 해제됩니다.

    void * reallocf(void *ptr, size_t size);

    FreeBSD API의 아이디어. realloc 과 비슷하지만 재할당에 실패하면 수신된 포인터를 지웁니다.

    void*valloc(size_t 크기);

    malloc 과 같지만 할당된 메모리는 페이지 정렬됩니다.

다른 많은 언어와 마찬가지로 C++에서 메모리는 정적으로(프로그램이 시작되기 전에 메모리가 할당되고 프로그램이 종료된 후에 해제됨) 또는 동적으로(프로그램 실행 중에 메모리가 할당되고 해제됨) 할당될 수 있습니다.

정적 메모리 할당은 포인터를 사용하지 않고 프로그램에서 명시적으로 선언된 모든 전역 및 지역 변수에 대해 수행됩니다. 이 경우 메모리 할당 메커니즘은 프로그램의 변수 선언 위치와 선언의 메모리 클래스 지정자에 의해 결정됩니다. 변수의 유형은 할당된 메모리 영역의 크기를 결정하지만 메모리 할당 메커니즘은 유형에 의존하지 않습니다. 정적 메모리 할당에는 두 가지 주요 메커니즘이 있습니다.

· 전역 및 정적(정적 지정자로 선언) 변수 각각에 대한 메모리는 유형 설명에 따라 프로그램 실행 시작 전에 할당됩니다. 프로그램 실행의 처음부터 끝까지 이러한 변수는 할당된 메모리 영역과 연결됩니다. 따라서 범위는 다르지만 전역 수명을 갖습니다.

블록 내부에 선언되고 지정자가 없는 지역 변수의 경우 공전,메모리는 다른 방식으로 할당됩니다. 프로그램이 시작되기 전에 (로드 될 때) 상당히 큰 메모리 영역이 할당됩니다. 스택(때때로 용어 사용 프로그램 스택또는 호출 스택스택을 추상 데이터 유형으로 구별하기 위해). 스택 크기는 개발 환경에 따라 다릅니다. 예를 들어 MS Visual C++에서는 기본적으로 스택에 1MB가 할당됩니다(이 값은 구성 가능). 프로그램 실행 중에 특정 블록에 들어가면 해당 유형에 대한 설명에 따라 블록에 지역화 된 변수에 대한 메모리가 스택에 할당되고 블록이 종료되면이 메모리가 해제됩니다. 이러한 프로세스는 자동으로 수행되므로 C++에서 지역 변수를 자주 호출합니다. 자동적 인.

함수가 호출되면 로컬 변수, 매개변수(매개변수의 값 또는 주소가 스택에 배치됨), 함수의 결과 및 리턴 포인트 저장(프로그램의 주소)을 위해 메모리가 스택에 할당됩니다. 함수가 끝날 때 반환하려는 위치. 함수가 종료되면 관련된 모든 데이터가 스택에서 제거됩니다.

"스택"이라는 용어의 사용은 설명하기 쉽습니다. 메모리 할당 및 해제에 대한 허용된 접근 방식에서는 스택에서 마지막으로 푸시된 변수(가장 깊게 중첩된 블록에 있는 변수)가 먼저 스택에서 제거됩니다. 즉, 메모리 할당 및 해제는 LIFO 원칙(LAST IN - FIRST OUT, 후입선출)에 따라 이루어집니다. 이것이 스택이 작동하는 방식입니다. 동적 데이터 구조로서의 스택과 가능한 구현은 다음 섹션에서 설명합니다.



정적으로 할당된 메모리 영역이 항상 실제로 데이터로 채워지는 것은 아니기 때문에 많은 경우에 정적 메모리 할당은 비효율적인 사용으로 이어집니다(대형 배열의 경우 특히 그렇습니다). 따라서 많은 언어에서와 마찬가지로 C++에도 변수를 동적으로 생성하는 편리한 방법이 있습니다. 동적 메모리 할당의 본질은 메모리가 프로그램의 요청에 따라 할당(캡처)되고 요청에 따라 해제된다는 것입니다. 이 경우 메모리 크기는 변수의 유형에 따라 결정되거나 요청에 명시적으로 지정될 수 있습니다. 이러한 변수를 호출 동적. 동적 변수를 만들고 사용하는 가능성은 포인터의 메커니즘과 밀접한 관련이 있습니다.

위의 모든 내용을 요약하면 프로그램 실행 중에 다음과 같은 메모리 할당 체계를 상상할 수 있습니다(그림 2.1). 그림에서 서로 상대적인 영역의 위치는 다소 조건부입니다. 메모리 할당의 세부 사항은 운영 체제에서 처리합니다.

그림 2.1 - 메모리 할당 체계

이 섹션의 결론을 내리기 위해 스택 작업 과정에서 발생하는 한 가지 고통스러운 문제인 스택 오버플로의 가능성(이 긴급 상황은 일반적으로 스택 오버플로). 문제가 발생한 이유는 이해할 수 있습니다. 프로그램이 로드될 때 스택에 할당되는 제한된 메모리 양입니다. 스택 오버플로가 발생할 가능성이 가장 높은 상황은 큰 크기의 로컬 배열과 재귀 함수 호출의 깊은 중첩입니다(보통 재귀 함수 프로그래밍이 부주의하게 프로그래밍될 때 발생합니다. 예를 들어 일부 터미널 분기를 잊어버린 경우).



스택 오버플로 문제를 더 잘 이해하기 위해 이러한 간단한 실험을 수행하는 것이 좋습니다. 기능 중 기본예를 들어 백만 요소 크기의 정수 배열을 선언합니다. 프로그램은 컴파일되지만 스택 오버플로 오류와 함께 실행됩니다. 이제 배열 선언의 시작 부분에 지정자를 추가하십시오. 공전(또는 함수에서 배열 설명을 제거하십시오. 기본) - 프로그램이 작동합니다!

이것에 대해 기적적인 것은 없습니다. 이제 배열이 스택이 아니라 전역 및 정적 변수 영역에 배치된다는 것입니다. 이 영역의 메모리 크기는 컴파일러에 의해 결정됩니다. 프로그램이 컴파일되면 작동합니다.

그러나 일반적으로 프로그램에서 거대한 정적으로 생성된 배열을 선언할 필요는 없습니다. 대부분의 경우 보다 효율적이고 유연한 방법은 이러한 데이터에 대해 메모리를 동적으로 할당하는 것입니다.

C++는 세 가지 기본 유형을 지원합니다. 배당(이상 "배포") 메모리, 그 중 두 가지는 이미 잘 알고 있습니다.

정적 메모리 할당및 변수를 유지합니다. 메모리는 프로그램이 시작될 때 한 번 할당되며 전체 프로그램에서 유지됩니다.

자동 메모리 할당및 에 대해 수행됩니다. 이러한 변수를 포함하는 블록이 들어갈 때 메모리가 할당되고 종료될 때 제거됩니다.

동적 메모리 할당이 수업의 주제입니다.

변수의 동적 할당

정적 및 자동 메모리 할당에는 두 가지 공통 속성이 있습니다.

동적 메모리 할당은 어떻게 작동합니까?

컴퓨터에는 프로그램에서 사용할 수 있는 메모리(많을 수도 있음)가 있습니다. 프로그램을 실행할 때 운영 체제는 해당 프로그램을 해당 메모리의 일부로 로드합니다. 그리고 프로그램에서 사용하는 이 메모리는 여러 부분으로 나뉘며 각 부분은 특정 작업을 수행합니다. 한 부분은 코드를 포함하고 다른 부분은 일반 작업(호출된 함수 추적, 전역 및 지역 변수 생성 및 소멸 등)을 수행하는 데 사용됩니다. 나중에 이야기하겠습니다. 그러나 사용 가능한 메모리의 대부분은 프로그램의 할당 요청을 기다리고 있습니다.

메모리를 동적으로 할당할 때 프로그램이 사용할 메모리의 일부를 예약하도록 운영 체제에 요청하는 것입니다. OS가 이 요청을 이행할 수 있으면 해당 메모리의 주소가 프로그램으로 다시 반환됩니다. 이제부터 프로그램은 원하는 대로 이 메모리를 사용할 수 있습니다. 이 메모리에 필요한 모든 작업을 이미 완료한 경우 다른 요청에 배포하기 위해 운영 체제로 다시 반환해야 합니다.

정적 또는 자동 메모리 할당과 달리 프로그램은 동적으로 할당된 메모리를 요청하고 반환합니다.

메모리 확보

변수를 동적으로 할당할 때 또는 균일 초기화(C++11에서)를 사용하여 초기화할 수도 있습니다.

int *ptr1 = 새로운 int(7); // 직접 초기화 사용 int *ptr2 = new int ( 8 ); // 균일한 초기화 사용

동적으로 할당된 변수로 필요한 모든 작업이 이미 완료된 경우 이 메모리를 해제하도록 C++에 명시적으로 지시해야 합니다. 변수의 경우 다음과 같이 수행됩니다. 운영자 삭제:

// ptr이 이미 새로운 delete ptr로 할당되었다고 가정합니다. // ptr이 가리키는 메모리를 다시 운영 체제로 되돌립니다. ptr = 0; // ptr을 null로 만듭니다(C++11에서는 0 대신 nullptr 사용).

삭제 연산자는 실제로 아무 것도 삭제하지 않습니다. 단순히 이전에 운영 체제에 다시 할당된 메모리를 반환합니다. 그런 다음 운영 체제는 해당 메모리를 다른 응용 프로그램(또는 동일한 응용 프로그램)에 재할당할 수 있습니다.

우리가 제거하는 것처럼 보일 수 있지만 변하기 쉬운하지만 그렇지 않습니다! 포인터 변수는 여전히 이전과 동일한 범위를 가지며 다른 변수와 마찬가지로 새 값을 할당할 수 있습니다.

동적으로 할당된 메모리를 가리키지 않는 포인터를 삭제하면 문제가 발생할 수 있습니다.

교수형 포인터

C++은 해제된 메모리의 내용이나 삭제되는 포인터의 값에 어떤 일이 발생하는지에 대해 보장하지 않습니다. 대부분의 경우 운영 체제로 반환된 메모리에는 이전과 동일한 값이 포함됩니다. 풀어 주다, 그리고 포인터는 여전히 이미 해제된(삭제된) 메모리만 가리킬 것입니다.

해제된 메모리를 가리키는 포인터를 호출합니다. 교수형 포인터. 댕글링 포인터를 역참조하거나 삭제하면 예기치 않은 결과가 발생합니다. 다음 프로그램을 고려하십시오.

#포함하다 int main() ( int *ptr = new int; *ptr = 8; // 할당된 메모리 위치에 값을 넣습니다. delete ptr; // 메모리를 다시 운영 체제로 반환합니다. ptr은 이제 댕글링 포인터입니다. std:: 쿠우트<< *ptr; // разыменование висячего указателя приведёт к неожиданным результатам delete ptr; // попытка освободить память снова приведёт к неожиданным результатам также return 0; }

#포함하다

정수 메인()

int * ptr = 새 int ; // 정수 변수를 동적으로 할당

* ptr = 8 ; // 할당된 메모리 위치에 값 넣기

ptr 삭제 ; // 메모리를 운영 체제로 되돌립니다. ptr은 이제 매달린 포인터입니다.

표준::cout<< * ptr ; // 매달린 포인터를 역참조하면 예기치 않은 결과가 발생합니다.

ptr 삭제 ; // 메모리를 다시 해제하려고 하면 예기치 않은 결과도 발생합니다.

반환 0 ;

위의 프로그램에서 이전에 동적 변수에 할당된 값 8은 해제된 후에도 여전히 있을 수도 있고 없을 수도 있습니다. 해제된 메모리가 이미 다른 응용 프로그램(또는 운영 체제 자체 사용을 위해)에 할당되었을 수 있으며 액세스를 시도하면 운영 체제가 자동으로 프로그램을 종료할 수 있습니다.

메모리를 해제하는 프로세스는 생성으로 이어질 수도 있습니다. 여러 개의교수형 포인터. 다음 예를 고려하십시오.

#포함하다 int main() ( int *ptr = new int; // 동적으로 정수 변수를 할당합니다. int *otherPtr = ptr; // otherPtr은 이제 ptr과 동일한 할당 메모리를 가리킵니다. delete ptr; // 메모리를 다시 운영 체제로 반환합니다. ptr 및 otherPtr은 이제 댕글링 포인터입니다 ptr = 0; // ptr은 이제 nullptr입니다 // 그러나 otherPtr은 여전히 ​​댕글링 포인터입니다! return 0; )

#포함하다

정수 메인()

int * ptr = 새 int ; // 정수 변수를 동적으로 할당

int * otherPtr = ptr ; // otherPtr은 이제 ptr과 동일한 할당 메모리를 가리킵니다.

ptr 삭제 ; // 메모리를 운영 체제로 되돌립니다. ptr 및 otherPtr은 이제 매달려 있는 포인터입니다.

ptr = 0 ; // ptr은 이제 nullptr입니다.

// 그러나 otherPtr은 여전히 ​​댕글링 포인터입니다!

반환 0 ;

첫째, 여러 포인터가 할당된 메모리의 동일한 부분을 가리키는 상황을 피하십시오. 이것이 가능하지 않은 경우 모든 포인터 중 메모리를 "소유"하고(및 삭제를 담당하는) 포인터와 단순히 액세스하는 포인터를 명확히 합니다.

둘째, 포인터를 삭제할 때 삭제 직후 종료되지 않으면 null로 만들어야 합니다. 값 0을 할당합니다(또는 C++11에서). "삭제 직후 범위를 벗어남"이란 포인터가 선언된 블록의 맨 끝에서 포인터를 삭제한다는 의미입니다.

규칙: 삭제 직후 범위를 벗어나지 않는 한 삭제된 포인터를 0(또는 C++11의 nullptr)으로 설정합니다.

새 연산자

운영 체제에서 메모리를 요청하면 드문 경우지만 사용하지 못할 수 있습니다(즉, 사용하지 못할 수 있음).

기본적으로 new 연산자가 작동하지 않으면 메모리가 할당되지 않은 것입니다. 예외 bad_alloc. 이 예외가 잘못 처리되면(아직 예외를 살펴보고 처리하지 않았기 때문에 그렇게 될 것입니다) 프로그램은 처리되지 않은 예외 오류와 함께 중단(충돌)합니다.

많은 경우에 new로 예외를 던지는 과정(프로그램 충돌은 물론)은 바람직하지 않으므로 메모리를 할당할 수 없는 경우 null 포인터를 반환하는 대체 형식의 new가 있습니다. 당신은 추가해야합니다 std::nothrow 상수새 키워드와 데이터 유형 사이:

int *value = new (std::nothrow) int; // 정수 변수의 동적 할당이 실패하면 포인터 값이 null이 됩니다.

위의 예에서 new가 동적으로 할당된 메모리가 있는 포인터를 반환하지 않으면 null 포인터가 반환됩니다.

또한 역참조하지 않는 것이 좋습니다. 이렇게 하면 예기치 않은 결과(대부분 프로그램 충돌)가 발생할 수 있기 때문입니다. 따라서 가장 좋은 방법은 메모리 할당에 대한 모든 요청을 확인하여 이러한 요청이 성공적으로 실행되고 메모리가 할당되었는지 확인하는 것입니다.

int *value = new (std::nothrow) int; // 정수 값에 대한 동적 메모리 할당 요청 if (!value) // new가 null을 반환하는 경우 처리(즉, 할당된 메모리가 없음) ( // 이 경우 처리 std::cout<< "Could not allocate memory"; }

new 연산자가 메모리를 할당하지 않는 경우는 극히 드물기 때문에 프로그래머는 일반적으로 이 검사를 잊어버립니다!

널 포인터와 동적 메모리 할당

Null 포인터(값이 0 또는 nullptr인 포인터)는 동적 메모리 할당에 특히 유용합니다. 그들의 존재는 "이 포인터에 메모리가 할당되지 않았습니다."라고 알려줍니다. 그리고 이것은 조건부 메모리 할당을 수행하는 데 사용할 수 있습니다.

// ptr에 아직 메모리가 할당되지 않은 경우 할당 if (!ptr) ptr = new int;

널 포인터를 제거해도 아무런 영향을 미치지 않습니다. 따라서 다음은 필요하지 않습니다.

if (ptr) 삭제 ptr;

만약 (ptr)

ptr 삭제 ;

대신 다음과 같이 작성할 수 있습니다.

ptr 삭제 ;

ptr이 null이 아니면 동적으로 할당된 변수가 제거됩니다. 포인터 값이 null이면 아무 일도 일어나지 않습니다.

메모리 누수

동적으로 할당된 메모리에는 범위가 없습니다. 명시적으로 해제될 때까지 또는 프로그램이 실행을 종료할 때까지(및 운영 체제가 모든 메모리 버퍼 자체를 플러시할 때까지) 할당된 상태로 유지됩니다. 그러나 동적으로 할당된 메모리 주소를 저장하는 데 사용되는 포인터는 일반 변수 범위 지정 규칙을 따릅니다. 이 불일치로 인해 흥미로운 동작이 발생할 수 있습니다. 예를 들어:

무효 doSomething() ( int *ptr = new int; )

동적 메모리로 작업하는 것은 특별한 트릭을 사용하지 않는 한 많은 알고리즘에서 병목 현상이 발생하는 경우가 많습니다.

이 기사에서는 그러한 몇 가지 기술을 살펴보겠습니다. 기사의 예제는 new 및 delete 연산자가 오버로드된다는 점에서 다릅니다(예: from ). 이로 인해 구문 구성이 최소화되고 프로그램 수정이 간단합니다. 그 과정에서 발견된 함정도 설명되어 있습니다(물론 처음부터 끝까지 표준을 읽는 전문가들은 놀라지 않을 것입니다).

0. 메모리를 사용하여 수동 작업이 필요합니까?

우선 할당자가 얼마나 스마트하게 메모리 작업 속도를 높일 수 있는지 확인해 봅시다.

C++ 및 C#에 대한 간단한 테스트를 작성해 보겠습니다(C#은 객체를 세대로 나누고 크기가 다른 객체에 대해 서로 다른 풀을 사용하는 뛰어난 메모리 관리자로 유명합니다).

클래스 노드( public: Node* next; ); // ... for (int i = 0; i< 10000000; i++) { Node* v = new Node(); }

Class Node ( public Node next; ) // ... for (int l = 0; l< 10000000; l++) { var v = new Node(); }

예제의 모든 "구형 진공"에도 불구하고 시간 차이는 10배로 나타났습니다(62ms 대 650ms). 추가로 C# 예제는 끝났고 C++의 매너 규칙에 따라 선택한 객체를 삭제해야 하므로 간격이 더 벌어집니다(최대 2580ms).

1. 개체 풀

확실한 해결책은 OS에서 큰 메모리 블록을 가져와 sizeof(노드) 크기의 동일한 블록으로 나누고, 메모리가 할당될 때 풀에서 블록을 가져오고, 해제되면 풀로 반환하는 것입니다. 풀을 구성하는 가장 쉬운 방법은 단일 연결 목록(스택)을 사용하는 것입니다.

프로그램을 가능한 한 적게 변경하는 것이 목표이므로 Node 클래스에 BlockAlloc 믹스인을 추가하기만 하면 됩니다.
클래스 노드: 공개 BlockAlloc

우선 OS 또는 C-런타임에서 가져오는 큰 블록(페이지) 풀이 필요합니다. malloc 및 free 함수 위에 구성할 수 있지만 효율성을 높이기 위해(추상화의 추가 계층을 건너뛰기 위해) VirtualAlloc/VirtualFree를 사용합니다. 이러한 함수는 4K의 배수인 블록에 메모리를 할당하고 64K의 배수인 블록에 프로세스 주소 공간을 예약합니다. 커밋 및 예약 옵션을 동시에 지정함으로써 다른 수준의 추상화를 건너뛰고 한 번의 호출로 주소 공간을 예약하고 메모리 페이지를 할당합니다.

PagePool 클래스

인라인 size_t align(size_t x, size_t a) ( return ((x-1) | (a-1)) + 1; ) //#define align(x, a) ((((x)-1) | ( (a)-1)) + 1) 템플릿 class PagePool ( public: void* GetPage() ( void* page = VirtualAlloc(NULL, PageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(page); return page; ) ~PagePool() ( for (vector ::반복자 i = 페이지.시작(); i != 페이지.엔드(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) 비공개: 벡터 페이지; );

그런 다음 주어진 크기의 블록 풀을 구성합니다.

BlockPool 클래스

주형 클래스 BlockPool: PagePool ( public: BlockPool() : head(NULL) ( BlockSize = align(sizeof(T), Alignment); count = PageSize / BlockSize; ) void* AllocBlock() ( // todo: lock(this) if (!head) FormatNewPage(); void* tmp = head; head = *(void**)head; return tmp; ) void FreeBlock(void* tmp) ( // todo: lock(this) *(void**)tmp = head; 헤드 = tmp; ) 비공개: 무효* 헤드; size_t 블록 크기; size_t 카운트; 무효 FormatNewPage() ( 무효* tmp = GetPage(); 헤드 = tmp; for(size_t i = 0; i< count-1; i++) { void* next = (char*)tmp + BlockSize; *(void**)tmp = next; tmp = next; } *(void**)tmp = NULL; } };

논평 // todo: 잠금(이것) 스레드 간 동기화가 필요한 위치는 표시됩니다(예: EnterCriticalSection 또는 boost::mutex 사용).

페이지를 "포맷"할 때 풀에 블록을 추가하는 데 FreeBlock 추상화가 사용되지 않는 이유를 설명하겠습니다. 다음과 같이 쓰여졌다면

(size_t i = 0; i에 대해< PageSize; i += BlockSize) FreeBlock((char*)tmp+i);

그러면 FIFO 원칙에 따라 페이지가 "역순"으로 표시됩니다.

연속적으로 풀에서 요청된 여러 블록은 내림차순 주소를 갖습니다. 그리고 프로세서는 뒤로 가는 것을 좋아하지 않습니다. 이로 인해 프리페치가 중단됩니다( UPD: 최신 프로세서와 관련 없음). 루프에서 마크업을 수행하는 경우
for (size_t i = PageSize-(BlockSize-(PageSize%BlockSize)); i != 0; i -= BlockSize) FreeBlock...
그런 다음 마크업 주기는 주소로 돌아갑니다.

이제 준비가 완료되었으므로 mixin 클래스를 설명할 수 있습니다.
주형 class BlockAlloc ( public: static void* operator new(size_t s) ( if (s != sizeof(T)) ( return::operator new(s); ) return pool.AllocBlock(); ) static void 연산자 delete(void * m, size_t s) ( if (s != sizeof(T)) ( ::operator delete(m); ) else if (m != NULL) ( pool.... static void* 연산자 new(size_t, void * m) ( return m; ) // ...배치 삭제 누락에 대한 경고... static void operator delete(void*, void*) ( ) private: static BlockPool 수영장; ); 주형 블록 풀 BlockAlloc ::수영장;

수표가 필요한 이유 설명 if (s != sizeof(T))
그들은 언제 일합니까? 그런 다음 클래스가 생성/삭제되면 기본 T에서 상속됩니다.
자손은 일반적인 new/delete를 사용하지만 BlockAlloc도 혼합할 수 있습니다. 따라서 우리는 프로그램에서 무언가를 손상시킬 염려 없이 풀을 사용해야 하는 클래스를 쉽고 안전하게 결정할 수 있습니다. 다중 상속도 이 믹스인과 잘 작동합니다.

준비가 된. BlockAlloc에서 Node를 상속하고 테스트를 다시 실행합니다.
테스트 시간은 이제 120ms입니다. 5배 더 빠릅니다. 그러나 C#에서는 할당자가 여전히 더 좋습니다. 아마도 연결된 목록이 아닐 것입니다. (그러나 new 직후 즉시 delete를 호출하여 많은 메모리를 낭비하지 않고 데이터를 캐시에 맞추면 62ms가 됩니다. 이상합니다. 마치 .NET CLR과 똑같습니다. GC를 기다리지 않고 바로 해당 풀로 이동)

2. 용기 및 다채로운 내용물

후자의 수명이 부모의 수명보다 길지 않도록 다양한 자식 개체를 많이 저장하는 클래스를 얼마나 자주 접하십니까?

예를 들어 Node 및 Attribute 클래스로 채워진 XmlDocument 클래스와 노드 내부의 텍스트에서 가져온 c-문자열(char*)일 수 있습니다. 또는 디렉토리를 다시 읽을 때 한 번 로드되고 더 이상 변경되지 않는 파일 관리자의 파일 및 디렉토리 목록.

소개에서 보듯이 삭제는 새 것보다 비용이 더 많이 듭니다. 기사의 두 번째 부분의 아이디어는 부모 개체와 연결된 큰 블록의 자식 개체에 대한 메모리를 할당하는 것입니다. 부모 개체가 삭제되면 평소와 같이 자식에서 소멸자가 호출되지만 메모리는 반환할 필요가 없으며 하나의 큰 블록에서 해제됩니다.

큰 블록에서 크기가 다른 청크를 물어뜯고 이전 블록이 소진되면 새로운 큰 블록을 할당할 수 있는 PointerBumpAllocator 클래스를 만들어 봅시다.

PointerBumpAllocator 클래스

주형 class PointerBumpAllocator ( public: PointerBumpAllocator() : free(0) ( ) void* AllocBlock(size_t block) ( // todo: lock(this) block = align(block, Alignment); if (block > free) ( free = align (block, PageSize); head = GetPage(free); ) void* tmp = head; head = (char*)head + block; free -= block; return tmp; ) ~PointerBumpAllocator() ( for (vector ::반복자 i = 페이지.시작(); i != 페이지.엔드(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) private: void* GetPage(size_t size) ( void* page = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(page) ; 페이지로 돌아가기; ) 벡터 페이지; 공허*머리; size_t 무료; ); typedef PointerBumpAllocator<>기본 할당자;

마지막으로 주어진 할당자에 액세스하는 오버로드된 new 및 delete가 포함된 ChildObject 믹스인을 설명하겠습니다.

주형 struct ChildObject ( static void* operator new(size_t s, A& allocator) ( return allocator.AllocBlock(s); ) static void* operator new(size_t s, A* allocator) ( return allocator->AllocBlock(s); ) static void operator delete(void*, size_t) ( ) // *1 static void operator delete(void*, A*) ( ) static void operator delete(void*, A&) ( ) private: static void* operator new(size_t s ); );

이 경우 하위 클래스에 믹스인을 추가하는 것 외에도 new에 대한 모든 호출을 수정해야 합니다(또는 "팩토리" 패턴 사용). new 연산자의 구문은 다음과 같습니다.

New(...연산자를 위한 매개변수...) ChildObject(...생성자를 위한 매개변수...)

편의상 A& 또는 A*를 사용하는 두 개의 새 연산자를 정의했습니다.
할당자가 부모 클래스에 멤버로 추가되면 첫 번째 옵션이 더 편리합니다.
node = new(할당자) XmlNode(노드명);
할당자가 조상(불순물)으로 추가되면 두 번째 것이 더 편리합니다.
node = new(this) XmlNode(노드명);

delete 호출을 위한 특별한 구문은 없습니다. 컴파일러는 개체를 만드는 데 어떤 new 연산자가 사용되었는지에 관계없이 표준 삭제(*1로 표시됨)를 호출합니다. 즉, 삭제 구문은 정상입니다.
노드 삭제;

ChildObject(또는 그 후계자)의 생성자에서 예외가 발생하면 이 개체를 만드는 데 사용된 new 연산자의 서명과 일치하는 서명으로 delete가 호출됩니다(첫 번째 size_t 매개 변수는 void*로 대체됨).

private 섹션에 new 연산자를 배치하면 할당자를 지정하지 않고 new를 호출하는 것을 방지할 수 있습니다.

Allocator-ChildObject 쌍을 사용하는 완전한 예를 들겠습니다.

class XmlDocument: public DefaultAllocator ( public: ~XmlDocument() ( for (vector) ::반복자 i = nodes.begin(); i != nodes.end(); ++i) ( 삭제 (*i); ​​) ) void AddNode(char* 내용, char* 이름) ( char* c = (char*)AllocBlock(strlen(내용)+1); strcpy(c, 내용 ); char* n = (char*)AllocBlock(strlen(name)+1); strcpy(n, content); nodes.push_back(new(this) XmlNode(c, n)); ) class XmlNode: public ChildObject ( public: XmlNode(char* _content, char* _name) : content(_content), name(_name) ( ) private: char* content; char* name; ); 비공개:벡터 노드; );

결론. 이 글은 1.5년 전에 샌드박스용으로 썼는데, 아쉽게도 진행자가 마음에 들지 않았습니다.



질문이 있으신가요?

오타 신고

편집자에게 보낼 텍스트: