C언어의 포인터와 메모리 관리 이슈

msung99·2022년 9월 24일
0
post-thumbnail

포인터

  • 주소를 가지고 있는 변수

  • 일반 변수앞에다 & 를 붙이면 주소가 리턴됨 (reference operator)

int*  pointerA = &varB;  // 포인터 변수 A에 변수 B의 주소를 할당하고 싶다면 
                     // 변수 B의 주소값을 주기 위해 & 를 붙일것 
  • 포인터 변수 앞에다 별표 * 를 붙이면 포인터 변수가 가리키는 변수에 저장된 값이 리턴됨 (value of operator)
int varA = *pointerB; // 포인터 변수 B가 가리키는 변수에 저장된 값을 리턴

예시

malloc() 으로 동적으로 할당한 4바이트 짜리 정수형 메모리 주소를 포인터에 할당함

서식문자 %p : 포인터의 주소값 출력

cf) malloc() 의 값들은 초기화가 안되서, 우리가 별도의 값을 할당하지 않는 이상 쓰레기값이 저장되어있다.


포인터 사용시 장점

  • 힙 메모리 공간을 제한없이 사용 가능하다

    • 힙 공간의 사이즈가 스택 공간의 사이즈보다 훨씬 큼
  • 동적으로 할당한 메모리 공간에 대해 언제든지 사이즈를 재조정(resize) 가능하다!

  • 함수 호출시 파라미터로 포인터를 넘겨주면, 같은 메모리 공간을 공유할 수 있다 ( => 함수끼리 동일한 메모리 공간을 공유하는 방법은 포인터를 사용해야 한다!)
    (함수안의 일반 로컬변수는 함수가 리턴(종료) 되면 stack 에 쌓였던 변수가 사라지지만, 포인터 활용시 malloc() 으로 동적인 메모리 공간을 생성하고 heap 에 존재하게 된 메모리의 값을 변경가능하다!. heap 에 있는 메모리 공간은 stack 에 있는 메모리 공간과 달리 함수 호출이 종료되어도 사라지지도 않음)


포인터 배열

  • 포인터는 배열의 특정 원소의 주소값을 저장 및 가리킬 수 있다.
int arr[4];
int *pointer = &arr[1];
  • 포인터를 배열처럼 사용 가능하다.
int *pointer = (int *)malloc(sizeof(int) * 2);
pointer[0] = 1;
pointer[1] = 2;
printf("%d \n", pointer[1]);
  • 포인터가 가지고 있는 주소값에 대해 산술 연산이 가능하다.
    • 주의!) 포인터 주소에다 숫자를 더하면 진짜로 그 숫자가 주소에 그대로 더해지는 것이 아닌, 해당 포인터가 가리키는 메모리 공간의 타입(int, char 등) 의 size 만큼 더해진다! (int형 메모리 공간일 경우 4바이트)
int *pointer = (int *)malloc(sizeof(int) *3);
pointer[0] = 0;
pointer[1] = 1;
pointer[2] = 2;

pointer = pointer + 2;  // 현재 포인터가 가리키는 동적 메모리공간의 시작주소에서 
   // (int형 4바이트) * 2 = 8바이트만큼 이동해서 그 주소를 포인터가 새롭게 가기키게 함

printf("%d \n", *pointer); 

포인터 배열 vs 배열 포인터

  • 포인터 배열 : 포인터로 구성된(타입이 포인터인) 배열
  • 배열 포인터 : 배열을 가리키는 (타입이 배열인) 포인터

  • int (*atpr)[4] : 포인터 배열 => 크기가 4인 int형 배열의 각 원소를 atpr의 각각 가리킴

  • ptr : 배열 포인터

  • aptr = &array : 배열 포인터(aptr)에다 배열의 주소(&array) 를 줘도 에러가 발생하지 않는다.

  • ptr = array : 그냥 포인터(ptr)에다 배열의 시작주소(array) 를 할당

cf) 만일 aptr = array 와 같은 연산이 일어나면 에러가 발생함
=> 배열 포인터(atpr)가 배열 타입의 메모리 공간을 가리켜야 하는데 배열의 시작주소, 즉 int형 타입의 메모리 공간을 가리키게 되니까 타입 에러가 발생!


이중 포인터(Double pointer)

  • 이중 포인터는 포인터 변수의 메모리 주소를 저장

예제1

  • 이중 포인터한테 포인터의 주소(&pointer) 를 넘겨줌

예제2

  • "int형 포인터" 타입의 동적 메모리 공간을 생성하고 이중 포인터에 주소값을 할당
int **dptr = (int **)malloc(sizeof(int *) * 2); // int형 포인터 2개가 생성(dptr[0], dptr[1])
  • dptr[0] = (int )malloc(sizeof(int) 4);
    => dptr 의 첫번째 원소인 int형 포인터(dptr[0]) 이 malloc() 공간을 가리키게 함
    => 이렇게하면 2차원 배열처럼 사용할 수 있다!
dptr[0][0] = 0; // 2차원 배열 처럼 사용하는 중!
dptr[0][1] = 1;
dptr[0][2] = 2;
dptr[0][3] = 3;

함수형 포인터(function pointer)

  • 함수의 주소를 저장하는 포인터

    • 데이터 타입 : 함수의 리턴타입
    • input 인자들 : 함수에 넣고싶은 값들
  • 함수의 이름의 곧 해당 함수의 주소값이다.

예제

  • 함수형 포인터 fptr : 데이터타입은 void, 인자타입은 int형 포인터
  • fptr = func; => 함수형 포인터가 func 함수의 주소값을 저장. 이떄 인자값으로 int형 포인터 변수인 mem 을 할당

C언어에서 자주 실수하는 메모리관련 버그

1. dereferencing(해당 주소에서 값을 자져올때)

scanf() 버그

  • scanf() 에 할당되어야 하는 인자가 주소값이여함. 그래야 해당 주소에 값을 넣어줄 수 있다.

  • scanf 함수는 인자값이 포인터인지 변수인지 신경쓰지 않고 주소라고 여긴다.

int var = 3;
scanf("%d", var); // var에 저장된 숫자 3을 주소라고 인식하고 동작 하려고 한다. 그럼 당연히 에러발생!
// => scanf("%d", &var); 이게 옳은 표현!

2. malloc() 초기화 문제

  • malloc() 은 안에 초기값으로 쓰레기 값을 저장하고 있다.
  • 새로운 메모리를 할당하면, 사용하기 전에 초기화를 했는지 안했는지 잘 체크하고 사용하자!

아래처럼 malloc() 에 초기화 과정을 별도로 안해줬는지 값을 참조하려고 하면 각 메모리 공간에 완전 이상한 값이 할당된다!


3. 스택 오버플로우(stack overflow) 발생

예시

  • gets() : 키보드에 입력되는 문자를 하나하나 읽어들임

=> 만일 크키가 64인 char형 배열에 64개 이상의 문자들을 gets() 로 할당시킬 경우, 스택 오버플로우가 발생! (invalid 한 메모리 공간까지 사용하려 하므로)


4. 메모리 타입 문제

  • A[i] = (int )malloc(sizeof(int) m); => 여기서 에러가 발생할 가능성이 있다.

이중 포인터 A의 각 원소(int형 포인터)들은 타입이 int 가 아닌, int형 포인터이다!

그런데 A의 각 원소들이 가리키고 있는 malloc() 메모리 공간을 보면 sizeof(int) 를 보듯이 타입이 int형 타입이다!

=> 그래서 래는 int형 포인터 <-> int형 간에 타입이 알맞지 않아서 에러가 발생해야 정상인데, 운좋게도 int형 포인터도 4바이트이고 int형도 4바이트이다. 따라서 다행히도 코드가 실행되는데 문제없이는 돌아간다.

but, 일부 시스템(i7 코어)에서는 int형 포인터가 8바이트라서, 에러가 발생한다!


5. 연산자 우선순위를 잘 고려하지 못한경우

  • *size-- : 여기서 동작할 수 있는 방법은 생각해보면 2가지 경우기 나올것이다.

case1) size 포인터가 가리키는 메모리 공간에 저장된 값이 1감소

  • ex. int val = 3; int* size = &val; 인 경우 변수의 val의 값이 3에서 2가 되는 경우
    case2) size 포인터 주소값이 4바이트만큼 감소

  • ex. size 포인터 변수의 주소값이 10000 이였다면, 9996이 되는 경우

    => 연산자 "--" 가 "*" 보다 우선순위가 높아서, case2 가 실행될 것이다.

이렇게 애매모호한 상황을 방지하기 위해, 괄호 "( )" 를 꼭 활용해서 연산의 우선순위를 잘 조정해주자!

cf) 연산자의 우선순위

  • 외울필요x


6. 포인터 연산

  • 포인터에다 어떤 정수값을 더하거나 뺴줄떄는 행위는 해당 포인터 타입 만큼 바이트가 주소값이 이동하는 것이다!

ex) p += 2; // p가 int형 포인터이라면, 주소값이 8바이트만큼 늘어남


7. 함수에서 선언한 변수의 주소값을 리턴하는 경우

위와 같이 함수에서 지역변수 val 을 선언하고 그 주소값을 리턴하면 문제가 발생한다.

함수가 종료시에 스택에 쌍였던 변수 val 이 소멸되는데, 이 소멸되는 변수의 주소값을 리턴하는 행위는 잘못된 행위이다.

=> 해결법 : int val; // val = (int )malloc(sizeof(int) * 1);
이렇게 해놓고 return &val; 을 하면 문제발생x
val가 가리키는 메모리 공간은 스택이 아니라 힙에 저장되어 있어서 소멸되지 않는다!


8. 메모리 누수(Memory Leaks)

위와 같이 메모리 공간 할당해놓고 사용하지도 않으면 쓸떄없는 메모리 공간 낭비가 된다.

cf) 최신 컴파일러는 똑똑해서 낭비되는 저런 메모리 공간을 자동으로 삭제시킴

0개의 댓글