메모리 구조와 동적 할당

falseman_go·2022년 5월 7일
0

R&D-Language

목록 보기
11/24

메모리 구조

메모리에는 다양한 영역이 존재한다. 실행할 코드가 저장되어 있는 코드 영역과 전역 혹은 정적 변수나 함수가 있는 데이터 영역, 지난 글에서 몇번 언급한 적이 있는 스택 영역은 지역 혹은 매개 변수가 저장되어 있고, 마지막으로 동적으로 할당한 메모리가 들어 있는 힙 영역으로 총 4가지 영역이 있다. 이 중에서 스택 영역은 주로 메인 함수에서 사용하며, 사용하던 메모리가 함수가 끝나면 정리되기 때문에 불안정하다고 한다. 하지만 함수에 매개 변수를 넘기거나 하는 용도로는 사용성이 좋다. 데이터 영역에 저장되는 전역변수나 정적 변수는 프로그램의 시작과 끝을 함께하고, 힙 영역은 C++에서 C 런타임 라이브러리(줄여서 CRT)의 힙 관리자를 통해 힙 영역을 사용하며, 사용자가 동적으로 메모리를 할당하는 곳이다.

동적 할당

지금까지 코드를 작성하면서 동적으로 메모리를 할당하여 사용했던 적은 거의 없었다.

	void* ptr = malloc(sizeof(Student));

가장 오래된 동적으로 메모리를 할당하는 방법의 첫 번째는 malloc 함수를 사용하는 것이다. 사용법은 간단한데, 할당할 메모리의 크기를 매개변수로 주면 그것을 void형 포인터로 반환해준다. 그런데 void형 포인터를 반환한다고 했을 때 좀 의아했던 것이 포인터형 변수는 주소 값을 갖고 있고 그 주소 값을 타고 들어가면 어떤 값이 있는지 알려주는 것이 앞의 데이터 타입이었다. 그런데 그 타입이 아무 것도 없는 void라고 하는 것이다.

void형 포인터와 malloc / free 함수

인터넷과 강의를 들으면서 알게 된 이 이상한 포인터는 메모리를 줄테니 알아서 형변환하여 쓰라는 의미를 가지고 있다고 한다. 그렇기 때문에 대부분의 인터넷 코드에서는 아래와 비슷한 형식으로 많이 코드를 짜는 것 같다.

// 간단히 만든 학생을 추상화한 클래스
class Student {
public :
	int _id;
	const char* _name;

	Student() {
		_id = 0;
		_name = "None";
	}
};

// 메인 함수
int main() {

	Student* student = (Student*)malloc(sizeof(Student));

	student->_id = 1;
	student->_name = "Petter";

	cout << student->_id << endl;
	cout << student->_name << endl;

	free(student);
}

따로 void형 포인터를 만들어 관리하지 않고 할당할 포인터에 형변환까지 한번에 하는 방식을 많이 찾아볼 수 있었다. 그런데 메모리를 할당하고 id값을 넣는 부분에서 초록색 밑줄이 뜨면서 NULL 포인터를 역참조하고 있다는 경고가 뜨는데 코드를 실행해볼 때는 문제가 없다. 경고가 뜨는 이유는 malloc 함수가 메모리를 할당하는데 실패할 경우 NULL을 반환하게 되는데 이런 상황이 일어날 수 있다는 것을 감안하여 밑줄을 쳐주는 것을 알게 되었다.

동적으로 메모리를 할당해주는 함수

종류기능
malloc매개 변수로 준 값 만큼 메모리를 할당
calloc기본적으로 동작은 malloc와 같지만 할당과 동시에 값을 0으로 초기화
realloc위의 두개의 함수로 할당받은 메모리의 사이즈를 변환

realloc 같은 경우는 메모리를 할당받은 포인터와 재할당 받을 메모리의 크기를 매개 변수로 줘야 한다. 또 프로그램이 끝나기 직전 혹은 사용하지 않게 될 경우에는 free 함수를 사용하여 할당받은 메모리를 해제 시켜줘야 한다. 위의 코드에서는 메인 함수의 마지막에 해제시켜 주었다.

연산자 new / delete

int main() {

	Student* student = new Student;

	student->_id = 1;
	student->_name = "Petter";

	cout << student->_id << endl;
	cout << student->_name << endl;

	delete student;
}

new와 delete는 함수가 아닌 연산자이다. 위의 코드는 앞서 작성한 코드에서 나온 경고문구가 뜨지 않으며 사용하는 법도 훨씬 간편하다. C#에서도 new 키워드는 많이 써봤지만 따로 delete를 한적은 없는데, 아마 가비지 컬렉터가 사용하지 않게 된 객체의 메모리를 찾아 해제해서 그런 것 같다. malloc / free 함수 처럼 new와 delete가 세트로 쓰인다. 둘 중 무엇을 사용할지는 다음과 같은 경우로 나뉜다고 생각한다.

malloc와 free 함수는 경우에 따라 타입에 상관없이 특정한 크기의 메모리 영역을 할당받을 수 있다. 반면에 new와 delete 연산자는 타입이 존재해야한다. 하지만 new와 delete는 사용과 편의성면에서 좋고 객체의 생성자와 소멸자를 호출해준다.

#include <iostream>

using namespace std;

class Student {
public :
	int _id;
	const char* _name;

	Student() {
		_id = 0;
		_name = "None";

		cout << "Student 생성자 호출" << endl;
	}

	~Student() { cout << "Student 소멸자 호출" << endl; }
};

int main() {

	Student* student = new Student;

	student->_id = 1;
	student->_name = "Petter";

	cout << student->_id << endl;
	cout << student->_name << endl;

	delete student;

	Student* student2 = (Student*)malloc(sizeof(Student));

	student2->_id = 2;
	student2->_name = "Parker";

	cout << student2->_id << endl;
	cout << student2->_name << endl;

	free(student2);
}

클래스에 기본 생성자와 소멸자를 만들고 각각의 방법으로 동적할당을 해보니, 앞의 new와 delete로 할당받을 때 생성자와 소멸자가 호출되는 것을 확인할 수 있었지만, malloc와 free로 할당받을 때는 호출되지 않은 것을 볼 수 있었다.

Tip new / delete를 배열처럼 쓰는 방법

int main() {

	Student* student = new Student[2];

	student->_id = 1;
	student->_name = "Petter";

	(student+1)->_id = 2;
	(student + 1)->_name = "Mils";

	cout << student->_id << endl;
	cout << student->_name << endl;

	cout << (student + 1)->_id << endl;
	cout << (student + 1)->_name << endl;

	delete[] student;
}
profile
정리하는 블로그

0개의 댓글