[c++] - class의 정적 선언과 동적 선언
사실 학부생 1학년때 배운 C를 제외하고는 나머지 언어는 독학하였다. C다음에 배웠던 언어는 C++로, 이미 알고 있던 C를 이용해서 객체지향 언어를 배우고자 하는 취지에서 였다.
객체지향을 C++로 시작했기 때문에 그 다음에 접했던 객체지향인 자바와 C#에서 좀 헷갈리는 부분이 있었는데
그 부분이 class의 정적선언과 동적 선언이다.
거두절미하고 정적선언과 동적선언은 다음과 같다.
A a(); // 정적(stack)
A* a = new A(); // 동적(heap)
첫 번째 방식이 정적선언으로 객체 a는 stack 메모리가 할당된다. 두 번째 방식이 동적 선언으로 heap 메모리를 할당하는 방식이다.
문제는 자바나 C#은 기본적으로 클래스는 동적으로 할당이 되고, 포인터를 사용하지 않기때문에 C++만 알고 있던 나의 관점에서 보았을 때 그 차이를 알지 못하였다. 각각 stack과 heap에 할당이 되기 때문에 확실하게 알고 있지 못하면 memory leak이 발생할 수도 있으므로 주의해야 함에도 불구하고 말이다.
더군다나 자바나 C#은 GC(garbage collector)가 있기 때문에 내가 직접 delete를 할 필요도 없어 본인이 직접 연구를 해보지 않는 이상 절대로 이게 정적할당인지 동적할당인지 알 수가 없었다.
애초에 이 class에 대한 정적할당은 C++에만 존재하며 다른 객체지향 언어에는 없는 개념이다. 기본적으로 자바나 C#은 클래스에 대해서는 동적으로 할당이 되고 이는 함수의 인자로 클래스를 넘길 때 알 수 있다. C++의 경우는 함수에서 레퍼런스(&) 키워드를 붙여서 이것을 복사 생성자를 이용해 복사하는 게 아니라 이 클래스 자체를 넘기겠다는 것을 명시 해주어야 한다. 그렇지 않으면 정적으로 선언된 클래스는 인자로 넘기면 자동으로 복사된다.
#include <iostream>
using namespace std;
class A {
public:
A() {
cout << "call constructor" << endl;
}
A(const A &a) {
cout << "call copyconstructor" << endl;
}
int a;
int* ptr = new int; // 동적 할당
};
A classReturnTest(A & _a) {
return _a;
}
int main() {
A a;
a.a = 1; // set 1
*a.ptr = 1;
A _a = a;
_a.a = 2; // set 2
*_a.ptr = 2;
cout << a.a << "/" << _a.a << endl;
cout << *a.ptr << "/" << *_a.ptr << endl;
cout << &a << "/" << &_a << endl;
return 0;
}
실험을 위해 간단한 코드를 짜봤다. 단순히 a라는 객체를 정적 선언하고 _a라는 객체에 직접 대입하였다.
call constructor
call copyconstructor
1/2
1/2
a의 주소 / _a의 주소
결과는 위와 같다. a를 _a에 직접 대입하는 순간 자동으로 복사생성자가 호출되는 것을 알 수 있다. 따라서 a와 _a는 서로 완전히 다른 객체로 이는 마지막줄의 서로의 주소값이 다른 것으로 확실히 알 수 있다. 그렇다면 코드에 선언된 classReturntest라는 함수를 사용해 보자.
#include <iostream>
using namespace std;
class A {
public:
A() {
cout << "call constructor" << endl;
}
A(const A &a) {
cout << "call copyconstructor" << endl;
}
int a;
int* ptr = new int; // 동적 할당
};
A classReturnTest(A & _a) {
return _a;
}
int main() {
A a;
a.a = 1; // set 1
*a.ptr = 1;
A _a = classReturnTest(a);
_a.a = 2; // set 2
*_a.ptr = 2;
cout << a.a << "/" << _a.a << endl;
cout << *a.ptr << "/" << *_a.ptr << endl;
cout << &a << "/" << &_a << endl;
return 0;
}
classReturnTest라는 함수를 통해 레퍼런스로 클래스를 받아서 반환하는 방식으로 하면 되지 않을까 생각했다. 실행해보면 위의 결과와 동일한것을 알 수 있다. 이는 결국 반환된 값을 다시 '=' 연산자를 통해 직접 대입하고 있으므로 당연한 결과다.
그렇다면 어떻게 해야 자바나 C#처럼 사용할 수 있는가는 아래의 코드와 같이 동적할당을 사용하면 된다.
#include <iostream>
using namespace std;
class A {
public:
A() {
cout << "call constructor" << endl;
}
A(const A &a) {
cout << "call copyconstructor" << endl;
}
int a;
int* ptr = new int; // 동적 할당
};
int main() {
A* a = new A;
a->a = 1; // set 1
*a->ptr = 1;
A* _a = a;
_a->a = 2; // set 2
*_a->ptr = 2;
cout << a->a << "/" << _a->a << endl;
cout << *a->ptr << "/" << *_a->ptr << endl;
cout << a << "/" << _a << endl;
return 0;
}
call constructor
2/2
2/2
동일한 주소
포인터를 사용해야 한다는 점에서 다른 객체지향 언어과 괴리감이 있지만, 이런식으로 동적할당을 통해 class를 선언해서 heap 메모리에 클래스 객체를 올려서 사용해야 한다. 다만 C++는 다른 언어와 달리 GC가 없으므로 동적할당을 했다면
delete 키워드를 통해 heap에서 해제 해줘야한다. 이 부분은 shared_ptr등의 class를 사용하면 해결되는 문제이긴하다.
이 글을 쓴 이유는 결국 정적선언과 동적선언 때문이다. 꼭 기억해야하는 것은 기본적으로 포인터 없이 선언하는 클래스는 C++에서 정적선언이라서 stack에 할당된다. 반면에 자바나 C#에서는 기본적으로 heap할당이 된다는 것이다. 포인터의 개념이 없는 언어들도 내부적으로는 포인터 연산을 통해 동적으로 할당을 하기 때문에 이런 부분은 필히 기억해야 메모리 누수를 방지할 수 있다고 생각한다.