포인터
포인터 (Pointer)
포인터는 어떤 객체나 함수의 메모리 주소를 담는 변수예요. C++에서 포인터는 객체 포인터, void 포인터, 함수 포인터, 멤버 포인터까지 여러 형태가 있어서, 이 페이지에서는 포인터 선언 문법부터 시작해 각 종류와 널 포인터, 무효 포인터, const 처리까지 차근차근 다룰게요.
출처: cppreference
본문
포인터 선언(pointer declaration)은 declarator가 다음 형태를 가진 어떤 simple-declaration이에요.
* attr (optional) cv (optional) declarator (1)
nested-name-specifier * attr (optional) cv (optional) declarator (2)
- 포인터 declarator: 선언
S* D;는D를 선언 지정자 시퀀스S가 정하는 타입을 가리키는 포인터로 선언해요. - 멤버 포인터 declarator: 선언
S C::* D;는D를C의 비정적 멤버(타입은S로 결정)를 가리키는 포인터로 선언해요.
포인터에 대한 참조는 없고, 비트 필드에 대한 포인터도 없어요. 보통 "포인터"라고만 말하면 (비정적) 멤버에 대한 포인터는 포함하지 않아요.
포인터 (Pointers)
포인터 타입의 모든 값은 다음 중 하나예요.
- 객체나 함수를 가리키는 포인터(그 객체/함수를 가리킨다고 말함),
- 객체의 끝을 지나는 포인터(past the end),
- 그 타입의 널 포인터 값,
- 무효 포인터 값(invalid pointer value).
객체를 가리키는 포인터는 그 객체가 차지하는 메모리의 첫 바이트 주소를 나타내요. 객체의 끝을 지나는 포인터는 객체의 저장 공간이 끝난 뒤 첫 바이트 주소를 나타내요.
같은 주소를 나타내는 두 포인터라도 값은 다를 수 있다는 점에 주의하세요.
struct C
{
int x, y;
} c;
int* px = &c.x; // value of px is "pointer to c.x"
int* pxe= px + 1; // value of pxe is "pointer past the end of c.x"
int* py = &c.y; // value of py is "pointer to c.y"
assert(pxe == py); // == tests if two pointers represent the same address
// may or may not fire
*pxe = 1; // undefined behavior even if the assertion does not fire
무효 포인터 값을 통한 간접 참조와, 무효 포인터 값을 해제(deallocation) 함수에 전달하는 것은 undefined behavior예요. 무효 포인터 값의 다른 어떤 사용도 구현 정의(implementation-defined) 동작이에요. 어떤 구현은 무효 포인터 값을 복사하면 시스템이 런타임 오류를 일으킨다고 정의할 수도 있어요.
객체 포인터 (Pointers to objects)
객체 포인터는 객체 타입의 어떤 표현식에든 주소 연산자(address-of operator)를 적용한 반환값으로 초기화할 수 있어요(다른 포인터 타입 포함).
int n;
int* np = &n; // pointer to int
int* const* npp = &np; // non-const pointer to const pointer to non-const int
int a[2];
int (*ap)[2] = &a; // pointer to array of int
struct S { int n; };
S s = {1};
int* sp = &s.n; // pointer to the int that is a member of s
포인터는 내장 간접 참조 연산자(단항 operator*)의 피연산자로 나타날 수 있는데, 이 연산자는 가리키는 객체를 식별하는 lvalue 표현식을 반환해요.
int n;
int* p = &n; // pointer to n
int& r = *p; // reference is bound to the lvalue expression that identifies n
r = 7; // stores the int 7 in n
std::cout << *p; // lvalue-to-rvalue implicit conversion reads the value from n
클래스 객체에 대한 포인터는 멤버 접근 연산자 operator->와 operator->*의 왼쪽 피연산자로도 나타날 수 있어요.
배열→포인터 암시 변환 덕분에, 배열의 첫 요소를 가리키는 포인터는 배열 타입 표현식으로 초기화할 수 있어요.
int a[2];
int* p1 = a; // pointer to the first element a[0] (an int) of the array a
int b[6][3][8];
int (*p2)[3][8] = b; // pointer to the first element b[0] of the array b,
// which is an array of 3 arrays of 8 ints
포인터의 파생→기반 암시 변환 덕분에, 베이스 클래스에 대한 포인터는 파생 클래스의 주소로 초기화할 수 있어요.
struct Base {};
struct Derived : Base {};
Derived d;
Base* p = &d;
Derived가 다형적이면 그런 포인터로 가상 함수 호출을 할 수 있어요.
배열 요소를 가리키는 포인터에는 덧셈·뺄셈·증가·감소 연산자가 일부 정의돼 있어요. 그런 포인터는 LegacyRandomAccessIterator 요구사항을 만족해서 C++ 라이브러리 알고리즘이 raw 배열로도 동작하게 해줘요.
객체 포인터에는 비교 연산자가 어떤 상황에서 정의돼요. 같은 주소를 나타내는 두 포인터는 같게 비교되고, 두 널 포인터 값은 같게 비교되며, 같은 배열의 요소를 가리키는 포인터는 그 요소의 배열 인덱스처럼 비교되고, 멤버 접근이 같은 비정적 데이터 멤버 포인터는 그 멤버의 선언 순서대로 비교돼요.
많은 구현은 임의 기원(random origin)의 포인터에 엄격한 전체 순서(strict total ordering)를 제공해요. 예를 들어 연속된 가상 주소 공간의 주소로 구현된 경우죠. 그렇지 않은 구현(예: 포인터의 모든 비트가 메모리 주소가 아니어서 비교 시 무시해야 하거나, 추가 계산이 필요해 포인터와 정수가 1:1 관계가 아닌 경우)은 포인터에 대한 std::less 특수화를 제공하는데, 이것이 그 보장을 제공해요. 이를 통해 임의 기원의 모든 포인터를 std::set이나 std::map 같은 표준 연관 컨테이너의 키로 쓸 수 있어요.
void 포인터 (Pointers to void)
어떤 타입의 객체 포인터든 (cv-qualified) void 포인터로 암시 변환할 수 있어요. 포인터 값은 그대로예요. 반대 변환은 static_cast나 명시적 캐스트가 필요하며 원래 포인터 값을 산출해요.
int n = 1;
int* p1 = &n;
void* pv = p1;
int* p2 = static_cast<int*>(pv);
std::cout << *p2 << '\n'; // prints 1
원래 포인터가 어떤 다형적 타입 객체 안의 베이스 클래스 하위 객체를 가리키고 있으면, dynamic_cast로 가장 파생 타입(most derived type)의 완전 객체를 가리키는 void*를 얻을 수 있어요.
void 포인터는 char 포인터와 같은 크기·표현·정렬을 가져요.
void 포인터는 알 수 없는 타입의 객체를 전달하는 데 쓰여요. C 인터페이스에서 흔하죠. std::malloc은 void*를 반환하고, std::qsort는 두 const void* 인자를 받는 사용자 제공 콜백을 기대하며, pthread_create는 void*를 받고 반환하는 사용자 제공 콜백을 기대해요. 모든 경우에 사용하기 전에 올바른 타입으로 캐스팅하는 것은 호출자의 책임이에요.
함수 포인터 (Pointers to functions)
함수 포인터는 비멤버 함수나 정적 멤버 함수의 주소로 초기화할 수 있어요. 함수→포인터 암시 변환 덕분에 주소 연산자는 생략 가능해요.
void f(int);
void (*p1)(int) = &f;
void (*p2)(int) = f; // same as &f
함수나 함수 참조와 달리, 함수 포인터는 객체라서 배열에 저장하거나 복사·할당할 수 있어요.
void (a[10])(int); // Error: array of functions
void (&a[10])(int); // Error: array of references
void (*a[10])(int); // OK: array of pointers to functions
함수 포인터를 포함한 선언은 타입 별칭으로 자주 단순화할 수 있어요.
using F = void(int); // named type alias to simplify declarations
F a[10]; // Error: array of functions
F& a[10]; // Error: array of references
F* a[10]; // OK: array of pointers to functions
함수 포인터는 함수 호출 연산자의 왼쪽 피연산자로 쓸 수 있고, 그러면 가리키는 함수를 호출해요.
int f(int n)
{
std::cout << n << '\n';
return n * n;
}
int main()
{
int (*p)(int) = f;
int x = p(7);
}
함수 포인터를 역참조하면 가리키는 함수를 식별하는 lvalue가 나와요.
int f();
int (*p)() = f; // pointer p is pointing to f
int (&r)() = *p; // the lvalue that identifies f is bound to a reference
r(); // function f invoked through lvalue reference
(*p)(); // function f invoked through the function lvalue
p(); // function f invoked directly through the pointer
함수 포인터는 함수·함수 템플릿 특수화·함수 템플릿을 포함하는 오버로드 집합으로 초기화할 수 있어요. 단 오직 하나의 오버로드만 포인터 타입과 일치해야 해요(오버로드된 함수의 주소 참고).
template<typename T>
T f(T n) { return n; }
double f(double n) { return n; }
int main()
{
int (*p)(int) = f; // instantiates and selects f<int>
}
함수 포인터에는 동등 비교 연산자가 정의돼 있어요(같은 함수를 가리키면 같게 비교).
멤버 포인터 (Pointers to members)
데이터 멤버 포인터 (Pointers to data members)
클래스 C의 멤버인 비정적 멤버 객체 m에 대한 포인터는 정확히 &C::m 표현식으로 초기화할 수 있어요. &(C::m)이나 C의 멤버 함수 안에서 &m 같은 표현식은 멤버 포인터를 만들지 않아요.
그런 포인터는 멤버 포인터 접근 연산자 operator.*와 operator->*의 오른쪽 피연산자로 쓸 수 있어요.
struct C { int m; };
int main()
{
int C::* p = &C::m; // pointer to data member m of class C
C c = {7};
std::cout << c.*p << '\n'; // prints 7
C* cp = &c;
cp->m = 10;
std::cout << cp->*p << '\n'; // prints 10
}
접근 가능하고 모호하지 않은 비가상 베이스 클래스의 데이터 멤버 포인터는, 같은 데이터 멤버를 가리키는 파생 클래스 포인터로 암시 변환할 수 있어요.
struct Base { int m; };
struct Derived : Base {};
int main()
{
int Base::* bp = &Base::m;
int Derived::* dp = bp;
Derived d;
d.m = 1;
std::cout << d.*dp << ' ' << d.*bp << '\n'; // prints 1 1
}
반대 방향 변환, 즉 파생 클래스의 데이터 멤버 포인터에서 모호하지 않은 비가상 베이스 클래스의 데이터 멤버 포인터로는, 베이스 클래스에 그 멤버가 없어도 static_cast와 명시적 캐스트로 허용돼요(포인터로 접근할 때 가장 파생 클래스가 그 멤버를 가질 경우).
struct Base {};
struct Derived : Base { int m; };
int main()
{
int Derived::* dp = &Derived::m;
int Base::* bp = static_cast<int Base::*>(dp);
Derived d;
d.m = 7;
std::cout << d.*bp << '\n'; // okay: prints 7
Base b;
std::cout << b.*bp << '\n'; // undefined behavior
}
멤버 포인터를 가리키는 타입은 그 자체가 멤버 포인터일 수 있어요. 멤버 포인터는 다단계(multilevel)일 수 있고 각 단계에서 다르게 cv-qualified될 수 있어요. 포인터와 멤버 포인터를 혼합한 다단계 조합도 허용돼요.
struct A
{
int m;
// const pointer to non-const member
int A::* const p;
};
int main()
{
// non-const pointer to data member which is a const pointer to non-const member
int A::* const A::* p1 = &A::p;
const A a = {1, &A::m};
std::cout << a.*(a.*p1) << '\n'; // prints 1
// regular non-const pointer to a const pointer-to-member
int A::* const* p2 = &a.p;
std::cout << a.**p2 << '\n'; // prints 1
}
멤버 함수 포인터 (Pointers to member functions)
클래스 C의 멤버인 비정적 멤버 함수 f에 대한 포인터는 정확히 &C::f 표현식으로 초기화할 수 있어요. &(C::f)이나 C의 멤버 함수 안에서 &f 같은 표현식은 멤버 함수 포인터를 만들지 않아요.
그런 포인터는 멤버 포인터 접근 연산자 operator.*와 operator->*의 오른쪽 피연산자로 쓸 수 있어요. 결과 표현식은 함수 호출 연산자의 왼쪽 피연산자로만 쓸 수 있어요.
struct C
{
void f(int n) { std::cout << n << '\n'; }
};
int main()
{
void (C::* p)(int) = &C::f; // pointer to member function f of class C
C c;
(c.*p)(1); // prints 1
C* cp = &c;
(cp->*p)(2); // prints 2
}
베이스 클래스의 멤버 함수 포인터는 같은 멤버 함수를 가리키는 파생 클래스 포인터로 암시 변환할 수 있어요.
struct Base
{
void f(int n) { std::cout << n << '\n'; }
};
struct Derived : Base {};
int main()
{
void (Base::* bp)(int) = &Base::f;
void (Derived::* dp)(int) = bp;
Derived d;
(d.*dp)(1);
(d.*bp)(2);
}
반대 방향 변환, 즉 파생 클래스의 멤버 함수 포인터에서 모호하지 않은 비가상 베이스 클래스의 멤버 함수 포인터로는, 베이스 클래스에 그 멤버 함수가 없어도 static_cast와 명시적 캐스트로 허용돼요.
struct Base {};
struct Derived : Base
{
void f(int n) { std::cout << n << '\n'; }
};
int main()
{
void (Derived::* dp)(int) = &Derived::f;
void (Base::* bp)(int) = static_cast<void (Base::*)(int)>(dp);
Derived d;
(d.*bp)(1); // okay: prints 1
Base b;
(b.*bp)(2); // undefined behavior
}
멤버 함수 포인터는 콜백이나 함수 객체로 쓸 수 있어요. 흔히 std::mem_fn이나 std::bind를 적용한 뒤에요.
#include <algorithm>
#include <cstddef>
#include <functional>
#include <iostream>
#include <string>
#include <vector>
int main()
{
std::vector<std::string> v = {"a", "ab", "abc"};
std::vector<std::size_t> l;
transform(v.begin(), v.end(), std::back_inserter(l),
std::mem_fn(&std::string::size));
for (std::size_t n : l)
std::cout << n << ' ';
std::cout << '\n';
}
출력:
1 2 3
널 포인터 (Null pointers)
모든 타입의 포인터는 그 타입의 널 포인터 값(null pointer value)이라는 특별한 값을 가져요. 값이 널인 포인터는 객체나 함수를 가리키지 않아요(널 포인터 역참조는 undefined behavior) 그리고 값이 널인 같은 타입의 모든 포인터와 같게 비교돼요.
널 포인터 상수(null pointer constant)는 포인터를 널로 초기화하거나 기존 포인터에 널 값을 할당하는 데 쓰여요. 다음 중 하나예요.
- 값이 0인 정수 리터럴,
std::nullptr_t타입의 prvalue(보통nullptr). (since C++11)
매크로 NULL도 쓸 수 있는데, 구현 정의된 널 포인터 상수로 확장돼요.
0-초기화(zero-initialization)와 값 초기화(value-initialization)도 포인터를 널 값으로 초기화해요.
널 포인터는 객체의 부재를 나타내는 데 쓸 수 있어요(예: std::function::target()), 또는 다른 오류 조건 표시로도 써요(예: dynamic_cast). 일반적으로 포인터 인자를 받는 함수는 값이 널인지 거의 항상 검사하고 그 경우를 다르게 처리해야 해요(예를 들어 delete 표현식은 널 포인터가 전달되면 아무것도 하지 않아요).
무효 포인터 (Invalid pointers)
포인터 값 p는 평가 e의 문맥에서 다음 조건 중 하나를 만족하면 유효해요.
p는 널 포인터 값,p는 함수에 대한 포인터,p는 객체o에 대한 포인터 또는 그 끝을 지나는 포인터이고,e가o의 저장 영역의 지속 기간 안에 있음.
포인터 값 p가 평가 e에서 쓰이는데 e의 문맥에서 유효하지 않으면:
e가 간접 참조이거나 해제 함수 호출이면 undefined behavior,- 그 외에는 구현 정의 동작.
int* f()
{
int obj;
int* local_ptr = new (&obj) int;
*local_ptr = 1; // OK, the evaluation "*local_ptr" is
// in the storage duration of "obj"
return local_ptr;
}
int* ptr = f(); // the storage duration of "obj" is expired,
// therefore "ptr" is an invalid pointer in the following contexts
int* copy = ptr; // implementation-defined behavior
*ptr = 2; // undefined behavior: indirection of an invalid pointer
delete ptr; // undefined behavior: deallocating storage from an invalid pointer
const (Constness)
- 포인터 선언에서
cv가*앞에 오면, 그것은 선언 지정자 시퀀스의 일부로서 가리키는 객체에 적용돼요. - 포인터 선언에서
cv가*뒤에 오면, 그것은 declarator의 일부로서 선언되는 포인터에 적용돼요.
| 문법 | 의미 |
|---|---|
const T* |
상수 객체를 가리키는 포인터 |
T const* |
상수 객체를 가리키는 포인터 |
T* const |
객체를 가리키는 상수 포인터 |
const T* const |
상수 객체를 가리키는 상수 포인터 |
T const* const |
상수 객체를 가리키는 상수 포인터 |
// pc is a non-const pointer to const int
// cpc is a const pointer to const int
// ppc is a non-const pointer to non-const pointer to const int
const int ci = 10, *pc = &ci, *const cpc = pc, **ppc;
// p is a non-const pointer to non-const int
// cp is a const pointer to non-const int
int i, *p, *const cp = &i;
i = ci; // okay: value of const int copied into non-const int
*cp = ci; // okay: non-const int (pointed-to by const pointer) can be changed
pc++; // okay: non-const pointer (to const int) can be changed
pc = cpc; // okay: non-const pointer (to const int) can be changed
pc = p; // okay: non-const pointer (to const int) can be changed
ppc = &pc; // okay: address of pointer to const int is pointer to pointer to const int
ci = 1; // error: const int cannot be changed
ci++; // error: const int cannot be changed
*pc = 2; // error: pointed-to const int cannot be changed
cp = &ci; // error: const pointer (to non-const int) cannot be changed
cpc++; // error: const pointer (to const int) cannot be changed
p = pc; // error: pointer to non-const int cannot point to const int
ppc = &p; // error: pointer to pointer to const int cannot point to
// pointer to non-const int
일반적으로 한 다단계 포인터에서 다른 다단계 포인터로의 암시 변환은 qualification conversions에서 설명한 규칙을 따라요.
복합 포인터 타입 (Composite pointer type)
비교 연산자의 피연산자나 조건 연산자의 두 번째·세 번째 피연산자가 포인터 또는 멤버 포인터이면, 그 피연산자들의 공통 타입으로 복합 포인터 타입(composite pointer type)을 정해요.
타입 T1, T2를 가진 두 피연산자 p1, p2가 주어졌을 때, p1과 p2는 다음 조건 중 하나를 만족할 때만 복합 포인터 타입을 가질 수 있어요.
p1과p2가 모두 포인터.p1,p2중 하나는 포인터이고 다른 하나는 널 포인터 상수.p1,p2가 모두 널 포인터 상수이고T1,T2중 적어도 하나는 비정수 타입. (since C++11) (until C++14)T1,T2중 적어도 하나가 포인터 타입, 멤버 포인터 타입 또는std::nullptr_t. (since C++14)
p1, p2의 복합 포인터 타입 C는 다음과 같이 정해져요.
p1이 널 포인터 상수면C는T2. (until C++11)p2가 널 포인터 상수면C는T1. (until C++11)p1,p2가 모두 널 포인터 상수면C는std::nullptr_t. (since C++11)- 그 외,
p1이 널 포인터 상수면C는T2. (since C++11) - 그 외,
p2가 널 포인터 상수면C는T1. (since C++11) - 그 외, 다음 조건을 모두 만족하면:
T1또는T2가 "cv1 void에 대한 포인터",- 다른 타입이 "
cv2 T에 대한 포인터"(여기서T는 객체 타입 또는void), C는 "cv12 void에 대한 포인터"(여기서cv12는cv1과cv2의 합집합).
- 그 외, 다음 조건을 모두 만족하면:
T1또는T2가 "함수 타입F1에 대한 포인터",- 다른 타입이 "noexcept 함수 타입
F2에 대한 포인터", F1과F2가noexcept를 제외하고 같음,C는 "F1에 대한 포인터". (since C++17)
- 그 외, 다음 조건을 모두 만족하면:
T1이 "C1에 대한 포인터",T2가 "C2에 대한 포인터",C1과C2중 하나가 다른 하나에 reference-related,C는,C1이C2에 reference-related면T1과T2의 qualification-combined type,C2가C1에 reference-related면T2와T1의 qualification-combined type.
- 그 외, 다음 조건을 모두 만족하면:
T1또는T2가 "클래스C1의 함수 타입F1에 대한 멤버 포인터",- 다른 타입이 "클래스
C2의 noexcept 함수 타입F2에 대한 멤버 포인터", C1,C2중 하나가 다른 것에 reference-related,F1과F2가noexcept를 제외하고 같음,C는,C1이C2에 reference-related면 "C2의 타입F1에 대한 멤버 포인터",C2가C1에 reference-related면 "C1의 타입F1에 대한 멤버 포인터". (since C++17)
- 그 외, 다음 조건을 모두 만족하면:
T1이 "클래스C1의 비함수 타입M1에 대한 멤버 포인터",T2가 "클래스C2의 비함수 타입M2에 대한 멤버 포인터",M1과M2가 최상위 cv-qualification을 제외하고 같음,C1,C2중 하나가 다른 것에 reference-related,C는,C1이C2에 reference-related면T2와T1의 qualification-combined type,C2가C1에 reference-related면T1과T2의 qualification-combined type.
- 그 외,
T1과T2가 유사(similar) 타입이면C는T1과T2의 qualification-combined type. - 그 외,
p1과p2는 복합 포인터 타입을 갖지 않으며, 그런C의 결정을 요구하는 프로그램은 ill-formed예요.
using p = void*;
using q = const int*;
// The determination of the composite pointer type of "p" and "q"
// falls into the ["pointer to cv1 void" and "pointer to cv2 T"] case:
// cv1 = empty, cv2 = const, cv12 = const
// substitute "cv12 = const" into "pointer to cv12 void":
// the composite pointer type is "const void*"
using pi = int**;
using pci = const int**;
// The determination of the composite pointer type of "pi" and "pci"
// falls into the [pointers to similar types "C1" and "C2"] case:
// C1 = int*, C2 = const int*
// they are reference-related types (in both direction) because they are similar
// the composite pointer type is the qualification-combined type
// of "p1" and "pc1" (or that of "pci" and "pi"): "const int**"
결함 보고서 (Defect reports)
이전에 발표된 C++ 표준에 소급 적용된, 동작을 바꾸는 결함 보고서는 다음과 같아요.
- CWG 73 (C++98): 객체 포인터는 배열의 끝을 지나는 포인터와 결코 같게 비교되지 않음 → 널·비함수 포인터는 나타내는 주소를 비교하도록 함.
- CWG 903 (C++98): 0으로 평가되는 정수 상수 표현식이면 무엇이든 널 포인터 상수였음 → 값 0인 정수 리터럴로 제한.
- CWG 1438 (C++98): 무효 포인터 값을 어떤 방식으로든 쓰는 것이 undefined였음 → 간접 참조와 해제 함수 전달은 undefined, 그 외는 구현 정의.
- CWG 1512(N3624) (C++98): 복합 포인터 타입 규칙이 불완전해
int**와const int**비교를 허용하지 않았음 → 완성. - CWG 2206 (C++98):
void에 대한 포인터와 함수에 대한 포인터가 복합 포인터 타입을 가졌음 → 그런 타입을 갖지 않음. - CWG 2381 (C++17): 복합 포인터 타입을 결정할 때 함수 포인터 변환이 허용되지 않았음 → 허용.
- CWG 2822 (C++98): 저장 영역의 지속 기간 끝에 도달하면 포인터 값이 무효화될 수 있었음 → 포인터 유효성은 평가 문맥에 기반.
- CWG 2933 (C++98): 함수 포인터는 항상 무효였음 → 항상 유효.