관리 메뉴

History

c 언어 온라인 무료강좌 8차시 정리 본문

Tipslab 강좌 복습/김성엽 선생님 c 강의 복습

c 언어 온라인 무료강좌 8차시 정리

luckybee 2021. 2. 3. 10:56
728x90
반응형

해당 게시물은 김성엽 선생님의 강의를 바탕으로 만든 게시물입니다.

 

●구조체

 

-서로 다른 데이터 형식을 묶는 방법

 

 

만약 나이, 시급, 월급 데이터를 입력하려고 했을 때, 

 

나이: 1byte 

시급: 2byte

월급: 4byte

 

이렇게 7byte의 용량이 필요하다.

 

또한 이 데이터는 배열에 저장할 수도 있고, 포인터 문법을 사용할 수 도 있고, 구조체를 사용할 수 도있다.

 

그리고 이 데이터들의 특성상 음수는 나올 수 없으므로 unsigned까지 붙이면 된다.

 

그럼 구조체로 데이터를 입력하기 전에 포인터 문법으로 이 데이터들을 입력해 보겠다.

 

 

+) 

__int8 *p //8bit 정수 사용하겠다는 뜻

UINT8 *p // windows.h 헤더파일을 인클루드 해야함

but

헤더파일을 include하지 않고 바로 쓰고 싶으면 구조체로 정의하면 된다


typedef unsigned char UINT8;   //8bit=1byte

typedef unsigned char UINT16; //16bit=2byte

typedef unsigned char UINT32; //32bit=4byte



이렇게 구조체로 만들고 UINT8*p 이렇게 쓰면

unsigned char *p와 같은 뜻이 된다.



UINT16*p 이렇게 쓰면

unsigned short *p와 같은 뜻이 된다.



마찬가지로 UINT32*p 이렇게 쓰면

unsigned int *p와 같은 뜻이 된다.

 

 

그럼 다시 넘어와서 코드를 작성해보면 아래와 같다.

#include<stdio.h>
#include<malloc.h>

typedef unsigned char UINT8;   //8bit=1byte 
typedef unsigned char UINT16; //16bit=2byte 
typedef unsigned char UINT32; //32bit=4byte


int main()

{

     UINT*p;

     p=(UINT8*)malloc(7); // 7바이트 만큼 동적할당

    *p=23;  //나이 대입
    *(UINT16*)(p+1)=10000;  //시급 대입
    *(UINT32*)(p+3)=3200000;  //월급 대입
    free(p);
}

주소를 p에 덧셈해서 이동하고 있다.

이처럼 구조체는 덧셈으로 위치를 찾아가고, 배열은 곱셈으로 찾아간다.

 

위의 코드를 그림으로 보자.

이렇게 그림에서도 봤듯이 중간에 빈 공간이 없어서 매우 효율적이다. 

 

실제로 현직에서 많이 사용하는 방식이기도 하다. 왜냐하면 데이터 전송에 효율이 좋기 때문이다.

 

이제 구조체로 구현해보자.

#include<stdio.h>

typedef unsigned char UINT8;   //8bit=1byte 
typedef unsigned char UINT16; //16bit=2byte 
typedef unsigned char UINT32; //32bit=4byte

typedef struct MyData 
{
    UINT8 age;     //나이
    UINT16 wage;   //시급
    UINT32 salary; //월급
}MD;  //MyData와 이름 달라도 된다.

int main()
{
    MD data;
    data.age=23;
    data.wage=10000;
    data.salary=3000000;
    
    //UINT*p;
    // p=(UINT8*)malloc(7); // 7바이트 만큼 동적할당
    //*p=23;  //나이 대입
    //*(UINT16*)(p+1)=10000;  //시급 대입
    //*(UINT32*)(p+3)=3200000;  //월급 대입
    //free(p);
}

확실히 구조체가 더 익숙하기도 하고 직관적이고 보기 편하다.

 

+)

구조체의 코드를 다른 방법으로 대입할 수 있다. 아까 포인터 문법으로 했던 방식과 유사한 방법이다.

#include<stdio.h>

typedef unsigned char UINT8;   //8bit=1byte 
typedef unsigned char UINT16; //16bit=2byte 
typedef unsigned char UINT32; //32bit=4byte

typedef struct MyData 
{
    UINT8 age;     //나이
    UINT16 wage;   //시급
    UINT32 salary; //월급
}MD;  //MyData와 이름 달라도 된다.

int main()
{
    MD data;
    *(UINT8*)&data=23;                        //data.age=23; 
    *(UINT16*((UINT8*)&data+2)&data=10000;    //data.wage=10000; 
    *(UINT32*((UINT8*)&data+4)&data=3000000;  //data.salary=3000000;
   
}

이렇게 복잡한 부분은 컴파일러가 알아서 해준다. 그냥 코드 보는 실력을 기르고 싶다. 아니면 누군가 알려줄 때 괴롭히고 싶다. 이럴 때 쓰는 방법이다.

 

또한 여기서 왜 +2가 나오고 +4가 나오는지 알아보자

 

typedef struct MyData 
{
    UINT8 age;    //1     
    UINT16 wage;  //2 
    UINT32 salary;//4 
}MD;  

옆에 보이는 숫자처럼 이 구조체 안의 데이터 크기는 7byte다.

 

그럼 실제로도 이 구조체가 7byte일까? 

 

정답은 아니다. 답은 8byte이다.

 

왜일까?

 

기본적으로 구조체는 8byte 정렬을 기반으로 두고 있다. 그러나 위의 코드처럼 각각의 바이트가 8보다 작으면 자기 크기에 맞은 데이터 크기를 사용한다. 그래서 age wage salary는 각각 1,2,4byte의 크기를 갖는다.

 

이제 왜 이 구조체가 8byte인지 그림으로 보자.

1byte 먼저 대입하고 2byte를 대입하려면 2의 배수의 칸에 대입해야 한다. 그래서 중간에 쓰레기 1byte가 생긴다.

구조체는 1 2 4 8바이트 단위로 정렬이 가능하다.

 

그럼 구조체 엘리먼트의 순서를 바꾸면 어떻게 될까?

typedef struct MyData 
{
    UINT8 age;    //1     
    UINT32 salary;//4 
    UINT16 wage;  //2 
}MD;  

이렇게 된 코드는 아래 그림과 같다.

이렇게 안되려면 가장 크기가 작은 엘리먼트를 가장 먼저 쓰는 것이 데이터 효율이 가장 좋게 나온다. 또한 UINT32의 크기가 중간에 들어왔기 때문에 현재 고정 엘리먼트는 4byte이다. 그래서 wage도 2byte만큼 용량을 버리게 되는 것이다.

 

 

ps) 구조체 값 대입 방법 +연습

#include<stdio.h>

typedef unsigned char UINT8;   //8bit=1byte 
typedef unsigned char UINT16; //16bit=2byte 
typedef unsigned char UINT32; //32bit=4byte

typedef struct MyData 
{
    UINT8 age;     
    UINT16 wage;   
    UINT32 salary; 
}MD;  

int main()
{
    MD data={32, 10000, 30000000}; //이렇게 대입이 가능하다.
    
    MD temp[3]={
    {32, 10000, 30000000},
    {33, 20000(★), 40000000},
    {34, 30000, 50000000}
    };  //구조체 배열   
        //이렇게 대입이 가능하다.
    temp[1].wage=20000;  //★모양 부분의 값이 바뀐다.
    
    
    //포인터 방식 구조체 대입
    (*(temp+1)).wage=30000;//★모양 부분의 값이 바뀐다.  //배열표현
    (temp+1)->wage=30000;//윗줄이랑 같은 내용이다.       //구조체 포인터 표현
}

포인터 방식 표현은 한번 더 연습해보자. 반복문에 대입한다고 가정을 해보자. 아래 코드를 보겠다.

 

#include<stdio.h>

typedef unsigned char UINT8;   //8bit=1byte 
typedef unsigned char UINT16; //16bit=2byte 
typedef unsigned char UINT32; //32bit=4byte

typedef struct MyData 
{
    UINT8 age;     
    UINT16 wage;   
    UINT32 salary; 
}MD;  

int main()
{
    MD data={32, 10000, 30000000}; //이렇게 대입이 가능하다.
    
    MD temp[3]={
    {32, 10000, 30000000},
    {33, 20000, 40000000},
    {34, 30000, 50000000}
    };  
    
    /*
    for(int i=0; i<3; i++)
    {
       temp[i].wage=20000;   //각 구조체의 wage에 20000을 대입 (덧셈연산 2번)
    }
    */
    
    
    for(int i=0; i<3; i++)
    {
       *((UINT16*)temp+5)=20000; //형변환을 통해 위의 코드와 같은 효과를 볼 수 있음
                                 //연산속도도 더 빠름 (덧셈연산 1번)
    }
}

이번에는 각 나이, 시급, 월급을 평균 내는 프로그램을 만들어보자.

 

아래는 코드이다.

#include<stdio.h>

typedef unsigned char UINT8;   //8bit=1byte 
typedef unsigned char UINT16; //16bit=2byte 
typedef unsigned char UINT32; //32bit=4byte

typedef struct MyData 
{
    UINT8 age;     
    UINT16 wage;   
    UINT32 salary; 
}MD;  

int main()
{
    MD data={32, 10000, 30000000}; //이렇게 대입이 가능하다.
    
    MD temp[3]={
    {32, 10000, 30000000},
    {33, 20000, 40000000},
    {34, 30000, 50000000}
    };  
    
    int total_age=0,total_wage=0,total_salary=0;
    
    for(int i=0; i<3; i++)
    {
       total_age+=temp[i].age;  //total_age+=(*(temp+i)).age;
       total_wage+=temp[i].wage;//total_wage+=(temp+i)->wage;  //이 방법하고 위의 방법하고 같다.
       total_salary+=temp[i].salary;//total_salary+=(temp+i)->.salary
    }
    //위 코드를 보면 for에서 i++를 한번을 하고 내부에서는 3번의+연산을 한다. =>비효율
}

우리가 지금까지 편하게 사용해왔던 코드이다. 이 코드는 현재는 데이터 양이 적어서 괜찮지만 나중에 데이터의 양이 방대해지면 어마어마하게 느려질 것이다. 그럼 이 코드를 효율적으로 바꿔보자.

 

#include<stdio.h>

typedef unsigned char UINT8;   //8bit=1byte 
typedef unsigned char UINT16; //16bit=2byte 
typedef unsigned char UINT32; //32bit=4byte

typedef struct MyData 
{
    UINT8 age;     
    UINT16 wage;   
    UINT32 salary; 
}MD;  

int main()
{
    MD *p;
    
    MD temp[3]={
    {32, 10000, 30000000},
    {33, 20000, 40000000},
    {34, 30000, 50000000}
    };  
    
    int total_age=0,total_wage=0,total_salary=0;
    p=temp;  //주소대입
    for(int i=0; i<3; i++)
    {
       total_age+=p->age; 
       total_wage+=p->wage;
       total_salary+=p->salary;
       p++;  //+연산을 한번으로 줄였다. 효율 up
    }
}

 

위 코드를 보면 반복문에 i가 숫자를 세어주는 일 말고 존재할 필요가 없다, 즉 쓸모없이 연산을 잡아먹고 있기 때문에 이 코드를 한 번 더 연산을 줄여보자.

 

#include<stdio.h>

typedef unsigned char UINT8;   //8bit=1byte 
typedef unsigned char UINT16; //16bit=2byte 
typedef unsigned char UINT32; //32bit=4byte

typedef struct MyData 
{
    UINT8 age;     
    UINT16 wage;   
    UINT32 salary; 
}MD;  

int main()
{
    MD *p;
    
    MD temp[3]={
    {32, 10000, 30000000},
    {33, 20000, 40000000},
    {34, 30000, 50000000}
    };  
    
    int total_age=0,total_wage=0,total_salary=0;
    p=temp;  //주소대입
    
    MD* p_limit=temp+3  //0 1 2 끝 구조체의 끝 <=이렇게 하려면 2까지
    while(p<p_limit)
    {
       total_age+=p->age; 
       total_wage+=p->wage;
       total_salary+=p->salary;
       p++;  //+연산을 한번으로 줄였다. 효율 up
    }
}

 

마지막으로 간단히 개념을 정리하는 코드를 보고 연결 리스트로 넘어가 보자.

#include<stdio.h>

typedef unsigned char UINT8;   //8bit=1byte 
typedef unsigned char UINT16; //16bit=2byte 
typedef unsigned char UINT32; //32bit=4byte

typedef struct MyData 
{
    UINT8 age;     
    UINT16 wage;   
    UINT32 salary; 
}MD;  

int main()
{
    MD data={32, 10000, 30000000}; //이렇게 대입이 가능하다.
    
    MD *p;
    p=&data;
    (*p).age=31;   //이렇게 써도 되고 (괄호를 쓴 이유는 *가 .보다 연산자 우선순위가 낮다)
    p->31;         //이렇게 써도 된다.
    
}

 

 

●연결 리스트

 

배열은 사용자가 몇 개의 데이터를 입력할지 모르기 때문에, 최대 크기로 배열 크기를 잡아야 한다.

 

포인터 경우는 아래와 같이 사용자에게 물어봐서 할당받는다.

 

배열은 대충 예측이 가는데 포인터일 경우가 예상이 안 가기 때문에 코드를 보겠다.

 

#include<stdio.h>
#inlcude<malloc.h>

int main()
{
   int *p_num;
   int cnt=0;
   printf("몇개를 입력하겠습니까?");
   scanf_s("%d",&cnt);
   if(cnt>0){
      p_num=(int*)malloc(sizeof(int)*cnt);
      if(p_num!=NULL)
      {
         free(p_num);
      }
   }
}

이것이 포인터를 사용하여 사용자에게 물어보고 실행하는 프로그램이다. 그러나 인터페이스도 그렇고 사용자가 불편해할 것 같다. 그래서 우리는 사용자에게 물어보지 않고 데이터를 추가하는 메모리를 만들기 위한 프로그램을 만들어야 한다.

그것이 "연결 리스트"이다.

 

그럼 연결 리스트를 만들어보도록 하겠다.

 

typedef struct listnode{
   int num;
   struct listnode*next;   //다음 주소값을 가르키는 next
}node

이 구조체는 int의 4byte *의 4byte 총 8byte의 구조체이다. 이 메모리는 next와 num으로 나누어져서 2개의 블록으로 만들어진다.

코드의 그림

그러면 이 블록을 가리키는 head라는 포인터 변수가 있다면 코드와 그림이 어떻게 될까? 

#include<stdio.h>
#include<malloc.h>

typedef struct listnode{
   int num;
   struct listnode*next;
}node;

int main()
{
   node*head=NULL; //head의 선언
   head=(node*)malloc(sizeof(node));  //head에 구조체 크기만큼 동적할당
}

 위 코드의 그림

이제 이 동적 할당한 메모리에 데이터를 집어넣겠다. 마찬가지로 코드와 그림을 같이 보자.

#include<stdio.h>
#include<malloc.h>

typedef struct listnode{
   int num;
   struct listnode*next;
}node;

int main()
{
   int num;      //num 선언;
   scnaf("%d",&num); //num의 값 입력
   node*head=NULL; 
   head=(node*)malloc(sizeof(node)); 
   head->num=num; //값 대입
   head->next=NULL;  //맨 뒤의 값은 null
}

만약 num값에 5를 입력했다고 가정하면 위의 코드의 그림은 이렇게 넘어간다.

 

다음은 이 상태에서 추가로 데이터를 입력하는 과정이다.

#include<stdio.h>
#include<malloc.h>

typedef struct listnode{
   int num;
   struct listnode*next;
}node;

int main()
{
   int num;      
   scnaf("%d",&num); 
   node*head=NULL; 
   head=(node*)malloc(sizeof(node)); 
   head->num=num;
   head->next=NULL;
   
   scnaf("%d",&num);  //값대입
   head->next=(node*)malloc(sizeof(node));  //head의 next에 새로운 노드 생성
   
   head->next->num=num;  //새롭게 동적할당한 부분에 num대입
   head->next->next=NULL; //next next에 null대입

}

기존 코드에 값을 대입하고 head의 next에 동적 할당을 하여 새로운 node가 생성되었다.

그 후 head->next->num=num을 대입하고 head->next->next에서는 NULL을 대입해서 프로그램의 오류가 없도록 한다.

 

 

PS)

head->next->next는 헷갈릴까 봐 구분동작으로 적겠다.

head: 100
head->next: num=5가 있는 노드
head->next->next: num=4가 있는 노드  
head->next->next=NULL: num=4가 있는 노드에 NULL대입 

 

 

그런데 우리는 연결 리스트를 만들 때 계속 next를 적어줘야 하는 불편함을 겪어야 하는가? 

 

그렇지 않다. 코드를 보면 반복되는 부분이 보이기 시작한다. 이제 이 코드를 반복문 안에 넣어서

 

코드를 간소화해보자. 

 

먼저 구분을 해야 한다. 첫 번째에 데이터가 대입되는 상황과 그 후에 데이터가 들어오는 상황을 구분하여 코드를 만들어보자

 

 

1: 먼저 데이터가 처음 대입되는 상황이다.

#include<stdio.h>
#include<malloc.h>

typedef struct listnode{
   int num;
   struct listnode*next;
}node;

int main()
{
   int num=0;      
   node*head=NULL; 
   printf("입력을 종료하고 싶다면 999를 입력하세요\n");
   
   while(1)
   {
      printf("값을 입력하세요: ");
      scnaf("%d",&num); 
      
      if(num!=999)
      {
         if(head==NULL)  //head에 데이터가 들어오지 않았다는 뜻이다.
         {
             head=(node*)malloc(sizeof(node));
             head->num=num;
             head->next=NULL;          
         }
      }
      else
      {
         break;
      }
   } 
}

 

2: head가 NULL이 아닐 때 즉 이미 head에 데이터가 들어와 있어서 그다음에 입력하는 코드를 작성해보자.

#include<stdio.h>
#include<malloc.h>

typedef struct listnode{
   int num;
   struct listnode*next;
}node;

int main()
{
   int num=0;      
   node*head=NULL, *p;  //*p 추가선언 
   printf("입력을 종료하고 싶다면 999를 입력하세요\n");
   
   while(1)
   {
      printf("값을 입력하세요: ");
      scnaf("%d",&num); 
      
      if(num!=999)
      {
         if(head==NULL)  //head에 데이터가 들어오지 않았다는 뜻이다.
         {
             head=(node*)malloc(sizeof(node));
             head->num=num;
             head->next=NULL;          
         }
         else
         {
            p=head; //head를 움직이면 안돼서 head가 가르키는 값을 p에 대입
            while(p->next!=NULL)  //p의 next가 NULL이 아닐 때까지 반복
            {     //p->next 이렇게 적어도 상관은없다.
               p=p->next;  //계속 한칸씩 넘어가는 코드
            }
            p->next=(node*)malloc(sizeof(node));
            p->next->num=num;
            p->next->next=NULL;  //이제 p->next가 끝노드니까 그의 next는 NULL 
         }
      }
      else
      {
         
         break;
      }
   } 
}

위의 코드를 간단하게 그려보면 아래와 같이 나온다.

p=head; //head를 움직이면 안돼서 head가 가르키는 값을 p에 대입
while(p->next!=NULL)  //p의 next가 NULL이 아닐 때까지 반복
{     //p->next 이렇게 적어도 상관은없다.
   p=p->next;  //계속 한칸씩 넘어가는 코드
}
p->next=(node*)malloc(sizeof(node));
p->next->num=num;
p->next->next=NULL;  //이제 p->next가 끝노드니까 그의 next는 NULL 

 

1: head가 가리키는 주소를 p에 같이 대입을 시켜준다.

2: while문으로 p의 next가 NULL이 아닐 때까지 반복

3: 끝 노드를 찾으면 p의 next에 동적 할당 후 num 대입

4: 끝 노드(p의 next, next)에 NULL대입

 

 

그러나 계속 데이터를 삽일할 때 while문을 계속 도는 것은 비효율적이다. 그러면 어떻게 해야 할까?

 

첫 노드를 가리키는 head가 있다면 끝 노드를 가르키는 tail을 만들면 끝 노드를 찾을 때 반복문을 돌릴 필요가 없다.

 

#include<stdio.h>
#include<malloc.h>

typedef struct listnode{
   int num;
   struct listnode*next;
}node;

int main()
{
   int num=0;      
   node*head=NULL, *tail;  //*tail 추가선언 
   printf("입력을 종료하고 싶다면 999를 입력하세요\n");
   
   while(1)
   {
      printf("값을 입력하세요: ");
      scnaf("%d",&num); 
      
      if(num!=999)
      {
         if(head==NULL)  
         {
             tail=head=(node*)malloc(sizeof(node));  //tail,head에 같은 주소 대입        
         }
         else
         {
            tail->next=(node*)malloc(sizeof(node)); 
            tail=tail->next   // tail의 위치 업데이트
         }
         tail->num=num;
         tail->next=NULL;   
      }
      else
      {
         
         break;
      }
   } 
   
   while(head)
   {
      tail=head; //head의 값 tail에 대입  tail, head :1 ->2 
      head=head->next;  //한칸씩 옮김     head: 2-> 3 
      free(tail);    //tail로 지움       tail: 1 >2
   }
   tail=NULL;  //마지막 청소 그래야 안전 근데 지금상황에서는 굳이 넣을 필요 없음
   
}

아까보다 훨씬 간단해졌다. else문에 tail을 사용해서 head가 NULL이 아니면 tail->next에 동적 할당을 하고 

tail의 주소를 next로 옮긴다. 그리고 if~else문 안에 있던 공통부분은 밖으로 빼서 중복 코드를 없앤다. if안의 head의 값은 tail과 같기 때문에 가능하다.

 

728x90
반응형
Comments