포인터
변수는 값을 담지만 포인터는 값을 담지 않고 변수의 주소값을 담는다. 메모리의 주소값을 알면 해당 공간을 직접 제어할 수 있다.
일반 변수에서 대입 연산(=)을 하면 변수에 있는 값이 그대로 복사가 된다.
하나의 변수를 다른 변수에 대입하면 새로운 메모리 공간에 동일한 값이 복제가 된다.
복사 이후에 두 변수는 서로 독립적인 공간을 가지므로, 한 쪽 값을 변경해도 다른 쪽에는 영향이 없다.
복사에는 복사 비용이 있는데 복사할 데이터의 크기가 커질수록 복사 비용이 커진다.
#include <iostream>
using namespace std;
int main()
{
const int SIZE = 1000000; // 1,000,000개의 정수 (약 4MB)
int arr1[SIZE];
int arr2[SIZE];
// 배열 복사 (매우 높은 복사 비용)
for (int i = 0; i < SIZE; i++)
{
arr2[i] = arr1[i];
}
cout << "배열 복사 완료" << endl;
return 0;
}
//위 코드가 1000번 사용되면 복사비용이 4GB가 된다.
C++에서는 값을 직접 복사하는 방식 대신 변수의 주소를 가리켜서 동일한 데이터에 접근할 수 있도록 할 수 있게 해준다.
그 기능이 바로 포인터(*) 이다.
포인터의 모든 연산은 주소값과 관련되어 있다. 아래의 2가지 연산은 오직 포인터 변수만 가능하다.
1. 변수의 주소값을 담을 수 있습니다.
2. 담고 있는 주소값에 해당되는 메모리에 있는 값을 읽거나 수정할 수 있습니다.
포인터 변수의 구성 요소
포인터를 사용하여 변수 주소값의 시작 주소는 알았는데 변수의 타입에 따라 크기가 모두 다르다보니 끝도 알아야 한다.
예시
메모리에 변수 3개가 있습니다. x는 정수형이고(4바이트) y와 z는 문자형(1바이트)입니다.
포인터 변수가 2개 있고, 하나는 x의 시작 주소 200, 하나는 y의 시작 주소 204를 담고 있습니다.
시작 주소가 200인 것은 알았지만 정수 변수 X의 크기(4바이트) 정보는 알 수 없습니다. X에 저장된 값 3을 제대로 읽으려면 시작 주소부터 4바이트를 읽어야 하므로, 포인터에는 변수의 크기 정보를 파악할 수 있는 타입 정보가 함께 필요합니다.
따라서 포인터는 주소 값 뿐만 아니라 가리키는 변수의 타입도 필요합니다.
1. 변수의 시작 주소
2. 변수의 타입(변수의 크기를 알기 위해 필요함)
C++에서는 포인터 변수는 선언 시 데이터 형 뒤에 *를 붙여 포인터임을 표시한다
예를 들어, int* ptr이라고 선언하면 ptr은 정수를 가리키는 포인터가 된다.
변수의 시작 주소 값을 담고, 해당 변수 타입이 정수라는 걸 알 수 있으므로 제대로 값을 읽어올 수 있게 된다.
예시. 포인터연산과 데이터 크기
#include <iostream>
using namespace std;
int main()
{
int x = 3;
char y = 'A';
int* ptr1 = &x;
char* ptr2 = &y;
cout << "ptr1: " << ptr1 << ", ptr1 + 1: " << ptr1 + 1 << endl;
cout << "ptr2: " << (void*)ptr2 << ", ptr2 + 1: " << (void*)(ptr2 + 1) << endl;
//(void*)은 변수가 문자형일경우 주소값을 출력하려면 앞에 붙여서 캐스팅해야됨.
return 0;
}
예시. 결과
int포인터에 +1을 할 경우 int의 데이터 크기인 4byte만큼 증가함. (끝4자리 F8E4 -> F8E8)
char포인터에 +1을 할 경우 char의 데이터 크기인 1byte 만큼 증가함. (끝4자리 F904 ->F905)
포인터 변수의 역참조
포인터를 활용하려면 해당 주소에 있는 실제 값을 읽고 수정할 수 있어야 합니다.
포인터는 주소를 다루는 특성 때문에 산술 연산 역시 주소를 제어하는 방식으로 동작합니다.
이를 위해 역참조 연산자(*)을 사용합니다.
A라는 포인터 변수가 있다면, A를 출력하면 주소값이 나오고, *A를 출력하면 해당 메모리에 있는 값이 나옵니다.
아래 그림을 보면 정수형 포인터 변수 ptr과 정수형 변수 x가 선언되었고, x의 값은 3입니다.
ptr = &x 즉 ptr이라는 포인터 변수에 x의 주소값을 저장합니다. 따라서 x의 시작 주소 200이 저장됩니다.
ptr을 출력하면 200이 출력됩니다. 내부에 주소값인 200이 저장되어 있기 때문입니다.

해당 메모리의 값을 출력하는 것뿐 아니라, 수정할 수도 있습니다.
*ptr = 40하면 ptr이 담고 있는 주소값으로 가서 값을 40으로 변경합니다.
이번 그림에는 char형 변수 y가 추가되었습니다. 이전에 ptr=&x의 경우엔 문제가 없었습니다. 그 이유는 x도 정수형 변수이고 ptr도 정수형 포인터이므로 타입이 일치하기 때문입니다.
하지만 ptr=&y의 경우엔 상황이 다릅니다. y는 문자형이기 때문에 주소값을 담으려고 시도하면 에러가 발생합니다.
예시 1. 포인터와 변수의 관계
#include <iostream>
using namespace std;
int main()
{
int x = 3; // 정수형 변수 x 선언
int* ptr = &x; // 포인터 ptr에 x의 주소 저장
cout << "x의 값: " << x << endl;
cout << "x의 주소: " << &x << endl;
cout << "ptr의 값(저장된 주소): " << ptr << endl;
cout << "*ptr이 가리키는 값: " << *ptr << endl;
return 0;
}
예시 1. 결과
예시 2. 포인터를 이용하여 값 변경
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int* p = &a; // a의 주소 저장
cout << "변경 전 a: " << a << endl;
*p = 20; // 포인터를 이용하여 값 변경
cout << "변경 후 a: " << a << endl;
return 0;
}
예시 2. 결과
배열 이름의 의미
배열 이름은 배열의 시작 주소를 가지고 있다.
배열이 인덱스를 통한 임의 접근이 가능한 이유는 메모리가 연속적으로 할당되기 때문이다.
배열의 시작 주소로 부터 메모리가 연속적으로 할당 되어 배열을 관리할 수 있기 때문에 배열 자체가 담고 있는 시작 주소를 변경할 수 없다.(시작 주소가 변경이 되면 배열로써의 기능이 안됨.)
이러한 이유로 변수는 값을 저장한 후 다른 값으로 변경해도 문제가 없었지만 배열의 시작 주소는 포인터에 담으면 다른 주소값으로 변경이 불가능하다.(상수 포인터가 된다.)
배열 이름은 주소값을 담고 있기 때문에 기존 포인터와 마찬가지로 *연산자를 통해 해당 주소의 값을 읽고 변경도 가능하다.
위 그림에서는 배열의 첫번째 값, buf[0]만 예시를 들었는데
만약 배열의 두번째, 세번째 값에 접근하고 싶다면?
buf[0] = 'X', *buf = 'X', *(buf + 0) = 'X'
이것을 활용하여
buf[1] = *(buf+1) = 'A', buf[2] = *(buf+2) = 'A',즉 buf[k] = *(buf + k) 이다.
처럼 쓸 수 있다. 중요하기 때문에 익혀두는것이 좋다.
예시. 포인터를 이용한 배열 접근
#include <iostream>
using namespace std;
// 배열 이름을 포인터처럼 활용하는 예제
int main()
{
int arr[3] = {100, 200, 300};
int *ptr = arr; // 배열 이름은 배열의 시작 주소를 가리킴
cout << "*ptr: " << *ptr << " " << ptr[0] << endl; // 100 (arr[0] 값)
cout << "*(ptr+1): " << *(ptr+1) << " " << ptr[1] << endl; // 200 (arr[1] 값)
cout << "*(ptr+2): " << *(ptr+2) << " " << ptr[2] << endl; // 300 (arr[2] 값)
return 0;
}
포인터 ptr에 배열첨자 연산자 [ ]를 사용할 수 있다.
위 코드를 통해 *(ptr+1) 과 ptr[1] 이 동일한 표현이란 것을 알 수 있다.
예시. 결과
포인터 배열과 배열 포인터
포인터 배열은 포인터를 원소로 갖는 배열입니다. 예를 들어, int* ptrArr[4];는 크기가 4이고, 각 원소가 int*인 배열입니다.
배열 포인터는 배열 전체를 가리키는 포인터입니다. 즉 단일 변수가 아닌 배열 통째를 가리키는 변수입니다. 보통 다차원 배열을 제어할 때 많이 사용합니다.
포인터 배열은 포인터로 이루어진 배열, 배열 포인터는 배열 자체가 포인터.
예시 1. 포인터 배열
#include <iostream>
using namespace std;
// 포인터 배열: 포인터를 원소로 갖는 배열
int main()
{
int a = 10, b = 20, c = 30;
int* ptrArr[3] = { &a, &b, &c }; // 포인터 배열 선언 및 초기화
// 포인터 배열을 이용하여 값 출력
cout << "*ptrArr[0]: " << *ptrArr[0] << endl; // 10
cout << "*ptrArr[1]: " << *ptrArr[1] << endl; // 20
cout << "*ptrArr[2]: " << *ptrArr[2] << endl; // 30
return 0;
}
예시 1. 결과
예시 2. 배열 포인터
#include <iostream>
using namespace std;
// 배열 포인터: 배열 전체를 가리키는 포인터
int main()
{
int arr[3] = { 100, 200, 300 };
int (*ptr)[3] = &arr; // 배열 포인터 선언
// 배열 포인터를 이용하여 배열 요소 접근
cout << "(*ptr)[0]: " << (*ptr)[0] << endl; // 100
cout << "(*ptr)[1]: " << (*ptr)[1] << endl; // 200
cout << "(*ptr)[2]: " << (*ptr)[2] << endl; // 300
return 0;
}
// 위 문법은 2차원 배열에서 사용하지만 자주 사용할 일은 없을것이다.
// 이런게 있구나 하고만 넘어가는 정도
예시 2. 결과
예시 3. 포인터 배열 vs 배열 포인터
#include <iostream>
using namespace std;
// 포인터 배열과 배열 포인터의 차이점 확인
int main()
{
int x = 1, y = 2, z = 3;
int* ptrArr[3] = { &x, &y, &z }; // 포인터 배열 (각 원소가 int* 타입)
int arr[3] = { 10, 20, 30 };
int (*ptr)[3] = &arr; // 배열 포인터 (배열 전체를 가리킴)
// 포인터 배열을 통한 접근
cout << "*ptrArr[0]: " << *ptrArr[0] << endl; // 1
cout << "*ptrArr[1]: " << *ptrArr[1] << endl; // 2
cout << "*ptrArr[2]: " << *ptrArr[2] << endl; // 3
// 배열 포인터를 통한 접근
cout << "(*ptr)[0]: " << (*ptr)[0] << endl; // 10
cout << "(*ptr)[1]: " << (*ptr)[1] << endl; // 20
cout << "(*ptr)[2]: " << (*ptr)[2] << endl; // 30
return 0;
}
예시 3. 결과
포인터 연산
포인터는 주소값을 담습니다. 따라서 산술 연산 시, 일반적인 수치 연산이 아닌 메모리 주소의 이동으로 해석됩니다.
ptr + 1을 하게 되면, ptr이 담고 있는 주소값에 대한 연산이 수행됩니다.
포인터의 타입에 따라, 해당 타입 변수의 크기만큼 담고 있는 주소를 증가시킵니다.
1. ptr + 1
ptr + 1을 실행하면 ptr이 가리키는 주소에서 한 단위 메모리 주소가 이동합니다.
이 한 단위라는 것은 포인터 자료형 크기에 따라 결정됩니다.
아래 그림을 보면 ptr은 정수형 포인터이기 때문에 ptr + 1을 하면 22(주소값) + 4(int의 크기)인 26이 출력된다.
만약 int형이 아니고 double형이였다면 8이 더해진다.
2. (*ptr) + 1
ptr이 가리키는 변수의 값을 1 증가시킵니다.
아래 그림을 보면 *ptr이 주소값 22의 값을 가져오는 것이므로 7 에 +1 을 하여 8이 출력된다.
3. *(ptr + 1)
*(ptr + 1)은 ptr[1]과 동일하다.
일반화하면 *(ptr + i)는 ptr[i]와 동일합니다.
아래 그림을 보면 현재 ptr주소에서 한 단위 이동한 후 해당 값인 3을 출력합니다.
포인터 연산의 경우 +로 예시로 들었지만 -도 가능하다. 하지만 *와 /는 불가능 하다.
레퍼런스
포인터를 사용하면 주소값을 직접 다루어야 하므로 복잡해질 수 있습니다.
이 문제를 완화하기 위해 C++에서는 변수에 또 다른 이름을 부여하는 ‘레퍼런스’ 문법을 도입했습니다.
레퍼런스는 일반 변수와 거의 동일하게 사용할 수 있습니다. 그러나 내부적으로는 해당 변수를 직접 가리켜 주는 역할을 합니다.
레퍼런스는 특정 변수에 대한 별명을 부여하는 것입니다.
한 번 특정 변수의 레퍼런스를 연결하면, 이후로는 마치 그 변수가 두 개의 이름을 갖는것과 같습니다.
선언 방법은 데이터형 뒤에 &를 붙이는 겁니다.
예를 들어, int& ref = var; 처럼 사용할 수 있습니다. 이렇게 하면 ref의 값 변경 시 var의 값도 변경 됩니다.
예시. 기본적인 레퍼런스 사용
#include <iostream>
using namespace std;
// 레퍼런스를 활용하여 변수에 별명을 부여하는 예제
int main()
{
int var = 10;
int& ref = var; // var의 레퍼런스 선언
cout << "초기 값:" << endl;
cout << "var: " << var << endl; // 10
cout << "ref: " << ref << endl; // 10
ref = 20; // ref를 변경하면 var도 변경됨
cout << "ref 값을 변경한 후:" << endl;
cout << "var: " << var << endl; // 20
cout << "ref: " << ref << endl; // 20
return 0;
}
예시. 결과
포인트와 레퍼런스의 차이점
1. 선언과 초기화 시점이 다릅니다.
포인터는 선언 후, 나중에 = 연산자를 통해 가리킬 대상을 변경할 수 있습니다.
반면에 레퍼런스는 선언과 동시에 초기화해야 하며, 초기화 이후에는 다른 대상에 재연결할 수 없습니다.
2. 레퍼런스는 항상 다른 변수와 연결되어 있기 때문에 NULL이라는 게 없습니다.
반면에 포인터는 유효한 대상이 없음을 나타내기 위해 NULL 혹은 nullptr을 가질 수 있습니다.
3. 간접 참조 문법의 유무입니다.
포인터는 주소값을 담으므로 접근할 때는 * 연산을 사용하고 주소를 가져올 때는 & 연산을 사용합니다.
하지만 레퍼런스는 변수 자체의 별명이므로 일반 변수와 연산하는 방법이 동일합니다.
예시 1. 포인터와 레퍼런스 선언 및 초기화 차이
#include <iostream>
using namespace std;
// 포인터와 레퍼런스의 선언 및 초기화 차이
int main()
{
int a = 10, b = 20;
// 포인터는 선언 후 나중에 다른 변수를 가리킬 수 있음
int* ptr = &a; // 포인터 선언 및 초기화
ptr = &b; // 포인터가 다른 변수를 가리킬 수 있음
// 레퍼런스는 선언과 동시에 초기화해야 함
int& ref = a;
// ref = b; // ❌ 오류! 레퍼런스는 다른 변수에 재할당할 수 없음
cout << "포인터 사용:" << endl;
cout << "*ptr: " << *ptr << endl; // 20 (포인터가 b를 가리키고 있음)
cout << "레퍼런스 사용:" << endl;
cout << "ref: " << ref << endl; // 10 (a를 가리키고 있음)
return 0;
}
예시 1. 결과
예시 2. 레퍼런스는 선언과 동시에 초기화, 포인터는 선언 후 초기화 가능
#include <iostream>
using namespace std;
// 포인터는 NULL을 가질 수 있지만, 레퍼런스는 반드시 변수와 연결되어야 함
int main()
{
int a = 42;
int* ptr = nullptr; // 포인터는 nullptr이 가능
ptr = &a; // 이후에 a를 가리키도록 설정 가능
// int& ref; // ❌ 오류! 레퍼런스는 반드시 선언과 동시에 초기화해야 함
int& ref = a; // 올바른 선언 방식
cout << "포인터 사용:" << endl;
cout << "ptr이 가리키는 값: " << *ptr << endl; // 42
cout << "레퍼런스 사용:" << endl;
cout << "ref: " << ref << endl; // 42
return 0;
}
예시 2. 결과
예시 3. 포인터와 레퍼런스의 간접 참조 문법 차이
#include <iostream>
using namespace std;
// 포인터와 레퍼런스의 간접 참조 문법 비교
int main()
{
int x = 5;
int* ptr = &x; // 포인터 선언
int& ref = x; // 레퍼런스 선언
cout << "포인터 접근 방법:" << endl;
cout << "x: " << x << endl; // 5
cout << "*ptr: " << *ptr << endl; // 5 (포인터를 통한 간접 참조)
cout << "ptr: " << ptr << endl; // x의 주소값
cout << "레퍼런스 접근 방법:" << endl;
cout << "ref: " << ref << endl; // 5 (레퍼런스는 그냥 변수처럼 사용 가능)
*ptr = 10; // 포인터를 사용하여 값 변경
cout << "포인터로 변경 후 x: " << x << endl; // 10
ref = 20; // 레퍼런스로 값 변경
cout << "레퍼런스로 변경 후 x: " << x << endl; // 20
return 0;
}
예시 3. 결과
상수 레퍼런스
레페런스에 상수 제약(const)를 걸어서 읽기 전용으로 사용할 수 있다.
예시. 상수 레퍼런스의 기본개념
#include <iostream>
using namespace std;
// 상수 레퍼런스를 사용하여 변수를 보호하는 예제
int main()
{
int x = 100;
const int& cref = x; // x를 읽기 전용으로 참조(상수 레퍼런스 선언)
cout << "cref: " << cref << endl; // 100
// cref = 200; // ❌ 오류 발생! 상수 레퍼런스는 값을 변경할 수 없음
x = 200; // 원본 변수 x는 변경 가능
cout << "x 변경 후 cref: " << cref << endl; // 200
return 0;
}
예시. 결과
'C++ 공부' 카테고리의 다른 글
C++ 4일차(상속, protected, 멤버 초기화 리스트, 다형성, 가상 함수(virtual), 순수 가상 함수) (0) | 2025.08.14 |
---|---|
C++ 공부 3일차(Class, 접근제어, getter, setter, 생성자, 헤더 파일, 소스 파일) (0) | 2025.08.13 |
C++ 숙제 (다이아몬드 모양으로 별 찍기) (4) | 2025.08.13 |
C++ 공부 2일차 (조건문, 계산기 추가 기능 구현하기, 반복문) (0) | 2025.08.12 |
C++ 공부 2일차(배열, 성적 관리 프로그램 만들기, 함수, 두 수의 합을 반환하는 함수 만들기) (0) | 2025.08.12 |