4 분 소요

포인터

‘메모리 주소 값’을 저장하는 변수

대표적인 예시로

‘int’ ‘*’ a = (int *)malloc(n * sizeof(int));;
위 내용은 ‘int’ 타입을 가리키는 ‘포인터 변수’ a를 선언한 것이다
(뒤의 malloc 과 캐스팅 은 지금은 무시하자)

요점은 ‘주소값’을 표현하는 ‘타입’이라는 것

(<타입> * 으로 해당 타입의 주소값을 가리킨다 인식해도 된다)

  • 왜 <타입> 이 필요하지??
    => 그래야 '얼마만큼'의 크기를 '어떠한 타입'으로 가리킬 지
    알 수 있으니까!!

주소 연산자 &

Ampersand 라 불리는 주소 연산자 & 이다
(펭귄이라 부를수도 있지만)

  • 비트 연산자 & 와 착각하지 말 것!
    피연산자를 2개 쓰면 비트 연산자로 쓰여지므로 주의
    (and 연산을 하면 두 피연산자의 ‘비트 패턴’을 비교하고 양쪽다
    1이 있는 새로운 비트 패턴을 가진 값으로 반환한다)
  • 피연산자의 ‘주소값’을 보여준다
    (16진수로 보여준다)

위의 예시와 합쳐
int* a = &num ;
으로 사용이 가능하다!
(int 타입 포인터 변수 a에 num의 주소값을 대입)

역참조 연산자 *

‘포인터가 가리키는 주소’의 값을 가져오는 연산자

위의 예시인
int* a = &num ;
로 보자면
a 포인터 변수에 저장된 int형의 num 값을 보고 싶다면
print(“%d”,*a) 로 볼 수 있다

  • 참조와 역참조
    • 참조
      포인터가 하는 일
      ‘변수’의 값을 직접 가져다 사용하는 것이 아니라
      ‘어느 위치’에 존재한다고 가리킴
    • 역참조
      주소로 직접 가서 거기 저장되어 있는 값에 접근

‘원본’의 값을 바꾸는 방식이기도 하다
int score = 100;
int* p = &score;
*p = 50;
=> 이러면 score의 값이 50으로 바뀐다

값에 의한 전달 vs 참조에 의한 전달

포인터를 함수 매개변수를 통해 전달하여
함수 내부에서 사용이 가능하다

void func(int* _a)
{
print(“%d\n”,*_a);
*_a = 20;
}

메모리 주소를 ‘복사’하여 전달하였으니 ‘값에 의한 전달’??
하지만 ‘원본’이 바뀌니 ‘참조에 의한 전달’??

=> 실제 내부로는 ‘메모리 주소’를 전달하므로 ‘값에 의한 전달’이 맞지만
‘원본’이 바뀐다는 점에서 ‘참조에 의한 전달’ 이라는 것이 올바른 의도 전달 방식

포인터를 함수 반환값으로 사용하면??

포인터도 함수 값으로 반환이 가능!!
int* do_something(int a, int b);

다만 유의할 점이 존재한다
‘함수의 지역변수’를 반환하지 말 것!

int* add(int a, int b){
int result = a + b;
return &result;
}

위 코드는 지역변수 ‘result’를 반환하고
이는 함수가 종료할 때,
해당 변수 스코프를 벗어나기에
결과적으로 반환되는 포인터는
‘유효하지 않은 주소’를 가리키게 된다

그렇기에 함수에서 ‘주소’를 반환하는 경우는

  • 전역변수
  • static 전역 변수
  • 함수 내 static 변수
  • 힙 메모리 생성 데이터를 가리킴
    등의 경우가 보통

댕글링 포인터 (dangling pointer)

위 같은 예시의 포인터처럼
‘유효하지 못한 주소’를 가리키는 포인터를
‘댕글링 포인터’라고 부른다

이러한 댕글링 포인터를 사용하면
결과를 예상할 수 없다
(정상적으로 값이 출력될지, 터질지…)

NULL 포인터

‘반환할 주소’가 없는 경우,
혹은 ‘포인터’ 변수를 초기화하는 경우
‘아무것도 가리키지 않는다’는 의미로 아래와 같이 사용한다

int* a = NULL;

(여기서 NULL은 상수 0 혹은 (void*)0 으로 표현된다)

NULL은 보통 3가지 방식으로 사용한다

  1. 포인터 변수 초기화
  2. 포인터 주소가 ‘유효’한가 확인하기 (if a != NULL)
  3. 댕글링 포인터를 막기 위하여 (free()이후 유효하지 못한 주소를 가리키는 경우를 막음)

포인터와 배열

배열 변수는 ‘포인터’와 아주 유사하다

int nums[3] = {0,1,2};
int* ptr = NULL;

ptr = nums;
ptr = &nums[0]; // 위와 같음

이 때, ptr은 nums의 첫번째 요소인
0을 가리킨다

  • 배열은 뭉쳐있는 데이터의 집합이기에
    주소와 자료형의 크기만 알면
    포인터로 직접 배열의 요소들을 건드릴 수 있음

    ex) ptr++;
    (nums의 0번째 요소에서 1번째요소를 가리키도록 함)
    (ptr += 1 과도 같은데,
    포인터의 경우, + - 와 관련된 정수 연산은
    내부에서 ‘포인터가 가리키는 타입의 크기’로 변환되어 작동한다)

    => 그렇기에 아래의 두 포인터는 둘다 nums의 3번째 요소를 가리킨다는 의미이다
    int* p1 = nums + 2;
    int* p2 = &nums[2];

    또한 이런것도 가능하다
    int* p1 = nums;
    int nums2 = p1[2];
    (포인터에서 [] 배열 첨자 연산자를 사용 가능하다)

물론 배열과 포인터의 차이점 역시 존재한다

  • sizeof() 연산자의 반환값
    포인터는 포인터의 크기를 반환하지만,
    배열은 ‘총 배열의 크기’를 반환한다
  • 문자열 초기화
    char d1[] = “friday”;
    char* d2 = “friday”;

    d1은 ‘영역’에 “friday”가 저장되어 문자열을 수정해도 괜찮지만
    d2의 경우는 ‘데이터 영역’에서 가져온 것이기에,
    수정할 수 없거나(컴파일 오류),
    정의되지 않은 결과를 일으킬 수 있음

  • 대입
    포인터의 경우, 새로운 ‘주소값’을 대입할 수 있지만,
    배열의 경우는, 할 수 없음

  • 포인터 산술 연산
    포인터는 ++,– 등의 연산이 가능하지만
    배열은 불가능

const 포인터

일반적으로 ‘수정’을 막으려면 const 키워드를 사용

포인터의 경우도, const 키워드를 이용할 수 있다!
그것도 무려 3가지 방식으로!!!

  • 주소를 보호하는 const 포인터
    <타입>* const 변수
    : 오른쪽에서부터 읽는다고 생각하자....
    변수는 const 포인터인데 그게 '타입'을 가리킨다
    int* const p = &num1;
    p = &num2; // 컴파일 오류
    p++;       // 컴파일 오류
  • 값을 보호하는 const를 가리키는 포인터
    const <타입>* 변수
    int* 가 가리키는 것이 const
    const int* p = &num ;
    *p = 0; // 컴파일 오류
    
  • 둘 다 지키는 const * const
    const <타입>* const 변수

함수 포인터

함수를 호출하는 것도 결국 ‘메모리 주소’로 하는 것
따라서 ‘함수의 주솟값’을 받아 호출시키는 것도 가능함

예시를 먼저 보자

  double add(double x, double y){
    return x+y;
  }

  // 함수 포인터 변수 선언
  double (* func)(double,double) = add;

  // 함수 포인터를 '매개변수'로 받을 때의 사용
  // 함수 선언
  double calc(double,double, double (*)(double,double));

  // 함수 매개변수
  double calc(double x, double y, double(* func)(double,double))
  {
    return func(x,y);
  }

함수 포인터를 읽을때 한가지 팁이 있다면,
‘오른쪽 왼쪽 규칙’을 통해 읽는 것이다

func 변수명 기준으로 ‘괄호’에 부딪힐 때마다,
‘읽는 방향’을 바꾸는 방식

func 는 포인터이며, (*)
func 는 두 개의 double형 매개 변수를 받는 함수 포인터이며,((double,double))
func 는 두 개의 double형 매개 변수를 받아 double을 반환하는 함수 포인터이다(double)

이중 포인터, 다중 포인터

포인터가 뭐라고?
‘주소를 저장하는 변수’

그렇다면 ‘포인터의 포인터’는 무엇일까??
‘변수의 주소를 저장한 변수의 주소를 저장한 변수’
(동어 반복이 아니다)

아래의 예시는 ‘이중 포인터’이다

  int num1 = 10;
  int num2 = 20;
  int* p1 = &num1;
  int* p2 = &num2;
  int** pp2 = &p1;

  printf("%d\n",**pp2); // <- 10을 가리킴
  *pp2 = p2;            // pp2가 역참조한 값 (p1)이 p2와 같은 주소를 가리키도록 수정

  printf("%d\n",*p1);  // <- 20

당연하게도,
이중 포인터를 가리키는 포인터가 있을 수도 있으며
이는 ‘삼중 포인터’라고 한다
(보통은 삼중 포인터까지는 가끔 사용하고)
(사중 포인터 이상은 거의 안씀)

그런데 이런걸 왜 사용할까??
‘2차원’ 배열이 2중 포인터와 비슷하다
(아니면 1차원 포인터 배열이랑도 비슷)
더 많은 ‘연관된 값’을 ‘배열’을 통해 사용할 수 있음

double_pointer

[출처] : https://kasimov.korea.ac.kr/dokuwiki/lib/exe/detail.php/activity/public/2021/cpp/%ED%8F%AC%EC%9D%B8%ED%84%B06.png?id=activity%3Apublic%3A2021%3Acpp%3A210713

댓글남기기