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. 연산자 우선순위
단항 > 이항(산술 > 관계 > 논리) > 삼항(조건) > 대입 > 콤마
'C' 카테고리의 다른 글
C / 포인터와 함수 (2) | 2025.04.20 |
---|---|
C / 배열과 포인터 (5) | 2025.04.16 |
C / 함수의 인자 평가 순서 (0) | 2025.04.09 |
C / 포인터를 향하여 - 프로그램 메모리 구조 - Ch.3 (0) | 2025.04.07 |
C / 포인터를 향하여 - 포인터 값 초기화 및 값 수정 - Ch.2 (0) | 2025.04.04 |