번 호 : 2647
게시자 : 김상형 (mituri )
등록일 : 1998-05-24 04:29
제 목 : [C++] OOP 기초 강좌 2부 #1
지금부터 OOP 기초 강좌 2부를 시작합니다. MFC나 OWL 등의 클
래스 라이브러리는 물론이고 델파이와 같은 비주얼 개발 툴에도
OOP의 영향력은 아주 막대합니다. OOP를 모르고서는 도저히 어
떤 개발 툴에도 손을 댈 수 없는 지경입니다. 더구나 최근의 가
장 핫 이슈인 자바조차도 OOP를 기반으로 하고 있습니다. 그런
데 윈도우즈 환경에서는 OOP를 배우기가 쉽지 않습니다. OOP를
배우려면 윈도우즈보다는 도스가 훨씬 더 배우기 쉽습니다. 이
강좌는 프로그래밍에 관심은 있지만 OOP를 몰라 애태우는 많은
사람들에게 조금이라도(정말 쬐금이라도) 도움이 되기 위해 쓰
여졌습니다.
가급적 잡스러운 얘기는 하지 않고 핵심만을 풀어서 설명하고자
하니 읽는데 지겹더라도 저를 믿고 끝까지 최선을 다해 주시기
바랍니다. 한번 보고 두번 보고 또 보고 자꾸 보면 보통의 머리
를 가진 보통 사람이라면 누구나 이해하실 수 있을 것이라 생각
됩니다.
감사합니다.
김 상형
*)1부 강좌는 두루물 10번 게시판에 있습니다. 1부 강좌를 읽지
않으신 분은 먼저 읽고 오십시요.
*)이 자료는 가급적 공개하고자 합니다. 혹시 이 강좌가 마음에
들어 다른사람들에게도 보여 주고 싶으시다면 다른 강좌란
에 올리셔도 무방합니다. 하이텔은 물론 천리안, 나우누리,유
니텔 등 어떤 대중 통신망이라도 상관하지 않겠지만 강좌이외
의 다른 목적에는 사용하지 말아 주십시요. 책으로 출판되어
법적으로 보호되는 글입니다. 단 다른 강좌로 옮기실 때 저에
게 메일 한통만 보내 주시기 바랍니다.
제 4 장
연산자 오버로딩
이 장에서는 클래스의 특성에 따라 클래스의 멤버 데이터를 가장
적절하게 가공할 수 있는 연산자 함수에 관해 알아보고 기존 연
산자의 기능을 바꾸는 방법에 관해 알아본다. 연산자의 기능은
미리 내부적으로 정의되어 있다. + 연산자는 좌변과 우변을 합하
는 기능으로, -- 연산자는 피연산자의 값을 1 감소시키는 기능으
로 되어 있다. 이렇게 미리 정의되어 있는 연산자의 기능을 클래
스의 특성에 맞게 다시 정의하는 것을 연산자 오버로딩이라고 한
다.
오버로딩이란 사전상의 의미는 "과적하다, 짐을 많이 싣다" 의
뜻이지만 여기서는 기존의 기능을 취소하고 새로운 기능을 다시
부여한다는 의미이다. 연산자 오버로딩에 의해 + 연산자가 곱셈
에 사용될 수도 있고 뺄셈에 사용될 수도 있다.
********************************************************
********************************************************
4-1 연산자 함수
가. 기존 타입의 연산자
C++이 제공하는 기본적인 데이터형인 int, char, double 등을 피
연산자로 취하는 연산 구문을 살펴보자. 덧셈을 예로 든다면 다
음과 같은 연산 구문이 있을 수 있다.
i=3+8
d=3.14+2.17
i에 대입되는 값은 11이 될 것이고 d에 대입되는 값은 5.31이 된
다. + 연산자는 피연산자의 데이터 타입이 달라도 연산을 제대로
수행해낸다. 분명히 정수끼리 더하는 앨거리듬과 실수끼리 더하
는 앨거리듬은 다른 것인데도 어떻게 하나의 연산자로 서로 다른
데이터 타입의 연산을 수행해내는 것일까?
그 이유는 컴파일러가 + 연산자의 피연산자를 살펴보고 데이터
타입이 실수일 경우 실수끼리 덧셈을 하는 코드를 호출해주고 정
수일 경우 정수끼리 덧셈을 하는 코드를 호출해주기 때문이다.
똑같은 모양의 + 연산자를 사용하지만 피연산자의 데이터 타입에
따라 다른 코드를 적용함으로써 다른 종류의 연산을 한다.
연산자의 이런 다재다능은 프로그래머에게 상당한 편리를 제공해
주는데 만약 데이터 타입에 따라 다른 연산자들을 사용한다면 얼
마나 골치가 아프겠는가. 그런데 이런 편리함은 어디까지나 C++
이 기본적으로 제공하는 데이터 타입에 한해서만 가능하다. 사용
자가 직접 정의하는 데이터 타입인 클래스에서는 이런 규칙이 적
용되지 않는다. 복소수를 표현하기 위해 만든 Complex 클래스를
예로 들어 보자.
class Complex {
private:
double real; // 실수부
double image; // 허수부
public:
Complex() {} // 생성자 1
Complex(double r,double i) // 생성자 2
{
real=r;
image=i;
}
void OutComplex() // 복소수 출력 함수
{
cout << real << '+' << image << 'i' << endl;
}
};
Complex형의 객체 COM1과 COM2를 다음과 같이 정의하여 덧셈을
하였다고 하자.
Complex COM1(1.1,2.2);
Complex COM2(3.3,4.4);
Complex COM3;
COM3=COM1+COM2;
과연 이 코드가 제대로 동작을 할 것인가 하면 그렇지가 못하다.
+ 연산자는 데이터 타입에 따라 지정한 처리를 할 수 있는 능력
을 가지고는 있지만 어디까지나 C++에 이미 정의되어 있는 기존
의 데이터 타입에 한해서이다. + 연산자는 복소수라는 데이터 타
입에 대해서 아는 바가 없으며 그런 클래스가 만들어졌는지도 모
르며 복소수끼리 덧셈할 능력이 없다. 그럼, 사용자가 만든 데이
터 타입의 덧셈을 꼭 하고자 할 경우는 어떻게 해야 하는가. 이
때는 덧셈 연산을 해주는 전문적인 함수를 만들거나 아니면 연산
자를 오버로딩시키는 방법이 사용된다.
연산자 오버로딩이 C에서는 지원되지 않다가 C++에서 지원이 되
는 이유는 사용자가 데이터 타입을 마음대로 만들 수 있도록 해
놓았기 때문이다. 마음대로 만들도록만 해줄 것이 아니라 만든
데이터 타입에 대해 연산 기능까지도 사용자가 직접 정의할 수
있어야 하기 때문이다.
나. 멤버 연산자 함수
시간을 나타내기 위해서 시, 분, 초의 데이터 멤버와 시간을 맞
추고, 출력하는 멤버 함수를 가진 Time 클래스를 만든다고 하자.
이때 클래스의 모양은 다음과 같이 될 것이다.
class Time {
protected:
int hour,min,sec; // 시간을 기억하는 데이터 멤버
public:
void settime(int h,int m,int s) { // 시간을 설정하는
함수
hour=h;
min=m;
sec=s;
}
void outtime(); // 시간을 출력하는 함수
};
시, 분, 초를 기억할 수 있는 정수형 변수 hour, min, sec 등을
만들고 시간을 정의하는 settime 멤버 함수와, 시간을 화면으로
출력하는 outtime 멤버 함수가 클래스 내부에 선언되어 있다.
Time 클래스는 사용자가 직접 만든 데이터 타입이므로 Time형의
객체를 피연산자로 취하는 연산을 쓸 수 없다.
그래서 여기에 두 개의 Time 객체를 더할 수 있는 함수 addTime
을 추가로 더 정의하고 addTime 함수를 사용해 두 개의 시간을
더해보자. 시간끼리 더한다는 것은 단순한 산술연산의 덧셈과는
많이 다르다. 시간이 시, 분, 초 세 개의 요소로 구성되어 있을
뿐만 아니라 분과 초는 60진법을 사용하고 시는 12진법을 사용하
기 때문에 각각의 자리수를 개별적으로 더하고 자리넘침이 발생
했는가도 개별적으로 check해야 한다. 다음에 addTime을 사용한
예제를 보였다.
#include <stdio.h>
#include
class Time {
protected:
int hour,min,sec;
public:
void settime(int h,int m,int s) {
hour=h;
min=m;
sec=s;
return;
}
void outtime();
Time addTime(Time);
};
Time Time::addTime(Time T) // 시간을 더하는 함수
{
sec +=T.sec; // 초를 더한다.
min +=T.min;1 // 분을 더한다.
hour+=T.hour; // 시를 더한다.
if (sec>60) { // 초에서 자리넘침이 발생하면
min++; // 분을 1 증가시킨다.
sec-=60;
}
if (min>60) { // 분에서 자리넘침이 발생하면
hour++; // 시를 1 증가시킨다.
min-=60;
}
hour %= 12;
return (*this); // 덧셈한 객체를 리턴한다.
}
void Time::outtime(void)
{
cout << endl << "time is " << hour << " hour " << min<< "
minute " << sec << " second." << endl;
}
void main()
{
Time a,b;
a.settime(2,12,23);
b.settime(11,10,40);
a=a.addTime(b); // 객체끼리 더하는 연산을 한다.
a.outtime();
}
main 함수에서 Time형의 객체 a와 b를 선언하고 a는 2:12:23초로
b는 11:10:40초로 각각 정의하였다. 그리고 a=a.addTime(b)에 의
해 a 객체가 가진 시간에 b 객체가 가진 시간을 더한 시간으로 a
객체를 다시 정의하였다. 사실 시간끼리 덧셈을 한다는 것은 무
의미하지만 2:12:23과 11:10:40초를 꼭 더하고자 하면 1:23:03초
가 된다. Time 클래스의 멤버 함수인 addTime을 분석해보자.
addTime은 Time형의 객체 T를 인수로 받아 자신을 호출한 객체가
가진 시간에 T가 가진 시간을 더한다. a=a.addTime(b)에서
addTime 함수를 호출하는 객체는 a 객체이며 인수로 넘어가는 객
체는 b 객체이다. addTime 함수 내의 sec +=T.sec 연산은
addTime을 호출한 객체, 즉 a 객체의 데이터 멤버인 sec에 인수
로 넘어온 객체, 즉 b 객체의 데이터 멤버인 sec을 더하는 동작
을 한다.
우변을 좌변에 더한다.
sec += T.sec
── ───
addTime 함수를 인수로 전달된 객체의 데이터 멤버
호출한 객체의 sec
데이터 멤버 sec
초끼리 덧셈을 한 후에는 분끼리 덧셈을 하고 분을 더한 후에는
시끼리 덧셈을 하고 마지막으로 자리넘침을 점검한다. 시간끼리
의 덧셈이 끝난 후에는 addTime 함수를 호출한 객체를 리턴해줌
으로써 대입문에 사용될 수 있도록 한다. this란 멤버 함수 내에
서 멤버 함수를 호출한 객체의 번지값을 가지므로 *this는 곧 멤
버 함수를 호출한 객1체가 된다.
계산 결과인 객체를 리턴해줌으로써 a=a.addTime(b)라는 대입식
이 가능해지며 또는 제 삼의 객체에 덧셈 결과를 바로 대입할 수
있는 c=a.addTime(b) 따위의 연산도 가능하다.
시간끼리의 덧셈이 산술적인 덧셈과 다르기 때문에 이런 식으로
별도의 멤버 함수를 정의해 두고 시간끼리 덧셈이 필요할시 객체
에서 덧셈을 해주는 멤버 함수를 호출하여 사용한다.
그런데 a=a.addTime(b)라는 표현보다는 a=a+b;라는 형식으로 마
치 산술적으로 표현하는 것이 더 간단 명료해보인다. 산술적인
덧셈을 하는 + 연산자와 시간끼리 덧셈을 하는 + 연산자가 분명
히 모양만 같고 내부적인 동작이 다르지만 a와 b가 다 Time 객체
이므로 a+b는 Time 객체끼리의 덧셈임을 컴파일러가 쉽게 구분해
낼 수 있다. 그래서 + 연산자가 Time형의 객체와 함께 쓰일 때는
산술 연산과는다른 별도의 처리를 할 수 있도록 정의해보기로
한다.
#include <stdio.h>
#include
class Time {
protected:
int hour,min,sec;
public:
void settime(int h,int m,int s) {
hour=h;
min=m;
sec=s;
return;
}
void outtime();
Time operator1+(Time); // 시간끼리 더하는 + 연산자
함수
};
Time Time::operator +(Time T) // + 연산자 함수의 정의
{
sec +=T.sec; // code는 addTime 함수와 똑
같다.
min +=T.min;
hour+=T.hour;
if (sec>60) {
min++;
sec-=60;
}
if (min>60) {
hour++;
min-=60;
}
hour %= 12;
return (*this);
}
void Time::outtime(void)
{
cout << endl << "time is " << hour << " hour " << min<< "
minute " << sec << " second." << endl;
}
void main()
{
Time a,b;
a.settime(2,12,23);
b.settime(11,10,40);
a=a+b; // 시간끼리 더함
a.outtime();
}
실행 결과는 time is 1 hour 23 minute 3 second.이다. 앞에 예
제와 다른 점은 멤버함수 addTime의 이름이 operator +로 바뀌
었다는 것이며 addTime을 호출한 부분이 a=a+b로 바뀌었다는 것
이다. 멤버 함수 addTime이 가진 기능을 그대로 operator +라는
이름으로 다시 정의하고 있으며 이때 "operator +"란 + 연산자를
말한다.
즉, + 연산자의 기능을 Time 클래스와 함께 쓰일 때는 addTime이
가지고 있던 기능으로 바꾼다는 뜻이다. 이것이 바로 연산자 오
버로딩이며 이후부터 Time형의 객체와 함께 쓰이는 + 연산자는
시간끼리 더하는 기능을 하게 된다. 물론 Time 객체 밖에서 쓰이
는 + 연산자는 본래의 의미대로 사용된다. 2+3이나 53+23 등에
사용된 + 연산자는 Time 객체와 함께 사용된 것이 아니므로 본래
의 산술 연산을 행하며 각각 5와 76의 연산 결과를 리턴할 것이
다.
오버로딩된 + 연산자는 이제 시간을 나타내는 클래스 Time의 특
성에 맞게 시간끼리 덧셈을 할 수 있는 기능을 가지게 되었다.
이때 오버로딩된 + 연산자는 인수를 취하며 내부적인 연산 과정
의 결과를 리턴값으로 반납한다는 면에서 함수와 동일한 자격을
가지게 되며 그래서 연산자 함수(operator function)라고 부른
다. a=a+b;를 a=a.operator +(b);라는 함수 호출 형식으로 바꾸
어도 마찬가지로 동작한다. 같은 구문이되 표현만 다를 뿐이다.
어차피 컴파일러는 c=a+b;라는 연산식을 만나면 a 객체의 멤버로
정의되어 있는 연산자 멤버 함수를 호출하게 된다. 연산자 함수
를 정의하는 일반적 문법은 다음과 같다.
리턴값 소속 class::operator 연산자(인수 리스트)
{
함수의 본체;
}
예)
int Time::operator +(int a) { }
MyClass Date::operator *(MyClass M) { }
다.인수와 피연산자의 개수
연산자 함수를 정의할 때 주의할 것은 피연산자의 개수와 연산자
함수의 인수의 개수이다. 위에서 정의된 +연산자는 시간과 시간
끼리 더하는 연산자이며 연산에 필요한 피연산자는 두 개이지만
취하는 인수의 개수는 하나뿐이다. 그 이유는 연산자 함수가 클
래스에 소속된 멤버 함수이기 때문이다. 클래스에 소속된 멤버
함수가 실행되기 위해서는 반드시 객체가 정의되어 있어야 하며
객체에 의해 호출되어야 한다.
따라서 연산자 함수는 자신을 호출한 객체 그 자체와 호출시 전
달된 인수를 피연산자로 취하게 된다. 연산자를 호출하는 객체
그 자체가 하나의 피연산자가되어주기 때문에 연산자 함수는 자
신이 취하는 고유의 피연산자 개수보다 하나가 더 적은 개수 만
큼만 인수로 요구하게 된다.
a=a+b라는 연산식에서 + 연산자는 두 개의 피연산자 a와 b를 요
구하지만 막상 + 연산자 함수의 정의 부분을 보면 Time형의 객체
T 하나만을 인수로 취한다. 피연산자가 하나밖에 없는 연산자의
경우는 호출하는 객체만 피연산자로 가지게 되므로 별도의 인수
가 더 필요없다. 시간을 1초 증가시키는 ++ 연산자를 정의하고자
한다면 다음과 같이 하면 된다. 연산자 함수의 인수의 개수를 잘
살펴보자.
#include <stdio.h>
#include
class Time {
protected:
int hour,min,sec;
public:
void settime(int h,int m,int s) {
hour=h;
min=m;
sec=s;
return;
}
void outtime();
Time operator ++(void); // 시간을 1 증가시키는 ++ 연
산자
};
Time Time::operator ++(void) // ++ 연산자 함수의 정의
{
sec ++; // 초를 1초 증가시킨다.
if (sec>60) { // 초 자리넘침 처리
min++;
sec-=60;
}
if (min>60) { // 분 자리넘침 처리
hour++;
min-=60;
}
hour %= 12;
return (*this); // 연산 결과인 객체를 리턴
}
void Time::outtime(void)
{
cout << endl << "time is " << hour << " hour " << min<< "
minute " << sec << " second." << endl;
}
void main()
{
Time a,b;
a.settime(2,12,23);
a.outtime(); // 시간을 출력
a=a++; // 1초 증가시킴
a.outtime(); // 증가시키고 난 후의 출력
}
실행 결과를 보자.
time is 2 hour 12 minute 23 second.
time is 2 hour 12 minute 24 second.
처음에 출력한 시간이 a 객체가 초기화된 시간이고 두 번째 출력
된 시간이 1초 증가한 후의 시간이다. 증가의 대상이 되는 피연
산자인 Time형의 객체 하나가 반드시 필요하지만 ++ 연산자 함수
를 호출하는 객체 자신이 피연산자가 되어 주므로 ++ 연산자 정
의 부분에서는 인수를 따로 전달받을 필요가 없다. 그래서 인수
를 취하지 않으며 연산자 함수 정의 부분의 인수 리스트에는
void가 기입되어 있다.
연산자 함수가 피연산자의 개수보다 하나 적은 인수를 취하는 것
은 연산자 함수가 멤버 함수일 경우에 한해서이며 외부에 정의된
friend 함수일 경우는 피연산자의 개수와 인수의 개수가 같아진
다. 이에 관해서는 friend 연산자 함수를 설명할 때 다시 살펴보
자.
번 호 : 2648
게시자 : 김상형 (mituri )
등록일 : 1998-05-24 04:30
제 목 : [C++] OOP 기초 강좌 2부 #2
라. 복소수 클래스 연산
연산자 오버로딩의 개념을 깨우쳤으니 앞에서 불가능했던 복소수
연산을 연산자로 구현해보도록 하자. 복소수 덧셈 연산자를 만들
어 보고 더불어 뺄셈 연산자도 함께 만들어 본다. 리스트는 아래
와 같다.
#include <iostream.h>
class Complex {
private:
double real; // 실수부
double image; // 허수부
public:
Complex() {}
Complex(double r,double i) // 생성자
{
real=r;
image=i;
}
Complex operator +(Complex C);
Complex operator -(Complex C);
void OutComplex() // 복소수 출
력 함수
{
cout << real << '+' << image << 'i' << endl;
}
};
Complex Complex::operator +(Complex C) // 복소수 덧
셈 연산
{
Complex T;
T.real = real+C.real;
T.image = image+C.image;
return T;
}
Complex Complex::operator -(Complex C) // 복소수 뺄
셈 연산
{
Complex T;
T.real = real-C.real;
T.image = image-C.image;
return T;
}
void main()
{
1omplex COM1(1.1,2.2);
Complex COM2(3.3,4.4);
Complex COM3,COM4;
COM3=COM1+COM2;
COM4=COM2-COM1;
cout << "COM1 : ";COM1.OutComplex();
cout << "COM2 : ";COM2.OutComplex();
cout << "COM3 : ";COM3.OutComplex();
cout << "COM4 : ";COM4.OutComplex();
}
실행 결과는 아래와 같다. COM3는 두 복소수를 더한 것이고 COM4
는 두 복소수를 뺀 것이다.
COM1 : 1.1+2.2i
COM2 : 3.3+4.4i
COM3 : 4.4+6.6i
COM4 : 2.2+2.2i
복소수의 덧셈은 허수부는 허수부끼리 더하고 실수부는 실수부끼
리 더한다는 것쯤은 이 책을 읽고 있는 사람이라면 누구나 알 것
이다. 복소수 덧셈을 하는 + 연산자 함수의 정의부를 살펴보면
계산 결과를 담기 위해 임시 복소수 변수 T를 만들고 T에 계산
결과를 담은 후에 T를 리턴하고 있다. T는 Complex형의 객체이며
함수 내부에서 선언되었으므로 기억 부류는 지역 변수이다. 임시
변수를 만드는 것이 번거로우면 다음과 같이 생성자를 직접 호출
하여 원하는 객체를 만듦과 동시에 리턴을 해도 상관없다.
return Complex(real+C.real,image+C.image);
뺄셈 연산도 거의 동일한 구조로 이루어져 있다.
마. 일반 함수와 연산자 함수 비교
사용자가 정의한 데이터 타입의 연산을 필요로 할 때 연산을 수
행하는 일반 함수를 만들어 쓸 수도 있고 연산자 함수를 별도로
만들어 쓸 수도 있다. 두 방법 중 어떤 방법을 쓸 것인가는 프로
그래머가 마음대로 선택할 수 있는 문제이기는 하지만 장단점을
비교해본다면 연산자 함수쪽이 더 유리한 면이 많다.
첫째, 연산자 함수를 사용하는 것이 직관적이기 때문에 이해하
기가 더 쉽다. 복소수 객체 c1과 c2가 있을 때 이 두 객체를 더
하는 두 경우를 보면 다음과 같다.
연산자 함수:c1+c2
일반 함수:addComplex(c1,c2)
일반 함수도 물론 함수의 이름으로 쉽게 그 내부적 동작을 유추
할 수 있지만 여러 개가 동시에 더해질 경우는 문제가 달라진다.
연산자 함수로 c1+c2+c3+c4와 같이 표현될 수 있는 연산식을 일
반 함수로 표현하려면 addComplex(c1,addComplex(c2,ddComplex
(c3,c4))) 가 되어 아주 복잡해지게 된다.
둘째, 연산자 함수는 연산자의 우선 순위가 유지되므로 별도의
괄호를 쓸 필요가 없다.
연산자 함수 : c1+c2*c3+c1*c4
일반 함수 : addComplex(addComplex(c1,multComplex(c2,c3)),
multComplex(c1,c4))
식을 구성하는 프로그래머도 골머리를 앓게 되겠지만 이 식을 읽는
다른 사람들도 아마 연산의 우선 순위를 계산하기 위해 괄호를 요
모조모 잘 살펴봐야 할 것이다.
********************************************************
********************************************************
4-2 friend 연산자 함수
가. 클래스와 기존 데이터의 연산
이번에는 Time 클래스에서 사용된 + 연산자의 인수를 바꾸어서 시간
끼리가 아닌 특정 시간에 정수값을 더하는 연산자를 작성해보자. 연
산이 꼭 같은 종류의 데이터 타입끼리만 이루어지라는 법은 없다.
#include <stdio.h>
#include
class Time {
protected:
int hour,min,sec;
public:
void settime(int h,int m,int s) {
hour=h;
min=m;
sec=s;
return;
}
void outtime();
Time operator +(int);
};
Time Time::operator +(int i) // 시간과 정수를 더하는 연산자
{
sec +=i; // 초를 먼저 더함
while (sec>60) { // 초의 자리넘침을 분으로 넘긴다.
min++;
sec-=60;
}
while (min>60) { // 분의 자리넘침을 시간으로 넘긴다.
hour++;
min-=60;
}
hour %= 12;
return (*this); // 연산 결과인 객체를 리턴한다.
}
void Time::outtime(void)
{
cout << endl << "time is " << hour << " hour " << min
<< " minute " << sec << " second." << endl;
}
void main()
{
Time a;
a.settime(2,12,23);
a.outtime();
a=a+88;
a.outtime();
}
일단 연산자 함수의 인수가 바뀌었다. Time형의 객체를 인수로
받아들이는 것이 아니라 더할 시간을 초 단위의 정수값으로 입력
받았다. 연산자 함수 +의 내부도 적당한 형태로 수정되었다. 인
수 n이 초를 의미하므로 객체의 sec와 덧셈을 먼저 하며 sec의
자리넘침에 의해 분과 시가 변경되도록 하였다. 자리넘침을
check하는 부분도 if문에서 while문으로 바뀌었다.
시간끼리의 덧셈에서는 자리넘침이 기껏해야 1번밖에 일어날 수
가 없다. 59초에 59초를 더해봐야 분단위가 증가해야 할 수는 1
분밖에 안된다. 하지만 초 단위만으로 시간을 더할 때는 60초 이
내만 더해진다는 보장이 없으며 현재 시간에 200초를 더하라든가
3000초를 더하라든가 하는 표현이 가능해야 하기 때문에 자리넘
침이 없을 때까지 여러 번 체크해주어야 한다.
이 예제에서는 자리넘침을 체크하기 위해 while 루프를 사용하였
는데 루프는 더해지는 정수값이 크면 속도가 떨어지므로 다음과
같은 코드를 써서 좀더 빠른 속도를 낼 수 있다.
long temp;
temp=hour*3600+min*60+sec+i;
sec=temp%60;
min=(temp/60)%60;
hour=(temp/3600)%12;
임시 변수 temp를 사용하여 현재 시간을 초 단위로 바꾼 후에 초
로부터 시, 분, 초를 분리해내는 방법이다. 더해지는 정수값이
클 경우는 루프를 여러 번 돌게 되므로 실행 속도가 느려지게 되
는데 이런 연산식에 의해 루프를 여러 번 실행하는 것을 방지할
수 있다. 하지만 더해지는 초가 작을 경우에는 오히려 이런 방법
이 더 느려질 수도 있으므로 어떤 방법을 쓰는가는 상황에 따라
적절하게 선택하면 될 것이다.
나. friend 함수
시간과 정수를 더하는 예제를 다시 한번 더 살펴보자. 연산자 함
수를 호출하는 부분이 a=a+88;로 되어 있다. 덧셈은 교환 법칙이
성립하므로 a=a+88;이 가능하다면 논리상 a=88+a;도 가능해야 한
다. 그러나 멤버 함수로 정의된 연산자 함수에서는 이것이 불가
능하다.
a=a+88을 함수 호출 구문으로 고치면 a=a.operator +(88)이 된
다. a 객체에 정의되어 있는 멤버 연산자 함수 +를 인수 88을 사
용하여 호출하게 되며 멤버 함수가 정의되어 있으므로 아무런 문
제없이 연산이 이루어진다. a=88+a를 함수 호출 구문으로 고치면
a=88.operator +(a)가 된다.
곧, operator +가 88이라는 객체가 소속되어 있는 클래스(int)의
멤버 함수라는 얘기가 되는데 int라는 클래스에 정의되어 있는 +
연산자는 Time형의 객체를 피연산자로 취해 덧셈을 할 수 있는
능력이 없다. a=88+a;를 그대로 쓰다가는 Illegal structure
operation error가 발생한다. 그렇다고 해서 수학에서 성립하는
덧셈 연산의 교환법칙을 무시할 수도 없다.
이러한 문제를 해결하기 위해서는 객체에서 호출되는 멤버 연산
자 함수가 아닌 클래스 외부에 독립적으로 존재하는 연산자 함수
가 필요하다. 연산자 함수가 멤버 함수일 경우는 이 함수를 호출
하는 객체가 자동으로 첫번째 피연산자가 되어 버리기 때문에 연
산자 함수를 클래스 내부에 두어서는 문제를 해결할 수 없다. 클
래스 외부에 존재하면서도 클래스 내부의 멤버를 Access하는 권
한을 갖기 위해서는 이러한 연산자 함수가 friend로 선언되어야
한다.
friend로 선언된 연산자 함수는 클래스 소속이 아니며 객체에 의
해 호출되는 것이 아니므로 피연산자를 임의의 데이터형으로 가
질 수 있다. 또한 호출하는 객체가 피연산자가 되어 주지 않으므
로 연산자가 가져야 할 고유의 피연산자 개수 만큼의 인수를 가
져야 한다. 즉 이항 연산자는 두 개의 인수를, 단항 연산자는 한
개의 인수를 꼭 가져야 한다. 다음 예제를 보자.
#include <stdio.h>
#include
class Time {
protected:
int hour,min,sec;
public:
void settime(int h,int m,int s) {
hour=h;
min=m;
sec=s;
return;
}
void outtime();
Time operator +(int); // 멤버 연산자 함
수 +
friend Time operator+(int i,Time N); // friend 연산자
함수 +
};
Time Time::operator +(int i) // 멤버 연산자 함수 정의
{
// Time형의 객체와 정수를 더한다.
sec +=i;
while (sec>60) {
min++;
sec-=60;
}
while (min>60) {
hour++;
min-=60;
}
hour %= 12;
return (*this);
}
void Time::outtime(void)
{
cout << endl << "time is " << hour << " hour " << min<< "
minute " << sec << " second." << endl;
}
Time operator+(int i,Time N) // friend 연산자 함수 +
{
return N+i; // 순서를 바꾸어준다.
}
void main()
{
Time a;
a.settime(2,12,23); // 객체의 초기화
a.outtime(); // 객체를 출력
a=a+88; // 객체에 정수 88을 더한다.
a.outtime();
a=88+a; // 88과 객체를 더해도 된다.
a.outtime();
}
실행 결과는 다음과 같다.
time is 2 hour 12 minute 23 second.
time is 2 hour 13 minute 51 second.
time is 2 hour 15 minute 19 second.
번째 출럭늘섟 최초에 초기화된 시간이며 두 번째 출력
은 a 객체에 정수 88을 더한 것이다. 세 번째 출력된 결과는 정
수 88과 객체 a를 더한 것이다. 더하는 순서에 상관없이 + 연산
자가 제대로 동작하고 있다. 이 예제에서는 두 개의 연산자 함수
를 선언하고 있다. 하나는 앞에서 본 것과 똑같은 Time 클래스에
속한 멤버 함수이고 하나는 두 개의 인수를 취하는 friend 함수
로 정의되어 있다.
이렇게 정의된 friend 함수가 하는 일은 정수형 인수 i와 Time형
인수 N을 받아 이 인수를 사용해 멤버 함수로 정의된 연산자 함
수를 호출해주는 것 뿐이다. friend 함수의 본체는 return N+i;
라는 아주 간단한 연산문만 있다. 인수는 i와 N의 순서로 받아들
이되 연산식은 N+i를 사용한다. N+i 연산식의 첫번째 피연산자가
Time형의 객체이므로 이 연산문 실행 과정에서 Time 클래스의 멤
버 함수인 + 연산자가 호출되며 실제 연산을 하는 주체는 멤버
연산자 함수로 정의된 + 연산자이다.
friend 함수로 정의된 + 연산자는 인수를 받아들이는 순서만 바
꾸어 줄 뿐이다. 이렇게 되면 a=a+88;도 가능하며 a=88+a;도 가
능해진다. a=a+88은 곧바로 a 객체의 멤버 연산자 함수를 호출하
여 연산을 할 것이고 a=88+a는 friend 함수를 한 단계 거쳐 멤버
연산자 함수에 의해 연산이 수행될 것이다. 이렇게 하는 방법 외
에도 아예 처음부터 연산자 함수를 friend 함수로 다중 정의해도
상관없다.
friend operator +(Time T,int n);
friend operator +(int n,Time T);
두 개의 똑같은 이름을 가진 함수를 정의하였지만 인수의 형이
다르므로 연산식의 피연산자 순서에 의해 어떤 연산자를 호출할
것인가가 구분된다. 연산자 오버로딩 기법은 다형성에 그 근거를
두고 있다. 첫번째 피연산자가 정수인 경우와 Time형의 객체인
경우를 다 정의해주었으므로 순서에 상관없이 + 연산자를 사용하
면 된다.
***************************************************
*****************************************************
4-3 연산자 오버로딩 규칙
Х】봉 특성에 맞게 연산자를 재정의하여 쓰는 것이 가능하기
는 해도 모든 연산자에 대해 아무렇게나 재정의할 수 있는 것은
아니다. 연산자를 재정의하는 데도 몇 가지 규칙과 제약이 따른
다. 어찌 보면 당연한 규칙들이고 어찌보면 연산자 오버로딩에
따른 부작용을 해결하기 위한 방편들이라 할 수 있다.
첫째, 원래 C++에서 제공하는 연산자의 기능을 바꿀 수 있을 뿐
C++에서 제공하지 않는 연산자를 임의로 만들어 쓸 수는 없다.
C++에서 제공하지 않는 **나 <- 등의 연산자를 만들어 쓸 수는
없다는 얘기이다. 만약 누승을 하는 연산자를 사용자가 굳이 **
연산자로 만들고자 하며 C++이 이것을 허용한다고 해보자.
c=a**b;라는 연산식이 있을 때 C++이 이 식을 해석하는 방식에
두 가지 모호한 상황이 발생하게 된다. 이 식을 과연 a를 b만큼
누승하라는 식으로 해석할 것인지 아니면 a와 포인터 변수 b가
가리키는 번지에 든 값(*b)을 곱하라( a*(*b) )는 것인지 알아
낼 수가 없다.
참고:
승이란 2의 5승 따위의 거듭 제곱식을 말하는데 C++에서는(C도
마찬가지다) 별도의 연산자를 제공하지 않고 pow라는 수학 함수
로 제공한다. BASIC에는 누승 연산자(^)가 별도로 준비되어 있다.
또한 사용자가 만든 연산자는 우선 순위가 정해져 있지 않기 때
문에 c=1+a**b라는 식에서 덧셈이 먼저인지 누승이 먼저인지 알
수가 없다. 물론 연산 순위나 다른 연산자와의 모호함은 사용자
가 지정을 함으로써 해결할 수도 있지만 연산자 하나 쓰기 위해
이런 복잡한 지정까지 해야 한다면 차라리 안 쓰는 것보다도 더
못한 꼴이 되고 만다. 사용자가 임의로 연산자를 만들 수 있도록
하는 것은 이론상 가능하지만 그렇게 해 봐야 득보다는 실이 더
많기 때문에 아예 허용을 하지 않는다.
둘째, C++의 모든 연산자는 취할 수 있는 인수의 개수에 따라
인수가 하나면 단항 연산자, 둘이면 이항 연산자 등으로 분류한
다. 연산자의 기능을 바꾸더라도 취하는 인수의 개수는 바꿀 수
가 없다. + 연산자는 원래 이항 연산자이므로 오버로딩된 후에도
이항 연산자가 되며 반드시 피연산자를 두 개 가져야 한다.
Time Time::operator +(Time T) // 가능
Time Time::operator +(int i) // 가능
friend operator +(Time T,int i) // 가능 friend
operator +(Time T,Time S,int i); // 불가능Time
Time::operator +(void) // 불가능Time
Time::operator +(Time T,int i) // 불가능
네 번째 예는
friend 함수이고 인수를 세 개 가지므로 피연산자가 세 개나 되
기 때문에 불가능하며 다섯 번째 예는 인수가 하나도 없고
friend 함수가 아닌 멤버 함수이므로 피연산자가 하나밖에 없어
서 불가능하다. 여섯 번째 예는 인수가 두 개이며 멤버 함수이므
로 피연산자가 세 개이고 따라서 불가능하다.
세째, 연산자의 기능이 바뀌더라도 연산 순위는 원래대로 유지
된다. + 연산자의 기능을 바꾸어 곱셈을 하도록 만들었다 하여도
+ 연산자의 연산 순위는 여전히 그대로이다. 따라서 연산자를 오
버로딩하여 사용할 때는 연산의 우선 순위에 대하여 세심한 주의
를 기울여야 한다. 연산 순위뿐만 아니라 결합 순서도 마찬가지
로 유지된다.
네째, C++에서 제공하는 연산자 중에 다음의 연산자들은 기능을
재정의할 수 없도록 되어 있다.
.(구조체 멤버 연산자) ::(scope 연산자)
?:(삼항 조건 연산자) .* sizeof
이 연산자들
은 C++의 대표적 기능이라 할 수 있는 클래스에 관계된 연산자이
기 때문에 클래스를 기반으로 재정의되는 연산자 오버로딩의 재
료로 쓰기에는 무리가 따른다. 삼항 조건 연산자는 피연산자가
셋이나 되기 때문에 오버로딩을 하더라도 까다롭기 때문에 재정
의를 못하도록 되어 있다. 재정의를 못하는 연산자도 있기는 하
지만 C에는 연산자가 풍부하게 존재하므로 사실 오버로딩의 재료
로 쓸 수 있는 연산자는 많고도 많은 편이다.
다섯째, 기본 자료형에 대한 연산자를 재정의할 수는 없다. 연
산자 오버로딩은 사용자가 정의한 데이터 타입에 관한 연산을 정
의하는 것이지 이미 정의되어 있는 기본 연산을 바꾸는 것은 아
니다. 만약 다음과 같이 정수끼리의 덧셈을 하는 연산을 사용자
가 마음대로 정의하였다고 해보자.
int operator +(int a,int b)
{
함수의 본체
}
가장 기본적인 연산인 정수의 덧셈을 바꾸어 버리면 그 이후의
혼란은 따로 설명하지 않아도 쉽게 짐작이 갈 것이다. 연산자 함
수는 클래스의 멤버 함수이거나, friend 함수이더라도 피연산자
로 반드시 사용자가 만든 클래스의 객체를 가져야만 한다. 즉 연
산자가 오버로딩될 수 있는 경우는 연산의 대상이 사용자가 만든
객체인 경우에 한해서이다.
연산자를 재정의하게 되면 어떤 경우에는 복잡한 것을 간단하게,
산만한 것을 좀 더 논리적으로 표현해낼 수 있다. 하지만 그에
따른 부작용이 있음을 알아야 한다. 연산자를 여러 형태로 다중
정의해 놓으면 코드 자체가 이해하기 어렵게 되기도 하며, 다른
사람이 자신의 작품을 분석할 때 일일이 그 연산자가 어떻게 정
의되어 있는지를 조사해보아야 하며, 그 코드가 정의되어 있는
소스 파일이 없이 라이브러리 형태로 되어 있다면 연산자의 정의
내용을 알 수가 없어진다. 물론 주석을 달아 이런 문제를 해결할
수 있지만 너무 복잡하게 되면 일정 시간이 지난 후에는 자기 자
신도 이해하지 못하는 경우가 생기기도 한다.
연산자 오버로딩 기능은 꼭 필요할 때가 아니면 자제하는 편이
좋으며 여기서 연산자를 재정의하는 방법을 공부하는 이유는 이
미 작성되어 있는 라이브러리나 I/O stream, 남이 구축해놓은 클
래스를 좀더 신속하게 이해하고 적극적으로 활용하기 위해서이
다.
번 호 : 2649
게시자 : 김상형 (mituri )
등록일 : 1998-05-24 04:31
제 목 : [C++] OOP 기초 강좌 2부 #3
********************************************************
********************************************************
4-4 오버로딩 예
오버로딩의 규칙을 아는 것도 물론 중요하지만 과연 어떤 경우에
연산자를 재정의함으로써 프로그래밍의 효율을 향상시킬 수 있는
가를 아는 것도 중요하다. 연산자 오버로딩은 적절한 곳에 사용
되지 않으면 오히려 그냥 함수를 쓰는 것만도 못한 경우가 많이
있다. 사용자가 정의한 클래스를 최대한 편리하게 잘 활용할 수
있을 때에만 연산자를 재정의하는 것이 좋다. 여기에 몇 가지 연
산자를 재정의한 예를 보이므로 연산자가 어떠한 패턴으로 재정
의되는가와 재정의시에 어떤 이점이 생기는가를 살펴보기 바란
다.
가. 관계 연산자
Time형 클래스가 일반 산술형 데이터와 다르므로 시간끼리의 대
소 비교를 하는 과정도 당연히 다르다. 그래서 Time형의 객체끼
리 대소 비교나 등가 비교를 하기 위해서도 연산자를 별도로 정
의해야 한다. 아래의 리스트는 Time형 객체의 비교를 할 수 있도
록 모든 관계 연산자를 다 정의한 것이다.
#include <stdio.h>
#include
class Time {
protected:
int hour,min,sec;
public:
void settime(int h,int m,int s) {
hour=h;
min=m;
sec=s;
return;
}
void outtime();
int operator >(Time T); // 시간끼리 크기 비교를 한
다.
int operator ==(Time T); // 시간의 등가 비교를 한
다.
int operator !=(Time T)
{
return !(*this==T);
}
int operator <(Time T)
{
return !(*this==T || *this>T);
}
int operator >=(Time T)
{
return (*this==T || *this>T);
}
int operator <=(Time T)
{
return !(*this>T);
}
};
int Time::operator >(Time T) // > 연산자 함수의 정의
{
if (hour>T.hour) return 1; // 먼저 시간을 비교
if (hourT.min) return 1; // 시간이 같으면 분을 비교
if (minT.sec) return 1; // 분이 같으면 초를 비교
else return 0;
}
int Time::operator ==(Time T) // == 연산자 함수의 정의
{
if (hour==T.hour && min==T.min && sec==T.sec)
return 1;
else return 0;
}
void Time::outtime(void)
{
cout << "time is " << hour << " hour " << min
<< " minute " << sec << " second." << endl;
}
void main()
{
Time a,b,c;
a.settime(2,12,23); // Time 객체를 세 개 만들고 값을 일단
보여준다.
b.settime(2,12,28);
c.settime(2,12,23);
cout << "a : ";
a.outtime();
cout << "b : ";
b.outtime();
cout << "c : ";
c.outtime();
if (a>b) cout << "a is bigger than b" << endl;
else cout << "a is smaller or equalthan b" << endl;
if (a>=c) cout << "a is bigger or equal than c" << endl;
else cout << "a is smaller than c" << endl;
}
출력 결과는 다음과 같다. 과연 제대로 비교되었는지를 살펴보아
라.
a : time is 2 hour 12 minute 23 second.
b : time is 2 hour 12 minute 28 second.
c : time is 2 hour 12 minute 23 second.
a is smaller or equal than b
a is bigger or equal than c
> 연산자는 이 연산자를 호출한 Time형의 객체(연산자의 좌변)가
인수로 주어진 객체(연산자의 우변)보다 큰지를 평가한다. 먼저
두 객체의 시간(hour data member)을 비교하여 좌변 객체의 시간
이 더 크면 더 이상 볼 것도 없이 좌변이 크다고 판단하고 참(1)
을 리턴한다. hour가 더 크면 min이나 sec는 더 볼 것도 없다.
크지 않으면 다시 조건 판단을 하여 좌변 객체의 시간이 우변 객
체의 시간보다 더 작은 경우를 검사하여 이 조건이 만족하면 거
짓(0)을 리턴한다.
앞의 두 조건이 다 거짓이라면, 즉 두 객체의 시간이 서로 크지
도 작지도 않고 같다면 시간으로는 대소 판단을 할 수 없으므로
분을 비교해본다. 분을 비교하는 방법은 시간을 비교하는 방법과
동일하며 만약 분까지도 같다면 마지막으로 초를 비교해본다. 좌
변 객체의 초가 우변 객체의 초보다 더 크면 참(1)을 리턴하고
그 나머지의 경우는 우변 객체의 초가 같거나 작은 경우, 즉 어
쨌든 크지는 않으므로 더 이상의 조건을 판단하지 않고 거짓(0)
을 리턴한다.
== 연산자는 크기를 비교하는 연산자보다도 훨씬 더 간단하다.
시간을 나타내는 두 객체가 같은 시간을 나타내기 위해서는 두
객체의 시, 분, 초가 완전히 일치해야 하므로 세 개의 조건을 &&
로 연결하여 세 조건이 다 만족할 때만 참(1)을 리턴하고 그 외
의 경우는 거짓을 리턴한다.
> 연산자와 == 연산자 외에도 다른 관계 연산자도 오버로딩되어
있기는 하지만 실제의 코드가 작성되어 있지는 않다. 관계 연산
자는 상호 논리적인 대체성이 있기 때문에 > 연산자와 == 연산자
의 코드만 작성해 놓으면 다른 연산자는 이 두 연산자를 호출하
여 그 결과로부터 논리 연산에 의해 관계 연산의 결과를 도출해
낼 수 있다. 가장 쉬운 예로 != 연산자는 == 연산자의 역값을 취
하기만 하면 되고 <= 연산자는 > 연산자의 역값을 취해서 돌려
주기만 하면 된다.
나머지 연산자들도 논리적으로 == 연산자와 > 연산자를 잘 이용
하기만 하면 별도의 코드를 만들 필요가 없다. 어떻게 잘 이용하
고 있는지는 직접 소스를 보기 바란다. == , > 이외의 연산자는
내부에서 직접 비교하지 않고 다른 연산자 함수를 빌려서 사용하
므로 길이가 짧다. 그래서 inline으로 작성하여 클래스 정의 블
럭 내에서 곧바로 정의하였다.
여기에서 예로 든 관계 연산자의 오버로딩 예는 수치를 나타내는
다른 클래스에도 거의 비슷한 원리로 약간만 변형시키면 적용할
수 있다. Time 클래스의 관계 연산자를 정의한 방법을 원용하면
복소수 클래스, 분수 클래스, 날짜를 나타낼 수 있는 Date 클래
스 등에도 똑같은 기능을 하는 관계 연산자를 만들 수 있을 것이
다.
나. 문자열 연결 연산자
C++에는 문자열형이라는 데이터 타입이 원래부터 없다. 비록 문
자형의 배열을 사용하면 문자열을 구현할 수 있지만 문자열끼리
연결을 할 때는 별도의 함수(strcat 함수)가 필요해 불편하다.
그래서 문자열을 담을 수 있는 별도의 클래스 STR을 만들고 이
클래스 안에 문자열을 연결하는 연산자 +를 정의하였다. 이 예제
의 STR 클래스는 약간 부족한 감이 있지만 이런 식으로 클래스를
확장 정의하면 다른 고급 언어에서처럼 문자열형의 데이터 타입
을 만들 수 있다.
#include
#include
class STR // 하나의 문자열을 담는 클래스
{
private:
char str[20];
public:1
STR() {}
STR(char *s)
{
strcpy(str,s);
}
void outstr()
{
cout << str << endl;
}
STR operator +(STR S); // 문자열 연결 연산자
};
STR STR::operator +(STR S) // +연산자의 정의
{
STR T; // 임시 변수
strcpy(T.str,str); // 좌변 객체를 복사한다.
strcat(T.str,S.str); // 우변 객체를 덧붙인다.
return T; // 그리고 리턴한다.
}
void main()
{
STR a("father ");
STR b("mother ");
STR c;
cout << "a : ";a.outstr();
cout << "b : ";b.outstr();
c=a+b; // 두 문자열을 연결한다.
cout << "c : ";c.outstr();
}
예제에서는 문자열형의 객체 두 개를 만들고 두 객체를 연결하는
시범을 보인다. 실행 결과는 다음과 같다. c 객체가 a와 b의 문
자열을 결합한 형태가 되는데 소스를 보면 c에 a와 b의 문자열을
결합하기 위해 c=a+b라는 아주 간단한 연산 처리를 한다는 것을
알 수 있다.
a : father
b : mother
c : father mother
문자열끼리 덧셈을 하여 연결하는 방법은 BASIC 등의 고급 언어
에서 지원하는 방법이며 논리상 아무런 하자가 없다. 누가 보더
라도 문자열끼리의 + 연산은 연결을 하는 연산임을 이해할 수 있
다. 오히려 더 깔끔해보이고 쉽게 코드를 이해할 수 있게 한다.
연산자 함수의 정의를 보면 임시 변수 T를 만들어 이 객체에 좌
변과 우변의 문자열을 차례로 담아 리턴해준다. 임시 객체를 생
성하여 사용하는 피연산자로 사용된 원래의 객체를 유지시키기
위해서이다. int1이 5이고 int2가 6일 때 int3=int1+int2 연산을
하면 int3에는 11이 대입되겠지만 int1과 int2의 값은 계속 유지
되지 않는가. 피연산자의 값을 보존하는 경우와 그렇지 않은 경
우를 비교해보기 위해 문자열끼리 연결해주는 또다른 연산자 +=
을 만들어 보자.
#include <iostream.h>
#include
class STR
{
private:
char str[20];
public:
STR() {}
STR(char *s)
{
strcpy(str,s);
}
void outstr()
{
cout << str << endl;
}
void operator +=(STR S); // 문자열을 연결해주는 연산
자인 +=
};
void STR::operator +=(STR S)
{
strcat(str,S.str); // 좌변 객체에 우변 객체를 더해
준다.
}
void main()
{
STR a("father ");
STR b("mother ");
cout << "a : ";a.outstr();
cout << "b : ";b.outstr();
a+=b; // 좌변 객체에 우변 객체를 더하는
연산식
cout << "a : ";a.outstr();
cout << "b : ";b.outstr();
}
실행 결과를 아래에 보인다. 실행 결과를 살펴보면 알겠지만 덧
셈을 한 결과가 좌변 객체에 직접 대입되어 있다.
a : father
b : mother
a : father mother
b : mother
+ 연산자는 두 객체를 연결해주되 피연산자로 사용된 객체는 원
래의 문자열을 유지하도록 해주며 아예 새로운 객체를 리턴해준
다. 이런 경우는 + 연산식의 결과를 다른 객체에 대입을 해주어
야 한다. c=a+b 연산식에서 a와 b는 원래의 문자열을 유지하며
연산의 결과를 c에 대입하므로 c에 대입하지 않으면 연산의 결과
는 버려지고 문자열끼리는 연결 되지 않게 된다.
반면 += 연산자는 피연산자가 되는 두 객체 중 좌변의 객체에 직
접 우변의 객체를 덧붙이도록 정의되어 있다. 따라서 += 연산 후
의 결과를 다른 제 삼의 객체에 대입할 필요가 없으며 리턴해주
는 값도 없다. a+=b 연산 후에 a 객체의 내용이 직접 변한다.
a+b는 홀로 있을 수가 없지만(아래 참조) a+=b는 홀로 있을 수
있다.
아래:
문법적인 하자는 없다. C는 원래 연산식이 홀로 있을 수 있도록
허용하기 때문이다. 무슨 말인지 잘 모르겠거든 아무 source에나
3+4;라는 문장을 넣어놓고 컴파일시켜 보아라. 뭔가 좀 이상해
보여도 error가 나지는 않는다. 하지만 의미가 전혀 없는 문장이
되고 만다.
STR 클래스에서 문자열을 보관하는 데이터 멤버가 크기가 20인
문자형의 배열로 선언되어 있다. 그래서 19문자 이상의 문자열을
담을 수 없으며 문자열끼리 연결할 때에도 연결한 결과가 19문자
를 넘어서는 안된다는 제약이 있다.
정적인 문자형 배열을 사용하는 것보다는 생성자에서 문자열의
길이 만큼 동적으로 메모리를 할당하여 사용하는 것이 좋을 것
같지만 이렇게 만들면굉장히 예민한 문제거리가 발생하게 된다.
어떤 문제냐면 문자열끼리 대입을 할 경우에 동적으로 할당한 메
모리를 두 개의 객체가 동시에 포인트한다는 데 있는데 이에 관
해서는 다음에 자세히 살펴본다.
다. 문자열 출력 연산자
문자열을 출력하고자 한다면 당연히 cout 객체와 << 연산자를 사
용하거나 printf 함수와 %s 서식을 사용할 것이다. 이 함수들은
문자열의 끝까지 출력하도록 되어 있으므로 지정한 길이 만큼만
출력하기 위해서는 별도의 함수를 만들어야 한다. 그렇기는 한데
함수로 할 수 있는 일은 연산자도 할 수 있으므로 여기서는 연산
자를 사용하여 똑같은 동작을 하도록 만들어 보자. 여기서 문자
열을 일정 길이분 만큼 출력하는 연산자로 선택한 것은 % 나머지
연산자이다.
왜 이 연산자를 선택했는가 하면 특별한 이유는 없고 단지 잘 사
용되지 않기 때문에, 그리고 여기서 필요로 하는 이항 연산자이
기 때문이다. 이 연산자가 마음에 들지 않는다면 피연산자를 둘
취하기만 하면 다른 연산자를 사용해도 무방하다.
#include <iostream.h>
#include
class STR
{
private:
char str[30];
public:
STR() {}
STR(char *s)
{
strcpy(str,s);
}
void outstr()
{
cout << str << endl;
}
void operator %(int i); // 문자열 출력 연산자
};
void STR::operator %(int i) // % 연산자 정의
{
int j=0;
i=(i>strlen(str)) ? strlen(str):i;
while (i--) // 우변의 정수 만큼 문자열을 출력한
다.
{
cout << str[j];
j++;
}
cout << endl; // 문자열을 출력한 후에 개행해준다.
}
void main()
{
STR a("korean girl is beautiful");
for (int k=0;k<20;k++)
a%k; // a 문자열을 k 문자분 만큼 출력
}
% 연산자는 좌변의 STR 객체를 우변의 정수가 지정하는 문자분
만큼 출력해준다. main 루프에서 k가 0에서 20까지 변하며 a%k
연산을 수행하므로 출력 결과는 다음과 같아질 것이다.
k
ko
kor
kore
korea
korean
korean
korean g
korean gi
korean gir
korean girl
korean girl
korean girl i
korean girl is
korean girl is
korean girl is b
korean girl is be
korean girl is bea
korean girl is beau
이 예제에서 배울 수 있는 것은 연산자라 하여 반드시 사칙연산
같은 연산만 할 필요는 없다는 것이다. 의미가 있는 동작이라면
어떤 종류든지 연산자를 사용할 수도 있다. 우리 말로 연산이란
계산을 말하지만 영어의 operation은 계산 이외에도 동작이라는
뜻도 있다. 연산자(operator)는 반드시 계산만 하는 것이 아니
다.
라. [ ] 연산자
[ ] 연산자는 내부적으로 포인터 연산으로 동작하는 첨자 연산자
이며 피연산자를 둘 취하는 이항 연산자이다. ptr[i] 연산은 정
의에 의해 *(ptr+i)와 같은 동작을 하도록 되어 있다.
그래서[ ] 연산자의 피연산자 둘 중 하나는 반드시 포인터값이
어야 하며 나머지 하나는 정수여야 한다. STR형의 객체 a가 있을
때 비록 이 객체가 문자열을 표현하는 객체이지만 객체에 직접 [
] 연산자를 사용할 수는 없다. a[3] 등의 연산식을 쓸 경우 [ ]
연산자는 STR형의 객체와 정수와의 연산을 할 수 있는 능력이 없
으므로 에러가 출력될 것이다. a 객체의 데이터 멤버인 str은 포
인터 값(문자형 배열의 시작 번지를 나타내는 포인터 상수)이므
로 a.str[3]과 같은 연산은 가능하다.
하지만 의미상 STR형의 객체가 곧 문자열이므로 a[3]과 같이 객
체를 직접 [ ] 연산자의 피연산자로 사용할 수 있다면 좋지 않겠
는가? 그래서 STR 클래스의 멤버 연산자 함수로 []를 정의하였다.
#include <iostrem.h>
#include
class STR
{
private:
char str[30];
public:
STR() {}
STR(char *s)
{
strcpy(str,s);
}
void outstr()
{
cout << str << endl;
}
char operator [](int i);
};
char STR::operator [](int i) // [] 연산자의 정의
{
if (i>strlen(str) || i<0) // 범위 check
{
cout << endl << "out of range";
return ' '; // 범위 밖이면 space 를 리턴
}
return str[i]; // 범위 안이면 문자열 내의 문자를 리턴
}
void main()
{
STR a("korean girl is beautiful");
for (int k=-3;k<30;k++)
cout << a[k]; // a 객체의 k문자를 출력한다.
}
[ ] 연산자는 STR 클래스의 멤버 함수로 정의되었으며 STR형의
객체와 정수를 피연산자로 취하여 STR 객체의 특정 위치의 문자
하나를 읽어낸다. 출력 결과는 다음과 같다.
out of range
out of range
out of range korean girl is beautiful
out of range
out of range
out of range
out of range
out of range
더구나 [ ] 연산자를 오버로딩하면서 첨자가 문자열의 범위를 넘
어섰을 경우의 에러 처리까지도 해주고 있다. C++은 배열의 한계
점검을 하지 않기 때문에 길이가 10인 문자열이더라도 str[50]
등과 같은 연산을 할 수 있도록 되어 있는데 이런 점이 속도를
빠르게 하지만 때로는 무척 위험한 연산이 될 수도 있다. 그래서
문자열의 길이를 초과하는 첨자를 지정하거나 음수의 첨자를 지
정할 때는 에러 메시지를 출력하도록 하였다.
main 함수의 k 루프에서 k가 -3에서 30까지 변하며 a[k] 연산을
수행하는데 이 중에서 k가 음수인 경우와 25 이상인 경우는 에러
로 간주되어 에러 메시지가 출력된다.
번 호 : 2650
게시자 : 김상형 (mituri )
등록일 : 1998-05-24 04:31
제 목 : [C++] OOP 기초 강좌 2부 #4
제 5 장
OOP
이 장에서 논하는 내용은 다소 OOP의 기초적인 사항과는 무관한
내용들이다. 논리의 전개 순서상 클래스와 상속을 설명하는 중간
중간에 삽입되어야 옳지만 자칫하면 처음 OOP를 대하는 사람에게
혼란의 소지가 있을 것 같아 굳이 한 장을 마련하여 따로 논하고
자 한다. 필자의 실력이 부족하여 정확한 해설을 하지 못한 부분
이 많이 있음을 이해해주기 바란다.
********************************************************
********************************************************
5-1 정적 멤버
가. 정적 데이터 멤버
클래스로부터 객체가 생성될 때 객체는 클래스에 정의되어 있는
데이터 멤버의 총 크기 만큼의 메모리를 할당받으며 멤버 함수는
모든 객체가 공유한다. 즉, 한 객체가 생성되면 객체는 클래스의
모든 데이터 멤버를 개별적으로 소유하게 된다. 그러나 때로는
데이터 멤버도 모든 객체에서 공유해야 할 필요가 있는데 어떤
경우냐면 각 객체의 작업 결과 내지는 현재 상태를 총체적으로
나타내는 멤버가 필요할 때이다.
예를 들어 Position 클래스가 화면상의 한 위치와 출력된 문자를
나타내는 클래스로정의되었고 Position형의 객체가 여러 개 생
성될 때 몇개의 객체가 현재까지 생성되었는지의 개수를 어딘가
에 보관을 해야 한다고 하자.
생성되어 있는 객체의 개수를 보관하는 변수 numpos를 만들어서
이 문제를 해결 할 수 있다. 그렇다면 numpos는 과연 어디에 위
치시켜야 논리적으로 아무런 문제가 없을까. 클래스내에 둔다면
생성되는 객체마다 numpos 데이터 멤버를 가지게 될 것인데 객체
마다 별도의 다른 값을 가지지도 않을 것이고 객체 내부에서는
별로 의미도 없는 이 값을 객체마다 가진다는 것은 일종의 낭비
이다. 뿐만 아니라 한 변수가 여러 객체에 흩어져 있으므로 관리
하기도 귀찮아진다.
그래서 클래스내에 둔다는 것은 불합리하다. 클래스내에 두지 않
는다면 클래스 바깥에 전역 변수로 numpos를 선언해 두고 객체가
생길 때마다 이 값을 1 증가시키고 객체가 파괴될 때마다 이 값
을 1씩 감소시키면 된다. 과연 전역 변수를 쓰면 깨끗하게 해결
된다.
하지만 이 방법이 문제가 되는 것은 왜 클래스와 관계 있는, 클
래스로부터 생성된 객체의 개수를 기억하는 변수를 클래스 바깥
에 두느냐 하는 것이다. 전역 변수로 선언해 버리면 클래스가 프
로그램의 부품이 된다는 독립성에 위배된다. 전역 변수기 때문에
이 변수는 숨기는 것이 불가능해지고 아무 함수나 이 변수를 액
세스할 수가 있다. 그래서 전역 변수로 선언하는 방법과 클래스
내에 두는 방법의 절충안인 정적 데이터 멤버를 사용한다. 정적
데이터 멤버의 정의는 다음과 같다.
클래스의 바깥에 위치하지만 클래스의 멤버가 되며 클래스로부터
생성되는 모든 객체가 공유하는 멤버.
정적 데이터 멤버를 선언하는 방법은 일반 멤버를 선언하는 것과
동일하되 앞에 static이라는 키워드만을 추가하여 준다.
Position 클래스에 numpos라는 정적 멤버를 포함시키고자 한다면
다음과 같이 될 것이다.
class Position {
private:
</PRE>