최초 글 작성 4월 12일 오후 6시 09분 글이랑 같이 진도뺀다.
오후 10시 25분 수정 시작, 11시 size_t 추가로 마무리
포인터의 연산은 C언어에서 중요한 개념 중 하나이다.
- 포인터는 단순한 주소 저장용 변수이지만, 연산을 통해 배열처럼 메모리에 연속으로 배치시키거나, 특정 위치를 가리키게 만들 수 있다.
기본적인 포인터 연산
C언어에서 포인터 연산은 정수값을 더하거나 빼는 연산, 또는 두 포인터 간의 차이를 계산하는 연산이 대표적이다.
즉, 연산이 가능한 형태는 이렇게 생각 할 수 있다.
연산식 | 쓸 수 있나요? | 왜? |
포인터 + 정수 | Yes | 포인터가 가리키는 배열의 정수만큼의 상대 위치로 이동한다. |
포인터 + 포인터 | NO | 주소 + 주소는 의미 있는 내용을 나타내지 못한다. 그래서 언어상에서도 금지된다. |
포인터 - 포인터 | Yes | 같은 배열 내에서, 위치 차이를 계산하는데 도움이 된다. |
포인터 - 정수 | Yes | 포인터를 정수 값 만큼 앞으로 이동시킨다. |
포인터에 정수를 더하고 빼는 것은 포인터에 직접적인 작업을 하는 것이니 크게 염려되진 않는다. 하지만,
포인터 - 포인터는 결과 값이 int로 반환된다고 한다.
엥? 왜?
포인터 간의 뺄셈은 정확히 ptrdiff_t 라는 타입으로 결과가 나온다. 포인터 사이의 거리를 저장하기 위해 사용하는 데이터 타입이다.
ptr diff t = Pointer Difference Type
typedef signed integer type ptrdiff_t;
내부적으론 long int, long long, int 등으로 되어있는데, 이는 주소 공간의 크기에 영향을 받는다.
보다 간단하게 생각하면, 운영체제의 32/64비트에 영향을 받게되고, 그로 인해 크기가 지정이 된다.
ptrdiff_t의 크기를 결정하는 과정은 대표적으로 두 가지이다.
- 플랫폼의 데이터 모델
- 간단한 대표들만 보면,
- 32비트 리눅스에서의 ptrdiff_t는 int 또는 long 이다.
- 64비트 운영체제 중, Windows가 아닌 대부분의 운영체제는 long이다.
- 64비트 Windows는 long long 이다.
- 이 중에서, pointer 크기를 충분히 표현 할 수 있는 가장 작은 부호 있는 정수형이다.
- 그리고 이 차이가 한 운영체제에서 컴파일된 C 프로그램이 다른 운영체제에서 동작 할 수 없는 대표적 이유 중 하나이다. 언젠가 다룰 이식성(portability) 문제의 핵심이 이것이다.
- 같은 long이어도 Linux, Windows에서 차지하는 크기가 달라진다. 여기서 더 생각해보면, long을 포함하는 구조체는 그럼 운영체제에 따라 크기도 달라진다는 것이니, 그럴 일은 없겠지만 진짜 실행이 된다고 한들 얼마 못가고 꺼질 것이다.
- 간단한 대표들만 보면,
- 컴파일러와 C언어 표준 라이브러리
- 심지어 헤더에 정의 된 내용을 보면, 실행을 목표로 하는 장치에 따라서도 데이터 타입이 다르다.
// GCC on 64-bit Linux (LP64)
typedef long ptrdiff_t;
// MSVC on 64-bit Windows (LLP64)
typedef __int64 ptrdiff_t;
나는 이 코드를 보고 이런 생각을 했다. 그럼 그냥 둘 중 하나를 다른 한 쪽에 맞추면 되지 않나?
안된대..
- 더 깊은 이야기로 논해보면, 이 프로그램에서 시작될 운영체제와 컴파일러 사이의 소통에서, 자체의 ABI 규칙을 갖는다.
- 이 ABI 라는 것은 함수 호출 방식, 구조체 정렬 및 패딩, 레지스터 사용 규약, 타입의 크기와 정렬 규칙따위를 논한다.
- 근데 우리가 논하고 있는 ptrdiff_t도 이 내용이다. 이거 하나를 살짝 바꾼다고 해결되진 않는다는 것이다.
- [야매 비유] 영어권 사람들이 한국어 배우는 것보단 일본 사람들이 한국어 배우는게 비교적 쉽다. 이게 아시아권이라서도 있겠지만 기본적으로 접하는 모국어의 문장 구조가 배울 언어에 비해 얼마나 비슷하냐에 달려있는 것인데, 우리가 기저에 깔려있는 단어 하나 바꾼다고 그게 다른 언어의 사람한테도 OK냐고 하면 글쎄.. 분명 아니지. 문제가 생길 것이다.
- 크로스 플랫폼이 이 ABI로 인해.. 무지막지한 퀘스트인 것이다.
증감 연산자
- p++ 은 p = p + 1와 동일한 의미를 가진다. p가 가리키는 데이터 타입의 size 만큼 증가하거나 감소하게 된다.
정수를 더하고 빼는 행위를 축약한 코드인 증감 연산자도 당연히 사용 할 수 있다.
int *p = arr;
printf("%d\n", *p++); // 10 출력, 이후 p는 arr[1]을 가리킴
printf("%d\n", *++p); // 30 출력, 이후 p는 arr[2]를 가리킴
배열과 포인터의 관계
- 배열 이름은 그 자체로 해당 배열의 첫 번째 요소의 주소를 의미하는 포인터이다. arr[i]는 *(arr + i)와 동일하다.
int arr[5] = {1, 2, 3, 4, 5};
printf("%d\n", *(arr + 2)); // 3
주의 할 점
- 포인터 범위가 초과되는 경우
- 포인터 연산은 실제 사용하는 메모리 범위 내에서 이루어져야한다. 배열의 끝을 넘어가는 포인터 접근은 정의되지 않은 동작이다.
- 다른 타입과의 연산
- void * 타입은 크기가 정의되지 않아서 포인터 연산이 직접적으로 안된다.
- 아니 뭐 빈 걸 가리키고 있나? 싶지만..
- 뭐든 가리킬 수 있지만, 가리키는 것의 실루엣 조차 모르는 상황인 것이다. 여기다가 뭐 해라라는게 말이 안되는 것이다.
- 보통 캐스팅 후 연산한다. 예: (char *)p + 1
- 아니 뭐 빈 걸 가리키고 있나? 싶지만..
구조체 포인터 연산
- 직접 C로 자료구조를 구현하는 상황에서, 구조체에 대한 포인터를 자주 접하게 될 것이다.
- 구조체는 데이터를 묶는다. 실제로 연속된 데이터로써 정의 된다. int first, int second를 정의하면 구조체의 첫 위치에다 +4 하면 second를 가리키게 할 수 있다.
typedef struct {
int first;
int second;
} Pair;
Pair *p;
((int *)p) + 1 // 얘는 int second를 가리킨다.
물론 이렇게 쓰겠냐? 상식적인 선에서는 p->second 로 접근한다.
ptrdiff_t뿐만 아니라 size_t 까지만큼은 알고 가자
size_t는 메모리 크기나 배열의 인덱스 등, 크기를 나타내는데 사용되는 자료형이다. 주로 크기 계산, 배열의 크기, 메모리 할당에 쓴다.
- 부호 없는 정수형이다.
- 크기는 시스템에 따라 다르다. 32비트에선 4바이트, 64비트에선 8바이트이다.
- sizeof 연산자의 반환 타입이 바로 size_t이다.
아 뭐.. 크기를 나타내는 목적에 있어서는 당연히 부호가 없는게 맞고, 이런거 저런거 생각하면 말이 맞다.
근데 내가 옛날에 게임 하나를 진짜 즐겨 했는데 걔는 x86에서도 되고 x64에서도 되는 진짜 신기한 온라인 게임이었다.
근데 생각해보니.. x86에서 설치한 파일을 x64에 옮겨본 적은 없었던 것 같다.
그니까 설치 전에 이미 운영체제를 파악하고 나서 설치 할 응용 프로그램을 적합하게 선택하는 것이었고,
사실 한 프로그램이 x86/x64에서 실행된다라는건 눈속임에 가깝다는 결론이 나왔다.
그리고 실제로 .exe 파일 한 개가 32/64비트를 지원하는 경우는 불가능하다.
- 이상한 말씀 하지 마세요. 그럼 그 프로그램을 설치 하는 setup.exe는 한개잔아여.
- 그 setup.exe의 대표적인 설치 시스템인 InstallShield 조차, 안에서 해당 운영체제의 비트 수에 따라 분기하게 되어있다.
- 즉, 구별하지 않는다는건 결국 안에서 완벽한 이중구조로 포장되어있다라는 의미가 된다.
어쨌든.. 왜 안되냐?
- 실행 파일 형식이 다르고, 데이터 모델에 대한 바이트 배정 차이가 있고, 시스템 콜과 라이브러리 호환성과 연관 되어있다.
- 지금 굵게 이야기하고 있는 얘가 포인터랑 직접적인 연관이 있다는 뜻이 되겠다. 32비트의 포인터는 4GB까지만 가리킬 수 있으니, 4.000001 GB에 있는 데이터에는 접근 할 수가 없다는것이다.
- 오 그럼 64비트는 반대로 32비트 프로그램을 쓸 수 있네요. 0 ~ 4 GB에 대한 접근은 어쨌든 가능하니까
- 예. 이건 WoW64에 대해 직접 찾아보길 바란다. 여기서 논하면 너무 벗어난다.
- 오 그럼 64비트는 반대로 32비트 프로그램을 쓸 수 있네요. 0 ~ 4 GB에 대한 접근은 어쨌든 가능하니까
- 지금 굵게 이야기하고 있는 얘가 포인터랑 직접적인 연관이 있다는 뜻이 되겠다. 32비트의 포인터는 4GB까지만 가리킬 수 있으니, 4.000001 GB에 있는 데이터에는 접근 할 수가 없다는것이다.
비록 포인터의 연산에 대해 이야기를 시작했지만, C언어는 기계 친화적인 만큼 기계 파편화에 대한 영향을 1순위로 받는 언어이다.
하드웨어와 가까우니 성능 최적화를 기계 친화적으로 이룰 수 있다. 하지만,
하드웨어 파편화의 영향을 직격으로 맞는다. 그냥 얘는 하드웨어라고 봐야한다.
욕심은 많지만, 나중에 더 다뤄보겠다.
'TIL' 카테고리의 다른 글
시스템 메모리에서의 스택과 큐 (0) | 2025.04.13 |
---|---|
[C] 동적 메모리 할당 (0) | 2025.04.13 |
트리 순회에 대해 논하기 (0) | 2025.03.27 |
TIL 0319 (0) | 2025.03.19 |
TIL 0318, 더더덜 쉬운 문제와 이론 공부 (0) | 2025.03.19 |