본문 바로가기

윤성우의 C Programming

Chapter 22 구조체와 사용자 정의 자료형 1 (2)

2. 구조체와 배열 그리고 포인터 

구조체 배열의 선언과 접근 

만약 전화번호부를 작성해야 한다고 생각해보자. 전화번호부에는 여러 사람의 정보를 저장해야 하기 때문에 이를 위해서 다수의 구조체 변수를 선언해야 하고, 이때 사용되는 것이 구조체 배열이다. 구조체 배열은 기본적으로 여러 개의 구조체 변수를 연속적으로 저장할 수 있게 해준다. 이를 설명하기 전에 먼저 구조체를 어떻게 선언하는지 간단히 살펴보자.

 

구조체 선언 

구조체는 다양한 데이터 타입을 하나의 단위로 묶어서 관리할 수 있게 해주는 툴이다. 예를 들어, 학생에 대한 정보를 저장하는 구조체를 만들고 싶다면 이름, 나이, 학점 등을 포함할 수 있다. 

struct Student {
    char name[50];
    int age;
    float grade;
};

구조체 배열 선언 

이저 'Student' 구조체를 사용하여 구조체 배열을 선언할는 방법을 알아보자. 배열을 선언하는 것과 매우 비슷하며, 구조체 이름 뒤에 배열의 크기를 지정하기만 하면 된다.

struct Student students[5];

 

이 코드는 Student 구조체 타입을 가진 students라는 이름의 배열을 선언한다.배열에는 5개의 Student 구조체 변수가 포함되며, 각각의 Student 변수는 이름, 나이, 학점을 저장할 수 있다. 

 

이제 구조체 배열을 한번 사용해보자! 구조체 배열의 각 요소에 접근하려면 배열 인덱스와 점('.') 연산자를 사용한다. 예를 들어, 첫 번째 학생의 이름을 설정하고 싶다면 다음과 같이 할 수 있다. 

strcpy(students[0].name, "홍길동");
students[0].age = 20;
students[0].grade = 3.5;

 

이렇게 구조체 배열을 선언하고 사용하면, 관련된 정보를 효율적으로 관리할 수 있다. 배열과 구조체를 조합하면 복잡한 데이터도 쉽게 다룰 수 있게 된다. 

구조체 배열의 초기화 

구조체 배열을 초기화하는 것은 배열의 각 요소에 초기값을 설정하는 과정이다. 구조체 배열을 초기화하는 방법에는 여러가지가 있지만, 가장 직관적인 방법은 배열 선언 시 직접 초기값을 제공하는 것이다. 

 

간단한 구조체 예를 들어 설명해보겠다. 학생 구조체를 정의하고, 이름과 나이를 멤버로 가지도록 하겠다. 

struct Student {
    char name[50];
    int age;
};

 

이 구조체를 예시로 구조체 배열을 초기화하는 방법을 알아보도록 하겠다. 구조체 변수를 선언과 동시에 초기화할 때에는 다음과 같이 중괄호를 통해서 초기화할 값을 명시한다. 

struct Student students[3] = {
    {"홍길동", 20},
    {"이순신", 25},
    {"박영희", 22}
};

 

이 예시에서, 'students' 배열은 3개의 'Student' 구조체 요소를 가지며, 각각의 요소는 이름과 나이 정보를 초기값으로 가진다. 

구조체 배열의 부분 초기화 

구조체 배열에서 모든 요소를 명시적으로 초기화하지 않아도 된다. 일부 요소만 초기화하고, 나머지 요소는 기본값으로 초기화된다(정수형은 0, 포인터는 NULL 등).

struct Student students[3] = {
    {"홍길동", 20},
    {"이순신", 25}
};

 

이 경우 구조체 요소 3개 중에 2개만 선언된 것을 알 수 있다. 그럼 마지막 요소는 어떻게 되냐고 ? 세 번째 요소의 'name'과 'age'는 각각 빈 문자열과 0으로 초기화된다. 

지정된 초기화자 사용

C99 표준부터, 구조체의 특정 멤버를 명시적으로 초기화하는 '지정된 초기화자(designated initializers)' 문법을 사용할 수 있다. 이를 통해 구조체 배열의 특정 요소만 선택적으로 초기화할 수 있다.

struct Student students[3] = {
    [0] = {"홍길동", 20},
    [2] = {"박영희", 22}
};

 

이 예시에서는 첫 번째와 세 번째 Student 요소만 초기화되고, 두 번째 요소는 기본값으로 초기화된다. 

구조체 변수와 포인터

구조체 포인터 변수는 구조체 변수의 주소를 저장하는 포인터이다. 이를 통해 구조체의 멤버에 접근하거나 구조체 변수를 함수에 전달할 때 유용하게 사용된다. 구조체 포인터 변수의 선언과 연산 방법에 대해 자세히 알아보자.

구조체 포인터 변수 선언 

먼저 구조체 타입을 정의해야 한다. 이전에 사용한 구조체 정의를 예시로 들어보겠다. 

struct Student {
    char name[50];
    int age;
};

 

이제 이 구조체 타입의 포인터 변수를 선언하면 다음과 같다. 

struct Student *ptr;

 

이 코드는 ptr이라는 이름의 struct Student 타입 구조체를 가리키는 포인터 변수를 선언한다. 초기화 없이 선언만 한 상태에서 ptr을 사용하려면, 반드시 유효한 구조체 변수의 주소를 가리키도록 설정해야 한다. 

구조체 포인터 초기화

구조체 포인터를 특정 구조체 변수의 주소로 초기화하는 방법은 다음과 같다. 

struct Student s1;
ptr = &s1;

 

여기서 &s1s1의 주소를 의미한다. 이제 ptr 포인터는 s1 구조체 변수를 가리키게 된다.

구조체 포인터를 통한 멤버 접근

구조체 포인터를 통해 구조체 멤버에 접근하려면 화살표 연산자(->)를 사용한다. 예를 들어, ptr을 통해 name과 age 멤버에 접근하고 값을 설정하려면 다음과 같이 하면된다. 

strcpy(ptr->name, "홍길동");
ptr->age = 20;

 

ptr->nameptr이 가리키는 구조체의 name 멤버를 의미하고, ptr->ageage 멤버를 의미한다. 

구조체 포인터를 통한 함수 호출

구조체 포인터는 특히 함수에 구조체 변수를 전달할 때 유용하다. 예를 들어, 학생의 정보를 출력하는 함수가 있다면, 구조체 포인터를 사용하여 구조체 변수를 전달할 수 있다. 

void printStudent(struct Student *s) {
    printf("이름: %s\n", s->name);
    printf("나이: %d\n", s->age);
}

// 함수 호출
printStudent(ptr);

 

이 예에서 printStudent 함수는 구조체 포인터를 매개변수로 받아, 해당 구조체의 멤버에 접근하여 정보를 출력한다. 

 

포인터 변수를 구조체의 멤버로 선언하기 

배열이 구조체의 멤버로 선언될 수 있듯이, 포인터 변수도 구조체의 멤버가 될 수 있다. 이 방식은 매우 흔한 패턴인데, 동적 메모리 관리, 복잡한 자료 구조 구현 등에 널리 사용된다. 

 

포인터 멤버를 포함한 구조체 선언 예

다음은 Student라는 구조체에 name이라는 포인터 멤버를 포함하는 예제이다. 

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

// 학생 정보를 저장하기 위한 구조체
struct Student {
    char *name; // 학생의 이름을 위한 포인터
    int age;    // 학생의 나이
};

int main() {
    struct Student s1;

    // 학생 s1의 이름을 저장하기 위한 메모리 할당
    s1.name = (char *)malloc(20 * sizeof(char)); // 이름을 저장할 공간 할당
    strcpy(s1.name, "홍길동"); // 이름 복사
    s1.age = 20; // 나이 설정

    // 학생 정보 출력
    printf("학생 이름: %s\n", s1.name);
    printf("학생 나이: %d\n", s1.age);

    // 동적으로 할당된 메모리 해제
    free(s1.name);

    return 0;
}

 

여기서 char *name; 이 부분이 바로 포인터 변수를 구조체의 멤버로 선언한 부분이다.(  name 포인터는 학생의 이름을 가리키게 된다.  ) 이 선언을 통해 name 멤버는 문자열(즉, 문자들의 배열)을 가리키는 포인터로 사용된다. 이 포인터는 동적으로 메모리가 할당되어 학생의 이름을 저장하는 데 사용된다. 

구조체 변수의 주소 값과 첫 번째 멤버의 주소 값은 동일하다. 

구조체 변수의 주소 값과 해당 구조체의 첫 번째 멤버의 주소 값이 동일한 이유는 C 언어에서 구조체의 메모리 배치 방식 때문이다. C 언어에서 구조체는 메모리에 연속적으로 배치되며, 첫 번째 멤버는 구조체 시작 주소에서 바로 시작한다. 이는 구조체의 메모리 할당 방식과 밀접한 관련이 있다. 

 

예를 들어, 다음과 같은 간단한 구조체가 있다고 가정해보자

struct Example {
    int number;
    char character;
};

 

이 경우, struct Example 타입의 변수가 메모리에 할당될 때, number 멤버는 구조체의 시작 주소에 배치된다. character 멤버는 number 다음에 메모리에 배치된다. 

 

구조체 변수 example의 주소와 그 첫 번째 멤버 number의 주소를 비교하는 코드는 다음과 같다. 

#include <stdio.h>

struct Example {
    int number;
    char character;
};

int main() {
    struct Example example;
    printf("구조체 변수의 주소: %p\n", (void *)&example);
    printf("첫 번째 멤버 'number'의 주소: %p\n", (void *)&example.number);

    return 0;
}

 

이 프로그램을 실행하면, example 변수의 주소와 example.number의 주소가 동일하다는 것을 확인할 수 있다. 이는 구조체의 첫 번째 멤버가 구조체 시작 주소에서 바로 시작하기 때문이다.