C++ 공부 - 모두의 코드(2)

자훈·2023년 11월 14일
0

C++ / C study

목록 보기
1/8
post-thumbnail

📌 포인터?? 가리키는 것?

모든 데이터들은 메모리 상의 특정 공간에 저장이 된다. 아파트로 비유를 해보자.
10층짜리 객체지향아파트는 각 집마다, 다른 주소를 가지고, 다른 가구원수를 가지고 있다.
내가 만약 객체지향아파트에 배달을 가게 된다면, 특정한 주소가 필요할 것이고, 그 주소에 해당하는 곳에 가야 배달을 잘 마무리 할 수 있을 것이다.
그 때, 안내표지처럼 주소를 보관하고 있는 것이 포인터이다.

📌 기본적인 포인터

int a = 10

이라고 표현한다면, 이는 메모리의 특정위치에서, 4바이트 공간을 차지하며 10이라는 데이터를 저장하고 있어라는 표현으로 볼 수 있는 것이다.

그럼 포인터는 변수가 아니냐?

아니. 포인터도 변수이다. 정확하게 말하면, 데이터의 시작 주소값을 가지고 있는 변수이다. 예를 들어보자.

#include <iostream>

int main() {
 int *a;
 *a = 10; 
return 0;
}

a라는 변수의 의미는 무엇이냐?
int형 데이터의 주소값을 저장하는 변수 a는 그 주소값에 10을 가지고 있는 것이다.

그럼 주소값을 가지고 있으면 데이터는 어떻게 가리키냐?? 정확히는 데이터 할당을 어떻게 하냐? 이다.
그 때 사용하는 것이 &엔드레퍼런스이다. 참조자라고 한다. 이는 주소값에 있는 데이터를 참조할 수 있다.

#include <iostream>

using namespace std;

int main() {
 int *a;
 int b;
 a = &b; // a는 현재 b의 주소값을 가지고 있다. 
 *a = 10 // b의 주소값을 가지고 있으며, 그걸 가리키는 주소변수에 10이라는 데이터를 할당한다.
 
 cout << *a << " " << b; 
 cout << a << " " << &b;
return 0;
}

두 가지의 출력은 같은 출력을 보여줄 것이다. 이것의 핵심은 무엇이냐?? 정해져있는 변수를 건드리지 않고 데이터를 할당한 주소값에 접근하여, 데이터를 바꿀 수 있다는 것이다! 물론 이 하나를 위해 배우는 개념은 아니니까, 계속해서 예시자료들을 통해 익혀나가보자.

📌 상수 포인터

상수 const의 의미는 데이터가 바뀔 수 없는 것을 의미한다. 만약 지정하여 바꾸기 싫은 변수가 있다면, lock처럼 잠궈놓을 수 있는 것이다. 굳이 할 필요가 있어보이나? 라고 하겠지만, 실수를 줄여주는 중요한 역할을 하므로, 고정이 필요한 변수에는 상수를 쓴다는 것을 인지해두자.

#include <iostream>

using namespace std;

int main() {
 int a;
 int b;
 const int *pa = &a;
 int* const ppa = &a;
 
 *pa = 3; //오류구문
 pa = &b; //정상작동 
 
 
 *ppa = 3; //정상작동
 ppa = &b; //오류구문 
 
return 0;
}

우리가 포인터에 대한 개념을 잘 이해하고 있다면, const int *pa가 담고 있는 것이 무엇인지 알 수 있을 것이다. 이는 int형 데이터를 가리키는데 그 값을 바꾸지 말라는 의미이다.
그렇기 때문에 *pa = 3 통해 데이터를 할당하려고하면, 오류가 발생하게 되는 것이다.

(*pa가 가리키는 것은 데이터지만, 실제 pa가 가지고 있는 것은 주소 값임을 혼동하면 안된다.)

int* const ppa = &a;const의 위치가 바뀌었다.
그럼 유추를 해보자. 이것은 ppa의 어떤 값을 바꾸지 말라고 하는 것일까? 주소값일까? 주소값에 들어있는 데이터일까??
주소값 자체를 바꾸지 말라는 선언이다. 차근히 다시 이해해보도록 하자.
int *ppa; 라는 선언을 하게 되면
*ppa는 주소값의 변수를 가리키는 것이고
ppa 자체는 주소값을 가지고 있다. (정확히는 데이터의 시작지점이다.)

그렇다면 int* const ppa = &a;ppa의 값을 바꾸지 말라고 선언하고 있으므로, 주소값을 변경시킬 수 없다는 선언이다. 즉 a의 주소를 가지고 있는 ppa의 주소값은 변할 수 없다. 그래서 아래처럼 나타나는 것이다.

 *ppa = 3; //정상작동
 ppa = &b; //오류구문 

(당연히 주소값이 가지고 있는 데이터는 바꿀 수 있다.)

📌 배열과 포인터 ?

#include <iostream>
using namespace std;
#define sp " "
#define ent "\n"

int main(){
 int pointer[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 
 for(int i = 0; i < 10; i++){
  cout << "배열의 주소값을 뽑아보자" << sp << &pointer[i] << ent;
 }

 return 0;
}

포인터 배열의 주소값을 출력하게 되면,

끝자리를 보면, int 자료형이 4바이트를 차지하는 것을 알고 있다면, 주소값이 4씩 (4바이트 만큼) 증가하는 것이 보일 것이다.

그럼 만약 포인터가 인트형 자료를 가리키고 있다면,
int *p; 로 선언하고 p+1 한다면, 자료형의 바이트 만큼 더해진다는 것이다. (여기서는 4일 것이다)

궁금하면?? 확인해보자

#include <iostream>
using namespace std;
#define sp " "
#define ent "\n"

int main(){
 int pointer[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 int *p;
 p = &pointer[0];

 for(int i = 1; i < 10; i++){
  cout << "배열의 주소값을 뽑아보자" << sp << &pointer[i] << ent;
   
  if(&pointer[i] == (p + i)){
    cout << "주소값이 일치합니다. " << ent; 
  }
 }
 cout << pointer << ent;
 return 0;
}

빙고!
사진이 좀 거대한 것이 굉장히 불편하긴 하지만... 줄이는 방법이 따로 없는 것 같다..

cout << pointer << ent; pointer 배열의 이름을 출력하면 뭐가나오냐??
배열의 시작주소 &pointer[0]가 나온다.

포인터가 어떻게 돌아가는지 안다면, 이제 배열과 포인터의 관계가 눈에 들어 온다.
&pointer[3] = (p+3)은 같은 것이다.

그럼 int *a[3];은 뭘까??

int main() {
    int a = 1, b = 2, c = 3;
    char k = '1', q = '2', p = '3';
    int *arr[3] = {&a, &b, &c};
    char *chr[3] = {&k, &q, &p};

    // arr[0], arr[1], arr[2]는 각각 a, b, c의 주소를 가리킵니다.
    std::cout << *arr[0] << std::endl;  // 1
    std::cout << *arr[1] << std::endl;  // 2
    std::cout << *arr[2] << std::endl;  // 3
    
    std::cout << *chr[0] << std::endl;
    std::cout << *chr[1] << std::endl;
    std::cout << *chr[2] << std::endl;

    return 0;
}

정수형 데이터의 포인터(주소)를 저장하는 배열이다.
만약 다른 데이터 char형태의 데이터를 할당하려고 하면 오류가 발생할 것이다.
자료형 + 포인터 + 배열변수의 형식은 자료형 데이터를 포인터 배열에 저장하는 것으로 생각하면 되겠다! (물론 배열 안에는 주소값이 저장되어 있는 것이다.. 잘 구분해야한다)

아래는 출력결과 사진이다.

#include <iostream>
#include <string>

using namespace std;
#define sp " "
#define ent "\n"

int main() {
    int a = 1, b = 2, c = 3;
    string k = "this is",  q = "fantasy";
    string *str[2] = {&k, &q};
    int *arr[3] = {&a, &b, &c};
    
    std::cout << *arr[0] << std::endl;  
    std::cout << *arr[1] << std::endl;  
    std::cout << *arr[2] << std::endl; 

    std::cout << *str[0] << std::endl;  
    std::cout << *str[1] << std::endl;  

    return 0;
}

물론 스트링에 대해서도 예제처럼 실행하게 되면

문제없이 출력이 된다는 점. 그래서 형식을 잘 기억하고, 포인터의 개념을 이해하면 될 것 같다.

📌 2차원 배열과 포인터 ?

그럼 2차원 배열의 주소값은 어떻게 할당되고 있을까??

#include <iostream>

using namespace std;
#define sp " "
#define ent "\n"

int main() {
  int m[3][3];

  for(int i = 0; i < 3; i++){
    for(int j = 0; j < 3; j++){
      cout << "해당 배열의 주소는 "<< &m[i][j] << ent;
    }
  }
  return 0;
}


신기하다. 혹시 파이썬을 하셨던 분들이라면, 리스트안의 리스트. 이중 리스트를 통해 2차원 배열을 구사하니까, 작동원리가 비슷하지 않을까 할 수도 있지만, 실제로 모든 배열주소를 찍어보면 정확히 자료형 바이트 만큼 차이가 난다. 즉 연속되어있는 데이터이지만, 내부적으로 다르게 인식하고 이어준다는 것으로 받아들일 수 있다.

#include <iostream>
#include <string>

using namespace std;
#define sp " "
#define ent "\n"

int main() {
  int m[3][3];

  cout << "m[0] is " << m[0] << ent;
  cout << "m[0][0] is " << &m[0][0] << ent;
  cout << "m[1] is " << m[1] << ent;
  cout << "m[1][0] is " << &m[1][0] << ent;
  return 0;
}

해당 코드를 실행해보면

해당 값들의 주소값이 동일한 것을 확인할 수 있다.
arr[0]arr[0][0] 의 주소와 같고
arr[1]arr[1][0]의 주소와 같다. 특별한 연산자가 사용되지 않을 경우 2차원 배열에서 1차원처럼 층수를 물어볼 때, 이는 암묵적으로 층수의 시작 주소값을 저장하고 있는 것을 확인할 수 있다.
arr[0]arr[0][0] 를 가리키는 포인터로 타입변환 되고
arr[1]arr[1][0]를 가리키는 포인터로 타입변환 된다는 것.

📌 포인터 배열과 배열 포인터 ?

#include <iostream>
#include <string>

using namespace std;
#define sp " "
#define ent "\n"

int main() {
  int *arr[3];
  int a = 1, b = 2, c = 3;
  arr[0] = &a;
  arr[1] = &b;
  arr[2] = &c;

  cout << *arr[0] << sp << *arr[1] << sp << *arr[2] << ent;
  cout << arr[0] << sp << arr[1] << sp << arr[2] << ent;
  cout << &arr[0] << sp << &arr[1] << sp << &arr[2] << ent;
  cout << &arr << ent;
  return 0;
}

출력 해보면 결과는 아래와 같다.

배열에 저장되어있는 주소와 실제 배열의 주소가 다르다.

배열은 생성되면서 각 배열의 주소값을 가지고 있으므로, 당연히 할당받는 주소도 다를 것이다. (변수들의 주소를 말한다)

다시 말해, 주소값을 가지고 있는 각각의 배열 공간(메모리주소)에, 변수들의 주소를 저장해놓는 다는 것이다.

어떤 방의 문을 열었더니 그 방은, 다른 방을 가르키는 주소를 가지고 있던 방이었다는 것이다. 아주 조금 더 자세한 비유는 포인터의 포인터에서 확인할 수 있다.

📌 포인터의 포인터 ???

#include <iostream>
#include <string>

using namespace std;
#define sp " "
#define ent "\n"

int main() {
  int a;
  int *pointer;
  int **pointer_p;

  pointer = &a;
  pointer_p = &pointer;

  a = 3;

  cout << "a is " << a << ent;
  cout << "a location is " << &a << ent;
  cout << "pointer has " << *pointer << ent;
  cout << "pointer location is " << pointer << ent; 
  cout << "pointer_p has " << **pointer_p << ent;
  cout << "pointer_p has also " << *pointer_p << ent;
  cout << "pointer_p location is " << pointer_p << ent;
  
  return 0;
}

우선 예제를 실행해보면

다음처럼 나오게 될 것이다. pointer location 과 pointer_p also has 가 같은 값이 나오게 됨을 볼 수 있다. 그리고 pointer_p의 location은 혼자 다른 값을 가지고 있다.

a가 3인 것은 당연하고
*a의 주소값이 저렇게 나오는 것도 이해했을 것이니 넘어가면,
pointer*pointer_p가 같은 것이 보일 것이다. 확인해보자.

pointer = &a;
pointer_p = &pointer;

pointer 에는 a의 주소값을 할당했기 때문에, 당연히 a와 같은 주소값으로 나온다.

**pointer_p*(*pointer_p)이고 *pointer_ppointer와 같기 때문에, *pointer가 되어, a의 주소값에 들어있는 데이터 3을 가르키게 되는 것이다.

그럼 이중포인터 변수 pointer_p의 주소값은 왜 혼자 다르냐??
그 주소값에 pointer의 주소값을 담고 있는 것이다!!

다시 한 번 아파트 비유로 돌아가보자
우리 객체지향 아파트에서 각 호수주소를 의미하고 그 주소가구원들이라는 데이터가 할당되어있다고 했다. 그럼 이중포인터는 뭐냐?? 객체지향 아파트 301호라고 적혀있어 배달을 갔더니 문앞에,

301호 택배는 302호에 놔주세요.

라고 적혀있는 것이다.
즉 데이터의 주소를 다른 주소값의 데이터로 보관하고 있는 것이 이중포인터이다.

본 내용은 씹어먹는c++을 통해 알게된 내용을 개인적인 공부를 위해 정리한 포스팅입니다. 저작권을 해칠 의도가 없으며, 모든 것은 해당 블로그 저자의 지식재산입니다.
https://modoocode.com/210

0개의 댓글