1. 포인터 개요
- 메모리란 우리가 데이터를 넣고 꺼내는 공간이다.
- 그래서 컴퓨터가 값을 넣거나 사용하기 위해서는 그 메모리 주소를 알아야만 한다.
- C는 managed language라고 불리는 이 메모리를 직접 접근해서 관리할 수 있는 언어이다.
- 메모리에 직접 접근해서 값을 변경하거나 사용하도록 해주는 자료형을 포인터라고 부른다.
1-1. 메모리의 주소와 주소 연산자
- 포인터는 주소를 담고 있는 자료형이다.
- 포인터는 해당 주소를 참조하여 시작지점 부터 얼마나까지를 가리키는 변수의 값으로 인식할 지 결정한다.
- 그러면 해당 주소를 나타내는 연산자가 필요한데 이것이 바로 &연산자이다. (Ampersand, 앰퍼샌드)
int a = 10; // 우리 컴퓨터 스택의 임의의 공간에 a라는 변수가 만들어지고 10이라는 값이 들어간다.
printf("%p", &a) // printf의 p는 주소를 출력하는 메타문자이다.
// &를 붙여서 a의 주소를 출력할 수 있다.
1-2. 간접 참조 연산자 (*) 와 포인터 선언 (*)
- 포인터는 담고 있는 주소를 가리키는 자료형이라고 위에서 말했다.
- 그럼 포인터에 주소를 할당함으로서 사용이 가능하다.
- * (asterisk, 애스터리스크) 라고 불리는 기호를 사용하는데, 필자가 포인터를 처음 접했을 때 헷갈렸던 부분을 한번 언급하고 넘어가고자 한다.
- * 연산자는 선언할 때 사용할 때와, 가리키는 변수의 값을 가지고 올 때로 두가지 용도로 쓰인다.
- 선언할 때 사용하는 *는 선언한 변수를 우리가 "포인터"로서 사용하겠다는 의미이다.
- 그 외에 *를 사용하면 이 포인터가 가리키고 있는 주소에 있는 변수의 값을 가지고 오겠다는 의미이다.
int a = 10;
int *pointer_a; // 지금 pointer_a 라는 이름의 (int *) 정수형 자료의 주소를 담는 정수 포인터를 선언했다.
pointer_a = &a; // 포인터는 주소를 담는 자료형이므로 a의 주소를 할당해준다.
// 그럼 이제부터 컴퓨터 메모리상에 pointer_a 의 값에는 a라는 변수의 메모리 주소가 담겨져 있는 것이다.
printf("value a is : %d", *pointer_a); // 여기서 사용하는 *는 위에서 선언할 때 사용한 *와 다르다.
//선언할 때를 제외한 * 기호는 해당 포인터가 가리키고 있는 자료형의 값을 가지고 온다.
2. 포인터 활용
2-1. 주소와 포인터의 크기
- C에서 변수는 8byte의 주소로 나타낸다.
- (64bit 운영체제에서 64bit 컴파일시 그렇다, 32bit 운영체제에서 32bit 컴파일시 4byte이다.)
- 여기서 주의할 점은 변수가 8byte라는 것이 아니라 주소가 8byte로 표현된다는 것이다.
- 예를 들어서 int a = 10 이라고 한다면 a 자체는 메모리상에 4byte 만큼 할당되지만 a의 주소를 나타내기 위해서는 8byte가 필요하다는 뜻이다.
- 그래서 포인터는 주소만을 담고 있는 자료형이기 때문에 어떤 자료형의 변수를 가리키는 것과 상관없이 크기가 8byte로 동일하다.
#include <stdio.h>
int main(void)
{
char ch;
int in;
double db;
char *pc = &ch;
int *pi = ∈
double *pd = &db;
printf("char형 변수의 주소 크기 : %lu\n", sizeof(&ch));
printf("int형 변수의 주소 크기 : %lu\n", sizeof(&in));
printf("double형 변수의 주소 크기 : %lu\n", sizeof(&db));
printf("char형 포인터의 크기 : %lu\n", sizeof(pc));
printf("int형 포인터의 크기 : %lu\n", sizeof(pi));
printf("double형 포인터의 크기 : %lu\n", sizeof(pd));
printf("char형 변수의 크기 : %lu\n", sizeof(ch));
printf("int형 변수의 크기 : %lu\n", sizeof(in));
printf("double형 변수의 크기 : %lu\n", sizeof(db));
return (0);
}
더보기
//출력 결과
char형 변수의 주소 크기 : 8
int형 변수의 주소 크기 : 8
double형 변수의 주소 크기 : 8
char형 포인터의 크기 : 8
int형 포인터의 크기 : 8
double형 포인터의 크기 : 8
char형 변수의 크기 : 1
int형 변수의 크기 : 4
double형 변수의 크기 : 8
2-2. 포인터 대입
- 위에서 언급한 중요한 내용 중에 가리키는 주소의 시작지점 부터 얼마나까지를 가리키는 변수의 값으로 인식할 지 결정한다. 라는 내용이 있었다.
- 우리가 포인터를 선언할 때, * 앞에 자료형을 언급하는 순간 포인터는 해당 주소에서 몇 byte 까지 우리가 가리키는 변수의 값으로 볼지를 결정한다.
- 아래 코드를 예시로 두면 우리는 double 포인터를 선언했기 때문에 우리가 가리키는 주소로 부터 8byte까지 값으로 볼 건데, 시작 주소로부터 4byte 까지만 값을 담는 정수를 넣었기 때문에, 어떤 결과가 나올 지 우리는 예상할 수 없다.
- 그러므로 포인터를 대입할 때는 자료형을 엄격히 맞춰야 하며, 다른 자료형을 대입할 때는 형변환 연산자(혼공C 4장)을 이용해서 대입해 주어야 한다.
#include <stdio.h>
int main(void)
{
int a = 10;
int *p = &a;
double *pd;// pd 포인터는 double 형을 가리키므로 시작주소부터 8byte 만큼을 값으로 인식한다.
pd = p; // 그런데 int 자료형의 주소가 pd에 들어왔다.
printf("%lf\n", *pd); // 이 결과는 예상할 수 없다.
// 왜냐하면 1이 시작주소라고 가정했을 때 1 ~ 4 까지 a 의 값이 들어가 있는데,
// 내 컴퓨터 메모리의 5 ~ 8 까지의 주소에 어떤 값이 담겨 있을 지는 알 수 없기 때문이다.
// 이런 동작을 UD, 예상하지 못하는 결과가 나온다고 한다.
return (0);
}
2-3. call by value, call by reference
- 함수를 선언하면 스택에 함수가 복사되어서 값이 들어가고 결과가 반환된다고 설명한 적 있다.
- 그래서 빠져나오지 못하는 재귀함수를 실행하면 스택이 터져버리는 스택 오버플로우가 발생한다.
- 마찬가지로 우리가 함수를 호출해서 인자를 넣어줄 때, 인자가 그대로 들어가는 것이 아니라 복사되어 들어간다.!
- 이 부분은 나중에 이중포인터와 결합되어서 정말 골치아프고 헷갈리게 작용되는 데, 우선 쉽게 설명하면 우리가 10이라는 값이 들어 있는 a라는 정수를 인자로 넘기면 컴퓨터 메모리의 어떤 공간에 a와 같은 값이 복사가 되고, 이 복사가 된 데이터가 들어간다!
- 이를 call by value라고 부른다.
#include <stdio.h>
void swap(int a, int b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
}
int main(void)
{
int a = 10, b = 20;
swap(a, b); // 여기서 우리 메모리 공간의 a와 b가 들어 가는 것이 아니라 a와 b의 값만 복사해서 들어간다.
printf("a is : %d, b is : %d\n", a, b); // 값이 변하지 않았다!
return (0);
}
- 그럼 무엇을 복사해서 넘겨야 우리가 원하는 대로 동작할까?
- 값이 복사되면 값을 바꿔줘도 함수호출이 끝나면 소멸되서 반영이 되지 않았었다.
- 그렇다면 우리가 바꾸고 싶은 변수를 가리키는 대상을 복사한다면?
- 엄밀히 말하면 같은 포인터가 변수 a에 접근하는 것은 아니지만 포인터가 복사되어도 그 복사된 포인터는 여전히 원본 a를 가리키고 있다.
- 그렇다면 복사된 포인터로 a의 주소에 접근해서 값을 바꾼다면 함수 호출이 끝나도 a의 값은 변한 상태를 유지할 수 있다.
- 이를 call by reference 라고 부른다.
- 아래는 예시 코드이다.
#include <stdio.h>
void swap(int *pa, int *pb) // 변수의 주소를 담는 포인터를 매개변수로 선언했다.
{
int tmp;
tmp = *pa;
*pa = *pb;
*pb = tmp;
}
int main(void)
{
int a = 10, b = 20;
swap(&a, &b); // 매개변수가 포인터였기 때문에 인자로 주소를 넘긴다.
printf("a is : %d, b is : %d\n", a, b); // 이제 값이 변한 걸 확인할 수 있다.
return (0);
}
2-4. 상수 포인터와 포인터 상수
- C에서 상수 포인터와 포인터 상수는 헷갈리기 좋은 요소이다.
- 쉽게 읽는 법으로 단어 순서대로 적는다! 라고 생각하면 구분하기 쉽다!
- "상수" "포인터" 는 상수가 먼저 나오고 다음이 포인터이므로 const 포인터 자료형 *변수명 으로 작성한다.
- "포인터" "상수" 는 포인터가 먼저 나오고 다음이 상수이므로 포인터 자료형 *const 변수명 으로 작성한다.
- 인트형 변수 a로 예시를 들면
- 상수 포인터는 : const int *a 로 표현하고
- 포인터 상수는 : int *const a 로 표현한다.
- 상수 포인터는 상수에 대한 포인터 라는 뜻이다. 즉 포인터로 가리키는 값을 변경할 수 없다는 뜻이다.
- 다만 포인터로 접근해서 값을 변경하는 행위가 불가능하지 a라는 변수가 값을 변경할 수 없음을 의미하지 않는다.
- 포인터 상수는 말 그대로 포인터 자체를 상수화한다는 것이다 예시로 설명하면 a에 담겨 있는 값인 주소 자체를 변경 불가하다
//상수 포인터
int a = 10;
int aa = 100;
const int *pa = &a;
*pa = 20; // 불가능하다.
a = 20; // 가능하다.
pa = &aa; // 가능하다.
// 포인터 상수
int a = 10;
int aa = 100;
int* const pa = &a;
*pa = 20; // 가능하다.
a = 20; // 마찬가지로 가능하다.
pa = &aa; // 불가능하다
3. 혼공C 9장 도전 실전 예제
- 실수 3개를 입력받은 후 큰 숫자부터 정렬하는 코드
#include <stdio.h>
void swap(double *pa, double *pb);
void line_up(double *maxp, double *midp, double *minp);
int main(void)
{
double max, mid, min;
printf("실수 3개 입력");
scanf("%lf%lf%lf", &max, &mid, &min);
line_up(&max, &mid, &min);
printf("정렬된 값 출력 : %.1lf, %.1lf, %.1lf\n", max, mid, min);
return (0);
}
void swap(double *pa, double *pb)
{
double tmp;
tmp = *pa;
*pa = *pb;
*pb = tmp;
}
void line_up(double *maxp, double *midp, double *minp)
{
if (*midp < *minp)
{
swap(midp, minp);
if (*maxp < *midp)
{
swap(maxp, midp);
if (*midp < *minp)
swap(midp, minp);
}
}
else if (*maxp < *midp)
{
swap(maxp, midp);
if (*midp < *minp)
swap(midp, minp);
}
}
'Language > C' 카테고리의 다른 글
혼공C (11장) - 문자와 버퍼 (getchar, putchar) (0) | 2024.04.11 |
---|---|
혼공C (10장) - 배열과 포인터 : 배열을 인자로 (0) | 2024.04.02 |
혼공C (8장) - C 배열, VLA, 문자열 혼공C 8장 실전예제 (1) | 2024.03.24 |
혼공C (7장) - C 함수 기초, 혼공C 7장 실전예제 (0) | 2024.03.22 |
혼공C (6장) - 반복문 (while, do while, for) (0) | 2024.03.20 |