C 언어

From Hidden Wiki
Jump to navigation Jump to search
필독 사항 유닠스 계열 저작물, 성인물, 도박 웹 써버 보안 프로그래밍 그래핔 파싱
필독 사항 고스트BSD 표면 웹 싸이트 제작 리눅스 마스터 파이썬 트킨터 뷰티펄 숲
수학 아이투피 마약, 아청물, 해킹 웹 싸이트 보안 웹 프로그래밍 데이터 분석 게임 제작
통계학 뮤와이어 다크넽 싸이트 제작 정보 보안 기사 쟁고우 팬더즈 파이게임

개요

#include <stdio.h>

int main(void) {
    printf("Hello, world!\n");
    return 0;
}


> The computing world has undergone a revolution since the publication of The C Programming Language in 1978.

> 1978년 책 "The C Programming Language" 출판 이후 컴퓨팅 세계는 혁명을 겪어왔다.

>----

> - The C Programming Language 2nd Edition[* 데니스 리치와 브라이언 커니핸 지음. [[1]]]


C 언어 (C language, C, , C (programming language), 씨언어, C언어)


1972년에 벨 연구소(Bell Labs)의 데니스 리치[* 흔히 C 언어의 공동제작자로 오해되곤 하는 사람들로 켄 톰슨과 브라이언 커니핸이 있는데, C의 탄생 및 발전에 관여하긴 했으나 엄밀하게 따지면 C 언어의 제작자는 아니다. 톰슨은 유닉스 OS와 C 언어의 전신인 B언어를 만들었고, 커니핸은 K&R로 불리는 The C Programming Language라는 책을 썼다.]가 만든 프로그래밍 언어. 보통 C 언어라고 한다. 세계적으로 가장 많이 쓰이는 프로그래밍 언어 중 하나이다.

온라인 상으로 코드를 실행시켜 보고 싶다면 [[2]]로.

역사

탄생 이전

ALGOL 60 (1960년)

C언어의 뿌리는 멀리서 보면 1960년에 발표된 ALGOL 60부터 영향을 받았다고 볼 수 있다.

CPL (1963년)

1963년에 ALGOL 60보다 더 다양한 용도로 확장하기 위해 CPL 언어가 케임브리지 대학교의 수학 연구소와 런던 대학교의 컴퓨터 공학부에서 공동 개발되었다.

BCPL (1967년)

1967년에 마틴 리차드가 CPL을 좀 더 단순화시킨 BCPL(Basic CPL)을 개발했다.

B (1969년)

1969년에 BCPL이 미국으로 물건너와 켄 톰슨이 'B'라는 언어를 만들었다. 보통 여기까지가 C언어의 조상격 프로그래밍 언어로 보고 있다.

탄생 이후

C (1972년)

1972년에 같은 벨 연구소의 데니스 리치가 PDP-11 컴퓨터를 제어하기 위해 B언어의 특징을 물려받은, 최종적으로 'C'라는 이름으로 언어를 만들었다. CPL의 'C'는 케임브리지 대학의 C를 뜻했다가 후에 임페리얼 칼리지 런던과 조인트 프로젝트가 된 이후에는 'Combined'를 뜻하게 되는데, 스트로우스트루프의 회고에 의하면 저건 윗분들의 생각이고, 자신들에게 있어서 'C'는 언제나 CPL의 언어 디자이너였던 크리스토퍼 스트레이치(Christopher Strachey)였다고 한다. BCPL을 거쳐 'B' 가 된 것은 '벨' 연구소의 B를 딴 것.

C가 나올 당시 시점에서 당시 널리 쓰이던 포트란이나 코볼, 베이식과 비교해 보자면, C는 언어 자체에는 아주 기초적인 기능 만을 탑재하여 언어에 기본 탑재되는 명령어를 최소한도로 줄였다는 특징이 있는데 START나 END같은 명령어도 배제하고, 대부분을 { } 같은 기호로 표현했기 때문에 코드가 매우 깔쌈하고 용량도 줄었다. C에서는 포트란에는 아직 남아 있던 천공카드 시대의 흔적이 거의 사라졌다.

최초의 C 컴파일러는 간단히 말하면 어셈블리어로 쓰였다. B 언어를 붙잡고 여러번 씨름하다가 나온 부산물 중 하나가 C 컴파일러고, C 역시 리치에 의해 여러 번 리파인되었다. 이 문제가 유명해진 이유는 최초의 C 컴파일러 중 하나가 C 인터프리터로 개발되었으며 그렇게 개발된 컴파일러를 개선시키기 위해서 역시 또 그 컴파일러를 사용했기 때문이다. 혹자는 이것을 "진흙탕에 빠진 사람이 자신의 구두끈을 잡아당겨 빠져나온 격"이라고 표현하기도 한다. 그리고 최근에는 Python이 같은 방식으로 업데이트되고 있다. [* 앨 스티븐스(Al Stevens)의 책에 나오는 말이다.][* 또, 동일한 이유로 운영체제 시동을 뜻하는 단어가 bootstraping을 줄여 booting이 되었다. 운영체제가 하는 일이 하드웨어 위에서 소프트웨어를 로드하여 돌릴 수 있도록 하는 것인데, 운영체제 그 자신도 소프트웨어이기 때문에 누가 운영체제를 로드할 것인가에 대한 모순이 생기기 때문.]

K&R C (1978년)

1978년에 브라이언 커니핸과 데니스 리치는 The C Programming Language의 초판을 집필해서 출간했다. C 프로그래머에게 "K&R"로 유명한 책으로 한동안 이 책을 기준 사양으로 사용되었다.

'ANSI C' (1989년), 'ISO C' (1990년)

1983년에 ANSI가 K&R C를 확장 겸 표준화하기 위해 짐 브로디를 주축으로 X3J11 위원회를 소집하고 1989년에 ANSI X3.159-1989라는 공식 명칭으로 C언어 표준을 제정했다. K&R C의 확장안 성격을 가지고 있으며, 이때 제정된 C언어 표준이라고 해서 C89라고도 불렀다.

1990년에 ANSI가 C언어 표준을 국제 표준화기구(ISO)에 넘기면서 내용 변화 없이 ISO/IEC 9899:1990라는 또 다른 공식 명칭으로도 채택되었다. 내용 자체는 먼저 제정된 ANSI C와 동일하기 때문에 C90이라고 부르기도 하지만 보통은 먼저 승인된 C89가 더 많이 불려진다.

2019년에도 이 C89/90가 사실상의 표준인데, 이유는 여러 가지가 있다. 먼저 C89/90 다음 버전부터는 마이크로소프트에서 표준안을 제대로 지키는 컴파일러를 내놓지 않은 영향이 가장 크다. 또 거의 대부분의 C 컴파일러는 여기서 큰 차이 없는 C99를 따르기 때문이다. 사실상 표준인 C89/90은 ANSI가 직접 제정한 표준이라서 관용적으로 'ANSI C'라고도 부른다. 이 페이지에서도 ANSI C는 C89/90를 의미한다. 하지만 공식적으로 2019년 시점에서 ANSI C와 ISO C는 최신 표준인 C18을 가리키므로. 혼동을 주기 쉬운 표현이다. 왜냐하면 ANSI가 C89/90이후 C 표준 제정에서 손을 떼고 ISO를 받아들였고, ISO에서는 C99, C11, C18까지 계속 업데이트를 했기 때문에.

C95 (1995년)

1995년에 기존 C90 표준의 일부 세부 사항이 수정되고 국제 문자 집합을 추가 지원하기 위해 ISO/IEC 9899/AMD1:1995라는 공식 명칭으로 C언어 표준 첫 수정안이 발표되었다.

C99 (1999년)

1999년에 ISO/IEC 9899:1999라는 공식 명칭으로 표준안이 발표되었고 이후 세 차례 개정되었다.

인라인 함수, 가변 길이 배열 및 가변 배열 원소, IEEE 754 부동 소수점 지원 강화, 가변 매크로 인수 등의 새로운 기능들이 추가 지원되었으며 이는 여러 C 컴파일러에서 이미 비표준 확장으로 구현해 놓았던 내용이다.

C90과 대부분 상호 호환이 가능하지만 형식 지정자가 없는 선언을 허용하는 않는 등의 엄격함이 더해졌는데, 대부분의 프로그래머들이 명시적으로 선언하는 습관을 기르도록 교육받았기 때문에 큰 혼란은 없었다.

C11 (2011년), C18 (2018년)

2007년부터 비공식으로 "C1X"라 불렸던 C언어 표준의 개정판이 논의되었다가 2011년 4월에 제출된 후 12월 8일에 최종 승인되었다.

제네릭 매크로, 익명 구조, 향상된 유니코드 지원, 원자적 조작, 멀티스레드, 경계 검사 기능 등 문법 및 라이브러리에 새로운 기능들이 대거 추가되었다.

2018년 6월에는 최신 표준인 C18이 최종 승인되었다. C18에는 새로운 기능은 없고, 이전 C11 표준의 결함에 대한 기술적인 수정 및 설명만 추가되었다. 단, 컴파일러 옵션은 여전히 {{{-std=c11}}}이 최신이다.

C11, C18은 2019년 현재까지는 실무에서 아직 잘 안 쓰인다. 가변길이 배열, 가변인자 매크로 등 오늘날 유용하게 쓰이는 대부분의 문법은 C99에서 추가되어 C11, C18의 필요성을 느끼는 곳이 많지 않기 때문이다.

설명

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는 "믿을 테니까 알아서 해라." 한마디로 끝낸다.

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

어플리케이션 영역이 아닌 운영체제를 어셈블리어가 아닌 언어로 작성한다는 것은 당시엔 금기였다. 유닉스의 전신인 MULTICS는 그 금기를 어기고 PL/1라는 고수준 언어로 작성하려고 시도했고, 망했다. 유닉스는 멀틱스에 대한 반성에서 단순하게 만드는 방향을 추구했으며, 그렇기에 이름부터가 UNI-로 시작하도록 지었다. C 언어와 유닉스는 소수의 예외를 제외하고 대부분을 C 언어로 작성하고서도 우려와는 달리 단점보다 장점이 훨씬 많다는것을 보이면서 이 금기에 정면으로 도전해서 승리하였다.[* 유닉스는 처음에는 어셈블러로 만들어졌지만 점차 C로 대체되었다. 그리고 그 덕분에 이식성을 확보하여 여러 기계로 퍼져나갔다.] 그리고 지금은 운영체제는 C 언어가 아니면 안된다는 새로운 금기가 생겼다. 이는 그동안 컴퓨터의 평균 성능이 놀랍게 발전했고, 운영체제 역시 초기와는 비교할 수 없을 정도로 커져 효율성이 중요하기 때문이기도 하다.

그래서 C는 처음부터 어셈블리어와 비교할만한 효율을 가지게 저수준으로 설계되었다. K&R 의 "C programing language" 책의 초판 서문에서도 C 를 어셈블리어를 대체하는 이식성 있는 어셈블리어 (portable assembler) 로 만드는 것을 목표로 설계되었음을 밝히고 있다. [* 서문 내용: 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 ...]

개발 당시의 고수준 언어로는 코볼, 포트란이 주로 쓰였는데, 이들은 천공 카드가 쓰이던 시절 만들어져서 문법이 매우 불친절하다.[* 천공 카드의 잔재로는 제일 왼쪽 몇 칸은 주석, 그 다음 몇 칸은 정의 하는 식으로 코딩할 때 칸까지 맞추어야 하는 규칙이 있다. 당시 프로그래밍 자료를 보면 요즘 사람들의 눈에는 모눈 원고지로밖에 보이지 않을 것이다.] 그리고 코볼은 원래는 프로그램 코드가 업무 서류로도 사용이 가능하도록(!) 설계가 돼있어서 그렇다. 주석이 코드고 코드가 주석일 경우의 아주 나쁜 사례.[* 2000년대에 애자일 프로그래밍에서 코드를 쉽게 읽을 수 있게 한다는 개념과는 다르다. 이쪽은 추상화~~느님~~으로 마치 자연어처럼 읽히는 코드를 써서 주석 자체를 코드로 최대한 대체해서 주석이 코드의 변화를 못 따라가는 불상사를 방지하는 것이다.] C는 이런 당대 고수준 언어들에 비해 매우 이해가 가기 쉬운 문법을 사용하여 초보자가 쉽게 접근할 수 있었다.

어셈블리 코드를 코드 안에 집어넣어서 구동 속도 면에서 이점을 얻을 수도 있다(그러나 호환성이 낮아진다.). 인라인 어셈블러가 이를 가능케 해 준다.

C 언어는 유닉스를 만들기 위한 언어로 시작했기 때문에 유닉스의 표준인 POSIX와도 관련이 깊고, 결국 C를 표준으로 공부하려면 현 시점에서는 성능이 우수하고 표준을 엄격하게 따르는 GCCLLVM/Clang리눅스와 조합하여 사용하는 것이 가장 낫다. 리눅스는 유닉스와 같은 뿌리를 공유하며 POSIX 표준 역시 따르고 있기 때문이다.[* macOS도 POSIX 표준을 따르는 운영체제이기 때문에 나쁘지 않은 선택이다.] 최소한 비주얼 스튜디오는 사용하지 않는 것이 좋다. 비주얼 스튜디오에 통합되어 있는 MSVC 컴파일러는 C99의 가변길이 배열조차 지원하지 않을 만큼 C언어 지원이 부족하기 때문이다. 윈도우 사용자라 하더라도 과거에 비해 리눅스를 사용하기가 한결 수월해졌는데, Windows 10부터는 Windows Subsystem for Linux(WSL)라는 이름으로 리눅스 서브시스템이 추가되었기 때문이다. 무거운 가상머신인 Cygwin을 써야 했던 시절을 생각하면 훨씬 상황이 좋아진 것. 특히 GCC, LLVM/Clang은 {{{-pedantic -Wall -Wextra -Werror}}} 컴파일 옵션[* MSVC의 경우 /Wall /WX /Za /permissive-]을 넣으면 사소한 경고 사항도 전부 에러로 변환하여 컴파일을 중단시키므로 코드의 품질을 잡는 데 많은 도움이 된다.

대한민국에선 많은 곳이 C로 공부를 시작하며 나머지는 C++, Java, C##, 비주얼 베이직, 어도비 플래시(액션스크립트) 등이 차지하고 있다. 즉 독점에 가까운 위치를 점유하고 있다. 이는 각 대학 혹은 학원들의 커리큘럼 탓이 가장 크다고 볼 수 있는데, 이 때문에 아직도 '자바 먼저' vs 'C 먼저'의 떡밥은 개발자들 사이에서 좋은 키배거리가 되고 있다. ~~요즘은 Python vs C언어 키배로 넘어가는 중~~ 그러나 후술할 듯 C가 미친 영향은 Java를 포함해서 매우 광범위한지라 어떻게 해도 결국 C가 ~~1라운드 보스~~ 맨 앞에 선다. 대한민국컴퓨터과학과 학부 과정에서는 이게 프로그래밍 언어의 기초 취급을 받고 있다. C언어 자체의 난이도는 위에서 봤듯이 무시할 게 못 된다. 그럼에도 C를 먼저 권유하는 입장은 간단하다. 컴퓨터 아키텍쳐 및 시스템 프로그래밍, OS를 배우기 위해선 C언어(+ 어셈블리어 조금)만한 게 없기 때문이다. Java 등의 고수준 언어는 추상화 레이어가 여러 OS 및 아키텍쳐 개념들을 가리고 있어서, C++는 다양한 멀티 패러다임을 언어에 집어넣느라 복잡한 문법이 많아져서 low-level한 직관을 얻기 힘들다.

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

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

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

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

C는 작지만 어셈블리어 다음으로 기계 레벨까지 접근 가능하고, 충분히 강력한 언어이기 때문에 C의 힘을 제대로 발휘하려면 어셈블리어 수준으로 프로그래밍해야 하고 하드웨어의 기능을 꿰어차고 있어야 한다. 오죽하면 C가 '시스템 독립적인 어셈블리(System Independent Assembly)'라 불릴 정도. 애초에 C 자체가 운영체제를 만들기 위해 고안된 언어이니만큼 그럴 수밖에 없었다.

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

장점

C로 짜여진 코드는 속도가 빠르고 바이너리 크기도 작지만 수정 사항을 확인하려면 컴파일이 필요하며 디버깅도 어려워 생산성이 비교적 낮다.--단점을 여기에 적으면 어떡해--[* 사실 C++나 다른 컴파일 언어에 비해서 C언어 자체는 컴파일 속도는 굉장히 빠른 편이다.] 이러한 특성 때문에 속도가 다른 무엇보다 (심지어는 생산성보다도) 중요한 임베디드 혹은 모바일 계열, 또는 시스템 프로그래밍 등에서 주로 사용된다. 그러나 모바일의 경우도 이미 하드웨어가 발전하여 제약이 많이 풀린 상태이며 생산성을 중요시하는 추세로 가고 있기 때문에 C가 설 입지는 점점 좁아지고 있다. 실제로 현재 시중에 판매되는 휴대 전화에 WIPI-C의 컴파일러 및 링커 설정을 고쳐서 C++를 사용하게 할 수 있는데 문제가 될 만한 속도 저하는 없었으며 여러 나라의 BREW 시스템에서도 C++로 인해 속도 문제가 생긴 적은 없다. 더군다나 최근 인기있는 안드로이드 OS 같은 경우 기본적으로 Java를 사용해서 개발하도록 되어 있다.[* 정확히 말하면 Java는 구현 언어로만 사용하는 것이며, 라이브러리는 전부 안드로이드 SDK를 사용해야 한다. 따라서 JVM 바이트코드를 생성하는 Kotlin, Scala 같은 언어로 코딩하는 것도 불가능한 것은 아니다. 실제로 현재 안드로이드의 공식 언어는 Kotlin이다.]

다음은 각각의 언어로 똑같은 출력(띄어쓰기 포함)을 하는 구구단 프로그램을 세 코드로 짰을 때의 파일 크기이다. || |||| C |||| C++ |||| Python || || 원어 파일 |||| 240byte |||| 241byte |||| 106byte || || (exe)실행파일[* exe실험은 콘솔에서 실행하고 출력은 세 언어파일이 같다. exe파일 하나로만 실행이 가능한 형태로 준비되었다.] |||| 24.8KB |||| 1,538KB (C의 약 54배)[* 그러나 .cpp 파일에 C언어로 작성하면 C언어 실행파일과 크기가 같아진다.] |||| 5,032KB (C의 약 176배) || || exe 실행시 RAM 소비 |||| 3.42MB |||| 5.61MB |||| 11.89MB || >컴퓨터의 성능과 코딩 방법 등에 따른 차이가 날 수 있음.

너무나 당연하게도 이 언어들로 다른 출력의 프로그램을 만든다면 차이는 더 커질 수 있다. 프로그램의 규모가 커질경우 C언어로 작성해서 얻을 수 있는 파일의 규모는 Python보다 훨씬 작다. [* C언어, Python을 더 확실하게 비교해 보기위해 OpenGL 게임을 제작했는데, C언어는 82MB크기의 실행파일이 나왔을 때, Python은 2,376MB크기의 실행파일이 나왔다. 즉, C에 약 300배 정도 버금가는 크기.]

과거에는 컴퓨터의 발달이 많이 되지않은 만큼 메모리도 적게 잡아먹는 프로그램을 선호했으며, 화성 탐사선도 이러한 점을 반영하여 C언어로 만든 --250만 줄의 코드-- 프로그램을 사용했다.


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

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

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

현시점에서 C의 가장 큰 의의는 사실상 모든 아키텍쳐와 운영체제에서 지원하는 언어라는 것이다. 일반적으로 C++는 지원하지 않더라도 C는 지원하는게 보통이다. 워낙 널리 쓰이다보니 CPU 디자이너들이 가장 먼저 하는 일은 C언어를 instruction set으로 포팅하는 것일정도. 심지어 C언어 설계 자체가 CPU 인스트럭션 설계에 영향을 주는 단계에 이르렀다. 그런 관계로 이식성이 중요한 경우는 대개 C를 사용한다. 자바의 멀티플랫폼과는 성격이 다르다. 자바는 각 플랫폼용으로 만들어진 가상머신 위에서 같은 소스가 실행되는 것이고, C의 경우는 각각의 시스템에 맞는 기계어로 컴파일 되는 것이다. 위에도 언급했던 자바 가상머신 자체가 C로 만들어져 있으므로 당연히 자바보다 범위가 넓다. 기존 C 프로그래머들은 진정한 멀티 플랫폼 언어는 자바가 아니라 C 라고 믿는 사람도 부지기수. 표준만 철저하게 지킨 C 코드는 C 컴파일러가 있는 어떤 플랫폼에서도 컴파일 - 실행이 가능하다.[* 다만, C 표준 자체가 많은 부분을 '모든 컴파일러에 동일하게'가 아니라 구현체에 따라(implementation-dependent) 정의하게 하기 때문에 이런 부분들에 대해서는 전부 각 플랫폼마다의 특성을 따로 반영하여 코딩해주어야 한다.] 그게 쉽지 않아서 문제지. 요즘 C를 사용하는 이유는 위에도 쓰여있듯이 저수준의 제어가 필요하기 때문에 사용하는데, 이는 플랫폼에서 제공하는 API를 사용하지 않고는 불가능하기 때문.

이렇게 이미 한물 간 언어처럼 보이지만, 여전히 '프로그래밍' 입문으로 C를 추천하는 사람이 많다. 사실, 이 말에도 일리가 있긴 한것이 C라는 언어는 매우 심플하면서도 배우는 과정중에 소프트웨어 구성의 최소단위인 bit부터 시작해서 메모리 관리, 그리고 고급 개념인 OOP 비스무리한 것까지[* 실제로 요즘 나오는 C 교재 중에는 후반부에 OOP 챕터도 넣어놓은 경우가 가끔 있다.] 흉내내면서 소프트웨어 전반을 훑게 되고, C 를 배우는 과정중에 나오는 과제들은 커맨드라인에서 이미 쓰이고있는 기본적인 툴들을 reinvent the wheel[* 바퀴의 재발명이란 뜻으로, 일반적으로는 이미 다 존재해서 갖다쓰기만 하면 되는것을 괜히 고민하면서 또 만들어내는걸 비판하는 데 쓰이는 문구이다. 다만 실전 개발에 있어서는 안좋은 습성이지만 교육용으로는 의도적으로 한번씩은 거치게 하는 편이다. 재발명하면서 기반 시스템의 구조에 대해서 알아갈 수 있기 때문이다.] 하는식의 과제들이 많기때문에 바닥부터 훑어가며 견문을 넓히는데 좋다. 실제로 가장 기저에 놓인 OS API[* 유닉스의 경우 POSIX API, 윈도우의 경우 Windows API]는 오늘날 플랫폼을 불문하고 거의 다 C 언어로 되어있고, 그외에도 대부분의 인프라가 되는 소프트웨어들은 C로 만들어진 후 타 언어로의 바인딩을 제공하는 식이다. 로우레벨부터 단계를 높여가며 관찰을 해보면, 머신코드는 머신에 따라 달라지고, 어셈블리어도 Intel/AT&T 등 문법에 따라 몇가지 버전이 존재하지만, 그 위쪽에서 결국 C 언어로 대통합이 이루어진다. 그리고, C 언어 위쪽으로 가면 다시 C++/Java/C#/Objective-C/Python 등으로 다양하게 갈라진다. 즉, 두개의 원뿔을 꼭지점끼리 붙여놓은 double cone 형태이며, 저 꼭지점 부분에 C 언어가 존재하는 형태이니 이것만으로도 C 언어의 중요성은 충분히 알 수 있다. 그렇기 때문에 이런 견문은 실제로 나중에 더이상 C 언어를 쓰지 않고 타 고급언어로 넘어가더라도 유용한 경우가 많다. (실제로, 많은 수의 언어가 C 언어와의 FFI를 제공한다.)

다만, 요즘 대학에서는 신입생들 프로그래밍 입문 강의로 C 언어를 기피하는 추세가 있는데, 대학은 '특정 프로그래밍 언어'를 배우거나 '프로그래머'를 양성하는 기관이 아니기 때문이다. 실제로 해외대학들은 C 언어를 입문강의로 사용하는 곳이 거의 씨가 말랐다. 그리고, 그 이후에도 모두가 C 언어를 배우는 것이 아닌, 하드웨어 쪽으로 방향을 정한 학생들만 C 언어를 배우는 경우가 많다. 사실 오늘날의 컴퓨터는 수학(논리학)과 전자공학의 중간즈음에 위치한 분야라 할 수 있는데, 해외에서는 학풍에 따라 프로그래밍 입문 강의를 순수수학/논리학과 가까운 Programming Language Theory의 입문으로 생각해서 가르치거나 보다 실용적인 소프트웨어 개발경험 정도로 생각해서 가르치는 것 중 하나를 선택하는 경우가 많다. 그리고, 전자의 경우에는 언어적으로 볼 때 너무나 심플해서(그냥 하드웨어를 그대로 노출시킨듯한 디자인의) 아무런 매력이 없는 C 언어를 사용할 이유는 거의 없으며, 후자 쪽도 소프트웨어 개발 전반을 경험하게 하기에는 한 학기 내내 커맨드라인 입출력만 끄적이는 게 고작이다보니 마찬가지로 C 언어를 사용할 이유가 없다. 한 학기 배우는 과정중에 어느 정도의 OOP 개념도 맛볼 수 있는 Java나, 한 학기만 배우면 학생들 스스로 어느 정도 괜찮은 프로젝트를 진행할 수 있게 되는 Python에 비하면 오늘날 C 언어의 매력은 크게 떨어진다.

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

대학에서 C 언어를 프로그래밍 입문으로 배울 경우, 그 강의는 프로그래밍 강의라기보다 순수한 C 언어 강좌가 돼버리는 경향이 있다. 예를들어, char p{{{[]}}} = "aaa"; *p = 'b'; 는 문제가 없는데 char *p = "aaa"; *p = 'b'; 는 런타임 에러가 난다.[* 리터럴 문자열(쌍따옴표로 감싼 문자열)은 컴파일 시 코드 영역(Code Segment)에 모이게 되는데, 메모리로 로드된 이후 이 코드 영역은 보안상 운영체제에 의해 읽기 전용으로 처리된다. 그러니 포인터로 해당 주소에 직접 접근하여 내용을 변경하려고 시도하면 운영체제가 이를 차단하고(Segmentation Fault) 프로그램을 강제로 죽여버리는 것. 반면 char p{{{[]}}} = "aaa";의 경우 "aaa"가 코드 영역에서 스택 영역으로 복사되고 p{{{[]}}}로 접근하게 된다. 따라서 코드 영역의 값을 변경하는 것이 아니므로 런타임 에러가 발생하지 않는다. [[3]] 다만 OS와 컴파일러에 따라 다른 결과가 나올 수 있다. 예컨대 MS-DOS 시절에 많이 쓰인 Turbo-C에서는 두 번째 코드도 런타임 에러가 나지 않았다.] 문자열, 포인터, 배열에 관해서 프로그래밍 개념적으로는 거의 의미없는 그냥 C 언어만의 함정들이 상당히 많은데다가, 코딩 시 자주 접하게 되는 결코 무시할 수 없는 함정들이기 때문에 그냥 넘길 수도 없고 거기에 낭비되는 시간도 은근히 많다. 대학 프로그래밍 강의의 목적을 생각하면 사실 무엇이 더 적합한지는 자명하기 때문에, 최근에는 많은 대학들이 입문 프로그래밍 언어를 Java, Python, MATLAB 등의 고수준 언어로 변경하고 있는 추세이다.

단점

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

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

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

C++에서는 전보다 조금 나아지긴 했지만 포인터를 그대로 유지하면서 여러 가지 상위 개념을 얹었기 때문에 말 그대로 조금이다. 특히나 저 포인터에 더해서 여러가지 상위 개념들의 컨셉까지 익혀야 하고, 그것들과 포인터의 화려한 향연까지 존재하기 때문에 습득하기도 엄청나게 어려운 언어이다. 저수준부터 고수준까지 다 되긴 하지만, 일단 발을 들여놓으면 그야말로 평생 공부해야 하는 평생 언어가 되었다. C++는 STL이 개발되어 그보다 조금 고급 개념을 제공하기는 한다. 그래도 위에 나온 것들을 덜 신경 쓰고 싶다면 현재 사용되는 개념을 많이 탑재한 Python, Java, C\#이 낫다.

다음은 C언어 사용 시에 어려움을 느끼기 쉬운 부분들이다.

* 느슨한 타입 검사. 초창기의 K&R C에 비해 ANSI C부터 타입검사가 비교적 양호해졌지만 아직도 서로 다른 종류의 포인터끼리의 대입조차 가능하다. 사실 느슨한 타입 검사 자체가 나쁜 건 아니고 최신 언어들은 느슨한 타입 검사를 채용하는 언어가 많은데, 문제는 느슨한 타입 검사를 통과한 타입 미스매치가 실행시에 치명적인 문제를 일으킬 수 있다.
* 배열 안에 접근할 때, 인덱스가 배열 범위를 벗어나도 이를 체크하지 않는다. 이는 C언어에서 배열을 경계가 정해진 추상적인 자료구조로 취급하는 게 아닌, 연속적인 메모리 뭉치의 첫부분을 가리키는 포인터로 다루기 때문에 일어난다. 때문에 이런 구조체 핵 같은 변태같은 짓도 가능한데, 그 예로 구조체 {{{struct _EE_PACKET}}} 에서 앞부분의 데이터는 {{{int cnt; int checksum; int magic;}}} 3개의 int형 변수를 가지며 당연히 이들의 자료형 크기는 불변이다. 하지만 이 구조체 바로 뒷부분에 다수의 {{{struct _EE_EE}}}의 구조체 데이터가 붙어 오게 되며 몇 개가 있는지는 {{{_EE_PACKET}}} 구조체 내부의 {{{cnt}}}에 의해 정해진다. (즉, 가변이다.) 이러한 경우 뒤의 {{{_EE_EE}}} 구조체 데이터에 접근하기 위해서는 {{{struct _EE_EE *EE = (struct _EE_EE *)(((struct _EE_PACKET *)packet_ptr) + 1);}}} 와 같이 할 수도 있지만, {{{_EE_PACKET}}}의 제일 뒷부분에 {{{struct _EE_EE EE[0];}}} 을 넣어 주고 {{{struct _EE_EE *EE = &packet_ptr->EE[0];}}} 처럼 할 수도 있다. zero-sized array나 구조체 내의 변수선언부분의 제일 뒤라면 선언해도 문제가 되지는 않는다. 이는 사실 struct hack 이라는 유명한 트릭으로, 웬만한 (제대로 된) C 교재에서도 대부분 다루고 있다. 참고로, ANSI C 에서는 zero-sized array를 보장하지 않았기 때문에 과거에는 주로 더미값 {{{[1]}}} 을 사용했는데, 이 트릭이 워낙 자주 쓰이다보니 gcc 등 C 컴파일러들은 저 트릭을 위해 자체적으로 zero-sized array 를 따로 허용을 하였고, 결국 C99에 와서 flexible array member 라는 이름으로 표준화가 되었다.[* C99 에서는 struct 마지막 어레이 멤버에 {{{[0]}}} 대신에 그냥 {{{[]}}} 로 써주면 된다.] 이렇게 배열 범위를 체크하지 않는다는 점은 심각한 보안 구멍을 만들어낸다. 일명 버퍼 오버플로 취약점이라고도 하는데 배열 체크를 하지 않는 C언어 때문에 생기는 보안상의 만악의 근원. 물론 프로그래머가 꼼꼼히 체크하면 되긴 하는데 인간 프로그래머는 항상 실수를 한다(...) 따라서 언어 자체에 내재된 취약점이라 볼 수 있다.[* 배열 경계 넘으면 버퍼 오버플로 취약점으로 인해 큰 일이 일어난다. 배열 경계를 넘기면 그 위에 선언한 변수들을 하나하나 덮어쓰기가 되고, 최종적으로 리턴 어드레스(함수의 실행을 끝낸 후 다시 함수를 호출한곳으로 돌아가는 주소값)까지 덮어쓰는 게 가능해진다. 기본적으로 이 취약점을 가진 프로그램을 네트워크에 물리면 원하는 동작을 원격에서 실행시킬 수 있게 되며, 최악의 경우는 루트 권한으로 실행되는 프로그램에서 리턴 어드레스를 쉘을 실행하는 코드가 있는 곳으로 덮어쓰면 루트 계정을 원격으로 탈취당할 수도 있다. 이를 버퍼 오버플로우 취약점이라고 한다. 1998년 전세계 서버를 감염시킨 모리스 웜도, finger라는 유닉스 유틸리티의 버퍼 오버플로우 취약점을 이용하였다. 이 웜은 쉽게 들통나지 않도록 여러가지 카무플라주 전략이 많이 포함되어있었고, 코넬대학은 결국 서버를 다운시키는 등의 조치를 취했지만 전 세계로 퍼져나갔다. 당시 이 웜을 만든 23세의 모리스는 벨 연구소에서 유닉스의 로그인 암호화를 담당했던 사람의 아들이었고, 현재 MIT 대학의 교수다. 2001년 한국에도 사이버 대란을 일으킨 코드레드 의 경우 전세계의 서버 몇십만개를 감염시킨 사례가 있다. 이 취약점 때문에 경계 체크를 안 하는 gets 같은 표준 함수 여러 개를 depreciated(사용 자제) 시켜야 했고, printf의 포맷 문자(%n) 하나가 날라갔다. 심지어 운영체제 자체에서 buffer overflow 방지 메커니즘을 제공하기까지 하는데도 간간히 뚫리는 게 이 취약점이다(...). 물론 공돌이들이 손 놓고 있는게 아니니 방지책도 많이 마련되어있긴 하지만 그럼에도 뚫릴 취약점은 뚫린다. 2014년 OpenSSL에 생긴 재양적인 버그인 하트블리드도 근본적으로는 C언어가 배열의 경계를 체크하지 않는다는 점에서 생겼다.][* visual studio 2012 버젼부터 함수에 _s 가 붙은 (printf_s, scanf_s 등) 보안을 위한 함수가 추가되었다. 기존의 printf 같은 함수는 그냥 쓸 수 있지만 scanf와 같은 함수를 쓰려고 하면 보안에 따른 에러 메시지가 뜬다(...) 그리고 이 안전 함수들은 C11에도 추가되었다.]
* 문자 처리 과정에서 실수가 있으면, 치명적 문제가 생길 수 있다. C에서는 문자열의 끝을 표시하기 위해 모든 비트가 0인 널(null) 문자를 맨 마지막에 달아 사용하는데, 문자열을 처리하는 과정에서 이 널 문자를 잘못 달아주는 실수를 프로그래머가 저지르기 쉽다. 이런 경우, 메모리의 경계를 넘어 다른 영역에 접근하는 치명적인 문제가 생긴다. 비교삼아 다른 언어의 문자열 처리 방식을 살펴보자면, 파스칼과 같은 언어는 문자열의 길이를 맨 앞에 따로 저장하여 처리한다.[* https://wiki.freepascal.org/Character_and_string_types#String *]
* 문자와 문자열을 숫자 또는 숫자의 배열로 취급한다. char 타입은 실제로는 1바이트(C언어 스펙상으로는 반드시 1바이트가 8비트일 필요는 없다.) 크기의 숫자 타입이고, 'A'와 같은 문자 상수(integer character constant)들은 실제로는 그냥 int형이다.[* 직관과는 다르게, char형이 아니다. 객체지향 언어인 C++의 문자 상수가 const char형인 것과 비교되는 부분인데, int형 위주의 수식 계산을 가정한 C언어와 자료형에 맞는 엄밀한 처리를 선호하는 C++의 관점의 차이를 알 수 있는 부분이다.] 따라서 int i='a'; char c = 'b' + i; 같은 식들이 매우 자연스럽다. 이와 비슷하게 문자열(정확히는 string literal)은 그냥 char 형 배열로 쓴다. 문자열 값의 끝에 붙는 널문자 {{{'\0'}}} 역시 int형 값으로 그냥 0 이다.[* 따라서 char c = 0; 이라고 하면 c에는 {{{ '\0' }}} 이 대입된다. 1과 {{{ '\1' }}}과 {{{ '\x1' }}}이 같은 값인 것과 같은 이치.] 
* 문자열 타입이 따로 없어서 문자형 배열을 대신 사용한다. 그 덕분에 문자열 처리가 까다로우며[* 포인터에 익숙하면 오히려 쉽게 느껴질 수도 있다. 포인터 자체가 iterator 패턴과 유사하게 사용 가능하며, 증가, 배열, 참조 연산자를 통해서 짧은 수식으로 여러가지 연산을 한번에 처리할 수 있기 때문이다. 예를 들면 {{{ while ( *dst++ = *src++ ) ; }}} 와 같은 짧은 코드만으로 문자열 복사가 가능하다.], 초보자들이 잘못 이해하거나 혼란을 느끼기가 쉽다. 주로 문자열 상수[* String literal. {{{ "str" }}} 같은 따옴표로 둘러싸인 부분을 뜻함.]에 대해 잘못 이해하는 경우가 많은데, 문자열 상수 자체는 '문자열'이나 '값' 자체가 아닌 '배열'이며[* 정확하게는, 문자열 상수는 변경 불가능한 char형 배열이다. 따라서 마치 배열이름처럼 {{{"abcdefg"[3]}}}와 같이 쓸 수 있으며, 이 수식의 결과값은 {{{'d'}}}이다. --한술 더 떠서 {{{3["abcdefg"]}}}도 {{{'d'}}}이다. {{{*(3 + "abcdefg)}}}이기 때문.--], 결과적으로는 해당 배열의 첫번째 글자가 저장된 위치를 가리키는 '주소값'이 된다.[* "배열 타입의 결과값을 갖는 모든 수식은, 몇몇 예외상황을 제외하고는 그 배열의 첫 번째 원소가 가리키는 포인터 주소값으로 변환된다." 이것이 흔히 말하는 '포인터 상수'의 진짜 모습이다.][* 수정하기 전 문서에서는 '문자열'의 주소라고 설명하고 있었는데, 정확히는 그 '문자열의 첫 번째 원소'의 주소값이다. 같은 말 아니냐고? {{{&arr}}}과 {{{&arr[0]}}}이 다른 것처럼 이 두 가지도 서로 다르다. (주소값은 같게 나오지만 그 타입은 서로 다르다. 전자는 포인터 타입이고 후자는 배열 타입이다.)] 초보들은 {{{char * s}}}를 문자에 대한 포인터가 아닌 문자열에 대한 포인터로 잘못 이해하여 s가 가리키고 있는 '문자열'을 가져오기 위해 s에 참조 연산자 {{{*}}}를 붙이는 경우가 있는데, 이렇게 하면 의도한 대로 원래의 문자열 전체를 가져올 수 없다. 다른 타입들에 대해서는 예를 들어 {{{int i, *j = &i; *j = 3;}}} 식으로 해당 포인터가 가리키는곳에 있는 '값'을 의미할때는 항상 * operator 를 붙여줘야 하지만, 문자열 전체일 경우에는 참조 연산자 {{{*}}}를 바로 쓰지 않고 {{{char *s; s = "Hello";}}} 처럼 구분해서 써야 한다. {{{*s}}} 는 {{{s[0]}}} 과 마찬가지로 문자 'H'의 값 그 자체를 의미하기 때문이다.
* 대입 (=) 연산자가 값을 반환한다. 가장 흔한 실수로 == 를 쓸 곳에 = 를 하나만 찍는 오타를 범했을 경우, integer 값이 리턴되는 경우에는 역시 문제없이 컴파일되고 직접 돌려서 해당 기능이 오작동을 하기 전까지 버그를 알아챌 수조차 없게 된다. 이 때문에 == 사용시 lvalue에 일부러 상수항을 사용하는 프로그래머들도 있다.[* Yoda condition 이라고 불린다. 스타워즈요다가 문장에 주어의 위치를 도치하던 것에서 유래.] 예를 들어, {{{ int a = 42; if(a == 37) { ... } }}} 같은 경우, {{{ 실수로 if(a = 37) }}} 이라 쓰면 false가 아닌 true가 뜨며(a = 37 이라는 expression의 값은 37 이고 0 이 아닌 값은 죄다 true로 간주된다.), a에 37 이 대입돼버려 완전히 오작동을 하게된다. 문제는, 컴파일이 문제없이 되기때문에 버그찾기가 더더욱 힘들어진다. 상수항을 왼쪽으로 옮겨서  if(37 == a) 로 써주면, 37 = a라 썼을 때 당연히 말이 되지 않으니[* expression의 l-value에는 수정할 수 있는 값(a와 같은 변수 등)이 와야 하기 때문이다.] 컴파일 시 에러가 떠서 쉽게 알아챌 수 있다... 라는 유명한 팁이 있지만, 사람의 직관과 맞지 않아 어색하게 느껴지기 때문에 일부러 익숙해지지 않으면 잘 쓰이지 않는다.-- 본능이 거부한다-- 요새는 컴파일러가 좋아져서 이런 위험코드는 대체로 경고처리해준다.[* gcc 4.8 기준으로 'suggest parentheses around assignment used as truth value'(값을 진리값으로 사용하는 assignment는 괄호를 씌우는 것을 권장함)라는 워닝 메시지가 뜬다.][* 보통 if 문 내부에 의도된 대입 연산자의 경우에는 {{{ if ((result = fn(...)) != NULL) { ... } }}} 같은 식으로 대입 표현식을 괄호로 감싼다.]
* 배열과 포인터의 차이가 비직관적이다. 사실 이 부분이 C언어를 배우는데 가장 커다란 난관이 되는 부분이다. 배열과 포인터는 비슷하다고 보기에는 차이가 너무 크고, 다르다고 보기에는 비슷한 부분이 너무 많다고 생각하는 경우가 많다. 덕분에 교재마다 접근방법이 달라지는데, 어떤 교재에서는 '비슷하다'고 전제한 뒤 차이점을 설명하는 식이고, 어떤 교재에서는 '다르다'고 전제한 뒤 공통점을 설명하는 식이다. 어느 쪽으로 접근하더라도 함정과 예외가 상당히 많아지기 때문에, 둘의 차이를 충분히 구분할 만한 상황적 경험이 적을 수밖에 없는 초심자들에게 결국은 이해보다 암기로 흘러가고, 어렵게 느끼는 경우가 많다. 배열 타입과 포인터 타입의 차이를 제대로 느끼지 못하는 상황은 배열 타입을 식에서 이용할 때 첫번째 원소를 가리키는 포인터로써 사용된다는 규칙을 제대로 숙지하지 못한 상황에서 배열과 포인터를 사용할 때 주로 발생한다. 즉, 식에서 배열 타입을 이용할 때, 실제로는 배열 타입이 아닌 포인터 타입으로써 이용이 되는데 프로그래머가 이를 배열 타입을 이용하는 것으로 착각하면 "읭 포인터 타입하고 많이 비슷하네?"라는 반응을 보이게 되는 것이다. 이렇게 겉으로 보기에는 배열 타입이지만 실제로는 포인터 타입으로써 사용될 수 있는 비 직관적인 요소가 존재한다.
* 숫자 타입을 처리하는 규칙들이 비직관적이고 복잡하다. 덕분에 언어 자체의 syntax/semantics를 넘어서 이에 대한 여러가지 꼼수와 내부적으로 처리하는 방식, 머신따라 달라지는 부분까지 염두에 두어야 한다. 숫자타입과 관련된 문제들은 한두가지들이 아니므로 하위 항목으로 분류한다.
 * C언어에서는 int형과 double형만 존재하는 것처럼 느껴질 수 있는데, 실제로, 모든 리터럴은 뒤에 f 등의 타입을 붙여주지 않으면 그냥 int 아니면 double이다. char건 true/false 값이건 short 건 그냥 다 int로 간주한다. 그리고, 타입변환이 필요할 때마다 정수 진급(integral promotion, C99에선 integer promotion)이라 해서, char, short 등의 타입은 일단 먼저 int로 바꾸고 나서 변환을 시작한다.
 * bool 타입은 존재하지 않으며[* C99 에서 _Bool타입이 생기긴 했다. stdbool.h 를 포함할 경우, 그냥 bool로 사용도 가능하긴 하다. 왜 그냥 bool로 안넣었냐면, 기존 프로그램중에 bool 이름을 사용한 코드가 있을 가능성이 있기 때문. 반면, 언더스코어 _ 뒤에 대문자가 오는 이름은 C 표준에서 사용하지 말라고 먼저 명시해놨기 때문에 이것을 사용한 _Bool 이 되었다.], 0은 false이고, 나머지 다른 것들은 전부 true로 처리한다.[* 음수도 true로 처리된단 뜻이다. 없으면 define 문으로 선언해서 쓰면 된다.] 때문에 임의의 char 값이 숫자인지 알아보는 isdigit 함수는 숫자가 아니면 0을 리턴하고 숫자일 경우 보통 1을 리턴하긴 하지만, 0이 아닌 그냥 어떤 수를 리턴하는 컴파일러도 있다. 매뉴얼페이지에는 보통 non-zero라고 되어 있다(...). 비슷하게 논리적 부정 연산을 의미하는 !의 경우는 0일 경우 1로, 0이 아닌 다른 모든 값은 0으로 바꾼다.
 * unsigned 타입이 섞인 계산에서는 음수가 양수가 되어버린다. 이 문제에 대해서는 다소 긴 설명이 필요하다. C 언어에서는 사칙연산과 비교연산자를 포함하여 많은 이항연산자들이 있는데, 이것들은 서로 다른 타입간에 이루어지는 계산을 처리하기 위해 usual arithmetic conversions라는 공통규칙을 적용한다. 그런데 이 규칙에 의하면, 같은 랭크(숫자 타입의 우선순위. 간단하게는 size라 생각해도 무방하다)인 부호형 정수와 무부호형 정수 사이에는 무부호형이 우선권을 가지며, 따라서 이항연산자의 두 항이 모두 무부호형으로 처리된다. 이 간단한 규칙이 불러오는 참극인 즉슨, unsigned 타입이 수식 중에 단 하나라도 섞여 있으면 그 안에 포함된 모든 음수가 양수로 변해버린다. (절대값이 아니라 mod 연산된 값으로 변한다. 경고나 에러 메시지 없이.) 그 결과 일반적인 수학 수식과는 전혀 동떨어진 이상한 결과가 나온다. 이를 방지하기 위해서는 그보다 더 랭크가 높고 실제 표현 범위도 큰 유부호 정수형으로 강제 형변환을 해야 한다. (혹은 unsigned형을 signed형으로 형변환한다.)
* 네임스페이스를 지원하지 않는다. 소스 수준에서 play_sound라는 함수를 어디엔가 정의를 해놓고 다른 소스 파일에서는 play_sound라는 이름만 같은 다른 함수를 만들고 같이 컴파일하면 에러가 난다. 더욱 골때리는건, 컴파일 단계는 무사히 진행되는데 링커 수준에서 에러가 난다는 것. 따라서, 외부 소스 파일로 노출시키고 싶지 않은 내부 함수들은 static 지시자를 앞에 붙여줘서 이름 유출을 방지하고 다른 소스에서 쓸만한 함수는 <모듈이름>_함수이름(e.g. NamuWiki_add_document)을 사용하여 이름 충돌을 최소화 하는게 일반적이다. 이는 사실 장점이자 단점인데, C++에서는 컴파일된 심볼명에 개별 지시자를 넣어 다른 네임스페이스나 클래스의 함수 등이 충돌이 일어나지 않으나, 문제는 C++ 표준에는 이 Naming Mangling에 대한 표준이 정의되어 있지가 않아 컴파일러 마다 호환이 되지 않는다. 그러나 C는 이러한 네이밍을 하지 않기 때문에 컴파일러 호환성 유지가 가능한 한가지 방법이며, 이 경우C++로 구현되어 있으나 외부 API를 제공하는 경우 C 형 네이밍 스킴을 사용한다.
* 기타 비직관적이거나 혼동하기 쉬운 규칙들을 아래에 정리하였다. 이런 규칙들은 대개 성능을 위해 포기한 부분이거나, 예전부터 이어진 전통인 경우가 많다.
 * 가변함수의 매개변수에는 기본 인자 진급(default argument promotion)이 적용된다. 
 * 함수의 매개변수로 선언된 배열은 배열이 아닌 포인터이다.
 * 함수 인자의 평가 순서는 정해져 있지 않다.
 * 피연산자 간의 평가 순서는 대체로 정해져 있지 않다. (&&나 || 등의 연산자는 예외)
 * 부호형과 무부호형을 섞어 쓴 수식의 형변환 규칙 [사례]
 * 시퀀스 포인트(sequence point)와 관련된 객체 변경 규칙

C 언어에는 저런 함정들이 매우 많이 도사리고 있으며, C 언어를 배운다는 것은 사실 '어떻게 프로그램을 만들것인가' 를 배우는 게 아니라, '어떻게 저런 함정을 피해갈 것인가' 를 배우는 거라는 사람도 있다.

위에 나온 대부분의 단점들은 한마디로 인간이 자연스럽게 쓰기 힘들다는 것인데, C의 컨셉이 그런 것이니 당연한 것이다. 인간인 프로그래머와의 친화가 우선이 아니라 컴퓨터와의 친화가 우선인 언어인 것이다. C와 컴퓨터 및 운영체제의 이해가 깊어지다 보면 왜 이렇게 쓰기 불편하게 만들어진게 사실 당연한 선택이었는지 이해가 간다.[* 간단한 예를 들자면 타입과 배열과 포인터가 혼돈의 카오스가 되는 이유인데, 사실 데이터를 실제 저장장치에 저장할 때 이게 정수인지 문자인지 배열 주소인지 포인터 주소인지는 기재되지 않기 때문이다. (그걸 일일이 기재하는 건 시간과 공간의 낭비고, 쓰잘데기도 없는 데다가, 데이터가 그런 단순데이터만 있는 게 아니기 때문에 거의 불가능하기까지 하다.) 마치 메모장에는 32871647200이라고 적어놓고 나중에 메모장을 펼쳐서 이 숫자가 무슨 숫자였지.... 하고 해석하는 것과 같다. 그 데이터가 무슨 데이터였는지 정확히 기억하고 알맞게 사용하는 것은 어셈블리에서는 순전히 사용자의 몫이고, 그때의 전통이 살아있던 C에서는 기본적인 타입 같은 건 컴파일러가 처리해주되 사용자가 그런 세세한 부분을 컨트롤할 수 있도록 허용해주는 것이다.]

그래서 조금 불편하지만 반대로 이 때문에 저레벨상에서 더 유연한 프로그래밍을 할 수 있다. 오히려 다른 언어는 변수 타입이나 참조 등에 제약이 많아 저레벨 프로그래밍을 할 때는 C언어보다 더 불편한 면이 있다. 예를 들어서 블록 암호화같이 비트/바이트 단위로 바이너리를 자유자재로 조작해야 하는 코드는 고수준 언어로 짜기 불편하다. 익숙해지면 구조체 같은 사용자 정의 데이터 타입을 이리저리 캐스팅해서 포인터 연산을 활용해 전혀 엉뚱한 데이터로 변환해서 쓰는 것도 가능하다.[* 소켓 프로그래밍이 보통 이렇다. 그리고 사실 구조체, 배열, 공용체는 문법적으로 같은 주소/다른 타입이 가능한 경우이기도 하다.]

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

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

사용되는 분야

2015년을 기준으로 C언어는 다음과 같은 분야에서 주로 쓰인다.

* 운영 체제[* 윈도우, 리눅스, 유닉스 커널의 핵심부는 모두 C로 짜여져 있다.][* 커널은 c언어도 많이 쓰지만 c++도 쓴다.] 및 디바이스 드라이버
* 마이크로컨트롤러
* 임베디드 시스템
* 매우 빠른 계산속도가 필요한 프로그램이나 라이브러리
* 암호학 라이브러리[* 비트 수준 연산이 많이 사용되고 속도도 빨라야 하기 때문이다.]
* 프로그래밍 언어 인터프리터(CPython 등)
* 웹 서버(Apache 등)
* 데이터베이스(PostgreSQL 등)

보면 알겠지만 어플리케이션 레벨 프로그래머에게는 어느 하나 쉬운 분야가 없다(...) 이 분야들에서는 C의 위상이 워낙 굳건하여 C++조차 쓰이지 않는 경우가 많다.

이식성

C언어 입문서를 보면[* C의 장점이 이식성이던 시절은 상당히 오래전이기도 해서, 최근에 쓰여진 입문서들은 다른 방향의 서술을 했을 가능성도 있다.] C의 주요 장점으로 이식성을 들고 있다. 그러나 쓰다보면 이 이식성은 정말로 체감하기 힘든 부분인데, 모든 플랫폼에서 동일한 가상 머신을 제공하는 Java는 커녕, 풍부한 자체 내장 라이브러리를 제공하는 Python 등의 스크립트 언어와 비교를 해봐도 딱히 이식성에 유리할 만한 부분이 없다. 그렇다고 이식성이 없는 것이냐 하면 그건 아니다. 가장 많은 플랫폼에서 지원하는 언어는 C언어이고, 어떤 아키텍처가 나와도 가장 먼저 지원하는 언어는 거의 대부분 C이다. 따라서 C언어의 이식성이란 것은 Java나 기타 언어들의 이식성과는 좀 다른 관점에서 바라볼 필요가 있다.

모든 프로그래밍 언어는 추상적인 개념 위에서 돌아간다. 좀 더 정확히는 프로그래밍 언어 자체가 실제가 아닌 추상적 개념이다. (이것은 C언어조차도 예외는 아니다.) 그런데 프로그래밍 언어에 의해 형성된 추상화 계층은 각각의 하드웨어와는 완벽하게 일치하지가 않는다. Java와 같은 언어는 그 하드웨어와 추상화 계층과의 차이를 가상머신으로 메워버린다. 그래서 코드 레벨에서 거의 완벽한 이식성을 확보할 수가 있다. 일반적으로 프로그래머들이 기대하는 이식성이란 바로 이런 코드 자체의 이식성이다.

C언어의 이식성은 약간 다르다. C언어의 추상화 계층은 Java의 두터움과는 달리 정말로 얇다. 그러면 필연적으로 각각의 하드웨어/아키텍쳐에 의한 차이는 고스란히 프로그래머에게 전달될 수밖에 없다. 그렇다고 이 추상화 계층을 두껍게 해 버리면 기존 legacy들과의 호환성 문제는 둘째치고[* 둘째치고 라고 썼지만, 기존의 것들과의 호환성은 C언어가 이리저리 꼬이고 복잡하게 된 가장 큰 이유중 하나다. 부호-음수 표기법이나 1의 보수 표기법까지도 C표준에 들어갈 정도.] 성능상 큰 손해를 보게 된다. 그러므로 여기서 등장하는 것이 표준문서이다. 하드웨어/아키텍쳐에 의한 차이가 발생하는 부분을 3단계로 나누어, 프로그래머가 '알아서' 대응하도록 한다. 그러면 프로그래머는 이식성 문제가 제시된 부분만 알면 되고, 컴파일러 제작자는 구현체(implementation) 만들기 쉬워서 좋고, 이런 저런 서로간의 타협이 이루어지게 되는 것이다.

C언어에는 이식성이 있는 것일까? 구현체(implementation) 자체의 이식성은 굉장히 좋다. 다양한 아키텍처를 배려하여 잘 정의된 표준문서로 인한 이론적 이식성과 함께, 기존의 풍부한 컴파일러 구현체 및 공개된 소스코드들이 실질적인 이식성까지 함께 확보해 준다. 그렇다면 코드의 이식성은 어떨까? 전적으로 프로그래머의 역량에 달린 일이겠지만, 그걸 제외하고 생각한다면 좋다고 봐야 할 것이다.[* 숙련된 프로그래머라면 피해갈 수 있는 함정이지만, 그런 수준까지 가기가 쉽지 않다는 점에서는 말장난처럼 느껴질 수도 있다. 그냥 경력이 쌓여서 자동으로 되는 일이 아니고, 임베디드 계통처럼 관련 문제들을 자주 접해 볼 수 있어야 한다.] C언어 자체만 '잘 알고 짜면' 코드가 의도한 것과는 '다르게' 작동하는 일은 없다. 그러면 프로그래머 입장에서 체감 이식성은? 유감스럽게도 매우 낮다. 기본 제공되는 표준 라이브러리는 그 기능이나 종류가 극히 제한적이라서 그것만 가지고서는 할 수 있는 일이 거의 없고, 추가적으로 다른 라이브러리나 API를 쓰게 되면 이식성이 추가된 부분들에 의해 제한받게 된다.

결국 'C언어'라는 공통 분모는 지극히 표준적인 사양의 동일한 물건이지만, 그것만 써서 프로그램을 작성하는 사람은 없고, 그 겉에 붙는 추가적인 부분들이 이식성을 제한하기 때문에, 결국 프로그래머가 체감할 수 있는 이식성은 없다고 봐야 한다.

다음은 단점 항목에서 분리된 이식성 관련 항목들이다.

C의 이식성 문제는 굉장히 까다롭다. 많은 C 입문서에서 첫머리에 C의 장점으로 '이식성이 있다'고 적어놓고 있는데, 이건 사실 프로그래머가 관련 내용들을 정확히 알고 있는 상태에서 코드를 주의깊게 작성했을 때에만 누릴 수 있는 장점이다. ~~좋은 소식은, 이식성이 있다는 것이다. 나쁜 소식은, 그걸 누리기 위해 죽어라 표준문서를 연구해야 한다는 것이다~~ 한편, C에서 기본 제공하는 표준 라이브러리는 그 기능이나 종류가 극히 제한적이기 때문에, 실질적으로는 다른 API나 라이브러리에 많이 의존하여 프로그램을 작성하게 되는데, 이런 API나 라이브러리들은 특정 플랫폼에 종속되는 경우가 많아서, 결국 C의 이식성은 여러모로 체감하기 어렵다.

C언어 표준의 이식성과 관련된 부분들은 크게 3단계로 구분된다. 동작의 내용을 분명하게 명시해야 하는 것(implementation-defined), 동작을 보장하되 그 내용을 명시할 필요가 없는 것(unspecified)', 동작을 보장할 필요가 없는 것(undefined). 이러한 것들을 표준문서에서는 Portability issues란 이름의 부록으로 따로 정리해 두고 있는데, 이식성을 보장받기 위해서는 자신이 프로그래밍하는 상황과 관련된 이식성 문제들을 모두 이해하고 코드를 작성해야만 한다. ~~그래서 실질적으로는 이식성이 보장 안된다.~~

여기서 특히 undefined에 대한 오해가 큰데, 그냥 단순히 정의되지 않는 것이라기 보다는, 거의 대부분 그냥 잘못된 코드이다. 단, C언어 표준 이외의 어떤 '약속'에 의해서 정상 동작을 보장하는 경우가 가끔 있기는 하다. (가장 유명한 예로는 POSIX 환경에서 동적 라이브러리를 호출할 때 사용하는 dlsym 함수가 있는데, 이 함수의 리턴값인 void*타입을 함수 포인터로 사용하는 것은 C99 표준상으로는 undefined이다.)

특정 기종(주로 x86 혹은 AMD64)에서만 프로그래밍을 하는 많은 프로그래머들은 이식성 문제가 자신과는 전혀 상관 없다고 생각하기 쉬운데, 16->32비트, 혹은 32->64비트 전환기에 많은 사람들이 실제로 고생했던 부분이며, 단순히 컴파일러의 버젼업만으로도 이런 이식성 문제들은 갑자기 튀어나올 수 있다. 즉, 아주 기본적인 이식성 주제들에 대해서는 알고 있어야 한다.

다음은 이식성과 관련된 몇 가지 예이다. ~~차후 정리 및 수정바람~~

* int의 크기는 하드웨어에 따라 제각각이다. 실제로는 '해당 환경에서 가장 빠르게 접근할 수 있는' 크기로 결정된다. 대체로 해당 아키텍처의 비트 수를 따라가는데, x86이나 x86-64만 지원한다면 문제는 없겠지만 그 외의 환경을 생각하면 아키텍처에 따라 다른 크기가 선택될 수도 있다.[* 이것에 대해 가장 유명한 문제는 64비트 컴퓨터의 정수형/포인터형 모델에 대한 문제이다. 다음 링크를 참조. http://www.unix.org/version2/whatsnew/lp64_wp.html ]
 * 이 문제를 해결하기 위해 C99부터 고정 크기 정수형을 지원한다. stdint.h 헤더를 포함하는 것으로 접근할 수 있으며, 형식은 `int비트수_t`와 같다. 무부호형은 `uint비트수_t`. 추가로 위에서 언급한 '가장 빠르게 접근할 수 있는 크기'를 좀 더 명확히 나타내는 `(u)int_fastBIT_t`도 추가되었다.
* 사칙연산을 비롯한 많은 이항연산자에 대해, 각 항의 평가 순서는 정해져 있지 않다(unspecified). 앞의 항부터 먼저 할 수도 있고, 뒤의 항부터 먼저 할 수도 있고, 심지어 상황에 따라 다르게 적용하거나 랜덤하게 할 수도 있다. 많은 사람들이 c = a + b 에서 a와 b의 평가 순서가 정해져 있지 않다는 사실에 당황하는 사람이 많은데, 이렇게 각 구현체에게 선택의 자유를 줌으로써 각각의 환경에 맞는 가장 효율적인 최적화를 가능하게 하는 것이다. (c = f1() + f2()와 같은 코드가 있을 때, 함수 f1이 먼저 실행될 지 함수 f2가 먼저 실행될 지, 심지어 여기에 어떤 일관성 있는 법칙이 있을지 없을지조차도 확실하지 않다는 뜻이다)
* [Point]: 위와 비슷해 보이면서도 실제로는 전혀 다른 주제가 한가지 있는데, 각 코드의 부수효과(side effect. 메모리 안 변수의 값이 실제로 변경되는 시점과 같은 것들)의 평가 순서는 정해져 있지 않으며, 그것이 적용되는 시점 또한 정해져 있지 않다. 다만 확실하게 완결되는 것이 보장되는 시점(예를 들어 함수 인자의 마지막 ')'라거나 수식의 끝을 알리는 ';'와 같은 지점들)이 있는데, 이를 sequence point라고 한다. 예를 들어, j = (i++) + (++i);에서 j에는 어떤 값이 대입될까? ['undefined'다.] (i++)와 (++i) 중 어느 쪽이 먼저 실행되는지 부터가 위에서 말한 대로 unspecified인 사항인데, 실제 문제는 거기에서 그치지 않고 더 심각하다. 언제 i를 CPU 캐시나 레지스터로 불러오거나 쓰고 실제 메모리상의 i의 값을 언제 참조하고 변경되는지조차 확실하지 않기 때문에, 실제 생길 수 있는 결과는 예측 불가능이다. 그리고 이것이 두 sequence point 사이에서 동일 객체를 두번 이상 수정(또는, 한번 수정하고 한번 이상 참조하는 것)하는게 undefined로 금지된 이유이다. 

그러나 이런 설명은 어디까지나 21세기의 프로그래밍 환경에서 보는 관점이다. C가 이식성이 좋다는 것은 원래 어셈블리어와 비교해서 나온 말이므로 사실 Java 같은 언어와 비교해서 이식성을 논하는 것은 원래의 의미와는 다르게 된다. 물론 C의 전성기에도 포트란이나 PL/1 과 같은 다른 고급언어들이 있었고 코드 자체의 이식성만으로는 이들 언어도 C보다 뒤떨어질 게 없었지만, C는 다음의 두 가지 측면 때문에 이식성 면에서 명성을 얻게 되었다.

* C는 OS의 구현이 가능할 정도로 저수준의 프로그래밍이 가능하다. 포트란이 아무리 이식성이 높다고 해도 실제로 사용하지 않는다면 의미가 없다. 용도가 수치계산으로 국한된 포트란이나 속도가 상관없는 분야에서만 사용할 수 있는 인터프리터 언어 등에 비해 C는 거의 모든 용도에서 사용할 수 있었다. 또한 기능이나 성능이 제한적인 언어들은 일부 기능을 어셈블리어로 직접 구현해야 하는 경우가 있었는데, C는 이런 문제가 거의 없었다.
* PL/1 이나 많은 인터프리터 언어들은 C만큼 많은 시스템에 구현되지 않았다. 해당 언어가 '모든 시스템에 구현된다면 높은 이식성이 있겠지만' 실제로 사용할 수 있는 시스템이 제한되는 경우 이식성이 좋다고 할 수 없다. C는 거의 모든 시스템에서 사용할 수 있었기 때문에 이식성을 장점으로서 누릴 수 있었다.

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

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

가장 널리 쓰이는 PC 플랫폼인 윈도우에서는 안타깝지만 반쯤은 버려진 언어이기도 하다. MS에서는 C를 Internal language로 규정하여 내부적으로 윈도우와 기타 MS 상품들을 만드는데는 사용하지만, C 프로그래밍 환경을 사용자에게 정식으로 제공하지는 않는다. 덕분에 윈도우가 자랑하는 비주얼 스튜디오에도 C 프로젝트 항목은 없다. (C++ 프로젝트를 선택하여 소스파일 확장자를 .c로 바꿔주거나 C로 컴파일한다고 프로젝트 옵션을 설정해야 한다.) 게다가, 그런 식으로 사용을 하더라도 MS의 C 지원은 순수하게 C++에 묻어가는 정도라, 새로운 ISO 스탠다드인 C99/C11의 기능들도 거의 지원하지 않으며, 앞으로의 계획도 C99/C11을 완전히 지원할 예정은 전혀 없고 C99/C11의 기능 중 C++98/C++11에도 포함되는 것만 지원할 예정이다.

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

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

마이크로소프트의 C언어 지원 수준

위에서 설명했듯이 사실상 반쯤 버려진 언어라 MS C++ 컴파일러로 컴파일한 코드에 CRT를 억지로 끼워맞추는 수준에 가깝다. 1999년에 확정된 C99마저 제대로 지원하지 않는 것은 MS의 개발 정책 때문이다. 이는 왕년에 MS가 악의 축 취급을 받았던 이유중 하나이다. MS는 2000년대 초 수년동안 C, C++, HTML, CSS 등에서 한참 동안 표준을 무시한 독자적인 방식을 고수하고, 그렇다고 그걸 개선이나 발전을 시키지도 않으면서 정체시킨 적이 있다. 단순히 비표준 확장을 집어 넣어서 문제가 되는 것이 아니라, 표준의 범위 내에서도 서로 충돌이 나는 것이 문제라서 꽤 골치아픈 문제였다. 표준과 MS 제품과의 차이점을 정확히 알고, 이중으로 코드를 작성해야 했으니까...

국내 교재나 대학의 경우 닥치고 윈도우 기준으로 설명하는 경우가 많은데, 비주얼 스튜디오 컴파일러는 ANSI C를 기반으로 상당히 많은 비표준 확장을 제공하고 있으며[* 대표적으로 멀티/와이드 바이트 문자의 동시 지원을 위한 tchar, tmain 등은 MS가 만든 비표준 함수들이다.] 표준에 어긋나 에러를 일으켜야 할 문장도 MSVC는 어떻게든 실행을 해 버리기 때문에 초보자에게는 오히려 독이 된다. GCC 역시 비표준 확장을 많이 제공하지만, 이후의 표준인 C99, C11에서 GCC의 비표준 확장이 대부분 표준화되었기 때문에 상황이 나은 편이다. 웬만하면 유닉스/리눅스에서 표준으로 먼저 배우고 MSVC 등의 비표준 기능은 차후에 문서를 보고 따로 익히거나, 꼭 MSVC로 배우고 싶다면 표준과 비표준, 나아가서 가능하면 C89/C99의 기능을 구분해서 설명하는 교재로 배우는 게 나중을 위해서 훨씬 나은 선택이다. ~~그런데 그런 교재가 없다~~

MSVC의 비표준 문법의 문제는 C뿐만이 아니라 C++에서도 나타나는데, C++가 아닌 MSVC 방언이라고 불릴 만큼 마이크로소프트는 표준 C++를 무시하고 있다. GCC나 Clang에서 오류를 내는 익명 클래스의 모호한 상속, 표준 라이브러리의 fstream wstring 오버라이드 등 수많은 문제를 만날 수 있다. 최근 들어서는 MS도 이 문제를 인지하고 점차 개선하는 중이지만, 여전히 표준 준수에 대해서는 GCC, Clang에 비해 갈 길이 멀다. __cplusplus 매크로와 같은 경우에도 비주얼 스튜디오 2017에 와서야 해결이 된 만큼, 구형 MSVC에서 컴파일 되는 코드들이 (non-secure 함수를 제외하고서도) 클래스 또는 템플릿에서 오류를 내는 경우가 잦다.

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

* { ... }을 이용한 블럭 (ALGOL/PASCAL 스타일의 begin ... end 보다 간결하다)
* 대입을 뜻하는 연산자를 =로, 동일함을 뜻하는 연산기호를 ==로 사용한다. 농담 좀 섞어서, 초심자의 C언어 컴파일 오류의 90%는 여기서 나온다. --나머지 10%는 :이다--[* 때문에 대부분 언어에서는 if에 bool값이 아닌 int값 등이 들어오면 에러를 내며, 일부 언어에서는 if같이 비교연산이 필요한 곳에서 대입연산을 쓰면 컴파일 에러를 낸다.]
* 다르다를 뜻하는 연산기호를 !=로 사용한다.
* 또는/그리고를 ||와 &&로 사용한다.[* 여담으로 C++에서는 &와 |도 가능하다. 차이점은 &&는 앞이 FALSE면 뒤의 것은 확인하지 않고 FALSE로 처리하지만, &는 뒤의 것도 보고 판단한다. C에서는 비트 연산자로 쓰인다.]
* +=, -=, *=, /=등의 직관적인 복합연산자를 지원한다.
* ++ 와 \-\-\ 라는 단항연산자를 사용한다. **와 //는 --당연히[* 1을 곱하거나 나눠서 달라지는게 없다.. 만약 2를 곱하고 나누고 싶다면 시프트 연산을 사용하면 된다.]-- 없다. **은 이중 포인터이고, //은 주석이다. 어차피 수에 1을 곱하거나 나누어도 변함은 없다.
* 그 외에 if, for, while 등 많은 예약어의 사용 방식.

어떤 의미에서는 프로그래밍 언어의 라틴어/한자라고 할 수도 있을지도 모른다. 현재 많은 주요 언어에서 { }를 이용한 블럭 표기나 C에서 쓰이는 표현식(==, ||, &&), 예약어(if, while) 등을 채택해서 사용하고 있다. 따라서 다른 언어를 배울 때 C언어를 먼저 배웠다면 친숙하게 느껴질 것이다.[* Python과 그에 영향을 받은 언어들은 이런 영향에서 약간 자유롭다. 이런 언어들은 C와 중괄호가 아닌 들여쓰기로 블럭을 구분하는 특징이 있다. --그리고 Delphi는 중괄호가 주석이어서 C 사용자가 멋모르고 코드를 짜다가 혼돈의 카오스에 빠지는 경우가 많다--]

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

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

C++와의 관계

이름의 유사성 때문에 C++를 C의 확장판 정도로 생각하는 사람들이 많다. 역사적으로 일단 C++의 시작은 C with Class에서 시작한 것이 맞다. 그러나 그 이후의 변화는 C와 C++의 공통적인 부분에서도 차이점을 만들기 시작했다. 간단하게 말해서, C 기준으로 작성된 소스 코드를 그대로 복사하여 C++ 코드에 옮겨붙인 뒤 컴파일하면 문제가 발생할 수 있다는 것이다. 자세한 것은 다음 문서를 참고하자. [C와 ISO C++의 차이] [* 이 글은 C99와 C++98의 차이점을 분석한 글이므로 현재의 표준들과는 차이가 있을 수 있다는 점을 감안하자 *]

ISO C++ 17 Annex C에는 다음과 같이 ISO C와의 차이점을 설명한 항목이 따로 존재한다. [* 다음 항목은 n4660 기준으로 작성되었으므로 차후 확인 및 수정 바람 ]

* 새로운 키워드~~ 너무 자명하기에 여백이 부족하여 적지 않겠다 ~~
* 문자상수('a')의 타입(C는 int, C++은 char)
* 문자열 상수의 const 유무(C는 변경불가능한 array of char, C++은 array of const char)
* 잠정적 정의(tentative definition)의 허용 여부(C는 가능, C++은 불가능)
* struct의 scope 유무(C는 struct가 scope에 해당하지 않음, C++은 scope에 해당함)
* const가 명시되었고 extern이 명시되지 않은 파일 범위(scope) 이름의 내부/외부 연결(nternal/external linkage) 여부(C는 external linkage, C++은 internal linkage)
* main 함수의 재귀적 호출 여부 및 주소 소유 여부(C에서는 가능, C++에서는 불가능)
* 몇몇 지점에서의 호환되는 타입(compatible type) 허용 여부(C에서는 가능하나 C++에서는 불가능한 상황이 있음. 예를 들어 내용이 같으나 태그네임이 다른 구조체와 같이)
* void *의 다른 포인터 타입으로의 형변환(C는 암묵적으로 가능, C++은 명시적으로 지정이 필요함)
* 함수의 비명시적 선언(implicit declaration of function)의 허용(C는 허용, C++은 허용하지 않음)
* bool 타입에 대한 감소 연산자(decrement operator)의 허용 여부(C는 허용, C++은 허용하지 않음)
* 수식 내에서의 새로운 타입 선언 가능 여부(C에서는 sizeof나 cast 수식 내에서 새로운 타입을 선언하는 경우가 있었음. C++에서는 불가)
* 조건 연산자, 대입 연산자, 콤마 연산자의 결과값은 lvalue(C는 아님)
* C++에서 명시적 또는 암시적 생성자와 함께 사용되는 이전 선언문으로의 점프 금지(switch 또는 continue 등과 관련)
* C++에서 어떤 값을 리턴하게 선언된 함수에서 아무 리턴값 없이 return문을 호출하는 행위 금지(C에서 이게 가능했었나?!)
* C++에서 static과 extern 한정자는 오로지 객체 또는 함수의 이름에만 적용될 수 있음. 타입 선언에 쓰이는 것은 C++에서는 잘못된 문법(C에서는 무시됨)
* C++에서 register는 storage class specifier가 아님. (C++에서는 쓰이지 않으며 단지 차후를 위해 예약됨)
* typedef 이름과 struct tag 이름의 충돌 여부(C에서는 두 이름공간(namespace)이 분리됨. C++에서는 동일함. 단, typedef struct name1 { ... } name1; 과 같은 형식은 허용)
* C++에서는 struct, class, union 등의 객체를 선언할 때 그 앞에 해당 키워드를 붙일 필요가 없음
* C++에서는 const 객체가 반드시 초기화되어야 함(C에서는 그럴 필요가 없음)
* 암시적인 int의 금지 (놀랍게도 type-specifier없이 선언해도 자동적으로 int가 붙는 경우가 C에 있었던 듯)
* C++에서는 auto 키워드를 storage class specifier로서 사용하는 것을 금지함
* C++에서 열거형(enumeration type)은 오로지 동일한 열거형으로만 대입이 가능함(C에서는 그냥 정수형이면 다 대입할 수 있었음)
* C에서는 열거형의 타입은 int였으나 C++에서는 그 자신의 타입을 가짐(int가 아닐 수도 있음).
* int f()와 같이 함수인자 없이 선언된 함수는 C++에서는 인자를 받지 않는 것으로 판단함(C에서는 함수 인자 갯수를 알지 못하는 것으로 처리)
* 함수 선언시에 파라미터 또는 리턴 타입 부분에서 곧바로 타입을 정의하는 것 금지
 * void f( struct S { int a; } arg ) {} // valid C, invalid C++
 * enum E { A, B, C } f() {} // valid C, invalid C++
* C++ 에서는 old style의 함수 정의를 금함. 함수 원형 선언을 강제함. (C프로그래머들조차도 생소한 오래된 스타일의 C 함수 선언 방법이 있음. 이를 금한다는 뜻임)
* C++에서는 char형 배열의 초기화 시에, 충분한 공간이 있어야 함
 * char array[4] = "abcd"; // valid C, invalid C++
* C++에서는 클래스 이름이 객체, 함수 또는 다른 객체 이름을 가릴 수 있음. (C에서는 struct tag 이름이 다른 객체 또는 함수의 이름을 가리지 않음)
* int형 비트필드의 타입은 반드시 signed로 강제됨(C에서는 implementation-defined)
* C++에서는 중첩된 클래스의 경우 그 범위가 클래스 내부로 한정됨. C에서는 클래스가 중첩되어 선언되었다 하더라도 동일한 범위를 가짐.
* C++에서는 typedef 이름이 한번 정의된 이후에 클래스 정의 안에서 다시 재선언되는 것이 금지됨

위의 차이점들이 쉽게 합쳐질 가능성은 그리 많지 않다. C와 C++의 근본적인 방향성의 차이 때문에 생기는 문제들(예를 들어 문자상수의 타입은 어떤 것이 적절하다고 생각하는가? char? int?)인데다가, 사소해 보여도 하나하나가 수십년간 쌓인 legacy 코드들을 완전히 무의미하고 위험하게 만드는 변경점들이기 때문이다. 차라리 언어를 새로 하나 만드는 것이 빠르다. [* 예전에 잠시 이 항목에 이런 [[5]][[6]]들을 기반으로 하여 C++17에서 합쳐질 것이라는 루머가 올라온 적이 있었는데, 해당 문서들의 내용을 잘못 확대해석한 것이다. C99대신 더 최신의 C11을 참조하여 만든다는 것과 C++이 C의 완전한 상위집합(superset)이 된다는 것은 전혀 다른 차원의 이야기이다. ]

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

포인터

포인터는 C의 자료형 중 하나로서, 다른 객체(object)가 저장 공간(data storage) 내에 저장되는 위치(주소) 값을 저장할 수 있다. 이 포인터를 통해 다른 객체에 접근하여 읽고 쓰거나 함수를 실행시킬 수 있다.

C에서의 포인터는 보통 정수 값으로 추상화 된다. 간단하게 설명하자면, 컴퓨터 메모리상의 모든 저장 공간에 대해 1Byte 단위로 1, 2, 3....과 같이 정수로 된 주소값을 부여하여, 이 주소값을 가지고 해당 위치에 접근하도록 한 것이라 생각하면 된다. 이러한 추상화 방식은 다른 고급 언어의 참조형들과 비교할 때 좀 더 하드웨어에 가깝고 직접적이며 직관적인 편이다.

하지만 그 점 때문에, 처음 배우는 사람들이 위험한 방식으로 잘못 이해하기 쉽고[* 대표적으로는 '포인터는 모두 동일한 크기'라거나, '포인터 주소값을 임의로 막 지정해서 집어넣어도 된다'같은 것들이 있다. 실제로 그렇게 쓰는 사례들이 있기에 이걸 일반화시켜 잘못 이해하는 경우가 많다.], 잘 알고 있어도 위험한 실수를 하기 쉽다. 포인터는 일반적인 변수의 정수형과 완전히 다른 그 무언가의 새로운 타입으로 받아들이는 것이 올바른 이해에 유리하며, 포인터를 안전하게 사용하기 위해서는 컴퓨터 구조에 대한 어느 정도의 지식이 필요하다.

모든 포인터형은 정수형과 상호 변환이 가능하다. 하지만 그 변환 규칙에 여러가지 제한이 따라붙기 때문에, 항상 서로 변환이 가능하다고 생각하는 것은 위험하다. 즉 아무 숫자나 포인터형으로 변환하여 쓸 수는 없다. 변환 규칙과 결과 자체는 구현체에서 각자 정의하도록 되어 있고(implementation-defined), 여기에 memory alignment나 trap representation과 같은 주제들이 있어서, 변환 시도 자체가 정의되지 않은 동작(undefined behavior)이 될 수 있다.

또한, 정수형과 유사하게 포인터에 대해서 연산(덧셈과 뺄셈)이 가능하지만 그 결과값은 정수형과 완전히 다르다. 예를 들어, 1000(포인터) + 1(정수형)의 결과값은 1001이 아닐 수 있다. 포인터가 가르키는 자료형의 크기에 따라서 1001이 될 수도, 1004나 1008이 될 수도 있다. 그러므로 포인터를 다룰 때는 항상 그 주소값 뿐안 아니라 그 포인터가 가리키는 타입이 무엇인지를 같이 생각하면서 다루어야 실수를 하지 않는다.

C의 자료형 중에서는 배열, 구조체, 공용체, 함수와 함께 유도형(derived type)으로 분류된다. 어떤 자료형이든 그 뒤에 *(asterisk)를 붙이면 앞의 자료형에 대한 포인터가 되며, 중첩해서 포인터에 대한 포인터를 사용하는 것도 가능하다. 참고로, 선언문으로부터 자료형을 읽어내는 좀 더 정확한 방법은, 변수 이름의 위치부터 시작해서 연산자의 우선순위에 따라 영어 어순으로 결합해 나가는 것이다.

다른 언어의 유사한 개념과 비교하자면, C의 포인터는 정수형을 기반으로 한 상당히 단순한 구조와 기능만을 가지고 있으며 언어 자체적으로도 여러 맥락에서 단지 값으로만 취급된다. 이에 비해 C# 과 같은 메니지드 언어는 별도의 "참조(Reference)형"이라는 타입이 있다. C#이나 Java에는 참조형만을 사용해야 하는 경우를 처리하기 위해서 기본형에 대한 Wrapper Class 들이 존재하고 이로 인해 Boxing과 Unboxing 동작이 필요해진다. C와 C++은 기본형에 대한 포인터나 참조를 사용하면 되므로 Boxing 이나 Unboxing이 존재하지 않는다.

C를 배우는 사람들은 포인터에서 한번쯤은 골머리를 앓아보았을것이다. 대부분 입문자들은 컴퓨터 구조에 대해 전혀 모르는 상황에서 C를 배우기 때문. 하지만 포인터를 제대로 사용할 수 있게 되면, 그 시점에서 컴퓨터 위주로 생각할 수 있게 된다고 볼 수 있다. 그렇기 때문에 사실상 컴퓨터 구조와 함께 배우는 셈이 되는데 컴퓨터 구조에 대한 기본 지식이 없는 학습자에게는 이해하기가 쉽지가 않다.

포인터 덕분에 메모리어셈블리어 수준으로 정밀하게 직접 읽고 쓸 수 있지만 그 반작용으로 에러의 90%는 궁극적으로 포인터 문제다. 포인터를 버그 없이 쓰려면 적어도 메모리의 정렬 제한 정도는 이해를 하고 있어야 한다. 그렇지 않으면 잘못된 포인터 테크닉을 남발하는 경우가 생기기 쉬운데다 연결 리스트를 배우다 보면 내가 무슨 짓을 하고 있는건지 스스로를 자책할 때가 많아질 수 있다.

이러한 어려움 때문에 JavaC\# 같은 매니지드(알아서 메모리를 관리해주는) 언어들은 기본적으로 객체 참조 기반으로 동작해서 포인터를 지원하지 않는다. 참조에 관련된 일은 저단계에서 알아서 해주기 때문에 쌩 포인터를 잡고 낑낑거리기 보다는 쉽다.[* C#은 unsafe 키워드로 정말정말 간절하게 필요한 경우 유사하게 만들어 사용할 수 있다. Java에도 Unsafe 클래스가 있긴 하지만 팩토리 메소드가 막혀있어 리플렉션을 사용해야만 이용할 수 있다. 게다가 Java에서 굳이 포인터를 직접 관리해줄 필요는 없다.] (결국 구조체와 배열 참조도 전부 포인터 참조로 이루어진다. 다만 사용자에게 제공하지 않을 뿐이다.) 이게 뭐가 문제냐면 시스템이 복잡해지고 입체적으로 확장되다보면 수많은 포인터의 사용문제가 결국 관리 비용의 엄청난 증가로 이어지기 때문이다. 프로그래머의 각자 역량에 관리를 맡겨버리는 포인터의 심플함은 결국 대규모 프로젝트의 생산성 저하로 이어지고 알 수 없는 오류에 대한 관리 비용 증가로 직결된다. 그래서 Java나 C#과 같은 메니지드 언어에서는 포인터를 시스템 레벨로 올려서 은폐시키고 프로그래머에게는 가비지 컬렉션 같은 간접적인 노출만 허용하게 되었다. 프로그래머가 직접적으로 메모리를 다루지 않아 오류 상황에서 OS 차원의 셧다운(강제종료)은 없어져서 운용면에서 안정성이 크게 증가되었으나, 퍼포먼스 저하와 더불어 엄청난 오버헤드를 감당하게 되었다. Java나 C#에서 제공하는 가비지 컬렉션의 악명은 설명하지 않아도 알게 될 것이다. 매우 불편하고 느리며 통제가 쉽지 않다. 다만 성능 문제는 현 시점에서 크게 문제가 되지 않는데, Java나 C#이 주로 사용되는 곳은 데스크톱 애플리케이션이 아닌 웹 서버 프로그래밍 쪽이다. 이쪽은 언어 자체의 퍼포먼스보다 데이터베이스의 쿼리 속도가 더 크게 성능을 좌우한다.

포인터는 정말 어렵다! 하는 인식은 과장되어 있는 측면이 크다. 개념 자체는 주소를 저장하는 변수이지만, 입문자에게 포인터가 벽으로 느껴지는 것은 첫 번째로 맞닥뜨리는 생소한 개념이기 때문이다. 그 전까지 간단한 논리 체계를 배우는 수준이었다면 포인터는 본격적으로 컴퓨터의 구조를 공부하게 되는 시점. 게다가 문제는 그 개념이 아니라 응용에 있어 허들이 높다. 제대로 관리하지 않으면 메모리 누수가 일어나고 잘못된 주소를 가리키거나 하는 등 본격적인 디버깅 지옥문. ~~그대신 등가교환으로 디버깅 스킬업~~ 게다가 여러가지 비정상적인 방식으로 포인터를 사용하는 스킬들도 많기때문에 사실상 끝이 없다. 사실 프로그래밍에 있어서는 필수적인 개념이다. 다른 객체지향언어를 놔두고 C를 익히는 이유 중 하나는 궁극적으로 포인터를 사용하여 쉽게 개발을 하겠다는 의도다.[* 코어 임베디드 개발 영역에서는 포인터 없이는 아예 못하는 것도 많다.]

포인터의 악명은 DOS 시절로 거슬러 올라간다. DOS 시절은 16비트 CPU[* 사실 8비트 CPU도 마찬가지지만.]의 한계로 포인터도 near 포인터(2바이트), far 포인터(4바이트)로 나뉘어져 있었는데 이 시절에는 저 포인터들을 다 컨트롤하기가 쉽지 않았다(주소 계산 방식부터 다르다). tiny, small, medium, compact, large 등등의 메모리 모델이 있었고 각각의 모델마다 사용되는 포인터가 달랐다. data segment 가 한개뿐인 모델에는 near 포인터를 사용하였고[* near pointer 는 offset만 저장한다. data segment 가 한개뿐인 상황에서는 offset 이상의 정보가 필요가 없으니.], 그 외의 보다 큰 메모리 모델에는 far pointer 가 사용되었는데 당연히 near pointer 가 오버헤드가 적기때문에 가급적이면 작은 메모리 모델을 골라서 사용하였다. 게다가 포인터가 깨지기라도 하면 아예 컴퓨터가 맛가는 경우도 많았기에 디버깅 지옥이었다. 본격적으로 32비트 시대가 열리고 난 이후[* 사실 32비트 시대에서도 DOS는 호환성 문제로 기본적으로 16비트 프로그래밍을 이용했기 때문에 큰 의미가 없었다. 이 부분은 16비트 버전 윈도우도 마찬가지.]에 near, far 개념이 사라지고 메모리 관리를 OS차원에서 어느정도 관리해주면서 포인터 지옥에서 상당히 해방되었음에도 불구하고 과거의 포인터의 악명이 그대로 이어져 오고 있는 것이다.[* Intel CPU에서 16비트 모드의 경우, seg:ofs로 나뉘는데, 이가 가리키는 주소는 addr = (seg << 4) | ofs; 로 표현되며 seg, ofs 둘다 16비트이기 때문에 이들의 값이 달라도 같은 주소를 가리키게 되는 경우가 많다. 예를 들어서, 22eeh:0000h = 22e0h:00e0h = 2200h:0ee0h = ...] 물론 드라이버와 같은 Low-Level 프로그램을 짠다면 포인터 한번 잘못 사용하는것으로 블루스크린 을 심심찮게 맛볼 수 있다. ~~블루스크린이라도 나오면 이는 매우 양호한 경우이고, 조금 안 좋으면 시스템 정지, 최악의 경우는 어떠한 반응도 없이 바로 재부팅이 되는 경우다. 사실 시스템 정지가 일어나는 경우는 동기화 객체를 잘못 다루었을 때에 발생하는 경우가 많다만~~

참고: 이러한 블루스크린 문제는 최신 OS들이 나오고 고성능 CPU등이 보편화된 2018년 지금 OS레벨에서 관리가 되고 있다. 즉 예전처럼 포인터 오류로 시스템이 셧다운되는 상황은 발생하지 않는다. 프로그램이 실제 메모리 공간에서 동작하는 것이 아닌 가상 메모리 공간에서 동작하기 때문에 프로그램이 잘못된 메모리 접근을 할 경우, 하드웨어에서 운영체제의 커널에 있는 잘못된 메모리 접근을 처리하는 예외 루틴을 동작시키기 때문이다.

포인터는 C에 입문하는 대학생들에게 필요이상의 심리적 부담을 안겨주어 시작하기도 전에 지레 겁부터 먹게 만드는 역효과를 가져왔다. 여기에 교수들의 C언어에 대한 이해 부족[* 교수들의 주요 관심사는 학술적인 자기 전공분야인지라 C언어의 문법에 대해서 자신이 주로 쓰는 것 이상으로 깊이있게 파지는 않는다. 이건 심지어 현업 프로그래머들도 마찬가지. ]과 잘못된 커리큘럼은 포인터를 더욱 큰 벽으로 느끼게 만들고 있다. 이 문제는 십여년이 흘렀어도 개선되지 않고 결국 난이도가 좀 더 낮은 언어인 자바로 프로그래밍 교육 커리큘럼이 이동하는 추세다.[* 몇몇 학교는 아예 MS의 지원을 받아서 C#을 입문 단계에서 강의한다.] 자바를 비롯한 현대 언어들은 포인터를 아예 없애 메모리에 직접적인 접근을 막고 그 자리에 참조자를 넣어서 자동으로 관리를 한다.[* C의 포인터를 배우면 자바의 참조변수나 얕은 복사/깊은 복사 같은 주제들을 좀 더 쉽게 이해할 수 있기는 하다. ] 자세한 건 Java 항목 참조.

어렵다는 단점이 있지만 포인터는 단순하면서도 강력한 도구임을 알 수 있다. 시스템 내부에서도 굉장히 많이 사용되며 크고 긴 문자열 값을 일일이 넘겨줄 필요 없이 포인터 하나만으로 대체할 수 있으며 알고리즘, 자료구조에도 많이 사용된다. 배열이나 구조체 참조에도 포인터를 사용하게 된다. 결국 C의 포인터를 제대로 이해하지 않으면 소프트웨어의 중요한 요소 중 하나인 메모리 관리 기법을 잘 다룰 수 없다.

꼭 하기는 해야 하는 상황인데도 포인터가 너무나도 블랙홀처럼 느껴진다면 그냥 어셈블리어를 아주 기본적인 부분만 어느정도 공부해보는 것이 낫다. C 언어에 비해 훨씬 직접적으로 메모리를 다루고 포인터 개념도 그와중에 많이 사용하기때문에, 대충 사용법만 짚고 넘어가는 경우가 많은 C 언어 교재들에 비해 설명도 하드웨어와 더불어 훨씬 자세한 경우가 많고, 어셈블리어와 어느정도 씨름하다보면 C 언어의 포인터는 그냥 저절로 이해가 된다. 어셈블리어까지 보는게 거부감이 들 수도 있겠지만, 고급기능들은 배제하고 기본적인 부분으로 한정하면 사실 아주 단순하고 시간도 얼마 걸리지도 않는다. 어셈블리어를 보다보면 C 언어가 얼마나 하드웨어와 가까운 언어인지도 실감이 날것이다.

파이썬에서는 ctypes 모듈에서 포인터를 지원한다. C 자료형밖에 안 되지만 숫자, 문자열은 메모리 공유가 되지 않는 파이썬 특성상 필요하기도 하다. 다만 멀티프로세싱에서는 불가능.

C언어에서 포인터는 대체로 다음과 같은 목적으로 쓰인다.

* 참조: 어떤 객체를 직접 복사해서 전달하는 것이 아니라, 그 객체의 주소값만을 취해 전달하거나 관리 및 처리.
 * 함수에 매개변수를 전달할 때, 객체 크기가 클 경우에는 객체 전체를 복사하는 것보다는 포인터를 통해 전달하는 것이 효과적.
 * 다른 객체와의 연결이 필요한 자료구조(연결리스트, 해쉬, 기타등등)의 경우에는 참조 기능이 반드시 요구됨. 프로그래밍 언어에 참조기능이 없다면, 배열에 자료들을 저장한 뒤 그 인덱스를 포인터처럼 사용하여야 함(한마디로, 배열의 인덱스로 참조 기능을 직접 구현).
 * 역시 크기가 큰 객체 여러개에 대해 어떤 자료구조(배열, 연결리스트, 해쉬, 기타등등...)를 통해 관리할 때, 직접 복사해서 관리하는 것보다는 주소값만을 가지고 있다가 필요할 때마다 주소값을 통해 접근하는 것이 효과적
* 메모리에 대한 직접 접근 및 강제 형변환: 객체는 원래 서로 호환되는 타입으로만 변환될 수 있는데, 포인터를 이용하면 객체의 메모리 공간에 대해 직접 접근하여, 마치 다른 메모리 타입인 것처럼 다루거나 내부표현 그 자체를 건드릴 수 있음.
 * 이에 대해서는 메모리 정렬 제한(memory alignment)이라는 개념에 대해 반드시 알고 있어야 안전하게 사용 가능함. CPU의 구조를 모르면 예상하기 힘든 부분이기에, 잘 모르는 경우가 의외로 많다. (널리 쓰이는 인텔CPU가 정렬제한에 상당히 느슨한 것도 그 이유중 하나이다.)
 * char형은 1byte(C언어에선 반드시 8bit는 아님) 크기라는 그 특성상 모든 메모리 정렬 제한으로부터 자유로우며, 따라서 포인터 주소값을 char *형으로 형변환한 뒤 그 주소값을 통해 메모리 공간에 접근하면 직접 메모리 공간을 byte 단위로 읽거나 쓸 수 있음. 이는 구조체 등의 거대한 객체를 복사할 때도 이용되는 테크닉.
 * 정렬 제한이나 내부 표현에만 주의한다면, 큰 크기의 정수나 구조체 등의 어떤 거대한 객체를 해당 아키텍쳐에서 가장 효율적인 크기(보통은 int형)로 쪼개어 접근하거나 처리할 수 있음.
 * signed integer 형이나 float형, 혹은 UTF-32등의 고정길이 문자형과 같이 자기 고유의 내부 표현을 갖는 개체들에 대해 마치 unsigned integer형(가장 널리 쓰이는게 unsigned char형)인 것처럼 접근하여, 비트연산 등을 통해 그 안의 내부표현이나 각 비트를 직접 조작할 수 있음.

참조가 상당히 추상적이고 프로그래밍 언어들이 비교적 보편적으로 지원하는 개념이라면, 메모리에 대한 직접 접근 및 강제 형변환은 하드웨어에 직접 맞닿아 있으면서 한편으로는 다른 프로그래밍 언어에는 잘 지원하지 않거나 혹은 되도록 쓰지 않도록 강제하는 기능이다. 전자 쪽은 비교적 쉬운 편이고(대신 이를 응용한 자료구조 및 알고리즘이 머리터짐), 후자 쪽은 하드웨어에 대한 어느 정도의 지식이 필요하기에 비교적 어려운 편이다. C언어 프로그래머들이 어렵다고 과장하면서 신성시하는 부분도 후자 쪽.

보통의 C언어 입문서에서는 주로 참조 쪽에 집중해서 설명하기에, 입문서의 포인터 부분만 열심히 들여다 본다고 해도 어려운 부분을 이해하는 데에는 큰 도움이 되지 못한다. 이런 경우에는 컴퓨터 구조를 따로 공부하거나 C언어의 다른 부분을 추가적으로 더 공부하는 것이 바람직하다. 이와 관련된 주요 주제들은 다음과 같으며, 하나같이 어렵고 그 양도 방대한 편이다.

* C언어의 타입 시스템과 암묵적인 형변환
* 주요 객체의 내부 표현 방식
* 비트 연산
* C언어 특유의 문자열과 배열과 포인터의 혼용
* C언어의 복잡한 선언과 수식을 읽는 방법
* 하드웨어의 메모리 정렬(memory alignment)
* 객체의 기억수명(storage duration)
* 컴퓨터 구조 전반

많은 사람들이 C언어를 마치 어셈블리와 같이 하드웨어에 직접 맞닿아 있다고 착각하지만, C언어도 나름 추상화가 이루어진 고급 언어이고, 컴파일 과정에서 최적화를 위해 프로그래머의 의도를 코드와는 다른 방식으로 구현하는 경우가 있다(loop unrolling과 같이 동일한 결과를 다른 방식으로 얻을 수 있다). 따라서 그 코드는 추상적 레벨에서 효과적으로 동작하도록 작성하는 것이 바람직하다. 입문서에서 참조 쪽에 집중해서 설명하는 것도 이 때문이다. 컴퓨터의 성능이 절대적으로 부족하고 최적화가 시원찮았던 과거의 전통 때문에 C언어의 문화는 한 땀 한 땀 정성들여 저수준의 테크닉을 사용하는 것에 집착하는데, CPU의 구조가 복잡해져서 프로그래머보다 컴파일러가 CPU를 더 잘 이해하는 지금에 와서는 오히려 역효과를 부를 수 있으므로 추상적인 동작에 더 집중하는 것이 현명한 방식이다. [* [항목 참조] ]

단, OS의 커널 레벨 코드나, 가전제품이나 간단한 장치 제어용 모듈을 설계하는 임베디드 프로그래밍 분야에서는 저수준의 테크닉이 여전히 절대적으로 중요하다.

주요 오개념: 배열과 포인터 사이의 관계

굳이 이 항목이 따로 분리된 이유는, 배열과 포인터 사이의 관계를 올바르게 이해하는 것이 포인터를 이해하는 데 있어 큰 비중을 차지하며, 한편으로 포인터 상수라는 부적절한 설명이 C언어 프로그래머들에게 널리 퍼져있기 때문이다[* 포인터 상수라는 용어에 대해 C FAQ에서는 지나치게 간소화한 용어라는 평가를 내리고 있다. http://www.cinsk.org/cfaqs/html/node8.html#6.9 ].

포인터 상수의 개념이 가지는 문제점은 다음과 같다. 우선, 실제하지 않는 존재인 포인터 상수를 실제하는 것처럼 받아들이게 된다. 또한 객체를 이해하는데 필요한 어떤 일관적인 개념의 틀을 망가뜨린다. 배열 또한 다른 객체들과 마찬가지로, 메모리 공간을 연속적으로 차지하는 어떤 자료 덩어리로서의 성질을 갖는데, 이를 마치 이름표와 덩어리가 분리된 특수한 형태로 잘못 받아들이게 된다. C언어의 다양한 수식(expression) 내에서 올바로 수식을 해석하는 능력을 떨어트림은 물론이다. 이는 단지 포인터를 사용할 때 뿐만 아니라, 수식과 타입 시스템 전체의 개념을 올바로 이해하는 데에도 악영향을 미친다. 마지막으로, & 연산자의 피연산자나 sizeof 연산자의 피연산자로 쓰일 때와 같은 예외상황에 대해 일관되게 잘 설명하지 못한다.

배열과 포인터 사이에 성립하는 정확한 규칙은 다음과 같다.

> C99 6.3.2.1 p3. > > Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type "array of type" is converted to an expression with type "pointer to type" that points to the initial element of the array object and is not an lvalue.

배열 타입을 갖는 수식(배열 이름 또한 여기에 해당한다)은 자동 변환되어, 그 배열의 첫번째 원소를 가리키는 포인터 주소값을 반환한다. 이 결과값은 lvalue(메모리상에 주소를 가진 =의 좌변값)가 아니다. 여기에는 아래의 3가지 예외상황이 존재한다.

* 예외1 : sizeof 연산자의 피연산자로 쓰인 경우. 예) {{{ sizeof(arr) }}}
* 예외2 : & 연산자의 피연산자로 쓰인 경우. 예) {{{ &arr }}}
* 예외3 : char형 배열의 초기화에 쓰이는 문자열 상수인 경우. 예) {{{ char str[] = "hello"; }}}
 * 해설 : 문자열 상수(string literal)도 배열이므로, 해당 규칙의 적용을 받아 배열처럼 사용이 가능하다. {{{ "abcdefg"[3] }}} 라던가 {{{ 3["abcdefg"] }}} 라던가... {{{ char *str = "hello"; }}} 의 경우에도 "hello"의 첫번째 원소를 가리키는 주소값이 대입된다. 그러나 {{{ char str[] = "hello"; }}} 와 같이 문자형 배열을 초기화할 때에는 이 예외3이 적용되어 위에서 설명한 변환 규칙이 적용되지 않는다.
 * 이 세번째 예외가 의미하는 바는, 문자형 배열을 초기화할 때는 반드시 문자열 상수로만 해야 된다는 뜻이다. 이 세번째 예외항목이 없다면(그리고 다른 예외규칙이 따로 없다면) {{{ char *s1="hello"; char s2[]=s1; }}}과 같은 코드가 사용 가능해진다.

위에서 말하는 수식은 일반적 의미의 수식이 아니라 C언어 문법의 수식(expression)을 의미한다.

배열 참조 연산자 {{{arr[0]}}}은 {{{*(arr + 0)}}}과 동치이며, 후자의 수식은 위의 규칙을 적용받아 해석된다. 배열의 첫번째 원소의 주소값이 곧 배열 자체의 주소값이고, 배열 이름은 그 배열의 첫번째 원소를 나타내므로 arr은 arr[] 배열과 배열 내 첫번째 원소를 가리키는 포인터 주소값이 된다.[* 참고로 이는 함수에도 똑같이 적용된다. 함수의 이름이 곧 함수의 주소를 나타낸다는 말. 이에 대해서는 이 항목의 아래에서 다시 설명한다. ] 여기에 1을 더하면({{{(arr + 1)}}}) 두번째 원소를 가리키는 포인터 주소값이 되며, 참조 연산자 * 를 씌우면({{{*(arr + 1)}}}) 배열의 두번째 원소 그 자체가 된다.

변수 선언부에서는 위의 규칙은 적용되지 않는다. 오로지 수식에서만 적용되는 규칙이다. 즉 포인터로 선언하면 포인터가 되고, 배열로 선언하면 배열이 된다. (단, 함수의 매개변수 선언부에서는 역시 마찬가지로 배열이 포인터로 자동 변환된다. 아래의 해당 부분 참고.)

다차원 배열의 경우에는 연쇄적으로 적용되지 않고 딱 한번만 적용된다. array of array of array of int는 pointer to array of array of int로 변환되지, pointer to pointer to pointer to int와 같이 연쇄적으로 변환되지는 않는다는 뜻이다. 실제 예를 들자면, 수식 내에서 쓰인 3차원 배열인 {{{ int arr[3][4][5] }}} 타입은 {{{ int (*arr)[4][5] }}} 타입으로 변환되지만 {{{ int (**arr)[5] }}} 나 {{{ int ***arr }}} 타입으로 변환되는 것은 아니다. 2차원 배열도 예를 들자면, {{{ int arr[6][7] }}} 타입은 {{{ int (*arr)[7] }}} 타입으로 변환되지만 {{{ int **arr }}} 타입으로 변환되는 것은 아니다.

{{{ arr1 = arr2; }}} 와 같이 배열끼리의 복사가 되지 않는 것도 이 규칙 때문이다. 이 규칙 때문에 대입 연산자 =의 좌변이 lvalue가 아니게 된다.

한편, 함수 선언시 매개변수 목록에서도 이와 비슷한 규칙이 하나 존재한다.

> C99 6.7.5.3 p7 > > A declaration of a parameter as "array of type" shall be adjusted to "qualified pointer to type", where the type qualifiers (if any) are those specified within the {{{ [ }}} and {{{ ] }}} of the array type derivation.

함수 선언 시 매개변수로 배열 타입이 쓰일 경우, 이것은 qualified pointer 타입으로 변환된다. 이때 붙이는 type qualifier(const, restrict, volatile를 의미)는 {{{ [ ] }}} 안에 적힌 것을 사용한다. (이때 static 도 {{{ [ ] }}} 안에 들어갈 수 있지만 이건 또 다른 주제라 생략한다.)

{{{#!syntax cpp int f1(int arr[3]); int f2(int (*arr)[]); // 위의 함수 f1과와 같다.

int f3(int arr[const 3]); int f4(int * const arr); // 위의 함수 f3과 같다. }}} 이것 또한 다차원 배열의 경우에는 연쇄적으로 적용되지 않고 딱 한번만 적용된다. {{{#!syntax cpp int f5(int arr[3][4][5]); int f6(int (*arr)[4][5]); // 위의 함수 f5와 같다. int f7(int (**arr)[5]); // 위의 함수 f6, f7과 다르다. }}} 2차원 배열도 마찬가지이다. {{{#!syntax cpp int f8(int arr[6][7]); int f9(int (*arr)[7]); // 위의 함수 f8과 같다. int f10(int (**arr)); // 위의 함수 f8, f9와 다르다. }}} 이 규칙으로 인해서, C에서는 함수의 매개변수로 배열을 직접 복사해서 넘길 수 없다. 다르게 표현하자면, 함수의 매개변수로 배열을 사용할 수 없다. 배열보다 훨씬 더 복잡한 객체인 구조체도 함수의 매개변수로 사용할 수 있는 것을 고려하면, 이 규칙은 상당히 독특하다고 할 수 있다. [* 심지어 배열을 멤버 변수로 가지는 구조체라 할지라도, 배열 안의 내용을 포함하여 구조체 전체가 그대로 복사되면서 함수 내부로 전달된다. 이 점은 구조체의 대입 연산인 경우와 동일하다. ]

함수 호출과 관련하여, 배열 타입이 포인터 주소값으로 변환되는 규칙과 유사한, 기묘한 규칙이 한가지 더 존재한다. C언어에서 모든 함수 타입은 함수 그 자체에 대한 포인터 주소값으로 자동 변환된다(배열의 경우와 비슷하게, 이 규칙의 예외는 함수타입이 sizeof 연산자[* 이 규칙과 sizeof 연산자 그 자체의 제한에 의해, 함수 타입에 대해 sizeof 연산자를 사용하면 안된다. 만약 예외에 sizeof 연산자가 끼어 있지 않았다면, 함수 이름에 대해 sizeof 연산자를 사용하면 함수 포인터의 크기를 정상적으로 반환하게 되었을 것이다. 물론 실제로는 그렇게 동작하지 않고 컴파일 에러가 발생한다. ] 또는 & 연산자의 피연산자로 쓰일 경우이다). 그리고 함수 호출은 함수 타입이 아니라 함수에 대한 포인터 주소값을 통해 이루어진다. > C99 6.3.2.1 p4 > A function designator is an expression that has function type. Except when it is the operand of the sizeof operator or the unary & operator, a function designator with type ‘‘function returning type’’ is converted to an expression that has type ‘‘pointer to function returning type’’.

이에 대해서는 예시를 직접 보는 것이 더 이해가 빠를 것이다. {{{#!syntax cpp int f(void) { return 0; }

int (*fp)(void);

fp = &f; // 함수 포인터 변수 fp에 함수 f의 주소를 대입한다.

fp = f; // 함수 타입은 함수 그 자체에 대한 포인터 주소값으로 자동 변환되므로, 이것 또한 올바른 수식이다.

f(); // 함수 지시자(function designator) f를 통해 함수를 호출한다. 좀 더 정확한 동작은 다음과 같다. // 함수 지시자(function designator) f는 함수 f를 가리키는 포인터 주소값이 되고, // 이 주소값을 통해 함수를 호출한다.

(*fp)(); // 함수 포인터 fp가 가리키는 함수 f를 호출한다.

fp(); // 함수 포인터 fp가 가리키는 함수 f를 호출한다. // 함수에 대한 포인터 주소값을 통해 호출이 이루어지므로 // 함수 포인터에 참조 연산자를 생략하여도 정상적으로 동작하며 올바른 수식이다.

(***************fp)(); // 함수 포인터에 참조 연산자를 적용하면 함수 타입이 되는데, // 함수 타입은 다시 함수 포인터 값으로 자동 변환되고, 여기에 다시 참조 연산자를 적용하여... // 컴파일러가 허용하는 한도 내에서 *를 붙여도 여전히 함수 포인터 타입이 되고, // 이걸 통해 함수가 호출된다. }}}

주요 오개념: 포인터의 크기 및 상호 변환 관계

포인터를 처음 배울 때는 이것을 '단일한 크기의 정수형 주소값'[* 어떤 아키텍쳐에서는 사실이나, 그렇다고 항상 그러한 것은 아니다.]이라는 개념으로 지나치게 단순화시켜 받아들이기가 쉽다. 그래서 포인터를 어느정도 배운 다음에는 그 강력함에 취해 형변환(Type casting)을 사용한 포인터 테크닉을 남발하기 일수이다. 하지만 실제로는 포인터 개념 안에 상당히 복잡한 여러 고려사항들이 존재하고 있기에, 특정 아키텍쳐의 사례만을 가지고 단순화시켜 생각해서는 위험한 함정에 빠지기 쉽다.[* 독학으로 특정 아키텍쳐에서 디버거의 시행착오를 통해서만 학습하는 사람들이 이런 함정에 잘 빠진다. 이런 사람들이 디버거와 역어셈블 결과를 가지고 우기기 시작하면 납득시킬 방법이 없었다. 요새는 스택 오버플로우가 있어서 좀 나은 편.][* C언어는 다양한 아키텍쳐와 역사 위에서 성립된 언어이고, 지금도 상상을 뛰어넘는 요상한 구조의 칩들에서 사용 중이다.]

우리가 주로 접하게 되는 컴파일러와 컴퓨터 아키텍쳐들이 그러하기에 포인터 크기가 모두 동일하다고 착각하기가 쉬운데, 놀랍게도 C언어에서 포인터의 크기는 모두 같을 필요가 없다.[* 좀 익숙한 예를 들자면 8086을 CPU로 쓰던 MS-DOS 시절에는 near포인터가 2바이트, far 포인터가 4바이트였다.] 우리가 주로 사용하는 컴파일러나 아키텍쳐들이 정렬제한에 상대적으로 느슨한데다가 동일한 크기의 포인터를 사용하기에 잘 느끼지 못하는 부분이지만, 간혹 임베디드나 오래된 아키텍쳐에서는 이러한 부분들이 문제가 될 수 있다. 다음의 링크글에 이와 관련된 질문과 답들이 있다.

[4바이트인 이유는?] [* 쓰레드 제목과 내용이 전혀 다르다.]

한편, 포인터가 반드시 실제 메모리 공간을 가리킬 필요도 없고, 포인터를 정수형으로 변환한 값이 포인터 안에 저장된 이진 데이터와 동일한 값일 필요도 없다. C언어가 요구하는 것은 포인터가 정수형으로 변환될 수 있고, 다시 원래의 포인터형으로 돌아갈 수 있다는 것 뿐이다. 예를 들자면, 다음과 같은 식으로 논리 주소였던 포인터가 정수형으로 변환될 때에만 물리 주소에 배정되는 구조 또한 가능하다.

[포인터간 변환을 지원하는 C/C++ 메모리 모델]

이와 관련해서 고정관념을 깨는 예를 하나 들자면, C 표준에서는 널 포인터 상수로 정수 0을 사용하지만, 그렇다고 널 포인터의 내부 표현(기계 내에서 실제로 메모리 상에 저장되는 값)이 모든 비트가 0일 필요는 없다. 좀 더 자세히 설명하자면, 언어적으로는 정수 상수 0을 널 포인터 상수로 사용하고, 널 포인터와 정수 0을 비교하면 '같다'라고 취급되지만, 포인터 변수 p에 대해 p = 0과 같이 0을 대입했을 때, 포인터 변수 p의 실제 메모리 상의 내부표현이 00000000 00000000 00000000 00000000(32비트 포인터의 경우)가 아닐수도 있다는 것이다. null pointer 안에는 OS 또는 하드웨어가 사용하는 정보들이 담길 수도 있고, 혹은 0번지가 아닌 특정 메모리 주소를 가리킬 수도 있다. 따라서 포인터 변수에 널 포인터를 대입할 때 대입 연산자를 이용하지 않고 포인터 변환이나 memset 함수를 통해 직접 0의 비트열을 기록하거나 가변 인자 함수의 매개변수로 형변환 연산자 없이 바로 정수 0을 널포인터 대신 넘겨버릴 경우, 아주 특수한 환경에서는 문제가 생길 수도 있다(물론 이는 일반적으로 찾아보기 힘든 드문 경우이다). 자세한 내용은 다음 링크를 참조하라.

[FAQ의 Null Pointer 항목] [0과 '\0' 의 차이점...]

C가 다른 언어들에 비해 하드웨어 친화적이기 때문에 C를 배우는 사람들은 C와 실제 하드웨어에서의 동작을 1:1로 대응하여 생각하는 경향이 있는데, C도 추상화된 고급 언어(High-level language)이고, 따라서 C언어 코드와 기계어 번역 결과물이 일대일로 대응되지 않음을(컴파일러가 프로그래머의 의도에 맞게 최적화를 거칠 수 있음을) 염두에 둘 필요가 있다.

포인터의 크기 및 상호 변환 관계에 대한 일련의 규칙들은 다음과 같다.

* 모든 포인터형은 void *형으로, 변환될 수 있으며, 그것을 원래의 포인터형으로 변환했을 때 동일한 값이어야 한다.
* 한정자(qualifier)가 붙지 않은 포인터형은 한정자가 붙은 포인터형과 서로 호환/변환되며, 붙지 않은 것과 붙은 것은 비교했을 때 같다고 취급한다.
* null pointer는 어떤 타입의 포인터이든 동일하게 취급된다. 즉 비교했을 때 서로 같다고 판정한다.
* 어떤 포인터값이 다른 타입의 포인터값으로 변환될 수 있긴 하나, 그 결과값이 올바로 정렬되어 있지 않다면 그 동작은 정의되어 있지 않다. (즉 변환 시도 그 자체만으로도 문제가 될 수 있다!)
 * 이 '올바로 정렬됨'의 성질은 추이적(transitive)이다. ~~주소값의 정렬제한 단위가 약수와 배수 관계가 되는거라 자명하다~~
* 모든 함수의 포인터는 상호 변환될 수 있다. 그러나 잘못된 타입의 포인터로 함수를 호출했을때의 동작은 정의되어 있지 않다.
* void*는 char*과 같은 정렬제한과 내부표현을 갖는다.
* 한정자에 상관없이, 서로 호환되는 데이터형들(compatible types)끼리는 동일한 정렬제한과 내부표현을 갖는다.
* 모든 구조체에 대한 포인터들끼리는 동일한 정렬제한과 내부표현을 갖는다.
* 모든 공용체에 대한 포인터들끼리는 동일한 정렬제한과 내부표현을 갖는다.
* 그 외의 포인터들끼리는 동일한 정렬제한과 내부표현을 가질 필요가 없다.

위의 내용들에 따라 다음과 같은 해석이 가능하다.

* 포인터의 크기가 int형 또는 해당 아키텍쳐의 최적 word의 크기와 같을 필요는 없다. int형보다 더 크거나 더 작을 수 있고, 여러가지 크기를 가질 수도 있다.
* void*형과 char*형은 다른 포인터형이 가진 정보를 모두 담을 수 있을 정도로 충분히 크고, 1byte 단위로 주소값을 기록할 수 있어야 한다.
 * void*형은 범용 포인터로서 사용이 가능하다. 그리고 아마, char*형도. 모든 포인터 타입은 void *타입으로, 그리고 다시 void *타입에서 원래의 타입으로 되돌아갈 수 있다.
 * 그러나 void *형에 저장된 임의의 주소값이 항상 특정 포인터형으로 변환될 수 있는 것은 아니다. 해당 주소값의 객체에 접근하는 행위 뿐만 아니라 주소값을 변환하는 행위 자체도 문제가 될 수 있다.
 * 실질적으로, char*형은 정렬제한에 자유롭게 모든 메모리 주소를 가리키는 데 사용할 수 있다.
  * 이것이 읽기와 쓰기 동작의 성공을 항상 보장하는 것은 아니겠지만, 문제가 되지 않는 경우라면 모든 자료형에 대해 char*형을 통해 1바이트 단위로 접근할 수 있다고 기대할 수 있다.
* 포인터 타입들끼리는 함부로 변환하는 것이 위험하다. 단 허용되는 몇몇 경우가 있다.
* 서로 호환되는 포인터형들끼리는 변환하는 것이 안전하다. (같은 크기에, 같은 내부표현을 갖는다)
* 구조체 포인터 타입들끼리는 변환하는 것이 안전하다. (같은 크기에, 같은 내부표현을 갖는다)
 * 따라서 구조체 포인터를 통해 객체지향을 흉내낼 수 있다.
* 공용체 포인터 타입들끼리는 변환하는 것이 안전하다. (같은 크기에, 같은 내부표현을 갖는다)
* 함수 포인터 타입들끼리는 변환하는 것이 안전하다. 단 그걸 잘못된 타입으로 호출하는건 위험하다.
 * 어떤 함수 포인터 타입이든 범용 함수 포인터로서 사용하는 것이 가능하다. 단, 호출하지 않는다는 가정 하에서만.
  * 이걸 이용해서 C언어에서 가상함수 구현이 가능해진다.
* 함수 포인터와 void* 포인터는 호환되지 않을 수도 있다.
 * POSIX 등의 다른 표준이 덧붙여진다면 호환될 수 있다.

포인터의 변환에 위와 같은 제한이 걸리는 이유를 이해하기 위한 핵심 개념 중 하나는 memory alignment이다. 어떤 아키텍쳐에서는 각각의 데이터형들에 대해 그 메모리상의 주소값이 특정 N의 배수이기를 요구한다. 이를 염두에 두지 않고 함부로 형변환을 했다간 CPU 수준에서 bus error가 발생할 수 있다.

이를 이해하기 위한 또 다른 중요한 개념은 Trap representation이다. 간단하게 말해서, 특정 범위의 표현값은 사용하는 것만으로도 문제를 일으킬 수 있다.

C++에서는 가상 멤버 함수에 대한 포인터로 인해 더더욱 포인터 크기가 달라질 수 있다. [to member functions are very strange animals]

단, POSIX 2008에서는 위와는 다르게 함수 포인터와 일반 포인터가 서로 호환되기를 요구하며(dllsym()과 같은 경우가 그러하다), 더 나아가 모든 포인터의 크기가 서로 같기를 요구한다.

위와 같은 내용들로 인해 포인터 사이의 형변환을 사용할 때는 이것이 허용되는 경우인지를 먼저 따져볼 필요가 있다. C++에서의 경우와 마찬가지로 기본적으로 포인터에 대한 모든 명시적인 형변환은 evil이라고 가정하고, 허용되는 경우만을 따져 필요한 경우에만 활용하는 것이 바람직하다.

주요 오개념: 스택(Stack)과 힙(Heap)

정말 놀랍게도 C언어 자체에는 스택과 힙 개념이 포함되어 있지 않다. C11의 Final Draft인 n1570에는 stack과 heap라는 단어 자체가 아예 나오지 않는다. 당연히, '지역변수는 stack에, malloc의 결과값은 heap에 위치한다'라는 널리 알려진 규칙 또한 존재하지 않는다. C언어는 심지어 함수 호출을 반드시 stack을 통해 구현하기를 요구하지 않는다. 스택과 힙을 사용하는지 여부는 C언어에서 요구하는 것이 아니며 CPU와 OS의 특성에 따라 결정된다.

더 자세한 내용은 다음 링크를 참조하라.

[C need a stack and a heap in order to run?]

C언어의 포인터를 설명할 때 스택과 힙을 설명하는 것은 다소 과도한 설명일 수 있다. 왜냐하면 대부분의 질문자에게 필요한 내용은 스택과 힙이 아니라 식별자의 통용 범위(Scope of identifiers)와 객체의 지속시간(Storage duration of objects)이기 때문이다. 해당 주제를 이해할 때에는 식별자(identifier)와 객체(object), 통용 범위(Scope)와 지속 시간(Storage Duration)의 차이를 구분해서 이해해야 한다. 학습 단계에서는 이를 서로 혼동하는 경우가 많다.

예를 들어 static이 아닌 함수 내의 지역변수를 함수 외부에서 접근하면 안되는 이유는 함수가 종료되면서 해당 객체의 수명(lifetime)이 끝나기 때문이다. 그러나 많은 사람들이 이를 스택과 힙의 개념으로 설명하려 하는데, 이는 실제 하드웨어가 어떻게 움직이는지에 대한 이해를 제공하기는 하나, 해당 내용 자체가 C언어에 포함된 내용은 아니며 따라서 추상화된 개념인 C언어 그 자체의 내용을 학습하는 데에는 부적합하다. 최소한, 통용 범위(Scope)와 지속 시간(Storage Duration)의 개념을 먼저 알고 나서 그 다음으로 아키텍쳐에서 실제 구현되는 방식의 예로 스택과 힙을 배워야 한다.

중괄호 스타일

C언어는 스코프를 지정할 때 중괄호 쌍을 { } 사용한다. C언어는 whitespace(공백, 탭, 리턴 문자열)가 의미 없는 언어라서[* 정확히는 화이트 스페이스 문자들인 뉴 라인, 탭, 스페이스를 구분하지 않고 아무 whitespace 하나 이상은 공백 하나로 계산된다. 예외는 C/C++의 문법의 일부가 아닌 매크로로서, 무조건 뉴 라인을 이용해서 매크로의 끝을 구분지어야 하며 여러 줄에 걸쳐 작성할 경우 뉴 라인 이전에 역슬래시를 하나 추가해야 한다.] 여는 중괄호를 엔터를 쳐서 다음 줄에 놓는 방법과, 이전 표현 바로 옆에 놓는 방식 두 가지 모두 가능하다. 이런 두 가지 스타일을 각각 Allman[* 한때 메일서버계의 IE로 불리던 sendmail의 저자이며, 게이 프로그래머로 유명한 Eric Allman의 이름에서 따옴. BSD style이라고도 한다.] 스타일과 K&R 스타일[* Kernighan&Ritchie의 <The C Programming Language>에서 쓰였기 때문.] 이라고 한다.

소스로 보자면 다음과 같다.

* K&R 스타일

아래와 같이 while, if 문 등의 옆에 여는 괄호가 붙어 있다. {{{#!syntax cpp int main() {

   while (x == y) {
       something();
       somethingelse();
       if (some_error) {
           /* the curly braces around this code block could be omitted */
           do_correct();
       } else
           continue_as_usual();
   }
   finalthing();

} }}}

* Allman 스타일

아래와 같이 while, if문 밑에 여는 괄호가 있다. {{{#!syntax cpp int main () {

   while (x == y)
   {
       something();
       somethingelse();
       if (some_error)
       {
           /* the curly braces around this code block could be omitted */
           do_correct();
       } 
       else
           continue_as_usual();
   }
   finalthing();
   ...

} }}}

* Horstmann 스타일

K&R과 Allman을 합친 스타일이다. {{{#!syntax cpp int main() { while (x == y)

   {   something();
       somethingelse();
       if (some_error)
       {   /* the curly braces around this code block could be omitted */
           do_correct();
       }
       else
           continue_as_usual();
   }
   finalthing();

} }}}

각각의 스타일은 장단점이 있는데, K&R 스타일은 여는 중괄호에 엔터를 치지 않기 때문에 적은 줄 수에 더 많은 내용을 담을 수 있는 반면 중괄호 블록이 눈에 잘 안 들어오는 단점이 있다. 반면에 Allman 스타일은 여는 위치와 닫는 위치가 같기 때문에 중괄호 블록이 명료한 반면에 수직적으로 많은 공간을 차지하는 단점이 있다. 그래서 지문을 많이 넣고 싶지 않은(=되도록 얇게 만들고 싶은) C언어계 책의 9할쯤은 전부 다 K&R 스타일이다. 반면 실제 코드의 스타일은 4할-4할 정도. 예제는 K&R 스타일로 쓰고서도 본문 중에 실제 작업에서는 Allman 스타일을 쓴다고 밝히는 저자도 있다. 2할은 기타 스타일. 그 중 Horstmann 스타일은, 중괄호 블록이 명료하게 보인다는 Allman의 장점과 적은 공간을 차지한다는 K&R의 장점을 합친 형태이다.[* {{{for(i=0;i<n;i++) { work(); } }}} 이런 식으로 쓰는 사람들도 있기는 있다. for과 if 등은 한 함수를 실행시킬거면, 중괄호를 안쳐도 되긴 하다. 다만, 들여쓰기 잘못하면 피바람 분다.(...) 함수가 한 개든 여러 개든 그냥 전부 중괄호를 치는 습관을 들이자.]

그 외 다양한 스타일은 [문서]를 참조하자.

참고로, Clang 컴파일러의 일부인 clang-format을 사용하면 소스 코드의 스타일을 일괄적으로 정리하고 유지할 수 있다.

문법 상 주의점

아래와 같은 방식으로 코딩하거나 교육하는 사람이 있을 경우 주의할 필요가 있다.

* gets/scanf 함수를 사용한다: gets 함수는 얼마나 입력받을지 미리 정할 수 없어 버퍼에 들어온 모든 입력을 써버리는 치명적인 문제가 존재한다. 버퍼 오버플로 참조. 현재는 표준 라이브러리에서 퇴출되어 더 이상 사용할 수 없다. fgets, gets_s를 쓰도록 하자.[* 다만 fgets는 입력의 처리가 gets와 조금 다르고, gets_s는 GCC와 LLVM/Clang에서는 지원하지 않으니 잘 알아보고 쓸 필요가 있다.] C 입문서에서 흔히 사용되는 scanf도 마찬가지로 문자열을 읽어들일 때 버퍼 오버플로 문제를 야기할 수 있으며 이쪽도 대안으로 VC++ 컴파일러의 scanf_s가 존재한다. 같이 쓰이는 puts와 printf의 경우에는 출력 함수라 버퍼 오버플로 문제가 존재하지 않는다.[* 다만 printf의 경우 별도로 포맷 스트링 문제가 존재한다.]
* short 자료형을 특별한 이유 없이 사용한다: 32비트 아키텍처 이상 CPU는 자료형의 팩/언팩 작업 때문에 short 자료형의 퍼포먼스가 int형보다 오히려 더 낮으며 오버플로 위험만 높인다.
* 간단한 함수를 모조리 #define 매크로로 작성한다: #define은 전처리 시점에서 특정 코드를 미리 정해놓은 코드로 통째로 치환시키는 전처리문으로 실행 시 별도로 함수를 불러오는 오버헤드가 발생하지 않는다는 이점이 있다. 이로 인해 과거 컴퓨터 성능이 좋지 못하던 시절에는 간단한 함수를 매크로로 작성하는 방법으로 프로그램의 속도 향상을 꾀할 수 있었다. 그러나 현대에는 컴퓨터 성능이 상당히 좋아졌기에 이 정도의 오버헤드는 크게 문제되지 않는다. 또한 #define은 정해진 코드를 통째로 치환하기 때문에 예상치 못한 동작을 보일 수 있으므로 사용에 주의가 필요하다.

문법

[include(틀:상세 내용, 문서명=C언어/문법)]

C언어 관련 자료 소개

The C Programming language

* 원 저자: 데니스 리치 & 브라이언 커니핸
* 번역서: (Kernighan의) C언어 프로그래밍
* 번역자: 김석환(2005년) → 김석환, 박용규, 최홍순(2016년 수정2판)
* 출판사: 대영사(2005년) → 휴먼사이언스(2016년 수정2판)

다른 언어들의 경우 여러 교재들이 서로 경쟁하는 추세이지만, 유독 C의 경우는 창시자인 데니스 리치와 브라이언 커니핸이 쓴 The C Programming language 2nd Edition 이 독보적인 위치를 점하고 있다. 이 두 사람의 이니셜을 따서 보통 K&R 이라 칭하거나[* 주의해야 할 것은, K&R C와 착각하면 안된다. 인터넷에서 검색하면(주로 C 역사) K&R이라는 단어를 K&R C를 지칭하며 사용하는 경우도 가끔 있다. K&R C는 ANIS C 이전 버전의 '언어'이고, 현재 이 페이지에서 칭하는 K&R 은 C 교재이며, K&R 1st Edition은 표준화 이전 K&R C의 교재이고, 본문의 K&R 2nd edition은 표준화된 ANSI C(C89) 교재이다.], 제목을 축약해서 TCPL이라고 부른다. 이 책이 나온 지 매우 오래 되긴 했지만, C언어가 별로 변화가 없는 언어인 데다가 많은 사람들이 C89/90을 표준으로 따르고 있기 때문에 낡은 책이라도 사용에 지장이 없다.

또한, 분량은 전부 다 합쳐도 300쪽도 안 된다. 그것도 Appendix 부분을 빼면 순수 튜토리얼은 200쪽도 안되며, 언어 자체뿐 아니라 프로그래밍에 대한 여러가지 깨알같은 조언까지 다 포함이 되어있다. 보통, 일반적인 프로그래밍 언어 교재가 작아도 500쪽을 가볍게 넘어가고, C++의 창시자인 비아르네 스트로우스트루프(Bjarne Stroustrup)가 K&R과 비슷한 네이밍으로 쓴 The C++ Programming language의 경우 1300쪽에 육박하는 것[* 3차 개정판 1000쪽, C++11로 업데이트된 4차 개정판에서 1300쪽]에 비하면 엄청나게 짧은 분량이다.

K&R이 바이블 취급을 받는 이유는 단순히 역사적이고 C 언어 창시자가 쓴 책이기 때문만은 아니다. C 언어가 겉으로는 심플해보이지만 워낙 숨은 함정이 많은 언어라 저런 부분들을 제대로 다 짚고 넘어가야 하는데, K&R 이외에 제대로 완전하게 짚어주는 교재가 거의 없다. 보통의 고수준 언어들처럼 접근해서 직관적으로 '이렇게 하면 된다.' 식으로 설명을 하는 교재들이 많은데, 사실 온갖 정의되지 않은 행동(Undefined Behaviour)과 구현 특화 상세내역(Implementation-specific Details), 하드웨어에 의존적인(Hardware Dependent) 함정들이 곳곳에 숨어있는 C 언어는 '이렇게 하면 된다.' 보다 '이렇게 하면 안된다.'를 중점적으로 설명해야 하는 언어이다. 특히 UI나 퍼포먼스 중심으로 돌아가는 어플리케이션 영역이 메인인 언어들과는 달리, OS와 딱 붙어서 보안이나 안정성 및 호환성이 중시되는 시스템 프로그래밍 영역이 메인인 언어라 어느 정도 깐깐하게 접근하는 게 맞는 언어다.[* 그렇기에 더더욱 초심자에게는 어울리지 않는다.] C언어를 C++를 익히기 전의 워밍업 정도로 취급하는 사람들에게는 저런식으로 설렁설렁 쉽게 써진 C언어 교재들도 좋은 평가를 듣는 경우도 많지만, 본격적으로 C 프로그래밍을 하려는 사람들 사이에서는 해외 기준으로 K&R과 C Programming: A Modern Approach(일명 K&K)[* 저자의 이름인 킹(K. N. King)의 앞글자를 땄다.] 두 가지 정도만이 제대로 된 교재 취급을 받는다. K&R이 나온 지 오래된 책이기는 하지만 중요하고 핵심적인 사항들은 오늘날에도 변함없이 적용되는 내용들이다. K&R을 나온 지 오래 된 책이라고 무시하기엔 좋은 내용들이 굉장히 많다.

그러나, 분량이 많은 교재들은 그만큼 친절하고 자세한 설명을 동반하여 비교적 술술 읽히는 것에 반해, K&R 은 짧은만큼 매 문장 하나하나[* 실제로 아주 중요한 내용이 구석탱이에 딱 한줄 써있는 경우가 많다.]의 중요도가 높기 때문에 프로그래밍 초심자에게 추천할 만한 책은 아니라는 주장도 있다. ~~그런데, The C++ Programming Language는 난이도도 TCPL 보다 더 높다.~~

게다가 K&R이 좋은 교재인 건 맞지만 오늘날 K&R을 교재로 추천하기가 마냥 좋지만은 않은 이유 중 하나도 책이 너무 오래되어서 초창기 C 언어의 유행이나 트릭 정도만을 담고 있고, 그 이후의 여러가지 패러다임의 변화 등을 반영하지 못하고 있기 때문이다. 어차피 저런걸 모두 담고 있는 교재가 없긴 하지만, 더하고 덜하고 정도의 차이는 있게 마련이고, 그 차이는 나중의 삽질과 시간낭비로 반드시 뿌린 대로 거두게 된다. 정 K&R을 첫번째 교재로 삼기가 어렵다면 다른 데서 C의 기초를 먼저 배우고 복습 차원으로 K&R을 보는 것도 좋다. 그렇게 하면 K&R의 내용 중 구식인 것과 아닌 것을 스스로 가려낼 수 있게 될 테니까.

C Programming: A Modern Approach

* 원 저자: K. N. King
* 번역서: (없음)
* 번역자: (없음)
* 출판사: (없음)

The C Programming language와 함께 C 표준을 준수하면서 세계적으로 인정받은 바이블격 도서 중에 하나다. 내용 분량이 TCPL보다 2배 이상이라 TCPL에 비하면 술술 읽히는 편이지만 아직까지도 한국어 번역서가 없어서 C언어 첫 번째 기본서로는 독해하기 어려울 수 있는게 단점. 그 대신 수능 영어 독해할 수 있는 실력이면 그럭저럭 읽을 수 있다.

Modern C

* 저자 : Jens Gustedt
* 출판 : Manning Publication (미출시)

2019년 기준 C11/C18 표준과 현업에서의 C 프로그래밍 스킬 및 트렌드에 대해 입문자 수준부터 전문가 수준까지 깊이있게, 정확히 체계적으로 설명하고 있다. 2019년 12월 출시 예정. 인터넷에서 pdf로 받을 수 있다.

C언어 펀더멘탈

* 저자: 전웅
* 출판: 한빛미디어

한국인이 쓴 책 중에서는 C99 표준을 제대로 정확하게 설명하는 몇 안되는 중급서. 실제 프로그래밍 도구의 사용법이나 하드웨어 특화적인 내용을 설명하지는 않기 때문에 직접적으로 곧바로 프로그래밍 스킬을 늘려주지는 않지만, C언어 전반에 대해 단단한 기반 지식을 형성해주는 책이라 할 수 있다. C언어 구석구석의 내용까지 충분한 설명 및 예제와 함께 다루기 때문에 구해서 읽을 수만 있다면 C언어 자체에 대한 이해에 큰 도움이 될 것이다. 일단 이 책의 범위까지만 익히면 암호문 같던 컴파일러의 에러 메시지가 해석이 되고, 컴파일러 매뉴얼의 각 항목들이 왜 그렇게 쓰여졌는지가 이해가 된다. 그러나 유감스럽게도 2019년 현재는 절판되어 있다. 저자는 한때 han.comp.lang.c 뉴스그룹과 KLDP에서 답변자로서 왕성한 활동을 보인 바 있다.

서평을 검색해보면 호불호가 극명하게 갈리는데, 이는 해당 책의 관점 때문이다. C표준의 관점에서 C언어를 설명하고 있기 때문에 용어 자체가 생소한 것들이 많은데, 컴파일러 대신 구현체(implementation)와 같이 일반적으로 잘 쓰이지 않는 용어를 사용한다던가, 정의되지 않은 동작(Undefined behavior)과 같이 사전지식 없이는 오해되기 쉬운 표준의 용어들을 빈번하게 사용하기 때문에 적절한 사전지식이 없다면 글 자체를 읽기가 어렵다. 한편으로는, 널리 사용되지 않는 오래된 아키텍쳐의 범위까지도 포괄하기 때문에 일반적인 프로그래머들이 받아들이기 힘든 부분들도 많이 포함되어 있어, 이해하기 어렵다거나 불필요한 내용이 너무 많다는 악평들이 많다. 만약 알기 쉬운 입문서를 기대하고 이 책을 읽는다면 큰 실망을 하게 될 것이다.

반면에 중급자의 수준에서 C언어의 깊이있는 부분을 제대로 알고 싶다면, 이만큼 자세하고 알기 쉽게 설명한 책도 달리 없다. 적어도 한국에서는, C FAQ 온라인 번역문 이외에는 이와 비슷한 내용을 접하기조차 쉽지 않다. C++의 경우에는 언어가 복잡한 만큼 오히려 Effective C++과 같은 중간 교량 역할을 하는 책들이 있지만 순수 C는 오히려 그런 책들이 드물고, 따라서 이 책의 가치는 더더욱 높을 수밖에 없다.

C Primer Plus 6th Edition

* 원 저자: Stephen Prata
* 번역서: C 기초 플러스 6판
* 번역자: 윤성일, 이선민, 조혜란
* 출판사: 성안당

K&R이나 K&K같은 두 바이블 양대 산맥의 도서만큼은 아니더라도 C 최신 표준이 충실하게 반영된 도서이다. 새로운 표준이 등장할 때마다 여러 차례 개정되어서 최신 개정판 기준으로 1000페이지가 넘는다. C언어 학습용 첫 번째 도서로써는 두꺼운 분량이라는 진입 장벽이 있지만 C11 표준을 다루는 몇 안 되는 번역서라서 이를 제대로 배우고자 하는 학습자에게 십중팔구 추천서로 거론되는 책이다. 한국에서는 'C 기초 플러스'라는 번역서로 현재 C11 표준까지 반영된 6판까지 출판되었다. 하지만 내용 자체가 너무 길어서 장황하게 보이기 쉬운 탓인지 입문자에겐 중도 포기하기 쉬운게 단점이다.

열혈 C 프로그래밍

* 저자: 윤성우
* 출판: 프리렉(2003년) → 오렌지미디어(2010년)

한국 한정으로 유명한 C언어 도서 중에 하나로, 초판 당시엔 '열혈강의 C 프로그래밍'이었으나 개정되면서 책 이름도 '강의' 글자만 빠진 이름으로 바뀌었다. 컴퓨터 하드웨어 구조에 대한 기초 지식이 없는 입문자에게 추천하는 책으로 널리 알려져 있다. 그런 독자를 타겟으로 집필된 기본서다보니 어려운 개념은 빠져있는데 여기까지는 초보자용 C 기본서로써 납득할 수 있는 부분이지만, 하필 중요한 개념 중에서도 오개념이 존재하는데다(대표적으로 Call by Reference) 소개하는 문법마다 C 표준인지 비표준인지 명확하게 구분하지 않아 전문가들에겐 혹평을 받는 책이기도 하다. C 표준을 제대로 공부하고 싶은 학습자에겐 추천하지 않지만, 입문자에겐 이해하기 쉽게 서술되어 있어서 C언어 첫 번째 교재로써는 나쁘지 않다. 그렇기 때문에 이 책으로 입문하는 대신 제대로 배우고 싶다면 다른 좋은 기본서 한 권 더 사서 복습 겸 공부하는 것이 좋다.

이것이 C 언어다 서현우의 C 프로그래밍 정복

* 저자: 서현우
* 출판: 한빛미디어

뇌를 자극하는 C 프로그래밍의 저자가 집필한 C 입문서. 입문서이기 때문에 열혈 C 프로그래밍과 비슷한 한계를 지니고 있지만 C언어 첫 번째 교재로서 많이 추천받는 교재로 평가받고 있다. 또한 2019년부터 책의 내용은 거의 그대로 유지한 채 가독성과 가시성을 높인 개정판 '혼자 공부하는 C언어'가 출판되고 있다.

터보 C 정복

* 저자: 임인건
* 출판: 가남사(1999)

PC통신 시절 국내한정으로 C언어 레퍼런스 중 하나만 꼽으라고 하면 열의 다섯은 주저없이 이 책을 꼽았던 명저로, 특히 배열과 포인터에 관한 설명은 당시 국내도서 중에서는 비교대상이 없다는 평가를 받았던 책이다. 1299페이지라는 압도적인 두께가 흠이긴 하지만, 그만큼 물샐틈 없이 설명을 해주고 있다.

다만 지금 시점에서 읽겠다면 시대적인 한계와 Turbo-C를 기준으로 쓰여진 책이라는 점은 고려를 해야 할 것이다. 1991년에 벌써 5판이 발매된 너무 오래된 책이기도 하거니와, 요새처럼 C언어 표준 ~~드래프트~~ 문서를 곧바로 받아볼 수 있는 시대에는 그 자세한 설명도 의미가 퇴색될 수밖에 없다.

저자인 임인건씨는 도스시절 한라프로라는 인기 한글 라이브러리를 만들었던 사람으로, 그의 PC통신 아이디인 터보이빨(turbo28)과 프로그래머 십계명으로도 유명하다.

기타 관련 자료 소개

* C언어 표준문서 최종 초안들: C언어 표준 문서는 [198스위스프랑(20~30만원 정도)에 구입]하거나 혹은 그 외의  [경로로] 구할 수 있다. 하지만 학생 수준에서는 적지 않게 부담이 되기 때문에 이용할 수 있는 것들이 바로 인터넷상에 공개된 최종 초안(Final draft)들이다. 최종 초안들이니만큼 완전히 같지는 않겠지만, 공부 또는 기본적인 사항들에 대해 참고하기 위해서라면 충분한 수준이다.
   * [[7]]
   * [+ TC1 + TC2 + TC3(N1256)]
   * [for the C99 standard]
* : [FAQ]: comp.lang.c 뉴스그룹의 주요 질문과 답변을 Steve Summit가 정리한 문서. 입문서의 범위를 완전히 공부한 이후 중급자로 가는 다리 역할을 해 줄 수 있는 문서이다. 신성국씨가 정식으로 허가를 받고 번역한 문서가 있다. [FAQS 한국어 번역판]
* [Between ISO C and ISO C++]: C99와 C++98과의 차이점을 David R. Tribble가 정리한 문서. 이 또한 신성국씨의 번역본이 있다.[C와 ISO C++의 차이]
* [언어 코딩 도장]
* [[8]]
* [보드게임] [링크]
* [레퍼런스 매뉴얼 번역본]: 전웅씨가 K&R 2판 부록의 레퍼런스 매뉴얼(ANSI C또는 C89의 요약본)을 번역하고 해설을 첨가한 번역본. C89를 기반으로 하고 있기에 다소 오래된 내용이지만, 분량이 72쪽으로 적은 편이라 표준문서가 어떤 형식으로 되어있는지 익히는데 큰 도움이 된다. 

C언어용 개발 도구들

* 비주얼 스튜디오 [공식 사이트]
마이크로소프트에서 만든 IDE. 여기에 포함된 C++ 컴파일러는 MSVC, 또는 Visual C++라고 한다.[* 정확히 말하자면 cl.exe라는 실행파일이다.] C/C++이 아니라 C++라는 점에 유의. C는 정식으로 지원하는 언어가 아니다. MSVC가 C를 지원하는 이유는 그저 C++가 C의 문법도 어느 정도 포함하는 언어이기 때문이다. 또한 C99 이후로는 C가 C++와 다른 길을 걷게 되었기 때문에, 비주얼 스튜디오에서는 C99/C11/C18의 일부 또는 전부를 지원하지 않는 것이다. MS는 C를 Internal Language로 규정하여, 윈도우나 기타 MS 소프트웨어 개발에 사용하기는 하지만 엔드 유저(프로그래머 포함)를 위해 지원하지는 않는다. 비주얼 스튜디오 2019에서도 C11, C18은 커녕 C99조차 일부 기능을 지원하지 않는다.[[9]] 
* CLion [사이트]
IntelliJ IDEA로 유명세를 날리고 있는 JetBrains에서 개발한 C/C++ IDE이다. 인터페이스가 직관적이고 다양한 종류의 컴파일러로 크로스플랫폼 개발이 가능하다는 장점이 있다. CMake 기반으로 프로젝트를 생성하며, 유료 구독권 형식으로 판매되고 있기 때문에 월/년마다 정기 구매해야 한다. 윈도우 환경에서 WSL에 접속하여 리눅스 기반 컴파일을 할 때 가장 편리한 툴이기도 하다.
* Xcode [사이트]
애플에서 직접 제작한 IDE. 예전에는 GCC를 그대로 가져다[* 사실은 애플이 GCC를 가져다가 사용하는 대신, 자체적으로 GCC에 추가한 기능 일부를 다시 GCC 측에 돌려주기로 약속했었다.] 썼지만, 요즘은 LLVM이라는 새 컴파일러 셋으로 이주하였다.[* 게다가 LLVM은 이것을 처음으로 고안한 사람을 애플이 스카우트하면서 거의 애플 소유의 프로젝트가 되었다. 하지만 스티브 잡스의 애플 복귀 이후 애플이 진행하는 대부분의 소프트웨어 프로젝트가 오픈 소스인지라 이것도 역시 오픈 소스로 계속 진행중. ~~오라클 보고 있나?~~] C, C++, Objective-C, Swift 등의 언어를 컴파일할 수 있다. macOS용 애플리케이션이나 iOS용 앱을 제작하는 데는 필수이다.
* 넷빈즈 [사이트]
오픈 소스 IDE 중 하나로, 원래 오라클에서 배포하였으나 2016년에 아파치 소프트웨어 재단으로 소유권이 이전되었다.
* 이클립스 CDT [사이트]
오픈 소스 IDE인 이클립스의 C/C++ 개발용 플러그인이다.
* Code::Blocks [사이트]
C/C++/Fortran용 오픈 소스 통합 개발 환경이며 라이선스는 GPL 3.0 버전을 따르고 있다. 2017년 12월 이후로 업데이트가 안 되고 있다.
* Bloodshed Dev-C++ [사이트]
GPL 라이선스를 따르는 오픈 소스 IDE. 2006년 이후로는 소식이 없다. 설치 속도가 빠르고 편리하지만 리소스 에디터가 없다. 설치할 때 기본적으로 같이 깔리는 컴파일러는 MinGW+GCC.
* Orwell Dev-C++ [사이트]
오웰(Orwell)이 Bloodshed Dev-C++ 4.9.9.2 소스로 개발하고 있는 IDE이다. 2015년 이후로는 무소식.
* Borland C [페이지]
볼랜드에서 개발한 유료 IDE. MS-DOS 시절에는 터보 파스칼과 함께 빠른 컴파일 속도로 인기가 많았으나, 윈도우의 시대가 오면서 점차 비주얼 스튜디오에 밀리게 되었다. 단순 컴파일러는 현재 무료로 다운로드 가능하다. 볼랜드가 개발언어 쪽만 전담하는 코드기어라는 자회사를 설립하면서 모든 권한을 넘겼는데, 코드기어가 엠바카데로와 합병되면 홈페이지가 여러 차례 이동되었다. 참고로 델파이의 개발도 볼랜드→코드기어→엠바카데로의 순서로 넘어갔다.
* Turbo C++ [페이지]
볼랜드 Turbo C++를 Windows 7, 8, 10을 위해 확장한 버전. 그 외에는 소식이 없다.
* Watcom C [사이트]
도스 시절 끝무렵에 DOS/4GW라는 메모리 확장 프로그램과 함께 잠깐 동안 쓰였다(8.5 버전부터 포함되었다). MS-DOS의 640KB 메모리 한계를 넘어 64MB까지의 메모리를 손쉽게 사용할 수 있게 해 주었기 때문이다. 그 당시의 게이머들이라면 실행 초기에 DOS/4GW라고 뜨던 안내문을 기억할 것이다. 공식 사이트가 위키 형식으로 되어 있다. Open Watcom Public License라는 라이선스를 따른다.
* Wipi-C
* LCC [영문]
A Retargetable C Compiler: Design and Implementation라는 책에 소스 코드가 실려있는 C 컴파일러. 데이브 핸슨(Dave Hanson)과 크리스 프레이저(Chris Fraser)가 만들었다. 아래에 설명하겠지만 이를 기반으로 하는 컴파일러가 몇몇 존재한다.
* lcc-win [[10]]
위의 LCC를 바탕으로 한 윈도우용 C 컴파일러. 제이컵 나비아(Jacob Navia)가 만들었다. 월간 마이크로소프트웨어를 통해 국내에 소개된 적이 있다.
* Pelles C [사이트]
위의 LCC를 기반으로 스웨덴의 펠레 오리니우스(Pelle Orinius)가 개발한 컴파일러. 윈도우 전용의 프리웨어 통합 개발 환경이다. MS와는 다르게 C99는 물론이고, C11도 완벽 지원하는 컴파일러를 제공한다. 게다가 툴이 가볍고 무료다.
* ICC (Intel C++ Compiler) [영문] [사이트]
인텔에서 자기들이 만든 CPU에 최적화된 코드를 만들어 낼 수 있도록 직접 제작한 컴파일러 스위트. 사용 설명서 시작 부분에 '인텔에서 제작하지 않은 CPU에서 구동 시 최적화된 성능을 보장할 수 없습니다'라고 적혀 있다.[[11]] C/C++ 및 Fortran 컴파일러를 제공한다. 윈도우에서 설치 시, 비주얼 스튜디오에 애드온 형태로 설치가 돼서 비주얼 스튜디오 내에서 기존 컴파일러 대신에 사용할 수 있다.
컴파일러 이외에도 어셈블리어 단위로 한땀한땀 손으로 최적화한 수치해석 라이브러리[[12]]와 다수의 컴퓨터가 동시에 컴퓨팅을 할 때 사용되는 MPI 라이브러리, 코드의 성능을 분석해주는 도구[[13]], 코드 최적화를 도와주는 도구[[14]], 성능에 악영향을 주는 에러를 찾아주는 도구[[15]], 등을 하나로 묶어서 [Parallel Studio]라는 이름으로 판매한다. 인텔 CPU와 인텔 가속기를 수만 개씩 사용하는 슈퍼컴퓨터 등에서는 매우 자주 쓰이는 유용한 도구.
Intel Parallel Studio는 학생 대상으로 무료로 사용 가능(윈도우용은 비주얼 스튜디오 통합 포함)하고, 수치해석 라이브러리는 일반인 대상으로 무료 사용 가능하다.

관련 문서

* WIPI


여담

C언어의 다음 언어?

B 언어 기반으로 C 언어가 탄생하면서 한때는 프로그래머 유머로 'C' 다음 언어가 과연 'D'일지 'P'일지 묻는 이야기가 있었지만 C++ 같은 것도 나왔고, ANSI C, C99, ~~C0x~~ C11,[* 과거 새로운 표준이 200x년에 나올줄 알고 C0x로 불렸으나, 2011년에 등장하여 C11이 되었다. C++ 역시 같은 이유로 C++0x로 불리다가 2011년에 등장하여 C++11이 되었다.] C18 등으로 가지를 뻗으며 진화 중인 현재의 C언어에게는 이미 과거의 이야기가 되어버렸다.[* 이들은 다른 언어가 아니라 버전이 다르다.] 사실 D라는 프로그래밍 언어도 있기는 있다. 벨 연구소가 아닌 [마르스](Digital Mars)에서 만든 것이기는 하지만. C++와의 문법적인 호환성은 없지만 C++의 단점을 보완하면서 고생산성을 추구하는 하이 레벨 언어들의 특성들을 반영한 네이티브 컴파일 언어라 사실상 C++의 리엔지니어링 버전처럼 취급하고 있다. ABA Games의 게임들 대부분도 D로 만든 것이다.

잘 모르는 사실이지만 1997년에 등장한 [[16]]라는 프로그래밍 언어도 있다. 등장한 순서만 따지면 2001년 12월에 등장해서 2007년에 정식판이 나온 D언어보다 훨씬 먼저 나온 셈. 그 외에 단일 영문자 이름을 지닌 프로그래밍 언어는 [[17]], [[18]], [[19]], [[20]], [[21]], [[22]], [[23]], [[24]], [[25]], [[26]]가 있다.

C언어와 '++', '#'

C언어의 문법으로 나오는 ++는 초기화된 변수의 값을 1 증가시켜 대입하는 연산자이다. 다시 말해 C++에서 ++은 C를 1 증가시켜 대입했다는 말. C#에서 #은 ++++이다.(++이 위 아래로 두 개) 또한 C♯, 그러니깐 음악에서도 의미를 따왔다.

Write in C

C 언어와 관련해서 다음과 같은 노래도 있다. 참고로 다음 노래는 비틀즈Let It Be를 패러디한 것.

[youtube(XHosLhPEN3k)] >When I find my code in tons of trouble >내가 짠 프로그램에 문제가 가득하단 걸 알았을 때 >friends and colleagues come to me, >친구와 동료들이 다가와 >speaking words of wisdom... >슬기로운 말을 해주었네 >..."write in C" >"C로 짜" > >And as the deadline fast approaches, >마감은 빠르게 다가오는데 >and bugs are all that I can see >버그밖에 보이지 않아 >Somewhere someone whispers: >어디선가 누군가가 속삭였지 >"Write in C" >"C로 짜" > >Write in C, Write in C, Write in C, Write in C, >C로 짜, C로 짜, C로 짜, C로 짜 >LOGO's dead and burried, >[[27]]는 이미 죽어서 묻혀버렸어 >Write in C >C로 짜 > >I used to write a lot of FORTRAN >예전엔 포트란으로 많이 짰었어 >For science it worked flawlessly >수식 계산에선 나무랄 데 없었지만 >Try using it for Grahpics! >포트란으로 그래픽을 해 보라고! >Write in C! >C로 짜 > >And if you've just spent nearly 30 hours >어셈블리 디버깅을 >debugging some assembly >30시간 정도 해 봤었다면 >Soon you will be glad to >금방 고마움을 느낄 거야 >write in C >C로 짜 > >Write in C, Write in C, Write in C, Write in C. >C로 짜, C로 짜, C로 짜, C로 짜 >BASIC's not the answer, >베이식으론 해결이 안 돼 >Write in C. >C로 짜 > >Write in C, Write in C, Write in C, Write in C. >C로 짜, C로 짜, C로 짜, C로 짜 >PASCAL won't quite cut it, >파스칼로는 부족한걸 >Write in C! >C로 짜

C 언어의 추종자들은 다음과 같은 소리를 한다. >전산과 신입생은 CPU부터 시작해서 C를 활용하는 데까지 차곡차곡 기초를 닦아야 합니다. 저는 솔직히 너무나도 많은 컴퓨터 관련 교육과정들이 자바가 가장 좋은 초보자용 언어라고 선전하는 현실에 질려 버렸습니다. 흔히 자바는 쉽고, 따분한 문자열이나 malloc과 같은 골칫덩어리를 다루는 과정에서 혼란을 겪지 않으며, 아주 큰 프로그램을 모듈로 나눠서 만들 수 있는 근사한 객체지향 프로그래밍 기법을 배울 수 있다는 화려한 이유들이 따라 나옵니다. 하지만 여기에는 교육적인 재앙이 있습니다. 졸업생들은 하향 평준화돼 러시아 페인트공 알고리즘[* 처리할 데이터의 양이 커지면 처리 시간이 지나치게 증가하는 알고리즘.]을 여기저기에 만들어내며, 심지어 자신의 잘못을 인식조차 못할 겁니다.[* 이는 알고리즘의 문제이지 C언어를 모름으로써 발생하는 문제가 아니다. ] 펄 스크립트에서 이런 사실을 결코 볼 수 없을지라도, (물론 어렵지만) 기본적으로 문자열이 무엇인지 아주 깊은 단계에서 이해하지 못하기 때문입니다. 다른 이들이 뭔가를 잘하도록 가르치길 원한다면, 기초부터 시작해야 합니다. 이는 마치 베스트 키드와 비슷합니다. 마루바닥을 쓸고 닦고 쓸고 닦고, 이렇게 3주만 하면, 자연스럽게 목표물을 향해 발이 쭉쭉 뻗어나갑니다. >-- 조엘 온 소프트웨어 (조엘 스폴스키)

C언어를 깊게 공부하면 시스템의 저수준에 대해서 이해하는데 많은 도움이 되지만, 그렇다고 해서 그것을 알아야만 잘 짜여진 프로그램을 만들 수 있는 것은 아니다. 그리고 컴퓨터의 속도가 매우 빨라지고 보편화된 현대에는 고도로 추상화되고 생산성이 높은 언어에 대한 수요가 매우 크기 때문에, 다른 언어를 먼저 공부하는 것도 나쁘지 않은 선택이다.

관련 문서