C++ 포인터 완전 가이드

목차- 포인터 개요

  • 주소 연산자 (&)
  • 역참조 연산자 (*)
  • 포인터 선언
  • 포인터와 배열
  • 포인터 초기화
  • 포인터 연산
  • 포인터와 const
  • 포인터와 문자열 리터럴
  • 다중 포인터
  • void 포인터
  • 유효하지 않은 포인터와 널 포인터
  • 함수 포인터

포인터 개요

이전 장에서 변수는 메모리의 위치로 설명되었으며, 이는 식별자(이름)로 접근할 수 있습니다. 이를 통해 프로그램은 데이터의 물리적 메모리 주소를 신경 쓸 필요가 없으며, 변수를 참조할 때마다 식별자만 사용하면 됩니다.

C++ 프로그램에 있어 컴퓨터의 메모리는 각각 1바이트 크기의 메모리 셀들의 연속으로 각 셀은 고유한 주소를 가집니다. 이러한 단일 바이트 메모리 셀들은 1바이트보다 큰 데이터 표현이 연속 주소를 가진 메모리 셀을 차지하도록 배열됩니다.

이렇게 각 셀은 고유한 주소를 통해 메모리에서 쉽게 위치를 찾을 수 있습니다. 예를 들어, 주소 1776의 메모리 셀은 항상 주소 1775의 셀 바로 다음에 위치하고 1777의 셀 바로 앞에 위치하며, 776에서 정확히 1000개의 셀 뒤에 있고 2776에서 정확히 1000개의 셀 앞에 있습니다.

변수가 선언될 때, 해당 변수의 값을 저장하는 데 필요한 메모리가 특정 메모리 위치(메모리 주소)에 할당됩니다. 일반적으로 C++ 프로그램은 변수가 저장될 정확한 메모리 주소를 적극적으로 결정하지 않습니다. 다행히도 이 작업은 프로그램이 실행되는 환경 - 일반적으로 런타임 시 특정 메모리 위치를 결정하는 운영체제 -에게 맡겨집니다. 그러나 프로그램이 런타임 중에 변수의 주소를 얻어 상대적인 특정 위치에 있는 데이터 셀에 접근할 수 있도록 하는 것이 유용할 수 있습니다.

주소 연산자 (&)

변수의 주소는 변수 이름 앞에 앰퍼샌드(&) 기호를 붙여 얻을 수 있으며, 이를 주소 연산자라고 합니다. 예를 들어:

target = &source_var;

이 코드는 source_var 변수의 주소를 target에 할당합니다. 변수 source_var의 이름 앞에 주소 연산자(&)를 붙이므로, 우리는 더 이상 변수 자체의 내용을 target에 할당하는 것이 아니라 그 주소를 할당하는 것입니다.

변수의 실제 메모리 주소는 런타임 전에는 알 수 없지만, 몇 가지 개념을 명확히 하기 위해 source_var가 런타임 시 메모리 주소 1776에 위치한다고 가정해 보겠습니다.

이 경우, 다음 코드 조각을 고려해 봅시다:

source_var = 25;
target = &source_var;
result = source_var;

이 코드 실행 후 각 변수에 포함된 값은 다음 다이어그램에 나와 있습니다:

먼저, 우리는 source_var(메모리 주소가 1776이라고 가정한 변수)에 값 25를 할당했습니다.

두 번째 문장은 target에 source_var의 주소를 할당하며, 이는 1776이라고 가정했습니다.

마지막으로, 세 번째 문장은 source_var에 포함된 값을 result에 할당합니다. 이는 이전 장에서 여러 번 수행된 표준 할당 연산입니다.

두 번째와 세 번째 문장의 주요 차이점은 주소 연산자(&)의 등장입니다.

다른 변수의 주소를 저장하는 변수(예시의 target와 같은)는 C++에서 포인터라고 합니다. 포인터는 저수준 프로그래밍에서 많은 용도로 사용되는 언어의 매우 강력한 기능입니다. 잠시 후에 포인터를 선언하고 사용하는 방법을 알아보겠습니다.

역참조 연산자 (*)

방금 본 바와 같이, 다른 변수의 주소를 저장하는 변수를 포인터라고 합니다. 포인터는 저장된 주소를 가리키는 변수를 "가리킨다"고 말합니다.

포인터의 흥미로운 속성은 포인터가 가리키는 변수에 직접 접근하는 데 사용될 수 있다는 것입니다. 이는 포인터 이름 앞에 역참조 연산자(*)를 붙여 수행됩니다. 연산자 자체는 "가리키는 값"으로 읽을 수 있습니다.

따라서 이전 예시의 값으로 계속 진행하면, 다음 문장은:

value = *target;

"target가 가리키는 값과 같다"로 읽을 수 있으며, 이 문장은 실제로 target가 1776이고 위 예시에 따르면 1776이 가리키는 값이 25이므로 value에 25를 할당합니다.

target가 값 1776을 참조하는 반면, *target(식별자 앞에 별표 *가 붙은)은 주소 1776에 저장된 값을 참조한다는 점을 명확히 구분하는 것이 중요합니다. 역참조 연산자를 포함하거나 포함하지 않는 것의 차이를 주목하세요(이 두 표현식이 어떻게 읽힐 수 있는지 설명하는 주석을 추가했습니다):

value = target;   // value가 target와 같음 (1776)
value = *target;  // value가 target가 가리키는 값과 같음 (25)  

참조 연산자와 역참조 연산자는 서로 보완적입니다:

  • &는 주소 연산자이며 "주소"로 간단히 읽을 수 있습니다
  • *는 역참조 연산자이며 "가리키는 값"으로 읽을 수 있습니다

따라서 이들은 반대 의미를 가집니다: &로 얻은 주소는 *로 역참조할 수 있습니다.

이전에 다음 두 가지 할당 연산을 수행했습니다:

source_var = 25;
target = &source_var;

이 두 문장 직후, 다음 모든 표현식은 결과로 true를 반환합니다:

source_var == 25
&source_var == 1776
target == 1776
*target == 25

첫 번째 표현식은 source_var에 수행된 할당 연산이 source_var=25였기 때문에 매우 명확합니다. 두 번째 표현식은 주소 연산자(&)를 사용하며, 이는 source_var의 주소를 반환하며, 이는 1776의 값을 가진다고 가정했습니다. 세 번째는 두 번째 표현식이 true였고 target에 수행된 할당 연산이 target=&source_var였기 때문에 다소 명백합니다. 네 번째 표현식은 "가리키는 값"으로 읽을 수 있는 역참조 연산자(*)를 사용하며, target가 가리키는 값은 실제로 25입니다.

그렇다면 모든 것을 고려할 때, target가 가리키는 주소가 변경되는 한 다음 표현식도 true가 될 것이라고 추론할 수 있습니다:

*target == source_var

포인터 선언

포인터가 가리키는 값에 직접 참조할 수 있는 능력 때문에, 포인터가 char를 가리킬 때와 int나 float를 가리킬 때 다른 속성을 가집니다. 역참조된 후, 타입을 알아야 합니다. 그리고 그를 위해 포인터 선언에는 포인터가 가리키게 될 데이터 타입이 포함되어야 합니다.

포인터 선언은 다음 구문을 따릅니다:

type * name; 

여기서 type은 포인터가 가리키는 데이터 타입입니다. 이 타입은 포인터 자체의 타입이 아니라 포인터가 가리키는 데이터의 타입입니다. 예를 들어:

int * 숫자;
char * 문자;
double * 실수;

이것은 세 가지 포인터 선언입니다. 각각은 다른 데이터 타입을 가리키도록 의도되었지만, 실제로 모두 포인터이며 메모리에서 동일한 양의 공간을 차지할 가능성이 높습니다(포인터의 메모리 크기는 프로그램이 실행되는 플랫폼에 따라 다름). 그럼에도 불구하고, 그들이 가리키는 데이터는 동일한 양의 공간을 차지하지 않으며 동일한 타입도 아닙니다: 첫 번째는 int를 가리키고, 두 번째는 char를 가리키며, 마지막은 double을 가리킵니다. 따라서 이 세 가지 예시 변수 모두 포인터이지만, 실제로 다른 타입을 가집니다: 각각 int*, char*, double*에 따라 가리키는 타입에 따라 다릅니다.

포인터를 선언할 때 사용되는 별표(*)는 포인터임을 의미할 뿐(이는 타입 복합 지정자의 일부입니다)이며, 앞서 본 역참조 연산자와 혼동해서는 안 됩니다. 그것은 동일한 기호로 표현된 두 가지 다른 것일 뿐입니다.

포인터에 대한 예시를 살펴보겠습니다:

// 첫 번째 포인터 예제
#include <iostream>
using namespace std;

int main ()
{
  int 첫번째값, 두번째값;
  int * 내포인터;

  내포인터 = &첫번째값;
  *내포인터 = 10;
  내포인터 = &두번째값;
  *내포인터 = 20;
  cout << "첫번째값은 " << 첫번째값 << '\n';
  cout << "두번째값은 " << 두번째값 << '\n';
  return 0;
}

첫번째값은 10
두번째값은 20

첫번째값과 두번째값 중 어느 것도 프로그램에서 직접 값이 설정되지 않았음에도 불구하고, 둘 다 내포인터를 사용하여 간접적으로 값이 설정된다는 점에 주목하세요. 이것이 발생하는 방식은 다음과 같습니다:

먼저, 내포인터는 주소 연산자(&)를 사용하여 첫번째값의 주소에 할당됩니다. 그런 다음, 내포인터가 가리키는 값에 10이 할당됩니다. 이 시점에서 내포인터가 첫번째값의 메모리 위치를 가리키고 있기 때문에, 이는 실제로 첫번째값의 값을 수정합니다.

포인터가 프로그램 수명 주기 동안 다른 변수를 가리킬 수 있다는 것을 보여주기 위해, 예시는 두번째값과 동일한 포인터인 내포인터로 프로세스를 반복합니다.

다음은 조금 더 복잡한 예시입니다:

// 고급 포인터 예제
#include <iostream>
using namespace std;

int main ()
{
  int 첫번째값 = 5, 두번째값 = 15;
  int * 포인터1, * 포인터2;

  포인터1 = &첫번째값;  // 포인터1 = 첫번째값의 주소
  포인터2 = &두번째값; // 포인터2 = 두번째값의 주소
  *포인터1 = 10;          // 포인터1이 가리키는 값 = 10
  *포인터2 = *포인터1;     // 포인터2가 가리키는 값 = 포인터1이 가리키는 값
  포인터1 = 포인터2;       // 포인터1 = 포인터2 (포인터 값이 복사됨)
  *포인터1 = 20;          // 포인터1이 가리키는 값 = 20
  
  cout << "첫번째값은 " << 첫번째값 << '\n';
  cout << "두번째값은 " << 두번째값 << '\n';
  return 0;
}

첫번째값은 10
두번째값은 20

각 할당 연산에는 각 줄이 어떻게 읽힐 수 있는지에 대한 주석이 포함되어 있습니다: 즉, 앰퍼샌드(&)를 "주소"로, 별표(*)를 "가리키는 값"으로 대체합니다.

포인터1과 포인터2를 사용하는 표현식이 역참조 연산자()를 사용한 것과 사용하지 않은 것 모두 있다는 점에 주목하세요. 역참조 연산자()를 사용한 표현식의 의미는 사용하지 않은 것과 매우 다릅니다. 이 연산자가 포인터 이름 앞에 나타날 때, 표현식은 가리키는 값을 참조하는 반면, 이 연산자 없이 포인터 이름이 나타날 때, 포인터 자체의 값(즉, 포인터가 가리키는 것의 주소)을 참조합니다.

또한 주목할 점은 다음 줄입니다:

int * 포인터1, * 포인터2;

이것은 이전 예시에서 사용된 두 포인터를 선언합니다. 하지만 각 포인터마다 별표()가 있어 둘 다 int 타입이 되도록 필요합니다. 이는 우선 순위 규칙 때문에 필요합니다. 만약 대신 코드가 다음과 같다면:

int * 포인터1, 포인터2;

포인터1은 실제로 int* 타입이지만 포인터2는 int 타입이 됩니다. 공간은 이 목적에는 전혀 중요하지 않습니다. 그러나 대부분의 포인터 사용자는 한 문장에서 여러 포인터를 선언할 때마다 하나의 별표를 기억하는 것만으로 충분합니다. 또는 더 나은 방법: 각 변수에 대해 다른 문장을 사용하세요.

포인터와 배열

배열의 개념은 포인터와 관련이 있습니다. 사실 배열은 첫 번째 요소에 대한 포인터와 매우 유사하게 작동하며, 실제로 배열은 항상 적절한 타입의 포인터로 암시적으로 변환될 수 있습니다. 예를 들어, 다음 두 선언을 고려해 보세요:

int 내배열 [20];
int * 내포인터;

다음 할당 연산은 유효합니다:

내포인터 = 내배열;

그 후, 내포인터와 내배열은 동일하며 매우 유사한 속성을 가집니다. 주요 차이점은 내포인터에 다른 주소를 할당할 수 있지만 내배열은 절대 아무것도 할당할 수 없으며 항상 동일한 20개의 int 요소 블록을 나타낸다는 것입니다. 따라서 다음 할당은 유효하지 않습니다:

내배열 = 내포인터;

배열과 포인터가 혼합된 예시를 살펴보겠습니다:

// 배열과 포인터
#include <iostream>
using namespace std;

int main ()
{
  int 숫자들[5];
  int * 포인터;
  포인터 = 숫자들;  *포인터 = 10;
  포인터++;  *포인터 = 20;
  포인터 = &숫자들[2];  *포인터 = 30;
  포인터 = 숫자들 + 3;  *포인터 = 40;
  포인터 = 숫자들;  *(포인터+4) = 50;
  for (int n=0; n<5; n++)
    cout << 숫자들[n] << ", ";
  return 0;
}

10, 20, 30, 40, 50, 

포인터와 배열은 동일한 의미를 가진 동일한 연산 집합을 지원합니다. 주요 차이점은 포인터에 새 주소를 할당할 수 있지만 배열은 그렇지 않다는 것입니다.

배열 장에서 대괄호([])는 배열의 요소 인덱스를 지정하는 것으로 설명되었습니다. 사실 이 대괄호는 오프셋 연산자로 알려진 역참조 연산자입니다. 그들은 *가 하는 것처럼 그들이 따르는 변수를 역참조하지만, 또한 대괄호 사이의 숫자를 역참조되는 주소에 추가합니다. 예를 들어:

a[5] = 0;       // a [오프셋 5] = 0
*(a+5) = 0;     // (a+5)가 가리키는 것 = 0  

이 두 표현식은 a가 포인터일 뿐만 아니라 배열일 때도 동등하고 유효합니다. 배열인 경우, 그 이름은 첫 번째 요소에 대한 포인터처럼 사용될 수 있음을 기억하세요.

포인터 초기화

포인터는 정의되는 순간 특정 위치를 가리키도록 초기화될 수 있습니다:

int 내변수;
int * 내포인터 = &내변수;

이 코드 후 변수의 결과 상태는 다음과 같습니다:

int 내변수;
int * 내포인터;
내포인터 = &내변수;

포인터가 초기화될 때, 초기화되는 것은 가리키는 주소(즉, 내포인터)이며, 가리키는 값(즉, *내포인터)은 아닙니다. 따라서 위 코드는 다음과 같이 혼동해서는 안 됩니다:

int 내변수;
int * 내포인터;
*내포인터 = &내변수;

어떻게든 큰 의미가 없을 것입니다(그리고 유효한 코드가 아닙니다).

포인터 선언의 별표()는 포인터임을 나타낼 뿐(2행)이며, 역참조 연산자(3행)가 아닙니다. 이 두 가지는 단지 동일한 기호()를 사용할 뿐입니다. 항상 그렇듯이 공간은 중요하지 않으며, 표현식의 의미를 절대 바꾸지 않습니다.

포인터는 변수의 주소(위의 경우와 같이) 또는 다른 포인터(또는 배열)의 값으로 초기화될 수 있습니다:

int 내변수;
int * 포인터A = &내변수;
int * 포인터B = 포인터A;

포인터 연산

정수 타입에 대해 연산을 수행하는 것보다 포인터에 대해 산술 연산을 수행하는 것은 조금 다릅니다. 시작하려면, 덧셈과 뺄셈 연산만 허용됩니다; 다른 연산은 포인터 세계에서 의미가 없습니다. 하지만 포인터에 대한 덧셈과 뺄셈은 가리키는 데이터 타입의 크기에 따라 약간 다른 동작을 합니다.

기본 데이터 타입이 소개될 때, 우리는 타입이 다른 크기를 가진다는 것을 보았습니다. 예를 들어: char는 항상 1바이트 크기를 가지며, short는 일반적으로 그보다 크고, int와 long은 더 큽니다; 이들의 정확한 크기는 시스템에 따라 다릅니다. 예를 들어, 특정 시스템에서 char는 1바이트를 차지하고, short는 2바이트를 차지하며, long은 4바이트를 차지한다고 가정해 보겠습니다.

이제 이 컴파일러에서 세 개의 포인터를 정의해 보겠습니다:

char * 내문자;
short * 내숫자;
long * 내긴숫자;

그리고 그들이 각각 메모리 위치 1000, 2000, 3000을 가리킨다고 알고 있다고 가정해 봅시다.

따라서 다음과 같이 작성한다면:

++내문자;
++내숫자;
++내긴숫자;

내문자는 예상대로 값 1001을 가지게 될 것입니다. 그러나 덜 명백하게, 내숫자는 값 2002를 가지게 되고, 내긴숫자는 3004를 가지게 됩니다. 각각이 한 번만 증가했음에도 불구하고 이유는, 포인터에 1을 더할 때 포인터가 동일한 타입의 다음 요소를 가리키도록 만들어지기 때문입니다. 따라서 가리키는 타입의 바이트 크기가 포인터에 추가됩니다.

이것은 포인터에 어떤 숫자를 더하거나 빼는 경우에도 적용됩니다. 다음과 같이 작성한다면 동일한 일이 발생합니다:

내문자 = 내문자 + 1;
내숫자 = 내숫자 + 1;
내긴숫자 = 내긴숫자 + 1;

증가(++) 및 감소(--) 연산자에 대해, 둘 다 표현식의 접두사 또는 접미사로 사용될 수 있으며, 동작에 약간의 차이가 있습니다: 접두사로 사용될 때, 증가는 표현식이 평가되기 전에 발생하고, 접미사로 사용될 때, 증가는 표현식이 평가된 후에 발생합니다. 이것은 역참조 연산자()도 포함하는 더 복잡한 표현식의 일부가 될 수 있는 포인터를 증가시키고 감소시키는 표현식에도 적용됩니다. 연산자 우선 순위 규칙을 기억하면, 접미사 연산자(예: 증가 및 감소)는 접두사 연산자(예: 역참조 연산자())보다 높은 우선 순위를 가집니다. 따라서 다음 표현식:

*포인터++

*(포인터++)와 동일합니다. 그리고 이것은 포인터의 값을 증가시켜(이제 다음 요소를 가리키게 됨) 접미사로 사용되기 때문에 전체 표현식은 원래 포인터가 가리키는 값(증가되기 전의 주소)으로 평가됩니다.

본질적으로, 이것은 역참조 연산자를 증가 연산자의 접두사 및 접미사 버전과 결합한 네 가지 가능한 조합입니다(감소 연산자에도 동일하게 적용됨):

*포인터++   // *(포인터++)와 동일: 포인터를 증가시키고, 증가되지 않은 주소를 역참조
*++포인터   // *(++포인터)와 동일: 포인터를 증가시키고, 증가된 주소를 역참조
++*포인터   // ++(*포인터)와 동일: 포인터를 역참조하고, 가리키는 값을 증가시킴
(*포인터)++ // 포인터를 역참조하고, 가리키는 값을 접미사 증가시킴 

이러한 연산자를 포함하는 전형적이지만 그렇게 간단하지 않은 문장은 다음과 같습니다:

*포인터A++ = *포인터B++;

++가 *보다 높은 우선 순위를 가지기 때문에 포인터A와 포인터B 모두 증가하지만, 두 증가 연산자(++)가 모두 접미사가 접두사가 아니기 때문에 *포인터A에 할당된 값은 포인터A와 포인터B가 모두 증가하기 전의 *포인터B입니다. 그리고 그 후 둘 다 증가합니다. 이것은 다음과 같이 대략적으로 동일합니다:

*포인터A = *포인터B;
++포인터A;
++포인터B;

항상 그렇듯이, 괄호는 표현식에 가독성을 추가하여 혼동을 줄입니다.

포인터와 const

포인터는 주소를 통해 변수에 접근하는 데 사용될 수 있으며, 이 접근에는 가리키는 값 수정이 포함될 수 있습니다. 하지만 가리키는 값을 읽기만 하고 수정하지는 않는 포인터를 선언하는 것도 가능합니다. 이를 위해서는 포인터가 가리키는 타입을 const로 한정하기만 하면 충분합니다. 예를 들어:

int x;
int y = 10;
const int * p = &y;
x = *p;          // ok: p 읽기
*p = x;          // 오류: p 수정, const로 한정됨 

여기서 p는 변수를 가리키지만 const로 한정된 방식으로 가리키며, 이는 가리키는 값을 읽을 수는 있지만 수정할 수는 없음을 의미합니다. 또한 &y 표현식은 int* 타입이지만 이것은 const int* 타입의 포인터에 할당됩니다. 이것은 허용됩니다: non-const에 대한 포인터는 const에 대한 포인터로 암시적으로 변환될 수 있습니다. 그러나 반대는 아닙니다! 안전 기능으로, const에 대한 포인터는 non-const에 대한 포인터로 암시적으로 변환될 수 없습니다.

const 요소에 대한 포인터의 사용 사례 중 하나는 함수 매개변수로서의 사용입니다: non-const에 대한 포인터를 매개변수로 받는 함수는 인수로 전달된 값을 수정할 수 있지만, const에 대한 포인터를 매개변수로 받는 함수는 그렇게 할 수 없습니다.

// 포인터를 인수로 사용:
#include <iostream>
using namespace std;

void 증가모두 (int* 시작, int* 끝)
{
  int * 현재 = 시작;
  while (현재 != 끝) {
    ++(*현재);  // 가리키는 값 증가
    ++현재;     // 포인터 증가
  }
}

void 출력모두 (const int* 시작, const int* 끝)
{
  const int * 현재 = 시작;
  while (현재 != 끝) {
    cout << *현재 << '\n';
    ++현재;     // 포인터 증가
  }
}

int main ()
{
  int 숫자들[] = {10,20,30};
  증가모두 (숫자들,숫자들+3);
  출력모두 (숫자들,숫자들+3);
  return 0;
}

11
21
31


출력모두가 const 요소를 가리키는 포인터를 사용한다는 점에 주목하세요. 이 포인터들은 수정할 수 없는 상수 콘텐츠를 가리키지만, 포인터 자체는 상수가 아닙니다: 즉, 포인터는 여전히 증가시키거나 다른 주소를 할당할 수 있지만, 가리키는 콘텐츠를 수정할 수는 없습니다.

그리고 여기서 포인터에 대한 constness의 두 번째 차원이 추가됩니다: 포인터 자체도 const가 될 수 있습니다. 이것은 가리키는 타입 뒤에 const를 추가하여 지정됩니다:

int x;
      int *       p1 = &x;  // non-const 포인터, non-const int
const int *       p2 = &x;  // non-const 포인터, const int
      int * const p3 = &x;  // const 포인터, non-const int
const int * const p4 = &x;  // const 포인터, const int 

const와 포인터의 구문은 확실히 혼란스럽고, 각 사용 사례에 가장 적합한 경우를 인식하는 데는 일부 경험이 필요합니다. 그러나 어쨌든, const와 포인터(및 참조)의 constness를 이해하는 것은 중요하지만, 이것이 const와 포인터의 조합에 처음 노출된 경우에는 모든 것을 파악하는 데 너무 걱정할 필요는 없습니다. 더 많은 사용 사례가 다음 장에서 나타날 것입니다.

const와 포인터의 구문에 조금 더 혼란을 더하기 위해, const 한정자는 가리키는 타입 앞에 또는 뒤에 올 수 있으며 정확히 동일한 의미를 가집니다:

const int * p2a = &x;  //      non-const 포인터, const int
int const * p2b = &x;  //      non-const 포인터, const int 

별호 주변의 공간과 마찬가지로, 이 경우 const의 순서는 단순히 스타일의 문제입니다. 이 장은 역사적 이유로 더 널리 퍼져 있는 접두사 const를 사용하지만, 둘은 정확히 동일합니다. 각 스타일의 장점은 여전히 인터넷에서 활발하게 논의 중입니다.

포인터와 문자열 리터럴

앞서 언급했듯이, 문자열 리터럴은 널 종결 문자 시퀀스를 포함하는 배열입니다. 이전 섹션에서 문자열 리터럴은 cout에 직접 삽입되거나 문자열을 초기화하고 문자 배열을 초기화하는 데 사용되었습니다.

하지만 직접 접근할 수도 있습니다. 문자열 리터럴은 모든 문자와 종료 널 문자를 포함하기에 적합한 배열 타입의 배열이며, 각 요소는 const char 타입입니다(리터럴이므로 절대 수정될 수 없음). 예를 들어:

const char * foo = "hello"; 

이것은 "hello"에 대한 리터럴 표현이 있는 배열을 선언하고, 그 첫 번째 요소에 대한 포인터가 foo에 할당됩니다. "hello"가 주소 1702에서 시작하는 메모리 위치에 저장된다고 가정하면, 이전 선언은 다음과 같이 표현할 수 있습니다:

여기서 foo는 포인터이며 값 1702를 포함하고 있으며, 'h'도 "hello"도 아닙니다. 그러나 1702는 이 둘의 주소임은 맞습니다.

포인터 foo는 문자 시퀀스를 가리킵니다. 그리고 포인터와 배열이 표현식에서 본질적으로 동일하게 작동하기 때문에, foo는 널 종결 문자 시퀀스 배열과 동일한 방식으로 문자에 접근하는 데 사용될 수 있습니다. 예를 들어:

*(foo+4)
foo[4]

두 표현식 모두 'o' 값(배열의 다섯 번째 요소)을 가집니다.

다중 포인터

C++는 다른 포인터를 가리키는 포인터 사용을 허용하며, 이는 다시 데이터(또는 다른 포인터)를 가리킵니다. 구문은 포인터 선언에서 각 간접 수준마다 별표(*) 하나만 요구합니다:

char a;
char * b;
char ** c;
a = 'z';
b = &a;
c = &b;

이것이 각 변수에 대해 임의로 선택된 메모리 위치 7230, 8092, 10502를 가정하면 다음과 같이 표현할 수 있습니다:

각 변수의 값이 해당 셀 내부에 표현되고, 각각의 메모리 주소가 그 아래 값으로 표현됩니다.

이 예시의 새로운 점은 변수 c로, 이것은 다중 포인터이며 세 가지 다른 수준의 간접 참조로 사용될 수 있으며, 각각은 다른 값에 해당합니다:

c는 char** 타입이고 값 8092
*c는 char* 타입이고 값 7230
**c는 char 타입이고 값 'z'

void 포인터

void 타입의 포인터는 특별한 종류의 포인터입니다. C++에서 void는 타입의 부재를 나타냅니다. 따라서 void 포인터는 타입이 없는 값(따라서 결정되지 않은 길이와 결정되지 않은 역참조 속성)을 가리키는 포인터입니다.

이것은 void 포인터가 정수 값이나 부동 소수점부터 문자열까지 모든 데이터 타입을 가리킬 수 있게 하여 큰 유연성을 부여합니다. 대가로, 그들은 큰 제한을 가집니다: 그들이 가리키는 데이터는 직접 역참조될 수 없습니다(논리적으로, 우리는 역참조할 타입이 없기 때문입니다). 따라서 void 포인터의 모든 주소는 역참조되기 전에 구체적인 데이터 타입을 가리키는 다른 포인터 타입으로 변환되어야 합니다.

그들의 가능한 사용 사례 중 하나는 함수에 제네릭 매개변수를 전달하는 것입니다. 예를 들어:

// 값 증가기
#include <iostream>
using namespace std;

void 증가 (void* 데이터, int 크기)
{
  if ( 크기 == sizeof(char) )
  { char* pchar; pchar=(char*)데이터; ++(*pchar); }
  else if (크기 == sizeof(int) )
  { int* pint; pint=(int*)데이터; ++(*pint); }
}

int main ()
{
  char a = 'x';
  int b = 1602;
  증가 (&a,sizeof(a));
  증가 (&b,sizeof(b));
  cout << a << ", " << b << '\n';
  return 0;
}

y, 1603

sizeof는 C++ 언어에 통합된 연산자로, 인수의 바이트 크기를 반환합니다. 비동적 데이터 타입의 경우 이 값은 상수입니다. 따라서 예를 들어, sizeof(char)는 1입니다. 왜냐하면 char은 항상 1바이트 크기를 가지기 때문입니다.

유효하지 않은 포인터와 널 포인터

원칙적으로 포인터는 변수의 주소나 배열 요소의 주소와 같은 유효한 주소를 가리키도록 의도되었습니다. 하지만 포인터는 실제로 유효한 요소를 참조하지 않는 주소를 포함하여 어떤 주소든 가리킬 수 있습니다. 이의 전형적인 예는 초기화되지 않은 포인터와 배열의 존재하지 않는 요소에 대한 포인터입니다:

int * p;               // 초기화되지 않은 포인터 (지역 변수)

int 내배열[10];
int * q = 내배열+20;  // 경계 밖의 요소 

p와 q는 알려진 값이 포함된 주소를 가리키지 않지만, 위의 문장 중 어느 것도 오류를 일으키지 않습니다. C++에서 포인터는 그 주소에 실제로 무언가가 있는지 여부에 관계없이 어떤 주소 값도 가질 수 있습니다. 오류를 일으킬 수 있는 것은 이러한 포인터를 역참조하는 것(즉, 실제로 가리키는 값에 접근하는 것)입니다. 이러한 포인터에 접근하는 것은 런타임 오류부터 무작위 값 접근까지 정의되지 않은 동작을 유발합니다.

하지만 때로는 포인터가 단순히 유효하지 않은 주소가 아닌 명시적으로 어디에도 가리키지 않아야 할 때가 있습니다. 이러한 경우를 위해, 모든 포인터 타입이 취할 수 있는 특별한 값이 있습니다: 널 포인터 값. 이 값은 C++에서 정수 값 0으로 또는 nullptr 키워드로 표현될 수 있습니다:

int * p = 0;
int * q = nullptr;

여기서 p와 q는 모두 널 포인터이며, 명시적으로 어디에도 가리키지 않음을 의미하며, 둘 다 실제로 동일하게 비교됩니다: 모든 널 포인터는 다른 널 포인터와 동일하게 비교됩니다. 또한 정의된 상수 NULL이 널 포인터 값을 참조하기 위해 이전 코드에서 사용되는 것을 볼 수도 있습니다:

int * r = NULL;

NULL은 표준 라이브러리의 여러 헤더에 정의되어 있으며, 0이나 nullptr과 같은 널 포인터 상수 값의 별칭으로 정의됩니다.

널 포인터를 void 포인터와 혼동하지 마세요! 널 포인터는 포인터가 "어디에도 가리키지 않음"을 나타내기 위해 모든 포인터가 취할 수 있는 값입니다. 반면 void 포인터는 특정 타입 없이 어딘가를 가리킬 수 있는 포인터 타입입니다. 하나는 포인터에 저장된 값을 참조하고, 다른 하나는 가리키는 데이터의 타입을 참조합니다.

함수 포인터

C++는 함수 포인터로 작업하는 것을 허용합니다. 이의 전형적인 사용은 다른 함수에 인수로 함수를 전달하는 것입니다. 함수 포인터는 일반 함수 선언과 동일한 구문으로 선언되지만, 함수 이름은 괄호()로 묶이고 이름 앞에 별표(*)가 삽입됩니다:

// 함수 포인터 예제
#include <iostream>
using namespace std;

int 덧셈 (int a, int b)
{ return (a+b); }

int 뺄셈 (int a, int b)
{ return (a-b); }

int 연산 (int x, int y, int (*호출할함수)(int,int))
{
  int g;
  g = (*호출할함수)(x,y);
  return (g);
}

int main ()
{
  int m,n;
  int (*빼기)(int,int) = 뺄셈;

  m = 연산 (7, 5, 덧셈);
  n = 연산 (20, m, 빼기);
  cout <<n;
  return 0;
}

8


위 예시에서 빼기는 두 개의 int 매개변수를 가진 함수에 대한 포인터입니다. 이것은 함수 뺄셈을 가리키도록 직접 초기화됩니다:

int (* 빼기)(int,int) = 뺄셈;

태그: C++ 포인터 메모리 관리 low-level 프로그래밍

7월 2일 20:07에 게시됨