[만화가 있는 C] 8. 연산자

8. 연산자(operator)

  [이 장에서는 아직 설명하지 못한 클래스, 구조체, 배열 등에 필요한 연산자의 설명을 포함합니다. 이러한 부분은 건너뛰었다가 나중에 다시 참고하기 바랍니다.]

  각 연산자의 명확한 기능을 이해하는 것이 필요합니다. 이 장에서는 연산자의 형식을 설명하고, 예제를 통해 사용법을 설명합니다. 연산자는 다음과 같은 범주로 구분할 수 있습니다.

    산술(Arithmetic)

    할당(Assignment)

    비트(Bitwise)

    C++에서만 사용가능(C++ specific)

    콤마(Comma)

    조건(Conditional)

    논리(Logical)

    후위표기(Postfix)

    전처리(Preprocessor)

    참조/역참조(Reference/Dereference)

    관계(Relational)

    sizeof

    형 변환(casting)

  C++에서 아래의 연산자들은 오버로드(overload) 될 수 없습니다.

 C++에서는 연산자의 기능을 사용자가 정의할 수 있습니다. 같은 연산자 함수가 여러 개 정의되면 문맥에 따라 적절한 함수가 호출됩니다. 이것을 연산자 오버로딩(operator overloading)이라고 합니다.

    .   C++ 직접 요소 선택(direct component selector)

    .*  C++ 재참조(dereference)

    ::   C++ 범위 접근/해결(scope access/resolution)

    ?:  조건 연산자(Conditional)

  위의 연산자들을 제외한 모든 연산자들은 오버로드 할 수 있습니다. 문맥에 따라, 같은 연산자 기호가 한 가지 이상의 의미를 가질 수 있습니다. 예를 들면, 앰퍼샌드ampersand(&)는 다음과 같이 해석될 수 있습니다.

 C에서도 이미 정의된 연산자(built-in operator)의 오버로딩은 지원되었습니다. 실제로 1+2와 1.0+2.0은 다른 덧셈을 합니다.

  (1) 비트 AND 연산자

  (2) 주소(address-of) 연산자

  (3) C++에서 참조에 의한 호출 변경자(reference modifier)

  2개의 심벌이 1개의 연산자 단위를 이룰 경우, 2개의 심벌 사이에 공백이 들어가면 안됩니다. 또한 위치가 바뀌어서도 안됩니다. 예를 들면, 관계연산자 >=가 > = 혹은 =>로 쓰이면 의미가 달라지거나 문법적인 오류입니다.


산술(Arithmetic)

  이제부터 설명하는 연산자들은, 특별히 명시되지 않는 한 이항 연산자binary operator입니다. 실제로 단항unary이나 삼항ternary 연산자들은 몇 개 되지 않습니다.

 피연산자(operand)의 수가 반드시 2개임을 의미합니다.

+      단항 부호연산자, 양수positive number를 의미합니다.

–       단항 부호연산자, 음수negative number를 의미합니다.

+      덧셈addition 연산자

–       뺄셈subtraction 연산자

*       곱셈multiplication 연산자

/       나눗셈division 연산자

%      나머지modulo 연산자

++     단항 전위prefix 증가increment 연산자

++     단항 후위postfix 증가 연산자

—      단항 전위 감소decrement 연산자

—      단항 후위 감소 연산자

  산술 연산자 +, -, *, /의 의미는 수학적인 의미와 같습니다. 가감승제 연산이 피연산자의 타입type에 따라 다른 연산을 한다는 것에 주의하세요. 2+3과 2.0+3.0은 다릅니다. 전자의 결과는 5이지만, 후자의 결과는 5.0입니다. 우리는 5의 표현과 5.0의 표현이 메모리 내부에서는 다르다는 것을 알고 있습니다. 정수integer는 2의 보수로 표현되지만, 실수floating는 부동 소수점floating point 표기법으로 표현됩니다. 덧셈, 뺄셈, 곱셈에서 그 차이를 별로 느낄 수 없지만, 나눗셈에서는 심각한 문제가 발생할 수 있습니다. 다음의 결과를 주목하세요.

#include <stdio.h>

void main() {
    int i=31,j=39;
    int k=10;
    printf("%d,%d\n",i/k,j/k);
    //      3,3
}

  정수 나눗셈의 결과는 소수점 이하는 항상 무시됩니다. 결과가 3.1이든 3.9이든 3이 됩니다.

 이것을 flooring이라고 합니다. 수학적인 기호로 ⌊3.9⌋=3처럼 씁니다. ⌈3.1⌉=4는 ceiling을 의미합니다. ⌈3.1⌉이든 ⌈3.9⌉이든 4입니다.

  나머지 연산자 %는 나눗셈의 나머지를 구합니다. i=10/3의 결과 i는 3입니다. 하지만, i=10%3의 결과 i는 1입니다. 10을 3으로 나눈 나머지remainder는 1이기 때문입니다. 나머지 연산자의 사용법을 정확히 익혀 두기 바랍니다. 다음과 같은 문제를 해결하는데 나머지 연산자가 사용될 수 있습니다

  (1) n의 배수를 구하는 문제

  (2) 화면의 (x,y)좌표의 실제 선형 메모리의 위치를 구하는 문제

  아래의 예는 1에서 100사이의 정수 중 3의 배수를 출력합니다.

#include <stdio.h>

void main() {
    int i=0;
    while ((++i)<=100)
        if (i%3==0) printf("%d,",i);
}

  증감 연산자는 대입 연산자 =처럼 부효과side effect가 있습니다. 즉, 표현식 자체가 변수의 값을 변경시킵니다. 일반적인 표현식은 식 자체가 값을 가지지만, 어떠한 변수의 값도 변경시키지 않습니다. 예를 들면, 3+5라는 표현식expression은 8입니다. 하지만, 어떠한 값도 변경시키지는 않습니다. i=3, j=5 라고 합시다. i+j는 8입니다. 하지만, i와 j는 변하지 않습니다. 즉 표현식은 식을 평가evaluate할 뿐 변수 값을 변경시키지 않습니다. 하지만, k=i+j라는 문장을 고려해 봅시다. i+j라는 식의 평가값이 k를 변경시킵니다. 이러한 표현식을 우리는 ‘부효과가 있다’고 합니다. 즉 표현식이 실행되고 난 뒤 변경되는 상태값(변수값)이 있다면 그 표현식에는 부효과가 있습니다.

++i

  위 문장은 i의 값을 1증가시킵니다. i의 값이 변하므로 부효과가 있습니다. 위 문장은 다음 문장과 동일합니다.

i++

  i를 1증가시키는 문장을 정리해 보도록 하겠습니다. 아래의 문장들은 모두 i값을 1증가시키는 문장이며, 모두 부효과가 있습니다.

    i=i+1;

    ++i;

    i++;

    i+=1;

 i+=1을 i=+1과 혼동하지 마세요. 전자는 i를 1증가시킵니다. 하지만 후자는 i에 양수 1을 대입합니다. i-=1과 i=-1 역시 마찬가지입니다.

  마지막 문장은 아직 다루지 않았지만, 대입 연산자에 의해 i값이 1 증가합니다. 증가 연산자를 전위에 붙이는 것과 후위에 붙이는 것은 어떤 차이가 있을까요? 우리는 이 차이를 이해하고 있어야 합니다. C++에서 전위 증감 연산자와 후위 증감 연산자를 오버로딩하는 문법에도 차이가 있습니다. 아래의 문장을 기억하세요.

“전위 증감 연산자는 식을 평가하기 전에 먼저 증감합니다. 후위 증감 연산자는 식을 평가한 후 증감됩니다.”

  i=2,j=3인 경우, i=i+(++j)와 i=i+(j++)는 다릅니다. 전자인 경우 i=6입니다. 후자인 경우 i=5입니다. 물론 둘 다 j=4입니다. 그렇다면 여러 개의 변수에 증감연산자가 사용되었을 때 어떻게 해석해야 할까요? 컴파일러는 ①전위 연산자를 위해 코드를 생성하고, ②표현식을 위해 코드를 생성한 다음, ③후위 연산자를 위해 코드를 생성합니다. 아래의 문장을 고려해 봅시다.

i=i+(++j)-(k–)+(j++)+k;

  위의 문장은 다음과 같이 번역됩니다.

    ① j=j+1;

    ② i=i+j-k+j+k;

    ③ k=k-1; j=j+1;

  위와 같은 일련의 문장이 실행되어 i,j,k의 값이 결정됩니다. 무척 복잡합니다. 증감 연산자의 혼용은 코드를 읽기 어렵게 만듭니다. 증감 연산자는 위의 예에서처럼 같은 변수에 대해서, 또 전위와 후위를 썩어서 사용하는 것은 권장되는 사항이 아닙니다. 증감 연산자를 사용하는 관례(convention)는 다음과 같습니다.

“한 표현식에서는 같은 형태 – 전위 혹은 후위 – 의 증감 연산자를 사용합니다. 같은 형태라도 한 변수에 대해서 1번 이상의 증감 연산을 사용하지 않습니다.”

  위의 예에서는 전위와 후위를 썩어 쓰고 있을 뿐만 아니라, 같은 변수 j에 대해서 2번씩이나 증감을 하고 있으므로 잘못 사용하고 있는 것입니다.


할당(Assignment)

=      간단한 할당 연산자

*=     /=     %=    +=     -=     <<=   >>=   &=    ^=     |=

  할당 연산자는 할당 연산자의 오른쪽 표현식의 값을 평가하여 왼쪽 변수에 대입합니다. i=j+k는 j+k의 평가값을 i라는 변수에 대입합니다. i값이 변경되므로 부효과가 있습니다. 우리는 할당문의 왼쪽에 있는 변수(left value: l-value)를 해석하는 방법과 오른쪽에 있는 변수(right value: r-value)를 해석하는 방법이 다르다는 것에 주목할 필요가 있습니다. i=2, j=3일 때

i=j;

라는 문장을 어떻게 해석할까요? 물론 3을 i에 대입합니다. j를 i에 대입하는 것이 아니라, j의 값인 3을 i에 대입하는 것입니다. 즉 할당문의 오른쪽에 있는 표현식의 값을 왼쪽 변수 – 즉 실제 i가 할당된 메모리의 곳 – 에 할당하는 것입니다. 그래서 왼쪽에는 항상 주소값을 구할 수 있는 변수가 위치해야 합니다. 하지만 오른쪽에는 값이 위치해도 됩니다. 다음 문장은 이러한 규칙을 위반한 에러입니다.

3=j;

이러한 문장은 컴파일러에 의해 다음의 에러메시지를 발생합니다.

“L-value required”

  ++1같은 표현식의 경우도 마찬가지입니다. 컴파일러는 이 문장을 1=1+1로 해석하려고 하기 때문에 L-value가 틀렸다는 에러메시지를 출력합니다.

  할당문의 종류가 많아 보이지만, 할당문을 만드는 규칙을 이해한다면, 이러한 할당문을 사용하는 것은 무척 쉽습니다. i=i+j는 i+=j와 동일합니다. 이러한 것은 다음과 같은 그림으로 일반화 됩니다.

 복잡한 할당문의 원리

  할당문의 왼쪽과 오른쪽의 첫번째 변수가 같다면, 복잡한 할당문으로 고칠 수 있습니다. 먼저 오른쪽 표현식에서 중복되는 같은 변수를 제거 합니다. 그러면 =+형태가 남습니다. 이 둘의 위치를 교환합니다.

  i+=j*k가 i=i+(j*k)임에 주의하세요. i=(i+j)*k를 복합 할당문을 사용하도록 고칠 수는 없습니다. 오른쪽의 첫번째 변수는 i+j이지 i가 아니기 때문입니다. 그러므로

i-=j+k+l;

은 i=i-j+k+l이 아니라, i=i-(j+k+l)입니다.

  위에서 제시한 연산자들 *=    /=     %=    +=     -=     <<=   >>=   &=    ^=     |=  은 이처럼 복잡한 할당문으로 만드는 것이 가능합니다.

  할당문도 표현식임에 주의하세요. i=j;라는 할당문은 j의 값을 i에 대입할 뿐만 아니라, i=j 라는 문장 자체가 i의 값을 가집니다.

 수학에서는 (i=j)는 값이 아닙니다. 단지 할당문 일뿐입니다. 하지만 C에서는 i=j는 표현식입니다. i=3이라는 문장은 i값을 3으로 바꿉니다. 또한 i=3 표현식 자체는 3이라는 값을 가집니다.

  그러므로 다음과 같은 문장을 사용하는 것은 가능합니다.

i=j=k=0;

  이 문장을 다음과 같이 해석하는 것은 엄격한 의미에서 잘못된 것입니다.

“0을 k에 대입하고, 0을 j에 대입하고, 0을 i에 대입합니다.”

  바른 해석은 다음과 같습니다.

“0을 k에 대입합니다. k=0을 j에 대입합니다. j=(k=0)을 i에 대입합니다.”

그러므로 오른쪽의 0이 i에 대입된 것이 아니라, j=k=0의 값 – 물론 이 값은 0 입니다 – 이 i에 대입된 것입니다.

  할당문을 사용할 때도 반드시 지켜야 할 관례가 있습니다. 할당문을 함수의 파라미터로 사용할 때, 한 개 이상의 할당문을 사용하지 마세요. 그리고 할당문을 한 개 사용하더라도, 할당문에서 사용된 변수를 다른 파라미터로 넘기지 마세요.    i=2; j=3인 경우, 아래의 문장을 고려해 봅시다.

printf(“%d\n”,i=j);

  위 문장은 j값 3을 i에 할당하므로, i값이 3이됩니다. 또한 i=j라는 할당문 자체의 값이 3이므로 3을 출력합니다. 하지만, 아래의 출력결과에 주목하세요.

printf(“%d,%d\n”, i, i=j);

  위 문장을 구현한 프로그래머의 의도는 i를 출력하고, i를 j값으로 초기화 시킨다음, i=j를 출력하려 했으므로, 결과가 2,3 이 될 것이라고 예측할 것입이다. 하지만, 결과는 3,3입니다. 이 문제는 뒷장에서 파라미터 전달 방법parameter passing method을 이야기 할 때 자세히 다루도록 하겠습니다. 아무튼 함수의 파라미터에 할당문을 되도록이면 사용하지 마세요.


비트(Bitwise)

&      비트 논리곱(bitwise AND) 연산자

|       비트 논리합(bitwise OR) 연산자

^      비트 배타적 논리합(bitwise exclusive OR) 연산자

~      단항 비트 논리부정(bitwise NOT) 연산자

<<     비트 왼쪽 쉬프트(bitwise shift left) 연산자

>>     비트 오른쪽 쉬프트(bitwise shift right) 연산자

  위 비트 연산자들과 논리 연산자(&&, ||, !)의 연산의 차이에 대해서 주의하세요. 비트 연산자는 전체 숫자값에 대해서 적용 되는 것이 아니고 각각의 비트에 대해서 적용됩니다. 각 비트 연산자의 역할은 다음과 같습니다.

&      bitwise AND: 두 비트가 모두 1이면, 1입니다. 그외의 경우는 0입니다.

|       bitwise inclusive OR: 두 비트가 모두 0이면, 0입니다. 그외의 경우는 1입니다.

^      bitwise exclusive OR: 1의 수가 홀수개이면, 1입니다. 그외의 경우는 0입니다.

~      bitwise complement: 단항 연산자이므로 피연산자가 1개입니다. 0과 1을 토글(toggle)합니다. 즉 1의 보수를 구합니다.

>>     bitwise shift right: 비트열(bit sequence)을 오른쪽으로 이동(shift)합니다. 빈 자리에는 0혹은 1로 채워집니다.

<<     bitwise shift left: 비트열을 왼쪽으로 이동합니다. 빈 자리에는 0으로 채워집니다.

 1^1=0임에 주의하세요. 1^1^1은 (1^1)^1=0^1=1입니다.

 남는 자리가 잘려나간 비트로 채워지는 것을 회전(rotation)이라고 합니다. 회전은 C의 연산자에서 지원하지 않지만, 구현할 수 있습니다. 연습문제를 참조하세요.

  비트 연산자들의 피연산자의 형은 반드시 정수호환 형 – char, int, long, enum등 – 이어야 합니다. 실수나 포인터에 비트연산자를 적용하는 것은 의미가 없습니다.

  다음의 진리값truth value 테이블을 참조하세요.

E1

E2

E1 & E2

E1 | E2

E1 ^ E2

0

0

0

0

0

0

1

0

1

1

1

0

0

1

1

1

1

1

1

0

 비트 연산자의 진리표

  아래 프로그램의 결과를 계산해 보세요.

#include <stdio.h>

void main() {
    int i=2,//0000 0000 0000 0010
        j=3,//0000 0000 0000 0011
        k=5;//0000 0000 0000 0101

    printf("%d,%d,%d,%d,%d\n", i&j, i|j, i^j^k, i<<2, k>>1);
    //       2  3  4  8  2
}

  쉬프트shift 연산자는 이진수의 특성상 2의 지수 곱셈에 대한 특별한 의미를 가집니다. 아래의 표현식을 봅시다.

i<<n

위 표현식은 i*2n과 동일합니다(오버플로우overflow는 고려하지 않았습니다).

i>>n

위 문장은 i/2n과 동일합니다(언더플로우underflow는 고려하지 않았습니다).

  우리는 또한 &와 |의 비트 마스크(bit mask)기능에 주목할 필요가 있습니다. 비트 마스크는 다음과 같은 두 가지의 기능을 구현하는데 사용됩니다.

  (1) bit set: 비트열의 특정한 부분을 1로 만듭니다.

  (2) bit clear: 비트열의 특정한 부분을 0으로 만듭니다.

  비트열의 특정한 부분을 1로 만들기 위해서는 | 연산자를 사용하고, 비트열의 특정한 부분을 0으로 만들기 위해서는 & 연산자를 사용합니다. 아래의 예제는 16진 정수 비트열의 11,10,9,8 위치의 비트를 0으로 지우고, 7,6,5,4 위치의 비트를 1로 설정합니다.

#include <stdio.h>

void main() {
    unsigned int i=0x1234;//0001 0010 0011 0100
    printf("%x\n", i&0xf0ff);
    //       1034

    printf("%x\n", i|0x00f0);
    //       12f4

    printf("%x\n", i&0xf0ff|0x00f0);
    //       10f4
}

  ^ 연산자는 특정한 숫자를 토글toggle시키기 위해 사용할 수 있습니다.

 n과 m을 토글시키기 위해서 i=n; i=i^(n^m); 을 사용할 수 있습니다.

  i=0일 경우, 아래의 문장은 0과 1을 토글합니다.

i=i^1;

  아래의 ~ 연산의 결과를 정확하게 계산해 봅시다.

 3은 2의 보수로, 0000 0011입니다. ~3은 비트를 반전시키므로, 1111 1100입니다. 이것은 부호있는 2의 보수표현에서, 최상위 비트가 1이므로 음수이며, 값은 1111 1100의 2의 보수입니다. 1111 1100의 2의 보수는 0000 0100이므로 4입니다. 그러므로 -4가 값입니다

#include <stdio.h>

void main() {
    char i=3;
    printf("%d\n",~i);
    //       -4
}

  &, >>, << 연산자는 문맥에 따라 의미가 달라지는데 주의하세요. &는 주소 연산자로 혹은 참조에 의한 변수전달 선언으로, <<와 >>는 C++의 표준 클래스 라이브러리(iostream)에서 출력과 입력 연산으로 오버로딩되어 있습니다.

  논리 연산자 – &&, || – 혼돈되지 않도록 하기 위하여, 아래의 문장을 기억해 둡시다.

“비트는 1자리를 의미합니다. 그러므로 비트 연산자는 1개의 심벌 – &, | – 을 사용합니다.”

  비트연산자와 논리 연산자의 결과는 다르다는 것에 주의하세요. 아래의 문장은 i와 j의 값이 모두 참인 경우, “hello”를 출력하는 if문입니다. i=1이고 j=2인 경우 “hello”는 출력됩니다.

 C언어는 0은 거짓이고, 0이 아니면 참으로 간주합니다. 그러므로 -1도 참입니다.

    int i=1,j=2;

    if (i && j) printf(“hello”);

  위의 문장을 프로그래머의 실수로 if문을 다음과 같이 적었다고 합시다.

if (i & j) printf(“hello”);

이제 “hello”는 출력되지 않습니다.

 i & j는 0000 0001 & 0000 0010이므로 결과는 0000 0000입니다.


C++에서만 사용가능(C++ specific)

  아래와 같은 연산자들은 C++에서만 사용할 수 있습니다.

::               범위 접근/해결 연산자(Scope access (or resolution) operator)

.*              클래스 멤버의 포인터의 재참조(Dereference pointers to class members)

->*            클래스 포인터 멤버의 포인터의 재참조(Dereference pointers to pointers to class members)

 클래스 멤버에 대한 포인터 변수가 선언되었을 때, 특정 객체의 해당 멤버를 접근하기 위해서 사용합니다.

 클래스 멤버에 대한 포인터 변수가 선언되었을 때, 특정 객체 포인터의 해당 멤버를 접근하기 위해서 사용합니다.

new            동적으로 메모리를 할당하고 클래스의 생성자를 호출합니다.

delete          파괴자를 호출하고, 동적으로 메모리를 해제합니다.

typeid          형이나 표현식에서 실행시간 정보(run-time identification)를 얻습니다.

dynamic_cast    포인터를 원하는 형type으로 변환cast합니다.

static_cast      포인터를 원하는 형으로 변환합니다.

const_cast      형(type)으로부터 const 혹은 volatile 변경자를 추가하거나, 제거합니다.

reinterpret_cast  안전하지 않거나(unsafe) 구현 의존적인(implementation dependent) 변환을 위한 형 변환에 사용합니다.

  범위 해결 연산자(::)를 범위 이름 없이 사용하면 전역 범위를 의미합니다. 이것은 블록 안에서 선언된 변수/함수와 이름이 같은 전역 변수/함수를 참조할 수 있도록 합니다. 또한 클래스의 멤버 함수를 정의하기 위해, 클래스의 정적 변수를 참조하거나 정적 멤버 함수를 호출하기 위해 사용합니다. 또한 상속 받은 클래스에서 부모 클래스의 변수/함수를 참조하기 위해서도 사용합니다. 클래스의 멤버 함수를 구현 할 때, 지역 변수와 클래스의 멤버 변수를 구분하기 위해서도 사용할 수 있습니다.

  다음의 예제를 통해 그 사용법을 익혀두기 바랍니다. 아직은 설명하지 않은 내용들 때문에, 이해되지 않는 부분이 있을 것입니다. 그러한 부분은 후에 다시 참고하기 바랍니다.

(1) 전역 변수/함수의 참조

 멤버 함수가 아닌 함수를 전역 함수(global function)이라고 합시다. 일반 함수(generic function)란 말이 더 적당할 것 같지만, C++의 일반 함수와 구분하기 위해 전역 함수란 말을 사용합니다.

#include <stdio.h>

int i=1;

void Print() {
    printf("%d\n",i);
}//Print

class CTest {
    int i;

  public:
    CTest(int j) {
        i=j;
    }//CTest
    void Print() {
        ::Print();//전역함수 Print()를 호출한다.
        printf("%d\n",i);
    }//Print
};//CTest

void main() {
    CTest c(2);
    char i=3;
    {
        int i=4;
        printf("%d,%d\n",i,::i);//::i는 전역변수를 참조한다.
    }
    c.Print();
}//main

/*

    4,1

    1

    2   */

  결과를 소스 아래에 설명문으로 적어 두었습니다. 실행해 보고, 범위 해결사의 용도를 익혀두기 바랍니다.

(2) 클래스 멤버 함수의 정의

  위의 예에서 CTest의 멤버 함수 Print()를 클래스 블록 밖에서 정의하려면, 다음과 같이 합니다. 이 때 범위 해결사는 멤버 함수가 속한 클래스를 연결하는 역할을 합니다.

    void CTest::Print() {
        ::Print();//일반함수 Print()를 호출한다.
        printf("%d\n",i);
    }//Print

(3) 클래스 정적 멤버 변수(static member variable)의 초기화, 정적 멤버 함수의 호출

  정적 멤버 변수는 객체마다 할당되지 않고 같은 형의 클래스의 객체가 모두 접근할 수 있는 메모리 영역에 할당됩니다. 정적 멤버 변수를 다음과 같이 초기화하는 것은 에러입니다.

#include <stdio.h>

class CTest {
    static int counter=0;//이 문장은 에러이다

  public:
    CTest() {
        ++counter;
    }//CTest

    static void PrintNOfObject() {
        printf("%d\n",counter);
    }//PrintNOfObject

    ~CTest() {
        --counter;
    }//~CTest
};

  단순한 정적 변수를 클래스를 선언하면서 초기활 수는 없습니다. 이러한 초기화의 실질적인 문제는 클래스가 별도의 헤더 파일에 선언되었을 때, 헤더 파일의 소유자가 정적 변수의 값을 각각 변경한다면 정적 변수의 초기값이 경우에 따라 달라지는 문제가 발생합니다. 하지만, const변경자를 붙이면 이 정적 변수의 값이 변경되지 않는다는 보장이 되므로, 클래스 선언문 안에서 초기화하는 것이 가능합니다.

  정적 변수를 초기화하는 것과 정적 함수를 호출하는 소스는 아래와 같습니다.

#include <stdio.h>

class CTest {
    static int counter;

  public:
    CTest() {
        ++counter;
    }//CTest

    static void PrintNOfObject() {
        printf("%d\n",counter);
    }//PrintNOfObject

    ~CTest() {
        --counter;
    }//~CTest
};

int CTest::counter=0;

void main() {
    CTest a,b,c;
    CTest::PrintNOfObject();
}//main

  위 프로그램의 출력결과는 3입니다.

(4) 상속 받은 클래스에서 부모 클래스의 변수/함수 참조

  아래의 예는 Base를 상속받은 Derived 클래스에서 같은 이름을 가지는 Base의 멤버들을 접근하는 방법을 보여줍니다.

#include <stdio.h>

class Base {
    int i;

  protected:
    int j;

  public:
    Base(int t=0) {
        i=j=t;
    }//Base

    void Print() {
        printf("%d\n",i);
    }//Print
};//class Base

class Derived : public Base {
    int j;
  public:

    Derived(int t) {
        j=t;
    }//Derived

    void Print() {
        Base::Print();//Base의 Print()를 호출한다
        printf("%d,%d\n",Base::j,j);//Base의 j를 참조한다
    }//Print
};//class Derived

void main() {
    Derived d(3);
    d.Print();
}//main

  출력결과는 아래와 같습니다.

0

0,3

(5) 지역 변수와 멤버 변수의 구분

  명시적으로 클래스의 이름을 사용하고 범위 해결사를 사용하는 것은, 같은 이름의 변수이지만, 변수가 속한 곳을 명시적으로 표현하는 곳에 사용할 수 있습니다. 아래의 예는 같은 이름을 가지는 지역 변수 i와 멤버 변수 i가 있을 때, 멤버 변수를 접근하는 방법을 보여줍니다.

#include <stdio.h>

class CTest {
    int i;

  public:
    CTest(int i) {
        CTest::i=i;
    }//CTest

    void Print() {
        printf("%d\n",i);
    }//Print
};//CTest

void main() {
    CTest t(3);
    t.Print();
}//main

  출력결과는 3입니다.

  .*와 ->*는 클래스의 멤버의 포인터로 선언된 변수에 대해, 객체의 해당 멤버를 참조하기 위해 사용합니다.

 클래스의 멤버의 주소는 특별하게 취급되어야 합니다. 그것은 메모리의 절대 주소를 말하는 것이 아니라, 객체가 할당된 곳에서 상대적인 주소를 나타냅니다.

아래의 예를 참조하여 개념을 파악해 두기 바랍니다.

#include <stdio.h>

class CTest {
    int a,b,c;

  public:
    CTest() { a=b=c=0; }
    friend void SetValue(CTest &t,int i);
    void Print() { printf("%d,%d,%d\n",a,b,c); }
};

void SetValue(CTest &t,int i)
{
    int CTest::*ip;
    ip=&CTest::a;
    t.*ip=i;
}//CTest::SetValue

void main()
{
    CTest t;
    SetValue(t,100);
    t.Print();
}//main

  프로그램의 출력결과는 100,0,0입니다. 위의 예에서 SetValue()는 t객체의 멤버 a를 100으로 초기화합니다. 지금 설명한 부분이 이해되지 않으면 이 부분을 건너뛰어도 좋습니다. 후에 C++을 다루는 책을 참고바랍니다. ->* 의 사용 역시 .* 와 동일하다. ->* 를 사용하기 위해서는 SetValue()를 어떻게 수정해야 할까요?


new

  C에서 메모리 할당은 함수가 할 수 있었습니다. 하지만, C++에서는 여러 가지 복잡한 기능을 지원해야 하므로 새로운 동적 메모리 할당(dynamic memory allocation) 연산자 new가 추가되었습니다. new는 C++에서 예약어(reserved word)이므로 변수 이름으로 new를 사용할 수 없습니다.

  C에서는 동적으로 메모리를 할당하기 위해, 주로 malloc()이라는 표준 함수를 이용했습니다. 예를 들어 10개의 short 정수를 저장하기 위해서, 메모리를 할당하는 경우 – 20바이트 할당 – 다음과 같은 문장을 사용할 수 있습니다.

i=(short*)malloc(sizeof(short)*10);

  물론 i는 short *로 선언되어 있어야하며, 포인터 연산의 정확성을 보장하기 위해, (short *)를 사용하여, 형 변환(casting)을 해주는 것이 필요합니다.

 우리는 char *와 int *의 차이점을 이해하고 있어야 합니다. 동적으로 메모리를 할당하는 경우, 포인터의 대상형(dereferenced type)을 알 수 없으므로, 명시적인 형 변환이 반드시 필요합니다. 이러한 형 변환은 포인터 증감 연산시 포인터의 바른 동작을 보장합니다. char *와 int *의 차이점은 다음과 같습니다. char *cp 인 경우, cp=cp+1은 cp값이 1증가합니다. 하지만, int *ip 인 경우, ip=ip+1은 ip를 4증가시킵니다.

  그리고 메모리 사용 후 해제free – 할당된 메모리를 다시 사용 가능하다고 운영체제에게 알려 주는 일 – 하기 위하여, 다음과 같이 사용합니다.

free(i);

  malloc()와 free()는 함수이므로, 이것들을 사용하기 위해서는 alloc.h를 포함inlcude시키는 다음과 같은 문장이 필요합니다.

    #include <alloc.h>

  이러한 사용에서 명시적인(explicit) 형 변환(type conversion)이 왜 필요한지 알아봅시다. 아래 프로그램의 출력 결과는 얼마일까요?

#include <stdio.h>
#include <alloc.h>

void main() {
    short a[]={0x1234,0x5678,0x9012};
    short *ip=a;
    char *cp=(char *)a;
    printf("%x,%x\n",*(ip+1),*(cp+1));
    //       5678,12
}//main

  출력결과는 놀랍게도 5678,12 입니다! 우리는 이 프로그램의 결과를 이해하기 위해서 먼저, 인텔 CPU의 역워드(inverted word) 구조에 대해서 이해해야 합니다.

 이것을 little endian – 이진수 표현에서 가중치가 작은 쪽(little)이 메모리에서 낮은 주소에 위치한다는 의미로 – 이라고도 합니다. little endian이 아닌 경우 big endian이라고 합니다. 이러한 엔디안 구조는 데이터 통신에서도 사용되므로 개념을 숙지하기 바랍니다.

우리는 위 프로그램의 메모리 상태가 아래 그림과 같다고 생각할 수 있으며, 개념적으로 틀리지 않습니다.

 메모리 상태

  short형의 배열이 1000번지에 할당되었으며, 각각의 배열 요소는 short이므로 2바이트의 메모리 공간을 차지합니다. 또한 ip는 포인터 이므로 4바이트 메모리 공간을 차지하고 있으며, cp또한 마찬가지입니다. 초기화 문장에 의해 ip와 cp모두는 1000번지를 가리킵니다. 어떤 CPU의 경우, 이 그림은 맞습니다. 하지만, 인텔 호환 마이크로 CPU를 사용하는 PC인 경우, 실제의 메모리 그림은 아래와 같습니다.

 인텔 CPU인 경우 메모리 구조

  인텔 CPU는 2바이트 이상의 변수를 위해 메모리 할당하는 경우, 낮은 바이트는 낮은 메모리 영역에, 높은 바이트는 높은 메모리 영역에 할당합니다. 이것을 역워드 형식이라고 합니다.

  0x1234가 메모리에 할당되는 경우 1000번지에 할당된다고 합시다. 0x1234는 2바이트 정수이므로, 1000번지와 1001번지에 걸쳐서 저장됩니다.

 별다른 표시가 없는 한 상수는 4바이트 정수(int)입니다.

  12는 1234의 상위 바이트에 해당합니다. 그러므로, 상위 번지인 1001번지에 저장됩니다. 하위 바이트 34는 하위 번지인 1000번지에 저장됩니다. 그러므로 메모리의 구조가 위의 그림처럼 결정되는 것입니다.

  하지만 왜 ip+1과 cp+1이 같지 않을까요? ip와 cp모두 1000임에는 틀림없습니다. 하지만 차이점이 존재합니다. ip는 short 포인터(short *)이므로, ip+1은 다음과 같이 해석한다.

“ip에서 1번째 떨어진 short 정수”

그러므로, ip+1은 1001이 아니라, 1002입니다. 또한 *(ip+1)도 1002번지의 내용이 아니라, 1002번지와 1003번지의 내용입니다. 그러므로 결과는 0x5678입니다.

  cp+1은 다음과 같이 해석합니다.

“cp에서 1번째 떨어진 1바이트 정수”

그러므로, cp+1은 1001이며, *(cp+1)은 0x12가 맞습니다. 이것이 왜 (char *)의 명시적인 형 변환이 필요한지에 대한 해답입니다. 일반적으로 포인터 ip의 증감 n의 의미는 다음과 같습니다.

ip+n ≡ ip+sizeof(*ip)*n

그러므로 ip가 int *라면, ip+2는 ip를 8증가시킵니다.

  이제 동적 메모리 할당에 대해서 알아봅시다. 아래의 프로그램 소스는 어디가 잘못일까요? 문법적인 에러는 없습니다. 논리적인 에러를 찾아 보세요.

#include <stdio.h>
#include <alloc.h>

void main() {
    int *ip;
    *ip=10;
    printf("%d\n",*ip);
}//main

  위 프로그램을 도스DOS 환경에서 실행한다면, 실행될지도 모른다. 하지만 Winows7환경에서 실행한다면, 일반 보호 에러(GPF: general protection fault)를 발생하고 프로그램은 중단될 것입니다.

  위 코드에서는 ip가 할당된 것이지 *ip가 할당된 것이 아닙니다. 즉 할당되지도 않은 *ip영역에 변수를 대입하는 오류를 범하고 있습니다. 이것은 분명히 잘못된 메모리 접근입니다. 아래 그림을 보면라 ip에는 의미없는 값이 들어 있는 것을 알 수 있습니다.

 댕글링 포인터dangling pointer

  ip가 1000번지에 할당되었다고 합시다. 처음 ip에 들어 있던 값을 ?라고 하면, *ip=10 은 메모리의 ?의 곳에 10을 대입합니다.

 이것을 쓰레기 값(garbage value)이라고 합니다.

  이것은 명백한 위법입니다. ?가 가리키는 곳이 이미 실행중인 다른 프로그램이 있는 곳이라면, 어떻게 될까요? 위 프로그램은 간단한 다음 규칙을 위배하고 있습니다.

“변수는 쓰기 전에 메모리 할당해야 합니다.”

  지금은 *ip를 사용할 것이므로, *ip를 위해 메모리를 할당해 주어야 합니다. 이것은 실행 시간(run time)에 할당되므로, 동적 메모리 할당이라고 합니다. 일반적인 변수 선언문 int i; 같은 것은 컴파일 시간(compile time)에 변수의 메모리 위치가 결정되므로 정적 메모리 할당(static memory allocation)이라고 합니다.

 변수나 함수의 주소를 결정하는 것을 바인딩(binding)이라고 합니다. 동적 메모리 할당은 바인딩이 실행 시간에 일어나므로, 늦은 바인딩(late binding)이라 고 합니다. 늦은 바인딩이 아닌 것을 이른 바인딩(early binding)이라고 합니다.

  위의 소스는 동적 메모리 할당을 사용하도록, 아래와 같이 고쳐야 합니다.

#include <stdio.h>
#include <alloc.h>
void main() {
    int *ip;
    ip=(int *)malloc(4);
    *ip=10;
    printf("%d\n",*ip);
    free(ip);
}//main

  malloc(4)는 4바이트의 사용 가능한 메모리를 할당하여 시작 주소를 리턴합니다. 그러므로 ?가 가리키는 곳은 할당된 곳이므로, 이제는 불법이 아닙니다. malloc()이 메모리의 어디를 할당할 것인가는 염려하지 않아도 됩니다. CRT 힙관리자heap manager에 의해 알맞은 메모리 장소가 결정됩니다. 또한 ip를 위해 (int *)의 형 변환이 반드시 필요합니다. 프로그램이 종료하기 전 동적으로 할당된 메모리는 반드시 해제해야 합니다. 그렇지 않으면, 다른 프로세스가 사용하지 못하는 메모리 영역이 점점 쌓여갈 것입니다.

 메모리 누수(memory leak)라고 합니다. 메모리 릭은 할당된 메모리를 해제하지 않아서 발생하며, 사용할 수 있는 메모리의 양이 점점 줄어들게 합니다.

  C++에서는 연산자 new를 사용하여 동적 메모리 할당을 할 수 있습니다. C++에서 동적 메모리 할당 연산자가 필요한 이유는, 객체에 대한 동적 메모리 할당에서 생성자 함수를 자동으로 호출할 필요가 있기 때문입니다.

C++에서는 위의 소스를 다음과 같이 new/delete를 사용하여 구현 가능합니다.

#include <stdio.h>
void main() {
    int *ip;
    ip=new int;
    *ip=10;
    printf("%d\n",*ip);
    delete ip;
}//main

  alloc.h를 포함하는 것은 더 이상 필요하지 않습니다. 또한, new에 더 이상의 형 변환이 필요하지 않습니다. new int 는 sizeof(int)크기의 메모리 4바이트를 할당하여 시작 주소를 리턴합니다. 그러므로 ip=new int 는 타당한 메모리 할당 문장입니다. 메모리 해제는 delete ip 처럼 사용합니다. new의 문법은 다음과 같습니다.

[::]new <type>[(초기값)]

  new앞에 범위 해결사 ::는 옵션(option)입니다. 후에 오버로딩된 new와 구분하기 위해서 ::new 처럼 사용할 필요가 발생합니다. 이것은 오버로딩된 new를 사용한다는 것이 아니라, 원래의 전역 new를 사용한다는 것을 보장해 줍니다. new 뒤에 적는 type은 생략해서는 안 됩니다. new type은 sizeof(type)만큼의 메모리를 할당해서 시작 주소를 리턴합니다. 아래의 문장은 4바이트를 할당합니다.

    long *lp;

    lp=new long;

  delete는 할당된 크기에 상관없이,

delete ip;

라고 사용합니다. 메모리 할당 후 대부분은 초기화를 필요로 합니다. 그래서 new는 메모리 할당과 동시에 초기화를 하는 것을 허락합니다. 물론 초기화는 생략될 수 있습니다. 위의 소스는 new의 초기화 문장을 사용하여 다음과 같이 고칠 수 있습니다.

#include <stdio.h>
void main() {
    int *ip;
    ip=new int(10);
    printf("%d\n",*ip);
    delete ip;
}//main

  이제 10개의 int형 변수를 할당하기 위해서는 어떻게 해야 하는지 알아봅시다. new는 이러한 여러 개의 기본형을 위한 메모리를 할당하기 위해서 다음과 같은 문법을 지원합니다. 이것은 new가 지원하는 두 번째 문법입니다.

[::]new type‘[’표현식‘]’

  표현식을 둘러싼 브래킷(bracket; [ or ] )은 선택 사항을 나타내는 심벌이 아니라, 문법 심벌입니다. 즉 생략해서는 안 됩니다. 아래의 문장은 정수 3개를 저장하기 위해서 12바이트의 메모리를 할당합니다.

new int[3]

그러므로 아래의 문장에서 ip는 12바이트의 시작주소를 가리킵니다.

    int *ip;

    ip=new int[3];//이것은 정수 3개를 할당한다.

  이것을 4바이트를 할당해서 3으로 초기화하는 아래 문장과 혼동하지 마세요.

    int *ip;

    ip=new int(3);//이것은 정수 1개를 할당한다.

  기본 형 여러 개로 메모리를 할당한 경우 delete의 문법 또한 다릅니다. new int[3]처럼 할당된 메모리는 delete[] 로 해제해야 합니다. 그러므로, 위의 6바이트 할당 예제에서 메모리 해제는 다음과 같이 합니다.

    int *ip;
ip=new int[3];

delete[] ip;

아래의 예제를 참고하세요.

#include <stdio.h>
void main() {
    int *ip;
    ip=new int[3];
    *ip=1; *(ip+1)=2; *(ip+2)=3;
    printf("%d,%d,%d\n",*ip, *(ip+1), *(ip+2));
    delete[] ip;
}//main

  기본 형 여러 개로 메모리를 할당하면서 초기화하는 다음과 같은 문장은 허락되지 않습니다.

ip=new int[3](1,2,3);

  후에 배울 연산자 오버로딩을 이용하면, 아래와 같은 문장이 가능하도록 코드를 작성할 수 있습니다. 이때 new는 연산자는 적절히 오버로딩 되어야 합니다.

ip=new(1,2,3) int[3];

  오버로딩된 new가 존재하면, 원래의 new를 호출하기 위해서는 다음과 같이 범위 해결사를 사용합니다.

ip=::new int[3];

우리는 내용 연산자(contents-of operator) []에 대해 알고 있습니다. *(ip+n)은 ip[n]과 동일합니다. 그러므로, 위의 소스는 아래와 같이 고쳐 쓸 수 있습니다.

 이때 ip는 포인터 변수라야 하며, n은 정수형입니다.

#include <stdio.h>
void main() {
    int *ip;
    ip=new int[3];
    ip[0]=1; ip[1]=2; ip[2]=3;
    printf("%d,%d,%d\n",ip[0], ip[1], ip[2]);
    delete[] ip;
}//main

  new를 사용하여 2차원처럼 사용할 수 있는 메모리를 할당할 수 있습니다. 아래의 예제를 참고하세요. 2차원 배열 a[][]에서 a는 포인터의 포인터로 취급되므로, 2차원처럼 사용하는 메모리를 할당하기 위해서는 포인터의 포인터 변수 선언이 필요합니다.

#include <iostream.h>
void display(long double **);
void de_allocate(long double **);
int m = 3;
  // THE NUMBER OF ROWS.
int n = 5;
  // THE NUMBER OF COLUMNS.
int main(void) {
   long double **data;
   data = new long double*[m];        // STEP 1: SET UP THE ROWS.
   for (int j = 0; j < m; j++)
       data[j] = new long double[n];  // STEP 2: SET UP THE COLUMNS
   for (int i = 0; i < m; i++)
      for (int j = 0; j < n; j++)
          data[i][j] = i + j;            // ARBITRARY INITIALIZATION
   display(data);
   de_allocate(data);
   return 0;
}

void display(long double **data) {
   for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++)
             cout << data[i][j] << " ";
       cout << "\n" << endl;
       }
}

void de_allocate(long double **data) {
   for (int i = 0; i < m;  i++)
       delete[] data[i];                 // STEP 1: DELETE THE COLUMNS
   delete[] data;                        // STEP 2: DELETE THE ROWS
}

  C++11의 초기화 리스트initialization list를 사용하면 new[]로 메모리를 할당 할 때, 각각의 초기화를 지정하는 것이 가능합니다. Visual Studio 2013이상에서 이 문법을 지원합니다. 위의 예는 다음과 같이 작성할 수 있습니다.

#include <stdio.h>

void main()
{
    int *ip;
    ip = new int[ 3 ]{1,2,3};
    printf( "%d,%d,%d\n", *ip, *( ip + 1 ), *( ip + 2 ) );
    delete[] ip;
}//main

  초기화 리스트의 문법은 new type{}입니다. {와 }안에 할당하는 단위 개수 만큼의 초기값을 적습니다.

  사실 new를 사용하는 이유는 클래스class 타입의 객체object를 위하여 메모리를 할당하고, 생성자constructor를 호출하기 위함입니다. 우리는 아직 클래스에 대해서 다루지 않았으므로 이 부분은 후에 필요할 때 설명하도록 하겠습니다.


delete

  delete는 아래의 두 가지 문법이 있습니다.

[::]delete <pointer>;

[::]delete[] <pointer>;

  할당이 ip=new int 처럼 되었다면, delete ip 를 사용합니다. 할당이 ip=new int[3] 처럼 사용되었다면, 반드시 delete[] ip 를 사용합니다. delete를 잘못 사용하면 메모리 릭memory leak이 발생하는 원인이 됩니다. delete와 delete[]의 차이점은 C++을 다루는 책을 참고하기 바랍니다.

  ip가 포인터의 포인터로 선언되었을 때, 포인터의 포인터에 해당하는 값, 즉 배열의 두 번째 인덱스에 해당하는 메모리를 해제할 때는 delete[] ip[0]의 형식으로 사용할 수 있습니다. 이러한 사용은 두 번째 문법 형식에 포함되는 것입니다.


typeid

  C++의 typeid를 사용하면 실행시간에 표현식이나 형의 형 정보type information를 구할 수 있습니다. typeid는 type_info라는 클래스의 상수 참조를 리턴합니다. 그러므로 typeid를 사용하기 위해서는 typeinfo.h를 포함해야 합니다.

  아래의 예는 typeid를 이용해서 실행시간에 형의 이름을 얻거나, 베이스 클래스의 이름을 얻는 방법을 보여줍니다.

#include <iostream>
#include <string>
#include <typeinfo>

struct Base {}; // non-polymorphic
struct Derived : Base {};
struct Base2 { virtual void foo() {} }; // polymorphic
struct Derived2 : Base2 {};

int main()
{
    int myint = 50;
    std::string mystr = "string"
    double *mydoubleptr = nullptr
    std::cout << "myint has type: " << typeid(myint).name() << '\n'
        << "mystr has type: " << typeid(mystr).name() << '\n'
        << "mydoubleptr has type: " << typeid(mydoubleptr).name() << '\n'
    // Non-polymorphic lvalue is a static type
    Derived d1;
    Base& b1 = d1;
    std::cout << "reference to non-polymorphic base: " << typeid(b1).name() << '\n'
    Derived2 d2;
    Base2& b2 = d2;
    std::cout << "reference to polymorphic base: " << typeid(b2).name() << '\n'
}

  출력결과는 다음과 같습니다.

myint has type: int

mystr has type: class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >

mydoubleptr has type: double *

reference to non-polymorphic base: struct Base

reference to polymorphic base: struct Derived2

  하위 클래스derived class의 정보를 얻기 위해서 베이스 클래스의 객체를 이용하는 경우, 베이스 클래스가 다형성polymorphic 클래스가 아닌 경우, type_info는 원래의 형 정보를 얻어 낼 수 없습니다. type_info가 원래의 형 정보를 얻어 낼 수 있으려면, 베이스 클래스가 다형성 클래스로 선언되어야 합니다.

typeid가 동작하기 위해서는 프로젝트 설정에서 실행시간 형식 정보를 사용하도록 설정해 주어야 합니다.

그림: Visual Studio 2013의 런타임 형식 정보 활성화 설정

  C++11에서는 해시 컨테이너의 인덱스로 사용할 수 있도록, type_info를 std::type_index의 파라미터로 전달 할 수 있습니다. type_index를 사용하는 예는 C++관련 책을 참고하기 바랍니다.


dynamic_cast

  [클래스와, 클래스의 상속성(inheritance)에 대해 확실히 개념을 파악하지 못했다면 이 부분을 건너 뛰세요. 상속성에 대한 개념 없이 dynamic_cast를 이해하는 것은 불가능합니다.]

  우리는 dynamic_cast를 이해하기 전에 서브타입의 원리(subtype principle)에 대해 이해할 필요가 있습니다. 아래와 같은 클래스 계층 구조를 생각해 봅시다.

 subtype principle

  클래스 B의 포인터 변수 bp를 고려해서, 다음과 같은 문장이 가능한가에 대해 이야기해 봅시다. 먼저 위의 그림에 해당하는 소스는 아래와 같습니다.

class A { void f(){} };
class B : public A { void f(){} };
class C : public B { void f(){} };
class D : public C { void f(){} };

void main() {
    B *bp;
    A a;
    B b;
    C c;
    D d;

    bp=&b;//명시적인 형 변환이 없어도 가능하다.
}//main

  bp=&b 는 타당한 문장입니다. bp는 클래스 B형의 객체 b의 시작 주소를 가리킵니다. 그러므로, bp->f()는 타당한 함수 호출이며, b의 멤버 함수 f()가 호출됩니다. 그렇다면, 다음 문장은 가능한가요?

bp=&a;

불가능합니다. 아래처럼 형 변환을 해주면 어떤가요?

bp=(B *)&a;

역시 불가능합니다. 자식이 부모의 포인터를 가지는 것은 불가능합니다(과연 그럴까요?). 왜 그런가를 이해하기 전에 다음 문장을 고려해 봅시다.

bp=(B *)&c;

bp=(B *)&d;

  위 두 문장은 모두 가능하며, bp->f()는 각각 클래스 B의 멤버 함수 f()를 호출합니다. c의 멤버 함수 f()와 d의 멤버 함수 f()가 호출되게 하려면, f()를 가상 함수(virtual function)로 만들어야 합니다. 왜 하위 클래스 – subtype – 의 포인터를 가지는 것만이 허용되는지, 또 어떻게 하위 클래스의 멤버 함수를 호출하는지는 C++의 중요한 주제입니다.

 상위 클래스를 supertype이라고 한다면, 상속 받은 하위 클래스는 subtype이 됩니다. 물론 super class/sub class, base class/derived class라는 용어도 사용합니다.

  상위 클래스의 포인터 변수를 이용해 하위 클래스의 멤버를 접근하는 것은 자연스럽습니다. 하지만 하위 클래스의 포인터를 이용해 상위 클래스의 멤버 함수를 접근하는 것은 부자연스럽습니다. 어떻게 자식(child)이 부모(parent)의 소지품을 건드릴 수 있는가요? bp가 B *이므로 등호의 오른쪽에 &b가 올 수 있는 것은 당연합니다. 여기에 서브타입의 원리의 핵심이 있습니다. 원리는 다음과 같습니다.

“supertype이 올 수 있는 자리에는 항상 subtype이 올 수 있다.”

  등호의 오른쪽에 B *가 올 수 있으므로, B의 서브 타입에 해당하는 C *와 D *는 올 수 있습니다. 하지만, B *의 수퍼 타입에 해당하는 A *는 올 수 없습니다. 이 원리는 가상함수와 함께 C++프로그래머라면 반드시 기억하고 있어야 합니다.

  하지만, C++의 표준은 (그것이 안전하기만 하다면) 반대의 형 변환을 지원합니다. 이러한 형 변환은 실행시에 일어나므로, 동적 형 변환(dynamic casting)이라고 합니다. 동적 형 변환을 위한 키워드는 dynamic_cast 입니다. 단 한가지 제약이 있습니다. 형 변환이 되는 클래스는 반드시 다형성 클래스(polymorphic class)라야 합니다.

 1개 이상의 가상함수를 가지는 클래스를 다형성 클래스라 합니다. 1개 이상의 순수 가상함수를 가지는 클래스는 객체를 만들 수 없으므로, 추상 클래스(abstract class)라 합니다. 추상 클래스는 다형성 클래스에 속합니다. 다형성 클래스의 가상함수에 관한 호출은 실행 시에 결정되므로 다형(polymorphic)이란 말은 적절합니다.

  즉 최소한 1개 이상의 가상 함수를 멤버로 가져야 한다. 생각해 보라. 가상함수를 가지지 않는다면, 상위 클래스로의 형 변환이 왜 필요한가?

  아래의 예제는 다음과 같은 클래스 계층 구조에서의 형 변환을 보여줍니다.

 클래스 계층 구조

  dynamic_cast를 사용하면, Base1 *를 Derived *로 형 변환하는 것이 가능합니다. 또한 Base1 *를 Base2 *로 형 변환하는 것도 가능합니다. 단 Base1에는 반드시 1개 이상의 가상함수가 존재해야 합니다. 문법은 다음과 같습니다.

dynamic_cast<converted_class_pointer>(original_class_pointer)

위 문장은 original_class_pointer를 converted_class_pointer로 변환합니다. 물론 converted_class pointer는 original_class_pointer의 서브 클래스가 될 수 있습니다. 아래의 예제를 참고하세요.

//This program must be compiled with the -RT (Generate RTTI) option.
//Run Time Type Information isn't supported
//  by some compiler like Borland C++ 3.1

#include <iostream.h>
#include <typeinfo.h>

class Base1 {
   // In order for the RTTI mechanism to function correctly,
   // a base class must be polymorphic.
   virtual void f(void) { /* A virtual function makes the class
                             polymorphic */ }
};//class Base1

class Base2 { };

class Derived : public Base1, public Base2 { };

int main(void) {
      Derived d, *pd;
      Base1 *b1 = &d;
      // Perform a downcast from a Base1 to a Derived.
      if ((pd = dynamic_cast<Derived *>(b1)) != 0) {
           cout << "The resulting pointer is of type "
                << typeid(pd).name() << endl;
      }//if

      // Attempt cast across the hierarchy.  That is, cast from
      // the first base to the most derived class and then back
      // to another accessible base.
      Base2 *b2;
      if ((b2 = dynamic_cast<Base2 *>(b1)) != 0) {
          cout << "The resulting pointer is of type "
               << typeid(b2).name() << endl;
      }//if

      return 0;
}//main

/* 결과:

   The resulting pointer is of type Derived *

   The resulting pointer is of type Base2 *         */

  이 외에도 실행시 형 정보(run time type information: RTTI)에 관한 키워드로 static_cast, const_cast, reinterpret_cast와 typesafe_downcast가 있습니다. 이러한 키워드는 C++ 표준이 정해지기전 발표된 몇몇 컴파일러에서는 지원되지 않았지만, Visual Studio 2013에서는 지원합니다.  자신이 사용하는 컴파일러의 도움말 시스템을 이용하여, 위에서 열거한 키워드에 대해서 각자 찾아보기 바랍니다. 우리가 마지막으로 명심해야 할 문장은 다음과 같습니다.

“subtype의 원리를 되도록 위반하지 마세요. 즉 dynamic_cast는 되도록 사용하지 마세요.”


콤마(Comma)

  가끔식 사용되기는 하지만, 콤마 연산자는 유용합니다. 콤마 연산자의 문법은 다음과 같습니다.

표현식[,표현식][…]

  표현식은 콤마에 의해 계속 연결될 수 있으며, 콤마에 의해 연결된 표현식은 하나의 문장입니다. 그리고 문장의 값은 마지막 표현식의 값이 됩니다. 그러므로 다음 문장의 값은 3입니다.

1,2,3

  위의 문장이 타당한 표현식임에 유의하세요. 아래의 소스는 3을 i에 할당합니다.

i=(1,2,3);

  콤마 연산자는 우선 순위(priority)가 가장 낮습니다. 그러므로 i=1,2,3 이라는 문장은 (i=1),2,3으로 해석되므로, i의 값은 1로 초기화되며, 문장의 값은 3입니다. 만약 j=((i=1),2,3)으로 쓰여졌다면, i에 1을 할당하며, 문장의 값 3을 j에 할당합니다. 그러므로 다음 문장

i=1,j=2,k=3;

은 i에 1을 할당하고, j에 2를 할당하고, k에 3을 할당하며, 전체 문장의 값은 마지막에 실행된 문장의 값인 3이 됩니다. 콤마 연산자로 연결된 표현식은 왼쪽에서 오른쪽으로 평가됩니다. 또한 문장의 값은 마지막에 실행된 표현식의 값입니다.

  위 문장은

i=1; j=2; k=3;

과는 다릅니다. 이것은 3개의 문장이며, 문장 각각이 1,2,3이라는 값을 가지지만, 전체 문장이 값을 가지는 것은 아닙니다. 문법에서도 언급했듯이, 콤마 연산자로 연결할 수 있는 것은 표현식만입니다. 아래의 소스의 결과에 주목하세요.

#include <stdio.h>

void main() {
    int i;
    printf("%d\n",(1,2,3));//값은 3이다.
    //       3
    i=(3,2,1);//마지막 값인 1이 i에 할당된다.
    printf("%d\n",i);
    //       1
}//main

  표현식만으로 연결되어야 하므로, 다음 문장은 에러입니다.

i=1, int j, k=2;

두 번째 문장은 표현식이 아니므로, 즉 값을 가지지 않으므로, 콤마로 연결할 수 없습니다.

  콤마 연산자는 반드시 한 문장으로 표현해야 하지만, 2개 이상의 문장이 필요한 경우 많이 사용됩니다. 사실 콤마는 이러한 경우에만 대부분 사용됩니다. i를 1부터 10까지 변화시키면서, j를 5부터, 50까지 5의 배수로 크기를 변화시켜야 한다고 생각해 봅시다. 우리는 for문을 사용하여 소스를 다음과 같이 작성할 수 있습니다.

j=5;

for (i=1;i<=10;++i) {

    …

    j+=5;

}//for

  이러한 문장이 가능은 하지만, 인덱스 변수가 2개라는 사실을 표현하는데는 부족합니다. 콤마 연산자를 사용하여 다음과 같이 소스를 수정할 수 있습니다.

for (i=1,j=5;i<=10;++i,j+=5)

    …

  마지막으로 아래 프로그램의 결과를 예측해 보세요.

#include <stdio.h>

void f(int i,int j) {
    printf("%d,%d\n",i,j);
}//f

void main() {
    int i,j,k;
    f((i=1,j=2),k=3);
}//main

  결과는 2,3이 출력됩니다. 당신의 결과와 일치하는가요? 이것은 콤마 연산자를 사용하여, 초기화와 함수의 파라미터 전달을 동시에 한 경우의 적절한 예입니다. 마지막 조언은 다음과 같습니다.

“for문에서의 어쩔 수 없는 경우를 제외하고는, 되도록이면 콤마 연산자를 사용하지 마세요. 즉 위와 같은 함수의 호출에 콤마 연산자를 사용하는 것은 좋지 않습니다.”


조건(Conditional)

  조건 연산자는 C에서 유일한 삼항 연산자(ternary operator)입니다. 삼항 연산자라는 말에 유의하세요. 이항 연산자(binary operator)에서 피연산자(operand) 2개가 모두 명시되어야 하듯이, 비록 필요 없는 경우가 생기더라도 조건 연산자의 피연산자 3개는 모두 명시되어야 합니다. 조건 연산자의 문법은 다음과 같습니다.

표현식0 ? 표현식1 : 표현식2

  위 문장은 표현식0의 값이 0이 아니면, 즉 참(true)이면 표현식1의 값으로, 아니면 표현식2의 값으로 결정됩니다. 그러므로 아래 문장의 값은 2입니다.

 문장과 표현식을 혼용하여 사용하고 있지만, 명확히 구분할 필요가 있습니다. 사실 문장의 값이 수(number)이므로, 표현식이라는 말이 정확합니다. 하지만, 문장의 특성을 강조하기 위해서, 표현식인 경우도 문맥에 따라 문장이라는 용어를 사용하고 있습니다.

0 ? 1 : 2

  if문으로 표현된 아래 문장은

    if (i>j)

        m=i;

    else

        m=j;

다음의 조건 연산자로 표현된 문장과 동일합니다.

m=(i>j)?i:j;

즉, i와 j중 큰 값을 m에 할당합니다. if문의 표현이

    if (i>j)

        m=i;

라고 해서, 대등한 조건 연산자 표현으로

m=(i>j)?i: ;

 굳이 사용해야 한다면 m=(i>j>?i:m; 처럼 사용하면 된다.

라고 해서는 안됩니다. 조건 연산자가 삼항 연산자이므로, 피연산자 어느 것도 생략해서는 안됩니다. 아래의 연산자는 무엇을 의미하는가요?

m=(i>j)?i:(j>k)?j:k

대등한 if문은 다음과 같습니다.

if (i>j)
    m=i;
else {
    if (j>k)
        m=j;
    else
        m=k;
}

  그러므로 아래 프로그램의 결과는 5입니다.

#include <stdio.h>
#include <iostream.h>

void main() {
    int m,i=2,j=5,k=3;
    m=(i>j)?i:(j>k)?j:k;
    cout << m <<endl;
}//main

  우리는 조건 연산자를 이용하여 max혹은 min이라는 매크로 함수(macro function)을 다음과 같이 만들 수 있을 것입니다.

    #define MAX(a,b) ((a)>(b)?(a):(b))

 매크로 함수의 파라미터를 괄호로 감싸야 함에 유의하세요. 그렇게 하지 않는다면, 연산자 우선 순위에 의해 심각한 문제가 발생할 수 있습니다. 이것은 ‘20. 전처리 명령어’에서 상세히 다루겠습니다.

    #define MIN(a,b) ((a)<(b)?(a):(b))

  물론 C++에서 이러한 매크로 함수는 권장되는 사항이 아닙니다. C++에서는 위의 함수 대신 다음과 같은 인라인 함수(inline function)를 작성할 것입니다.

inline int Max(int a,int b) {
    return a>b?a:b;
}//Max

  위의 Max함수는 정수형의 Max에 대해서만 동작합니다. 모든 형에 대해 동작하는 Max함수는 템플릿(template)을 이용하여 구현 가능합니다. 아래의 소스는 아직 몰라도 됩니다.


template<class T> inline T Max(T a,T b) {
    return a>b?a:b;
}//Max


논리(Logical)

&&    논리곱(logical AND) 연산자

||       논리합(logical OR) 연산자

!       단항 논리부정(logical NOT) 연산자

  먼저, 주의해야 할 사항이 있습니다. C에서의 논리 연산자와 수학에서의 논리 연산자는 의미가 다릅니다. 수학에서는 논리 연산자의 표현식이 논리 표현식이지만, C에서는 수치 표현식입니다.

 표현식의 결과가 참(true: 1) 혹은 거짓(false: 0)인 표현식

  즉 논리 연산의 결과는 참/거짓이 아니라 수(number)입니다. 이것을 제외한 다른 차이점은 없습니다. C는 0을 거짓으로 간주하며, 0이 아닌 숫자를 참으로 간주합니다.

 숫자가 0이 아니면, 비트열(bit sequence) 중 최소한 1비트는 1입니다. 그러므로 C언어에서 참/거짓의 검사는 모든 비트가 0인지, 그렇지 않은지를 검사합니다.

  0을 거짓으로 보고, 1을 참으로 본다면, 진리표는 아래와 같습니다.

&&

||

0

0

1

1

0

1

0

1

0

0

0

1

0

1

1

1

 논리 연산자의 진리표

!

0

1

1

0

 논리 부정의 진리표

  0은 거짓이지만, 0이 아니면 항상 참이라는 것에 주목하세요. 1은 참입니다. 100도 참이며, -1도 참입니다. 그러므로 !0은 참입니다. !1, !100, !-1은 참을 부정하는 문장으로 간주하므로, 거짓(0)입니다. 그러므로 printf(“%d\n”,!-1)은 0을 출력합니다.

  아래 프로그램의 결과는 얼마일까요?

int i=2,j=3,k=4;

if (i<j && j<=k)
    printf("logical operator\n");

  문자열 “logical operator”는 출력됩니다. 왜 문자열이 출력되는가요? 아래와 같은 대답은 맞는가요?

“i<j는 참입니다. j<=k는 참입니다. ‘참 AND 참’은 참이므로 if의 조건이 만족됩니다. 그러므로, printf()가 실행됩니다.”

  위의 대답이 틀린 것은 아니지만, 좀 더 정확한 대답은 다음과 같습니다.

“i<j는 관계 비교가 참이므로 1입니다. j<=k역시 1입니다. ‘1 AND 1’은 1이므로 printf()가 실행됩니다.”

즉 if문이 실행된 이유는 괄호 안의 조건이 참이기 때문이 아니라, 괄호 안의 표현식이 1이기 때문입니다. C에서 참/거짓이란 값은 존재하지 않습니다. 0이면 조건 비교를 거짓으로 간주하고, 0이 아니면 참으로 간주합니다. 이 개념을 확실히 알고 있다면, 참/거짓이란 말을 사용해도 좋습니다.

  그러므로 아래 프로그램의 출력결과는 1입니다.

#include <stdio.h>

void main() {
    int i=2,j=3,k=4;
    printf("%d\n",i<j && j<=k);
}//main

  핵심(key)은 다음과 같습니다.

“논리 연산자와 관계 연산자의 결과는 0아니면 1입니다. 즉 숫자 표현식입니다.”

  논리 연산자를 다룰 때, 주의 해야할 사항은 ‘짧은 평가(short circuit)’에 관한 것입니다. 위의 예에서 if의 조건이 논리합(OR)이라면 결과는 얼마인가요?

#include <stdio.h>

void main() {
    int i=2,j=3,k=4;
    if (i<j || j<=k)//짧은 평가에 의해, j<=k는 평가하지 않는다.
        printf("short circuit\n");
}//main

  물론 “short circuit”이란 문자열은 출력됩니다. 둘 다 참(1)이므로, 논리합의 결과 역시 참입니다. 핵심은 여기에 있습니다. j<=k를 평가해야 하는가요? 사실 이 평가는 불필요합니다. i<j가 참이라면, 논리합의 특성에 의해 두 번째 표현의 참/거짓에 관계없이 항상 참입니다. 실제로 컴파일러는 j<=k를 평가하지 않습니다. 이것을 짧은 평가라 합니다. 물론 논리곱(AND)에 대해서도 짧은 평가는 성립합니다. 짧은 평가에 관한 조언은 다음과 같습니다.

“연속된 조건을 명시할 때, 빠르게 참/거짓이 결정될 것 같은 문장들을 앞쪽(왼쪽)에 사용하세요. 이것은 짧은 평가에 의해 조금의 속도 향상을 꾀할 수 있습니다.”

  논리 부정(NOT)은 전체 논리식을 부정합니다. 사실상 수학에서와 같이, 논리 부정은 다음 연산자를 서로 교환(swap)합니다.

&&  ↔  ||

>  ↔  <=

>=  ↔  <

==  ↔  !=

그러므로 다음 문장은

!(i>j && j!=k)

아래 문장과 같습니다.

(i<=j || j==k)

  그렇다면 언제 논리 부정 연산자를 사용할 것인가요? 문맥에 맞게 논리 부정을 사용하세요. 우리의 의도가 ‘i가 j보다 크지 않은지 비교한다’면 i<=j보다 !(i>j)가 적당할 것입니다. 물론 차이는 없습니다. 하지만, 의도대로의 코딩은 프로그램을 읽기 쉽게(improve readability) 합니다.

  아래에 논리 연산자의 전체적인 예를 들었습니다. 프로그램의 결과는 적지 않습니다. 꼭 실행해서 결과를 확인해 보세요.

#include <stdio.h>

void main() {
    int i=2,j=3,k=4;

    printf("%d\n",i && j);
    printf("%d\n",!i && !j);
    printf("%d\n",i>j || j==k);
    printf("%d\n",!(i>j));
    printf("%d\n",!(!(i<j)));
    printf("%d\n",!(j!=k));
}//main

 1 0 0 1 1 0이 출력됩니다.

  종종 무한 루프(infinite loop)를 만들어야 하는 경우가 생깁니다. 논리 연산의 특징을 이용해서 다음과 같은 무한 루프를 만드는 것이 가능합니다.

while(1) { … }

do { … } while(1);

for(;1;) { … }

 for(;;) { … }역시 무한 루프입니다. for문에서 생략된 비교 문장은 참을 의미합니다.

  while(-1) { … } 역시 무한 루프입니다. 하지만, 관례(convention)상 무한 루프를 만드는 표현식은 1을 사용합니다.


후위표기(Postfix)

  C에서 연산자처럼 느껴지지 않는 몇몇 연산자들은 후위표기법(postfix notation)을 사용합니다. 아래의 연산자들은 후위 표기법으로 표현합니다.

 일반적으로 연산자(operator)는 피 연산자(operand)들의 사이에 위치합니다. i + j는 일반적입니다. 하지만, 피 연산자가 연산자보다 먼저, 혹은 나중에 위치하도록 표기할 수도 있습니다. i j +는 전위표기법(prefix notation)이라 합니다. + i j는 후위 표기법이라 합니다. 어떤 경우 후위 표기법 등이 연산을 처리하기에 훨씬 효과적인 경우가 많습니다. 실제적으로 계산기의 표현식 평가는 중위 표기법(infix notation)을 후위 표기법으로 전환(transform)한 다음 평가합니다. 표기법의 전환은 스택을 사용하여 구현 할 수 있습니다.

  연산자임에는 분명하지만, 표현식이 아닌 것도 있습니다. 예를 들면 ()가 함수 호출 연산자(function call operator)로 사용된 경우, 결과가 수(number)인 것은 아닙니다.

()      표현식을 그룹화 하기 위해, 조건 표현식을 독립시키기 위해, 함수 호출을 위해, 함수의 파라미터를 지정하기 위해 사용합니다. 함수 호출 연산자(function call operator)라 합니다.

[]      배열의 첨자(subscript)를 가리키기 위해, 포인터가 가리키는 곳의 내용을 참조하기 위해 사용합니다. 첨자 연산자 혹은 내용 연산자(contents-of operator)라 합니다.

{}      복합문(compound statement)의 시작과 끝을 나타내기 위해 사용합니다. 복합문은 하나의 문장 취급됩니다. 블록(block)이라고 합니다.

.       구조체(structure), 공용체(union)와 클래스(class)의 멤버를 접근(access)하기 위해 사용합니다. 멤버 연산자(member operator)라 합니다.

->     포인터로 선언된 구조체 등의 멤버를 접근하기 위해 사용합니다. 포인터 멤버 연산자(pointer member operator)라 합니다.

  함수 호출 연산자 ()는 함수 호출이라는 특별한 목적이외에도 문법 구조를 이루는 심벌로 자주 등장합니다. 2+3*4는 14입니다. 하지만 (2+3)*4는 20입니다. 이것은 2+3이라는 것을 그룹화한 것을 의미합니다. ()는 연산자의 우선 순위가 가장 높습니다.

  함수 호출 연산자에 대해 살펴봅시다. 아래의 소스를 보세요. 무엇이 잘못 되었는가요?

#include <stdio.h>
#include <conio.h>

void main() {
    int i;
    printf("Press y key");
    i=getch;//이 부분이 잘못인가?
    if (i=='y')
        printf("You pressed small y key\n");
}//main

  아마도 사용자는 getch의 뒤에 괄호 ()를 적는 것을 빠뜨린 것 같습니다.

 getch()는 키보드에서 한 개의 키 입력을 받아, 그 키의 아스키(ASCII) 코드 값을 2바이트 정수로 리턴합니다. 이것은 표준함수가 아니므로 conio.h에 선언되어 있습니다.

  하지만, 우리가 주의해야 할 사항이 바로 여기에 있습니다. 컴파일 시간 에러가 난 원인은 다음과 같습니다.

“getch 뒤에 ()가 없어서 에러가 난 것이 아닙니다. getch 는 함수의 시작 주소를 나타내는 함수 포인터(function pointer)이고 i는 정수(integer)이기 때문에, 형 불일치(type mismatch) 에러가 난 것입니다.”

  즉 위의 에러 원인은 float형의 변수를 int에 대입하려고 했을 때 발생하는 에러와 같습니다. 그렇다면 getch는 무엇인가요? 바로 getch의 시작 주소입니다. 그러므로 함수 호출이 일어나기 위해서는 함수 호출 연산자를 함수의 시작 주소 뒤에 명시해 주어야 합니다. 수정된 소스는 아래와 같습니다.

#include <stdio.h>
#include <conio.h>

void main() {
    int i;
    printf("Press y key");
    i=getch();//함수 호출이 일어난다.
    if (i=='y')
        printf("You pressed small y key\n");
}//main

  후에 포인터를 다룰 때, 함수 포인터에 대해서 자세히 다룰 것입니다. 지금 이해가 되지 않는다고 걱정할 필요는 없습니다.

  []는 배열의 인덱스 연산자로 알고 있지만, 실제로는 내용 연산자(contents-of operator)입니다. 배열의 인덱스 연산자는 배열의 이름이 시작 주소인 덕분에 얻어지는 내용 연산자의 부산물(added goods)입니다. C++에서 []는 배열에 대해서 적용하는 경우보다 포인터에 대해 적용하는 것이 일반적입니. 이 부분 역시 ‘6. 포인터, []연산자’에서 자세히 다루었으므로 넘어 갑니다. 배열의 인덱스 연산자로 쓰이는 부분만 간단히 알아봅시다.

  int a[5]={1,3,5,7,9} 는 크기가 5인 정수형 배열을 선언하여 1부터 시작하는 홀수로 배열의 각 셀(cell)을 초기화합니다. 실제로 할당된 메모리는 10바이트이며, 각각의 정수는 인덱스(index)를 이용하여 참조합니다. 타당한(valid) 인덱스는 0부터 4입니다. 배열의 인덱스가 0부터 시작한다는데 주의 하세요. 파스칼(Pascal)같은 언어 등에서는 1부터 시작하기도 합니다.

  이런 상황에서 배열 a의 2번째 요소 3을 출력하기 위해서는

printf(“%d”,a[1]);

을 사용할 수 있습니다. 배열의 요소를 참조하기 위해 사용하는 []을 배열의 인덱스 연산자라 합니다.

  여러 개의 문장이 모여 하나의 문장을 이룰 때 이를 복합문(compound statement)이라 합니다. {} 는 복합문, 즉 블록 구조(block structure)를 만들기 위해 사용합니다. 복합문이라는 단어가 의미하듯이 복합문이 하나의 문장이라는 것에 주의하세요.

  우리가 알고 있는 if문의 문법은 다음과 같습니다.

if (표현식1)

    문장1;

[else if (표현식2)

    문장2;][…]

[else

    문장3;]

  이것은 if 뒤의 괄호 안의 표현식이 참이면(0이 아니면), 연속하는 문장 1개를 실행함을 의미합니다. 예를 들면, 아래의 소스는

int i=2,j=3;
if (i<j)
    printf("i is greater than j.\n");

  i<j 가 참이므로, printf()를 실행합니다. 만약 괄호 안의 조건이 참이 될 때, 실행할 문장이 2문장 이상이라면, 어떻게 할 것인가요?

if (i<j)
    printf("i is greater than j.\n");
    printf("it\'s joke.\n");

위와 같은 단순한 들여 쓰기에 의해, 두개의 printf()문장이 실행되는 것은 아닙니다. if의 문법에서 보듯이 괄호 안의 조건이 만족된다면, 항상 바로 밑에 있는 문장 1개를 실행합니다. 그러므로 위의 소스는 if의 조건에 상관없이

printf(“it\’s joke.\n”);

는 항상 실행한니다. 어떻게 이 문제를 해결할 것인가요? 바로 복합문 연산자, 즉 2개의 문장을 하나의 문장으로 만드는 것입니다. {}를 사용하여 해결한 소스는 아래와 같습니다.

    if (i<j) {
        printf("i is greater than j.\n");
        printf("it\'s joke.\n");
    }

  어떤 곳에서는 if의 문법을 설명하기 위해

if (표현식)

    문장;

or

if (표현식) {

    문장;

    …

}

이라는 두 개의 표현을 병행하고 있습니다. 틀린 것은 아니지만, 복합문 연산자, 즉 블록 구조를 만드는 { 와 } 의 역할을 제대로 이해하고 있지 못하기 때문입니다. 즉, 제어 구조(control structure)에서 사용된 – switch 문을 제외하고 – {}는 제어 구조의 문법을 이루는 심벌이 아니라, 블록 구조를 나타내는 심벌입니다.

 제어 구조에는 if, switch, for, while과 do문의 5개가 있으며, 놀랍게도 이것이 전부입니다!

  블록 구조(block structure)는 다음과 같은 특징이 있습니다.

  (1) 하나의 문장 취급됩니다.

  (2)-a 블록의 첫 부분에, 변수 선언을 가질 수 있습니다.(예전 C언어)

  (2)-b 블록 안에서는 어디나 변수 선언을 가질 수 있습니다.(C++언어)

  [(3) 겹쳐질(nesting) 수 있습니다.]

(3)번은 블록이 하나의 문장 취급되므로, 당연한 사실입니다. 이것은 제어 구조가 아니더라도, 블록 안에 얼마든지 블록을 만들 수 있음을 의미합니다.

#include <stdio.h>

void main() {
    printf("a");
    {
        printf("b");
        {
            printf("c");
        }
    }
}

위 소스는 에러가 아니며, 완전합니다. 함수의 정의가

<함수의 헤더> <블록>

으로 이루어져 있음에 주목하세요. void main()은 함수의 헤더에 해당하고, { }는 바로 1개의 블록입니다. 흔히 함수의 블록은 몸체(body)라고 합니다.

  (2)번 성질은 왜 함수의 몸체에 변수 선언이 타당한가에 대한 근거입니다.

{
    int i,j;//C에서 변수 선언은 반드시 블록의 시작에 위치합니다.
    printf("C specific");
}

위의 소스는 C/C++에서 모두 가능합니다. 하지만, 아래의 소스는 C++에서만 가능합니다.

{
    int i,j;
    printf("C specific");
    int k;//C++에서는 블록의 임의의 위치에 변수선언을 가질 수 있습니다.
    printf("C++ specific");
}

  우리가 블록 개념을 이해하면서 이해해야할 또 한가지는 블록 안에서 선언된 변수에 관한 것입니다. 블록 안에서 선언된 변수는 블록 안에서만 사용할 수 있습니다. 이것을 변수가 블록 범위(block scope)를 가진다고 합니다. 블록 범위를 가지는 변수를 지역 변수(local variable)라 합니다.

 변수의 입장에서 블록 범위를 가집니다. 변수를 사용하는 입장에서, 블록 안에서만 변수가 보입니다. 그래서 로컬 변수는 블록에서만 보인다(block visibility)고 합니다. 즉 scope와 visibility는, 캡슐화(encapsulation)와 추상화(abstraction)처럼 입장의 차이입니다.

  . 와 -> 는 C에서 구조체/공용체의 멤버 연산자로 사용되었습니다. 물론 C++에서 클래스의 멤버를 접근하기 위해서 사용됩니다. 클래스는 구조체의 확장된 개념이므로 이것은 자연스럽습니다. 후에 클래스와 구조체에 대해서는 자세히 다루므로, 여기서는 구조체에 대해서만 간단하게 설명합니다.

  구조체는 여러 개의 서로 다른 데이터 형(data type)을 필드(field)로 가지는 데이터 형입니다. 다른 언어에서는 이것을 레코드(record)라고 하기도 합니다. 클래스와의 호환성을 위해 필드란 말 대신 멤버란 말을 사용하기로 합시다. 구조체에 관한 설명과 구조체를 정의하는 법은 ‘24. C++의 구조체’ 부분을 참고하기 바랍니다. 아래의 예제에서 STest라는 구조체는 두개의 멤버를 가지며, 이 멤버를 참고하기 위해, . 을 사용하고 있습니다.

#include <stdio.h>
//#include <string.h>
#include <iostream.h>

struct STest {
    int age;
    char name[80];
};//struct STest

void main() {
    struct STest s={30,"Seo JinTaek"};

    cout << s.age << endl;//STest의 멤버 age를 참고하기 위해 . 을 사용한
                          //다.
    cout << s.name << endl;
}//main

 C에서는 구조체의 형 이름은 struct STest입니다. 하지만, C++에서는 STest도 형 이름입니다. 만약 C++모드에서 이 프로그램을 컴파일한다면, struct STest를 STest로 대치해서 컴파일해도 문제가 없습니다. 이것은 클래스와의 호환성 때문에 C++에서 개선된 사항입니다.

  배열과는 다르게 s는 구조체 자신임 – 이것이 무엇이든 – 을 주의하세요. 구조체의 시작 주소를 얻기 위해서는 &s를 사용해야 합니다.

  구조체가 선언되지 않고 구조체를 가리키는 포인터가 선언된 경우 어떻게 할 것인가요?

#include <stdio.h>
#include <string.h>
#include <iostream.h>

struct STest {
    int age;
    char name[80];
};//struct STest

void main() {
    struct STest *s;
    s=new STest;//포인터이므로 메모리 할당이 필요하다.
    (*s).age=30;
    strcpy((*s).name,"seojt");//(*s).name="seojt"는 왜 안되는가?
    cout << (*s).age << endl;
    cout << (*s).name << endl;
    delete s;//new로 할당한 메모리는 반드시 delete한다.
}//main

  우리는 s가 구조체가 아니라, s가 가리키는 내용, 즉 *s가 구조체라는 것을 알고 있습니다. 그러므로 구조체 *s의 멤버를 접근하기 위해 . 을 사용하여, (*s).age 처럼 쓴다. 연산자의 우선 순위 때문에 괄호가 반드시 필요합니다. 여기에 핵심이 있습니다.

  (*s).age는 s->age와 동일합니다. 즉 -> 는 구조체 포인터 변수에서 쉽게 멤버를 접근하기 위해 사용되는 연산자입니다. 이것은 구조체가 포인터로서 사용될 일이 많기 때문에 효율적인 코딩 – 타이핑을 줄이기 위해 – 을 위해서 만들어진 연산자입니다. 수정된 소스는 아래와 같습니다.

#include <stdio.h>
#include <string.h>
#include <iostream.h>

struct STest {
    int age;
    char name[80];
};//struct STest

void main() {
    struct STest *s;
    s=new STest;
    s->age=30;
    strcpy(s->name,"seojt");
    cout << s->age << endl;
    cout << s->name << endl;
    delete s;
}//main


전처리(Preprocessor)

#      스트링화 연산자

##     토큰 연결(token concatenation) 연산자

  #는 파운드 기호(pound sign)라고 읽고, ##는 더블 파운드 기호(double pound signs)라고 읽습니다. 일반적으로 샾(sharp)라고 읽으므로, 이렇게 읽어도 무방합니다.

  우리는 처리(processing)란 말이 컴파일러가 기계어 코드를 생성하는 과정을 의미한다는 것을 알고 있습니다. 그래서 컴파일 전에 사용되는 명령문을 전처리 명령문(preprocessing operator) 혹은 컴파일러 지시자(compiler directive)라 합니다. 컴파일하기 전에 어떤 일을 지시하는 것입니다. 위의 두 가지 연산자는 전처리 명령문에 사용되기 때문에, 전처리 연산자로 구분됩니다.

  #은 큰따옴표(“)가 없는 문자 순서(string sequence)를 문자열로 만듭니다.

#define stringit(x) #x

라고 선언되었다면, 프로그램 소스에서

stringit(Seo JinTaek)

라고 쓰면, 컴파일 전에 “Seo JinTaek”라고 치환됩니다.

  ##는 두 개의 토큰(token)을 컴파일 전에 연결하는 연산자입니다.

 컴파일러가 기계어 코드를 만들기 위해 처리하는 기본 단위를 토큰이라 합니다. void main ( )에서 토큰은 4개입니다.

#define tokencat(x,y) x##y

라고 선언되었다면,

tokencat(i,j)

라고 쓰면, 컴파일 전에 ij 로 치환됩니다. 아래의 소스를 참고하세요.

#include <iostream.h>
//#define charit(x) #@x//이 연산자 #@는 각자가 사용하는 컴파일러의 도움말
                       //을 참고하세요.

#define stringit(x) #x
#define tokencat(x,y) x##y

void main(void)
{
    int i=1,j=2,ij=3;
    cout << stringit(hello) << '\n';
    cout << tokencat(i,j) << '\n';
}
/* hello
   3    */


참조/역참조(Reference/Dereference)

&      참조 연산자 혹은 주소 연산자(address-of operator)

*       역참조 연산자

 재참조 연산자, 간접지정 연산자(indirect operator)라고도 합니다.

  &는 앰퍼샌드(ampersand)라고 읽으며, *는 애스트리스크(asterisk)라고 읽습니다. *는 별표(star)라고 읽어도 무방합니다.

  &는 데이터가 저장된 곳의 주소를 얻기 위해 사용합니다.

int i=2;

라고 선언된 변수 i에 대해, i는 2를 의미합니다. 하지만, &i는 실제 값 2가 들어 있는 곳의 메모리의 위치, 즉 주소(address)를 의미합니다. i값을 의미하는 것이 아니라, i값이 있는 곳의 참조(reference)를 의미하도록 할 수 있는데 이때 &를 참조 연산자라 합니다.

  C에서 &의 결과는 주소를 의미하므로, 포인터 변수에만 대입 가능합니다.

int j;

에 대해

j=&i;

는 불가능합니다. 형이 다른 변수끼리는 대입이 불가능합니다. 아래의 소스는 j를 i의 참조 값, 즉 i의 주소로 초기화시킨니다.

#include <stdio.h>

void main() {
    short i=2;
    int *j;
    j=&i;
}//main

 i가 1000번지에 할당되었고, j가 1002번지에 할당되었다면 메모리의 구조는 다음과 같습니다.


 메모리의 구조

  즉 j에는 i의 주소 값 1000이 들어 있습니다.

 정수 1000과 주소 1000을 구분하기 위해, 문맥에 따라 [1000]을 주소 1000으로 사용합니다.

  j가 가리키는 값 – i값, 즉 1000번지에 있는 값 – 을 참조하기 위해서는 어떻게 할 것인가요? 바로 역참조 연산자 *를 사용하는 것입니다. *j는 j가 가리키는 값, 즉 정수 2를 의미합니다. 아래 프로그램의 결과를 주의해서 살펴 보세요.


#include <stdio.h>

void main() {
    int i=2;
    int *j;
    j=&i;
    printf("%d,%p,%p\n",*j,j,&j);
}//main

  메모리의 구조가 위 그림과 같다면,

2,[1000],[1002]

가 출력될 것입니다. 역참조 연산자는 오직 포인터 변수에만 쓸 수 있습니다. *i는 무의미하며, 컴파일되지도 않습니다.

  변수를 선언(declaration)하는 시점의 *와 사용(use)하는 시점의 *를 구분하세요.

int *j;

는 j가 int를 가리키는 포인터 변수임을 의미합니다. 하지만,

*j

라고 사용되면, j가 가리키는 값을 의미합니다. j값을 사용하려면,

j

라고 사용합니다.

  마지막으로 주의할 사항은, 문맥에 따라 & 는 비트 곱(bitwise AND), 참조 연산자로도 사용되며, * 은 곱하기(multiply) 연산자로도 사용된다는 것입니다.


관계(Relational)

==     같음 연산자(equality operator)

!=      안같음 연산자(inequality operator)

<      작다 연산자(less-than operator)

>      크다 연산자(greater-than operator)

<=     작거나 같다 연산자(less-than or equal operator)

=     크거나 같다 연산자(greater-than or equal operator)

 일반적으로 기준이 되는 수(number) n이 왼쪽에 있다고 봅니다. 그러므로 n < 의 형태이므로 ‘보다 작다(less than)’라고 읽습니다.

  수학에서는 관계 연산자의 결과가 참/거짓이지만, C에서는 1/0입니다. 이것은 관계 연산자가 수치 표현식(numerical expression)임을 의미합니다. 비록 1을 참이라고 간주해도 되지만, 정확한 것은 아닙니다. 1은 참이 아니라, 2바이트 정수 0000 0000 0000 0001(2)입니다.

  ==은 할당 연산자 =와 구분하세요.

i==j

i=j

는 매우 다릅니다. i==j는 i와 j의 값을 비교하여, 같으면 1, 아니면 0이 되는 표현식이며, 이 표현식을 거친 후 i,j의 값에는 변함이 없습니다. 하지만, i=j는 j의 값을 i에 할당하며, 표현식을 거친 후, i의 값은 j의 값으로 바뀝니다. 만약

if (i==j)

라고 표현되어야 할 문장이,

if (i=j)

라고 표현되었다면 심각한 문제가 발생합니다. 두 번째의 if문은 j가 0이 아니라면 조건은 항상 참(1)입니다.

  !=는 수학의 ≠에서 유래(origin)하였습니다.

  >= 같은 연산자가 => 나 > = 처럼 표현되어서는 안 됩니다. 2개의 심벌이 1개의 토큰을 이루는 경우입니다. 아래 종합적인 예제를 참조하세요.

#include <stdio.h>

void main() {
    int i=2,j=3,k=4;
    printf("%d,%d,%d,%d\n",i>j, i==j, i!=j, j<=k);
    //       0  0  1  1
}//main


sizeof

sizeof <expression>

sizeof ( <type> )

  sizeof 연산자는 표현식이나 형이 차지하는 바이트 수(number)를 구하기 위해 사용합니다. sizeof 가 구하는 바이트 수가 형(type)에 관한 것이면, 반드시 괄호를 필요로 합니다. 하지만, 표현식이라면 괄호가 없어도 됩니다. 그러므로, sizeof 2는 sizeof(2)와 동일합니다. 하지만, sizeof int라고 쓸 수 없으며, sizeof(int)라고 써야 합니다.

 이것이 함수 호출처럼 보이지만, 연산자임에 주의하세요.

  우리가 사용하는 운영체제가 16비트 운영체제라면, 혹은 컴파일러가 16비트용이라면, sizeof 2와 sizeof(int)의 값은 모두 2일 것입니다. 아래의 예제를 참고하세요.

 int는 어떤 기계에서는 4바이트입니다. 10진 상수 표현 2는 일반적으로 정수 상수이므로, int의 크기와 동일합니다. 크기가 큰 정수형 상수를 명시적으로 선언하기 위해서는 접미어(postfix) L,UL등을 붙입니다.

  이 예제는 16비트 컴파일러에서 컴파일 되었으므로, 결과는

2,2

2,4

입니다.

#include <stdio.h>

void main() {
    int i,j;
    i=sizeof(int);
    j=sizeof i;
    printf("%d,%d\n",i,j);
    printf("%d,%d\n",sizeof 2,sizeof 2L);
}//main

  sizeof 는 동적 메모리 할당에서 크기를 얻기 위해, 빈번히 사용합니다. 아래의 소스는 무엇이 잘못인가요?

#include <stdio.h>
#include <alloc.h>

void main() {
    int *ip;
    ip=(int *)malloc(2);//왜 2바이트를 할당해야 하는가요?
      //! 위 문장은 정수 할당을 보장하지 못합니다.
    *ip=2;
    printf("%d\n",*ip);
    free(ip);
}//main

  int가 2바이트라면, 위의 소스는 에러가 없습니다. 하지만, int가 4바이트인 컴파일러에서 실행한다면, 실행 시(run time)에 에러가 발생할 소지(possibility)가 있습니다. 위의 소스는 아래와 같이 고쳐서 사용하는 것이 마땅합니다.

#include <stdio.h>
#include <alloc.h>

void main() {
    int *ip;
    ip=(int *)malloc(sizeof(int));//확실하게 정수 할당을 보장합니다.
    *ip=2;
    printf("%d\n",*ip);
    free(ip);
}//main

  sizeof를 포인터 변수에 대해서 쓸 때 주의해야 할 사항이 있습니다. 아래의 소스는 어디가 잘못인가요?

#include <stdio.h>
#include <alloc.h>
#include <string.h>

struct STest {
    int age;
    char name[80];
};//struct STest

void main() {
    struct STest *s;
    s=(struct STest*)malloc(sizeof(s));//s는 4바이트이다.
    s->age=30;
    strcpy(s->name,"seojt");
    printf("%s : %d\n",s->name,s->age);
    free(s);
}//main

  s는 포인터이므로 4바이트입니다. 그러므로 sizeof(*s)가 되는 것이 맞습니다. 확실하게 sizeof(struct STest)라고 하는 것이 좋습니다.

 물론 메모리 모델(memory model)과 기계(machine)에 따라 2바이트가 될 수도 있습니다.

#include <stdio.h>
#include <alloc.h>
#include <string.h>

struct STest {
    int age;
    char name[80];
};//struct STest

void main() {
    struct STest *s;
    s=(struct STest*)malloc(sizeof(struct STest));
    s->age=30;
    strcpy(s->name,"seojt");
    printf("%s : %d\n",s->name,s->age);
    free(s);
}//main

  우리가 sizeof 와 더불어 주의해야 할 사항은 size_t에 관한 것입니다. 이 부분은 C++관련 서적을 참고하세요.


 형 변환(casting: type conversion)

  아래 프로그램의 결과는 얼마일까요?

#include <stdio.h>

void main() {
    char c=12;
    int i=0x1234;
    c=i;//이 문장에서 무엇이 일어났는가요?
    printf("%x\n",c);
}

  결과는 다음과 같습니다.

34

 16진수로 인쇄된 것입니다.

  결과가 우리의 예상과 같습니까? 위의 프로그램은 분명히 잘못입니다. 왜냐하면, 2바이트 정수를 1바이트 정수에 대입하려고 시도했기 때문입니다. main()에서

c=i;

란 문장은 2바이트 정수 i를 1바이트 정수 c에 대입하려고 시도합니다. 하지만, 이것은 불가능합니다. 비둘기 집이 5개이고, 비둘기가 6마리이면, 1마리의 비둘기는 집이 없습니다. 그래서 C++ 컴파일러는 2바이트 정수를 1바이트 정수로 형을 변환하려고 시도합니다. 이 예의 경우 i의 2바이트 값 중 상위 바이트(higher byte) 0x12를 무조건 버립니다. 그리고, 하위 바이트(lower byte) 0x34를 c에 대입하는 것입니다.

  실제로 위의 문장은

c=(char)i;

라고 사용해야 합니다. (char)은 연산자로서 피연산자가 1바이트 정수로 형 변환되어야 함을 지시합니다. 이러한 연산자를 형 변환 연산자라 합니다. 하지만 위의 예에서처럼, 명시적으로 (char)을 지정하지 않아도 형 변환이 일어나는데 이러한 형 변환을 자동 형 변환(automatic casting)이라 합니다. 하지만, 자동 형 변환에 의존하는 것은 좋은 프로그램 습관이 아닙니다. 아래의 예를 보세요. 잘못된 부분을 수정해 보세요.

#include <stdio.h>

void main() {
    char c=12;
    int i=0x1234;
    i=c;
    printf("%x\n",c);
}

  실행 결과는

c

 12의 16진수는 c입니다.

입니다. 위의 예에서 i=c; 란 문장에서 역시 자동 형 변환이 일어났습니다. 1바이트 정수값 c를 2바이트 정수값 i에 대입한 것입니다. 하지만, 이 경우 자동 형 변환은 언제나 안전(safe)합니다. 왜냐하면, 1바이트를 2바이트로 확장하는 경우 오버플로우가 발생하지 않기 때문입니다. 그럼에도 불구하고, 위의 소스는 잘못되었습니다.

i=c;

란 문장은 1바이트 정수가 2바이트 정수로 형 변환이 일어난다 – 그것이 실제 일어남에도 불구하고 – 라는 표현을 하지 못합니다. 그러므로 다음과 같이 소스를 수정하는 것이 바람직합니다.

#include <stdio.h>

void main() {
    char c=12;
    int i=0x1234;
    i=(int)c;
    printf("%x\n",c);
}

  물론 위의 소스는 이전 소스와 동작과 결과가 같습니다. 하지만, 분명한 형 변환을 명시하므로, 이전의 소스보다 읽기 좋습니다. 정수형끼리 실수형끼리 형 변환은 자동으로 일어납니다. 형 변환은 모든 형에 대해서 허용됩니다. 즉 괄호안에 명시할 수 있는 형은 사용자 정의형을 포함하여 모든 형입니다. C++에서는 형 변환 연산자를 오버로드하는 것까지 허용합니다.

  실행시에 클래스의 계층 정보를 이용하여 안전하게 형을 변환하는 것을 허용하기 위하여, 새로운 4개의 형 변환 연산자가 추가되었습니다. 이 주제는 ‘만화가 있는 C++’에서 자세히 다룰 것입니다.

  분명하게 형 변환 연산자를 써 주어야 하는 경우가 발생합니다. 그것은 대부분의 경우 포인터와 연관된 것입니다. 아래의 소스를 보세요. 무엇이 잘못인가요?

#include <stdio.h>
#include <stdlib.h>

void main() {
    int* ip;
    ip=malloc(sizeof(int));
    *ip=100;
    printf("%d\n",*ip);
    free(ip);
}

Visual C++에서 실행하면 다음과 같은 에러 메시지가 발생합니다.

cannot convert from ‘void *’ to ‘int *’

  malloc()은 힙에서 동적으로 메모리를 할당합니다. 위의 예에서 malloc()은 정수를 위해 메모리를 할당하고 그것의 시작 주소를 리턴합니다. 만약 그것이 [1000]이었다면, [1000]을 ip에 대입합니다. 하지만, 여기서 오류가 발생합니다. malloc()는 자신이 할당한 메모리가 어떤 용도로 사용될지 알지 못합니다. 그래서 malloc()가 리턴하는 포인터 형은 void* 입니다.

ip=malloc(sizeof(int));

그러므로 위 문장은 void*를 int*로 대입하려고 하므로, 에러가 발생하는 것입니다. 서로 다른 형 사이에 실제로는 대입이 불가능합니다. 알다시피 포인터의 경우는 가리키는 대상이 무엇이냐에 따라 가감의 해석이 달라지므로, 형 변환은 매우 중요합니다. 그래서 컴파일러가 에러를 발생하는 것입니다. 소스는 다음과 같이 수정되어야 합니다.

#include <stdio.h>
#include <stdlib.h>

void main() {
    int* ip;
    ip=(int*)malloc(sizeof(int));
    *ip=100;
    printf("%d\n",*ip);
    free(ip);
}

  실행 결과는

100

입니다.

  어떤 명시적인 형 변환이 항상 안전하게 일어나는 것일까요? C++의 클래스가 복잡하게 계층이 얽힌 경우 반드시 그렇지 않습니다. 결론부터 말하면, C의 형 변환은 그 변환이 안전하다라는 보장을 하지 못합니다. 그래서 C++에는 항상 안전하게 형 변환을 하도록 4개의 새로운 형 변환 연산자가 추가되었습니다.


함수형 형변환(functional casting)

  정수형(int)의 변수 i를 문자형(char)의 변수 c로 변환하기 위한 문장은

        int i=65;

        char c;

        c=(char)i;

와 같습니다. 위의 형 변환 문장은 함수호출처럼 사용할 수 있습니다.

c=char(i);

  위의 문장이 비록 파라미터 i를 가지는 char()함수를 호출하는 것처럼 보이지만, 위의 문장은 c=(char)i;와 같습니다. 즉, i의 형을 명시적으로 char로 변환하여 결과를 c에 대입합니다. 아래의 프로그램 조각은 A를 출력합니다.

#include <iostream.h>

void main() {
    int i=65;
    char c;
    c=char(i);
    cout << c << endl;
}//main

결과는 다음과 같습니다.

A


 실습문제

1. xy를 계산하는 최적의 방법을 기술하세요.

2. 16n을 계산하기 위해 <<를 어떻게 이용할 수 있습니까?

3. 어셈블리어의 회전 연산자(rotate operator)를 함수로 구현하세요.

4. C++에서 사용 가능한 모든 연산자를 체계적으로 구분하고 간단히 설명하세요.

5. 아래의 소스에서 잘못된 부분이 있으면, 잘못된 부분을 수정하세요.

       #include <stdio.h>
       void main() {
line4:    printf("hello");
line5:    printf("world");
       }

6. 아래의 소스에서 t.*대신에 t->*를 사용하도록 하려면, 소스를 어떻게 수정해야 할까요?

void SetValue(CTest &t,int i)
{
    int CTest::*ip;
    ip=&CTest::a;
    t.*ip=i;
}//CTest::SetValue

@

만화가 있는 C

Leave a Reply