C언어

C언어의 포인터 기본 개념

Jerry_K 2024. 10. 5. 09:37

✨ C언어 포인터

C언어의 포인터는 메모리 주소를 저장하는 변수이다.

(변수나 배열의 값을 간접적으로 접근하고 조작하는 방식)

 


💡도대체 포인터는 왜 필요할까 ? 

포인터를 사용하는 이유는 다음과 같다. 

 

1. 포인터는 특정 변수나 데이터의 메모리 주소를 가리켜서 직접적으로 메모리 접근이 가능하다.

2. 포인터를 사용하면 함수를 호출할 때 값을 복사하지 않고 주소를 전달하여, 메모리 낭비를 줄이고 직접 수정이 가능하다.

3. 동적으로 메모리를 할당하여 프로그램의 메모리사용을 효율적으로 관리한다.  

4. 복잡한 자료구조 (연결 리스트, 트리, 그래프 등 ) 동적 자료 구조 구현에 필수적이다.

 

💡그렇다면 왜 !! 이중 포인터가 필요할까 ? 

1. 다차원 배열을 처리할때, 이중 포인터로 각 행을 동적으로 할당하거나 관리 할 수 있다.

2. 함수에서 포인터 자체를 변경해야 할 때 이중 포인터를 사용한다.

3. 이중 포인터는 2차원 동적 배열을 할당하고 해제하여 유연한 배열 구조를 만들 수 있다


🔖 포인터 선언

int *p;

 

정수를 가리키는 포인터를 선언

& 연산자주소를 p에 저장한다. 

* 연산자는 포인터가 가리키는 주소의 값을 참조할 수 있다. 

 

#include <stdio.h>

int main()
{
    int a = 10 ;
    int *p = &a ;
    
    printf("a의 값 : %d \n",a);
    printf("a값의 주소  : %p \n", &a);
    printf("p의 값 : %p \n",p);
    printf("p가 가르키는 값 : %d \n", *p);
    
    return 0;
}

 

 

 

💡포인터에 타입이 있는 이유 ? 

포인터가 가리키는 데이터의 크기와 형식을 알아야한다. 

 

1. 포인터 연산 ( p + 1 등)을 할 때, 가리키는 데이터의 크기만큼 이동해야함 

ex) int *은 p+1 이 4 바이트 이동하지만, char *는 1바이트만 이동 

 

2. 포인터의 타입은 해당 메모리 주소에 접근할 때 데이터 형식을 결정한다. 

 

 

🔖 상수 포인터 (const)

1. 상수를 가리키는포인터

#include <stdio.h>

int main()
{
    int a,b;
    const int* ptr = &a; 
    
    //값 변경 불가능
    // *ptr = 10 ; 
    
    // 주소 변경 가능
    ptr = &b; 
    
    return 0;
}

 

포인터가 가리키는 값을 변경할 수 없지만, 

포인터의 주소를 변경하여 다른 변수를 가리키도록 할 수 있다.

 

2.주소를 변경할 수 없는 상수 포인터

#include <stdio.h>

int main()
{
    int a,b;
    int* const ptr = &a; 
    
    //값 변경 가능
    *ptr = 10 ; 
    
    // 주소 변경 불불가능
    // ptr = &b; 
    
    return 0;
}

 

포인터가 지정하는 주소를 바꾸지 못하지만,

포인터가 가리키는 값은 변경 가능하다.

 

3. 값, 주소 모두 변경 할수 없는 상수 포인터

#include <stdio.h>

int main()
{
    int a,b;
    const int* const ptr = &a; 
    
    //값 변경 가능
    // *ptr = 10 ; 
    
    // 주소 변경 불불가능
    // ptr = &b; 
    
    return 0;
}

 

포인터가 가르키는 값,주소 모두 변경 불가능

 

 

💡상수 포인터를 사용하는 이유는 ?

1. 코드 안정성 (실수로변경되는 것 방지) 

2. 가독성 향상 

3. 최적화 (컴파일러가 상수로 인식하여 메모리 최적화)

최적화 방법 : 불필요한 메모리 접근 제거 
→ const로 선언된 값은 변경되지 않아 메모리에 접근하지 않고 캐시 또는 레지스터에 상수를 저장하여 참조 속도를 높힌다.

 

 

🔖포인터의 덧셈

#include <stdio.h>

int main()
{
    int a ; 
    int* pa ; 
    pa = &a;
    
    printf("%p\n",pa);
    printf("%p\n",pa+1);
    
    return 0;
}

 

포인터에 덧셈을 하면 해당 데이터 타입만큼 더해진다.

 

#include <stdio.h>

int main()
{
    int a;
    char b;
    double c;
    
    int *pa = &a;
    char *pb = &b;
    double *pc = &c;
    
    printf("(int)pa 값 : %p\n",pa);
    printf("(int)pa +1 값 : %p\n\n",pa+1);
    
    printf("(char)pb 값 : %p\n",pb);
    printf("(char)pb +1 의 값 : %p\n\n",pb+1);
    
    printf("(double)pc 값 : %p\n",pc);
    printf("(double)pc +1 값 : %p\n\n",pc+1);
    
    return 0;
}

 

int4 바이트, char1 바이트, double8바이트 식 더해졌다. (16진수 더하기)

 

 

🔖 배열과 포인터 

 

배열들의 각 원소는 메모리 상에 연속되게 놓여있다. 

#include <stdio.h>

int main()
{
    int arr[5] = {5,6,7,8,9};
    int *parr  = &arr[0] ;
    

    for(int i=0; i<4;i++){
        printf("arr[%d] : %d | parr+%d : %d\n",i,arr[i],i,*(parr+i));
    }
   return 0;
}

 

그래도 배열은 배열이고 포인터는 포인터이다... !! 

 

 

🔖 이중 포인터 

 

 

🔖 2차원 배열 구조 

 

#include <stdio.h>
int main() {
  int arr[2][2];

  printf("arr[0] : %p \n", arr[0]);
  printf("&arr[0][0] : %p \n\n", &arr[0][0]);

  printf("arr[1] : %p \n", arr[1]);
  printf("&arr[1][0] : %p \n", &arr[1][0]);

  return 0;
}

 

arr[0]의 값은 arr[0][0]의 주소값과 같고, 

arr[1]의 값은 arr[1][0]의 주소값과 같다. 

 

 

🔖 배열 포인터 

#include <stdio.h>

int main() {
  int arr[3] = {1, 2, 3};  
  int (*parr)[3] = &arr;  

  printf("arr[1] : %d \n", arr[1]);      
  printf("parr[1] : %d \n", (*parr)[1]); 

  return 0;
}

 

💡배열 포인터는 왜 필요할까 ?   

(이거는 그냥 배열 복사 붙여넣기 아닌가라 생각했다...?)

 

배열 포인터는 배열의 시작 주소를 참조하여, 

배열이 저장된 메모리의 주소만 포인터에 저장한다. 

 

이렇게 할 경우 메모리를 낭비하지 않고 배열의 원소들에 직접 접근 수정 가능하다. 

 

#include <stdio.h>
int main() {
  int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
  int (*parr)[3] = arr ;
  
  printf("parr[1][2] : %d, arr[1][2] :%d \n",parr[1][2],arr[1][2]);

  return 0;
}

 

parr은 크기가 3인 배열을 가리키는 포인터이다. 

1차원 배열에서 배열의 이름첫번쨰 원소를 가리키는 포인터로 타입 변환이 된 것 처럼,

2차원 배열에서 배열의 이름첫 번째 행을 가리키는 포인터로 타입 변환이 되야한다. 

 

 

#include <stdio.h>
int main() {
  int arr[2][3];
  int brr[10][3];
  int crr[2][5];

  int(*parr)[3];

  parr = arr;  // O.K
  parr = brr;  // O.K
  parr = crr;  // 오류!!!!

  return 0;
}

 

위의 예시를 통해 좀 더 이해 할 수 있을 것이다. 

 

 

🔖 포인터  배열

#include <stdio.h>
int main() {
  int *arr[3];
  int a = 1, b = 2, c = 3;
  
  arr[0] = &a;
  arr[1] = &b;
  arr[2] = &c;
  
  printf("a : %d, *arr[0] : %d \n", a ,*arr[0]);
  printf("b : %d, *arr[1] : %d \n", b ,*arr[1]);
  printf("c : %d, *arr[2] : %d \n", c ,*arr[2]);
  
  printf("&a : %p, arr[0] : %p \n", &a, arr[0]);
  
  return 0;
}

 

포인터 배열은 말 그래도 포인터들로 구성된 배열이다.

 

 

🔖 포인터 받는 함수 인자

#include <stdio.h>

int change_val(int *pi){
    printf("pi가 가르키는 값 : %d\n", *pi);
    printf("pi의 값 : %p\n", pi);

    *pi = 3 ;
    return 0;
}

int main(){
    int i = 0;

    printf("i 변수의 주소값 : %p \n", &i);
    printf("호출 이전의 i 값 : %d \n",i);
    change_val(&i);
    printf("호출 이후의 i 값 : %d \n",i);
    return 0 ;
}