구조체는 서로 다른 타입의 변수들을 한 그룹으로 묶는 방법을 제공한다. 이렇게 함으로써 관련된 데이터를 하나의 단위로 취급할 수 있게 된다.
구조체 메모리 할당 방식
우선 구조체를 시작하면서 구조체의 메모리 할당 방식, 구조체의 크기 계산 방법, 그리고 구조체와 포인터의 상호 작용에 대해서 알아보자. 이 개념을 배우고나면 구조체가 메모리 상에서 어떻게 위치하는지 이해할 수 있고, 메모리를 효츌적으로 활용하는 방법을 배울 수 있다. 또한 이를 통해 프로그램의 성능 향상과 메모리 관리를 위한 기본적인 이해를 갖출 수 있다.
구조체는 메모리 상에서 연속적인 공간에 할당된다. 즉, 구조체를 선언하면 구조체의 모든 멤버가 메모리상에서 순차적으로 위치하게 된다. 이러한 방식은 구조체의 각 멤버에 접근하거나 값을 변경할 때 효율적이다. 아래의 코드를 보면 구조체 Rectangle은 int형 멤버 2개를 가지고 있는 것을 볼 수 있다. 따라서 구조체의 크기는 8바이트가 된다. 또한 구조체의 크기를 계산하기 위해 sizeof 연산자를 사용하면 되는 것을 발견할 수 있다. 여기서 sizeof 연산자는 메모리의 바이트 단위 크기를 반환하는 특수한 연산자이다.
#include <stdio.h>
struct Rectangle {
int length;
int width;
};
int main() {
struct Rectangle rect1;
rect1.length = 5;
rect1.width = 7;
printf("Size of rect1: %lu bytes\n", sizeof(rect1)); // 출력: Size of rect1: 8 bytes
return 0;
}
아래 코드에서 MyStruct라는 구조체는 int, double, char 3가지 타입의 멤버를 가지고 있다. 각 멤버의 크기는 일반적으로 4, 8, 1 바이트를 가지나, sizeof의 결과는 16바이트가 나온다. 이는 C/C++에서 데이터는 특정 바이트 경계에 맞춰 메모리에 저장되는 것에 의해 발생되는데, 이를 data alignment라고 한다. 따라서 이 구조체의 크기는 13바이트가 아니라 16바이트가 된다.
#include <stdio.h>
struct MyStruct {
int i;
double d;
char c;
};
int main() {
struct MyStruct s;
printf("Size of MyStruct: %lu bytes\n", sizeof(s)); // 출력: Size of MyStruct: 16 bytes
return 0;
}
따라서 구조체의 크기를 정확히 계산하려면 data alignment에 대한 이해가 필요한데, 이는 컴퓨터 아키텍처에 따라 다르며 효율적인 메모리 접근을 위해 중요하다. 대부분의 경우 컴파일러가 이를 자동으로 관리해주지만, 때로는 프로그래머가 직접 메모리 정렬을 관리해야 할 때도 있다. 그런 경우에는 특정 키워드(__attribute__((aligned)) 등)을 사용하여 메모리 정렬을 조절할 수 있다.
구조체 포인터
구조체 포인터는 구조체의 주소를 저장하는 포인터로 이를 통해 구조체의 멤버에 동적으로 접근하거나 함수로 구조체를 전달할 수 있다. 선언방법은 struct MyStruct *p; 와 같이 선언이 가능하며, 이렇게 선언된 구조체 포인터를 사용하여 구조체의 멤버에 접근하려면 '->' 연산자를 사용한다.
이런 구조체 포인터를 사용하면 구조체를 반환하는 함수를 만들 수 있는데, 함수 내부에서 생성된 구조체의 주소를 반환함으로써 함수 외부에서도 해당 구조체를 사용할 수 있게 만든다. 이러한 방식은 동적 메모리 할당과 함께 사용되어, 큰 규모의 데이터를 효율적으로 관리하는데 도움이 되지만, 이렇게 사용할 때는 메모리 누수에 주의해야할 필요가 있다. 함수 내에서 동적으로 할당한 메모리는 반드시 적절한 시점에 해제되어야 한다. 그렇지 않으면 사용하지 않는 메모리가 계속 쌓여서 시스템의 자원을 낭비하게 되는 memory leak 문제가 발생한다. 이는 심각한 성능 저하를 초래하며, 프로그램의 안정성을 위협할 수 있다.
이러한 구조체 포인터의 활용은 다음 코드 예시를 보면 이해할 수 있다.
struct Student {
int id;
char name[20];
};
struct Student std;
struct Student *ptr = &std;
ptr->id = 123;
strcpy(ptr->name, "John Doe");
또한 구조체 포인터는 동적 메모리 할당을 통해 런타임에 구조체를 생성하는데 사용될 수 있다. 아래의 예에서는 malloc 함수를 사용하여 Student 구조체의 크기만큼 메모리를 동적으로 할당하고, 그 주소를 ptr에 저장한다.
struct Student *ptr = (struct Student*)malloc(sizeof(struct Student));
if(ptr != NULL) {
ptr->id = 123;
strcpy(ptr->name, "John Doe");
}
그리고 구조체 포인터는 함수의 매개변수나 반환 값으로 사용될 수 있다. 이는 큰 구조체를 복사하는 오버헤드를 줄이고, 함수 내에서 구조체의 값을 변경할 수 있게 해준다. 이렇게 구조체 포인터를 이해하고 활용하면 프로그램의 메모리 효율성을 향상하고 코드의 가독성을 높일 수 있다.
구조체 중첩
한 구조체 안에 다른 구조체가 포함되는 것을 말한다. 즉, 다른 구조체를 멤버로 포함하는 구조체를 정의하는 것이다. 이러한 중첩 구조체는 각 구조체 멤버에 접근할 때마다 '.' 연산자를 사용한다.
struct Address {
char city[50];
char postcode[10];
};
struct Student {
char name[50];
struct Address address;
};
구조체 배열의 중첩
구조체 내부에 배열을 가지고 있는 것을 의미한다. 그리고 그 배열의 각 원소가 또 다른 구조체일 수 있다. 이런 방식은 데이터를 그룹화하여 관리할 때 특히 유용하다. 그리고 구조체 내부의 배열에 접근하기 위해서는 2번의 인덱싱을 사용한다. 첫 번째 인덱싱은 배열 내의 특정 요소를 지정하고, 두 번째 인덱싱은 그 요소 내의 특정 필드를 지정한다. 즉, 2단계의 인덱싱을 사용하여 배열 내부의 구조체 필드에 접근할 수 있는 것이다. 이런 방식은 데이터를 계층적으로 구조화하는데 매우 유용하다. 다만 각 구조체와 배열이 메모리를 차지하기 때문에 너무 큰 배열이나 복잡한 구조체를 중첩 사용하면 메모리 부족 문제가 발생할 수 있음을 유의해야 한다.
struct Subject {
char name[50];
int score;
};
struct Student {
char name[50];
struct Subject subjects[5]; // 5개 과목을 가진다.
};
strcpy(student.subjects[0].name, "Mathematics");
구조체와 함수의 중첩
구조체는 데이터를 그룹화하는데 매우 효율적이다. 그러나 때때로 이런 데이터를 조작하거나 다루는 함수들이 필요한데, 이때 구조체와 함수를 함께 사용하면 매우 효율적인 결과를 얻을 수 있다. 물론 C언어에서는 구조체 내부에 직접 함수를 정의할 수는 없다. 대신 함수 포인터를 이용해 구조체와 함수를 연결할 수는 있다. 다음 코드를 참고하자.
struct Student {
char name[50];
int age;
// 구조체에 함수를 포함할 수 없지만,
// 함수를 구조체와 연관시키는 방법으로, 함수 포인터를 사용할 수 있습니다.
void (*printDetails)(struct Student*);
};
void printDetails(struct Student *s) {
printf("Name: %s\n", s->name);
printf("Age: %d\n", s->age);
}
int main() {
struct Student student1;
strcpy(student1.name, "John");
student1.age = 20;
student1.printDetails = printDetails;
student1.printDetails(&student1);
return 0;
}
typedef를 이용한 구조체 별칭
typedef는 기존의 데이터 타입에 새로운 이름을 부여하고자 할 때 사용하는 키워드이다. 이를 이용해 구조체에 별칭을 부여하면 코드를 보다 간결하고 직관적으로 만들 수 있다. 이는 특히 복잡한 데이터 타입이 자주 사용되는 프로그램에서 매우 유용하며, 이는 코드의 가독성을 높이고 유지보수를 용이하게 만든다.
typedef struct Student {
char name[20];
int age;
double grade;
} Student;
다음과 같이 여러 표현법으로 나타낼 수도 있다.
// typedef와 구조체(1)
struct Point{
int x;
int y;
};
typedef struct Point Point; // struct Point 대신에 Point를 사용한다는 의미
// typedef와 구조체(2)
typedef struct Point{
int x;
int y;
}Point; // struct Point 대신에 Point를 사용한다는 의미
// typedef와 구조체(3)
typedef struct{
int x;
int y;
}Point; // struct Point 대신에 Point를 사용한다는 의미
// typedef와 구조체(4)
struct Point{
int x;
int y;
}typedef Point; // struct Point 대신에 Point를 사용한다는 의미
인용
'Language > C' 카테고리의 다른 글
#define에 대해서 (0) | 2023.11.11 |
---|---|
선언(declaration)과 정의(definition) (0) | 2023.11.05 |
키워드 _CRT_SEUCRE_NO_WARNINGS에 대하여 (0) | 2023.11.05 |
typedef, enum, union (0) | 2023.11.05 |
컴파일과 디버깅 (0) | 2023.11.02 |