C

C / 포인터를 향하여 - Ch.1

발아현미 2025. 4. 2. 16:22

 C++을 중점적으로 다루고 싶지만, C++은 C언어 기반, 객체지향 프로그래밍 언어로서 만들어졌기 때문에 C에서의 개념이 다수 사용된다. 그 중 포인터의 개념은 정말 중요해서 이 부분을 소홀히 하면 C++에서도 흠이 생길 수 밖에 없다. 따라서 C언어 중 포인터의 개념과 심화과정에 대해 다뤄 보려 한다.

- 포인터 변수를 어떻게 바라보아야 할까?

 포인터 변수를 바라볼 때는 2가지만 기억하면 편하다. 그 변수가 담고 있는 정보의 주소, 정보의 type이다.

1. 포인터 변수는 초기화된 정보의 주소를 기억하고 있다.
2. 포인터 변수는 초기화된 정보의 type를 기억하고 있다.

 아래 코드를 살펴보며 자세히 분석해보자.

#include <stdio.h>

int main(){
	int a[]={10, 11, 12};
	int *p;
    	p = a;
    
   	return 0;
}

 a는 각 원소의 변수타입이 int인, 길이 3짜리 배열의 식별자이다. p는 int pointer타입의 변수이다. p에 a를 대입했으므로, p는 a의 배열의 주소를 저장함과 동시에, a는 각 원소의 변수 타입이 int인 배열인 것도 저장된다.

 - 배열 식별자에 대해 알아야 할 3가지 규칙

 보통 포인터는 배열과 연관이 많다. 포인터를 사용하여 배열에 접근할 때 특정 배열의 식별자를 통해 접근하곤 하는데, 프로그래머는 코드 내에서 배열자가 어떻게 행동하는지 잘 알아야 할 필요가 있다. 

 

 배열 식별자가 연산에 사용되었을 때 3가지 의미로 사용될 수 있다.

1. 다음 두 경우를 제외하고는 '첫 번째 원소의 포인터'로 사용된다.
2. sizeof 관련 연산과 사용되었을 때 - 배열이 할당하고 있는 메모리 반환
3. &연산과 사용되었을 때 - 배열 자체의 포인터

 위 규칙들을 파악해 보기 위해 아래 코드를 살펴보자

#include <stdio.h>

int main() {
	char c[] = "JANE";

	printf("%p\n", c); // F754 출력
	printf("%d\n", sizeof(c)); // 5 출력
	printf("%p\n", &c); //F754 출력
	printf("%p\n", c+1); // F755 출력
	printf("%p\n", &c+1); // F759 출력

	return 0;
}
    • printf("%p\n", c)
      배열의 식별자가 sizeof, &연산과 사용되지 않았기 때문에 '첫 번째 원소의 포인터'로 사용되었다. 따라서 c배열의 주소인 F754가 출력된다.
    • printf("%d\n", sizeof(c))
      배열의 식별자가 sizeof연산과 사용되었기 때문에, c배열의 크기(바이트 수)를 출력한다. c배열은 {"J", "A", "N", "E". "\0"} 총 5바이트로 이루어져 있으므로(char은 1바이트) 5가 출력된다.
    • printf("%p\n", &c)
      여기서 바로잡아야 할 것이, printf("%p\n", c)와 printf("%p\n", &c)이다. 
      전자는 배열의 식별자가 '첫 번째 원소의 포인터' 이지만, 후자는 '배열 자체의 주소'를 반환한다. 결과적으로 두 명령어의 출력은 같겠지만, 차이점은 바로 뒤에서 나온다.
    • printf("%p\n", c+1)
      이때 배열의 식별자 c는 sizeof, &연산과 사용되지 않았기 때문에 첫 번째 원소의 포인터로 사용되었다. 따라서
      c == F754가 되겠다. 이때 1을 더하므로 출력값은 F755가 된다.
    • printf("%p\n", &c+1)
      c는 &연산과 사용되었기 때문에 c는 배열 자체의 주소를 반환한다. 명목적으로는 &c == F754겠지만 &c+1의 값은 F755가 아닌 F759이다. 사진과 함께 이해하면 쉽다.

  • c는 그저 '배열의 첫번째 원소'의 주소를 나타내므로 c+1 == F755지만, &c는 '배열 그 자체의 주소'를 나타내므로 J부터 시작해서 \0으로 끝나는 배열 전체를 묶어서 생각해야 한다. 따라서 &c+1 ==F759가 된다.

- 문자열 포인터와 ++ 연산자의 결합

 앞 단락에서 배열의 식별자가 어떤 연산자와 결합되느냐에 따라 표의하는 것이 다르다는 것을 알게 되었다. 그렇다면 ++연산자와 결합된다면 어떻게 될지 살펴보자.

#include <stdio.h>

int main()
{
    int a[] = { 1, 2, 3 };
    int* p = a;
    printf("%d\n", *p++);
    printf("%d\n", (*p)++);
    printf("%d\n", *p);
    printf("%p\n", p);
    printf("%p\n", a);

     return 0;
}

 

 눈으로만 해석해 보았을 때 쉽게 감이 잡히지 않을 것이다. 뇌가 복잡해질 당신을 위해, 알면 편한 규칙 몇개를 먼저 소개해 주겠다.

증감 연산자의 성격 
 - 조건: 증감 연산자는 수정 가능한 공간에 한해 증감연산을 시행한다. [ex) p++가능, 4++불가능]
 - 기능: 피연산자를 증가시키는 연산자이다.
 - 결과: p++ == operand(공간)의 값 / ++p == 증가된 operand(공간)의 값

 

 또한 위 코드에서는 여러 가지 연산자들이 얽혀 있다. 어느 연산부터 해야 할 지 헷갈리기 때문에, 연산자 우선순위도 첨부해 둔다. 

증감 접근 산술 관계 논리 대입 기타
++a *a +a ~a a > b !a a = b a &= b f(a)
--a &a -a a & b a < b a&&b a += b a |= b (int) a
a++ a[b] a + b a | b a >= b a || b a -= b a ^= b a ? b : c
a-- a.b a - b a ^ b a <= b   a *= b a <<= b sizeof(a)
  a->b a * b a << b a == b   a /= b a >>= b a, b
    a / b a >> b a != b   a %= b    
    a % b            

 대략적으로 순서를 정하자면 아래와 같겠다.

연산자 우선순위(간략화)
단항 > 이항(산술 > 관계 > 논리) > 삼항(조건) > 대입 > 콤마

 

다시 코드로 돌아가서, 하나하나 분석해보자.
 a는 각 원소가 int type인, 그리고 원소의 개수가 3인 정수 배열의 식별자이다.

 p는 int pointer type의 변수이고, a의 주소 & a의 type를 저장하고 있다.

  • printf("%d\n", *p++);
     여기서 *p++의 연산자는 누가 먼저일까? 증감연산자인 ++일 것이다. 따라서 *(p++) 라고 표기해 두는 것이 더 와닿을 것이다. p와 *연산자가 만나야지 a배열의 원소에 접근할 수 있는 것이고, *연산과 만나기 이전에 ++연산과 만났으므로, 출력되는 것은 (a배열의 첫번째 원소의 주소)++인 것이다. 이 때 p는 a가 int type인 것을 저장했기 때문에 ++연산의 결과가 &a[0]+4 == &a[1]일 것이다.
    [결론] : a[0]의 값인 1 출력, p == &a[1]으로 변화
  • printf("%d\n", (*p)++);
     바로 위 코드와는 다르게, 여기선 *연산을 먼저 시행하고 이후에 증감연산을 시행한다. *p이므로 a[1]의 값이 대입되고, 그 이후 증감 연산이 시행된다. %d에는 a[1]의 값 2가 출력되는건 알겠는데....
    증감값이 어떻게 변하는거지?   a[1]++인가?      p++인가?
    p와 *연산이 만난다는 것은, 포인터에 저장된 값을 참조하여 다룬다는 의미이다. 따라서, (*p)++ == a[1]++이 되겠다. 따라서 a[1]의 값은 3으로 변경된다.
    [결론] : a[1]의 값인 2 출력, a[1] == 3으로 변경
  • printf("%d\n", *p);
     현재까지의 코드로, p는 a[1]의 주소값을 담고 있다. 따라서 3을 출력할 것이다.
    [결론] : 수정된 a[1]의 값, 3 출력
  • printf("%p\n", p);
     대충 보고 'p의 주소 출력~' 하면 안된다. p는 포인터 변수로, 어떤 대상의 주소와 변수 type을 저장한다. 따라서 위 코드는, p에 저장된 어떤 공간의 주소를 출력하는 코드이다.
    [결론] : p의 주소 61FE14 출력
  • printf("%p\n", a);
     a는 배열의 식별자다. &연산, sizeof연산과 사용되지 않으면 "첫 번째 원소의 포인터"로 사용된다. 따라서 a[0]의 주소 61FE10이 출력된다.
    [결론] : a[0]의 주소 61FE10 출력

 

정리

1. 포인터가 갖고 있는 정보

1. 포인터 변수는 초기화된 정보의 주소를 기억하고 있다.
2. 포인터 변수는 초기화된 정보의 type를 기억하고 있다.

 

2. 배열 식별자가 연산에 쓰였을 때

1. 다음 두 경우를 제외하고는 '첫 번째 원소의 포인터'로 사용된다.
2. sizeof 관련 연산과 사용되었을 때 - 배열이 할당하고 있는 메모리 반환
3. &연산과 사용되었을 때 - 배열 자체의 포인터

 

3. 증감 연산자의 성격

1. 조건: 증감 연산자는 수정 가능한 공간에 한해 증감연산을 시행한다.
2. 기능: 피연산자를 증가시키는 연산자이다.
3. 결과: p++ == operand(공간)의 값 / ++p == 증가된 operand(공간)의 값

 

4. 연산자 우선순위

단항 > 이항(산술 > 관계 > 논리) > 삼항(조건) > 대입 > 콤마