C언어 구조체

Minimal_user·2024년 5월 13일

c언어

목록 보기
13/17

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}, // x와 y는 0으로 초기화
	{"아무병", 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;
//Kim.pName=(char *)malloc(strlen(Albert.pName)+1);
//strcpy(Kim.pName,Albert.pName);
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);
//free(Kim.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);
}

// 원래는 "크기=2, 값=ff8f" 가 나와야 하는데
// WSL ubuntu 22.04 gcc 11.4.0 의 결과는 "크기=2, 값=ffffff8f" 로 나온다.
// 일단 비트 구조체를 이용할 때는 typedef를 이용하여 만들 수 없다.
// 멤버의 타입에 따라 비트 필드 전체의 크기가 달라진다고 했는데 char를 넣고 마지막에 char*로 캐스팅해도 ffffff8f이 나온다.
// 심지어 *(short*)&bit 원래 예제 상으로 bit만 있지만 %x 서식과 타입이 매치가 되지 않는다고 에러가 발생하여 강제로 캐스팅했음.

// 아무튼 기회가 되면 다시 살펴봐야할 것 같다.
  • 멤버가 선언된 순서대로 하위 비트에서 순서대로 할당되며 구조체 자체의 크기는 모든 비트 멤버의 총 비트수와 같다.
    • 비트 필드가 선언 순서대로 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;};
      • 두 가지 단위 중 원하는 단위의 수치를 입력할 수 있다 (한 덩어리에 두 가지 방식을 입력할 수 있다는 점이 포인트)
      • 단 이 경우 공용체에 저장된 값이 어떤 멤버를 기준으로 (밀리인지 인치인지) 알 수 있는 방법은 없다.
        • 필요하다면 공용체가 어떤 타입의 값을 저장하고 있는지를 기억하는 별도의 변수를 둘 수 있다.
  • 공용체도 선언하면서 초기화를 할 수 있는데, 단 첫 번째 멤버에 대해서만 초기값을 줄 수 있다.

    • `tag_unit length={15};

  • 이름이 없는 공용체

    • 변수를 선언할 때 이름을 주지 않으면 이름없는 공용체(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 / 김상형 저 / 와우북스

profile
White book for everything I need.

0개의 댓글