1. 객체지향 프로그래밍 - [객체란?, 클래스]
2. 함수 오버로딩, 생성자 - [함수 오버로딩(Overloading), (예시)Date 클래스, 생성자(Constructor), 생성자 오버로딩]
1. 객체지향 프로그래밍
- 절차지향(Procedure): 프로시져(함수)를 지향한다는 것.
→ 프로그램 설계 시, 중요한 부분을 하나의 함수로 만들어 쪼개 처리한다는 것이다. 하지만, 프로그램의 크기가 거대해지면서 새로운 패러다임인 객체지향언어(Object oriented language)가 등장하게 되었다.
객체란?
객체란? 변수들과 참고 자료들로 이루어진 소프트웨어 덩어리
객체지향이란? 절차지향의 한계를 극복하고, 캡슐화·상속·다형성 등의 개념을 활용하여 코드의 재사용성과 유지보수성을 높이는 프로그래밍 패러다임
ex) Animal
→ Animal 타입 구조체가 있고, 해당 타입의 animal 변수를 만들었다고 해보자. 그 후, animal(여러 동물 중 하나)을 인자로 갖는 Play라는 함수가 있다고 생각해보자. 이는 Play가 Animal을 하는 꼴이다. 실제로는 Animal이 Play를 하는 꼴이 맞음에도 불구하고 말이다.
Animal animal;
// 여러가지 초기화 (생략)
animal.play(); // 즉 내가 (animal 이) Play 를 한다!
animal.sleep(); // 내가 sleep 을 한다!
→ 위 코드와 같은 형태로 하면, play 함수에 animal을 인자로 주지 않아도 된다. 내가 play를 하는 것으로, 내 정보는 이미 play 함수가 다 알고 있는 꼴이 되기 때문이다.
즉, animal은 자신의 상태를 알려주는 변수(variable), 자신이 하는 행동들을 수행하는 함수(method)로 이루어졌다고 볼 수 있다.
- 추상화
객체가 현실세계에서 존재하는 것들을 나타내기 위해서는 추상화(abstraction) 과정이 필요하다.
현실 세계 처리를 컴퓨터 세계의 처리로 적절하게 바꾸는 것인데, 예를 들어 휴대폰의 '전화를 한다', '문자를 보낸다' 와 같은 것들은 '핸드폰이 하는 것' 이므로 함수로 추상화시킬 수 있고,핸드폰의 상태를 나타내는 것들, 예를 들어서 자기 자신의 전화 번호나 배터리 잔량 같은 것은변수로 추상화시킬 수 있다.
객체는 자신만의 정보를 나타내는 변수, 그리고 그 겉의 함수들로 둘러 쌓여 있다. 이 때, 객체에 (정의되어)있는 변수나 함수를 인스턴스 변수(instance variable), 인스턴스 메서드(instance method)라 부른다.
인스턴스 메서드가 인스턴스 변수를 둘러싸고(보호하고) 있는 것으로 보이는 이유는 정말 보호되고 있기 때문이다. 외부에서 객체 인스턴스 변수 값을 변경하지 못하고, 객체의 인스턴스 함수를 통해서만 가능하다는 것이다(일반적).
ex) 인스턴스 변수 값 변경
Animal animal;
// 초기화 과정 생략
animal.food += 100; // 불가능
animal.increase_food(100); // 가능
→ 이와 같이 외부에서 직접 인스턴스 변수의 값을 바꿀 수 없고, 인스턴스 메서드를 통해 간접적으로 조절할 수 있는 것을 캡슐화(Encapsulation)라고 부른다.
+) 캡슐화를 왜 쓸까? 객체가 내부적으로 어떻게 작동하는지 몰라도 사용할 수 있게 되기 때문이다. animal.increase_food(100);을 하면 내부적으로 food 변수 값이 100 증가하는 것 뿐만이 아니라, 몸무게, 행복도 등 여러 작업이 일어나도록 설정할 수 있다.
하지만, 사용하지 않았다면? animal.food += 100; animal.weight += 10; //... 이런 식으로 여러 처리를 직접 프로그래머가 해야 한다. 프로그래머가 직접 정확하게 값을 조절하기 위해서는 내부 설계를 정확하게 알고 있어야 할 것이다. 반대로 인스턴스 메서드를 이용하면 “food를 늘리려면 increase_food를 늘리시면 됩니다.” 정도만 전달하면 객체 내부적으로 알아서 처리되어 내부를 굳이 이해하지 않아도 되게 된다.
ex) 키보드 ‘a’
- 키보드 ‘a’를 눌렀을 때, 캡슐화 되어 있지 않다면? → “키보드 스위치가 눌렸는지 확인, 눌린 키가 a인지 확인, 키보드 신호 cpu로 전달, cpu가 신호 해석해 아스키코드로 변환, 그래픽 드라이버가 …”
- 캡슐화 되어 있다면? → 키보드 ‘a’를 누른다. → 화면에 ‘a’가 표시된다.
클래스
객체를 C++ 상에서 만들 수 있는 장치(설계도)를 클래스라고 한다. C++ 안에서 클래스를 이용해 만들어진 객체를 인스턴스라고 한다.
ex) Animal Class
#include <iostream>
class Animal {
private:
int food;
int weight;
public:
void SetAnimal(int _food, int _weight) {
food = _food;
weight = _weight;
}
void IncreaseFood(int inc) {
food += inc;
weight += (inc / 3);
}
void ViewStat() {
std::cout << "이 동물의 food : " << food << std::endl;
std::cout << "이 동물의 weight : " << weight << std::endl;
}
}; // 세미콜론..
int main() {
Animal animal;
animal.SetAnimal(100, 50);
animal.IncreaseFood(30);
animal.ViewStat();
return 0;
}
→ 기존에는 구조체의 경우 앞에 struct를 명시했어야 한다. 하지만, 위 코드에서는 Animal(type)만 써주면 된다. Animal animal; 을 통해 Animal 클래스 인스턴스 animal을 생성한 것이다.
+) 여기서, 클래스 상에서 보이는 food, weight, set_animal, increase_food, view_stat 들은 멤버 변수, 멤버 함수라고 부른다. 그리고 인스턴스로 생성된 객체에서는 인스턴스 변수, 인스턴스 함수라고 부른다. → 즉, 그냥 클래스 상에서는 멤버 변수, 멤버 함수라고 부르고 / 인스턴스로 생성된 객체에서는 인스턴스 변수, 인스턴스 함수라고 부른다. 이 때, 멤버 변수와 함수는 실재하는 것이 아니다. 설계도 상에 있다고 아파트가 실제 존재하는 것이 아니다.
- 접근 지시자(private …)
→ 외부에서 멤버들에 접근 가능 여부를 표시하는 것이다. private 키워드는 아래 쓰여진 내용이 객체 내에서 보호되고 있다는 것이다. 모든 것들이 본인 객체 안에서만 접근할 수 있고, 객체 외부에서는 접근할 수 없게 된다.
// 객체 안에서 food와 weight에 접근하는 것은 가능하다.
void SetAnimal(int _food, int _weight) {
food = _food;
weight = _weight;
}
// 객체 외부에서 인위적으로 food에 접근하는 것은 불가능하다.
int main() {
Animal animal;
animal.food = 100;
}
→ public 키워드를 사용한 멤버 함수들을 통해서 외부에서 마음껏 이용할 수 있다.
animal.SetAnimal(100, 50);
animal.IncreaseFood(30);
animal.ViewStat();
위 코드를 모두 private으로 설정해버린다면, 해당 객체는 외부에서 접근을 할 수 없는 객체가 되고, 이는 아무 쓸모 없는 덩어리로 남게 된다.
+) 키워드 명시를 하지 않으면 기본적으로 private으로 설정된다.
위 코드의 멤버 변수들을 public으로 공개하면, 구조체를 사용했던 것처럼 animal.food = 100; 이런 식으로 접근이 가능해지게 된다.
문제 풀어보기) Date Class 디자인하기
- SetDate는 Date 함수 내부를 초기화한다.
- AddDay, AddMonth, AddYear은 일(0~31), 월(0~12), 년을 더한다(입력 값 제한).
cf.) 2012년 2월 28일에 3일을 더하면 2월 31일이 아닌 3월 2일이 된다.
#include <iostream>
class Date {
private:
int year_;
int month_; // 0 부터 12 까지.
int day_; // 0 부터 31 까지.
public:
void SetDate(int year, int month, int day) {
year_ = year;
month_ = month;
day_ = day;
};
// [4로 나누어떨어지고, 100으로 나누어떨어지지 않거나]
// [400으로 나누어 떨어져야] `윤년`이다.
bool IsLeapYear(int year)
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
// 월에 대한 최대 일 수를 반환하도록 한다.
int GetDaysInMonth(int year, int month) {
const int daysInMonth[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if ((month == 2) && IsLeapYear(year)) {
return 29;
}
return daysInMonth[month - 1];
}
//year, month, day를 고려해서 값이 변경되도록 파악한다.
void AddDay(int inc) {
while (inc > 0) {
int daysInThisMonth = GetDaysInMonth(year_, month_);
// 이번 달에서 남은 일 수
int remain = daysInThisMonth - day_;
// 이번 달의 최대 일수로 소화 가능하면 그대로 더하고 종료
if (inc <= remain) {
day_ += inc;
inc = 0;
}
else {
// 남은 일수를 초과하면, 이 달을 끝까지 채우고 다음 달로 넘어간다
day_ = 1;
inc -= (remain + 1);
AddMonth(1);
}
}
}
void AddMonth(int inc) {
int totalMonths = month_ + inc;
if (totalMonths > 12) {
month_ = totalMonths - 12;
AddYear(1);
}
else
{
month_ = totalMonths;
}
}
void AddYear(int inc) {
year_ += inc;
}
void ShowDate() {
std::cout << "현재 날짜: " << year_ << "년 " << month_ << "월 " << day_ << "일\\n";
}
};
int main(void) {
Date today;
today.SetDate(2019, 1, 31);
today.ShowDate();
today.AddDay(30);
today.ShowDate();
today.AddMonth(10);
today.ShowDate();
today.AddYear(5);
today.ShowDate();
return 0;
}
2. 함수 오버로딩, 생성자
함수 오버로딩(Overloading)
printf, scanf는 C 라이브러리에 각각 단 1개만 존재한다. 하지만, C++에서는 같은 이름을 가진 함수가 여러 개 존재해도 된다. 함수 이름에 과부화(Overload)가 걸려도 괜찮다는 것이다. 이 때, 구분은 사용하는 인자를 통해 진행된다.
ex) print 함수 생성
/* 함수의 오버로딩 */
#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() {
int a = 1;
char b = 'c';
double c = 3.2f;
print(a);
print(b);
print(c);
return 0;
}
→ C언어였을 경우, 타입에 따른 함수 이름을 다르게 만들어 호출했어야 하지만, C++에서는 컴파일러가 알아서 적합한 인자를 갖는 함수를 찾아 호출한다.
+) 정확히 일치하는 인자를 갖는 함수가 없다면, 자신과 최대로 근접한 함수를 찾게 된다.
- 오버로딩 과정(규칙): 어느 정도는 숙지해두는 것이 좋다.
→ 1 단계
자신과 타입이 정확히 일치하는 함수를 찾는다.
→ 2 단계
정확히 일치하는 타입이 없는 경우 아래와 같은 형변환을 통해서 일치하는 함수를 찾아본다.
- Char, unsigned char, short 는 int 로 변환된다.
- Unsigned short 는 int 의 크기에 따라 int 혹은 unsigned int 로 변환된다.
- Float 은 double 로 변환된다.
- Enum 은 int 로 변환된다.
→ 3 단계
위와 같이 변환해도 일치하는 것이 없다면 아래의 좀더 포괄적인 형변환을 통해 일치하는 함수를 찾는다.
- 임의의 숫자(numeric) 타입은 다른 숫자 타입으로 변환된다. (예를 들어 float -> int )
- Enum 도 임의의 숫자 타입으로 변환된다 (예를 들어 Enum -> double)
- 0 은 포인터 타입이나 숫자 타입으로 변환된 0 은 포인터 타입이나 숫자 타입으로 변환된다
- 포인터는 void 포인터로 변환된다.
→ 4 단계
- 유저 정의된 타입 변환으로 일치하는 것을 찾는다
- 만약에 컴파일러가 위 과정을 통하더라도 일치하는 함수를 찾을 수 없거나 같은 단계에서 두 개 이상이 일치하는 경우에 모호하다 (ambiguous) 라고 판단해서 오류가 발생하게 된다.
ex) 오버로딩 오류
// 모호한 오버로딩
#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;
}
→ print(int x), print(char x) 밖에 없어서, double type의 c를 인자 갖도록 해서 함수를 호출한다고 생각해보자. 1단계에서 걸러지고, 2단계에서는 double의 캐스팅에 대한 내용이 없어 일치하는 것이 없다. 3단계에서는 double은 int로도, char로도 변환이 가능해서, 모호한 호출 오류(Ambiguous Call Error)가 발생한다. 즉, 같은 단계에 두 개 이상의 가능한 일치가 존재해 오류가 발생하게 된다.
Date 클래스
→ 입력 제한 없는 Date 클래스를 만들어보자.
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;
}
→ 함수 정의만 클래스 내부에 존재하고, 함수의 몸통은 밖에 나와있다. Date::를 함수 이름 앞에 붙이면 해당 함수가 ``Date 클래스에 정의된 함수라는 의미를 부여받게 된다. 보통, 간단한 함수를 제외하고는, 대부분 클래스 밖에 함수를 작성한다. 클래스 내부에 쓸 경우 너무 길어져 가독성이 떨어지기 때문이다.
+) 예외적으로, 템플릿 클래스의 경우 모두 클래스 내부에 작성한다.
생성자(Constructor)
생성한 객체를 초기화 하는 과정을 초기화 하는 과정이 중요한데, C++에서는 이를 언어 차원에서 생성자(constructor)를 통해 도와주는 장치로 두었다. 기본적으로 객체 생성 시 자동으로 호출되는 함수 라고 볼 수 있다.
ex) 생성자 사용
#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;
}
→ SetDate() 없이 초기화된 상태를 알 수 있다.
- 암시/명시적 방법: 함수 호출하듯 암시적으로 축약해 사용하는 방식이 선호된다.
Date day(2011, 3, 1); // 암시적 방법 (implicit)
Date day = Date(2012, 3, 1); // 명시적 방법 (explicit)
- 디폴트 생성자(Default Constructor)
맨 처음 SetDate 함수를 이용해 객체를 초기화 했을 때, 생성자를 명시하지 않았는데, 이 때도 생성자가 호출되었던 것일까? 맞다.
생성자를 정의하지도 않았는데 호출된 생성자는 디폴트 생성자이다. 이는 인자를 하나도 가지지 않는 생성자인데, 클래스에서 어떤 생성자도 명시적으로 정의하지 않았을 때, 컴파일러가 자동으로 추가해주는 생성자이다. 자동으로 생성될 때에는 아무런 일도 하지 않는다. 그렇게 쓰레기 값이 나오게 되기도 한다.
+) 사용자가 다른 생성자를 추가한 순간 컴파일러는 자동으로 디폴트 생성자를 삽입하지 않는다.
ex) 디폴트 생성자 직접 정의해보기
class Date {
int year_;
int month_; // 1 부터 12 까지.
int day_; // 1 부터 31 까지.
public:
void ShowDate();
Date() {
year_ = 2012;
month_ = 7;
day_ = 12;
}
};
아래와 같이 디폴트 생성자를 이용해 day 와 day2 를 추가할 수 있게 된다.
Date day = Date();
Date day2;
하지만, 아래와 같이는 안된다. 디폴트 생성자를 이용해 초기화하는 것이 아니라 리턴 값이 Date 이고 인자가 없는 함수 day3를 정의하게 된 것으로 인식하게 된다.
Date day3();
절대로 인자가 없는 생성자를 호출하기 위해서 A a() 처럼 하면 안된다. 해당 문장은 A 를 리턴하는 함수 a 를 정의한 문장이다. 반드시 그냥 A a 와 같이 써야한다.
- 디폴트 생성자 명시하기
C++ 11 이전에는 디폴트 생성자를 사용하고 싶을 경우, 그냥 생성자를 정의하지 않았어야 했다고 한다. 이 때문에, 코드를 읽는 사용자 입장에서 개발자가 생성자 생성을 깜빡 잊은 건지, 디폴트 생성자를 이용하고자 작성하지 않은 것인지를 알 수 없었다.
C++ 11 부터는 명시적으로 디폴트 생성자를 사용하도록 명시할 수 있게 되었다.
class Test {
public:
Test() = default;
};
→ 생성자의 선언 바로 뒤에 = default 를 붙여 Test의 디폴트 생성자를 정의하라고 컴파일러에게 명시적으로 알려줄 수 있다.
생성자 오버로딩
생성자도 함수이기 때문에, 오버로딩이 적용될 수 있다. 즉, 해당 클래스의 객체를 여러 방식으로 생성할 수 있다는 것이다.
ex) 생성자 오버로딩
#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;
}
'공부 > C++' 카테고리의 다른 글
[C++] 초기화 리스트, static 변수/함수, this, 상수(const) 함수, 복사 생략 (0) | 2025.03.24 |
---|---|
[C++] 복사생성자(Copy Constructor), 소멸자(Destructor) (1) | 2025.03.21 |
[C++] Static, new/delete, Naming Rules (0) | 2025.03.16 |