현재 선택한 정보처리 과정

정보처리기사 필기 이론 학습

이론 목록으로 돌아가기

C 배열·문자열·포인터·구조체

배열은 같은 형의 원소를 연속 배치한 객체이며, 포인터 변수와 같지 않다. C 문자열의 널 종료, 포인터의 원소 단위 산술, 구조체 멤버 접근과 대입을 코드로 구분한다.

예상 읽기 17

배열은 같은 형의 원소를 연속 배치한 객체이며, 포인터 변수와 같지 않다. C 문자열의 널 종료, 포인터의 원소 단위 산술, 구조체 멤버 접근과 대입을 코드로 구분한다.

그림으로 확인하기

좌우로 이동해 그림을 확인하세요.그림 크게 보기
a+i는 i번째 원소를 가리킨다. 한 칸 뒤 포인터는 만들 수 있지만 역참조할 수 없다.
a+i는 i번째 원소를 가리킨다. 한 칸 뒤 포인터는 만들 수 있지만 역참조할 수 없다.

배열: 연속된 원소를 하나의 객체로 묶기

배열의 기본 성질

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int scores[4] = {70, 80, 90, 100};

이 선언에서 scoresint 원소 4개를 가진 배열 객체다.

  • 원소형: int
  • 원소 수: 4
  • 유효 첨자: 0, 1, 2, 3
  • 원소 배치: 선언된 순서대로 연속
  • 전체형: int[4]

scores[0]은 첫 원소이고 scores[3]은 마지막 원소다. scores[4]는 존재하지 않는다. 배열 경계를 벗어난 원소에 접근해서는 안 된다.

배열의 원소 수는 반드시 소스 코드의 정수 리터럴로만 정하는 것은 아니다. 실행 중 계산된 길이를 사용하는 가변 길이 배열이 허용되는 C 환경도 있다. 다만 어떤 방식으로 길이를 정했든 배열 객체가 만들어진 뒤 그 배열이 자동으로 확장되거나 축소되지는 않는다.

초기화 원칙도 구분해야 한다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int a[4] = {1, 2};  /* {1, 2, 0, 0} */
int b[4];           /* 자동 저장 기간이면 원소값이 자동으로 0이 되지 않음 */

초기화 목록에서 생략된 뒤쪽 원소는 0으로 초기화된다. 반면 블록 안의 일반 지역 배열을 초기화 없이 선언하면 각 원소에 사용할 수 있는 값이 자동으로 들어간다고 가정하면 안 된다.

첨자와 포인터 산술의 관계

배열 첨자는 다음 관계로 정의된다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
a[i]  ==  *(a + i)

배열식 a가 첫 원소를 가리키는 포인터로 변환되는 문맥에서 a + ii번째 원소 위치를 가리키고, *가 그 원소에 접근한다. 따라서 다음 두 식은 같은 원소를 나타낸다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
scores[2]
*(scores + 2)

이 관계가 있다고 해서 배열 객체와 포인터 변수가 같은 것은 아니다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int values[4] = {10, 20, 30, 40};
int *p = values;

p = &values[2];   /* 가능: 포인터 변수 p에 다른 주소값 대입 */
/* values = p; */ /* 불가: 배열 객체 이름에 새 주소를 대입할 수 없음 */

p는 포인터값을 저장하는 변수라서 다른 유효한 주소값을 대입할 수 있다. values는 원소 4개 자체를 가진 배열 객체이므로 대입의 왼쪽 피연산자가 될 수 없다.

배열이 포인터로 변환되지 않는 대표 문맥

배열식은 대부분의 표현식에서 첫 원소 포인터로 변환된다. 그러나 다음과 같은 대표 문맥에서는 배열형을 유지한다.

  1. 배열식이 sizeof의 피연산자일 때
  2. 배열식에 단항 주소 연산자 &를 적용할 때
  3. 문자 배열을 문자열 리터럴로 초기화할 때
C코드 영역 안에서 좌우로 이동할 수 있습니다.
int a[4] = {10, 20, 30, 40};

sizeof a      /* 배열 전체의 바이트 크기 */
sizeof a[0]   /* 원소 하나의 바이트 크기 */
&a            /* int (*)[4]: 배열 전체를 가리키는 포인터 */
a             /* 대부분의 식에서 int *: 첫 원소를 가리키는 포인터로 변환 */

실제 배열이 보이는 범위에서는 다음 관용식을 사용해 원소 수를 구할 수 있다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
size_t count = sizeof a / sizeof a[0];

이 식은 a가 실제 배열 객체일 때만 배열 원소 수를 구한다. a가 포인터라면 분자는 배열 전체 크기가 아니라 포인터 자체의 크기이므로 잘못된 결과가 된다.

a&a는 주소 표현이 같아 보여도 형이 다르다

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int a[4] = {10, 20, 30, 40};
int *element = a;        /* &a[0]과 같은 첫 원소 위치 */
int (*whole)[4] = &a;    /* 배열 전체를 가리킴 */
한 칸 이동의 의미
elementint *int 원소 한 개
wholeint (*)[4]int[4] 배열 한 개

elementwhole이 출력상 같은 시작 주소처럼 보일 수 있어도 형이 다르다. element + 1a[1]을 가리키지만, whole + 1은 배열 a 전체의 바로 다음 위치를 가리킨다. 형을 무시하고 주소 숫자만 비교해서는 안 된다.

함수 매개변수의 배열 표기는 포인터로 조정된다

함수 매개변수 위치에서 배열형으로 쓴 선언은 대응하는 포인터형으로 조정된다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
void print_values(const int values[], size_t count);

이 선언의 values는 함수 안에서 const int *values와 같은 매개변수 역할을 한다. 대괄호를 썼다고 해서 배열 전체와 원소 수가 함께 복사되는 것이 아니다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
void f(int values[10]) {
    /* 여기서 values는 int * 매개변수다. */
}

[10]은 일반적인 함수 호출에서 “반드시 원소 10개가 전달된다”는 실행 시 검사를 만들지 않는다. 또한 함수 안에서 sizeof values / sizeof values[0]으로 호출자의 원래 배열 길이를 복원할 수 없다. 따라서 길이가 필요한 함수는 보통 포인터와 원소 수를 함께 받는다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int sum(const int values[], size_t count) {
    int total = 0;

    for (size_t i = 0; i < count; ++i) {
        total += values[i];
    }
    return total;
}

2차원 배열은 “배열의 배열”이다

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int matrix[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

matrix는 원소가 int[3]인 길이 2의 배열이다. 메모리에는 첫 번째 행의 원소가 놓인 뒤 두 번째 행의 원소가 이어지는 행 우선(row-major) 순서로 배치된다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
matrix[0][0], matrix[0][1], matrix[0][2],
matrix[1][0], matrix[1][1], matrix[1][2]

대부분의 식에서 matrix는 첫 행을 가리키는 포인터로 변환된다. 그 형은 int (*)[3]이며 int **가 아니다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int (*row)[3] = matrix;
int value = row[1][2];   /* 6 */

2차원 배열을 함수에 전달할 때는 주소 계산에 필요한 뒤쪽 차원의 크기를 알려야 한다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
void print_matrix(size_t rows, int matrix[][3]);

여기서 matrix 매개변수는 int (*)[3]으로 조정된다. int **는 일반적으로 “int *를 가리키는 포인터”이므로 실제 2차원 배열과 형도, 메모리 구성도 다르다.

문자열: 널 문자로 끝나는 char 연속열

문자 배열과 C 문자열의 관계

C 문자열은 첫 널 문자 \0까지의 문자 연속열이다. 다음 배열은 네 칸을 가진 유효한 C 문자열이다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
char word[] = "cat";
인덱스0123
저장값'c''a''t''\0'

보이는 문자는 3개지만 저장공간은 4바이트가 필요하다. 문자열의 논리 길이는 종료 널 문자를 포함하지 않는다.

반대로 다음 배열은 char 배열이지만 널 문자가 없으므로 C 문자열이 아니다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
char raw[3] = "cat";

이 초기화 자체는 배열 세 칸에 'c', 'a', 't'를 저장할 수 있다. 그러나 종료 널 문자가 없으므로 strlen(raw), %s, strcmp(raw, ...)처럼 유효한 C 문자열을 요구하는 연산에 전달해서는 안 된다.

판별 기준: char 배열이라는 사실만으로 문자열이 되는 것이 아니다. 배열 범위 안에서 널 문자를 만날 수 있어야 한다.

문자 리터럴과 문자열 리터럴

C코드 영역 안에서 좌우로 이동할 수 있습니다.
'A'    /* 문자 상수: C에서는 int 형의 정수 문자 상수 */
"A"    /* 문자열 리터럴: 'A', '\0'을 담은 char 배열 */
  • 작은따옴표는 한 문자에 대응하는 정수 문자 상수를 만든다.
  • 큰따옴표는 마지막에 널 문자가 붙는 문자 배열을 만든다.
  • sizeof "A"는 2다.
  • strlen("A")는 종료 널 문자를 제외하므로 1이다.

문자열 리터럴로 문자 배열을 초기화하면 배열에 내용이 복사되어 그 배열 원소는 변경할 수 있다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
char editable[] = "cat";
editable[0] = 'C';       /* 가능: editable은 별도 배열 */

반면 문자열 리터럴 자체를 수정하려 해서는 안 된다. 실무에서는 다음처럼 가리키는 문자를 수정하지 못하도록 선언하는 편이 안전하다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
const char *message = "cat";
/* message[0] = 'C'; */  /* 허용하지 않음 */

C에서 문자열 리터럴의 배열형이 문법상 const로 한정되지 않았다는 이유로 수정 가능한 저장공간이라고 판단하면 안 된다. 문자열 리터럴을 변경하려는 동작에는 정의된 결과가 없다.

\0NULL은 역할이 다르다

  • \0: 값이 0인 널 문자를 나타내는 문자 상수로, C 문자열의 끝을 표시한다.
  • NULL: 아무 객체도 가리키지 않는 널 포인터를 표현할 때 사용하는 매크로다.

둘이 정수 값 0과 관련될 수 있다는 이유만으로 같은 개념은 아니다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
char text[2] = {'A', '\0'};
char *p = NULL;

text[1]은 배열 안의 문자이고, p는 현재 유효한 객체를 가리키지 않는 포인터다. 널 문자는 읽을 수 있는 배열 원소지만 널 포인터는 역참조할 수 없다.

sizeofstrlen을 구분한다

C코드 영역 안에서 좌우로 이동할 수 있습니다.
char word[] = "cat";
결과의미
sizeof word4배열 전체 저장 용량을 바이트 단위로 계산
strlen(word)3첫 널 문자 전까지의 바이트 수를 계산
sizeof ""1빈 문자열 리터럴도 널 문자 한 칸을 가짐
strlen("")0널 문자 앞의 내용이 없음

strlen<string.h>에 선언되어 있고 결과형은 size_t다. 유효한 널 종료 문자열을 받았다는 전제에서 첫 널 문자까지 탐색한다. 배열의 전체 용량을 알려 주지는 않는다.

문자열 중간에 널 문자가 있으면 그 지점이 논리적 끝이다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
char data[] = {'A', '\0', 'B', '\0'};
  • sizeof data는 4
  • strlen(data)는 1

또한 strlen은 화면에 보이는 글자 수가 아니라 바이트 수를 센다. 여러 바이트로 표현되는 문자가 포함된 인코딩에서는 사람이 보는 문자 수와 다를 수 있다.

문자열 복사와 비교의 핵심 조건

독립 배열끼리는 대입할 수 없다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
char source[] = "cat";
char target[4];

/* target = source; */   /* 배열 대입 불가 */

문자열을 복사할 때는 대상 배열의 용량이 복사할 바이트 수와 종료 널 문자까지 수용하는지 확인해야 한다. 문자열 비교도 ==가 아니라 내용을 비교하는 함수를 사용해야 한다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
strcmp("cat", "cat") == 0   /* 두 문자열 내용이 같음 */

두 문자 배열 이름에 ==를 사용하면 대부분의 식에서 각각 포인터로 변환되어 시작 주소를 비교할 뿐, 문자열 내용을 비교하지 않는다.

포인터: 형, 대상, 수명, 경계를 함께 추적하기

포인터값과 역참조

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int x = 10;
int *p = &x;
의미
x정수 객체의 값int
&xx를 가리키는 포인터값int *
pp에 저장된 포인터값int *
*pp가 가리키는 x 객체int lvalue
&p포인터 변수 p 자체를 가리키는 포인터값int **

*p = 20;을 실행하면 p의 주소 숫자를 바꾸는 것이 아니라 p가 가리키는 x의 값을 20으로 바꾼다.

포인터를 역참조하려면 다음 조건을 함께 확인해야 한다.

  1. 포인터가 올바르게 초기화되었는가?
  2. 널 포인터가 아닌가?
  3. 가리키는 객체의 수명이 아직 끝나지 않았는가?
  4. 요구되는 형과 정렬 조건에 맞는 대상인가?
  5. 배열 원소라면 현재 위치가 실제 원소 범위 안인가?

포인터 산술은 원소 단위다

C코드 영역 안에서 좌우로 이동할 수 있습니다.
int values[3] = {10, 20, 30};
int *p = values;
  • p + 0values[0] 위치
  • p + 1values[1] 위치
  • p + 2values[2] 위치
  • p + 3은 마지막 원소 바로 다음 위치

p + 1이 이동하는 실제 바이트 수는 sizeof(int)에 대응하지만, 프로그래머가 포인터식에 더하는 값은 원소 개수다.

포인터 산술은 같은 배열 객체의 원소들과 그 배열의 마지막 원소 바로 다음 위치(one-past-the-end) 를 기준으로 해야 한다. 마지막 다음 포인터는 반복 종료 조건이나 포인터 차이 계산에는 사용할 수 있지만 실제 원소를 가리키지 않으므로 역참조할 수 없다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
for (int *it = values; it != values + 3; ++it) {
    printf("%d\n", *it);
}

이 반복문에서 values + 3은 종료 경계일 뿐 *(values + 3)으로 읽을 원소가 아니다.

같은 배열 안의 두 포인터를 빼면 사이의 원소 수 차이를 얻는다. 결과형은 ptrdiff_t다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
#include <stddef.h>

ptrdiff_t distance = &values[2] - &values[0];  /* 2 */

서로 관계없는 별도 객체를 가리키는 포인터끼리 빼거나, 배열 경계를 벗어난 임의 포인터를 만들어 계산하는 것은 올바른 배열 순회 방식이 아니다.

널 포인터·초기화되지 않은 포인터·댕글링 포인터

종류상태역참조 가능 여부
유효 포인터수명이 유효한 적절한 객체 또는 함수 등을 가리킴대상·권한·경계 조건을 만족하면 가능
널 포인터어떤 객체도 가리키지 않음불가
초기화되지 않은 포인터사용할 수 있는 포인터값이 설정되지 않음불가
댕글링 포인터과거에는 유효했지만 대상의 수명이 끝남불가
마지막 다음 포인터배열 끝 경계를 나타내지만 실제 원소는 아님역참조 불가

“주소처럼 보이는 숫자가 들어 있다”는 사실은 유효성을 보장하지 않는다. 포인터의 유효성은 어떤 객체에서 유래했는지, 그 객체가 살아 있는지, 현재 위치가 허용된 범위인지로 판단한다.

구조체: 서로 다른 멤버를 하나의 값으로 묶기

선언과 멤버 접근

C코드 영역 안에서 좌우로 이동할 수 있습니다.
struct Student {
    char name[8];
    int scores[3];
};

struct Student는 문자 배열과 정수 배열을 함께 묶는 구조체형이다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
struct Student student = {"Kim", {70, 80, 90}};
struct Student *ps = &student;

student.scores[0] = 100;  /* 구조체 객체: . */
ps->scores[1] = 95;       /* 구조체 포인터: -> */

화살표 연산자는 역참조와 점 연산을 합친 표기다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
ps->scores
(*ps).scores

두 표기는 같은 멤버를 지정한다. 괄호가 필요한 이유는 점 연산자가 단항 *보다 먼저 결합하기 때문이다.

구조체 대입은 멤버 값을 복사한다

같은 호환 구조체형끼리는 대입할 수 있다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
struct Student original = {"Kim", {70, 80, 90}};
struct Student copy = original;

copy.name[0] = 'L';
copy.scores[0] = 100;

구조체 대입에서는 각 멤버의 값이 복사된다. 독립 배열끼리는 대입할 수 없지만, 구조체 안의 배열 멤버는 구조체 값의 일부로 함께 복사된다. 따라서 위 코드에서 copy의 배열 원소를 바꿔도 original의 배열 원소는 바뀌지 않는다.

구조체는 함수에 값으로 전달하거나 함수에서 값으로 반환할 수도 있다. 이때도 구조체 값의 복사라는 관점으로 이해한다.

포인터 멤버의 복사는 깊은 복사가 아니다

C코드 영역 안에서 좌우로 이동할 수 있습니다.
struct View {
    int *data;
    size_t count;
};

int values[3] = {10, 20, 30};
struct View first = {values, 3};
struct View second = first;

second = firstcount 값과 data에 들어 있는 포인터값을 복사한다. values 배열 자체를 새로 복제하지 않는다. 따라서 first.datasecond.data는 같은 배열을 가리킨다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
second.data[0] = 99;
/* values[0]과 first.data[0]도 99로 관찰됨 */
멤버 종류구조체 대입의 결과
정수·실수 등 값 멤버값 복사
배열 멤버배열 원소 값이 구조체의 일부로 복사
포인터 멤버포인터값 복사, 가리키는 대상은 자동 복제되지 않음

따라서 “구조체를 대입했으므로 모든 간접 대상까지 독립 복제된다”는 설명은 틀리다.

멤버 순서와 패딩

구조체의 일반 멤버는 선언 순서대로 뒤쪽 주소에 배치되지만, 각 형의 정렬 조건을 맞추기 위해 멤버 사이 또는 구조체 끝에 패딩(padding) 이 들어갈 수 있다.

C코드 영역 안에서 좌우로 이동할 수 있습니다.
struct Example {
    char tag;
    int value;
};

이 구조체의 sizeof가 반드시 sizeof(char) + sizeof(int)와 같다고 단정할 수 없다. 정확한 크기와 오프셋은 대상 구현과 정렬 규칙에 따라 달라질 수 있다.

패딩과 바이트 순서, 정수 표현 등이 다를 수 있으므로 구조체 메모리를 통째로 파일이나 네트워크에 보내는 방식은 이식 가능한 직렬화 규칙이 아니다. 필요한 필드를 정한 형식으로 하나씩 변환해야 한다.

구조체 비교와 공용체 구분

C는 구조체 전체에 내장 ==·!= 비교를 제공하지 않는다. 의미상 같은지를 판단하려면 필요한 멤버를 각각 비교해야 한다. 패딩이 있을 수 있으므로 구조체 전체 바이트를 단순 비교하는 것 역시 일반적인 값 비교와 같다고 볼 수 없다.

공용체(union) 는 구조체와 선언 모양이 비슷하지만 모든 멤버가 같은 저장공간을 공유한다.

구분구조체공용체
저장공간각 멤버가 자신의 저장 영역을 가짐멤버들이 같은 저장 영역을 공유
동시에 보존되는 값여러 멤버 값을 함께 보존일반적으로 현재 저장한 멤버의 표현이 공간을 차지
대표 용도관련 속성을 하나의 레코드로 묶기여러 표현 중 하나를 같은 공간에 보관하기

“구조체는 가장 큰 멤버 하나만큼의 공간을 공유한다”는 설명은 공용체의 특징을 구조체에 잘못 적용한 것이다.

배열 초기화와 유효한 첨자 위치

int a[4] = {2, 5};처럼 초기화 목록이 원소 수보다 짧으면 나머지 원소는 0으로 초기화되어 [2, 5, 0, 0]이 된다. 이는 초기화 자체가 없는 일반 자동 지역 배열의 원소를 0이라고 가정해도 된다는 뜻이 아니다.

첨자식 p[k]*(p + k)와 같은 의미이다. p가 배열 첫 원소가 아니라 중간 원소를 가리킬 수도 있다. 예를 들어 p가 &a[2]라면 p[-1]a[1]이며 유효하다. 음수 첨자라는 표기만으로 오류를 단정하지 말고 계산한 포인터가 실제 배열 경계 안의 원소를 가리키는지 확인한다. 반대로 배열 끝 다음 위치의 포인터를 만드는 것과 그 위치를 역참조하는 것은 다르다.

문자 배열에 중간 널 문자를 쓰면 그 위치에서 문자열이 끝나는 것으로 해석하지만, 그 뒤 배열 원소가 삭제되는 것은 아니다. sizeof는 배열 전체 저장 크기를, strlen은 주어진 시작 위치부터 첫 널 문자 전까지의 길이를 구한다.