TL;DR
- C는 러스트 프로그래머에게 불리언, 문자열, 정수 폭, 오류 처리, 필드 접근, 배열, 포인터에서 서로 다른 규칙을 적용하는 시스템 프로그래밍 언어임.
- C의 불리언은 C23 이전에는 기본 언어 요소가 아니며, 이전 버전에서는
<stdbool.h>의 정의를 사용해야 함. - C 문자열은 길이를 별도로 저장하지 않고 널 종료 문자(
\0)를 사용하므로, 종료 공간 확보와 삽입이 필요하며 누락 시 범위 밖 읽기가 발생할 수 있음. - C 정수형의 폭은 플랫폼에 따라 달라지며, 이식성이 중요하면
<stdint.h>의 고정 폭 정수형 사용이 권장됨. - C에는 러스트의 빌림 검사와 참조가 없고, 오류 처리를 강제하지 않으며, 함수 인자로 전달된 배열은 포인터로 변환됨.
불리언은 기본 제공되지 않음
- 최초 C에는 불리언 기본형이 없어 프로그램에서 정수 0과 1을 사용했으며, C99에서는 선택적 헤더
<stdbool.h>가 불리언을 제공함. - C99에서
true와false는 다른 언어의 키워드나 리터럴이 아니라 각각 1과 0으로 확장되는 정의임. - C23에서는 불리언이 언어의 기본 요소가 됐지만, 이전 버전으로 컴파일할 때는
<stdbool.h>를 포함해야 함. _Bool은 헤더 없이 사용할 수 있지만,true와false정의에는 여전히 해당 헤더가 필요함. Clang의 C23 지원은 아직 부분적일 수 있음.
널 종료 문자열
- 러스트의
&str은 메모리 주소 8바이트와 문자열 길이 8바이트를 포함해 16바이트를 사용함. 동적으로 크기가 정해지는 타입인str의 길이를 포인터 메타데이터로 추적하기 때문임. - 이 방식은 문자열 길이를 효율적으로 가져오지만
&str참조마다 메모리를 더 사용함. 반면 C는 길이를 별도로 저장하지 않고 모든 문자열 끝에 널 바이트(\0)를 둠. - C의 널 종료 방식에는 다음 특성이 따름.
- 문자열과 함께 별도의 길이 변수를 관리할 필요가 없음.
- 모든 문자열에 널 바이트를 위한 공간이 필요하므로 빈 문자열
""도 메모리 1바이트를 차지함. - 문자열 중간에 널 바이트를 넣으면
<string.h>함수가 제대로 동작하지 않을 수 있음. - 널 종료 문자를 빼먹으면 범위 밖 읽기가 발생할 수 있음.
- 문자열을 뒤집는 C 함수는 길이를 구한 뒤 문자열과 널 종료 문자 공간을 합쳐
len + 1바이트를 할당하고, 마지막 위치에\0을 직접 기록함. - 대응하는 러스트 함수는 널 종료 문자를 따로 처리하지 않음. 다만 제시된 함수는 관용적인 러스트 코드가 아니며 ASCII 텍스트만 올바르게 처리함. 실제 사용을 위한 구현은
forward.chars().rev().collect::<String>()한 줄로 작성할 수 있음.
플랫폼에 따라 달라지는 정수 폭
- C 정수형은 정확한 비트 수가 보장되지 않으며 폭은 대상 플랫폼에 따라 달라짐.
char: C 표준상 최소 8비트, 64비트 유닉스와 윈도우에서 8비트임.short: 최소 16비트, 두 플랫폼에서 16비트임.int: 최소 16비트, 두 플랫폼에서 32비트임.long: 최소 32비트, 64비트 유닉스에서 64비트이고 64비트 윈도우에서 32비트임.long long: 최소 64비트, 두 플랫폼에서 64비트임.- 플랫폼 간 호환성이 중요하다면
<stdint.h>가 제공하는 고정 폭 정수형을 사용하는 방식이 권장됨. - 러스트
u8,u16,u32,u64에 대응하는 C 형식은 각각uint8_t,uint16_t,uint32_t,uint64_t임. - 러스트
usize에 대응하는 C 형식은size_t이며,i8,i16,i32,i64에는 각각int8_t,int16_t,int32_t,int64_t가 대응함. - 러스트
isize에 대응하는 C 형식은ptrdiff_t임.usize와isize는size_t,ptrdiff_t와 의미상 완전히 일치하지 않으므로 사용 전 차이를 확인할 필요가 있음.
오류 처리는 번거로움
- 러스트의
Result, 합 타입인 열거형,match문은 오류 처리를 강제하고 이를 쉽게 작성하도록 돕지만, C의 오류 처리는 주로 -1 같은 특정 정수나 널 포인터를 오류 신호로 반환하는 방식임. - 스레드 로컬 정수인
errno를 확인하면 특정 오류 종류에 관한 정보를 더 얻을 수 있지만, 실제 오류 메시지나 스택 추적을 얻기는 훨씬 어려움. - C는 오류 처리를 강제하지 않으므로 함수가 실패할 수 있다는 점을 직접 기억해야 함. 예를 들어
malloc()은 실패 시 널 포인터를 반환하며, 이를 확인하지 않고reversed[i]에 접근하면 메모리 부족 상황에서 세그멘테이션 오류가 발생할 수 있음. - 할당 결과가 널 포인터인지 확인하고, 널이면
perror("Error")로 오류를 출력한 뒤 프로그램을 종료하면 세그멘테이션 오류보다 명확한 결과를 제공함. 예시 출력은Error: Cannot allocate memory임. - 태그가 있는 공용체로 러스트의
Result를 C에서 흉내 내는 방법을 시험했지만, 사용하기 복잡하고 오류 처리를 하지 않은 채result.value.ptr에 접근하는 것도 막지 못함. private와public같은 가시성 제어자가 없다는 점은 의도적인 설계 선택으로 보이며, C는 프로그래머가 올바르게 처리할 것이라는 전제에 크게 의존하고 계약이나 안전한 추상화를 위한 기능은 제한적임.- 프로그램 요구 사항을 타입 시스템에 표현해 컴파일러가 검사하도록 하는 편을 선호하며, 이를 통해 코드가 컴파일되면 올바르게 작성됐다는 점을 합리적으로 확신할 수 있다는 관점임. 러스트 타입 시스템으로 코드 정확성을 보장하는 SIMD 사례를 다룬 글도 참고 대상으로 언급됨.
필드 접근 문법
- C에는 값의 필드에 접근하는
.연산자와 포인터를 통한 필드 접근에 쓰는->연산자가 있음. - 러스트는 모든 경우에
.을 사용하므로 C의 두 연산자 구분이 처음에는 낯설 수 있음. ->연산자가 생긴 역사에 관해서는 Stack Overflow의 설명이 참고 자료로 언급됨.
배열은 포인터로 변환됨
- C 배열은 상황에 따라
sizeof()로 크기를 확인할 수 있는 값이 되거나, 크기를 알 수 없는 포인터처럼 보임. 일반적으로 배열이 정의된 함수 안에서 다루는지 여부가 차이를 만듦. - 함수 안에서 선언된 크기 지정 배열과 초기값으로 크기를 추론한 배열은 각각 3바이트를 차지하며,
sizeof()로 이를 확인할 수 있음. - 배열을 함수에 인자로 전달하면 첫 요소를 가리키는 포인터로 암묵 변환됨. 따라서
char declared_size[3]와char inferred_size[]를 매개변수로 선언해도 함수의 관점에서는 둘 다char*임. - 예시 환경에서 함수 안의
sizeof()결과는 배열의 3바이트가 아니라 포인터의 8바이트임. Clang은 배열 함수 매개변수에sizeof()를 적용하면 배열 대신char *의 크기를 반환한다는 경고를 표시함.
참조는 없고 포인터만 있음
- C에는 빌림 검사도 참조 개념도 없으며, 모든 참조 작업은 원시 포인터로 처리함.
- 포인터 산술을 사용해 배열 인덱스 대신 포인터를 순회할 수 있지만, 이 방식이 좋은 선택인지는 확실하지 않음.
- 포인터를 다룰 때는 주의가 필요함. 메모리 처리 실수는 버퍼 오버플로와 범위 밖 쓰기를 일으킬 수 있으며, 이는 코드 보안에 상당한 위협이 됨.
결론
- C를 배우는 과정은 즐거웠으며, 개인 프로젝트에서 선택할 가능성은 낮더라도 시스템 프로그래머가 알아야 할 핵심 언어라는 결론임.
- 글의 모든 예제는 GitHub에서 확인할 수 있다고 안내함.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요