C++

포인터의 의미와 사용 이유

yoooon1212 2024. 12. 6. 21:51

포인터

주소를 저장하는 변수

 

"주소"

모든 것에는 주소가 있음

메모리에도 메모리 주소가 있음

 

기본적으로 변수를 만들면, 컴퓨터가 알아서 해당 메모리 주소를 찾아가서 변수를 저장함

개발자가 해당 변수의 주소를 알려고 한다면?

예시) 변수 a의 주소로 가세요 = 7349347 번지로 가세요

 

코드 예시

#include <iostream> 

using namespace std;

int main() {

	int a = 10;

	cout << "변수 a의 값 : " << a << endl;
	cout << "변수 a의 주소값 : " << &a << endl; // &a : 메모리 공간의 시작 주소

	return 0;
}

 

출력 결과

int = 4 byte 

메모리 중에 4 byte를 저장할 수 있는 공간 중에

알아서 골라서 들어감(랜덤)

 

주소값이 변하는 이유

 

컴퓨터 메모리 공간에 랜덤하게 저장되기 때문이다

 

 

 

 

포인터 변수

int *a;

 

포인터 변수가 어떤 메모리의 주소를 저장함

= 저장한 주소에 해당하는 메모리를 포인터 변수가 가리킴 ( &a: 메모리 공간의 시작 주소를 포인터 변수가 가리킴)

 

 

코드 예시(가장 숙지해야 할 부분!!!)

#include <iostream> 

using namespace std;

int main() {

	int a = 10;
	int* b = &a; // a의 주소값을 넣음

	cout << "a의 주소값은 : " << &a << endl;
	cout << "a의 주소값은 : " << b << endl;
	cout << "a의 값은 : " << a << endl;
	cout << "a의 값은 : " << *b << endl;

	return 0;
}

 

출력 결과

 

 

 

포인터 변수를 통한 역참조 가능

int a = 3;
int *b = &a;

--> *b는 a를 의미함

--> *b는 포인터 b가 가리키고 있는 변수 a

--> 즉, *b는 3이 됨

 

int *b 
int* b
--> 문법 상 문제 없으나 하위 코드를 자주 사용함.

 

 

코드 예시

#include <iostream> 

using namespace std;

int main() {

	int a = 10;
	int* b = &a; // a의 주소값을 넣음

	cout << "a의 주소값은 : " << &a << endl;
	cout << "a의 주소값은 : " << b << endl;
	return 0;
}

 

출력 결과

 

 

* 을 사용할 경우 

1. 포인터 변수를 선언할 때
2. 역참조할 때

 

 

메모리 크기

- 포인터 변수의 크기는 언제나 같음 (주소값을 가리키기 때문)

정수형, 실수형(float 4byte/ double 8byte), 문자형

 

- 컴파일러 종류에 따른 포인터 변수

(32bit) 4byte, (64bit) 8byte

 

 

코드 예시

#include <iostream> 

using namespace std;

int main() {

	int a;
	float b;
	double c;

	int* d;
	float* e;
	double* f;

	cout << "int형 크기 : " << sizeof(a) << endl;
	cout << "float형 크기 : " << sizeof(b) << endl;
	cout << "double형 크기 : " << sizeof(c) << endl;
	cout << "int포인터형 크기 : " << sizeof(d) << endl;
	cout << "float포인터형 크기 : " << sizeof(e) << endl;
	cout << "double포인터형 크기 : " << sizeof(f) << endl;

	return 0;
}

 

출력 결과

int, float 형은 실수, 정수 이런 것을 저장함.

반면, 포인터는 메모리 공간에 대한 주소를 저장함.

 

그 자료형이 저장된 메모리 공간을 가리키기 때문에 

포인터의 메모리 크기는 8 byte로 똑같다.

 

 

 

포인터가 필요한 이유

 

코드 예시

그냥 변수로는 값을 바꿀 수 없다.

#include <iostream> 
using namespace std;

void swap(int x, int y);

int main() {

	int a = 10;
	int b = 5;

	cout << "a의 값은 " << a << "입니다" << endl;
	cout << "b의 값은 " << b << "입니다" << endl;

	swap(a, b);

	cout << "a의 값은 " << a << "입니다" << endl;
	cout << "b의 값은 " << b << "입니다" << endl;

	return 0;
}

void swap(int x, int y) {
	int temp;
	temp = x;
	x = y;
	y = temp;

	return;
}

 

출력 결과

 

swap  함수에서는 x = 5, y = 10으로  값이 바꼈지만

함수는 지역변수라 함수 호출이 끝나면

메모리 공간이 사라지고, 

a, b는 변경 없이 그대로 출력된다.

 

이 문제를 해결하기 위해 포인터를 사용해야 한다.

 

x, y에 포인터를 사용하여 

각각 포인터가 가리키는 메모리 주소에 접근해서

값을 바꿔줘야 한다.

 

 

 

코드 예시(포인터 사용)

포인터 변수를 사용하여 주소값 자체를 바꿔줌.

#include <iostream> 
using namespace std;

void swap(int* x, int* y);

int main() {

	int a = 10;
	int b = 5;

	cout << "a의 값은 " << a << "입니다" << endl;
	cout << "b의 값은 " << b << "입니다" << endl;

	swap(&a, &b); // 주소값(함수의 파라미터와 맞춰야 함)

	cout << endl << "a의 값은 " << a << "입니다" << endl;
	cout << "b의 값은 " << b << "입니다" << endl;

	return 0;
}

void swap(int* x, int* y) {
	int temp;

	temp = *x; // temp에 x가 가리키는 값을 넣어줌
	*x = *y;
	*y = temp;

	return;
}

 

출력 결과

 

 

- 함수는 각각 변수의 메모리 영역이 다르므로 서로에게 영향을 줄 수 없음.

- 함수 호출을 하면서 값을 전달하는 방식을 값에 의한 호출(Call by Value)라고 함.

예) 값에 의한 호출 : a = 10, b = 5 -> x = 10, y = 5 (값만 넘어옴)

=> 이 한계를 포인터로 해결할 수 있다!

 

- 함수와 값을 주고 받을 때 포인터를 사용하여 주소 값을 전달하면 변수의 메모리 주소가 전달됨.

- 함수 안에서 해당하는 메모리가 가리키는 값을 변경할 수 있음.

- 주의!! 포인터는 가리키는 변수의 형태가 같을 때만 대입해야 한다!!

 

 

 


 

 

배열에서의 포인터

 

포인터 배열

- 배열의 각 요소는 주소를 가짐

- int의 경우 4 byte씩 늘어남

- 배열의 이름은 주소

- a는 &a[0]과 같음

- 배열의 이름은 배열의 첫 번째 주소

 

 

코드 예시

배열의 이름이 배열의 첫 번째 주소와 동일하다

#include <iostream> 
using namespace std;

void swap(int* x, int* y);

int main() {

	int ary[5] = { 1,2,3,4,5 };

	for (int i = 0; i < 5; i++) {
		cout << "배열 ary[" << i << "]번째 주소는 " << &ary[i] << "입니다" << endl;
	}

	cout << "배역의 이름 ary는 배열의 주소와 같습니다 : " << ary << endl;

	return 0;
}

 

출력 결과

주소의 마지막 숫자가 4 byte씩 커지는 이유는

배열은 메모리 공간에서 이어서 저장되기 때문이다

 

ary(배열의 이름)과  ary[0]의 주소(&ary[0])는 동일하다

 

 

 

- 배열의 2번째, 3번째... 요소도 포인터를 통하여 가리킬 수 있음

- +1

=> 포인터에서의 +1은 주소 단위 하나를 더하는 것이다.

=> 주소의 단위가 정수라면, +1은 4 byte를 더하게 된다.

예) 첫번째 주소 = p일 경우 p , p+1, p+2 ...(4 byte씩 옆으로 한 칸씩 간다)

 

코드 예시

#include <iostream> 
using namespace std;

int main() {

	int ary[5] = { 1,2,3,4,5 };
	int* b = ary; // 배열의 첫번째 주소값
	// b = &ary[0]

	cout << "포인터 b가 가리키고 있는 값은 " << *b << "입니다" << endl; // 1

	b = b + 1; // 4 byte 만큼 옆으로 갔고, 다음 요소인 2를 가리킴
	cout << "포인터 b가 가리키고 있는 값은 " << *b << "입니다" << endl; // 2

	b = b + 1; // 4 byte 만큼 옆으로 갔고, 다음 요소인 3를 가리킴
	cout << "포인터 b가 가리키고 있는 값은 " << *b << "입니다" << endl; // 3

	b = b + 2; // 4 byte 만큼 2칸 옆으로 갔고, 2칸 다음 요소인 5를 가리킴
	cout << "포인터 b가 가리키고 있는 값은 " << *b << "입니다" << endl; // 5

	return 0;
}

 

출력 결과

 

 

 

2차원 배열 주소

int a[2][3] = {1,2,3,4,5,6}

 

a[0][0] = a[0] = a --> 주소값이 다 같음

a[0][1]

a[0][2]

a[1][0] = a[1]

a[1][1]

a[1][2]

 

 

2차원 배열 주소 연산

위 코드에 +1

a : 배열의 이름, 배열의 첫번째 주소

a + 1 : a[1]의 주소 => a는 a[0]의 주소라서 +1 시 행이 넘어감 => a[1]의 주소

a[1] + 1 : a[1][1]의 주소 => a[1] 은 a[1][0] 이고 +1 시 한 칸(열)이 넘어감 => a[1][1]의 주소

 

 

코드 예시

+ 1 위치 파악하기

#include <iostream> 
using namespace std;

int main() {

	int a[2][3] = { 1,2,3,4,5,6 };
	cout << "배열 a 시작 메모리 주소 : " << a << endl;
	cout << "a[0][0]의 주소 : " << &a[0][0] << endl;
	cout << "a[0]의 주소 : " << &a[0] << endl;

	cout << endl << "a[1][0]의 주소 : " << &a[1][0] << endl;
	cout << "a[1]의 주소 : " << &a[1] << endl;

	cout << endl << "배열 a의 크기 : " << sizeof(a) << endl; // 6개 x 4 byte
	cout << "배열 a[0]의 크기 : " << sizeof(a[0]) << endl; // 1줄 당 3칸 x 4 byte 
	cout << "배열 a[1]의 크기 : " << sizeof(a[1]) << endl; // 1줄 당 3칸 x 4 byte 

	cout << endl << "배열 a 시작 메모리 주소 : " << a << endl;
	cout << "a + 1의 주소 : " << a + 1 << endl; // a[1]의 주소와 동일
	cout << "a + 2의 주소 : " << a + 2 << endl;

	cout << endl << "a[0] + 1의 주소 : " << a[0] + 1 << endl; // a[0][1]의 주소와 동일
	cout << "a[0][1]의 주소 : " << &a[0][1] << endl;

	cout << endl << "a[1] + 1의 주소 : " << a[1] + 1 << endl; // a[1][1]의 주소와 동일
	cout << "a[1][1]의 주소 : " << &a[1][1] << endl;
	
	return 0;
}

 

출력 결과

 

 

이중 포인터(포인터의 포인터)

포인터 변수의 주소를 저장하는 변수

 

코드 예시

값 / 주소값 / 역참조값 / 역참조의 역참조값

#include <iostream> 
using namespace std;

int main() {

	int a = 10;
	int* b = &a; // a의 주소값
	int** c = &b; // b의 주소값

	cout << "a의 값 : " << a << endl; // a의 값
	cout << "a의 주소값 : " << &a << endl; // a의 주소값
	cout << "b의 값 : " << b << endl; // a의 주소값
	cout << "b의 주소값 : " << &b << endl; // b의 주소값
	cout << "c의 값 : " << c << endl; // b의 주소값
	cout << "c의 주소값 : " << &c << endl; // c의 주소값

	cout << "b의 역참조값 : " << *b << endl; // b가 가리키고 있는 값(a의 값)
	cout << "c의 역참조값 : " << *c << endl; // c는 b를 가리키고, b가 가리키고 있는 값(b의 값 = a의 주소값)
	cout << "c의 역참조의 역참조값 : " << **c << endl; // c는 b를 가리키고, b는 a를 가리키므로 b가 가리키고 있는 값(a의 값)
	
	return 0;
}

 

출력 결과

 

 

실습 문제 1

포인터의 값을 바꾸어 주는 함수를 이중 포인터를 사용하여 만드세요
#include <iostream> 
using namespace std;

void swap_ptr(int** p1, int** p2);

int main() {

	int a = 3;
	int b = 5;
	int* ap = &a; // ap는 a의 주소값
	int* bp = &b; // bp는 b의 주소값

	cout << "a의 주소값 : " << ap << endl;
	cout << "a의 값 : " << *ap << endl;
	cout << "b의 주소값 : " << bp << endl;
	cout << "b의 값 : " << *bp << endl;

	swap_ptr(&ap, &bp); // 매개변수: ap의 주소값, bp의 주소값
	cout << endl << "변경된 결과입니다" << endl;
	cout << "a의 주소값 : " << ap << endl;
	cout << "a의 값 : " << *ap << endl;
	cout << "b의 주소값 : " << bp << endl;
	cout << "b의 값 : " << *bp << endl;

	return 0;
}

void swap_ptr(int** p1, int** p2) {
	// p1은 ap의 주소값, p2는 bp의 주소값
	int* tp; // p1은 이중 포인터라서 주소를 가리키는 포인터 temp로 만든거임

	// p1과 p2가 가리키는 값을 역참조함
	tp = *p1; // 이중 포인터의 한 번 역참조(a의 주소값)
	*p1 = *p2; // b의 주소값(*p2)을 a의 주소값(*p1)에 담음
	*p2 = tp; // tp에 담겨있던 a의 주소값(*p1)을 b의 주소값(*p2)에 담음
}

 

 

 

출력 결과

 

 

 

실습 문제 2

배열 a를 생성하여 1,2,3,4,5를 저장하고 포인터 b를 통해 배열 a의 값을 6,7,8,9,10으로 변경한 후 다양한 방법(배열/포인터 사용)으로 출력하세요.
#include <iostream> 
using namespace std;

int main() {

	int a[5] = { 1,2,3,4,5 };
	int* b = a; // 배열의 첫번째 주소값

	cout << "기본 배열 a의 값 출력 : ";
	for (int i = 0; i < 5; i++) {
		cout << a[i] << " "; // 1 2 3 4 5
	}
	cout << endl;

	for (int i = 0; i < 5; i++) {
		*b = *b + 5; // 현재 가리키고 있는 값(1) + 5
		b = b + 1; // 포인터가 옆 칸(a[1])을 가리키도록 함
	} // 맨 끝 칸을 가리킴(a[5])
	b = a; // 맨 앞 칸을 가리킴(a[0])

	cout << "변경된 배열의 값 : ";
	for (int i = 0; i < 5; i++) {
		cout << b[i] << " ";
	}
	cout << endl;

	cout << "변경된 배열의 값 : ";
	for (int i = 0; i < 5; i++) {
		cout << *(b + i) << " ";
	}
	cout << endl;

	return 0;
}

 

출력 결과

 

 

 

 

실습 문제 3

수 3개를 입력 받아 오름차순으로 정렬하여 출력하는 프로그램을 함수를 사용하여 만드세요.
#include <iostream> 
using namespace std;

void line_down(int* x, int* y, int* z); // 그냥 바꾸면 값이 바꿔지지 않기 때문에 포인터 사용
void swap(int* x, int* y);

int main() {

	int max, mid, min;

	cout << "수 3개를 차례대로 입력해주세요";
	cin >> min >> mid >> max;

	line_down(&min, &mid, &max); // 주소값

	cout << "정렬 결과는 " << min << " " << mid << " " << max;
	return 0;
}

void line_down(int* x, int* y, int* z) {
	// 맨 처음 가리키고 있는 값이 두 번째 값보다 크다면
	if (*x > *y)
		swap(x, y);
	if (*x > *z)
		swap(x, z);
	if (*y > *z)
		swap(y, z);
}

void swap(int* x, int* y) {
	int temp = *x;
	*x = *y;
	*y = temp;
}

 

출력 결과

 

 

 

실습 문제 4

길이가 6인 int형 배열 a를 선언하고 1 ~ 6으로 초기화.
그 다음 이 배열을 저장된 값을 거꾸로 6,5,4,3,2,1이 되도록 변경하라.
배열 앞과 뒤를 가리키는 포인터 변수 두 개를 활용하세요.
#include <iostream> 
using namespace std;

void line_down(int* x, int* y, int* z); // 그냥 바꾸면 값이 바꿔지지 않기 때문에 포인터 사용
void swap(int* x, int* y);

int main() {

	int a[6] = { 1,2,3,4,5,6 };
	int* front = &a[0];
	int* back = &a[5];
	int temp;

	cout << "배열 a의 현재 내용" << endl;
	for (int i = 0; i < 6; i++) {
		cout << a[i] << " ";
	}
	cout << endl;

	// 3개(1과 6을 바꿈, 2와 5를 바꿈, 3과 4를 바꿈)
	for (int i = 0; i < 3; i++) {
		temp = *front;
		*front = *back;
		*back = temp;
		front += 1; // 한 칸씩 앞으로 가도록
		back -= 1; // 한 칸씩 뒤로 가도록
	}

	cout << "배열 a의 변경된 내용" << endl;
	for (int i = 0; i < 6; i++) {
		cout << a[i] << " ";
	}
	cout << endl;

	return 0;
}

 

출력 결과

 

 

 


 

 

함수 포인터

 

어떤 함수를 호출할지 알 수 없고, 프로그램이 실행될 때 결정된다면?

함수 포인터를 사용한다.

 

함수 이름 = 함수의 주소

 

 

코드 예시

#include <iostream> 
using namespace std;

int sum(int a, int b);

int main() {

	int (*fp)(int, int); // 출력(int)과 입력(int a, int b)이 동일해야 함.
	// int* ap;
	fp = sum; // 함수의 이름이 함수의 시작 주소이다(함수 포인터의 함수 주소)
	cout << "fp : " << fp << endl;
	cout << "sum : " << sum << endl;

	//int result = sum(10, 20); -> 아래 코드와 동일함
	int result = fp(10, 20);

	cout << "결과 : " << result << endl;
	return 0;
}

int sum(int a, int b) {
	return a + b;
}

 

출력 결과

 

 

 

실습 문제 1

사용자의 선택에 따라 두수의 합, 곱, max값을 구하는 프로그램을 함수 포인터를 사용해서 만드세요.

함수 포인터를 왜 써야 하는가?
사용자의 입력을 받기 전까지는 어떤 함수를 사용해야 하는지 알 수 없음
#include <iostream> 
using namespace std;

int _sum(int a, int b);
int _mul(int a, int b);
int _max(int a, int b);
int _mul(int a, int b);
void _select(int (*fp)(int, int)); // 입력: 함수 포인터

int main() {
	
	int sel;

	cout << "1:합 / 2:곱 / 3:비교(큰 값 구하기)" << endl;
	cin >> sel; // 사용자의 입력 받기
	// 사용자의 입력을 받기 전까지는 어떤 함수를 사용해야 하는지 알 수 없음
	// -> 함수 포인터를 사용하여 함수 하나만 구현해둬도 그때그때 올바른 코드를 고를 수 있도록 함

	if (sel == 1)
		// _select 함수의 매개변수인 int (*fp)(int, int) 여기에 _sum이 들어감
		_select(_sum);
	else if (sel == 2)
		_select(_mul);
	else if (sel == 3)
		_select(_max);
	return 0;
}

int _sum(int a, int b) {
	return (a + b);
}

int _mul(int a, int b) {
	return (a * b);
}

int _max(int a, int b) {
	if (a > b)
		return a;
	else
		return b;
}

void _select(int (*fp)(int, int)) {
	int a, b, result;

	cout << "두 정수를 입력하세요";
	cin >> a >> b;
	result = fp(a, b);
	cout << "결과 : " << result;
}

 

출력 결과