Skip to main content

포인터와 배열

Views 17약 10분생성형 AI 활용
Summary

배열 이름은 왜 주소처럼 보일까?

앞 장에서 우리는 포인터를 배웠습니다. 포인터는 주소를 저장하는 변수였고, &는 변수의 주소를 얻는 연산자였으며, *는 포인터가 가리키는 곳의 값을 읽거나 쓰는 연산자였습니다.

이제 그 포인터를 배열과 연결해 보겠습니다. 사실 C에서 배열을 깊이 이해하려면 포인터 감각이 꼭 필요합니다. 배열은 메모리에 연속해서 놓이고, 포인터는 메모리 주소를 다룹니다. 둘은 자연스럽게 만날 수밖에 없습니다.

앞에서 이런 코드를 보았습니다.

int scores[5] = {90, 85, 72, 100, 64};

이 배열은 int 값 다섯 개를 메모리에 나란히 저장합니다.

scores[0]  scores[1]  scores[2]  scores[3]  scores[4]
   90         85         72        100         64

그런데 C에서 배열 이름 scores는 많은 상황에서 배열의 첫 번째 원소 주소처럼 동작합니다.

printf("%pn", (void *)scores);
printf("%pn", (void *)&scores[0]);

두 줄은 보통 같은 주소를 출력합니다. scores가 배열의 시작 위치, 즉 scores[0]의 주소를 가리키는 것처럼 사용되기 때문입니다.

여기서 중요한 질문이 생깁니다.

"그렇다면 배열은 포인터와 같은 것일까?"

답은 조심스럽게 말해야 합니다. 배열과 포인터는 같지 않습니다. 다만 배열 이름은 많은 표현식에서 첫 번째 원소를 가리키는 포인터로 변환됩니다. 이 차이를 이해하는 것이 이번 장의 핵심입니다.

포인터와 배열

배열은 연속된 메모리다

배열을 다시 메모리 관점으로 보겠습니다.

int numbers[4] = {10, 20, 30, 40};

int 하나가 4바이트인 환경이라고 가정하면, 배열은 대략 다음처럼 놓입니다. 실제 int 크기는 환경에 따라 달라질 수 있으므로 정확한 크기는 sizeof(int)로 확인해야 합니다.

주소        값
0x1000    numbers[0] = 10
0x1004    numbers[1] = 20
0x1008    numbers[2] = 30
0x100C    numbers[3] = 40

첫 번째 원소와 두 번째 원소 사이가 1바이트가 아니라 sizeof(int)만큼 떨어져 있다는 점이 중요합니다. 배열의 원소들은 같은 자료형이므로, 컴퓨터는 시작 주소와 원소 크기만 알면 원하는 인덱스의 위치를 계산할 수 있습니다.

numbers[i]의 위치 = 배열 시작 주소 + sizeof(int) × i

그래서 배열 인덱스는 0부터 시작했습니다. numbers[0]은 시작 주소에서 0칸 떨어진 곳이고, numbers[1]은 시작 주소에서 1칸 떨어진 곳입니다.

이 구조를 코드로 확인해 봅시다.

#include <stdio.h>

int main(void)
{
    int numbers[4] = {10, 20, 30, 40};

    for (int i = 0; i < 4; i++) {
        printf("&numbers[%d] = %pn", i, (void *)&numbers[i]);
    }

    return 0;
}

출력되는 주소 값은 실행 환경마다 다르지만, 각 주소가 일정한 간격으로 증가하는 것을 볼 수 있습니다. 그 간격은 보통 sizeof(int)와 같습니다.

배열 이름과 첫 번째 원소의 주소

배열 이름은 많은 상황에서 첫 번째 원소의 주소로 변환됩니다.

#include <stdio.h>

int main(void)
{
    int numbers[4] = {10, 20, 30, 40};

    printf("numbers      = %pn", (void *)numbers);
    printf("&numbers[0]  = %pn", (void *)&numbers[0]);

    return 0;
}

대부분의 환경에서 두 주소는 같습니다.

numbers      = 0x...
&numbers[0]  = 0x...

따라서 다음과 같이 포인터에 배열 이름을 저장할 수 있습니다.

int numbers[4] = {10, 20, 30, 40};
int *p = numbers;

이 코드는 다음과 같은 의미입니다.

int *p = &numbers[0];

p는 numbers[0]을 가리킵니다.

p ─────→ numbers[0]  numbers[1]  numbers[2]  numbers[3]
          10          20          30          40

그래서 *p는 첫 번째 원소의 값입니다.

printf("%dn", *p);  // 10

여기까지 보면 배열 이름과 포인터가 완전히 같은 것처럼 보일 수 있습니다. 하지만 곧 차이가 드러납니다.

배열과 포인터는 같지 않다

다음 코드를 봅시다.

#include <stdio.h>

int main(void)
{
    int numbers[4] = {10, 20, 30, 40};
    int *p = numbers;

    printf("sizeof(numbers) = %zun", sizeof(numbers));
    printf("sizeof(p)       = %zun", sizeof(p));

    return 0;
}

sizeof(numbers)는 배열 전체의 크기입니다. int가 4바이트라면 4 × 4 = 16바이트가 나올 수 있습니다.

반면 sizeof(p)는 포인터 변수 하나의 크기입니다. 64비트 환경에서는 보통 8바이트, 32비트 환경에서는 보통 4바이트일 수 있습니다.

즉, numbers는 실제 배열이고, p는 그 배열의 첫 번째 원소 주소를 저장한 포인터 변수입니다. 둘은 메모리에서 차지하는 의미가 다릅니다.

또 하나의 차이도 있습니다.

int numbers[4] = {10, 20, 30, 40};
int other[4] = {1, 2, 3, 4};
int *p = numbers;

p = other;       // 가능: p가 다른 배열의 첫 원소를 가리키게 함
// numbers = other; // 오류: 배열 이름에는 대입할 수 없음

포인터 변수 p에는 다른 주소를 다시 저장할 수 있습니다. 하지만 배열 이름 numbers는 배열 전체를 대표하는 이름이지, 값을 바꿔 넣을 수 있는 포인터 변수가 아닙니다.

정리하면 이렇게 볼 수 있습니다.

  • 배열은 실제 원소들이 연속해서 놓인 공간입니다.

  • 포인터는 주소를 저장하는 변수입니다.

  • 배열 이름은 많은 표현식에서 첫 번째 원소의 주소처럼 변환됩니다.

  • 하지만 배열 자체와 포인터 변수는 같은 것이 아닙니다.

이 차이를 기억해 두면 sizeof, 함수 매개변수, 문자열 처리에서 헷갈림이 줄어듭니다.

포인터 연산: 한 바이트가 아니라 한 원소씩 이동한다

포인터에는 덧셈과 뺄셈을 할 수 있습니다. 이것을 포인터 연산이라고 합니다.

int numbers[4] = {10, 20, 30, 40};
int *p = numbers;

이때 p는 numbers[0]을 가리킵니다.

printf("%dn", *p);       // 10
printf("%dn", *(p + 1)); // 20
printf("%dn", *(p + 2)); // 30

여기서 p + 1은 주소 값을 숫자 1만큼 더한다는 뜻이 아닙니다. p가 int *이므로, p + 1은 int 한 칸 뒤로 이동합니다. 만약 int가 4바이트라면 실제 주소는 4바이트 뒤로 이동합니다.

p        → numbers[0]
p + 1    → numbers[1]
p + 2    → numbers[2]

C는 포인터의 자료형을 보고 몇 바이트씩 이동해야 하는지 압니다.

char chars[4] = {'A', 'B', 'C', 'D'};
char *cp = chars;

char는 보통 1바이트이므로 cp + 1은 보통 1바이트 뒤를 가리킵니다. 반면 double * 포인터라면 p + 1은 double 한 칸, 보통 8바이트 뒤를 가리킬 수 있습니다.

이것이 포인터 선언에서 자료형이 중요한 이유입니다.

int *p;

이 선언은 단순히 "p는 주소를 저장한다"만 말하지 않습니다. "p가 가리키는 곳에는 int가 있고, p를 한 칸 이동하면 int 크기만큼 이동한다"는 정보도 담고 있습니다.

arr[i]와 *(arr + i)

C에서 배열 인덱스 표현은 포인터 연산과 깊이 연결되어 있습니다.

numbers[i]

이 표현은 개념적으로 다음과 비슷합니다.

*(numbers + i)

배열 이름 numbers가 첫 번째 원소 주소처럼 사용되고, 거기에 i칸 이동한 뒤, *로 그 위치의 값을 읽는 것입니다.

#include <stdio.h>

int main(void)
{
    int numbers[4] = {10, 20, 30, 40};

    for (int i = 0; i < 4; i++) {
        printf("numbers[%d] = %d, *(numbers + %d) = %dn",
               i, numbers[i], i, *(numbers + i));
    }

    return 0;
}

출력해 보면 numbers[i]와 *(numbers + i)가 같은 값을 가리킨다는 것을 확인할 수 있습니다.

그렇다고 평소에 *(numbers + i)를 더 많이 쓰라는 뜻은 아닙니다. 배열을 다룰 때는 numbers[i]가 훨씬 읽기 쉽습니다. 다만 내부적으로 이런 관계가 있다는 사실을 알면 포인터와 배열이 왜 자주 함께 등장하는지 이해할 수 있습니다.

포인터 변수도 배열처럼 사용할 수 있습니다.

int numbers[4] = {10, 20, 30, 40};
int *p = numbers;

printf("%dn", p[0]); // 10
printf("%dn", p[1]); // 20
printf("%dn", p[2]); // 30

p[1]은 *(p + 1)과 같은 의미입니다. 그래서 동적 메모리 할당으로 얻은 포인터도 배열처럼 사용할 수 있었습니다.

int *scores = malloc(sizeof(int) * count);
scores[0] = 90;
scores[1] = 85;

scores는 배열 이름은 아니지만, 연속된 int 공간의 시작 주소를 가리키는 포인터이므로 scores[i]처럼 사용할 수 있습니다.

포인터로 배열 순회하기

배열은 보통 인덱스로 순회합니다.

int numbers[4] = {10, 20, 30, 40};

for (int i = 0; i < 4; i++) {
    printf("%dn", numbers[i]);
}

포인터로도 순회할 수 있습니다.

#include <stdio.h>

int main(void)
{
    int numbers[4] = {10, 20, 30, 40};
    int *p = numbers;

    for (int i = 0; i < 4; i++) {
        printf("%dn", *(p + i));
    }

    return 0;
}

또는 포인터 자체를 이동시킬 수도 있습니다.

#include <stdio.h>

int main(void)
{
    int numbers[4] = {10, 20, 30, 40};
    int *p = numbers;
    int *end = numbers + 4;

    while (p < end) {
        printf("%dn", *p);
        p++;
    }

    return 0;
}

end는 마지막 원소 바로 다음 위치를 가리킵니다. 이 위치를 역참조하면 안 됩니다. 하지만 반복의 끝을 표시하는 경계로는 사용할 수 있습니다.

numbers       → numbers[0]
numbers + 4   → 마지막 원소 다음 위치

이런 방식은 C 라이브러리나 시스템 코드에서 자주 볼 수 있습니다. 다만 초보 단계에서는 인덱스 방식이 더 읽기 쉬운 경우가 많습니다. 포인터 순회는 배열이 연속된 메모리라는 사실을 이해하기 위한 도구로 먼저 받아들이면 됩니다.

배열을 함수에 전달하면 무슨 일이 생길까?

배열을 함수에 전달하는 코드를 다시 보겠습니다.

int sum_array(int arr[], int count)
{
    int sum = 0;

    for (int i = 0; i < count; i++) {
        sum += arr[i];
    }

    return sum;
}

매개변수에 int arr[]라고 적혀 있으니 배열 전체가 함수로 복사되는 것처럼 보일 수 있습니다. 하지만 그렇지 않습니다. C에서 배열을 함수에 전달하면 배열 전체가 복사되지 않습니다. 함수에는 배열의 첫 번째 원소 주소가 전달됩니다.

다음 두 선언은 함수 매개변수에서는 사실상 같은 의미입니다.

int sum_array(int arr[], int count);
int sum_array(int *arr, int count);

둘 다 arr은 int를 가리키는 포인터 매개변수입니다. 그래서 함수 안에서 sizeof(arr)를 사용하면 배열 전체 크기가 아니라 포인터 크기가 나옵니다.

#include <stdio.h>

void print_size(int arr[])
{
    printf("sizeof(arr) in function = %zun", sizeof(arr));
}

int main(void)
{
    int numbers[5] = {1, 2, 3, 4, 5};

    printf("sizeof(numbers) in main = %zun", sizeof(numbers));
    print_size(numbers);

    return 0;
}

main 안에서 sizeof(numbers)는 배열 전체 크기입니다. 하지만 print_size 안에서 sizeof(arr)는 포인터 크기입니다.

그래서 배열을 함수에 넘길 때는 원소 개수도 함께 넘겨야 합니다.

void print_array(int arr[], int count)
{
    for (int i = 0; i < count; i++) {
        printf("%dn", arr[i]);
    }
}

함수 안의 arr만 보고는 원래 배열에 원소가 몇 개인지 알 수 없습니다. C는 배열의 크기를 자동으로 함께 전달하지 않습니다. 이것이 C 배열을 사용할 때 중요한 책임 중 하나입니다.

함수에서 배열 원소 바꾸기

배열을 함수에 전달하면 첫 번째 원소 주소가 전달됩니다. 따라서 함수 안에서 배열 원소를 바꾸면 원본 배열도 바뀝니다.

#include <stdio.h>

void set_zero(int arr[], int count)
{
    for (int i = 0; i < count; i++) {
        arr[i] = 0;
    }
}

int main(void)
{
    int numbers[5] = {1, 2, 3, 4, 5};

    set_zero(numbers, 5);

    for (int i = 0; i < 5; i++) {
        printf("%d ", numbers[i]);
    }
    printf("n");

    return 0;
}

출력은 다음과 같습니다.

0 0 0 0 0

set_zero 함수 안에서 바뀐 것은 복사본 배열이 아닙니다. 함수는 원본 배열의 시작 주소를 받아 그 주소를 통해 원소에 접근합니다. 그래서 원본 배열의 내용이 바뀝니다.

이 점은 함수의 일반 변수 전달과 다릅니다.

void change(int n)
{
    n = 100;
}

일반 int 값은 복사되어 전달되므로 함수 안에서 바꿔도 원본이 바뀌지 않습니다. 하지만 배열은 함수에 전달될 때 첫 원소 주소로 전달되므로, 원소를 수정하면 원본 배열에 영향을 줍니다.

원본을 바꾸지 않겠다는 의도를 표현하고 싶다면 const를 사용할 수 있습니다.

void print_array(const int arr[], int count)
{
    for (int i = 0; i < count; i++) {
        printf("%dn", arr[i]);
    }
}

const int arr[]는 함수 안에서 배열 원소를 바꾸지 않겠다는 약속입니다. 다음 코드는 컴파일 오류가 됩니다.

void print_array(const int arr[], int count)
{
    arr[0] = 10; // 오류
}

읽기만 하는 함수라면 const를 붙이는 습관이 좋습니다. C에서는 포인터를 통해 원본 값을 바꿀 수 있으므로, 바꾸지 않는다는 약속을 코드에 적어 주는 것이 중요합니다.

문자열과 포인터

C 문자열은 널 문자 0으로 끝나는 char 배열이라고 배웠습니다.

char name[] = "Mina";

메모리에는 다음처럼 저장됩니다.

'M' 'i' 'n' 'a' '0'

배열 이름 name은 많은 상황에서 첫 번째 문자 주소처럼 사용됩니다.

char *p = name;

이제 p는 문자열의 첫 문자를 가리킵니다.

printf("%cn", *p);       // M
printf("%cn", *(p + 1)); // i
printf("%sn", p);        // Mina

%s는 포인터가 가리키는 위치에서 시작해 0을 만날 때까지 문자를 출력합니다. 그래서 p를 넘겨도 문자열이 출력됩니다.

#include <stdio.h>

int main(void)
{
    char word[] = "Hello";
    char *p = word;

    printf("%sn", p);
    printf("%sn", p + 1);
    printf("%sn", p + 2);

    return 0;
}

출력은 다음과 같습니다.

Hello
ello
llo

p + 1은 두 번째 문자 e의 주소입니다. %s는 그 위치부터 0까지 출력하므로 ello가 나옵니다.

이 예제는 문자열 함수들이 왜 주소를 받는지 보여 줍니다. C에서 문자열은 내용 전체가 함수로 복사되어 전달되는 값이 아니라, 시작 주소와 0이라는 끝 표시로 다루는 데이터입니다.

문자 배열과 문자열 리터럴 포인터

다음 두 코드는 비슷해 보이지만 차이가 있습니다.

char a[] = "Hello";
char *p = "Hello";

a는 문자 배열입니다. 배열 안에는 H, e, l, l, o, 0이 복사되어 들어갑니다. 따라서 배열 원소를 바꿀 수 있습니다.

char a[] = "Hello";
a[0] = 'Y';
printf("%sn", a); // Yello

반면 p는 문자열 리터럴의 시작 주소를 가리키는 포인터입니다.

char *p = "Hello";

문자열 리터럴은 수정하면 안 됩니다. 다음 코드는 위험하며 정의되지 않은 동작입니다.

char *p = "Hello";
p[0] = 'Y';  // 잘못된 코드

그래서 문자열 리터럴을 가리킬 때는 보통 const char *를 사용하는 편이 좋습니다.

const char *p = "Hello";

이제 p[0] = 'Y'; 같은 실수를 컴파일러가 막아 줄 수 있습니다.

문자열을 수정해야 한다면 배열을 사용하세요.

char text[20] = "Hello";
text[0] = 'Y';

문자열을 읽기만 한다면 const char *를 사용하세요.

void print_message(const char *message)
{
    printf("%sn", message);
}

이 함수는 문자열 내용을 바꾸지 않겠다는 약속을 합니다. 문자열 리터럴도, 문자 배열도 안전하게 넘길 수 있습니다.

문자열 비교에서 ==가 위험한 이유

문자열 장에서 우리는 문자열 비교에 strcmp를 써야 한다고 배웠습니다. 이제 그 이유를 포인터 관점에서 볼 수 있습니다.

char a[] = "cat";
char b[] = "cat";

if (a == b) {
    printf("같습니다.n");
}

이 코드는 문자열 내용이 같은지 비교하지 않습니다. a와 b는 각각 다른 배열의 시작 주소처럼 사용됩니다. 따라서 a == b는 두 배열의 시작 주소가 같은지 비교합니다. 두 배열은 내용이 같아도 서로 다른 메모리 공간에 있으므로 보통 같지 않습니다.

문자열 내용 비교는 다음처럼 해야 합니다.

#include <string.h>

if (strcmp(a, b) == 0) {
    printf("내용이 같습니다.n");
}

strcmp는 두 문자열의 시작 주소를 받아서, 각 위치의 문자를 하나씩 비교합니다. 둘 다 0에 도달할 때까지 모든 문자가 같으면 0을 반환합니다.

이제 strcmp가 왜 포인터를 받는지도 이해할 수 있습니다. 문자열은 배열 전체가 통째로 전달되는 것이 아니라, 첫 문자의 주소를 통해 다루어지기 때문입니다.

이중 포인터 맛보기

포인터를 배우다 보면 int **pp 같은 표현도 만납니다. 이것을 이중 포인터라고 부릅니다. 말 그대로 포인터를 가리키는 포인터입니다.

처음에는 어렵게 느껴지지만 원리는 같습니다.

int value = 10;
int *p = &value;
int **pp = &p;

그림으로 보면 다음과 같습니다.

pp ─────→ p ─────→ value
          [주소]    [10]

p는 value의 주소를 저장합니다. pp는 포인터 변수 p 자신의 주소를 저장합니다.

printf("%dn", value);   // 10
printf("%dn", *p);      // 10
printf("%dn", pp);    // 10

*pp는 pp가 가리키는 곳, 즉 p입니다. pp는 p가 가리키는 곳, 즉 value입니다.

이중 포인터는 언제 필요할까요? 대표적으로 함수 안에서 포인터 변수 자체를 바꾸고 싶을 때 필요합니다.

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

int allocate_int(int **out)
{
    int *p = malloc(sizeof(int));

    if (p == NULL) {
        return 0;
    }

    *p = 100;
    *out = p;
    return 1;
}

int main(void)
{
    int *value = NULL;

    if (allocate_int(&value)) {
        printf("%dn", *value);
        free(value);
    }

    return 0;
}

allocate_int 함수는 main의 포인터 변수 value에 새 주소를 넣어야 합니다. 일반 변수 값을 바꾸려면 그 변수의 주소를 넘겼듯이, 포인터 변수 값을 바꾸려면 포인터 변수의 주소를 넘깁니다.

allocate_int(&value);

value의 자료형은 int *입니다. 따라서 &value의 자료형은 int **입니다.

지금은 이중 포인터를 완벽하게 익힐 필요는 없습니다. 다만 "포인터 자체도 변수이고, 그 변수의 주소도 가질 수 있다"는 사실을 기억해 두면 됩니다. 동적 배열을 함수 안에서 확장하거나, 연결 리스트의 머리 포인터를 바꿀 때 이 개념이 다시 등장합니다.

포인터 실수 모음

포인터와 배열이 만나는 지점에서는 실수가 자주 나옵니다. 대표적인 패턴을 정리해 봅시다.

첫 번째는 배열 범위를 넘는 포인터 연산입니다.

int arr[3] = {1, 2, 3};
int *p = arr + 3;

printf("%dn", *p); // 잘못된 접근

arr + 3은 마지막 원소 다음 위치입니다. 반복문의 경계로 사용할 수는 있지만, 역참조하면 안 됩니다.

두 번째는 함수 안에서 sizeof로 배열 크기를 구하려는 실수입니다.

void print_array(int arr[])
{
    size_t count = sizeof(arr) / sizeof(arr[0]); // 잘못된 생각
}

함수 매개변수 arr은 포인터입니다. 원래 배열 전체 크기를 알 수 없습니다. 배열 크기는 별도 인자로 받아야 합니다.

void print_array(int arr[], int count)
{
    for (int i = 0; i < count; i++) {
        printf("%dn", arr[i]);
    }
}

세 번째는 문자열 리터럴을 수정하려는 실수입니다.

char *p = "Hello";
p[0] = 'Y'; // 위험

수정할 문자열은 배열로 만들어야 합니다.

char text[] = "Hello";
text[0] = 'Y';

읽기 전용으로 사용할 문자열 리터럴은 const char *로 받는 편이 좋습니다.

const char *message = "Hello";

네 번째는 포인터가 가리키는 대상의 수명을 잊는 것입니다.

int *bad(void)
{
    int local = 10;
    return &local; // 위험
}

local은 함수가 끝나면 사라지는 지역 변수입니다. 그 주소를 밖으로 반환하면 더 이상 유효하지 않은 주소를 들고 있게 됩니다.

다섯 번째는 포인터와 포인터가 가리키는 값을 혼동하는 것입니다.

p = &arr[0];  // p에 주소를 저장
*p = 10;      // p가 가리키는 곳의 값을 변경

p는 주소이고, *p는 그 주소에 있는 값입니다. 이 둘을 계속 구분해야 합니다.

배열과 포인터를 읽는 질문

배열과 포인터 코드를 읽을 때는 다음 질문을 차례대로 던져 보세요.

  • 이 이름은 실제 배열인가, 포인터 변수인가?

  • 이 포인터는 어떤 자료형의 원소를 가리키는가?

  • p + 1은 몇 바이트가 아니라 몇 원소 이동하는가?

  • 이 포인터를 역참조해도 유효한 범위 안인가?

  • 함수에 배열을 넘길 때 원소 개수도 함께 넘겼는가?

  • 이 문자열은 수정 가능한 배열인가, 읽기 전용 문자열 리터럴인가?

이 질문들은 조금 귀찮아 보입니다. 하지만 C에서 메모리 오류를 줄이는 가장 좋은 방법은 질문을 습관으로 만드는 것입니다. 포인터는 "주소를 따라가는 도구"입니다. 주소를 따라가기 전에 그 주소가 어디를 가리키는지 확인해야 합니다.

이번 장에서 기억할 것

이번 장에서는 포인터와 배열의 관계를 살펴보았습니다. 배열은 같은 자료형의 값이 연속된 메모리에 놓인 구조이고, 배열 이름은 많은 상황에서 첫 번째 원소의 주소처럼 사용됩니다.

핵심을 정리해 봅시다.

  • 배열 이름은 많은 표현식에서 &배열[0]처럼 첫 번째 원소 주소로 변환됩니다.

  • 배열 자체와 포인터 변수는 같지 않습니다. sizeof와 대입 가능 여부에서 차이가 드러납니다.

  • 포인터 연산에서 p + 1은 한 바이트가 아니라 p가 가리키는 자료형 한 칸만큼 이동합니다.

  • arr[i]는 개념적으로 *(arr + i)와 연결됩니다.

  • 배열을 함수에 전달하면 배열 전체가 복사되지 않고 첫 번째 원소 주소가 전달됩니다.

  • 함수 안에서는 원래 배열 크기를 알 수 없으므로 원소 개수를 함께 전달해야 합니다.

  • 문자열은 char 배열이며, %s와 문자열 함수들은 시작 주소에서 0까지 읽습니다.

  • 문자열 리터럴은 수정하지 말고, 읽기 전용 문자열은 const char *로 다루는 습관이 좋습니다.

  • 포인터가 가리키는 범위와 대상의 수명을 항상 확인해야 합니다.

포인터와 배열은 C에서 가장 많이 헷갈리는 주제 중 하나입니다. 하지만 둘을 메모리 그림으로 보면 훨씬 단순해집니다. 배열은 값들이 줄 서 있는 공간이고, 포인터는 그 줄의 어느 칸을 가리키는 주소입니다.

다음 장에서는 이 감각을 더 넓혀 프로그램 전체의 메모리 구조를 살펴봅니다. 코드 영역, 데이터 영역, 스택, 힙이라는 큰 그림 속에서 변수, 배열, 포인터, 동적 메모리가 각각 어디에 놓이는지 확인해 보겠습니다.

책 목록

처음 만나는 C언어: 메모리와 포인터로 배우는 컴퓨터의 원리

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기