[자료구조 C 언어] C 프로그래밍 자료구조 - 2 : 배열과 구조체

Programming/C · 2020. 2. 27. 09:47

C 프로그래밍 기초에서 배열과 포인터에 대해 공부하면서 배열에 대한 이해는 충분히 됐다고 생각합니다.

 

C 언어에서 배열과 포인터는 거의 동일한 역할을 합니다.

 

단지, 배열은 상수 포인터는 변수라는 차이점을 가지고 있을 뿐입니다.

 

상수는 등호의 왼쪽에 설 수 없습니다.

 

위의 예제를 보시면 이해가 가시죠?

 

3 = 2; 라고 선언할 수 없으니까요... (a는 &a[0]과 같고 &a[0]은 주소 값입니다.)

 

 

배열은 간단히 정의만 하고 넘어가겠습니다.

 

 

1. 배열

 

배열은 동일한 타입의 데이터들을 묶는 구조입니다.

 

메모리의 연속된 위치에 차례대로 데이터를 저장합니다.

 

주소 당 1byte의 메모리가 할당됩니다.

 

즉, int a[3]는 각 원소 당 4byte의 메모리가 할당되고, 총 12byte의 메모리가 할당됩니다.

 

주소    0x00 ~ 0x03 // 0x04 ~ 0x07 // 0x08 ~ 0x0B

메모리      4byte    //      4byte     //     4byte

 

배열과 포인터는 다음과 같은 특징을 갖고 있습니다.

 

 

 

2. 구조체

 

구조체는 하나 이상의 자료형을 기반으로 '사용자 정의 자료형'을 만들 수 있는 문법 요소입니다.

 

배열과 다른 점은 다양한 자료형을 포함하고 있다는 것입니다.

 

선언하는 방법이 다양한데 3가지로 나눠서 보겠습니다.

 

1) 

struct A {

   int a;

   char b;

   double c;

};

 

이렇게 구조체와 구조체 이름을 선언할 수 있습니다.

 

사용하기 위해선

('struct 구조체 이름 구조체 변수')

 

// A란 struct 이름이 있고, 거기에 구조체 변수를 정해준다. (B or C)

struct A B;

struct A C;

 

B.a = 3;

B.b = 'c';

B.c = 0.2;

 

C.a = 1;

C.b = 'e';

C.c = 0.7;

 

2)

 

struct A {

   int a;

   char b;

   double c;

} B;

 

B.a = 3;

B.b = 'c';

B.c = 0.2;

 

이렇게 구조체 이름 선언과 동시에 변수를 정해줄 수도 있습니다.

 

이것도 다시 struct A C; ('struct 구조체 이름 구조체 변수')와 같은 구문을 추가 함으로써 아래와 같은 코드를 작성할 수 있습니다.

 

C.a = 1;

C.b = 'e';

C.c = 0.7;

 

3)

typedef struct A {

   int a;

   char b;

   double c;

} sA;

 

typedef @ &는 @를 앞으로 &라고 부를거야 라고 선언해주는 역할을 합니다.

 

예를들어, typedef unsigned short int UINT16과 같이 unsigned short int를 typedef를 통해 앞으로 UINT16으로 쓸 수 있게 만듭니다.

 

위와 같이 구조체 이름을 선언한 경우 다음과 같이 구조체의 변수를 선언할 수 있습니다.

('struct 구조체 이름 구조체 변수' == '구조체 별명 구조체 변수') => 두가지 방법 모두 가능

 

sA B;

sA C;

 

B.a = 3;

B.b = 'c';

B.c = 0.2;

 

C.a = 1;

C.b = 'e';

C.c = 0.7;

 

가끔 이럴 때도 있습니다.

 

typedef struct {

   int a;

   char b;

   double c;

} sA;

 

사용은 방금 전과 같습니다.

 

마지막 선언 방식은 익명 구조체라고 합니다.

 

물론 이름이 없기 때문에

 

'struct 구조체 이름 구조체 변수'와 같은 구문을 사용할 수 없습니다.

 

2-1. 구조체 포인터

 

다른 변수들과 마찬가지로 구조체도 포인터를 활용할 수 있습니다.

 

struct A {

    int a;

    char b[3];

};

 

위와 같이 선언되었을 때 구조체 이름을 다음과 같이 선언합니다.

 

struct A AA;

struct AA *pA = &AA;

 

구조체 멤버는 다음과 같이 접근할 수 있습니다.

 

AA.a = 10;

AA.b[0] = "123";

 

(*pA).a = 10; // AA.a = 10과 동일합니다. // 괄호가 중요합니다!!! *의 우선순위는 엄청 낮아요.

pA -> a = 10; // (*pA).a == AA.a = 10과 동일합니다.

 

 

2-2. 구조체 배열

 

마찬가지로 구조체로 배열을 만들 수 있습니다.

 

struct A AA{3] = {{1, "123"}, {2, "234"}, {3, "345"}};

 

위와 같이 선언을 하면 AA[0], AA[1], AA[2] 3개의 구조체 이름이 생성된 것과 같습니다.

 

초기화는 바로 괄호를 통해 해주었습니다.

 

단일 구조체 변수도 괄호로 초기화 할 수 있습니다.

(struct A BB = {4, "456"};)

 

2-3 구조체 포인터와 배열

 

구조체 배열을 구조체 포인터와 void 포인터를 통해 가리키는 것을 해보겠습니다.

 

일단 위와 동일한 struct A가 선언됐다고 가정하고 진행하겠습니다.

 

struct A AA{3] = {{1, "123"}, {2, "234"}, {3, "345"}};

struct A *p1;

 

void *p2 = malloc(sizeof(struct A) * 3); // 빈 포인터에 구조체 A 3개 만큼의 메모리 할당

(A는 int, char [3]로 구성되어 있으므로 (4+1*3)*3 = 21의 메모리가 할당된다.)

(는 뻥입니다...하하하 8*3 = 24의 메모리가 할당됩니다.)

(글 말미에 설명드리겠습니다.)

 

p2 = AA; // p2에 AA 배열의 첫번째 요소 주소를 넣습니다.

 

이렇게 선언하고 난 다음에 다음과 같이 멤버 변수에 접근할 수 있습니다.

 

p1[0].a = 2; //== AA[0].a = 2;

p1[0].b[2] = '2'; //== AA[0].b[2] = '2'; 

 

또는, 위와 같은 기능을 하기 위해

 

p1[0].a는 (*(p1+0)).a와 (p1+0) -> a로 바꿔 사용할 수 있습니다.

(원래 p.a를 (*p).a로 쓸 수 있는데 (*p).를 쓰기 귀찮아서 간접참조 연산자인 p->로 대체했다고 생각하시면 편합니다.)

(즉, p.a == (*p).a == p->a가 모두 동일한 역할을 합니다.)

 

void 포인터 p2는 다음과 같이 멤버 변수에 접근할 수 있습니다.

 

(*((struct A*)p2+1)).a == ((struct A*)p2+1)->a

(*((struct A*)p2+1)).b[1] == ((struct A*)p2+1)->b[1]

 

p1과 거의 동일한데 강제 형변환 (struct A*)을 p2 앞에 붙여주는 것만 다릅니다.

 

아래 코드를 통해 결과를 확인하실 수 있습니다.

 

struct A{

    int a;

    char b[3];

};

 

struct A AA[3] = {{1, "123"}, {2, "234"}, {3, "345"}};

struct A *p1 = AA;

 

 

int main(int argc, const char * argv[]) {

 

    printf("%d\n", p1[1].a); //2

    printf("%d\n", p1[1].b[1]); //3 

 

    printf("%d\n", (p1+1)->a);  //2

    printf("%d\n", (*(p1+1)).a);  //2

  

    printf("%s\n", (p1+1)->b); // 234

    printf("%s\n", (*(p1+1)).b); // 234

 

    printf("%c\n", (p1+1)->b[1]); // 3

    printf("%c\n", (*(p1+1)).b[1]); // 3

    

    printf("%c\n", *((p1+1)->b+1)); // 3

     

//    printf("%c\n", *(*(p1+1).(b+1)); // 안 됨 ㅜㅜ

    

 

    printf("%d\n", ((struct A*)p2)[1].a); // 2

    printf("%c\n", ((struct A*)p2)[1].b[1]); //3

    printf("%d\n", ((struct A*)p2+1)->a); //2
    printf("%d\n", (*((struct A*)p2+1)).a); //2

    printf("%s\n", ((struct A*)p2+1)->b); // 234
    printf("%s\n", (*((struct A*)p2+1)).b); // 234

    printf("%c\n", ((struct A*)p2+1)->b[1]); // 3
    printf("%c\n", (*((struct A*)p2+1)).b[1]); // 3

    printf("%c\n", *(((struct A*)p2+1)->b+1)); // 3

 

    return 0;

}

 

3. 자기 참조 구조체

 

구조에의 속성 중 하나가 스스로를 가리키는 구조체 입니다.

 

struct A{

    char data;

    struct A *A;

};

 

위와 같이 선언해 줄 수 있습니다.

 

연결 리스트의 구현에 많이 사용됩니다.

 

지금은 이름만 기억해주세요!!

 

큐, 스택 이후에 연결 리스트 게시물에서 주구장창 할 거에요...

 

 

**추가

 

구조체의 메모리 사이즈에 관하여!

 

1. 각각의 멤버를 저장하기 위해서 기본 4 byte 단위로 메모리가 할당된다.

-> char 1개를 읽어올 때 1byte만 달랑 읽는게 아니라 1word (4byte) 단위로 읽어온다.

=> 이런 방식이 속도가 더 빠르다.

 

2. 구조체의 각 멤버 중에 가장 큰 멤버의 크기에 영향을 받는다.

 

struct A{

    char a;

    int b;

};

1번 규칙이 적용 됐습니다.

struct A{

    char a;

    char b;

    int c;

};

1번 규칙이 적용돼서 여유가 생긴 a뒤 3byte 중에 b가 들어갔습니다.

 

struct A{

    char a;

    int c;

    char b;

};

순서에도 영향을 받습니다. 크기가 작은 자료형을 앞에 몰아넣는 것이 유리할 것 같습니다.

struct A{

    char a;

    double b;

};

2번 규칙이 적용되어 double 형의 크기를 따라갑니다.

struct A{

    char a;

    int c;

    double b;

};

2번 규칙이 적용되어 a 뒤에 생긴 7byte에 c도 포함될 수 있습니다. 이때 a를 읽어오기 위한 4byte는 1번 규칙에 의해 유지됩니다.

참조 : https://blog.naver.com/sharonichoya/220495444611

 

다음 게시물에서는 큐와 스택을 들어가기 전 순서 리스트와 배열과 구조체를 통한 다항식의 표현에 대해 알아보겠습니다.

반응형