프롤로그
ELITE HACKER Bootcamp 3rd 4주차 수업 공부 내용
aws 우분투 서버 하나 파서 연습
구조체, 선언, 포인터, 배열, 함수, 중첩, 메모리구조, 패딩
구조체
일종의 클래스와 비슷한 개념이라고 보면된다. 클래스... 하나의 객체를 만들어놓는 것이다. 다만 클래스랑 조금 다른 것이 있다면, 메서드(행동)이 없다는 것.
그러니까 이제 구조체 정의 자체가 구조체는 서로 관련된 여러 데이터를 하나의 단위로 묶기 위해 사용되는 사용자 정의 데이터 타입인거고, 클래스는 여기에 행위까지 같이 넣어두었으나, C언어에서는 행위까지는 존재하지않는다. 라고 알면 될 것 같다.
조금 예시를 들자면,
시험점수를 입력하려고 한다고 치자. 한 사람마다 국어, 수학, 영어 점수가 있을 것이고, 그 사람의 전체 등급이 있을 것이다. 그런데 그 사람은 또 학번, 이름도 있을 것이다.
학번, 국어, 수학, 영어 같은 경우는 int형식으로 선언하면 되기에 그냥 int 배열을 만들어서 사용할 수도 있을 수 있다. 그런데 이름같은 경우는 문자열일 것이다. 이렇게 서로 다른 자료형이 있을 경우에는 배열을 선언하기 참 애매해진다. 아 물론 파이썬은 그딴 거 상관없지만 여기는 파이썬이 아니지 않은가..
그럴 때 사용하는 것이 구조체이다.
서로 다른 데이터 타입들을 하나로 묶는 사용자 정의 데이터 타입으로써, 데이터 관련성을 명확히 하기 위해 사용된다.
예시 코드를 나오자면
struct Student{
char name[30]; //이름
int student_id; //학번
float korean, math, english; //국어, 수학, 영어
char grade; //성적 (A, B, C...)
};
이렇게 묶음으로 관리하는 것이다.
구조체 사용하기
아까와 같이
struct Student{
char name[30]; //이름
int student_id; //학번
float korean, math, english; //국어, 수학, 영어
char grade; //성적 (A, B, C...)
};
이렇게 구조체를 선언했다면, 해당 구조체를 이용해서 정보들을 저장해야한다. 저 구조체의 이름은 Student이다.
성밍쟁, 섬밈잼 이렇게 두 사람이 있다고 가정을 하고 만들어보자. 메인함수에서 해당 구조체를 변수로 만들수가 있다.
struct Student s1;
struct Student s2;
데이터를 넣어주려면 이렇게 한다.
struct Student s1 = {"성밍쟁", 1, 95, 96, 97, 'A'};
struct Student s2;
strcpy(s2.name, "섬밈잼");
s2.student_id = 2;
s2.korean = 30;
s2.math = 20;
s2.english = 30;
s2.grade = 'F';
s1처럼 선언할 때 초기화 해주는 방법이 있고, s2는 멤버별로 .을 이용해서 접근해서 데이터를 넣어주는 방식이다.
#include <stdio.h>
#include <string.h>
struct Student{
char name[30]; //이름
int student_id; //학번
float korean, math, english; //국어, 수학, 영어
char grade; //성적 (A, B, C...)
};
int main(){
struct Student s1 = {"성밍쟁", 1, 95, 96, 97, 'A'};
struct Student s2;
strcpy(s2.name, "섬밈잼");
s2.student_id = 2;
s2.korean = 30;
s2.math = 20;
s2.english = 30;
s2.grade = 'F';
printf("%s, %d, %f, %f, %f, %c\n", s1.name, s1.student_id, s1.korean, s1.math, s1.english, s1.grade);
printf("%s, %d, %f, %f, %f, %c\n", s2.name, s2.student_id, s2.korean, s2.math, s2.english, s2.grade);
}
전체 코드는 이와같고, .을 이용해서 접근한다는 것까지도 이제 잘 알았다.
구조체의 배열
위에는 사람들 2명이기 때문에 s1, s2로 했을 뿐인데, 저런 사람이 몇백명이 된다면? 그래서 구조체 또한 배열로 선언이 가능하다. 왜냐? 어쨌거나 구조체도 struct Student라는 사용자지정 자료형이기 때문에.
int main(){
struct Student student[2] = {
{"성밍쟁", 1, 95, 96, 97, 'A'},
{"섬밈잼", 2, 30, 20, 30, 'F'}
};
이렇게 직접 만들어주어도 되고, 기존 배열처럼 student[0].name 이런식으로 접근해서 채워주어도 된다.
#include <stdio.h>
#include <string.h>
struct Student{
char name[30]; //이름
int student_id; //학번
float korean, math, english; //국어, 수학, 영어
char grade; //성적 (A, B, C...)
};
int main(){
struct Student student[2] = {
{"성밍쟁", 1, 95, 96, 97, 'A'},
{"섬밈잼", 2, 30, 20, 30, 'F'}
};
printf("%s, %d, %f, %f, %f, %c\n", student[0].name, student[0].student_id, student[0].korean, student[0].math, student[0].english, student[0].grade);
printf("%s, %d, %f, %f, %f, %c\n", student[1].name, student[1].student_id, student[1].korean, student[1].math, student[1].english, student[1].grade);
}
하면
구조체와 포인터
배열과 포인터의 관계는 뗄레야 뗄 수가 없는 관계이다. 배열 이름 자체가 포인터의 이름이 되기도 하여, 배열 이름을 넣어주면 해당 배열의 시작 주소로 이동하게 된다. 그렇다면 구조체를 넘겨주었을 때, 포인터로 넘겨주게되면? 해당 배열의 시작지점으로 이동할 거고,포인터+1을 한다면, 다음 배열 요소인 1번인덱스로 이동하게 될 것이다.
이때, 포인터를 통해서 요소를 접근하려면 .이 아닌 ->를 이용한다. 화살표...
#include <stdio.h>
#include <string.h>
struct Student{
char name[30]; //이름
int student_id; //학번
float korean, math, english; //국어, 수학, 영어
char grade; //성적 (A, B, C...)
};
int main(){
struct Student student[2] = {
{"성밍쟁", 1, 95, 96, 97, 'A'},
{"섬밈잼", 2, 30, 20, 30, 'F'}
};
struct Student * ptr = student;
printf("%s, %d, %f, %f, %f, %c\n", ptr->name, ptr->student_id, ptr->korean, ptr->math, ptr->english, ptr->grade);
printf("%s, %d, %f, %f, %f, %c\n", (ptr+1)->name, (ptr+1)->student_id, (ptr+1)->korean, (ptr+1)->math, (ptr+1)->english, (ptr+1)->grade);
}
물론
printf("%s, %d, %f, %f, %f, %c\n", student->name, student->student_id, student->korean, student->math, student->english, student->grade);
이렇게 해도 잘 작동할 것이다.
구조체와 함수
이제 struct 내부에서 메서드(함수)를 만들수는 없으나, 그냥 함수 자체는 만들 수 있다.
이때, 매개변수로 자료형을 우리가 구조체로 만들었던 데이터를 넘겨줄 수가 있는데, 이 역시 객체를 넘겨줄 수 있고, 포인터로도 넘겨줄 수 있다. 객체로 넘겨주었다면, .을 이용해서 사용하여야만 하는 거고(물론 포인터 선언해서 ->로 해줄수도 있긴하다), 포인터로 매개변수를 넘겨주었다면 ->을 이용해줘야한다 느낌이다.
#include <stdio.h>
#include <string.h>
struct Student{
char name[30]; //이름
int student_id; //학번
float korean, math, english; //국어, 수학, 영어
char grade; //성적 (A, B, C...)
};
void printStudent1(struct Student s){
printf("%s, %d, %f, %f, %f, %c\n", s.name, s.student_id, s.korean, s.math, s.english, s.grade);
}
void printStudent2(struct Student* ptr){
printf("%s, %d, %f, %f, %f, %c\n", ptr->name, ptr->student_id, ptr->korean, ptr->math, ptr->english, ptr->grade);
}
int main(){
struct Student student[2] = {
{"성밍쟁", 1, 95, 96, 97, 'A'},
{"섬밈잼", 2, 30, 20, 30, 'F'}
};
struct Student * ptr = &student[1];
printStudent1(student[0]);
printStudent2(ptr);
}
함수에다가 구조체를 넘겨주었고, 아니면 포인터를 넘겨주었다.
typedef
typedef는 기존 데이터타입에 새로운 이름을 부여해주는 것이다.
typedef unsigned int uint;
uint age = 25; // unsigned int age = 25; 와 동일
이렇게 내가 원하는 대로 데이터타입을 바꿔줄 수 있는 것이다.
아니면 별칭을 지어줄 수도 있다.
typedef int Scores[5];
Scores math_scores = {90, 85, 80, 95, 88}; // int math_scores[5]와 동일
typedef int* IntPointer;
IntPointer ptr1, ptr2; // int* ptr1, int* ptr2 와 동일
이게 구조체랑 무슨 관련이 있냐?
자 구조체 선언할 때 struct를 붙이는 게좀 귀찮다.
struct Student{
char name[30]; //이름
int student_id; //학번
float korean, math, english; //국어, 수학, 영어
char grade; //성적 (A, B, C...)
};
struct Student s1 = {"성밍쟁", 1, 95, 96, 97, 'A'};
struct Student s2;
struct를 계속 붙여줘야하니까 귀찮은 것이다.
이럴 때 typedef를 이용한다.
typedef struct {
char name[30]; //이름
int student_id; //학번
float korean, math, english; //국어, 수학, 영어
char grade; //성적 (A, B, C...)
} Student;
이렇게 해버리면, 이제부터 밑에 struct Student s1; 이렇게 안 하고
Student s1;
Student s2;
이렇게 선언을 해버릴 수가 있는 것이다.
다만 착각하면 안 되는 게, 별칭만을 만드는 거지 내가 무언가를 새로 창조해내는 것이 아니기 때문에 동작은 기존과 똑같이 진행된다.
typedef vs #define
사용 대상 | 데이터 타입 | 상수, 매크로 |
컴파일 단계 | 컴파일러가 처리 | 전처리기(preprocessor) 처리 |
디버깅 | 디버깅 시 타입 정보 유지 가능 | 디버깅 시 정보 유지되지 않음 |
문법적 한계 | 포인터, 배열, 함수 포인터 등 지원 | 복잡한 타입 정의 불편 |
구조체 중첩
데이터베이스 같은 경우에서 이제 다른 테이블을 참조해야할 경우 그럴 때 많이 이용하는 경우도 있고
뭐, 선언할 때 본인 자기 자신을 참조해야하는 경우도 발생한다.
구조체 멤버로 또 다른 구조체를 포함 시키는 게 구조체 중첩이다.
- 계층적 데이터 표현: 관련 데이터를 그룹화하여 논리적 관계를 명확히 표현.
- 캡슐화: 데이터의 세부 구조를 외부에서 쉽게 접근하지 못하도록 함.
- 메모리 관리: 중첩 구조체의 메모리는 포함된 구조체가 차지하는 크기를 반영.
이러한 특징이 있다.
(1) 기본 중첩 구조체
#include <stdio.h>
#include <string.h>
struct Address{
char country[50];
char city[50];
};
struct Student{
char name[30];
int age;
struct Address address; //중첩 구조체
};
int main(){
struct Student s1 = {"서민재", 24, {"대한민국", "대전광역시"}};
printf("Name : %s\n", s1.name);
printf("Age : %d\n", s1.age);
printf("Country : %s\n", s1.address.country);
printf("city : %s\n", s1.address.city);
}
위와 같이 struct로 구조체를 두 개를 만들어 준다음에, Student안에다가 Address를 선언해준 것이다. 이렇게 하면 기본 중첩 구조체가 생성이 된다.
해당 내부로 들어가기 위해서는 s1.address 뒤에 .을 하나 더 붙여 해당 구조체의 요소로 접근한다.
(2) 포인터를 이용한 중첩 구조체
#include <stdio.h>
#include <string.h>
struct Address{
char country[50];
char city[50];
};
struct Student{
char name[30];
int age;
struct Address address; //중첩 구조체
};
int main(){
struct Student s1 = {"서민재", 24, {"대한민국", "대전광역시"}};
printf("Name : %s\n", s1.name);
printf("Age : %d\n", s1.age);
printf("Country : %s\n", s1.address.country);
printf("city : %s\n", s1.address.city);
struct Student* s2 = &s1;
printf("Name : %s\n", s2->name);
printf("Age : %d\n", s2->age);
printf("Country : %s\n", s2->address.country);
printf("city : %s\n", s2->address.city);
}
아까랑 똑같은데, 포인터로 접근할 때이다.
포인터로 선언이 된 곳에는 ->으로 넘겨주는 것을 알 수 있다. 그래서 s1같은 경우에는 배열을 선언하고 중첩은 객체로 넘어가기 때문에
s1.address.country 같은 구조로 접근하지만
포인터로 접근해서 내부 객체로 들어가게 된다면
s2->address.country 와 같이 작동해야하기에, 포인터인지, 객체로 접근인지 유의해야한다.
(3) 익명 구조체
익명구조체란, 구조체 이름을 정의하지 않고 선언과 동시에 변수를 생성하는 구조체를 말한다.
- 이름이 없음:
- 구조체의 이름이 없으므로 다른 곳에서 해당 구조체를 재사용할 수 없다.
- 선언과 변수 정의를 동시에 진행:
- 구조체를 선언하면서 바로 변수를 생성
- 가벼운 데이터 정의:
- 간단한 데이터 그룹화를 위해 사용
#include <stdio.h>
struct Student {
char name[30];
int age;
struct {
char city[50];
char state[50];
}; // 익명 중첩 구조체
};
int main() {
struct Student student = {"Alice", 20, {"Seoul", "Korea"}};
// 익명 구조체 멤버 직접 접근
printf("City: %s, State: %s\n", student.city, student.state);
return 0;
}
그래서 위와같이 Student내부에 Address 구조체를 따로 만들지 않고 그냥 저렇게 사용하였다
(4) typedef 구조체
아까랑 똑같다. 그냥 struct쓰기 싫어서 구조체에 다가 별칭으로 넣는 것 뿐이다.
#include <stdio.h>
typedef struct {
char city[50];
char state[50];
} Address;
typedef struct {
char name[30];
int age;
Address address; // typedef로 정의된 Address 사용
} Student;
int main() {
Student student = {"Alice", 20, {"Seoul", "Korea"}};
printf("Name: %s, Age: %d\n", student.name, student.age);
printf("City: %s, State: %s\n", student.address.city, student.address.state);
return 0;
}
구조체의 메모리 구조와 패딩
이제 마지막이다.
일단 기본적으로 구조체가 메모리에서 저장되는 방식은 구조체에서 선언한 순서대로 메모리에 저장된다.
struct Student{
char name[30];
int age;
double score;
};
이런 구조체가 있다고 치자.
순서대로 char 1바이트, int 4바이트, double 8바이트를 차지 하게 될 것이다.
근데, 각 자료형이 메모리에서 배치될 때 규칙이 있다. char은 1의 배수 메모리에 저장이 되어야하고, int는 4바이트 배수 메모리 주소에 저장, double같은 경우 8바이트 배수 메모리에 위치 되어야한다. 이를 정렬 크기라고 한다.
(1) 메모리 정렬
컴퓨터의 CPU는 특정 크기의 메모리 단위(정렬 크기)에 맞춰 데이터를 읽고 쓰는 것이 더 효율적이다. 이를 위해 구조체 멤버는 해당 데이터 타입에 맞는 정렬 크기(alignment requirement)에 따라 배치된다.
- char: 1-byte 정렬 (1의 배수)
- short: 2-byte 정렬 (2의 배수)
- int, float: 4-byte 정렬 (4의 배수)
- double: 8-byte 정렬 (8의 배수)
보통 자기 자료형을 따라간다.
(2) 패딩
그런데, 아까 같이
struct Student{
char name[30];
int age;
double score;
};
이렇게 있다면 어떻게 될까?
일단 char가 30바이트를 찾아갈 것이다. 그런데 int는 4바이트 배수로 메모리 주소상에 위치해야하기 때문에, 30에다가 바로 int형을 배치시킬 수가 없다. 그래서 빈 공간을 좀 만들어서 2바이트를 채워준 다음에 int형을 넣어주게 된다. 저렇게 빈공간을 만들어놓는 것. 이것이 패딩이다.
만약에 char name[30]이 아니라 char name; 으로 선언이 되어있었다고 치면 1바이트기 때문에, 4바이트 배수로 맞춰주기 위해서 3바이트를 띄워주어야한다.
그럼 만약에 이런 상황이면?
struct Student{
char name;
double score;
int age;
};
double은 8의 배수이기 때문에 char은 1바이트밖에 차지않아 패딩을 7바이트나 넣어주어야한다. 메모리적으로 굉장히 비효율적일 수 밖에 없다
또한 구초제는 최종적으로 가장 큰 자료형 바이트 단위이기 때문에 맨 마지막 부분이 char로 1바이트만 채워지게 되었다고 한다면, 결국 마지막에 double에 맞춰주기 위해 7바이트 패딩을 채워준다.
(3) 구조체 크기 계산 방법
struct Test {
char a; // 1 byte
short b; // 2 bytes
int c; // 4 bytes
};
이렇게 있다고 쳤을 때, 메모리에 맞게 계산을 해보는 것이다. 각 자료형 크기 + 패딩이 몇일까?
char 1바이트 | 패딩 1바이트 | short 2바이트| int 4바이트 | 패딩 2바이트
이렇게 해서 총 8바이트를 먹고 있을 것이다.
struct Student{
char name;
double score;
int age;
};
이런 경우에는 ?
double이 8바이트기 때문에 24바이트를 먹을 것이고 구조는
char 1바이트 | 패딩 7바이트 | double 8바이트 | int 4바이트 | 패딩 4바이트(8의 배수)
이렇게 될 것이다.
크기를 확인해볼까?
#include <stdio.h>
struct Test {
char a; // 1 byte
short b; // 2 bytes
int c; // 4 bytes
};
struct Student{
char name;
double score;
int age;
};
int main() {
printf("Size of struct: %lu\n", sizeof(struct Test));
printf("Size of struct: %lu\n", sizeof(struct Student));
return 0;
}
(4) 패딩 최적화
이제 우리가 구조체 내부 자료형들을 어떻게 배치하느냐에 따라 메모리를 효율적으로 아낄 수 있다.
struct Student{
char name;
double score;
int age;
};
이거는 메모리가 24바이트를 차지했었다.
struct Student{
double score;
int age;
char name;
};
만약에 이렇게 배치했다면?
double 8바이트 | int 4바이트 | char 1바이트 | 패딩 3바이트
해서 총 16바이트만 차지할 것이다. 순서만 바꿨는데 8바이트나 아꼈다. 이게 패딩 최적화이다. 실제로 확인해보면
#include <stdio.h>
#include <string.h>
struct Student1{
double score;
int age;
char name;
};
struct Student2{
char name;
double score;
int age;
};
int main(){
printf("Size of struct: %lu\n", sizeof(struct Student1));
printf("Size of struct: %lu\n", sizeof(struct Student2));
return 0;
}
저 메모리를 잘 고려해야한다.
(5) 패딩 제어
저 패딩은 컴파일러가 알아서 메모리 만들어주고 설정하는데, 우리가 임의적으로 패딩 넣는 것을 조절할 수 있다.
a. 패딩 제거
#pragma pack(1) 으로 패딩을 비활성화 할 수 있다. 1+4+1 =6을 그대로 사이즈를 가져간 것을 확인할 수 있다.
#include <stdio.h>
#pragma pack(1) // 패딩 비활성화
struct Example {
char a;
int b;
char c;
};
int main() {
printf("Size of struct: %lu\n", sizeof(struct Example)); // 6 bytes
return 0;
}
#pragma pack() // 패딩 설정 초기화
b. 정렬 크기 변경
#include <stdalign.h>
#include <stdio.h>
struct Example {
char a;
alignas(8) int b;
char c;
};
int main() {
printf("Size of struct: %lu\n", sizeof(struct Example)); // 6 bytes
return 0;
}
alignas(8) 설정으로 정렬 기준을 8로 바꿔버릴 수 있다. 그렇다면 1+7+4+1+3로 16바이트를 잡아먹게 되는 구조가 되어버린다.
alignas(8)을 쓰기 위해선 #include <stdalign.h>가 정의되어 있어야한다.
에필로그
어렵다;;이제 자료구조 영역이라 그런가.
다음 글에서는 구조체를 활용한 여러 코드들을 짜보자.
'KnockOn' 카테고리의 다른 글
[1주차 TIL] KnockOn Bootcamp 프로토콜, OSI, TCP, UDP (0) | 2024.12.03 |
---|---|
[1주차 TIL] KnockOn Bootcamp 웹이란? (4) | 2024.12.02 |
[KnockOn] Linux/Ubuntu C언어 <string.h> (0) | 2024.11.24 |
[KnockOn] Linux/Ubuntu C언어 문자열 (0) | 2024.11.23 |
[KnockOn] Linux/Ubuntu C언어 포인터 (0) | 2024.11.21 |