본문 바로가기
프로그래밍/C

구조체

by 길냥이 2025. 6. 12.
728x90

목차

  1. 자료형의 분류
  2. 구조체 union
  3. 구조체 선언과 구조체 크기
  4. 구조체 참조와 . 연산
  5. 다양한 구조체 멤버
  6. 구조체 변수의 대입과 비교
  7. 구조체 배열
  8. 구조체 포인터와 -> 연산자
  9. 구조체와 함수

 

1. 자료형의 분류

통상적으로 자료형은 다음과 같이 분류됩니다.

int, long, float, char.. 등의 기초자료형

배열, 포인터, 구조체, 공용체 등의 파생자료형

typedef, enum 등의 사용자 정의 자료형 말이죠.

 

하지만 실무나 사용자 입장에서 보면 좀 달라요.

int, long,float, ...배열, 포인터를 기초자료형으로 잡고,

구조체, 공용체, typedef, enum 등을 사용자 정의 자료형이라 하기도 합니다.

 

2. 구조체 

그래서, 구조체가 뭡니까?

구조체란, 각자 다른 자료형을 가진 변수들을 모아서 새 변수를 선언하는 겁니다.

 

예를 들어, 학생의 성적은 int형 배열로 선언할 수 있지만..

학생에 대한 데이터를 모으려면요?

학번은 int형일거고, 학점은 실수일거고, 이름은 문자열일 거고, 성별은 bool형일 거고..

 

이런 관리하기 귀찮은 것들은 배열로 못 묶습니다. 자료형이 다르잖아요.

이런 서로 다른 자료형들은 구조체로 묶을 수 있습니다.

 

3. 구조체 선언과 구조체 크기

대신 좀 귀찮습니다. 구조체 선언이랑 변수 선언은 서로 다르거든요.

구조체는 선언 후 변수 선언 과정을 거쳐야 합니다.

그러니까, 구조체 선언 → 구조체 변수 선언 과정이라는 거에요.

 

구조체가 어떻게 선언되는지 알아봅시다.

구조체는 세 조각으로 이루어져 있습니다.

 

구조체 선언 명령어, 구조체의 이름(tag), 구조체의 멤버 세 조각이죠.

선언 명령어는 struct입니다. 이건 고정이에요.

구조체 이름은 변수 이름처럼 아무거나 가져다 써도 됩니다.

구조체 멤버는 중괄호 안에 들어가며, 끝에 세미콜론이 들어가고, 내부에는 구조체, int형, 배열 등 뭐든 들어갑니다.

 

다음과 같이 선언할 수 있습니다.

struct student
{
	int number;    //학번
	char name[8]; //이름
	double grade;  //학점
};

 

물론 저걸 사용할 수는 없습니다. 구조체는 선언한 뒤에 구조체 변수를 추가적으로 선언해야 합니다.

뭐, "선언"에서 알았겠지만, int main 외부에서 먼저 선언해야 합니다. 

그러니까, 함수 선언하는거처럼요.

 

구조체 변수는 다음과 같이 선언하면 됩니다.

struct student s1;

 

원한다면 선언하자마자 초기화도 가능합니다.

struct student s1= { 20, "Kim", 4.4 }

 

메모리에는 다음과 같이 저장됩니다.

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
number name[8] grade

음...

사실은 아니에요! 컴파일러는 연산을 빠르게 하기 위해 구조체의 맴버 사이에 패딩 바이트(padding byte)를 넣어 멤버들을 정렬합니다.

즉, 실제로 이 구조체는 메모리에 다음과 같이 저장됩니다.

number name[8]
name[8]        
grade

왜 name[8]이 아래 2번째 줄을 사용하지 않나면, 본질적으로 저건 char들의 모음이기 때문이에요.

다만 이 패딩은 구조체 내에서 가장 큰 자료형의 크기에 따라 정렬됩니다.

구조체의 가장 큰 자료형이 int라면 4바이트 단위로 만들어질거고, double이나 long long라면 8바이트 단위겠죠.

 

#pragma pack(n)을 통해서 패딩을 없에버릴 수 있지만, 그 대가로 속도가 느려질 수 있습니다.

그러니 메모리를 절약할 것인가, 속도를 올릴 것인가 중에서 선택해야 해요.

 

4. 구조체 참조와 . 연산

그럼 이제 실습을 해 봅시다.

구조체는 여러 자료형들의 묶음이기에, 배열처럼 단일로 불러올 수 없습니다.

구조체 변수.구조체 멤버 를 통해 불러와야 합니다.

그럼 바로 불러와 볼까요

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

// 학생 정보를 저장할 구조체 정의
struct student
{
	int number;       // 학번 (정수형)
	char name[8];     // 이름 (최대 7글자 + null 문자)
	double grade;     // 학점 (실수형)
};

int main(void)
{
	struct student std;  // student 구조체 변수 선언

	// 사용자로부터 학번 입력 받기
	printf("학생의 학번을 입력하세요 : ");
	scanf("%d", &std.number);

	// 사용자로부터 이름 입력 받기
	printf("학생의 이름을 입력하세요 : ");
	scanf("%s", std.name);  // name은 배열이므로 & 필요 없음

	// 사용자로부터 학점 입력 받기
	printf("학생의 학점을 입력하세요 : ");
	scanf("%lf", &std.grade);

	// 입력된 정보 출력
	printf("\n");
	printf("\t학번 : %d\n", std.number);
	printf("\t이름 : %s\n", std.name);
	printf("\t학점 : %lf\n", std.grade);

	return 0;
}

 

선언된 구조체 변수는 그 안에 여러 개의 멤버를 가지므로 특정 멤버를 골라서 사용해야 합니다.

이때 별도의 멤버 접근 연산자 .가 필요해요.

 

5. 다양한 구조체 멤버

구조체 멤버로 앞서 선보인 int, char[], double 말고도 많은 것을 사용할 수 있습니다.

포인터, 이미 선언된 구조체, 혹은 뒤에 배울 공용체나 열거형까지 말이죠.

 

한번 구조체 안에 다른 구조체와 포인터를 넣어봅시다.

포인터로는 성별(male, female)등을 미리 선언해주고 포인터로 값만 받아오는 것이 좋겠어요.

학생의 정보를 담을 구조체를 만들고, 그 안에 학생의 거주정보를 담은 구조체를 만들어 봅시다.

 

#define _CRT_SECURE_NO_WARNINGS 
#include <stdio.h>              

//거주지 정보를 저장하는 구조체 정의
struct address
{
    char city[20];     // 도시명 (예: 서울)
    char district[20]; // 구/군 이름 (예: 강서구)
};

//학생 한 명의 정보를 저장하는 구조체 정의
struct student
{
    int number;               // 학번
    char name[20];            // 이름
    double grade;             // 학점
    struct address addre;     // 주소 정보 (중첩 구조체 사용)
    char gender[20];          // 성별 (문자열로 저장)
};

int main(void)
{
    struct student std;       // 학생 정보 저장용 구조체 변수 선언
    int temp_gender = 0;      // 성별 선택 임시 저장 변수 (1: 남자, 2: 여자)

    //사용자에게 학생 정보 입력 받기
    printf("학번 : ");
    scanf("%d", &std.number);        // 정수 입력

    printf("이름 : ");
    scanf("%s", std.name);           // 문자열 배열: & 불필요

    printf("학점 : ");
    scanf("%lf", &std.grade);        // 실수(double) 입력

    printf("거주하는 도시 : ");
    scanf("%s", std.addre.city);     // 주소 정보 중 city 필드 입력

    printf("거주하는 지역 : ");
    scanf("%s", std.addre.district); // 주소 정보 중 district 필드 입력

    //성별 선택: 1 또는 2로 구분하여 gender 배열에 문자열 복사
    printf("성별 선택 (1, 남자)(2, 여자) : ");
    scanf("%d", &temp_gender);
    if (temp_gender == 1)
        strcpy(std.gender, "남자");  // gender에 문자열 복사
    else
        strcpy(std.gender, "여자");

    //입력된 학생 정보 출력
    printf("\n학생 정보\n");
    printf("학번: %d\n", std.number);
    printf("이름: %s\n", std.name);
    printf("학점: %.2lf\n", std.grade);
    printf("거주지: %s %s\n", std.addre.city, std.addre.district);
    printf("성별: %s\n", std.gender);

    return 0;
}

출력은 다음과 같습니다.

 

학번 : 20250001
이름 : 홍길동
학점 : 3.85
거주하는 도시 : 서울시
거주하는 지역 : 강서구
성별 선택 (1, 남자)(2, 여자) : 1

학생 정보
학번: 20250001
이름: 홍길동
학점: 3.85
거주지: 서울시 강서구
성별: 남자

물론 구조체는 일반적인 배열 대신 포인터 멤버를 받을 수도 있습니다.

하지만 포인터 멤버는 나중에 받도록 할게요. 이건 동적 할당에 숙달되야 가능한지라..

지금은 일반적인 배열만 사용하도록 합시다.

 

아무튼 저 구조체는 다음과 같이 저장됩니다.

총 96바이트짜리로, 중간에 패딩 하나 없이 훌륭하게 만들어져 있습니다.

아니 완전히 없는 건 아니에요, 제일 마지막에 4바이트가 패딩으로 들어갑니다.

 

6. 구조체 변수의 대입과 비교

구조체 변수들은 같은 struct를 가지고 있을 경우, 복사 대입은 가능합니다. 

p1 = p2처럼 말이죠.

 

하지만 비교는 불가능합니다. if( p1 == p2 )는 불가능하다는 거죠.

이유는 구조체 자체가 == 연산을 지원하지 않기 때문입니다.

뭐, 배열처럼 하나하나 검사해야겠네요..

 

다음과 같은 예제를 만들어 봅시다.

#include <stdio.h>

// 학생 정보를 담는 구조체 정의
struct student
{
	int id;           // 학번
	char name[12];    // 이름 (최대 11자 + 널문자)
	double grade;     // 학점
};

int main(void)
{
    // 세 명의 학생 정보 초기화
    struct student
        st1 = { 20250001, "홍길동", 2.49 },
        st2 = { 20250002, "이유진", 3.87 },
        st3 = { 20250003, "박소은", 4.13 };

    struct student max;  // 가장 높은 학점을 가진 학생 정보를 저장할 변수

    // 학점 비교를 통해 최고 학점 학생 찾기
    if (st2.grade > st1.grade)
    {
        if (st3.grade >= st2.grade)
            max = st3;   // st3이 가장 높을 경우, 또는 둘이 같을 때
        else
            max = st2;   // st2가 가장 높을 경우
    }
    else
        max = st1;       // st1이 st2보다 높거나 같고, st3보다도 높거나 같은 경우

    // 최고 학점 학생 정보 출력
    printf("학번 : %d\n", max.id);
    printf("이름 : %s\n", max.name);
    printf("학점 : %.2lf\n", max.grade);

	return 0;
}

중간에 구조체에 정보를 집어넣는 부분을 보면, 편의상 줄을 내려버린 것을 볼 수 있습니다.

콤마와 세미콜론을 적절히 사용하시면 저렇게 가독성을 올릴 수 있어요.

 

결과는 다음과 같습니다.

 

학번 : 20250003
이름 : 박소은
학점 : 4.13

구조체를 구조체에 대입연산하면 전부 복사되어 처리되기 때문에, 구조체로 묶여 있으면 이 귀찮은 과정을 쉽게 처리할 수 있습니다. 깔끔하고 편리하네요!

 

다만 구조체 내부에 포인터가 포함되어 있을 경우, 얕은 복사(shallow copy)만 시행됩니다.

포인터 값 자체는 복사되지만, 포인터가 가리키는 메모리는 복사되지 않아요.

이는 따로 관리하거나 깊은 복사(deep copy)를 시행해야 합니다.

 

7. 구조체 배열

구조체로도 배열을 만들 수 있습니다.

음.. 굳이 따지자면 2차원 배열과 비슷할 거에요.

선언 방법은 간단합니다.

 

일반적인 구조체를 선언하듯이 만든 다음, 

struct student list[100];

마지막에 배열처럼 선언하면 됩니다.

 

한번 사용해보죠.

#include <stdio.h>

//지역(도, 시) 정보를 담는 구조체 정의
struct location
{
    char region[16];  // 도 이름 (예: 경기도)
    char city[16];    // 시/군 이름 (예: 파주시)
};

int main(void)
{
    //구조체 배열에 5개의 지역 정보 초기화
    struct location province[5] =
    {
        {"경기도","파주"},
        {"강원도","원주"},
        {"충청북도","충주"},
        {"경상북도","고령"},
        {"전라남도","순천"},
    };

    // 배열 요소 개수 계산 (sizeof 활용)
    int size = sizeof(province) / sizeof(province[0]);

    //모든 지역 정보 출력 반복문
    for (int i = 0; i < size; i++)
    {
        printf("도 : %s\n", province[i].region);  // 도 출력
        printf("시 : %s\n", province[i].city);    // 시 출력
        printf("\n");  // 줄 바꿈으로 구분
    }

    return 0;
}

뭐, 위쪽의 s1,s2,s3..보다 훨씬 편리하지 않나요?

 

구조체 배열의 요소 개수 또한 배열의 크기를 구하는 것처럼 자르면 됩니다.

 

출력은 다음과 같습니다.

도 : 경기도
시 : 파주

도 : 강원도
시 : 원주

도 : 충청북도
시 : 충주

도 : 경상북도
시 : 고령

도 : 전라남도
시 : 순천

여기서 잠깐 더 나아가 봅시다.

 

배열은 본질적으로 그 배열의 주소라고 했습니다.

구조체 배열도 본질적으로는 다르지 않아요. 단지 크기만 조금 더 커졌을 뿐..

 

8. 구조체 포인터와 -> 연산자

구조체와 포인터는 두 부류로 쪼갤 수 있습니다.

기존에도 말했던 포인터를 멤버로 가지는 구조체나

혹은 구조체 자체를 가리키는 포인터 말이죠.

 

한번 구조체를 가리키는 포인터를 만들어 봅시다.

#include <stdio.h>

//지역(도, 시) 정보를 담는 구조체 정의
struct location
{
    char region[16];  // 도 이름 (예: 경기도)
    char city[16];    // 시/군 이름 (예: 파주시)
};

int main(void)
{
    //구조체 변수 province를 선언하고 초기화
    struct location province = { "경기","파주" };
    struct location* p;  //구조체 포인터 선언

    p = &province;   //포인터 p에 구조체 province의 주소를 대입
    
    //구조체 변수 자체를 이용해 지역 정보 출력
    printf("%s도 %s시\n", province.region, province.city);

    //구조체 포인터를 역참조하여 지역 정보 출력
    printf("%s도 %s시\n", (*p).region, (*p).city);
}

출력은 다음과 같습니다.

 

경기도 파주시
경기도 파주시

 

뭐, 저장은 이렇게 됩니다.

 

주소값은 아마 1012 1230같은게 들어갈겁니다, 구조체 크기는 32바이트에요.

 

그런데 말이죠, (*p).를 하나하나 쓰기에는 너무 귀찮아요.

 

조금 편한 방법이 없을까요?

그럴 때 -> 을 사용합니다.

 

(*p).region 대신 p->region을 사용해도 됩니다. 

이때 p는 주소를 담고 있어야 합니다.

#include <stdio.h>

//지역(도, 시) 정보를 담는 구조체 정의
struct location
{
    char region[16];  // 도 이름 (예: 경기도)
    char city[16];    // 시/군 이름 (예: 파주시)
};

int main(void)
{
    //구조체 변수 province를 선언하고 초기화
    struct location province = { "경기","파주" };
    struct location* p;  //구조체 포인터 선언

    p = &province;   //포인터 p에 구조체 province의 주소를 대입
    
    //구조체 변수 자체를 이용해 지역 정보 출력
    printf("%s도 %s시\n", province.region, province.city);

    //구조체 포인터를 역참조하여 지역 정보 출력
    printf("%s도 %s시\n", p->region, p->city);
}

다음과 같이 만들면 동일한 출력이 나옵니다.

 

일반적으로 (*p).는 p가 가리키는 구조체 변수를 먼저 연산하고 (*p), 괄호가 붙어서 우선적으로 *p가 처리됩니다.

괄호가 없으면 . 가 먼저 처리됩니다, 그 이후에 p가 가리키는 구조체 변수의 멤버를 처리하지만, 

p->는 p가 가리키는 구조체 변수의 멤버를 바로 처리합니다.

 

이제 포인터를 멤버로 가지는 구조체를 만들어 봅시다. 

#include <stdio.h>

//인구수와 평균연봉 정보를 담는 구조체 정의
struct info
{
    int pop;      //인구수
    int income;   //평균 연봉
};

//지역(도, 시) 정보를 담는 구조체 정의
struct location
{
    char region[16];  // 도 이름 (예: 경기도)
    char city[16];    // 시/군 이름 (예: 파주시)
    struct info* stat;
};

int main(void)
{
    //구조체 변수를 선언하고 초기화
    struct location province = { "경기","파주" };
    struct info status = { 497775,29720000 };
    struct location* p = &province;

    //구조체 포인터 stat 에 구조체 status의 주소를 대입
    province.stat = &status;

    //구조체 포인터를 역참조하여 지역 정보 출력
    printf("%s도 %s시\n", p->region, p->city);
    //province.stat가 이미 포인터이기 때문에 참조연산은 선행될 필요가 없습니다.
    printf("인구 : %d명\n평균연봉 : %d원\n",province.stat->pop, province.stat->income);
}

출력은 다음과 같습니다.

 

경기도 파주시
인구 : 497775명
평균연봉 : 29720000원

 

9. 구조체와 함수

구조체를 함수의 매개변수로 전달할 수 있습니다.

 

다만 이 경우에는 구조체 전체의 사본이 함수로 전달되고, 구조체의 크기가 커지면 그만큼 시간과 메모리가 더 많이 필요합니다.

 

한번 구조체를 함수로 보내봅시다.

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

void run(struct location);

//지역(도, 시) 정보를 담는 구조체 정의
struct location
{
    char region[16];  // 도 이름 (예: 경기도)
    char city[16];    // 시/군 이름 (예: 파주시)
};

int main(void)
{
    //구조체 변수를 선언하고 초기화
    struct location province1 = { "경기","파주" };

    run(province1);
    //구조체 포인터를 역참조하여 지역 정보 출력
    printf("%s도 %s시\n", province1.region, province1.city);
}

void run(struct location province2)
{
    strcpy(province2.region, "강원");
    strcpy(province2.city, "원주");

    //구조체 포인터를 역참조하여 지역 정보 출력
    printf("%s도 %s시\n", province2.region, province2.city);
}

다음 같은 경우에는 province1의 구조체를 run 함수에 province2로 복사해서 전달합니다.

 

따라서 출력은 다음과 같아요.

강원도 원주시
경기도 파주시

이런 문제를 해결하거나.. 

아니 문제는 아니죠, 필요할 때는 이래도 되긴 합니다.

 

하지만 원본을 보내고 싶을 때가 있죠.

그럴 때는 구조체 배열로 만들어서 보내거나, 혹은 구조체의 포인터를 보내면 됩니다.

함수에서 run(province1) 대신 run(&province1)을 보내고, 받을 때는 구조체의 포인터를 받으면 됩니다.

이때 함수 run 내에서는 *을 통해 참조연산을 하거나, ->을 통한 참조연산을 해야 합니다.

 

한번 구조체 배열을 함수로 받아서 처리해봅시다.

함수로 배열을 보내면 포인터로 처리된다는 것을 기억하세요.

#define _CRT_SECURE_NO_WARNINGS 
#include <stdio.h>

// 지역 정보를 입력받는 함수 선언
void print(struct location[], int size); //struct location *province도 가능합니다

//지역(도, 시) 정보를 담는 구조체 정의
struct location
{
    char region[16];  // 도 이름 (예: 경기도)
    char city[16];    // 시/군 이름 (예: 파주시)
};

int main(void)
{
    //구조체 배열 초기화
    struct location province[5];

    // 배열 요소 개수 계산 (sizeof 활용)
    int size = sizeof(province) / sizeof(province[0]);

    //지역 정보 입력
    print(province, size);
    printf("\n");

    //모든 지역 정보 출력 반복문
    for (int i = 0; i < size; i++)
    {
        printf("도 : %s\n", province[i].region);  // 도 출력
        printf("시 : %s\n", province[i].city);    // 시 출력
        printf("\n");  // 줄 바꿈으로 구분
    }

    return 0;
}

//사용자로부터 도, 시 입력받는 함수 정의
void print(struct location province[], int size)
{
    for (int i = 0; i < size; i++)
    {
        printf("도, 시 입력 : ");
        //배열 그 자체가 주소(&)이기 때문에 참조연산은 필요없음
        //구조체 하나만 포인터로 보냈을 경우 -> 참조가 필요합니다.
        scanf("%s %s", province[i].region, province[i].city);
    }
}

결과는 다음과 같습니다.

 

도, 시 입력 : 경기도 파주
도, 시 입력 : 강원도 원주
도, 시 입력 : 충청북도 충주
도, 시 입력 : 경상북도 고령
도, 시 입력 : 전라남도 순천

도 : 경기도
시 : 파주

도 : 강원도
시 : 원주

도 : 충청북도
시 : 충주

도 : 경상북도
시 : 고령

도 : 전라남도
시 : 순천

근데 고령은 시가 아니잖아?

 

가끔 포인터 원본을 읽기만 하고 수정할 필요가 없을 경우, 이전에도 죽어라 써먹었던 const를 사용하면 됩니다.

 

구조체를 함수에서 return 하는 경우에도 사본이 전달됩니다.

728x90

'프로그래밍 > C' 카테고리의 다른 글

이중 포인터와 다차원 데이터  (0) 2025.06.13
공용체와 열거형  (0) 2025.06.13
함수 - 예제 모음  (0) 2025.06.11
다차원 배열  (0) 2025.06.11
배열 - 예제 모음  (0) 2025.06.11