[C] 형 변환과 포인터

Gyuwon Lee·2022년 6월 14일
1

42 Seoul 7기

목록 보기
4/6
post-thumbnail

📢 42서울 7기 라피신을 대비하여 모두의 코드 '씹어먹는 C 언어' 강좌의 내용을 정리한 게시물로, 이전 블로그로부터 옮겨 온 글입니다. (원문)

1. 타입 캐스팅 (형 변환)

가끔씩 프로그래밍을 하다 보면 형이 다른 변수 끼리 대입을 하는 연산을 필요로 한다.

예를 들어서 double 형 변수의 값을 int 형 변수에 대입하거나, float 형 변수에 double 형 변수의 값을 대입하는 경우다.

하지만 형이 다른 변수 끼리의 대입이나 연산은 컴파일이 불가능하거나, warning 을 띄우게 된다.

#include <stdio.h>
int main() {
  int a;
  double b;

  b = 2.4;
  a = b;

  printf("%d", a); // 2
}
  • int 형 변수에 double 형 변수를 대입하면 소수 부분이 잘려서 정수 부분만 들어간다.
  • 컴파일러는 이에 따른 데이터 손실에 대한 warning 을 띄운다 (error 가 아니기에 컴파일은 된다).

여기서는 컴파일러가 암묵적으로 형 변환을 해주었지만, 다른 사람에게 이 코드를 보여준다고 생각하면 명시적으로 형 변환을 밝혀 주는 것이 좋겠다.

#include <stdio.h>
int main() {
  int a;
  double b;

  b = 2.4;
  a = (int)b;

  printf("%d", a); // 2
}
  • a의 자료형은 int로 선언되어 있으므로 b의 앞에 (int)를 붙여 형 변환 시킨 값을 a에 넣어주겠다는 뜻이다.

형 변환 자체가 어려운 개념은 아니지만, 어쨌든 내가 선언한 변수가 컴파일 과정에서 다른 타입으로 변환되어 계산될 수 있다는 사실을 인지하는 것은 중요하다.

이후 포인터를 본격적으로 이야기하게 되면 '배열'과 '배열 이름'은 완전히 다르다는 사실을 계속 이야기할 것이다.

int arr[10];

이 배열 arr의 타입은 int[10]이고, sizeof&연산자를 사용한 연산을 제외하고는 변수명 arr를 사용할 때 int * 타입으로 형변환된다. 그래서 컴파일러 상에서는 포인터 연산으로 해석된다.

당최 무슨 소리인지! 차차 이해하게 될 것이다.


2. 포인터

지금부터 이어지는 글은 온갖 포인터 관련 강의들, 정리들과 스택오버플로우를 떠돌며 나에게 필요한 포인터 관련 개념을 총정리한 내용으로, 개인적으로 일독을 권한다고 나름 자신있게 말할 수 있다.

사실 그냥 그렇구나~ 하고 받아들이면 포인터를 공부하며 크게 헤멜 일은 없을지도 모른다.

하지만 어째서? 정확한 원리가 뭐지? 저렇게 연산되는 이유는 뭐지? 와 같은 의문을 끊임없이 던진다면 포인터에 대해 최소 하루이틀은 온종일 붙잡고 있어야 할 수도 있다.

1) 포인터를 공부하기 전에

몇 가지만 알아두자.

#include <stdio.h>
int main() {
  int arr[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

  printf("arr 배열의 2 행 3 열의 수를 출력 : %d \n", arr[1][2]);
  /*  arr 배열의 2 행 3 열의 수를 출력 : 6 */
  return 0;
}
  • 2 차원 배열이나 1 차원 배열 모두 메모리 상에서 연속적으로 쭈르륵 존재한다.
  • 메모리의 한 '방'의 크기는 보통 1바이트로 정의된다.
  • 컴퓨터는 이 각각의 '방'들을 구분하기 위해 고유의 주소를 붙인다.
int a = 10;
  • 위 코드는, 컴파일러를 거쳐 "메모리의 특정 방에서부터 4 바이트(방 4개)의 공간에 있는 데이터를 10 으로 바꾸어라!" 라는 명령으로 변환된다.

2) 포인터도 변수다!

포인터는 우리가 앞에서 보았던 intchar 변수들과 다른 것이 전혀 아니다. 포인터도 변수다.

int 형 변수가 정수 데이터, float 형 변수가 실수 데이터를 보관했던 것 처럼, 포인터도 특정한 데이터를 보관하는 변수다.

바로, 특정한 데이터가 저장된 주소값을 보관하는 변수인 것이다.

포인터

  • 메모리 상에 위치한 특정한 데이터의 (시작)주소값을 보관하는 변수!
  • (포인터에 주소값이 저장되는 데이터의 형) *(포인터의 이름);
int* p; // int 타입 데이터를 가리키는 포인터 변수

부연하자면 포인터 역시 형(type)을 가지며, 초기화 시 어떤 타입의 데이터를 가리킬 것인지 명시해주어야 한다.


3) & 연산자

단항 연산자 &는 비트 연산자 AND 와 같은 기호를 사용하지만 다르게 해석된다.

어떤 데이터(변수)의 앞에 사용하여 다음과 같이 연산된다:

/* & 연산자 */
#include <stdio.h>

int main() {
  int a;
  a = 2;

  printf("%p \n", &a);
  return 0;
}
/* 포인터의 시작 */
#include <stdio.h>
int main() {
  int *p;
  int a;

  p = &a;

  printf("포인터 p 에 들어 있는 값 : %p \n", p);
  printf("int 변수 a 가 저장된 주소 : %p \n", &a);

  /* 포인터의 값과 변수의 주소가 모두 동일한 값으로 출력될 것이다 */

  return 0;
}

4) * 연산자

이렇게 & 연산자를 사용하여 변수(데이터)가 저장되어 있는 메모리 주소를 꺼내올 수 있다면, 반대로 해당 주소의 메모리에 저장되어 있는 실제 값을 꺼내서 사용할 수도 있어야 하는 것이 당연하다.

"나(포인터)를 나에게 저장된 주소값에 위치한 데이터로 생각해줘!"

#include <stdio.h>
int main() {
    int *p;
    int a;

    p = &a;
    a = 10;

    printf("a의 값: %d". a); // 10
    printf("*p의 값: %d". *p); // 10
}
  • 포인터 변수 p는 변수 a의 메모리 주소를 담고 있었다.
  • 앞에 *연산자를 붙임으로써 본인이 가리키고 있던 그 메모리 주소에 저장되어 있던 실제 값을 가져왔다.

즉, 변수 a*p는 정확히 동일하다.

반대로 생각하면, 포인터를 통해 특정 메모리 주소에 저장되어 있는 값을 그대로 가져올 뿐만 아니라, 해당 값에 접근해 직접 수정할 수도 있다.

#include <stdio.h>
int main() {
    int *p;
    int a;

    p = &a;
    a = 10;
    *p = 5

    printf("a의 값: %d". a); // 5
    printf("*p의 값: %d". *p); // 5
}

🤔 포인터를 왜 쓰는건데?

이렇듯 포인터는 &연산자를 사용하여 어떤 값이 저장된 '메모리' 주소로 접근할 수 있고, *연산자를 사용하여 그 값을 꺼내어 사용할 수도 있는 변수라는 아주 기본적인 개념을 알아보았다.

C에서는 이 포인터가 대체 왜 이렇게 중요하게 다뤄질까? 그 이유는 C언어가 컴퓨터를 효율적으로 사용할 수 있도록 설계된 프로그래밍 언어이기 때문이다.

다른 예시로 자바스크립트는 브라우저를 제어하기 위해 탄생한 언어다. 같은 맥락에서 C언어는 하드웨어를 제어하고 메모리를 조작하는 등 제한을 두지 않고 프로그래머에게 강력한 제어구조를 제공한다.

포인터를 사용하면 메모리 주소를 참조하여 배열과 같은 연속된 데이터에 접근과 조작이 용이하다. 원소의 크기를 알면 메모리 주소를 통해 포인터 연산만으로 간편하게 접근할 수 있다. 이는 추후 이야기할 '동적 할당된 메모리 영역(힙 영역)에 대한 접근 및 조작'에서 아주 중요한 역할을 한다. 또한 메모리에 직접 접근하므로 메모리 공간을 효율적으로 사용할 수 있다.

자바스크립트의 경우, 변수 값이 바뀌면 기존 값이 저장되어 있던 메모리 공간은 바로 비워지지 않고 컴파일러가 임의로 나중에 삭제한다. 값이 수시로 바뀌어야 하는 경우 컴파일러에 의한 임의의 딜레이가 쌓이면 비효율을 초래할 수 있을 것이다.

하지만 C는 포인터를 통해 메모리에 직접 접근할 수 있으므로 메모리 공간을 내가 필요한 만큼만 사용하거나 확보할 수 있다.

profile
하루가 모여 역사가 된다

0개의 댓글