우리는 C에서 함수 포인터를 아마 배웠을 거에요. 한번 복습해 보자면
int add(int a, int b)
{
return a + b;
}
위와 같은 add 함수가 있을 때
int (*func)(int, int) = add;
func와 같은 함수 포인터를 만들 수 있습니다. 앞의 int는 반환 형, 괄호 안의 int, int는 인자를 나타내겠죠. 함수를 이렇게 변수처럼 저장할(가리킬) 수 있는 것은 함수명은 함수의 시작 지점에 대한 정보를 가지고 있기 때문입니다.
우리는 클래스를 배웠으니 클래스의 멤버 함수도 포인터로 만들어 볼 수 있을 것 입니다.
class something{
public:
void func(int x, int y) {
return x * y;
}
};
당연한 말이지만 클래스의 멤버 함수기 때문에 void (*func)(int, int)
와 같은 꼴로는 선언할 수 없습니다. 함수 포인터를 정의해 줄 때에는 함수의 원형과 같도록 선언해야 하는데 어떤 클래스의 멤버 함수인지에 해당하는 정보가 빠졌기 때문입니다. 제대로 작성하려면
void (something::*func)(int, int)
와 같이 선언해야 합니다.
함수 객체는 객체를 마치 함수처럼 호출해 사용하는 것인데, C++에서는 다양한 연산자들을 오버라이딩 해서 사요할 수 있습니다. 그 중에 () 연산자도 있어서, ()연산자에 오버라이딩을 사용하면 마치 함수처럼 사용할 수 있습니다.
class Func {
public:
int add(int x) {
sum += x;
}
int operator()(int a) {
return sum * a;
}
private:
int sum = 0;
};
...
Func func;
func.add(10);
func.add(20);
int x = func(3);
...
당연하게도 x는 90이 되겠죠.
함수 객체는 여러 장점이 있습니다. 함수 형태가 같아도 객체 타입이 다르면 다른 타입으로 인식하고, 주소를 전달하는 방식의 함수 포인터는 인라인이 될 수 없지만 함수 객체는 인라인이 될 수 있고 컴파일러가 쉽게 최적화 가능합니다. 실제로 STL의 많은 구현은 이 함수 객체를 통해서 이루어 집니다.
이제 이 함수 객체도 귀찮다고 생각한 프로그래머들은 람다를 만들어 냅니다.(물론 꼭 그런 이유 때문은 아니지만) 람다는 함수 객체보다 훨씬 간편합니다. 람다에 대해서는 저번 주에 간단하게 설명했었는데 많이 써봐야 익숙해질 거에요.
오늘은 캡슐화와 정보은닉, 상속에 대해서 조금 배워봅시다.
클래스는 공개되어야 할 것과 숨겨야 할 것을 나눠서 최소한의 정보만 공개하고 불필요한 side effect를 최소화할 수 있습니다. 우리가 사용했던 public 키워드와 private, protected 키워드로 멤버 변수, 함수의 접근을 제한할 수 있습니다. 아래의 클래스를 봅시다.
class Student {
public:
int id;
private:
int grade;
};
...
Student a;
cout << a.grade << endl; // << Error
Student클래스 외부에서 private으로 선언된 grade 변수에 접근할 수 없습니다. private으로 선언된 변수와 함수는 클래스 내부에서만 사용할 수 있고 외부에서는 접근이 불가능 하므로 들어낼 필요가 없거나 불필요한 변경을 막기 위해 사용할 수 있습니다.
캡슐화는 일단 함수와 변수를 묶는 것을 말합니다. 아래의 클래스를 보도록 합시다.
class Student {
private:
int grade;
public:
void setGrade(int value) {
if (0 <= value && value <= 99)
grade = value;
}
int getGrade() {
return grade;
}
};
grade는 private변수로 선언해 외부에서 바꿀 수 없지만 public으로 선언된 두 함수 setGrade getGrade로 접근하거나 값을 바꿀 수 있습니다. 특히 grade는 setGrade를 통해서 만 수정이 가능하니 여러 예외를 값을 변경하는 순간에 처리하고 줄일 수 있습니다.
C#같은 좀 더 최신의 언어에서는 getter, setter를 사용해 이러한 캡슐화를 좀 더 편하게 사용하고 있습니다.
상속을 사용하면 기존의 클래스를 토대로 비슷한 클래스를 정의할 수 있습니다. 코드의 재 사용을 직접 느낄 수 있는 가장 좋은 방법이죠. 아래의 두 클래스, 사람과 사람을 상속받은 학생 클래스를 봅시다.
class Person {
private:
string name;
int age;
public:
string getName() { return name; }
int getAge() { return age; }
};
class Student : public Person {
private:
int id;
string major;
public:
string getMajor() { return major; }
int getId() { return id; }
};
Student는 Person의 public을 상속받기 때문에(class Student : public Person을 잘 보세요.) getName, getAge같은 매서드들을 호출할 수 있습니다.
Student a;
cout << a.getName() << endl;
위 코드와 같이 말이죠.
위 코드는 변수들이 초기화 되어있지 않기 때문에 적절한 초기 값을 넣고 실행해야 합니다.
물론 private Person으로 상속하여 name, age 변수에 접근할 수 도 있습니다.
상속은 흔히 is a 관계라 합니다. 즉 위의 예제에선 Student is a Person이라는 소리죠.
상속받은 자식은 부모 매서드를 그대로 가져올 수도 있지만 부모의 매서드를 재정의 할 수도 있습니다. 아래의 코드를 위의 예제의 Student - public:에 추가해 봅시다.
string getName() {
return Person::getName() + ": " + major;
}
위 함수는 Person으로 부터 상속받은 getName함수를 덮어씁니다. Student클래스들은 getName() 함수를 호출하면 이름과 전공이 같이 나오게 됩니다.
오늘의 과제는 출석을 부르는 프로그램을 작성하는것 입니다. 아래의 명세를 따라주세요.
- 사람은 이름과 나이를 가지고 있습니다.
- 사람은 나이와 이름을 불려질 수 있습니다.
- 유치원생은 사람이고, 유치원 이름과 반 이름을 가지고 있습니다.
- 유치원생의 반은 ["햇님반", "꽃님반", "달님반"] 중 하나 입니다.
- 유치원생의 이름을 부르면 이름 앞에 반 이름, 이름 뒤에 **"네네 선생님"**을 붙여서 말합니다.
- 초등학생은 사람이고, 학교 이름과 학년, 반 번호를 가지고 있습니다.
- 초등학생의 반은 6반 보다 작거나 같습니다.
- 초등학생의 이름을 부르면 이름 앞에 학교 이름, 학년, 반, 이름 뒤에 **"입니다."**를 붙여서 말합니다.
- 중학생은 사람이고, 학교 이름과 학년, 반 번호를 가지고 있습니다.
- 중학생의 반은 8반 보다 작거나 같습니다.
- 중학생의 이름을 부르면 이름 앞에 학교 이름, 이름 뒤에 "이다." 를 붙여서 말합니다.
- 어떤 반에 유치원생 3명, 초등학생 5명 중학생 7명이 있습니다.
- 이들에 대한 정보는 랜덤 합니다.
- 이들 모두에 대한 이름을 한번씩 불러서 출력합니다.