'C_grammer'에 해당되는 글 6건

  1. 2010.11.19 표준 함수
  2. 2010.11.18 기억 부류
  3. 2010.11.17 함수
  4. 2010.11.16 연산자
  5. 2010.11.15 제어문
  6. 2010.11.12 변수

표준 함수

old/C Grammer 2010. 11. 19. 11:05
1. 수학 함수

1) 표준 함수

C컴파일러는 많은 수의 표준 함수들을 제공한다. 공통적으로 자주 사용되는 기능들을 모든 개발자들이 직접 만들어 쓴다면 시간도 많이 걸릴 것이고 사회적인 낭비도 심할 것이다. 모든 개발자들이 화면 입출력을 위해 printf, scanf 같은 함수를 일일이 만들어 써야 한다면 얼마나 끔찍하겠는가? 이런 함수들의 기능은 워낙 뻔하기 때문에 누가 만들어도 비슷한 모양을 가질 것이며 어떤 특수하고도 고유한 기능을 요구하는 것도 아니다.

표준은 필요한 함수의 최소한의 목록만을 규정하며 컴파일러 제작사는 필요에 따라 함수를 더 추가로 정의할 수도 있다. 그래서 컴파일러 제작사에 따라 런타임 라이브러리의 구성이 조금씩 달라질 수 있으므로 약간의 주의를 기울일 필요는 있다. 예를 들어 터보 C에는 gotoxy, clrscr 함수들이 있지만 비주얼 C++에는 이 함수들이 없으며 같은 함수라도 이름이 조금씩 다른 경우도 있다. 하지만 printf, puts, getch 같은 기본적인 함수들은 대부분의 컴파일러에 공통적으로 존재하므로 컴파일러에 상관없이 자유롭게 사용할 수 있다. 표준 함수들은 기능에 따라 다음과 같이 분류할 수 있다.

분류

함수

입출력 함수

printf, scanf, gets, puts, getch, putch

수학 함수

sin, cos, tan, pow, floor, ceil, hypot

문자열 함수

strcpy, strlen, strcat, strstr, strchr

시간 함수

time, asctime, clock

파일 입출력 함수

fopen, fclose, fseek, fread, fwrite

프로그램 제어

exit, abort, system

메모리 할당

malloc, free, realloc, calloc

기타

rand, delay


 2) 삼각 함수

수학 함수는 수학적인 계산을 하는 함수들이다. 수학 함수들의 원형은 모두 math.h에 선언되어 있으므로 이 함수들을 사용하려면 제일 먼저 #include <math.h> 전처리문을 삽입하여 이 헤더 파일을 포함시켜야 한다. 이 책에서 사용하는 Turboc.h가 이 헤더 파일을 포함하고 있지 않으므로 math.h를 포함하지 않으면 수학 함수를 쓸 수 없다. 실제 프로젝트를 할 때도 반드시 math.h를 인클루드해야 함을 꼭 기억해 놓도록 하자.

수학 함수 중에 비교적 이해하기 쉬운 삼각 함수에 대해 먼저 정리해 보자. 삼각 함수들은 이름만 다르고 원형이 모두 동일하다. 실수형 인수를 하나 받아들이며 이 인수의 삼각함수값을 계산하여 그 결과를 실수로 리턴한다.

double sin(double x);
double cos(double x);
double tan(double x);
double asin(double x);
double acos(double x);
double atan(double x);
double sinh(double x);
double cosh(double x);
double tanh(double x); 

기본적인 수학 교육을 받았다면 sin, cos, tan 함수가 어떤 값을 계산한다는 것은 잘 알고 있을 것이다. asin, acos, atan 함수는 기본 삼각 함수의 역함수들이며 sinh, cosh, tanh는 쌍곡선 삼각함수라는 것이다. 설마 그럴리야 없겠지만 삼각 함수가 뭐하는 함수인지 모르겠다는 사람은 수학책을 참고하기 바란다. 이 책은 수학책이 아니므로 함수들이 구하는 값의 수학적 의미에 대한 설명은 하지 않기로 한다.

삼각 함수들이 받아들이는 인수 x는 360분법의 각도가 아니라 호도(라디안)값이다. 1호도는 원주의 길이가 반지름과 같아지는 각도인데 180/3.1416으로 정의되어 있다. 따라서 각도값으로 호도를 구할 때는 다음 공식을 사용하면 된다.

호도=각도*3.1416/180

sin(r*3.1416/180)


3) 지수 함수

지수 함수는 거듭승이나 제곱근, 로그 따위의 값을 구하는 함수들이다. 실수 차원에서 계산을 하므로 취하는 인수나 리턴값은 모두 정밀도가 높은 double 실수형이다. 다음과 같은 것들이 있다.

함수

설명

double sqrt(double x);

x 제곱근

double pow(double x,double y);

xy. x y

double log(double x);

자연 대수

double log10(double x);

상용 대수

double exp(double x);

자연 대수 exp

double hypot(double x,double y);

직삼각형의 사변 길이

 sqrt 함수는 제곱근, 즉 두 번 곱해서 그 값이 되는 수를 구한다. sqrt(4)의 결과는 2.0이 될 것이며 sqrt(2)는 에 해당하는 1.414214를 리턴할 것이다. 어떤 수 x의 제곱을 구할 때는 x를 두 번 곱하는 x*x식을 사용하는데 세제곱 이상을 구할 때는 pow 함수(Power의 약자이다.)를 사용하는 것이 더 편리하다. 예를 들어 2의 10승을 구하고자 한다면 pow(2,10)을 호출하면 된다. 베이직이나 파스칼같은 고급 언어는 거듭승을 구하는 별도의 연산자(^)가 제공되지만 C언어는 거듭승 연산자를 따로 제공하지 않으므로 pow 함수를 사용해야 한다.

log 함수는 로그값을 계산하고 log10은 밑이 10으로 고정되어 있는 상용 로그값을 구한다. 지수 함수 중에는 hypot 함수(Hypotenuse의 약자이다.)가 제일 어려운데 이 함수도 잘 알아두면 유용하게 쓰일 곳이 많다. hypot가 계산하는 값은 인수로 주어진 x와 y의 제곱의 합에 대한 양의 제곱근이다. 무척 복잡한 것 같지만 수식으로 표현해 보면 이다. 이 수식은 직각 삼각형의 빗변 길이를 구하는 공식인데 이른바 피타고라스의 정리라고 한다.

hypot(x,y)는 sqrt(pow(x,2)+pow(y,2))와 동일하며 조금 더 간단하게 쓴다면 sqrt(x*x+y*y)와 같다. 두 점의 좌표를 알고 있을 때 이 두 점간의 거리를 구하고 싶다면 hypot 함수를 사용한다. 예를 들어 한 점을 중심으로 하고 나머지 한 점까지를 반지름으로 하는 원을 그린다거나 할 때 이 함수가 필요할 것이다. 사용예를 보이고 싶으나 콘솔 환경에서는 그래픽을 그릴 수 없으므로 다음에 그래픽을 배우면 그때 직접 실습해 보기 바란다.

요즘은 컴퓨터라는 기계가 게임도 하고 인터넷도 하고 영화도 보고 다양한 용도로 활용되고 있지만 원래 컴퓨터는 수학적인 계산을 위해 만들어진 것이다. 그래서 프로그래밍은 수학과 아주 밀접한 연관이 있는데 간단한 프로그램은 사칙 연산으로도 원하는 대부분의 계산을 할 수 있지만 조금만 복잡해지면 고등 수학이 필요해진다.


4) 정수화 함수

정수화 함수는 실수형 데이터에서 정수부만을 취하는, 즉 소수점 이하의 소수부를 잘라 버리는 함수이다. 소수부를 잘라 버린다고 해서 계산 결과가 정수가 되는 것은 아니며 리턴값은 여전히 실수이다. 실수값의 소수부만을 0으로 만든다고 생각하면 된다. 정수화 함수에는 다음 두 가지가 있다.

double floor( double x );
double ceil( double x );

두 함수는 소수점 이하를 자르는 방식이 다른데 floor는 소수점 이하를 버리고 정수부만을 취하고 ceil은 소수점 이하를 올림해서 정수부를 1증가시킨다. 다음 호출 예를 보면 쉽게 이해가 될 것이다.

floor(3.14);             // 결과는 3.0
ceil(3.14)           // 결과는 4.0

floor는 내림을 하는 함수이고 ceil은 올림을 하는 함수라고 일단 정리할 수 있다. 그러나 단순히 내림, 올림으로 이 두 함수의 동작을 정의하는 것은 정확하지 않다. 다음 예를 보자.

floor(-3.14);       // 결과는 -4.0
ceil(-3.14)              // 결과는 -3.0 

결과가 조금 이상해 보이는데 인수가 음수일 때 floor는 정수부가 1 감소하며 ceil은 소수부를 버린다. 왜 그런가 하면 음수에서도 버림에 의해 수가 더 작아져야 하고 올림에 의해 수가 더 커져야 하기 때문이다. 그래서 floor, ceil 함수의 동작을 좀 더 일반적으로 표현하면 다음과 같다.

floor : 주어진 인수보다 크지 않은 최대 정수
ceil : 주어진 인수보다 작지 않은 최소 정수

수직선상에서 이 함수들의 동작을 설명해 보면 floor는 인수의 바로 왼쪽 정수값을 구하고 ceil은 바로 오른쪽 정수값을 구한다.

수 체계를 시각화해서 보면 좀 더 쉽게 이해가 갈 것이다. floor는 "마루, 바닥"이라는 뜻이고 ceil은 "천장"이라는 뜻인데 단어뜻과 연관지어 보면 자연스럽게 이해가 될 것이다.

실수 x 반올림한 = floor(x+0.5)
소수점 둘째자리 반올림 값 = floor(x*10+0.5)/10

 
5) 절대값 함수

절대값 함수는 인수의 부호를 강제로 양수로 바꾼다. 즉 3은 그냥 3으로 두고 -3은 3으로 바꾸는 것이다. 인수의 타입에 따라 3가지 함수가 준비되어 있다. 이외에 복소수 타입에 대한 절대값 함수도 있으나 여기서는 다루지 않기로 한다. 

int abs(int n);
long labs(long n);
double fabs(double x);

위에서부터 순서대로 정수형, long형, 실수형에 대한 절대값을 구한다.


2. 난수 함수

1) 표준 난수 함수

난수(Random Number)란 무작위로 만들어지는 알 수 없는 값이다. 마치 주사위를 던졌을 때 어떤 수가 나올지 미리 알수 없는 것처럼 말이다. 어떤 값을 가지게 될 지 예측할 수 없는 수라는 뜻인데 이런 난수가 필요한 이유는 말 그대로 예측을 허용하지 않기 위해서이다.

만약 포커 게임을 만드는데 게임을 할 때마다 나오는 패가 동일하다면 이 게임은 정말 재미없을 것이다. 또한 슈팅 게임에서 적이 움직이는 경로에 일정한 규칙이 있다거나 퍼즐 게임의 퍼즐이 예측 가능하다면 이 또한 제대로 된 게임이라고 할 수 없다. 패를 무작위로 섞기 위해, 적의 움직임을 미리 알 수 없도록 하기 위해 난수가 필요하다.

난수를 만들 때는 일반적으로 random 이라는 함수를 사용하며 난수 루틴을 초기화할 때는 randomize라는 함수를 사용한다. 그러나 이 함수들은 진짜 함수가 아니라 매크로로 정의되어 있는 가짜 함수들이다. 가짜 함수만 쓸 수 있어도 난수를 만드는데는 큰 불편함이 없지만 내부를 좀 더 정확하게 이해하기 위해 이 매크로 함수들을 분석해 보자. 난수를 만드는 진짜 함수는 다음 두 개이다.

int rand(void);
void srand(unsigned int seed);

rand 함수는 0~RAND_MAX 범위의 수 중에서 무작위로 한 수를 생성해 낸다. RAND_MAX는 컴파일러에 따라 다르지만 일반적으로 32767(0x7ffff)로 정의되어 있다. 그래서 rand 함수를 호출하면 0부터 32767중의 임의의 정수 하나가 리턴된다.

#define randomize() srand((unsigned)time(NULL))  // 완전 난수

randomize 함수는 현재 시간을 사용하여 난수 발생기를 초기화하며 random 함수는 인수로 전달된 n사이의 난수를 발생시킨다. rand, srand 함수보다 훨씬 더 직관적이고 원형이 간단하기 때문에 아주 옛날부터 난수 생성을 위해 이 두 매크로 함수를 사용하는 것이 정석이었다. 그래서 볼랜드사의 터보 C, 볼랜드 C 계열 컴파일러는 이 두 함수를 stdlib.h 헤더 파일에 정의해 놓았다.

그러나 볼랜드 이외의 컴파일러들은 이 매크로를 헤더 파일에 정의해 놓지 않아서 rand, srand 함수를 직접 사용해야 하는 불편함이 있다. 하지만 컴파일러가 제공하지 않는다고 randomize, random 함수를 못 쓰는 것은 아니며 필요하면 이 매크로를 직접 정의해서 사용할 수 있다. 그래서 이 책은 Turboc.h 헤더 파일에 이 두 매크로를 미리 정의해 놓았으며 이 헤더만 포함하면 터보 C를 쓰듯이 두 함수를 자유롭게 사용할 수 있다. 만약 실전에서 이 두 함수가 필요하면 Turboc.h에 있는 매크로 정의문을 복사해서 사용하면 된다.

2) 난수의 생성

random 함수 자체는 간단하지만 응용하기에 따라서는 아주 다양한 유형의 난수를 만들 수 있다. 유형별로 random 함수의 응용예를 보도록 하자.

(1) 0~n사이의 난수는 random(n)으로 생성한다. 이때 생성되는 난수의 범위에 인수 그 자체는 제외되며 난수 중 제일 큰 값은 n-1이다.

random(10)       // 0~9 까지의 난수
random(89)       // 0~88 까지의 난수

(2) random 함수가 만들어내는 난수의 최소값은 항상 0으로 고정되어 있다. 난수 범위의 끝 값은 인수 n으로 조정하며 범위의 시작값은 random 호출 결과에 상수를 더해 조정한다.

random(10)+1         // 1~10까지의 난수
random(20)+10   // 10~29 까지의 난수

random 함수로 생성한 난수에 상수를 더하면 난수의 범위가 평행이동된다.

(3) 난수의 범위가 평행 이동되면 범위의 끝도 같이 이동되므로 미리 계산에 포함하여 범위도 적당히 줄여야 한다. 5~10 사이(10은 제외)의 난수를 만들고 싶다고 해서 random(10)+5라고 호출해서는 안되며 random(5)+5라고 호출해야 한다. 일정한 범위의 난수 생성문을 일반적으로 정의하면 다음과 같다.

a~b사이(b 제외)의 정수 난수를 구할 때 : random(b-a)+a;
a~b까지(b 포함)의 정수 난수를 구할 때 : random(b-a+1)+a;

(4) 난수 사이의 간격은 난수를 구한 후 곱을 사용한다. 이때 범위를 지정하는 인수는 곱해주는 수를 미리 나누어 구해야 한다. 만약 0~100 미만의 짝수 중 하나를 구하고자 한다면 다음과 같이 한다.

random(100/2)*2

100/2는 50이므로 random 함수에 의해 0~49사이의 난수가 생성되며 이 값에 2를 곱하면 0~98로 범위가 확장된다. 정수에 2를 곱했으니 생성된 난수는 모두 짝수일 수밖에 없다. 만약에 홀수를 구하고 싶다면 1을 더하면 될 것이다.

(5) 실수 난수가 필요하면 먼저 충분한 크기의 정수 난수를 구하고 필요한 유효자리수만큼 10의 거듭승으로 나눈다. 소수점 한자리까지 유효한 0.0~9.9사이의 난수는 다음과 같이 생성하면 된다.

random(100)/10.0

random(100)에 의해 0~99까지의 정수가 생성되는데 이 값을 10.0으로 나누면 0.0~9.9까지로 범위가 축소될 것이다. 이때 나누어주는 수는 반드시 실수 상수여야 / 연산자가 실수 나눗셈을 하게 된다. 소수점 두 자리까지 유효한 실수 난수는 0~1000까지 정수 난수를 구한 후 100.0으로 나누면 된다.

(6) 분리된 범위의 난수도 원한다면 생성할 수 있다. 다음 예는 0~4, 14~18 사이의 두 범위에 있는 수 중 하나를 골라준다.

(random(5)+5)*(random(2)==0 ? 1:-1)+9

(random(2)==0 ? 1:-1) 연산문은 앞에서 생성한 난수의 부호를 난수로 선택하는 역할을 한다. random(2)가 0이라는 표현은 1/2의 확률을 의미한다. 사실 이 예는 좀 억지스럽고 실용성이 없지만 이런 식으로도 응용할 수 있다는 것을 보여주기에는 충분하다.

(7) 전혀 연관성이 없는 수들 중 하나를 난수로 선택할 수도 있다. 예를 들어 3, 7, 12, 15 중 하나를 선택하고 싶다고 한다면 다음과 같이 하면 된다.

int i;
do {
 
    i=random(16);
} while (i!=3 && i!=7 && i!=12 && i!=15); 

원하는 난수가 나올 때까지 루프를 계속 돌리기만 하면 된다. 다음 장에서 배울 배열을 사용하면 배열에 원하는 값들을 넣어 놓고 배열의 첨자를 난수로 고를 수도 있다. 이런 식으로 제어구조까지 활용하면 사실 못만들어낼 난수가 없는 셈이다.

난수의 활용 용도는 무궁 무진한데 불규칙한 수의 생성 뿐만 아니라 확률을 제어하고 싶을 때도 사용된다. if (random(n) == 0) { } 조건문은 일정한 확률로 어떤 문장을 실행한다. 예를 들어 if (random(10) == 0) 은 열번에 한 번꼴로, 10%의 확률로 참이 된다. 난수의 범위가 넓을수록 확률은 작아지고 좁을수록 확률이 커진다. 이때 ==0은 별 의미가 없으며 1이나 2와 비교를 해도 똑같은 결과를 얻을 수 있다.


3. 시간 함수

1) time

컴퓨터안에는 시계가 내장되어 있어 항상 정확한 시간을 유지하고 있는데 프로그램에서 시간을 필요로 할 경우 시간 함수로 이 값을 조사할 수 있다. 또한 조사한 시간을 목적에 맞게 조정하거나 변환 및 포맷팅할 수도 있다. 모든 시간 함수의 원형은 time.h 헤더 파일에 선언되어 있으므로 시간 관련 함수를 사용하려면 반드시 time.h를 인클루드해야 한다. 이 책에서 사용하고 있는 Turboc.h가 미리 time.h를 인클루드하고 있으므로 여기서는 그럴 필요가 없지만 실전에서는 그렇지 않음을 명심하자. 시간과 관련된 가장 기본적인 함수는 현재 시간을 구하는 time 함수이다.

time_t time( time_t *timer );
char *ctime( const time_t *timer );

time 함수는 1970년 1월 1일 자정 이후 경과한 초를 조사하는데 리턴 타입인 time_t형은 시스템에 따라 달라지며 윈도우즈에서는 4바이트 정수(typedef long time_t;)로 정의되어 있다. time 함수는 time_t형의 포인터를 인수로 받아 이 인수에 조사된 시간을 채워 주기도 하고 같은 값을 리턴하기도 한다. 둘 중 아무값이나 사용해도 상관없으며 리턴값만 사용할 경우는 인수로 NULL을 전달할 수도 있다. 다음 두 코드는 동일하다.

time_t now
now=time(NULL);

time_t now
time(&now);

이 함수는 최대 2038년 1월 18일까지의 날짜를 표현할 수 있으며 64비트 버전인 _time64 함수는 3000년 12월 31일까지 표현 가능하다. 이 함수가 조사하는 시간은 초단위이기 때문에 이 값으로부터 우리가 일상적으로 사용하는 시간을 바로 구하기는 무척 어렵다. 또한 세계 표준시인 UTC 포맷으로 되어 있어 우리나라 시간과 일치하지도 않는다.

ctime 함수는 time_t형의 경과초를 출력하기 편리한 문자열 형태로 바꾸며 UTC로 된 시간을 지역 설정에 맞게 조정해 주기도 한다. 지역 설정이란 각 국가의 경도에 따른 세계 표준시와의 차이점과 일광 절약 시간(Daylight Saving time;흔히 Summer Time이라고 한다)의 운영 여부 등에 따라 달라지는데 우리나라의 경우 세계 표준시보다 9시간 더 빠르다.

변환된 문자열은 26문자 길이로 되어 있으며 끝에 개행 문자가 있어 printf 등의 함수로 곧바로 출력할 수 있다.

실행 결과는 다음과 같다. 이 실행 결과를 보면 저자가 일요일 새벽에도 잠을 자지 못하고 원고를 열심히 쓰고 있음을 알 수 있다.

현재 시간은 Sun Sep 05 03:35:27 2004
입니다.

 초단위로 된 시간을 문자열 형태로 변환하므로 읽기는 편하지만 영문으로 출력되는데다 개행 코드가 작성되어 있어 다른 문자열 중간에 삽입하려면 개행 코드를 지운 후 사용해야 하는 번거로움이 있다. 다음 두 함수는 날짜와 시간을 문자열 형태로 바로 구하는 좀 더 간단한 함수이다. 

char *_strdate(char *datestr);
char *_strtime(char *timestr); 

_strdate는 날짜를 MM/DD/YY 포맷으로 구해 datestr 버퍼에 복사하며 _strtime은 시간을 HH:MM:SS 포맷으로 구해 timestr 버퍼에 복사하는데 이 함수가 구해주는 시간은 24시간제이다. 두 함수로 전달되는 버퍼는 널 문자까지 고려하여 최소한 9바이트 이상이어야 한다. ctime이 변환 결과를 저장하기 위해 사용하는 버퍼는 라이브러리에서 미리 할당해 놓은 정적 메모리 영역이며 이 영역은 asctime, gmtime, localtime 등의 함수들이 공유한다. 따라서 상기 함수 중 하나를 호출하면 다른 함수가 작성한 문자열은 파괴되므로 변환한 문자열을 계속 사용하려면 사본을 복사해 두어야 한다.

시간 관련 함수들이 버퍼를 공유하는 이런 설계는 이후 멀티 스레드에서 문제거리가 된다. C 라이브러리 함수를 만들 때는 멀티 스레드라는 것이 없었기 때문에 이런 점을 미처 고려하지 못했다.


2) 시간 구조체

time 함수를 사용하면 현재 시간을 쉽게 구할 수 있고 ctime을 사용하면 이 시간을 문자열로도 바꿀 수 있지만 포맷팅을 마음대로 할 수 없어 무척 불편하다. 시간 포맷을 자유롭게 변경하고 싶다면 경과초 형태로 되어 있는 값에서 각각의 시간 요소를 분리해야 한다. 다음 함수들은 time_t형의 값을 tm 구조체로 변환한다. 

struct tm *gmtime(const time_t *timer);
struct tm *localtime(const time_t *timer);
time_t mktime(struct tm *timeptr); 

gmtime, localtime 함수는 둘 다 time_t형의 값을 tm 구조체로 변환하는데 gmtime은 세계 표준시로 변환하며 localtime은 지역시간으로 변환한다. 세계 표준시는 잘 사용되지 않으므로 localtime 함수가 훨씬 더 자주 사용된다. 이 두 함수도 라이브러리에 정적으로 할당되어 있는 tm 구조체를 사용하므로 한 함수가 구해 놓은 정보는 다른 함수를 호출하면 파괴된다. mktime 함수는 반대의 변환을 하는데 tm 구조체를 time_t형으로 바꾼다. tm 구조체는 time.h 헤더 파일에 다음과 같이 선언되어 있다.

struct tm {
 
       int tm_sec;     /* seconds after the minute - [0,59] */
        int tm_min;     /* minutes after the hour - [0,59] */
        int tm_hour;    /* hours since midnight - [0,23] */
        int tm_mday;    /* day of the month - [1,31] */
        int tm_mon;     /* months since January - [0,11] */
        int tm_year;    /* years since 1900 */
        int tm_wday;    /* days since Sunday - [0,6] */
        int tm_yday;    /* days since January 1 - [0,365] */
        int tm_isdst;   /* daylight savings time flag */
}; 

날짜와 시간을 구성하는 여러 가지 멤버들이 포함되어 있으며 주석도 비교적 상세하게 작성되어 있다. 각 멤버의 이름이 무척 쉽게 작성되어 있어 따로 외울 필요까지는 없지만 멤버마다 베이스가 제각각이므로 쓸 때는 조금 주의해야 한다.

멤버

설명

tm_sec

(0~59)

tm_min

(0~59)

tm_hour

시간(0~23)

tm_mday

날짜(1~31)

tm_mon

(0~11)

tm_year

1990 이후 경과 년수

tm_wday

요일(0~6). 0 일요일

tm_yday

년중 날짜(0~365)

tm_isdst

일광 절약 시간과의

 

#include <Turboc.h>

void main()
{
     time_t t;
     tm *pt; 

     time(&t);
     pt=localtime(&t);

     printf("현재 시간 %d년 %d월 %d일 %d시 %d분 %d초입니다.\n",
          pt->tm_year+1900,pt->tm_mon+1,pt->tm_mday,
          pt->tm_hour,pt->tm_min,pt->tm_sec);;
}

현재 시간 2004년 9월 5일 4시 12분 56초입니다.

시간 요소 사이에 한글을 넣을 수도 있고 시간 요소의 출력 순서를 마음대로 조정할 수 있어서 훨씬 더 자유롭고 깔끔한 출력을 할 수 있다. asctime 함수는 tm 구조체를 문자열로 바꾸는데 ctime 함수와 마찬가지로 출력 결과가 영어로 되어 있어 한글 환경에는 실용성이 없고 개행 문자도 포함되어 있다.

char *asctime(const struct tm *timeptr);
size_t strftime(char *strDest, size_t maxsize, const char *format, const struct tm *timeptr); 

strftime 함수는 시간을 다양한 방식으로 포맷팅하는데 첫 번째 인수로 버퍼, 두 번째 인수로 버퍼의 길이, 세 번째 인수로 포맷팅 방식, 네 번째 인수로 tm 구조체를 준다. 세 번째 인수에 포맷팅 서식을 어떻게 지정하는가에 따라 시간을 다양한 형식의 문자열로 바꿀 수 있다.

#include <Turboc.h>

void main()
{

     time_t t;
     char Format[128];

      time(&t);
     strftime(Format,128,"%Y %B %d %A %I:%M:%S %p",localtime(&t));
     puts(Format);
}



3) 기타 시간 함수

시간은 여러 모로 쓸 데가 많은 정보이다. 다음 함수는 프로그램이 실행을 시작한 후의 경과된 시간(Process Time)을 조사한다.

clock_t clock( void );       

clock_t 타입은 long형으로 정의되어 있으며 이 함수가 조사한 값을 CLOCKS_PER_SEC으로 나누면 프로그램 실행 후의 경과 초를 알 수 있다. 이 값은 시스템에 따라 다른데 윈도우즈에서는 1000으로 정의되어 있다.

typedef long clock_t;
#define CLOCKS_PER_SEC  1000 

실행 후의 경과 시간 자체는 별로 쓸 데가 없지만 두 작업 시점간의 시간을 계산하거나 일정한 시간만큼 특정 작업을 계속하고 싶을 때 clock 함수가 조사하는 시간이 기준점으로 사용될 수 있다.

delay도 정확한 시간을 지연시키기는 하지만 기다리는동안 다른 일을 할 수 없다는 점이 다르다. 시스템 속도에 상관없이 일정 시간동안 어떤 작업을 하고 싶다면 clock 함수로 구한 시간을 이용하면 된다. 다음 함수는 두 시간값의 차를 구해준다.

double difftime(time_t timer1, time_t timer0);

실수형을 리턴하는 것으로 되어 있지만 계산 결과가 초단위로 되어 있기 때문에 정밀한 시간 계산이나 코드의 성능 측정 등에 쓰기에는 무리가 있다.

출처: http://winapi.co.kr/

'old > C Grammer' 카테고리의 다른 글

기억 부류  (0) 2010.11.18
함수  (0) 2010.11.17
연산자  (0) 2010.11.16
제어문  (0) 2010.11.15
변수  (0) 2010.11.12
Posted by jazzlife
,

기억 부류

old/C Grammer 2010. 11. 18. 11:31

 

1. 지역 변수


1) 전역 변수와 지역 변수

기억 부류(Storage Class)란 변수가 저장되는 위치에 따라 결정되는 변수의 여러 가지 성질을 의미한다. 변수가 어디에 생성되는가에 따라 통용 범위와 파괴 시기 등의 특징이 결정된다. 이 내용은 C의 문법 체계를 이해하는데 상당히 중요한 비중을 차지하므로 숙독하여 완전히 이해하도록 하자. 기억 부류에는 4가지 종류가 있는데 일단 도표로 특성을 요약하였다.

기억 부류

전역

지역

정적

레지스터

지정자

extern

auto

static

register

저장 장소

정적 데이터 영역

스택

정적 데이터 영역

CPU 레지스터

선언 위치

함수의 외부

함수의 내부

함수의 내부

함수의 내부

통용 범위

프로그램 전체

함수의 내부

함수의 내부

함수의 내부

파괴 시기

프로그램 종료시

함수 종료시

프로그램 종료시

함수 종료시

초기값

0으로 초기화

초기화되지 않음

0으로 초기화

초기화되지 않음



#include <Turboc.h>

void func();

int global;                    // 함수 외부에서 선언되었으므로 전역변수

void main()
{
     int local;                 // main 함수의 지역변수
     global=1;                // 가능
     local=2;                  // 가능
     i=3;                        // 불가능
}

 void func()
{
     int i;                       // func 함수의 지역변수 
     global=1;                // 가능
     local=2;                  // 불가능
     i=3;                             // 가능
}



(1)
변수의 선언 위치가 다르다. 두 부류의 가장 뚜렷한 차이점인데 전역변수는 함수 바깥에서 선언하고 지역변수는 함수 내부에서 선언한다. 위 예제에서 global 변수는 main 함수 이전에 선언되었으므로 전역변수이고 local과 i는 각각 main 함수와 func 함수 내부에서 선언되었으므로 지역변수이다.

(2) 변수의 통용 범위가 다르다. 통용 범위란 변수가 사용될 수 있는 범위를 지칭한다. 전역변수는 특정한 함수 내부에서 선언된 것이 아니므로 함수에 속하지 않고 프로그램 전체가 공유한다. 따라서 변수가 선언된 위치 이후에는 어디서든지 이 변수를 사용할 수 있다. 위 예제에서 보다시피 global은 main 함수나 func 함수에서 자유롭게 읽고 쓸 수 있다.

, 아무리 전역변수라 하더라도 자신이 선언된 이후에만 사용할 수 있다. 만약 위 예제에서 global 변수를 main 다음에 선언한다면 main에서는 global을 참조할 수 없다. 물론 func에서는 참조할 수 있다. C 컴파일러는 1패스 방식으로 동작하기 때문에 변수든 함수든 사용하기 전에 항상 선언을 먼저 해야 한다.

반면 지역변수는 자신이 선언된 함수에 소속되어 있기 때문에 함수 외부에서는 이 변수를 사용할 수 없다. 변수의 값을 읽지도, 쓰지도 못하며 변수의 존재 자체가 알려지지 않기 때문에 이 변수를 들먹거리는 것조차 허용되지 않는다. 지역변수는 함수가 자신의 임무를 수행하기 위해 잠시 쓰고 버리는 것이다.

main에서 자신의 지역변수 local에 값을 대입하거나 func 함수에서 자신의 지역변수 i를 사용하는 것은 가능하다. 그러나 main에서 i 를 참조하거나 func에서 local을 참조하는 것은 불가능하다. main 함수에게 i라는 변수는 없는 것과 마찬가지이다.

(3) 변수의 파괴 시기가 다르다. 변수는 값을 기억하기 위해 메모리를 할당받아 사용한다. 변수를 다 사용했으면 파괴되는데 변수를 파괴한다는 것은 이 변수가 차지하고 있던 메모리를 회수한다는 뜻이다. 시스템의 메모리가 무한하지 않으므로 변수는 필요할 때만 메모리를 차지하며 다 사용하면 다른 변수를 위해 자리를 내 주어야 한다. 변수의 메모리가 회수되면 변수의 존재 자체가 사라진다.

전역변수는 프로그램에 소속되어 있고 모든 함수에서 사용 가능해야 하므로 프로그램이 실행중인 동안에는 파괴되지 않는다. 실행 직후에 생성되어 프로그램이 실행되는 동안에는 계속 메모리를 차지하고 있으며 프로그램이 종료되면 비로소 파괴된다. 즉, 전역변수는 프로그램과 운명을 같이 한다.

지역변수는 특정 함수 내부에서만 사용되므로 함수가 실행중일 때만 메모리를 차지하며 함수가 끝나면 변수의 생명도 끝이 난다. 함수의 임무를 위해 생성되는 임시 기억 장소이기 때문에 함수가 종료되면 더 이상 이 변수를 유지할 필요가 없어진다. 즉, 지역변수는 자신이 속해 있는 함수와 운명을 같이 한다. 함수가 호출되면 생성되고 함수가 끝나면 파괴된다. 그러다가 또 함수가 호출되면 생성되고 파괴되기를 계속 반복한다.

위 예제에서 global은 프로그램 종료시에 파괴되며 local은 main 함수 종료시에, i는 func 함수 종료시에 파괴된다. main의 지역변수는 우연히 전역변수와 생성, 파괴 시기가 거의 동일한데 이는 main 함수 자체가 프로그램 그 자체이기 때문이다.

(4) 변수가 생성되는 기억 장소가 다르다. 전역변수는 한 번 정해진 메모리 위치에 계속 남아 있어야 하므로 정적 데이터 영역에 생성된다. 정적 데이터 영역이란 프로그램의 코드 바로 다음에 위치하는 실행 파일의 한 부분인데 프로그램이 실행될 때 메모리로 로드되어 실행중에 계속 유지된다. 지역변수는 프로그램 실행중에 생성, 파괴를 반복하므로 스택에 생성된다. 스택(Stack)이라는 용어는 조금 어려운 개념인데 일단 데이터의 임시 저장소라고 생각하기 바란다.

프로그램은 실행에 필요한 임시적인 정보들을 스택에 차곡차곡 저장한다. 지역변수, 인수, 함수 실행후 돌아갈 번지 등이 스택에 생성되었다가 사라졌다가를 반복한다. 지역변수는 임시 저장소인 스택에 생성되기 때문에 통용 범위가 함수로 국한되고 함수가 종료되면 같이 사라지는 것이다.

(5) 초기화 여부가 다르다. 전역변수는 별도의 초기식이 없더라도 0으로 초기화된다. 위 예제의 global은 생성되자 마자 0이 될 것이다. 물론 int global=5; 와 같이 별도의 초기식을 주면 이 값으로 초기화된다. 전역변수는 컴파일될 때 컴파일러에 의해 초기화된 채로 실행 파일에 새겨지므로 초기화에 걸리는 시간은 0이다. 전역변수는 프로그램 전체에 걸쳐 사용되는 중요한 변수이므로 초기값을 지정하지 않더라도 안전을 위해 쓰레기값을 치워 0으로 초기화한다.

반면 지역변수는 별도의 초기식이 없을 경우 초기화되지 않는다. 따라서 무슨 값을 가지게 될 지 알 수 없는데 이때 초기화되지 않은 값을 쓰레기값(garbage)이라고 한다. 물론 선언할 때 초기값을 명시적으로 지정하면 초기화할 수도 있다. 초기값이 없을 때 지역변수를 초기화하지 않는 이유는 전역변수와는 달리 함수가 호출될 때마다 변수가 새로 생성되기 때문이다. 매번 변수를 초기화하자면 그만큼 실행 속도가 느려지므로 초기화를 하지 않는다.

지역변수는 함수 내부에서만 사용되므로 설사 지역변수의 쓰레기값이 문제가 된다 하더라도 그 함수만 점검하면 되지만 전역변수가 쓰레기값을 가지게 되면 프로그램 전체에 걸쳐 말썽을 부릴 수도 있다. 그래서 전역변수에 대해서는 초기값을 주지 않아도 쓰레기를 치우지만 지역변수는 그렇게 하지 않는다. 만약 지역변수에 쓰레기값이 들어가는 것이 싫다면 명시적으로 초기화를 해야 한다.


2) 지역 변수의 장점

지역변수는 함수 내부에서만 사용할 수 있고 함수가 끝나면 파괴되는데 비해 전역변수는 모든 함수에서 자유롭게 사용할 수 있고 프로그램이 실행중인 동안은 계속 유지된다. 통용 범위도 넓고 지속 기간도 길기 때문에 모든 면에서 지역변수보다는 사용하기 편리하며 지역변수로 할 수 있는 거의 대부분의 일은 전역변수로도 할 수 있다.

그렇다면 지역변수라는 것은 아예 사용하지 말고 모든 변수를 전역으로 사용하면 될 것이다. 지역변수를 쓰지 않고도 얼마든지 프로그램을 작성할 수 있으며 실제로 지역변수를 지원하지 않는 언어도 있다. 어셈블리에서는 사실 변수라는 개념 자체가 없고 모든 것이 메모리 주소이기 때문에 모든 값은 전역이다(스택에 의도적으로 임시 변수를 생성할 수는 있다). 고전적인 베이직 언어에서도 지역변수라는 것이 없다. 하지만 C나 파스칼, 자바, 비주얼 베이직 같은 근대적인 언어들은 모두 지역변수의 개념을 지원한다.

심지어는 PHP나 ASP, 자바 스크립트 같은 스크립트 언어들까지도 지역변수를 지원한다. 왜 이런 언어들이 지역변수를 지원하는가 하면 프로그램의 구조화에 큰 도움을 주고 유지, 보수를 쉽게 해주는 등의 여러 가지 장점이 있기 때문이다.

장점

(1) 함수의 독립성을 높인다. 프로그램은 함수로 구성되고 함수는 프로그램의 부품이라고 했다. 부품은 불가피한 경우를 제외하고는 가급적이면 스스로 작동할 수 있도록 만들어야 재활용하기 좋다. 부품끼리 공유하는 것(전역변수)이 많아지다 보면 의존 관계를 가지게 되므로 서로 얼키고 설켜서 좋지 않은 구조를 만들어 낸다. 부품은 전체를 구성하는 한 부분으로서 다른 부품과 상관없이 자신에게 부여된 임무를 독립적으로 수행할 수 있어야 한다. 이름만 봐도 무엇을 하는 함수인지 쉽게 짐작할 수 있을 것이며 본체의 코드를 보면 함수의 동작을 한눈에 파악할 수 있을 것이다. 재활용할 함수가 단독 함수라면 그래도 가져갈만 하겠지만 만약 일련의 함수군을 재활용하려면 어떻게 되겠는가? 이 함수군들이 사용하는 모든 전역변수 목록을 조사하고 이름이 충돌하는지 살펴보고 본체의 변수 참조문을 다 바꿔야 한다. 게다가 단순 변수도 아닌 구조체나 사용자 정의 타입이라면 재활용하는 것보다 차라리 다시 작성하는게 더 속편할 것이다.

지역변수는 함수가 자신이 필요로 하는 모든 정보를 다 가질 수 있도록(Self Contained) 해 줌으로써 함수의 독립성을 높여 준다. 그렇다면 이런 식으로 함수를 완전히 독립적으로 작성한다면 함수끼리의 정보 교환은 어떻게 하는가? 부품은 혼자 동작하는 게 아니므로 부품간의 정보 교환은 반드시 필요할 것인데 이럴 경우라도 전역변수는 사용할 필요가 없다. 왜냐하면 이럴 때 쓰라고 만들어 놓은 인수와 리턴값이라는 좋은 장치가 있기 때문이다. 함수끼리 정보를 주고 받기 위해서는 인수와 리턴값을 사용하는 것이 정석이다.

(2) 지역변수는 디버깅 효율을 향상시킨다. 버그, 즉 논리적인 에러가 발생하는 원인의 십중팔구는 변수를 잘못 조작한 것이다. 전역변수가 편하다고 남발하다 보면 디버깅을 할 때 살펴 봐야 할 변수의 수가 많아진다. 프로그램 하나를 만들기 위해서 필요한 변수는 보통 수백개, 많으면 수천개가 되는데 이 변수들이 전부 다 전역이라면 디버깅은 정말 끔찍한 작업이 될 것이다. 더구나 전역변수는 통용 범위가 프로그램 전체이기 때문에 어떤 함수가 이 변수를 잘못 건드렸는지 찾아내기가 아주 어렵다.

그러나 지역변수는 디버깅하기 아주 쉽다. 일단 지역변수를 많이 쓰면 전역변수의 수가 상대적으로 줄어들게 되므로 관찰 대상 변수의 범위가 대폭 좁아진다. 또한 지역변수는 말썽을 부려봐야 자신이 소속된 함수안에서만 유효하므로 그야말로 뛰어봤자 벼룩이고 부처님 손바닥안의 손오공이다. 특정 지역변수가 말썽을 부린다면 그 함수 내부만 정밀하게 점검해 보면 금방 문제점을 발견할 수 있다.

프로그램이 대략 1000줄 정도만 되도 프로그램 개발 속도를 좌지우지하는 중요한 관건은 디버깅 속도이다. 프로그램 개발 기간에 소요되는 시간 중의 70%가 에러를 잡아내는 디버깅이라고 하지 않는가? 대형 프로젝트나 팀 프로젝트에서는 디버깅을 얼마나 빨리 할 수 있는가가 프로젝트의 성공을 결정한다. 디버깅의 간편함이 가지는 의미는 상상외로 크다.

(3) 지역변수는 메모리를 절약한다. 전역변수는 프로그램이 실행될 때 같이 생성되며 계속 값을 유지해야 하므로 그만큼의 메모리를 항상 차지하게 된다. 지역변수는 함수가 호출될 때만 생성되며 함수가 종료되면 즉시 파괴되므로 자신이 속해 있는 함수가 실행중일 때만 메모리를 차지한다. 그래서 전역변수 대신 지역변수를 많이 쓰면 메모리를 절약할 수 있다.

(4) 재귀 호출이나 상호 호출같은 특별한 기법은 지역변수가 있어야만 사용할 수 있다. 이런 기법에 대해서는 다음에 배우게 되겠지만 함수가 호출될 때마다 새로운 변수가 생성되어야만 가능한 기법이다. 재귀 호출이 가능하기 위해서는 각 호출시마다 고유의 값을 유지해야 하는데 전역변수로는 이런 기법을 구사할 수 없다.

 지역변수가 전역변수에 비해 월등히 많은 장점을 가지고 있다. 전역변수는 편리하기는 하지만 복잡한 문제를 일으킬 수 있기 때문에 전역변수의 사용은 가급적이면 자재하는 것이 좋다. 전역변수를 전혀 사용하지 않고도 프로그램을 작성할 수 있다는 것이 이미 수학적으로 증명되어 있으며 전역변수를 병적으로 싫어하는 개발자들도 있다.


3) 외부 변수

[지정자] 타입 변수명;

지정자(Specifier)는 기억 부류를 비롯하여 상수 지정, 최적화 금지 등 변수의 여러 가지 성질을 지정하는 키워드인데 필요없을 경우 생략할 수도 있다. 기억 부류를 지정할 때는 auto, extern, static, register 등의 키워드를 사용하는데 먼저 지역변수를 지정하는 auto 키워드에 대해 알아보자.

auto int i, sum;

변수를 지역변수로 선언할 때는 변수의 타입앞에 auto 키워드를 붙인다. 일반적으로 티폴트값이 auto이기 때문에 우리는 생략해서 사용한다.

- 함수 내부에서 사용되면 지역변수가 된다.
- 함수 외부에서 사용되면 전역변수가 된다.

extern int value;

extern 키워드는 변수가 외부 어딘가에 선언되어 있다는 것을 알리는 역할을 한다.
외부에 선언되어 있는 경우에도 사용하지만 뒷쪽에 선언되어 있는 변수를 사용할 때에도 쓰인다.


2. 정적 변수


1) 정적 변수

정적변수(Static Variable)는 전역변수와 지역변수의 성격을 동시에 가지는 좀 특별한 기억 부류이다. 앞의 도표에 기록되어 있는 정적변수의 특징들을 살펴보자.

선언 위치는 지역변수와 마찬가지로 함수의 선두이다.
통용 범위는 지역변수와 마찬가지로 함수 내부로 국한된다.
저장 장소는 전역변수가 저장되는 정적 데이터 영역이다.
정적 데이터 영역에 저장되므로 프로그램 실행중에 항상 존재한다.
초기값 지정이 없으면 0으로 초기화되고 프로그램 실행시 단 한 번만 초기화된다. 

정적변수의 성질을 요약하자면 저장 장소는 전역변수이되 통용 범위는 지역변수라 할 수 있다. 정적변수를 선언할 때는 반드시 static이라는 지정자를 붙여야 한다.

static int i;
static double d;

이렇게 선언하면 i나 d는 정적변수가 되어 정적 데이터 영역에 저장되며 통용 범위는 선언문이 있는 함수 내부로 국한된다.

정적변수에서 한가지 유의할 점은 이 변수가 언제 초기화되는가 하는 점이다. 함수 선두에서 정적변수를 선언하고 있으므로 함수가 호출될 때마다 초기화될 것 같지만 그렇지 않으며 함수가 최초로 호출될 때 단 한 번만 초기화된다. 사실 초기화라는 말 자체에 이미 일회성의 의미가 내포되어 있지 않은가? 초기화되는 코드가 호출될 때마다 실행된다면 이 변수가 값을 계속 유지하지 못할 것이다.

또 만약 정적변수 선언문에서 초기화를 하지 않으면 전역변수와 마찬가지로 0으로 자동 초기화된다. 함수 내부에서 큰 배열을 선언하고 초기화할 때는 초기화 시간을 절약하기 위해 정적으로 선언하여 한 번만 초기화하도록 해야 한다. 그렇지 않으면 함수가 호출될 때마다 큰 배열이 매번 생성, 초기화, 파괴를 반복하므로 느려진다.

함수 내부에서 선언된 정적변수를 내부 정적변수라고 한다. 정적변수는 그 특성상 특정 함수 전용으로 선언하는 경우가 많기 때문에 보통 정적변수라고 하면 내부 정적변수를 의미한다. 흔하지는 않지만 함수 외부에서도 정적변수를 선언할 수 있는데 이렇게 선언된 변수를 외부 정적변수라고 한다. 외부 정적변수는 특정 함수에 소속되어 있지 않으므로 일반적으로 전역변수와 같은 성질을 가진다.

다만 전역변수와 다른 점은 extern 선언에 의해 외부 모듈로 알려지지 않는다는 점이다. 즉 자신이 선언된 모듈에서만 사용할 수 있는 모듈 전역변수가 된다. 외부에서 extern 선언을 하더라도 이 변수를 참조할 수 없게 된다.

전역변수이면서도 외부에 알려서는 안되는 그런 변수가 필요할 때 외부 정적변수를 사용한다. 주로 모듈의 재활용성을 높이기 위해 사용하는데 구체적인 예를 들어 보자. 그래픽 관련 함수들을 제공하는 graphic.cpp와 graphic.h를 아주 공들여서 제작했는데 이 모듈에서 mode, color 같은 전역변수를 사용하고 있다고 하자. 이 모듈을 쓰고 싶은 사람이 graphic.* 파일만 복사해서 사용하면 되도록 하고 싶다.

그런데 이 모듈을 사용하는 프로젝트에 이미 mode, color라는 전역변수가 사용되고 있다면 명칭의 충돌이 발생하게 될 것이고 양쪽 중 하나는 변수의 이름을 바꾸어야 한다. 이럴 때 mode, color 변수를 외부 정적변수로 선언하면 다른 모듈에는 알려지지 않으므로 이름의 충돌을 방지할 수 있게 되고 이 모듈은 아무 프로젝트에서나 재활용하기 쉬워진다.


2) 레지스터 변수

레지스터형 변수는 앞에서 논한 세 개의 기억 부류와 좀 다른 유별난 점이 있다. 지역, 전역, 정적변수들은 정적 데이터 영역이든 스택이든 어쨌든 메모리의 한 구석에 생성되지만 레지스터형 변수는 메모리가 아닌 CPU의 레지스터에 저장된다. 레지스터(Register)란 CPU를 구성하는 부품 중 하나이며 CPU가 데이터를 처리하기 위해 사용하는 임시 작업장이라고 생각하면 된다.

컴퓨터의 가장 핵심 부품인 CPU의 한 가운데에 있는 기억 장소이기 때문에 레지스터의 속도는 메모리와 비교가 되지 않을 정도로 빠르다. 값을 읽거나 쓰는데 수십억분의 1초 정도밖에 걸리지 않는다. CPU의 종류에 따라 다르지만 레지스터는 보통 10개~20개 정도밖에 없는 아주 귀한 기억 장소인데 여기에 변수를 저장하면 이 변수를 참조하는 문장의 속도가 빨라진다.

레지스터의 크기는 CPU의 비트수를 결정하는 중요한 기준인데 레지스터가 32비트면 32비트 CPU라고 부른다. 386이후부터 최신의 팬티엄 4까지 현재까지 우리가 사용하는 CPU는 대부분 32비트이므로 레지스터들도 전부 32비트이고 따라서 레지스터에 저장할 수 있는 변수의 타입은 int, unsigned, 포인터 형 등의 32비트형뿐이다. double같은 실수형은 저장할 수 없으며 구조체나 배열 따위는 당연히 안된다. 에러는 아니지만 지정해 봐야 무시당한다.

CPU의 레지스터 개수가 많지 않기 때문에 레지스터형 변수는 두 개까지만 선언할 수 있다. 컴파일러나 플랫폼에 따라 레지스터형 변수를 위해 할당하는 레지스터가 다른데 인텔 플랫폼에서는 많이 사용되지 않는 ESI, EDI 레지스터를 사용한다. 만약 세 개 이상의 레지스터형 변수를 선언하면 최초 두 개까지만 레지스터형이 되고 나머지는 지역변수가 된다.

register int r;               // 레지스터형 변수 r선언
register double d;        // 실수는 레지스터형 변수가 될 수 없음. 지역변수로 선언된다.
register a,b,c;             // a,b만 레지스터형 변수가 되고 c는 지역변수가 된다.

레지스터는 한정된 자원이기 때문에 일시적으로 사용할 지역변수에만 지정할 수 있으며 전역변수에는 레지스터 기억 부류를 지정할 수 없다. 프로그램과 생명을 같이 하는 전역변수가 레지스터 하나를 차지한다면 프로그램 실행중인 동안 레지스터 하나가 묶여 버리게 될 것이다. 전역변수에 register 기억 부류를 지정하면 명백한 에러로 처리된다. 지역변수 또는 함수의 형식 인수에 대해서만 이 기억 부류를 사용할 수 있다.

레지스터형 변수를 사용하는 이유는 조금이라도 더 빠른 속도를 얻기 위해서이다. 대규모의 루프를 돌린다거나 할 때 루프 제어 변수를 레지스터형으로 선언하면 이 변수의 읽기, 증감 속도가 빨라지므로 전체 루프의 실행 속도가 빨라질 것이다.

레지스터형 변수는 메모리에 생성되는 것이 아니므로 &연산자는 사용할 수 없다. 레지스터는 CPU 내부에 있기 때문에 번지를 가지지 않으므로 &연산자로 이 변수의 메모리 주소를 조사할 수 없다. 그러나 레지스터형 포인터 변수가 번지를 기억할 수는 있으므로 *연산자를 사용하는 것은 가능하다. C 스팩에는 레지스터형 변수와 &, * 연산자의 관계가 이렇게 규정되어 있으며 상식적으로 이해가 갈 것이다.


3) 정적 함수

기억 부류 중에 함수에 적용되는 것은 정적(static) 기억 부류밖에 없다. 정적 함수는 특정 모듈에서만 사용할 수 있는데 앞에서 살펴본 외부 정적변수의 특성과 유사하다. 함수 정의문 앞에 static이라는 지정자만 붙이면 이 함수는 정적 함수가 된다.

static void func()
{
     ....
}

정적 함수와 반대되는 개념에 대해 별다른 명칭은 없고 굳이 이름을 붙인다면 비정적 함수나 외부 함수 정도가 될 것이다. 외부 함수는 별다른 지정이 없는 한 외부로 항상 알려지며 원형 선언만 하면 어떤 모듈에서나 이 함수를 호출할 수 있다.

그러나 정적 함수는 특정 모듈에서만 사용하도록 정의된 것이므로 외부에서 원형을 선언한다 하더라도 이 함수를 호출할 수 없다. 외부에서 그 존재를 알 수 없도록 해야 하는 이유는 외부 정적변수의 경우와 마찬가지로 이름 충돌을 방지하기 위해서이다. 재사용을 위해 작성한 모듈에서 ReadFile이라는 함수를 사용하는데 이 이름이 너무 일반적이어서 프로젝트내의 다른 함수명과 충돌될 것 같으면 이 함수를 static으로 선언하면 된다.


3. 통용 범위


1) 통용 범위 규칙

변수나 함수, 태그 같은 명칭은 상호 구분되어야 하므로 중복되어서는 안된다. 그래서 같은 이름을 가진 두 개의 변수를 선언할 수 없다. 다음과 같이 작성하면 에러로 처리된다.

void main()
{
     int i;
     double i; 

i라는 명칭으로 정수형 변수와 실수형 변수를 동시에 선언했다. 이유를 설명할 필요도 없이 이 코드는 에러로 처리된다.

'i' : redefinition; different basic types

i라는 명칭이 정수형으로 선언되었다가 실수형이라는 다른 타입으로 중복 선언되었다는 뜻이다. 이름이 중복되면 다음에 i를 참조할 때 정수형 변수 i를 의미하는 것인지 실수형 변수 i를 의미하는 것인지 구분할 수 없는 모호함이 발생할 것이다. 컴퓨터 프로그램의 논리에 모호함이란 절대 있을 수 없으므로 명칭의 중복은 허락되지 않는다.

명칭이 중복되지 말아야 한다는 것은 지극히 상식적이다. 그러나 이 법칙에 예외가 있는데 통용 범위가 다른 명칭끼리는 같은 이름을 가질 수도 있다. 다음 예를 보자.

 void func()
{
     int i;
     .... 

void proc()
{
     double i;
     .... 

func 함수에서는 i를 정수형으로 선언했고 proc 함수에서는 같은 이름의 i를 실수형으로 선었했지만 논리적으로 아무 문제가 없다. 둘 다 지역변수이고 통용 범위가 분명히 다르기 때문에 같은 명칭 i를 참조하더라도 func 함수에서는 정수형으로, proc 함수에서는 실수형으로 구분할 수 있어 모호함이 발생하지 않는다.


2) 블록 범위

앞에서 지역변수란 함수 내부에서 선언된 변수라고 했는데 이는 어디까지나 편의상 이해하기 쉽도록 설명한 것이다. 이제 통용 범위까지 살펴 봤으므로 지역변수를 좀 더 정확하게 정의해 보도록 하자. 지역변수는 { } 괄호안의 블록에 선언된 변수를 의미하며 변수가 선언된 블록 내부에서만 통용된다. { } 괄호안에서만 통용되는 범위를 블록 범위라고 하는데 { } 괄호가 보통 함수의 시작과 끝을 나타내고 함수 선두에 지역변수를 선언하는 경우가 많기 때문에 지역변수의 통용 범위는 함수 내부가 되는 것이다.

지역변수의 통용 범위는 정확하게 함수 내부가 아니고 선언된 블록의 내부이다. 그래서 함수 내부안에 { } 블록을 만들고 이 안에 또 다른 지역변수를 만들 수도 있다.

#include <Turboc.h>

void main()
{
     int i;                           // 함수 범위
     i=5;
     {
          int i;                       // 블록 범위
          i=3;
          printf("i=%d\n",i);
     }
     printf("i=%d\n",i);
}


3) 선언과 정의

이쯤에서 선언과 정의에 대해 구분해 보자. 아주 비슷한 용어인 것 같지만 달라도 한참 다르며 다소 헷갈리는 용어라 정리가 필요하다. 일단 도표로 선언과 정의의 특성을 정리해 보자.

 

역할

메모리

정보의 완전성

중복 가능성

선언

알린다.

사용 안함

불완전해도

가능

정의

생성한다.

할당

항상 완전해야

불가능

선언과 정의의 대상이 되는 것에는 함수, 변수, 타입, 매크로, 태그 등 여러 가지가 있으나 주로 함수와 변수가 주 대상이다.

선언(Declaration) - 컴파일러에게 대상에 대한 정보를 알린다. 함수가 어떤 인수들을 전달받으며 어떤 타입을 리턴하는지를 알리는 원형 선언이 대표적인 선언이다. 컴파일러에게 정보만 제공하는 것이므로 본체를 가지지 않으며 실제 코드를 생성하지도 않는다. 그래서 다음처럼 여러 번 중복되어도 상관없다.

int Max(int a, int b);
int Max(int a, int b);

물론 똑같은 내용을 일부러 이렇게 중복 선언할 경우는 없겠지만 헤더 파일을 여러 번 포함하다 보면 중복 선언될 경우가 있다. 이렇게 본의 아니게 중복 선언되더라도 문제는 없다. 단, 중복 선언할 경우 앞의 선언과 뒤의 선언이 달라서는 안된다. 앞에서는 int Max(...); 로 선언해 놓고 다시 선언할 때는 double Max(...)로 선언할 수는 없다.

정의(Definition) - 대상에 대한 정보로부터 대상을 만든다. int i; 정의문에 의해 4바이트를 할당하며 int Max(int, int) { } 정의로부터 함수의 본체를 컴파일하여 코드를 생성한다. 정의는 변수의 타입, 함수의 인수 목록을 컴파일러에게 알려 주기도 하므로 항상 선언을 겸한다. 그래서 함수를 호출부보다 더 앞쪽에서 정의하면 컴파일러가 이 함수의 본체를 만들면서 모든 정보를 파악할 수 있으므로 별도의 원형 선언을 하지 않아도 된다.

정의는 실제 대상을 만들어 내기 때문에 중복되어서는 안된다. 전체 프로그램을 통해 단 한 번만 나타나야 하며 두 번 이상 중복할 필요도 없다. 만약 정의를 두 번 반복하면 컴파일러는 왜 똑같은 함수를 두 번 정의하느냐는 에러 메시지를 출력할 것이다.

이렇듯 선언과 정의는 분명히 다른 용어이지만 실제로는 명확하게 구분하지 않고 대충 사용하는 경향이 있다. 지역변수의 경우 정의와 선언이 완전히 일치하며 만든 영역에서만 사용하므로 별도의 선언을 할 수도 없고 할 필요도 없다. 그래서 지역변수는 정의만 가능한 대상이지만 일반적으로 "선언한다"라고 하지 "정의한다"라고는 하지 않는다. 전역변수의 경우는 int i;가 정의이고 extern int i;가 선언으로 분명히 구분되지만 관습적으로 전역변수 정의문인 int i; 도 선언문이라고 부른다.

매크로의 경우도 실제 메모리를 할당하는 것은 아니므로 선언이 맞지만 일반적으로 정의라고 표현한다. 매크로를 정의하는 명령어가 #define이지 #declare가 아닌 것만 봐도 그렇다. typedef에 의한 사용자 정의 타입도 키워드에 def라는 단어가 포함되어 있기는 하지만 원칙적으로 선언이라는 용어가 옳다. 그러나 표준 문서에서 조차도 typedef를 사용자 선언 타입이라고는 표현하지 않으며 타입을 정의한다고 표현한다. 구조체나 열거형의 태그는 주로 선언한다고 하지만 가끔 정의한다는 표현을 쓰기도 한다. 이렇듯 두 용어는 분명히 다르지만 실제로는 별 구분없이 사용되는 경향이 있다.


4) 설계 원칙

함수를 작성하는 문법과 호출하는 방법, 인수를 받아들이고 리턴하는 방법을 익히는 것은 그다지 어렵지 않다. 그러나 함수를 정말로 함수답게 잘 나누고 디자인하는 것은 무척 어렵고 단기간에 체득되지 않는다. 함수는 프로그램을 구성하는 단위로서 잘 나누어 놓으면 프로그램의 구조가 탄탄해지고 확장하기도 쉽고 재사용성도 좋아진다. 잘 짜여진 프로그램을 분석해 보면 함수의 분할 구조가 감탄스러울 정도로 잘 되어 있음을 볼 수 있고 그런 함수를 만드는 능력이 부러워지기까지 한다.

그러나 함수를 잘못 디자인하면 코드는 더 커지고 프로그램은 더 느려지며 조금이라도 수정하려면 어디를 건드려야 할지 판단하기 힘든 나쁜 구조가 만들어진다. 함수에 의해 코드는 꼬이기만 하고 엉망이 된 코드 사이로 버그가 창궐할 수 있는 환경만 만들어지니 아예 함수를 만들지 않느니만도 못한 상태가 되기도 한다.

프로그래밍에 처음 입문한 사람들에게 함수 디자인이라는 주제는 아주 어렵고 힘든 고비이다. 책을 읽어서 비법을 얻는 것은 불가능하고 잘 하는 사람에게 개인 지도를 받아도 어렵고 혼자서 연습해 보기는 더욱 더 어렵다. 함수 디자인은 오로지 많은 분석과 실습만으로 얻어지는 경험이다. 그래서 꾸준한 연습만이 해결책이다. 다음은 함수를 잘 만드는 기본적인 지침들이다.

(1) 함수의 이름을 최대한 설명적으로 작성하여 이름만으로 무엇을 하는 함수인지, 이왕이면 어떻게 쓰는 것인지도 알 수 있도록 한다. 마치 함수의 이름이 주석인 것처럼 해야 한다. 함수는 이름으로 호출되므로 좋은 이름을 붙여 두면 함수를 관리하는 사람과 쓰는 사람 모두가 편해진다. 아주 간단한 규칙인 것 같지만 함수를 설계하는 첫 번째 원칙이 될 정도로 좋은 이름을 붙이는 것은 중요하다. 특히 팀 작업을 하거나 오랫동안 관리해야 할 코드라면 더욱 더 정성스럽게 이름을 붙여야 한다.

함수명은 보통 동사와 목적어 그리고 약간의 수식어로 구성된다. GetScore, DrawScreen, TestGameEnd 등은 이름만으로 어떤 동작을 하는지 쉽게 알 수 있으므로 좋은 함수명이다. 동작이 좀 더 구체적이라면 GetHighestScore, GetAverageScore 등의 수식어를 붙이는 것도 좋다. 이런 이름은 무엇을 어떻게 하는지를 분명히 표현한다.

반면 Score, Draw, Test 따위는 점수나 그리기와 상관이 있는 동작을 하는 것 같아 보이기는 하지만 구체적으로 무엇을 어떻게 하는지를 표현하지 못하므로 좋지 않다. 함수를 만든 사람은 당장은 이 함수들을 이해할 수 있다 하더라도 조금만 시간이 지나면 함수의 본체를 다 읽어 봐야 무엇을 하는 함수인지 알 수 있으므로 코드를 유지 및 확장하기 어려워진다. 함수에 좋은 이름을 붙이는 것은 어려운 기술이 아니라 조금의 관심만 기울이면 누구나 할 수 있는 기술이며 이름을 구성하는 적절한 영어 단어만 잘 선정하여 조립하면 된다.

(2) 두 번 이상 중복된 코드는 반드시 함수로 분리한다. 똑같은 코드를 중복된 채로 내버려 두면 프로그램의 크기가 쓸데없이 커진다. 10줄짜리 코드를 10번 반복한다면 나머지 90줄은 불필요하게 용량만 차지하는 것이다. 용량의 문제보다 더 심각한 것은 코드를 유지, 확장하기가 아주 곤란해진다는 점이다. 요구가 바뀌어 해당 동작을 수정해야 한다고 해 보자. 이 동작을 함수로 분리해 두었으면 함수만 고치면 되지만 중복되어 있다면 일일이 찾아가서 고쳐야 한다. 실수로 한 곳을 고치지 않으면 이것이 바로 버그의 원흉이 된다.

중복되는 회수에 상관없이 앞에서 이미 만들었던 코드와 비슷한 코드를 또 작성해야 한다면 일단 그 부분을 함수로 만들고 기존 코드를 함수 호출로 수정해야 한다. 즉 다음과 같이 구조를 만든다.

아니! 고작 두 번 중복되었을 뿐인데 이런 것들도 함수로 분리해야 한단 말인가 하는 생각이 들지도 모르겠다. 그 대답은 당연히 그렇다이다. 한 번 중복된 코드는 조만간 다시 필요해질 가능성이 아주 높다. 뿐만 아니라 십중팔구 그 코드는 잠시 후 확장되어야 한다. 그래서 중복이 발견되는 즉시, 그것이 단 두 군데 뿐이더라도 무조건 함수로 분리하는 습관을 가져야 한다. 네 번, 다섯 번 중복될 때 분리하겠다고 생각한다면 이미 프로그램은 엉망이 되어 가고 있는 것이다.

(3) 반복되지 않더라도 한 단위로 볼 수 있는 작업은 함수로 만든다. 설사 이 함수를 딱 한 번만 호출하고 다른 곳에서 호출할 확률이 아주 희박하더라도 이렇게 하는 것이 좋다. 예를 들어 어떤 구조체나 배열을 초기화하는 코드 덩어리는 Init~ 라는 이름으로 분리하고 화면을 출력하는 코드 덩어리는 Draw~ 따위의 이름을 주어 분리한다.

함 함수의 소스가 아주 길어져서 수백줄이 되면 그 많은 코드들의 어떤 부분이 어떤 작업을 하는지 얼른 파악되지 않는다. 게다가 다른 일을 하는 코드들이 한 곳에 섞여 있으면 필시 꼬이게 마련이며 이런 복잡한 코드는 대체로 메인 코드인 경우가 많다. 이 코드들의 그룹을 나누어 함수로 분리해 두면 메인 코드는 이 함수들을 조립하는 수준으로 간단해진다.

이렇게 분리되면 메인 코드를 읽기 쉬워지고 이미 완성된 코드들은 더 이상 신경쓰지 않아도 되는 이점이 있다. 또한 이 함수들이 현재 프로젝트에서는 반복되지 않더라도 다른 프로젝트에서는 재사용되기 쉽다.

(4) 함수는 한 번에 하나의 작업만 해야 한다. 함수는 프로그램을 구성하는 부품이며 부품이란 전체를 구성하는 원자적인 단위이다. 물론 한 함수가 두 가지 일을 동시에 수행할 수도 있고 그렇게 하는 것이 더 효율적일 때도 있다. 그러나 이 함수를 나누어 더 작은 부품을 만들어 놓으면 두 일을 각각 따로 실행해야 할 필요가 있을 때 작은 부품을 활용할 수 있다. 만약 꼭 여러 가지 일을 한꺼번에 해야 하는 함수가 필요하다면 각각의 함수를 만든 후 이 함수들을 호출하는 함수를 하나 더 만들면 된다.

이렇게 되면 이 함수를 호출하는 기존의 코드는 영향을 받지 않으면서 새로운 작은 단위의 작업을 호출할 수도 있게 된다.

(5) 입력과 출력이 직관적이고 명확해야 한다. 인수는 함수에게 주어지는 작업거리인데 함수가 하는 일에 꼭 필요한 정보만 최소한의 인수로 받아들여야 한다. 나머지 인수로부터 알 수 있는 값이나 연산에 사용하지 않는 불필요한 정보는 전달될 필요가 없다. 예를 들어 화면의 특정 위치에 메시지를 출력하는 OutMessage 함수를 작성한다고 해 보자. 

void OutMessage(int x, int y, char *str, int len)

이 함수에서 메시지의 길이 len은 불필요한 인수이다. 세 번째 인수 str이 널 종료 문자열이라면 str로부터 길이를 계산할 수 있다. 물론 메시지의 일부만을 출력하는 기능이 있다면 이럴 때는 len이 필요할 것이다. 함수의 작업 결과는 가급적이면 리턴값으로 보고해야 하는데 설사 그 값을 호출원에서 사용하지 않는다 하더라도 일단 보고할 내용이 있다면 보고하는 것이 좋다.

(6) 함수는 자체적으로 에러 처리를 해야 한다. 함수는 독립된 작업을 하며 재사용 가능한 부품이므로 그 자체로서 완벽하게 동작할 수 있어야 한다. 현재 작성중인 프로젝트에서 잘 실행된다고 해서 이 함수가 안전하고 완벽하게 동작한다는 것을 보장할 수는 없다. 특히 입력된 인수의 유효성을 잘 점검해야 한다. 포인터의 경우 NULL이 전달될 수도 있고 정수가 터무니없이 크다거나 음수가 될 수 없는 값에 대해 음수가 전달되는 경우에도 에러 처리를 해야 한다. 그래야 어떤 프로젝트로 가져 가든 별도의 수정없이 재사용 가능한 부품이 된다.

여기서 논한 함수 설계에 대한 지침은 어디까지나 일반적인 참고사항일 뿐이다. 특수한 실무 환경에서는 이 지침과는 다르게 함수를 만들어야 하는 불가피한 경우도 존재한다. 이 지침이 함수 설계를 잘 하고 싶은 사람들에게 아주 중요한 내용인 것은 사실이지만 이 지침들을 몽땅 외운다고 해서 함수 설계 경험이 금방 늘어나는 것은 아니다. 마치 "여성의 마음을 사로잡는 100가지 비법"을 달달 외운다고 해서 사교계의 달인이 될 수 없는 것과 같다. 일단 이 권고안을 머리속에 새겨 두고 끊임없이 분석, 연습해 봐야 한다.

출처: http://winapi.co.kr/

'old > C Grammer' 카테고리의 다른 글

표준 함수  (0) 2010.11.19
함수  (0) 2010.11.17
연산자  (0) 2010.11.16
제어문  (0) 2010.11.15
변수  (0) 2010.11.12
Posted by jazzlife
,

함수

old/C Grammer 2010. 11. 17. 15:57

1. 함수의 구성 원리

1) 함수

사용자 정의 함수를 만드는 기본 형식은 다음과 같다.

 type name(인수 목록)
{
          함수의 본체
}

name : 함수의 이름이며 이 이름을 통해 함수를 호출한다. 함수의 이름도 명칭(Identifier)이므로 명칭을 만드는 규칙대로 기억하기 쉽고 의미를 잘 표현할 수 있는 이름을 주는 것이 좋다. 점수를 출력하는 함수라면 PrintScore, 게임을 끝내는 함수라면 EndGame, 합계를 구하는 함수라면 GetSum 같은 이름을 붙이면 된다.

인수 목록 : 함수가 해야 할 일의 세부 사항을 지정하며 함수의 작업거리라고 할 수 있다. 함수는 고유의 기능을 가지고 있고 호출부에서는 이 기능을 사용하기 위해 함수를 호출하는데 이때 함수에게 일을 시키기 위해서는 작업에 필요한 값을 전달해야 한다. 함수의 동작에 필요한 인수는 없을 수도 있고 여러 개일 수도 있는데 인수 목록에 필요한 인수의 타입과 이름을 밝힌다.

예를 들어 점수를 화면으로 출력하는 PrintScore 함수의 경우 출력할 현재 점수가 몇점인가를 가르쳐 주어야 하며 이런 정보가 인수로 전달된다. 만약 점수값 하나만 인수로 전달받는다면 PrintScore(int Score) 식으로 점수를 전달받을 것이다. printf는 서식 문자열과 출력할 값을 인수로 전달받으며 gotoxy는 이동 좌표를, delay는 지연시간을 전달받는다. 인수는 필요한만큼 사용할 수 있으며 개수의 제한은 없다. 필요하다면 점수를 출력할 좌표나 점수의 출력 형태 등도 인수로 전달받을 수 있다.

type : 함수가 리턴하는 값의 데이터형이며 함수의 작업 결과라고 할 수 있다. 함수는 고유의 작업을 실행하고 그 결과를 호출원에게 다시 돌려 준다. 예를 들어 합계를 구하는 함수 GetSum은 자신이 구한 합계를 호출원에게 보고하는데 이때 돌려주는 값의 타입이 바로 함수의 타입이다. 정수형 값을 리턴한다면 int, 실수형 값을 리턴한다면 double이라고 타입을 써 준다. getch 함수는 입력된 문자값을 리턴하며 wherex, wherey는 커서 좌표를 조사한다. 단순히 어떤 기능만 수행하는 함수라면 리턴하는 값이 없을 수도 있는데 이런 함수를 void 함수라고 한다.

본체 : { } 괄호안에 실제 함수의 코드가 위치한다. 이 블록 안에 함수의 고유 기능을 수행하는 코드를 작성하면 된다. PrintScore 함수의 본체에는 인수로 전달된 점수값을 printf 함수로 출력하는 코드가 작성될 것이다.

2) 인수

인수(Parameter)는 호출원에서 함수에게 넘겨주는 작업 대상이라고 할 수 있다. 두 함수 사이의 정보 교환에 사용되므로 매개 변수(Argument)라고도 한다. Max 함수의 경우 두 정수값 중 큰 값을 선택하는 기능을 하므로 대상이 되는 두 정수값을 전달받아야 하며 그래서 int a, int b를 인수 목록에 적어 주었다. 다음은 여러 가지 함수의 인수 예이다.

(x,y) 위치에 점수 Score를 출력하는 함수 : PrintScore(int x, int y, int Score)
실수 x의 제곱근을 구하는 함수 : GetSqrt(double x)
문자열에서 공백의 개수를 조사하는 함수 : GetSpaceNum(char *str)

작업 대상을 따로 전달받을 필요가 없다면 인수를 전혀 사용하지 않을 수도 있다. 예를 들어 화면을 지우는 clrscr 함수는 어떤 동작을 할 것인지 이미 정의되어 있으며 화면을 지우는데 별다른 지시 사항이 없으므로 별도의 인수를 전달받지 않는다. 인수가 없는 함수는 인수 목록을 비워 두거나 아니면 인수를 받지 않는다는 것을 분명히 표시하기 위해 인수 목록에 void라고 적는다.

 clrscr();
clrscr(void);

 인수는 형식인수와 실인수로 구분되는데 함수의 인수 목록에 나타나는 인수를 형식 인수라고 하며 함수 호출부에서 함수와 함께 전달되는 인수를 실인수라고 한다.

형식 인수는 호출원에서 전달한 실인수값을 잠시 저장하기 위한 임시 저장 장소이므로 어떤 이름이든지 사용해도 상관없다. 자신에게 전달된 인수값을 형식 인수에 잠시 대입해 놓고 함수 본체에서는 형식 인수로 호출원으로부터 전달된 실인수값을 읽기만 하면 된다. Max 함수는 다음과 같이 작성해도 완전히 동일하다.

int Max(int num1, int num2)
{
     if (num1 > num2) {
          return num1;
     } else {
          return num2;
     }
}
 
호출원에서 전달받은 값을 이 함수내에서 num1, num2로 부르겠다는 뜻이다. Max 함수가 이렇게 정의되어 있고 main 에서 Max(a,b)를 호출했다면 형식 인수 num1이 실인수 a의 값을 가지게 되고 형식인수 num2는 실인수 b의 값을 가지게 될 것이다. 인수 전달은 일종의 대입 연산이며 함수 호출 과정에서 a, b의 값이 num1, num2로 대입된다. Max 함수는 전달받은 형식인수값의 대소를 판단하여 더 큰 수를 리턴하기만 하면 된다. 다음 예제는 두 정수의 값을 더하는 함수 Add를 정의한 것이다.

3) return

인수가 호출원으로부터 전달되는 작업 대상이라면 리턴값은 함수가 호출원으로 돌려주는 작업 결과이다. 앞에서 이미 봤지만 함수가 결과를 리턴할 때는 return 문을 사용한다. return문의 기능은 다음 두 가지이다.

우선 가장 일반적인 기능은 함수의 결과값을 호출원으로 돌려주는 것이다. return 예약어 다음에 리턴하고자 하는 값을 써 준다. Max 함수의 경우 a, b값의 대소에 따라 return a나 return b 명령으로 a나 b의 값 중 하나를 리턴하며 Add 함수는 ttt와 ddd를 더한 결과값을 리턴한다.

함수는 자신에게 맡겨진 임무대로 계산을 수행하여 그 결과를 호출원으로 리턴하며 호출원에서는 함수가 리턴한 값을 함수의 결과값으로 취한다. 함수가 어떤 값을 리턴할 것인가는 함수의 타입으로 이미 정의되어 있으므로 미리 정해진 타입의 값을 리턴하면 된다. Max와 Add는 모두 정수형 값을 리턴하도록 정의되었다.

호출원에서는 타입만 맞다면 함수가 리턴하는 값을 곧바로 사용할 수 있다. 즉, int 타입이 올 수 있는 곳이라면 int 타입을 리턴하는 함수도 항상 올 수 있다. Max 함수는 정수값을 리턴하므로 m=Max(a,b) 구문으로 Max 함수가 리턴하는 값을 정수형 변수 m에 대입할 수 있으며 Add 함수가 리턴하는 정수값을 printf의 %d 서식과 대응시킬 수도 있다.

함수는 단 하나의 유일한 결과를 리턴하기 때문에 타입만 맞다면 수식내에서도 바로 사용할 수 있다. d=Add(Max(a,b),c) 식은 a와 b중 큰 값과 c를 더한 값을 d에 대입한다. Max가 리턴하는 값이 정수형이므로 이 함수의 호출 결과를 Add 함수의 첫 번째 인수로 사용할 수 있고 Add가 더한 값을 정수형 변수 d에 대입할 수 있다. 함수는 한 번에 하나의 리턴값만 돌려줄 수 있기 때문에 타입만 맞다면 수식내에서 함수를 바로 쓸 수 있는 것이다. 다음 함수 호출도 모두 적법하다.

gotoxy(Max(a,b),Add(c,d));
Add(Add(Add(Add(1,2),3),4),5);

두 번째로 return 문은 결과를 돌려주는 것 외에 함수를 강제 종료시키는 기능을 한다. 함수 실행중에 return문을 만나면 함수의 뒷부분이 남아있건 말건 무조건 함수를 종료하고 호출원으로 돌아가 버린다. return문이 함수의 결과값을 돌려주는 명령인데 결과값을 돌려 주는 시점이 함수의 임무가 끝난 시점이므로 더 이상 함수의 나머지 부분을 실행할 필요가 없다.

4) void형

함수는 작업한 결과를 리턴값으로 돌려줄 수 있는데 모든 함수가 리턴값을 가지는 것은 아니다. 리턴할 값이 없는 함수도 있는데 이런 함수를 void형 함수라 한다. 정수형 값을 리턴하면 정수형 함수, 실수형 값을 리턴하면 실수형 함수 등으로 함수가 리턴하는 값의 데이터형이 곧 함수의 타입(type)이 되는데 void형 함수는 아무런 값도 리턴하지 않는 함수다.

단순히 "삐" 하는 효과음을 낸다든가, 미리 정해진 메시지를 출력한다든가 하는 함수들은 특별히 호출원으로 돌려줄 값이 없다. 이런 함수를 정의할 때는 함수 타입에 void라고 적는다. void형 함수는 내부적으로 작업만 할 뿐이지 계산 결과를 리턴하지 않으므로 호출원에서는 함수를 호출만 하며 리턴값을 대입받거나 사용하지 말아야 한다.

5) 함수의 다른 이름

함수(function) : 특정 계산을 수행하며 리턴값이 있다. 반드시 수식내에서만 사용할 수 있으며 함수 단독으로 문장을 구성할 수 없다. 이 경우는 수학적 의미의 함수와 거의 유사하므로 적합한 용어 사용예라 할 수 있다.

프로시저(procedure) : 특정 작업을 수행하며 리턴값이 없다. 리턴값이 없기 때문에 수식내에서는 사용할 수 없으며 단독으로 문장을 구성할 수는 있다. C의 void 함수가 이에 해당한다.

 C의 함수는 파스칼의 함수와 프로시저에 해당하는 특성을 모두 가진다. 리턴값이 있을 수도 있고 없을 수도 있으며 모든 함수는 단독으로 사용할 수 있다. 파스칼은 형태에 따라 함수와 프로시저를 엄격하게 구분하여 실수를 방지하지만 C언어는 특별한 구분이 없으므로 융통성이 있다. 다음은 다른 언어와 구분되는 C 함수의 특징들이다.

함수끼리는 서로 평등한 관계에 있으며 상호 수평적이다. 즉, 함수끼리 언제나 호출 가능하다는 뜻이며 한 함수가 다른 함수에 예속되지 않는다. 반면 파스칼은 함수 내부에 지역 함수를 정의할 수 있어 함수끼리 수직적인 계층을 이룰 수 있다.

함수 중에서 가장 기본이 되는 함수를 main이라고 하며 프로그램의 시작점이 된다. 그러나 main이 다른 함수들과 특별히 다르지는 않으며 다만 프로그램의 실행 시작점이라는 것만 조금 특수할 뿐이다.

리턴값은 있을 수도 있고 없을 수도 있다. 리턴값이 있는 함수는 리턴 타입을 가지며 그렇지 않은 함수는 void형으로 선언하면 된다.

항상 단독으로 문장을 구성할 수 있다. 리턴값이 없는 함수는 단독으로만 사용할 수 있고 리턴값이 있는 함수는 수식 내에서 쓸 수도 있고 단독으로 쓸 수도 있다. 값을 리턴하는 함수라고 해서 반드시 리턴 값을 대입받거나 수식내에서만 써야 하는 것은 아니며 리턴값을 버리는 것이 가능하다. printf함수도 출력한 문자의 개수를 리턴하며 에러 발생시 음수를 리턴하는데 에러가 발생하는 경우가 거의 없고 출력 길이에도 특별한 관심이 없으므로 보통 단독으로 사용된다.

값에 의한 호출 방식을 사용한다. 실인수가 형식인수에 대입될 때 항상 값이 대입된다는 뜻이다. 호출 방식에 대해서는 잠시 후 따로 연구해 볼 것이다.


2. 헤더 파일

1) 함수의 원형

함수의 원형을 이해하기 위해서는 C 컴파일러의 컴파일 방식에 대해 알아야 한다. 프로그래밍 언어는 해석 방식에 따라 인터프리터 방식과 컴파일 방식으로 나누어지는데 컴파일 방식이 훨씬 더 성능이 좋기 때문에 대부분의 언어가 컴파일 방식을 사용한다. C언어도 물론 컴파일 방식을 사용한다.

컴파일 방식은 소스를 읽어 기계어로 한꺼번에 번역하는 방식인데 번역을 몇 번에 나누어 하느냐에 따라 1패스, 2패스 등으로 구분된다. 한 번 죽 읽어서 번역을 다 해 내면 1패스 (one pass)방식이라고 하며 한 번 읽어서 컴파일 준비를 한 후 다시 읽으면서 기계어 코드로 바꾸는 방식을 2패스 방식이라고 한다. 2패스 방식의 대표적인 예가 어셈블러인데 소스상의 위치를 나타내는 레이블을 실제 번지로 바꾸기 위해 처음부터 끝까지 소스를 읽어 레이블의 번지를 미리 파악한 후 다시 처음부터 읽으면서 기계어 코드로 번역한다. 어셈블러는 이런 식으로 소스를 두 번 읽지 않으면 컴파일이 불가능하다.

3패스 방식을 채택하는 언어도 있고 디스 어셈블러들은 5패스 방식까지도 사용한다. 언어가 복잡해질수록 패스 수가 늘어나며 사용하는 메모리는 많아지고 컴파일 속도는 떨어진다. 초기의 C 컴파일러들은 컴파일 속도를 높이기 위해 통상 1패스 방식을 많이 채택했었다. C는 다른 언어보다 문법 구조가 복잡해서 컴파일 속도가 느린 편이라 패스를 여러 번 할 수가 없었다. 그래서 C 소스는 컴파일러에 의해 읽히는 족족 기계어로 번역된다.

물론 최근의 C컴파일러들은 1패스가 아닌 것들도 있지만 초기의 컴파일러들이 1패스 방식으로 작성되었기 때문에 C표준은 이런 컴파일러를 위해 한 번에 소스를 읽을 수 있는 장치를 마련할 필요가 있었다. 이러한 C의 1패스 방식 때문에 함수의 원형이라는 것이 필요하다. 원형(ProtoType)이란 함수에 대한 정확한 정보라는 뜻이며 리턴 타입, 함수 이름, 인수 리스트 등의 정보들로 구성된다.

원칙적으로 함수는 사용되기 전에 미리 그 형태를 컴파일러가 알 수 있도록 해야 하는데 그 방법이 바로 원형(ProtoType)을 선언하는 것이다. 함수의 원형을 미리 선언해 두면 본체는 뒤쪽에 있더라도 함수 호출부에서 이 명칭이 함수이고 어떤 인수를 요구한다는 것을 미리 알 수 있게 된다. 이렇게 함수의 정보를 미리 컴파일러에게 알려 주는 것을 "원형을 선언한다"라고 한다.

int Max(int a, int b);               // 원형 선언

void main() {

2) 원형의 형식

함수의 원형은 컴파일러에게 함수에 대한 정보를 제공하기 위해 작성한다. 그래서 함수의 본체는 적지 않으며 리턴 타입, 함수 이름, 인수 목록만 적는다. 함수를 정의하는 형식에서 본체를 빼고 뒤에 세미콜론을 붙이면 이것이 함수의 원형이다. 원형 선언 자체도 하나의 문장이므로 세미콜론은 반드시 있어야 한다.

함수를 정의하는 형식에서 함수 이름이 포함된 첫 번째 줄만 옮겨 적고 뒤에 세미콜론을 붙이면 이것이 함수의 완전한 원형이다. Max 함수의 완전한 원형은 int Max(int a, int b);가 된다. 이때 함수의 원형에 적는 형식 인수의 이름은 사실 아무 의미가 없다. int Max(int kkk, int mmm); 이라고 써도 결과는 완전히 동일하다. 왜냐하면 형식인수란 함수의 본체내에서 호출원으로부터 전달된 값을 참조하기 위해 사용하는 것인데 원형 선언은 본체를 가지지 않으며 형식 인수를 사용하지 않기 때문이다.

그러나 인수들의 타입은 아주 중요한 의미가 있다. int Max(int a, int b); 원형은 이 함수의 이름이 Max이고 정수형 값을 리턴한다는 것 외에 두 개의 정수를 인수로 취한다는 정보를 담고 있다. 컴파일러는 이 원형을 미리 기억하고 있다가 혹시 사용자가 Max(1, 3.14) 같이 실수를 전달하면 소수점 이하가 잘린다는 경고를 출력하거나 Max(2,"name") 같이 변환할 수 없는 인수일 경우는 에러로 처리한다. 즉, 원형 선언이란 컴파일러에게 "혹시 내가 실수를 하면 좀 알려 줘"하고 요청해 놓는 것이다.

함수의 원형에서 형식 인수의 이름은 의미가 없기 때문에 형식 인수를 생략하는 간략한 원형 선언 방식도 허용된다. 인수 리스트에서 형식 인수 이름은 빼 버리고 인수의 타입만 적는 방식인데 Max 함수의 간략한 원형은 int Max(int,int);가 된다. 이 원형만으로도 컴파일러는 Max 함수에 대한 모든 정보를 다 알 수 있다.

이 외에 구형 C 컴파일러는 인수 리스트를 생략하는 int Max(); 같이 더 간략화된 형식도 지원했었으나 요즘의 C++ 컴파일러들은 인수 리스트의 생략은 허용하지 않는다. 다음에 배우게 되겠지만 C++은 인수 목록이 다르면 다른 함수로 취급하는 다형성을 지원하기 때문이다. 그래서 함수의 원형은 다음 두 가지 형식으로 쓸 수 있다.

int Max(int a, int b);          // 완전한 원형 - 형식 인수명도 적어준다.
int Max(int, int);               // 간략한 원형 - 인수의 타입만 밝힌다.

어떤 방식으로 원형을 선언하는가에 따른 차이점은 거의 없다. 어차피 컴파일러는 형식 인수의 이름 따위에는 전혀 관심이 없기 때문이다. 그래서 메모리나 하드 디스크값이 비쌌던 과거에는 간략한 형식의 원형을 많이 사용했었고 요즘도 이런 형식을 즐겨 쓰는 사람들이 많다. 의미도 없는 형식 인수명을 일일이 타이프하는 것도 꽤 성가신 일이기 때문이다.

하지만 최근에는 가급적이면 함수의 원형을 완전하게 적는 것이 더 권장된다. 형식 인수의 이름이 컴파일러에게는 아무 도움이 되지 않지만 함수를 사용하는 사람에게는 도움이 되기 때문이다. 예를 들어 화면에 원을 그리는 DrawCircle이라는 함수가 있다고 하자. 아마 이 함수의 원형은 다음과 같이 작성될 것이다.

void DrawCircle(int x, int y, int radius);

별다른 설명이 없더라도 이 원형으로부터 첫 번째, 두 번째 인수가 원의 중심점이고 세 번째 인수가 반지름이라는 것을 직관적으로 알 수 있다. 만약 간략한 형태로 void DrawCircle(int, int, int); 와 같이 써 버리면 이 함수를 사용하는 사람은 함수의 본체를 보거나 레퍼런스를 참조해야만 인수들의 정확한 의미를 알 수 있을 것이다. 원형에 포함된 형식 인수의 이름은 주석보다 더 좋은 참고 정보가 되므로 가급적이면 인수의 의미를 정확하게 전달할 수 있는 이름을 적어 놓는 것이 좋다.

표준 함수의 원형은 사용자에게 가급적이면 많은 정보를 제공하기 위해 모두 완전한 형태로 작성되어 있는 경우가 많다. 컴파일러에 따라서는 간략한 원형을 사용하는 것도 있다. 사용자 정의 함수들도 특별한 이유가 없는 한 완전한 형태로 적는 것이 좋다.

3) 헤더 파일

표준 함수의 원형을 미리 작성해 놓은 것을 헤더 파일(Header File)이라고 하며 stdio.h가 대표적인 헤더 파일이다. #include문은 헤더 파일을 소스 안으로 읽어 들이는 역할을 하므로 #include <stdio.h>명령에 의해 stdio.h에 선언된 모든 표준 함수의 원형을 공짜로 선언할 수 있게 된다.

4) 모듈

각각의 기능을 구현하는 함수 집합에 대해 모듈을 따로 만들 수 있으며 각 모듈은 개별적으로 컴파일되어 링커에 의해 하나의 실행 파일로 합쳐진다.

모듈 분할 컴파일 방식의 장점
 (1) 컴파일 속도가 빠르다. 모듈을 작게 나눌 수록 컴파일 속도는 빨라진다.
 (2) 분담 작업이 가능하다. 각 기능별로 분담이 가능하다.
 (3) 프로젝트 관리가 쉽다. 소스 관리가 쉽다.
 (4) 모듈을 재사용할 수 있다. 다른 프로젝트에 공유가 가능하다.

5) 모듈 예제

#include <Turboc.h>

void BoxMessage(char *str);

void main()
{
     BoxMessage("박스를 그리고 그 안에 문자열을 출력한다.");
     BoxMessage("전달된 문자열의 길이에 적당한 박스를 스스로 계산한다.");
     BoxMessage("신기하군");
} 

void BoxMessage(char *str)
{
     int i;
     int len;   
   
   
 len=strlen(str);
     
 puts("");

     for (i=0;i<len+4;i++) {
          putch('-');
     }
     puts("");

    
printf("| %s |\n",str);
     for (i=0;i<len+4;i++) {
          putch('-');
     }
     puts("");
}



3. 함수 호출 방식

인수란 호출원에서 함수에게 일을 시키기 위한 정보인데 인수를 어떻게 전달하는가에 따라 값 호출(call by value) 방식과 참조 호출(call by reference) 방식이 있다. 인수를 넘기는 방식에 따라 실인수의 값이 변경되는가 아닌가의 차이점이 있다. 다소 어려울 수도 있지만 함수를 이해하는데 아주 중요한 내용이므로 잘 알아 두도록 하자.

1) 값 호출 (call by value)

먼저 값 호출에 대해 알아보자. 값 호출 방식이란 실인수의 값이 형식 인수로 전달되는 방식이다. 다음 예제의 plusone 함수는 하나의 정수값을 전달받아 그 값에 1을 더한 값을 리턴한다.

j=plusone(i);

int plusone(int a)
{
     a=a+1;
     return a;
}

값을 복사해서 넘긴다라고 생각하면 쉽겠다.


2) 참조 호출 (call by reference)

값 호출과 참조 호출의 또 다른 차이점은 실인수로 상수를 전달할 수 있는가 하는 점이다. 값 호출 방식은 값을 전달하기 때문에 plusone(5)와 같이 상수를 실인수로 사용할 수 있다. i든 k든 5나 3이든 값을 가지기만 하면 형식인수 a가 이 값을 대입받을 수 있다. 심지어 i*k+1같은 수식도 계산된 후에는 값으로 평가되므로 이 값을 전달할 수 있다. 그러나 참조 호출은 번지를 전달하기 때문에 번지를 가지는 변수만 사용할 수 있으며 상수는 사용할 수 없다. 상수는 메모리를 점유하고 있지 않기 때문에 번지가 없다. 즉, 상수는 좌변값이 아니며 참조 호출 함수의 실인수로는 좌변값만 사용할 수 있다.

만약 plusref(&5)와 같이 상수의 번지를 넘기고 싶다고 해 보자. 5라는 상수는 좌변값이 아니므로 &5라는 표현부터가 벌써 잘못된 것이다. 5는 메모리에 저장된 값이 아니므로 번지가 없고 상수 5는 어디까지나 상수 5일 뿐이지 어떤 방법을 쓰더라도 이 값은 6이나 4가 될 수는 없다.

plusref(&i);

void plusref(int *a)
{
    *a=*a+1;
}

3) 출력용 인수

 입력용 인수 : 가장 일반적인 의미의 인수이며 호출원에서 함수에게 작업 대상을 지정하기 위해 전달된다. plusone 함수의 a인수, printf 함수의 모든 인수들, gotoxy 함수의 인수들이 모두 입력용 인수이다. 함수의 입장에서 볼 때 이 인수들은 호출원으로부터 입력되는 값이므로 입력용 인수라고 한다.

gotoxy(10,20);
plusone(i);
printf("The Result is %d",k); 

이 호출문들에서 사용된 모든 인수가 바로 입력용 인수들이다. 함수로 입력되는 실체는 값이므로 상수를 바로 쓸 수도 있다. 하지만 초기화되지 않은 변수를 쓰는 것은 일반적으로 경고 처리되는데 다음 호출문을 보자.

int i,j;
j=plusone(i);

i를 선언만 하고 바로 plusone 함수에게 이 값을 전달했다. i를 선언만하고 초기화하지 않았으므로 이때 plusone이 받게 되는 값은 쓰레기값이며 컴파일러에 의해 경고로 처리된다. 알지도 못하는 값에 대해 어떤 동작을 하려고 했으므로 정상적인 코드가 아니라는 뜻이며 이대로 내버려 두면 골치아픈 버그의 원인이 되기도 한다. 입력용 인수는 함수로 전달되기 전에 반드시 원하는 값으로 초기화되어야 한다.

 출력용 인수 : 참조 호출로 전달되는 인수들의 대부분이 출력용 인수이다. 함수에게 작업거리를 주기 위해 전달되는 것이 아니라 함수가 작업한 결과를 돌려 받기 위해서 사용된다. 함수의 입장에서 볼 때 호출원에서 입력되는 값이 아니라 호출원으로 출력되는 값이므로 출력용 인수라고 한다. 대표적인 예는 scanf 함수의 인수이다.

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

정수값 하나를 입력받기 위해 i변수를 선언하고 이 변수의 번지를 scanf 함수에게 전달했다. 참조 호출이기 때문에 상수는 사용할 수 없고 반드시 변수만 사용할 수 있다. 또한 입력되는 값이 아니므로 초기화할 필요가 없으며 위 코드는 경고없이 정상적으로 컴파일된다. scanf는 i의 값을 입력받아 어떤 동작을 하는 것이 아니라 먼저 어떤 동작을 한 후 그 결과를 i에 대입하기 위해 이 변수의 번지를 전달받았다. 그래서 scanf의 인수는 출력용이다.

함수가 호출원으로 작업 결과를 돌려주는 일반적인 방법은 리턴값을 사용하는 것이지만 이런 식으로 출력용 인수를 사용하여 리턴값을 돌려줄 수도 있다. 리턴값은 단 하나밖에 돌려줄 수가 없지만 출력용 인수를 사용하면 여러 개의 값을 동시에 돌려줄 수 있다는 이점이 있다. 예를 들어 게임의 캐릭터 좌표를 조사하는 함수를 만든다면 다음과 같이 만들어야 한다.

void GetCharacterPos(int *x, int *y);

평면상의 좌표는 (x,y) 두 개의 정수값으로 표현되기 때문에 리턴값으로는 이 좌표를 돌려줄 수 없으며 참조 호출로 두 개의 정수값을 돌려 주도록 해야 한다. 함수의 리턴값이 하나밖에 없는 이유는 유일한 값을 리턴해야만 함수를 수식내에서 바로 사용할 수 있기 때문이다. 이 예에서처럼 여러 개의 값을 동시에 조사해야 하는 경우라면 출력용 인수를 사용하거나 아니면 구조체를 리턴하도록 해야 한다. 이 함수를 호출하려면 반드시 두 개의 변수를 선언해야 하며 수식내에서 바로 사용할 수도 없다.

 입출력용 인수 : 이 인수는 입력과 출력을 겸하는 인수이다. 즉, 호출원에서 함수에게 작업 대상을 전달하기 위해서도 사용하며 함수가 호출원에게 작업 결과를 전달하기 위해서도 사용한다. 출력을 해야 하므로 이 인수는 참조 호출 방식으로 전달되어야 하며 형태상으로는 출력용 인수와 동일하다.

출력용 인수와 다른 점은 입력을 겸하고 있기 때문에 반드시 초기화를 해야 한다는 점이다. 그러나 초기화하지 않고 전달할 경우 컴파일러가 이를 에러나 경고로 처리하지는 않는데 왜냐하면 이 인수가 출력용인지 입출력용인지는 형태상으로 구분되지 않기 때문이다. 하지만 초기화되지 않은 쓰레기값을 사용하면 당연히 원하는 결과는 나오지 않는다.

참조 호출 방식이므로 상수는 물론 사용할 수 없으며 변수만 사용할 수 있다. 입출력용 인수의 예는 그리 흔하지는 않지만 바로 앞에서 만들어 보았던 plusref 함수의 a 인수가 바로 입출력을 겸하는 인수이다. 이 함수는 a의 값을 입력으로 받아 들이기도 하며 이 값을 1 증가시켜 다시 a로 돌려 주기도 한다. 입력, 출력을 모두 겸하고 있으니 입출력용 인수라고 하는 것이다.


3. 전처리기 (PreProcessor)

1) #include

컴파일러는 #include 명령을 먼저 처리하여 헤더 파일을 모두 포함한 후에 컴파일을 시작한다. 그래서 #include <stdio.h> 명령을 소스 선두에 작성해 놓으면 stdio.h에 선언된 함수의 원형, 데이터 타입, 열거 상수 등을 공짜로 사용할 수 있는 것이다.

#include 명령을 쓰는 대신 헤더 파일의 내용을 복사하여 직접 소스에 붙여 넣어도 결과는 동일하지만 불편할 것이다. #include 명령으로 포함시키면 하나의 헤더 파일을 여러 소스에서 동시에 사용할 수 있으며 소스의 길이가 짧아져 관리하기에도 좋다. #include 다음에 포함할 파일의 이름을 적는데 사용하는 괄호에 따라 두가지 형태가 있다. 

#include <file.h> : C에서 제공하는 표준 헤더파일을 포함시키고자 할 때 < > 괄호를 사용한다. 컴파일러의 옵션중에 표준 헤더 파일들이 어떤 디렉토리에 있는지를 기억하는 옵션이 있는데 비주얼 C++의 경우 도구/옵션/프로젝트/VC++ 디렉토리(6.0의 경우 Tools/Options/Directories) 대화상자에서 헤더 파일 디렉토리를 지정한다. < >괄호를 사용하면 표준 헤더 파일 디렉토리에서 지정한 파일을 찾는다.

#include "file.h" : 사용자가 직접 작성한 헤더 파일을 포함시키고자 할 때 " "괄호를 사용한다. 이 괄호를 사용하면 소스 파일과 같은 디렉토리에서 헤더 파일을 먼저 찾아 본다. 직접 만든 헤더 파일은 보통 소스와 같은 디렉토리에 두므로 이 괄호를 사용하면 된다.

괄호 형식에 따라 헤더 파일을 어디서 먼저 찾을 것인가의 검색 순서가 달라지는데 사실 두 괄호는 그다지 엄격하게 구분할 필요가 없다. " " 괄호를 사용했더라도 현재 디렉토리에 이 파일이 없으면 표준 헤더 파일 디렉토리도 검색하며 반대로 < > 괄호를 사용했더라도 현재 디렉토리도 같이 검색한다. 즉 #include "stdio.h"라고 할 경우 현재 디렉토리에 stdio.h가 있는지 보고 없다면 표준 헤더 파일 디렉토리도 검색하므로 결과는 마찬가지다.

괄호에 따라 결과가 달라지는 경우는 표준 헤더 파일 디렉토리와 현재 디렉토리에 같은 이름을 가지는 헤더 파일이 있을 경우인데 어떤 파일이 우선적으로 포함되는가만 다를 뿐이다. 이런 특수한 경우가 아니라면 굳이 괄호를 구분할 필요는 없다. 그러나 관행상 표준 헤더 파일은 < > 괄호를, 사용자 정의 헤더 파일은 " "를 사용하고 있으므로 이 관행을 지키는 것이 바람직하다.

#include 명령은 주로 헤더 파일을 포함시키기 위해 사용하지만 꼭 헤더 파일만 가능한 것은 아니다. 확장자가 cpp인 파일도 포함할 수 있으며 txt나 임의의 파일이라도 텍스트 파일이기만 하면 다 포함할 수 있다. 예를 들어 1000줄쯤 되는 아주 큰 배열 정의문이 있는데 이 정의문이 너무 길어 소스를 편집하기가 불편하다면 이 부분만 array.cpp로(또는 array.txt, array.inc) 따로 떼어 내고 주 파일에서는 #include "array.cpp"로 불러 오면 된다.

#include 다음의 파일명은 대소문자를 구분하지 않는다. C언어 자체는 대소문자를 구분하지만 윈도우즈의 파일 시스템이 대소문자를 구분하지 않기 때문에 #include <STDIO.H>라고 쓸 수도 있고 #include <stdio.h>라고 해도 문제가 없다. 물론 유닉스나 리눅스 환경에서는 대소문자가 구분되므로 가급적이면 원래 파일명과 똑같이 쓰는 것이 좋다.

#include 명령은 중첩 가능하다. 포함한 파일이 다른 파일을 포함하고 있다면 포함된 모든 파일이 주 파일로 읽혀진다. 예를 들어 A가 B를 포함하고 있는 상태에서 주 파일이 #include "A" 명령을 사용하면 A가 B까지 같이 가지고 주 파일에 포함된다.

2) #define

#define전처리문은 매크로 상수를 정의하는데 기본 문법은 다음과 같다.

#define 매크로명 실제값

#define GAMETIME 240

기억하기 쉬운 적당한 이름을 주고 실제값을 뒤에 써 주면 되는데 예를 들어 YEAR라는 매크로 상수를 365로 정의하고 싶다면 #define YEAR 365라고 정의하면 된다.

다음은 #define 문으로 매크로 상수를 정의할 때의 일반적인 주의 사항이다. 상식적으로 쉽게 이해될 것이다.

#define문은 전처리문이지 코드를 생성하는 명령이 아니다. 그래서 행 끝에 세미콜론은 붙이지 않는다. #define MACH 1200.0;이라고 써서는 안된다. 만약 이렇게 쓰면 세미콜론조차도 매크로의 실제값에 포함되어 버린다. #include는 물론이고 다음에 배울 모든 전처리문들도 세미콜론은 붙이지 않으며 주석을 제외한 다른 문장이 뒤따라 올 수 없다.

매크로의 이름도 일종의 명칭이기 때문에 명칭 규칙에 맞게 작성해야 한다. 중간에 공백이 들어간다거나 숫자로 시작한다거나 다른 명칭과 충돌해서도 안된다. 매크로 상수는 다른 명칭과 구분될 수 있도록 관습적으로 대문자를 사용하는 것이 일반적이다. NUM, MY_MACRO 등은 적법한 명칭이며 3RD는 숫자로 시작되었으므로 부적격하며 MY MACRO는 중간에 공백이 있으므로 매크로명으로 사용할 수 없다.

매크로 이름에는 공백이 들어갈 수 없지만 매크로의 실제값은 공백을 가질 수 있다. #define 전처리문은 매크로를 실제값으로 단순 치환할 뿐이므로 공백이 있건 한글을 사용하건 전혀 상관하지 않는다. 다음은 자주 쓰는 메시지 문자열을 매크로로 정의한 것이다.

#define ERRMESSAGE "똑바로 하란 말이야"

문자열을 통째로 ERRMESSAGE라는 매크로 상수로 정의했으며 좀 어색해 보이지만 적법한 문장이다. 실제 코드에서 puts(ERRMESSAGE); 형식으로 사용하면 된다.

문자열 상수내에 있는 매크로나 다른 명칭의 일부로 포함된 경우는 치환되지 않는다. #define HUMAN 5 라는 전처리문은 소스의 모든 HUMAN을 찾아 5로 치환할 것이다. 그러나 printf("I am a HUMAN"); 문장에 있는 HUMAN은 매크로 상수와 이름은 같지만 어디까지나 문자열일 뿐이므로 치환하지 않는 것이 합리적이다. HUMANNAME이라는 다른 명칭에도 HUMAN이라는 이름이 포함되어 있지만 분리된 명칭이 아니므로 5NAME으로 치환되지 않는다.

매크로는 중첩 가능하다. 즉, 매크로 상수가 매크로 상수를 참조할 수 있다는 얘기다. 다음 예를 보자. 

#define A 3
#define B (A*2) 

A를 3으로 정의했고 B는 A에 2를 곱한 값으로 정의했으므로 6이 될 것이다. B가 A를 참조하고 있는 셈이다. 두 번 세 번 얼마든지 중첩할 수 있되 단 중첩되는 매크로가 먼저 정의되어 있어야 하므로 순서에 신경써야 한다.

값을 가지지 않는 빈 매크로도 정의할 수 있다. 

#define PROFESSIONAL 

이 매크로는 값을 가지지 않으며 매크로 상수 자체만 존재할 뿐이다. 이렇게 값을 가지지 않는 매크로는 주로 조건부 컴파일 지시자와 함께 사용되며 존재하는가 아닌가만으로 의미를 가진다. 또한 아무 값도 가지지 않음을 명시할 때도 빈 매크로를 정의한다.

#define 전처리문의 동작이 단순하기 때문에 매크로 상수는 쉽게 이해가 갈 것이다. 그렇다면 매크로 상수를 어느 수준으로 사용할 것인가에 대해 생각해 보자. 모든 상수를 다 매크로 상수로 정의한 후 사용할 수도 있고 적당한 수준에서 상수를 직접 사용할 수도 있는데 이는 개인적인 취향에 따라 결정할 문제이다.

매크로 상수는 분명히 기억하기 쉽도록 해 주고 일괄적인 수정에 도움을 준다. 그러나 너무 남발하면 오히려 소스를 더 읽기 어렵게 만들 수도 있으며 기억력 보조에도 별 도움이 되지 못하는 경우가 있다. 다음 예를 보자.

3) 매크로 함수

#define을 이용하여 다음과 같이 매크로 함수를 작성할 수는 있지만 컴파일이 진행될 때 각 매크로를 함수의 내용으로 그대로 치환하는 형식이라 효율적인 방법은 아니다.

#define dubae(i) i+i;
#define VALUE2 VALUE+100;
#define printmsg(x,y,str) { gotoxy(x,y); puts(str); }

4) C 프로그램의 구조

C프로그램은 대체로 다음과 같은 구조를 가진다고 했다. 물론 필요에 따라 순서를 조금씩 바꿀 수 있겠지만 여러 사람들이 오랫동안 C로 프로그래밍을 해 본 결과 이 구조가 가장 이상적이라는 것을 알게 되었으며 그래서 대부분의 C 소스는 이런 구조로 만들어진다. 처음 배울 때는 남들 하는대로 일단 따라하는 것이 좋다.

#include <...>
#define ...

함수의 원형
전역변수

 void main()
{
     코드
}

함수
함수
함수

 제일 먼저 헤더 파일을 포함하는 #include 문이 오고 다음으로 매크로를 정의하는 #define이 온다. 그리고 이 프로그램이 사용하는 함수의 원형과 전역변수를 선언하고 main 함수가 가장 먼저 오며 main 함수 다음에 사용자 정의 함수를 작성한다. 이 방식대로 코드를 작성하면 제일 말썽이 없고 코드를 유지 보수하기에 편리하다.

출처: http://winapi.co.kr/

'old > C Grammer' 카테고리의 다른 글

표준 함수  (0) 2010.11.19
기억 부류  (0) 2010.11.18
연산자  (0) 2010.11.16
제어문  (0) 2010.11.15
변수  (0) 2010.11.12
Posted by jazzlife
,

연산자

old/C Grammer 2010. 11. 16. 17:13

프로그램은 데이터와 코드로 구성되는데 데이터는 프로그램이 처리하는 재료이고 코드는 데이터를 가공하는 수단이다. 데이터(Data)를 우리말로 번역하면 자료이며 이 데이터를 처리하여 인간에게 유용한 형태로 가공하면 이것이 정보(Information)가 된다. 자료와 정보는 비슷한 것 같지만 많이 다르다. 자료는 불규칙하게 널려 있는 여러 가지 형태의 수치나 문자열이며 정보는 인간이 곧바로 사용할 수 있도록 정리된 유용한 것이다.

예를 들어 성적 처리 프로그램의 경우 각 학생의 과목별 성적은 데이터인데 이 자체로는 별로 유용한 정보를 얻을 수 없다. 이 자료를 가공하여 총점, 평균, 석차를 내면 비로소 학력 평가를 알 수 있는 유용한 정보가 된다. 또한 인터넷에 널려있는 웹 문서들은 그 자체로서는 가치가 없으며 검색 엔진 등에 의해 필요한 것을 추출, 정리해야 비로소 가치있는 정보가 된다. 프로그램이란 사람을 대신해서 이런 일을 하는 소프트웨어이며 프로그램이 자료를 가공하여 정보를 만드는 주된 수단이 바로 이 장의 주제인 연산자다.

연산자(Operator)가 무엇인가는 굳이 문장화하여 정의를 내리지 않더라도 이미 이해하고 있을 것이다. 더하고 빼고 곱하고 나누는 동작을 하는 것들이 바로 연산자이며 실생활에도 늘상 사용하고 있는 것들이다. 물론 소프트웨어 공학적인 측면에서 정의를 내린다면 좀 더 복잡해지겠지만 일단은 여러분들이 상식적으로 이해하고 있는 연산자와 같다고 생각하면 된다.

C는 아주 많은 종류의 연산자를 제공하는데 이런 풍부한 연산자 지원이 C의 큰 장점이기도 하다. 제공하는 연산자가 많다는 것은 그만큼 데이터를 가공할 수 있는 능력이 탁월하다는 뜻이며 이런 연산자들을 자유 자재로 사용할 수 있으면 복잡한 연산을 간단하게 처리할 수 있다. 수가 많으므로 연산자를 종류별로 분류해 보도록 하자. 분류 방법에는 여러 가지가 있지만 일단 연산자의 기능별로 분류하는 것이 가장 일반적이다.

기능별 종류

연산자

산술 연산자

+ - * / %

부호 연산자

+ -

대입 연산자

= 복합 대입 연산자

관계 연산자

== != <= < >= >

증감 연산자

++ --

포인터 연산자

* & []

구조체 연산자

. ->

논리 연산자

|| && !

비트 연산자

| & ~ >> <<

삼항 조건 연산자

? :

쉼표 연산자

,

sizeof 연산자

sizeof

캐스트 연산자

(type) type()

괄호 연산자

()

C++ 연산자

new delete :: .* ->*

도표를 보면 이 많은 연산자들을 언제 다 익히나 싶을 정도로 양이 많다. 양만 많을 뿐이지 어렵지는 않으므로 이 장을 통해 하나씩 익혀 나가도록 하자. 솔직히 연산자 자체는 결코 어렵다고 할 수 없으나 적재 적소에 상황에 맞는 최적의 연산자를 선정하여 요긴하게 사용하는 응용력을 키우는 것이 어렵다. 연산자 하나를 배울 때마다 데이터를 다룰 수 있는 능력이 향상된다고 생각하면 즐거운 마음으로 공부할 수 있을 것이다. 프로그래머를 무사에 비유한다면 연산자는 칼, 활 같은 무기라고 할 수 있다.

이 도표는 연산자의 기능별로 분류한 것이고 이 외에 피연산자의 개수에 따라 분류하기도 한다. 피연산자란 연산 대상을 말하는데 몇 개의 피연산자를 요구하는가에 따라 단항 연산자(Unary Operator), 이항 연산자(Binary Operator)로 분류한다. a=1+2; 라는 문장에서 +연산자는 1과 2의 합을 계산한다. 양쪽에 두 개의 피 연산자가 있으므로 + 연산자는 이항 연산자이다. = 대입 연산자도 a와 1+2의 결과인 3을 피연산자로 요구하므로 이항 연산자이다. 반면 -3 같은 부호 연산자나 a++같은 연산자는 피연산자가 하나밖에 없으므로 단항 연산자이다. 앞에서 포인터를 소개할 때 잠시 구경해 본 *, & 연산자도 단항 연산자이다.


1. 산술 연산자

산술(Arithmetic) 연산자는 더하고 곱하고 빼고 나누는 가장 기본적이고 또한 가장 많이 사용되는 연산자이다. 초등학교때부터 배웠고 일상 생활에서도 늘상 사용하는 연산자이므로 더 이상의 상세한 설명이 필요치는 않을 것 같다. 연산자는 기호로 표시하는데 더하기와 빼기는 수학에서와 마찬가지로 +, - 기호를 사용한다. 곱하기는 보통 ´ 기호를 사용하지만 알파벳 X와 모양이 동일해서 *를 대신 사용한다. 그리고 나누기는 보통 ¸기호를 사용하는데 이 문자가 키보드에 없기 때문에 / 기호를 사용하여 분수 형태로 표현한다.

기호가 조금 다른 것 빼고는 학교에서 배운 것과 동일하되 다만 나눗셈을 하는 / 연산자에 대해서만 약간의 주의를 하면 된다. 이 연산자는 피연산자의 타입에 따라 연산의 결과가 달라지는 특징이 있다. 피연산자가 모두 정수형이면 결과도 정수형이 되고 피연산자중에 실수형이 있으면 결과도 실수형이 된다.

6/3                   // 결과는 2
3.0/2.0              // 결과는 1.5

출력 문자열 앞쪽에 공백이 하나 더 있음을 유의하도록 하자. 이 공백은 이전의 달팽이 꼬리를 지우는 역할을 하는데 없을 경우 잔상이 남게 된다.

산술 연산자에 대해서는 다 알아보았고 산술 연산자와 모양이 똑같은 부호 연산자에 대해 알아보자. 부호 연산자는 피연산자의 부호를 바꾼다. score 변수에 23이라는 값이 들어 있다면 -score는 부호를 바꾸어 -23이 된다. 산술 연산자의 -와 부호 연산자의 -는 모양은 같지만 기능은 다르다. 뺄셈 연산자 -는 피연산자를 두 개 취하는 이항 연산자이고 부호 연산자 -는 피연산자 하나만 취하는 단항 연산자이다. 다음 식을 보자.

a*-b+c-d;

b앞의 -는 부호 연산자이고 d앞의 -는 산술 연산자이다. 어렵지 않게 구분될 것이다. 그럼 다음 대입문에서 -는 어떤 연산자일까?

a=-1;

여기서 사용된 -기호는 언뜻 보기에 부호 연산자인 것 같지만 사실은 연산자가 아니다. -1이라는 상수의 한 부분일 뿐이며 부호를 바꾸는 동작을 하지는 않는다. 부호 연산자 -를 사용하면 변수에 저장되어 있는 값의 음수값을 얻을 수 있다. 다음은 두 개의 정수형 변수 a와 b에 대한 연산예이다.

a+b         // a와 b를 더한다.
a+-b;       // a와 b의 음수값을 더한다.
a-b;        // a에서 b를 뺀다.
a--b;       // a에서 b의 음수값을 뺀다.

이 연산문중 앞쪽 세 개는 모두 합법적이다. 그러나 4번째 연산문은 에러로 처리되는데 왜냐하면 --는 감소 연산자라는 단항 연산자로 별도로 정의되어 있기 때문이다. 수학에서 --는 +와 같아지므로 a--b는 사실 a+b와 같지만 C에서는 그렇지 않다. 만약 정 이런 연산을 할 필요가 있다면 a- -b와 같이 공백을 하나 넣어 주든가 아니면 a-(-b)같이 괄호를 싸야 한다.


대입 연산자

대입(Assignment)이란 변수에 어떤 값을 집어 넣는 동작이며 대입 연산자는 변수의 값을 변경할 때 사용한다. = 기호를 사용하며 다음이 가장 간단한 대입문의 예이다.

i=1;

i라는 변수에 1이라는 값을 대입한다. = 연산자는 우변에 있는 값을 좌측으로 대입하는데 위와같이 우변이 상수일 경우는 계산할 것도 없이 상수를 바로 좌변에 대입한다. 우변이 다음과 같은 수식일 경우는 이 수식을 계산한 결과가 좌변으로 대입된다.

i=2+3;
i=j+k*m;

i에 2+3을 더한 5가 대입될 것이다. 이런 상수 수식은 계산 결과가 상수이기 때문에 실행시에 계산되지 않으며 컴파일할 때 컴파일러에 의해 미리 계산된다. 즉 i=2+3;이라고 대입하면 컴파일러는 이 명령을 i=5;로 고쳐서 써 넣으므로 2+3이라고 쓰나 5라고 쓰나 전혀 차이가 없다. 변수가 들어가 있는 수식은 변수의 값에 따라 결과가 달라지므로 실행할 때 계산되어 i로 대입될 것이다.

복합 대입 연산자

복합 대입 연산자는 대입 연산자와 다른 연산자가 결합된 연산자이다. 간단한 사용예를 보자.

a=a+3;

이 수식은 원래 a값에 3을 더해 a에 다시 대입하라는 뜻이며 a를 3만큼 증가시킨다. 변수의 현재값을 기준으로 상대적으로 값을 증감시킬 때 아주 많이 사용하는 식이다. 이처럼 좌변의 변수가 우변의 수식에 포함되어 있을 때는 이것을 복합 대입 연산자로 바꿀 수 있다.

a+=3;

여기서 사용된 +=이라는 연산자가 바로 복합 대입 연산자이며 좌변의 값을 우변만큼 증가시킨다. a=a+3과 a+=3은 완전히 같은 식이되 변수명이 다음과 같이 아주 길다면 이 변수를 두 번 쓰는 것보다는 복합 대입 연산자로 한 번만 쓰는 것이 편리하다. 

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

 그러나 아무 연산자나 다 복합 대입 연산자로 만들 수는 없으며 위에 보인 10개가 전부이다. 이런 복합 대입 연산자를 만나면 원래 수식을 떠올려 보면 된다. 복합 대입 연산자를 쓰면 식이 좀 더 간단해지는 반면 몇 가지 부작용이 있기 때문에 복잡한 수식에서는 사용하지 않는 것이 좋다. a+=n 정도의 간단한 수식에만 사용하는 것이 바람직하다.

증감 연산자

증감 연산자는 피연산자를 1씩 증가시키거나 감소시킨다. 루프의 제어 변수처럼 순서대로 어떤 작업을 할 때는 변수값을 하나씩 증감시키는 경우가 많기 때문에 별도의 연산자가 마련되어 있다. 증가 연산자는 ++로 쓰며 감소 연산자는 --를 쓴다. 둘 다 사용 방법은 비슷하므로 ++연산자에 대해서만 집중적으로 연구해 보자. 다음 세 식은 모두 동일하며 a를 1 증가시킨다.

전위형(Prefix) : 증감연산자가 피연산자 앞에 위치한다. ++a, --a
후위형(Postfix) : 증감연산자가 피연산자 뒤에 위치한다. a++, a--


2. 논리 연산자


관계 연산자

관계 연사잔는 피연산자 두개를 취하는 이항연산자이며 좌변과 우변을 비교할 때 쓰이는 연산자이다.

==, !=, >, <, >=, <=

논리 연산자

논리 연산자는 주로 관계 연산자와 함께 사용되며 두 개 이상의 조건식을 결합하여 하나의 진리값을 만들어 낸다. 다음 세 가지 종류가 있다.

연산자

설명

!

논리 부정(Not)

논리식의 진위를 반대로 만든다.

&&

논리곱(And)

논리식이 모두 참이어야 참이다.

||

논리합(Or)

논리식 하나만 참이면 참이다.


if ((a > 5 && a < 10) || (b >= 20 && b <= 100) && c != 7) 명령;


비트 연산자

비트 연산자는 논리 연산자와 비슷하지만 비트를 연산 대상으로 한다는 점이 조금 다르다.

연산자

설명

~

비트를 반전시킨다.

&

대응되는 비트가 모두 1 1이다.

|

대응되는 비트가 모두 0 0이다.

^

개의 비트가 달라야 1이다.

<<

지정한 수만큼 왼쪽으로 비트들을 이동시킨다.

>>

지정한 수만큼 오른쪽으로 비트들을 이동시킨다.



쉬프트 연산자

쉬프트(Shift) 연산자는 비트들을 지정한 수만큼 좌우로 이동시킨다. >> 연산자는 오른쪽으로 비트를 이동시키며 << 연산자는 왼쪽으로 비트를 이동시킨다. 연산자의 모양이 이동 방향의 화살표와 비슷하게 되어 있어 직관적이다. a << 1은 a를 1비트 왼쪽으로 이동시키며 a << 3은 3비트 왼쪽으로 이동시킨다. 두 개의 피연산자를 취하는 이항 연산자인데 좌변은 보통 정수형 변수가 오고 우변은 정수 상수가 온다. 물론 양변이 모두 변수(a << b)일 수도 있고 양변이 모두 상수(1 << 3)일 수도 있다.

if (((a & 0x7c00) >> 10) == 2)

a를 0x7c00과 & 연산하여 마스크 오프시키고 10번 오른쪽으로 민 후 이 값이 ㄱ의 코드인 2인지 비교했다. 반대로 초성만 특정한 값으로 바꿀 때는 어떻게 할지 생각해 보기 바란다. 쉬프트 연산은 고속의 그래픽 처리가 필요할 때 비디오 램을 직접 액세스하기 위해서도 많이 사용된다. 비디오 램에 들어 있는 이미지를 쉬프트하면 스크롤될 것이다.

쉬프트 연산의 피연산자는 주로 부호없는 정수형이다. 실수형은 당연히 안된다. 부호있는 정수형은 가능은 하지만 이 경우 동작이 조금 달라진다. 최상위에 있는 부호 비트는 쉬프트 대상에서 제외되는데 부호는 값이 아니기 때문에 유지하는 것이 옳다. 부호있는 정수에 대한 쉬프트 연산은 권장되지 않으며 실제로 의미를 가지는 경우도 드물다.

쉬프트 연산이 곱셈에 비해 불리한 점은 2의 거듭승에 대해서만 곱셈이 가능하다는 점이다. 2배, 4배, 8배, 16배 등만 할 수 있으며 3배, 17배 이런 연산은 할 수 없다. 그러나 쉬프트 연산과 덧셈, 뺄셈을 잘 조합하면 이런 연산이 가능해지기도 한다.

3배 : a << 1 + a;
9배 : a << 3 + a;
15배 : a << 4 - a;
60배 : a << 6 - a << 2;

특히 제일 마지막의 쉬프트 연산으로 60배를 하는 코드는 아주 기발한 응용예이며 감탄을 금하기 어려운 예술 코드라고 할 수 있다. 정밀하게 측정해 보면 이런 연산들이 곱셈보다 수배~수십배 더 빠르다. 보통의 경우라면 일반적인 곱셈을 하는 것이 소스를 유지하기에 편리하지만 속도가 지극히 중요하다면 곱셈보다는 가급적이면 쉬프트 연산을 사용하는 것이 좋다.

회전 연산자

회전(Rotate) 연산은 쉬프트 연산과 유사한 비트 조작 명령이다. 쉬프트는 비트를 선형으로 이동시키는데 비해 회전 연산은 원형으로 이동시킨다. 비트 이동에 의해 밀려나는 비트는 버려지지 않고 반대쪽으로 다시 이동된다는 것이 특징이다. 회전 연산은 쉬프트 연산에 비해 많이 사용되지 않기 때문에 연산자 형태로는 제공되지 않으며 _rotl, _rotr 함수로 제공된다. C 수준에서는 사용할 일이 그리 많지 않다. 과거 도트 프린터나 잉크젯 프린터의 경우 헤더가 수직으로 배열되어 있으므로 한 비트씩 추출하여 한 줄을 만들었는데 이럴 때 회전 연산이 사용되었다.

3. 기타 연산자

삼항조건 연산자

삼항 조건 연산자는 특이하게도 피연산자를 세 개나 가지는데 다른 언어에는 없는 C언어의 독특한 연산자이다. 기본 형식은 다음과 같다.

(조건식) ? 1:2

 ? 앞에 조건식이 있고 ? 뒤에 :을 사이에 두고 두 개의 값이 온다. ?와 :은 한 연산자를 구성하는 짝이기 때문에 반드시 같이 와야 하며 단독으로 사용할 수는 없다. if else, do while처럼 짝을 이루어 사용되는 연산자이다. 이 연산자는 조건식을 평가해 보고 참이면 값1을 리턴하고 거짓이면 값2를 리턴한다. 조건식 자리에는 보통 변수의 값을 비교하는 관계 연산문이 오지만 조건식으로 사용될 수 있는 식이면 어떤 것이든지 가능하다. 변수나 상수, 함수호출문 등 논리값을 리턴하는 모든 식이 올 수 있다. 조건식을 감싸는 괄호는 반드시 필요한 것은 아니나 괄호가 있는 것이 보기에 좋고 안정감이 있어 보인다.

printf("%d는 %s수입니다.\n",i,i%2==0 ? "짝":"홀");

쉼표 연산자

쉼표 연산자는 쉼표 기호(,)를 사용하는데 모양만으로 보면 구두점같이 생겨서 연산자가 아닌 것처럼 보이기도 한다. 하지만 분명히 연산자이다. 피연산자로 양쪽에 두 개의 표현식을 취하며 좌변을 먼저 평가하고 우변을 평가한 후 우변의 연산 결과를 리턴한다. 쉼표 연산자는 어떤 연산을 한다기보다는 두 연산식을 하나로 묶는 역할만 한다. 이 연산자를 사용하면 두 개의 표현식을 하나로 합칠 수 있다.

j=i=3,i+2;
for ({ i=1;j=2; };i<5;{ i++;j+=2; }) {

sizeof 연산자

다른 연산자들은 모두 +, -, && 같은 기호로 표현하는데 sizeof 연산자는 단어로 되어 있어 조금 특이해 보인다. 이 연산자는 피연산자로 주어진 타입 또는 변수의 크기를 계산한다. 기본 형식은 다음과 같다.

sizeof(타입 또는 변수)

피연산자로 int, double같은 타입을 쓸 수도 있고 변수를 쓸 수도 있으며 상수를 사용할 수도 있다. 아뭏든 괄호안에 있는 대상이 메모리를 얼마나 차지하고 있는지 계산한다.

캐스트 연산자

캐스트 연산자는 수식내에서 변수의 타입을 강제로 다른 타입으로 바꾼다. 별다른 지정이 없으면 변수의 고유한 타입대로 연산이 수행되는데 가끔 타입을 바꿔서 연산해야 할 경우가 있다. 캐스트 연산자의 형식은 다음 두 가지가 있다.

(타입)변수
타입(변수) 

전자는 C언어의 캐스트 연산자 형식이며 후자는 C++언어에서 새로 추가된 캐스트 연산자 형식이다. C++ 컴파일러에서는 두 형식 모두 사용할 수 있는데 C++ 형식이 함수 호출문과 유사해서 더 명시적이고 가독성이 좋다. 하지만 C 형식도 기능상 특별한 문제가 없고 오랫동안 사용해 왔기 때문에 아직까지도 C++형식보다는 C형식이 더 많이 사용된다. 둘 중 어떤 형식을 사용할 것인가는 쓰는 사람의 자유이다.

r=(double)i/j;


4. 연산 규칙 


연산 순서

순위

연산자

결합순서

1

( ) [ ] -> .

왼쪽 우선

2

! ~ ++ -- + -(부호) *(포인터) & sizeof 캐스트

오른쪽 우선

3

*(곱셈) / %

왼쪽 우선

4

+ -(덧셈, 뺄셈)

왼쪽 우선

5

<< >>

왼쪽 우선

6

< <= > >=

왼쪽 우선

7

== !=

왼쪽 우선

8

&

왼쪽 우선

9

^

왼쪽 우선

10

|

왼쪽 우선

11

&&

왼쪽 우선

12

||

왼쪽 우선

13

? :

오른쪽 우선

14

= 복합대입

오른쪽 우선

15

,

왼쪽 우선

 
결합 순서

결합 순서는 수식내에 같은 종류의 연산자가 있을 때 어떤 방향의 연산을 먼저 수행할 것인가를 지정한다. 연산 순위는 다른 종류의 연산자에 대한 실행 순서인 반면 결합 순서는 같은 연산자(또는 같은 순위내의 다른 연산자)의 실행 순서를 지정한다.

대부분의 이항 연산자들은 왼쪽 우선 순위를 가지기 때문에 수식에 등장하는 순서대로 실행된다. a=b+c+d; 연산문은 b와 c를 먼저 더하고 그 결과와 d를 다시 더하는데 덧셈은 교환 법칙이 성립하므로 사실 결합 순서가 큰 의미가 없다. 앞서 실습한 바 있는 다음 대입문을 보자.

 a=b=c=3;

 이 대입문은 a, b, c 모두 3을 대입하는데 대입 연산자는 오른쪽 우선이다. 즉 제일 오른쪽에 있는 c=3이 가장 먼저 실행되고 차례대로 b=c, a=b가 대입된다. 만약 대입 연산자가 왼쪽 우선 순위를 가지게 되면 a=b, b=c, c=3 순서대로 실행되어 a는 b의 쓰레기값을 가질 것이고 b는 c의 쓰레기값을 가지며 결국 3이 되는 것은 c밖에 없을 것이다.

대입 연산자가 오른쪽 우선의 결합 순서를 가지므로 복합 대입 연산자들도 모두 오른쪽 우선으로 되어 있다. 그외 단항 연산자들은 모두 오른쪽 우선 순위를 가진다. 다음 캐스트 연산문을 보자.

 (double)(unsigned)i;

(unsigned)가 먼저 실행되어 i의 부호를 없앤 후 (double)이 실행되어 실수 타입으로 바꾼다.

출처: http://winapi.co.kr/

'old > C Grammer' 카테고리의 다른 글

기억 부류  (0) 2010.11.18
함수  (0) 2010.11.17
제어문  (0) 2010.11.15
변수  (0) 2010.11.12
현정누나 요약 C/C++  (0) 2010.08.17
Posted by jazzlife
,

제어문

old/C Grammer 2010. 11. 15. 12:06

1) if문

조건문이란 주어진 조건에 따라 명령의 실행 여부를 결정하는 문장이다. 프로그램이란 항상 동일한 결과만 출력하는 것이 아니라 다양한 상황을 판단하여 다르게 동작하기도 한다. 이런 판단의 상황은 우리의 실생활에도 흔히 만나게 되는데 다음은 어느 백수의 일일 생활 순서도이다.

순서도에서 사각형은 동작을 나타내며 마름모는 조건 판단을 나타낸다. 마름모 안에 있는 문장이 조건인데 조건에 따라 특정 동작을 할 것인지 말것인지를 결정할 수 있다. 프로그램도 이와 마찬가지로 여러 가지 조건에 따라 상황을 판단하여 명령의 수행 여부를 결정한다. C언어의 조건문은 키워드 if를 사용하며 다음과 같은 형식을 가진다.

if (조건) 명령;

괄호안에 조건을 쓰고 이 조건이 만족할 때 실행할 명령을 괄호 뒤에 작성한다. 괄호는 조건과 명령문을 구분하기 위해 존재하며 생략할 수 없다. 베이직 언어의 조건문은 "if 조건 then 명령"으로 조건과 명령 사이에 키워드 then이 있는데 괄호나 then이나 모두 어디까지가 조건이고 어디부터 명령인지를 구분하는 역할을 한다. 조건은 주로 변수의 값을 비교하는 연산식인데 이때 다음과 같은 비교 연산자가 사용된다.

연산자

조건

==

좌변과 우변이 같다.

!=

좌변과 우변이 다르다.

>

좌변이 우변보다 크다.

<

좌변이 우변보다 작다

>=

좌변이 우변보다 크거나 같다.

<=

좌변이 우변보다 작거나 같다.

 수학에서 쓰는 등호, 부등호와 거의 유사하다. 단, 같다라는 조건은 =를 쓰지 않고 = 기호를 두 번 써서 ==로 표현한다. 그리고 다르다는 표현은 !=이라는 점을 주의하도록 하자. 부등 비교 연산자는 수학에서 ,≥로 표기하지만 이런 문자가 키보드에 없기 때문에 >=, <=로 표기하는데 =>, =<가 아님도 주의하도록 하자. "크거나 같다"라고 하지 "같거나 크다"라고는 하지 않으므로 자연어의 순서에 맞게 부등호가 먼저 온다고 외워 두면 헷갈리지 않을 것이다. 다음은 비교 연산자를 사용한 조건문의 예이다.

if (i == 5)           // i가 5이면
if (i != 5)            // i가 5가 아니면
if (i > 5)            // i가 5보다 크면

블록 안에 하나의 명령만 들어 있지만 { } 괄호로 묶었으므로 형태상 복문이다. 이 조건에 포함되는 다른 명령이 추가될 경우 { } 괄호안에 명령만 써 넣으면 된다. 명령을 미리 { }안에 작성해 놓으면 명령이 추가될 때 실수를 줄일 수 있고 소스를 읽기도 편해진다. 미리 { } 괄호를 싸 두지 않으면 명령만 추가하고 { } 괄호로 묶는 것을 깜박 잊어 버리는 경우가 많다. 아예 if 문의 기본 형식을 다음과 같이 암기하고 있는 것이 좋다.

if (조건) { 명령들 }

if 문의 기본 형식에 원래부터 { }가 포함된 것으로 생각하는 것이 바람직하며 if문을 입력할 때도 조건 다음에 아예 { }를 먼저 입력해 놓고 이 안에 명령을 작성하는 습관을 들여야 한다. 설사 if 조건에 걸리는 명령이 하나밖에 없어 { } 괄호가 불필요하더라도 이 괄호 때문에 프로그램이 더 커지거나 느려지거나 하지는 않는다. 블록은 하나의 문장으로 취급되므로 문장이 들어갈 수 있는 위치라면 블록도 언제나 들어갈 수 있다.

다음은 if문을 한 단계 더 확장해 보자.

if (조건) 명령1; else 명령2;

기본 if문은 조건이 만족할 때 특정 명령을 실행할 것인가 아닌가만 지정하는데 비해 else 문은 조건이 만족되지 않을 때의 동작까지도 같이 지정한다. else는 말 그대로 "그 외에"라는 뜻이며 조건이 만족되지 않을 때 실행할 명령을 지정한다. else가 있는 if문은 괄호안의 조건을 평가해 보고 이 조건이 참이면 명령1을 실행하고 거짓이면 명령2를 실행한다.

if (조건1) 명령1; else if (조건2) 명령2; else 명령3;

if (조건1) 명령1; else { if (조건2) 명령2; else 명령3 };


2) for 반복문

반복문은 비슷한 명령들을 여러 번 실행하는 제어 구조이다. 컴퓨터가 처리하는 데이터가 원래 반복적인 성격을 가지고 있기 때문에 반복문은 아주 많이 사용된다. 사실 컴퓨터가 제일 잘 하는 일이 아무 생각없이 주어진 명령을 계속 반복해 대는 것이다. 생각이 없다보니 속도도 빠르고 같은 일을 계속 시켜도 불평이 없다.

1번 학생부터 60번 학생까지 총점과 평균을 구하는 프로그램을 작성해야 한다면 똑같은 과정을 60번 반복해야 한다. 각 계산에서 달라지는 것은 학생의 번호뿐이며 총점을 구하는 방법이나 평균을 구하는 공식이 달라지는 것은 아니다. 이럴 때는 똑같은 처리를 60번 나열하는 것보다 한 번만 작성해 놓고 이 처리를 60번 반복하도록 하는 것이 훨씬 더 간단하다.

프로그램에서 이렇게 반복되는 부분을 루프(loop)라고 한다. 루프를 구성하는 방법에는 여러 가지가 있는데 C언어의 기본적인 반복문은 for문이다. for문의 형식은 다음과 같다.

for (초기식;조건식;증감식) 명령;

초기식 : 반복문은 보통 특정 변수가 일정한 범위에 있는 동안에 실행된다. 이때 반복문을 통제하는 변수를 제어 변수라고 한다. 초기식은 제어 변수의 초기값을 지정하며 루프가 시작될 때 한 번만 수행된다. i=0 이나 count=3 같은 대입문의 형식을 가지는 것이 보통이다.

조건식 : 반복문이 언제까지 실행될 것인가를 지정하며 이 조건이 참인동안 계속 루프를 돈다. 루프 실행을 계속할 계속 조건(탈출 조건이 아니라)이므로 조건이 거짓이 되면 루프를 탈출한다. 조건을 나타내므로 i < 10 또는 count < 100와 같은 제어 변수에 대한 비교 연산문이 온다. 조건문은 루프가 실행될 때마다 계속 평가된다.

증감식 : 한 번 루프를 돌 때 제어 변수를 얼마나 증감시킬 것인가를 지정한다. i=i+1 같이 제어 변수의 값을 변화시키는 연산문이 온다. 루프가 한 번 실행될 때 증감식도 한 번 실행된다.

명령 : 반복 실행될 명령이다. 하나의 명령이 올 수도 있고 { } 로 둘러싸인 복문이 올 수도 있는데 반복적인 처리는 보통 복문인 경우가 많다. 설사 루프에 포함된 명령이 하나뿐이더라도 실수 방지와 확장 편의성을 위해 가급적이면 { } 괄호를 싸 복문을 구성하는 것이 좋다.

for (i=1;i<=100;i=i+2)               // 1~100사이의 모든 홀수에 대해 반복
for (i=100;i>0;i=i-1)                 // 100~1까지 1씩 감소하며 역순으로 반복
for (f=0.1;f<=10.0;f=f+0.1)              // 0.1~10.0까지 0.1씩 증가하며 반복

(무한루프)

무한 루프란 반복 횟수가 미리 정해져 있지 않고 무한히 반복되는 루프이다. 제어 변수를 사용하는 루프는 제어 변수가 일정한 범위에 있을 때만 반복하므로 실행 회수가 미리 정해져 있는데 비해 무한 루프는 실행 회수를 미리 알 수 없다. 무한 루프를 만드는 방법은 아주 간단하다.

for (;;) {
           
명령;
}
 

조건식을 명시하지 않으면 이 조건은 항상 참으로 평가되기 때문에 루프가 끝나지 않게 된다. 그렇다면 무한 루프는 정말 무한히 반복되는가 하면 그렇지는 않다. 만약 정말로 무한히 반복된다면 루프 바깥의 코드가 실행될 수 없으므로 시스템 다운 상태가 되고 말 것이다. 무한 루프의 정확한 정의는 반복 회수가 가변적인 루프를 의미한다.

루프 자체에는 종료 조건이 포함되어 있지 않으며 명령을 실행하다가 일정한 조건이 되면 루프를 탈출한다. 즉 무한 루프란 형식상 무한히 반복되도록 해 놓고 루프 내부에서 끝낼 시점을 결정하도록 하는 루프이다. 그래서 무한 루프의 명령 블록에는 루프 탈출 처리가 반드시 포함되어 있어야 한다. 루프를 탈출할 때는 break문을 사용하는데 break는 조건식을 무시하고 강제로 루프를 종료하는 명령이다. 무한 루프의 일반적인 형태는 다음과 같다.

for (;;) {
     명령;
     if (탈출조건)
          break;
}

(다중루프)

다중 루프란 두 개 이상의 루프가 겹쳐 있는 제어 구조이다. 루프 안에는 반복의 대상이 되는 명령이 들어가는데 이 명령이 또 루프라면 이중 루프가 된다. 어떤 명령을 반복하는 동작을 또 반복하는 것이다. 그만큼 반복이란 흔한 동작이다. 다중 루프의 전형적인 예인 구구단 프로그램을 만들어 보자.

 다중 루프란 루프가 중첩(Nesting)되어 있는 것이다. 즉, 루프안에 루프가 완전하게 포함되어 있을 때 이를 다중 루프라 한다. 단순히 루프가 계속 이어진다고 해서 다중 루프가 아니다. 다음 예를 보자.

 

for (i=...) {
}
for (j=...) {
}

i루프와 j루프가 있지만 j가 i에 포함되어 있지 않고 i루프 바깥에 있다. 그래서 i루프가 완전히 종료되면 j루프가 시작된다. 이것은 단순 루프가 두 개 있는 것이지 다중 루프가 아니다.

for (i=...) {
    for (j=...) {
    }
}


3) while 반복문

while문은 for 문과 유사한 반복문이되 성격이 조금 다르다. 기본 형식은 다음과 같으며 키워드 while을 사용한다는 것 외에는 if문과 동일하다. if문은 딱 한 번만 조건을 판단하여 명령의 실행 여부를 결정하는데 비해 while문은 조건이 만족하는동안 명령을 계속 실행한다는 점이 다르다.

while (조건) 명령;

명령 자리에는 물론 여러 개의 명령을 묶어 놓은 복문이 올 수 있으므로 while (조건) {명령들} 이라고 외워두는 것도 좋다. while의 영어 뜻 그대로 조건이 참인 "동안" 명령을 계속 반복한다. 초기식이나 증감식 같은 것은 따로 없으므로 명령 블록에서 루프를 끝낼 수 있도록 해야 한다.

while문으로도 while (TRUE) 명령; 형식으로 무한 루프를 만들 수 있다. 조건이 TRUE로 고정되어 있으므로 while의 조건은 항상 참이 되어 명령을 무한히 반복할 것이다. 물론 정상적인 코드가 되기 위해서는 명령 블록 내에 일정한 조건이 되면 이 루프를 탈출(break)하는 문장이 포함되어 있어야 한다. 다음은 while문의 변형인 do while 문을 보자. 기본 형식은 다음과 같으며 do와 while이 짝을 이루어 사용된다. do만 있고 뒤에 while이 없으면 에러로 처리된다.

do 명령; while (조건);

 do 다음의 명령을 while의 조건이 만족하는 동안 반복적으로 실행하는데 파스칼의 repeat until 제어문과 동일하다. 명령은 보통 복문이 오므로 { } 괄호를 싸 주어야 한다.

for문 : 가장 큰 특징은 제어 변수를 사용한다는 점이다. 루프를 통제하는 변수를 선언하고 이 변수가 일정한 범위의 값을 가지는 동안 명령을 계속 반복한다. 그래서 통상 for문은 반복 횟수가 이미 정해져 있고 루프 중간에서 탈출하는 경우가 별로 없다. 물론 break문으로 강제로 탈출할 수도 있지만 일반적으로 반복 횟수가 정해져 있다. 그래서 for문은 1~100까지, 1번 학생~60번 학생까지의 경우처럼 미리 정해진 횟수만큼 반복할 때 가장 편리하다. 또한 문장안에 초기식, 조건식, 증감식이 포함되어 있어서 루프의 선두만 봐도 변수의 변화를 쉽게 파악하고 변경할 수 있다.

while문 : 루프를 계속할 조건만 있고 초기식이나 증감식이 없다. 아예 제어 변수라는 개념이 없으며 루프 내부에서 조건식의 진위 여부를 변경해야 한다. 그래서 while문은 반복 횟수가 가변적이다. 사용자의 입력이나 네트워크의 변화, 특정 신호의 입력 등 언제 발생할지 모르는 조건에 대해 반복할 때는 while문을 쓰는 것이 적합하다.

do~while : while문과 마찬가지로 제어 변수가 없고 반복 횟수가 가변적이지만 조건을 점검하는 시기가 다르다. while문은 루프로 들어가기 전에 조건을 점검하지만 do~while문은 일단 명령을 실행한 후 루프 계속 여부를 점검한다. 그래서 while문은 조건에 따라 한 번도 실행되지 않을 수도 있지만 do~while문은 최소한 한 번은 실행된다는 차이점이 있다. 요약하자면 while문은 선평가 후실행문이며 do~while문은 선실행 후평가문이다.

 

세가지 반복문은 상호 대체성이 있어서 for문 대신 while문을 쓸 수도 있고 while문 대신 do~while을 쓰는 것도 가능하다. 다음은 for문을 동일한 while문으로 변환하는 공식이다.

for (초기식;조건식;증감식) {

     명령;

}

초기식;

while(조건식) {

     명령;

     증감식;

}

 초기식을 먼저 실행하고 루프로 진입하며 매 명령을 실행할 때마다 증감식을 실행하면 while문으로도 for문과 똑같은 구조를 만들 수 있다. 물론 완전히 같지는 않아서 루프 내부에서 continue명령을 사용할 때의 효과가 약간 달라진다. 반대로 while (조건) 명령;도 for (;조건;) 명령; 형식으로 변환할 수 있다. 제어문에 따른 실행 속도나 코드의 크기는 거의 차이가 없으므로 실행 속도는 제어문을 선택하는 기준이 아니다.

하지만 어느 쪽이 더 효율적이고 코드의 가독성이 높은지, 부작용은 없는지를 비교해 보면 세가지 반복문 중 가장 적절한 것이 있을 것이다. 세가지 제어 구조의 특징을 잘 파악해야 상황에 가장 적절한 반복문을 선택할 수 있다. 1~100까지 숫자의 합계를 구하는 루프는 for문이 가장 적당하다. 반복 범위가 미리 정해져 있고 이 값이 루프내에서 사용되어야 하므로 제어 변수를 쓰는 것이 효율적이며 코드도 훨씬 더 짧고 명료하다.

정수를 입력받고 홀짝을 판별해서 메시지를 출력하는 일련의 코드를 do~while 루프로 감싸고 while의 조건문에 (i != 0)이라고 적으면 된다. 프롬프트를 출력하고 정수를 입력받은 후 홀짝 판별을 하는 코드 전체가 반복 단위임을 잘 파악해야 한다. 다음과 같이 반복 대상을 잘못 선택하면 엉터리로 동작하거나 차칫하면 무한 루프에 빠져 버릴 위험이 있다.

printf("정수를 입력하세요(끝낼 때는 0) : ");

do {

     scanf("%d",&i);

     if (i % 2 == 0) {

          printf("%d는 짝수입니다.\n",i);

     } else {

          printf("%d는 홀수입니다.\n",i);

     }

} while (i != 0);


printf("정수를 입력하세요(끝낼 때는 0) : ");

scanf("%d",&i);

do {

     if (i % 2 == 0) {

          printf("%d는 짝수입니다.\n",i);

     } else {

          printf("%d는 홀수입니다.\n",i);

     }

} while (i != 0);

 do~while문은 정수 하나를 입력받아 이 값의 홀짝을 판별한 후 i값을 평가해 보고 이 과정을 계속할 것인지 그만 둘 것인지를 결정한다. i가 0이 아니면 루프를 계속 실행하고 0이면 루프를 탈출한다. 따라서 0이 입력될 때까지 이 과정을 계속 반복할 것이다.


4) switch 문


다중 선택문이란 하나의 변수값을 평가하여 각 값에 대해 개별적인 처리를 지정할 수 있는 문장이다. 예를 들어 사용자가 입력한 숫자가 1일 때는 이렇게 하고, 2일 때는 저렇게 하고, 3일 때는 요렇게 하고 각각의 값에 대한 처리를 다르게 지정하고자 할 때 다중 선택문을 사용한다. 키워드 switch를 사용하며 기본 형식은 다음과 같다.

switch (변수) {

case 값1: break;

case 값2;

     // 값2에 대한 처리

     break;

}

switch문 다음의 괄호안에 평가할 변수를 적고 case문에 이 변수가 가질 수 있는 값과 이 값에 대한 처리 코드를 작성한다. case를 끝낼 때는 break문으로 switch 블록을 강제로 탈출해야 한다. case는 원하는만큼 얼마든지 작성할 수 있다. default는 case의 특별한 경우로 변수가 앞쪽 case에 있는 값 이외의 값을 가질 때의 처리를 지정한다.


5) 그 외 제어문

가) goto

goto문은 지정한 곳으로 무조건 점프하는 제어문이다. goto라는 말이 의미하듯이 조건없이 무조건 제어를 옮겨 버리기 때문에 사실 가장 사용하기 쉬운 제어문이다. goto로 제어를 옮길 지점은 레이블(label)이라는 것으로 표식을 단다. 블록의 끝만 제외하고 프로그램의 어느 곳에나 레이블을 배치해 놓고 goto 레이블명; 이라는 명령을 내리면 레이블 위치로 즉시 이동한다. 레이블보다 앞에서 뒤로 이동할 수도 있고 뒤에서 앞으로 이동할 수도 있되 단 함수 내에서만 이동할 수 있으며 다른 함수로는 점프할 수 없다.

레이블은 일종의 명칭이므로 명칭 규칙에만 맞으면 자유롭게 작성할 수 있다. 레이블 다음에 콜론(:)을 붙여 점프할 위치에 삽입해 놓기만 하면 된다. 다음이 goto문의 사용예이다.

here:
.....
.....
goto here;

점프하기를 원하는 곳에 here라는 이름으로 레이블을 붙여 놓고 goto here; 명령을 내리면 즉시 here 다음의 문장으로 이동한다. 다른 순환문과는 달리 복잡한 형식을 필요로 하지 않기 때문에 처음부터 제어 구조를 설계할 필요도 없고 언제든지 어느 곳으로나 제어를 옮길 수 있는 무척 간편한 명령이다.


나) break

break문은 이미 앞에서 여러 번 사용해 본 바 있다. 이 명령은 반복문이나 switch문 내에서 사용되며 루프를 강제로 벗어날 때 사용한다. for문이나 while문 내에서 break가 사용되면 조건식의 진위 여부에 상관없이 즉시 루프를 탈출하는데 우리말로 번역하자면 "당장 튀어 나와"라고 할 수 있다. 보통 if 조건문과 함께 사용되며 무한 루프에서 루프를 끝낼 조건이 되었을 때 break를 사용한다.

여러 개의 루프가 중첩되어 있는 다중 루프에서 break문이 사용되면 현재 루프 하나만 탈출한다. 다음은 이중 루프의 예이다.

for (i=...) {
     for (j=...) {
          break;
     }
}

다) continue

continue는 루프의 나머지 부분을 무시하고 조건 점검부로 점프하여 루프의 다음 값을 실행하도록 하는 명령이다. 루프를 돌던 중에 특정 조건에 대해서는 처리를 제외시키고자 할 때 이 명령을 사용한다. 루프의 조건을 다시 점검하도록 할 뿐이지 루프를 처음부터 다시 시작하는 것은 아니므로 제어 변수의 값은 그대로 유지되며 다음 증감문으로 이동한다.

for (i=...) {
     for (j=...) {
          continue;
     }
}

continue는 실전에서 그다지 자주 사용되지는 않으며 비교적 정밀한 제어 구조를 만들 때 가끔씩 사용된다.

출처: http://winapi.co.kr/

'old > C Grammer' 카테고리의 다른 글

기억 부류  (0) 2010.11.18
함수  (0) 2010.11.17
연산자  (0) 2010.11.16
변수  (0) 2010.11.12
현정누나 요약 C/C++  (0) 2010.08.17
Posted by jazzlife
,

변수

old/C Grammer 2010. 11. 12. 16:22

1-1. 변수의 정의

변수는 고정되어 있지 않은 수라는 뜻이며 1이나 45 또는 3.14같은 상수의 반대 개념이다. 이름은 변수이지만 반드시 수치값만 저장되는 것은 아니며 문자열이나 포인터같은 좀 더 복잡한 값도 저장될 수 있다. 변수에서 말하는 수(數)를 좀 더 일반적으로 표현하면 데이터(data)이며 한국말로 알아듣기 쉽게 설명하면 값이다.

80386이상의 CPU는 최대 4G까지의 메모리를 장착할 수 있다. 40억개나 되는 이런 기억 소자들에 대해 이놈, 저놈, 개똥이, 말똥이 같은 이름을 일일이 붙여줄 수는 없다. 그래서 컴퓨터는 연산 대상 메모리의 위치를 구분하기 위해 숫자로 된 번지(Address)라는 개념을 사용한다. 메모리를 구성하는 각 바이트들은 0부터 시작해서 40억까지의 고유한 번지를 가지고 있으며 이 번지를 대상으로 값을 읽거나 쓴다. 예를 들어 "1234번지에서 1237번지까지의 4바이트에 56을 기록한다" 는 식으로 동작한다.

그런데 번지라는 것은 사람의 입장에서 보면 굉장히 다루기 힘든 형태로 되어 있다. 번지는 값이 크기 때문에 통상 8자리의 16진수로 표현하는데 사람들이 흔히 쓰는 10진수를 쓰면 자리수가 가변적이어서 오히려 더 혼란스럽다. 16진수는 각 자리수의 형태가 비슷 비슷하기 때문에 사람이 그 위치를 정확하게 기억하기 힘들다. 예를 들어 어떤 값이 0x183c7eda번지에 저장되어 있다면 사람이 이 위치를 암기하여 사용하는 것은 거의 불가능하다. 이런 값을 10개만 사용해도 사람의 머리로는 각 값을 구분할 수 없다. 즉 번지라는 것은 전혀 인간적이지 않은 기계의 값이다.

그래서 번지를 직접 사용하는 대신 좀 더 기억하기 쉬운 변수를 사용한다. 숫자로 된 번지에 별도의 이름을 붙여 놓은 것이 바로 변수이다.



이렇게 이름을 붙여 놓으면 Num=56같이 간단하게 값을 기록할 수 있고 Result=Num같이 값을 읽기도 쉬워진다. 만약 변수라는 것이 없다면 두 값을 더해 다른 변수에 대입하고자 할 때 다음과 같은 식을 사용해야 할 것이다. 다음 표현식에서 [ ] 기호는 이 번지에 들어있는 값을 의미한다.

 

[0x3e98234a] = [0x3eff81a0] + [0x3d00aef6]

 

이런 숫자, 그것도 16진수의 나열보다는 salary=pay+bonus가 사람에게 훨씬 더 쉬우며 변수의 이름이 대상을 잘 표현할수록 더욱 쉬워진다. 컴파일러는 변수가 실제 어떤 번지를 가리키는지를 기억하고 있다가 이 변수를 사용하면 실제 번지에 값을 읽거나 쓰는 코드를 대신 작성해 준다. 흔히 변수는 값을 저장하는 상자에 비유되곤 한다. 상자에 물건을 넣어 두듯이 변수를 하나 만들어 놓으면 여기에 값을 저장하거나 다시 꺼내올 수 있다.

변수는 C언어의 구성 요소 중에 명칭으로 분류된다. 그래서 명칭을 작성하는 일반적인 규칙대로 자유롭게 작성할 수 있다. 키워드는 쓸 수 없고 영문, 숫자, 밑줄 문자로만 구성되어야 하며 대소문자를 구분한다. 이런 문법적인 제약 외에도 변수명을 작성하는 일반적인 몇 가지 법칙이 있다.

 

 의미를 잘 설명할 수 있는 이름을 주는 것이 좋다. 사람의 이름을 기억한다면 Name이라는 이름을 쓰는 것이 좋고 평균값을 기억하는 변수에는 Average, 위치를 기억한다면 Position 같은 이름을 준다. a, b, ttt 같은 의미없는 이름을 쓰는 것도 물론 가능은 하지만 변수의 수가 많아지면 변수끼리 구분하기 어려워진다.

 변수명의 길이에는 제약이 없지만 3~10자 내외로 작성하는 것이 좋다. Num, TotalScore, NowStage 등이 좋은 예이다. Highest_Score_Of_Today와 같이 변수명을 길게 쓰면 의미를 분명하게 나타낼 수는 있지만 변수명을 기억하기 어려워지고 오타를 입력할 가능성이 많아져 바람직하지 않다.

 대소문자 구성을 일관되게 하는 것이 좋다. C언어는 대소문자를 구분하므로 Score와 score는 다른 변수이다. 모두 소문자로 쓰든지 아니면 첫 문자만 대문자로 쓰든지 자신만의 규칙을 정하고 그대로 따르는 것이 좋다. 선언할 때는 Score라고 해 놓고 쓸 때는 score라고 쓰면 이 변수는 선언되지 않은 것으로 에러 처리된다.

 변수명은 보통 짧은 영어 단어를 활용한다. 한글 변수명은 사용할 수 없지만 원한다면 한글같은 변수명을 사용할 수는 있다. JumSoo, Saram, Juso 같은 식으로 작성해도 된다. 좀 어색하기는 하지만 영어 실력이 부족해서 이런 변수명이 더 기억하기 쉽다면 그렇게 하는 편이 좋다. 다른 변수와 구분 가능하고 의미를 쉽게 알 수 있으므로 명칭의 요건은 제대로 만족하는 것이다.

 

처음 실습을 할 때는 편의상 a, b, i같은 짧은 변수명을 즐겨 사용하지만 실무에서는 적절한 변수명을 붙이는 것이 아주 중요하다. 좀 쓸만한 프로그램을 만들어 볼려면 수백개나 되는 변수가 필요한데 이 변수들에 아무렇게나 이름을 붙여 놓으면 정말 힘들어진다. 더구나 여럿이 같이 하는 팀 프로젝트의 경우 내맘대로 성의없이 이름을 붙이면 팀원들에게 왕따를 당하는 수가 있으며 시간이 지난 후 자기 스스로도 코드를 파악하기 어려워진다. 간단 명료한 이름을 붙이는 것은 생각보다 훨씬 더 중요하며 또한 어려운 기술이기도 하다. 



1-2. 변수의 종

변수는 메모리의 위치를 기억한다고 했다. 그러나 실제로 컴파일러가 변수를 참조할 때는 메모리 번지를 참조하는 것이 아니라 번지에 기억된 값을 참조한다. 예를 들어 Num=56이라는 대입문이 있다고 하자. 컴파일러는 이 명령을 다음 둘 중 어떤 것으로 해석할까?

 

 Num이 가리키는 번지를 56으로 바꾸어라.

 Num이 가리키는 번지에 들어있는 값을 56으로 바꾸어라.

 

상식적으로 생각해 봐도 후자가 맞다. 변수는 내부적으로 번지에 대한 위치를 가리키지만 컴파일러는 변수를 번지에 기억된 값으로 참조한다. 즉, 변수의 실체는 번지가 아니라 그 번지에 기록된 값이다. 컴파일러가 변수를 통해 값을 참조하기 위해서는 단순히 메모리 위치만 알아서는 안된다. Num이 1234번지를 가리키고 있다고 할 때 "Num을 읽어라"는 명령을 내리면 1234번지부터 어디까지를 읽어야 하는 것인지를 알아야 하므로Num의 길이가 필요하다.

또한 읽은 값을 어떻게 해석할 것인가도 알아야 한다. 메모리에는 0과 1만 나열되어 있으므로 위치와 길이 정보만으로는 값의 형태를 파악할 수 없다. 1234번지에 들어있는 값이 정수인지 실수인지 또는 구조체인지 등의 정보가 있어야 컴파일러가 이 값을 정확하게 해석할 수 있다. 정수와 실수는 값의 형태가 다르며 메모리에 기록되는 방식도 당연히 다르다. 그래서 변수로부터 값을 정확하게 읽으려면 변수의 형태에 대해서도 알아야 한다.


변수는 저장된 메모리 위치와 함께 길이와 형태에 대한 정보를 가지는데 이런 변수의 특성을 타입(Type)이라고 한다. 타입이란 컴파일러가 변수를 읽고 쓰는 방법에 대한 정보이며 타입에 따라 Num은 정수형 변수이고 Average는 실수형 변수임을 알 수 있게 된다. C언어는 아주 많은 타입을 지원한다. 일단 어떤 타입들이 있는지 정리해 보자. 크게 기본형과 유도형으로 나누어진다.

 

구분

타입

설명

기본형

정수형

정수

문자형

문자

실수형

실수

열거형

가능한 값들에 대한 나열형

void

타입이 정해지지 않은 자료형

유도형

배열

같은 타입의 자료 집합

구조체

다른 타입의 자료 집합

공용체

메모리를 공유하는 자료 집합

포인터

대상체의 번지를 가리키는 타입

함수형

함수의 번지를 가리키는 타입

 

기본형이란 하나의 단일 값을 기억하는 단순한 타입이다. 정수값 하나를 기억하는 정수형이 가장 직관적으로 이해하기 쉽고 사용 빈도도 높다. 정수형 변수는 1234나 38같은 정수값 하나를 저장할 수 있고 실수형 변수는 3.14나 1.5 같은 실수값 하나를 저장할 수 있다. 기본형은 전부 수치를 저장한다는 점에서 공통적이다.

유도형은 기본형으로부터 만들어지는 타입이다. 같은 종류의 변수 여러 개를 모으면 배열이 되고 다른 종류의 변수 여러 개를 모으면 구조체가 된다. 유도형은 다소 복잡하기 때문에 이 장에서는 소개만 하고 관련 장에서 다시 상세하게 다룰 것이다. 각 타입별로 한 장 이상씩을 차지할 정도로 유도형은 복잡하다.


1-2. 변수의 선언
 

변수를 사용하려면 먼저 선언해야 한다. 선언(Declaration)이란 컴파일러에게 앞으로 이런 이런 이름을 가진 어떤 타입의 변수를 사용하겠으니 준비해 달라고 부탁하는 것이다. 예를 들어 Num이라는 이름으로 정수형 변수를 선언하면 컴파일러는 이 변수를 위해 정수형 하나를 저장할 수 있는 메모리 4바이트를 할당한다. 앞으로 이 메모리에 들어있는 값은Num이라는 변수 이름으로 참조하면 된다. 변수를 선언하는 기본 형식은 다음과 같다.

 

타입 변수명[=초기값][,변수명변수명, ...];

 

기본 형식에서 [ ] 괄호안에 있는 것은 모두 생략 가능하다는 뜻이며 ... 표기는 임의 개수만큼 반복될 수 있다는 뜻이다. 반드시 필요한 것은 타입과 변수명, 그리고 제일 끝에 있는 세미콜론밖에 없다. 변수 선언문도 문장이므로 끝에 세미콜론은 반드시 있어야 한다. 정수형 변수 Num을 선언하고 싶다면 다음과 같이 선언문을 작성한다.

 

int Num;

 

잠시 후에 다시 배우겠지만 정수형 타입은 int키워드로 표현한다. 이렇게 선언만 하면 Num을 위해 메모리가 할당된다. 그러나 할당만 되며 값은 초기화되지 않는데 C는 속도를 위해 특별한 지정이 없으면 변수값을 초기화하지 않는다. 성능을 최우선으로 하는 C의 특징을 잘 보여주는 대목이다. 이렇게 되면 변수는 할당된 번지에 원래부터 들어 있던 의미없는 값을 가지게 되며 이 값을 쓰레기값(Garbage)이라고 한다. 만약 선언과 동시에 어떤 값으로 초기화하고 싶다면 변수명 다음에 = 구두점을 쓰고 원하는 초기값을 뒤에 쓰면 된다.

 

int Num=12;

 

Num 변수가 선언됨과 동시에 이 변수가 12라는 값을 가지도록 초기화된다. C는 성능을 최우선으로 하기 때문에 명시적인 지정이 없으면 별다른 동작을 하지 않는다. 초기값이 필요없으면 선언만 하고 필요할 때만 초기값을 지정한다. 여러 개의 변수를 같은 타입으로 한꺼번에 선언할 수도 있는데 콤마로 구분된 변수명을 계속 나열하면 된다. 이때도 = 구분자로 그 중 일부를 초기화할 수 있다.

 

int a,b=3,c,d;

 

4개의 정수형 변수가 선언되며 이 중 b는 3으로 초기화된다. 물론 다음처럼 각각의 변수 선언을 별도의 명령으로 따로 선언해도 상관없다. 효과는 동일하지만 줄 수가 길어진다.

 

int a;

int b=3;

int c;

int d;

 

다른 타입의 변수는 한꺼번에 같이 선언할 수 없으며 각각 따로 선언해야 한다. 다음은 정수형 변수 두 개와 실수형 변수 하나를 선언한 것이다.

 

int angle, radius;

double ratio;

 

변수는 필요한만큼 얼마든지 선언할 수 있다. 앞장에서 실습했던 삼각형 그리기 예제를 보면 두 개의 정수형 변수 i와 j를 선언하고 있다.






3. 정수형

2-1. 정의

정수(Integer)란 부호는 있지만 소수점 이하를 표현하지 못하는 수이다. 0, -23, 156 이런 값들은 정수이며 1.28, 25.4 이런 값은 소수점 이하가 있으므로 정수가 아니다. 정수의 정의는 중학교 수학 수준에서 설명되는 것이므로 더 상세한 설명이 필요하지는 않을 것이다. 단, 컴퓨터의 메모리는 유한하기 때문에 수학적 정의와 같은 무한대의 범위를 지원하지 않는다는 정도만 다르다.

정수형이란 이런 정수값을 저장할 수 있는 타입이다. 컴퓨터라는 존재가 원래 정수적인 존재이고 실생활에서 가장 많이 사용되는 수이기 때문에 정수형 타입이 가장 흔하게 사용된다. 정수형 변수의 타입 이름은 Integer의 앞 세 자를 딴 int이다. 따라서 정수형 변수 i를 선언하려면 다음과 같은 선언문을 사용한다.

 

int i;

 

키워드 int 다음에 원하는 변수 이름 그리고 세미콜론으로 구성되어 있다. 컴파일러는 이 선언문을 만났을 때 정수값을 저장할만한 4바이트의 공간을 할당하고 이 공간에 대해 i라는 이름을 붙여줄 것이다. 이후 i라는 변수명을 통해 이 메모리에 정수값을 저장할 수 있고 또 값을 읽을 수도 있다. 정수형은 최대 표현 가능한 값의 크기와 부호의 존재 유무에 따라 여러 가지 종류로 나누어진다.

먼저 변수의 크기와 표현 가지수의 관계에 대해 알아보자. 값 하나를 표현하기 위해 몇 비트를 사용할 것인가에 따라 표현 가능한 수의 개수가 달라진다. 예를 들어 1비트로만 구성된 정수형이 있다면 이 정수로는 0과 1의 두 가지 상태밖에 기억하지 못한다. 이런 비트가 두 개 모이면 00, 01, 10, 11 네 가지 각각 다른 상태를 표현할 수 있다. 비트 세 개가 모인다면 각 비트값의 조합에 따라 다음 8가지 상태를 표현할 수 있다.

 

이진수

십진수

000

0

001

1

010

2

011

3

100

4

101

5

110

6

111

7

 

같은 원리로 비트가 4개 모이면 16가지 상태를 표현할 수 있을 것이다. 일반적으로 n개의 비트가 모이면 2n가지의 수를 표현할 수 있으며 0부터 시작하므로 최대 표현 가능한 수는 2n-1이 된다. 8비트로 구성되는 1바이트는 총 256가지 종류의 수를 표현할 수 있고 표현 가능한 최대 수는 255가 되어 0~255까지의 정수를 기억할 수 있다. 2바이트(16비트)라면 216종류의 값을 기억할 수 있고 4바이트(32비트)라면 232 종류의 값을 기억할 수 있을 것이다.

다음은 부호 여부에 따른 표현 범위의 차이를 보자. 부호가 있는 정수(signed)는 제일 왼쪽의 비트(MSB라고 한다)를 부호 비트로 사용하며 이 비트가 0이면 양수이고 1이면 음수가 된다. MSB를 부호 비트로 사용하면 값을 기억하는 비트 하나가 줄어들게 되므로 표현할 수 있는 최대값은 절반으로 줄어드는 대신 음의 값을 표현할 수 있다. 값의 범위가 음수 영역으로 평행이동하는 것이다.

8비트 정수의 경우 총 256가지의 수를 표현할 수 있는데 부호가 없으면 0~255까지의 표현 범위를 가진다. 부호가 있다면 음수 범위로 절반이 이동하여 -128 ~ 127까지의 수를 표현할 수 있을 것이다. 16비트 정수는 부호가 없을 때 0~65535까지, 부호가 있으면 -32768~32767까지를 표현한다. 결국 표현할 수 있는 가지수는 같지만 부호 여부에 따라 범위만 달라지는 것이다.

int라는 정수형의 타입앞에 크기를 나타내는 short, long같은 수식어와 부호를 나타내는 signed, unsigned같은 수식어가 붙어 다음과 같은 다양한 정수형 타입을 선언할 수 있다.

 

타입

크기(바이트)

부호

범위

int

4

있음

-2147483648~2147483647

short int

2

있음

-32768~32767

long int

4

있음

-2147483648~2147483647

unsigned int

4

없음

0~4294967295

unsigned short int

2

없음

0~65535

 

도표를 보면 각 타입별로 할당된 바이트 수와 부호 여부가 다른데 이 크기와 부호 여부에 따라 표현 가능한 수의 범위가 달라진다. 4바이트 크기의 unsigned int는 최대 42억이라는 큰 값을 기억할 수 있는데 비해 2바이트 크기의 unsigned short int는 65535까지만 기억할 수 있다. int형은 부호가 있고 4바이트의 크기를 가지므로 -231~231-1까지의 범위를 가지는데 비해 unsigned형은 같은 4바이트이지만 부호가 없기 때문에 음수를 표현할 수 없는 대신 0~232-1까지 표현할 수 있다. 타입의 이름이 좀 긴데 다음 두 가지 규칙에 의해 좀 더 간략하게 표현할 수도 있다.

 

① 부호에 대한 수식어가 생략되면 signed가 적용되어 부호가 있는 것으로 선언된다. signed int는 int와 같고 signed short int는 short int와 같다. 그래서 signed는 보통 붙이지 않는다.

② int앞에 수식어가 있을 경우 int는 생략할 수 있다. 그래서 unsigned int는 unsigned로 간단하게 쓸 수 있으며 long int는 long과 같다. 부호있는 4바이트 정수형은 signed int라고 쓰는 것이 원칙이나 signed를 생략하고 int로 쓸 수도 있고 int를 생략하고 signed로 쓸 수도 있다. 그러나 통상 signed라고 쓰지 않고 int라고 간략하게 쓰는 것이 보통이다.

 

똑같은 정수형에 대해서도 다양한 타입이 준비되어 있는데 이는 상황에 따라 가장 적절한 타입을 선택해서 쓸 수 있도록 하기 위해서이다. 년도나 온도를 기억한다면 2바이트의 short형으로도 충분하므로 굳이 기억 장소를 낭비해 가면서 4바이트나 차지하는 int형을 쓸 필요가 없다. 또한 나이나 성적같이 음수값이 존재하지 않는다면 부호가 없어도 상관없으므로 unsigned형을 쓰는 것이 더 좋다.

 

2-2. 정수형의 길이

C의 정수형, 더 정확하게 말해서 컴퓨터가 표현하는 정수라는 개념은 수학에서 말하는 정수와는 의미가 약간 다르다. 수학의 정수는 음양으로 무한대의 값을 표현할 수 있지만 유한한 메모리를 가진 컴퓨터는 이런 무한한 값을 표현하지 못하며 자신에게 할당된 메모리 양만큼의 값만 기억할 수 있다. 그래서 가끔 연산 결과가 용량을 넘어서는 경우가 발생하기도 하는데 다음 예제를 실행해 보자.

[overflow]

정수형 변수로 간단한 덧셈, 뺄셈을 해 보았는데 실행 결과는 다음과 같다.

 

20000+30000=-15536

20000-30000=55536

 

세 개의 2바이트 정수(short) a, b, c를 선언하고 a에 20000, b에 30000을 대입한 후 이 두값을 + 연산자로 더해 c에 대입했다. 그러면 c는 당연히 50000이라는 값을 가져야겠지만 실제 결과는 엉뚱하게도 -15536으로 출력된다. 왜냐하면 a, b, c 변수는 부호있는 2바이트의 정수형인 short형으로 선언되었고 최대 32767이상의 수를 저장할 수 없기 때문이다. 50000이라는 값이 대입되기는 하지만 short형은 최상위 비트를 부호 비트로 해석하기 때문에 음수가 되어 버리는 것이다. 이런 식으로 변수의 저장 용량을 넘어서는 현상을 오버플로우(Overflow)라고 한다.

이런 문제가 발생한 근본적인 원인은 만단위의 수치를 저장하는데 short형을 사용했다는데 있다. a,b,c를 unsigned short형으로만 바꾸어도 위 예제는 제대로 실행된다. 그러나 그렇게 하더라도 65535이상의 수를 저장할 수는 없다. 더 큰 수를 다루려면 int나 unsigned같은 4바이트의 더 큰 타입을 사용해야 한다. int는 20억 정도의 큰 수치를 저장할 수 있으므로 일반적으로 오버플로우 걱정을 하지 않아도 된다.

변수의 표현 범위를 초과하는 현상과 반대로 최소 표현수에 미치지 못하는 경우도 발생할 수 있다. s, t, u는 모두 부호를 표현하지 못하는 unsigned short로 선언되었으며 20000이라는 값을 가지는 s에서 30000이라는 값을 가지는 t를 빼서 u에 대입했다. u에 대입되는 값은 -10000이 아니라 55536이라는 양수값이 되어 버린다. unsigned short형이 표현할 수 있는 최소수는 0인데 이 값보다 더 작은 값을 대입했으므로 계산 결과가 틀려지는 것이다.

수학적인 연산을 할 때는 항상 이 점을 주의해야 한다. 아주 간단할 것 같은 연산도 정확한 타입과 함께 사용해야만 결과가 제대로 나온다. 메모리가 지극히 부족한 상황이 아닌 한은 정수가 필요할 때 부호 있는 4바이트 정수인 int를 사용하면 별 문제가 없다. int는 음양으로 20억이라는 실생활에서 거의 부족하지 않는 정도의 표현 범위를 가지고 있기 때문이다.

정수형 타입의 도표를 보면 int와 long은 크기나 부호 여부가 동일하며 따라서 표현할 수 있는 수의 범위도 완전히 동일하다. 왜 똑같은 타입을 둘 씩이나 정의해 놓았는지 의아하겠지만 이 둘은 엄밀하게 말하면 다른 타입이다. 아니, 다른 타입이라기 보다는 달라질 수 있는 타입이라고 하는 편이 옳을 것 같다.

C 언어의 타입 정의에 int 형은 "CPU의 레지스터와 동일한 크기를 가지는 타입"으로 정의되어 있다. 레지스터란 CPU내의 임시 기억 장소이며 레지스터의 크기에 따라 CPU의 비트 수를 정의한다. 즉, 레지스터가 16비트이면 16비트 컴퓨터, 32비트이면 32비트 컴퓨터라고 부른다. 비트 수가 높으면 높을수록 CPU가 한 번에 처리할 수 있는 자료양이 많아지므로 더 성능이 높다고 할 수 있다.

, int형은 CPU가 가장 효율적으로 다룰 수 있는 정수형으로 정의되어 있으며 그래서 int형의 실제 크기는 플랫폼에 따라 달라진다. 다음에 알아볼 포인터형도 마찬가지이다. 과거 8086이나 80286같은 16비트 CPU 시절, 윈도우즈 3.1같은 16비트 운영체제에서 int는 16비트였었다. 그러나 386이후의 CPU와 윈도우즈 95이후의 32비트 운영체제에서 int는 32비트이다. 64비트 CPU가 나오면(이미 나와 있다) 그때는 int형이 64비트(8바이트)가 될 것이다.

반면 long형은 그 크기가 4바이트로 고정되어 있어 어떤 플랫폼에서나 4바이트이다. int와 long이 동일한 크기를 가지는 것은 32비트 플랫폼에서 뿐이며 16비트에서는 서로 다른 타입이고 64비트에서도 달라질 것이다. 꼭 4바이트를 쓰고 싶으면 long형으로 선언하고 플랫폼의 환경에 따라 적절한 크기를 자동으로 선택하고 싶다면 int형으로 선언하면 된다.

최근 64비트 CPU가 발표되고 점점 더 큰 수를 다룰 일들이 많아지면서부터 C언어도 64비트의 정수를 지원하기 시작했다. 비주얼 C++과 Dev-C++은 __int64라는 타입을 지원하며 이 타입을 사용하면 무려 1800경(264)이라는 엄청난 수를 표현할 수 있다. 다음은 64비트 정수를 사용하여 억단위의 정수끼리 곱해본 것이다.

[int64]

흔히 전자 계산기를 테스트하기 위해 일련의 1을 곱해보는데 12345678987654321이라는 결과가 나오면 제대로 동작하는 것이다. printf로 64비트 정수를 출력하려면 %I64d라는 서식을 사용한다.


2-3. 정수형 상수

정수형 상수를 표기하는 방법은 아주 쉽고 상식적이다. 아라비아 숫자와 부호로 직접 그 값을 표기하면 된다. 다음이 정수형 상수의 예이다.

 

123

8906299

-86400

 

이렇게 값을 바로 표기하면 그 크기와 형태를 보고 적당한 타입으로 메모리에 저장된다. 위 예의 경우 차례대로 short, int, int의 타입을 가진다. 123은 2바이트로 표현할 수 있으므로 short형이면 충분하고 8906299는 2바이트의 범위를 넘으므로 int형이 되어야 한다. 만약 크기를 강제로 지정하고 싶으면 상수 뒤에 L(Long, 소문자로 써도 됨)을 붙이고, 부호없는 타입으로 지정하고 싶으면 U(Unsigned, 소문자로 써도 됨)을 붙인다.

예를 들어 28은 short형이지만 28L로 표기하면 4바이트의 long형 상수가 되고 123U는 부호없는 2바이트의 정수인 unsigned short가 된다. 만약 부호없는 4바이트의 상수로 표기하고 싶으면 순서나 대소문자에 상관없이 UL, LU, ul, lu 중 하나를 붙이면 된다. C/C++언어는 타입을 중요하게 생각하므로 상수에도 정확한 타입을 지정할 수 있도록 되어 있다. 정수형 상수를 별도의 표기없이 그냥 쓰면 10진수로 해석된다. 그러나 진법에 따라 다음 두가지 형식으로 상수를 표현할 수도 있다.

 

 8진수 : 0으로 시작하면 8진수로 인식되며 027, 032등과 같이 표현한다. 09같은 상수는 에러로 처리되는데 9라는 숫자는 8진수에서 쓸 수 없는 숫자다.

 16진수 : 0x 또는 0X로 시작하면 16진수이다. 0x12ab, 0x3f와 같이 표현한다. 16진수에서 10 이상의 값을 표현하는 A~F는 대소문자에 상관없이 아무 문자나 사용할 수 있다. 접두로 붙는 0x는 알파벳의 "오엑스"가 아니라 숫자 0과 알파벳 x이므로 "영엑스" 또는 "공엑스"로 읽는다.

 

보편적으로 10진수를 사용하지만 어셈블리와 관계있는 값이나 비트별로 의미가 다른 상수는 16진수로 표기하면 더 편리한 경우가 많다. 아쉽게도 C는 2진 상수 표기법은 제공하지 않으므로 2진수는 16진수로 바꾸어 표기해야 한다. 예를 들어 2진수 10100110은 16진수 0xa6으로 표기한다. 사실 8진 표기보다는 2진 표기법이 더 많이 사용되는데 베이직도 지원하는 2진 표기법이 C문법에 빠진 것은 데니스 리치의 실수가 아닌가 생각된다. 실제로 표준 위원회에 2진 표기법을 도입하자는 건의가 여러 차례 있었으나 익숙해지면 16진수로 암산 가능하다는 이유로 표준에서 제외됐다.

C에서 0으로 시작하는 상수는 8진수라는 것은 잘 알아 두어야 한다. 그렇지 않으면 다음과 같은 엉뚱한 실수를 할 수도 있다. HPNum에 10진수로 17을 대입하고자 했는데 전혀 문제가 없는 문장이기 때문에 컴파일러는 어떠한 에러 메시지도 출력하지 않는다.

 

int HPNum;

HPNum=017;

 

이렇게 하면 상수 017이 8진수로 해석되어 Num에 17이 대입되는 것이 아니라 15가 대입될 것이다. C 컴파일러는 017과 17을 엄연히 다른 상수로 취급하는데 0으로 시작하는 상수는 8진수라는 것을 잘 기억해 두도록 하자.

참고로 진법이란 수치값을 표기하는 다른 방법일 뿐이지 표현하는 값 자체가 다른 것은 아니다. 012나 10이나 0xa나 모두 십진수로 10(이진수로는 1010)을 나타내며 메모리에 기록될 때는 똑같은 값이다. 따라서 "10진수 값을 어떻게 16진수로 바꾸나요?" 이런 질문은 잘못된 것이다. 10진수를 16진수 형태로 출력한다거나 문자열 형태로 저장된 16진수를 10진값으로 구하는 방법을 질문하는 것은 옳지만 말이다.



4. 실수형

4-1. 종류
 

실수(Real Number)란 소수점 이하를 가지는 수이며 정수보다는 한 단계 더 확장된 범위를 포괄한다. 3.14나 57.4같이 정수부 다음에 소수점과 소수 이하의 소수부가 있다. 실수형 타입은 이런 실수를 저장하는 타입이며 C에서는 크기별로 다음 세 가지 종류가 제공된다. 실수 타입은 모두 부호를 가지고 있다.

 

타입

바이트 

범위

유효자리수(십진)

float

4

3.4*10-38~3.4*1038

7

double

8

1.7*10-308~1.7*10308

15

long double

10~16

1.2*10-4932~3.4*104932

19

 

float는 4바이트의 작은 실수형이며 double은 8바이트의 큰 실수형이다. 실수형의 값을 기억할 변수가 필요하다면 double d; 형식으로 선언하면 된다. long double형은 C언어 표준에는 있지만 비주얼 C++은 6.0과 7.0 모두 이 타입을 지원하지 않으며 double형과 동일하게 취급한다. gcc는 12바이트 크기의 long double형을 지원한다. 정수와 마찬가지로 실수도 수학에서의 실수와는 달리 무한대의 크기와 정밀도를 제공하지는 않으며 할당된 메모리 크기만큼의 크기와 정밀도만 표현한다.

컴퓨터는 원래 정수만 다룰 수 있기 때문에 실수를 기억하는 방법이 아주 독특하다. 만약 4바이트를 2바이트씩 나누어 정수부와 소수부를 따로 저장한다고 해 보자. 이렇게 단순한 방법을 사용하면 32767.00000 보다 더 큰 수를 표현하지도 못할 뿐더러 소수부도 기껏해야 소수점 이하 다섯 자리도 채 표현하지 못할 것이다. 그래서 실수는 부동 소수점이라는 좀 특이한 방법으로 저장한다. 부동(浮動) 소수점이란 실수를 정수부와 소수부로 나누는 것이 아니라 지수부와 가수부로 나누어 기억하는 방식이다.

가수부는 값의 모양을 표현하며 지수부는 10의 거듭승으로 값의 크기를 표현한다. 실수 123.456을 부동 소수점 형식으로 표현하면 1.23456*102으로 표현할 수 있으며 이를 공학적 표기법으로 바꾸어 1.23456E2로 표현하기도 한다. 이 예에서 가수는 123456이고 지수는 2이다. 정수부와 소수부를 기억하는 방식보다 부동 소수점 방식으로 실수를 기억하면 훨씬 더 큰 수를 표현할 수 있고 정밀도도 높아진다.

제일 왼쪽 비트(MSB)는 항상 부호 비트이며 이 비트가 0이면 양수, 1이면 음수이다. 지수부와 가수부의 크기는 float의 경우 8비트, 23비트이며 double형의 경우 11비트 52비트이다. 그래서 float보다는 double이 두 배의 크기를 가지는 대신 훨씬 더 큰 수를 정확하게 표현할 수 있다.

이런 실수 표현법은 C언어의 고유한 방식이 아니라 IEEE에서 제정한 국제 표준이며 이 표준은 모든 언어가 공통적으로 따르고 있다. 부동 소수점 수의 정확한 구조는 위에서 대충 설명한 것보다 조금 더 복잡한데 필요하다면 별도의 자료를 찾아 보기 바란다. 이 책의 18장에서 실수의 구조에 대해 대략적인 설명을 하고 있다.


4-2. 실수형 상수

소수부를 가지면 실수형 상수로 인식된다. 다음이 실수형 상수의 예이며 실생활에서 직접 사용하는 표기법이므로 전혀 어렵지 않을 것이다.

 

-3.14

123.456

5.0

 

5.0의 경우 같은 값이라도 5라고 적으면 정수형 상수가 된다. 실수형 상수임을 명확히 나타내려면 5.0이라고 적든가 아니면 0은 생략하고 5.이라고 적어야 한다. 이런 식으로 소수점을 기준으로 왼쪽에 정수부 오른쪽에 소수부를 적는 방법을 고정 소수점 표기법이라 한다. 실수형 상수 표기에 부동 소수점 표기법을 사용할 수도 있다.

 

3.14e-1

1.23456e2

 

e를 기준으로 왼쪽에 가수 오른쪽에 지수를 적는다. 실수는 내부적으로 모두 부동 소수점 방식으로 기억되지만 상수를 표현할 때는 고정 소수점, 부동 소수점 표기법을 모두 사용할 수 있다. 정수 상수 뒤에 크기와 부호를 나타내는 U, L 등의 접미사를 붙일 수 있듯이 실수형 상수 뒤에도 정확한 크기를 명시하는 F 접미사(소문자도 가능)를 붙일 수 있다. 접미사가 없으면 double형 상수로 인식되며 F를 붙이면 float형으로 인식되고 L을 붙이면 long double형이 된다. 다음 예제는 실수형 값을 출력하는 여러 가지 방법을 보여준다.


[float]

실수형 변수 d를 선언하고 이 변수에 123.456이라는 실수 상수를 대입했다. d=1.23456e2;와 같이 부동 소수점 표기법으로 대입해도 결과는 같다. 실수를 표현하는 방법이 여러 가지가 있기 때문에 printf의 실수에 대응되는 서식도 여러 가지가 있다. %f 서식은 고정 소수점으로 실수를 출력하며 %e 서식은 부동 소수점으로 출력한다. %g 서식은 %f와 %e 중 더 짧고 간단한 방법을 자동으로 선택한다. 실행 결과는 다음과 같다.

 

고정 소수점 : 123.456000

부동 소수점 : 1.234560e+002

일반형 : 123.456

 

앞 절에서 배운대로 %와 서식 사이에 총 자리수나 소수점 이하 자리수를 지정할 수도 있다. 소수점 이후 두 자리까지만 출력하고 싶다면 %f 대신 %.2f 서식을 사용하면 된다. 이 경우 소수점 이하 3번째 자리에서 반올림되어 123.46이 출력될 것이다.

실수는 정수에 비해 소수점 이하를 표현할 수 있고 천문학적인 큰 수를 다룰 수 있는 장점이 있지만 구조가 복잡하기 때문에 굉장히 느리다. 요즘 CPU는 부동 소수점을 보조 프로세서가 직접 처리하므로 훨씬 더 빨라졌지만 그렇지 못한 시스템에서는 정수에 비해 대략 10배 정도 느리다. 그래서 꼭 필요한 경우가 아니면 잘 사용되지 않으며 불가피한 경우라도 정수형으로 바꾸어서 다루는 경우가 많다. 예를 들어 소수점 이하 두 자리까지의 정확도를 가지는 백분율이 필요하다면 아예 100을 곱해 만분율을 사용하는 것이 더 유리하다.



5.  문자형
 

5-1. 문자

문자형이란 문자 하나를 표현하는 자료형이다. 컴퓨터는 원래 숫자밖에 모르기 때문에 문자도 숫자로 기억한다. 이때 어떤 숫자와 어떤 문자를 대응시키는가에 따라 여러 가지 인코딩 방식이 있는데 통상 아스키(ASCII) 코드 방식을 많이 사용한다. 아스키 코드는 0~255사이의 숫자에 문자를 대응시켜 놓았는데 이 표를 보면 대문자 A는 문자 코드 65(0x41)로 표현하며 숫자 1은 49(0x31)로 표현한다.

 

10

16

문자

10

16

문자

10

16

문자

10

16

문자

10

16

문자

0

0

Null

47

2F

/

68

44

D

89

59

Y

110

6E

n

7

7

Bell

48

30

0

69

45

E

90

5A

Z

111

6F

o

8

8

BS

49

31

1

70

46

F

91

5B

[

112

70

p

9

9

Tab

50

32

2

71

47

G

92

5C

\

113

71

q

10

A

LF

51

33

3

72

48

H

93

5D

]

114

72

r

13

D

CR

52

34

4

73

49

I

94

5E

^

115

73

s

32

20

공백

53

35

5

74

4A

J

95

5F

_

116

74

t

33

21

!

54

36

6

75

4B

K

96

60

`

117

75

u

34

22

"

55

37

7

76

4C

L

97

61

a

118

76

v

35

23

#

56

38

8

77

4D

M

98

62

b

119

77

w

36

24

$

57

39

9

78

4E

N

99

63

c

120

78

x

37

25

%

58

3A

:

79

4F

O

100

64

d

121

79

y

38

26

&

59

3B

;

80

50

P

101

65

e

122

7A

z

39

27

'

60

3C

<

81

51

Q

102

66

f

123

7B

{

40

28

(

61

3D

=

82

52

R

103

67

g

124

7C

|

41

29

)

62

3E

>

83

53

S

104

68

h

125

7D

}

42

2A

*

63

3F

?

84

54

T

105

69

i

126

7E

~

43

2B

+

64

40

@

85

55

U

106

6A

j

127

7F

Del

44

2C

,

65

41

A

86

56

V

107

6B

k

 

 

 

45

2D

-

66

42

B

87

57

W

108

6C

l

 

 

 

46

2E

.

67

43

C

88

58

X

109

6D

m

 

 

 

 

255개의 서로 다른 문자를 기억하기 위해서는 단지 1 바이트만 있으면 된다. 문자형 타입은 char인데 정수형과 마찬가지로 char형도 앞에 수식어를 붙일 수 있다. 길이는 1바이트로 고정되어 있으므로 long이나 short같은 크기에 대한 수식어는 붙일 수 없고 부호의 여부에 따라 unsigned, signed 수식어만 붙일 수 있다. unsigned char형은 8비트 길이를 가지므로 0~255까지를 표현할 수 있고 signed char 형은 1바이트의 좁은 공간에도 음수 표현을 위해 MSB를 부호 비트로 사용하므로 최대 표현 수가 절반으로 줄어든다.

 

타입

크기(바이트)

부호

범위

signed char

1

있음

-128 ~ 127

unsigned char

1

없음

0 ~ 255

 

부호 수식어없이 char라고만 쓰면 컴파일러와 설정 옵션에 따라 부호가 있을 수도 있고 없을 수도 있다. 대부분의 컴파일러들은 char형을 부호있는 타입으로 인식하므로 signed수식어는 생략할 수 있으며 signed char는 char 타입과 일단 동일하다. 컴파일러의 설정에 상관없이 부호 여부를 정확하게 지정하려면 signed, unsigned 수식어를 명시적으로 붙여야 한다. 문자 상수는 다음과 같이 표기한다.

 

'A', 'Z', '1', '&'

 

홑따옴표로 문자 하나를 감싸 주면 된다. 문자 상수는 아스키 코드값으로 해석되는데 'A'는 A문자의 아스키 코드값인 65와 같다. 다음 예제는 문자형 변수 ch를 선언하고 이 변수에 대문자 A를 대입한 후 출력해 본 것이다.


[char]

ch 변수는 char형으로 선언되었으므로 문자 하나를 저장할 수 있는 1바이트가 할당될 것이다. 이 변수에 'A', 'B' 같은 문자형 상수를 대입할 수 있는데 예제에서는 'A'를 대입하여 ch 변수에 'A' 문자를 기억시켰다. 문자형 상수를 쓰는 대신 ch=65(또는 ch=0x41)와 같이 아스키 코드값을 직접 대입해도 결과는 동일하다. 하지만 누가 보더라도 ch=65; 보다는 ch='A'가 훨씬 더 읽기 쉽다.

문자형 변수를 출력할 때는 putch 함수를 사용한다. 또는 printf 함수의 %c 서식을 사용하여 printf("%c",ch); 로 출력할 수 있다. 예제를 실행해 보면 화면에 A 문자만 하나 출력될 것이다. 한글은 한 음절이 2바이트로 구성되기 때문에 문자형 변수에는 저장할 수 없으며 잠시 후에 배울 문자열을 사용해야 한다.

문자형은 문자 하나를 저장할 수 있는 적당한 길이를 가진다는 뜻으로 붙여진 이름이지 오로지 문자만 저장할 수 있다는 뜻은 아니다. char 타입은 실제로 8비트의 정수형이므로 크기가 작은 정수를 저장하는 용도로도 사용할 수 있다. 예를 들어 0~100까지의 범위를 가지는 점수라면 Score라는 이름으로 char형 변수를 선언한 후 여기에 저장하면 적당하다. 문자형이 일종의 정수형이라면 이 변수에 들어 있는 값은 과연 어떻게 해석될까? 다음 예제를 보자.


[charcontext]

문자열 변수가 문자로 해석될 것인가 정수로 해석될 것인가는 이 변수가 사용되는 위치에 따라 달라지는데 문자가 올 수 있는 곳이면 문자로, 정수가 올 수 있는 곳이면 정수로 해석된다. printf 함수의 %c 서식과 대응되면 문자가 출력될 것이고 %d와 대응되면 정수값이 출력될 것이다. putch(ch)는 ch값을 문자로 해석하며 gotoxh(ch, 8)은 ch를 x 좌표를 나타내는 정수로 해석한다.


5-2. 확장열

문자 상수는 홑따옴표안에 문자를 써서 표기한다. 문자 Y에 대한 문자 상수는 'Y'다. 이 상수를 키보드로 입력하려면 키보드에서 '를 치고 Y를 치고 '를 치면 된다. 그런데 따옴표안에 직접 입력할 수 없는 문자가 있다. 대표적으로 개행 코드를 들 수 있는데 Enter키를 누르는 즉시 정말로 다음 줄로 내려가 버리기 때문에 따옴표안에 개행 코드를 담아서 표현하는 것은 불가능하다.

또한 문자 상수를 표현할 때 사용하는 홑 따옴표 구두점도 문자 상수로 바로 표현할 수 없다. ''' 이렇게 쓰면 두 번째 '가 닫는 따옴표인지 문자 '를 나타내는지 컴파일러가 구분할 수 없다. 그래서 키보드로 직접 입력할 수 없는 문자들은 좀 특수한 방법으로 표현하는데 이를 확장열(Escape Sequence)이라고 한다. 확장열은 백슬레쉬(\) 문자 다음에 기호 하나를 써서 표현한다.

 

확장열

코드

설명

\a

0x07

 소리

\b

0x08

 스페이스

\t

0x09

\n

0x0a

개행

\x##

0x##

16 코드

\###

0###

8 코드

\\

0x5c

백슬레쉬

\'

0x27

홑따옴표

\"

0x22

겹따옴표

\?

0x3f

물음표

 

개행 코드는 \n으로 표현하는데 이 코드는 First예제에서 이미 실습해 보았다. 탭이나 백 스페이스 같은 문자도 직접 키보드로 입력해서는 따옴표안에 표기할 수 없기 때문에 확장열로 표기해야 한다. 홑따옴표 문자 하나는 '''와 같이 표기할 수 없고 확장열을 사용해서 '\''와 같이 표기해야 한다.

확장열 표기에 \문자를 사용하기 때문에 \문자 자체도 확장열이 될 수밖에 없다. 16진 코드는 키보드에 없는 문자에 대해 코드를 직접 쓸 때 사용한다. 'A'는 '\x41'과 동일하다. 16진 코드를 확장열로 표기할 때 \다음의 x는 반드시 소문자로 써야 하며 대문자는 인정하지 않는다. 문자열 상수내에 16진 코드를 직접 쓸 경우 16진수로 인식되는 모든 문자를 확장열로 취급한다는 점을 주의하자. '\x53trike"는 \x53이 s이므로 "strike"이지만 "\53econd"는 \다음의 53ec까지를 16진수로 해석해 버리므로 "second"가 되지 않고 에러로 처리된다. 왜냐하면 53ec는 문자 코드 범위 바깥이므로 하나의 문자가 아니기 때문이다. 이런 경우는 16진 코드를 쓰지 않거나 다른 방법으로 문자열을 표기해야 한다.

확장열을 쓰는 이유는 꼭 키보드로 표현하지 못해서뿐만 아니라 환경에 따라 달라질 수 있는 코드를 논리적으로 표현하기 위해서이다. 개행을 하는 방식은 시스템마다 다른데 윈도우즈에서는 CR, LF의 조합으로, 유닉스는 LF만으로, 매킨토시는 CR만으로 개행 문자를 표현한다. C는 이런 방식에 상관없이 개행을 표현할 수 있는 확장열을 제공하고 프로그래머는 개행이 필요할 때 \n이라고만 적으면 된다.

탭의 경우 일정한 자리를 띄우는 것이 아니라 현재 위치에서 다음 탭 위치로 이동하는 기능을 하므로 소스상에 입력된 탭에 의해 띄워진 빈칸과 실제 출력될 위치의 탭 크기가 다를 수 있다. 그래서 탭키를 문자열 상수에 직접 쓰지 않고 \t 확장열로 표기하여 출력되는 상황에 맞게 탭을 적용하도록 한다.



5-3. 문자열

문자열(String)은 일련의 문자가 연속되어 있는 것이며 문자의 집합이 곧 문자열이다. 사람의 이름이나 에러 메시지 등과 같은 일반적인 문장을 문자열이라고 한다. 문자열 상수는 문자 상수와 달리 겹따옴표로 감싸서 표현한다.

 

"Korea", "문자열"

 

문자 상수는 홑따옴표로 표현하는데 비해 문자열 상수는 겹따옴표를 사용함을 분명히 구분해야 한다. 'S'와 "S"는 비슷해 보여도 내부적으로 엄청난 차이가 있다. 'S'는 S라는 문자 하나만을 표현하지만 "S"는 한 글자로만 된 문자열이다.

문자열 상수는 있지만 이런 문자열을 저장할 수 있는 문자열 타입이라는 것은 없다. 베이직이나 파스칼, 자바같은 언어는 별도의 문자열 타입을 제공하지만 C언어는 별도의 문자열 타입을 제공하지 않는다. 왜냐하면 문자열이란 문자형 변수의 배열로 표현할 수 있고 포인터와 함께 사용하면 훨씬 더 유연하게 활용할 수 있기 때문이다.

그러나 처음 배우는 사람에게 문자열 변수가 없다는 것은 상당히 불편한 점이다. 그래서 C보다 상위 언어인 C++은 string이라는 클래스를 제공하며 MFC 라이브러리에도 CString이라는 문자열 클래스가 정의되어 있다. 다음 코드는 str이라는 이름으로 문자 배열을 선언하고 이 배열에 "Korea"라는 문자열 상수를 저장한다.

 

char str[6]="Korea";

 

이 선언문에 의해 메모리에 다음과 같은 기억 공간이 할당되고 초기화된다.

배열이란 같은 타입의 변수를 모아 놓은 것이며 문자 배열이란 문자형 변수 여러 개가 연속적으로 배치되어 있는 것이다. 그림에서 각 격자는 1바이트의 문자들이며 각 격자에 문자 하나씩이 들어있다. 제일 끝에 있는 \0는 여기가 문자열의 끝임을 알리는 역할을 하며 널 종료 문자라고 한다. 메모리는 연속적인 공간이기 때문에 그 끝을 명확히 표시해야 한다.

문자열 상수를 쓰면 컴파일러가 상수의 끝에 널 종료 문자를 자동으로 붙여 준다. 그래서 "Korea"라는 다섯 글자를 저장하기 위해서는 널 종료 문자의 길이까지 고려하여 배열 크기를 6으로 선언해야 한다. 문자열을 화면으로 출력할 때는 printf 함수의 %s 서식을 사용하거나 아니면 puts같은 좀 더 간단한 함수를 사용한다.

문자열 상수내에서도 확장열을 모두 사용할 수 있다. 단, 문자 상수와는 다른 차이점이 한가지 있는데 홑따옴표는 그대로 써도 되나 겹따옴표는 반드시 확장열로 표기해야 한다는 점이다. "Let's go" 이런 문자열 상수를 쓸 수 있지만 "say "go" now" 이런 문자열 상수는 쓸 수 없다. 문자열 상수 표기에 겹따옴표를 사용하기 때문에 "say \"go\" now"와 같이 문자열 내의 겹 따옴표는 확장열로 표기해야 한다. 마찬가지 이유로 홑따옴표를 사용하는 문자 상수의 경우는 ' ' '가 안되며 ' " '는 가능하다.

문자열은 굉장히 기본적인 타입이지만 C언어는 별도의 문자열 타입을 제공하지 않고 배열로 문자열을 표현하도록 되어 있다. 그래서 문자열을 자유 자재로 다루려면 배열에 대해 익숙해져야 한다. 배열과 문자열의 관계는 다음에 배우기로 하고 여기서는 문자열은 문자 배열로 기억한다는 것과 문자열 상수의 표기법 정도만 알아 두고 넘어가도록 하자.


5-4. 3중 문자

3중 문자(Trigraph)란 세 개의 연속된 문자를 하나의 문자로 대체하는 표현이다. 잘 알다시피 C 언어는 미국에 의해 만들어졌고 그러다보니 자기네 나라의 문자 코드인 ASCII표에 정의되어 있는 { } [ ] 문자들을 별 생각없이 구두점으로 정의하고 있다. 그런데 유럽의 몇 개 국가들은 영어에 없는 자신의 고유한 글자(움라우트 등)를 표현하기 위해 이 문자 코드에 다른 문자를 할당해서 사용하고 있으며 그러다 보니 이런 나라의 키보드에는 ASCII표에 있는 9개의 문자들이 없을 수도 있다. 우리 나라의 경우도 \ 문자를 원화 표시인 \로 다르게 표시하고 있지 않은가?

그래서 C표준은 C에서 사용할 수 있는 문자들을 모든 나라에 공통적인 영문자, 숫자와 몇 가지 기호만으로 제한하고 있으며 나머지 문자들은 표준에 있는 문자들의 조합으로 입력할 수 있는 방법을 제공해야 할 필요가 생긴 것이다. 이런 이유로 C 표준은 3중 문자를 정의하고 있다. 물론 키보드에 이런 문자들이 있다면 굳이 3중 문자를 쓰지 않아도 된다. 3중 문자의 종류는 다음과 같다.

 

3 문자

대체 문자

3 문자

대체 문자

??=

#

??/

\

??'

^

??!

|

??(

[

??)

]

??<

{

??<

}

??-

~

 

 

 

??로 시작하는 세 개의 문자를 연속으로 사용하면 이 문자는 컴파일러가 토큰을 분석하는 과정에서 대체 문자로 바뀐다. 다음 예제는 3중 문자로 작성한 것이다. 굉장히 이상해 보이지만 잘 컴파일되며 Trigraph Test #1. num is 2를 출력한다. 

[Trigraph]

{ } 괄호 대신 ??< ??>괄호를 사용할 수 있고 배열을 표기할 때도 [ ] 대신 ??( ??)를 사용할 수 있다. 다소 혼란스러워 보이기도 하고 가독성이 심하게 떨어지기는 하지만 C표준에 의해 정상적인 소스로 인정된다. 재미있는 것은 3중 문자 치환은 구문 해석(Parsing) 전에 일어나므로 문자열 상수에 있는 3중 문자까지도 치환된다는 점이다. 그래서 문자열 내에서 부작용이 발생할 수 있으며 이 부작용을 해결하기 위해 확장열에 3중 문자의 선두로 사용되는 ?를 표기할 수 있는 \?가 따로 정의되어 있는 것이다.

이 외에도 C++ 표준은 연산자나 구두점을 위한 대체 토큰인 이중 문자(Alternative token)라는 것도 정의하고 있다. 예를 들어 <%는 {로 대체되며 %>는 }로 대체되는 식인데 비주얼 C++과 Dev-C++은 2중 문자를 지원하지 않는다. 키보드에 { } 문자가 없는 나라들은 3중 문자가 필요하겠지만 우리나라 키보드에는 이 문자들이 모두 있으므로 우리가 3중 문자에 관심을 가져야 할 이유는 전혀 없는 셈이다.



  
6. 열거형


6-1. 정의

열거형(Enumeration)이란 변수가 가질 수 있는 가능한 값들을 나열해 놓은 타입이다. 어떤 변수가 가질 수 있는 값의 종류가 일정한 범위로 정해져 있다면 정수형 대신 열거형을 쓰는 것이 더 편리하다. 열거형 변수를 선언할 때는 enum 키워드를 사용한다.

 

enum { 멤버멤버, ... } 변수;

 

열거형으로 가능한 값들을 열거 멤버라고 하는데 { } 괄호안에 값의 이름을 나열하면 된다. 구체적인 예를 들어 보자.

 

enum { EAST, WEST, SOUTH, NORTH } mark;

 

mark라는 변수는 방향값을 기억하는데 가능한 값은 동서남북 넷 중 하나이다. mark에 값을 대입하거나 비교할 때는 열거 멤버를 사용한다.

 

mark=EAST;           // mark에 EAST를 대입

if (mark==WEST)          // mark가 WEST이면

 

열거형은 내부적으로 정수로 처리되며 각 열거 멤버는 0부터 1씩 증가하는 정수값을 가진다. 위 예에서 EAST는 0이고 WEST는 1이고 SOUTH, NORTH는 각각 2와 3이다. 컴파일러는 열거형의 멤버들이 어떤 정수값을 가지는지 기억해 두었다가 열거 멤버를 만나면 실제값을 적용한다. 열거형은 정수를 직접 사용하는 방식에 비해 다음과 같은 장점이 있다.

 

① 기억이 용이하다. 열거형 대신 정수형 변수를 대신 사용할 수도 있으나 이렇게 되면 각 정수의 의미가 무엇인지 사람이 일일이 기억해야 하는 불편함이 있다. 값이 10개 정도 된다면 무척 혼란스러울 것이다. 부서를 기억하는 변수 part가 있다고 하자. 이 변수를 정수형으로 선언한다면 각 부서에 대해 0은 총무부, 1은 영업부, 2는 인사부, 3은 경리부, .... 10은 관리부 등으로 의미를 정하고 외워야 하지만 열거형을 사용하면 그럴 필요가 없다. 사람은 숫자보다 문자를 더 잘 기억한다. 10개 정도가 아니라도 LEFT, RIGHT 두 개만 있어도 사람은 무척 혼란스러워한다.

② 소스의 가독성(Readability)이 높아진다. 즉, 읽기만 하면 어떤 의도로 작성된 소스인지 쉽게 파악할 수 있다. mark=3이라고 되어 있으면 3이 무슨 뜻인지 바로 알기 어렵지만 mark=NORTH라고 되어 있으면 북쪽을 대입했다는 것을 분명히 알 수 있다. 팀 작업을 할 때는 이런 가독성이 아주 중요하다.

③ 열거형은 정수형보다 안전하다. mark에 대입될 수 있는 값은 4가지중 하나로 제한되며 동서남북중 하나만 대입될 수 있다. mark를 정수형으로 선언한다면 이 변수에 5를 대입할 수도 있지만 열거형은 가능한 값 중 하나만 대입할 수 있기 때문에 이런 어처구니없는 실수를 컴파일러가 허락하지 않는다.

 

열거 멤버는 별도의 지정이 없으면 0부터 시작하는 정수값으로 정의되며 이어지는 멤버는 앞 멤버의 값+1이 된다. 만약 열거 멤버의 값을 특정한 값으로 분명히 지정하고 싶다면 =다음에 원하는 값을 직접 적어준다.

 

enum { EAST=5, WEST=10, SOUTH, NORTH} mark;

 

이렇게 하면 EAST는 5의 값을 가지면 WEST는 10의 값을 가진다. SOUTH, NORTH는 별도의 값 지정이 없으므로 WEST다음의 정수인 11, 12의 값을 가진다. 다음 열거형은 요일 타입을 정의한다. 요일의 가능한 값은 월요일~일요일까지 일곱가지밖에 없으므로 열거형으로 선언하기에 적당하다.

 

enum { mon, tue, wed, thr, fri, sat, sun } day;

 

이렇게 선언하면 mon이 0이 되고 tue부터 차례대로 1~6까지의 값을 가질 것이다. C는 항상 0부터 수를 세지만 사람은 1부터 세는 것에 더 익숙하기 때문에 첫 번째 멤버의 값을 1로 바꾸는 것이 더 합리적인 경우가 많다. 이럴 때 enum {mon=1, ... 으로 선언하면 월요일부터 일요일까지 1~7의 값을 가지게 된다.

열거 멤버는 일종의 명칭이므로 다른 변수명과 중복되어서는 안되며 유일한 이름을 가져야 한다. 한 열거형내에서 열거 멤버끼리 중복되는 것도 물론 허용되지 않는다. 따라서 다음과 같이 열거형을 선언하는 것은 잘못된 것이다.

 

enum { man, woman, man } human;

 

man이라는 멤버가 두 번 중복되어 있는데 이렇게 되면 human=man이라는 대입문이 0을 대입하라는 것인지 2를 대입하라는 것인지 애매해지게 된다. 언어는 애매해서는 안되므로 이런 식의 열거형 선언은 허용되지 않는다. 하지만 열거 멤버의 값은 중복되어도 상관없다.

 

enum { man=1, woman=2, girl=2 } human;

 

woman과 girl이 똑같은 값을 가지고 있지만 애매하지는 않으므로 이것이 문제가 되지는 않는다. 이 경우 woman과 girl은 일종의 동의어로 처리되며 human=woman 대입문과 human=girl 대입문은 둘 다 똑같이 human에 2를 대입하게 된다. 열거 멤버는 내부적으로 정수형으로 처리되므로 음수를 사용하는 것도 물론 가능하다.






7. 유도형의 소개
 

유도형은 기본형의 조합에 의해 만들어지는 타입들이며 기본형 변수 여러 개를 모아서 또는 기본형을 약간 변형하여 만들어지는 타입들이다. 이절의 제목을 보면 알겠지만 상세한 이론은 다루지 않고 간단한 소개만 하기로 한다. 각각의 유도형들에 대한 문법은 분량도 방대하거니와 복잡하기 때문에 여기서 한꺼번에 다 알기는 어렵다. 각 주제들은 개별 장에서 다시 상세하게 다룰 것이므로 여기서는 개념 위주로만 알아보도록 하자.


7-1. 배열 

배열(Array)은 가장 흔한 자료 구조이면서 또한 가장 실용적이다. 돌(silicon)로 만든 돌머리인 컴퓨터는 판단이나 인식같은 것은 할 수 없으며 오로지 주어진 명령을 아무 생각없이 반복하는 것을 제일 잘한다. 배열은 이런 반복적인 작업과 아주 잘 어울린다. 구조가 단순하기 때문에 속도가 빠르며 이해하기 쉽고 사용하기도 쉽다.

우선 배열의 정의부터 문장화해 보면 동일한 타입을 가지는 자료들의 집합으로 정의된다. 동일한 타입이라는 뜻은 정수형이면 정수형끼리만, 문자형이면 문자형끼리만 모여야 배열이 된다는 뜻이다. 다른 타입들이 모이면 배열이 될 수 없으며 다음에 배울 구조체가 되어야 한다. 동일한 타입의 자료 여러 개가 집합을 이루어야만 배열이 될 수 있다.

정수형 변수 하나는 정수값 하나만 기억할 수 있고 실수형 변수 하나는 실수값 하나만 기억할 수 있다. 배열은 이런 개별 변수들을 여러 개 묶어서 하나의 이름으로 선언한 것이며 동종 자료의 집합을 표현할 수 있다. 배열을 구성하는 각 개별 변수들을 요소(Element)라고 한다. 배열은 다음과 같이 선언한다.

 

타입 배열명[크기][크기]...;

 

 타입 : 배열의 요소가 어떠한 값을 담는지를 지정한다. 즉, 배열이 어떤 값들의 집합인가를 지정한다. int, char, double 등의 기본 타입은 물론이고 유도형이나 사용자가 만든 타입도 가능하다.

 배열명 : 배열도 변수이므로 이름이 있어야 한다. 명칭 규칙에 합당하게 마음대로 이름을 작성할 수 있다.

 크기 : 요소의 개수가 몇개인가를 [ ] 괄호안에 정수 상수로 지정한다. 크기 지정이 하나만 있으면 1차원 배열이라고 하며 두 개 이상이면 다차원 배열이라고 한다.

 

다음은 배열을 선언한 예이다.

 

int array[5];           // 정수형 변수 5개의 집합인 배열 array를 선언

double rate[10];      // 실수형 변수 10개의 집합인 배열 rate를 선언

 

컴파일러는 배열 선언문을 만나면 요소의 크기만한 메모리를 개수만큼 연속적으로 할당한다. array는 정수형이므로 4바이트의 메모리 5개, 즉 20바이트가 할당될 것이며 rate는 80바이트가 할당될 것이다. 메모리에는 다음과 같이 array가 생성되며 이 그림에서 각 격자는 정수 하나의 크기인 4바이트이다.

이렇게 할당된 배열에서 요소를 참조할 때는 [ ] 괄호와 첨자(Index)를 사용한다. 첨자란 요소가 그 배열의 몇 번째에 있는지를 나타내는 순서값이다. C는 항상 0부터 수를 세기 때문에(Zero Base) 첫 번째 요소의 첨자는 0이 되며 마지막 요소의 첨자는 배열 크기보다 항상 하나 더 작다. int array[5]의 마지막 요소는 array[5]가 아니라 array[4]가 된다. 배열 요소를 이렇게 첨자로 참조할 수 있는 이유는 같은 배열에 속한 요소들은 모두 연속적인 메모리에 이웃하게 배치되기 때문이다. 그래서 첨자에 타입의 크기를 곱한 위치를 읽으면 쉽게 요소의 메모리를 액세스할 수 있다.

배열의 각 요소는 배열이라는 큰 집합의 일부분이라는 것 외에는 같은 타입의 변수와 완전히 동일한 자격을 가진다. array[3]이라는 요소는 정수형 변수이며 정수형 변수와 똑같이 사용한다. array[3]=123;과 같이 정수를 대입할 수도 있고 printf("%d",array[3]); 과 같이 값을 읽을 수도 있다. array가 정수형 변수를 모아 놓은 것이고 array[3]은 그 중 하나를 떼어 놓은 것이므로 완전한 정수형 변수인 것이다.

배열의 사용예를 보도록 하자. 학생 30명의 성적을 처리하는 프로그램을 작성한다고 해 보자. 30개나 되는 변수를 각각 따로 만들 필요없이 크기 30의 배열을 하나 선언하기만 하면 된다.

 

int Score[30];

 

이렇게 하면 Score라는 이름으로 30개의 성적을 저장할 수 있는 정수형 변수 집합이 생성된다. Score[0]부터 Score[29]까지 정수형 변수 30개가 연속적인 메모리 공간에 생성되는 것이다. 이 배열에 학생들의 성적을 입력받을 때는 다음과 같은 반복문을 사용한다.

 

for (i=0;i<30;i++)

     scanf("%d",&Score[i]);

 

for문은 반복적인 처리를 하는 문장인데 다음 장에서 배우게 될 것이다. 이렇게 입력된 성적을 가공하는 것도 아주 쉽다. 점수의 총합을 구하고 싶으면 Score[0]~Score[29]까지의 값을 더하기만 하면 된다.

 

Sum=0;

for (i=0;i<30;i++)

     Sum=Sum+Score[i];

 

Score라는 하나의 이름으로 성적 자료를 한곳에 모아 두었으므로 평균이나 분산, 최빈값 등을 구하는 것도 아주 쉽다. 뿐만 아니라 특정 학생의 성적을 조회한다거나 바꾸는 조작도 학생 번호를 첨자로 사용하면 간단하게 해결된다. 만약 배열이 없다면 반복을 할 수가 없고 다음과 같이 해야 할 것이다.

 

int Score0, Score1, Score2, Score3, ..... Score29;

scanf("%d",&Score1);

scanf("%d",&Score2);

scanf("%d",&Score3);

....

scanf("%d",&Score29);

 

필요한만큼 변수를 일일이 선언한 후 사용해야 하니 얼마나 끔찍한가? 30명 정도라면 이렇게 할 수 있겠지만 학생이 천 명정도 된다면 도저히 이런 방식으로는 자료를 처리할 수가 없을 것이다. 가끔 다음 코드가 동작할 것이라고 생각하는 순진한 사람도 있다.

 

for (i=0;i<30;i++)

     scanf(%d",&Scorei);

 

i를 0 ~ 29까지 반복하면서 Scorei를 참조하면 차례대로 Score0, Score1, Score2 변수를 사용할 것 같지만 컴파일러는 Scorei를 하나의 명칭으로 인식하므로 이렇게 되지는 않는다. 그래서 배열이 필요한 것이다. 배열은 하나의 이름으로 동일한 자료들의 집합을 다룰 수 있고 반복적인 처리가 가능하다는 점에서 아주 실용적인 타입이다.

배열의 크기값을 두 개 주면 2차 배열이 된다. 2차 배열은 두 개의 첨자를 가지며 요소를 참조할 때는 두 첨자를 밝혀야 한다. 마치 2차원 좌표 공간에서 한 점을 지정할 때 x, y 두 개의 좌표값을 주는 것과 같다. 다음은 학생과 과목번호를 첨자로 사용하는 2차 배열을 선언한 예이다.

 

int Score[3][10];

 

첫 번째 첨자가 과목의 번호이고 두 번째 첨자가 학생의 번호이다. 컴파일러는 이 선언문에 의해 Score라는 배열에 3*10개의 정수형 변수를 담을 수 있는 메모리(총 120바이트)를 할당한다. 이때의 메모리 모양을 그림으로 그려 보면 다음과 같을 것이다. 실제 메모리에는 선형적으로 배치되지만 2차 배열은 개념적인 도표(table)로 생각할 수 있으므로 일반적으로 표 형식으로 그리며 그렇게 생각하는 것이 더 편리하다.

이렇게 배열에 도표 형태로 성적값들이 저장되어 있다면 이 배열로 성적을 처리하는 것은 아주 쉽다.

 

1번 과목의 총합 : Score[1][0] ~ Score[1][9]까지의 합

5번 학생의 총점 : Score[0][5] ~ Score[2][5]의 합

전체 학생 전체 과목의 총점 : Score[0][0] ~ Score[2][9]까지의 합

 

이런 식으로 반복 처리하면 된다. 총점이 구해지면 평균이나 석차는 약간의 처리만 하면 쉽게 구할 수 있다. 3차 배열이나 4차 배열도 만들 수 있는데 메모리만 충분하다면 얼마든지 큰 배열도 가능하다. 배열에 대한 좀 더 자세한 내용에 대해서는 다음에 따로 배우게 될 것이다. 여기서는 배열이 동일 타입 변수의 집합이라는 정의와 반복적인 처리에 유리하다는 점에 대해서만 직관적으로 이해하도록 하자.


7-2. 구조체

동일한 타입의 집합인 배열과는 달리 구조체(Structure)는 서로 다른 타입의 집합이다. 이때 구조체에 속하는 개별 변수들을 멤버(Member)라고 한다. 정수형 변수와 실수형, 문자형 등의 기본형 변수뿐만 아니라 배열이나 구조체같은 큰 변수도 멤버가 될 수 있다. 구조체도 변수이므로 당연히 이름을 가져야 한다. 또한 구조체에 속하는 각 멤버들도 고유의 타입과 이름을 가진다. 구조체를 선언할 때는 struct라는 키워드를 사용하며 기본 형식은 다음과 같다.

 

struct {

          멤버 목록

변수명;

 

멤버 목록에는 일반 변수 선언문이 오는데 구조체 하나에 여러 개의 변수들을 포함할 수 있으므로 필요한만큼 멤버 변수를 선언하면 된다. 다음이 구조체의 예이다.

 

struct {

     char Name[10];

     int Age;

     double Height;

} Friend;

 

Friend 구조체는 친구 한명의 신상 정보를 가지며 이름, 나이, 키 등을 멤버로 가지고 있다. Name, Age, Height 등은 타입이 다른 변수들이지만 Friend라는 하나의 구조체 이름으로 모여 있다. 더 많은 정보를 저장하고 싶다면 주소, 성별, 전화번호, 취미, 몸무게 등의 멤버를 얼마든지 추가할 수 있다. 멤버는 선언된 순서대로 메모리에 할당된다.

Name 멤버가 10바이트, Age 멤버는 정수형이므로 4바이트, Height는 실수형이므로 8바이트를 차지할 것이다. 구조체 전체의 크기는 멤버들 크기의 총합과 같은데 Friend 구조체는 22바이트의 크기를 가진다. 구조체의 멤버를 참조할 때는 .(점)연산자를 사용하여 구조체.멤버 식으로 적으면 되는데 이때 . 연산자는 영어로 of, 한국어로 ~의로 해석하면 쉽게 이해할 수 있다.

 

Friend.Age=28;

printf("내 친구 이름은 %s이다",Friend.Name);

 

구조체 하나는 여러 개의 다른 타입 변수들을 포함할 수 있으므로 얼마든지 복잡한 정보를 표현할 수 있다. 도서 정보라면 책 제목, 저자, 출판사, 가격, ISBN 등의 정보를 가질 것이고 컴퓨터에 대한 정보라면 CPU, 하드 디스크, 메모리, 광학 드라이브, 모니터 크기 등의 정보들을 가지는데 이런 정보들을 하나의 구조체로 관리할 수 있다.

또한 이런 구조체 여러 개를 모아 구조체 배열을 만들면 복잡한 정보의 집합을 다룰 수도 있다. Friend 구조체는 한 사람에 대한 신상을 기억하는데 이런 타입의 배열을 만들면 주소록이 된다. Friend[100]의 형식으로 구조체 배열을 작성하면 최대 100명의 친구 신상을 기억시킬 수 있을 것이다.

공용체(Union)는 구조체와 유사하지만 멤버끼리 기억 공간을 같이 공유한다는 점이 조금 다르다. struct 키워드 대신 union키워드를 사용하며 초보자에게는 다소 어려운 개념이므로 여기서는 공용체라는 것도 있다는 것만 알아두고 자세한 사용 방법은 다음에 보도록 하자. 

7-3. 포인터

다음은 C의 데이터 타입중에 가장 이해하기 어려운 포인터에 대해 간략하게 소개한다. C 공부를 시작할 때 벌써 소문을 들어서 알고 있겠지만 C의 문법중에서 가장 어렵고 난해한 주제이면서 또한 C를 가장 강력한 언어로 만들어주는 일등 공신이기도 하다. 포인터를 직접 다룰 수 있기 때문에 C언어를 고급언어가 아닌 중급언어로 분류하며 어셈블리와 같은 수준의 시스템 프로그래밍까지도 가능하다.

정수형이나 실수형의 일반적인 변수들은 수치값을 저장한다. 이에 비해 포인터형은 번지를 기억한다는 면에서 일반적인 변수와는 조금 다르다. 데이터가 보관되어 있는 메모리 위치를 기억하고 있기 때문에 직접 값을 조작할 수도 있고 주변의 다른 값까지도 손델 수 있다. 또한 위치는 단순히 4바이트의 번지이기 때문에 함수의 인수로 전달하거나 받기도 효율적이며 함수로 포인터를 전달하면 포인터가 가리키는 메모리를 함수가 직접 조작하는 것이 가능하다.

포인터를 알려면 우선 번지(Address)의 개념에 대해 알아야 한다. 컴퓨터의 주 기억 장치로 사용되는 RAM은 용량이 아주 커서 보통 백만(M) 또는 10억(G) 단위를 사용한다. 컴퓨터에 실제 RAM이 얼마만큼 장착되어 있는가에 상관없이 32비트 운영체제 환경에서 각 프로그램은 32비트의 가상 메모리 공간을 사용할 수 있다. 즉 값을 기억할 수 있는 메모리의 용량은 최대 4G가 된다.

컴퓨터는 메모리의 위치를 구분하기 위해 순서대로 번호를 붙여 관리하는데 이 번호를 번지라고 한다. 최초의 시작 번지는 0번이고 순서대로 1번, 2번, 3번,... 순으로 번호가 매겨져서 40억까지 번지가 붙어 있다. 그래서 컴퓨터는 메모리 중의 특정 바이트를 지칭할 때 이 번지를 사용하여 정확한 위치의 값을 읽고 쓴다.

 

int Num;

 

이 선언문은 앞으로 Num이라는 이름의 정수형 변수를 사용하고 싶다는 뜻이다. 컴파일러는 이 선언문을 만났을 때 당장 사용되지 않는 메모리 공간 4바이트를 할당하고 이 번지에 Num이라는 이름을 붙여 준다. Num이 실제 어떤 번지에 할당될 것인가는 여유 메모리가 어디쯤에 있는지에 따라 달라지기 때문에 실행할 때마다 다르다. 설명의 편의상 1234번지에 할당되었다고 한다면 이때의 메모리 상황은 다음과 같을 것이다.

사용자는 Num 변수가 메모리의 어느 위치에 할당되어 있는가에 상관없이 Num=629; 이런 식으로 변수의 값을 읽거나 쓰기만 하면 된다. 컴파일러는 Num이 할당된 번지를 기억하고 있다가 이 변수를 참조하면 해당 메모리의 값을 읽거나 변경한다.

이때 Num으로 읽고 쓰는 것은 Num이 할당된 번지에 기억되어 있는 값이지 Num의 실제 위치인 번지가 아니다. 즉, C 컴파일러는 변수에 대한 참조문을 변수의 값에 대한 참조로 해석하며 변수의 번지를 참조하는 것이 아니다. 따라서 Num을 읽으면 629가 읽혀지며 1234가 읽혀지지 않는다. 만약 Num이 할당되어 있는 번지값을 알고 싶거나 직접 다루고 싶다면 이때 포인터라는 것이 필요하다. 포인터란 변수의 값이 아닌 변수가 저장되어 있는 메모리의 번지를 기억하는 타입이다. 포인터는 다음과 같이 선언한다.

 

타입 *변수명;

 

타입은 포인터가 가리키는 변수가 어떤 종류인가를 지정하며 int, char, double 등의 기본형과 배열, 구조체, 사용자 정의형 등의 모든 타입이 가능하다. 이 선언문에 사용된 *는 뒤쪽의 명칭이 포인터 변수임을 지정하는 구두점이다. 정수형 변수의 번지를 기억하는 변수 pi는 다음과 같이 선언한다.

 

int *pi;

 

이후 pi는 임의의 정수형 변수가 기억된 번지를 가질 수 있다. 다음 두 연산자는 포인터 변수와 함께 사용되어 번지와 관련된 연산을 한다.

 

* : 포인터가 가리키는 번지의 값을 읽는다.

& : 변수가 기억되어 있는 메모리 번지를 읽는다.

 

일단 두 연산자의 의미를 암기한 후 다음 예제를 작성해 보자. 

[pointer]

포인터의 개념을 설명하기 위한 아주 간단한 예제이다. 실행하면 "Num의 값은 629입니다."라는 문자열이 출력된다. int Num=629; 선언에 의해 4바이트의 메모리 공간이 할당되고 629라는 값으로 초기화된다. Num이 어떤 번지에 할당될 것인가는 알 수 없지만 설명의 편의상 1234번지에 할당되었다고 가정하자.

int *pi; 선언에 의해 정수형 변수의 번지를 가리킬 수 있는 포인터 변수 pi가 생성된다. 그리고 pi=&Num; 대입문에 의해 pi는 Num이 기억되어 있는 메모리 번지 1234라는 값을 가지게 될 것이다. &연산자를 변수명앞에 붙이면 변수의 값이 아닌 번지가 조사된다. 여기까지의 메모리 상황은 다음과 같다.

Num은 1234번지에 할당되어 있고 629라는 값을 가지고 있다. pi=&Num; 대입문에 의해 pi에 1234라는 번지값이 기억되는데 pi는 Num이 저장된 번지를 가리키고 있는 것이다. 이 상태에서 *pi로 값을 읽으면 Num 변수의 값이 읽혀진다.

*연산자는 포인터 변수가 가리키는 번지의 값을 읽는데 pi가 1234번지를 가리키고 있으므로 이 번지로 찾아가 그 값을 읽어온다. pi가 &Num의 값, 즉 Num의 번지값을 가지고 있는 상황에서는 *pi가 Num과 동일하다. Num에 어떤 값을 대입하는 문장 대신 *pi=값; 식으로 대입하는 것도 가능하다. 이 경우 pi가 가리키는 1234번지를 찾아가 값을 변경한다.

그렇다면 Num 변수를 바로 읽으면 되는데 왜 이렇게 포인터라는 간접적인 방법을 사용하는가? 한단계 더 중간 과정을 거치게 되면 그 중간 과정에서 많은 유용한 조작이 가능해지기 때문이다. 소프트웨어 공학에서는 융통성을 위해 중간 과정(전문 용어로 레이어라고 한다)을 삽입하는 경우가 아주 빈번하며 한 번 더 과정을 거침으로써 많은 것들이 가능해진다. 예를 들어 비디오 드라이버, 자바 가상 머신 등등이 레이어의 좋은 응용예이며 네트워크는 무려 7개의 레이어로 구성되어 있다.

위 예제는 실용적인 가치는 없지만 포인터의 개념을 이해하는데 많은 도움을 준다. 이 예제가 어떻게 실행되는지 잘 감이 잡히지 않는 사람을 위해 비슷한 예제 하나를 더 작성해 보도록 하자. 다음 예제는 포인터를 사용하여 간접적으로 값을 대입한다.

[pointer2]


이번에는 실수형 변수를 사용해 보았다. Num1, Num2 두 개의 실수형 변수를 선언하고 Num1에 3.14라는 상수를 기억시켜 놓았다. Num2는 초기화되지 않았으므로 쓰레기값을 가지고 있을 것이다. 실수형이므로 둘 다 8바이트의 메모리를 점유하고 있다. 이 변수들이 할당된 실제 번지는 알 수 없지만 편의상 Num1은 1234, Num2는 5678에 할당되었다고 하자.

실수형 포인터 변수 pd는 &Num1, 즉 Num1이 할당된 번지값을 대입받았으므로 그 값은 1234일 것이다. 이때의 메모리 상황은 다음과 같다.

pd가 Num1의 번지를 가리키고 있다. 이 상태에서 Num2=*pd; 대입문을 실행하면 다음과 같은 연산이 수행된다.

pd가 가리키고 있는 번지를 먼저 찾아 가고 *연산자로 그 값을 읽는다. 그리고 Num2에 값을 대입하였다. 결국 이 예제는 포인터를 통해 간접적인 연산을 하지만 결과적으로는 Num2=Num1; 이라는 대입 연산을 하고 있다. 이 예제를 이해하면 포인터의 개념과 *, & 연산자에 대해 이해했다고 할 수 있다. 이 예제는 *, & 연산자의 동작을 잘 설명하는 전형적인 예제인데 만약 이 두 연산자가 헷갈린다면 항상 이 예제를 생각해 보기 바란다.

이상으로 포인터 타입에 대한 간략한 소개만 했다. 포인터는 한 장을 다 할애해도 설명하기 힘들고 포인터만 다루는 전문 서적이 있을 정도로 어려운 개념이므로 현재 단계에서 크게 욕심 내지 말고 개념만 익히기 바란다. 누가 "포인터가 뭐야?" 라고 물으면 짧게나마 "포인터란 이런 것이야" 라고 대답할 수 있을 정도면 충분하다.

 

앞의 두 예제는 포인터의 기본 개념과 *, & 연산자의 동작을 이해하는데 아주 중요한 의미를 가지고 있다. 논리를 이해한 후 소스를 다시 복원해 보고 이왕이면 메모리속에서 일어나는 일들을 그림으로 그려 설명해 보자. 


7-4. 사용자 정의형

언어가 지원하는 데이터 타입이 아무리 풍부해도 프로그램의 특수한 요구를 다 수용할 수는 없다. 그래서 C는 기존 타입들로부터 사용자가 새로운 타입을 정의하는 방법을 제공한다. 사용자 정의형 타입을 만드는 기본 형식은 다음과 같다.

 

typedef 타입정의 타입이름;

 

형을 정의하는 것도 문장이므로 끝에 세미콜론을 반드시 붙여야 한다. 다음은 사용자 정의형 타입에 대한 몇 가지 예이다.

 

typedef int jungsoo;

typedef enum { True, False } Bool;

typedef int *pint;

typedef int arint[10];

typedef struct { int a; double b; } myst;

 

첫 번째 예는 별로 실용성은 없지만 jungsoo라는 이름으로 int 형에 대한 단순한 별명을 만든 것이다. jungsoo가 int와 똑같은 타입이 되었으므로 int i;라고 선언하나 jungsoo i;라고 선언하나 똑같은 변수가 만들어질 것이다. 두 번째 예는 True, False를 멤버로 가지는 열거형을 Bool이라는 이름의 사용자 정의형 타입으로 새로 만들었다. 이렇게 타입을 정의해 놓으면 다음부터는 Bool형의 변수를 언제든지 선언할 수 있다. 사용자가 만든 타입도 int나 double같은 기본형과 완전히 동일하며 기본형이 올 수 있는 모든 곳에 사용자 정의형도 올 수 있다.

 

Bool Male;

Male=True;

 

pint는 정수형 포인터 타입이며 arint는 크기 10의 정수형 배열 타입이다. 보다시피 타입이름이 반드시 끝에만 나오는 것이 아니라 조금 혼란스럽기도 하다. X형의 데이터 타입을 정의하는 방식은 요령만 알면 생각보다 간단하다. X형의 변수를 선언하는 문장에서 변수를 타입명으로 바꾸고 앞에 typedef키워드만 붙이면 된다.

사용자 정의 타입도 모든 면에서 기존 타입과 동등한 자격을 가진다. 사용자 정의형의 배열을 만들 수 있고 사용자 정의형 포인터도 가능하다. 사용자 정의형은 가독성을 높이는 효과가 있는데 주로 구조체같이 큰 타입에 대해 별도의 타입을 정의하여 사용하는 경우가 많다.


 

'old > C Grammer' 카테고리의 다른 글

기억 부류  (0) 2010.11.18
함수  (0) 2010.11.17
연산자  (0) 2010.11.16
제어문  (0) 2010.11.15
현정누나 요약 C/C++  (0) 2010.08.17
Posted by jazzlife
,