본문 바로가기
Tech/Coding

C 언어 기본] 포인터

by redcubes 2024. 3. 23.

목차

A. 포인터의 기본 개념

B. 포인터 이해하기

C. 요약 및 정리

A. 포인터의 기본 개념

{선언된 블록 내부에서만 사용할 수 있는 스코프 규칙}을 벗어나 데이터 공유 가능한 포인터

____1. 메모리의 주소, 주소 연산자

- 주소값은 바이트 단위로 구분. 0부터 바이트 단위로 1씩 증가.
2바이트 이상의 크기를 갖는 변수는 여러개의 주소값에 걸쳐 할당.

int a; //메모리 100번지부터 할당되었다면 100~103까지 4바이트에 걸쳐 할당됨.
변수 선언 이후에는 4바이트 전체를 a라는 이름으로 사용.

- 주소 연산자 &

#include <stdio.h>

int main(void) {
  int a;
  double b;
  char c;

  printf("int형 변수의 주소: %u\n", &a);
  printf("double형 변수의 주소: %u\n", &b);
  printf("char형 변수의 주소: %u\n", &c);

  return 0;
}

&는 변수명을 피연산자로 시작 주소를 구한다.

791 792 793 794 795 796 797 798 799 800
char double  
801 802 803 804 805 806 807 808 809 810
      int      

+ 주소는 보통 16진수로 표기. 전용 변환 문자 %p
주소값의 데이터 크기에 따라 자릿수를 맞춰 16진수 대문자로 출력(소문자로 나오는데?)
시스템에서 주소 값 자체를 8바이트로 처리한다면 16진수 한 자리는 4비트에 해당하므로
주소값 10번지는 000000000000000A 와 같이 16진수 16자리로 출력

____2. 포인터와 간접 참조 연산자 *

포인터: 메모리의 주소를 저장하는 변수
선언할 때 *를 붙임.

#include <stdio.h>

int main(void) {
  int a;   //일반 변수 선언
  int *pa; //포인터 선언

  pa = &a;  //포인터에 a의 주로를 대입하고
  *pa = 10; //포인터로 변수 a에 10 대입

  printf("포인터로 a값 출력: %d\n", *pa);
  printf("변수명으로 a값 출력: %d\n", a);

  return 0;
}
자료형 *변수명;  //포인터 선언 변수의 자료형을 적음.

pa → a 포인터 pa는 변수 a를 가리킨다.
포인터 pa로 변수 a를 사용할 수 있음. *pa 는 a와 가리키는 메모리 주소가 같음.

* 는 간접참조연산자라고 함.

scanf로 입력할 때는 입력할 변수가 메모리 어디에 할당되었는지 저장 공간의 위치를 알아야 함.(저장 공간의 주소를 함수 인수로 전달함.)

*pa와 a가 같으므로 &a는 &*pa와 같은데 이것은 사실상 포인터(주소)가 기리키는 주소의 주소를 구하는 것이므로 pa와 같습니다.

scanf("%d", &a);
scanf("%d", &*pa);
scanf("%d", pa);

다 같은 말.

- 여러가지 포인터 사용해보기 : 포인터를 이용한 두 정수의 합과 평균 계산

#include <stdio.h>

int main(void) {
  int a = 10, b = 15, total;   //변수 선언과 초기화
  double avg;
  int *pa, *pb; //포인터 선언
  int *pt = &total; //포인터 선언 및 초기화(주소 저장)
  double *pg = &avg;

  pa = &a;  //포인터에 a의 주로를 대입하고
  pb = &b;

  *pt = *pa + *pb; //*pt는 total, *pa는 a, *pb는 b
  *pg = *pt / 2.0;

  printf("두 정수의 값 : %d, %d\n", *pa, *pb);
  printf("두 정수의 합 : %d\n", *pt);
  printf("두 정수의 평균 : %.1lf\n", *pg);
  
  return 0;
}

____3. const를 사용한 포인터

가리키는 변수의 값을 바꿀 수 없다는 의미.

#include <stdio.h>

int main(void) {
  int a = 10, b = 20;
  const int *pa = &a;             // 포인터 pa는 변수 a를 가리킨다.

  printf("변수 a 값 : %d\n", *pa);  // 포인터를 간접 참조해 a출력
  pa = &b;                        // 포인터가 변수 b를 가리키도록 변경
  printf("변수 b 값 : %d\n", *pa);   // 포인터를 간접 참조해 b출력
  pa = &a;                         // 포인터가 변수 a를 가리키도록 변경
  a = 20;                          // 변수 a에 20을 대입(직접 참조)
  printf("변수 a 값 : %d\n", *pa);   // 포인터를 간접 참조해 a출력
  
  return 0;
}

const를 써도 잘만 바뀐다. 여기서 const는 간접참조를 할 수 없다는 의미.

*pa = 20;

을 하면 에러가 발생됨.
왜 포인터에 const를 쓰나?

B. 포인터 이해하기

  • 포인터는 주소를 저장하는 메모리 공간.
  • 언제든 다른 주소를 저장하거나 포인터끼리 대입 가능.
  • 일반 변수와 달리 대입 연산에 엄격한 기준이 있음.

____1. 주소와 포인터의 차이, 주소와 포인터의 크기

- 주소: 변수에 할당된 메모리 저장 공간의 시작 주소 값 자체. 바뀌지 않음. 상수 주소. 
- 포인터: 그 값을 저장하는 또 다른 메모리 공간. 바꿀 수 있음. 주소 변수. 두 포인터가 같은 변수를 동시에 가리킬 수 있음.

&a = &b 는 불가능(상수이므로 대입연산자 왼쪽에 올 수 없음.)

포인터의 크기: 저장할 주소의 크기에 따라 결정.(크기가 클 수록 더 넓은 범위의 메모리 사용 가능)

각 컴파일러별로 포인터 크기가 다를 수는 있으나 한 컴파일러 내에서 모든 주소와포인터는 가리키는 자료형과 관계없이 그 크기가 같다.

sizeof 연산자로 확인해보기

#include <stdio.h>

int main(void)
{
  char ch;
  int in;
  double db;

  char* pc = &ch;
  int* pi = &in;
  double* pd = &db;

  printf("char형 변수의 주소 크기 : %d\n", sizeof(&ch));
  printf("int형 변수의 주소 크기 : %d\n", sizeof(&in));
  printf("double형 변수의 주소 크기 : %d\n\n", sizeof(&db));

  printf("char * 포인터의 크기 : %d\n", sizeof(pc));
  printf("int * 포인터의 크기 : %d\n", sizeof(pi));
  printf("double * 포인터의 크기 : %d\n\n", sizeof(pd));

  printf("char * 포인터가 가리키는 변수의 크기 : %d\n", sizeof(*pc));
  printf("int * 포인터가 가리키는 변수의 크기 : %d\n", sizeof(*pi));
  printf("double * 포인터가 가리키는 변수의 크기 : %d\n", sizeof(*pd));

  return 0;
}

____2. 포인터의 대입 규칙

  1. 가리키는 변수의 형태가 같을 때만 대입해야 함.
    포인터가 변수의 영역을 넘어가거나 잘려서 예상하지 못한 결과가 나타남.

  2. 형 변환을 사용한 포인터의 대입은 언제나 가능함.
    자료형이 달라도 형 변환 연산자를 사용하면 대입 가능.

#include <stdio.h>

int main(void)
{
	int a = 10;             // 변수 선언과 초기화
	int* p = &a;            // 포인터 선언과 동시에 a를 가리키도록 초기화
	double* pd;             // double형 변수를 가리키는 포인터

	pd = p;                 // 포인터 p 값을 포인터 pd에 대입
	printf("%lf\n", *pd);   // pd가 가리키는 변수의 값 출력

	return 0;
}

double a = 3.4;//`double` 타입의 변수 `a`를 선언하고, 3.4라는 실수 값을 저장합니다.
double *pd = &a;//`double` 타입을 가리키는 포인터 변수 `pd` 선언, 변수 `a` 주소 저장. `pd`-> `a`
int *pi;// `int` 타입을 가리키는 포인터 변수 `pi`를 선언
pi = (int *)pd; //pd 값을 형 변환해 pi에대입

pd가 가리키는 주소를 int 타입의 포인터로 형 변환하여 pi에 대입.
여기서 형 변환((int *))이 필요한 이유는 pd와 pi가 서로 다른 타입의 데이터를 가리키고 있기 때문.
pd는 double 타입의 데이터의 주소를 가리키고 있고, pi는 int 타입의 데이터의 주소를 가리킬 것으로 선언.
형 변환을 통해, 본래 double 타입의 주소를 가리키던 pd의 값을 int 타입의 포인터 변수인 pi에 할당할 수 있게 됨.

하지만, 이러한 형 변환은 주의해서 사용해야 합니다. 왜냐하면, double과 int는 내부적으로 데이터를 표현하는 방식이 다르기 때문에, pi를 통해 해당 주소에 접근하려고 할 때 예상치 못한 결과나 오류가 발생할 수 있습니다. 예를 들어, double 타입의 데이터는 int 타입으로 직접적으로 변환될 때 정보의 손실이 발생할 수 있으며, 메모리에서의 해석 방식도 달라집니다. 따라서 실제 프로그래밍에서는 이와 같은 직접적인 형 변환을 신중하게 사용해야 합니다.

C언어에서 double 타입의 변수를 int 타입의 포인터로 형 변환하여 사용하는 것은 주의를 요하는 작업입니다. 예제 코드에서 보인 것처럼 double 타입의 변수 a의 주소를 double 포인터 pd에 할당하고, 이후 pd를 int 포인터 타입으로 형 변환하여 pi에 대입하는 과정은 가능하지만, 이렇게 함으로써 pi를 통해 a의 메모리를 int 타입처럼 해석하고 접근하게 됩니다.

변수 a는 double 타입이므로, 일반적으로 8바이트(64비트 시스템 기준)의 메모리 공간을 사용합니다. 반면, int 타입은 시스템에 따라 다르지만 보통 4바이트(32비트)를 사용합니다. pd를 int 포인터인 pi로 형 변환하면, pi를 통해 a의 메모리 첫 부분의 4바이트만을 int 값으로 해석하게 됩니다.

이런 방식으로 a의 일부를 int형 변수처럼 사용할 수 있지만, 몇 가지 주의해야 할 점이 있습니다:

  1. 정보의 손실: double 값의 전체 정보를 int 타입으로 변환하면, 소수점 이하의 정보가 손실됩니다. 이 경우, double의 메모리 일부만을 사용하므로, 변환 과정에서 데이터의 해석이 부정확할 수 있습니다.
  2. 엔디안(endian) 문제: 메모리의 바이트 순서(엔디안)에 따라 int 포인터로 접근했을 때 해석되는 값이 달라질 수 있습니다. 즉, 같은 코드가 다른 시스템에서는 전혀 다른 결과를 낼 수 있습니다.
  3. 정렬(alignment) 문제: 일부 시스템에서는 특정 타입의 변수를 메모리의 특정 정렬에 맞춰야만 합니다. double과 int는 다른 정렬 요구 사항을 가질 수 있으며, 이로 인해 성능 저하 또는 런타임 오류가 발생할 수 있습니다.

따라서, 이런 방식은 특정한 저수준 프로그래밍 작업에서 메모리를 직접 제어할 필요가 있을 때 제한적으로 사용됩니다. 일반적인 상황에서는 타입에 맞는 변수와 포인터를 사용하여 타입 안전성을 유지하는 것이 좋습니다. 데이터의 형 변환이 필요한 경우, 안전한 방법(예: 데이터를 명시적으로 변환하는 함수 사용)을 통해 이루어져야 합니다.

____3. 사용하는 이유

- 임베디드에서 메모리 직접 접근할 때.

-동적 할당한 메모리를 사용하는 경우.

- 두 변수의 값을 바꾸며 포인터 이해하기

#include <stdio.h>

void swap(int* pa, int* pb);   // 두 변수의 값을 바꾸는 함수의 선언

int main(void)
{
	int a = 10, b = 20;        // 변수 선언과 초기화

	swap(&a, &b);              // a, b의 주소를 인수로 주고 함수 호출
	printf("a:%d, b:%d\n", a, b);   // 변수 a, b 출력

	return 0;
}

void swap(int* pa, int* pb)    // 매개변수로 포인터 선언
{
	int temp;                  // 교환을 위한 임시 변수

	temp = *pa;                // temp에 pa가 가리키는 변수의 값 저장
	*pa = *pb;                 // pa가 가리키는 변수에 pb가 가리키는 변수의 값 저장
	*pb = temp;                // pb가 가리키는 변수에 temp 값 저장
}

스왑 함수와 메인 함수가 변수 a,b를 공유할  수 있게 됨. 인수로 전달하게 되면 값을 복사하므로 결국 연산 뒤 리턴해서 원래 변수에 대입하는 과정이 필요.

C. 요약 및 정리

____1. 요약

- 포인터는 메모리 사용법, 주소 연산자(&)로 메모리 위치 확인, 포인터로 가리키는 변수 사용할 때 간접 탐조 연산자(*)씀.

-주소는 상수 포인터는 변수, 포인터와 주소는 크기가 같음. 자료형이 같아야 주소를 저장할 수 있음.

-포인터의 주요 기능: 함수 간 데이터 공유.

____2 . 문제풀이

-

#include <stdio.h>

void swap(double *pa, double *pb) { // 두 수치를 바꾸는 함수
    double temp = *pa;
    *pa = *pb;
    *pb = temp;
}

void line_up(double *maxp, double *midp, double *minp) { // 함수 선언
    if (*maxp < *midp) {
        swap(maxp, midp); // maxp가 midp보다 작으면 바꾼다
    }
    if (*maxp < *minp) {
        swap(maxp, minp); // maxp가 minp보다 작으면 바꾼다
    }
    if (*midp < *minp) {
        swap(midp, minp); // midp가 minp보다 작으면 바꾼다
    }
}

int main(void) {
    double max, mid, min;

    printf("실수값 3개 입력 : ");
    scanf("%lf%lf%lf", &max, &mid, &min);

    line_up(&max, &mid, &min); // 세 변수의 값을 크기 순으로 정렬하는 함수 호출

    printf("정렬된 값 출력 : %.1lf, %.1lf, %.1lf\n", max, mid, min);

    return 0;
}

____3. 궁금한 점

추후(이해 후) 정리.

https://www.pinterest.co.kr/pin/only-a-c-programmer-can-understand-it--441704675961038850/    https://www.pinterest.co.kr/pin/only-a-c-programmer-can-understand-it--441704675961038850/