포인터
포인터
‘메모리 주소 값’을 저장하는 변수
대표적인 예시로
‘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가지 방식으로 사용한다
- 포인터 변수 초기화
- 포인터 주소가 ‘유효’한가 확인하기 (if a != NULL)
- 댕글링 포인터를 막기 위하여 (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 <타입>* 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차원 포인터 배열이랑도 비슷)
더 많은 ‘연관된 값’을 ‘배열’을 통해 사용할 수 있음
댓글남기기