본문 바로가기

컴퓨터공학/C, C++

C,C++> 구조체

구조체

C 데이터 타입 중 가장 덩치가 크다. 정수나 실수 또는 문자열 등 단순한 형태로 나타낼 수 없는 복잡한 데이터를 표현할 때 구조체를 사용한다. 

구조체는 타입이 다른 변수 집합이다. 배열은 타입이 같은 변수 집합이다. 구조체라고 모든 멤버 타입이 반드시 다를 필요는 없다. 

구조체 속한 변수들을 멤버라고 한다. 멤버가 될 수 있는 타입은 전혀 제한이 없다. 

구조체 선언은 구조체 모양을 컴파일러에게 알릴 뿐이다. 따라서 초기값을 줄 수 없다. 

 

구조체 태그

보통 태그를 먼저 정의하고 구조체 변수를 선언한다. 태그 선언문은 컴파일러에게 구조체 모양이 어떻다는 걸 등록한다. 메모리를 할당하거나 변수를 생성하지 않는다. 중복선언해도 상관없다. 

 

struct { char Name[10]; int Age; double Height; } Friend;
struct tag_Friend { char Name[10]; int Age; double Height; }; tag_Friend Friend;
typedef struct { char Name[10]; int Age; double Height; } FriendType; FriendType Friend;

위 세 선언문은 동일한 구조체 변수이다. 

 

멤버 참조

멤버 연산자 

배열 요소 참조 방식이 단순한 이유는 요소 크기가 일정하고 서로 인접하기 때문이다. 그러나 구조체는 멤버 타입이 제각각 다르며 크기도 다르다. 따라서 순서값을 사용할 수 없으며 별도 연산자와 멤버 이름을 사용해야 한다. 

구조체 멤버를 읽을 때 멤버 연산자를 사용하는데 모양은 마침표이다. 

 

열거형이나 구조체 같은 사용자 정의 타입은 main 함수 이전에 선언해야 모든 함수에서 이 타입을 사용할 수 있다.

main 함수 안에 구조체 태그를 선언할 수 있지만 main 함수 안에서만 사용할 수 있는 지역 타입이 된다. 

태그 정의는 실제로 변수를 생성하는 것이 아니므로 앞쪽에 선언해서 컴파일러가 먼저 알 수 있게 하는 게 좋다.

구조체 시작 번지에서 멤버까지 거리인 오프셋(offset)을 더해 멤버를 읽는다. 

구조체는 멤버 집합이며 메모리에 생성될 때 선언된 멤버가 순서대로 할당된다. 

컴파일러는 구조체가 선언될 때 각 멤버 오프셋과 타입을 기억한다. 

그리고 멤버 참조 문장을 보면 구조체 시작 주소값에서 오프셋을 더한만큼 이동하고 이 위치에서 멤버 타입 길이만큼 값을 읽는다. 이 동작을 하는 연산자가 바로 . 연산자이다. 

 

포인터 멤버 연산자 

포인터 연산자가 2순위고 멤버 연산자가 1순위라서 괄호를 생략할 수 없다. 

괄호를 반드시 써야해서 가독성이 떨어지기 때문에 화살표 연산자(Arrow Operator)가 있다. 

좌변에 구조체 포인터, 우변에 멤버 이름을 입력하여 포인터가 가리키는 구조체 멤버를 읽는 연산을 한다.

 

prk 구조체를 가리키는 포인터고 m이 멤버일 때,

(*p).m 은 p->m과 같다.

 

구조체 초기화

선언과 동시에 대입할수 있다.

초기화 방법은 배열과 비슷해서 선언 시 = 구분자와 { } 괄호를 쓰고 괄호 안 멤버 초기값을 나열하면 된다. 

크기가 큰 구조체를 초기화할 때 지역 선언은 피하자. 컴파일러가 구조체 초기식을 대입문으로 바꿔서 기록하기 때문에 구조체 초기화는 많은 시간이 걸린다. 함수 호출할 때마다 값을 대입하느라 느려진다. 

초기화가 필요한 구조체나 배열은 보통 참고용 정보라서 읽기 전용 속성을 갖는 경우가 많다. 

이런 경우 한 번 초기화한 후 바뀌지 않기 때문에 매번 초기화할 필요가 없다. 

따라서 전역 변수로 한 번 초기화하는 것이 좋다. 

함수 내부로 통용 범위를 두고 싶으면 static으로 선언하자. 

 

 

구조체 대입

구조체가 배열과 다른 차이점은 대입이 가능하다는 점이다. 

배열 이름은 시작 번지를 가리키는 포인터 상수이기 때문이다. 

따라서 좌변값이 아니며 대입식 좌변에 놓을 수 없다. 

하지만 구조체는 대입을 허용한다. 

컴파일러가 구조체 이름을 좌변값으로 인정하기 때문이다. 

구조체는 대입이 가능해서 함수 인수나 리턴값으로 사용할 수 있다. 

함수 호출 시 형식 인수가 실인수로 전달되는 과정은 대입 연산이기 때문에 구조체 그 자체를 인수로 사용할 수 있다. 

그러나 실제 구조체를 함수 인수로 사용하는 경우는 별로 없다. 왜냐하면 구조체가 커지면 인수 절달에 많은 시간과 메모리를 소모하기 때문이다. 따라서 구조체보다 포인터를 사용하는 방법이 더 효율이 좋다. 

포인터를 사용하면 참조 호출했으므로 함수 내부에서 구조체를 변경할 수 있다. 

그리고 성능상 포인터 방식이 더 빠르다. 

 

지역변수는 함수 종료 시 사라진다. 지역변수를 반환하면 지역변수 자체가 아니라 지역변수 복사본이다. 즉시 이 값은 다른 구조체가 대입받는다. 대입받지 않으면 버려진다. 

하지만 지역변수 구조체 주소값을 리턴하면 안된다. 곧 사라질 지역변수 주소값을 반환했다고 경고가 생긴다.

스택에 저장된 값은 다른 함수 호출 시 파괴된다. 다른 변수 뿐만 아니라 지역변수로 선언된 구조체 주소값을 반환하는 건 옳지 않다.

값은 임시 사본이 반환된다.

구조체를 동적 할당해서 주소값을 반환하면 가능하다.

동적 할당 메모리가 일부러 파괴하지 않는 한 계속 보존하기 때문이다. 

 

깊은 복사 

구조체 멤버 중 포인터가 있고, 이 포인터가 외부 대상을 참조하고 있다면, 사본을 만들 수 없다.

왜냐하면 주소값도 같이 복사가 되어서 두 개가 동시에 하나를 참조하기 때문이다. 

동적 할당한 메모리를 공유했을 때 하나를 바꾸면 양쪽 모두 영향을 받는다. 

대입으로 일시적으로 같은 상태이지만 서로 종속 관계가 되었으므로 사본이라고 할 수 없다. 

사본은 서로 영향을 주면 안된다. 

그리고 동적 할당했으므로 파괴하기 전에 메모리 해제를 해야하는데 메모리 해제하면 같이 해제되므로 이상 동작한다.

대입연산자로 단순 대입하여 구조체 사본을 만드는 것을 얕은 복사(Shallow Copy)라고 한다. 

구조체 멤버가 정수나 실수같은 단순 타입으로만 있으면 얕은 복사로 사본을 만들 수 있지만 포인터가 포함하면 대입으로 똑같은 주소값을 가리키는 문제점이 있다. 

포인터는 별도 메모리를 할당하고 내용을 복사해야 두 변수가 독립성을 갖는다. 

이런 식으로 포인터 멤버는 주소값을 바로 대입하지 않고 필요한 길이만큼 따로 할당하고 원본 내용만 복사하는 것을 깊은 복사(Deep Copy)라고 한다. 

 

비트 구조체 

정의

비트 구조체는 비트를 멤버로 가지는 구조체이며 비트 필드라고 부른다. 

멤버가 가질 수 있는 값 범위가 작다면 32비트 int나 8비트 char보다 더 작은 단위로 비트를 쪼개 정보를 기억할 수 있는데 이 때 사용하는 것이 비트 구조체이다. 

각 멤버 이름 다음에 비트 크기를 적는다. 멤버 타입은 원칙적으로 정수만 가능하다. 부호 여부에 따라 타입을 지정한다. 최상위 1비트를 부호 비트로 할당할 수 있다. 하지만 비트로 표현하는 정보는 기호나 표식인 경우가 많아서 부호를 쓰는 경우는 드물다. 따라서 통상 unsigned 타입이다. 

struct tag_bit {
	unsigned short a:4;
    unsigned short b:3;
    unsigned short c:1;
    unsigned short d:8;
};

멤버 선언 순서대로 할당되며 구조체 크기는 모든 비트멤버 총 비트수와 같다. 

비트 필드 순서대로 MSB에 저장될 지 LSB에 저장될지 컴파일러마다 다르다. 

일반적으로 오른쪽(LSB)부터 채워 나간다. 

왼쪽이 상위 비트이며 오른쪽이 하위 비트이다. 

 

비트 구조체 특징은 다음과 같다.

1. 멤버 이름을 생략할 수 있다.  하지만 이름 없는 멤버는 코드에서 참조할 수 없으며 자리만 차지한다. 

하지만 왜 필요할까? 경계를 걸치면 값을 읽고 쓸 때 쉬프트 연산을 해야 하며 속도가 떨어지기 때문이다.

메모리는 바이트 단위이고 1비트를 버린다고 기억 장소가 낭비되는 것은 아니다. 15비트나 16비트나 필요한 메모리는 2바이트이고 1비트를 버려서 속도를 증가하는게 더 좋다. 

 

2. 이름이 없는 비트의 크기를 0으로 지정할 수 있다. 이러면 현재 워드의 미사용 비트를 모두 버린다. 크기가 0인 멤버 다음 멤버는 새로운 워드의 경계에 배치된다. 역시 속도를 증가시킨다. 

struct tag_bit {
	unsigned short a:4;
    unsigned short  :0;
    unsigned short c:1;
    unsigned short d:8;
};

 

이러면 a가 최하위 4비트를 차지한다. c는 다음 워드 경계에서 새로 시작한다. a의 왼쪽 12비트가 버려진다. 

워드는 컴퓨터 설계시 정해지는  메모리 기본 단위라서 컴퓨터마다 다르다. 

 

3. 비트 멤버는 자신 타입보다 큰 비트 크기를 가질 수 없다. 비트 멤버란 일부만 사용하는 것이다. 

 

4. 비트 멤버는 값을 읽고 쓸 수 있는 좌변값이다. 보통 좌변값은 & 연산자를 사용할 수 있지만 비트멤버는 & 연산자를 사용할 수 없다. 비트 멤버는 메모리 점유하고 있지만 바이트 단위 주소를 가지는 것이 아니고 구조체 일부로써 존재하기 때문이다. 좌변값이면서 &연산자를 쓸 수 없는 대상은 비트 필드와 register 기억 부류 두 가지이다. 

 

5. 

비트 필드와 일반 멤버를 한 구조체에서 같이 선언할 수 있다. 

 

비트 구조체는 메모리를 낭비하지 않고 쓸 수 있다는 장점이 있지만 속도는 느리다.

게다가 예전처럼 메모리가 부족하지 않아서 굳이 쓸 필요가 없다. 

 

공용체

정의

구조체와 같으며 선언 문법이나 사용하는 방법은 같다. 다만 공용체 멤버들이 기억 장소를 공유하다는 것만 다르다. 

공용체 멤버들은 항상 공용체 선두 번지와 같은 공간에 배치된다. 따라서 a에 어떤 값을 대입하면 b 값도 덩달아 바뀐다. 

한 번에 하나의 멤버만 유효한 값을 갖는데 왜 기억 장소를 공유할까? 

그 이유는 두 개의 멤버가 같은 공간에 있으면 원하는 타입으로 선택해서 읽고 쓸 수 있기 때문이다. 

똑같은 값의 다른 표인이라든가 값 자체가 표현하는 목적이 동일해야 공용체가 될 수 있다.

예를 들어 ip주소는 32비트 부호없는 정수이지만 표기할 땐 한 바이트씩 10진수로 쓴다. 

union tag_ip {
	unsigned long addr;
    unsigned char sub[4];
};

표기를 위해서 sub 배열을 읽는 것이 편리하고 실제 주소값을 전달할 땐 addr 멤버를 읽는 것이 효율적이다. 

 

공용체를 선언하면서 초기화할 수 있지만 첫 번째 멤버만 초기값을 줄 수 있다. 

 

이름없는 공용체

이름없는 공용체는 변수를 같은 기억 장소를 공유하도록 묶어주는 역할만 한다. 

공용체 변수 이름이 지정되지 않았으므로 a와 b는 변수 쓰듯이 그냥 a=3, b=14식으로 사용한다.

따라서 이름없는 공용체 멤버는 공용체 변수 소속이 아니므로 공용체 바깥 변수와 명칭이 중복되면 안된다. 

union {
	int a;
    double  b;
} Name;
char a; //가능

union {
	int a;
    double  b;
};
char a; //불가능

위는 Name.a 같이 소속을 밝혀야 한다. 그리고 a라는 이름의 변수를 또 선언할 수 있다. 

아래는 같은 이름의 a를 변수로 선언할 수 없다. 

 

이름없는 공용체는 주로 구조체 내 멤버를 선언할 때 사용한다. 

C 컴파일러는 이름없는 공용체를 지원하지 않아서 반드시 이름이 있어야 하고 

C++ 컴파일러는 지원하므로 간단하게 할 수 있다. 

공용체 이름을 지정하지 않으면 마치 구조체에 속한 것처럼 편리하게 사용할 수 있어 편리하다. 

 

 


구체적인 코드가 아니라 문제를 푸는 방식을 배워야 한다.  

코드가 똑같이 반복되는 경우는 찾기 힘들다. 

프로그램에 사용되는 자료구조, 알고리즘은 천차만별이고 풀고자 하는 문제에 가장 적합한 알고리즘은 매번 선택해야하기 때문에 재사용이 어렵다. 

OOP나 컴포넌트 방식 등 개발 방법은 많이 소개되어 있지만 이런 건 재활용을 쉽게 해 줄 뿐이지 완벽한 재활용을 보장하지 않는다. 

코드에 집착하지 말고 문제 푸는 패턴에 집중해야 한다. 

코드는 재사용되지 않지만 문제 푸는 방법은 반복된다. 

문제 해결법을 익힌 사람은 비슷한 문제를 응용할 수 있으며 유사 코드를 쉽게 만든다. 

남이 만든 코드를 볼 때 문구 한 줄에 집착하지 말고 문제 푸는 방식을 파악할 수 있는 거시적인 안목을 가져야 한다. 

'컴퓨터공학 > C, C++' 카테고리의 다른 글

C,C++> 함수 고급  (0) 2022.07.27
C,C++> 포인터 고급  (0) 2022.07.25
C,C++> 배열과 포인터  (0) 2022.07.19
C,C++> 포인터  (0) 2022.07.05
C,C++> 배열  (0) 2022.06.09