열혈 C - Chapter 23 구조체와 사용자 정의 자료형2

2024. 10. 31. 22:40Programming Language/C

반응형

23-1 구조체의 정의와 typedef 선언

typedef는 기존 자료형에 새로운 이름을 부여하는 키워드이다.

 

typedef는 이미 존재하는 자료형에 별칭(alias)를 붙여 사용할 수 있게 해준다. 

이를 통해서 가독성을 높이고 유지보수를 용이하게 할 수 있다.

typedef의 선언

* 표준적인 typedef의 선언법

typedef 기존자료형 별칭;

// typedef 선언법
typedef int INT;

typedef double MYTYPE;

//typedef 변수 선언법
INT num;        // == int num;
MYTYPE * ptr;   // == double * ptr;

 

구조체 정의와 typedef의 선언

구조체를 typedef선언하는 방법은 두가지가 있는데 첫번째로 그냥 아래에 typedef를 사용해주는 방법이다.

struct point{
    int xpos;
    int ypos;
};

typedef struct point POINT;

이건 어려울 것 없이 그냥 구조체를 선언한 다음에 그 구조체를 typedef선언한 방법이다.

이 방법 말고 두번째 방법은 이 두가지를 한번에 합친 방법이다.

typedef struct point{
    int xpos;
    int ypos;
} POINT;

구조체 point를 정의함과 동시에 typedef로 POINT를 선언한 형태이다.

 

이걸 사용하는 예시를 한번 보자.

먼저 struct point는 선언한 다음에 typedef로 타입을 선언해보고 struct person은 선언과 동시에 typedef를 선언해보자.

첫번째로 struct point를 만들어보면

이렇게 선언을 했다.

그리고 이제 person을 만들어보면

이렇게 선언할 수 있다.

 

그리고 이제 이렇게 생성한 POINT와 PERSON을 사용해서 main함수를 구성해보면

와 같이 구성을 해봤고 그 결과는 

이렇게 기존에 그냥 구현했던 방식과 동일하게 문제 없는 결과를 보여준다.

 

*전문*

 

구조체의 이름 생략

위에서 구조체를 선언하면서 typedef를 사용하는 것을 보았을것이다.

그러면 저기서 한번에 선언한 구조체는 기존에 있는 person이란 이름을 사용할 수 있을까?

동일하게 결과값을 잘 불러온다.

 

그런데 우리가 이렇게 typedef를 통해서 기존의 구조체를 한번에 선언한 경우는 typedef를 통해 선언하는 동시에 별칭이 생겼기에 기존의 이름을 사용하지 않더라도 해당 구조체를 사용할 수 있게 된다.

그래서 그런지 모르겠지만 

이렇게 선언한 경우에는 기존에 구조체를 선언하면서 생성한 구조체 명을 생략하더라도 아무런 문제가 되지 않는다.

동일하게 아무런 에러도 생성하지 않고 코드가 정상적으로 실행이 된다.

 

23-2 함수로의 구조체 변수 전달과 반환

함수의 인자로 전달되고 return 문에 의해 반환되는 구조체 변수

함수를 사용할때 구조체 변수를 인자로 전달받는 상황과 return문을 통해서 구조체를 반환하는 경우에 대해서 예제를 한번 만들어보면서 알아보자.

 

먼저 하나의 구조체 하나를 만들어보자(typedef를 사용해서)

이제 이 구조체를 인자로 받는 함수 하나를 선언해보자.

해당 함수는 구조체를 받아서 구조체 내부에 어떤 값들이 있는지 반환한다.

 

그리고 다음번엔 구조체를  return하는 함수를 하나 선언해보자.

해당 함수는 호출되면 사용자의 입력을 통해 구조체의 값을 채우고 그 구조체를 통째로 반환한다.

그리고 이제 이 함수들을 사용하는 main함수를 선언해주자.

main함수는 GetCurrentPosition을 통해서 구조체를 하나 생성하고 그 값을 ShowPosition에 전달해서 출력하는 내용을 담는 형태로 구성해보자.

그리고 이제 이 main함수를 실행해보자.

매우 간단하게 구조체를 생성하고 출력까지 실행했다..! 

 

이걸 좀 더 단순하게 한다면 아예 담는과정없이 한번에 실행도 가능하다.

결과도 동일하게 출력된다.

 

배열까지도 통째로 복사

구조체의 멤버로 배열이 선언되는 경우도 구조체 변수를 인자로 전달하거나 구조체를 반환하는 경우 배열까지도 통째로 복사가 이루어진다.

 

이에 대한 예시를 한번 생성해보자.

 

배열을 멤버로 갖고 있는 구조체 하나를 선언한 다음에 위와 동일하게 인자로 구조체를 받는 함수, return값으로 구조체를 반환하는 함수 하나를 선언한 후에 main함수에서 이를 활용해보자.

 

먼저 person구조체를 선언하고 

 

인자로 구조체를 받는 함수 ShowPersonInfo를 선언하고 그 내부에는 전달 받은 구조체의 내용을 모두 출력하는 함수를 하나 생성해보자.

 

그리고 이제 구조체 PERSON 전체를 반환하는 ReadPersonInfo를 만들어보자.

이 함수는 return 값을 PERSON타입으로 하고, 내부에서는 사용자의 입력을 통해 return할 구조체를 생성한다.

 

그리고 이전과 동일하게 ReadPersonInfo를 통해서 구조체를 생성한 후에  이를 구조체 변수에 담고 ShowPersonInfo를 통해서 해당 구조체의 내용을 출력해보자.

이제 main함수를 실행해보자.

결과를 잘 출력하는 것을 볼 수 있다.

 

그리고 동일하게 TMI지만 이 또한 내용을 줄일 수 도 있다.

 

구조체 기반의 Call-by-reference

이전에 변수를 공부할때 함수의 인자로 변수의 주소값을 전달하면 함수의 내부에서 해당 변수에 직접접근할 수 있는 방법을 Call-by-reference라고 했었었다.

 

구조체에서도 동일하게 Call-by-reference의 개념에 대해서 확인해보자.

해당 프로그램은 먼저 구조체를 하나 생성하고 OrgSymTrans라는 return값이 없는 함수의 인자로 해당 구조체 포인터 변수를 받는다. 

그리고 그 내부에서 전달 받은 구조체 포인터 변수가 가리키고 있는 xpos와 ypos의 값을 반전시킨다(-5라면 +5로).

그 이후에 출력을 도와줄 함수를 하나 생성하고 메인 함수에서 이를 사용한다.

 

여기서 구조체의 Call-by-reference에 대해서 볼 수 있는 함수는 OrgSymTrans이다.

이 함수는 인자로 구조체변수의 주소값을 받고 *연산자를 통해서 값을 변경하기 때문에 변수의 유효범위가 아니더라도 해당 변수의 값을 변경할 수 있다.

일반 변수와 다를건 없다.

물론 구조체기에 변수에 접근하는 방법은 일반 변수에 접근하는것과는 다르다.

이점에 대해서는 별로 어렵지 않을 것이다.

 

구조체 변수를 대상으로 가능한 연산

구조체 변수를 대입연산을 통해서 복사하면 구조체 변수 내부에 멤버의 값까지 모두 복사된다.

사이즈와 값이 모두 동일한 것을 보면 복사했을 때 내부 멤버값도 모두 복사함을 알 수 있다.

 

그러나 구조체 변수를 대상으로 덧셈 및 뺄셈 연산은 불가능하다.

그렇기에 덧셈 함수와 뺄셈 함수를 정의해야 한다.

이렇게 각각 함수로 생성해서 연산해줘야 한다.

물론 이것도 원한다면

이렇게 짧게 만드는것도 문제는 없다.

23-3 구조체의 유용함에 대한 논의와 중첩 구조체

구조체를 정의하는 이유

구조체를 정의하는 이유는 간단히 말해서 연관있는 데이터를 하나로 묶어 관리 및 표현이 용의하게 하기 하기 위함이다.

 

중첩된 구조체의 정의와 변수의 선언

구조체를 정의한 다음에 다른 구조체에서 선언한 구조체를 사용하면 해당변수는 기본자료형 이름과 마찬가지로 사용할 수있다.

예를 들면 아래와 같은 경우이다.

구조체를 하나 정의하고 해당 구조체를 사용하는 다른 구조체 하나를 더 추가로 선언하자.

그러고 이제 이 구조체를 또 사용하는 함수를 정의해보자.

먼저 해당 함수는 구조체 변수의 주소(구조체 포인터 변수 타입을 가짐)를 전달인자로 받아 함수 내부에서 값을 출력해주는 기능을 가지게끔 만들어보자.

 

그리고 이제 이걸 main함수 내부에서 사용해보자.

잘 접근하고 사용하는 것을 볼 수 있다.

 

23-4 공용체(Union Type)의 정의와 의미

공용체는 구조체와 달리 매우 제한적으로 사용된다. 

그렇다고 불필요한건 아니고 경우에 따라서 매우 유용하게 사용되기도 한다.

 

그리고 공용체에 대해서 활용적 측면을 이야기 하기 위해서는 다양한 실무적인 상황에서 필요한 경우가 있기 때문이다.

그게 물론 자주는 아니겠지만 언젠가는 찾을 수 있을 것이다.

 

그러니까 그냥 문법에 대해서만 이해하고 있고 어떨때 사용하는지는 실무를 경험하면서 이럴 때 사용하는 구나 상황을 잘 기억해두기로 하자.

구조체 vs 공용체 : 선언 방식의 차이

우선 공용체를 선언하는 방법에 대해서 알아보자.

그냥 struct라는 키워드가 union으로 변경하기만 하면 공용체를 선언할 수 있다.

그러면 구조체와 공용체의 차이는 무엇일까?

 

구조체 vs 공용체: 실행결과를 통한 관찰

공용체 변수를 이루는 멤버의 시작 주소값은 모두 동일하다.

이렇게 생성한 코드를 한번 확인해보면 

 

구조체를 확인해보면 해당 멤버의 주소값은 8byte의 크기만큼 서로 차이가 있는 반면 공용체의 경우는 모든 멤버의 주소값이 동일하게 출력되는 것을 확인할 수 있다.

그리고 구조체의 경우는 24byte의 크기를 가지고 있는 반면 공용체의 경우는 8byte의 크기를 갖고 있다는 차이점을 확인할 수 있을 것이다.

 

여기서 우리는 아니 그러면 동일한 메모리 공간이면 덮어 씌워지는게 아닌가..?라는 의문점이 생길 것이다.

 

여기서 먼저 생각해볼게 멤버는 동일하게 int, double, long형이기에 4byte, 8byte, 8byte의 크기를 갖고 있는데 왜 24byte일까?

먼저 컴파일러에 따라 다르지만 여기서는 가장 큰 변수의 크기에 맞춰 변수를 정렬해두는 것으로 보인다.

이를 패딩이라고 부르는것 같은데 4바이트의 int형 변수가 메모리공간에 할당 되고 바로 double변수가 오는게 아니라 가장큰 멤벼인 double형 에 맞춰 4바이트의 패딩을 준다. 

그 다음에 8바이트 double형 멤버가 온 다음에 동일한 크기인 long형 멤버가 오는 것이다.

이는 물론 컴퓨터나 OS, 컴파일러등에 따라 다르게 보여줄 수 도 있다.

 

아무튼 근데 중요한건 SVAR이 8바이트라는 것이다.

멤버가 3개가 존재하는데 이 주소값이 동일하다는건 무슨 의미를 가질까?

이건 이미지 하듯이 그냥 ivar와 dvar와 lvar가 같은 공간을 공유 하는 것이다.

 

 그래서 메모리 공간에 할당 될때 공용체 변수가 선언될때 컴파일러가 내부에 있는 멤버중 가장 크기가 큰 변수를 찾고 이걸 기준으로 메모리 공간을 할당한다.

 

그러고 ivar에 접근하면 그 공간중 int형 크기에 맞는 4바이트에 변수의 내용을 작성한다(u.ivar = 10;)

그다음에 dvar에 접근하면 ivar를 할당했던 공간을 포함해서 8바이트에 변수의 내용을 작성한다(u.dvar = 3.14).

lvar도 동일하게 접근한다.

 

그러면 기존에 ivar로 작성되어 있던 값은 그냥 덮어 씌워지는 것이다.

그리고 lvar를 접근하기 전에 작성했던 dvar에 있던값 또한 lvar의 값이 저장되는 순간 덮어 씌워진다.

 

그래서 이걸 공용체라고 부르는 것이다.

 

값을 그냥 덮어 씌워버리는데... 아니 그럼 이걸 어디다 쓰지?

 

구조체 vs 공용체: 메모리적 차이

위에서 봤던 내용을 확인해보자면

이렇게 이전에 생성한 코드에서 구조체에는 초기화를 하면서 생성하고 공용체에는 값을 하나씩 넣어봤다.

해당 코드의 결과를 확인해보면 

이렇게 마지막에 넣어준 값으로 모든 멤버가 덮어씌워진것을 확인할 수 있다.

 

또한 공용체는 값이 덮어 씌워지기에 전체 값을 초기화하면서 선언할 수는 없다.

하지만 단일 멤버만 선언하여 초기화하는 것은 가능하다.

이 경우엔 해당 공유체의 가장 첫번째 멤버에 값을 넣는 방법이고

이렇게 {.멤버명 = 값 }을 대입해서 원하는 멤버를 지정해서 값을 넣어줄 수 도 있다.

 

공용체의 유용함: 문제 제시

이번엔 예를 통해서 공용체의 활용도에 대해서 확인해보자.

사용자로 부터 int형 정수를 하나 입력받아라.
입력받은 정수의 상위 2바이트와 하위 2바이트 값을 양의 정수로 출력하라.
상위 1바이트와 하위 1바이트에 저장된 값을 아스키 문자로 출력하라.

이를 위해 사용할 수 있는 공용체는 

typedef struct dbshort{
    unsigned short upper;
    unsigned short lower;
} DBShort;

typedef union rdbuf{
    int iBuf;
    char bBuf[4];
    DBShort sBuf;
} RDBuf;

이다.

 

여기서 나는 물론 이해가 잘안되서 해결부터 봤지만 키가 되는 부분은 공유체는 하나의 공간을 모든 멤버가 공유한다는 것이다.

먼저 int iBuf, char bBut[4], DBShort(unsigned short *2)의 크기는 모두 4바이트로 동일하다.

(int형 4바이트, char형 1바이트 *4 = 4바이트, unsigned short형 2바이트 * 2 = 4바이트)

 

그렇기에 사용자한테 입력을 받아서 iBuf에 저장하면 상위 2바이트와 하위 2바이트는 각각 DBShort 구조체의 upper와 lower를 통해서, 상위 1바이트와 하위 1바이트는 char형 배열의 첫번째 값인 bBuf[0]와 bBuf[3]이 갖고 있을 것이다.

 

이걸 통해서 코드를 만들어보면

이렇게 결과를 확인할 수 있다.

23-5 열거형(Enumerated Type)의 정의와 의미

C언어에서 열거형(enum)은 명명된 정수 혹은 상수의 집합을 정의하는 사용자 정의 데이터 타입으로 열거형을 선언하기 위한 키워드는 enum이다.

쉽게 말하자면 관련된 상수들을 하나의 이름 아래에 모아 놓는것이다.

이 열거형은 구조체, 공용체와 동일하게 자료형을 정의하는 방법중 하나이다.

 

열거형 또한 구조체, 공용체와 동일하게 정의하고 이를 통해서 변수를 선언할 수 있다.

이러면 다 똑같구나 라고 생각할 수 있으나 선언 방식, 사용방식을 제외한 각각의 성격들은 구조체나 공용체는 조금 비슷한 면이 있을 수 있는데 열거형은 전혀 다른 성격을 갖고 있다.

 

예를 들어 Day라는 열거형을 정의한다면 

enum Day{
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
};

와 같이 작성해주면 열거형을 정의할 수 있다.

그리고 변수를 선언하는 방법도

enum Day date;

로 구현체, 공용체와 비슷한 결을 가진다.

또한 typedef선언 또한 가능한데 이는 조금 있다가 보고 우선 이 열거형 자료형을 정의하는 것이 어떤 의미를 가지는지 알아보자.

 

기존에 구조체는 변수들을 하나의 구조체로 묶어주는데에 의미를 갖고 있었고 공용체는 묶인 멤버가 공간을 공유하는 것에 의미를 갖고 있었다.

열거형의 경우는 내가 변수에 저장할 값을 지정하겠다, 내가 사용할 특정값들의 목록을 만들겠다는 의미가 강하다.

기존에 구조체, 구현체의 경우는 타입과 변수명만 지정했지 값을 정해주진 않았는데 열거형의 경우는 값을 모두 지정해준다.

enum Day{
    MONDAY=1,
    TUESDAY=2,
    WEDNESDAY=3,
    THURSDAY=4,
    FRIDAY=5,
    SATURDAY=6,
    SUNDAY=7
}

 

와 같이 Day라는 열거형 타입이 정의되었다.

그러면 Day형 타입의 변수가 선언되었을때 그 변수에 저장할 수 있는 값의 종류가 위에 열거되어 있다.

그렇기에 열거형이란 이름이 붙었다.

그래서 이 열거형 상수들에는 결과적으로 1부터 6까지의 값을 저장할 수 있다고 작성해둔 것이다.

그리고 이렇게 enum을 선언해두면 나중에 MONDAY 부터 SUNDAY가 상수가 되어 선언이 되고 이를 사용할 수 있게 된다.

다시 말하면 enum Day를 선언하는 순간 7개의 상수가 선언이 되고 이 상수들을 열거형 상수라고 한다.

 

이 열거형 타입 Day를 사용해서 변수를 생성하는 방법은

enum Day date = WEDNESDAY;

이렇게 선언하고 그러면 date에는 정수 3 저장될 것이다.

 

이게 가능하냐고 물어본다면 가능하다.

enum으로 타입을 선언하는 경우 사실 저게 묶어놓은것 처럼 보이나 저 내부에 생성된 상수들은 어디서나 접근이 가능한 상수로 선언이 되는 것이다.

 

열거형의 정의와 변수 선언의 예

먼저 위에서 생성한 날짜에 관련된 열거형 Day 타입을 선언해주자.

그리고 이걸 상수로 사용해서 해당 내용에 해당하는 값을 모두 출력해주는 main문을 생성해보자.

이렇게 그냥 상수로 사용이 가능하다.

 

여기서 혹시 

enum Day{
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
};

이렇게 선언하면 값이 어떻게 선언이 될까?

 

값을 지정하지 않고 열거형을 생성하면 순차적으로 0부터 n의 값을 부여받는다.

typedef enum Day {
	MONDAY,    // => 0
	TUESDAY,   // => 1
	WEDNESDAY, // => 2
	THURSDAY,  // => 3
	FRIDAY,    // => 4 
	SATURDAY,  // => 5
	SUNDAY     // => 6
}DAY;



그런데 만약 어떤건 값을 할당하고 어떤건 값을 할당하지 않았다면 명시되지 않은 상수는 앞에 정의된 상수의 값에서 그냥 1증가한 값으로 결정된다.

typedef enum Day {
	MONDAY = 1,
	TUESDAY,
	WEDNESDAY = 7,
	THURSDAY,
	FRIDAY = 10,
	SATURDAY,
	SUNDAY = 20
}DAY;

열거형은 변수의 선언 목적이 아닌 상수를 선언할 목적인 경우에만 생성하고 열거형의 이름과 typedef의 선언을 생략하기도 한다.

이 열거형을 선언하는 목적은 둘 이상의 연관이 있는 이름의 상수를 선언해서 프로그램의 가독성을 높이기 위함이 크다.

 

반응형