재귀를 이용한 하노이의 탑 시각화(windows와 linux에서만 컴파일 가능)





-sleep 함수 사용에 대해..


windows에서는 windows.h헤더 내에 선언된 Sleep함수(단위 ms)를 이용.


Linux에서는,

linux의 unistd.h에 존재하는 sleep 함수는 최소 대기시간이 1초이다. 만약 1초보다 짧은 시간 동안 프로그램을 재우고 싶다면, 다른 함수가 필요하다.

usleep함수는 microsecond 단위로 대기할 수 있지만, 더이상 잘 사용되지 않는다.

time.h의 nanosleep(시간단위는 nanosecond)을 사용했다(http://stackoverflow.com/a/7684399).




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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
// Visualization: Towers of Hanoi
 
#include <stdio.h>
#include <stdlib.h>
 
#ifdef _WIN32 // Windows (x64 and x86)
#include <windows.h>
#define WAIT(n) Sleep(400*(n))
#define CLRSCR() system("cls")
 
#elif defined __linux__
#include <time.h>
#define WAIT(n) nanosleep((const struct timespec[]){{0, 400000000L * (n)}}, NULL);
#define CLRSCR() system("clear && printf '\e[3J'")
#endif
 
void showTower(int arr[], int n);
void solveHanoi(int *arr, int n, int numDisks, int fromPeg, int toPeg);
void moveDisk(int *arr, int n, int fromPeg, int toPeg);
 
int main(void)
{
    int *arr;
    int size = -1;
    int fromPeg, toPeg;
    
    while (size < 0 || size > 9) {
        printf("원판의 갯수 입력(1-9): ");
        scanf("%d"&size);
    }
    getchar(); // skips '\n'
    for (;;) {
        printf("원판들이 있는 현재 막대기 입력(A, B, C): ");
        fromPeg = getchar();
        if (fromPeg == 'a' || fromPeg == 'A') {
            fromPeg = 0;
            break;
        }
        if (fromPeg == 'b' || fromPeg == 'B') {
            fromPeg = 1;
            break;
        }
        if (fromPeg == 'c' || fromPeg == 'C') {
            fromPeg = 2;
            break;
        }
    }
    getchar(); // skips '\n'
    for (;;) {
        printf("원판들이 이동할 막대기 입력 - 위에서 입력한 것과 다른 막대기(A, B, C): ");
        toPeg = getchar();
        if (fromPeg != 0 && toPeg == 'a' || toPeg == 'A') {
            toPeg = 0;
            break;
        }
        if (fromPeg != 1 && toPeg == 'b' || toPeg == 'B') {
            toPeg = 1;
            break;
        }
        if (fromPeg != 2 && toPeg == 'c' || toPeg == 'C') {
            toPeg = 2;
            break;
        }
    }
    
    /*
    원판 위치에 대한 배열 생성:
    배열의 i번째 멤버의 값은 (i+1)크기의 원판이 위치한 기둥 번호를 나타냄
    0: 기둥 A, 1: 기둥 B, 2: 기둥 C
    ex) arr[] = {0, 2, 1};
    => 크기 1의 원판은 기둥 A, 크기 2의 원판은 기둥 C, 크기 3의 원판은 기둥 B에 있음
    */
    arr = malloc(sizeof(int* size);
    
    for (int i = 0; i < size; i++) {
        arr[i] = fromPeg;
    }
 
    showTower(arr, size);
    WAIT(1);
    solveHanoi(arr, sizesize, fromPeg, toPeg);
    printf("\tdone! :)\n");
    return 0;
}
 
void showTower(int arr[], int n)
{
    int **coord;
    coord = malloc(sizeof(int ** n);
    CLRSCR();
    if (coord == NULL) {
        printf("malloc failed\n");
        exit(EXIT_FAILURE);
    }
    for (int i = 0; i < n; i++) {
        if ((coord[i] = malloc(sizeof(int* 3)) == NULL) {
            printf("malloc failed: %d\n", i);
            exit(EXIT_FAILURE);
        }
    }
 
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < 3; j++)
            coord[i][j] = 0;
    }
 
    int heightA = 0, heightB = 0, heightC = 0;
    for (int i = n - 1; i >= 0; i--)
    {
        switch (arr[i]) {
        case 0:
            coord[n - 1 - heightA++][0= i + 1;
            break;
        case 1:
            coord[n - 1 - heightB++][1= i + 1;
            break;
        case 2:
            coord[n - 1 - heightC++][2= i + 1;
            break;
        default:
            printf("error: %d\n", i);
            return;
        }
    }
 
    printf("\n\n");
    for (int i = 0; i < n; i++) {
        printf("\t");
        for (int j = 0; j < 3; j++) {
            int num = coord[i][j];
            if (!num)
                printf(":\t");
            else
                printf("%d\t", num);
 
        }
        printf("\n");
    }
    printf("\t_________________\n\tA\tB\tC\n");
 
    //frees the array
    
    for (int i = 0; i < n; i++)
        free(coord[i]);
    free(coord);    
    
    WAIT(1);    
}
 
void solveHanoi(int *arr, int n, int numDisks, int fromPeg, int toPeg)
{
    int sparePeg;
    if (numDisks == 0)
        return;
 
    switch (fromPeg + toPeg) {
    case 0 + 1:
        sparePeg = 2break;
    case 0 + 2:
        sparePeg = 1break;
    case 1 + 2:
        sparePeg = 0break;
    }
    solveHanoi(arr, n, numDisks - 1, fromPeg, sparePeg);
    moveDisk(arr, n, fromPeg, toPeg);
    solveHanoi(arr, n, numDisks - 1, sparePeg, toPeg);
}
 
void moveDisk(int *arr, int n, int fromPeg, int toPeg)
{
    for (int i = 0; i < n; i++) {
        if (arr[i] == fromPeg) {
            arr[i] = toPeg;
            break;
        }
    }
    showTower(arr, n);
}
cs


26.1 The <stdarg.h> Header: Variable Arguments


printf와 scanf 함수는 특이하게 인자의 개수에 대한 제한이 없는데, <stdarg.h> 헤더에서는 나만의 함수에서도 가변 숫자의 매개변수를 사용할 수 있는 기능을 제공한다.


<stdarg.h> 헤더는 한개의 타입(va_list)을 선언하고, 몇개의 매크로를 정의한다. C89에서는 va_start, va_arg, va_end의 세개의 매크로가 있다. C99에서는 function-like 매크로인 va_copy가 추가되었다.


integer 인자들(숫자 제한 없이) 중에서 가장 큰 수를 찾는 max_int 함수를 써 보자. max_int 호출은 다음과 같이 이뤄진다:


max_int(3, 10, 30, 20)


첫번째 인자는 추가로 몇개의 인자가 있는지를 지정한다. 위와 같이 max_int를 호출하면 함수는 30(10, 30, 20 중 가장 큰 수)을 리턴할 것이다.


int max_int(int n, ...)   /* n must be at least 1 */

{

    va_list ap;

    int i, current, largest;


    va_start(ap, n);

    largest = va_arg(ap, int);


    for (i = 1; i < n; i++) {

        current = va_arg(ap, int);

        if (current > largest)

            largest = current;

    }

    

    va_end(ap);

    return largest;

}


매개변수 리스트의 ... 기호(ellipsis로 불림)는 매개변수 n 뒤에 가변 숫자의 추가적인 매개변수가 뒤따른다는 것을 나타낸다. 

max_int의 body는 va_list 타입의 변수를 선언하면서 시작한다.


va_list ap;

이 변수를 선언하는 것은 max_int가 n 뒤에 따라오는 인자들에 접근하기 위해서 필수적이다.


va_start(ap, n);

위 statement는 가변 길이의 인자가 어디서 시작되는지 지정한다(이 경우, n 바로 뒤). 가변 개수 인자를 갖는 함수는 반드시 최소 하나의 "normal" 매개변수를 가져아 하고, 생략 기호는 매개변수 리스트의 가장 끝(마지막 노멀 파라미터 뒤에)에 와야 한다.


largest = va_arg(ap, int);

위 statement는 max_int의 두번째 인자(n 바로 뒤에 오는 것)를 읽어들이고, 그것을 largest에 할당한다. 그리고 자동으로 다음 인자로 진행한다. int는 max_int의 두번째 인자가 int type을 갖는 것으로 기대한다는 의미이다.

current = va_arg(ap, int);

루프 내에 있는 위 statement는 max_int의 남은 인자를 하나씩 하나씩 읽어들이게 된다.


va_end(ap);

위 statement는 함수 리턴 전에 "clean up" 하기 위해 필요하다. (또는 반환하는 대신에 va_start를 호출해서 인자 리스트를 처음부터 다시 탐색할 수도 있다).


va_copy 매크로는 src(a va_list value)를 dest(a va_list value)에 복사한다. va_copy는 src를 dest로 복사하기에 앞서 여러 번 va_arg를 호출할 수 있다는 점에서 유용하다. va_copy를 호출함으로써 인자 리스트를 기억할 수 있고 같은 지점으로 돌아와서 인자(그리고 그 뒤에 있는 인자들도)를 다시 탐색할 수 있다.

 각각의 va_start나 va_copy 호출은 va_end와 짝을 이뤄야 하며, 같은 함수 내에 나타나야 한다. va_arg의 호출은 반드시 va_start(또는 va_copy) 호출과 그와 짝을 이루는 va_end호출 사이에서 이뤄져야 한다.


Calling a Function with a Variable Argument List

가변 인자 함수 호출하기


가변 인자 리스트 함수를 호출하는 것은 본질적으로 위험한 작업이다. printf와 scanf함수 사용시에 이미 보았지만, 잘못된 인자를 전달하는 것은 매우 위험하다. 다른 가변 길이 인자 함수도 그와 동일하게 민감하다. 주된 어려움은 가변 인자 함수가 인자의 개수나 형식을 알 방법이 없다는 것이다. 그러한 정보가 함수로 전달되며, 또는(and/or) 함수에 의해 가정된다. max_int 함수는 첫번째 인자에 의존해서 추가 인자가 몇 개나 올지를 알 수 있다. 또 인자들의 형식이 int라고 가정한다. printf와 scanf와 같은 함수는 추가적인 인자와 각각의 타입을 나타내 주는 format string에 의존한다.

 또 다른 문제는 NULL을 인자로 전달하는 문제이다. NULL은 보통 0으로 정의된다. 그런데 0이 가변 인자로 전달되는 경우 컴파일러는 그것을 integer 형식이라고 간주한다. 해결책은 cast로 (void*) NULL 또는 (void*) 0 이라고 표기하는 것이다.

IEEE(Institude of Electrical and Electronics Engineers)에 의해 제정된 IEEE Standard 754는 부동 소수점 숫자에 대한 두 가지 기본 형식을 제공한다. single precision(32비트), double precision(64비트)


숫자들은 과학적 표기법(지수 표기법) 형태로 저장되며, 각각의 숫자는 세가지 요소(a sign, an exponent, a fraction)으로 구성되어 있다. 

exponent를 위해 예약된(reserved) 비트 수에 의해 숫자가 얼마나 클(작을) 수 있는지 결정된다.

fraction을 위해 예약된 비트의 수는 정확도를 결정한다.


single-precision에서는 exponent의 길이가 8비트이고, fraction은 23비트이다. 그 결과 single-precision number의 최대값은 대략 3.40 X 10^38이며, 정확도는 십진수로 약 6개의 숫자이다(with a precision of about 6 decimal digits).


IEEE 표준에는 두 가지 다른 포맷도 있는데, single extended precision과 double extended precision이다. 표준에서는 각 포맷의 비트 수를 명시하고 있지는 않으나, single extended 타입의 경우 최소 43비트를, double extended type의 경우 최소 79비트를 차지해야 한다.


더 자세한 사항은, What Every Computer Scientist Should Know About Floating-Point Arithmetic, by David Goldberg, published in the March, 1991 issue of Computing Surveys


22.4 Character I/O


글자 한개를 읽고 쓰는 함수들이다. 이 함수들은 text stream, binary stream 모두에서 잘 작동한다.


Output Functions

int fputc(int c, FILE *stream);

int putc(int c, FILE *stream);

int putchar(int c);


putchar는 하나의 문자를 stdout 스트림에 쓴다.

putchar(ch);    /* writes ch to stdout */


fputcputc는 putchar의 더 일반적인 버전으로 하나의 문자를 임의의 스트림에 쓴다.

fputc(ch, fP);    /* writes ch to fp */

putc(ch, fp);    /* writes ch to fp */


putc와 fputc는 동일한 작업을 하지만, putc는 보통 매크로로 구현되어 있다(함수로도 구현되어 있다). fputc는 오직 함수로만 구현되어 있다. 따라서 putc가 fputc보다 빠르고 더 선호된다.

putchar 자체가, 보통 putc를 이용한 매크로로 정의되기도 한다.

#define putchar(c) putc((c), stdout)


C 표준에서는 putc 매크로가 stream 인자를 두 번 이상 측정하는 것을 허용하고 있다. 하지만 fputc에서는 그렇지 않다. putc는 더 빠르지만, fputc는 매크로가 가지고 있는 잠재적인 문제점으로부터 자유롭다.

http://stackoverflow.com/questions/14008907/fputc-vs-putc-in-c


읽기 에러가 발생하면, 세 함수 모두 스트림의 error indicator를 set하고 EOF를 리턴한다. 그 외에는 모두 쓰인 글자를 리턴한다.


Input Functions

int fgetc(FILE *stream);

int getc(FILE *stream);

int getchar(void);

int ungetc(int c, FILE *stream);


getchar는 stdin 스트림으로부터 문자 한개를 읽는다.

ch = getchar();    /* reads a character from stdin */


fgetcgetc는 임의의 스트림으로부터 문자 한개를 읽는다.

ch = fgetc(fp);    /* reads a character from fp */

ch = getc(fp);     /* reads a character from fp */


세 함수는 모두 문자를 unsigned char 형식의 값으로 간주한다(그리고 리턴하기 전에 int 형식으로 변환해 리턴한다). 그 결과, 리턴 값은 EOF를 제외하면 절대로 음의 값을 리턴하지 않는다.

getc와 fgetc의 차이는 putc와 fputc의 차이와 유사하다. getc는 보통 매크로로 구현되어 있다(함수로도 구현되어 있다). fgetc는 항상 함수로만 구현되어 있다. getchar도 보통 매크로로 구현되어 있다.

#define getchar() getc(stdin)


보통은 getc를 fgetc보다 선호한다. 하지만 getc가 

fgetc, getc, getchar 함수도 에러가 발생했을 때 똑같은 행동을 한다. 파일의 끝이 되면, 스트림의 end-of-file indicator를 set하고 EOF를 리턴한다. 만약 읽기 에러가 발생한 경우, 스트림의 error indicator를 set하고 EOF를 리턴한다. 둘 중 어떤 문제가 발생했는지 알기 위해, feof와 ferror를 호출해야 한다.


fgetc, getc, getchar의 가장 흔한 사용법은 파일에서 end-of-file이 나타날 때까지 문자를 한개씩 읽는 것이다. 다음 while loop가 그런 목적을 위해 사용된다.


while ((ch = getc(fp)) != EOF) {

    ...

}


fgetc, getc, getchar의 리턴 값을 언제나 char 변수가 아닌 int 형식 변수에 저장해야 한다. char 변수에 EOF를 테스트하는 것은 틀린 결과를 낳을 수 있다.


ungetc 함수는 스트림에서 읽은 문자를 되돌리고 스트림의 end-of-file indicator를 clear한다. 이것은 우리가 입력 도중 "미리보기" 문자를 필요로 할 때 유용하다. 예를 들어, 연속된 숫자를 읽기 위해, 첫번째 nondigit에서 멈추려면 다음과 같이 쓴다.


while (isdigit(ch = getc(fp))) {

   ...

}

ungetc(ch, fp);    /* pushes back last character read */


읽기 작업을 방해하지 않으면서 ungetc를  연속 호출해서 되돌릴 수 있는 문자의 갯수는 implementation과 스트림의 종류에 따라 다르다. 첫번째로 호출한 ungetc만 성공이 보장된다. file-positioning function(fseek, fsetpos, rewind) 호출시 pushed-back character를 잃어버린다.



/*
fcopy.c
to copy the file from f1.c to f2.c, use the command
fcopy f1.c f2.c

*/

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

int main(int argc, char *argv[])
{
    FILE *source_fp, *dest_fp;
    int ch;

    if (argc != 3) {
        fprintf(stderr, "usage: fcopy source dest\n");
        exit(EXIT_FAILURE);
    }

    if ((source_fp = fopen(argv[1], "rb")) == NULL) {
        fprintf(stderr, "Can't open %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    if ((dest_fp = fopen(argv[2], "wb")) == NULL) {
        fprintf(stderr, "Can't open %s\n", argv[2]);
        fclose(source_fp);
        exit(EXIT_FAILURE);
    }

    while ((ch = getc(source_fp)) != EOF)
        putc(ch, dest_fp);

    fclose(source_fp);
    fclose(dest_fp);

    return 0;
}


22.5 Line I/O


여기서 다를 함수들은 'lines'를 읽고 쓰는 함수들이다. 대부분 바이너리 스트림에서 사용하는것도 legal하지만, 대부분 텍스트 스트림에서 쓰인다.


Output Functions

int fputs(const char * restrict s,

          FILE * restrict stream);

int puts(const char *s);


puts 함수는 13.3에서 다룬 바 있다. string of characters를 stdout에 쓴다.

puts("Hi, there!");    /* writes to stdout */


문자열의 문자들을 쓴 이후, puts는 언제나 개행 문자를 더한다.


fputs는 puts의 더 일반적인 버전이다. 두번째 인자는 output을 어떤 스트림에 쓸 지를 지정한다.

fputs("Hi, there!", fp);    /* writes to fp */


puts와 달리, fputs 함수는 문자열의 끝에 추가로 개행문자를 더하지 않는다.

두 함수 모두 에러 발생시에는 EOF를, 다른 경우에는 음이 아닌 정수를 리턴한다.


Input Functions

char *fgets(char * restrict s, int n,

            FILE * restrict stream);

char *gets(char *s);


gets 함수는 13.3에서 다룬 바 있다. stdin으로부터 한 줄의 라인을 읽는다.

gets(str);    /* reads a line from stdin */


gets는 문자들을 하나씩, 개행 문자를 읽을 때까지 읽어서, str이 가리키는 배열에 저장한다. 개행 문자는 버려진다.


fgets 함수는 gets의 일반적인 버전으로 어떤 스트림에서든 읽을 수 있다. 또한 fgets는 gets보다 더 안전한데, 읽어들일 문자의 개수를 제한하기 때문이다. str이 문자의 배열이라 가정할 때 다음과 같이 fgets를 사용할 수 있다.

fgets(str, sizeof(str), fp);    /* reads a line from fp */


fgets는 첫번째 개행문자를 읽거나 sizeof(str) - 1개의 문자를 읽으면(둘 중 한 조건을 만족하면) 문자 읽기를 멈춘다. 개행문자를 읽으면, fgets는 그것을 다른 문자들과 같이 저장한다. 따라서 gets는 개행문자를 절대 저장하지 않고, fgets는 somtimes does.)


gets와 fgets 함수는 에러가 발생하거나 문자를 저장하기 전 입력 스트림의 끝에 도달했을 때 null 포인터를 리턴한다. (이 때는 feof와 ferror 함수를 호출해 어떤 상황이 발생했는지를 판별할 수 있다) 다른 경우에는 두 함수는 두 함수 입력을 저장한 배열을 가리키는 첫번째 인자를 리턴한다. 두 함수 모두 문자열 끝에 null character를 추가한다.

 이제 fgets를 알았기 때문에, 항상 언제나 gets보다 fgets를 사용하는 것이 좋다. gets의 경우, 항상 저장되는 배열의 범위를 넘을 수 있는 위험성이 존재하기 때문에, 읽히는 문자열이 배열의 크기를 초과하지 않는다는 것이 보장될 때에만 안전하다. 만약 그러한 보장이 없을 때에는 fgets를 사용하는 것이 훨씬 안전하다.



22.6 Block I/O


size_t fread(void * restrict ptr,

             size_t size, size_t nmemb,

             FILE * restrict stream);

size_t fwrite(const void * restrict ptr,

              size_t size, size_t nmemb,

              FILE * restrict stream);


fread와 fwrite 함수는 프로그램이 한번에 데이터의 큰 블럭을 읽고 쓰도록 해 준다. fread와 fwrite 함수는 주로 바이너리 스트림에서 사용되지만, 주의해서 다룬다면 텍스트 스트림에서도 사용할 수 있다.


fwrite 함수는 배열을 메모리에서 스트림으로 복사하도록 설계되었다. 첫번째 인자는 배열의 주소, 두번째 인자는 각 원소의 크기(in bytes), 세번째 인자는 쓸 원소의 개수이다. 네번째 인자는 파일 포인터로 데이터를 어디에 쓸 지를 지정한다. 배열 a의 모든 원소를 쓸 때 다음과 같이 fwrite를 사용한다.


fwrite(a, sizeof(a[0]), sizeof(a) / sizeof(a[0]), fp);


배열의 모든 원소를 써야 한다는 법은 없으며 그 일부만 복사해도 된다. fwrite는 쓰인 원소의 개수(not bytes)를 리턴한다. 에러가 발생했을 때 리턴 값은 세번째 인자보다 작은 값이 된다.


fread 함수는 스트림에서 배열의 원소들을 읽는다. 인자들은 fread와 비슷하다. 순서대로 배열의 주소, 각 원소의 크기(in bytes), 읽을 원소의 개수, 파일 포인터이다. 파일의 내용을 읽어서 배열 a에 저장하려면, 다음과 같이 fread를 호출한다.


n = fread(a, sizeof(a[0]), sizeof(a) / sizeof(a[0]), fp);


fread의 리턴 값을 체크하는 것이 중요하다. 이는 실제로 읽은 원소의 개수를 나타낸다. 입력 파일의 eof에 도달하거나 읽기 에러 발생한 경우가 아니라면 리턴 값은 세번째 인자와 같아야 한다. 세번째 인자보다 더 작은 값이 리턴된 경우 feof와 ferror 함수를 사용해서 이유를 알 수 있다.


n = fread(a, 1, 100, fp);   /* return value will be between 0 and 100) */

n = fread(a, 100, 1, fp);   /* return value will be either 0 or 1 */


fwrite는 종료 이전에 데이터를 파일에 저장해야 할 때 유용하다. 나중에 그 프로그램이나 또는 다른 프록그램에서 fread로 데이터를 메모리로 다시 읽을 수 있다. fread와 fwrite가 다루는 데이터는 꼭 배열 형태일 필요가 없다. 어떤 종류의 변수에 대해서도 잘 작동한다. 특히 구조체도 읽고 쓸 수 있다. 구조체 변수 s를 파일에 쓰고자 할 때, 다음과 같이 fwrite를 호출할 수 있다.

fwrite(&s, sizeof(s), 1, fp);


fwrite로 포인터 변수가 들어있는 구조체를 쓸 때는 조심해야 한다. 이 값들은 다시 읽었을 때 유효하리라는 보장이 없다.


22.7 File Positioning


int fgetpos(FILE * restrict stream,

            fpos_t * restrict pos);

int fseek(FILE *stream, long int offset, int whence);

int fsetpos(FILE *stream, const fpos_t *pos);

long int ftell(FILE *stream);

void rewind(FILE *stream);


모든 스트림은 file position과 관련되어 있다. 파일이 열렸을 때, 파일의 포지션은 파일 가장 처음으로 set된다. (만약 파일이 "append" 모드로 열렸을 경우, 처음 파일 포지션은 파일의 가장 처음일 수도, 마지막일 수도 있다. implementation-dependent) 그리고 읽기나 쓰기 작업이 이뤄지면, 파일 내에서 자동적으로 이동해서 우리가 파일 내에서 순차적으로 움직일 수 있게 된다.

 많은 프로그램에서 순차적인 진행으로 좋지만, 어떤 프로그램에서는 파일 내에서 점프해서 이곳저곳의 데이터에 접근해야 한다. <stdio.h>는 5가지의 함수를 제공해서 파일의 현재 포지션을 알아내거나 바꿀 수 있게 해 준다.


fseek 함수는 첫번째 인자(파일 포인터)와 관련된 파일 포지션을 변경한다. 세번째 인자는 파일의 시작위치, 현재 위치, 또는 파일의 마지막 위치로부터 상대적 거리로 표시된 새 위치를 지정한다. <stdio.h>에서는 다음 세가지 매크로를 제공한다.


SEEK_SET    Beginning of file

SEEK_CUR    Current file position

SEEK_END    End of file


두번째 인자는 byte count이고 음수가 될 수도 있다.

fseek(fp, 0L, SEEK_SET);      /* moves to beginning of file */

fseek(fp, 0L, SEEK_END);      /* moves to end of file */

fseek(fp, -10L, SEEK_CUR);    /* moves back 10 bytes */


숫자 뒤 L은 long을 의미한다. L을 붙이지 않더라도 인자들은 자동적으로 변환된다. 

보통 fseek은 0을 리턴한다. 만약 에러 발생시(예를 들어 요청한 포지션이 존재하지 않는 경우), fseek은 0이 아닌 값을 리턴한다. file-positioning 함수는 바이너리 스트림에 사용되는 것이 가장 좋다. 텍스트 스트림에 사용하는 것이 가능하지만, 운영 체제간의 차이 때문에 조심해서 사용해야 한다. 텍스트 스트림의 경우, (1) offset(fseek의 두번째 인자)이 0이거나, (2) whence(세번째 인자)가 SEEK_SET이고 offset이 이전에 ftell 호출해서 얻은 값이어야 한다. (다시 말해 fseek으로는 텍스트 스트림의 가장 처음, 끝 또는 이전에 간 적이 있었던 지점으로만 갈 수 있다) 바이너리 스트림의 경우, whence가 SEEK_END인 경우의 호출을 지원하지 않아도 된다.


ftell 함수는 현재 파일 위치를 long integer로 리턴한다. 만약 에러 발생시, ftell은 -1L을 리턴하고 에러 코드를 errno에 저장한다. ftell의 리턴값을 저장했다가 이후에 fseek 호출시 인자로 사용해서 이전 파일 위치로 돌아가도록 사용할 수 있다.


long file_pos;

...

file_pos = ftell(fp);             /* saves current position */

...

fseek(fp, file_pos, SEEK_SET);    /* returns to old position */


fp가 바이너리 스트림인 경우, ftell(fp) 호출시 byte count로 측정된 현재 위치를 리턴한다(0은 파일의 시작을 나타냄). fp가 텍스트 스트림인 경우, ftell(fp)는 꼭 byte count이지 않아도 된다. 따라서, ftell의 리턴값에 산술 연산을 수행하지 않는 것이 바람직하다. 예를 들어, 파일의 서로 다른 위치 간의 거리를 알기 위해 두 위치에서 얻은 ftell의 값을 빼는 것은 좋은 생각이 아니다. 


rewind 함수는 파일 포지션을 처음으로 설정한다. rewind(fp)와 fseek(fp, 0L, SEEK_SET)은 거의 동일하다. 차이점은 rewind는 리턴 값이 없지만 fp의 error indicator를 clear한다. fseek 호출시에는 end-of-file indicator만 clear된다.


fseek와 ftell 함수가 가지는 문제점: 위치를 long integer로 저장할 수 있는 파일들에만 사용할 수 있다. 매우 큰 파일을 다룰 때를 위해, 두개의 함수가 추가로 제공된다. fgetpos, fsetpos. 이 함수들은 fpos_t 형식의 값을 사용해 파일 위치를 나타낸다. fpos_t 값은 꼭 정수이지 않아도 되며 예를 들어 구조체일 수도 있다.

 fgetpos(fp, &file_pos) 를 호출하면 fp와 관련된 파일 포지션을 file_pos 변수에 저장한다. fsetpos(fp, &file_pos) 호출시 fp와 관련된 파일의 포지션을 file_pos에 저장된 값으로 설정한다. (이 값은 file_pos 변수에 의해 저장된 값이어야 한다) 만약 fgetpos나 fsetpo가 실패했을 때에는 에러 코드를 errno에 저장한다. 두 함수 모두 성공했을 때에는 0을, 실패했을 때에는 0이 아닌 값을 리턴한다.


fpos_t file_pos;

...

fgetpos(fp, &file_pos);         /* saves current position */

...

fsetpos(fp, &file_pos);         /* returns to old position */


// invclear.c


/* Modifies a file of part records by setting the quantity
   on hand to zero for all records */
   
#include <stdio.h>
#include <stdlib.h>

#define NAME_LEN 25
#define MAX_PART 100

struct part {
    int number;
    char name[NAME_LEN+1];
    int on_hand;
} inventory[MAX_PARTS];

int num_parts;

int main(void)
{
    FILE *fp;
    int i;
    
    if ((fp = fopen("inventory.dat", "rb+")) == NULL) {
        fprintf(stderr, "Can't open inventory file\n");
        exit(EXIT_FAILURE);
    }
    
    num_parts = fread(inventory, sizeof(struct part),
                      MAX_PARTS, fp);
                      
    for (i = 0; i <num_parts; i++)
        inventory[i].on_hand = 0;
    
    rewind(fp);
    fwrite(inventory, sizeof(struct part), num_parts, fp);
    fclose(fp);
    
    return 0;
}

rewind를 호출하는 것은 매우 중요하다. fread를 호출한 후에, 파일의 위치는 eof이다. 만약 rewind를 호출하지 않고 fwrite함수를 호출하면, 원래 파일 내용에 덮어쓰는 것이 아니라 기존 파일에 이어서 쓰게 된다.



22.8 String I/O


이 섹션에서 설명하는 함수들은 다소 생소한데, 스트림이나 파일과 아무 관련이 없기 때문이다. 대신 이 함수들을 통해 마치 문자열이 스트림인 것처럼 데이터를 읽고 쓸 수 있다. sprintf와 snprintf 함수는 스트림에 쓸 때와 같은 방식으로 문자열에 문자들을 쓴다. sscanf 함수는 스트림에서 읽는 것인 양 문자열에서 문자들을 읽는다. 이 함수들을 쓸 때 ...printf의 포맷 스트링 능력과 ...scanf의 강력한 패턴 매칭 능력을 실제 스트림을 사용하지 않고서도 이용할 수 있다. 이 섹션에서 spirntf, snprintf, sscanf의 디테일을 다룬다. 

 세가지 비슷한 함수(vsprintf, vsnprintf, vsscanf)도 <stdio.h>에 속해 있다. 하지만 이 함수들은 <stdarg.h>에 선언된 va_list 형식에 의존하기 때문에 그 헤더를 다루는 26.1에서 다룬다.


Output Functions

int sprintf(char * restrict s,

            const char * restrict format, ...);

int snprintf(char * restrict s, size_t n, const char * restrict format, ...); // (C99)


sprintf 함수는 printf, fprintf 함수와 비슷하다. 차이점은 이 함수는 출력을 첫번째 인자가 가리키는 문자 배열에 쓴다는 점이다. sprintf의 두번째 인자는 printf에서 사용되는 포맷 스트링과 동일하다.


sprintf(date, "%d/%d/%d", 9, 20, 2010);


은 date에 "9/20/2010"이라고 쓴다. 문자열에 쓰기를 끝내면 끝에 null character를 더하고 쓰인 문자의 개수(null character 제외)를 리턴한다. 만약 인코딩 에러가 발생하면(와이드 문자가 적합한 멀티바이트 문자로 translate될 수 없었음), sprintf는 음의 값을 리턴한다.


snprintf 함수는 추가로 매개변수 n이 있는 것을 제외하면 sprintf 함수와 동일하다. null character를 제외하고 최대 n-1개의 문자만 문자열에 쓰일 수 있다. n이 0이 아닌 이상 null 문자는 항상 쓰이게 된다.


snprintf(name, 13, "%s, %s", "Einstein", "Albert");


는 name 변수에 "Einstein, Al"을 쓰게 된다.

snprintf는 만약 길이 제한이 없었다면 쓰였을 글자의 개수(null character 제외)를 리턴한다. 인코딩 에러가 발생하면, snprintf 함수는 음의 값을 리턴한다. 


Input Functions

int sscanf(const char * restrict s,

           const char * restrict format, ...);


sscanf 함수는 fscanf, scanf함수와 비슷하다. 차이는 sscanf 함수는 스트림 대신 문자열(첫번째 인자가 가리키는)로부터 읽는다는 점이다. sscanf의 두번째 인자는 scanf와 fscanf 함수에서 쓰이는 포맷 스트링과 동일하다.

 sscanf는 다른 입력 함수에서 읽은 문자열로부터 데이터를 추출할 때 유용하다. 예를 들어 fgets로 입력 한 라인을 얻고, sscanf로 그 라인을 전달할 수 있다.


fgets(str, sizeof(str), stdin);     /* reads a line of input */

sscanf(str, "%d%d", &i, &j);        /* extracts two integers */


scanf나 fscanf 대신 sscanf를 사용하는 것의 장점은 입력 라인을 원하는 만큼 포맷을 바꿔가면서 읽을 수 있다는 점이다.

scanf와 fscanf와 마찬가지로 sscanf는 성공적으로 읽고 저장한 데이터의 개수를 리턴한다. 첫번째 아이템을 찾기 전에 null character를 만난 경우, EOF를 리턴한다.

22.3 Formatted I/O


이 섹션에서는, 포맷 스트링을 사용해서 입력과 출력을 제어하는 라이브러리 함수들을 알아본다. printf와 scanf를 포함하는 이 함수들은 입력시 문자 형태의 데이터를 숫자 형태의 입력으로 변환하고, 출력시 숫자 형태의 데이터를 문자 형태로 변환한다. 다른 I/O 함수들은 그런 변환을 할 수 없다.


The ...prinf Functions

int fprintf(FILE * restrict stream,

            const char * restrict format, ...);

int printf(const char * restrict format, ...);


fprintf와 printf 함수는 출력의 외양을 제어하는 포맷 스트링을 사용해서 가변 갯수의 데이터 아이템을 출력 스트림에 쓴다. 두 함수의 원형은 ... 기호(an ellipsis)로 끝난다. 이것은 인자가 가변 갯수만큼 추가될 수 있음을 나타낸다. 두 함수 모두 쓰인 문자의 갯수를 리턴한다. 에러 발생시에는 음의 값을 리턴한다.


 printf와 fprintf의 유일한 차이는 printf는 항상 stdout(표준 출력 스트림)에 쓰는 반면, fprintf는 첫번째 인자가 지정하는 스트림에 쓴다는 점이다.

printf("Total: %d\n", total);         /* writes to stdout */

fprintf(fp, "Total: %d\n", total);    /* writes to fp */


printf를 출력하는 것은 fprintf의 첫번째 인자를 stdout으로 출력하는 것과 동일하다.

fprintf를 단지 디스크의 파일에만 데이터를 저장하는 함수로 생각하면 안 된다. <stdio.h>의 많은 함수들과 마찬가지로, fprintf는 어떤 출력 stream에든 잘 작동한다. fprintf의 가장 흔한 용도 중 하나는 에러 메시지를 디스크의 파일과는 아무런 관련이 없는 stderr(표준 에러 스트림)으로 출력하는 것이다.

fprintf(stderr, "error: data file can't be opened.\n");


stderr에 메시지를 쓰는 경우 stdout이 redirect된 경우에도 메시지가 화면에 나타난다.

formatted output을 스트림에 쓰는 함수는 이 외에도 vfprintf와 vprintf가 있지만 잘 쓰이지 않는다. 두 함수는 <stdarg.h>헤더에 선언된 va_list 형식에 의존한다.(나중에 설명)


...printf Conversion Specifications

printf와 fprintf 함수는 일반적인 문자와 conversion specifier를 모두 포함할 수 있는 format string을 필요로 한다. 일반적인 문자들은 있는 그대로 출력되고, conversion specification은 나머지 인자들이 어떻게 문자로 변환되어 출력될지를 지정한다.

 ...printf 함수의 conversion specifaction은 % 문자와, 그 뒤에 오는 5개의 개별적인 아이템으로 구성된다.


Flags(optional; 한개 이상 사용 가능)


Minimum field width(optional). 이 숫자만큼의 자리를 점유하지 못하는 작은 아이템의 경우, 공백이 추가된다. 기본값으로 공백이 왼쪽에 추가되어 우측 정렬된다. 이 숫자보다 더 많은 글자를 차지해야 하는 큰 아이템이더라도 전체가 다 표시된다. 필드 너비는 정수이거나 문자 * 이어야 한다. 만약 *가 된 경우, 필드 너비는 그 다음 인자로부터 얻어진다. 만약 인자가 음수라면, - flag가 붙은 양의 정수로 취급된다.


Precision(optional). precision의 의미는 conversion에 따라 다르다.

d, i, o, u, x, X: 숫자들의 최소 개수(만약 더 적은 숫자만으로 표시되는 경우 0이 앞쪽에 추가됨)

a, A, e, E, f, F: 소수점 이후에 오는 숫자의 개수

            g, G: 유효숫자의 개수

               s: 바이트의 최대 숫자

precision은 마침표(.) 뒤에 정수가 오거나 문자 * 이 온다. 만약 * 이 사용된 경우, precision은 다음 인자로부터 얻어진다. 만약 인자가 음수인 경우, precision을 지정하지 않은 것과 같게 된다. 마침표만 사용된 경우, precision은 0이 된다.


Length modifier(optional). length modifier가 존재한다면 표시될 아이템의 형식이 특정 conversion specification의 일반적인 형식보다 길거나 짧다는 것을 나타낸다. (예를 들어, %d는 보통 int 값을 표시하고, %hd는 short int를, %ld는 long int를 표시할 때 사용된다.)


Conversion specifier. 다음 표에 있는 문자들만 conversion specifier가 될 수 있다. f, F, e, E, g, G, a, A는 double 값을 나타내기 위해 설계되었지만, float 값에 대해서도 잘 작동한다. default argument promotion 덕분에 가변 인자를 갖는 함수에 전달된 float 인자는 double로, 자동적으로 변환된다. 비슷하게, ...printf 로 전달된 char 인자는 int로 변환되고, %c 변환이 잘 작동하게 된다.


C99 Changes to ...printf Conversion Specifications


C99에서 printf와 fprintf의 conversion specification에 생긴 몇가지 변화들


추가된 length modifiers. hh, ll, j, z, t length modifier가 추가되었다. hh와 ll은 추가적인 길이 옵션을 제공하고, j는 가장 큰 정수 타입을 쓸 수 있게 해 주고, z와 t는 각각 size_t와 ptrdiff_t 형식의 값을 쓰기 쉽게 해 준다.


추가된 conversion specifiers. F, a, A conversion specifier가 추가되었다. F는 infinity와 NaN을 쓰는 방법을 제외하면 f와 동일하다. a와 A 변환은 드물게 사용된다. 16진수 부동 소수점 상수와 연관되어 있다.


infinity와 NaN을 쓸 수 있음. IEEE 754 floating-point standard에서는 부동 소수점 연산 결과를 infinity, negative infinity, 또는 NaN("not a number")으로 할 수 있게 허용했다. 예를 들어 1.0을 0.0으로 나누면 positive infinity, -1.0을 0.0으로 나누면 negative infinity, 0.0을 0.0으로 나누는 것은 NaN(결과가 수학적으로 정의되지 않았기 때문)이 된다. C99에서 a, A, e, E, f, F, g, G conversion specifier는 이러한 특별한 값을 출력할 수 있게 해 준다. 소문자 변환지정자들은 inf (또는 infinity), - inf (또는 -infinity), nan, -nan으로, 대문자 변환 지정자들은 똑같은 것들을 대문자로 표시한다.


wide 문자 지원. fprintf를 통해 와이드 문자를 쓸 수 있게 되었다. %lc로는 와이드 문자 하나를 쓴다. %ls로는 와이드 문자열을 쓴다.


정의되지 않은 conversion specificasions들이 허용됨. C89에서는 %le, %lE, %lf, %lg, %lG가 정의되지 않았었다. C99에서는 이제 이런 표현들이 허용된다.(l length modifier가 무시된다)



Examples of ...printf Conversion Specifications


#include <stdio.h>

int main(void)
{
    printf("Result of applying conversion to 123\n");
    printf(  "%8d\n", 123);
    printf( "%-8d\n", 123);
    printf( "%+8d\n", 123);
    printf( "% 8d\n", 123);
    printf( "%08d\n", 123);
    printf("%-+8d\n", 123);
    printf("%- 8d\n", 123);
    printf("%+08d\n", 123);
    printf("% 08d\n", 123);

    printf("Result of applying conversion to -123\n");
    printf(  "%8d\n", -123);
    printf( "%-8d\n", -123);
    printf( "%+8d\n", -123);
    printf( "% 8d\n", -123);
    printf( "%08d\n", -123);
    printf("%-+8d\n", -123);
    printf("%- 8d\n", -123);
    printf("%+08d\n", -123);
    printf("% 08d\n", -123);
}
/*
Result of applying conversion to 123
ººººº123
123ººººº
ºººº+123
ººººº123
00000123
+123ºººº
º123ºººº
+0000123
º0000123
Result of applying conversion to -123
ºººº-123
-123ºººº
ºººº-123
ºººº-123
-0000123
-123ºººº
-123ºººº
-0000123
-0000123
*/



#include <stdio.h>

int main(void)
{
    printf("Result of applying conversion to 123\n");
    printf( "%8o\n", 123);
    printf("%#8o\n", 123);
    printf( "%8x\n", 123);
    printf("%#8x\n", 123);
    printf( "%8X\n", 123);
    printf("%#8X\n", 123);

    printf("Result of applying conversion to 123.0\n");
    printf( "%8g\n", 123.0);
    printf("%#8g\n", 123.0);
    printf( "%8G\n", 123.0);
    printf("%#8G\n", 123.0);
}
/*
Result of applying conversion to 123
ººººº173
ºººº0173
ºººººº7b
ºººº0x7b
ºººººº7B
ºººº0X7B
Result of applying conversion to 123.0
ººººº123
º123.000
ººººº123
º123.000
*/


#include <stdio.h>

int main(void)
{
    char *s1 = "bogus", *s2 = "buzzword";

    printf(   "%6s\n", s1);
    printf(  "%-6s\n", s1);
    printf(  "%.4s\n", s1);
    printf( "%6.4s\n", s1);
    printf("%-6.4s\n", s1);

    printf(   "%6s\n", s2);
    printf(  "%-6s\n", s2);
    printf(  "%.4s\n", s2);
    printf( "%6.4s\n", s2);
    printf("%-6.4s\n", s2);
}
/*
ºbogus
bogusº
bogu
ººbogu
boguºº
buzzword
buzzword
buzz
ººbuzz
buzzºº
*/


#include <stdio.h>

int main(void)
{
    printf("%.4g\n", 123456.);
    printf("%.4g\n",  12345.6);
    printf("%.4g\n",   1234.56);
    printf("%.4g\n",    123.456);
    printf("%.4g\n",     12.3456);
    printf("%.4g\n",       1.23456);
    printf("%.4g\n",        .123456);
    printf("%.4g\n",        .0123456);
    printf("%.4g\n",        .00123456);
    printf("%.4g\n",        .000123456);
    printf("%.4g\n",        .0000123456);
    printf("%.4g\n",        .00000123456);
}

/*
1.235e+005
1.235e+004
1235
123.5
12.35
1.235
0.1235
0.01235
0.001235
0.0001235
1.235e-005
1.235e-006

The first two numbers have exponents of at least 4, so they're displayed in %e form.
The next eight numbers are displayed in %f form.
The last two numbers have exponents less than -4, so they're displayed in %e form.
*/

minimum field width와 precision에 * 문자를 사용하고 다음 인자로 숫자를 지정할 수 있기 때문에, 다음 문장들은 모두 동일한 출력을 생산한다.

printf("%6.4d", i);

printf("%*.4d", 6, i);

printf("%6.*d", 4, i);

printf("%*.*d", 6, 4, i);


min. field width와 precision을 별도의 인자로 지정할 수 있는 것은 몇가지 이점이 있다. 우선 width나 precision을 매크로를 사용해 지정할 수 있다.

printf("%*d", WIDTH, i);


width와 precision이 되는 인자는 constant가 아니고 프로그램 실행 중에 결정되는 값이어도 된다.

printf("%*d", page_width / num_cols, i);


%n 변환은 ...printf의 호출로 얼마나 많은 문자가 쓰였는지 알아내는 데 사용된다.

printf("%d%n\n", 123, &len);

printf가 %n 이전까지 3개의 문자(123)을 쓰게 되므로, len의 값은 3이 된다.



The ...scanf Functions

int fscanf(FILE * restrict stream,

           const char * restrict format, ...);

int scanf(const char * restrict format, ...);


fscanf와 scanf는 입력 스트림에서 데이터 아이템을 읽으며, 포맷 스트링을 사용해 입력의 레이아웃을 지정한다. 포맷 스트링 이후에는 객체를 가리키는 포인터들이 추가적인 인자로 온다. 입력된 아이템은 변환되어(포맷 스트링의 conversion specification에 따라) 이 객체들에 저장된다.

scanf는 언제나 stdin(표준 입력 스트림)으로부터 읽는 반면, fscanf는 첫번째 인자로 지정된 스트림으로부터 읽는다.

scanf("%d%d", &i, &j);         /* reads from stdin */

fscanf(fp, "%d%d", &i, &j);    /* reads from fp */


scanf 호출은 첫번째 인자가 stdin인 fscanf 호출과 동일하다.

...scanf 함수는 input failure가 발생하거나(더 이상의 입력 문자를 읽을 수 없는 경우) matching failure가 발생하면(입력된 문자가 포맷 스트링과 맞지 않는 경우) 조기에 리턴한다. (C99에서는, input failure는 encoding error - multibyte character를 읽으려는 시도를 했지만 입력 문자에 multibyte character가 없는 경우)로 인해서 발생할 수도 있다) 두 함수는 모두 읽어들이고 객체에 할당한 데이터 아이템의 갯수를 리턴한다.


scanf의 리턴 값을 테스트하는 반복문은 흔하게 쓰인다. 다음 while 문은 정수를 하나씩 읽어들이고, 첫번째로 문제가 발생하는 지점에서 멈춘다.

while (scanf("%d", &i) == 1) {

    ...

}


...scanf Format Strings

...scanf 함수들은 ...printf 함수들과 비슷하지만, 작동 방식은 꽤 다르다. scanf와 fscanf를 "pattern-matching" 함수로 생각하면 도움이 된다. 포맷 스트링은 ...scanf 함수가 입력을 읽어들여서 매치하려고 시도할 패턴을 나타낸다. 만약 입력이 포맷 스트링과 매치되지 않으면, 그 mismatch를 찾자마자 바로 함수는 리턴한다. 그리고 매치되지 않은 문자는 나중에 읽을 함수를 위해 "pushed back"된다.


...scanf의 포맷 스트링은 세가지를 포함할 수 있다.

Conversion specifications. ...printf 함수의 것과 닮아 있다. 대부분의 conversion specification은 입력 아이템의 처음에 오는 white-space 문자를 무시한다(예외는 %[, %c, %n이다). conversion specification들은 뒤따르는 white-space 문자는 절대로 무시하지 않는다. 만약 입력 값의 앞에 공백, 123, 개행 문자가 오면 숫자 앞에 오는 공백은 무시되지만 개행 문자는 읽지 않은 채로 남긴다.

White-space characters. 하나 이상의 연속적인 white-space 문자들은 입력 스트림에 있는 0개 또는 그 이상의 white-space character들과 매치된다.

Non-white-space characters. %를 제외한 white-space가 아닌 문자들은 입력 스트림에서 동일한 문자와 매치된다.


...scanf Conversion Specifications

...printf 함수의 경우보다 약간 더 간단하다. ...scanf 함수의 conversion specification은 % 문자 뒤에 다음 항목을 포함한다(순서대로)


* (optional). * 문자가 있다면 assignment suppression을 나타낸다. 입력 아이템을 읽어들이기는 하지만 객체에는 할당되지 않는다. *을 사용한 매치된 아이템은 리턴 값에 포함되지 않는다.

Maximum field width(optional). 필드 최대 너비는 입력되는 값의 글자 수를 제한한다. 이 숫자에 도달하면 아이템의 변환이 종료된다. 변환의 처음 부분에서 스킵된 white-space 문자는 포함되지 않는다.

Length modifier(optional). length modifier가 있다면 객체의 형식이 특정 conversion specification의 보통 형식보다 길거나 짧은 형식이라는 뜻이다.


Conversion specifier. 다음 표의 문자들 중 하나가 되어야 한다.

수치 데이터 아이템은 항상 부호와 함께 시작할 수 있다. o, u, x, X specifier는 이런 아이템들을 unsigned 형태로 변환하므로(unsigned char를 입력받는 데 -3을 입력하면, 253으로 변환됨), 보통 음수를 읽는 데에는 사용되지 않는다.

 [ specifier는 s specifier의 약간 복잡하고 더 유연한 버전이다. 전체 conversion specification은 %[set] 또는 %[^set]이 된다(set은 문자열로 구성된 임의의 집합). 단, 집합 중 ]가 포함되어 있다면 가장 먼저 와야 한다. %[set]은 집합(the scanset)의 원소들로 이루어진 모든 연속된 문자를 매치한다. %[^set] 은 집합의 여집합의 원소들로 이루어진 모든 연속된 문자를 매치한다. %[abc]는 a, b, c로 이루어진 문자열을 매치하고, %[^abc]는 a, b, c가 포함되지 않은 모든 문자열을 매치한다.

...scanf 함수의 conversion specifier 중 다수는 <stdlib.h> 내의 numeric conversion 함수들과 밀접한 관련이 있다. 이 함수들은 문자열을 동등한 수치 값으로 변환한다. 예를 들어 d specifier는 +또는 - 부호(optional)를 찾고, 연속된 10진수 숫자를 찾는다. 이는 strtol 함수가 문자열을 10진수로 변환하도록 요청받았을 때 하는 것과 정확히 똑같다.


C99 Changes to ...scanf Conversion Specifications

...printf 함수들 만큼 광범위하지는 않다.


Additional length modifiers. hh, ll, j, z, t length modifier가 추가되었다. ...printf conversion specification과 대응된다.

Additional conversion specifiers. F, a, A conversion specifier가 추가되었다. 이는 ...printf와의 대칭성 때문에 제공된다. ...scanf에서는 e, E, f, g, G와 동일하게 간주된다.

Ability to read infinity and Nan. ...printf 함수들이 infinity와 NaN을 쓸 수 있는 것처럼, ...scnaf 함수들은 이 값들을 읽을 수 있다. 제대로 읽기 위해서는 printf 함수들이 쓰는 것과 같은 형태여야 하고, 대/소문자는 무시된다.

Support for wide characters. multibyte character를 읽고 변환해서 저장한다. %lc conversion specification은 하나의 또는 연속된 multibyte character를 읽는다. %ls는 multibyte character의 문자열을 읽고, 마지막에 null character를 추가한다. %l[set], %l[^set] 도 multibyte character의 문자열을 읽는다.



Detecting End-of-File and Error Conditions

void clearerr(FILE *stream);

int feof(FILE *stream);

int ferror(FILE *stream);

...scanf 함수를 사용해서 n개의 데이터 아이템을 읽어오도록 요청했을 때, 함수의 리턴 값은 n이 되어야 정상이다. 리턴이 n이 아니라면 무언가 잘못된 경우이고, 세 가지 가능성이 있다.


End-of-file. 함수가 포맷 스트링을 전부 매치하기 전에 end-of-file에 도달했다.

Read error. 함수가 스트림으로부터 문자를 읽을 수 없었다.

Matching failure. 데이터 아이템이 잘못된 포맷. 예를 들어, 포맷 스트링에서는 정수를 읽도록 지시했는데 문자를 읽은 경우.


모든 스트림에는 두 개의 indicator가 있다. 하나는 error indicator, 하나는 end-of-file indicator. 두 indicator들은 스트림이 열렸을 때 'cleared'된다. end-of-file을 만나면 end-of-file indicator가 set되고, 읽기 에러가 발생했을 때(또는 출력 스트림에서 쓰기 에러가 발생했을 때) error indicator가 set된다. 매칭 실패는 두 indicator를 바꾸지 않는다.


end-of-file indicator가 set되면, 명시적으로 clear되기 전까지 계속 같은 상태에 있다. 명시적 clear는 clearerr 함수 호출을 통해 가능하다. clearerr 호출시 end-of-file, error indicator를 모두 clear한다.

clearerr(fp);    /* clears eof and error indicators for fp */

feof, ferror 함수는 각각 end-of-file indicator와 error indicator가 set되었는지를 알려주는 함수이다. set되었으면 nonzero value를 리턴.

만약 scanf 함수가 예상한 값보다 더 적은 값을 리턴했을 때, feof와 ferror 함수를 호출해서 그 이유를 판별한다.



int file_int(const char *filename)
{
    FILE *fp = fopen(filename, "r");
    int n;

    if (fp == NULL)
        return -1;  /* can't open file */

    while (fscanf(fp, "%d", &n) != 1) {
        if (ferror(fp)) {
            fclose(fp);
            return -2;          /* read error */
        }
        if (feof(fp)) {
            fclose(fp);
            return -3;          /* integer not found */
        }
        fscanf(fp, "%*[\^n]");  /* skips rest of line */
    }

    fclose(fp);
    return n;
}


find_int는 파일에서 정수를 읽는 시도를 한다. 만약 시도가 실패하면(fscanf가 1이 아닌 다른 값을 리턴하면) ferror와 feof를 호출해서 문제가 read error인지 eof 에러인지 찾는다. 둘 다 아닌 경우에는 매칭 에러로 fscanf가 실패한 것이다.

conversion %*[^\n]은 다음 개행문자를 만날 때까지 모든 글자들을 스킵하고, 정수 읽기를 다시 시작한다.

C의 입출력 라이브러리는 표준 헤더 중에서 가장 크고 가장 중요한 부분이다. 그런 위상에 걸맞게 이 헤더를 다루는데 한 챕터 전체를 할애한다(그리고 책에서 가장 긴 챕터다).


개요

21.1 stream의 개념, FILE type, input and output redirection, difference between text files and binary files

22.2 파일과 함께 사용되도록 디자인된 함수들, 파일을 열고 닫는 파일들을 포함해서

22.3 printf, scanf 그리고 관련된 "formatted input/output" 함수들

그리고 unformatted 함수들(22.4, 22.5)

22.4 한번에 문자 한 개를 읽고 쓰는 getc, putc, 관련 함수들

22.5 한번에 한 줄을 읽고 쓰는 gets, puts, 관련 함수들

22.6 데이터의 blocks를 읽고 쓰는 fread, fwrite

22.7 파일에 random access operation을 수행하는 방법

22.8 printf와 scanf의 변종인 sprintf, snprintf, sscanf(문자열을 읽거나 문자열에 쓰는 함수들)


이 챕터에서는 <stdio.h> 헤더 중 8개의 함수를 제외하고 모두 다룬다. 그중 하나인 perror 함수는 <errno.h> 헤더와 밀접한 관련이 있으므로 그와 함께 24.2에서 다룬다. 26.1에서는 나머지 함수들(vfprintf, vprintf, vsprintf, vsnprintf, vfscanf, vscanf, vsscanf)를 다룬다.

C89에서는 모든 표준 입출력 함수가 <stdio.h>에 들어 있었지만, C99에서는 그렇지는 않다. 몇몇 입출력 함수는 <wchar.h> 헤더에 선언되어 있다. <wchar.h>의 함수들은 보통 문자가 아닌 와이드 문자를 다루며, <stdio.h>에 나오는 함수들과 가까운 닮은 함수들이다. <stdio.h>의 데이터를 읽고 쓰는 함수들은 byte input/output functions라고 불리고, <wchar.h>에 있는 유사한 함수들은 wide-character input/output functions라고 불린다.



22.1 Streams


C에서 stream이라는 용어는 입력의 모든 원천 또는 출력의 모든 목적지를 의미한다. 많은 작은 프로그램들에서는 입력을 하나의 스트림(보통 키보드와 관련된 것)으로 받아서 하나의 스트림(보통 화면과 관련된 것)으로 출력한다.

 더 큰 프로그램들은 추가적인 스트림을 필요로 하기도 한다. 이런 스트림들은 다양한 미디어(HDD, CD, DVD, 플래시 메모리 등)에 저장된 파일이기도 하고, 파일을 저장하지 않는 장치들(네트워크 포트, 프린터 등)과 관련된 것일수도 있다. 우리는 흔하고 이해하기 쉬운 파일에 집중한다. stream을 사용할 곳에 file이란 용어를 쓰기도 할 것이다. <stdio.h>에 있는 함수들은 파일 뿐만 아니라 모든 스트림에 대해 잘 작동한다는 것을 유념하고 넘어가자.


File Pointers

스트림에 접근하는 것은 file pointer를 통해 이뤄진다. 그 타입은 FILE *이며, <stdio.h>에 선언되어 있다. 어떤 스트림은 표준 이름을 갖는 파일 포인터로 표현된다. 추가적인 파일 포인터들도 선언할 수 있다. 예를 들어, 프로그램에서 표준 포인터에 추가해 두개의 스트림을 필요로 하면, 다음과 같은 선언을 할 수 있다.

FILE *fp1, *fp2;

프로그램에서는 FILE * 변수들을 제한 없이 만들 수 있지만, 보통 운영체제에서 한번에 열릴 수 있는 스트림의 갯수를 제한한다.


Standard Streams and Redirection

<stdio.h>는 표준 스트림을 제공한다. 이 스트림들은 준비되었고 사용하기만 하면 된다. 우리는 이것들을 선언하지도 않고, 열거나 닫지도 않는다.

지금까지 우리가 사용해온 함수들 - printf, scanf, putchar, getchar, puts, gets - 는 입력을 stdin에서 얻어서 출력을 stdout으로 보낸다. 기본값으로, stdin은 키보드를 나타낸다. stdout과 stderr는 화면을 나타낸다. 하지만 많은 운영 체제에서는 redirection이라는 메커니즘을 통해 이 기본적인 의미를 수정할 수 있게 허용한다.

 통상적으로 프로그램이 입력을 키보드가 아닌 파일에서 받을 수 있도록 프로그램에 강제할 수 있다. command line에 "<파일명"을 입력하면 된다.

demo <in.dat

이 방법은 input redirection이라고 알려져 있으며 stdin 스트림이 키보드가 아닌 파일(이 경우 in.dat)을 나타내도록 본질적으로 만들어 준다. 이 redirection의 멋진 점은 demo 프로그램에서는 자신이 in.dat에서 읽어온다는 점을 모른다는 것이다. 단지 어떤 데이터를 stdin에서 얻어오는 것은 키보드를 통해 온다고 인식한다.

Output redirection도 유사하다. stdout 스트림을 redirect하는 것은 ">파일명"이라고 command line에 입력하면 된다.

demo >out.dat

stdout에 쓰인 모든 데이터는 이제 화면에 나타나는 대신 out.dat 파일로 들어가게 된다. 다음과 같이 input/output redirection을 동시에 사용할 수도 있다.

demo <in.dat >out.dat
// < > 문자는 파일명과 붙어있지 않아도 되고, redirection의 순서가 바뀌어도 괜찮다.

demo < in.dat > out.dat

demo >out.dat <in.dat


output redirection의 한가지 문제는, stdout에 쓰이는 모든 것들이 파일에 들어간다는 것이다. 만약 프로그램에서 오류가 발생해서 에러 메시지를 쓰기 시작한다면, 파일을 들여다보기 전까지는 그것을 보지 못한다. 그래서 stderr가 생겼다. 에러 메시지는 stdout이 아닌 stderr에 따로 씀으로써, stdout이 redirect되었을 때에도 우리는 에러 메시지를 화면을 통해 볼수 있게 된다. (운영체제에서는 가끔 stderr의 redirection도 허용하기도 한다.)


Text Files versus Binary Files

<stdio.h> 는 두가지 종류의 파일을 지원한다. 텍스트와 바이너리이다. text file의 바이트들은 문자를 나타내며, 사람이 파일을 보거나 수정할 수 있게 해 준다. C 프로그램의 소스 코드는 text file로 저장되어 있다.

반면 binary file은 바이트가 꼭 문자로 저장되지 않는다. 바이트들의 묶음은 정수나 부동 소수점 숫자 같이 데이터의 다른 형식을 나타낼 수도 있다. 실행 가능한 C 프로그램은 binary file로 저장된다.


텍스트 파일은 바이너리 파일엔 없는 두 가지 특징을 가지고 있다.

1. 텍스트 파일은 라인으로 구분된다. 텍스트 파일의 각각의 라인은 한개, 또는 두개의 특별한 문자로 끝난다. 그 문자를 선택하는 것은 운영체제가 한다. 윈도우즈에서는, end-of-line 마커가 carriage-return 문자('\x0d')와 바로 뒤에 오는 line-feed 문자('\x0a')이다. UNIX와 신버전의 매킨토시 운영 체제(Mac OS)의 end-of-line 마커는 하나의 line-feed 문자이다. Mac OS의 구버전들은 하나의 carriage-return 문자를 사용한다.

2. 텍스트 파일은 특별한 "end-of-file" 마커를 포함할 수 있다. 어떤 운영체제는 특별한 바이트가 텍스트 파일의 끝을 나타내는 마커로 사용되는 것을 허용한다. 윈도우즈에서 그 마커는 '\x1a' (Ctrl+Z)이다. Ctrl-Z가 존재해야 한다는 요구사항은 없지만, 만약 존재한다면 그것은 파일의 끝을 나타내어야 한다. Ctrl-Z 뒤의 모든 바이트들은 무시된다. UNIX를 포함한 대부분의 다른 운영 체제는 end-of-file을 나타내는 특별한 문자가 존재하지 않는다.


바이너리 파일은 라인으로 구분되지 않는다. 바이너리 파일엔 end-of-line, end-of-file 마커가 없으며 모든 바이트가 동등하게 간주된다.


파일을 저장할 때, 우리는 텍스트 형태로 저장할 지, 바이너리 형태로 저장할 지를 결정해야 한다. 숫자 32767을 파일에 저장한다고 해 보자. 하나는 텍스트 형태로, 문자 '3', '2', '7', '6', '7'을 저장하는 것이다. 문자 집합이 ASCII인 경우, 다음과 같이 5바이트가 된다.

다른 옵션은 바이너리 형태로 저장하는 것이다. 이 때에는 2바이트가 필요하다.

(리틀 엔디안 방식으로 저장되는 운영 체제의 경우는 다음과 같이 바이트의 순서가 뒤바뀐다)

위 사례에서 볼 수 있듯이 바이너리 형태로 숫자들을 저장하는 것은 공간을 꽤 절약해 준다.

 

파일로부터 읽거나 파일에 쓰는 프로그램을 작성할 때, 우리는 그것이 텍스트 파일인지 바이너리 파일인지를 고려해야 한다. 파일의 내용을 화면에 출력하는 프로그램은 보통 그것을 텍스트 파일이라고 가정할 것이다. 반면 파일을 복사하는 프로그램은, 복사될 파일이 텍스트 파일이라고 가정할 수 없다. 만약 그렇게 한다면 end-of-file 문자가 들어있는 binary 파일은 완벽하게 복사되지 않게 된다. 만약 파일이 텍스트 파일인지 바이너리 파일인지 모른다면, 바이너리라고 가정하는 것이 더 안전하다.



22.2 File Operations


input/output redirection의 매력은 단순함이다. 파일을 열고 닫거나 기타 파일 조작에 대한 명시적인 것들을 수행할 필요가 없다. 그러나 이것은 대부분의 프로그램들에게는 너무 제한적이다. 프로그램이 redirection에 의존하면 파일에 대한 조작을 할 수 없다. 심지어 파일명도 알 수 없다. 또한 프로그램이 두개 이상의 파일에서 읽거나 쓰는 경우 도움이 되지 못한다.

 redirection이 충분하지 못한 경우, 우리는 <stdio.h>에서 제공하는 파일 작업을 사용하게 된다. 이 섹션에서는 그러한 작업들, 파일 열기, 파일 닫기, 파일 버퍼 방식 변경, 파일 삭제, 파일 이름 바꾸기 등을 살펴본다.


Opening a File

File *fopen(const char * restrict filename,

            const char * restrict mode);

스트림으로 사용하기 위해 파일을 열기 위해서는 fopen 함수를 호출해야 한다. fopen의 첫번째 인자는 열 파일의 이름이 담긴 문자열이다. ("파일 이름"이란 것은 드라이버 지정자와 경로가 포함된 파일의 위치에 대한 정보를 포함할 수도 있다) 두 번째 인자는 "mode string"으로 파일에 대해 어떤 작업을 수행할 지를 결정한다. 예를 들어 "r"은 데이터를 파일에서 읽어오기는 하지만 파일에 쓰지는 않는다는 것을 나타낸다.

 fopen 함수의 원형에 restrict 키워드가 두번 나타난다. 이것은 filename과 mode는 같은 메모리 영역을 공유하는 문자열을 가리켜서는 안된다는 것을 나타낸다. C89에서의 원형은 restrict 키워드가 제외되어 있다는 점을 빼면 동일하다.


윈도우에서 filename을 쓸 때 \문자는 escape sequence의 시작을 나타낸다는 점을 유의해야 한다. 

fopen("c:\project\test1.dat", "r")

이라고 쓰면 \t가 tab character를 나타내고 프로그램은 이를 인식하지 못한다. \p는 유효한 escape sequence가 아니고 그 의미는 정의되어 있지 않다.

이 문제를 해결하는 방법은 두 가지가 있다. 하나는 \ 대신 \\를 쓰는 방법이다.

fopen("c:\\project\\test1.dat", "r")

다른 방법은 \ 문자가 아니라 /를 쓰는 것이다. 

fopen("c:/project/test1.dat", "r")

윈도우에서는 \ 대신 /를 썼을 때 디렉토리 구분자로 인식할 것이다.


fopen 함수는 변수에 저장해서 원할 때마다 사용할 수 있는 파일 포인터를 리턴한다. 다음은 fopen 호출의 전형적인 예이다. fp의 형식은 FILE * 이다.

fp = open("in.dat", "r");   /* opens in.dat for reading */


나중에 프로그램에서 in.dat로부터 읽는 입력 함수를 호출할 때, fp를 인자로 전달하게 된다.

만약 파일을 열지 못한다면, fopen은 NULL 포인터를 리턴한다. 파일이 존재하지 않거나, 다른 장소에 있거나, 권한이 없는 경우이다. fopen이 파일을 항상 열거라는 가정을 해선 안된다. 언제나 fopen의 리턴 값이 null 포인터가 아닌지를 확인해야 한다.


Modes


fopen에 전달할 mode 문자열은 파일에 수행할 작업 뿐 아니라 파일이 텍스트 파일인지 바이너리 파일인지에 대한 정보도 담고 있다. 


텍스트 파일의 모드 문자열


바이너리 파일의 모드 문자열



fopen으로 바이너리 파일을 열 때는, 글자 b를 추가한다. <stdio.h>는 데이터를 쓰는 것과 데이터를 추가하는 것을 구분한다. 데이터를 파일에 쓸 때는, 보통 기존에 있던 곳에 데이터를 덮어쓴다. appending(추가)을 위해 파일을 열었을 때는, 데이터는 파일의 끝에 더해지고, 기존에 있던 파일 내용이 보존된다.

 읽기와 쓰기 모두를 위해 파일을 열었을 때는(mode 문자열에 + 문자를 넣어서) 특별한 규칙이 적용된다. 읽기 작업에서 파일의 끝에 도달하지 않는 한 먼저 file-positioning 함수를 호출하지 않고서는 읽기에서 쓰기로 전환할 수 없다. 또한, fflush나 file-positioning 함수를 호출하지 않으면 쓰기에서 읽기로 전환할 수 없다.


Closing a File

int fclose(FILE *stream);

fclose 함수는 더 이상 사용하지 않는 파일을 프로그램이 닫을 수 있도록 해 준다. fclose의 인자는 반드시 fopen이나 freopen에서 얻은 파일 포인터여야만 한다. 파일이 성공적으로 닫혔다면, fclose는 0을 반환한다. 그렇지 않은 경우에는 에러 코드 EOF(<stdio.h>에 정의된 매크로)를 반환한다.


Attaching a File to an Open Stream

FILE *freopen(const char * restrict filename,

              const char * restrict mode,

              FILE * restrict stream);

freopen은 열려 있는 스트림에 다른 파일을 붙인다. 가장 흔하게 freopen을 사용하는 것은 표준 스트림(stdin, stdout, stderr) 중 하나와 파일을 연관시키는 것이다. 예를 들어 파일 foo에 프로그램이 쓰도록 하게 하려면, 다음과 같이 freopen을 호출한다.


if (freopen("foo", "w", stdout) == NULL) {

    /* error; foo can't be opend */

}


stdout과 기존에 연관되어 있던 파일을 닫으면(command line redirection이나 freopen을 앞서서 호출한 경우), freopen은 foo를 열어서 stdout과 연관시킨다.

 freopen의 정상적인 반환값은 자신의 세번째 인자(파일 포인터)이다. 만약 새 파일을 열지 못한 경우, null 포인터를 리턴한다. (freopen은 예전 파일이 닫히지 못하는 오류는 무시한다.)

C99에서 새롭게 추가된 것으로, 만약 filename이 null 포인터인 경우, freopen은 현재 스트림의 모드를 지정된 모드 파라미터로 변경하려고 시도한다. 이 기능을 반드시 구현할 것을 요구하지는 않지만, 만약 구현하는 경우에는 특정 모드로의 변경을 막는 제한을 설정할 수 있다.


Obtaining File Names from the Command Line

파일을 여는 프로그램을 작성할 때 어떻게 파일의 이름을 프로그램에 전달해야 할까? 파일 이름 자체를 프로그램 내에 집어넣는 것은 유연성이 떨어지고, 사용자가 파일명을 입력하게 만드는 것은 다소 어색하다. 가장 좋은 해결책은 프로그램이 파일 이름을 command line에서 받아오는 것이다. 예를 들어 demo라는 프로그램을 실행할 때, command line에 파일 명을 같이 입력해서 프로그램에게 전달할 수 있다.

demo names.dat dates.dat

13.7에서 main 함수를 두 개의 매개변수를 갖는 함수로 선언해서 command-line 인자에 접근하는 방법을 다룬 바 있다.


/*
canopen.c
This program determines if a file exists and can be opened for reading.
When the program is run, the user will give it a file name to check:

canopen file

The program will then print either file can be opened or file can't be
opened. If the user enters the wrong number of arguments on the command
line, the program will print the message "usage: canopen filename" to
remind the user that canopen requires a single file name.
We can use redirection to discard the output of canopen and simply test
the status value it returns.
*/

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

int main(int argc, char *argv[])
{
    FILE *fp;

    if (argc != 2) {
        printf("usage: canopen filename\n");
        exit(EXIT_FAILURE);
    }

    if ((fp = fopen(argv[1], "r")) == NULL) {
        printf("%s can't be opened\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    printf("%s can be opened\n", argv[1]);
    fclose(fp);
    return 0;
}

Temporary Files

FILE *tmpfile(void);

char *tmpnam(char *s);


실제 사용되는 프로그램들은 종종 임시 파일 - 프로그램이 실행되는 동안에만 존재하는 파일들 - 을 만들어야 할 때가 있다. 예를 들어 C 컴파일러도 종종 임시 파일들을 생성한다. 컴파일러는 먼저 C 프로그램을 어떤 중간 형태로 번역해서 파일에 저장한다. 컴파일러는 나중에 파일을 읽어서 object code로 번역한다. 프로그램이 완전히 컴파일되면, 중간 형태를 담은 파일은 더 이상 보존될 필요성이 사라진다. <stdio.h>에서는 임시 파일들의 작업을 위해 tmpfile과 tmpnam, 두 가지 함수를 제공한다.


tmpfile은 임시 파일을 생성한다("wb+" 모드로 열린다). 그 파일은 닫히거나 프로그램이 종료될 때까지 존재한다. tmpfile의 호출은 임시 파일에 접근할 수 있는 파일 포인터를 반환한다.


FILE *tempptr;

...

tempptr = tempfile();   /* creates a temporary file */


만약 파일 생성에 실패하면, tmpfile은 null 포인터를 리턴한다.

tmpfile은 사용하기 쉽지만, 두 가지 문제점이 있다. 1) tempfile이 생성하는 파일명을 알 수 없다. 2) 나중에 그 파일을 영구적으로 저장할 수 없다. 만약 이런 제약이 문제가 된다면, 대안은 fopen으로 임시 파일을 생성하는 것이다. 물론 기존에 생성한 이름과 같은 파일명을 갖길 원하지 않기 때문에, 새로운 파일명을 생성할 방법이 필요하다. 그래서 tmpnam 함수가 생겨났다.


tmpnam은 임시 파일을 위한 이름을 생성한다. 만약 인자가 null 포인터인 경우에는, tmpnam은 파일명을 static variable에 저장하고 그에 대한 포인터를 반환한다. 그렇지 않은 경우에는, 전달받은 문자 배열에 파일명을 복사한다.


char *filename;

...

filename = tmpnam(NULL);   /* creates a temporary file name */


char filename[L_tmpnam];

...

tmpnam(filename);   /* creates a temporary file name */


후자의 경우, tmpnam은 배열의 첫번째 문자를 가리키는 포인터도 리턴한다. L_tmpnam은 <stdio.h>에 정의된 매크로로, 임시 파일 이름을 저장할 문자 배열의 길이를 지정한다.

tmpnam의 인자가 최소 L_tmpnam 만큼의 길이를 갖는 배열을 가리켜야 한다. 또, tmpnam을 너무 자주 호출하지 않도록 주의해야 한다. TMP_MAX 매크로(<stdio.h>에 정의)는 프로그램 실행 중에 tmpnam이 만들 수 있는 최대의 임시 파일 이름의 갯수를 지정한다. 만약 파일명 생성에 실패하면, tmpnam은 null 포인터를 반환한다.


File Buffering

int fflush(FILE *stream);

void setbuf(FILE * restrict stream,

            char * restrict buf);

int setvbuf(FILE * restrict stream,

            char * restrict buf,

            int mode, size_t size);


디스크 드라이브로부터/로 데이터를 전송하는 것은 상대적으로 느린 작업이다. 그 결과, 프로그램에서 파일에서 바이트를 읽거나 파일에 바이트를 쓸 때마다 디스크 파일에 직접 접근하는 것은 실현할 수 없다. 납득할만한 퍼포먼스를 해낼 수 있는 비결은 buffering이다. stream에 쓰인 데이터는 사실은 메모리의 버퍼 영역에 저장된다. 버퍼가 꽉 차면(또는 스트림이 닫히면), 버퍼가 "flushed" (실제의 출력 장치에 쓰인다). 입력 스트림도 비슷한 방식으로 버퍼될 수 있다. 버퍼는 입력 장치에서 온 데이터를 담고 있다. 입력은 장치 자체가 아니라 버퍼로부터 읽는다. 버퍼링은 효율성에서 막대한 이득을 가져온다. 버퍼로부터 바이트를 읽거나 버퍼에 바이트를 저장하는 것은 거의 시간이 소요되지 않기 때문이다. 물론 버퍼의 내용을 디스크와 전송하는 데는 시간이 소요되지만, 큰 "block move"는 미미한 여러개의 바이트를 이동시키는 데 비하면 훨씬 시간이 적게 걸린다.

 <stdio.h>의 함수들은 이롭다고 생각될 때 자동적으로 버퍼링을 수행한다. 버퍼링은 보통 보이지 않는 곳에서 이뤄지고 우리가 신경쓰지 않아도 된다. 하지만 가끔씩 적극적으로 버퍼링에 개입해야 할 때가 있다. 그 때 우리는 fflush, setbuf, setvbuf 함수를 사용한다.


프로그램이 파일을 출력에 쓸 때, 데이터는 보통 버퍼에 먼저 들어간다. 버퍼는 꽉 차거나 파일이 닫힐 때 자동적으로 flush된다. 하지만 fflush를 호출하면, 프로그램은 파일의 버퍼를 바로 flush한다.

fflush(fp);   /* flushes buffer for fp */

는 fp와 연결된 파일의 버퍼를 flush한다.

fflush(NULL);   /* flushes all buffers */

는 모든 출력 스트림을 flush한다. fflush는 성공적인 경우 0을, 오류가 발생한 경우 EOF를 리턴한다.

C 표준에 따르면, fflush호출은 다음에 대해 정의되어 있다: 스트림이 (a) 출력에 대해 열려 있는 경우. 또는 (b) 업데이트로 열려 있고(fopen mode에 "+"가 붙은 경우) 마지막 작업이 읽기가 아닌 경우. 그 외 다른 모든 경우 fflush의 호출시 효과는 정의되어 있지 않다. 만약 fflush가 null 포인터를 전달받으면 (a)나 (b)를 만족하는 모든 스트림을 flush한다.


setvbuf는 스트림이 버퍼되는 방식을 변경하거나 버퍼의 크기와 위치를 제어할 수 있게 해 준다. 함수의 세번째 인자는 버퍼의 종류를 지정하며, 다음 세 가지 매크로 중 하나만 지정할 수 있다.

_IOFBF(full buffering). 버퍼가 비어있을 때 스트림에서 데이터를 읽어들이고 버퍼가 꽉 찼을 때 스트림에 데이터를 쓴다.

_IOLBF(line buffering). 한번에 한 줄(line)씩 데이터를 스트림에서 읽거나 스트림에 쓴다.

_IONBF(no buffering). 버퍼를 사용하지 않고 데이터를 스트림에서 바로 읽고 스트림에 바로 쓴다.


세 매크로는 <stdio.h>에 정의되어 있다. interactive 장치가 아닌 경우 full buffering이 기본값이다.

setvbuf의 두번째 인자(null 포인터가 아닌 경우)는 원하는 버퍼의 주소이다. 버퍼는 static storage duration을 갖거나, automatic storage duration을 갖거나, 심지어 동적할당될 수도 있다. 버퍼를 automatic으로 만들면 block이 끝날 때 그 공간은 자동적으로 해제된다. 버퍼를 동적할당하는 경우에는 우리가 직접 버퍼를 해제할 수 있게 된다. 마지막 인자는 버퍼에 있는 바이트의 숫자이다. 큰 버퍼는 더 나은 퍼포먼스를 보일 수 있고, 작은 버퍼는 공간을 절약한다.

 예를 들어, 다음 setvbuf 호출은 stream의 버퍼링을 full buffering으로, buffer 배열을 버퍼로, N 바이트를 사용한다.


char buffer[N];

...

setvbuf(stream, buffer, _IOFBF, N);


setvbuf는 반드시 stream이 오픈된 이후에, 그리고 모든 다른 작업이 그에 대해 수행되기 이전에 호출되어야 한다.

setvbuf의 두번째 인자를 null 포인터로 호출할 수도 있다. 이는 setvbuf가 지정된 크기의 버퍼를 만들 것을 요청한다. 그것이 성공적이었다면 setvbuf는 0을 리턴한다. 만약 mode 인자가 부적합하거나 요청이 실행되지 못했다면 0이 아닌 값을 리턴한다.


setbuf는 더 오래된 함수로 버퍼링 모드와 사이즈에 기본값을 가정한다. buf가 null 포인터인 경우, setbuf(stream, buf) 호출은

(void) setvbuf(stream, NULL, _IONBF, 0);

과 동일하다.

buff가 null 포인터가 아닌 경우, 다음과 동일하다.

(void) setvbuf(stream, buf, _IOFBF, BUFSIZ);

BUFSIZ는 <stdio.h>에 정의된 매크로다. setbuf는 구식으로 간주되며 새로운 프로그램에서 사용하는 것은 추천하지 않는다.


setvbuf나 setbuf를 사용할 때는, 버퍼가 할당 해제되기 전에 스트림을 닫아야 한다. 특히 버퍼가 함수에 local이고 automatic storage duration을 갖는 경우, 함수가 값을 리턴하기 전에 스트림을 닫아야 한다.


Miscellaneous File Operations

int remove(const char *filename);

int rename(const char *old, const char *new);


remove와 rename 함수는 프로그램에서 기본적인 파일 관리 작업을 할 수 있게 해 준다. 이 장의 다른 함수들과 달리, remove와 rename 함수는 파일 포인터가 아닌 파일 이름을 넣어 사용한다. 두 함수 모두 성공하면 0을, 실패하면 0이 아닌 값을 리턴한다.


remove는 파일을 삭제한다.

remove("foo");    /* deletes the file named "foo" */

만약 tmpfile이 아닌 fopen으로 임시 파일을 생성한 경우, 프로그램 종료 전에 remove로 그 임시 파일을 삭제할 수 있다. 파일을 삭제하기 전에 스트림이 닫혀야 한다. 열려 있는 파일을 삭제하는 경우의 효과는 implementation-defined.


rename은 파일의 이름을 바꾼다.

rename("foo", "bar");    /* renames "foo" to "bar" */

rename은 fopen으로 생성한 임시 파일을, 프로그램 종료 전에 영구적인 파일로 보존해야겠다고 결정했을 때 유용하다. 만약 새로운 파일 이름이 이미 존재하는 경우의 효과는 implementation-defined.


만약 이름을 바꾸려는 파일이 열려 있는 경우, rename을 호출하기 전에 그것을 닫아야 한다. 열려 있는 파일의 이름을 바꾸려고 하면 실패할 수도 있다.

이전 챕터들에서는 C 라이브러리를 단편적으로 살펴보았다. 이 챕터에서는 그 라이브러리 전체에 대해 초점을 맞춘다.


21.1 Using the Library


C89 표준 라이브러리는 15개의 파트로 나눠져 있으며, 각각의 파트는 헤더에 의해 설명된다.

C99 표준 라이브러리에는 9개의 헤더가 추가되어 총 24개의 헤더가 있다.

<assert.h> 

<inttypes.h>* 

<signal.h> 

<stdlib.h> 

<comlex.h>*

<iso646.h>* 

<stdarg.h> 

<string.h> 

<ctype.h> 

<limits.h> 

<stdbool.h>* 

<tgmath.h>* 

<errno.h> 

<locale.h> 

<stddef.h> 

<time.h> 

<fenv.h>* 

<math.h> 

<stdint.h>* 

<wchar.h>* 

<float.h> 

<setjmp.h> 

<stdio.h> 

<wctype.h>* 

*C99 only


대부분의 컴파일러는 광범위한 라이브러리가 있고 위 표에 나오지 않는 헤더도 많이 존재한다. 추가적인 헤더는 표준이 아니므로, 다른 컴파일러에서도 사용 가능하다고 생각할 수 없다. 그런 헤더들이 제공하는 함수는 종종 특정 컴퓨터나 운영체제에만 적용된다. 


표준 헤더에는 주로 함수 원형, 타입 정의, 매크로 정의 등이 들어 있다. 어떤 파일에서 특정 헤더 안의 함수를 호출하거나 정의된 타입, 매크로를 사용하려면 파일 첫 부분에서 그 헤더를 include해야 한다. 여러 개의 표준 헤더를 include 할 때 그 순서는 상관이 없다. 또 표준 헤더를 두번 이상 include해도 된다.


Restrictions on Names Used in the Library

표준 헤더를 include한 파일에서는 몇가지 규칙을 따라야 한다. 

첫째, 헤더 안에서 정의된 매크로를 다른 목적으로 사용할 수 없다. 예를 들어 <stdio.h>를 include하면 NULL이라는 이름은 이미 선언되었기 때문에 다른 용도로 쓸 수 없다.

둘째, file scope를 갖는 라이브러리의 name들(특히 typedef names)은 파일 레벨에서 재정의될 수 없다. 따라서 한 파일에서 <stdio.h>를 include하면 size_t를 file scope의 identifier로 정의할 수 없다. <stdio.h>에서 size_t를 typedef 이름으로 정의하기 때문이다.


위 제한은 꽤 당연해 보이지만, 다음과 같은 제약도 존재한다. 다음 규칙들이 항상 강제적인 것은 아니지만, 지키지 않을 경우 프로그램을 포터블하지 못하게 만들 수 있다.


밑줄+대문자로 시작하거나, 밑줄+밑줄로 시작하는 identifier는 라이브러리 내에서만 사용될 목적으로 예약되어 있다. 이런 형태의 이름을 어떤 목적으로도 사용해서는 안 된다.


밑줄로 시작하는 identifer는 file scope를 갖는 identifier들과 tag들로 예약되어 있다. 함수 내에서 사용할 것이 아니라면 이러한 이름을 자신만의 목적으로 사용해선 안 된다.


표준 헤더 내의 external linkage를 갖는 모든 identifier는 external linkage를 갖는 identifier로 예약되어 있다. 특히 표준 라이브러리의 모든 함수 이름은 예약되어 있다. 따라서 한 파일에서 <stdio.h>를 include하지 않는다고 하더라도, printf라는 이름을 갖는 external function을 정의해서는 안 된다.


이 규칙들은 프로그램 내의 모든 파일에, 어떤 헤더를 include했는지에 관계없이 적용된다. 또 현재 라이브러리에 존재하는 이름들 뿐 아니라, 미래에 사용될 수 있게 남겨진 이름에 대해서도 적용된다. 예를 들어 str + 소문자로 시작되는 이름들이 미래에 <string.h> 헤더에 추가될 때를 대비해 예약되어 있다.


Functions Hidden by Macros

표준 라이브러리 내의 어떤 함수 이름들은, 함수로도 정의되어 있지만 동시에 parameterized 매크로로 정의된 경우도 있다.

예를 들어, getchar 함수는 <stdio.h> 헤더 내에 선언된 라이브러리 함수이다. 그 원형은

int getchar(void);


그런데 <stdio.h>는 종종 getchar를 다음과 같은 매크로로도 정의한다.

#define getchar() getc(stdin)


getchar 호출시 기본값으로 매크로 호출로 간주된다(매크로 이름은 preprocessing시 대체되기 때문에).

대부분의 경우 진짜 함수 대신 매크로를 사용하는 것이 속도 측면에서 유리하므로 매크로를 기쁘게 사용하겠지만 가끔은 함수 자체를 원할 때도 있다 (아마 실행 코드의 크기를 최소하하기 위해서).

그럴 필요가 있을 때는 macro 정의를 제거하거나

#include <stdio.h>

#undef getchar


다음과 같이 이름 주위에 괄호를 씌워서 매크로로 인식하지 않게 만든다.

ch = (getchar)();    /* instead of ch = getchar(); */

매크로 이름 뒤에 왼쪽 괄호가 없기 때문에, preprocessor는 이것을 parameterized macro로 인식하지 않는다. 대신 컴파일러는 getchar를 함수로 인식하게 된다.



21.2 C89 Library Overview


<assert.h> Diagnostics (24.1)

assert 매크로만 들어 있다. 이 매크로는 프로그램 내에서 자체적으로 체크할 수 있게 해준다. 만약 체크에 실패하면, 프로그램이 종료된다.


<ctype.h> Character Handling (23.5)

문자들을 분류하고, 소문자를 대문자로 변환하거나 그 역인 함수들을 제공한다.


<errno.h> Errors (24.2)

lvalue인 errno("error number")를 제공한다. 이것은 어떤 라이브러리 함수 호출 이후에 테스트되어 호출 도중에 에러가 발생했는지를 알 수 있게 해 준다.


<float.h> Characteristics(특징) of Floating Types (23.1)

floating 타입들의 특징을 설명하는 매크로를 제공한다. 타입들의 범위, 정확도 같은 것들.


<limits.h> Sizes of Integer Types (23.2)

integer 타입(character 타입들 포함)들의 특징을 설명하는 매크로를 제공한다. 최대, 최소값 같은 것들.


<locale.h> Localization (25.1)

국가나 지리적 영역에 프로그램의 행동이 적응할 수 있게 돕는 함수를 제공한다. 국가/지역 한정적인 것들에는 숫자 표현 방식(소수점에 쓰이는 문자), character set, 시간과 날짜의 표현 방식 같은 것들이 있다.


<math.h> Mathematics (23.3)

수학 함수들을 제공한다. 삼각함수, 하이퍼볼릭, exponential, 로그, power, 반올림, 절대값, 나머지 함수 같은 것들.


<setjmp.h> Nonlocal Jumps (24.4)

setjmp, longjmp 함수를 제공한다. setjmp는 프로그램의 한 지점을 표시한다. longjmp는 나중에 그 지점으로 돌아올 수 있다.


<signal.h> Signal Handling (24.3)

예외적인 상황들(signals) - 중단, 런타임 에러같은 - 을 다루는 함수들을 제공한다. signal 함수는 나중에 signal이 발생하면 호출되는 함수를 설치한다. raise 함수는 signal을 발생시킨다.


<stdarg.h> Variable Arguments (26.1)

가변 인자를 갖는 함수(ex. printf, scanf)를 쓸 수 있게 해 주는 도구를 제공한다.


<stddef.h> Common Definitions (21.4)

자주 사용되는 타입과 매크로의 정의를 제공한다.


<stdio.h> Input/Output (22.1-22.8)

입출력 함수들의 커다란 묶음을 제공한다. 순차 액세스 파일과 무작위 액세스 파일 모두에 대한 조작을 포함한다.


<stdlib.h> General Utilities (26.2)

다른 헤더에 속하지 않는 함수들을 포함하는 "잡동사니"이다. 이 헤더의 함수들에는 문자열을 숫자로 변환, 의사 난수 생성, 메모리 관리 작업, 탐색과 정렬, 멀티바이트 문자와 와이드 문자 간의 변환 같은 것들이 있다.


<string.h> String Handling (23.6)

문자열 조작에 관한 함수들 - 복사, 연결, 비교, 탐색 - 을 제공한다. 뿐만 아니라 임의의 메모리 블록을 조작하는 함수들도 제공한다.


<time.h> Date and Time (26.3)

시간과 날짜를 결정하고, 시간을 조작하고, 시간을 출력하도록 포맷팅하는 함수들을 제공한다.



21.3 C99 Library Changes


C99로 바뀌면서 생긴 커다란 변화 중 몇몇은 표준 라이브러리에 영향을 주었다. 그 변화들은 세가지 그룹으로 나뉜다:


추가된 헤더들: C99 표준 라이브러리는 C89에 없었던 9개의 헤더가 추가되었다. 이 중 세개(<iso646.h>, <wchar.h>, <wctype.h>)는 1995년에 C89가 개정되었을 때 이미 추가되었다. 다른 여섯개(<complex.h>, <fenv.h>, <inttypes.h>, <stdbool.h>, <stdint.h>, <tgmath.h>)는 C99에서 새롭게 추가되었다.


추가된 매크로와 함수들: C99 표준에서는 기존 헤더들에 매크로와 함수가 추가되었다. 주로 <float.h>, <math.h>, <stdio.h>에 추가되었다. <math.h>에 추가된 것들은 아주 광범위해서 23.4에서 따로 다룬다.


강화된 기존 함수: 기존에 있던 몇몇 함수들(printf와 scanf를 포함해서)이 C99에서 추가적인 능력을 갖게 되었다.


<complex.h> Complex Arithmetic (27.4)

complex와 I 매크로를 정의한다. 이들은 복소수를 다룰 때 유용하게 사용된다. 또 복소수 연산을 수행하는 함수들을 제공한다.


<fenv.h> Floating-Point Environment (27.6)

Provides access to floating-point status flags and control modes. For example, a program might test a flag to see if overflow occured during a floating-point operation or set a control mode to specify how rounding should be done.


<inttypes.h> Format Conversion of Integer Types (27.2)

<stdint.h>에 선언된 정수형 타입을 input/output하기 위한 포맷 스트링에 사용되는 매크로가 들어 있다. 또 greatest-width integer를 위한 함수를 제공한다.


<iso646.h> Alternative Spellings (25.3)

몇몇 연산자(&, |, ~, !, ^가 포함된 것들)를 나타내는 매크로를 정의한다. 해당 문자들이 local character set에 포함되지 않은 환경의 프로그램에서 유용하게 사용된다.


<stdbool.h> Boolean Type and Values (21.5)

bool, true, false 매크로를 정의한다. 또 이 매크로들이 정의되었는지를 테스트하는 매크로를 정의한다.


<stdint.h> Integer Types (27.1)

특정 폭을 갖는 정수 타입을 선언하고 관련된 매크로(각각 타입의 최대값과 최소값 같은)를 정의한다. 또 특정 타입의 정수 상수를 만드는 parameterized macro를 정의한다.


<tgmath.h> Type-Generic Math (27.5)

C99에는, 같은 작업을 수행하지만 타입이 다른, 다른 버전의 수학 함수가 <math.h>와 <complex.h> 헤더에 들어 있다. <tgmat.h> 헤더 안의 "type-generic" 매크로는 자신에게 전달된 인자의 타입을 탐지해서 그것을 <math.h> 또는 <complex.h> 헤더에 있는 적합한 함수를 호출한다.


<wchar.h> Extended Multibyte and Wide-Character Utilities (25.5)

와이드 캐릭터 입출력과 와이드 문자열 조작을 위한 함수들을 제공한다.


<wctype.h> Wide-Character Classification and Mapping Utilities (25.6)

<ctype.h>의 와이드 캐릭터 버전이다. 와이드 캐릭터를 분류하는 함수와 대/소문자를 변환하는 함수들을 제공한다.



21.4 The <stddef.h> Header: Common Definitions


<stddef.h> 헤더는 자주 사용되는 타입과 매크로의 정의를 제공한다. 함수의 선언은 없다.


Types

ptrdiff_t. 두 포인터에 대해 뺄셈을 했을 때 결과의 타입이다.

size_t. sizeof 연산자가 리턴하는 타입이다.

wchar_t. 모든 지역에서 사용되는 가능한 문자를 표현할 수 있을 정도로 넓은 타입이다.


세가지 이름은 모두 정수형 타입이다. ptrdiff_t는 signed type, size_t는 unsigned type이어야만 한다. wchar_t는 25.2에서 자세히 다룬다.


Macros

NULL. null pointer

offsetof. parameterized macro로, 인자는 두 개이다. type(구조체 타입)과 member-designator(구조체의 멤버). 


offsetof는 구조체의 시작 지점과 지정한 멤버 사이에 몇 바이트가 있는지 계산한다.

struct s {

    char a;

    int b[2];

    float c;

};

offsetof(struct s, a)는 무조건 0이다. C는 구조체의 첫번째 멤버가 구조체 자체와 같은 주소에 있는 것을 보장하기 때문이다. b와 c의 offset은 어떻게 될 지 확실하게 말할 수 없다. 한가지 가능성은 offsetof(struct s, b)가 1(a가 1바이트이므로)이고, offsetof(struct s, c)는 9(int가 32-bit라는 가정 하에)가 되는 것이다. 하지만 어떤 컴파일러에서는 구조체에 "구멍"을 남기기 때문에, offsetof의 값에 영향을 준다. 예를 들어 컴파일러가 a 뒤에 3바이트의 구멍을 남긴다면, b와 c의 offset은 각각 4와 12가 된다. 이렇게 컴파일러에 따라 offsetof의 값이 달라지는 점이 중요하다. 모든 컴파일러에서 정확한 offset을 계산해 주기 때문에, 포터블한 프로그램을 만들 수 있게 해 준다.


만약 s 구조체의 c 멤버는 제외하고 a, b멤버만 파일에 저장하고 싶다고 하자. fwrite함수로 sizeof(struct s) 바이트를 저장해서 구조체 s 전체를 저장하는 대신에, offsetof(structs, c) 바이트만큼만 저장하면 된다.


<stddef.h>안의 타입과 매크로 몇개는 다른 헤더에도 나타난다. 예를 들어 NULL 매크로는 <locale.h>, <stdio.h>, <stdlib.h>, <string.h>, <time.h>, C99의 <wchar.h> 헤더에도 들어 있다. 그 결과 <stddef.h>를 include 해야 하는 프로그램은 거의 없다.



21.5 The <stdbool.h> Header (C99): Boolean Type and Values


<stdbool.h> 헤더는 네개의 매크로를 정의한다.

bool (defined to be _Bool)

true (defined to be 1)

false (defined to be 0)

__bool_true_false_are_defined (defined to be 1)


bool, true, false 매크로를 사용하는 예시는 이미 많이 보아왔다.

__bool_true_false_are_defined 매크로의 사용 가능성은 제한적이다. 프로그램에서는 자기 자신 버전의 bool, true, false를 정의하려는 시도를 하기 전에 전처리기 지시문(#if나 #ifdef)으로 이 매크로를 테스트할 수도 있다.



Q&A

Q: "standard header file"이라는 용어 대신 "standard header"라는 용어가 사용되고 있는데, "file"이라는 단어를 사용하지 않은 이유는?

A: C 표준에 따르면, "standard header"가 파일이 될 필요는 없다. 대부분의 컴파일러는 표준 헤더를 파일로 저장하기는 하지만, 헤더들은 컴파일러 자체에 빌드되어도 된다.


Q: 14.3에서는 parameterized macro를 함수 대신 사용할 때의 단점을 설명하고 있다. 이 관점에서 표준 라이브러리 함수의 대체로 매크로를 제공하는 것이 위험하지 않은가?

A: C 표준에 따르면, 라이브러리 함수를 대체하는 parameterized macro는 괄호로 "완전히 보호되어야 하고", 그 값을 단 한 번만 측정해야만 한다. 이 규칙들로 인해 14.3에 언급된 대부분의 문제들을 피해갈 수 있다.


C언어에서 공식적으로 2진수 표현 방법을 지원하지 않아서 직접 만들었다.

void print_int_to_bin 함수는 unsigned int를 인자로 받아 그것을 2진수 형태로 변환해서, 8자리마다 한 칸씩 띄워서 출력한다.

#include <stdio.h>

void print_char_to_bin(unsigned char ch)
{
    int a;
    _Bool b;
    for (a = 0; a < 8; a++) {
        b = (ch & 1 << 8 - a - 1)? 1 : 0;
        printf("%d", b);
    }
}

void print_int_to_bin(unsigned int i)
{
    unsigned char int8;
    int len = sizeof(int);
    int a;
    printf("in bin: ");

    for(a = 0; a < len; a++)
    {
        int8 = i >> (len - a - 1) * 8 & 0xFF;
        print_char_to_bin(int8);
        printf(" ");
    }
    printf("\n");
}


int main(void)
{
    print_int_to_bin(0xffffffff);
    return 0;
}


비트 레벨에서 연산이 필요한 프로그램들: 시스템 프로그램(컴파일러와 운영체제), 암호화, 그래픽, 빠른 연산과 효율적인 공간 사용이 필요한 프로그램들

이 장에 소개되는 테크닉들은 메모리에 데이터가 저장되는 방식에 종속적이며 기계와 컴파일러에 매우 종속적이다. 따라서 이러한 방법에 의존하는 것은 프로그램의 portability를 감소시킬 가능성이 높다. 절대적으로 필요한 상황이 아니면 피하는 것이 좋으며, 사용할 때는 꼭 documentation을 해야 한다.


20.1 Bitwise Operators


6개의 bitwise operators가 존재함


Bitwise Shift Operators

integer의 binary representation의 비트들을 왼쪽 또는 오른쪽으로 shift한다.

<< 와 >> 연산의 피연산자는 char를 포함한 모든 정수형 타입이다. integer promotion은 양쪽 피연산자에 대해 모두 이루어지며, 결과의 타입은 promotion 후의 left 연산자 type과 같다.


i << j의 값은 i 내의 bit들이 왼쪽으로 j자리만큼 shift되었을 때의 값이다. i의 왼쪽 끝으로 밀려서 사라지는 숫자만큼 오른쪽에 0 bit가 들어온다. i >> j의 값은 i가 오른쪽으로 j자리만큼 shift된 값이다. 만약 i가 unsigned type이거나 i가 음수가 아닌 수이면, 필요한 만큼 왼쪽 자리에 0이 추가된다. i가 음수인 경우, 결과는 implementation-defined. 어떤 implementation들은 왼쪽 끝에 0을 더하고, 1들을 더해서 sign bit를 보존하는 것들도 있다.


C99 표준 6.5.7 중

If the value of the right operand is negative or is greater than or equal to the width of the promoted left operand, the behavior is undefined.


portalbility를 위해, shift 연산을 unsigned number에만 사용하는 것이 좋다.


unsigned short i, j;


i = 13;       /* i is now 13 (binary 0000000000001101) */

j = i << 2;   /* i is now 52 (binary 0000000000110100) */

j = i >> 2;   /* i is now  3 (binary 0000000000000011) */


두 연산자 모두 피연산자의 값을 수정하지는 않는다. 변수의 값을 수정할 때는 compound assignment operators(<<=, >>=)를 사용한다.


Bitwise Complement, And, Exclusive Or, and Inclusive Or

나머지 4개의 bitwise 연산자들

~연산자는 unary(단항)이다. integer promotion은 그 피연산자에 적용된다.

다른 연산자들은 binary(이항)이다.


~, &, ^, | 연산자들은 피연산자들의 모든 bit에 대해 Boolean 연산을 수행한다.

~: 피연산자의 complement를 생산. 0이 1로, 1이 0으로 대체된다.

&: 두 피연산자의 대응되는 모든 비트에 대해 Boolean and 연산 수행

^: 두 피연산자의 대응되는 모든 비트에 대해 Boolean or 연산 수행 (두 비트가 모두 1이면 0 생산)

|: 두 피연산자의 대응되는 모든 비트에 대해 Boolean or 연산 수행 (두 비트가 모두 1이면 1 생산)


unsigned short i, j, k;


i = 21;      /* i is now    21 (binary 0000000000010101) */

j = 56;      /* j is now    56 (binary 0000000000111000) */

k = ~i;      /* k is now 65514 (binary 1111111111101010) */

k = i & j;   /* k is now    16 (binary 0000000000010000) */

k = i ^ j;   /* k is now    45 (binary 0000000000101101) */

k = i | j;   /* k is now    61 (binary 0000000000111101) */


~i의 값은 unsigned short의 값이 16비트를 차지한다는 가정 하의 값이다.

~연산자는 포터블한 low-level program을 만드는데 도움이 된다. integer에 몇개의 비트가 포함되는지 모르더라도, 모든 비트가 1인 integer를 ~0으로 표현할 수 있다. 비슷하게 마지막 5개의 비트를 제외한 모든 비트가 1인 integer는 ~0x1f로 표현할 수 있다.


4개의 연산자들 간 우선순위는 ~, &, ^, | 순이다.

&, ^, | 에 대한 compound assignment operators: =&, =^, =|


Using the Bitwise Operators to Access Bits

low-level programming을 할 때, 종종 정보를 하나의 비트, 또는 비트의 모음에 저장하게 된다. 예를 들어 그래픽 프로그램에서는 두개 또는 그 이상의 픽셀을 고작 1바이트 공간에 저장하기도 한다. bitwise 연산자들을 사용해서 몇개의 bit들에 저장되어 있는 데이터를 추출하거나 수정할 수 있다.


i가 16비트 unsigned short 변수라고 가정하자. i의 가장 왼쪽(most significant) 비트의 번호를 15로 붙이고 가장 오른쪽(least significant) 비트의 번호를 0으로 붙이자. 

LSB 0 numbering : https://en.wikipedia.org/wiki/Bit_numbering

Setting a bit. i의 4번 비트를 set하려면 i의 값과 상수 0x0010(4번 자리에 1을 담은 "mask")와 비트 or 연산을 수행한다.


i = 0x0000;           /* i is now 0000000000000000 */

i |= 0x0010;          /* i is now 0000000000010000 */


i |= 1 << j;          /* sets bit j */


Clearing bit. i의 4번 비트를 clear하려면 4번 자리가 0, 나머지 모든 비트는 1인 마스크를 사용한다.


i = 0x00ff;           /* i is now 0000000011111111 */

i &= ~0x0010;         /* i is now 0000000011101111 */


i &= ~(1 << j);       /* clears bit j */


Testing a bit. 다음 if문은 i의 4번 비트가 set되었는지 테스트한다.


if (i & 0x0010) ...   /* tests bit 4 */


if (i & 1 << j) ...   /* tests bit j */



더 쉽게 비트들을 다루기 위해서 비트에 이름을 붙일 수 있다. 0, 1, 2번 비트가 각각 파랑색, 녹색, 빨강색에 대응한다고 가정하면,


#define BLUE  1

#define GEEEN 2

#define RED   4


i |= BLUE;         /* sets BLUE bit   */

i &= ~BLUE;        /* clears BLUE bit */

if (i & BLUE) ...  /* tests BLUE bit  */


i |= BLUE | GREEN;           /* sets BLUE and GREEN bits   */

i &= ~(BLUE | GREEN);        /* clears BLUE and GREEN bits */

if (i & (BLUE | GREEN)) ...  /* tests BLUE and GREEN bits  */


Using the Bitwise Operators to Access Bit-Fields

여러 개의 연속된 비트들(a bit-field)을 다루는 방법이다.


Modifying a bit-field. a bit-field를 수정하려면 bitwise and(bit-field를 clear하기 위해)를 사용하고, 그 뒤에 bitwise or(bit-field에 새 비트들을 저장하기 위해)를 사용한다. 다음 문장은 변수 i의 4-6번 비트에 binary 값 101을 저장한다.

i = i & ~0x0070 | 0x0050;      /* stores 101 in bits 4-6 */

& 연산자는 i의 4-6번 비트를 clear한다. | 연산자는 6번, 4번 비트를 set한다. 이것을 조금 더 일반화하기 위해, j는 i의 4-6번째 비트에 담길 값을 저장하고 있다고 가정하자. bitwise or 연산 수행 전에 j를 shift해야 한다.

i = (i & ~0x0070) | (j << 4);      /* stores j in bits 4-6 */

i = i & ~0x0070 | j << 4;          /* | operator has lower precedence than & and << */


Retrieving a bit-field. bit-field가 오른쪽 끝에 있으면, 그 값을 얻는 것은 쉽다. 다음 문장은 변수 i의 0-2번째 비트를 얻는다.

j = i & 0x0007;                /* retrieves bits 0-2 */

bit-field가 i의 오른쪽 끝에 있지 않은 경우, 우선 bit-field가 오른쪽 끝에 자리잡도록 shift한 뒤에 위와 동일한 방법을 쓰면 된다. 다음 문장은 i의 4-6번째 비트를 얻는다.

j = (i >> 4) & 0x0007;         /* retrieves bits 4-6 */


xor.c

/* Performs XOR encrpytion */

#include <ctype.h>
#include <stdio.h>

#define KEY '&'

int main(void)
{
	int orig_char, new_char;
	
	while ((orig_char = getchar()) != EOF) {
		new_char = orig_char ^ KEY;
		if (isprint(orig_char) && isprint(new_char))
			putchar(new_char);
		else
			putchar(orig_char);
		}
		
	return 0;
}


20.2 Bit-Fields in Structures


앞서 살펴본 bit-fields를 다루는 방법들은 까다롭고 헷갈리기 쉽다. 그 대안으로 C에서는 멤버들이 bit-field의 길이를 나타내는 구조체를 선언하는 것을 제공한다.

예를 들어 MS-DOS에서 파일이 생성되거나 수정되는 시점의 날짜를 어떻게 저장하는지 알아보자. days, months, years가 작은 숫자이므로, 보통의 정수형 변수에 이것들을 저장하는 것은 공간을 낭비한다. 그 대신 DOS에서는 날짜를 위해 단 16비트만 할당한다. 날짜를 위해 5비트, 달을 위해 4비트, 연도를 위해 7비트.

bit-field를 이용해, 다음과 같이 위 그림과 같은 구조체를 정의한다.

struct file_date {

    unsigned int day: 5;

    unsigned int month: 4;

    unsigned int year: 7;

};

각각의 멤버 뒤에 있는 숫자는 그 멤버의 비트 길이를 나타낸다. 모든 멤버가 같은 타입을 가지므로, 다음과 같이 선언을 줄일 수 있다.

struct file_date {

    unsigned int day: 5, month: 4, year: 7;

};


bit-field의 타입이 될수 있는 것은 int, unsigned int, signed int 밖에 없다. int를 사용하는 것은 모호하다. 어떤 컴파일러는 필드의 가장 높은 순서의 비트를 sign 비트로 간주하고, 다른 컴파일러는 그렇지 않다. portability를 위해서 모든 bit-field를 unsigned int 또는 signed int로 선언하는 것이 좋다.

(C99) C99에서는 bit-field의 형식이 _Bool이 될 수 있다. C99 컴파일러는 추가로 다른 형식을 허용할 수도 있다.


다음과 같이 bit-field를 구조체의 다른 멤버와 같이 사용할 수 있다.

struct file_date fd;

fd.day = 28;

fd.month = 12;

fd.year = 8;    /* represents 1988 */

다른 평범한 구조체의 멤버와 같이 bit-field를 사용하면 된다.

year 멤버는 1980년을 기준으로 저장된다(마이크로소프트에 따르면 세계가 시작된 해)


bitwise 연산을 사용해도 같은 결과를 얻을 수 있고, bitwise 연산자를 쓰는 것이 약간 더 빠르다. 하지만 보통은 몇 마이크로초를 절약하는 것 보다 읽기 쉬운 프로그램을 작성하는 것이 낫다.

bit-field가 다른 구조체 멤버에 비해 가지고 있는 제한이 하나 있는데 bit-field는 일반적인 의미의 주소를 가지고 있지 않기 때문에, bit-field에 주소 연산자(&)를 사용할 수 없다. 따라서 scanf 같은 함수는 bit-field에 직접 데이터를 저장하지 못한다.

scanf("%d", &fd.day);    /*** WRONG ***/

물론 scanf를 통해 다른 평범한 변수에 값을 저장하고 fd.day에 할당하는 것은 괜찮다.


How Bit-Fields Are Stored

C 표준에서는 bit-field를 어떻게 저장할 지에 대해 컴파일러에게 상당한 자유를 허용한다. 

컴파일러가 bit-field를 다루는 규칙은 "storage units"의 개념에 따라 달라진다. storage unit의 크기는 implementation-defined이며, 통상적인 값은 8 bits, 16 bits, 32 bits이다. 구조체 선언을 처리할 때, 컴파일러는 다음 bit-field를 위한 공간이 없을 때까지 bit-field들을 하나씩 storage unit에 채우며, 각 필드 사이에는 공간을 남기지 않는다. 다음 필드를 위한 공간이 없게 되면, 어떤 컴파일러는 다음 storage unit으로 넘어가고, 또 어떤 컴파일러는 bit-field를 storage unit들 여러개에 걸쳐서 저장한다. (implementation-defined) bit-field가 저장되는 순서(왼쪽->오른쪽 또는 오른쪽->왼쪽)도 implementation-defined이다.

위에서 사용한 예시인 file_date는 storage unit이 16비트라고 가정한다(8비트인 경우도 컴파일러가 month 필드를 두개의 storage unit에 걸쳐 저장한다면 괜찮다). 또 비트 필드가 오른쪽에서부터 왼쪽으로 할당된다고 가정한다. 

bit-field의 이름을 생략해도 된다. 이름 없는 비트 필드는 다른 비트 필드들이 제대로 자리를 잡도록 해주는 역할을 할 수 있다. DOS 파일에서 시간은 다음과 같이 저장된다.

struct file_time {

    unsigned int seconds: 5;

    unsigned int minutes: 6;

    unsigned int hours: 5;

};

(0-59를 저장하는 데 단 5비트만 쓰이는 이유는 DOS에서 초를 2로 나누어서 0부터 29까지의 값으로 저장하기 때문이다.)

만약 seconds field에 관심이 없다면, 그 이름을 비워놓으면 된다.

struct file_time {

    unsigned int : 5;          /* not used */

    unsigned int minutes: 6;

    unsigned int hours: 5;

};

나머지 비트 필드들은 seconds 필드가 여전히 존재하는것과 동일하게 정렬된다.


이름 없는 비트 필드의 길이를 0으로 지정할 수도 있다. 이것은 컴파일러에게 다음 비트 필드를 storage unit이 시작되는 곳에 저장하라고 보내는 신호이다.

struct s {

    unsigned int a: 4;

    unsigned int : 0;     /* 0-length bit-field */

    unsigned int b: 8;


만약 storage unit의 크기가 8비트인 경우, 컴파일러는 a 멤버를 위해 4비트를 할당하고, 다음 storage unit까지 4비트를 넘기고, b 멤버를 위해 8비트를 할당한다. storage unit의 크기가 16비트인 경우, a를 위해 4비트를 할당하고, 12비트를 넘기고, b를 위해 8비트를 할당한다.


20.3 Other Low-Level Techniques


Defining Machine-Dependent Types

char type은 정의에 의해 1바이트를 점유하기 때문에, character 형식을 byte로 다루기도 한다. 문자 형태가 되지 않아도 되는 데이터를 char 형식으로 저장하는 것이다. 이럴 때는 BYTE type을 정의하는 것도 좋다.

typedef unsigned char BYTE;


machine에 따라, 추가적인 타입을 정의하고 싶을 수 있다. x86 아키텍쳐에서는 16비트 words를 광범위하게 사용하므로, 해당 플랫폼에서는 다음 정의가 유용할 것이다.

typedef unsigned short WORD;


이 BYTE와 WORD 타입은 이후의 예제에서 사용된다.


Using Unions to Provide Multiple Views of Data

C에서 공용체는 때때로 원래 의미와는 완전히 다른 목적 - 메모리 블록을 둘 또는 이상의 측면에서 보는 것 - 으로 사용된다.

위에서 썼던 file_date 구조체를 보면, 이 구조체는 2바이트에 해당한다. 따라서 모든 2바이트 값을 file_date 구조체로 볼 수 있다. 특히 unsigned short 값을 file_date 구조체로 볼 수 있을 것이다(short integer의 크기가 16비트라는 가정 하에). 다음 공용체는 short integer를 파일의 날짜로, 또는 그 역으로 변환하기 쉽게 해 준다.

union int_date {

    unsigned short i;

    struct file_date fd;

};

이 공용체를 통해, 2바이트로 저장된 값을 extract해서 month, day, year 필드를 얻을 수 있다. 역으로, 날짜를 file_date 구조체로 만들어서 2바이트로 저장할 수 있다.


아래 함수는 unsigned short 인자를 전달받아 그것을 파일의 날짜 형태로 출력한다.

void print_date(unsigned short n)

{
    union int_date u;


    u.i = n;

    printf("%d/%d/%d\n", u.fd.month, u.fd.day, u.fd.year + 1980);

}


공용체를 이용해 데이터를 여러 측면에서 보는 것은 register(종종 작은 unit으로 나눠짐)를 다룰 때 특히 유용하다.  예를 들어 x86 프로세서는 AX, BX, CX, DX라는 16비트 레지스터를 가지고 있다. 각각의 레지스터는 두개의 8비트 레지스터로 취급 가능하다. 예를 들어 AX는 두개의 레지스터 AH와 AL로 나눠진다(H and L stand for "high" and "low").


AX, BX, CX, DX 레지스터를 표현하는 변수를 만들려면: 16비트, 8비트 레지스터 모두에 접근할 수 있어야 하고, 레지스터들의 관계도 성립해야 한다(AX를 수정하면 AH와 AL 모두에게 영향을 미치고, AH나 AL을 수정하면 AX도 수정됨). 아래의 공용체는 두개의 멤버를 가지고 있다. 한 멤버는 16비트 레지스터에 대응하는 구조체이고, 다른 멤버는 8비트 레지스터에 대응하는 멤버를 갖는 구조체이다.


union {

    struct {

        WORD ax, bx, cx, dx;

    } word;

    struct {

        BYTE al, ah, bl, bh, cl, ch, dl, dh;

    } byte;

} regs;


word 구조체의 멤버는 byte 구조체의 멤버와 겹쳐진다. ax는 al, ah와 같은 메모리를 점유한다. regs 공용체는 다음과 같이 사용될 수 있다.

regs.byte.ah = 0x12;

regs.byte.al = 0x34;

printf("AX: %hx\n", regs.word.ax);   /* AX: 1234 */


byte 구조체에서 AL 레지스터가 "low" half이고 AH 레지스터가 "high" half인데도 al이 ah보다 먼저 온다는 점을 유의하자. 1바이트를 넘는 데이터 아이템이 저장될 때, 그것을 메모리에 저장하는 논리적 방법은 두 가지가 있다. 가장 왼쪽에 있는 바이트에 먼저 저장하는 "자연스러운" 순서대로 하는 방법이 있고, 가장 왼쪽에 있는 바이트에 가장 나중에 저장하는 반대 순서대로 하는 방법이 있다. 전자는 big-endian이라고 불리고, 후자는 little-endian이라고 불린다. 이 순서는 C가 아닌 프로그램을 실행하는 CPU에 의해 결정된다. 그런데 x86 프로세서들은 데이터가 little-endian으로 저장된다고 가정한다. 따라서 regs.word.ax의 첫번째 바이트가 low byte가 된다.


Using Pointers as Address

11.1에서 포인터는 메모리 주소의 어떤 종류이고, 그 자세한 사항을 알 필요는 없다고 했었지만 low-level 프로그래밍을 하게 되면 디테일이 중요하다. 주소는 종종 integer(또는 long integer)와 같은 비트 갯수를 갖고 있다. 특정 주소를 나타내는 포인터를 만드는 것은 쉽다: integer를 포인터로 cast하면 된다. 예를 들어 다음과 같이 주소 1000(hex)를 포인터 변수에 저장할 수 있다.

BYTE *p;

p = (BYTE *) 0x1000;    /* p contains address 0x1000 */


Viewing Memory Locations

이 프로그램은 메모리 세그먼트를 보여준다. 대부분의 CPU는 프로그램을 "protected mode"로 실행하기 때문에 프로그램에서는 해당 프로그램에 속해 있는 부분의 메모리에만 접근할 수 있다. 이것은 한 프로그램이 다른 프로그램에 속하거나, 운영체제에 속한 메모리에 접근하거나 수정하려는 것을 방지한다. 따라서 이 프로그램으로는 프로그램 자체에 할당된 메모리 영역에만 접근이 가능하다. 이 지역을 벗어나면 프로그램이 깨진다.

 viewmemory.c는 우선 자체의 main 함수의 주소와, 한 변수의 주소를 출력한다. 이를 통해 사용자는 어떤 메모리 영역을 탐색할 수 있는지를 짐작할 수 있다.

 여기서는 int 값이 32비트로 저장되고, 주소 값도 32비트로 저장된다고 가정한다. 주소는 관습적으로 16진수 형태로 출력된다.


viewmemory.c

/* Allows the user to view regions of computer memory */

#include <stdio.h>
#include <ctype.h>

typedef unsigned char BYTE;

int main(void)
{
	unsigned int addr;
	int i, n;
	BYTE *ptr;
	
	printf("Address of main function: %x\n", (unsigned int) main);
	printf("Address of addr variable: %x\n", (unsigned int) &addr);
	printf("\nEnter a (hex) address: ");
	scanf("%x", &addr);
	printf("Enter number of bytes to view: ");
	scanf("%d", &n);
	
	printf("\n");
	printf(" Address               Bytes             Characters\n");
	printf(" ------- ------------------------------- ----------\n");
	
	ptr = (BYTE *) addr;
	for (; n > 0; n -= 10) {
		printf("%8X  ", (unsigned int) ptr);
		for (i = 0; i < 10 && i < n; i++)
			printf("%.2X ", *(ptr + i));
		for (; i < 10; i++)
			printf("   ");
		printf(" ");
		for (i = 0; i < 10 && i < n; i++) {
			BYTE ch = *(ptr + i);
			if (!isprint(ch))
				ch = '.';
			printf("%c", ch);
		}
		printf("\n");
		ptr += 10;
	}
	
	return 0;
}

%X conversion specifier는 %x과 유사하며, 16진수 A, B, C, D, E, F를 대문자로 표시한다.

리눅스에서는 main 함수의 앞쪽 영역을 표시했을 때, ELF라는 글자를 볼 수 있다. 나는 해볼수 없어서 생략.

addr 변수의 주소를 입력했을 때에는, 입력한 addr 변수의 값을 그대로 관찰할 수 있다.



addr의 주소인 0022FE38에 들어간 값은 38 FE 22 00이다. 이것을 거꾸로 하면 00 22 FE 38로 입력된 값이 된다. 순서가 거꾸로 된 이유는 앞서 살펴본 대로 x86 프로세서에서 리틀 인디언 방식으로 데이터를 오른쪽 바이트부터 저장하기 때문이다.


The volatile Type Qualifier

어떤 컴퓨터에서는 특정 메모리 영역은 "volatile"하다. 그 영역에 저장된 데이터는 프로그램이 실행되는 중에 바뀔 수 있다. 프로그램 자체적으로 그곳에 새로운 값을 저장하지 않을 때에도 바뀔 수 있다. 예를 들어 어떤 메모리 영역은 입력 장치에서 직접 전달되는 데이터를 저장할 수 있다.

 volitle type qualifer를 사용해서 프로그램에서 사용되는 데이터 중 volatile 데이터가 있는지를 알려줄 수 있다. volatile은 보통 volatile한 메모리 영역을 가리키는 포인터 변수를 선언할 때 나타난다.

volatile BYTE *p;    /* p will point to a volatile byte */

왜 volatile이 필요할까? p를 사용자가 가장 마지막으로 입력한 문자를 담는 메모리를 가리킨다고 가정하자. 이 영역은 volatile하다. 사용자가 키보드에서 문자를 입력할 때마다 그 값이 바뀌기 때문이다. 다음 반복문을 사용해서 키보드로 입력받은 문자들을 얻고 버퍼 배열에 저장할 수 있다.


while (buffer not full) {

    wait for input;

    buffer[i] = *p;

    if (buffer[i++] == '\n')

        break;

}


수준 높은 컴파일러는 이 반복문에서 p와 *p의 값을 변경시키지 않는다는 것을 인식해서, *p을 값을 단 한번만 얻게 해서 프로그램을 최적화하려고 할 수도 있다.


store *p in a register;

while (buffer not full) {
    wait for input;

    buffer[i] = value stored in register;

    if (buffer[i++] == '\n')

        break;

}


이 최적화된 프로그램에서는 우리의 의도와는 달리 버퍼에 동일한 문자만 계속 채워질 것이다. p가 volatile 데이터를 가리킨다고 선언함으로써 컴파일러에게 *p의 값을 그것이 필요할 때 마다 매번 메모리 영역에서 얻어와야 한다는 것을 알려주게 된다.

19.1 Modules


C(또는 다른 언어로 된) 프로그램을 디자인할 때, 프로그램을 독립적인 모듈 여러개로 보는 것은 유용한 시각.

module: '서비스'의 모음으로, 그 중 일부는 프로그램의 다른 부분(the clients)에서 사용 가능함. 각각의 모듈은 interface(사용 가능한 서비스를 설명)를 가지고 있다. 모듈의 자세한 사항 - 서비스의 소스 코드 포함 - 은 모듈의 implementation 내에 저장된다.


C의 맥락에서는..

service: 함수들

interface: clients(source files)에서 사용하게 될 함수들의 prototype이 담긴 헤더 파일

implementation: 모듈에 있는 함수의 정의가 담긴 소스 파일


15.1, 15.2에서 다룬 계산기 프로그램을 예로 들면, (calc.c, stack.h, stack.c를 포함) calc.c는 stack 모듈의 client이고, stack.h는 모듈의 interface, stack.c는 모듈의 implementation

C 라이브러리는 모듈의 집합이다. 각각의 헤더는 모듈의 interface로 기능함. <stdio.h>는 I/O 함수들이 포함된 모듈의 interface, <string.h>는 문자열을 다루는 함수들이 담긴 모듈의 interface


프로그램을 모듈로 분할하는 것의 장점:

Abstraction: 모듈이 적절하게 디자인되었다면, 그것을 abstractions(추상화)로 간주할 수 있다. 우리는 그것이 어떤 기능을 하는지는 알지만, 그 자세한 부분까지 알아야 할 필요가 없다. 추상화 덕분에, 한 부분을 바꾸기 위해서 프로그램의 모든 전체 부분을 이해하지는 않아도 된다. 추상화는 팀이 한 프로그램을 작업할 때도 도움이 된다. 모듈의 interface들에 대한 합의가 있다면, 각각의 모듈의 implement를 만드는 책임은 특정 개인에게 위임된다. 그렇기 때문에 팀의 멤버들은 서로에게서 독립되어 일할 수 있다.


Reusability: 서비스를 제공하는 모든 모듈은 잠재적으로 다른 프로그램에서 재사용될 수 있다. 예를 들어 stack 모듈도 재사용할 수 있다. 모듈을 처음 만들 때 재사용성을 염두에 두면 좋다.


Maintainability: 작은 버그는 보통 한개의 module implmentation에만 영향을 미치므로, 버그를 찾아서 고치기 쉬워진다. 버그가 고쳐진 후에 프로그램을 rebuild하는 것은 해당되는 모듈 implementation에만 국한된다. (그리고 모든 프로그램을 link해야 함) 더 큰 관점에서는 프로그램을 다른 플랫폼으로 이식하거나 퍼포먼스를 향상시키기 위해 한 모듈 implementation 전체를 바꿀 수도 있다.


셋 다 중요하지만 그 중 가장 중요한 것은 maintainability. 현실의 대부분의 프로그램들은 몇년동안 서비스되는데 도중에 버그가 발견되고, 발전이 이뤄지고, 변화되는 요구에 맞춰 수정이 이루어진다. 프로그램을 모듈화된 방법으로 만들면 유지가 용이해진다.


Cohesion and Coupling

좋은 모듈 인터페이스는 선언들을 아무렇게나 모아놓은 것이 아니다. 잘 디자인된 프로그램에서 모듈은 두가지 특성을 가져야 한다.

High cohesion(화합, 결합, 응집력): 각각의 모듈의 원소들은 서로가 밀접하게 연관되어 있어야 한다; 공통의 목표를 향해 협력하는 것들로 생각해야 한다. 높은 응집력은 모듈 사용과 프로그램에 대한 이해를 쉽게 만들어 준다.

Low coupling(연결, 결합): 모듈은 서로 다른 모듈과 가능한 한 독립적이어야 한다. 낮은 연결성?은 프로그램 수정과 모듈 재사용을 쉽게 만들어 준다.


Types of Modules

high cohesion, low coupling의 필요성으로 인해 모듈은 전형적인 카테고리로 분류되는 경향이 있다.


data pool: 관련된 변수나 상수들의 모음이다. C에서 이 타입의 모듈은 보통 하나의 헤더 파일이다. 디자인의 관점에서 헤더 파일에 변수를 넣는 것은 보통 좋은 생각이 아니지만, 연관되어 있는 상수들을 하나의 헤더 파일에 모아 넣는 것은 유용할 때가 많다. C 라이브러리에서 <float.h>와 <limits.h>가 data pool이다.

library: 연관된 함수의 모음이다. <string.h> 헤더는 문자열을 다루는 함수들의 라이브러리의 인터페이스이다.

abstract object: 숨겨진 데이터 구조를 다루는 함수들의 모음이다. (이 챕터에서 쓰이는 'object' 는 책의 다른 부분에서 쓰인 'object'와 의미가 다르다. C 용어에서 object는 단순히 어떤 값을 저장하는 메모리 블록을 말한다. 하지만 이 챕터에서 쓰이는 object는 데이터의 모음, 그리고 데이터에 대한 작업을 묶은 말이다. 만약 데이터가 숨겨져 있다면, 그 object는 "abstract"하다.) stack 모듈은 이 카테고리에 속한다.

abstract data type(ADT): 표현이 숨겨진 형식이다. 클라이언트 모듈은 이 형식의 변수를 선언할 수는 있지만 그 변수가 어떻게 정의되어 있는지 알지 못한다. 클라이언트 모듈이 해당 변수에 대한 작업을 수행하려면 ADT에서 제공되는 함수를 호출해야 한다.



19.2 Information Hiding


잘 디자인된 모듈에서는 종종 몇몇 정보를 클라이언트에게는 비밀로 한다. 예를 들어 stack 모듈에서 클라이언트는 스택이 배열에 저장되는지, 연결리스트에 저장되는지, 혹은 다른 형태로 저장되는지 알 필요가 없다. 모듈이 클라이언트로부터 의도적으로 정보를 숨기는 것을 information hiding이라고 한다. 이것의 두가지 장점은:


Security: 클라이언트가 스택이 어떻게 저장되는지 모른다면, 클라이언트는 스택의 내부 작업에 쓸데없이 참견해서 망치는 일이 일어나지 않는다. 스택에 대한 작업을 수행하려면, 클라이언트들은 모듈 내부의 함수 -이미 검증된 것들- 를 통해서만 할 수 있다.

Flexibility: 모듈의 내부 작업 내용을 변경(그것이 크든 작든)하는 것이 어렵지 않다. 예를 들어, 스택을 처음에는 배열로 만들었다가, 연결리스트나 다른 형태로 바꿀 수 있다. 당연히 모듈의 implementation은 재작성해야 하겠지만, 모듈이 제대로 디자인 되었다면 모듈의 인터페이스는 변경하지 않아도 된다.


C에서 imformation hiding을 강화하는 주된 방법은 static storage class를 이용하는 것이다. file scope를 갖는 변수를 static으로 선언하면 internal linkage를 가지므로 모듈의 클라이언트들을 포함해 다른 파일들에서 접근할 수 없다. static으로 함수를 선언하면 같은 파일에 있는 함수들에서만 그 함수를 호출할 수 있다.



19.3 Abstract Data Types


위에서 살펴본 스택 module과 같은 abstract object 모듈의 가장 큰 단점은 해당 object에 대한 복수의 개체를 만들 수 없다는 것이다. 이걸 하기 위해서는 한걸음 더 나아가서 새로운 type을 만들어야 한다.

 Stack type을 정의하게 되면 원하는 만큼의 stack을 만드는 것이 가능하다. 

Stack s1, s2;

make_empty(&s1);
make_empty(&s2);
push(&s1, 1);
push(&s2, 2);
if (!is_empty(&s1))
    prtinf("%d\n", pop(&s1));   /* prints "1" */


이 프로그램 조각은 한 프로그램 내에서 서로 다른 두개의 스택을 사용하는 것을 나타낸다. 우리는 s1과 s2가 정확히 어떤 타입인지(배열, 구조체?) 모르지만, 상관이 없다. 클라이언트에게 s1과 s2는 특정한 operations(make_empty, is_empty, is_full, push, and pop)에 응답하는 abstractions이다.


stack.h에서 Stack type을 제공하도록 변경하기

#define STACK_SIZE 100

typedef struct {
    int contents[STACK_SIZE];
    int top;
} Stack;

void make_empty(Stack *s);
bool is_empty(const Stack *s);
bool is_full(const Stack *s);
void push(Stack *s, int i);
int pop(Stack *s);

make_empty, push, pop 함수의 스택 파라미터는 포인터가 되어야 하는데, 해당 함수들은 스택을 변경하기 때문이다. is_empty와 is_full 함수의 파라미터는 포인터일 필요는 없지만 포인터로 했다. Stack 대신 Stack 포인터를 함수에 패스하는 것이 더 효율적이다. Stack을 직접 전달하는 것은 구조체의 복사를 수반하기 때문이다.


Encapsulation

Stack은 abstract data type은 아니다. stack.h에 Stack type의 정체가 드러나 있기 때문이다. 클라이언트에서 Stack 변수를 구조체로 사용하는 것을 막을 수 없다.

Stack s1; s1.top = 0; s1.contents[top++] = 1;

클라이언트에서 top과 contents에 접근할 수 있기 때문에, stack을 망치는 것을 방지할 수 없다. 그리고 클라이언트에서 스택에 접근할 수 없게 하면서 위와 같은 방식으로 저장하는 것을 동시에 할 수 없다. 새로운 C 기반의 언어들(C++, Java, C#)에서는 encapsulation을 더 잘 지원하지만, C에서는 제한된 방식으로만 type을 encapsulate할 수 있다.


Incomplete Types

C에서 encapsulation을 위해 제공하는 유일한 도구는 incomplete type이다. C 표준에서는 incomplete type을 다음과 같이 설명한다: types that describe objects but lack information needed to determine their sizes.

struct t;   /* incomplete declration of t */

위 선언은 컴파일러에게 t가 structure tag라는 것은 알려주지만, 구조체의 멤버에 대해선 알려주지 않는다. 그 결과 컴파일러에겐 이 구조체에 대한 크기를 결정할 충분한 정보가 없다. 그 의도는 프로그램의 다른 어떤 곳에서 그 incomplete type이 완성되는 것이다.

 type이 incomplete한 동안, 그 사용은 제한되어 있다. 컴파일러가 incomplete type의 크기를 알 수 없으므로, 변수를 선언할 수도 없다. 하지만 incomplete type에 대한 포인터를 정의하는 것은 legal하다.

struct t s;   /*** WRONG ***/

typedef struct t *T;   /*** LEGAL ***/

이 타입 정의는 type T의 변수가 tag t를 가진 구조체의 포인터라는 것을 나타낸다. 이제 우리는 type T를 가진 변수를 선언해서, 그것을 함수의 인자로 사용하고, 다른 포인터 관련 연산을 수행할 수 있다. (포인터의 크기는 그것이 가리키는 것과는 무관하다. 때문에 C도 이런 것을 허용한다) 다만 컴파일러는 t 구조체의 멤버에 대해서는 아는 것이 없으므로 -> 연산은 수행할 수 없다.


19.4 A Stack Abstract Data Type


19.5 Design Issues for Abstract Data Types


19.4에선 스택 ADT를 소개하고 그것을 구현하는 몇가지 방법을 다뤘다. 하지만 이 ADT는 실제로 적용되기에는 몇가지 문제점을 지니고 있다. 그 문제점들과 해결책을 알아본다.


Naming Conventions

스택 ADT의 함수들은 짧고 이해하기 쉬운 이름을 가지고 있다. create, destroy, make_empty, is_empty, is_full, push, pop 등. 만약 우리가 하나 이상의 ADT를 프로그램에 가지고 있다면 서로 다른 ADT에 있는 서로 다른 함수들의 이름이 충돌할 가능성이 매우 높다. 따라서 함수 이름에 ADT의 이름을 포함시킨다. ex) create 대신 stack_create


Error Handling

스택 ADT에서 에러에 대처한 방법은 에러 메시지를 띄우고 프로그램을 종료시키는 것이었다. 하지만 프로그램을 종료시키지 않고 에러로부터 프로그램을 복구할 수 있는 방법을 마련하고 싶을 수 있다. 

 대안은 push와 pop 함수가 성공했는지 실패했는지 여부를 bool 값으로 리턴하게 하는 것이다. push는 기존 형식이 void였으므로 push 작업이 성공했을 때 true를 리턴하게 하는 것은 쉽다. pop 함수의 경우 기존 형식이 int이고 pop된 값을 리턴하도록 되어 있었지만, pop이 값의 포인터를 리턴하도록 하고, 실패했을 경우 NULL 포인터를 리턴하게 하면 된다.

 C 표준 라이브러리에서는 assert라는 parameterized macro를 제공해 특정 조건이 만족되지 않았을 경우 프로그램을 종료하도록 할 수 있다. 스택 ADT 내에 있는 if문들과 terminate 호출을 이 매크로로 대체할 수 있다.


Generic ADTs

스택에 저장되는 아이템의 형식을 쉽게 바꿀 수 있게 되었다(typedef으로 정의한 Item의 형식만 바꾸면 된다). stack.h를 수정하지 않고 stack이 모든 형식을 받아들이면 더 좋을 것이다. 또한 스택 ADT에 심각한 결점이 있는데 프로그램에서 스택 여러개를 만들더라도 각각 다른 형식의 아이템을 받아들이지는 못한다는 것이다. 

 하나의 "gerenic" 스택 타입을 만들어서 integer의 스택, 문자열의 스택 등등을 만들 수 있다. 가장 흔한 방법은 item type으로 void *를 사용해서 임의의 포인터를 push하고 pop하는 것이다. 이때 stackADT.h에서 push와 pop 함수는 다음과 같은 원형을 갖는다.

void push(Stack s, void *p);

void *pop(Stack s);

pop은 스택에서 pop된 아이템의 포인터를 리턴하고, 스택이 비어있는 경우엔 NULL 포인터를 리턴한다.


하지만 void *를 아이템의 형식으로 사용할 때 두가지 단점이 있다. 첫째로 이 방법은 포인터 형태로 나타낼 수 없는 데이터에 사용할 수 없다는 것이다. 아이템이 문자열(문자열의 첫 글자를 가리키는 포인터로 표현), 동적할당된 구조체가 될 수는 있지만 int나 double같은 basic type이 될 수 없다.

두번째로 더 이상 에러를 체크할 수 없다. void * 아이템을 저장하는 스택은 서로 다른 형식의 포인터들을 아무 문제 없이 받아들이므로, 잘못된 형식을 가리키는 포인터를 push해서 생기는 에러를 탐지할 수 없다.


ADTs in Newer Languages

위에서 다른 문제점들은 새로운 C 기반의 언어들, C++, Java, C# 등에서는 더 명확히 다루어진다. 이름의 충돌 문제는 class 내에서 함수를 정의함으로써 방지된다. stack ADT는 Stack class로 표현된다; stack 함수들은 이 클래스 내에 소속되며 Stack object에 적용된 경우에만 컴파일러에서 인식한다. 또 이런 언어들은 exception handling 기능을 적용해서 push나 pop같은 함수에서 에러가 발생한 경우 예외를 "throw"할 수 있다. client 내에 있는 코드에서는 이런 예외를 "catch"해서 에러에 대처할 수 있다. generic ADT의 경우 C++를 예로 들면 stack template를 정의해서 아이템 타입을 미지정할 수 있다.


변수를 선언함으로써, 우리는 컴파일러가 잠재적인 오류가 있는지 프로그램을 검사하고 object code로 변환하는 데 필요한 중요한 정보를 전달한다. 이전 챕터들까지는 자세하게 알아보지 않고 선언을 사용했는데, 이번 장에서는 듬성듬성한 개념을 채워본다. 선언에서 사용될 수 있는 세련된 옵션들에 대해 알아보고, 변수의 선언이 함수의 선언과 꽤나 공통점이 많다는 것도 알아본다. 또 storage duration, scope, linkage에 대해 더 잘 이해할 수 있는 기반도 마련한다.


18.1 지금까지 미뤄왔던, syntax of declarations in their most general form


그 이후의 4개 장은 선언에 등장하는 item들에 초점을 맞춘다.

18.2 storage classes

18.3 type qualifiers

18.4 declarators

18.5 initializers


18.6 inline keyword(C99)



18.1 Declaration Syntax


선언은 컴파일러에게 identifier의 의미에 대한 정보를 전달한다.

int i; 

라고 쓰면, 우리는 컴파일러에게 현재의 scope에서 i라는 이름은 integer 형식의 변수를 나타낸다는 정보를 전달한다.

float f(float); 

는 컴파일러에게 f는 float 형식의 값을 반환하는 함수이며 float 형식의 인자 하나를 갖는다는 것을 전달한다.

일반적으로, 선언은 다음과 같은 형태를 갖는다.


declarations-specifiers declarators ;


Declaration specifiers는 선언되는 변수나 함수의 특징을 설명한다. Declarators는 그것들의 이름을 나타내고 그들의 특징에 대해 추가적인 정보를 제공할 수도 있다.


Declaration specifier는 세가지 카테고리로 나뉜다.


Storage classes: 4개의 storage classes가 존재한다: auto, static, extern, register. 선언 하나에는 최대 한개의 storage class가 올 수 있다. 만약 존재한다면, 가장 처음에 와야 한다.


Type qualifiers: C89에서는 두 개의 type qualifer만 존재한다: const, volatile. C99에선 세번째 qualifier가 등장했다: restrict. 선언에는 type qualifier가 하나도 없거나 여러개 존재할 수 있다.


Type specifiers: void, char, short, int, long, float, double, signed, 그리고 unsigned는 모두 type specifier에 속한다. 이 단어들은 Chapter 7에서 설명된 대로 결합될 수 있다; 결합 순서의 영향이 없다(int unsigned long은 long unsigned int와 동일함). type specifier는 structure, union, enumerations도 포함한다. typedef으로 생성된 type name도 type specifier에 속한다.


C99에서는 declaration specifier의 4번째 종류가 있는데, function specifier이며 함수 선언시에만 사용된다. 이 카테고리의 멤버는 inline keyword 뿐이다. 


Declaration specifier 구분

storage class 

auto, static, extern, register 

type qualifier

const, volatile, restrict(C99) 

type specifier

void, char, short, int, long, float, double, signed, unsigned, struct, union, enum, type name created by typedef 

function specifier(C99)

inline(only in funcion declraration) 


type qualifier와 type specifier는 storage class 뒤에 와야 하고, 둘 간 순서는 제한이 없다. 이 책에서는 type qualifier를 type specifier 앞에 둔다.


Declarator에는 identifiers(names of simple variables), indentifiers followed by [](배열 이름), identifiers preceded by *(포인터 이름), 그리고 identifiers followed by () (함수 이름)이 있다. declarator는 콤마에 따라 구분된다. 변수를 나타내는 declarator 뒤에는 initializer가 올 수 있다.



18.2 Storage Classes


Storage class는 변수 및 더 작은 범위의 함수와 매개 변수에 대해 지정할 수 있다. 지금은 변수에 대해 집중한다.

 10.3에서 다룬 block이란 용어는 함수의 body(중괄호로 묶인)이나 compound statement를 의미하며 그 안에는 선언이 포함되어 있을 수 있다. C99에서는 조건문(if, switch), 반복문(while, do, for)과 그 안에 있는 statements도 block으로 취급된다. storage class의 종류에는 다음 네가지가 있다: auto, static, extern, register. 이 storage class들은 아래 단락에 나오는 변수들의 속성을 지정한다.


Properties of Variables

C 프로그램 내의 모든 변수는 세 가지 속성을 가지고 있다.


1. storage duration. 변수를 위한 메모리가 설정되고 해제되는 시점

automatic storage duration: 변수의 storage는 변수를 감싼 block이 실행되었을 때 할당되고, block이 종료될 때 해제되며 변수가 그 값을 잃는다. 

static storage duration: 변수가 프로그램이 실행되는 동안 계속 같은 storage location에 머무르며, 값을 무기한으로 저장한다.


2. scope. program text 중에서 변수가 참조될 수 있는 범위

block scope: 그 변수의 선언 시점부터 변수가 속한 block이 종료되는 지점까지 visible

file scope: 변수가 그 변수의 선언 시점부터 파일이 끝나는 곳까지 visible


3. linkage. 프로그램의 다른 부분들과 공유될 수 있는 범위

external linkage: 변수가 프로그램 내의 여러 개의 파일(아마도 전부)에서 공유될 수 있다. 

internal linkage: 변수가 하나의 파일로 제한되어 있지만, 그 파일 안에 있는 함수들 사이에서는 공유될 수 있다. (만약 같은 이름을 가진 변수가 다른 파일에서 등장하면, 둘은 다른 변수로 간주된다.) 

no linkage: 변수가 하나의 함수 내에만 속하며 전혀 공유될 수 없다.


세가지 속성의 기본값은 변수의 선언 위치에 따라 다르다.

block 안에서(함수의 body 포함) 선언:automatic storage duration, block scope, no linkage

아무 block에도 속하지 않은 곳(outermost level of a program)에서 선언: static storage duration, file scope, external linkage.


int i;         // static storage duration, file scope, external linkage 

void f(void) {

    int j;     // automatic storage duration, block scope, no linkage

}

많은 변수들은 storage duration, scope, linkage의 기본값이 사용자가 원하는 값과 일치한다. 만약 그렇지 않은 경우, storage class를 명시적으로 지정한다.


The auto Storage Class

블록 안에서 변수를 선언할 때만 legal.

automatic storage duration, block scope, no linkage

auto storage class는 명시적으로 지정되는 일이 거의 없는데, block 내에서 선언된 변수의 기본값이 이것이기 때문이다.


The static Storage Class

static storage class는 모든 변수에 대해, 선언되는 장소에 관계없이 사용할 수 있다. 

효과는 변수가 block의 바깥에서 선언되는 경우와 block 내부에서 선언된 경우에 대해 다르다. 

block 바깥에서 사용: internal linkage

block 안에서 사용: static storage duration


static int i;     // static storage duration, file scope, internal linkage

void f(void) {

   static int j;  // static storage duration, block scope, no linkage

}


information hiding: block 바깥의 선언에서 사용되었을 때, static은 변수를 그것이 선언된 파일 안으로 숨긴다; 같은 파일 내에 있는 함수들만 그 변수를 볼 수가 있다. 함수 내부에서 변수 중 하나를 static으로 선언하는 것은 함수 호출 사이에서 변수의 정보를 프로그램의 다른 부분에서는 접근하지 못하는 "숨겨진" 영역에 저장하도록 해 준다. (19.2)


static 변수의 흥미로운 특징들:

- block 안의 static 변수는 프로그램이 실행되기에 앞서서, 단 한번만 초기화된다. auto 변수는 그것이 존재하게 될 때마다 매번 초기화된다(initializer가 있다는 가정 하에).

- 함수가 재귀적으로 호출될 때 마다, 함수는 새로운 auto 변수들의 집합을 얻는다. 반면 static 변수를 가지고 있다면, 그 변수는 함수의 모든 호출 동안 공유된다.

- 함수가 auto 변수에 대한 포인터를 반환해서는 안되지만 static 변수에 대한 포인터를 반환하는 것은 아무 문제가 없다.


static은 프로그램을 효율적으로 만들기 위해 사용한다.


char digit_to_hex_char(int digit)

{

    const char hex_chars[16] = "0123456789ABCDEF";

    return hex_chars[digit];

}

매번 digit_to_hex_char 함수가 호출될 때마다, "012...DEF" 문자들이 hex_chars 배열로 복사되며 초기화된다. 저 변수를 static으로 만들면,

char digit_to_hex_char(int digit)

{

    static const char hex_chars[16] = "0123456789ABCDEF";

    return hex_chars[digit];

}

static 변수는 단 한번만 초기화되므로, digit_to_hex_char의 속도를 개선할 수 있게 된다.


The extern Storage Class

여러개의 소스 파일이 변수에서 공유할 수 있게 한다. 15.2에서 이에 대한 내용을 이미 다룬바 있어서 여기서는 간단히 설명.

extern int i;

라는 선언은 컴파일러에게 i가 int 변수이지만, i에 대한 메모리를 할당하지는 않도록 알린다. C 용어로는, 이 선언은 i의 정의(definition)이 아니다; 단지 콤파일러에게 우리가 다른 어딘가(같은 파일의 뒤쪽, 또는 다른 파일- 이게 더 흔함 -)에 정의되언 변수에 접근하겠다고 알려주는 역할을 한다. 

변수는 프로그램 내에서 여러번 선언declaration될 수는 있지만, 정의definition은 단 한번만 될 수 있다.

예외: extern을 사용한 변수 선언이 initializer와 함께 쓰이는 경우. 예를 들어

extern int i = 0;은 int i = 0; 과 동일한 효과이며 변수의 정의로 취급된다. (여러개의 extern declaration에서 제각각 다른 값으로 변수를 초기화하는 것을 방지하는 규칙)


extern으로 선언된 변수는..

storage duration: 항상 static storage duration

scope of variable: block 안에서 선언되면 block scope, 그렇지 않은 경우는 file scope

linkage: 만약 변수가 파일 내 앞선 위치에서 static으로, 모든 함수 정의 바깥에서 이미 선언되어 있는 경우, 변수는 internal linkage

           그 이외 경우(일반적인 경우), 변수는 external linkage이다.

extern int i;     // static storage duration, file scope, ? linkage

void f(void) {

    extern int j; // static storage duration, block scope, ? linkage

}


The register Storage Class

register storage class를 사용해서 변수 선언시 변수를 다른 변수와 같이 메인 메모리에 저장하는 것이 아니라 register에 저장하도록 컴파일러에게 "요청"한다. 

(register: CPU 내에 있는 저장 공간. register에 저장되어 있는 데이터는 보통 메모리에 저장된 변수들보다 더 빠르게 접근되고 업데이트될 수 있다.) 

변수의 storage calss를 register로 지정하는 것은 요청이며, 명령이 아니다. 컴파일러는 자유롭게 register 변수를 메모리에 저장할 수도 있다.


변수가 block 내에서 선언된 경우에만 legal. 

register 변수는 auto 변수와 storage duration, scope, linkage가 동일함. 

그러나 register 변수는 auto 변수에 있는 중요한 특징을 가지고 있지 않다: register에는 주소가 없기 때문에, register 변수에 & 연산자를 사용하는 것은 illegal. 이 제한은 컴파일러가 변수를 register가 아닌 메모리에 저장했을 때에도 유효하다.

 register는 고빈도로 접근/업데이트 되는 변수를 위해 최선으로 사용된다. 하지만 register는 C 프로그래머 사이에서 예전만큼 대중적으로 사용되지 않는다. 오늘날의 컴파일러는 옛날 컴파일러에 비교하면 훨씬 정교해졌다; 많은 컴파일러들은 자동적으로 어떤 변수를 register에 저장했을 때 가장 이익이 될지를 구분할 수 있다. 그래도 여전히 register를 사용하는 것은 컴파일러가 프로그램의 퍼포먼스를 최적화하는 데 도움을 주는 유용한 정보가 된다. 특히, 컴파일러는 register 변수는 그 주소를 얻을 수 없고, 포인터로 값이 수정될 수 없다는 것을 안다. 이 관점에서는 register 키워드는 C99의 restrict 키워드와 연관되어 있다.


The Storage Class of a Function

함수의 선언(정의)는 변수의 선언과 마찬가지로 storage class를 포함할 수 있다. 하지만 extern과 static 두 종류만 사용할 수 있다.

extern : 함수가 external linkage.

static : 함수가 internal linkage. 함수의 이름 사용이 선언된 파일 내로 제한됨.

storage class를 지정하지 않으면, 기본값으로 external linkage


함수를 static으로 선언한다고 하더라도 다른 파일에서 호출되는 것을 완전히 막지는 못한다; 함수 포인터를 통한 간접적인 호출은 여전히 가능하다.

 함수를 extern으로 선언하는 것은 변수를 auto로 선언하는 것과 비슷하게, 어떤 효과가 없고, 사용했을 때 해가 되지도 않는다.


다른 파일에서 사용되지 않을 모든 함수 선언 앞에 static을 붙이는 것이 추천됨. 함수를 static으로 선언하는 것의 장점:


더 쉬운 유지보수. 함수 f를 static으로 선언하면 f는 선언이 있는 다른 파일에서 보이지 않는다는 것이 보장된다. 그 결과, 나중에 프로그램을 수정하는 사람이 f의 수정이 다른 파일에 영향을 주지 않는다는 것을 알 수 있다. (예외: 다른 파일에 들어있는 f의 포인터를 전달받는 함수는 f의 변경에 영향을 받을 수 있다. 다행히 그런 상황은 f가 정의되어 있는 파일을 조사하면 쉽게 찾을 수 있다. f를 전달하는 함수도 그곳에 정의되어 있어야 하기 때문)


"name space pollution"이 덜함. 

"name space pollution": 다른 파일에 있는 이름들이 의도치 않게서로 충돌하는 상황

static으로 선언된 함수들이 internal linkage이므로, 그 이름들이 다른 파일에서 재사용될 수 있다.

규모가 큰 프로그램에선 함수의 이름을 다른 목적으로 재사용하는 것을 피하기 어렵고, external linkage를 갖는 이름이 너무 많으면 "name space pollution"을 초래할 수 있다. static은 그러한 상황을 방지하는 데 도움이 된다.


함수의 파라미터는 automatic storage duration, block scope, no linkage(same as auto variables)

파라미터에 지정될 수 있는 storage class는 register밖에 없다.


Summary


int a;

extern int b;

static int c;


void f(int d, register int e)

{
    auto int g;

    int h;

    static int i;

    extern int j;

    register int k;

}


Name 

Storage Duration 

Scope 

Linkage 

static 

file

external

static 

file 

static 

file 

internal 

automatic

block 

none 

automatic 

block 

none 

automatic 

block 

none 

automatic 

block 

none 

static 

block 

none 

static 

block 

automatic 

block 

none 


?: b와 j의 정의가 나와있지 않기 때문에 두 변수의 linkage를 알 수 없다. 대부분의 경우 변수가 다른 파일에 정의되어 있으며, extern 변수는 external linkage를 갖는다.


4개의 storage class들 중 가장 중요한 것은 static, extern이다. auto는 아무런 효과가 없으며, 최신 컴파일러에서는 register가 덜 중요하다.


18.3 Type Qualifiers


type qualifier의 종류: const, volatile(used for low-level programming), restrict(C99, used only with pointers)

volatile은 20.3에서 다루고 여기선 미룸.

const는 변수와 유사하지만 "읽기 전용"인 object를 선언함.

const로 선언된 object는 constant가 와야 할 표현식에 사용될 수 없다.

const int n = 10;

int a[n];  /*** WRONG ***/

C99에서는, a가 automatic storage duration인 경우에만 VLA로 간주되고 위 표현이 legal.

숫자나 문자에 대한 상수들은 #define을 이용해 매크로를 사용하는 것을 추천함.


18.4 Declarators


declator에는 identifer(선언될 변수나 함수의 이름)이 포함되며 그 앞에 * 문자가 오거나 뒤에 [ ] ( )이 붙는다.


declarator 앞의 * 심볼은 포인터를 나타낸다. declarator 뒤의 대괄호[ ]는 배열을, 괄호( ) 는 함수를 선언함을 나타낸다.

int abs();

void swap();

함수 선언시 괄호를 비어 있게 선언할 수도 있지만, 함수 호출시 알맞은 인자가 전달되었는지 컴파일러가 체크할 수 없게 하기 때문에 거의 쓰이지 않게 된 스타일이다.


Deciphering Complex Declarations

int *(*x[10])(void);

같은 복잡한 선언을 어떻게 해독해야 할까?

두개의 규칙을 통해 복잡한 선언을 해석할 수 있다.


1. declarator는 항상 안에서 바깥으로 읽어라. 우선 선언되는 identifer를 찾고 그 identifer부터 해독을 시작한다.

2. [], ()가 *보다 언제나 우선된다. identifier 앞에 *이 있고 뒤에 []가 있다면, identifer는 배열이고, 포인터가 아니다. identifier 앞에 *이 있고 뒤에 ()가 있다면, 함수이고 포인터가 아니다. (물론 괄호 사용시 모든 연산자에 우선한다)


허용되지 않는 것들

함수는 배열을 리턴할 수 없다.

int f(int)[];   /*** WRONG ***/

함수는 함수를 리턴할 수 없다.

int g(int)(int);   /*** WRONG ***/

함수의 배열은 불가능하다.

int a[10](int);   /*** WRONG ***/


각각의 경우, 포인터를 사용해 원하는 효과를 얻을 수 있다. 함수는 배열의 포인터를 리턴할 수 있고, 함수가 함수의 포인터를 리턴할 수 있고, 배열은 함수의 포인터들이 원소가 될 수 있다.


Using Type Definitions to Simplify Declarations

복잡한 선언을 단순화하기 위한 방법. 위에서 쓴 예시를 다시 살펴보자

int *(*x[10])(void);

//x의 형식을 더 쉽게 이해하기 위해 다음과 같은 type 정의를 사용한다.

typedef int *Fcn(void);

typedef Fcn *Fcn_ptr;

typedef Fcn_ptr Fcn_ptr_array[10];

Fcn_ptr_array x;

이걸 거꾸로 읽으면, x는 Fcn_ptr_array 형식, Fcn_ptr_array는 Fcn_ptr을 원소로 갖는 배열, Fcn_ptr은 Fcn의 포인터, Fcn은 인자가 없고 int값을 리턴하는 함수의 포인터이다.



18.5 Initializers


변수를 선언함과 동시에 변수의 시작 값을 지정할 수 있다. declarator 뒤에 = 기호를 써서 변수를 initialize하며, 이것은 assignment operator와는 다른 것이므로 주의해야 한다; initialization ≠ assignment


simple variable의 경우: 형식이 일치하지 않을 때에는 = 뒤의 expression이 conversion during assignment에 의해 변환된다.

pointer variable: initializer의 형식이 변수와 같은 포인터 형식이거나 void* 이어야 함.


static storage duration 변수의 initializer는 반드시 constant여야 한다.

automatic storage duration 변수의 initializer는 constant가 아니어도 된다.

배열, 구조체, 공용체 등의 괄호로 감싸진 initializer들은 constant expression만 가능하고, 변수나 함수의 호출이 올 수 없다.

automatic sturcture, union의 initializer는 다른 구조체나 공용체가 될 수 있다.


Uninitialized Variables

초기화되지 않은 변수의 값은 항상 undefined일까? 그렇지 않다. 변수의 storage duration에 따라 다르다.

automatic storage duration: initial value의 기본값이 없고 예측할 수 없다. 변수가 존재하게 될 때마다 달라질 수 있다.

static storage duration: 기본값으로 0이 된다. calloc이 모든 bit를 0으로 설정하는것과 달리, 변수의 형식에 맞는 0이 된다. 정수형은 0, float는 0.0, 포인터는 null pointer


변수가 static storage duration이더라도, 위 성질을 이용하지 않는 것이 코드를 보는 사람을 위해서 더 좋다.



18.6 Inline Functions(C99)


C99에서 등장한 새로운 키워드 inline은 기존의 범주와는 다른 새로운 종류의 declaration specifier이다.

함수 호출 전에 기계 단계에서 이뤄지는 instruction들이 몇가지 필요하다.

"overhead": 함수 자체적으로 수행하는 일 이외에 함수 호출시와 리턴받을 때 발생하는 누적되는 작업

함수 호출시 함수 내의 첫번째 지시로 점프한다. 함수를 실행함에 따라 함수 내의 추가적인 지시들이 실행된다. 함수에 인자가 있는 경우, 그 인자들이 복사되어야 한다. 함수로부터 값을 리턴할 때도 호출한 함수와 호출된 함수에서 비슷한 노력이 소요된다. 

overhead가 프로그램을 느리게 하는 정도는 미미하지만, 함수가 수백만번 호출될 때, 오래되고 느린 프로세서를 사용할 때(또는 embedded 시스템 사용할 때), 시간상 아주 엄격한 데드라인이 적용될 때에는 그것을 무시할 수 없다.


C89 -> parameterized macro를 이용해서 해결

C99 -> 더 좋은 솔루션인 inline function을 제공함. "inline" 이라는 단어는 컴파일러가 각각의 함수 호출을 machine instruction으로 대체한다는 것을 의미함.

이렇게 해서 함수 호출에 대한 overhead를 피할 수 있음. 대신 컴파일된 프로그램의 크기는 약간 늘어난다.


함수를 inline으로 선언해도 컴파일러가 그 함수를 "inline"하도록 강제하는 것은 아님. 컴파일러가 함수의 호출을 최대한 빨리 수행하도로고 suggest할 뿐임. 컴파일러가 이 suggestion을 받아들일지 무시할지 결정하는 것은 자유임. 이 관점에서 inlinie은 register, restrict 키워드와 유사하다. 컴파일러는 프로그램의 퍼포먼스를 개선하려고 시도할 수도 있고, 무시할 수도 있다.


Inline Definitions

inline 키워드를 declaration specifier로 사용해서 정의한다.

inline double average(double a, double b)

{

    return (a + b) / 2;

}

average는 external linkage이지만, average의 정의는 컴파일러에서 external definition으로 취급 안한다(대신 inline definition임).

따라서 다른 파일에서 average를 호출하려고 하면 에러이다.


위 문제의 해결 방법

1. 함수 정의시 static keyword 추가

static inline double average(double a, double b) {...}

이제 average가 internal linkage를 가지므로, 다른 파일에서 호출될 수 없다.

2. average에 대한 external definition을 제공해서 다른 파일에서도 호출될 수 있게 하기.

average의 inline 정의를 헤더 파일(예를 들어 average.h)에 집어넣는다.

#ifndef AVERAGE_H

#define AVERAGE_H

inline double average(double a, double b) {...}

#endif

그리고 대응하는 소스 파일(average.c)에는 다음과 같이 추가

#include "average.h"

extern double average(double a, double b);


이제 다른 파일에서 average 함수를 사용하고 싶은 경우 함수의 inline definition이 들어있는 average.h를 include하기만 하면 된다.

함수 프로토타입 역할: average.h에서 included된 average의 정의는 external definition이 된다.


Restrictions on Inline Functions

일반적인 함수들과 비교해서 inline 함수는 구현되는 방식이 다르기 때문에, 적용되는 규칙과 제한도 다르다. 

inline function with external linkage의 제한: 

함수는 수정 가능한 static variable을 정의할 수 없다.

함수는 internal linkage를 갖는 변수의 참조를 포함할 수 없다.





/**** stack.h ****/
#include <stdbool.h>

void make_empty(void);       // 스택 내의 모든 노드를 삭제
bool is_empty(void)  ;       // 스택이 비어있는지 여부를 반환함
void push(int i);            // 정수 i를 스택의 맨 위에 push
int pop(void);               // 스택의 맨 위에 있는 정수를 pop


/**** main.c ****/
#include <stdio.h>
#include "stack.h"

int read_number(void);
int main(void) {
	int i;
	printf("연결 리스트로 스택 짜기\n"); 
	do {
		i = read_number();
		push(i);
	} while (i!=0);
	for(;;)
		printf("%d ", pop());
}

int read_number(void) {
	int i;
	printf("숫자 입력(0: 종료): ");
	scanf("%d", &i);
	return i;
}


/**** stack.c ****/
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>


struct node {
	int value;
	struct node *next;
};
/* external variables */
struct node *top = NULL;

void make_empty(void) {
	struct node *prev = NULL;
	while (p!= NULL) {
		prev = top;
		top = top->next;
		free(prev);
	}
}

bool is_empty(void) {
	return (top == NULL);
}

void stack_underflow(void) {
	printf("stack is empty.\n");
	exit(EXIT_FAILURE);
}

void stack_overflow(void) {
	printf("cannot allocate memory for new member in stack.\n");
	exit(EXIT_FAILURE);
}

bool push(int i) {
	struct node *new_top;
	new_top = malloc(sizeof(struct node));
	if (new_top == NULL)
		return false;
	new_top->value = i;
	new_top->next = top;
	top = new_top;
	return true;
}

int pop(void) {
	struct node *temp;
	int pop_value;
	if (is_empty())
		stack_underflow();
	temp = top;
	top = top->next;
	pop_value = temp->value;
	free(temp);
	return pop_value;
}


Chapter 17 - Advanced Uses of Pointers - 2/3에서 계속됨.


Q&A


Q: NULL 매크로는 무엇을 나타내는가?

A: NULL은 0을 나타낸다. 만약 문맥상 포인터가 올 곳에 0을 넣으면, 컴파일러는 그것을 정수 0이 아닌 포인터로 간주한다. NULL 매크로는 단지 혼란 방지를 위해 제공된다.

p=NULL; 은 p가 포인터라는 것을 확실하게 나타내 준다.


Q: 0이 널 포인터를 나타내기 위해 사용된다면, 널 포인터는 모든 비트값이 0인 주소인가?

A: 꼭 그렇지는 않다. 컴파일러마다 다른 방식으로 널 포인터를 표현할 수 있고, 모든 컴파일러가 0 주소를 사용하지는 않는다. 어떤 컴파일러는 존재하지 않는 메모리 주소를 널 포인터에 사용한다. 그래서 널 포인터를 통해 메모리에게 접근하려고 하면 하드웨어에 의해 감지된다.

 널 포인터가 컴퓨터에 어떻게 저장되는지는 우리의 걱정거리는 아니다; 컴파일러가 걱정해야 할 디테일이다. 중요한 것은 포인터의 문맥에서 0이 사용되면 컴파일러가 적절한 내부 형태로 알아서 변환해 준다는 것이다.


Q: NULL을 null 문자로 사용해도 괜찮은가?

A: 절대 그렇지 않다. NULL은 널 포인터를 나타내는 매크로지, null character를 나타내지 않는다. 어떤 컴파일러에서는 NULL을 null character에서 사용해도 괜찮지만, 전부다 괜찮지는 않다(어떤 컴파일러는 NULL을 (void *) 0으로 정의하기 때문에). 어떤 경우에도 NULL을 포인터가 아닌 다른 것으로 사용하는 것은 굉장한 혼란을 불러일으킨다. 만약 null character에 이름을 붙이고 싶다면

#define NUL '\0'


Q: malloc이나 다른 메모리 할당 함수의 리턴값에 캐스팅하는 것의 장점이 있는가?

A: 보통 그렇지 않다. 이 함수들이 리턴하는 void * 포인터에 캐스팅하는 것은 불필요하다. 왜냐하면 void * 형식의 포인터는 assign되는 포인터의 형식으로 자동 변환되기 때문이다. 리턴 값에 캐스팅하는 습관은 옛 버전의 C에서 내려온 잔재이다(메모리 할당 함수가 char * 값을 리턴했기 때문에 형변환을 해야만 했었다). C++ 코드로 컴파일할 목적으로 디자인된 코드에서는 형변환이 이득이 될 수 있지만, 그것이 유일한 이유이다.


Q: calloc 함수는 메모리 블록의 모든 비트를 0으로 초기화한다. 이것은 블록의 모든 데이터 아이템이 0이 된다는 것을 의미하는가?

A: 보통은 그렇지만 항상 그렇지는 않다. 모든 비트를 0으로 설정하면 정수는 0이 된다. 모든 비트를 0으로 설정했을 때 부동 소수점 형식의 수는 보통 0이 되지만, 항상 그렇다는 보장은 없다 - 부동소수점 숫자가 저장되는 방식에 따라 다르다. 포인터에 대해서도 마찬가지로, 모든 비트가 0인 포인터가 반드시 null 포인터가 되지는 않는다.


Q: malloc을 잘못된 인수로 호출해서 너무 많은 메모리나 너무 적은 메모리를 할당하는 일이 꽤 흔한 실수 같다. 더 안전하게 malloc을 사용할 방법이 있는가?

A: 그렇다. 어떤 프로그래머들은 malloc으로 하나의 object를 위한 메모리를 할당할 때 다음과 같은 관용구를 사용한다:

p = malloc(sizeof(*p));

sizeof(*p)는 p가 가리키는 object의 크기이므로, 이 문장은 할당될 정확한 분량의 메모리를 할당하는 것을 보증한다. 언뜻 보기에 이 관용구는 좀 이상해 보인다. p가 초기화되어있지 않다면 *p의 값은 정의되어 있지 않기 때문이다. 그러나 sizeof 연산자는 *p의 값을 측정하는 것이 아니라 단지 크기만을 계산하기 때문에, 이 관용구는 p가 초기화되어있지 않거나 null 포인터를 담고 있더라도 작동한다. 만약 n개의 원소를 가진 배열의 메모리를 할당하려면, 다음과 같이 수정해서 사용한다.

p = malloc(n * sizeof(*p));


위 idiom을 사용하는 예제를 만들어 보았다.

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

int main(void) {
	int i, n;
	printf("문자열 변수의 크기를 입력하세요: "); 
	scanf("%d", &n);
	char *str = malloc(n * sizeof(*str));
	printf("문자 %d개를 입력하세요: ", n - 1);
	getchar();
	for (i = 0; i < n; i++) 
		scanf("%c", str + i);
	*(str + n - 1) = '\0';
	
	printf("입력받은 문자열: %s\n", str);
	printf("str에 저장된 문자들 (아스키 코드): "); 
	
	for(i=0; i<n; i++) 
		printf("%d ", *(str+i));	
	return 0;
}

Q: qsort는 왜 sort가 아니고 qsort인가? 

A: qsort라는 이름은 C.A.R. Hoare의 Quicksort algorithm(1962년 출판)에서 따온 것이다. 역설적이게도, 많은 컴파일러들이 qsort에서 퀵소트 알고리즘을 사용하지만 C 표준은 qsort가 꼭 퀵소트 알고리즘을 쓰도록 강제하지는 않는다.


Q: qsort를 호출할 때 첫번째 인자를 void* 형으로 형변환 시켜야 하는가?

A: 그렇지 않다. 모든 형식의 포인터는 자동으로 void* 형식으로 변환된다.


Q: 문자열의 배열을 정렬할 때, qsort의 비교 함수로 strcmp를 사용하고 싶다면?

int compare_strings(const void *p, const void *q)

{

    return strcmp(p, q);

}

이렇게 넣었을 때 컴파일은 되지만 정렬이 되지 않는다.

A: strcmp 자체를 qsort에 전달할 수는 없다. qsort에 들어가야 할 비교 함수는 두개의 cost void * 파라미터를 가져야 하기 때문이다. 이 compare_strings 함수는 작동하지 않는다. 왜냐하면 p와 q가 문자열(char * 포인터)이라고 잘못 가정하기 때문이다. 사실 p와 q는 char * 포인터를 담은 배열의 원소를 가리키는 포인터이다. compare_strings를 고치려면, p와 q를 char**로 형변환하고, * 연산자로 하나의 참조 단계를 제거해야만 한다.

int compare_strings(const void *p, const void *q)

{

    return strcmp(*(char **)p, *(char **)q);

}




Chapter 17 - Advanced Uses of Pointers - 1/3 에서부터 계속됨


Deleting a Node from a Linked List

링크드 리스트를 사용해 데이터를 저장하는 것의 큰 장점은 필요하지 않은 노드를 쉽게 제거할 수 있다는 것이다. 노드를 만드는 것과 같이 노드를 삭제하는 것은 세 가지 단계를 거친다.

1. Locate the node to be deleted.

2. Alter the previous node so that it "bypasses" the deleted node.

3. Call free to reclaim the space occupied by the deleted node.


1단계는 보기보다 어렵다. 만약 리스트 내에서 검색을 한다면, 삭제될 노드의 포인터를 얻을 것이다. 그러나 이렇게 하면 2단계를 수행할 수 없다. 바로 전 단계의 노드를 수정해야 하기 때문이다.

 이 문제의 해결법은 여러가지가 있다. 여기선 "trailing pointer" 방법을 이용한다: 1번 단계를 통해 노드를 검색해 나가면서, 바로 전 단계 노드의 포인터(prev)도 현재 노드의 포인터(cur)와 함께 저장하는 것이다. 만약 변수 list가 검색될 리스트를 가리키고, n이 삭제될 정수라면, 1번 단계를 다음과 같이 구현한다:

for (cur = list, prev = NULL;

     cur != NULL && cur->value != n;

     prev = cur, cur = cur->next)

   ;


2단계:  이전 노드가 삭제되는 노드를 지나쳐 가도록 함.

prev->next = cur->next;

이전 노드 안에 있는 포인터가 현재 노드 바로 다음 노드를 가리키도록 한다.


3단계: 삭제되는 노드가 차지한 메모리를 해제.

free(cur);


다음 함수 delete_from_list는 위에서 설명한 전략을 사용한다. 리스트와 정수 n이 주어졌을 때, 이 함수는 n이 담겨 있는 첫번째 노드를 삭제한다. 만약 n이 포함된 노드가 없으면, 아무것도 하지 않는다. 두 경우 모두 리스트를 가리키는 포인터를 반환한다.


struct node *delete_from_list(struct node *list, int n)

{

    struct node *cur, *prev;

    for (cur = list, prev = NULL;

         cur != NULL && cur->value != n;

         prev = cur, cur = cur->next)

    ;

    if (cur == NULL)

        return list;              /* n was not found */

    if (prev == NULL)

        list = list->next;        /* n is in the first node */

    else

        prev->next = cur->next;   /* n is in some other node */

    free(cur);

    return list;

}


리스트의 첫번째 노드를 삭제하는 것은 특별한 경우이고 2단계의 적용 코드가 다르다. prev==NULL 테스트를 통해 첫번째 노드를 삭제해야 하는지 확인한다.


Ordered Lists

리스트의 노드들이 노드 내에 저장된 데이터에 의해 정렬된 경우 리스트가 정렬되었다고 한다(the list is ordered). 정렬된 리스트에 노드를 삽입하는 것은 더 어렵다(새 노드가 항상 리스트의 처음에 놓이지 않는다), 하지만 검색은 더 빠르다(원하는 데이터가 있을법한 지점까지만 검색하고, 더 이상 찾지 않아도 된다). 아래 프로그램은 더 어려운 삽입, 더 빠른 검색에 대해 보여준다.


16.3에서 만들었던 부품관리 프로그램을 데이터를 링크드 리스트에 저장하는 버전으로 다시 만들어 보자. 배열이 아닌 링크드 리스트를 사용하는 것은 두가지 중요한 장점이 있다.

1. 데이터베이스의 크기 제한을 미리 걸 필요가 없다. 부품을 저장할 메모리 공간이 없을 때까지 계속 데이터베이스가 커질 수 있다.

2. 데이터베이스를 쉽게 부품 번호에 의해 정렬해서 유지할 수 있다 - 데이터베이스에 새로운 부품을 저장할 때, 쉽게 적절한 공간에 삽입할 수 있다. 원래 프로그램에서는 데이터베이스가 정렬되지 않았었다.

 새 프로그램에서는 part 구조체가 새로운 멤버(링크드 리스트의 다음 노드를 가리키는 포인터)를 포함한다. 또 inventory 변수는 배열이 아니라 리스트의 첫번째 노드를 가리키는 포인터가 된다.


struct part {

    int number;

    char name[NAME_LEN+1];

    int on_hand;

    struct part *next;

};

struct part *inventory = NULL;    /* points to first part */


새 프로그램의 대부분의 함수는 원래 프로그램에 대응되는 함수와 아주 닮았다. 하지만 find_part, insert 함수는 더 복잡해진다. inventory의 노드를 저장하고 부품 번호대로 정렬해야 하기 때문이다.

 원래 프로그램에서 find_part는 inventory 배열의 index를 반환했다. 새 프로그램에서는, find_part는 원하는 부품 번호가 담긴 노드를 가리키는 포인터를 반환한다. 만약 부품 번호를 찾지 못했을 경우, find_part는 null pointer를 리턴한다. inveotry 리스트가 부품 번호순으로 정렬되어 있으므로, 새로운 버전의 find_part는 찾는 부품 번호보다 큰 번호를 만났을 때 검색을 중단해서 시간을 절약할 수 있다. find_part의 검색 loop는 다음과 같은 형태이다:

for (p = inventory;

     p != NULL && number > p->number;

     p = p->next)

   ;

루프는 p가 NULL이거나(부품 번호를 찾지 못했다는 뜻) number > p->number가 거짓일 때(찾고 있는 부품 번호가 저장된 노드에 있는 번호보다 작거나 같을 때) 종료된다. 후자의 경우, 원하는 번호를 찾았는지 알 수 없기 때문에 if문이 하나 더 필요하다.

if (p != NULL && number == p->number)

    return p;


원래 버전의 insert 함수는 새로운 부품을 배열 중 사용가능한 첫번째 원소에 저장했다. 새로운 버전에서는 새 부품이 리스트의 어디에 삽입되어야 하는지를 판단해서 그 자리에 넣어야 한다. 또한 새 부품 번호가 이미 리스트에 들어있는지도 확인한다. find_part에서 사용한 루프와 비슷한 형태를 사용해서 두가지 작업을 모두 해낼 수 있다.

for (cur = inventory, prev = NULL;

     cur != NULL && new_code->number > cur->number;

     prev = cur, cur = cur->next)

   ;


이 루프는 두 개의 포인터에 의존한다: cur - 현재 노드를 가리킴, prev - 바로 전 노드를 가리킴. 루프가 종료되었을 때, insert는 cur이 NULL이 아니고 new_node ->number와 cur->number가 같은지 테스트한다; 만약 그렇다면, 부품 번호는 이미 리스트에 있다. 그렇지 않은 경우 insert는 prev, cur 사이에 새로운 노드를 삽입한다. 노드를 삭제할 때 썼던 것과 비슷한 방법으로 삽입한다. (이 전략은 부품 번호가 리스트의 어떤 숫자보다 클 때도 적용된다; 그때는 cur이 NULL이 되지만 purev는 리스트의 마지막 노드를 가리킨다.)


(전체 코드는 생략)


17.6 Pointers to Pointers


13.7에서 포인터에 대한 포인터 기호를 사용했었다. 원소의 형식이 char *인 배열을 사용했었고, 그 배열의 원소를 가리키는 포인터의 형식은 char ** 였다. 이 "포인터에 대한 포인터" 개념은 링크드 데이터 구조에서도 자주 등장한다. 특히, 함수의 인자가 포인터 변수인 경우, 함수 내에서 포인터가 가리키는 곳을 바꿔서 포인터를 수정하고 싶을 때가 있다. 그렇게 하려면 포인터에 대한 포인터를 사용해야 한다.

 17.5에서 썼던 add_to_list 함수(링크드 리스트의 가장 처음에 노드를 삽입)를 고려하자. add_to_list를 호출할 때, 함수에 원래 리스트의 첫번째 노드를 가리키는 포인터를 pass한다; 그리고 업데이트된 리스트의 첫번째 노드를 가리키는 포인터를 반환한다.


struct node *add_to_list(struct node *list, int n)

{
    struct node *new_node;


    new_node = malloc(sizeof(struct node));

    if (new_node == NULL) {

        printf("Error: malloc failed in add_to_list\n");

        exit(EXIT_FAILURE);

    }

    new_node->value = n;

    new_node->next = list;

    return new_node;

}

이 함수를 수정해서 new_node를 반환하는 것이 아니라 new_node를 list에 assign하도록 바꿔 보자. return statement를 없애고 

list = new_node;

라고 하면, 이 방법은 작동하지 않는다. 

add_to_list(first, 10); 

함수를 호출하면, first는 list에 복사된다. 포인터는 다른 모든 인수와 마찬가지로 passed by value임. 함수의 마지막 줄은 list의 값을 바꿔서 새로운 노드를 가리키게 만든다. 하지만 이 assignment는 first에는 영향을 주지 못한다.

 add_to_list가 first를 수정하게 하려면 first의 포인터를 전달해야 한다. 수정된 정답 버전:

void add_to_list(struct node **list, int n)

{
    struct node *new_node;

    new_node = malloc(sizeof(struct node));

    if (new_node == NULL) {

        printf("Error: malloc failed in add_to_list\n");

        exit(EXIT_FAILURE);

    }

    new_node->value = n;

    new_node->next = *list;

    *list = new_node;

}

새로운 버전의 add_to_list를 호출할 때, 첫번째 인자는 first의 주소가 되어야 한다.

add_to_list(&first, 10);

list가 first의 주소에 할당되므로, *list와 first는 동일하다. 또 *list에 new_node를 할당하는 것은 first를 수정한다.



17.7 Pointers to Functions


포인터가 변수, 배열의 원소, 동적할당된 메모리 블록 등 다양한 데이터를 가리킬 수 있다는 것을 보아왔다. 포인터가 데이터만 가리킬 수 있는 것은 아니다; 포인터는 함수도 가리킬 수 있다. 함수도 메모리의 장소를 차지하기 때문에, 변수들이 주소를 가지고 있는 것처럼 모든 함수에는 주소가 있다. 


Function Pointers as Arguments

함수 포인터는 데이터 포인터와 비슷하게 사용할 수 있다. 함수 포인터를 인자로 전달하는 것은 꽤 흔한 일이다. 수학 함수 f를 점 a부터 b 사이에서 적분하는 'integrate' 함수를 쓰고 싶다고 하자. f를 인자로 전달해서 이 함수를 가능한 일반화하고 싶다. C에서 그 효과를 얻으려면, f를 함수를 가리키는 포인터로 선언해야 한다. double 형식을 인자로 받아 double 형식을 반환하는 함수를 적분하고 싶다고 가정하면, integrate 함수의 원형은 다음과 같다:

double integrate(double (*f) (double), double a, double b);

*f를 둘러싼 괄호는 f가 함수에 대한 포인터라는 것을 나타내며, 포인터를 리턴하는 함수라는 뜻이 아니다. 또 f를 함수인 것처럼 선언해도 된다:

double integrate(double f(double), double a, double b);

컴파일러 관점에서 두 원형은 동일하다.


integrate 함수를 호출할 때, 첫번째 인자로는 함수의 이름을 전달한다. 예를 들어, 다음과 같은 호출은 sin(사인) 함수를 0부터 π/2까지 적분한다:

result = integrate(sin, 0.0, PI /2 );

중요한 점은 함수 이름인 sin 뒤에 괄호가 오지 않는 점이다. 함수의 이름 뒤에 괄호가 붙지 않으면, C 컴파일러는 함수를 호출하는 것이 아니라 함수의 포인터를 생산한다. 위 예시에서 우리는 sin을 호출하는 것이 아니며, integrate 함수에 sin을 가리키는 포인터를 전달하는 것이다. 이 것은 배열을 다루는 방식과 유사하다. a가 배열의 이름일 때, a[i]는 배열의 한 원소를 나타내지만, a는 배열을 가리키는 포인터로 쓰인다. 비슷하게 f가 함수인 경우, C는 f(x)는 함수의 호출로 다루지만, f 자체는 함수에 대한 포인터로 간주한다.

 integrate 함수의 바디에서 f가 가리키는 함수를 호출할 수 있다:

y = (*f)(x);

*f는 f가 가리키는 함수를 나타내며, x는 호출시의 인자이다. 따라서 integrate(sin, 0.0, PI / 2)가 실행되는 동안 *f를 호출하는 것은 사실 sin을 호출하는 것이다. (*f)(x)의 대안으로, f(x)로 f가 가리키는 함수를 호출할 수 있다. f(x)가 더 자연스러워 보이지만, 여기에서는 f가 함수의 이름이 아닌 함수의 포인터라는 것을 상기하는 차원에서 계속 (*f)(x)라고 표기한다.


The qsort Function

함수에 대한 포인터 개념이 평균적인 프로그래머와 관련 없어 보일수도 있지만, 그것은 사실과 전혀 다르다. 사실은 C 라이브러리의 가장 유용한 함수들 일부는 인자로 함수의 포인터를 받는다. 그중 하나는 <stdlib.h> 헤더에 속한 qsort이다. qsort는 우리가 선택한 기준에 따라 배열을 정렬할 수 있는 범용 정렬 함수이다.

 정렬하는 배열의 원소는 어떤 형식이든 될 수 있다 - 심지어 구조체나 공용체도 가능 - 배열의 두 원소 중 어떤 것이 "작은지"에 대한 기준을 qsort에게 알려줘야 한다. 이 정보를 comparison function을 써서 qsort에게 전달하게 된다. p와 q가 배열 원소를 가리키는 포인터일 때, 비교 함수는 반드시 *p가 *q보다 "작으면" 0보다 작은 정수를, *p와 *q가 "같다면" 0을, *p가 *q보다 "크다면" 0보다 큰 정수를 반환해야 한다. "같다면", "크다면", "작다면"과 같이 따옴표로 묶은 것은 비교의 기준을 정하는 것이 프로그래머의 몫이기 때문이다. 

qsort의 원형은 다음과 같다:

void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));

base는 배열의 첫번째 원소를 가리켜야 한다(만약 배열의 일부분만 정렬할 거라면, base는 정렬하는 부분의 가장 처음 원소를 가리켜야 한다). 가장 간단한 경우에는 base는 정렬할 배열의 이름이 된다. 

nmemb는 정렬될 원소의 갯수이다(꼭 배열에 있는 원소의 갯수가 아니어도 된다). 

size는 바이트 단위로 측정된 배열의 각 원소의 크기이다.

compar는 비교 함수에 대한 포인터이다.

qsort가 호출되면, 필요한 만큼 비교 함수를 호출해서 배열 원소를 비교하면서 배열을 오름차순으로 정렬하게 된다. 

 16.3의 inventory 배열을 정렬하려면, 다음과 같이 qsort를 호출한다:

qsort(inventory, num_parts, sizeof(struct part), compare_parts);

두번째 인자는 MAX_PARTS가 아닌 num_parts임에 유의하자. inventory 배열 전체가 아니라, 현재 부품이 저장되어 있는 부분만 정렬할 것이기 때문이다. 마지막 인자, compare_parts는 두 part 구조체를 비교하는 함수이다.

 qsort는 compare_parts 함수의 두 매개변수로 void * 타입을 요구하지만, void* 형식으로는 part 구조체의 멤버에 접근할 수 없고 (p->number 같은 접근 불가능) struct part * 타입의 포인터가 필요하다. 문제 해결을 위해, [① compare_parts에서 struct part * 형식의 변수 p1, q1에 자신의 파라미터인 p와 q를 할당과 동시에 변환한다]. (void *는 어떤 형식의 포인터이든 자유롭게 변환될수 있으니까 casting 불필요) compare_parts는 이제 자신의 변수들로 p와 q가 가리키는 구조체의 멤버에 접근할 수 있다. inventory 배열을 부품 번호의 오름차순대로 정렬하는 경우, compare_parts 함수는 다음과 같다:


int compare_parts(const void *p, const void *q)

{
    const struct part *p1 = p;  //

    const struct part *q1 = q;


    if (p1->number < q1->number)

        return -1;

    else if (p1->number == q1->number)

        return 0;

    else

        return 1;

}


p1과 q1의 선언에는 const가 포함되어 있는데 이는 컴파일러의 경고를 피하기 위함이다. p와 q가 const 포인터(그것이 가리키는 object를 수정할 수 없음)이므로, 얘네들은 마찬가지로 const로 선언된 포인터 변수에만 assign될 수 있다.

 위 버전의 compare_parts도 작동하지만, 더 간결하게 쓸 수 있다. 먼저, p1과 q1을 cast expression으로 대체할 수 있다.

int compare_parts(const void *p, const void *q)

{

    if (((struct part *) p)->number <

        ((struct part *) q)->number)

        return -1;

    else if (((struct part *) p)->number ==

             ((struct part *) q)->number)

        return 0;

    else

        return 1;

}


((struct part *) p) 에서 바깥의 괄호는 필요하다. 이 괄호가 없으면 컴파일러는 p->number의 형식을 struct part* 형식으로 변환하려고 시도한다.

 위 compare_parts 함수에서 if 문장을 제거함으로써 더 짧게 만들 수 있다:

int compare_parts(const void *p, const void *q)

{

    return ((struct part *) p)->number -

             ((struct part *) q)->number;

}


q의 부품 번호에서 p의 부품 번호를 뺐을 때의 값이 리턴 값이 된다(p와 q의 부품 번호 대소관계에 따라 반환됨).

주의: 두 정수의 뺄셈은 잠재적으로 오버플로우가 발생할 가능성이 있다. 여기서는 부품 번호가 모두 양의 정수이기 때문에 그럴 문제는 없다.

 inventory 배열을 부품 번호가 아닌 부품의 이름순으로 정렬하려면, 다음과 같은 compare_parts 함수를 사용한다:

int compare_parts(const void *p, const void *q)

{

    return strcmp(((struct part *) p->name,

                  ((struct part *) q->name);

}


문자열 간의 비교는 strcmp 함수를 호출함으로써 쉽게 negative, 0, postive 결과를 얻을 수 있다.


Other Uses of Function Pointers

함수의 인자로 함수의 포인터를 사용하는 경우의 유용성을 강조했지만, 그게 함수 포인터 사용의 전부가 아니다. 함수를 가리키는 포인터는 데이터를 가리키는 포인터처럼 간주된다. 함수를 가리키는 포인터를 변수에 저장하거나, 배열의 원소, 구조체, 공용체의 멤버로도 사용할 수 있다. 심지어 함수의 포인터를 리턴하는 함수도 만들 수 있다.

void (*pf) (int);

위 함수는 함수의 포인터를 저장하는 변수의 예시이다.

pf는 정수 파라미터를 갖고 리턴의 형식이 void인 모든 함수를 가리킬 수 있다. 만약 f가 그런 함수라면, 다음과 같이 pf로 f를 가리킬 수 있다:

pf = f;

f 앞에는 & 연산자가 오지 않는다. pf가 f를 가리키게 되면, 다음 두 가지 모두 f를 호출한다.

(*pf) (i);

pf(i);


함수 포인터가 원소인 배열을 놀랄 정도로 많은 기능을 가지고 있다. 예를 들어, 우리가 사용자가 선택하는 명령 메뉴를 표시하는 프로그램을 짠다고 하자. 다음과 같이 명령을 수행하는 함수들을 하나의 배열 안에 모두 저장할 수 있다:

void (*file_cmd[])(void) = {new_cmd, open_cmd, close_cmd, close_all_cmd, save_cmd, 

                            save_as_cmd, save_all_cmd, print_cmd, exit_cmd};


만약 사용자가 0부터 8 사이에 있는 n 명령을 선택한다면, file_cmd 배열에서 subscription으로 대응하는 명령을 호출할 수 있다:

(*file_cmd[n])();     /* or file_cmd[n]();  */


switch 문을 사용해서 비슷한 효과를 낼 수도 있다. 하지만 함수 포인터의 배열을 사용하는 것이 더 유연하다. 배열의 크기가 프로그램 실행 도중에 바뀔 수 있기 때문이다.




17.8 Restricted Pointers (C99)

->gcc로 컴파일했을 때 책에선 undefined behavior 발생한다고 하지만 잘 된다.


이 섹션과 다음 섹션은 C99의 포인터와 연관된 기능을 설명한다. 두 기능은 고급 기능에 속한다.

C99에서는 restrict 키워드가 포인터 선언시에 등장할 수 있다.

int * restrict p;


restirct를 사용해서 선언된 포인터를 restricted pointer(제한된 포인터)라고 한다. 만약 p가 나중에 수정된 object를 가리키면, 그 object는 p를 제외한 어떤 방법으로도 접근될 수 없다. (p를 제외한 다른 방법이란 다른 포인터가 같은 object를 가리키도록 하거나 p를 어떤 명명된 변수를 가리키게 하는것을 포함한다) 하나의 object에 접근하는 방법이 하나보다 많은 것을 aliasing이라고 부른다.

 제한된 포인터가 방해하는 행동의 예를 살펴보자. p와 q를 다음과 같이 선언하자

int * restrict p;

int * restrict q;


p = malloc(sizeof(int));

(p에 변수의 주소나 배열의 원소를 assign해도 비슷한 상황이 발생한다)

보통은 p를 q에 복사하고, q를 통해 정수를 수정하는 것은 legal하다. 

q = p;

*q = 0;   /* causes undefined behavior */


그러나 p가 제한된 포인터이기 때문에, *q=0;은 정의되지 않아 있다. p와 q를 같은 object를 가리키게 함으로써, *p와 *q는 aliases가 되었다.

 만약 제한된 포인터 p가 extern storage class(18.2) 없이 지역변수로 선언된다면, restric는 p가 선언되어 있는 블록 안의 p에서만 적용된다. (함수의 body도 블록이다) restrict는 함수의 파라미터, 포인터 형식 등에도 사용 가능한데, 그러한 경우엔 함수가 실행되는 도중에만 적용된다. 그러나 file scope를 가지는 포인터 변수에 쓰였을 경우 그 제한은 프로그램 전체를 실행하는 내내 지속된다.

 restrict 사용의 정확한 규칙은 복잡하다(C99 표준을 참고). 제한된 포인터로부터 생성된 alias가 legal인 경우도 존재한다. 예를 들어 제한된 포인터 p가 함수의 지역변수이고 다른 제한된 포인터 변수 q가 함수 몸체 안의 nested block에서 정의된 경우에는, p를 정상적으로 q로 복사할 수 있다.

 restrict의 사용에 대해 알아보기 위해 <string.h> 헤더에 속한 memcpy와 memmove 함수를 살펴보자. C99에서 memcpy 함수의 원형은 다음과 같다:

void *memcpy(void * restrict s1, const void * restrict s2, size_t n);


memcpy는 strcpy와 유사하지만 strcpy가 문자들을 한 문자열에서 다른 문자열로 복사하는 것과 달리 memcpy는 한 object에서 다른 object로 bytes를 복사한다. s2는 복사될 데이터, s1은 복사의 목적지를 가리키며, n은 복사할 byte 수를 나타낸다. s1과 s2 모두에 restrict를 사용한 것은 복사의 소스와 목적지가 겹쳐서는 안된다는 것을 의미한다. (그러나 겹치지 않는 것을 보장해 주지는 않는다)

 반면에 memmove의 원형에는 restrict가 나타나지 않는다:

void *memmove(void *s1, const void *s2, size_t n);

memmove는 memcpy와 같은 것을 한다; 한 곳에서 다른 곳을 바이트들을 복사한다. 차이점은 memmove는 복사의 소스와 목적지가 겹치더라도 작동되는 것이 보장된다는 점이다. 예를 들면 배열의 원소를 한 자리씩 옮기는데에 memmove 함수를 사용할 수 있다.


int a[100];

...

memmove(&a[0], &a[1], 99 * sizeof(int));


C99 이전에는 memcpy와 memmove의 차이점을 기술할 방법이 없었다. 두 함수의 원형은 거의 동일했다:

void *memcpy(void *s1, const void *s2, size_t n);

void *memmove(void *s1, const void *s2, size_t n);

C99에서 memcpy의 원형에서 restrict가 사용됨으로써 프로그래머는 s1과 s2가 가리키는 object가 겹쳐서는 안된다는 것, 만약 겹친다면 함수의 제대로 된 작동을 보장할 수 없다는 것을 알 수 있게 되었다.

 restrict를 함수 원형에서 쓰는 것은 유용하지만 주된 존재 이유는 아니다. restrict는 컴파일러에게 더 효율적인 코드를 만들수 있다는 정보를 준다 - a process known as optimization. (the register storage class:18.2 도 같은 목적이다) 하지만 모든 컴파일러가 프로그램 최적화를 시도하지는 않는다. 또 시도하는 컴파일로도 보통 최적화를 프로그래머가 disable할수 있게 한다. 그 결과, C99 표준에서는 표준을 따르는 프로그램에서 restrict가 프로그램에 미치는 영향이 없음을 보증한다: 만약 restrict가 있던 곳에서 전부 제거되더라도, 동일하게 동작한다.

 대부분의 프로그래머들은 최상의 성능을 내기 위해서 프로그램을 미세하게 조정하지 않는 이상은 restrict를 사용하지 않는다. 그렇지만 C99의 표준 라이브러리 함수들의 원형에 restrict가 등장하기 때문에 알아둘 가치는 있다.



17.9 Flexible Array Members (C99)


가끔은 정해지지 않은 크기의 배열을 담고 있는 구조체를 정의해야 할 때가 있다. 예를 들어, 보통의 문자열과 다른 형태의 문자열을 저장해야 할 수 있다. 일반적으로 문자열은 가장 끝에 null character가 오는 문자들의 배열이다. 그러나, 문자열을 다르게 저장하는데서 오는 장점이 있다. 한가지 대안은 null character를 제외하고 딱 문자열의 길이만큼만 문자들을 저장하는 방법이다. 문자들의 길이와 문자들은 다음과 같은 구조체에 저장될 수 있다:

struct vstring {

    int len;

    char chars[N];

};

N은 문자열의 최대 길이를 나타내는 매크로이다. 고정된 길이의 문자열을 사용할 때 이런 구조체는 모든 문자열의 길이를 동일하게 만들면서 메모리를 낭비하기 때문에 바람직하지 않다. 

 C 프로그래머들이 전통적으로 이 문제를 해결해온 방법은 이렇다. chars의 길이를 1(a dummy value)로 선언하고) 각각의 문자열을 동적 할당하는 것이다.

struct vstring {

    int len;

    char chars[1];

};

...

struct vstring *str = malloc(sizeof(struct vstring) + n - 1);

str->len = n;


이것은 구조체가 선언될 때 가졌던 메모리보다 더 많이 할당하는 "꼼수" 이다(여기서는 n-1 바이트만큼 추가로 할당한다). 이런 방식이 널리 퍼져서 나중에는 "struct hack"이라는 이름도 갖게 되고, 많은 컴파일러서는 이것을 지원할 뿐 아니라 명시적으로 이 트릭을 사용한다는 것을 알 수 있도록 chars 배열의 길이를 0으로 선언할 수 있도록 허용했다. 그리고 C99에서는 표준으로 flexible array member를 지원하게 되었다. 만약 구조체의 마지막 멤버가 배열인 경우에, 그 길이는 생략될 수 있다:

struct vstring {

    int len;

    char chars[];    /* flexible array member - C99 only */

};


chars 배열의 길이는 vstring 구조체를 저장할 메모리가 할당되기 전까지는 결정되지 않는다. 주로 malloc을 호출해서 할당한다:

struct vstring *str = malloc(sizeof(struct vstring) + n);

str->len = n;


위 예시에서, str은 chars 배열이 n개의 문자를 차지하는 vstring 구조체를 가리킨다. sizeof 연산자는 구조체의 크기를 계산할 때 chars 멤버를 무시한다. (flexible array member는 구조체 안에서 공간을 차지하지 않는다는 점에서 특이하다.)

구조체에 flexible array member를 포함시킬 때는 몇가지 규칙이 적용된다. flexible array member는 구조체의 가장 마지막 멤버여야 한다. 또 구조체에 반드시 하나 이상의 다른 멤버가 있어야 한다. FAM이 포함된 구조체를 복사하면, FAM을 제외한 다른 멤버들만 복사된다.

// Flexible Array Members(C99)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct vstring {
	int len;
	char chars[];
};

// Flexible Array Member가 포함되어 있는 구조체를 복사할 때, 나머지 멤버들은 복사되지만 
// Flexible array member 자체는 복사되지 않는다. 
int main(void) {
	int n;
	printf("Enter n: ");
	scanf("%d", &n);
	
	struct vstring *str = malloc(sizeof(struct vstring) + n);
	str->len = n;
	
	printf("size of chars is: %d\n", n);
	
	strncpy(str->chars, "original message", n-1); 
	str->chars[n-1] = 0;	// assign null character
	
	struct vstring *copy_dest = malloc(sizeof(struct vstring) + n);
	*copy_dest = *str;	// FAM이 들어있는 구조체를 복사 시도. 
        //copy_dest에 동적할당으로 n바이트를 추가 할당하지 않으면 세그멘테이션 오류 발생
	
	printf("원본\nlen: %d\nchars: %s\n", str->len, str->chars);
	printf("복사본\nlen: %d\nchars: %s\n",  copy_dest->len, copy_dest->chars);
	
}

 f.a.m을 포함하는 구조체는 incomplete type이다. incomplete type은 저장을 위한 메모리가 얼마나 필요한지에 대한 정보가 결여되어 있다. 19.3에서 또 알아보겠지만 이런 형식에는 다양한 제약 사항이 가해진다. 특히 incomplete type(FAM이담긴 구조체도 포함)은 다른 구조체의 멤버가 되거나 배열의 원소가 될 수 없다. 하지만 배열은 FAM이 포함되어 있는 구조체의 포인터는 원소로 가질 수 있다.


이전 장에서 포인터의 중요한 두가지 용도를 보았음. 11장: 변수를 가리키는 포인터를 함수 인수로 사용하여 함수가 변수를 수정하는 방법. 12장: 배열 원소에 대한 포인터에 대해 산술 연산을 수행하여 배열을 처리하는 방법. 이 장에서는 동적 저장소 할당, 함수에 대한 포인터라는 두 가지 기능을 알아보면서 포인터에 대한 내용을 완성함.


동적 할당을 사용해 프로그램은 실행 중 필요에 따라 메모리 블록을 얻을 수 있음. 

17.1: basics of dynamic storage allocation.

17.2: dynamically allocated strings, which provide more flexibility than ordinary character arrays.

17.3: dynamic storage allocation for arrays in general.

17.4: storage deallocation - releasing blocks of dynamically allocated memory when they're no longer needed.


동적 할당 구조는 연결되어 리스트, 트리, 기타 매우 유연한 데이터구조를 형성할 수 있기 때문에 C 프로그래밍에서 큰 역할을 함.

17.5: Linked lists(the most fundamental linked data structure)

17.6: Pointer to pointer(enough to warrant a section of its own)

17.7: pointer to functions


마지막 두 섹션은 고급 C프로그래머가 관심가질 내용이고 초보자는 스킵해도 무방하다.

17.8: restricted pointers(C99)

17.9: flexible array members(C99)



17.1 Dynamic Storage Allocation


C의 데이터 구조는 보통 그 크기가 정해져 있음. 배열의 크기는 프로그램 컴파일 후 바뀌지 않는다. (C99에서 VLA는 런타임 시점에 길이가 결정되지만, 그 이후로는 배열이 살아있는 동안 고정되어 있다) 고정된 크기의 데이터 구조는, 프로그램을 수정하고 다시 컴파일하지 않으면 그 크기를 바꿀 수가 없다.

 dynamic storage allocation을 사용하면, 데이터구조를 필요에 따라 키우고 줄일 수 있다.


Memory Allocation Functions

동적 할당을 사용하기 위해 <stdlib.h> 헤더에 선언된 다음 메모리 할당 함수 중 하나를 호출해야 한다.

malloc : 메모리 블록을 할당하지만 초기화하지는 않는다.

calloc : 메모리 블록을 할당하고 내용을 지운다.

realloc : 이전에 할당된 메모리 블록의 크기를 조정한다.


셋 중 malloc이 가장 많이 사용된다. 이는 calloc에 비해 효율적인데, 할당하는 메모리를 청소해야 할 필요가 없기 때문이다.

 메모리 할당하는 함수를 호출해서 메모리 블록을 요청할 때, 그 함수는 우리가 어떤 형식의 데이터를 블록에 저장하려는지 모른다. 따라서 int, char같은 일반적인 형식의 포인터를 리턴하지 못한다. 대신 void * 포인터를 리턴한다. void * 는 "일반적인" 포인터로, 본질적으로 그냥 메모리 주소이다.


Null Pointers

메모리 할당 함수가 호출되었을 때, 그 요청을 만족하기에 충분한 크기의 메모리 블록을 찾지 못할 가능성은 항상 존재한다. 그런 일이 일어나면, 함수는 null pointer를 반환한다. null pointer는 "아무것도 아닌 것에 대한 포인터"로, 다른 모든 유효한 포인터로부터 구별되는 특별한 값이다. 함수의 리턴값을 포인터 변수에 저장한 후에, 그것이 null pointer인지 아닌지 반드시 확인해야 한다.

 null pointer는 NULL 매크로에 의해 표현되며, <locale.h>, <stddef.h>, <stdio.h>, <stdlib.h>, <string.h>, <time.h> 그리고 C99의 <wchar.h> 헤더에 정의되어 있다. 이 중 하나만 include하면 NULL 매크로가 정의된다.

if (p == NULL) ...

if (!p) ...

// p가 null 매크로인지 확인. null pointer는 '거짓'이고 모든 다른 포인터는 '참'이다. 따라서 위 두 라인은 동일하다

if (p != NULL) ...

if (p) ...

// 위 두줄도 동일하다.


17.2 Dynamiccaly Allocated Strings


동적 할당은 문자열을 다룰 때 편리하게 쓰는 경우가 많다. 문자열은 문자 배열에 저장되며, 그 배열의 길이가 얼마나 되야 할 지는 예측하기 힘들다. 문자열을 동적 할당 함으로써, 그 결정을 프로그램이 실행될 때까지 미룰 수 있다.


Using malloc to Allocate Memory for a String

malloc 함수의 원형은 다음과 같다:

void *malloc(size_t size);

malloc은 size 바이트 만큼의 블록을 할당해서 그것을 가리키는 포인터를 반환한다. size의 형식은 size_t(unsigned integer)이다. 매우 거대한 메모리 블록을 할당하는 것이 아니라면, size를 평범한 정수형 타입으로 간주해도 무방하다.

 malloc으로 문자열을 위한 메모리를 할당하는 것은 쉽다. 왜냐하면 C에서 char 형식 값은 항상 정확히 1바이트의 저장 공간을 차지하도록 되어 있기 때문이다. n개의 문자가 있는 문자열을 위한 공간을 할당하기 위해, 이렇게 쓴다:

char *p;

p = malloc(n + 1);

인수는 null character를 위한 자리를 위해 n이 아닌 n + 1이다. malloc이 리턴하는 일반적인 포인터는 할당이 수행될 때 char * 형으로 변환된다: 형식 지정은 필요하지 않다. (일반적으로, void * 값을 어떤 타입의 포인터에든 assign할 수 있고, 그 역도 성립한다) 그래도 malloc의 리턴 값을 cast하는 프로그래머들도 있다.

p = (char *) malloc(n + 1);


malloc으로 할당된 메모리는 지워지거나 초기화되어있지 않으므로, p 는 초기화되지 않은 n+1크기의 문자 배열을 가리킨다.

이 배열을 초기화하는 방법 중 하나는 strcpy 함수를 호출하는 것이다:

strcpy(p, "abc");


Using Dynamic Storage Allocation in String Functions

동적 할당을 이용하면 "새로운" 문자열 - 함수 호출 전에는 존재하지 않았던 - 을 리턴하는 함수를 만들 수 있다. 두 개의 문자열을, 둘다 수정하지 않고 연결하는 함수를 만들어 보자. C의 표준 라이브러리에는 그러한 함수가 없다(strcat은 인수로 전달받은 문자열 변수 중 하나를 수정하기 때문에 우리가 원하는 함수가 아니다). 

 이 함수는 연결되어야 하는 두 문자열의 길이를 측정하고, malloc 함수를 호출해서 연결될 결과물에 맞는 공간을 할당한다. 그리고 나서 첫번째 문자열을 새로운 공간에 복사하고, strcat으로 두번째 문자열을 뒤에 연결한다.

char *concat(const char *s1, const char *s2)

{

    char *result;

    

    result = malloc(strlen(s1) + strlen(s2) + 1);

    if (result == NULL) {

        printf("Error: malloc failed in concat\n");

        exit(EXIT_FAILURE);

    }

    strcpy(result, s1);

    strcat(result, s2);

    return result;

}


만약 malloc이 null pointer를 반환한 경우, 함수는 에러 메시지를 출력하고 프로그램을 종료한다. 이것이 항상 좋은 해결책은 아니다; 어떤 프로그램은 메모리 할당 실패를 복구하고 계속 실행되어야 한다.

concat같은 동적으로 저장공간을 할당하는 함수는 조심스럽게 사용해야 한다. 함수가 리턴하는 문자열이 더이상 필요가 없어질 때, free 함수를 호출해 문자열이 점유하는 공간을 release해 주어야 한다. 그렇지 않으면 프로그램은 결국 메모리가 부족해질 수 있다.


Arrays of Dynamically Allocated Strings

13.7에서 문자열을 배열에 저장할 때의 문제를 해결했다. 문자열을 문자의 2차원 배열에 열로 저장했을 때 공간이 낭비되었기 때문에, string literal을 가리키는 포인터들의 배열로 만들어 문제를 해결했다. 13.7에서 사용한 방법은 배열의 원소가 동적 할당된 문자열인 경우에도 사용할 수 있다. 



17.3 Dynamically Allocated Arrays


동적 할당된 배열은 동적 할당된 문자열과 같은 이점을 갖는다(당연히 문자열도 배열이니까). 프로그램을 작성할 때, 종종 배열의 적합한 크기를 추정하기 어려울 때가 있다; 배열의 크기를 결정하는 것을 프로그램이 실행될 때로 미루면 더 편리할 것이다. 배열을 위한 공간을 프로그램 실행 도중 할당하고 나서, 배열의 첫번째 원소를 가리키는 포인터를 통해 배열에 접근할 수 있다. 12장에서 보았듯이 배열과 포인터 간에 밀접한 관계가 있으므로, 동적 할당된 배열을 사용하는 것은 평범한 배열을 사용하는 것과 똑같이 쉽다.

 malloc이 배열에 공간을 할당할 수 있지만, calloc 함수가 대신 사용될 때도 있다. 왜냐하면 calloc 함수는 할당하는 메모리를 초기화하기 때문이다. realloc 함수는 필요에 따라서 배열을 늘리거나 줄일 수 있게 해 준다.


Using malloc to Allocate Storage for an Array

 문자열을 위한 공간을 할당할 때와 거의 같은 방식으로 malloc을 사용해 배열을 위한 공간을 할당할 수 있다. 주요한 차이점은 임의의 배열의 원소는 문자열과 달리 항상 1바이트가 아니라는 것이다. 그 결과 각각의 원소에 필요한 공간을 계산하기 위해 sizeof 연산자를 사용해야 한다.

 원소가 정수이고 크기가 n인 배열을 쓰는데 n은 프로그램 실행중에 결정된다고 하자. 먼저 포인터 변수를 선언한다.

int *a;

프로그램 내에서 n의 값을 알게 되면, malloc을 호출해서 배열을 위한 공간을 할당한다.

a = malloc(n * sizeof(int));

주의: 배열을 위한 공간을 할당할 때는 항상 sizeof 연산자를 사용해야 한다. 


a가 동적 할당된 메모리 블록을 가리키게 되면, a가 포인터라는 사실을 무시하고 배열의 이름으로 사용해도 된다. 이는 C에서의 배열과 포인터의 관계 덕분이다. 예를 들어, 다음과 같은 루프를 사용해 a가 가리키는 배열의 원소를 초기화할 수 있다.

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

    a[i] = 0; 


The calloc Function

비록 malloc 함수로 배열을 위한 메모리를 할당할 수 있지만, 대안인 calloc 함수는 때때로 더 낫다. calloc 함수는 <stdlib.h>에 선언되어 있으며 원형은 다음과 같다.

void *calloc(size_t nmemb, size_t size);


calloc은 각각의 원소가 size 바이트를 점유하는 원소 nmemb개를 가진 배열을 위한 공간을 할당한다; 만약 요청받은 공간을 할당하지 못하면 null pointer를 리턴한다. 메모리를 할당한 후, calloc은 그 메모리 공간의 모든 비트를 0으로 초기화한다. 예를 들어, 다음과 같이 calloc을 호출하면 크기가 n인 정수 배열을 위한 공간을 할당하고, 모든 값을 0으로 초기화 시킨다.

a = calloc(n, sizeof(int));

calloc은 malloc과 달리 할당한 메모리를 지우기 때문에, 배열이 아닌 object의 공간을 할당하기 위해서 calloc을 사용하는 경우가 종종 있다. calloc을 호출하면서 첫번째 인자로 1을 넣으면, 어떤 타입의 데이터든간에 공간을 할당한다.

struct point { int x, y; } *p;

p = calloc(1, sizeof(struct point));

위 코드를 실행하면 p는 x와 y가 0인 point 구조체를 가리키게 된다.


The realloc Function

배열을 위한 메모리를 할당하고 나서, 그 크기가 너무 크거나 작은 경우, realloc 함수를 사용해 배열의 크기를 바꿀 수 있다. <stdlib.h>에 있는 realloc 함수의 원형은 다음과 같다:

void *realloc(void *ptr, size_t size);


realloc이 호출될 때, ptr은 반드시 이전에 malloc, calloc, 또는 realloc을 호출해서 얻어진 메모리 블록을 가리켜야 한다(그렇지 않은 경우, undefined behavior). size 매개변수는 블록의 새로운 크기를 나타내며, 원래의 크기보다 늘리거나 줄일 수 있다. ptr이 배열로 사용되고 있는 메모리를 반드시 가리켜야 하지는 않지만, 실제로는 보통 그렇다.


C 표준에서는 realloc의 행동에 대한 몇가지 규칙을 정해 놓고 있다.

- 메모리 블록을 확장할 때, realloc은 블록에 추가된 바이트들을 초기화하지 않는다.

- realloc이 요청받은 대로 메모리 블록을 확장하지 못했을 때, null pointer를 반환한다; 원래 메모리 블록에 저장되어 있던 데이터는 바뀌지 않는다.

- realloc의 첫번째 인자가 null pointer인 경우, malloc과 동일하게 작동한다.

- realloc의 두번째 인자가 0인 경우, 메모리 블록을 할당 해제한다.

표준 문서는 이 이상으로 realloc의 작동 방식을 상세히 설명하지는 않지만, 합리적으로 효율적일 거라고 기대된다. 메모리 블록을 감소시킬 때, 블록에 저장되어 있는 데이터를 움직이는 일 없이 제자리에 둔다. 마찬가지로, 메모리 블록을 늘릴 때도 데이터는 움직이지 않는다. 만약 메모리 블록을 늘릴 수 없는 경우(바로 다음 공간이 다른 목적으로 이미 사용되고 있는 경우), realloc은 다른 공간에 새로운 블록을 할당하고, 예전 데이터를 그 곳에 복사한다.


realloc 함수가 값을 리턴한 후, 반드시 모든 포인터를 새 메모리 블록을 가리키도록 업데이트 해야 한다. 왜냐하면 realloc이 메모리 블록을 다른 어딘가로 옮겼을 가능성이 존재하기 때문이다.



17.4 Deallocating Storage


malloc과 다른 메모리 할당 함수가 메모리 블록을 얻어오는 곳은 heap이라고 알려져 있는 저장 공간이다. 이 함수들을 너무 자주 호출하거나 너무 큰 메모리 블록을 요구하는 것은 heap을 소진시켜서, 함수들이 null pointer를 리턴하게 만들 수가 있다.

 설상가상으로, 프로그램은 메모리를 할당하고 더 이상 추적하지 못해서 공간을 낭비할 수 있다.

p = malloc(...);

q = malloc(...);

p = q;


두번째 줄까지 실행했을 때의 결과이다. p가 한 메모리 블록을 가리키고 있고, q는 또다른 메모리 블록을 가리키고 있다.

q가 p로 assign된 후(q is assigned to p), 첫번째 블록을 가리키는 포인터가 더 이상 존재하지 않는다(shaded). 따라서 이 공간은 다시 사용할 수 없다. 더 이상 프로그램에서 접근할 수 없는 메모리 블록을 garbage라고 한다. 가비지를 남기는 프로그램은 memory leak을 가지고 있다. 어떤 언어에서는 garbage collector를 제공해서 자동적으로 garbage를 찾아내고 recycle하지만, C는 그렇지 않다. 대신 C 프로그램에서는 free 함수를 사용해 필요하지 않게 된 메모리를 해제함으로써 garbage를 직접 recycle해야 한다.


The free Function

<stdlib.h>에 있는 free 함수의 원형은 다음과 같다:

void free(void *ptr);

free를 사용하는 것은 간단하다; 더 이상 필요하지 않은 메모리 블록을 가리키는 포인터를 인자로 전달하면 된다.

p = malloc(...);

q = malloc(...);

free(p);

p = q;


free 함수를 호출하면 p가 가리키던 메모리 블록을 해제한다. 이제 그 블록은 다음에 malloc이나 기타 메모리 할당 함수를 호출했을 때 이용할 수 있다. 

free함수의 인자는 null pointer거나(이때 free함수는 아무런 효과가 없다) 이전에 메모리 할당 함수로 리턴된 값이어야만 한다. 다른 object에 대한 포인터를 전달한 경우(배열 원소 같은), undefined behavior가 발생한다.


The "Dangling Pointer" Problem

free 함수를 통해 더 이상 필요하지 않은 메모리를 되찾을 수 있지만, 새로운 문제도 생긴다: dangling pointers(달랑거리는 포인터?). free(p)를 호출하는 것은 p가 가리키던 메모리 블록을 해제하지만, p 자체를 바꾸는 것은 아니다. p가 더이상 유효한 메모리 블록을 가리키지 않는다는 사실을 잊어버리면 혼돈이 뒤따를 수 있다.

char *p = malloc(4);

...

free(p);

...

strcpy(p, "abc");    /*** WRONG ***/


dangling pointers는 찾기 어려울 수 있는데, 여러 개의 포인터들이 같은 메모리 블록을 가리킬 수도 있기 때문이다. 만약 그 블록이 해제되면, 블록을 가리키던 모든 포인터들은 'dangling'이 되어 버린다.



17.5 Linked Lists


동적 할당은 리스트, 트리, 그래프, 또 다른 링크드 데이터 구조를 만들 때 특히 유용하다. 이 절에서는 링크드 리스트에 대해 알아본다; 다른 링크드 데이터 구조는 이 책의 범위를 넘어선다.

 linked list는 연속된 구조체(nodes)로 구성되어 있다. 각각의 노드는 체인의 다음 노드를 가리키는 포인터를 담고 있다.

리스트의 가장 마지막 노드에는 null pointer가 들어 있다. 위 그림에선 대각선으로 표시되었다.

 이전 장들에서, 우리는 데이터 아이템들의 콜렉션을 저장하고 싶을 때에는 항상 배열을 사용했다; 링크드 리스트는 그 대안이 되어 준다. 링크드 리스트는 배열보다 더 유연하다: 쉽게 노드를 삽입하거나 삭제할 수 있어, 리스트를 늘리거나 줄일 수 있다. 반면, 배열처럼 아무 곳에나 접근할 수는 없다. 배열의 각 원소에 접근하는 데에는 모두 동일한 시간이 걸린다; 링크드 리스트 안 노드의 경우에는 시작 지점에서 가까우면 시간이 적게 걸리고, 끝에 가까우면 오래 걸린다.

이 절에서는 링크드 리스트 구현, 그를 통해서 할 수 있는 몇가지 흔한 연산 - 리스트 시작지점에 노드 삽입, 노드 찾기, 노드 삭제에 대해 설명한다.


Declaring a Node Type

링크드 리스트를 설정하려면, 가장 먼저 필요한 것은 리스트를 구성하는 노드를 나타내는 구조체이다. 간단히 생각해서 노드는 하나의 정수와 다음 노드를 가리키는 포인터로 구성되어 있다고 가정하자. 노드 구조체는 다음과 같이 생겼다:

struct node {

    int value;           /* data stored in the node */

    struct node *next;   /* pointer to the next node */

};

next 멤버의 형식은 struct node *이며, node 구조체를 가리키는 포인터임을 의미한다. 이름인 node에는 특별한 점이 있으며, 평범한 structure tag이다.

 중요하게 짚고 넘어갈 점은, 16.2에서 살펴봤듯이, 구조체 종류에 이름을 붙일 때 structure tag 방법과 typedef으로 특정 구조체의 종류에 이름을 붙이는 방법이 있다. 이 경우 구조체에 있는 포인터 멤버는 항상 같은 종류의 구조체만 가리키기 때문에, structure tag를 사용해야만 한다. node 태그 없이는, next 형식을 선언할 방법이 없다.

 이제 node 구조체가 선언되었으므로, 어디서 리스트의 시작 위치를 추적할 방법이 필요하다. 다른 말로, 언제나 리스트의 첫 번째 노드를 가리킬 변수가 필요하다. 그 변수를 first라고 하자:

struct node *first = NULL;

first를 NULL 로 설정하는 것은 리스트가 초기화되면서 비어 있음을 나타낸다.


Creating a Node

링크드 리스트를 만들 때, 노드를 하나 하나 만들어서 리스트에 연결시킨다. 노드를 만드는 것은 세 단계를 거친다:

1. Allocate memory for the node.

2. Store data in the node.

3. Insert the node into the list.

우선 여기서는 1과 2에 집중한다.

 node를 만들 때, 노드가 리스트에 삽입될 때까지 임시적으로 그것을 가리킬 포인터 변수가 필요하다. 이 포인터 변수를 new_node라고 하자:

struct node *new_node;

malloc을 사용해 새로운 노드를 위한 메모리를 할당하고, 그 리턴 값을 new_node에 저장하자.

new_node = malloc(sizeof(struct node));

이제 new_node는 node 구조체를 저장하기에 딱 알맞은 크기의 메모리 블록을 가리키게 된다:


그리고 새로운 node의 value 멤버에 데이터를 저장한다.

(*new_node).value = 10;

이 assignment 후에는 이런 그림이 된다:

노드의 value 멤버에 접근하기 위해 indirection 연산자(*)를 사용하였고, 선택 연산자 "." (구조체의 멤버를 선택하기 위해)를 사용하였다. *new_node는 괄호로 감싸져 있어야 한다. "." 연산자는 * 연산자보다 우선순위가 높기 때문이다.


The -> Operator

새로운 노드를 리스트 안에 삽입하는 다음 단계로 넘어가기 전에, 유용한 바로가기에 대해 알아보자. 포인터를 사용해 구조체의 멤버에 접근하는 것이 굉장히 흔한 일이기에 C에서는 이 목적만을 위한 특별한 연산자를 제공한다. right arrow selection, "->" 연산자이다. 

*(new_node).value = 10;

new_node->value = 10;

둘은 동일하다. -> 연산자는 *와 . 연산자를 결합한 것이다; new_node를 간접 참조해서 그것이 가리키는 구조체를 찾고, 그 멤버인 value를 선택한다.

-> 연산자는 lvalue를 만들어 내므로 다른 일반적인 변수가 들어가는 곳에 사용할 수 있다. scanf함수에서도 많이 사용된다.

scanf("%d", &new_node->value);


Inserting a Node at the Beginning of a Linked List

링크드 리스트의 장점은 노드가 리스트 안의 어느 지점이든 연결될 수 있다는 것이다: 시작, 끝지점, 또는 중앙 어떤 곳이든. 리스트 가장 처음에 넣는 것이 가장 쉽기 때문에 이것을 먼저 보자.

 new_node가 삽입될 노드를 가리키고 있고, first가 링크드 리스트의 첫번째 노드를 가리키고 있다면, 두개의 statement를 사용해서 리스트에 노드를 넣는다. 먼저, 새로운 노드의 next 멤버가 원래 처음에 있던 노드를 가리키도록 많든다:

new_node->next = first;

두번째로, first가 새로운 노드를 가리키도록 만든다.

first = new_node;

이 방법은 리스트가 비어 있을 때에도 사용할 수 있다.

다음은 비어 있는 리스트에 두 개의 노드를 연결하는 코드이다.


first = NULL;    // 리스트 초기화


new_node = malloc(sizeof(struct node));

new_node->value = 10;

new_node->next = first;

first = new_node;    // 첫번째 노드를 링크에 삽입


new_node = malloc(sizeof(struct node));

new_node->value = 20;

new_node->next = first;

first = new_node;    // 두번쨰 노드를 링크에 삽입


링크드 리스트에 노드를 삽입하는 것은 매우 흔한 연산이어서 그를 위한 함수를 쓰고 싶을 때가 많다. 그 함수의 이름을 add_to_list라고 하자. 이 함수는 두 개의 매개변수를 갖는다: list(원래 리스트의 첫번째 노드를 가리키는 포인터), n(새로운 노드에 저장될 정수)


struct node *add_to_list(struct node *list, int n)

{

    struct node *new_node;

    new_node = malloc(sizeof(struct node));

    if (new_mode == NULL) {

        printf("Error: malloc failed in add_to_list\n");

        exit(EXIT_FAILURE);

    }

    new_node->value = n;

    new_node->next = list;

    return new_node;

}

위 함수는 list 포인터를 바꾸지 않는다. 그 대신, 새로 삽입된 노드를 가리키는 포인터를 반환한다. add_to_list를 호출할 때, 그 반환값을 first에 저장해야 한다.

first = add_to_list(first, 10);

first = add_to_list(first, 20);


위 문장들은 10과 20이 담겨 있는 노드를 first가 가리키고 있는 리스트에 삽입한다. add_to_list가 first의 새로운 값을 반환하는 게 아니라 first를 직접 업데이트하는 것이 꽤 까다롭다. 이 문제에 대해선 17.6에서 설명한다. 

 다음 함수는 add_to_list를 사용해서 사용자가 입력한 숫자들을 담고 있는 링크드 리스트를 만든다.

struct node *read_numbers(void)

{
    struct node *first = NULL;

    int n;

    

    printf("Enter a series of integers (0 to terminate): ");

    for (;;) {

        scanf("%d", &n);

        if (n == 0)

            return first;

        first = add_to_list(first, n);

    }

}

숫자들은 입력한 역순으로 리스트에 저장된다. first는 항상 가장 마지막으로 입력한 숫자를 담은 노드를 가리키고 있기 때문이다.


Searching a Linked List

링크드 리스트를 만들었으면, 특정한 데이터를 검색해야 될 수 있다. 리스트를 검색하기 위해서 while 문도 쓰일 수 있지만, for 문이 종종 더 낫다. 우리는 카운팅을 포함하는 루프에서 for 문을 사용하는 것에 익숙하지만, for 문의 유연성 덕분에 링크드 리스트를 포함한 다른 작업에 적합한 경우가 많다. 링크드 리스트의 노드들을 다음과 같은 관례적 방법을 사용해서 접근한다. 포인터 변수 p를 "현재" 노드를 추적하기 위해 사용한다:

for (p = first; p != NULL; p = p->next)

    ...


p = p ->next

이 assignment는 포인터 p를 다음 노드로 전진시킨다. 이런 형태의 assignment는 C에서 링크드 리스트를 가로지르는 loop을 작성할 때 변함 없이 쓰인다.


search_list 함수: 정수 n을 리스트(매개변수 list가 가리키는)에서 검색한다. 만약 n을 찾으면, n을 담고 있는 노드의 포인터를 반환한다; 찾지 못하면 null pointer를 반환한다. 위에 쓴 관례적 표현을 사용해서 써 보자.

struct node *search_list(struct node *list, int n)

{

    struct node *p;

    for (p = list; p != NULL; p = p->next)

        if (p->value == n)

            return p;

    return NULL;

}


search_list를 다르게 쓰는 방법도 많이 있다. 대안 중 하나는 p 변수를 없애고, list 자체를 이용해서 현재 노드를 추적하는 것이다:

struct node *search_list(struct node *list, int n)

{

    for (; list != NULL; list = list->next)

        if (list->value == n)

            return list;

}


list 변수는 원래 리스트의 포인터의 복사본이므로, 그 자체를 함수 내에서 바꾸더라도 전혀 문제가 되지 않는다(*list의 값을 바꾸지 않았음).

또 다른 대안은 list->value == n 테스트를 list != NULL 테스트와 결합하는 것이다.

struct node *search_list(struct node *list, int n)

{

    for (; list != NULL && list->value != n; list = list->next)

        ;

    return list;

}


리스트의 마지막에 도달했을 때, list의 값은 NULL 이므로 n을 찾지 못해서 리스트의 마지막에 도달했을 때 list를 리턴하는 것은 NULL을 리턴하는 것이다. 이 버전을 while 문으로 바꾸면 좀더 깔끔해진다.

struct node *search_list(struct node *list, int n)

{

    while (list != NULL && list->value != n)

        list = list->next;

    return list;

}


Chapter 17 - Advanced Uses of Pointers - 2/3 에서 계속됨


세가지 새로운 타입 소개: structures, unions, enumerations

structure: collection of values(members), possibly of different types.

union: similar to a structure, except that tis members share the same storage -> able to store one member at a time, not all members simultaneously

enumeration: integer type whose values are named by the programmer


structure 가장 중요함. 1,2,3절은 이에 대해 다룸

16.1 how to declare structure variables and perform basic operations on them

16.2 how to define structure types, write functions accept structure args or return structures

16.3 how arrays and structures can be nested

16.4 unions

16.5 enumeration



16.1 Structure Variables


지금까지 다로운 유일한 데이타 구조는 배열. 배열의 중요한 두 가지 특징: 첫째, 모든 원소들이 같은 type. 둘째, 배열의 원소를 선택하려면, 그 포지션을 특정해 주어야 함.

 structure(구조체)의 특징은 배열과 꽤나 다름. 구조체의 원소들(C용어로는 members)은 같은 타입이 아니어도 된다. 또 구조체들의 멤버들은 이름을 가지고 있다. 그들을 선택할 때 포지션으로 하는 것이 아니라 이름으로 한다.

 다른 언어들에서도 비슷한 개념을 가지고 있는데, 구조체는 records, 그 멤버들은 fields라고 하는 언어들이 있음.


Declaring Structure Variables

관련되어 있는 데이터 아이템들의 콜렉션을 저장하고 싶을 때 구조체를 사용한다. 예를 들어 창고에 있는 부품들을 기록할 때.

각각의 부품에 대해, 부품의 번호(an integer), 부품의 이름(a string of characters), 부품의 재고(an integer)의 정보를 저장해야 함.

struct {

    int number;

    char name[NAME_LEN+1];

    int on_hand;

} part1, part2;


각각의 구조체 변수는 세개의 멤버를 가지고 있다: number(부품번호), name(부품의 이름), on_hand(부품의 재고)

part1, part2는 해당 타입의 변수이다.

구조체의 멤버가 메모리에 저장되는 방식은 선언된 순서대로이다. 다음과 같이 가정한다면

(1) part1이 주소 2000에 저장됨

(2) 정수는 4바이트를 차지

(3) NAME_LEN의 값은 25

(4) 멤버들 사이에 gap이 없음


part1의 모습은 다음과 같다.


각각의 구조체는 새로운 자기만의 scope를 갖는다; 그 scope 안에서 선언된 모든 이름은 프로그램 내의 다른 이름과 충돌하지 않는다.(C용어로는, 각각의 스트럭쳐는 그 멤버들을 위한 독립된 name space를 갖는다)


Initializing Structure Variables

배열과 마찬가지로, 구조체 변수는 선언과 동시에 초기화될 수 있다. 구조체를 초기화하기 위해서는 구조체에 저장될 값들의 리스트를 중괄호 안에 넣는다.

struct {

    int number;

    char name[NAME_LEN+1];

    int on_hand;

}   part1 = {528, "Disk drive", 10},

    part2 = {914, "Printer cable", 5};


initializer안에 있는 변수는 구조체의 멤버와 같은 순서대로 써야 한다. part1의 경우, 그 number 멤버는 528, name 멤버는 "Disk drive", on_hand 멤버는 10이다. 


Structure initializer의 규칙은 array initializer의 규칙과 비슷하다. in'zer 안의 expression은 모두 상수여야 한다. (C99에서는 이 제한이 풀어졌다. 18.5 참고) in'zer의 멤버는 구조체 선언에 있는 것보다 적어도 된다; 배열과 마찬가지로, "남는" 멤버들의 값은 0으로 주어진다. 남는 문자 배열의 바이트들의 값은 0이 되고, 빈 문자열이 된다.


Designated Initializers(C99)

initializer의 형태를 다르게 지정할 수 있다.

{528, "Disk drive", 10}

{.number = 528, .name = "Disk drive", .on_hand = 10} // 동일

마침표와 멤버 이름이 결합된 것을 designator라고 한다(배열의 원소를 지정하는 designator와 형태가 다르다).

이 방법의 장점: 읽기 쉽다, 바꾸기 쉽다, 구조체 선언시와 순서가 바뀌어도 된다. 또 모든 값 앞에 designator가 오지 않아도 된다.


Operations on Structures

배열 연산중 가장 흔하게 쓰이는 것은 subscripting - 위치로 원소를 선택하는 것 - 이다. 구조체에서 가장 흔하게 하는 연산도 그 멤버를 선택하는 것이다. 구조체에선 포지션이 아닌 이름으로 멤버에 접근한다.

구조체이름.멤버이름

printf("Part number: %d\n", part1.number);

printf("Part name: %s\n", part1.name);

printf("Quantity on hand: %d\n", part1.on_hand);


구조체의 멤버는 lvalue이다. 따라서 assignment의 왼쪽에 올 수 있고 increment, decrement operator를 사용할 수 있다.

멤버의 이름 앞에 오는 구둣점(.)은 배열의 subscribtion시 쓰는 [] 처럼 연산자이다. postfix ++, -- 연산자와 우선순위가 같으며, 거의 모든 연산자들보다 앞선다.

scanf("%d", &part1.on_hand); // part1의 on_hand 멤버의 값을 입력받음


part2 = part1;

이 assignment operation은 part1.number를 복사해서 part2.number에 넣고, part1.name을 복사해서 part2.name에 넣는 식이다.


배열이 이렇게 간단하게 복사가 되지 않는데 비해 구조체는 이렇게 쉽게 복사할 수 있는데, 심지어 구조체 내에 있는 배열도 이렇게 복사할 수 있다. 이 특성을 이용해 "더미" 구조체를 만들어 배열을 간단히 복사할 수 있다.

struct { int a[10]; } a1, a2;

a1 = a2;    // legal, seince a1 and a2 are structures


= 연산자는 compatible type인 구조체들 사이에서만 사용 가능하다. 동시에 선언된 두개의 구조체는 호환 가능하다. 같은 "structure tag"를 이용해 선언되었거나, 같은 타입 이름을 가진 구조체들도 호환 가능하다. ->수정할것


assignment를 제외하면 구조체 전체에 대해 쓸 수 있는 연산자는 없다. == 나 != 연산자로 구조체 전체가 같은지 다른지 비교할 수 없다.



16.2 Structure Types


naming structure types.

만약 변수를 프로그램의 다른 지점에서 선언해야 한다면? 동일한 구조체 정보를 가진 선언을 앞, 뒤로 쓸 것인가? 이것은 코드를 뻥튀기시킬 뿐 아니라, 그렇게 선언된 두 구조체 변수는 compatible하지 않고, 둘 간에 assign연산자도 사용할 수 없다. 또 두 변수의 type에 대한 이름이 없기 때문에 함수 호출시 인자로 사용할 수도 없다.

 이러한 어려움을 피하기 위해, 어떤 구조체의 type을 나타낼 수 있는 이름을 정의해야 한다 - 특정 구조체 함수의 이름이 아닌. 

구조체의 이름을 짓는 데에 두가지 방법이 있다. "structure tag"를 선언하거나, typedef으로 타입의 이름을 정의할 수 있다.


Declaring a Structure Tag

structure tag는, 특정한 종류의 구조체를 식별하기 위한 이름이다. 아래 예제는 part 라는 이름의 structure tag를 선언한다.

struct part {

    int number;

    char name[NAME_LEN+1];

    int on_hand;

};

닫는 중괄호 뒤에 세미콜론이 있어야 선언이 끝난다.


part 태그를 만들고 나면, 그것을 변수 선언에 사용할 수 있다.

struct part part1, part2;

part는 type 이름이 아니기에, struct를 생략할 수 없다.

구조체 태그는 struct라는 단어가 앞에 오지 않으면 인식이 안 되기 때문에, 프로그램 내 다른 이름들과 충돌하지 않는다. 따라서 part라는 이름의 변수를 만드는 것은 좀 혼란을 가중시키기는 하겠지만 아무런 문제가 없다. 구조체 태그의 선언은 구조체 변수의 선언과 동시에 이루어질 수 있다. struct part로 선언된 모든 구조체는 서로 compatible하다.


Defining a Structure Type

structure tag를 선언하는 것의 대안으로, typedef를 써서 genuine type name을 정의할 수 있다. 다음 예는 Part 라는 이름을 가진 type을 정의한다.

typedef struct {

    int number;

    char name[NAME_LEN+1];

    int on_hand;

}   Part;

이 경우에는 type의 이름인 Part가 struct 뒤에 오는것이 아니라 가장 뒤에 온다.

기존 type 변수를 선언하는 것과 동일한 방식으로 변수를 선언한다.

Part part1, part2;

Part가 typedef의 이름이므로, struct Part 라는 표현은 쓸 수 없다. 모든 Part 변수는 어디에서 선언되었든간에 compatible하다.

구조체가 linked list 안에서 쓰이기 위해서는 structure tag 방식으로 선언되어야 한다. 이 책에선 대부분의 경우 typedef보다 structure tags 방법을 사용한다.


Structure as Arguments and Return Values

함수는 구조체를 인자나 리턴 값으로 가질 수 있다.

void print_part(struct part p)

{

    printf("Part number: %d\n", p.number);

    printf("Part name: %s\n", p.name);

    printf("Quantity on hand: %d\n", p.on_hand);

}

part 구조체를 인자로 받아, 그 구조체의 멤버를 출력하는 함수

호출: print_part(part1);


struct part build_part(int number, const char *name, int on_hand)

{

    struct part p;

    p.number = number;

    strcpy(p.name, name);

    p.on_hand = on_hand;

    return p;

}

인자들로부터 part 구조체를 구성해서 반환하는 함수

part1 = build_part(528, "Disk drive", 10);


함수로 구조체를 전달하는 것, 또 함수로부터 구조체를 반환하는 것 모두 구조체 안의 모든 멤버의 복사본을 만들어야 한다. 그 결과 이러한 작업은 상당한 양의 부하를 일으키며, 특히 구조체가 크다면 더 그렇다. 이러한 오버헤드를 피하기 위해, 구조체 자체를 pass하는것보다 pointer를 pass하는것이 추천되는 때가 있다. 비슷하게, 함수로부터 구조체 자체를 리턴받는것이 아니라 함수가 구조체를 가리키는 포인터를 리턴하도록 할 수 있다. -> 17.5에서 자세히 다룸

 

효율성 외에도 구조 복사를 피하는 다른 이유가 있습니다. 예를 들어, <stdio.h> 헤더는 일반적으로 구조 인 FILE 형식을 정의합니다. 각 FILE 구조는 열린 파일의 상태에 대한 정보를 저장하므로 프로그램 내에서 고유해야합니다. 파일을 여는 <stdio.h>의 모든 함수는 FILE 구조체에 대한 포인터를 반환하고 열린 파일에서 작업을 수행하는 모든 함수는 FILE 포인터를 인수로 필요로합니다.

  경우에 따라 함수 내에서 구조체 변수를 초기화하여 함수에 대한 매개 변수로 제공되는 다른 구조체와 일치시킬 수 있습니다. 다음 예제에서 part2의 이니셜 라이저는 f 함수에 전달 된 매개 변수입니다. (구글버녁)


void f(struct part part1)

{

    struct part part2 = part1;

    ...

}

이런 초기화도 허용된다. 단 초기화하는 구조체(이 경우에는 part2)가 automatic storage duration(local to function, not has been declared static)이어야 한다.


Compound Literals(C99)

9.3에서 C99의 compound literal 기능을 소개했다. 이름이 없는 배열을 만들기 위해, 주로 함수에 배열을 pass할 목적으로 사용되었다. compound literal은 "on the fly"한 구조체를 만들기 위해서도 사용될 수 있다. 매개변수로 pass되거나, 함수로부터 리턴되거나, 변수에 할당될 수 있다.

1. 함수에 전달될 구조체를 만들기

print_part((struct part) {528, "Disk drive", 10});

compound literal(굵은 부분)은 part 구조체를 만든다. 그리고 함수의 인자로 전달한다.


2. 변수로 할당

part1 = (struct part) {528, "Disk drive", 10};

in'zer가 담겨진 선언과 비슷해 보이지만, 같지 않다. in'zer는 선언할 때에만 사용할 수 있고, 이런 문장에서는 사용할 수 없다.


일반적으로 compound literal은 괄호 안의 타입 이름, 중괄호로 감싼 값들의 집합이 온다. type name은 structure tag가 될수도 있고 typedef의 이름이 될 수도 있다. designated initializer처럼 designator가 올 수도 있다.

print_part((sturct part) {.on_hand = 10, .name = "Disk drive", .number = 528});


16.3 Nested Arrays and Structures


구조체와 배열은 제한 없이 혼합될 수 있다. 배열은 그 원소로 구조체를 가질수 있고, 구조체는 배열이나 구조체를 멤버로 가질수 있다. 이미 구조체 내에 배열이 nested 된 것을 보았다(part 구조체의 name 멤버). 이번엔 멤버가 구조체인 구조체와, 멤버가 구조체인 배열을 살펴보자.


Nested Structures

구조체 안에 구조체를 넣기.


Arrays of Structures

원소가 구조체인 배열. 배열과 구조체의 가장 흔한 콤비네이션중 하나이다. 이런 배열은 간단한 데이터베이스가 된다. 예를 들어, 이 part의 배열은 100가지 부품에 대한 정보를 저장할 수 있다.

struct part inventory[100];

배열 안에 있는 부품 중 하나에 접근하기 위해서, subscripting을 이용한다. i번째에 위치한 부품을 출력하기 위해선

print_part(inventory[i])

inventory[i].number = 883;    // i번째 부품의 번호를 수정

inventory[i].name = '\0';    // i번째 부품의 이름을 empty string으로 수정


Initializing an Array of Structures

구조체의 배열을 초기화하는 것은 다차원 배열을 초기화하는 것과 비슷하다. 각각의 구조체는 중괄호로 묶여진 in'zer가 있다; 배열의 in'zer는 구조체의 in'zer들을 중괄호로 묶는다.

 배열의 구조체를 초기화하는 이유 중 하나는 프로그램 실행 도중 바뀌지 않는 데이터베이스를 사용하기 위해서이다. 국제전화 국가 코드에 접근하는 프로그램을 만든다고 생각해 보자. 먼저 국가의 이름과 코드가 담긴 구조체를 선언한다.

struct dialing_code {

    char *country;

    int code;

};

country가 문자의 배열이 아닌 포인터임에 주목하자. 이는 만약 dialing_code 구조체를 변수로 사용하면 문제가 된다. 하지만 dialing_code 구조체를 초기화하면 country는 string literal을 가리키는 포인터가 되기 때문에 문제가 없다.


그 다음엔 이 구조체의 배열을 선언하면서 각각의 구조체에 나라들의 이름과 코드를 담도록 초기화한다.

const struct dialing_code country_codes[] =

     {{"Argentina",    54},     {"Bangladesh",      880},

      {"Brazil",       55},     {"Egypt",            20}};

안의 중괄호는 생략이 가능하다. 스타일상 저자는 생략 안함.


C99에서는 한 아이템에 대해 두개 이상의 designator를 사용할 수 있다.

struct part inventory[100] = { [0].number = 528,[0].on_hand = 10, [0].name[0] = 'b' };

굵은 부분: inventory의 첫번째 아이템의 이름의 첫번째 글자가 'b'


16.4 Unions


union(공용체)은 구조체와 비슷하게 하나 이상의 멤버(타입이 같지 않아도 되는)로 구성된다. 하지만 컴파일러는 가장 큰 멤버를 저장할 수 있을 만큼만 메모리를 할당한다. 그 공간 안에서 서로를 덮어쓰게 된다.

union {

   int i;

   double d;

} u;

struct {

   int i;

   double d;

} s;

두 개의 멤버를 가진 union u. union의 선언은 structure와 닮았다.



int가 4바이트, double이 8바이트를 차지한다고 가정하면 s와 u는 메모리에 위와 같이 저장된다.

s 구조체에선, i와 d는 다른 메모리 주소를 차지한다; s의 전체 크기는 12바이트이다.

u 공용체에선, i와 d가 겹친다(i는 d의 첫번째 4바이트를 차지한다); u의 전체 크기는 8바이트밖에 안 된다. 또한 i와 d의 주소는 같다.


공용체의 멤버에 접근하는 방법은 구조체의 그것과 동일하다.

컴파일러가 공용체의 멤버들의 저장공간을 서로 덮기 때문에, 멤버 중 하나의 값을 바꾸면 나머지 멤버들의 값도 바뀌게 된다. 따라서 만약 u.d에 값을 저장하면, u.i의 값을 잃는다(만약 u.i의 값을 조사하면 의미없어보이는 값이 나온다). 이 성질 때문에, u는 i와 d중 하나의 값만 저장할수 있는 장소라고 생각할 수 있다.

 공용체의 성질은 구조체의 성질과 거의 동일하다. union tag, union type을 구조체와 같은 방법으로 사용 가능하고, = 연산자로 복사할 수 있고, 함수로 전달되거나 함수가 리턴할 수 있다. designated initializer를 사용 가능하다.


Using Unions to Save Space

구조체의 공간을 절약하기 위한 방법으로 공용체를 사용할 수 있다. 카탈로그로 판매된 아이템에 대한 정보를 담은 구조체를 디자인한다고 생각해 보자. 카탈로그에는 세 가지 상품만 들어 있다: 책, 머그, 셔츠. 각각의 아이템은 재고 번호와 가격, 또 각각의 아이템에 따라 다른 추가 정보가 있다


Using Unions to Build Mixed Data Structures

공용체는 서로 다른 타입이 섞여 있는 데이터 구조를 만드는데 유용하다. 예를 들어 int와 double 값이 섞인 배열을 만들고 싶을 때 공용체를 이용할 수 있다. 먼저, 공용체 타입을 정의하는데 그 멤버들은 배열에 들어갈 수 있는 타입의 종류들로 한다. 그리고 Number를 원소로 갖는 배열을 만든다.

typedef union {

   int i;

   double d;

} Number;

Number number_array[100];



number_array의 각각의 원소들은 Number union이다. Number union에는 int, double중 하나의 값만 저장할 수 있으므로, int와 double을 혼합해서 number_array에 넣을 수 있다.


Adding a "Tag Field" to a Union

공용체의 큰 문제점은 공용체 중 어떤 멤버가 마지막으로 바뀌었고, 유의마한 값을 지니고 있는지 구별할 방법이 없다는 것이다. 이 문제를 해결하기 위해 구조체 안에 다른 멤버 - "tag field" 또는 "discriminant" - 와 함께 공용체를 집어넣는 방법이 있다. 그 멤버의 목적은 공용체 안에 어떤 값이 저장되어 있는지 알려주는 것이다.


#define INT_KIND 0

#define DOUBLE_KIND 1

typedef struct {

    int kind;    /* tag field */

    union {

        int i;

        double d;

      } u;

} Number;


Number의 멤버는 kind와 u가 있다. kind의 값은 INT_KIND, 또는 DOUBLE_KIND 둘 중 하나이다. 만약 n이 Number 변수이고 멤버 i에 변수를 할당한다면

n.kind = INT_KIND;

n.u.i = 82;


Number variable로부터 값을 꺼낼 때는, kind값을 통해 어떤 멤버의 값이 마지막으로 저장된지 알수 있다.

void print_number(Number n)

{

    if (n.kind == INT_KIND)

        printf("%d", n.u.i);

    else

        printf("%g", n.u.d);

}

tag의 값을 바꾸는 것은 물론 프로그래머의 몫이다.



16.5 Enumerations


많은 프로그램에서는, 의미있는 값의 범위가 작은 변수가 필요하다. 대표적으로 boolean 변수가 있다: "true" 또는 "false"만 필요.

트럼프 카드의 무늬를 저장하는 변수는 4개의 값만 저장된다: "clubs", "diamonds", "hearts", "spades". 이러한 변수를 다루는 방법은 그 변수를 정수로 선언하고, 그것을 변수의 사용 가능한 값을 나타내게 하는 것이다.


int s; /* s will store a suit */

...

s = 2; /* 2 represents "hearts" */

이 방법도 통하지만, 코드를 읽는 사람이 s가 네가지의 값만 갖는 변수라는 것을 알 수 없고, 2의 의미도 분명하지 않다. 


좀더 발전된 방법은 매크로를 사용해서 suit "type"과 각각의 무늬에 대한 이름을 정의하는 것이다.

#define SUIT     int

#define CLUBS    0

#define DIAMONDS 1

#define HEARTS   2

#define SPADES   3


이제 바로 위 표현을 아래같이 나타내서 좀더 읽기 쉽다

SUIT s;

s = HEARTS;


이 방법은 발전되었지만 최선의 답은 아니다. 프로그램을 읽는 사람은 각각의 매크로가 같은 "type"의 값을 나타낸다는 것을 알 수 없다. 또 가능한 값이 꽤 많다면, 일일이 매크로로 정의하는 것도 지루하다. 게다가 위에서 정의한 이름들은 전처리기에서 다 바꿔버리므로, 디버깅할 때 사용할 수 없다.


C에서 가능한 값이 적을 때 사용하기 위해서 만든 특별한 타입이 enumerated type(열거형)이다. 이 타입의 값들은 프로그래머에 의해서 열거되며(enumerated = listed) 각각의 값들은 이름(enumeration constant)이 있어야 한다. 아래는 s1과 s2에 할당될 수 있는 값들(클럽, 다이아, 하트, 스페이드)을 열거한다.

enum {CLUBS, DIAMONDS, HEARTS, SPADES} s1, s2;

열거형은 구조체나 공용체와 공통점이 거의 없지만, 선언 방법은 비슷하다. 그러나 enumeration constat들의 이름은 선언하고 있는 scope 내에 있는 것들과 겹쳐서는 안된다.

 enumeration constant는 #define 지시문으로 만들어진 상수들과 비슷하지만, 같지는 않다. 예를 들어, E.C는 C의 범위 규칙을 따른다. 만약 enum이 함수 내에서 선언되었다면, 그 상수들은 함수 밖에서는 visible하지 않다.


Enumeration Tags and Type Names

구조체나 공용체와 비슷한 이유로 enumeration의 이름을 종종 만들게 된다. 앞서 배운 둘과 마찬가지로, tag를 선언하거나 typedef를 통해 새로운 type 이름을 정의할 수 있다.


enum suit {CLUBS, DIAMONDS, HEARTS, SPADES};

enum suit s1, s2;

// tag 이용

typedef enum {CLUBS, DIAMONDS, HEARTS, SPADES} Suit;

Suit s1, s2;

// typedef 이용


C89에선 typedef enum이 Boolean type을 정의하는 훌륭한 방법이었다.

typedef enum {FALSE, TRUE} Bool;


Enumerations as Integers

C는 enumeration의 값과 상수를 정수로 취급한다. 기본값으로 컴파일러는 특정 열거형 내 상수에 0, 1, 2..를 할당한다. 할당되는 정수의 값을 지정할 수 있다.

enum suit {CLUBS = 1, DIAMONDS = 2, HEARTS = 3, SPADES = 4};

순서 없이 임의의 정수를 입력해도 된다.

enum dept {RESEARCH = 20, PRODUCTION = 10, SALES = 25};

여러개의 상수가 같은 값을 가지는 것도 허용된다. 또 상수에 특정 값이 할당되지 않으면, 바로 직전의 상수보다 1 큰 값이 된다.


Using Enumerations to Declare "Tag Fields"

typedef struct {

    enum {INT_KIND, DOUBLE_KIND} kind;

    union {

        int i;

        double d;

      } u;

} Number;


16.4에서 접한 문제 - 공용체에 마지막으로 할당된 값을 구별하는 것 - 를 완벽하게 해결할 수 있다. kind 멤버를 정수 대신 enum으로 만들자. 이 새로운 구조체는 예전 것과 정확히 같다. 이 방법의 장점: INT_KIND, DOUBLE_KIND 는 매크로가 아닌 enumeration constant이다. 또 kind의 의미, 둘 중 하나만 사용 가능하단 것이 명확해졌다.





프로젝트 #1

void write_line(void)

{

int extra_spaces, spaces_to_insert, i, j;


extra_spaces = MAX_LINE_LEN - line_len;

for (i = 0; i < line_len; i++) {

if (line[i] != ' ')

putchar(line[i]);

else {

// spaces_to_insert = extra_spaces / (num_words - 1);

                        // 앞에 있는 단어들 사이의 공백이 뒤쪽의 공백보다 좁음

spaces_to_insert = (int) ceil(extra_spaces / (float) (num_words - 1)); 

                        // 앞에 있는 단어들 사이의 공백이 더 넓고, 뒤쪽의 공백이 더 좁음

for (j = 1; j <= spaces_to_insert + 1; j++)

putchar(' ');

extra_spaces -= spaces_to_insert;

num_words--;

}

}

putchar('\n');

}



#2

justify.c의 main 함수에서 다음 라인을 삭제


    if (word_len > MAX_WORD_LEN)

    word[MAX_WORD_LEN] = '*';


word.c의 read_word 함수의 가장 끝에 다음 두 줄을 추가


    if (pos >= len)

        word[pos - 1] = '*';


나머지는 프로젝트는 파일로만 저장함(각각 챕터 폴더 안에 프로젝트로)


지금까지 #define, #include와 같은 directive(지시문)들을 자세한 설명 없이 써 왔다.

이 지시문들은 preprocessor(전처리기) - 컴파일 전에 C 프로그램을 수정하는 소프트웨어 - 에 의해 handle된다.

preprocessor는 강력한 도구이나 찾기가 어려운 버그의 원인이 되기도 한다.

어떤 C 프로그래머들은 preprocessor에 깊이 의존하는 경향이 있는데, 적당한 사용이 좋다.


14.1 how the preprocessor works

14.2 general rules that affect all preprocessing directives

14.3 macro definition

14.4 conditional compilation

14.5 lesser-used directives: #error, #line, #pragma



14.1 How the Preprocessor Works


preprocessor -> PP로 축약함.


PP의 행동은 preprocessing directives - #가 앞에 오는 명령문 - 에 의해 제어된다.


#define : defines a macro(상수나 자주 쓰이는 expression을 나타내는 이름)

PP는 #define directive에 이렇게 대응한다-> 매크로의 이름과 정의를 저장한다.

이후 매크로가 프로그램 내에서 쓰이게 되면, PP는 매크로를 "확장"해서, 정의된 값으로 교체한다.


#include : 특정한 파일을 열어서 컴파일하는 파일의 한 부분으로 "include"하도록 PP에게 시킨다.



directive들을 가지고 있는(가능성이 있는) C program이 preprocessor에 input된다.

PP는 이 directive들을 프로세스 내에서 제거한다.

PP의 output은 편집된 C program이며, 더이상 directive들은 존재하지 않는다.

PP의 output은 직접 컴파일러 - 프로그램의 에러를 검사하고 object code(usualy a  machine language)로 번역 - 에 전달된다.

PP는 directive들을 실행하는것 외에도, 주석을 single space character으로 대체한다.

또 어떤 PP는 불필요한 white-space 문자들 - space, 들여쓰기에 쓰인 tab 같은 - 도 제거한다.


C의 초창기에는 PP가 컴파일러가 별개의 프로그램으로, 그 아웃풋이 컴파일러에 전달되었었다.

요즘의 PP는 컴파일러의 한 부분인 경우가 많으며, 아웃풋 중 일부는 꼭 C 코드가 아니어도 된다.

예를 들어, #include <stdio.h> 같이 standard header를 include하는 것이 그 헤더를 프로그램 내로 복사하지 않으면서도 이뤄진다.

대부분의 C 컴파일러들은 PP의 output을 볼 수 있는 기능이 있다. GCC에선 -E option으로 가능.


주의할 점은 PP는 C에 대한 제한적인 지식밖에 없다. 그래서 directive를 처리하면서 잘못된 프로그램을 만들 가능성이 존재한다. 원래 프로그램이 멀쩡해 보이는데도, 그렇게 에러가 발생해서 찾기 힘들수도 있다. 복잡한 프로그램을 만들 때, PP의 output을 검색하는 것이 이러한 에러에 대처하는 유용한 방법이 될 수 있다.



14.2 Preprocessing Directives


Macro definition

#define directive는 매크로를 정의한다.

#undef directive는 정의된 매크로를 제거한다.


File inclusion

#include directive는 특정한 파일의 내용을 프로그램에 포함시킨다.


Conditional compilation

#if, #ifdef, #ifndef, #elif, #else, #endif directive는 PP에서 테스트하는 조건문의 결과에 따라, block of text를 프로그램에 포함시키거나 또는 포함시키지 않는다.


나머지 directive - #error, #line, #pragma 는 더 특수하며 자주 쓰이지 않는다. 이 챕터에서는 이 지시문들에 대해 더 자세히 다룬다. 단 #include는 15.2에서 다루고 여기서는 제외한다.


다음은 모든 directive지시문에 적용되는 규칙들이다.


directive는 항상 기호 #으로 시작된다. #는 화이트스페이스를 제외하면 줄 가장 앞에 와야 한다. #이 오고, directive의 이름이 오고, 그 다음엔 기타 정보들이 온다.

directive의 token들 사이에는 공백이나 horizontal tab이 얼마든지 올 수 있다.

directive는 명시적으로 이어지지 않는 이상 new-line character에서 끝난다. 만약 여러 줄로 만들고 싶다면 줄 마지막에 '\'를 써 줘야 한다.

directive는 프로그램 어디에나 올 수 있다. 통상적으로 #define, #include를 파일의 가장 처음에 배치하기는 하지만, 다른 지시문들은 더 나중에 쓰기도 하고, 함수 정의 내에서 쓰기도 한다.

주석이 directive와 같은 줄에 올 수 있다. directive 옆에 주석을 달아 매크로에 대한 설명을 써놓는 것이 좋다.



14.3 Macro Definitions


Chapter 2부터 지금까지 계속 써온 매크로는 'simple macro'이다. 왜냐면 파라미터가 없기 때문.


Simple Macros

C 표준에서는 ojbect-like macro


#define identifier replacement-list


replacement-list is any sequence of preprocessing tokens.

2.8에서 나온 token(의미단위로 쪼갠 최소 단위)과 유사하다.

replacement list는 identifier, keyword, numeric constant, character constant, string literal, operator, punctuation등을 포함할 수 있다.

PP는 파일의 이후에 identifier가 나타나면, 그것을 replacement-list로 대체한다.


#define STR_LEN 80

#define TRUE     1

#define FALSE    0

#define PI       3.14159

#define CR       '\r'

#define EOS      '\0'

#define MEM_ERR  "Error: not enough memory" 


#define으로 상수들에게 이름을 부여하는 것은 다음과 같은 장점이 있다.


- 프로그램을 읽기 쉽다. 알수 없는 숫자들로 이뤄진 프로그램은 읽는 사람을 혼동시킨다.

- 프로그램을 수정하기 쉽다. "하드코드"된 상수들은 고치기가 매우 번거롭다.

- 비일관성과 오타로 발생하는 에러를 피하게 해 준다.

- C의 기호를 약간 변경시킬 수 있다. 나만의 언어도 만들어버릴 수 있다.

  #define LOOP for (;;)

  기호를 너무 많이 바꾸면 다른사람은 읽기 힘들어질 것이다.

- type의 이름을 다시 부여할 수 있다.

  #define BOOL int

- "conditional compilation"을 사용하는 데 중요하다. 14.4에서 다룸.


매크로가 상수로 사용되었을 때, 보통은 그 이름을 전부 대문자로 사용한다. 다른 목적일 때는 책마다 다른데 매크로(특히 parameterized macros)가 버그의 원인이 될 수 있으므로 주의를 위해 전부 대문자로 사용하기도 한다. K&R의 책에서는 소문자 이름을 선호한다.


Parameterized Macros

function-like macro라고도 한다.


#define identifier( x1 , x2 , ... , xn ) replacement-list


x1, x2, ..., xn은 identifiers임(macro's parameters). 이 파라미터들은 replacement-list에 몇번이고 나올 수 있다.

parameter list가 비어 있어도 된다.

identifier과 여는 괄호 사이에는 공백이 있어서는 안 된다. 만약 공백이 있다면 PP는 이것을 simple macro로 간주하고 괄호부터 전부 replacement list로 간주한다.


directive 이후의 파일에 identifier(y1, y2, ..., yn) 같은 표현이 등장하면, 이것을 replacement-list의 형태로 바꾼다.


#define MAX(x, y)    ((x)>(y)?(x):(y))

#define IS_EVEN(n)   ((N)%2==0))

#define TOUPPER(c)   ('a'<=(c)&&(c)<='z'?(c)-'a'+'A':(c))


parameterized macro를 사용하는 것의 장점

- 프로그램이 약간 더 빨라질 수 있다. 함수 호출은 약간의 부하를 일으킬 수 있다 - context information 저장, 인자 복사 등 - 매크로를 사용하면 그런 부하가 없다. (하지만 C99의 'inline function'은 매크로를 사용하지 않아도 부하를 겪지 않게 해 준다)

- 매크로는 "generic"하다. 함수의 파라미터와 달리 매크로의 파라미터는 특정한 type이 없다. 따라서 type이 달라져도 함수를 새로 써야 하는 불편함이 없다.


단점

- compiled code가 종종 더 길어진다. 매크로 이름이 대응하는 replacement list로 대체되면서, 소스 프로그램의 길이가 길어진다.

- argument의 타입이 체크되지 않는다. 함수가 호출될 때는 컴파일러에서 인자의 type이 적절한지를 검사하고 적절한 type으로 자동변환하거나 에러 메시지를 출력한다. 매크로의 인자는 PP에 의해 검사되지도 않고 변환되지도 않는다.

- 포인터가 매크로를 가리키게 할 수 없다. 반면 포인터는 함수를 가리킬 수 있다.

- 매크로는 인자의 값을 한번보다 더 많이 evaluate할 수 있다. 만약 인자에 side effect가 있다면, 원치 않는 결과가 나타날 수 있다.

n = MAX(i++, j);

n = ((i++)>(j)?(i++):(j));    // PPing 이후의 라인


parameterized macro는 함수를 simulate하는 것 이외에도, 반복적인 코드를 더 간결하게 만들어줄 수 있다.

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

#define PRINT_INT(n) printf("%d\n", n)



The # Operator

macro 정의시에 쓰이는 특별한 연산자: #, ##

둘 다 컴파일러가 인지하지 않으며 PP단계에서 실행된다.

# 연산자는 매크로의 argument를 string literal로 변환한다. 

parameterized macro의 replacement list에서만 사용할 수 있다.

# operator의 기능을 "stringization"이라고 부른다(사전에 없는 단어)


#define PRINT_INT(n) printf(#n " = %d\n", n)


PRINT_INT(i/j);

printf("i/j" " = %d\n", i/j);    // PP를 거치면서 이렇게 치환된다    

printf("i/j = %d\n", i/j);       // compiler는 서로 인접한 string literal을 하나로 합친다


만약 i = 11, j = 2라면

i/j = 5 라고 출력됨


The ## Operator

## 연산자는 두개의 토큰(identifiers, for example)을 붙여서 하나의 토큰으로 만든다. token-pasting 이라고 알려져 있다.

피연산자 중 하나가 매크로 파라미터일 때에는, 먼저 parameter가 해당되는 argument로 대체된 후에 붙여진다.


#define MK_ID(n) i##n

int MK_ID(1), MK_ID(2), MK_ID(3);    //먼저 n이 1, 2, 3으로 각각 대체된 후에 i와 붙는다.

int i1, i2, i3;      // PP 처리 후


## 연산자는 자주 사용되지는 않지만, 다음과 같은 상황에서는 의미가 있다


#define GENERIC_MAX(type)            \

type type##_max(type x, type y)      \

{                                    \

    return x > y ? x : y;            \

}


각 type에 대한 MAX함수를 쉽게 만들어 준다.

GENERIC_MAX(float) -> float 변수에 대한 max 함수 -  float float_max(float x, float y)  - 생성


General Properties of Macros


- 매크로의 replacement list에는 다른 매크로의 invocation이 들어올 수 있다.

#define PI        3.14159

#define TWO_PI    (2*PI)

 PP는 모든 매크로 name이 제거될 때까지 replacement list를 계속 rescan한다

- PP는 온전한 토큰만 대체한다. 다른 변수 이름 등에 매크로의 이름이 들어있다고 해서 바꾸지 않는다.

- 매크로의 scope는 보통 매크로가 정의된 시점부터 파일의 끝까지이다. 함수 내에서 선언된다고 함수 안에서 끝나지 않는다.

- 매크로는 두번 정의될 수 없다. 두번째 정의가 space를 제외하고 첫번째것과 동일하지 않은 한.

- 매크로는 #undef directive에 의해서 "undefined"될 수 있다.

#undef indentifier

identifier가 매크로의 이름일 때, 그 매크로에 대한 정의를 제거하고, 그 이름을 다르게 정의할 수 있게 해 준다. 만약 그 이름을 가진 매크로가 없다면 아무 효과가 없다.


Parentheses in Macro Definitions


앞서 선언한 매크로들의 replacement list 주위에 괄호를 계속 둘렀는데 이것이 필요한 것인가?

답은 Yes. 만약 괄호를 사용해서 replacement list들을 감싸주지 않으면 기대하지 않은 결과를 가져올 수 있다.

매크로 정의시 괄호 사용에 대한 두 가지 규칙.

1. 만약 replacement list에 operator가 있다면, replacement list를 항상 괄호로 감싼다.

2. 만약 매크로에 parameter가 있다면, replacement list 안에 있는 모든 parameter를 괄호로 감싼다.

괄호를 사용하지 않았을 때, 매크로 주변에 있는, 또는 argument의 연산자들의 precedence, 그리고 associativity가 의도하지 않은 방향으로 결과를 만들어낼 수 있다.

#define TWO_PI 2*3.14159      // replacement list에 괄호가 없음

cnv_factor = 360/TWO_PI;      

cnv_factor = 360/2*3.14159;   // 실제 결과

cnv_factor = 360/(2*3.14159); // 원하는 결과


#define scale(x) (x*10)       // parameter에 괄호가 없음

j = SCALE(i+1);             

j = (i+1*10);                 // 실제 결과

j = (i+1)*10;                 // 원하는 결과


괄호를 제대로 사용하지 않으면, 프로그램이 잘 컴파일되고 매크로도 작동되는 것처럼 보이지만, 찾기 힘든 오류를 발생시킨다.


Creating Longer Macros


콤마 연산자를 사용해서 replacement list에 expression을 연속으로 넣고 좀더 복잡한 매크로를 만들 수 있다.

#define ECHO(s) (gets(s), puts(s))


콤마 연산자를 사용하지 않고 중괄호를 이용해서 여러 개의 expression들을 compound statement로 만들어도 되는가?

#define ECHO(s) { gets(s); puts(s); }

if문 내에서 매크로를 사용시 compound statement와 세미콜론으로 인해 뒤 else문은 짝을 찾지 못해 에러가 발생함.


만약 매크로에 expression이 아닌 여러 개의 statement를 담아야 한다면? 콤마 연산자를 사용할 수 없다. 콤마 연산자는 expression은 묶어줘도 statement를 묶어 주지 못함. 해결책은 do.. while (false) loop를 사용하는 것. 가장 뒤에 세미콜론을 붙이지 않은 채로.


do while 문법 복습 -> 가장 뒤에 세미콜론이 붙는다.

do statement while ( expression ) ;


#define ECHO(s)    \

    do {           \

        gets(s);   \

        puts(s);   \

    } while(0)     \


매크로 정의시 while(0) 뒤에 세미콜론이 붙지 않는 점을 유의.


ECHO(str);    /* becomes do { gets(str); puts(str); } while (0);


Predefined Macros


미리 정의되어 있는 매크로들이다. 각각의 매크로는 정수 상수, 또는 string literal을 나타낸다. 컴파일에 대한 정보, 또는 컴파일러에 대한 정보를 제공한다.


Name 

Description 

__LINE__ 

Line number of file being compiled 

__FILE__ 

Name of file being compiled 

__DATE__ 

Date of compilation (in the form "Mmm dd yyyy") 

__TIME__ 

Time of compilation (in the form "hh:mm:ss") 

__STDC__ 

1 if the compiler conforms to the C standard(C89 or C99) 

* Mmm은 세글자로 된 달 이름의 축약어. ex) Dec Feb..


__DATE__, __TIME__ 매크로는 프로그램이 언제 컴파일했는지 알려준다.

__LINE__, __FILE__ 매크로를 에러를 찾는 데 사용할 수 있다.


division by zero 에러를 찾아내 주는 매크로

#define CHECK_ZERO(divisor)  \

    if (divisior == 0)       \

        printf("*** Atempt to divide by zero on line %d "  \

                "of file %s ***\n", __LINE__, __FILE__)

// CHECK_ZERO 매크로를 나눗셈 전에 넣어 준다.

CHECK_ZERO(j);

k = i / j;

*** Attempt to divide by zero on line 9 of file foo.c *** // 0으로 나누려고 하면 이런 메시지를 출력한다


에러를 탐지하는 매크로는 상당이 유용해서 C 라이브러리에는 assert라는 에러를 찾는 매크로가 들어 있다.


Empty Macro Arguments (C99)


C99에선 매크로의 argument가 비어있어도 된다. 콤마의 갯수는 argument를 넣어서 호출할 때와 같게 한다(arg가 생략된 것을 쉽게 알수 있음).

argument를 빈칸으로 두면, 대응하는 parameter에는 아무것도 들어가지 않게 된다.


#define ADD(x, y) (x+y)

i = ADD(,k);

i = (+k);


비어있는 argument 앞에 #연산자가 오면, "" 비어있는 문자열이 된다.

#define MK_STR(x) #x

char empty_string[] = MK_STR();

char empty_string[] = "";


##연산자의 피연산자 중 하나가 비어있는 경우, 보이지 않는 "placemarker" 토큰이 그 자리를 대체한다. 연산의 결과는 비어있지 않은 다른 피연산자가 그대로 나온다. 두개의 placemarker 끼리 연산하면, 결과는 placemarker 하나가 나온다.

#define JOIN(x,y,z)  x##y##z

int JOIN(a,b,c), JOIN(a,b,), JOIN(a,,c), JOIN(,,c);

int abc, ab, ac, c;


Macros with a Variable Number of Arguments


C89에서는 매크로의 argument 갯수는 고정되어 있다. C99에선 제한이 완화되어 argument의 갯수가 무한대도 될 수 있다. 

함수에 대해서는 이 기능이 예전부터 가능했었다(variable-length argument lists). 매크로에서도 가능하게 된 것.

매크로가 가변 인자를 가져야 하는 주된 이유는 printf나 scanf 처럼 가변 인자를 받는 함수에 인자를 전달하기 위해서이다.


#define TEST(condition, ...) ((condition)?    \

    printf("Passed test: %s\n", #condition):  \

    printf(__VA_ARGS__))


Test(voltage <= max_voltage, "Voltage %d exceeds %d\n", voltage, max_voltage);

... 토큰은 ellipsis라고 하며 매크로의 다른 일반적인 매개변수 뒤 맨 마지막에 자리잡는다. 

__VA_ARGS__ 는 가변 인자를 갖는 매크로에서만 replacement list에 들어가는 특별한 identifier이다. ellipsis에 대응하는 모든 인자들을 나타낸다. ellipsis에 대응하는 최소 하나의 인자가 존재해야 한다(empty argument도 가능).


The __func__ Identifier(C99)


__func__ 자체는 PP와는 관계가 없지만 다른 PP의 기능과 비슷하게 디버깅에 유용하다.

모든 함수는 __func__ identifier에 접근할 수 있으며, 현재 실행되고 있는 함수의 이름이 들어있는 문자열 변수와 같다.

#define FUNCTION_CALLED() printf("%s called\n", __func__);

#define FUNCTION_RETURNS() printf("%s returns\n", __func__);


void f(void)    {

    FUNCTION_CALLED();   /* displays "f called" */

    ...

    FUNCTION_RETURNS();  /* displays "f returns" */


__func__의 다른 용도로, 함수 a내에서 함수 b를 호출하면서 인자로 전달되면 함수 a의 이름을 전달해 준다.



14.4 Conditional Compilation


PP에 의해 테스트되는 조건에 따라 프로그램의 일부가 포함되고, 제외된다.


The #if and #endif Directives


디버그를 위해 어떤 값을 프로그램의 중요한 위치에서 printf함수로 출력하고 싶을 때가 있다. 버그를 찾았을 때, 나중을 위해서 그 printf 함수를 남겨놓고 싶지만 컴파일러가 그 함수를 무시하게 하려면?

#define DEBUG 1


#if DEBUG

printf("Value of i: %d\n", i);

printf("Value of j: %d\n", j);

#endif


PP는 #if 지시문이 DEBUG의 값이 0인지 아닌지 test하고, 0이 아니라면 아래에 있는 두 printf함수를 프로그램에 포함시키고 #if, #endif 지시문은 제외시킨다. 다. DEBUG의 값이 0이라면 위 4줄 전부를 프로그램에서 제외시킨다.


일반적인 형태

#if constant-expression

#endif


The defined Operator


앞서 #, ##연산자를 다뤘다. 남은 하나의 연산자는 defined 연산자이다. defined 연산자는 PP에서만 사용되고 그 identifier가 정의된 매크로라면 1을, 정의되지 않은 매크로라면 0을 생산한다.

#if defined(DEBUG)

...

#endif

#if defined DEBUG    // 괄호를 사용하지 않아도 된다.


defined 연산자는 매크로 이름이 정의되었는지 아닌지만을 확인하기 때문에, 그 매크로 이름에 값을 주지 않아도 된다.

#define DEBUG


The #ifdef and #ifnef Directives

#ifdef identifier

#ifdef는 #if와 사용법이 유사하다. 단지 조건이 매크로의 이름이 정의되었는지만을 따진다.


#ifdef identifier

#if defined(identifier)

위 두 지시문의 의미는 동일하다.


#ifndef는 #ifdef와 비슷하지만, 매크로의 이름이 정의되지 않았는지를 테스트한다.


#ifndef identifier

#if !defined(identifier)

동일


The #elif and #else Directives


#if, #ifdef, #ifndef block은 평범한 if문처럼 nested형태로 사용 가능하다. 또한 #elif와 #else directive도 사용 가능하다.


#if expr1

Lines to be included if expr1 is nonzero

#elif expr2

Lines to be included if expr1 is zero but expr2 is nonzero

#else

Lines to be included otherwise

#endif


#if 대신 #ifdef, #ifndef가 와도 된다. #elif는 여러개가 올 수 있으나, #else는 #if와 #endif 사이에 단 하나만 올 수 있다.


Uses of Conditional Compilation

Conditional compilation은 디버깅에도 유용하지만, 다음과 같이 유용하게 쓸 수 있다.


다양한 기계나 운영체제에서 사용 가능한 프로그램을 만들 때

WIN32, MAC_OS, LINUX중 어떤 것이 정의되어 있는 매크로인지에 따라 세 그룹 중 하나의 그룹만 프로그램에 포함된다.


#if defined(WIN32)

...

#elif defined(MAC_OS)

...

#elif defined(LINUX)

...

#endif


서로 다른 컴파일러에서 컴파일 가능한 프로그램을 만들때

어떤 컴파일러는 C의 표준을 받아들이고, 어떤 것은 그렇지 않다. __STDC__ 매크로를 통해 PP는 컴파일러가 표준을 따르는지 아닌지를 알 수 있다. (Visual Studio 2015에서는 __STDC__ 매크로가 정의되어있지 않은듯함)


#if __STDC__

Function Prototypes

#else

Old-style function declarations

#endif


매크로가 정의되어 있는지 확인해서, 안되어 있으면 새로 정의

#ifndef BUFFER_SIZE

#define BUFFER_SIZE 256

#endif


일시적으로 코드 일부를 disabling(conditioning out). /* */가 들어있는 코드는 또 /* */를 사용해서 주석으로 만들 수 없다. 그러나 주석 처리하는것과는 다르며 사이에 있는 코드에 끝나지 않은 statement가 있거나 paired되지 않은 따옴표가 있거나 하면 오류 발생.

#if 0

Lines containing comments

#endif



14.5 Miscellaneous Directives


#error, #line, #pragma 지시문(간단히)


The #error Directive

#error message

message는 any sequence of tokens


PP가 #error directive를 만나면, message가 담겨 있는 에러 메시지를 출력한다. VS, DEV-C++에선 컴파일도 해주지 않는다.


The #line Directive

1. 프로그램 라인에 번호가 붙는 방법을 바꾼다.(보통은 1,2,3...과 같이 붙는다)

2. 컴파일러에게 다른 이름을 가진 파일에서 프로그램을 읽어들인다고 생각하도록 만들 수 있다.


#line n

n은 1과 32767(C99에선 2147483647) 사이의 정수여야 한다.

이 지시문이 있으면 그 다음 줄의 번호가 n이 된다. n, n+1, n+2...

#line n "file"

이 지시문 뒤의 줄 번호가 n이 되며, "file"에서 왔다고 간주된다. 

#line 지시문 사용시에 __LINE__ 매크로의 값이 바뀌며, __FILE__ 매크로의 값도 바뀔 수 있다. 대부분의 컴파일러들이 이 정보를 이용해서 #line 지시문을 에러 메시지를 만드는 데 사용한다.

#line 지시문은 주로 C 코드를 만들어내는 프로그램들이 사용한다(yacc).


The #pragma Directive

#pragma directive는 컴파일러가 특별한 행동을 하도록 요청한다. #pragma 뒤에 오는 명령어들은 컴파일러에 따라 다르다. #pragma 뒤에 알 수 없는 명령어가 오더라도, PP는 에러메시지를 출력해서는 안 되고 단지 무시하기만 한다.






#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#define STACK_SIZE 100

/**** STACK ****/

/* external variables */
int contents[STACK_SIZE];
int *top_ptr = &contents[0];

void make_empty(void) {
	*top_ptr = 0;
}

bool is_empty(void) {
	return top_ptr == &contents[0];
}

bool is_full(void) {
	return top_ptr == &contents[STACK_SIZE];
}

void stack_underflow(void) {
	printf("stack underflow.\n");
	exit(EXIT_FAILURE);
}

void stack_overflow(void) {
	printf("stack overflow.\n");
	exit(EXIT_FAILURE);
}

void push(int i) {
	if (is_full())
		stack_overflow();
	else
		*top_ptr++ = i;
}

int pop(void) {
	if (is_empty())
		stack_underflow();
	else
		return *--top_ptr;
}


13.5 Using the C String Library


몇몇 프로그래밍 언어들은 문자열을 복사, 비교, 연결, 부분집합 선택하는 것 등이 가능하다. 그러나 C의 연산자들은 근본적으로 문자열에 대해서는 쓸모가 없다. 문자열이 기본적으로 배열이기 때문에, 배열이 가지고 있는 제한과 마찬가지로 문자열도 제한되어 있다. 특히, 연산자를 이용해 복사하거나 비교할 수 없다.


char str1[10], str2[10];

str1 = "abc";    /*** WRONG ***/

str2 = str1;    /*** WRONG 배열의 이름은 lvalue가 될 수 없음 ***/


char str1[10] = "abc";    // legal

if (str1 == str2)...    /*** WRONG 포인터 끼리의 비교임***/


다행히 C에서는 string.h 헤더를 통해 문자열에 관한 다양한 함수를 제공한다.

#include <string.h>


string.h에 정의되어 있는 함수들은 대부분 최소한 하나의 문자열 argument가 있어야 한다. String parameter는 type char * 로 선언되며, 문자 배열, char * type의 변수, 스트링 리터럴이 argument가 될 수 있다.


이제부터 나오는 예제에서 str1str2는 문자 배열(string)으로 사용된다.



The strcpy(String Copy) Function


char *strcpy(char *s1, const char *s2);


문자열 s2를 문자열 s1으로 복사한다. (엄밀히 표현하면, s2가 가리키는 문자들 - 첫번째로 만나는 null character까지 - 을 s1이 가리키는 배열로 복사한다.


strcpy(str2, "abcd");    /* str2 now contains "abcd" */

strcpy(str1, str2);    /* str1 now contains "abcd" */


 strcpy의 리턴 값은 버리는 경우가 대부분이다. 다음과 같은 경우엔 리턴 값을 사용하기도 한다.


strcpy(str1, strcpy(str2, "abcd"));    /* both str1 and str2 now contain "abcd" */


이 함수는 각 문자열 변수의 길이를 체크해 주지 않는다. str1이 가리키는 배열의 길이가 n이라면, str2가 가리키는 문자열의 길이가 n-1을 넘어서는 안 된다(마지막의 null character를 감안). 만약 str1 배열의 길이가 str2가 가리키는 문자열의 길이보다 적다면, undefined behavior가 발생한다.


strncpy 함수는 더 느리지만 더 안전하다. strcpy와 비슷하지만 세번째 인자를 통해 복사될 문자의 갯수를 제한한다.


strncpy(str1, str2, sizeof(str1));    // ①


strncpy(str1, str2, sizeof(str1) - 1);    // ②

str1[sizeof(str1) - 1] = '\0';

① str1이 str2에 저장된 문자열을 저장하기에 충분하다면, 복사는 정상적으로 이뤄진다. 만약 str1에 자리가 부족하다면, 자리만큼만 복사가 이뤄지는데 한가지 문제는 str1은 null character로 끝나지 못한다. 

②는 str1이 항상 null character로 끝나는 것을 보장해 주는 더 안전한 방법이다.



The strlen(String Length) Function


size_t strlen(const char *s);


size_t는 C library에 정의되어 있으며 a typedef name that represents one of C's unsigned integer types.


아주 매우 긴 문자열을 다루지 않는다면, 이것이 기술적으로 문제되지는 않는다. 이 값을 integer로 저장해도 무방하다.

strlen 함수는 문자열 s의 길이를 리턴한다 - 첫번째 null character가 나오기 직전까지의 문자의 갯수 - 


int len;

len = strlen("abc");    /* len is now 3 */

len = strlen("");    /* len is now 0 */

strcpy(str1, "abc");

len = strlen(str1);    /* len is now 3 */



The strcat(String Concatenation) Function


char *strcat(char *s1, const char *s2);


s2의 내용을 s1의 끝에 더해 주고, s1을 반환한다.


strcpy(str1, "abc");

strcat(str1, "def");    /* str1 now contains "abcdef" */

strcpy(str1, "abc");

strcpy(str2, "def");

strcat(str1, str2);    /* str1 now contains "abcdef" */

 

strcpy와 마찬가지로 strcat의 리턴 값은 보통 버려지지만, 다음과 같이 사용될 수도 있다.


strcpy(str1, "abc");

strcpy(str2, "def");

strcat(str1, strcat(str2, "ghi"));

/* str1 now contains "abcdefghi", str2 contains "defghi" */

strncat 함수는 strcat의 더 안전하지만 느린 버전이다. 


strncat(str1, str2, sizeof(str1) - strlen(str1) - 1);



The strcmp(String Comparison) Function


int strcmp(const char *s1, const char *s2);


strcmp 함수는 string s1과 s2를 비교해서 0보다 작거나, 같거나, 혹은 더 큰 값을 리턴한다.

값을 리턴하는 기준은 lexicographic ordering이다. 


다음과 같은 경우 s1<s2라고 판단한다.

- s1, s2의 첫번째 i개의 문자가 동일(match)하나, i+1번째 문자를 비교했을 때 s1의 것이 s2의 것보다 작다. "abc"는 "bcd"보다 작고, "abd"는 "abe"보다 작다.

- s1의 모든 문자가 s2에 match되지만, s1이 s2보다 짧은 경우. "abc"는 "abcd"보다 작다.


문자 비교시 문자의 numerical code가 기준이다. 아스키 코드가 가지는 몇가지 특징은 다음과 같다.

- A-Z, a-z, 0-9의 연속된 문자들은 코드도 연속적이다.

- 대문자는 소문자보다 작다(ASCII에서 대문자는 65~90이고, 소문자는 97~122이다).

- 숫자는 문자보다 작다(48~57)

- 공백은 다른 모든 'printing characters'보다 작다(공백은 32).

  

Remind.c

  

  

13.6 String Idioms


string을 조작하는 방법 중 가장 유명한 idiom들을 이용해서 strlen과 strcat 함수를 직접 써 본다. 물론 이 함수들은 표준 라이브러리에 들어 있으므로 실무에서 함수를 직접 쓸 일은 없다. 하지만 간결한 스타일을 배우기 위해 여기서는 써 보는 것.


함수를 새로 쓸 때는 라이브러리 안에 있는 함수의 이름과 같은 함수를 쓸 수 없으므로, 이름을 좀 바꿔야 한다(ex. strlen -> my_strlen).


Searching for the End of a String


size_t strlen(const chr *s)    

{

size_t n;

for (n = 0; *s != '\0'; s++)

n++;

return n;

}


포인터 s가 왼쪽에서 오른쪽으로 움직임에 따라, n은 문자가 몇개나 있었는지를 저장한다. s가 null character를 가리키게 되면, n의 값은 전체 문자열의 길이가 된다. 


size_t strlen(const char *s)

{

size_t n = 0;

for(; *s != '\0'; s++)

n++;

return n;

}


*s!='\0' 과 *s!=0 은 동일한데, null character의 정수 값이 0이기 때문이다. 또한 *s!=0인지 확인하는 것은 *s를 테스팅하는 것과 같다.


size_t strlen (const char *s)

{

size_t n = 0;

for (; *s; s++)

n++;

return n;

}


12.2에서 보았듯이 하나의 expression으로 s를 증가시키면서 *s를 test할 수 있다.


size_t strlen (const char *s)

{

size_t n = 0;

for(; *s++;)

n++

return n;

}

  

for 문에 들어가는 세개의 expr 중에서 가운데 있는 탈출조건만 남아있기 때문에, while문으로 대체할 수 있다.


size_t strlen(const char *s)

{

size_t n = 0;

while (*s++)

n++;

return n;

}

지금까지의 변형은 속도와는 관계가 없으나, 아래 버전은 (어떤 컴파일러에서는) 더 빠르다.


size_t strlen(const char *s)

{

const char *p = s;

while (*s)

s++;

return s - p;

}


이 방법은 null character가 있는 위치에서 string의 첫번째 문자의 위치를 빼서 길이를 계산한다. while loop 안에서 길이를 1씩 증가시키지 않기 때문에 더 빠르다. p를 정의할 때 const가 있어야 한다. Without it, the compiler would notice that assigning s to p places the string that s points to at risk. VS2015에서는 C4090 경고를 해 준다.

  

while (*s)

s++;


while (*s++)

;

 

첫번째 idiom은 string의 끝에 있는 null character를 찾는다. s는 null character를 가리키게 된다.

두번째 idiom은 string 끝의 null character 바로 다음을 가리킨다.



Copying a String

 

strcat 함수의 두가지 버전


직관적이지만, 좀 긴 버전


char *strcat(char *s1, const char *s2)

{

char *p = s1;

while (*p != '\0')

p++;

while (*s2 != '\0')    {

*p = *s2;

p++;

s2++;

}

*p = '\0';

return s1;

}


이 버전은 두 단계의 알고리즘을 사용한다

(1) s1 끝의 null character를 찾아서 p가 그 위치를 가리키게 한다.

(2) s2가 가리키는 문자들을 p로 하나씩(one by one) 복사한다.


strlen을 간결하게 만들었듯이, 이 함수도 간결하게 만들어 보자.


char *strcat(char *s1, const char *s2)

{

char *p = s1;

while (*p)

p++;

while (*p++ = *s2++)

;

return s1;

}


위 코드의 핵심은 "string copy" idiom이다.


while (*p++ = *s2++)

;


두 개의 postfix increment operator를 제외하면, 

*p = *s2

이 표현식은 s2가 가리키고 있는 문자를 복사해서 p가 가리키는 곳에 넣는다. 이 할당이 끝나면, p와 s2는 increment된다.

이 것을 반복해서 s2가 가리키고 있는 문자들을 p가 가리키는 곳으로 계속 복사한다.

루프가 끝나는 것은? while statement는 assignment가 끝난 후의 값을 테스트 한다. 즉 복사되는 문자의 값이 test된다. null character를 제외한 모든 문자들은 true를 반환하므로, 루프는 null character가 한 번 복사된 이후에 끝나게 된다. 따라서 마지막에 따로 null character를 집어넣을 필요가 없다.



13.7 Array of Strings


문자열의 배열을 생각할 때, 문자의 2차원 배열 - remind.c에서 했던 것처럼 - 로 저장하는 것이 바로 떠오르는 해결책이지만 이 방법은 메모리 낭비를 초래함. 문자열들의 길이가 제각각인데 가장 긴 문자열을 기준으로 배열의 크기를 지정해야 하므로, 나머지 문자열을 저장할 때 '\0'을 불필요하게 많이 저장하게 된다.

char planets[][8] = { "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto" };


 이 비효율성을 해결하기 위해서 필요한 것은,

ragged array: 각각의 행이 서로 다른 길이를 가지고 있는 이차원 배열


C에서 ragged array type을 제공하지는 않으나, 만들어 낼 수 있다.

char *planets[] = { "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto" };


planets을 2차원 배열 대신 포인터로 만들었을 뿐이지만 그 차이는 엄청나게 크다. planets의 각 원소는 null-character로 끝나는 문자열이며, 더 이상 낭비되는 문자 공간이 없다. 

각각의 행성 이름에 접근할 때는 plantets에 subscript하기만 하면 된다. 포인터와 배열의 관계 덕분에, 행성 이름에 접근하는 것은 2차원 배열의 원소에 접근하는 것과 동일한 방법으로 하면 된다.



Command-Line Arguments


프로그램을 실행할 때 추가적인 정보를 써넣기도 한다. 파일명이나 추가적인 실행 지침 등

UNIX의 ls command를 예로 들면

ls를 command line에 입력하면 현 디렉토리 내의 파일명을 보여준다.

ls -l을 입력하면 파일명과 자세한 정보(파일 크기, 소유자, 수정 시간 등)을 보여준다.

ls -l remind.c 와 같이 단 하나의 파일에 대한 자세한 정보를 출력할 수도 있다.


위와 같이 command-line arguments (C표준으로는 program parameters)를 사용하기 위해서는 다음과 같이 main 함수를 정의한다.


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

{ ... }


argc("argument counter"): number of command-line arguments

argv("argument vector"): array of pointers to the command-line arguments, which are stored in string form.


argv[0]은 프로그램의 이름을 가리킨다.

argv[1]부터 argv[argc-1]은 나머지 command-line argument를 가리킨다.


argv[argc]는 언제나 null pointer - 아무것도 가리키지 않는 포인터 -다. 이에 대한 자세한 내용은 17.1에서 다룬다. 일단은 macro NULL을 null pointer로 쓴다.


사용자가 ls -l remind.c 라고 입력하면

argc는 3, argv[0]는 프로그램의 이름, argv[1]은 문자열 "-l"의 포인터, argv[2]는 문자열 "remind.c"의 포인터, argv[3]은 null pointer가 된다.


parameter로 문자열(들)을 받아서 행성의 이름인지 확인하는 프로그램




Q&A


string literal의 길이 제한은 C89에선 최소 509글자, C99 에선 최소 4095글자이다.

string literal의 포인터이고 컴파일러는 그에 대한 수정 시도를 막지 못하기 때문에 string constants라고 불리지 않는다.

string literal을 수정하는 것이 위험해 보이지 않은데 어째서 undefined behavior를 발생시키는가? 어떤 컴파일러는 메모리를 저장하기 위해 동일한 string literal을 하나만 저장하려고 한다. char *p = "abc", char *q = "abc"; 에서 "abc"를 한 번만 저장하고 p와 q가 모두 그 곳을 가리키게 한다. 또한, string literal은 메모리의 읽기 전용 구역에 저장될 수 있고 이에 대해 수정 시도하면 크래시를 일으킨다.

문자열이 아닌 문자들의 배열은 꼭 null character를 포함하지 않아도 된다.

printf와 scanf의 첫번째 인자는 string literal이 아닌 string variable도 올 수 있다.

printf의 인자에 문자열 변수를 넣는 것은 위험할 수 있는데, 변수 안에 %가 들어가면 함수는 그것을 conversion specification의 시작으로 간주한다.

만약 에러나 파일의 끝이어서 문자를 읽지 못하는 경우 getchar는 int type의 EOF(매크로)를 반환한다.

strlen이나 strcat같은 함수는 실제로는 최고의 효율성을 위해 어셈블리로 작성되는 경우가 많다.

command-line argument 대신 program parameter란 용어를 사용하는 것은 프로그램이 이제 마우스로 실행되는 경우가 많고 기타 정보가 직접 타이핑해서가 아닌 다른 방식으로 전달되는 경우가 많기 때문이다.

main 함수의 파라미터로 argc와 argv라는 이름을 사용하는 것은 convention이며 꼭 그렇게 하지 않아도 된다.

*arvg[] 대신 **arvg를 사용해도 된다.





지금까지 char 변수와 char의 배열을 이용해서 문자열(series of characters, a string in C terminology)을 표현했는데, 더 편리한 방법을 배운다. 

String constant - C 용어로 literal. 한글로도 그냥 문자 리터럴이라고 표현함.

string variables - 프로그램 중간에 바뀔 수 있다.


13.1 - string literal에 관한 규칙

13.2 - how to declare string variables

13.3 - ways to read and write strings

13.4 - how to write functions that process strings


13.5 - string-handling functions in C library

13.6 - idioms related to strings

13.7 - how to set up arrays whose elements are pointers to strings of different lengths



13.1 String Literals


String literal은 따옴표(") 안에 문자열이 배치된 것이다.

"When you come to a fork in the road, take it."

string literal은 printf와 scanf함수를 다룰 때 이미 나온바가 있다.

string literal을 수정하려고 하면 undefined behavior가 발생한다.

char *p = "abc";

*p = "def";    /* WRONG */


Escape Sequences in String Literals


string literal은 ecape sequence(\n, \t, \a 등)을 포함할 수 있다.

"Candy\nIs dandy\n"


octal 또는 hexadecimal escape를 사용할 때 주의할 점

octal escape는 3자리 숫자까지 escape로 인식하고, 그 전에 non-octal character가 오면 끝낸다.

"/1234" -> /123과 4

"/189" -> /1, 8, 9

반면 hexadecimal escape는 몇자리까지에 대한 제한이 없다. non-hex character가 와야만 끝난다.

"/xfcber" -> /xfcbe, r

만약 über를 입력하고 싶다면 "/xfc" "ber"라고 쓴다.


Continuing a String Literal


string literal이 너무 길 때는, 마지막에 \를 쓰고 다음 줄로 넘어간다. \ 문자는 두개 이상의 줄을 하나의 줄로 인식하게 만들어 준다.

\ 문자 뒤에는 보이지 않는 개행문자 외에는 다른 문자가 와서는 안 된다.

그러나 \ 다음 줄에 반드시 문자가 와야 한다는 단점이 있다(그리고 탭 문자를 쓴 것처럼 띄어져서 출력된다)

printf("When you come to a fork in the road, take it. "

"--Yogi Berra");


위처럼 닫힌 따옴표 뒤에 열린 따옴표가 다시 나오고 그 사이에 whitespace 문자만 있다면, 컴파일러는 두 따옴표 안에 묶인 내용을 하나로 합쳐서 하나의 string으로 만든다. 


How String Literals Are Stored


C는 string literal을 문자(char)의 배열인 것처럼 다룬다. C 컴파일러가 n의 길이를 가진 string literal을 만나면, 그 string을 위해서 n+1 바이트의 메모리를 할당한다. 문자들의 가장 뒤에 하나의 문자 - null character - 가 추가된다. null character는 모든 bit가 0인 byte로, escape sequence로 \0으로 표현된다.


"abc"라는 string literal은 a, b, c, \0 이렇게 네 개의 문자로 저장된다.

비어있는 string literal은 single null character로 저장된다.

string literal이 배열로 저장되므로, pointer of type char*로 다뤄진다. printf, scanf함수는 모두 첫번째 인자로 char* type을 받는다.



Operations on String Literals


일반적으로 C가 char * pointer를 허용하는 곳에서는 항상 string literal을 사용 가능하다. 예를 들어, string literal은 assignment 오른쪽에 올 수 있다.

char *p;

p = "abc";


이 assignment는 "abc"의 문자들을 복사하는 것이 아니며 p가 문자열의 첫 글자를 가리키게 할 뿐이다.

포인터에 대한 subscription이 가능하기 때문에, string literal에도 가능하다.


char ch;

ch = "abc"[1];


ch의 새로운 값은 'b'가 된다. 

이 성질을 이용한 유용한 함수

char digit_to_hex_char(int digit)

{

return "0123456789ABCDEF"[digit];

}



String Literals versus Character Constants


하나의 문자가 담겨진 string literal은 문자 상수와는 다르다. 

string literal "a"는 a와 null character가 담겨진 메모리를 가리키는 포인터로 표현되고,

character constant 'a'는 정수(그 문자에 대한 숫자 코드)로 표현된다.



13.2 String Variables


어떤 언어에서 string을 위한 special type이 존재하는 것과 다르게, C에서는 character로 이루어진 일차원 배열은 string을 저장하는 데 사용할 수 있다. 이로 인해 발생하는 어려움이 있다. 문자의 배열이 string으로 쓰였는지 아닌지 알기가 어렵다. string-handling 함수를 쓸 때 null character를 다루는 것에 대해 조심해야 한다. 문자열의 길이를 알려면 처음부터 시작해서 null character를 찾는것 보다 더 빠른 방법이 없다.


#define STR_LEN 80

...

char str[STR_LEN+1];


위 표현은 common practice. str에 80개 이상의 문자가 들어갈 수 없다는 것을 강조한다. 마지막 글자는 null character가 되기 때문에 배열의 길이를 81로 해야 80개의 문자가 들어갈 수 있다.



Initializing a String Variable


선언과 동시에 initialize될 수 있다.

char date1[8] = "June 14";

char date1[8] = {'J', 'u', 'n', 'e', ' ', '1', '4', '\0'};

// equivalent statements

만약 배열의 길이보다 더 적은 문자가 입력되면, array initializing의 일반적인 규칙에 따라 나머지 원소는 0으로 채워진다(0 = null character)


char date2[9] = "June 14";

char date3[7] = "June 14";

배열의 길이보다 더 길게 입력되는 것은 illegal하지만, 딱 한 자리만 부족해서 null character를 넣지 못하는 경우는 허용한다.

(dev c++에서 실험한 결과 배열의 길이가 더 길게 입력되면 warning이 뜬다. char 배열에서 길이를 글자수와 같게 지정하면 경고가 뜨지 않는다.)


char date4[] = "June 14";

선언시 배열의 길이를 생략한 경우에는 컴파일러가 길이를 계산해 준다. 이 경우에는 8



Character Arrays versus Character Pointers


char date[] = "June 14";    // date를 배열로 선언

char *date = "June 14";    // date를 포인터로 선언

배열과 포인터 간의 밀접한 관계 덕에 date를 string으로 사용하고자 할 때 두 버전 모두 사용 가능하다.

또한, function이 받는 type이 둘 중 어떤 것이더라도 둘을 바꿔서 쓸 수 있다.


그렇지만 두 표현 간에는 중요한 차이가 있다.

- 배열 버전에서는, date에 저장되어 있는 문자들이 수정 가능하다(다른 모든 배열과 마찬가지로). 포인터 버전에선, date는 string literal의 포인터이고 문자를 개별적으로 수정할 수 없다.

- 배열 버전에서는, date는 배열의 이름이다. 포인터 버전에서는, date가 변수의 이름이고 다른 문자열을 가리키도록 수정될 수 있다.


따라서 문자가 수정 가능하도록 하려면 포인터를 사용하는것 만으로는 불충분하며 배열을 사용해야 한다.


char *p;

위 선언은 포인터 변수를 위해 필요한 메모리만을 설정하고 문자열을 위한 메모리를 설정하지 못한다(길이가 얼마나 되는지도 모른다). 만약 p를 string으로 사용하고 싶다면, 문자 배열을 가리켜야 한다.

char str[STR_LEN+1], *p;

p = str;


다른 방법으로는 p가 dynamically allocated string을 가리키게 하는 것이 있다.




13.3 Reading and Writing Strings


printf에서 string을 출력하기 위한 conversion string: %s

char str[] = "Are we having fun yet?";

printf("%s\n", str);

==> Are we having fun yet?


printf 함수는 문자열에 들어있는 문자들을 출력하다가 null character를 만나면 멈춘다. 만약 null character가 없다면, string의 끝을 지나서 메모리 어딘가에 들어있는 null character를 발견할 때까지 계속 출력한다.

문자열의 일부만 출력하고 싶을 때는


%.ps


p : number of characters to be displayed


printf("%.6s\n", str);

==> Are we


%ms


m: m만큼의 칸에 문자열을 표시. 만약 문자열이 m보다 길다면, 짤리지 않고 전체가 표시된다. 문자열이 m보다 짧다면, 

우측 정렬된다. m 대신 -m을 사용하면 좌측 정렬된다.


puts 함수로도 string을 출력할 수 있다. puts 함수는 하나의 인자(출력될 문자열)만 받으며, string이 출력되면 항상 추가로 new line character까지 출력해서 다음 줄로 진행한다.

puts(str);



Reading Strings Using scanf and gets


scanf 함수에서 %s conversion specification은 문자열을 읽어 문자 배열에 넣어 준다.

scanf("%s", str);


배열의 이름이 포인터로 쓰일 수 있으므로, str 앞에 & operator가 오지 않아도 된다.

scanf가 호출되면 whitespace를 스킵하고 문자를 읽는다 - white-space character를 만날 때까지. 따라서 scanf 함수로 저장된 문자열은 절대로 white-space character를 포함하지 않고, 보통 입력된 전체 라인을 읽어들이지 못한다.


scanf는 항상 문자열의 끝에 null character를 저장한다.


전체 라인을 읽지 못하는 경우가 많은 scanf의 대안으로 gets가 된다. get도 scanf처럼 문자를 배열에 저장하고 null character를 덧붙이지만 scanf와 다른 점이 있다.

- gets는 scanf와 다르게 문자열을 읽어들이기 전에 white space를 스킵하지 않는다.

- gets는 new-line character를 찾기 전까지는 계속 문자를 읽는다(scanf는 white-space 문자를 찾으면 멈춘다). 자연스럽게 gets는 new-line character를 저장하지 않고, 대신 null character를 마지막에 저장한다.


scanf의 경우 conversion specification %ns를 사용해서 (n: maximum number of characters to be stored) 문자의 최대 개수를 지정할 수 있으나, gets는 그런 안전장치가 없으며, 만약 배열의 크기보다 더 많은 문자가 입력되었을 경우 undefined behavior를 일으킨다. 따라서 gets는 위험성을 내포하고 있으며, fgets는 더 나은 대안이다.



Reading Strings Character by Character


scanf와 gets 모두 위험성이 있고 충분히 유연하지 못하기 때문에, 프로그래머들은 자신만의 input 함수를 쓰는 경우가 많다. 문자열을 한번에 하나의 문자로만 읽으면, standard input function보다 좀더 넓은 범위의 컨트롤이 가능하다.


만약 자신만의 input function을 짜기로 결정했다면, 다음 이슈를 고려해야 한다.

- 함수가 문자열 앞에 오는 white space를 스킵할 것인가?

- 어떤 문자가 왔을때 함수가 그만 읽게 할 것인가? new-line character? any white-space character? or some other character? 그 문자를 버릴 것인가 또는 포함시킬 것인가?

- 입력된 문자가 너무 길 경우 어떻게 처리할 것인가? 초과된 문자를 버릴 것인가 아니면 다음 input operation을 위해 남길 것인가?



문자열 앞의 white-space를 스킵하지 않고, new-line character를 처음으로 만났을 때 읽기를 중단(new-line character는 저장하지 않음)하고, 초과된 문자들은 버리는 함수

int read_line(char str[], int n)    {

int ch, i = 0;


while ((ch = getchar()) != '\n')

if (i < n)

str[i++] = ch;

str[i] = '\0';        // terminates string

return i;            // number of characters stored


ch 가 char type이 아닌 int type임에 유의하자. getchar 함수는 문자를 int 값으로 반환하기 때문이다.



13.4 Accessing the Characters in a String


문자열은 배열로 저장되므로, subscripting을 통해 각 문자에 접근할 수 있다. 모든 문자에 접근하기 위해서는 루프문을 사용한다. 이때 포인터를 사용하면 편리하다.


using array subscripting

int count_spaces(const char s[])

{

int count = 0, i;

for (i = 0; s[i] != '\0'; i++)

if (s[i] == ' ')

count++;

return count;

}


using pointer arithmetic

int count_spaces(const char *s)

{

int count = 0;

for(; *s != '\0'; s++)

if (*s == ' ')   

count ++;

return count;

}


const는 함수가 s를 수정하는 것을 막지 않는다. 다만 s가 가리키는 것은 바꾸지 못하게 한다. s가 함수에 전달된 포인터의 복사본이므로, 함수 안에서 이뤄지는 s의 increment는 원래 포인터에게 영향을 주지 못한다.


이 함수에서 제기할 수 있는 의문들

- 문자열에 있는 문자들에 접근할 때, array operation과 pointer operation 중 어느 것이 더 나은가?

더 편리한 것을 쓰면 되고, 섞어서 써도 된다. 포인터 사용시 index를 위한 int 변수를 쓰지 않아도 되므로 좀더 단순하게 표현 가능하다. 전통적으로 프로그래머들은 문자를 다룰 때 포인터 연산을 사용했다.


- 문자열 파라미터가 어떤 것으로 선언되어야 하는가? 배열 or 포인터? 

둘 간에 차이가 없다. 12.3에서 살펴봤듯이 컴파일러는 배열 파라미터를 포인터로 선언된 것과 마찬가지로 취급한다.


- 파라미터의 형태(s[] or *s)가 함수에 넣을 수 있는 argument에 영향을 끼치는가?

NO. 위 count_spaces 함수에서 argument는 배열의 이름, 포인터 변수, 또는 문자 리터럴이 될 수 있다. 위 함수는 그 차이를 구분하지 못한다.




Ch11 introduced pointers, and how they're used as function arguments, values returned by the functions.

포인터가 배열의 원소를 가리킬 때, 포인터에 덧셈과 뺄셈 연산을 할 수 있다. 

포인터와 배열의 관계는 C를 마스터하는데 critical한 부분이다. 그러나 배열을 다루는데 포인터를 써야 하는 가장 큰 이유 - 효율성- 은 예전만큼 중요하지 않은데, 컴파일러들이 더 발전했기 때문이다.


12.1 pointer arithmetic and how pointers can be compared using the relational and equality operators

12.2 how we can use pointer arithmetic for processing array elements

12.3 array name can serve as a pointer to the array's first element

12.4 how the topcis of first three sections apply to multidimensional arrays

12.5 wrap-up, by exploring the relationship between pointers and variable-length arrays (C99)



12.1 Pointer Arithmetic


int a[10], *p;

라고 정의 되어 있을 때,

p = &a[0]; 

=> p가 a[0]을 가리키게 한다.


그림으로 보면 위와 같다.


*p = 5; 라고 하면, a[0]자리에 5가 들어간다.



포인터에 pointer arithmetic (or address arithmetic)을 수행함으로써 배열의 다른 원소에 접근할 수 있다.

pointer arithmetic에는 다음 세가지 종류가 있다.


1. Adding an integer to a pointer

2. Subtracting an integer from a pointer

3. Subtracting one pointer from another


각각의 연산을 살펴보자. 다음부터 나올 예시에서는 다음 선언이 유효하다고 가정한다.

int a[10], *p, *q, i;



Adding an Integer to a Pointer


포인터 p의 값에 정수 j를 더하면, p가 가리키는 원소에서 j자리만큼 뒤의 원소를 가리킨다.

만약 p가 a[i]를 가리킨다면, p + j는 a[i+j]를 가리킨다(물론 a[i+j]가 있다는 전제 하이다).


p = &a[2];


q = p + 3;


p += 6;



Subtracting an Integer from a Pointer


p가 a[i]를 가리킬 때, p - j 는 a[i-j]를 가리키게 된다.


p = &a[8];


q = p - 3;


p -= 6;




Subtracting One Pointer from Another


포인터끼리 뺄셈을 수행했을 때, 그 결과는 포인터 간의 거리(measured in array elements)이다.

p가 a[i]를 가리키고 q가 a[j]를 가리킬 때, p - q는 i - j와 같다.


p = &a[2];

q = &a[5];

i = p - q;    /* i is 3 */

i = q - p;    /* i is -3 */


※ 배열의 원소를 가리키지 않는 포인터에 연산을 수행하는 것은 undefined behavior를 일으킨다. 나아가, 한 포인터에서 다른 포인터를 빼는 것도 두 포인터가 같은 배열의 원소를 가리키지 않으면 undefined behavior를 일으킨다.



Comparing Pointers


관계 연산자(<, <=, >, >=)와 동등 연산자(==)를 통해 복수의 포인터를 비교할 수 있다.

두 포인터에 관계 연산자를 사용하는 것은 두 포인터가 같은 배열에 있는 원소를 가리킬 때만 의미가 있다.

비교의 결과는 두 원소의 상대적인 포지션의 결과에 따른다. 예를 들어,

p = &a[5];

q = &a[1];

이면, p<=q의 값은 0이고 p>=q의 값은 1이다.




12.2 Using Pointers for Array Processing


Pointer arithmetic을 통해 배열의 원소에 반복적으로 증가하는 포인터 변수를 통해 하나하나 접근할 수 있다.


#define N 10

...

int a[N], sum, *p;

...

sum = 0;

for(p = &a[0]; p < &a[N]; p++)

sum += *p;


포인터를 사용하지 않고도 쉽게 loop문을 subscripting (a[i])을 이용해 쓸 수 있다.

subscripting 대신 pointer arithmetic을 써야 한다는 주장의 가장 큰 근거는 실행 속도가 더 빠르다는 것인데, 이는 implementation에 따라 다르다. 어떤 컴파일러는 subscripting을 사용할 때 더 나은 코드가 되기도 한다.



Combining the * and ++ Operators


배열의 원소를 다루는 statement에서 흔하게 * (indirection)과 ++ 연산자를 혼합해서 사용한다.

간단한 예로 한 배열 원소에 값을 저장하고, 다음 원소로 넘어가는 경우를 생각하자.

array subscripting 사용시,

a[i++] = j;

p가 배열 원소를 가리키고 있다면, 그에 대응하는 statement는

*p++ = j;

postfix ++연산자는 *보다 우선순위가 빠르기 때문에, 컴파일러는 위 식(expression)을 다음과 같이 본다.

*(p++) = j;


p++의 값은 p이다(postfix ++연산자를 사용하고 있는데, p의 값은 다음 expression(여기서는 j)이 evaluated되기 전까지는 증가하지 않는다).

따라서 *(p++)의 값은 *p, 즉 p가 가리키는 object이다.

(*p)++는 p가 가리키는 object의 값을 반환한 후, 그 object의 값을 1 증가시킨다(p 자체는 변하지 않는다).


 Expression

Meaning 

 *p++ or *(p++)

Value of expression is *p before increment; increment p later 

 (*p)++ 

Value of expression is *p before increment; increment *p later 

 *++p or *(++p) 

Increment p first; value of expression is *p after increment 

 ++*p or ++(*p) 

Increment *p first; value of expression is *p after increment


4개의 표현은 모두 사용하지만, *p++를 더 자주 사용한다. 이 표현은 loop에서 유용하다.


for (p = &a[0]; p < &a[N]; p++)

sum += *p;

대신에

p = &a[0];

while (p < &a[N])

sum += *p++;

라고 쓸 수 있다.



10.2에서 사용했던 stack에서 integer variable이었던 top을 바꿔 보자.

int *top_ptr = &contents[0];


void push(int i)

{

if (is_full())

stack_overflow();

else

*top_ptr++ = i;

}

int pop(void)

{

if (is_empty())

stack_underflow();

else

return *--top_ptr;

}


*top_ptr-- 이 아닌, *--top_ptr이라고 쓴 것에 유의할 것. pop 함수에서 top_ptr을 먼저 1 감소시키고, 그다음 그것이 가리키는 값을 얻기를 원하기 때문이다.



12.3 Using Array Name as a Pointer


배열의 이름은 배열 첫번째 원소를 가리키는 포인터로 쓰일 수 있다.


int a[10];

*a = 7;    /* stores 7 in a[0]    */

*(a+1) = 12;    /* stores 12 in a[1]    */


일반적으로 a + i는 &a[i] (a의 i번째 원소를 가리키는 포인터)와 같고, *(a + i) 는 a[i] (원소 i)와 같다.

배열의 이름을 포인터로 쓸 수 있기 때문에 배열 안을 도는 루프문을 더 간단하게 표현할 수 있다.

for (p = &a[0]; p < &a[N]; p++)

sum += *p;

대신

for (p = a; p < a + N; p++)

sum += *p;


배열의 이름이 포인터로 쓰이기는 하지만, 새로운 값을 assign할 수는 없다. 배열의 이름을 포인터 변수에 복사해서 그 포인터 변수에 값을 assign할 수는 있다.


/* Reverses a series of numbers (pointer version) */


#include <stdio.h>


#define N 10


int main(void)

{

int a[N], *p;


printf("Enter %d numbers: ", N);

for (p = a; p < a + N; p++) 

scanf("%d", p);

printf("In reverse order:");

for (p = a + N - 1; p >= a; p--)

printf(" %d", *p);

printf("\n");

return 0;

}



Array Arguments (Revisited)


함수로 전달되었을 때, 배열의 이름은 항상 포인터로 간주된다.

일반적인 변수가 함수로 전달될 때, 그 값은 복사되어 전달된다. 파라미터에 대한 어떤 변동도 원래 변수에는 영향을 끼치지 않는다. 반면, 배열이 argument로 사용되었을 때는 배열이 복사되지 않고, 함수 내에서의 값의 변동에 보호되지 않는다. 만약 배열 파라미터가 바뀌지 않고 그대로 남아있기를 원한다면, 함수 선언부에 const를 넣어 준다.


int find_largest(const int a[], int n)


만약 const가 있다면, 컴파일러는 함수의 body에 배열의 원소에 assignment가 있는지를 체크한다.

배열의 크기는 배열을 함수로 pass 하는 시간에 영향을 끼치지 않는다. 왜냐하면 배열의 복사본이 만들어지지 않기 때문이다.

배열 파라미터는 원한다면 포인터로 선언되어도 된다.


int find_largest(int *a, int n)


a를 포인터로 선언하는 것은 배열로 선언하는 것과 동일하다. 컴파일러는 그 둘을 동일하게 취급한다.

배열을 파라미터로 갖는 함수에는 배열의 "slice" - a sequence of conseucutive elements 가 전달될 수 있다. 예를 들어 b라는 배열의 b[5], ..., b[14] 중 가장 큰 원소를 찾고 싶다면, 

largest = find_largest(&b[5], 10);


Using a Pointer as an Array Name


배열의 이름을 포인터로 사용할 수 있다면, 포인터를 마치 배열인 것처럼 subscript해서 사용할 수 있는가? 답은 Yes이다.


#define N 10

...

int a[N], i, sum=0, *p = a;

...

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

sum += p[i];


컴파일러는 p[i]를 *(p+i)와 같게 취급한다(perfectly legal use of pointer arithmetic).



12.4 Pointers and Multidimensional Arrays


포인터는 일차원 배열의 원소를 가리킬 수 있을 뿐 아니라, 다차원 배열의 원소도 가리킬 수 있다. 이 섹션에서는 이차원 배열을 다루지만, 더 높은 차원의 배열에도 적용할 수 있다.


Processing the Elements of a Multidimensional Array


8.2에서 살펴보았듯이 2차원 배열은 row-major order로 저장된다.


이 점을 포인터와 연관지어 이용해 볼 수 있다. 포인터 p를 반복적으로 증가하면서, 2차원 배열의 모든 원소에 접근할 수 있다.


int a[NUM_ROWS][NUM_COLS];

int row, col;

...

for (row = 0; row< NUM_ROWS; row++)

for (col = 0; col < NUM_COLS; col++)

a[row][col] = 0;


이것이 지금까지 배운 nested for loops로 이차원 배열의 모든 원소에 접근하는 방법이다. 그러나 배열 a를 일차원 배열로 바라보면(실제로 그렇게 저장됨), 이중 for 문을 하나의 for 문으로 표현할 수 있다.


int *p;

...

for (p = &a[0][0]; p <= &a[NUM_ROWS-1][NUM_COLS-1]; p++)

*p = 0;


이러한 방법은 대부분의 C 컴파일러에서 제대로 작동한다. 가독성을 다소 떨어트리기는 하지만, 오래된 컴파일러에서는 이러한 방식이 효율성 측면에서 더 우월하다. 그러나 최근의 많은 컴파일러에서는 속도의 차이가 거의 없거나 아예 없는 경우가 많다.


Processing the Rows of a Multidimensional Array


배열 i행의 첫번째 원소에 접근하는 포인터는,

p = &a[i][0]; 또는 p = a[i];


배열의 한 행 전체에 접근하는 코드

int a[NUM_ROWS][NUM_COLS], *p, i;

...

for (p = a[i]; p < a[i] + NUM_COLS; p++)

*p = 0;



Processing the Columns of a Multidimensional Array


배열이 열이 아니라 행으로 저장되기 때문에 열에 접근하는 것은 좀 더 복잡하다.

int a[NUM_ROWS][NUM_COLS], (*p)[NUM_COLS], i;

...

for (p = &a[0]; p < &a[NUM_ROWS]; p++)

(*p)[i] = 0;


(*p) [NUM_COLS] : NUM_COLS만의 길이를 가진 정수 배열의 포인터(괄호 없이 *p[NUM_COLS]라고 하면 포인터로 구성된 배열을 의미한다)

p++ : p가 다음 행을 가리키게 함

(*p) [i]: *p는 a의 한 행을 가리키므로, (*p) [i]는 그 행의 i열을 가리킨다(괄호 없이 *p[i]라고 하면 *(p[i])를 의미한다.



Using the Name of a Multidimensional Array as a Pointer


일차원 배열의 이름이 포인터로 쓰일 수 있는 것과 마찬가지로, 차원에 관계 없이 모든 배열은 그 이름이 포인터로 쓰일 수 있다. 그러나 좀더 조심해서 다뤄야 한다.


int a[NUM_ROWS][NUM_COLS]; 

위와 같은 배열이 있을 때, a는 a[0][0]에 대한 포인터가 아닌 a[0]의 포인터이다.

C의 관점에서는 배열 a를 이차원 배열이 아닌 일차원 배열(원소가 일차원 배열인)로 본다.

a가 포인터로 쓰였을 때, a의 type은 int (*) [NUM_COLS]이다(길이가 NUM_COLS인 배열의 포인터).

a가 a[0]을 가리킨다는 사실을 이용하면 이차원 배열의 원소를 다루는 루프를 간단하게 만들 수 있다.

for (p = &a[0]; p < &a[NUM_ROWS]; p++)

(*p)[i] = 0;

// 대신

for (p = a; p < a + NUM_ROWS; p++)

(*p)[i] = 0;


이것을 다르게 이용하면, 다차원 배열을 일차원 배열인 것처럼 C를 '속이는' 경우에 편리하다.


int a[NUM_ROWS][NUM_COLS];

...

largest = find_largest(a[0], NUM_ROWS * NUM_COLS);

largest = find_largest(a, NUM_ROWS * NUM_COLS);    /*** WRONG ***/


마지막 줄은, 포인터 a의 타입은 int (*)[NUM_COLS]이지만 find_largest가 expect하는 것은 int * 이므로 틀렸다.


Q&A 해석





최대공약수(gcd, great common divisor)를 구하는 함수이다. 이 방법은 유클리드의 알고리즘(Euclid's algorithm으로 알려져 있다. 내용은 다음과 같다.

m과 n을 각각 양의 정수라고 하자(m이 n보다 크거나 같다). 만약 n이 0이면, 멈추고, m이 최대공약수가 된다.

그렇지 않다면, m을 n으로 나눈 나머지를 n에 저장하고, 원래 n의 값을 m에 저장한다. 이것을 n이 0이 될 때까지 반복한다.


int get_gcd(int m, int n) {

int temp;

if (m < n) {

temp = m;

m = n;

n = temp;

}

while (n > 0) {

temp = m%n;

m = n;

n = temp;

}

return m;

}


한편 두 수의 최대공약수를 구하면 최소공배수(lcm, least common multiple)도 쉽게 구할 수 있다. 

m * n = gcd(m, n) * lcm(m, n) 이므로, lcm(m, n) = m * n / gcd(m, n)이다.


int get_lcm(int m, int n) {

int gcd = get_gcd(m, n);

return m * n / gcd;

}

포인터는 C에서 가장 중요하고, 또 가장 혼란을 많이 겪는 부분. 이 중요성 때문에 세장에 걸쳐, 11, 12, 17장에서 다룬다.

챕터11은 포인터의 기본을 중점적으로, 12, 17은 advanced uses를 소개


11.1 memory address와 pointer variable간의 관계

11.2 address, indirection operators

11.3 pointer assignment

11.4 how to pass pointers to functions

11.5 returning pointers from functions



11.1 Pointer Variables


pointer가 machine level에서 어떤 것을 나타내는가?

main memory는 bytes로 나누어지고, 각각의 byte는 여덟개의 bit를 저장한다.


 0

1

0


각각의 byte는 메모리의 다른 바이트로부터 구분하기 위한 고유의 address를 가진다.

만약 메모리에 n byte가 있다면, 0부터 n-1까지를 각 바이트의 주소라고 가정해 볼 수 있다.


Address

Contents 

 0

 01010011

 1

 01110101

 2

 01110011

 3

 01100001

 4

 01101110

 

 ...

 n-1

 01000011



executable program은 code와 data로 이루어짐. 각각의 프로그램은 하나 이상의 byte를 메모리에서 점유한다.

변수가 점유한 바이트 중 첫번째 바이트가, 그 변수의 주소이다.


 

...

 

 2000

 

 } i

 2001

 

 

 ...

 


위 그림처럼, 변수 i가 주소 2000, 2001의 바이트를 점유하고 있다면, i의 주소는 2000이 된다.

비록 주소가 숫자로 표현되기는 하지만, 그 값의 범위가 integer type이 허용하는 범위와 다를 수 있기 때문에 - 범위를 초과할 수 있기 때문에 -  평범한 integer 변수에 주소를 저장해서는 안된다. 대신, 특별한 pointer variable에 저장한다. 

pointer variable p에 변수 i의 주소를 저장할 때, p "points to" i라고 말한다. 다시 말해, 포인터는 그냥 주소이다. 그리고 포인터 변수는 그 주소를 저장하는 변수이다.



Declaring Pointer Variables


int *p;


평범한 변수를 정의하는 것과 비슷하다. 단지 별표가 변수 이름 앞에 와야 한다.

위 선언은, p가 type int의 object를 가리키는 포인터 변수라는 것을 나타낸다.

p가 변수에 속하지 않는 메모리 영역을 가리킬 수도 있을 때, variable 대신 object용어를 사용할 것임. 자세한 것은 17, 19장에서 설명됨


int i, j, a[10], b[20], *p, *q;


이렇게 다른 변수들과 같이 선언하는 것도 가능하다.

모든 포인터 변수는 오로지 지정된 타입의 object만을 가리켜야 한다(the referenced type). 


int *p;         /* points only to integers    */

double *q;      /* points only to doubles    */

char *r;        /* points only to characters */

referenced type에 대한 제한은 없다. 심지어, 포인터 변수는 다른 포인터를 가리킬 수도 있다.



11.2 The Address and Indirection Operators


포인터 만을 위한 연산자가 두 개 있다. 

1. 변수의 주소를 찾을 때, & (address) operator를 사용한다. 만약 x가 변수라면,  &x는 그 변수가 있는 메모리의 주소이다. 

2. 포인터가 가리키는 object에 접근하기 위해서는 * (indirection) operator를 사용한다. p가 포인터라면, *p는 p가 가리키는 object를 나타낸다.


The Address Operator


포인터를 선언하면 포인터를 위한 공간을 set(?) 하지만, 그 포인터는 어떤 object도 가리키지 않는다.

int *p;    /* points nowhere in particular */


p를 사용하기 전에 초기화하는 것이 중요하다. 포인터를 초기화하는 방법 하나는, 어떤 변수의 주소(더 일반적으로 말하자면 lvalue)를 할당하는 것이다. 이 때 &연산자를 사용한다.


int i, *p;

...

p = &i;    /* i의 주소를 변수 p에 할당함으로써, 이 statement는 p가 i를 가리키도록 만든다. */


포인터를 선언하면서 초기화하는 것도 가능하다.


int i;

int *p = &i;


//심지어 i가 먼저 선언되었다는 전제 하에, i의 선언과 p의 선언/초기화를 동시에 할 수도 있다.

int i, *p = &i;


The Indirection Operator


포인터 변수가 object를 가리키게 되면, * (indirection) operator를 사용해 그 object에 저장된 것에 접근할 수 있다.

예를 들어 p가 i를 가리키고 있다면, 다음과 같이 i의 값을 print 할 수 있다.


printf("%d\n", *p);


이러면 printf 함수는 i의 주소가 아닌 i의 값을 표시해 준다.

변수에 &연산자를 적용하면, 그 변수의 포인터가 된다.

포인터에 *연산자를 적용하면, 다시 원래 변수가 된다.

j = *&i;    /* same as j = i; */


p가 i를 가리킨다면, *p는 i의 다른 표현방법이다. *p는 i와 같은 값을 가지고 있을 뿐 아니라, *p의 값을 바꾸는 것 또한 i의 값을 바꾸게 된다.

(*p는 lvalue이기 때문에, 그것에 assign하는 것은 legal이다.)


주의:

초기화되지 않은 포인터 변수에 * 연산자를 적용해서는 안 된다. -> undefined behavior.

다음과 같은 코드는 쓰레기값을 띄우거나, 프로그램이 깨지거나, 또는 다른 이상한 결과를 발생시킬 수 있다.

int *p;

printf("%d", *p);    /*** WRONG ***/


*p에 값을 할당하는 것은 특히 위험하다. 만약 p가 우연히 유효한 메모리 값을 가지고 있었다면, *p에 값을 할당하는 것은 그 주소의 원래 데이터 값을 바꾸게 된다.

int *p;

*p = 1;    /*** WRONG ***/



11.3 Pointer Assignment


포인터를 복사하기 위해 assignment operator를 쓰는 것이 허용된다. 단 그 변수들이 같은 type 이어야만 한다.

int i, j, *p, *q;

p = &i;

q = p;


이제 *p를 바꾸든, *q를 바꾸든 i의 값이 따라서 바뀌게 된다.

위에서 q=p; 와 *q = *p; 는 전혀 다르다.


p = &i;

q = &j;

i = 1;

*q = *p;


이 코드는 p가 가리키는 변수의 값(즉 i의 값)을 복사해서 q가 가리키는 변수(j)에 할당하게 된다.



11.4 Pointers as Arguments


9.3에서 변수가 argument로 함수로 전달될 때, 그 변수의 값은 변하지 않음을 다뤘었다(because C passes arguments by value).

이 특성은 함수로 변수의 값을 바꾸고 싶을 때 골칫거리가 된다.

그러나 포인터를 활용하면 이 문제를 해결할 수 있다. 변수 자체가 아닌 변수의 포인터를 전달하면 된다.

함수를 통해 변수 x의 값을 바꾸고 싶을 때, 함수의 argument로 x가 아닌 &x, 즉 x의 포인터를 전달한다. 그리고 대응하는 파라미터 p(포인터)를 정의한다.

함수가 호출되었을 때, p는 &x의 값을 가지고 *p(p가 가리키는 object)는 x의 alias가 된다. function의 body에 나오는 *p는 x의 indirect reference가 되며, 함수가 x의 값을 읽기도 하고 수정할 수도 있게 한다.


// 배열에서 가장 작은 원소와 큰 원소 찾기

void max_min(int a[], int n, int *max, int *min);


int main(void) {

int b[N], i, big, small;

printf("Enter %d numbers: ", N);

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

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


max_min(b, N, &big, &small);

printf("Largest: %d\n", big);

printf("Smallest: %d\n", small);

return 0;

}


void max_min(int a[], int n, int *max, int *min) {

int i;

*max = *min = a[0];

for (i = 1; i < n; i++) {

if (a[i] > *max)

*max = a[i];

else if (a[i] < *min)

*min = a[i];

}

}



Using const to Protect Arguments


x의 값을 함수에 전달하고는 싶지만, x의 값이 변하지 않게 하려면?

(굳이 변수의 값을 바꾸고 싶지도 않은데 포인터를 이용하는 이유는 효율성 때문이다. 변수가 많은 양의 저장공간을 필요로 한다면, 변수의 '값'을 전달하는 것은 시간과 공간을 낭비할 수 있다.)

변수의 주소는 함수에 전달되지만, 값을 바꾸지 않게 하고 싶다면 변수 선언시 파라미터 앞에 const를 써 준다.

void f(const int *p) {

*p = 0;    // WRONG: expression must be a modifiable lvalue라고 뜬다.

}



11.5 Pointers as Return Values


인터를 함수로 pass할 뿐 아니라, 함수가 포인터를 return할 수 있다. 이러한 함수들을 챕터13에서 다룰 것.


int *max(int *a, int *b) {

if (*a > *b)

return a;

else

return b;

}

int main(void) {

int *p, i, j;

p = max(&i, &j);

}


위 함수는 두 정수의 포인터를 받아서, 더 큰 쪽의 변수를 리턴한다.

max 함수를 호출하면, 두 정수 변수의 포인터가 전달되고 결과의 포인터가 리턴된다.

함수는 external variable의 포인터, 또는 static으로 선언된 local variable의 포인터를 리턴할 수도 있다.






하나의 프로그램 안에서 하나 이상의 함수를 사용할 때 발생하는 문제를 다룬다. 

10.1 : local variables

10.2 : external variables

10.3 : blocks (compound statements containing declarations)

10.4 : c의 'scope - 범위 or 변수 범위'. local, external, blocks안에서 선언된 name의 규칙

10.5 : 함수 프로토타입, 함수 정의, 변수선언 등에 대한 방법


10.1 Local Variables


함수의 body 안에서 선언된 변수를 그 함수에 local하다, local variable이다라고 한다.


int sum_digits(int n)    {

int sum = 0;    /* local variable */

while (n > 0) {

sum += n % 10;

n /= 10;

}

return sum;

}


properties of local variables


automatic storage duration: storage duration (or extent)은 프로그램 내에서 변수를 위한 storage(저장공간?)가 존재하는 부분을 말한다. local variable의 storage는 그 변수가 속한 함수가 호출될 때 '자동적으로' allocate(할당)되고, 함수가 return할 때 deallocate(해제?)된다. local variable은 속한 함수가 값을 리턴했을 때 값을 유지하지 않는다. 따라서 함수가 다시 호출되었을 때, 예전 값을 유지한다는 보장이 없다.


block scope: 변수의 scope(범위)는 프로그램 중에서 그 변수가 참조될 수 있는 부분이다. local variable은 'block scope'를 갖는다. 그 변수의 선언부터, 속한 함수 body의 끝 부분까지. local variable의 범위가 속한 함수를 넘지 못하기 때문에, 다른 함수에서는 같은 이름의 변수를 다른 목적으로 사용할 수 있다.

(C99) C99에서는 변수의 선언이 반드시 함수의 맨 처음에 오지 않아도 되기 때문에, local variable의 범위를 아주 작게 할 수도 있다.


Static Local Variables


로컬 변수 선언시에 'static'을 추가하면, 그 변수는 automatic storage duration이 아닌 static storage duration을 갖는다. 이제 이 변수의 storage는 함수가 끝날 때 같이 끝나지 않고 프로그램이 끝날 때까지 유지된다. 그러나, 여전히 block scope를 가지며 다른 함수에서 visible하지 않다.


void f(void)    {

static int i;    /* static local variable */

...

}


Parameters


파라미터도 local variable과 같은 특성을 가진다(automatic storage duration, block scope). 

파라미터와 local variable의 유일한 실질적인 차이점은 파라미터는 함수가 호출될 때 자동적으로 initialize된다는 것.



10.2 External Variables


함수와의 정보 전달은 passing argument를 통해서도 가능하지만, exeternal variable을 통해서도 가능하다.

external variable(또는, global variable)은 함수 몸체 바깥에서 선언된 변수이다.


properties of external variables


static storage duration: static으로 선언된 local variable과 같다. 한번 변수에 저장된 값은 계속 저장되어 있다.

file scope: 변수가 선언된 시점부터 프로그램이 끝날 때까지 visible하다. 따라서 선언 이후에 나오는 모든 함수에서 접근하고 값을 수정할 수 있다.


using external variables to implement a stack(fragment)


Pros and Cons of External Variables

다수의 함수가 하나의 변수를 공유하거나, 몇개의 함수가 다수의 변수를 공유할 때 external variable을 사용하면 편리하다.

그러나 대부분의 경우에는 parameter를 통한 전달이 변수를 공유하는 것보다 낫다. 그 이유는

- 프로그램을 관리하면서 external variable을 수정할 때(type 등) 그것이 각각의 함수에 대해 어떤 영향을 미치는지 일일이 체크해야 한다.

- external variable에 잘못된 값이 할당되었을 때, 어떤 함수가 잘못되었는지 찾기 힘들다.

- external variable에 의존하는 함수는 다른 프로그램에서 재사용하기가 힘들다. (not self-contained)

  재사용을 위해 그 함수가 필요로 하는 모든 external variable까지 같이 가져가야 한다.


external variable을 남용하는 프로그래머들이 많다.

external variable을 사용할 때는, 의미있는 이름을 사용해야 한다.


숫자 맞추기 게임(external variable을 사용)


숫자 맞추기(external variable 사용하지 않고. 바뀐 부분은 볼드체)



10.3 Blocks


앞서 5.2에서 여러개의 statement를 묶어 { statements } 로 만든 것을 다뤘다. (selection statement에서)

이제부터 여러개의 statement를 중괄호로 묶은 것을 block이라고 표현한다.

block 안에 변수의 선언도 들어갈 수 있는데, 그렇게 선언된 변수들은 automatic storage duration, block scope이다.

블럭 안에서 변수를 선언하는것의 장점 - 아주 잠깐 사용할 변수들을 함수 앞부분에 정의해 어수선하게 만들지 않아도 된다. name conflict를 줄인다.

(C99) 함수와 마찬가지로, 블럭 안 어디서나 변수를 선언할 수 있다.


10.4 Scope


범위에 대한 규칙

블럭 내에서 선언된 변수의 이름이 이미 visible한(file scope이거나, 소속한 블럭에서 이미 선언되었거나) identifier(식별자: https://msdn.microsoft.com/ko-kr/library/e7f8y25b.aspx)경우, 새로운 선언은 예전 것을 'hide'하고, 식별자는 새로운 의미를 갖는다. 그 블럭이 끝나면, 그 식별자는 다시 예전 의미를 다시 얻는다.


10.5 Organizing a C Program


여기서는 하나의 파일 안에서 이뤄지는 프로그램을 다룸. 여러개의 파일을 사용하는 프로그램은 15장에서 다룸.


프로그램 순서 예시:

#include directives

#define directives

Type definitions

Declarations of external variables

Prototypes for functions other than main

Definition of main

Definitions of other functions


포커 점수 계산






주사위 2개를 '시행회수' 만큼 굴린다.

각각의 주사위에서 1부터 6까지 값이 나온 빈도와 두 주사위의 합의 빈도를 표시.


참고로 두 주사위의 합에 대한 확률은 다음과 같다.

합: 확률(퍼센트)    확률(분수)

2 : 2.78    1/36

3 : 5.56    2/36

4 : 8.33    3/36

5 : 11.11    4/36

6 : 13.89    5/36

7 : 16.67    6/36

8 : 13.89    5/36

9 : 11.11    4/36

10 : 8.33    3/36

11 : 5.56    2/36

12 : 2.78    1/36


#include <stdio.h>

#include <time.h> // time function

#include <stdlib.h> // srand, rand function

#define 시행회수 1000000


int roll_dice(void);

int ar_dice1[6] = { 0 }, ar_dice2[6] = { 0 };

int sum[13] = { 0 };



int main(void) {

int i;

srand((unsigned)time(NULL));

for (i = 0; i < 시행회수; i++)

roll_dice();


printf("DICE 1 SUMMARY\n");

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

printf("%d: %d회 (%.3f%%)\n", i+1, ar_dice1[i], (float) ar_dice1[i]*100 / 시행회수);

printf("\nDICE 2 SUMMARY\n");

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

printf("%d: %d회 (%.3f%%)\n", i + 1, ar_dice2[i], (float)ar_dice2[i]*100 / 시행회수);

printf("\nSUM SUMMARY\n");

for (i = 2; i <= 12; i++)

printf("%d: %d회 (%.3f%%)\n", i, sum[i], (float)sum[i] * 100 / 시행회수);

}


int roll_dice(void) {

int dice1, dice2;

dice1 = rand() % 6 + 1;

dice2 = rand() % 6 + 1;

ar_dice1[dice1-1]++;

ar_dice2[dice2-1]++;

sum[dice1 + dice2]++;

}


결과(예시):


DICE 1 SUMMARY

1: 166469회 (16.647%)

2: 166873회 (16.687%)

3: 166482회 (16.648%)

4: 167126회 (16.713%)

5: 167062회 (16.706%)

6: 165988회 (16.599%)

DICE 2 SUMMARY

1: 166995회 (16.699%)

2: 166479회 (16.648%)

3: 166093회 (16.609%)

4: 166700회 (16.670%)

5: 166916회 (16.692%)

6: 166817회 (16.682%)


SUM SUMMARY

2: 27616회 (2.762%)

3: 55578회 (5.558%)

4: 83506회 (8.351%)

5: 111237회 (11.124%)

6: 138731회 (13.873%)

7: 166778회 (16.678%)

8: 139064회 (13.906%)

9: 110607회 (11.061%)

10: 83449회 (8.345%)

11: 55763회 (5.576%)

12: 27671회 (2.767%)

계속하려면 아무 키나 누르십시오 . . .


함수는 간단히 statement들을 묶고 이름을 붙인 것이다. C에서 함수는 반드시 값을 리턴하지 않아도 되고, argument가 있지 않아도 된다. 

함수는 하나의 작은 프로그램이고 각각 선언과 statement를 가진다. 함수는 재사용이 가능하기 때문에 중복된 코드를 줄일 수 있다.

이제까지는 main이라는 함수만 사용했지만, 다른 함수들을 사용하게 될 것이다.


9.1 Defining and Calling Functions


두 수의 평균을 구하는 함수 average를 정의하기. 이 정의는 여기에선 main함수 바깥에, main보다 앞서서 적도록 하자. 이유는 9.2에 나올것

double average(double a, double b)

{

return (a + b) / 2;

}

double: average 함수의 return type

identifiers a and b(parameters of the function): average함수가 불려질 때 제공되는 두 숫자. 각각의 parameter 앞에 type이 따로 따로 적혀 있어야 한다(생략 불가능).

{}로 묶인 부분: body. 함수의 실행되는 부분.


함수를 호출할 때에는, 함수의 이름을 쓰고, 뒤이어 괄호 안에 arguments를 적는다. 

 

Function Definitions

return-type function-name ( parameters )

{

declarations

statements

}

return type에 대해: 배열을 return할 수는 없으나, 그 외에 제한은 없다.

함수의 type을 void로 지정하는 것은 그 함수가 어떤 값을 return하지 않는다는 것을 뜻한다.

만약 return-type을 생략하면 C89는 int로 간주한다. C99에서는 생략을 허용하지 않는다. 하지만 VS2015에서는 C89 방법을 따르는 듯 하다.


Function Calls

average(x, y)

print_count(i)

print_pun()    // void function

괄호가 뒤에 붙지 않으면 - average, print_count 오류가 나지는 않지만 아무일도 일어나지 않는다. 함수 호출이 되지 않는다.

void function 뒤에는 항상 세미콜론(;)을 붙여 statement로 만든다. 다른 type의 function은 어떤 값이 만들어지기 때문에 다른 변수에 넣거나, 프린트하거나 등등으로 사용할 수 있다. 

non-void function 뒤에 세미콜론이 붙어 statement가 되면, 어떤 값이 만들어지지만 쓰이지 않고 버려진다.

함수의 값이 버려진다는 것을 명확히 나타내기 위해서 함수 호출 앞에 (void) 라고 붙여줄 수 있다. "casting to void" = "throwing away"

서로 다른 함수 안에서 같은 이름의 변수가 선언될 때, 각각의 변수는 서로 다른 메모리에 저장되므로 서로의 값에 영향을 끼치지 않는다.

함수 내에 return statment가 여러개 올 수 있다. 그러나 그 중 단 하나만 실행 가능하다. 함수 내에서  return statement가 실행되면 함수는 그 값을 return하고 끝낸다.


9.2 Function Declarations


앞의 예에서 순서를 바꿔, main 함수가 앞에 오고, main 안에서 쓰이는 average 함수가 뒤에 오도록 배치해 보자. main에서는 그 함수에 대한 정보가 없다. 이 경우 컴파일러는 그 함수가 int value를 return한다고 간주한다. 이를 implicit declaration이라고 함. 그러나 사실은 average 함수의 return value는 double이기 때문에, 결국 오류가 발생한다. main 함수를 앞에 배치하면서 이같은 오류를 방지하기 위해 function declaration이 필요하다. 함수를 호출하기 전에 선언(declare)만 해 주면서 콤파일러에 그 함수의 간략한 정보를 미리 전달하는 것.

return-type function-name ( parameters ) ;

이 형태의 function declaration을 function prototype이라고 부른다. 이 용어는 옛 버전의 C에서 사용했던 스타일 - 괄호 안을 비워놓음 - 과 구분하기 위함. 

parameter의 경우에는, name은 생략하고 type만 입력해도 된다. 

ex) double average(double, double);



9.3 Arguments


parameters: 함수를 정의할 때 사용

arguments: 함수를 호출할 때 사용

두 용어의 차이에 대한 설명: http://ohgyun.com/410

의미를 엄격히 구분해서 사용하지는 않고, argument를 두 가지 모두의 의미로 사용하기도 함.

arguments are passed by value. 값이 복사되어 전달되며, 함수 내부에서 그 값이 바뀌더라도 argument의 복사된 값이 바뀌는 것이고 그 변수 자체는 바뀌지 않음. 

예를 들어 x라는 변수를 함수의 argument로 쓰고, 함수 내부에서 그 값을 바꿔버려도, 원래 x의 값은 변하지 않고 그대로 있다. 

Argument Conversions

parameters의 type과 맞지 않는 type이 arguments로 사용되는 것을 허용한다. 이 때 arguments를 형변환하게 되는데 규칙은 함수 호출에 앞서 prototype이나 정의가 있었는지 없었는지에 따라 다르다.

-호출에 앞서 prototype이 있었던 경우

각 arguments의 값은 parameters의 type에 맞게 암시적 형변환된다. 

-호출에 앞서 prototype이 없는 경우(이 방법은 안전하지 않다)

default argument promotion이 이루어짐. float는 double로, char나 short는 int로(integral promotion). C99에서는 integer promotion


Array Argmuments

배열은 종종 arguments로 사용된다. 배열의 parameter가 1차원 배열인 경우, 배열의 길이는 지정되지 않아도 되며 보통 지정하지 않는다.

int f(int a[])

그러나 배열의 길이를 전달은 해야 하는데 

int f(int a[], int n)과 같이 두번째 파라미터로 배열의 길이를 지정해 준다.

다차원 배열의 경우에는, 첫번째 차원의 길이만 생략 가능하고 나머지는 지정해야 한다.


#define LEN 10

int sum_two_dimensional_array(int a[][LEN], int n) {

...

}


9.4 The return Statement


non-void function의 경우 반드시 return statement를 통해 어떤 값이 return 되어야 한다.

return expression ;

return n >= 0 ? n : 0;

이 statement는 n이 0보다 크거나 같으면 n을, 아니면 0을 리턴한다.

void function에서는

return;

이렇게 뒤에 아무런 expression 없이 나타나기도 한다.



9.5 Program Termination


The exit Function

main 함수 내에 return statement를 넣는 것 외에 프로그램을 종료시키는 방법에는 exit 함수를 호출하는 것이 있다(stdlib.h에 속함). exit에 전달된 argument는 main의 return value와 동일한 의미(종료 시점의 프로그램 상태)이다. 0(=EXIT_SUCCESS)은 정상적인 종료를, 1(=EXIT_FAILURE)은 비정상적인 종료를 의미한다.


exit(EXIT_SUCCESS);    /* normal termination */

exit(EXIT_FAILURE);    /* abnormal termination */

return expression;

exit(expression);    /* main안에서는 두 statement는 동일하다.


9.6 Recursion


자기 자신을 호출하는 함수를 재귀적(recursive)이라고 한다. factorial이나 power를 구하는 함수에서 이용할 수 있다. 어떤 언어는 recursion에게 굉장히 의존하고, 허용조차 하지 않는 언어도 있는데 C언어는 그 중간 쯤에 있다.


/* function computes factorial of n recursively */

int fact(int n)

{

if (n <= 1)

return 1;

else

return n * fact(n - 1);

}


/* power function using conditional expression */

int power(int x, int n)

{

return n == 0 ? 1 : x * power(x, n - 1);

}


The Quicksort Algorithm

사실 위에 예를 든 두 함수는 재귀가 굳이 필요하지 않다. 재귀는 자기 자신을 두번 혹은 그 이상 호출하는 복잡한 알고리즘에서 매우 유용하게 쓰인다.

divide-and-conquer: 큰 문제를 같은 알고리즘이 적용되는 작은 조각으로 나누어 해결하는 것. 가장 고전적인 예로 Quicksort algorithm이 있다.

1부터 n까지의 index가 있는 배열을 quicksort하는 방법:

1. 배열의 원소 e(partitioning element, 칸막이 원소?)를 선택하고, 원소 1, 2, ..., i-1 은 e보다 같거나 작고, 원소 i, i+1, ..., n은 e보다 크거나 같도록 배열을 재배열한다.

2. 1, ..., i-1까지의 원소를 Quicksort로 재귀적으로 sort한다.

3. i, ..., n까지의 원소를 Quicksort로 재귀적으로 sort한다.






지금까지 다뤄온 변수들은 모두 scalar로, 하나의 아이템만을 가진 변수들. 여기서는 변수들의 집단(aggregate)을 다룸.

C에서의 변수 집단은 두 가지로 나눌 수 있음. array(배열), structure(구조체)

8.1 - 1차원 배열

8.2 - 다차원 배열

8.3 - variable-length arrays (C99)


8.1 One-Dimensional Arrays


array: a data structure containing a number of data values, all of wich have the same type.

these values ,called elements, can be individually selected by their position within the array.

int type의 원소 10개를 가진 배열 a를 선언

int a[10];


배열의 크기를 나중에 바꾸게 될 때를 대비해 배열의 크기를 macro로 선언하는 것도 좋은 방법.

#define N 10

int a[N];


Array Subscripting

배열의 각 원소에 접근하기 위해 배열 이름 뒤에 [i] 를 붙이는 것. susbscripting 또는 indexing이라 부른다.

a가 10개의 원소를 가진 배열이라면, a[0], a[1], ..., a[9]

a[i]는 lvalue이므로, 보통의 변수들과 같이 사용 가능하다.

a[0] = 1;

printf("%d\n", a[5]);

++a[i];



Array Initialization

array를 선언함과 동시에 값을 지정해 주는 것

가장 흔한 형태

int a[5] = {1, 2, 3, 4, 5};


initializer가 array보다 짧으면 남은 자리에 모두 0이 들어간다.

int a[5] = {1, 2, 3};    /* initial value of a is {1, 2, 3, 0, 0} */


쉽게 모든 원소를 0으로 하는 배열을 만들 수 있다.

int a[5] = {0};    /* initial value of a is {0, 0, 0, 0, 0} */


initializer가 있으면 배열의 길이는 생략 가능하다.

int a[] = {1, 2, 3, 4, 5};


designated initializers (C99)

int a[5] = {[0] = 1, [2] = 3, [4] = 5};    /* initial value of a is {1, 0, 3, 0, 5} */

int a[] = {[5] = 3, [19] = 20};    /* length of a is 20 */



Using the sizeof Operator with Arrays

sizeof 연산자는 배열의 크기도 알려준다. 단 원소의 갯수가 아닌 byte 기준으로

a가 10개의 int가 있는 배열이라면, sizeof(a)는 보통 40

배열에 몇 개의 원소가 들어있는지 알기 위해서는 하나의 원소를 저장하는 데 필요한 bytes 로 나누면 된다.

sizeof(a)/sizeof(a[0]);    /* 배열의 길이 */


또는 macro를 이용(나중에 macro에 parameter를 넣으면 유용해진다)

#define SIZE ((int) (sizeof(a) / sizeof(a[0])))

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

a[i] = 0;




8.2 Multidimensional Arrays


이차원 배열을 2차원의 표처럼 visualize하지만, 실제로 메모리에 저장되는 방식은 row-marjor order에 따른다.

가장 처음에 row 0이 저장되고, row 1이 그 다음으로 저장되는 식이다.

m[5][9]이라는 배열이 있을 때, 아래와 같이 저장된다.




Initializing a Multidimensional Array

int m[3][4] = { {0,1,2,3},

{4,5,6,7},

{8,9,0,1} };


initializer가 다차원 배열을 채우기에 충분하지 못한 경우, 나머지 공간의 원소는 0이 된다.

int m[3][4] = { {0,1,2,3},

 {4,5,6,7}};    // 마지막 행은 {0,0,0,0}이 된다.


안쪽 리스트가 한 행을 채우기에 충분하지 못하면, 그 행의 나머지 원소는 0이 된다.

int m[3][4] = { {0,1,2,3},

 {4,5,6},       // 마지막 원소는 0으로 채워짐

 {7,8,9,0}};


내부의 괄호를 생략해도 된다

int m[3][4] = {0,1,2,3,

4,5,6,7,

8,9,0,1};


1차원 배열과 같이 designated initializer 가능(C99)

double ident[2][2] = {[0][0] = 1.0, [1][1] = 1.0};    // 지정하지 않은 자리의 원소는 0이다.


Constant Arrays

array 선언시 const라는 글자를 맨 앞에 써주면 원소들은 constant가 되어 수정할 수 없게 된다.

const char hex_chars[]


8.3 Variable-Length Arrays(C99)


가변 길이 배열, 줄여서 VLA

때로는 배열의 길이를 constant로 지정하지 않고 선언하는것이 가능하다.

int n;

printf("Input length of the array: ");

scanf("%d", &n);

int a[n];

위 코드에서 배열 a의 길이는 프로그램이 컴파일될 때가 아니고 실행될 때 결정된다.

VLA의 제한은 static storage duration을 가질 수 없는 것, initializer를 가질 수 없다는 것이다.

보통 main이 아닌 다른 함수에서 많이 쓰인다.



7.4 Type Conversion


컴퓨터는 엄격해서 16-bit integer와 32-bit integer 간의 연산도 허용하지 않는다. 

C는 서로 다른 type끼리의 연산을 허용한다. integer, floating-point numbers, characters

implicit conversion: 프로그래머의 관여 없이 컴파일러에서 자동적으로 변환하는 것.

다음의 경우에 implicit conversion이 이루어진다.

1. 산술 연산이나 논리연산의 피연산자(operand)가 같은 형이 아닌 경우(usual arithmetic conversions)

2. assign하는 변수(좌변)와 우변의 표현식의 type이 다를 때

3. 함수를 호출할 때 argument가 해당하는 parameter와 type이 다를 때

4. 함수의 return type과 return statement에 있는 expression의 type이 다른 경우

여기서는 앞의 두가지 경우를 다루고, 나머지는 9장에서 다룬다.


The Usual Arithmetic Conversions

binary, arithmetic, relational, equality operation에서 피연산자들의 type이 다를 때

"narrow": 거칠게 표현해서, A type을 저장하는 데 B type을 저장하는 것 보다 더 적은 바이트가 필요하다면, A type이 B type보다 더  narrow하다고 한다.

promotion: 서로 다른 두 type이 피연산자일 때, 더 narrow한 type을 다른 type으로 변환하는 것. 더 적은 범위를 표현하는 type을 더 넓은 범위의 type으로 변환

integral promotion: char, short int 를 int로 변환


promotion 단계

두 타입중 하나가 floating type인 경우

float -> double -> long double

두 타입 모두 floating type이 아닌 경우

int -> unsigned int -> long int -> unsigned long int


int가 unsigned int로 변환되는 것에 유의하자. unsigned int 10과 int -10을 비교할 때 -10은 unsigned int로 promote되면서 4,294,967,296이 더해지면서(4,294,967,295가 unsigned int의 가장 큰 값) 이상한 값이 나온다. unsigned integer는 최대한 사용하지 않는 것이 좋고, 만약에 쓰게 된다면 signed integer과 엮이는 일이 절대 없어야 한다.


int + char = int

int + short = int

unsigned int + int = unsigned int

long + unsigned int = long

float + unsigned long = float

double + float = double

long double + double = long double


Conversion During Assignment

assignment에 있어서는 arithmetic conversion이 아닌 더 단순한 룰이 적용된다.

우변에 있는 표현식의 type이 좌변의 변수와 같은 type으로 변환된다.

int = char -> 우변이 int로 변환

double = float -> 우변이 double로 변환

int i;

i = 842.97 -> 우변 842가 int로 변환되어 i = 842가 된다.

우변의 type을 더 narrow하게 변환해야 하는 경우, 의미없는 결과가 나올 수 있다

ex) i = 1.0e20;

부동 소수점 숫자를 입력하는 경우 기본적으로 double type이다. 만약 float으로 저장하고 싶다면, 뒤에 소문자 f를 붙인다.

3.14159 -> double

3.14159f -> float


C99에서의 usual arithmetic conversion에 대한 설명은 생략.


Casting

컴파일러에서 암시적으로 하는 것이 아닌, 프로그래머가 명시적으로 형변환을 하는 것

( type-name ) expression


floating type의 소수점 구하기

float f, frac_part;

frac_part = f - (int) f;


컴파일러의 기존 규칙과 다른 규칙을 적용하고 싶을 때

float q;

int a, b;

a = 3, b = 2;

q = a/b;    // q는 float이지만 1.5가 아닌 1.0이다.

q = (float) a/b;    // q = 1.5


오버플로우를 피하기 위해

long long int i;

int j;

j = 1000000;

i = (long long int) j * j;    

// overflow가 일어나지 않도록, int의 범위를 벗어나는 결과가 예상되면 casting해줌

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


( type name ) 은 unary operator임. binary operator보다 더 우선순위가 빠르다. ( long ) i * j; 는 i를 long으로 변환시킨 후에 j와 곱한다.


7.5 Type Definitions


typedef int Bool;

Bool flag; /* same as int flag; */

Bool이라는 type을 정의하고, flag라는 변수를 Bool type으로 선언. 사실 int로 선언한 것과 다르지 않다.


Advantages of Type Definitions

이름을 잘 지으면 더 이해하기 쉬운 코드를 짤 수 있다. 또 나중에 수정하기가 쉽다

ex) typedef float Dollars;    // 돈에 관한 변수임을 알기 쉽고, 만약 float 대신 double이 필요하다면 모둔 변수의 선언 부분 대신 typedef 부분만 수정하면 됨


Type Definitions and Portability

같은 이름의 type이 서로 다른 시스템에선 다른 범위를 나타낼 수 있기 때문에, typedef를 이용해 type의 범위를 지정하는 것이 코드의 portability에 도움이 될 수 있다.

C99에서는 <stdint.h>를 이용해 변수에 몇 비트를 할당할 것인지 지정해 줄 수 있다. 예를 들어 int32_t는 32bit의 signed integer를 의미한다. 

typedef int32_t Int32; // 새로운 32비트의 signed integer 'Int32' type 정의


7.6 The sizeof Operator


타입을 저장하려면 메모리가 얼마나 필요한가?

sizeof ( type-name )


이 결과는 type-name(type의 이름일 수도 있고, 변수일 수도 있다)를 저장하는 데 몇 바이트가 필요한지 를 나타내는 양의 정수이다. sizeof의 type은 size_t라는 unsigned 정수형의 type이다. 괄호 안에 char를 넣으면 1, int는 (보통) 4. 이 값은 unsigned long의 범위를 벗어날 수도 있다. 그러나 casting을 해주지 않고도 overflow 걱정 없이 값을 나타낼 수 있다.

printf("Size of int: %zu\n", sizeof(int));    // C99 only


--추가

hexadecimal floating constants의 장점: 10진수 형태로 나타낸 소수는 2진수 형태로 변환될 때 나타나는 rounding error에 영향받기 쉽지만, hexadecimal 의 경우에는 그렇지 않다. 따라서 더 정확한 표현이 가능하다. 특히 숫자가 작을수록.



+ Recent posts