Ⅰ. C Promgram

C-language

□□□□□□□ 모든 C 프로그램에 반드시 나타나야 할 이름이며 ()는 앞의 이름이 함수임을 의미,

□      main이란 함수는 모든 C 프로그램에 반드시 있어야 하며 이 함수에서 프로그램의 수행

□      이 시작된다.

main()

{   □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□ 이 중괄호는 함수 또는 블럭의 시작을 나타낸다.

printf("Hello, world !\n");  □□□□□ 문장을 나타내며 맨 끝을 보면 ';'이 있는데 이는 문장의 끝

}  □□□                            을 나타내는 기호로 C의 모든 문장은 반드시 ';'로 끝나도록

□                            되어 있다.

2번줄의 중괄호에 대한 닫음이다(main 함수의 끝을 나타낸다).

C는 열은 괄호의 갯수와 닫은 괄호의 갯수가 같아야 한다.

C에는 출력문이 존재하지 않으며, 3번줄의 문장은 printf라는 함수를 호출하는 함수 호출문이  며 printf라는 함수가 화면에 출력시키는 일을 하는 것이다. 이 때 소괄호 사이에 들어 있는 내   용이 화면에 그대로 출력되는데, 3번 줄의 문장을 보면 이에 해당하는 것이 "Hello, world !\n"    로 이중 따옴표로 싸여 있는 것을 볼 수 있다.

C에서 이중 따옴표로 둘러 싸인 것은 바로 스트링(string-스트링은 여러 개의 문자가 연이어    나온 것을 의미)으로 3번줄의 문장은 "Hello, world !\n"란 스트링을 출력하라는 의미가 된다.

실제로 위의 프로그램을 수행하면 다음과 같이 출력 결과가 나온다.

Hello, world !

즉, '\n'은 출력되지 않는다. 여기서 '\n'은 \n을 출력하라는 것이 아니라 다음 줄로 넘어가라는   것을 의미한다. □\n□은 스트링의 어느 위치에나 올 수가 있으며 그 의미는 항상 같다. 따라서      printf("Hello,\nworld !\n"); 을 수행하게 되면 다음과 같은 출력을 얻게 된다.

Hello,

world !

main() {

printf("< Warning! \"%%%%%%\" >\n");

}                  □□□□□□□□□□□□□□

□□□□□□□□□□□□□□□□□  printf에는 이중 따옴표가 한 쌍만 존재해야 하는                                              데 뒤의 이중 따옴표를 출력하기 위해 '\'를 붙임.

'%'를 printf를 사용하여 출력하고자 할 경우에는 앞에 '%'를 하나 더 붙여야 한다.

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

< Warning! "%%%" >

□□□□□□□□□□□□□□ #include <stdio.h>는 printf함수를 쓰기 위해 printf가 포함되어 있는 표준 라      □              이브러리인 stdio(Standard Input Output)의 header file을 포함하는 것이다.

□              stdio.h의 양쪽에 부등호로 묶은 것은 이것이 C의 표준 라이브러리 헤더임을      □              알려준다. 만약 사용자가 만든 헤더 화일일 경우 큰 따옴표(" ")로 묶어준다.

#include <stdio.h>

main() {

int height, base;               /* height : 높이,  base : 밑변의 길이  */

float extent;                   /* extent : 면적                       */

base = 5;

printf(" Enter the length of the height : ");

scanf("%d",&height);

extent = 0.5 * base * height;                      /* 면적을 계산    */

printf(" The extent of a triangle is %f\n",extent);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Enter the length of the height : 4

The extent of a triangle is 10.000000

10 뒤의 소숫점자리 뒤에 0이 6개나 붙은 이유는 printf함수가 소수를 표현할 때 그 유효숫자의 내정치(default)를 6자리까지로 정해 놓았기 때문이다.

/*   주석  ....................................................   */

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

이 사이의 내용은 컴파일러가 상관하지 않기 때문에, 이 사이에는 아무 내용이나 집어 넣을     수 있다 (실제로 이 내용은 프리프로세서가 깨끗히 지워주기 때문에  C 컴파일러 자신은 주석    이 있었는지조차 알지 못한다).

주석안에 또 다른 주석이 있어서는 안된다.

/*   바깥쪽 주석 .............   /*   안쪽 주석 ....................   */      ....................  */

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□  □□□□□□□□□□□□□□□□→ 에러 발생

주석 안에 '/*'로 시작해서 '*/'로 끝나는 주석이 또 존재하는데 이와 같이 주석이 중첩되어서   는 안된다. 이는 프리프로세서가 '/*'로 주석이 시작된 것을 안 다음에는 무조건 '*/'이 나올 때  까지 지우기 때문이다.

scanf("%d",&height);

scanf함수는 printf함수와 함께 C의 입출력 함수 중 가장 많이 쓰이는 함수이다. 여기서 '%d'는  10진수의 입력을 받겠다는 뜻이며, height앞의 &기호의 의미는 그 다음에 붙어 있는 변수의 주   소를 알려준다. 왜냐하면 scanf 함수는 입력받고자 하는 변수의 주소를 원하기 때문이다.

printf문내의 '%f'는 부동 소수점형(floating type)으로 출력을 하라는 뜻이다.

Ⅱ. 데이터 유형

< 정수 데이터 유형 >

short int (short, signed short int) unsigned short int (unsigned short) int (signed, signed int) unsigned int (unsigned) long int (long, signed long int) unsigned long int (unsigned long)	2byte 2byte 2byte 2byte 4byte 4byte	-32,768 ~ 32,767 0 ~ 65,535 -32,768 ~ 32,767 0 ~ 65,535 -2,147,483,648 ~ 2,147,483,647 0 ~ 4,294,967,295

위에서 괄호 안의 이름들은 모두 대신 사용할 수 있는 것들이며 unsigned가 붙은 것과 붙지 않은 것, 크게 두 종류로 구분할 수 있는데 unsigned가 붙은 것은 말 그대로 부호가 필요 없는 정수, 즉 양의 정수를 의미한다. unsigned가 없는 것들은 당연히 부호가 있는 일반 정수를 의미하며 여기에는 양의 정수와 음의 정수가 다 존재할 수 있다. 반면에 unsigned를 빼면 short int 와 그냥 int, long int로 나눌 수 있는데 이는 정수의 크기에 따른 분류이다. 이름에서 알 수 있듯이 short int는 크기가 작은 정수들을 의미하며 long int는 그 보다는 큰 정수들을 의미한다.

IBM-PC에서 수행되는 MSC와 TC의 경우에 short int는 메모리의 2byte를 차지하는 정수를 의미하며 16bit의 크기로 -2(16-1) ~ (2(16-1)-1) = -32768 ~ +32767의 정수를 나타내며 long int의 경우에는 32bit이므로 -2(32-1) ~ (2(32-1)-1) = -2147483648 ~ +2147483647의 정수를 나타낼 수 있다.

short int와 long int의 크기가 2byte와 4byte로 거의 고정적인데 반해 int 유형의 정수의 크기는 시스템마다 달라진다. 예를 들어 IBM-PC의 MSC와 TC의 경우에는 short int와 같이 2byte가 되나 SUN workstation의 경우에는 long int와 같이 4byte가 int 유형의 크기가 된다. 이 크기는 사용하는 시스템에서 가장 잘 처리할 수 있는 데이터의 크기 즉 한 word의 크기를 갖는 것이 일반적이다. 따라서 16bit 컴퓨터인 IBM-PC의 경우에는 int 데이터 유형이 당연히 16bit가 된다(32bit 프로세서의 경우에 하드웨어는 32bit이지만 16bit 컴파일러들을 그대로 사용하기 때문에 int 유형이 16bit가 된다). 그러므로 int 데이터 유형은 언제나 가장 빨리 처리할 수 있는 데이터 유형이 되며 일반적으로 제일 많이 사용되고 있다.

unsigned가 붙은 것들은 크기는 같지만 음수가 없고 모두 양수만 있다고 가정하기 때문에 표현할 수 있는 범위가 더 커지게 되는데 unsigned short int는 0 ~ (216-1) = 0 ~ 65535까지의 수를 나타낼수 있으며 unsigned long int는 0 ~ (232-1) = 0 ~ 4294967295까지 나타낼 수 있다.

※ bit는 컴퓨터로 표현되는 가장 작은 단위인 0과 1의 상태이고, 8개의 bit가 모여 1byte를 이루며, 2byte를 word라 하고, 4byte를 double word라 한다. IBM-PC의 메모리 입출력의 기본은 1byte이다.

정수형(int)은 양의 방향으로 최대가 32767인데 만약 int로 선언한 변수의 값이 이 범위를 넘어갔을 경우 결과는 다음과 같다.

#include <stdio.h>

main() {

int boxes, kilogram;

int total;

kilogram = 30;

boxes = 1500;

total = boxes * kilogram;

printf("There are %d kilograms in 1500 boxes of apple.\n",total);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

There are -20536 kilograms in 1500 boxes of apple.

%d의 %는 출력할 위치를 나타내고 d는 정수(decimal)임을 나타낸다.

C 컴파일러는 에러를 내지 않는다. 프로그래머가 알아서 조치를 취해야만 하는데 여기서는 int형 대신에 unsigned int나 long형을 쓰면 올바른 답을 얻을 수 있다. 단, long형으로 고칠 경우에는 printf문 내의 '%d'를 '%ld'로 고쳐야 한다(unsigned int형일 경우 %u).

-20536이 나온 이유를 알기 위해서는 컴퓨터 내부의 숫자 표현 방법을 알아야 한다. int형에서 32767을 넘게 되면, 즉 32768을 int형 변수에 대입하면 -32768이 되어 버린다. 계속해서 32769를 대입하면 -32767이 되고 32770을 대입하면 -32766의 값을 가지게 되는데, 이것은 컴퓨터 내부의 숫자 표현 방식이 2의 보수 표현 방식을 채택하고 있기 때문이며 signed의 경우 맨 왼쪽의 1bit를 부호 비트로 사용하고 있기 때문이다.

< 실수 데이터 유형 >

float

double

long double (Unix에서는 아직 사용할 수 없다)

MSC와 TC의 경우에 float은 32bit를 사용하여 실수를 나타내고 있으며 double은 64bit, 그리고 long double은 80bit를 사용하고 있다. 컴퓨터에서 실수를 표현할 때에는 지수 부분과 가수 부분

으로 나누어 표현한다.

□□□□□□□□□□□□□□□□□□□□□□□

□ 가수부분 □ 지수부분 □

□□□□□□□□□□□□□□□□□□□□□□□

3byte       1byte

float은 소숫점 이하 7자리까지의 수를 표현할 수 있으며 double은 소숫점 이하 15자리 까지, long double은 소숫점 이하 19자리까지 수를 표현할 수 있다. 따라서 보다 더 정밀한 계산을 하고자 할 때에는 double이나 long double을 사용하면 된다.

#include <stdio.h>

main() {

float f_amount;

double d_amount;

f_amount = 123456750.12;

d_amount = 123456750.12;

printf(" Float type result = %f\n",f_amount);

printf("Double type result = %lf\n",d_amount);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Float type result = 123456752.000000

Double type result = 123456750.120000

※ IBM-PC의 내부에는 부동 소수점 연산을 할 수 있는 장치가 되어있지 않으며 따로 부동 소수점 연산 장치(math-coprocessor : 8087, 80287, 80387 등)를 설치해야 한다. 그러나 C에서는 부동 소수점 에뮬레이터(emulator)라는 것을 사용하여 부동 소수점 연산 장치 없이도 부동 소수점 연산을 할 수 있도록 만들어 놓았으며, 부동 소수점 변수를 사용할 경우 자동으로 이러한 에뮬레이터 루틴을 exe 화일에 링크 시켜 준다. 이런 이유로 아주 간단한 프로그램도 실행 화일의 크기가 커지는 경우가 있다.

< 문자 데이터 유형 >

char (signed char) unsigned char	1byte 1byte	-128 ~ 127 0 ~ 255

C에서의 문자 데이터는 문자 하나, 즉 글자 한 글자를 의미하며 정수 데이터 유형의 경우와 같이 unsigned가 붙어 있는 것과 그렇지 않은 것이 있는 것을 알 수 있다.

문자를 컴퓨터에 표현하려면 특정 코드를 사용하여야 하는데 C에서는 이 코드로 ASCII라는 이름의 코드를 사용하고 있다. ASCII코드는 7bit로 1문자를 나타내기 때문에 C에서도 7bit로 문자를 나타내게 되는데 메모리의 기본 단위가 1byte = 8bit이기 때문에 8bit로 한 문자를 나타내게 된다. 그런데 이 코드라는 것이 2진수이기 때문에 겉으로 볼 때 정수와 전혀 차이가 없다. 그래서 C언어에서는 아예 이 둘 사이의 구분을 없애 버렸다. 단지 문자 데이터 유형은 1byte의 크기를 갖는 정수 데이터 유형으로 간주하게 된 것이다. 즉 C에서의 문자 데이터 유형은 크기가 1byte인 정수 데이터 유형으로 생각하면 된다.

문자 데이터 유형은 정수 데이터 유형과 같이 나타낼 수 있는 수의 범위가 존재하게 되는데 크기가 모두 1byte이기 때문에 char는 -2(8-1) ~ (2(8-1)-1) = -128 ~ +127 사이의 정수를 나타낼 수 있으며 unsigned char는 0 ~ (28-1) = 0 ~ 255의 정수를 표현할 수 있다.

※ 프로그램 내에서 데이터를 사용하고자 할 때에는 이를 어떤 데이터 유형으로 표현할 것인가를 결정지어야 한다. 이 경우 선택할 수 있는 것이 여러 개가 존재할 수 있다. 예를 들어 +2라는 정수를 사용하고자 할 때 이를 나타낼 수 있는 데이터 유형은 short int, unsigned short int, int, unsigned int, long int, unsigned long int, char, unsigned char 등 모두 8가지나 있다. 이 때 어떤 것을 선택하는 것이 가장 좋은가는 여러 가지 면을 고려해 보아야 하는데 이 중 특히 메모리의 양과 처리 속도를 생각하지 않을 수 없다. 단순히 메모리 양만 따진다면 unsigned char나 char가 가장 좋을 것이다.

그러나 실제로 꼭 1byte 밖에 차지하지 않는 것은 아니다. 프로그램 내에서 데이터를 사용하게 되면 이 데이터는 메모리를 차지하게 되는데 이 때 특정 주소의 메모리만 차지하도록 제한이 주어지는 경우가 있다. 가장 많은 것이 특정 수의 배수의 주소에만 데이터가 들어가도록 제한하는 것인데 예를 들어 주소가 4의 배수인 경우에만 데이터가 들어가도록 제한할 수 있다.

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

□                                      □   이때 char 유형의 데이터를 2개 사용하게 되면

□   주소   □           □               □  2byte의 메모리를 사용해야 하는데 실제 사용하는

□   1000   □□□ char1□□□□               □  메모리의 양은 2byte가 아니라 8byte가 되며, 이

□   1001   □□□□□□□□□□□□□□□             □  중 6byte는 안 쓰고 낭비하게 된다. 따라서 실제로

□   1002   □□□□□□□□□□□□□ □ 사용 안함   □  long int 2개를 사용하는 것과 같은 결과를 낳게

□   1003   □□□□□□□□□□□□□ □             □  되는데, 메모리를 절약하고자 할 때에는 이와 같은

□   1004   □□□ char2□□□□□□             □  사항을 잘 고려하여야 한다. MSC의 경우에는 주소

□   1005   □□□□□□□□□□□□□□□             □  가 2의 배수인 짝수 주소에만 데이터를 할당하며

□   1006   □□□□□□□□□□□□□ □ 사용 안함   □  TC의 경우에는 char 유형은 임의의 주소에, 그밖

□   1007   □□□□□□□□□□□□□ □             □  의 데이터는 짝수 주소에 할당하고 있다. 그러나

□          □□□□□□□□□□□□□□□             □  Unix의 경우에는 주소가 4의 배수인 경우에만 할

□                                      □  당하고 있어서 메모리가 낭비될 가능성이 높다.

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

< 상수 >

상수는 말 그대로 프로그램 전체에 걸쳐 그 의미가 변하지 않는 값을 말하는데, 예를 들어 123은 정수 백이십삼을 나타내는 상수로 이는 항상 정수 백이십삼을 나타내는 기호로 사용된다. 또 꺼꾸로 정수 백이십삼을 프로그램 내에서 사용하고 싶을 때에는 123이라는 상수를 사용하여야 한다.

⑴ 정수 데이터 유형의 상수

10진수

8진수   □□ 하드웨어를 제어하거나 한글 처리와 같이 비트 단위 연산이 필요한 경우

16진수  □□

일반적으로 다른 프로그래밍 언어에서는 10진수 밖에는 정수 상수를 표현할 수 없으나 C에서는 이를 8진수나 16진수로도 표현할 수 있다. 10진수로 정수 상수를 나타낼 때에는 우리가 일반 생활에서 사용하듯이 127, 0, -89와 같이 나타내면 된다. 이 때 양의 정수의 경우에는 앞에 +란 부호를 붙여도 된다.

반면에 8진수로 정수 상수를 나타내고자 할 때에는 앞에 0을 붙이고 원하는 숫자를 적으면 된다. 013은 8진수 13으로 8*1+3 = 11, 즉 십진수 11을 나타낸다. 주의할 것은 숫자 앞에 0이 있으면 8진수로, 없으면 10진수로 간주하기 때문에 013과 13은 같은 수가 아니라는 점이다. 그리고 숫자 앞에 0이 10개가 있건 100개가 있건 시작이 0으로 시작하면 무조건 8진수이다.

16진수로 정수 상수를 나타내고자 할 때에는 앞에 0x나 0X를 붙이고 원하는 숫자를 16진수로 적으면 된다. 예를 들어 0x13은 16진수 13으로 16*1+3 = 19, 즉 십진수 19를 의미하며 0xff는 십진수 255를 의미한다.

지금까지의 정수들은 모두 int 유형에 속한 것이 된다. 10진수를 사용하건 8진수를 사용하건 숫자뒤에 아무 것도 안붙이면 모두 int 유형의 상수가 된다.

123,  -75,  0755,  -0563,  0x3ABB,  -0XFF

unsigned int 유형의 정수 상수를 사용하고자 할 때에는 맨 끝에 u나 U를 붙이면 된다.

123u,  65535U,  0755u,  0177777U,  0x3ABBu,  0XFFFFU

long int 유형의 정수 상수의 경우에는 끝에 l이나 L을 붙이면 된다.

123l,  -2100000000L,  077777L,  -0356l,  0x0L,  -0X7FFFl

unsigned long int 유형의 정수 상수의 경우에는 unsigned의 u와 long의 l을 끝에 같이 붙이면 된다. 이 때 대소문자 어느 것이나 사용해도 좋으며 u와 l 중 어느 것을 먼저 써도 상관 없다.

0ul,  123245UL,  07324lu,  0177777LU,  0xFFFFlU,  0xABCDLu

※ C에서 short int와 unsigned short int 유형의 정수 상수는 존재하지 않는다. 최소한 int 유형 이상은 되어야 한다는 것이다. 즉 C에서는 정수 수식을 처리할 때 short int나 unsigned short int가 있으면 무조건 int나 unsigned int로 변환하여 처리하기 때문에 short int나 unsigned short int의 정수 상수가 필요하지 않게 된다.

※ MSB(Most Significant Bit)는 최상위 비트의 약자로 맨 왼쪽 비트를, LSB(Least Significant Bit)는 최하위 비트의 약자로 맨 오른쪽 비트를 의미하는데 각 비트 멤버는 하위(오른쪽) 비트부터 채워져 간다.

7   6   5   4   3   2   1   0       → 비트 번호 맨 오른쪽이 0

27   26   25   24   23   22   21   20      → 각 비트의 값

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

□ 0  □ 1  □ 1  □ 0  □ 0  □ 1  □ 0  □     (2진수 0110010을 예로 듦)

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

2    1      4    2    1      4    2    1

□□□□□□□□□□□ □□□□□□□□□□□□□□□□ □□□□□□□□□□□□□□□□

□ 0  □ 1  □ □ 1  □ 0  □ 1  □ □ 0  □ 1  □ 0  □

□□□□□□□□□□□ □□□□□□□□□□□□□□□□ □□□□□□□□□□□□□□□□

□□□□□□□     □□□□□□□□□□□□     □□□□□□□□□□□□

1              5                2     → 8진수로 152 (0152)

8    4    2    1      8    4    2    1

□□□□□□□□□□□□□□□□□□□□□ □□□□□□□□□□□□□□□□□□□□□

□ 0  □ 1  □ 1  □ 0  □ □ 1  □ 0  □ 1  □ 0  □

□□□□□□□□□□□□□□□□□□□□□ □□□□□□□□□□□□□□□□□□□□□

□□□□□□□□□□□□□□□□□     □□□□□□□□□□□□□□□□□

6                      A         → 16진수로 6A (0x6A)

⑵ 실수 데이터 유형의 상수

소숫점 형태

지수 형태

지수 형태는 가수와 지수 형태로 나타내는 것으로 가수E지수 와 같은 형태로 실수를 나타낸다. 이것의 의미는 가수*10지수인데 이때 가수는 소숫점 형태의 실수이며 지수는 정수이고 E는 소문자 e를 사용해도 된다.

지금까지의 모든 실수 상수들은 double 유형의 데이터가 되며 내부적으로 8byte를 차지하게 된다(정확도를 고려).

0.127,  -23.,  -3.14,  72.09,  +31.04,  +0.0123,  .12

0.127E10(0.127*1010),  -12.35e-3(-12.35*10-3),  -.34E+3(-0.34*103)

float 유형의 실수 상수를 사용하고자 할 때에는 끝에 f를 붙여주면 된다.

0.123f,  -.45F,  23.45f,  0.25E+5f,  -123.6E-5F,  .0f

long double 유형의 실수 상수를 사용하고자 할 때에는 끝에 l이나 L을 붙이면 된다.

0.123456789012345678l,  -123.45678901234L,  23.57800000012307E-4l

⑶ 문자 데이터 유형의 상수

'A',  '0',  'a',  '*',  '?',  '!',  '9',  ','

원하는 문자를 단일 따옴표로 싸면 되고 □'□와 □'□사이에는 반드시 하나의 문자만 와야 한다.

'AB'와 같이 사용할 수 없으며 이 경우 컴파일할 때 에러가 발생하게 된다.

문자들 가운데는 단일 따옴표 안에 나타내기 곤란한 문자들이 있다. 예를 들어 단일 따옴표 자체를 나타내고자 할 때 이를 '''로 나타내면 " 다음에 '가 하나 더 붙은 것인지(이 경우 에러가 된다) 아니면 ' 자체를 나타낸 것인지 알수 없게 된다. 또 문자 가운데는 키보드로 입력이 곤란한 문자들이 있다. 각종 제어문자나 <Enter> 문자, <Tab> 문자, 그래픽 문자들은 키보드로 입력하기가 곤란하기 때문에 위와 같은 방법으로는 나타내기 힘들게 된다.

C에서는 이를 위해 '\'로 시작하는 표현 방법을 제공하며 이를 Escape Sequence라고 부른다.

Escape Sequence	해당 ASCII 문자	이 름
\0 \a \b \f \n \r \t \v \\ \' \" \8진수 \x(X)16진수	NULL BEL BS FF LF CR HT VT \ ' " 8진수 코드 16진수 코드	NULL 문자 Bell 문자(삑하는 벨소리를 내게 만듦) Backspace(왼쪽에 있는 글자 하나를 지우는 문자) Formfeed(프린터의 페이지 넘김 문자) Linefeed(Newline-다음 라인으로 이동) Carriage Return(그 줄 맨 처음으로 이동) Horizontal Tab(탭문자) Vertical Tab(수직 탭) Backslash('\' 그 자체) 단일 따옴표 이중 따옴표 8진수가 나타내는 코드의 문자 16진수가 나타내는 코드의 문자

Escape Sequence도 한 문자이기 때문에 단일 따옴표로 싸야 한다.

'\0'는 ASCII 코드 값이 0인 문자로, 이 문자는 화면에 출력시켜도 아무것도 출력되지 않는다. 그래서 NULL 문자라고 부르며 C에서는 이 문자가 매우 중요하게 사용되는데 스트링의 끝을 나타내는 문자로 사용한다.

※ 탭 문자를 출력한 후 출력을 내보내게 되면 그 탭 위치에 출력되게 된다. 탭 위치는 보통 사용하는 시스템마다 다르지만 일반적으로 8의 배수에 위치하게 된다. 예를 들어 첫번째 탭 위치는 8칸을 띄우고 9번째 자리가 되며, 두번째 탭은 16칸을 띄우고 17번째 자리가 된다.

탭을 출력한 후 출력을 내보내게 되면, 지금까지 출력된 위치에서 오른쪽으로 가장 가까운 위치에 있는 탭의 위치로 출력이 나가게 된다. 예를 들어 "Press any key"라고 출력한 후 탭을 출력한 다음 "ABC"라고 출력하면 다음과 같이 출력되게 된다.

1         2         3         4         5

12345678901234567890123456789012345678901234567890  → 칸의 위치

t       t       t       t       t       t   → 탭의 위치

Press any key   ABC  → 가장 가까운 탭의 위치

□□□□□□□□□□ 탭을 출력

⑷ 문자열 상수

스트링은 여러개의 문자들을 의미한다. 그래서 문자열이라고 하기도 하는데 C에서는 이를 기본 데이터 유형으로 제공하지 않고 있다(C에서 스트링은 문자 유형의 배열로 처리하고 있다).

이중 따옴표로 싸면 스트링 상수가 된다.

"test",  "This is string",  "123",  "Computer",  "Hello, world !\n"

printf(" Warning !!\a\n");

printf 다음의 괄호 안에 있는 것이 바로 스트링 상수라는 것은 알 수 있다.

스트링 상수 내에서 Escape Sequence 형태의 문자를 마음대로 사용할 수 있다. Warning !!이라고 출력시키고 삑하는 벨소리를 낸다음 다음 줄로 이동한다.

※ 스트링 상수와 문자상수

위의 예는 메모리상에 다음과 같이 배열된다.

W	a	r	n	i	n	g		!	!	\a	\n	\0

C에서는 문자열의 마지막에 '\0'이 들어가 문자열의 길이는 실제보다 하나 더 커진다는 것을 알 수 있다.

스트링 상수의 내용이 길 경우에는 여러 줄로 나누어 사용할 수도 있다.

"This is an example of "

"a very long string, "

"which seems to be composed of three lines."

#include <stdio.h>

main() {

printf("\aBell \n");                  /*  삑 소리를 나게 한다  */

printf("double quote \"\n");          /*  큰 따옴표의 출력  */

printf("back slash \\\n");            /*  역 슬래쉬  */

printf("graphic character \xDB\n");   /*  그래픽 문자 표시   */

printf("abcdef\b\b\b\b");

printf("    ");

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Bell

double quote "

back slash \

graphic character □

ab    _

다섯번째 printf는 abcdef를 화면에 표시한 후 백스페이스를 네번 출력하는데 이는 실제로 키보드에서 백스페이스를 누를 때와는 달리 글자를 지우지는 않고 단지 뒤로 이동만을 하여 c의 위치에 커서가 위치하게 된다. 그리고 마지막의 공백이 네개인 printf문을 만나서 cdef가 지워지고 화면에는 ab만이 남게 된다.

< 변수의 선언 >

변수란 값이 변하는 수로, 한 순간에 하나의 데이터 값을 가질 수 있고 이 값을 다른 값으로 마음대로 바꿀 수 있는 것을 의미하는데, 모든 변수는 반드시 이름이 붙어 있도록 되어 있다. 따라서 C 프로그램 내에서 변수를 사용하려면 먼저 이름을 지어야 하는데 프로그램 내에서 사용하는 이런 이름들을 식별자 또는 명칭(identifier)이라고 한다. 이런 명칭은 변수 뿐만이 아니라 새로운 함수를 정의하거나 새로운 데이터 유형을 정의할 때에도 필요한데 C에서는 모든 명칭을 다음과 같은 규칙에 맞게 만들도록 규정하고 있다.

① 모든 명칭은 영문자 소문자와 대문자, 숫자 그리고 밑줄 문자('_')만 사용하여 만든다(대소문     자는 구별한다).

② 명칭은 숫자로 시작해서는 안된다.

③ 예약어는 명칭으로 사용할 수 없다.

예약어(reserved word)는 키워드라고 하는 것으로 미리 C언어에서 그 의미를 규정해 놓은 이름들로 다음과 같다.

_asm(MSC,TC)    auto           _based(MSC)        break               case

_cdecl(MSC,TC)  cdecl(TC)      char               const               continue

_cs(TC)         default        do                 double              _ds(TC)

else            _emit(MSC)     enum               _es(TC)             _export(MSC,TC)

extern          _far(MSC,TC)   far(TC)            _fastcall(MSC,TC)   float

for             _fortran(MSC)  goto               _huge(MSC,TC)       huge(TC)

if              int            _interrupt(MSC,TC) interrupt(TC)       _loadds(MSC,TC)

long            _near(MSC,TC)  near(TC)            _pascal(MSC,TC)    pascal(TC)

register        return         _saveregs(MSC,TC)   _seg(TC)           _segment(MSC)

_segname(MSC)   _self(MSC)     short               signed             sizeof

_ss(TC)         static         struct              switch             typedef

union           unsigned       void                while              asm(TC)

위에서 MSC로 표시된 것은 MSC내에서만 사용되는 키워드들이며 TC로 표시된 것은 TC내에서만, 그리고 아무 표시도 없는 것은 Unix와 MSC, TC 모두에서 사용되는 키워드들이다.

명칭의 길이에는 원칙적으로 제한이 없으나 컴파일러마다 효율성을 위해 그 길이를 제한하는 경우가 많은데 MSC의 경우에는 처음 31자까지만 받아들이고 TC의 경우에는 앞 32자까지만 받아들인다. 반면에 Unix의 경우에는 길이에 제한이 없다.

변수 선언은 C 프로그램 내에서 다른 문장들보다 앞서 맨 처음에 와야 한다. 따라서 다음 프로그램은 잘못되었는데

main() {

printf("Begin of variable declaration.\n");

int i;          /* 변수를 printf 다음에 선언 */

＞

}

변수 선언인 int i; 앞에 printf문이 있기 때문이다.

< 초기값의 지정 >

int i = 100 * 37 - 25 * 15;

int j = 12.76;                /*  j = 12   */

char c = 33;                  /*  c = '!'  */

short int small = 100000L;

float pi = 3.141592F;

const int num = 255;

int x, y, z;

x = y = z = 0;

변수 i의 초기값으로 100*37-25*17을 지정하였는데 실제로 초기값으로 수식을 사용하여도 상관 없다. 변수 j와 c는 다른 데이터 유형으로 초기값을 주었는데 다른 데이터 유형으로 초기값을 주면 변수의 데이터 유형으로 형 변환(type conversion)이 일어나게 되며 컴파일할 때 전혀 에러가 발생하지 않는다. 그러나 small의 경우에는 문제가 발생하게 된다. small은 short int 유형이기 때문에 최고 32767까지 밖에는 값을 가질 수 없는데, 이 변수에 이보다 훨씬 큰 10만을 초기값으로 주었기 때문에 당연히 문제가 발생한다. 그러나 컴파일시에 에러가 발생하지 않으며 small에는 전혀 엉뚱한 값이 들어간다. C에서는 작은 크기의 데이터를 큰 크기에 넣을 때에는 별 문제가 없지만 큰 크기의 데이터를 작은 크기에 넣을 때에는 문제가 발생할 수 있다.

pi는 실수 값 3.141592를 초기값으로 갖게 된다. 3.141592 다음의 F는 이 상수가 float 유형의 상수임을 나타내는 것이며 이것이 없으면 이는 double 유형의 상수가 되어 pi 변수의 데이터 유형과 다르게 되므로 주의해야 한다.

num은 데이터형 앞에 const라는 수식어를 쓴 것으로 num은 원칙적으로 그 초기값을 변경시키지 못한다. const int num = 255 와 같이 선언과 동시에 초기화를 한 다음 num의 값을 변경시키면 에러가 발생 한다.

마지막의 x, y, z의 값을 모두 0으로 만드는데 대입의 순서는 먼저 z에 0이 대입되고 y에 0이 끝으로 x에 0이 대입된다. 그러나, 이러한 대입방법은 변수의 선언부에서 사용해서는 안된다. 즉 int x = y = z = 0; 과 같은 문장은 잘못된 것이다. 굳이 하려면, int x = 0, y = 0, x = 0; 이라고 해야 한다.

Ⅲ. 기본 입출력

< 기본 출력 >

⑴ 정수 데이터 유형의 출력

포 맷	의 미
%Nd %No %Nx %Nu %Nhd %Nho %Nhx %Nhu %Nld %Nlo %Nlx %Nlu	int 유형의 데이터를 N칸에 맞게 10진수로 출력 unsigned int 유형의 데이터를 N칸에 맞게 8진수로 출력 unsigned int 유형의 데이터를 N칸에 맞게 16진수로 출력 unsigned int 유형의 데이터를 N칸에 맞게 10진수로 출력 short int 유형의 데이터를 N칸에 맞게 10진수로 출력 unsigned short int 유형의 데이터를 N칸에 맞게 8진수로 출력 unsigned short int 유형의 데이터를 N칸에 맞게 16진수로 출력 unsigned short int 유형의 데이터를 N칸에 맞게 10진수로 출력 long int 유형의 데이터를 N칸에 맞게 10진수로 출력 unsigned long int 유형의 데이터를 N칸에 맞게 8진수로 출력 unsigned long int 유형의 데이터를 N칸에 맞게 16진수로 출력 unsigned long int 유형의 데이터를 N칸에 맞게 10진수로 출력

int i = 100;

printf("i = %5d.\n",i);

□□□□□                지정한 칸수 보다 출력될 숫자의 길이가 작은 경우에는 위의 예처럼

i =  100.              지정한 칸수의 오른쪽에 딱 붙게 출력되고(이를 오른쪽으로 정렬된 출                           력이라고 한다) 남는 것은 빈칸으로 채워진다. 그리고 왼쪽에 붙이고     printf("i = %-7d.\n",i);  싶을 때에는 칸수 앞에 '-'를 붙이면 된다. 그러면 왼쪽에 딱 붙게 출        □□□□□□□            력되며(이를 왼쪽으로 정렬된 출력이라고 한다) 남는 것은 역시 빈칸

i = 100    .            으로 채워지게 된다.

int i = 12345;

printf("i = %3d.\n",i);     지정한 칸수보다 출력시킬 정수의 값이 더 큰 경우 C에서는 지정한

□□□□□               칸수를 무시하고 출력할 데이터의 칸수에 딱맞게 데이터를 출력하게

i = 12345.               된다.

int i = 35, j = 43;         C에서 8진수를 사용할 때는 앞에 0을 붙였지만 출력 결과에는 0이

printf("i = %o.\t",i);      붙지 않는다. 따라서 10진수로 출력한 것과 외관상 차이가 없기 때

printf("j = %x.\n",j);      문에 주의해야 한다. 마찬가지로 16진수로 출력하고자 할 때에도 앞

에 0x나 0X를 붙였지만 %x로 출력할 때에는 이것이 나오지 않으므

i = 43.       j = 2b.     로 주의하여야 한다.

main() {

int i = 100;

printf("i = %d, %o, %x\n",i,i,i);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

i = 100, 144, 64

printf의 ".........."(이를 포맷 스트링이라고 한다) 내의 포맷 순서와 변수의 순서는 반드시 일치하여야 하며 printf는 이같은 가정하에 변수의 값을 출력하고 있다.

main() {

int i = 10, j = 2;

short int s = 12;

long int l = 10000L;

unsigned u = 32000u;

printf("i = %5d\nj = %5d\ns = %5hd\nl = %5ld\nu = %5u\n",i,j,s,l,u);

printf("i + 1 = %d, j - 3 = %d, s * 20 = %hd, l - 5000 * 2 = %ld, u - 27000 = %u\n",

i+1,j-3,s*20,l-5000L*2L,u-27000ul);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

i =    10

j =     2

s =    12

l = 10000

u = 32000

i + 1 = 11, j - 3 = -1, s * 20 = 240, l - 5000 * 2 = 0, u - 27000 = 5000

int i = 10, j = 20, k = 30;

printf("i = %d, j = %d\n",i,j,k);

위의 printf 문에서는 포맷의 갯수보다 더 많은 변수를 지정하고 있다. 따라서 i와 j의 값은 출력을 할 수 있지만 k의 값은 출력할 수 있는 방법이 없다. 그런데 컴파일할 때나 수행할 때나 전혀에러가 발생하지 않는다. 그리고 다음과 같은 출력을 내게 된다.

i = 10, j = 20

다음과 같이 출력할 변수가 모자랄 경우에는 어떻게 되는가?

int i = 10, j = 20, k = 30;

printf("i = %d, j = %d, k = %d\n",i,j);

이 경우에도 컴파일할 때에는 에러가 발생하지 않는다. 그러나 수행시에는 다음과 같이 엉뚱한 결과가 나오게 된다.

(Unix) i = 10, j = 20, k = 128

(MSC) i = 10, j = 20, k = 278

(TC)  i = 10, j = 20, k = 1438

즉 넘칠 때는 괜찮고 부족할 때는 에러가 나는 것은 printf의 처리 방법 때문인데 printf는 실제로 포맷 스트링만을 본다. 왼쪽에서 오른쪽으로 훑어 가면서 처리하다가 %d와 같은 포맷이 나오면 그때서야 오른쪽에 주어진 데이터의 값을 사용하여 출력하게 된다. 따라서 주어진 데이터가 많은 경우에는 전혀 문제가 발생하지 않는데 포맷 스트링을 처리할 만한 충분한 데이터가 주어져 있기 때문이다. 출력되지 못한 데이터는 아예 건드리지도 않으니까 아무 문제가 발생하지 않는다.

그러나 모자랄 때에는 항상 있을 것으로 간주하고 무조건 갖고 온다. 따라서 엉뚱한 메모리의 값을 데이터로 이용하기 때문에 위와 같이 엉뚱한 결과가 나오게 된다.

⑵ 실수 데이터 유형의 출력

포 맷	의 미
%N.Mf %N.Me %N.Mlf %N.Mle %N.MLf %N.MLe	float 유형의 데이터를 N칸에 맞게 소숫점 이하 M자리를 소수점 형태로 출력 float 유형의 데이터를 N칸에 맞게 소숫점 이하 M자리를 지수 형태로 출력 double 유형의 데이터를 N칸에 맞게 소숫점 이하 M자리를 소숫점 형태로 출력 double 유형의 데이터를 N칸에 맞게 소숫점 이하 M자리를 지수 형태로 출력 long double 유형의 데이터를 N칸에 맞게 소숫점 이하 M자리를 소숫점 형태로 출력 long double 유형의 데이터를 N칸에 맞게 소숫점 이하 M자리를 지수 형태로 출력

여기서 N은 출력할 전체 칸수를 의미하며 M은 소숫점 자리수를 의미한다. 따라서 N은 최소한 M+1(소숫점도 한자리이다) 보다는 커야 한다. N과 M 중 어느 하나만 지정해도 상관 없으며 출력할 데이터의 크기가 지정한 N보다 큰 경우에는 정수 데이터 유형의 경우와 같이 N을 무시하고 내부적으로 정한 규칙에 따라 출력하게 된다. 또 M보다 소숫점 이하가 더 있을 경우에는 M 바로 다음(M+1) 자리에서 반올림하여 출력하게 되며 남을 경우에는 0으로 채워져 출력된다.

main() {

float f = 1.789f;

double g = 32.789345678;

long double a = 12.345e-100L;

printf("f = %f,  g = %lf,  a = %Lf\n",f,g,a);

printf("f = %e,  g = %le,  a = %Le\n",f,g,a);

printf("f = %E,  g = %lE,  a = %LE\n",f,g,a);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

f = 1.789000,  g = 32.789346,  a = 0.000000

f = 1.78900e+00,  g = 3.27893e+01,  a = 1.23450e-99

f = 1.78900E+00,  g = 3.27893E+01,  a = 1.23450E-99

첫번째 printf 문은 실수 변수의 값을 소수점 형태로 형태로 출력하고 있는데 f와 g는 주어진 값이 비슷하게 나오지만 a는 값이 너무 작기 때문에 0으로 출력된다. 그 다음 printf 문에서는 지수 형태로 출력하기 때문에 제대로 출력된다. 마지막 printf 문은 □%e□ 대신에 □%E□ 를 사용한 것으로 이 때 출력은 소문자 'e' 대신에 대문자 'E'가 나오게 된다.

⑶ 문자 데이터 유형의 출력

포 맷	의 미
%c %d %u	char 유형이나 unsigned char 유형의 데이터를 문자로 출력 char 유형의 데이터를 10진수의 숫자로 출력 unsigned char 유형의 데이터를 10진수의 숫자로 출력

unsigned char u = 176;     unsigned char의 경우에는 문자 보다는 숫자의 개념이 강하다. 확

printf("u = %c\n",u);    장된 ASCII 코드에서의 그래픽 문자나 한글 코드와 같이 8bit를                                  다 사용하는 경우에는 unsigned char를 사용하며 이 경우 %c로 출    u = □                   력시키면 원하는 문자를 얻을 수 있다. 반면에 이를 숫자로 출력하                               고자 할 때에는 %d 대신 %u를 사용하면 된다.

main() {

char c1 = 'A';

char c2 = 'B';

char c3 = 'C';

printf("c1 = %c,  %d\n",c1,c1);

printf("c2 = %c,  %d\n",c2,c2);

printf("c3 = %c,  %d\n",c3,c3);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

c1 = A,  65

c2 = B,  66

c3 = C,  67

%o나 %x를 사용하면 8진수나 16진수로 된 ASCII 코드 값을 얻을 수 있다. 반면에 정 반대로 char 변수에 ASCII 코드 값을 주고 %c를 사용하여 해당 문자로 출력할 수도 있다.

main() {

char c = 65;

char d = 'z';

unsigned char u = 179;

printf("c = %c,  d = %d,  u = %c\n",c,d,u);

printf("c + 10 = %c,  d - 5 = %c, u + 1 = %c\n",c+10,d-5,u+1);

printf("%c %c %c %c %c %c\n",'a'+1,'z'-3,'0'+2,'9'-4,'F'+32,'q'-32);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

c = A,  d = 122,  u = □

c + 10 = K,  d - 5 = u,  u + 1 = □

b w 2 5 f Q

char 유형이 ASCII 코드 값을 갖고 있기 때문에 여기에 정수값을 더하면 다른 문자가 된다. 예를 들어 'A'는 65이고 'a'는 97이기 때문에 'A'+32는 'a'가 된다. 또 ASCII 코드의 특성상 'A' 다음이 'B'이기 때문에 'B'+32는 'b'가 되며 따라서 대문자에 32를 더하면 소문자가 된다. 마찬가지로 '0'+4는 '5'가 되며 '9'-1은 '8'이 된다.

⑷ putchar를 이용한 문자 데이터 유형의 출력

putchar('A');

putchar 다음에는 반드시 한 문자만 와야 하며 그 문자가 화면에 출력된다.

putchar는 printf와는 달리 1문자 밖에는 출력시킬 수가 없다. 따라서 A를 출력시키고 다음 줄로 넘어가려면 다음과 같이 두개의 putchar를 사용하여야 한다.

putchar('A');

putchar('\n');

문자 단위로 여러 개를 출력해야 할 경우에는 printf보다 putchar가 더 빠르기 때문에 많이 사용되고 있다.

#include <stdio.h>

main() {

char c = 'a';

char d = '5';

putchar(c);

putchar(c+2);

putchar(c+5);

putchar(' ');

putchar(d);

putchar(d-3);

putchar(d+3);

putchar(c-32);

putchar(107);

putchar('\n');

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

acf 528Ak

※ DOS 에서 그래픽 문자의 출력(ASCII 코드표 참조)

현재 PC에서 사용할 수 있는 문자들을 ASCII 문자라고 하는데, 이 중 키보드의 자판에는 없는 문자를 그래픽 문자라고 한다. 예를 들어 □±□ 같은 문자는 키보드 자판상에는 없지만 ASCII 표에는 있기 때문에 사용할 수 있다.

이들 그래픽 문자를 출력할 때에는 우선 ASCII 표에서 원하는 그래픽 문자를 찾아야 한다. 그리고 그 옆의 숫자를 본다. 그러면 두 개의 숫자가 있는 것을 볼 수 있는데, 이 중 □Dec□라고 쓰여진 곳의 숫자를 찾아 이를 다음과 같이 putchar를 사용하여 출력하면 된다.

putchar(Dec에 해당하는 값);

예를 들어 □±□를 출력한다고 할 경우에 이를 ASCII 표에서 찾으면 그 옆에 숫자가 241(이는 10진수이다)과 F1(이는 16진수이다)이 있는데, 이 중 Dec 밑의 숫자가 241이므로 다음과 같이 출력하면 된다.

putchar(241);

위를 보면 241이 단일 따옴표로 둘러 싸이지 않은 것을 볼 수 있다. 즉 putchar('241')이 아닌 putchar(241)이다.

printf를 사용할 수도 있는데 이때에는 □Hex□ 밑의 값을 사용한다.

printf("\xHex에 해당하는 값");

□±□의 Hex 값이 F1이므로, □±□를 출력하고자 할 때에는 다음과 같이 하면 된다.

main() {

printf("The result is \xF1 3\n");

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

The result is ± 3

printf를 사용하지 않고 putchar를 사용할 경우에는 다음과 같이 하면 된다.

#include <stdio.h>

main() {

printf("The result is ");

putchar(241);

printf(" 3\n");

}

putchar의 경우에도 '\xF1'을 사용할 수 있는데, 이때에는 putchar('\xF1'); 를 사용하면 된다.

#include <stdio.h>

main() {

printf("The result is ");

putchar('\xF1');

printf(" 3\n");

}

DOS상에서 키보드 자판상에는 없지만 ASCII 표에는 있는 문자를 출력하고자 할 때에는 이를 표에서 찾아 10진수의 값은 putchar로, 16진수의 값은 printf와 putchar로 출력하면 된다.

※ 한글 DOS 에서 그래픽 문자의 출력(KS 완성형 표 참조)

main() {

printf("\xA5\xE1 = 0.3\n");

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

□ = 0.3

영문 DOS에서 □□□를 출력할 때 ASCII 표에서 □□□를 찾아보면 Dec가 224이므로 putchar(224); 를 사용하면 된다. 그런데 한글 DOS의 경우에는 이와 같이 할 수가 없다. 이는 한글 DOS가 사용하는 글자들이 영문 DOS와는 다르기 때문인데, 한글 DOS에서 사용하는 글자를 KS 완성형이라고 한다.

한글 DOS에서 그래픽 문자를 출력할 때에는 원하는 문자를 KS 완성형 표에서 찾아야 한다. 그리고 그 문자의 왼쪽에 있는 값과 그 위에 있는 값을 찾으면 되는데, □□□의 경우 표에서 찾으면 그 왼쪽의 값이 A5-E0이고(16진수의 값이다) 그 위의 값이 1인 것(16진수의 값이다)을 볼 수 있는데, 우선 A5-E0의 A5를 출력한 후 A5-E0의 'E'와 위의 값 □1□을 합해 'E1'을 만들어 이를 다음과 같이 출력하면 된다.

putchar('\xA5');

putchar('\xE1');

즉 한글 DOS 상에서 그래픽 문자를 출력하고자 할 때에는 putchar를 2번 사용해야 한다. 반면에 printf를 사용할 때에는 다음과 같이 하면 된다.

printf("\xA5\xE1");

마찬가지로 □±□를 출력하고자 할 때에는 □±□가 왼쪽의 값이 A1-B0이고 위의 값이 E이므로 다음과 같이 출력하면 된다.

putchar('\xA1');

putchar('\xBE');

또는

printf("\xA1\xBE");

main() {            /*  영문 DOS인 경우  */

printf("\xDA\xC4\xC4\xC4\xC4\xC4\xC4\xC4\xC4\xBF\n");

printf("\xB3 OUTPUT \xB3\n");

printf("\xC0\xC4\xC4\xC4\xC4\xC4\xC4\xC4\xC4\xD9\n");

}

main() {           /*  한글 DOS인 경우  */

printf("\xA6\xA3\xA6\xA1\xA6\xA1\xA6\xA1\xA6\xA4\n");

printf("\xA6\xA2OUTPUT\xA6\xA2\n");

printf("\xA6\xA6\xA6\xA1\xA6\xA1\xA6\xA1\xA6\xA5\n");

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

□□□□□□□□□□

□ OUTPUT □

□□□□□□□□□□

< 기본 입력 >

⑴ 정수 데이터 유형의 입력

데이터를 읽어들일 때에는 기본적으로 scanf 라는 것을 사용해야 한다. 이의 사용법은 다음과 같다.

scanf("포맷 스트링",&변수);

scanf는 printf와는 달리 변수가 반드시 있어야 한다. 이 변수는 읽어들인 데이터를 값으로 가질 변수이다. 포맷 스트링은 어떤 데이터를 읽어들일 것인가를 나타내는데 int 데이터 유형의 경우에는 %d를 사용한다.

printf에서는 %로 시작하는 포맷 외에 다른 것을 포맷 스트링에 넣으면 그대로 출력되어 나왔지만 scanf에서는 포맷 문자외에는 사용해서는 안된다.

위의 scanf를 보면 변수 앞에 '&'가 붙어 있는 것을 볼 수 있다. 이는 반드시 있어야 하는 것으로 scanf에서는 변수 앞에 반드시 '&'가 있어야 한다.

main() {

int i;

printf("Input the value of i(Integer): ");

scanf("%d",&i);

printf("i = %d\n",i);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input the value of i(Integer): 100 □

i = 100

프로그램을 수행하다가 scanf를 만나게 되면 입력을 받아들이기 위해 수행이 멈추게 된다. 이 때 원하는 값을 입력하면 이를 읽어들여 해당 변수의 값으로 저장하게 되며 여백 문자는 신경쓰지 않고 처음 입력된 숫자부터 칸수를 계산한다.  □ □100□와 같이 앞에 두칸 띄우고 입력해도 위와 같은 결과를 낳게 된다.

scanf에서 %o를 사용한 경우에는 8진수 형태로 데이터를 입력하여야 하며(이때 맨 앞에 '0'은 붙이지 않아도 된다) %x의 경우에는 16진수 형태로 입력하여야 한다(이때에도 앞에 '0x'는 붙이지 말아야 한다).

main() {

int i, j;

printf("Input the value of i(Octal number): ");

scanf("%o",&i);

printf("Input the value of j(Hexadecimal number): ");

scanf("%x",&j);

printf("i = %o,  j = %x\n",i,j);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input the value of i(Octal number): 1234 □

Input the value of j(Hexadecimal number): ABCD □

i = 1234,  j = abcd

지금까지의 scanf에서는 하나의 변수값만 읽어들였다. 그런데 printf와 같이 하나의 scanf 문으로 여러 개의 변수값을 읽어들일 수 있는데 이때에는 다음과 같이 여러 개의 포맷과 변수를 사용해야 한다.

scanf("%F1.....%Fn",&변수1.....&변수n);

이때 포맷과 변수들의 순서는 일치하여야 하며 Fi는 &변수i의 포맷이 되어야 한다.

main() {

unsigned int u;

long int l;

unsigned long int u1, u2, u3;

printf("Input u, l, u1, u2, u3 : ");

scanf("%u%ld%lu%lo%lx",&u,&l,&u1,&u2,&u3);

printf("u = %u,  l = %ld,  u1 = %lu,  u2 = %lo,  u3 = %lx\n",u,l,u1,u2,u3);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input u, l, u1, u2, u3 : 43200 2015700000 3896000000 1234567123 FFFFFFFF □

u = 43200,  l = 2015700000,  u1 = 3896000000,  u2 = 1234567123,  u3 = ffffffff

위의 수행 결과에서는 한 줄에 데이터를 모두 입력하였는데 꼭 이렇게 하지 않아도 되며 다음과 같이 한줄에 하나씩 입력해도 되고

Input u, l, u1, u2, u3 : 43200 □

2015700000 □

3896000000 □

1234567123 □

FFFFFFFF □

u = 43200,  l = 2015700000,  u1 = 3896000000,  u2 = 1234567123,  u3 = ffffffff

다음과 같이 제멋대로 입력해도 된다. 즉 전체적으로 데이터가 5개만 들어오면 된다. 그리고 위의 scanf를 보면 포맷들이 바로 붙어있는 것을 볼 수 있는데 사이를 한칸씩 띄워도 된다. 그러나 엄격한 의미에서 보면 %d%d와 같이 붙인 것과 %d %d와 같이 띈 것은 차이가 있는데 현재 상태에서는 %d%d와 같이 붙여 사용한다고 알아두기만 하면 된다.

Input u, l, u1, u2, u3 : 43200 □

2015700000  3896000000 1234567123 □

FFFFFFFF □

u = 43200,  l = 2015700000,  u1 = 3896000000,  u2 = 1234567123,  u3 = ffffffff

printf에서는 포맷 문자에 출력될 데이터의 전체 칸수를 %5d와 같이 지정할 수 있었는데 scanf의 경우에도 칸수를 지정할 수 있다. 그리고 이경우 읽어들일 숫자의 전체 길이를 의미하게 된다.

포 맷	의 미
%Nd %No %Nx %Nu %Nhd %Nho %Nhx %Nhu %Nld %Nlo %Nlx %Nlu	최대 N칸의 10진수 형태의 int 유형의 데이터를 입력 최대 N칸의 8진수 형태의 unsigned int 유형의 데이터를 입력 최대 N칸의 16진수 형태의 unsigned int 유형의 데이터를 입력 최대 N칸의 10진수 형태의 unsigned int 유형의 데이터를 입력 최대 N칸의 10진수 형태의 short int 유형의 데이터를 입력 최대 N칸의 8진수 형태의 unsigned short int 유형의 데이터를 입력 최대 N칸의 16진수 형태의 unsigned short int 유형의 데이터를 입력 최대 N칸의 10진수 형태의 unsigned short int 유형의 데이터를 입력 최대 N칸의 10진수 형태의 long int 유형의 데이터를 입력 최대 N칸의 8진수 형태의 unsigned long int 유형의 데이터를 입력 최대 N칸의 16진수 형태의 unsigned long int 유형의 데이터를 입력 최대 N칸의 10진수 형태의 unsigned long int 유형의 데이터를 입력

int i;

printf("Input i: ");         이 프로그램에서는 %3d로 i의 값을 읽어들이고 있다. 이는 3글자로

scanf("%3d",&i);         된 정수값을 읽어들이라는 의미인데, i의 값으로 3자리의 정수를 입

printf("i = %d\n",i);      력했으면 아무 문제가 없지만 다음과 같이 더 많이 입력했을 때에는                            3자리까지만 읽으라고 했기 때문에 123만 읽어들이고 나머지 45는

Input i: 12345 □        다음 scanf에서 읽어들이게 되는데 scanf가 없으므로 그냥 없어지게

i = 123                  된다

main() {

int i, j;

short int s;

unsigned long int u;

long int l;

printf("Input i, j, s, u, l: ");

scanf("%3d%5d%2hd%10lu%10ld",&i,&j,&s,&u,&l);

printf("i = %d,  j = %d,  s = %hd,  u = %lu,  l = %ld\n",i,j,s,u,l);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input i, j, s, u, l: 12345 67890-12345 □

i = 123,  j = 45,  s = 67,  u = 890,  l = -12345

첫번째 포맷인 %3d에 의해 i는 123을 값으로 갖게 된다. 그 다음이 %5d인데 남은 데이터가 45 67890-12345이므로 45에서 일단 끝나 j는 45를 값으로 갖게 된다. 그럼 67890-12345가 남았는데, 그 다음이 %2hd이므로 67을 s가 갖게 된다. 다음 포맷이 %10lu인데 남은 데이터가 890-12345이므로 10칸을 읽으려고 시도한다. 그런데 890 다음이 '-'이고 이는 숫자가 아니므로 일단 여기서 데이터가 끝났다고 간주하게 된다(scanf에서는 정수 데이터를 읽어들일 때 정수가 아닌 입력이 오면 데이터가 끝났다고 간주한다). 그래서 u는 890을 값으로 갖게 된다. 이제 마지막으로 남은게 %ld인데 남은 데이터가 -12345이므로 당연히 l은 -12345를 값으로 갖게 된다(맨 처음의 '-'는 바로 부호이다).

main() {

int i = 20, j = 40;

printf("Input i, j: ");

scanf("%d%d",&i,&j);

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

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input i, j: 123.456 789.012 □

i = 123,  j = 40

j의 값이 전혀 읽혀들여지지 않은 이유는 '.'은 정수 데이터의 한 부분이 아니기 때문에 일단 여기서 데이터가 끝났다고 간주한다. 그래서 i는 여기까지의 데이터인 123을 값으로 갖게 된다. j는 남은 데이터가 .456 789.012인데 시작이 '.'이고 이는 역시 정수 데이터가 아니므로 또 데이터가 끝났다고 보는 것이다. 그런데 이 경우 실제 읽어들인 데이터가 없으므로 당연히 j의 값은 읽혀들여지지 않게 된다. 그래서 j의 값은 초기값을 그대로 갖게 된다. 만약 그 다음에 또 scanf가 있어서 %d로 읽어들였다 해도 역시 같은 결과를 낳게 된다. 즉 '.'이 읽혀들여지지 않는한(이는 천상 문자 데이터나 실수 데이터로 읽어들여야 한다) 그 뒤의 데이터는 읽혀들여지지 않고 제자리에 있게 되는 것이다. 그런데 이때에도 전혀 에러가 발생하지 않기 때문에 주의해야 한다.

scanf에서  앞의 포맷의 숫자와 뒤의 변수의 갯수는 당연히 같아야 하지만 포맷이 모자랄 경우에는 printf의 경우와 같이 scanf도 포맷을 처리하면서 그 때마다 필요한 변수들을 사용하기 때문에 이 경우에는 아무 문제가 없다.

int i = 0, j = 0, k = 0;

printf("Input 3 integers: ");

scanf("%d%d",&i,&j,&k);

printf("i = %d,  j = %d,  k = %d\n",i,j,k);

scanf에서 %d는 2개인데 반해 변수는 3개가 지정되어 있다. 그러나 이를 컴파일하여 수행하면 컴파일할 때나 수행할 때나 전혀 에러가 발생하지 않으며 다음과 같은 출력이 나오게 된다.

Input 3 integer: 2 3 5 □

i = 2,  j = 3,  k = 0

위의 출력 결과를 보면 i와 j의 값만 읽혀 들여지고 k의 값은 읽혀들여지지 않았는데 이는 포맷이 2개만 지정되었기 때문이다. 반면에 포맷 보다 변수의 갯수가 부족할 때에는 printf와 같이 에러가 발생하게 된다.

int i = 0, j = 0, k = 0;

printf("Input 3 integers: ");

scanf("%d%d%d",&i,&j);

printf("i = %d,  j = %d,  k = %d\n",i,j,k);

따라서 위의 프로그램을 컴파일하면 컴파일시에는 에러가 발생하지 않지만 수행시에는 에러가 발생하게 된다.

main() {

int i, j;

printf("Input i, j: ");

scanf("%d%d",&i,&j);

printf("i = %d,  %o,  %x\n",i,i,i);

printf("j = %d,  %o,  %x\n",j,j,j);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input i, j, k: 20 200 □

i = 20,  24,  14

j = 200,  310,  c8

⑵ 실수 데이터 유형의 입력

실수 데이터의 경우도 역시 printf에서 실수 데이터를 출력시킬 때 사용했던 포맷을 그대로 사용하면 된다. 우선 float 유형의 데이터를 소숫점 형태로 출력시킬 때 사용했던 것이 %f 이므로 소숫점 형태의 float 데이터를 입력하고자 할 때에도 %f를 사용하면 된다.

main() {

float f;

printf("Input a floating number: ");

scanf("%f",&f);

printf("f = %.2f\n",f);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input a floating number: -123.755 □

f = -123.75

int 데이터 유형의 경우와 같이 실수 데이터 유형의 변수 잎에도 '&'를 반드시 붙여야 한다.

main() {

float f1, f2;

printf("Input f1, f2(Exponent): ");

scanf("%e%e",&f1,&f2);

printf("f1 = %e,  f2 = %e\n",f1,f2);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input f1, f2(Exponent): 1.234e20 -32.567E-7 □

f1 = 1.23400e+20,  f2 = -3.25670e-06

%e를 사용하면 지수 형태로 된 float 데이터 유형의 값을 읽어들이게 된다. 이때 사용자는 C에서 사용하는 지수 형태, 즉 3.2E-10과 같이 입력하여야 한다.

포 맷	의 미
%Nf %Ne %Nlf %Nle %NLf %NLe	최대 N칸의 소숫점 형태의 float 유형의 데이터를 입력 최대 N칸의 지수 형태의 float 유형의 데이터를 입력 최대 N칸의 소숫점 형태의 double 유형의 데이터를 입력 최대 N칸의 지수 형태의 double 유형의 데이터를 입력 최대 N칸의 소숫점 형태의 long double 유형의 데이터를 입력 최대 N칸의 지수 형태의 long double 유형의 데이터를 입력

printf의 경우에는 %N.Mf 와 같이 전체 자리수와 소숫점 이하 자리수를 지정할 수 있었는데 반해, scanf의 경우에는 입력할 데이터의 전체 크기만 지정할 수 있다. 즉 %10f 와 같이 지정할 수 있는데 이는 최대 10칸까지만 읽어서 이를 실수값으로 읽어들이라는 의미이다. 물론 입력한 데이터의 길이가 10칸 이내일 때에는 입력한 것만 읽혀들여진다(이는 정수 데이터 유형의 경우와 같다). 또 입력한 데이터가 10자리가 넘을 때에는 10자리까지만 읽혀지고 남은 것은 다음 데이터로 사용된다(이도 정수 데이터 유형의 경우와 동일하다).

main() {

float f1, f2;

double d;

long double l;

printf("Input f1, f2, d, l: ");

scanf("%3f%10f%5lf%3Le",&f1,&f2,&d,&l);

printf("f1 = %20.10f\n",f1);

printf("f2 = %20.10f\n",f2);

printf(" d = %20.10lf\n",d);

printf(" l = %20.10Lf\n",l);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input f1, f2, d, l: 1.2.345 678.21e56 □

f1 =                    1.2000000477

f2 =                    0.3449999988

d =                  678.2000000000

l =               100000.0000000000

첫번째 포맷이 %3f이므로 3글자를 우선 가져가게 된다. 따라서 f1의 값은 1.2가 된다('.'도 하나의 데이터이다). 남은 데이터가 .345 678.21e56인데 그 다음 포맷이 %10f이므로 10칸을 잡아야 하는데 데이터가 일단 .345로 끝나기 때문에 f2는 여기까지만 가지게 되며 따라서 f2의 값은 0.345를 값으로 갖는다. 이제 678.21e56이 남았는데 다음 포맷이 %5lf이므로 5칸의 데이터를 가져가 678.2를 d가 갖게 된다. 그 다음 남은 것이 1e56이고 또 마지막 포맷이 %3Le이므로 3칸을 가지면 1e5가 되어 l은 le5 = 100000을 값으로 갖게 된다. 마지막으로 남은 6은 사용하지 않고 버려진다.

main() {

float f1 = 1.2f, f2 = 2.4f;

printf("Input two float numbers: ");

scanf("%f%f",&f1,&f2);

printf("f1 = %f,  f2 = %f\n",f1,f2);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input two float numbers: 123 456 □

f1 = 123.000000,  f2 = 456.000000

실수형 데이터 대신 정수형 데이터를 입력해도 전혀 무리없이 잘 입력되는 것은 정수형 데이터를 표현하는데 사용되는 숫자가 모두 실수형 데이터를 표현하는데도 사용되기 때문이다.

main() {

float f = 1.2f;

printf("Input float number: ");

scanf("%e",&f);

printf("f = %f\n",f);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input float number: 12.345 □

f = 12.345000

%e에 소숫점 형태의 실수를 입력해도 제대로 입력이 되는 것을 알 수 있다. 같은 이유인데 소숫점 형태로 나타낼 때 사용하는 문자들을 지수 형태에서도 다 사용하기 때문이다.

main() {

float f = 1.2f;

printf("Input float number: ");

scanf("%f",&f);

printf("f = %f\n",f);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input float number: 7.8e3 □

f = 7800.000000

소숫점 형태로 입력하라고 했을 때 지수 형태를 입력해도 잘 입력되는데, 이로써 scanf 에서는 %f와 %e가 차이가 없음을 알 수 있다. 즉 어느 것이나 사용해도 되고 입력도 어느 것이나 해도 된다.

⑶ 문자 데이터 유형의 입력

문자 데이터 유형은 정수로써 사용할 수도 있고 또 문자 자체로도 사용할 수가 있다. 따라서 문자 데이터 유형을 입력할 때에도 해당 코드값을 정수 데이터 유형 형태로 읽어들일 수도 있고 또 문자 자체를 입력할 수도 있는데, 전자의 경우에는 정수 데이터 유형의 값을 읽어들이는 방법을 사용하면 된다. 그러나 문자 데이터 유형의 입력이라면 보통 후자를 의미하는데 문자를 값으로 갖는 변수에는 char와 unsigned char가 있다. 읽어들일 문자가 키보드 자판에 있는 문자인 경우에는(한글은 제외) char 변수를 사용하면 되고, 그래픽 문자나 한글인 경우에는 unsigned char 변수를 사용하면 된다.

main() {

unsigned char c1, c2, c3, c4;

printf("한글 두 글자를 입력하시오: ");

scanf("%c%c%c%c",&c1,&c2,&c3,&c4);

printf("당신이 입력한 글자는: %c%c%c%c\n",c1,c2,c3,c4);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

한글 두 글자를 입력하시오: 이오 □

당신이 이력한 글자는: 이오

한글 1자를 읽어들이기 위해서는 unsigned char 변수 2개가 필요하다. 키보드상에서 입력할 때는 한글자이지만 실제로 입력되는 것은 문자 2개가 된다.

main() {

unsigned char c;

printf("Input a graphic character: ");

scanf("%c",&c);

printf("You typed: %c\n",c);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input a graphic character: ± □

You typed: ±

그래픽 문자는 키보드 자판상에는 없고 ASCII 코드표에 있는 문자를 의미한다. 사용자가 그래픽 문자를 입력하기 위해서는 우선 입력할 문자를 ASCII 표에서 찾은 다음, 이의 Dec 밑의 값을 찾은후, 그 값을 <Alt> 키를 누른 상태에서 해당 숫자를 키보드 숫자 패드에 있는 숫자들로 누르면 바로 그 그래픽 문자가 입력되게 된다. 예를 들어 '±'를 입력하고자 할 때에 이를 ASCII 표에서 찾으면 Dec의 값이 241인데, <Alt>키를 누른 상태에서 키보드 숫자 패드에 있는 숫자를 2→4→1 의 순서로 누르면 화면에 '±'가 출력된다.

즉 char나 unsigned char 값을 읽어들일 때에는 다같이 '%c' 포맷을 사용하면 된다.

scanf에서는 printf와는 달리 '%5c'와 같이 칸수를 지정하는 것이 의미가 없다. 이는 항상 한 글자만 읽어들이기 때문이다.

포 맷	의 미
%c	한 문자를 입력

%c를 이용하여 하나의 문자를 입력할 때 주의해야 할 사항이 있다. 그것은 일단 <Enter> 키를 입력해야만 입력이 시작된다는 것이고 또 <Enter> 키 자체도 하나의 문자라는 것이다.

main() {

char c1, c2;

printf("Input first character: ");

scanf("%c",&c1);

printf("Input second character: ");

scanf("%c",&c2);

printf("c1 = %d %c,  c2 = %d %c\n",c1,c1,c2,c2);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input first character: A □

Input second character: c1 = 65 A,  c2 = 10

위를 보면 A를 입력하고 <Enter> 키를 누르면 그 다음 문자는 입력할 틈도 없이 바로 출력이 나오는 것을 알 수 있는데 이는 입력한 문자가 A와 <Enter>로 두 글자이기 때문에 필요한 문자를 다 읽어들여서 출력이 나온 것이다. 그래서 c1은 'A'를 c2는 '\n'을(즉<Enter>문자를) 값으로 갖게 된 것이다.

main() {

char c1, c2, c3;

printf("Input first character: ");

scanf("%c",&c1);

scanf("%c",&c3);

printf("Input second character: ");

scanf("%c",&c2);

scanf("%c",&c3);

printf("c1 = %d %c,  c2 = %d %c\n",c1,c1,c2,c2);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input first character: A □

Input second character: B □

c1 = 65 A,  c2 = 66 B

위의 프로그램에서는 문자 데이터 한 개를 읽을 때마다 scanf를 두개씩 사용하고 있다. 첫번째 scanf는 진짜 필요한 데이터를 읽어들이기 위함이고 두번째 scanf는 그 뒤에 따라오는 <Enter> 키를 읽어버리기 위함이다. 그러나 입력한 문자 자체가 '\n'라면 어떻게 할 것인가?

위의 프로그램에서는 무조건 2번째 글자를 읽어 버리기 때문에 '\n' 다음 글자(이는 두번째로 읽어야할 올바른 문자가 될 것이다)는 버리게 되고 3번째 글자(이는 바로 <Enter>키가 될 것이다)가 두번째 데이터로 들어가게 된다. 이를 해결하기 위해서는 결국 입력한 데이터가 '\n'이면 다음 글자를 건너 뛰지 말고 아니면 하나 더 읽어서 <Enter>를 건너 뛰는 것이 필요하다. 이와 같이 하려면 if문이 필요하게 된다. 이 보다 더 간단한 방법은 <Enter>키를 입력하지 않아도 바로 입력이 되는 그런 입력 방법을 사용하는 것이다. 예를 들어 getch라는 함수는 무조건 한 글자만 입력되면 바로 처리하는 성질을 갖고 있다.

main() {

unsigned char c1, c2;

unsigned char c3, c4, c5;

printf("Input two characters: ");

scanf("%c%c",&c1,&c2);

printf("Input three characters: ");

scanf("%c%c%c",&c3,&c4,&c5);

printf("Result: %c%c%c%c%c\n",c3,c4,c5,c1,c2);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input two characters: es □

Input three characters: Box □

Result:

Boes

처음에 입력한 문자들이 'es'이고 나중에 입력한 문자들이 'Box'이므로 'Boxes'라고 출력이 나와야 한다. 그런데 위의 출력을 보면 'Boes'가 나온 것을 볼 수 있다. 또 'Result: '다음에 바로 출력이 나오지 않고, 그 다음 줄에 출력이 나오는 것을 볼 수 있다. 왜 이러한 출력이 나오는가?

실제적으로 다음과 같이 7 문자를 입력한 것이 되는데

es<Enter>Box<Enter>

이중 처음 2글자를 c1과 c2가 읽어들이므로 c1은 'e'를, c2는 's'를 값으로 갖게 된다. 그런데 그뒤 c3, c4, c5가 3글자를 읽어들이므로 c3은 <Enter>를, c4는 'B'를, 그리고 c5는 'o'를 값으로 갖게 된다. 따라서 c3이 <Enter>를 값으로 갖기 때문에 위와 같은 출력이 나오게 된 것이다.

main() {

unsigned char c1, c2;

unsigned char c3, c4, c5;

unsigned char tm;

printf("Input two characters: ");

scanf("%c%c%c",&c1,&c2,&tm);

printf("Input three characters: ");

scanf("%c%c%c",&c3,&c4,&c5);

printf("Result: %c%c%c%c%c\n",c3,c4,c5,c1,c2);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input two characters: es □

Input three characters: Box □

Result: Boxes

위에서 tm은 <Enter>키를 읽어 버릴 변수를 선언한 것인데 변수를 따로 선언하지 않고 c3이나 c4, c5를 이용할 수도 있다. c3, c4, c5는 그 다음의 scanf에서 데이터를 읽어들이기 때문이다.

main() {

unsigned char c1, c2;

unsigned char c3, c4, c5;

printf("Input two characters: ");

scanf("%c%c%c",&c1,&c2,&c3);

printf("Input three characters: ");

scanf("%c%c%c",&c3,&c4,&c5);

printf("Result: %c%c%c%c%c\n",c3,c4,c5,c1,c2);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input two characters: es □

Input three characters: Box □

Result: Boxes

⑷ getchar를 이용한 문자 데이터 유형의 입력

문자 데이터를 입력할 때 scanf외에 getchar라는 것을 이용하여 하나의 문자를 입력할 수 있다.

scanf와는 많이 다른데, 다음과 같은 형태로 사용한다.

c = getchar();

위에서 c는 char 또는 unsigned char 변수로, 문자 하나를 읽어들여 이를 c에 할당하게 된다. 따라서 다음과 같은 의미를 갖는다.

scanf("%c",&c);

getchar 도 scanf와 같이 일단 <Enter>가 들어와야 입력을 받아들인다. 따라서 scanf와 같은 문제가 발생하는데 다음과 같이 사용하면,

getchar();

이는 문자 하나를 읽어들이되 이를 어떤 변수의 값으로 사용하지 말고 그냥 버리라는 뜻이 된다. 즉 문자 하나를 그냥 읽어 버리는 역할을 하는데, 필요없는 데이터를 건너 뛰고 필요한 데이터로 이동하고자 할 때 위와 같은 형태를 사용하게 된다.

#include <stdio.h>

main() {

char c1, c2;

printf("Input first character: ");

c1 = getchar();

getchar();

printf("Input second character: ");

c2 = getchar();

getchar();

printf("c1 = %d %c,  c2 = %d %c\n",c1,c1,c2,c2);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input first character: A □

Input second character: B □

c1 = 65 A,  c2 = 66 B

#include <stdio.h>

main() {

unsigned char c1, c2;

unsigned char c3, c4, c5;

printf("Input two characters: ");

scanf("%c%c",&c1,&c2);

getchar();

printf("Input three characters: ");

scanf("%c%c%c",&c3,&c4,&c5);

getchar();

printf("Result: %c%c%c%c%c\n",c3,c4,c5,c1,c2);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input two characters: es □

Input three characters: Box □

Result: Boxes

위의 두 프로그램은 앞서 scanf로 작성했던 프로그램을 getchar를 사용하여 다시 작성한 것이다.

그러면 getchar가 좋은가 아니면 scanf가 좋은가? 여러 개의 문자를 한번에 읽어들이는 경우에는 scanf가 더 편하지만 한 문자씩 여러번 반복해서 읽어들이는 경우에는 메모리의 양은 접어두고 수행 속도만 비교해 보면 getchar가 더 빠르다.

#include <stdio.h>

main() {

char c1, c2, c3;

printf("Input three lowercase characters: ");

c1 = getchar();

c2 = getchar();

c3 = getchar();

printf("Converted: ");

putchar(c1-32);

putchar(c2-32);

putchar(c3-32);

putchar('\n');

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input three lowercase characters: way □

Converted: WAY

Ⅳ. 연산자

< 산술 연산자 >

기호	인자의 수	위치	의미
+ - + - * / % ++ --	단일 단일 이진 이진 이진 이진 이진 단일 단일	전치 전치 중치 중치 중치 중치 중치 전치, 후치 전치, 후치	양의 부호 음의 부호 덧셈 뺄셈 곱셈 나눗셈 나머지 1증가 1감소

단일 연산자인 +와 -는 바로 부호를 의미한다. 여기서 +는 양의 부호를 -는 음의 부호를 의미하는데 +는 인자의 부호를 변경하지 않지만 -는 인자의 부호를 정반대로 바꾸는 역할을 한다. 따라서 +의 계산 결과는 뒤의 인자 그대로가 되나 -의 계산 결과는 뒤의 인자의 부호를 바꾼 것이 계산 결과가 된다.

이진 연산자인 +와 -는 덧셈과 뺄셈을 나타내는 연산자이며 *와 /는 각각 곱셈과 나눗셈 연산자이다. 그리고 % 연산자는 나머지 연산자인데 다음은 이의 몇 가지 예이다.

int i = 10, j =20;

float f = 1.2f, g = 2.4f;

i + j                         = 30

f + g                         = 3.6

i - j                         = -10

g - f                         = 1.2

j - f                         = 18.8

정수값 j 에서 실수값 f 를 빼면 그 결과는 실수가 된다. 즉 정수에서 실수의 값을 빼면 에러가 발생하지 않고 실수가 된다.

int i = 10, j = 4, k = -4;

float f = 12.4, g = 3.1;

float h1 = 10.0, h2 = 4.0;

i * j                          = 40

f * g                          = 38.44

h1 * h2                        = 40.0

i * f                          = 124.0

i / j                          = 2

j / i                          = 0

i / k                          = -2

f / g                          = 4.0

h1 / h2                        = 2.5

i / h2                         = 2.5

정수와 실수 사이의 곱셈은 i * f 의 결과에서 나타났듯이 실수가 된다. 반면 / 의 경우에 정수끼리의 나눗셈은 정수가 되고 그 결과 몫만 계산된다는 것이다. 그래서 i / j 의 값이 2.5가 아닌 2가 되었다. 마찬가지로 i / k 는 -2.5가 아닌 -2가 되는데, 이를 보면 정수 나눗셈의 경우 결과가 양수이면 버림을, 음수이면 올림을 하는 꼴이 된다.

실수끼리의 나눗셈의 경우에는 제대로 계산이 되어서 h1 / h2 가 2.0이 아닌 2.5가 된다. 곱셈과 마찬가지로 실수와 정수의 나눗셈은 실수가 된다.

int i = 30, j = 7, k = 10, l = -30, m = -7;

float f = 30.0, g = 7.0;

i % j                         = 2

i % k                         = 0

f % g                         = 에러

i % m                         = 2

l % j                         = -2

l % m                         = -2

% 연산자는 몫을 구하고 난 나머지가 계산 결과가 되는데 나머지는 정수 나눗셈에서만 의미가 있기 때문에 % 연산자는 정수 데이터 유형들간에만 사용할 수 있다.

% 연산자의 왼쪽 인자가 음수이면 나머지도 음수이고 양수이면 나머지도 양수이다. 그리고 나머지 계산은 양수로 간주해서 계산한다. 그래서 l % j 와 l % m 의 경우에 l이 음수이기 때문에 나머지가 음수가 되었으며 i % m 의 경우에는 m이 음수이지만 i는 양수이기 때문에 나머지가 양수가 되었다. 반면에 실수에 대해 % 연산자를 적용하면 에러가 발생하게 된다.

a % b  ∃  a - (a / b) * b

위의 식은 나머지를 구하는 알고리즘 중의 하나인데, 여기서 a와 b는 정수이며 b는 0이 아니다.

마지막으로 남은 ++와 --는 단일 연산자이기 때문에 인자는 하나만 필요하다. 그런데 인자로 반드시 변수만 올 수 있다. 또 연산자가 인자의 앞에 올 수도 있고 뒤에 올 수도 있는데, 앞에 왔을 때와 뒤에 왔을 때의 의미가 다르다.

int i = 10;

float f = 1.2;

++i                         = 11( i의 값도11이 된다 )

i++                         = 10( i의 값은 11이 된다 )

++f                         = 2.2( f의 값도 2.2가 된다 )

f++                         = 1.2( f의 값은 2.2가 된다 )

++10                        = 컴파일 에러

--i                         = 9( i의 값도9가 된다 )

i--                         = 10( i의 값은 9가 된다 )

--f                         = 0.2( f의 값도 0.2가 된다 )

f--                         = 1.2( f의 값은 0.2가 된다 )

--10                        = 컴파일 에러

++i는 일단 i의 값을 1증가한 후에 이 증가된 값이 계산 결과가 되는데 반해 i++는 일단 현재의 i의 값을 계산 결과로 사용하고 그 뒤에 i의 값을 증가시키게 된다.

++는 정수 데이터 유형은 물론 실수 데이터 유형의 변수에도 사용할 수 있다. 그리고 ++10이 에러가 나는 것은 ++ 다음의 인자에 변수만 와야 하는데 상수인 10이 왔기 때문이다.

그리고 --는 ++와 사용하는 방법, 의미까지도 완전히 똑같은데 하나 더하는게 아니라 하나 빼는 것만 다르다.

main() {

int i = 10, j = 10;

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

printf("++i = %d  j++ = %d\n",++i,j++);

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

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

i = 10     j = 10

++i = 11  j++ = 10

i = 11     j = 11

main() {

int n = 5, num = 3, i = 10;

printf("n = %d,  n * n++ = %d\n",n,n*n++);

printf("num++ + num = %d\n",num++ + num);

printf("++i = %d,  ++i = %d, ++i = %d\n",++i,++i,++i);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

n = 6,  n * n++ = 30

num++ + num = 7

++i = 13, ++i = 12, ++i = 11

위 프로그램에서 원했던 결과는

n = 5,  n * n++ = 25

num++ + num = 6

++i = 11,  ++i = 12,  ++i = 13

인데, 그렇지 않은 것은

C에서는 2개의 항을 필요로 하는 연산자(단, && || , ? 는 제외)에서 어느 항을 먼저 처리할 것이냐의 평가순서는 특별히 정해져 있지 않고 컴파일러가 상황에 따라 자기 편한대로 결정한다. 즉 n * n의 계산이 끝난 뒤에 n++가 되는 것이 아니라 * 연산자의 우측항을 먼저 평가해서 n++ 이 된 다음 n * n이 된다. 그리고 num++ + num의 경우도 전체수식을 다 계산한 후에 후위형을 처리하는 것이 아니라 수식을 참고해 나가면서 증감 연산자가 있으면 바로 증감이 일어나기 때문에 변수가 어떤 수식 또는 어떤 함수에 매개변수로 두 번 이상 쓰이면 그 변수에 ++나 --연산자는 절대로 사용하지 않는다.

산술 연산자들의 우선순위와 결합방향은 다음과 같다.

우선 순위 : ++, --, +(단일), -(단일)  ∀  *, /, %  ∀  +, -

결합 방향 :      <□□                        □□>         □□>

<□□ 는 오른쪽에서 왼쪽을 의미하며 □□> 는 왼쪽에서 오른쪽의 결합방향을 의미한다. 그리고 ∀ 는 우선순위를 나타낸다.

main() {

int i = 10, j = 7, k = 3, l = 5, m = 10, n = 3;

printf("Result = %d\n",--i/k++ + m/j/n - -n*-l%j);

printf("i = %d, j = %d, k = %d, l = %d, m = %d, n = %d\n",i,j,k,l,m,n);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Result = 2

i = 9, j = 7, k = 4, l = 5, m = 10, n = 3

위 프로그램은 먼저 우선순위를 따져보고 다음 괄호친 것부터 계산해야 한다.

(--i)/(k++) + m/j/n - (-n)*(-l)%j         i = 10, j = 7, k = 3, l = 5, m = 10, n = 3

우선 --i는 9가 되며 동시에 i의 값도 9가 되고 k++의 값은 k가 되므로 3이 되며 그후 k의 값이 하나 늘어나므로 k는 4가 된다. 그리고 -n과 -l은 당연히 -3과 -5가 된다.

9/3 + m/j/n - (-3)*(-5)%j                i = 9, j = 7, k = 4, l = 5, m = 10, n = 3

그 다음 계산해야 할 것은 우선순위가 높은 것들로 다음 괄호친 것들이다.

(9/3) + (m/j/n) - ((-3)*(-5)%j)           i = 9, j = 7, k = 4, l = 5, m = 10, n = 3

괄호 안의 연산자들은 모두 왼쪽에서 오른쪽으로 결합 하므로 처리 순서는 다음과 같다.

(9/3) + ((m/j)/n) - (((-3)*(-5))%j)        i = 9, j = 7, k = 4, l = 5, m = 10, n = 3

이들을 차례대로 계산하면 다음과 같이 된다.

(3) + (1/n) - (15%j)                      i = 9, j = 7, k = 4, l = 5, m = 10, n = 3

3 + 0 - 1 = 2                             i = 9, j = 7, k = 4, l = 5, m = 10, n = 3

< 할당 연산자 >

기호	인자의 수	위치	의미
= *= /= %= += -= <<= >>= &= ^= |=	이진 이진 이진 이진 이진 이진 이진 이진 이진 이진 이진	중치 중치 중치 중치 중치 중치 중치 중치 중치 중치 중치	오른쪽 값을 할당 곱한 값을 할당 나눈 값을 할당 나머지 값을 할당 더한 값을 할당 뺀 값을 할당 왼쪽 쉬프트한 값을 할당 오른쪽 쉬프트한 값을 할당 AND한 값을 할당 XOR한 값을 할당 OR한 값을 할당

우선 '=' 부터 살펴보면 이는 x = y 형태로 사용하는 이진 연산자이며 ++와 유사하게 =의 왼쪽 인자(즉 x)에는 반드시 변수가 와야 한다. 그러나 오른쪽(즉 y)에는 아무 수식이 와도 상관 없다. x = y 의 계산 결과는 x와는 전혀 상관없이 무조건 y의 값이 계산 결과가 된다.  즉 y의 값을 x의 변수에 할당하게 된다(그래서 이름이 할당 연산자이다). 이것은 FORTRAN이나 BASIC에서 사용하는 할당문과 똑같은데 실제로 하는 일도 비슷하다. 그러나 C에서는 이것이 문장이 아니라 연산자, 즉 수식이란 것이다. 따라서 다른 연산자와 같이 섞어 쓸 수도 있으며 x = y = z와 같이 여러 개를 같이 사용할 수도 있다.

나머지 할당 연산자들은 모두 '=' 앞에 뭔가가 붙어 있는데 이를 'op='라고 하면(op는 =앞에 붙은 연산자를 의미한다) 이는 다음과 똑같은 의미를 갖는다.

x op= y  ∃  x = x op y

다음은 이의 몇 가지 사용예이다.

int i = 3, j = 5, k = 10;

i += j;                   = 8 (i = 8)

j *= k;                   = 50 (j = 50)

위와 같이 축약된 형태의 연산자를 사용할 때 주의할 점이 있는데, 위에서 x *= y는 x = x * y라고 했는데 그러면 x *= y + 1은 무엇이 되겠는가 하는 것이다. x = x * y를 그대로 따른다면 x = x * y + 1이 되는데 그러면 x = (x * y) + 1이 된다. 과연 그런가? 이는 실제로 해보면 알 수 있는데 x *= y + 1은 x = x * (y + 1)과 같다. 따라서 x op= y는 x op= (y)로 생각하는 것이 좋다. y의 전체 값에 대해 연산을 수행하기 때문이다.

또 x++ op= y와 같이 x에 대해 부수효과를 일으키는 ++나 --가 있는 경우이다. 할당 연산자의 왼쪽에는 반드시 변수가 와야 하기 때문에 x++는 올 수 없다(x++는 변수가 아니라 수식이다). 그러나 하나 예외가 있는데 x가 포인터 변수인 경우에는 가능하게 된다. 이때 x++ op= y가 역시 x++ = x++ op y가 된다면 x의 값은 2번 증가해야 할 것이다. 그러나 이 경우 x의 값은 하나밖에 증가하지 않는다. 즉 x++ op= y는 x++ = x op y가 되므로 주의해야 한다.

할당 연산자의 우선순위는 앞으로 나올 콤마 연산자 다음으로 가장 낮고 결합방향은 오른쪽에서 왼쪽이다. 따라서 x = y = z = 3은 x = (y = (z = 3))으로 처리되며 이의 계산 결과는 3이 되고 부수효과로 x, y, z는 모두 3을 값으로 갖게 된다.

다음은 할당 연산자에 관한 예이다.

int i = 3, j = 6, k = 9, l = 12;

float f = 1.2f, g = 0.1f;

① i += j -= k *= l /= 3       = -27 (i = -27, j = -30, k = 36, l = 4)

② f *= g + 2.1f               = 2.64f (f = 2.64f)

③ k += j - i *= 3             (컴파일 에러)

④ k += j - (i *= 3)           = 6 (k = 6)

①은 우선순위가 같기 때문에 결합방향은 오른쪽에서 왼쪽으로 (i += (j -= (k *= (l /= 3))))으로 처리된다. l /= 3은 l = l / 3이고 이는 l = 4이므로 계산 결과는 4가 된다(이때 l도 4가 된다). 따라서 (i += (j -= (k *= 4)))가 되며 k *= 4는 36이므로 i += (j -= 36)이 되고 j -= 36은 -30이므로 결국 i += -30이 되어 전체 계산 결과는 -27이 된다.

②는 f *= (g + 2.1f)가 되므로 f *= 2.2f가 되어 이는 2.64f가 된다.

③은 할당 연산자가 우선순위가 가장 낮기 때문에 k += ((j - i) *= 3)으로 처리 되는데 이는   k += (3 *= 3)으로 '*='의 왼쪽에는 반드시 변수가 와야 하는데 상수 3이 왔으므로 컴파일 에러가 발생한다.

④의 경우에는 괄호로 *= 를 먼저 처리했으므로 k += j - 9가 되어 이는 k += -3이 되고 이는 6이 된다.

< 관계 연산자 >

기호	인자의 수	위치	의미
< <= > >= == !=	이진 이진 이진 이진 이진 이진	중치 중치 중치 중치 중치 중치	작다 작거나 같다 크다 크거나 같다 같다 같지 않다

'>='에서 >와 =는 반드시 붙여써야 하며 > =와 같이 띄어 쓰게 되면 컴파일할 때 에러가 발생하게 된다. 이는 <=도 마찬 가지이다.

모든 연산자는 계산 결과가 있는데, 즉 3 < 4도 계산 결과가 있다는 것이다. 참인 경우는 무조건 계산 결과가 1이 되며 거짓인 경우는 무조건 계산 결과가 0이 된다. 3 < 4는 참이기 때문에  계산 결과가 1이 되며 7 < 2는 거짓이기 때문에 0이 된다.

int i = 1, j = 0, k = -1, l = 0;

i < j                         = 0

i <= (j + 1)                  = 1

k > l                         = 0

j >= l                        = 1

(i + k) >= (j + l)            = 1

x == y는 x와 y의 값이 같으면 참이 되고 같지 않으면 거짓이 된다. 반면에 x != y는 x와 y의 값이 서로 같지 않으면 참이 되고 같으면 거짓이 되는데 x != y와 !(x == y)는 서로 동일한 수식('!'는 다음에 나올 논리 연산자)이다.

int i = 1, j = 1, k = 0, l = -1;

i == j                        = 1

i != j                        = 0

(j + l) == k                  = 1

(i + j) != (j - l)            = 0

('a' + 1) == i                = 0

마지막의 ('a' + 1) == i 에서 ' '기호로 묶이면 char형을 나타내는데 여기에 산술 연산자가 쓰이면 해당 문자의 ASCII 코드 값과 연산을 한다.

관계 연산자들의 우선순위와 결합방향은 다음과 같다.

우선 순위 : >, >=, <, <=  ∀  ==, !=

결합 방향 :       □□>           □□>

이들의 우선순위는 산술 연산자보다는 낮다. 따라서 다음과 같은 순서로 결합된다.

int i = 1, j = 2, k = -5;

① -i - 5 * j >= k + 1            /*  ((-i) - (5 * j)) >= (k + 1)  */

② i >= j == k != 2 <= j - 5      /*  ((i >= j) == k) != (2 <= (j - 5))  */

①은 변수에 해당값을 넣어 계산하면 ((-1) - (5 * 2)) >= (-5 + 1)은 -11 >= -4이 되는데 이것은 거짓이므로 0이 된다.

②는 (0 == -5) != 0이 되고 0 != 0이 되어 거짓이므로 0이 된다.

main() {

int i, j;

printf("Input two integers: ");

scanf("%d%d",&i,&j);

printf("\nYou typed: %d and %d\n",i,j);

printf("The result is %d\n",(i > j) - (i < j));

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input two integers: 2 5 □

You typed 2 and 5

The result is -1

위 프로그램은 정수값 두개를 읽어들여 앞의 정수가 뒤의 정수보다 크면 1을, 같으면 0을, 그리고 작으면 -1을 출력시키는 프로그램인데 우선 두개의 정수값을 읽어야 하니까 두 개의 정수 변수가 필요하며 이를 각각 i, j라고 한다. 그러면 i > j면 1을, i == j면 0을, 그리고 i < j면 -1을 출력시켜야 한다. 그런데 이 세가지 중 반드시 어느 하나만 참이지 두개 이상이 동시에 참이 될 수 없기 때문에 다음과 같은 수식을 만들수 있다.

A(i > j) + B(i == j) + C(i < j)

우선 i > j가 참일 때 다른 것들은 다 거짓이므로(이는 0이다) 이경우 위의 수식은 A만 남게 되며 이때 이 값이 1이어야 하므로 A = 1이 된다. 반면에 i == j가 참인 경우에는 B만 남게 되는데 이때 이 값이 0이므로 B는 0이 되어야 한다. 마찬가지로 i < j인 경우에는 C만 남는데 이 값이 -1이어야 하므로 C는 -1이 된다. 따라서 위의 수식은 다음과 같이 된다.

1 * (i > j) + 0 * (i == j) + -1 * (i < j)

∀ (i > j) - (i < j)

< 논리 연산자 >

기호	인자의 수	위치	의미
&& || !	이진 이진 단일	중치 중치 중치	AND(논리곱) OR(논리합) NOT(부정)

예를 들어 P와 Q가 참거짓을 나타내는 명제라고 할 때

P Q	P AND Q	P OR Q	NOT P
참 참	참	참	거짓
참 거짓	거짓	참	거짓
거짓 참	거짓	참	참
거짓 거짓	거짓	거짓	참

C에서 &&, ||, !은 연산자이기 때문에 계산 결과가 반드시 있어야 한다. 그래서 C에서는 "0은 거짓이고 0이 아닌 것은 무조건 참이다" 라는 규칙을 정해 사용하고 있다. 즉 0이 아닌 것은 무조건 참이기 때문에 -1도 참이고 0.1도 참이고 'A'도 참이다. 이는 관계 연사자와 똑같이 하면 되는데 그 결과가 참이면 무조건 1이고 거짓이면 무조건 0으로 하는 것이다.

다음은 이의 몇가지 예이다.

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

char c = 'A', d = '0', e = '\0';

float f = 1.2f, g = 0.0f;

i && j                    = 1

i && 0                    = 0

(i + j) && k              = 1

c && d                    = 1   /*  '0'은 코드값이 48이므로 참  */

c && e                    = 0   /*  e의 값은 0이고 거짓  */

0 || 0.1                  = 1   /*  0.1은 0이 아니므로 참  */

f || g                    = 1

e || g                    = 0   /*  g는 0(실수 0.0도 0이다)이므로 거짓  */

!i                        = 0

!e                        = 1

!d                        = 0

!f                        = 0

!g                        = 1

논리 연산자를 사용할 때 하나 주의할 것은 &&나 ||는 반드시 붙여 사용하여야 한다는 것이다.  & &나 | |와 같이 띄어쓰게 되면 컴파일 에러가 발생한다.

논리 연산자의 결합방향과 우선순위를 지금까지 나온 연산자들과 비교해서 나타내면

우선 순위 : !, ++, -- (산술 연산자)  ∀  관계 연산자  ∀  &&  ∀  ||

결합 방향 : <□                                            □>      □>

&&연산자가 || 보다 우선순위가 높다. 하지만 둘 다 단항, 산술 그리고 관계 연산자보다는 우선순위가 낮다. 결합방향은 왼쪽에서 오른쪽으로 된다. 반면에 !는 ++, --와 우선순위가 같으며 결합 방형은 오른쪽에서 왼쪽이다. 다음의 사용예를 살펴보자.

char c = 'B';

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

double x = 0.0, y = 2.3;

i && j && k                      = 1  /* (i && j) && k 가 된다 */

!i - j                           = -3 /* (!i) - j 가 되어 0 - 3이 된다 */

i * !x                           = 3 /* x가 0이라서 !x는 참, 즉 3 * 1이 된다 */

i < j || x > y                   = 0 /* (i < j) || (x > y) 가 된다 */

c - 1 == 'A' || c + 1 == 'Z'     = 1 /* ((c - 1) == 'A' || ((c + 1) == 'Z') 가 된다 */

main() {

int i = 0, j = 3;

printf("%d\n",i != 0 && j / i == 1);

printf("%d\n",i == 0 || j / i == 1);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

0

1

위 프로그램에서 첫번째 printf문은 i != 0 && j / i == 1 의 값을 출력시키고 있는데 i의 값이 0 이므로 i != 0 은 거짓이다. &&는 어느 한쪽이 거짓이면 거짓이기 때문에 결과는 거짓이 되어 0이 된다. 그런데 문제는 j / i == 1 에 있다. 현재 i의 값이 0이기 때문에 j / i 는 3 / 0 이 되므로 0으로 나눗셈을 하게 되어 수행시 당연히 에러가 발생해야 된다. 그러나 이 경우에는 에러가 발생하지 않고 0이 출력된다.

MSC와 TC의 경우 &&에서 왼쪽의 값이 거짓(즉 0)일 때 오른쪽의 값을 계산하지 않는데 이것을 연결된 AND(Wired-AND) 처리 방법이라고 하며 그렇기 때문에 ++와 같이 부수효과를 일으키는 연산자는 &&의 오른쪽에 사용하지 않는 것이 좋다.

이와 같은 문제는 && 뿐만 아니라 ||에도 발생할 수 있는데, 두번째 printf문은 &&의 경우와 정반대라고 할 수 있다. i == 0 || j / i == 1 에서 현재 i의 값이 0이므로 i == 0 은 참이 된다.

|| 은 한쪽이 참이면 무조건 참이기 때문에 i == 0 || j / i == 1 은 참이 된다. 그러나 마찬가지로 j / i == 1 을 계산하면 i의 값이 0이기 때문에 수행시 에러가 발생하게 된다. 따라서 || 의 경우 왼쪽의 값이 참(즉 0이 아닌 값)일 때 오른쪽의 값을 계산할 것이야 아니냐하는 문제를 갖게 된다. 이 때 계산하지 않는 것을 연결된 OR(Wired-OR) 처리 방법이라고 한다.

MSC와 TC는 ||도 연결된 OR 형태로 처리하며 위의 프로그램의 경우 에러가 발생하지 않고 1이 출력되게 된다.

main() {

int i;

printf("Input score: ");

scanf("%d",&i);

printf("The grade is: %c\n",65 * (i >= 90) + 66 * (80 <= i && i <=89) +

67 * (70 <= i && i <= 79) + 68 * (60 <= i && i <= 69) + 70 * (i <= 59));

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input score: 95 □

The grade is: A

위 프로그램은 점수를 읽어(이는 정수값으로 0 ~ 100점 사이라고 가정) 이의 값이 90 이상이면 A를, 80~89면 B를, 70~79면 C를, 60~69면 D를, 그 이하면 F를 출력시키는 것으로, 우선 정수값 하나를 읽어들이므로 이를 위한 변수로 i를 사용하고 있다.

입력한 i의 값이 95이기 때문에 printf문의 연산식 중에서 65 * (i >= 90)만 참이 되고 나머지는 모두 거짓이 되기 때문에 다음과 같이 되는데,

printf("The grade is: %c\n",65 * (1) + 66 * (0) + 67 * (0) + 68 * (0) + 70 * (0));

이를 모두 합한 결과를 %c로 출력하는데 A의 코드 값이 65이므로 원하는 결과를 얻을 수 있다.

< 형변환 규칙과 CAST 연산자 >

C에서는 서로 다른 데이터 유형간의 연산을 자유롭게 허용하고 있는데 수식 안에 서로 다른 데이터 유형이 있을 경우 한 데이터 유형이 다른 데이터 유형으로 자동적으로 변환되는 형변환(type conversion)이 발생하게 된다. 이 형 변환은 임의로 발생하지 않고 컴파일러가 내부적으로 정한 규칙을 따르게 된다.

① char와 unsigned char는 일단 int로 변환한다.(이 때 char는 앞의 부호가 확장되고 unsigned    char는 0이 확장된다.

② short int는 int로, unsigned short int는 unsigned int로 변환한다. 그러면 수식에는 char와    short int 유형은 없게 된다.

③ 수식이 x op y라고 할 때 x와 y중 더 큰 데이터 유형으로 x와 y를 변환한다. 이때 데이터 유    형의 크기는 다음과 같다.

long double > double > float > unsigned long int > long int > unsigned int > int

예를 들어 char = c 일 때 c + 3.2는 먼저 c의 값이 int로 변환된다. 그러면 int + double이 되는데(3.2는 double이다) double > int 이므로 c의 값이 다시 double로 변환한다. 따라서 계산 결과는 double이 된다. 다음 수식의 계산 결과의 데이터 유형은?

int i;

double g;

i += g          = int

g는 double이고 i는 int이므로 역시 double이 되어야 할 것이다. 그러나 할당 연산자의 경우에는 오히려 왼쪽 변수의 데이터 유형을 따르게 된다. 따라서 double이 int로 변환되어 i 값에 들어가게 되고 이 값이 바로 계산 결과가 된다.

어떤 데이터 유형의 값을 강제적으로 다른 데이터 유형으로 변환하는 것이 필요할 때가 많은데, C에서는 다음과 같은 형태로 형변환 연산자를 제공하고 있다.

(데이터 유형 이름) 수식

변환하고 싶은 데이터 유형의 이름에 괄호를 치면 된다. 형변환 연산자는 ++, --와 같이 우선순위가 제일 높으며 결합방향은 오른쪽에서 왼쪽이다. 따라서 (int)i + 1은 ((int)i + 1)로 처리되고 (float)(int)x는 ((float)((int)x))로 처리되는데 예를 들면 다음과 같다.

char c = 'A';

int i = 10;

float f = 3.2f;

(int)(c + i + f)                  /*  65 + 10 + 3 = 78(int)  */

(int)f * i - 2                    /*  3 * 10 - 2 = 28(int)  */

(float)(int)f - (int)c - i        /*  3.0 - 65 - 10 = -72.0(float)  */

(long double)c - f * 10.0         /*  65.0 - 32.0 = 33.0(long double)  */

※ 부호 확장(sign extension)과 0 확장(zero extension)

큰 데이터 유형의 값을 작은 데이터 유형의 값에 할당하거나 큰 데이터 유형의 값을 작은 데이터 유형으로 변환하면 오버플로우(overflow)가 발생하기 쉽다. 예를 들어 long int유형의 값 300000을 short int의 변수에 할당하게 되면 short int로는 300000을 표현할 수 없으므로 오버플로우가 발생하게 된다. 반면에 작은 데이터 유형의 값을 큰 데이터 유형에 할당하거나 변환하게 되면 빈 공간이 생길 수 있다. 이는 정수 데이터 유형간에서 발생할 수 있는데 예를 들어 char 유형의 값을 int 유형에 할당하게 되면 char는 1바이트이지만 int는 2바이트이기 때문에 1바이트가 남게 된다. 이때 남는 1바이트를 무엇으로 채워넣느냐는 문제가 발생하게 된다.

char c = -1;

int i;

위의 프로그램에서 i = c와 같이 i에 c의 값을 할당한다고 한다면 i의 값은 당연히 -1이 되어야 할 것이다. 그런데 c = -1에서 c의 값은 2진수로 11111111이 된다. 이를 i에 그대로 할당하고 남는 것을 0으로 채워 넣는다면 0000000011111111이 되어 이는 -1이 아닌 +255가 되어 버린다. int유형에서 -1은 1111111111111111이므로 오히려 1을 다 채워 넣어야 올바른 결과가 된다.

그렇다면 양수의 경우에는 0을, 음수의 경우에는 1을 채워넣는 것이 올바른 결과를 낳게 되는데 이를 바로 부호 확장(sign extension)이라고 한다.

unsigned char c = 129;

unsigned int i;

위에서 i = c라고 했을 때 만약 부호 확장을 한다면 c의 값이 10000001이므로 1이 확장되 i의 값은 1111111110000001이 되어 65409가 되어 버린다. 이는 올바른 결과가 이닐 것이다. 이 경우에는 오히려 무조건 0을 채워 넣는 것이 맞게 되는데 이를 바로 0 확장(zero extension)이라고 한다. unsigned 유형의 데이터를 unsigned 유형의 데이터에 할당할 때에는 이와 같이 0 확장하도록 되어 있다.

unsigned 유형의 데이터를 signed 유형의 데이터에 할당할 때에는 unsigned 유형의 값이므로 0 확장하는 것이 올바르게 되며 signed 유형의 데이터를 unsigned 유형의 데이터에 할당할 때에는 부호 확장하는 것이 올바른 결과를 낳게 된다.

< 기타 연산자 >

⑴ 조건 연산자

조건 연산자는 C에서만 볼 수 있는 연산자인데 특이하게도 3진 연산자이며 다음과 같은 형태로 사용한다.

수식1 ? 수식2 : 수식3

먼저 수식1을 계산한다. 그래서 그 계산 결과가 참이면 수식2를 계산하고 수식2를 계산한 결과가 이 조건 연산자의 계산 결과가 되며 수식1을 계산한 결과가 거짓이면 수식3을 계산하고 이를 계산한 결과가 바로 이 조건 연산자의 계산 결과가 된다. 주의할 것은 수식2가 계산되면 수식3은 계산되지 않으며 마찬가지로 수식3이 계산되면 수식2는 계산되지 않는다는 것이다.

x = (y < z) ? y : z;

조건 연산자의 우선순위는 할당 연산자 보다 높기 때문에(그 외의 다른 연산자보다는 우선 순위가 낮다) 괄호는 안 써도 되지만 괄호는 무엇을 조건으로 하는지 분명하게 해 주는 역할을 하기 때문에 써주는 것이 좋다. 그리고 결합방향은 오른쪽에서 왼쪽이다. 따라서 x ? y : z ? w : v 는 x ? y : (z ? w : v)로 처리된다.

char a = 'a', b = 'b';

int i = 1, j = 2, k;

double x = 7.07;

① i == j ? a - 1 : b + 1             = 99 (int)

② j % 3 == 0 ? i + 4 : x             = 7.07 (double)

③ j % 3 ? i + 4 : x                  = 5.0 (double)

④ k = i - 1 ? j += 3 : i * j         = 2 (int)

① i와 j의 값은 서로 다르기 때문에 b + 1을 처리한다. 그래서 b의 ASCII 코드값인 98에 1을 더해 결과값은 99가 된다. 그리고 a와 b는 char형이므로 형변한 규칙에 의해 정수형이 된다.

② j를 3으로 나눈 나머지는 2가 되므로 0과는 같지 않기 때문에 x의 값인 7.07이 결과 값이 된다. 그리고 i + 4와 x중 x의 형이 더 높으므로 전체수식도 double형이 된다.

③ 나머지가 2이므로 i + 4가 결과값이 되어 5가 되지만 전체수식의 형이 double형이므로 5.0이 된다.

④ 조건 연산자가 =보다는 우선 순위가 높기 때문에 k = ((i - 1) ? (j += 3) : (i * j))로 처리 되며 i - 1은 거짓이기 때문에 i * j가 결과값이 된다.

main() {

int i;

printf("Input the number of books: ");

scanf("%d",&i);

printf("%d book%c\n",i,(i == 1) ? '\0' : 's');

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input the number of books: 25 □

25 books

위의 두번째 printf문에서 조건 연산자를 사용하고 있는데 i가 1이면 '\0'을, 1이 아니면 's'를 계산 결과로 사용하고 있다. 이를 %c가 받으므로 결국 i가 1이면 널 문자가 출력되어 아무것도 안나오게 되며 1이 아니면 's'가 출력되게 된다.

다음은 3개의 정수값을 읽어 이의 최대값을 구하는 프로그램이다. 최대값은 일단 2개 중 큰 값을 구한 다음 이를 나머지 하나와 비교해 그 중 큰 값을 구하면 된다.

main() {

int x, y, z;

printf("Input three numbers: ");

scanf("%d%d%d",&x,&y,&z);

printf("The largest number is %d\n",(x = (x > y) ? x : y) > z ? x : z);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input three numbers: 22 87 -32 □

The largest number is 87

⑵ 비트 단위 연산자

기호	인자의 수	위치	의미
& | ^ ~ << >>	이진 이진 이진 단일 이진 이진	중치 중치 중치 전치 중치 중치	비트 단위의 AND 비트 단위의 OR 비트 단위의 XOR 1의 보수 왼쪽 쉬프트 오른쪽 쉬프트

위의 연산자 가운데 &와 |, ^ 는 두 개의 비트에 대해 동작하는 연산자로 &는 AND 연산을, | 는 OR 연산을, 그리고 ^는 XOR 연산을 수행한다. 이들 연산자의 계산 방법은 다음과 같다.

X Y	X & Y	X | Y	X ^ Y
0 0	0	0	0
0 1	0	1	1
1 0	0	1	1
1 1	1	1	0

&와 |, ^를 C의 데이터 유형에 적용하게 되면 각 해당하는 비트끼리 계산을 수행하게 된다. 따라서 같은 크기의 데이터 유형에 대해서만 위의 연산자들을 적용하여야 하며, 각 비트 단위로 동작하기 때문에 부호는 의미가 없다. 그래서 보통 unsigned 유형의 데이터에 대해서만 위의 연산자들을 사용하고 있다.

다음은 위의 3 연산자의 몇 가지 사용 예이다.

unsigned char c = 0x7A, d = 0xA3;

c & d                              = 0x22

c | d                              = 0xFB

c ^ d                              = 0xD9

c = 01111010               c = 01111010                c = 01111010

d = 10100011               d = 10100011                d = 10100011

□□□□□□□□□□□□□□□□□□□□       □□□□□□□□□□□□□□□□□□□□□        □□□□□□□□□□□□□□□□□□□□

c & d = 00100010          c | d  = 11111011            c ^ d = 11011001

= 0x22                     = 0xFB                      = 0xD9

데이터의 값이 10진수인 경우에는 이를 2진수로 변환해야만 비트 단위의 연산을 수행할 수 있고 또 음수값을 준 경우에는 2의 보수로 표현되기 때문에 주의해야 한다. 다음은 이의 한 예이다.

char c = 37, d = -5;

c & d                  = 33

c | d                  = -1

c ^ d                  = -34

c = 00100101               c = 00100101                c = 00100101

d = 11111011               d = 11111011                d = 11111011

□□□□□□□□□□□□□□□□□□□□       □□□□□□□□□□□□□□□□□□□□□        □□□□□□□□□□□□□□□□□□□□

c & d = 00100001          c | d  = 11111111            c ^ d = 11011110

= 33                       = -1                        = -34

그리고 단일 연산자인 ~는 각 비트를 1은 0으로, 0은 1로 뒤집는 역할을 한다. 각 비트를 뒤집는 것이 바로 1의 보수이기 때문에 ~는 1의 보수를 구하는 연산자가 된다. 다음은 ~연산자의 사용예이다.

char c = 0, d = -1, e = 56;

~c                         = -1

~d                         = 0

~e                         = -57

c는 0이므로 00000000이며 d는 11111111, e는 00111000이 된다. 그러면 ~c는 11111111, ~d는 00000000이 되며 ~e는 11000111이 된다. 이를 다시 10진수로 변환하면 위와 같은 결과를 얻을 수 있다. 참고로 □와 ~□의 값을 합하면 항상 -1이 된다는 것이다.

마지막으로 <<와 >>는 주어진 데이터를 왼쪽으로 쉬프트하거나 오른쪽으로 쉬프트한 결과를 계산 결과로 삼는다. <<와 >>는 x << y, x >> y 와 같은 형태로 사용하는데 x << y는 x의 값을 y번 왼쪽으로 쉬프트한 값이 계산 결과가 되며 x >> y는 x의 값을 y번 오른쪽으로 쉬프트한 것이 계산 결과가 된다. 이때 주의할 것은 x자체의 값은 쉬프트 되지 않는다는 것이다. 즉 이 연산자는 부수효과가 없기 때문에 x의 값은 바뀌지 않는다.

그리고 y는 반드시 양의 정수값이 와야 하며 y가 0일 때에는 그냥 x의 값이 되지만(0번 쉬프트 한것은 안한것과 같다) y가 음수 값인 경우에는 에러가 발생하지 않고 그냥 0이 되어버리므로 주의하여야 한다.

다음은 01001011의 값을 왼쪽으로 쉬프트한 것이다.

0 1 0 0 1 0 1 1

↘↘↘↘↘↘↘↘

1 0 0 1 0 1 1 0 ←

위에 나타난 바와 같이 2번째 비트는 첫번째 비트로, 3번째 비트는 2번째 비트로,... 자리 이동을 하며 맨 마지막 비트(즉 8번째 비트)에는 무조건 0이 들어오게 된다. 그리고 첫번째 비트는 갈곳이 없기 때문에 사라지게 된다.

다음은 01001011의 값을 오른쪽으로 쉬프트한 것이다.

0 1 0 0 1 0 1 1

♩♩♩♩♩♩♩♩

→ ? 0 1 0 0 1 0 1

왼쪽 쉬프트와는 정반대로 오른쪽 쉬프트의 경우에는 맨 끝(즉 8번째)의 비트가 사라지게 되며 첫번째 자리에 새 데이터가 들어오게 되는데 이 경우에는 무조건 0이 들어오지 않는다. 일반적으로 데이터의 유형이 unsigned인 경우에는 무조건 0이 들어오게 된다(이 경우 이 쉬프트를 논리적 쉬프트라고 부른다). 반면에 쉬프트한 데이터의 유형이 signed인 경우에는 쉬프트하기 전의 첫번째 비트가 1인 경우에는 1이 들어오게 되며 0인 경우에는 0이 들어오게 된다. 즉 첫번째 비트와 같은 값이 들어온다(이를 산술 쉬프트라고 부른다).

다음은 쉬프트 연산자에 대한 몇 가지 예이다.

unsigned char c = 0x71, d = 0xB9, e = 47, f = -2;

c << 1                            = 0xE2

d << 2                            = 0xE4

e >> 3                            = 0x05

f >> 4                            = 0x0F

위의 계산 결과를 구하려면 각 변수의 2진수 형태를 먼저 구해야 한다. c는 01110001이고 d는 10111001, e는 00101111, 그리고 f는 11111110이 된다. 그러면 c << 1은 11100010이 되며 d << 2는 두번 쉬프트하므로 11100100이 된다. 반면에 e >> 3과 f >> 4는 e와 f가 모두 unsingned 유형이므로 논리 쉬프트를 수행하면 e >> 3은 00000101이 되고 f >> 4는 00001111이 된다. 만약 이들이 singed char였으면 e >> 3은 그대로 00000101이 되나 f >> 4는 11111111이 되어 달라지게 된다.

비트 단위 연산자의 우선 순위와 결합 방향은 다음과 같다.

우선 순위 : ~, !, ++, --  ∀  산술 연산자  ∀  <<, >>  ∀  관계 연산자  ∀  &  ∀  ^

∀  |  ∀  논리 연산자

결합 방향 : <□                □>                  □>                        □>     □>

main() {

unsigned int u;

printf("Input a number: ");

scanf("%d",&u);

printf("2\'s complement is: %x\n",~u+1);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input a number: 256 □

2's complement is: ff00

위 프로그램은 2의 보수를 16진수로 출력하는 프로그램인데, 비트 단위 연산자 가운데 2의 보수를 구하는 연산자는 없다. 그러나 2의 보수가 1의 보수에 1을 더한 것이기 때문에 이를 이용하면 쉽게 구할 수 있다. ~u가 1의 보수이므로 ~u+1은 2의 보수를 나타낸다.

※ 산술 쉬프트와 논리 쉬프트

논리 쉬프트의 경우에는 쉬프트를 비트들의 순수한 자리 이동으로 보며 오른쪽 쉬프트의 경우에는 첫번째 비트가 두번째 자리로 옮겨 가고 두번째 비트는 세번째 자리로 옮겨가고... 등 자리 이동으로 간주한다. 그리고 빈 자리에는 0이 들어 온다고 생각한다. 일반적으로 쉬프트하면 이 논리 쉬프트를 의미하는 경우가 많다. 그렇다면 산술 쉬프트는 이를 어떻게 보는가?

char c = 3;       /*  c = 00000011  */

c << 1                  = 00000110            = 6

c << 2                  = 00001100            = 12

c << 3                  = 00011000            = 24

c << 4                  = 00110000            = 48

위의 프로그램에서는 c의 값을 왼쪽으로 쉬프트 할 때마다 * 2한 것과 같은 결과를 낳고 있다. 즉 c << 1은 c * 2와 같으며 c << 2는 c * 2 * 2와 같고 c << 3은 c * 2 * 2 * 2와 같다. 이와 같이 왼쪽으로 쉬프트한 것이 곱한것과 관계가 있기 때문에 컴퓨터 가운데는 곱셈을 이 쉬프트를 이용하여 구현하는 경우가 대부분이다. 산술 쉬프트는 이와 같이 왼쪽으로 쉬프트하는 것을 곱셈으로 본다. 즉 * 2로 보는 것이다. 그러면 오른쪽 쉬프트는 어떻게 되겠는가?

char c = 48;       /*  c = 00110000  */

c >> 1                   = 00011000            = 24

c >> 2                   = 00001100            = 12

c >> 3                   = 00000110            = 6

c >> 4                   = 00000011            = 3

오른쪽으로 쉬프트를 할 때마다 이는 ÷ 2와 같은 효과를 낳게 된다(물론 이는 정수 나눗셈이다). 산술 쉬프트는 이와 같이 오른쪽으로 쉬프트하는 것을 나눗셈으로 간주한다. 그런데 문제는 음수의 경우에 발생한다. 위와 같이 한다면 -4를 오른쪽으로 쉬프트하면 -2가 되어야 한다. 그런데 이를 논리 쉬프트와 같이 그냥 오른쪽으로 쉬프트하고 새로 들어올 자리에 0을 넣게 되면 다음과 같이 엉뚱한 결과가 나오게 된다.

-4              = 11111100

-4 >> 1         = 01111110        = 126

즉 -4를 오른쪽으로 그냥 쉬프트하면 +126이 되어 버리는데 이는 산술 쉬프트의 의미와 맞지 않는 것이다. 산술 쉬프트에서는 오른쪽으로 쉬프트하면 ÷2가 되므로 -2가 되어야 하기 때문이다. 그러면 어떻게 해야 하는가? 위의 경우 0이 아닌 1이 들어가게 되면 다음과 같이 올바른 결과가 된다.

-4              = 11111100

-4 >> 1         = 11111110        = -2

그래서 산술 쉬프트의 경우 오른쪽으로 쉬프트할 때에, 양수 값은 0이 들어오게 되고 음수값은 1이 들어오게 된다. 이것이 논리 쉬프트와 다른 점이다.

※ 비트 단위의 연산자의 응용

비트 단위의 연산자는 특히 특정 비트의 값을 0으로 세팅하거나 1로 세팅하고자 할 때, 특정 비트의 값만 짤라내고자 할 때에 사용한다. 이는 한글을 처리하거나 그래픽 처리, 하드웨어 제어 등에서 매우 자주 사용되는 연산들이다.

우선 특정 비트만 0으로 만들고자 할 때(이를 선택적 리셋(reset)이라 한다), 예를 들어 unsigned char유형의 데이터를 첫번째 비트와 4, 5번째 비트를 0으로 세팅하고자 할 때 보통 마스크(mask)라고 부르는 것을 먼저 만들어야 한다.

선택적 리셋을 위한 마스크는 다음과 같이 만들면 된다. 즉 0으로 세팅하고자 하는 비트는 0으로, 나머지 비트는 1로 만들면 되는데, 1, 4, 5번째 비트를 0으로 세팅하고자 하므로 마스크는 01100111이 된다. 이 마스크를 처리할 데이터와 '&' 연산을 수행하면 다음과 같이 된다.

데이터: 11010101

마스크: 01100111

& □□□□□□□□□□

01000101

이와는 정반대로 특정 비트를 1로 세팅하고자 할 때(이를 선택적 셋(set)이라 한다), 이 경우에도 먼저 마스크를 만드는데 예를 들어 1, 7, 8비트를 1로 세팅하고자 할 때의 마스크는 10000011이 된다. 이 마스크를 이번에는 데이터와 '|' 연산을 수행하면 된다.

데이터: 11010101

마스크: 10000011

| □□□□□□□□□□

11010111

main() {

unsigned char c;

printf("Input a character: ");

scanf("%c",&c);

printf("%1d",(c & 0x80) >> 7);

printf("%1d",(c & 0x40) >> 6);

printf("%1d",(c & 0x20) >> 5);

printf("%1d",(c & 0x10) >> 4);

printf("%1d",(c & 0x08) >> 3);

printf("%1d",(c & 0x04) >> 2);

printf("%1d",(c & 0x02) >> 1);

printf("%1d\n",(c & 0x01));

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input a character: A □

01000001

코드 값을 10진수나 8진수, 또는 16진수로 출력시킨다면 문제가 간단하지만 2진수의 경우에는 해당 포맷이 없기 때문에 출력할 수 있는 방법이 없다. 위의 프로그램은 unsigned char유형의 문자를 입력받아 이의 코드값을 2진수 형태로 출력시키는 것으로 이는 앞의 선택적 리셋을 사용하면 된다.

맨 처음 비트를 출력하고자 하면 첫번째 비트를 제외한 나머지 비트를 모두 0으로 세팅한 다음에 오른쪽으로 7번 쉬프트하면 된다. 그러면 0000000X 형태가 되는데 이 X의 값이 바로 첫번째 비트의 값이 된다. 따라서 이 값을 출력시키면 첫번째 비트의 값을 출력하게 된다. 마찬가지로 2번째 비트의 값도 역시 2번째 비트의 값을 제외한 나머지를 0으로 세팅한 후 이를 오른쪽으로 6번 쉬프트하여 출력시키면 된다. 이렇게 출력시키다 맨 마지막 8번째 비트는 마지막 8번째 비트를 제외한 나머지 비트들을 0으로 리셋시킨 후 쉬프트할 필요없이 그대로 출력하면 된다.

⑶ 콤마 연산자

C에서는 ','가 두가지로 사용된다. 인자나 변수를 구분하기 위한 기호로도 사용되며 연산자로도 사용되는데 이때에는 다음과 같이 2진 연산자 형태로 사용한다.

수식1, 수식2

일단 수식1을 계산한다. 그리고 수식2를 계산한다. 그런데 계산 결과는 무조건 수식2가 된다. 왜냐하면 쉼표 연산자의 최종적인 형과 값은 맨 마지막에 계산된 수식의 결과에 따르기 때문이다.

예를 들어 (10 * 3 - 25, 7)의 경우 10 * 3 - 25를 먼저 계산한다. 그러면 5가 되는데 이는 계산만하고 그냥 버린다. 그리고 두번째 수식을 보면 7이므로 이의 계산 결과는 7이 된다.

main() {

int a, b, c, d, e;

int f = 5;

d = (a = 5, b = 3, c = 4);        /*  괄호는 ','가 연산자임을 나타내기 위해 사용  */

e = (f = 2, f + 3);

printf("a = %d, b = %d, c = %d, d = %d, e = %d, f = %d\n",a,b,c,d,e,f);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

a = 5, b = 3, c = 4, d = 4, e = 5, f = 2

d변수의 값을 보면 c변수가 가지고 있던 4를 가지고 있음을 확인할 수 있다. 즉 맨 마지막에 계산된 수식의 값이 쉼표 연산자의 최종 값이 된다. 그리고 e의 값은 5가 되는데 f의 값이 2로 바뀐 다음 f + 3의 결과가 e에 대입되기 때문이다.

※ 콤마 연산자와 콤마

C에서 콤마는 두가지 용도로 사용된다. 첫째는 인자나 변수를 구분하는 역할을 하는 기호로, 둘째는 바로 콤마 연산자로 사용된다. 구분하는 기호로는 변수 선언할 때와 함수 호출할 때 인자를 구분하기 위해서만 사용되기 때문에 이를 콤마 연산자와 혼동할 염려는 거의 없다. 그런데 함수를 호출할 때 콤마를 사용하게 되면, 그 주위에 괄호가 있고 없고에 따라 그 의미가 완전히 달라지게 된다.

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

위의 printf문 내의 콤마는 인자들을 구분하는 의미로 사용된 것이다. 따라서 i와 j의 값이 제대로 출력되어 나오게 된다. 그런데 이를 다음과 같이 괄호로 둘러 싸게 되면 그 의미가 완전히 달라지게 된다.

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

즉 i,j의 콤마는 콤마 연산자로 해석되어 위의 printf문은 아래와 같은 의미를 갖게 된다.

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

⑶ sizeof 연산자

C에서만 볼 수 있는 연산자로 다음과 같은 형태로 사용한다.

sizeof 수식

sizeof (데이터 유형)

이 연산자는 기호를 사용하지 않고 특이하게 이름을 사용한다. 그리고 인자로 수식이나 데이터 유형의 이름이 올 수 있는데, 인자가 수식일 때에는 괄호를 붙이지 않아도 되지만 어떤 데이터 유형의 이름일 때에는 반드시 괄호를 붙여야 한다. 예를 들어 sizeof ++i에서 ++i는 수식이므로 괄호가 필요 없지만 sizeof(int)에서는 int가 데이터 유형의 이름이므로 괄호가 필요하다.

이 sizeof 연산자는 크기를 계산하는 연산자인데 수식의 경우에는 그 수식의 계산 결과의 데이터 유형의 크기를 계산하며 데이터 유형의 경우에는 그대로 데이터 유형의 크기를 계산한다. 계산은 바이트 단위로 계산한다. 따라서 IBM-PC의 경우 sizeof(int)는 2가 되며 sizeof(double)은 8이 된다. 그리고 sizeof("test")는 5가 되는데 스트링은 그 스트링의 길이 + 1이 되기 때문이다.

결합방향은 오른쪽에서 왼쪽이고 우선순위는 ++, --와 같다. 따라서 sizeof sizeof(double)은 sizeof(sizeof(double))로 처리되는데 이의 값은 2가 된다. 왜냐하면 sizeof(double)은 8이 되지만 8이란 값 자체는 int 유형이기 때문이다. 다음은 sizeof 연산자에 관한 몇 가지 예이다.

int i, j = 3;

char c;

long l;

sizeof i + j                  = 5       /*  sizeof(i) + j로 처리  */

sizeof (i + j)                = 2

sizeof i + sizeof j           = 4

sizeof sizeof c               = 2       /*  sizeof(1) 이 되므로 int 유형이 된다  */

main() {

printf("The size of data types is computed\n\n");

printf("       char = %2d byte\n",sizeof(char));

printf("  short int = %2d bytes\n",sizeof(short int));

printf("        int = %2d bytes\n",sizeof(int));

printf("   long int = %2d bytes\n",sizeof(long int));

printf("      float = %2d bytes\n",sizeof(float));

printf("     double = %2d bytes\n",sizeof(double));

printf("long double = %2d bytes\n",sizeof(long double));

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

The size of data types is computed

char =  1 byte

short int =  2 bytes

int =  2 bytes

long int =  4 bytes

float =  4 bytes

double =  8 bytes

long double = 10 bytes

다음은 지금까지 나온 모든 연산자들의 우선순위와 결합방향을 나타낸 것이다.

( ) [ ] -> .	왼쪽 → 오른쪽
! ~ ++ -- + - * & (type) sizeof	오른쪽 → 왼쪽
* / %	왼쪽 → 오른쪽
+ -	왼쪽 → 오른쪽
<< >>	왼쪽 → 오른쪽
< <= > >=	왼쪽 → 오른쪽
== !=	왼쪽 → 오른쪽
&	왼쪽 → 오른쪽
^	왼쪽 → 오른쪽
|	왼쪽 → 오른쪽
&&	왼쪽 → 오른쪽
||	왼쪽 → 오른쪽
? :	오른쪽 → 왼쪽
= += -= *= /= %= &= ^= |= <<= >>=	오른쪽 → 왼쪽
,	왼쪽 → 오른쪽

Ⅴ. 제어구조와 루프

< if문과 블럭 >

C의 if문은 다음과 같은 형태로 사용한다.

if (조건식)     /*  조건식을 둘러싸고 있는 괄호는 반드시 있어야 한다  */

문장1;

[else           /*  [ ]로 둘러싼 부분은 생략할 수 있음을, 즉 없어도 됨을 의미한다  */

문장2; ]

조건식이 참이면(0이 아니면) 조건식 뒤의 문장1을 수행하고 if 문 다음 문장을 수행한다. 조건식의 값이 거짓이면(0이면) 문장2를 수행하고 if 문 다음 문장을 수행한다. 만약 문장2가 없을 경우(즉 [ ]로 둘러싼 부분이 생략되었을 때)에는 그냥 if 문 다음 문장을 수행한다.

다음 if 문의 경우 문장1과 문장2는 반드시 하나의 문장이 와야 하는데 다음과 같이 문장1에 여러개의 문장이 오게 되면

if (x > 0)

x += 1;

printf("%d\n",x);

else

x -= 1;

엉뚱하게 else에서 에러가 발생하게 된다. 이는 x += 1;로 일단 문장1이 되었다고 보고 그 다음에 else가 오는지를 보는데 printf가 왔으므로 else가 생략된 것으로 알게 된다. 그래서 완전히 if문이 끝났다고 보는데 갑자기 else가 오니까 if 문 없이 else가 온 것으로 간주해 에러가 발생하게 된다. 이때는 블럭을 사용하면 되는데 블럭은 하나의 문장으로 간주되기 때문에 다음과 같이 아무 문제 없이 원하는 결과를 얻을 수 있다.

if (x > 0)

{ x += 1;

printf("%d\n",x); }     /*  블럭의 끝에 ';'(널 문장)을 붙여서는 안된다  */

else

x -= 1;

문장2에 여러 개의 문장이 와야 할 경우도 역시 마찬가지로 블럭을 사용하면 된다.

if (x > 0)

x += 1;            /*  else 앞에 ';'이 생략되면 안된다  */

else

{ ++y;

printf("x = %d, y = %d\n",x,y); }

x += y;

x의 값이 0보다 크면 x += 1;을 수행하고 x += y;를 수행하게 되지만 x의 값이 0과 같거나 0보다 작게 되면 ++y; printf("x = %d, y = %d\n",x,y);를 수행하고 x += y;를 수행하게 된다.

if - else문의 else의 앞이 여러 문장이 아니면 즉 블럭으로 되어 있지 않으면 위 보기에서와 같이 else 앞에 세미콜론이 꼭 들어가게 된다.

블럭 내에 또 다른 블럭이 존재할 수 있는데 이 경우 블럭이 중첩되었다고 한다. 다음은 중첩된블럭의 예이다.

{ int i = 1;          □□□□□□□□□□□

++i;                          □

{ int i = 5;        □□□□□□□   □

i += 3;                 □   □

{ int j = 3;      □□□   □   □

i += j;           □   □   □

printf("%d",i); □□□   □   □

}                ⑶   □   □

printf("%d",i);         □   □

}                 □□□□□□□   □

printf("%d\n",i);    ⑵       □

}                   □□□□□□□□□□□

⑴

위의 프로그램을 보면 맨 바깥쪽 블럭⑴에 변수 i가 선언되어 있다. 그런데 그 다음 블럭⑵에서도 역시 같은 이름의 변수 i를 선언하여 사용하고 있다(블럭이 중첩되었을 때 안의 블럭에서 바깥쪽의 블럭의 변수와 같은 이름의 변수를 선언할 수 있다). 따라서 블럭⑵에서는 블럭⑴의 i의 값을 사용할 수 없게 되며 블럭⑵의 i는 자신이 선언한 i가 된다(블럭 내에 선언된 변수들은 그 블럭 내에서만 사용할 수 있다).

그러면 블럭⑶의 i는 어느 것이 되는가? 블럭⑶에는 i란 변수가 선언되어 있지 않으므로 결국 자신을 둘러싼 블럭의 i 값을 찾게 되는데 이때 자신으로부터 가장 가까운 곳에 있는 블럭에 선언된 변수값을 사용한다. 블럭⑶의 입장에서 보면 블럭⑵가 블럭⑴보다 당연히 가까운 곳에 있으므로 블럭⑶의 i는 당연히 블럭⑵의 i가 된다.

따라서 위의 프로그램은 다음 프로그램과 똑같은 의미를 갖는다.

{ int i = 1;          □□□□□□□□□□□

++i;                          □

{ int k = 5;        □□□□□□□   □   /*  서로 다른 변수에는 다른 이름을 사용  */

k += 3;                 □   □   /*  하는 것이 좋은 방법이다  */

{ int j = 3;      □□□   □   □

k += j;           □   □   □

printf("%d",k); □□□   □   □

}                ⑶   □   □

printf("%d",k);         □   □

}                 □□□□□□□   □

printf("%d\n",i);    ⑵       □

}                   □□□□□□□□□□□

⑴

하나 더 알아 둘 것은 main()이라고 하는 것 자체도 하나의 블럭이라는 것이다. main()은 반드시 '{'로 시작해서 '}'로 끝나기 때문이다. 따라서 main() 내에서 선언한 변수들은 그 안에서만 사용할 수 있게 된다. 이 내용은 다른 함수들을 정의하게 되면 아주 중요한 의미를 갖게 된다.

다음은 이의 한 예이다.

main() {

int i = 10, j = 2;

i += j++;

printf("i = %d, j = %d\n",i,j);       /*  ⑴  */

{ int i = 2;

i += j;

j -= i;

printf("i = %d, j = %d\n",i,j);     /*  ⑵  */

{ int j = 5;

j *= i + 1;

++i;

printf("i = %d, j = %d\n",i,j);   /*  ⑶  */

}

printf("i = %d, j = %d\n",i,j);     /*  ⑷  */

}

printf("i = %d, j = %d\n",i,j);       /*  ⑸  */

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

i = 12, j = 3

i = 5, j = -2

i = 6, j = 30

i = 6, j = -2

i = 12, j = -2

위의 프로그램에서 ⑴의 printf 문의 경우에는 main의 i, j값이 되므로 i = 12, j = 3이 된다(++가 뒤에 붙었으므로 ++는 나중에 계산된다). ⑵의 경우에는 i는 블럭 내에 선언된 값을, j는 main에선언된 값을 사용하므로 i = 5, j = -2가 된다. ⑶의 경우에는 i는 첫번째 블럭의 값을 j는 자신이 선언한 값을 사용하므로 i = 6, j = 30이 된다. ⑷의 경우에는 다시 자신의 i와 main의 j를 사용하므로 i = 6, j= -2가 되며 ⑸의 경우에는 main의 i, j를 사용하므로 i = 12, j = -2가 된다.

if 문 자체도 하나의 문장이기 때문에 if 문이 문장1이나 문장2로 올 수가 있다. 이때 문장1로 if 문이 오고 그 뒤에 else가 오면 이 else가 어느 if 문에 속한 것인지 모호해질 수가 있다. 예를 들어 다음 프로그램에서

if (a > 0)

if (b > 0)

z = a;

else

z = b;

else가 첫번재 if에 대한 else인지, 아니면 두번째 if에 대한 else인지 헷갈리게 된다. 이때 C에서는 이 else를 가장 가까운 if에 속한 것으로 간주한다. 위의 프로그램의 경우는 두번재 if가 첫번째 if 보다 else에 가까운 위치에 있으므로 두번째 if에 대한 else가 되며 따라서 다음과 같은 의미가 된다.

if (a > 0)

if (b > 0)

z = a;

else

z = b;

그리고 else를 첫번재 if에 대한 else로 하려면 이 경우에도 블럭을 사용하면 된다.

if (a > 0)

{ if (b > 0)

z = a; }

else

z = b;

main() {

int i;

printf("Input an integer: ");

scanf("%d",&i);

if (i >= 0) {

if (i % 2 == 0)                 /*  if 다음에 if - else 구조가 온 경우  */

printf("Even number\n");      /*  if - else 구조를 블럭으로 싸는 것이  */

else                            /*  좋은 프로그래밍 방법이다  */

printf("Odd number\n");

}

else

printf("Negative number\n");

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input an integer: 56 □

Even number

반면에 문장2에 if 문이 올 수도 있는데 이 경우 다음과 같이 else if가 형성되게 된다.

if (a > 0)

++a;

else

if (b > 0)

--a;

위의 형태는 다른 프로그래밍 언어의 ELSE IF와 같은 역할을 한다. 그러나 C에서는 else if라는 특별한 구조가 있는 것이 아니고 단지 else 다음의 문장으로 if 문이 온 것이며 계속 else 다음의 문장으로 if 문이 올 수 있다.

main() {

int sc;

printf("Input score: ");

scanf("%d",&sc);

if (sc > 100 || sc < 0)        /*  일반적으로 에러 조건부터 배제하는 것이  */

printf("Data Error\n");      /*  더 간단하게 문제를 해결할 수 있다   */

else if (sc >= 90)

printf("A\n");

else if (sc >= 80)

printf("B\n");

else if (sc >= 70)

printf("C\n");

else if (sc >= 60)

printf("D\n");

else

printf("F\n");

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input score: 96 □

A

main() {

int i = 2, j = 4;

if (i * j > i + j)

i += j -= 3;

else

i *= j /= 3;

if (i == j);

++i;

if (++i >= ++j * 3)

j += 2;

else ;

--i;

if (i > 0)

if (i == 2 * j - 1)

i += j;

else

i -= j;

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

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

i = 2, j = 2

위의 프로그램에서 우선 첫번째 if 문을 보면 i * j > i + j가 8 > 6으로 참이므로 i += j -= 3;이 수행된다. 따라서 i = 3, j = 1이 된다. 그 다음 if 문을 보면 조건이 i == j이므로 이는 거짓인데 이에 해당하는 else가 없으므로 다음 문장으로 넘어가야 한다. 그런데 if (i == j)의 바로 다음에 ';'이 있는데 C에서는 수식에 ';'만 붙이면 다 문장이 된다(이를 널 문장이라 부른다). 그래서 if 문의 문장은 이 널 문장으로 끝난것이 된다.

따라서 ++i;가 if 문 다음의 문장이 되어서 이를 수행하게 되고 그 결과 i = 4, j = 1이 된다. 그 다음의 if 문도 마찬가지인데 ++i >= ++j * 3에서 5 > 6이 되므로(이때 i = 5, j = 2가 된다) 역시 거짓이라서 else 다음의 문장을 수행해야 하는데 else 다음의 문장도 역시 ';', 즉 널 문장이므로 할 것이 없다. 마찬가지로 if 다음 문장인 --i를 수행하면 i = 4, j = 2가 된다. 그 다음의 if 문의 조건인 i > 0은 참이므로 그 다음 문장을 수행하는데 여기에 또 if 문이 왔으므로 다시 조건을 조사하여 i == 2 * j - 1이고 이는 거짓이므로 else 다음의 문장을 수행해(이 else는 앞에서 설명한 바와 같이 바로 두번째 if에 대한 else이다).  i -= j;를 수행하면 i = 2, j = 2가 된다. 이것이 최종 결과가 된다.

※ 조건 연산자와 if 문의 비교

조건 연산자를 이용하면 if 문을 더 간단히 고쳐 쓸 수가 있다.

if (count > 10)            ∃ result = (count > 10 ? count * 100 : count * 80);

result = count * 100;

else

result = count * 80;

조건 연산자로 else if 문도 간단히 할 수가 있다.

if (num < 10)              ∃ numsize = num < 10 ? 1 :

numsize = 1;                          num < 100 ? 2 :

else if (num < 100)                     num < 1000 ? 3 : 4

numsize = 2;

else if (num < 1000)

numsize = 3;

else

numsize = 4;

< while과 do while문 >

while 문과 do while 문은 조건식이 참일 동안 반복을 수행하게 하는 제어문이다. while 문의 사용법은 다음과 같다.

while (조건식)      /*  조건식의 괄호는 반드시 있어야 한다  */

문장1;

우선 조건식을 계산하는데 조건식의 계산 결과가 참이면(0이 아니면) 문장1을 수행하고 다시 조건식을 계산하는데 계산한 결과가 거짓이면(0이면) while 문 다음의 문장으로 간다. 즉, 조건식이 참인 동안 계속해서 문장1을 수행하게 되며 처음부터 조건식의 값이 거짓이면 문장1은 한번도 실행되지 않는다.

c = 'A';

while (c <= 'Z')

putchar (c++);     /*  반드시 하나의 문장만 와야 하며 여러 개의 문장을  */

putchar('\n');       /*  반복 하고자 할 때에는 블럭을 사용한다  */

위 프로그램은 c값을 조사해서 그 값이 'Z'보다 작거나 같으면 c를 출력시키고 c값을 하나 증가시킨다. 증가된 c값을 다시 'Z'와 비교해서 작거나 같으면 다시 c를 출력시키고 값을 하나 증가시킨다. c의 처음 값이 'A'이므로 결국은 A부터 Z까지 출력을 해주게 된다. 'Z'까지 출력을 한 후에는 '\n'을 출력하는 다음 문장을 실행하게 된다.

while 문이 유용한 것은 어떤 조건이 만족되는 동안 계속해서 똑같은 일을 할 때이다. 예를 들어 정수 데이터가 계속 입력되는데 -1이 입력될 때까지(데이터의 끝을 나타내는 표시라고 하면) 이를 계속 읽어 전체의 합을 구하고자 한다면 다음과 같이 할 수 있다.

sum = 0;

scanf("%d",&data);

while (data != -1) {

sum += data;

scanf("%d",&data);

}

위 프로그램을 연결된 AND 형태로 처리하면 다음과 같이 더 간단하게 만들 수도 있다.

sum = 0;

while (scanf("%d",&data) && data != -1)

sum += data;

※ 다음과 같은 경우에는 주의를 해야 한다.

while (--i)

printf("%d\n",i);

이 프로그램의 의도는 현재의 i값부터 1까지 출력을 시켜주는 것인데 만약에 i의 값이 처음부터 0이하일 때 문제가 발생하게 된다. i값이 0보다 작은 값이면 그 값을 아무리 감소시켜도 0이 되지 않기 때문이다(사실은 integer형 변수이면 그 값이 -32768까지 감소한 후에 32767로 되어버리고 그 후에는 그 값이 계속 감소해서 0까지 도달할 수 있을 것이다). 이처럼 조건식의 값이 거짓이 되는지에 대한 여부를 확실히 하지 않으면 엉뚱한 결과를 초래하게 된다. 이와 같은 경우에는 i값이 0이하이면 아예 실행하지 않도록 해주는 것이 좋을 것이다. 그러기 위해서는 위 프로그램을 아래와 같이 고쳐야 한다.

while (--i > 0)

printf("%d\n",i);

다음에 나오는 프로그램들은 while 문에 관한 예이다.

main() {

char c;

scanf("%c",&c);

while (c != 'Q') {       /*  읽어들인 문자가 'Q'가 아닌 동안  */

printf("%c",c);        /*  읽어들인 문자를 출력  */

scanf("%c",&c);        /*  다시 다음 문자를 읽어들임  */

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

EO Computer QSystem □

EO Computer

scanf 대신에 getchar를 사용할 수 있는데, 이 경우 다음과 같이 할 수 있다.

#include <stdio.h>

main() {

char c;

c = getchar();

while (c != 'Q') {

putchar(c);

c = getchar();

}

}

또는 다음과 같이 바꿀 수도 있다.

#include <stdio.h>

main() {

char c;

while ((c = getchar()) != 'Q')

putchar(c);

}

#include <stdio.h>

main() {

char c;

while ((c = getchar()) != '\n')    /*  읽어들인 문자가 '\n'이 아닐 동안  */

if (c >= 'a' && c <= 'z')        /*  읽어들인 문자가 소문자이면  */

putchar(c-32);                 /*  대문자로 출력(소문자와 대문자의 코드값이  */

/*  32가 차이나므로 소문자에서 32를 빼면 된다)  */

else                             /*  소문자가 아니면  */

putchar(c);                    /*  그대로 출력  */

putchar('\n');

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

This is test □

THIS IS TEST

#include <stdio.h>

main() {

char c;

int i = 0;

/*  C에서는 화일의 끝을 나타내는 기호로 EOF를 사용  */

while ((c = getchar()) != EOF)   /*  하나 읽어들인 문자가 화일의 끝이 아닐 동안  */

++i;                           /*  '\n□(<Enter>키)도 문자로 포함된다  */

printf("Total number of characters = %d\n",i);

}

위 예에서 char 변수 c는 값을 읽어들이기만 했지 사용하지는 않고 있다. 즉 불필요한 변수가 되는데, 이를 아예 사용하지 않고 다음과 같이 작성할 수도 있다.

#include <stdio.h>

main() {

int i = 0;

while (getchar() != EOF)

++i;

printf("Total number of characters = %d\n",i);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

1234567890 □

abcdefghijklmnopqrstuvwxyz □

^Z □

Total number of characters = 38

#include <stdio.h>

main() {

char c;

long int nc = 0l;                     /*  문자의 수를 기억  */

int nl = 0;                           /*  라인의 수를 기억  */

while ((c = getchar()) != EOF) {

++nc;

if (c == '\n')                      /*  라인을 다 읽었으면  */

++nl;                             /*  라인의 수를 증가  */

}

printf("%ld characters, %d lines\n",nc,nl);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

1234567890 □

abcdefghijklmnopqrstuvwxyz □

^Z □

38 characters, 2 lines

#include <stdio.h>

main() {

char c;

int begin = 1;                      /*  맨 처음 시작인지를 나타냄  */

while ((c = getchar()) != EOF)      /*  문자 하나를 읽어서 그것이 화일의 끝이 아니면  */

if (c != '\n' || begin == 0) {    /*  그것이 라인의 끝이 아니거나 라인의 끝이라도

맨 처음 시작이 아니면  */

if (begin == 1)                 /*  맨 처음 시작이면  */

begin = 0;                    /*  시작이 아니라고 표시  */

if (c == '\n')                  /*  라인이 끝이면  */

begin = 1;                    /*  새로 라인이 시작된다고 표시  */

putchar(c);

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>line < readme □

위 프로그램은 화일을 끝까지 읽어 그 중 빈(<Enter> 문자 하나만 있는) 라인을 제외한 나머지를 그대로 출력하는 것으로 리다이렉션('<' 기호)을 이용하여 기존의 화일을 데이터로 입력하도록 하고 있다.

#include <stdio.h>

main() {

long int res = 0l;

char c;

while ((c = getchar()) == '0' || c == '1')      /*  2진수인 동안  */

res = res * 2l + c - '0';                     /*  읽은 수를 10진수로 변환  */

printf("Dec = %ld,  Oct = %lo,  Hex = %lx\n",res,res,res);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

10110101 □

Dec = 181, Oct = 265, Hex = b5

do while 문의 사용법은 다음과 같다.

do

문장1;

while (조건식)      /*  조건식의 괄호는 생략할 수 없다  */

일단 무조건 문장1을 수행한다. 그리고 조건식을 계산하여 계산결과가 참이면 다시 문장1을 수행하고 조건식의 계산 결과가 거짓이면 다음 문장을 수행한다. 그러므로 조건식의 값이 처음부터 거짓이라도 문장1의 부분이 한번은 실행된다. 이점만 빼고는 while 문과 동일하다.

do while 문은 일단 한 번은 수행해야만 하는 경우에 사용하면 편리하다. 모든 do while 문은 while 문으로 변환할 수 있고 또 조건을 처음부터 조사하는 경우가 많기 때문에 do while 문은 별로 사용되지 않는다.

다음에 나오는 프로그램들은 do while 문에 관한 예이다.

#include <stdio.h>

main() {

char c;

do {

c = getchar();

if (c >= 'a' && c <= 'z')                     /*  읽은 문자가 소문자이면  */

putchar(c-32);                              /*  대문자로 출력  */

else if (c >= 'A' && c <= 'Z' && c != 'Q')    /*  대문자고 'Q'가 아니면  */

putchar(c+32);                              /*  소문자로 출력  */

else

putchar(c);                                 /*  읽은 문자를 출력('Q'도 포함)  */

} while (c != 'Q');

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>line < readme □

#include <stdio.h>

main() {

char c;

do

c = getchar();

while (c != '%' && c != EOF);        /*  '%'와 화일의 끝이 아닌 동안  */

if (c != EOF) {                      /*  화일의 끝이 아니면  */

c = getchar();                     /*  한 글자를 읽어들임  */

while (c != EOF && c != '%') {     /*  그것이 화일의 끝이 아니고 '%'가 아니면  */

putchar(c);                      /*  그대로 출력  */

c = getchar();                   /*  다음 글자를 읽어들임  */

}

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Using %BorlandC will help you learn how to use% C-language □

BorlandC will help you learn how to use

위 예제는 사용자가 입력한 문자들 가운데 처음 '%' 문자부터 그 다음 '%' 문자 또는 화일의 끝까지의 사이에 있는 문자들을 그대로 출력하는 프로그램으로 사용자가 직접 입력하지 않고 기존의 화일을 리다이렉션을 이용하여 입력할 수도 있다.

main() {

int sc;

do {

printf("Input score (0 for stop): ");

scanf("%d",&sc);

printf("Score : %2d   Grade : ",sc);

if (sc > 100 || sc < 0)

printf("Error\n");

else if (sc >= 90)

printf("A\n");

else if (sc >= 80)

printf("B\n");

else if (sc >= 70)

printf("C\n");

else if (sc >= 60)

printf("D\n");

else

printf("F\n");

} while (sc);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input score (0 for stop): 96 □

Score : 96   Grade : A

Input score (0 for stop): 0 □

Score :  0   Grade : F

< for문 >

C의 for문은 다음과 같은 형태로 사용한다.

for (수식1 ; 수식2 ; 수식3)   /*  괄호는 생략해서는 안되며 수식은 생략할 수 있지만  */

문장1;                     /*  수식들을 구분하는 ';' 2개는 생략해서는 안된다  */

수식1 부분에는 제어변수의 초기값을 설정하는 초기식이 들어가고 수식2 부분에는 루프를 계속 실행시킬 것인지의 조건식이 들어가며 수식3에는 제어변수를 변경시키는 증감식이 들어간다. 즉 수식2의 값이 참인 동안 계속해서 문장1을 수행한다.

다음의 예제는 for 문이 실행되는 순서를 알아보는 프로그램이다.

main() {

int i;

for (printf("expression1"), i = 0;       /*  for 문에서의 콤마는 구분하는 역할을  */

printf("\n\texpression2"), i < 3;   /*  하는 것이 아니라 연산자이다  */

printf("\n\t\texpression3"), i++)   /*  좌우를 다 계산하고 결과는 무조건 오른쪽  */

printf("%10d",i);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

expression1

expression2         0

expression3

expression2         1

expression3

expression2         2

expression3

expression2

이는 while 문과 비슷한데 다른 점은 문장1이 수행될 때마다 수식3도 같이 계산된다는 점이다.

for (수식1 ; 수식2 ; 수식3)

문장1;

다음의 while 문과 같은 의미를 갖는다.

수식1;

while (수식2) {

문장1;

수식3;

}

반면에 다음의 while 문은

while (1)

문장1;

다음의 for 문으로 변경할 수 있다.

for ( ; ; )     /*  수식2가 생략되면 무조건 참으로 간주한다  */

문장1;

while 문으로 할 수 있는 것은 모두 for 문으로 할 수 있고, 또 for 문으로 할 수 있는 것은 역시 마찬가지로 while 문으로 할 수 있는데 while 문은 보통 특정 조건이 만족할 때까지 반복해서 해야할 때 많이 사용되고 for 문은 100번이나 1부터 100까지와 같이 특정 횟수 만큼 반복하거나 어떤 범위의 수해 대해 같은 연산을 수행하고자 할 때 주로 사용된다.

다음은 for 문에 관한 예제들이다.

main() {

int i, sum;

for (sum = 0, i = 1; i <= 100; i++)

sum += i;

printf("sum = %d\n",sum);

}

1부터 100까지의 합을 구하는 프로그램으로 위의 for 문은 다음과 같이 더 간단하게 할 수 있다.

main() {

int i, sum;

for (sum = 0, i = 1; i <= 100; sum += i++);   /*  ';'는 널 문장으로 for 문 다음에는  */

/*  반드시 하나의 문장이 와야 한다  */

printf("sum = %d\n",sum);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

sum = 5050

main() {

int i, n, m;

printf("Input N and M: ");

scanf("%d%d",&n,&m);

for (i = n; i <= m; i++)    /*  N에서 M까지  */

if (i % 2 == 0)           /*  2로 나누어 떨어지면  */

printf("%10d",i);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input N and M: 2 20 □

2          4          6          8         10         12         14         16

18         20

다음은 N개의 숫자를 읽어 들여 이들 중 최대값과 최소값, 그리고 합과 평균을 계산하여 출력하는 프로그램이다.

main() {

int n;                     /*  데이터의 갯수를 기억  */

int i;                     /*  반복을 위한 카운터 변수  */

long int sum;              /*  합을 기억  */

long int max;              /*  최대값을 기억  */

long int min;              /*  최소값을 기억  */

long data;                 /*  읽어들인 데이터를 기억  */

printf("Input number of data: ");

scanf("%d",&n);

if (n == 0)                /*  데이터의 갯수가 0이면 할일이 없음  */

printf("No data\n");

else if (n < 0)            /*  데이터의 갯수가 음수이면 에러  */

printf("Error: Negative Data Number\n");

else {

printf("Input 1 data -> ");

scanf("%ld",&data);             /*  데이터 하나를 읽어 이를  */

sum = max = min = data;         /*  최대값과 최소값으로 만듦  */

for (i = 2; i <= n; i++) {      /*  하나 읽었으므로 2번째부터 N까지  */

/*  데이터의 갯수가 1인 경우 for 문의 조건이  */

/*  거짓 이므로 바로 printf 문이 출력된다  */

printf("Input %d data -> ",i);

scanf("%ld",&data);

if (data > max)          /*  지금 읽어들인 것이 현재의 최대값 보다 크면  */

max = data;            /*  그것이 바로 최대값이 된다  */

if (data < min)          /*  지금 읽어들인 것이 현재의 최소값 보다 작으면  */

min = data;            /*  그것이 바로 최소값이 된다  */

sum += data;             /*  읽어들인 데이터를 더해 합을 구한다  */

}

printf("The maximum is %ld\n",max);           /*  결과를 출력  */

printf("The minimum is %ld\n",min);

printf("The sum is %ld\n",sum);

printf("The average is %.2lf\n",(double)sum/(double)n);

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input number of data: 3 □

Input 1 data -> 78 □

Input 2 data -> 89 □

Input 3 data -> 90 □

The maximum is 90

The minimum is 78

The sum is 257

The average is 85.67

다음은 N이란 숫자를 읽어 들여 1부터 N까지의 소수를 출력하는 프로그램이다.

main() {

int number;                /*  소수인지 조사 하는 수  */

int divisor;               /*  나누는 수  */

int n;                     /*  소수의 한계  */

int count = 0;             /*  현재까지 발견된 소수의 수  */

printf("Input limit -> ");

scanf("%d",&n);

if (n <= 1)                /*  N은 2 이상이어야 한다  */

printf("You should input number >= 2\n");

else

for (number = 2; number <= n; number++) {                 /*  2부터 N까지 조사  */

for (divisor = 2; number % divisor != 0; divisor++);    /*  나누어지는 수가

있는지를 조사  */

if (divisor == number) {                  /*  나누어지는 수가 자기 자신이면  */

printf("%7d",number);

if (++count % 10 == 0)                  /*  10개의 데이터가 출력되었으면  */

printf("\n");                         /*  다음 라인으로  */

}

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input limit -> 25 □

2      3      5      7     11     13     17     19     23

다음은 앞에서 비트 단위 연산자만을 사용하여 만든 프로그램을 for 문을 사용하여 다시 만든 것으로 입력한 10진수의 코드 값을 16진수와 8진수, 그리고 2진수로 출력시키고 있다.

main() {

int i, x;

printf("Input number -> ");

scanf("%d",&x);

printf("Dec(%d) -> Hex = %X, Oct = %o,  Bin = ",x,x,x);

for (i = 15; i >= 0; i--)

printf("%d",(x >> i) & 0x01);

printf("\n");

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input number -> 2525 □

Dec(2525) -> Hex = 9DD,  Oct = 4735,  Bin = 0000100111011101

< switch문과 break >

switch 문은 어떤 값에 따라 선택적으로 해당하는 실행을 해야할 때 사용하는 문장으로 다음과 같이 사용한다.

switch (수식) {                /*  수식은 반드시 괄호로 둘러 싸여 있어야 한다  */

[ case 상수1 : ] [ 문장1 ]     /*  수식에 해당하는 부분은 정수값을 갖는 식이나  */

[ case 상수2 : ] [ 문장2 ]     /*  변수가 와야하며 case 뒤에 오는 값은  */

:             /*  상수이거나 상수식이어야 한다  */

[ case 상수n : ] [ 문장n ]

[ default :        문장 ]

}

먼저 괄호안의 수식을 계산하여 이 수식의 계산 결과와 같은 상수가 case 다음에 있는지 위에서 부터 차례로 찾는다. 같은 상수를 발견했으면 그 옆에 있는 문장부터 차례로 switch 문의 끝까지 수행하고 옆에 문장이 없으면 그 다음에 있는 문장부터 차례로 끝까지 수행한다.

같은 상수가 없으면 default : 옆에 있는 문장부터 차례로 switch 문의 끝까지 수행하는데 그 옆에 문장이 없으면 그 다음에 있는 문장부터 차례로 끝까지 수행한다.

같은 상수가 없고 default : 로 시작하는 문장도 없으면 아무것도 수행하지 않고 switch 문 다음의 문장을 수행한다.

main() {

int i;

printf("Input a number(1 ~ 3): ");

scanf("%d",&i);

switch (i) {                  /*  괄호 안의 수식이 정수 데이터 유형이 아닌 경우에는  */

/*  자동적으로 정수 데이터 유형으로 변환되게 된다  */

case 1: printf("ONE\n");

case 2: printf("TWO\n");

case 3: printf("THREE\n");

default: printf("Error\n");

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input a number: 2 □

TWO

THREE

Error

위 프로그램을 보면 수식을 계산한 후 같은 값의 상수 옆의 문장만 수행하는 것이 아니라 그 뒤에 있는 문장 까지 모조리 다 수행하고 있다.

수식과 일치되는 것이 있으면 그 옆에 있는 문장만 수행하고 그 뒤의 것은 수행하지 않도록 하기 위해서는 break 문을 사용하면 되는데 switch 문 내에서 break 문을 만나면 무조건 switch 문을 빠져나가 switch 문 다음 문장을 수행하게 된다. 따라서 위 프로그램에서는 각 case에 해당하는 문장들이 끝날 때마다 break 문을 넣게 되면 원하는 결과를 낳을 수 있다.

break 문은 switch 문 이외에도 for 문과 while 문, 그리고 do while 문 내에서도 사용할 수 있으며 그 외 다른 곳에서 break 문을 사용하면 컴파일 에러가 발생한다.

main() {

int i;

printf("Input a number(1 ~ 3): ");

scanf("%d",&i);

switch (i) {

case 1: printf("ONE\n");

break;

case 2: printf("TWO\n");

break;

case 3: printf("THREE\n");

break;

default: printf("Error\n");    /*  default는 일반적으로 끝에 오지만 중간에 와도  */

}                              /*  상관은 없으며 이 경우 끝나는 지점에 break를  */

}                                /*  두어야 한다  */

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input a number: 2 □

TWO

모든 switch 문은 if 문으로 변환할 수 있지만

switch (n) {

case 1:                   ∃  if (n >= 1 && n <= 5)

case 2:                         result = n;

case 3:                       else if (n >= 6 && n <= 10)

case 4:                         result = n - 5;

case 5: result = n;

break;

case 6: case 7: case 8: case 9:

case 10: result = n - 5;

break; }

다음과 같이 실행해야 할 범위가 일정하지 않고 뒤죽박죽일 때에는 switch 문을 사용하는 것이

훨씬 편리하다.

switch (n) {

case 1:                   ∃ if (n >= 1 && n <= 3 || n == 5 || n == 7 || n == 11)

case 2:                        result = n;

case 3:                      else if (n == 4 || n == 6 || n >= 8 && n <= 10)

case 5:                        result = n - 5;

case 7:

case 11: result = n;

break;

case 4: case 6: case 8: case 9:

case 10: result = n - 5;

break; }

switch 문이 if 문 보다는 훨씬 더 프로그램을 이해하기 쉽게 만든다.

switch 문을 사용할 때 같은 상수가 여러 개의 case 다음에 오지 않도록 주의해야 한다.

switch (n) {

case 10: ++x;

break;

case 20: --y;

break;

case 10: ++z;       /*  앞서 10이란 상수가 나왔기 때문에 컴파일 에러가 발생한다  */

무한 순환을 빠져나가고자 할 때 break 문을 사용하면 편리한데 다음은 이의 한 사용 예이다.

main() {

long int left, right;               /*  두 개의 정수값을 기억  */

long int res;                       /*  승을 계산할 때 중간 결과를 기억  */

long int i;                         /*  카운터 변수  */

char op;                            /*  연산자  */

while (1) {                         /*  무한 순환  */

printf("Input Expression -> ");

scanf("%ld",&left);                    /*  처음 숫자를 읽음  */

scanf("%c",&op);                       /*  연산자를 읽음  */

if (op == 'Q' || op == 'q') break;     /*  연산자가 'Q'나 'q'일 경우 무한 순환을  */

/*  빠져나가 수행이 종료  */

scanf("%ld",&right);                   /*  두번째 숫자를 읽음  */

switch (op) {

case '+': printf("%ld + %ld = %ld\n",left,right,left+right);

break;

case '-': printf("%ld - %ld = %ld\n",left,right,left-right);

break;

case '*': printf("%ld * %ld = %ld\n",left,right,left*right);

break;

case '/': printf("%ld / %ld = %ld\n",left,right,left/right);

break;

case '%': printf("%ld %% %ld = %ld\n",left,right,left%right);

break;

case '^': if (right < 0)

printf("Error: negative exponent\n");

else {

res = 1l;

for (i = 1l; i <= right; i++)

res *= left;

printf("%ld ^ %ld = %ld\n",left,right,res);

}                        /*  switch 문 내에 쓰인  break 문은  */

break;                     /*  swithc 문 까지만 벗어날 수 있다  */

default : printf("Error: Unknown operator\n");

}

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input Expression -> 1234*1234 □

1234 * 1234 = 1522756

Input Expression -> Q □

break 문은 자신이 속한 switch 문이나 for, while, do while 문이 여러 개 있을 때 가장 가까운 곳에 있는 것 하나만 빠져 나간다.

다음은 switch 문에 관한 프로그램 이다.

#include <stdio.h>

main() {

char c;                 /*  문자 하나를 기억  */

int a = 0;              /*  □a' 또는 'A'의 갯수를 기억  */

int e = 0;              /*  □e' 또는 'E'의 갯수를 기억  */

int i = 0;              /*  □i' 또는 'I'의 갯수를 기억  */

int o = 0;              /*  □o' 또는 'O'의 갯수를 기억  */

int u = 0;              /*  □u' 또는 'U'의 갯수를 기억  */

int n = 0;              /*  번호의 갯수를 기억  */

while ((c = getchar()) != EOF)        /*  읽어들인 것이 화일의 끝이 아닌 동안  */

switch (c) {

case 'A':

case 'a': ++a;          /*  읽어들인 문자가 'a' 또는 'A'인경우 a의 값을 1증가  */

break;        /*  switch 문을 빠져나감  */

case 'E':

case 'e': ++e;

break;

case 'I':

case 'i': ++i;

break;

case 'O':

case 'o': ++o;

break;

case 'U':

case 'u': ++u;

break;

case '1': case '2': case '3': case '4': case '5':

case '6': case '7': case '8': case '9':

case '0': ++n;

}

printf("     A -> %d\n",a);      /*  모음의 갯수를 출력  */

printf("     E -> %d\n",e);

printf("     I -> %d\n",i);

printf("     O -> %d\n",o);

printf("     U -> %d\n",u);

printf("Number -> %d\n",n);      /*  숫자의 갯수를 출력  */

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>aeiou < readme □

< exit, continue와 goto문 >

exit은 다음과 같은 형태로 사용한다.

exit(정수값);

프로그램을 수행하다가 위의 문장을 만나게 되면 무조건 프로그램의 수행을 중단하게 된다. exit 다음의 정수값은 MS-DOS에게 넘겨주는 값으로 에러가 나서 수행을 중단한 경우 보통 0이 아닌 값을 넘겨주게 된다.

main() {

int d1, d2, d3;             /*  한 줄에 3개의 변수를 읽어들일 변수들  */

int n;                      /*  전체 라인수를 기억할 변수  */

int i;

int sum;

printf("Input the number of data lines: ");

scanf("%d",&n);                                /*  전체 라인수를 읽어들임  */

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

printf("Input three numbers(0 ~ 100) -> ");

scanf("%d%d%d",&d1,&d2,&d3);

if (d1 > 100 || d1 < 0) {                 /*  데이터가 0에서 100사이에 있지 않으면  */

printf("Error in the first data -> %d\n",d1);

exit(1);                                /*  프로그램의 수행을 중단  */

}

if (d2 > 100 || d2 < 0) {

printf("Error in the second data -> %d\n",d2);

exit(1);

}

if (d3 > 100 || d3 < 0) {

printf("Error in the third data -> %d\n",d3);

exit(1);

}

sum = d1 + d2 + d3;

printf("    Sum = %d\n",sum);

printf("Average = %.2f\n",(float)sum/3.0f);

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input the number of data lines: 3 □

Input three numbers(0 ~ 100) -> 78 89 90 □

Sum = 257

Average = 85.67

Input three numbers(0 ~ 100) -> 68 87 120 □

Error in the third data -> 120

에러가 발생하여 프로그램의 수행을 중단하고자 할 때 exit 이외에 break, continue 또는 goto  문 등을 사용할 수 있는데 이 중 continue 문은 순환 제어구조 내에서 특정 조건이 만족되었을 경우 나머지 부분을 수행할 필요가 없을 때, 이를 건너뛰기 위한 목적으로 사용한다.

continue 문은 break 문과 같이 for, while, do while 문 내에서만 사용하며 그 외에서 사용하면 컴파일 에러가 발생한다. 또 자신을 둘러싼 for, while, do while 문이 여러 개 있을 때에는 가장 가까운 것에 대해서만 수행된다. 실제로 continue 문은 자신이 속한 for, while, do while 문의 맨 끝으로 이동하여 다음번 반복을 시작하도록 만드는 역할을 한다.

다음은 위의 프로그램을 continue 문을 사용하여 다시 작성한 것이다.

main() {

int d1, d2, d3;

int n;

int i;

int sum;

printf("Input the number of data lines: ");

scanf("%d",&n);

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

printf("Input three numbers(0 ~ 100) -> ");

scanf("%d%d%d",&d1,&d2,&d3);

if (d1 > 100 || d1 < 0) {

printf("Error in the first data -> %d\n",d1);

continue;      /*  자신이 속한 for 문의 맨 끝으로 이동하여 다음번 반복을 시작  */

}

if (d2 > 100 || d2 < 0) {

printf("Error in the second data -> %d\n",d2);

continue;      /*  자신이 속한 for 문의 맨 끝으로 이동하여 다음번 반복을 시작  */

}

if (d3 > 100 || d3 < 0) {

printf("Error in the third data -> %d\n",d3);

continue;      /*  자신이 속한 for 문의 맨 끝으로 이동하여 다음번 반복을 시작  */

}

sum = d1 + d2 + d3;

printf("    Sum = %d\n",sum);

printf("Average = %.2f\n",(float)sum/3.0f);

}                /*  continue 문을 만났을 때 이동하는 부분(for 문의 맨 끝)  */

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input the number of data lines: 3 □

Input three numbers(0 ~ 100) -> 78 89 90 □

Sum = 257

Average = 85.67

Input three numbers(0 ~ 100) -> 68 87 120 □

Error in the third data -> 120

Input three numbers(0 ~ 100) -> 92 38 87 □

Sum = 217

Average = 72.33

#include <stdio.h>

main() {

char c;

while ((c = getchar()) != 'Q') {      /*  하나 읽어 들인 것이 'Q'가 아닐 동안  */

if (c == ' ' || c == '\n')          /*  읽어 들인 문자가 ' '나 '\n' 이면  */

continue;                         /*  아무것도 하지 말고 다음 문자를 읽어 들임  */

printf("%c --> %d\n",c,c);

}

}

위 프로그램은 실제로 continue 문 대신에 다음과 같이 if 문을 사용하는 것이 더 간단한데 if 문이 복잡할 경우에는 continue 문이 더 간편한 경우가 많다.

#include <stdio.h>

main() {

char c;

while ((c = getchar()) != 'Q') {      /*  하나 읽어 들인 것이 'Q'가 아닐 동안  */

if (c != ' ' && c != '\n')          /*  읽어 들인 문자가 ' '나 '\n' 이 아니면  */

printf("%c --> %d\n",c,c);

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

This is test Q □

T --> 84

h --> 104

i --> 105

s --> 115

i --> 105

s --> 115

t --> 116

e --> 101

s --> 115

t --> 116

마지막으로 goto 문은 여러번 중첩된 루프를 한꺼번에 빠져나와야 할 경우에 사용하는데 이의 사용법은 다음과 같다.

goto 레이블;

.....

레이블: 문장;

goto 문은 어느 곳이든 갈 수가 있는데, 실제로 한 함수 안에서만 사용할 수 있으며 다른 함수에 있는 레이블로는 goto 문을 사용할 수 없다. 그리고 레이블은 블럭의 맨 끝에 올 수 없는데 레이블이 블럭의 맨 끝에 오게 될 경우에는 레이블 뒤에 널 문장을 써주어야 한다.

C에서 레이블은 숫자가 아니라 문자, 즉 변수 이름과 같은 식별자이며 미리 선언할 필요 없이 그냥 바로 사용하면 된다. 또 옆에 문장이 없이 레이블만 있을 수도 있으며 같은 문장에 두 개 이상의 레이블을 붙일 수도 있다.

다음은 1부터 100까지 출력하는 프로그램을 if 문과 goto 문 만으로 작성한 것이다.

main() {

int i = 1;

loop:              /*  레이블 옆에 문장이 없어도 레이블 다음에 나오는  */

if (i > 100)       /*  첫번째 문장에 레이블이 붙은 것으로 간주된다  */

goto end;

printf("%d\n",i);

++i;

goto loop;

end: ;             /*  레이블이 블럭의 맨 끝에 올 경우에는  */

}                  /*  널문장인 ';'를 반드시 써주어야 한다  */

실제로 if 문과 goto 문만 있으면 어떤 문장이든 다 만들 수 있지만 goto 문을 많이 사용하면 프로그램을 이해하기 어려워지기 때문에 될 수 있는 한 사용하지 않는 것이 좋다. 그러나 goto 문을 사용하면 훨씬 더 편리하고 효율적인 경우가 있는데, 예를 들면 여러 개의 while 문이나 for 문의 안에서 바깥쪽으로 나가고자 할 때에나 반대로 안으로 들어갈 때이다. 또 switch 문에서 나가거나 반대로 switch 문으로 들어올 수도 있다. 그러나 주로 많이 사용되는 것은 바깥쪽으로 나갈 때이다.

Ⅵ. 함수와 메모리 유형

C에서 함수를 사용하는 목적은 바로 계산 결과를 이용하기 위해서이다. 그런데 계산 결과도 하나의 데이터이기 때문에 역시 특정 데이터 유형을 갖게 된다. 그리고 C에서는 반드시 하나의 데이터 유형의 값만 계산 결과로 산출하도록 되어 있다. 이는 어느 때는 2를(즉 정수 데이터 유형의 값을) 어느 때는 2.2를(즉 실수 데이터 유형의 값을) 계산 결과로 산출할 수는 없다는 얘기가 된다(정수값만 산출하거나 아니면 실수값만 산출하여야 한다).

C에서의 함수는 특정 작업을 수행하고 그 결과를 넘겨주는 역할을 하는데, 예를 들어 다음과 같은 출력을 내는 프로그램을 작성한다고 하면...

**********************************************************************

OUTPUT BY HONG GIL DONG

**********************************************************************

INPUT DATA1

**********************************************************************

OUTPUT1

**********************************************************************

이를 printf로 출력시키면 되지만 이경우는 for 문을 사용하여 반복 출력하는 것이 편한데...

#include <stdio.h>

main() {

int i;

for (i = 1; i <= 70; i++)               /*  첫번째로 '*'를 70개 출력시킴  */

putchar('*');

putchar('\n');

printf(" OUTPUT BY HONG GIL DONG\n");

for (i = 1; i <= 70; i++)               /*  두번째로 '*'를 70개 출력시킴  */

putchar('*');

putchar('\n');

printf(" INPUT DATA1\n");

for (i = 1; i <= 70; i++)               /*  세번째로 '*'를 70개 출력시킴  */

putchar('*');

putchar('\n');

printf(" OUTPUT1\n");

for (i = 1; i <= 70; i++)               /*  네번째로 '*'를 70개 출력시킴  */

putchar('*');

putchar('\n');

}

위의 프로그램을 보면 다음 문장이 4번 반복되는 것을 알 수 있다.

for (i = 1; i <= 70; i++)

putchar('*');

putchar('\n');

따라서 이를 한번만 입력하고 필요할 때마다 불러 쓰면 편한데... 그래서 사용하게 된 것이 바로 함수이다. 위의 경우 함수를 사용하게 되면 다음과 같이 된다.

#include <stdio.h>

print_star() {         □□□□□□

int i;                      □

for (i = 1; i <= 70; i++)   □ '*'를 70개 출력시키는 함수

putchar('*');             □

putchar('\n');              □

}                      □□□□□□

main() {

print_star();   <□□□□□□□□□□□□□ ①

printf(" OUTPUT BY HONG GIL DONG\n");

print_star();   <□□□□□□□□□□□□□ ②

printf(" INPUT DATA1\n");

print_star();   <□□□□□□□□□□□□□ ③

printf(" OUTPUT1\n");

print_star();   <□□□□□□□□□□□□□ ④

}

위의 프로그램을 앞의 프로그램과 비교해 보면 훨씬 정리된 느낌을 줄 것이다. 또 print_star란 것이 '*'를 한 줄 출력시키는 것이라는 것을 안다면 위의 프로그램이 앞의 프로그램 보다 훨씬 더 이해하기 쉽다. 그리고 위의 print_star는 필요할 때마다 사용할 수 있고 다른 사람의 다른 프로그램에서도 사용할 수 있다는 장점을 갖게 되는데 이것이 함수를 사용하는 가장 큰 이유라고 할 수 있다. 그러나 함수를 많이 사용하면 수행시간이 많이 걸리기 때문에 속도는 떨어지게 된다. 따라서 속도에 민감한 프로그램을 작성하고자 할 때에는 함수의 사용에 신중을 기해야 한다.

다음은 함수 호출이 어떻게 처리되는지를 나타내는 프로그램이다.

main() {        □                               ②

int i = 10;   □① □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□→ sum(int x, int y) {

int j = 100;  □   □                              □   printf("sum = %d\n",x+y);

sum(i, j);   ↓□□□□         ④                ③ □   ++x;

++i;          ←□□□□□□□□□□□□□□□□□□□□□□□□□□□□□    □   ++y;

++j;              □                         □    □   printf("x = %d, y = %d\n",x,y);

printf("%d□,i+j); □⑤                       □□□□↓   }

}                ↓

일단 함수 호출문까지 수행된다(①). sum이라고 하는 함수 호출을 만나게 되면 실제로 sum함수로 제어가 이동하여(②), 이를 차례대로 수행하게 된다(③). 수행후 함수의 끝에 도달하게 되면, 제어가 자신을 부른 함수, 즉 main으로 다시 돌아가게 되며(이때 sum 호출이 있던 부분으로 돌아간다)(④), 그 다음 부분부터 차례대로 수행하게 된다(⑤). 위의 경우에는 함수 호출 자체가 하나의 문장이었으므로 그 다음 문장부터 수행하지만 함수 호출이 수식 내의 일부였을 때에는 그 수식을 계속 계산하게 된다.

< 함수의 정의와 호출  >

⑴ 함수의 정의

[메모리 유형] [데이터 유형] 함수 이름 ( [형식인자1,.....형식인자n] )

{ [함수 프로그램] }

우선 메모리 유형은 이 함수를 어떤 곳에서 사용할 수 있는 가를 나타내는데 여기서는 일단 생략되었다고 가정한다. 데이터 유형은 바로 이 함수가 계산하는 계산 결과의 데이터 유형을 의미한다(모든 함수는 계산 결과가 있기 때문에 이 데이터 유형도 반드시 있어야 한다). 그러나 계산 결과가 int 데이터 유형일 경우 생략할 수 있다. 지금까지 사용한 main도 함수이기 때문에 계산 결과가 있는데 이의 데이터 유형이 생략되었으므로 바로 int 유형의 값을 계산 결과로 산출하는 함수였음을 알 수 있다. 함수이름은 그 함수가 하는 역할에 따라 적당하게 알아보기 쉬운 말을 쓰면 되는데 이는 변수의 이름과 같이 알파벳과 숫자를 섞어 사용할 수 있으며 시작은 역시 알파벳으로 시작하여야 한다. 함수 이름 다음의 소괄호는 앞의 이름이 함수라는 것을 알려주는 기호이기 때문에 반드시 있어야 하며 소괄호 안에는 인자들의 목록이 오는데 이 인자들을 특별히 형식 인자라고 한다(형식 인자들은 변수 선언과 같은 형태로 오게 된다). 그 다음의 중괄호는 함수의 실제 내용인 프로그램을 둘러싸고 있는 것으로 역시 반드시 와야 한다. 특히 프로그램이 없는 경우에도 중괄호는 있어야 한다. 중괄호 안에는 바로 이 함수에 대한 프로그램이 오는데 main에서와 같이 변수를 선언하여 사용할 수도 있다. 위 함수의 정의에서 [ ]로 둘러싸인 부분은 생략할 수 있다. 따라서 가장 간단한 함수는 다음과 같이 된다.

do_noting() {

}

위 함수는 프로그램이 없기 때문에 하는 일도 없다. 그렇기 때문에 아무것도 하는게 없는 함수라고 할 수 있는데 계산 결과의 데이터 유형만은 int이다. 따라서 계산 결과의 데이터 유형이 있다고 해서 반드시 계산 결과가 있는 것은 아니라는 것을 알 수 있다.

다음은 이보다 조금 더 복잡한 함수의 한 예이다.

print_hello() {

printf("Hello!\n");

}

이 함수를 부르게 되면 언제나 Hello!란 메시지를 출력하게 된다. 그리고 함수 이름 앞에 데이터 유형이 생략되었기 때문에 int 데이터 유형의 값을 계산 결과로 산출하는 것이 되나 위 함수에서는 계산 결과라는 것이 아예 없다. 따라서 위의 함수는 일만 하고 계산 결과는 없는 함수이다.

다음 함수의 예를 살펴보자.

sum(int x, int y) {

printf("x + y = %d\n",x+y);

}

위 함수에서는 드디어 인자가 사용되었다. 위의 함수 sum은 두 개의 인자를 갖는데 첫번째 인자의 이름은 x이며 이의 데이터 유형은 int이다. 따라서 첫번째 인자로는 정수 데이터가 와야 한다. 마찬가지로 두번째 인자의 이름은 y이며 이의 데이터 유형도 int이기 때문에 위의 함수 sum을 사용하려면 sum(10,20)과 같이 두 개의 정수 데이터를 인자로 주어야 한다.

또 다음 함수를 살펴보자.

float sum2(float x, float y) {

return (x+y);

}

위 함수에서도 인자를 역시 2개를 갖는데 둘 다 float 데이터 유형으로 이 sum2란 함수는 float 데이터 유형의 값을 계산 결과로 산출함을 의미한다. 그 다음 실제 하는 일은 바로 return 문으로 이루어져 있으며 이 return 문은 함수의 계산 결과를 산출하는 문장으로 위의 함수 sum2는 x+y를 계산 결과로 산출하게 된다.

다음에 나오는 함수는 지금까지의 함수 가운데 제일 복잡한 형태로 되어 있다.

long int power(int n) {

int i;

long int res = 1l;

if (n < 0)

return (-1l);

else if (n == 0)

return (1);

else {

for (i = 1; i <= n; i++)

res *= i;

return (res);

}

}

위 함수가 계산 결과로 산출하는 값의 데이터 유형은 long int이며 int 유형의 인자를 한 개 갖는다. 그리고 자체적으로 int 유형의 변수 i와 long int 유형의 변수 res를 정의하여 사용하고 있는데 이 변수는 물론 위의 함수 내에서만 사용할 수 있다.

⑵ 함수의 호출

함수 이름(실인자1,.....실인자n)

즉 원하는 함수의 이름 다음에 괄호를 붙이고 그 안에 인자의 값을 주면 되는데, 인자가 없는 함수의 경우라도 괄호는 절대로 생략할 수 없다. 따라서 앞의 do_nothing 함수를 호출하고자 할 때에는 다음과 같이 사용하면 된다.

do_nothing()

do_nothing은 실제로 하는 일이 없으므로 위와 같이 호출하여도 아무런 변화도 발생하지 않는다. 그러나 print_hello를 호출하게 되면

print_hello()

Hello!라는 메시지가 화면에 나타나게 된다. 반면에 sum 함수의 경우에는 두 개의 인자가 있기 때문에 다음과 같이 두 개의 값을 지정하여 호출하여야 한다.

sum(2,3)

이때 sum 다음의 인자들은 sum 함수의 형식 인자들에 다음과 같이 차례대로 할당된다.

□□□□□2는 x에 전달

sum(2,3)□□□□□□□□□□□□□□□□□□□□□□□□□ 함수 호출(실인자)

□□□□□□□□□□3은 y에 전달

□      □

sum(int x, int y)□{□□□□□□□□□□□□□□ 함수 정의(형식인자)

printf("x + y = %d\n",x+y);

}

함수 정의에서의 인자들은 값이 없다(이는 당연히 초기값도 줄 수가 없다). 이들이 값을 갖게 되는 것은 바로 함수 호출시 같이 지정한 인자들을 통해서이다. 따라서 앞의 예의 경우에는 x가 2, y가 3이 되므로 x + y = 5라고 하는 출력이 나오게 된다.

함수 호출은 수식의 하나로 간주되며 이는 함수 호출을 수식 내에서 사용할 수 있음을 의미한다. 또 함수 호출에 ';'만 붙이면 바로 수식문이, 즉 문장이 됨을 의미한다. 따라서 다음과 같이 사용할 수 있다.

sum(1,5);

함수 호출도 수식이기 때문에 계산 결과가 있어야 하는데 함수 호출의 계산 결과는 바로 그 함수의 계산 결과가 된다. 따라서 power 함수를 다음과 같이 호출하였을 때.

1+power(3)

power(3)의 계산 결과는 power 함수에 3이란 값의 실인자로 호출하였을 때의 계산 결과, 즉 6이 되며 따라서 1+power(3)은 7이 된다. 단 power는 계산 결과가 long int이기 때문에 1+power(3)의 계산은 int+long int가 되어 long int 유형이 된다. 그러면 다음의 예를 보자.

x = power(2)+power(3);

위의 문장에서는 함수 호출이 2개나 있다(같은 수식 내에 함수 호출이 여러 개 있어도 문제가 되지 않는다). 이들을 계산하면 power(2)는 2, power(3)은 6이 되므로 결국 x = 2+6이 되며 부수효과에 의해 x의 값도 8이된다. 또 다음 문장을 살펴보자.

y = power(power(3));

위 문장은 실인자로 또 함수 호출이 사용되었다. C의 실인자로 수식이 올 수 있기 때문에 위와 같이 당연히 함수 호출이 올 수 있다. 이경우 안의 함수 호출을 먼저 계산하므로 power(3)은 6이 되고 이는 power(6)이 되어 결국 y는 720의 값을 갖게 된다.

다음은 실제로 함수를 정의하여 사용하는 프로그램의 몇 가지 예이다.

long factorial(int n) {               /*  long형을 되돌리는 함수  */

int i;

long product = 1l;

for (i = 2; i <= n; i++)

product *= i;

return product;

}

main() {

int num;

long fact;

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

fact = factorial(num);            /*  factorial 함수 호출뒤 fact에 대입  */

printf("%2d! = %8ld\n",num,fact);

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

1! =        1

2! =        2

3! =        6

4! =       24

5! =      120

6! =      720

7! =     5040

8! =    40320

9! =   362880

10! =  3628800

위의 for 문은 fact 변수를 없애고 다음과 같이 간결하게 할 수도 있다.

for (num = 1; num <= 10; num++)

printf("%2d! = %8ld\n",num,factorial(num));

void a() {

printf("This is function test.\n");

printf("Now in function a()\n");

}

main() {

int s;

a();                       /*  a 함수 호출  */

sum(10,20);                /*  sum 함수를 실인자 10과 20으로 호출  */

s = sum(20,60);            /*  sum 함수를 20과 60으로 호출한 후 되돌림 값을 s에 저장  */

printf("sum from 20 to 60 = %d\n",s);

printf("sum from 10 to 100 = %d\n",sum(10,100));

/*  sum 함수의 되돌림 값을 printf함수의 변수로 사용  */

}

int sum(int a, int b) {      /*  정수형 변수 2개를 받아 정수형을 되돌리는 함수  */

int i, s = 0;

printf("Now in function b()\n");

for (i = a; i <= b; i++)

s = s + i;

return s;                  /*  s의 값을 호출한 함수에게 되돌린다  */

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

This is function test.

Now in function a()

Now in function b()

Now in function b()

sum from 20 to 60 = 1640

Now in function b()

sum from 10 to 100 = 5005

위 프로그램에서 처음의 sum 함수 호출에서의 되돌림 값은 아무곳에도 사용되지 않았다. 이와같이 함수의 되돌림 값은 필요에 따라 무시될 수도 있다. 다음의 sum 함수 호출에서는 되돌림 값이 변수 s에 대입된 후 화면으로 출력되고 있으며 마지막의 printf 문을 보면 sum함수 자체가 직접 사용되고 있다. C에서는 함수의 되돌림 값이 꼭 어떤 변수에 대입될 필요는 없다.

이처럼 되돌림 값은 변수에 대입될 수도 있고 다른 함수 호출의 실인자로 직접 사용될 수도 있음을 알 수가 있다.

#include <stdio.h>

main() {

char c;

int vno = 0;        /*  모음의 갯수를 기억할 변수  */

int cno = 0;        /*  자음의 갯수를 기억할 변수  */

int uno = 0;        /*  대문자의 갯수를 기억할 변수  */

int lno = 0;        /*  소문자의 갯수를 기억할 변수  */

int dno = 0;        /*  숫자의 갯수를 기억할 변수  */

while ((c = getchar()) != EOF) {

if (is_vowel(c))             /*  읽은 문자가 모음이면  */

++vno;                     /*  모음 숫자를 하나 증가  */

else if (is_alphabet(c))     /*  모음이 아닌 알파벳이면, 즉 자음이면  */

++cno;                     /*  자음 숫자를 하나 증가  */

if (is_upper(c))             /*  읽은 문자가 대문자이면  */

++uno;                     /*  대문자 숫자를 하나 증가  */

else if (is_lower(c))        /*  대문자가 아닌 소문자이면  */

++lno;                     /*  소문자 숫자를 하나 증가  */

else if (is_digit(c))        /*  읽은 것이 숫자면  */

++dno;                     /*  숫자 카운트를 하나 증가  */

}

printf("    Number of Vowels : %d\n",vno);

printf("Number of Consonants : %d\n",cno);

printf("Number of Uppercases : %d\n",uno);

printf("Number of Lowercases : %d\n",lno);

printf("    Number of Digits : %d\n",dno);

}

is_vowel(char c) {

switch (c) {

case 'a': case 'e': case 'i': case 'o': case 'u':

case 'A': case 'E': case 'I': case 'O':

case 'U': return (1);       /*  문자가 모음인 경우 참인 1을 계산 결과로 산출  */

default : return (0);       /*  모음이 아니므로 거짓인 0을 계산 결과로 산출  */

}

}

is_alphabet(char c) {                /*  주어진 문자가 영문 알파벳인지 판단하는 함수  */

return (is_upper(c) || is_lower(c));

}

is_upper(char c) {                   /*  주어진 문자가 대문자인지 판단하는 함수  */

return (c >= 'A' && c <= 'Z');

}

is_lower(char c) {                   /*  주어진 문자가 소문자인지 판단하는 함수  */

return (c >= 'a' && c <= 'z');

}

is_digit(char c) {                   /*  주어진 문자가 숫자인지 판단하는 함수  */

return (c >= '0' && c <= '9');

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>count < readme □

Number of Vowels : 5370

Number of Consonants : 9072

Number of Uppercases : 2481

Number of Lowercases : 11961

Number of Digits : 265

※ 함수와 서브루틴

다른 프로그래밍 언어에는 두가지 종류의 함수가 있다. 원래 의미의 함수와 서브루틴(또는 프로시저)이라고 부르는 것들인데, 서브루틴(subroutine)은 계산 결과가 없고 일만 하는데 반해 함수는 계산 결과가 반드시 있다. 예를 들어 다음 프로그램의 경우

print_hello() {

printf("Hello!\n");

}

Hello!라는 메시지를 출력시키는 일만 하고 계산 결과는 없으므로(return 문이 없으면 계산 결과가 없는 것이다) 이는 서브루틴에 해당된다. 반면에 다음 프로그램의 경우에는

sum(int x, int y) {

return (x+y);

}

x+y의 값을 계산 결과로 산출하므로 함수에 해당된다. 함수와 서브루틴은 호출할 때도 차이가 있다. 즉 서브루틴은 다음과 같이 호출 자체만으로(이것으로만) 문장이 된다.

print_hello();

그러나 함수는 다음과 같이 반드시 수식과 같이 사용되어야만(그것도 부수효과가 있는 경우에만) 문장으로서 의미가 있다.

x = sum(1,3);

C에서는 서브루틴과 함수를 모두 함수로 취급하기 때문에 다음과 같이 의미 없는 문장들도 가능하게 된다.

x = print_hello();

sum(2,4);

위에서 print_hello()는 계산 결과가 없는데 이것이 마치 있는 것처럼 x에 값을 할당하고 있다. 이렇게 해도 컴파일할 때나 수행할 때 전혀 에러가 발생하지 않지만 x에는 전혀 예상할 수 없는 값이 들어가기 때문에 주의해야 한다. 그리고 sum(2,4);의 경우에는 일단 계산을 하면 6이 되고 결국 6; 과 같은 수식문이 되어 전혀 쓸모 없는 문장이 되어 버리는데 C에서는 이를 도와주기 위해 void란 데이터 유형을 제공하고 있다.

즉 함수의 데이터 유형을 다음과 같이 void로 선언하게 되면

void print_hello() {

printf("Hello!\n");

}

이 함수는 계산 결과가 없다고 하는 의미가 된다. 따라서 다음과 같이 계산 결과를 사용하려고 하면

x = print_hello();

컴파일 에러나 경고 메시지가 발생하게 되어 에러를 방지할 수 있다. 따라서 계산 결과가 없는 함수(즉 서브루틴에 해당하는 것들)는 반드시 void로 선언하여야 한다. 지금까지 선언한 main 함수도 계산 결과가 없을 때에는 다음과 같이 void로 선언하는 것이 정확한 표현이 된다.

void main() {

:

void 유형은 역시 C에서만 볼 수 있는 특이한 데이터 유형으로 일반 변수에는 사용할 수 없고 함수에만 사용된다. 다음은 void 유형에 관한 예이다.

#include <stdio.h>

void print_char(char c, int n) {

int i;

for (i = 1; i <= n; i++)

printf(" %c",c);

}

main() {

int n;

int i;

printf("Input N -> ");

scanf("%d",&n);

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

print_char(' ',n - i);

print_char('*',2 * i - 1);

putchar('\n');

}

for (i = 1; i <= n - 1; i++) {

print_char(' ',i);

print_char('*',2 * n - 2 * i - 1);

putchar('\n');

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input N -> 10 □

*

* * *

* * * * *

* * * * * * *

* * * * * * * * *

* * * * * * * * * * *

* * * * * * * * * * * * *

* * * * * * * * * * * * * * *

* * * * * * * * * * * * * * * * *

* * * * * * * * * * * * * * * * * * *

* * * * * * * * * * * * * * * * *

* * * * * * * * * * * * * * *

* * * * * * * * * * * * *

* * * * * * * * * * *

* * * * * * * * *

* * * * * * *

* * * * *

* * *

*

void print_star(int n) {

int i;

for (i = 1; i <= n; i++)

printf(" *");

}

main() {

int i, n;

printf("Input N -> ");

scanf("%d",&n);

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

print_star(i);

printf("%d\n",i);

}

for (i = n; i > 0; i--) {

print_star(i);

printf("%d\n",i);

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input N -> 10 □

*1

* *2

* * *3

* * * *4

* * * * *5

* * * * * *6

* * * * * * *7

* * * * * * * *8

* * * * * * * * *9

* * * * * * * * * *10

* * * * * * * * *9

* * * * * * * *8

* * * * * * *7

* * * * * *6

* * * * *5

* * * *4

* * *3

* *2

*1

< RETURN 문 >

return 문은 함수 내에서 계산 결과를 산출하여 자신을 호출한 함수에 되돌리는 역할을 하는 문장으로 다음과 같은 형태로 사용한다.

return;

return (수식);

return 문 다음에는 변수와 상수는 물론 수식이 올 수 있다. 수식이 올 경우 괄호로 묶어 준다. 그리고 되돌림 값이 void형인 함수에서의 return 문은 그 함수를 중단시키는 역할을 한다.

다음은 return 문의 몇가지 예 이다.

return;                   /*  단순히 함수를 종료하는 역할을 한다  */

return (a + b);           /*  수식일 경우 괄호로 묶도록 한다  */

return (i++);             /*  i 값을 하나 증가시킨 후 값을 되돌린다  */

return (iabs(n) * 2);     /*  다른 함수를 호출하여 return할 수도 있다  */

다음은 음력 1992년 1월 1일은 수요일이라는 조건하에 그 이후의 년, 월, 일을 입력하면 그 날짜의 요일을 출력하는 프로그램으로 1992년 1월 1일을 기점으로 지나간 날 수를 계산하면 되는데 윤년이 있으므로 이를 같이 고려하여야 한다. 윤년은 4의 배수인 해인데 100의 배수는 윤년이 아니고 또 400의 배수는 윤년이다. 각 달마나 날수가 틀리므로 그 달 까지의 날 수를 계산해 주는 함수를 사용하도록 한다.

long int compute_days(int y, int m, int d) {     /*  1992년 1월 1일을 기준으로 한 날수를

계산  */

long int ds;

int i;

ds = (y - 1992) * 365l + mdays(y, m) + d;      /*  우선 윤년을 빼고 계산  */

for (i = 1992; i < y; i += 4)              /*  그 사이의 윤년을 찾아 1씩을 더함  */

if ((i % 400 == 0) || ((i % 100 != 0) && (i % 4 == 0)))

++ds;

return (ds);

}

void print_day(int d) {

switch (d) {

case 1: printf("The day is Wednesday.\n");

break;

case 2: printf("The day is Thursday.\n");

break;

case 3: printf("The day is Friday.\n");

break;

case 4: printf("The day is Saturday.\n");

break;

case 5: printf("The day is Sunday.\n");

break;

case 6: printf("The day is Monday.\n");

break;

case 0: printf("The day is Tuesday.\n");

break;

}

}

void main() {

int year, mon, day;

long int days;

printf(" Input Year -> ");

scanf("%d",&year);

printf("Input Month -> ");

scanf("%d",&mon);

printf("  Input Day -> ");

scanf("%d",&day);

if (!day_is(year,mon,day))

printf("Incorrect date!!\n");

else {

days = compute_days(year,mon,day);

print_day((int)(days % 7));

}

}

day_is(int y, int m, int d) {     /*  년, 월, 일이 올바른지 조사하는 함수  */

int e = 0;

if (y < 1992 || m < 1 || m > 12 || d < 1 || d > 31)   /*  1992년 이전이나 월이 1 ~ 12

사이가 아니거나 일이 1 ~ 31 사이가 아니면 잘못  */

return (0);                   /*  값이 1이면 올바르며, 0이면 틀림  */

if ((y % 400 == 0) || ((y % 100 != 0) && (y % 4 == 0)))   /*  윤년이 계산  */

e = 1;

switch (m) {

case 1: case 3:

case 5: case 7:

case 8: case 10:

case 12: return (1);

case 2: if (d <= 28 + e)  /*  2월이고 일수가 28 + 1(윤년의 경우) 보다 작으면 정상  */

return (1);

else

return (0);

case 4:

case 6:

case 9:

case 11: if (d <= 30)

return (1);

else

return (0);

}

}

mdays(int y, int m) {       /*  1월 1일을 기준으로 바로 전월까지의 날수를 계산  */

int e = 0;

if ((y % 400 == 0) || ((y % 100 != 0) && (y % 4 == 0)))   /*  윤년을 계산  */

e = 1;

switch (m) {

case 1: return (0);

case 2: return (31);

case 3: return (59 + e);

case 4: return (90 + e);

case 5: return (120 + e);

case 6: return (151 + e);

case 7: return (181 + e);

case 8: return (212 + e);

case 9: return (243 + e);

case 10: return (273 + e);

case 11: return (304 + e);

case 12: return (334 + e);

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input Year -> 1995 □

Input Month -> 7 □

Input Day -> 24 □

The day is Monday.

다음은 계산기를 다음과 같이 확장한 프로그램이다.

① 각 숫자는 b, d, o, x로 시작할 수 있다. b로 시작하면 이진수이며 d는 십진수, o는 8진수, x는    16진수이다. 따라서 -d123 + x34는 십진수 -123 과 16진수 34의 합을 의미한다. b, d, o, x가     생략되었을 경우에는 10진수로 가정한다.

② 숫자와 연산자 사이를 <Space>나 <Tab> 등으로 띄울 수 있다. 또 숫자 앞에도 <Space>나    <Tab>이 들어갈 수도 있다. 따라서 -127 *   o345와 같이 사용할 수 있다.

이 프로그램에서는 scanf를 사용할 수 없다. 숫자 앞에 b, d, o, x라는 문자가 있을 수 있을 뿐만 아니라 <Space>나 <Tab>도 있을 수 있기 때문이다. 따라서 이 경우에는 한자씩 읽어서 이를 숫자로 변환하는 것이 필요한데, 이를 하나의 함수로 만들어 사용하도록 하겠다. 그런데 문제는 한자씩 숫자가 아닐 때까지 읽어 이를 변환하기 때문에 끝의 한자를 더 읽어야 변환이 가능하다는 것이다. 예를 들어 17*34라고 입력한 경우 17* 까지 읽어야 '*'가 숫자가 아니므로 '1'과 '7'을 17로 만들 수 있다. 이때 '*'를 이미 읽어 버렸기 때문에 이를 다시 사용할 수 없게 된다. 이의 해결책은 마지막으로 읽은 것을 취소해 그 다음에 다시 읽어들이도록 하는 것이다. 이를 위한 라이브러리 함수로 ungetc라는 것이 있으며 이는 ungetc(c, stdin)이란 형태로 사용하면 되는데 c가 취소할 문자가 된다. 또 x의 y승을 계산하여야 하는데 이에 대한 라이브러리 함수로 pow가 있다. pow(x, y)는 x의 y승을 계산하는 것인데 x와 y가 모두 double 유형이기 때문에 이로 변환하여 호출하는 것이 필요하다.

#include <stdio.h>              /*  ungetc(c,stdin) 함수를 사용하기 위해 선언  */

#include <math.h>               /*  pow(x,y) 함수를 사용하기 위해 선언  */

long int read_number() {        /*  하나의 숫자를 읽음  */

int base = 10;                /*  기본 진법은 10진수  */

long int res = 0l;            /*  지금까지 읽은 숫자를 기억  */

int sign = 1;                 /*  기본 부호는 +  */

char c;

again: while ((c = getchar()) == ' ' || c == '\t');  /*  <Space>와 <Tab>은 무시  */

switch (c) {

case 'd': break;            /*  d로 시작하면 10진수  */

case 'b': base = 2;         /*  b로 시작하면 2진수  */

break;

case 'o': base = 8;         /*  o로 시작하면 8진수  */

break;

case 'x': base = 16;        /*  x로 시작하면 16진수  */

break;

case '+': goto again;       /*  +면 부호에 변화가 없음  */

case '-': sign *= -1;       /*  -면 부호가 바뀜  */

goto again;

default : ungetc(c,stdin);  /*  다른 값이면 입력을 취소  */

}

while ((c = getchar()) && (((base != 16) && (c >= '0') &&

(c - '0' <= base - 1)) || ((base == 16) && (c >= '0' && c <= '9') ||

(c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))))

/*  진법에 맞는 수가 입력되는 동안  */

switch (c) {

case 'a':      /*  16진수 A  */

case 'A': res = res * base + 10l;

break;

case 'b':      /*  16진수 B  */

case 'B': res = res * base + 11l;

break;

case 'c':      /*  16진수 C  */

case 'C': res = res * base + 12l;

break;

case 'd':      /*  16진수 D  */

case 'D': res = res * base + 13l;

break;

case 'e':      /*  16진수 E  */

case 'E': res = res * base + 14l;

break;

case 'f':      /*  16진수 F  */

case 'F': res = res * base + 15l;

break;

default : res = res * base + c - '0';   /*  그외는 이와 같이 계산됨  */

}

ungetc(c,stdin);   /*  하나 더 읽은 것을 취소시켜 다음에 읽도록 함  */

return (res * sign);

}

void skip_last() {             /*  에러가 발생했을 때 나머지 문자들을 읽어 버림  */

while (getchar() != '\n');

}

void main() {

long int x, y;

char op;

while (1) {

printf("--> ");            /*  프롬프트  */

x = read_number();         /*  첫번째 정수를 읽어버림  */

while ((op = getchar()) == ' ' || op == '\t');     /*  연산자나 명령을 읽음  */

if (op == 'q' || op == 'Q')

break;

y = read_number();         /*  두번째 정수를 읽어버림  */

switch (op) {

case '+': printf("%ld\n",x+y);

break;

case '-': printf("%ld\n",x-y);

break;

case '*': printf("%ld\n",x*y);

break;

case '/': printf("%ld\n",x/y);

break;

case '%': printf("%ld\n",x%y);

break;

case '^': printf("%.0lf\n",pow((double)x,(double)y));

break;

default : printf("Error : Unknown Operator %c\n",op);

}

skip_last();

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

--> b10101010 *   25 □

4250

--> Q □

※ main에서의 return 문

main() {

return (2);

printf("Hello!\n");

}

return 문은 자신을 부른 함수에게로 돌아가게 만든다고 언급한 바 있는데, main 함수 내에서 return 문을 사용할 경우에도 다른 return 문과 같이 처리된다. 즉 자신을 부른 함수에게 계산 결과를 넘겨주고 자신의 수행은 끝내게 되는데 main이 프로그램의 시작이므로 결국 프로그램의 수행이 끝나게 되어 그 다음의 printf 문은 수행되지 않는다. 그런데 문제는 main을 부른 함수가 없다는 것이다. 이는 바로 OS(즉, 위 프로그램을 수행시킨 운영 체계)에게 전달된다. 예를 들어 MSC나 TC의 경우에는 DOS에 계산 결과가 전달되며 Unix는 Shell에게 계산 결과가 전달된다. 마치 OS가 main을 부른 함수라고 생각하면 되는데 이 OS에게 전달된 값은 OS 상에서 액세스하여 사용할 수 있다. DOS의 경우에는 배치 화일 상에서 □IF ERRORLEVEL == 값□ 과 같은 형태로 확인하는 것이 가능하다.

예를 들어 위의 프로그램의 수행화일이 test.exe라고 할 때 다음과 같이 배치 화일을 만들고 이를 수행하게 되면

@ECHO OFF

TEST

IF ERRORLEVEL == 2 GOTO PRINT

GOTO END

:PRINT

ECHO The result is 2!

:END

test.exe가 return (2);를 통해 2를 계산 결과로 DOS에 넘겨주므로 ERRORLEVEL == 2는 참이 되어 다음과 같은 수행 결과를 얻게 된다.

The result is 2!

main에서의 return 문은 실제로 어떤 계산 결과라기 보다는 이 프로그램이 어떻게 끝났는 지를 알려주는 성격이 더 강하다. 일반적으로 0은 정상적으로 수행이 종료하였음을 의미하며 음수값이나 0보다 큰 값은 수행이 비정상적으로(즉 에러가 발생했다든지 하여) 끝났음을 의미하는 경우가 많다. 따라서 이 값을 보면 방금 수행한 프로그램의 상태를 알 수 있으므로 이를 바탕으로 다른 프로그램을 수행할 것인지를 판단할 수 있다. 실제 프로그램 상에서 프로그램의 수행을 중단하고 OS에게 돌아가게 할 때에는 exit 문을 더 많이 사용한다.

< 인자의 전달 >

인자의 전달이란 실인자의 값이 형식 인자의 값으로 어떻게 전달되는가 하는 것을 의미한다.

void swap(int x, int y) {

int t;

t = x;

x = y;

y = t;

}

main() {

int x = 10;

int y = 20;

swap(x, y);

printf("x = %d, y = %d\n",x,y);

}

위의 swap 함수는 x와 y의 값을 서로 바꾸고 있다. 그러면 main 함수에서 swap(x, y)를 호출하였으니 x와 y의 값이 바뀌어야 할 텐데 위 프로그램을 실제로 수행해 보면 x = 10, y = 20으로 전혀 바뀌지 않았음을 알 수 있는데 이유는 바로 C의 인자 전달 방법 때문이다. C에서는 값에 의한 호출(call by value) 방법으로 인자를 전달하고 있으며 철저하게 값만 인자로 전달된다. 이 얘기는 main에서 swap(x, y);로 x와 y를 실인자로 제공하고 있지만 swap(x, y)의 x, y에 전달되는 것은 main의 x, y의 값, 즉 10, 20이 전달된다. 따라서 위의 swap 호출은 swap(10, 20);과 똑같은 의미를 갖게 된다. 그러므로 당연히 swap에서 아무리 x, y의 값을 바꾸어 보았자 main의 x, y의 값은 바뀌지 않는 것이다(swap의 x, y와 main의 x, y는 이름만 같지 전혀 다른 변수이다).

값에 의한 호출이란 실인자를 전달할 때 항상 그 값만을 전달해 준다는 뜻으로 매개변수가 전달되는 과정을 보다 깊숙이 살펴보면 어떤 함수가 호출되기 전에 매개변수의 값들은 stack이라는 컴퓨터 내부 기억장소에 저장된후 호출된 함수에서는 다시 그 값을 꺼내 사용하는 방식을 취하고 있다. IBM-PC에서는 내부 스택이 정해져있어 주로 함수의 호출 및 매개변수 전달에 쓰이고 있다. 이러한 스택은 이른바 LIFO(Last In First Out)의 구조를 가지고 있어 가장 최근에 저장된 값이 바로 꺼내질 수 있도록 되어 있다.

※ 함수의 인자 수에 대하여

함수의 인자 수는 고정되어 있는게 보통이다. 그리고 그 인자 수에 맞는 실인자를 함수 호출시 제공하여야 한다. 그런데 C에서는 함수의 인자수 보다 적은 실인자를 제공하거나 더 많은 실인자를 제공해도 컴파일할 때 전혀 에러가 발생하지 않는다. 예를 들어 다음 프로그램의 경우,

void func(int x, int y) {

printf("x = %d, y = %d\n",x,y);

}

위 함수를 사용하려면 int 유형의 인자를 2개 제공하여야 한다. 그런데 만약 func(1,2,3)으로 3개를 사용하면 분명히 잘못된 것이지만 이를 실제로 해보면 컴파일시나 수행시 전혀 에러가 발생하지 않음을 알 수 있다. 그리고 x = 1, y = 2란 출력을 얻게 된다. 즉 알아서 필요한 만큼 가져간 것이다. 그러면 func(1)과 같이 모자라게 호출하면 어떻게 되겠는가? 역시 이 경우에도 컴파일시는 전혀 에러가 발생하지 않는다. 그런데 수행시에는 다음과 같이 엉뚱한 결과가 발생하게 된다.

x = 1, y = 0

이는 func에서 무조건 두 개의 int 유형의 인자가 필요하므로 이를 사용하려고 시도해서 첫번째는 제대로 사용하지만 두번째는 엉뚱한 곳의 것을 사용하기 때문이다. 특히 다음과 같이 함수 호출시 인자를 괄호로 둘러싸게 되면

func(1,(2,3));

첫번째 ','는 인자를 구분하는 기호로 인식되지만 두번째 콤마는 콤마 연산자로 인식된다. 따라서 콤마 연산자를 먼저 계산하기 때문에(이의 계산 결과는 무조건 오른쪽이다) 다음과 같이 호출한 것과 같은 의미를 갖게 된다.

func(1,3);

인자로 복잡한 수식을 사용할 경우 괄호를 사용하는 수가 많은데 위와 같은 실수로 다음 인자까지 괄호로 둘러쌀 수 있다. 이 경우 잘못된 결과를 얻게 되므로 주의하여야 한다.

< 변수의 메모리 유형 >

메모리 유형은 변수의 동작 범위와 존속 기간에 영향을 미친다. 변수의 동작 범위란 그 변수를 프로그램의 어느 부분에서 사용할 수 있는 가에 대한 것으로, 함수 안에서 정의된 변수들은 자신이 정의된 함수 내에서만 사용할 수 있으므로 그 함수가 동작 범위가 되며, 마찬가지로 블럭 안에서 정의된 변수들은 그 블럭이 동작 범위가 된다. 존속 기간이란 그 변수를 얼마나 오래 사용할 수 있는가 하는 것으로, 함수 안에서 정의된 변수들은 그 함수를 호출해서 함수 안의 프로그램을 수행할 때에만 사용할 수 있으므로 존속 기간이 함수와 같다고 할 수 있다.

⑴ auto 메모리 유형

auto 메모리 유형은 지금까지 사용했던 변수들의 메모리 유형을 의미한다. 따라서 원래는 다음과 같이 선언하여야 하는데,

main() {

auto int i;      /*  auto 메모리 유형의 변수들을 지역 변수라고도 한다  */

auto char c;

:

auto를 생략한 것이다(실제로 생략해서 더 많이 사용한다). auto 메모리 유형의 변수들은 우선 동작 범위가 자신이 선언된 함수나 블럭이 된다. 반면에 존속 기간은 함수 안에 정의된 변수들은 그 함수 안에 제어가 있는 동안, 즉 그 함수의 프로그램을 수행하는 동안이 된다. 그리고 그 함수의 수행이 끝나게 되면 변수들은 사라지게 된다. 마찬가지로 블럭 안에 선언된 변수들은 그 블럭 안에 제어가 있는(그 블럭 내의 프로그램을 수행하는) 동안 살아있게 되며 블럭에서 제어가 떠나면, 즉 블럭 밖의 프로그램을 수행하게 되면 사라지게 된다.

auto 변수의 특징은 프로그램이 컴파일될 때 기억장소가 만들어지는 것이 아니고 프로그램 실행 도중에 그 변수가 선언된 블럭이나 함수가 시작될 때 기억 장소가 할당 되었다가 그 블럭이나 함수를 떠날 때는 자동으로 메모리에서 제거된다.

⑵ extern 메모리 유형

extern 메모리 유형의 변수들은 함수 안이 아닌 함수 바깥쪽에 정의되며 메모리 유형이 없는 변수들은 모두 extern 메모리 유형의 변수들이다.

extern 메모리 유형의 변수들의 동작 범위는 자신이 정의된 위치부터 프로그램의 끝까지가 된다.   다음은 각 extern 메모리 유형 변수의 동작 범위를 나타낸 것이다.

int i = 100; □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

char c = 'A';                                           □

□

main() {                                                □

int j = i;      /*  i를 사용  */                      □

char d = 'f';                                         □

d += c - 'e';   /*  c를 사용  */                      □  i와 c의 동작 범위

proc1();                                              □

printf("j = %d, d = %c\n",j,d);                       □

}                                                     □

□

int k = 123; □□□□□□□□□□□□□□□□□□□                        □

□                        □

proc1() {                      □    k의 동작 범위       □

int l = k;  /*  k를 사용  */ □                        □

printf("l = %d\n",l);        □                        □

} □□□□□□□□□□□□□□□□□□□□□□□□□□□□ □□□□□□□□□□□□□□□□□□□□□□□□

반면 중간에 선언된 extern 메모리 유형의 변수들을 그 앞에서도 사용할 수 있는데 이 경우에는 사용하기 전에 변수를 선언하는 것이 필요하다. 이 선언은 지금까지의 변수의 정의와는 다른 것으로, 변수를 선언한 경우에는 extern 변수를 어디에 정의하였건 전체 프로그램이 동작 범위가 될 수 있다. 그래서 extern 메모리 유형의 변수들을 전역 변수라고 부른다. 변수 선언만 하면 프로그램의 어느 부분에서도 사용할 수가 있고, 심지어는 다른 화일의 프로그램에서도 사용할 수 있기 때문이다.

extern 메모리 유형의 변수들의 존속 기간은 전체 프로그램의 수행시간이 된다. 즉, 프로그램이 수행되는 동안(프로그램의 어느 부분을 수행하건) 이 변수들은 살아 있어서, 이들 변수의 값을 사용할 수 있다. 따라서 extern 메모리 유형의 변수들은 각 함수에서 공통적으로 사용하는 변수들로 사용된다. 예를 들어 모든 함수에서 사용해야할 데이터가 있을 경우 이를 extern 메모리 유형의 변수를 이용하여 사용하면 매우 효율적인 프로그램을 작성할 수 있다. 또 함수에서는 자신을 부른 함수의 데이터를 변경할 수 없으므로 다른 함수들이 변경해야 할 데이터를 extern 메모리 유형으로 사용하면 함수들이 이를 쉽게 변경할 수 있다.

변수의 선언은 다음과 같은 형태로 사용한다.

extern 데이터 유형  변수 이름;

맨 앞에 반드시 extern이라고 붙여야 하며 나머지는 변수의 정의와 같다.

변수를 extern 형으로 선언하는 것은 그 변수가 다른 화일이나 블럭 또는 함수에 존재한다는 것을 컴파일러에게 알려서 현재의 화일이나 블럭 또는 함수에서 사용할 수 있도록 해주는 것이다.

예를 들어 다음 프로그램의 경우.

main() {  □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

extern int count;                        □

int i;                                   □

i = count - 5;                           □

++count;                                 □

f1();                                    □

printf("i = %d, count = %d\n",i,count);  □   count 변수의 동작 범위

}                                        □

□

int count = 5;                             □

f1() {                                     □

++count;                                 □

} □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

위 프로그램에서 다음과 같이 main 앞에 count를 정의하면 변수 선언을 할 필요가 없다.

int count = 5; □□□□□□□□□□□□□□□□□□□□□□□□□□□□□

main() {                                   □

int i;                                   □

i = count - 5;                           □

++count;                                 □

f1();                                    □

printf("i = %d, count = %d\n",i,count);  □

}                                        □ count 변수의 동작 범위

□

f1() {                                     □

++count;                                 □

} □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

반면에 반드시 변수 선언을 해야만 하는 경우가 있는데 이는 사용하고자 하는 변수가 다른 화일에 정의되어 있는 경우이다.

/*  main.c  */

main() {

extern int count; □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

int i = 5;                                       □

f1();                                            □ 변수 선언에 따라 새로 생긴

printf("i = %d, count = %d\n",i,count);          □ count의 동작 범위

} □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

/*  f1.c  */

int count = 10; □□□□□□

f1() {               □ count의 원래 동작 범위

++count;           □

} □□□□□□□□□□□□□□□□□□□

count가 extern 메모리 유형의 변수이기 때문에 위외 같이 변수를 선언하면 main에서도 사용할 수 있게 된다. 실제로 변수의 선언은 위와 같이 주로 다른 화일에 정의된 변수를 사용하고자 할 때 사용한다.

그런데 변수 선언시의 데이터 유형과 실제 정의 때의 데이터 유형이 다르면 어떻게 되는가?

main() {            /*  main.c  */

extern int count;

extern char data;

int i = count;

char c = data;

f1();

printf("i = %d, c = %c\n",i,c);

}

char count = 'A';    /*  f1.c  */

int data = 300;

f1() {

++count;

++data;

}

count는 char 유형으로, data는 int 유형으로 정의되어 있다. 그런데 main에서는 이들을 count는 int로, data는 char로 선언하여 사용하고 있는데, 이렇다 하더라도 컴파일시에는 아무런 에러가 나지 않는다. 이는 컴파일러가 철저하게 화일 단위로 컴파일하기 때문인데 main.c를 컴파일할 때에는 사용자가 선언한 것을 그대로 믿고 컴파일하며 f1.c에서는 정의한 그대로 컴파일하기 때문이다(이때 전에 컴파일한 것은 잊어 버린다). 그리고 링크시에도 링커가 아는 것은 오로지 이름 뿐이다. 따라서 다음과 같이 수행시 아무런 에러 메시지 없이 잘못된 결과가 나오게 되므로 주의해야 한다.

i = 11329, c = ,

extern으로 선언할 변수가 많은 경우에는 위와 같은 실수를 할 수 있는데 이를 방지하는 방법은 별도의 header 화일을 만들어 이를 include 하는 것이다.

그리고 다음과 같이 이중으로 변수를 선언할 수도 있다.

extern int data         /*  extern으로 변수를 선언하였다고 해서 메모리가 할당  */

extern char count;      /*  되는 것은 아니고 컴파일러에게 이러한 변수가 다른  */

/*  파일 내에 존재하고 있음을 알려주는 것에 불과하다  */

main() {

extern int data;

:

}

f1() {

extern int count;

:

}

data와 count를 전역적으로 선언해 놓고 main과 f1 내에서 또 선언하고 있다. 이와 같이 중복되게 선언하게 되면 이 둘의 선언이(즉 데이터 유형이) 같을 때에는 그냥 무시하고 지나 가지만 다를 경우에는 에러가 발생하게 된다. 따라서 위의 프로그램의 경우 data는 두 개의 선언이 같기 때문에 별 문제가 없지만 count의 경우에는 전역적으로 char 유형으로 선언하고 f1에서는 이를 다시 int로 선언했기 때문에 컴파일시 에러가 발생하게 된다.

⑶ static 메모리 유형

static 메모리 유형의 변수들은 메모리 유형앞에 static이라고 선언된 변수들을 의미하며 지역 static 메모리 유형과 전역 static 메모리 유형이 있다.

지역 static 메모리 유형의 변수들이란 auto 메모리 유형의 변수들에 auto대신 static이 붙은 변수들을 의미하는데, 지역 static 메모리 유형의 변수들이나 auto 유형의 변수들이나 별로 차이가 없는 것처럼 보인다. 실제로도 지역 static 메모리 유형의 변수들의 동작 범위는 auto 변수의 경우와 같이 자신이 정의된 함수나 블럭 내에서만 사용할 수 있다. 그러나 존속 기간의 경우 지역 static 메모리 유형의 변수들은 extern 변수와 같다. 즉 전체 프로그램이 수행 되는 동안 계속해서 살아 있으면서 자신의 값을 유지하게 된다. 다음은 이의 한 예이다.

void test() {

static int s_count;        /*  static 메모리 유형은 초기화시키지 않았을 경우  */

int a_count = 0;                                         /*  0의 값을 가진다  */

s_count++;

a_count++;

printf("static count = %2d\tauto count = %d\n",s_count,a_count);

}

main() {

int i;

for (i = 0; i < 10; i++)

test();

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

static count =  1       auto count = 1

static count =  2       auto count = 1

static count =  3       auto count = 1

static count =  4       auto count = 1

static count =  5       auto count = 1

static count =  6       auto count = 1

static count =  7       auto count = 1

static count =  8       auto count = 1

static count =  9       auto count = 1

static count = 10       auto count = 1

위 프로그램에서는 test란 함수를 10번 부르고 있다. test 내에는 static 변수 s_count와 auto 변수 a_count가 정의 되어 있는데 이들의 값을 하나씩 증가시킨 후 출력하고 있다. 그리고 끝나게 되는데, 이 때 auto 변수인 a_count는 사라지지만 static 변수인 s_count는 사라지지 않고 자신의 값을 갖고 있게 된다. 즉 동작 범위는 auto와 같으나 존속 기간은 extern과 같다.

static 변수들은 프로그램이 시작될 당시 한번만 초기화 되며 한 번 값을 가지면 그 값을 프로그램이 끝날 때까지 갖게 되므로 extern 변수와 같이 메모리를 차지하고 있게 된다.

함수 내에 있으면서 자신의 값을 계속 유지해야할 필요가 있는 경우에 static 변수를 사용하면 된다.

전역 static 메모리 유형의 변수들은 extern 메모리 유형의 변수들과 같이 함수 바깥쪽에 정의된 변수들 가운데 메모리 유형이 static이라고 정의된 변수들을 의미한다.

예를 들어 다음 프로그램의 경우.

int i = 10;

static int j = 3; □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

□

main() {                                           □

int k = 1;                                       □

printf("i = %d, j = %d, k = %d\n",i,j,k);        □

proc1();                                         □

}                                                □ j의 동작 범위

□

char c = 'A';                                      □

static char d = 'B'; □□□□□□□□□□                    □

□                    □

proc1() {                     □ d의 동작 범위      □

printf("%c%c\n",c,d);       □                    □

} ←□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

전역 static 메모리 유형의 변수들은 static만 없으면 바로 extern 메모리 유형의 변수들이다. 그래서 extern 메모리 유형의 변수들과 유사한 성질도 갖고 있는데 존속 기간이 extern 메모리 유형의 변수들과 같다. 즉 프로그램이 수행되는 동안 계속해서 자신의 값을 갖고 있게 된다. 그러나 extern 메모리 유형의 변수들이 변수 선언이라는 것만 하면 프로그램의 어느 부분에서도 이를 사용할 수 있었는데 반해, 이 전역 static 메모리 유형의 변수들은 철저하게 자신이 정의된 위치부터 그 화일의 끝까지가 자신의 동작 범위가 된다. 따라서 그 외의 부분에서는 변수 선언을 해도 이 변수를 사용할 수 없다.

전역 static 메모리 유형은 변수를 다른 함수나 프로그램으로부터 감추는 역할을 한다.

/*  main.c  */

main() {

extern int count;

int i = 5;

f1();

printf("i = %d, count = %d\n",i,count);

}

/*  f1.c  */

static int count = 10;

f1() {

++count;

}

앞에서 언급한 바와 같이 count변수는 f1.c 내에서만 사용할 수 있기 때문에 main.c에서 extern으로 선언한 count변수는 없는 것이 되며(f1.c의 count 변수는 다른 곳에서는 사용하지 못하므로 없는 것과 마찬가지이다) 링크시 에러가 발생하게 된다.

위에서와 같이 static 으로 선언한 경우 현재 화일에서만 그 변수의 사용을 가능하게 만들어 주어 같은 이름의 변수가 다른 화일에 존재하여도 간섭을 받지 않는다.

⑶ register 메모리 유형

컴퓨터의 CPU 안에는 레지스터라는 기억장소가 존재한다. 이러한 레지스터들은 CPU가 처리하는 논리 연산작용에서 일시 기억장소로 쓰이거나 프로그램을 실행해 나가는 과정에서 CPU에게 필요한 중요한 데이터들의 보관 장소로 이용된다. 레지스터는 보통 변수의 기억 장소로 쓰이는 RAM 보다 입출력 속도가 매우 빠르다. IBM-PC의 8086 CPU의 레지스터는 모두 16비트 레지스터로서 그 중에는 그 값을 바꾸어도 별로 상관 없는 범용 레지스터라는 것들이 존재하는데 그 레지스터들을 정수형 변수처럼 사용하는 것이 레지스터 변수이다. 아주 자주 쓰이는 변수에  이 레지스터 변수를 사용할 경우 속도가 매우 빨라진다. 그러나 int형과 unsigned int형만이 레지스터 변수로 사용될 수 있는데 그 이유는 레지스터가 16비트(2바이트)이기 때문이다. 그리고 레지스터 변수로 선언 했다고해서 항상 레지스터 변수가 되는 것은 아니고 레지스터 변수가 되도록 컴파일러에게 요구하는 정도이다(IBM-PC의 경우 register 변수가 사용할 수 있는 레지스터의 수는 1 ~ 2개 밖에는 되지 않는다).

참고로 TURBO C 에서는 레지스터 변수로 레지스터 SI와 DI를 사용하며, 굳이 레지스터 변수로 선언하지 않아도 TURBO C 컴파일러는 블럭의 처음에 나오거나 자주 쓰이는 자동변수를 레지스터 변수로 사용한다.

※ auto와 register의 비교 (MSC나 C++ Version으로 컴파일 해야 한다)

#include <stdio.h>

#include <dos.h>

void main(void) {

struct dostime_t t1, t2;

register int r;

int a, k;

long int i;

printf("\n** register **\n");

_dos_gettime(&t1);

printf("start --> %d %d.%d\n",t1.minute,t1.second,t1.hsecond);

for (k = 1; k <= 1000; k++)

for (r = 1; r <= 30000; r++);

_dos_gettime(&t2);

printf("  end --> %d %d.%d\n",t2.minute,t2.second,t2.hsecond);

i = t2.minute * 60 * 100 + t2.second * 100 + t2.hsecond -

t1.minute * 60 * 100 - t1.second * 100 - t1.hsecond;

printf("time = %ld milisecs\n",i);

printf("\n** auto **\n");

_dos_gettime(&t1);

printf("start --> %d %d.%d\n",t1.minute,t1.second,t1.hsecond);

for (k = 1; k <= 1000; k++)

for (a = 1; a <= 30000; a++);

_dos_gettime(&t2);

printf("  end --> %d %d.%d\n",t2.minute,t2.second,t2.hsecond);

i = t2.minute * 60 * 100 + t2.second * 100 + t2.hsecond -

t1.minute * 60 * 100 - t1.second * 100 - t1.hsecond;

printf("time = %ld milisecs\n",i);

}

다음은 최대 공약수와 최소 공배수를 구하는 프로그램이다.

main() {

int gcd = 1;

int n, m;

int min;

register int i;                  /*  카운터 변수를 register로 선언  */

printf("Input N and M -> ");

scanf("%d%d",&n,&m);

min = n > m ? m : n;             /*  둘 중 작은 값을 구함  */

for (i = 2; i <= min; i++)

if (n % i == 0 && m % i == 0)  /*  두 수를 다 나누면 지금까지의 최대 공약수가 됨  */

gcd = i;

printf("GCD : %d\n",gcd);

printf("LCD : %ld\n",(long int)m*n/gcd);

}

최대 공약수는 위와 같이 구하는 것보다 일반적으로 많이 사용되는 것은 큰 수를 작은 수로 계속 나누어서 떨어질 때 찾는 것이다. 나누어 떨어지게 만든 수가 바로 최대 공약수가 된다.

main() {

int gcd;

int max;

int n, m;

int t;

printf("Input N and M -> ");

scanf("%d%d",&n,&m);

gcd = n > m ? m : n;          /*  둘 중 작은 값을 구함  */

max = n > m ? n : m;          /*  둘 중 큰 값을 구함  */

while (max % gcd != 0) {      /*  큰 값을 작은 값으로 계속 나누어 떨어질 때까지  */

t = max;                    /*  큰 값과 작은 값을 서로 바꿈  */

max = gcd;

gcd = t % gcd;

}

printf("GCD : %d\n",gcd);

printf("LCD : %ld\n",(long int)m*n/gcd);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input N and M -> 270 720 □

GCD : 90

LCD : 2160

※ 변수들의 영역 규칙

동작 범위에 따라 변수를 나누면 메모리 유형에 따라 전역 변수와 지역 변수로 나눌 수 있다. 즉 extern 메모리 유형과 전역 static 메모리 유형의 전역 변수와 auto메모리 유형과 지역 static메모리 유형의 지역 변수로 나눌 수 있는데, 이들 변수들이 서로 같은 이름을 가질 수 있다. 예를 들어 다음 프로그램의 경우.

int i = 1, j = 2;

void main() {

int i = 3;                               /*  int l = 3;  */

int k = 4;

i += j + i;                              /*  l += j + l;  */

k *= i - j;                              /*  k *= l - j;  */

printf("i = %d, j = %d\n",i,j);          /*  printf("i = %d, j = %d\n",l,j);  */

{ int i = 5, k = 6;                      /*  int m = 5, n = 6;  */

i = ++j;                               /*  m = ++j;  */

k = ++i;                               /*  n = ++m;  */

printf("i = %d, j = %d\n",i,j);        /*  printf("i = %d, j = %d\n",m,j);  */

}

i = f1(j);

j = f1(k);

printf("i = %d, j = %d\n",i,j);          /*  printf("i = %d, j = %d\n",l,j);  */

}                                        /*  다른 변수는 다른 이름을 사용한다  */

f1(int x) {

return (x + ++i - j--);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

i = 8, j = 2

i = 4, j = 3

i = 2, j = 25

프로그램의 한 곳에서 사용할 수 있는 같은 이름의 변수가 여러 개 있으면 가장 가까운 곳에 정의된 변수만 사용하기 때문에 결국 나머지 변수들은 사용할 수 없다. 위의 프로그램의 경우 main내에서 바깥쪽의 i는 사용할 수 없으며 블럭 내에서 main의 k는 사용할 수 없다.

< 함수의 메모리 유형 >

함수도 변수와 같이 메모리 유형이 있는데 다음과 같이 두 개의 메모리 유형이 있다.

⑴ extern 메모리 유형

⑵ static 메모리 유형

지금까지 함수를 사용하면서 메모리 유형을 사용한 적이 없다. 즉 이를 생략한 것인데 이 경우 이들 함수들은 모두 extern 메모리 유형을 갖고 있는 것이 된다. extern 메모리 유형의 함수들은 실제로 메모리 유형을 지정하지 말아야 하며 지금까지와 같이 생략해서 사용한 함수들이 extern 메모리 유형의 함수들이다. 그러나 static 메모리 유형의 함수들은 다음과 같이 static이라고 분명히 선언된 함수들을 의미한다.

static int f1(int x) {

:

}

static 메모리 유형의 함수는 자신이 정의된 화일 내에서만 불러 사용할 수 있다. 다른 화일의 프로그램에서는 사용할 수 없게 된다. 이는 전역 static 메모리 유형의 변수와 유사한데, static 메모리 유형의 함수들도 그 화일 내에서만 함수를 사용하고 다른 화일에서는 사용하지 못하도록 막고자할 때 사용한다. 다음은 이의 한 예이다.

void main() {               /*  main.c  */

int i = 10;

int s;

s = f1(i);

s += abs(s);

printf("s = %d\n",s);

}

abs(int i) {                 /*  static abs(int i) {  */

return (i > 0 ? i : -i);

}

/***********************************************************************************/

float abs(float x) {         /*  f1.c  */

return (x > 0.0f ? x : -x);

}

f1(int i) {

float f = 2.3f;

f *= i - 13.2f;

i = abs(f);

return (i);

}

위의 프로그램에서 main.c의 main에서 f1.c화일의 f1을 부르고 있으며 그 후 자신과 같은 화일의 함수 abs를 부르고 있다. 그런데 f1.c에도 같은 이름의 함수 abs가 정의되어 있고 f1에서 이를 부르고 있다. 따라서 한 프로그램 내에 같은 이름의 함수가 두 개 존재하기 때문에 링크시에 에러가 발생하게 된다. 그러나 abs를 static 메모리 유형의 함수로 정의하게 되면 아무 문제가 발생하지 않는다(실제로 이 중 하나만 static으로 정의해도 에러는 발생하지 않는다).

반면에 extern 메모리 유형의 함수는 프로그램의 어느 부분에서든지 호출하여 사용할 수 있다. 심지어 그 함수와 다른 화일에 있는 프로그램에서도 함수를 호출하여 사용할 수 있다(이때 함수를 선언해야만 사용할 수 있는 경우도 있다). 따라서 extern 메모리 유형의 함수의 동작 범위는 전체 프로그램이 된다.

< 함수의 선언 >

함수의 선언은 컴파일러에게 자신이 호출할 함수의 정보를 알려주는 역할을 한다. 즉 함수의 이름과 그 함수가 산출하는 계산 결과의 유형, 인자의 수와 각 인자의 유형등을 알려 줘서 그 후 컴파일러가 그 함수에 대한 호출문을 만났을 때 이 정보를 바탕으로 처리하도록 하는 것이다.

지금까지 함수를 사용한 예를 보면 호출하는 함수 보다 앞에 함수를 정의한 적도 있었고 오히려 뒤에 정의한 적도 있었다. 상당히 불규칙적으로 정의했었는데 실제로 그렇게 정의해야 하기 때문에 그렇게 한 것이었다.

C 컴파일러가 컴파일 도중에 함수 호출문을 만나게 되면 현재까지 처리한 내용 중에 그 함수에 대한 정의가 있는지를 찾는다. 만약 있으면 이를 바탕으로 그 호출문을 처리하게 된다. 즉 그 함수가 계산 결과로 넘겨주는 것의 유형을 찾아 그대로 오도록 해주고 인자의 유형은 맞는지 조사하게 된다. 그런데 이 경우는 함수 호출문 보다 함수가 먼저 정의되어 있는 경우이다. 즉 프로그램 순서상으로 호출될 함수가 호출한 함수 보다 먼저 나타난 경우가 이에 해당된다. 그러나 그 반대일 수도 있는데 이 경우는 호출문을 만났을 때 무조건 그 함수가 int 유형의 값을 계산 결과로 돌려주는 함수로 간주하여 처리한다. 그리고 계속해서 처리해 나가는데, 뒤에 그 호출될 함수가 int 유형의 값을 계산 결과로 돌려주는 함수면 아무 문제가 없지만 만약 다른 데이터 유형의 값을 계산 결과로 산출하는 함수일 경우 컴파일 에러가 나게 된다.

따라서 다음과 같은 사실을 알 수 있다. 함수가 int 유형의 값을 계산 결과로 산출하는 경우에는 함수 호출문 보다 앞에 와도 좋고 뒤에 와도 좋지만, int 유형이 아닌 다른 데이터 유형을 산출하는 경우에는 함수 호출문 보다 앞에 와야 한다. 지금까지의 함수들은 바로 이 규칙에 따라 작성되었다. 그래서 int 유형을 계산 결과로 산출하는 함수들은 main 다음에 왔었고 그렇지 않은 함수들은 main 보다 앞에 왔었다.

만약 함수가 다른 화일에 있는 경우에도 그 함수가 int 유형의 값을 계산 결과로 산출하는 함수라면 수행시 아무 문제가 없지만 그렇지 않은 경우에는 수행시 잘못된 결과가 나오게 된다. 이를 방지해 주는 것이 바로 함수의 선언으로 다음과 같은 형태로 사용한다.

extern int f1(int x, int y);

float f2(float, int);

extern f3();

long int f4(void);

extern void f5(int, int, ...);

위에서 첫번째는 int 유형의 값을 계산 결과로 산출하는 함수 f1을 사용하겠음을 컴파일러에게 알려주고 있다. 이의 인자는 2개가 있는데 둘 다 int 유형이라는 것도 알려주고 있다.

두번째는 float 유형의 데이터를 계산 결과로 산출하는 함수 f2를 선언하고 있는데 역시 인자는 2개이고 첫번째는 float 유형이고 두번째는 int 유형이라고 컴파일러에게 알려 주고 있다.

그러나 세번째는 단순히 f3이 int 유형의 값을 계산 결과로 산출한다고만 알려주고 있는데, 이 함수의 인자가 몇 개가 있는지는 나타나 있지 않다. 따라서 컴파일러는 계산 결과만 조사하게 되며 인자는 신경쓰지 않게 된다.

반면에 네번째 경우에는 f4가 long int 유형의 값을 계산 결과로 산출하고 인자는 없다고 확실히 선언하고 있다. void는 인자가 없음을 의미하는데, 그 후에 f4를 호출하면서 여기에 인자를 주게 되면 컴파일러는 잘못된 것으로 인식하여 에러 메시지를 내게 된다.

마지막 다섯번째는 f5가 계산 결과가 없는 함수인데 처음 두 개의 인자는 int 유형이여야 하고 그 다음의 인자는 가변적임을, 즉 인자가 더이상 없을 수도 있고 3개가 올 수도 있고 10개가 올 수도 있고 또는 100개 올 수도 있음을 나타낸다. 이는 맨 끝의 '...'이 나타내고 있으며, 따라서 f5는 printf와 같이 가변 인자 함수임을 알 수 있다.

함수의 선언은 컴파일러에게 정보를 가르켜 주는 것이 주 목적이기 때문에 인자의 이름은 전혀 중요하지가 않다. 그래서 이를 생략하여 위의 두번째 예와 같이 데이터 유형만 줄 수 있다. 또 인자의 이름을 준다 해도 그 이름이 실제 함수 정의시의 형식 인자의 이름과 같을 필요도 없다. 단지 함수 정의와 비슷하게 함수를 선언하도록 구색을 맞춘 것에 불과하다.

함수의 선언은 변수의 선언과 같이 함수나 블럭 안에 실행문이 오기 전에 하거나 아니면 함수 바깥쪽에 할 수 있는데, 이때 변수 선언과 같이 동작 범위, 즉 그것의 효력의 범위가 달라지게 된다. 함수나 블럭 내에 온 경우에는 그 함수나 블럭 내에서만 효과가 있다. 따라서 그 함수나 블럭을 벗어나게 되면 컴파일러는 이에 대한 정보를 잊게 되어 함수를 선언하지 않은 것과 같은 효과를 내게 된다. 반면에 함수 바깥쪽에 선언한 경우에는 그 위치부터 그 화일의 끝까지 효력을 유지하게 된다. 예를 들어 다음 프로그램의 경우.

void print_head();           /*  함수의 원형을 선언한다  */

main() {

int i, n = 100;

print_head();

for (i = 2; i <= n; i++)

if (prime(i))

printf("%3d ",i);

printf("\n");

}

void print_head() {

printf("==========================================================");

printf("=====================\n");

printf("                       Prime Number from 1 to 100\n");

printf("==========================================================");

printf("=====================\n");

}

int prime(int n) {

int i;

if (n % 2 == 0)

return (n == 2);          /*  짝수이고 2가 아니면 0을 되돌림  */

if (n % 3 == 0)

return (n == 3);          /*  3의 배수이고 3가 아니면 0을 되돌림  */

if (n % 5 == 0)

return (n == 5);          /*  5의 배수이고 5가 아니면 0을 되돌림  */

for (i = 7; i * i <= n; i += 2)   /*  소수임을 확인  */

if (n % i == 0)                 /*  i의 배수 이면 0을 되돌림  */

return 0;

return 1;                  /*  위의 모든 경우가 아닐 경우 소수이다  */

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

===============================================================================

Prime Number from 1 to 100

===============================================================================

2   3   5   7  11  13  17  19  23  29  31  37  41  43  47  53  59  61  67  71

73  79  83  89  97

※ 함수의 선언 내용과 실제 함수의 정의 내용이 다를 경우

변수의 선언과 마찬가지로 함수의 정의가 함수를 선언한 것과 같은 화일에 있는 경우에는 컴파일러가 이 둘이 다름을 인식할 수 있으므로 컴파일시 에러가 나게 된다. 그러나 서로 다른 화일에 있는 경우에는 컴파일러가 이 둘이 차이가 있다는 것을 모르기 때문에 선언한 대로 호출문을 컴파일하게 되어 결국 수행시 잘못된 결과가 나오게 된다. 또 변수의 선언과 같이 같은 함수를 여러 번 중복되게 선언할 수도 있다. 예를 들어 함수 바깥쪽에 전역적으로 선언해 놓고 함수 안에서 또 선언할 수도 있다. 이 경우 두 선언이 같거나 충돌의 소지가 없으면(즉 한 선언에서는 인자를 명시하지 않았는데 다른 선언에서는 인자를 명시한 것과 같이) 그냥 무시하지만 다르거나 충돌의 소지가 있으면 역시 에러 메시지를 내게 된다.

< 되부름 함수 >

되부름 함수란 자기자신을 다시 호출하는 함수를 일컫는데 자기가 자기 자신을 부르는 것을 재귀적 호출(recursive call) 또는 리커젼(recursion)이라고 한다.

다음은 하나의 문장을 입력받아 거꾸로 출력해 주는 프로그램이다.

#include <stdio.h>

void print_back() {

int ch;

if ((ch = getchar()) != '\n')   /*  한글자를 입력받고 줄바꿈 문자인지 확인  */

print_back();                 /*  줄바꿈 문자가 아닌경우 되부름  */

putchar(ch);                    /*  입력받은 문자 표시  */

}

main() {

printf("Enter a line -> ");

print_back();

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Enter a line -> Hello □

olleH

□□□□□□□                              □□□□□□□

Hello\n□□□□□□6□ \n  □                              □ \n  □1□□□□□□ \nolleH

□□□□□         □□□□□□□                              □□□□□□□          □□□□□

□□□□□□□□□□□□□5□  o  □                              □  o  □2□□□□□□□□□□□□□□

□□□□          □□□□□□□                              □□□□□□□           □□□□

□□□□□□□□□□□□□4□  l  □                              □  l  □3□□□□□□□□□□□□□□

□□□           □□□□□□□        □□□□□□□□□□□□□□>       □□□□□□□            □□□

□□□□□□□□□□□□□3□  l  □                              □  l  □4□□□□□□□□□□□□□□

□□            □□□□□□□                              □□□□□□□             □□

□□□□□□□□□□□□□2□  e  □                              □  e  □5□□□□□□□□□□□□□□

□             □□□□□□□                              □□□□□□□              □

□□□□□□□□□□□□□1□  H  □                              □  H  □6□□□□□□□□□□□□□□

□□□□□□□스택에 쌓여 올라감            □□□□□□□ 위부터 꺼내서 출력됨

우선 print_back() 함수를 보면 하나의 문자를 입력받아 그 아스키 코드를 정수형으로 되돌리는 함수인 getchar() 함수를 호출하여 ch에 대입한 후 그 값이 줄바꿈 문자가 아니면 다시 되부름 호출을 한다. 이렇게 계속 나가다가 줄바꿈 문자를 만난 시점에서 되부름 호출을 하지 않고 putchar(ch)를 실행하여 화면에 하나의 문자를 표시하는 과정을 문장의 처음까지 반복하게 되어 문자가 거꾸로 찍히게 된다.

다음은 재귀적 호출을 사용하여 N!을 구하는 프로그램이다.

long int fact(int n) {

if (n <= 1)

return (1);

else

return (n * fact(n - 1));

}

main() {

int num;

for (num = 1; num <= 10; num++)

printf("%2d! = %8ld\n",num,fact(num));

}

위에서 fact함수를 보면 형식인자 n의 값이 1과 같거나 작을 경우 1을 되돌리고 그렇지 않을 경우에는 n과 fact함수에 n - 1의 값을 다시넣어 호출한 값을 되돌린다.

이 되부름 함수를 다시 차근차근 따져보면, 'fact(1)'로 호출했을 경우에는 처음의 if문에 걸려 1을 되돌리는 것이 전부이며 'fact(2)'로 호출했을 경우에는 '2 * fact(2 - 1)'을 되돌리게 되는데 fact(1) = 1이므로 결국 2를 되돌린다. 이러한 식으로 'fact(3) = (3 * fact(3 - 1)) = (3 * (2 * fact(2 - 1))) = (3 * (2 * 1))) = 6이 된다.

double power(int x, int y) {

return ((y == 0) ? 1 : x * power(x, y - 1));

}

main() {

int x = 10, y;

double d;

for (y = 15; y <= 20; y++) {

d = power(x,y);

printf("%2d ^ %2d = %21.0lf\n",x,y,d);

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

10 ^ 15 =      1000000000000000

10 ^ 16 =     10000000000000000

10 ^ 17 =    100000000000000000

10 ^ 18 =   1000000000000000000

10 ^ 19 =  10000000000000000000

10 ^ 20 = 100000000000000000000

※ 되부름 함수는 스택이라는 메모리를 매우 많이 소비한다. 특히 지역변수인 자동변수가 많은 되부름 함수일 경우 더욱 많은 메모리를 사용한다. 실행중 너무 많은 되부름으로 인해 컴퓨터가 작동을 중지하거나 프로그램에 이상이 생기는 수가 발생할 수도 있다. 이러한 단점 외에도 되부름 함수를 사용함으로써 보통 함수를 사용했을 때보다 더 속도가 느려지거나 프로그램을 이해하기 어려운 상태로 만들어 버리는 경우가 있다.

※ 다음은 입력한 텍스트 가운데 다음과 같은 스트링이 몇번 나타나는지를 계산하는 프로그램이다.

if, the, who, while, do, text, tree

이 경우 찾는 스트링이 i, w, d, t로만 시작하므로 맨 처음 글자가 i, w, d, t일 경우만 신경쓰면된다. 만약 if를 찾을 경우, 입력한 글자가 i였을 때 그 다음 글자가 f이면 이는 if가 온 것이므로 if의 갯수를 기억하고 있는 변수 값을 증가시키면 된다. 이때 입력한 글자가 i인 것을 하나의 상태라고 하면 이 상태에서 입력이 f가 들어오면 if의 카운터를 증가시키고 다시 처음으로 가면 되고 그 외의 다른 입력이 들어오면 다시 처음 상태로 가면 된다. 이를 그림으로 나타내면 다음과 같다.

모든 입력

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

□      그외의 입력                                 □

□ □□□□□□□□□□□□□□□□□□□□□□□□□                        □

□ □            i          □           f            □

S0 □□□□□□□□□□□□□□□□□□□□□> S1 □□□□□□□□□□□□□□□□□□□□□> S2  if의 카운터 증가

위에서 S0은 처음 상태이며 여기서 i가 들어오면 S1의 상태로 감을 의미한다. S1상태에서 f가 들어오면 S2는 if의 상태가 되며 그 외의 다른 입력이 들어 오면 다시 S0의 상태로 가게 된다. S2의 상태에서는 무조건 if의 카운터를 증가시키게 되며 그리고 역시 무조건 S0으로 가게 된다(이는 if가 입력되었기 때문이다).

이와 같은 방법으로 위의 7개 스트링(if, who, while, do, the, text, tree)에 대해 상태 전이 다이어그램(state transition diagram)을 작성하면 다음과 같이 된다.

i          f

S0 □□□□□□□> S1 □□□□□□□> S2 [if발견]

□    w           h           o

□□□□□□□□□> S3 □□□□□□□> S4 □□□□□□□> S5 [who발견]

□                      □    i           l           e

□                      □□□□□□□□□> S6 □□□□□□□> S7 □□□□□□□> S8 [while발견]

□    d           o

□□□□□□□□□> S9 □□□□□□□> S10 [do발견]

□    t            h           e

□□□□□□□□□> S11 □□□□□□□> S12 □□□□□□□> S13 [the발견]

□   e           x           t

□□□□□□□□> S14 □□□□□□□> S15 □□□□□□□> S16 [text발견]

□   r           e           e

□□□□□□□□> S17 □□□□□□□> S18 □□□□□□□> S19 [tree발견]

위에서 없는 입력은 모두 S0으로 가며 이는 그림에 나타나 있지 않다.

다음은 이것을 프로그램으로 옮긴 것이다.

#include <stdio.h>

void main() {

char c;

int ifno = 0;

int whono = 0;

int whileno = 0;

int dono = 0;

int theno = 0;

int textno = 0;

int treeno = 0;

int status = 0;         /*  현재의 상태  */

while ((c = getchar()) != EOF) {

switch (c) {

case 'i': if (status == 0)

status = 1;

else if (status == 4)

status = 6;

else {

ungetc(c,stdin);

status = 0;

}

break;

case 'f': if (status == 1)

++ifno;

status = 0;

break;

case 'w': if (status == 0)

status = 3;

else {

ungetc(c,stdin);

status = 0;

}

break;

case 'd': if (status == 0)

status = 9;

else {

ungetc(c,stdin);

status = 0;

}

break;

case 't': if (status == 0)

status = 11;

else if (status == 15) {

++textno;

status = 0;

}

else {

ungetc(c,stdin);

status = 0;

}

break;

case 'h': if (status == 3)

status = 4;

else if (status == 11)

status = 12;

else

status = 0;

break;

case 'o': if (status == 9)

++dono;

else if (status == 4)

++whono;

status = 0;

break;

case 'l': if (status == 6)

status = 7;

else

status = 0;

break;

case 'r': if (status == 11)

status = 17;

else

status = 0;

break;

case 'x': if (status == 14)

status = 15;

else

status = 0;

break;

case 'e': if (status == 7) {

++whileno;

status = 0;

}

else if (status == 12) {

++theno;

status = 0;

}

else if (status == 11)

status = 14;

else if (status == 14)

status = 15;

else if (status == 17)

status = 18;

else if (status == 18) {

++treeno;

status = 0;

}

else

status = 0;

break;

default : status = 0;

}

}

printf("   Number of if: %d\n",ifno);

printf("  Number of who: %d\n",whono);

printf("Number of while: %d\n",whileno);

printf("   Number of do: %d\n",dono);

printf("  Number of the: %d\n",theno);

printf(" Number of text: %d\n",textno);

printf(" Number of tree: %d\n",treeno);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>test < test.c □

Number of if: 24

Number of who: 4

Number of while: 5

Number of do: 4

Number of the: 4

Number of text: 4

Number of tree: 4

Ⅶ. 포인터

< 포인터 변수의 정의 >

포인터는 주소를 나타내는 데이터 유형이다. 다시 말해서 포인터 데이터 유형의 값은 주소라는 얘기가 되며, 이 주소로 역시 숫자를 사용하고 있다. 단지 주소에는 음수가 있을 수 없기 때문에 포인터 데이터 유형의 주소는 양의 정수가 되며 외관상으로 unsigned 데이터 유형과 같게 된다.

다음은 포인터 변수를 정의한 몇 가지 예이다.

⑴ int *ip;

⑵ char *cp;

⑶ int *ip, *jp;

⑷ int *ip, jp;

지금까지의 변수 정의와 다른것은 변수 앞에 '*'가 있는 것인데, '*'는 뒤의 변수가 포인터 유형의 변수임을 나타내는 역할을 한다. 위에서 ⑴은 int 유형의 데이터가 기억되어 있는 주기억 장치의 주소를 값으로 갖는 포인터 변수 ip를 정의한 것이고, ⑵는 포인터 변수 cp를 정의한 것으로 이 주소에는 char 유형의 데이터가 있음을 의미한다. ⑶은 같은 데이터 유형의 포인터 변수들을 한번에 같이 정의한 것으로, 이때 각 포인터 변수 앞에 '*'를 붙여야 한다. 그러나 ⑷와 같이 정의할 경우 ip는 포인터 변수이지만 jp는 그냥 int 유형의 변수가 되므로 주의해야 한다.

실제로 위의 변수들은 주소를 값으로 갖는다는 점에서는 같다. 그러나 그 주소에 어떤 데이터가 있느냐가 다르기 때문에 ip의 값을 cp에 할당할 수 없으며, 마찬가지로 cp의 값을 ip에 할당할 수없다.

※ far 포인터 변수와 near 포인터 변수, 그리고 huge 포인터 변수

IBM-PC의 주소는 2가지가 존재하는데, 우선 가장 많이 사용되는 near 포인터의 경우에는 오프셋 값만 주소로 갖게 된다. 이 경우 세그먼트 주소는 고정되어 있음을 의미한다. 따라서 near 포인터 변수의 크기는 16비트가 되며 현재 고정된 세그먼트 주소로부터 64K 바이트 이내의 값만 주소로 갖게 된다. 반면에 far 포인터 변수는 세그먼트 주소와 오프셋을 각각 16비트씩 값으로 갖는다. 따라서 far 포인터 변수는 32비트의 크기를 갖게 되며 주기억 장치의 어느 곳이나 주소를 값으로 가질 수 있다. huge 포인터는 far 포인터와 유사하나 오프셋의 값이 항상 0에서 15사이의 값을 갖도록 만든 것이 다르다. 이 주소를 사용하면 주기억 장치의 한 장소 중에서 주소가 딱 하나로 결정되는 장점이 있다.

포인터를 far 또는 near, huge로 선언하고자 할 때에는 다음과 같이 변수 이름과 데이터 유형 사이에 키워드를 집어 넣으면 된다.

int near *ip;             /*  포인터 앞에 아무것도 없을 경우에는 사용하는  */

char far *cp;             /*  메모리 모델에 의해 near, far, huge 등이 결정된다  */

float huge *fp;

많이 사용하는 small 메모리 모델의 경우에는 모두 near 포인터가 되며 large 메모리 모델의 경우에는 모두 far 포인터가 된다.

※ IBM-PC에서의 주소

세그먼트 주소 : 주기억 장치를 16개의 바이트로 나눈 것으로, 이 나눈 것에 주소를 붙인 것이 세그먼트 주소이다. IBM-PC의 주소의 크기가 20비트이기 때문에, 이는 1M 바이트가 되며 이를 16바이트로 나누게 되면 65536 = 216개의 세그먼트가 존재하게 된다. 따라서 세그먼트 주소의 크기는 16비트가 된다.

오프셋 : 한 세그먼트 내의 바이트의 위치를 나타낸다. 한 세그먼트의 크기가 16바이트이므로 이의 위치는 0에서 15까지의 수로, 즉 4비트 크기의 주소로 나타낼 수 있다. 따라서 세그먼트 주소와 오프셋을 합하면 바로 한 바이트를 나타내는 주소가 된다. 그런데 IBM-PC에서는 특이하게도 오프셋을 16비트로 표현하고 있다. 따라서 세그먼트의 주소가 1000(16)이고 오프셋이 2343(16)인 경우 이것이 나타내는 바이트의 주소는 다음과 같다.

10000(16) + 2343(16) = 12343(16)

위와 같이 바이트를 서로 다른 여러 개의 세그먼트 주소와 오프셋의 조합으로 나타낼 수 있음을 알 수 있다. 일반적으로 세그먼트 주소는 고정시켜 놓고 오프셋만 주소로 사용하는데, 이 경우 오프셋의 주소가 16비트이므로 최고 64K 바이트의 주기억 장치까지만 사용할 수 있다.

< '&' 연산자 >

포인터 유형의 변수는 주소를 값으로 가지기 때문에 이를 사용하려면 주소를 할당하여야 하는데  C에서는 이를 위해 '&' 연산자를 제공하고 있다. 이는 다음과 같은 형태로 사용한다.

&변수이름

계산 결과는 바로 뒤의 변수의 현재 주소가 된다(이 주소도 역시 메모리 모델에 따라 near 주소가 되거나 far, huge 주소가 되며, 특정 주소로 변경하려면 (int far *)와 같이 형 변환 연산자를 사용하면 된다). 주소는 양의 정수이기 때문에 & 연산자의 계산 결과는 양의 정수값이 된다. 따라서 이 값을 %u나 %lu를 사용하여 출력시키면 변수의 주소를 출력할 수 있다.

int i;

char c;

float f;

double d;

static int si;

static char sc;

static float sf;

static double sd;

main() {

int mi;

char mc;

float mf;

double md;

printf("Address of i = %u\n",&i);

printf("Address of c = %u\n",&c);

printf("Address of f = %u\n",&f);

printf("Address of d = %u\n",&d);

printf("Address of si = %u\n",&si);

printf("Address of sc = %u\n",&sc);

printf("Address of sf = %u\n",&sf);

printf("Address of sd = %u\n",&sd);

printf("Address of mi = %u\n",&mi);

printf("Address of mc = %u\n",&mc);

printf("Address of mf = %u\n",&mf);

printf("Address of md = %u\n",&md);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Address of i = 2017

Address of c = 2004

Address of f = 2013

Address of d = 2005

Address of si = 2032

Address of sc = 2019

Address of sf = 2028

Address of sd = 2020

Address of mi = 65476

Address of mc = 65479

Address of mf = 65480

Address of md = 65484

auto 메모리 유형의 변수들과 extern, static 메모리 유형의 변수들의 주소가 크게 차이가 남을 볼 수 있는데, auto 유형의 변수들은 스택(stack)이라고 불리는 메모리 영역에 할당된다. 스택은 프로그램이 수행됨에 따라 그 크기가 가변적으로 변하는데, auto 유형의 변수들도 이에 맞추어 자신이 선언된 함수나 블럭에 있을 때에는 스택에 존재하고 그 외의 경우에는 없어지게 된다. 반면에 extern과 static 유형의 변수들은 힙(heap)이라고 불리는 영역에 할당된다. 이는 별도로 존재하기 때문에 존속 기간이 전체 프로그램의 수행 시간과 같다.

'&' 연산자는 위와 같이 변수의 주소를 계산하기 위해서 있는 것은 아니다. 바로 포인터 변수에 주소를 할당할 때 이 '&' 연산자를 사용한다.

int i = 10;

int *ip;

ip = &i;

포인터 변수 ip가 int 유형의 주소를 값으로 갖는 변수이기 때문에 같은 int 유형의 변수인 i의 주소를 할당 받고 있다. 그러나 다음과 같이 할당할 수는 없다.

char c;

int *ip;

ip = &c;

c가 int 유형이 아닌 char 유형의 변수이기 때문이다. 포인터의 경우에 같은 유형의 포인터 값이 아니면 컴파일시에 바로 에러가 나게 된다. 그러나 다음과 같이 형 변환을 사용하면 다른 데이터 유형의 주소를 포인터 변수에 할당할 수 있다.

char c;

int *ip;

ip = (int *)&c;

또 '&' 연산자를 사용하면 포인터 유형의 변수들에게 다음과 같이 초기값을 줄 수 있다.

int i = 10;

int *ip = &i;

char c = 'A';

char *cp = &c;

'&' 연산자는 변수 이외의 것에는 사용할 수 없으며, 따라서 다음과 같이 사용할 수 없다.

⑴ &3                 /*  3은 변수가 아니라 상수이다  */

⑵ &(x+y)             /*  x+y는 변수가 아니라 수식이다  */

⑶ &&x                /*  &(&x)가 되어 &(주소)가 되는데, 주소는 변수가 아니다  */

⑷ register int x;    /*  register 유형의 변수는 주기억 장치가 아닌 레지스터에  */

&x;                /*  할당되기 때문에 주소라는 것이 있을 수가 없다  */

※ 포인터 변수에 바로 주소를 할당

주소는 양의 정수이기 때문에 다음과 같이 직접 주소를 할당하는 것이 실제로 가능하다

int *ip = (int *) 2000;       /*  반드시 형 변환 연산자를 사용하여야 한다  */

위와 같이 직접 주소를 할당하였을 때에는 당연히 그 주소에 해당 데이터 유형의 데이터가 있어야 한다. 예를 들어 위의 경우에 2000번지에 int 유형의 데이터가 있어야 한다. 그렇지 않은 경우에는 위의 포인터를 사용할 때 에러가 발생하게 된다.

PC의 경우에는 주소가 고정되어 있는 경우가 많기 때문에 위와 같이 주소를 직접 지정해 주는 것이 의미가 있는데, 특히 비디오 메모리를 액세스할 때에는 직접 주소를 할당하는 것이 더 효율적인 경우가 많다.

※ NULL 포인터

포인터가 가질 수 있는 값 중에 NULL이란 것이 있는데, 먼저 #include <stdio.h>를 선언해야만  사용할 수 있다. 이는 실제로 0의 값인데 포인터 변수가 이 값을 갖게 되면 0번지를 값으로 갖는 것이 아니라 아직 값이 없는 것이 된다. 따라서 포인터 변수에 초기값을 주고 싶을 때에는 이 NULL을 다음과 같이 주면 된다.

#include <stdio.h>

char *cp = NULL;

:

< '*' 연산자 >

포인터 변수를 사용하는 목적은 그 변수가 값으로 갖고 있는 주소의 데이터를 사용하기 위해서이다. 포인터 변수가 값으로 갖고 있는 주소의 데이터를 사용하고자 할 때에는 '*'를 사용하면 되는데, 다음과 같은 형태로 사용한다.

int i = 100, j;

int *ip = &i;

j = *ip;

ip가 i의 주소를 값으로 갖고 있기 때문에 *ip는 ip가 갖고 있는 주소의 값, 즉 i의 값이 된다. 따라서 j는 i의 값인 100을 갖게 되며 결국 j = *ip; 는 j = i; 와 같은 의미를 갖게 된다.

또 다음의 경우를 보면

int i = 10, j;

int *ip = &i, *jp;

jp = ip;

j = *jp + *ip;

ip가 i의 주소를 갖고 있는데 ip의 값을 jp에 할당했으므로 jp도 i의 주소를 갖고 있게 된다. 따라서 *ip와 *jp는 모두 i가 되며 따라서 위의 j = *jp + *ip는 j = i + i가 되어 j의 값은 20이 된다.

위에서는 ip와 jp 둘 다 int 유형의 데이터의 주소를 갖는 포인터였기 때문에 jp = ip와 같이 값을 그대로 할당할 수 있었지만 만약 서로 다른 데이터 유형의 주소를 값으로 갖는 포인터 였다면 이와 같이 할당할 수는 없고 다음과 같이 형 변환 연산자를 사용해야만 한다.

int i = 10, *ip = &i;

char c = 'A', *cp = &c;

cp = (char *)ip;

그러나 위와 같이 했다고 해서 cp의 값과 ip의 값이 다른 것은 아니다. 둘 다 변수 i의 주소를 값으로 갖게 되는데 '*' 연산자를 사용할 때 차이가 발생한다. 즉 *ip의 경우에는 주소에 int 유형의 데이터가 있다고 간주하여 그 주소로부터 2바이트의 값을 가져 오는 반면 *cp의 경우에는 주소에 char 유형의 데이터가 있다고 간주하여 그 주소로부터 1바이트의 값만 갖고 오게 된다.

다음에 나오는 두 프로그램은 이를 잘 나타내고 있다.

main() {

int i;             /*  int형 변수의 내용은 필요로한 int값을 저장할 수 있고  */

int *iptr;         /*  int형 포인터 변수의 내용은 int형 변수의 번지수를 저장

할 수 있다  */

i = 256;           /*  256 == 0x100  */

iptr = &i;

printf("%d\n",*iptr);    /*  *iptr은 iptr이 가리키고 있는 메모리의 내용  */

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

256

main() {

int i;

char *iptr;

i = 256;

iptr = (char *)&i;      /*  형 변환 연산자를 사용하지 않으면 경고 메시지가 출력된다  */

printf("%d\n",*iptr);   /*  iptr이 char형이기 때문에 1바이트만을 참조한다  */

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

0

위 프로그램의 메모리 상태를 그림으로 보면 다음과 같다.

□□□□□□

30AB □ 00 □□□□□□□□□□□□  변수 i가 만들어진 장소 (내용이 들어있는 상태를 보면 앞뒤가

□□□□□□   □        바뀌어서 들었음을 볼 수 있는데 이것은 IBM-PC의 특성상 그러한

30AC □ 01 □□□□□        방법으로 데이터가 들어가게 된다)

□□□□□□□□□□□□□□□□□

30AD □ AB □□□□□□□□□□□□ 포인터 변수 iptr이 만들어진 장소

□□□□□□   □        (iptr이 int형일 때 30AB부터 30AC까지 2바이트를 참조하는 반면

30AE □ 30 □□□□□         iptr이 char형이기 때문에 30AB번지의 1바이트 만을 참조한다.

□□□□□□□□□□□□□□□□□        그렇기 때문에 결과는 0이 된다)

이처럼 메모리를 차지하는 데는 포인터의 형이 영향을 미치지 못하지만 메모리를 참조하는 데는 커다란 차이가 있다.

이상의 결과를 다시한번 정리하면 다음과 같다.

int *iptr; 이라고 정의했을 때

iptr : 다른 int형 변수의 번지를 저장한다.

printf("%p",iptr);

† iptr이 가지고 있는 번지(i의 주소)가 화면에 표시된다.

*iptr : iptr이 가리키고 있는 번지의 내용에 해당된다. 즉 *iptr과 i는 같은 것이

되는데 iptr이 i를 가리킬 때, *iptr은 i의 (alias : 별명)다른 명칭이라 한다.

printf("%d",*iptr);

int i; 라고 정의했을 때

i : 메모리상에 정수값을 저장할 장소를 만든다.

&i : i가 만들어진 메모리상의 번지를 의미한다.

'*' 연산자의 계산 결과는 그 주소의 값이지만, 그 주소의 변수와 똑같이 사용할 수 있다는 것이다. 즉 반드시 변수가 와야 하는 곳에 '*' 연산자가 붙은 수식이 올 수도 있다. 예를 들면 다음과 같다.

int i = 10, *ip = &i;

*ip = 100;

++*ip;

ip = &*ip;

'='의 왼쪽이나 ++ 연산자와 & 연산자의 인자에는 반드시 변수가 와야 하는데, 위에서는 '*' 연산자가 오고 있다. '*' 연산자는 그것이 주소를 갖고 있는 변수 자체를 의미하기 때문에 이것이 가능하게 된다.

ip가 i의 주소를 값으로 갖고 있기 때문에 *ip는 바로 i가 된다. 따라서 *ip = 100은 i = 100과 같으며 i의 값이 100으로 변하게 된다. 결국 i 변수 자체를 사용하지 않고 다른 방법에 의해 i의 값을 변경한 것이 된다. 마찬가지로 ++*ip는 ++i와 같기 때문에 i의 값이 1증가하여 101이 된다. 마지막의 ip = &*ip는 *ip가 i이므로 ip = &i가 되어 결국 자신의 값을 그대로 유지하게 된다.

하나 더 주의할 것은 ++*ip와 *ip++가 다르다는 것이다. ++와 '*'는 우선 순위가 같으나 결합방향이 오른쪽에서 왼쪽이기 때문에 ++*ip는 ++(*ip)로 처리되어 결국 ++i가 되지만 *ip++의 경우에는 *(ip++)로 처리된다. 이때 ip뒤에 ++가 붙었기 때문에 이는 나중에 계산되므로 *ip가 되고 이를 계산한 후(이는 i이다) ip++에 의해 ip의 값을 증가하게 된다. 따라서 이 경우 i의 값은 변하지 않고 오히려 ip의 값이 변하게 된다. 만약 *ip에 대해 ++를 뒤에 적용하고 싶으면 (*ip)++와 같이 괄호를 사용하면 된다.

< 포인터를 이용한 참조에 의한 호출 >

C에서는 모든 것을 값에 의한 호출 형태로 인자를 전달하고 있다. 값만 넘어가기 때문에 받은 쪽에서는 부른 쪽의 데이터를 변경할 수 없게 된다. 반면에 참조에 의한 호출의 경우에는 변수 자체가 넘어가기 때문에 받은 쪽에서 부른 쪽의 데이터를 변경할 수 있게 되는데 C에서는 참조에 의한 호출을 전혀 지원하지 않기 때문에 이를 기본적으로는 할 수 없지만 포인터를 사용하면 이와 같은 효과를 낼 수 있다. 즉 실인자에 값을 주는 것이 아니라 변수가 할당되어 있는 번지를 건내주므로써 간접적인 참조에 의한 호출을 행한다. 다음이 그 프로그램이다.

main() {

void swap(int *, int *);

int i = 10, j = 20;

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

swap(&i,&j);                     /*  □&□연산자를 사용하여 주소를 넘겨주고 있다  */

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

}

void swap(int *ip, int *jp) {      /*  받는 쪽은 주소를 받아야 하므로 포인터가 된다  */

int t;

t = *ip;

*ip = *jp;

*jp = t;

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

i = 10, j = 20

i = 20, j = 10

결과적으로 변수 i와 j를 넘겨준 참조에 의한 호출과 같은 결과를 낳게 되었다. 그러나 실제로 참조에 의한 호출로 인자가 전달된 것은 아니다. 역시 값만 넘어 갔는데 그 값이 바로 주소였기 때문에 swap에서 main의 변수를 사용할 수 있었던 것이다. 즉 어떤 변수를 참조에 의한 호출로 전달하고자 할 때에는 부르는 쪽에서는 그 변수의 주소를 넘겨주고, 받는 쪽에서는 이에 대한 포인터 변수로 받으면 된다. 그러면 받는 쪽에서 부르는 쪽의 변수를 마음대로 사용할 수가 있게 된다. 다음은 이의 몇 가지 예이다.

#include <stdio.h>

read_number(long int *res) {        /*  하나의 정수를 읽음  */

int base = 10;                     /*  기본 진법은 10  */

int sign = 1;                      /*  기본 부호는 +  */

char c;

void skip_last(void);              /*  함수 선언  */

*res = 0l;

again: while ((c = getchar()) == ' ' || c == '\t');

/*  처음 <Space>와 <Tab>은 무시  */

switch (c) {

case 'd': break;                 /*  d로 시작하면 10진수  */

case 'b': base = 2;              /*  b로 시작하면 2진수  */

break;

case 'o': base = 8;              /*  o로 시작하면 8진수  */

break;

case 'x': base = 16;             /*  x로 시작하면 16진수  */

break;

case '+': goto again;            /*  +면 부호에 변화가 없음  */

case '-': sign *= -1;            /*  -면 부호가 정반대로  */

goto again;

case EOF: return (EOF);

default : ungetc(c,stdin);       /*  하나 더 읽을 것을 다음에 읽기 위해 보관  */

}

while ((c = getchar()) != EOF && (((base != 16) && (c >= '0') &&

(c - '0' <= base - 1)) || ((base == 16) && ((c >= '0' && c <= '9') ||

(c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))))     /*  읽은 문자가 진법에

맞는 동안  */

switch (c) {

case 'a':                      /*  16진수의 A  */

case 'A': *res = *res * base + 10l;

break;

case 'b':                      /*  16진수의 B  */

case 'B': *res = *res * base + 11l;

break;

case 'c':                      /*  16진수의 C  */

case 'C': *res = *res * base + 12l;

break;

case 'd':                      /*  16진수의 D  */

case 'D': *res = *res * base + 13l;

break;

case 'e':                      /*  16진수의 E  */

case 'E': *res = *res * base + 14l;

break;

case 'f':                      /*  16진수의 F  */

case 'F': *res = *res * base + 15l;

break;

default : *res = *res * base + c - '0';     /*  읽어 들인 문자를 숫자로 변환  */

}

if (c == EOF)                    /*  화일의 끝이면 할 것이 없음  */

return (EOF);

if (c == ' ' || c == '\t')       /*  나머지 <Space>와 <Tab>을 무시  */

skip_last();

if (c == '\n' || c == ' ' || c == '\t') {

*res *= sign;                  /*  지금까지의 결과에 부호를 곱함  */

return (1);

}

return (0);                         /*  그렇지 않으면 에러  */

}

void skip_last() {                    /*  '\n'이 나올 때까지 읽어 버리는 함수  */

while (getchar() != '\n');

}

main() {

long int i;

long int sum = 0;

int status;

while ((status = read_number(&i)) != EOF) {    /*  입력의 끝까지 숫자를 읽어  */

if (status == 0) {

printf("Input error!!\n");

return (1);

}

sum += i;                        /*  합을 구함  */

}

printf("The sum is %ld.\n",sum);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

b11000111 □

o234567 □

xabcdef □

2525 □

^Z □

The sum is 11342346.

main() {

int x, y, z;

void swap(int *, int *);

printf("Input three numbers -> ");

scanf("%d%d%d",&x,&y,&z);         /*  scanf는 참조에 의한 호출로 변수를 받는다

scanf에서 x,y,z의 값을 변경할 수 있어야 하기 때문이다  */

if (x > y) swap(&x,&y);

if (x > z) swap(&x,&z);

if (y > z) swap(&y,&z);

printf("Sorted result : %d %d %d\n",x,y,z);

}

void swap(int *ip, int *jp) {

int t;

t = *ip;

*ip = *jp;

*jp = t;

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input three number -> 250 72 128 □

Sorted result : 72 128 250

< 포인터 연산 >

포인터 변수는 int형일 수도 있고, char형일 수도 있고 float형 일수도 있다. 하지만 그것에 무관하게 메모리에서 차지하는 양은 단지 2바이트만을 차지한다.

char *cptr;   → char형 포인터

int *iptr;    → int형 포인터

float *fptr;  → float형 포인터

□□□□□□□□□□□□□□□□□□□

30AB □     □ □□

□□□□□□□  □□□□□□□ cptr이 차지하는 메모리

30AC □     □ □□

□□□□□□□□□□□□□□□□□□□

30AD □     □ □□

□□□□□□□  □□□□□□□ iptr이 차지하는 메모리

30AE □     □ □□

□□□□□□□□□□□□□□□□□□□

30AF □     □ □□

□□□□□□□  □□□□□□□ fptr이 차지하는 메모리

30B0 □     □ □□

□□□□□□□□□□□□□□□□□□□

메모리에서 차지하는 양을 보면 포인터의 형과는 무관하게 단지 2바이트만을 차지하지만 포인터 변수 앞에 정의한 형은 상당히 중요한 역할을 한다. 가리키고 있는 메모리가 int형인지, char형인지를 판단하게 해주는 역할을 한다. 예를 들어보면

void main(void) {

int i, *iptr;

i = 5;

iptr = &i;

printf("iptr = %p\n",iptr);

iptr++;

printf("iptr = %p\n",iptr);

iptr = iptr + 2;

printf("iptr = %p\n",iptr);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

iptr = FFD2

iptr = FFD4

iptr = FFD8

포인터의 증감은 그 형의 크기에 따라 증가 또는 감소한다. 즉 int형 포인터 변수를 1증가하면 실제 번지수 값은 2가 증가되며 float형은 4가, char형은 1이 증가됨을 알 수 있다. 즉 숫자적인 덧셈이 아니라 다음 몇 번째란 의미를 갖는데, 실제로 주소는 그 포인터의 데이터 유형의 크기만큼 증가하게 된다.

다음의 그림은 이를 나타내고 있다.

data *p;                                    주소

□□□□□□□□□□□□□□

p □□□□□□□□□□□□□□□□□→ □   data0    □      N

□□□□□□□□□□□□□□

p + 1 □□□□□□□□□□□□□→ □   data1    □      N + 1 * sizeof(data)

□□□□□□□□□□□□□□

p + 2 □□□□□□□□□□□□□→ □   data2    □      N + 2 * sizeof(data)

□□□□□□□□□□□□□□

p + 3 □□□□□□□□□□□□□→ □   data3    □      N + 3 * sizeof(data)

□□□□□□□□□□□□□□

마찬 가지로 포인터 변수에서 어떤 값을 빼게 되면 그 값 만큼 이전 데이터를 의미하게 된다.

data *p;                                    주소

□□□□□□□□□□□□□□

p - 3 □□□□□□□□□□□□□→ □   data-3   □      N - 3 * sizeof(data)

□□□□□□□□□□□□□□

p - 2 □□□□□□□□□□□□□→ □   data-2   □      N - 2 * sizeof(data)

□□□□□□□□□□□□□□

p - 1 □□□□□□□□□□□□□→ □   data-1   □      N - 1 * sizeof(data)

□□□□□□□□□□□□□□

p □□□□□□□□□□□□□□□□□→ □   data0    □      N

□□□□□□□□□□□□□□

하나 더 사용할 수 있는 것이 있는데 이는 같은 데이터 유형의 포인터들 사이의 뺄셈이다.

int *ip = (int *) 1000, *jp = (int *) 1200;

printf("jp - ip = %d\n",jp - ip);

위의 경우는 1000번지와 1200번지 사이에 int 유형의 데이터들이 죽 들어 있다고 가정하여 jp의 데이터와 ip의 데이터 사이가 순서로 몇 번째 차이 인지를 의미하는 것으로 이는 두 포인터 변수의 값을 뺀 후 이를 int 유형의 크기로 나누면 된다. 따라서 jp - ip는 (jp - ip) / sizeof(data)와 같은 의미를 갖게 되며 다음과 같은 출력을 내게 된다.

jp - ip = 100  (MSC, TC)

이외의 다른 연산자나 수식은 모두 사용할 수 없고, 사용하면 컴파일시 에러가 발생하게 된다.

다음은 포인터 연산에서 사용할 수 없는 것들이다.

int *ip, *jp;

char *cp;

⑴ ip + jp      /*  두 포인터 간에 덧셈은 할 수 없다  */

⑵ ip * 2       /*  포인터 변수에 곱셈을 하게 되면 컴파일시 에러가 발생한다  */

⑶ jp / 3       /*  사용할 수 없는 연산자를 사용했다  */

⑷ ip + 3.2     /*  정수값만 더해야 하는데 실수값(3.2)을 더했다  */

⑸ ip - cp      /*  같은 데이터 유형의 포인터들 사이에만 뺄셈을 할 수 있다

(이 경우 어느 한 쪽을 형 변환하면 사용할 수 있다)  */

다음은 포인터 연산에 관한 프로그램이다.

main() {

int i = 10, *ip = &i;

int j, k;

char c = 'D', *cp = &c;

double *jp = (double *) 2000;

*ip++ = ++i;

++*--ip;

++*cp++;

*--cp = c + 3;

jp += 5;

j = jp - (double *) 80;

k = (int)(jp - 80);

printf("i = %d, j = %d, k = %d, c = %c\n",i,j,k,c);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

i = 12, j = 245, k = 1400, c = H

우선 *ip++ = ++i의 경우 i의 값을 1 증가시키고(i = 11이 된다) 이를 *ip에 할당하고(*ip는 i이므로 i = 11) ip가 sizeof(int) 만큼 증가된다. 그 다음 ++*--ip;에서 --ip를 먼저 계산하므로 ip는 도로 원래의 값이 되며 따라서 *ip는 i가 되고 이를 1 증가하므로 i = 12가 된다. ++*cp++는 *cp와 cp가 1 증가하므로(sizeof(char)는 1이다) c는 'E'가 되고 cp는 1증가한다.

그 다음 *--cp = c + 3에서 c + 3은 'E' + 3이 되며 *--cp에서 --cp로 다시 cp가 c의 주소를 가지므로 *--cp는 c가 된다. 따라서 c는 'E' + 3 = 'H'를 갖게 된다.

그리고 jp += 5에서 jp가 2000을 값으로 가지므로 jp = 2000 + sizeof(double) * 5가 되어 2040이 되며, 그 다음 j = jp - (double *) 80에서 80이 주소로 형 변환 되었으므로 포인터 사이의 뺄셈이 되어 (2040 - 80) / sizeof(double)이 되므로 j는 245가 된다.

마지막으로 k = (int)(jp - 80)의 경우 jp - 80에서 80이 주소가 아닌 정수이므로 jp - 80 * sizeof(double)이 되어 2040 - 80 * 8 = 1400이 되므로 k는 1400이 된다.

Ⅷ. 배열

< 1차원 배열 >

배열은 같은 데이터 유형의 변수를 많이 만들 때 유용하게 사용된다.

int ave[10];

□   □   □□□□□□□□□□□□□□ 배열의 갯수   /*  반드시 0부터 시작하여야만 한다  */

□   □□□□□□□□□□□□□□□□□□ 배열의 이름

□□□□□□□□□□□□□□□□□□□□□□ 배열의 데이터 유형

( ave[0], ave[1], ave[2], ave[3], ..... ,ave[8], ave[9] ) 10개의 int 변수가 생긴다.

ave[0]에서 부터 ave[9]까지는 모두 int형 변수이다. 그리고 '[ ]'안에 쓰여 있는 숫자를 '첨자'라고 부른다. 이 첨자를 프로그램에서 사용할 때는 숫자대신 변수를 써도 되며 첨자의 범위는 '0'에서부터 '정의할 때 쓰여진 숫자 - 1'까지 이다. 그리고 첨자의 범위를 사용자 마음대로 정의할 수는 없다. 예를 들어 ave[3]에서 ave[7]까지의 5개의 원소로 이루어진 배열은 정의할 수 없으며, 첨자는 반드시 0부터 시작하여야만 한다.

int a = 3, ave[10];

ave[a] = 100;

ave[9] = 200;

ave[10] = 400;  → 에러가 발생하지는 않지만 치명적인 버그가 발생할 수 있다. 정의할 때

'10'이라고 크기를 정했기 때문에 쓰일 때는 '0 ~ 9□까지 쓸 수 있다.

배열에 대해서도 다른 변수와 같이 초기값을 지정할 수 있다. 단, 이 경우에는 그 배열의 메모리 유형이 auto여서는 안되고 extern이나 전역 static, 지역 static 유형인 경우에만 초기값을 줄 수 있다(MSC와 TC에서는 auto 유형의 배열도 초기값을 줄 수 있도록 허용하고 있다).

다음은 1차원 배열의 초기화 예이다.

⑴ int ave[10] = { 10, 30, 50, 70, 100, 40, 30, 55, 223, 765 };

⑵ int ia[] = { 10, 30, 50, 70, 100, 40, 30, 55, 223, 765 };

⑶ int ja[10] = { 10, 30, 50, 70, 100, 40, 30, };

□□□□□□□□ 숫자 뒤에 콤마가 있다.

⑷ int ka[3] = { 1, 10, 5, 7, 8 };

⑸ char ca[5] = { "Test" };

첫번째는 모든 배열의 원소를 초기화한 것이다. 그리고 두번째는 첨자를 생략한 경우이다. 첨자를 생략했다는 것은 배열의 크기를 생략했다는 뜻이 된다. 이러한 경우는 배열의 모든 데이터를 초기화할 때만 가능한 것이며, 첨자를 생략하면 컴파일러는 초기화한 데이터의 갯수 만큼의 배열을 자동으로 잡게 되어 있다. 즉 초기화한 데이터가 10개 이므로 배열은 10개가 잡히게 된다.

세번째는 데이터의 갯수가 7개 밖에 없으며 마지막 데이터 뒤에는 콤마가 찍혀있는데, 이것은 10개의 배열 중에서 ja[0]부터 ja[6]까지만 초기화하고 나머지는 모두 0으로 초기화하라는 뜻이다(실수 데이터 유형의 경우에는 당연히 0.0이 되며 char 유형의 경우에는 널 문자('\0')가 되고 포인터 데이터 유형의 경우에는 널 포인터(NULL)가 된다). 반면에 네번째와 같이 지정한 원소의 갯수보다 더 많은 초기값을 주었을 때에는 컴파일 에러가 발생하기 때문에 주의하여야 한다.

마지막 다섯번째는 char 유형의 배열에 스트링 형태로 초기값을 준 것인데, 스트링은 여러 개의 문자들이 모인 것으로 맨 끝에 널 문자('\0')가 있는 것이 다르다. 따라서 위의 정의는 다음과 같은 의미를 갖는다.

char ca[5] = { 'T','e','s','t','\0' };

또한 초기값을 스트링 형태로 준 경우에는 다음과 같이 중괄호를 생략할 수 있다.

char c3[] = □Computer";

char c4[10] = "string";

다음은 1차원 배열을 사용한 합계 프로그램이다.

void main(void) {

int i, data[] = { 78, 55, 99, 75, 84, 39, 67, 98, 87, 100 };

long int sum = 0;

float ave;

for (i = 0; i < 10; i++)

sum += data[i];

ave = (float)sum / 10;

printf("TOTAL = %ld  AVERAGE = %.2f\n",sum,ave);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

TOTAL = 782  AVERAGE = 78.20

main() {

int year, mon, day;

int days;            /*  전체 날수를 기억  */

int ia[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };

printf("Year ? ");

scanf("%d",&year);

printf("Month ? ");

scanf("%d",&mon);

printf("Day ? ");

scanf("%d",&day);

if (mon >= 1 && mon <= 12) {

days = ia[mon-1] + day;      /*  날수를 계산  */

if (mon > 2 && (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)))

++days;                         /*  그해가 윤년이고 월이 3월 이후면 하루를 증가  */

printf("It is %d days.\n",days);

}

else

printf("Error : Invalid month!\n");

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Year ? 1995 □

Month ? 10 □

Day ? 30 □

It is 303 days.

main() {

int ia[10] = { 0, };            /*  초기값이 모두 0  */

int i;

printf("*** Input numbers between 0 and 99 ***\n");

scanf("%d",&i);

while (i >= 0 && i <= 99) {

++ia[i / 10];

scanf("%d",&i);

}

printf("FREQUENCIES\n");

for (i = 0; i < 10; i++)

printf("%2d ~ %2d : %10d\n",i*10,(i+1)*10-1,ia[i]);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

*** Input numbers between 0 and 99 ***

2 4 6 8 10 20 30 40 50 60 70 80 90 100 □

FREQUENCIES

0 ~  9 :          4

10 ~ 19 :          1

20 ~ 29 :          1

30 ~ 39 :          1

40 ~ 49 :          1

50 ~ 59 :          1

60 ~ 69 :          1

70 ~ 79 :          1

80 ~ 89 :          1

90 ~ 99 :          1

다음은 N개의 정수를 읽어들여 이를 작은 수에서 큰 수의 순서로 출력하는 프로그램으로, N은 먼저 읽어 들이며 100을 넘지 않는다고 가정한다.

void main() {

static int data[100];

int i, j, t, N;

printf("Input the number of data: ");

scanf("%d",&N);

for (i = 0; i <= N - 1; i++)        /*  배열의 입력  */

scanf("%d",&data[i]);

for (i = 0; i <= N - 2; i++)

for (j = i + 1; j <= N - 1; j++)

if (data[i] > data[j]) {        /*  버블 정렬(bubble sort)  */

t = data[i];

data[i] = data[j];

data[j] = t;

}

printf("Sorted data:\n");

for (i = 0; i <= N - 1; i++) {      /*  정렬된 배열을 출력  */

printf("%10d",data[i]);

if ((i + 1) % 7 == 0)

printf("\n");

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input the number of data: 10 □

19 95 □

10 □

30 □

33 26 36 82 □

62 70 □

Sorted data:

10        19        26        30        33        36        62

70        82        95

다음은 데이터를 읽어 그 중 가장 긴 라인의 길이와 그 라인 전체를 출력하는 프로그램으로, 읽을 데이터로는 이 프로그램 자체를 넣은 것이다.

#include <stdio.h>

char data[250];           /*  get_line 함수에서 읽어들인 데이터를 기억하는 배열로

extern 배열로 정의 하였다  */

void main() {

char mdata[250];        /*  지금까지 읽은 라인 중 가장 긴 라인을 기억시킬 배열  */

int max = 0;

int l, i;

while (l = get_line())            /*  한 줄을 읽음  */

if (l > max) {                  /*  이것이 현재 라인 보다 길면  */

for (i = 0; i <= l - 1; i++)

mdata[i] = data[i];         /*  그대로 복사  */

max = l;

}

if (max == 0)                     /*  읽어들인 라인이 없으면  */

printf("No data.\n");

else {

printf("The longest line is:\n");

for (i = 0; i <= max - 1; i++)

putchar(mdata[i]);

printf("The length is: %d\n",max);

}

}

int get_line() {                    /*  한 줄을 읽음  */

int i = 0;

while ((data[i] = getchar()) != EOF)

if (data[i++] == '\n')

return (i);

return (i);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>longline < longline.c □

The longest line is:

printf("\nThe length is: %d\n",max);

The length is: 41

< 다차원 배열 >

다차원 배열은 다음과 같이 행열을 계산하는 프로그램을 만들 때 사용한다.

[0] [1] [2] [3]

[0]   A   B   C   D

[1]   E   F   G   H

[2]   I   J   K   L

/***** 프로그램 내부에서 행열의 각각의 원소를 저장하는 방법 *****/

int mat[3][4];

mat[0][0] = A;  mat[0][1] = B;  mat[0][2] = C;  mat[0][3] = C;

mat[1][0] = E;  mat[1][1] = F;  mat[1][2] = G;  mat[1][3] = H;

mat[2][0] = I;  mat[2][1] = J;  mat[2][2] = K;  mat[2][3] = L;

위와같이 행열의 데이터를 다루는 데는 2차원 배열이 가장 편리한 데이터 구조가 된다. 그리고 3차원(입체)적인 물체의 데이터를 가지고 있어야되는 프로그램을 짠다고 한다면 이것은 3차원 배열이 각 데이터에 접근하기에 가장 편리한 데이터 구조가 될 것이다.

다차원 배열이라고 해도 메모리는 일차원이기 때문에 결국 일차원 형태로 메모리에 존재하게 되는데, 여기에는 행(row) 우선 방법과 열(column) 우선 방법의 두 가지가 있다.

예를 들어 ia[2][3];의 경우.

/*  [그림 1] 행 우선으로 배열을 할당  */

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

메모리 □ia[0][0] □ ia[0][1] □ ia[0][2] □ ia[1][0] □ ia[1][1] □ ia[1][2] □

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

/*  [그림 2] 열 우선으로 배열을 할당  */

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

메모리 □ia[0][0] □ ia[1][0] □ ia[0][1] □ ia[1][1] □ ia[0][2] □ ia[1][2] □

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

C에서는 행(row) 우선으로 배열을 할당하기 때문에 [그림 1]과 같이 존재하게 된다. 즉 맨 끝의 첨자에서 맨 처음의 첨자가 변하는 순으로 각 배열의 원소들이 할당되게 된다.

그러나 C 언어에는 원칙적으로 다차원 배열이란 없다. 다만 C에서 배열의 요소는 무엇이 되어도 상관이 없다.

int ia[3][2];

배열 1 □□□

□□□□□□□□□□□□□□□□□      □□□□□□□□□□□□□□□□□      □□□□□□□□□□□□□□□□□

□□□□□□□□□□□□□□□□□      □□□□□□□□□□□□□□□□□      □□□□□□□□□□□□□□□□□

□ 한 개의 배열 요소를 확대해 보면

배열 2 □□□ ↓

□□□□□□□□□□□□□□□□□□□ 배열 요소 자체도 배열로 이루어져 있다.

□□□□□□□□□□□□□□□□□□□

/*  즉 2차원 배열은 배열의 요소로 '1차원 배열'을 가진다는 말이 된다  */

/*  다음은 다차원 배열이 메모리에 존재하는 상태를 그림으로 나타낸 것이다  */

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

메모리  □       □       □       □       □       □       □

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

[0][0] [0][1]   [1][0] [1][1]   [2][0] [2][1]

부분배열        부분배열        부분배열

/*  부분배열이 다차원 배열의 요소가 된다  */

다차원 배열도 일차원 배열과 같이 초기값을 줄 수 있다. 이 경우 역시 auto 메모리 유형은 줄 수 없다(MSC와 TC는 auto 유형의 다차원 배열에도 초기값을 줄 수 있도록 허용하고 있다).

다음은 다차원 배열의 초기화 예이다.

⑴ int ia[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };

⑵ int ia[][3] = { 1, 2, 3, 4, 5, 6 };

⑶ int ia[][] = { 1, 2, 3, 4, 5, 6 };

⑷ int ja[2][3][2]  = { { {1,2},{3,4},{5,6} }, { {7,8},{9,10},{11,12} } };

⑸ int ja[][3][2] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };

⑴의 첫번째 중괄호는 초기값 전체를 의미한다. 그 안에 다시 두 개의 중괄호가 있는데, 이는 각각 ia[0]으로 시작하는 행과 ia[1]로 시작하는 행에 대한 초기값을 의미한다. 즉 초기값도 각 행별로 주게되며 각 행은 중괄호로 구분하게 된다.

⑵에서는 아예 중괄호 구분을 없애고 하나로 초기값을 주고 있다. 이 경우 메모리에 위치하는 순서대로 차례로 초기값이 할당된다. 그리고 맨 처음 첨자를 생략했는데, 다 생략할수는 없고 맨 처음 첨자만 생략이 가능하다. 그러나 ⑶과 같이 모두 생략할 경우 컴파일 에러가 발생한다.

⑸의 3차원 배열도 마찬가지로 맨 처음 첨자는 생략할 수 있으나, 그 외 첨자는 생략할 수 없다.

다음은 3 X 3 행열 2개를 읽어들여 이의 합을 출력하는 프로그램이다.

void print_matrix(int M[3][3]) {

int i, j;

for (i = 0; i < 3; i++) {

for (j = 0; j < 3; j++)

printf("%d ",M[i][j]);

printf("\n");

}

}

void read_matrix(int M[3][3]) {        /*  3*3 행열을 읽어들이는 함수  */

int i, j;

for (i = 0; i < 3; i++)

for (j = 0; j < 3; j++)

scanf("%d",&M[i][j]);

}

main() {

int A[3][3], B[3][3], C[3][3];

int i, j;

printf("*** Input the first matrix(3*3) ***\n");

read_matrix(A);                      /*  배열의 이름을 인자로 전달한다  */

printf("\t## A ##\n");

print_matrix(A);

printf("*** Input the second matrix(3*3) ***\n");

read_matrix(B);

printf("\t## B ##\n");

print_matrix(B);

for (i = 0; i < 3; i++)

for (j = 0; j < 3; j++)

C[i][j] = A[i][j] + B[i][j];

printf("\n*** C = A + B ***\n");

print_matrix(C);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

*** Input the first matrix(3*3) ***

1 2 3 4 5 6 7 8 9 □

## A ##

1 2 3

4 5 6

7 8 9

*** Input the second matrix(3*3) ***

10 20 30 40 50 60 70 80 90 □

## B ##

10 20 30

40 50 60

70 80 90

*** C = A + B ***

11 22 33

44 55 66

77 88 99

< 배열과 포인터 >

1차원 배열의 이름 중에서 '[ ]'부분을 뺀 나머지는 배열의 첫번째 요소를 가리키는 포인터 상수이다. 다음 그림을 보면

int ar[3];

□□□□□□□□□□□□□□□□□□□□□□□□□□□

메모리 □     □     □     □

□□□□□□□□□□□□□□□□□□□□□□□□□□□

□□□□□□   □ar[0] ar[1] ar[2]

□  □□□□□□□

□□□□□□

ar: int형 포인터 상수 (ar[0]의 시작번지가 있음)

ar은 int형 포인터 상수이다. 즉 *ar은 ar[0]이 되고, *(ar + 1)은 ar[1]이 되며, *(ar + 2)는 ar[2]와 같은 값이 된다. 다음은 이 관계를 그림으로 나타내었다.

int ar[n]; 일 경우 *(ar + n)과 ar[n]은 완전히 동일한 수식.

ar[0] ar[1] ar[2]

□□□□□□□□□□□□□□□□□□□□□□□□

메모리 □  □  □  □  □  □  □

□□□□□□□□□□□□□□□□□□□□□□□□

□     □     □□□□□□□□□□□□□ (ar + 2) ar은 int형 포인터이기 때문에 2를 더하면

□□□□□□  □     □                   4바이트가 증가해서 ar[2]의 번지수가 된다.

□  □□□□□□     □□□□□□□□□□□□□□□□□□□ (ar + 1) ar은 int형 포인터이기 때문에 1을 더하면

□□□□□□                            2바이트가 증가해서 ar[1]의 번지수가 된다.

ar: int형 포인터 상수

(ar[0]의 시작번지가 있음)

† 이렇게 되는 것은 C에서의 배열은 각 원소가 낮은 주소에서 높은 주소의 순서로

차곡차곡 존재하기 때문이다.

한 가지 주의할 것은 배열의 이름은 상수이지 변수가 아니라는 것이다. 따라서 ar + 1은 사용할 수 있지만 ++ar은 사용할 수 없다.

다음은 포인터를 이용하여 배열을 사용하는 예이다.

#include <stdio.h>

void main() {

static int ia[] = { 2, 5, 7, 9, 11 };

static char ca[] = "abcdef";

int *ip, *jp;

int t;

char *cp;

ip = ia;

while (ip <= ia + 4)    /*  ia 배열의 각 원소의 값이 1씩 증가하여 3, 6, 8, 10, 12  */

++*ip++;              /*  가 된다  */

ip = ia;              /*  ip는 ia 배열의 첫번째 원소를 가리키고 있다  */

jp = ip + 4;          /*  jp는 ia 배열의 맨 마지막 원소를 가리키고 있다  */

while (ip < jp) {

t = *ip;

*ip++ = *jp;          /*  ip의 값은 증가시키고 jp의 값은 감소시키면서 두 포인터가  */

*jp-- = t;            /*  주소를 갖고 있는 데이터를 서로 교환하고 있다  */

}

for (ip = ia; ip <= ia + 4; ip++)

printf("%d ",*ip);

putchar('\n');

cp = ca;                /* cp는 ca 배열의 첫번째 원소를 가리키고 있다  */

while (*cp)

putchar(*cp++);

putchar('\n');

cp = ca;

while (*cp++)           /*  포인터는 2씩 증가하게 되며  */

putchar(++*cp++);     /*  ca 배열의 원소의 값도 1 증가하게 된다  */

putchar('\n');

for (cp = ca; *cp; )

putchar(*cp++);

putchar('\n');

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

12 10 8 6 3

abcdef

ceg

acceeg

main() {

int ia[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

int *ip, i;

ip = ia+9;                    /*  배열의 이름은 그 배열의 맨 처음 변수의 주소와 같다  */

for (i = 0; i < 10; i++)                                 /*  즉 ia+9는 &ia[9]와 같다  */

printf(" ia[%d] = %2d\n",9-i,*(ip-i));

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

ia[9] = 10

ia[8] =  9

ia[7] =  8

ia[6] =  7

ia[5] =  6

ia[4] =  5

ia[3] =  4

ia[2] =  3

ia[1] =  2

ia[0] =  1

※ 일차원 배열의 인자 전달

배열 자체를 인자로 전달하고자 할 때에는 다음과 같이 그 배열의 이름을 넘겨주면 된다.

int ia[7] = { 1, 3, 5, 7, 9, 12, 17 };

:

func(ia);

그러면 받는 쪽에서는 다음과 같이

func(int x[7]) {

:

하면 된다. 그런데 넘어 오는게 바로 ia이고, 이는 ia[0]의 주소를 갖는 포인터 상수이기 때문에 다음과 같이 아예 포인터 변수로 받는 것이다.

func(int *x) {

:

또 다음과 같이 배열과 비슷하게 받아도 된다.

func(int x[]) {

:

위에서는 x가 배열이라는 것만 나타내고 있고 그 크기는 얼마인지 나타내지 않고 있다. 실제로 받는 쪽에서는 이를 알 필요가 없는데, 배열의 시작 주소만 넘어오기 때문이다.

배열의 경우 그 자체가 포인터이기 때문에 결국 참조에 의한 호출로 넘겨 준 것이 된다. 따라서 받는 쪽에서는 부른 쪽의 배열을 마음대로 변경할 수 있다. 오히려 이 경우에는 값에 의한 호출이 불가능하고, 굳이 하려면 새로운 배열을 만들어 똑같이 복사하여 넘겨 주어야 한다.

다음은 배열의 인자 전달에 관한 프로그램이다.

void main(void) {

int a[10] = { 23, 75, 12, 50, 328, 432, 58, 68, 33, 30 };

int i;

for (i = 0; i < 10; i++)

printf(" %d   ",a[i]);

printf("\n");

printf(" Maximum with max_item_a() = %d\n",max_item_a(a));  /*  호출하는 부분이 둘다  */

printf(" Maximum with max_item_b() = %d\n",max_item_b(a));  /*  배열의 첫번째 요소를  */

}                                                           /*  가리키는 포인터 상수  */

int max_item_a(int item[]) {     /*  배열이라는 개념으로 계산한 함수  */

int i, max = 0;

for (i = 0; i < 10; i++)

if (max < item[i])           /*  item[i]가 max보다 크면  */

max = item[i];             /*  max에다 item[i]를 넣는다  */

return max;

}

int max_item_b(int *item) {      /*  포인터라는 개념으로 계산한 함수  */

int i, max = 0;

for (i = 0; i < 10; i++)

if (max < *(item+i))         /*  *(item+i)가 max보다 크면  */

max = *(item+i);           /*  max에다 *(item+i)를 넣는다  */

return max;

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

23    75    12    50    328    432    58    68    33    30

Maximum with max_item_a() = 432

Maximum with max_item_b() = 432

다음은 각 줄의 데이터를 읽어들여 각 줄마다 거꾸로 출력하는 프로그램이다.

#include <stdio.h>

void main() {

char line[250];

char *cp;

int l;

void reverse(char *, int);

while (l = get_line(line)) {    /*  한줄을 읽어  */

reverse(line, l);             /*  이를 거꾸로 만든 뒤  */

for (cp = line; cp < line + l; cp++)     /*  이를 출력  */

putchar(*cp);

}

}

int get_line(char l[]) {          /*  한 줄을 읽음  */

int len = 0;

while ((*l = getchar()) != EOF) {

++len;

if (*l++ == '\n')

return (len);

}

*l = '\n';                     /*  라인의 맨 끝에 항상 '\n'이 있도록 한다  */

return (len);

}

void reverse(char *line, int len) {     /*  라인을 거꾸로 함  */

char *cp = line + len - 2;     /*  맨 끝에 항상 '\n'이 있고 이는 움직여서는 안되므로  */

int t;                             /*  cp의 값이 line+len-1이 아닌 line+len-2가 된다  */

while (line < cp) {

t = *line;                   /*  처음과 끝을 서로 교환하고 하나씩 가까이 이동  */

*line++ = *cp;

*cp-- = t;

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>reverse < reverse.c □

※ 스트링

스트링 자체가 배열이므로 이를 다루고자 할 때에는 당연히 char 유형의 포인터나 배열을 사용하는 것이 좋은데, 특히 char 유형의 포인터를 사용할 경우에는 다음과 같이 바로 스트링을 할 당할 수 있다.

char *cp;

cp = "test";

이 경우 cp는 뒤의 스트링의 처음 시작 위치를 값으로 갖게 된다. 반면에 배열에 스트링을 할당하고자 할 때에는 다음과 같이 하나씩 복사하여야 한다.

char ca[20];

char *cp1 = ca;

char *cp2 = "This is test";

while (*cp1++ = *cp2++);

그러나 스트링을 읽어들이고자 할 때에는 배열을 사용하여야 한다. 즉 이를 기억할만 한 충분한 공간을 지닌 배열을 미리 선언한 다음에 앞의 get_line()과 같은 함수를 통해 읽어 들이면 된다(  이 경우에는 포인터 변수는 사용할 수 없는데, 포인터 변수는 읽어 들인 스트링을 저장할 공간을 갖고 있지 않기 때문이다). 앞의 get_line() 함수와 같이 스트링을 읽어 들이는 함수를 작성하여 사용할 수도 있지만 scanf를 사용할 수도 있다. 이때 바로 %s 포맷을 사용하는데, 예를 들어 ca란 배열에 스트링을 읽어들이고자 할 때에는 다음과 같이 사용하면 된다.

char ca[80];

scanf("%s",ca);            /*  ca 자체가 주소이기 때문에 □&□를 붙일 필요가 없다  */

scanf는 스트링을 읽어 들여 이를 ca에 저장하게 되는데, 여백 문자(즉 □ □나 '\t', '\n'과 같은)가 나타날 때까지 읽어들이게 된다. 따라서 다음과 같이 입력하면

This is test<Enter>

실제 ca에는 This만 입력되기 때문에 주의하여야 한다. 만약 한 줄의 스트링을 모두 입력하고자 할 때에는 다음과 같이 사용하면 된다.

char ca[80];

scanf("%[^\n]s",ca);

위의 경우 [^\n]이 더 추가 되었는데, 이의 의미는 '\n'이 나올 때까지이다. 그러나 이 경우 입력시 문제가 발생할 소지가 많기 때문에 앞의 get_line()함수와 같이 자신이 스스로 함수를 만들어 사용하는 것이 좋다. 다음은 스트링 처리에 관한 몇 가지 예이다.

#include <stdio.h>                    /*  getchar()를 사용하기 위해 선언  */

main() {

char com[20], name[20], pass[20];

scanf("%[^,]",com);                 /*  ','이 읽혀질 때까지  */

getchar();                          /*  □,□를 읽어버림  */

scanf("%[^:]",name);                /*  ':'이 읽혀질 때까지  */

getchar();                          /*  □:□를 읽어버림  */

scanf("%[^\n]s",pass);              /*  <Enter>키가 입력될 때까지  */

getchar();                          /*  '\n'를 읽어버림  */

printf("Computer : %s\n",com);

printf("Name     : %s\n",name);

printf("Password : %s\n",pass);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□486DX2-80,GRACE:1684 □

결과

Computer : 486DX2-80

Name     : GRACE

Password : 1684

다음은 □device"란 모든 스트링을 □DEVICEHIGH"로 변환하여 출력하는 프로그램이다.

#include <stdio.h>

void main() {

char data[6];

char c;

char *cp = "evice";               /*  첫자를 제외한 나머지를 찾을 스트링  */

int n;

data[5] = '\0';                   /*  스트링으로 만듦  */

while ((c = getchar()) != EOF) {

if (c == 'd') {                 /*  d로 시작하면 같을 가능성이 있음  */

n = nget_line(data, 5);       /*  최고 5자만 읽음  */

if (n == 5 && compstr(data, cp))      /*  cp를 사용하지 않고 바로

compstr(data, "evice")로 할 수 있다  */

printf("DEVICEHIGH");       /*  device 대신 출력  */

else

printf("d%s",data);         /*  device가 아니므로 그대로 출력  */

}

else

putchar(c);

}

}

int nget_line(char *c, int n) {     /*  최대 n자의 문자들을 읽어버림  */

int i = 0;

while ((*c++ = getchar()) != EOF)

if (++i == n)

break;

return (i);

}

int compstr(char *s1, char *s2) {   /*  두 개의 스트링이 같은지 비교  */

while (*s1 == *s2) {

if (*s1 == '\0')

return (1);

++s1;

++s2;

}

return (0);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>change < \windows\system.ini | more □

다차원 배열의 경우에도 '[ ]'부분을 뺀 나머지 이름은 모두 포인터 상수이다.

int ar[2][3];       /*  1개의 값은 메모리 2바이트를 차지한다  */

ar □□□□□□□□□□□□□ ar[0] □□□□□□□□□□□□□ □□□□□□□□□□□□□□□□□

□   ar[0][0]    □

ar[0]+1 □□□□□□□□□□□ □□□□□□□□□□□□□□□□□

□   ar[0][1]    □

ar[0]+2 □□□□□□□□□□□ □□□□□□□□□□□□□□□□□

□   ar[0][2]    □

ar+1 □□□□□□□□□□□ ar[1] □□□□□□□□□□□□□ □□□□□□□□□□□□□□□□□

□   ar[1][0]    □

ar[1]+1 □□□□□□□□□□□ □□□□□□□□□□□□□□□□□

□   ar[1][1]    □

ar[1]+2 □□□□□□□□□□□ □□□□□□□□□□□□□□□□□

□   ar[1][2]    □

□□□□□□□□□□□□□□□□□

ar은 물론 포인터 상수일 뿐만 아니라 ar[0], ar[1]도 역시 포인터 상수가 된다. 그런데 ar[0]은 바로 ar[0][0]의 주소를 갖는 int * 형태의 포인터 상수인데 반해 ar의 값은 ar[0][0]의 주소이면서 ar[0] 행 전체를 가리키는 포인터 상수가 된다. ar[0]은 ar[0][0]을 가리키기 때문에 ar[0]+1을 하면 ar[0][0]의 다음 원소, 즉 ar[0][1]이 되지만 ar은 ar[0]으로 시작하는 행 전체를 가리키기 때문에 ar+1을 하면 ar[1] 행이 되어 값은 ar+3*sizeof(int)가 된다(여기서 3은 한 행의 원소의 갯수이다).

다음 프로그램을 이를 잘 나타내고 있다.

void main() {

static int ar[2][3] = { 1, 2, 3, 4, 5, 6 };

int i, j;

for (i = 0; i < 2; i++)

for (j = 0; j < 3; j++)

printf("Address of ar[%d][%d] = %u\n",i,j,&ar[i][j]);

printf("ar = %u, *ar = %u, **ar = %u\n",ar,*ar,**ar);

printf("ar+1 = %u, *(ar+1) = %u, **(ar+1) = %u\n",ar+1,*(ar+1),**(ar+1));

printf("ar[0] = %u, *ar[0] = %u\n",ar[0],*ar[0]);

printf("ar[0]+1 = %u, *(ar[0]+1) = %u\n",ar[0]+1,*(ar[0]+1));

printf("ar[1] = %u, *ar[1] = %u\n",ar[1],*ar[1]);

printf("ar[1]+1 = %u, *(ar[1]+1) = %u\n",ar[1]+1,*(ar[1]+1));

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Address of ar[0][0] = 404

Address of ar[0][1] = 406

Address of ar[0][2] = 408

Address of ar[1][0] = 410

Address of ar[1][1] = 412

Address of ar[1][2] = 414

ar = 404, *ar = 404, **ar = 1

ar+1 = 410, *(ar+1) = 410, **(ar+1) = 4

ar[0] = 404, *ar[0] = 1

ar[0]+1 = 406, *(ar[0]+1) = 2

ar[1] = 410, *ar[1] = 4

ar[1]+1 = 412, *(ar[1]+1) = 5

다차원 배열에서 한 개의 배열 요소를 참조하는데는 다음과 같이 여러 가지 방법으로 참조할 수 있다.

int ar[l][m][n];     /*  3차원 배열을 정의했을 경우  */

ar[l][m][n]   *(ar[l][m]+n)  *(*(ar[l]+m)+n)  *(*(*(ar+l)+m)+n)

(*(ar+l))[m][n]  (*(*(ar+l)+m))[n]

※ 다차원 배열의 인자 전달

1차원 배열과 마찬가지로 배열의 이름만 전달하게 되며, 받는 쪽에서는 포인터 유형으로 받게 된다. 예를 들어 다음과 같이 전달한 경우.

char ca[2][3];

:

f1(ca);

f1쪽에서는 다음 셋 중 하나로 받을 수 있다.

f1(char ca[2][3]) {

:

f1(char ca[][3]) {     /*  초기값의 경우와 같이 맨 처음의 첨자만 생략할 수 있다  */

:

f1(char (*ca)[3]) {    /*  ca가 3개의 원소로 이루어진 행 자체를 가리키므로  */

:

특히 char 유형의 2차원 배열은 많이 이용되는데, 스트링의 배열이 바로 char 유형의 2차원 배열이 되기 때문이다.

3차원 배열의 경우도 마찬가지이며, 다음과 같이 호출하였을 때.

int ar[2][3][4];

:

f2(ar);

f2에서는 다음 셋 중의 하나로 받을 수 있다.

f2(int ar[2][3][4]) {

:

f2(int ar[][3][4]) {

:

f2(int (*ar)[3][4]) {

:

다음은 입력한 데이터를 단어 단위로 알파벳 순으로 정렬하는 프로그램으로, 각 단어의 길이는 30자 이내로 가정하며 단어의 구분은 □ □나 □\t', '\n'으로 하고 알파벳 이외의 다른 글자들은 ASCII코드 순으로 정렬하고 있다.

#include <stdio.h>

#include <string.h>

void main() {

static char word[100][30];

char (*cp1)[30] = word;    /*  30개의 원소로 구성된 char 유형의 일차원 배열을 가리키는

포인터가 된다. 즉 이 cp1의 값을 1 증가시키면 char 유형 30개의 크기 만큼 증가하게            되며, 이는 word와 같다. 따라서 cp1 = word;와 같이 할당할 수 있게 된다  */

char (*cp2)[30];

char t[30];

int n = 0;

while (get_word(*cp1++) && ++n < 100);         /*  단어를 100개 이하로 읽음  */

for (cp1 = word; cp1 < word + n - 1; cp1++)    /*  단어를 정렬  */

for (cp2 = cp1 + 1; cp2 < word + n; cp2++)

if (strcmp(*cp1, *cp2) > 0) {              /*  strcmp 함수의 원형

int strcmp(s1, s2);

const char *s1    비교할 문자열

const char *s2    비교할 문자열

s1 문자열과 s2 문자열을 비교해서 s1 < s2 이면 0보다 작은 값을 반환하고,                    s1 == s2 이면 0을 반환한다. 그리고 s1 > s2 이면 0보다 큰 값을 반환한다.

문자의 ASCII 코드를 가지고 문자열의 처음 문자부터 시작하여 다음 문자로                     차례로 이어져 검색하는데 대응문자가 다르든지 문자열의 끝을 만나든지 하면                    중단한다(대문자와 소문자는 구별하지 않는다)  */

strcpy(t, *cp1);                /*  strcpy 함수의 원형  */

strcpy(*cp1, *cp2);             /*  char *strcpy(dest, src);  */

strcpy(*cp2, t);                /*  char *dest -> 문자열을 복사할 행선지  */

}                               /*  const char *src -> 복사할 문자열  */

for (cp1 = word; cp1 < word + n; cp1++)        /*  정렬된 단어를 출력  */

printf("%s\n",*cp1);

}

int get_word(char *s) {

int l = 0;

while ((*s = getchar()) != EOF) {

if (!l && (*s == ' ' || *s == '\t' || *s == '\n')) /*  처음 <Space>와 <Tab>은 무시  */

continue;

if (l == 29 || *s == ' ' || *s == '\t' || *s == '\n') {   /*  읽은 것이 30자가

되거나 단어가 끝나면  */

if (l == 29)                               /*  읽은 것이 30자면  */

ungetc(*s, stdin);                       /*  읽은 것을 취소하고 스트링을 만듦  */

*s = '\0';

return (1);

}

++s;

++l;

}

if (l) {                                       /*  EOF를 만나고 읽은 것이 있으면  */

*s = '\0';

return (1);

}

else

return (0);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>test < test.c □

< 포인터 배열 >

포인터 자체도 하나의 데이터이므로 이에 대한 배열도 선언할 수 있다.

int *ar[3];

□□□□□□□□□□□□□□□□□□□□□□□□

메모리 □  □  □  □  □  □  □

□□□□□□□□□□□□□□□□□□□□□□□□

□     □     □□□□□□□□□□□□□ 포인터 변수 *ar[2]

□     □□□□□□□□□□□□□□□□□□□ 포인터 변수 *ar[1]

□□□□□□□□□□□□□□□□□□□□□□□□□ 포인터 변수 *ar[0]

이것은 int 유형의 주소를 갖는 포인터 3개로 구성된 배열을 정의한 것이 된다. 즉 이름이 ar[0], ar[1], ar[2]인 포인터 변수 3개를 정의했다고 생각하면 된다. 그리고 ar은 다른 일차원 배열과 같이 이 배열의 첫번째 원소의 주소를 갖고 있는 포인터가 되므로 ar = &ar[0]이 된다. 그러면 *ar은 당연히 ar[0]이 되며 **ar은 ar[0]이 가리키고 있는 정수 값이 된다. 마찬가지로 int ia[10][5];의 경우에도 **ia는 int 유형의 정수가 되기 때문에, 위의 포인터 배열은 int 유형의 2차원 배열과 유사한 점이 많다는 것을 알 수 있다.

포인터 배열은 일반적으로 문자열 배열을 선언할 때 사용한다.

void main(void) {

char string[][20] = { "Lee seung wook", "Oh jung pyo", "Kwon oh jin" };

printf(" Total used memory : %d\n",sizeof(string));

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Total used memory : 60

다음 그림은 위 프로그램의 총 메모리 사용량이 60바이트 임을 보여주고 있다.

string[0] □□□□

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

메모리      □L□e□e□ □s□e□u□n□g□ □w□o□o□k□0□ □ □ □ □ □     † 20 * 3 = 60 바이트

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

string[1] => □O□h□ □j□u□n□g□ □p□y□o□0□ □ □ □ □ □ □ □ □     † 문자열 상수 뒤에는 항상

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□     □\0□이 붙는다.

string[2] => □K□w□o□n□ □o□h□ □j□i□n□0□ □ □ □ □ □ □ □ □     † 빈공백은 메모리 낭비

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□  20바이트

위 메모리 상태를 보면 알 수 있듯이 문자열 상수의 크기에 관계없이 2차원 배열로 부분 배열의 크기를 20으로 선언했기 때문에 문자열 상수로 채워지지 않은 부분은 메모리의 낭비를 가져온다.

다음은 똑 같은 예제를 포인터 배열을 이용해서 만든 것이다.

#include <string.h>                     /*  strlen 함수를 사용하기 위해 선언  */

void main(void) {

char *string[] = { "Lee seung wook", "Oh jung pyo", "Kwon oh jin" };

int ps, sum = 0, i;

ps = sizeof(string);

printf("  String pointer size : %d\n",ps);

for (i = 0; i < 3; i++)               /*  strlen은 문자열의 길이를 구하는 함수로  */

sum += strlen(string[i]) + 1;       /*  NULL 문자는 포함되지 않는다. 그래서 □ + 1□  */

printf(" String constant size : %d\n",sum);

printf("    Total used memory : %d\n",ps+sum);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

String pointer size : 6

String constant size : 39

Total used memory : 45

위 프로그램의 메모리 소비를 그림으로 나타내면 다음과 같다

□□□□□□□            □□□□□□□□□□ □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

string[0] □  □□□□□□□□□□□□□□□□□           □L□e□e□ □s□e□u□n□g□ □w□o□o□k□0□

□□□□□□□            □□□□□□□□□□ □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

string[1] □  □□□□□□□□□□□□□□□□□           □O□h□ □j□u□n□g□ □p□y□o□0□

□□□□□□□            □□□□□□□□□□ □□□□□□□□□□□□□□□□□□□□□□□□□□

string[2] □  □□□□□□□□□□□□□□□□□           □K□w□o□n□ □o□h□ □j□i□n□0□

□□□□□□□                       □□□□□□□□□□□□□□□□□□□□□□□□□□

포인터 변수가 있는 메모리(6바이트)     † 문자열 상수 뒤에는 항상 □\0□이 붙는다.

† 39 + 6 = 45 바이트

이와 같이 문자열 상수를 메모리의 낭비없이 선언할 때 포인터 배열이 쓰인다.

다음은 사람 이름을 정렬하는 프로그램으로 포인터 배열을 이용한 것이다.

#include <string.h>            /*  문자열을 비교하는 함수 strcmp 를 사용하기 위해 선언  */

void sort_name(char *ptr[]) {         /*  포인터 배열을 이용하여 문자열상수를 정렬한다  */

int i, j;

char *tem;

for (i = 0; i < 6; i++)

for (j = i + 1; j < 7; j++)

if (strcmp(ptr[i], ptr[j]) > 0) {    /*  strcmp는 두 개의 문자열을 비교하는 함수  */

tem = ptr[i];

ptr[i] = ptr[j];

ptr[j] = tem;

}

}

void list(char *ptr[]) {             /*  포인터 배열로 지정된 문자열 상수를 화면에  */

int i;                             /*  표시하는 함수  */

for (i = 0; i < 7; i++)

printf(" %s\n",ptr[i]);

}

void main(void) {

char *name[] = { "Howard Ashman", "Alan Menken", "Brian W. Kernighan",

"Dennis M. Ritchie", "Lawrence H. Miller",

"Al Kelley", "Ira Pohl" };

char *n_ptr[7];

int i;

for (i = 0; i < 7; i++)              /*  새로운 포인터 배열에 초기화 시킴  */

n_ptr[i] = name[i];

printf(" Unsorted list ----------\n");

list(n_ptr);

sort_name(n_ptr);

printf("\n");

printf("   Sorted list ----------\n");

list(n_ptr);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Unsorted list ----------

Howard Ashman

Alan Menken

Brian W. Kernighan

Dennis M. Ritchie

Lawrence H. Miller

Al Kelley

Ira Pohl

Sorted list ----------

Al Kelley

Alan Menken

Brian W. Kernighan

Dennis M. Ritchie

Howard Ashman

Ira Pohl

Lawrence H. Miller

다음은 몇 년도의 몇 번째 일을 입력으로 받아서 그 해의 월, 일로 출력하는 프로그램이다.

void main() {

static char *month[] = { "January", "February", "March", "April", "May", "June",

"July", "August", "September", "October", "November", "December" };

static int mno[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

int year, days;

int i;

printf("Year? ");

scanf("%d",&year);

printf("Days? ");

scanf("%d",&days);

mno[1] += ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0);

for (i = 0; days > mno[i]; days -= mno[i++]);

printf("The date is: %s, %d\n",month[i],days);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Year? 1995 □

Days? 303 □

The date is: October, 30

< 이중 포인터 >

이중 포인터는 포인터의 포인터를 말한다. 포인터 변수는 어떤 변수를 가리키는 번지를 저장하는 변수이다. 그리고 포인터의 포인터는 포인터 변수의 번지를 기억하는 변수이다. 이 관계를 그림으로 표현하면 다음과 같다.

메모리           메모리           메모리

□□□□□□□□         □□□□□□□□         □□□□□□□□

□  □□□□□□□□□□□□> □  □□□□□□□□□□□□□>□      □

□□□□□□□□         □□□□□□□□         □□□□□□□□

이중 포인터 변수    포인터 변수      정수형 변수

이중 포인터를 정의하는 방법은 일반 포인터를 정의할 때처럼 하고 '*'를 한번 더 써주면 된다.

int **dptr

이렇게 정의하고 나면 'dptr'은 이중 포인터 변수, '*dptr'은 포인터 상수, '**dptr'은 정수의 의미를 가진다.

다음은 이중 포인터의 사용예이다.

void main(void) {

int **dptr, *ptr, i;

i = 10;

ptr = &i;

dptr = &ptr;

printf("   i = %4d,  *ptr = %4d,  **dptr = %4d\n",i,*ptr,**dptr);

printf("  &i = %4p,   ptr = %4p,   *dptr = %4p\n",&i,ptr,*dptr);

printf("&ptr = %4p,  dptr = %4p,   &dptr = %4p\n",&ptr,dptr,&dptr);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

i =   10,  *ptr =   10,  **dptr =   10

&i = FFD2,   ptr = FFD2,   *dptr = FFD2

&ptr = FFD0,  dptr = FFD0,   &dptr = FFCE

포인터 배열에서 배열 이름은 포인터를 가리키는 포인터이기 때문에 이중 포인터 상수라고 생각해도 된다. 이 관계를 그림으로 표현하면 다음과 같다.

char *str[3]; 로 정의해서 초기화 했다.

□□□□□□     메모리

□  □□□□□□>□□□□□□□ str[0]       □□□□□□□□□□□□□□□□□□□□□□

□□□□□□    □  □□□□□□□□□□□□□□□□> □ 첫번째 문자열 상수 □

str      □□□□□□□              □□□□□□□□□□□□□□□□□□□□□□

이중 포인터 □     □

□□□□□□□ str[1]       □□□□□□□□□□□□□□□□□□□□□□

□  □□□□□□□□□□□□□□□□> □ 두번째 문자열 상수 □

□□□□□□□              □□□□□□□□□□□□□□□□□□□□□□

□     □

□□□□□□□ str[2]       □□□□□□□□□□□□□□□□□□□□□□

□  □□□□□□□□□□□□□□□□> □ 세번째 문자열 상수 □

□□□□□□□              □□□□□□□□□□□□□□□□□□□□□□

□     □

이러한 이유때문에 다음과 같은 현상이 일어난다.

*str       == str[0]

*(str + 1) == str[1]

*(str + 2) == str[2]

이것은 각각의 문자열을 가리키는 포인터 변수로써 어느 곳에 갖다놔도 같은 값이 나온다. 또 문자열 상수의 문자 하나하나를 지정할 수도 있다.

**str             == str[0][0]     /*  † *(*(str + n) + m) ...  == str[n][m] ...  */

*(*(str + 1))     == str[1][0]

*(*(str + 2))     == str[2][0]

*(*str + 1)       == str[0][1]

*(*(str + 1) + 1) == str[1][1]

*(*(str + 2) + 1) == str[2][1]

다음은 텍스트를 줄 단위로 정렬하는 프로그램으로, 전체 텍스트의 길이는 3000자를 넘지 않는다고 가정한다.

#include <stdio.h>

#include <string.h>

void main() {

static char l[3000];      /*  텍스트의 최대 크기는 3000자  */

static char *cp[400];     /*  각 라인의 시작 위치를 기억하는 포인터 배열  */

char **cpp = cp;

char **cp1, **cp2;

char *t;

int n = 0;

int len;

while (len = get_line(*cpp++ = l + n))  /*  각 라인을 읽어 이의 시작 위치를 기억  */

n += len;

for (cp1 = cp; cp1 < cpp - 1; cp1++)    /*  정렬  */

for (cp2 = cp1 + 1; cp2 < cpp; cp2++)

if (strcmp(*cp1, *cp2) > 0) {

t = *cp2;

*cp2 = *cp1;

*cp1 = t;

}

for (cp1 = cp; cp1 < cpp; cp1++)

printf("%s",*cp1);

}

int get_line(char *l) {              /*  한 줄을 읽어들임  */

char c;

int i = 0;

while ((*l = getchar()) != EOF)

if (++i && *l++ == '\n') {

*l = '\0';

return (i + 1);

}

if (i) {

*l++ = '\n';

*l = '\0';

return (i + 2);

}

return (0);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>textsort < testsort.c □

< main 함수와 인자 >

DOS 프롬프트가 보이는 상태에서 어떤 프로그램을 실행시키기 위해서는 그 프로그램의 화일 이름을 치고서 <Enter>키를 누르면 실행되는 것을 알고 있을 것이다. 그리고 어떤 프로그램은 인자가 있다는 것도 알고 있을 것이다. 예를 들어서 xcopy라는 DOS 프로그램은 인자가 2개 이거나 1개이다.

C:\DOS>xcopy *.* a:/p

□    □   □□□□□□□ 두번째 인자

□    □□□□□□□□□□□ 첫번째 인자

□□□□□□□□□□□□□□□□ 프로그램 이름

위와 같이 인자를 받아 들이는 부분을 프로그램상에서 만들어야 한다.

지금까지 main이란 함수를 사용할 때 인자없이 바로 main() 와 같이 사용해 왔다. 그런데 실제로 main에도 인자가 있는데, 다음과 같은 형태의 인자들이 넘어오게 된다.

int main(int argc, char **argv, char **env)

위의 원형에서 이중 포인터는 다음과 같이 써도 된다.

int main(int argc, char *argv[], char *env[])

이 때 인자의 순서는 바뀌어서는 안되지만 뒤에서부터 차례대로 생략할 수 있다.

각 인자의 내용은 다음과 같다.

† int argc;

이 변수는 Command라인에서 쓰여진 인자의 갯수를 정수(int)값으로 가지고 있다. 인자의 구분은 space, tab 과 같은 기호로 구분한다. 이 때 정수 값은 인자가 없으면 1, 1개면 2, ... 와 같은 값을 가진다.

† char *argv[];

이것은 포인터 배열로 Command라인에서 입력한 인자들이 존재하는 메모리의 번지수를 차례대로 기억하고 있다. 이 때 각 포인터 배열은 다음과 같다.

argv[0] - 패스명과 실행파일 이름.

argv[1] - 첫번째 인자의 내용.

argv[2] - 두번째 인자의 내용.

:

argv[n] - n번째 인자의 내용.

다음은 Command라인에서 인자를 받아서 모든 인자를 화면에 출력 해주는 프로그램이다.

int main(int argc, char *argv[]) {

int i;

printf(" Program name %s\n",argv[0]);

if (argc < 2)

printf(" Argument is not exist !\n");

else

for (i = 1; i < argc; i++)

printf(" Argument %d is %s\n",i,argv[i]);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>noname □ 이라고 실행했을 경우

Program name C:\TC\NONAME.EXE

Argument is not exist !

C:\TC>noname *.* a:/v □ 이라고 실행했을 경우

Program name C:\TC\NONAME.EXE

Argument 1 is *.*

Argument 2 is a:/v

다음 프로그램은 텍스트의 어떤 스트링 s1을 모두 s2로 바꾸어 출력하는 것으로, 아래와 같이 사용한다고 가정한다.

C:\TC>strsub s1 s2

#include <stdio.h>

#include <string.h>

main(int argc, char *argv[]) {

char data[80];

char c;

int n = 0;

int sl1;

if (argc != 3) {                         /*  인자를 2개 주지 않으면 잘못 사용한 것임  */

printf("Usage: %s string1 string2\n",*argv);

return (1);

}

sl1 = strlen(argv[1]);                   /*  바꿀 스트링의 최대 길이는 80자  */

if (sl1 > 80) {

printf("%s is too long(exceeding 80 characters).\n",argv[1]);

return (2);

}

data[sl1 - 1] = '\0';

while ((c = getchar()) != EOF) {

if (c == *argv[1]) {                   /*  처음 글자가 같으면  */

if (sl1 - 1)

n = nget_line(data, sl1 - 1);      /*  나머지를 읽어 비교  */

if (n == sl1 - 1 && !strcmp(data, argv[1] + 1))

printf("%s",argv[2]);

else

printf("%c%s",*argv[1],data);

}

else

putchar(c);

}

}

int nget_line(char *c, int n) {

int i = 0;

while ((*c++ = getchar()) != EOF)

if (++i == n)

break;

return (i);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>strsub □ 이라고 실행했을 경우

Usage: C:\TC\STRSUB.EXE string1 string2

C:\TC>strsub DEVICEHIGH DEVICE < \config.sys □

반면에 3번째 인자인 env는 환경 변수의 값들이 X=Y 형태의 스트링으로 넘어오게 된다(환경 변수는 DOS의 경우에는 SET 명령에 의해 값이 정의된 변수들을 의미한다). argc 처럼 갯수를 알려주는 인자가 없으므로 이것의 갯수는 알 수가 없고, env의 값을 증가시켜 나가다가 이 값이 널 포인터가 되면 바로 이것이 데이터의 끝이라는 것을 알 수 있다.

다음은 env로 넘어오는 값들을 모두 출력하는 프로그램이다.

void main(int argc, char *argv[], char *env[]) {

int i = 0;

while (*env)

printf("env[%d] = %s\n",i++,*env++);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

env[0] = COMSPEC=C:\COMMAND.COM

env[1] = PROMPT=$P$G

env[2] = TEMP=C:\NU\TEMP

env[3] = PATH=C:\DOS;C:\NU;C:\TC;C:\C600\BINB;C:\C600\BIN

env[4] = LIB=C:\C600\LIB

env[5] = INCLUDE=C:\C600\INCLUDE

env[6] = HELPFILES=C:\C600\HELP\*.HLP

env[7] = INIT=C:\C600\INIT

< void형 포인터 >

'void'의 의미는 3가지로 분류할 수 있다. 첫째 함수명 앞에 붙여서 쓰인 경우, 이 때는 "이 함수는 return값이 없다."라고 알리는 표시이다. 둘째 함수명 뒤의 형식인자 자리에 쓰인 경우, 이 때는 "이 함수에는 형식 인자가 없다."라는 표시이다. 그리고 마지막의 포인터로써의 의미이다.

포인터 변수가 무슨형이냐에 따라 가리키는 변수가 틀려지는데, 때에 따라 형을 무시하고 일을 처리 해야 할 때가 생길 것이다. 이 때에는 포인터를 선언할 때 가리키는 형이 무엇이든 된다고 선언을 해야 하는데, 이 때 사용하는 것이 'void'이고 이 포인터를 void형 포인터라 부른다.

void *ptr;

어떠한 포인터도 void형 포인터에 형 변환 연산자의 도움없이 대입이 가능하다. 하지만 반대로 void형 포인터는 형 변환 연산자없이 결코 다른 포인터 변수에 대입할 수 없다. 물론 ++, --또한 형 변환 연산자 없이는 불가능하다. void형 포인터는 그것이 가리키고 있는 대상의 크기를 모르기 때문이다.

다음은 void형 포인터를 사용해서 char형 배열을 int형 배열로 복사하는 프로그램이다.

void array_cpy(void *scr, void *des, int n) {

int i;

for (i = 0; i < n; i++)

*((char *)des + i) = *((char *)scr + i);

}

void main(void) {

char scr[6] = { 'A', '\0', 'B', '\0', 'C', '\0' };

int des[3], i;

for (i = 0; i < 6; i++)

printf(" Character array scr[%d] = %c\n",i,scr[i]);

printf("\n");

array_cpy(scr, des, 6);

for (i = 0; i < 3; i++)

printf(" Integer array des[%d] = %d\n",i,des[i]);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Character array scr[0] = A

Character array scr[1] =

Character array scr[2] = B

Character array scr[3] =

Character array scr[4] = C

Character array scr[5] =

Integer array des[0] = 65

Integer array des[1] = 66

Integer array des[2] = 67

char형 배열이 차례대로 'A','\0'이 int형 배열로 복사되면 메모리 안에는 'A'에 해당하는 ASCII코드 값이 먼저 들어가고 그 다음에 '\0'이 들어간다. 이 값을 int형으로 읽으면 역 워드 방식이므로 '65'만 값으로 나타난다.

< 함수 포인터 >

포인터는 무슨 포인터이든지 메모리에서 2바이트를 차지한다. 그리고 그 포인터를 정수형 포인터 또는 배열 포인터라고 부르는 것은 그 포인터가 가리키고 있는 데이터가 어떠한 유형의 데이터인가에 달려 있다.

그런데 C의 포인터가 너무나도 강력한 나머지 하나의 함수까지 가리킬 수 있을 뿐만 아니라, 가리킨 함수를 실행까지 시킬 수 있다.

아래에 함수 포인터를 선언하는 방법을 나타내었다.

함수의 데이터 유형 (*포인터 이름)(가리킬 함수의 매개변수 리스트);

함수의 데이터 유형 - 함수 포인터가 가리키는 함수의 되돌리는 값(return value)이다.

함수를 정의할 때 함수명 앞에 쓰는 데이터 유형과 같다.

포인터 이름 - 일반 변수명을 쓰듯이 사용자 임의대로 쓴다.

가리킬 함수의 매개변수 리스트 - 함수 포인터가 가리키는 함수의 인자이다. 이 값은 함수를

정의할 때 함수명 오른쪽에 써 넣는 인자의 내용과 같다.

ex) void (*fptr1)(int a, char *b);

int (*fptr2)(char *a[], int n);

간단한 함수 포인터의 예를 들어 보았다.

int func1(int val);

void main(void) {

int i;

int (*fptr)(int i);         /*  함수 포인터의 선언  */

i = 123;

fptr = func1;               /*  괄호를 쳐서는 안된다  */

i = (*fptr)(i);             /*  func1을 실행한다  */

printf(" i = %d\n",i);

}

int func1(int val) {

printf(" val is %d\n",val);

return (2525);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

val is 123

i = 2525

위 프로그램에서 'fptr = func1;'이라고 쓴 부분은 함수 포인터의 초기화 부분이다. fptr은 함수 포인터가 되고 func1은 함수 포인터의 값이 된다. 즉 함수에서 '()'와 그 안에 들어있는 부분을 제외한 함수의 이름은 함수 포인터 상수가 된다.

Ⅸ. 구조 데이터 유형

< 구조 데이터 유형의 선언과 사용 >

C에서의 구조 데이터란 여러 개의 서로 다른 데이터를 모아 하나로 묶어 놓은 것을 의미한다.

예를 들어 다음과 같은 성적처리 프로그램을 작성한다고 가정하면.

char name[30];      /*  사람이름  */

int kor;            /*  국어 성적  */

int eng;            /*  영어 성적  */

int math;           /*  수학 성적  */

int total;          /*  총점  */

float ave;          /*  평균  */

위와 같이 한 사람에 필요한 변수들을 정하고, 이번에는 이 변수들을 하나의 구조체로 묶으면 다음과 같다.

□□□□□□□□□□□□□□□□□□□□□□□ 구조체 택 (structure tag)

struct man {                    (구조체 택은 다른 변수들의 이름과 같아서는 안된다)

char name[30];     □□□

int kor;             □

int eng;             □□□ 구성요소

int math;            □   (구성 요소들의 정의는 일반 변수와 같으나

int total;           □    초기값은 줄 수 없다)

float ave;         □□□

} person;  □□□□□□□□□□□□□ 프로그램상에서 변수를 참조할 때 쓰는 변수 이름

person이라는 구조체를 정의 하였다.

(구조체 택과 변수 이름이 같아서는 안된다)

모든 구조 데이터 유형은 반드시 struct로 시작하게 된다. 그 다음에 이 구조의 실제 이름(위 예에서는 man)이 오게 되는데, 이는 지정할 수도 있고 지정하지 않을 수도 있다(일반적으로 지정한다). 그 다음의 중괄호는 실제 구조의 내용을 담고 있으며 그 안에 그 구조에 속하는 데이터들을 차례대로 정의해주면 된다. 그리고 그 뒤의 변수(위 예에서는 person)는 이와 같은 구조를 값으로 갖는, 즉 struct 유형의 변수를 정의한 것이다.

다음의 예는 구조체 택을 생략한 것이다.

□□□□□□□□□□□□□□□□□□□□□□□□□ 구조체 택 (structure tag)을 생략

struct {

char name[30];     □□□

int kor;             □□□ 구성요소

int eng;             □   (구성요소는 그 구조 내에서만 구별할 수 있는 이름이면

int math;            □    되기 때문에, 구성 요소의 이름이 같은 다른 구조도 선

int total;           □    언할 수 있다)

float ave;         □□□

} person;  □□□□□□□□□□□□□ 프로그램상에서 변수를 참조할 때 쓰는 변수 이름

person이라는 구조체를 정의 하였다.

위 예에서는 구조 데이터 유형을 선언해 놓고 이의 이름(구조체 택)을 주지 않았다. 단지 이런 구조를 가진 변수로 person만 정의했을 뿐이다. 이렇게 해도 상관은 없으나 차후에 person과 같은 구조를 갖는 변수를 또 정의하려면 위와 같이 구조 전체를 다시 선언해야만 한다.

이번에는 구조체 택이 꼭 필요한 경우의 예를 든 것이다.

□□□□□□□□□□□□□□□□□□□□□□□ 구조체 택 (structure tag)

struct man {

char name[30];     □□□

int kor;             □

int eng;             □□□ 구성요소

int math;            □

int total;           □

float ave;         □□□

};                 /*  □;□을 생략해서는 안된다  */

struct man person;

□     □□□□□□□□□□□□□□□□□ 변수 이름

□□□□□□□□□□□□□□□□□□□□□□□ 구조체 택

이 방법은 먼저 구조체의 형을 선언해 놓고 아래에서 진짜 메모리에 만들어지는 변수를 정의하고 있다. 이때 구조체 택을 사용하고 있는데, 이처럼 구조체 택은 구조체를 선언하는 방법에 따라 생략할 수도 있고 생략할 수 없을 수도 있다.

구조 데이터 유형을 정의해 놓은 후에 이를 값으로 갖는 변수를 별도로 정의할 수 있는데, 이 경우 다음과 같이 하면 된다.

struct student s[100], *ps;      /*  배열이나 포인터도 정의할 수 있다  */

struct complex v1, v2;

struct booktype book[30];

그러나 구조체 택이 생략되어 있으면 위와 같이 변수를 정의할 수 없다.

구조 데이터 유형을 정의한 경우 그 정의가 함수나 블럭 안에 있는 경우에는 자신이 정의된 함수나 블럭 내에서만 사용할 수 있으며 함수 바깥쪽에 정의된 경우에는 정의된 위치부터 그 화일의 끝까지 그 이름을 사용할 수 있다.

예를 들어 각 구조들은 다음과 같은 범위 내에서만 사용할 수 있다.

struct s1 { □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

int i, j;                                   □

};                                          □

main() {                                           □

struct s2 { □□□□□□□□□                            □

int k, l;    □                            □

};           □ s2를 사용할 수 있는 범위   □

:                □                            □ s1을 사용할 수 있는 범위

} □□□□□□□□□□□□□□□□□□□                            □

struct s3 { □□□□□□□□□□□                            □

int m, n;      □                            □

};             □ s3을 사용할 수 있는 범위   □

f1() {                □                            □

:                  □                            □

} □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

main에서 s3은 사용할 수 없으며 마찬가지로 f1에서는 s2를 사용할 수 없다. 이것은 변수가 아니기 때문에 변수와 같이 extern 해서 사용할 수도 없다. 일반적으로 구조 데이터 유형은 모든 프로그램에서 공통적으로 사용하기 때문에 s1과 같이 화일의 맨 처음에 선언해 두는 것이 좋다.

구조를 정의하였으면 이를 이용해야 하는데, 여기에는 두 가지 방법이 있다. 즉, 구조 전체를 사용하는 방법과 구조 내의 각 구성요소를 사용하는 방법이 있다. 우선 각 구조의 구성요소를 사용하고자 할 때에는 다음과 같이 사용하면 된다.

구조 변수 이름.구성 요소 이름

즉 변수 이름 다음에 '.'을 붙이고 그 다음 원하는 구성요소의 이름을 사용하면 되는데, 구성요소의 이름은 구조 내에 정의되어 있는 이름이어야 한다. 그러면 이것을 다른 변수와 같이 사용할 수 있으며, 이의 데이터 유형은 바로 구성요소의 데이터 유형이 된다. 예를 들어 다음과 같이 사용할 수 있다.

struct multi {

int idata;

float fdata;

char *cp;

};

struct multi m;        /*  m은 idata와 fdata, cp를 구성요소로 갖게 된다  */

float f = 1.2f;

m.idata = 10;

m.fdata = f * 12.5f;

m.cp = "string";

위에서 '.'은 일종의 연산자로 간주되며, 계산 결과는 해당 구성요소가 된다. 이의 우선 순위는 가장 높기 때문에 ++m.idata의 경우에 ++(m.idata)로 처리된다. 또 다음과 같이 여러 개를 같이 사용할 수도 있는데.

s1.s2.s3.data

이때 결합 방향이 왼쪽에서 오른쪽이므로 ((s1.s2).s3).data로 처리된다. 이와 같이 여러 개를 사용하려면 구조 안에 또 구조가 있는 형태로 정의되어야 하는데, 이러한 구조를 중첩된 구조라고 한다. 반면에 구조 전체를 사용할 수도 있는데, 이 경우에는 그 구조를 값으로 갖는 변수의 이름만 사용하면 된다. 예를 들어 앞의 프로그램안에 다음과 같이 사용할 수 있다.

struct multi m2;

m2 = m;

위에서는 multi 유형의 구조 변수 m2에 바로 같은 데이터 유형의 변수 m의 값을 할당하였다. 이와 같이 하면 각 구성요소의 값들이 다음과 같이 차례대로 할당되는 것과 같다.

struct multi m2;

m2.idata = m.idata;

m2.fdata = m.fdata;

m2.cp = m.cp;

구조 내에 구성요소가 많을 경우에는 각 구성요소를 하나씩 할당하는 것 보다는 앞의 방법과 같이 변수 이름을 사용하여 할당하는 것이 훨씬 더 효율적이다.

※ 구조 데이터 유형의 메모리 할당

struct 유형도 변수이므로 메모리를 차지하게 된다. 이때 실제로 메모리를 차지하는 것은 각 구성요소들이며, 이는 다음과 같다.

struct man {

char c;

int k;

float e;

double m;

};

void main(void) {

struct man a;

printf("  Address of a = %u, Sizeof(a) = %d\n",&a,sizeof(a));

printf("Address of a.c = %u, Sizeof(a.c) = %d\n",&a.c,sizeof(a.c));

printf("Address of a.k = %u, Sizeof(a.k) = %d\n",&a.k,sizeof(a.k));

printf("Address of a.e = %u, Sizeof(a.e) = %d\n",&a.e,sizeof(a.e));

printf("Address of a.m = %u, Sizeof(a.m) = %d\n",&a.m,sizeof(a.m));

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

(TC)

Address of a = 65476, Sizeof(a) = 15

Address of a.c = 65476, Sizeof(a.c) = 1

Address of a.k = 65477, Sizeof(a.k) = 2

Address of a.e = 65479, Sizeof(a.e) = 4

Address of a.m = 65483, Sizeof(a.m) = 8

(MSC)

Address of a = 3562, Sizeof(a) = 16

Address of a.c = 3562, Sizeof(a.c) = 1

Address of a.k = 3564, Sizeof(a.k) = 2

Address of a.e = 3566, Sizeof(a.e) = 4

Address of a.m = 3570, Sizeof(a.m) = 8

위의 결과를 보면 MSC의 경우 a.c다음의 1바이트가 사용되지 않고 비어있음을 알 수 있다. 이것은 하드웨어 구조상 변수가 특정 메모리 위치에 오는 것이 더 효율적이기 때문이다. 또 한가지 알 수 있는 것은 구성요소의 내용이 같다하더라도 그 순서가 다르면 완전히 다른 구조가 된다는 것이다. 예를 들어 다음(MSC)의 경우.

struct {          /*  s1은 8바이트  */       struct {         /*  s2는 6바이트  */

char c;                                      int i;

int i;                                       char c;

char c2;                                     char c2;

int i2;                                      int i2;

} s1;                                        } s2;

s1과 s2는 int 유형과 char 유형의 구성요소를 두개씩 갖고 있지만 이들의 순서가 다르기 때문에 서로 다른 구조가 된다. 따라서 구성요소의 순서가 매우 중요함을 알 수 있다.

구조 데이터 유형의 변수에도 초기값을 줄 수 있다. 이때 배열의 경우와 같이 그 변수의 메모리 유형이 auto여서는 안된다(MSC와 TC는 auto의 경우도 초기값을 줄 수 있도록 허용하고 있다).

다음은 이의 한 예이다.

struct info {

char name[20];    /*  구성요소중에 배열이 있는 경우 배열의 크기는  */

char addr[40];    /*  생략할 수 없다  */

char tel[10];

};

struct p {

int x, y;

} point = {320, 240};

void main(void) {

struct info data = { "Lee S.W", "Seoul Korea", "900-2525" };

printf(" x : %d  y : %d\n",point.x,point.y);

printf("Name   : %s\n",data.name);

printf("Address: %s\n",data.addr);

printf("Tel.   : %s\n",data.tel);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

x : 320  y : 240

Name   : Lee S.W

Address: Seoul Korea

Tel.   : 900-2525

초기값의 순서와 구성 요소의 순서, 그리고 초기값의 데이터 유형과 구성요소의 데이터 유형은일치하여야 한다. 배열의 경우와 같이 초기값이 모자랄 수도 있는데 이 경우 초기값이 없는 것들은 0으로 초기화 된다. 반면에 넘치게 되면 컴파일러에 따라 에러로 처리할 수도 있고 경고 메시지만 내보내고 남는 것은 무시할 수도 있다. 또 구조의 배열도 정의할 수 있는데, 이경우 각 구성 요소에 대한 초기값을 구조에 대한 초기값으로 주면 된다. 다음은 이의 한 예이다.

struct mon {

char *name;

char days;

};

struct mon m[] = { {"Jan",31},{"Feb",28},{"Mar",31},{"Apr",30},{"May",31},{"Jun",30},

{"Jul",31},{"Aug",31},{"Sep",30},{"Oct",31},{"Nov",30},{"Dec",31} };

위를 보면 2차원 배열에 초기값을 줄 때와 비슷하다는 것을 알 수 있다. 그래서 2차원 배열의 경우와 같이 아예 하나로 주어도 된다.

struct mon m[] = { "Jan",31,"Feb",28,"Mar",31,"Apr",30,"May",31,"Jun",30,

"Jul",31,"Aug",31,"Sep",30,"Oct",31,"Nov",30,"Dec",31 };

구조 데이터 유형도 함수를 호출할 때 인자로 전달할 수 있다. 이때 각 구성요소의 값을 따로 전달할 수도 있지만 구조 전체를 통채로 전달할 수도 있다.

예를 들어 다음과 같이 사용할 수 있다.

struct da {

int x;

int y;

};

void main(void) {

struct da val = { 10, 30 };

int sum = 0;

sum = total(val);

printf(" Sum = %d\n",sum);

}

int total(struct da tot) {       /*  함수가 실행될 때 함수안에 있는 변수들과 매개변수 */

return (tot.x + tot.y);        /*  들은 함수가 실행될 때 마다 메모리에 만들어진다  */

}

위 프로그램을 보면 da란 구조 데이터 유형의 변수로 선언된 val 자체를 인자로 total에 넘겨주고 있다. 반면에 이를 다음과 같이 구성요소로 나누어 구성요소별로 전달할 수도 있다.

struct da {

int x;

int y;

};

void main(void) {

struct da val = { 10, 30 };

int sum = 0;

sum = total(val.x,val.y);

printf(" Sum = %d\n",sum);

}

int total(int i, int j) {

struct da tot;

tot.x = i;

tot.y = j;

return (tot.x + tot.y);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Sum = 40

위 프로그램의 경우 앞의 프로그램과 결국 같은 결과를 낳지만 더 비효율적인 것을 알 수 있다. 또 배열의 경우 배열 자체를 계산 결과로 산출하는 함수는 있을 수 없지만, 구조의 경우에는 이를 통채로 계산 결과로 산출하는 함수를 작성할 수 있다. 이때 그 함수의 데이터 유형을 구조 데이터 유형으로 선언해 주면 되는데, 다음은 이의 한 예이다.

struct all {

int i;

float f;

float res;

};

:

struct all a;

:

a = sfunc();

struct all sfunc() {

static struct all b = {10, 1.3f};

b.res = b.i * b.f - 12.5;

return (b);

}

위에서 sfunc는 all이란 구조 데이터 유형의 값을 계산 결과로 산출하는 함수로 정의되었으며 실제로 이 함수가 산출하는 값은 같은 구조 유형의 변수인 a에 할당하고 있다. 따라서 위의 프로그램을 수행하게 되면 a.i는 10을, a.f는 1.3을, 그리고 a.res는 0.5를 값으로 갖게 된다.

다음은 아래와 같은 데이가 있다고 할 때, N명의 학생에 관해 읽어서 각 학생의 평균 성적을 구한 후 평균 성적이 높은 학생에서 낮은 학생의 순으로 이름과 번호, 평균 점수를 출력하는 프로그램이다. 단, N은 맨 처음 읽어들이도록 하며 학생의 수는 100명을 넘지 않는다고 가정하고 학생의 번호는 92XXXX이며 시험 점수는 0에서 100점 사이이다.

(data)

4

Lee Seung Wook

923682 85 78 92 90 77

Kwon Oh Jin

923326 95 67 82 50 100

Kwon Oh Chang

922525 88 85 30 65 50

Oh Jung Pyo

925252 80 70 60 50 100

#include <stdio.h>

struct student {             /*  학생에 관한 구조  */

char name[30];        /*  이름  */

long int no;          /*  학번  */

float avg;            /*  평균값  */

} s[100];

main() {

int n;                     /*  학생의 수  */

int i, j;

struct student t;

void read_name(char *);

scanf("%d",&n);

if (n > 100) {             /*  학생이 100명 이상이면  */

printf("Error: too many records(exceeding 100).\n");

return (1);

}

for (i = 0; i < n; i++) {

int sum = 0;

int grade;

read_name(s[i].name);     /*  이름을 읽음  */

scanf("%ld",&s[i].no);

if (s[i].no < 920000 || s[i].no > 929999) {   /*  번호가 잘못되었으면  */

printf("Error: invalid student number in the %dth record.\n",i+1);

return (2);

}

for (j = 0; j < 5; j++) {             /*  각 성적을 읽어  */

scanf("%d",&grade);

if (grade < 0 || grade > 100) {     /*  성적이 잘못되었으면  */

printf("Error: invalid grade in the %dth record.\n",i+1);

return (3);

}

sum += grade;                /*  성적의 합을 구함  */

}

s[i].avg = sum / 5.0f;         /*  평균을 계산  */

}

for (i = 0; i < n - 1; i++)      /*  평균 별로 정렬  */

for (j = i + 1; j < n; j++)

if (s[i].avg < s[j].avg) {

t = s[i];

s[i] = s[j];

s[j] = t;

}

printf("%5s%28s%17s%10s\n","Rank","Name","ID Number","Average");

for (i = 0; i < n; i++)

printf("%5d%30s%15ld%10.1f\n",i+1,s[i].name,s[i].no,s[i].avg);

}

void read_name(char *c) {

do

*c = getchar();                /*  처음 여백 문자들은 무시  */

while (*c == '\n' || *c == ' ' || *c == '\t');

do

*++c = getchar();

while (*c != '\n');              /*  그 줄 끝까지 읽음  */

*c = '\0';

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>test < data □

Rank                        Name        ID Number   Average

1                Lee Seung Wook         923682      84.4

2                  Kwon Oh Jin          923326      78.8

3                   Oh Jung Pyo         925252      72.0

4                 Kwon Oh Chang         922525      63.6

< 구조와 포인터 >

구조도 하나의 데이터 유형이므로 이의 주소를 값으로 갖는 포인터도 정의하여 사용할 수 있다.

다음은 이의 한 예이다.

#include <string.h>

struct man {

char name[30];

int kor;

int eng;

int math;

int total;

float ave;

};

void main(void) {

struct man a, *pa;           /*  □*□는 뒤의 변수가 포인터 변수임을 의미한다  */

pa = &a;                     /*  a의 번지를 pa에 대입  */

strcpy(a.name,"GunMan");

a.kor = 87;

a.eng = 90;

a.math = 75;

a.total = a.kor + a.eng + a.math;

a.ave = a.total / 3.0f;

printf("   Name : %s\n",(*pa).name);   /*  pa가 구조체 a의 메모리를 가리키고 있기  */

printf(" Korean = %d\n",(*pa).kor);    /*  때문에 pa를 출력해도 같은 결과가 나옴  */

printf("English = %d\n",(*pa).eng);

printf("   Math = %d\n",(*pa).math);

printf("  Total = %d\n",(*pa).total);

printf("Average = %.2f\n",(*pa).ave);

}

위에서는 man이란 이름의 구조를 정의하고 있고 이의 변수로 a를, 그리고 이의 주소를 갖는 포인터로 pa를 선언하고 있다. 또 pa의 초기값으로 a의 주소를 할당하고 있다. 그리고 이 포인터를 사용하여 구조의 각 구성요소의 값을 출력하고 있는데 *pa가 a이므로 (*pa).name은 a.name이 된다. 여기서 *pa를 괄호로 싼 것은 우선 순위 때문인데 '.'과 '*'의 경우 '.'이 우선 순위가 높기 때문에 *pa.name으로 사용하면 *(pa.name)이 되어 버리기 때문이다. 반면에 구조에 대한 포인터를 사용할 경우 위와 같이 (*pa).name으로 사용하는 것 외에 다른 방법으로도 구성요소를 사용할 수 있도록 허용하고 있는데, 다음과 같이 사용할 수 있다.

† pa : 구조체 포인터일 경우

pa->name  ==  (*pa).name   /*  두 식은 완전히 동일한 수식이다  */

다음은 위 프로그램을 구조체 포인터 연산자로 바꾸어서 나타낸 것이다.

#include <string.h>

struct man {

char name[30];

int kor;

int eng;

int math;

int total;

float ave;

};

void main(void) {

struct man a, *pa;

pa = &a;

strcpy(pa->name,"Gunman");

pa->kor = 87;

pa->eng = 90;

pa->math = 75;

pa->total = pa->kor + pa->eng + pa->math;

pa->ave = pa->total / 3.0f;

printf("   Name : %s\n",pa->name);

printf(" Korean = %d\n",pa->kor);

printf("English = %d\n",pa->eng);

printf("   Math = %d\n",pa->math);

printf("  Total = %d\n",pa->total);

printf("Average = %.2f\n",pa->ave);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Name : Gunman

Korean = 87

English = 90

Math = 75

Total = 252

Average = 84.00

< 중첩된 구조 >

구조의 구성요소로 또 다른 구조를 사용할 수 있는데, 이 경우 구조 안에 구조가 있는 형태를 취하게 된다. 예를 들어 다음과 같이 사용할 수 있다.

struct time {

int year;

int month;

int day;

int hour;

int min;

int sec;

};

struct plan {

struct time start;

struct time end;

int cost;

int number;

} p;

plan 구조를 보면 time이란 구조를 값으로 갖는 구성요소를 2개 가지고 있다. 이와 같이 구조 안의 구성요소가 또 구조를 갖고 있는 경우 이를 중첩된 구조(nested structure)라고 한다. 구조가 중첩된 경우 실제 값을 할당하기 위해서는 다음과 같이 '.'을 여러 개 사용하게 된다.

p.start.year = 1992;

p.end.year = 1995;

또 초기값도 다음과 같이 각 구조마다 하나의 중괄호를 싸서 지정하게 된다.

struct plan p2 = {{1992,10,10,10,30,0},{1996,10,9,10,29,59},10000,100};

위에서 {1992, 10, 10, 10, 30, 0}은 p2.start를 위한 초기값이며 {1996, 10, 9, 10, 29, 59}는 p2.end를 위한 초기값이다. 중첩된 구조는 복잡한 자료 구조를 나타낼 때 흔히 사용한다.

다음은 2차원 배열의 초기화와 중첩된 구조체의 초기화를 비교한 것이다.

int a[2][3] = {

{3, 4, 5},

{6, 7, 8}

};             /*  2차원 배열  */

struct {

struct a { int x, y, z; };

struct b { int w, x, y; };

} test = {

{3, 4, 5},

{6, 7, 8}

};           /*  중첩된 구조체  */

< 공용 데이터 유형 >

공용 데이터 유형의 의미는 같은 메모리를 서로다른 데이터형으로 사용한다는 의미이다. 다음 그림은 이것을 나타내고 있다.

□□□□□□□□□ int형일 경우에 사용하는 메모리

char형일 경우 □□□□□   □

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

메모리 □   □   □   □   □   □   □   □   □   □   □

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

□□□□□ 여기서부터 시작하는 메모리를 char형 으로도 int형 으로도 사용 한다.

다음은 공용 데이터 유형의 몇 가지 선언예이다.

union u_type {

int i;

float f;

char c;

} u;

union all {

char name[80];

double d[10];

long int l;

};

공용체의 선언 형태는 구조 데이터 유형과 완전히 동일하며, 각 구성요소는 바로 독립적인 변수가 된다. 그리고 '.'나 '->'또한 동일하게 사용된다. 그러나 구조체와는 다른 차이가 있는데, 바로 각 구성요소들이 기억 장소를 차지하는 방법에 차이가 있다. 구조 데이터 유형의 경우에는 각 구성요소들이 따로따로 메모리를 차지하고 있으며 그 구조의 크기는 이들 구성요소의 크기를 모두 합한 것이 된다. 그렇지만 공용 데이터 유형에서는 각 구성요소들이 메모리를 모두 공유하게 된다. 즉 같은 메모리를 같이 사용하기 때문에 공용 데이터 유형의 크기는 각 구성요소 중 가장 크기가 큰 구성요소의 크기가 된다.

다음은 이를 나타낸 프로그램이다.

main() {

struct s_type {

char c;

int i;

float f;

} s;

union u_type {

char c;

int i;

float f;

} u;

printf("Address of s = %u, size of s = %d\n",&s,sizeof(s));

printf("\tAddress of s.c = %u\n",&s.c);

printf("\tAddress of s.i = %u\n",&s.i);

printf("\tAddress of s.f = %u\n",&s.f);

printf("Address of u = %u, size of u = %d\n",&u,sizeof(u));

printf("\tAddress of u.c = %u\n",&u.c);

printf("\tAddress of u.i = %u\n",&u.i);

printf("\tAddress of u.f = %u\n",&u.f);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Address of s = 65480, Size of s = 7

Address of s.c = 65480

Address of s.i = 65481

Address of s.f = 65483

Address of u = 65488, Size of u = 4

Address of u.c = 65488

Address of u.i = 65488

Address of u.f = 65488

따라서 공용 데이터 유형은 각 구성요소들 중 어느 하나만 사용하거나 이들이 서로 배타적으로 사용될 때에만 사용하여야 한다. 예를 들어 프로그램의 어느 한 부분에서는 정수 유형의 변수 i만 사용하고 다른 한 부분에서는 실수 유형의 변수 f만 사용한다고 할 때 이 둘을 공용 데이터 유형으로 묶어 사용하면 전체적으로 메모리는 실수 유형의 변수 f만 사용할 때와 같으면서 두개의 변수를 사용할 수 있으므로 경제적이 된다.

또한 각 구성요소가 같은 장소를 서로 같이 사용하기 때문에 공용 데이터 유형의 한 구성요소의 값을 변경하게 되면 당연히 다른 구성요소의 값도 바뀌게 된다. 예를 들어 다음 프로그램의 경우

union ch_int {

char ch;

int in;

} a;

void main(void) {

a.in = 0xBC37;

printf(" ch = %X  in = %X\n",a.ch,a.in);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

ch = 37  in = BC37

다음 그림은 위에서 입력한 데이터가 메모리상에 어떠한 형태로 존재하는가를 나타낸 것이다.

□□□□□□□□□□□ int형일 경우에 사용하는 메모리

char형일 경우 □□□□□□    □

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

메모리 □ 37 □ BC □    □    □    □    □    □    □    □    □

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

□□□□□□□□ 공용체 a는 여기서 부터 존재한다.

† 'a.ch'로 공용체를 참조할 경우는 char부분만을 참조하므로 결과는 37이 된다.

† 'a.in'으로 공용체를 참조할 경우는 int부분을 참조하므로 결과는 BC37이 된다.

main() {

union u_type {

char c;

int i;

float f;

} u;

u.c = 'A';

printf("u.c = %c\n",u.c);

printf("u.i = %d\n",u.i);

printf("u.f = %f\n",u.f);

u.i = 100;

printf("u.c = %c\n",u.c);

printf("u.i = %d\n",u.i);

printf("u.f = %f\n",u.f);

u.f = 2.2f;

printf("u.c = %c\n",u.c);

printf("u.i = %d\n",u.i);

printf("u.f = %f\n",u.f);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

u.c = A

u.i = 1601

u.f = 0.000000

u.c = d

u.i = 100

u.f = 0.000000

u.c = Ａ

u.i = -13107

u.f = 2.200000

공용 데이터 유형은 각 구성요소 1개만 의미가 있기 때문에 구조 데이터 유형과는 달리 공용체 전체를 함수의 인자로 전달할 수 없으며, 이 전체를 계산 결과로 산출하는 함수도 만들 수 없다.

< 비트필드 (Bit-field) >

비트필드는 각 구조체 구성요소를 비트 단위로 나눌 수 있다. 일반적인 구조체에서 구조체 구성요소는 C가 지원하는 데이터 형태로만 나누어지는데 비해 비트필드는 비트 단위로 구성요소를 나눌 수 있기 때문에 더욱더 세밀한 데이터 조정이 가능하다.

비트필드는 구조 내에서 사용되기 때문에 먼저 구조 유형을 선언하여야 하는데, 다음과 같은 형태로 선언하면 된다.

struct bit_field {

unsigned b1: 1;

unsigned b2: 2;

unsigned b3: 4;

} x;

위를 보면 다른 구조의 정의와 같은데 구성요소의 이름 다음에 ':'를 붙이고 그 뒤에 비트의 크기를 지정하는 것이 다르다. 이때 이 구성요소는 위의 구조 내에서 딱 비트 크기 만큼의 비트 수만 차지하게 된다. 그리고 비트 필드를 사용하는 경우에 구성요소의 이름을 생략할 수 있는데, 이 경우 그 구성요소에 해당하는 비트들은 사용할 수 없게 된다(실제로 사용하지 않겠다는 의미가 된다). 예를 들어 다음과 같이 정의한 경우

struct nobits {                /*      □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□    */

unsigned rec: 3;        /*    n = □ 0 □ 1 □ 2 □ 3 □ 4 □ 5 □ 6 □ 7 □      */

unsigned    : 2;        /*      □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□    */

unsigned sen: 2;        /*          □□□□□□□□□   □□□□□   □□□□□   □        */

unsigned con: 1;        /*             rec     사용안함  sen   con       */

} n;

또 비트 크기를 0으로 선언할 수도 있다. 이는 단어의 경계면으로 이동하여 그 다음 비트부터는 다른 바이트나 단어에 할당하라는 의미가 된다. 예를 들면

struct move {

unsigned x: 3;

unsigned y: 3;

unsigned z: 0;

unsigned w: 5;

} m;

x는 처음 3비트를, y는 그 다음 3비트를 차지하게 되지만 z는 크기가 0이다. 따라서 z는 메모리를 차지하지 않으며 그 다음의 w를 y다음이 아닌 다음 워드(PC의 경우 짝수 바이트)에 할당하라는 의미가 된다. 따라서 m은 다음과 같은 구조를 갖게 된다.

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

m = □0 □1 □2 □3 □4 □5 □6 □7 □8 □9 □10□11□12□13□14□15□0 □1 □2 □3 □4 □

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

□□□□□□□  □□□□□□□□ □□□□□□□□□□□□□□□□□□□□□□□□□□□□□  □□□□□□□□□□□□□

x        y              사용 안함                   w

일반적으로 하나의 비트필드의 크기는 단어의 크기를 초과할 수 없도록 되어 있으며, 단어 경계면에 걸쳐 존재할 수도 없게 되어 있다. 따라서 특정 비트필드가 단어 경계면에 존재하게 될 경우에는 위와 같이 다음 단어로 이동하는 것이 필요하게 된다. 그리고 비트 필드로 지정된 구성요소의 데이터 유형도 signed는 의미가 없기 때문에 unsigned int가 사용되며 비트 필드 형태의 배열은 사용할 수 없고 당연히 여기에 '&'연산자도 적용할 수 없다.

다음은 조합형 코드로 된 한글과 영문이 입력되었을 때 영문의 경우에는 그 코드값을, 한글의 경우에는 초성, 중성, 종성의 코드를 분리하여 출력하는 프로그램이다.

조합형 한글코드의 체계를 그림으로 나타내면 다음과 같다.

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

메모리 =  □0 □1 □2 □3 □4 □5 □6 □7 □8 □9 □10□11□12□13□14□15□

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

□ □□□□□□□□□□□□□  □□□□□□□□□□□□□  □□□□□□□□□□□□□

flag 초성(5비트)    중성(5비트)     종성(5비트)

† flag이 1 = 한글,  0 = 영어로 판단할 때 사용한다.

#include <stdio.h>

struct hangeul {

unsigned int s: 1;        /*  부호  */

unsigned int f: 5;        /*  초성  */

unsigned int m: 5;        /*  중성  */

unsigned int l: 5;        /*  종성  */

};

main() {

unsigned char c;

unsigned char c2;

struct hangeul h;

while ((c = getchar()) != 255) {      /*  unsigned char라서 -1이 아닌 255  */

h.s = ((c & 0x80) >> 7);

if (h.s == 0) {

printf("%c -> %d\n",c,c);

continue;

}

h.f = ((c & 0x7c) >> 2);

h.m = ((c & 0x3) << 3);

c2 = getchar();

h.m |= ((c2 & 0xe0) >> 5);

h.l = c2 & 0x1f;

printf("%c%c -> %u, %u, %u\n",c,c2,h.f,h.m,h.l);

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

컴퓨터^Z □

컴 -> 17, 7, 17

퓨 -> 19, 26, 1

터 -> 18, 7, 1

위의 출력을 보면 받침이 없는 경우도 종성의 값이 1로 되어 있는데 이를 filler라고 한다. 이 filler의 값이 달라서 이상한 받침이 들어가 나타나는 경우가 있는데, 이 때에는 이를 조정해 주면 된다.

Ⅹ. 전처리기(PREPROCESSOR)

C 프로그램을 컴파일할 때는 다음과 같이 여러 단계를 거치게 된다.

□□□□□□□□□□□□□□□    □□□□□□□□□□□□□□□    □□□□□□□□□□□□□□□□

□  전처리기   □ → □ C 컴파일러  □ → □    링커      □ → 수행 화일(.exe)

□□□□□□□□□□□□□□□    □□□□□□□□□□□□□□□    □□□□□□□□□□□□□□□□

C 프로그램(.c)    전처리된 C 화일   목적 프로그램(.obj)

전처리기는 텍스트를 텍스트로 변환하는 순순한 텍스트 처리기로 C 프로그램을 받아 C 프로그램을 결과로 산출한다. 이 전처리기가 하는 일은 고정되어 있는데, 이른바 프리프로세서문이라고 불리는 문장들을 처리하는 역할을 하며, 또한 주석(comment)을 지우는 역할도 하고 있다. 따라서 C 컴파일러는 주석이 있었는지 조차 알지 못하며 주석이 없는 순수한 C 프로그램을 전달하게 된다. 그리고 프리프로세서문은 전처리기가 처리하는 문장이기 때문에 C의 문장과는 다른 형태로 되어 있으며, 반드시 '#'으로 시작하도록 되어 있다.

Unix의 경우에는 이 '#'이 반드시 첫 열에 오도록 되어 있으나 MSC나 TC의 경우에는 중간에 와도 되며 '#' 다음에는 다음과 같은 미리 정해진 문장들이 올 수 있다.

define, undef, include, if, ifdef, ifndef, elif, else, endif, line, error, pragma

전처리기는 텍스트 처리기이기 때문에 프리프로세서문은 프로그램의 어느 부분에나 올 수 있다.   프리프로세서문은 자신이 있는 위치부터 그 화일의 끝까지만 효력을 발생한다. 따라서 프로그램이 여러 개의 화일로 나뉘어져 있는 경우에는 각 화일마다 같은 프리프로세서문을 사용하여야 하는 경우가 많으며, 이 경우 다음에 나올 헤더 화일이 매우 유용하게 사용된다.

< #define 문 >

제일 처음에 나오는 것이 바로 #define 문인데 이는 다음과 같이 두 가지의 형태로 사용할 수 있다.

⑴ #define 이름 의미

⑵ #define 이름(매개변수) 의미       /*  매크로 함수  */

⑴은 원래는 오른쪽의 의미로 사용하여야 할 것을 대신 앞의 이름으로 사용하겠다는 뜻이다.

예를 들어 다음과 같이 사용한 경우.

#define PI 3.141592

전처리기는 이 다음부터 나타나는 모든 PI를 3.141592로 바꾸게 된다. 따라서 사용자는 3.141592대신에 PI라고 사용하면 된다.

l = 2.0 * PI * r;

a = PI * r * r;

전처리기는 이를 처리하여 다음과 같이 바꾸어 C 컴파일러에게 전달하기 때문에

l = 2.0 * 3.141592 * r;

a = 3.141592 * r * r;

컴파일러는 PI라는 값에 대해서 알 지 못하게 된다. 따라서 PI는 3.141592를 대신한 하나의 상수라 볼 수 있으며, 숫자가 아닌 기호 형태로 되어 있기 때문에 이를 기호 상수라고 부른다.

반면에 스트링 안이나 이름 안에 있는 것은 치환되지 않는다. 예를 들어 다음과 같이 정의되어 있다고 할 때.

#define MSG "Hello!"

main() {

printf("MSG");

}

MSG는 스트링 내에 있기 때문에 전처리기에 의해 치환되지 않는다.

기호 상수를 정의할 때 이미 앞에서 정의한 상수를 사용할 수 있다.

#define TWO 2

#define FOUR TWO*TWO

이때 x = FOUR;로 사용하면 이는 전처리기에 의해 x = 2*2;로 바뀌게 된다.

이밖에도 #define명령은 사용자가 기억하기 힘든 수치나 데이터들을 알아보기 쉽게 나타낼 때에도 사용한다. 다음은 이것의 한 예이다.

#include <stdio.h>

#include <conio.h>

#define ESC    27

#define ENTER  13

#define UP     72

#define DOWN   80

#define LEFT   75

#define RIGHT  77

void main(void) {

char ch;

int old_x, old_y, x, y;

old_x = x = 40;

old_y = y = 12;

clrscr();

while (1) {               /*    □□□□□□□□□□□□□□□□□□□□□□□                 */

ch = getch();           /*  ASCII   □□□□□□□□□□□□□□□□□□□□□             */

if (!ch) {              /*          □ ② SCAN □① ASCII □ ← ch       */

ch = getch();         /*  SCAN    □□□□□□□□□□□□□□□□□□□□□  (2byte)    */

old_x = x;            /*    □□□□□□□□□□□□□                           */

old_y = y;

switch (ch) {

case UP   : y = (y <= 1) ? 23 : y - 1;

break;

case DOWN : y = (y >= 23) ? 1 : y + 1;

break;

case LEFT : x = (x <= 1) ? 78 : x - 1;

break;

case RIGHT: x = (x >= 78) ? 1 : x + 1;

break;

}

gotoxy(x,y);

putchar('*');

gotoxy(old_x,old_y);

putchar(' ');

}

else

switch (ch) {

case ESC  :

case ENTER: return;

}

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

위 프로그램의 실행 결과는 아래 설명과 같다.

UP key     : □*□를 위로 진행시킨다.

DOWN key   : '*'를 아래로 진행시킨다.

LEFT key   : '*'를 왼쪽으로 진행시킨다.

RIGHT key  : '*'를 오른쪽으로 진행시킨다.

ESC, ENTER : 프로그램을 중단한다.

< 매크로 함수 >

⑵는 매크로(macro)를 정의하는 것이다. 매크로는 함수와 유사한데, 다음 그림은 매크로 함수의 선언 방법을 설명한 것이다.

#define SQ(x) ((x) * (x))

□  □  □□□□□□□□□□□

□  □       □□□□□□□□□□□ 형식 인자 양쪽에는 괄호를 쳐주는 것이 좋다

□  □□□□□□□□□□□□□□□□□□□ 형식 인자

□□□□□□□□□□□□□□□□□□□□□□ 매크로 함수이름(함수 이름과 뒤에오는 괄호에 공백을

두어서는 안된다)

매크로 함수라고 해서 특별한 것은 아니다. 단지 #define문을 사용하여 함수와 같은 효과를 거둔 것에 불과하다. 다음은 위 예가 프로그램상에서 어떻게 사용되는가를 보여주는 것이다.

x = SQ(3)               □□□□□□□□□□□>               x = ((3)*(3))

y = SQ(9)               □□□□□□□□□□□>               y = ((9)*(9))

z = SQ(a)               □□□□□□□□□□□>               z = ((a)*(a))

제곱한 값을 되돌린다                                전처리기를 통과한 후

매크로 함수를 사용할 때 한 가지 주의할 점이 있는데, 바로 함수부분의 변수 'x'에 괄호를 친 것이다. 만약 괄호를 없애고 프로그램을 한다면 다음과 같이 잘못된 결과가 나타날 것이다.

#define SQ(x) (x * x)

[ SQ() 를 사용한 예 ] □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

x = SQ(3)            □□□□□□□□□□>          x = (3 * 3);           /*  답 : 9  */

y = SQ(4 + 5)        □□□□□□□□□□>          x = (4 + 5 * 4 + 5);   /*  답 : 29  */

사용예 부분을 보면 첫번째 경우는 결과가 정상적으로 나오지만 두번째 경우에서는 원하는 값이 나오지 않고 있음을 알 수 있다. 즉 9의 제곱한 값 '81'을 원했는데 결과는 곱하기의 연산 우선순위 때문에 답이 '29'가 나옴을 볼 수 있다. 이러한 오류를 막기 위해서 매크로 함수의 형식 인자양쪽에는 괄호를 반드시 해 주어야 하는 것이 원칙으로 되어 있다.

다음은 #define문을 사용하는 프로그램의 한 예이다.

#include <stdio.h>

#define MAX   100

#define MIN    10

#define FALSE   0

#define max(x,y) (((x) > (y)) ? (x) : (y))

#define min(x,y) (((x) < (y)) ? (x) : (y))

#define NL   '\n'

#define SPACE ' '

#define TAB  '\t'

#define TABNO   8

#define PR(x) printf("%d\n",x);

#define PRC(x) printf("%c\n",x);

#define tab(x,n) for(x = 0; x < n; x++) putchar(SPACE);   /*  매크로는 데이터 유형에 신경

쓸 필요가 없고 여러 데이터 유형에 다 사용할 수 있다  */

void main() {

static char ca[] = "This\tis\tstring.\n";

char *cp = ca;

int i = MAX;

int j = MIN;

if (max(i-j,j) > min(i+j,i))

++i;

else

--j;

PR(i)

PR(j)

while (*cp != FALSE) {

if (*cp == TAB)

tab(i,TABNO)

else if (*cp == NL)

break;

else

PRC(*cp);

++cp;

}

}

위 프로그램에서 모든 기호 상수와 매크로를 다 없애고 원래의 의미대로 변환하면 다음과 같이 된다.

#include <stdio.h>

void main() {

static char ca[] = "This\tis\tstring.\n";

char *cp = ca;

int i = 100;

int j = 10;

if ((((i - j) > (j)) ? (i - j) : (j)) > (((i + j) < (i)) ? (i + j) : (i)))

++i;

else

--j;

printf("%d\n",i);

printf("%d\n",j);

while (*cp != 0) {

if (*cp == '\t')

for (i = 0; i < 8; i++)

putchar(' ');

else if (*cp == '\n')

break;

else

printf("%c\n",*cp);

++cp;

}

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

100

9

T

h

i

s

i

s

s

t

r

i

n

g

.

매크로 함수를 사용함으로써 생기는 이점은 샐행속도가 빨라진다는 것이다. 이유는, 일반 함수의경우 함수를 호출할 때 시간이 걸리는데 반해 매크로 함수는 사실 함수가 아니기 때문에 함수 호출 시간이 소모되지 않는다(매크로 함수는 함수처럼 보일 뿐이지 사실은 연속적인 프로그램일 뿐이다).

※ SWAP 함수

SWAP이란 바꾼다는 뜻으로 두 개의 변수 값을 바꿀 때 사용하는 함수의 이름으로 많이 쓰인다.   두 변수의 값을 바꿀때는 보통 함수안에 다음과 같이 일시적 변수를 만들어 한 변수의 값이 없어지지 않도록 보관한다.

void swap(int *a, int *b) {

int temp;

temp = *a;

*a = *b;

*b = temp;

}

위 함수는 두 개의 정수 포인터를 받아 그 값을 바꾸어 주는 함수이다. 이 함수의 단점은 지역변수 temp를 사용한다는 점과 단지 두 개의 변수값을 바꾸기 위해 함수 호출을 해야한다는 것인데, 이것을 비트연산인 XOR를 이용하여 매크로 함수로 만들면 다음과 같다.

#define swap(a,b) { a ^= b; b ^= a; a ^= b; }

다음은 이것을 가지고 초기값이 5, 6인 변수의 값을 바꾸는 것을 나타낸 것이다.

□□□□□□□□□□

□ 초기값 □  a = 5(이진수:101),  b = 6(이진수:110)

□□□□□□□□□□

a = a ^ b;  (101 xor 110 = 011)  □□□□□□□> a = 3, b = 6

b = b ^ a;  (110 xor 011 = 101)  □□□□□□□> a = 3, b = 5

a = a ^ b;  (011 xor 101 = 110)  □□□□□□□> a = 6, b = 5

결과적으로 두 변수의 값은 바뀌었다. 이 매크로 함수의 특징은 함수 호출이 되지 않고, 프로그램 중간에 끼어 들어가므로 실행 속도도 빠르다. 뿐만 아니라 이 알고리즘은 조금만 수정하면 정수형 뿐만 아니라 다른 데이터 유형에도 적용시킬 수 있다.

< #include 문 >

#include 문은 #define 문과 마찬가지로 전처리기의 일종이다. #include문은 다음과 같이 두 가지 형태로 사용한다.

⑴ #include <화일 이름>         /*  시스템 제공 화일  */

⑵ #include "화일 이름□         /*  사용자 정의 화일  */

#include하는데는 두 가지 형식이 있는데, ⑴과 같이 < >로 둘려싸여 있는 화일은 C에서 미리 정해놓은 디렉토리(MSC와 TC는 자신이 설치된 디렉토리 밑의 INCLUDE)에서 찾게 된다. 거기에 해당 화일이 있으면 이를 읽어들이고 없으면 에러가 발생한다. 반면에 ⑵와 같이 " "로 둘러싸여 있는 화일은 현재의 디렉토리에서 찾게 된다.

#include라는 명령어는 뒤에 쓰인 화일을 현재 화일의 윗부분으로 읽어온다(즉 화일을 읽어 들이는 문장이라고 할 수 있다). 이렇게 해서 새로운 화일을 형성한 다음 컴파일하게 된다.

#include해서 읽을 수 있는 화일은 텍스트 화일이면 어느것이나 괜찮다(일반적으로 확장명이 *.h인 경우가 많다). 하지만 컴파일할 때 에러가 발생하지 않으려면 반드시 C언어 문법에 맞게 쓰여진 화일이어야 한다. 그리고 #include로 포함한 화일 내에 또 #include 문이 있을 수 있으며 이경우 해당 화일을 마찬가지로 읽어들이게 된다.

예를 들어 다음과 같은 화일들이 존재한다고 가정할 때.

/*  def.h  */

#define MAX 100

#define MIN  10

/*  main.c  */

#include "def.h"       /*  #include 문이 프로그램의 맨 처음에 오는 것이 일반적인  */

main() {               /*  사용 방법이다  */

printf("max = %d, min = %d\n",MAX,MIN);

}

main.c의 #include "def.h"를 만나게 되면 전처리기는 def.h란 화일을 읽어들여 #include 문이 있는 위치에 삽입하게 된다. 그러면 다음과 같이 된다.

/*  def.h  */

#define MAX 100 □□

#define MIN  10 □□□□□□□□□□□

□ 삽입

/*  main.c  */            □

#define MAX 100 <□□□□□□□□□□

#define MIN  10

main() {

printf("max = %d, min = %d\n",MAX,MIN);

}

그러면 결국 컴파일러에게는 다음과 같은 프로그램이 전달되게 된다.

main() {

printf("max = %d, min = %d\n",100,10);

}

만약 #include 문이 없다면 처리되지 않은 문장 MAX, MIN이 컴파일러에게 전달되어 컴파일시 에러가 발생하게 될 것이다.

main() {

printf("max = %d, min = %d\n",MAX,MIN);

}

< #undef 문 >

#undef는 #define과는 정 반대의 역할을 하는 것으로 이미 정의된 기호 상수를 취소시켜 정의되지 않은 상태로 만들고자 할 때 사용한다.

예를 들어 다음 프로그램에서

#define SIZE 100

i = SIZE;

#undef SIZE

j = SIZE;

i = SIZE;를 수행할 때에는 SIZE가 100으로 정의되어 있으므로 i = 100;과 같지만, 그 다음 문장 #undef SIZE에 의해 SIZE의 정의가 취소되었기 때문에 j = SIZE;에서는 SIZE가 모르는 기호라고 에러가 나게 된다.

#undef를 사용하면 프로그램의 부분에 따라서 같은 이름의 기호를 다른 의미로 지정할 수도 있다. 예를 들어 다음과 같이 사용할 수 있다.

#define SIZE 100

i = SIZE;

#undef SIZE

#define SIZE 200

j = SIZE

만약 위에서 #undef를 사용하지 않고 다음과 같이 그냥 사용하게 되면

#define SIZE 100

i = SIZE;

#define SIZE 200

j = SIZE;

SIZE를 중복 정의한 것이 되어 컴파일시에 에러가 발생하게 된다. 특히 시스템 헤더 화일을 포함할 때 자신이 정의한 기호 상수와 같은 이름의 상수가 정의되어 있을때, 이와 같은 에러가 발생할 수 있다. 이때에도 #undef를 사용하면 이와 같은 에러를 방지할 수 있다.

< #if, #elif, #else, #endif, #ifdef, #ifndef 문 >

이들 문장들은 이른바 조건부 컴파일을 위한 것으로 특정 조건이 만족하면 컴파일 작업을 수행하고 그렇지 않으면 수행하지 않도록 하기 위해 사용된다. 예를 들어 다음 프로그램의 경우

#if MAX > 100        /*  MAX는 #define에 의해 정의되어 있어야 한다  */

size = 100;

#elif MAX > 10       /*  elif는 else if의 약자이다  */

size = 10;

#else

size = 0;

#endif

위의 의미는 if 문과 유사하다. 단 위의 문장이 전처리기에 의해 처리되는 것이 다른데, if 다음의 조건이 만족되면 전처리기는 size = 100;을 처리하여 컴파일러에게 넘겨 준다. 만약 조건이 만족되지 않으면 그 다음의 elif로 가서 조건이 만족되는지를 보고, 만족되면 전처리기는 size = 10;을 처리하여 컴파일러에게 전달하게 된다. 계속 그렇게 해 나가다가 만족되는 조건이 없으면 else 다음의 size = 0;을 처리하여 전달하게 된다. 만약 else 또한 없는 경우에는 위의 문장들을 하나도 처리하지 않게 되므로 컴파일러에게는 문장들이 하나도 전달되지 않아 마치 프로그램에 아예 없는 것과 같은 결과를 낳게 된다. 반면에 #if나 #else 다음에 또 #if가 올 수 있기 때문에 위 프로그램은 다음과 같이 다르게 만들수도 있다.

#if MAX > 100

size = 100;

#else

#if MAX > 10

size = 10;

#else

size = 0;

#endif

#endif

그리고 #ifdef와 #ifndef는 #endif하고만 같이 사용하며 다음과 같은 형태로 사용한다.

#ifdef 기호           /*  #ifdef은 뒤의 기호가 #define에 의해 정의되어 있으면  */

문장들;            /*  참이 되어 그 다음의 문장들을 처리하게 되고, 그렇지  */

#endif                /*  않으면 #endif 다음으로 넘어가게 된다  */

#ifndef 기호          /*  #ifndef은 뒤의 기호가 #define에 의해 정의되어 있지 */

문장들;            /*  않으면 참이 되어 그 다음의 문장들을 처리하게 되며,  */

#endif                /*  정의되어 있으면 #endif 다음으로 넘어가게 된다  */

다음은 이것을 예로 든 프로그램이다.

main() {

#define TEST 100

#ifdef TEST

printf("TEST is defined.\n");

#endif

#ifdef SIZE

printf("SIZE is defined.\n");

#endif

#ifndef TEST

printf("TEST is not defined.\n");

#endif

#ifndef SIZE

printf("SIZE is not defined.\n");

#endif

#undef TEST

#define SIZE               /*  값이 없게 정의되었으나 이도 엄연히 정의된 것이다  */

#ifdef TEST

printf("TEST is defined.\n");

#endif

#ifdef SIZE

printf("SIZE is defined.\n");

#endif

#ifndef TEST

printf("TEST is not defined.\n");

#endif

#ifndef SIZE

printf("SIZE is not defined.\n");

#endif

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

TEST is defined.

SIZE is not defined.

SIZE is defined.

TEST is not defined.

※ 프리프로세서문을 위한 연산자

프리프로세서문 내에서만 사용할 수 있는 연산자들이 있는데, 다음과 같이 3개가 있다.

#, ##, defined

#과 ##은 주로 #define을 사용하여 매크로를 정의할 때 사용되는 연산자들로 #은 바로 뒤의 인자를 스트링으로 바꾸어주는 역할을 한다. 예를 들어 어떤 정수 변수의 이름과 그 값을 출력시키는 매크로를 작성하고자 할 때 다음과 같이 하면 된다.

#define PRI(x) printf(#x "= %d\n",x)

위 예에서 x앞에 '#' 연산자를 사용하고 있다. 위와 같이 정의한 후 다음과 같이 매크로를 사용하게 되면

PRI(i);

이는 다음과 같이 바뀌게 된다.

printf("i" "= %d\n",i);

즉 #x에 의해 x가 스트링 x, 즉 "x"로 바뀌게 되어 "i"가 되며 두 개의 스트링을 연접해서 사용하면 이는 하나의 스트링으로 간주되므로, 다음과 같은 결과가 된다.

printf("i = %d\n",i);

'#' 연산자는 위와 같이 인자의 이름을 출력하거나 스트링에 포함시키고자 할 때 주로 사용하고 있다.

반면에 '##' 연산자는 x##y의 형태로 사용하는데, x와 y를 붙여 하나의 이름으로 만든다. 예를 들어 다음과 같이 사용할 수 있다.

#define CON(x,y) (x##y)

위와 같이 정의해 놓고 다음과 같이 사용하면

CON(i,1) = 100;

이는 다음과 같이 바뀌게 된다.

i1 = 100;

즉 변수의 이름들을 둘로 나눌 수 있을 때, 또는 상황에 맞게 서로 다른 변수들을 사용하고자 할 때 ## 연산자를 사용하면 편한 경우가 많다.

마지막으로 defined 연산자는 #if 문에서 사용하는 것으로 다음과 같이 사용한다.

#if defined(이름)

이는 이름 그대로 이름이 정의되어 있는가를 물어보는 것으로서 정의되어 있으면 참이되고 정의되어 있지 않으면 거짓이 된다. 따라서 위의 문장은 다음과 같다.

#ifdef 이름

그러나 여러 개의 이름이 동시에 정의되어있는 지를 조사하고자 할 때에는 다음과 같이 defined를 사용하는 것이 편리한 경우가 많다.

#if defined(OS) && defined(MACHINE)

< 기타 전처리기 >

⑴ #line 문

#line 문은 사용자를 위한 문장이기 보다는 컴파일러 자체를 위한 프리프로세서문이다.

예를 들어 다음과 같이 test.c란 화일이 있을 때

main() {                        /*  test.c 1번 라인  */

int x, y;                     /*  test.c 2번 라인  */

x = 1;                        /*  test.c 3번 라인  */

#line 100 "main.c"            /*  test.c 4번 라인  */

y = 1;                        /*  main.c 100번 라인  */

printf("%d, %d\n",x,y);       /*  main.c 101번 라인  */

}                             /*  main.c 102번 라인  */

처음 3줄을 처리할 때는 이 화일의 이름이 test.c가 되고 라인의 번호도 차례대로 1, 2, 3이 된다. 그런데 그 다음 #line 100 "main.c"를 만나면서 이 화일의 이름이 main.c로 바뀌고 라인 번호도 5가 아닌 100부터 시작하게 된다.

이는 다음과 같이 일부러 에러가 나게 만들고 컴파일 작업을 해보면 쉽게 알 수 있다.

main() {

int j = i;

#line 100 "error.c"

k = 1;

}

다음은 위 프로그램을 컴파일할 때 나타나는 메시지이다.

(TC)

C:\TC>tcc test.c

Turbo C  Version 2.0  Copyright (c) 1987, 1988 Borland International

test.c

Error test.c 2: Undefined symbol 'i' in function main

Error error.c 100: Undefined symbol 'k' in function main

Warning error.c 101: 'j' is assigned a value which is never used in function mai

n

*** 2 errors in Compile ***

Available memory 395584

(MSC)

C:\C600\BIN>cl test.c

Microsoft (R) C Optimizing Compiler Version 6.00

Copyright (c) Microsoft Corp 1984-1990. All right reserved.

test.c

test.c(2) : error C2065: 'i' : undefined

error.c(100) : error C2065: 'k' : undefined

위를 보면 2번 줄의 에러는 제대로 test.c란 화일의 에러라고 나오고 있지만 #line 문 다음의 에러는 error.c란 화일의 100번째 줄에 대한 에러로 나오는 것을 볼 수 있다.

이와 같이 #line문은 에러 메시지를 출력하기 위해 사용한다. 예를 들어 프로그램이 main.c란 이름으로 다음과 같이 작성되었을 때

#include "my.h"

main() {

int k = i;

:

}

맨 처음 #include 문에 의해 전처리기가 이 화일을 읽어 들이고 읽어들인 것을 컴파일러에게 전달하게 된다. 그런데 my.h에 에러가 있다고 가정한다면, 컴파일러는 my.h란 화일을 읽었는지 모르기 때문에 main.c에서 에러가 났다고 할 것이다. 또 my.h를 읽어들이므로 해서 위의 main.c는 늘어나게 된다. 즉 라인 번호가 증가하기 때문에 만약 int k = i;에서 에러가 났다 하더라도 이는 라인 번호 3이 아닌 보다 큰 값으로 나오게 된다. 그렇기 때문에 사용자는 어디서 에러가 났는지 전혀 감을 못잡게 될 것이다. 그러나 위의 프로그램을 다음과 같이 작성하게 되면 에러가 나더라도 올바른 메시지를 얻을 수 있게 된다.

#line 1 "my.h"           /*  실제로 사용자가 이와 같이 하지 않더라도  */

#include "my.h"          /*  전처리기가 알아서 처리해 주기 때문에 사용자들은  */

#line 2 "main.c"         /*  올바은 에러 메시지를 얻고 있다  */

main() {

int k = i;

:

}

⑵ #error 문

#error 문은 전처리기로 하여금 에러 메시지를 출력시키고 컴파일 작업을 하지 않도록 만드는 것으로 다음과 같은 형태로 사용한다.

#error "에러 메시지□

전처리기가 이 문장을 만나게 되면 뒤의 에러 메시지를 실제로 컴파일 에러가 난 것처럼 출력하게 된다. 따라서 컴파일 작업이 중단되고 수행 화일은 만들어지지 않는다. 예를 들어 다음 프로그램을 컴파일 해 보면

main() {

#error "After main"        /*  에러 메시지에는 □ □를 찍지 않아도 된다  */

int i = 10;

#error "after i"           /*  공백을 제외한 첫자부터 에러 메시지로 인식한다  */

printf("%d\n",i);

}

다음과 같은 결과가 나옴을 볼 수 있다.

(TC)

C:\TC>tcc test.c

Turbo C  Version 2.0  Copyright (c) 1987, 1988 Borland International

test.c

Error test.c 2: Error directive: "After main" in function main

*** 1 errors in Compile ***

Available memory 403586

(MSC)

C:\C600\BIN>cl test.c

Microsoft (R) C Optimizing Compiler Version 6.00

Copyright (c) Microsoft Corp 1984-1990. All right reserved.

test.c

test.c(2) : error C2189: #error :  "After main"

test.c(4) : error C2189: #error :  "after i"

#error 문은 전처리기에게 에러가 발생했음을 알려주고자 할 때 사용한다. 예를 들어 각 운영체제에 맞게 프로그램을 작성할 때 사용자가 운영체제를 정의하지 않을 수 있다. 이때 다음과 같이 #error 문을 사용하면 에러로 처리할 수 있게 된다.

#if OS == DOS

DOS 프로그램

#elif OS == Unix

Unix 프로그램

#elif OS == VMS

VMS 프로그램

#else

#error "OS is not defined!"

#endif

< 그밖의 기능 >

⑴ typedef 문

typedef 문은 새로운 데이터 유형을 정의할 때 사용한다. 그러나 실제로 완전히 새로운 데이터 유형을 만드는 것은 아니고 단지 기존에 있는 데이터 유형의 이름을 정의하는 것에 불과하다.

typedef 문은 다음과 같은 형태로 사용한다.

typedef <여러 가지 데이터 유형> <대표하는 문자열>;

<여러 가지 데이터 유형>은 C가 지원하는 모든 데이터 유형이 올 수 있다.

<대표하는 문자열>은 사용자가 임의로 정하고 나중에 변수를 정의할 때 사용한다.

[사용 예] □□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

typedef unsigned char byte;          /*  unsigned char형을 byte라고 대표함  */

typedef unsigned int word;           /*  unsigned int형을 word라고 대표함  */

typedef int array[10];               /*  int형 배열을 array라고 대표함  */

byte ch;                             /*  unsigned char ch와 같음  */

word i;                              /*  unsigned int i와 같음  */

array x;                             /*  int x[10]과 같음  */

다음은 typedef를 사용하여 구조체를 선언한 예이다.

□□□□□□□□□□□□□□□□□□ 구조체 택 (structure tag) 생략 가능

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

typedef□struct {                           □

□char name[30];     □□□             □

□int kor;             □             □

□int eng;             □□□ 구성요소  □

□int math;            □             □

□int total;           □             □

□float ave;         □□□             □

□}□MAN;□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

□□□ □□□□□□□□□□□□□□□□□□□□□ typedef 문에 의해서 MAN이 점선 박스부분을 대표한다

void main(void) {

MAN person;

□    □□□□□□□□□□□ 프로그램상에서 변수를 참조할 때 쓰는 변수이름 person이라는

□                구조체를 정의 하였다

□□□□□□□□□□□□□□□□ typedef으로 대표된 구조체 이름

:

:

}

main 함수 안에서 마치 int형 변수나 char형 변수 등을 정의하는 것과 같이 구조체를 정의한다.

typedef 문을 사용하면 프로그램을 보다 더 쉽게 이해할 수 있도록 만들 수 있을 뿐만 아니라 나중에 프로그램을 변경하거나 수정할 때에도 편리하다. 반면에 #define 문을 사용해도 이와 비슷한 효과를 얻을 수 있다. 예를 들어 다음 프로그램은

typedef unsigned char TEXT;

TEXT a, b, c;

다음과 같이 #define 문을 사용하여도 같은 의미를 갖게 된다.

#define TEXT unsigned char

TEXT a, b, c;

결과는 같지만 처리 과정이 다른데 #define 문은 전처리기에 의해 처리되지만 typedef 문은 C의 문장이기 때문에 컴파일러에 의해 처리된다. 그리고 typedef 문은 데이터 유형만 정의할 수 있는데 반해 #define 문은 어떠한 스트링도 정의할 수 있다. 그러나 typedef 문은 컴파일러에 의해 처리되기 때문에 C의 구조를 이용할 수 있다(다음의 vector나 matrix를 #define 문으로 정의하기는 매우 어렵다).

typedef int vector[100];

typedef vector matrix[100];

vector는 int 유형의 원소 100개로 된 배열을 정의한 것이고 matrix는 vector 유형 100개로 된 배열을 정의한 것인데, vector 자체가 100개의 원소로 된 int 유형의 배열이므로 결국 matrix는 ia[100][100]과 같이 100 * 100개의 원소로 된 배열을 가리키는 이름이 된다.

⑵ 열거 데이터 유형(enumeration data type)

열거 데이터 유형의 사용 방법은 구조 데이터 유형과 유사한데 다음과 같은 형태로 사용한다.

⑴ enum 열거 데이터 유형 이름 { 원소1, ...,원소n } [ 변수1, ...,변수n ];

⑵ enum 열거 데이터 유형 이름  변수1, ...,변수n;    /*  enum은 enumeration의 약자로

열거 데이터 유형임을 나타내며, 뒤의 이름은 열거 데이터 유형의 이름을 의미한다  */

⑴은 새로운 열거 데이터 유형을 정의하는 것으로 이는 중괄호 안의 원소들로 이루어진 데이터 유형이 된다. 그리고 그 뒤에 변수들을 정의하면 이 변수들은 모두 앞의 열거 데이터 유형의 변수들이 되며, 이 변수들은 중괄호 안의 원소들만 값으로 가질 수 있다. 예를 들어 다음과 같이 정의하였을 때

enum weekend { Saturday, Sunday } d1, d2;

변수 d1과 d2는 Saturday나 Sunday를 값으로 가질수 있게 된다. 그리고 주의할 것은 Saturday와 Sunday는 정수 상수이지 스트링이 아니라는 것이다. 따라서 d1과 d2는 내부적으로 정수 유형의 변수가 된다.

반면에 ⑵는 이미 열거 데이터 유형이 정의되어 있을 때 이 유형의 변수들을 별도로 정의할 때 사용한다. 예를 들어 위와 같이 weekend란 열거 데이터 유형이 정의되어 있을 때.

enum weekend newday;

위에서 newday는 weekend 유형의 변수가 되어 Saturday나 Sunday를 값으로 가질수 있다.

새로운 열거 데이터 유형을 정의하면 C 컴파일러는 이를 내부적으로는 0부터 시작하는 정수로 변환한다. 예를 들어 다음과 같이 정의하였을 때.

enum days { Mon, Tue, Wed, Thu, Fri, Sat, Sun } d;

Mon부터 차례로 0, 1, 2, ... 의 값을 갖게 되어 Mon은 0, Tue는 1, Wed는 2, Thu는 3, Fri는 4, Sat은 5, 그리고 Sun은 6이 된다. 따라서 d = Sat;은 Sat이 5이기 때문에 실제로 d = 5;와 같다.   그러나 각 원소들의 값을 enum 데이터 유형으로 선언할 때 사용자가 지정할 수도 있는데, 다음과 같이 원소 = 값의 형태로 지정하면 된다.

enum days { Mon=1, Tue=3, Wed=3, Thu=-1, Fri=Mon+100, Sat=10, Sun=5 } d;

Tue와 Wed는 같은 값을 갖게 되는데, 이와 같이 같은 값을 가져도 상관은 없다. 또 특정 원소만 값을 지정하고 나머지는 안줄 수도 있는데, 이때 값이 지정되지 않은 원소들은 값이 지정된 가장 가까운 원소에서부터 1씩 증가하는 값을 갖게 된다.

enum days { Mon, Tue, Wed=5, Thu, Fri=100, Sat, Sun };

Mon은 지정된 값이 없고 맨 처음 원소이므로 0이 되며, 따라서 그 다음 원소인 Tue는 1이 된다. 그리고 Wed가 5이기 때문에 Thu는 값이 지정되지 않았어도 6이 되며, 마찬가지로 Fri가 100이기 때문에 Sat과 Sun은 각각 101과 102가 된다.

열거 데이터 유형을 사용하는 이유는 프로그램을 보다 더 알기 쉽게 만들기 위해서이다. 예를 들어 다음과 같이 프로그램을 작성하는 것이

if (day == Sun)

weekend = true;

다음과 같이 작성하는 것 보다 훨씬 더 이해하기 쉽다.

if (day == 6)

weekend = 1;

enum su { zero, one, two, three, four, five, six, seven, eight, nine, ten };

void test(void);

void main(void) {

enum su a;

a = two + six;

printf(" Two + Six = %d\n",a);

test();

}

void test(void) {

enum su b;

b = one + three;

printf(" One + Three = %d\n",b);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Two + Six = 8

One + Three = 4

※ volatile과 const 지시어

지금까지는 변수를 정의할 때 데이터 유형과 메모리 유형만 정의했었지만 실제로 하나가 더 있는데 그것이 바로 volatile과 const 지시어 이다.

우선 const를 데이터 유형 앞에 붙여 변수를 정의하게 되면 그 변수의 값은 바꿀 수가 없게 된다. 예를 들어 다음과 같이 정의하게 되면

const int i = 10;

const float pi = 3.141592;      /*  i와 pi의 값은 변경할 수 없다  */

위 두 변수의 값을 변화시키려고 하면 에러가 발생하게 된다.

또한 포인터와 관련해서 const 지시어를 사용할 때, 다음과 같이 3가지로 사용할 수 있다.

⑴ const char *s1 = "Test";

⑵ char *const s2 = "Test";

⑶ const char *const s3 = "Test";

⑴의 경우는 s1이 가리키는 값이 const, 즉 변하지 않는 것이 된다. 따라서 s1이 가리키는 스트링 □Test"를 변경할 수 없으며 strcpy(s1,"Time");과 같은 것도 사용할 수 없다. 그러나 s1의 값은 바꿀 수 있기 때문에 s1 = "New string"과 같이 사용할 수 있다. 반면에 ⑵는 s2 자체의 값을 변경할 수 없음을 의미한다. 따라서 s2 = "New string□과 같이 사용할 수는 없으나 s2가 가리키는 스트링의 값은 바꿀 수 있기 때문에 strcpy(s2,"Time");의 사용이 가능하게 된다. 마지막으로 ⑶은 s3의 값도 바꿀 수 없고 s3이 가리키는 스트링도 바꿀 수 없음을 의미한다. 따라서 s3 = "New string"은 물론 strcpy(s3,"Time")도 사용할 수 없게 된다.

const 지시어는 이름 그대로 값을 변경해서는 안되는 변수를 선언할 때 사용한다. 특히 배열과 같이 받는 쪽에서 변경할 수 있는 데이터를 인자로 전달하는 경우 받는 쪽에서 이를 변경하지 못하도록 막고자 할 때 const 지시어를 사용하면 편리한 경우가 많다.

volatile 키워드는 조금 특이한 것으로 뒤의 변수가 프로그램의 외적인 요인에 의해 값이 변할 수 있음을 컴파일러에게 알려 주는 역할을 한다. 예를 들어 다음과 같이 정의하면

volatile int t;

변수 t의 값은 프로그램을 수행하는 어느 순간에나 외적인 요인에 의해 값이 변할 수 있음을 컴파일러에게 알려 주어, 컴파일러가 볁수 t의 값을 사용할 때 주의하도록 만든다(주로 하드웨어를 제어하는 프로그램에서 발생하는데, 변수가 특정 하드웨어의 레지스터나 포트를 가리키는 경우).   특히 volatile로 정의된 변수는 레지스터에 할당되지 않는다.

□. 화일 입출력

일반적으로 화일 처리라 하면 새로운 화일을 만들고 화일의 내용을 읽고 쓰는 것을 의미하는데, C에서는 이를 기본 기능으로 제공하지 않으며 표준 라이브러리 함수들을 통해 수행하도록 하고 있다. 이 라이브러리 함수에는 두 가지 종류가 있는데, 이른바 고수준의 화일 처리 함수들로 이것을 사용하면 다양한 포맷으로 입출력을 수행할 수 있으나 효율이 떨어지는 단점이 있다. 반면에 저수준의 입출력 함수를 사용하면 포맷을 지정할 수 없는 단점이 있으나 매우 효율적으로 입출력을 수행하게 된다. 따라서 실무 프로그램에서는 오히려 저수준의 화일 입출력 함수들을 선호하는 경우가 많다.

< 고수준의 화일 입출력 함수들  >

고수준의 화일 입출력에서는 FILE이라는 구조를 공통적으로 사용하고 있다. 이는 stdio.h에 다음과 같이 정의되어 있으며, 따라서 고수준의 화일 입출력을 이용하기 위해서는 반드시 stdio.h 화일을 포함시켜야 한다.

typedef struct   {

short           level;         /* fill/empty level of buffer */

unsigned        flags;         /* File status flags    */

char            fd;            /* File descriptor      */

unsigned char   hold           /* Ungetc char if no buffer */

short           bsize;         /* Buffer size          */

unsigned char   *buffer;       /* Data transfer buffer */

unsigned char   *curp;         /* Current active pointer */

unsigned        istemp;        /* Temporary file indicator */

short           token;         /* Used for validity checking */

}       FILE;                          /* This is the FILE object */

화일을 사용하기 위해서는 먼저 화일을 여는 것이 필요하며 이때 다음과 같이 fopen함수를 사용하여야 한다.

FILE *fp;

fp = fopen(□화일 이름", □모드□);

그러면 fopen은 지정한 화일 이름의 화일을 찾아 이를 사용할 수 있는 형태로 열고 이 화일에 대한 FILE 구조를 하나 만든 후 이 구조에 대한 포인터를 계산 결과로 돌려주게 된다. 두번째의 모드는 앞의 화일을 어떤 형태로 사용할 것인가를 나타내는 것으로 다음과 같은 것들이 있다.

모드	의 미
"r"	화일을 읽기 전용으로 연다. 해당 화일이 이미 존재해 있어야 한다.
"w"	화일을 쓰기 전용으로 연다. 이미 존재하면 그 화일의 내용은 제거되며, 없으면 새로운 화일이 자동으로 만들어진다.
"a"	화일을 쓰기 전용으로 연다. 화일의 내용이 있는 경우 그 내용은 지워지지 않으며 화일의 맨 끝으로 자동으로 이동해 맨 끝에 덧붙여 쓸 수 있도록 한다. 만약 해당 화일이 없으면 새로운 화일이 자동으로 생성된다.
"r+"	화일을 읽기와 쓰기 두 가지 목적으로 연다. 해당 화일이 없으면 에러가 난다.
"w+"	화일을 읽기와 쓰기 두 가지 목적으로 연다. 해당 화일이 있으면 그 내용은 지워 지며 없으면 새로운 화일이 만들어진다.
"a+"	화일을 읽기와 쓰기 두 가지 목적으로 연다. 해당 화일이 있으면 그 화일 맨 끝으 로 이동하여 그 위치부터 읽기와 쓰기가 이루어지게 되며, 화일이 없으면 새로운 화일이 생성된다.

쓰기용("w")을 사용할 때, 이미 존재하는 화일을 열려고 하면 그 화일이 없어져버리므로 사용시에 주의를 해야 한다. 그리고 추가용("a")은 화일의 끝에 데이터를 추가하기 위해서 사용하며 갱신용("+")은 화일을 열어서 읽기와 쓰기를 한 화일에 한꺼번에 할 필요가 있을 때 사용한다.

예를 들어 test.txt란 화일의 내용을 읽기 위해서는 다음과 같이 한다.

fp = fopen("test.txt", "rb");

열기 형식에는 일반적으로 텍스트 화일("t")이나 이진 화일("b")이 들어가야 하는데 생략을 하게 되면 텍스트 화일 형식이 된다.

test.txt란 화일을 지우고 새로 쓰고 싶을 때에는 다음과 같이 하면 된다.

fp = fopen("test.txt", "w");    /*  생략했으므로 텍스트 화일이 된다  */

반면에 test.txt란 화일을 지우지 않고 화일의 끝에 덧붙이고 싶을 때에는 다음과 같이 한다.

fp = fopen("test.txt", "a");

화일명에는 드라이브와 패스를 지정할 수 있다. 패스를 표시할 때는 역슬래쉬('\')를 이중('\\')으로 해야 하는데, 그렇지 않으면 확장열이 되어 엉뚱한 결과를 가져오게 되므로 주의해야 한다.

"c:\\work\\test.txt"

화일을 열다가 에러가 발생하면(예를 들어 읽기전용으로 화일을 열 때 화일이 디스크상에 없거나, 화일을 열려는 드라이브에 디스크가 들어있지 않다거나 할 때 발생한다) fopen함수는 NULL값을 계산 결과로 산출한다. 따라서 다음과 같이 항상 조사하는 것이 필요하다.

if ((fp = fopen("test.txt", "rb")) == NULL) {

printf("File open error.\n");

exit(-1);         /*  exit함수를 만나서 프로그램을 종료한다  */

}

그리고 fp 대신에 사용할 수 있는 것이 있는데, C의 입출력에서는 데이터를 하나 또는 그 이상의 물리적(스크린이나 프린터, 플로터 등의 컴퓨터에 연결될 수 있는 모든)장치로 지향되는 스트림(stream)의 형태로 나타내기도 한다.

다음의 표는 스트림을 이용한 표준 입출력 장치인데, stdio.h헤더에 정의되어 있다.

#define stdin   (&_streams[0])

#define stdout  (&_streams[1])

#define stderr  (&_streams[2])

#define stdaux  (&_streams[3])    /*  보조 입출력 장치, 보통 COM1에 연결되어 있다  */

#define stdprn  (&_streams[4])    /*  표준 프린터 장치, LPT1에 연결되어 있다  */

stdin은 표준 입력 장치로 입력을 받아 들일 때 화일을 사용하지 않고 scanf나 getchar 등을 사용하여 입력을 받아들이던 곳을 의미하는데, 이는 보통 사용하고 있는 컴퓨터의 키보드가 된다. 반면에 stdout은 표준 출력 장치로 printf나 putchar를 사용하여 출력했을 때 그 출력이 나가는 곳을 의미하며, 이는 일반적으로 사용하고 있는 컴퓨터의 화면이 된다. stderr은 표준 에러출력 장치를 말하며, 에러 메시지가 나가는 곳으로 표준 출력과 유사하게 이도 역시 보통 사용하고 있는 컴퓨터의 화면이 된다. Unix에서는 표준 출력과 표준 에러가 다르지만 DOS에서는 이 둘을 구별하지 않기 때문에 실제로 차이가 없게 된다. 따라서 고수준의 화일 입출력을 사용할 때에 키보드로 부터 입력을 받아들이고 싶으면 fp 대신에 stdin이란 이름을 사용하면 되고 화면에 출력하고 싶으면 역시 fp란 이름 대신에 stdout이나 stderr을 사용하면 된다.

화일을 열었으면 읽거나 써야 하는데, 화일로 부터 읽어들이고자 할 때에는 다음의 fscanf함수를 사용한다. 예를 들어 두 개의 정수값을 test.dat란 화일로부터 읽어들이고자 할 때에는 다음과 같이 하면 된다.

FILE *fp;

int i, j;

if ((fp = fopen("test.dat", "r")) == NULL) {

printf("Error: Cannot open test.dat\n");

return (1);

}

fscanf(fp,"%d%d",&i,&j);

실제로 지금까지 사용해 온 scanf함수는 다음과 같은 의미를 갖는다.

fscanf(stdin,"포맷□,변수 리스트);

다음은 fscanf를 사용하는 한 예이다.

#include <stdio.h>

main() {

FILE *fp;                                       /*  □□□□□□□□□□□□□□□□□  */

int i;                                          /*  □   DATA.DAT    □  */

int no = 0;                                     /*  □  100          □  */

long int sum = 0l;                              /*  □  78           □  */

if ((fp = fopen("data.dat", "r")) == NULL) {    /*  □  52           □  */

printf("Error: Cannot open data.dat\n");      /*  □  34           □  */

return (1);                                   /*  □  86           □  */

}                                             /*  □  32           □  */

while (fscanf(fp,"%d",&i) != EOF) {             /*  □  16           □  */

sum += i;                                     /*  □  82           □  */

++no;                                         /*  □  99           □  */

}                                             /*  □  75           □  */

if (no == 0)                                    /*  □□□□□□□□□□□□□□□□□  */

printf("No data.\n");

else {

printf("Total %d numbers.\n",no);

printf("Sum = %ld\n",sum);

printf("Average = %.2f\n",(float)sum/(float)no);

}

fclose(fp);   /* 화일을 열어서 사용한 후에는 반드시 화일을 닫아야 한다. 화일의 닫음은                 버퍼에 들어있던 내용을 모두 디스크로 보내서 버퍼를 비우고 fopen 함수로 화일을               열어서 되돌림 값을 받을 때 사용했던 포인터를 재사용 할 수 있게 해준다.

fclose 함수의 인자는 fopen 함수에서 되돌려 받았던 포인터가 들어가고 되돌림

값은 닫기가 성공일 때 0, 에러일 때 EOF를 되돌린다  */

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>test □

Total 10 numbers.

Sum = 654

Average = 65.40

scanf 함수가 표준 입력 장치(stdin)로 부터 입력받던 것을 fscanf 함수는 화일로부터 읽어오는데 사용한다.

int a;

long int b;

double c;

char d[10] = { 0, };

.....

fscanf(fp,"%d %ld %lf %s",&a,&b,&c,d);  /*  scanf 함수와 마찬가지로 fscanf 함수의

입력 데이터들간의 구분은 공백문자(' ', \t, \n 등)로 이루어진다  */

/*  화일에 다음과 같은 데이터가 들어있다고 가정한다  */

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

□5□ □4□0□0□0□0□ □1□2□.□3□4□\r□\n□H□e□l□l□o□ □w□o□r□l□d□\r□\n□

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

/*  위의 fscanf 함수가 실행되고 나면 각 변수에는 다음과 같은 값이 들어가게 된다  */

a = 5

b = 40000

c = 12.34

d = "Hello"

또 getchar와 같이 한 글자씩 화일로부터 읽어들일 수도 있는데 이때에는 getc를 사용하면 된다.

char c;

FILE *fp;

c = getc(fp);   /*  fp가 가리키는 화일로부터 한 문자를 읽어 이를 계산 결과로 산출  */

따라서 getchar는 다음과 같은 의미를 갖는다.

getc(stdin);

다음은 getc를 사용하여 DOS의 type과 같은 일을 하는 프로그램의 한 예이다.

#include <stdio.h>

main(int argc, char *argv[]) {

FILE *fp;

char c;

if (argc == 1) {           /*  인자가 없으면  */

printf("USAGE : %s filename(1) ...filename(n)\n",*argv);

return (1);

}

while (--argc > 0) {       /*  인자가 있으면  */

if ((fp = fopen(*++argv, "r")) == NULL) {

printf("Error: Cannot open %s\n",*argv);

continue;

}

printf("\n*** %s ***\n",*argv);

while ((c = getc(fp)) != EOF)

putchar(c);

}

fclose(fp);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>fileview \config.sys \autoexec.bat □

지금까지는 입력에 대해 알아보았다. 출력의 경우도 마찬가지인데 다음과 같이 fprintf 함수를 사용하면 된다.

fprintf(fp,"포맷□,변수 리스트);

따라서 printf는 다음과 같은 의미를 갖게 된다.

fprintf(stdout,"포맷□,변수 리스트);

fprintf를 사용하면 stdout이 아닌 stderr으로도 출력을 내보낼 수 있는데, 이때에도 다음과 같이 사용하면 된다.

fprintf(stderr,"포맷□,변수 리스트);

stderr은 에러 메시지를 내보내기 위한 것이기 때문에, 에러 메시지는 이를 통해 내보내는 것이 좋다.

다음 프로그램은 sort [-r] file(1) file(2)와 같이 사용하는 것으로 [ ]는 생략할 수 있는 부분을 의미하며 file(1)화일을 라인 단위로 정렬하여 file(2)에 출력하는데 -r을 지정하면 큰 순서에서 작은 순서로 정렬하고, 지정하지 않으면 작은 순서에서 큰 순서로 출력하는 것이다.

#include <stdio.h>

#define REVERSE     -1

#define NORMAL       1

#define MAXLINENO  100

#define MAXCNO      80

#define TRUE    EOF*-1

main(int argc, char *argv[]) {

static char text[MAXLINENO][MAXCNO];

char t[MAXCNO];

FILE *fp1, *fp2;

int direction = NORMAL;

int no = 0;

int i, j;

if ((argc != 3 && argc != 4) || (argc == 4 && strcmp(*(argv+1), "-r"))) {

fprintf(stderr,"USAGE: %s [-r] filename(1) filename(2)\n",*argv);

return (1);

}

if (argc == 4) {              /*  -r을 주면 역으로 정렬  */

direction = REVERSE;

++argv;

}

if ((fp1 = fopen(*++argv, "r")) == NULL) {

fprintf(stderr,"Error: Cannot open %s\n",*argv);

return (2);

}

if ((fp2 = fopen(*++argv, "w")) == NULL) {

fprintf(stderr,"Error: Cannot create %s\n",*argv);

return (3);

}

while (get_line(fp1, text[no++]) != EOF);

for (i = 0; i < no - 2; i++)

for (j = i + 1; j < no - 1; j++)

if (strcmp(text[i], text[j]) * direction > 0) {

strcpy(t, text[i]);

strcpy(text[i], text[j]);

strcpy(text[j], t);

}

for (i = 0; i < no - 1; i++)

fprintf(fp2,"%s\n",text[i]);

fclose(fp1);

fclose(fp2);

}

int get_line(FILE *fp, char *s) {     /*  화일로부터 한 라인을 읽어들임  */

char *cp = s;                       /*  □□□□□□□□□□□□□□□□□□□□□□  */

while ((*s = getc(fp)) != EOF)      /*  □  ( DATA )          □  */

if (*s == '\n') {                 /*  □  Lee Seung Wook    □  */

*s = '\0';                      /*  □  Kwon Oh Jin       □  */

return (TRUE);                  /*  □  Oh Jung Pyo       □  */

}                               /*  □  Kim Joon Sik      □  */

else                              /*  □  Yun Young Il      □  */

++s;                            /*  □  Kwon Oh Chang     □  */

if (cp == s)                        /*  □  Kim Chang Ju      □  */

return (EOF);                     /*  □  Hong Gil Dong     □  */

*s = '\0';                          /*  □  Kim Young Hoon    □  */

return (TRUE);                      /*  □  Kim Jong Min      □  */

}                                   /*  □□□□□□□□□□□□□□□□□□□□□□  */

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>sort data sdata □

C:\TC>type sdata □

Hong Gil Dong

Kim Chang Ju

Kim Jong Min

Kim Joon Sik

Kim Young Hoon

Kwon Oh Chang

Kwon Oh Jin

Lee Seung Wook

Oh Jung Pyo

Yun Young Il

C:\TC>sort -r data rdata □

C:\TC>type rdata □

Yun Young Il

Oh Jung Pyo

Lee Seung Wook

Kwon Oh Jin

Kwon Oh Chang

Kim Young Hoon

Kim Joon Sik

Kim Jong Min

Kim Chang Ju

Hong Gil Dong

다음의 예제는 키보드로부터 입력받은 문장들에 줄번호를 붙여서 test.txt 화일로 저장하는 일을 하는 것으로, 프로그램의 종료는 아무 문장도 입력하지 않고 엔터키를 치면 된다.

#include <stdio.h>

void main(void) {

FILE *fp;

char str[80];

int line = 0;

if ((fp = fopen("test.txt", "w")) == NULL) {

printf("File open error ... \n");

exit(-1);

}

for ( ; ; ) {

gets(str);

if (str[0] == '\0')

break;

line++;

fprintf(fp,"%3d : %s\n",line,str);

}

fclose(fp);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>test □

Time □

can □

never □

mend. □

□

C:\TC>type test.txt □

1 : Time

2 : can

3 : never

4 : mend.

또 putchar와 같이 한 글자씩 출력하는 함수도 있는데, 이의 이름은 putc이다. putc는 다음과 같은 형태로 사용한다.

char c;

FILE *fp;

putc(c,fp);   /*  fp가 가리키는 화일에 c가 출력되게 된다  */

따라서 putchar(c)는 다음과 같은 의미를 갖는다.

putc(c,stdout);

다음은 putc의 한 사용예이다.

#include <stdio.h>

main() {

char c;

char fname1[20], fname2[20], fname3[20];

FILE *fp1, *fp2, *fp3;

printf("Input the first file name -> ");

gets(fname1);

printf("Input the second file name -> ");

gets(fname2);

printf("Input the file name to create -> ");

gets(fname3);

if ((fp1 = fopen(fname1, "r")) == NULL) {

printf("Error : Cannot open %s\n",fname1);

exit(-1);

}

if ((fp2 = fopen(fname2, "r")) == NULL) {

printf("Error : Cannot open %s\n",fname2);

fclose(fp1);

exit(-1);

}

if ((fp3 = fopen(fname3, "w")) == NULL) {

printf("Error : Cannot create %s\n",fname3);

fclose(fp1);

fclose(fp2);

exit(-1);

}

while ((c = getc(fp1)) != EOF)         /*  화일로부터 한자씩 읽어들일 때에는 getc를,  */

putc(c,fp3);                         /*  출력할 때에는 putc를 사용하면 된다  */

fclose(fp1);

while ((c = getc(fp2)) != EOF)

putc(c,fp3);

fclose(fp2);

fclose(fp3);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

Input the first file name -> \config.sys □

Input the second file name -> \autoexec.bat □

Input the file name to create -> sysbat.txt □

이 밖에도 화일에서 한 글자를 입출력하는 함수로 fgetc와 fputc가 있다.

int fgetc(FILE *fp)

int fputc(int c, FILE *fp)

fgetc 함수는 지정한 화일에서 읽어들인 문자를 되돌리고 화일 포인터를 한 개 증가시킨다. 화일 포인터가 화일의 끝에 도달했을 경우에는 EOF를 되돌린다. 이 EOF값과 0xff값을 구분하기 위해서 읽어들인 문자는 int형으로 변환되어 반환된다.

fputc함수는 스트림(stream)으로 지정한 화일에 문자 c를 출력한다. 디스크가 꽉찼을 경우나 출력시 에러가 발생하면 EOF값을 되돌리고, 텍스트 모드로 오픈되어 있으면 '\n'을 CR·LF로 변환한다.

다음의 예제는 fgetc 함수와 fputc 함수를 이용한 화일복사 프로그램이다.

#include <stdio.h>

void main(int argc, char *argv[]) {

FILE *srcfp, *objfp;

int ch;

if (argc != 3) {

printf("USAGE : %s sourcefile objectfile\n",*argv);

exit(-1);

}

if ((srcfp = fopen(argv[1], "rb")) == NULL) {

printf("File open error : %s\n\a",argv[1]);

exit(-1);

}

if ((objfp = fopen(argv[2], "wb")) == NULL) {

printf("File creation error : %s\n\a",argv[2]);

exit(-1);

}

printf("FileCopy %s to %s\n\n",argv[1],argv[2]);

while ((ch = fgetc(srcfp)) != EOF)

fputc(ch,objfp);

fclose(srcfp);

fclose(objfp);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>scopy tc.exe ec.exe □

FileCopy tc.exe to ec.exe

< 저수준 화일 입출력 함수들 >

저수준의 화일 입출력에서는 FILE이란 구조 대신 간단하게 각 화일마다 번호를 사용하는데, 이를 화일 식별자(file descriptor), 또는 핸들(handle)이라고 한다. 이 핸들은 0 이상의 값을 가지고 있는데 실제로 0과 1, 2 는 고정된 의미(핸들 0은 표준 입력을 위한 번호이며 1은 표준 출력, 그리고 2는 표준 에러로 사용)를 갖고 있어서 화일을 처음 열게 되면 그 화일의 핸들은 3이 된다.

저수준의 화일 입출력에서도 화일을 사용하기 위해서는 화일을 먼저 열어야 하며, 이때 다음과 같이 open 함수를 사용한다.

int fd;

fd = open("화일 이름□, 액세스 방식[, 모드]);

위에서 [ ]는 역시 생략할 수 있는 부분을 의미하며, 화일 이름은 fopen과 같이 열 화일의 이름이고 액세스 방식은 이 화일을 어떻게 열 것인가인데, fopen과는 달리 다음과 같은 형태로 사용한다.

모드	값	내 용
O_RDONLY	0x0001	읽기 전용으로 화일을 연다.
O_WRONLY	0x0002	쓰기 전용으로 화일을 연다.
O_RDWR	0x0004	읽고 쓰기 위해 화일을 연다.
O_CREAT	0x0100	화일이 없을 경우 새로운 화일을 만든다.
O_TRUNC	0x0200	현재 있는 화일의 내용을 0으로(제거) 한다.
O_EXCL	0x0400	O_CREAT과 함께 사용하며, 화일이 없을 경우에만 연다.
O_APPEND	0x0800	화일을 쓰기용으로 열고 화일 포인터를 화일의 끝에 위치시킨다.
O_TEXT	0x4000	화일을 텍스트 형식으로 연다.
O_BINARY	0x8000	화일을 이진 형식으로 연다.

위의 O_로 시작하는 것들은 모두 상수로 이의 정의는 fcntl.h(이것은 file control의 약자)에 들어 있기 때문에 open 문을 사용하려면 반드시 fcntl.h를 포함하여야 한다.

위의 것들은 액세스 방식의 한 조건들로 여러 개를 동시에 같이 사용할 수 있으며 이 때에는 각 조건들을 '|'(비트 연산자 OR)를 이용해서 묶으면 된다.

data.dat 란 화일을 읽기 전용으로 열고자 할 때에는 다음과 같이 하면 된다.

fd = open("data.dat", O_RDONLY);

그리고 기존의 화일이 있으며, 이를 지우고 쓰기 전용으로 열 때에는 다음과 같이 하면 된다.

fd = open("data.dat", O_WRONLY | O_TRUNC);

O_TRUNC이 추가 되었는데, 만약 이를 써 주지 않으면 쓰기 전용이라도 화일의 내용은 없어지지 않는다. 반면에 화일의 끝에 추가하고자 할 때에는 다음과 같이 O_APPEND를 써 주면 된다.

fd = open("data.dat", O_WRONLY | O_APPEND);

fopen 문과 다른 것은 해당 화일이 없는 경우 화일이 만들어지지 않는다는 것이다. 만약 화일이 없을 때 화일을 만들도록 하고 싶으면 다음과 같이 O_CREAT를 지정하여야 하며, 이 때에는 세번재 인자가 필요하다.

fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC, 모드);

/*  화일이 있으면 그 내용을 지우고 없으면 생성, fopen의 "w"와 같음  */

fd = open("data.dat", O_WRONLY | O_CREAT | O_APPEND, 모드);

/*  화일이 있으면 그 끝으로 이동하고 없으면 생성, fopen의 "a"와 같음  */

세번째 인자인 모드에는 액세스 방식에서 O_CREAT 플래그가 지정된 경우, sys\stat.h(TC의 경우)에 정의되어 있는 다음 기호 중 하나가 사용된다(일반적으로 8진수 0700을 주면 된다).

S_IWRITE               /*  써 넣기 가능  */

S_IREAD                /*  읽어 내기 가능  */

S_IREAD | S_IWRITE     /*  읽기 / 쓰기 가능  */

읽기용 화일에서는 모드를 생략해도 화일의 사용에는 별 지장이 없다.

그밖에 O_RDWR은 읽고 쓰고자 할 때 사용하며 O_EXCL은 O_CREAT하고만 같이 사용하는데, 지정한 화일이 있으면 에러가 난다. 즉 화일이 없는 상태에서 새로 만들고자 할 때에는 O_EXCL과 O_CREAT를 같이 사용하면 된다.

open이 제대로 화일을 열게 되면 음이 아닌 정수값을 반환하는데, 그 화일의 핸들을 계산 값으로 산출하게 되며 에러 발생의 경우 -1을 계산 결과로 산출하게 된다. 따라서 다음과 같이 항상 조사하는 것이 필요하다.

if ((fd = open("data.dat", O_RDONLY)) == -1) {

fprintf(stderr,"Error: Cannot open data.dat\n");

:

화일을 열었으면 이를 읽고 써야 하는데, 읽을 때에는 다음과 같이 read란 함수를 사용한다.

int n;

char *buf;

int size;

int fd;

n = read(fd, buf, size);

여기서 fd는 open에서 넘겨준 화일의 번호이고 buf는 읽어들일 데이터를 저장할 공간(버퍼)을 가리키는 포인터이다. 그리고 size는 몇 개의 바이트를 읽어들일 것인가, 그 크기를 나타낸다. 즉 read는 화일 fd로부터 size 만큼의 바이트를 읽어들여 이를 buf가 가리키는 곳에 저장하게 된다.

화일 내에 size 만큼의 데이터가 있지 않을 수도 있는데, 이를 조사하기 위해 read는 자신이 실제로 읽어들인 바이트의 수를 계산 결과로 산출하고 있다. 따라서 read를 사용할 때에는 항상 위의 n 값을 잘 조사하여야 하며, 이것이 실제 읽어들인 데이터의 수이기 때문에 buf가 가리키는 공간에는 바로 n 바이트가 존재하게 된다. 이 n이 0이라는 것은 읽어들인 데이터가 없다, 즉 EOF라는 의미이며 이 값이 -1이면 이는 읽을 때 에러가 발생한 것을 의미한다. 예를 들어 data.dat란 화일로부터 80 바이트의 데이터를 읽어들이고자 할 때에는 다음과 같이 사용한다.

int fd;

char buf[80];

int n;

fd = open("data.dat", O_RDONLY);

n = (fd, buf, 80);        /*  두번째 인자(buf)에 들어가는 값은 포인터이어야 한다  */

이때 n이 80이면 잘 읽은 것이고 다른 값이면 데이터가 부족하거나 뭔가 잘못된 것이다. 일반적으로 읽을 데이터의 크기는 다르지만 버퍼의 크기는 보통 고정적으로 사용하는데 stdio.h에 정의되어 있는 BUFSIZ를 사용하는 것이 관례로 되어 있다. 이 크기는 한 화일로부터 가져올 수 있는 데이터의 최적치이기 때문에 많은 양의 데이터를 읽거나 쓸 때에는 이 것을 사용하는 것이 성능을 최대한 높일 수 있다.

다음은 read 함수의 한 예로서 size file(1) ... fine(n)과 같이 사용하여 각 화일들의 크기를 계산하여 출력하는 프로그램이다.

#include <stdio.h>

#include <fcntl.h>

main(int argc, char *argv[]) {

int fd;

long int size;

int n;

char buf[BUFSIZ];

if (argc == 1) {

fprintf(stderr,"USAGE: %s file(1) file(2) ... file(n)\n",*argv);

return (0);

}

while (--argc > 0) {

if ((fd = open(*++argv, O_RDONLY | O_BINARY)) == -1) {

fprintf(stderr,"Error: Cannot open %s\n",*argv);

continue;

}

size = 0l;

while ((n = read(fd, buf, BUFSIZ)) > 0)

size += n;

if (n == 0)

fprintf(stdout,"%s: %ld bytes.\n",*argv,size);

else

fprintf(stderr,"Error in reading %s\n",*argv);

}

close(fd);          /*  핸들이 다루는 화일을 닫는다. 되돌림 값은 화일 닫기가  */

}                   /*  성공하면 0, 실패하면 -1의 값을 되돌린다  */

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>size tc.exe tcc.exe □

tc.exe: 290249 bytes.

tcc.exe: 179917 bytes.

저수준의 화일 입출력에서 화일로부터 읽어들이는 것은 이 read 밖에 없다. 즉 문자 하나씩 밖에 읽어들이지 않기 때문에 만약 정수값을 읽어들이고자 할 때에는 읽어들인 것을 정수로 변환하여야 한다. 따라서 복잡한 포맷으로 된 데이터를 읽어들일 때에는 고수준의 화일 입출력을 사용하는 것이 좋다.

read 와 반대로 화일에 쓰고자 할 때는 다음과 같이 write이라는 함수를 사용한다.

int fd;

char *buf;

int size;

int n;

n = write(fd, buf, size);

fd는 open에 의해 넘겨 받은 화일의 핸들이며 buf는 출력할 데이터가 들어 있는 곳을 가리키는 포인터이고, size는 출력할 데이터의 크기(바이트수)를 의미한다. 즉 fd가 나타내는 화일에 size만큼의 바이트를 buf로부터 가져와 출력하라는 의미가 된다. 이때 write 함수는 실제로 쓴 바이트의 수를 계산 결과로 산출하게 된다. 이는 항상 size와 같지는 않은데, 예를 들어 디스크가 꽉차서 더 이상 쓸 공간이 없게 되면 지정한 크기 보다 적은 수를 쓰게되므로 n이 size보다 작을 수 있다. 그리고 에러가 발생한 경우에는 read와 같이 -1을 계산 결과로 산출한다.

다음은 read와 write 함수를 사용하여 한 화일을 똑같이 복사하는 프로그램이다.

#include <stdio.h>

#include <fcntl.h>

main(int argc, char *argv[]) {

int fd1, fd2;

char buf[BUFSIZ];

int n;

if (argc != 3) {

fprintf(stderr,"USAGE: %s sourcefile objectfile\n",*argv);

return (1);

}

if ((fd1 = open(*(argv+1), O_RDONLY | O_BINARY)) < 0) {

fprintf(stderr,"Error: Cannot open %s\n",*(argv+1));

return (2);

}

if ((fd2 = open(*(argv+2), O_WRONLY | O_TRUNC | O_CREAT | O_BINARY, 0700)) < 0) {

fprintf(stderr,"Error: Cannot create %s\n",*(argv+2));

return (3);

}

printf("FileCopy %s to %s\n\n",*(argv+1),*(argv+2));

while ((n = read(fd1, buf, BUFSIZ)) > 0)

if (write(fd2, buf, n) != n) {    /*  read와 마찬가지로 저수준의 화일 입출력에서 출력          함수는 write 밖에 없다. 따라서 문자 데이터를 출력하기에는 괜찮지만 정수나 실수값을

출력하고자 할 때에는 이를 ASCII 코드 형태로, 즉 문자 형태로 변환한 다음 출력하여야

하기 때문에 그러한 경우에는 고수준의 입출력 함수를 사용하는 것이 더 편리하다  */

fprintf(stderr,"Error in writing %s\n",*(argv+2));

return (4);

}

if (n < 0)

fprintf(stderr,"Error in reading %s\n",*(argv+1));

close(fd1);

close(fd2);

}

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□

결과

C:\TC>slcopy tc.exe ec.exe □

FileCopy tc.exe to ec.exe