설명

난수를 생성합니다. rand()는 0부터 RAND_MAX 사이의 난수를 생성합니다. 그러나 문제는 프로그램을 새로 실행할 때 마다 매번 다른 난수를 만들어 내지 않고 계속 같은 난수를 반복하게 됩니다.

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

int main( void)
{ 
   int   ndx;   
        
   for ( ndx = 0; ndx < 10; ndx++)
   {
      printf( "%d %dn", ndx, rand() % 100);
   }        
   
   return 0;
}
]$ ./a.out
0 83
1 86
2 77
3 15
4 93
]$ ./a.out
0 83
1 86
2 77
3 15
4 93
]$ 

결과를 보듯이 난수는 생성하지만 실행할 때마다 똑 같은 난수를 똑 같이 생성합니다.

이 문제를 해결하기 위해서는 난수를 생성하기 전에 난수를 생성하기 위한 씨앗 즉, 난수 seed를 바꾸어 주어야 하며, seed를 바꾸기 위해서는 srand()를 사용해야 합니다. 이에 대한 자세한 설명은 강좌 게시판의 "난수 만들기"를 참고하여 주십시오.

헤더stdlib.h
형태int rand( void);
반환

int

0부터 RAND_MAX 사이의 난수
예제
#include <stdio.h>
#include <stdlib.h>
#include <time.h>    // time()
#include <unistd.h>  // getpid()
int main( void)
{
   int   ndx;

   srand( (unsigned)time(NULL)+(unsigned)getpid());
   for ( ndx = 0; ndx < 5; ndx++)
      printf( "%d %dn", ndx, rand() %100 +1);

   return 0;
}

]$ ./a.out 0 45 1 48 2 72 3 60 4 78 ]$ ./a.out 0 2 1 80 2 63 3 99 4 93



설명

파일의 읽기/쓰기 위치를 파일의 처음 위치로 초기화합니다.

파일의 위치는 기준 옵션에 따라 앞으로 또는 뒤로 읽기/쓰기 위치로 건너 띕니다.

whence설명

SEEK_SET

파일의 시작
SEEK_CUR현재 읽기/쓰기 포인터 위치
SEEK_END파일의 끝

건너 띈다는 말씀은 인수로 받은 숫자의 위치로 이동한다는 뜻이 아니라 건너 띄듯이 count 한다는 뜻입니다. 아래의 그림을 참고하여 주십시오.

헤더stdio.h
형태int fseek( FILE *stream, long offset, int whence);
인수FILE *stream대상 파일 스트림
long offset이동할 바이트 수
int whence시작 시점
반환int0을 반환, 오류가 발생하면 -1을 반환
예제
#include <stdio.h>

int main( void)
{
   FILE *fp;
   char  str[1024];
   int   ndx;

   fp = fopen( "./test.txt", "r");

   printf( "%sn", fgets( str, 1024, fp));

   fseek( fp, 10, SEEK_SET);              // 파일의 시작에서 10번을 건너 띈다.
   printf( "%sn", fgets( str, 1024, fp));

   fseek( fp,  5, SEEK_SET);              // 파일의 시작에서 5번을 건너 띈다.
   fseek( fp,  5, SEEK_CUR);              // 현재 위치에서 다시 5번을 건너 띈다.
   printf( "%sn", fgets( str, 1024, fp));

   fseek( fp,  -5, SEEK_END);             // 파일의 시작에서 5번을 건너 띈다.
   printf( "%sn", fgets( str, 1024, fp));

   fclose( fp);

   return 0;
}

test.txt 파일에 16진수 문자, 0부터 f 까지 입력되어 있을 때 실행 결과입니다.

0123456789abcdef

]$ ./a.out 0123456789abcdef abcdef abcdef bcdef ]$



순차 정렬(Sequential Sort) 알고리즘 

이번에는 반복적인 방법으로 해결하는 순차 정렬(Sequential Sort) 알고리즘을 살펴볼게요. 

정렬 알고리즘은 배열의 자료를 원하는 순으로 배치하는 알고리즘을 말해요. 정렬 알고리즘은 입력 인자로 정렬할 자료들이 있는 배열의 시작 주소와 원소 개수, 비교 알고리즘이 필요합니다. 그리고 수행 후에는 배열 내의 자료들은 원하는 순서로 배치한 상태여야 합니다. 

순차 정렬은 맨 앞에서부터 제일 작은 원소를 배치하게 만들어 나가는 알고리즘이예요. 이를 위해 배치할 자리에 있는 원소를 뒤쪽에 있는 원소들과 비교하면서 작은 것을 발견하면 배치할 위치의 원소와 교환해요.

순차 정렬(base:배열의 시작 주소, n: 원소 개수, compare:비교 논리)

    반복(i:=0->n)

        반복(j:=i+1->n)

            조건(compare(base[i], base[j]) > 0)

                교환(base[i],base[j])

순차 정렬 알고리즘 바로가기


버블 정렬 (Bubble Sort) 알고리즘

이번에는 반복적인 방법으로 해결하는 버블 정렬 알고리즘을 살펴봅시다. 

 정렬 알고리즘은 배열의 자료를 원하는 순으로 배치하는 것을 말합니다. 이를 위해 입력 인자로 정렬할 자료들이 있는 배열의 시작 주소와 원소 개수, 비교 알고리즘을 전달합니다. 그리고 수행 후에는 배열 내의 자료들이 원하는 순서로 보관한 상태여야 합니다. 

 이 중에 버블 정렬은 앞에서부터 이웃하는 원소의 값을 비교하여 위치를 교환하는 것을 반복합니다. 이를 끝까지 수행하면 제일 큰 값이 맨 뒤에 위치합니다. 그리고 정렬할 개수를 1 줄인 후에 다시 반복합니다. 정렬할 원소의 개수가 1이면 모든 작업을 완료합니다. 

버블 정렬(base:배열의 시작 주소, n: 원소 개수, compare:비교 논리)

    반복(i:=n;  i>1  ; i:= i-1)

        반복(j:=1; j<i ; j:=j+1)

            조건(compare(base[j-1], base[j]) > 0)

                교환(base[j-1],base[j]) 

버블 정렬 알고리즘 바로가기

선택 정렬 (Selection Sort) 

 이번에는 반복 알고리즘일 이용하는 선택 정렬 알고리즘을 알아봅시다. 

 선택 정렬 알고리즘은 제일 큰 값을 찾아 맨 뒤의 요소와 교체하는 방법을 반복하여 전체를 정렬하는 알고리즘입니다. 물론 제일 작은 값을 찾아 맨 앞의 요소와 교체하는 방법을 반복할 수도 있습니다. 

 선택 정렬 알고리즘을 의사코드(pseudo code: 논리적인 수행 흐름을 이해할 수 있게 작성한 코드)는 다음과 같습니다. 

선택 정렬(base:컬렉션,n:원소 개수,compare:비교 논리)

    반복(i:=n;  i>1  ; i:= i-1)

        반복(max=0,j:=1; j<i ; j:=j+1)

            조건(compare(base[max], base[j]) < 0)

                max := j

        temp: = base[i-1]

        base[i-1] = base[max]

        base[max] = temp

선택 정렬 알고리즘 바로가기


삽입 정렬 (Insertion Sort) 

이번에는 반복 알고리즘 중에 삽입 정렬 알고리즘을 알아봅시다. 

 삽입 정렬 알고리즘은 점진적으로 정렬 범위를 넓혀 나가는 방식으로 정렬하는 알고리즘입니다. 이를 위해 새로운 범위에 포함하는 마지막 원소를 앞으로 이동하면서 자신보다 작은 요소를 찾을 때까지 이동하면서 자리를 교환합니다. 

삽입 정렬(base:컬렉션, n:원소 개수, compare:비교 논리)

    반복(i:=1;  i<n  ; i:= i+1)

        반복(j=i; j>0 ; j:=j-1)

            조건(compare (base [j-1], base [j]) < 0)

                temp: = base [j-1]

                base[j-1] = base [j]

                base[j] = temp

            아니면

                루프 탈출

삽입 정렬 알고리즘 바로가기

쉘 정렬 (Shell Sort) 

쉘 정렬은 삽입 정렬 알고리즘을 이용하는 정렬 방식입니다.

쉘 정렬은 같은 간격에 있는 원소들을 삽입 정렬 원리로 정렬하는 것을 반복합니다. 

간격의 초기값은 배열의 크기/2이며 간격이 1일 때까지 1/2로 줄이면서 반복합니다.

퀵 정렬(Quick Sort)

퀵 정렬 알고리즘은 재귀적인 방법으로 문제를 해결하는 알고리즘입니다. 

 퀵 정렬 알고리즘은 피벗 값을 선택하여 피벗 값보다 작은 값들은 왼쪽으로 보내고 큰 값들은 오른쪽으로 보낸 후에 이들 사이에 피벗을 위치시키는 원리를 이용합니다. 이후 피벗보다 작은 값들을 재귀 호출로 정렬하고 피벗보다 큰 값들도 재귀 호출로 정렬하는 방식입니다. 

 그런데 퀵 정렬은 어떠한 요소를 피벗으로 선택하냐에 따라 성능에 차이가 납니다. 만약 전체 요소의 중간 순위의 요소를 선택하면 재귀 호출에서 반씩 나누어 정렬을 하게 되어 좋은 성능을 발휘합니다. 하지만 가장 작은 값이나 가장 큰 값을 피벗으로 선택하면 최악의 성능을 발휘합니다. 

 여기에서는 맨 앞과 맨 뒤, 그리고 중간 위치의 요소를 비교하에 세 값 중에 중간 값을 피벗으로 선택할게요. 

퀵 정렬(base:컬렉션,n: 원소 개수, compare:비교 논리)

    조건(n<=1)

        종료

    피벗을 선택한다.

    피벗과 맨 앞의 요소를 교환한다.

    big:=0

    small:=n

    반복(big<small)

            반복(big:=big+1; big<n; big:=big+1)

                조건(compare(base[0],base[big])<0)

                    루프 탈출

            반복(small:small-1;small>0;small:small-1)

                조건(compare(base[0],base[small])>0)

                    루프 탈출

            조건(big<small)

                교환(base [big], base [small])

    교환(base [0], base [small])

    퀵 정렬(base,small, compare)

    퀵 정렬(base+big, n-big, compare)

 

 퀵 정렬 알고리즘의 탈출 조건은 n이 1보다 작거나 같을 때입니다.

퀵 정렬 알고리즘 바로가기

병합 정렬(Merge Sort, 합병 정렬) 알고리즘

이번에는 병합 정렬 알고리즘을 살펴봅시다. 

 병합 정렬 알고리즘은 배열을 작은 단위의 배열로 분할한 후에 분할한 배열을 정렬하고 이들을 다시 정렬하면서 전체 배열을 정렬하는 알고리즘입니다. 

병합 정렬(base:배열의 시작 주소, n: 원소 개수, compare:비교 논리)

    ah:= n/2

    bh:= n - ah;

    조건(n 1보다 작거나 같으면) 종료

    병합정렬(base,ah,compare)

    병합접열(base+ah,bh,compare)

    tbase에 동적 메모리 할당(원소크기*원소개수)

    메모리 복사(tbase,base)

    ai:=0

    bi:=ah

    i:=0

    반복(ai ah보다 작으면서 bi n보다 작다)

        조건(tbase[ai]가 tbase[bi]보다 작거나 같으면

            base[i] := base[ai]

            ai:= ai+1

        아니면

             base[i]:= base[bi]

             bi:= bi+1

        i:=i+1

    반복(ai가 ah보다 작다)

        base[i]:= tbase[ai]

        i:=i+1

        ai:=ai+1

    반복(bi가 n보다 작다)

        base[i]:= tbase[bi]

        i:=i+1

        bi:=bi+1

병합 정렬 알고리즘 바로가기

힙 정렬(Heap Sort)

힙 정렬은 힙 트리를 이용하는 알고리즘입니다. 최대 힙을 사용하면 크기 순(Ascend)으로 정렬하고 최소 힙을 사용하면 크기 역순(Descend)으로 정렬합니다. 

힙 정렬은 먼저 힙 트리를 구성합니다. 그리고 루트의 값과 맨 마지막 값을 교환한 후에 정렬 범위를 1 줄입니다. 이와 같은 작업을 반복하여 정렬 범위가 1일 때까지 반복합니다. 

최대 힙 트리에서 루트는 최대 값을 갖습니다. 따라서 마지막 값과 교환하면 제일 큰 값이 맨 뒤로 배치할 수 있습니다. 그리고 난 후에 정렬 범위를 줄여나가면 최종적으로 정렬 상태를 만들 수 있는 것입니다. 

힙 정렬(base:배열의 시작 주소, n: 원소 개수, compare:비교 논리)

    초기 힙 구성

    루트와 맨 마지막 자손 교환

    n 1 감소

    반복(n: n->1)

        힙 구성

        루트와 맨 마지막 자손 교환

        n을 1 감소

힙 정렬 알고리즘 바로가기


이번 강좌에서는

  • 포인터에 대한 완벽한 이해
  • *, & 단항 연산자의 의미


  우왕~ 안녕하세요 여러분. 아마 C 언어를 배웠거나 배우고 있는 사람들은 포인터에 대해 익히 들어 보셨을 것 입니다. 이해하기 힘들기로 악명 높은 그 포인터를 말이죠. 하지만, 저와 함께 한다면 큰 무리 없이 배우 실 수 있을 것이라 생각됩니다.


  포인터를 이해하기 앞서
 

  앞서 3 강에서 이야기 하였지만 모든 데이터들은 메모리 상에 특정한 공간에 저장 되어 있습니다. 우리는 앞으로 편의를 위해, 메모리의 특정한 공간을 '방' 이라고 하겠습니다. 즉, 각 방에는 데이터들이 들어가게 되는 것 입니다. 보통 사람들은 한 방의 크기를 1 바이트 라고 생각합니다. 우리가 만약 int 형 변수를 정의한다면 4 바이트 이므로 메모리 상의 4 칸을 차지하게 됩니다. 그런데 말이죠. 프로그램 작동 시 컴퓨터는 각 방에 있는 데이터를 필요로 하게 됩니다. 따라서, 서로 구분하기 위해 각 방에 고유의 '주소(address)' 를 붙여 주었습니다. 우리가 아파트에서 각 집들을 호수로 구분하는 것 처럼 말입니다. 예를 들어 우리가 아래와 같은 int 변수 a 를 정의하였다면 특정한 방에 아래 그림 처럼 변수 a 가 정의됩니다.

int a = 123; // 메모리 4 칸을 차지하게 한다.



메모리에 123 이란 수가 있고 이 수는 메모리에 0x152839 (앞에 0x 는 이 수가 16 진수로 표시되었다는 것을 의미해요) 에 위치



이 때, 0x152839 는 제가 아무렇게나 정한 이 방의 시작 주소 입니다. 참고로, 0x 가 뭐냐고 물어보는 사람들이 있을 텐데, 이전 강좌에서도 이야기 하였지만 16 진수라고 표시한 것 입니다. 즉, 16 진수로 152839 (10 진수로 1386553) 라는 위치에서 부터 4 바이트의 공간을 차지하며 123 이라는 값이 저장되어 있게 하라는 뜻이지요.

그렇다면 아래와 같은 문장은 어떻게 수행 될까요?

a = 10;


사실 컴파일러는 위 문장을 아래와 같이 바꿔주게 됩니다. 

메모리 0x152839 위치에서 부터 4 바이트의 공간에 있는 데이터를 10 으로 바꾸어라! 

결과적으로, 컴퓨터 내부에서는 올바르게 수행되겠지요. 
참고적으로 말하는 이야기 이지만 현재 (아마 이 블로그에 접속하는 사람들 중 99% 이상이) 많은 사람들은 32 비트 운영체제를 사용하고 있습니다. 이 32 비트에서 작동되는 컴퓨터들은 모두 주소값의 크기가 32 비트 (즉, 4 바이트.. 까먹었다면 2-3 강 참조) 로 나타내집니다. 즉 주소값이 0x00000000 ~ 0xFFFFFFFF 까지의 값을 가진다는 것이지요. 어랏! 조금 똑똑하신 분들이라면 32 비트로 사용할 수 있는 주소값의 가지수는 2 의 32 승 바이트, 즉 RAM 은 최대 4 GB 까지 밖에 사용할 수 없다는 사실을 알 수 있습니다. 맞습니다. 이 때문에 32 비트 운영체제에서는 RAM 의 최대 크기가 4 GB 로 제한되지요(즉, 돈을 마이 들여서 RAM 10GB 로 만들어도 컴퓨터는 4 GB 까지 밖에 인식하지 못합니다. 어찌 이렇게 슬플수가..) 1

여기까지는 상당히 직관적이고 단순해서 이해하기 쉬웠을 것 입니다. 그런데 C 를 만든 사람은 아주 유용하면서도 골때리는 것을 하나 새롭게 만들었습니다. 바로 '포인터(pointer)' 입니다. 영어를 잘하는 분들은 이미 아시겠지만 '포인터' 라는 단어의 뜻이 '가리키는 것(가르켜지는 대상체를 말하는 것이 아닙니다)' 이란 의미를 가지고 있습니다. 

사실, 포인터는 우리가 앞에서 보았던 int 나 char 변수들과 다른 것이 전혀 아닙니다. 포인터도 '변수' 입니다. int 형 변수가 정수 데이터, float 형 변수가 실수 데이터를 보관했던 것 처럼, 포인터도 특정한 데이터를 보관하는 '변수' 입니다. 그렇다면 포인터는 무엇을 보관하고 있을 까요? 

바로, 특정한 데이터가 저장된 주소값을 보관하는 변수 입니다. 여기서 강조할 부분은 '주소값' 이라는 것 이지요. 여기서 그냥 머리에 박아 넣어 버립시다. 이전에 다른 책들에서 배운 내용을 싹 다 잊어 버리고 그냥 망치로 때려 넣듯이 박아버려요. 포인터에는 특정한 데이터가 저장된 주소값을 보관하는 변수 라고 말이지요. 크게 외치세요. '주소값!!!!!' 

암튼, 뇌가 완전히 세뇌되었다고 생각하면 다음 단계로 넘어가도록 하겠습니다. 아직도 이상한 잡념이 머리에 남아 있다면 크게 숨을 호흡하시고 주소값이라고 10 번만 외쳐 보세요.. 

자. 되었습니다. 이제 포인터의 세계로 출발해 봅시다. 뿅

  포인터
 

다시 한 번 정리하자면

"포인터" : 메모리 상에 위치한 특정한 데이터의 (시작)주소값을 보관하는 변수!

우리가 변수를 정의할 때 int 나 char 처럼 여러가지 형(type) 들이 있었습니다. 그런데 놀랍게도 포인터에서도 형이 있습니다. 이 말은 포인터가 메모리 상의 int 형 데이타의 주소값을 저장하는 포인터와, char 형 데이터의 주소값을 저장하는 포인터가 서로 다르다는 말입니다. 응?? 여러분의 머리속에는 아래와 같은 생각이 번개 처럼 스쳐 지나갈 것입니다. 

"야 이 Psi 같은 놈아. 아까 포인터는 주소값을 저장하는 거래며. 근데 우리가 쓰는 컴퓨터에선 주소값이 무조건 32 비트, 즉 4 바이트래며!! 그러면 포인터의 크기는 다 똑같은것 아냐? 근데 왜 포인터가 형(type)을 가지는 건데!! 아아아아악" 

휴우우. 진정좀 하시고. 여러분 말이 백번 맞습니다 - 단, 현재 까지 배운 내용을 가지고 생각하자면 말이지요. 포인터를 아주 조금만 배우면 왜 포인터에 형(type) 이 필요한지 알게 될 것입니다. 

C 언어에서 포인터는 다음과 같이 정의할 수 있습니다. 

(포인터에 주소값이 저장되는 데이터의 형) *(포인터의 이름);

혹은 아래와 같이 정의할 수 도 있습니다.

(포인터에 주소값이 저장되는 데이터의 형)* (포인터의 이름);

  예를 들어 p 라는 포인터가 int 데이터를 가리키고 싶다고 하면 ...

int *p; // 라고 하거나
int* p; // 로 하면 된다

라 하면 올바르게 됩니다. 즉 위 포인터 p 는 int 형 데이터의 주소값을 저장하는 변수가 되는 것 입니다. 와우! 

  & 연산자
 

그런데 말입니다. 아직도 2% 부족합니다. 포인터를 정의하였으면 값을 집어 넣어야 하는데, 도대체 우리가 데이터의 주소값을 어떻게 아냐는 말입니까? 하지만, 여러분의 이러한 욕구를 충족시키는 연산자가 C 에 (당연히) 있습니다. 바로 & 연산자 이지요. 

그런데, 아마 복습을 철저하게 잘하신 분들은 당황할 수 도 있습니다. 왜냐하면 & 가 AND 연산자이기 때문입니다. (4 강 참조) 그런데, & 연산자는 두 개의 피연산자를 필요로 했습니다. 즉,

a & b; //o.k
a & // NOT ok

와 같이 언제나 2 개가 필요 하다는 것이지요. 그런데, 여기에서 소개할 & 연산자는 오직 피연산자가 1 개인 연산자 입니다. (이러한 연산자를 단항(unary) 연산자라 합니다) 즉, 위의 AND 연산자와 완전히 다르 다는 것이지요. 이는 데이터의 주소값을 불러 옵니다. 사용은 그냥 아래와 같은 꼴로 사용해 주면 됩니다.

& (주소값을 계산할 데이터)

백설(說)이 불여일행(行). 한 번 프로그램을 짜 봅시다. 

/* & 연산자 */
#include <stdio.h>
int main()
{
    int a;
    a = 2;

    printf("%x \n", &a); 
    return 0;
}

성공적으로 컴파일 했다면


와 같이 나옵니다. 참고로, 여러분의 컴퓨터에 따라 결과가 다르게 나올 수 도 있습니다. 사실, 저와 정말 인연 이상의 무언가가 있지 않는 이상 전혀 다르게 나올 것 입니다. 더 놀라운 것은 실행할 때 마다 결과가 달라질 것입니다. 

2 번째 실행한 것


위와 같이 나오는 이유는 나중에 설명하겠지만 주목할 것은 '값' 이 출력되었다는 것 입니다. 

    printf("%x \n", &a);

위 문장에서 &a 의 값을 16 진수 (%x) 로 출력하라고 명령하였습니다. 근데요. 눈치가 있는 사람이라면 금방 알겠지만 위에서 출력된 결과는 4 바이트(16 진수로 8 자리)가 아닙니다! (여러분의 컴퓨터는 다를 수 있습니다. 아무튼..) 하지만 저는 32 비트 운영체제를 사용하고 있습니다. 그렇다면 뭐가 문제인가요? 사실, 문제는 없습니다. 단순히 앞의 0 이 잘린 것 이지요. 주소값은 언제나 4 바이트 크기, 즉 16 진수로 8 자리 인데 앞에 0 이 잘려서 출력이 안된 것일 뿐입니다. 따라서 변수 a 의 주소는 아마도 0x001EF8D4 가 될 것입니다. 

 아무튼 위 결과를 보면, 적어도 제 컴퓨터 상에선 int 변수 a 는 메모리 상에서 0x001EF8D4 를 시작으로 4 바이트의 공간을 차지하고 있었다는 사실을 알 수 있습니다. 

 자, 이제 & 연산자를 사용하여 특정한 데이터의 메모리 상의 주소값을 알 수 있다는 사실을 알았으니 배고픈 포인터에게 값을 넣어 봅시다. 

/* 포인터의 시작 */
#include <stdio.h>
int main()
{
    int *p;
    int a;

    p = &a;

    printf("포인터 p 에 들어 있는 값 : %x \n", p);
    printf("int 변수 a 가 저장된 주소 : %x \n", &a);

    return 0;
}

실행해 보면 많은 이들이 예상했던 것 처럼.... 


똑같이 나옵니다. 어찌 보면 당연한 일입니다. 

    p = &a;

에서 포인터 p 에 a 의 주소를 대입하였기 때문이죠. 참고로, 한 번 정의된 변수의 주소값은 바뀌지 않습니다. 따라서 아래 printf 에서 포인터 p 에 저장된 값과 변수 a 의 주소값이 동일하게 나오게 됩니다. 어때요. 쉽죠? 

  * 표  
 

  이제, 드디어 포인터의 핵심에 다다랐습니다. 현재 까지 우리가 배운 바로는 "포인터는 특정한 데이터의 주소값을 보관한다. 이 때 포인터는 주소값을 보관하는 데이터의 형에 * 를 붙임으로써 정의되고, & 연산자로 특정한 데이터의 메모리 상의 주소값을 알아올 수 있다" 까지 알고 있습니다. 참고로 아래에서 설명하는 * 는 앞서 포인터를 정의할 때 사용하였던 * 와 다른 의미를 지닌 다는 사실을 알고 있으세요. 

  앞서 & 연산자가 2% 부족한 부분을 채워준다고 했지만 안타깝게도 1% 가 남았습니다. 하지만 * 연산자가 이 1% 를 채워질 것 입니다.... 잠깐만. * 연산자?? 어디서 많이 들어본 것 같네요.. 맞아요. * 연산자도 & 처럼 피연산자를 2 개 가질 때 에는 곱셈 연산자로 사용됩니다. 즉

a * b; // a 와 b 를 곱한다.
a *; // Not OK

하지만 이 연산자는 위 & 처럼 1 개의 피연산자를 가지는 단항 연산자 입니다. * 연산자는 쉽게 풀이하자면

 "나(포인터)를 나에게 저장된 주소값에 위치한 데이터라고 생각해!"

  라는 의미의 연산자 입니다. 한 번 아래 예제를 봅시다. 

/* * 연산자의 이용 */
#include <stdio.h>
int main()
{
    int *p;
    int a;

    p = &a;
    a = 2;

    printf("a 의 값 : %d \n", a);
    printf("*p 의 값 : %d \n", *p);

    return 0;
}

성공적으로 컴파일 한다면 


가 됩니다. 

    int *p;
    int a;

일단 int 데이터를 가리키는 포인터 p 와 int 변수 a 를 각각 정의하였습니다. 평범한 문장 이지요. 

    p = &a;
    a = 2;

그리고 포인터 p 에 a 의 주소를 집어 넣었습니다. 그리고 a 에 2 를 대입하였습니다. 

    printf("a 의 값 : %d \n", a);
    printf("*p 의 값 : %d \n", *p);

일단 위의 문장은 단순 합니다. a 의 값을 출력하란 말이지요. 당연하게도 2 가 출력됩니다. 그런데, 아래에서 *p 의 값을 출력하라고 했습니다. * 의 의미는 앞서, "나를 나에 저장된 주소값에 해당하는 데이터로 생각하시오!" 로 하게 하는 연산자라고 하였습니다. 즉, *p 를 통해 "p 에 저장된 주소(변수 a 의 주소)에 해당하는 데이타, 즉 변수 a" 를 의미할 수 있게 되었습니다. 다시 말해 *p 와 변수 a 는 정확히 동일합니다. 즉, 위 두 문장은 아래 두 문장과 10000% 일치합니다.

   printf("a 의 값 : %d \n", a);
    printf("*p 의 값 : %d \n", a);

마지막으로 * 와 관련된 예제 하나를 더 살펴 봅시다.

/* * 연산자 */
#include <stdio.h>
int main()
{
    int *p;
    int a;

    p = &a;
    *p = 3;

    printf("a 의 값 : %d \n", a);
    printf("*p 의 값 : %d \n", *p);

    return 0;
}

성공적으로 컴파일 하였다면


아마 많은 여러분들이 예상했던 결과 이길 바랍니다^^

    p = &a;
    *p = 3;

위에서도 마찬가지로 p 에 변수 a 의 주소를 집어 넣었습니다. 그리고 *p 를 통해 "나에 저장된 주소(변수 a 의 주소)에 해당하는 데이터(변수 a) 로 생각하시오" 를 의미하여 *p = 3 은 a = 3 과 동일한 의미를 지니게 되었습니다. 어때요. 간단하지요? 이로써 여러분은 포인터의 50% 이상을 이해하신 것 입니다~~! 짝짝짝짝

 자. 그럼 '포인터' 라는 말 자체의 의미를 생각해 봅시다. int 변수 a 와 포인터 p 의 메모리 상의 모습을 그리면 아래와 같습니다.

포인터 p 도, a 도 메모리 상에 각각 존재합니다만, 그 위치에는 a 의 경우 3 이 있지만 p 의 경우 a 의 주소값이 있습니다.

참고로 주소값은 제가 임의로 정한 것 입니다.


  즉, 포인터  p 는 * 를 통해 a 를 의미 할 수 (가리 킬 수) 있게 되었지요. 흔히 많은 책들은 포인터 p 가 변수 a 를 가리키고 있다 라고 말합니다. 사실 저는 이 말이 여러분에게 어렵게 다가올 까봐 여태까지 하고 있지 않았지만 아무튼, 포인터 p 에 어떤 변수 a 의 주소값이 저장되어 있다면 '포인터 p 는 변수 a 를 가리킨다' 라고 말합니다. 참고적으로 말하지만 포인터 또한 엄연한 변수 이기 때문에 특정한 메모리 공간을 차지합니다. 따라서 위 그림과 같이 포인터도 자기 자신만의 주소를 가지고 있지요. 

 이제 여러분들은 포인터에 왜 '형(type)' 이 필요한지 이야기 할 수 있는 단계가 되었습니다. (아마 여러분들 중 반 수 이상은 이미 짐작하고 계실 것 입니다.) 다시 말해 '형' 이 없다는 것은 포인터가 자신이 가리키고 있는 대상에 대해 어떠한 정보도 가지지 않아도 된다는 것 입니다. 여기서 포인터를 선언하기 위해 pointer 라는 저 만의 키워드를 이용했다고 합시다. (실제로 이런게 사용되는 것이 아닙니다;;;) 

int a;
pointer *p;
p = &a;
*p = 4;

자. 위 명령이 올바른 결과를 출력할까요? 포인터 p 에는 명백히 변수 a 의 '시작 주소' 가 들어 있습니다. '시작 주소' 란 말입니다. 즉, *p 라고 했을 때 포인터 p 에는 자신이 가리키는 대상의 시작 주소가 있지만 대상의 크기에 대한 정보가 없습니다. 헉! 컴퓨터는 *p 라고 했을 때 메모리에서 0x12345678 로 부터 (&a 가 0x12345678 이라 가정합시다) 몇 개의 바이트를 더 읽어 들어서
값을 변경해야 할 지 모른다는 말입니다. 결국 포인터는 쓸모 없게 됩니다. 

하지만, 우리가 여태까지 해왔던 것 처럼 

int a;
int *p;
p = &a;
*p = 4;

라고 한다면 어떨 까요? 컴퓨터는 0x12345678 로 부터 포인터 p 가 int * 라는 사실을 보고 "아하. 이 포인터는 int 데이터를 가리키는 구나!" 라고 알게 되어 정확히 4 바이트를 읽어 들어 값을 바꾸게 됩니다. 따라서 정확한 값이 출력될 수 있겠지요. 

여러분이 현재 까지 배운 내용에 대해 완벽하게 이해를 하고 있다면 아래와 같은 궁금증이 생길 것 입니다. 

"야. 그런데 말야. 주소값은 무조건 4 바이트 잖아? 그러면 int *p; 처럼 귀찮게 * 을 붙이지 말고 int p; 라고 한다음에 p = &a; 라고 해도 상관 없는거 아니야? 그지? 그지? 그지? " 

  훌륭한 생각입니다. 한 번 해봅시다. 

/* 될까? */
#include <stdio.h>
int main()
{
    int p;
    int a;

    p = &a;
    a = 3;

    printf("*p 의 값? : %d \n", *p);

    return 0;
}

안타깝게도 printf 부분에서 아래와 같은 오류가 출력됩니다.

error C2100: 간접 참조가 잘못되었습니다.

사실, 우리가 현재 까지 배운 내용을 바탕으로 이해해 보자면 위에서 왜 오류가 발생하는지 이해하기 힘듦니다. 그냥 단순히 * 연산자는 포인터들에게만 적용된다는 사실만을 알아 두시면 감사하겠습니다.

/* 포인터도 변수이다 */
#include <stdio.h>
int main()
{
    int a;
    int b;
    int *p;

    p = &a;
    *p = 2;
    p = &b;
    *p = 4;

    printf("a : %d \n", a);
    printf("b : %d \n", b);
    return 0;
}

성공적으로 컴파일 하였다면



    p = &a;
    *p = 2;
    p = &b;
    *p = 4;

  사실, 이런 예제까지 굳이 보여주어야 하나 하는 생각이 들었지만 그래도 혹시나 하는 마음에 했습니다. 앞에서도 말했듯이 포인터는 '변수' 입니다. 즉, 포인터에 들어간 주소값이 바뀔 수 있다는 것이지요. 위와 같이 처음에 a 를 가리켰다가, (즉 p 에 변수 a 의 주소값이 들어갔다가) 나중에 b 를 가리킬 수 (즉 p 에 변수 b 의 주소값이 들어감) 있다는 것 이지요. 뭐 특별히 중요한 예제는 아니였습니다만. 나중에 상수 포인터, 포인터 상수에 대해 이야기 하면서 다시 다루어 보도록 하겠습니다.

마지막으로, 강의를 마치며 여러분에게 포인터에 대해 완벽히 뇌리에 꽂힐 만한 동화를 들려드리겠습니다.

옛날 옛날에 .. 대략 2 년 전에 (뭐.. 전 여러분과 옛날의 정의가 다릅니다ㅋ) 변철수, 변수철, 포영희라는 세 명의 사람이 OO 아파트에 살고 있었습니다. 

int chul, sue; 
int *young;

그런데 말이죠. 포영희는 변철수를 너무나 좋아한 나머지 자기 집 대문 앞에 큰 글씨로 "우리집에 오는 것들은 모두 철수네 주세요" 라고 써 놓고 철수네 주소를 적어 놓았습니다

young = &chul;

어느날 택배 아저씨가 영희네 집에 물건을 배달하러 왔다가 영희의 메세지를 보고 철수네에 가져다 주게 됩니다.

*young = 3; // 사실 chul = 3 과 동일하다!

영희에 짝사랑이 계속 되다가 어느날 영희는 철수 보다 더 미남인 수철이를 보게 됩니다. 결국 영희는 마음이 변심하고 수철이를 좋아하기로 했죠. 영희는 자기 대문 앞에 있던 메세지를 떼 버리고 "우리집에 오는 것은 모두 수철이네 주세요." 라 쓰고 수철이네 주소를 적었습니다.

young = &sue;

며칠이 지나 택배 아저씨는 물건을 배달하러 영희네에 왔다가 메세지를 보고 이번엔 수철이네에 가져다 줍니다.

*young = 4; // 사실 sue = 4 와 동일하다

이렇게 순수한 사랑이 OO 아파트에서 모락 모락 피어났습니다..... 끝

return 0; // 종료를 나타내는 것인데, 아직 몰라도 되요. (정확히 말하면 리턴...) 

생각해 볼 문제 

* 와 & 연산자의 역할이 무엇인지 말해보세요 (난이도 : 下)

int **a; 와 같은 이중 포인터(double-pointer) 에 대해 생각해 보세요 (난이도 : 中上)


함수에서 외부의 값(실인수)을 전달받기 위해 사용되는 변수를  형식인수라한다. 이 형식인수에 외부값(실인수)이 전달되는 방식에 따라 값에 의한 호출과 참조에 의한 호출로 나뉘어 지는데, 값호출과 참조호출로 나뉜다. 일반적으로 실인수를 형식인수에 전달시 대입연산을 하는 경우와 마찬가지 동작이 일어나, 실인수의 값을 형식인수에 대입을 한다. 메모리상에선 실인수의 값을 복사한 후 형식인수에게 할당을 한다. 그 결과 같은 값을 지닌 변수가 서로 다른 메모리 공간에 2개 존재하게 된다. 이 경우 형식인수값을 조작하더라도 실인수의 값은 어떤 영향도 받지 않다. 이런 호출방식을 값호출이라고한다.


그럼 참조에 의한 호출은 어떤 것인까?

실인수의 값을 전달하는 것이 아닌 주소를 전달하는 것이다. 사실 내부적으로 보면 값호출이나 참조호출이나 같은 동작이 일어난다. 주소도 갓이므로 주소값이 형식인수에 복사가 된다. 그런데 주소값을 전달하기 위해서는 형식인수의 타입은 실인수타입의 포인터여야 한다. 즉 참조호출은 형식인수가 포인터기 때문에, 내부적으로는 실인수의 값을 알기위해서는 형식인수(포인터)에 의한 참조 방식으로 알아야 한다. 결국 이런 전달방식은 실인수에 직접 접근이 가능하다.


그럼 값호출과 참조호출의 차이점을 간단히 예를 들어보자


값에 의한 호출

위 코드의 동작후 결과 값은 3을 출력한다. 의도한바는 a의 값을 전달받아 3을 더 더해서 6이 되길 바라는 코드였다. 그런데 위에서 설명했듯이, 값에의한 호출은 값만을 형식인수에 복사하기 때문에, 실인수 a와는 아무런 상관없이 함수내부의 형식인수가 6의 값을 갖는다. 함수내부의 a라는 값은 지역변수기 때문에 함수가 리턴될 때 바로 소멸된다.


함수 내부에서 외부의 값을 다루기 위해선 포인터에의한 참조를 통해 외부값을 조작해야한다.


참조에 의한 호출

위 코드는 참조호출로 바꾼 코드다. 결과 값은 6을 출력한다.

단순히 주소값을 전달하여 내부적으로는 실인수에 접근하여 값을 조작했다. 함수내부에서 선언된 지역변수인 형식인수 a가 소멸되지만, 소멸과 관계없이 실인수 a의 값은 조작된 후다. 



그러면 이번엔 좀 더 깊게 생각해보자.


위 코드는 동적 메모리의 함수를 우해 래퍼함수인 n_call 함수를 만든 것이다. 포인터를 전달한후 동적할다을 하여 TEST라는 값을 넣었다. 그리고 실인수의 값을 출력하는 코드이다. 그런데 이상하게 제대로 동작하지 않는다.

분명 name도 포인터 변수고, name 가리키는 공간의 주소값을 넘겨주어서 그 공간에대한 메모리의 동적 할당을 하는 것인데 왜 안될까.. 이렇게 생각 될 수도 있겠다. 그러면 다음의 그림을 보자.


값에 의한 호출이 된 경우

좀 헷갈릴 수 있겠으나, 기본적인 동작은 같다. 실인수의 동적할당을 제대로 받기 위해선 실인수의 포인터를 전달해야한다. 위 그림은 형식인수가 동적할당은 받은 후 TEST를 입력받았다. 이 경우도 함수가 리턴된후 a의 값은소멸된다. 그리고 Name은 아무런 변화가 없다. 즉 값에의한 호출이다.

 단순히 Name의 값만 바꾸려 했다면 제대로 동작 했을 것이다. 이런일이 일어나는 이유는 함수내부에 있는 malloc이 포인터의 값을 변화시키는 함수기 때문이다. 때문에 포인터를 기준으로 생각해야한다.

실제로 원하는 동작을 만들기 위해선 형식인수로 &Name을 전달해야하며, 형식인수의 타입은 char **a 가되어야 한다. 그리고 함수 내부에서 형식인수는 '*'연산으로 실인수의 실제 값을 참조해야한다. 다음 그림을 참조하길바란다.


제대로 참조에 의한 호출이 된경우



2014/02/11 - [프로그래밍/C언어] - 이중포인터에 대한 이해 C언어


이경우 C언어의 문자열을 다루는 형식의 특성상 2중포인터를 사용하긴 했으나, 실제로, 다른 참조호출의 동작과 같은 것이다. 원리 또한 동일하다. 단지 위 경우는 기준점이 포인터 Name 이었기 때문이다.


C언어의 메모리 구조


프로그램을 실행시키면 운영체제는 우리가 실행시킨 프로그램을 위해 메모리 공간을 할당해준다. 

할당되는 메모리 공간은 크게 스택(Stack), 힙(Heap), 데이터(Data)영역으로 나뉘어진다. 

이러한 메모리 공간이 어떠한 용도로 언제, 어디서 할당되는지 알아보도록 하자.


할당 시기 : 프로그램이 실행될 때마다

할당 장소 : 메인 메모리(RAM)

할당 용도 : 프로그램 실행 시 필요한 메모리 공간(지역변수, 전역변수 선언을 위해) 할당




데이터(Data) 영역


 - 전역 변수와 static 변수가 할당되는 영역

 - 프로그램의 시작과 동시에 할당되고, 프로그램이 종료되어야 메모리에서 소멸됨

 

#include <stdio.h>

int a = 10;	// 데이터 영역에 할당
int b = 20;	// 데이터 영역에 할당

int main() {

	...

	return 0;
}

위와 같은 코드에서 int형 변수 ab는 프로그램 실행시, main 함수가 호출되기 전에 데이터 영역에 할당된다.

그렇기 때문에 프로그램이 종료될 때까지 메모리상에 존재한다.

(전역변수가 프로그램이 종료될 때 까지 존재하는 이유)



스택(Stack) 영역


 - 함수 호출 시 생성되는 지역 변수와 매개 변수가 저장되는 영역

 - 함수 호출이 완료되면 사라짐

 

#include <stdio.h>

void fct1(int);
void fct2(int);

int a = 10;	// 데이터 영역에 할당
int b = 20;	// 데이터 영역에 할당

int main() {

	int i = 100;	// 지역변수 i가 스택 영역에 할당

	fct1(i);
	fct2(i);

	return 0;
}

void fct1(int c) {
	int d = 30;	// 매개변수 c와 지역변수 d가 스택영역에 할당
}

void fct2(int e) {
	int f = 40;	// 매개변수 e와 지역변수 f가 스택영역에 할당
}

main함수와 fct1fct2라는 함수를 추가하였다. 

ab를 데이터 영역에 할당한 뒤에 main함수를 호출하면서 int형 변수 i는 지역변수로서 스택영역에 할당된다.

그 뒤에 fct1()이라는 함수를 호출하면서 fct1함수의 매개변수인 c와 d가 스택영역에 할당된다.

fct1()이라는 함수호출이 끝나면 c와 d는 스택영역에서 삭제되며, 

그 뒤 fct2()라는 함수를 호출하면서 매개변수 e와 지역변수 f가 스택영역에 할당된다.

스택영역은 그 이름그대로 스택의 성질을 띄고있다.


 

힙(Heap) 영역


 - 필요에 의해 동적으로 메모리를 할당 할 때 사용


지금까지 데이터영역과 스택영역을 알아보았는데, 저 두가지 영역만 있으면 코드를 문제없이 짤 수 있을것 처럼 보인다.

그럼 힙영역은 왜 필요한 것일까?


힙 영역은 왜 필요할까?

제일 첫번째 그림을 보면 힙 영역은 프로그래머가 할당한다고 되어있다. 

그럼 언제 할당을 할까? 

배열을 예를들어서 설명을 하겠다.


우리는 배열을 선언할때 상수로 선언을 한다.

int main() {

	// 정상적인 배열선언
	int arr[10];

	// 비 정상적인 배열선언
	int i = 0;
	scanf("%d", &i);
	int arr[i];

	return 0;
}

배열의 길이를 사용자가 입력한 숫자로 잡아주는 것은 비 정상적인 배열선언이다. 왜 비 정상적일까?

메모리 구조에 대해서 잘 파악하고 있다면 당연한 이야기다.


제일 첫번째 그림을 다시보자, 스택 영역에 할당될 메모리의 크기는 컴파일 타임(컴파일 하는 동안)에 결정된다고 되어있다.

정상적인 배열 선언의 경우 arr이라는 배열의 크기가 40바이트 라는것을 알 수 있다.

하지만 비 정상적인 배열선언의 경우 i의 크기가 4바이트 라는 것을 알 수 는 있으나, arr이라는 배열의 크기는 알 수 없다.


그렇다면 다음과 같이 배열을 선언할 때는 문제가 없을까?

int main() {
	
	int i = 10;
	int arr[i];

	return 0;
}

i 라는 변수가 10이기 때문에 arr이라는 배열의 크기가 10이라는 것을 알 수 있지 않을까?

결과는 아니다.


컴파일을 하는 동안 i가 4바이트의 크기라는 것을 알 수는 있으나, 그 값이 10으로 초기화 되었다는 사실은 무시하고 넘어간다. 값이 10으로 초기화 되었다는 사실은 실행되는 동안, 즉 런타임에 결정된다.

그렇기 때문에 컴파일러는 arr의 크기가 40바이트가 된다는 사실을 알 수 없다. 


사용자의 요구에 맞게 메모리를 할당해 주기 위해서는(런타임에 메모리 크기를 결정하고 싶을 때) 메모리 동적 할당을 통해 힙 영역에 메모리를 할당해야 한다.


힙 영역 : 할당해야 할 메모리의 크기를 프로그램이 실행되는 동안 결정해야 하는 경우(런 타임때) 유용하게 사용되는 공간


힙 영역을 사용하기 위해서는 동적할당에 대해서 공부하여야 한다.

switch문이란, 조건문의 일종인데, 여러 개의 if~else 문을 대신하여 간결하게 작성할 때 사용하는 것입니다. if~else 문이 중첩되어 있으면 가독성이 떨어지기 때문에 스위치문이 필요합니다.

그러나 switch문 다음의 괄호()에는 "i <= 0" 이런 식의 판단문이 들어갈 수는 없고, 정수형이나 문자형(char)의 숫자만 들어갈 수 있는 제약이 있습니다. double 등의 실수는 안되고 error C2450: switch expression of type 'double' is illegal 이런 에러가 납니다.

switch는 "함수"가 아니고 "키워드"입니다.

  switch (정수) {
    case 상수 : 실행문; break;
    case 상수 : 실행문; break;
    case 상수 : 실행문; break;
    case 상수 : 실행문; break;

    default : 실행문; break;
  }



스위치문에서 주의해야 할 점은 각 case문 끝에 break; 를 꼭 붙여야 한다는 것입니다. break; 가 없으면, 그 아래쪽의 case문들까지 모두 실행되어 버립니다. break;를 만날 때까지 멈추지 않고 계속 실행됩니다.

의도적으로 break;를 생략한 경우가 아니라, 실수로 누락했을 때는 소스가 폭주하여 위험한 에러가 발생할 수 있습니다. 따라서 우선 무조건 break;를 붙여 놓고 소스의 흐름을 검토하는 것이 안전합니다.

default 라는 것은, 위의 case문들 중에서 어느 것도 해당되지 않을 때 실행할 코드입니다. 필요하지 않다면 default문이 없어도 됩니다.


switch문 사용 방법 예제 소스


소스 파일명: example.cpp
(※ 스크롤 박스 사용법: 박스 안을 마우스로 클릭한 후, 키보드의 좌우 화살표키를 누르면 양옆으로 움직일 수 있습니다. 박스에서 다시 나오려면, 박스 바깥의 아무곳이나 클릭하면 됩니다.)

#include <stdio.h>
#include <conio.h> // getch()
#include <ctype.h> // tolower()


int main(void) {

  int i = 1;

/*
i 의 값이 1일 경우에는 "자장면"이 출력
i 의 값이 2일 경우에는 break가 없기에
군만두"와 "탕수육"이 한꺼번에 같이 출력

i 의 값이 3일 경우에는 "탕수육"이 출력
i 의 값이 4일 경우에는 "짬뽕"이 출력
만약 i 의 값이 그밖의 숫자일 경우에는 "그런 음식은 없습니다."가 출력
*/

  switch (i) {
    case 1 : printf("자장면\n"); break;
    case 2 : printf("군만두\n"); // 아래의 탕수육도 실행됨
    case 3 : printf("탕수육\n"); break;
    case 4 : printf("짬뽕\n"); break;

    default : printf("그런 음식은 없습니다."); break;
  }



  // 또한 아래와 같이, 문자(char)형으로도 판단할 수 있음
  // 다만 double, float 같은 실수형은 불가능

  char ch = (char) getch(); // 키보드에서 문자 1개 입력 받기
  // 글자를 소문자로 변환
  // (대소문자 구분 없이 입력받기 위해)
  ch = (char) tolower(ch);


  switch (ch) {
    case 'a'  : printf("A를 누르셨군요\n");
                break;
    case 'b'  : printf("B를 누르셨군요\n");
                break;
    case 'c'  : printf("C를 누르셨군요\n");
                break;
    case '9'  : printf("9를 누르셨군요\n");
                break;
    case 0x0D : printf("Enter키를 누르셨군요\n");
                break;
    case 0x1B : printf("Esc키를 누르셨군요\n");
                break;

    default   : printf("그밖의 문자...\n");
                break;
  }



  return 0;
}


C언어 포인터 기본에 대해서 배워보자

 

C언어를 처음 접하신 분들은 포인터에 대해서 어려워 하시는 분들이 있는데 어려워 할 필요 없습니다.

 

C언어 포인터 에 대해서 조금만 이해하신다면 예제보고 테스트 소스를 코딩해보고 그러다 보면 언젠가 이게 뭐가 어렵지? 라는 생각을 하게 될 것입니다.

 

포인터에 대해서 조금 더 깊게 파고 들어가면 포인터연산, 함수포인터, 이중포인터 등등 여러가지가 나오는데 이 부분은 일단 포인터에 대해서 공부를 한다음 진도를 나가 봅시다.

 

C언어 를 테스트 할려면 컴파일러가 필요한데 아래 Dev-C++ 무료 컴파일러 입니다. 사용하시는 컴파일러가 있다면 굳이 다운로드 안하셔도 됩니다.

 

C언어 무료 컴파일러인 Dev-c++ 다운로드 방법 입니다.

http://seonissi.tistory.com/admin/entry/post/?id=25

 

 

Dev-c++ 간략한 사용법

http://seonissi.tistory.com/admin/entry/post/?id=26

 

 

C언어 포인터는 단순히 주소라고 생각하면 됩니다.

 

서울시 구로구 구로동 XXXX아파트 101동 1101호

우리가 사는 집엔 모두 주소가 붙듯이 변수를 선언하면 주소가 붙게 됩니다.


 

 

 

 

char 형태의 p라는 변수를 선언 했습니다.

 

 


선언된 변수를 printf문으로 출력해볼 경우 주소가 나타납니다..

 

 

 

 

 

 

 

이렇게 사용하면 변수의 주소를 공유하게 되 포인터로도 변수의 값을 사용 및 수정이 가능합니다. 예를 들어서 우리가 사는 주소에 A라는 사람이 들어가면 그 주소안에는 A라는 사람이 있는거고 B라는 사람이 들어가게 되면 그 주소안에는 B라는 사람이 있는것과 동일한 것입니다..

 

 

변수와 포인터의 주소값을 출력해볼 경우 동일하다라는걸 확인이 가능합니다.

 

 

  

 

포인터 p에 변수를 할당 하지 않을 경우 경우 각각 주소가 다릅니다.

 

위의 결과처럼 주소값이 1만 증가할수 있지만 경우에 따라서 주소값이 다른곳을 가르킬수 있습니다. 포인터가 가르키는 주소값은 메모리 어느 부분을 가르키고 있는데 주소를 할당하지 않은채 사용하다가는 언젠가는 프로

그램이 죽는 현상이 발생하니 주의해야 합니다.

 

 

C언어 포인터 예제 1

 

int main(int argc, char *argv[])

{
    char ch;
    char *p;

 

ch = 'a';
 
printf("ch = %c\n", ch);


    p = &ch;
 
    printf("*p = %c\n", *p);
    printf("ch = %c\n", ch);

}

 

위 예제를 보게 되면 char 형태의 ch를 선언하고 포인터 p를 선언했습니다.

char 형태의 변수값에 'a'라는 값을 넣고 포인터 p에 변수 ch 주소를 할당 했습니다.

그리고 포인터p 가 어떻게 변했느가를 살펴보는 예제 입니다.

 

 

 

 

 결과값을 보면 ch와 포인터p와 값이 동일하다는게 확인이 됩니다.

 

 

 


C언어 포인터 예제 2

int main(int argc, char *argv[])
{
 char ch1;
 char ch2;
 char *p;

 

 ch1 = 'a';
 ch2 = 'b';
 
 printf("ch1 = %c\n", ch1);
 printf("ch2 = %c\n", ch2);
 
 p = &ch1;
 printf("*p1 = %c\n", *p);
 
 p = &ch2;
 printf("*p2 = %c\n", *p);
}

 

 

 

결과값을 보면 할당된 포인터 값에 따라 포인터p의 값이 변환되는 것을 알수 있습니다.

이처럼 포인터는 주소값을 가지고 움직인다는것을 확인했습니다.

 

 

 

C언어 포인터 예제 3

void test1 (char *p)
{
 printf("test1 *p = %c\n", *p);
 *p = '1';
}

 

void test2 (char *p)

{

 printf("test2 *p = %c\n", *p);
 *p = 'a';
}

 

int main(int argc, char *argv[])
{
 char ch1 = 'z';

char *p;

 

 p = &ch1;

 

printf("*p = %c\n", *p);

 test1(p);
 printf("ch1 = %c\n", ch1);

 test2(p);
 printf("ch1 = %c\n", ch1);
}

 

위 예제를 보면 함수에 포인터를 대입해서 함수를 호출 했을때 인자값으로 전송 받았던 포인터 값에 대해 값이 변함이 없는지를 확인할 수 있는 예제 입니다.


거기에 인자값으로 받은 포인터에 다른 값을 대입했을 시 변수로 선언한 ch값이 변화가 있는지 없는지도 확인이 가능합니다.


함수안에서 값을 변경해도 main에서 선언된 ch1이라는 변수에도 영향을 미칩니다.


위의 예제와 같이 함수를 호출할때 함수에 포인터 변수나 주소를 전달하는 것을 call by reference라고 합니다..


ch값을 받고 할당 받은 포인터를 함수로 전달받고 그 값을 printf값으로 출력 하는 예제입니다.

 

 

 


결과값에서 봤듯이 인자값으로 받은 포인터가 ch값과 동일하다는것을 확인할 수 있습니다.


함수안에서 포인터 값을 변경해도 변수로 선언된 ch값 까지 변환되는 부분까지 확인이 가능합니다.


함수의 인자값으로 포인터를 사용해서 그 값을 공유할수 있다는 것입니다.
위와 같이 많이 사용하게 될텐데 포인터를 잘 사용해야 프로그램을 개발할때 모듈화를 하기가 쉽습니다.


이 부분은 더 공부를 해야 될 것입니다.

 


C언어 포인터 예제 4

int main(int argc, char *argv[])

{
 char ch1[10];
 char ch2[20];
 char *p;

 

 ch1[0] = '1';
 ch1[1] = '2';

 ch2[0] = 'a';
 ch2[1] = 'b';
 
 p = ch1;
 printf("ch1 p[0] = %c\n", p[0]);
 printf("ch1 p[1] = %c\n", p[1]);

 

 p = ch2;
 printf("ch2 p[0] = %c"\n, p[0]);
 printf("ch2 p[1] = %c\n", p[1]);
}

 

위 예제는 포인터와 배열에 대한 부분인데 ch1은 10개의 박스를 다 소유하고 있다고 생각하면 됩니다.

 

 

10개의 박스의 안에는 박스 사이즈에 맞게 물건을 넣을수 있는데 사이즈는 "char"형 이라고 보면 되고 물건은 "char"로 선언된 변수라고 보시면 됩니다.

 

포인터로 선언된 p로 ch1을 가르키게 되면 ch1의 데이타를 쓰기 및 읽어올수가 있습니다.


배열을 선언하고 그 배열을 포인터로 할당한 다음 간단하게 포인터로 제어를 하는 식으로 많이 사용합니다.


위의 예제를 보면 포인터 p에 ch1을 할당해서 ch1에 설정된 a, b값을 포인터 p로도 확인을 할수가 있습니다.


다시 포인터 p에 ch2를 할당하면 ch2와 동일하게 사용이 가능합니다.

 

 



결과값을 보시면 ch1과 ch2의 값을 포인터 p로 할당한 다음 printf문으로 출력했을 경우 동일하게 출력 된다는 것을 확인할 수 있습니다.

 

 

C언어 포인터 예제5

 

int main(int argc, char *argv[])

{
 char ch1 = '1';
 char *p1;
 char *p2;

 p1 = &ch1;
 p2 = &ch1;
 
 printf("*p1 = %c\n", *p1);
 printf("*p2 = %c\n", *p2);
 
*p1 = 'a';
 
 printf("*p2 = %c\n", *p2);
}

 

위의 예제를 보면 ch1의 값을 포인터p1과 포인터p2를 동시에 할당을 했습니다. 이럴경우 p1과 p2가 동일하다고 생각하면 되는데 같은 ch1의 변수를 할당받았고 p1을 수정했을 경우 p2의 값도 변한다는 것을 확인할 수 있습니다.


이렇게 포인터를 여러개 선언해서 같이 수정도 가능하다. ch1이 아니라 "p1 = p2"이렇게 선언해도 상관없습니다.
p1도 포인터고 p2도 포인터기 때문에 언제든 주소를 다시 할당할수 있습니다.

 

 

 

 

 

결과값을 보면 p1과 p2는 동일하게 변수 ch1의 값을 동일하게 printf문으로 출력을 하고 있음이 확인 가능합니다.

 

위의 예제처럼 포인터의 경우 다양한 사용이 가능합니다. 구조체를 선언해서 포인터로 할당해서 사용이 가능하고 여러가지 사용이 가능합니다. 위에는 간단하게 C언어 포인터 기본에 대해서만 예제를 설명했습니다.

 

C언어에서 포인터를 사용하지 않아도 프로그램 코딩은 가능하나 포인터를 사용하지 않고 프로그램 코딩을 하는 개발자는 없을 것입니다.

 

실제로 프로그램을 개발하게 되면 이렇게 간단하게 포인터를 사용하지 않고 포인터 연산도 들어가고 포인터 구조체도 쓰고 배열도 들어가고 복잡하게 진행이 됩니다.

 

포인터를 잘못사용할 시 메모리가 뒤죽박죽이 되버릴 수 있습니다. 그리고 포인터를 잘못 정의해도 컴파일러가 체크를 못하는 경우가 종종 발생하기 때문에 개발 완료 후 문제가 발생할 가능성이 있습니다. 그래서 좀더 세세한 코딩이 필요합니다.

도요타 급발진 문제처럼 버그가 발생할 수 있기 때문에 주의해서 개발을 해야 합니다.

C/C++ 언어는 기본 데이터형을 최소화하기 위해서 많이 사용하는 형식의 데이터형만을 기본 데이터형
으로 정의하고 나머지는 프로그래머가 데이터형을 정의해서 사용할수 있도록 만들어졌습니다. 그래서
기본데이터형으로 char, int, float, double 을 제공하고 나머지 형식은 프로그래머가 배열, 포인터,
typedef, 구조체, 공용체 같은 문법을 가지고 직접 정의해서 사용하면 됩니다.
 
배열과 같은 문법으로 사용자 정의 데이터형을 만들면 동일한 데이터형만을 그룹지을수 있기 때문에
단순한 형태만 정의가능하다는 단점이 있는데 이것을 보완하기 위해 만들어진게 구조체라는 문법입니다.
 
구조체는 서로 다른 데이터형을 하나의 데이터로 군집화하여 사용할 수 있도록하는 사용자 정의
데이터형입니다. 따라서 구조체는 동일 데이터형을 군집화하여 사용할 수 있는 배열보다 자유로운
구성이 가능합니다. 그래서 사용자 정의 데이터형을 가장 잘 표현할 수 있는 문법입니다.
 
이 강좌에서는 구조체를 사용하여 사용자 정의 데이터형을 만드는 방법과 만들어진 데이터형을 사용하는
방법에 대해서 소개하도록 하겠습니다.
 
 
1. 구조체 정의하기
 
    구조체를 정의할 때에는 다음과 같은 형식을 사용합니다. 이렇게 선언된 구조체명이 새로 만들어진
    데이터형의 이름이됩니다. 즉, int char 와 같은 역할을 하게됩니다.
 
    struct 구조체명 {
        데이터형 변수명;
        ...
    };
 
    구조체는 struct 키워드를 선두에 명시한 후 사용할 구조체명을 적어서 정의합니다. 이 때 구조체
    내부를 구성하는 데이터는 미리 정의된 데이터형으로 선언해야하고, 각 데이터는 " ; " 키워드로
    분리해야합니다. 예를 들어 이름, 나이, 키, 몸무게와 같은 인적사항을 저장하는 People 이라는
    구조체를 정의하면 다음과 같습니다.
 
    struct People {
        char name[20];
        int age;
        double height;
        double weight;
    };
 
    위에서도 말했듯이 구조체 내부의 데이터는 미리 정의된 데이터형이여야하기 때문에 char, int, double
    등과 같은 기본 데이터형 외에 다른 구조체나 사용자가 따로 정의한 데이터형을 사용하고 싶으면
    이 구조체보다 먼저 정의되어 있어야합니다.
 
    일반적이지는 않지만 한번만 선언해서 사용하는 경우 아래와 같이 구조체명을 생략해서 사용하는
    경우도 있습니다. 즉, 구조체를 정의함과 동시에 data 라는 변수를 선언하기 때문에 이 데이터형을
    다시 사용할 필요가 없다면 구조체명도 필요없기 때문에 생략 가능합니다.
 
    struct {
        char name[20];
        int age;
        double height;
        double weight;
    } data;
 
 
2. 구조체 사용하기
 
    정의된 구조체는 구조체명을 이용하여 변수를 선언하듯이 선언해주면 되는데 C 언어에서 구조체를
    이용하여 변수를 선언할 때에는 반드시 구조체명 앞에 "struct" 키워드를 붙여주어야합니다.
 
    // People 구조체를 사용하여 data 변수를 선언한다.
    struct People data;
 
    C++ 언어로 구조체를 선언할 때에는 struct 키워드를 생략하여 선언할 수 있지만 C 언어의 경우에는
    구조체를 선언할 때마다 struct 키워드를 명시해야 하기때문에 아래와 같이 typedef 명령어를 이용
    하여 구조체를 또다른 데이터 타입으로 정의하여 사용하기도 합니다.
 
    typedef struct People PEOPLE;
 
    이렇게 선언하면 C 언어에서도  People 구조체를 사용하여 변수를 선언할때 struct 키워드를
    적지 않게되어 좀더 편리하게 사용할수 있습니다.
 
    PEOPLE data;  // struct People data;
 
    하지만, 구조체 정의 따로 typedef 정의 따로 사용하는 방식이 프로그램을 이해하기에 불편할수도
    있기 때문에 이것을 아래와 같이 한번에 사용하여 구조체를 정의하기도 합니다.
 
    // struct people { ... } 구조체를 PEOPLE 이라는 데이터형으로 정의한다.
    typedef struct People{
        char name[20];
        int age;
        double height;
        double weight;
    } PEOPLE;
  
    // data 라는 변수명으로 people 구조체를 선언한다.
    PEOPLE data;
 
    최근에는 C++ 를 사용하는 사람들이 많아짐에따라 struct 가 생략가능해지면서 typedef 을 사용하는
    형식도 점차 줄어들고 있습니다.
   
    구조체로 선언한 데이터형도 기본데이터형(char, int, ...)과 동일한 데이터 형이기 때문에 변수 선언시
    배열, 포인터 문법을 아래와 같이 그대로 사용할수 있습니다.
 
    PEOPLE data;      // 일반 변수
    PEOPLE list[20];  // 배열
    PEOPLE *p;         // 포인터
 
    이렇게 선언된 구조체 변수는 아래의 코드처럼 구조체의 요소에 접근할 때에는 " . " 를 이용하여
    구조체의 "변수명.요소명" 과 같은 형식을 사용합니다.
     PEOPLE one;
 
    strcpy(one.name, "운영진");
    one.age = 23;
    one.height = 179.9;
    one.weight = 70;
 
     
 
 
    그리고 메모리를 동적으로 할당하여 선언하고, 사용하는 방법은 다음과 같습니다.
 
    PEOPLE *two = (PEOPLE *)malloc(sizeof(PEOPLE));
    strcpy((*two).name, "사용자");
    (*two).age = 30;
    (*two).height = 185;
    (*two).weight = 75;
 
    위 코드에서 *two.age 라고 하지 않고 (*two).age 라고 사용한 이유는 " * "  연산자보다 " . " 연산자의
    연산자 우선순위가 높기때문에 정확한 표현을 하기 위해서입니다. 따라서 ( ) 를 생략하면 전혀
    다른 의미로 해석되어 컴파일시에 오류가 발생합니다.
 
    이렇게 연산자 우선순위 때문에 구조체를 포인터 형식으로 사용하면 ( ) 을 적어야 하는 불편함이
    생기는데 이런 불편함을 줄여주기 위해서 C/C++ 에서는 " -> "  라는 약식 표현법이 제공됩니다.
    이 약식 표현법으로 위 코드를 다시 적으면 아래와 같습니다.
 
     PEOPLE *two = (PEOPLE *)malloc(sizeof(PEOPLE));

    strcpy(two->name, "사용자");
    two->age = 30;
    two->height = 185;
    two->weight = 75;
 
     
 
 
3. 구조체의 크기와 메모리 배열 ( struct member alignment )
 
    구조체의 크기는 일반적으로 구조체를 구성하는 데이터형의 크기를 합산한 값입니다. 따라서
    위에서 정의한 People 데이터형은 40 바이트 입니다. 하지만, VC++ 에서는 데이터 처리의
    수행능력을 높이기 위해서 "struct member alignment" 을 사용하기 때문에 위 구조체의 크기가
    40 바이트 일수도 아닐수도 있습니다.
 
    Windows 32비트 운영체제는 레지스터의 크기, 데이터 버스의 크기, 데이터 처리 단위, 포인터의 크기
    등이 모두 4 바이트로 되어 있어서 해당 크기로 연산하고, 처리하는 것이 속도가 더 빠릅니다. 그래서 
    컴파일 옵션에 따라서 구조체를 해석할때 그 배열을 짝수 또는 4의 배수로 재구성하는 작업을
    수행하기 때문에 실제로 구조체가 구성되는 것이 조금 다를 수 있습니다.
    ( People 구조체는 다행이도 짝수 배열이 유지되어 struct member alignment 사용해도 크기가
      동일하기 때문에 다른 구조체를 정의하여 설명하도록 하겠습니다. )
 
    typedef struct Test {
        char a;       // 1 바이트의 데이터 크기를 가짐
        int b;          // 4 바이트의 데이터 크기를 가짐
        short int c;  // 2 바이트의 데이터 크기를 가짐
        int d;          // 4 바이트의 데이터 크기를 가짐
        char e;       // 1 바이트의 데이터 크기를 가짐
    } TEST;
 
    위와 같이 정의된 TEST 구조체를 선언하면 12 바이트의 메모리가 할당된다고 생각할 수도 있지만
    구조체를 구성하고 저장하는 단위가 4 바이트이기 때문에 b 나 d 같은 4 바이트 이상의 크기를 가지는
    변수가 4의 배수 주소 값에 위치하도록 아래와 같은 형태로 메모리가 사용되어 20 바이트의 메모리가
    할당됩니다.
 
    
 
    그렇기때문에 쓸데없는 메모리의 낭비를 막기 위해서는 아래와 같이 데이터를 구성하는 순서를
    변경하여 구조체를 정의하는 것이 좋습니다.
 
    typedef struct Test {
        int b;          // 4 바이트의 데이터 크기를 가짐
        int d;          // 4 바이트의 데이터 크기를 가짐
        short int c;  // 2 바이트의 데이터 크기를 가짐
        char a;       // 1 바이트의 데이터 크기를 가짐
        char e;       // 1 바이트의 데이터 크기를 가짐
    } TEST;
 
    위와 같이 구조체를 정의하면 4의 배수 주소에 위치해야하는 b, d 가 선두에 먼저 배치되고 c, a, e 는
    4바이트 내에 모두 저장되어 아래의 그림처럼 메모리 낭비가 생기지는 것을 볼 수 있습니다. 최악의
    경우 20 바이트의 크기를 가졌던 구조체가 데이터의 배치만 변경하여 12 바이트의 크기로 줄어들게
    되었습니다.
 
    
 
    b, d 와 같은 4 의 배수 크기를 가진 데이터뿐만 아니라 c 변수같은 2 바이트의 크기를 가진 데이터는
    짝수 주소에 저장되려는 특징이 있기때문에 c, a, e 순서로 정의하지 않고, 아래와 같이 a, c, e 순서로
    정의하면 메모리가 아래의 그림처럼 구성되어서 16 바이트의 크기를 가지게 됩니다.
 
    typedef struct Test {
        int b;          // 4 바이트의 데이터 크기를 가짐
        int d;          // 4 바이트의 데이터 크기를 가짐
        char a;       // 1 바이트의 데이터 크기를 가짐
        short int c;  // 2 바이트의 데이터 크기를 가짐
        char e;       // 1 바이트의 데이터 크기를 가짐
    } TEST;
 
    
 
    이처럼 구조체를 정의할 때 어떻게 배치하는가에 따라 구조체의 크기가 변경될 수 있기때문에
    메모리를 낭비하지 않도록 주의해야하며, 프로그래밍을 하다가 구조체의 크기를 명시해야하는
    경우에는 수치 값이 아닌 sizeof 매크로를 사용하여 구조체의 크기를 반환받아 사용해야합니다.
 
    TEST *data = (TEST *)malloc(12);                    // 위험한 표현
    TEST *data2 = (TEST *)malloc(sizeof(TEST)); // 안전한 표현


"Editplus" 와 "Visual Studio 2010" 컴파일러 연동하기


C/C++ 코딩을 할 때 알고리즘을 풀이 하거나 기본적인 테스트를 위해서

Visual Studio를 실행하면 너무 무겁기에 Editplus 를 사용하는 편이다.


하지만 Editplus는 컴파일러가 내장되어 있지 않기 때문에 Visual Studio에 있는 컴파일러를 사용하기로 한다.

먼저 환경변수 등록을 해야 한다.


[컴퓨터] -> [속성] -> [고급 시스템 설정] -> [환경변수]




[시스템 변수]

1. Path 항목에 2가지를 추가한다.

C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin

C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE


2. 새로 만들기를 눌러 "LIB" 항목을 새로 만들고 다음 경로를 추가한다.

C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\lib


3. 새로 만들기를 눌러 "INCLUDE" 항목을 새로 만들고 다음 경로를 추가한다.

C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include


★ 경로를 추가할 때 다른 값들과 구별되기 위해서 세미콜론 ";"을 반드시 넣도록 한다. 


------------------------------------------------------------------------------------------


그 다음은 EditPlus 에서의 설정 내용


[도구] -> [사용자 도구 구성] -> [Group1] 에서 -> [추가>>]



메뉴제목 : C/C++ Compile (적당히 적는다.)

명령 : C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\cl.exe

인수 : $(FilePath)

디렉토리 : $(FileDir)

(출력 내용 캡처 항목을 체크한다.)



메뉴제목 : Run (적당히 적는다.)

명령 : $(FileNameNoExt)

인수 :

디렉토리 : $(FileDir)


★ EditPlus 설정을 마치면 적용을 누르고 프로그램을 종료하고 다시 켜야한다. 


컨트롤 + 1 => 컴파일

컨트롤 + 2 => 실행



다음과 같이 내용이 출력되면 제대로 설정되었다.

★ 하지만 kernel32.lib 파일이 없다고 나올 경우 


C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Lib 여기서 Kernel32.lib 파일을 찾아서

C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\lib 이쪽으로 복사 붙여넣기 해준다.


출처: http://osebin.tistory.com/48

+ Recent posts