brunch

매거진 Sinclair

You can make anything
by writing

C.S.Lewis

by Sinclair Aug 19. 2016

구조체, 공용체, 열거형

오 놀라워라~ C언어의 확장성 I



지금부터 공부할 것은 우리의 C 프로그래밍을 보다 윤택하고 멋지게 할 녀석들입니다.


프로그램이 커지면서 많은 종류의 데이터를 처리해야 할 필요성이 증가합니다.

그 때 적절한 해결책이 되어줄 수 있는 멋진 놈들입니다. 심지어 이 녀석들을 적절히 사용하게 되면 절차지향적이라고 하는 C언어로 OOP(객체지향프로그래밍)를 구현할 수도 있습니다.

에이 설마~ 라고 의심하겠지만 C++나 Java의 클래스는 하늘에서 뚝 떨어진 개념이 결코 아닙니다. 클래스에 대해 쉽게 한마디로 표현하자면 데이터와 함수를 구조체처럼 묶은 것입니다.

클래스와 관련된 자세한 것은 나중에 OOP 서적을 참고하길 바랍니다. 여기에서는 우선 C언어에 충실해야겠습니다.  


관계 있는 데이터끼리 일촌 맺기, 구조체


struct 구조체이름(태그) {

    멤버타입들 멤버이름들;

} ;  // 반드시 구조체 선언은 세미콜론으로 끝나야 합니다.



사실 구조체는 데이터베이스의 레코드와 같은 개념이라고 말합니다.

RDBMS(Relational DataBase Management System)에서는 각 정보를 처리하기 위해 필요한 데이터들을 각각 따로 다루지 않습니다. 서로 연관된 데이터를 하나로 묶어서 레코드라 부르고 그것들을 모아 하나의 테이블로 처리하는 것이 매우 효율적이기 때문입니다.

이때 하나의 레코드는 구조체로 표현할 수 있고 테이블은 구조체 배열 또는 연결리스트가 됩니다. 예를 들어 우리가 성적을 처리해야 한다면 각각 장래희망, 과목점수들, 이름, 학번, 키, 나이, 몸무게, 총점, 과목명, 평균, 취미 등 신상 정보들 중 과목점수들, 과목명, 학번, 이름, 총점, 평균 등이 필요하고 이것들을 하나로 묶어서 처리할 수 있게 해주는 것이 바로 구조체입니다.

그러나 이제는 이렇게 눈에 보이는 데이터들뿐만 아니라 메타 데이터라 부르는 데이터를 위한 데이터도 매우 중요합니다. 진정한 고수라면 눈에 보이지 않는 데이터도 함께 설계할 수 있어야 합니다.


많은 사람들이 배열이 같은 종류의 데이터 집합이라면 구조체는 서로 다른 종류의 데이터 집합이라고 말합니다.

그리고 구조체이름은 구조체 선언이 반복될 때 선언할 구조체를 구별하기 위한 장치입니다. 프로그램에서 사용하는 구조체가 하나라면 따로 구조체이름을 선언할 필요는 없지만 그런 경우는 극히 드뭅니다.

뿐만 아니라 C++에서는 구조체이름만으로도 충분히 타입의 의미를 갖지만 C에서는 반드시 struct 구조체이름으로 타입을 지정해야 합니다. 그러므로 선언할 때 의미 있는 구조체이름도 함께 주는 것이 효율적입니다.

우리가 이미 알고 있는 것처럼 배열을 함수나 대입 연산자와 함께 사용할 때 몇 가지 문제점들이 있었습니다. 하지만 일반적으로 구조체 변수는 일반 타입의 변수와 동일하게 사용됩니다.

그래서 대입 연산자를 사용할 때도 일반 변수에 대입하듯이 양쪽의 구조체 타입만 맞춰주면 모든 멤버의 값이 그대로 전달됩니다. (표준 C99 이전에는 동일한 멤버 순서와 타입을 갖는다면 구조체이름(태그)가 다르더라도 서로 대입이 되었지만 현재는 그렇게 동작하지 않습니다.) 이것을 유식한 말로 하자면 대입 연산자의 l-value가 될 수 있다는 의미 입니다. 뿐만 아니라 함수의 리턴 값으로도 활용할 수 있습니다. 

일반적으로 구조체 안에 멤버를 선언할 때는 그 순서가 중요하지 않습니다. 배열은 []연산자를 사용하여 일정한 사이즈만큼 순차적으로 데이터를 참조하고 있지만 구조체는 일정한 사이즈의 데이터들이 모여 있는 것이 아니기 때문에 .연산자를 사용하여 멤버이름으로 직접 참조해야 합니다. 또한 일반적인 경우 구조체 멤버를 직접 참조할 수 없기 때문에 서로 다른 구조체 타입 안에서 같은 멤버이름을 사용하더라도 아무런 문제가 없습니다. 구조체 변수이름으로 각 멤버를 구별할 수 있기 때문입니다.  


struct d {

    char c ;

    double pi ;

} ; // 반드시 구조체 선언은 세미콜론으로 끝나야 합니다.


struct data {


    char c ; // 위의 구조체와 같은 멤버이름을 사용할 수 있습니다.

    int i ;

} ; // 이렇게 구조체 타입을 선언했다면


struct data myData ;     // on C

// data urData ;     // just on C++

// 실제 사용할 땐 이렇게 변수를 선언해야 합니다.


struct d urData ;     // 이렇게 선언하면

// urData.c 와 myData.c로 구별하여 사용하기 때문에 같은 멤버이름을 가져도 괜찮습니다.

// 잘 알고 있듯이 괜찮다는 것과 좋다는 것은 다릅니다. ^^;;



 

typedef struct _data {

    char c ;

    int i ;

} Data ;

// 이렇게 새로운 타입으로 선언했다면

Data someData ;

// typedef를 사용하면 C++처럼 struct를 생략한 선언도 가능합니다.  



그리고 구조체 선언은 일반적인 변수 선언과 같은 영역의 제한을 갖기 때문에 함수 안에서 선언하면 그 함수 안에서, 파일 안에서 선언하면 오직 그 파일 안에서만 의미를 갖습니다. 그래서 구조체 선언은 반드시 함수 밖에 선언하거나 헤더파일로 선언하여 필요한 곳 마다 포함시키는 것이 보다 효율적입니다. 그러나 일반 변수와는 다르게 구조체 선언은 단순한 타입의 선언에 불과합니다. 그래서 구조체 (타입)선언을 하고 난 후에 반드시 구조체 변수 선언과 초기화라는 세 단계를 거쳐야 합니다.

그리고 표준 C99부터는 .멤버이름=수식(값)을 사용하여 중간에 있는 멤버만 따로 초기화할 수도 있습니다. 배열에서처럼 여기서도 초기화 할 때 멤버 중 하나라도 초기화하게 되면 나머지 멤버들은 모든 비트가 0으로 설정되어 각 타입에 맞는 디폴트 값을 갖게 됩니다.


struct data {

    char c ;

    int i ;

}
ourData = {'0',11} ;

// 구조체 선언과 변수 선언, 초기화를 한 번에  


myData = { '*' } ;

// 하나라도 초기화하면

// 나머지는 디폴트 값으로   


other = { .i = 26 } ;

// .멤버명=수식(값)을 사용하여 중간 멤버만 따로 초기화 가능



struct data {

    char c ;

    int i ;

} herData ;

// 구조체 선언과 변수 선언만 여기에


herData.c = '*' ;

herData.i = 1004 ;

// 초기화는 따로  


struct data hisData = {'a',1};

// 구조체 변수 선언과 초기화를 따로



struct data {

    char c ;

    int i ;

} ; // 구조체 선언


struct data datum ; // 변수 선언

datum.c = '+' ;

datum.i = 26 ;

// 초기화를 각각 따로따로

datum = herData ;

/* 이렇게 대입 연산자도 막 바로 사용가능 */



또한 일반적으로 구조체 멤버로 선언하는 타입은 거의 제한이 없습니다. 일반 데이터 타입과 포인터 타입, 심지어 사용자 정의 타입과 다른 구조체나 공용체를 멤버로 가질 수도 있습니다. 구조체가 다른 구조체나 공용체를 포함하게 되면 .연산자를 두 번 사용하게 됩니다.

주의해야 할 것은 구조체가 const나 volatile 타입의 멤버를 가질 수 있으나 register 멤버를 포함하면 컴파일에러가 발생합니다. 메모리와 레지스터를 한번에 묶어서 사용할 수 없기 때문입니다. 단, const 멤버의 경우 구조체 타입 선언후 구조체 변수를 선언하면서 처음에 함께 초기화하지 않으면 절대로 코드 중간에 값을 변경할 수 없습니다.  


#include <stdio.h>  

/*

* copyleft (l) 2006 - 2017 programmed by Sinclair

*/

#define ABS(i) ((i) > 0 ? (i) : -(i))

struct point {

    short x ;

    short y ;

} ;

struct coord {

    struct point topLeft ;

    struct point bottomRight ;

    int area ;

} ;  


int main() {

    struct coord Rectangle ;

    Rectangle.topLeft.x = 100 ;

    Rectangle.topLeft.y = 200 ;

    Rectangle.bottomRight.x = 300 ;

    Rectangle.bottomRight.y = 700 ;

    Rectangle.area =    

        (Rectangle.bottomRight.x - Rectangle.topLeft.x) *

        (Rectangle.topLeft.y - Rectangle.bottomRight.y) ;


    printf("area = %d\n", ABS(Rectangle.area)) ;

    return 0 ;

} // end main()   




구조체 멤버 연산자와 sizeof


구조체의 멤버를 참조하려면 .연산자를 사용하게 되고 이 때 반드시 모든 멤버의 사이즈들을 다 기억하고 있거나 앞에 있는 멤버들의 사이즈를 먼저 연산해야 하는 불편함이 생깁니다. 그렇기 때문에 일반적으로 OS나 컴파일러가 일반 데이터나 배열이 아닌 구조체의 멤버들을 배치할 때는 주로 메모리 block단위를 사용하게 되고 사용하지 않는 메모리 빈 공간이 생기기도 합니다. 그래서 구조체 멤버를 설계할 때 멤버의 순서보다는 멤버의 사이즈와 메모리 블록에 맞게 배치하는 것이 좋습니다.


struct data {

    char c ;

    int i ;

} ;


printf("size = %d\n" , sizeof(struct data)) ;  



위의 소스의 결과는?? 그때, 그때 달라요~ 일반적으로 OS나 컴파일러에 따라서 다르게 나옵니다. 하지만 대부분의 경우 5가 아니라 8이 나오게 됩니다.

일반적으로 32bit GNU Linux의 경우 4바이트인 word 단위로 데이터가 배치되므로 char 한 바이트가 들어가고 나면 남은 3바이트에 정수형이 들어갈 수 없습니다. 그래서 남은 그 공간을 비우고 다음 4바이트에 저장합니다. 그 결과 5가 아닌 8이 나오게 됩니다.

이런 문제 때문에 서로 다른 종류의 시스템간의 구조체 전달을 할 때는 매우 주의해야 합니다.

경우에 따라서 데이터가 잘리거나 잘못된 데이터가 들어오게 되는 엄청난 사태가 발생하기도 합니다. 주먹 구구식으로 데이터 사이즈를 예상하면 위험합니다. 반드시 sizeof 연산자를 사용하길 바랍니다.





물론 이때 선행처리기 문법#pragma 나 컴파일러 옵션을 사용하여 구조체 각 멤버를 packed하게 메모리에 배치하도록 하면 빈 공간 없이 데이터를 채워 넣을 수 있어 이런 문제를 미리 방지할 수 있습니다.



#pragma pack(1)

struct data {
    char c ;
    int i ;
} ;

printf("size = %d\n" , sizeof(struct data)) ;  


이제 멤버를 packed하게 빈틈없이 1 바이트 단위로 넣을 수 있어 32bit 운영체제라면 5가 나옵니다. 하지만 이후에 모든 메모리 관리가 바이트단위로 이뤄져 프로그램 전체의 속도저하는 불을 본듯 뻔해집니다.



#pragma pack(1)

struct data {
    char c ;
    int i ;
} ;
#pragma pack(4)

// 요렇게 원상복구 해주는 센스(?)


하지만 모든 운영체제가 다 4바이트를 기준으로 잡지도 않을 뿐더러 매번 시스템이나 운영체제가 바뀔 때마다 일일이 찾아서 바꾸는 건 옳지않아요~

(물론 이때 gcc의 경우 -fpack-struct=1 컴파일러 옵션을 사용하면 좀더 유연할 수 있지만, 적용범위가 컴파일 되는 파일(들) 단위로 넓어집니다.)



// 가장 일반적인 방법

#pragma pack(push,1) // 현재 기준값을 스택에 넣고 1 바이트로 팩

struct data {
    char c ;
    int i ;
} ;
#pragma pack(pop) // 스택에 넣은 값을 꺼내 원상복귀


// GCC 4.0 미만인 경우엔 이렇게 해야 합니다.

struct data {
    char c ;
    int i ;
}  __attribute__((aligned(1), packed)) ;    

// 또는,

struct  __attribute__((packed)) data {
    char c ;
    int i ;
}  ;


이렇게 packed하게 데이터를 넣은 구조체라면  .연산자가 멤버를 참조하는 속도가 느려지는 대가를 지불해야 합니다. no pain, no gain! give and take입니다.










C언어 및 기타 프로그래밍 관련 질문은 오픈 카톡으로

group talk - https://is.gd/yourc

1:1 talk - https://is.gd/aboutc

#Sinclair #씽클레어 #싱클레어 #씽클레어도씨 #씨언어 #씨프로그래밍  #C언어 #Cprogramming #C_Programming #C #Programming #Clanguage #C_Language

매거진의 이전글 구조체, 공용체, 열거형
작품 선택
키워드 선택 0 / 3 0
댓글여부
afliean
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari