개인적으로 공부한 C++ 내용을 공유합니다.


함수의 오버로딩


본격적인 객체 지향 프로그래밍을 시작하기에 앞서 C++에 C와는 다른 새로운 기능을 살펴보는 것으로 함수의 오버로딩에 대해 다뤄보겠습니다.

함수의 오버로딩이란 함수를 호출 했을 때 함수의 이름이 같다면 사용하는 인자를 보고 어떤 함수를 호출할지 결정하는 것입니다. C++에서는 C와 달리 함수의 이름이 같더라도 인자가 다르면 다른 함수라고 정의할 수 있습니다.

#include <iostream>

void print(int x) { std::cout << "int : " << x << std::endl; }
void print(char x) { std::cout << "char : " << x << std::endl; }
void print(double x) { std::cout << "double : " << x << std::endl; }

int main(void)
{
	int a = 1;
	char b = 'c';
	double c = 3.2f;

	print(a);
	print(b);
	print(c);
}

위에 코드에서 들어가는 인자에 따라서 다른 함수가 호출되는 것을 볼 수 있습니다. 하지만 아래과 같이 인자로 넣은 변수의 형으로 정의되어 있다면 어떻게 되는지 한번 살펴봅시다.

#include <iostream>

void print(int x) { std::cout << "int : " << x << std::endl; }
void print(double x) { std::cout << "double : " << x << std::endl; }

int main(void)
{
	int a = 1;
	double c = 3.2f;

	print(a);
	print(c);
}

위 코드에서 int 타입의 인자나 double 타입의 인자를 하나 받는 함수가 하나 밖에 없습니다. 하지만 main 에서 각기 다른 타입의 인자들 ( int , char , double )로 print 함수를 호출하게 됩니다. ac의 경우 각자 자기를 인자로 하는 정확한 함수들이 있어서 성공적으로 호출 될 수 있겠지만, char 의 경우 자기와 정확히 일치하는 인자를 가지는 함수가 없기 때문에 자신과 최대로 근접한 함수를 찾게 됩니다.

C++ 컴파일러에서 함수를 오버로딩하는 과정은 다음처럼 진행됩니다.

1 단계

자신과 타입이 정확히 일치하는 함수를 찾는다.

2 단계

정확히 일치하는 타입이 없는 경우 아래와 같은 형변환을 통해서 일치하는 함수를 찾아본다.

  • char, unsigned char, shortint 로 변환된다.
  • unsigned shortint 의 크기에 따라 int 혹은 unsigned int 로 변환된다.
  • floatdouble 로 변환된다.
  • enumint 로 변환된다.

3 단계

위와 같이 변환해도 일치하는 것이 없다면 아래의 좀 더 포괄적인 형변환을 통해 일치하는 함수를 찾는다.

  • 임의의 숫자(numeric) 타입은 다른 숫자 타입으로 변환된다. (예를 들어 float -> int )
  • enum 도 임의의 숫자 타입으로 변환된다. (예를 들어 enum -> double )
  • 0 은 포인터 타입이나 숫자 타입으로 변환된 0이면 포인터 타입이나 숫자 타입으로 변환된다.
  • 포인터는 void 포인터로 변환된다.

4 단계

유저 정의된 타입 변환으로 일치하는 것을 찾는다.

만약에 컴파일러가 위 과정을 통하더라도 일치하는 함수를 찾을 수 없거나 같은 단계에서 두 개 이상이 일치하는 경우에 모호하다(ambiguous) 라고 판단하여 오류를 발생하게 됩니다.

이렇기 때문에 위에 코드에서 print(b) 는 1 단계를 지나서 2 단계 과정에서 print(int x) 함수를 호출하게 됩니다.

다음으로 2 단계 과정을 지나 3 단계 과정에서 포괄적인 형변환을 진행할 때, 어떤 것으로 형 변환을 해야할지 모호하다면 어떻게 될지 한번 생각해봅시다.

// 모호한 오버로딩
#include <iostream>

void print(int x) { std::cout << "int : " << x << std::endl; }
void print(char x) { std::cout << "double : " << x << std::endl; }

int main() {
	int a = 1;
	char b = 'c';
	double c = 3.2f;

	print(a);
	print(b);
	print(c);

	return 0;
}

위에 코드는 double 형에 대한 print 함수가 존재하지 않습니다. 따라서 눈여겨 봐야할 곳은 print(c) 부분입니다. C++ 컴파일러가 위에 메커니즘으로 어떤 함수를 호출할 지 찾을때, double 에 대한 형이 없으므로 1 단계 과정은 지나가게됩니다. 그 다음 2 단계에서 마찬가지로 double 에 대한 내용이 없으므로 지나갑니다. 그리고 3 단계에서를 살펴보면,


클래스 뜯어보기



#include<iostream>

class Date {
  int year_;
  int month_;  // 1 부터 12 까지.
  int day_;    // 1 부터 31 까지.

 public:
  void SetDate(int year, int month, int date);
  void AddDay(int inc);
  void AddMonth(int inc);
  void AddYear(int inc);

  // 해당 월의 총 일 수를 구한다.
  int GetCurrentMonthTotalDays(int year, int month);

  void ShowDate();
};

void Date::SetDate(int year, int month, int day) {
  year_ = year;
  month_ = month;
  day_ = day;
}

int Date::GetCurrentMonthTotalDays(int year, int month) {
  static int month_day[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  if (month != 2) {
    return month_day[month - 1];
  } else if (year % 4 == 0 && year % 100 != 0) {
    return 29;  // 윤년
  } else {
    return 28;
  }
}

void Date::AddDay(int inc) {
  while (true) {
    // 현재 달의 총 일 수
    int current_month_total_days = GetCurrentMonthTotalDays(year_, month_);

    // 같은 달 안에 들어온다면;
    if (day_ + inc <= current_month_total_days) {
      day_ += inc;
      return;
    } else {
      // 다음달로 넘어가야 한다.
      inc -= (current_month_total_days - day_ + 1);
      day_ = 1;
      AddMonth(1);
    }
  }
}

void Date::AddMonth(int inc) {
  AddYear((inc + month_ - 1) / 12);
  month_ = month_ + inc % 12;
  month_ = (month_ == 12 ? 12 : month_ % 12);
}

void Date::AddYear(int inc) { year_ += inc; }

void Date::ShowDate() {
  std::cout << "오늘은 " << year_ << " 년 " << month_ << " 월 " << day_
            << " 일 입니다 " << std::endl;
}

int main() {
  Date day;
  day.SetDate(2011, 3, 1);
  day.ShowDate();

  day.AddDay(30);
  day.ShowDate();

  day.AddDay(2000);
  day.ShowDate();

  day.SetDate(2012, 1, 31);  // 윤년
  day.AddDay(29);
  day.ShowDate();

  day.SetDate(2012, 8, 4);
  day.AddDay(2500);
  day.ShowDate();
  return 0;
}

위에 코드는 간단히 만든 `Data` 클래스 입니다. 그런데  다른 형태로 되어있는걸   있습니다. `class` 내부에 함수들이 아래와 같이 선언만 정의된 코드들로만 들어있는    있고

```cpp
void SetDate(int year, int month, int date);
void AddDay(int inc);
void AddMonth(int inc);
void AddYear(int inc);

// 해당 월의 총 일 수를 구한다.
int GetCurrentMonthTotalDays(int year, int month);

void ShowDate();

함수 전체 몸통은 아래 처럼 밖에 나와있습니다.

void Date::ShowDate() {
  std::cout << "오늘은 " << year_ << " 년 " << month_ << " 월 " << day_
            << " 일 입니다 " << std::endl;
}

이걸로 보아 Data:: 를 함수 이름 앞에 붙여주게 되면 이 함수가 “Data 클래스의 정의된 함수” 라는 의미를 부여하게 되는 것을 생각해 볼 수 있습니다.

void ShowDate() { //.. }

만일 위와 같이 작성하였다면 위 함수는 클래스의 멤버 함수가 아니라 그냥 일반적인 함수가 되는 것입니다.

위에 처럼 클래스 내부에 함수를 정의하지 않고 바깥에 정의하는 이유는 클래스 내부에서 정의를 하게 되면 클래스 크기가 너무 길어져서 보기 좋지 않기 때문입니다.

  Date day;
  day.SetDate(2011, 3, 1);
  day.ShowDate();

  day.AddDay(30);
  day.ShowDate();

이제 main 함수를 살펴보면, 위 처럼 day 인스턴스를 생성해서 SetDate 로 초기화 한 다음에 ShowDate 로 내용을 한 번 보여주고, 또 AddDay 를 해서 30일을 증가 시킨뒤 다시 새로운 날짜를 출력하도록 하였습니다. 여기서 가장 중요한 부분은 처음의 SetDate 부분 입니다. 만일 이것을 하지 않는다면 초기화 되지 않은 값들에 덧셈과 출력 명령이 이어져서 쓰레기 값들이 출력되게 될 수 있습니다. 문제는 개발자의 실수로 이러한 SetDate 와 같은 함수를 사람들이 꼭 뒤에 써주지 않게 될 수도 있습니다. 물론 훌륭한 프로그래머들은 생성 후 초기화를 항상 숙지하고 있겠지만 간혹 실수로 생성한 객체를 초기화하는 과정을 빠트린다면 끔찍한 일이 벌어질 수 있게 됩니다.

다행스럽게도 C++ 에서는 이를 언어 차원에서 도와주는 장치가 있는데 바로 생성자(constructor) 입니다.


생성자(Constructor)


#include <iostream>

class Date {
  int year_;
  int month_;  // 1 부터 12 까지.
  int day_;    // 1 부터 31 까지.

 public:
  void SetDate(int year, int month, int date);
  void AddDay(int inc);
  void AddMonth(int inc);
  void AddYear(int inc);

  // 해당 월의 총 일 수를 구한다.
  int GetCurrentMonthTotalDays(int year, int month);

  void ShowDate();

  Date(int year, int month, int day) {
    year_ = year;
    month_ = month;
    day_ = day;
  }
};

// 생략

void Date::AddYear(int inc) { year_ += inc; }

void Date::ShowDate() {
  std::cout << "오늘은 " << year_ << " 년 " << month_ << " 월 " << day_
            << " 일 입니다 " << std::endl;
}
int main() {
  Date day(2011, 3, 1);
  day.ShowDate();

  day.AddYear(10);
  day.ShowDate();

  return 0;
}

위에 코드는 생성자를 사용하는 예시 코드입니다. 중요시 봐야할 부분은 바로 아래 부분입니다.

// in class
// 생성자 정의
// 객체를 초기화 하는 역할을 하기 때문에 리턴값이 없다!
/* 클래스 이름 */ (/* 인자 */) {}
Date(int year, int month, int day) {
year_ = year;
month_ = month;
day_ = day;
}

// in main
Date day(2011, 3, 1); // 암시적 방법 (implicit)
Date day = Date(2012, 3, 1); // 명시적 방법 (explicit)

생성자는 기본적으로 객체 생성시 자동으로 호출되는 함수라고 볼 수 있으며 이 때 자동으로 호출되면서 객체를 초기화 해주는 역할을 담당하게 됩니다. 생성자를 정의할때에는 클래스 이름(인자) 와 같이 정의를 합니다. 생성자는 객체를 초기화 해주는 역할을 하기 때문에 반환 형은 없습니다.

객체는 위에 코드에서 처럼 암시적 방법과 명시적 방법으로 생성자를 통해 객체를 만들 수 있습니다. 마치 함수를 호출하듯이 사용하는 것이 암시적 방법, 생성자를 호출 한다는 것을 보여주는 것이 명시적 방법입니다. 많은 경우 암시적 방법으로 축약해서 쓸 수 있으므로 이를 선호하는 편입니다.


디폴트 생성자(Default constructor)


한 가지 의문이 드는 부분이 있을 수 있습니다. 맨 처음에 단순히 SetDate 함수를 이용해서 객체를 초기화 했을때 우리는 생성자를 명시하지 않았습니다. 즉, 처음에 생성자 정의를 하지 않은 채 Date day;와 같이 객체를 선언했을 때 과연 생성자가 호출되는 지에 대한 여부에 대해 질문을 던져볼 수 있습니다. 정답은 ‘생성된다.’ 입니다. 그런데 우리가 생성자를 정의하지 않았기때문에 어떤 생성자가 호출이 되는지 알아야하는데, 이것은 바로 디폴트 생성자 입니다.

디폴트 생성자는 인자를 하나도 가지지 않는 생성자인데, 클래스에서 사용자가 어떠한 생성자도 명시적으로 정의하지 않았을 경우에 컴파일러가 자동으로 추가해주는 생성자입니다. 물론 컴파일러가 자동으로 생성할 때에는 아무런 일도 하지 않게 됩니다. 그렇기에 맨 처음에 SetDate 를 하지 않았을 때 쓰레기 값이 나오게 된다고 한 것입니다.

디폴트 생성자 또한 직접 정의할 수 있습니다.

class Date {
  int year_;
  int month_;  // 1 부터 12 까지.
  int day_;    // 1 부터 31 까지.

 public:
  void ShowDate();

  Date() {
    year_ = 2012;
    month_ = 7;
    day_ = 12;
  }
};

void Date::ShowDate() {
  std::cout << "오늘은 " << year_ << " 년 " << month_ << " 월 " << day_
            << " 일 입니다 " << std::endl;
}

int main() {
  Date day = Date();
  Date day2;

  day.ShowDate();
  day2.ShowDate();

  return 0;
}

일반 생성자와 마찬가지로 클래스 인스턴스 하나를 선언할 수 있으며 그냥 클래스 객체 인스턴스를 하나 선언할 때 처럼 사용하더라도 정의한 인자가없는 디폴트 생성자가 호출되어 잘 초기화가 진행됩니다.

Date day3();

다음과 같이 선언을 하면 문제가 생깁니다. 그 이유는 day3 객체를 디폴트 생성자를 이용하여 초기화 하는 것이 아니라, 리턴값이 Date 이고 인자가 없는 함수 day3를 정의하게 된것으로 인식하게 되기 때문입니다. 이는 암시적 표현으로 객체를 선언할 때 반드시 주의 해 두어야 할 사항입니다. 절대로 디폴트 생성자를 쓸 때에는 Date day3() 이 아닌 Date day3 처럼 사용해야 합니다.


생성자 오버로딩


앞 부분에서 함수 오버로딩에 대해서 다뤄보았는데요, 생성자 역시 함수이기 때문에 오버로딩이 적용 가능합니다. 다시말해 클래스의 객체를 여러가지 방식으로 생성할 수 있다는 것입니다.

#include <iostream>

class Date {
  int year_;
  int month_;  // 1 부터 12 까지.
  int day_;    // 1 부터 31 까지.

 public:
  void ShowDate();

  Date() {
    std::cout << "기본 생성자 호출!" << std::endl;
    year_ = 2012;
    month_ = 7;
    day_ = 12;
  }

  Date(int year, int month, int day) {
    std::cout << "인자 3 개인 생성자 호출!" << std::endl;
    year_ = year;
    month_ = month;
    day_ = day;
  }
};

void Date::ShowDate() {
  std::cout << "오늘은 " << year_ << " 년 " << month_ << " 월 " << day_
            << " 일 입니다 " << std::endl;
}
int main() {
  Date day = Date();
  Date day2(2012, 10, 31);

  day.ShowDate();
  day2.ShowDate();

  return 0;
}

위에 코드처럼 디폴트 생성자와 year, month, day를 인자로 한 생성자 함수 모두 사용이 가능합니다.

⤧  Next post Oh-My-C-P-P 04 ⤧  Previous post Oh-My-C-Lang - Section 4 - 03