Skip to content
Snippets Groups Projects
Commit f156759a authored by BeomSooHeo's avatar BeomSooHeo
Browse files

pointer

parent d2aad428
No related branches found
No related tags found
No related merge requests found
*.exe
let &t_ti.="\e[1 q"
let &t_SI.="\e[5 q"
let &t_EI.="\e[1 q"
let &t_te.="\e[0 q"
# Concepts :: Pointer
포인터의 개념 학습 및 실습을 통한 확인
## 1. Pointer란?
보통 변수에는 값(value)을 저장했지만,
포인터는 변수가 저장되어 있는 주소값(address value)을 저장한다.
즉 포인터는 변수의 주소값을 저장하는 변수라고 할 수 있다.
우리가 친구 집에 방문하기 위해 친구의 집 주소를 찾아가면 친구를 만날 수 있듯이, 변수도 마찬가지다. 변수의 주소에 접근하면 우리는 변수를 읽고 쓸 수 있다. 앞서 포인터는 변수의 주소값이 저장되어 있다고 했다. 그렇다. 우리는 포인터를 통해 다른 변수에 접근할 수 있게 된 것이다.
## 2. Pointer의 선언과 참조
### 1) 포인터의 선언
포인터의 선언은 다음과 같이 data_type과 포인터의 이름 사이에 <strong>'*'</strong> 을 반드시 써야 한다.
<strong><I>data_type</I> * pointer_name </strong>
```c
ex) int * p = NULL;
```
int형 변수를 가리키는 포인터 p를 선언하였다.
만약 어떤 변수를 참조할지 결정이 되지 않았다면 '현재 포인터가 가리키는 대상이 없다', '포인터가 비어있다'는 의미로 <strong>NULL</strong>로 초기화 시켜주는 것이 좋다.
### 2) &을 통한 변수의 주소값 얻기
```c
int * p = NULL;
int n = 10;
p = &n; // n의 주소값이 p에 assign된다.
```
포인터 p가 int형 변수 n을 가리키도록 해보자.
포인터는 변수의 주소값을 저장한다. 그렇다면 변수의 주소값이 필요하다.
변수의 주소값은 위 3번째 줄과 같이 변수의 이름 앞에 '&'을 붙이면 얻을 수 있다.
### 3) *을 통한 포인터 참조
```c
int * p = NULL;
int n = 10;
p = &n; // n의 주소값이 p에 assign된다.
printf("%d\n", *p); // 10
*p = 20; // n=20
```
2)를 통해 이제 포인터 p는 n을 가리킨다.
친구집의 주소까지 찾아갔다면, 벨을 눌러서 문을 열어 달라고 해야된다.
이 역할을 하는 것이 <strong>*</strong> 이다.
포인터 변수의 이름 앞에 *를 쓰면, 포인터가 가리키는 변수의 값에 접근할 수 있다. 3~4번째 줄에서 확인할 수 있듯이, 값을 읽어 오거나, 다른 값을 쓸 수 있다.
## 3. 포인터의 타입과 크기
모든 primitive data type에 대한 포인터가 있고, 사용자 정의 data type에 대해서도 포인터 선언이 가능하다. 한편, 포인터의 크기는 포인터가 주소값을 담기 때문에 type에 관계 없이 같고, 시스템에 따라 포인터의 크기가 다르다. 가령 32-bit system일 경우 포인터는 4byte, 64-bit system이라면 포인터는 8byte에 해당한다.
## 4. an_arr[] vs * an_arr
```C
int an_arr[32];
int * an_arr = (int *)malloc(sizeof(int)*32);
```
배열 an_arr는 int형 변수를 32개를 가졌다. 배열의 이름 an_arr을 printf로 확인해보면 주소값이 나오는데, 이 주소값은 몇 번째 원소의 주소값일까? 당연히도 첫 번째 주소값이다. 배열의 이름은 첫 번째 element의 주소를 저장하고 배열 내의 원소를 참조할 때는 자료형의 크기에 index의 차이만큼 곱한 값을 배열의 첫 번째 원소의 주소값에 더한다.
우리가 알 수 있는 사실은, 배열의 이름 "an_arr"은 배열의 첫 번째 element인 an_arr[0]의 주소값을 가진다는 것, 따라서 배열의 이름은 포인터이고 포인터의 연산을 할 수 있다는 것이다.
우리는 이제 아래의 식이 참임을 알 수 있다.
```c
*(an_arr + i) == an_arr[i]
```
<strong>물론 그렇다고해서 포인터와 배열이 같다고 할 수 없다.</strong> 포인터는 가리키는 대상을 바꿀 수 있는 반면,
배열의 이름의 피연산자로 주소값이 올 수 없기 때문이다. 상수형 포인터라고 생각할 수도 있을지 모르겠다.
## 5. constant pointer
<strong>const</strong> keyword를 통해 포인터도 상수화시킬 수 있다.
```c
int const A //constant integer
int const * A //(variable) pointer to a constant integer
int * const A //constant pointer to a (variable) integer
int * const * A //pointer to a constant pointer to an integer
int const * * A //pointer to a pointer to a constant integer
int const * const * A //pointer to a constant pointer to a constant integer
```
내려갈수록 복잡하다...오른쪽에서 왼쪽으로 해석하면 읽기 좀 더 수월하다.
## 6. void * pointer
기본적으로 C 언어는 자료형이 다른 포인터끼리 메모리 주소를 저장하면 컴파일 경고(warning)가 발생한다. 하지만 void 포인터는 일반적인 포인터와는 달리 자료형을 명시하지 않았기 때문에 어떤 자료형으로 된 포인터든 모두 저장할 수 있다. 반대로 다양한 자료형으로 된 포인터에도 void 포인터를 저장할 수도 있다. 따라서 변수, 함수, 포인터 등 어떠한 값도 가리킬 수 있지만, 포인터 연산이나 메모리 참조와 같은 작업은 할 수 없다. 즉 void 포인터는 주소값을 저장하는 것 이외에는 아무것도 할 수 없는 포인터다. 또한, void 포인터를 사용할 때에는 반드시 먼저 사용하고자 하는 타입으로 명시적 타입 변환 작업을 거친 후에 사용해야 한다.
```c
void * ptr;
int a = 10;
char b = 'b';
int *n1 = &a;
char *c1 = &b;
ptr = n1; // void 포인터에 int 포인터 저장
printf("%d", *ptr); // void 포인터는 * 사용할 수 없음.
ptr = c1; // void 포인터에 char 포인터 저장
printf("%c", *ptr); // void 포인터는 * 사용할수 없음.
Result: Compile Error!
```
다음의 예에서 void 포인터의 형변환을 확인할 수 있다.
```c
#include <stdio.h>
int main()
{
int num1 = 10;
float num2 = 3.5f;
char c1 = 'a';
void *ptr;
ptr = &num1;
// printf("%d\n", *ptr); // 컴파일 에러
printf("%d\n", *(int *)ptr); // 10: void 포인터를 int 포인터로 변환한 뒤 역참조
ptr = &num2; // num2의 메모리 주소를 void 포인터 ptr에 저장
// printf("%f\n", *ptr); // 컴파일 에러
printf("%f\n", *(float *)ptr); // 3.500000: void 포인터를 float 포인터로 변환한 뒤 역참조
ptr = &c1; // c1의 메모리 주소를 void 포인터 ptr에 저장
// printf("%c\n", *ptr); // 컴파일 에러
printf("%c\n", *(char *)ptr); // a: void 포인터를 char 포인터로 변환한 뒤 역참조
return 0;
}
실행 결과
10
3.500000
a
```
<참조 : https://dojang.io/mod/page/view.php?id=495>
그렇다면 void 포인터를 어떻게 사용할 수 있을까? 가령, 함수에서 다양한 자료형을 받아들일 때, 함수의 반환 포인터를 다양한 자료형으로 된 포인터에 저장할 때, 자료형을 숨기고 싶을 때 사용할 수도 있겠다.
## 7. function pointer
이제 포인터가 무엇인지는 알기 때문에 function pointer가 무엇인지 쉽게 알수 있을 것이다. 그렇다. 함수의 주소를 저장하고 있는 변수이다. 함수의 이름이 함수의 시작 주소이다.
```c
void func() {}
void main() {
printf("func : %x\n", func);
printf("main : %x\n), main);
}
***result***
func: 4111d1
main: 411140
```
함수 포인터의 선언은 다음과 같이한다.
return_type <strong>(*fp) </strong>(parameter_list);
반드시 포인터라는 의미의 *를 쓰고, 괄호로 감싸야한다.
또한, 함수 포인터의 선언을 할 때는 함수 포인터가 가리킬 함수의 return_data_type과 parameter_list가 동일해야 한다.
마지막으로, 함수 포인터에 함수를 할당할 때는 =에 함수의 이름만 지정해주면 된다.
```c
ex)
#include <stdio.h>
int add(int a, int b) {return a+b;}
int sub(int a, int b) {return a-b;}
int mul(int a, int b) {return a*b;}
int div(int a, int b) {return a/b;}
int (*calc)(int a, int b);
...생략
calc = add; //함수 포인터에 함수를 할당할 때는 =에 함수의 이름만 지정해주면 된다.
printf("%d\n", calc(3,5)); //8
...
```
<br>
함수 포인터를 사용해서 얻을 수 있는 장점에 대해 생각해보자. 함수 포인터를 사용하면 하나의 인터페이스로 여러 기능을 제공할 수 있고, 함수 포인터를 통해 콜백을 구현할 수 있다는 장점이 있다.
## 8. finally..
### double (*f[10])(int const *a, double (*g[10])(double h)); 은 무엇인가요?
return type이 double이고
int형 변수를 가리키는 상수형 포인터와
return type이 double이고 double type변수 1개를 매개변수로 받는 함수를 가리키는 길이가 10인 함수 포인터 배열을 parameter로 받는
함수를 가리키는 길이가 10인 함수형 포인터 배열을 뜻한다!
# Practice:: test-0 ~ test-7
## 1. test-0
```c
#include <stdio.h>
void func(int *p)
{
int *q = p;
*q = 100;
q++;
*q = 200;
}
int main()
{
int a = 1;
int b = 2;
int *pa, *pb;
long long pp;
pa = &a;
pb = &b;
pp = (long long) pa;
printf("%d %d\n", a, b);
func(&b);
printf("%d %d\n", *pa, *pb);
printf("%d %d %d\n", pa, pb, *(int *) pp);
printf("%d %d\n", a, b);
printf("%d, %d, %d, %d\n", a, &a, *(&a), *(int *)(long long)(&a) );
}
```
1. func는 전달받은 변수의 값에서 주소값을 임의로 더한후, 이를 바꾸기 때문에 보안상 좋지 않다. (memory-hacking by pointer)
2. 주소값을 저장한 변수에 pointer type으로 casting할 수 있고, *연산을 통해 변수의 값을 읽을 수 있다.
## 2. test-1
```c
#include <stdio.h>
int main()
{
int a[10] = { 0, 0, 0, 1, 4, 5, 6, 7, 8, 9};
// void *b;
long long *c;
// b = &a[0];
c = (long long *)a;
printf("%d, %d\n", a, *a); //..1
printf("%x, %llx\n", c, *c); //..2
printf("%x, %llx\n", c+1, *(c+1));//..3
float kk = 0.75f;
printf("%f : %d, %x \n", kk, *(int *)&kk, *(int *)&kk);//..4
}
**result **
6422180, 0
61fea4, 0
61feac, 100000000
0.750000 : 1061158912, 3f400000
```
1. a는 배열의 주소값 *a는 배열의 첫번째 element인 0이다.
2. c는 a의 주소값이고 *c는 *a와 같으므로 0이다.
3. c는 long long type이기 때문에 8byte단위로 데이터를 인식한다. 따라서 c+1은 2에서 구한 61fea4에 8을 더한 61feac이다. *(c+1)은 a[3]부터 a[0]까지 읽은 100000000이다.
4. float type인 kk는 0.750000, *(int *)&kk 는 kk를 int형 포인터로 casting한 후, 이를 *로 접근하여 10진수 정수로 출력하여 1061158912이고 이를 16진수로 출력한 것이 3f400000이다.
## 3. test-2
```c
#include <stdio.h>
int func(int a)
{
int b[a];
b[0] = 200;
printf("%d\n", b[0]);
}
int main()
{
int a = 1;
int b = 2;
void *v = &a;
printf("v add %d \n", v);
v++;
printf("v add %d \n", v);
printf("%d %d\n", a, b);
func(10);
printf("%d %d\n", a, b);
}
***result***
v add 6422212
v add 6422213
1 2
200
1 2
```
본래 v++와 같이 data_type이 없는 void 포인터를 대상으로한 연산은 이루어지지 않지만,
void 포인터에 후위연산한 결과 주소값이 1만큼 증가했다.
또한 void 포인터 v를 사용하기 위해서는 *((*int) v)와 같이 casting이 필요하다.
## 4. test-3
```c
#include <stdio.h>
void set_elmt(int *a)
{
a[0] = 3;
}
int main()
{
const int a = 10;
int *b = &a;
printf("%d \n", a);
set_elmt(b);
printf("%d \n", a);
int * const c = b;
*c = 100;
printf("%d \n", a);
}
***Result***
10
3
100
```
주목해야할 것은 const인 a가 포인터를 통한 접근으로 값이 바뀌었다는 것이다. 그렇다면 const는 아무 쓸모가 없는 것인가? 그렇지 않다.적어도 compile time에 상수의 변화를 막아준다. 그러나 런타임시 바꾸는 것을 막지 못한다. 즉 실수로 접근하는 것 정도는 막아주고, 포인터에 의한 접근은 의도적으로 접근하는 것으로 판단하여 막지 않는 것 같다.
## 5. test-4
```c
#include <stdio.h>
int main()
{
int a[10], b[30], c[40];
int * t[3];
t[0] = a;
t[1] = b;
t[2] = c;
t[1][35] = 200;
printf("%d\n", b[35]);
for (int i = 0 ; i < 40 ; i++)
printf("%d ", b[i]);
}
```
배열 b의 길이가 30임에도 불구하고, index가 35에 대한 접근, 35부터 39까지의 접근을 해도 컴파일이 성공적으로 이뤄졌다. 그러나 index가 3500인 경우 컴파일 에러를 일으켰다. 이를 통해 boundary condition이 잘못 되었더라도 너무 크지만 않다면 컴파일에러, 런타임에러가 발생하지 않을 수 있다는 것을 알 수 있다.
## 6. test-5
```c
#include <stdio.h>
int f10(int a)
{ return a+10; }
int f5(int a)
{ return a+5; }
int f1(int a)
{ return a+1; }
int f22(int a)
{ return a+22; }
int main()
{
int a = 200;
int (*f[4])(int) = {f10, f5, f1, f22};
printf("%d\n", f[2](10));
}
```
함수 포인터를 element로 삼는 배열도 구성이 가능하다.
결과는 함수 포인터 f의 3번째 element인 f1에 10을 전달한 11이다.
## 7. test-6
```c
#include <stdio.h>
int f10(int a)
{ return a+10; }
int f5(int a)
{ return a+5; }
int f1(int a)
{ return a+1; }
int f22(int a)
{ return a+22; }
int main()
{
double (*f[10])(int const *a, double (*g[10])(double h));
int (*q)(int, int, double);
int a = 200;
int (*ff)(int);
ff = f10;
printf("%d\n", ff(a)); //ff는 함수 f10.
}
```
함수 포인터 ff에 f10을 assign했으므로 ff는 f10이나 마찬가지다.
따라서 ff(a) 는 210이다.
double (*f[10])(int const *a, double (*g[10])(double h)); 에 대한 설명은 위에서 했으므로 생략하겠다.
## 8. test-7
```c
#include <stdio.h>
float f_f() {return 100.0; }
int f_i() {return 100; }
int main()
{
int (*f)(void);
float (*ff)(void);
f = f_i;
printf("%d\n", f()); // f()는 함수 f_i
ff = f_f;
printf("%f\n", ff()); //ff()는 함수 f_f
}
```
6, 7번과 동일하게 함수 포인터를 사용하는 방법을 보여주는 예제이다. 위에서 충분히 설명했으므로 결과만 나열하면 100, 100.0이다.
\ No newline at end of file
#include <stdio.h>
void func(int *p)
{
int *q = p;
*q = 100;
q++;
*q = 200;
}
int main()
{
int a = 1;
int b = 2;
int *pa, *pb;
long long pp;
pa = &a;
pb = &b;
pp = (long long) pa;
printf("%d %d\n", a, b);
func(&b);
printf("%d %d\n", *pa, *pb);
printf("%d %d %d\n", pa, pb, *(int *) pp);
printf("%d %d\n", a, b);
printf("%d, %d, %d, %d\n", a, &a, *(&a), *(int *)(long long)(&a) );
}
#include <stdio.h>
int main()
{
int a[10] = { 0, 0, 0, 1, 4, 5, 6, 7, 8, 9};
// void *b;
long long *c;
// b = &a[0];
c = (long long *)a;
printf("%d, %d\n", a, *a);
printf("%x, %llx\n", c, *c);
printf("%x, %llx\n", c+1, *(c+1));
float kk = 0.75f;
printf("%f : %d, %x \n", kk, *(int *)&kk, *(int *)&kk);
}
\ No newline at end of file
#include <stdio.h>
int func(int a)
{
int b[10];
b[0] = 200;
printf("%d\n", b[0]);
}
int main()
{
int a = 1;
int b = 2;
void *v = &a;
printf("v add %d %d\n", v, *((int*)v));
v++;
printf("v add %d \n", v);
printf("v add %d \n", (int*)v);
printf("%d %d\n", a, b);
func(10);
printf("%d %d\n", a, b);
}
#include <stdio.h>
int main()
{
const int a = 99;
int const *b = &a;
*(int *)b = 200;
printf("%d \n", a);
}
#include <stdio.h>
void set_elmt(int *a)
{
a[0] = 3;
}
int main()
{
const int a = 10;
int *b = &a;
printf("%d \n", a);
set_elmt(b);
printf("%d \n", a);
int * const c = b;
*c = 100;
printf("%d \n", a);
}
#include <stdio.h>
int main()
{
int a[10], b[30], c[40];
int * t[3];
t[0] = a;
t[1] = b;
t[2] = c;
t[1][35] = 200;
printf("%d\n", b[35]);
for (int i = 0 ; i < 40 ; i++)
printf("%d ", c[i]);
}
#include <stdio.h>
int f10(int a)
{ return a+10; }
int f5(int a)
{ return a+5; }
int f1(int a)
{ return a+1; }
int f22(int a)
{ return a+22; }
int main()
{
int a = 200;
int (*f[4])(int) = {f10, f5, f1, f22};
printf("%d\n", f[2](10));
}
#include <stdio.h>
int f10(int a)
{ return a+10; }
int f5(int a)
{ return a+5; }
int f1(int a)
{ return a+1; }
int f22(int a)
{ return a+22; }
int main()
{
double (*f[10])(int const *a, double (*g[10])(double h));
int (*q)(int, int, double);
int a = 200;
int (*ff)(int);
ff = f10;
printf("%d\n", ff(a));
}
#include <stdio.h>
float f_f() {return 100.0; }
int f_i() {return 100; }
int main()
{
int (*f)(void);
float (*ff)(void);
f = f_i;
printf("%d\n", f());
ff = f_f;
printf("%f\n", ff());
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment