1. 포인터의 개념
포인터는 주소를 상징적으로 표현하는 것이다. 포인터는 프로그램이 참조에 의한 호출을 시뮬레이션하고, 동적 데이터 구조를 만들고 조작할 수 있게 해준다. 배열이나 다른 데이터 구조의 요소 위에서 반복하는 것이 포인터의 주요 사용 중 하나라 할 수 있겠다.
작업 중인 변수의 주소는 동일한 데이터 유형(int 또는 string 등)을 가리키는 포인터 변수에 할당된다.
포인터는 잘만 사용한다면, 코드를 줄이고 성능을 향상시킨다. 포인터는 문자열, 트리, 배열, 구조 및 함수를 검색하는 데 사용된다.
포인터를 사용하면 함수에서 여러 값을 반환할 수도 있다. 또한, 컴퓨터 메모리의 메모리 위치에 액세스할 수도 있다. 결국 모든 것은 사용하기 나름이다.
구문:
datatype *var_name;
int *ptr; // ptr can point to an address which holds int data
C++에서 포인트를 쓰는 방법은 다음과 같다.
① 포인터 변수 정의
② 변수의 주소를 반환하는 단항 연산자(&)를 사용하여 포인터에 변수의 주소를 할당
③ 해당 피연산자가 지정한 주소에 있는 변수 값을 반환하는 단항 연산자(*)를 사용하여 주소에 저장된 값에 액세스
데이터 유형을 포인터에 연결하는 이유는 데이터가 몇 바이트에 저장되어 있는지 알기 때문이다. 포인터를 늘리면, 포인터가 가리키는 데이터 유형의 크기만큼 포인터를 늘린다.
예시:
// C++ program to illustrate Pointers
#include <bits/stdc++.h>
using namespace std;
void gutis()
{
int var = 20;
// declare pointer variable
int* ptr;
// note that data type of ptr and var must be same
ptr = &var;
// assign the address of a variable to a pointer
cout << "Value at ptr = " << ptr << "\n";
cout << "Value at var = " << var << "\n";
cout << "Value at *ptr = " << *ptr << "\n";
}
// Driver program
int main()
{
guti();
return 0;
}
출력:
Value at ptr = 0x7ffe454c08cc
Value at var = 20
Value at *ptr = 20
2. 레퍼런스와 포인터
C++ 인수를 함수에 전달하는 방법은 총 3가지다.
① 값에 의한 호출 (Call-By-Value)
② 포인터 인수가 있는 참조에 의한 호출 (Call-By-Reference with a Pointer Argument)
③ 참조 인수가 있는 참조에 의한 호출 (Call-By-Reference with a Reference Argument)
예시:
// C++ program to illustrate call-by-methods
#include <bits/stdc++.h>
using namespace std;
// Pass-by-Value
int square1(int n)
{
// Address of n in square1() is not the same as n1 in
// main()
cout << "address of n1 in square1(): " << &n << "\n";
// clone modified inside the function
n *= n;
return n;
}
// Pass-by-Reference with Pointer Arguments
void square2(int* n)
{
// Address of n in square2() is the same as n2 in main()
cout << "address of n2 in square2(): " << n << "\n";
// Explicit de-referencing to get the value pointed-to
*n *= *n;
}
// Pass-by-Reference with Reference Arguments
void square3(int& n)
{
// Address of n in square3() is the same as n3 in main()
cout << "address of n3 in square3(): " << &n << "\n";
// Implicit de-referencing (without '*')
n *= n;
}
void guti()
{
// Call-by-Value
int n1 = 8;
cout << "address of n1 in main(): " << &n1 << "\n";
cout << "Square of n1: " << square1(n1) << "\n";
cout << "No change in n1: " << n1 << "\n";
// Call-by-Reference with Pointer Arguments
int n2 = 8;
cout << "address of n2 in main(): " << &n2 << "\n";
square2(&n2);
cout << "Square of n2: " << n2 << "\n";
cout << "Change reflected in n2: " << n2 << "\n";
// Call-by-Reference with Reference Arguments
int n3 = 8;
cout << "address of n3 in main(): " << &n3 << "\n";
square3(n3);
cout << "Square of n3: " << n3 << "\n";
cout << "Change reflected in n3: " << n3 << "\n";
}
// Driver program
int main() { guti(); }
출력:
address of n1 in main(): 0x7fffa7e2de64
address of n1 in square1(): 0x7fffa7e2de4c
Square of n1: 64
No change in n1: 8
address of n2 in main(): 0x7fffa7e2de68
address of n2 in square2(): 0x7fffa7e2de68
Square of n2: 64
Change reflected in n2: 64
address of n3 in main(): 0x7fffa7e2de6c
address of n3 in square3(): 0x7fffa7e2de6c
Square of n3: 64
Change reflected in n3: 64
C++에서는 기본적으로 인수가 값 별로 전달되고, 호출된 함수에서 변경된 내용은 전달된 변수에 반영되지 않는다. 변경 내용은 호출된 함수에 의해 만들어진 클론으로 만들어지는 것이다. original copy를 직접 수정하거나 copy의 오버헤드를 피하기를 원한다면, 우리는 Pass-by-Reference를 사용해야 한다.
3. 포인터와 배열의 이름
배열 이름은 배열의 첫 번째 요소의 주소를 포함하며, 이는 상수 포인터와 같은 역할을 한다. 즉, 배열 이름에 저장된 주소는 변경할 수 없는 것이다. 예를 들어 val이라는 배열이 있으면, val과 &val[0]을 서로 교환하여 사용할 수 있다.
예시:
// C++ program to illustrate Array Name as Pointers
#include <bits/stdc++.h>
using namespace std;
void guti()
{
// Declare an array
int val[3] = { 5, 10, 20 };
// declare pointer variable
int* ptr;
// Assign the address of val[0] to ptr
// We can use ptr=&val[0];(both are same)
ptr = val;
cout << "Elements of the array are: ";
cout << ptr[0] << " " << ptr[1] << " " << ptr[2];
}
// Driver program
int main() { guti(); }
출력:
Elements of the array are: 5 10 20
Pointer ptr이 인수로 함수에 전송되면, 배열 val도 유사한 방식으로 접근할 수 있다.
4. 포인터 표현과 산술
다음과 같은 포인터에 대해 제한된 산술 연산 세트를 수행할 수 있다.
① 증분(++)
② 감소(--)
③ 포인터에 정수(+ 또는 +=) 추가
④ 포인터에서 정수 제거( - 또는 -= )
⑤ 두 포인터의 차(p1-p2)
*참고: 포인터 산술은 배열에서 수행되지 않으면 의미가 없다.
예시:
// C++ program to illustrate Pointer Arithmetic
#include <bits/stdc++.h>
using namespace std;
void guti()
{
// Declare an array
int v[3] = { 10, 100, 200 };
// declare pointer variable
int* ptr;
// Assign the address of v[0] to ptr
ptr = v;
for (int i = 0; i < 3; i++) {
cout << "Value at ptr = " << ptr << "\n";
cout << "Value at *ptr = " << *ptr << "\n";
// Increment pointer ptr by 1
ptr++;
}
}
// Driver program
int main() { guti(); }
출력:
Value at ptr = 0x7ffe5a2d8060
Value at *ptr = 10
Value at ptr = 0x7ffe5a2d8064
Value at *ptr = 100
Value at ptr = 0x7ffe5a2d8068
Value at *ptr = 200
5. 고급 포인터 표기법
2차원 숫자 배열에 대한 포인터 표기법을 생각해 보도록 한다. 다음 선언을 생각해 보자.
int nums[2][3] = { { 16, 18, 20 }, { 25, 26, 27 } };
일반적으로, nums[ i ][ j ] 는 *(*(nums+i)+j) 와 같다.
6. 포인터와 문자열 리터럴
string 리터럴은 null-terminated 문자 시퀀스를 포함하는 배열이다. string 리터럴은 type 문자와 terminating null 문자의 배열이며, 각 요소는 type const char(*문자를 수정할 수 없음)이다.
이것은 문자 그대로 "guti"를 나타내는 배열을 선언하고, 그 첫 번째 요소에 대한 포인터가 ptr에 할당된다. 만약 "guti"가 주소 1800에서 시작하는 메모리 위치에 저장된다고 한다면, 우리는 이전의 선언을 다음과 같이 나타낼 수 있다:
포인터와 배열이 식에서 동일한 방식으로 작동하므로 ptr을 사용하여 문자열 리터럴의 문자에 액세스할 수 있다. 예를 들면 다음과 같다:
char ptr = 0;
char x = *(ptr+3);
char y = ptr[3];
7. 포인터 to 포인터
C++에서 데이터나 다른 포인터를 가리키는 포인터를 만들 수 있다. 구문은 포인터를 선언하는 동안, 각 수준의 indirection에 대한 단항 연산자(*)를 필요로 한다.
char a;
char *b;
char ** c;
a = ’g’;
b = &a;
c = &b;
여기서 b는 'g'를 저장하는 char를 가리키고 c는 포인터 b를 가리킨다.
8. 보이드(Void) 포인터
이것은 유형이 없음을 나타내는 C++에서 사용할 수 있는 특수한 유형의 포인터다. 보이드 포인터는 유형이 없는 값을 가리키는 포인터인 것이다. 따라서 길이가 결정되지 않고, 참조 해제 속성도 결정되지 않는다. 보이드 포인터는 어떤 데이터 유형을 가리킬 수 있기 때문에 큰 유연성을 가지고 있다고 해석할 수도 있다. 이러한 포인터는 직접 참조 해제될 수 없다. 우선적으로, 이러한 포인터는 참조 해제되기 전에 특정 데이터 유형을 가리키는 다른 포인터 유형으로 변환되어야 한다.
예시:
// C++ program to illustrate Void Pointer
#include <bits/stdc++.h>
using namespace std;
void increase(void* data, int ptrsize)
{
if (ptrsize == sizeof(char)) {
char* ptrchar;
// Typecast data to a char pointer
ptrchar = (char*)data;
// Increase the char stored at *ptrchar by 1
(*ptrchar)++;
cout << "*data points to a char"
<< "\n";
}
else if (ptrsize == sizeof(int)) {
int* ptrint;
// Typecast data to a int pointer
ptrint = (int*)data;
// Increase the int stored at *ptrchar by 1
(*ptrint)++;
cout << "*data points to an int"
<< "\n";
}
}
void geek()
{
// Declare a character
char c = 'x';
// Declare an integer
int i = 10;
// Call increase function using a char and int address
// respectively
increase(&c, sizeof(c));
cout << "The new value of c is: " << c << "\n";
increase(&i, sizeof(i));
cout << "The new value of i is: " << i << "\n";
}
// Driver program
int main() { geek(); }
출력:
*data points to a char
The new value of c is: y
*data points to an int
The new value of i is: 11
9. 유효하지 않은(Invalid) 포인터
포인터는 유효한 주소를 가리켜야 하지만, 반드시 (*배열의 경우와 같이) 유효한 요소를 가리켜야 하는 것은 아니다. 이를 유효하지 않은, Invalid 포인터라고 한다. 초기화되지 않은 포인터도 Invalid 포인터다.
int *ptr1;
int arr[10];
int *ptr2 = arr+20;
여기서 ptr1은 초기화되지 않으므로 유효하지 않은 포인터가 되고, ptr2는 arr의 경계를 벗어나므로 유효하지 않은 포인터가 된다.
*참고: 유효하지 않은 포인터가 반드시 컴파일 오류를 발생시키는 것은 아니다.
10. 널(Null) 포인터
Null 포인터는 그저 유효하지 않은 주소가 아닌, 아무 곳도 가리키지 않는 포인터다. 포인터를 Null로 할당하는 방법은 2가지이다.
int *ptr1 = 0;
int *ptr2 = NULL;
'C++' 카테고리의 다른 글
C++ 댕글링, 보이드, 널, 와이드 포인터 (0) | 2024.05.16 |
---|---|
C++ 포인터 산술(Pointer Arithmetic) (1) | 2024.05.01 |
C++ 포인터와 레퍼런스 정리 (0) | 2024.04.29 |
C++ 람다식(Lambda expression) 총정리 (2) | 2024.04.25 |
C++사용자 정의 예외 (0) | 2024.04.25 |