본 글을 개인적 학습을 위한 글입니다. 틀린 내용이 있을 시 마구 지적해주시면 감사합니다.
이번 글은 클래스의 심화에 대해 다루는 글입니다. 내용이 방대하다보니, 두 개의 글 정도로 나누어 알아보겠습니다.

아마 C++에서 사용하는 클래스 중 가장 많이 사용하는 클래스라고 하면 String Class일 것입니다. 문자열을 다루기에 아주 좋은 클래스이죠. 사실상 primitive와 같이 사용된다고 해도 무방할 것입니다. 먼저 이 string에 대해 간단하게나마 알아보며 글을 시작해보겠습니다.
string에는 다음과 같은 주요 기능이 있습니다.
이 외에도
클래스의 멤버 함수를 이야기하기 전에 먼저 함수 그 자체에 대해 알아볼 필요성이 있습니다. 그 중에서 인자(매개 변수) 전달에 대해 먼저 보겠습니다.
인자 전달 방식은 기본적으로 2가지 범주로 나뉘어집니다.
값에 의한 호출 (Call by Value)
주소에 의한 호출 (Call by Address)
이 두 가지 방법에서 객체가 전달되는 것을 포인트로 하여 조금 더 자세히 살펴 보자면,
→ 매개 변수 객체의 생성자가 실행되지 않는 이유
: 호출되는 순간의 실인자의 객체 상태를 매개 변수 객체에 그대로 전달하기 위함
위의 설명이 잘 이해가 되지 않는다면, 다음과 같은 코드를 확인하면 됩니다. 만일 매개 변수의 생성자가 호출된다고 가정한다면, “Oh!!!”가 총 두 번 출력될 것입니다. 하지만, 실제로 돌려본다면 한 번만 출력되는 것을 확인할 수 있습니다.
class MyClass{
public:
MyClass(){
cout << "Oh!!!" << endl;
}
};
void test(MyClass in){
cout << "Function!"<<endl;
}
int main(void){
MyClass tmp;
test(tmp);
return 0;
}
그 외에 매개변수의 특성에 대해 조금 더 살펴보겠습니다.
참조는 C++에서 도입된 강력한 기능 중 하나입니다. 아래의 설명과 함께 이해해보겠습니다.
→ 직관적으로, 포인터의 개념과 매우 유사합니다.
참조는 값만 보내는 것이 아니라 변수 자체를 보내는 것이다.
함수로 예를 들어보자면,
int & getA() { return g_a; }이 경우에, getA() = a와 같이 이용하면 g_a의 값을 바꿀 수 있게 되는 것이다. 사실 상, 포인터를 레퍼런스로 포장한 것일 뿐이다. 주의할 점은 로컬 변수를 reference로 return하지 않도록 하는 것이다.
값에 의한 호출과 주소에 의한 호출에 더해진 새로운 호출 방식입니다.
→ Reference를 함수의 인자로 사용할 때, 무조건 포인터를 가질 수 있는 변수를 넣어야 합니다.
C++에서는 다양한 형태의 생성자를 지원합니다. 이들을 잘 이용하고, 구현한다면 훨씬 좋은 형태의 클래스를 만들어 사용할 수 있을 것입니다. 가장 대표적인 복사 생성자를 살펴 보기 전에, 복사란 무엇인지에 대해서 먼저 살펴 보겠습니다.
보통 복사라 함은 크게 얕은 복사와 깊은 복사로 나뉘어 집니다. 이 둘의 차이를 확실히 알아야 복사 생성자 등을 구현할 때 깊은 이해가 생길 것입니다.
사본과 원본은 메모리를 공유하는 문제가 발생하지 않음
복사 생성자는 사용자 정의 자료형(클래스)의 융통성을 수직으로 상승시켜주는 장치입니다. 이는 객체의 복사 생성 시 호출되는 특별한 생성자입니다.
사실, 컴파일러가 자동으로 생성하는 생성자가 2개 존재한다.
C++을 공부한 사람이면 알다시피, 컴파일러가 자동으로 생성하는 생성자는 Default 생성자입니다. 이는 생성자가 존재하지 않을 때, 아무것도 하지 않는 생성자를 컴파일러가 묵시적으로 선언하는 것입니다. 하지만 컴파일러는 복사 생성자를 기본으로 생성합니다. 둘의 극명한 차이는 Default 생성자는 다른 생성자가 존재하면 컴파일러가 생성하지 않는다 즉, 존재하지 않을 수도 있다는 것입니다. 하지만, 복사 생성자는 항상 반드시 언제나 존재합니다. 이는 복사 생성자는 개발자에 의해 선언되더라도 바꿔치기 당하는 것에 불과하다는 것을 이야기합니다.
MyClass(const MyClass &in){ } // 복사 생성자의 기본적인 형태
그렇다면, 복사 생성자를 어떨 때 사용해야 하는 것일까요? 바로 Class 내에서 동적 할당이 발생할 때입니다. 이 때, 깊은 복사(Deep Copy)를 위해 반드시 복사 생성자를 선언해야 합니다. 그러나, 만일 여의치 않은 상황이거나 깊은 복사를 발생하게 하고 싶지 않다면, 오류를 방지하기 위해 복사 생성자를 private로 선언해주어 애초에 사용하는 것을 방지해야 합니다.
위에서 언급한 컴파일러가 클래스 내에 명시적 복사 생성자가 존재하지 않을 때, 묵시적으로 선언하는 기본 복사 생성자에 대한 설명입니다.
클래스에 대한 더 얘기를 하기 전에 함수에 대해 조금 더 알아볼 것이 있습니다. 함수 중복(Function Overloading)이라고 불리우는 이 문법은 C++이 더 강력한 기능과 다형성을 제공할 수 있도록 도와줍니다.
함수 중복 성공 조건 → “컴파일러가 구분할 수 있는가?”
- 중복된 함수들의 이름 동일
- 중복된 함수들의 매개 변수 타입이 다르거나 개수가 달라야 함
- 리턴 타입이 다르다고만 해서 함수 중복이 되지 않는다.
함수 중복을 왜 사용하느냐라고 하면 당연히 편리성이 가장 큰 이유가 될 것입니다. 동일한 이름의 함수들을 사용하면 함수 이름을 구분하여 기억할 필요가 없고, 함수 호출을 잘못하는 실수를 줄일 수 있습니다.
생성자 함수도 중복이 가능합니다. 객체 생성 시, 매개 변수를 통해 사용자가 정의한 대로 다양한 형태의 초깃값을 전달할 수 있습니다.
MyClass() { }
MyClass(int age) { this->age = age; }
MyClass(int age, string name) { this -> age = age; this->name = name; }
하지만 소멸자는 함수 중복이 불가합니다. 첫째로 소멸자는 매개 변수를 가지지 않습니다. 이는 컴파일러가 구분할 수 있는 최소한의 기준조차 가지지 않았음을 뜻합니다. 둘째로 C++에서는 한 클래스 내에 소멸자는 오직 하나만 존재하도록 하고 있습니다. 문법적으로도 불가하다는 이야기이죠.
디폴트 매개 변수도 함수 사용의 편의성과 다형성을 제공할 수 있는 기능입니다. 매개 변수에 값이 넘어오지 않는 경우, 디폴트 값을 받도록 선언하는 것입니다. 물론 값을 넘겨주면 디폴트 값이 아닌 인자로 들어온 값이 넘어옵니다.
디폴트 매개변수는 사용할 때 이의 제약 조건을 주의하며 사용해야 합니다. 디폴트 매개변수는 보통 매개변수 앞에 선언될 수 없다는 것입니다. 즉, 끝 쪽에 몰아서 선언해야 한다는 것이죠. 아래 간략한 코드를 보며 확인해보겠습니다.
void func1(int a = 10, int b, int c = 20) // int a로 인해 컴파일 오류
void func2(int b, int a = 10, int c = 20) // 사용 가능
또 주의할 점은 함수 선언에만 default 값이 들어가고, 구현부에는 들어가지 않는다는 것입니다. 이는 C++에만 존재하는 특징적인 기능으로 설명됩니다. 더하여 인자가 다른 같은 이름의 함수를 선언할 때, Default Parameters를 사용한다면, 굉장히 주의해야 합니다. 이는 동적 바인딩에 대한 설명과 함께 추후에 다시 알아보겠습니다.
디폴트 매개 변수를 사용할 때의 장점이라고 할 수 있는 것은 함수 중복의 간소화일 것입니다. 그렇기에 중복 함수들과 디폴트 매개 변수를 가진 함수를 함께 사용하는 것에 제한됩니다. 이유라 함은 당연히 컴파일러가 어떤 함수를 호출할 지 결정할 수 없다는 것입니다. 이러한 현상을 모호성(Ambiguity)이라고 표현하기도 합니다.
void show(int x) {
cout << "show(int x): " << x << endl;
}
void show(int x, int y = 20) { // 디폴트 매개변수 제공
cout << "show(int x, int y): " << x << ", " << y << endl;
}
show(10); // 이 때, 어떤 함수를 호출할지 컴파일러는 정할 수 없습니다.
위에서 얘기했듯이, 함수 중복이 모호하여 컴파일러가 어떤 함수를 호출하는지 판단하지 못하는 경우에 모호성이 존재한다고 합니다. 이에 대한 대표적인 케이스를 살펴보겠습니다.
class Example {
public:
void show(int x) {
cout << "show(int): " << x << endl;
}
void show(double x) {
cout << "show(double): " << x << endl;
}
};
int main() {
Example obj;
obj.show('A'); // 'A'는 char인데, int와 double로 둘 다 변환 가능 -> 모호성 발생
return 0;
}
class Example {
public:
void show(int& x) { // 비const 참조
cout << "show(int&): " << x << endl;
}
void show(const int& x) { // const 참조
cout << "show(const int&): " << x << endl;
}
};
int main() {
Example obj;
int a = 10;
const int b = 20;
obj.show(a); // 비const 참조 가능
obj.show(b); // const 참조 사용
obj.show(30); // 어떤 함수가 호출될까? (모호성 발생)
return 0;
}
오늘은 클래스의 활용 심화 일부에 대해서 다루어 보았습니다. 추가적인 내용은 다음 글에서 다루어보도록 하겠습니다.