Joinc 팀블로그 리눅스 메뉴얼 정리 Joinc 위키
댓글

Recent Comments

Powered by Disqus
팀블로그 카테고리
  전체 (1105)
   공지사항 (1)
   검색엔진 (21)
   기술동향 (58)
   게임 (2)
   독서 (6)
   리눅스 (12)
   보안 (1)
   사회문제 (22)
   어셈블리 (43)
   영화 (3)
   오픈소스 (10)
   음악 (9)
   인물 (1)
   포인터 (4)
   프로그래머 (23)
   팀블로그 (20)
   테터툴즈 (29)
   C/C++ (152)
   FireFox (11)
   Gimp (2)
   Google (98)
   Java (13)
   Perl (2)
   Pthread (11)
   STL (13)
   TCP/IP (8)
   Tools (31)
   Web2.0 (42)
   Wiki (1)
«   2010/07   »
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
2007/09/11 19:37

리눅스 환경에서의 C 프로그래밍 14장 : End of Eva

마치며

어쨋든 입문서라고 할만한 리눅스 환경에서의 C 프로그래밍을 끝마쳤다. 그렇다고 해서 완전한 문서는 아니다. 글쓰는 스타일이 일단 빠르게 완성하고 나서, 나중에 다시보자 하는 스타일이기 때문에, 부족한점이 많을 거라 생각한다.

지금 언뜻 생각해도, 구조체 설명하면서 union을 빼먹었고, 매크로 문에 대한 설명등 빼먹은게 생각이 난다. 이 부분들은 차차 완성해 나갈 계획이다. 어쨋든 이걸로 리눅스 환경에서의 C 프로그래밍 Ver 1.0을 완성한 것으로 자축할 생각이다. 고생했다고 칭찬해줄? 여친이라도 있으면 굳이 자축할 필요도 없겠지만...

이제 이 문서는 조금씩 보강을 해서 완성도 있는 문서로 바꾸어나갈 생각이다.

이 다음에는 리눅스 시스템 프로그래밍에 대한 문서를 만들 생각이다. C 언어를 떼고 나면, 이걸 도대체 어디에 써먹을 수 있는지 고민하게 되는데, 시스템 프로그래밍이 이에 대한 길잡이가 된다고 보기 때문이다. 이전에도 시스템 프로그래밍 위키에서 그동안 다루었던 시스템 프로그래밍의 가지가지 요소를 정리해둔 문서가 있긴 하지만 단순 인덱스 수준일 뿐이니, 책 형식으로 새롭게 정리하는 것도 의미가 있는 일일 것이다.

그럼..

End of Ava

태그 :

최신 문서는 Joinc Wiki 에서.
:::
2007/08/20 19:39

리눅스 환경에서의 C 프로그래밍 12장 라이브러리

Hello World 다시 보기

hello world 프로그램을 다시 한번 보기로 하자.

#include <stdio.h>

int main(int argc, char **argv)
{
printf("Hello World!!!\n");
return 1;
}
위 프로그램을 자세히 뜯어보면, #include문이 보일 것이다. 이것이 어디에 쓰는 물건인지 자세히 알아보도록 할 것이다. 우리는 7장 함수편에서 함수에 대해서 다루었다. 이를 통해서 우리는 함수를 사용하기 위해서는 함수선언함수원형이 필요하다는 것을 알게 되었다. 함수를 사용하는 이유는 중복되는 코드를 따로 묶어 둠으로써, 코드관리를 쉽게 하기 위함이라는 것도 역시 알고 있다. 하지만 편하겠지라고만 알고 있을 뿐, 실제 어떻게 편하게 사용되는지는 경험을 해보진 못했다.

자.. 우리는 함수라는 것을 알고 있다. 그렇다면 어떻게 해야 함수를 더 편하게 사용할 수 있을까. 답은 함수를 위한 코드를 따로 분리시킨다 이다. 위의 Hello World 프로그램은 이러한 전형적인 모습을 보여주고 있다. printf 함수는 거의 모든 프로그램에서 필수적으로 사용되는 함수다. 이런 코드를 사용자가 필요할때 마다 일일이 사용하는건 여간 귀찮은일이 아닐 것이다. 그렇다면 printf 함수를 별도의 코드로 만들어서, 모듈형태로 만들어 두면 될것이다. 그래서 printf 함수가 필요할 때, 가져다 쓰기만 하면 된다.

그런데, 컴파일러는 printf 함수가 어떤 모습을 가지는지 알 수가 없다. 그러니까 리턴값이 무엇이고, 인자로 무엇이 사용되는 지를 알 수가 없다는 얘기가 된다. 그러므로 컴파일러에게 printf 함수의 정보를 알려줄 수 있어야 한다. 그게 #include 문이 하는 일이다. stdio.h는 표준입출력과 관련된 함수의 정보가 들어있는 파일로 헤더파일이라고 부른다. 이 헤더파일에는 printf 함수가 선언되어 있다. stdio.h 헤더파일은 /usr/include 디렉토리 밑에 존재한다.

이제 우리는 stdio.h 만을 include 시킴으로써, 어느 코드에서든지 간단하게 printf 함수를 사용할 수 있게 된다.

컴파일 과정

이제 위의 hello world 프로그램이 어떻게 컴파일이 되는지 알아보도록 하자. 프로그램의 이름은 hello.c 이다.
  1. 컴파일러는 hello.c 프로그램을 읽어 들인다.
  2. hello.c 코드를 해석해서, 기계어 형태의 object 파일로 만든다. object 파일은 컴퓨터가 해석할 수 있는 단위 모듈이다.
  3. 여기에 printf 함수가 정의되어 있는 이미 만들어져 있는 object 파일과 hello object 파일을 서로 링크(연결)한다.
  4. 완전한 실행 파일이 만들어 진다.

덧셈 함수를 가진 프로그램

그럼 덧셈 함수를 가진 계산기 프로그램을 만들어서, 모듈별로 작성하고 이것들을 object 파일로 만들어서 link 시켜서 실행파일을 만드는 방법에 대해서 알아보도록 하겠다.

모듈별로 작성하지 않고, 하나의 파일로 이루어진 프로그램은 아래와 같이 작성할 수 있을 것이다.
#include <stdio.h>
int sum(int a, int b);
int main(int argc, char **argv)
{
int value;
value = sum(120, 199);
printf("%d\n", value);
}
int sum(int a, int b)
{
return a+b;
}
아주 간단하며 문제없이 작성될 것이다. 또한 덧셈을 위한 sum 이라는 함수를 만들었으니, 이후 덧셈계산이 필요할 때 마다, 그냥 sum 함수를 그대로 쓰기만 하면 될것이다. 그런데 이 프로그램은 통짜로 작성되어 있기 때문에, 다른 프로그램에서 sum 함수를 사용할려고 하면 copy & paste 할 수 밖에 없다. 이것은 비효율 적이다. 이제 sum 함수를 모듈형식으로 완전히 분리 시켜 보도록 하자. 이를 위해서는 다음과 같이 3개의 파일이 만들어져야 한다.
  1. sum 함수의 선언이 들어 있는 include 파일
  2. sum 함수를 사용할 main 함수가 있는 C 파일
  3. sum 함수가 정의되어 있는 C 파일

include 파일은 아주 간단하게 만들 수 있다. include 파일의 이름은 sum.h 로 하겠다.
int sum(int a, int b);

이제 sum 함수가 정의되어 있는 C 코드를 만들어보자. 역시 간단하다. 파일이름은 sum.c로 하겠다.
int sum(int a, int b)
{
return a + b;
}

마지막으로 main 함수를 만들어 보자. 파일이름은 calc.c 로 하겠다.
#include "sum.h"
#include <stdio.h>

int main()
{
int value;
value = sum(130, 199);
printf("%d\n");
}

include 는

헤더파일의 경로

우리는 #include 키워드를 이용해서, 포함시킬 헤더파일을 지정할 수 있다. 이때 헤더파일의 완전한 경로를 포함시켜 줘야 한다. 만약 따움표를 사용했다면, 이는 현재 디렉토리에서 헤더파일을 찾겠다는 것을 의미한다. 그렇지 않고 <> 를 사용했다면, 표준 Include 디렉토리와 컴파일러 옵션을 통해서 지정된 디렉토리에서 찾게된다. 유닉스 시스템의 경우 /usr/include 가 표준 Include 파일이 된다.

헤더파일을 찾을 경로의 지정은 -I옵션을 이용하면 된다. 예를 들어 /home/yundream/include 에서 찾도록 하길 원한다면
# gcc -I/home/yundream/include -o sum sum.c
와 같이 하면 된다.

만약 /home/yundream/include 를 헤더파일 찾기 경로로 지정하고 싶다면, 다음과 같이 하면 된다.
#include "/home/yundream/include"


모듈별 분할 컴파일

자 이제 sum.h, sum,c, calc,c 3개의 파일이 만들어졌다. 이 3개의 파일을 컴파일해서 실행가능한 프로그램을 만들어보자.

위에서 언급되었듯이 가장 먼저 해야할일은 sum.c 와 calc.c 를 기계어가 해석가능한 object 코드로 만드는 일이다. 오브젝트 코드는 gcc에 -c 옵션을 이용해서 만들어낼 수 있다.
# gcc -c sum.c calc.c
이제 sum.o 와 calc.o 라는 파일이 만들어진걸 확인할 수 있을 것이다. 확장자 .o는 이 파일이 오브젝트 파일이라는 것을 알려준다. 이제 두개의 object 파일을 링크시켜서 실행파일을 만들면 된다. -o 옵션을 이용하면, 만들어진 오브젝트 파일들을 합쳐줄 수 있다.
# gcc -o calc sum.o calc.o
이제 실행파일인 calc가 만들어졌다.

이렇게 만들어진 object 파일은 기계어로 만들어져 있기 때문에, 이후에 사용할때는 sum.c를 다시 object 파일로 컴파일할 필요가 없다. 그냥 sum.o 프로그램에 링크시켜주기만 하면 된다. 다음과 같은 프로그램을 만들어보자. 프로그램의 이름은 mycal.c로 하자.
#include "sum.h"
#include <stdio.h>

int main(int argc, char **argv)
{
int value;
int a;
int b;
if (argc != 3)
{
printf("Usage : %s num1 num2\n", argv[0]);
return 1;
}

a = atoi(argv[1]);
b = atoi(argv[2]);
value = sum(a, b);
printf("%d + %d = %d\n", a, b, value);
return 0;
}
이 프로그램은 첫번째 프로그램보다 더 진보된 프로그램으로, 프로그램의 명령행 인자로 받아들인 숫자를 더할 수 있도록 되어 있다. atoi(3)는 문자열을 int형 숫자로 변환해주는 함수다. sum 함수는 이미 컴파일 되어서 object 파일로 만들어져 있으므로, 별도로 컴파일할 필요가 없다. 다음과 같은방법으로 실행파일을 만들 수 있다.
# gcc -c mycal.c
# gcc -o mycal sum.o mycal.o

4칙연산 프로그램

위의 프로그램은 덧셈만을 지원하고 있다. 여기에 덧붙여 뺄셈, 나눗셈, 곱샘까지 지원하는 프로그램을 만들어 보도록 하자. 각각의 연산은 모두 함수로 작성되며, 각각의 함수가 헤더파일과 함수가 정의된 C 코드 파일을 가지게 될 것이다. 그렇다면, 이 프로그램은 4칙연산을 위한 4개의 함수와 4개의 헤더파일 1개의 main 함수를 포함하는 C 파일로 구성될 것이다.
  • 헤더파일 : sum.h, sub.h, mul.h, div.h
  • 함수정의된 C 파일 : sum.c, sub.c, mul.c, div.c
  • main 함수파일 : simplecal.c

먼제 헤더파일을 작성해보자.
sum.h
int sum(int a, int b);

sub.h
int sub(int a, int b);

mul.h
int mul(int a, int b);


div.h
int div(int a, int b);

이제 함수의 정의를 담고 있는 4개의 C 소스코드 파일을 만들어야 한다.

sum.c
int sum(int a, int b)
{
return a + b;
}

sub.c
int sub(int a, int b)
{
return a - b;
}

mul.c
int mul(int a, int b)
{
return a * b;
}

div.c
int div(int a, int b)
{
return a / b;
}

이제 main 함수를 가진 코드를 만들면 된다.
#include "sum.h"
#include "sub.h"
#include "mul.h"
#include "div.h"

int main(int argc, char **argv)
{
int a = 1200, b=25;

printf("sum : %d\n", sum(a, b));
printf("sub : %d\n", sub(a, b));
printf("mul : %d\n", mul(a, b));
printf("div : %d\n", div(a, b));
}

코드를 만들었다면, gcc를 이용해서 object를 만들고 이것들을 링크시켜서 실행파일로 만들면 된다.
# gcc -c sum.c sub.c mul.c div.c simplecalc.c
# gcc -o simplecalc sum.o sub.o mul.o div.o simplecalc.o

라이브러리

이렇게 단위 함수를 별개의 소스코드와 헤더파일로 나누어서 관리하게 되면, object 혹은 단위 소스코드 파일을 재활용할 수 있다는 장점을 가진다. 그러나 여전히 불편한 점이 있다. 함수가 많아지면, 자칫 수십개의 오브젝트 파일이 생성될 수 있을건데, 이들을 관리하는건 매우 귀찮은 일이기 때문이다.

그렇다면 4개의 object 파일을 하나로 묶을 수만 있다면, 함수들을 더 편리하게 관리할 수 있을 것이다. 이렇게 오브젝트들을 하나의 파일로 다시 묶은 것을 라이브러리(library)라고 한다.

라이브러리는 다시 정적 라이브러리공유 라이브러리로 나뉜다. 정적라이브러리는 실행파일에 완전히 포함되어버리는 형식의 라이브러리를 말한다. 반면 공유 라이브러리는 실행파일에 포함되지 않고, 실행될때 해당 라이브러리를 불러오는 형식의 라이브러리를 말한다.

정적라이브러리

static library라고 부르기도 한다. 이 라이브러리는 단순한 오브젝트의 모음일 뿐이다. 정적라이브러리는 ar이라는 프로그램을 통해서 만들 수 있다. 그럼 ar을 이용해서 위의 사칙연산을 위한 4개의 오브젝트를 모아서 libmycalc.a라는 이름의 정적라이브러리를 생성해보도록 하자. rc 옵션을 이용하면, 정적라이브러리를 만들 수 있다.

r은 정적라이브러리를 만들겠다는 옵션이고, c는 새로 생성을 하겠다는 옵션이다.
# ar rc libmycalc.a sum.o sub.o mul.o div.o
libmycalc.a 라는 파일이 생성된걸 확인할 수 있을 것이다. t 옵션을 이용하면, 해당 라이브러리가 어떤 오브젝트를 포함하고 있는지도 확인할 수 있다. t 옵션을 사용하면 된다. 참고로 정적 라이브러리의 이름은 libNAME.a의 형식을 따라야 한다.
# ar t libmycalc.a
div.o
mul.o
sum.o
sub.o

그럼 정적라이브러리를 이용해서 실행파일을 만들어 보도록 하자. 이전에는 4개의 오브젝트 파일을 모두 링크시켜줘야 했지만, 이제는 libmycalc.a 만 링크시켜주면 된다.

라이브러리의 링크방식은 오브젝트를 링크하는 것과는 약간 차이가 있다. library의 위치를 명확히 명시해 주어야 한다. -L 옵션을 이용해서 라이브러리가 있는 디렉토리의 위치를 명시해주고, -l옵션을 이용해서, 라이브러리 파일의 이름을 정해줘야 한다. 다음은 simplecalc.c 를 정적라이브러리를 이용해서 컴파일하는 방법을 보여준다.
# gcc -o simplecalc simplecalc.c -L./ -lmycalc
-L./은 현재 디렉토리를 라이브러리 찾기 디렉토리로 하겠다는 의미가 된다. -l 옵션뒤에 붙이는 라이브러리 파일의 이름에 주목할 필요가 있다. 라이브러리 이름은 lib.a를 제외한 이름을 사용한다.

공유 라이브러리

공유 라이브러리는 함께 사용하는 라이브러리라는 의미다. 즉 정적 라이브러리 처럼 실행파일에 붙는 것이 아니고, 시스템의 특정디렉토리에 위치하면서, 다른 모든 프로그램들이 공유해서 사용할 수 있게끔 제작된 라이브러리다. 그러므로 공유 라이브러리를 사용하도록 제작된 프로그램은 실행시에 사용할 라이브러리를 호출하는 과정을 거치게 된다.

공유 라이브러리역시 오브젝트를 이용해서 만든다는 점에서는 정적라이브러리와 비슷하지만, 호출시에 링크하기 위한 부가적인 정보를 필요로 하므로, 정적라이브러리와는 전혀 다른 형태로 만들어 진다. 정적라이브러리와 이름이 헛갈릴 수 있으니, 라이브러리 이름은 mycalcso 로 하겠다.
# gcc -fPIC -c sum.c sub.c mul.c div.c
# gcc -shared -W1,-soname,libmycalcso.so.1 -o libmycalcso.so.1.0.1 sum.o sub.o mul.o div.o

  1. 오브젝트 파일을 만들때 부터 차이가 있는데, -fPIC 옵션을 줘서 컴파일 한다.
  2. 그다음 -shared 옵션을 이용해서 공유라이브러리 파일을 생성한다.
위의 과정을 끝내고 나면, libmycalcso.so.1.0.1 이라는 파일이 생성이 된다. 이 라이브러리는 프로그램을 컴파일할때와 실행시킬때 호출이 되는데, 호출될때는 libmycalcso.so 를 찾는다. 그러므로 ln 명령을 이용해서 libmycalcso.so 링크파일을 생성하도록 하자.
# ln -s libmycalcso.so.1.0.1 libmycalcso.so
이렇게 링크를 만들게 되면, 여러가지 버전의 라이브러리 파일을 이용할 수 있으므로 관리상 잇점을 가질 수 있다. 새로운 버전의 라이브러리가 나올 경우, 오래된 버전의 라이브러리를 쓰는 프로그램은 실행시 문제가 발생할 수 있는데, 이런 문제를 해결할 수 있기 때문이다.

이제 링크하는 과정이 남았다. 링크과정은 정적 라이브러리를 사용할때와 동일하다.
# gcc -o simplecalcso simplecalc.c -L./ -lmycalcso

이제 프로그램을 실행시켜 보도록 하자. 아마 다음과 같은 에러메시지를 만나게 될 것이다.
# ./simplecalcso
./simplecalcso: error while loading shared libraries: libmycalc.so:
cannot open shared object file: No such file or directory

이러한 에러가 발생하는 원인에 대해서 알아보도록 하자. 정적라이브러리는 실행파일에 라이브러리가 붙여지므로, 일단 실행파일이 만들어지면, 독자적으로 실행이 가능하다. 그러나 공유라이브러리는 라이브러리가 붙여지는 방식이 아니고, 라이브러리를 호출해서 해당 함수코드를 실행하는 방식이다. 그러므로 공유라이브러리 형식으로 작성된 프로그램의 경우 호출할 라이브러리의 위치를 알고 있어야만 한다.

위의 simplecalcso 프로그램을 실행시키면, 이 프로그램은 libmycal.so 파일을 찾을 것이다. 이때 파일을 찾는 디렉토리는 /etc/ld.so.conf에 정의 되어 있다.
# cat /etc/ld.so.conf
/usr/lib
/usr/local/lib
만약 위에서 처럼되어 있다면, 프로그램은 /usr/lib 와 /usr/local/lib 밑에서 libmycal.so 를 찾게 될 것이다. 그런데 libmycal.so 가 없으니, 위에서와 같은 에러가 발생하는 것이다.

가장 간단한 방법은 라이브러리 파일을 ld.so.conf에 등록된 디렉토리중 하나로 복사하는 방법이 될 것이다. 혹은 환경변수를 이용해서, 새로운 라이브러리 찾기 경로를 추가할 수도 있다. 이때 사용되는 환경변수는 LD_LIBRARY_PATH 다.
# export LD_LIBRARY_PATH=./:/home/myhome/lib
이제 프로그램을 실행시키면 LD_LIBRARY_PATH 에 등록된 디렉토리에서 먼저 검색하게 되고, 프로그램은 무사히 실행 될 것이다.

공유라이브러리와 정적라이브러리의 장단점

이들 2가지 라이브러리의 장단점에 대해서 알아보도록 하자. 장단점을 알게되면 어떤 상황에서 이들 라이브러리를 선택할 수 있을지 알 수 있을 것이다.

정적라이브러리의 장점은 간단한 배포방식에 있다. 라이브러리의 코드가 실행코드에 직접 붙어버리는 형식이기 때문에, 일단 실행파일이 만들어지면 간단하게 복사하는 정도로 다른 컴퓨터 시스템에서 실행시킬 수 있기 때문이다. 반면 동적라이브러리는 프로그램이 실행될때 호출하는 방식이므로, 라이브러리까지 함께 배포해야 한다. 라이브러리의 호출 경로등의 환경변수까지 덤으로 신경써줘야 하는 귀찮음이 따른다.

일반적으로 정적라이브러리는 동적라이브러리에 비해서 실행속도가 빠르다. 동적라이브러리 방식의 프로그램은 라이브러리를 호출하는 부가적인 과정이 필요하기 때문이다.

정적라이브러리는 실행파일 크기가 커진다는 단점이 있다. 해봐야 얼마나 되겠느냐 싶겠지만, 해당 라이브러리를 사용하는 프로그램이 많으면 많을 수록 X 프로그램수만큼 디스크 용량을 차지하게 된다. 반면 공유라이브러리를 사용할 경우, 라이브러리를 사용하는 프로그램이 10개건 100개건 간에, 하나의 라이브러리 복사본만 있으면 되기 때문에, 그만큼 시스템자원을 아끼게 된다.

마지막으로 버전 관리와 관련된 장단점이 있다. 소프트웨어 개발 세계의 불문율이라면 버그 없는 프로그램은 없다이다. 어떠한 프로그램이라도 크고작은 버그가 있을 수 있으며, 라이브러리도 예외가 아니다.

여기 산술계산을 위한 라이브러리가 있다. 그리고 정적 라이브러리 형태로 프로그램에 링크되었어서 사용되고 있다고 가정해보자. 그런데 산술계산 라이브러리에 심각한 버그가 발견되었다. 이 경우 산술계산 라이브러리를 포함한 A 프로그램을 완전히 새로 컴파일 해서 배포해야만한다. 문제는 이 라이브러리가 A 뿐만 아니라 B, C, D 등의 프로그램에 사용될 수 있다는 점이다. 결국 B, C, D 프로그램 모두를 새로 컴파일 해서 배포해야 하게 된다. 더 큰 문제는 어떤 프로그램이 버그가 있는 산술계산 라이브러리를 포함하고 있는지 알아내기가 힘들다는 점이다.

공유라이브러리 형태로 작성하게 될경우에는 라이브러리만 새로 컴파일 한다음 바꿔주면된다. 그러면 해당 라이브러리를 사용하는 프로그램이 몇개이던간에 깔끔하게 문제가 해결된다.

실제 이런 문제가 발생한 적이 있었다. zlib 라이브러리는 압축을 위한 라이브러리로 브라우저, 웹서버, 압축관리 프로그램등에 널리 사용된다. 많은 프로그램들이 이 zlib를 정적라이브러리 형태로 포함해서 배포가 되었는데, 심각한 보안문제가 발견되었다. 결국 zlib를 포함한 모든 프로그램을 새로 컴파일해서 재 설치해야 하는 번거로운 과정을 거치게 되었다. 공유라이브러리였다면 문제가 없을 것이다.

이상 정적라이브러리와 공유라이브러리를 비교 설명했다. 그렇다면 선택의 문제가 발생할 것인데, 자신의 컴퓨터나 한정된 영역에서 사용할 프로그램을 제작하지 않는한은 공유라이브러리 형태로 프로그램을 작성하길 바란다. 특히 인터넷을 통해서 배포할 목적으로 작성할 프로그램이라면, 공유라이브러리 형태로 작성하는게 정신건강학적으로나 프로그래밍 유지차원에서나 좋을 것이다.

:::
2007/08/02 10:09

리눅스 환경에서의 C 프로그래밍 10장 포인터 (Pointer)

이 문서는 수정될 수 있습니다. 최신문서는 Joinc Wiki에서

1 메모리

현대적인 컴퓨터는 입력, 연산, 출력의 3가지 과정이 분리되어 있다. 이는 인간의 생각하는 구조와 비슷하다. 여러분의 눈앞에 날아 들면, 눈으로 이를 확인하고 (입력), 뇌로 전달해서 고민을 한다음 (연산), 눈을 감을 건지 얼굴을 돌릴건지를 결정 (출력) 하게 될것이다. 이때 모든 정보는 로 전달되어서, 뇌의 특정부위에 저장된후에 처리됨을 알 수 있다. 컴퓨터 역시 키보드, 하드디스크, CD 등을 통해서 입력된 모든 정보는 일단 메모리에 저장이 된 다음 CPU 에 의해서 연산처리를 하고, 그 결과를 모니터등을 통해서 출력하게 된다.

2 메모리의 구조

모든 정보를 일단 메모리에 저장한다는 것은 쉽게 이해할 수 있을 것이다. 그렇다면 메모리를 제어하는 기능역시 필요로 할 것이다. 즉 데이터가 저장될 메모리를 할당해서, 해당 영역에 데이터를 저장하고, 다시 데이터를 꺼내오는 일이다. 이를 위해서는 데이터가 메모리의 어느 지점에 저장되었는지에 대한 정보를 가지고 있어야할 필요가 있다. 이를 위해서 현대의 컴퓨터 시스템은 메모리에 주소를 부여해서 사용하고 있다. 주소는 우편함과 매우 비슷한 점이 있다.

agl.gif

우편함과 비슷하게 컴퓨터는 메모리에 일련의 숫자로된 주소를 부여해서, 데이터가 저장된 위치를 찾게 도니다. 이 주소는 0부터시작하게 된다. 주소의 최소단위는 byte 다. 이렇게 주소방식을 채택한 이유는 간단한 관리가 가능하며, 관리를 위한 도구를 쉽게 만들 수 있기 때문이다.

3 포인터

이렇게 메모리의 주소를 관리하기 위해서, C언에서서 사용하는 도구가 바로 포인터다. 포인터는 메모리상의 주소값을 저장하기 위한 데이터 타입으로 4byte 의 저장공간을 가지며, 별표 *를 이용해서 포인터형 변수를 선언할 수가 있다. 변수명앞에 *를 붙여주면 포인터형 변수가 된다.
*data;

여기에서 주목할 점은 포인터는 데이터가 저장된 곳의 주소값만을 가지고 있다는 점이다. 해당 영역의 데이터가 어떤 타입인지를 알수는 없다. 해서 주소가 가리키는 곳에 저장된 데이터의 데이터타입을 명시하기 위해서 *와 함께 데이터 타입을 함께 사용한다. 예를 들어 int 형데이터가 저장된 곳의 주소를 가리키는 포인터형 변수 *data는 다음과 같이 선언된다.
int *data;
이제 우리는 int 형 데이터가 저장된 메모리상의 위치를 알게 되었다. 또한 해당 메모리에 저장될 데이터의 타입이 int 형이란걸 알게 되었으므로, int형 데이터를 마음대로 제어할 수 있게 된다. 우리는 data 를 int 형 포인터 변수라고 한다.

문제
  1. float 형 포인터 변수 fdata 를 선언해 보자.
  2. char형 포인터 변수 cdata 를 선언해 보자.

4 메모리 할당

변수는 값을 저장하기 위해서 사용한다. int 형 변수 data 에 4를 저장하고자 한다면 아래와 같이 코드를 만들면 될 것이다.
int data;
data = 4;

포인터변수 역시 값을 저장하기 위해서 사용한다. 그러나 이 값이 주소값이라는 다소 특이한 면이 있다. 다른 데이터 타입의 변수들은 대입연산자를 통해서 사용자가 직접 값을 입력할 수 있지만, 주소값은 그렇게 할 수가 없다. 왜냐하면 데이터가 저장될 위치는 단지 운영체제만이 결장할 수 있기 때문이다. 그러므로 포인터에 사용될 주소값을 우리가 직접 결정하는 대신에, 운영체제에게 요청을 하게 된다.

이 요청은 다음과 같은 의미를 가진다.
데이터를 저장하기 위해서 몇 byte 정도의 공간이 필요합니다. 이 공간을 만들고, 이 공간의 주소를 알려주세요.

유닉스 운영체제는 프로그램이 쉽게 이러한 메모리를 요청할 수 있도록 malloc(3) 이라는 함수를 제공하고 있다. malloc(3)함수를 이용해서 필요한 공간만큼을 바이트 단위로 요청하면, 운영체제는 그만큼의 공간을 메모리에서 찾아낸다음, 해당 메모리영역의 주소값을 포인터 데이터 타입으로 알려준다. 다음은 malloc 함수의 사용법이다.
#include <stdlib.h>

void *malloc(size_t size);
size_t 는 typedef unsigned int size_t 로 정의되어 있다. 그러므로 최대 4Gbyte 정도의 메모리 할당이 가능하다. 프로그램이 한번에 사용가능한 메모리의 총크기가 4Gbyte다 라는 말을 들어보았을 것이다. 이 제한은 여기에서 생긴다. 이것은 일반적인 32bit 컴퓨터/운영체제 시스템의 경우이고, 64bit 운영체제라면 테라단위의 메모리 사용이 가능할 것이다. 뭐 굳이 4Gbyte 이상의 메모리를 쓸일이 있을려나 하고 생각할지도 모르겠지만, 과거 DOS 시절 64k 로도 메모리가 차고넘친다고 주장했던 적이 있었음을 기억해보기 바란다. 고성능/대용량 프로그램의 출현으로 64bit 컴퓨팅 환경이 요청이 앞당겨지고 있다.

malloc을 호출했을 때, 리턴되는 값은 메모리의 주소정보를 가지고 있는 포인터이다. 이 포인터는 할당된 메모리의 첫번째 주소를 가리키게 된다.

malloc.png

그럼 int 형 포인터를 하나 만들고, 여기에 5개의 int 형 값이 들어갈 수 있는 메모리 공간을 만들어 보도록 하겠다.
#include <stdlib.h>

int main()
{
int *idata; // int 형 포인터 idata 의 선언
idata = malloc(sizeof(int) * 5); // 메모리 할당.
}
sizeof는 주어진 데이터타입의 크기를 알려주기 위해서 사용하는 키워드 명령어다. int는 4byte의 크기를 가지므로, 4*5 = 20 해서 5개의 int 형데이터가 들어가는 공간이 만들어지게 된다. malloc 함수는 20byte 만큼 크기의 메모리공간을 확보한다음에, 그 주소를 되돌려준다. 이 주소값은 포인터형 변수 idata에 들어간다.

5 선언시 정의

위의 경우 malloc 를 이용해서 프로그램의 실행 중간에 메모리 공간을 할당했다. 이렇게 중간 중간 필요할때 할당하는 것을 동적할당이라고 한다. 그렇지 않고, 포인터 변수가 선언될때 값을 정의 하는 방법도 있다. 아래는 9장에서 다루었던, hello 프로그램의 포인터 버젼이다.
#include <stdio.h>

int main()
{
char *hello = "hello world\0";
printf("%s\n",hello);
}
이 경우 프로그램이 실행되는 시점에서 메모리 공간을 할당한 다음, 해당 공간에 "hello world\0"을 저장한 다음, 저장된 주소를 포인터 변수 hello에 넘겨주게 된다. 메모리의 구성은 배열과 동일하다.

문제
hello.c 의 포인트 버젼을 루프와 배열첨자를 통해서 문자를 출력하도록 해보자.


6 포인터에 접근하기 - 배열첨자 이용

malloc.png

자 이렇게 해서 메모리 공간이 확보되고, 그에 대한 주소값도 알아왔다. 이제 해당 주소에 접근해서 값을 읽거나 쓰면 될것이다. 메모리가 할당된 위의 그림을 보면 알겠지만, 할당된 크기만큼 인덱스로 접근할 수 있음을 알 수 있을 것이다. 이것은 마치 배열과 같은 접근구조인데, 실제 배열과 동일하게 배열첨자를 이용해서 메모리 공간에 접근할 수 있다. 위의 malloc 프로그램을 약간 수정해서 5개의 공간에 0부터 5까지 채워넣도록 만들어 보자.
#include <stdlib.h>

int main()
{
int *idata;
int i;

idata = malloc(sizeof(int) * 5);

for (i = 0; i < 5; i++)
{
idata[i] = i;
}
}

7 포인터 접근하기 - 포인터를 이용

위의 프로그램은 배열첨자를 이용해서 데이터에 접근을 했지만, 포인터 연산을 이용해서 접근할 수도 있다. 포인터도 일반 데이터 타입이므로 +와 같은 연산자를 사용할 수 있다. 만약 포인터에 +1 을 한다면, 포인터가 가리키는 주소값이 1만큼 증가해서 다음 값을 가리키게 될것이다. 포인터 연산은 다음과 같이 간단하게 수행할 수 있다.
char *cdata = "hello world\0";
cdata = cdata+1;
cdata는 "hello world\0" 가 저장된 메모리 영역의 첫번째 주소값을 가지고 있으므로, h를 가리키고 있을 것이다. 여기에 + 1 을 해준다면, 1byte 만큼 증가된 위치인 e를 가리키게 될 것이다. 아래의 그림을 보면 쉽게 이해할 수 있을 것이다.

pointer2.png

여기에서 혼동할 수 있는 것이 왜 *cdata = *cdata+1; 이 아니고 cdata = cdata+1; 인가 하는 점일 것이다. *는 참조 연산자로 포인터가 가리키는 곳의 을 참조하겠다는 것을 의미한다. 즉 *cdata는 포인터 변수 cdata가 가리키는 주소의 값인 h를 참조하겠다는 의미이며, *cdata+1 을 해버리면, 주소를 증가시키는게 아닌, 값 'h'에 +1을 하는 효과를 지내게 된다. 'h'에 1을 더하면, 'i'가 된다. 이것은 우리가 원하는 바가 아니다.

우리가 원하는 것은 포인터 값이 아닌 주소를 1만큼 증가시키는 것이므로 cdata+1을 해야만 한다. 아래의 코드를 실행시켜 보기 바란다.
#include <stdio.h>

int main()
{
int *idata;
int i;
idata = (int *)malloc(sizeof(int)*5);
for(i = 0; i < 5; i++)
{
idata[i] = i;
}
*idata = *idata+2;
printf("%d\n", idata[0]);
}
*idata 가 가리키는 주소의 값은 0이다. 여기에 2를 더하게 되므로 *idata = *idata+2 는 포인터가 가리키는 첫번째 값을 2로 바꾸어 버리게 될 것이다. idata = idata+2 로 하게 되면 우리가 원하는 바대로, 3번째 값인 2를 읽어오게 될 것이다. 다음과 같이 코드를 변경한다음 실행해보도록 하자.
    idata = idata+2;
printf("%d\n", *idata);
이제 idata 포인터는 처음 주소에서 두번째 만큼 증가한 곳을 가리키게 된다. 이제 *idata를 printf로 출력하면, idata가 가리키는 주소의 값인 2를 출력한다. 좀 더 정확히 설명하자면, idata+2를 하게 되면, 현재 주소값에서 4*2 만큼을 더해주게 된다. 왜냐면 idata가 int형 포인터 타입이고, int 데이터 타입은 4byte를 가리키게 되기 때문이다. 만약 char 형포인터변수 였다면 2만큼만 증가하게 될 것이다.

포인터연산에 있어서 덧셈연산은 매우 자주 사용된다. 그래서 증가연산자를 사용할 수 있도록 허용하고 있다. idata = idata+2는 다음과 같이 변경할 수 있다.
*idata += 2;

자 그럼 idata의 모든 값을 출력하는 프로그램을 만들어 보도록 하자.
#include <stdio.h>

int main()
{
int *idata;
int i;
idata = (int *)malloc(sizeof(int)*5);
for(i = 0; i < 5; i++)
{
idata[i] = i;
}
for (i = 0; i < 5; i++, *idata++)
{
printf("%d\n", *idata);
}
}

8 배열과 포인터

배열과 포인터는 기본적으로 동일한 메모리 구조를 가진다. 때문에 포인터를 배열처럼 사용할 수 있는 것과 마찬가지로, 배열을 포인터처럼 사용할 수도 있다. 배열을 사용해서 데이터를 저장했다고 하더라도 결국에는 메모리상의 주소를 이용해서 데이터를 꺼내오는 것이기 때문이다. 이미 앞에서도 포인터와 배열을 함께 쓰는 경우를 봤으므로, 쉽게 이해할 수 있을 것이다.

우리는 데이터의 주소값을 얻어오기 위한 주소 연산자를 이용해서 데이터가 저장된 곳의 주소값을 얻어올 수가 있다. 주소 연산자는 &를 사용하면 된다. 다음은 hello 프로그램의 배열 버전으로 배열변수에서 주소연산자를 이용해서 포인터를 얻어오는 법을 보여주고 있다.
#include <stdio.h>

int main()
{
char hello[] = "hello world\0";
char *cdata1;

cdata1 = &hello[0];
printf("%s\n",cdata1);
}
위의 프로그램을 약간 수정해서 char *cdata2 라는 포인터 변수를 만들고, w의 주소를 가리키도록 해보자. 아래와 같이 간단하게 구현할 수 있다. 어떤 결과가 출력될지를 예측해 보도록 하자. 아래의 그림을 보면 더욱 쉽게 결과를 예측할 수 있을 것이다.
arrayto.png
#include <stdio.h>

int main()
{
char hello[] = "hello world\0";
char *cdata2;

cdata2 = &hello[6];
printf("%s\n",cdata2);
}

9 포인터에서 사용되는 연산들

앞에서 이미 포인터에 사용되는 연산자들과 포인터를 이용해서 할수 있는 연산에 대해서 알아봤다. 이들은 매우 중요하므로 정리하는 차원에서 다시 설명해보도록 하겠다.

9.1 참조 연산자

참조 연산자는 *로 해당 주소가 가리키는 곳의 값을 참조하며 다음과 같이 사용할 수 있다.
#include <stdio.h>

int main()
{
char *data = "hello world\0";
int i;
char *cp = data;

for (i = 0;;i++)
{
if (*data == '\0') break; // data가 가리키는 주소가 참조하는 값이 '\0'이면 루프를 빠져나온다.
printf("%c\n", *data); // 참조 연산자를 이용 data가 가리키는 주소의 참조값을 출력한다.
*data++; // 증가연산자를 이용해서 포인터를 1만큼 증가시킨다.
}

printf("============\n");
data = cp;
for (i = 0;;i++)
{
if (*data == '\0') break; // 주소의 참조값이 '\0'이면 루프를 빠져나온다.
printf("%s\n", data); // 포인터가 가리키는 곳의 데이터를 출력한다.
// %s는 '\0'을 만나기 전까지의 데이터를 화면에 출력한다.
*data++;
}
}
위 프로그램을 실행시키면 다음과 같은 결과를 출력한다.
h
e
l
l
o

w
o
r
l
d
============
hello world
ello world
llo world
lo world
o world
world
world
orld
rld
ld
d
왜 이런 결과가 나오는지 이해하는건 그리 어렵지 않으리라 생각된다.

9.2 주소 연산자

어떤 값이 저장된 곳의 주소를 얻어오기 위해서 사용하는 연산자이다. 다음과 같이 사용할 수 있다.
    char *hello = "hello world";
char *data;
data = &hello[0];
printf("%s\n", data); // hello world 가 출력된다.
data = &hello[6];
printf("%s\n", data); // world가 출력된다.
포인터 변수에 들어가는 것은 값이 저장된 데이터의 주소이므로, 주소연산자 &를 이용해서 얻어온 주소를 포인터 변수에 대입할 수 있게 된다.

9.3 덧셈연산

포인터는 덧셈연산이 가능하다. 왜냐하면 저장된 데이터가 주소이기 때문이다. 주소에 숫자를 더하는 방식으로 메모리의 다음위치에 있는 주소를 얻어올 수 있기 때문이다. 이러한 덧셈연산이 가능한건, 데이터를 저장할 메모리를 할당할때 연속된 공간에 할당하기 때문이다. 이를테면 주소+1 하면 다음 주소를 가리키게 된다.
    char *hello = "hello world";
printf("%s\n", hello+1);

마찬가지로 포인터 변수끼리 더하거나 빼는 것도 가능하다.
    char *hello = "hello world\0";
char *fp = hello;
char *sp = hello+6;

printf("%d\n", sp-fp);
메모리 상에서 sp와 fp의 거리는 6일 것이다. 그러므로 두개의 포인터를 뺀 결과로 6이 출력될 것이다. 이런 포인터 연산을 어디에 써먹을 수 있을지 감이 잡히지 않을 수 있을 것이다. 그래서 포인터 연산을 이용한 간단한 예제프로그램을 만들어 보았다. 아래의 예제 프로그램은 "::" 사이에 있는 문자만을 얻어오는 프로그램이다.
#include <stdio.h>

int main()
{
char *hello = "aaa::data::bbb\0";
char *org = hello;
char *fp;
char *sp;

char buf[10] = {'\0',};

fp = strstr(hello, "::");
printf("%s\n",fp); // fp는 ::data::bbb를 가리킨다.

sp = strstr(fp+2, "::"); // fp+2 는 data::bbb 이다.
printf("%s\n",sp); // 그러므로 sp는 ::bbb가 된다.

memcpy(buf,fp+2,(sp - fp)-2); // fp+2는 data::bbb 이다.
// sp - fp 는 6이다. 여기에는 "::"도 포함되어 있으므로 -2를 해준다.
// 결국 data::bbb 에서 4만큼으 크기의 데이터를 buf에 복사한다.

printf("%s\n",buf); // 그러므로 data가 출력된다.
}
strstr(3) 은 주어진 문자열을 찾는 함수로, 문자열이 발견된 위치의 포인터를 리턴한다. memcpy(3)는 데이터를 정해진 크기만큼 복사하기 위해서 사용하는 함수다.

10 다차원 포인터

배열과 마찬가지로 다차원 포인터를 사용한다. 말이 다차원이고, 3차원 이상을 넘어가면 인간이 인지하기가 매우 어렵기 때문에 - 우리는 3차원 공간에 살고 있지만 3차원을 머리로 그려내는건 쉬운일이 아니다. 3차원 전략 시뮬레이션이 컨트롤하기 까다로운 이유가 여기에 있다. - 일반적으로 다차원 포인터란 2차원 포인터만을 가리킨다.

포인터는 데이터가 저장된 주소를 가리키는 데이터 타입이다. 이는 어떠한 종류의 데이터타입이라도 가리키는 포인터의 생성이 가능함을 얘기한다. int 형 포인터, float 형 포인터가 좋은 얘가 되겠다. 2차원 포인터는 포인터 데이터 타입을 를 가리키는 포인터이다. 이러한 특징 때문에 포인터의 포인터라고 부르기도 한다. 2차원 포인터의 가장 단적인 얘는 라인단위 편집기를 예로 들 수 있을 것이다. 아래와 같은 문장이 있다고 가정해 보자.
Hello World!!
My name is yundream.
What's your name.
Thank you.

이 문장을 각각의 라인단위로 저장을 하는 프로그램을 만든다고 가정해 보자. 그렇다면 4개의 char 형 포인터가 필요할 것이다. 이제 이 4개의 char 형 포인터 데이터를 저장하기 위한 포인터변수를 만들면 될것이다. 포인터를 저장하는 포인터변수인 셈이다. 포인터의 포인터변수는 별표를 하나 더 붙이는 것으로 간단히 선언할 수 있다. 위 예의 경우 char 포인터를 저장하기 위한 포인터는 아래와 같이 선언할 수 있다.
char **data;
차원이 늘어날 수록 그만큼의 별표를 붙여주면 된다. 사용할일이 거의 없겠지만, 3차원 포인터는 다음과 같다.
char ***data;

그럼 2차원 포인터를 이용해서 위의 문장을 출력하는 줄단위 출력 프로그램을 만들어 보자. 포인터형 변수의 이름은 char **data로 선언한다. 선언을 했다면, 메모리 할당을 해야 할것이다. 2차원 포인터 이므로 2번의 메모리 할당이 이루어져야 한다.
  1. 4개의 문자형 포인터를 저장하기 위한 공간 할당.
  2. 각 문자데이터를 저장하기 위한 공간할당.

우선 문자형 포인터를 저장하기 위한 공간은 다음과 같이 할당할 수 있다.
char **data;
data = malloc(sizeof(char *)*4);
데이터를 저장하기 위한 공간을 할당하려면, 데이터 타입의 크기를 알고 있어야 한다. 앞서 sizeof를 이용하면 데이터 타입의 크기를 얻어올 수 있음을 언급했다. 우리가 저장하기 위한 데이터 타입은 문자형 포인터이므로 sizof(char *)를 이용해서 크기를 얻어올수 있다. 이게 4개가 필요하므로 *4를 해주면 4개의 문자형 포인터 데이터를 저장하기 위한 공간을 할당받게 된다. 포인터 데이터 타입의 크기는 4 이므로 16byte 만큼의 공간을 할당받게 된다.

이제 4개의 문자형 포인터가 사용할 공간을 할당하고, 그 주소값(포인터)를 얻어와야 할 차례다. 원칙적으로 하자면, 각 문자열의 크기만큼 다르게 할당해야 겠으나, 그렇게 하면 귀찮으니, 모든 문자열을 넣기에 충분하다고 생각되는 25byte 를 동일하게 할당해보도록 하겠다. for 루프문을 이용하면 간단하게 할당할 수 있다.
char **data
data = malloc(sizeof(char *)*4);
for (i = 0; i < 4; i++)
{
*data = malloc(sizeof(char) * 25);
*data++;
}

pointer_array.png

이렇게 해서 25 byte 만큼의 공간이 할당되었다. 만약 3차원 포인터라면 3번의 메모리 할당이 이루어져야 할것이다. 그런데 솔직히 2번의 메모리 할당이 이루어지는 2차원 포인터도 이해하기가 쉽지 않을 건데, 3차원 포인터를 이해하기란 정말 힘들 것이다. 프로그램을 만들때는 특수한 경우가 아니면 가능한 "이해하기 쉬운 코드"를 작성하는 방향으로 가야 한다는 점에서, 3차원 이상의 포인터는 고려하지 않는게 좋을 것이다. 실제로 3차원 이상의 포인터를 볼 수 있는 경우는 거의 없을 것이다. 필자 역시 3차원 포인터를 사용한 프로그램을 본 기억은 거의 없다.

메모리 상에서의 실질적인 데이터공간 할당구조는 아래 그림과 같을 것이다.

point_array2.png

다음은 완성된 프로그램이다. 프로그램의 이름은 point2.c로 하겠다.
#include <unistd.h>

int main()
{
char **data;
char **org;
int i;

data = (void *)malloc(sizeof(char *)*4); // 4개의 포인터를 저장하기 위한 메모리 공간 확보
org = data; // 원래 포인터의 주소를 저장하기 위한 용도.
for (i = 0; i < 4; i++)
{
*data = malloc(sizeof(char)*25); // 각각의 포인터에 최대 25개의 char 데이터를 저장하기 위한
*data++; // 공간을 확보한다.
}

data = org;
strcpy(*data,"hello world!!\0");

*data++;
strcpy(*data,"My name is yundream.\0");

*data++;
strcpy(*data,"What's your name.\0");

*data++;
strcpy(*data,"Thank you.\0");

data = org;
for (i = 0; i < 4; i++)
{
printf("%s\n", *data);
*data++;
}
}

11 할당되지 않은 포인터

그렇다면 할당되지 않은 포인터를 사용하면 어떻게 될가. 이전장에서 값이 할당되지 않은 변수는 알수없는 값이 들어있다는 것을 배웠었다. 메모리공간을 요청받은 운영체제는 메모리 공간만 할당하지, 그 메모리 공간을 0 등으로 초기화 시켜주는 일은 하지 않기 때문이다.
선언시 값을 초기화 시켜주는 고수준 언어들
Java, PHP, Perl 등의 C 보다 높은 수준의 언어들은 선언과 동시에 초기화를 시켜준다. 그러나 C는 초기화시켜주지 않는다. int a; 를 선언했으니 a에는 0이 들어있을 거라고 생각했다가는 버그를 가진 프로그램을 만들게 된다.

포인터변수도 마찬가지로, 어떤 값을 가지고 있을지 알수 없다. 다른 데이터 타입에 비해서 더욱 치명적인것은 주소값을 가지고 있다는 점이다. 알수 없는 주소값이 들어있다는 뜻인데, 이 주소값이 다른 프로세스가 사용중인 주소값일 수 있기 때문이다. 여기에 데이터를 저장하려고 시도할경우 프로그램이 죽어버리게 된다. 그러므로 포인터는 사용하기전에 메모리 공간을 할당해야 한다. 아래의 프로그램을 컴파일하고 실행시키면 비정상 종료되어 버릴 것이다.
int main()
{
char *data;
strcpy(data, "hello world\0");
}
반드시 malloc() 함수등을 이용해서, 메모리를 할당받아야 된다.

12 선언시 포인터의 초기화

int a; 라는 값을 선언한다고 가정해 보자. 그냥 선언만 할 수도 있겠지만, 어떤 값이 들어가 있는지 알 수 없고, 때문에 잘못된 프로그램을 만들 수도 있다. 예를들자면 다음과 같은 경우다.
int a;
...
...
if (a ==0)
{
// 이런저런 코드들
...
}
a에는 알 수 없는 값이 들어가 있으므로 잘못된 결과를 출력할 것이다. 이런 실수는 그다지 많이 발생하진 않겠지만, 미연에 예방하는 차원에서 일반적으로 사용하는 값으로 초기화 시켜줄 필요가 있다. int 형이라면 0 이 될것이다.
int a =0;
float b = 0.0F;

포인터의 경우에는 NULL이라는 것을 사용한다. 이것은 할당되지 않았음을 의미하는 키워드로 다음과 같이 사용될 수 있다.
char *data = NULL;
이렇게 NULL 로 초기화 할경우, 포인터를 사용할때 다음과 같이 포인터값을 검사함으로써, 좀더 robost(견고한) 프로그램을 만들 수 있게 된다.
char *data = NULL;
if (data == NULL)
{
data = malloc(24);
}
strcpy(data, "hello world\0");

13 문제

  1. 배열과 포인터는 메모리구조의 유사성으로 거의 동일하게 사용할 수 있음을 배웠다. 그렇다면 point2.c 프로그램을 배열을 사용하도록 바꾸어 보도록 하자.
:::
2007/07/30 22:20

리눅스 환경에서의 C 프로그래밍 11장 - 구조체


1 원시데이터 타입

C 언어는 매우 기본적으로 사용하는 5가지 정도의 원시 데이터 타입이라는 것을 가지고 있다는 것을 앞서 배웠다. 이들 기본 타입은 다음과 같은 것들이다.
  • int, float, double, char, long long int, Pointer
인간이 다루는 매우 복잡한 데이터들도 숫자와 문자, 도형 이라는 걸 생각하면 컴퓨터가 이렇게 단지 몇가지만의 데이터 타입을 가지는 것도, 어찌보면 당연한 결과라고 할 수 있을거 같다. C 언어뿐만 아니라 거의 대부분의 언어가 6-8개정도의 원시데이터 타입만을 가지고 있을 뿐이다. 종류역시 한두개 정도만 제외하고는 C와 거의 차이가 없다.

2 원시데이터 타입의 구조화

인간이 다루는 데이터로 보자면, 숫자,문자,도형만 있어도 모든 정보를 다룰 수 있기는 하다. 그렇지만 너무나 비효율적이다. 그래서, 이들 데이터 타입을 구조화해서 새로운 데이터 타입을 만들어서 사용하게 된다. 예를 들자면, 주소 정보를 관리하기 위해서 주소록을 만들고, 개인신상관리를 위해서 신상카드를 만들어서 사용하는 것이다. 이렇게 구조화하게 되면, 정보를 훨씬 깔끔하게 다룰 수 있게 된다.

만약 유저정보를 관리할 목적이라면, 아래와 같이 데이터를 구조화 할 수 있을 것이다.
  +--- User Info  -----------------------+
| Name : Text |
| Age : Number |
| Address : Text |
| Email : Text |
| Home : Text |
+--------------------------------------+
TextNumber만으로 유저정보 관리를 위한 User Info라는 새로운 데이터 타입을 만들었다.

3 구조체

C언어도 원시데이터 타입을 구조화해서 새로운 데이터 타입을 만들 수 있도록 지원하고 있다. 이것을 우리는 구조체(Structure)라고 한다. 구조체는 다음과 같은 방식으로 만들 수 있다.
struct 구조체이름 
{
데이터타입 변수명;
데이터타입 변수명;
데이터타입 변수명;
};

위에서 예로 들었던, 유저정보를 구조체로 만들어 보도록하자. 이름은 문자열이 들어가게 되므로 char의 배열이나 포인터형식으로 선언해야 할 것이다. 포인터는 좀 귀찮으니, 모든 문자열은 char의 배열로 하도록 하겠다. 나이는 int형으로 하면 될것이고, 주소, 이메일, 홈페이지는 모두 char 배열로 하면 문제없을 것이다.
struct userInfo
{
char name[12];
int age;
char address[80];
char email[40];
char home[40];
};
구조체는 내부적으로 자신이 사용할 변수들을 유지하게 되는데, 이러한 변수를 멤버변수라고 한다.

4 구조체의 정의, 선언 그리고 사용

구조체는 원시데이터 타입을 요소로 가지는 사용자 정의 데이터타입으로 볼 수 있다. 그러므로 다른 원시데이터 타입과 마찬가지로 선언해서 사용하면 된다. 그러나 사용자 정의 데이터 타입이기 때문에, 구조체의 구조를 먼저 정의해줘야 한다. 인사기록 카드를 만들려면, 카드에 어떤 내용이 들어가야 하는지를 먼저 정의해야 하는것과 마찬가지다.

구조체의 정의는 위에서 이미 설명한바가 있다. 이제 정의를 하는 위치가 문제가 되는데, 구조체는 프로그램 전체에서 선언되고 사용될 수 있으므로, 글로벌영역에서 정의가 된다. 예를 들자면 아래와 같다.
// userInfo 구조체를 정의한다.
struct userInfo
{
char name[12];
int age;
char address[80];
char email[40];
char home[40];
};

int main()
{
struct userInfo MyUser;
}

선언은 일반데이터타입과 마찬가지다. 구조체의 이름뒤에 변수명을 적어주면 된다.
struct userInfo Myuser;

이렇게 정의와 선언이 끝났다면, 이제 사용하는 일만 남았다. 구조체는 다른 원시 데이터 타입들과는 달리, 내부에 멤버변수를 가진다. 그러므로 각각의 멤버변수별로 접근할 수 있어야 한다.

C 언어는 멤버 연산자 "."을 이용해서 멤버변수에 접근할 수 있도록 하고 있다. userInfo 구조체 선언인 MyUser에서 각각의 멤버변수는 다음과 같이 접근할 수 있다.
strcpy(MyUser.name, "yundream\0");
MyUser.age = 33;
strcpy(MyUser.email, "yundream@gmail.com\0");
strcpy(MyUser.home, "http://www.joinc.co.kr\0");

5 구조체와 배열

어렵게 생각할 필요는 없다. 구조체도 데이터 타입이므로, 다른 원시 데이터처럼 배열을 이용해서 동일하게 구조화할 수 있다. 만약 유저정보를 5개를 저장하는 프로그램을 만든다면, 다음과 같이 배열로 선언하면 된다.
struct userInfo Myuser[5];

접근 역시 배열첨자를 이용하면된다.
strcpy(MyUser[0].name, "yundream\0");
MyUser[0].age = 33;
strcpy(MyUser[0].email, "yundream@gmail.com\0");
strcpy(MyUser[0].home, "http://www.joinc.co.kr\0");

아주 간단하다.

6 구조체와 포인터

배열과 포인터는 메모리 상에서 근본적으로 동일한 구조를 가진다는 것을 배웠다. 구조체를 배열로 다룰 수 있으니, 마찬가지로 포인터로도 다룰 수 있으며, 사용하는 방법도 10장에서 배웠던것과 동일하다.

참, 다른 원시데이터 타입과 다른점이 있다. 구조체는 멤버변수를 가지고 있기 때문이다. 앞에서 구조체의 멤버변수에 접근하기 위해서 멤버연산자 .를 사용하면 된다는 것을 배웠다. 그러나 구조체를 포인터로 선언했을 경우에는 멤버연산자를 사용할 수가 없다. 멤버연산자는 을 가져오기 위해서 사용하는 연산자인데, 포인터는 이 아닌 주소를 다루기 때문이다. 그러므로 주소가 가리키는 곳의 을 가져오기 위한 새로운 연산자가 필요하게 된다. C는 구조체 멤버변수의 포인터연산을 위해서 참조연산자라는 것을 제공한다. 참조연산자는 ->를 사용하면 된다.
strcpy(MyUser->name, "yundream\0");
MyUser->age = 33;
strcpy(MyUser->email, "yundream@gmail.com\0");

당연하지만, 포인터는 주소만 가리키는 도구이므로, 실제 데이터를 저장하기 위해서는 메모리를 할당해야만 한다. 메모리 할당은 malloc(3) 함수를 이용하면 된다. 아래 코드는 userInfo 구조체를 포인터로 선언한다음, 5개의 userInfo 정보를 저장할 수 있도록 메모리를 할당하는 프로그램이다. 메모리를 할당하기 위해서는 구조체의 크기를 알아야 할것인데, 다른 데이터 타입과 마찬가지로 sizeof명령을 이용해서 알아낼 수 있다.
#include <unistd.h>
#include <stdlib.h>

struct userInfo
{
char name[12];
int age;
char address[80];
char email[40];
char home[40];
};

int main()
{
struct userInfo *MyUser;

printf("structure Size is %d\n", sizeof(struct userInfo));
MyUser = (struct userInfo *)malloc(sizeof(struct userInfo) * 5);
}

7 예제 프로그램

그럼 간단한 예제 프로그램을 만들어 보도록하자. 이 프로그램은 사용자 정보를 입력받아서 출력하는 일을 한다. 입력받는 정보는 다음과 같다.
  • 이름 : 문자열
  • 나이 : 숫자
다음과 같이 구조체를 정의할 수 있을 것이다.
struct userinfo
{
char name[20];
int age;
};
나이는 100살을 넘기기 힘들 것이다. 그러므로 age 변수의 경우 short int로 정의를 할 수도 있을 것이다. short int는 2byte이므로 4byte의 age에 비해서 2byte의 크기를 절약할 수 있을것이라고 생각할 수 있다. 하지만 다른 여러가지 이유들 때문에, 꼭 메모리 크기를 절약할 수 있는 것은 아니다. 이에 대한 내용은 따로 기회가 되면 다루도록 하겠다. 우선은 그냥 int형으로 하겠다.

사용자 정보는 5개까지만 입력하도록 하겠다.
#include <stdio.h>
#include <string.h>

struct userinfo
{
char name[20];
int age;
};

int main(int argc, char **argv)
{
int age;
int i;
char buf[40];
struct userinfo myfriend[5];

for (i = 0; i < 5; i++)
{
printf("Name : ");
fgets(buf, 19, stdin);
buf[strlen(buf)-1] = '\0'; // <--- 1
sprintf(myfriend[i].name, "%s", buf);

printf("Age : ");
fgets(buf, 19, stdin);
age = atoi(buf);
myfriend[i].age = age;
}

printf("=======================\n");
for (i = 0; i < 5; i++)
{
printf("%12s : %d\n", myfriend[i].name, myfriend[i].age);
}
}
fgets(3)은 키보드로 부터 문자열을 입력받기 위해서 사용하는 함수다. 1은 키보드로 입력된 개행문자를 제거하기 위해서 사용했다.

atoi(3) 함수는 문자열을 int형 값으로 변경하기 위해서 사용한다. 위의 예제는 나이를 숫자로 받아서 하는일이 없으니, 그냥 문자열 그대로 저장해도 상관은 없을 것이다. 그러나 나이를 가지고 비교한다던지 하는 숫자연산 작업이 있을 수 있으므로, 나중을 위해서 int 형으로 변환하는게 좋을 것이다.

다음은 테스트 결과다.
# ./userinfo
Name : yundream
Age : 32
Name : kopete
Age : 28
Name : dream
Age : 31
Name : minsu
Age : 29
Name : test
Age : 32
=======================
yundream : 32
kopete : 28
dream : 31
minsu : 29
test : 32

문제
위의 예제를 포인터를 사용하도록 수정해보자.

8 리스트

구조체 역시 일반 원시데이터타입과 마찬가지로 배열과 포인터를 이용해서 구조화 할 수 있음을 배웠다. 배열 혹은 포인터의 경우 메모리 상에 다음과 같이, 저장을 위한 공간이 만들어 질 것이다.

structarray.png

그러나 모든 데이터 타입을 원소로 가질 수 있다는 구조체의 특징은 배열보다 좀더 유연한 자료구조의 활용이 가능하게 한다. 링크드 리스트와 같은 자료구조의 활용이 가능해진다는 점이다.

배열 혹은 포인터를 이용해서 메모리를 할당하는 방식의 문제점에 대해서 생각해보도록 하자. 이 방식은 저장해야 하는 대상의 갯수를 알고 있을 때, 간단하면서도 효과적으로 사용할 수 있다. 그러나 그 크기를 알 수 없을 때에는 문제가 된다. 즉 다음과 같은 경우가 될 것이다.
  • 원소의 크기가 얼마가 될지 알수 없을 경우
    명함첩 프로그램을 만들경우, 몇개의 명합을 위한 공간이 필요할지 예측하기가 힘들다. 수십개가 될 수도 있지만, 수천개가 될 수도 있다. 최대 수집가능한 명합의 갯수를 예상해서 배열을 충분히 크게 하는 방법도 있겠지만, 그럴 경우 너무 많은 메모리공간을 소비하게 된다. 또한 충분히 크게 잡았다고 해도, 공간을 초과해서 데이터가 들어올 수도 있다.
  • 중간에 데이터가 추가될 경우
    100개의 데이터가 있는데, 새로추가된 데이터를 2번째 위치에 집어넣는 경우를 생각해보자. 유일한 방법은 98개의 데이터를 전부 한칸씩 뒤로 미룬다음에, 2번째 위치에 새로 추가된 데이터를 복사하는 수밖에 없다.

배열(혹은 포인터)를 이용해서 공간을 한꺼번에 할당하는 것은 너무 유연하지 못한 방법임을 알 수 있다.

그렇다면 리스트 형태로 하면 어떻게 될까. 그러니까 새로운 데이터가 들어올때마다. 데이터를 저장하기 위한 공간을 할당하는 방식이다. 만약 새로운 데이터가 추가되었고, 이를 위해서 메모리가 할당되었다면, 추가된 다음 데이터의 위치를 알고 있어야 할것이다. 우리는 포인터를 이용해서 데이터가 저장된 위치를 찾아낼 수 있음을 알고 있다. 그렇다면, 각각의 데이터가 다음 데이터의 위치를 가리킬 수 있도록 하면 될것이다. 예컨데, 구조체에 다음 저장된 데이터의 위치를 가리키는 포인터를 두는 것이다.

즉 다음과 같이 리스트형태로 만드는 것이다.

list.png

리스트는 배열에 비해서 다음과 같은 장점들을 가진다.
  1. 중간에 쉽게 데이터를 삽입할 수 있다.
    데이터를 하나 생성하고, 포인터만 2번 변경해주면 된다.
  2. 메모리를 효율적으로 사용할 수 있다.
    필요한 만큼만 메모리를 사용한다.
  3. 폭넓은 응용이 가능하다.
    링크드리스트, 더블링크드 리스트, 환형 링크드 리스트, tree, graph 모든 고수준의 자료구조들이 리스트의 응용이다.

다음은 리스트에서 데이터를 삽입하는 방법을 보여준다. 매우 효율적으로 데이터를 삽입할 수 있음을 알 수 있다.

list_add.png

반면 배열에 비해서 사용하기가 좀 까다롭다는 단점을 가지는데, 리스트가 가지는 장점에 비할바는 아니다.

9 리스트 응용

그러면 위에서 다루었던 사용자 정보 관리 프로그램을 list(리스트)버전으로 바꿔보도록 하겠다. 구조체는 거의 비슷하지만, 다음 추가될 데이터의 주소를 저장해야 하므로, 포인터형 변수가 추가되어야 한다.
struct userinfo
{
char name[20];
int age;
struct userinfo *NextItem;
};

다음은 완성된 프로그램이다. 약간 복잡하게 보일 수도 있지만, 몇번 실행시키면서 천천히 생각해보면 이해가 갈것이다.
#include <stdio.h>
#include <string.h>

struct userinfo
{
char name[20];
int age;
struct userinfo *NextItem;
};

int main(int argc, char **argv)
{
int age;
int i;
int ItemNum = 0;
char buf[40];
struct userinfo *myfriend;
struct userinfo *first = NULL; // 처음 포인터를 저장하기 위한 변수
struct userinfo *prev = NULL; // 이전 포인터를 저장하기 위한 변수

while(1)
{
printf("Name : ");
fgets(buf, 19, stdin);
buf[ strlen(buf)-1] = '\0';

// strcmp는 문자열을 비교한다.
// 두개의 문자열이 같다면 0을 리턴한다.
// 사용자가 Name 에 quit를 입력하면 루프를 빠져나간다.
if (strcmp(buf,"quit") == 0)
break;
else
myfriend = (struct userinfo *)malloc(sizeof(struct userinfo)*1);

sprintf(myfriend->name, "%s", buf);

printf("Age : ");
fgets(buf, 19, stdin);
age = atoi(buf);

// 다음을 가리키는 원소가 없으므로
// NextItem은 NULL 이된다.
myfriend->age = age;
myfriend->NextItem = NULL;

// 만약에 이전 원소가 있다면,
// 이전 원소에게 현재 원소의 포인터를 알려준다.
if (prev != NULL)
prev->NextItem = myfriend;

// first 가 NULL 이라면
// 최초입력되는 원소임을 알 수 있다.
// 이 원소의 포인터를 저장한다.
if (first == NULL)
first = myfriend;

// 현재 원소의 포인터값을 저장한다.
prev = myfriend;
ItemNum++;
}
printf("Item : %d\n", ItemNum);

// first에는 최초입력된 원소의 포인터가 들어있다.
myfriend = first;

// NextItem이 NULL이 아닐때까지 루프를 돌면서
// 원소를 출력한다.
while(myfriend != NULL)
{
printf("%12s : %d\n",myfriend->name, myfriend->age);
myfriend = myfriend->NextItem;
}
}

이 문서는 수정될 수 있습니다. 최신문서는 Joinc Wiki 에서.
:::
2007/06/27 03:01

리눅스 환경에서의 C 프로그래밍 9장 배열과 문자열

문제 풀이

8장의 1번 문제를 풀어보도록 하자. hello world를 찍는 문제였다. char 하나에는 하나의 문자만 들어갈 수 있으므로 공백문자까지 포함 11개의 char를 선언해서 사용해야 한다.

#include <stdio.h>

int main(int argc, char **argv)
{
char h='h';
char e='e';
char l='l';
char o='o';
char w='w';
char d='d';
char r='r';
char space=' ';

printf("%c%c%c%c%c%c%c%c%c%c%c\n",
h,e,l,l,o,space,w,o,r,l,d);
return 0;
}

배열

그럭저럭 hello world를 출력하긴 했지만, 지나치게 복잡하고 비효율적이라는 느낌이 들 것이다. C 에서 제공하는 원시 데이터 타입이란 것은 말그대로 최소한의 데이터 타입일 뿐으로, 좀 복잡한 프로그램을 만들기 위해서는 이것을 구조화 할 필요가 있다.

이것은 서류철을 만드는 것과 비슷한 과정이라고 보면 될것이다. 서류 하나하나를 일일이 관리하는건 상당히 짜증나는 일일 것이다. 그래서 우리는 (이름 등으로 정리된) 서류철을 만들고 다시 서류철을 보관하기 위한 캐비넷을 사용한다. 자료구조화 하는 것이다.

프로그램에서 사용되는 데이터(자료)들도 이렇게 구조화할 필요가 있다. 이것을 자료구조라고 한다. 이러한 자료구조들 중에서 가장 기본이 되는게, 배열이다.

배열은 서류철로 볼 수 있다. 서류는 최소단위가 되고, 이것을 나란히 쭉 정리해서 하나로 묶은게 서류철이다. 배열은 최소단위의 데이터를 일렬로 나열한 자료구조다. 서류철 같은 것을 보면 서류를 쉽게 찾기 위해서 인덱스를 한다. 보통은 이름으로 인덱스를 하는데, 배열역시 데이터를 쉽게 찾기 위해서 인덱스를 가지고 있다. 서류철과 다른점은 인덱스가 숫자로 되어 있다는 것 정도가 될 것이다. 이 숫자는 0 부터 시작한다.

다시 문제로 되돌아가보자. 문자열 hello world을 구조적으로 쉽게 표현할 수 있는 방법은 바로 배열을 이용하는 것이다. 이 배열의 최소단위는 문자 즉 char 데이터가 될 것이다. 이것을 위해서 아래와 같이 12 개의 char 형 데이터를 저장할 수 있는 공간을 만들어서 각각의 문자들을 집어 넣으면 된다. 이렇게 자료를 인덱스를 key로 해서 구조화 시킨것을 배열이라고 한다. 인덱스를 통해서 서류철에서 서류를 꺼낼 수 있듯이, 이 인덱스를 이용해서 (value)를 가져올 수 있다.

array.png

C 뿐만 아니라, 거의 모든 프로그래밍 언어가 배열을 지원하고 있다. 그리고 배열의 첫번째 인덱스는 1이 아닌 0부터 시작한다. 인덱스가 0부터 시작하는 것은 우리의 일반적인 직관과는 거리가 있어서, 초기에 혼동하기 쉬우니 주의하기 바란다. 배열의 인덱스 뿐만 아니라, 컴퓨터는 모든 것의 첫번째는 0부터 시작한다. 예를 들어 메모리의 첫번째 주소는 0이다.

C에서 배열

배열은 자료구조이므로 구조화가 가능한 모든 데이터들은 배열로 만들어서 선언할 수 있다. 선언방법은 간단하다.
데이터타입  변수명 [크기];

예를들어 정수 데이터 10개를 저장하기 위한 배열은 아래와 같이 선언할 수 있다.
int  mydata[10];
매우 간단하다. 이제 우리가 목적으로 하는 hello world를 저장하기 위한 배열은 다음과 같이 만들면 될 것이다. 변수명은 hello 로 하도록 하겠다.
char hello[12];
여기에서 약간 의문시 되는 점이 있을 것이다. 우리가 저장하고자 하는 hello world는 11자인데, 12개의 공간을 가지도록 선언했기 때문이다. 이는 \0을 저장하기 위한 공간을 하나더 필요로 하기 때문이다. \0을 만나면 프로그램은 문자열이 여기에서 끝났다고 판단을 하게 된다. 만약 \0을 만나지 못한다면, 저장공간을 초과해서 \0을 만날때까지 계속 데이터를 읽을 려고 할 것이다. 프로그램은 바보라는 것을 명심해야 한다. 어디가 시작이고 어디가 끝인지를 명확하게 해주어야만 한다.

자 그럼 만들어진 배열 hello 에 hello world를 저장해 보도록 하자.

문제
double 데이터 16 개를 저장하기 위한 배열을 선언해 보자.

배열 선언과 정의

배열에 값을 저장하는 데에는 두가지 방법이 있다. 하나는 선언과 동시에 값을 대입하는 것이다.
#include <stdio.h>

int main()
{
char hello[12] = {'h','e','l','l','o',' ','w','o','r','l','d','\0'};
printf("%s\n", hello);
}
중괄호 "{","}" 사이에 각각의 원소 데이터를 넣으면 된다. 데이터와의 구분은 ,을 통해서 이루어진다. 1부터 5까지 저장하는 int 형 배열은 다음과 같이 선언할 수 있을 것이다.
int data[5] = {0, 1, 2, 3, 4, 5};

값이 여러개가 중복이 될 경우가 있을 수 있다. 예를 들어 배열의 크기가 100인데, 모든 값을 1로 해서 선언하고 싶을 때가 있을 것이다. 이경우 ,뒤에 데이터를 쓰지 않으면 된다. 그럼 가장 마지막 데이터로 배열의 끝까지 채워지게 된다.
int data[100]={0,};

아래의 프로그램을 실행시키고 결과를 확인해 보도록 하자.
#include <stdio.h>

int main()
{
char hello[12] = {'h','e','l','l','o',' ','w','o','r','l','d','\0'};
int data[100] = {0,};
printf("%s\n", hello);
printf("%d\n", data[2]);
}

단 문자열의 경우 예외적으로 괄호 {}를 사용하지 않고, 쌍따움표를 이용해서 직접 선언할 수 있도록 지원하고 있다. 위의 코드는 아래와 같이 좀더 간단하게 표현할 수 있다.
    char hello[12] = "hello world\0";

배열의 데이터에 접근

서류철에서 원하는 서류에 접근하기 위해서 인덱스를 사용하듯이, 배열역시 인덱스를 통해서 접근할 수 있다. 이 인덱스는 0부터 시작되는 정수로 배열첨자라고 부르기도 한다. 접근 방법은 간단하다. 괄화 [] 안에 꺼내오기 원하는 데이터의 배열첨자를 넣어주기만 하면 된다.
char hello[12] = {'h','e','l','l','o',' ','w','o','r','l','d','\0'};
printf("%c", hello[4]);
아래의 배열 이미지를 보면 hello[4] 가 o를 가져오리란걸 쉽게 예상할 수 있을 것이다.

array.png

대입연산자를 이용하면 배열의 원하는 위치에 데이터를 쓰는 것도 가능하다. hello[4]의 값을 'w'로 바꾸어보자.
hello[4] = 'w';

이제 이전에 배웠던 루프문을 이용해서, 변수 hello 의 값을 출력하는 프로그램을 만들어 보자.
#include <stdio.h>

int main()
{
char hello[12] = "hello world\0";
int i = 0;
for (i = 0; i < 12; i++)
{
printf("%c", hello[i]);
}
printf("\n");
}

잘못된 배열첨자의 사용

배열의 크기가 12 인데, 이를 초과해서 데이터를 집어 넣거나, 배열첨자를 초과해서 데이터를 가져오는 경우를 생각해 보자.

유닉스는 다중 사용자, 다중 프로세스를 지원하고 있다. 이말은 동시에 여러개의 프로그램들이 돌아갈 수 있음을 의미한다. ps는 현재 실행중인 프로세스의 목록을 보여주는 유닉스 프로그램이다. ps 를 이용해서 현재 떠있는 프로세스의 목록을 알아보도록 하자.
프로세스란
프로세스는 프로그램이 실행된 이미지다. 여러분이 어떤 프로그램을 실행시키면, 하드디스크에 있는 프로그램이 직접 수행되는게 아니고, 해당 프로그램의 복사본이 메인메모리에 올라가서 수행이 된다. 그러므로 하나의 프로그램은 여러개의 프로세스로 생성될 수 있다.

]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Jan30 ? 00:00:35 init [3]
root 2 1 0 Jan30 ? 00:00:00 [keventd]
root 3 1 0 Jan30 ? 00:00:00 [kapmd]
root 4 1 0 Jan30 ? 00:00:00 [ksoftirqd_CPU0]
root 5 1 0 Jan30 ? 00:04:44 [kswapd]
root 6 1 0 Jan30 ? 00:00:00 [bdflush]
root 7 1 0 Jan30 ? 00:00:04 [kupdated]
root 8 1 0 Jan30 ? 00:00:00 [mdrecoveryd]
root 12 1 0 Jan30 ? 00:08:54 [kjournald]
root 71 1 0 Jan30 ? 00:00:00 [khubd]
root 1181 1 0 Jan30 ? 00:00:00 [kjournald]
root 1480 1 0 Jan30 ? 00:01:11 syslogd -m 0
root 1484 1 0 Jan30 ? 00:00:00 klogd -x
rpcuser 1521 1 0 Jan30 ? 00:00:00 rpc.statd
root 1607 1 0 Jan30 ? 00:00:00 /usr/sbin/apmd -p 10 -w 5 -W -P
root 1644 1 0 Jan30 ? 00:05:22 /usr/sbin/sshd
root 1658 1 0 Jan30 ? 00:00:00 xinetd -stayalive -reuse -pidfil
root 1671 1 0 Jan30 ? 00:00:16 crond
daemon 1693 1 0 Jan30 ? 00:00:00 /usr/sbin/atd
root 1711 1 0 Jan30 tty1 00:00:00 /sbin/mingetty tty1
root 1712 1 0 Jan30 tty2 00:00:00 /sbin/mingetty tty2
... ...
아무리 간단한 유닉스 시스템이라고 하더라도 최소한 30개 이상의 프로세스가 떠있을 것이다. 그렇다면 해결해줘야 할 문제가 있다. 프로세스는 메인메모리 상에서 실행되고, 자신이 사용하는 데이터를 역시 메모리에 올려놓고 읽거나 쓰게 된다. 그런데 다른 프로세스가 자신의 메모리 영역을 침범하면 안될 것이다. 유닉스 운영체제는 프로세스를 관리하면서, 동시에 각 프로세스가 다른 프로세스의 메모리영역을 침범하는지를 감시를 한다. 만약 다른 프로세스의 메모리 영역을 침범했다면, 운영체제는 프로세스를 강제로 종료시켜버리게 된다.

우리는 아주 간단하게 이런 프로그램을 작성할 수 있다.
#include <stdio.h>

int main()
{
char hello[12] = "hello world!! My site is Joinc";
int i = 0;
printf("%s", hello[i]);
printf("bye bye\n");
}

이 프로그램은 char 형의 배열인 hello 를 위해서 12의 크기를 할당했다. 그러나 12byte를 초과하는 문자열을 집어 넣고 있음을 알 수 있다. 이 프로그램을 컴파일 해서 실행시키면 다음과 같은 에러메시지가 떨어질 것이다. 프로그램의 이름은 array_test 로 하겠다.
# ./array_test
Segmentation fault
printf("%s", hello[i]); 를 하면서, 자신에게 할당되지 않은 영역을 읽어 들이려고 시도를 하는 중에, 다른 프로세스가 사용하는 메모리 영역을 침범했고 이 때문에 Segmentation fault메시지를 출력하면서 강제 종료되어 버렸다. bye bye\n은 출력조차 되지 못했다.

array2.png

앞으로 많은 프로그램을 작성할 건데, 가장 흔하게 볼 수 있는 에러메시지가 Segmentation fault가 될 것이니, 지금 부터 익숙해지기를 바란다. 인간은 서류철의 용량을 초과해서 서류가 들어오면, 억지로 용량을 늘리던지, 안되면 풀로붙이던지 하는 등의 유도리를 발휘해서 업무를 처리할 수 있지만, 우리의 컴퓨터는 그렇게 똑똑하지 못한점 이해를 해주어야 한다. 프로그램을 작성할 때는 할당한 공간을 초과했는지, 제대로 사용했는지를 엄격하게 검사해야 한다.

위의 프로그램은 제대로 수행되는 경우도 있을 것이다. 왜냐면 쓸려고 하는 메모리 영역이 다른 프로세스에 의해서 사용되지 않는 자유상태일 수도 있기 때문이다. 이 경우에는 다른 프로세스 메모리 영역을 침범한 행위는 아니므로, 에러가 발생하지는 않을 것이다. 물론.. 제대로된 결과가 나올 것이라는건 보장할 수가 없다. 자유상태이기 때문에, 프로세스가 실행되고 있는 도중에, 다른 프로세스에게 해당 메모리 영역이 할당되어버릴 수 있기 때문이다. 이럴 경우 Segmentation fault 를 출력하면서 종료되어 버릴 것이다.

문자열의 출력

다시 문제로 되돌아가 보자. for 문과 배열을 이용해서 좀 더 쉽게 문제를 풀기는 했지만, 문자열 하나 출력할려고 루프문까지 사용하는건 합리적이지 못하다. printf 함수를 이용하면 쉽게 이 문제를 풀 수 있다. 이 함수는 주어진 변수를 형식화해서 출력할 수 있도록 도와준다.
#include <stdio.h>

int main()
{
char hello[12] = "hello world\0";
printf("%s", hello[i]);
}
이걸로 문제는 간단히 해결됐다. "%s" 는 주어진 인자를 문자열로 인식해서 출력하는 포맷 옵션이다.

문제
char 배열을 받아들여서 문자열로 출력하는 함수를 작성해보자.

문자열 복사

문자열을 배열에 복사하는 간단한 방법에 대해서 생각해보자. 다음과 같이 코드를 작성하면 어떻게 될까.
int main()
{
char hello[12];
hello = "hello world\0";
}
문제 없을 거라고 생각할 수도 있지만, 컴파일 하면 아래와 같은 에러메시지와 함께 컴파일 실패하게 된다.
# gcc -o array array.c
array.c: In function `main':
array.c:6: incompatible types in assignment
대입연산자는 같은 타입에 대해서만 허용되는 연산자이기 때문에, 배열에 문자열을 대입하는건 C 문법에 어긋나기 때문이다. 결국 문자열을 처리하기 위해서는 별도의 함수를 만들어서 사용하는 수 밖에 없다. 다행히 C 는 표준 라이브러리 형식으로 몇개의 유용한 문자열 처리 함수를 제공한다. strcat(3), strcpy(3)와 같은 함수가 문자열 처리를 위한 대표적인 함수다. 다음은 strcpy 함수를 이용해서 문자열을 복사한 프로그램이다.
#include <stdio.h>

int main()
{
char hello[12];
strcpy(hello,"hello world\0");
printf("%s\n", hello);
}

문제풀이

문제
char 배열을 받아들여서 문자열로 출력하는 함수를 작성해보자.

문제는 다양한 방식으로 풀 수 있을 것이다. 아래는 다양한 방식중 하나이다.
#include <stdio.h>

void print_string(char a[])
{
int i=0;
while(a[i] != '\0')
{
printf("%c", a[i]);
i++;
}
}
int main()
{
char hello[12] = "hello world\0";
print_string(hello);
}

이 문서는 수정될 수 있습니다. 최신문서는 Joinc Wiki에서 확인하세요.
:::
2007/06/25 23:54

리눅스 환경에서의 C 프로그래밍 8장 데이터 타입

우리는 5장에서 데이터 타입에 대해서 간단히 알아보았다. 여기에서는 이들 데이터 타입에 대해서 좀더 자세히 알아보도록 할것이다.

1 Data Types

우리가 사용하는 데이터는 컴퓨터의 메모리에 연속된 비트의 나열로 저장이 된다. 이러한 비트의 나열은 컴퓨터입장에서는 문제가 안되겠지만 인간의 입장에서는 알아보기 힘들다는 문제가 발생한다. 그래서 데이터 타입을 두어서 인간이 좀더 쉽게 사용할 수 있도록 하고 있다.

데이터 타입은 다음과 같은 특징을 가지고 있다.
  • 데이터가 어떻게 표현되고 사용될지를 결정한다.
  • 데이터 타입에 따라 컴퓨터가 어떻게 데이터를 다룰지를 알려주다. (숫자로 다룰지 아니면 문자로 다룰지 등..)
  • 모든 값은 데이터 타입에 의해서 표현될 수 있다.

예를 들어서 메모리에 다음과 같은 비트패턴이 저장되어 있다고 가정해보자.
0000 0000 0110 0111

이 값이 어떻게 표현될까 ? 이것은 데이터 타입을 어떻게 정의하느냐에 따라 달라진다. 만약 데이터 타입을 'int로 하기로 했다면 숫자 103으로 표현될 것이다. 그러나 문자를 저장하는 char로 하기로 했다면 영문자 g로 표현이 된다.

데이터 타입은 이렇게 인간의 입장에서 컴퓨터 메모리에 저장된 데이터를 어떻게 다룰 것인지를 결정하기 위해서 사용이 된다.

아래의 코드를 실행시켜보기 바란다.
#include <stdio.h>

int main(int argc, char **argv)
{
int a = 103;
char b = 103;

printf("%d\n", a);
printf("%c\n", b);
}
똑같은 103인데, 서로 다르게 출력되는걸 확인할 수 있을 것이다.

2 Primitive Data Types

인간은 매우 다양한 형태의 데이터를 다루기를 원하고, 그런일을 할 수 있는 프로그램을 만들어낼 수 있어야 한다. 다루는 데이터의 형태가 다양하니, 데이터 타입역시 다양하면 좋을 것이다. 그러나 컴퓨터는 매우 단순한 기계다. 쓸데없이 데이터 타입을 이것 저것 만들면, 이것을 다루는 컴퓨터 역시 달가워하지 않을 것이다. 프로그래머 역시 각 데이터 타입에 따른 고려사항이 늘어나니 권장할만한 사항이 아니다.

그래서 데이터타입을 다루어야 하는 프로그래밍 언어는 Primitive Data Type이라고 불리우는 최소한의 반드시 필요한 데이터 타입을 지원하고 있다. Primitive Data Type은 원시 데이터 타입 이라고 부르기도 한다.

각 원시 데이터 타입은 고유의 크기를 가지고 있으며, 표현할 수 있는 데이터의 한계가 정의되어 있다. 다음은 C 언어에서 지원하는 원시 데이터 타입과 크기, 데이터 범위를 보여주는 표다.

2.1 숫자형 원시 데이터 타입

숫자를 표현하기 위해서 사용되는 데이터 타입이다. 크게 정수형 데이터를 표현하기 위한 정수형 원시 데이터 타입부동소숫점형 원시 데이터 타입으로 나눌 수 있다.
  • 정수형 원시 데이터 타입
    char 1byte -128 127
    short 2byte -32768 32767
    long int 4byte 2,147,483,648 2,147,483,647
    long long int 8byte 9,223,372,036,854,775,808 9,223,372,036,854,775,807

이들 정수형 데이터 타입들은 signed비트라는 것을 가지고 있어서, 음수까지 표현할것인지를 정의할 수 있다. 음수까지 표현할 거라면 signed를 양수만 표현할거라면 unsigned를 타입의 앞에 붙여준다. 따로 붙여주지 않았다면 signed 가 붙은걸로 해석을 한다. 즉 위의 데이터 타입은 실제로는 signed char, signed short, signed long int, signed long long int와 동일하다.

unsigned를 명시하게 되면 양의 정수만 표현하게 된다. 음의 정수를 표현할 필요가 없으니 그만큼 양수 쪽으로 표현범위가 늘어날 것이다. unsigned char 이라면 255, unsigned long int 라면 4,294,967,295 가 된다.

  • 부동소숫점형 원시 데이터 타입
    float 4byte +/- 10E-37 +/- 10E38
    double 8byte +/- 10E-307 +/- 10E308

2.2 enumerated type

일반적으로 enumerated 타입은 숫자로 나열된 카테고리 같은 데이터를 만들기 위해서 사용한다. C에서는 enum을 이용해서 enumerated 타입의 데이터를 정의할 수 있다.
enum cardsuit {
CLUBS,
DIAMONDS,
HEARTS,
SPADES
};
이제 enum 을 이루는 각각의 요소들은 숫자 0,1,2... 로 차례대로 대응되게 된다. 다음의 프로그램을 실행시켜 보자.
#include <stdio.h>
enum cardsuit {
CLUBS,
DIAMONDS,
HEARTS,
SPACES
};
int main()
{
printf("Card CLUBS is %d\n", CLUBS);
printf("Card DIAMONDS is %d\n", DIAMONDS);
printf("Card HEARTS is %d\n", HEARTS);
printf("Card SPACES is %d\n", SPACES);
}

다음과 같은 결과를 확인할 수 있을 것이다.
# ./enum
Card CLUBS is 0
Card DIAMONDS is 1
Card HEARTS is 2
Card SPACES is 3

그렇지만 CLUBS 가 0이 아닌 다른 수로 대응되어야 할 경우도 생길 것이다. 그럴 땐, 필요한 값을 대입시켜 주면 된다. 즉 값을 명시하지 않으면 0부터 시작해서 1씩 증가하고, 값을 명시하면 명시된 값을 시작으로 1씩 증가하는 것으로 보면 된다.

문제
cardsuit를 다음과 같이 정의 했을 때, 어떤 값이 출력될지 생각해 보라.

enum cardsuit {
CLUBS = 1,
DIAMONDS,
HEARTS = 100,
SPACES
};

2.3 Pointer type

컴퓨터는 계산을 하기 위한 기계다. 이때 계산에 사용될 모든 데이터는 일단 메모리로 읽혀져서 필요한 계산을 하게 된다. 예를 들어 하드디스크에 A 라는 문서가 있다고 가정해 보자. 이 문서를 편집하기 위한 프로그램을 가동시키면, 프로그램은 하드디스크에 있는 문서를 모두 읽어서 컴퓨터의 메모리로 불러들인 다음 필요한 일을 하게 된다. 다른 모든 연산들역시 마찬가지다.

그렇다면 문제가 발생한다. 컴퓨터에는 여러개의 프로그램이 떠 있을테니, 각각의 프로그램이 사용하는 데이터가 메모리의 여기저기 저장되어 있을 것이다. 이때 프로그램은 자신이 사용할 데이터가 메모리상의 어느 위치에 있는지 알고 있어야 한다.

이를 위해서 메모리에는 아래 그림과 같이 주소 값이 부여되어 있다.

pointer.png

프로그램은 이 기억값을 기억해서 데이터의 위치를 정확히 찾아내어서 읽어오게 되는 것이다. Pointer는 이러한 주소값을 저장하는 데이터 타입이다.

다른 데이터 타입들이 그렇듯이 pointer도 고유의 크기를 가지고 있다. pointer 데이터 타입의 크기는 4byte이다. 메모리상의 주소에는 마이너스 값이 필요가 없으므로 0 - 4,294,967,295의 숫자가 저장된다. 32bit 컴퓨터에 사용가능한 메모리의 총크기가 4Giga다 라는 얘기가 여기에서 나온다. 가리킬수 있는 숫자의 범위가 4,294,967,295 (약 4giga)이므로, 이를 초과한 영역에 저장된 데이터는 읽어올 수가 없기 때문이다.

이는 C 와 같은 프로그래밍 언어에도 그대로 적용된다. 포인터의 크기가 4byte 이니, 최대다룰 수 있는 메모리의 크기가 4Giga로 제한이 된다. 물론 여러분이 64bit 컴퓨터운영체제 그리고 컴파일러를 사용한다면 테라 byte급의 데이터를 다룰 수 있다. 64bit 컴퓨터가 대용량 데이터 처리에 유리하다는 얘기가 나오는 이유다.

포인터는 이쯤에서 끝내도록 하겠다. 포인터에 대한 자세한 내용은 따로 한장 정도를 할애해서 자세히 다루도록 하겠다.

3 type casting - 형변환

아래 프로그램을 컴파일 후 실행시켜 보자.
#include <stdio.h>

int main(int argc, char **argv)
{
unsigned char ch = 'c';

printf("%c\n", ch);
ch = ch+1;
printf("%c\n", ch);
ch = ch+1;
printf("%c\n", ch);
}
결과로 c d e 가 출력될 것이다.

이상하군. 분명히 두개는 서로 다른 데이터 타입인데, 더하기가 되는군 ?

앞서 언급했지만, 데이터형이란 표현방식에 따른 것일 뿐이다. 컴퓨터 입장에서는 모두가 비트일 뿐이다. 즉 ch+1은 컴퓨터 입장에서 다음과 같이 계산이 된다.
                               0110 0011   = 'a'
0000 0000 0000 0000 0000 0000 0000 0001 = '1'
==========================
0000 0000 0110 0100 = 'b'
만약 printf("%d\n" ch+1) 을 한다면, 100 이 출력될 것이다. 비트패턴을 숫자로 표현하도록 표현방식을 바꿨기 때문이다.

이렇게 모두 동일한 비트일 뿐임으로 서로 다른 타입간의 계산이 가능해진다. 그러나 이건 어디까지나 가능하다일뿐 실제로는 의도하지 않는 다양한 문제가 발생할 수 있다.

  • 데이터타입의 크기에 따른 문제
    데이터 타입은 서로 다른 크기를 가지고 있다. 위의 코드에서 ch 에 1000을 더하면 어떻게 될까. 1099가 나오길 예상할 수 있겠지만 ch의 테이터 타입인 char는 1byte의 크기로 255까지만 표현이 가능하다. 때문에 데이터 저장공간을 초과하게 될 것이다. 실제로는 1byte의 상위 비트는 버려지게 된다. 고로 0-255까지의 값이 출력이 될것이다. 뭐.. 값이 255를 초과하지 않는 범위에서 연산을 한다면 문제가 없기는 하겠지만 실수로 문제가 발생할 소지가 다분하다.

  • 다른 데이터 타입끼리라도 주의해서 프로그래밍을 하면 문제 없겠지만 가능하면, 타입을 맞추어서 계산을 하는게 좋다.

  • signed bit 문제
    정수형 데이터 타입은 signed bit 를 가지고 있어서 이걸로 양수인지 음수인지를 판단하게 된다. 다음의 코드를 확인해 보자.

    #include <stdio.h>

    int main(int argc, char **argv)
    {
    unsigned int i = 100;

    if (i < -10)
    {
    printf("Large\n");
    }
    }
    상식적으로 100 은 -10보다 크기 때문에 if문의 블럭은 실행되지 않아야 겠지만, 컴파일해서 실행시켜 보면 블럭문이 실행이 되는걸 알 수 있다. 이는 i 가 unsigned 형으로 -10 을 unsigned 형으로 보고 비교를 하기 때문이다. -10이 unsigned 형이 되면 4,294,967,285 으로 표현이 된다. 이를 이해하기 위해서는 2의 보수를 통한 singed 데이터 처리에 대해서 알고 있어야 하는데, 5장 데이터와 비트문서를 읽어보기 바란다.

    이 문제를 해결하기 위해서 형변환을 수행한다. 위의 프로그램의 경우 문제를 피해가는 가장 좋은 방법은 i를 signed 형으로 선언하는게 될것이다. 그러나 불가피 하게 unsigned 형을 고집해야 할 경우가 발생한다. 그때는 casting(형변환)연산자를 통해서 형변환을 하도록 한다.

  • if ((signed)i < -10)
    {
    ...
    }
    이제 제대로 되는걸 확인할 수 있을 것이다.

프로그램을 작성할 때는 데이터가 어디에 쓰일 것인지를 명확히 해서, 그에 맞는 데이터 타입을 지정해 줘야 한다. 그렇지만 사람이다 보니, 위에서 처럼 사소한 실수를 하기도 한다. 문제는 이런 프로그램도 문제 없이 컴파일이 된다는 것이다. 결국 프로그램이 실행하는 도중에 문제를 일으키게 될 것이다. 다행히 gcc 컴파일러는 컴파일 옵션을 통해서 저러한 문제를 사전에 잡아낼 수 있게 하고 있다. 위 프로그램을 type.c로 저장하고 아래와 같은 옵션을 주고 컴파일 해보자.
# gcc -W -Wall -o type type.c
...
type.c:7: warning: comparison between signed and unsigned
...
comparison between signed and unsigned와 같은 경고메시지를 출력함을 알 수 있다. 자세한 내용은 리팩토링 : 모든 경고메시지를 제거하라 문서를 읽어보기 바란다. 아직 읽기 버겁다면, 그냥 대충 저런게 있나보다 하는 수준에서 읽어두어도 도움이 될 것이다.

4 문자와 문자열 표현

데이터는 크게 문자와 숫자로 이루어져 있음을 알고 있다. 그런데 정작 문자문자열을 표현하기 위한 데이터 타입을 다룬거 같지를 않다. 이에 대해서 얘기해 보고자 한다.

C 언어에서 문자를 위한 데이터 타입으로는 char 데이터 타입을 사용한다. char 는 1byte 256의 크기를 가지는데, 1byte 문자권의 영어와 수십개의 특수문자를 충분히 표한할 수 있는바 char를 문자를 저장하기 위한 데이터 타입으로 사용하고 있다.

컴퓨터에서 표현되는 문자는 0에서 255까지의 각 크기에 대응되는 문자들이 표준으로 정의되어 있다. 각 값에 대응되는 문자는 ASCII 테이블로 정리되어 있다.

http://www.joinc.co.kr/albums/album01/age.gif

http://www.joinc.co.kr/albums/album01/agf.gif

그러나 ASCII 테이블만 가지고는 일본어, 한글, 중국어와 같은 2byte 문자는 표현할 수 없다. 2byte 문자는 char를 2개 이상 사용해서 저장해야 한다.

이제 마지막으로 문자열이 남았다. C는 문자열을 위한 데이터 타입을 가지고 있지 않다. C에서 문자열을 처리하기 위해서는 배열을 사용해야만 한다. 이것은 원시 데이터 타입을 여러개를 포함하고 있는 데이터 구조다. 예를 들어 문자열은 char를 여러개 포함할 수 있는 데이터 구조를 이용하면 표현할 수 있을 것이다. 배열은 다음장에서 자세히 다루도록 하겠다.

5 문제

  1. char 데이터 타입을 이용해서 hello world를 화면에 출력해 보자.
    • char를 여러개 써야함.
    • printf 를 통해서 출력할 수 있음.
  2. char 데이터 타입을 이용해서 hello world를 출력해보자. 단 정수형 숫자를 사용해야 한다.
    • ASCII 테이블을 이용하면 됨.
이 문서는 수정될 수 있습니다. 최신문서는 Joinc Wiki에서 확인하세요.
:::
2007/06/23 12:47

리눅스 환경에서의 C 프로그래밍 7장 - 함수의 사용

1 함수

함수는 어떤 일을 처리하는 단위로 function 혹은 subroutine라고 부른다. Pascal과 같은 언어에서는 procedure라고 부르기도 하는데, 용어만 다를 뿐 의미하는 바는 같다. 함수는 특정한 연산을 encapsulate(캡슐화) 해서 프로그램을 구조적이고 단순하게 만들 수 있도록 도와 준다. 예를 들어서 당신이 만든 프로그램이 일정한 범위의 숫자를 더하는 연산을 여러번 한다고 가정해 보자. 그러면 다음과 같은 코드를 만들 수 있을 것이다.
1 int main()
2 {
3 int start = 0;
4 int end = 0;
5 int sum = 0;
6
7 // 10부터 120 까지 더한다.
8 for (start = 10; start < 121; start++)
9 {
10 sum = sum+start;
11 }
12 printf("%d\n", sum);
}

프로그램을 만드는건 간단하다. 그렇지만 150부터 170까지 더하고, 또 1000부터 12000까지 더해서 결과를 출력해야 한다면, 어떡할 것인가. 물론 무식하게? 8번부터 11번까지를 for의 조건만 바꿔가면서 필요한 수만큼 copy & paste 하는 방법이 있겠지만, 코드의 길이도 길어지고, 난잡해질 것이다. 다음과 같이 함수를 이용하면 이러한 문제를 해결할 수 있다.

이 프로그램은 완전히 실행되는 프로그램이다. 이름은 rangesum.c로 하겠다.
#include <stdio.h>
int rangesum(int start, int end);

int main()
{
printf("%d\n",rangesum(1, 100));
printf("%d\n", rangesum(1000, 1500));
printf("%d\n", rangesum(310, 5000));
}

int rangesum(int start, int end)
{
int sum = 0;
for (start; start < end+1; start++)
{
sum = sum+start;
}
return sum;
}
프로그램이 훨씬 깔끔해 졌음을 알 수 있다. 이렇게 자주 사용하는 코드등을 함수화 하면 프로그램을 유지보수하기 편해 지며, 프로그램에 문제가 생겨도 해당 함수만 수정하면 되기 때문에, 디버깅 하기도 훨씬 편해지게 된다.

1.1 함수 만들기

함수는 어떤 정보를 입력받아서 처리하고 그것에 대한 값을 돌려주는 일을 한다. 여기에서 우리는 함수를 만들기 위해서는 다음 3개의 구성요소가 필요함을 알 수 있다.
  • 입력 부
  • 처리부
  • 출력 부

함수의 모습은 다음과 같이 코드로 나타낼 수 있는데, 모든 함수는 아래의 기본적인 형태를 따른다.
return type FunctioNname(arg1, arg2, ....)
{
// 실행 코드
...
...

return Value; // 결과 값을 리턴한다.
}

1.2 입력부 : 함수 인자

함수는 어떤 일을 처리하기 위한 단위 코드 조각이다. 일을 처리하기 위해서는 처리할 데이터를 받아야 할 것이다. 이러한 처리해야 하는 데이터를 인자라고 한다. 여러분이 두개의 수를 비교해서 큰 수를 찾아내는 프로그램을 만들려고 한다면, 2개의 인자를 가지는 함수를 만들어야 할 것이다.
int diff(int a, int b)
{
if ( a < b )
return b;
else
return a;
}

각각의 인자는 처리해야 하는 데이터와 일치하는 타입이 명시되어야 한다.

1.3 출력부 : 함수값 리턴

인자로 주어진 데이터를 가지고 어떤일을 처리했다면, 함수를 호출한 함수에게 처리 값을 알려줘야 할 것이다. 이렇게 자신을 호출한 함수에게 알려주는 값을 리턴값이라고 하며, return 문을 이용해서 값을 리턴할 수 있다. 리턴하는 값은 함수의 return type과 반드시 일치해야만 한다. 사람이라면 대충 처리결과가 문자열인지, 숫자인지 알아낼 수 있지만 C 컴파일러는 이를 구분해낼 수 없기 때문이다.

#include <stdio.h>

int sum(int a, int b)
{
return a+b;
}
int main()
{
int rtv;
int a = 20;
int b = 10;
rtv = sum(a, b);
printf("%d + %d = %d\n", a, b, rtv);
}
sum 함수는 int형 인자 두개를 받아들인다음, 이것을 더한 값을 리턴한다. 위의 경우 30이 리턴될 것이다. 위 프로그램을 실행시키기 전에 실행결과를 미리 예측해보고, 예측과 실행결과가 맞았는지를 체크해보기 바란다.

1.4 void

때때로 리턴값이 필요 없는 함수도 있을 것이다. 예를 들어 이름을 입력하면 "안녕하세요 누구누구씨"라고 답변하는 함수가 있다고 하면, 굳이 자신을 호출한 함수에게 결과값을 넘겨줄 필요 없이, 인삿말을 출력하고 종료해도 될 것이다.

void비어있는 타입이란 뜻으로 함수의 리턴값을 void로 하면, 리턴값이 없는 함수를 만들 수 있다.
#include <stdio.h>

void hello(char *name)
{
printf("안녕하세요 %s님\n", name);
}

int main()
{
hello("yundream");
}
프로그램의 도움말을 출력하는 함수같은 경우에도 굳이 리턴값이 필요없으므로 void 형으로 작성해도 될 것이다.
void help()
{
printf("Usage : ./test a b\n");
}

1.5 main 함수

main 은 특수한 함수로, 실행되는 모든 C로 작성된 프로그램은 반드시 하나의 main() 함수를 가지고 있어야 한다. 모든 함수는 main() 함수에서 시작된다. 선조뻘 되는 함수라고 볼수 있겠다.

main 함수역시 다음과 같이 리턴값과 인자들을 가질 수 있으며, 이들을 통해서 처리할 데이터를 입력받고, 처리된 결과를 리턴할 수 있다.

int main(int argc, char **argv)
{
...
...
}

다른 함수들이야 main 함수에서 호출되니까. main 함수 혹은 상위 함수로 부터 인자를 받을 수 있다지만 main함수는 어디에서 인자를 받아야 하는가.

main 함수는 실행하는 명령으로 부터 인자를 받는다. 그래서 함수인자를 받는다라고 하지 않고, 실행인자를 받는다고 한다. 2번째 인자에, 프로그램을 실행시킬시 주어지는 인자가 전달이 된다. 1번째 인자는 받아들인 인자의 갯수다.

예를들어 키우고 있는 동물의 목록을 입력받아서 출력하는 프로그램이 있다고 가정해보자. 이 프로그램의 이름은 hello_pat으로 안녕 개, 안녕 고양이 식으로 입력받은 동물의 이름을 출력한다. 쉘에서 다음과 같이 실행할 것이다.
# ./hello_pat 개 고양이 악어 
안녕 개
안녕 고양이
안녕 악어

다음은 완성된 프로그램이다.
#include <stdio.h>

int main(int argc, char **argv)
{
int i = 0;

printf("동물의 수 : %d\n\n", argc);
for (i = 0; i < argc; i++)
{
printf("안녕 %s\n", argv[i]);
}
return 0;
}

이 프로그램을 실행시켜 보자.
# ./hello_pat 개 고양이
동물의 수 : 3

안녕 ./hello_pat
안녕 개
안녕 고양이
공백문자를 기준으로 해서 인자의 목록을 받아들이는 것을 알 수 있다. 그런데, 버그가 발견되었다. 프로그램이름까지 실행인자로 포함되어서 안녕 ./hello_pat까지 출력되어 버렸다. 주의할 점인데, 프로그램의 실행인자는 프로그램자신까지를 포함한다. 그러므로 배열의 0 번째는 프로그램이름이 들어간다. 아래는 버그를 수정한 프로그램이다.
  printf("동물의 수 : %d\n\n", argc - 1);
for (i = 1; i < argc; i++)
{
printf("안녕 %s\n", argv[i]);
}
여러분은 아직 배열포인터를 배우지 않았기 때문에 main 함수의 인자인 char **argv가 의미하는 바를 명확히 알지 못할 것이다. 배열과 포인터는 다음장에서 다룰 것이다. 우선은 목록을 저장하기 위한 데이터 단위정도로만 알고 넘어가도록 하자.

1.6 int main ?

함수의 리턴값이 자기를 호출한 함수에게 결과값을 넘겨주기 위해서 필요하다는 것을 이해했을 것이다. 그렇다면 main 함수의 리턴값은 어떻게 설명해야 할까. main은 가장 상위에 있는 함수인데, int main 이라면 누구에게 값을 넘겨줄 건지가 궁금할 것이다.

main의 리턴값은 자신을 실행시킨 프로세스에게 넘겨진다. 보통은 shell에서 프로그램을 실행시킬테니, 실행 shell로 값이 리턴된다. bash shell을 사용하고 있다면 다음과 같이 리턴값을 확인해 볼 수 있다.

main 함수의 리턴값이 정말 전달되는지 확인해보자. 아래와 같은 간단한 코드를 만들고 main_return.c 로 저장한 후 컴파일 해보자.
int main(int argc, char **argv)
{
return 2;
}

이제 실행시키면, main 함수의 리턴값 2가 출력되는걸 확인할 수 있을 것이다. main 함수의 리턴값은 프로그램이 종료될때 되돌려지는 값이므로, 리턴값 이란 용어대신 종료값이란 용어를 사용한다. 앞으로는 종료값이란 용어를 사용하도록 하겠다.
# ./main_return
# echo $?
2

$? 는 bash shell에서 최근실행 시킨 프로그램의 종료값을 저장하고 있는 특수변수다.

main 함수의 종료값은 프로그램이 어떻게 종료되었는지를 확인하기 위한 좋은 방법을 제공한다. 전통적으로 Unix에서는 프로그램이 주어진 일을 제대로 해냈다면 0을 그렇지 않다면, 0보다 큰값을 리턴하도록 하고 있다. 다음은 프로그램의 리턴값을 이용해서 프로그램을 실행하는 예이다. 쉘스크립트로 작성되었는데, 이해하는데에는 크게 어려움이 없을 것이다.
#!/bin/bash
which mutt 2>&1>&/dev/null

echo -n "Default Mail Client is "
if [ $? = 0 ]
then
echo "mutt"
else
echo "pine"
fi
which(1)는 실행 프로그램이 존재하는지를 확인하는 프로그램이다. 만약 프로그램이 존재한다면 0이 리턴되고, 그렇지 않다면 0보다 큰 값이 리턴된다. 위 스크립트 프로그램은 이러한 특성을 이용해서 which로 mutt프로그램이 있는지 확인하는 일을 한다. 만약 mutt 가 존재한다면 echo "mute"가 실행되고, 그렇지 않다면 echo "pine"이 실행된다.

1.7 exit 함수

exit(2)함수는 프로그램을 종료시키기 위해서 사용하는 함수다. 어떤 위치에서든지 exit 함수를 호출하면 프로그램은 그 즉시 종료된다. exit 함수는 하나의 int형 인자를 받아들이는데, 이 인자값은 프로그램의 종료값으로 사용된다.
int main(int argc, char **argv)
{
exit(2);
}
컴파일 후 echo $? 를 실행하면 2가 출력되는걸 확인할 수 있을 것이다.

1.8 문제

  1. 입력된 숫자의 제곱구하기
    하나의 숫자를 입력하면 제곱한 결과를 리턴하는 프로그램을 작성하라. 제곱하는 코드는 함수형태로 작성되어야 한다.
  2. int형 값을 리턴하는 함수가 있다. 그런데 함수에서 아무것도 리턴하지 않으면 어떻게 될까 ?
  3. 프로그램 중간에 종료시키고 빠져나올려면 어떻게 해야 할까 ?
  4. main 함수의 return 값을 어떤용도로 사용할 수 있을지 생각해 보자.
이 문서는 수정될 수 있습니다. 최신문서는 Joinc Wiki에서 확인하세요.
:::
2007/06/23 02:06

리눅스 환경에서의 C 프로그래밍 6장 - 흐름제어와 논리표현

1 소개

이번 장에서는 C 프로그램에서 사용되는 여러가지 종류의 흐름제어(Control of flow)어 알아보도록 하겠다. 흐름제어는 주어지는 조건에 따라서 프로그램의 흐름을 제어하기 위한 목적으로 사용된다. 간단한 예로 성인인증을 위한 프로그램을 만든다고 하면, 나이를 기준으로 18세 이하면, 경고메시지를 출력하게 하고, 18세 이상이면 ok 사인을 보내는 등의 흐름제어가 필요하다.

flow1.png

위의 이미지를 보면 알겠지만, 흐름제어를 위해서는 거짓을 확인하기 위한 논리적 판단이 필요함을 알 수 있다. 때문에 흐름제어와 함께, 논리표현까지 자연스럽게 다루게 될 것이다.

어떤 조건에 따라서, 프로그램의 흐름을 달리하고자 할때 사용한다. 이면 이쪽으로, 거짓이면 저쪽으로 정도로 이해할 수 있겠다.

1.1 if

if 문은 다음과 같은 3가지 형식 중 하나를 가진다.
if (expression) statement

if (expression) statement
else statment

if (expression) statement
else if (expression) statement2
...
else statementN
if문은 매우 직관적이다. 만약 ~이면 ~을 행하라. 그렇지 않으면 ~을 행하라이다. expression에는 분기를 위한 논리조건인 ~이면에 해당하는 문맥이 들어간다. 위의 성인인증예를 if문으로 표현하자면 아래와 같을 것이다. 아래의 코드는 컴파일 후 실행이 되는 완전한 코드다.
<stdio.h>

int main()
{
int age;
age = 19;
if (age >= 18) // expression
{ // --+
printf("성인 입니다."); // |-- statement
} // --+
else
{ // --+
printf("너무 어리네요"); // |-- statement2
} // --+
return 0;
}

위의 경우에는 조건이 하나였다. 하지만 18세 가능에 더불어 15세 가능이라는 또다른 조건이 들어갈 수 있을 것이다. 이 경우에는 여러개의 조건이 사용되게 되는데, 이때에는 3번째형식을 사용하면 된다. 3번째 형식을 사용하게 되면 조건이 몇개라하더라도 거기에 맞는 코드를 실행시킬 수 있다. 위의 셈플코드를 15세 인증까지 가능하도록 바꾸어 보자.
<stdio.h>

int main()
{
int age;
age = 16;
if (age >= 18)
{
printf("성인 입니다.\n");
}
else if ((age >=15) && (age < 18))
{
printf("15세 이상 컨텐츠를 이용할 수 있습니다\n");
}
else
{
printf("나이를 좀 더 먹고 오세요\n");
}
return 0;
}

1.2 while 문

구체적이지 못한 추상적인 문제도 효과적으로 풀어내는 인간과 달리, 1과 0만을 구분할 수 있는 컴퓨터는 인간처럼 감각적으로 문제를 풀 수가 없다. 여러분의 눈앞에 1부터 k까지의 13장의 카드가 있다면, 여러분은 직관의 능력을 이용해서 어렵잖게 재배치를 할 수가 있다.

그러나 컴퓨터는 그렇게 하지 못한다. 컴퓨터로 이런일을 하려면 카드를 한장씩 일일이 비교해서 참인지 거짓인지 확인해서 배치를 하는 반복적인 작업을 해야만 한다. 그래서 while, do, for 와 같은 루프문이 필요하게 되며, 모든 언어를 통해서 프로그래밍을 할때, 가장 중요하며, 가장 자주 사용되는 요소이기도 하다. if 문과 while 문만 알아도 프로그램을 짤 수 있다는 얘기가 여기에서 나온다.

이러한 루프문중 가장 널리 쓰이는 while문에 대해서 우선 알아보도록 하겠다. while은 다음과 같은 형식을 가진다.
while(expression)
{ // ---+
// 필요한 작업을 한다. |--- 루프 블럭
} // ---+
expression결과가 참이면 계속적으로 주어진 작업을 하는 식이다. 빠른 이해를 위해서 1부터 100까지 값을 더하는 프로그램을 만들어보자.
int main()
{
int i = 0;
int sum = 0;
while(i < 101)
{
sum = sum + i;
i++;
}
printf("%d\n", sum);
}
이 프로그램은 다음과 같은 흐름을 가지게 된다.
 1. i에 0을 대입한다.
2. i와 101을 비교한다.
3. 0이 101보다 작으므로(결과가 참이므로)
4. sum = sum+i 코드를 수행한다. <--+
5. i++을 했으므로 i는 1이된다. |--> 루프 블럭의 코드를 반복 수행
6. i와 101을 비교한다. |
7. 1이 101보다 작음으로 -----------+
8. i가 101이 되면 i<101이 거짓이 되고 비로서 루프를 빠져나온다.

1.3 do while 문

조건검사를 블럭의 마지막에서 한다는걸 제외하면 while루프문과 완전히 동일하다.
do {

// 필요한 작업을 한다.

} while (i < 100);
위의 while문 예제를 do while을 써서 바꾸어 보자.
int main()
{
int i = 0;
int sum = 0;
do {
sum = sum+i;
i++;
} while( i < 101);
printf("%d\n", sum);
}
do while과 while은 완전히 동일하게 사용할 수 있다. 둘 중 어떤 것을 사용할런지는 개인의 코딩취향에 영향을 받는 경우가 많다. 보통은 do while 보다는 while문을 많이 사용한다.

다음은 do while문을 사용한 또 다른 예이다.
#include <stdio.h>

int main()
{
int input;

do
{
printf("Menu ====================\n");
printf("1. Make a new account\n");
printf("2. Delete a new account\n");
printf("3. Deposit\n");
printf("4. Withdraw\n");
printf("5. Exit\n");
printf("> ");
scanf("%d", &input);
} while((input > 0 && input < 6) || (input == 5));
return 0;
}
위 프로그램은 메뉴를 출력하는 프로그램이다. scanf()함수는 키보드로 부터 입력을 받는 함수다. 이러한 메뉴프로그램의 경우 조건검사를 루프의 마지막에 하는게 더욱 자연스러운데, 이럴 경우 while문 보다 do while문을 사용하면 더 보기 좋은 코드를 만들 수 있기 때문이다.


1.4 for 문

조건을 만족하는 동안 루프를 순환한다.
for (initialize; check; update) 
{
// 필요한 작업을 한다.
}
  • initialize : 검사할 값을 초기화 한다.
  • check : 조건을 만족하는지 검사한다.
  • update : 검사할 값을 update한다.
예를 들어 위의 1부터 100까지 더하는 while문으로된 프로그램은 다음과 같이 바꿀 수 있다.
int i=0;
int sum=0;
for (i = 0; i <= 100; i++)
{
sum=sum+i;
}
printf("%d\n", sum);
for (i = 0; i <= 100; i++)문을 해석해 보자면, i에 0을 넣어서 초기화 하고, i가 100보다 더 작으면 루프문을 계속수행하라. 루프문을 수행했다면 i에 1을 더한다. 정도로 해석할 수 있다. 어떻게 보면 while문을 좀 더 단순하게 표현했다고 볼 수 있는데, 실제 while이나 do while문에 비해서 문장이 한눈에 잘 들어온다.

예를들어서 무한루프를 돌면서 변수에 1씩 더해주는 프로그램은 forwhile문을 이용해서 아래와 같이 동일하게 코딩할 수 있다.
// while 버젼
int i = 0;
while(1)
{
printf("%d\n",i);
i++;
}

// for 버젼
for(i=0;;i++)
{
printf("%d\n",i);
}
대부분의 경우 for 문을 사용할지 while문을 사용할지는 순전히 개인의 기호에 따라 나뉜다.

1.5 switch 문

if문의 단점이라면, 조건이 여러개일 때 소스코드가 어지러워진다는 점이 될 것이다.
if (expression) statement
if else (expression) statement2
if else (expression) statement3
...
else statementN
이걸 switch를 이용하면 좀더 깔끔하게 바꿀 수 있다. switch문은 다음과 같은 형식을 가진다.
switch(expression)
{
case (const1): statments
case (const2): statments2
case (const3): statments3
default:
}

1.6 break문

switch 문을 이용하면, 몇몇 경우에 있어서 좀더 깔끔한 조건 분기의 처리가 가능하겠지만 한가지 문제점이 있다. 그것은 case를 한번 만족하게 되면, 하위 case의 조건을 검사하지 않는 다는 점이다. 즉 전부 이 되어버리는 문제가 발생한다.

int main()
{
int menu=2;

switch(menu)
{
case (1):
printf("1. insert\n");
case (2):
printf("2. delete\n");
case (3):
printf("3. update\n");
case (4):
printf("4. quit\n");
default:
printf("error : unknown\n");
}
}
변수 menu에 2가 대입되었으므로, 우리는 case (2)의 문맥만 실행되기는걸 예상할 수 있겠지만, case (2), case (3), case (4), default 문까지 몽땅 실행 되어버리는걸 확인할 수 있다. break; 문을 이용하면 해당 루프를 즉시 빠져나오게 됨으로 이러한 문제를 해결할 수 있다.

루프의 순환을 중단하고 즉시 빠져나온다는 break의 특성은 while, for 등의 루프문에서도 사용할 수 있다. 다음은 1부터 100까지 더하는 연산을 break 문을 통해 구현한 예다.
int i = 0;
int sum = 0;
while(1)
{
sum += i;
if (i == 100)
{
break;
}
i++;
}
printf("Result : %d\n", sum);

1.7 continue

루프의 수행 중에 continue를 만나게 되면, continue의 아래코드를 건너뛰고, 즉시 루프의 처음으로 되돌아간다.
while(...) <------+
{ |
... |
if (...) |
{ |
continue; ----+
}
...
...
...
}

다음은 1에서 100까지의 숫자에 포함된 모든 짝수를 더하는 프로그램이다.
#include <stdio.h>

int main(int argc, char **argv)
{
int i =0;
int sum = 0;
for(i=0; i < 101; i++)
{
if (i%2 != 0)
continue;
sum += i;
}
printf("Sum : %d\n", sum);
}

1.8 프로그램의 가독성

continue와 break문은 루프를 빠져나오거나 특정한 코드를 뛰어넘기 위한 매우 쉬운방법을 제공하지만, 코드의 가독성을 해친다는 이유로 가능하면 사용하지 않는걸 권장한다. 프로그램의 규모가 커지고, 공동작업을 하는 경우가 많아지다 보니, 특히 남이 알아보기 쉬운 코드를 작성하는 것도 중요한 일로 간주되고 있다.

코드든 다른 것이든지 간에, 가능하면 흐름이 끊기지 않고 이어지는 방향으로 나아가는게, 인간지향적인 환경을 만들어준다. 트로트감상하는 중인데, 힙합이 튀어나오면 아무래도 당황스러울 것이다. continue와 break문은 루프의 흐름을 끊어버리는 점에서 사용하기에 편리할지는 모르지만 인간지향적이지는 않은 방법으로 이는 가독성을 떨어트리는 결과로 나타나게 된다. 그러므로 필요한 경우가 아니면 이들 문은 사용하지 않는 것을 권장 한다.

위의 짝수를 더하는 프로그램과 같은 경우 아래와 같이 continue를 사용하지 바꿀 수 있을 것이다.
  for(i=0; i < 101; i++)
{
if (i%2 == 0)
sum += i;
}

이 문서는 수정될 수 있습니다. 초신문서는 Joinc Wiki에서 확인해 주세요.
:::
2007/06/22 13:49

리눅스 환경에서의 C 프로그래밍 5장 데이터 다루기

1 소개

C언어를 이용해서 프로그램을 만드는 이유는 입력받은 데이터를 가공하거나 연산해서 결과물을 출력받기 위함이다. 이 데이터는 인간의 관점에서 보자면, 문자혹은 문자열일 수도 있고, 숫자일 수도 있다. 숫자라면 정수일 수도 있고, 소수점을 가진 숫자일 수도 있다. 혹은 이들 데이터의 묶음일 수도 있다.

1.1 컴퓨터는 모든 데이터를 bit 로 본다

그러나 인간이 데이터를 숫자, 문자, 문자열등 다양하게 구별하는 것과는 달리 컴퓨터는 모든 데이터를 bit로 본다. 숫자든 문자든 문자열이든지 간에 bit의 나열일 뿐이다. 컴퓨터는 bit이외의 다른 데이터는 알 수가 없다.

사람이라면 100000은 숫자고 사람은 문자 임을 경험적으로 알 수 있다. 그러나 컴퓨터는 경험이라는 걸 가지고 있지 못하다. 둘다 비트의 나열일 뿐이다. 컴퓨터는 이게 숫자인지 문자인지 구분하지 못한다. 0인지 1인지가 중요할 뿐이다.
00000000 1000011 11000000 00010000   <--- 100000
00100101 0011001 00010111 01100001 <--- "사람"

프로그램은 컴퓨터와 대화를 하는 객체다. 프로그램을 이용해서 우리는 덧셈/곱셈을 하거나 문자열을 처리하도록 해서 필요한 결과물을 얻어낸다. 그런데, 컴퓨터는 숫자와 문자를 구분하지 못하는데, 어떻게 프로그램의 요청을 처리할 수 있을까.

컴퓨터가 처리할 수 없으므로, 프로그램을 통해서 처리하는 수 밖에 없다. 즉 똑같은 00000110이라는 정보가 주어졌을 때, 어떨 때는 숫자로 처리하도록 하고, 어떨 때는 문자로 처리하도록 하는 일을 프로그램에서 맡아주어야 한다.

1.2 데이터 타입을 통한 추상화

컴퓨터가 0과 1만을 구분해 낼수 있다는 문제를 해결하기 위해서 프로그래밍 언어들은 데이터 타입이라는 것을 두어서 이문제를 해결한다.
     숫자 97          문자 'a' 
^ ^
| |
+- 데이터 타입 --+ | 프로그래밍 언어 수준
| |
1100001 | 기계적 수준에서 본 숫자 97과 문자 'a'
위에서 처럼 컴퓨터에는 1100001 이라고 저장되어 있는 값을 프로그래밍 언어에서 어떤 데이터 타입으로 읽느냐에 따라서 숫자 97로 혹은 문자 'a'로 읽을 수도 있다.
#include <stdio.h>

int main()
{
char id = 'a';

printf("%d\n", (int)id); // 주 1
printf("%c\n", id);

printf("%c\n", (int)id + 2);
printf("%c\n", 99);
return 0;
}
주 1에서 사용된 (int) 는 cast연산자로 변수id를 int형으로 읽으라는 걸 의미한다. 그러다 보니 문자와 숫자를 더하는 등의 주 2와 같은 연산도 가능해진다.
   00000000 00000000 00000000 1100001        <---- 'a'
+ 00000000 00000000 00000000 0000010 <---- '2'
------------------------------------
00000000 00000000 00000000 1100011 <---- 문자 'c', 숫자로는 99

1.3 숫자 다루기

숫자를 다루기 위한 데이터타입으로 int, long 형이있다는 걸 알고 있을 것이다. 우선 가장 많이 쓰이는 int에 대해서 알아보자. int는 정수를 나타내기 위해서 사용하며, 4 byte의 크기를 가지고 있다. 그러므로 최대 0~2^32 범위의 정수가 들어감을 알 수 있을 것이다.

그러나 정수에는 양의 정수와 함께 음의 정수가 있다. 이를 위해서 32bit의 첫 비트를 부호를 표시하기 위해서 사용하는데, 만약 첫 비트가 1이면 음의 정수 그렇지 않고 첫 비트가 0이면 양의 정수가 된다.
00000000 00000000 00000000 00000001    <--- 양의 정수 1
11111111 11111111 11111111 11111111 <--- 음의 정수 -1
개념은 쉽게 이해할 수 있을 것인데, 한가지 혼동되는 점이 있을 것이다. -1이면 100000000 00000000 ....이 아닌가? 하는 것이다. 여기에서 보수개념이 나온다. 보수는 다음 두 가지 방식이 있는데, 보수를 이용해서 음수를 표현하게 되는 매우 중요한 개념이니 숙지하고 넘어가도록 하자.
  • 1의 보수 : 양수의 비트를 모두 반전시키는 방법이다. 5는 00000101 이고 -5는 11111010이 된다. 단순하기는 하지만 이경우 +0과 -0이 존재한다는 단점을 가진다.
  • 2의 보수 : 1의 보수에 1을 더해서 음수를 표현한다. 모든 비트를 반전시킨 후 1을 더하면 된다. 이 방식에 따르면 -5는 11111011이 된다. 이 방식은 0이 하나만 존재하며, 모든 bit가 1이면 -1이 된다.

현대의 컴퓨터는 두개의 방식중에서 2의 보수의 방식만을 이용해서 음수를 표현한다. 다음 코드의 값을 예상해보고, 실행시킨 결과와 비교해 보기 바란다.
int main()
{
int i = 1;
int j;

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

~보수연산자이다. 1은 00000000 00000000 00000000 00000001 이므로 j는 11111111 11111111 11111111 11111111 11111110이 되고, 이것은 -2가 됨을 알 수 있을 것이다. 만약 여러분이 양의 정수음의 정수로 혹은 그 반대로 바꾸고 싶다면 보수에 + 을 해주면 된다. 위 프로그램을 약간 수정해서 정수 변환 프로그램을 만들어 보기 바란다.

1.4 데이터 형변환

이상 컴퓨터는 모든 데이터를 bit로만 본다는 것을 이해했을 것이다. 이것을 인간이 보기 쉽게 일종의 약속을 통해서, 읽어들인 bit값을 숫자, 문자로 보게된다. 어차피 이것은 약속이기 때문에, 숫자나 문자 이외의 어떠한 데이터(구조체 같은)로도 가공할 수 있다.

그렇다면 각 데이터 타입을 서로 변환하는 것도 가능하리란걸 예상할 수 있을 것이다. int<->char, signed int <-> unsigned int, int <--> long 등의 데이터 타입간 변환이 가능하다. int형 데이터라 하더라도 char 데이터인것 처럼 읽자라고 약속만 하면 되기 때문이다.

이러한 약속을 가능하게 하는게 형변환연산자(캐스팅 연산자)이다.
#include <stdio.h>

int main()
{
int i=1;
unsigned int j=2;

i = ~(i << 31); // 주 1

if (j < i+1)
printf("i+1 > j \n");
else
printf("i+1 < j \n");
}
주 1은 int로 표현가능한 양의정수 중 가장 큰 수를 구하기 위한 코드다.
i << 31   은 10000000 00000000 00000000 00000000  
~(i << 31)은 01111111 11111111 11111111 11111111
이 되므로 가장큰 정수인 2147483647 이 된다.
여기에 +1 을 해서 2와 비교를 했다. 단순하게 생각하면 i는 2147483678 이 될 것이므로 j보다 크다라고 생각할 수 있겠지만 결과는 반대로 나올 것이다. 왜냐하면 i에 +1을 하면 100000000 00000000 00000000 00000000이 되는데, 이 것은 -2147483648이 되기때문이다.

이는 isigned 타입이라서 가장 왼쪽 비트가 음수를 나타내는데 사용되었기 때문이다. 이 문제는 i에 저장된 데이터를 unsigned 형으로 읽어라라고 해주는 것으로 해결할 수 있다. cast 연산자는 이럴 때 사용한다.
if (j < i+1) 을 
if (j < (unsigned int)i+1) 로 변경한다.
위에서 처럼 변경하고 컴파일 한 후, 다시한번 실행시켜 보면, 원하는 데로 결과가 나오는 것을 확인할 수 있을 것이다. 캐스트 연산자를 사용함으로써 가장왼쪽 비트를 음수를 나타내기 위한 표시가 아닌 값(2^31)으로 읽어들였기 때문이다.

데이터를 비교할 때는 타입을 정확히 맞추어 주어야 한다.
위의 코드가 문제가 되는 것은 비교할 타입을 명확히 하지 않았기 때문이다. int형은 int형끼리, 또한 같은 int형이더라도 signed와 unsigned도 서로 맞추어 주어야 한다. 타입을 서로 다르게 하더라도 컴파일되고 실행이 되지만 위에서 처럼 전혀 엉뚱한 결과가 나올 수 있기 때문이다.

이 문서는 수정될 수 있습니다. 최신 문서는 Joinc Wiki를 확인해 주세요.
:::
2007/06/18 16:10

리눅스 환경에서의 C 프로그래밍 - 4장 변수와 연산자

1 변수와 연산자

1.1 C에서 사용되는 문자들

인간이 쓰는 언어의 숫자만 해도 아마 100여개가 넘을거라고 생각된다. C언어는 기계어를 대신해서 사용할 수 있도록 인간의 언어로 쓰여진 프로그래밍 언어인데, 그렇다면 인간의 언어중 어떤 언어를 사용하고 있을까. 답은 영어다. 컴퓨터라는 기계가 영어문명권에서 발명이 되다보니, 컴퓨터에서 프로그램을 만들기 위해 사용한 C언어도 자연스럽게 영어 알파벳을 기본 문자로 사용하도록 만들어 졌다. - 한글로된 컴퓨터 언어를 사용해보자라고 해서 씨앗이라는 한글로 사용가능한 언어가 있었기는 했다. 당시에는 꽤 주목을 받기도 했었는데, 소리소문 없이 잊혀지고 말았다. 그때가 내가 대학 1학년 때인가? 되었던 듯 싶다. -

C에서 사용하는 문자들은 다음과 같다.
a b c d e f g h i j k l m n o p q r s t u v w x y z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
0 1 2 3 4 5 6 7 8 9
! " # % & ' ( ) * + , - . /
: ; < = > ? [ \ ] ^ _ { | } ~
space, tab, newline, form feed
위의 문자셋은 C언어 뿐만 아니라. 다른 거의 대부분의 프로그래밍 언어에서도 기본으로 사용하고 있다.

2바이트 이상의 문자는 주석외에는 사용할 수 없는게 일반적이다.

1.2 변수와 상수

컴퓨터는 연산을 빠르게 하기 위한 기계로, 연속된 수 많은 연산들을 처리해 나간다. 연산에는 연산자피연산자가 필요하다는 것은 누구나 알고 있을 것이다.
    +-------------- 피연산자
|
+-------+
| |
1 + 2
|
+-------------- 연산자
이제 연속된 연산, 예를들어서 1부터 100까지의 숫자를 더하는 연산을 해야한다고 가정해보자. 단순하게 1부터 100까지 더할경우 100번의 덧셈이 필요하게 되는데, 이를 위해서는 각각의 연산결과를 어딘가에 저장을 해야 한다. 이런경우 암산능력이 좋은 사람은 머리에 연산결과를 기억할 것이고, 그렇지 못한 사람은 종이등을 이용하게 될 것이다.

컴퓨터는 메모리공간을 이용한다. 위의 1부터 100까지 더하는 연산을 할경우 매번 메모리의 저장장소에 연산결과를 넣었다 뺐다하는 일을 한다. 이런 컴퓨터의 메모리 공간에는 일련의 숫자로된 주소가 메겨져 있다. 여러분의 컴퓨터의 메모리가 2 Gigabyte라면, 0부터 2147483647까지의 번호가 매겨져 있고, 이중 사용하지 않는 곳의 주소의 번호를 알아내어서 그곳에 연산결과를 저장하는 식이다.

여러분이 연산결과를 저장하기 위해서 2184912 번째 주소를 사용해야 한다라고 가정해보자. 기억해서 사용하기가 그렇게 만만치 않을 것이다. 그래서 사용하기 쉽게, 숫자대신 알파벳문자를 사용해서 해당 주소를 대신 가리키게 된다. 이때 연산의 결과로 저장되는 값을 변수라고 하고, 변수가 저장될 공간을 쉽게 사용하기 위해서 영어로 명명한 것을 변수명 이라고 한다.

변하는 수가 있으면 변치 않는 수가 있을 것이다. 이를 상수라고 하며, 마찬가지로 상수명을 이용해서 상수가 저장될 메모리 공간을 가리키게 된다.

              +----------<--------- sum = 1+2
|
+-----------+---+---------+
| 메모리 | | |
+-----------+---+---------+
위의 그림에서 sum이 변수명이 되고, sum에는 연산의 결과인 3이 저장된다.

간단하게 1+10을 더하는 프로그램을 만들어 보자.
#include <stdio.h>

int main()
{
int a, b;
int sum;

a = 1;
b = 10
sum = a+b;
printf("%d\n", sum);
}
아직 C문법을 공부하진 못했지만, 대략 이해하는데 어려움이 없을 것이다.
  1. a = 1 : 변수 a가 가리키는 곳에 1을 저장하고
  2. b = 10 : 변수 b가 가리키는 곳에 10을 저장한다.
  3. sum = a+b : 변수 a가 가리키는 곳의 값 1과 변수 b가 가리키는 곳에 저장된 값 10을 가져와서 더하고, 그 결과값인 11을 sum에 저장한다.
  4. 변수 sum이 가리키는 곳에 저장된 값 11을 가져오고 printf라는 함수를 이용해서 화면에 출력한다.

1.2.1 명명 규칙

변수명과 상수명은 알파벳 영문자와 소문자, 숫자,_가 사용될 수 있다. 또한 첫글자는 반드시 '영문소문자','대문자', '_'여야 한다.
변수명 허용여부 설명
sum1 O
Sum_2 O
_sum O
1sum X 첫머리에 숫자가 왔다
sum 1 X 공백이 존재하면 안된다
sum!2 X 특수문자는 사용될 수 없다

1.3 데이터 형과 크기

1.3.1 기본 데이터 형

인간과 달리 사물을 추상화 시켜서 생각할 수 있는 능력이 없는 컴퓨터이다 보니, 데이터가 어떤 종류의 것이며 (데이터형), 어느정도의 크기 (데이터 크기)를 가지고 있는지를 명확히 알려줘야 할 필요가 있다.

인간은 "천원은 큰돈이다"와 "1000+2000=3000" 에서 문자열 "천"과 숫자 "1000"을 구분해 낼 수 있지만 컴퓨터는 도대체 구분해 낼 수가 없기 때문이다.

때문에 모든 언어는 고유의 크기를 가지는 데이터 (Type)을 제공하고 있다. C언어는 다음과 같은 크기를 가지는 데이터 형이 준비되어 있다.
데이터 형 크기 설명
int 4byte 정수형 숫자
char 1byte 단일 문자
float 4byte 실수
double 8byte 실수

예를 들어 데이터타입이 inta라는 변수가 있다고 가정했을 때, 여기에 저장할 수 있는 데이터의 크기는 2^32 이니 0 - 4294967296이 될거라고 생각할 수 있다. 그러나 실제로는 음수를 표현해야 하기 때문에 -2147483648 - 2147483647사이의 값을 저장할 수 있다.

char 형은 하나의 문자를 저장하기 위해서 사용하는데, 2^8에서 음수를 표현해야 하므로 -127 - 128의 값을 사용할 수 있다.

float,과 double 형은 소숫점을 가지는 실수를 표현하기 위해서 사용한다.

1.3.2 signed 와 unsigned형

정수에는 음의 정수와 양의 정수가 있다. 그렇다면 컴퓨터에서 어떻게 음수와 양수가 처리되는지에 대해서 알아보도록 하자. 정수를 저장하기 위한 데이터형은 4byte의 크기를 가지는 int 형이다. 이것을 컴퓨터 메모리 상에서 보자면 다음과 같은 모습을 하고 있을 것이다.
  |<----------- 32 --------->|
31 ... 43210
+--------------- -------+
| ... |
+---------------- -------+
int형 변수 a에 1을 저장했다면, 0번째 비트에 1이, 2를 저장했다면 1번째 비트에 1, 2번째 비트에 0이 저장될 것이다. -1을 저장하고 싶다면 ? 이러한 음의 값의 표시를 위해서 마지막 비트를 따로 남겨 두었다. 즉 31번째 비트에 1이 있으면 음수, 0이 있으면 양수라고 약속을 한 것이다. 다음은 int형 정수가 메모리 상에 어떻게 저장되는지를 나타내고 있다.
  • 2 : 00000000 0000000 00000000 00000010
  • 1 : 00000000 0000000 00000000 00000001
  • 0 : 00000000 0000000 00000000 00000000
여기에서 -가 되면 아래와 같이 표현된다.
  • -1 : 11111111 11111111 11111111 11111111
  • -2 : 11111111 11111111 11111111 11111110
  • -3 : 11111111 11111111 11111111 11111101
이렇게 해보면 양의 정수로 가장큰 수와 음의 정수중 가장 작은 수는 아래와 같음을 계산할 수 있다.
  • 가장 큰 양의 정수 : 011111111 11111111 11111111 11111111 (2147483647)
  • 가장 작은 양의 정수 : 100000000 00000000 00000000 00000000 (-2147483648)

약간 혼란스럽기는 하겠지만, 이해하는데 크게 어려움은 없을 것이다.

이렇게 해서 음의 수를 표현하긴 했는데, 대신에 저장가능한 숫자의 크기가 2^32 에서 2^31으로 2배만큼 줄어듬을 알 수 있다. 그렇다면 음수를 사용할 필요가 없을 경우 굳이 마지막 비트를 음수인지 양수인지를 판단하기 위해서 낭비할 필요가 없을 것이다.

이를 위해서 unsigned가 제공된다. 부호없는 뜻으로 해석하면 되며, 기본 자료형앞에 써주기만 하면 된다. unsigned int부호없는 정수를 저장하기 위한 데이터형이다. 동일하게 unsigned char, unsigned double 등의 자료형을 사용할 수 있다. unsigned가 생략될 경우 signed가 적용된다.

100000000 00000000 00000000 00000000가 주어질 경우 signed int 에서는 -2147483648이던 것이 unsigned int 에서는 2147483648이 되는 식이다.

다음은 signed와 unsigned를 이해하기 위한 간단한 C 코드다.
#include <stdio.h>
#include <string.h>

int main()
{
int a = -2147483648;
printf("%d\n", a);
printf("%u\n", a);
printf("================\n");
a = a + 1;
printf("%d\n", a);
printf("%u\n", a);
}
아래와 같이 컴파일 시키고 결과까지 확인해 보도록 하자.
# gcc -o signed signed.c
# ./signed
-2147483648
2147483648
================
-2147483647
2147483649
printf 함수는 값을 포맷에 맞추어 화면에 출력시키기 위해서 사용하는 함수다. %d는 주어진 인자를 signed int 형으로 출력하라는 포맷옵션이고, %uunsigned 형으로 출력하라는 포맷옵션이다.

1.4 상수

변수는 말그대로 변하는 수이고, 상수는 말그대로 변하지 않는 수이다.

사용자의 나이를 입력받아서 어떤 일을 하는 프로그램을 만들어야 하는 경우를 생각해보자. 대부분의 사용자가 정상적으로 입력하겠지만, 900, 28129315 와 같은 터무니 없는 수를 입력하는 경우도 발생할 것이다. 이런 문제는 입력가능한 최대값을 정해놓고 비교하는 것으로 해결가능 할것이다.

아래는 컴파일 후 실행가능한 예제 프로그램이다. 이 프로그램은 사용자의 입력을 숫자로 변환한다음 250을 초과하는지를 검사한다.
#include <stdio.h>


int main(int argc, char **argv)
{
int age; // ** 변수 age의 선언
char buf[12]; // 사용자의 입력을 저장할 버퍼
const int maxage=250; // ** 상수 maxage의 선언

age = 0; // age의 기본값으로 0을 입력한다.
printf("Your Age is ? ");
fgets(buf, 11, stdin); // 표준입력으로 입력을 받아들여서 buf에 저장한다.
age = atoi(buf); // 읽어들인 값을 int 정수형으로 변환한다.

if (age > maxage) // age와 maxage를 비교한다.
{
printf("Are you crazy ? \n");
printf("Max age is %d\n", maxage);
return 1;
}
printf("OK your age is %d\n", age);
return 0;
}
상수는 const 키워드를 이름 앞에 붙이면된다. 예를 들어 문자열 상수를 선언하기를 원한다면, const char *name = "yundream" 하면 된다. const가 일단 붙으면, 선언할때에만 값을 넣어줄 수 있으며, 중간에 값을 변경할 수 없다. age= atoi(buf) 다음줄에 maxage=180를 넣어보기 바란다. 실행은 커녕 컴파일조차 안될 것이다.
# gcc -o input input.c
input.c: In function ‘main’:
input.c:13: error: assignment of read-only variable ‘maxage’

상수를 사용하는 이유는 뭘까 ?
상수는 권한을 정함으로써, 프로그래머의 실수를 미연에 방지하고자 할 때 유용하게 사용할 수 있다. 위의 예에서와 같이 maxage는 고정된 값으로 프로그램 여기저기에서 사용될 수 있을 것이다. 만약 이게 변수라면, 프로그래머가 실수로, 한쪽에서는 150, 다른 함수에서는 100 으로 입력하는 등의 문제가 발생할 수 있을 것이고, 이는 프로그램의 심각한 논리적 버그가 될 수 있다. const를 이용해서 변수를 상수로 선언함으로써 이러한 문제를 예방할 수 있다.

1.4.1 변수의 선언과 정의

엄격히 하자면 선언이름만 만드는 것이고, 정의는 선언된 이름에 을 주는 행위다.
  1. int i; // int형 변수 i를 선언하다.
  2. int i=a; // int형 변수 i를 정의하다.
그러나 C에서 변수는 선언과 동시에 정의가 이루어 지므로 차이가 없으므로, 변수 x를 선언했다는 의미는 변수에 대한 선언과 정의까지를 의미한다. C 컴파일러가 int i를 선언함과 동시에 4byte의 메모리 공간을 할당하고, 그 메모리에 있던 값을 사용하게 되기 때문이다. 메모리에 있던 값이 어떤 값인데? 물론 그것은 알 도리가 없다. 프로그램의 실행시 할당된 4byte의 메모리에 어떤값이 있느냐에 따라 달라지기 때문이다. 아래의 프로그램을 연속해서 실행시켜 보기 바란다.
#include <stdio.h>
int main()
{
int i;
printf("%d\n", i);
}
실행할 때 마다 값이 달라지는 것을 알 수 있을 것이다. 이렇게 변수를 선언할 경우 어떤 값이 들어 있을지 알 수 없으므로, 선언과 동시에 값을 입력해주는 초기화작업이 필요하다. 초기화 작업을 하지 않을 경우 심각한 문제를 가진 프로그램이 만들어 질 수 있기 때문이다.
<!> 아래의 예제는 좀 더 쉬운 예제로 변경할 필요가 있음..
#include <stdio.h>

int main()
{
int i = 0;
int k;
while(k <= 100)
{
i = i+k;
k++;
}
printf("%d\n", i);
}
1부터 100까지의 모든 수를 더하는 프로그램을 만드는게 목적이였지만, i의 값을 초기화 해주지 않은 이유로 전혀 엉뚱한 결과가 출력이 될 것이다. 아래와 같은 방법으로 선언과 동시에 초기화 하거나 혹은 사용하기 전에 초기화 해주는 센스가 필요하다.
  int k = 0;
혹은
int k;
k = 0;
while(i< 100)

이미 몇개의 셈플 프로그램의 코드를 (비록 완전히 이해 하지 못했겠지만)보아 왔으므로, 어떻게 변수를 선언해야 하는지에 대한 감은 잡고 있으리라 생각된다. 몇개의 예를 더 드는 것으로 설명을 마치도록 하겠다.
  1. char a : 하나의 문자를 저장하기 위한 문자형 변수 a의 선언
  2. long a : long형 변수 a의 선언
  3. char *a : 메모리의 주소정보를 저장하기 위한 포인터 변수 a의 선언
    • 포인터는 나중에 따로 다룰 것이다.

1.5 연산자

연산자는 단일 문자혹은 단어로 내부함수를 사용할 수 있도록 한다. 우리는 이미 몇개의 예제 프로그램을 통해서 사칙연산을 위한 연산자, 비교연산자를 사용했었다. 이러한 연산자는 결과를 출력하기 위해서 하나이상의 피연산자를 필요로 한다. 사칙연산자라면 2개의 피연산자를 필요로 할 것이다. 예를 들어 덧셈을 위한 연산자인 +를 이용해서 4 + 5를 했다면 2개의 피연산자를 더한 결과로 9를 얻게 될 것이다.

C는 많은 수의 연산자를 제공하는데, 대략 3개의 큰 카테고리로 분류할 수 있다.
  • 대입 연산자 : 변수에 값을 대입하기 위한 연산자
  • 산술 연산자 : +, -, *, / 와 같은 수치계산을 위한 연산자
  • 비교 연산자 : >, =, < 와 같은 비교를 위한 연산자
  • 기타 연산자 : 논리 연산자, 비트 연산자, 캐스트 연산자
이들 연산자는 카테고리 별로 자세히 살펴보도록 하겠다.

1.5.1 대입 연산자

+,- 혹은 함수의 계산에 의해서 만들어진 결과는 변수에 저장이 되어야 한다. 이렇게 어떤 결과값을 다른 변수에 저장하기 위해 사용하는 연산자가 대입 연산자이다. 대입연산자는 =를 사용하며, 하나의 피연산자만을 가진다.

아래에 대입연산자를 사용하는 방법이 나왔다.
int eng, kor, math, total; 

eng = 85;
kor = 76;
math = 80;

total = eng+kor+math;
대입연산자는 =를 기준으로 오른쪽의 값을 왼쪽의 변수에 밀어넣는다. 오른쪽의 값을 변수에 저장한다고 이해하면 된다. 위 코드에서 eng변수에 85가 저장되었음을 알 수 있다.

왼쪽에는 반드시 변수가 와야 한다. 아래는 잘못된 코드다
5 = 2 + 3;

일반적으로 '''='''를 같다라는 의미로 사용하는 경우가 있는데, C에서 '''='''는 비교를 위해서 사용하는 연산자가 아니다. C는 같음을 비교를 위해서 '''=='''를 사용한다. 경험있는 프로그래머도 '''=='''를 쓸곳에 '''='''를 쓰는 잘못은 흔하게 한다.

1.5.2 일반 수치연산자

가장 기본적인 연산을 위해서 사용된다. 사칙연산을 위한 연산자가 대표적이다. 다음은 C언어에서 지원하는 일반 수치연산자들이다. 이들 연산자는 C뿐만 아닌 다른 모든 언어에서 공통적으로 찾아볼 수 있다.
  • + : 덧셈
  • - : 뺄셈
  • / : 나눗셈
  • * : 곱셈
  • % : 나머지값
+-는 역수를 만들기 위한 목적으로도 사용된다.
a = 5;
b = -a;
// b에는 -5가 대입된다.

1.5.3 논리 연산자

논리 연산자는 또는 거짓을 판별하기 위해서 사용된다. C는 다음과 같은 연산자를 지원한다.
  • && : AND (모두 참일 때 참)
  • || : OR (하나라도 참이면 참)
  • ! : NOT (참이면 거짓, 거짓이면 참)

아래의 경우 ab보다 크고, bc보다 큰 조건이 모두 만족할 때 참이 된다.
( a > b) && ( b > c)

아래는 약간 더 복잡한 경우다.
(a > b) || !(a > c)
a가 b보다 더크거나 혹은 a 가 c보다 크지 않다면 참이 된다.

1.5.4 증감 연산자

C에서는 ++--라는 특수한 형태의 연산자를 제공한다. 이들은 각각 증가연산자감소 연산자라고 불리운다.
  • ++ : 변수에 1을 더한다
  • -- : 변수에서 1을 뺀다.

a = a + 1a++와 동일한 결과를 보여준다. 또한 ++a와도 동일한다. 코드의 양을 줄여서 가독성을 높이기 위한 목적으로 주로 사용된다- 증감연산자를 사용한다고 해서 반드시 가독성이 좋아지는 건 아니긴 하다 -. 이들 증감연산자는 포인터의 위치를 증가하거나 감소하기 위한 목적으로도 사용할 수 있다. 포인터는 나중에 다루게 될 것이다.


<!> 엄격히 말하자면 a++++a는 사용되는 코드에 따라서 다른 결과를 보여줄 수도 있는데, 이는 나중에 언급하도록 하겠다.

1.5.5 비트 연산자

컴퓨터는 0-9를 사용하는 인간과 달리 0과 1로된 비트를 이용해서 계산을 한다는 것은 다들 알고 있을 것이다. 그러하다 보니 컴퓨터를 이용해서 정보를 제대로 다루기 위해서는 비트를 제대로 이해하고 다루는게 매우 중요하게 된다. 비트 연산자는 비트를 다루기 위한 목적으로 사용한다.

우리가 일반적으로 사용하는 x86컴퓨터의 경우 byte를 기본단위로 사용하게 된다. 때문에 비트연산자를 통해서 비트연산을 할 때에도 byte단위로 연산하게 된다. C는 다음과 같은 비트연산자를 준비하고 있다.
a & b 비트단위 AND AND 연산자
a | b 비트단위 OR OR 연산자
a ~ b 비트단위 exclusive
a << b 왼쪽으로 비트를 이동 쉬프트 연산자
a >> b 오른쪽으로 비트를 이동
~a 1의 보수

& 연산자는 간단하다. 십진수 10와 14가 있을경우 이 두 수를 & 연산하면 다음과 같은 결과를 보여줄 것이다. |연산은 굳이 설명하지 않도록 하겠다.
 10      00000000 00000000 00000000 00001010
14 00000000 00000000 00000000 00001110
--------------------------------------------
00000000 00000000 00000000 00001010 10

쉬프트 연산자 중 << 는 비트를 왼쪽으로 이동시킨다. 왼쪽으로 이동시키면 오른쪽이 남게 될건데, 남은 자리는 0으로 채워진다. 6<< 1하면 12가 될것이다.
    00000000 00000000 00000000 00000110
00000000 00000000 00000000 00001100

>>연산자는 오른쪽으로 이동시키는데, 주의할 필요가 있다. 오른쪽으로 이동시키면 왼쪽 비트가 남게 될건데, 이때 남는 비트는 오른쪽에 있던 비트로 채워지게 된다는 점이다. 아래의 예를 보자.
    10000000 00000000 00000000 00000000 이것을 >> 1하면
11000000 00000000 00000000 00000000 이렇게 된다.

1.6 연산자의 사용

다음은 지금까지 배운 연산자를 활용한 간단한 프로그램이다.

#include <stdio.h>

int main()
{
int my_int;
printf("일반 수치연산 : \n\n");
my_int = 6; // 대입
printf("my_int = %d, -my_int = %d\n", my_int, -my_int);

printf("int 1+2 = %d\n", 1 + 2);
printf("int 5-1 = %d\n", 5 - 1);
printf("int 5*1 = %d\n", 5 * 1);
printf("int 5/2 = %d\n", 5 / 2);
printf("int 5/2 = %d\n", 5 % 2);

printf("double 9/4 = %f\n", 9.0 / 4.0);

}
프로그램의 실행결과가 어떠할런지 예상하는건 어렵지 않을 것이다. 컴파일 후 실행시켜서 예상한 결과가 나왔는지 확인해 보도록 하자.

1.7 연산자 우선순위

연산자 우선순위는 여러개의 연산자로 이루어진 라인 코드가 있을 경우, 어떤 순서로 처리할 것인지를 결정하기 위해서 사용된다. 다음은 2+5*4 의 연산결과를 출력하는 코드다.
#include <stdio.h>

int main()
{
printf("%d\n", 2 + 5 * 4);
}
언뜻 새각하기에 28이라는 결과가 나올 거라고 예상할 수 있지만, C는 나름대로의 연산자 우선순위에 따라서 *연산을 +연 산보다 먼저하게 된다. 그래서 22라는 결과가 나오게 된다. 즉 위의 코드의 경우 C는 ((5*4) + 2)로 해석해서 계산을 한다. 눈치챘겠지만 연산순위를 무시하거나 잘못 이해할 경우 전혀 엉뚱한 잘못된 프로그램을 만들어 낼 수 있다.

다음은 C에서 적용되는 연산자 우선순위다. 참고로 결합성은 우선순위가 같은 연산자들이 여럿있을 경우 어느방향으로 처리할 건지를 결정하기 위해 사용된다. 좌->우는 왼쪽에서 먼저, 우->좌는 오른쪽에서 먼저 계산한다는 걸 의미한다.
순위 연산자 결합성
1 (), [], -> 좌-> 우
2 !-, ++, --, +(단항), -(단항), *(포인터), &, sizeof 우->좌
3 *, /, % 좌->우
4 +, - 좌->우
5 <<, >> 좌->우
6 <, <=, >, >= 좌->우
7 ==, != 좌->우
8 &(비트연산자) 좌->우
9 ~ 좌->우
10 ! 좌->우
11 && 좌->우
12 | | 좌->우
13 ?: 좌->우
14 , 우->좌

위의 우선순위를 이해했다면, 아래의 좀 복잡해 보이는 코드가 어떻게 계산될지 예상할 수 있을 것이다.
a=10*3-40/20*12+20%-2
위의 코드는 다음고 가탕이 계산된다.
a=[(10*3)-{(40/20)*15}]+{20%(-2)}

그러나 연산순위를 이해하고 있다고 하더라도 많은 연산이 들어가는 코드를 작성하다 보면 필연적으로 실수를 하게 된다. 게다가 코드의 가독성도 극적으로 떨어진다. a=10*3-40/20*12+20%-2가 무슨일을 하는지 한눈에 이해하기란 쉬운일이 아니다. 그러니 연산순위같은 것에 신경쓰지말고 괄호 ()를 이용해서 직접 우선순위를 정하는 방법을 사용하도록 한다. a=10*3-40/20*12+20%-2를 괄호를 이용하면 다음과 같이 재 작성할 수 있다. 실수도 막아줄 뿐더러, 이해하기도 훨씬 쉽다는걸 느낄 것이다.
a = ( (10*3) - ((40/20)*15) ) + ( 20%(-2) )
:::