지난 포스팅에 이어서 C++의 개체 지향 프로그래밍 내용을 정리해 보고자 한다.
복사 생성자
복사 생성자는 나와 같은 클래스에 있는 개체를 매개변수로 받는 생성자이다. 아래처럼 선언하고 구현할 수 있다.
// Vector.h
class Vector
{
public:
Vector(const Vector& other);
private:
int mX;
int mY;
};
// Vector.cpp
Vector::Vector(const Vector& other)
: mX(other.mX)
, my(other.mY)
{
}
같은 클래스에 속한 다른 개체를 이용하여 새로운 개체를 초기화한다. 코드에 기본 생성자가 없을 경우 컴파일러가 자동으로 기본 생성자를 만들어 주는 특성이 있다고 했었는데, 복사 생성자도 마찬가지이다. 코드에 복사 생성자가 없는 경우, 컴파일러가 암시적 복사 생성자를 자동으로 생성한다. 암시적 복사 생성자는 얕은 복사를 수행하며 각 멤버의 값을 복사한다.
만약 클래스에 포인터형 변수가 있으면 어떻게 될까? 다음과 같은 예제가 있다고 가정해 보자.
// ClassRecord.h
class ClassRecord
{
public:
ClassRecord(const int* scores, int count);
~ClassRecord();
private:
int mCount;
int* mScores;
};
// ClassRecord.cpp
ClassRecord::ClassRecord(const int* scores, int count)
: mCount(count)
{
mScores = new int[mCount];
memcpy(mScores, scores, mCount * sizeof(int));
}
ClassRecord::~ClassRecord()
{
delete[] mScores;
}
// 암시적 복사 생성자
ClassRecord::ClassRecord(const ClassRecord& other)
: mCount(other.mCount)
, mScores(other.mScores)
{
}
// Main.cpp
ClassRecord classRecord(scores, 5);
ClassRecord* classRecordCopy = new ClassRecord(classRecord);
delete classRecordCopy
이 코드가 실행되면 다음과 같이 메모리 구조가 구성된다. 먼저 Main.cpp 에서 line 1에 의해 스택에 mCount = 5, mScores = 2048(int 5개 배열의 주소)이 할당이 된다. 그리고 line 2를 통해 복사 생성자를 통한 새로운 클래스를 만든다. 그러면 힙에 [5, 2048]의 값이 저장되면서 그 값의 주소를 스택에서 1024로 저장한다. 이 때 2048은 int 5개 배열의 주소이므로 보라색 힙 영역 메모리도 이 배열을 바라보게 된다. 그리고 line 3이 실행되면 보라색 데이터가 날라가고 소멸자가 호출되면서 mScores를 지운다. 반면 스택의 2048이 계속 가리키고 있기 때문에 문제가 생길 수 있다.
이를 해결하기 위해 직접 복사 생성자를 만들어서 깊은 복사(deep copy)를 하는 것을 권장한다. 이로 인해 포인터 변수가 가리키는 실제 데이터까지 복사할 수 있다. 아래와 같이 만들 수 있다.
ClassRecord::ClassRecord(const ClassRecord& other)
: mCount(other.mCount)
{
mScores = new int[mCount];
memcpy(mScores, other.mScores, mCount * sizeof(int));
}
이렇게 되면 새로 classRecordCopy를 만들더라도 힙에서 기존의 int 배열을 포인터로 가리키는 것이 아닌, 새로운 배열을 하나 만들어서 값을 저장하므로 소멸자로 지워주어도 원본 데이터는 안전하게 보호할 수 있게 된다.
함수 오버로딩(Overloading)
함수 오버로딩은 정확히는 OOP 개념은 아니다. 오버로딩은 같은 이름을 가지면서 매개변수의 갯수와 타입이 다른 경우를 의미한다. 반환형의 경우 달라도 상관이 없다.
void Print(int score); // OK
void Print(const char* name); // OK
void Print(float gpa, const char* name); // OK
int Print(int score); // Compile Error
int Print(float gpa); // OK
컴파일러는 오버로딩 매칭을 통해 어떤 함수를 호출해야 하는지를 판단한다. 매칭되는 함수를 찾을 수 없거나, 매칭되는 함수를 여러개 찾을 경우 컴파일 에러가 발생한다. 오직 적합한 함수를 하나 찾은 경우만 정상적으로 실행된다.
연산자 오버로딩
연산자는 함수처럼 작동하는 부호이다. C++에서는 프로그래머가 연산자를 오버로딩 할 수가 있다.
// Vector.h
class Vector
{
public:
Vector operator+(const Vector& rhs) const;
private:
int mX;
int mY;
};
// main.cpp
Vector v1(10, 20);
Vector v2(3, 17);
Vector sum = v1 + v2;
// Vector.cpp
Vector Vector::operator+(const Vector& rhs) const
{
Vector sum;
sum.mX = mX + rhs.mX;
sum.mY = mY + rhs.mY;
return sum;
}
연산자 오버로딩에서 const를 사용하는 이유는 멤버 변수의 값이 바뀌는 것을 방지하기 위해서이다. 따라서 최대한 많은 곳에 const를 붙이는 것을 권장하며 지역 변수까지도 const를 붙이는 걸 코딩 표준으로 삼는 경우가 많다. 또한 const &를 사용하는 이유는 불필요한 개체의 사본이 생기는 것을 방지하기 위함이며, 멤버 변수가 바뀌는 것도 방지하기 위해서이다.
friend 키워드
friend 키워드는 클래스 정의 안에서 사용할 수 있다. 이를 가지고 다른 클래스나 함수가 나의 private 또는 protected 멤버에 접근할 수 있게 허용한다. 클래스 뿐만 아니라 함수에도 friend 키워드를 줄 수가 있다.
// X.h
class X
{
friend class Y;
private:
int mPrivateInt;
};
// Y.h
#include "x.h"
class Y
{
public:
void Foo(X& x);
};
// Y.cpp
void Y::Foo(X& x)
{
// 만약 friend class Y가 없으면 컴파일 에러
x.mPrivateInt += 10 // OK
}
friend 함수는 멤버 함수가 아니다. 하지만 다른 클래스의 private 멤버에 접근할 수 있다. 멤버 아닌 연산자 오버로딩을 작성하는 법은 아래와 같다.
// header
friend <return-type> operator<operator-symbol>(<argument-list>);
// cpp
<return-type> operator<operator-symbol>(<argument-list>)
{
}
// example
friend void operator<<(std::ostream& os, const Vector& rhs);
friend Vector operator*(int scalar, const Vector& lhs);
참고자료
- C++ 언매니지드 프로그래밍, 포큐 아카데미
'Prog. Langs & Tools > C++' 카테고리의 다른 글
[C++] Ch07. 캐스팅(Casting) (0) | 2020.11.16 |
---|---|
[C++] Ch06. OOP 3 (0) | 2020.10.19 |
[C++] Ch04. OOP 1 (0) | 2020.09.22 |
[C++] Ch03. 파일 입출력(I/O) (0) | 2020.08.26 |
[C++] Ch02. 참조, 문자열 (0) | 2020.08.11 |