[TCPL] C++ 둘러보기

 

The C++ Programming Language

2장 워밍업 : C++ 둘러보기

 

2.1 C++ 이란 무엇인가?

 

모든 용도에 쓸수 있으며 시스템 프로그래밍에 강한 프로그래밍 언어

 

C++의 기본 성격

  • C를 기본으로 발전시킨 언어
  • 데이터 추상화를 지원
  • 객체 지향 프로그래밍을 지원
  • 일반화 프로그래밍을 지원

2.2 C++는 프로그래밍 패러다임이 한두 개가 아니다

 

객채 지향 프로그래밍은 ‘훌륭한’ 프로그램을 짜게 해 주는 패러다임(paradigm)중 하나 이다.

어떤 프로그래밍 언어가 어떤 프로그래밍 스타일을 지원한다고 말할 수 있으려면 그 스타일을 편하게(쉽고, 안전하고, 효율적으로) 쓸 수 있도록 하는 언어적 장치가 뒷받침 되어야 한다.

 

프로그래밍 스타일을 지원하기 위한 조건

  • 모든 기능은 깔끔하고 세련되게 언어에 녹아 있어야 함.
  • 별도의 기능이 필요할 것 같은 문제에도 기존의 기능들을 조합해서 해결할 수 있어야 함.
  • 이름만 번지르르 하고 특수한 용도로 마련된 기능은 최대한 적어야 함.
  • 기능의 구현은 그 기능을 쓰지 않은 프로그램엔 큰 오버헤드를 주지 않도록 해야 함.
  • 언어에서 제공되는 기능 중 확실한 몇 개만 알아도 프로그램을 작성할 수 있어야 함.

 

C++의 프로그래밍 스타일

절차적 프로그래밍 – 객체 지향 프로그래밍 – 일반화 프로그래밍

2.3 절차적 프로그래밍

 

절차적 프로그래밍의 패러다임

– 문제 해결에 적합한 처리 절차를 정하고 찾을 수 있는 최선의 알고리즘을 구사한다.

 

대게 함수에 인자를 전달하고 함수로부터 값을 반환 하는 메커니즘이 제공되는 것이 보통이다.

다음은 제곱근을 계산하는 함수 이다.

 

2.3.1 변수의 산술 연산

 

모든 이름과 표현식에는 어떤 연산이 수행될지 정해주는 타입이란 것이 붙는다.

 

C++의 다양한 타입들 중 몇 가지

타입 크기 사용법
bool 불(Boolean) 1 byte True, false
char 문자(character) 1 byte ‘k’, ‘h’, ‘j’, ‘4’
int 정수(integer) 2 or 4 bytes 1, 342, 6342
double 배정밀도 부동소수점실수 8 bytes 3.14, 29348.0

*컴퓨터나 컴파일러에 따라 크기가 다를 수 있음

 

이런 타입들에 대해 계산을 처리할 때 다음과 같은 산술 연산자와 비교 연산자가 사용된다.

산술 연산자 비교 연산자
+ 덧셈 == 같은가?
뺄셈 != 다른가?
* 곱셈 < 작은가
/ 나눗셈 > 큰가?
% 나머지 <= 작거나 같은가?
>= 크거나 같은가

 

대입 연산과 산술 연산을 기본 제공 타입을 가지고 수행 할 경우에는, 다른 타입을 쓰더라고 자유롭게 섞어 쓸 수 있도록 적절한 타입 변환이 내부 적으로 이루어진다.

다음은 타입 변환이 이루어지는 예제이다.

<TEXTAREA class=c name=code row=”10″ col=”60″>#include <iostream.h> void main () { double a = 2.2; //a는 double형으로 소수점 까지 표시 가능 int b = 7; //b는 int형 cout<<“a : “<<a; //a의 초기값 cout<<“nb : “<<b; //b의 초기값 cout<<“nnn”; a = a + b; //a 에 a와 b를 더한 값을 넣는다.(2.2+7) cout<<“nafter —– a = a + b :”; cout<<“na : “<<a; //a 는 9.2가 되어 있음 cout<<“nb : “<<b; //b 는 7 cout<<“nnn”; b = a * b; //b 에 a와 b를 곱한 값을 넣는다.(9.2 * 7) cout<<“nafter —– b = a * b :”; cout<<“na : “<<a; //a 는 9.2 cout<<“nb : “<<b; //b 는 64.4가 되어야 하지만 int타입이므로 타입 변환이 이루어져 64가 출력 된다. } </TEXTAREA>

b의 int 타입에 맞게 변환 되었다.

 

2.3.2 조건검사 및 루프

 

C++ 에서는 분기(selection)와 루프처리(looping)를 나타낼 수 있는 구문들이 있다.

다음은 간단한 입력을 받아 그 응답을 나타내는 불(bool)값을 표시하는 예제이다.

<TEXTAREA class=c name=code row=”10″ col=”60″>#include <iostream.h> bool accept( ) { char answer = 0; //문자 변수 cout<<“Do you want to stop (y or n) ?n”; //질문 출력 cin>>answer; //대답을 읽는다. if(answer == ‘y’) return true; //’y’ 이면 1 (true)을 반환 return false; //’y’가 아니면 0 (false)을 반환 } void main( ) { bool true_false = accept(); //bool변수에 accept 함수값을 저장 cout<<true_false; //true, false에 따라 1, 0 을 출력 } </TEXTAREA>

위의 예제 에서 accept 함수를 n을 받아들였을 경우까지 고려하면 조금 더 짜임새 있게 된다.

<TEXTAREA class=c name=code row=”10″ col=”60″>bool accept2() { char answer =0; cout<<“Do you want to stop (y or n) ?n”; cin>>answer; switch(answer) { case ‘y’: return true; //’y’ 이면 1 (true)을 반환 case ‘n’: return false; //’n’ 이면 0 (false)을 반환 default: cout<<“I’ll take that for a no.n”; return false; //’y’. ‘n’둘 다 아닐 때는 메시지와 함께 0 (false)을 반환 } } </TEXTAREA>

위 예제의 switch문은 상수의 집합에 대해 어떤 값을 검사하는 조건문이다. ‘case 상수’는 서로 겹치지 않아야 하고 검사하는 갑이 어떤 상수에도 해당 하지 않으면 default쪽으로 처리 흐름이 오게 된다.

 

위 예제에 루프를 적용한다면 사용자가 몇 번의 시도를 할수록 고칠 수 있다.

<TEXTAREA class=c name=code row=”10″ col=”60″>bool accept3( ) { int tries = 1; //시도 횟수 카운트를 위한 변수 while (tries<4) //tries 값이 4보다 작을때 아래를 처리 { cout<<“Do you want to stop (y or n) ?n”; char answer = 0; cin >> answer; switch(answer) { case ‘y’: return true; //’y’일때 true값을 반환하고 나온다. case ‘n’: return false; //’n’일때 false값을 반환하고 나온다. default: cout<<“Sorry, only ‘y’ or ‘n’n”; //’y’ , ‘n’ 둘다 아닐때 보내는 메세지 tries = tries++; //tries변수에 1을 더한다음 다시 처음으로 돌아간다. } } cout<<“I’ll take that for a no.n”; return false; //1~3까지 3번이 반복되어 tries변수가 4가 되면 while문을 빠져나와 메시지를 보낸 후 false를 반환한다. }</TEXTAREA>

위 예제에서 while문은 조건이 거짓(false)이 될 때까지 실행하게 만드는 구문이다.

 

2.3.3 포인터와 배열

 

배열의 선언

 

<TEXTAREA class=c name=code row=”10″ col=”60″>char k[4]; //문자 4개의 배열 </TEXTAREA>

– char타입 4개가 선언 되었다. ( k[0], k[1], k[2], k[3] )

포인터의 선언

 

<TEXTAREA class=c name=code row=”10″ col=”60″>char *p; //문자를 가리키는 포인터 </TEXTAREA>

– char 타입의 객체가 저장된 메모리 주소를 가진다.

<TEXTAREA class=c name=code row=”10″ col=”60″> p = &k[2]; // p는 k의 3번째 원소를 가리키는 포인터 </TEXTAREA>

– 단항 연산자인 &은 주소 ( address-of ) 연산자이다.

 

아래는 한 배열에서 다른 배열로 4개의 원소를 복사 하는 예제이다.

<TEXTAREA class=c name=code row=”10″ col=”60″>#include <iostream.h> void main( ) { int k1[4] = {1,2,3,4}; //복사할 변수 선언과 함께 데이터 입력 int k2[4] = {0,0,0,0}; //복사 받을 변수 선언과 함께 0으로 초기화 int i; cout <<“Before copy————“; for (i = 0; i<3; i++) //복사 하기전의 배열을 보여주는 for문 { cout<<“nnk1[“<<i<<“] : “<<k1[i]; cout<<“nk2[“<<i<<“] : “<<k2[i]; } for (i=0; i<4; i++) k2[i] = k1[i]; //k1 배열을 k2배열로 복사 하는 for문 cout <<“nnnAfter copy————“; for (i = 0; i<3; i++) //복사한 후의 배열을 보여주는 for문 { cout<<“nnk1[“<<i<<“] : “<<k1[i]; cout<<“nk2[“<<i<<“] : “<<k2[i]; } } </TEXTAREA>

위 예제에 쓰인 for문은 i를 0으로 설정하고 이 값이 4보다 작은 동안 i번째의 원소를 복사하고 i를 증가시킨다

2.4 모듈화 프로그래밍

 

문제 해결에 사용하는 데이터를 만들어 두면, 그 데이터를 조작하는 프로시저가 자연스럽게 하나 둘 생기게 되는데 이것들이 모인 것을 가리켜 모듈(module)이라 한다.

 

모듈화 프로그래밍의 패러다임

– 문제 해결에 적합한 모듈을 정한다.

– 프로그램을 쪼개어 데이터가 모듈 속에 숨겨지도록 한다.

 

이런 패러다임을 가리켜 데이터 은닉 원칙(data-hiding principle)이라고도 한다.

 

모듈화 프로그래밍을 잘 보여주는 대표적인 예로 스택이 있는데, 스택의 구현은 다음과 같이 이루어지는 것이 보통이다.

  1. 스택 조작을 위한 사용자(프로그래밍) 인터페이스를 제공한다 [push, pop]
  2. 스택을 나타내는 데이터 (배열로 나타낼수도 있다)는 1.에서 제공한 인터페이스로만 접근(access) 할 수 있도록 한다.
  3. 반드시 사용 전에 스택의 초기화가 이루어지도록 한다.

 

다음은 namespace를 이용 하여 Stack이란 이름의 모듈에 대한 사용자 인터페이스를 선언한 결과와 이 인터페이스의 사용 예제이다.

<TEXTAREA class=c name=code row=”10″ col=”60″>namespace stack //인터페이스 (모듈 선언부) { void push(char); char pop( ); } void main( ) { stack::push(‘c’); if(stack::pop( ) != ‘c’) error(“impossible”); } </TEXTAREA>

stack:: 이란 한정표시(qualification)은 그 뒤의 push( )pop( )이 stack namespace의 것이라는 뜻이다. 이로써 똑같은 push( )pop( )을 쓰더라도 이름 혼동이 일어나지 않는다

stack 모듈의 정의(구현)코드는 (선언부와) 별도로 컴파일된 파일에서 받아 올 수 있다.

<TEXTAREA class=c name=code row=”10″ col=”60″>namespace stack { const int max_size = 200; char v[max_size]; int top = 0; void push(char c) { /* overflow를 점검하고 c를 push한다.*/ } char pop( ) { /* underflow를 점검하고 pop한다. */ } } </TEXTAREA>

이 모듈에서 사용자 코드가 데이터표현부와 분리 되어 있음에 주목하자. 이는 stack::push( )stack pop::( )이 있기 때문에 가능하다.

C++의 namespace 에는 어떤 선언이든 넣을 수 있다. 이 모듈은 스택을 나타내는 수단중 하나일뿐이다.

2.4.1 분할 컴파일

 

여러 개의 하위 구성 요소를 모아서 하나의 프로그램을 만들 때 분할 컴파일 (separate compilation)을 사용 한다.

 

모듈 인터페이스를 지정하는 선언부는 ‘이것은 인터페이스’라는 것을 잘 알려 주는 이름을 가진 파일에 넣어 둔다.

<TEXTAREA class=c name=code row=”10″ col=”60″>namespace stack //인터페이스 { void push(char); char pop( ); } </TEXTAREA>

스택을 조작하는 함수를 선언해 둔 인터페이스를 stack.h라는 헤더파일(header file)에 넣는다. 사용자는 이 파일을 include 해서 사용자 코드로 가져 온다.

<TEXTAREA class=c name=code row=”10″ col=”60″>#include “stack.h” //인터페이스를 가져 온다. void main( ) { stack::push(‘c’); if(stack::pop( ) != ‘c’) error(“impossible”); } </TEXTAREA>

일관성을 유지할 수 있도록, stack 모듈의 구현부를 가지고 있는 파일 쪽에서도 인터페이스를 include한다.

<TEXTAREA class=c name=code row=”10″ col=”60″>#include “stack.h” //인터페이스를 가져온다. namespace stack // 스택의 데이터 표현 { const int max_size = 200; char v[max_size]; int top = 0; } void push(char c) { /* overflow를 점검하고 c를 push한다.*/ } char pop( ) { /* underflow를 점검하고 pop한다. */ } </TEXTAREA>

이 모듈을 사용 하는 코드는 제 3의 파일 에 둔다. stack.h안에 만들어 놓은 인터페이스 정보를 여러 군데서 끌어오고 있지만, 그렇다고 해서 파일들 사이에 어떤 종속 관계가 있는 것은 전혀 아니다. stack.h를 불러 오는 모든 파일을 각각 따로 컴파일 된다.

 

프로그램을 잘 작성 하려면

  1. 모듈화의 정도를 극대화 한다.
  2. 언어에서 제공되는 특징을 반영하여 모듈관계를 논리적으로 표현한다.
  3. 효과적인 분할 컴파일을 위해 모듈들을 별도의 파일에 분산 시켜 구성한다.

2.4.2 예외 처리

 

모듈의 집합으로 설계 되어 있는 프로그램 에서는 오류 처리도 모듈 중심으로 해 주어야 한다. stack 모듈을 예를 들어 stack을 초과하는 문자를 push( ) 할 때 어떻게 대처 해야 할까? 해결책은 Stack모듈 개발자 쪽에서 overflow가 나는 경우를 찾아 사용자에게 알려주는 것이다.

<TEXTAREA class=c name=code row=”10″ col=”60″> namespace stack //인터페이스 { void push(char); char pop( ); class overflow { }; //overflow 라는 예외 상황을 나타내는 타입 } </TEXTAREA>

stack::push( ) 는 오버플로가 발생 했다 판단되면 적절한 예외 처리 코드를 호출 한다.

<TEXTAREA class=c name=code row=”10″ col=”60″> void stack::push(char c) { if ( top == max_size ) throw overflow( ); // c를 push한다. } </TEXTAREA>

throwstack::overflow 타입을 가진 예외를 처리 하는 블록으로 제어권을 넘긴다. 이 블록은 stack::push( )를 직간접적으로 호출한 함수 어딘가에 있어야 한다. 이 ‘제어권 넘기기’를 위해 C++환경은 호출부의 문맥을 제대로 찾아 갈수 있도록 호출 스택에 대한 풀기 동작을 적절히 수행한다. 쉽게 말해 여러 군데에 return을 하는 것과 마찬가지이다.

<TEXTAREA class=c name=code row=”10″ col=”60″> void main( ) { //.. try { //여기서 일어난 예외의 처리는 아래에 정의된 처리자가 맡는다. while(true) stack::push(‘c’); } catch (stack::overflow) { //스택 overflow에 대해 적절한 조치를 취한다. } //… } </TEXTAREA>

이 코드에서 while 루프는 무한정 돌게 된다. 따라서 stack::overflow에 대한 처리자가 있는 catch 절은 스택이 넘쳐서 stack::push( )에서 throw가 실행 된 후에야 움직인다.

예외 처리 메커니즘은 오류 처리 코드를 좀더 조직적이고 읽기 좋게 하기 위해 마련된 것이다.

2.5 데이터 추상화

 

규모가 큰 프로그램 중에 제대로 된 것은 모두 ‘모듈화가 잘 되었다’ 라는 특징을 기본적으로 가지고 있다. 모듈을 통해 사용자 정의 타입의 ‘모양새’를 제공하는 방법과, 이 방법을 썼을 때 생기는 문제를 해결 하기 위해 사용자 정의 타입을 직접 정의 하는 방법을 알아보자.

2.5.1 모듈로 타입을 정의하는 방법

 

모듈 방식으로 프로그래밍을 하다 보면, 한 가지 타입을 가진 데이터는 모두 타입 관리자 모듈(type manager module) 같은 것을 써서 모아 두는 경우가 생길 수 밖에 없게 된다. 앞에서 본 stack 모듈을 여러 개 쓰고 싶은 경우, 필요한 인터페이스가 달린 스택 관리자를 정의해서 쓰는 것이다.

<TEXTAREA class=c name=code row=”10″ col=”60″>namespace stack { struct Rep; //스택 데이터를 나타내는 Rep. 이것의 정의는 어디에 있을 것이다. typedef Rep& stack; stack create( ); //스택하나를 새로 만든다. void destroy(stack s); //s를 삭제한다. void push(stack s, char c); //c를 s에 push한다. char pop(stack s); //s를 pop한다. } </TEXTAREA>

선언문 struct Rep; 에서 Rep은 타입 이름에 해당 하지만, 타입 자체는 나중에 정의되도록 내버려 둔 상태이다.

두 번째로 나온 선언문 typedef Rep & stack; 은 ‘Rep에 대한 참조자(reference)’에 stack이란 이름을 붙인다. 이 stack::stack은 기본 제공 타입의 변수와 똑같이 쓸 수 있다.

<TEXTAREA class=c name=code row=”10″ col=”60″> struct Bad_pop { }; void f( ) { stack::stack s1 = stack::create( ); //스택 하나를 새로 만든다. stack::stack s2 = stack::create( ); //스택을 하나 더 만든다. stack::push(s1,’c’); stack::push(s2,’k’); if (stack::pop(s1) != ‘c’) throw Bad_pop( ); if (stack::pop(s2) != ‘k’) throw Bad_pop( ); stack::destroy(s1); stack::destroy(s2); }</TEXTAREA>

Stack의 인터페이스를 구현 하는 방법은 여러 가지가 있을 수 있겠지만, 어떤 방법을 쓰든 사용자는 신경 쓰지 않아도 된다.

구현방법의 한 가지 예 : 스택 몇 개를 미리 만들어 할당해 놓고, stack::create( )는 미사용 스택에 대한 참조자를 끄집어 내는 일만 하게 한다. stack::destroy( )는 사용 되는 것들은 다시 ‘미사용’ 이라고 표시해 두어, 나중에 stack::create( )가 재사용할 수 있도록 하는 것이다.

<TEXTAREA class=c name=code row=”10″ col=”60″> namespace stack { //실제 스택 데이터의 표현부 const int max_size = 200; struct Rep { char v [max_size]; int top; }; const int max = 16; Rep stacks[max]; bool used[max]; typedef Rep& stack; } void stack::push(stack s, char c) { /* 오버플로를 점검하고 c를 push한다. */ } char stack::pop(stack s) { /* s가 언더플로가 났는지 점검하고 pop한다. */ } stack::stack stack::create( ) { //미사용 중인 Rep을 꺼내고 이것을 ‘사용 중’이라고 표시한 후, 초기화하고 참조자를 반환한다. } void stack::destroy(stack s) { /* s를 “미사용” 상태로 바꾼다. */ } </TEXTAREA>

지금까지의 코드를 보면, 스택을 표현하는 타입이 인터페이스 함수들로 둘러싸인 형태이다. 이렇게 에둘러 만들어진 ‘스택 타입’의 동작 방식을 좌우하는 요인은 여러 가지로서, 조금씩 달라진다. 그러나 이 방법은 이상적인 설계에서 약간 어긋나 있다. 유사 타입을 사용하는 방법이 실제 데이터의 내부사항에 따라 판이하게 달라질 수 있고, 사용자는 이로 인해 혼란스러워 한다.

2.5.2 사용자 정의 타입

 

이 문제에 대한 해결책으로 C++에서는 기본 제공 타입과 똑같은 동작 방식을 가지는 타입을 사용자가 직접 정의할 수 있도록 하였다. 이런 타입을 가리켜 추상 데이터 타입 (abstract data type)이라고들 한다.

 

사용자 정의 타입을 사용한 프로그래밍의 이론적인 틀

  • 문제 해결에 필요한 타임들을 정한다.
  • 각타입에 대해 동작 연산을 완벽하게 제공한다.

 

사용자 정의 타입의 예는 유리수나 복소수 따위를 나타내는 산술 타입이 대표적이다. 다음 예제를 보자.

<TEXTAREA class=c name=code row=”10″ col=”60″> class complex { double re, im; public: complex(double r, double i) { re=r; im=i; } //두 개의 스칼라로부터 복소수를 만든다 complex(double r) { re=r; im=0; } //하나의 스칼라로부터 복소수를 만든다. complex( ) { re = im = 0; } friend complex operator + (complex, complex); friend complex operator – (complex, complex); //이항 연산 friend complex operator – (complex, complex); //단항 연산 friend complex operator * (complex, complex); friend complex operator / (complex, complex); friend complex operator == (complex, complex); //같음 비교 friend complex operator != (complex, complex); //다름 비교 //… }; </TEXTAREA>

complex란 이름의 클래스(사용자 정의 타입)는 복소수 데이터의 표현부와 조작에 필요한 연산들을 모두 가지고 있다. 여기서 데이터표현부는 외부에 가려져 있다. (re와 im) 이 두 데이터는 complex 클래스의 선언부에서 지정된 함수들만 접근할 수 있다. 이들 ‘지정된’ 함수는 다음과 같이 정의 될 수 있다.

<TEXTAREA class=c name=code row=”10″ col=”60″> complex operator + ( complex a1, complex a2) { return complex (a1.re + a2.re, a1.im + a2.im); } </TEXTAREA>

 클래스와 똑같은 이름을 가진 멤버 함수를 볼 수 있는데, 이것을 생성자(constructor)라고 한다. complex 클래스에 정의된 생성자는 double하나를 받아 complex객체를 초기화하는 것, double 두 개를 받아 초기화하는 것, 그리고 기본 값으로 초기화하는 것, 이렇게 세 가지이다.

 

complex 클래스의 사용법은 다음과 같다.

<TEXTAREA class=c name=code row=”10″ col=”60″> void f(complex z) { complex a = 2.3; complex b = 1/a; complex c = a+b*complex(1,2.3) //… if (c != b) c = -(b/a) + 2*b; } </TEXTAREA>

 C++ 컴파일러는 complex 타입의 복소수를 피연사자로 사용하는 연산자를 읽어, 그 클래스에서 정의된 함수 호출 코드로 바꾸어 준다.

2.5.3 구체 타입

 

앞서 보았던 stack 타입이 complex 타입처럼 만들어졌다고 생각 해보자. 약간 쓸만하게 다듬으려면 스택 내부의 원소 개수를 인자로 받아들이도록 다음과 같이 정의 할 수 있을 것이다.

<TEXTAREA class=c name=code row=”10″ col=”60″> class stack { char*v; int top; int max_size; public: class underflow { }; // 예외로 사용하기 위함 class overflow { }; // 예외로 사용하기 위함 class bad_size { }; // 예외로 사용하기 위함 stack(int s); // 생성자 stack( ); // 소멸자 void push(char c); char pop( ); }; </TEXTAREA>

‘생성자’라는 이름이 붙은 stack(int)는 이 클래스의 객체가 메모리에 만들어질 때마다 호출되는 함수 이다. 이 함수가 맡은 일은 객체의 초기화이다. 이 클래스의 객체가 유호 범위를 벗어날 경우에 끝 마무리 작업이 필요 하다면 소멸자(destructor)라는 함수를 준비해 두면 된다.

<TEXTAREA class=c name=code row=”10″ col=”60″> stack::stack(int s) // 생성자 { top = 0; if (s0 || 10000<s) throw bad_size( ); // ‘||’의 뜻은 ‘또는’이다. max_size = s; v = new char[s]; // 자유 저장공간에 원소 할당 } stack::~stack( ) // 소멸자 { delete[ ] v; // 공간을 다시 쓸 수 있도록 할당한 원소를 해제한다. } </TEXTAREA>

여기서 생성자가 할 일을 새로 생성된 stack 변수를 초기화하는 것이다. 사용자는 기본제공 타입의 변수를 대하듯이 stack 객체를 만들어 쓰면 되는 것이다.

<TEXTAREA class=c name=code row=”10″ col=”60″> stack s_varl(10); //10개의 원소를 가진 전역 스택 void f(stack& s_ref; inti) //stack 객체에 대한 참조자 { stack s_var2(i); // i 개의 원소를 가진 지역 스펙 stack* s_ptr = new stack(20); //자유 저장공간에 할당된 stack객체 포인터 s_var1.push(‘a’); //변수를 통해 접근하는 방법 s_var2.push(‘b’); s_ref:push(‘c’); //참조자를 통해 접근하는 방법 s_ptr -> push(‘d’); //포인터를 통해 접근 하는 방법 //… }</TEXTAREA>

 

이 stack 타입은 이제 동작 방식이 int나 char 등의 기본 제공 타입과 똑같아졌다.

앞에서 본 complex와 여기서 본 stack과 같은 타입을 가리켜 구체 타입(concrete type) 이라고 한다. 추상 타입(abstract type)과 반대되는 의미인데, 이 추상 타입은 인터페이스 밖에 없기 때문에 사용자 코드가 구현코드로부터 더 분리 된다.

2.5.4 추상 타입

 

stack 이란 타입을 나타내는 문제를 모듈 형태의 ‘유사 타입’에서 진짜 타입으로 옮기면 좋은 점도 있지만 안타깝게도 한 가지 성질을 잃게 된다. 바로 데이터표현부가 사용자 인터페이스와 떨어지지 않은 상태로 남는다는 점이다.

사용하는 타입이 자주 바뀌지 않는다든지 지역 변수를 사용해서 깔끔하고 효율적으로 작업할 수 있는 경우에는 구체 타입이 이상적일 때가 많다. 하지만 스택을 사용하는 프로그래머와 스택 구현부와의 관계를 완전히 끊어 버려야 할 때도 있는데, 이 경우에는 stack 클래스로도 어림없다. 해결책은 단 하나, 인터페이스와 데이터표현부를 떼어 놓고 지역 변수를 포기하는 것이다.

우선, 인터페이스를 이렇게 정의한다.

<TEXTAREA class=c name=code row=”10″ col=”60″> class stack { public: class underflow { }; class overflow { }; virtual void ush(char c) = 0; virtual char pop( ) = 0; }; </TEXTAREA>

virtual 이란 키워드가 붙은 함수는 이 클래스로부터 파생된 클래스에서 나중에 재정의될 수 있다는 것을 의미 한다. 정리하면, 이 stack은 push( )pop( ) 함수를 ‘구현’ 하게 될 미지의 클래스에게 인터페이스, 즉 틀만을 제공하는 것이다.

이 stack은 어떻게 사용 될까? 다음을 보자.

<TEXTAREA class=c name=code row=”10″ col=”60″> void f(stack& s_ref) { s_ref.push(‘c’); if(s_ref:pop( ) != ‘c’) trow bad_pop( ); } </TEXTAREA>

f( )는 실제 구현부를 전혀 모르고도 stack 인터페이스를 사용 하고 있다. 이렇게 다른 클래스가 쓸 수 있는 인터페이스를 제공하는 클래스를 가리켜 다형성 타입(polymorphic type)이라고 한다.

stack이 인터페이스가 됐으므로, stack이 구체 타입이었을 때 가지고 있던 것들을 전부 떼어 내면 구현부를 구성할 수 있을 것이다.

 

—–

 

2.5.5 가상 함수

 

f( )에 들어있는 s_ref:pop( )은 어떻게 해당 클래스의 함수 정의부로 정확히 해석(resolve)되는 것일까? h( )에서 호출되는 f( )에서는 list_stack::pop( )이 호출 되어야 하고 g( )에서 f( )가 호출되는 경우에는 array_stack::pop( )이 호출되어야 한다. 적절한 함수 정의를 찾아내려면, 런타임에 호출될 함수를 가리키는 정보가 stack객체 어딘가에 들어 있어야 한다. 이 매커니즘은 일반적으로 컴파일러 쪽에서 virtual 키워드가 붙은 함수의 이름을 함수 포인터 테이블의 색인번호로 바꾸는 식으로 만들어져 있다. 이 테이블을 가리켜 가상 함수 테이블(virtual function table) 이라고 하며, 짧게 vtbl이라고 한다.

2.6 객체 지향 프로그래밍

 

사용자 정의 타입만 썼을 때 생기는 문제 그리고 이 문제를 뛰어넘는 해결책인 클래스 계통이란 것을 살펴보자

 

2.6.1 구체 타입의 결정적 약점

 

구체타입은 일단 한번 만들어지고 나면 외부에서 통할 수 있는 길이 극히 한정되어 있다. 예를 들어 그래픽 시스템에 사용 하려고 shape란 타입을 하나 정의 한다고 가정해 보자. 그리고 다음과 같은 타입도 있다고 가정하자.

<TEXTAREA class=c name=code row=”10″ col=”60″> class Point{/*…*/}; class Color{/*…*/}; </TEXTAREA>

도형(shape)을 나타내는 타입을 정의해 보자

<TEXTAREA class=c name=code row=”10″ col=”60″> enum kind {circle, triangle, square}; //나열자 타입 class shape { kind k; //도형 타입 필드 point center; color col; // … public: void draw( ); void rotate(int); //… }; </TEXTAREA>

‘도형 타입 필드’란 이름이 붙은 k라는 변수가 있어야 하는 이유는 draw( )와 rotate( )에서 자신이 처리하는 도형이 어떤 것인지를 구분할 수 있어야 하기 때문이다.

 

….

 

2.6.2 클래스 계통

 

문제의 핵심은, 도형마다 가지고 있는 일반적인 성질과 그 도형만의 고유한 성질이 명확하게 갈라지지 않았다는 데 있다. 이 차이를 언어로 나타내고 이용하는 것이 바로 객체지향 프로그래밍이다.

일반성과 특수성의 구분을 이루어 내는 답은 바로 상속 메커니즘이다. 우선 도형마다 공통적으로 가진 성질을 정의하는 클래스를 만든다.

<TEXTAREA class=c name=code row=”10″ col=”60″> class Shape { point center; color col; //… public: point where( ) {return center;} void move(Point to) { center = to; /*…*/ draw( ); } virtual void draw( ) = 0; virtual void rotate(int angle) = 0; //… }; </TEXTAREA>

호출 인터페이스는 정의할 수 있지만 구현코드는 바로 제공하지 않아도 되는 함수를 가상 함수로 선언된다. (draw( ), rotate( ) )

이렇게 클래스를 정의해 두었으면 도형에 대한 포인터를 담은 벡터를 조작하는 일반함수를 만들어서 일괄적으로 처리할 수 있다.

<TEXTAREA class=c name=code row=”10″ col=”60″>void rotate-all(vetor<Shape*>& v, int angle) //v의 원소가 되는 각 도형을 angle도 만큼 회전 { for (int i = 0; i<v.size(); ++i) v[i] -> rotate(angle); }</TEXTAREA>

이제 특정한 도형을 정의한다. ‘이것은 도형이다’라고 알려 줌과 동시에 개별적인 성질을 지정해 주는 것이다.

<TEXTAREA class=c name=code row=”10″ col=”60″> class circle : public shape { int radious; public: void draw( ) {/*…*/} void rotate(int) { } //아무것도 안 하는 함수 }; </TEXTAREA>

파생 클래스는 기본 클래스가 가진 모든 것을 물려 받기 때문에, 이런 클래스의 관계를 이용하는 것을 상속 이라고 한다.

 

객체 지향 프로그래밍의 이론적 틀은 다음과 같다.

  • 문제 해결에 필요한 클래스가 무엇인지 정한다.
  • 클래스에 대해 모든 함수 집합을 만들어 준다.
  • 클래스 사이의 공통적인 부분을 상속관계를 써서 드러낸다.

2.7 일반화 프로그래밍

 

일반화 프로그램의 이론적인 틀은 다음과 같다.

  • 문제 해결에 필요한 알고리즘을 정한다.
  • 이들 알고리즘이 여러 가지의 타입과 자료 구조에 대해 동작 할 수 있도록 알고리즘을 매개변수화한다.

 

2.7.1 컨테이너

 

C++에서는 앞에서 만들던 ‘문자만 담는 스택’ 타입을 ‘어느 것이든 담는 스택’ 타입으로 바꿀수 있는 방법이 있는데, 타입을 템플릿(template)으로 만들고 char라는 특정한 타입을 탬플릿 매개변수로 바꾸는 것이다.

<TEXTAREA class=c name=code row=”10″ col=”60″> template <class T> class stack { T*v; int max_size; int top; public: class underflow{ }; class overflow{ }; stack(int s); //생성자 ~stack( ); //소멸자 void push(T); T pop( ); }; </TEXTAREA>

 

클래스 이름의 앞에 붙은 template<class T> 라는 접두사가 바로 T를 이 타입의 매개 변수로 만들어 주는 부분이다.

멤버 함수의 코드 정의는 이전의 것과 크게 다르지 않으나 T를 고려해서 이루어진다.

<TEXTAREA class=c name=code row=”10″ col=”60″> template<class T> void stack<T>:: push(T c) { if (top == max_size) throw overflow( ); v[top] = c; top = top + 1; } template<class T>T stack<T>:: pop( ) { if ( top == 0 ) throw underflow( ); top = top -1; return v[top]; } </TEXTAREA>

이렇게 만든 스택(템플릿)은 다음과 같이 사용 한다.

<TEXTAREA class=c name=code row=”10″ col=”60″> stack<shar>sc(200); //200개의 문자를 담는 스택 stack<complex>scplx(30); //30개의 복소수를 담는 스택 stack<list<int> > sli(45); //45개의 정수 리스트를 담는 스택 void f( ) { sc.push(‘c’); if(sc.pop( ) != ‘c’) throw bad_pop( ); scplx.push(complex)(1.2)); if (scplx.pop( ) != complex(1,2)) throw bad_pop( ); } </TEXTAREA>

여기서 보인 스택처럼 리스트, 벡터, 맵 같은 것들도 템플릿으로 정의할 수 있다. 이렇게 어떤 타입의 원소를 모아서 담을 수 있는 클래스를 가리켜 컨테이너 클래스라고 한다. 편하게 컨테이너(container)라고 불러 주기 바란다.

2.7.2 일반화 알고리즘

 

C++ 표준 라이브러리에는 위에서 이야기한 컨테이너라고 불리는 것들이 들어 있으며, 없는 컨테이너는 직접 만들 수 있다.

컨테이너가 정확히 어떻게 생겼는지 몰라도 그 컨테이너를 조작할 수 있는 일반화된 방법을 찾아야 한다. 여기에는 여러 가지 방법이 있을 수 있겠으나 표준 C++ 라이브러리에서 제공하는 컨테이너와 비수치형 알고리즘에서 취한 방법은 원소들이 순서대로 연결된 시퀸스라는 개념으로 컨테이너를 모델링하고 이 시퀸스를 반복자라는 것으로 조작하게 만드는 것이었다.

하나의 시퀸스엔 반드시 시작과 끝이 존재한다. 반복자는 시퀸스 안의 우너소를 가리키고 자신이 참조하는 원소의 다름 것으로 옮겨갈 수 있는 연산을 제공한다. 일반화 프로그래밍에서 시퀸스의 끝은 시퀸스의 마지막 원소의 바로 다음을 가리키는 반복자를 뜻한다. 또한 ‘반복자가 가리키는 원소에 접근하기’와 ‘반복자를 다음 원소로 옮기기’ 같은 기본적인 연산에 대한 표준 방식도 이미 정해져 있다. 우리들이 쉽게 접근하고, 증가 연산자인 ++를 써서 다음 원소로 옮겨 가게 한것이다. 그렇기 때문에 이런 식의 코딩이 가능하다.

<TEXTAREA class=c name=code row=”10″ col=”60″>template<class in, class out> void copy(in from, in too_far, out to) { while (from != too_far) { *to = *from; //반복자가 지금 참조하고 있는 원소를 복사한다. ++to; //대상이 되는 곳의 반복자를 증가시킨다. ++from; //바탕이 되는 곳의 반복자를 증가시킨다. } } </TEXTAREA>

C++에서 기본적으로 제공되는 배열과 포인터 타입은 반복자 연산에 정확히 맞아 떨어지기 때문에 다음과 같이 작성할 수 있다.

<TEXTAREA class=c name=code row=”10″ col=”60″> char vc1[200]; //200개 문자를 담는 배열 char vc2[500]; //500개 문자를 담는 배열 void f( ) { copy(&vc1{0}, &vc1[200], &vc2[0]); } </TEXTAREA>

vc1의 첫 원소부터 끝 원소까지를 vc2에 복사하는 코드로, 대상위치는 vc2의 처음부터 시작한다.

hk69.pdf

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url=""> 

This site uses Akismet to reduce spam. Learn how your comment data is processed.