C언어 포인터 정리

개발자 강세영·2023년 5월 17일
0

TIL

목록 보기
55/70
post-thumbnail
post-custom-banner

포인터란?

  • 포인터란 기억공간의 주소를 가리키는(pointing) 자료형이다.
  • C언어에서 변수가 선언되면 기억장소에 변수의 값을 저장하고 주소로 각각의 위치를 구별한다.
  • 포인터 변수는 실질적인 데이터가 아니라 데이터가 저장된 기억장소의 주소를 저장한다.
  • 어떤 변수든 차지하고 있는 기억공간의 영역이 존재하고, 그 영역은 반드시 주소(번지)가 부여되어 있으며 이에 대한 자료형이 포인터인 것이다.
  • 포인터가 가리키는 변수의 값을 가져오는 것을 역참조(dereference)라고 한다.
  • C언어에서 포인터 변수는 메모리의 주소를 지정하는 값을 가지며, 이를 사용하여 메모리의 데이터에 액세스한다.
  • 변수의 메모리 주소는 CPU 설계 또는 운영체제에 의해 결정된다.
  • 포인터 변수는 임의의 위치를 바꿀 수 있다는 유연성을 가지지만, 아직 정의되지 않은 메모리 영역을 액세스할 수 있다는 단점도 있다.

포인터 변수의 선언

  • 자료형* 변수명; 또는 자료형 *변수명;
  • C언어의 모든 자료형에 대해 포인터 변수를 만들 수 있다.
  • C언어에서 포인터 변수를 선언하려면 변수명 앞에 *를 붙인다는 것만 제외하면, 일반 변수의 선언 방식과 동일하다.
  • 포인터 변수를 선언하고 사용하기 위해서 &(주소 연산자), *(내용 연산자)가 필요하다.
  • &는 변수가 위치한 메모리 주소 값을 구하는 연산자 이다.
  • 포인터 변수 = &포인터가 가리키는 변수명;형식으로 주로 초기화한다.
  • 즉, 일반 변수의 변수명 앞에 &를 붙이면 그 변수의 주소를 의미한다.
  • 반면에 포인터 변수는 그 자체가 주소이므로, 포인터 변수에 저장된 주소를 이용할 때는 &를 붙이지 않는다.
  • 포인터 변수에 &를 붙이면 포인터 변수가 가리키는 객체의 주소가 아닌 포인터 변수 자체가 위치한 주소를 의미한다.
  • *는 포인터 변수가 가리키는 객체의 내용을 가져오는 연산자 이다. 즉 역참조를 위한 연산자 이다.
  • *는 또한 곱셈을 하는 이항연산자이기도 하며, 포인터 변수를 선언하거나 함수의 매개변수로 포인터를 지정할 때도 사용되므로 혼동하지 않도록 주의해야 한다.
  • 한 변수의 자료형과 그 변수를 가리키는 포인터의 자료형이 일치하지 않으면 여러 문제가 발생하기 때문에 변수와 포인터의 자료형은 반드시 일치해야 한다.
  • C언어의 일반 자료형들은 그 종류에 따라 크기가 다양하지만 포인터의 크기는 자료형의 종류에 따라 다르지 않고 모두 같다.
  • 보통 단독주택 보다 아파트가 차지하는 땅이 더 넓지만 주소의 길이는 큰 차이가 없다는 것과 비슷하다.
  • 포인터의 크기는 어떻게 컴파일 하는 지에 따라 결정된다.
  • 일반적으로 32비트로 컴파일하면 포인터의 크기는 4바이트, 64비트로 컴파일하면 8바이트 이다.
#include <stdio.h>

int main()
{
  int a, b;
  a = 123;
  int *a_ptr; // 포인터 변수 a_ptr 선언, 아직 초기화 되지 않음
  a_ptr = &a; // 포인터 변수 a_ptr은 변수 a의 주소를 가짐
  // int *a_ptr = &a; 변수 선언과 동시에 초기화 가능
  
  int *ptr = (int *)0xFF000000; // 16진수 메모리 주소 값을 직접 할당할 수 있음
  
  /*
  printf함수에서 a_ptr가 가리키는 변수의 값을 출력하려면 역참조 연산자 *을 붙여준다.
  %p는 포인터 변수의 값(메모리 주소)를 16진법으로 나타낸다.
  밑에서는 %p로 포인터 변수의 주소를 참조하여 출력하려고 하는 것이기 때문에, &를 붙이지 않는다.
  */
  printf("%d %d %p\n", a, *a_ptr, a_ptr); 

  *a_ptr = 456; // 포인터 변수 a_ptr의 역참조를 통해 변수 a의 값을 456으로 변경
  printf("%d %d %p\n", a, *a_ptr, a_ptr);

  b = *a_ptr; // 변수 b의 값을 a_ptr의 역참조를 통해 저장
  printf("%d\n", b); // 456

  *a_ptr = 789;
  printf("%d %p\n", b, &b);
  printf("%d %d %p\n", a, *a_ptr, a_ptr);

  b = 12;
  printf("%d %p\n", b, &b);
  printf("%d %d %p\n", a, *a_ptr, a_ptr);
  
  a = 1004;
  printf("%d %d %p\n", a, *a_ptr, a_ptr);

  return 0;
}

포인터의 코딩 스타일

  • 포인터 변수 선언 시 필요한 *의 위치에 따라 코딩 스타일이 달라진다.
  • *의 위치에 따라 실제로 코드가 동작하는 방식도 바뀔 수 있기 때문에 주의해야 한다.
  • 주로 사용되는 방식은 다음의 세 가지가 있다.
    • 자료형 뒤에 붙이는 방식: int* ptr;
    • 변수명 앞에 붙이는 방식: int *ptr;
    • 변수명 앞에 빈칸을 넣는 방식: int * ptr
    • 빈칸을 넣는 방식은 많이 쓰이지 않는다.
  • 일관성 있게 한 가지만 사용하여 코딩하는 게 좋다.
  • int *a, b; a만 포인터이고, b는 그냥 int 변수이다.
  • int* a, b;int *a, *b;는 변수 a, b 모두 포인터로 선언한 것이다.

void 포인터와 NULL 포인터의 사용

  • 컴파일 타임에 자료형을 결정하지 못하고 런타임에 결정하는 경우가 있다.
  • 이를 위해 void형 포인터가 사용된다.
  • void* vptr; 다른 자료형의 포인터와 선언 방법은 같다.
  • void포인터는 어떤 형태의 포인터라도 모두 저장할 수 있다.
  • 하지만 void포인터에 어떤 변수의 주소를 저장하기 전에 반드시 명시적 형변환을 해줘야 한다.
  • NULL 포인터란 NULL 매크로를 활용하여 포인터를 초기화하는 것을 말한다.
  • NULL 포인터는 아무것도 가리키지 않는 상태를 의미한다. 그러므로 역참조를 할 수 없다.
  • NULL 포인터를 역참조하면 여러 문제가 발생하므로 이를 방지하는 코드가 필요하다.
  • 실무에서도 포인터가 NULL이면 메모리를 할당하거나 NULL이 아닌 경우에만 역참조하는 패턴을 자주 사용한다.
#include <stdio.h>

int main()
{
  int a = 100;
  char b = 'b';
  void* void_ptr = NULL;
  void_ptr = (int*)&a; // void 포인터에 a의 주소를 저장하기 위해 명시적 형변환 해줌
  printf("*void_ptr = %d\n", *(int*)void_ptr); // 값을 출력할 때도 형변환이 필요함

  int *safer_ptr = NULL;
  int num = 123;

  int c;
  scanf("%d", &c);

  if (c % 2 == 0) // 입력받은 c가 짝수인 경우에만 safer_ptr 변경
    safer_ptr = &num;

  if (safer_ptr != NULL) { // safer_ptr이 NULL 포인터가 아닌 경우에만 사용
    printf("%p\n", safer_ptr);
    printf("%d\n", *safer_ptr);
  }
  return 0;
}

포인터 매개변수와 참조에 의한 호출

  • 포인터는 함수의 매개변수로도 사용될 수 있다.
  • 매개변수로 일반 변수를 사용하면 값에 의한 호출(call by value)로만 사용할 수 있다.
  • 값에 의한 호출을 하면 함수의 인수로 넣은 변수 자체가 변경되지 않는다.
  • 왜냐하면 인수로 넣은 변수의 범위가 호출된 함수의 안쪽으로 한정되기 때문이다.
  • 즉, 인수의 값만 복사되어 함수 안에서 활용된다.
  • 함수의 인수로 넣은 변수 자체를 변경하려면 참조에 의한 호출(call by reference)을 해야 한다.
  • 그러나 C언어에서 모든 함수는 값에 의한 호출만 가능하다.
  • C언어에서 참조에 의한 호출과 비슷한 효과를 보려면 포인터 매개변수가 필요하다.
  • C언어에서 함수에 포인터 매개변수를 쓰면 변수의 값 자체가 아닌 변수가 위치한 주소가 함수에 입력된다.
  • 포인터 매개변수를 쓴다고 참조에 의한 호출을 하는 것은 아니며 함수 호출의 결과가 참조에 의한 호출을 하는 것과 동일할 뿐이다.
  • 따라서 C언어에서 포인터 매개변수를 쓰는 방식을 주소에 의한 호출(call by address)이라 부르기도 한다.
#include <stdio.h>

void swap_value(int a, int b) { // 값에 의한 호출을 하는 함수
  printf("swap by value\n");
  printf("argument address: ");
  printf("%p %p\n", &a, &b); // 여기서 a, b는 함수 안의 지역변수이므로, main 함수에서 인수로 넣은 변수와 다른 값이 출력된다
  int temp = a;
  a = b;
  b = temp;
}

void swap_address(int *, int *); // 함수의 원형만 선언 할 때는 매개변수명을 생략할 수 있다.

void swap_address(int *a, int *b) { // 포인터 매개변수를 받는 함수
  printf("swap by address\n");
  printf("argument address: ");
  printf("%p %p\n", &a, &b); // swap_address 함수의 지역변수 *a와 *b는 swap_address 함수의 호출 스택에서 새롭게 정의된 포인터 변수이다.
                             // 따라서 변수 a와 b의 주소(&a, &b)는 main()에서 인수로 넣은 변수의 주소와 다르다.
  printf("argument value:   ");
  printf("%p %p\n", a, b); // 여기서는 a와 b에 저장된 값을 출력하기 때문에 main()에서 인수로 넣은 변수의 주소와 동일하게 출력된다.
  int temp = *a;           // 임시 변수 temp에 a 포인터가 가리키는 변수의 값을 저장한다.
  *a = *b;                 // a 포인터가 가리키는 변수에 b 포인터가 가리키는 변수의 값을 저장한다.
  *b = temp;               // b 포인터가 가리키는 변수에 temp에 저장된 값을 저장한다.
}

int main() {
  int a = 123;
  int b = 456;
  printf("main() var adress\n");
  printf("%p %p\n", &a, &b); // swap_value와 swap_address에서 출력되는 값과 비교해볼 것
  printf("\n");

  swap_value(a, b);
  printf("%d %d\n", a, b); // a와 b의 값이 바뀌지 않는다.
  printf("\n");

  swap_address(&a, &b);    // 포인터 매개변수이므로 주소연산자 &를 변수명 앞에 붙여서 호출해야 한다.
  printf("%d %d\n", a, b); // a와 b의 값이 서로 바뀐다.
  
  return 0;
}
  • C언어에선 참조에 의한 호출을 할 수 없지만, C++에선 참조형(reference, &)을 지원하며, 함수를 정의할 때 매개변수에 &를 사용하면 참조에 의한 호출을 할 수 있다.
#include <iostream>

void swap_reference(int &a, int &b) {
  int temp = a;
  a = b;
  b = temp;
}

int main() {
  int x = 123;
  int y = 456;
  int &xref = x; // int x에 대한 참조형 선언
  int &yref = y; // int y에 대한 참조형 선언
  
  std::cout << "Before swap: x = " << x << ", y = " << y << "\n";
  swap_reference(xref, yref); // swap_reference(x, y); 도 가능함
  std::cout << "After swap: x = " << x << ", y = " << y << "\n";

  return 0;
}

배열과 포인터

  • C언어의 다른 자료형들과는 다르게 배열의 이름은 포인터같이 쓸 수 있다.
  • 배열은 특정 자료형의 집합이며 배열의 각 요소들은 메모리에 연속적으로 저장된다.
  • 배열의 이름만 쓰면 배열이 시작하는 첫 번째 요소의 주소로 암시적 변환이 된다.
  • 포인터는 곧 주소이므로, 결국 배열명을 포인터 같이 쓸 수 있게 된다.
  • 배열과 포인터가 서로 호환적이긴 하지만, 완전히 같은 것은 아니다.
  • 포인터 변수는 가리키는 주소를 변경할 수 있지만, 배열은 불가능하다.
  • 배열은 메모리 레이아웃의 데이터 영역을 고정적으로 확보하는 데 비해 포인터는 유동적으로 확보한다.
  • 즉, 포인터는 필요할 때만 기억공간을 확보하고 필요가 없으면 확보하지 않는다.
  • 따라서 자료의 개수가 가변적인 경우에는 배열보단 포인터를 사용하는 것이 효율적이다.
  • 이는 메모리 동적할당(malloc()등)과 관련이 있다.
  • char *cp = "COMPUTER"; 문자열에 대한 포인터는 이런식으로도 만들 수 있다.
  • 문자열은 곧 문자 배열이므로 이러한 할당이 가능하다.
  • 배열의 특정 위치에 대한 포인터를 만드려면 배열명[인덱스]에 주소 연산자 &를 붙여야 한다.
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;      // 포인터 p는 배열 arr의 첫번째 요소를 가리킴
int* p1 = &arr[0]; // 포인터 p1는 배열 arr의 첫번째 요소를 가리킴
int* p2 = &arr[1]; // 포인터 p2는 배열 arr의 두번째 요소를 가리킴
  • 다차원배열에 대한 포인터는 배열명으로 선언하거나 다차원배열의 첫번째 요소를 참조하는 방식으로 선언할 수 있다.
  • 다차원배열도 물리적으로는 1차원적으로 기억공간에 저장되기 때문이다.
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int* parr1 = arr;
int* parr2 = arr[0]; // 2차원 배열의 경우 arr[0]은 arr과 비슷하게 &없이 사용 가능
int* parr3 = &arr[0][0];
printf("%d %d %d\n", *parr1, *parr2, *parr3); // 1 1 1

포인터의 연산

  • 포인터의 연산은 C언어의 숫자 자료형(int, float, double 등)과는 다른 방식이 적용된다.
  • 포인터의 실제 값(주소)은 포인터가 가리키는 자료형의 크기 단위로 증가 또는 감소할 수 있다.
  • 예를 들면 int* p라는 int 자료형의 포인터 p가 있다.
  • int의 크기는 일반적으로 4바이트 이므로 p + 1은 포인터 p가 가리키는 주소에서 4를 더한 주소를 의미한다.
  • 따라서 포인터 변수에 1을 증감하는 것은 포인터가 가리키는 번지를 중심으로 한 단위 이전 또는 이후의 주소를 가리키는 것이다.
  • 이러한 포인터의 특성을 이용하여 배열의 각 요소를 접근하기 위해 포인터를 활용할 수 있다.
  • 포인터 변수에는 + 또는 -부호를 붙일 수 없다.
  • 포인터의 연산은 덧셈과 뺄셈만 가능하다.
  • 포인터와 포인터 간의 덧셈과 뺄셈은 불가능하지만, 두 포인터가 같은 배열을 가리키는 경우에는 뺄셈이 가능하다.
  • 이 때 뺄셈의 결과는 두 포인터 사이의 거리를 나타낸다.
  • void포인터는 산술 연산을 할 수 없다.
  • 역참조 연산자 *와 함께 포인터 연산을 할 때는 괄호에 유의해야 한다.
  • *(p + 2)는 포인터 p의 주소로부터 2단위 뒤에 있는 주소의 내용을 의미하는 것이다.
  • *p + 2는 p번지의 내용에 2를 더한 값을 의미하는 것이다.
#include <stdio.h>

int main() {
  int arr[10];

  int num = sizeof(arr) / sizeof(arr[0]); // num은 배열 arr의 크기를 의미하므로 10이다
  for (int i = 0; i < num; ++i) // 배열 arr 초기화
    arr[i] = (i + 1) * 100; // {100, 200, 300, ... , 800, 900, 1000}

  int *ptr = arr;
  printf("%p %p %p\n", ptr, arr, &arr[0]);

  ptr += 2;
  printf("%p %p %p\n", ptr, arr + 2, &arr[2]);

  // arr += 2; // error

  printf("%d %d %d\n", *(ptr + 1), *(arr + 3), arr[3]); // 모두 400(arr의 네번째 값) 출력

  // *연산자에 괄호가 없는 경우, 주의할 것
  printf("%d %d %d\n", *ptr + 1, *arr + 3, arr[3]); // 301(arr[2]+1), 103(arr[0]+3), 400(arr[3]) 출력

  for (int i = 0, *ptr = arr; i < num; ++i) // 여기서 ptr은 for 반복문 안의 지역변수이기 때문에 위의 ptr과 변수명이 같아도 됨
    printf("%d %d\n", *ptr++, arr[i]); // 둘다 100 200 ... 1000 출력

  for (int i = 0, *ptr = arr; i < num; ++i)
    printf("%d %d\n", *(ptr + i), arr[i]); // 둘다 100 200 ... 1000 출력
   
  for (int i = 0, *ptr = arr; i < num; ++i) 
    printf("%d %d\n", *ptr + i, arr[i]); // *ptr + i: 100 101 102 ... 109 출력, arr[i]: 100 200 ... 1000 출력

  return 0;
}
  • *연산자와 함께 증감 연산자(++ 또는 --)를 사용할 경우 연산자 우선순위에 유의해야 한다.
  • *p++:
    • *p는 포인터 p가 가리키는 주소의 값을 가져온다.
    • p++는 포인터 p를 다음 위치로 증가 시킨다.
    • 이 연산은 후위 증가 연산자로, 먼저 p위치의 값을 가져와서 사용하고, 그 다음에 p를 1 증가 시키는 것이다.
  • *++p:
    • ++p는 포인터 p를 다음 위치로 증가 시킨다.
    • *++p는 증가 시킨 위치를 가리키는 포인터 p의 값을 가져온다.
    • 이 연산은 전위 증가 연산자로, 먼저 포인터 p를 1 증가 시키고, 증가 된 p위치의 값을 가져오는 것이다.
  • ++*p:
    • *p는 포인터 p가 가리키는 주소의 값을 가져온다.
    • ++*p는 해당 주소의 값을 1 증가 시킨다.
    • 이 연산은 전위 증가 연산자로, 먼저 p를 1 증가 시키고, 증가 된 p위치의 값을 가져오는 것이다.
#include <stdio.h>

int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  int* p = arr; // 포인터 p는 배열 arr의 처음부터 시작
  
  // *p++, *++p, ++*p 비교
  printf("%d\n", *p++); // 1
  printf("%d\n", *p);   // 2
  
  printf("%d\n", *++p); // 3
  printf("%d\n", *p);   // 3
  
  printf("%d\n", ++*p); // 4
  printf("%d\n", *p);   // 4
  return 0;
}
#include <stdio.h>

int main() {
  void* vptr = NULL;
  // printf("%d", *vptr); // error
  
  int* ptr = 0;
  printf("%p %lld\n", ptr, (long long)ptr); //int인 경우 0, 0

  ptr += 1; // try -+, ++, --, -, +
  // ptr += 1; 자료형 하나의 크기 만큼을 더한다는 뜻

  printf("%p %lld\n", ptr, (long long)ptr); //int인 경우 4, 4

  // 포인터에는 부호를 붙일 수 없음
  //ptr = -ptr; 
  //ptr = +ptr; 

  // 포인터간 빼기
  int arr[10];
  int *ptr1 = &arr[3], *ptr2 = &arr[5];

  //ptr2 = ptr1 + ptr2; // 포인터끼리 더하는 것은 불가능
  int i = ptr2 - ptr1; // 같은 배열을 가리키는 포인터끼리 빼는 건 가능
  printf("%p %p %d\n", ptr1, ptr2, i); // 인덱스5와 3의 차이이므로 i는 2로 출력됨
  // 즉 포인터 간의 빼기로 배열의 요소간 인덱스의 차이를 계산할 수 있음.

  return 0;
}
#include <stdio.h>

int main() {
	int a = 0;
	
	/*
	포인터 연산
	- 포인터 대입
	- 값 찾기(역참조, dereferncing)
	- 포인터 주소 연산자
	- 포인터에 정수 더하기
	- 포인터 1씩 증감
	- 포인터에 정수 빼기
	- 포인터에 포인터 빼기
	- 포인터간 비교하기
	*/
	int arr[5] = {100, 200, 300, 400, 500};
	int *ptr1, *ptr2, *ptr3;
	int i;
	ptr1 = arr; 
	
	printf("%p\n", arr);
	
	// ptr1: 역참조(arr의 주소), *ptr1: ptr1의 주소에 있는 값, &ptr1: 포인터 변수의 주소
	printf("%p %d %p\n", ptr1, *ptr1, &ptr1); 
	ptr2 = &arr[2]; // 배열 arr의 세번째 요소 참조
	printf("%p %d %p\n", ptr2, *ptr2, &ptr2);
	ptr3 = ptr1 + 4; // 배열 arr의 마지막(다섯번째) 요소 참조
	printf("%p %d %p\n", ptr3, *ptr3, &ptr3);
	
	printf("%td\n", ptr3 - ptr1); // 포인터의 차이를 출력할때는 %td 사용
	
	ptr3 = ptr3 - 4; // ptr3가 ptr1과 같아짐
	printf("%p %d %p\n", ptr3, *ptr3, &ptr3);
	
	if (ptr1 == ptr3)
	  printf("Same\n");
	else
	  printf("Different");
	
	double d = 3.14;
	double *ptr_d = &d;
    
	//if (ptr1 == ptr_d) // warning C4133: 서로 다른 자료형을 비교했기 때문에 경고가 뜸, 경고를 없애려면 형변환 해야함
	//if ((double*)ptr1 == ptr_d)
	//if ((void*)ptr1 == (void*)ptr_d)
    
	if (ptr1 == (int*)ptr_d)
	  printf("Same\n");
	else
	  printf("Different\n");
	
	return 0;
}

포인터 배열

  • 포인터 배열이란 말 그대로 포인터로 이루어진 배열을 말한다.
  • 일반 배열처럼 동일한 속성의 포인터를 여러 개 담을 수 있는 배열이다.
  • char *names[3];일반 배열처럼 선언하고 *만 붙여주면 포인터 배열을 선언할 수 있다.
  • 포인터 배열은 2차원 배열과 비슷하게 동작하지만, 기억공간을 더 효율적으로 관리할 수 있다.
  • 예를 들어 배열char carr[4][20];은 char 자료형의 크기가 1바이트 이므로 20바이트 짜리 문자열 4 개가 있는 배열이다.
  • 따라서 배열carr은 요소의 내용과 관계없이 고정된 80바이트의 기억공간을 차지 한다.
  • 그러나 포인터 배열 char *parr[4];은 포인터 4개의 크기만큼만 차지 한다.
  • 따라서 배열과 포인터 배열의 내용이 서로 같아도 두 배열의 크기는 다를 수 있다.
  • int (*ptr)[3]int *ptr[3]는 서로 다른 종류의 포인터 변수이다. 괄호에 의해 의미가 완전히 달라진다.
    • int (*ptr)[3]: 1차원 배열을 가르키는 포인터이고 각 요소는 크기가 3인 정수형 배열을 가리키는 것이다. 이런 표현은 주로 다차원 배열에 대한 포인터를 만들 때 자주 쓰인다. int arr[2][3]; int (*ptr)[3] = arr;
    • int *ptr[3]: int형을 가리키는 포인터들의 배열을 의미한다.
#include <stdio.h>

int main() {
  char carr[4][20] = {"ASDF", "QWERTYUIOP", "ABC", "FOOBAR"};
  char *parr[4];
  parr[0] = carr[0];
  parr[1] = carr[1];
  parr[2] = carr[2];
  parr[3] = carr[3];
  printf("size of carr: %d\n", (int)sizeof(carr)); // 80
  printf("size of parr: %d\n", (int)sizeof(parr)); // 32 (64비트 컴파일)

  return 0;
}
#include <stdio.h>

int main() {
  // 1차원 배열 두 개 -> 2차원 배열 포인터 parr
  int arr0[3] = {1, 2, 3};
  int arr1[3] = {4, 5, 6};

  // int *parr[2] = {arr0, arr1}; // 포인터 배열로 2차원 배열 생성

  // for (int j = 0; j < 2; ++j) {
  //   for (int i = 0; i < 3; ++i)
  //     printf("%d(==%d, %d) ", parr[j][i], *(parr[j] + i), (*(parr + j))[i]); // 셋 다 같은 값 출력
  //   printf("\n");
  // }
  // printf("\n");

  // 2차원 배열은 1차원 배열들의 배열이다.
  int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};

  int *parr0 = arr[0];
  int *parr1 = arr[1];
  for (int i = 0; i < 3; ++i)
    printf("%d ", parr0[i]); // 1 2 3
  printf("\n");

  for (int i = 0; i < 3; ++i)
    printf("%d ", parr1[i]); // 4 5 6
  printf("\n");

  int *parr[2];
  parr[0] = arr[0];
  parr[1] = arr[1];

  printf("\n");
  for (int j = 0; j < 2; ++j) {
    for (int i = 0; i < 3; ++i)
      printf("%d %d %d %d\n", arr[j][i], parr[j][i], *(parr[j] + i), *(*(parr + j) + i)); // 모두 같은 값 출력
    printf("\n");
  }

  printf("%p\n", &parr[0]); // &parr[0]: 포인터 자체의 주소

  // 모두 같은 값 출력
  printf("%p\n", parr[0]);
  printf("%p\n", arr);
  printf("%p\n", &arr);
  printf("%p\n", &arr[0]); // 배열병의 주소는 배열의 첫번째 요소의 주소와 같음
  printf("%p\n", arr[0]);
  printf("%p\n", &arr[0][0]);

  printf("\n");

  // Array of string of diverse lengths example
  printf("%zd\n", sizeof(char *));
  char *name[] = {"Aladdin", "Jasmine", "Magic Carpet", "Genie"}; // name이라는 문자 배열의 포인터
  const int n = sizeof(name) / sizeof(char*);
  for (int i = 0; i < n; ++i)
    printf("%s at %u\n", name[i], (unsigned)name[i]); // %u: 부호없는 10진수
  printf("\n");

  char aname[][15] = {"Aladdin", "Jasmine", "Magic Carpet", "Genie"}; //  각 요소가 15개의 문자를 저장하는 문자열 배열로 이루어진 2차원 배열이라는 의미
  const int an = sizeof(aname) / sizeof(char[15]);

  for (int i = 0; i < an; ++i)
    printf("%s at %u\n", aname[i], (unsigned)&aname[i]); // 15씩 출력
  printf("\n");

  return 0;
}
#include <stdio.h>

int main() {
  //float arr2d[2][4] = {{1.0f, 2.0f, 3.0f, 4.0f}, {5.0f, 6.0f, 7.0f, 8.0f}};

  //printf("%u\n", (unsigned)arr2d); // Use unsigned long long in x64
  //printf("%u\n", (unsigned)arr2d[0]);
  //printf("\n");

  //// arr2d points to arr2d[0](arr2d[0][0]의 주소), not arr2d[0][0](배열의 첫번째 값), KNK Ch. 12.4
  //printf("%u\n", (unsigned)*arr2d);
  //printf("%u\n", (unsigned)&arr2d[0]); // C language allows this
  //printf("%u\n", (unsigned)&arr2d[0][0]);
  //printf("%f %f\n", arr2d[0][0], **arr2d); // 값 출력
  //printf("\n");

  //printf("%u\n", (unsigned)(arr2d + 1)); // 4개의 float 만큼을 건너뜀(16, x86)
  //printf("%u\n", (unsigned)(&arr2d[1]));
  //printf("%u\n", (unsigned)(arr2d[1]));
  //printf("%u\n", (unsigned)(*(arr2d + 1)));
  //printf("%u\n", (unsigned)(&arr2d[0] + 1));
  //printf("%u\n", (unsigned)(&arr2d[1][0]));
  //printf("\n");

  //printf("%f\n", *(*(arr2d + 1) + 2)); //arr2d[1][2]와 같음
  //printf("\n");

  //for (int j = 0; j < 2; ++j) {
  //  printf("[%d] = %u, %u\n", j, (unsigned)(arr2d[j]), (unsigned)*(arr2d + j)); // 같음

  //  for (int i = 0; i < 4; ++i) {
  //    printf("[%d][%d] = %f, %f\n", j, i, arr2d[j][i], *(*(arr2d + j) + i));

  //    *(*(arr2d + j) + i) += 1.0f; // arr2d[j][i] += 1.0f;
  //  }
  //}
  //printf("\n");

  /* Pointers to Multidimensional Arrays */
  // 다차원 배열에 대한 포인터 문법
  float arr2d[2][4] = {{1.0f, 2.0f, 3.0f, 4.0f}, {5.0f, 6.0f, 7.0f, 8.0f}};

  float (*pa)[4]; // a SINGLE pointer to an array of 4 floats, 4개의 float를 가진 배열에 대한 하나의 포인터
  float* ap[2];  // an array of TWO pointers-to-float, 두 개의 포인터를 원소로 가지는 배열, [2]의 우선순위가 *보다 높기 때문
   
  printf("%zu\n", sizeof(pa)); // 4 in x86, 8 in x64
  printf("%zu\n", sizeof(ap)); // 8 in x86, 16 in x64
  printf("\n");

  pa = arr2d; // 이중포인터같이 쓸 수 있음
  // pa[0] = arr2d[0]; // error
  // pa[1] = arr2d[1]; // error

  // ap = arr2d;		// error
  ap[0] = arr2d[0]; // arr2d[0], arr2d[1]은 값이 아니라 주소이므로 포인터로 설정 가능.
  ap[1] = arr2d[1];

  printf("%u %u\n", (unsigned)pa, (unsigned)(pa + 1)); // pa + 1은 다차원 배열에서 1행 넘어간 것으로, arr2d[1]과 같다.
  printf("%u %u\n", (unsigned)arr2d[0], (unsigned)arr2d[1]);
  printf("%u %u\n", (unsigned)pa[0], (unsigned)(pa[0] + 1));
  printf("%f\n", pa[0][0]);
  printf("%f\n", *pa[0]);
  printf("%f\n", **pa);
  printf("%f\n", pa[1][3]);
  printf("%f\n", *(*(pa + 1) + 3));
  printf("\n");

  printf("%u %u\n", (unsigned)ap, (unsigned)(ap + 1));
  printf("%u %u\n", (unsigned)arr2d[0], (unsigned)arr2d[1]);
  printf("%u %u\n", (unsigned)ap[0], (unsigned)(ap[0] + 1));
  printf("%f\n", ap[0][0]);
  printf("%f\n", *ap[0]);
  printf("%f\n", **ap);
  printf("%f\n", ap[1][3]);
  printf("%f\n", *(*(ap + 1) + 3));

  return 0;
}

포인터에 대한 포인터(이중 포인터)

  • 포인터 변수를 가리키는 포인터를 만들 수 있다.
  • 이를 이중 포인터라고 한다.
  • 즉 이중 포인터는 자신이 가리키는 포인터 변수의 주소 값을 저장한다.
  • 이중 포인터를 가리키는 삼중 포인터도 만들 수 있고 그 이상의 다중 포인터도 만들 수 있다.
  • *를 연달아 써서 선언한다.
  • 이중 포인터가 가리키는 포인터가 가리키는 값을 참조하려면 **를 써야 한다.
#include <stdio.h>

int main() {
  char a = 'A';
  char *p = &a;
  char **pp = &p;  
  
  // *pp는 pp가 가리키는 포인터인 p가 가리키는 변수 a의 주소를 의미한다.
  printf("%p\n%p\n%p\n", &a, p, *pp); // 모두 같음
  printf("\n");
  printf("%p\n%p\n", &p, pp); // 모두 같음
  printf("\n");
  printf("%c %c %c\n", a, *p, **pp); // A A A

  return 0;
}

함수 포인터

  • 함수 포인터는 함수를 가리키는 포인터 이다.
  • 함수 포인터를 선언할 때는 함수를 정의할 때와 마찬가지로 매개변수와 반환 값의 자료형을 명시해야 한다.
  • 함수 포인터 선언 시 배열명과 비슷하게 함수명 앞에 주소 연산자 &를 붙이지 않아도 된다.
  • 함수의 이름 자체가 포인터로 변환되기 때문이다.
  • 프로그래머는 함수의 이름을 이용하여 프로그래밍 하지만, 컴파일러에선 함수명을 메모리 주소로 인식한다.
  • 즉, 함수를 실행하는 것은 함수의 주소에 있는 명령어들을 순차적으로 실행하는 것과 같다.
#include <stdio.h>

void f1() {
  printf("It is f1().\n");
  return;
}
int f2(char i) {
  return i + 1;
}
int main()
{
  void (*pf1)() = f1;
  int (*pf2)(char) = f2;

  // (*pf1)(); pf1(); 둘 다 호출 가능
  (*pf1)(); // pf1을 통해 f1() 호출
  pf1(); 

  int a = pf2('A'); //  (*pf2)('A'); 도 가능
  printf("%d\n", a); 
  return 0;
}
#include <ctype.h> // toupper(), tolower()
#include <stdio.h>

void ToUpper(char *str)
{
  while (*str) {
    *str = toupper(*str);
    str++;
  }
}

void ToLower(char *str)
{
  while (*str) {
    *str = tolower(*str);
    str++;
  }
}

// void UpdateString(char *str, int (__cdecl *pf)(int)); // __cdecl 넣어서 선언하는 방법
void UpdateString(char *str, int (*pf)(int))
{
  while (*str) {
    *str = (*pf)(*str); // *pf가 가리키는 함수(ToUpper 또는 ToLower) 호출
    str++;
  }
}

int main()
{
  char str[] = "Hello, World!";
  void (*pf)(char *);
  pf = ToUpper;
  // pf = &ToUpper; // OK
  // pf = ToUpper(str); // not acceptible in C

  printf("String literal %lld\n", (long long)("Hello, World!"));
  printf("Function pointer %lld\n", (long long)ToUpper);
  printf("Variable %lld\n", (long long)str);

  (*pf)(str);
  // pf(str); // 이러한 호출 방법은 K&R에선 X, ANSI C에선 OK
  printf("ToUpper %s\n", str);
  pf = ToLower;
  pf(str);

  printf("ToLower %s\n", str);

  /*
    passing function pointers to functions
    함수의 포인터를 함수 호출 인수로 넣어서 사용하는 방법
  */

  UpdateString(str, toupper);
  printf("ToUpper %s\n", str);

  UpdateString(str, tolower);
  printf("ToLower %s\n", str);

  // Note: __cdecl is function calling convention, 함수 호출 규약
  return 0;
}

구조체, 공용체와 포인터

  • C언어의 구조체는 배열과는 다르게 여러 가지의 자료형을 조합해서 사용자 정의하여 만들 수 있는 자료형이다.
  • 구조체 안에 속한 변수를 멤버 변수라고 한다.
  • 공용체는 구조체와 비슷하게 여러 자료형의 멤버 변수를 정의하여 사용할 수 있는 자료형이다.
  • 구조체의 모든 멤버 변수들은 독립적이고, 구조체의 크기는 멤버 변수들의 자료형과 패딩에 따라 결정된다.
  • 반면에 공용체의 모든 멤버 변수들은 같은 메모리 공간을 공유한다.
  • 따라서 공용체의 멤버 변수 중 가장 큰 것의 크기가 곧 공용체의 크기가 된다.
  • 구조체는 동시에 여러 개의 멤버 변수에 접근할 수 있지만, 공용체는 한 번에 하나의 멤버 변수에만 접근할 수 있다.
  • 구조체는 struct, 공용체는 union 키워드로 선언한다.
  • 구조체 또는 공용체에 대한 대한 포인터를 만들 수 있다.
  • 구조체와 공용체에 대한 포인터 선언과 초기화는 일반적인 포인터와 동일하게 할 수 있다.
  • 구조체와 공용체의 멤버 변수에 접근하려면 연산자 .를 사용해야 한다.
  • 구조체와 공용체의 포인터로 멤버 변수에 접근하려면 .이 아닌 ->를 사용해야 한다.
  • .*보다 연산자 우선순위가 높기 때문에 그런 것이며, 포인터 변수에 괄호를 사용하면 .를 쓸 수 있다. (*sptr).member
  • 구조체와 공용체에 대해 typedef 키워드로 별칭을 만들어서 사용하면 편리하다.
  • typedef 선언과 구조체 또는 공용체를 동시에 정의하면 이름을 생략할 수 있다.
#include <stdio.h>

struct MyStruct {
  int number;
};

typedef struct {
  char name[20];
  int age;
} Student;

typedef union {
    int intValue;
    float floatValue;
} MyUnion;

int main() {
  struct MyStruct s;
  s.number = 123;
  
  Student s1 = {"John", 20};
  Student *sptr = &s1;
  printf("%s %s\n", s1.name, sptr->name); // John John
  printf("%d %d\n", s1.age, sptr->age);   // 20 20
  
  MyUnion u1;
  u1.intValue = 123;
  MyUnion *uptr = &u1;
  
  printf("%d %d\n", u1.intValue, uptr->intValue); // 123 123
  
  u1.floatValue = 3.14f;
  printf("%f %f\n", u1.floatValue, uptr->floatValue); // 3.140000 3.140000

  /* 공용체는 한 번에 하나의 멤버 변수에만 접근할 수 있다. 
  위에서 멤버 변수 intValue를 초기화하고 출력하는 것 까지는 문제 없지만
  또다른 멤버 변수 floatValue를 초기화하면 공용체의 멤버 변수들이 공유하는 메모리를 덮어씌우게 되므로 
  intValue에 저장된 값을 올바르게 해석할 수 없다. */
  printf("%d %d\n", u1.intValue, uptr->intValue); // 따라서 위에서 초기화한 값(123)이 아닌 다른 값이 출력된다.
  return 0;
}
post-custom-banner

0개의 댓글