제5강에서는 C언어에서 자주 사용하는 '연산자'에 대해서 알아보도록
하겠습니다^-^;; 내용이 생각보다 많아서 간단하게 설명하도록 하겠습니다!

 1. 대입 연산자 & 산술 연산자

 일단 2개의 피연산자를 필요로 하는 연산자를 '이항 연산자'라고 합니다.
C언어에서 사용하는 연산자는 대부분 이항 연산자입니다. 물론 단항 연산자도 존재하는대 좀 이따가 설명하도록 하구요~

 = : 오른쪽에 있는 값을 왼쪽에 있는 변수에 대입합니다.
      ex) int value =  10;
 + : 2개의 피연자의 값을 더합니다.
      ex) int value = 10 + 20;
 - : 왼쪽의 값에서 오른쪽에 있는 값을 빼줍니다.
      ex) int value = 20 - 10;
 * : 2개의 피연자의 값을 곱합니다.
      ex) int value = 10 * 20;
 / : 왼쪽의 값을 오른쪽의 값으로 나눕니다.
      ex) int value = 10/2;
 % : 왼쪽의 값을 오른쪽의 값으로 나눈 나머지를 반환합니다.
      ex) int value = 10%2;

 이러한 이항연산자에서 중요한 점은 값들의 자료형이 동일해야만 연산이 가능합니다. 그렇다면 연산자를 사용할때마다 다른 자료형은 형 변환을 통해서 자료형을 일치시켜줘야될까요? 아닙니다. 똑똑한 C언어는 연산을 위해서 각기 다른 피연산자들의 자료형을 일치시켜줍니다. 여기서 규칙은 값의 표현 범위가 넓은 쪽으로 변환된다는 점이죠.

 2. 복합 대입 연산자

 1번에서 살펴본 이항연산자들은 실제로 소스코딩 과정에서 빈번하게 사용됩니다. 그래서 C언어는 좀 더 간결하게 표현하기위해 '복합 대입 연산자'를 만들어냈습니다.

 a = a + b     ->     a += b
 a = a - b     ->     a -= b
 a = a * b     ->     a *= b
 a = a / b     ->     a /= b
 a = a % b     ->     a %= b

 왼쪽에 있는 수식과 오른쪽에 있는 수식은 완전하게 동일합니다. +=, -= 이런 것들이 복합 대입 연산자에 속합니다. 그러나 개인적으로 소스코드의 가독성을 위해 복합 대입 연산자 보다는 풀어서 사용하는 편입니다. 좋은 프로그래머가 되기위해서는 여러가지 표현 방법을 익혀두는 것이 좋습니다.

 3. 단항 연산자

 + : 양수를 의미합니다.
 - : 음수를 의미합니다.
 ++ : 피연산자에 저장된 값을 1 증가 시켜줍니다.
 -- : 피연산자에 저장된 값을 1 감소 시켜줍니다.

 여기서 ++ 와 -- 를 보통 증가/감소 연산자로 부르는데요 보기에는 별거 아닌 것 같아보여도 상당히 중요한 연산자입니다. 증가/감소 연산자는 '전위 연산자'와 '후위 연산자'가 있습니다. 

 int value = 10;
 value++;
 ++value;

 위 예제를 보시면 연산자가 앞 또는 뒤에 붙어있느냐에 따라서 전위와 후위로 나눠지게 됩니다. 이 부분은 연산자의 우선순위에서 다루지만 저는 우선순위는 따로 설명할 계획이 없어서 지금 간략하게 설명해드리도록 하겠습니다^-^!!;;;
일단 결과값은 value++ 는 10에서 1이 증가했으므로 11이 됩니다. ++value는 증가된 11에서 1이 증가했으므로 12가 됩니다. 여기서 무엇이 다르냐구요? 후위 연산자는 우선순위가 제일 마지막이라는 것 입니다. 위와 같은 예제에서는 차이점은 없습니다. 1이 증가된다는 사실 이외에 눈으로 확인할 수 있는 방법이 없죠... 

 int value = 0;
 printf("%d", ++value);

여기서는 전위 연산자가 사용됬습니다. 출력되는 값은 1이 되겠죠?

 int value = 0;
 printf("%d", value++);
 printf("%d", value);
 
여기서는 후위 연산자가 사용됬습니다. 출력 값은 2개가 존재하겠죠? 처음으로 출력되는 값은 0입니다. 후위 연산자가 사용됬으므로 printf함수를 실행한 다음에 value의 값을 1증가 시킵니다. 두번째 printf문에서는 그냥 value의 값을 출력시켜 봤습니다. 출력값은 1입니다. 전위 연산자와 후위 연산자의 차이점을 아셨나요?

 4. 관계 연산자

 '관계 연산자'는 말 그대로 크기나 동등관계를 비교하는 연산자 입니다. 관계 연산자도 이항연사자에 속하죠~ 바로 예를 들어서 설명하겠습니다.
 
 n1 < n2 : n1의 값이 n2의 값보다 작다
 n1 > n2 : n1의 값이 n2의 값보다 크다
 n1 == n2 : n1의 값과 n2의 값이 같다
 n1 != n2 : n1의 값과 n2의 값이 다르다
 
 여기서 '!' 기호는 부정을 뜻합니다. 그리고 < , > 연산자에서 <=, >= 이렇게 사용할 수 있는데요, 이 뜻은 같거나 작다 또는 같거나 크다를 의미 합니다.
관계 연산자의 결과 값은 bool형을 반환하는데요 참이면 1, 거짓이면 0을 반환합니다. 예를 들어 10 == 20 이라면 같지 않으므로 거짓어 되어 0 이라는 값을 반환하구요 10 < 20 이면 10이 20보다 작으므로 참이 되어 1 이라는 값을 반환합니다.

 5. 논리 연산자

 논리 연산자도 관계 연산자와 마찬가지로 참과 거짓을 구별해줍니다. 그러나 관계 연산자와는 조금 다르죠~

 A && B : A와 B가 모두 참이면 참값 1을 반환합니다.
 A  || B : A와 B 둘중 하나라도 참이면 참값 1을 반환합니다.
    !A     : A가 참이면 그 반대 값인 거짓, A가 거짓이면 그 반대 값인
             참을 반환 합니다.

 좀 더 구체적으로 예를 들어보겠습니다.

 int number1 = 1, number2 = 2, number3 = 3;
 int result1, result2, result3;

 result1 = (number1 == 1 && number2 == 2); // 결과값 1
 result2 = (number1 == 1 || number 2 == 3); // 결과값 1
 result3 = !number2; //결과값 0

 result1과 result2는 이해가 가셨나요~? 그렇다면 result3은 어떻게 된걸까요? number2에 저장되어 있는 값은 2입니다. 이 값을 참으로 판단하여 그의 반대인 거짓값 0을 반환했습니다. 여기서 중요한 것은 0이외에 모든 값은 참으로 인식한다는 점 입니다. 따라서 2도 참값으로 판단하기때문이죠~~~

 6. 콤마 연산자

 int number1 = 1;
 int number2 = 2;

 이러한 소스코드를 다음과 같이 나타낼 수 있습니다. 물론 콤마 연산자를 사용해서요^^

 int number1 = 1, number2 = 2;

위의 예제와 아래의 예제는 동일합니다. 콤마 연산자는 번거로움을 덜어주죠^^

7. 자료형 변환 연산자

  int형을 double형으로 또는 double형을 int형으로 변환시켜주는 것처럼 각각의 자료형간의 형 변환을 가능하게 해주는 연산자를 자료형 변환 연산자고 합니다.

 printf("%f", (double)3 );
 printf("%d", (int)3.14 ) ;


첫 번째 문장은 3.14는 분명 정수형인대도 불구하고 서식문자 %f를 사용해서 나타냈습니다. (double)3 은 int형 상수 3을 double형 상수로 변환하라는 의미입니다. 반대로 (int)3.14는 double형 상수 3.14를 int형 상수로 변환하라는 의미이구요~ 결과값은 3.000000 과 3 으로 출력 됩니다.

 7. sizeof 연산자

 'sizeof' 연산자는 피연산자의 크기를 '바이트'단위로 계산해서 반환하는 연산자입니다.

int number = 1234;
printf("%d", sizeof(number) );

 위 예제에서 number은 몇 바이트가 될까요? 당연히 number은 int형이기 때문에 4바이트가 출력됩니다. double형이면 8바이트가 출력이 되겠죠? 여럽지 않습니다~ 다만 나중에 배우실 배열에서는 조금 생각하여 계산해봐야되는 부분이 있긴하지만 그건 나중에 다시 짚고 넘어가도록 하겠습니다.

이상으로 연산자에 대한 강의를 마치도록 하겠습니다^~^ㅋㅋㅋㅋ

이번 강의에서 공부해볼 내용은 자료형과 변수에 대한 내용을 좀 더 심도 있게 다뤄볼 생각입니다^-^

1. 정수형

 정수를 표현하는 자료형은 여러가지가 있습니다. 일단 간단하게 정리해보도록 하겠습니다.

 char       : 1 byte           long        : 4 byte
 short      : 2 byte           long long : 8 byte
 int          : 4 byte
 
 이렇게 표현이 됩니다. 예를 들어 자료형 int 는 정수를 4바이트로 표현한다! 라는 이야기 입니다. 크기가 클 수록 표현 할 수 있는 수의 범위가 많아지겠죠?

 언더플로우와 오버플로우를 설명하겠습니다. 둘다 비슷한 개념이지만 단어의 뜻 대로 언더는 밑으로 오버는 위로 표현범위가 넘처나는 것을 뜻하는데요 이번 에도 int 형을 예로 들면 만약 int 형이 표현할수 있는 범위가 -10에서 +9까지라고 생각해봅시다. 사실 int 형은 -2147483646 부터 2147483647 이지만 쉽게 설명하기 위해서 범위를 축소하여 예를 들었습니다.
 만약 int 형을 사용하여 -11이라는 정수를 표현한다고 생각해봅시다. 그러나 int 형은 값을 표현할 수 있는 범위가 -10에서 +9이기 때문에 -11을 표현할 수 없습니다. 다만 표현은 해야 되기 때문에 -11과는 전혀다른 +9의 값으로 표현 해줍니다. 전혀 쓸모 없는 값이 출력이 되는 것이죠.
 따라서 어떠한 수를 표현할때에는 자료형의 범위를 고려하여 프로그램을 설계해야 합니다. 그렇다고 1200을 표현하기 위해서 long long형을 사용한다면 short형을 사용하여 충분함에도 불구하고 메모리 공간이 낭비되므로 적절한 자료형을 사용해야 합니다. 너무 작은 자료형을 사용하면 오버/언더 플로우가 발생하겠죠~?

 unsigned은 무엇인가?
간단하게 말해서 각각의 자료형 앞에 접두어로 unsigned을 붙여주면 자료형은 모두 양의 정수만 표현하게 됩니다. 만약 int형이 -10 부터 +9까지 나타낼 수 있다면 unsinged int 형은 0 부터 +20까지 표현할 수 있다는 소리 입니다. 즉 나타내는 수의 갯수는 같지만 양의 정수로 표현이 되기때문에  음의 정수를 표현하던 크기만큼 더~~~~ 양수를 표현할 수 있다는 이야기 입니다. 참고로 signed은 음수와 양수 모두 표현할 수 있다는 말이고 int 와 signed int 와는 같습니다. 즉, 모든 자료형 앞에는 signed이 생략되어이 있다고 생각할 수 있습니다.

2. 실수형

 
실수 자료형은 정수 자료형에 비해 표현할 수 있는 수가 상대적으로 적습니다. 이유는 소숫점을 나타내기 위함이죠^^ 일단 보시죠

 float            : 4 byte ( 소수점 6자리 )
 double        : 8 byte ( 소수점 10자리 )
 long double : 12 byte ( 소수점 10자리 이상 )

 괄호안에 보이는 소수점 6자리라는 소리는 float라는 실수 자료형은 소수점 6자리 까지는 오차가 발생하지 않는다는 소리입니다. 즉 정밀도를 이야기하며 float -> double -> long double 순으로 정밀도가 좋습니다.

 
3. 상 수

 이번에는 상수에 대해서 알아봅시다. 시스템을 좀 더 안정적으로 만들어 주기 위해 필요한 개념?이라고 생각해두시면 편하겠습니다.
프로그램 소스코드를 작성하다보면 일반적인 숫자 1,2,3 등과 같은 숫자들을 따로 정의한 부분이 없음에도 불구하고 숫자 자체를 시스템은 그대로 인식을 합니다. 즉 1은 1이다! 라는 소리이죠. 프로그래머가 임의로 1을 2라고 정의하지 못합니다. 실제로 변수에 저장된 값이 변경되지 않도록 상수화 시키는 키워드가 C언어에서는 존재합니다. 그 이름하여 const

 const int SEVEN = 7;
 const int TWO = 2;

 이러한 소스를 작성했다고 가정 합시다. 그럼 int 형 변수 SEVEN에 숫자 7을 선언을 했죠? 그리고 제일 앞에 보시면 const라는 키워드가 존재 합니다. 그럼 SEVEN이라는 변수명은 무조건 7을 나타냅니다. 상수화 시켜버리는 거죠. 때문에 값의 변경이 불가능합니다. 
 
 const int SEVEN = 7;
 SEVEN = 8;

 이러한 소스코드는 컴파일 에러를 발생합니다. 분명 첫 번째 줄에서 SEVEN을 상수화 시켜놓고 두 번재 줄에서는 이 값을 바꾸려고 시도하기 때문입니다. 이해가 좀 가셨나요~?

 이번 강의는 여기서 마치도록 하겠습니다. 뭔가눈에 확 들어오지 않는 강의이지만.... 개인적인 복습의 개념으로 하다보니....ㅎㅎㅎㅎㅎ;; 무튼 이상입니다!

#include <stdio.h>
int main(void)
{
    int n8 = 010;
    int n10 = 10;
    int n16 = 0x10;

    printf(" 8진수 : %o \n",n8);
    printf(" 10진수 : %d \n",n10);
    printf(" 16진수 : %x \n",n16);

    return 0;
}

 위의 소스 코드를 살펴 봅시다. int형인 변수 n8, n10, n16에 8진수, 10진수, 16진수의 숫자를 저장했습니다. 

 int n8 = 010; 이 부분에서 숫자 앞에 0이 붙었죠? 그럼 8진수로 인식합니다.
 int n16 = 0x10; 여기서는 숫자 앞에 0x가 붙어 있습니다. 그럼 16진수로 인식합니다. 즉 n8에는 8진수 10, n16에는 16진수 10이 저장되어 있다는 겁니다~

 그 다음으로는 printf문을 보시게 되면 서식문자 %o, %d, %x 가 보이시나요?
%o는 8진수를, %d는 10진수를, %x는 16진수를 표현하게 됩니다. 만약 변수에 16진수 0x10을 대입하고 printf문에서 서식문자를 %d를 사용하여 출력하면 16진수가 10진수로 바뀌어 출력이 되겠죠~? 어렵지 않아요~~~

 아마 다음장에서 서식문자나, 출력 형태, 자료형 등 자세한 내용을 다룰 것 같아서 이 번장에서는 간략하게 설명하고 넘어가도록 하겠습니다.

 자료형이 무엇이냐구요~? 우리가 지금까지 배운내용을 살펴보면 int형 변수를 사용했습니다. 즉 int는 자료형중에 하나로써 10진수 정수형으로 값을 저장, 참조 하라는 의미를 나타냅니다. int 같은 경우는 정수 자료형이고 double 같은 경우는 실수 자료형 입니다. 말그대로 실수형으로 값을저 저장, 참조 하라는 의미인거죠^^

 int val_int = 10;
 int val_double = 10.12345;

 printf(" int형 변수 : %d \n", val_int);
 printf(" double형 변수 : %f \n", val_double);

 double 형 변수는 실수 값을 저장할 수 있기 때문에 소숫점 자리도 표현이 가능합니다. 서식문자는 %f 가 사용 되었습니다. 이 것은 10진수 실수의 형태로 출력하게 해줍니다.

 이번 강은 여기 까지만 하도록 하겠습니다~!!! 생각보다 짧았네요.. 자세한건 다음장에서 다루도록 하겠습니다~ 감사합니다^-^

int main(void)
{
    return 0;
}
 이러한 형태가 프로그램의 가장 기본적인 틀이라고 할 수 있습니다. main이라는 이름을 가진 함수의 안에 프로그래머가 원하는 코드를 채워 넣습니다. 위의 코드를 실행해도 실행은 되지만 아무런 결과는 나오지 않습니다. 채워놓은 내용이 없기 때문이죠. 그렇다면 return 0; 은 무엇입니까? main함수의 종료를 나타내는 겁니다. 즉, 메인함수의 종료로 프로그램의 종료까지 이어집니다. 함수의 시작과 끝은 { , } 로 나타냅니다.

#include <stdio.h>
int main(void)
{
   
printf("test입니다.\n");
    return 0;
}

 printf 함수는 괄호안의 내용을 출력합니다. 출력하는 내용은 큰 따옴표 " , "안에 넣습니다. \n은 개행을 출력하는 것 으로 키보드의 Enter의 역할을 합니다.
#include<stdio.h>는 헤더파일을 선언하는 문장으로 stdio.h 이라는 헤더파일을 포함한다는 의미입니다. printf 함수는 이 stdio.h 헤더파일을 포함해야 사용 할 수 있습니다. 문장의 끝에 기재되어있는 세미콜론(;)은 문장의 끝을 나타냅니다. 예를들어 printf문과 return을 구분하기 위한, 즉 명령문을 구분하는 역할을 합니다.

 \n와 같은 문장을 이스케이프 시퀀스라고 하는데 개행출력을 나타내는 \n이외에도 많은 문장들이 존재합니다. 간단히 살펴보고 넘어가죠~

- \a : 경고음                   - \' : 작은 따옴표 출력 
- \b : 백스페이스             - \" : 큰 따옴표 출력
- \t : 수평 탭                   - \? : 물음표 출력
- \v : 수직 탭                  - \\ : 역슬래쉬 출력

 이번에는 서식문자에 대해서 알아보겠습니다.

 printf(" 숫자 : %d, 숫자 : %d ", 1, 2);

이러한 printf 문에서 %d 와 같은 문장을 서식문자라고 하는데요. %d가 의미하는 바는 데이터를 10진수 정수형으로 나타내라는 의미 입니다. 서식문자에 들어가는 데이터는 큰 따옴표 문장 뒤에 쉼표(,)로 구분하여 나타냅니다. 위 의 문장 같은 경우는 " 숫자 : 1, 숫자 : 2 " 라고 출력이 되겠네요~

 이번에는 메모리에 공간을 할당해 나타낼 수 있는 변수를 가지고 출력을 해봅시다.

 int value = 100;
 printf("
변수에 저장된 숫자는 ? %d 입니다.", value);

 value 라는 이름을 가진 int 형 변수에 100 이라는 숫자를 저장합니다. 그리고 전과 같은 printf문이 보이죠? 직접적인 숫자대신 변수 value이 들어가 있습니다. 출력은 " 변수에 저장된 숫자는 ? 100 입니다 " 라고 나타나겠죠~? 물론 큰 따옴표는 제외입니다. 큰 따옴표를 출력하기 위해서는 이스케이프 시퀀스를 사용해야 정상적으로 출력할 수 있겠죠~?ㅋㅋㅋ " = " 등호의 의미는 수학에서는 2개의 값이 같다고 나타내지만 프로그래밍 언어에서는 오른쪽에 있는 값을 왼쪽에 대입한다는 의미로 사용됩니다.

 마지막으로 주석에 대해서 알아봅시다!!! 별거 아닌 것 같았지만 진짜로 아주 아주 중요하게 사용되는 문장 중에 하나입니다. 실질적으로 프로그램을 작성할때 아무런 결과를 나타내지는 않습니다. 주석은 코드에 간단하게 메모하는 기능을 담당합니다. 즉, 소스코드를 쉽게 분석할 수 있도록 프로그래머가 소스에 간단하게 메모하는 것이죠~

 printf("주석을 달아주세요\n");    //출력함수입니다.

 이런 방법으로 메모를 해주는데요 주석의 표시 방법에는 // 과 /* ...내용... */으로 나타낼 수 있습니다. //는 한 줄, /* */는 여러 행에 걸쳐서 표현 할 수 있습니다.

 // 한 줄 메모
 /* 메모가
     가능합니다. */

 이러한 형태로 나타내는 겁니다~ 쉽죠? 오늘은 딱 요기까지만 하도록 하겠습니다. 사실 어설픈...? 강의의 형태로 포스팅을 하지만 너무 딱딱하게 요약 정리만 해서 포스팅하면 뭔가가 좀 그래서...... 개인적으로 복습도 할겸 혹시라도 누군가에게 단 한사람이라도 도움이 되지 않을까해서...^^;;ㅎㅎㅎㅎ 이상!! 감사합니다^-^

C언어는 프로그래밍 언어 중 가장 기본이 되는 언어이며 인간이 이해하기 쉬운 고급언어 입니다. C언어 입문에 앞서서 컴파일과 링커에 대한 기본적인 이해가 필요할 것 같아 간단하게 설명하고 넘어가도록 하겠습니다.

 C언어나 JAVA같은 고급언어는 컴퓨터가 전혀 알아듣지를 못합니다. 우리나라 사람이 한국 말을 전혀 모르는 외국인에게 한국말로 백날 얘기해봤자 외국인은 전혀 알아들을 수 없죠. 한국말을 외국어로 번역해주는 통역인이 필요합니다. 이와같이 우리가 사용하는 프로그래밍 언어를 컴퓨터가 알아들을 수 있는 바이너리 코드로 변환해주는 것이 컴파일러라고 생각하시면 됩니다.

 다시 얘기하자면 한국어(C언어)를 외국어(바이너리코드)로 통역해주는 통역인이 컴파일러가 되는 것 이죠. 참 쉽죠~????ㅋㅋㅋ 그렇지만 사실 C언어를 다이렉트로 바이너리코드로 변환해주는 것은 아닙니다. 중간에 어셈블리어로 한번 더 변환 후, 바이너리 코드로 변환하죠. 어셈블리어를 바이너리 코드로 변환해주는 것은 어셈블러가 담당합니다.

 C프로그램을 컴파일러가 어셈블리 코드로 변환해주는 과정에서는 각 컴퓨터의 환경마다 각기 다른 컴파일러를 필요로 합니다. 예를들어 Intel이면 Intel컴파일러, ARM이면 ARM 컴파일러가 사용됩니다~

 그럼 링커란? 링커는 최종적으로 변환된 바이너리 코드를 실행파일로 만들어주는 역할을 담당합니다. 마찬가지로 컴퓨터의 환경마다 실행파일의 형태가 다르긴 하지만 링커가 알아서 알맞게 실행파일을 만들어 줍니다. 

 어느 정도 이해가 되셨나요~? 좀 더 사실적으로 설명해보겠습니다.
C언어로 프로그램을 만들었습니다. 그럼 C언어로 제작된 소스파일이 있겠지요~? 이 소스 파일을 컴파일러(+어셈블러)가 바이너리 코드인 오브젝트 파일로 변환 시켜 줍니다. 그럼 이 오브젝트 파일이 실행되느냐? 아닙니다. 이 것을 링커가 실행파일로 다시 변환해주어 .exe라는 확장자를 가진 실행파일이 되는 겁니다^~^!!! 아 그리고 일반적으로 컴파일러가 어셈블러의 기능까지 같이 담당하기 때문에 컴파일러가 소스파일을 바로 오브젝트 파일로 변환이 가능한 것 입니다. 나름 쉽게 설명한다고 했는데 이해는 잘 가셨나요~?ㅎㅎㅎㅎ


   
[소스파일] ----------->[오브젝트 파일] ----------->[실행파일]
                  (컴파일러)                           (링커)

 이러한 형태로 나타낼 수 있겠네요~ㅋㅋㅋ 이것으로 요번장을 마치도록 하겠습니다. 감사합니다(-_-)(_ _)(-_-)!



 드디어 C++ 언어의 마지막 강의입니다. 사실 포스팅 할 내용은 많으나 도저히 못하겠어요... 이놈의 C++ 을 얼마나 끌었는지 모르겠네요... 복습개념이라 단기로 바짝해서 올려야되는대 그게 안되다보니 길어지도 지치고 ㅠㅠㅠㅠ 오늘은 예외처리에 대해서 공부하도록 할 건데요~ 짧게 마치도록 하겠습니다.


[예외 처리의 개념]


 예를 들어, int a 와 int b 라는 변수가 있다고 가정합시다. 실제 연산에서 a/b 라는 연산을 하여 출력값을 cout문으로 화면상에 출력을 한다고 가정했을때를 생각해보세요. 일반적인 경우는 컴파일에러 없이 무난하게 출력해 줄 겁니다. 하지만 b 의 값을 0으로 전달 받았다면 정상적으로 처리가 될까요? 당연히 오류가 발생하게 됩니다. 이미 중학교 수학시간에 분모는 0이 될 수 없음을 배웠습니다. 즉, b 라는 변수가 0이되는 상황이 바로 예외 상황이라고 할 수 있습니다. 따라서 if문을 사용하여 b의 값이 0이라면 사용자에게 입력값 오류라는 내용을 통보해주어야 하고 0이 아니라면 정상적으로 처리할 수 있게끔 코딩해줘야 한다는 말이죠~ 

 if문을 사용한 것은 이해를 돕기 위할 뿐 실제 코딩에서는 if문을 사용하지 않습니다. 기본적인 예외 처리 매커니즘인 try, catch, throw 라는 3개의 키워드를 사용합니다.


- try : 예외 발생에 대한 검사 범위를 설정할 때 사용합니다.

- catch : 예뢰를 처리하는 코드 블록을 선언할 때 사용합니다.

*** catch 블록은 항상 try블록 바로 뒤에 위치시켜주어야 합니다.

- throw : 예외 상황이 발생하였음을 알릴 때 사용합니다.


 즉, throw 문에 의해 던져지는 예외는 catch 블록에 의해 처리됩니다. 다음과 같이 말이죠.


try{

if(b==0)  throw b;

//정상코드 실행 부분

}

catch(int exception) {    

//예외 상황 처리 부분

}


 이해가 되셨나요~? 결코 어려운 내용은 아닐겁니다. 아주아주 기본적인 내용만 다루어봤는대요 대충 이런거구나~ 라고 하시고 넘어가시면 될 것 같습니다. 이걸로써 C++ 언어는 모두 끝이 났습니다! TCP/IP 나 C#을 다음 강의 내용으로 생각 중인대 정확히 어떤걸 할 지는 아직 모르겠네요... 수고하셨습니다





 연산자 오버로딩 두번째 시간입니다.

이번에는 연산자를 오버로딩하되, 전역 함수에 의한 오버로딩이 반드시 필요한 상황 하나를 소개하겠습니다.

차근차근 진행할거니까 잘 따라오세요~


[잘못된 형태의 연산자 오버로딩]


int main(void)

{

int a=10, b=20;

int c=a+b;

...(생략)

}


 위 예제처럼 a와 b를 더하 결과 값이 단순히 c에 저장되는 것이지 변수 a 혹은 b 의 값이 변경되는 것은 아닙니다.

바로 다음 예제를 살펴보겠습니다.


#include <iostream>

using std::endl;

using std::cout;


class Point {

private:

int x, y;

public:

Point(int _x=0, int _y=0):x(_x), y(_y){}

void ShowPosition();

Point operator+(int val);

};

void Point::ShowPosition() {

cout<<x<<" "<<y<<endl;

}

Point Point::operator+(int val) {

Point temp(x+val, y+val);

return temp;

}


int main(void)

{

Point p1(1,2);

Point p2=p1+3;

p2.ShowPosition();


return 0;

}


//결과값 : 4 5


 두번째 볼드처리된 문장을 보시면 p1과 3을 덧셈 연산하고 있습니다. 다음과 동일한 문장이 될겁니다.

-> Point p2 = p1.operator+(3);


 첫번째 볼드처리된 문장을 봅시다. 여기서 정의되어있는 operator+ 함수를 보면 x, y 인자에 전달된 피연산자 val을 더해서 새로운 객체를 생성 및 리턴하고 있습니다. 말씀드리고자하는 중요한 사실은 일반적인 + 연산자와 operator+ 함수의 기능이 일치한다는 것이죠. 본래의 + 연산자의 기능을 크게 벗어나지 않는 것이 좋은 연산자 오버로딩입니다.


[교환 법칙의 적용]


 이미 Point 클래스의 객체가 정의 되어있다고 가정한다면 Point 클래스의 객체를 이용해서 10 + p 와 같은 형태의 연산도 가능할까요? 방금 실행했던 예제처럼 오버로딩이 되어있다고 했을때라면 결론은 "불가능하다" 입니다. 10 + p는 다음과 동일한 문장이라고 할 수 있습니다.

                                    -> 10.operator+(p);

뭔가 이상하죠? 결국 전역 함수에 의한 연산자 오버로딩이 필요하다는 결론이 나옵니다. 즉, 우리 입장에서는 p + 10 과 10 + p 가 서로 다른 문장으로 취급된다는걸 알고 있지만 +연산자는 교환법칙이 성립되므로 같게 만들어 주어야 합니다.


그러면 다음예제를 통해서 10 + p 가 "p + 10" 이 되도록 바꿔주기만 하면 됩니다.


Point operator+(int val, Point& p)

{

return p+val;

}


위와 같이 정의하면 Point p3 = 3 + p2 는 내부적으로 어떻게 해석이 될까요?

Point p3 = 3 + p2   ---->   Point p3 = operator+(3,p2)   ---->   Point p3 = "p2+3;의 연산 결과"

이해가 가시나요~? 어렵지 않습니다.


*참고 : + 연산자가 두가지 형태로 오버로딩된 부분이 이상할 수 있습니다. 하지만 오버로딩된 함수를 호출하는 피연산자의 형태가 각각 다르기 때문에 전혀 문제되지 않습니다. 물론 피연산자의 형태가 둘다 동일하다면 문제가 되겠죠?


[임시 객체의 생성]


 임시 객체란 임시적으로 생성되었다가 소멸되는 객체입니다. 다음 예제는 임시 객체 생성의 예를 보여줍니다.


#include <iostream>

using std::endl;

using std::cout;


class AAA{

char name[20];

public:

AAA(char* _name) {

strcpy(name, _name);

cout<<name<<" 객체 생성 " <<endl;

}

~AAA() {

cout<<name<<" 객체 소멸 "<<endl;

}

};


int main(void)

{

AAA aaa("aaa Obj");

cout<<" ---임시 객체 생성 전---"<<endl;

AAA(" Temp Obj");

cout<<"---임시 객체 생성 후---"<<endl;

return 0;

}


볼드 처리된 부분을 보면 AAA클래스의 생성자를 호출하면서 임시 객체를 생성하고 있습니다. 그런데 이러한 임시 객체의 특징은 "이름이 없다" 는 것입니다. 따라서 생성한 이후에 그 줄에서 사용하지 않으면 바로 소멸되어 버리는 특징을 가집니다.


실행결과 :

aaa Obj 객체 생성

---임시 객체 생성 전---

Temp Obj 객체 생성

Temp Obj 객체 소멸

---임시 객체 생성 후---

aaa Obj 객체 소멸


 실행 결과는 임시 객체가 생성되었다가, 그 다음줄로 넘어가면서 바로 소멸된다는 것을 보여주고 있습니다.

그렇다면 이러한 임시 객체를 언제 사용할까요? 임시 객체의 효율적인 사용 예를 하나 들어보겠습니다.


Point Point::operator+(int val)

{

Point temp(x+val, y+val);

return temp;

}


 위 함수는 새로운 객체를 만들어서 리턴하는 함수입니다. 그래서 temp라는 이름의 객체를 생성하고, 그 다음줄에서 그 객체를 리턴해주고 있지요. 임시 객체를 사용하면 다음과 같이 간결하게 표현할 수 있습니다.


Point Point::operator+(int val) {

return Point(x+val, y+val);

}


 위 함수는 임시 객체를 생성하자마자 바로 리턴해주고 있습니다. 임시 객체는 다음 줄로 넘어가면 소멸된다고 했지만 위의 경우에는 생성하자마자 리턴해 주므로 문제될 것이 없겠습니다.


 한가지 더 중요한 사실은 위와 같은 형태로 임시 객체를 사용할 경우, 컴파일러에 따라서 프로그램 최적화가 진행된다는 사실입니다.


[cout, cin 그리고 endl의 구현 이해]


 지금 소개할 내용은 전혀 새로운 내용의 주제가 아닙니다. 이미 우리가 사용하고있고 알고 있는 것들을 위주로 살펴보도록 하겠습니다. 우선 다음 예제를 살펴보겠습니다.


#include <stdio.h>


namespace mystd

{

char* endl = " \n"; //\는 역슬래쉬인거 아시죠?


class ostream

{

public:

void operator<<(char * str) {

printf("%s", str);

}

void operator<<(int i) {

printf("%d", i);

}

void operator<<(double i) {

printf("%e", i);

}

};

ostream cout; //ostream 객체 생성

}


using mystd::cout;

using mystd::endl;


int main(void)

{

cout<<"hello\n";

cout<<1.23;

cout<<endl;

cout<<1;

cout<<endl;

return 0;

}


 여기서 사용한 cout와 << 연산자는 우리가 직접 정의한 것 입니다. 출력을 위한 << 연산자는 오버로딩된 함수의 형태를 지니고 있는거 아시겠죠? mystd라는 이름 공간은 표준 이름공간 std를 흉내 낸 것 입니다. mystd 안에는 ostream 이라는 출력을 위한 클래스도 정의되어 있습니다. ostream 클래스의 객체도 cout 라는 이름으로 생성되어 있습니다.


 우리는 위의 예제를 통해서 cout 와 << 연산자의 정체를 파악할 수 있게 되었습니다. 그러나 여기에는 한가지 문제가 있습니다. 다음 문장을 보세요~


 cout<<"Hello"<<100<<endl;


위 문장은 아래 문장과 동일합니다.


 ((cout<<"hello")<<100)<<endl;


그렇다면 실행 과정은 다음과 같이 진행될 겁니다.


  ((cout<<"hello")<<100)<<endl;  ====> (cout<<100)<<endl; ====> cout<<endl;

  ->연산후 cout 리턴                                ->연산후 cout 리턴


즉, 연산자를 오버로딩하고 있는 operator<< 함수는 cout 객체를 반환해야 된다는 중요한 결론이 나옵니다. 따라서 다음과 같이

이름공간을 수정해보겠습니다.


namespace mystd

{

char* endl = " \n"; //\는 역슬래쉬인거 아시죠?


class ostream

{

public:

ostream& operator<<(char * str) {   //참조에 의한 리턴

printf("%s", str);

return *this;

}

ostream& operator<<(int i) {

printf("%d", i);

return *this;

}

ostream& operator<<(double i) {

printf("%e", i);

return *this;

}

};

ostream cout; //ostream 객체 생성

}

 


operator<< 함수의 정의를 보면 마지막에 자기 자신을 리턴하고 있음을 볼 수 있습니다. 또한 리턴 타입이 ostream 이 아닌 ostream& 입니다. 참조에 의한 리턴을 하고 있는 것이죠. 사실 이 상황에서는 어떠한 형태로 리턴을 하건 실행 결과에는 차이가 없습니다. 하지만 리턴하는 대상이 함수 내에서 생성된 객체가 아니므로, 참조에 의한 리턴이 보다 효율적이기 때문이죠.


 [<<, >> 연산자의 오버로딩]


다음 문장이 가능하도록 오버로딩을 하고 싶을땐 어떻게 해야될까요?


int main(void)

{

Point p(1, 2);

cout<<p;

}


 결과값은 [1, 2]가 출력되기를 기대하고 있습니다. "cout<<p"의 형태로 객체 p의 맴버 변수가 출력되기를 원한다면, 역시 연산자 오버로딩 밖에 방법이 없습니다. 만약 맴버 함수에 의한 방법으로 해결을 하고자 한다면 다음 문장이 성립되어야 합니다.

 cout<<p   ->  cout.operator<<(p)

하지만 cout 객체의 맴버함수 operator<<는 Point 객체 p를 인자로 받을 수 있을까요? Point 객체는 우리가 임의대로 정의한 클래스 입니다. 때문에 객체 p를 인자로 받는 다는 것은 불가능하지요.


 만약에 맴버 함수에 의한 오버로딩이 가능하려면 sotream 클래스에 다음과 같은 형태의 맴버 함수를 정의해 넣어야 합니다만 불가능한 일입니다. 왜냐하면 c++표준에서 제공하는 클래스를 임의로 변경할 수 없기 때문입니다.


 ostream& operator<< (const Point& p)


 따라서 전역 함수에 의한 오버로딩 방식을 사용해야합니다. 다음과 같은 형태로 말이죠


 ostream& operator<< (ostream& os, const Point& p)


전체적인 예를 나타내보겠습니다.


#include <iostream>

using std::endl;

using std::cout;


using std::ostream;


class Point {

private:

int x, y;

public:

Point (int _x = 0, int _y = 0):x(_x),y(_y){}

friend ostream& operator<<(ostream& os, const Point& p);

};


ostream& operator<<(ostream& os, const Point& p) //cout을 인자로 전달 받는다. 따라서 ostream 객체를 인자로 받는다고

{                                                                          선언되어 있는데, ostream은 이름공간 std 안에 선언되어 있다.

os <<" [ " <<p.x<<","<<p.y<<"]"<<endl;

return os;

}


int main(void)

{

Point p(1,3);

cout<<p; //operator<< (cout, p); 와 동일한 문장입니다.

return 0;

}

 

[배열의 인덱스 연산자 오버로딩]

 

 이번에 오버로딩해 볼 것은 배열의 요소에 접근할 때 사용되는 []연사자 입니다.(array[3]) 다들아시죠~?

다음예제를 기준으로 설명하겠습니다. 일단 기본이 되는 예제를 살펴보도록 하겠습니다.

 

#include <iostream>

using std::endl;

using std::cout;

 

const int SIZE=3;

 

class Arr {

private:

int arr[SIZE];

int idx;

public:

Arr() : idx(0) {}

int GetElem(int i);

void SetElem(int i, int elem);

void AddElem(int elem);

void ShowAllData();

};

int Arr::GetElem(int i) {

return arr[i];

}

void Arr::SetElem(int i, int elem) {

if(idx <= i) {

cout<<"존재하지 않는 index 입니다.!"<<endl;

return;

}

arr[i]=elem;

}

void Arr::AddElem(int elem) {

if(idx >= SIZE) {

cout<<"index 용량 초과!"<<endl;

return;

}

arr[idx++]=elem;

}

void Arr::ShowAllData() {

for(int i=0; i<idx; i++)

cout<<"arr["<<i<<"]="<<arr[i]<<endl;

}

 

int main(void)

{

Arr arr;

arr.AddElem(1);

arr.AddElem(2);

arr.AddElem(3);

arr.ShowAllData();

 

arr.SetElem(0, 11);

arr.SetElem(1, 22);

arr.SetElem(2, 33);

 

cout<<arr.GetElem(0)<<endl;

cout<<arr.GetElem(1)<<endl;

cout<<arr.GetElem(2)<<endl;

 

return 0;

}

 

 위의 예제는 따로 설명 안드려도되겠죠? 충분히 이해하시리라 믿습니다.

 

자 여기서 우리가 정의한 클래스를 이제 인덱스 연산자([])를 통한 참조가 가능하도록 만들어야합니다.

아래와 같은 식으로 말이죠.

 

int main(void)

{

Arr arr;

arr.AddElem(1);

arr.AddElem(2);

arr.AddElem(3);

arr.ShowAllData();

 

arr[0] = 1;

arr[1] = 2;

arr[2] = 3;

 

cout<<arr[0]<<endl;

cout<<arr[1]<<endl;

cout<<arr[2]<<endl;

return 0;

}

 

 여기서는 인덱스 연산자를 이용해서 접근을 시도함을 알 수 있습니다.

arr[i] 는 arr.operator[](i) 와 같은 문장으로 해석이 될 겁니다. 이러한 인덱스 연산자에

대입연산자를 사용하여 사용이 되고 있습니다. 여기서 중요한 것! 대입 연산자를 사용하기 위함은

즉, 참조에 의한 리턴이 이루어져야 가능한 일입니다. 때문에 다음과 같은 맴버 함수가 필요하겠죠?

int& Arr::operator[] (int i) {

return arr[i];

}

 

 자 이번에는 조금 더 생각해볼 문제입니다.

만약에 Point 클래스가 있고 p1과 p2 객체를 만들었다고 해봅시다.

main함수에서 p1= p2; 라는 문장이 성립이 될까요? 예 성립이 됩니다. 물론 클래스 정의부분에서는

오버로딩하는 함수가 따로 없더라도 말입니다.

 

 왜냐하면 대입연산자를 오버로딩하는 함수도 프로그래머가 정의해 주지 않으면 디폴트 대입 연산자라는 이름으로 제공이

됩니다. 즉, 맴버 변수 대 맴버 변수의 복사를 이루게되죠. 다음과 같은 맴버 함수가 정의되어 있습니다.

 

Point& Point::operator=(const Point& p)

{

x = p.x;

y = p.y;

return *this;

}

 

 리턴 타입이 Point& 형이고, 자기 자신을 리턴하는 이유는 p1 = p2 = p3 와 같은 문장을 허용하기 위함이죠.

그러나 디폴트 대입 연산자에는 문제점이 하나 있습니다. 일단 다음 예제를 살펴보도록 하겠습니다.

 

#include <iostream>

using std::endl;

using std::cout;

using std::ostream;

 

class Person {

private:

char* name;

public:

Person(char* _name);

Person(const Person& p);

~Person();

friend ostream& operator<<(ostream& os, const Person& p);

};

 

Person::Person(char* _name) {

name = new char[strlen(_name)+1];

strcpy(name,_name);

}

Person::Person(const Person& p) {

name = new char[strlen(p.name)+1];

strcpy(name,p.name);

}

Person::~Person() {

delete[] name;

}

 

ostream& operator<<(ostream& os, const Person& p)

{

os<<p.name;

return os;

}

 

int main()

{

Person p1("ABC");

Person p2("DEF");

 

cout<<p1<<endl;

cout<<p2<<endl;

 

p1 = p2;

 

cout<<p1<<endl;

return 0;

}

 

 자 과연 어느 부분에서 문제가 생기는 걸까요? 네 디폴트 대입 연산자에 있습니다. 물론 보이지는 않습니다.

아래의 문장이 Person 클래스에 자동으로 삽입되는 디폴트 대입 연산자의 형태입니다.

 

Person& Person::operator=(const Person& p)

{

name=p.name;

return *this;

}

 

 이러한 대입 연산자는 맴버 대 맴버를 복사하는 형태로 정의되어 있습니다. 하지만 문제가 생깁니다.

p1 = p2 라는 문장을 생각해봅시다.

 문제는 얕은 복사에 있습니다. 때문에 p1의 맴버 변수 name이 단순하게 p2 의 맴버변수 name이 가리키는 대상을 가리키게 될 것 입니다. 이는 포인터 부분에서 설명한 적 있는데요. 이유는 단순히 포인터 값만 복사했기 때문입니다. 그렇다면!! 객체가 소멸되는 시점에 각각의 객체의 소멸자가 호출될 겁니다. 그 그결과 p1의 맴버 변수에 저장되어있던 값은 이 문자열을 가리키는 대상이 없어졌기 때문에 접근이 불가능하게 됩니다. 동적으로 할당된 메모리 공간이기 때문에 메모리 유출이 발생을 하겠죠? 그리고 p2의 맴버 변수인 name은 p1의 소멸자, p2의 소멸자가 각각 호출되기때문에 두번 삭제되는 문제점을 가지고 있습니다.

 때문에 우리가 직접 정의해주는 대입 연산자에서는 적절한 메모리 해제와 깊은 복사가 이루어지도록 정의해주어야 합니다. 다음과 같이 말이죠

 

Person& Person::operator=(const Person& p)

{

delete []name; //메모리 유출방지. 즉, p1의 name이 할당한 메모리공간을 해제시켜줍니다.

name = new char[strlen(p.name)+1];

strcpy(name,p.name);

return *this;

}

 

 자 이번장은 조금 길었습니다.... 가내 수작업이라 며칠동안 나눠서 작업을 했네요..ㅜ

슬슬 c++도 마무리 지어가는 것 같습니다! 화이팅!

 


 2013년 첫 포스팅이네요~ 새해 복 많이들 받으시구요

최고의 해를 보내기 위해 열심히 노력합시다~


 이번 시간에는 "연산자 오버로딩" 에 관하여 살펴보도록 하겠습니다.

함수만 오버로딩 되는 것이 아니라 C++에서는 연산자도 오버로딩 됩니다~


[operator+ 라는 함수]


#include <iostream>

using std::endl;

using std::cout;


class Point {

private :

    int x, y;

public:

    Point (int _x = 0, int _y = 0 ) : x(_x), y(_y) {}

    void ShowPosition();

    void operator+ (int val);

};


void Point::ShowPosition() {

    cout<<x<<" "<<y<<endl;

}

void Point::operator+(int val) {

x+=val;

y+=val;

}


int main(void)

{

Point p(3,4);

p.ShowPosition();


p.operator+(10);

p.ShowPosition();

return 0;

}


여러운건 특별히 없습니다. 실행 결과로는

 3  4

13 14

가 출력되겠죠~? 자 이제 중요합니다 위 의 예제에서 main 함수를 변경해보겠습니다.


int main(void)

{

Point p(3,4);

p.ShowPosition();


p+10;

p.ShowPosition();

return 0;

}


위의 예제로 변경해도 결과값은 똑같습니다.

p+10 이라는 부분이 이해가가 가나요~? + 연산자 왼쪽에는 Point 객체 p가, 오른쪽엔 int 형 데이터가 있습니다.

operator 라는 키워드와 연산자 기호를 사용하여 함수이름을 정의하면, 함수의 이름을 이용한 함수 호출뿐만 아니라

연산자를 이용한 함수 호출도 허용이 가능해집니다.


 즉, p+10이라는 문장은 p가 그냥 기본 자료형 변수라면 덧셈을 할겁니다. 하지만 p가 객체라면 p.operator+(10) 이라는

문장으로 해석하게 되는 겁니다.


  p+10 ------------------> p.operator+(10) 둘은 완전히 같은 문장이라고 보셔도 됩니다.


[연산자 오버로딩의 두가지 방법]


 첫 번째로는 맴버 함수에 의한 오버로딩, 두 번째는 전역 함수에 의한 오버로딩 입니다.


 1. 맴버 함수에 대한 연산자 오버로딩


class Point {

private:

int x,y;

public:

Point(int _x=0, int _y=0) : x(_x), y(_y) {}

void ShowPosition();

};

void Point::ShowPosition() {

cout<<x<<" "<<y<<endl;

}


 위 예제는 x, y 좌표를 나타내기 위한 Point 클래스입니다. 우리는 이러한 Point 클래스 객체를 대상으로

+ 연산을 하기 원합니다. 다음 main 함수처럼 말이죠.


int main(void)

{

Point p1(1, 2);

Point p2(2, 1);

Point p3 = p1 + p2;

p3.ShowPosition();


return 0;

}


 main 함수를 보시면 아~ p3는 p1의 좌표와 p2의 좌표를 더한 값을 나타내는구나~ 라고 생각할 수 있습니다.

분명한 것은 operator+ 라는 이름의 함수를 호출하는 과정이 필요하다는 것 입니다. 그렇다면 operator+ 라는

이름의 함수는 어디에 있어야 할까요?


  operator+ 라는 이름의 함수는 맴버 함수로 존재할 수도, 전역 함수로도 존재할 수 있습니다. 지금은 맴버 함수로

존재하는 경우를 알아보고 있으니 다음과 같이 구현해 보도록 하겠습니다.


Point Point::operator+ (const Point& p) const

{

Point temp(x+p.x, y+p.y);

return temp;

}


 위의 코드는 인자로 Point 객체를 받고 있습니다. 성능 향상을 위해서 레퍼런스로 받고 있으며, 전달 인자의 변경을

허용하지 않기 위해서 const 선언을 동시에 해주고 있습니다. 또한 함수 내에서 맴버 변수의 조작을 할 필요가 없으므로

함수도 상수화를 하여 안정성을 높였죠.


 

class Point {

private:

int x,y;

public:

Point(int _x=0, int _y=0) : x(_x), y(_y) {}

void ShowPosition();

Point operator+ (const Point& p);

};

void Point::ShowPosition() {

cout<<x<<" "<<y<<endl;

}

 

Point Point::operator+ (const Point& p) const

{

Point temp(x+p.x, y+p.y);

return temp;

}


int main(void)

{

Point p1(1, 2);

Point p2(2, 1);

Point p3 = p1+p2; // p1+p2 ----> p1.operator+(p2)

p3.ShowPosition();


return 0;

}


 Point p3 = p1 + p2   ------------> Point p3 = " 리턴 된 temp 객체" 


 

 

이정도면 충분히 이해가 가셨나요?


 2. 전역 함수에 대한 연산자 오버로딩


이번에는 위의 예제들을 전역 함수에 의한 오버로딩 방식으로 변경해서 공부해 보겠습니다.


class Point {

private:

int x, y;

public:

Point (int _x = 0, int _y = 0) : x(_x), y(_y) {}

void ShowPosition();

};

void Point::ShowPosition() {

cout<<x<<" "<<y<<endl;

}


int main(void)

{

Point p1(1, 2);

Point p2(2, 1);

Point p3 = p1 + p2;

p3.ShowPosition();


return 0;

}


 자 여기서 한가지 짚고 넘어가야 될 부분이 있습니다.

p1 + p2 가 맴버 함수로 오버로딩 된 경우는 p1.operator+(p2)와 동일한 것은 알겁니다. 그렇다면

전역 함수로 오버로딩 된 경우는 어떨까요~? operator+ (p1, p2)가 될겁니다.  왼쪽에 있는 피연산자가

첫 번째 인자로, 오른쪽에 있는 피연산자가 두 번째 인자로 전달이 됩니다.


 다음은 + 연산자를 오버로딩하고 있는 전역 함수 입니다.


Point operator+ (const Point& p1, const Point& p2)

{

Point temp(p1.x+p2.x, p1.y+p2.y);

return temp;

};


 그러나 이상한점이 있지않나요~? Point 객체의 private 맴버에 직접 접근하고 있음을 볼 수 있습니다. 그런대

여기서 정의한 operator+ 함수는 전역 함수, 즉 Point 클래스의 외부라는 것 입니다. 따라서 private 영역에 접근이

불가능 하겠죠? 때문에 friend 선언을 이용해야 합니다.


 

class Point {

private:

int x, y;

public:

Point (int _x = 0, int _y = 0) : x(_x), y(_y) {}

void ShowPosition();

friend Point operator+(const Point& p1, const Point& p2);

};

void Point::ShowPosition() {

cout<<x<<" "<<y<<endl;

}

Point operator+ (const Point& p1, const Point& p2)

{

Point temp(p1.x + p2.x, p1.y + p2.y);

return temp;

}


int main(void)

{

Point p1(1, 2);

Point p2(2, 1);

Point p3 = p1 + p2;

p3.ShowPosition();


return 0;

}


 자 이렇게 두가지 방법을 알아보았습니다. 그렇다면 어떤 방법을 사용하는 것이 좋을까요?

사실 객체지향에는 전역이라는 개념이 존재하지 않습니다. 때문에 맴버 함수를 이용하는 방법이

좀 더 좋은 방법이라고 말할 수 있겠네요. 

 


[연산자 오버로딩의 주의사항]


 1. 의도를 벗어난 연산자 오버로딩은 피하자!

ex) p+3 라는 문장을 보면 p 객체의 맴버 x, y 에 3 을 더해서 새로운 객체를 생성해서 리턴하라는 것인지,

      p객체의 맴버 x, y 자체가 3씩 증가하겠는가? 

 2. 연산자 우선 순위와 결합성을 바꿀 수는 없다.

 3. 디폴트 매개 변수 설정이 불가능하다.

 4. 디폴트 연산자들의 기본 기능까지 빼앗을 수 없다.

ex)

int operaotor+(int a, int b)

{
    return a+b+3;

}

-->int 형 변수의 + 연산은 이미 그 법칙이 정해져 있습니다. 이처럼 디폴트 연산자들이 지니고 있는 기능을 변경하는

형태로는 연산자를 오버로딩 시킬 수 없습니다.


[단항 연산자의 오버로딩]


 1. 증가, 감소 연산자 오버로딩


 대표적인 단항 연산자로 증가 및 감소 연산자(++,--)가 있습니다. 단항연산자 역시 두가지 형태(맴버함수, 전역함수)로 오버로딩이 가능합니다.


++p   --->  1. p.operator++() : 맴버 함수로 오버로딩 된 경우

                2. operator++(p)  : 전역 함수로 오버로딩 된 경우


다음은 Point 클래스를 대상으로 ++, -- 연산자를 오버로딩한 예제입니다. 단, ++연산자는 맴버함수에 의한 방법이고 --연산자는 전역 함수에 의한 방법을 사용했습니다.


#include <iostream>

using std::endl;

using std::cout;


class Point {

private:

int x, y;

public:

Point(int _x = 0, int _y = 0) : x(_x), y(_y) {}

void ShowPosition();

Point& operator++();

friend Point& operator--(Point& p);

};

void Point::ShowPosition() {

cout<<x<<" "<<y<<endl;

}


Point& Point::operator++() {

x++;

y++;

return *this;

}


Point& operator--(Point& p) {

p.x--;

p.y--;

return p;

}


int main(void)

{

Point p(1, 2);

++p;

p.ShowPosition();  // 2 3


--p;

p.ShowPosition();  // 1 2


++(++p);

p.ShowPosition(); // 3 4


--(--p);

p.ShowPosition();  // 1 2


return 0;

}


 이해가 가셨을꺼라 생각됩니다. 자 여기서 중요한 부분을 한가지 살펴보고 넘어가도록 하겠습니다.

세 번째 볼드 처리된 문장 보이시나요? return *this 부분입니다. *this 가 의미하는 것은 무엇일까요?

this는 객체 자신을 가리키는 포인터입니다. 여기에 * 연산을 하게 되면 포인터가 가리키는 대상을 참조하겠다는

뜻이 됩니다. 즉, 자기 자신을 리턴 하겠다는 의미를 가지고 있습니다

 그렇다면 왜 자기 자신을 리턴할까요?

맴버 변수 x, y 값을 증가시켜주는 걸로 충분하지 않을까요? 만약에 아무것도 리턴하지 않는다면 ++p 연산 후에는

아무것도 존재하지 않으므로 그 다음 연산은 불가능하게 되어 컴파일 오류를 발생시킵니다.

 리턴 타입이 Point가 아니라 Point&인 이유는 무엇일까요?

위의 예제에서 리턴 타입이 Point&가 아니라 Point라고 한다면 ++(++p) 는 값이 1만 증가할 것 입니다. ++p 연산에

의해서 리턴되는 것은 p의 참조가 아닌, p의 복사본이기 때문입니다.

==> [참조를 리턴하는 경우] : ++(++p) -> ++(p의 참조) -> p의 값 2 증가

      [복사본을 리턴하는 경우] : ++(++p) -> ++(p의 복사본) -> p의값 1 증가, p의 복사본 값 1증가


2. 선 연산과 후 연산의 구분


 예제는 위의 예제와 동일하다는 가정하에 살펴보겠습니다.


int main(void)

{

Point p1(1, 2);

(p1++).ShowPosition();


Point p2(1, 2);

(++p2).ShowPosition();

return 0;

}


 여기서 예상되는 결과 값이 있나요~? 그러나 실행 결과는 다음과 같이 나옵니다.

2 3

2 3

으로 말이죠. 그렇다면 전위 연산자와 후위 연산자가 구분이 안된다는 결론이 나옵니다.

즉, ++p 와  p++ 와는 별다를게 없이 동일하게 해석된다는 문제가 있습니다.


 그러나 c++은 다음과 같은 방법을 제안합니다.

"++ 연산의 경우, 전위 증가와 후위 증가의 형태를 구분 짓기 어려우므로, 후위 증가를 위한 함수를

오버로딩할 경우, 키워드 int를 매개변수로 선언하면 그것이 후위 증가를 의미하는 것으로 간주합니다."


 즉, ++p -> p.operator++();

      p++ -> p.operator++(int); 가 되는겁니다.


 자 이제 우리가 원하는 형태의 결과를 얻을 수 있도록 연산자를 오버로딩하는 것 입니다. 우선 main 함수와

원하는 형태의 출력 결과를 살펴보겠습니다.


int main(void)

{

Point p1(1, 2);

(p1++).ShowPosition(); //1 2 출력

p1.ShowPosition(); // 2 3 출력


Point p2(1, 2);

(++p2).ShowPosition(); // 2 3 출력

return 0;

}


#include <iostream>

using std::endl;

using std::cout;


class Point {

private :

int x, y;

public:

Point (int _x = 0, int _y = 0) : x(_x), y(_y) {}

void ShowPosition();

Point& operator++();

Point operator++(int);

};


void Point::ShowPosition() {

cout<<x<<" "<<y<<endl;

}


Point& Point::operator++() {

x++;

y++;

return *this;

}

Point Point::operator++(int) {

Point temp(x, y);

x++;

y++;

return temp;

}


 후위 연산자를 오버로딩하기 위한 함수를 살펴봅시다. temp 객체가 사용되었습니다. 이는 값을 증가하기 전의 상태를 보존하기 위해서  임시 객체를 생성한 것 입니다. temp를 리턴하므로 값이 증가하기 전의 복사해 놓은 객체를 내놓겠죠? temp는 맴버 함수 내에서 정의된 지역 객체기 때문에 당연히 리턴형태는 레퍼런스가 아닌 Point 형으로 지정해주었습니다.


 이번 강의는 여기까지입니다. 아직 더 남아있지만 내용이 다소 길어질 것 같아 2회로 나누어서 올리겠습니다.

수고하셨습니다^-^

+ Recent posts