관리 메뉴

History

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

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

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

luckybee 2021. 1. 31. 14:46
728x90
반응형

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

 

 

● 이차 포인터

 

먼저 상식적인 일차 포인터를 구현해보자

int main()
{
   int data;
   int *p;
   p=&data;
   *p=5;   //data=5;
}

이런 방법으로 포인터를 사용할 수 있다. 

 

그럼 일반 변수에다가 주소값을 대입하는 방법을 보자. 매우 간단하다.

int main()
{
   int data;
   int *p;
  
   
   int temp;
   temp=&data;  //가능하긴 하나 경고 발생
}

이렇게 temp라는 일반 변수도 주소값을 대입할 수 있다. 그러나 이럴경우 경고가 발생하는데 그 이유는

일반 변수인데 주소를 넣으려고 해서 그렇다. 

 

그렇다면 경고를 없앨 수 있는 방법이 뭐가 있을까? 그것은 형변환을 해주는 것이다.

 

int main()
{
   int data;
   int *p;

   int temp;
   temp=(int)&data; //int 형으로 형변환을 해준다. 
}

위의 코드처럼 int형으로 형변환을 해주면 temp에 주소값을 넣는 것이 단순히 저장개념으로 사용한다는 뜻을 가진다.

 

왜냐하면 temp는 일반 변수이기 때문에 *temp=5;라는 코드를 사용하지 못한다.

(일반 변수이기 때문에 번지 지정 포인터를 사용 못하기 때문) 

 

그러면 왜 이런짓을 하는걸까?

 

아래 코드를 보자.

 

int main()
{
   int data;
   int *p;

   int temp;
   temp=(int)&data; //int 형으로 형변환을 해준다. 
   
   p=temp;   //temp가 가지고 있는 값은 주소값이다. 그래서 이런 코드가 가능하다.


}

아래 코드를 보면 p=temp라는 코드가 있는데 이 코드는 가능하다. 왜냐하면 temp는 자신이 *연산을 하는 것이 아니라 단순 대입 개념으로 사용했기 때문에 가능한 것이다. 그러나 이러한 코드도 경고가 뜰 것이다. 그러면 어떻게 해야 경고가 뜨지 않고 코드를 짤 수 있을까? 

 

아래 코드를 보자.

 

int main()
{
   int data;
   int *p;
  
   int temp;
   temp=(int)&data;  
   
   p=(int *)temp;   
   
   *p=6; //data=6; 


}

그렇다 아까 했던 것처럼 경고가 안 뜨게하려면 포인터 형식으로 형변환 해주면 해결된다. p는 포인터 변수이기 때문에 (int*)으로 일반변수를 형변환 해준것이다.  

 

그래서 위처럼 temp를 대입해도 *p를 이용하면 data에 영향을 줄 수 있는 주소값을 얻을 수 있다.

 

이해가 잘 안 갈 수도 있으니 그림으로 보자.

 

 

결론: 주소 저장만이 목적이라면 굳이 포인터 변수를 사용하지 않더라도 일반 4byte 변수를 선언하여 저장이 가능

       but 직접사용은 불가능 또한 코드가 복잡해짐

 

코드 그림 설명

-data의 주소값은 100이라고 가정
-포인터 변수 p 선언
-일반 변수 temp 선언 

1: 형변환으로 인해 temp에 data의 주소값을 대입(상수 값으로 대입)
2: temp를 *temp로 참조할 수 없다. 왜냐하면 temp는 일반변수이기 때문에
3: 포인터 변수 p에 temp에 저장되어있는 data의 주소값 100을 형변환하여 대입
4: *p로 data의 주소값을 참조하여 값 변경

 

근데 데이터의 흐름이 아래 그림처럼 temp가 data를 참조할 수 있으면 더 효율적이지 않을까?

그럼 아래 그림처럼 하려면 어떻게 해야할까?

temp의 값을 *p로 옮기는 것이 아니라 temp의 주소값을 *p로 옮기면 어떻게 될 것같다. 한번 해보자

int main()
{
   int data;
   int *p;
  
   int temp;
   temp=(int)&data;  
   
   p=&temp;   
   
   *p=6; //temp=6; ????? 


}

내가 원하는대로 바꿔봤더니 여전히 temp에서 data를 참조하지 못한다. 때문에 temp의 값이 바뀐다.

이건 내가 원하는 상황이 아니다.

그럼 한 번 더 수정해보자.

int main()
{
   int data;
   int *p, *p_temp;
  
   int temp;
   temp=(int)&data;  
   
   p=&temp;   
   p_temp=(int*)*p;  //*p의 값은 temp의 주소값 즉 100이다. *p_temp는 포인터 변수이다
                     //*p를 해주는 순간 자료형이 int로 바뀌기 때문에 (int *)를 붙혀서
                     //형 변환을 해준다.
   *p_temp=6; //data=6; 
}

이번에는 *p_temp라는 포인터 변수를 하나 더 추가해봤다. 이렇게 하면 정상적으로 *p_temp를 이용해서 data의 값을 참조할 수 있다. 

 

이 코드의 흐름을 그림으로 알아보자.

 

-data의 주소값은 100이라고 가정 
-포인터 변수 p 선언
-포인터 변수 p_temp 선언 
-일반 변수 temp 선언  

1: 형변환으로 인해 temp에 data의 주소값을 대입(상수 값으로 대입) 

2: *p에 temp의 주소값을 대입 (temp의 주소값 110)

3: temp를 *temp로 참조할 수 없다. 왜냐하면 temp는 일반변수이기 때문에 

4: 포인터 변수 p가 가르키는 값을 p_temp에 대입(*p의 값은 data의 주소) 또한 *p_temp에 대입하는 것이기 때문에
*p라고 하더라도 정수 100이 되기 때문에 자료형이 맞지 않는다. 그래서 형변환(int*)이 필요

5: *p_temp로 data의 주소값을 참조하여 값 변경 

근데 뭔가 좀 이상하다. 내가 처음에 설계했던 그림이 아니다. *p_temp를 선언하지 않고 만들고 싶은데 어떻게 하면 좋을까?

 

그럼 이차포인터를 써보자.

int main()
{
   int data;
   int **p;
   int temp;
   temp=(int)&data;  
   
   p=(int**)&temp;   //&temp는 (int*)이다 그러나 p는 **이기 때문에 형변환을 해준다.
   **p=6; //data=6; 
   //==*(*p)=6;
}

코드는 이렇게 나온다. 코드만 봐서는 이해가 힘드니 그림과 같이보자.

-data의 주소값은 100이라고 가정  
-이차 포인터 변수 p 선언 
-일반 변수 temp 선언   

1: 형변환으로 인해 temp에 data의 주소값을 대입(상수 값으로 대입)  
2: **p에 temp의 주소값을 형변환 대입 (temp의 주소값 110)  
3: **p를 구분지어서 해석->*(*p)그러면 ()안의 *p를 먼저 해석-> *p==100
4: *(*p)에서 *p가 100이니까 *(100)-> 주소값 100은 data의 주소값 => 값이 6으로 바뀜

 

위에 코드랑 같은 코드가 아래 코드이다

int main()
{
   int data;
   int **p;
   //int temp;
   //temp=(int)&data;  
   //p=(int**)&temp;   
   
   int *p_temp;
   p_temp=&data;
   p=&p_temp;
   
   **p=6; //data=6; 
   
}

이렇게 하면 형변환을 사용할 필요가 없다. 왜냐하면 일반변수를 가르키는건 일차 포인터이다. 

그리고 일차 포인터를 가르키는건 이차 포인터이고 이차포인터를 가르키는건 3차 포인터이다.

이렇게 한단계씩 연관이 있다고 생각하면 된다.

 

+)주소는 4byte면 다 대입 가능 대신 형변환 해야함

 

●동적 메모리 할당

 

정적메모리(배열)는 []안에 상수만 허용된다. 변수가 들어가면 안된다.

아래는 예시 코드이다.

#include<stdio.h>

int main()
{
   short data[5];
   
   for(int i=0; i<5; i++)
   {
      scanf_s("%d",data+i);
   }
   
   for(int i=0; i<5; i++)
   {
      printf("%d ",data[i]);
   }
}

그럼 위의 코드를 포인터로 바꾸면 어떻게 될까?

 

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

int main()
{
   short *p_data;
   int cnt=5;
   
   p_data=(short*)malloc(sizeof(short)*cnt);  //10byte 메모리 할당
   
   if(p_data!=NULL)   //동적할당 성공 여부 체크
   {
      for(int i=0; i<cnt; i++)
      {
         scanf_s("%d",p_data+i);
      }
    
      for(int i=0; i<cnt; i++)
      {
         printf("%d ",*(p_data + i));
      }
   } 
   free(p_data);  //할당 해제
}

포인터 변수 자체는 stack에 존재(일반 변수 포함), 위의 코드에서는 stack 메모리에 8byte 존재

malloc 동적할당은 heap에 존재, 위의 코드에서는 10byte존재

 

동적할당을 하면 크기에 대한 유도리가 생기고 heap은 최대 2G를 사용할 수 있다. 대신에 stack에 비해 동작속도는 느려진다. 

 

단점으로는 메모리할당을 했을 때 너무 적은 용량만 사용한다면 손해이다.

 

이제 이차 배열로 실습을 해보자.

 

아래와 같은 배열이 있으면 값을 수정해야할 때 다시 빌드하고 코드를 전체 수정해야한다.

short data[3][4];  //정적 메모리

동적메모리로 바꾸면 어떻게 될까? 2차 포인터로 바꾸면 된다.

 

근데 난 실력이 딸린다. 

 

대처 방안을 알아보자.

 

아래 코드와 같이 한 개만 상수로 고정시켜서 만들어보자.

short data[상수][변수];

 

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

int main()
{
   short *p[5];
   int cnt=3;
   
   for(int i=0; i<5; i++)
   {
      p[i]=(short*)malloc(sizeof(short)*cnt);  //2*cnt byte 메모리 할당
      if(p[i]!=NULL)   //동적할당 성공 여부 체크
       {
            for(int j=0; j<cnt; j++)
            {
              *(p[i]+j)=0;
            }
       }
   }
   
    
   for(int i=0; i<cnt; i++)
   {
       if(p[i]!=NULL)   //동적할당 성공 여부 체크 
       {
           free(p[i]);  //할당 해제
       }
   }
    return 0;
}

 

포인터 배열 short*p[5]를 각각 cnt만큼 동적할당해준다. 이것을 그림으로 표현하면

 

*p[5]의 동적할당의 그림

또한   *(p[i]+j)=0; 코드를 이용하여 안에 있는 쓰레기 값들을 0으로 대입하는 코드가 있고

 

마지막에는 p[i]가 없어질 때까지 free(p[i])를 싹 다 해준다.

 

 

이제 아래 코드처럼 해보겠다 위와는 다른점이 상수로 고정시킨 부분이 뒤쪽이다. 

short data[변수][상수];
#include<stdio.h>
#include<malloc.h>

int main()
{
   short *(p)[3];
   int cnt=5;
   
   //이름을 뺀 나머지가 자신의 타입이 된다.short *(p)[3](형변환임);에서 p를 뺀 나머지
   //(short[3])가 자기 자료형이 된다.
   
   p=(short(*)[3])malloc(sizeof(short[3])*cnt); //6*cnt byte 할당
    
   if(p!=NULL)
   {
       for(int i=0; i<cnt; i++)
       {
          for(int j=0; j<3; j++)
          {
           (*(p+1))[j])=0;  //p[i][j]=0;과 동일
          }
       }
   }
    free(p);  //할당 해제    
    return 0;
}

 

아래 그림으로 살펴보자.

 

처음 동적할당을 시작할 때 short(*)[3] byte 크기만큼 해준다. 이 크기는 6byte이다. 왜냐하면 short[3]의 크기를 가르키기 때문이다. 그런 후 cnt만큼 곱해주면 p는 30byte의 크기만큼 동적할당이 된다.

 

그 후 p가 NULL이 아니면 반복문과  (*(p+1))[j])=0; 코드를 통해 0으로 초기화 시켜준다.

 

 (*(p+1))[j])=0;는 p가+0이면 0번째 칸이고 +1을 하면 6byte만큼 커진다.

 

그런 후 동적할당은 1번만 했으므로 free(p);를 해준다.

 

 

+)

int data 의 자료형은 int

 

char a[5]의 자료형은 char[5]

 

결론: 자료형은 이름을 뺀 나머지가 자료형임

 

이 코드들은 전부 10번정도 타이핑 해야한다. 최소..

 

 

이제 2차 포인터를 이용한 2차 배열을 구현해보자.

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

int main()
{
   short **p;
   int g_cnt=5,s_cnt=3;
   
   pp=(short**)malloc(sizeof(short*)*g_cnt); 
   
   if(pp==NULL)
     return -1;
   
   for(int i=0; i<g_cnt; i++)
   {
     *(pp+i)=(short*)malloc(sizeof(short)*s_cnt);
     
     if(*(pp+i)!=NULL)
     {
        for(int s_i=0; s_i<s_cnt; s_i++)
        {
          *(*(pp+i)+s_i)=0;
        }
     }
   }
   
   for(int i=0; i<g_cnt; i++)
   {
      if(*(pp+i)!=NULL)
        free(*(pp+i));
   }
   free(pp);  //할당 해제    
   return 0;
}

매우 복잡한 코드가 나왔다.

 

그림으로 보자.

이차 포인터 동적할당 과정

1: **pp는 이차 포인터 변수 즉 지역변수이기 때문에 stack 메모리에 존재한다.

2: short형은 2byte지만, short*는 포인터 형이기 때문에 4byte 그래서 동적할당하면 *g_cnt를 통해 20byte 생성
3: 3번과 같은 표가 만들어지고 시작번지가 100번지라고 했을 때 **pp는 100번지라는 주소를 가지게 된다.
4: *(pp+i)를 반복문으로 3번에서 만든 표(포인터 아님 그냥 개당 4바이트 배열임)로 각각 접근할 수 있게 만든다.
5: 동적할당을 통해 6byte의 메모리를 구현
6: 한칸당 2byte의 메모리가 생성 총 6byte
7: 각각의 pp+i가 6번의 메모리를 가짐
8: : *(*(pp+i)+s_i)=0;을 통해 새로 만들어진 메모리에 0 대입


for(int i=0; i<g_cnt; i++) 
{ 
    if(*(pp+i)!=NULL)

     free(*(pp+i)); 
}

free(pp);



마지막 코드에서 이렇게 쓴 이유는 먼저 free(pp)를 해버리면 1번에 있는 stack영역의 포인터 변수가 
사라지기 때문에 반복문으로 동적할당했던 메모리들은 쓰레기들이 되어 메모리를 잡아먹는다. 
한마디로 엄마 없는 고아이다.



그렇기 때문에 반복문으로 파생된 메모리들을 먼저 free해준 후 마지막에 pp를 free해준다.

그러나 이렇게 사용하면 메모리가 훨씬 많이 들어간다. 총 배열을 구현하는데만 54byte를 사용한다. 그래서 적은 용량을 사용하게 되면 배보다 배꼽이 더 커질 수 있다.

728x90
반응형
Comments