이하는 제가 예전에 올렸었던 "C 의 개념"에서 이어지는 후속편 입니다.
C++ 에서 C 의 부분을 제외한 C++ 의 부분을 다루고 있습니다.
한번 읽어 보시구요.. 감상평 부탁드립니다..^^;;
Chapter 2. C++ 의 개념
이번 장에서는 앞서 배운 C 의 연장선에서 추가적으로 C++ 에 대해서 알아본다. 먼저 개요적인 내용들로 시작하여 캡슐와 상속성 다형성 그리고 그 밖에 요소들에 대해서 알아본다. 최대한 간략히 요약된 내용들이지만 이 정도만 알아도 MFC 를 사용하는 데 있어서는 충분하다. 그럼 하나씩 알아보도록 하자.
Section 1.
C++을 시작하며..
집필에 있어서의 고민과 재밋는 사실
C++ 파트의 집필을 기획하면서 고민이 많았다. 본서는 MFC를 목표로 한다. 다시말해서 MFC를 배워서 사용하는 데 필요한 지식만을 다룬다. MFC를 사용하는 데 꼭 필요하지 않는 고급내용은 일단 뒤로 뺀다는 말이다. 헌데 재밋는 사실은 자신이 클래스를 작성하려면 상속성이니 다형성이니 하는 개념을 빠삭하게 꿰고 있어야 하나 단지 MFC를 사용만 하려는 경우에는 개념정도만 알아도 된다는 사실이다. 이러한 이유로 본서의 기획의도에 의거해 필요하다고 판단되는 정도의 지식만을 논할 생각이다.
개발방법론의 변천
컴퓨터 소프트웨어의 재활용은 과거의 C 에서도 있었다. 바로 함수 라이브러리인데 어떤 하나의 기능을 하는 함수들을 만들어 놓고 이러한 함수들을 가져다 쓰는 방식으로 프로그래밍을 할 수 있었던 것이다. 필자는 한때 잘 모를 때는 이렇게 과거의 C 에서도 함수 라이브러리의 재활용이 있었으므로 재활용이 이미 C에서도 충분하지 않았나 하며 C++의 생산성에 대해 그 필요성을 의심했던 적이 있었다. 헌데 C++을 제대로 알가가면서 그 재활용성에 대한 의구심은 감탄으로 변하게 되었다. 멤버 변수 뿐만이 아니라 관련있는 멤버 함수까리 한데 묶는 캡슐화로 멤버 변수와 멤버 함수가 유기적으로 작용하여 멤버 변수는 멤버 함수가 스스로 처리하도록 되어 (함수단위가 아닌) 독립적인 객체로서의 단위로 재활용을 할 수 있게되었고 이러한 클래스 단위의 재사용성은 상속성이라는 계층구조적 라이브러리로 인해 C의 라이브러리와는 비교할 수 없는 재활용성을 얻을 수 있게 되었다. 게다가 다형성으로 인해 융통성까지 갖게된 C++ 에서의 개발 생산성은 그만큼 편리해지고 강력하다.
그리고 C++ 에 대한 소개
C++ 은 C 언어를 완전히 포함한다. 이러한 특징은 다른 관점에서 보면 C에, C++만의 객체지향적 특징이 추가된 모습으로 볼 수 있다. 이러한 이유로 우리가 이제까지 배운 C의 연장선에서 C++에서 추가된 요소들만 추가적으로 공부함으로서 처음부터 완전한 C++을 공부할 수 있다. 앞으로 우리는 이제까지 배운 C와 공통된 부분의 C++ 은 제외하고 추가되는 C++ 의 부분만을 공부하게 될 것이다. 그럼에 있어서 본서의 기획 의도에 의거해 필요한 개념정도의 지식만을 배울것인데 이렇게 추가되는 C++의 내용은 크게 다음의 다섯가지로 볼 수 있겠다.
캡슐화
상속성과 계층구조
다형성을 이루는 함수 중복과 연산자 중복 그리고 가상함수
기본툴즈 생성자와 파괴자 그리고 메모리관리등
고급툴즈 템플릿과 예외처리등
이러한 내용들 중 우리가 앞으로 공부할 내용은 다음과 같다.
개요(클래스와 객체)
캡슐화
상속
다형성들..
기본 툴즈
이렇게 전반적인 개관을 먼저 언급하는 이유는 본서의 기획의도 상 많지 않은 지식을 다루게 될 텐데 행여나 C++의 내용중 빠뜨린 것이 없을까하는 독자의 염려가 있을까 싶어서이다. 필요한 지식의 정도만을 다루지만 중요한 개념은 빠뜨리지 않고 다룬다. 이점 독자 여러분이 안심하고 따라오길 바란다
객체지향 프로그래밍
프로그래밍이란 현실 세계에서의 대상 개념들을 컴퓨터 상에서 구현하는 것이다. 예를 들자면 데이터베이스는 명함철이나 서류 양식들을 컴퓨터 상의 프로그램으로서 구현하는 것이다. 그리고 피부에 와 닫는 예를 들자면 3D 게임이나 시뮬레이션을 들 수 있다. 이러한 사물을 컴퓨터 상에서 표현하는데 있어서 객체지향 프로그래밍은 현존하는 프로그래밍 방식 중에서 최신의 방식이며 가장 이상적인 방식이다. 객체지향 프로그래밍에서 표현하고자 하는 대상은 객체 즉 클래스로 표현 즉 구현되게 된다. 그리고 클래스는 멤버변수와 멤버함수로 이루어지게 된다. 즉 객체지향 프로그래밍에서 표현하고자 하는 대상은 클래스로 구현하게 되며 이러한 구현은 멤버변수와 멤버함수로 이루어지게 된다는 말이 된다. 멤버변수는 어떠한 대상의 상태 즉 속성을 나타내고 구현하기 위해 사용이 된다. 멤버함수는 사물의 동작을 나타내고 구현하기 위해 사용되게 된다. 이렇게 두 가지 요소로 모든 사물을 나타낼 수 있으며 그렇기에 클래스로 모든 사물을 표현하고 구현할 수 있는 것이다. (사실 이벤트라는 요소도 관련이 있기는 하지만 이는 외부에서 제공되게 된다.)이렇듯 객체지향 프로그래밍에서는 표현하고자 하는 모든 사물이나 대상을 객체 즉 클래스로 구현한다. 그리고 이러한 구현에 있어서 중심이 되는 개념이 있는데 그것은 바로 상속이라는 것이다. 왜 상속이 객체지향 프로그래밍에서 구현의 기반이 되냐면 상속이라는 구조를 통해 사물이나 대상을 구현하게 되기 때문이다. 예를 들면 생물이라는 대상을 표현하는 클래스에서 상속을 받아 동물과 식물이라는 대상을 나타내는 클래스를 만들고 동물이라는 대상을 나타내는 클래스에서 상속을 받아 포유류라는 대상을 나타내는 클래스를 만들고 또다시 이 포유류라는 대상을 나타태는 클래스에서 상속을 받아 개나 고양이라는 대상을 나타내는 클래스를 만든다는 식이다. 이렇게 상속을 받아 대상을 구현하게 되면 재사용성과 효율성이라는 엄청난 효과를 볼 수가 있다. 그렇게 때문에 객체지향적인 구현에 있어서 상속성을 사용한 구현은 매우 중요하며 핵심적인 위치를 차지하게 된다.
클래스
C++ 을 초기에는 C with class 라고 불렀다고 한다. 그만치 C++ 에서 클래스는 C++ 의 핵심적인 특징이 된다. 클래스는 구조체와 비슷하다. 사실 C++ 에서 구조체와 클래스의 차이는 디폴트 접근 지정자의 상태가 구조체에서는 public 이고 클래스는 private라는 것 뿐이다. C++ 에서는 구조체에도 멤버 함수를 포함시킬 수 있다. 하지만 보통 구조체는 멤버 변수들만을 포함시켜 사용한다. 멤버 함수를 포함시키는 경우에는 클래스를 사용한다. 어찌 되었던 클래스는 구조체와 개념적으로 같기 때문에 클래스 대표명 아래 멤버 변수들을 갖게 된다. 그러나 구조체와는 달리 멤버함수를 추가적으로 포함시켜 사용한다. 사실 구조체가 멤버 변수 중심이라면 클래스는 멤버 함수 중심이라고 볼 수 있을 것이다. 결론적으로 클래스는 C++ 의 객체지향 프로그래밍의 중심이 된다. 관련있는 요소들을 한 데 묽는 캡슐화를 기반으로, 상속성을 그 중심으로 그리고 다형성에 이르기까지 객체지향 프로그래밍에 있어서 클래스는 핵심이자 중심이 되는 것이다. 뒤에서 배우겠지만 클래스의 주축을 이루는 핵심적 요소 개념은 바로 캡슐화와 상속이다. 이들은 이후에서 이어질 설명에서 살펴보게 될 것이다.
클래스와 객체
C++ 에서 클래스란 용자가 정의하는 하나의 데이터 타입이다. 극단적으로 이야기 해서 C에서 기본적으로 제공되는 데이터 타입인 int나 char 와 같이 취급된다. 이러한 사용자가 정의한 데이터 타입으로는 먼저 C에서 배운 구조체가 있으며 클래스는 이러한 구조체로 만드는 데이터 타입이나 기본적으로 제공되는 데이터 타입과 같이 생각해도 무방하다. 어떤 자료형의 변수(객체)를 생성하듯 어떤 클래스형의 객체(변수)를 선언하여 변수를 사용하듯 객체를 사용하고 함수의 매개변수로 사용한다거나 하는 등 기본 자료형의 변수를 사용하는 것과 같이 생각하고 사용해도 된다. 또한 이렇게 클래스(형)를 사용하여 마치 기본 자료형을 사용하듯 배열이나 포인터로도 사용할 수도 있다.그리고 기본적으로 제공되는 데이터 타입(자료형)의 변수(객체)를 선언하듯이 (이러한 변수의 선언이란 어떤 형에 의해 메모리에 공간을 생성하는 것이다.)클래스 형으로 객체를 선언하면 클래스 형이 정의하는 틀에 의거해 메모리상에 클래스가 정의하는 구조의 객체(변수)를 생성할 수 있는 것이다. 우리가 이제까지 C 에서 사용해 왔던 변수라는 메모리상의 공간은 C++에서의 객체와 비슷하다고 볼 수 있다. 기본자료형에 의거해 변수를 메모리 상에 생성했듯이 클래스가 정의하는 구조대로 메모리상에 객체를 생성할 수 있는 것이다.
OOP언어의 세가지 특성
다른 모든 객체지향 언어들과 마찬가지로 C++은 세 가지 중요한 특징을 가지고 있다. 이것들은 바로 캡슐화, 상속성, 다형성 인데, 이제부터 하나씩 자세히 알아가 보자.
Section 2.
캡슐화(Encapsulation)
캡슐화의 개념
캡슐화란 한마디로 말해 관련있는 것끼리 한데 묶는 것이다. 우리는 이전에 구조체에 대해서 배운바 있다. 구조체는 데이터를 일괄적으로 관리하기 위해 사용된다. 여기서 일괄적이라 함은 연관된 각각의 변수를 따로 따로 사용하지 않고 하나의 타이틀 아래에 한데 묶어서 분류하고 모아서 사용한다는 것이다. 이렇게 캡슐화를 하여 한데 묶어 사용을 하게되면 관리하기가 편해지고 보기에도 간단 명료해 지게 된다. 이러한 구조체의 특성은 데이터베이스에서도 잘 나타나는데 관련있는 여러 개의 필드를 하나의 테이블 명 아래에 저장하는 방법 즉 관련된 것들 끼리 묶어서 저장하는 방법은 데이터를 효율적으로 관리할 수 있게 하는 것이다.
이렇게 구조체에서 관련있는 멤버변수들을 하나의 형 이름 아래에 한데 묶는 것과 같이 C++의 클래스에서는 ,좀 더 발전되어, 서로 관련있는 멤버 변수들 뿐만 아니라 멤버 함수들까지 클래스라는 하나의 형이름 아래에 유기적으로 한 데 묶는다. 이렇게 관련있는 함수까지 한데 묶어서 관리하면 멤버변수와 멤버 함수가 서로 유기적으로 작용하여 더욱 시너지를 내게 된다.
즉 이렇게 관련있는 함수까지 멤버로 묶어 놓게 되면(캡슐화하게 되면)
하나의 클래스로 캡슐화된(관련있는 변수들끼리 묶어놓은) 멤버 변수들은
그 클래스에 캡슐화된(멤버 변수와 관련있는 함수들을 함께 묶어 놓은) 멤버 함수들에 의해서 관리되고 처리되게 된다. 이에 대해 좀더 부연설명을 하면 캡슐화된 멤버 변수들을 중심으로 이 멤버 변수들을 관리 및 처리하는 함수들을 한 데 묶음으로 인해 멤버 변수들과 멤버 함수들이 유기적으로 동작하여 독립적으로 작동하는 하나의 독립적인 개체를 만들 수 있게 된다.
즉 관련된 모든 기능을 한 클래스 내에서 독립적으로 수행할 수 있게 되어
하나의 독립적으로 제 구실을 하는 객체를 만들 수 있다는 말이다. C 의 함수들이 회로 기판의 소자라 한다면 C++ 에서의 클래스는 회로 기판이라 할 수 있는 것이다. 이로 인해 마더 보드에 회로 기판인 도터보드를 연결하여 컴퓨터를 만들듯이 이러한 클래스들을 연결하여 프로그램을 보다 쉽게 만들게 되는 것이다.
그리고 캡슐화에 포함되는 다른 하나의 뜻이 있는데 이는 '감추기'이다.
감추기라함은 감춰지도록 설정된 멤버(변수나 함수)를 외부에서 접근하여 사용할 수 없도록 하는 것인데 이는 관련 있는 것들끼리 묶어 놓으면서
내부적으로만 사용하는 비공개 멤버와 외부에서 사용할 수 있는 공개 멤버로 크게 나누어 놓게 된다. 이렇게 나누면 비공개되는 멤버를 외부에서 잘못되게 접근하는 사용을 막을 수 있으며 공개되는 멤버만을 사용하게 되므로 외부에서 보기에 간단해 지게 된다.
이렇게 멤버를 감추는 이유는 다음과 같다. 가령 오디오의 예를 들어 설명을 해 보겠다. 오디오가 동작하는 내부는 복잡하지만 오디오를 사용하는 사용자는 그러한 복잡한 동작에 대해 알 필요가 없다. 단지 외부에 보이는 버튼들 등의 조작 사용법만 알면되는 것이다. 캡슐화의 감추기는 이러한 예에서 볼 수 있는 원리와 같다.객체의 내부적으로 작동되는 복잡한 것들을 객체를 사용하는 사람이 알 필요는 없다.단지 객체를 사용하는 데 필요한 것만 알면 되는 것이다. 즉 외부에서 사용되기 위해 공개되는 멤버만을 사용하면 된다라는 말이다.이렇게 내부적으로만 사용되는 멤버 변수와 멤버 함수를 감춤으로 인해서 객체를 사용하는 외부에서 보기에 객체가 간단해 보이는 효과를 낼 수 있다. 왜냐면 객체를 사용하는 데 있어서 공개되는 멤버만을 염두에 두고 사용하면 되기 때문이다. 이는 클래스의 외부에선 비공개 되는 멤버를 접근하여 사용할 수 없기 때문에 그런 것이다.
그리고 또 한가지 만일 사용자가 오디오의 내부를 함부로 만진다면 안전을 보장할 수 없을 것이다. 이와 마찬가지로 사용자가 사용하는 부분 외에는 내부를 함부로 건들지 못하게 하는 것이 안전하다. 이와 같이 비공개되는 멤버는 외부에서 접근할 수 없게 된다. 이렇게 관련있는 멤버 변수들끼리 묶어놓고 또한 이러한 비공개되는 멤버변수들을 직접 접근하여 사용하지 못하게 함으로서 기존의 전역 변수의 폐혜를 막을 수 있게 되어 변수들을 관리하는 데 있어서 안정성과 효율성이 높아지게 된다.
캡슐화의 실제 코드로의 구현
그럼 C++ 에서 캡슐화를 구현하는 클래스에 대해서 알아보자.
C++ 에서 클래스는 다음과 같은 형식으로 사용된다.
class class-name
{
private :
protected :
public :
}
위의 클래스 선언 형식에 대해 설명을 하겠다. 먼저 class 라는 키워드를 사용한 뒤 클래스의 이름을 쓴다. 이 "class라는 키워드"는 뒤에 잇따라 나오는 클래스 이름이 새로운 자료형 이름이라는 것을 알려주기 위한 것이다. class 키워드 다음으로 클래스 이름이 나오게 되는데 이는 새로운 자료형의 자료형 이름이 된다.그리고 그 다음으로 중괄호가 나오며 그 속에 멤버 변수와 멤버 함수들이 오게 되는데 이러한 멤버들이 비공개 되는지 아니면 공개 되는지를 명시하기 위해 접근 지정자라는 것을 사용하여 멤버의 접근 속성을 지정하여 나누게 된다. 이러한 접근지정자는 C++ 에서는 세가지가 있는데 이것들이 바로 private, protected, public 이다.
private 접근 지정자는 멤버 변수와 멤버 함수가 해당 클래스 내부에서만 사용될 수 있고 클래스의 외부에서는 이러한 멤버 변수와 멤버 함수에 접근할 수 없다는 것을 의미한다.
protected 접근 지정자로 선언된 멤버를 클래스의 외부에서 사용할 수 없는 것은 private와 같지만 유일하게 private와 다른 점이 있는데 그것은 파생되는 클래스의 멤버 함수에서는 접근이 가능하다는 것이다.
public 접근 지정자는 public으로 선언된 멤버 변수와 멤버 함수들을
클래스의 다른 멤버들이 내부적으로 사용할 수 있을 뿐만 아니라 클래스의 외부(즉 다른 클래스 등)에서도 이러한 멤버 변수와 멤버 함수를 접근하여 사용이 가능하다는 것을 나타낸다. 보통 다른 클래스의 멤버함수에서 클래스의 객체를 생성한 다음에 public 으로 선언되어 있는 멤버함수를 호출하여 사용함으로서 클래스의 외부에서 클래스의 public 멤버를 사용하게 된다.
그리고 C++의 클래스에서는 아무런 접근지정자가 없는 맨 처음 부분은 디폴트로 private 이다.
이렇게 중괄호 안에 들어가는 함수들은 반드시 함수원형(prototype)으로 선언되어 있어야 한다.
그리고 한가지, 앞으로 우리가 MFC를 사용하게 되면서 멤버 변수나 멤버 함수를 클래스에 추가할 때 어떤 영역 즉 어떤 접근 지정자의 속성을 갖게 할 것인지의 판단 기준은 앞의 캡슐화의 개념에 의거 내부적으로 사용되는 멤버 변수나 함수는 protected속성으로 하고 (private는 잘사용되지 않는다. private보다는 protected를 많이 사용한다.) 외부에서 접근하여 사용해야하는 함수등은 public으로 하도록 한다.
그리고 기본적으로 멤버변수는 비공개(protected)이나 개중에 클래스의 외부에서 멤버변수에 접근해야 할 경우가 있는데 이러한 경우에는 멤버변수라 해도 공개(public)으로 설정하기도 하나 이보다 더 객체지향적인 방법은 멤버변수를 반환하는 공개 멤버 함수를 만들어 놓고 이 공개 멤버함수를 이용하여 멤버변수를 사용하는 방법이다. 헌데 이렇게 하면 좀 더 어려워 지게 되므로 일단은 쉽게하는 방편으로 멤버 변수를 공개(public)로 하여 사용해도 좋다.
Section 3.
상속성(Inheritance)
상속성의 개념
상속성이라함은 먼저 만들어져 있는 클래스의 모든 특성(기능-멤버 변수와 멤버 함수)을 그대로 상속(이어)받아, 새로운 기능을 추가하여 새로운 클래스를 생성하는 것이다. 이때 먼저 만들어져 있는 클래스를 기본 클래스라고 하고 새롭게 만들어지는 클래스를 파생 클래스라고 한다. 상속성의 본질적인 파워는 상속성을 사용하여 "계층구조적으로" 클래스 라이브러리를 만들어 놓고 이것으로 부터 프로그래밍을 하는 데 있다. 이렇게 계층구조적으로 클래스들을 만들어 놓음에 있어서 기반 클래스는 앞으로 파생클래스가 갖게 될 일반적이고도 공통적인 특성을 갖도록 만들어 놓고 파생클래스는 이러한 기반클래스의 기능을 상속받고 게다가 자신만의 독특한 기능을 추가하도록 하여 만드는 것이다. 이렇게 미리 계층구조적인 기반 클래스들을 많이 만들어 놓고 이것으로 부터 프로그래밍을 하면 비슷한 기능을 가지는 프로그램을 매번 처음부터 만들때와 비교하여 엄청난 생산성을 낼 수 있는데 그 이유는 기반 클래스의 기능에 해당하는 기능들이 중복되는 문제가 해결되기 때문이다. 이는 미리 기반클래스들의 기능을 계층구조적으로 체계적으로 만들어 놓고 이로부터 프로그래밍을 하게 되면 이미 만들어져 있는 기반클래스의 기능은 매번 다시 만들 필요 없이 그대로 상속을 받고
파생클래스에서 추가적으로 필요한 기능만을 추가하여 만들면 되기 때문이다.
이렇게 미리 만들어져 있는 기반클래스에 파생클래스만의 독특한 기능을 추가하기만 하면 되는 프로그래밍 방식의 커다란 생산성 때문에 미리 계층구조적으로 기반클래스들을 만들어 놓고 이로부터 프로그래밍을 할 수 있도록 하는 것이 바로 클래스 라이브러리이며 이것이 바로 우리가 목표로 하여 배우고 있는 MFC인 것이다. 사실 MFC를 사용하면서 잘 못느끼면서도 가장 강력한 기능이 바로 이 상속성이다. 소스코드 상으로는 간단히 기반클래스를 상속받는다는 한 줄 짜리 문장만을 보게되지만 이러한 간단한 문장 이면의 기능은 실로 엄청나서 우리는 만들게 될 프로그램의 고유한 기능만을 추가시키기만 하면 그 복잡한 윈도 프로그램을 만들어 낼 수 있게 되는 것이 이 바로 상속성의 이면에 있는 방대하고도 체계적인 클래스 라이브러리가 있기 때문인 것이다. 실제로 우리는 많은 경우에 있어서 핸들러 정도의 처리만으로 프로그램을 만들게 된다.
상속성의 실제 코드로의 구현
상속성을 구현하기 위한 형식은 다음과 같다.
[ class ] [ 파생 클래스 이름 ] [ : ] [ 접근 지정자 ] [ 기반 클래스 ]
{
멤버들..
};
이렇게 한 줄만 정의를 해 놓으면 기반클래스의 모든 기능을 이어 받게 되는 새로운 클래스를 만들게 되는 것이다. 보기에는 간단해 보이는 한 줄이지만 실로 막강한 기능인 것인데 [ 접근 지정자 ]에는 사실, private, protected, public 등이 모두 들어 갈 수는 있으나 MFC에서는 거의 모든 경우에 예외 없이 public을 사용하여 파생클래스를 만든다. (다른 접근 지정자들은 거의 안사용한다고 봐도 좋다. 그러므로 MFC를 사용하는 수준에서는 이정도만 알아도 상관 없다.) 이 public 접근 지정자를 사용하여 파생클래스를 만들면 기반 클래스의 모든 멤버를 상속받으면서 기반 클래스의 모든 멤버의 접근 지정자의 속성을 그대로 이어 받는다. 가령 기반클래스에서 private로 지정되었었던 멤버는 private 속성으로 이어 받고 (엄밀히 말하면 이 기반클래스에서 private로 지정되었던 멤버는 파생클래스의 멤버가 접근할 수는 없다.-이 말은 이해 못해도 되는 어려운 설명임.)기반 클래스에서 protected로 지정되어 있었던 멤버는 파생클래스에서도 protected속성을 가지게 된다. 그리고 기반 클래스에서 public 속성 이었던 멤버들은 파생클래스에서도 public 속성이게 된다. 다음의 예는 위의 '형식'의 실례이다.
class derivedclass : public baseclass
{
멤버들..
};
위의 예에서도 볼 수 있듯이 상속을 받아 클래스를 만들기 위해서는 먼저 class 키워드를 쓴 뒤, 새로 만들 파생클래스의 이름을 적고, ' : ' 를 사이에 넣은 뒤, 접근 지정자를 적는다. 그리고 마지막으로 기반클래스의 이름을 적는다. 이렇게 한 줄만 적어주면 기반 클래스의 모든 멤버변수와 멤버 함수를 상속받는 새로운 파생 클래스를 만들 수 있게 된다. 그럼 우리는 파생된 클래스에 멤버변수와 멤버 함수를 추가하기만 하면되는 것이다. 더 이상 처음부터 시작할 필요는 없다. 기반클래스의 모든 멤버를 상속 받았으므로..
Section 4.
다형성(Polymorpism)
다형성의 목표는 고지식한 컴퓨터에 융통성을 부여하는 것이다. 다형성의 출현 배경은 컴퓨터는 사람과는 달리 바보라는 것이다. 사람과는 달리 고지식한 컴퓨터는 똑같은 기능을 하더라도 사용되는 자료형이 다르다면 이를 각기 다른 이름의 함수로 만들어 구현해야 되게 된다. 가령 어떠한 하나의 목표의 기능을 하는 함수를 만들어 사용하는데 있어서 다형성이 적용되지 않는다면 함수에서 사용되는 매개변수의 자료형 마다 이름이 다른 여러 함수들을 만들어서 사용해야 한다. 하지만 다형성이 적용되면 하나의 이름의 함수를 사용할수 있게 된다. 이는 함수에서 사용된 자료형에 따라 같은 이름을 갖지만 미리 여러 버전으로 준비해 놓은 여러 함수들 중에서 사용된 자료형과 일치하는 함수가 자동으로 결정되어 사용되게 되기 때문이다. 즉 하나의 목표 기능을 하는 하나의 이름을 갖는 함수를 사용할 수 있다는 말이며 이는 미리 만들어 둔 자료형에 따른 여러 버전이 미리 만들어져 있고
이들 중에서 사용된 자료형에 따라 일치하는 버전이 자동으로 실행되기 때문이다.다형성을 사용하게 되면 프로그램을 보다 단순하게 만드는 효과를 볼수가 있다. 이러한 다형성의 구현을 다음에서 나오는 함수중복과 연산자중복 그리고 가상함수를 통해서 살펴보도록 하자.
함수 중복
C에서는 비슷하거나 같은 동작을 해도 매개변수의 자료형이 다르다면 각 자료형을 처리하는 함수를 다른 이름을 사용하여 만들어야 했다. 예로서 입력된 수의 제곱을 구해주는 함수를 만든다면 매개변수로 어떤 자료형을 쓰느냐에 따라 다른 이름을 갖는 함수를 다음과 같이 만들어야 했다.
int squareint(int i)
float squarefloat(float f)
하지만 C++에서는 매개변수로 주어진 자료형이 달라도 자료형만 다르고 하는 일은 같다면 하나의 이름으로 여러 자료형을 처리하는 함수를 만들 수 있다. 즉 하나의 함수명을 가지며 각기 다른 자료형을 처리하는 여러 버전의 함수들을 만들어 놓고 이들을 사용할 적에는 사용하는 매개변수의 자료형에 따라 일치하는 자료형의 버전의 함수가 실행되게 되는 것이다. 이러한 사용의 례는 다음과 같다.
int square(int i)
float square(float f)
이것을 함수 중복(overloading)이라하며 이렇게 하나의 이름을 공유하고 매개변수의 자료형이 다른 여러개의 함수들 중에서 하나를 결정하는 방법은 사용자가 함수를 호출할적에 매개변수로 넣은 인수즉 매개변수의 형에 따라 컴파일러가 자동으로 어떤 자료형의 매개변수를 갖는 함수를 실행시킬지를 결정하게 된다. 이로서 프로그래머는 매개변수의 유형에 따라 각기 다른 함수를 만들어 놓고 사용할때 마다 다른 이름의 함수를 사용하지 않고
하나의 함수이름으로 여러 자료형을 갖는 각기 다른 버전의 함수들을 만들어 놓고 하나의 이름으로만 사용하면 사용된 매개변수의 자료형에 따라 실행될 함수가 자동으로 결정되는 효과를 볼수 있다는 것이다.
그리고 매개변수의 자료형 뿐만 아니라 매개변수의 수가 다른 복수의 함수도 기능에 있어서 관련이 있다면 하나의 이름으로 만들어 사용할 수 있다.
즉 다음과 같이
int square(int i)
int square(int i, int i2)
매개변수의 수가 다른 복수의 함수가 하나의 동일한 이름을 가질 수 있고
이러한 하나의 이름을 갖는 여러 함수들 중에서 실행될 함수를 결정하는 방법은 함수의 호출시에 사용된 매개변수의 형과 개수에 따라 실행될 함수가 결정되는 것이다. 이러한 사용의 예는 구조체 하나를 매개변수로 받는 함수의 버전과 구조체의 멤버변수들을 매개변수로 받는 함수의 버전 등을 만들어 놓고 상황에 따라 더 적합한 함수의 버전을 사용하는 것을 들 수 있다.
이러한 함수 중복은 MFC에서 가장 많이 사용되는 다형성을 대표하는 요소이다. (연산자 중복이나 가상함수는 이에 비해 잘 안 사용된다.) 실제로 MFC에서는 여러개의 함수버젼을 준비해 놓고 이들 중에서 프로그래머가 편한 버젼의 함수를 사용할 수 있도록 하고 있다. 가령 하나의 구조체를 매개변수로 가지는 함수와 구조체의 각 필드에 해당하는 변수를 함수의 각 매개변수로 전달하는 함수 등의 두가지 버젼을 마련해 놓고 편한 함수를 사용하도록 하는 것이다.
연산자 중복
연산자 중복이란 기존에 C에서 제공되는 기본 연산자들을 클래스에 연관지어 클래스에 맞도록 연산자의 기능을 재정의 하는 것이다. 연산자의 사용이 필요한 경우 이 연산자 재정의에 의해 비로소 클래스가 자체적으로 통합될 수 있게 되는데 연산자 중복이 필요한 이유는 바로 클래스가 새로운 자료형이기 때문이다. 기존의 C 에서 제공되는 기본 자료형의 경우 제공되는 연산자만으로 충분히 연산을 할 수 있으나 사용자가 새롭게 정의한 자료형인 클래스의 경우에는 클래스에 맞게 새롭게 정의된 연산이 필요하게 된다. 예로서 다음과 같은 클래스를 생각해 보자
class point
{
int x ;
int y ;
public :
void set_xy(int x2, int y2)
}
이러한 클래스를 사용하여 다음과 같이 연산을 한다고 해 보자
point a, b, c ;
c = a + b ;
이렇게 새롭게 만든 자료형인 point 클래스의 객체를 생성해서 이들 객체를 사용해서 연산을 할 때 이 객체들 사이에서 사용된 연산자가 어떻게 동작할 것인가에 대해서 여러분들은 어떻게 생각하는가.. 분명히 클래스는 사용자가 정의하는 하나의 자료형이다. 그리고 이 자료형인 클래스는 기본 자료형과 같이 연산자를 통해 연산될 수 있어야 한다. 헌데 새롭게 만든 클래스는 기존의 연산자로는 연산을 할 수 없게 된다. 즉 연산자가 어떤 클래스와 연관되어 어떤 방식으로 연산을 수행하게 될지를 정의해야 할 필요성이 대두되게 되는 것이다. 위의 point 클래스의 경우에는 + 연산자가 두 point클래스 객체 사이에서 사용될 때 양쪽 각 객체의 멤버변수 x는 x끼리 더하고 y는 y끼리 더하도록 +연산자를 재정의 하면 이 point 클래스 객체에 있어서 + 연산이 제대로 연산되도록 되는 것이다.
여기서는 연산자 중복에 대한 개념만 간단히 설명을 하고 있다.
왜냐면 우리가 클래스를 구축하려면 이 연산자 중복에 대해서 자세히 알아야 겠지만 MFC에서 제공되는 클래스들을 사용하는 데 있어서는 연산자 중복의 개념만 알면 우리가 연산자 중복을 구축할 필요는 없기 때문에 MFC에서 제공되는 클래스들에 이미 정의되어 있는 연산자들을 그냥 사용만 하면 되기 때문이다. 이러한 이유로 연산자 중복을 하는 방법이나 규칙등은 더 이상 설명하지 않겠다. 나중에 클래스를 스스로 구축해야할 필요성이 생기게 된다면 다른 서적을 참고하기 바란다. 다시 한번 말하지만 MFC를 사용하는 수준에서는 연산자를 중복하는 방법은 몰라도 상관 없다.
가상함수
가상함수란 기반 클래스의 포인터로 파생클래스에 접근하는 경우에 파생클래스에서 오버라이딩된 함수가 호출되지 않는 문제를 해결하는 용도와 그리고 실시간 다형성을 구현하는 용도로 사용되나 우리가 사용하게 될 비주얼 C++의 MFC에서는 메시지 맵이라는 가상함수를 대체하는 존재가 있기때문에 비주얼 C++ 을 사용하는 수준에서는 잘 몰라도 큰 지장 없다. 실제로 필자는 처음에 한동안 비주얼 C++의 MFC를 사용하면서 먼저 배웠던 가상함수의 실시간 다형성이 나오질 않아 어리둥절 했었는데 어쨌든 결론은 이렇다. 이 실시간 다형성이란 기능은 일반적으로 잘 사용되지 않기에 몰라도 상관 없으며 파생클래스에서 오버라이딩된 함수가 호출되지 않는 문제의 해결을 위해 클래스의 설계시에 오버라이딩될 함수들은 미리 가상함수로 선언하는데 우리는 클래스를 설계해서 만들것이 아니고 그대로 가져다 쓰거나 기껏해야 클래스를 파생해서 사용하는 것이 전부이므로 우리가 가상함수로 선언하는 것을 고려할 필요는 없다는 것이다.
Section 5.
나머지 주제들
자 이제까지 우리는 객체지향 언어로써의 C++의 핵심적인 개념들에 대해서 알아보았다. 사실 이러한 개념이 중요한 것이다. C는 암기할 내용은 많지 않다 하지만 개념이 어려운 언어이다. 헌데 C++ 의 객체지향 프로그래밍은 C 에서의 개념들 보다 더 어렵다고 한다.필자도 본서를 집필할 적에 오히려 기능적인 사항들의 설명에 대해서는 쉬웠지만 개념설명을 집필하는데 있어서는 곤욕을 치러야 했었다. 그만큼 개념이 중요하다는 말이다. 이제부터는 핵심적인 개념설명을 지나 부수적인 사항들에 대해서 알아보겠다.
생성자와 소멸자 함수
C++ 에는 생성자와 소멸자라고 하는 특별한 두 함수가 있다. 생성자라 불리는 함수는 클래스의 객체가 생성되는 시점에 자동으로 호출되는 함수이며
소멸자라 불리는 함수는 객체가 소멸되는 시점에 자동으로 호출되는 함수이다. 이러한 함수가 필요한 이유는 그리고 이러한 특별한 함수들을 만들어 둔 이유는 초기화와 마무리 작업을 자동화 하기 위함이다.
클래스 객체는 구조체의 개념과 비슷해서클래스는 여러 멤버 변수들을 가지게 된다. 즉 클래스 이름은 대표명이며 실제로 다루는 것은 멤버 변수들인 것이다. 그리고 이러한 멤버 변수들은 우리가 변수들을 다룰 때 초기화를 하듯이 초기화를 해 둬야 할 필요성이 흔히 있게 된다.
클래스 내에서 메모리를 동적 할당하여 사용한 뒤에는 반드시 할당한 메모리를 해제해야 한다고 앞에서 배웠다. 이렇게 사용한 메모리를 해제하는 절차는 반드시 매번 해야 하고 이러한 절차를 자동으로 이뤄지도록 한다면 편리하게 될 것이다.
이러한 위의 필요로 부터 생성자 함수와 소멸자 함수를 만들자는 아이디어가 나오게 되었다. 물론 위에서 말한 처리 외에도 다른 처리도 얼마든지 할 수 있다. 다만 생성자 함수와 소멸자 함수의 용도가 대부분 위의 처리에 사용되는 것이다.
즉 모든 클래스 객체는 이러한 두개의 특별한 함수를 기본적으로 갖게 되는 것이다. 생성자 함수의 이름은 클래스 이름과 같다. 그리고 소멸자 함수의 이름은 클래스 이름의 앞에 ~를 붙여준다.
C++ 에는 생성자만 해도 매개변수를 전달해 줄 수 있는 생성자 등 깊숙히 들어가면 여러가지 사항들이 있지만 MFC에서 흔히 사용되는 사용은 주로 기본적으로 생성되는 생성자에 그 객체가 생성 될 시점에 그 객체의 멤버 변수를 초기화하는 루틴을 넣어주는 정도의 사용만하게 되므로 그 정도만 알고 있어도 대부분의 경우에 큰 문제는 없다.
다음은 이러한 사용의 례이다.
ClassConstructer :: ClassConstructer( )
{
//여기에 필요한 초기화 루틴을 넣어준다.
m_var = 0;
m_var2 = 0;
... ...
}
이 외에 생성자에 매개변수를 전달해 줄 수도 있는데 이러한 내용은 나중에 필요할 때 다루도록 하겠다.
디폴트 매개변수
디폴트 매개변수란 함수를 호출할 때 매개변수를 넣어 주지 않으면 미리 정해둔 디폴트 값이 자동으로 매개변수로 사용되게 되는 것이다. 이러한 디폴트 매개변수를 사용하기 위한 형식은, 매개변수의 선언에서 자료형과 매개변수 이름 다음에 = 를 위치시키고 그 다음에 디폴트 값을 위치시키면 된다.
이는 다음과 같은 형태이다.
int FuncName(int first = 1, BOOL second = TRUE ) ;
{
}
위의 예에서 보듯이 매개변수 선언 뒤에 '=' 와 '디폴트 값'이 있게 된다.
이렇게 선언된 함수는 다음과 같이 사용될 수 있다.
FuncName( 1, TRUE ) ;
이렇게 사용할 수는 있지만 이러한 경우에는 이렇게 사용하지 않는다. 다음과 같이 사용한다.
FuncName( ) ;
이렇게 디폴트 매개변수가 선언되어 있는 함수를 호출하면서 아무런 매개변수도 주지 않으면 디폴트 매개변수가 사용되게 된다. 이러한 사용의 의미는 어차피 넣고자 하는 값이 디폴트로 설정된 값과 같기 때문에 아무런 값도 설정을 안해 주면 디폴트로 설정된 값이 사용되기 때문에 이렇게 사용하는 것이다.
만일 첫번째 매개변수의 값이 디폴트값과 다르고 뒤의 값은 디폴트 값과 같다면 다음과 같이 사용할 수 있다.
FuncName( 2 ) ;
만일 넣고자하는 매개변수의 값이 디폴트 값과 모두 다르다면 일일이 각 매개변수에 넣고자하는 값을 넣어 함수를 호출해 주어야 한다.
FuncName( 2, FALSE ) ;
즉 결론적으로 디폴트 매개변수가 선언되어 있는 함수를 호출할 적에 매개변수를 명시적으로 설정하지 않는다면 디폴트 값이 사용된다는 것이다.
그리고 함수 호출시에 앞에 있는 디폴트 매개변수를 사용하기 위해 매개변수를 지정하지 않고 뒤에 있는 디폴트 매개변수를 사용하지 않기 위해 디폴트 값이 아닌 다른 값을 지정하는 것 또한 허용되지 않는다. 즉 일단 디폴트 매개변수를 사용하기위해 아무 매개 변수도 지정하지 않기 시작했으면 그 뒤에 오는 모든 디폴트 매개변수는 디폴트 매개 변수로 사용해야 하며 앞에서 디폴트 매개변수를 사용하지 않고 다른 값을 지정했더라도 뒤에오는 디폴트 매개변수는 디폴트 매개변수로서 사용할 수 있다.
(MFC에서는 디폴트 매개변수가 제공되는 함수들을 사용하는 방법만 알면 무난하지 않을까 싶다. 웬간해서는 디폴트 매개변수가 있는 사용자 정의 함수를 만드는 경우가 없기 때문에 일단은 길게 더 부연설명을 하지 않고 여기에서 일단락을 짓고자 한다. 추후에 필요가 있다면 디폴트 매개변수에 대한 보다 자세한 내용이 필요하다면 그때 추가적으로 설명하기로 하겠다.)
this 포인터
이 this포인터라는 개념은 상당히 쉽지 않은 개념인데.. this 포인터란 한 마디로 말하자면, 멤버 함수가 속한 객체에 대한 포인터를 말한다. 멤버 함수는 클래스 객체의 멤버이다. 즉 멤버 함수는 클래스 객체에 속한다. 그리고 this 포인터는 클래스의 멤버 함수 내에서 사용이 되게 되는데 이는 this 포인터가 멤버 함수 내에서 사용될 적에 멤버 함수가 속한 클래스 객체를 가리키기 위해 사용이 되기 때문이다. 즉 this 포인터란 멤버 함수가 속한 클래스 객체에 대한 포인터이다. (멤버 함수가 속한 클래스 객체를 가리키는 포인터이다.) 다시 말하지만 this 포인터는 멤버 함수 내에서만 사용이 된다.
이는 클래스 내 에서만 this 포인터가 사용이 되는데 클래스의 멤버에는 멤버 변수와 멤버 함수만이 있으며 멤버 변수에서 this 포인터가 사용이 될 수는 없고 멤버 함수 내에서만 this 포인터를 사용하게 되기 때문이다.
보통 멤버 함수 내에서 다른 멤버를 사용할 적에 다시 말해서 멤버 함수 내에서 그 멤버함수가 속한 객체의 다른 멤버 함수를 호출하여 사용하거나
멤버함수 내에서 멤버 변수를 사용할 적에는 그냥 멤버의 이름을 사용하게 된다. 하지만 원래는 멤버 함수나 멤버 변수의 이름 앞에 'this->'를 붙여 주어야 한다. 이는 지금의 멤버 함수가 속한 클래스 객체의 멤버 라는 뜻인데
멤버 함수 안에서 다른 멤버를 사용할 때에는 이러한 'this->'를 생략할 수 있기에 보통은 이를 생략 하고 그냥 멤버의 이름을 바로 사용하는 것이다.
기억하기 바란다. 멤버 함수 내에서 this 가 사용이 되면 그 멤버 함수가 속한 클래스 객체를 가리키는 포인터라는 것을 말이다.
new와 delete
앞의 C 파트의 포인터에서 동적할당의 개념에 대해서 다루었다. 여기서는 그 개념을 토대로 C++ 에서 동적할당을 하기 위해서 제공하는 연산자인new 연산자와 delete 연산자에 대해서 다시 한 번 더 살펴 보도록 하겠다.
new 연산자와 delete 연산자는 동적 할당을 하기 위해 C++ 에서 제공하는 연산자들이다. 메모리를 동적으로 할당하기 위해서는 new 연산자를 사용하는데 이 new 연산자는 C 의 malloc() 함수에 비해 여러모로 편리하고 강력한 기능을 가진다. 그리고 이렇게 동적으로 할당된 메모리를 해제하기 위해서 delete 연산자를 사용한다. 동적으로 메모리를 할당하는 방법은 다음과 같다.
p-var = new type ;
new 연산자는 type 에 오는 자료형 크기의 메모리 공간을 힙 상에 동적으로 할당한 뒤 이렇게 할당된 메모리 공간에 대한 포인터(시작주소)를 반환한다. 그럼 이 반환되는 메모리 주소를 대입 연산자를 사용하여 왼 편에 위치하는 포인터에 저장하게 되는 것이다. 이 때 type 위치에 오는 자료형과 p-var 위체에 오는 포인터 변수의 자료형이 일치해야 한다.
type 에는 메모리를 할당하고자 하는 변수나 객체의 자료형이 오는데 기본 자료형이나 사용자 정의 자료형 즉 클래스(형)등이 올 수 있다. 그리고 이렇게 자료형만을 주게 되면 그 자료형의 크기가 자동으로 계산되어 주어진 자료형 크기의 메모리 공간을 할당하게 되는 것이다. 더 이상 C 에서 처럼 어떠한 자료형의 크기를 알아내기 위해 sizeof 연산자를 사용할 필요는 없다.
그리고 p-var 에는 type 에 오는 형과 같은 형의 포인터 변수가 온다.
delete 연산자는 위와 같이 동적으로 할당된 메모리 영역을 해제시킨다.
이 delete 연산자의 사용법은 다음과 같다.
delete p-var ;
p-var 에는 해제시킬 메모리 공간을 가리키는 포인터가 오게 된다. delete 연산자 다음에 해제시킬 메모리 공간에 대한 포인터를 위치시키면 힙 상에서 이 포인터가 가리키는 메모리 공간의 사용이 해제되게 된다.
참조
참조는 무엇인가..
C++ 에서는 참조(reference)라는 포인터와 유사하다고도 볼 수도 있는 요소를 제공한다. 사실 참조를 규명하여 정의내리는 것은 상당히 어려웠다.
그만치 명확하고 단순하게 정의를 내릴 수 있는 요소는 아닌 듯 싶다. 왜냐면 이러한 참조라는 것을 명확하게 규명하여 설명하고자 한다면 참조가 컴파일러 내에서 구현되는 방법에 대해 알아야 겠다고 느껴졌는데 필자가 아직 그 정도 까지는 모르기 때문이다. 어쨌든 필자가 파악한 참조에 대해서 한 번 알기쉽게 설명해 보겠다.
참조란 기본적인 관점에서 보면 어떠한 변수의 변수명이 가리키는 메모리 주소를 공유하는 다른 변수명 이라고 볼 수 있다. 즉 참조를 만들 때 주어진 변수명이 가리키는 메모리 주소를 갖는 다른 변수명인 것이다. 좀 더 부연하자면 참조를 만드는 것은 참조를 만들 때 주어진 변수명이 가리키는 메모리 주소를 공유하는 다른 변수명을 만드는 것이다. 여기서 메모리 주소를 공유한다는 표현을 썼는데 이는 다른 말로 하나의 변수 공간을 공유한다는 말로 달리 말할 수 있다. 변수 공간은 하나의 시작 주소를 갖게 되므로 하나의 시작 주소를 공유한다는 말은 하나의 변수 공간을 공유한다는 말과 같은 것이다. 이렇게 서로 다른 두 개의 변수명이 하나의 변수공간을 공유하기에
하나의 변수명에 값을 넣고 다른 하나의 변수명의 값을 확인해 보면 같게 된다. 이렇듯 참조는 하나의 변수명을 가지고 다른 변수명을 만드는 것이며
이 두 변수명은 하나의 변수공간을 공유하게 된다.
참조의 선언과 초기화
참조를 만드는 것은 일반 변수를 선언하는 것과 비슷하다. 단 차이점이 있다면 자료형과 참조명 사이에 & 가 온다는 것이다. 마치 포인터를 선언할 때 * 가 왔던 것과 비슷하다. 이 & 는 자료형과 참조명 사이에 올 때는 참조를 선언하기 위해 사용이 되며 이 외의 곳에서 사용이 되면 변수의 주소를 반환하는 주소 연산자로 사용이 된다.
참조는 선언할 때 반드시 초기화 되어야 한다. 참조의 사용에 있어서 초기화의 의미는 각별하다. 다른 일반 변수나 포인터는 선언시에 초기화를 안 해 주어도 상관 없지만 참조는 선언시에 반드시 초기화를 해 주어야만 한다. 왜냐면 참조란 어떠한 변수명을 가지고 그 변수명의 다른 이름을 만드는 것이기 때문이다. 이렇게 참조를 만드면서 초기화하는 개념은 뒤에서 이어지게 될 참조 매개 변수와 반환되는 참조 객체의 사용에서 일관되게 볼 수 있다.
초기화를 하기 위해서 대입 연산자의 오른 편에는 참조를 만들 변수 공간의 변수명이 오게 된다. 이 때 포인터와는 달리 변수명의 앞에 & 연산자가 오지 않는다. 그냥 변수명만을 위치시킨다. 그럼 참조를 선언하는 례를 보도록 하자
int Var = 100 ; //먼저 참조를 생성할 타겟 변수를 선언한다.
int &rVar = Var ; //먼저 만든 변수를 가지고 참조를 만든다.
위와 같이 & 를 사용하여 참조를 선언하면서 동시에 먼저 만들어 둔 변수를 대입하고 있다. 참조란 것이 어떻한 변수의 다른 이름을 만드는 것이기에
참조를 선언하면서 다른 변수의 변수명을 대입해 주게 된다. 이때 변수의 값 즉 여기서 100 을 대입해 주는 게 아니다. 이 변수명을 사용해서 참조를 생성하고 있는 것이다. 이렇게 변수명을 대입해서 참조를 초기화할 때 대입하는 변수의 자료형과 참조의 자료형이 일치해야 한다.
그리고 이렇게 만든 참조 rVar 은 Var 을 사용하듯이 사용하면 된다. 즉 이제 부터는 Var을 사용하든 rVar 을 사용하든 같은 결과를 가져 온다. 왜냐면 앞에서 설명했듯이 이름만 다를 뿐 어차피 같은 변수 공간을 가리키기 때문이다. 구조체나 클래스의 멤버를 사용할 때도, 포인터를 사용할 때 처럼 포인터명 앞에 역참조 연산자를 붙이고 . 연산자를 사용하거나 -> 연산자를 사용하는 게 아닌 그냥 원래의 객체의 멤버를 사용하듯 . 연산자만을 사용하여 멤버를 사용하면 된다. 결론은 이름만 다를 뿐 원래의 같은 객체를 사용하듯 사용하면 된다는 것이다.
위와 같이 일단 참조를 만들어 놓으면 rVar 에 값 1을 넣고 Var 의 값을 확인해 보면 값 1 이 출력되고 Var 에 값 2 를 넣고 rVar 의 값을 확인해 보면 값 2 가 출력된다. 즉 참조를 만든 후 부터는 둘 중 어느 것을 사용해도 같은 결과가 되게 되는 것이다. 이는 이 두 변수명이 하나의 변수공간을 공유하기 때문이다.
사실 보통 때 이렇게 하나의 변수공간에 두 개의 변수명을 만들어 사용할 일은 없을 것이다. 이러한 참조를 사용하는 이유는 함수에 매개변수를 참조로 전달하거나 함수에서 반환되는 반환 값을 참조로 전달 받아 옴으로 인해
call by reference 방식을 구현하기 위함이다. 왜냐면 C 에서 함수에 매개변수를 전달하거나 함수에서 반환값을 전달받아 올 때 전달하는 매개 변수와 전달받아오는 반환값의 복사본이 생성되어 전달되는데 이로인해 전달하는 원래의 매개변수가 변경되지 않고 또 전달 받아오는 객체를 직접 사용하여 변경할 수 없게 되기 때문이다. C 에서는 이러한 문제를 해결하기 위해 포인터를 사용해 왔다. 그러나 C++ 에서 제공하는 참조를 사용하면 이러한 문제를 간단히 해결할 수 있게 된다. 이는 바로 참조로 매개변수를 보내거나 참조로 반환값을 반환하면 디폴트로 되어있는 call by value 방식을 call by reference 방식으로 전환하는 효과를 내게 되기 때문이다. 사실 참조의 대부분의 사용은 이러한 것이다. 즉 참조는 call by reference 방식을 구현하기 위해 있는 것이라고 볼 수 있는 것이다. 그럼 이제부터 이러한 사용인 참조매개변수와 반환된 참조의 사용에 대해 알아보도록 하자..
참조 매개변수
함수에 매개변수를 건네줄 때 참조 매개 변수를 넘겨 줄 수 있다. 참조 매개 변수를 사용하면 포인터를 사용할 때와 같은 효과를 낼 수 있다. 즉 call by reference 방식으로 매개 변수를 전달하는 효과를 구현할 수 있다는 말이다. 참조 매개변수의 사용은 포인터 매개 변수와 마찬가지로 넘겨주는 원래의 매개변수에 값을 저장하여 얻어와 원래의 변수를 변경할 필요가 있을 때 사용된다.
이제부터 하는 설명을 주목해 보자. 여기서는 필자가 분석한 내용을 토대로 다른 책에서 길게 예를 들어 설명하는 것을 핵심적인 한 마디로 해설하려 한다. 매개변수를 참조 매개변수로 선언하는 것은 C 에서 디폴트로 call by value 방식으로 매개 변수를 전달하는 것을 참조매개변수를 선언함으로 call by reference 방식으로 매개변수를 전달하도록 디폴트로 설정된 방식을 바꾸는 옵션이라고 보면 된다. 매개변수를 참조 매개변수로 선언하면 포인터를 사용해서 매개변수를 전달하는 것 처럼 하지 않고 call by value 방식으로 매개변수를 전달하는 것 처럼 매개변수를 전달하고 함수 내에서 전달 받은 매개변수를 사용해도 포인터 매개변수를 사용하여 call by reference 방식으로 매개변수를 전달하여 변수를 사용하는 것 처럼 전달 받은 원래의 변수가 변경되게 된다. 즉 참조 매개변수를 사용하면 자동으로 call by reference 방식이 구현되게 되는 것이다. 다음의 코드를 보자 다음은 참조 매개변수를 선언한 함수 원형 부분이다.
int Func(int & rVar)
이와 같이 매개변수를 참조매개변수로 선언하면 디폴트로 되어있는 call by value 방식을 call by reference 방식으로 바꾸겠단 의미가 된다. 그럼 함수 내에서 넘겨받은 참조 매개변수에 역참조 연산자를 사용하지 않고 매개변수의 값을 변경하면 원래의 넘겨준 매개변수가 변경되게 되는 것이다.
사실 위의 매개변수를 넘겨주는 부분은 앞의 포인터 매개변수에서도 그러하였듯이 매개변수를 초기화 하는 것이라 볼 수 있다. 즉 만일 위의 함수를 호출하는 부분이 다음과 같다면
//매개변수가 참조매개변수로 선언된 함수를 호출하면서
//매개변수를 그냥 넘겨준다.
Func(Var);
이 참조 매개 변수를 넘겨주고 넘겨 받는 두 루틴의 합은 다음의 코드와 같다고 볼 수 있을 것이다.
//넘겨주는 매개변수를 넘겨줌으로
//참조매개변수를 초기화한다고 볼 수 있다.
int & rVar = Var ;
이로 인해 결론적으로, 참조를 선언하면서 초기화하여 참조를 만들어 내는 것이 되게 된다. 즉 전달하는 변수로 참조 매개변수를 초기화 하는 것이 되므로 참조매개변수를 변경하면 원래의 전달하는 매개변수가 변경되게 되는 것이다.
사실 이렇게 참조를 사용하여 매개변수를 call by reference 방식으로 전달하는 것은 포인터의 사용에 비해 비교적 덜 사용되는 것 같다. 이는 포인터가 C 부터 오랬동안 사용되어 왔고 또 참조가 포인터에 비해 사용하기가 더 간편하기는 하지만 참조는 포인터 보다 제약이 더 많아 포인터가 더 활용도가 더 높기 때문이기도 할 것이다. 어쟀든 call by reference 방식을 구현하기 위해 참조 보다 포인터가 더 흔히 사용되는 것은 사실이다.
반환된 참조의 사용
MFC 에서 참조가 가장 많이 사용이 되는 사용이 바로 이 반환되는 참조의 사용이다. C++ 에서 함수는 값을 반환할 때 참조의 형태로 반환할 수 있다. 참조를 사용하여 변수나 객체를 반환할 필요성은 다음과 같다.
C 나 C++에서 함수가 변수나 객체의 값을 반환할 적에 디폴트로 변수나 객체의 복사본이 생성되어 반환되게 된다. 즉 call by value 형태로 반환이 이루어지게 되는 것이다. 그렇기에 단지 어떠한 변수나 객체의 값 즉 데이터를 반환받아 사용만 하는 경우라면 문제가 될 게 없지만 ,원래의 매개변수를 수정하여 저장을 받아올 때 처럼, 만일 함수 내에서 동적으로 생성해 놓은 객체에 어떠한 데이터를 추가하는 등의 수정을 가해야 하는 경우라면 문제가 되게 된다. 왜냐면 디폴트로 되어 있는 반환의 형태는 복사본이 생성되어 반환되는 것이기에 반환받은 객체를 수정해도 반환한 원래의 객체가 수정되지 않아 원래의 객체에 데이터가 추가되지 않게 되기 때문이다. 이 문제를 해결하기 위해 동적으로 생성한 객체의 포인터를 반환할 수도 있지만(참고로 지금 말하는 경우는 반드시 동적으로 객체를 할당하는 경우에 한정된다. 왜냐면 함수내에서 지역적으로 생성한 변수나 객체는 함수가 종료되면 자동으로 소멸되기 때문이다. 그러므로 지역적으로 생성한 객체에 대한 포인터나 참조는 의미가 없기 때문이다.) 참조를 반환함으로서 이 문제를 간단히 해결할 수 있다.
참조를 사용하여, 참조의 형태로 변수나 객체를 반환하면 참조 매개변수의 경우와 같이 call by reference 처럼 반환되게 된다. 즉 반환하는 원래의 객체의 복사본이 생성되어 넘어가는 것이 아니라 반환되는 원래의 객체가 반환되게 되는 것이다. 이로인해 참조의 형태로 반환받은 객체에 변경을 가하면 반환된 원래의 객체가 변경되게 되는 것이다. 이렇게 참조의 형태로 반환되는 객체를 받아 사용하기 위해서는 반환되는 객체의 형과 동일한 형의 참조 변수 객체를 선언하여 생성하면서 대입연산자로 반환되는 값으로 초기화를 시키게 된다. 그리고 이렇게 선언하면서, 반환되는 값으로 초기화된 참조 객체를 사용할 때에는 포인터를 사용할 때 처럼 -> 연산자로 멤버를 사용하는 게 아니라 원래의 객체를 사용하듯이 그리고 참조 매개변수를 사용하듯이 . 연산자를 사용하여 클래스나 구조체 객체의 멤버를 사용하게 된다. 이렇게 참조를 반환하는 함수는 다음과 같이 함수 원형을 선언하게 된다.
int & FuncName(void);
위와 같이 반환형과 함수 이름 사이에 참조를 반환함을 의미하는 & 를 위치시키게 된다. 그리고 이렇게 참조를 반환하는 함수가 반환하는 참조를 사용하는 예는 다음과 같다.
int & Var = FuncName();
위에서 호출되는 함수는 참조를 반환하도록 선언된 함수이며 이 함수가 참조의 형태로 반환하는 변수를 가지고 참조 변수를 선언하면서 초기화 시켜 참조 변수를 생성하고 있다.
이렇듯 참조는 C 와 C++ 에서 함수가 디폴트로 call by value 방식으로 매개변수를 전달하거나 반환 값을 반환하는 것을 call by reference 방식으로 자동으로 바꾸도록 하기 위한 것이라고 보면 된다. 참조를 다른 목적을 위해 사용할 수도 있겠지만 주요한 사용은 위에서 설명한 내용이 된다.
Section 6.
돌아보며 정리하기
C++ 의 개념을 마치며..
자 이상으로 MFC 를 공부하기 위한 기반적인 지식에 해당하는 C/C++ 에 대해서 알아보았다. 물론 앞에서 배운 지식들이 C/C++ 의 전부는 아니다. 하지만 중요한 개념들을 전체적으로 살펴 보았는데 중요한 것은 이러한 개념이다. 흔히 C/C++ 은 개념이 어려운 언어라고 한다. 흔히 C/C++ 을 다루면서 표면적인 사용법에 치중하는 경향이 있는데 본서에서는 표면적인 사용법 이면에 가려져 있는 핵심 개념에 포커스를 두고 있다. 그러면서도 모든 걸 자세하게 나열하여 핵심적인 개념과 줄기를 놓치는 것을 방지하고자 핵심적인 뼈대 외에 자잘한 고급 사항들은 설명에서 제외시켰다. 이렇게 제외된 보다 세부적인 내용들은 차후에 설명될 것이다. 일단 본서는 입문서이니 기본적인 사항들에 포커스를 두고 있는 것이다.
이제부터는 MFC 에 대해서 알아보기 시작할 것이다. 단 이제부터는 원리적인 측면이 아닌 사용법 적인 측면에서 다룰 것이다. 물론 필자의 내용있는 시각은 여전히 계속되겠지만 만일 MFC 를 원리적으로 다루려고 한다면 그 난이도와 양이 만만치가 않기에 본서의 기획 의도인 비베와 비교를 한 입문서의 성격을 벗어나게 되기 때문이다.
자 그러면 실제적 사용적인 MFC 프로그래밍 환경으로 들어가 보자..
댓글 없음:
댓글 쓰기