자료구조를 공부하기 위해서는 클래스에 대한 이해가 선행되어 있어야 한다. 그래서 한동안은 클래스에 대한 글이 될 것 같다. 바로 본론으로 들어가자.
[1] 메모리 유출(Memory Leak)
C++에서는 새로운 메모리 공간이 필요할때마다 new를 통해 자원을 요청할 수 있다. 이를 '동적 할당'이라고 한다. 동적 할당은 스택 메모리가 아닌 힙 메모리를 사용하는 특징이 있다. 여기서 힙 메모리는 자유 저장소(free store)라고 부르기도 한다. 연산자 new는 주어진 타입의 객체를 생성하는데 필요한 메모리를 자유 저장소로부터 동적으로 할당하여 이 객체에 대한 포인터를 반환한다.
그런데 동적으로 할당된 객체들은 할당 후 삭제하지 않으면 문제가 발생할 수 있다. 만약에 새로운 객체 p를 동적으로 할당한 뒤 삭제하지 않은 상태에서 주소값을 바꿔버렸다고 가정해보자. p의 주소는 원래 4번지였는데 6번지로 변경한 것이다. 그러면 4번지에 접근할 수 있는 방법이 없다. 4번지는 삭제되지 않았으므로 자유 저장소에 포함되지 않는다. 따라서 4번지 주소는 프로그램이 끝날때까지 사용되지 못한다.
이렇게 동적 메모리에 접근할 수 없는 객체가 있는 것을 메모리 유출이라고 한다. 메모리 유출이 발생하면 실제 메모리가 충분한데도 메모리 부족으로 실행이 멈추는 상황이 발생할 수 있다. 그래서 C++에서는 아래와 같은 규칙을 꼭 지켜야 한다.
💡 객체가 new로 할당되었다면, 그 객체는 반드시 delete문으로 삭제되어야 한다.
※ 배열은 delete [] 가 사용되는 것에 주의하자
메모리 유출은 클래스를 사용하면서 발생하기 쉽다. 어떤 상황에서 발생할 수 있는지 알아보자.
1. 클래스에서 메모리를 할당한 경우
클래스에는 객체가 소멸될 때 자동적으로 호출되는 멤버 함수인 '소멸자(destructor)'가 있다. 소멸자가 반드시 필요하지는 않지만, 클래스에서 메모리를 할당한 경우는 얘기가 다르다. 메모리를 할당한 변수들은 소멸자에서 반드시 할당 해제를 해줘야 한다. 아래 예시를 보자.
class Vect{
private:
int* data;
int size;
public:
Vect(int n = 10){ // default 10
size = n;
data = new int[n];
}
~Vect(){
delete [] data;
}
// .. 생략
};
소멸자에서 data 배열의 할당을 해제하지 않는다면 어떻게 될까? Vect 객체를 삭제하는 순간 data 배열은 접근할 수 없는 메모리가 된다. 따라서 n 크기만큼의 메모리 낭비가 발생한다.
2. 얕은 복사 (shallow copy)
// Vect 클래스의 정의는 위 코드 참고
Vect a(100);
Vect b = a;
Vect c; // 기본 크기는 10
c = a;
위 예시는 크기가 100인 별도의 배열을 세 개 생성한 것 같이 보인다. 과연 그럴까? 아니다. 실제로는 세 벡터가 크기 100인 하나의 배열을 공유하고 있다. 이유가 뭘까? 복사 생성자가 생성되지 않은 경우 시스템은 디폴트 방법을 사용하는데, 이는 단순히 a의 각 멤버를 b에 복사하는 방식이다. 즉, a를 b에 복사할 때 아래와 같이 동작한다.
// 복사 생성자가 생성되지 않은 경우 'Vect b = a' 의 동작방식
b.data = a.data
data는 포인터이기 때문에, 결론적으로 a,b의 포인터는 같은 곳을 향하게 된다. a, b는 독립적인 배열을 가지고 있는게 아니라 하나의 배열을 공유하게 되는 것이다. 이와 같은 복사 방법을 '얕은 복사'라고 한다.
이제 c를 살펴보자. c같은 경우 먼저 초기화 된 뒤에 a를 복사하기 때문에 메모리 누수가 발생한다. 이게 무슨 뜻이냐 하면, c는 크기가 10인 배열을 가지고 있는데 a를 복사하는 과정에서 얕은 복사로 인해 크기가 10인 c의 원래 배열에 대한 포인터를 잃게 된다는 의미다.
이제 a, b, c 모두 자유 저장소의 같은 배열을 가리키는 멤버를 가지고 있다. 만약 셋 중 하나가 배열의 내용을 변경하면 다른 두 개의 배열도 그대로 반영된다. 만약 a 객체가 소멸자에 의해 삭제된다면, 다른 두 객체는 멤버를 잃게 된다.
따라서 만약 클래스가 메모리를 할당하면, 복사를 위해 새로운 메모리를 할당하는 복사 생성자와 배정 연산자를 반드시 제공해야 한다. 새로운 메모리를 할당하는 복사 방법을 '깊은 복사'라고 한다. 이러면 객체를 복사해도 독립적인 메모리 공간을 부여받기 때문에 위와 같은 문제가 발생하지 않는다. 복사 생성자와 배정 연산자는 아래와 같다. new를 통해 새로운 배열을 할당한 것에 주목하자!
// 1. 복사 생성자: Vect b(a) 면 a를 b에 복사한다는 뜻
Vect::Vect(const Vect& a){
size = a.size; // 크기 복사
data = new int[size]; // 새로운 배열 할당
for(int i = 0; i < size; i++){ // 내용 복사
data[i] = a.data[i];
}
}
// 2. 배정 연산자: Vect b = a 면 a를 b에 복사한다는 뜻
Vect& Vect::operator=(const Vect& a){
if(this != &a){ // a = a 인 경우는 피한다
delete [] data; // 옛 배열 할당 해제
size = a.size; // 크기 복사
data = new int[size]; // 새로운 배열 할당
for(int i = 0; i < size; i++){ // 내용 복사
data[i] = a.data[i];
}
}
return *this;
}
여기까지 메모리 누출이 무엇이지, 언제 발생하는지, 해결 방법은 무엇인지 알아봤다. new로 새로운 메모리 공간을 할당받을 때만 조심하면 된다. 다음으로 프랜드 클래스를 살펴보자.
[2] Friend 함수 & Friend 클래스
프랜드는 아주 간단하게만 짚고 넘어가자. 크게 함수로 선언되는 경우와 클래스로 선언되는 경우가 있다. 둘 다 '선언된 클래스의 private 및 protected 멤버에 접근할 수 있음'을 의미한다.
// 1. friend 함수
class F1 {
private :
string name;
public:
friend void set_name(F1&, string);
// set_name은 private 변수인 name에 접근 가능
};
void set_name(F1& f, string s) {
f.name = s; // private 변수에 바로 접근
cout << f.name << "\n";
}
먼저 함수로 선언된 경우를 살펴보자. 주석 설명 그대로다. 원래 F1에서 name은 private 변수로 외부에서 접근 불가능하다. 하지만 set_name 함수에 friend를 선언해줘서 set_name 함수는 name에 접근 가능하다.
// 2. friend 클래스
class F1 {
private :
string name;
public:
friend class F2;
// F2는 F1의 name에 접근 가능
};
class F2{
public :
void set_name(F1& f, string s) {
f.name = s;
}
void show_name(F1& f) {
cout << f.name << "\n";
}
};
클래스로 정의된 경우다. 프랜드 함수와 별반 다를게 없다. F1 클래스에서 F2를 친구로 설정했으므로 F2는 F1의 모든 멤버에 접근 권한이 생겼다. F2의 멤버 함수에서 F1의 name에 직접적으로 접근한 것을 볼 수 있다.
여기까지 friend에 대해 간단하게 다뤄봤다. 마지막으로 중복된 헤더를 피하는 방법에 대해 살펴보자.
[3] 중복된 헤더 피하기
일반적인 C++ 프로그램은 수많은 헤더 파일들을 가지며 이들은 서로의 헤더 파일을 포함하기도 한다. 따라서 어떤 프로그램은 같은 헤더파일이 여러 번 포함될 수 있다. 그러나 헤더 파일이 중복되면 중복된 정의로 인해 컴파일 에러를 초래할 수도 있다. 따라서 헤더 파일들은 중복된 포함을 피하기 위해 '#ifndef', '#define', '#endif'와 같은 전처리기 명령어를 사용한다.
// CreditCard.h
#ifndef CREDIT_CARD_H
#define CREDIT_CARD_H
/*
... 코드 내용
*/
#endif
#define 변수이름
전처리기 변수 정의. 변수 이름은 보통 헤더 파일의 이름을 이용하고 관례적으로 모두 대문자를 사용한다고 한다.
#ifndef 변수이름
'if not defined' 를 의미한다. 즉, 위 코드에서 전처리기 변수 CREDIT_CARD_H가 정의되지 않을 경우에만 헤더파일을 포함하라는 의미다. #endif 까지가 한 블록이다.
원리는 간단하다. 위 헤더 파일을 처음 만난 경우에만 'CREDIT_CARD_H'라는 전처리기 변수를 정의하고, 이후에 이 파일을 포함하려고 할 때는 이미 'CREDIT_CARD_H'가 정의되어 있기 때문에 더 이상 포함하지 않는다.
'CS > 자료구조' 카테고리의 다른 글
[ 자료구조 ] 연결 리스트 : 이중 연결 리스트 (0) | 2022.03.04 |
---|---|
[ 자료구조 ] 연결 리스트(Linked List) : 단일 연결 리스트 (0) | 2022.03.04 |
[ 자료구조 ] 템플릿, 예외처리 (0) | 2022.03.03 |
[ 자료구조 ] 상속, 보호 타입, 다형성, 추상클래스 (0) | 2022.03.03 |
[ 자료구조 ] 자료구조 포스팅을 시작하며 (0) | 2022.03.01 |