본문 바로가기

별걸다하는 IT/프로그래밍언어

[C,C++ 강좌]C언어의 꽃 포인터 총정리(*, &), 포인터 사용 예시, 포인터 연산자

[C/C++ 완전정복 링크 ]

 

C/C++ 목차, C/C++강좌, 링크 모음

C언어 문법 C언어란? C/C++언어 역사 및 특징 C/C++ 개발환경 비주얼스튜디오(Visual Studio) 설치 및 빈 프로젝트 생성 비주얼스튜디오 단축키 정리 (Visual Studio shortcuts) C/C++ 개발환경 이클립스(eclipse..

jhnyang.tistory.com

C언어의 핵심! 꽃! 포인터!!

포인터는 C언어가 고급언어인데도 Low 레벨 언어의 특성을 지닌다고 이야기하게 만든 장본인입니다. 포인터가 왜 중요하냐!! 바로 메모리를 직접적으로 접근하고 제어할 수 있게 해주기 때문이죠.

이렇게 컴퓨터의 하드웨어에 접근하는 특성 때문에 게임같이 메모리나 성능이 중요한 프로그램들이 C나 C++로 만들어집니다.

이 외에도 운영체제단처럼 직접적으로 하드웨어와 소통해야 하는 프로그램들은 대부분이 전~부! C로 만들어져 있어요

 

자바를 먼저 접한 사람은, 자바에는 포인터라는 것이 없던데 쓸 모 없는 것이냐 할 수도 있지만 노노!

자바 언어 기반이 C언어예요. 결국 내부적으로 C가 다 포인터를 이용해서 처리해줬기 때문에 보이지 않았을 뿐이랍니다.

 

1. 변수는 메모리에 어떻게 저장되는가 - 메모리 주소 

포인터란 결국 메모리 주소와 연관되어 있는 문법이예요.

이를 이해하기 위해 먼저, 우리가 데이터를 저장하면 메모리에 어떻게 저장이 되는지 알고 넘어갑시다.

#include <stdio.h>
int main()
{
    int n = 10;
    double d = 3.141592;
    return 0;
}

n이라는 이름을 가진 박스에다가 10을 저장했다' 라는 말은 곧 10을 메모리 어딘가에다가 저장하고 우리가 쉽게 쓸 수 있게 n이라는 이름을 붙인겁니다. 그림을 보면 아래와 같습니다.

메모리 위치는 같은 코드라도 작성할 때마다 바껴요. 

00120B14라는 메모리 주소에 int 크기 즉 4byte만큼 공간을 잡고 10을 넣어줬네요 (항상 C언어는 메모리 시작번지만 가지고 위치를 표현하고 거기서부터 자료형의 크기만큼 공간을 할당합니다, 즉 그림상에서는 00120B14라는 위치를 잡고 정수 크기인 4바이트만큼 쭉 공간을 할당한거네요!) 

실수도 마찬가지로 double크기인 8byte만큼 시작주소부터 공간을 잡은 다음에, 3.141592 값을 넣고 d라고 이름을 붙여줬어요.

n, d 이런 이름을 통해 우리가 복잡한 메모리 주소를 기억하지 않아도 쉽게 호출할 수 있는거죠!

 

 

 

 

 

 

 

 

 

2. &연산자, 포인터란? 포인터의 쓰임. 

그런데 우리는 가끔 메모리 주소를 알아야 할 때가 있어요. 참고로 눈치챘겠지만 메모리 주소값을 저장할 수 있는 자료형이 포인터입니다! 포인터도 int(정수를 저장하는 자료형), char(문자를 저장하는 자료형)처럼 결국 하나의 자료형, 타입이예요!

→ 포인터도 하나의 타입이다.

포인터가 필요한 예

포인터가 언제 필요하냐...예를 들어 우리가 함수를 작성했다고 생각해봅시다!

#include <iostream>
using namespace std;
void add(int a, int b, int sum)  //a와 b를 더해 sum에 저장하는 함수.
{
    sum = a + b;
}
int main()
{
    int a = 3, b = 4;
    int sum = 0;
    add(a, b, sum);  //add함수 호출
    cout << sum;
    return 0;
}

이렇게 add라는 함수를 정의해줬어요. 그리고 호출했습니다.

(출력 값은 0입니다) 

그런데 왜 sum값이 3+4인 7이 아니고 0이죠?!?! main에 있는 sum에 값이 저장되지 않은 것을 확인할 수 있습니다.

add함수 인자로 값을 넘겨 준 뒤, 함수 내부에서 덧셈 연산을 수행해도 메인 sum 값에 영향을 미치지 못한 것을 확인할 수 있어요. 

 

왜냐! 실제 sum의 주소 값이 인자로 넘겨지는 것이 아니기 때문이죠.

add(a, b, sum)의 sum과 void add(int a, int b, int sum)의 sum이 이름은 같지만 같은 상자가 아닌거예요. 동명이인이랄까. 그러니 아무리 더해도 서로 상관관계가 없어서 main의 sum에는 영향이 못미친 겁니다.

한 번 소스를 통해 직접 확인해볼까요

#include <iostream>
using namespace std;
void add(int a, int b, int sum)
{
    sum = a + b;
    cout<<"함수 안 sum 메모리 위치"<<&sum<<"\n";
}
int main()
{
    int a = 3, b = 4;
    int sum = 0;
    add(a, b, sum);
    cout<<"main sum 변수의 메모리 위치"<< &sum<<"\n";
    return 0;
}

여기서 변수명 앞에 &를 붙여주면 실제 잡힌 메모리 위치를 보여줍니다.

일반적으로 &연산자와 *연산자를 포인터 연산자라고 합니다.

'scanf("%d", &a);' 많이 썼던 출력 함수죠? 여기 &도 결국 주소를 의미합니다. a라는 변수의 주소에다가 값을 받아서 저장시켜달라는거죠~ 

아무튼 저 코드의 실제 출력 값을 확인해보면 

달라요!! 정확한 메모리 위치 값은 신경쓰지 않아도 됩니다. 어차피 휘발생 RAM메모리라 프로그램 껐다 키면 또 바껴있을 값이예요. ㅎㅎ 여기서 초점은 같은 sum인데도 저장공간이 다르다. 즉 다른 변수이다가 뽀인트 입니당!

sum이라고 적혀있는 상자가 두 개 있는 거랑 마찬가지예요. 이름만 같을 뿐 실제 값을 저장하고 있는 저장공간은 다른거죠.

#include <iostream>
using namespace std;
void add(int a, int b, int *sum)
{
    *sum = a + b;
    cout<<"함수 안 sum 메모리 위치"<<sum<<"\n";
}
int main()
{
    int a = 3, b = 4;
    int sum = 0;
    add(a, b, &sum);
    cout<<"main sum 변수의 메모리 위치"<< &sum<<"\n";
    return 0;
}

그럼 기존의 sum의 값을 변경시키고 싶으면 어떻게 해야할까요? 바로 매개변수에 sum이 저장되어 있는 주소를 직접적으로 알려주는거예요. 그러면 그 주소에 직접적으로 접근하는 것이니 내가 의도하고자 했던 sum을 가리키게 되고 값 변경이 적용되겠죠!

주소값이 같아 가리키는 값이 같다!

이것은 포인터가 쓰이는 한 예일 뿐입니다. 

포인터 특성상 주소 값을 저장할 수 있기 때문에 실제 값이 담긴 변수명을 몰라도 접근할 수 있어요 이렇게 다른 변수의 주소를 직접적으로 가리켜 변수명 없이도 접근하는데 사용하기도 합니다. 실제로 포인터는 다양한 상황에서 유용하게 또 필수적으로 쓰여요! 앞으로 많이 만나게 될 친구입니다 ㅎㅎㅎ

 

3. 포인터 문법.

[ ※ 선언할 때!, 주소 값을 저장 할 때 ]

=> 포인터 변수를 선언할 때는 데이터형을 먼저 쓰고, *를 쓴 다음, 변수명을 적어줍니다.

곱하기할 때 사용하는 연산자 '*'와 기호는 같지만 다른 연산자인 거예요.

 

'num1 * num2' 에서 별표(*)는 곱하기 연산자인거고

여기서 설명하고자 하는 연산자는 포인터연산자 입니다.

연산자 aestrisk(*)는 쓰임새에 따라 역할이 달라져요.

 

포인터 변수를 선언할 때

왼쪽 사진처럼 데이터형과 *를 함께 써주는데, *기호가 해당 변수를 포인터 타입으로 만듭니다.

 

char*   :  char형 변수의 주소 값 

int*     :  int형 변수의 주소 값 

double*: double형 변수의 주소! 알겠죠? 

'int* a;'이든 'int * a'이든 'int *a' 이든 별표의 위치는 어디든 상관없어요.

 

 

[ ※ 포인터 초기화 ]

#include <iostream>
int main()
{
	int* ptr1 = nullptr;  
	float* ptr2 = NULL;
	return 0;
}

포인터 변수 값이 할당되지 않았을 경우,

초기화는 NULL로 해주시면 됩니다. NULL을 의미하는 nullptr 키워드를 사용해서 초기화해도 돼요.

 

[ ※ 포인터 변수의 자료형 역할 ]

double a;하면 자료형은 8바이트를 메모리상에 잡아 놓는 역할을 해요. a변수의 크기는 8바이트가 되는거죠. 

근데 포인터의 경우 double* a;했을 때 8바이트를 만들라는 뜻으로 double이 있는 것이 아닙니다.

어차피 주소값은 4바이트로 일정하기 때문이죠.

즉 포인터 변수의 크기는 포인터 변수가 가리키는 변수의 데이터형에 관계없이 4바이트로 항상 같습니다. 

여기서 자료형은! 포인터 변수의 크기가 아닌, 포인터 변수가 가진 주소가 가리키는 값이 int형 변수다 라는 뜻이예요!

그래야 그 주소값을 찾아갔을 때 내가 얼만큼의 크기를 읽어들여야 하는지 알 수 있으니까요 ㅎㅎ

#include <iostream>
using namespace std;
int main()
{
    double num = 3.1415;
    double* numAddress = &num;
    cout << sizeof(numAdress)<<"\n";
    cout << sizeof(num) << "\n";
    return 0;
}

실수 num의 주소 값을 numAddress라는 포인터 변수에다가 저장해줬습니다. 

출력값은 numAddress의 크기는 4가 되고 num의 크기는 8을 나타냅니다. 

(주소는 결국 정수 값이기 때문에 4바이트, 실수 double은 8바이트의 크기를 가지기 때문)

 

[ ※ 한 번에 선언하기 ]

선언시 주의 사항은?

#include <iostream>
using namespace std;
int main()
{
    int a, b;
    int *c, d;
    int *e, *f;
    return 0;
}

'int a, b;' 는 결국 'int a; int b;'랑 같아서 포인터도 한 번에 이어쓰려고 6라인처럼 쓰는 사람들이 있는데, 정확히는 7라인처럼 써줘야 'int*e; int*f;'로 적용됩니다. 6라인은 c는 포인터 변수, d는 int 변수가 돼요.

 

※ 포인터 역참조 ]

*의 쓰임법은 선언할 때 말고 호출할 때도 사용됩니다. 자료형 없이 ' * 포인터변수명' 하면 (ex 'cout << *ptr' 처럼 )

포인터변수에 저장되어 있는 주소를 역으로 참조해 그 주소가 가지고 있는 실제 값을 가져옵니다. 

#include <iostream>
using namespace std;

int main()
{
    double num = 3.1415;
    double* numAddress = &num; //선언
    cout << sizeof(numAddress)<<"\n";
    cout << sizeof(num) << "\n";
    
    cout << "num: " <<num<< "\n";
    cout<<" &num: "<<&num << "\n";
    cout<<" numAddress: "<<numAddress << "\n";
    cout<<" &numAddress: "<<&numAddress << "\n";
    cout<<" *numAddress: "<<*numAddress << "\n"; //호출
    return 0;
}

 

*numAddress 호출을 하면 numAddress에 저장되어 있는 주소 값 00B3FA00을 찾아가서 그 안에 저장된 값 3.1415를 가져오는 것을 볼 수 있어요. 이를 상자에 빗대어 그림으로 표현해봤어요! 

나중에 이중 포인터, 삼중 포인터 하다보면 머리가 혼돈이되니 개념을 단단히 잡아놓는게 좋습니다. ㅎㅎ

 

4. 포인터 사용시 주의사항

포인터는 선언과 초기화를 같이 하면 좋지만 때에 따라 따로 해줘야 할 때가 있는데요. 

int main()
{
    int * ptr;
    *ptr = 100;
}

3줄처럼 포인터 변수를 선언만 하고 초기화하지 않으면, 포인터 변수는 쓰레기 값으로 초기화 돼요. 이런 상태에서 * 연산을 통해서 100을 저장하는 것은 치명적인 결과로 이어질 수 있습니다. ptr이 가리키는 위치를 모르니까요! (정말 중요한 곳이라면 운영체제가 차단하긴 하지만..ㅎㅎ)

따라서 나중에 주소값을 저장할 예정이라면 위의 초기화 섹션에서 설명드렸듯이 널포인터로 초기화하는 것이 바람직합니다.

NULL로 초기화 한 후, 나중에 주소값을 대입하지 않은 상태에서 포인터 연산을 해버리면 메모리 참조가 잘못되었다는 에러 메세지를 많이 접하게 될 겁니다.ㅎ.ㅎㅎ

NULL로 초기화 하고 꼭 나중에 매칭 잘 시켜준 후 연산하기!

 

포인터와 배열은 엄청 긴밀한 관계를 갖고 있어요. 다음시간에 이에 대해 살펴볼게요 

▼ 배열과 포인터 ! https://jhnyang.tistory.com/329

 

[C언어, C++언어 강좌] 배열과 포인터 상관관계 완벽히 이해하기~! ARRAY and POINTER! 헷갈리는거 다 알

[C언어 C++언어 프로그래밍 완전정복 목차] 안녕하세요 양햄찌 블로그 주인장입니다. 저번 포스팅에서 간략하게 배열과 포인터에 대해 알아봤는데요. 사실 배열과 포인터 사이에는 아주 밀접한 �

jhnyang.tistory.com

오늘도 고생하셨습니다. 도움되셨다면 공감 죠아요~!