위에서 데이터 구조(Struct)인 Student와 이를 출력하는 함수 print_student()가 분리되어 있다. 단순히 함수는 데이터를 받고 출력하는 역할만을 수행할 것이다.
그러나 객체지향에서는
class Student { string name; int score;public: Student(string name, int score): name(name), score(score) {} void print() { cout << name << ": " << score << endl; }}int main() { Student hoyeong = Student("최호영", 80); hoyeong.print(); return 0;}
위와 같이 Student라는 객체의 특성을 정의하는 클래스(Class)가 있고, 이 안에 name, score라는 변수, print()라는 함수를 가지도록 정의했다. 이제 Student라는 클래스를 통해 만들어지는 객체(여기서는, hoyeong)은 print()라는 기능을 가지고 있다.
클래스로부터 만들어지는 객체에서, 그 객체의 속성에 접근하려면 .을 붙이면 된다. 따라서 hoyeong.print()로 hoyeong이 가지는 print()라는 기능(함수)을 호출할 수 있다.
이렇게 객체가 고유한 특성으로써 가지는 함수를 메소드(Method)라고 칭한다.
절차지향과 객체지향의 관점이 각각 기능과 객체였다고 말한것을 기억하는가?
다음은 각 코드의 main() 에서 출력을 실행하는 부분이다.
절차지향 프로그래밍
print_student(hoyeong);
객체지향 프로그래밍
hoyeong.print();
절차지향 프로그래밍에서는 기능을 의미하는 함수 print_student()가 선행하고 이 안에 hoyeong이라는 데이터를 넣었다. 하지만 객체지향 프로그래밍에서는 객체를 의미하는 hoyeong이 선행하고, 이 객체가 가지고 있는 메소드를 호출한다는 의미로 .print()가 뒤따라오는 것을 볼 수 있다.
이 짧은 코드 한 줄에서 절차지향 프로그래밍과 객체지향 프로그래밍이 중점적으로 생각하는 요소가 다름을 알 수 있다.
데이터와 객체
데이터와 객체가 비슷한 개념으로 느껴질 수 있다. 앞에서 hoyeong은 절차지향 프로그래밍에서는 field값을 가지는 단순한 데이터였다가 객체지향 프로그래밍에서는 데이터를 넘어서는 객체가 되었다.
실제로 둘은 근본적으로 매우 비슷하다. 다만 절차지향에서는 그저 값 이상의 의미를 가지지 않았던 데이터의 기본 단위에, 주체성격을 부여하였음을 강조하는 의미로 객체라고 부르는 것으로 이해해도 좋다. 물론 자신의 데이터를 변경하는 함수를 그 객체가 소유하는 것도 매우 큰 특징이다.
1.4. 객체지향의 프로그래밍의 장점
객체 위주로 생각한다는 객체지향 프로그래밍의 특징은 이제 어렴풋이 알 것이다. 하지만, 이게 왜 좋다는걸까?
객체의 기능을 호출하는 방법을 다시 떠올려보자. 다음과 같이 .을 붙이고 뒤따라 객체의 메소드를 호출할 수 있었다.
hoyeong.print()
그런데… 이거이거 매우 비슷한 광경을 많이 본 것 같다.
아래는 문자열을 공백 기준으로 분할한 리스트를 만드는 파이썬 코드이다.
a = "hello world"b = a.split()
여기서도 문자열a에 .split()을 통해 기능을 호출하고 있다!
앞에서 분명 .으로 후행되는 메소드는 객체의 특성이라 하였다. a는 문자열이라는 타입의 값인데 어떻게 이게 가능할까?
정수 타입, Boolean 타입, 소수 타입, 리스트 타입, 딕셔너리 타입… 등등 우리가 파이썬에서 어떠한 타입이라고 배운 것들은 사실 전부 클래스로부터 생성되는 객체들이다. 우리가 파이썬에서 편리하게 사용했던 .split(), .upper(), .append() 등등 .으로 접근했던 모든 메소드들은, 파이썬이 이미 데이터를 값이 아닌 객체로써 다루기에 가능했던 것이다.
그렇기에 파이썬처럼 자동으로 데이터를 객체로 만들어주지 않는 C/C++ 등 많은 언어에서 int, float 등의 원시적인 데이터에 .을 붙여 갖가지 함수를 호출하는 편리한 방법은 제공되지 않는다.
마지막으로 객체지향 프로그래밍이 불가능했던 C 언어에서 위의 문제(문자열을 공백 문자 기준으로 분리)를 해결하기 위해 필요했던 함수의 코드를 보면서, 객체지향 프로그래밍이 어째서 유용한지 느껴보자.
char** split(const char* input, int* count) { *count = 0; const int MAX_TOKENS = 100; char** result = malloc(sizeof(char*) * MAX_TOKENS); if (!result) return NULL; char* buffer = malloc(strlen(input) + 1); if (!buffer) { free(result); return NULL; } strcpy(buffer, input); char* token = strtok(buffer, " "); while (token != NULL && *count < MAX_TOKENS) { result[*count] = malloc(strlen(token) + 1); if (!result[*count]) { for (int i = 0; i < *count; i++) free(result[i]); free(result); free(buffer); return NULL; } strcpy(result[*count], token); (*count)++; token = strtok(NULL, " "); } free(buffer); return result;}
2. 객체지향의 기본
2.1. 클래스와 인스턴스, 객체
앞에서 우리가 객체를 설명하며 대충 짚고 넘어간 클래스에 대한 내용을 정리해보자.
클래스(Class)는 객체를 만들기 위한 설계도로 생각하면 된다. 함수를 정의해놓으면 어디서든 간단하게 여러번 실행할 수 있듯이, 이 클래스를 정의해놓으면 간단한 생성자 호출을 통해 설계도대로의 객체를 마음껏 찍어낼 수 있다.
이렇게 만들어지는 클래스의 객체를 인스턴스(Instance)라 한다. 객체와 인스턴스 모두 혼용 가능한 단어이지만, 인스턴스는 객체 중에서도 특정 클래스로부터 생성된 객체임을 조금 더 강조한다.
Example
앞에서 살펴본 Student 클래스를 예시로 들 때 다음과 같은 표현이 가능하다.
hoyeong은 객체이다 ○
hoyeong은 클래스 Student의 객체이다 ○
hoyeong은 클래스 Student의 인스턴스이다 ○
hoyeong은 인스턴스이다 △
2.2. 클래스 만들기
2.2.1. 멤버 변수와 멤버 함수
지금까지 우리는 객체가 가지는 함수를 Method로 칭했다. 이는 실제로 객체지향 프로그래밍에서 쓰이는 단어로 대부분의 언어에서 비슷한 의미로써 통용된다.
C++에서는 공식적으로 위의 단어 대신에, 객체가 가지는 변수를 특별히 멤버 변수(Member Variable), 메소드는 멤버 함수(Member Function)이라 부른다. 이 글에서는 앞으로 객체가 가지는 변수를 멤버 변수, 객체가 가지는 함수는 메소드로 통일하여 표기하겠다.
2.2.2. public, private
C++에서 Student라는 클래스를 정의하고자 할 때, 멤버 변수와 메소드를 다음과 같이 작성한다.
class Student로 시작하는 브라켓 안에 일반적인 변수와 함수를 선언하듯이 열거하면 속성과 메소드가 정의된다.
이때, 위 코드에서 아무런 notation이 없는 부분과private, public으로 구분된 부분의 3가지 section으로 나뉘는 것을 볼 수 있다.
private: string address; void method2() { ... };
private으로 구분되어 있는 섹션에 정의된 속성과 메소드는 클래스의 외부에서 접근할 수 없도록 제한된다. 예를 들어 위에서 학생의 주소 address라는 멤버 변수는 나쁜 사람이 알 수 없도록, 아무에게나 공개되어서는 안된다. 따라서 다음과 같이 객체 외부에서 .을 통해 접근할 수 없게 된다.
hoyeong.address // 외부에서 접근 불가! 컴파일 에러 발생
private 변수는 같은 클래스의 다른 인스턴스의 메소드에서는 접근할 수 있다.
즉, 위 예제에서 같은 Student 클래스로부터 만들어진 또 다른 객체 gildong이 있다면, gildong은 자신의 속성 address 뿐 아니라 같은 클래스로부터 만들어진 다른 객체 hoyeong의 address에도 접근할 수 있다.
이러한 private한 속성과 메소드와는 다르게 public으로 구분된 섹션에 정의된 속성과 메소드는 그 의미에 맞게 외부의 어떠한 곳에서도 접근할 수 있다.
public: string name; void method3() { ... };
나머지 하나, 영역을 명시하지 않은 파트에서 선언되는 변수와 메소드는 자동적으로 private으로 지정된다. 기본적인 실행이 private하다는 것에서, 객체는 기본적으로 자신의 내부 상태를 감추려는 습성이 있음을 유추할 수 있다.
지금까지 살펴본 내용을 바탕으로 예제의 변수, 메소드를 정리하면 다음과 같다.
class Student { int score; // private void method1() { // private ... };private: string address; // private void method2() { // private ... };public: string name; // public void method3() { // public ... };}
2.2.3. 생성자와 기본 생성자
class Student {private: string name; int grade;}
위의 클래스 Student로부터 실제 객체를 생성해보자. 객체를 생성하려면 아래의 두가지 방식을 이용할 수 있다.
Student hoyeong;Student gildong = Student();
이 중 아래 방식을 보면 마치 함수를 실행하듯 호출하는 것을 볼 수 있는데, 이렇게 호출되는 Student()가 바로 생성자(Constructor)라 불리는 특수한 메소드이다. 생성자는 객체가 만들어질 때 필요한 멤버 변수 초기화나 다른 초기화 작업을 수행하는 역할을 맡는다.
생성자의 특징
생성자는 특수 메소드로 다음의 규칙을 따른다.
생성자가 정의되지 않으면 빈 생성자가 자동으로 삽입된다.
생성자의 이름은 클래스 이름과 동일해야 한다.
생성자의 반환 타입은 void이기에 어떠한 값도 return할 수 없다.
생성자의 반환 타입은 명시할 수 없다.
객체 생성 시에 단 한번 호출되며, 임의로 재호출할 수 없다.
생성자가 private 섹션에 정의되면, 다른 private 메소드와 마찬가지로 외부에서 Student()와 같이 호출할 수 없다.
앞에서 만든 Student 클래스가 아무 문제 없이 실행될 수 있는 이유는, 생성자를 정의하지 않았지만 컴파일러가 자동으로 다음과 같이 빈 생성자를 삽입해주기 때문이다.
class Student {private: string name; int grade;public: Student() {} // 생성자가 없으면 컴파일 시에 자동으로 빈 생성자 삽입!}
이제 우리가 직접 생성자를 정의해보자. 학생은 기본적으로 1학년부터 시작한다. 따라서 생성자 Student()는 멤버변수 grade를 기본값 1로 설정하는 초기화 과정을 수행하도록 작성할 수 있겠다.
class Student {private: string name; int grade;public: Student() { grade = 1; }}
또한, 생성자는 C++의 다른 함수와 마찬가지로 함수 오버로딩이 가능한데,그중에서도 아무 인자가 없는 생성자를 기본 생성자(Default Constructor)라 한다.
기본 생성자가 꼭 필요한 순간
Student student;
앞서 보았던 객체를 생성하는 두가지 방법 중 첫 번째 방법에서는 딱히 생성자라 할만한 메소드를 호출하지 않았던 것을 기억할 것이다. 이 경우 C++은 자동으로 해당 클래스의 기본 생성자를 찾아 호출하며 이 과정에서 당연하게도 우리는 어떠한 인자도 생성자에 전달할 수 없다.
따라서 정의한 많은 생성자들 중에 기본 생성자가 없다면 위와 같은 방식으로는 객체를 생성할 수 없다.
이는 특히 객체 배열 등을 만들 때에 중요하다. 다음과 같이 기본적인 방식으로 객체가 든 배열을 선언과 동시에 초기화하려면 노가다를 하지 않는 이상 기본 생성자가 꼭 필요함을 알 수 있다.
여기서 또 특이한 것은 int arr[3] = {0};은 가능하지만Student students[3] = {Student("A")};은 불가능하다는 점이다!
이는 배열의 원소 타입이 데이터 타입이 아닌 클래스의 객체인 경우, Student students[3] = {Student("A")};같이 작성했을 때 나머지 요소들은 Student("A")가 아닌 기본 생성자로 초기화되는 규칙 때문이다!
Student students[3] = {Student("A")};// 위 코드는 아래와 같다.Student students[3] = {Student("A"), Student(), Student()};
2.2.4. this 포인터
이제 생성자에서 학생의 이름 name 멤버 변수를 초기화해보자. 하지만 이름이라는 속성은 마땅한 기본값이 있는 것이 아니라 학생이라는 객체를 생성할 때에 우리가 지정해주어야 하는 값이다.
생성자는 이럴 때를 대비해 외부에서 값을 받아 사용할 수 있도록 인자를 받을 수 있다.
class Student {private: string name; int grade;public: Student(string name) { grade = 1; name = name; }}
그런데 위 코드의 8번 줄에서 무언가 석연치 않은 것이 보인다. name = name에서 우리는 분명 인자로 들어온 name을 멤버 변수 name에 복사하고 싶은데, 인자와 멤버 변수의 이름이 같아 우리가 원하는 동작이 불가능하다. (당연히 인자나 멤버 변수 이름을 바꾸면 되긴 하다)
따라서 메소드에서는 객체 자신의 멤버변수 혹은 메소드를 명확히 지시할 수 있도록 자신을 가리키는 this라는 특별한 포인터(여기서는, Student*타입)를 가진다.
class Student {private: string name; int grade;public: Student(string name) { this->grade = 1; this->name = name; }}
이제 생성자의 동작이 명확해졌다. grade에도 this->를 붙여 멤버변수임을 명시했고, this->name = name에서는 더이상 인자로 들어온 name이 멤버변수 name을 가리지 않아 원하는 대로의 동작이 일어난다.
어떠한 객체를 가리키는 포인터에 ->를 붙이면 해당 포인터가 가리키는 객체의 속성에 바로 접근할 수 있다.
Student 클래스를 구체화하다 보면 많은 멤버 변수를 추가하며 생성자 부분이 길어지는 불편함이 있을 수 있다.
class Student {private: string school; string name; int grade; int score; int absenseCount; ...public: Student(string school, string name, int grade, int score, int absenseCount) { this->school = school; this->name = name; this->grade = grade; this->score = score; this->absenseCount = absenseCount; ... }}
이 불편함을 없애기 위해 멤버 초기화 리스트(Member Initialization List)라 불리는 구문을 사용할 수 있는데, 이를 사용해 초기화한 아래의 코드는 위의 코드와 동일한 효력을 갖는다.
class Student {private: string school; string name; int grade; int score; int absenseCount; ...public: Student(string school, string name, int grade, int score, int absenseCount): school(school), name(name), grade(grade), score(score), absenseCount(absenseCount) { ... };}
이처럼 멤버변수(인자)형식을 통해 간편하게 객체 초기화가 가능하며, 이후 함수 로직 안에서 추가적인 초기화나 이미 멤버 초기화 리스트로 초기화한 멤버 변수를 또다시 수정하는 것도 가능하다.
멤버 초기화 리스트는 생성자 함수가 실행되기 전에 실행된다.
TODO 멤버 초기화 리스트가 필수적인 경우(const, 참조형 멤버, 멤버 클래스) 추가 기술