오늘은 C++의 개체지향 프로그래밍 부분을 공부하고 정리한 내용을 포스팅 해 보려고 한다. 양이 많아서 두 번의 포스팅에 나누어서 적어보려고 한다.
개체지향 프로그래밍 개념은 C++에만 있는 건 아니다. Java에도 있고 다른 많은 언어에도 있다. Java로 예를 들면, 다음과 같은 개념들은 자바와 C++ 모두 있는 것들이다.
- 클래스
- 개체
- 생성자
- 함수 오버로딩
- 힙에 개체 생성하기 등
하지만 자바에는 없고 C++에만 있는 개념들도 있다. 예를 들면
- 스택에 개체 생성하기
- 복사 생성자
- 소멸자
- 연산자 오버로딩 등
C++은 OOP와 OOP가 아닌 것들을 섞어서 쓸 수 있다는 장점도 가지고 있다. 자바는 OOP에 관해서 엄격한 편이지만, C++은 C의 후방호환성을 가지고 있어서 유연하다는 장점이 있다.
OOP의 핵심 개념은 사람들이 세상을 바라보는 방식이다. OOP는 재사용성을 기본적인 원칙으로 하는데, 만약 이 개념을 무너트리려고 하면 그 순간부터 문제가 생기기 시작한다.
C++의 경우 기본 접근 권한은 private이다. 그리고 다음과 같은 접근 제어자(Access Modifier)를 사용한다.
- public : 누구나 접근 가능
- protected : 자식 클래스에서 접근 가능
- private : 해당 클래스에서 접근 가능
보통 아래와 같이 C++ 멤버들을 그룹 짓는 방식이 일반적이다.
class SomeClass
{
public:
int PublicMember;
protected:
int mProtectedMember;
private:
int mPrivateMember1;
int mPrivateMember2;
}
벡터라는 클래스가 있다고 가정하고 이 클래스를 통해 개체를 생성하는 코드는 아래와 같다. 스택 메모리에 만드는 방법과 힙 메모리에 만드는 방법이 있다. 참고로 자바는 힙 메모리에 개체를 만드는 것만 가능하다.
// Vector 클래스
class Vector
{
int mX; // private 멤버 변수
int mY; // private 멤버 변수
}
// 스택(stack) 메모리에 만듦 (빠름)
Vector a;
// 힙 메모리에 만듦 (느림)
Vector* b = new Vector();
스택은 예약된 로컬 메모리 공간으로, 크기가 일반적으로 1MB 이하로 작은 편이다. 함수 호출과 반환이 이 메모리에서 일어난다. 스택 포인터를 위아래로 바꿔 가면서 새로운 값을 입력받아 저장할 수 있기 때문에, 메모리를 할당 및 해제할 필요가 없다. 스택에 할당된 메모리는 범위를 벗어나면 사라진다. 그리고 변수와 매개변수를 위해 필요한 크기는 컴파일 도중에 알 수 있다. 만약 스택에 큰 개체를 넣으면 스택오버플로우(stack overflow)가 발생할 수 있으며, 성능에도 영향을 미친다.
힙은 전역 메모리 공간이다. 크기가 GB 단위로 큰 편이다. 큰 메모리가 비어 있고 연속된 메모리 블록을 찾아야 한다. 만약 5MB가 필요하면 한 번에 5MB가 비어있는 곳을 찾아야 하며, 2MB, 3MB 이렇게 나누어서 합쳐서 쓸 수는 없다. 프로그래머가 메모리를 직접 할당 및 해제해야 하며, 그러지 못할 경우 메모리 누수가 발생한다.
스택 예제를 한 번 살펴보자
Vector AddVector(const Vector& a, const Vector& b)
{
Vector result;
result.mX = a.mX + b.mY;
result.mY = a.mY + b.mY;
return result;
}
void Foo()
{
Vector c = AddVector(a, b);
}
이번에는 힙 예제이다. PrintVector() 함수를 통해 힙에 Vectors 메모리가 할당이 되는데, delete를 통해 함수가 스택에서 사라지기 전에 힙에서 메모리 해제를 해 주어야 메모리 누수가 발생하지 않음에 명심하자.
Vector PrintVectors(const Vector& a, const Vector& b)
{
Vector* result = new Vector;
result.mX = a.mX + b.mY;
result.mY = a.mY + b.mY;
// ... 출력
delete result;
}
void Foo()
{
PrintVectors(a, b);
}
C++ 에서 개체 배열을 만드는 방법은 다음과 같이 두 가지가 있다. 힙에 개체 메모리 공간을 할당해 주는 것과, 포인터 공간을 할당해 주는 차이가 있다.
// 10개의 백터 개체를 힙에 만듦 (JAVA는 이걸 할 수 없음)
Vector* list = new Vector[10];
// 10개의 포인터를 힙에 만듦
Vector** list = new Vector*[10];
개체의 메모리를 힙에 할당하고 나서는 반드시 delete를 통해 메모리 해제를 해 주어야 한다.
생성자(constructor) 함수는 new를 가지고 개체를 처음 생성할 때 호출되는 함수를 의미한다. 초기화 리스트를 사용해서 구현한 Vector 클래스는 다음과 같이 나타낼 수 있다. 그냥 값을 대입할 수도 있지만, 그 경우는 오브젝트 초기화가 이루어지고 나서 이후에 값을 대입하는 것으로 약간의 차이가 있다.
// 초기화 리스트
class Vector
{
public:
Vector()
: mX(0)
, mY(0)
{
}
private:
int mX;
int mY;
};
// 대입
class Vector
{
public:
Vector()
{
mX = 0;
mY = 0;
}
private:
int mX;
int mY;
};
클래스를 헤더 파일과 cpp 파일로 분리해서 작성해 보면 다음과 같이 작성해 볼 수 있다. 헤더 파일에서는 클래스를 정의하고, cpp 파일에서는 초기화 리스트를 통해 클래스를 구현하는 역할을 분리해서 한다고 볼 수 있다.
// vector.h
class Vector // 클래스 정의
{
pulbic:
Vector();
Vector(int x, int y);
private:
int mX;
int mY;
};
// vector.cpp
Vector::Vector() // 클래스 구현
: mX(0)
, mY(0)
{
}
Vector::Vector(int x, int y)
: mX(x)
, mY(y)
{
}
생성자는 기본 생성자와 매개변수를 받는 생성자로 나누어 볼 수 있다. 만약 클래스에 생성자가 없으면 컴파일러는 기본 생성자를 자동적으로 만들어준다. 만약 클래스에서 생성자가 있다면 기본 생성자를 만들어 주지 않는다. 그리고 생성자는 멤버 변수를 초기화하지 않다는 점을 주의하자.
생성자 오버로딩(Overloading)은 같은 이름을 가진 여러 개의 생성자를 만드는 것을 의미한다. 받는 파라미터의 갯수와 자료형이 달라야 한다. 자바에도 있는 익숙한 개념이다.
소멸자(Destructor)는 new로 만든 개체가 지워질 때 호출된다. 소멸자를 통해 개체에 메모리를 할당한 이후 지워줄 때 메모리를 같이 해제함으로써 메모리 관리를 할 수 있다. 자바는 가비지를 수집하기 때문에 소멸자가 필요가 없으나 C++ 클래스는 그 안에서 동적 메모리 할당이 이루어질 수 있기 때문에 소멸자를 통해 메모리를 해제해 주어야 한다.
// vector.h
class Vector
{
public:
~Vector();
private:
int mX;
int mY;
};
// vector.cpp
Vector::~Vector()
{
}
클래스 안에 const 키워드가 붙어 있는 경우가 있다. 이 키워드는 값을 바꿀 수 없는 경우를 의미한다. 변수인 경우 그 값을 상수로 고정시키며, const 멤버 함수인 경우 멤버 변수가 변하는 것을 방지한다. 기본적으로 멤버 함수는 const로 짜는 것이 바람직하며, 그 안에서 값을 바꿔야 하는 경우 const를 빼서 처리해주는 방법을 권장한다.
int GetX() const
{
return mX; // OK
}
void AddConst(const Vector& other) const
{
mX = mX + other.mX; // 컴파일 에러
mY = mY + other.mY; // 컴파일 에러
}
이번에는 구조체와 클래스의 차이를 알아본다. C++에서는 구조체를 클래스처럼 쓸 수 있다. 일반적으로 클래스는 구조체의 상위 개념이다. C++에서 구조체와 클래스는 거의 같은 개념으로 볼 수 있다. 차이점이 있다면 기본 접근권한이 구조체는 public이고, 클래스는 private이다.
C++에서는 구조체를 클래스처럼 쓸 수 있다. 하지만, 그러지 말고 구조체는 C 스타일로 쓰는 것을 권장한다. 구조체는 순수하게 데이터뿐이어야 한다(Plain Old Data). 사용자가 선언한 생성자, 소멸자가 없어야 하고 public을 사용하며 가상함수가 없고 메모리 카피가 가능해야 한다.
참고자료
- C++ 언매니지드 프로그래밍, 포큐 아카데미
'Prog. Langs & Tools > C++' 카테고리의 다른 글
[C++] Ch06. OOP 3 (0) | 2020.10.19 |
---|---|
[C++] Ch05. OOP 2 (0) | 2020.10.01 |
[C++] Ch03. 파일 입출력(I/O) (0) | 2020.08.26 |
[C++] Ch02. 참조, 문자열 (0) | 2020.08.11 |
[C++] Ch01. 입출력(I/O) (0) | 2020.08.03 |