C++ 문법(자료형) - 4. 포인터

Ui Jin·2021년 9월 14일
0

C++ Grammar

목록 보기
5/13

포인터


포인터란?

어떤 변수의 위치를 가리키는 변수

왜 필요할까? 변수에는 유효범위라고하는 적용 범위가 있습니다. 예를들어 main함수와 A함수가 있다고 생각해보면 A함수에서 쓰던 변수들은 main함수로 나올수 없다는 것이죠. 왜 필요한지에 더 자세한 이유는 읽다보시면 이해되실 것 같습니다.


원리

포인터가 어떤 변수의 주소(위치)를 가리키기 위해서는 다음의 2가지 정보가 필요할 것입니다.
1. 변수의 "주소"

  • 메모리에는 각 byte마다 주소가 존재합니다.
  • 변수의 주소라고 할 경우 맨 앞에있는 byte의 주소를 의미합니다. 예를들어, int의 경우 4byte의 크기를 가지는데 맨 앞의 byte의 주소를 얘기합니다.

2. 그 변수의 종류(자료형)

  • 예를 들면 double의 형태를 가지는 변수가 있다고 생각해 봅시다.
  • 변수는 메모리에 8byte의 크기를 확보할 뿐이고, 그 공간에 변수의 자료형에 대한 정보를 따로 저장하지 않습니다.
  • 따라서 포인터 생성을 위해서는 가리키는 변수의 종류에 대한 정보도 알려주어야 합니다.

이제 변수의 주소값을 저장하기위해 포인터를 생성하면 각 실행파일의 bit에 맞는 크기의 공간이 확보 됩니다.

*중요*: 프로그램을 32bit의 실행파일로 만들경우 포인터는 4byte의 크기를 가지고, 64bit의 실행파일로 만들경우 포인터는 8byte의 크기를 가집니다.

(주의: char* a라고해서 포인터가 1byte의 크기를 가지는 것이 아닙니다!)


정의와 선언

1) 정의

C++에서 기본으로 제공되는 문법이므로 따로 정의의 과정이 존재하지 않습니다.

2) 사용

2-1) 선언(생성)

가리킬 변수의 자료형과 * 기호를 통해 선언이 가능합니다.

int* ptr1;	// int형 변수를가리키는 포인터
double* ptr2;	// double형 변수를 가리키는 포인터

해석 순서: (* ptr1): 포인터인데 -> int형 변수를 가리킴

2-2) 접근

포인터를 사용하는 방법에는 2가지가 있습니다.

int* a;
a;		// a에 저장된 값(주소)에 접근
*a;		// a에 저장된 주소에 저장되어 있는 값에 접근

a에 저장된 정보("주소값"): a와 같이 일반 변수들처럼 사용하면 됩니다.

a가 가리키는 공간("그 자체"): *a와 같이 *연산자를 사용하면 a가 가리키는 곳 "그 자체"를 의미하게 됩니다.

(즉, *에는 3가지 용도가 있다는 것을 알 수 있습니다.)

2-3) 저장

여느 변수와 같이 포인터로 선언된 변수에도 우리가 미리 정해놓은 데이터의 종류, 즉"주소" 만 저장이 가능합니다.

char a
char* aptr;

aptr = &a;

(&, Ampersand연산자를 통해 어떤 변수의 주소값을 구할 수 있습니다.)

aptr에는 변수a가 메모리상에서 위치하는 주소값을 저장합니다.

2-4) 초기화

char a;
char* b = &a;

여느 변수와 마찬가지로 초기화 또한 가능합니다.


활용

1) 다양한 종류의 포인터

  • 연산자들에는 적옹 순서가 존재합니다. 이때 *연산자는 순위가 낮은편에 속합니다.
    (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)를 적용해 함수를 실행합니다.
}

2) 형변환

int i = 0;
void* iptr1 = &i;

int* iptr2 = (int*)iptr1;

(포인터 자료형) 의 형식을 이용하면 됩니다.

3) const

포인터는 2가지 정보를 가지고 있다고 했죠? 따라서 상수로 만들 수 있는 부분도 2가지 존재합니다.

3-1) 이 경우는 ptr이 가리키는곳의 변수는 const int타입이라는 의미입니다.

int a = 0;
const int* ptr = &a;
  • 즉, ptr에 저장하는 주소값(&a)는 변경 할 수 있지만 포인터로는 그 값을 변경할 수 없음을 얘기합니다.
    (*ptr을 const로 인식하게 해서 *ptr = 3과 같은 방법으로는 정보를 변경할 수 없게 됩니다.)
  • 단, a = 3과 같이 포인터를 사용하지 않는 방법으로는 변경이 가능합니다.

3-2) 이 경우는 bptr포인터가 const 타입이라는 것을 의미합니다.

int a = 0;
int b = 0;
int* const bptr = &b;

즉, bptr이 가리키는 변수(*bptr)는 변경 할 수 있지만 가리키는 곳은 변경할 수 없음을 얘기합니다.
(bptr = &a와 같은 주소 변경은 불가능 합니다.)

3-3) 이 경우는 cptr이 가리키는곳과 가리키는곳의 값 모두변경할 수 없음을 의미합니다.

int c = 0;
const int* const cptr = &c;

마찬가지로 c = 3과 같이 포인터를 사용하지 않으면 변경 가능합니다.


주의점

1) 포인터는 항상 초기화 하는 습관을 가져야 합니다.

  • 포인터를 초기화 하지 않으면 "쓰레기값"에 의해 이상한곳을 가리키게 되고 그곳의 데이터에 접근하면 프로그램이 망가질 수 있습니다.
int* ptr = 0;	//바로 가리킬 곳이 없다면 0(Null)으로 초기화 해주자.
int* ptr = NULL;	// 0과 NULL은 같은 의미입니다.

2) 포인터에는 주소값만 저장할 수 있습니다

  • 포인터에는 '값'을 저장할 수 없습니다.
    (참고로 배열의 이름은 "주소값 상수"를 의미했습니다.)

3) 의미들을 헷갈리지 맙시다

  • 생성에 있어서 * 는 포인터 자료형을 의미합니다.

  • 접근에 있어서 * 는 그 주소에 담겨있는 값을 의미합니다.

4) 연산순서들을 조심합시다.



주소

주소를 구하는 방법

1) 일반변수: & (Ampersand)연산자

&a;

위와 같이 &(Ampersand) 연산자를 사용하면 해당 변수의 주소값을 반환 받을 수 있습니다.

2) 포인터

int a;
int* a = &a;

a;

이미 포인터에 주소가 저장되어 있을 경우 그냥 이름만 사용하면 됩니다.

3) 배열의 이름

char a[] = "string";

배열의 이름은 첫번째 원소([0]번 인덱스)의 주소값을 의미합니다.

( 위의 배열의 경우 a는 's'의 주소를 의미합니다.)

  • 즉, 배열의 이름은 "상수"가 됩니다.
    (a = 3과 같은 코드는 상수에 상수를 저장하는, 오류를 일으키는 문장입니다.)

  • 즉, 변수와는 다르게 a가 그 값을 나타내지 않습니다.


주소의 연산

1) 주소간 덧셈/뺄셈

int a[10] = {0};
int* a1ptr = &a[1];
int* a6ptr = &a[6];

cout << a6ptr - a1ptr;	// 5
  • 위의 경우 포인터간 뺄셈이므로 5*4 (인덱스차이*자료형 크기)라고 생각할 수 있습니다. 그러나 포인터간 뺄셈은 그 자료형의 크기까지 고려한다고 생각합시다.
  • 즉, 5가 출력되게 됩니다.

2) 주소와 수의 덧셈/뺄셈

float b[10] = {0.0};

cout << b+3;	//b[3]

마찬가지로 자료형의 크기까지 고려하여야 합니다. 이때 b는 &b[0]을 의미하는 것이므로 b+3b[3]을 의미하게 됩니다.

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;	//target의 주소 반환

3) 이항연산: 비트단위 연산

target & 3;

call by value
call by reference


profile
github로 이전 중... (https://uijinee.github.io/)

0개의 댓글