티스토리 뷰

C++이 자바와 가장 크게 달라지는 부분이 객체의 생성과 사용에서 객체 변수를 이용할 때이다. 자바는 모든 클래스 타입의 변수는 반드시 참조를 가진다. 또한 참조되는 객체는 항상 힙 메모리에 존재한다. 이것은 스택 영역의 관리와 메모리 가비지컬렉션을 쉽게 해주지만 대신 객체를 사용할 때 항상 간접접근(참조를 한번 읽어오고 해당 객체를 또 읽어와야 되는 이중 로드)해야 하는 문제가 생긴다. 이것은 상당한 성능 상의 부담을 가져온다.

C++은 성능을 최적화하기 위해 (또 C의 구조체의 개념을 그대로 가져왔기 때문이기도 하다) 언제든 객체 변수를 선언할 수 있다. 예를 들어 Student 클래스에 대해 다음의 지역변수 선언은 객체를 스택 영역에 하나 생성하고 st라는 이름으로 그 객체를 나타낸다. (이것은 자바의 참조와 달리 이름이 그대로 메모리 영역이고 값이다. 기본 타입의 경우와 같다고 볼 수 있다.) 이 객체는 함수 호출 시 지역변수 영역에 생성되고 함수가 반환될 때 자동해지된다.

Student st;

심지어는 객체배열도 선언할 수 있다. 다음 코드는 스택 영역에 학생 객체 20개를 생성하고 그것을 배열로 관리하게 해 준다. 이 객체배열은 함수가 끝나면 자동 소멸한다. 모든 배열에 대해 C++은 스택 영역에 지역변수 배열을 생성할 수 있다. 함수내에서 사용할 배열을 선언했다가 함수가 종료하면 자동해지된다. 전역 또는 지역변수로 선언된 객체나 배열은 선언 즉시 생성되고 별도의 new가 필요하지 않으며 따로 명시적인 해지(delete)하지 않아도 자동으로 해지되므로 메모리 관리가 편리할 뿐 아니라 메모리 사용의 효율성을 높일 수 있다.

Student stList[20];

객체변수의 선언을 통해 객체가 생성될 때마다 생성자가 불려진다. 즉 위의 코드 한 줄만으로 우리는 Student() 생성자 함수를 20번 호출한 게 된다. 또한 범위가 끝나면 20개의 객체에 대해 20번의 소멸자가 불려진다. (자동해지)

다음 코드는 생성자와 소멸자를 가지는 student 클래스를 정의하고 그것을 이용하여 객체 변수, 지역 객체 배열과 동적할당 객체 배열 등 다양한 경우를 테스트하는 코드다. test1()에서 test4()는 이러한 경우에 대한 사용예와 함께 그 경우에 생성자와 소멸자가 어떻게 불려지는지를 보여준다.

#include <iostream>
#include <string>
using namespace std;

class student {
public:
	string name;
	int grade;
	student() {
		cout << "학생 객체 생성자 호출" << endl;
	}
	~student() {
		cout << "학생 객체 소멸자 호출" << endl;
	}
};
void print(student& st)
{
	cout << st.name << ' ' << st.grade << endl << endl;
}
void test1();
void test2();
void test3();
void test4();

void main() {
	test1();
	test2();
	test3();
	test4();
	system("PAUSE");
}
void test1()
{
	cout << "==> test1" << endl;
	student st;
	cout << "test1: ";
	cin >> st.name >> st.grade;
	print(st);
}
void test2()
{
	cout << "==> test2" << endl;
	student st[3];
	cout << "test2: ";
	cin >> st[0].name >> st[0].grade;
	print(st[0]);
}
void test3()
{
	cout << "==> test3" << endl;
	student *st[3];
	st[0] = new student();
	cout << "test3: ";
	cin >> st[0]->name >> st[0]->grade;
	print(*st[0]);
	delete st[0];
}
void test4()
{
	cout << "==> test4" << endl;
	student *st = new student[3];
	cout << "test4: ";
	cin >> st[0].name >> st[0].grade;
	print(st[0]);
}

이 코드에서는 다음과 같은 객체의 생성과 소멸을 확인할 수 있다.

먼저 test1에서는 하나의 객체가 생성되고 소멸자도 한번 호출된다.

test2에서는 생성자 3번 호출되고 입력과 출력 이후 소멸자가 세번 불려진 것을 볼 수 있다. 배열에 객체가 3개 생성되고 함수가 끝나면서 범위가 종료되어 소멸자가 자동으로 세번 불려진 것이다.

test3에서는 포인터 배열을 만드는데, 이 때는 생성자가 불려지지 않는다. 대신 new를 통해 객체를 생성할 때 생성자가 한번 불려졌다. 또한 명시적으로 delete를 할 때 소멸자가 한번 불려진 것을 확인할 수 있다.

마지막으로 test4에서는 new student[3]에서 힙에 객체 세 개짜리 배열이 할당되는데, 이 때 생성자가 세번 호출되고 함수가 종료해도 소멸자가 불려지지 않는다. 힙 객체는 해지되지 않은 채 프로그램이 종료하면 힙 메모리는 모두 회수될 것이다.

위 코드는 또한 print 함수에 student 객체가 전달되는 것도 보여준다. 이 함수는 매개변수를 참조로 받아(student&) 출력하는데, 호출부에서 보낸 실매개변수는 객체 변수거나 포인터 변수에 *를 붙인 것이 전달됨을 볼 수 있다.

프로그램 안에서 객체를 나타내는 변수를 사용하기 위해서는 객체 변수의 지정이나 매개변수 전달이 필요하다. 객체 매개변수는 포인터로 전달, 참조로 전달도 가능하지만 값으로 전달도 가능하다. 포인터로 전달은 객체의 주소를 가져가고 참조로 전달은 참조를 가져가므로 호출자가 보낸 동일한 객체를 함수에서도 가리키게 된다. 그러나 값으로 전달의 경우는 그 함수의 스택 영역에 역시 student 객체가 생성된다. 이 객체에 호출자가 보낸 객체가 복사되어 전달된다. 이때 디폴트 복사생성자가 불려진다.

void test5(student st1, student* st_ptr, student& st_ref) {
   st1.grade++;
   st_ptr->grade++;
   st_ref.grade++;
}
void main() {
	student st[3];
	cin >> st[0].name >> st[0].grade;
	st[1] = st[2] = st[0];
	test5(st[0], &st[1], st[2]);
	print(st[0]);
	print(st[1]);
	print(st[2]);
}

위 코드에서 st[1] = st[0]과 같이 쓸 수 있는 것은 c++에서 객체 변수의 지정을 허용하기 때문이다. 이때 일어나는 일은 메모리 복제(memcpy)라고 보면 된다. 이것도 디폴트 지정연산으로 정의되는 것이라고 볼 수 있다. student& operator=(const student&);와 같은 프로토타입으로 선언되어 있다. 그 결과 세 개의 객체는 모두 입력받은 kim과 21을 가지게 된다.

이것을 test5 함수를 호출하면서 첫번째는 값으로 객체 전달, 두번째는 포인터로 주소 전달, 세번째는 참조형으로 참조 전달로 매개변수를 세 개 전달한다. 첫번째와 같이 값으로 전달된 매개변수는 함수 스택 영역에 또하나의 학생 객체가 생성되고 호출자가 보낸 객체의 값을 복사해서 가지게 된다. 즉 student st1 = stList[0]; 와 같은 지정문의 효과가 난다. 이 새로운 객체 st1는 함수가 종료하면 자동해지된다. 여기서 생성자 호출이 출력되지 않는 이유는 st1의 생성이 우리가 만들어준 기본 생성자를 쓰는 것이 아니라 디폴트 복사생성자 student(const student&);를 호출하기 때문이다.

test5의 호출 후 결과는 st[0]는 바뀌지 않는다. 왜냐하면 함수에서 자체로 생성한 student 객체의 나이를 바꾸었기 때문이다. 이것은 값으로 전달이므로 호출한 쪽의 객체의 나이는 바뀌지 않는다. 그러나 포인터와 참조로 전달된 객체는 호출부의 객체의 나이가 증가되었음을 볼 수 있다.

객체로 선언된 변수는 자동으로 생성자와 소멸자가 불려져서 메모리 관리가 편할 뿐 아니라 스택 영역에 바로 메모리가 잡히므로 캐시미스도 줄어들고 포인터로 간접접근하는 추가적인 로드도 발생하지 않아 더 빠르다. C++은 이처럼 지역변수 영역의 완전한 메모리 해지를 가능하게 해 줄 뿐 아니라 힙 영역에 할당된 객체도 delete를 통해 완전히 해지할 수 있다. 이것은 C++이 자바로 작성된 소프트웨어에 비해 메모리 사용이 놀라울 정도로 작은 이유다. 

예를 들어 윈도우즈 환경에서 파워포인트 프로그램이 돌면서(종료하지 않고) 1000개의 파일을 열었다 닫는 것을 반복할 때 메모리 누출이 있다면 파워포인트 프로그램이 차지하는 메모리가 점점 증가해서 언젠가는 메모리를 다 잠식하게 된다. 마이크로소프트에서 만든 프로그램들은 이러한 것을 무한히 반복해도 이론적으로 메모리가 한 바이트도 누출되지 않게 메모리를 완전히 청소하게끔 짜져있다. 이것은 쓰레기를 엄청나게 만들어내는 자바 프로그램과 비교할 때 어마어마한 강점이 된다. (자바 프로그램이라면 점점 느려지다가 언젠가 프로그램이 잠시 먹통이 되면서 가비지 콜렉터가 동작하겠지만 그래도 남는 쓰레기가 있고 그렇게 몇번 반복하다가 메모리 부족으로 VM이 종료될 수도 있다. 실제로 몇년전까지도 이클립스에서 그렇게 blue screen이 나타나는 경우가 종종 있었다.)

객체 변수를 통해 객체가 그 자리에 생성되고 범위가 끝나면 자동해지되는 장점이 있으나 스택에 너무 많은 메모리를 사용할 수 없고 그렇다고 전역변수에 큰 메모리를 할당하기도 어렵다. 그러므로 적절한 시점에 new를 통해 생성하고 필요하지 않으면 delete를 통해 해지하는 방식으로 힙 객체를 사용해야 되는 경우가 많이 있다. 또한 객체를 전달할 때 새로운 객체를 만들어 복제를 하는 것이 아니라 원래 객체를 가져가서 그것으로 함수의 동작을 해야 되는 경우도 있다. 이 경우는 부득이 포인터나 참조를 이용해야 하고 이런 경우 메모리 참조와 관리의 방법을 잘 알고 있어야 한다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함