어떤 변수의 위치를 가리키는 변수
왜 필요할까? 변수에는 유효범위라고하는 적용 범위가 있습니다. 예를들어 main함수와 A함수가 있다고 생각해보면 A함수에서 쓰던 변수들은 main함수로 나올수 없다는 것이죠. 왜 필요한지에 더 자세한 이유는 읽다보시면 이해되실 것 같습니다.
포인터가 어떤 변수의 주소(위치)를 가리키기 위해서는 다음의 2가지 정보가 필요할 것입니다.
1. 변수의 "주소"
- 메모리에는 각 byte마다 주소가 존재합니다.
- 변수의 주소라고 할 경우 맨 앞에있는 byte의 주소를 의미합니다. 예를들어, int의 경우 4byte의 크기를 가지는데 맨 앞의 byte의 주소를 얘기합니다.
2. 그 변수의 종류(자료형)
- 예를 들면 double의 형태를 가지는 변수가 있다고 생각해 봅시다.
- 변수는 메모리에 8byte의 크기를 확보할 뿐이고, 그 공간에 변수의 자료형에 대한 정보를 따로 저장하지 않습니다.
- 따라서 포인터 생성을 위해서는 가리키는 변수의 종류에 대한 정보도 알려주어야 합니다.
이제 변수의 주소값을 저장하기위해 포인터를 생성하면 각 실행파일의 bit에 맞는 크기의 공간이 확보 됩니다.
*중요*: 프로그램을 32bit의 실행파일로 만들경우 포인터는 4byte의 크기를 가지고, 64bit의 실행파일로 만들경우 포인터는 8byte의 크기를 가집니다.
(주의: char* a
라고해서 포인터가 1byte의 크기를 가지는 것이 아닙니다!)
C++에서 기본으로 제공되는 문법이므로 따로 정의의 과정이 존재하지 않습니다.
가리킬 변수의 자료형과 * 기호를 통해 선언이 가능합니다.
int* ptr1; // int형 변수를가리키는 포인터
double* ptr2; // double형 변수를 가리키는 포인터
해석 순서:
(* ptr1)
: 포인터인데 ->int
형 변수를 가리킴
포인터를 사용하는 방법에는 2가지가 있습니다.
int* a;
a; // a에 저장된 값(주소)에 접근
*a; // a에 저장된 주소에 저장되어 있는 값에 접근
a에 저장된 정보("주소값"):
a
와 같이 일반 변수들처럼 사용하면 됩니다.
a가 가리키는 공간("그 자체"):
*a
와 같이 *연산자를 사용하면 a가 가리키는 곳 "그 자체"를 의미하게 됩니다.
(즉, *에는 3가지 용도가 있다는 것을 알 수 있습니다.)
여느 변수와 같이 포인터로 선언된 변수에도 우리가 미리 정해놓은 데이터의 종류, 즉"주소" 만 저장이 가능합니다.
char a
char* aptr;
aptr = &a;
(&, Ampersand연산자를 통해 어떤 변수의 주소값을 구할 수 있습니다.)
aptr
에는 변수a
가 메모리상에서 위치하는 주소값을 저장합니다.
char a;
char* b = &a;
여느 변수와 마찬가지로 초기화 또한 가능합니다.
연산자들에는 적옹 순서가 존재합니다. 이때 *연산자는 순위가 낮은편에 속합니다.
(ex. [ ], .(dot), * 순서로 우선순위를 가짐)
배열이나 함수에도 그 종류가 존재합니다.
일반변수의 종류: 자료형
배열의 종류: 배열을 이루는 자료형 + [그 개수]
구조체의 종류: 구조체 이름
함수의 종류: 함수의 반환값 + (시그니처)
이를 이용하면 다양한 종류의 포인터를 만들 수 있게 됩니다.
1) 배열을 가리키는 포인터
생성시:(*pa)
즉, 포인터임을 먼저 알리고 생성합니다.
이때 가리키는 자료형으로 배열의 종류를 알려줍니다.사용시:
(*pa)
즉, 그 주소에 담긴 값임을 먼저 알리고 사용합니다.int (*pa)[10]; //생성시: ((*pa) 즉, 포인터이고) (가리키는곳은 int [10] 즉, 배열) int* pb[10]; //생성시: (pb[10] 즉, 10개의 배열을 생성하고) (그 자료들의 종류는 int* 즉, 포인터) (*pa)[3]; //사용시(단항연산): ((*pa) 즉, 담겨있는 주소에 있는 값의 (= int [10]배열에 해당하는 값의) ((*pa)[3] 즉, 3번째 원소) *pb[3]; //사용시(단항연산): (pb[3], 즉, 배열의 3번째 원소) (*pb[3] 즉, 3번째원소에 담겨있는 주소에 있는 값)
사용 예시
double array[10]; double (*p)[10] = &array; //즉, array배열 자체를 가리키는 하나의 포인터를 만들었습니다. (*p)[3] = 12 //이렇게 연산순서를 바꾸어야 포인터가 가리키는 배열에 접근할 수 있습니다.
2) 포인터를 가리키는 포인터(2중 포인터)
포인터도 메모리에 엄연히 자신의 공간을 가집니다. 이를 인지하고 있으면 몇개의 포인터라도 이을 수 있습니다.
char c = 'a' char* pc = &c; // pc에 c의 주소값을 저장합니다 char** ppc = &pc; // ppc에 pc의 주소값을 저장합니다. //((*ppc)즉, 포인터인데, 가리키는곳은 char* 즉 포인터
이처럼 *를 붙여 사용하면 2중, 3중, ...의 포인터를 만들 수 있습니다.
3) 구조체를 가리키는 포인터
구조체 안에 포인터를 두어 자신을 가리키게 하는 방법도 있었던것 기억나시나요? 당연히 구조체 외부에서 구조체를 가리키게 하는 것도 가능합니다
struct point { int x, y; }; point a = {2, 3}; point* ptr = &a;
4) 함수를 가리키는 포인터
프로그램시 필요한 모든 것은 대부분 당연히 메모리에 위치하게 됩니다. 따라서 함수의 주소(함수의 내용에 대한 주소) 또한 포인터에 저장할 수 있습니다.
생성시:
(*p)
를 먼저 계산하여 포인터임을 알리고 생성합니다.
이때 가리키는 자료형으로 함수의 종류를 알려줍니다.사용시:
(*p)
즉, 그 주소에 담긴 값을 사용할 것을 먼저 알리고 사용합니다.void findme(int x, int y) { cout << '(' << x << ',' << y << ')' <<'i am here' << endl; } int main() { void (*p) (int, int); // 생성시: 포인터임을 알리고, // 가리키는 곳의 데이터 종류는 void (int, int)임을 알립니다. p = &findme; (*p)(2, 2); // 그 장소에 담긴 값을 사용할 것임을 알리고 // 그곳(함수)에 (2,2)를 적용해 함수를 실행합니다. }
int i = 0;
void* iptr1 = &i;
int* iptr2 = (int*)iptr1;
(포인터 자료형)
의 형식을 이용하면 됩니다.
포인터는 2가지 정보를 가지고 있다고 했죠? 따라서 상수로 만들 수 있는 부분도 2가지 존재합니다.
int a = 0;
const int* ptr = &a;
- 즉,
ptr
에 저장하는 주소값(&a)는 변경 할 수 있지만 포인터로는 그 값을 변경할 수 없음을 얘기합니다.
(*ptr
을 const로 인식하게 해서*ptr = 3
과 같은 방법으로는 정보를 변경할 수 없게 됩니다.)
- 단,
a = 3
과 같이 포인터를 사용하지 않는 방법으로는 변경이 가능합니다.
int a = 0;
int b = 0;
int* const bptr = &b;
즉,
bptr
이 가리키는 변수(*bptr
)는 변경 할 수 있지만 가리키는 곳은 변경할 수 없음을 얘기합니다.
(bptr = &a
와 같은 주소 변경은 불가능 합니다.)
int c = 0;
const int* const cptr = &c;
마찬가지로
c = 3
과 같이 포인터를 사용하지 않으면 변경 가능합니다.
1) 포인터는 항상 초기화 하는 습관을 가져야 합니다.
int* ptr = 0; //바로 가리킬 곳이 없다면 0(Null)으로 초기화 해주자.
int* ptr = NULL; // 0과 NULL은 같은 의미입니다.
2) 포인터에는 주소값만 저장할 수 있습니다
3) 의미들을 헷갈리지 맙시다
생성에 있어서 * 는 포인터 자료형을 의미합니다.
접근에 있어서 * 는 그 주소에 담겨있는 값을 의미합니다.
4) 연산순서들을 조심합시다.
&a;
위와 같이 &(Ampersand) 연산자를 사용하면 해당 변수의 주소값을 반환 받을 수 있습니다.
int a;
int* a = &a;
a;
이미 포인터에 주소가 저장되어 있을 경우 그냥 이름만 사용하면 됩니다.
char a[] = "string";
배열의 이름은 첫번째 원소([0]번 인덱스)의 주소값을 의미합니다.
( 위의 배열의 경우 a는 's'의 주소를 의미합니다.)
즉, 배열의 이름은 "상수"가 됩니다.
(a = 3
과 같은 코드는 상수에 상수를 저장하는, 오류를 일으키는 문장입니다.)
즉, 변수와는 다르게 a가 그 값을 나타내지 않습니다.
int a[10] = {0};
int* a1ptr = &a[1];
int* a6ptr = &a[6];
cout << a6ptr - a1ptr; // 5
- 위의 경우 포인터간 뺄셈이므로 5*4 (인덱스차이*자료형 크기)라고 생각할 수 있습니다. 그러나 포인터간 뺄셈은 그 자료형의 크기까지 고려한다고 생각합시다.
- 즉, 5가 출력되게 됩니다.
float b[10] = {0.0};
cout << b+3; //b[3]
마찬가지로 자료형의 크기까지 고려하여야 합니다. 이때 b는 &b[0]을 의미하는 것이므로
b+3
은b[3]
을 의미하게 됩니다.
int c[10] = {0};
int *cptr = &c[0]
for (int i = 0; i<10; i++) {
*(cptr+i) = 7; // 1번
cptr[i] = 7; // 2번
}
- 주소간의 덧셈/뺄셈을 생각해 보면 위의 1번과 2번은 같은 표현입니다.
- 즉,
cptr+i
가 가리키는곳이라는 표현인*(cptr+i)
는cptr[i]
로 표현될 수 있습니다. (포인터 -> 배열로 표현 가능)
*의 3가지 용도.
1) 선언할 때: 포인터 자료형
int a = 0;
int* aptr = &a; // aptr은 포인터임
2) 단항연산: 그 주소가 "가리키는 곳 그 자체"
*aptr = 3; // aptr이 가리키는 곳에 (a)에 3을 저장
3) 이항연산: 곱셈
1*3;
&의 3가지 용도.
1) 선언할 때: 레퍼런스 자료형(별명)
int target = 10;
int& ref = target; //target의 또다른 이름은 ref임
레퍼런스는 어떤 변수의 또다른 이름(별명)을 알려주는 것입니다.
포인터나 다른 자료형들과는 다르게 별도의 저장공간이 메모리에 생기지 않습니다.(컴파일러 자체파악)
레퍼런스는 const와 마찬가지로 반드시 초기화가 필요합니다.
2) 단항연산: 그 변수의 주소를 반환하는 연산자
⌖ //target의 주소 반환
3) 이항연산: 비트단위 연산
target & 3;
call by value
call by reference