Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
C/Pointer

C- Pointer 포인터 개념 및 활용 (포인터 변수)

by Toughie 2024. 2. 17.

목차 바로가기 🚀

 

01. C에 대한 첫인상

02. 메모리와 프로세스

03. 포인터란?

04. 포인터 자료형

05. 포인터 변수

06. 주소 연산자 &

07. 포인터 연산자 *

08. 포인터 변수의 사이즈

09. 포인터 변수의 타입

10. 포인터와 배열

11. 문자열 포인터

12. 문자열 상수 포인터

13. Call by Value, Call by Reference

14. 포인터 반환

15. const 지정자


🤔  C에 대한 첫인상

Python, Swift, Java를 배우다 드디어 C언어를 학습하게 되었다. 

막 엄청나게 복잡하고 문법도 괴랄 할 줄 알았는데 다행히? 막상 해보니 그 정도는 아닌 것 같다..ㅋㅋㅋ 

확실히 최신 언어들이 C언어 기반이 맞구나 싶은 부분들도 많았다.

 

예전에는 언어 하나라도, 하나만 제대로 파는게 좋지 않아?라는 생각에 강하게 사로잡혀 있었는데

자의든 타의든 여러 언어를 맛보게 되면서 각 언어의 공통점/차이점을 알아가는 것이 오히려 재미있게 느껴졌다.

물론 문법이 조금씩 다르기 때문에 헷갈리는 건 어쩔 수 없지만.. 

 

특히나 앱개발을 공부하면서 코드의 동작 방식에 대한 탐구를 했던 기간이 있는데, 

메모리 구조와 함수의 동작 방식에 대한 더 깊은 이해를 하기 위해서는 C의 포인터 개념이 필수적이지 않을까 싶다.

약간 머릿속으로 감만 잡았던 것들이 좀 더 명확해지는 기분이랄까..

 

본 포스팅은 스스로 포인터에 대한 개념 정리를 위해서, 훗날 다시 복기하기 위해서 작성해 본다.

구글에 포인터 치면 엄청나게 잘 정리된 포스팅도 많겠지만 스스로 작성하는 것이 더 기억에 남는 것 같다.

엄청 딥한 내용까지는 아니고 포인터의 기본적인 내용과 관련 개념들을 다룰 예정이다.


💻 메모리와 프로세스

비휘발성 장치인 메모리. Ram

물리적인 개념보다는 운영체제 개념까지 접목된 상태에서 간단히 바라보자.

예전 안드로이드 휴대폰을 사용해 봤다면, 램이 12기가라고 해서 샀는데 설정에서 확인해 보면 

남은 메모리 용량이 절대 12기가가 아닐 것이다. 이는 운영체제가 메모리 공간을 사용하고 있어서 그렇다.

 

'모든 프로그램은 메모리에 적재돼서 실행된다.'

 

메모리는 크게 '커널 영역'과 '사용자 영역'으로 나눌 수 있다.

커널 영역에는 iOS, Android 같은 운영체제 및 관련 유틸리티 프로그램이 적재되고,

우리가 앱을 실행하면 이 앱은 사용자 영역에 적재된다. 

 

메모리에 올라와서 실행 중인 프로그램이 바로 프로세스이다. 

(윈도우 작업관리자에서 프로세스 종료가 바로 이 프로세스이다.)

 

물리 메모리의 용량이 정해져 있더라도(4G, 8G 등) 운영체제는 가상 메모리 개념을 사용해서

프로세스 메모리를 관리한다. 개발자들은 실제 물리 메모리 용량을 크게 고려하지 않아도 되고,

운영체제 입장에서도 다중 프로세스들의 메모리 관리를 더 효율적으로 할 수 있다. 

* 32비트 운영체제에서는 프로세스당 4G까지 할당이 가능하다. (레지스터의 크기와 동일하다.)

메모리 관련으로 페이지 테이블, 디스크 Swap 등 더 복잡한 관련 개념들이 있지만 여기서는 생략한다.

 

🗺️ 메모리 주소

메모리 공간은 주소라는 개념을 사용한다. (낮은 번지부터 높은 번지 000x번지 등)

텅 빈 공간에 프로세스들이 둥둥 떠다니면 바로바로 찾아가기가 힘들 것이다.

그렇기 때문에 특정한 주소만 알면 그 주소로 바로 찾아갈 수가 있을 것이다. (이것이 Random Access다!)

메모리에 적재되는 모든 데이터는 운영체제를 통해 빈 공간을 할당받고, 주소를 통해 데이터를 구분한다.

 

여기서는 포인터 개념을 다룰 것이니, 프로세스의 메모리 구조인 코드, 데이터, 힙, 스택을 위주로 이해한다.

 

출처


🎯 Pointer ?

레이저 포인터를 생각해 보자. 레이저 포인터는 특정한 위치를 콕 가리킨다. 

포인터도 말 그대로 특정한 위치를 콕 가리킨다. 그 데이터가 어디 있는데?!

포인터는 실행 중인 프로세스의 임의의 주소이다.

결국 모든 프로그램은 메모리에 적재돼서 실행되는 것이기 때문에  대부분의 프로그래밍 언어에는 포인터의 개념이 있다.

다만 숨겨져 있다. 왜? 복잡하고 어려우니까.. ㅋㅋㅋ

 

C에서는 기계어, 어셈블리어처럼 메모리를 직접 조작할 수 있다는 점이 가장 큰 특징이다.

물론 메모리 할당과 해제 등 필요한 절차들을 누락하기도 쉽고, 실수로 인해서 메모리 누수가 일어나는 등 
정말 복잡하고 많은 숙련도가 필요하겠지만, 잘 다룰 줄만 알면 아주 빠르고 효율적인 프로그램을 설계할 수 있는 것이다. 

 

그래서 최신 언어에서는 위와 같은 포인터의 작업을 많이 숨기고 간결하게 만들어서 개발자의 편의를 봐주고 있다.

Swift의 ARC, 자바의 GC가 메모리 해제를 알아서 스마트하게 해주는 것처럼!

또한 필요한 경우에는 포인터를 사용할 수  있도록 하는 언어도 있다.(ex. C++)

 

다만 Class 사용 시 알아야 하는 참조형 변수 등의 개념을 더 잘 이해하려면

포인터 개념이 많은 도움이 될 것이라 생각한다. (참고로 C에는 클래스가 없어서 충격이었다. 그래서 C++ 이 나왔다.)


🎯 포인터 자료형

변수의 타입이 다양하듯, 포인터의 타입도 여러 가지이다.

메모리에 접근할 대상의 타입에 따라 포인터의 선언법과 사용법이 달라진다.

 

[ 포인터 변수, 배열 포인터, 포인터 배열, 함수 포인터, 다중 포인터, void형 포인터 ]


🎯 포인터 변수

변수에는 값을 담을 수 있다. 

포인터 변수는 다른 객체의 메모리 주소를 저장하는 변수이다.

즉 프로세스의 특정 데이터의 '시작 주소'를 대입받아서 활용가능한 것이다.


🎯 포인터 변수 선언

(*의 위치는 타입과 변수명 사이에만 있으면 된다.)

 

//타입 asterisk 변수명;
int *intPtr;
int* intPtr;
int * intPtr;

🎯 주소 연산자 &(ampersand)

변수에 할당된 데이터의 메모리 시작 주소를 반환하는 연산자.

주소 연산자는 상수나 수식의 주소로는 사용할 수 없다.

 

//불가능한 예시
int *ptr = &10;
int *ptr2 = &(num1 + num2);

 

상수나 수식은 컴파일러가 지정한 메모리 위치(임시 메모리)에 저장되기 때문에 &를 통해 주소를 얻을 수 없다.

 

포인터 변수는 초기화해주지 않으면 쓰레기 값을 가지기 때문에 꼭 초기화해줘야 한다.

그렇기 때문에 NULL로 초기화해주거나, 주소 연산자를 통해 주소를 할당해 준다.

 

int main() {

    int num = 100;
    
    int *ptr1; //선언만 해줘서 4바이트짜리 쓰레기가 생겼다.
    int *ptr2;
    int *ptr3 = NULL; //쓰레기 값 방지, NULL체크를 통해 안전하게 참조하기 위해 NULL로 초기화

    
    ptr2 = #
    ptr3 = #

    printf("%p,%p,%p",ptr1, ptr2, ptr3); //쓰레기, 같은 주소, 같은 주소
    return 0;
}

🎯 포인터 연산자 * (asterisk)

포인터 변수가 가리키는 주소에 있는 데이터를 참조하는 연산자. 

(이 주소에 뭐 있어요?)

실행문에서 *변수명은 포인터 연산을 통해 변수에 담긴 주소를 통해 데이터에 접근하는 간접참조 말한다.


🎯 포인터 변수의 메모리 할당(사이즈)

포인터 변수의 크기는(메모리에서 차지하는 크기)

'타입과 상관없이' 32bit OS에서는 4byte / 64bit OS에서는 8byte이다.

(sizeof로 출력해 보면 4바이트 혹은 8바이트가 정확히 나온다. 어떤 타입이든)


🎯 포인터 변수의 타입

포인터 변수의 타입은 반드시 참조할 데이터의 타입과 동일해야 한다!

포인터 변수는 참조할 데이터의 '시작 주소'를 가지고 있다.

시작 주소만 안다고 데이터를 온전히 참조할 수가 없다. 어디가 끝인지를 알아야 한다.

어디가 끝인지를 알 수 있도록 데이터 타입을 통일해줘야 하는 것이다.

 

만약 int 변수가 있고, 이를 참조하는 포인터 변수를 실수로 char타입으로 선언했다 생각하자. (잘못된 예시)

int는 4바이트이고 char는 1바이트이다.

 

int 변수의 시작주소가 1000번지이면, 원래는 1000번지부터 1004 번지까지가 이 데이터의 영역이다.

하지만 포인터 변수가 char타입으로 선언되었기 때문에 참조하려고 하면 1001번지까지 한 칸만 참조할 것이다.

그러면 쓰레기 값이 나온다.(이상한 값이 찍힌다.)

만약 포인터 변수를 double 타입으로 선언했다면 int 변수의 영역을 넘어가서 참조하기 때문에

또 쓰레기 값이 나온다. 그렇기 때문에 타입 통일을 꼭 명심해야 한다.

 

또한 메모리 주소는 실수가 없기 때문에, 포인터 연산에서는 무조건 '정수형 연산'만 가능하다.

(반복문에서 + i, 증감연산자 등) 연산은 자료형의 크기에 맞춰 증감한다. 

만약 정수형 포인터에 ++을 하면 4만큼 주소가 증가하는 것이다. 


🧑🏻‍💻 포인터 변수 기본 예시 

선언, 초기화, 주소 연산자, 포인터 연산자, 형식 지정자, 간접 참조, 포인터 연산

 

// 표준 입출력 라이브러리 헤더 파일(전처리 -> 컴파일 -> 링킹 단계에서 전처리 단계에서 처리된다.)
#include <stdio.h>

// 프로그램의 entry point인 메인 메서드
int main() {
    // 각 변수는 타입의 크기에 맞게 할당
    // 포인터 변수는 전부 4바이트로 할당(32bitOS)
    char ch = 'Z';
    char *cp;
    //변수 선언 방식은 위와 같이 따로, 혹은 아래와 같이 한 줄에 가능하다. 
    int num = 100, *ip;
    float fnum = 5.0, *fp;
    double dnum = 7.0, *dp;

    // 현재 포인터 변수에는 쓰레기 값들이 들어 있다. NULL로 초기화 해주지도 않았다.
    // 따라서 먼저 포인터 변수에 참조할 주소를 저장해준다. (주소 연산자 & 사용)
    cp = &ch;
    // cp라는 이름의 포인터 변수에 ch의 주소를 대입해준다.
    ip = &num;
    fp = &fnum;
    dp = &dnum;

    // 포인터 변수를 사용해서 값 출력 (간접 참조 == 역참조)
    // 포인터 변수가 가진 데이터의 시작 주소로 가서 데이터 타입의 크기만큼만 메모리를 읽는다. 
    printf("%c, %d, %f, %lf\n", *cp, *ip, *fp, *dp);
    // 이 경우 각각 1byte, 4byte, 4byte, 8byte만큼의 메모리를 읽을 것이다.
    // 다시! 포인터 변수의 크기는 모두 4byte로 동일하다!

    // * 포인터 연산자를 통해 주소에 있는 값을 변경해본다.
    // ip가 가지고 있는 시작주소로 가서 값을 간접참조 했더니 100이 있네? 100을 300으로 바꿀래.
    *ip = 300;
    printf("%c, %d, %f, %lf\n", *cp, *ip, *fp, *dp);

    // 포인터 변수가 아닌 변수에 있는 값 자체를 직접 참조해보기
    printf("%c, %d, %f, %lf", ch, num, fnum, dnum);

    //쓰레기 주소에 값을 할당하는 불상사를 방지하기 전에 포인터 변수를 NULL로 초기화 해주는 것이 좋다.
    int myNum = 1;
    int *myP = NULL;

    myP = &myNum;
    //NULL 체크
    if (myP != NULL) {
	    *myP = 7;
    }
    printf("%d", *myP); //7
    
    //포인터 연산(정수 연산만 가능하다!)
    printf("%p", ip); // 0093FAD4
    ip++; //ip에다 1을 증가시키는 것이 아니라 타입의 크기만큼 증가시킨다.
    printf("%p", ip); // 0093FAD8  int는 4바이트 이기 때문에 4바이트 만큼 증가했다.

    return 0;
}

🧑🏻‍💻 배열 활용

먼저 배열을 변수에 초기화하는 경우 변수에는 배열 객체에 대한 주소값이 할당된다. 

더 정확히는 배열의 첫 번째 요소를 가리키는 포인터 상수를 말한다. 

 

int arr[2] = {1, 2}; //int 타입 1차원 배열
int *ptr = arr; // arr은 포인터 상수

❗️ arr++; //상수는 값 변경이 불가능함! 잘못된 코드이다.

printf("%p, %p",arr, &arr[0]) //같은 주소값이 출력된다.

🍡 숫자 배열과 포인터

#include <stdio.h>

int main() {
    // 배열의 타입, 배열명, 배열 사이즈 (면, 행, 열)
    int numbers[5] = {1, 2, 3, 4, 5};
    int *ptr = NULL;

    printf("%d, %d\n", sizeof(numbers), sizeof(ptr)); //20(배열의 전체 사이즈), 4 or 8

    // 포인터 변수 초기화
    ptr = numbers;
    ptr = &numbers[0];
    
    for (int i = 0; i < 5; i++) {
        printf("%p, %d, %d, %d\n", &numbers[i], numbers[i], *(ptr + i), *(numbers + i));
    }
    // 루 프 결 과
    // 0x16fdff050, 1, 1, 1
    // 0x16fdff054, 2, 2, 2
    // 0x16fdff058, 3, 3, 3
    // 0x16fdff05c, 4, 4, 4
    // 0x16fdff060, 5, 5, 5

    return 0;
}

 

루프 결과를 살펴보자. (배열의 길이만큼 루프) 형식지정자로 주소, 정수, 정수, 정수를 받는다. 

ptr++은 가능, numbers++은 불가능.

 

&numbers[i]

배열원소를 직접 참조하고 주소연산자를 통해 그 주소를 가져온다. 

int 배열이기 때문에 주소 값이 4씩 증가하는 것을 알 수 있다.

 

numbers[i]

배열원소를 직접 참조한다.(값)

 

*(ptr + i)

배열 포인터 변수 연산을 통해 간접 참조(역참조)를 통해 값을 반환한다.

ptr은 배열의 시작 주소이다. 거기서 i (int라 4바이트) 만큼 이동한 후 해당 위치의 값을 반환한다. 

연산 우선순위에 따라 먼저  (ptr + i)를 통해 찾아갈 주소를 계산 후 * 포인터 연산자를 통해 값을 참조한다. 

ptr은 포인터 '변수'이기 때문에 *(ptr++)도 완전히 동일한 동작을 한다.

 

*(numbers + i)

numbers는 배열의 첫 번째 요소의 시작 주소이다. 하지만 '상수'라는 점에 유의하자.
따라서 numbers++는 틀린 문법이다.
배열의 첫 번째 요소의 시작 주소에서 i 만큼 이동한 후, 해당 위치의 값을 반환한다.

 

위와 같이 포인터 변수와 포인터 연산자를 사용할 때 아래 문장에 집중해 보면 좋은 것 같다. (쓰다 보면 많이 헷갈림)

"값이야?  주소야?"


🚂  문자열과 포인터

참고로 c에는 String 타입이 없다. char타입 배열을 통해 문자열을 구현한다. 

 

#include <stdio.h>

int main() {

    char str[20] = "toughie";
    char *ptr = str;

    printf("%d\n", sizeof(str)); // 20
    printf("%c, %c, %c, %c \n", str[1], *(str + 1), *str, *ptr); //o, o, t, t
    printf("%p, %s, %s\n", str, str, ptr); //0x16fdff050, toughie, toughie

    printf("=====forLoop\n");
    for (int i = 0; str[i]; i++) {
        printf("%c | %c \n", *(ptr + i), *(str + i));
    }
    
    printf("=====whileLoop\n");
    int i = 0;
    while (str[i]) {
        printf("%c ", *(ptr + i));
        i++;
    }

    printf("\n=====ptrLoop\n");
    while(*ptr) {
        printf("%c", *(ptr));
        ptr++;
    }

    return 0;
}

 

이 코드도 하나씩 뜯어보자.

 

char str[20] = "toughie"; 

char *ptr = str;

사이즈 20바이트의 문자열 배열을 선언하고 "toughie"로 초기화했다.

c에서 문자열의 경우 항상 마지막에 NULL 종료 문자인 '\0'이 들어간다. 

그렇기 때문에 20바이트의 문자열의 경우 19자까지 넣을 수 있다. 만약 종료문자의 여유공간을 두지 않으면

다른 메모리 주소를 침범하는 불상사가 생길 것이다.

 

또한 문자열 배열의 타입과 동일하게 char로 포인터 변수를 초기화해줬다. 

str은 문자열의 첫 번째 요소의 주소를 가리키는 포인터 상수라는 점을 기억하자.

 

str[1]

*(str + 1)

보통 다른 언어에서도 배열을 활용할 때는 index라는 개념, 그리고 대괄호 안에 인덱스 넘버를 넣는 서브스크립트(첨자) 문법을 사용한다. 

이게 사용자의 편의를 위해서 존재하는 기능인데, 포인터를 배우고 나면 어떻게 동작하는지 더 자세히 알 수 있다. 

 

str[1]은 str 문자열에서 2번째 문자이다. (배열은 0번부터 시작한다.)

*(str + 1)을 해석하면, str배열의 첫 번째 요소의 주소에서 1만큼 이동한다(char타입이니 주소도 1만큼). 

그 후 * 포인터 연산자를 통해 해당 주소의 값을 간접참조한다. 그러면 당연히 문자열의 1번째 문자가 나오는 것이다. 

서브스크립트 문법은 포인터 문법을 더 간편하게 사용하도록 만들어진 것이다. (컴파일 단계에서 변형되어 실행되는 것으로 알고 있음)

 

forLoop

루프 조건식을 보면 str[i]이다. 즉 직접참조를 통해 값이 있다면 true이다. 

 *(ptr + i) == *(str + i) 

 

whileLoop

포인터를 활용한 while루프와 비교하기 위한 예시다.

결국은 문자 배열에서 특정 주소에 문자가 있는지? 종료 문자가 아닌지? 체크하는 것이 골자다. 

 

ptrLoop

포인터 변수를 기깔나게 썼다. 

간접 참조를 통해 값이 있으면(종료 문자가 아니면) 해당 문자를 찍고

포인터 변수의 값을 증감 연산자를 통해 증가시킨다. 

그러다가 종료 문자를 만나면 루프가 종료된다.

(cf. 포인터 '변수'이기 때문에 가능한 것이다. str은 상수이기 때문에 str++이 안 되는 것을 기억하자.)


🚂 문자열 상수 포인터

c에서 사용하는 문자열 리터럴은 문자열 상수 포인터이다. (기본 타입처럼 값 자체가 할당되는 것이 아니라는 뜻!)

변수를 문자열 리터럴로 초기화하면 문자열이 메모리의 Data Segment( of read-only 영역)에 저장되고,

변수에는 해당 메모리의 시작주소가 들어간다.

 

#include <stdio.h>

int main() {

    char *ptr = "I'm toughie!";

    printf("%d, %p, %s", sizeof(ptr), ptr, ptr); //4, 0x100003f90, I'm toughie!

    // 런타임 에러 발생
    // Exception has occurred.
    // EXC_BAD_ACCESS
    ptr[0] = 'i';
    printf("%s",ptr);
    return 0;
}

 

문자열 리터럴은 read-only 영역에 저장되기 때문에 서브스크립트 문법 등으로 값을 변경하려고 하면 런타임 에러가 발생한다.

따라서 문자열의 값을 변경하고자 하면 문자열 배열을 선언해 주도록 하자.


🐍 Call by Value vs Call by Reference

인수 전달의 방법.

함수 호출 시 전달하는 값(argument)에 따라 호출 방식이 달라진다. 

 

Call by Value

값타입이 전달되면 해당 값이 복사돼서 함수 스코프 내에서만 사용됨. 

지역변수, 함수 실행이 끝나 스택 프레임이 소멸되면 같이 소멸됨.

 

Call by Reference

참조타입이 전달되면(주소, 포인터) 해당 주소에 대한 변경이 적용될 수 있음.

변수의 값이 아니라, 주소가 복사돼서 전달되는 것이다. 

(즉 값의 변경 가능성이 있기 때문에 추후 상수화를 통해 이를 방지하는 방법도 기술하겠다. Const)

 

#include <stdio.h>

void failSwap(int a, int b);
void succSwap(int *x, int *y);

int main() {
    int a = 100, b  = 200;
    
    failSwap(a,b);
    printf("Call by Value 함수 호출 후 a: %d, b: %d\n", a, b); // 100, 200

    succSwap(&a,&b);
    printf("Call by Reference 함수 호출 후 a: %d, b: %d\n", a, b); //200, 100

    int *aPtr = &a, *bPtr = &b;

    succSwap(aPtr,bPtr);
    printf("포인터 전달 함수 호출 후 a: %d, b: %d\n", a, b); // 100, 200

    return 0;
}

// Call by Value
void failSwap(int a, int b) {
    int tmp = a;
    a = b;
    b = tmp;
}

// Call by Reference
void succSwap(int *x, int *y) {
    int tmp = *x;
    *x = *y;
    *y = tmp;
}

 

위 코드는 함수를 통해 두 숫자의 값을 바꾸려고 하고 있다. 

 

void failSwap(int a, int b)

 

이 함수는 call by value 함수이다. 정수형 a, b 두 개의 인수를 받는다.

값들이 '복사'되어서 전달되는 것이고, 함수 블록 내부에서 복사된 값들이 사용된 후 함수 실행이 끝나면

(즉 스택프레임이 소멸하면) 메모리에서 해제된다. 

이 함수는 메인메서드(부모함수)에서 호출되었지만, 값이 복사되어 독립적이기에 두 숫자를 바꾸는 

원하는 동작이 일어나지 않은 것이다. 즉 값 복사에 의한 호출은 부모함수의 실인수에 아무런 변화를 일으킬 수 없다 !

 

 

void succSwap(int *x, int *y)

 

이 함수는 call by reference 함수이다. 포인터 변수를 인수로 받는다. 

즉 주소가 복사되어서 전달되는 것이고, 해당 주소를 참조하기 때문에 부모함수(main)의 실인수인

a와 b의 값을 바꿀 수 있는 것이다. 

함수 내부에서 복사된 값을 조작하는 것이 아니라, 주소로 찾아가서 값 자체를 조작한다고 이해하자.

 

Swift에서 inout 키워드가 있는데, 사실상 동일한 메커니즘(참조 전달 후 call by reference)이라 생각된다.

func swapInts(_ a: inout Int, _ b: inout Int) {
    let temp = a
    a = b
    b = temp
}

 

물론 언어적 특성의 차이로 인해 메모리에 직접 접근/ 간접 접근 정도의 차이는 있겠지만 intout이 *와 유사하다고 보면 될 것 같다. 

inout을 잘 사용하지는 않았지만 당시에 이해가 너무 안 됐는데 포인터를 배우니까 바로 아! 하게 된다. 재밌네..


 

또한 배열이 인수로 전달되면 call by Reference이다.

배열을 변수에 초기화하면 변수에는 배열의 값이 아니라, 배열의 시작 주소가 할당된다고 했다.

따라서 함수에 전달할 때도 배열을 전부 복사해서 전달하는 것이 아니라, 시작 주소를 복사해서 전달하는 것이다.

 

#include <stdio.h>

void add10toArr(int *ptr);

int main() {
    // arr은 배열 포인터 상수이다 !
    int arr[5] = {1, 2, 3, 4, 5};
    printf("%d, %p\n", sizeof(arr), arr); //20, 0x16fdff040 - 배열의 전체 크기, 배열의 첫 번째 요소의 주소

    add10toArr(arr);

    return 0;
}

void add10toArr(int *ptr) {

    printf("pointer: %p, pointer size: %d \n", ptr, sizeof(ptr));

    for (int i = 0; i < 5; i++) {
        *(ptr + i) = *(ptr + i) + 10;
        printf("%d ", *(ptr + i));
    }
}

포인터 반환

포인터도 타입이기 때문에 함수에서 반환될 수 있다.

 

#include <stdio.h>

int * pocketMoney();

int main () {

    int * location = pocketMoney();
    printf("%d", *location);

    return 0;
}

int * pocketMoney() {
    //in Data Segment
    static int money = 100000;
    return &money; //money의 시작 주소를 반환한다.
}

Const 지정자

Swift에서 변수는 var, 상수는 let이었다면, c에서는 상수 선언을 위해 const 지정자를 사용한다.

특히 함수에서 포인터 변수를 사용하는 경우 참조값에 대한 변경 가능성이 있기 때문에 이를 방지하기 위해 사용하는 경우가 많다.

 

#include <stdio.h>

void changeNumber(int * ptr);

int main() {

    int num1 = 1;
    const int num2 =10;
    // expression must be a modifiable lvalue;
    // num2 = 30;

    const int * ptr = &num1;
    
    // read-only variable is not assignable
    // expression must be a modifiable lvalue
    *ptr = 2;

    return 0;
}

이렇게 포인터에 대한 기본 개념과 활용 방법에 대해 정리해 보았다.

결국 포인터 핵심은 값과 주소를 구분하는 것에서 시작이라 생각한다. 

 

프로세스의 기본적인 메모리 구조인 코드, 데이터, 힙, 스택과 더불어

함수 호출 시 스택프레임 생성과 소멸, 지역변수의 개념

그리고 c에서는 없지만 다른 언어에서 class를 사용하면서 heap의 개념 등등..
(c에서는 동적 할당을 따로 할 수 있다 _배열 malloc 등)

 

'메모리 주소'에 대한 개념을 항상 인지하면서 코드를 이해하고 작성하면 더 빠르게 성장하고,

에러가 났을 때도 더욱 빠르게 수정할 수 있는 힘을 기를 수 있을 것이라 믿는다 💪🏻

 

 

추후 목차별로 부족한 부분이나 수정이 필요한 부분은 업데이트 할 예정입니다 :)

 

틀린 내용이 있다면 언제든 알려주시면 감사하겠습니다!