최근 수정 시각 : 2025-01-23 02:58:19

C(프로그래밍 언어)/상세

파일:상위 문서 아이콘.svg   상위 문서: C(프로그래밍 언어)
1. 개요2. 상세
2.1. 사용 분야2.2. 등장 배경2.3. 장점2.4. 단점2.5. 저수준 언어로서의 C2.6. 고수준 언어로서의 C2.7. 이식성2.8. 점유율과 플랫폼별 지원 현황2.9. 다른 프로그래밍 언어에 미친 영향
2.9.1. C++와의 관계
2.10. 대한민국에서의 위상
2.10.1. 입문용 언어?

1. 개요

C언어를 상세하게 정리한 문서

2. 상세

C언어의 정신은 C99 Rationale에서 다음과 같이 묘사하고 있다.
  • 프로그래머를 믿어라. (Trust the programmer)
  • 프로그래머가 작업을 못하게 방해하지 마라. (Don't prevent the programmer from doing what needs to be done)
  • 언어를 작고 간단하게 유지하라. (Keep the language small and simple)
  • 명령을 실행하는 방법을 하나만 제공하라. (Provide only one way to do an operation)
  • 호환성은 장담할 수 없더라도 빨리 작동하게 만들어라. (Make it fast, even if it is not guaranteed to be portable)

첫 줄의 '프로그래머를 믿어라' 부분이 오늘날 다른 언어들과 가장 큰 차이를 불러오는 것이다. 오늘날 다른 고생산성 언어들이 프로그래머를 못 믿고 퍼포먼스 희생을 감수하고서라도 문제가 생길만한 부분들을 컴파일러 또는 가상 머신에서 자동으로 처리해주거나 프로그래머가 이상한 코드를 짜지 못하도록 엄격하게 컴파일해준다면, C언어는 "프로그래머인 당신을 믿을 테니까 알아서 프로그래밍해라." 한마디로 끝낸다.[1]

C언어 자체는 지원되는 기능이 적고 문법이 간단하다. 객체 지향 프로그래밍(OOP)이나 코루틴, 클로저, 메타 프로그래밍 등 고수준의 기능들을 지원하는 언어들과 비교하면 특히나 그렇다. 550쪽 정도밖에 안 되는 C언어 표준에서도 순수 문법 부분은 200쪽 정도밖에 안 되며 나머지는 다 라이브러리 관련 부분이고, 함수의 개수로 치면 고작해야 150개 근처이다. 수천 개나 되는 기본 라이브러리를 지원하는 다른 언어들과 비교하면 정말 작고 간단하다. 초반의 포인터 장벽만 넘는다면 문법 자체를 마스터하고 간단한 커맨드라인 프로그래밍을 할 수 있는 수준까지는 엄청 쉽다.

하지만 기능이 적다고 결코 쉬운 건 아니다. 프로그래밍을 할 때 지원하는 기능이 적다고 그 적은 기능만 쓸 수는 없다. 따라서 기본 라이브러리에서 지원하지 않는 기능은 결국 프로그래머가 직접 구현해서 써야 하는데, 1970년대 이후 프로그래밍 언어계에서 오늘날 영어와 같은 위치를 차지하고 있던 덕분에 그런 기능의 구현이나 최적화에 관한 많은 트릭들이 존재하고 이것을 얼마나 많이 알고 있는 가가 사실 C언어의 핵심이다.

오늘날 고수준 언어들이 다수의 프로그래머가 함께 개발하는 것을 염두에 두고 팀에 누가 될 만한 위험하거나 생산성에 저해되는 부분들을 언어 차원에서 강제로 제외시키는 경향이 있다면, C언어는 이런 부분을 완전히 개방했다. 초기 C언어는 커다란 규모의 프로그램을 거의 염두에 두지 않고 개발됐다. 당시 IBM 메인 프레임에 사용되던 System/360이 수천 명의 프로그래머가 달라붙어 어셈블리어로 수백 만 줄이었는데, C언어로 만들어진 가장 큰 프로그램인 초창기 유닉스의 커널은 고작 만 줄 정도였다. 상황이 이렇다 보니 같은 프로그램이라도 프로그래머의 지식 수준과 능력에 따라 퀄리티 차이가 그야말로 극과 극으로 벌어지는 언어이다. 리눅스 등 C언어로 작성된 대규모 오픈소스 프로젝트를 보면 C언어 활용의 예술을 볼 수 있다. 방대한 양의 코드를 함수, 구조체, 포인터, 매크로만을 이용해서 철저히 모듈 단위로 잘 관리하고 있다.

어떤 언어든 그 실력을 충분히 발휘하려면 주로 쓰이는 분야에서 사용하게 되는 기술을 익혀야 하는데, C언어의 주 사용 분야라는 것이 하필 기계 제어. 제대로 사용하려면 프로그래머들이 보통 싫어하는 하드웨어와 어셈블리어에도 결국 손을 대야 한다.

일반적인 프로그래밍도 역시 가능하지만, 그런 용도로는 더 적합한 언어들이 널려 있다. 2010년대부터는 데스크톱 애플리케이션보다 웹, 스마트폰 애플리케이션의 활용도가 높아져서, 하드웨어 컨트롤이나 성능이 중요한 분야[2]가 아니면 C언어를 써야 할 이유가 거의 없다. 즉, 초심자가 C언어를 배우는 것까지는 문제가 없지만 배우고 나서 뭔가 제대로 할만한 건 사실상 없다. 그리고, 바로 그렇기 때문에 프로그래밍 입문용 언어의 자리도 해외 기준으로는 대부분 JavaPython으로 대체됐다. 또한 데스크톱 애플리케이션도 HTML/CSS/JavaScript를 이용하는 Electron 프레임워크가 나오면서 예전보다 개발이 훨씬 쉬워졌다.[3]

2.1. 사용 분야

C언어는 다음과 같은 분야에서 사용되며, 일반 사용자의 눈에 보이지 않는 컴퓨터 산업의 기반이 되는 곳에서 주로 쓰인다.

2.2. 등장 배경

C언어 이전에도 고수준 언어들은 많이 존재했지만, 대부분 특정 애플리케이션 영역을 대상으로 하거나, 컴퓨터 과학 이론을 입증하기 위해 만들어진 실험실 언어들이었다.

어플리케이션 영역이 아닌 운영체제를 어셈블리어가 아닌 언어로 작성한다는 것은 당시엔 금기였다. 유닉스의 전신인 MULTICS는 그 금기를 어기고 PL/1라는 고수준 언어로 작성하려고 시도했으나, 불운하게도 성공하지 못했다. 유닉스는 멀틱스에 대한 반성에서 단순하게 만드는 방향을 추구했으며, 그렇기에 이름부터가 UNI-로 시작하도록 지었다. C언어와 유닉스는 소수의 예외를 제외하고 대부분을 C언어로 작성하고서도 우려와는 달리 단점보다 장점이 훨씬 많다는 것을 보이면서 이 금기에 정면으로 도전해서 승리했다.[9] 그리고 지금은 운영체제는 C언어가 아니면 안된다는 새로운 금기가 생겼다. 실질적으로 성능과 효율성을 희생하지 않으면서도 수많은 플랫폼에서의 이식성을 제공하는 언어가 C언어 외에 그리 마땅치가 않기 때문이다.

C언어가 등장하던 당시에는 코볼, 포트란 등이 고수준 언어로써 주로 쓰였는데, 이들은 천공 카드가 쓰이던 시절 만들어져서 문법이 매우 불친절하다.[10][11] C언어는 이런 당대 고수준 언어들에 비해 매우 이해가 가기 쉬운 문법을 사용하여 초보자가 쉽게 접근할 수 있었다.

2.3. 장점

C언어로 짜여진 코드는 속도가 빠르고 바이너리 크기도 작아 속도가 다른 무엇보다 (심지어는 생산성보다도) 중요한 임베디드 혹은 모바일 계열, 또는 시스템 프로그래밍 등에서 주로 쓰인다. Python등의 다른 언어들에 비해 기본으로 포함되는 크기도 작을 뿐더러 같은 알고리즘으로 짜도 결과물의 크기가 더 작은 경향이 있다. 과거에는 메모리 가격이 비쌌던 만큼 메모리를 적게 잡아먹는 프로그래밍 기법을 선호했으며, 화성 탐사선도 이러한 점을 반영하여 C언어로 만든 프로그램을 사용했다.

이러저러한 고급 언어들이 나오는 상황에서도 아직 저수준의 제어를 위해 C언어가 필요한 경우도 많다. 예를 들어, OS를 만든다면 아무리 생산성을 고려한다고 해도 시스템 제어 측면과 OS의 기능 위에서 돌아가는 애플리케이션 때문에라도 속도라는 면은 중요하고[12], 그렇다고 속도를 높이기 위해 어셈블리어나 기계어로만 OS를 짜기에는 생산성이 매우 낮아지니, 타협점으로 C언어를 쓴다. 물론 시스템 콜 인터페이스나 ABI, 인터럽트, 부트 스트랩, 드라이버 등 머신에 직결된 부분에는 어셈블리나 기계어를 사용해야 한다. 아니면 머신 제조업체가 제공하는 라이브러리를 사용하거나. 최근 C언어/C++ 수준의 기계 제어와 안전한 메모리 관리를 동시에 제공하는 Rust라는 언어가 새로 나오기는 했지만[13], C언어와 C++는 원체 레거시가 오랫동안 유지되는 성향이 강해서 Rust가 메이저로 부상하기는 쉽지 않은 상황이다.

또한 대부분의 운영체제가 제공하는 API/시스템 콜은 C언어 기반이므로, 이를 직접 사용하려면 어찌되건 C언어를 래핑하는 방식으로밖에 쓸 수 없다. 그 외에 임베디드 시스템에서 단가 문제로 시스템 처리 능력과 메모리 제한이 매우 심각한 경우가 많은데, 이 경우도 C언어가 그나마 적합하다. 옛 시절 어셈블리어가 차지했던 자리를 현재는 C언어가 차지하고 있다고 봐도 된다. 이렇게 활용되는 부분이 많으므로 당분간 C언어가 사장될 가능성은 없다. 게다가 막대한 분량의 레거시 코드도 있고. 실제로 프로그래밍 언어 점유율 조사에서 한때 자바를 제치고 1위를 차지한 적도 있는 것을 보아서는 당분간 현역으로 왕성하게 활동할 것으로 보인다. 물론 이건 C언어의 점유율이 늘어났다기보다는 타 현대적인 언어들 덕분에 자바의 점유율이 줄어든 것이지만.[14]

안정성보다는 퍼포먼스를 골수까지 극한으로 뽑아내야 하는 게임 프로그래밍 분야 또한 C언어/C++가 대세. 게임 프로그래머들이 C언어에서 C++로 넘어가기를 끝까지 싫어했던 것은 오로지 C언어(지금은 C++)가 다른 언어보다 속도를 빠르게 최적화할 수 있다는 생각 때문이며, 그래서 다른 분야보다 보수적이라는 소리를 듣는 편이다. 그러나 요즘에는 모바일 게임 시장이 급속하게 커지면서, 코어 부분만 C언어/C++로 만들고 그 외의 상당 부분은 Python, Java, C# 등의 고생산성 언어로 대체하는 경우가 늘어나고 있다.

현 시점에서 가장 중요한 장점 중 하나는 사실상 모든 아키텍처와 운영체제에서 가상머신 등의 추가적인 단계 없이 네이티브로 지원하는 언어라는 것이다. 그 다음으로 널리 쓰이는 C++조차도 이 정도로 지원되지는 않는다. 워낙 널리 쓰이다 보니 CPU 디자이너들이 가장 먼저 하는 일은 C언어를 instruction set으로 포팅하는 것일 정도다. 심지어 C언어 설계 자체가 CPU 인스트럭션 설계에 영향을 주는 단계에 이르렀다. 그런 관계로 이식성이 중요한 경우는 대개 C언어를 사용한다. 자바의 멀티플랫폼과는 성격이 다르다. 자바는 각 플랫폼용으로 만들어진 가상머신 위에서 같은 소스가 실행되는 것이고, C언어는 각 시스템에 맞는 기계어로 컴파일되는 것이다. 위에도 언급했던 자바 가상머신 자체가 C언어로 만들어져 있으므로 당연히 자바보다 범위가 넓다. 기존 C언어 프로그래머들은 진정한 멀티 플랫폼 언어는 자바가 아니라 C언어라고 믿는 사람도 부지기수. 표준을 고려하여 주의깊게 작성된 C언어 코드는 C언어 컴파일러가 있는 어떤 플랫폼에서도 최소한의 노력만으로 컴파일 - 실행이 가능하다.[15] 그게 쉽지 않아서 문제지. 요즘 C언어를 사용하는 이유는 위에도 쓰여 있듯이 저수준 제어인데, 이는 플랫폼에서 제공하는 API를 사용하지 않고는 불가능하다. 이런 특성상 런타임의 관점에서 보면 Rust, Python, Java, C++ 등의 사실상 거의 모든 언어의 런타임들부터가 C언어 런타임에 의존하고 있다.

이미 한물 간 언어처럼 보이지만, 여전히 '프로그래밍' 입문으로 C언어를 추천하는 사람이 많다. C언어라는 언어는 매우 심플하면서도 배우는 과정에서 소프트웨어 구성의 최소 단위인 비트부터 시작해서 메모리 관리, 그리고 고급 개념인 OOP 비스무리한 것까지[16] 흉내내면서 소프트웨어 전반을 훑게 되고, C언어를 배우면서 나오는 과제들은 커맨드 라인에서 이미 쓰이고 있는 기본적인 툴을 'reinvent the wheel'[17] 하는 식의 과제들이 많으므로 바닥부터 훑어가며 견문을 넓히는 데 좋다. 실제로 가장 기저에 놓인 OS API[18]는 오늘날 플랫폼을 불문하고 거의 다 C언어로 되어 있고, 그 외에도 대부분의 인프라가 되는 소프트웨어는 C언어로 만들어진 후 타 언어로의 바인딩을 제공하는 식이다. 로우 레벨부터 단계를 높여가며 관찰을 해보면, 머신 코드는 머신에 따라 달라지고, 어셈블리어도 Intel/AT&T 등 문법에 따라 몇 가지 버전이 존재하지만, 그 위쪽에서 결국 C언어로 대통합이 이루어진다. 그리고, C언어 위쪽으로 가면 다시 C++/Java/C#/Objective-C/Python 등으로 다양하게 갈라진다. 즉, 두 개의 원뿔을 꼭짓점끼리 붙여 놓은 형태이며, 저 꼭짓점 부분에 C언어가 존재하는 형태이니, 이것만으로도 C언어의 중요성은 충분히 알 수 있다. 그래서 이런 견문은 실제로 나중에 더 이상 C언어를 쓰지 않고 타 고급 언어로 넘어가더라도 유용한 경우가 많다. (실제로, 많은 수의 언어가 C언어와의 FFI를 제공한다.) 즉 결론만 말하자면 C언어를 배우면 활용도가 매우 높다는 말. C언어만 알아도 같은 절차지향 프로그래밍 언어뿐만 아닌, 객체지향 프로그래밍 언어도 어느정도 이해할 수 있다. 물론 이해했다는건 단편적이고 객체지향에 대해 공부는 조금 더 해야하긴한다.

하드웨어나 컴퓨터 아키텍처를 배우게 된다면, C언어의 특징은 오히려 장점이 된다. Java나 Python 같은 언어들은 일반적인 상황에서 생산성을 높이기는 좋지만 특정한 상황에서 속도를 높이기는 어렵다. 일반적인 개발을 하려면 많은 상황을 처리할 수 있도록 강력하고 복잡하게 만들어야 하기 때문이다. 따라서 고성능이 필요한 특정 목적이 필요할 경우 언어에서 쌓은 추상화의 장벽을 뚫고 저수준(low level) 개념을 이용할 필요가 있는데, 이에 관한 개념을 제대로 이해하려면 처음부터 OS와 기계 제어를 위해 태어난 C언어를 사용하는 것이 가장 효과적이다. 다시 말해, C언어를 공부한다는 말은 곧 하드웨어를 공부한다는 말과 같다고 할 수 있다.

2.4. 단점

성능이라는 대명제에 충실해서 작게는 변수 초기화, 배열 범위 점검, 널 포인터 문제에서부터 크게는 쓰레기 수집(Garbage Collection; GC), 예외 처리 같은 것까지 조금이라도 하드웨어에 오버 헤드가 걸릴 것 같은 기능은 다 무시하기 때문에 주니어 프로그래머에게는 어렵다. 프로그래머가 메모리 관리까지 전부 신경써야 하는 것이다.

현재 쓰이는 고수준 개념들 자체는 의외로 오래된 경우가 많다. 예를 들어서 쓰레기 수집은 1959년에 최초로 구상됐고, 타입 에러를 컴파일 타임에 모두 잡아낼 수 있는 Hindley-Milner 타입 인터페이스가 1970년대 후반에 나와서 Haskell 등지에서 쓰이고 있다. 클로저 개념도 1960년대~70년대에 나왔다. 시간상으로는 지원한다 해도 이상하지 않다. 그럼에도 이런 기능을 C89/C90가 지원하지 않았던 이유는, 당시 언어에 고수준 개념을 구현하기에는 하드웨어의 성능이 절대적으로 떨어졌다는 점이다. C언어가 컴파일러를 거쳐 기계어 파일이 나오면 그걸 다시 사람이 직접 최적화를 해줘야 했다. 당시 컴파일러의 프로그램 최적화 성능이 떨어졌던 이유도 있겠지만, 당시 하드웨어는 초고도로 최적화를 하지 않으면 만족할 만한 속도가 나오지 않았다. 그래서 그 당시에는 프로그래머라면 저런 부분은 알아서 관리해가며 쓰는 것이 기본이었다.

어셈블리어보다 이식성이 좋다고는 하지만, 하드웨어마다 달라지는 부분들을 언어 내에서 컨트롤해서 일관성을 유지하는 것이 아니라, 성능을 위해서 전혀 후처리를 하지 않고 그대로 프로그램에 반영해버리므로 사실 C언어의 이식성도 미신이라는 사람들이 많다. 그대로 때려박는 특성상 컴퓨터 아키텍처가 다르면 똑같게 동작할 리 없다는 것이 이유다. 그나마, C99에서는 컴퓨터 성능이 좀 올라간 것을 반영해서 여러가지 엣지 케이스에 대해 어느 정도 통일성을 만들려고 한 노력이 엿보이긴 한다.

다음은 C언어 사용 시에 어려움을 느끼기 쉬운 부분들이다.
  • 느슨한 타입 검사: 특히 포인터와 관련하여 문제가 생기기 쉽다.
  • 범위를 벗어난 배열 접근: 배열에 접근할 때, 인덱스가 배열 범위를 벗어나도 이를 체크하지 않는다. 이를 이용한 flexible array member와 같은 기능도 존재한다.[19] 문제는 이것이 버퍼 오버플로 취약점이라는 심각한 보안 버그로 이어질 수 있다는 것이다. 물론 프로그래머가 꼼꼼히 체크하면 되긴 하는데 사람이 실수를 하지 않을 수는 없기 때문에...[20][21]
  • 문자와 문자열에 대한 추상화 부재: C에서는 문자에 대해 적절한 추상화를 제공하지 않고 날 것의 숫자 코드 그대로 처리하며, 문자열에 대해서도 string과 같은 타입이 따로 없고 그 대신에 char형 배열을 그대로 사용한다. 언어 자체적으로 이를 간편하게 다루는 기능이 없기 때문에 표준 문자열 함수에 의존해서 처리해야 하며, 초보자들이 잘못 이해하거나 혼란을 느끼기도 쉽고 실수를 하거나 버그를 만들기도 쉽다.[22][23]
  • 문자열 처리 방식상의 취약점: C언어에서는 모든 비트가 0인 널(null, \\0) 문자를 써서 문자열의 마지막을 표현하는데, 실수로 널 문자열을 덮어쓸 경우 경계를 침범하기도 쉽고, 문자열의 길이를 구하려면 매번 길이를 세어 보아야 한다는 문제가 있다.[24]
  • 배열과 포인터의 혼동: C언어의 배열은 문맥 상황에 따라 포인터로 바뀌는 경우가 많고, 따라서 초보자가 느끼기에 무척 혼란스럽고 구분이 어렵다. 심지어 익숙해진 다음에도 버그를 만들기가 쉽다.[25]
  • 네임 스페이스 미지원: C언어는 C++과 달리 네임 스페이스를 여러 단계로 구분하지 않기 때문에, 함수 이름과 전역변수끼리 서로 충돌이 날 가능성이 높아진다. C언어에서 서로 이름이 충돌하지 않게 하는 유일한 방법은 충분히 길게 이름을 짓고 다른 라이브러리와 겹치지 않게 기도하는 것 뿐이다.[26]
  • 각종 형변환 규칙: 정수 승격, 일반 산술 변환, 기본 인자 진급 등 암묵적으로 수행되는 생소한 형변환 규칙들이 학습을 어렵게 만들고 학습 이후에도 프로그래머의 실수를 유도한다.[27]
  • 예상하기 어려운 최적화, 부수 효과(side effect): C언어는 불필요하다고 판단되는 수식을 평가하지 않을 수 있다. 즉, 극단적으로 코드를 변형하거나 제거하여 최적화를 수행할 수 있다. 만약 프로그래머가 코드를 올바르게 작성하지 않는다면 컴파일러는 이를 잘못 판단하여 최적화 과정에서 예상하기 어려운 오동작을 만들어낼 수 있다.
  • bool 타입의 부재: C에서는 정수형이 bool 타입을 대신한다. 0은 false이고, 나머지 다른 것들은 전부 true로 처리한다.[28] 때문에 임의의 char 값이 숫자인지 알아보는 isdigit 함수는 숫자가 아니면 0을 리턴하고 숫자일 경우 보통 1을 리턴하긴 하지만, 0이 아닌 그냥 어떤 수를 리턴하는 컴파일러도 있다. 매뉴얼 페이지에는 보통 non-zero라고 되어 있다. 비슷하게 논리적 부정 연산을 의미하는 !의 경우는 0일 경우 1로, 0이 아닌 다른 모든 값은 0으로 바꾼다.[29]

C언어에는 저런 함정들이 매우 많이 도사리고 있으며, 이러한 것들이 가장 기초적인 단계에서부터 서로 얽혀 등장한다. C언어를 배운다는 것은 사실 '어떻게 프로그램을 만들 것인가'를 배우는 게 아니라, '어떻게 저런 함정을 피해갈 것인가'를 배우는 거라는 사람도 있다.

C언어는 타 언어에 비해 기계에 좀 더 가까운 얇은 추상화를 제공하기 때문에, 사용자가 기계에 대해 더 잘 알고 있어야 하고 예외인 경우도 많으며 그에 비해 안전장치는 굉장히 빈약하다. 따라서 위와 같은 여러 어려운 부분들이 생기기 쉽다.

하지만 반대로 저레벨 제어를 할 때는 변수 타입이나 참조 등에 제약이 적어 다른 언어보다 편하고 효율적인 점도 있다. 예를 들어서 블록 암호화 같이 비트/바이트 단위로 바이너리를 자유자재로 조작해야 하는 코드는 고수준 언어로 짜기 불편하다. 익숙해지면 구조체 같은 사용자 정의 데이터 타입을 이리저리 캐스팅해서 포인터 연산을 활용해 전혀 엉뚱한 데이터로 변환해서 쓰는 것도 가능하다.[30]

사실, 초창기 C언어는 비교적 사용하기 편리한 고수준 언어로 분류됐고 그렇기 때문에 큰 인기를 끌었지만, 오늘날 C언어는 오히려 불편한 언어에 속한다. 그럼에도 불구하고 많이 사용되는 이유 중 하나는 투명성이다. 고수준 언어들의 경우, 하드웨어로부터 거의 완전히 추상화를 시킨 경우가 많기 때문에 프로그램 로직에만 신경 쓰면 된다는 장점이 있지만, 그게 정확히 컴퓨터에서 어떤 식으로 돌아가는지를 예측하기는 그 추상화 수준만큼 힘들어지게 된다. 반면, C언어는 기능 자체가 적고 하드웨어에 맞춤 형태로 최소 한도의 추상화만 시킨 수준이기 때문에, 어셈블리어와의 호환성도 좋고 코딩과 동시에 실제 어떤 식으로 하드웨어가 움직일지 예상하기가 비교적 쉽다.

그렇기 때문에 C언어로 코딩을 한다는 것은 곧 저런 장점을 살리고 싶다는 것이고, 그러려면 결국 컴퓨터 아키텍처에 대한 지식도 필요하며, C언어 자체에 대해서도 아주 디테일한 수준까지 알고 있어야 한다. 이런 측면 때문에 언어 자체가 간결하다고 하여 복잡한 기능들을 많이 제공하는 고수준 언어들에 비해 쉽다고 보긴 힘들다.

이것은 별개의 이야기이며, 어느 정도 사람마다 의견이 갈리기는 하나, 포인터에 대한 이해가 빠르다면 오히려 JAVA같은 객체지향 언어보다 C언어가 편하다는 사람들이 많다. 이는 객체지향의 주 포인트인 추상화, 캡슐화, 상속, 다형성이 C에서 없거나 다른 방식으로 구현되기 때문인데, 아무래도 처음 JAVA 등을 접하는 사람은 그냥 위에서부터 아래로 책 읽듯이 읽으면 되는 프로시저 언어에 비해 이리갔다, 저기갔다, 오버라이딩도 하고, 추상화 메서드도 있고, 인터페이스도 있어서 혼란스러운 JAVA 같은 것보다는 훨씬 보기 수월하기 때문. 물론 처음에 이런 불편함을 느꼈던 초보자들이나 프로시저 언어에 매우 익숙했던 사람들도 점차 적응이 되다보면 C가 생각보다 불편한 언어였다는 것을 알고 메롱메롱 하기도 한다.

2.5. 저수준 언어로서의 C

C언어는 저수준 언어와 고수준 언어의 특성을 둘 다 가진다. 그 중에서 특히 주목받는 것은 저수준 언어로서의 강력함이다. 비록 이식성을 추구하기 위해 어느정도 추상화를 거치긴 했으나, C언어는 정수형 모델, 비트 연산, 공용체, 포인터, 형변환 규칙 등 다양한 부분들에서 기계에 대한 직접적인 접근을 의도하고 있다.

C언어는 처음부터 어셈블리어와 비교할만한 효율을 가지게 저수준으로 설계됐다. K&R의 "C programing language" 책의 초판 서문에서도 C언어를 어셈블리어를 대체하는 이식성 있는 어셈블리어(portable assembler)로 만드는 것을 목표로 설계됐음을 밝히고 있다.[31]

C언어가 주로 쓰이는 곳이 저수준 제어가 필요한 분야이다 보니, 많은 C언어 구현체들이 인라인 어셈블러를 통해 어셈블리 코드를 코드 안에 직접 집어넣을 수 있는 확장 기능을 적극적으로 지원한다. 이런 경우 호환성을 희생하고 속도를 얻을 수 있다.

2.6. 고수준 언어로서의 C

C언어가 저수준에서 가지는 강력함에도 불구하고, C언어는 분명히 고수준 언어이며, 추상성에 대해 아래에서 설명할 매우 강력한 규칙을 가지고 있다. 다만 C언어가 가정하는 추상 기계가 실제 CPU 및 기계와 상당히 가깝게, 기계어로 번역하기가 편하게, 다양한 하드웨어에서 효율적으로 돌아갈 수 있도록 잘 만들어져 있을 뿐이다. 추상층이 얇을 뿐이지 그 얇은 추상층은 소스 코드와 그것이 번역된 실행 파일을 엄격하게 분리한다.

C언어는 소스 코드를 번역할 때, 어떤 추상 기계를 가정하여 그 기계가 의미론적으로 동작하는 원리에 따라 프로그램을 번역하고 생성한다. 의미론적이라는 얘기는 실제 과정이나 동작에 상관 없이 결과만 같으면 된다는 뜻이다.[32] 또한 의미있는 결과나 부작용을 가지지 않는다고 판단하는 부분에 대해서는 그냥 평가하지 않아도(없애버려도) 무방하다.[33][34][35] 이런 강력한 추상성을 통해 C언어가 얻는 것은 최적화의 가능성과 이식성이며, 이는 (C++이 아닌)C로 제작되는 수많은 수십 년 단위의 초대형 프로젝트들이 증명한다.

많은 C입문자들이 C언어가 기계를 직접적으로 1:1 제어하는, 일종의 이식성을 가진 어셈블러(Portable Assembler)이기를 기대한다. 그러나 현실적으로 C언어는 절대, 전혀, 이식성 있는 어셈블리어라고 할 수가 없다. 왜냐하면 기계와 직접적으로 상호 작용하지 않기 때문이다. 순수한 C언어 문법만으로는 CPU 레지스터, CPU 캐시, I/O 포트, 페이지 테이블, 스택 프레임, 버스, USB, RAM 등 하드웨어와 어떤 방식으로도 직접적으로 접근할 수 없다. 한편으로 위에서 설명한 바와 같이 C언어 소스 코드는 1:1로 그대로 기계어로 치환되지도 않으며, 오히려 의미론적으로 해석되어 최적화를 거친 채 번역되는 것이 보통이다.

이러한 특징은 오히려 이식성에 큰 영향을 끼쳤다고 볼 수도 있는데 하드웨어에 직접 접근하지 못하므로 하드웨어에 의존적인 기능들은 사용하지 않게 됐고 구현체의 추상화 레이어가 기계별로 다른 동작을 일관화시켰기 때문이다. 다만 이 점은 여러 컴파일러가 성능 경쟁을 하고 하드웨어 발전 속도가 빨라진 지금에는 그리 타당성 있다고 볼 수는 없다.

2.7. 이식성

C언어의 이식성은 java나 기타 다른 좀 더 최근에 만들어진 언어들의 이식성과는 방향이 다르다. 좀 더 최근에 만들어진 언어들은 대체로 엄밀한 정의를 통해 동일한 코드가 동일한 동작을 보이는 쪽을 선호하며, 이를 위한 약간의 낭비나 비효율성은 감수하는 편이다.

반면에 C언어는 효율성을 해치지 않는 범위 내에서의 이식성을 추구한다. C언어가 만들어지고 널리 퍼지던 70~80년대에는 지금보다 더 다양한 특성을 가진 다양한 하드웨어들이 범람하고 있었으며, 그 성능 또한 일체의 낭비를 허용하지 못할 정도로 충분하지가 못했다. 따라서 어떤 최소한의 공통 부분을 제시하되 그 위에 각 환경에 맞는 자율성을 컴파일러 제작자들에게 보장해야 했다.

C언어 표준의 이식성과 관련된 부분들은 크게 3단계로 구분된다. 동작의 내용을 분명하게 명시해야 하는 것(implementation-defined), 동작을 보장하되 그 내용을 명시할 필요가 없는 것(unspecified)', 동작을 보장할 필요가 없는 것(undefined). 이러한 것들을 표준 문서에서는 Portability issues란 이름의 부록으로 따로 정리해 두고 있다. 컴파일러 제작자는 이렇게 제시된 선택의 범위 내에서 컴파일러를 만들어야 하고, 프로그래머는 이러한 선택과 가능성들을 고려하여 코드를 작성해야 한다.

이식성 문제는 다른 환경/기계로의 이식 뿐만 아니라, 잘못된 최적화로 인한 성능저하 및 오동작에도 큰 영향을 미친다. C언어는 불필요하다고 판단되면 코드의 일부를 변형하거나 제거할 수 있는데, 동작을 보장하지 않는 경우(undefined)의 경우에는 이러한 '판단'이 실제 하드웨어의 동작방식이나 프로그래머의 기대에 맞추어 동작하지 않는 경우가 많다. 그 결과 컴파일러의 정상적인 최적화가 프로그래머의 잘못된 가정과 맞물려 오동작과 보안오류를 만들 수 있다.[36][37] 이러한 오동작은 원인이 되는 부분에서 발생하는 것이 아니라 원인이 되는 부분이 정상적으로 동작할 것이라고 상식적으로 판단하여 최적화를 진행하는 전혀 엉뚱한 부분에서 날 수도 있기에 이를 찾고 대처하기가 까다롭다.

따라서 C언어 추상머신의 규칙과 의도를 올바로 이해하지 않고 실제 기계의 동작과 구현에만 의존하여 판단하는 것은 위험하며, 코드를 이식할 필요가 없는 프로그래머들조차도 이식성 문제에 주의를 기울여야 한다.

2.8. 점유율과 플랫폼별 지원 현황

오랜 시간동안 Java와 함께 몇 년째 1, 2위를 다투고 있었다.출처[38] 그 이외의 언어와는 넘사벽의 비율을 보여준다. 그야말로 부동의 원투 펀치. 다른 언어들이 3위 경쟁을 하는 동안 C언어와 Java가 양대 산맥을 형성하고 있다. 좀 더 넓게 C언어 계열(C++, C# 등 C언어 문법 혹은 그와 매우 유사한 문법 체계를 사용하는 언어)와 JAVA 계열(Arduino 등 JAVA 문법 혹은 그와 매우 유사한 문법 체계를 사용하는 언어)로 보자면 C언어 계열이 단연코 확고한 1위. (C언어와 C++, C#만 합쳐도 1/3이다. 거기에 자바와 파이썬의 점유율을 합치면 거의 절반.)

가장 널리 쓰이는 PC 플랫폼인 윈도우에서는 MSVC 2019 16.8 버전 이후부터 적극 지원 중이다. 사실 그 이전 Visual Studio는 VS개발진 본인들도 반쯤은 버려진 언어임을 인정하기도 했다.[39] MS에서는 C언어를 Internal language로 규정하여 내부적으로 윈도우와 기타 MS 상품들을 만드는 데는 사용하지만, C언어 프로그래밍 환경을 사용자에게 정식으로 제공하지는 않았다. 덕분에 윈도우가 자랑하는 Visual Studio에도 C언어 프로젝트 항목은 없으며, 정식으로 C11/C17을 지원하는 현재(VS 2019/VS 2022)도 기본 사양에는 C 프로젝트 템플릿이 포함되어 있지 않다. (C++ 프로젝트를 선택하여 소스 파일 확장자를 .c로 바꿔주거나 C언어로 컴파일한다고 프로젝트 옵션을 설정해야 한다.) 게다가, 기존에는 그런 식으로 사용을 하더라도 MS의 C언어 지원은 순수하게 C++에 묻어가는 정도라, 새로운 ISO 스탠다드인 C99/C11의 기능들도 거의 지원하지 않았다. 다행히 MSVC 2019 16.8부터는 정식으로 C11/C17을 지원하게 됐으므로 이제는 Visual Studio를 통해 최신 C언어를 경험할 수 있게 됐다.

리눅스의 경우에는 GCC라는 사실상의 오픈소스 표준 컴파일러 덕분에 지원이 괜찮으며, Unix-like 운영체제라는 버프도 있고,[40] C언어를 배우고 여러가지 시험해보면서 놀기에 적합한 환경을 제공해준다. 윈도우와 다르게 커널부터 오픈소스로 개발되고, 이 커널이 C언어로 만들어져 있어 C언어의 사용도 활발하다. 이쪽 프로그래머들은 개발 환경을 vim이나 Emacs로 사용하는 사람들이 많이 있다.

macOS(구 OS X)는 신생 컴파일러인 LLVM/Clang[41]을 사용하며, 역시 지원은 좋은 편이다. 이는 플랫폼 메인 개발 언어를 Objective-C로 잡았기 때문인데, Objective-C는 C언어와 완전히 호환이 되기 때문에 달랑 Objective-C만 지원해도 C언어가 완전히 지원되는 셈. 새로운 스탠다드의 적용도 세 플랫폼 중 가장 빠르다. MS는 위에서 이야기했듯이 C++의 subset인 부분에 한해서만 C언어의 최신 표준을 지원하고, GCC와 LLVM/Clang은 C11을 모두 지원한다.

C언어를 표준으로 공부하려면 현 시점에서는 성능이 우수하고 표준을 엄격하게 따르는 GCCLLVM/Clang을 사용하는 편이 좋다. 그러나 MSVC 2019 이전의 Visual Studio는 사용하지 않는 것이 좋다. 구버전 Visual Studio에 통합되어 있는 MSVC 컴파일러는 C99의 가변 길이 배열조차 지원하지 않을 만큼 C언어 지원이 부족했기 때문이다. MSVC 2019 16.8 이후에는 Visual Studio도 제대로 C11/C17을 지원하므로 윈도우 사용자라면 MSVC 2019 이상을 사용하는 것이 좋다. 만약 Visual Studio를 사용하지 않을 윈도우 사용자라 하더라도 MinGW와 같이 WSL와 달리 GCC/Clang을 사용해서 Win32 네이티브 바이너리를 컴파일하는 환경도 제공하고 있다. 특히 GCC, LLVM/Clang은 -Wall -Wextra -Werror 컴파일 옵션[42]을 넣으면 사소한 경고 사항도 전부 에러로 변환하여 컴파일을 중단시키므로 코드의 품질을 잡는 데 많은 도움이 된다.

====# MSVC 2019 이전까지 마이크로소프트의 C언어 지원 수준 #====
아래의 서술은 구버전 MSVC에서의 문제이다. MSVC 2019 (16.8) 이상은 C11/C17 표준을 완전하게 지원하며 MSVC 2019 (16.8) 이후에는 적용되지 않는 내용이니 주의를 바란다.

MSVC 2019 이전까지의 버젼에서, C는 사실상 반쯤 버려진 언어라 MS C++ 컴파일러로 컴파일한 코드에 CRT를 억지로 끼워 맞추는 수준에 가까웠다. 1999년에 확정된 C99마저 제대로 지원하지 않는 것은 MS의 개발 정책 때문이었다. 단순히 비표준 확장을 집어 넣어서 문제가 되는 것이 아니라, 표준의 범위 내에서도 서로 충돌이 나는 것이 문제라서 꽤 골치 아픈 문제였다. 표준과 MS 제품과의 차이점을 정확히 알고, 이중으로 코드를 작성해야 했으니까...

국내 교재나 대학의 경우, 수강자가 친숙하고, 또한 예제를 따라하기 비교적 훨씬 쉬운 윈도우 + Visual Studio 기준으로 설명하는 경우가 많다. 그러나 Visual Studio (MSVC) 컴파일러는 ANSI C를 기반으로 상당히 많은 비표준 확장을 제공하고 있으며, 표준에 어긋나 에러를 일으켜야 할 문장도 MSVC는 어떻게든 실행을 해 버리기 때문에 초보자에게는 오히려 독이 됐다. GCC 역시 비표준 확장을 많이 제공하지만, 이후의 표준인 C99, C11에서 GCC의 비표준 확장이 대부분 표준화돼서 비교적 나은 편이다.

따라서 한동안은 웬만하면 GCC/Clang환경에서 표준으로 먼저 배우고 MSVC 등의 비표준 기능은 차후에 문서를 보고 따로 익히거나, 꼭 MSVC로 배우고 싶다면 표준과 비표준, 나아가서 가능하면 C89/C99의 기능을 구분해서 설명하는 교재로 배우는 것이 권장됐다.

MSVC의 비표준 문법의 문제는 C언어뿐만이 아니라 C++에서도 나타나는데, C++가 아닌 MSVC 방언이라고 불릴 만큼 마이크로소프트는 표준 C++를 무시하고 있었다. GCC나 Clang에서 오류를 내는 익명 클래스의 모호한 상속, 표준 라이브러리의 fstream wstring 오버 라이드 등 수많은 문제를 만날 수 있었다.

최근 들어서는 MS도 이 문제를 인지하고 점차 개선하는 중이지만, 여전히 표준 준수에 대해서는 GCC, Clang에 비해 갈 길이 멀다. __cplusplus 매크로와 같은 경우에도 Visual Studio 2017에 와서야 해결이 된 만큼, 구형 MSVC에서 컴파일 되는 코드들이 (non-secure 함수를 제외하고서도) 클래스 또는 템플릿에서 오류를 내는 경우가 잦다.

2.9. 다른 프로그래밍 언어에 미친 영향

  • {...}을 이용한 블럭 (ALGOL/PASCAL 스타일의 begin … end 보다 간결하다.[43])
  • '대입'을 뜻하는 연산자를 '='로, '동일함'을 뜻하는 연산 기호를 '=='로 사용한다. 농담 좀 섞어서, 초심자의 C언어 컴파일 오류의 90%는 여기서 나온다.[44]
  • '다르다'를 뜻하는 연산 기호를 !=로 사용한다.
  • '또는'과 '그리고'를 ||와 &&로 사용한다.
  • +=, -=, *=, /=등의 직관적인 복합 연산자를 지원한다.
  • ++ 와 \-\- 라는 단항 연산자를 사용한다. **와 //는[45] 다른 뜻을 가지고 있다. **은 이중 포인터이고, //은 주석이다.
  • 그 외에 if, for, while 등 많은 예약어의 사용 방식.

어떤 의미에서는 프로그래밍 언어의 라틴어[46]/한자라고 할 수도 있을지도 모른다. 현재 많은 주요 언어에서 { }를 이용한 블럭 표기나 C언어에서 쓰이는 표현식(==, ||, &&), 예약어(if, while) 등을 채택해서 사용하고 있다. 따라서 다른 언어를 배울 때 C언어를 먼저 배웠다면 친숙하게 느껴질 것이다.[47]

추후 C++로 발전됐으며, C++에서는 OOP 기능을 지원한다. 다만, C언어로 OOP를 구현할 수 없는 것은 아닌데, 객체 지향은 개념일 뿐이며 C언어로도 그 개념을 구현할 수 있다. 일례로, 당장 C 표준의 일부인 파일 I/O는 객체 지향[48]이며, Win32 API나 리눅스의 VFS(가상 파일 시스템)도 이처럼 '객체지향적'으로 코딩되어 있다. 다만, 언어 차원에서 지원이 없기 때문에 군더더기가 늘어날 수 있다는 점은 감안해야 한다.

JavaC#, Objective-C 등 여러 언어들의 모태가 된다. 때문에 C언어를 기초로 만들어진 언어들을 흔히 C-like Language[49]라고 부른다. 그런 이유로 C언어를 제대로 익히고 나면 C-like 언어들은 쉽고 빠르게 익힐 수 있다. 단, 위에서 이미 언급했지만 C언어 자체는 엄청나게 어렵다. 그 대신 C언어나 C언어를 모태로 한 언어를 공부하면 자연스럽게 컴퓨터와 프로그램의 작동 방식에 대한 기초 지식을 습득할 수 있어 다른 언어나 프로그래밍 관련 스터디를 할 때 도움이 된다.

2.9.1. C++와의 관계

C++의 시작은 C with Class였으나, 그 이후 수십년간의 변화는 C언어와 C++의 공통적인 부분에서도 차이점을 만들기 시작했다. 쉽게 말해 C언어로 작성된 소스 코드를 그대로 복사하여 C++ 코드에 옮겨 붙인 뒤 컴파일하면 문제가 발생할 수 있다는 것이다. 두 언어의 근본적인 정체성과 지향점 때문에 발생하는 차이인지라, 이러한 차이점들은 오히려 늘어날 것이다.[50]

참고로, Objective-C(Objective-C++이 아니다!)의 경우에는 C++와 달리 C언어를 완전히 포함한다. 즉 Objective-C는 C언어의 완전한 상위 집합(superset)이다.

2.10. 대한민국에서의 위상

대한민국에선 많은 곳이 C언어로 공부를 시작하며 나머지는 C++, Java, C#, 비주얼 베이직, 어도비 플래시(액션스크립트) 등이 차지하고 있다. 즉 입문용 언어로서는 거의 독점에 가까운 위치를 점유하고 있다.

이는 각 대학 혹은 학원들의 커리큘럼 탓이 가장 크다고 볼 수 있는데, 이 때문에 아직도 '자바 먼저' vs 'C언어 먼저'의 떡밥은 개발자들 사이에서 좋은 키보드 배틀 거리가 되고 있다. 그러나 후술할 내용처럼 C언어가 미친 영향은 Java를 포함해서 매우 광범위한 지라 어떻게 해도 결국 C언어가 맨 앞에 선다. 게다가 하드웨어와 메모리를 직접 다룰 수 있다는 특성상, 프로그래밍뿐 아니라 컴퓨터 자체를 이해하는 데에도 상당히 도움이 된다. 이러한 이유로 대한민국의 대부분 컴퓨터공학과 학부 과정에서는 C언어로 1학년을 보낸다. 다만 컴퓨터의 구조를 굳이 깊이 파고들 필요가 없는 기타 이공계 전공이라면 오히려 불필요할 수 있기 때문에 학과 특성이나 교수 재량에 따라 다른 언어를 먼저 배우기도 한다. 다만 현재는 이런 커리큘럼에 대한 반발도 많은데, 이유는 후술.

입문용이라는 인식과 달리 C언어 자체의 난이도는 위에서 봤듯이 무시할 게 못 된다. 저수준 언어인데다 컴파일 언어라서 초심자 입장에선 접근성이 떨어지고 진입장벽이 있으며, 포인터로 들어가면 어느 정도 논리적/수학적 사고능력까지 요구된다. 그럼에도 C언어를 먼저 권유하는 이유는 간단하다. 컴퓨터 아키텍처 및 시스템 프로그래밍, OS를 배우기 위해선 C언어(+ 어셈블리어 조금)만한 게 없기 때문이다. Java, Python 등의 고수준 언어는 추상화 레이어가 여러 OS 및 아키텍처 개념들을 가리고 있으며, C++는 다양한 멀티 패러다임을 언어에 집어넣느라 복잡한 문법이 많아지고 Raw pointer 접근을 하지 않는 쪽으로 발전하고 있다 보니 low-level한 직관을 얻기 힘들기 때문이다.

쉽게 말하자면 다른 언어들은 개발자가 쓰기 쉽게 '인간 입장에서 편한 방향으로' 편의성이 발달하다보니, 너무 편리한 도구에 물들어 근본적으로 컴퓨터가 어떻게 로직을 굴리는지를 확인하기 어렵다.[51]

2.10.1. 입문용 언어?

C언어는 분명 중요하고, 그 의미가 큰 언어임은 사실이다. 다만 교육적인 측면과 실용적인 측면은 입장이 다를 수밖에 없는데, 오히려 온갖 편의성과 강력한 확장성으로 무장한 C++, C#, Java, Python 등이 실무 업계를 차지하고 있다. C언어는 '컴퓨터를 이해하기 좋다'는 것 외에는 사실상 실전성을 상실한 언어이며, 2024년 기준 실전성이 거의 제로에 수렴한 언어에 교육이라는 명목으로 쓸데없이 매달린다는 비판 또한 존재한다. 이는 정말 학술적인 목적으로 진학하는 곳이던 과거와 달리, 취업을 위한 중간과정으로 변모한 탓에 '효율적인' 커리큘럼이 중요해진 현대 대학교의 특성 또한 큰 이유가 된다.[52] C가 하던 일은 현재는 C++과 C#이 대부분 대체했고, C가 다른 언어와 비교되는 강력한 차별점이었던 포인터조차 현재는 그 장점보다 단점이 더 치명적인데다, 포인터의 개념 자체는 알아둘 필요가 있다 하더라도 어차피 이론적인 내용은 굳이 C언어를 안해도 얼마든지 학습이 가능하다.[53]

더욱이 개발자라는 직업 자체가 전공자들만의 리그였던 과거와 달리 비전공자들도 프로그래밍을 익혀서 업계에 뛰어들 수 있게 된 현재는 C언어의 필요성이 더욱 희석되었다. '전공자니까', '기본으로 알아야 된다'라는 구실이라도 있는 전공자와 달리 정말 기술만을 배우기 위해 프로그래밍을 배우는 전문대생이나 비전공자들은 C언어가 전혀 필요가 없다.[54] 상술되어있는대로 C언어를 알고서 다른 언어를 배우면 더 쉽게 배울 수 있는 것은 사실이나, 겨우 그런 이유로 실무에서 쓰지도 않을 언어에 시간을 낭비할 바에 그 다른 언어에 조금 더 공부를 투자하면 될 일이다.

현실의 언어 중 라틴어와 비슷하다고 보면 이해가 쉽다. 라틴어는 현대 인류가 사용하는 상당수 언어에 영향을 끼쳤고 역사적으로 언어적으로 매우 위상이 높은 언어지만, 현 시점에서 라틴어는 아예 죽은 언어라서 실용성도 없고, 라틴어를 몰라도 영어나 기타 유럽권 언어를 공부하는 데에는 전혀 지장이 없다. C언어 입문론은 사실상 영어를 배우려면 라틴어부터 마스터해야 한다고 주장하는 것과 같은 꼴.

다만 주의할 점은, 여기서 이야기하는 "실전성"이니 "실무"니 하는 것들은 웹 프로그래밍 같은 고수준의 이야기다.[55] 사용분야 단락만 봐도 추상화 수준이 떨어지는, 기계에 가까운 분야에서는 여전히 쓰일 때가 있기 때문에, 그 쪽으로 진입하려 한다면 C 언어를 알아야 한다.[56]

그리고 C 언어를 배워두면 컴퓨터를 제대로 이해하는데 도움이 되므로, 정말 제대로 컴퓨터를 공부하고자 한다면 C 언어를 입문용으로 배우는 것도 결코 나쁜 선택이 아니다. 비전공자와 전공자를 가르는 핵심 요소 중 하나가 "컴퓨터에 대한 이해" 이다.[57] 개발자란게 결국 "CPU 조작하는 사람", 더 자세히는 "CPU 에 입력할 명령어 덩어리(알고리즘)을 고안하거나 수정하는 사람" 인데, 정작 CPU가 무엇이며 어떻게 굴러가는지를 제대로 모르고 있으면 당연히 한계가 있다.[58] 물론 C 를 배우면 도움이 된다는 거지, C를 모른다고 cpu 나 컴퓨터 구조를 못 배우는 건 또 아니므로,[59] 결국 입문용으로 C를 쓰냐 아니냐는 선택의 영역이긴 하다.



[1] 좋게 말하면 프로그래밍 스크립트 제작에 제한이 거의 없다는 뜻이 되지만, 다르게 말하면 스크립트에 에러나 버그 등의 문제가 여럿 나타날 수 있다는 확률이 다른 언어들에 비해 상대적으로 높다는 얘기다.[2] 단순히 고성능 라이브러리를 가져다 쓰는 게 아닌 고성능이 필요한 라이브러리 그 자체[3] 현재 Electron은 마이크로소프트에게 인수됐으며, 스카이프Visual Studio Code의 개발에 요긴하게 사용됐다.[4] 윈도우, 리눅스, 유닉스 커널의 핵심부는 모두 C언어로 짜여 있다.[5] 리눅스 외의 커널에서는 C++도 사용되곤 한다.[6] 비트 수준 연산을 많이 사용하고, 속도도 빨라야 한다.[7] PLC와 연계해서 사용하기도 하며, 주로 비전쪽에 사용한다. 필요에 따라 고수준 언어를 사용하기도 한다.[8] 특히나 양자역학 등 과학 계산 용도. 쓸모 있는 데이터 하나를 얻기 위해 엄청난 연산량을 요구하는 이런 분야에서는 계산 하나하나의 사소한 퍼포먼스 저하가 연구 성과를 심하면 문자 그대로 몇 년씩 늦추므로 극한의 퍼포먼스가 필요하다.[9] 유닉스는 처음에는 어셈블리어로 만들어졌지만 점차 C언어로 대체됐다. 그리고 그 덕분에 이식성을 확보하여 여러 기계로 퍼져나갔다.[10] 천공 카드의 잔재로는 제일 왼쪽 몇 칸은 주석, 그 다음 몇 칸은 정의하는 식으로 코딩할 때 칸까지 맞추어야 하는 규칙이 있다. 당시 프로그래밍 자료를 보면 요즘 사람들의 눈에는 모눈 원고지로밖에 보이지 않을 것이다.[11] 그리고 코볼은 원래는 프로그램 코드가 업무 서류로도 사용이 가능하도록(!) 설계가 되어 있어서 그렇다. 주석이 코드고 코드가 주석일 경우의 아주 나쁜 사례. 2000년대에 애자일 프로그래밍에서 코드를 쉽게 읽을 수 있게 한다는 개념과는 다르다. 이쪽은 추상화로 마치 자연어처럼 읽히는 코드를 써서 주석 자체를 코드로 최대한 대체해서 주석이 코드의 변화를 못 따라가는 불상사를 방지하는 것이다.[12] 쉽게 말해서, OS의 특정 기능이 느리면 그 기능을 사용하는 애플리케이션 전부가 덩달아 느려진다.[13] unsafe 콘텍스트로 C언어가 할 수 있는 모든 것을 동일한 성능으로 할 수 있다.[14] 하지만 2020년 5월 기준 C언어가 다시 자바를 누르고 1위를 차지했는데, C언어가 최근 1년간 점유율이 2.82%나 증가해서 나온 결과다.[15] 다만, C언어 표준 자체가 많은 부분을 '모든 컴파일러에 동일하게'가 아니라 구현체에 따라(implementation-dependent) 정의하게 하므로 이런 부분들에 대해서는 전부 각 플랫폼마다의 특성을 따로 반영하여 코딩해주어야 한다.[16] 실제로 요즘 나오는 C언어 교재 중에는 후반부에 OOP 챕터도 넣어 놓은 경우가 가끔 있다.[17] 직역하면 '바퀴의 재발명'이다. 이미 다 존재해서 가져다 쓰기만 하면 되는 것을 괜히 고민하면서 또 만들어내는 것을 비판하는 데 쓰이는 문구이다. 다만 실전 개발에서 안 좋은 습성이고, 교육용으로는 일부러라도 한 번씩은 거치게 하는 편이다. 재발명하면서 기반 시스템의 구조에 대해서 알아갈 수 있다는 것이 이유다.[18] 유닉스의 POSIX API, 윈도우의 Windows API[19] 원래는 zero-sized array라는 일종의 테크닉이었는데, C99에서 정식으로 편입됐다.[20] 버퍼 오버플로 취약점을 이용하면 배열 경계를 넘겨서 그 위에 선언한 변수들을 하나하나 덮어쓰기가 되고, 최종적으로 리턴 주소(함수의 실행을 끝낸 후 다시 함수를 호출한 곳으로 돌아가는 주소)까지 덮어쓰는 게 가능해진다. 이를 응용하여 기존 스택 프레임에 바이트를 조작하여 실행 가능한 기계어 코드를 주입하고, 반환 주소에 주입한 코드의 처음 주소를 가리키게 하여 공격자가 인위적으로 만들어낸 코드를 실행시키는 공격 기법을 코드 인젝션(Code Injection)이라 한다. 기본적으로 이 취약점을 가진 프로그램을 네트워크에 물리면 원하는 동작을 원격에서 실행할 수 있게 되며, 최악의 경우는 루트 권한으로 실행되는 프로그램에서 리턴 어드레스를 셸을 실행하는 코드가 있는 곳으로 덮어쓰면 루트 계정을 원격으로 탈취당할 수도 있다. 이를 버퍼 오버플로 취약점이라고 한다. 1998년 전 세계 서버를 감염시킨 모리스 웜도, finger라는 유닉스 유틸리티의 버퍼 오버플로 취약점을 이용했다. 이 웜은 쉽게 들통나지 않도록 여러가지 위장 전략이 많이 포함되어 있었고, 코넬 대학은 결국 서버를 다운시키는 등의 조치를 취했지만 전 세계로 퍼져나갔다. 당시 이 웜을 만든 23세의 모리스는 벨 연구소에서 유닉스의 로그인 암호화를 담당했던 사람의 아들이었고, 현재 MIT 대학의 교수다. 2001년 한국에도 사이버 대란을 일으킨 코드 레드 의 경우 전세계의 서버 몇 십만 개를 감염시킨 사례가 있다. 이 취약점 때문에 경계 체크를 안 하는 gets 같은 표준 함수 여러 개를 deprecated(사용 자제) 처리해야 했고, printf의 포맷 문자(\\n) 하나가 날아갔다. 심지어 운영체제 자체에서 버퍼 오버플로 방지 메커니즘을 제공하기까지 하는데도 간간히 뚫리는 게 이 취약점이다. 물론 공돌이들이 손 놓고 있는 게 아니니 방지책도 많이 마련되어 있긴 하지만 어쨌든 뚫릴 취약점은 뚫린다. 2014년 OpenSSL에 생긴 재앙적인 버그인 하트블리드도 근본적으로는 C언어가 배열의 경계를 체크하지 않는다는 점에서 생겼다.[21] visual studio 2012 버전부터 함수에 _s 가 붙은 (printf_s, scanf_s 등) 보안을 위한 함수가 추가됐다. 기존의 printf 같은 함수는 그냥 쓸 수 있지만 scanf와 같은 함수를 쓰려고 하면 보안에 따른 에러 메시지가 뜬다. Visual Studio에서는 _CRT_SECURE_NO_WARNINGS를 전처리기에 선언하라고 안내한다. 그리고 이 안전 함수들은 C11에도 추가됐다.[22] 예를 들어 대문자 판별을 위해 c >= 'A' && c <= 'Z' 와 같이 쓰는 경우가 많은데, 이는 EBCDIC 코드와 같이 A부터 Z까지 연속이 아닌 문자셋에서 문제가 발생한다. 따라서 isupper()와 같은 함수로 판별하는 것이 바람직하다.[23] 특히 주로 문자열 상수(String literal. "str" 같은 따옴표로 둘러싸인 부분을 뜻함.)에 대해 잘못 이해하는 경우가 많은데, 문자열 상수 자체는 '문자열'이나 '값'이 아닌 '배열'이며, 결과적으로는 해당 배열의 첫 번째 글자가 저장된 위치를 가리키는 '주소 값'이 된다. 이걸 제대로 다루기 위해서는 악명높은 포인터까지 완전히 배워야 하기 때문에, 언어 난이도가 급상승한다.[24] 비교삼아 다른 언어의 문자열 처리 방식을 살펴보자면, 파스칼과 같은 언어는 문자열의 길이를 맨 앞에 따로 저장하여 처리한다.#[25] 덕분에 교재마다 접근 방법이 달라지는데, 어떤 교재에서는 '비슷하다'고 전제한 뒤 차이점을 설명하는 식이고, 어떤 교재에서는 '다르다'고 전제한 뒤 공통점을 설명하는 식이다. 어느 쪽으로 접근하더라도 함정과 예외가 상당히 많아지기 때문에, 둘의 차이를 충분히 구분할 만한 상황적 경험이 적을 수밖에 없는 초심자들에게 결국은 이해보다 암기로 흘러가고, 어렵게 느끼는 경우가 많다.[26] 소스 수준에서 play_sound라는 함수를 어디엔가 정의를 해 놓고 다른 소스 파일에서는 play_sound라는 이름만 같은 다른 함수를 만들고 같이 컴파일하면 에러가 난다. 더욱 골때리는 건, 컴파일 단계는 무사히 진행되는데 링커 수준에서 에러가 난다는 것. 따라서, 외부 소스 파일로 노출시키고 싶지 않은 내부 함수들은 static 지시자를 앞에 붙여줘서 이름 유출을 방지하고 다른 소스에서 쓸만한 함수는 <모듈 이름>_함수 이름(e.g. NamuWiki_add_document)을 사용하여 이름 충돌을 최소화 하는 게 일반적이다. 이는 사실 장점이자 단점인데, C++에서는 컴파일된 심볼명에 개별 지시자를 넣어 다른 네임 스페이스나 클래스의 함수 등이 충돌이 일어나지 않으나, 문제는 C++ 표준에는 이 Naming Mangling에 대한 표준이 정의되어 있지가 않아 컴파일러마다 호환이 되지 않는다는 점이다. 그러나 C언어는 이러한 네이밍을 하지 않기 때문에 컴파일러 호환성 유지가 가능한 한 가지 방법이며, 이 경우 C++로 구현되어 있으나 외부 API를 제공하는 경우 C언어형 네이밍 스킴을 사용한다.[27] 특히 정수형 계산 관련하여 골치 아픈 사례들이 많은데, 대표적으로는 부호형(signed)과 무부호형(unsigned)이 만나 음수가 양수로 변하는 마술이 있다.[28] 음수도 true로 처리된단 뜻이다. 없으면 define 문으로 선언해서 쓰면 된다.[29] 다만, C99에서 _Bool 타입이 생기긴 했다. stdbool.h를 포함할 경우, 그냥 bool로 사용도 가능하긴 하다. 왜 그냥 bool로 안 넣었냐면, 기존 프로그램 중에 bool 이름을 사용한 코드가 있을 가능성이 있기 때문. 반면, 언더 스코어(_) 뒤에 대문자가 오는 이름은 C언어 표준에서 사용하지 말라고 먼저 명시해놨기 때문에 이것을 사용한 _Bool이 됐다.[30] 소켓 프로그래밍이 보통 이렇다. 그리고 사실 구조체, 배열, 공용체는 다른 타입과의 포함 관계라서 문법적으로 같은 주소/다른 타입이 가능한 경우이기도 하다.[31] 서문 내용: C is a relatively "low level" language. This characterization is not pejorative; it simply means that C deals with the same sort of objects that most computers do, namely characters, numbers, and addresses. ......... Again, because the language reflects the capabilities of current computers, C programs tend to be efficient enough that there is no compulsion to write assembly language instead. ......... Although C matches the capabilities of many computers, it is independent of any particular machine architecture, and so with a little care it is easy to write "portable" programs...[32] 예를 들어 1부터 100까지 루프를 통해 합을 구할 때, 그냥 5050을 대입해도 상관없다.[33] 예를 들어 시간 지연을 위해 집어넣은 루프를 없애 효과가 없는 경우가 발생한다.[34] 또 다른 예로는, if (signed_i+1<signed_i) printf("OVERFLOW!"); 같은 오버플로우 검출 코드는 항상 틀린 것으로 간주하여 그냥 없애버릴 수 있다.[35] 해당 규칙은 함수 호출이나 volatile인 대상체에 대한 접근 포함이다. 따라서 volatile 키워드를 사용했다고 해서 최적화를 항상 막을 수는 없다.[36] 컴파일러의 판단 기준은 C언어 표준의 규칙과 그로 인한 추상머신의 동작이다. 어떤 정의되지 않은 동작이 특정 하드웨어에서는 정상적인 동작을 가진다 해도, 컴파일러는 그런 상황 자체가 없을 것이라 가정하고 최적화를 진행할 수 있다.[37] 대표적으로는 부호있는 정수형의 오버플로와 쉬프트 연산 등이 있다. 예를 들어 if (signed_i+1<signed_i) f(); 과 같은 코드를 컴파일러는 항상 거짓이라 판단하여 지워버릴 수 있다. 부호있는 정수형의 오버플로가 나머지 연산으로 정상동작하는 환경에서조차도.[38] 현재는 Python이 1위를 차지하고 C언어는 2위, Java는 3위로 밀려났다. 4위와 5위는 각각 C언어의 파생언어(라고는 하지만 C언어와는 용도가 전혀 다르다.)인 C++C#이다.[39] "For many years Visual Studio has only supported C to the extent of it being required for C++". 링크 참고.[40] C언어는 애초에 유닉스 운영체제를 만들기 위해 탄생한 언어이기 때문에 유닉스 운영체제와 시스템 궁합이 좋은 편이다.[41] 사실, Clang은 GCC를 대체하는 것이 목표기 때문에 macOS 외에 FreeBSD 등의 유닉스 계열 운영체제에도 사용된다.[42] MSVC의 경우 /Wall /WX /Za /permissive-[43] 베릴로그 HDL에서도 블럭을 이렇게 묶는다.[44] 때문에 대부분 언어에서는 if에 bool 값이 아닌 int 값 등이 들어오면 에러를 내며, 일부 언어에서는 if 같이 비교 연산이 필요한 곳에서 대입 연산을 쓰면 컴파일 에러를 낸다.[45] 1을 곱하거나 나눠서 달라지는 게 없다.. 만약 2를 곱하고 나누고 싶다면 시프트 연산을 사용하면 된다.[46] 라틴어는 지금의 프랑스어, 이탈리아어, 스페인어, 포르투갈어, 루마니아어의 기원이자 영어, 네덜란드어, 독일어에 지대한 영향을 끼친 언어이다. C언어도 후에 나온 프로그래밍 언어의 뼈대가 되는 문법을 만드는 데 지대한 영향을 끼친 것이다.[47] Python과 그에 영향을 받은 언어들은 이런 영향에서 약간 자유롭다. 이런 언어들은 C언어와 중괄호가 아닌 들여쓰기로 블럭을 구분하는 특징이 있다[48] fopen() 등의 결과로 반환되는 FILE 포인터를 파일 객체에 대한 포인터로 보면 이해가 쉽다.[49] C언어족 언어, C언어계 언어, C과 언어, C목 언어 등으로 번역할 수 있다.[50] ISO C와 ISO C++의 차이, N4860 C.5 C++ and ISO C 참고.[51] 단적인 예시로 Python의 for문은 직관적이고 활용성도 높지만, 내부적으로 컴퓨터가 어떻게 처리하는지를 알기가 어렵다. 반면 C언어의 for문은 직관성과 가독성은 떨어지지만 컴퓨터가 '반복'이라는 개념을 어떻게 구현하는지 그 자체로 보여준다.[52] 서울대학교(자연대, 공대), 카이스트, 포항공대 모두 공통졸업요건으로 CS101 수강을 요구하는데, 세 학교 중 가장 끝까지 C를 고집했던 포항공대 학부생들의 반발이 적지 않았다. 2010년대 후반까지만 해도 C로 CS101 과목을 계속 돌릴 태세를 취하다가, 결국 2023년에서야 다른 두 학교를 따라 Python으로 변경.[53] 컴퓨터공학과 학생들도 1학년 때 배운 후로 C언어를 실전에서 쓸 일이 없어 잊어버리는 경우도 흔하다. C언어를 계속 붙잡을 시간에 그냥 자기가 가고싶은 분야에서 주로 쓰이는 언어를 익히는게 훨씬 도움이 된다.[54] 당연히 학벌주의가 존재하는 한국에서는 전공자를 더 대우해주긴 하지만, 말 그대로 그저 학력이 있는 사람이니까 대우해주는 것이지 C언어가 문제가 아니다.[55] 당연히 여기서 고수준이라는 건 수준이 높다는 뜻이 아니라 추상화가 많이 이루어져서 기계로부터 멀리 떨어져있다는 뜻이다.[56] 애초에 고수준에서 C가 쓰이니 마니 하는 이유는 그 만큼 추상화가 잘 되어 있기 때문이고, 그 추상화를 해주는 누군가가 있기 때문이다. 결국 누군가는 C 언어 컴파일러를 만들어야 하고, 누군가는 그 C 로 운영체제나 파이썬 라이브러리 등을 작성해야 한다. 이런 누군가들의 작업 내용은 전부 제외해버리고, 누군가들이 쌓아올린 추상화 위에서 파이썬을 두들기고 자바를 두들기는 것만을 추려낸 게 여기서 말하는 "실무"이다.[57] 코딩은 비전공자도 가르쳐만주면 얼마든지 한다. 아니, 초등학생도 코딩 정도야 얼마든지 할 수 있다. 그러나 컴파일러론을 배우지 않으면 자기가 짠 코드가 어떤 식으로 기계어로 변환되는지 모르고, 운영체제를 배우지 않으면 자기가 만든 프로세스가 어떻게 관리되는 지도 모르며, 컴퓨터 구조를 배우지 않으면 그냥 자기 컴퓨터가 어떻게 이루어져 있는 지조차 모르게 된다. 다 떠나서, 애초에 비전공자 중에선 하다못해 String을 C로 직접 구현해 본 사람조차 보기 힘들다. 'A 를 입력하면 E가 출력되는구나' 정도로 이해하는 사람과, 'A 를 입력하면 B, C, D 를 거쳐 E가 출력되는구나' 라고 이해하는 사람은 결국 시간이 지날수록 격차가 벌어질 수 밖에 없다.[58] 비전공자한테 추상화가 잘 이루어진 파이썬만 가르치고 끝내는 기조가 이런 맥락이다. 어차피 가볍게 코딩할 사람들한테 C부터 들이밀면 오히려 역효과만 날 뿐 어떤 이점도 없다.[59] 물론 깊숙히 파고 들거면 이것도 이야기가 달라진다. 예를 들어 운영체제를 깊게 파고들어서 리눅스 커널 뜯으며 놀고 싶다면 C는 물론이고 어셈블리까지 파야한다. 그렇게까지 깊게 가진 않고 이론적으로만 배운다면 C 없이도 학습이 가능하긴 하다는 뜻일 뿐이다.

파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 문서의 r1669에서 가져왔습니다. 이전 역사 보러 가기
파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 다른 문서에서 가져왔습니다.
[ 펼치기 · 접기 ]
문서의 r1669 (이전 역사)
문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)