C는 멈춘 언어가 아니다
C언어를 처음 배울 때 자주 듣는 말이 있습니다. "C는 오래된 언어다." 이 말은 절반만 맞습니다. C는 오래된 언어입니다. 하지만 멈춘 언어는 아닙니다.
우리가 지금까지 배운 int, char, 배열, 포인터, 구조체, 파일 입출력은 모두 아주 오래전부터 C의 중심에 있던 개념입니다. 그런데 C는 수십 년 동안 같은 모습으로 박제되어 있었던 것이 아닙니다. 컴퓨터 환경이 바뀌고, 컴파일러가 발전하고, 보안과 이식성에 대한 요구가 커지면서 C 표준도 계속 다듬어졌습니다.
여기서 말하는 표준은 "C언어는 이런 문법과 이런 동작을 가져야 한다"고 국제적으로 정해 둔 약속입니다. 컴파일러는 이 표준을 참고하여 C 코드를 해석합니다. 물론 실제 세상은 조금 더 복잡합니다. 표준에 들어간 기능이라고 해서 모든 컴파일러가 같은 날, 같은 수준으로 지원하는 것은 아니기 때문입니다.
이번 장의 핵심은 최신 문법을 줄줄 외우는 것이 아닙니다. 더 중요한 감각은 이것입니다.
현대 C를 안다는 것은 "최신 기능을 안다"를 넘어, "표준과 내 도구의 지원 상태가 다를 수 있다"는 사실을 이해하는 것이다.
이 감각은 앞으로 운영체제, 임베디드, 게임 엔진, 네이티브 라이브러리, 오픈소스 프로젝트를 다룰 때 아주 중요해집니다. 어떤 코드는 내 컴퓨터에서는 잘 컴파일되지만 다른 환경에서는 실패합니다. 어떤 책이나 블로그에서 본 문법은 표준에는 들어갔지만, 내가 쓰는 컴파일러에서는 아직 동작하지 않을 수 있습니다. 반대로 어떤 문법은 표준이 아닌데 특정 컴파일러 확장 기능으로 조용히 받아들여지기도 합니다.
이제 C 표준의 흐름을 따라가 보겠습니다.

K&R C에서 C23까지
C언어의 초창기 모습은 흔히 K&R C라고 부릅니다. Brian Kernighan과 Dennis Ritchie가 쓴 《The C Programming Language》에서 널리 알려진 형태입니다. 이 시절의 C는 오늘날처럼 엄격하게 표준화된 언어라기보다, 유닉스와 함께 성장한 실용적인 시스템 프로그래밍 언어에 가까웠습니다.
그러다 C가 다양한 컴퓨터와 컴파일러에서 사용되기 시작하자 문제가 생겼습니다. 같은 C 코드라도 컴파일러마다 다르게 해석하면 프로그램을 옮기기가 어려웠습니다. 그래서 "C라면 최소한 이런 규칙은 따라야 한다"는 공식 약속이 필요해졌습니다.
C 표준의 큰 흐름은 다음과 같이 볼 수 있습니다.
K&R C: 표준화 이전의 전통적인 C
C89/C90: ANSI와 ISO를 통해 정리된 첫 공식 표준
C99:
//주석,long long, 가변 길이 배열 등 여러 현대적 기능 도입C11: 멀티스레딩, 원자적 연산,
_Static_assert등 시스템 프로그래밍 요구 반영C17/C18: 큰 기능 추가보다는 C11의 결함을 정리한 보정판 성격
C23: 최신 C 표준으로, 더 명확한 문법과 C++과의 호환성 개선, 새 속성 문법 등을 포함
C89와 C90은 이름은 다르지만 거의 같은 표준을 가리키는 말로 쓰입니다. C17과 C18도 비슷합니다. C17은 흔히 부르는 이름이고, ISO 표준 문서의 연도 때문에 C18이라고 부르기도 합니다.
그리고 이제 C23이 등장했습니다. C23은 공식적으로 ISO/IEC 9899:2024이며, C17을 대체하는 현재 C 표준입니다. WG14는 C23 표준이 ISO와 IEC에 의해 2024년에 채택되었다고 설명합니다.1 이름은 C23인데 문서 번호에 2024가 들어가는 것이 조금 이상하게 보일 수 있습니다. C 표준 이름은 보통 내부 버전 매크로나 작업 시점의 관습과 맞물려 불리기 때문에, 실제 ISO 문서 발행 연도와 항상 완벽히 일치하지는 않습니다.
C23에서는 __STDC_VERSION__이라는 미리 정의된 매크로 값도 202311L로 바뀝니다.2 이 매크로는 "현재 컴파일러가 어떤 C 표준 모드로 컴파일하고 있는가"를 확인할 때 쓸 수 있는 힌트입니다.
#include <stdio.h>
int main(void)
{
#ifdef __STDC_VERSION__
printf("__STDC_VERSION__ = %ldn", __STDC_VERSION__);
#else
printf("__STDC_VERSION__ is not definedn");
#endif
return 0;
}이 코드를 컴파일할 때 어떤 옵션을 주느냐에 따라 출력이 달라질 수 있습니다. 예를 들어 GCC나 Clang에서는 다음처럼 표준 모드를 명시할 수 있습니다.
gcc -std=c17 version.c -o version
gcc -std=c23 version.c -o version여기서 중요한 것은 gcc라는 같은 명령을 쓰더라도 -std=c17로 컴파일하느냐, -std=c23으로 컴파일하느냐에 따라 컴파일러가 코드를 해석하는 규칙이 달라질 수 있다는 점입니다.
C23은 혁명이라기보다 정리와 현대화에 가깝다
C23을 처음 들으면 "이제 C가 완전히 새로운 언어가 된 걸까?" 하고 궁금할 수 있습니다. 답은 "아니요"에 가깝습니다.
C23은 C를 Python이나 JavaScript처럼 바꾸려는 시도가 아닙니다. C의 핵심은 여전히 값, 주소, 메모리, 포인터, 구조체, 함수, 파일, 컴파일과 링킹입니다. malloc과 free를 이해해야 하고, 배열 범위를 벗어나면 위험하며, 문자열 끝의 0도 여전히 중요합니다.
다만 C23은 오래된 부분을 조금 더 명확하게 만들고, 실무에서 자주 쓰이던 편의 기능을 표준으로 끌어오고, C++과 함께 쓰일 때 생길 수 있는 혼동을 줄이는 방향으로 움직입니다. 최신 C는 화려한 문법을 잔뜩 붙인 언어라기보다, 오래된 기계실의 배선을 하나씩 라벨링하고 위험한 스위치에 경고 스티커를 붙이는 작업에 가깝습니다.
C23의 변화는 아주 많지만, 입문 단계에서 모든 기능을 외울 필요는 없습니다. 이번 장에서는 방향을 이해하기 좋은 기능 몇 가지를 골라 보겠습니다.
메시지 없는 static_assert
C11에서는 _Static_assert가 도입되었습니다. 이것은 프로그램을 실행하기 전에, 컴파일 단계에서 조건을 검사하는 장치입니다.
예를 들어 어떤 코드가 int 크기가 최소 4바이트라는 가정 위에서 동작한다고 해 봅시다. 그렇다면 실행 중에 문제가 터지기를 기다리는 대신 컴파일할 때 바로 확인할 수 있습니다.
_Static_assert(sizeof(int) >= 4, "int must be at least 4 bytes");
int main(void)
{
return 0;
}조건이 참이면 컴파일이 계속되고, 거짓이면 컴파일러가 에러를 냅니다. 이것을 정적 단언이라고 부릅니다. "이 조건은 컴파일 시점에 반드시 참이어야 한다"고 선언하는 것입니다.
C23에서는 static_assert가 더 자연스럽게 쓰일 수 있고, 메시지 없이도 사용할 수 있습니다. cppreference의 C23 기능 목록에도 단일 인자 static_assert가 새 언어 기능으로 정리되어 있습니다.2
static_assert(sizeof(int) >= 4);
int main(void)
{
return 0;
}물론 입문자에게는 메시지를 함께 쓰는 편이 더 친절합니다.
static_assert(sizeof(int) >= 4, "This program assumes int is at least 4 bytes");실무에서는 컴파일 에러 메시지를 읽는 사람이 미래의 나일 수도 있고, 같은 팀의 동료일 수도 있습니다. 조건만 덩그러니 있는 것보다 "왜 이 조건이 필요한지"를 적어 두면 코드가 훨씬 친절해집니다.
nullptr: 널 포인터를 더 분명하게
우리는 포인터 장에서 NULL을 배웠습니다. 포인터가 아무것도 가리키지 않는 상태를 나타낼 때 흔히 NULL을 사용했습니다.
int *p = NULL;
if (p == NULL) {
printf("p는 아무것도 가리키지 않습니다.n");
}그런데 NULL은 역사적으로 매크로입니다. 구현에 따라 0, ((void *)0) 같은 형태로 정의될 수 있습니다. C에서는 이것이 대체로 잘 동작하지만, C++과 섞이거나 함수 오버로드 같은 C++ 기능이 개입되면 "정수 0인가, 널 포인터인가?"라는 혼동이 생길 수 있습니다.
C23은 nullptr와 그에 대응하는 nullptr_t 타입을 도입했습니다.2 nullptr는 "이 값은 널 포인터를 뜻한다"는 의도를 더 분명하게 표현합니다.
#include <stddef.h>
#include <stdio.h>
int main(void)
{
int *p = nullptr;
if (p == nullptr) {
printf("p는 널 포인터입니다.n");
}
return 0;
}이 코드는 C23을 지원하는 컴파일러와 표준 모드에서 사용할 수 있습니다.
여기서 중요한 태도가 있습니다. nullptr가 들어왔다고 해서 C가 C++이 된 것은 아닙니다. C23의 nullptr는 C++ 문법을 무작정 따라 한 장식이 아니라, 널 포인터의 의미를 더 명확히 하려는 변화로 이해하는 편이 좋습니다. C와 C++은 여전히 다른 언어입니다. 다만 두 언어가 함께 쓰이는 현장이 많기 때문에, 불필요한 충돌과 혼동을 줄이려는 방향으로 일부 문법이 가까워지는 것입니다.
속성 문법: 컴파일러에게 남기는 쪽지
C23에서 눈에 띄는 변화 중 하나는 [[...]] 형태의 속성(attribute) 문법입니다. 속성은 프로그램의 핵심 동작을 직접 바꾸는 문법이라기보다, 컴파일러에게 추가 정보를 주는 표식에 가깝습니다.
비유하자면 코드 옆에 붙이는 작은 포스트잇입니다.
"이 함수의 반환값은 버리면 위험해."
"이 변수는 일부러 안 쓰는 거야. 경고하지 마."
"이 함수는 예전 방식이야. 새 코드에서는 쓰지 마."
이런 의도를 코드에 직접 표시하면 컴파일러가 더 좋은 경고를 낼 수 있고, 코드 품질을 관리하기 쉬워집니다.
C23은 [[nodiscard]], [[maybe_unused]], [[deprecated]], [[fallthrough]] 같은 표준 속성을 포함합니다.2 하나씩 가볍게 살펴보겠습니다.
[[nodiscard]]: 반환값을 그냥 버리지 마세요
어떤 함수는 반환값이 매우 중요합니다. 예를 들어 메모리 할당 함수가 성공했는지 확인하지 않으면 프로그램이 바로 위험해질 수 있습니다.
#include <stdlib.h>
[[nodiscard]] void *allocate_array(size_t count)
{
return malloc(sizeof(int) * count);
}
int main(void)
{
allocate_array(10); // 반환값을 사용하지 않음
return 0;
}이 코드에서 allocate_array(10);은 메모리를 할당하고 그 주소를 반환합니다. 그런데 반환값을 저장하지 않았습니다. 그러면 할당된 메모리를 나중에 free할 방법도 없습니다. 메모리 누수가 생길 수 있습니다.
[[nodiscard]]는 컴파일러에게 "이 함수의 반환값을 무시하면 경고해 줘"라고 알려 줍니다. 컴파일러가 이 속성을 지원한다면, 위와 같은 코드를 보고 경고를 낼 수 있습니다.
조금 더 친절하게 메시지를 넣을 수도 있습니다.
#include <stdlib.h>
[[nodiscard("반환된 포인터를 저장하고 나중에 free해야 합니다")]]
void *allocate_array(size_t count)
{
return malloc(sizeof(int) * count);
}이 속성은 파일 열기, 메모리 할당, 오류 코드 반환처럼 "반환값을 확인해야 의미가 있는 함수"에 잘 어울립니다.
#include <stdio.h>
[[nodiscard]] FILE *open_data_file(const char *path)
{
return fopen(path, "r");
}이 함수를 호출하고 반환값을 확인하지 않는다면, 파일 열기에 실패했는지 성공했는지 알 수 없습니다. [[nodiscard]]는 그런 실수를 컴파일 단계에서 잡을 기회를 줍니다.
[[maybe_unused]]: 일부러 안 쓰는 이름입니다
개발하다 보면 어떤 변수나 매개변수를 당장은 쓰지 않지만 남겨 두어야 할 때가 있습니다. 예를 들어 함수의 형태를 맞추기 위해 매개변수를 받아야 하지만, 특정 구현에서는 그 값을 쓰지 않을 수 있습니다.
#include <stdio.h>
void log_event([[maybe_unused]] int level, const char *message)
{
printf("%sn", message);
}여기서 level은 지금 함수 안에서 사용되지 않습니다. 보통 컴파일러 경고 옵션을 엄격하게 켜면 "사용하지 않는 매개변수"라는 경고가 나올 수 있습니다. 하지만 [[maybe_unused]]를 붙이면 "이 이름은 사용되지 않을 수 있다는 사실을 알고 있다"고 컴파일러에게 알려 줄 수 있습니다.
이 속성은 경고를 무작정 숨기는 도구가 아닙니다. 오히려 반대입니다. 불필요한 경고를 정리해 두면, 진짜 중요한 경고가 더 잘 보입니다. 디버깅 장에서 말했듯이 경고는 미래의 버그가 보내는 작은 신호입니다. 신호가 너무 많이 울리면 사람은 결국 경고음을 무시하게 됩니다. [[maybe_unused]]는 의미 있는 경고와 의도된 상황을 구분하는 데 도움을 줍니다.
[[deprecated]]: 이제는 낡은 길입니다
프로그램은 한 번에 완성되지 않습니다. 시간이 지나면서 더 좋은 함수가 생기고, 예전 함수는 남겨 두되 새 코드에서는 쓰지 않게 만들고 싶을 때가 있습니다.
#include <stdio.h>
[[deprecated("use print_message instead")]]
void print_old(const char *message)
{
printf("%sn", message);
}
void print_message(const char *message)
{
printf("%sn", message);
}
int main(void)
{
print_old("hello");
return 0;
}[[deprecated]]는 "이 함수는 아직 존재하지만 더 이상 권장하지 않는다"고 표시합니다. 컴파일러는 이 함수를 사용하는 코드를 발견하면 경고할 수 있습니다.
이런 장치는 큰 프로젝트에서 특히 유용합니다. 함수를 갑자기 삭제하면 기존 코드가 한꺼번에 깨집니다. 대신 먼저 deprecated로 표시해 사용자에게 시간을 줍니다. "지금은 컴파일되지만, 앞으로는 새 함수를 쓰세요"라고 알려 주는 셈입니다.
속성은 마법이 아니다
속성을 처음 보면 코드가 갑자기 똑똑해지는 마법처럼 느껴질 수 있습니다. 하지만 속성은 마법이 아닙니다. 속성은 컴파일러가 더 잘 도와줄 수 있도록 단서를 제공하는 장치입니다.
예를 들어 [[nodiscard]]를 붙였다고 해서 반환값을 무시하는 코드가 반드시 모든 환경에서 에러가 되는 것은 아닙니다. 컴파일러와 경고 설정에 따라 경고 수준이 달라질 수 있습니다. [[deprecated]]도 마찬가지입니다. 경고를 낼 수는 있지만, 그 함수를 강제로 삭제하지는 않습니다.
현대 C에서 좋은 습관은 "컴파일러에게 의도를 알려 주기"입니다. 자료형을 정확히 쓰고, const로 바꾸지 않을 값을 표시하고, static_assert로 가정을 고정하고, 속성으로 위험한 사용을 알려 주는 것입니다. C는 개발자를 많이 믿는 언어입니다. 그래서 개발자가 컴파일러에게 더 많은 정보를 줄수록 안전망이 촘촘해집니다.
작지만 반가운 편의 문법들
C23에는 입문자가 보기에도 비교적 이해하기 쉬운 편의 문법들이 있습니다. 이 기능들은 C의 본질을 바꾸지는 않지만, 코드를 조금 더 읽기 쉽게 만들 수 있습니다.
먼저 이진 정수 리터럴입니다. C23에서는 0b 또는 0B로 시작하는 이진수 표기를 사용할 수 있습니다.2
#include <stdio.h>
int main(void)
{
int flags = 0b1010;
printf("flags = %dn", flags);
return 0;
}비트 연산을 다룰 때 0b1010은 10보다 의도를 더 잘 드러낼 수 있습니다. 물론 모든 숫자를 이진수로 쓰라는 뜻은 아닙니다. 비트 패턴 자체가 중요한 상황에서 유용합니다.
숫자 구분자도 있습니다. 작은따옴표 '를 사용해 긴 숫자를 읽기 쉽게 나눌 수 있습니다.2
#include <stdio.h>
int main(void)
{
int speed_of_light = 299'792'458;
printf("%dn", speed_of_light);
return 0;
}사람의 눈에는 299792458보다 299'792'458이 훨씬 읽기 쉽습니다. 컴파일러는 구분자를 값의 일부로 보지 않습니다. 즉 위 숫자는 그냥 299792458과 같은 값입니다.
전처리기에도 작은 변화가 있습니다. 예를 들어 #elifdef, #elifndef 같은 지시문이 추가되었습니다.2 기존에는 다음처럼 썼습니다.
#if defined(WINDOWS)
/* Windows용 코드 */
#elif defined(LINUX)
/* Linux용 코드 */
#else
/* 기타 환경 */
#endifC23에서는 지원되는 환경이라면 다음처럼 조금 더 직접적인 표현이 가능합니다.
#ifdef WINDOWS
/* Windows용 코드 */
#elifdef LINUX
/* Linux용 코드 */
#else
/* 기타 환경 */
#endif문법이 아주 크게 달라진 것은 아니지만, 조건부 컴파일을 읽는 부담이 조금 줄어듭니다.
또한 C23에서는 true, false, bool 같은 이름의 취급도 달라집니다. 이전에는 보통 <stdbool.h>를 포함해 사용했지만, C23에서는 이들이 언어 차원에서 더 자연스럽게 다루어집니다.2 다만 이 변화는 기존 코드와 충돌할 수도 있습니다. 예를 들어 예전 C 코드에서 직접 typedef int bool;처럼 정의해 두었다면 C23 모드에서 문제가 될 수 있습니다.
이 지점에서 중요한 사실 하나가 나옵니다.
새 표준은 항상 완벽한 하위 호환을 보장하지 않는다.
즉, C17에서 컴파일되던 모든 코드가 C23에서 아무 수정 없이 컴파일된다고 생각하면 안 됩니다. 특히 오래된 코드일수록 새 키워드와 이름이 충돌할 수 있습니다.
C와 C++은 가까워지지만 같아지지는 않는다
C23의 여러 변화는 C++과의 호환성을 의식합니다. nullptr, [[nodiscard]], [[maybe_unused]], [[deprecated]], 메시지 없는 static_assert 같은 기능은 C++ 개발자에게 익숙한 모습입니다.
하지만 여기서 오해하면 안 됩니다. C23은 C++의 부분집합이 아닙니다. C++도 C의 단순한 확장판이 아닙니다. 두 언어는 역사적으로 가까운 친척이지만, 타입 규칙, 함수 오버로드, 객체 모델, 템플릿, 메모리 관리 방식에서 큰 차이를 가집니다.
C23이 C++과 비슷한 문법을 일부 받아들이는 이유는 "C를 C++로 만들기 위해서"라기보다 "두 언어 사이의 불필요한 마찰을 줄이기 위해서"라고 보는 편이 좋습니다. 실제 프로젝트에서는 C 라이브러리를 C++ 프로그램에서 호출하거나, C와 C++ 파일을 같은 빌드 시스템 안에서 함께 다루는 일이 많습니다. 이때 널 포인터 표현, 속성 문법, 정적 단언 같은 기본 도구가 비슷해지면 코드를 읽고 유지보수하기가 쉬워집니다.
그러나 여전히 C 코드는 C의 방식으로 생각해야 합니다. malloc으로 얻은 메모리는 직접 free해야 하고, 배열은 자기 크기를 기억하지 않으며, 문자열은 널 문자로 끝납니다. C23을 배운다고 해서 이 사실이 사라지지는 않습니다.
표준에 들어갔다고 바로 쓸 수 있는 것은 아니다
이제 가장 현실적인 이야기를 해야 합니다.
C23에 어떤 기능이 들어갔다고 해서, 오늘 내가 쓰는 컴파일러에서 바로 쓸 수 있다는 뜻은 아닙니다. 표준은 약속이고, 컴파일러는 그 약속을 실제로 구현하는 도구입니다. 약속이 발표된 뒤 도구가 따라오는 데에는 시간이 걸립니다.
GCC는 C23 지원 상태를 공식 문서로 정리하고 있으며, C23이 2024년에 출판된 ISO C 표준 개정이라고 설명합니다. 또한 GCC 15부터 C23 모드가 기본값이며, 명시적으로는 -std=c23 또는 GNU 확장을 함께 쓰는 -std=gnu23 옵션을 사용할 수 있다고 안내합니다.3
gcc -std=c23 main.c -o mainGNU 확장까지 허용하려면 다음처럼 쓸 수 있습니다.
gcc -std=gnu23 main.c -o mainc23과 gnu23의 차이도 기억해 둘 만합니다. c23은 표준 C23에 더 엄격하게 맞추려는 모드이고, gnu23은 C23을 기반으로 하되 GCC가 제공하는 GNU 확장 기능도 허용하는 모드입니다. 실무에서는 기존 오픈소스 프로젝트가 GNU 확장에 기대고 있는 경우가 있어 gnu23을 쓰기도 합니다. 반대로 이식성을 최대한 중요하게 생각한다면 c23에 더 가까운 방식으로 컴파일해 보는 것이 좋습니다.
Clang도 C23 모드를 제공합니다. Clang 문서는 -std=c23 옵션을 사용할 수 있으며, C23 지원 상태가 부분적이라고 설명합니다.4
clang -std=c23 main.c -o main여기서 "부분적"이라는 말이 중요합니다. 어떤 기능은 이미 지원하고, 어떤 기능은 아직 구현 중이며, 어떤 기능은 플랫폼이나 라이브러리 상태에 따라 다를 수 있습니다. 같은 -std=c23 옵션을 주더라도 GCC와 Clang의 결과가 완전히 같지 않을 수 있습니다.
MSVC도 C 표준 지원이 별도의 흐름을 가집니다. 특히 Windows에서 Visual Studio를 사용하는 독자는 "내가 쓰는 MSVC가 C23의 어떤 기능을 지원하는지"를 따로 확인해야 합니다. C와 C++은 Visual Studio 안에서 함께 다뤄지는 경우가 많지만, C++23 지원과 C23 지원은 같은 이야기가 아닙니다.
지원표를 읽는 습관
C23 기능이 실제로 내 컴파일러에서 되는지 알고 싶을 때 유용한 자료 중 하나가 cppreference의 컴파일러 지원표입니다. cppreference는 C23의 핵심 언어 기능별로 GCC, Clang, MSVC 등 여러 컴파일러의 지원 상태를 정리해 둡니다.5
예를 들어 어떤 표에서는 [[nodiscard]], [[maybe_unused]], [[deprecated]], 메시지 없는 static_assert, 이진 리터럴, 숫자 구분자 같은 기능이 어느 컴파일러의 어느 버전부터 지원되는지 확인할 수 있습니다.
하지만 지원표를 절대적인 진리처럼 받아들이면 안 됩니다. cppreference의 컴파일러 지원 페이지는 최선을 다해 관리되지만, 최신 컴파일러 릴리스보다 늦을 수 있다고 직접 안내합니다.6 즉 지원표는 훌륭한 지도이지만, 실시간 내비게이션은 아닙니다.
실무에서는 보통 다음 순서로 확인하는 것이 안전합니다.
먼저 사용하는 컴파일러의 종류와 버전을 확인한다.
공식 컴파일러 문서에서 해당 표준 옵션과 기능 지원 여부를 확인한다.
cppreference 같은 지원표로 기능별 지원 상태를 대략 파악한다.
작은 테스트 코드를 직접 컴파일해 본다.
실제 프로젝트의 빌드 옵션과 CI 환경에서도 같은 결과가 나오는지 확인한다.
버전 확인은 간단합니다.
gcc --version
clang --version컴파일 옵션을 바꾸어 작은 코드를 실험해 볼 수도 있습니다.
#include <stdio.h>
int main(void)
{
int *p = nullptr;
if (p == nullptr) {
puts("nullptr works");
}
return 0;
}이 파일을 test_nullptr.c라고 저장한 뒤 다음처럼 컴파일해 봅니다.
gcc -std=c23 test_nullptr.c -o test_nullptr성공하면 그 환경에서는 적어도 이 기능이 동작한다고 볼 수 있습니다. 실패하면 컴파일러 버전이 낮거나, C23 모드가 제대로 활성화되지 않았거나, 해당 기능이 아직 구현되지 않았을 수 있습니다.
기능 테스트 매크로와 조건부 코드
전처리기 장에서 #ifdef, #ifndef를 배웠습니다. 현대 C에서는 이런 조건부 컴파일이 표준과 기능 지원을 확인하는 데에도 쓰입니다.
예를 들어 __STDC_VERSION__을 확인해 C23 이상인지 검사할 수 있습니다.
#include <stdio.h>
int main(void)
{
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
puts("C23 or later mode");
#else
puts("Before C23 mode");
#endif
return 0;
}하지만 이것만으로 모든 기능 지원을 완벽히 알 수 있는 것은 아닙니다. 어떤 컴파일러는 C23 모드를 제공하지만 일부 기능은 아직 구현 중일 수 있습니다. 반대로 C23 이전 모드에서도 특정 기능을 확장으로 지원할 수 있습니다.
속성 지원을 확인할 때는 __has_c_attribute 같은 장치가 도움이 될 수 있습니다. C23 기능 목록에는 __has_c_attribute도 포함되어 있습니다.2
#ifndef __has_c_attribute
#define __has_c_attribute(x) 0
#endif
#if __has_c_attribute(nodiscard)
#define NODISCARD [[nodiscard]]
#else
#define NODISCARD
#endif
NODISCARD int read_score(void)
{
return 100;
}
int main(void)
{
read_score();
return 0;
}이 코드는 [[nodiscard]]를 지원하는 환경에서는 NODISCARD를 해당 속성으로 정의하고, 지원하지 않는 환경에서는 빈 매크로로 정의합니다. 이런 방식은 여러 컴파일러를 지원해야 하는 라이브러리에서 자주 볼 수 있습니다.
다만 입문 단계에서는 이런 코드를 직접 많이 작성하기보다, "아, 표준 기능도 조건을 걸어 사용할 수 있구나" 정도로 이해하면 충분합니다.
오래된 코드가 새 표준에서 깨질 수 있는 이유
새 표준은 보통 더 좋은 방향으로 언어를 발전시키지만, 그 과정에서 오래된 코드와 충돌할 수 있습니다.
대표적인 이유는 새 키워드입니다. 예전에는 평범한 이름으로 쓸 수 있었던 단어가 새 표준에서 키워드가 되면 문제가 생깁니다.
/* 예전 코드에서 이런 이름을 썼다고 가정해 봅시다. */
typedef int bool;C23에서는 bool, true, false 같은 이름의 취급이 바뀌었기 때문에 이런 코드가 충돌할 수 있습니다.
또 다른 변화는 함수 선언 방식입니다. 오래된 C에서는 다음과 같은 선언이 "매개변수 정보를 명확히 적지 않은 함수"처럼 취급되던 시절이 있었습니다.
int func();현대 C에서는 이런 애매한 표현을 줄이고, 매개변수가 없다는 뜻이라면 명확히 void를 쓰는 습관이 중요합니다.
int func(void);사실 이 책에서는 처음부터 main(void)처럼 쓰는 방식을 권장했습니다.
int main(void)
{
return 0;
}이런 습관은 단지 깔끔해 보이기 위한 것이 아닙니다. 표준이 엄격해지고 컴파일러 경고가 강해질수록, 애매한 코드는 점점 더 문제가 됩니다. 좋은 C 코드는 컴파일러가 오해할 여지를 줄이는 코드입니다.
어떤 표준으로 공부하고, 어떤 표준으로 일해야 할까?
입문자는 여기서 고민할 수 있습니다.
"그럼 나는 C23으로 공부해야 하나요?"
답은 상황에 따라 다릅니다.
C의 기본기를 배우는 단계라면 C99나 C11 수준의 문법을 기준으로 공부해도 충분합니다. 변수, 조건문, 반복문, 함수, 배열, 문자열, 포인터, 구조체, 동적 메모리, 파일 입출력은 표준 버전이 바뀌어도 핵심이 크게 달라지지 않습니다. 이 책에서 다룬 대부분의 내용도 C의 중심부에 해당합니다.
반면 새 프로젝트를 시작하고 최신 컴파일러를 사용할 수 있다면 C23 기능을 조금씩 살펴볼 만합니다. 특히 static_assert, 속성 문법, nullptr, 이진 리터럴, 숫자 구분자처럼 코드의 의도를 분명하게 만드는 기능은 배워 둘 가치가 있습니다.
하지만 회사, 학교, 임베디드 장비, 오래된 운영체제, 특정 SDK에서는 최신 표준을 바로 쓰기 어렵습니다. 어떤 마이크로컨트롤러용 컴파일러는 최신 GCC나 Clang보다 표준 지원이 늦을 수 있습니다. 게임 콘솔이나 산업 장비처럼 인증과 안정성이 중요한 환경에서는 일부러 오래된 컴파일러와 표준을 유지하기도 합니다.
그래서 실무적인 태도는 다음과 같습니다.
배울 때는 최신 흐름을 이해한다.
쓸 때는 프로젝트의 컴파일러와 플랫폼을 확인한다.
공유할 때는 빌드 옵션과 요구 표준을 명시한다.예를 들어 프로젝트 문서에 다음처럼 적어 두면 좋습니다.
Required C standard: C17
Tested compilers:
- GCC 13
- Clang 17또는 C23 기능을 쓴다면 이렇게 더 명확히 적을 수 있습니다.
Required C standard: C23
Required features:
- nullptr
- [[nodiscard]]
- binary integer constants
Tested compilers:
- GCC 15 with -std=c23이런 문서화는 사소해 보이지만, 다른 사람이 프로젝트를 빌드할 때 큰 차이를 만듭니다.
C23을 확인하는 작은 실험실
마지막으로 C23 기능 몇 가지를 한 파일에서 실험해 봅시다. 이 코드는 모든 컴파일러에서 바로 동작한다고 보장할 수 없습니다. 바로 그 점을 확인하기 위한 실험입니다.
#include <stdio.h>
#include <stdlib.h>
static_assert(sizeof(int) >= 4, "This example expects int to be at least 4 bytes");
[[nodiscard("반환된 메모리는 free해야 합니다")]]
void *create_buffer(size_t size)
{
return malloc(size);
}
[[deprecated("use print_new_message instead")]]
void print_old_message(void)
{
puts("old message");
}
void print_new_message(void)
{
puts("new message");
}
void debug_log([[maybe_unused]] int level, const char *message)
{
puts(message);
}
int main(void)
{
int mask = 0b1010;
int distance = 299'792'458;
printf("mask = %dn", mask);
printf("distance = %dn", distance);
int *p = nullptr;
if (p == nullptr) {
puts("p is nullptr");
}
create_buffer(100); // [[nodiscard]] 경고를 기대할 수 있음
print_old_message(); // [[deprecated]] 경고를 기대할 수 있음
debug_log(1, "debug log");
return 0;
}GCC 15 이상이 설치되어 있다면 다음처럼 시도해 볼 수 있습니다.
gcc -std=c23 -Wall -Wextra c23_test.c -o c23_test여기서 -Wall -Wextra는 여러 경고를 켜는 옵션입니다. 속성 문법은 경고와 함께 쓸 때 가치가 더 잘 드러납니다.
만약 컴파일이 실패한다면 실패 자체도 좋은 학습입니다. 에러 메시지를 읽어 보세요. 어떤 기능을 모르는지, 어떤 문법에서 멈췄는지, 컴파일러 버전이 무엇인지 확인해 보세요. 디버깅 장에서 배운 것처럼 에러 메시지는 적이 아니라 단서입니다.
현대 C의 핵심은 도구를 의심하는 능력이다
C를 처음 배울 때는 문법 하나하나가 어렵습니다. 포인터는 낯설고, 문자열은 불친절하고, malloc과 free는 조심스럽습니다. 그런데 어느 정도 배우고 나면 새로운 어려움이 나타납니다.
"이 코드는 왜 내 컴퓨터에서는 되는데 서버에서는 안 될까?"
"책에서는 된다는데 왜 내 컴파일러는 에러를 낼까?"
"표준 C라고 했는데 왜 경고가 이렇게 많을까?"
이 질문에 답하려면 문법만 알아서는 부족합니다. 표준, 컴파일러, 버전, 옵션, 플랫폼을 함께 봐야 합니다.
C23은 최신 C의 얼굴입니다. 하지만 C23을 배운다는 것은 새 키워드 목록을 암기하는 일이 아닙니다. 더 중요한 것은 C가 지금도 변하고 있다는 사실을 알고, 내가 쓰는 도구가 그 변화를 어디까지 따라왔는지 확인하는 습관을 갖는 것입니다.
지금까지 이 책은 계속 같은 질문으로 돌아왔습니다.
값은 어디에 저장되는가?
주소는 무엇인가?
배열은 왜 자기 크기를 기억하지 않는가?
포인터는 왜 조심해야 하는가?
메모리는 누가 할당하고 누가 해제하는가?
이제 여기에 질문 하나를 더 추가해 봅시다.
내가 쓴 C 코드는 어떤 표준과 어떤 컴파일러 위에서 해석되는가?
이 질문을 할 수 있다면, 여러분은 단순히 C 문법을 배운 사람을 넘어 C 프로그램이 실제 세계에서 어떻게 살아 움직이는지 이해하는 사람에 가까워진 것입니다.
다음 장에서는 지금까지 배운 내용을 모아 최종 프로젝트를 만들어 봅니다. 구조체로 데이터를 묶고, 동적 메모리로 크기를 늘리고, 파일에 저장하고, 여러 파일로 프로그램을 나누는 흐름까지 연결할 것입니다. 이제 C의 조각들을 하나의 프로그램으로 조립할 차례입니다.
참고
3C Standards Support in GCC - GNU Project
4Clang - C Programming Language Status
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.
