TCP 스쿨 11.40 11.41. 11.42. 11.43 보고 필기 |
구조체(structure type)
구조체란 사용자가 C언어의 기본 타입을 가지고 새롭게 정의할 수 있는 사용자 정의 타입 (사용자정의 자료형)
배열 : 같은 타입의 변수 집합 vs 구조체 : 다양한 타입의 변수 집합을 하나의 타입으로 나타낸 것
구조체를 구성하는 변수
구조체의 멤버 또는 멤버변수(member variable) 라 한다.
구조체 정의
struct 구조체이름
{
멤버변수1의타입 멤버변수1의이름;
멤버변수2의타입 멤버변수2의이름;
...
}; // 세미콜론에 주의
구조체 선언
struct 구조체이름 구조체변수이름;
동시에 정의와 선언하기
struct 구조체이름
{
멤버변수1의타입 멤버변수1의이름;
멤버변수2의타입 멤버변수2의이름;
...
} 구조체변수이름;
typedef : 이미 존재하는 타입에 새로운 이름을 붙일 때 쓴다
구조체 변수를 선언하거나 사용할 때에는 매번 struct 키워드를 사용하여 구조체임을 명시해야 하는데
typedef 키워드를 사용하여 구조체에 새로운 이름을 선언하면 매번 struct 키워드를 사용하지 않아도 된다.
구조체의 정의와 typedef 선언을 동시에 할 시 구조체 이름 생략 가능하다.
선언하기
typedef struct 구조체이름 구조체의 새로운이름;
동시에 정의와 선언하기
typedef struct (구조체이름)
{
멤버변수1의타입 멤버변수1의이름;
멤버변수2의타입 멤버변수2의이름;
...
} 구조체의새로운이름;
구조체 멤버 접근과 초기화
구조체에서 구조체 멤버로의 접근은 멤버 연산자 (.)를 사용한다.
구조체의 주소값과 구조체의 첫 번째 멤버 변수의 주소값은 언제나 같다.
구조체변수이름.멤버변수이름
구조체 멤버 초기화
구조체 변수 초기화는 멤버 연산자(.) 와 중괄호 { } 를 사용한다.
원하는 멤버 변수만 초기화
원하는 멤버 변수만 초기화 가능하며, 초기화하지 않은 멤버변수는 0으로 초기화된다.
// 원하는 멤버 변수만 초기화
// 순서 상관 없음
구조체변수이름 = {.멤버변수1이름 = 초깃값, .멤버변수2이름 = 초깃값, ...};
배열의 초기화 방법으로 초기화
구조체 정의에서 멤버 변수가 정의된 순서에 따라 차례로 초기값 설정, 나머지 멤버변수는 0으로 초기화
// 배열의 초기화와 같은 방법으로 구조체 변수 초기화
// 정의된 순서에 따라 차례대로 초깃값이 설정되며, 나머지 멤버 변수는 0으로 초기화
구조체변수이름 = {멤버변수1의초깃값, 멤버변수2의초깃값, ...};
구조체 변수 초기화 예제
#include <stdio.h>
struct book.
{
char title[30];
char author[30];
int price;
};
int main(void)
{
struct my_book = {"HTML과 CSS", "홍길동", 28000};
struct java_book = {.title = "Java language", .price = 30000};
printf("첫 번째 책의 제목은 %s이고, 저자는 %s이며, 가격은 %d원입니다.\n",
my_book.title, my_book.author, my_book.price);
printf("두 번째 책의 제목은 %s이고, 저자는 %s이며, 가격은 %d원입니다.\n",
java_book.title, java_book.author, java_book.price);
return 0;
}
#include <stdio.h>
// typedef 키워드 사용으로 새 이름 선언 후 사용하는 예제
typedef struct
{
char title[30];
char author[30];
int price;
} TEXTBOOK;
int main(void)
{
TEXTBOOK my_book = {"HTML과 CSS", "홍길동", 28000};
TEXTBOOK java_book = {.title = "Java language", .price = 30000};
printf("첫 번째 책의 제목은 %s이고, 저자는 %s이며, 가격은 %d원입니다.\n",
my_book.title, my_book.author, my_book.price);
printf("두 번째 책의 제목은 %s이고, 저자는 %s이며, 가격은 %d원입니다.\n",
java_book.title, java_book.author, java_book.price);
return 0;
}
구조체 배열 선언
C 언어에서 배열의 요소가 될 수 있는 타입에는 제한이 없다. 즉 구조체도 배열의 한 요소가 될 수 있다.
구조체 배열 선언은 다른 타입의 배열을 선언하는 방법과 같고,
구조체 배열에서 각 배열 요소로 접근하는 방법도 일반 배열의 접근 방법과 같고,
멤버 연산자(.)를 사용하여 각 배열 요소의 멤버에 접근한다.
#include <stdio.h>
struct book
{
char title[30];
char author[30];
int price;
};
int main(void)
{
struct book text_book[3] =
{
{"국어", "홍길동", 15000},
{"영어", "이순신", 18000},
{"수학1", "강감찬", 10000}
};
puts("각 교과서의 이름은 다음과 같습니다.");
printf("%s, %s, %s\n", text_book[0].title, text_book[1].title, text_book[2].title);
return 0;
}
구조체를 가리키는 포인터
구조체 변수를 가리키는 포인터는 다음처럼 선언한다.
// struct 구조체이름* 구조체포인터이름;
struct book* ptr_my_book;
구조체의 주소값과 구조체의 첫 번째 멤버 변수의 주소값은 언제나 같다. 그러나
배열의 경우와 달리 구조체의 이름은 구조체를 가리키는 주소가 아니다.
즉 포인터에 할당할 때에는 구조체는 반드시 주소 연산자(&)를 사용해야 한다.
구조체 포인터를 이용하여 구조체의 멤버에 접근하는 방법
구조체 포인터를 이용하여 구조체의 멤버에 접근하는 방법은 아래 두 가지이다.
화살표 연산자가 좀 더 많이 사용된다.
1. 참조 연산자(*) 이용
참조 연산자(*)는 멤버 연산자(.)보다 연산자 우선순위가 낮으므로 반드시 괄호(())를 사용
// (*구조체포인터).멤버변수이름
(*ptr_my_book).author
2. 화살표 연산자(->) 이용
// 구조체포인터 -> 멤버변수이름
ptr_my_book -> author
예시
#include <stdio.h>
#include <string.h>
struct book
{
char title[30];
char author[30];
int price;
};
int main(void)
{
struct book my_book = {"C언어 완전 해부", "홍길동", 35000}; //title, author, price
struct book* ptr_my_book; // 구조체 포인트 선언
ptr_my_book = &my_book;
strcpy((*ptr_my_book).title, "C언어 마스터"); // 참조 연산자(*)를 이용하는 방법
strcpy(ptr_my_book->author, "이순신"); // 화살표 연산자(->)를 이용하는 방법
my_book.price = 32000; // 구조체 변수를 이용한 직접 수정
printf("책의 제목은 %s이고, 저자는 %s이며, 가격은 %d원입니다.\n", my_book.title, my_book.author, my_book.price);
return 0;
}
함수와 구조체
C에서는 함수를 호출할 때 전달되는 인수나, 함수가 종료될 때 반환되는 값으로 구조체를 사용할 수 있고
기본 타입과 똑같은 방식으로 사용한다.
구조체를 가리키는 포인터나 구조체의 한 멤버변수만을 사용할 수도 있다.
구조체를 인수로 전달하는 예제
구조체를 인수로 전달하면 장점 : 함수가 원본 구조체의 복사본을 갖고 작업하여 안전하다.
#include <stdio.h>
#include <typeinfo>
#include <iostream>
typedef struct
{
int savings;
int loan;
} PROP;
int calcProperty(int, int);
int main(void)
{
int hong_prop;
PROP hong = {10000000, 4000000};
hong_prop = calcProperty(hong.savings, hong.loan); // 구조체의 멤버 변수를 함수의 인수로 전달함
printf("홍길동의 재산은 적금에 %d원 대출 %d원을 제외한 총 %d원입니다.\n", hong.savings, hong.loan, hong_prop);
std::cout << typeid(hong).name() << std::endl;
printf("%s\n", typeid(hong_prop).name());
printf("%s\n", typeid(calcProperty).name());
return 0;
}
int calcProperty(int s, int l)
{
return (s - l);
}
함수의 인수로 구조체의 주소를 직접 전달하는 예제
구조체 포인터를 인수로 전달하는 방식의 장점 : 구조체의 복사본이 아닌 주소 하나만 전달해로 처리가 빠르다.
하지만 호출된 함수에서 원본 구조체에 직접 접근하므로 원본 데이터의 보호 측면에서는 매우 위험
#include <stdio.h>
typedef struct
{
int savings;
int loan;
} PROP;
int calcProperty(PROP*);
int main(void)
{
int hong_prop;
PROP hong = {10000000, 4000000};
hong_prop = calcProperty(&hong); // 구조체의 주소를 함수의 인수로 전달함.
printf("홍길동의 재산은 적금 %d원에 대출 %d원을 제외한 총 %d원입니다.\n", hong.savings, hong.loan, hong_prop);
return 0;
}
int calcProperty(PROP* money)
{
money->savings = 100; // 호출된 함수에서 원본 구조체의 데이터를 변경
return (money->savings - money->loan);
}
보완 방법 : const를 사용하여 전달된 인수를 함수 내에서 직접 수정 못하게 한다.
#include <stdio.h>
typedef struct
{
int savings;
int loan;
} PROP;
PROP initProperty(void);
int calcProperty(const PROP*);
int main(void)
{
PROP prop;
int hong_prop;
prop = initProperty();
hong_prop = calcProperty(&prop);
printf("홍길동의 재산은 적금 %d원에 대출 %d원을 제외한 총 %d원입니다.\n", prop.savings, prop.loan, hong_prop);
return 0;
}
PROP initProperty(void)
{
PROP hong = {10000000, 4000000};
return hong; // 구조체를 함수의 반환값으로 반환함.
}
// const 키워드를 사용하여 구조체의 데이터를 직접 수정하는 것을 방지
int calcProperty(const PROP* money)
{
return (money->savings - money->loan);
}
initProperty() 함수는 반환값으로 구조체를 직접 반환한다.
기본적으로 C언어의 함수는 한 번에 하나의 데이터만을 반환할 수 있으나
구조체를 사용하면 여러 개의 데이터를 한 번에 반환할 수 있다.
중첩 구조체
C에서는 구조체를 정의할 때 멤버 변수로 다른 구조체를 포함할 수 있다.
#include <stdio.h>
struct name
{
char first[30];
char last[30];
};
struct friends
{
struct name friend_name;
char address[30];
char job[30];
};
int main(void)
{
struct friends hong =
{
{ "길동", "홍" },
"서울시 강남구 역삼동",
"학생"
};
printf("%s\n\n", hong.address);
printf("%s%s에게,\n", hong.friend_name.last, hong.friend_name.first);
printf("그동안 잘 지냈니? 아직 %s이지?\n", hong.job);
puts("공부 잘 하고, 다음에 꼭 한번 보자.\n잘 지내^^");
return 0;
}
구조체의 크기
구조체의 크기는 멤버 변수들의 크기에 따라 결정되나 언제나 멤버 변수들의 크기 총합과 같지는 않다.
구조체 크기 변수 크기 총합과 다른 이유 : 흰트 (패딩)
c는 구조체의 멤버 중 가장 크기가 큰 걸 기준으로 그 배수로 구조체의 사이즈를 늘려나간다.
그리고 기준보다 작은 멤버들은 패딩을 더해 기준과 같게 만든다.
패딩이 생기는 이유는 CPU가 메모리에 접근하는 걸 최소화해 속도를 빠르게 하기 위해서다.
그래서 멤버의 위치만 바껴도 구조체의 크기가 달라지기도 한다.
구조체의 최적화를 위해서 멤버의 위치도 고려해야 한다.
예시
#include <stdio.h>
typedef struct
{
char a;
int b;
double c;
} TYPESIZE;
int main(void)
{
puts("구조체 TYPESIZE의 각 멤버의 크기는 다음과 같습니다.");
printf("%d %d %d\n", sizeof(char), sizeof(int), sizeof(double));
puts("구조체 TYPESIZE의 크기는 다음과 같습니다.");
printf("%d\n", sizeof(TYPESIZE));
return 0;
}
바이트 패딩 (byte padding)
구조체를 메모리에 할당할 때 컴파일러는 프로그램 속도 향상을 위해 바이트 패딩 규칙을 사용한다.
구조체는 다양한 크기의 타입을 멤버변수로 가지는 타입이며, 컴파일러는 메모리 접근을 쉽게 하려고 크기가 가장 큰 멤버 변수를 기준으로 모든 멤버변수의 메모리 크기를 맞춘다. 이게 바이트 패딩이며 아까도 더보기에 설명한 부분이다.
이 때 추가되는 바이트를 padding byte라 한다.
공용체 (union)
거의 구조체와 같은데, 모든 멤버 변수가 하나의 메모리 공간을 공유한다는 점이 구조체와 차이점이다.
모든 멤버변수가 같은 메모리를 공유해 공용체는 한 번에 하나의 멤버 변수밖에 사용할 수 없다.
순서가 불규칙하며 미리 알 수 없는 다양한 타입의 데이터 저장용으로 나온 타입이다.
가장 큰 멤버 변수의 자료형의 공간(크기)를 공유한다.
union A {
int x; //가장 큰 int 자료형의 공간 공유
char y;
}
// 공용체 정의
union 공용체이름 {
자료형 멤버이름;
};
// 공용체 변수 선언
union 공용체이름 변수이름;
// 동시에 정의, 선언
union 공용체이름{
자료형 멤버이름;
} 변수;
// 동시에 정의, 별칭
typedef union 공용체이름{
자료형 멤버이름;
} 공용체별칭;
// 익명 공동체
typedef union {
자료형 멤버이름;
} 공용체별칭;
공용체 멤버 접근은 (.) 사용
공용체 배열을 사용하면 같은 크기로 구성된 배열 요소에 대한 다양한 크기의 데이터를 저장할 수 있다.
공용체 멤버 변수를 하나만 초기화했는데 나머지 멤버 변수들이 같은 데이터를 공유하는 걸 알 수 있는 예제
#include <stdio.h>
typedef union
{
unsigned char a;
unsigned short b;
unsigned int c;
} SHAREDATA;
int main(void)
{
SHAREDATA var;
var.c = 0x12345678;
printf("%x\n", var.a);
printf("%x\n", var.b);
printf("%x\n", var.c);
return 0;
}
열거체 (enumerated types)
새로운 타입을 선언하면서, 동시에 해당 타입이 가질 수 있는 정수형 상수값도 같이 명시할 수 있는 타입.
열거체 사용의 장점 : 프로그램의 가독성 좋아짐, 변수가 지니는 값에 의미 부여 가능
enum 키워드를 사용해서 선언한다.
사용자가 별도로 멤버에 해당하는 상수값을 명시할 수 있다.
명시하지 않으면 0부터 시작되며, 그 다음 멤버 값은 바로 앞 멤버값보다 1만큼 증가하여 정의된다.
enum Weather {SUNNY = 0, CLOUD = 10, RAIN = 20, SNOW = 30};
예제
#include <stdio.h>
enum Weather {SUNNY = 0, CLOUD = 10, RAIN = 20, SNOW = 30};
int main(void)
{
enum Weather wt;
wt = SUNNY;
switch (wt)
{
case SUNNY:
puts("오늘은 햇볕이 쨍쨍!");
break;
case CLOUD:
puts("비가 올락말락하네요!");
break;
case RAIN:
puts("비가 내려요.. 우산 챙기세요!");
break;
case SNOW:
puts("오늘은 눈싸움하는 날!");
break;
default:
puts("도대체 무슨 날씨인건가요!!!");
}
puts("각각의 열거체 멤버에 해당하는 정수값은 다음과 같습니다.");
printf("%d %d %d %d\n", SUNNY, CLOUD, RAIN, SNOW);
return 0;
}
#include <stdio.h>
enum Days {MON, TUE, WED, THU, FRI, SAT, SUN};
int main(void)
{
enum Days today;
today = SAT;
if (today >= SAT && today <= SUN)
{
puts("오늘은 주말이네요~ 주말에도 열심히 공부하는 여러분은 최고에요!");
}
else
{
printf("주말까지 %d일 남았어요~ 조금만 더 힘내자구요!", 5 - today);
}
puts("각각의 열거체 멤버에 해당하는 정수값은 다음과 같습니다.");
printf("%d %d %d %d %d %d %d\n", MON, TUE, WED, THU, FRI, SAT, SUN);
return 0;
}
구조체와 비트
지금까지 구조체의 멤버는 각 자료형 크기만큼 공간을 차지했는데
구조체 비트 필드를 사용하면 구조체 멤버를 비트 단위로 저장할 수 있다.
CPU나 기타 칩의 플래그를 다루는 저수준(low level) 프로그래밍을 할 때 기본 자료형보다 더 작은 비트 단위로 값을 가져오거나 저장하는 경우가 많으므로 구조체 비트 필드가 유용하게 사용
모든 정수 자료형을 사용할 수 있고 보통 부호 없는(unsigned) 자료형을 주로 사용한다.
실수 자료형은 비트 필드로 사용할 수 없다. 멤버를 선언할 때 : (콜론) 뒤에 비트 수를 지정해준다.
struct 구조체이름 {
정수자료형 멤버이름 : 비트수;
};
사용 예제
#include <stdio.h>
struct Flags {
unsigned int a : 1; // a는 1비트 크기
unsigned int b : 3; // b는 3비트 크기
unsigned int c : 7; // c는 7비트 크기
};
int main()
{
struct Flags f1; // 구조체 변수 선언
f1.a = 1; // 1: 0000 0001, 비트 1개
f1.b = 15; // 15: 0000 1111, 비트 4개
// 비트 필드에 지정한 비트수보다 할당한 비트 수가 많다
f1.c = 255; // 255: 1111 1111, 비트 8개
printf("%u\n", f1.a); // 1: 1, 비트 1개만 저장됨
printf("%u\n", f1.b); // 7: 111, 비트 3개만 저장됨
printf("%u\n", f1.c); // 127: 111 1111, 비트 7개만 저장됨
printf("%d", sizeof(struct Flags)); // 4: 멤버를 unsigned int로 선언했으므로 4
return 0;
}
비트 필드 구조체의 크기는 예제에서 멤버를 unsigned int로 선언하여 4가 나온다.
비트필드의 멤버를 선언하는 자료형보다 큰 비트 수는 지정할 수 없다.
unsigned int a : 37; // error
비트 필드에는 지정 비트 수만큼만 저장되고 나머지 비트는 버려진다.
비트 필드의 각 멤버는 최하위비트 (Least Significant Bit, LSB)부터 차례로 배치된다.
예제에서는 a가 최하위비트에 온다.
비트 필드와 공용체 함께 사용
코드에서 값을 지정 시 비트 필드를 사용하지만 CPU나 칩에 값 설정 시 모든 비트를 묶어서 한번에 저장한다.
한번에 저장하는 방법은 공용체를 비트 필드와 함께 사용하는 것.
비트 필드로 사용할 멤버는 익명 구조체로 감싼다.
#include <stdio.h>
struct Flags {
union { // 익명 공용체
struct { // 익명 구조체
// 비트 수 지정
unsigned short a : 3; // a는 3비트 크기
unsigned short b : 2; // b는 2비트 크기
unsigned short c : 7; // c는 7비트 크기
unsigned short d : 4; // d는 4비트 크기
}; // 합계 16비트
unsigned short e; // 2바이트(16비트)
};
};
int main()
{
struct Flags f1 = { 0, }; // 모든 멤버를 0으로 초기화
// 비트 필드에 값 할당
f1.a = 4; // 4: 0000 0100
f1.b = 2; // 2: 0000 0010
f1.c = 80; // 80: 0101 0000
f1.d = 15; // 15: 0000 1111
printf("%u\n", f1.e); // 64020: 1111 1010000 10 100
return 0;
}
문제 : Flags 안을 채워서 3, 1, 63이 출력되게 하시오.
#include <stdio.h>
struct Flags {
};
int main()
{
struct Flags f1;
f1.a = 0xffffffff;
f1.b = 0xffffffff;
f1.c = 0xffffffff;
printf("%u %u %u\n", f1.a, f1.b, f1.c);
return 0;
}
해설 : 비트 수를 조절하여 값을 도출한다.
0xffffffff 는 1이 32개므로 모든 비트가 1로 채워진다. 각 값이 1이 몇 개인지 판단하면 된다.
3, 1, 63을 표현하려면 3은 1이 2개, 1은 1이 1개, 63은 1이 6개 필요하다.
#include <stdio.h>
struct Flags {
unsigned int a : 2;
unsigned int b : 1;
unsigned int c : 6;
};
int main()
{
struct Flags f1;
f1.a = 0xffffffff;
f1.b = 0xffffffff;
f1.c = 0xffffffff;
printf("%u %u %u\n", f1.a, f1.b, f1.c);
return 0;
}
참고한 곳
TCP 스쿨 : http://tcpschool.com/c/c_struct_intro
코딩공장 : https://coding-factory.tistory.com/639
모두의 코드 : https://modoocode.com/55
'C, C++' 카테고리의 다른 글
함수의 인수 전달 방법 3가지 (0) | 2021.09.27 |
---|---|
콜론연산자(:), 더블콜론연산자(::) (0) | 2021.09.25 |
알고리즘이 보이는 그림책 3. 연습문제, 4. 연습문제 (0) | 2021.09.09 |
c 난수 생성 함수 (0) | 2021.09.08 |
보안 경고 #define _CRT_SECURE_NO_WARNINGS (0) | 2021.09.08 |