C 포인터의 이해와 활용

sham·2021년 7월 26일
0

1. 개요

포인터의 크기와 데이터 타입

포인터에 NULL이 할당되면 해당 포인터는 아무것도 가리키지 않음을 의미한다. 0이 할당되는 것과는 다르다.

NULL 매크로는 상수 정수 0을 void 포인터로 캐스팅한 것이다.

#define NULL ((void *)0)

NULL 포인터와 초기화 되지 않은 포인터는 명백히 다르다.

NULL을 포인터가 아닌 곳에서 사용하는 것은 목적에 맞지 않다.

정적(static)/전역(global)변수는 힙 영역(동적 메모리 할당)에 저장된다.

sizeof로 포인터의 크기를 알 수 있다.

일반적인 포인터 사용

const int *p : 상수 포인터 - 가리키는 주소를 변경할 수는 있으나 포인터를 통해 변경하는 것은 불가능.

int *const p : 포인터 상수 - 가리키는 주소를 변경할 수는 없지만 포인터를 통해 참조값을 수정하는 것은 가능.

2. 동적 메모리 관리

동적 메모리 할당

malloc을 통해 할당하고 free로 해제해주는 것이 일반적.

동적할당으로 지정된 메모리보다 더 많은 메모리를 사용하려들경우 추가 메모리를 손상하게 된다.

메모리 누수

할당된 메모리가 해제되지 않을 때 발생한다.

  • 메모리의 주소를 잃어버리는 경우
  • free 해주지 않은 경우

누수가 반복적으로 발생하면 사용 가능한 메모리가 부족해져 OOM(Out Of Memory)문제로 발생할 수 있다.

  • malloc - 힙에 메모리 할당
  • realloc - 기존 할당된 메모리 크기 변경 - 메모리가 남으면 힙으로 반환, 모자르면 가능한 인접 영역에 이어서 할당, 인자로 들어온 크기가 0이면 메모리 할당 해제 / 좋은 방식은 아님, 지양
  • calloc - 힙에 메모리 할당 및 크기 0으로
  • free - 할당된 메모리 힙으로 반환 - free를 해줘도 메모리에 접근할 수 있음. (댕글릿 포인터) 해제된 포인터에 NULL 할당할 것.

댕글링 포인터

포인터가 여전히 해제된 메모리 영역을 가리키고 있는 것을 말한다.

해당 포인터가 가리키는 메모리는 더는 유효하지 않다.

아래와 같은 문제가 발생한다.

  • 메모리 접근 시 예측 불가능한 동작
  • 메모리 접근 불가 시 세그멘테이션 오류
  • 잠재적인 보안 위험

다음과 같은 동작의 결과로 발생한다.

  • 메모리 해제 후, 해제된 메모리에 접근
  • 함수 호출에서 자동 변수를 가리키는 포인터의 반환

3. 포인터와 함수

프로그램 스택

프로그램 스택은 함수의 실행을 지원하기 위한 메모리 영역이며, 일반적으로 프로그램 스택과 힙은 같은 메모리 영역을 공유한다.

프로그램 스택은 스택 프레임을 포함하고 있으며 스택 프레임은 활성화 레코드 또는 활성화 프레임으로 불리기도 한다.

스택 프레임은 함수 호출 시 전달되는 매개변수와 로컬 변수를 포함한다.

함수가 호출되면 함수의 스택 프레임이 스택에 추가(push)되며, 입력된 스택은 맨 위쪽에 쌓인다.

함수가 종료될 때 맨 위의 스택 프레임이 프로그램 스택에서 제거(pop)된다.

스택 프레임의 구성

  • 반환 주소
  • 로컬 변수 저장소
  • 매개변수 저장소
  • 스택 포인터와 기반 포인터

스택 프레임이 생성되면 매개변수들이 선언과 반대 순서로 프레임에 추가되며 → 함수 호출의 반환 주소가 입력되고 → 로컬 변수들이 추가된다.

int num_sum(int *arr, int len)
{
	int sum;
	
	sum = 0;
	for (int i = 0; i < len; i++)
		sum += arr[i];
	return (sum);
}

int main(void)
{
	int arr[] = {1, 2, 3};
		num_sum(arr, 3);
}

로컬 변수 sum

반환 주소

매개변수 arr

매개변수 len


main

포인터에 의한 전달과 반환

포인터를 함수로 전달하면 해당 개체를 전역으로 만들지 않고도 참조 및 접근이 가능해진다.

포인터를 포함한 매개변수들은 값(인자의 복사본)으로 전달되기에 큰 데이터를 전달할 때 효과적이다.

포인터의 포인터로 전달하면 리턴하지 않아도 해당 이중 포인터의 역참조가 인자로 준 포인터의 주소를 가리키게 된다.

데이터의 수정이 필요하지 않은 경우라면 상수 포인터의 전달이 좋은 방법이다.

  • const int* num

로컬 데이터 포인터

동적할당 하지 않고 로컬 배열을 만들고 리턴할 경우 함수가 반환되는 즉시 함수의 스택 프레임이 스택에서 제거되기 때문에 더 이상 유효하지 않게 된다.

함수 포인터

[반환 값(void)] (*[함수 포인터의 변수 이름])([매개변수]);

프로그램이 잠재적으로 느리게 동작한다.

파이프라이닝과 분기 예측을 함께 사용하지 못 할 수도 있다.

함수 포인터를 사용하려면 함수의 주소를 함수 포인터에 사용해야 한다.

  • 함수의 이름은 함수의 주소로 사용된다.

함수 포인터 반환? 함수 포인터 배열?

4. 포인터와 배열

배열의 이름 자체는 배열의 주소를 반환한다. 포인터에 할당하는 것이 가능하다.

배열의 요소(인덱스) 앞에 &를 붙여 구체적인 주소를 반환할 수도 있다.

int arr[] = {1, 2, 3, 4, 5};

int *p;

*p = arr;

*p = &arr[0];

arr[i] == *(arr + i)

  • 포인터 연산은 해당 포인터가 가리키는 데이터 타입의 곱만큼 주소를 증가시킨다.
  • 포인터에 이차원 배열을 동적할당할 경우 연속적으로 메모리에 할당되어 있을 거라고 보장할 수 없음.
    • 바깥쪽(이중 포인터)를 동적할당한 후 안쪽(포인터)를 각각 동적할당
    • 일차원 배열에 모든 메모리를 할당

5. 포인터와 구조체

구조체에 메모리가 할당될 때, 구조체가 가진 멤버 크기의 합보다 더 많은 크기가 할당될 수 있다. 이는 패딩이 발생하기 때문인데, 데이터 타입을 특정한 범위 안에 정렬하기 위해 이용된다.

int, int, int, short 멤버를 가지고 있다면 마지막 short에 2바이트 패딩이 추가된다.

구조체에 메모리가 할당될 떄, 런타임 시스템은 구조체 내부에 선언된 포인터에 자동으로 메모리를 할당하지 않는다. 같은 이치로, 구조체가 삭제될 때도 구조체 내부의 포인터에 할당된 메모리를 자동으로 해제하지 않는다.

profile
씨앗 개발자

0개의 댓글