2016년 3월 18일 금요일

[c++기초] 1. 클래스

25 장. 클래스 

25장부터는 3부이며 여기서부터 객체 지향 프로그래밍 방법을 연구한다. 2부까지는 C/C++ 언어의 문법과 절차적 프로그래밍 방법에 대해 논했었는데 이번 장부터는 클래스를 통해 객체를 프로그래밍하는 방법에 대해 다룬다. 객체 지향이란 단순한 언어의 기능적인 확장이 아니라 완전히 새로운 프로그래밍 방법이며 절차적인 방법과는 근본적으로 다르다.
그래서 구조적 프로그래밍 기법에 이미 익숙해져 있는 사람은 습관을 완전히 바꿀 필요가 있다고 하는데 이는 어느 정도 사실이다. 그렇다고 해서 이미 배워 놓은 구조적 프로그래밍 기법이 완전히 쓸모 없어지는 것은 아니다. 클래스의 멤버 함수안에서는 여전히 구조적 기법이 필요하기 때문이다. C를 공부할 때와 마찬가지로 C++도 콘솔 환경에서 실습을 진행한다. 그래픽 환경에서 멋진 예제들을 만들어 보면 좋겠지만 문법을 배울 때는 문법만 살펴볼 수 있는 단순한 환경이 훨씬 더 효율적이다.

25-1.OOP

25-1-가.소프트웨어 위기

초기의 컴퓨터는 과학 기술용 연구 도구로 개발되었으며 극히 제한된 분야에서 복잡한 공학적 계산에만 활용되었다. 가격도 비쌌기 때문에 일반 사무용은 물론이고 개인적인 용도로 사용하는 것은 엄두도 내지 못했다. 그러나 하드웨어의 발전에 힘입어 가격이 저렴해지고 활발하게 보급됨에 따라 컴퓨터의 활용 분야가 점점 넓어졌다. 일반 사무용이나 개인 업무는 물론이고 심지어 게임이나 멀티미디어 등의 놀이에도 활용되었으며 요즘은 컴퓨터가 없는 생활을 상상하기 힘든 정도가 되었다.
컴퓨터가 업무와 생활 곳곳에 활용됨에 따라 특수한 용도에 맞는 다양한 소프트웨어가 필요해졌다. 또한 하드웨어가 빨라지고 대용량화 됨으로써 컴퓨터로 할 수 있는 일들이 많아져 소프트웨어의 기능도 과거보다 훨씬 더 복잡해지고 만들기도 어려워졌다. 사람들은 고성능 컴퓨터의 기능을 십분 활용하여 강력하고 편리하면서 또한 예쁜 프로그램을 쓰고 싶어한다. 즉 양적으로나 질적으로나 소프트웨어에 대한 요구가 대폭적으로 증대된 것이다.
그러나 시간과 개발 인력의 부족으로 인해 소프트웨어 공급은 수요의 증가를 따르지 못했다. 뿐만 아니라 생산된 소프트웨어도 규모가 커지고 구조가 복잡해짐에 따라 질적인 결함이 많아 신뢰성이 떨어졌다. 한 두 개의 버그는 당연히 존재하는 것으로 인식될 정도였으며 때로는 이런 버그들이 심각한 문제가 되어 어렵게 만든 소프트웨어가 폐기처분되기도 했다. 소프트웨어가 하드웨어의 발전을 따라가지 못하는 이런 현상을 소프트웨어 위기(Software Crisis)라고 한다.
잘 알려진 무어의 법칙에 의할 것 같으면 매 18개월마다 반도체의 트랜지스터 집적도는 2배씩 높아지고 속도도 2배씩 빨라진다고 한다. 이 계산대로라면 10년동안 중앙 처리 장치(CPU)는 대략 100배 정도 더 빨라지는데 이 법칙은 수십년간 기가 막히게 들어 맞았고 하드웨어의 성능은 그야말로 기하급수적으로 향상되어 왔으며 앞으로도 더 발전할 것이다. 그러나 이런 하드웨어의 눈부신 발전에 비해 소프트웨어의 발전은 더디기 그지 없었다.
소프트웨어 위기의 주요 원인 중 하나는 기존 절차식 프로그래밍 방법의 낮은 생산성이었다. 절차식 프로그래밍은 간결하고 빠른 실행 파일을 만들기는 하지만 규모가 커지면 개발뿐만 아니라 유지 보수에 한계를 드러냈다. 절차식이란 문제를 해결하는 절차가 중심인데 현실의 문제는 아주 특수하기 때문에 해결 방법도 특수할 수밖에 없다. 코드의 일반성이 없으므로 한 번 만든 코드가 수정없이 재사용되는 경우가 드물며 매번 현실의 문제에 맞게 처음부터 다시 개발해야 한다. 설사 재사용된다 하더라도 기존 코드를 그대로 쓰기 보다는 필요에 따라 조금씩은 수정해야만 했다.
그러다 보니 대규모의 소프트웨어를 만드는데 수년의 개발 기간과 과다한 인력을 소모하게 되었다. 고급 인력의 부족은 소프트웨어의 공급 부족을 초래했고 시간의 부족은 소프트웨어의 질을 저하시켰다. 하드웨어는 이미 만들어져 있는 트랜지스터나 IC 등을 조립하는 방식으로 개발할 수 있으며 부품의 신뢰성이 높으므로 완제품의 질도 좋을 수밖에 없다. 이에 비해 소프트웨어는 항상 처음부터 다시 개발해야 하므로 시간이 걸리고 신뢰성이 떨어진다.
그래서 대안으로 여러 가지 개발 기법들이 제시되었는데 그 중에서 가장 탁월한 기법으로 인정받은 것이 바로 절차가 아닌 데이터를 중심으로 개발을 진행하는 객체 지향적 프로그래밍(Object Oriented Programming) 방식이다. 문제의 핵심인 데이터를 정의하고 데이터에 절차를 결합하여 현실의 사물을 표현할 수 있는 객체를 만든다. 그리고 이런 독립적인 객체를 조립하여 프로그램을 완성해 나가는 방식이다.
객체는 재활용성이 아주 높아서 한 번 잘 만들어 놓으면 수정없이 다른 프로젝트에도 사용할 수 있다. 심지어 직접 만든 것이 아니더라도 약간의 약속만 준수하면 어렵지 않게 가져다 쓸 수도 있고 내가 만든 객체를 다른 사람에게 제공하거나 품질만 보장된다면 팔 수도 있다. 하드웨어 부품을 조립하듯이 객체를 조립하여 대규모의 소프트웨어를 단기간에 제작할 수 있게 된 것이다. 객체가 모든 프로젝트와 다양한 환경에서 동작할 수 있는 범용성을 갖추기는 쉽지 않지만 일단 만들기만 하면 얼마든지 재사용할 수 있고 신뢰성도 확보된다.
객체는 완전한 부품이 되기 위해 스스로 에러를 처리하고 자신의 무결성을 지키는 기능을 가지며 객체를 사용하기 위해 꼭 알아야 하는 것만 외부로 공개한다. 그래서 개발자가 객체의 내부 구조나 동작 원리를 잘 몰라도 이 객체들을 조립하여 고기능의 소프트웨어를 신속하게 만들 수 있다. 설사 개발자가 사소한 실수를 한다 하더라도 객체는 이런 실수로부터 스스로를 보호하기도 한다. 그래서 꼭 고급 인력이 아니더라도 조립 정도만 할 수 있는 초급 개발자도 소프트웨어를 만들 수 있게 되었다.
전자 상가에 가면 하드웨어 부품들과 설명서가 포함되어 있는 라디오 조립 키트같은 실습용 제품들이 많이 있다. 중학생 정도만 되면 이런 조립 키트를 사서 설명서대로 기판에 트랜지스터, 저항, 다이오드 같은 부품을 조립하고 납땝하여 라디오 정도는 얼마든지 손쉽게 만들 수 있다. 하드웨어에 대한 지식이 거의 없어도, 각 부품이 정확하게 어떤 동작을 하는지 몰라도 되므로 이 얼마나 생산적인가? 소프트웨어도 이런 식으로 상세한 것을 잘 몰라도 뚝딱 뚝딱 금방 만들 수 있다면 얼마나 좋겠는가? 이것이 바로 OOP가 필요해진 이유이다.
객체 지향 방식은 조립식이기 때문에 결과물의 성능(속도와 크기)이 맞춤형의 절차식에 비해 조금 떨어지는 단점이 있다. 그러나 하드웨어의 발전에 힘입어 그 정도의 차이는 큰 문제가 되지 않아 무시할만하다. 현대의 개발 관건은 프로그램의 성능보다 오히려 개발 용이성과 유지, 보수 편의성, 신뢰성이다. 인건비가 하드웨어보다 훨씬 더 비싸고 개발 기간이 곧 비용과 직결되기 때문에 얼마나 빨리 정확한 소프트웨어를 생산하는가가 관건이며 그 해답이 바로 객체 지향 프로그래밍이다.


25-1-나.OOP의 특징

본격적인 학습에 들어가기 전에 객체 지향 프로그래밍의 일반적인 특징들에 대해 먼저 정리해 보도록 하자. 학자에 따라 이 특징들 외에 몇 가지를 더 추가하기도 하며 각 특징의 범주가 조금씩 달라지는 경우도 있다. 심지어 객체 지향에 대한 정확한 정의와 범위마저도 완벽하게 합의되어 있지 않은 상태이다보니 각 특징에 대한 정의도 조금씩 견해가 다를 수 있다.

■ 캡슐화(Encapsulation) : 표현하고자 하는 자료(Data)와 동작(Function)을 하나의 단위로 묶는 것이며 이렇게 묶어 놓은 것을 객체(Object)라고 한다. 대상의 특징을 나타내는 데이터와 이 데이터를 관리하는 함수가 항상 하나의 묶음으로 사용되므로 객체는 스스로 독립적이며 프로그램의 부품으로 활용될 수 있다. 그래서 객체를 소프트웨어 IC라고 부르기도 한다.
■ 정보 은폐(Information Hiding) : 객체는 자신의 상태를 기억하기 위한 속성과 속성을 관리하는 동작을 정의한다. 이 중 외부에서 사용하는 기능만 공개하고 나머지는 숨길 수 있는데 이를 정보 은폐라고 한다. 외부에서 객체의 상태를 마음대로 바꾸거나 허가되지 않은 동작을 요청하지 못하도록 함으로써 스스로의 안전성을 확보하는 수단이며 정보 은폐에 의해 객체는 더욱 견고하게 캡슐화된다.
■ 추상화(Abstraction) : 현실의 사물을 객체로 표현하기 위해서는 이 사물이 어떤 특징을 가지며 어떤 동작이 가능한지를 조사해야 하는데 이를 데이터 모델링이라고 한다. 모델링의 결과 필요한 자료와 동작의 목록이 작성되면 이들을 캡슐화하여 객체로 정의한다. 그리고 외부에서 사용해야 하는 기능은 공개하고 제한해야 하는 기능은 숨긴다. 추상화란 객체의 효율적이고도 안전한 사용을 위해 인터페이스를 설계하는 것이며 캡슐화와 정보 은폐에 의해 구현된다. 추상화에 의해 외부에서는 객체의 인터페이스만 볼 수 있으며 내부 구현은 볼 수 없다. 그래서 사용 방법이 간단 명료하고 외부의 조작에 대해 안전해지며 객체는 추상적인 인터페이스를 유지하는 한도내에서 숨겨진 내부 구현을 마음대로 수정할 수 있어 기능 개선이 쉬워진다. 개념화라고도 한다.
■ 상속(Inheritance) : 상속은 이미 만들어진 클래스를 파생시켜 새로운 클래스를 정의하는 기법이다. 파생된 클래스는 기존 클래스의 모든 속성과 동작을 물려받으며 여기에 더 필요한 기능을 추가하거나 필요없는 기능을 제거 또는 변경할 수 있다. 객체를 아무리 추상적으로 잘 정의해 놓았다 하더라도 현실의 문제는 특수하기 때문에 모든 경우에 일반적으로 적용되지는 않는다. 이럴 때는 객체를 상속받아 원하는 부분만 수정할 수 있으며 기존 객체를 최대한 재활용함으로써 시간과 노력을 절약할 수 있다.
■ 다형성(Polymorphism) : 똑같은 호출이라도 상황에 따라, 호출하는 객체에 따라 다른 동작을 할 수 있는 능력을 다형성이라고 한다. 실제 내부 구현은 다르더라도 개념적으로 동일한 동작을 하는 함수를 하나의 인터페이스로 호출할 수 있으므로 객체들을 사용하는 코드를 일관되게 유지할 수 있다. 다형성은 동적 바인딩을 하는 가상 함수에 의해 구현된다.

OOP의 특성에 대해 요약적으로 설명했는데 이 설명을 읽고 당장 OOP의 본질을 파악하는 것은 쉽지 않을 것이다. OOP의 이런 주요 특성들은 객체 지향을 논할 때 으례히 첫 부분에 나오는 설명들이다. 그러나 이 특징들을 다 이해하고 느끼려면 객체 지향 전체를 다 경험해 봐야 할 정도로 어려운 내용이기도 하다. 앞으로 구체적인 예제를 곁들인 설명을 읽고 또 직접 실습을 진행하다 보면 차츰 의미를 이해하게 될 것이다.
이 책은 OOP의 여러 특징들을 설명하기 위해 각 특징에 대해 1~2개의 장을 할애하고 있다. 한 개념을 설명하고 이해하는데 수십 페이지를 읽고 실습해 봐야 할 정도로 어려운 개념이라는 뜻이다. 이 특징들을 간단하게 한국말로 번역해 보면 다음과 같이 정리할 수 있다.

특징
간단한 설명
캡슐화
묶는다
정보 은폐
숨긴다
추상화
표현한다
상속
재사용한다
다형성
상황에 따라 달라진다.

물론 빠른 이해를 위한 간략한 비유일 뿐이므로 정확한 정의는 아니며 핵심적인 내용만 간추려 정리한 것이다. 특히 다형성은 한 단어로 설명하기 참 어려운 개념이다.

절차적 프로그래밍 : 문제를 분석하고, 문제를 잘게 쪼개서, 하나의 기능만 하는 함수 단위로 나누는 ( 위에서 아래로 )
객체 지향 프로그래밍 : 필요한 부품을 만들어서 조립해 나가는 ( 아래에서 위로 ) 


25-2.C++로의 확장

25-2-가.개선 사항

C++은 C언어에 여러 가지 기능을 추가하거나 개선하여 만들어진 C의 상위 버전이다. 하위 호환성이 있으므로 대부분의 C코드는 C++ 컴파일러에서도 별다른 수정없이 그대로 컴파일된다. C++이 C와 달라진 가장 큰 차이점을 꼽는다면 역시 클래스를 지원한다는 점이다. C++언어의 초기 이름이 C with class였는데 이 이름은 C에 클래스 기능을 추가하여 C++을 만들었다는 것을 상징적으로 나타낸다.
C와 C++의 문법은 비슷하거나 약간 확장된 정도의 차이밖에 없어 소스 코드만으로는 잘 구분되지 않을 정도로 흡사하다. 그래서 C++을 배우기 전에 C에 익숙해 있으면 C++ 구문을 쉽게 받아들일 수 있다. C++은 제어문이나 함수 포인터, 연산자 등의 주요 문법을 C의 것을 그대로 계승받았기 때문이다. 그러나 개발 방법은 완전히 다른데 C는 구조적 프로그래밍 방식을 사용하고 C++은 객체 지향적인 방식을 사용한다. 그래서 두 언어를 완전히 다른 언어라고 주장하는 사람도 있다.
C에 익숙한 사람은 습관 때문에 C++의 객제 지향적인 방식에 익숙해지기 더 어렵다고도 하는데 실제로 이는 사실이다. 수년동안 구조적 기법으로 원하는 프로그램을 자유롭게 작성해 왔는데 갑자기 객체 지향 방식을 강요하게 되면 이 변화를 쉽게 받아들이지 못한다. 그래서 요즘은 프로그래밍을 처음 배울 때 아예 C를 건너뛰고 C++부터 배움으로써 애초에 객체 지향적인 방식에 익숙해지도록 하는 방법이 시도되고 있다. 그러나 객체라는 개념이 쉽게 이해하기 어려운 주제이므로 아직까지는 전통적인 순서대로 C를 먼저 공부하는 방식(溫故知新)이 더 효율적인 것으로 여겨지고 있다.
C++에 새로 추가된 클래스와 관련 이론은 3부 전체의 주제이므로 천천히 연구해 보기로 하고 여기서는 조그마한 개선 사항부터 정리해 보자. C와 C++이 10년 정도의 시차를 두고 개발되었기 때문에 많은 변화가 있었다. 이 책은 이런 변화들 중 클래스와 직접적인 상관이 없는 것들은 1, 2부의 관련 부분에서 이미 설명해 왔다. 2부 이전과 3부 이후는 C와 C++로 구분된 것이 아니라 구조적 프로그래밍과 객체 지향적 프로그래밍으로 구분되어 있기 때문이다. 어떤 기능들이 C++에서 새로 추가된 것인지 도표로 정리해 보자.

내용
위치
간단한 설명
범위 연산자
7-3-
지역변수에 의해 가려진 전역변수를 참조한다.
명시적 캐스팅
5-3-
(int)var형식이 아닌 int(var) 형식으로 캐스팅한다.
인라인 함수
16-3
본체가 호출부에 삽입되는 함수
디폴트 인수
16-4
실인수가 생략될  형식 인수에 적용되는 기본값
함수 오버로딩
16-5
같은 이름의 함수를 여러  정의하는 기능
태그가 타입으로 승격됨
13-1-
구조체 태그로부터 변수를 바로 선언할  있다.
이름없는 공용체
13-5-
공용체 이름없이 멤버들이 기억 장소를 공유한다.
한줄 주석
2-4-
//   끝까지 주석을 단다.
레퍼런스
15-4
변수에 대한 별명을 붙인다.
bool 타입
3-7-
1바이트의 진위형 타입

이 기능들은 모두 C++의 클래스를 제대로 지원하기 위해 추가된 것들이지만 클래스와 상관없는 부분에도 사용할 수 있다. 각 기능들을 학습했던 위치를 밝혀 두었으므로 생각이 잘 나지 않으면 앞 부분으로 돌아가 복습을 하고 오기 바란다. 특히 레퍼런스와 16장의 함수 관련 토픽은 앞으로 클래스를 학습하는데 꼭 필요한 선행 지식으로서 중요하다.
현대의 C컴파일러들은 모두 C++도 지원하기 때문에 이런 기능들이 원래 C에 있던 것인지 C++에서 새로 추가된 것인지를 아는 것은 중요하지 않다. 다만 교양적으로 알아 두면 언어의 역사를 이해하는데 도움이 되는 내용일 뿐이다. 지금은 C/C++이라는 통합된 언어로 지칭되고 있어 표준을 따르는 모든 컴파일러에서 이런 기능을 자유롭게 사용할 수 있다. 이외에 C++에서 달라진 점이라면 함수의 중간에서도 변수를 선언할 수 있다는 것 정도이다. C언어는 모든 변수들이 함수의 선두에 선언되어야 하지만 C++은 필요할 때 언제든지 변수를 추가로 선언할 수 있다.

void func()
{
     int i;
     double d;

     i=1;
     d=12.34;
}
void func()
{
     int i;
     i=1;

     double d;
     d=12.34;
}
C의 변수 선언문
C++의 변수 선언문

C형식으로 함수의 선두에 변수를 한꺼번에 선언하면 지역변수의 목록을 한눈에 파악할 수 있어 훨씬 더 깔끔하다는 장점이 있다. 그래서 C++에서도 가급적이면 지역변수들을 선두에 모아서 선언하는 것이 관례이다. 하지만 함수의 내용이 아주 길어질 때는 변수가 사용되는 곳과 가급적 가까운 곳에 선언을 두는 것이 더 편리하다. 취향에 따라 선택하되 나는 함수가 지나치게 길지 않는 한 선두에 지역변수를 모아서 선언하는 것을 좋아하는 편이다.


25-2-나. IOStream

어떤 언어를 배울 때 가장 먼저 배우는 것은 입출력 명령이다. 베이직을 배울 때는 PRINT 명령부터 시작하며 C언어에 입문할 때도 printf를 가장 먼저 배웠고 윈도우즈 API에서는 TextOut이 제일 기초적이다. 심지어 도스나 유닉스같은 운영체제를 배울 때도 파일 목록을 확인하는 dir이나 ls 명령을 최우선적으로 학습한다. 일단 입출력이 가능해야 언어의 동작을 확인할 수 있기 때문인데 C++에서 이런 기본적인 입출력 수단은 바로 입출력 스트림이며 cin, cout이 입출력 객체이다.
C++에서는 모든 것을 객체로 표현하기 때문에 입출력을 담당하는 것도 함수가 아니라 객체다. cout은 C++의 가장 기본적인 출력 객체이고 cin은 입력 객체이다. 다음 예제는 cout 객체를 사용하여 문자열을 화면으로 출력하는데 이 책에서 제일 처음으로 만들었던 First예제의 C++ 버전이라고 생각하면 된다.

  : cout
#include <iostream>
using namespace std;

void main()
{
     cout << "Welcome C++" << endl;
}

실행해 보면 "Welcome C++"이라는 문자열이 화면으로 출력될 것이다. 첫 줄에서 iostream을 포함시키고 있는데 이 파일은 C의 stdio.h 쯤에 해당하는 기본 헤더 파일이다. cout 출력 객체가 이 헤더 파일에 정의되어 있으므로 이 객체를 사용하려면 반드시 iostream을 포함시켜야 한다. 두 번째 줄은 네임 스페이스 std를 사용하겠다는 선언인데 cout 객체를 사용하기 위해서는 이 선언이 있어야 한다.
네임 스페이스는 명칭을 저장하는 기억 영역으로서 C++에 새로 추가된 기능이다. C++ 표준 라이브러리는 std라는 네임 스페이스에 모두 정의되어 있으므로 std를 사용하겠다는 선언을 해야 한다. 네임 스페이스에 대해서는 다음에 따로 연구해 볼 것이다. C프로그램이 대부분 #include <stdio.h>로 시작하는 것처럼 C++ 프로그램은 거의 항상 이 두 줄로 시작된다. cout의 기본 형식은 다음과 같다.

cout << 데이터 << 데이터 ....;

<< 연산자(Insertion:삽입 연산자) 다음에 출력할 데이터를 적는데 데이터의 타입은 객체가 알아서 판단한다. 그래서 printf처럼 %d, %f같은 서식을 지정할 필요가 없어서 편리하며 서식이 불일치하여 생기는 문제점도 없다. endl은 개행 코드를 의미하며 확장열 '\n'과 기능적으로 동일하다. main 함수에서 cout 객체로 문자열을 출력하고 개행했다. 만약 실습용으로 사용하고 있는 컴파일러에서 이 예제가 컴파일되지 않는다면 다음과 같이 수정해야 한다.

  : coutold
#include <iostream.h>

void main()
{
     cout << "Welcome C++" << endl;
}

헤더 파일 이름이 iostream.h로 바뀌었고 네임 스페이스 지정문이 사라졌다. 이 방식은 C++ 표준이 정해지기 전의 방식이고 cout 예제가 현재의 표준대로 작성한 것이다. C++ 표준 위원회는 새로운 표준을 정하면서 구식 C++ 문법과 새로 정한 표준 문법과의 차이점 때문에 헤더 파일 이름을 일괄적으로 바꾸기로 했는데 확장자 .h를 없애기로 결정했다. 헤더 파일은 통상 하드 디스크에 저장되는 물리적인 파일이지만 컴파일러에 따라서는 파일이 아닌 메모리나 미리 컴파일된 정보들을 참조할 수도 있으므로 파일 냄새를 풍기는 .h 확장자를 없애 버린 것이다. 물론 아직까지 최신 컴파일러에서도 헤더는 물리적인 파일이지만 앞으로는 아닐 수도 있으며 #include는 포함할 정보의 명칭을 지정할 뿐이다.
그래서 최신 컴파일러들은 iostream.h 대신 iostream 만을 가지고 있는데 오랫동안 C 코딩을 해 온 사람들에게는 좀 어색한 변화라고 할 수 있다. 비주얼 C++ 6.0은 표준이 한창 제정되던 시기의 과도기적 컴파일러이기 때문에 둘 다 지원하며 7.0이상은 표준대로 cout 예제만 컴파일할 수 있다. Dev-C++은 두 방식을 모두 지원한다. 좀 더 오래된 컴파일러는 반대로 coutold는 컴파일하지만 cout은 컴파일하지 못할 것이다. 앞으로는 표준이 정한 바대로 cout 방식으로 코드를 작성해야 하며 앞으로 이 책의 예제들은 표준에 맞게 작성할 것이다.
여러 개의 데이터를 이어서 출력할 때는 << 연산자를 계속해서 사용한다. 다음 예제는 다양한 타입의 변수를 cout 객체로 한꺼번에 출력한다.

  : coutmulti
#include <iostream>
using namespace std;

void main()
{
     int i=123;
     char ch='A';
     double d=3.14;
     char str[]="문자열";

     cout << i << ch << d << str << endl;
}

출력 결과는 "123A3.14문자열" 이렇다. 변수나 상수들을 연쇄적으로 출력할 수 있으며 정수형이든 실수형이든 문자열이든 무조건 << 연산자로 보내기만 하면 된다. 다음은 cin 연산자로 입력을 받아 보자. cin 다음에 >> 연산자(Extraction:추출 연산자)를 쓰고 입력한 값을 대입받는 변수를 적되 마찬가지로 변수의 타입은 구분하지 않아도 된다.

  : cin
#include <iostream>
using namespace std;

void main()
{
     int i;
     cout << "정수를 입력하십시오 : ";
     cin >> i;
     cout << "입력한 값은 " << i << "입니다." << endl;
}

정수 하나를 키보드로부터 입력받아 i에 대입하고 그 결과를 cout으로 다시 출력해 보았다. 실행 결과는 다음과 같다.

정수를 입력하십시오 : 25
입력한 값은 25입니다.

언뜻 보기에도 cin이 scanf보다는 훨씬 더 좋아 보인다. 입출력 객체는 C 표준 라이브러리의 printf, scanf함수에 비해 많은 장점을 가지고 있다.

① 사용 방법이 훨씬 더 직관적이다. 출력할 때는 << 연산자로 데이터를 출력 객체에게 보내고 입력 객체는 >> 연산자로 입력받은 값을 변수로 보내는 모양을 하고 있어 사용하기 쉽다. <<, >> 연산자의 머리 부분이 입출력 방향을 명시하므로 모양대로 사용하면 된다.
② 입출력 객체가 데이터의 타입을 자동으로 판별하기 때문에 서식을 일일이 기억할 필요도 없고 서식을 잘못 적는 실수를 할 리도 없으니 안전하다. printf는 서식과 인수의 개수가 맞지 않거나 타입이 틀릴 경우 컴파일 에러는 발생하지 않지만 실행중에 프로그램이 다운될 수 있다. scanf는 입력받을 데이터가 문자열이 아닌 경우 반드시 &연산자로 주소를 넘겨야 하는데 이를 깜박 잊으면 마찬가지로 프로그램이 먹통이 되어 버린다. 입출력 객체는 자신이 처리하지 못하는 타입에 대해 컴파일 에러를 발생시키므로 훨씬 더 안전하다.
③ 입출력 객체의 <<, >> 연산자는 여러 가지 기본 타입에 대해 중복 정의되어 있는데 필요할 경우 사용자 정의 타입을 인식하도록 확장할 수 있다. 이때 사용되는 기술이 연산자 오버로딩이다. 이 기술을 사용하면 날짜, 시간, 신상 명세 등의 복잡한 정보도 표준 입력 객체로 출력할 수 있다. printf, scanf는 라이브러리가 제공하는 서식만 다룰 수 있는 것과 비교된다.

입출력 객체가 여러 가지 면에서 printf, scanf 보다는 장점이 많은 것이 사실이지만 이 책에서는 앞으로도 printf를 계속 애용할 것이다. 어차피 printf나 cout이나 예제 동작 확인용으로만 사용하는 것이므로 익숙한 방법을 계속 쓰는 것이 좋으며 가독성도 printf가 cout보다 오히려 더 좋다. 또한 C++ 표준이 적용되고 있는 중이라 컴파일러마다 cout을 쓰는 방법이 조금씩 달라 실습에 방해가 되는 점도 고려했다.
물론 실제 프로젝트를 한다면 printf보다는 cout을 쓰는 것이 더 안전하고 좋다. 하지만 요즘같은 그래픽 세상에 콘솔에 문자열을 출력하는 프로그램을 만들 일이 흔하지 않기 때문에 실제로 cout을 써 볼 기회는 별로 많지 않을 것이다. printf나 cout이나 현재는 실습을 위한 확인 기능 정도밖에 없다고 할 수 있다. C++에서는 반드시 cout으로 출력해야 한다는 것은 일종의 고정 관념일 뿐이다. 입출력 객체에 대한 상세한 설명은 이 책 끝에서 따로 다루기로 한다.
C 문법 학습을 위해 사용했던 Turboc.h 헤더 파일도 당분간 계속 사용하기로 한다. 콘솔에서 원활하고 흥미있는 실습을 하기 위해서는 아직까지도 gotoxy, clrscr, delay 같은 함수가 여전히 필요하다. iostream은 필요할 경우에만 포함할 것이다. C 문법을 잘 아는 사람들은 이 책을 25장부터 읽는 경우도 있을텐데 이런 사람은 1장의 실습 준비편이 지시하는대로 Turboc.h 헤더 파일을 설치하기 바란다. 이 파일이 설치되어 있지 않으면 이 책의 예제들은 대부분 컴파일되지 않는다.


25-2-다. new

new, delete는 C의 malloc, free에 대응되는 C++의 메모리 할당 연산자이며 실행중에 메모리를 할당한다는 점에서 용도가 비슷하다. 할당 연산자인 new의 기본 형식은 다음과 같다.

포인터 = new 타입[(초기값)];

new 다음에 할당 대상 타입을 밝히면 sizeof(타입)만큼의 메모리가 할당되고 할당된 포인터가 리턴된다. new가 리턴하는 번지는 같은 타입의 포인터 변수로 대입받는다. 할당과 동시에 메모리를 초기화하고 싶으면 타입 다음의 괄호에 원하는 초기값을 적되 초기화를 할 필요가 없으면 생략할 수 있다. 초기화하지 않은 메모리는 물론 쓰레기값을 가진다. 메모리 부족 등의 이유로 할당에 실패하면 NULL을 리턴하는데 원칙적으로 이 리턴값을 점검해 보아야 하지만 32비트 환경에서는 실패할 확률이 거의 없어 점검을 생략하는 경우도 많다.
이렇게 할당된 메모리를 해제할 때는 delete 연산자를 사용하는데 해제할 포인터를 delete 다음에 지정한다. 만약 할당만 하고 해제를 하지 않으면 메모리 일부를 사용할 수 없게 되는 메모리 누수(Memory Leak)가 발생하므로 동적 할당한 메모리는 반드시 delete해야 한다. 한 포인터에 대해 delete를 두 번 하는 것은 안되지만 NULL 포인터를 삭제하는 것은 가능하다. 즉 다음과 같이 할 필요가 없다.

if (pi != NULL) {
     delete pi;
}

delete는 NULL 포인터에 대해서는 아무런 동작도 하지 않도록 정의되어 있으므로 안전하다. 위 코드에서 if문으로 점검할 필요없이 무조건 pi를 해제해도 상관없다. 다음은 new, delete로 정수형 변수 하나를 동적으로 생성해 본 것이다.

  : newdelete
#include <Turboc.h>

void main()
{
     int *pi=new int;
     *pi=123;
     printf("*pi=%d\n",*pi);
     delete pi;
}

new연산자에 의해 정수형 하나를 저장할만큼의 공간(4바이트)이 할당되는데 할당된 포인터를 정수형 포인터 변수 pi로 대입받았다. 힙에 할당된 4바이트를 pi가 가리키고 있으며 이후 *pi는 동적 할당된 정수형 변수가 되며 정수값 하나를 기억할 수 있다. 예제에서는 *pi에 123이라는 정수값을 대입하고 확인을 위해 출력만 해 보았다. 다 사용하고 난 다음에는 delete pi로 해제한다. 다음은 new 연산자로 실수형과 문자형 변수를 할당하는 예이다.

double *pd=new double;
char *pc=new char;

pd에는 8바이트가 할당되고 pc에는 1바이트가 할당될 것이다. 잘 사용되지는 않지만 new int; 라고 쓰는 대신 괄호를 써서 new(int)라고 쓰는 방법도 있으며 이때도 new(int)(123) 형식으로 초기값을 지정할 수 있다. new/delete 연산자는 malloc/free와 기능적으로 동일하기 때문에 위 예제를 다음과 같이 고쳐 써도 똑같이 동작한다.

int *pi=(int *)malloc(sizeof(int));
*pi=123;
printf("*pi=%d\n",*pi);
free(pi);

new 대신 malloc을 사용했고 delete 대신 free를 사용했다. 메모리 할당의 면에서만 본다면 new/delete는 malloc/free와 동일하지만 차이점도 많이 있다. new/delete는 malloc/free보다 메모리를 관리하는 방식이 훨씬 더 진보적이며 속도도 빠르고 OOP에 적합한 특징들을 많이 가지고 있다. 어떤 점이 다른지 보자.

① malloc/free는 라이브러리가 제공하는 함수인데 비해 new/delete는 언어가 제공하는 연산자이다. 그래서 별도의 헤더 파일을 포함할 필요없이 언제든지 사용할 수 있으며 이 연산자를 쓴다고 해서 프로그램이 커지는 것도 아니다. 연산자이기 때문에 사용자 정의 타입에 대해 오버로딩할 수도 있다.
② malloc 함수는 필요한 메모리양을 바이트 단위로 지정하고 void *를 리턴하므로 sizeof 연산자와 캐스트 연산자의 도움을 받아야 한다. 이에 비해 new는 할당할 타입을 지정하고 해당 타입의 포인터를 리턴하므로 sizeof 연산자와 캐스트 연산자를 쓸 필요가 없다. 할당한 타입과 같은 타입의 포인터 변수로 대입만 받으면 된다.
③ malloc은 메모리를 할당하는 것만이 목적이므로 초기값을 줄 수 없지만 new 연산자는 동적으로 생성한 변수의 초기값을 지정할 수 있다. 즉 할당과 동시에 초기화를 할 수 있는데 할당 타입 다음의 괄호에 초기값을 적어 주면 된다. int *pi=new int; *pi=123; 두 문장은 int *pi=new int(123); 하나로 합칠 수 있다.
④ new 연산자로 객체를 할당할 때 생성자가 자동으로 호출된다. 생성자란 객체를 자동으로 초기화하는 특별한 함수인데 다음 장에서 배우게 될 것이다. 생성자는 생성과 동시에 객체를 초기화할 수 있도록 함으로써 클래스가 기존 타입과 동등한 자격을 가지도록 하는 중요한 역할을 한다. 생성자를 호출한다는 점이 malloc과 new의 가장 큰 차이점이며 C++에서 별도의 할당 연산자가 추가된 이유이다. 마찬가지로 delete로 객체를 삭제할 때는 파괴자라는 특별한 함수가 자동으로 호출된다.

new 연산자는 기본 타입뿐만 아니라 구조체나 배열, 사용자 정의형 타입도 할당할 수 있다. 다음은 new 연산자로 구조체를 할당하는 예제이다.

  : newstruct
#include <Turboc.h>

struct tag_Friend {
   char Name[10];
   int Age;
   double Height;
};

void main()
{
     tag_Friend *pF=new tag_Friend;
     strcpy(pF->Name,"아무개");
     pF->Age=22;
     pF->Height=177.7;
     printf("이름=%s, 나이=%d, 키=%.1f\n",pF->Name,pF->Age,pF->Height);
     delete pF;
}

new 연산자에 의해 sizeof(tag_Friend)만큼의 메모리가 할당되고 tag_Friend * 타입이 리턴된다. 구조체를 할당과 동시에 초기화하려면 다음 장에서 배울 생성자 함수라는 것을 만들어야 한다. 배열을 할당하는 방법은 조금 특수하다. 할당 타입 다음의 [ ] 안에 배열의 크기를 지정하는데 다음은 정수형 배열을 할당하는 예이다.

  : newarray
#include <Turboc.h>

void main()
{
     int *ar=new int[5];
     int i;
     for (i=0;i<5;i++) {
          ar[i]=i;
     }
     for (i=0;i<5;i++) {
          printf("%d번째 = %d\n",i,ar[i]);
     }
     delete [] ar;
}

new int[5];에 의해 정수형 변수 5개를 저장할 수 있는 메모리가 할당되며 int * 타입이 리턴된다. 리턴값을 int *형의 ar로 대입받으면 ar은 정수형 배열과 동등하며 배열처럼 사용할 수 있다. 배열을 할당할 때는 new연산자로도 초기값을 지정할 수 없다. 동적으로 할당하는 것이므로 할당할 배열의 크기를 지정하는 값이 꼭 상수여야 할 필요는 없으며 변수로도 크기를 지정할 수 있다. 즉 다음과 같은 코드도 가능하다.

int n;
printf("도대체 몇 개나 필요하니? : ");
scanf("%d",&n);
int *ar=new int[n];

동적으로 할당한 배열을 삭제할 때는 delete 대신 반드시 delete [ ] 문을 사용해야 한다. 그렇지 않았을 때 그러니까 할당은 new [ ]로 해 놓고 해제는 delete로 했을 때의 동작은 정의되어 있지 않으며(Undefined) 상황에 따라 다르다. 일반적으로 배열의 첫 번째 요소에 대해서만 파괴자가 호출되고 나머지는 파괴자가 호출되지 않으므로 메모리 누수가 발생할 것이다. 또는 할당 헤더의 구조가 달라 첫 번째 요소가 제대로 파괴되기 전에 다운될 수도 있는데 정의되지 않은 동작의 결과는 컴파일러에 따라 달라진다. 흔히 많이 실수하는 부분이므로 동적 할당한 배열은 delete [ ]로 해제한다는 것을 꼭 기억해 놓도록 하자. 중요한 내용이므로 한 번 더 반복한다. new는 delete와 짝이고 new [ ]는 delete [ ]와 짝이다.
new/delete의 가장 큰 장점은 객체가 생성, 파괴될 때 생성자와 파괴자가 호출된다는 점인데 이에 대해서는 다음에 따로 연구해 볼 것이다. 이 점만 제외하면 malloc/free와 큰 차이점은 없다. C++에서는 가급적이면 malloc/free 대신 new/delete를 사용할 것을 권장하지만 반드시 그래야 하는 것은 아니다. 단순히 메모리 할당만 한다면 malloc/free도 아직까지 쓸만하며 오히려 더 편리한 면도 있다.
예를 들어 malloc으로 할당한 메모리는 realloc으로 크기를 바꿔 재할당할 수 있지만 new에는 이에 대응하는 기능이 없어 새로 할당하여 복사하고 원래 메모리를 해제하는 과정을 직접 해야 한다. 그래서 재할당할 때마다 매번 번지가 바뀌며 심지어 축소할 때도 번지가 바뀐다. 또한 실행중에 할당 블록의 크기를 조사하는 _msize에 해당하는 기능도 없다. 할당 대상이 객체가 아니고 재할당을 빈번하게 한다면 malloc/free를 사용할 수도 있고 객체를 할당할 때는 반드시 new/delete를 써야 한다. 단, 할당, 해제 함수는 반드시 짝을 맞추어야 하며 섞어서 쓸 수는 없다. new로 할당한 메모리는 반드시 delete로 해제해야 하고 malloc으로 할당한 메모리는 free로 해제한다.


25-3.구조체의 확장

25-3-가.멤버 함수

앞 두 절에서 비록 대충이기는 하지만 OOP 개론에 대해 소개했고 C언어에 대한 C++의 개선점과 추가된 기능들에 대해서 연구해 보았다. 이른바 준비 운동이 끝난 셈인데 이 절부터 본격적으로 C++과 객체 지향 프로그래밍에 대해 연구해 본다. C++의 대표적인 간판 스타 클래스의 정의에 대해 연구해 보는 것으로 짜릿한 OOP 여행을 시작하자.
구조체는 타입이 서로 다른 이형 변수의 집합이다. 화면상의 한 좌표와 그 위치에 출력될 문자에 대한 정보를 저장하고 싶다면 다음과 같은 구조체를 선언해야 한다. 좌표값 (x, y)는 정수형이고 문자 ch는 문자형이므로 서로 타입이 다르고 그래서 이형 타입 변수의 집합인 구조체로 이 변수들을 묶어 정의한다.

struct Position
{
     int x;
     int y;
     char ch;
};

타입의 이름은 Position이고 이 구조체 안에 x, y, ch 멤버가 포함되어 있다. 다음 예제는 이 구조체를 사용하여 화면에 한 문자를 출력한다. 눈으로 죽 읽기만 해도 이해될 정도로 쉽다.

  : Pos1
#include <Turboc.h>

struct Position
{
     int x;
     int y;
     char ch;
};

void OutPosition(Position Pos)
{
     gotoxy(Pos.x, Pos.y);
     putch(Pos.ch);
}

void main()
{
     Position Here;
     Here.x=30;
     Here.y=10;
     Here.ch='A';
     OutPosition(Here);
}

실행해 보면 화면의 (30,10) 위치에 문자 'A'가 출력될 것이다. 문자를 출력하는 작업은 OutPosition 함수가 처리하는데 인수로 전달받은 구조체 Pos의 정보대로 지정한 위치에 문자를 출력했다. main 함수는 Position형의 구조체 변수 Here를 선언하고 이 구조체의 멤버에 각각 30, 10, 'A'를 대입했으며 OutPosition 함수를 호출하여 (30,10)위치에 'A'를 출력했다. C수준에서 아주 쉽게 이해되는 간단한 예제이다.
이 예제에서 Position 구조체와 OutPosition 함수는 상호 의존적인 관계에 있다. Position 구조체가 없으면 OutPosition은 인수를 받아들일 수 없으므로 컴파일되지 않으며 동작할 수도 없다. 또한 OutPosition 함수가 없으면 Position 구조체는 화면에 출력되지 못하므로 자신의 존재를 나타낼 방법이 없다. Position은 정보를 가지고 OutPosition은 동작을 정의하는데 정보를 보여주기 위해서는 동작이 필요하고 동작이 수행되려면 정보가 필요한 것이다.
만약 이 구조체를 다른 프로그램에서 재사용하고자 한다면 구조체와 함수를 같이 가지고 가야 하며 둘 중 하나만 가지고 가면 아무 짝에도 쓸모가 없다. 이렇게 밀접한 구조체와 함수는 한 쌍으로 볼 수 있는데 C++은 연관된 코드와 데이터를 하나의 범위에 포함시킬 수 있는 방법을 제공한다. 이 개념이 바로 캡슐화이다. 구조체가 다양한 타입의 멤버 변수를 포함하듯이 함수도 포함할 수 있다. 다음은 Position 구조체와 OutPosition 함수를 하나로 합친 것이다.

  : Pos2
#include <Turboc.h>

struct Position
{
     int x;
     int y;
     char ch;
     void OutPosition() {
          gotoxy(x, y);
          putch(ch);
     }
};

void main()
{
     Position Here;
     Here.x=30;
     Here.y=10;
     Here.ch='A';
     Here.OutPosition();
}

실행해 보면 결과는 앞의 예제와 동일하다. 소스의 구조는 다소 변경되었는데 OutPosition 함수가 구조체 선언 안에 포함되었다. 구조체와 관련된 함수를 따로 정의할 필요없이 아예 구조체에 포함시켜 버린 것이다. 이렇게 구조체에 포함된 함수를 멤버 함수라고 부르며 구조체에 포함된 변수는 멤버 변수라고 부른다.
, C++에서 구조체는 멤버 변수와 멤버 함수로 구성된다. C의 구조체는 이형 변수의 집합, 즉 타입이 다른 변수들의 집합이다. 변수만 포함될 수 있었으므로 단순히 멤버라는 용어를 사용했지만 C++의 구조체에는 함수도 같이 포함될 수 있으므로 두 종류의 멤버를 구분할 수 있는 별도의 이름이 필요해진 것이다. 다른 언어에서는 멤버 변수를 필드(Field), 멤버 함수를 메소드(Method)라고 부르기도 하는데 같은 뜻이다.
OutPosition 함수가 구조체에 포함됨으로써 소스의 다른 부분들도 많이 달라졌다. 어떻게 달라졌는지 정리해 보자.

① OutPosition 함수가 인수를 받아들일 필요가 없다. 일반 함수일 때는 어떤 구조체의 정보를 사용할 것인지를 인수로 전달받아야 했지만 구조체에 소속되었기 때문에 소속된 구조체의 정보를 사용하면 된다.
② OutPosition 함수 내부에서 x, y 멤버 변수를 참조할 때 소속 구조체를 밝힐 필요가 없어졌다. 구조체 밖에 있을 때는 어떤 구조체에 속한 멤버 변수인지를 밝혀야 하지만 멤버 함수는 별도의 지정없이 자신이 속해 있는 구조체의 멤버 변수를 이름만으로 액세스할 수 있다.
③ main에서 OutPosition 함수를 호출할 때 함수가 소속된 구조체 변수 Here를 앞에 적어주었다. OutPosition 함수는 독립된 함수가 아니라 구조체에 속한 멤버이므로 어떤 구조체의 정보를 대상으로 동작할 것인지를 밝혀야 한다. 멤버 함수를 호출하는 방법은 멤버 변수를 참조하는 것과 동일하다. 점 연산자를 사용하여 구조체.함수() 식으로 호출하며 구조체 포인터라면 구조체->함수() 식으로 호출한다.

구조체가 멤버 함수를 포함하면 스스로 동작할 수 있는 독립성이 부여된다. 멤버 변수로 정보를 기억할 수 있고 이 정보들을 바탕으로 직접 동작도 할 수 있다. 독립성이 생기면 재사용성이 확보된다. 내부에 정보와 함수를 모두 포함하고 있으므로 이 구조체만 다른 프로젝트로 가져가면 쉽게 재사용할 수 있다.

Pos2 예제가 보여주는 구조체와 함수의 통합, 이것이 바로 OOP 캡슐화의 기본적인 개념이다. 변수든 함수든 논리적으로 관련된 것을 한 곳에 모아 묶어 놓음으로써 구조체가 프로그램의 부품 역할을 할 수 있게 된 것이다.

멤버 함수 작성법

리턴타입 소속::멤버함수명(인자){본문}
내부정의 : 인라인 속성, 실제 함수가 호출 되는 것이 아니라, 멤버함수를 호출하는 코드가 함수의 본체코드로 대체 
외부정의 : 일반적인 멤버함수 호출로 스택을 통하여 인수를 넘기고, 제어의 분기가 발생 



25-3-다.액세스 지정

구조체의 멤버는 외부에서 언제든지 참조할 수 있다. 앞 예제에서 보았다시피 main에서 Here.x를 읽을 수도 있고 새로운 값을 대입할 수도 있으며 Here의 멤버 함수 OutPosition을 호출하여 어떤 동작을 할 수도 있다.

Position Here;
Here.x=20;                            // 구조체의 멤버에 값 대입
Here.OutPosition();           // 구조체의 멤버 함수 호출

비단 구조체의 멤버 뿐만 아니라 선언되어 있는 변수를 참조하거나 작성되어 있는 함수를 호출할 수 있는 것은 C 수준에서는 당연한 얘기다. 그러나 구조체 내부의 멤버를 외부에서 마음대로 건드리도록 내 버려 두면 부주의한 사용으로 인해 버그가 발생할 수 있어 위험할 뿐만 아니라 객체의 독립성도 떨어진다. 그래서 C++에서는 구조체(또는 클래스)의 멤버에 대한 외부의 참조를 허가할 것인지 금지할 것인지를 지정할 수 있다. 이를 액세스 지정이라고 하는데 다음 세 가지 종류가 있다.

■ private : 이 속성을 가지는 멤버는 외부에서 액세스할 수 없으며 구조체의 멤버 함수만 액세스할 수 있다. 외부에서는 프라이비트 멤버를 읽을 수 없음은 물론이고 존재 자체도 알려지지 않는다.
■ public : 이 속성을 가지는 멤버는 외부로 공개되어 누구나 읽고 쓸 수 있고 함수의 경우는 호출할 수 있다. 구조체가 자신의 속성이나 동작을 외부로 공개하는 수단이 되며 퍼블릭 멤버를 소위 인터페이스라고 한다.
■ protected : private와 마찬가지로 외부에서는 액세스할 수 없으나 단, 상속된 파생 클래스는 이 멤버를 액세스할 수 있다. 프라이비트 멤버는 파생 클래스에서조차도 참조할 수 없으며 오로지 자신만이 이 멤버를 참조할 수 있다는 점이 다르다.

액세스 지정자는 구조체 선언문 내에서만 사용되는데 다른 액세스 지정자가 나올 때까지 계속 이 속성이 적용된다. 액세스 지정자 사이가 한 블록이 되어 이 블록에 선언된 멤버들의 액세스 속성을 지정한다. 다음 예를 보자.

  : BaboAccess
#include <Turboc.h>

struct Babo
{
private:
     int a;
     double b;
     char ch;
     void Initialize();
public:
     int x;
     int y;
     void func(int i);
protected:
     float k;
};

void main()
{
     Babo Kim;
     Kim.a=1;                // 에러
     Kim.x=10;               // 대입 가능
     Kim.func(3);           // 호출 가능
     Kim.Initialize();       // 에러
}

Babo 구조체안에 여러 가지 멤버 변수와 멤버 함수가 선언되어 있는데 각각 선언된 영역이 다르다. a, b, ch 멤버는 비공개 영역에 있으므로 외부에서 이 멤버를 읽을 수 없으며 initialize 함수도 외부에서 호출할 수 없다. 반면 x, y는 공개된 영역에 선언되어 있으므로 외부에서 마음대로 액세스할 수 있고 func 함수도 외부에서 호출할 수 있다. 보호 영역에 있는 k는 외부에서는 액세스할 수 없으며 클래스 내부나 상속된 파생 클래스만 액세스할 수 있다. 만약 비공개 또는 보호된 멤버를 액세스하면 컴파일러는 이런 시도를 에러로 처리한다.
액세스 지정자의 순서에 대한 제약은 없으며 필요에 따라 여러 번 중복될 수도 있다. private: 가 먼저 나오고 public: 이 나온 후 다시 private: 가 나와도 상관없다. 그러나 가급적 같은 액세스 속성을 가지는 멤버는 한 곳에 모으는 곳이 보기에 좋다. 통상 private, protected, public 순으로 선언한다.
액세스 지정자를 사용하여 특정 멤버를 숨길 수 있는 OOP의 기능을 정보 은폐라고 한다. 그렇다면 선언된 멤버를 외부에서 액세스하지 못하도록 숨겨야 하는 이유는 무엇이며 숨기면 어떤 이점이 있을까? 정보 은폐는 객체의 안전성을 확보하고 독립성을 높이는데 이는 무척 큰 주제이므로 27 장에서 따로 상세하게 다룰 것이다. 여기서는 액세스 지정자의 개념만 파악하도록 하자.



25-4.클래스

25-4-가. class

C++의 구조체는 멤버 함수를 포함할 수 있다는 면에서 C의 구조체에 비해 의미가 확장되었다. 이형 타입 변수의 집합인 구조체가 스스로의 동작을 정의할 수 있다는 것은 아주 중요한 의미가 있으며 객체 지향 구현을 위한 첫 걸음이라 할 수 있다. C++의 창시자인 스트로스트룹은 확장된 의미의 구조체에 뭔가 멋있고 새로운 이름을 붙여 주었는데 그것이 바로 클래스이다. 구조체라는 용어를 그냥 사용해도 별 문제는 없겠지만 C의 전통적인 구조체와 C++에서 확장된 구조체의 차이를 명확하게 구분하고 싶었고 그래서 이름을 바꾼 것이다.
별도의 이름을 붙였다는 것은 언어 창시자가 일종의 애정 표현을 한 것이다. 결국 OOP의 핵심이라고 할 수 있는 클래스는 "확장된 구조체"로 간단하게 정의할 수 있다. C의 전통적인 구조체는 타입이 다른 변수의 집합이며 C++의 확장된 구조체, 즉 클래스는 여기에 함수를 더 추가한 것이다. 구조체 선언문에서 struct 키워드를 class라는 새로운 키워드로 바꾸기만 하면 클래스가 된다. 앞 절에서 만들었던 확장된 구조체 Position을 클래스로 다시 선언해 보자. struct 키워드를 class로 바꾸고 선언문 선두에 public: 액세스 지정자만 추가하면 된다.

class Position
{
public:
     int x;
     int y;
     char ch;
     void OutPosition();
};

Pos2, Pos3의 예제를 이렇게 수정해도 잘 실행된다. 확장된 구조체와 클래스의 유일한 차이점은 멤버에 대한 디폴트 액세스 지정뿐이다. 구조체는 멤버 함수를 가질 수 없는 것으로 잘못 아는 사람들이 있는데 그렇지 않다. 구조체도 멤버 함수, 생성자, 파괴자를 가질 수 있고 상속도 가능하며 클래스가 쓰이는 모든 곳에 쓸 수 있다. 단지 아무런 액세스 지정없이 멤버를 선언할 때 이 멤버에 어떤 액세스 지정이 적용되는지만 다를 뿐이다. 다음 코드를 보자.

struct S
{
     int x;
     ....
};
S s;
s.x=1234;           // 가능
class C
{
     int x;
     ....
};
C c;
c.x=1234;                // 에러

아무런 액세스 지정없이 멤버 변수 x를 선언했다. 이 경우 구조체의 멤버 s.x는 public 속성을 가지며 외부에 공개되는데 비해 클래스의 멤버 c.x는 private 속성을 가져 외부로부터 숨겨진다. 구조체의 디폴트 액세스 지정은 public이고 클래스의 디폴트 액세스 지정은 private이다. 클래스는 객체의 안전성을 위해 외부에서 함부로 값을 건드리지 못하도록 멤버를 숨기는 경향이 있는데 비해 구조체는 가급적이면 멤버를 공개하는 경향이 있다. 구조체의 디폴트 액세스 지정이 public일 수밖에 없는 이유는 C언어와의 호환성을 유지해야 하기 때문이다. C에서 구조체의 멤버는 외부에서 자유롭게 액세스할 수 있으므로 C++의 구조체도 당연히 그렇게 해야 한다.
물론 디폴트가 그렇다뿐이지 양쪽 모두 명시적인 액세스 지정자로 멤버의 공개 여부를 변경할 수 있다. 다음 두 쌍의 선언문이 완전히 동일하다는 것을 확인하도록 하자. 디폴트 액세스 지정을 private로 바꾼 구조체는 클래스와 동일하며 반대로 디폴트 액세스 지정을 public으로 바꾼 클래스는 구조체와 동일하다. 이 외에 구조체와 클래스는 어떠한 차이점도 없다.
전통적인 구조체의 의미를 확장하면서 자연스럽게 클래스라는 새로운 개념을 소개하는데 이는 이미 알고 있는 구조체의 지식을 기반으로 클래스에 좀 더 쉽게 다가가도록 하기 위한 의도이다. 두 용어가 비록 의미는 같지만 지금부터는 멤버 함수를 가지는 확장된 구조체를 클래스로 부르기로 한다. 이미 습득한 구조체에 대한 문법, 예를 들어 .연산자, -> 연산자, 구조체 대입, 중첩 구조체 등도 클래스에 그대로 적용되며 이후 배우게 될 상속, 다형성, 연산자 오버로딩 등 클래스에 적용되는 모든 이론과 문법은 구조체에도 동일하게 적용된다.
클래스를 선언하는 문법은 다음과 같다. 구조체 선언문과 비슷하되 struct 키워드 대신 class 키워드를 사용하고 멤버 선언문 중간 중간에 액세스 지정이 온다는 것만 다르다. 구조체와 마찬가지로 선언문의 제일 끝에 세미콜론이 반드시 있어야 한다는 점을 주의하자. 문장 끝이 세미콜론으로 끝난다는 것은 C문법의 가장 기초에 해당하지만 숙련된 프로그래머도 실수하는 경우가 많다.

class 이름
{
액세스 지정:
     멤버 변수;
     멤버 함수;
     ....
};                          // 여기 세미콜론을 빼 먹지 말자!


구조체와 마찬가지로 멤버 개수에는 제한이 없으며 필요한만큼 얼마든지 많은 변수와 함수를 멤버로 포함할 수 있다. 멤버의 타입에도 물론 제한이 없다. int, long, double 등의 기본형 변수는 물론이고 배열, 구조체 등의 유도형과 다른 클래스형의 변수까지도 멤버로 포함될 수 있다. 구조체끼리 중첩이 가능하듯이 클래스끼리도 중첩 가능하고 열거형, typedef 등의 타입 정의도 포함될 수 있다. 확장된 구조체와 클래스는 사실상 같지만 관행상 멤버 함수를 가지는 경우는 클래스로 선언하는 것이 일반적이다.


25-4-나. 클래스는 타입이다

C++에서는 구조체의 태그가 타입으로 승격되어 태그로부터 바로 구조체 변수를 선언할 수 있다. 구조체가 하나의 타입으로 인정되는 것과 마찬가지로 클래스도 하나의 타입으로 취급된다. 클래스의 이름은 int, double, char 같은 기본형 타입과 동등한 자격을 가지며 사용 방법도 똑같다. C++은 클래스가 완전한 타입이 되기 위한 여러 가지 언어적 장치(생성자, 연산자 오버로딩 등)를 제공하는데 다음에 도표로 간략하게 정리해 보았다.

정수형
Complex 클래스
C++ 관련 문법
int i;
Complex C;
클래스의 이름이 타입과 같은 자격을 가진다.
int i=3;
Complex C(1.0, 2.0);
생성자선언과 동시에 초기화할  있다.
int i=j;
Complex D=C;
복사 생성자같은 타입의 다른 객체로부터 생성된다.
i=j;
D=C;
대입 연산자
i+j;
D+C;
연산자 오버로딩
i=3.14
Complex C(1.2);
변환 생성자변환 함수
3+i
1.0+C;
전역 연산자 함수와 프렌드

이런 여러 가지 문법에 의해 복소수 클래스인 Complex가 int와 완전히 똑같은 자격을 가질 수 있다. 세부적인 문법은 차차 구경하게 될 것이고 우선은 직관적으로 이해하기 쉬운 클래스를 예로 들어 보자.

class Complex
{
private:
     double real;
     double image;

public:
     멤버 함수들;
};

Complex 클래스는 제곱했을 때 음수가 되는 복소수를 표현한다. 복소수는 자연에 실제로 존재하지 않는 허수이지만 과학 계산에는 중간 과정 계산을 위해 꼭 필요하다. 수학에서 복소수는 보통 a+bi로 표현되는데 a를 실수부라 하고 bi를 허수부라 한다. Complex 클래스는 실수부의 값을 가지는 real과 허수부의 값을 가지는 image를 멤버로 가짐으로써 실세계의 복소수를 모델링하며 이 두 멤버의 조합으로 필요한 모든 복소수를 다 표현한다. Complex 클래스의 멤버 함수들은 초기화, 대입, 연산, 출력 등의 동작을 할 것이다.
C/C++은 언어 차원에서 복소수 타입을 지원하지 않지만 필요하다면 이런 타입을 클래스로 만들어 사용할 수 있다. Complex 클래스는 int, double과 마찬가지로 변수를 생성할 수 있으며 함수의 인수나 리턴값으로도 사용할 수 있다. 뿐만 아니라 연산자 함수를 작성하면 복소수끼리 더하고 빼고 곱하는 연산도 가능하다. int, double이 올 수 있는 위치이면 Complex도 언제나 올 수 있으며 그래서 클래스는 모든 면에서 타입이라고 할 수 있다.
C/C++이 클래스를 완전한 타입으로 인정하므로 클래스로부터 유도형 타입을 만들 수도 있다. T형이 있을 때 T형 포인터와 T형 배열이 항상 가능하므로 클래스의 배열이나 클래스형 변수를 가리키는 포인터도 만들 수 있다. 다음 예제는 Position 클래스의 배열을 선언하고 Position형 변수에 대한 포인터로 이 배열을 관리한다.

  : PositionClass
#include <Turboc.h>

class Position
{
private:
     int x;
     int y;
     char ch;

public:
     void InitRand() {
          x=random(80);
          y=random(24);
          ch=random('Z'-'A'+1)+'A';
     }
     void OutPosition() {
          gotoxy(x, y);
          putch(ch);
     }
     void ErasePosition() {
          gotoxy(x, y);
          putch(' ');
     }
};

void main()
{
     Position arPos[50];
     Position *pPos;
     int i;

     randomize();
     for (i=0;i<sizeof(arPos)/sizeof(arPos[0]);i++) {
          arPos[i].InitRand();
          arPos[i].OutPosition();
          delay(50);
     }

     delay(1000);
     pPos=arPos;
     for (i=0;i<sizeof(arPos)/sizeof(arPos[0]);i++) {
          pPos->ErasePosition();
          pPos++;
          delay(50);
     }
}

기존의 Position 클래스에 두 개의 멤버 함수가 추가되었다. InitRand 함수는 출력할 위치와 문자를 난수로 초기화하고 ErasePosition 함수는 이미 출력되어 있는 문자를 삭제한다. Position 클래스에 난수로 자신을 초기화하는 기능과 이미 출력한 문자를 지우는 기능이 추가되었다. main 함수의 코드를 분석해 보자. arPos는 Position형의 변수 50개를 담을 수 있는 클래스의 배열로 선언되었다. arPos가 메모리에 구현된 모양을 그려 보자면 다음과 같다.
개념적으로 구조체 배열과 동일하다. 첫 번째 i루프는 0~49까지 반복하면서 arPos[i]를 난수로 초기화하고 초기화된 arPos[i]의 OutPosition 함수를 호출하여 난수 위치에 문자를 출력한다. 여기까지 실행하면 화면에 50개의 문자들이 무작위 위치에 출력되어 있을 것이다.
두 번째 i루프는 ErasePosition 함수를 호출하여 이미 출력된 문자들을 삭제하는데 이때는 Position형의 포인터 변수 pPos를 사용했다. pPos=arPos 대입문은 pPos에 arPos 배열의 선두 번지인 &arPos[0]를 대입하며 pPos++은 pPos의 번지를 sizeof(Position)만큼 증가시켜 arPos 배열의 다음 요소로 이동한다. arPos[0]~arPos[49]까지 루프를 돌면서 ErasePosition 멤버 함수를 호출했으므로 출력된 문자들이 차례대로 사라진다.
보다시피 arPos 배열이나 pPos 포인터 변수가 동작하는 방식은 int형 배열이나 int *형 변수가 동작하는 방식과 완전히 동일하다. 필요하다면 Position형 포인터 배열이나 Position형 이차 배열, Position형 이중 포인터 등도 만들 수 있으며 이때 기존의 C 문법이 클래스에 대해서도 일관되게 적용된다. 클래스가 완전한 타입이기 때문이다.
클래스의 이름은 일종의 명칭이기 때문에 모델링한 대상을 잘 표현할 수 있는 이름을 자유롭게 붙일 수 있다. 이때 이 명칭이 클래스임을 명확하게 나타내기 위해 일종의 접두어를 붙이는 것이 관행이다. 마이크로소프트는 CString, CObject, CArray처럼 클래스 이름앞에 C를 붙이는데 여기서 C는 class의 첫 글자이다. 볼랜드는 주로 T를 붙여 TWindow, TPen과 같이 클래스의 이름을 작성하는데 이때 T는 Type의 첫 글자이며 클래스가 타입이라고 보는 관점을 잘 드러내고 있다. 이런 클래스 이름 작성법은 어디까지나 관행일 뿐이므로 꼭 지킬 필요는 없다.


25-4-다. 인스턴스

클래스는 어디까지나 타입일 뿐이지 그 자체가 정보를 저장하는 변수는 아니다. 구조체를 선언한다고 해서 구조체 변수가 생기는 것이 아닌 것처럼 클래스를 선언한다고 해서 실제로 값을 기억할 수 있는 메모리가 할당되지는 않는다. 클래스 선언은 어떤 타입의 어떤 멤버들이 포함되어 있는지를 컴파일러에게 알리는 역할만 할 뿐이며 클래스형의 변수를 선언해야 실제 메모리가 할당된다.

Position Here;
Complex C;

Position Here; 선언에 의해 Position 타입의 변수 Here가 메모리에 생성되며 이후 Here는 특정 위치를 가지는 문자 하나에 대한 정보를 기억할 수 있다. Complex C; 선언문은 복소수를 기억하는 변수 C를 생성한다. int i; 선언에 의해 정수형 변수 i가 생성되고 double d;에 의해 실수형 변수 d가 생성되는 것과 마찬가지이다. int i; 선언문에서 4바이트가 할당되는 대상은 i이지 int가 아니다. Position, Complex는 int, double과 같은 자격의 타입 이름이고 Here, C가 i, d같은 변수인 것이다.
선언문에 의해 생성된 클래스형의 변수를 인스턴스(Instance)라고 한다. 인스턴스는 클래스가 메모리에 구현된 실체이며 우리가 지금까지 변수라고 불러왔던 개념과 동일하다. Here는 Position형의 인스턴스이며 C는 Complex형의 인스턴스이다. 기본형에 대해서도 똑같은 용어를 적용할 수 있는데 i는 int형의 인스턴스, d는 double형의 인스턴스라고 할 수 있다. 클래스가 표현하는 정보를 실제로 기억하고 관리하는 주체가 바로 인스턴스이며 프로그래밍 대상은 클래스가 아니라 인스턴스이다. 한 글래스에 대해 여러 개의 인스턴스를 동시에 생성할 수 있다.

Position A,B,C;

이 선언에 의해 Position형의 인스턴스 A, B, C가 메모리에 각각 생성된다. int i,j,k; 선언에 의해 세 개의 정수형 변수가 동시에 생성되는 것과 같다.
이때 각 인스턴스들은 클래스에 선언된 멤버 변수를 각각 따로 가진다. 그래야 개별 인스턴스가 독립적인 정보를 저장할 수 있는데 A의 x, y, ch와 B의 x, y, ch가 다른 값을 가질 수 있어야 두 인스턴스가 각각 다른 화면 위치의 문자 정보를 가질 수 있을 것이다. 그래서 인스턴스의 실제 크기는 클래스에 선언된 모든 멤버 변수의 총 크기와 일치한다. Position 클래스에 포함된 x, y, ch의 크기 총합은 9바이트(sizeof(Position))이므로 A, B, C인스턴스들도 모두 9바이트(=sizeof(A))씩 차지할 것이다(실제로는 컴파일러의 정렬 기능에 의해 12바이트를 차지한다).
독립된 정보 저장을 위해 멤버 변수는 각 인스턴스들이 따로 가지지만 멤버 함수는 클래스에 속한 모든 인스턴스들이 공유한다. 인스턴스의 상태는 달라질 수 있지만 동작은 모두 동일하기 때문에 함수를 인스턴스별로 따로 가질 필요는 없다. A, B, C 인스턴스들이 기억하는 좌표와 문자는 달라지더라도 문자를 출력하는 코드는 항상 gotoxy(x,y); putch(ch); 이면 된다.
인스턴스의 다른 표현이 바로 오브젝트(Object)이다. 두 용어는 클래스형의 변수라는 같은 대상을 가리키지만 사용되는 문맥이 조금 다르다. 인스턴스는 클래스가 메모리상에 구현된 실체라는 뜻으로 사용되며 오브젝트는 프로그램을 구성하는 독립적인 부품이라는 뜻으로 사용된다. 똑같은 여자를 칭하는 말로 여성, 숙녀, 아줌마, 소녀 등등 여러 가지가 있는데 경우에 따라 쓰는 단어가 다른 것처럼 어감이 조금 틀린 같은 뜻의 단어일 뿐이다.
오브젝트를 우리말로 번역할 때는 객체(客體)라고 하는데 이 용어는 다소 직역한 느낌이 들어 부자연스럽다. 독립성을 강조하여 개체(個體)라고 번역하는 경우도 있는데 이 번역이 훨씬 더 자연스럽다. 그러나 객체라는 번역이 워낙 우세하기 때문에 이 책에서도 우세한 번역을 따르기 위해 객체라는 용어를 쓰기로 한다. 같은 대상을 칭하는 용어가 많아 조금 혼란스러운데 도표로 정리해 보면 다음과 같다.

영어
번역
의미
인스턴스(Instance)
실체
메모리에 구현되었다.
오브젝트(Object)
객체
독립성을 가진 부품이다.

결국 이 4가지 용어를 일반화해서 가장 알아듣기 쉽게 표현하면 변수라고 할 수 있다.

25-4-라.클래스의 예

클래스의 정의에 대해 알아보았는데 이제 클래스를 어디다 써 먹을 수 있을지 몇 가지 간단한 예를 구경해 보자. 실세계의 모든 사물들은 자신만의 독특한 속성을 가지고 있고 고유의 동작을 한다. 사람은 나이, 키, 피부색, 성별 등의 속성을 가지며 말한다, 걷는다, 먹는다 등의 동작을 할 수 있다. 노트북이나 자동차, 전화기 따위의 물건들도 마찬가지로 고유의 속성과 동작을 추출할 수 있을 것이다. 세상의 모든 사물은 속성과 동작 두 가지 특징으로 설명 가능하다.
그래서 속성은 멤버 변수로 나타내고 동작은 멤버 함수로 나타내는 식으로 클래스를 사용하여 실세계의 사물들을 정확하게 모델링할 수 있다. 학생, 고객, 여자, 괴물, 책, 집, 나무, 태양 등의 눈에 보이는 물건들은 물론이고 예금, 권한, 감정, 건강 등 보이지 않는 개념적인 사물까지도 클래스로 나타낼 수 있다. 현실 세계 사물의 특성을 추출하여 속성과 동작으로 표현하는 것을 추상화라고 한다.
앞에서 우리는 위치를 가지는 문자 클래스 Position과 복소수 클래스 Complex를 만들어 봤는데 이외에 필요한 모든 것들을 클래스로 선언하고 그 클래스의 객체를 만들 수 있다. 실세계의 사물들이 어떻게 클래스로 모델링되는지 여기서는 몇 가지 예를 더 들어 보도록 하자. 이 클래스들은 이 책에서 앞으로 객체 지향 프로그래밍을 연구하는 도구 및 예제 클래스로 종종 사용되므로 모양을 잘 기억해 두면 실습이 편해진다. 다음 클래스는 시간을 표현한다.

  : TimeClass
#include <Turboc.h>

class Time
{
private:
     int hour,min,sec;

public:
     void SetTime(int h,int m,int s) {
          hour=h;
          min=m;
          sec=s;
     }
     void OutTime() {
          printf("현재 시간은 %d:%d:%d입니다.\n",hour,min,sec);
     }
};

void main()
{
     Time Now;

     Now.SetTime(12,30,40);
     Now.OutTime();
}

시간이란 시, 분, 초로 구성되어 있는데 Time 클래스는 이 속성들을 hour, min, sec 멤버 변수로 기억한다. 시간을 설정하는 기능과 현재 저장된 시간을 문자열로 출력하는 두 가지 동작을 SetTime, OutTime 멤버 함수로 표현한다. 시간을 구성하는 요소가 세 개나 되기 때문에 단순한 정수에 비해서는 훨씬 더 다루기 까다로운데 이렇게 클래스로 정의해 놓으면 간편하게 사용할 수 있다.
Time형의 객체 Now를 선언하고 SetTime 멤버 함수를 호출하여 시간을 설정하였다. 이 시간을 출력할 필요가 있으면 OutTime 함수만 호출하면 된다. 필요하다면 시간값을 증가, 감소시키는 EllapseTime 함수와 시간끼리 비교하는 CompareTime 함수도 작성할 수 있다. 시간이라는 개념적인 대상을 하나의 객체로 다룰 수 있으므로 시간을 함수의 인수로 전달하거나 다른 구조체의 멤버로 포함시키는 것도 가능하다.
비슷한 방법으로 날짜를 다루는 Date형 클래스도 만들 수 있을 것이다. 날짜는 년, 월, 일의 요소로 구성되는데 월마다 일수가 다르고 2월의 경우 윤년의 영향을 받으므로 윤년을 계산하는 좀 더 복잡한 처리가 필요하다. 꼭 사물만 표현할 수 있는 것은 아니고 개념적인 것들도 모델링할 수 있다. 다음 클래스는 인치와 밀리미터 단위로 길이를 표현한다.

  : Length
#include <Turboc.h>

class Length
{
private:
     double mili;

public:
     void SetMili(double m) { mili=m; }
     double GetMili() { return mili; }
     void OutMili() { printf("길이 = %fmili\n",GetMili()); }
     void SetInch(double i) { mili=i*25.4; }
     double GetInch() { return mili/25.4; }
     void OutInch() { printf("길이 = %finch\n",GetInch()); }
};

void main()
{
     Length m;

     m.SetInch(3);
     m.OutMili();
}

인치나 밀리미터나 둘 다 길이라는 면에서는 동일하고 명확한 변환 공식이 존재하므로 두 속성을 모두 멤버로 포함할 필요는 없다. Length 클래스는 길이값 저장을 위해 밀리미터 단위를 기억하는 mili 멤버 변수만 가지며 인치는 mili에 25.4라는 상수를 곱하거나 나누어서 입출력한다. 멤버 함수들은 설정, 조사, 출력을 하되 인치 단위와 밀리미터 단위를 각각 다룰 수 있다. 그래서 설정할 때는 인치 단위를 쓰고 출력할 때는 밀리미터 단위를 쓰면 두 단위가 자동으로 변환된다. 예제의 코드는 3인치가 몇 밀리미터인지 조사하여 출력한다.

길이 = 76.200000mili

SetInch 함수는 인수 3에 25.4를 곱하여 3인치에 대한 밀리미터 값을 mili에 대입해 놓으며 이렇게 대입된 값을 OutMili로 출력하면 3인치가 몇 밀리미터인지 알 수 있다. 밀리미터로 넣고 인치를 구하는 반대의 변환도 물론 가능하다. 클래스가 인치와 밀리미터의 관계를 정확하게 알고 있고 멤버 함수들이 두 단위 사이를 자동으로 변환하므로 사용하는 측에서는 세부 구현에 신경쓸 필요없이 멤버 함수로 원하는 단위의 값을 넣고 빼기만 하면 된다.
클래스가 변환 공식을 내부에 포함하고 있으므로 1인치가 25.4밀리미터라는 것을 몰라도 이 클래스를 사용할 수 있다. 여기에 기능을 더 추가한다면 포인트, 트윕스, 마일 등의 단위도 같이 다룰 수 있도록 확장 가능하다. Length 클래스는 길이나 거리를 표현하는 여러 가지 단위와 복잡한 변환 공식을 멋지게 캡슐화하고 있는 것이다. 많은 단위를 다루려면 만들기는 번거롭겠지만 일단 완성되면 사용하기는 굉장히 편리하다.
이번에는 좀 더 실질적인 클래스 사용예를 들어 보되 실제 코드를 보이기는 어려우므로 가상의 코드만 보인다. 철수의 모험이라는 게임을 만들고 있는데 주인공 철수가 사이버 세계를 돌아다니며 흉칙한 적을 무찌르는 어드벤처 게임이다. 주인공 철수는 게임속에서 많은 속성을 가지며 다양한 동작을 할 수 있는데 이런 것들을 묶어서 다음과 같은 클래스로 표현한다.

class chulsoo
{
private:
     int x,y;                                      // 현재 위치
     int hp;                                  // 체력
     int shield;                             // 보호막
     int level;                               // 레벨
     double exp;                          // 경험치
     item items[MAXITEM];                   // 보유한 무기들

public:
     void walk();                          // 걷는다.
     void jump(short height);              // 점프한다.
     void turn(int dir);                        // 방향을 바꾼다.
     BOOL attack(int what);           // 적을 신나게 공격한다.
     void defence();                          // 방어한다.
     BOOL hurt(int fromwhom);     // 공격을 당한다.
     BOOL die();                          // 너무 많이 맞아서 죽는다.
};

멤버 변수들은 어드벤처 게임의 캐릭터가 보편적으로 가져야 하는 상태값들을 저장한다. 체력이나 경험치같은 단순 타입의 멤버도 있고 item같은 다른 클래스의 객체에 대한 배열이 포함될 수도 있다. 멤버 함수들은 게임 속에서 철수의 동작을 표현하는데 철수를 화면에 그리고 상태를 변경하는 코드가 작성될 것이다. walk는 걸어다니는 철수를 보여주고 위치를 옮기며 attack은 적을 공격하도록 하고 공격 결과에 따라 경험치와 레벨을 올린다. hurt는 맞아서 아파하는 철수를 그리고 보호막과 체력을 감소시키며 체력이 0이 되면 die 함수를 호출하여 철수를 죽게 만든다.
게임의 주인공인 철수에 대한 모든 것들을 chulsoo 클래스로 표현했는데 게임을 완성하려면 이 외에도 적이나 지도 등의 클래스들이 더 필요할 것이다. 이런 클래스를 완벽하게 만드는 데는 시간과 노력이 많이 필요하다. 하지만 일단 만들어지면 chulsoo CS; 선언문 하나로 철수 객체를 만들 수 있으며 main에서는 철수에 대한 세부 구현은 더 이상 신경쓸 필요없이 다른 객체와 철수와의 관계만 관리하면 된다. CS는 자신의 상태를 스스로 기억하고 관리하며 동작까지 할 수 있는 독립된 객체이다.
클래스는 게임의 부품인 객체들을 표현하며 main에서 객체끼리의 상호작용 방식을 어떻게 정의하는가에 따라 게임의 내용이 달라진다. 철수가 적1을 공격하면 체력이 감소할 것이고 일정 이하로 체력이 떨어지면 철수가 사망할 것이다. 적들은 철수를 지능적으로 공격하며 아이템은 철수에 의해 소유되고 소비된다. 이런 식으로 개발된 전형적인 프로그램이 바로 스타크래프트라는 게임인데 마린은 공격하고 메딕은 치료하는 등 서로간의 관계에 의해 게임이 실행된다.
체들을 먼저 만들고 객체들을 조립하여 프로그램을 완성하므로 상향식 개발이라고 한다. 각 클래스를 여러 개발자가 분담해서 개발할 수도 있고 만들어진 객체는 재사용도 가능하다. 클래스를 작성할 때는 보통 헤더 파일에 클래스의 선언을 작성하고 같은 이름의 구현 파일에 멤버 함수의 본체를 작성한다. 클래스를 정의하는 모듈은 클래스 이름과 같은 파일명을 쓰는 것이 편리하다. 예를 들어 Time 클래스를 작성한다면 다음과 같이 Time.h에 클래스를 선언하고 Time.cpp에 멤버 함수의 본체를 정의한다.
이렇게 헤더 파일과 구현 파일에 클래스의 선언과 정의를 나누어 놓으면 클래스를 사용하는 모듈에서 헤더 파일만 인클루드하면 되므로 재사용하기 편리하다. 이 클래스가 아주 유용해서 다른 프로젝트에서 쓰고 싶다면 헤더 파일, 구현 파일 한 쌍만 복사하면 된다. 이런 클래스 정의 관행은 너무 일반적인 것이기 때문에 가급적이면 이 관행을 따르는 것이 좋다. 위 프로젝트는 한번씩 만드는 실습을 해 보는 것이 좋은데 조금이라도 규모가 있는 프로그램을 작성하려면 이 관행을 따라야 하므로 미리 익숙해질 필요가 있다.
, 이 책에서는 매 실습마다 클래스를 위해 별도의 모듈을 새로 만드는 것이 번거로와 편의상 메인 파일에서 선언, 구현을 다 하고 있다. 어디까지나 실습의 편의를 위해서 한 파일에 통합하는 것뿐이므로 실제 프로젝트를 할 때는 클래스별로 헤더 파일과 구현 파일을 만들기 바란다.





댓글 없음:

댓글 쓰기

[Effective C++] 항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 인라인 함수의 아이디어는  함수 호출문을 그 함수의 본문으로 바꿔치기하자는 것  남발했다가는 코드의 크기가 커질 게 뻔하다. 인라인 함수로 부풀려진 ...