1. 구조체
- 구조체는 C의 데이터 타입 중에 가장 덩치가 크다.
- 정수나 실수 또는 문자열 등의 단순한 형태로 나타낼 수 없는 복잡한 데이터를 표현할 때 구조체를 사용한다.
- 배열이 타입이 같은 변수들의 집합인 것에 비해 구조체는 다른 타입을 가지는 변수들을 하나의 이름으로 묶어둔 것이다.
- 구조체 변수 선언
struct {
char Name[10];
int Age;
double Height;
int x,y;
} Friend;
printf("%d, %d\n", Point.x, Point.y);
- 타입이 같은 변수들도 하나의 구조체로 묶을 수 있으며(
int x,y;), 멤버를 하나만 가지는 구조체를 선언하는 것도 가능하다.
- 구조체 테그
- 구조체 태그를 먼저 정의하고 이 태그로 구조체 변수를 선언할 수 있다.
- 구조체 태그는 타입에 대해 이름을 붙이는 것이다.
- 구형 C 컴파일러는 태그를 사용할 때 구조체 태그라는 것을 명확하게 알리기 위해 struct라는 키워드를 태그 앞에 붙여야 한다.
struct tag_Friend{
char Name[10];
int Age;
double Height;
int x,y;
};
void main(){
struct tag_Friend Friend;
}
- 새로운 타입을 정의 하는 typedef문을 사용하면 태그를 정의하는 것과 동일한 효과를 낼 수 있다.
typedef struct {
char Name[10];
int Age;
double Height;
int x,y;
} FriendType;
void main(){
FriendType Friend;
}
- 타입 처럼 이용할 수 있으므로 이 타입으로부터 파생되는 유도형 변수를 선언할 수 있다.
- 예시 :
tag_Friend *pFriend;, tag_Friend pFriend[100];
- 참고로 태그도 정의하면서 변수도 같이 선언하는 형식도 사용할 수 있지만 자주 애용되는 방법은 아니다.
2. 멤버의 참조
2.1. 멤버 연산자
- 구조체의 멤버를 읽을 때는 멤버 연산자(.)를 사용한다. "구조체명.멤버명" 형식으로 사용한다.
void main(){
struct tag_Friend Friend;
strcpy(Friend.Name,"아무개");
Friend.Age=30;
Friend.Height=178.2;
printf("%s, %d, %.1f\n",Friend.Name,Friend.Age,Friend.Height);
}
- 열거형이나 구조체 같은 사용자 정의 타입은 가급적이면 main 함수 이전에 선언해야 모든 함수에서 이 타입을 사용할 수 있다.
- main 함수 안에 구조체 태그를 선언할 수도 있지만 이렇게 되면 이 구조체는 main 함수 안에서만 사용할 수 있는 지역 타입이 된다.
- 구조체 멤버들은 크기가 제각각이기 때문에 배열처럼 단순한 곱셈으로 멤버의 위치를 찾을 수 없으며, 구조체 시작 번지로부터 멤버까지의 거리인 오프셋(offset)을 더해 멤버를 읽는다.
- 컴파일러는 구조체가 선언될 때 멤버의 오프셋과 타입을 기억해 둔다.
그리고 멤버를 참조하는 문장을 만나면 구조체의 시작 번지에서 오프셋을 더한만큼 이동한 후 이 위치에서 멤버의 타입 길이만큼 값을 읽도록 코드를 생성할 것이다.
이런 동작을 하는 연산자가 바로 . 연산자다.
2.2. 포인터 멤버 연산자
- 구조체에 대해서도 배열과 포인터를 선언할 수 있다고 했다.
struct tag_Friend Friend;
struct tag_Friend *pFriend;
pFriend=&Friend;
(*pFriend).Age=30;
- 위 예에서
(*pFriend).Age의 괄호는 생략할 수 없다. (포인터 연산자는 2순위이고 멤버 연산자는 1순위이기 때문)
- 구조체를 가리키는 포인터 p로 이 구조체의 멤버 m을 읽고 싶다면
(*p).m 연산문을 사용해야 하는데,
포인터로 부터 구조체 멤버를 참조하는 이런 연산문은 굉장히 자주 사용되는데 괄호를 반드시 써야 하기 때문에 보기에 좋지 않으며 별도의 포인터 멤버 연산자라는 것을 만들어 놓았는데 이 연산자가 바로 -> 연산자다.
- 포인터 멤버 연산자
-> 좌변에는 구조체 포인터, 우변에 멤버 이름을 취하며 포인터가 가리키는 번지에 저장된 구조체의 멤버를 읽는 연산을 한다.
struct tag_Friend Friend;
struct tag_Friend *pFriend;
pFriend=&Friend;
strcpy(pFriend->Name,"아무개");
pFriend->Age=30;
pFriend->Height=178.2;
printf("%s, %d, %.1f\n",
pFriend->Name, pFriend->Age,pFriend->Height);
2.3. 구조체 배열
struct tag_Friend *pJuso[10];
int i;
for (i=0;i<10;i++) {
pJuso[i]=(struct tag_Friend *)malloc(sizeof(struct tag_Friend));
}
pJuso[3]->Age=40;
for (i=0;i<10;i++) {
free(pJuso[i]);
}
2.4. 중첩 구조체
- 중첩 구조체란 다른 구조체를 멤버로 포함하는 구조체다.
- 단, 자기 자신을 포함하는 구조체는 선언할 수 없다.
- 또한 같은 상호 중첩도 안 된다.
- A구조체가 B구조체를 포함하고, B구조체가 A구조체를 포함한 형태
- 자기 자신을 포함시키는 구조체는 불가능하지만 자신과 같은 타입의 구조체에 대한 포인터를 멤버로 가지는 것은 가능하다.
- 자신과 같은 타입의 포인터를 멤버로 가지는 이런 구조체를 자기 참조 구조체라고 한다.
- 이러한 구조체는 연결 리스트나 트리 구성에 아주 요긴하게 사용되는 자료 구조다.
3. 구조체의 초기화
- 구조체의 각 멤버에 원하는 값을 대입할 수 있는 멤버가 아주 많다면 일일이 대입하기 무척 귀찮을 것이다.
- 그래서 구조체를 선언함과 동시에 멤버의 값을 초기화시킬 수 있는 방법이 제공된다.
- 배열과 거의 비슷한데, 선언시에
= 구분자와 { } 괄호를 쓰고 괄호 안에 멤버의 초기값을 나열하면 된다.
- 초기값이 없는 멤버는 자동으로 0으로 초기화된다.
- 배열과 다른 점이라면 초기값이 대응되는 멤버의 타입과 같아야 한다.
struct tag_Friend Friends[10]={
{"아무갑", 30, 178.2, 1, 2},
{"아무을", 42, 169.8},
{"아무병", 26, 176.5, 5, 6},
};
printf("%s, %d, %.1f\n",
Friends[2].Name,Friends[2].Age,Friends[2].Height);
- 구조체가 배열과 다른 가장 큰 차이점은 대입이 가능하다는 점이다.
- 구조체끼리 대입 연산 동작은 구조체의 길이만큼 메모리 복사로 정의되어 있다.
- 배열과 달리 구조체에 대해서 이러한 방법을 허용하는데 컴파일러가 구조체의 이름을 좌변값으로 인정하기 때문이다.
- 구조체는 대입 가능하기 때문에 함수의 인수나 리턴값으로 사용할 수 있다.
- 배열은 대입이 안 되기 때문에 배열 자체를 인수로 전달할 수는 없고 배열을 가리키는 포인터를 전달해야하는 것과는 구분됨.
- 구조체를 함수의 인수로 전달할 수 있는 것은 굉장히 편리한 기능이다.
- 그러나 실제로 구조체를 함수의 인수로 직접 사용하는 경우는 별로 없다.
- 구조체가 커지면 인수 전달에 그만큼 시간을 필요로 하고 메모리도 많이 소모하기 때문에 구조체보다는 포인터를 사용하는 방법이 더 효율적이다.
- 하지만 함수 내부에서 구조체를 변경할 수 있다.
번지를 알고 있으므로 -> 연산자로 실인수 자체를 읽고 쓸 수 있다.
void OutFriend(struct tag_Friend f)
{
printf("%s, %d, %.1f\n", f.Name, f.Age, f.Height);
}
void main()
{
struct tag_Friend Friend={"아무개", 30, 180.0 };
OutFriend(Friend);
}
void OutFriend(struct tag_Friend *pf)
{
printf("%s, %d, %.1f\n", pf->Name, pf->Age, pf->Height);
}
void main()
{
struct tag_Friend Friend={"아무개", 30, 180.0 };
OutFriend(&Friend);
}
- 구조체가 인수로 사용될 수 있는 것처럼 리턴값으로도 사용될 수 있다.
struct tag_Friend GetFriend()
{
struct tag_Friend t;
strcpy(t.Name,"아무개"); t.Age=22; t.Height=177.7;
return t;
}
void main()
{
struct tag_Friend Friend;
Friend=GetFriend();
printf("%s, %d, %.1f\n",
Friend.Name,Friend.Age,Friend.Height);
}
- 리턴되는 값은 지역변수 자체가 아니라 지역변수의 복사본이며 리턴되는 즉시 이 값을 다른 구조체가 받지 않으면 리턴된 구조체는 버려진다.
- 구조체 지역 변수의 포인터를 리턴하는 것은 안된다.
- 당연히 사라질 수 있는 지역변수의 번지를 리턴하는 것이기 때문이다.
- 예를 들어 GetFriend 함수를 호출한 뒤 printf 함수를 실행하면 기존에 GetFriend가 사용해서 스택 영역을
printf가 사용하면서 GetFriend의 지역변수 정보가 파괴 될 수 있다.
- 즉, 스택상의 번지(지역변수 번지)는 리턴 직후에만 유효하며 다른 함수를 호출하는 즉시 파괴되는 성질을 가지고 있다.
- 만약 함수 내에에서 지역변수 t가 아닌 malloc으로 동적 할당한 구조체의 번지를 리턴한다면 이 경우는 가능하다.
* 동적으로 할당된 메모리는 일부러 파괴하지 않는 한 그 내용을 계속 보존하기 때문이다.
- 구조체끼리 대입에 의해 예상치 몫한 문제가 발생하는 경우가 있다.
- 예를 들어 구조체 멤버 중에 포인터 타입이 있다면, 구조체 대입 연산시 이 포인터의 멤버의 주소값도 복사가 될 것이다.
- 하지만 결론적으로 복사의 대상의 구조체(src)의 이 포인터 멤버와 새로 복사받은 구조체(dest)의 포인터 멤버는 같은 곳을 가리키고 있다.
- 즉, src 구조체의 포인터 멤버의 대상체를 변경하면 dest 구조체의 포인터 멤버 또한 동일하게 변경된 대상체를 가질 수 밖에 없다.(역으로 그렇다.)
- 이렇게 단순한
=을 이용한 복사를 얕은 복사라고 한다.
- 따라서 깊은 복사를 위해
= 연산을 통해 복사 받은 후 별도로 dest 구조체의 포인터 멤버에 malloc을 이용하여 별도의 다른 주소값을 기재함으로써 깊은 복사를 구현할 수 있다.
struct tag_Friend {
char *pName;
int Age;
double Height;
};
struct tag_Friend Albert={NULL,80,165.0};
struct tag_Friend Kim;
Albert.pName=(char *)malloc(32);
strcpy(Albert.pName,"알버트 아인슈타인");
Kim=Albert;
printf("이름=%s, 나이=%d, 키=%.1f\n",Kim.pName,Kim.Age,Kim.Height);
strcpy(Albert.pName,"아이작 뉴튼");
printf("이름=%s, 나이=%d, 키=%.1f\n",Kim.pName,Kim.Age,Kim.Height);
free(Albert.pName);
4. 비트 구조체
- 비트 구조체는 비트들을 멤버로 가지는 구조체이며 비트 필드(bit field)라고도 부른다.
- 비트 구조체를 선언하는 기본 형식은 각 멤버 이름 다음에 ':'를 입력하고 이 멤버의 비트 크기를 적는다.
- 비트 구조체는 메모리를 구성하는 최소 단위인 1비트까지도 알뜰하게 사용할 수 있다는 것이 장점이다.
- 여러 가지 값들을 꼭 필요한 만큼 비트를 잘게 쪼개 값을 기억시킬 수 있으므로 매모리 효율이 아주 좋다.
- 멤버의 타입은 원칙적으로 정수만 가능하며 부호의 여부에 따라 unsigned int 또는 signed int 둘 중 하나의 타입을 지정한다.
- 하지만 컴파일러에 따라서 short, long, char 등 정수와 호환되는 모든 타입을 허용한다.
- 그래서 비트 멤버의 타입으로는 모든 정수형을 다 사용할 수 있으며
!! 멤버의 타입에 따라 비트 필드 전체의 크기가 달라진다.
struct tag_bit {
unsigned short a:4;
unsigned short b:3;
unsigned short c:1;
unsigned short d:8;
};
void main()
{
struct tag_bit bit;
bit.a=0xf;
bit.b=0;
bit.c=1;
bit.d=0xff;
printf("크기=%ld, 값=%x\n", sizeof(bit), *(short*)&bit);
}
- 멤버가 선언된 순서대로 하위 비트에서 순서대로 할당되며 구조체 자체의 크기는 모든 비트 멤버의 총 비트수와 같다.
- 비트 필드가 선언 순서대로 MSB에 저장될지 LSB에 저장될지는 컴파일러마다 다른데 일반적으로 오른쪽(LSB)부터 채워 나간다.
- 비트 멤버의 이름을 생략할 수 있다.
unsigned short a:4; unsigned short b:3; unsigned short :1; unsigned short d:8;
- 이름이 없는 멤버는 코드에서 칭할 수 없으므로 참조할 수 없으며 자리만 차지한다.
- 이렇게 되면 세 번재 멤버는 괜히 1비트를 그냥 버리는 역할만 한다.
- 이런 것이 필요한 이유는 바이트나 워드의 경게에 걸치면 값을 쓸 때 쉬프트 연산을 해야 하며 따라서 속도가 떨어지기 때문이다.
- 그래서 다음 멤버가 바이트의 처음부터 시작할 수 있도록 1비트를 버리기 위해 이름없는 멤버를 하나 적어준다.
- 이름이 없는 비트의 크기를 0으로 지정할 수 있다.
unsigned short a:4; unsigned short :0; unsigned short d:8;
- 이렇게 되면 현재 워드의 미사용 비트를 모두 버린다.
- 크기가 0인 멤버 다음의 멤버는 새로운 워드의 경계에 배치됨으로써 역시 속도를 증가시키는 효과가 있다.
- 예제를 2진수로 표현하자면 '11111111 0000 0000 0000 1111' 로 워드 길이 중에 멤버 a를 1111로 채운 이후에 남은 길이를 버리고 새로운 워드에 멤버 d를 채운다.
- 참고로 이 구조체의 크기는 어쩔 수 없이 4 바이트가 된다.
- 비트 멤버는 자신의 타입보다 더 큰 비트 크기를 가질 수 없다.
- 비트 멤버는 값을 읽을 수도 있고 쓸 수도 있는 좌변값이다.
- 일반적으로 좌변값은
&연산자를 사용할 수 있지만 비트 멤버에 대해서는 예외적으로 &연산자를 사용할 수 없다.
- 비트 필드와 일반 멤버를 한 구조체에 같이 선언할 수도 있다.
- 메모리가 넉넉하다는 가정하에 비트 구조체를 이용하여 메모리를 절약할 필요도 없고 (대부분의 경우 쉬프트 연산을 사용해야 할 수도 있으므로, 이러한 경우에) 속도면에서 불리한 단점이 있어 잘 사용되지 않는다.
5. 공용체
-
공용체(Union)는 모든 면에서 구조체와 같으며 선언 문법이나 사용하는 방법이 동일하다.
- 다만 고용체에 속한 멤버들이 기억 장소를 공유한다는 것만 다르다.
-
struct {int a; short b[2];} st;
union {int a; short b[2];} st;
- 구조체의 경우 8바이트가 할당되지만, 공용체의 경우 4바이트가 할당된다.
- 공용체의 경우 a에 4바이트 값을 입력하던지 b의 2바이트 2개의 요소를 입력할 수 있다.
-
공용체 멤버들은 항상 공용체의 선두 번지와 같은 공간에 배치되는데 a의 번지나 b의 번지나 동일하다.
- 따라서 a에 어떤 값을 대입하면 같은 기억 공간에 존재하는 b의 값도 덩달아 바뀌게 되며 반대로 b를 변경하면 a도 같이 변경된다.
- b의 경우 일텔 계열의 CPU는 정수값을 거꾸로 저장(역워드 방식)하므로
b[1]이 a의 상위 필드 b[0]이 a의 하위 워드를 가진다.
-
기억 장소를 공유하는 이유는 두 개의 멤버가 같은 공간에 배치되어 있으면 원하는 타입을 선택해서 쓸 수 있기 때문이다.
- 이때 공용체에 속한 멤버들은 전혀 상관 없는 값이 아니라 논리적으로 유사한 값이어야 한다.
-
활용 예
union tag_ip{unsigned long addr; unsigned char sub[4];};
- addr을 통해 32비트 IP를 다 읽을 수 있고, sub를 통해 8비트씩 원하는 섹션(IP자리)를 읽을 수 있다.
union tag_unit{int mili; double inch;};
- 두 가지 단위 중 원하는 단위의 수치를 입력할 수 있다 (한 덩어리에 두 가지 방식을 입력할 수 있다는 점이 포인트)
- 단 이 경우 공용체에 저장된 값이 어떤 멤버를 기준으로 (밀리인지 인치인지) 알 수 있는 방법은 없다.
- 필요하다면 공용체가 어떤 타입의 값을 저장하고 있는지를 기억하는 별도의 변수를 둘 수 있다.
-
공용체도 선언하면서 초기화를 할 수 있는데, 단 첫 번째 멤버에 대해서만 초기값을 줄 수 있다.
-
이름이 없는 공용체
- 변수를 선언할 때 이름을 주지 않으면 이름없는 공용체(Anonymous Union)가 된다.
- 변수는 당영히 이름을 가져야 하는데 공용체는 특별히 이름을 가지 않을 수가 있다.
- 태그는 필요할 경우 붙일 수도 있는데 통상 이름없는 공용체는 1회용이기 때문에 태그를 붙이지 않는다.
union { int a; double;};
- 이름이 없는 공용체는 단순히 둘 이상의 변수가 같은 기억 장소를 공유하도록 묶어주는 역할만 한다.
- 변수 자체의 이름이 지정되지 않았으므로 a와 b는 마치 독립된 변수처럼 앞에 소속을 밝힐 필요없이 그냥
a=3, b=3.14식으로 사용한다.
- 따라서 이름없는 공용체의 멤버는 공용체 변수의 소속이 아니기 때문에 공용체 바깥의 변수와 명칭이 중복되어서는 안된다.
- 공용체 변수 이름이 있는 공용체의 멤버를 참조할 때는
공용체변수명.a 식으로 소속을 밝혀야 하기 때문에 공용체 바깥에 a라는 다른 변수를 또 선언할 수 있다.
- 이러한 특성을 이용하여 구조체의 멤버로 이름없는 공용체를 다음과 같이 사용할 수 있다.
struct tag_student{
char Name[16];
union {
int HakBun;
char Jumin[14];
};
int Grade;
};
void main()
{
struct tag_student st1;
strcpy(st1.Name, "핫산");
st1.HakBun=88520;
int Grade=100;
printf("%s, %d, %d\n", st1.Name, st1.HakBun, Grade);
}
출처 : 혼자 연구하는 C/C++ 1 / 김상형 저 / 와우북스