모노산달로스의 행보

[C programming] 구조체와 열거형 본문

ProgrammingLanguage/C

[C programming] 구조체와 열거형

모노산달로스 2024. 4. 17. 21:33

C programming - 구조체와 열거형

 

 

리눅스 환경에서 네트워크 프로그래밍을 공부하기 위해서 C언어를 다시 복습해야 할 필요성을 느꼈습니다. 따라서 이번 기회에 배열부터 전처리기까지 내용들을 정리하겠습니다.


구조체란?

출처 : https://www.reddit.com/r/rust/comments/pchp8h/media_struct_update_syntax_in_rust/

 

구조체가 대체 무엇일까요? 사전적으로는 하나 이상의 변수를 묶어 그룹화 하는 사용자 정의 자료형을 의미합니다. 위 사진 처럼 Cat 이라는 구조체가 namebreed 그리고 age이라는 멤버 변수를 가진 것 처럼 말이죠. Java와 같은 언어에서 사용하는 Class와도 유사한 점이 존재합니다.

 

구조체의 목적은 데이터를 하나로 모아 구조적으로 만들어 데이터 참조를 용이하게 하는 것입니다. 예를 들어 고양이의 이름을 참조하는 함수가 100개 정도 있다고 생각해봅시다. 만약 여기서 고양이의 나이를 참조해야하는 경우가 생긴다면 어떨까요? 100개의 함수를 모두 수정하여 고양이의 나이를 넘겨주어야합니다. 하지만 구조체를 사용하여 데이터를 전달하는 경우 구조체 멤버 변수로 하나 추가해주기만 하면 됩니다.

 


구조체 정의와 선언 그리고 초기화

지금 부터 구조체를 정의하는 방법을 자세히 알아보겠습니다.

struct point
{
    int x;
    int y;
};

 

먼저 sturct를 사용하여 구조체의 시작을 알립니다. 그리고 구조체의 이름을 작성합니다. 여기서는 point라고 지정하였습니다. 다음으로 멤버 변수를 지정합니다. 그 결과 int x와 int y가 멤버 변수로 존재하는 구조체 point가 완성되었습니다.

 

이렇게 구조체를 정의한 뒤 구조체 변수를 선언하여 사용할 수 있습니다.

 

#include<stdio.h>

struct point {
    int x;
    int y;
};

int main(void)
{
    struct point p1, p2, p3;
    
    return 0;
}

 

이제 감이 좀 오시나요? 구조체를 정의한다는 것은 우리가 사용하는 int 혹은 char 같은 자료형처럼 자신만의 자료형을 만드는 것입니다.

 

#include<stdio.h>

struct point {
    int x;
    double y;
};

int main(void)
{
    struct point p1;

    p1.x = 10;
    p1.y = 20.4;

    printf("p1.x : %d\n", p1.x);
    printf("p1.y : %lf\n", p1.y);
    
    return 0;
}

예제 코드의 출력 결과

 

구조체의 멤버 변수에 접근하는 방법은 위와 같습니다. p1이라는 구조체를 변수를 선언하고 .(멤버변수)를 붙여주면 원하는 멤버 변수에 접근이 가능합니다. 값을 수정하거나 출력하는 것 모두 자유롭게 가능합니다.

 

struct point p2 = {10, 24.7};

struct point p3;
p3 = {10, 24.7}; // error

 

위와 같이 구조체 변수를 선언과 동시에 초기화 하는 것 또한 가능합니다. 구조체 또한 배열과 같이 연속적인 메모리 구조를 가지고 있기 때문에 위와 같이 값을 초기화 할 수 있습니다. 다만 p3와 같이 선언과 초기화를 따로 수행하는 경우 에러가 발생하니 주의해야합니다.

 

struct point p1 = {10, 24.7};
struct point p2 = {0, 0};

p2 = p1;

p2 + p1 // error
p2 - p1 // error

 

위와 같이 구조체 변수 p1의 값을 같은 구조체 변수인 p2로 복사하는 것 또한 문제없이 작동합니다. 하지만 구조체 변수간 산술연산은 불가능합니다.

 


중첩 구조체

구조체 내에 구조체가 포함되는 것을 중첩 구조체라고 합니다. 즉, 구조체 변수를 멤버 변수로 사용하는 것입니다.

#include<stdio.h>

struct score
{
    double math;
    double english;
    double total;
};
struct student
{
    int no;
    struct score s;
};

int main(void)
{
    struct student stu;
    
    stu.no = 20101323;
    stu.s.math = 90;
    stu.s.english = 80;
    stu.s.total = stu.s.math + stu.s.english;

    printf("학번: %d \n", stu.no);
    printf("총점: %lf \n", stu.s.total);
    
    return 0;
}

예제 코드의 출력 결과

 

위와 같이 score 구조체가 student 구조체의 멤버 변수로 사용되는 것을 확인할 수 있습니다. 해당 구조체에 접근하는 방법은 이전과 같이 .(멤버변수)를 여러 번 사용하여 이루어집니다.

 

struct student stu = {20101323, {90, 80, 0}};
struct student stu = {20101323, 90, 80, 0};

 

이러한 중첩 구조체 또한 선언과 동시에 초기화가 가능합니다.

 

typedef의 사용

typedef란 자료형의 재정의를 알리는 키워드입니다. 이를 구조체에 사용하면 구조체 사용이 조금 더 편리해집니다.

#include<stdio.h>

typedef struct score
{
    double math;
    double english;
    double total;
} SCORE;
struct student
{
    int no;
    SCORE s;
};

typedef struct student STUDENT;

int main(void)
{
    STUDENT stu;
    ...
    return 0;
}

 

 

앞서 보았던 코드를 typedef를 통하여 수정하였습니다. 먼저 struct score라는 자료형 앞에 typdef를 작성하여 재정의를 알렸습니다. 그리고 새로운 이름인 SCORE를 작성했습니다. 이와 같이 구조체 정의와 동시에 typedef를 선언하는 것과 student와 같이 구조체가 정의된 이후 개별적으로 사용하는 것 모두 허용됩니다.

 

이제는 score 구조체를 사용하기 위해서 struct score s가 아닌 STURCT s와 같이 간편하게 표현이 가능합니다.

 


구조체와 배열

구조체 또한 여타 변수들과 마찬가지로 배열 사용이 가능합니다.

typedef struct score
{
    double math;
    double english;
    double total;
} SCORE;
struct student
{
    char no[10];
    char name[20];
    SCORE s;
};

typedef struct student STUDENT;
STUDENT stu[3] = {
        {"20101323", "Park", 80, 80, 0},
        {"20101324", "Kim", 95, 85, 0},
        {"20101325", "Lee", 100, 90, 0}
};

 

구조체 배열을 선언함과 동시에값을 초기화하는 것이 가능합니다. 문자 배열로 선언된 멤버 변수 또한 값을 초기화 해줄 수 있습니다.

 

int main(void)
{
    int i = 0;
    STUDENT stu;

    stu.no = "20101323"; // error
    stu.name = "Park"; // error

    return 0;
}

 

하지만 위와 같이 접근하는 경우 에러가 발생합니다. 왜냐하면 stu.no과 stu.name은 멤버 변수 문자 배열을 가리킵니다. 배열의 이름은 배열의 시작 주소를 가리키기 때문에 문자열로 값을 초기화 하려고 하면 에러가 발생하는 것입니다.

 

이를 해결하는 방법은 두 가지가 존재합니다.

int main(void)
{
    int i = 0;
    STUDENT stu;

    strcpy(stu.no, "20101323");
    strcpy(stud.name, "Park");

    return 0;
}

 

첫 번째 방법은 strcpy() 함수를 사용하는 것입니다. strcpy() 함수는 두 번째 인자의 문자열을 첫 번째 인자의 주소로 복사하는 기능을 수행합니다. 이를 이용하면 값을 초기화하는 것과 같은 결과를 얻을 수 있습니다.

 

struct student
{
    char* no;
    char* name;
    SCORE s;
};

typedef struct student STUDENT;

int main(void)
{
    int i = 0;
    STUDENT stu;

    stu.no = "20101323";
    stu.name = "Park";

    return 0;
}

 

두 번째 방법은 멤버 변수로 포인터를 선언하는 것입니다. 포인터는 주소를 저장하는 변수로써 문자열 상수의 주소 값을 가리키게 할 수 있습니다.

 

구조체와 포인터

#include<stdio.h>

struct point {
    int* x;
    int** y;
};

int main(void)
{
    int num1 = 4;
    struct point p1;

    p1.x = &num1;
    p1.y = &p1.x;

    printf("%d %d %d \n", num1, *p1.x, **p1.y);

    return 0;
}

예제 코드의 출력 결과

구조체의 멤버 변수로 위와 같이 포인터를 사용할 수 있습니다. 포인터 멤버 변수의 값을 참조하는 방법은 기존의 포인터 값의 참조와 같습니다. .연산자*연산자보다 우선순위가 높기 때문에 위와 같은 표현이 가능합니다.

 

#include<stdio.h>

struct student {
    char no[10];
    char name[20];
    double total; 
};

int main(void) {
    struct student stu = {"20101323", "Park", 160};
    struct student* p=NULL;
    struct student** pp=NULL;

    p = &stu;
    pp = &p;

    printf("%s %s %lf \n",stu.no, stu.name, stu.total);
    printf("%s %s %lf \n",(*p).no, (*p).name, (*p).total);
    printf("%s %s %lf \n",p->no, p->name, p->total);
    printf("%s %s %lf \n",(**pp).no, (**pp).name, (**pp).total);
    printf("%s %s %lf \n",(*pp)->no, (*pp)->name, (*pp)->total);

    return 0;
}

예제 코드의 출력 결과

위와 같이 포인터 멤버 변수가 아닌 구조체 포인터 변수를 선언하는 것 또한 가능합니다. 구조체 포인터 변수를 통해 값에 접근하는 방법은 조금 특이합니다. (*p).no 와 같이 기존 포인터와 같은 방법을 사용할 수도 있지만 p->no와 같이 간략하게 표현할 수도 있습니다.\

 

#include<stdio.h>

struct student {
    char name[20];
    int age;
    struct student* link;
};

int main(void) {
    struct student stu1 = {"Kim", 90, NULL};
    struct student stu2 = {"Lee", 80, NULL};
    struct student stu3 = {"Goo", 60, NULL};

    stu1.link = &stu2;
    stu2.link = &stu3;

    printf("%s %d \n", stu1.name, stu1.age);
    printf("%s %d \n", stu1.link->name, stu1.link->age);
    printf("%s %d \n", stu1.link->link->name, stu1.link->link->age);

    return 0;
}

예제 코드의 출력 결과

위와 같이 자기 참조 구조체 포인터 변수를 선언할 수 있습니다.

 

 

위와 같이 구조체 변수들이 연결된 형태로 표현됩니다. stu1.link->link->name 과 같은 방식으로 포인터를 통해 stu3의 값에 접근하는 것이 가능해집니다. 연결 리스트를 만들 때도 같은 방법을 사용할 수 있습니다. 연결 리스트에 관한 내용은 차후 동적 메모리 할당에 대한 내용을 정리하면서 공부하겠습니다.

 


열거형

열거형은 변수가 갖는 값에 의미를 부여하는 역할을 합니다. 이를 통해 프로그램의 가동성을 높일 수 있습니다.

enum week {ONE, TWO, THREE, FOUR, FIVE};

 

열거형 키워드 enum을 통해 열거형을 선언합니다. 이후 열거형의 이름과 열거형 데이터로 사용할 상수의 이름을 지정합니다.

 

#include<stdio.h>
enum week {ONE = 1, TWO, THREE, FOUR, SIX = 6, SEVEN};

int main(void)
{
    enum week p1, p2, p3, p4, p6, p7;
    p1 = ONE;
    p2 = TWO;
    p3 = THREE;
    p4 = FOUR;
    p6 = SIX;
    p7 = SEVEN;

    printf("%d %d %d %d %d %d\n", ONE, TWO, THREE, FOUR, SIX, SEVEN);
    printf("%d %d %d %d %d %d\n", p1, p2, p3, p4, p6, p7 );

    return 0;
}

예제 코드의 출력 결과

 

열거형의 상수들은 0부터 시작하여 1씩 증가합니다. ONE과 SIX와 같이 값을 지정하면 해당 수 부터 다시 값이 증가하게 됩니다.