본문 바로가기

Programming/C

[ProjectH4C] 코딩도장 Unit 51~53 Write-Up

 

구조체 활용

멤버로 문자형 변수와 정수형 변수를 하나씩 가지고 있는 구조체가 있다고 치자. 문자형은 1바이트, 정수형은 4바이트이기 때문에 구조체의 전체크기는 5바이트가 되어야 하지만, 아래의 코드에서 실제 구조체의 크기를 출력해보면 5바이트가 아닌 8바이트가 출력된다.

#include <stdio.h>

struct PacketHeader {
    char flags;    // 1바이트
    int seq;       // 4바이트
};

int main()
{
    struct PacketHeader header;

    printf("%d\n", sizeof(header.flags));           // 1: char는 1바이트
    printf("%d\n", sizeof(header.seq));             // 4: int는 4바이트
    printf("%d\n", sizeof(header));                 // 8: 구조체 전체 크기는 8바이트
    printf("%d\n", sizeof(struct PacketHeader));    // 8: 구조체 이름으로 크기 구하기

    return 0;
}

 

실행 결과

1
4
8
8

 

이러한 결과가 나오는 이유는 구조체의 특성 때문이다. C언어에서 구조체를 정렬할 때 멤버 변수 중 가장 큰 자료형의 배수로 정렬한다. 무슨 말인가 하면, 위의 코드에서 구조체 멤버 중 가장 큰 자료형은 4바이트인 int형이다. 해당 구조체는 크기를 정할 때 4의 배수로 정하는데, 4바이트의 크기로는 5바이트를 담을 수 없기 때문에 8바이트의 공간을 확보한다. 이 때, 각각의 자료형의 공간도 4의 배수만큼 할당이 되는데, 구조체 정렬을 그림으로 나타내면 다음과 같다.

 

이 때 char형 의 공간이 4바이트만큼 할당 되었지만, 1바이트만 사용하고 3바이트가 남는다. 이렇게 남게 되는 공간을 패딩(padding)이라고 한다. 실제로 사용하진 않지만 공간을 차지하게 되고 골치거리가 되는데, C언어 자체에서는 구조체 정렬에 관련된 표준 방법이 없다. 대신, 각 컴파일러마다 제공하는 지시자를 이용하면 패딩을 없앨 수 있다.

  • Visual Studio, GCC 4.0 이상
#pragma pack(push, 정렬크기)
#pragma pack(pop)

 

  • GCC 4.0 미만
__attribute__((aligned(정렬크기), packed))

 

해당 지시자들을 구조체를 정의하는 부분에서 사용해주면 되는데 사용 방법은 다음과 같다.

 

//Visual Studio, GCC 4.0 이상

#pragma pack(push, 1)    // 1바이트 크기로 정렬
struct PacketHeader {
    char flags;    // 1바이트
    int seq;       // 4바이트
};
#pragma pack(pop)        // 정렬 설정을 이전 상태(기본값)로 되돌림


//GCC 4.0 미만

struct PacketHeader {
    char flags;    // 1바이트
    int seq;    // 4바이트
} __attribute__((aligned(1), packed));    // GCC 4.0 미만: 1바이트 크기로 정렬

 

#pragma pack(push, 1)에서 1바이트의 크기로 정렬한 이유는 C언어에서 자료형은 모두 바이트 단위이고, 이 중 가장 작은 게 1바이트 크기이기 때문에 이를 기준으로 정렬하여서 빈 공간이 없도록 하기 위함이다. 만약 #pragma pack(pop) 지시자가 없다면, 이후에 정의되는 구조체에 대해서도 1바이트 단위로 정렬이 이루어지게 되기 때문에, 만약 해당 구조체만 1바이트 크기로 정렬하고자 한다면 구조체 정의가 끝난 후 꼭 #pragma pack(pop)을 선언해주어야 한다. 실제로 위의 작업을 해준 후에 크기를 출력하면 구조체의 크기가 8바이트가 아닌 5바이트가 출력되는 것을 볼 수 있다. 만약 멤버의 자료형 중 가장 큰 자료형의 크기인 4바이트보다 크게 정렬하고자 해서 8, 16 등으로 정렬해도 구조체의 크기는 8이 나온다. 구조체 안에서 가장 큰 자료형의 크기보다 큰 값으로 정렬할 수 없기 때문이다.

 

 

구조체와 메모리

구조체 변수를 선언하거나 메모리를 할당하면 메모리 공간을 차지하기 때문에 구조체도 메모리 관련 함수를 사용해서 관리할 수 있다. 

먼저 구조체의 멤버를 한번에 초기화 하는 방법이다.

struct 구조체이름 변수이름 = {0, };

구조체의 내용을 모두 0으로 초기화 할 수있지만 malloc 함수로 할당한 메모리엔 사용할 수 없다. 중괄호를 이용해 초기화 하는 방법 말고 memset 함수를 이용해 초기화 하는 법도 있다.

memset(&구조체변수, 0, sizeof(struct 구조체이름));

memset 함수는 malloc 함수로 할당한 메모리에도 사용할 수 있다.

 

 

memcpy 함수를 이용해 구조체의 내용을 복사할 수도 있다. p1 구조체의 내용을 p2 구조체에 복사한다면 코드는 다음과 같다.

memcpy(&p2, &p1, sizeof(struct 구조체이름));

 

만약, 동적 할당한 구조체끼리 복사하고자 한다면 &p2, &p1 대신 p2, p1을 써서 복사하면 된다.

동적 할당한 구조체와 구조체 변수로 선언한 구조체끼리도 변수 형태만 맞춰주면 복사가 가능하다.

 

 

 

구조체 배열

구조체를 정의했다면, 구조체를 사용하는 것은 하나의 변수를 사용하는 것과 같다. 만약 좌표를 저장하는 구조체를 정의했는데, 좌표를 굉장히 많이 저장해야 한다면 일일이 구조체 변수를 만들어 사용할 수는 없는 노릇이다. 그렇기에, C언어에서는 구조체도 배열로 선언해서 사용할 수 있다.

struct 구조체이름 배열이름[크기];

struct 구조체이름 배열이름[크기];

선언하고 사용하는 것은 기본적인 배열 사용법과 모두 동일하다.

 

구조체 포인터 배열

구조체 배열도 포인터로 선언해서 사용할 수 있다.

struct 구조체이름 *배열이름[크기];

[크기]는 세로부분의 크기이다. 각각의 줄에 가로의 크기를 할당하지 않았기 때문에 for문을 이용해서 각 줄에 메모리를 할당해주면 된다.

 

struct Point2D *p[3];

for (int i = 0; i < sizeof(p) / sizeof(struct Point2D *); i++){
    p[i] = malloc(sizeof(struct Point2D));
}

일반적인 포인터 배열을 선언하고 각 줄마다 동적 할당하는 것과 동일하다. 

만약 구조체 포인터 배열을 생성해서 사용한다면, 각 멤버에 접근 할 땐 점이 아닌 화살표(->)로 접근하는 것을 주의해야 한다.

 

 

 

 

 

 


 

 

#51.3 퀴즈

구조체의 크기를 구하는 법은 sizeof(struct 구조체 이름), sizeof(구조체 변수이름), sizeof 구조체변수이름 이다. 답은 b, d, e.

 

구조체를 정렬하는 지시어는 __attribute__((aligned(크기), packed))이다. 답은 e.

 

 

 

구조체를 정렬할 땐 자료형의 크기가 가장 큰 단위로 이루어진다. 멤버들의 자료형의 크기는 위에서부터 1, 4, 4, 8, 4이므로 long long(8)을 기준으로 정렬된다.

 

구조체 정렬의 크기를 지시하는 지시자는 #pragma pack(push, 크기), #pragma pack(pop)이다. 답은 e.

 

 

#51.4 연습문제: 압축 헤더 크기 구하기

#include <stdio.h>

struct CompressHeader {
    char flags;
    int version;
};

int main()
{
    struct CompressHeader header;

    printf("%d\n", ______________);

    return 0;
}

해당 구조체는 정렬 기준에 대한 명시가 없으므로 크기가 8(패딩이 3)인 구조체로 선언된다. 8을 출력하려면 해당 구조체 변수로 선언된 header의 크기를 출력하면 된다. 

sizeof(header)

 

 

#51.5 연습문제: 패킷 크기 조절하기

#include <stdio.h>

#pragma pack(push, 1)
struct Packet {
    _________ length;
    int seq;
};
#pragma pack(pop)

int main()
{
    struct Packet pkt;

    printf("%d\n", sizeof(pkt));

    return 0;
}

해당 코드를 실행했을 때 6이 출력되어야 한다. pkt의 사이즈가 6이여야 하기 때문에, 이들의 멤버 변수의 크기가 6바이트여야 한다. Packet 구조체를 1바이트로 정렬했기 때문에 빈 공간은 없고, int형 변수의 크기 4를 빼면 2가 남기 때문에 length의 자료형의 크기는 2바이트여야 한다. 크기가 2바이트인 자료형은 short.

 

#51.6 심사문제: 암호화 헤더 크기 구하기

#include <stdio.h>

struct EncryptionHeader {
    char flags;
    ________
    ________
};
 
int main()
{
    struct EncryptionHeader header;
 
    printf("%d\n", sizeof(header));

    return 0;
}

header 구조체 변수의 크기를 출력했을 때 실행 결과가 12가 나와야 한다. 정렬 기준을 따로 정해주지 않았기 때문에 4바이트 크기의 자료형 변수하나와 4바이트 이하의 변수 하나를 선언해주면 3개의 멤버가 각각 4바이트씩 차지해서 전체 크기는 12바이트가 된다. 

int a;
int b;

 

#51.7 구조체 멤버 정렬 사용하기

#include <stdio.h>

____________________
____________________
____________________
____________________
____________________
____________________

int main()
{
    struct Packet pkt;

    printf("%d\n", sizeof(pkt));

    return 0;
}

pkt의 크기가 3이 되어야 한다. 구조체의 크기를 3바이트로 해주기 위해선, 단순하게 멤버 변수 3개를 모두 char형으로 선언해주면 된다.

 

struct Packet{
    char a;
    char b;
    char c;
};

 

 

#52.3 퀴즈

memset 함수의 사용법은 memset(구조체의 주소, 초기화할 값, 구조체 크기)이다. 답은 b.

 

 

 

memcpy 함수의 첫 번째, 두 번째 인자는 각각 복사할 곳의 주소, 복사당할 곳의 주소가 와야한다. p1은 포인터이기 때문에 변수 자체가 주소가 되고 p2는 일반 구조체 변수이기 때문에 &p2를 인자로 넘겨주어야 한다. 답은 a.

 

 

#52.4 연습문제: 2차원 좌표 초기화하기

#include <stdio.h>
#include <stdlib.h>

①_______________________

struct Point2D {
    int x;
    int y;
};

int main()
{
    struct Point2D p;
    struct Point2D *ptr = malloc(sizeof(struct Point2D));

    memset(②____________________________);
    memset(③____________________________);

    printf("%d %d %d %d\n", p.x, p.y, ptr->x, ptr->y);

    free(ptr);

    return 0;
}

memset 함수를 통해 p와 ptr을 초기화 해야한다. 이를 사용하기 위해 string.h 헤더 파일을 선언해야 한다.

1. #include <string.h>

2. &p, 0, sizeof(struct Point2D)

3. ptr, 0, sizeof(struct Point2D)

 

 

#52.5 연습문제: 2차원 좌표 복제하기

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Point2D {
    int x;
    int y;
};

int main()
{
    struct Point2D p1;
    struct Point2D *p2 = malloc(sizeof(struct Point2D));

    p1.x = 10;
    p1.y = 20;

    memcpy(________________________________);

    printf("%d %d\n", p2->x, p2->y);

    free(p2);

    return 0;
}

p1.x, p1.y에 저장된 10, 20을 각각 p2->x, p2->y에 복사해야 한다. memcpy(복사대상, 복사원본, 복사크기)로 작성하면된다.

p2, &p1, sizeof(struct Point2D)

 

 

#52.6 심사문제: 인적 정보 삭제하기

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>

struct Person {
    char name[20];
    int age;
    char address[100];
};

int main()
{
    struct Person p1;

    strcpy(p1.name, "홍길동");
    p1.age = 30;
    strcpy(p1.address, "서울시 용산구 한남동");

    memset(____________________________);

    printf("이름: %s\n", p1.name);
    printf("나이: %d\n", p1.age);
    printf("주소: %s\n", p1.address);

    return 0;
}

char형 배열에 '\0'을 넣어주면 아무것도 출력되지 않고, int형 정수에 '\0'을 넣어주면 0이 출력된다. memset 함수를 통해 멤버 변수의 초기값을 '\0'로 지정해주면 된다.

&p1, '\0', sizeof(struct Person)

 

 

#52.7 심사문제: 인적 정보 복제하기

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Person {
    char name[20];
    int age;
    char address[100];
};

int main()
{
    struct Person *p1 = malloc(sizeof(struct Person));
    struct Person p2;

    strcpy(p1->name, "고길동");
    p1->age = 40;
    strcpy(p1->address, "서울시 서초구 반포동");

    memcpy(____________________________);

    printf("이름: %s\n", p2.name);
    printf("나이: %d\n", p2.age);
    printf("주소: %s\n", p2.address);

    free(p1);    

    return 0;

}

p1의 내용을 p2로 복사해야 한다. memcpy(복사대상, 복사원본, 구조체크기)를 활용하면 된다. 이 때, p1은 포인터이므로 p1, p2는 구조체 변수이므로 &p2로 사용하는 것을 주의해야 한다.

&p2, p1, sizeof(struct Person)

 

#53.3 퀴즈

구조체 배열은 struct 구조체이름 배열이름[크기] 로 선언한다. 답은 b.

 

 

#53.3 퀴즈

구조체 포인터 배열은 struct 구조체이름 *배열이름[크기]로 선언한다. 답은 b.

 

포인터 변수가 아니기 때문에 화살표가 아닌 점으로 접근해야 한다. 답은 d.

 

포인터 변수이기 때문에 화살표로 접근해야 한다. 답은 f.

 

 

#53.4 연습문제: 2차원 좌표 출력하기

#include <stdio.h>

struct Point2D {
    int x;
    int y;
};

int main()
{
    ①__________________________

    p[0].x = 10;
    p[0].y = 20;
    p[1].x = 30;
    p[1].y = 40;
    p[2].x = 50;
    p[2].y = 60;

    ②_____________________________________________________
    ...
    ______________________________________________________

    return 0;
}

먼저 3개짜리 구조체 배열을 선언한 후에 각 멤버에 값을 지정해주고 for문을 통해 p[0]의 x, y p[1]의 x, y p[2]의 x, y를 순차적으로 출력하면 된다.

1. struct Point2D p[3];

 

2.

for(int i=0; i<sizeof(p) / sizeof(struct Point2D); i++){
    printf("%d %d \n", p[i].x, p[i].y);
}

 

 

#53.5 연습문제: 인적 정보를 초기화하기

아래에 free 함수가 있기 때문에 3000개 크기의 구조체 포인터 배열을 선언하고 memset을 통해 0으로 초기화 해주면 된다.

1. struct Person *p[3000];

 

2.

for(int i=0; i<sizeof(p) / sizeof(struct *Person); i++){
    p[i] = malloc(sizeof(struct Person));
    memset(p[i], 0, sizeof(struct Person));
}

 

 

#53.6 심사문제: 선의 길이 구하기

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <math.h>

struct Point2D {
    int x;
    int y;
};

int main()
{
    struct Point2D p[4];
    double length = 0.0f;

    scanf("%d %d %d %d %d %d %d %d", 
        &p[0].x, &p[0].y, &p[1].x, &p[1].y,& p[2].x, &p[2].y, &p[3].x, &p[3].y
    );

    _______________________
    _______________________
    _______________________
    _______________________

    _______________________
    _______________________ 

    printf("%f\n", length);

    return 0;
}

p[0]와 p[1]사이의 거리를 length에 더해주고, p[1]와 p[2]사이의 거리를 length에 더해주고, p[2]와 p[3]사이의 거리를 length에 더해주면 되는데, 이 때, 각각의 점 사이의 거리를 구하는 공식은 모두 같기 때문에 for문을 통해 한번만 작성해주면 된다.

 

for(int i=0; i<(sizeof(p) / sizeof(struct Point2D))-1; i++){
    double disX = p[i].x - p[i+1].x;
    double disY = p[i].y - p[i+1].y;
    length += sqrt((disX)*(disX) + (disY)*(disY));
}

 

 

#53.7 심사문제: 나이가 가장 많은 사람 찾기

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

struct Person {
    char name[30];
    int age;
};

int main()
{
    struct Person *p[5];

    ____________________________
    ____________________________
    ____________________________
    ____________________________

    ____________________________
    ____________________________
    ____________________________    
    ____________________________
    ____________________________
    ____________________________
    ____________________________

    ____________________________
    ____________________________
    ____________________________    
    ____________________________
    ____________________________
    ____________________________
    ____________________________

    ____________________________

    for (int i = 0; i < sizeof(p) / sizeof(struct Person *); i++)
    {
        free(p[i]);
    }

    return 0;
}

먼저 구조체 포인터 배열 *p[5]의 각각의 배열에 구조체의 크기만큼 동적 할당해야 하고, 나이를 비교하기 위해 임시로 나이를 저장할 변수(oldest)와 나이가 가장 많은 사람의 인적 정보가 저장된 인덱스를 저장할 변수(oldestIdx)를 선언한다.oldest는 0으로 초기화해준다. 그 후에 for문을 이용해 5명의 이름과 나이를 받아 각각의 배열에 저장하고, 저장과 동시에 oldest 변수와 비교해서 나이가 더 큰 사람의 인덱스를 저장한다. 출력은 p[oldestIdx]->name이 된다. 

 

int oldest=0, oldestIdx;
for(int i=0; i<sizeof(p) / sizeof(struct Person *); i++){
    p[i] = malloc(sizeof(struct Person));
    scanf("%s %d", p[i]->name, &p[i]->age);
    if(p[i]->age > oldest){
        oldest = p[i]->age;
        oldestIdx = i;
    }
}
printf("%s \n", p[oldestIdx]->name);