본문 바로가기

프로그래밍 언어/C

C 언어 | 구조체 (Structure, struct)

구조체는 C언어에서 복잡한 데이터를 관리하고, 의미있게 정보를 조직화하는데 있어 필수적인 도구다. 

 

C에서 구조체(structure)는 하나 이상의 서로 다른 타입의 변수들을 묶어 새로운 타입을 정의할 수 있게 해준다.

이는 관계있는 서로 다른 데이터들을 하나의 단위로 처리할 수 있게 한다. 

 

구조체는 Java의 클래스와 거의 유사하다. 다만 메서드를 포함하지 않고 필드만 지닌다는 점?

그리고 객체지향적 개념들이 포함되지 않아 훨씬 단순한 구조라고 볼 수 있다.

 

이번에 알아본 것은 다음과 같다.

  • struct, 구조체 변수 선언 및 멤버 접근, 구조체가 메모리에 할당되는 방식, 구조체를 함수 인자로 전달하기, 구조체 배열, 구조체 포인터, 중첩 구조체, typedef

1. 구조체 기본 문법

구조체 선언: struct

struct Student {
    char name[50];
    int id;
    float grade;
};

 

Student 라는 새로운 데이터 타입을 정의했다.

구조체가 설계도(class)라면, 실제 제품(instance)을 만들어야겠지?

 

Java의 인스턴스에 대응되는 개념이 구조체 변수이다.

struct Student student1; // C에서 구조체 타입의 변수 선언

Student student1 = new Student(); // Java에서 클래스 인스턴스 생성

 

생성자가 빠진 모습이다. 

 

구조체 변수 선언 + 멤버 접근

int main(void){
    // 구조체 타입의 변수를 선언.
    struct Student student1; // student1이라는 이름의 Student 구조체 인스턴스를 만듦. 

    // 각 멤버에 대한 값을 초기화
    strcpy(student1.name, "Jay"); // 문자열을 복사하는 함수 strcpy를 사용
    student1.id = 1;
    student1.grade = 4.0;

	// 멤버 접근하기
    printf("%s \n", student1.name); // "Jay"
    printf("%i \n", student1.id); // 1
    printf("%f \n", student1.grade); // 4.000
}

 

구조체 변수 선언과 값 초기화를 한 줄로 해결할 수 있다. 

 

    // 구조체 변수 선언 + 값 초기화
    struct Student student2 = {"Alice", 2, 3.5};
    
    printf("%s \n", student2.name); // "Alice"
    printf("%i \n", student2.id); // 2
    printf("%f \n", student2.grade); // 3.500 ..

 

{ } 안에 명시된 값은 구조체의 멤버 변수들이 선언된 순서대로 초기화된다. 

이를 '지정 초기화'라고 부른다. 

 

 

2. 구조체와 메모리

구조체 변수의 메모리 할당

구조체 변수가 선언될 때, 컴파일러는 구조체의 각 멤버에 필요한 메모리를 할당하고, 이들을 연속적인 메모리 공간에 배치한다.

struct Example {
    char a;     // 1바이트
    int b;      // 4바이트 (대부분의 시스템에서)
    char c;     // 1바이트
};

 

 

패딩(Padding), 정렬(Alignment)

컴퓨터는 메모리에 접근할 때 특정 정렬(Alignment)에 따라 최적으로 동작한다. 이는 데이터를 특정 크기의 경계(예를 들어, 4바이트 또는 8바이트)에 맞추어 저장함으로써 달성된다.

 

구조체의 경우, 멤버 변수들은 각 타입에 적합한 경계에 정렬되도록 메모리에 배치된다. 예를 들어, 4바이트 정수는 4바이트 경계에 딱 줄 맞춰서 서있어야 한다. 이 줄맞추기를 하기 위해 컴파일러가 패딩(Padding)을 삽입하기도 한다.

 

예를 들어, 위의 Example 구조체가 선언된 상황을 보자. 

이 경우, 메모리는 'a' -> 'b' -> 'c' 순으로 연속적으로 배치된다. 정렬 속성에 따르면, 다음과 같은 모양으로 저장된다. 

 

 

이게 추가적으로 의미하는 바가 뭐냐??

-> 구조체의 전체 크기는 각 멤버의 개별 크기의 합보다 클 수 있다.

 

즉, 구조체의 메모리 크기를 계산할 때는 삽입된 패딩도 고려해야 한다. 

sizeof(struct Example) 과 같은 함수를 통해 구조체의 크기를 확인할 수 있다. 

 

 

3. 구조체와 함수

우선 함수를 선언하려면 main 함수 밖에서 선언을 해줘야 한다.

구조체를 함수의 인자로 전달하기 

구조체를 함수의 인자로 전달할 수 있다. 일반적으로 두 가지 방법을 사용한다.

 

값에 의한 전달 

값을 통해 구조체의 복사본을 함수에 전달하게 된다.

// 함수 프로토타입 선언 (컴파일러에게 함수 존재 알려주기)
void printName(struct Student someone);

int main(void){
    // 구조체 타입의 변수를 선언.
    struct Student student1; // student1이라는 이름의 Student 구조체 인스턴스를 만듦. 

    // 각 멤버에 대한 값을 초기화
    strcpy(student1.name, "Jay"); // 문자열을 복사하는 함수 strcpy를 사용
    student1.id = 1;
    student1.grade = 4.0;
	
    ...

    // 구조체를 함수의 인자로 전달하기
    printName(student1); // Jay, 1, 4.000000
}

// printName 함수 정의
void printName(struct Student someone){
    printf("%s, %d, %f \n", someone.name, someone.id, someone.grade);
}

 

 

주소에 의한 전달

이 방법은 구조체의 주소(포인터)를 전달하여 메모리 사용을 줄이고, 원본 데이터에 대한 접근 및 변경을 가능케 한다.

void modifyGrade(struct Student *someone, float new_grade);

int main(void){
	위에서 구조체 변수 선언, jay 정보 만들었음
    
    ...

	// 주소값 건네주기
    modifyGrade(&student1, 3.5);
    printName(student1); // Jay, 1, 3.500..

}


// modifyGrade 함수 정의
void modifyGrade(struct Student *someone, float new_grade) {
    someone->grade = new_grade; // 포인터를 통해 멤버에 접근
}

 

main() 내의 modifyGrade() 함수 사용 부분을 보면, 포인터에 맞는 값을 주기 위해 주소를 건네주는 것을 확인할 수 있다. 

 

 

함수에서 return 값으로 구조체 포인터를 반환할 수도 있다.

구조체 자체를 반환하는 경우도 있지만, 구조체 크기가 커지면 비효율적이다. 

따라서 구조체 포인터를 반환하는 방식이 자주 사용된다. 

 

 

4. 구조체의 배열과 포인터 

구조체 배열 선언과 사용

student1, student2 .. 다 같은 범주에 있고 번호만 바꾸면 되는데 전교생 언제 다 만듦?

 

구조체 배열은 동일한 타입의 구조체 여러 개를 하나의 이름으로, 연속적인 메모리 공간에 저장하기 위해 사용한다. 

배열 선언 및 사용 예시는 다음과 같다.

    struct Student students[10]; // 10명의 학생을 저장할 수 있는 Student 구조체 배열 선언

    strcpy(students[0].name, "김덕배"); // 0번째 학생 정보 저장
    students[0].id = 4;
    students[0].grade = 3.3;

 

 

구조체 포인터 ('->' 연산자 사용)

각 구조체 변수(인스턴스)를 가리키는 포인터(참조변수)가 필요하다. 

그리고, 포인터를 사용할 때는 '->' 를 사용한다. 

포인터가 주소가 적힌 종이라면, -> 는 종이를 주면서 이 주소로 가라'라는 뜻이다. 

 

다음은 [구조체 포인터를 선언 -> 구조체 변수의 주소 할당 -> 포인터로 구조체 접근] 예시:

    struct Student student10; // 구조체 변수 선언
    struct Student *p10 = &student10; // 선언한 놈을 가리키는 포인터 p10
    p10 -> id = 10; // p10이 담고있는 주소로 가서(->) id를 10으로 지정하라

 

 

 

5. 중첩 구조체

구조체 내부에 다른 구조체 포함하기

중첩 구조체는 한 구조체 내에 다른 구조체를 멤버로 포함하는 것이다.

이는 데이터의 계층적 관계를 표현할 때 유용하다. 

 

예를 들어 'Date' 라는 구조체를 Student 안에 넣을 수 있다. 

struct Date {
    int year;
    int month;
    int day;
};

struct Student {
    char name[50];
    int id;
    float grade;
    struct Date birthday; // Date 구조체 변수 birthday 를 생성 후 Student 구조체 멤버로 포함
};

 

birthday라는 Date 구조체 변수를 생성 후, Student 구조체 멤버로 포함하였다. 

 

    strcpy(students[1].name, "james");
    students[1].id = 5;
    students[1].grade = 4.3;
    students[1].birthday.year = 2000;
    students[1].birthday.month = 3;
    students[1].birthday.day = 29;

 

하위 구조체 변수에 접근하려면 두 다리를 거쳐야 한다. 

 

 

중첩 구조체 - 구조체 포인터 활용

중첩 구조체와 구조체 포인터를 함께 사용하는 것은 C 프로그래밍에서 복잡한 데이터 구조를 관리하는 데 매우 효과적이다. 

중첩 구조체를 통해 복잡한 데이터간의 관계를 계층적으로 모델링할 수 있고,

구조체 포인터를 사용하면 이러한 데이터들에 대한 접근 및 조작을 메모리 효율적으로 할 수 있다.

 

위 예시를 그대로 활용해보자. 

    struct Student *p3 = &students[3];
    p3->id = 6;
    p3->grade = 3.5;
    p3->birthday.year = 1999;
    p3->birthday.month = 10;
    p3->birthday.day = 7;

    printf("%i", p3->id); // 6

 

 

6. 구조체와 타입 별칭

타입 별칭 붙이기: typedef

'typedef'는 C언어에서 타입에 별명을 지정하는데 사용되는 키워드이다. 구조체와 함께 사용하면, typedef는 구조체 타입에 더 간결하고 의미있는 이름을 부여할 수 있게 해준다. 이를 통해 코드 가독성과 프로그램 유지보수성을 개선할 수 있다. 

 

일반적으로 구조체를 정의할 때는 'struct' 키워드를 사용해야 하며, 구조체 변수를 선언할 때마다 'struct' 키워드를 반복해야 한다.

    // 구조체 변수 선언.
    struct Student student1; // student1이라는 이름의 Student 구조체 인스턴스를 만듦.

 

그런데 typedef를 통해 별칭을 정해주면 이러한 반복을 피할 수 있다. 

 

문법은 다음과 같다.

typedef struct {
	...
} 별칭이름;

 

 

typedef를 사용한 예시: 

typedef struct {
    int year;
    int month;
    int day;
} Date; // 꼬리에 별칭을 정해준다.

typedef struct {
    char name[50];
    int id;
    float grade;
    Date birthday; // typedef로 별칭을 붙이면 내장 타입인 것처럼 선언 가능
} Student;

 

쉽게 말해서, struct는 사용자가 자기 입맛에 맞게 정의한 사용자 정의 데이터타입이다.

때문에 struct 타입명 변수명 을 명시해주는 건데, 만약 우리 팀에서는 이게 너무 당연하고 자주 쓰이는 데이터타입이 돼버렸다면? 

(int, float, char 등 기본 데이터 타입들처럼)

 

int age; 처럼 단순하고 당연하게 선언할 수 있도록 하는게 더 편할 것이다. 

typedef는 그래서 등장한 기능이라고 볼 수 있다.