C언어 - 코드 분석 연습 (중급 예제)

1. C언어 코드 분석 중급 예제

1.1. 포인터의 크기와 길이

  • 문자열 리터럴을 가리키는 문자 포인터의 동작을 이해한다.
  • sizeofstrlen서로 다른 기준으로 값을 계산한다는 것을 확인한다.
#include <stdio.h>
#include <string.h>

int main(void) {
    char* text = "0123456789";

    size_t result = sizeof(text) + strlen(text);
    printf("%zu\n", result);

    return 0;
}
  • char *text는 문자열 내용을 저장하는 배열이 아니라 문자열 리터럴의 시작 주소를 저장하는 포인터이다.
  • sizeof(text)는 포인터 변수 자체의 크기이며, 가리키는 문자열 길이와 다르다.
  • strlen(text)는 포인터가 가리키는 위치부터 '\0'을 만나기 전까지의 문자 수를 센다.
  • 포인터 크기는 실행 환경에 따라 달라질 수 있으며, 64비트 환경을 기준으로 설명한다.

1.2. 포인터 산술

  • 배열 이름은 배열의 첫 번째 원소 주소를 나타내는 값처럼 동작하기 때문에 배열 이름을 포인터처럼 사용할 수 있다.
  • 포인터에 +1을 하면 단순히 1만큼 이동하는 것이 아니라, 자료형 크기만큼 이동하여 다음 배열 원소를 가리키게 된다.
  • 즉, 배열 이름은 시작 주소 역할을 하며, 포인터 연산을 하면 배열의 다음 칸으로 이동하는 것과 같은 의미이다.
#include <stdio.h>

int main(void) {
    int nums[] = {4, 8, 15, 16, 23, 42};
    int *p = nums + 1;

    int result = *(p + 2) + p[-1];
    printf("%d\n", result);

    return 0;
}
  • nums + 1은 배열의 두 번째 원소 주소를 의미한다.
  • *(p + n)p[n]은 같은 방식으로 해석된다.
  • p[-1]처럼 음수 인덱스도 현재 포인터 위치를 기준으로 계산된다.

1.3. 포인터 함수 인자

  • C 언어에서 함수 인자는 값에 의해 전달된다는 사실을 이해한다.
  • 포인터를 인자로 전달할 경우 호출자의 변수를 변경할 수 있는 원리를 이해한다.
  • 값으로 전달된 변수와 주소로 전달된 변수가 함수 호출 이후 어떻게 달라지는지 비교한다.
#include <stdio.h>

static void update(int *left, int right) {
    right += *left;
    *left = right - (*left / 2);
}

int main(void) {
    int x = 8;
    int y = 3;

    update(&x, y);

    printf("%d\n", x + y);
    return 0;
}
  • &x는 변수 x의 주소이고, 함수 안에서 *left로 원본 x에 접근할 수 있다.
  • 일반 정수 인자로 받은 값은 함수 내부에서 바뀌어도 호출자의 변수에는 영향을 주지 않는다.

1.4. 이중 포인터 할당

  • 함수 내부에서 동적 할당한 메모리의 주소를 호출자에게 전달하는 방식을 이해한다.
  • char **를 사용하여 호출자의 char * 변수를 변경하는 흐름을 이해한다.
  • 할당된 버퍼에 문자열을 복사하고, 사용 후 해제하는 과정을 추적한다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int make_copy(char **out, const char *src) {
    size_t len = strlen(src);

    *out = malloc(len + 1);
    if (*out == NULL) {
        return -1;
    }

    memcpy(*out, src, len + 1);
    (*out)[0] = 'A';

    return (int)len;
}

int main(void) {
    char *buff = NULL;

    int result = make_copy(&buff, "abcde");
    if (result < 0) {
        return 1;
    }

    printf("%s,%d\n", buff, result);

    free(buff);
    return 0;
}
  • &buff는 포인터 변수 buff 자체의 주소이며, char ** 매개변수로 받을 수 있다.
  • *out = malloc(...)은 호출자의 buff가 새로 할당된 메모리를 가리키게 한다.
  • strlen(src) + 1만큼 할당해야 널 문자까지 복사할 수 있다.
  • memcpy(..., len + 1)은 문자열 본문과 마지막 '\0'을 함께 복사한다.
  • (*out)[0]은 할당된 문자열의 첫 번째 문자에 접근한다.

1.5. 버퍼 크기와 문자열 누적

  • 기존 문자열 뒤에 새 문자열을 덧붙일 때, 남은 버퍼 공간을 계산하는 방법을 이해한다.
  • 이어 붙이기 함수가 남은 공간 범위 내에서만 복사되는 동작을 이해한다.
  • 여러 번의 함수 호출이 동일한 버퍼 상태를 누적하여 변경한다는 점을 추적한다.
#include <stdio.h>
#include <string.h>

static int append_limited(char *dst, size_t dst_size, const char *src) {
    size_t dst_len = strlen(dst);
    size_t src_len = strlen(src);

    if (dst_len + src_len >= dst_size) {
        size_t room = dst_size - dst_len - 1;

        memcpy(dst + dst_len, src, room);
        dst[dst_size - 1] = '\0';
        return -1;
    }

    memcpy(dst + dst_len, src, src_len + 1);
    return 0;
}

int main(void) {
    char buffer[10] = "abcd";

    append_limited(buffer, sizeof(buffer), "efgh");
    append_limited(buffer, sizeof(buffer), "ijkl");

    printf("%s\n", buffer);
    return 0;
}
  • 문자열을 뒤에 덧붙일 때는 현재 문자열 길이와 전체 버퍼 크기를 계산하여 버퍼를 넘지 않도록 해야 한다.
  • 이때 문자열의 끝을 나타내는 널 문자('\0')를 저장할 공간도 반드시 남겨야 한다.
  • 문자열을 여러 번 이어붙이면 이전 결과를 기준으로 계속 누적된다.
  • 그리고, 실제로 붙이는 위치는 dst + dst_len으로 표현되는 현재 문자열의 끝 지점을 가리키게 된다.