Develop/C, C++

[C, C++] 포인터(Pointer)

JuniTech 2023. 12. 18. 22:47
728x90
C/C++

이번 포스팅에서 우리가 배워볼 것은 다음과 같다.

  1. 포인터
  2. 포인터 변수의 선언과 사용 예
  3. 포인터 증감 연산
  4. 포인터는 어디에서 사용되는가?
  5. 포인터 사용시 주의점 : nullptr
  6. 포인터와 배열
  7. 심화 : 포인터 구조 확인

1. 포인터

포인터

  • 포인터 변수메모리의 주소를 저장하는 변수를 의미한다.

 

Pointer 변수 선언

  • data_type *variable_name; 또는 data_type* variable_name;
    •  최근에 푸른 색을 더 선호하는 편이다.
    • 저렇게 선언을 하면 컴퓨터는 변수의 주소를 받을 준비가 되어 있다고 생각한다.

 

Pointer 연산

  • 포인터 연산
    • 변수의 주소를 계산하는 연산자[주소 연산자] : &
      • 변수의 메모리 주소 값 : &variable
      • 시작하는 위치의 주소를 계산한다!
      • 아래 사진에서 number의 주소를 1008이라고 하면 포인터 변수 p에 number 변수의 주소 1008이 들어가게 되고 이에 따라 *p가 가리키는 값은 10이 된다.
      • int number = 10; // 정수형 변수 number 선언 
        int *p; // 포인터 변수 p 선언 
        p = &number; // 변수 number의 주소가 포인터 p로 대입​
      • 주소 연산자 &
  •  
    •  
    • 간접 참조 연산자 * : 포인터가 가리키는 값을 가져오는 연산자
      • Pointer 변수가 가리키는 메모리 공간의 내용을 가져오려면 *pointer_variable 를 사용하면 된다.
      • #include <iostream> 
        
        using namespace std; 
        int main() { 
          int number = 10;  // 변수 number의 주소를 계산하여 p에 저장한다.
          int *p = &number;
        
          cout << *p << endl;  // p가 가리키는 공간에 저장된 값을 출력한다.
        
          return 0;
        }​
         

 

CF) *의 세 가지 다른 의미

  1. 곱셈 연산자 (이항 연산) : a = b * c;
  2. Pointer variable 선언 : void separate(double num, char* signp, int* wholep, double* fracp)
  3. Expression에서 "포인터가 가리키는 곳"(derefernecing)
int i = 10; // 정수형 변수 i 선언 
int *p; // 포인터 변수 p 선언 
p = &i; // 변수 i의 주소가 포인터 p로 대입 
printf("%d", *p); // 출력값 : 10

 

2. 포인터 변수의 선언과 사용 예

(1)

#include <iostream>

using namespace std;

int main() {
  char c1;
  char c2 = 'B';
  char *signp;
  signp = &c2; // signp: c2의 주소를 가리킵니다.
  c1 = *signp; // c1에 *signp의 값, 즉 c2의 값이 "복사"됩니다. 이 시점에서 c1은
               // c2와 같은 값이 됩니다.
  *signp = 'A'; // *signp가 c2를 가리키는데, 이를 통해 c2의 값이 'A'로 변경됩니다.

  cout << "signp 값(=c2 주소) : " << signp << endl; // signp가 가리키는 메모리에 저장된 값, 즉 'A'를 출력합니다.
  cout << "c1 주소 : " << &c1 << endl;
  cout << "c2 주소(=signp 값) : " << &c2 << endl;
  /* 정확한 메모리 주소를 출력하려면 주소값을 문자열로 변환하지 않도록 조치해야 합니다. 
  여러 가지 방법이 있지만 가장 간단한 방법은 reinterpret_cast를 사용하여 주소값을 정수로 
  캐스팅한 후 출력하는 것입니다. 다음은 코드를 수정하여 주소값을 정수로 변환하여 출력하는 방법이다.*/
  cout << "c1 주소 : " << reinterpret_cast<void*>(&c1) << " /" << " c2 주소 : " << reinterpret_cast<void*>(&c2) << " /" << " signp의 주소 : " << &signp << endl;
  cout << "c1 값 : " << c1 << endl; // c1의 값, 즉 c2의 초기 값이 출력됩니다.
  cout << "c2 값 : " << c2 << endl; // c2의 현재 값, 즉 'A'가 출력됩니다.
}

(1)의 결과

 

(2)

#include <iostream>

using namespace std;

int main() {
  int i = 33;
  int j = 44;
  int k = 0;
  int *p, *q;  // 선언에서 *p, *q는 정수를 가리키는 포인터입니다.
  p = &j;    // p는 1004로 할당됨 – 이는 j의 주소입니다
  i = j;    // i는 44로 지정되어 있으며, 이는 j의 값입니다
  k = *p;    /*식에서 "dereference p"로 읽음 – */
            /* p의 값은 1004 입니다. "deliverse 1004"는 1004번지에서 44라는 값을 가져온다는 것을 의미합니다." */
  *p = 88;  /*"dereference p" - p(주소값 1012)에 저장하는 것이 아니라, 1004번지에 있는 곳에 저장한다.
  (p의 값을 '주소' & '그 주소에 저장하는용'으로 사용된다.*/

  cout << i << endl;
  cout << j << endl;
  cout << k << endl;
  cout << p << endl;
  cout << q << endl;
}

(2)의 결과
(2)의 결과를 그림으로 나타낸 것


 

3. 포인터 증감 연산

포인터의 증가 연산의 경우, 이란 변수와는 약간 다르다. 증가되는 값은 포인터가 가리키는 객체의 크기이다. 따라서 가리키는 객체의 크기만큼 증가하게 된다.

포인터 타입 ++ 연산 후 증가되는 값
char 1
short 2
int 4
float 4
double 8

예시 : int형 포인터 변수 i가 있을 때 i++ 하게 되면 아래와 같이 바뀌게 된다.

(처음 i의 값) ===> ===> ===> (p++ 후 i의 값)          

 

4. 포인터는 어디에서 사용되는가?

  • 포인터 변수를 이용해서 주소 값을 통해 결국 변수에 접근하게 된다. 그러면 여기서 '굳이 포인터 변수를 사용하지 않고, 원래 있던 변수를 바로 참조하면 되는 것이 아닌가?'하고 의문을 가질 수 있다.
  • 여기서 우리가 알아야 할 점은 일반 변수는 메모리를 컴퓨터가 관리하고, 포인터로 관리하는 변수는 개발자가 직접 메모리를 관리할 수 있다는 점이다. 즉, 장치 드라이버나 동적 메모리 할당에서 메모리를 주소로 참조해야 하는 경우가 발생하는데 이 때 포인터가 사용되는 것이다.
  • 또한, 함수의 포인터 형식 인자를 통한 실제 인자 접근(access)할 때 사용한다.
    • 함수의 인수 전달 방법으로 '값에 의한 호출(call by value)'와 '참조에 의한 호출(call by reference)'가 있다. '값에 의한 호출'은 C에서 기본적인 방법이며, '참조에 의한 호출'은 C에서 포인터를 이용하여 흉내낼 수 있다. 따라서 포인터 형식 인자를 통한 실제 인자 접근은 '참조에 의한 호출'이다.
      • call by value(값에 의한 전달) : 실매개변수의 값을 형식매개변수로 복사하여 전달하는 기법
        • 실매개변수의 값이 호출되는(called) 함수의 형식매개변수에 복사가 된다.
        • 호출되는(called) 함수에서 형식매개변수의 값을 변경해도 호출하는 함수(caller) 쪽에서 실매개변수 값에 영향을 미치지가 않는다.(애초에 복사를 한 것이라 실체가 다르기 때문!)
      • call by reference(참조에 의한 전달) : 실매개변수의 주소를 형식매개변수로 전달하는 기법
        • 즉, 실체가 똑같게 된다.
    • Call by value의 예시 - 변수 2개의 값을 바꾸는 작업을 함수로 이용하기
      • #include <iostream> 
        
        void swap(int x, int y); 
        
        using namespace std; 
        int main() { 
          int a = 100, b = 200; 
          swap(a, b); cout << "a : " << a << endl; 
          cout << "b : " << b << endl; 
          return 0; 
        } 
        void swap(int x, int y) { 
          int tmp; 
          tmp = x; 
          x = y; 
          y = tmp; 
        }
      • 위 코드의 결과
        이 코드는 a와 b의 값을 바꿀려고 만든 코드인데, 실제로는 바뀌지가 않는다. 왜냐하면 함수 swap(100, 200)를 호출하면 실매개변수 a, b 값이 함수 swap의 형식매개변수 x, y로 각각 전달이 되는데 원본을 복사해서 전달하기 때문이다. 함수 swap에서 x와 y의 값을 바꾸지만, 수행이 끝난 후, 호출한 곳으로 돌아오면 a와 b의 값은 변하지 않는 것이다.
    • Call by reference의 예시
      • #include <iostream>
        
        void swap(int *px, int *py);
        using namespace std;
        
        int main() {
          int a = 100, b = 200;
        
          swap(&a, &b);
          cout << "a : " << a << endl;
          cout << "b : " << b << endl;
          return 0;
        }
        
        void swap(int *px, int *py) {
          int tmp;
        
          tmp = *px;
          *px = *py;
          *py = tmp;
        }
      • 위 코드의 결과
        함수 호출시에 원본 주소가 복사되며, 함수 내에서 값이 변경됨에 따라 동시에 원본값도 바뀌는 것을 알 수 있다.
  • 동적할당 ( vs 정적할당)
    • 정적할당은 배열 int a[10];같은 경우이며 항상 10개로 고정이다.
    • 동적할당은 배열의 크기를 조절할 수 있으며, 포인터를 이용하지 않고는 동적할당 이용할 수가 없다.
    • 동적할당에 관한 포스팅은 다음을 참고하자.
  •  
  • 지금까지 : 포인터 사용의 총 장점 정리
    • 메모리에 직접 접근이 가능
    • 구조화된 자료를 만들어 효율적 운영이 가능
    • Call by Reference
    • 배열, 구조체 등의 복잡한 자료 구조와 함수에 쉽게 접근
    • 메모리 동적 할당

 

5. 포인터 사용시 주의점 : nullptr

  • NULL 포인터란 말 그대로 아무 것도 가리키고 있지 않은 포인터를 말한다.
  • 만약 포인터가 선언만 되고 초기화되지 않았다면 포인터는 임의의 주소를 가리키게 된다.
    • => 여기서 포인터를 이용하여 메모리의 내용을 변경한다면 문제가 발생한다.
  • 따라서 포인터가 아무 것도 가리키고 있지 않을 때는 nullptr로 설정해야 한다.
  • NULL 값도 정수 0이여서 약간의 문제가 있다.
  • Ex) int *p = nullptr; (O) / int *p = NULL; (X)

 

6. 포인터와 배열

  • 배열과 포인터는 아주 밀접한 관계를 가지고 있다.
  • 배열 이름이 바로 포인터이다.
  • 포인터는 배열처럼 바로 사용이 가능하다.
  • 인덱스 표기법을 포인터에 사용할 수 있다.
  • 사진
  • 예시) int S[5];
    • 배열 이름 S는 &S[0]과 같다. 
      S[0] S[1] S[2] S[3] S[4]
    • *S = 10;	// S[0] = 10;
      *(S + 1) = 20;	// S[1] = 20;
      *(S + i) = 30;	// S[i] = 30;​


  • 배열 매개변수 VS Pointer 매개변수
    • Array 변수는 pointer 변수로 취급되기 때문에 함수 정의에서  array 변수를 인자로 사용하는 것과 pointer 변수를 인자로 사용하는 것은 같다.
    • /* 아래의 두 prototype은 동등함. */
      double sum(double a[], int n);
      double sum(double *a, int n);
    • 코드 예시 - 배열의 원소들의 합을 구하는 함수
    • #include <iostream>
      
      using namespace std;
      
      double sum(double a[], int n){
        double sum = 0.0;
        int i;
        for (i = 0; i < n; i++) {
          sum += a[i];
        }
        
        return sum;
      }
      
      double sum_by_pointer(double* a, int n) {
        double sum = 0.0;
        int i;
        
        for (i = 0; i < n; i++) {
          sum += a[i];  // sum += *(a+i);
        }
        
        return sum;
      }
      
      int main() {
        double v[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        cout << sum(v, 10) << endl;  // sum(&v[0], 10);  1 ~ 10 까지의 합
        cout << sum_by_pointer(v, 10) << endl;    // 1 ~ 10 까지의 합
        cout << sum_by_pointer(&v[7],3) << endl;  // 8 + 9 + 10
        
      }
    •  

결과

 

7. 심화 : 포인터 구조 확인

#include <stdio.h>
#include <iostream>

using namespace std;

int main() {
	int var = 10;
	int* p_var = NULL;

	cout << sizeof(var) << endl;
	cout << sizeof(p_var) << endl;

	int a[10];
	int length = sizeof(a) / sizeof(int);
	for (int i = 0; i < length; i++) {
		a[i] = i * i;
	}

	int* point = &a[0];
	for (int i = 0; i < length; i++) {
		int* now = point + i;
		wcout << "i [" << i << "] > 메모리 주소 " << now << "배열의 값 : " << *now << endl;
	}

	return 0;
}

결과

위 코드를 통해 알 수 있는 점

  • 포인터 변수는 64비트 운영체제에서 8바이트 크기를 가진다.
    • 8byte * 8bit = 64bit
    • 0x... 으로 시작하는 것은 16진수를 의미한다.
    • 16진수는 4비트로 표현 가능하다 -> 64/4 = 16자리
    • 그래서 실행결과 화면에 16자리로 나오는 것이다.