구조체의 포인터와 배열에 대해 알기 위해 문자열에서 C의 키워드 중 어떤것이 몇번 나왔는지 세는 프로그램을 다시 작성해 보자. 여기서는 배열의 첨자 대신 포인터를 쓰기로 한ㄷ. keytab의 외부 선언은 바꿀 필요가 없고 main과 binsearch는 조금 수정해야한다.


#include <stdio.h> 

#include <ctype.h> 

#include <string.h> 


#define MAXWORD 100

int getword(char *, int);

struct key *binsearch(char *, struct key *, int);


/* count C keywords; pointer version */

int main()

{

    char word[MAXWORD];

    struct key *p;

    while (getword(word, MAXWORD) != EOF)

        if (isalpha(word[0]))

            if ((p=binsearch(word, keytab, NKEYS)) != NULL)

                p->count++;

    for (p = keytab; p < keytab + NKEYS; p++)

        if (p->count > 0)

            printf("%4d %s\n", p->count, p->word);

        return 0;

}


/* binsearch: find word in tab[0]...tab[n-1] */

struct key *binsearch(char *word, struct key *tab, int n)

{

    int cond;

    struct key *low = &tab[0];

    struct key *high = &tab[n];

    struct key *mid;

    

    while (low < high) {

        mid = low + (high-low) / 2;

        if ((cond = strcmp(word, mid->word)) < 0)

            high = mid;

        else if (cond > 0)

            low = mid + 1;

        else

            return mid;

    }

    return NULL;

}


여기서 몇가지를 살펴보자.

첫째 binsearch 함수는 정수 대신에 struct key의 포인터를 리턴하도록 선언되어야 한다 이것은 호출하는곳(프로그램 6행)과 binsearch에서 동시에 선언되어야 한다. binsearch 가 단어를 발견하면 그것의 포인터를 리턴하며 발견하지 못하면 NULL을 리턴한다.

둘째 kyetab의 요소에 대한 모든 엑세스는 포인터에 의해 행해져야 한다. 이를 위해 binsearch를 바꾸어야 한다

low와 high의 초기화는 테이블의 시작과 끝의 포인터로 한다. 중간요소의 계산이 다음과 같이 단순하지는 않다.


mid = (low+high)/2 /* wrong */


그 이유는 두 개 포인터의 합이 문법에 맞지 않기 때문이다 하지만 뺼셈은 된다. 따라서 high-low는 원소의 수를 나타내며 다음과 같은 표현은 low와 high사이의 중간에 있는 원소의 포인터 mid를 넣는다


mid = low + (high-low) / 2;


이렇게 하면 배열밖의 원소를 찾는 엉뚱한 동작을 방지할 수있다. 문제는 &tab[-1]과 &tab[n]이 tab이라는 배열의 밖을 나타낸다는 것이다. 전자는 확실히 잘못된 것이고 후자는 엉뚱한 곳을 가리킨다는 점에서 잘못되었다. 사실은 배열의 끝 다음의 첫 번째 원소(즉,&tab[n])에 관련된 포인터 연산은 제대로 동작하도록 되어있다. (일부러 썻을 경우 원하는 대로 된다). main에서는 다음과 같이 썻다.


 for (p = keytab; p < keytab + NKEYS; p++)


만일 p가 구조체에 대한 포인터라면 p에대한 어떤 산술적인 계산도 구조체의 실제 크기를 고려하도록 되어있다 그래서 p++은 구조체의 배열에 있어서 다음 요소를 얻는데 알맞을 만큼 p를 증가시키게 되고 루프는 정상적으로 동작하게 된다.

하지만 구조체의 크기가 모든 멤버 크기의 합이라고 생각하면 안된다. 서로 다른 대상의 정렬제한 규칙으로 인해 구조체에 이름이 붙지 않는 빈칸(hole)이 생길수 있기 때문이다 예를 들어 char이 1바이트 이고 int가 4바이트라면 구조체는


struct {

        char c;

        int i;

    };


5바이트가 아닌 8바이트를 요구할지도 모른다. sizeof라는 연산자를 사용하면 실제크기를 알수 있다.

마지막으로 프로그램의 형식에 대해 생각해 보자. 만일 함수가 다음과 같이 구조체 포인터 같은 복잡한 형식을 리턴할때


struct key *binsearch(char *word,struct key *tab,int n)


어떤것이 함수의 이름인지 알아보기가 어렵다 따라서 다음과 같은 방식이 사용될 떄도 있다


struct key *

binsearch(char *word,struct key *tab,int n)


어느것을 사용할 것인가는 개인의 취향이므로 경우에 따라 좋다고 생각되는 형식을 사용하기 바란다.

 

문자열에서 C에서 사용되는 키워드(keyworkd)중에 어느것이 몇번 나왔는지 세는 프로그램을 생각해 보자 이 플그램에는 아름을 나타내는 문자열의 배열과 그 수를 세기 위한 정수의 배열이 필요하다. 그 한가지 방법은 keyworkd 와 keycount라는 두개의 배열을 사용하는 것이다


char *keyword[NKEYS];

int keycount[NKEYS];


여기서 두개의 배열 대신 구조체를 사용하는 것이 가능하다 필요한 변수는 다음과 같은 두개이다


char *word;

int count;


이 배열 쌍을 구조체로 선언하려면 다음과 같이 한다.


struct{

    char *word;

    int count;

    } keytab[NKEYS];


이 선언은 배열 keytab이 이런 형태의 구조체라는 것을 나타내고 각각에 기억장소를 할당한다 즉 keytab은 구조체의 배열이 된다. 이것은 다음과 같이 쓸 수도 있다.


struct{

    char *word;

    int count;

    };


struct key keytab[NKEYS];


keytab이라는 구조체는 멤버가 무엇인지 정해져 있으므로 이것들이 정의할때 초깃값을 주는 것은 쉬운일이다. 구조체에 초깃값을 주는 것은 앞의 것과 아주 비슷한데 정의 뒤에 초깃값을 중괄호 속에 나타내면 된다.


struct{

    char *word;

    int count;

    } keytab[] = {

        "auto", 0,

        "break", 0,

        "case", 0,

        "char", 0,

        "const", 0,

        "continue", 0,

        "default", 0,

        /* ... */

        "unsigned", 0,

        "void", 0,

        "volatile", 0,

        "while", 0

    };


초깃값들은 각 구조체의 멤버 쌍으로 되어있다. 각 열마다 초깃밧을 중괄호로 둘러싼느 것이 더 정확할것이다.


    { "auto", 0 },

    { "break", 0 },

    { "case", 0 },

    ...


그러나 초깃값이 단순 변수이거나 문자열이고 값이 모두 주어진 경우에는 중괄호가 필요없게 된다. 초깃값이 주어져 있고[]가 비어있을 때(배열의 크기가 나타나지 않을때)는 초깃값으로 준 원소의 개수가 그 배열의 크기로 지정된다

키워드 계산 프로그램은 keytab의 정의부터 시작된다. main루틴에서는 한 단어씩을 읽어들이는 getword함수를 계속해서 호출하여 입력을 읽어 들인다. 각 단어는 제 3장에서 썻던 이진탐색 함수로 keytab에 있는지를 검색한다. 키워드의 리스트는 문자 값이 증가하는 순서대로 정렬된다.


#include <stdio.h> 

#include <ctype.h> 

#include <string.h>


#define MAXWORD 100


int getword(char *, int);

int binsearch(char *, struct key *, int);

/* count C keywords */

int main()

{

    int n;

    char word[MAXWORD];

    while (getword(word, MAXWORD) != EOF)

        if (isalpha(word[0]))

            if ((n = binsearch(word, keytab, NKEYS)) >= 0)

                keytab[n].count++;

    for (n = 0; n < NKEYS; n++)

        if (keytab[n].count > 0)

            printf("%4d %s\n",

               keytab[n].count, keytab[n].word);

    return 0;

}

/* binsearch: find word in tab[0]...tab[n-1] */ int binsearch(char *word, struct key tab[], int n) {

    int cond;

    int low, high, mid;

    low = 0;

    high = n - 1;

    while (low <= high) {

        mid = (low+high) / 2;

        if ((cond = strcmp(word, tab[mid].word)) < 0)

            high = mid - 1;

        else if (cond > 0)

            low = mid + 1;

        else

            return mid;

    }

    return -1;

}


getword라는 함수는 단어를 찾아서 그 단어를 첫번쨰 매개변수인 배열에 복사한다 NKEYS는 keytab에 있는 키워드의 수를 나타낸다 키워드의 수는 처음부터 리스트로 주어지기 때문에 몇개인지 세어서 숫자로 정해도 되겠지만 리스트가 바뀔경우를 대비해서 프로그램에서 하기로 한다.

한가지 방법은 초깃밧들의 맨 마지막에 null 포인터를 두고 ,keytab을 반복하다가 null 포인터가 발견되면 끝내는 방법이다. 그러나 이것은 배열의 크기가 컴파일 시에 완전히 결정되기 때문에 불필요한 일을 하는 것이된다. 배열의 크기는 다음식에서 나온 값의 제곱이 된다.


 keytab의 크기/ struct key의 크기


C에는 컴파일 시에 어떤 대상의 크기를 계산하는 sizeof라는 연산자가 있는데 다음과 같이 사용된다.


sizeof 대상

sizeof(형의 이름)


이 수식은 지정된 대상이나 type 의 바이트 단위의 크기가 정수로 나타난다(엄밀하게 하면 sizeof는 헤더 <stddef.h> 에서 정의된 size_t형의 unsigned 정수로 나타난다 이때 대상은 변수나 배열 구조체가 될수있다 int ,double 과 같은 기본형의 이름이나 구조체가 포인터와 같은 유도된 형태의 이름이 될수있다

우리의 경우 키워드의 수는 배열의 크기를 원소의 크기로 나눈 것과 같다. 이 계산은 #define 문에서 NKEYS의 값을 정할 때 쓰인다


#define NKEYS (sizeof key tab/sizeof(struct key))


다른 방법으로 저장된 원소의 크기로 배열의 크기를 나누는 방법이 있다.


#define NKEYS (sizeof keytab/sizeof keytab[0])


이방법은 type이 변해도 그대로 사용할 수있다는 장점이 있다

프리프로세서는 type을 분석하지 않으므로  #if문에 sizeof 를 사용할수 없다 하지만 #define은 프리프로세서가 값을 미리 구하지 않고 문자열을 치환하기만 하기 때문에 sizeof의 표현이 가능하다

getword라는 함수를 보자 우리는 필요로 하는것 이상으로 더 범용적인 getword 함수를 작성하였다 하지만 복잡한 것은 아니었다. getword는 입력(파일)으로 부터 다음 단어를 가져오는데 단어는 하나 이상의 문자의 모임이다 함수의 리턴 값은 단어의 첫 문자, 파일의 끝인 EOF또는 문자 그자체(영문자가 아닌경우)가 된다


/* getword: get next word or character from input */

int getword(char *word, int lim)

{

    int c, getch(void);

    void ungetch(int);

    char *w = word;


    while (isspace(c = getch()))

        ;

    if (c != EOF)

        *w++ = c;

    if (!isalpha(c)) {

        *w = '\0';

        return c;

    }

    for ( ; --lim > 0; w++)

        if (!isalnum(*w = getch())) {

            ungetch(*w);

            break;

        }

    *w = '\0';

    return word[0];

}


getword는 제 4장에서 나온 getch와 ungetch를 사용한다 한 문장이 형성되었을때 getword는 한 문자를 더 읽게 된다 그 후 ungetch를 호출해서 더 읽은 문자를 다음의 호출을 위해 입력으로 되돌리게 된다. getword는 또한 공백을 건너뛰기 위해 isspace를 사용하며 문자를 확인하기 위해 isalpha를 사용하고 문자와 숫자를 확인하기 위해 isalnum을 사용하는데 이들 모두는 표준헤더인 <ctype.h>에 들어있다


예제 6-1 현재의 getword는 밑줄 _나 문자열 상수,코멘트 또는 프리프로세싱에 사용하는 제어행을 적당히 다루지 못한다. 프로그램을 계산해보라


구조체로 할 수있는 연산은 복사하는 것. 저장(assign) 하는것 그리고 &로 구조체의 주소를 찾아서 그 멤버중의 하나를 사용하는 것밖에 없다. 복사와 지정이라 함은 함수로 인자(argument)를 넘기거나 함수로부터 값을 받는 것을 포함해서 하는 말이다.

점과 사각향을 다루기 위한 몇개의 함수를 사용해 봄으로써 구조체를 연구해 보자 여기에는 최소한 세개의 접근방법이 있다. 독립적으로 요소를 보내거나 하나의 구조체를 보내거나 또는 포인터로 보내는 세가지 방법이 있다 각각은 장단점이 있다.

첫번쨰 함수 makepoint는 두 개의 정수를 받아 point 라는 구조체를 리턴해준다.


/* makepoint: make a point from x and y components */

struct point makepoint(int x, int y)

{

    struct point temp;

    

    temp.x = x;

    temp.y = y;

    return temp;

}


여기서 변수이름과 똑같은 이름의 멤버이름(x,x.)은 전혀 다른 변수임에 유의하라 makepoint느 ㄴ어떠한 구조체를 초기화 하는데 사용하거나 구조체의 멤버에 어떤값을 넣는 함수로 사용할수 있다


struct rect screen;

struct point middle;

struct point makepoint(int, int);

screen.pt1 = makepoint(0,0);

screen.pt2 = makepoint(XMAX, YMAX);

middle = makepoint((screen.pt1.x + screen.pt2.x)/2,

                   (screen.pt1.y + screen.pt2.y)/2);


다음으로 점에 관한 산술연산을 해 보자. 즉


/* addpoints: add two points */

struct addpoint(struct point p1, struct point p2) 

{

    p1.x += p2.x;

    p1.y += p2.y;

    return p1;

}


여기서는 구조체를 매개변수로 받고 구조체를 리턴해 준다.

다른예로 어느 점이 삭가형 속에 있는지 없는지를 검사하는 ptinrect라는 함수를 보자. 왼쪽과 및변은 사각형에 포함되고 위쪽과 오른쪽 변은 사각형에 포함되지 않는다고 가정한다.


/*ptinrect: return1ifpinr,0ifnot*/

int ptinrect(struct point p, struct rect r)

{

    return p.x >= r.pt1.x && p.x < r.pt2.x

    && p.y >= r.pt1.y && p.y < r.pt2.y;

}


이와 같이 두 번의 비교만으로 판단이 가능한 것은 사각형을 나타내는 표준적인 방법이 왼쪽 아래 점을 pt1 오른쪽 위 점을 pt2로 나타내는 것이기 때문이다. 다음하누는 표준적인(canonical)형태로 사각형을 리턴해 준다.


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

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


/* canonrect: canonicalize coordinates of rectangle */

struct rect canonrect(struct rect r)

{

    struct rect temp;

    temp.pt1.x = min(r.pt1.x, r.pt2.x);

    temp.pt1.y = min(r.pt1.y, r.pt2.y);

    temp.pt2.x = max(r.pt1.x, r.pt2.x);

    temp.pt2.y = max(r.pt1.y, r.pt2.y);

    return temp;

}


만일 큰 구조체가 함수로 전달 된다면 그 큰 구조체를 복사하기보다는 포인터를 넘겨주는 것이 일반적으로 더효율적이다 구조체의 포인터는 일반적인 변수의 포인터와 같은 방법으로 사용하면 된다. 선언은 다음과 같다


struct point *pp;


여기서 pp는 struct point 형태의 구조체 포인터이다 *pp는 구조체 이고 (*pp).x와 (*pp).y는 멤버이다. pp를 사용하기 위해서는 다음 예와 같이 해야한다.


struct point origin, *pp;


pp = &origin;

printf("origin is (%d, %d)\n",(*pp).x,*(pp).y);


(*pp).x에서 .의 연산자가*보다 우선순위가 높으므로 괄호가 필요하다 *pp.x라는 표현은 *(pp.x)를 나타내는데 여기서는 x는 포인터가아니므로 문법적으로 틀리다.

구조체의 포인터로서 단축된 표현으로 제공되는 또 다른 표현이 자주 쓰인다. 만일 p가 구조체의 포인터라면


p->멤버


는 그 구조의 멤버를 가리킨다(연산자->는 음수 부호와 >의 두 문자로 있다). 따라서 위의 표현을 다르게 표현할수 있다.


printf("origin is (%d, %d)\n",pp->x,pp->y);


.과 ->는 왼쪽에서 오른쪽으로 적용되며,다음과 같이 주어지면


struct rect r, *rp = &r;


다음의 네 가지 표현은 모두 같은 결과를 가져온다.


r.pt1.x

rp->pt1.x

(r.pt1).x

(rp->pt1).x


구조체 연산자 . 과 ->는 함수를 부르기 위한 () 및  첨자를 위한[]와 함께 우선순위가 가장 높다. 예를들어


struct {

        int len;

        char *str;

    } *p;


위와 같이 선언이 있으면


++p->len


는 ++(p->len)과 같은 의미이므로 포인터 p가 아닌 len을 증가시킨다. 괄호는 결함순서를 바꿀수 있다. 즉 (++p)->len은 len을 엑세스 하기 전에 p를 증가시키지만 (p++)->len은 p를 나중에 증가시킨다(이 경우 괄호는 없어도 된다.)

같은 방법으로 *p->str는 str가 가리키는 것을 가져오며 *p->str++는 (*s++처럼)가리키는 것을 가져온후 str을 증가시킨다(*p->str)++는 str가 가리키는 것을 증가시키지만 *p++->str가 가리키는 것을 가져온후 str을 증가시킨다

구조체는 하나의 이름으로 불릴수 있는 서로 다른 형의 변수들의 모임이다 구조체는 특히 큰 프로그램에서 여러변수들의 모임을 하나의 독립된 양으로 취급할수 있게 해 주므로 복잡한 자료를 다루는것을 편하게 해준다

구조체의 전형적인 예는 회사의 직원 인사 정보인데 직원들의 이름 주소 주민등록번호 급여등의 속성을 묶어서 하나의 구조체로 표시한 것을 말한다 구조체의 내부에 또 구조체가 있을 수도 있다. 예컨대 이름은 여러개로 구분될수 있으며 주소와 급여도 마찬가지이다. 또 다른 예로 그래픽 분야에서 좌표상의 점이나 사각형에서 양 끝 두점 등이 구조체가 될수있다


간단한 구조의 예로 정수로 이루어진 x좌표와 y좌표의 점의 위치를 나타낸다고 생각해 보자

두개의 구성원소는 다음과 같이 선언된 구조체에 들어가게 된다.


struct point {

        int x;

        int y;

    };


struct 명령어는 구조체의 시작을 나타낸다 구조체의 이름은 struct라는 명령어 뒤에 온다(여기서는 point)구조체의 이름을 struct tag 라고도 하는데 후에 복잡한 선언 대신에 간단히 쓰일수 있다. 구조체에 속하는 변수들을 멤버(member)라고 한다 구조체의 멤버나 이름이 아닌 보통의 변수들은 항상 의미상 구별이 가능하기 때문에 멤버와 보통변수가 같은 이름이 되어도 좋다 동일한 이름의 멤버를 다른 구조체에서도 사용할수 있다

struct 선언은 형을 정의하는 것이다 멤버의 끝에 나타내는 우측 중괄호 뒤에 콤마로 분리하여 변수를 써 주면 여러개의 변수를 함께 정의할 수있다


struct { ... } x, y, z;


이것은 구문적으로 x, y, z를 어떤 일정한 형의 변수로 선언하고 각각에 기억장소를 배정한다는 점에서 다음과 유사하다


int x, y, z;


변수열이 따르지 않는 구조체의 선언의 경우에는 기억장소가 배당되지 않고 단순히 구조체의 형만을 기술한다 그러나 만일 구조체에 이름(tag)이 붙어있다면 그 이름은 나중에 실제 구조체의 선언에 사용될 수있다. 예를 들면 위의 point 선언의 경우는


struct point pt;


변수 pt를 point형의 구조체로 선언하는 것을 뜻한다 구조체의 경우 다음과 같이 각 요소에 대한 초깃값을 열거해서 정의함으로써 초기화 할수 있다.


struct point maxpt = { 320,200 };


구조체형의 자동변수는 그 구조체의 선언과 같은 형의 값을 리턴하는 함수를 부르거나 지정에 의해 초기화 될수 있다.

구조체의 멤버는 다음과 같은 형태로 사용된다


구조체이름.멤버


구조체 멤버의 연산자 "."는 구조체의 이름과 멤버이름을 연결시킨다 예를 들어 점 pt의 좌표값을 인쇄하기 위해 다음과 같이 쓸 수있다.


printf("%d,%d",pt.x,pt.y);


또는 점 pt의 원점에서부터의 거리를 구하기 위해 다음과 같이 사용할 수있다.


double dist,sqrt(double);

dist = sqrt((double)pt.x*pt.y+(double)pt.y*pt.y);


구조체의 구조체도 있을 수있다. 하나의 사각형은 대각선으로 마주 본 두점을 지정해 줌으로써 나타낼수있다.


struct rect {

        struct point pt1;

        struct point pt2;

    };


rect라는 구조체는 두개의 point 구조체를 포함한다 만약 screen 이 다음과 같이 선언되어있다면 


struct rect screen;


screen의 pt1이라는 멤버의 x좌표는 다음과 같이 나타낼수 있다.


screen.pt1.x



C에서는 선언문의 구조가 아주 복잡해지는 경우가 가끔 생긴다 간단한 경우에는 별 무제가 없지만 선언이 복잡해지면 뜻하지 않는 방법으로 선언이 되는 수도 있다. 선언은 외쪽에서 오른쪽으로 차례로 되는것이 아니므로 괄호를 많이 사용하게 되는 경우도 있다 다음의 두문장


int *f(); /* 정수형에 포인터를 리턴하는 함수 */


int (*pf)(); /* 정수를 리턴하는 함수의 포인터 */


를 생각해 보자 *는 앞에 붙는 연산자인데 괄호보다 우선순위가 낮기 때문에두번쨰 예에서는 괄호가 꼭 필요하다. 실제상황에서 아주 복잡하게 얽힌 선언문이 필요한 경우는 거의 없지만 이해할 필요가 있다 선언문을 작성하는 좋은 방법중 하나는 6.7에서 설명할 typedef를 사용하는 것이다 또 한가지 방법으로 여기서는 선언문을 말로 표현해주는 함수와 그 역동작을 하는 함수를 소개한다

우선 선언문을 설명으로 바꾸어 주는 함수 dcl에 대해 설명한다 실행 예를 몇가지 보이면 다음과 같다.


char **argv

argv:  pointer to char

int (*daytab)[13]

daytab: pointer to array[13] of int

int *daytab[13]

daytab: array[13] of pointer to int

void *comp()

comp: function returning pointer to void

void (*comp)()

comp: pointer to function returning void

char (*(*x())[])()

x: function returning pointer to array[] of 

pointer to function returning char

char (*(*x[3])())[5]

x: array[3] of pointer to function returning 

pointer to array[5] of char


dcl은 C의 선언에 관한 문법을 그대로 적용한 프로그램이다 구조는 다음과 같다


dcl: optional *'s direct-dcl


direct-dcl name

(dcl)

direct-dcl()

direct-dcl[optional size]


위에서 보듯이 direct-dcl을 선언 할떄 사용되는 이름등 직접적으로 선언하는데 사용되는 단어를 말하고 dcl은 그 단어앞에 *가 붙어서 포인터임을 나타내는것을 말한다 direct-dcl 뒤에()나[]가 붙은것도 direct-dcl이 된다 위의 설명을 잘 모르겠으면 다음의 예를 보자


(*pfa[])()


pfa는 이름이므로 direct-dcl이다 pfa[]도 direct-dcl이고 *pfa[]는 dcl로 인식된다. 그래서 (*pfa[])는 direct-dcl,(*pfa[])()는direct-dcl 이다


dcl 프로그램의 가장 중요한 부분은 dcl과 dirdcl이라 불리는 두 함수이다 이 두 프로그램은 되부름에 의해 반복적으로 불리면서 선언문을 분석해 낸다.


/* dcl: parse a declarator */

void dcl(void)

{

    int ns;

    for (ns = 0; gettoken() == '*'; ) /* count *'s */

        ns++;

    dirdcl();

    while (ns-- > 0)

        strcat(out, " pointer to");

}


/* dirdcl: parse a direct declarator */

void dirdcl(void)

{

    int type;

    

    if (tokentype == '(') {   /* ( dcl ) */

        dcl();

        

        if (tokentype != ')')

            printf("error: missing )\n");

    } else if (tokentype == NAME) /* variable name */

        strcpy(name, token);

    else

        printf("error: expected name or (dcl)\n");

    while ((type=gettoken()) == PARENS || type == BRACKETS)

        if (type == PARENS)

            strcat(out, " function returning");

        else {

            strcat(out, " array");

            strcat(out, token);

            strcat(out, " of");

         }

}


이 두 프로그램은 설명을 위해 만들어진 것 이기때문에 실제 사용하기에는 무리가 있다 데이터의 형도 문자형과 정수형밖에 다루지 못한다. 그러므로 함수 const 등을 처리하지 못한다 공백을 잘못쓰면 프로그램이 오작동하는 경우도 있다. 개선은 연습으로 남기고 우선 주 프로그램을 보인다


#include <stdio.h> 

#include <string.h> 

#include <ctype.h>


#define MAXTOKEN 100


enum { NAME, PARENS, BRACKETS };


void dcl(void);

void dirdcl(void);


int gettoken(void);

int tokentype;          /* type of last token */

char token[MAXTOKEN];   /* last token string */

char name[MAXTOKEN];    /* identifier name */

char datatype[MAXTOKEN];/* data type = char, int, etc. */

char out[1000];         /* output string */


int main() /* convert declaration to words */

{

    while (gettoken() != EOF) { /* 1st token on line */

        strcpy(datatype, token); /* is the datatype */

        out[0] = '\0';

        dcl(); /* parse rest of line */

        if (tokentype != '\n')

            printf("syntax error\n");

        printf("%s: %s %s\n", name, out, datatype);

    }

    return 0;

}


gettoken 함수는 빈칸이나 탭은 무시하고 입력중에서 다음번 토큰을 찾는 함수이다. 토큰이라 함은 이름,숫자나 문자 한자가 들어있는 괄호나 대괄호등을 말한다.


int gettoken(void) /* return next token */ {

    

    int c, getch(void);

    void ungetch(int);

    char *p = token;

    

    while ((c = getch()) == ' ' || c == '\t')

        ;

    if (c == '(') {

        if ((c = getch()) == ')') {

            strcpy(token, "()");

            return tokentype = PARENS;

        } else {

            ungetch(c);

            return tokentype = '(';

        }

    } else if (c == '[') {

        for (*p++ = c; (*p++ = getch()) != ']'; )

            ;

        *p = '\0';

        return tokentype = BRACKETS;

    } else if (isalpha(c)) {

        for (*p++ = c; isalnum(c = getch()); )

            *p++ = c;

        *p = '\0';

        ungetch(c);

        return tokentype = NAME;

    } else

            return tokentype = c;

}


getch와 ungetch는 제 4장에서 설명했던 것이다.

말로 설명된 것을 선언문으로 바꾸는 것은 어렵지 않다 만약 x는 함수이고 리턴값은 문자를 리턴하는 함수들의 포인터 배열의 포인터라는 말을 


x()*[]*() char


라고 표현하면


char (*(x())[])()


라고 표현해주는 함수 undcl을 생각한다. 말을 그대로 입력하지 않고 요약된 표현으로 된 입력을 사용하기 때문에 gettoken 함수를 사용할수 있다. undcl은 dcl과 같은 외부변수를 사용한다 프로그램은 다음과 같다.


예제 5-18 dcl의 입력에 에러가 있으면 고칠 수 있도록 해보라.


예제 5-19 undcl을 수정해서 결과로 나온 선언문에 필요 없는 괄호가 들어가지 않도록 하라.


예제 5-20 dcl을 보강해서 함수 매개변수나 그외 const등도 처리할 수있도록 만들어라.


C에서 함수는 변수가 아니지만 함수의 포인터는 정의할 수있다. 이 포인터는 매개변수로 사용될수도 있고 배열의 원소로 사용될 수도 있는 등 여러가지 방법으로 조작이 가능하다. 5.6에서 작성했던 정렬 프로그램을 고펴서 선택 매개변수 -n이 있으면 정렬이 사전식이 아닌 숫자 크기 순으로 하는 프로그램을 만들어 보겠다

정렬은 대개 세 부분으로 행해진다. 첫 부분은 두 대상체를 비교하는 부분 두번째 부분은 두 대상체의 순서를 바로잡는 부분이며 세번째 부분은 전체의 순서가 바로 될때 까지 비교와 교환을 계속하는 부분이다. 우리가 작성하려고 하는 프로그램은 숫자 정렬과 사전식 정렬 두가지 동작이 필요하므로 비교하는 부분은 두가지가 준비외어야 한다.

두행을 사전식으로 비교하는 것은 앞에 나왔던 strcmp 함수를 사용하면 되고 숫자 크기를 비교하는 것은 numcmp에 의해 행해진다. strcmp와 numcmp는 출력이 같은 형식이므로 함꼐 사용할 수있다.

main함수보다 먼저 numcmp함수가 선언되어야 하고 qsort도 함수의 포인터를 받는 형태로 선언되어야 한다. 프로그램은 다음과 같다.



#include <stdio.h> 

#include <string.h>

#define MAXLINES 5000 /* max #lines to be sorted */ 

char *lineptr[MAXLINES]; /* pointers to text lines */


int readlines(char *lineptr[], int nlines);

void writelines(char *lineptr[], int nlines);

void qsort(void *lineptr[], int left, int right,

           int (*comp)(void *, void *));

int numcmp(char *, char *);


/* sort input lines */

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

{

    int nlines; /* number of input lines read */

    int numeric = 0; /* 1 if numeric sort */

    if (argc > 1 && strcmp(argv[1], "-n") == 0

numeric = 1;

    if ((nlines = readlines(lineptr, MAXLINES)) >= 0){

        qsort((void**) lineptr, 0, nlines-1,

            (int (*)(void*,void*))(numeric ? numcmp : strcmp));

        writelines(lineptr, nlines);

        return 0;

    } else {

        printf("input too big to sort\n");

        return 1;

    }

}


qsort를 호출할때 strcmp와 numcmp는 함수를 주소로 가지게 된다 strcmp와 numcmp는 함수로 선언되어 있으므로 &연산자를 사용할 필요가 없다. 배열에 &연산자를 쓸 필요가 없는것과 마찬가지이다.

qsort 함수는 문자 뿐만 아니라 다른 어떤형의 데이터도 처리할수 있도록 작성되었다 qsort가 실행하기 위해 전달되어야 할 값은 포인터의 배열 정수 두개 포인터 매개변수를 가진 함수 하나이다. 포인터의 매개변수들은 void* 를 사용해서 일반성을 도모했다. 포인터를 void로 선언하더라도 어떤 정보의 손실이나 착오 없이 데이터를 잘 다룰 수 있다는 것을 보이기 위해 이번의 예에서 사용해 보았다.


/* qsort: sort v[left]...v[right] into increasing order */

void qsort(void *v[], int left, int right,

        int (*comp)(void *, void *))

{

    int i, last;

    void swap(void *v[], int, int);

    

    if (left >= right)      /* do nothing if array contains */

        return;             /* fewer than two elements */

    swap(v,left,(left+right)/2);

    last = left;

    for(i = left+1;i<= right;i++)

        if((*comp)(v[i],v[left])<0)

            swap(v,++last,i);

    swap(v,left,last);

    qsort(v, left, last-1, comp);

    qsort(v, last+1, right, comp);

}


선언문에 대해 좀더 생각해 보자 qsort함수의 네번쨰 매개변수는


 int (*comp)(void *, void *)


인데 이것은 comp가 함수의 포인터 이며 그 함수는 void* 매개변수를 가지고 리턴값은 정수라는것을 말한다 comp를 사용하는 부분은


if((*comp)(v[i],v[left])<0)


긴데 comp는 함수의 포인터이므로 *comp는 함수이며,


(*comp)(v[i],v[left])<0)


는 함수를 호출하는 것이다. 괄호는 꼭 있어야 한다 만약 괄호가 없다면 예를 들어


int *comp(void * ,void *) /* wrong */


와 같은 문장은 comp가 함수인데 리턴값이 정수의 포인터라는 뜻이 된다

함수 strcmp는 앞에서 보았으므로 두 문자열의 숫자 크기를 비교하는 함수 numcmp를 보자 이 함수는 atof 함수를 사용하고 있다.


#include <stdlib.h>


/* numcmp: compare s1 and s2 numerically */

int numcmp(char *s1, char *s2)

{

    double v1, v2;

    v1 = atof(s1);

    v2 = atof(s2);

    if (v1 < v2)

        return -1;

    else if (v1 > v2)

        return 1;

    else

        return 0;

}


다음은 두 포인터를 바꾸는 swap 함수이다 이 장의 앞에서 swap 함수와 같은것인데 선언부분이 void*로 바뀌었다.


void swap(void *v[], int i, int j) {

    void *temp;

    temp = v[i];

    v[i] = v[j];

    v[j] = temp;

}


예제 5-14 선택매개변수 -r를 다룰수 있도록 sort를 수정하라  -r를 쓰면 정렬을 역순으로 하라는 것을 뜻한다 물로 -r는 -n과 함꼐 동작해야한다


예제 5-15 정렬할 떄 대문자와 소문자를 구별하지 않도록 하는 -f를 sort 프로그램에 첨가하라


예제 5-16 문자와 숫자 공백에 대해서만 비교를 수행하는 -d(사전식 순서)를 sort에 첨가하라 -f와 함께 동작해야 한다.


예제 5-17 한 행을 몇개의 필드로 나누어 이 필드들에 대해 정렬이 수행되도록 하는 기능을 첨가해 보라

C를 지원하는 환경(예:MS-dos,unix)에서 어떤프로그램을 실행 할때 어떤 명령라인 매개변수를 프로그램에 넘겨주는 것이 가능하다 함수 main이 실행될때 두개이 매개변수가 전달된다. 첫번째 것(argc라 불림)은 그 프로그램을 실행하기 위한 매개변수의 개수이고 두번째 것(argv라 불림)은 매개변수들의 모임인 문자열을 가리키는 포인터 이다 이런 문자열을 다룰때 보통 몇중으로 되어있는 포인터를 사용하게 된다.

간단한 예로 echo라는 프로그램을 보자 이 프로그램은 매개변수를 그대로 출력하는 동작을 한다 다음과 같이 echo 프로그램을 실행 시키면


echo hello ,world


와 같이 출력된다


hello world


argv[0]은 프로그램 자신의 이름이다 그러므로 argc는 적어도 1이 된다. argc가 1이라는 말은 명령라인 매개변수가 없다는 것을 의미한다. 위의 예에서 argc는 3이며 argv[0]은 "echo",arg[1]은 "hello",argv[2]는 "world"이다.첫번째 매개변수는 argv[1]이고 마지막 매개변수는 argv[argc-1]이 된다. 그리고 argv[argc]는 NULL 포인터가 된다.


우선문자의 포인터로 이루어진 배열을 사용해서 argv 함수를 작성해 본다


#include <stdio.h>


/* echo command-line arguments; 1st version */

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

{

    int i;

    for (i = 1; i < argc; i++)

        printf("%s%s", argv[i], (i < argc-1) ? " " : "");

    printf("\n");

    return 0;

}


argv는 배열에 대한 포인터 이므로 배열첨자를 직접 사용하는 것보다 포인터를 사용하는 것이 더 좋은 방법이다. 위 프로그램을 조금 바꾸어 argc를 감소기키면서 argv를 증가시키도록 해본다 다음에 프로그램이 있다.


#include <stdio.h>


/* echo command-line arguments; 2nd version */

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

{

    while (--argc > 0)

        printf("%s%s", *++argv, (argc > 1) ? " " : "");

    printf("\n");

    return 0;

}


argv는 매개변수 문자열의 배열중 첫번째 것을 가리키는 포인터 이므로 1 증가시키는 연산(++argv)은 포인터가 argv[1]을 가리키도록 해준다 그 값을 증가시킬 때마다 그다음 매개 변수를 가리키게 되고 그와 동시에 argc는 감소 되므로 그 값이 0일 때는 출력할 매개변수가 없다는 말이다 위 프로그램에서 printf 부분을 다음처럼 해도 좋다


printf((argv>1)?"%s":"%s",*++argv);


두번쨰 예로서 4.1에 나왔던 패턴을 찾는 프로그램을 개선한 프로그램을 보자 UNIX에 있는 grep을 참고하여 패턴을 Command-line 의 첫번째 매개변수로 넣어 주도록 해서 작성한 프로그램이 다음에 있다.


#include <stdio.h> 

#include <string.h> 

#define MAXLINE 1000

int getline(char *line, int max);


/* find: print lines that match pattern from 1st arg */

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

{

    char line[MAXLINE]; int found = 0;

    if (argc != 2)

        printf("Usage: find pattern\n");

    else

        while (getline(line, MAXLINE) > 0)

            if (strstr(line, argv[1]) != NULL) { printf("%s", line);

                found++;

            }

    return found;

}


표준 라이브러리 함수 stratr(s,t)는 문자열 s내에 문자열 t와 일치하는 ㅓㅅ번째 문자의 포인터를 리턴하는 함수이다 일치하는 것이 없으면 NULL 이 리턴된다 이함수는 <string.h> 에 정의되어있다

위 프로그램을 포인터를 좀더 이용한 형태로 고쳐보자 그리고 선택 매개변수를 사용할수 있도록 해보자 선택 매개변수는 두개인데 하나는 "패턴과 일치하지 않는 라인을 출력하라" 이고 또 하는 "행 번호를 붙여라" 이다

UNIX에서 C를 사용할 경우 선택 매개변수는-를 이용하여 나타낸다 지금 정한 두 선택 매개변수를 각각 -x와 -n이라고 이름을 붙이자 그러면


find -x -n pattern


은 패턴이 맞지 않는 행을 행번호와 함께 출력할 것이다. 선택 매개변수는 순서를 바꾸어 써도 괜찮아야ㅑ 한다. 프로그램의 내용은 선택 매개변수의 수와 관계없어야 하고 또 선택 매개변수를 다음과 같이 묶어서 쓸수 있으면 좋다.


find -nx pattern


프로그램은 다음과 같다


#include <stdio.h> 

#include <string.h> 

#define MAXLINE 1000


int getline(char *line, int max);



/* find: print lines that match pattern from 1st arg */

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

{

    char line[MAXLINE];

    long lineno = 0;

    int c, except = 0, number = 0, found = 0;

    while (--argc > 0 && (*++argv)[0] == '-')

        while (c = *++argv[0])

        switch (c) {

            case 'x':

                except = 1;

                break;

            case 'n':

                number = 1;

                break;

            default:

                printf("find: illegal option %c\n", c); argc = 0;

                found = -1;

                break;

        }

    if (argc != 1)

        printf("Usage: find -x -n pattern\n");

    else

        while (getline(line, MAXLINE) > 0) {

            lineno++;

                if ((strstr(line, *argv) != NULL) != except) {

                    if (number)

                    printf("%ld:", lineno);

                    printf("%s", line);

                    found++;

                }

        }

    return found;

}


각 선택 매개변수를 가용하기 전에 argv는 증가되고 argc는 감소 되었다 while루프 끝에서 argc는 아직 처리되지 않은 선택 매개변수의 개수를 가지고 있고 argv는 그중 첫번째 것을 가리키고 있어야 한다. 그러므로 while를 벗어날때 argc는 1이되고 *argv는 패턴을 갈키고 있어야 한다. *++argv는 매개변수를 가리키는 포인터이고(*++argv[0])은 그 매개변수의 첫번째 문자이다. *나 ++보다 []이 우선순위가 더 높기 때문에 괄호가 있어야 한다 없으면 *++(argv[0])과 같은 말이 된다. 위 프로그램에서 while루프 속에 있는 *++argv[0]은 실제로 argv[0](포인터)을 1 증가시키기게 된다


지금 설명하는 프로그램 정도만 이해하면 포인터의 사용을 잘 알고 있다고 생각해도 무방하다 아주 복잡하게 얽혀있는 포인터는 몇단계로 나누어 해석하면 된다.


예제 5-10 reverse polish(4.3참조) 식을 계산하는 프로그램 expr을 작성하라. 오퍼랜드는 라인에서 받아들여지고


expr 2 3 4 + *


와 같이 하면 계산은 2*(3+4)와 같이 된다.

,

예제 5-11 제 1장에서 나왔던 entab과 detab 프로그램을 고쳐서 tab위치를 매개변수로 받아들이도록 하라 매개변수를 정해주지 않으면 컴퓨터의 기본설정 값을 그대로 이용해야 한다.


예제 5-12 entab과 detab을 고쳐서


entab -m +n


과 같이 하면 m열부터 n열 간격으로 tab이 설정되도록 해 보라.


예제 5-13 입력과 마지막 n행만을 출력하는 프로그램 tail을 작성하라.

n의 기본설정값은 10으로 하고 명령은


tail -n


과 같이 한다 필요한 최소의 메모리를 사용하도록 프로그램을 하라. 행의 저장은 5.6에있는 정렬 프로그램과 같은 방식이 되어야 한다






c를 많이 사용해 보지 않는 사람은 2차원 배열과 포인터 배열을 혼동하기 쉽다 다음과 같은 선언이 주어지면

    

int a[10][20];

int *b[10];


a[3][4]나 b[3][4]나 같은 정수 값이 된다. 그래서 두 경우가 같다고 생각할지 모르지만 사실은 차이가 있다 a는 2차원 배열이므로 200개의 정수가 한곳에 모여 저장되어있다. 그래서 a[row][col]의 위치는 처음부터 20*row+col 번째가 된다 b에서는 10개의 포인터가 선언되고 포기화는 이루어지지 않는다. 초기화를 하려면 초기화를 위한 문장을 써야 한다. b의 각 포인터가 20 요소의 배열을 가리키고 있다면 필요한 전체 메모리는 정수 200개와 10개의 포인터 만큼이 된다.즉 b의 경우 더 많은 메모리가 소요된다.b의 장점은 각 행의 길이가 달라도 된다는 것이다. 다시 말하면 b의 포인터는 요소 20개인 배열을 가리킬 수도 있고 20개가 아닌 배열을 가리킬 수도 있다는 말이다.

지금까지 요소가 정수인 배열과 그 포인터에 대해 설명 했는데 요소가 문자인 경우도 마찬가지 이다. 다른 길이의 문자열의 포인터를 사용하는 month_name이 좋은 예이다. 포인터의 배열을 사용하는 경우에는 다음과 같다


char *name[] = { "Illegal month", "Jan", "Feb", "Mar" };


2차원 배열을 사용하는 경우에는 다음과 같다


char aname[][15] = { "Illegal month", "Jan", "Feb", "Mar" };


예재 5-9 day_of_year와 month_day 함수를 포인터를 사용하도록 고쳐라.

몇번째 달인지를 알려주면 그 달의 이름(January 등)을 출력하는 함수 month_name(n)을 생각해 보자. 물론 출력은 이름을 가지키는 포인터가 될 것이다 내부 정적 배열(internal static array)의 사용에 중점을 두고 설명을 읽기 바란다 함수 month_name은 문자열을 원소로 가지고 있다가 이 함수가 호출될 떄 적당한 원소(예:n이 1일때 January라는 문자열)를 가리키는 포인터를 리턴하게 된다. 앞에서 배운 초깃밧을 주는 구문과 비슷한 방법으로 이 함수를 다음과 같이 작성할수 있다


/* month_name: return name of n-th month */

char *month_name(int n)

{

    static char *name[] = {

        "Illegal month",

        "January", "February", "March",

        "April", "May", "June",

        "July", "August", "September",

        "October", "November", "December"

    };

    return (n < 1 || n > 12) ? name[0] : name[n];

}


문자를 가리키는 포인터의 배열인 name의 선언은 정렬(sorting) 프로그램의 lineptr과 같은 방법으로 이루어진다 초깃값은 문자열인데 각각의 문자열은 배열 내의 지정된 위치에 놓인다 다시 설명하면 i번째 문자열은 기억장소 내의 어떤 우치에 놓이고 그곳의 포인터가 name[i]에 기억된다 배열의 그키가 정해지지 않았으므로 배열의 크기는 컴파일러가 정해준다



C는 다차원 배열(multi dimensional arrays)을 제공해 주지만 포인터의 배역보다는 많이 사용되지 않는다 이 절에서는 다차원 배열의 몇가지 성질에 대해서 살펴보겠다.

어떤달의 며칠이 1년의 몇번째 날인가를 계산하고 그 역으로도 계산하는 문제를 풀어보자 예를들면 3월1일은 그해의 60번째 날이 되고 윤년에는 61번째 날이 될 것이다 우선 두개의 함수를 정의하자 day_of_year는 월별 날짜를 연별 날짜로 바꾸는 함수이고,month_day는 연별 날짜를 월별 날짜로 바꾸는 함수이다.month_day 함수는 달 날짜 두 값을 리턴하게되므로 달과 날짜를 나타내는 매개변수는 포인터로 하기로 한다.


    month_day(1988,60,&m,&d)


는 m을 2로 d를 29로 해서 1988년의 60번째 날은 2월 29일임을 계산한다.

두함수는 같은정보로 실행되는데 그 정보는 어느 달이 며칠까지 있는지와 어느해가 윤년인지 하는 것이다. 이정볼르 가지고 있는 배열과 두함수는 다음과 같다.


static char daytab[2][13] = {

    {0,31,28,31,30,31,30,31,31,30,31,30,31},

    {0,31,29,31,30,31,30,31,31,30,31,30,31}

};

/* day_of_year : set day of year from month & day */

int day_of_year(int year,int month,int day)

{

    int i,leap;

    

    leap = year%4 == 0 && year%100 != 0 || year%400 == 0;

    for (i=1; i<month; i++)

        day += daytab[leap][i];

    return day;

    

}


/* month_day: set month, day from day of year */

void month_day(int year, int yearday, int *pmonth, int *pday) {

    int i, leap;

    

    leap = year%4 == 0 && year%100 != 0 || year%400 == 0;

    for (i = 1; yearday > daytab[leap][i]; i++)

        yearday -= daytab[leap][i];

    *pmonth = i;

    *pday = yearday;

}


윤년인지 아닌지를 나타내는 논리변수 leap의 값은 1(true) 또는 0(false)이므로 daytab이라는 배열의 첨자로 그대로 이용할수있다

배열 daytab은 day_of_year와 month_day에서 공동으로 사용할 수있게 외부형(external)으로 선언했다 여기서는 char 형의 변수에 다른 형의 데이터를 넣는 예를 보여주기 위해 daytab을 char로 했다

daytab은 2차원 배열이다.C에서는 2차원 배열을 각 원소가 1차원 배열인 배올로 취급한다 그러므로 배열 표시도 다른 언어에서 처럼


daytab[i,j] /* wrong */


로 하지 않고


daytab[i][j] /* [row][col] */


와 같이 한다. 표시방법이 다르지만 사용자는 다른언어와 같은 방법으로 배열을 사용하면 된다 2차원의 각 배열의 요소는 행단위로 지정된다 즉 1열 원소 2열 원소..이런식으로 저장된다

배열의 초기값을 정의할 때 초깃값을 중괄호 속에 써 준다. 2차원 배열의 경우 각 행의 초기값을 중괄호로 묶어서 써준다. C에서 배열의 첫번째 요소는 0번째 원소인데 1~12월 숫자 그대로 나타내기 위해 13요소의 배열을 잡고 0번째 요소의 값은 0으로 해주었다

2차원 배열이 함수로 전달되고 그 함수 내에서 배열을 선언할떄 열의 수는 반드시 써야 한다 2차원 배열을 매개변수로 사용할 때 각 행의 포인터가 전달되므로 행의 수는 선언할 필요가 없다 위와 같은 경우 13개의 정수형으로 이루어진 배열의 포인터가 전달되는 것이다 그러므로 daytab이란는 배열이 함수 f로 전달될 경우 f선언은


f(int daytab[2][13]){ ... }


또는


f(int daytab[ ][13]){ ... }


처럼 하면 되는데 행 번호를 나타내지 않으므로


f(int (*daytab)[13]){ ... }


로 해도 좋다. 이 문장은 매개변수가 13개 정수들의 배열을 가리키는 포인터임을 알려주는 것이다 이떄[]가 *보다 우선순위가 높으므로 ()가 필요하다. 만약


int *daytab[13]


과 같이 선언한다면 이는 13개 정수의 포인터로 이루어진 배열을 선언하는 것이 된다. 행의 숫자를 생략할 수있음은 앞에서 설명한 바와 같고 다른 것(열의 숫자)은 생략하면 안된다.


예제 5-8 day_of_year와 month_day 함수에 에러 검사하는 부분을 첨가해 보라