로딩로딩중

C(프로그래밍 언어)

최근 수정 시각:

파일:나무위키프로젝트.png
이 문서는 나무위키 컴퓨터 프로젝트 · 나무위키 프로그래밍 프로젝트에서 다루는 문서입니다.
해당 프로젝트 문서를 방문하여 도움이 필요한 문서에 기여하여 주세요!

#include <stdio.h>

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

TIOBE에서 선정한 프로그래밍 언어 월간 점유율 순위 (2018년 7월 기준)

[ 펼치기 · 접기 ]

1. Java

2. C

3. C++

4. Python

16.139%
+2.37% 증가

14.662%
+7.34% 증가

7.615%
+2.04% 증가

6.361%
+2.82% 증가

5. VB .NET

6. C#

7. PHP

8. JavaScript

4.247%
+1.20% 증가

3.795%
+0.28% 증가

2.832%
-0.26% 감소

2.831%
+0.22% 증가

9. SQL

10. Objective-C

11. Swift

12. Ruby

2.334%
+2.33% 증가

1.453%
-0.44% 감소

1.412%
-0.84% 감소

1.203%
-1.05% 감소

13. 어셈블리어

14. R

15. MATLAB

16. Object Pascal

1.154%
-1.09% 감소

1.150%
-0.95% 감소

1.130%
-0.88% 감소

1.109%
-1.38% 감소

전년 동월 대비 변화 수치이다. 자세한 내용은 TIOBE에서 확인할 수 있으며, 기준은 다음과 같다.

1. 개요2. 역사3. 설명
3.1. 단점3.2. 사용되는 분야3.3. 이식성3.4. 과거3.5. 현재
4. 점유율과 플랫폼별 지원상황5. 다른 언어에 미친 영향
5.1. C++와의 관계
6. C 언어용 개발 도구들7. 여담
7.1. Write in C7.2. 포인터
7.2.1. 배열과 포인터 사이의 관계
7.3. 중괄호 스타일7.4. 기타
8. 관련 문서

1. 개요[편집]

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]


1972년에 벨 연구소(Bell Labs)의 데니스 리치(Dennis MacAlistair Ritchie)[2]가 만든 프로그래밍 언어. 보통 C 언어라고 한다.

지금 당장 써보고 싶다면 여기로.

2. 역사[편집]

케임브리지 대학교에서 시작된 CPL이라는 언어에서 BCPL(Basic CPL)이 탄생하였고, BCPL이 미국으로 물건너와 켄 톰슨이 'B' 라는 언어를 만든 뒤에, 같은 벨 연구소의 데니스 리치가 최종적으로 'C'라는 이름으로 언어를 만들었다. 참고로, CPL의 'C'는 케임브리지 대학의 C를 뜻했다가 후에 임페리얼 칼리지 런던과 조인트 프로젝트가 된 이후에는 'Combined'를 뜻하게 되는데, Stroustrup의 회고에 의하면 저건 윗분들의 생각이고, 자신들에게 있어서 'C'는 언제나 CPL의 언어 디자이너였던 Christopher Strachey였다고 한다. BCPL을 거쳐 'B' 가 된 것은 '벨' 연구소의 B를 딴 것. 그리고 C가 되면서 한때는 프로그래머 유머로 'C' 다음 언어가 과연 'D'일지 'P'일지 묻는 이야기가 있었지만 C++ 같은 것도 나왔고, ANSI C, C99, C0xC11[3][4]으로 가지를 뻗으며 진화 중인 현재의 C언어에게는 이미 과거의 이야기가 되어버렸다. 사실 D라는 프로그래밍 언어도 있기는 있다. 벨 연구소가 아닌 디지털 마르스(Digital Mars)에서 만든 것이기는 하지만. ABA Games의 게임들 대부분도 D로 만든 것이다.

최초의 C 컴파일러는 뭘로 쓰였을까? 닭이 먼절까 달걀이 먼절까? 같은 문제 같겠지만 간단히 말하면 어셈블리어로 쓰였다. B 언어를 붙잡고 여러번 씨름하다가 나온 부산물 중 하나가 C 컴파일러고, C 역시 리치에 의해 여러 번 리파인되었다. 이 문제가 유명해진 이유는 최초의 C 컴파일러 중 하나가 C 인터프리터로 개발되었으며 그렇게 개발된 컴파일러를 개선시키기 위해서 역시 또 그 컴파일러를 사용했기 때문이다. 혹자는 이것을 "진흙탕에 빠진 사람이 자신의 구두끈을 잡아당겨 빠져나온 격"이라고 표현하기도 한다.[5][6]

참고로 ++은 그 변수를 1 증가시켜 대입하는 연산자이다. 다시 말해 C++에서 ++은 C를 1 증가시켜 대입했다는 말. C#에서 #은 ++++이다.(++이 위 아래로 두 개) 또한 C♯, 그러니깐 음악에서도 의미를 따왔다.

3. 설명[편집]

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 언어 이전에도 고수준 언어들은 많이 존재했지만, 대부분 특정 어플리케이션 영역을 타겟으로 하거나, 컴퓨터 과학 이론을 입증하기 위해 만들어진 실험실 언어들이었다. 어플리케이션 영역이 아닌 운영체제를 어셈블리어가 아닌 언어로 작성한다는것은 당시엔 일종의 금기에 가까웠고[7], C 언어와 유닉스는 소수의 예외를 제외하고 대부분을 C 언어로 작성하고서도 우려와는 달리 단점보다 장점이 훨씬 많다는것을 보이면서 이 금기에 정면으로 도전해서 승리하였다고 볼 수 있다.[8] 그리고 지금은 운영체제는 C 언어가 아니면 안된다는 새로운 금기가 생겼다.

세계적으로 가장 많이 쓰이는 프로그래밍 언어 중 하나이다. 어셈블리 코드를 코드 안에 집어넣어서 구동 속도 면에서 이점을 얻을 수도 있으며(그러나 호환성은 안드로메다로...) 기본적으로는 기존의 언어들 물론 어디까지나 C가 탄생하던 6-70년대 이야기다. 에 비해 매우 이해가 가기 쉬운 문법을 사용하여 초보자가 쉽게 접근할 수 있도록 제작되었다. 참고로 C가 개발된 시기는 70년대인데, C 개발 전에 주로 쓰이던 코볼 내지 포트란이 얼마나 불친절한 언어인지는 더 이상의 자세한 설명은 생략한다. 요즘 프로그래밍에 입문하는 사람들은 상상도 못하겠지만 코볼과 포트란은 소스 코딩할 때 칸까지 맞추어야 했다. 파이썬? 제일 왼쪽 몇 칸은 주석, 그 다음 몇 칸은 정의... 이런 식으로.이 언어들은 천공카드가 쓰이던 시절 만들어져서 그렇다.[9] 그리고 사실 코볼의 경우 원래는 프로그램 코드가 업무 서류로도 사용이 가능하도록(!) 설계가 돼있어서 그렇다. 주석이 코드고 코드가 주석일 경우의 아주 나쁜 사례. 2000년대에 애자일 프로그래밍 정신 중 하나로 부활했다 카더라.[10]

다른 언어들의 경우 여러 교재들이 서로 경쟁하는 추세이지만, 유독 C의 경우는 창시자인 데니스 리치와 브라이언 커니건이 쓴 The C Programming language 2nd Edition 이 독보적인 위치를 점하고 있다. 이 두 사람의 이니셜을 따서 보통 K&R 이라 칭하거나[11], 제목을 축약해서 TCPL이라고 부른다. 이 책이 나온 지 매우 오래 되긴 했지만, C 라는 언어가 별로 변화가 없는 언어인 데다가 C99, C11 등 ISO에서 계속 업데이트를 했음에도 불구하고 이를 무시하는 프로그래머들의 성향[12][13] 때문에 ANSI C가 현재 사실상 표준인 상태다.[14] 또한, 분량은 전부 다합쳐도 300페이지도 안된다. 그것도 Appendix 부분을 빼면 순수 튜토리얼은 200페이지도 안되며, 언어 자체뿐 아니라 프로그래밍에 대한 여러가지 깨알같은 조언까지 다 포함이 되어있다. 보통, 일반적인 프로그래밍 언어 교재가 작아도 500 페이지를 가볍게 넘어가고, C++의 창시자인 Bjarne Stroustrup 이 K&R 과 비슷한 네이밍으로 쓴 The C++ Programming language 의 경우 1000페이지에 육박하는 것[15]에 비하면 엄청나게 짧은 분량이라 할 수 있다. 그러나, 분량이 많은 교재들은 그만큼 친절하고 자세한 설명을 동반하여 비교적 술술 읽히는 것에 반해, K&R 은 짧은만큼 매 문장 하나하나[16]의 중요도가 높기 때문에 프로그래밍 초심자에게 추천할 만한 책은 아니라는 주장도 있다. 그런데, The C++ Programming Language는 난이도도 TCPL 보다 더 높다. 사실, K&R이 바이블 취급을 받는 이유는 단순히 역사적이고 C 언어 창시자가 쓴 책이기 때문만은 아니다. C 언어가 겉으로는 심플해보이지만 워낙 숨은 함정이 많은 언어라 저런 부분들을 제대로 다 짚고 넘어가야 하는데, K&R 이외에 제대로 완전하게 짚어주는 교재가 거의 없다. 보통의 고수준 언어들처럼 접근해서 직관적으로 '이렇게 하면 된다.' 식으로 설명을 하는 교재들이 많은데, 사실 온갖 정의되지 않은 행동(Undefined Behaviour)과 구현 특화 상세내역(Implementation-specific Details), 하드웨어에 의존적인(Hardware Dependent) 함정들이 곳곳에 숨어있는 C 언어는 '이렇게 하면 된다.' 보다 '이렇게 하면 안된다.'를 중점적으로 설명해야 하는 언어이다.

특히 UI나 퍼포먼스 중심으로 돌아가는 어플리케이션 영역이 메인인 언어들과는 달리, OS와 딱 붙어서 보안이나 안정성 및 호환성이 중시되는 시스템 프로그래밍 영역이 메인인 언어라 어느 정도 깐깐하게 접근하는 게 맞는 언어다.[17] C 언어를 C++를 익히기 전의 워밍업 정도로 취급하는 사람들에게는 저런식으로 설렁설렁 쉽게 써진 C 교재들도 좋은 평가를 듣는 경우도 많지만, 본격적으로 C 프로그래밍을 하려는 사람들 사이에서는 해외 기준으로 K&R과 C Programming: A Modern Approach(일명 K&K)[18] 두 개 정도만이 제대로 된 교재 취급을 받는다.

국내 교재나 대학의 경우 닥치고 윈도우 기준으로 설명하는 경우가 많은데, 비주얼 스튜디오 컴파일러는 ANSI C를 기반으로 상당히 많은 비표준 확장을 제공하고 있으며[19] 표준에 어긋나 에러를 일으켜야 할 문장도 MSVC는 어떻게든 실행을 해 버리기 때문에 초보자에게는 오히려 독이 된다. GCC 역시 비표준 확장을 많이 제공하지만, 이후의 표준인 C99, C11에서 그런 비표준 확장의 상당 부분이 표준화되었기 때문에 상황이 나은 편이다. 웬만하면 유닉스에서 표준으로 먼저 배우고 MSVC 등의 비표준 기능은 차후에 문서를 보고 따로 익히거나, 꼭 MSVC로 배우고 싶다면 표준과 비표준, 나아가서 가능하면 C89/C99의 기능을 구분해서 설명하는 교재로 배우는 게 나중을 위해서 훨씬 나은 선택이다. 근데 어차피 C는 MS 월드에서 찬밥이다. C 언어는 유닉스를 만들기 위한 언어로 시작했기 때문에 유닉스의 표준인 POSIX와도 관련이 깊고, 결국 C를 표준으로 공부하려면 어찌됐건 유닉스 환경이 훨씬 편리하다. 정 유닉스 환경을 사용하기가 부담스럽다면 유닉스와 같은 뿌리를 공유하는 리눅스 환경을 이용하는 것도 좋은 방법이다. GCC의 경우 -Werror 컴파일 옵션을 넣으면 사소한 경고 사항도 전부 에러로 변환하여 컴파일을 중단시키기 때문에 문법적 오류를 잡는 데 많은 도움이 된다.

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

C언어 자체는 지원되는 기능이 적고 문법이 간단하다. OOP나 코루틴, 클로저, 메타 프로그래밍 등 고수준의 기능들을 지원하는 언어들과 비교하면 특히나 그렇다. 550쪽 정도밖에 안 되는 C언어 표준에서도 순수 문법 부분은 200쪽 정도밖에 안 되며 나머지는 다 라이브러리 관련 부분이고, 함수 갯수로 치면 고작해야 150개 근처이다. 수천 개나 되는 기본 라이브러리를 지원하는 다른 언어들과 비교하면 정말 작고 간단하다.

하지만 기능이 적다고 결코 쉬운 건 아니다. 초반의 포인터 장벽만 넘는다면 문법 자체를 마스터하고 간단한 커맨드라인 프로그래밍을 할 수 있는 수준까지는 엄청 쉽지만, 프로그래밍이란 게 기능이 적다고 그 적은 기능만 쓰는 건 절대로 아니다. 없으면 결국 직접 구현해서 쓰게 되는데, 거의 30년간 프로그래밍 언어계에서 오늘날 영어와 같은 위치를 차지하고 있던 덕분에 그런 기능의 구현이나 최적화에 관한 많은 트릭들이 존재하고 이것을 얼마나 많이 알고있는가가 사실 C 언어의 핵심이다.

게다가 K&R이 좋은 교재인 건 맞지만 오늘날 K&R을 교재로 추천하기가 마냥 좋지만은 않은 이유 중 하나도 책이 너무 오래되어서 초창기 C 언어의 유행이나 트릭 정도만을 담고 있고, 그 이후의 여러가지 트릭이나 패러다임의 변화 등을 반영하지 못하고 있기 때문이다.[20][21] 또한, 오늘날 고수준 언어들이 다수의 프로그래머가 함께 개발하는 것을 염두에 두고 팀에 누가 될 만한 위험하거나 생산성에 저해되는 부분들을 언어 차원에서 강제로 제외시키는 경향이 있다면, C 언어는 저것들에 대해 완전히 개방된 언어에 속한다.[22] 상황이 이렇다보니 같은 프로그램이라도 프로그래머의 지식수준과 능력에 따라 퀄리티 차이가 그야말로 극과 극으로 벌어지는 언어이다. C언어 활용의 예술을 보려면 리눅스 등 C언어로 작성된 대규모 오픈소스 프로젝트를 보면 된다. 방대한 양의 코드를 함수, 구조체, 포인터, 매크로만을 이용해서 철저히 모듈 단위로 잘 관리하는 모습을 볼 수 있다.

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

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

일반적인 프로그래밍도 역시 가능하지만, 그런 용도로는 더 적합한 언어들이 널려있다. 최근에는 데스크톱 애플리케이션보다 웹 애플리케이션의 활용도가 높아져서, 하드웨어 컨트롤이 중요한 분야가 아니면 C를 써야 할 이유가 거의 없다. 즉, 초심자가 C언어를 배우는 것까지는 문제가 없지만 배우고 나서 뭔가 제대로 할만한 건 사실상 없다고 보면 된다. 그리고, 바로 그렇기 때문에 프로그래밍 입문용 언어의 자리도 해외 기준으로는 대부분 JavaPython으로 대체되었다.

그래도 정 배우고 싶은 사람은 이곳을 참조해보자. C 언어 코딩 도장 JoinC 이것을 쉽게 배울수 있는 보드게임도 있다.

3.1. 단점[편집]

닥치고 성능이라는 대명제에 충실해서 작게는 변수 초기화, 배열 범위 점검, 허상포인터(Null Pointer) 문제에서부터 크게는 쓰레기 수집(Garbage Collection; GC), 예외처리 같은 경우까지 조금이라도 하드웨어에 오버헤드가 걸릴 것 같은 기능은 다 무시한(그래서 프로그래머가 그런거까지 다 신경써야되는), 저급프로그래머 입장에서는 악몽 같은 언어다. 이 모양인 이유는 사실 C가 처음 나왔을 때는 그런 기능들에 신경쓸 여유도 없는 데다가[23] 그 당시에는 프로그래머라면 모름지기 저걸 알아서 관리해가며 쓰는 게 기본이었기 때문. 언어에 고수준 개념들을 구현하기에는 하드웨어의 성능이 절대적으로 떨어졌기 때문에 그럴 수밖에 없었다.[24]

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

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

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

  • 느슨한 타입 검사. 초창기의 K&R C에 비해 ANSI C부터 타입검사가 비교적 양호해졌지만 아직도 서로 다른 종류의 포인터끼리의 대입조차 가능하다. 사실 느슨한 타입 검사 자체가 나쁜 건 아니고 최신 언어들은 느슨한 타입 검사를 채용하는 언어가 많은데, 문제는 C는 강타입 언어라 느슨한 타입 검사를 통과한 타입 미스매치가 실행시에 치명적인 문제를 일으킬 수 있다는 거. (그니깐 컴파일 시 warning을 무시하지 마라. warning을 잡아주면 당신의 명줄이 더욱 길어진다.)

  • 배열 안에 접근할 때, 인덱스가 배열 범위를 벗어나도 이를 체크하지 않는다. 이는 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 라는 이름으로 표준화가 되었다.[25] 이렇게 배열 범위를 체크하지 않는다는 점은 심각한 보안 구멍을 만들어낸다. 일명 buffer overflow 취약점이라고도 하는데 배열 체크를 하지 않는 C언어 때문에 생기는 보안상의 만악의 근원. 물론 프로그래머가 꼼꼼히 체크하면 되긴 하는데 여러분도 알다시피 프로그래머는 항상 실수한다(...) 따라서 언어 자체에 내재된 취약점이라 볼 수 있다.[26][27]

  • 문자 처리 과정에서 실수가 있으면, 치명적 문제가 생길 수 있다. C에서는 문자열의 끝을 표시하기 위해 모든 비트가 0인 널(null) 문자를 맨 마지막에 달아 사용하는데, 문자열을 처리하는 과정에서 이 널 문자를 잘못 달아주는 실수를 프로그래머가 저지르기 쉽다. 이런 경우, 메모리의 경계를 넘어 다른 영역에 접근하는 치명적인 문제가 생긴다. 문자열을 출력했는데 뒤에 왠지 모를 한자가 딸려나온다거나.. 긄꿻얋儆儆儆儆儆儆儆儆 라인피드와 캐리지리턴까지 섞이면 이때는 대략 정신이 멍해진다 이상한 언어가 나와서 보니까 구자라트어라든데 참고로 파스칼과 같은 언어는 문자열의 길이를 따로 저장하여 처리한다.

  • 문자열 타입이 없어서 문자형 배열을 대신 사용한다. 그 덕분에 문자열 처리가 까다로우며[28], 초보자들이 잘못 이해하거나 혼란을 느끼기가 쉽다. 주로 문자열 상수[29]에 대해 잘못 이해하는 경우가 많은데, 문자열 상수 자체는 '문자열'이나 '값' 자체가 아닌 '배열'이며[30], 결과적으로는 해당 배열의 첫번째 글자가 저장된 위치를 가리키는 '주소값'이 된다.[31][32] 초보들은 char * s를 문자에 대한 포인터가 아닌 문자열에 대한 포인터로 잘못 이해하여 s가 가리키고 있는 '문자열'을 가져오기 위해 s에 참조 연산자 *를 붙이는 경우가 있는데, 이렇게 하면 의도한 대로 원래의 문자열 전체를 가져올 수 없다. 다른 타입들에 대해서는 예를 들어 int i, *j = &i; *j = 3; 식으로 해당 포인터가 가리키는곳에 있는 '값'을 의미할때는 항상 * operator 를 붙여줘야 하지만, 문자열 전체일 경우에는 참조 연산자 *를 바로 쓰지 않고 char *s; s = "Hello"; 처럼 구분해서 써야 한다. *ss[0] 과 마찬가지로 문자 'H'의 값 그 자체를 의미하기 때문이다.

  • 대입 (=) 연산자가 값을 반환한다. 가장 흔한 실수로 == 를 쓸 곳에 = 를 하나만 찍는 오타를 범했을 경우, integer 값이 리턴되는 경우에는 역시 문제없이 컴파일되고 직접 돌려서 해당 기능이 오작동을 하기 전까지 버그를 알아챌 수조차 없게 된다. 이 때문에 == 사용시 lvalue에 일부러 상수항을 사용하는 프로그래머들도 있다. 예를 들어, int a = 42; if(a == 37) { ... } 같은 경우, 실수로 if(a = 37) 이라 쓰면 false가 아닌 true가 뜨며(a = 37 이라는 expression의 값은 37 이고 0 이 아닌 값은 죄다 true로 간주된다.), a에 37 이 대입돼버려 완전히 오작동을 하게된다. 문제는, 컴파일이 문제없이 되기때문에 버그찾기가 더더욱 힘들어진다. 상수항을 왼쪽으로 옮겨서 if(37 == a) 로 써주면, 37 = a라 썼을 때 당연히 말이 되지 않으니[33] 컴파일 시 에러가 떠서 쉽게 알아챌 수 있다... 라는 유명한 팁이 있지만, 사람의 직관과 맞지 않아 어색하게 느껴지기 때문에 일부러 익숙해지지 않으면 잘 쓰이지 않는다. 본능이 거부한다 요새는 컴파일러가 좋아져서 이런 위험코드는 대체로 경고처리해준다.[34][35]

  • 배열과 포인터의 차이가 비직관적이다. 사실 이 부분이 C 언어를 배우는데 가장 커다란 난관이 되는 부분이다. 배열과 포인터는 비슷하다고 보기에는 차이가 너무 크고, 다르다고 보기에는 비슷한 부분이 너무 많다고 생각하는 경우가 많다. 덕분에 교재마다 접근방법이 달라지는데, 어떤 교재에서는 '비슷하다'고 전제한 뒤 차이점을 설명하는식이고, 어떤 교재에서는 '다르다'고 전제한 뒤 공통점을 설명하는 식이다. 어느쪽으로 접근하더라도 함정과 예외가 상당히 많아지기 때문에, 둘의 차이를 충분히 구분할만한 상황적 경험이 적을수밖에 없는 초심자들에게 결국은 이해보다 암기로 흘러가고, 어렵게 느끼는 경우가 많다. 배열 타입과 포인터 타입의 차이를 제대로 느끼지 못하는 상황은 배열타입을 식에서 이용할 때 처음 원소를 가리키는 포인터로써 사용된다는 규칙을 제대로 숙지하지 못한 상황에서 배열과 포인터를 사용할 때 주로 발생한다. 즉, 식에서 배열타입을 이용할 때, 실제로는 배열 타입이 아닌 포인터 타입으로써 이용이 되는데 프로그래머가 이를 배열 타입을 이용하는 것으로 착각하면 "읭 포인터 타입하고 많이 비슷하네?"라는 반응을 보이게 되는 것이다. 이렇게 겉으로 보기에는 배열 타입이지만 실제로는 포인터 타입으로써 사용될 수 있는 비 직관적인 요소가 존재한다.

  • 숫자 타입을 처리하는 규칙들이 비직관적이고 복잡하다. 덕분에 언어 자체의 syntax/semantics를 넘어서 이에 대한 여러가지 꼼수와 내부적으로 처리하는 방식, 머신따라 달라지는 부분까지 염두에 두어야 한다. 숫자타입과 관련된 문제들은 한두가지들이 아니므로 하위 항목으로 분류한다.

    • C언어에서는 int형과 double형만 존재하는 것처럼 느껴질 수 있는데, 실제로, 모든 리터럴은 뒤에 f 등의 타입을 붙여주지 않으면 그냥 int 아니면 double이다. char건 true/false 값이건 short 건 그냥 다 int로 간주한다. 그리고, 타입변환이 필요할 때마다 정수 진급(integral promotion, C99에선 integer promotion)이라 해서, char, short 등의 타입은 일단 먼저 int로 바꾸고 나서 변환을 시작한다.

    • bool 타입은 존재하지 않으며[36], 0은 false이고, 나머지 다른 것들은 전부 true로 처리한다.[37] 때문에 임의의 char 값이 숫자인지 알아보는 isdigit 함수는 숫자가 아니면 0을 리턴하고 숫자일 경우 보통 1을 리턴하긴 하지만, 0이 아닌 그냥 어떤 수를 리턴하는 컴파일러도 있다. 매뉴얼페이지에는 보통 non-zero라고 되어 있다(...). 비슷하게 논리적 부정 연산을 의미하는 !의 경우는 0일 경우 1로, 0이 아닌 다른 모든 값은 0으로 바꾼다.

    • char 타입은 아예 int형 타입과 거의 자유자재로 호환이 되며[38], 문자 상수 'A' 같이 분명히 char 타입으로 보이는것들도 실상은 char 타입이 아니라, int 타입이다. (C++에서는 이와 달리 문자 상수는 const char형이다.) 문자열은 그냥 char 형 배열로 쓴다. 문자열 값의 끝에 붙는 널문자 '\0' 역시 int 값으로 그냥 0 이다.[39]

    • unsigned 타입이 섞인 계산에서는 음수가 양수가 되어버린다. 이 문제에 대해서는 다소 긴 설명이 필요하다. C 언어에서는 사칙연산과 비교연산자를 포함하여 많은 이항연산자들이 있는데, 이것들은 서로 다른 타입간에 이루어지는 계산을 처리하기 위해 usual arithmetic conversions 라는 공통규칙을 적용한다. 그런데 이 규칙에 의하면, 같은 랭크(숫자 타입의 우선순위. 간단하게는 size라 생각해도 무방하다)인 부호형 정수와 무부호형 정수 사이에는 무부호형이 우선권을 가지며, 따라서 이항연산자의 두 항이 모두 무부호형으로 처리된다. 이 간단한 규칙이 불러오는 참극인 즉슨, unsigned 타입이 수식 중에 단 하나라도 섞여 있으면 그 안에 포함된 모든 음수가 양수로 변해버린다. (절대값이 아니라 mod 연산된 값으로 변한다. 경고나 에러 메시지 없이.) 그 결과 일반적인 수학 수식과는 전혀 동떨어진 이상한 결과가 나온다. 이를 방지하기 위해서는 그보다 더 랭크가 높고 실제 표현 범위도 큰 유부호 정수형으로 강제 형변환을 해야 한다. (혹은 unsigned형을 signed형으로 형변환한다.)

  • 네임스페이스를 지원하지 않는다. 소스 수준에서 play_sound라는 함수를 어디엔가 정의를 해놓고 다른 소스 파일에서는 play_sound라는 이름만 같은 다른 함수를 만들고 같이 컴파일하면 에러가 난다. 더욱 골때리는건, 컴파일 단계는 무사히 진행되는데 링커 수준에서 에러가 난다는 것. 따라서, 외부 소스 파일로 노출시키고 싶지 않은 내부 함수들은 static 지시자를 앞에 붙여줘서 이름 유출을 방지하고 다른 소스에서 쓸만한 함수는 <모듈이름>_함수이름(e.g. NamuWiki_add_document)을 사용하여 이름 충돌을 최소화 하는게 일반적이다.

  • 기타 비직관적이거나 혼동하기 쉬운 규칙들을 아래에 정리하였다. 이런 규칙들은 대개 성능을 위해 생각하기를 포기한 부분이거나, 예전부터 이어진 전통인 경우가 많다.

    • 가변함수의 매개변수에는 기본 인자 진급(default argument promotion)이 적용된다.

    • 함수의 매개변수로 선언된 배열은 배열이 아닌 포인터이다.

    • 함수 인자의 평가 순서는 정해져 있지 않다.

    • 피연산자 간의 평가 순서는 대체로 정해져 있지 않다. (&&나 || 등의 연산자는 예외)

    • 부호형과 무부호형을 섞어 쓴 수식의 형변환 규칙 참조 사례

    • 시퀀스 포인트(sequence point)와 관련된 객체 변경 규칙


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

위에 나온 대부분의 단점들은 한마디로 인간이 자연스럽게 쓰기 힘들다는 것인데, C의 컨셉이 그런 것이니 당연한 것이다. 인간인 프로그래머와의 친화가 우선이 아니라 컴퓨터와의 친화가 우선인 언어인 것이다. C와 컴퓨터 및 운영체제의 이해가 깊어지다 보면 왜 이렇게 쓰기 불편하게 만들어진게 사실 당연한 선택이었는지 이해가 간다.[40]

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

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

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

3.2. 사용되는 분야[편집]

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

  • 운영 체제[42] 및 디바이스 드라이버

  • 임베디드 프로그래밍

  • 매우 빠른 계산속도가 필요한 프로그램이나 라이브러리

  • 암호학 라이브러리[43]

  • 프로그래밍 언어 인터프리터(CPython 등)

  • 웹 서버(Apache 등)

  • 데이터베이스(Postgresql)


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

3.3. 이식성[편집]

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

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

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

C언어에는 이식성이 있는 것일까? 구현체(implementation) 자체의 이식성은 굉장히 좋다. 다양한 아키텍처를 배려하여 잘 정의된 표준문서로 인한 이론적 이식성과 함께, 기존의 풍부한 컴파일러 구현체 및 공개된 소스코드들이 실질적인 이식성까지 함께 확보해 준다. 그렇다면 코드의 이식성은 어떨까? 전적으로 프로그래머의 역량에 달린 일이겠지만, 그걸 제외하고 생각한다면 좋다고 봐야 할 것이다[46]. 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만 지원한다면 문제는 없겠지만 그 외의 환경을 생각하면 아키텍처에 따라 다른 크기가 선택될 수도 있다.[47]

    • 이 문제를 해결하기 위해 C99부터 고정 크기 정수형을 지원한다. stdint.h 헤더를 포함하는 것으로 접근할 수 있으며, 형식은 `int비트수_t`와 같다. 무부호형은 `uint비트수_t`. 추가로 위에서 언급한 '가장 빠르게 접근할 수 있는 크기'를 좀 더 명확히 나타내는 `(u)int_fastBIT_t`도 추가되었다.

  • 사칙연산을 비롯한 많은 이항연산자에 대해, 각 항의 평가 순서는 정해져 있지 않다(unspecified). 앞의 항부터 먼저 할 수도 있고, 뒤의 항부터 먼저 할 수도 있고, 심지어 상황에 따라 다르게 적용하거나 랜덤하게 할 수도 있다. 많은 사람들이 c = a + b 에서 a와 b의 평가 순서가 정해져 있지 않다는 사실에 당황하는 사람이 많은데, 이렇게 각 구현체에게 선택의 자유를 줌으로써 각각의 환경에 맞는 가장 효율적인 최적화를 가능하게 하는 것이다.

    • c = a + b에서 a와 b의 평가 순서가 정해져 있지 않다는 것은 이런 얘기다. j = (i++) + (++i);에서 j에는 어떤 값이 대입될까? 이건 'undefined'다. ++i부터 먼저 계산하면 j에는 2i+1이 대입되지만 i++부터 먼저 계산하면 2i+2가 대입되는데, 둘 중 뭐가 맞는지는 정의되지 않는다. 누군가 이런 코드 쓰면 좀 맞으라고 한 것처럼, 그리고 위에서 '정의되지 않은'의 속뜻을 풀어주었듯이 C 표준에서 이걸 정의하지 않는 취지는 이딴 코드, 즉 a와 b의 평가 순서가 중요한 코드는 쓰지 말라는 것이다.


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

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

  • PL/1 이나 많은 인터프리터 언어들은 C만큼 많은 시스템에 구현되지 않았다. 해당 언어가 '모든 시스템에 구현된다면 높은 이식성이 있겠지만' 실제로 사용할 수 있는 시스템이 제한되는 경우 이식성이 좋다고 할 수 없다. C는 거의 모든 시스템에서 사용할 수 있었기 때문에 이식성을 장점으로서 누릴 수 있었다.

3.4. 과거[편집]

C가 나올 당시 시점에서 당시 널리 쓰이던 포트란이나 코볼, 베이식과 비교해 보자면, C는 언어 자체에는 아주 기초적인 기능 만을 탑재하여 언어에 기본 탑재되는 명령어를 최소한도로 줄였다는 특징이 있다.

C에서는 START나 END같은 명령어도 배제하고, 대부분을 { } 같은 기호로 표현했기 때문에 코드가 매우 깔쌈하고 용량도 줄었다. C에서는 포트란에는 아직 남아 있던 천공카드 시대의 흔적이 거의 사라졌다. 그러나 테이프 프린터 시절의 유물이 남아있다는게 함정

3.5. 현재[편집]

노인 학대

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

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

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

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

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

이렇게 이미 한물 간 언어처럼 보이지만, 여전히 '프로그래밍' 입문으로 C를 추천하는 사람이 많다. 사실, 이 말에도 일리가 있긴 한것이 C라는 언어는 매우 심플하면서도 배우는 과정중에 소프트웨어 구성의 최소단위인 bit부터 시작해서 메모리 관리, 그리고 고급 개념인 OOP 비스무리한 것까지[52] 흉내내면서 소프트웨어 전반을 훑게 되고, C 를 배우는 과정중에 나오는 과제들은 커맨드라인에서 이미 쓰이고있는 기본적인 툴들을 reinvent the wheel[53] 하는식의 과제들이 많기때문에 바닥부터 훑어가며 견문을 넓히는데 좋다. 실제로 가장 기저에 놓인 OS API[54]는 오늘날 플랫폼을 불문하고 거의 다 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 언어 강좌가 돼버리는 경향이 있다. 예를들어, char p[] = "aaa"; *p = 'b'; 는 문제가 없는데 char *p = "aaa"; *p = 'b'; 는 런타임 에러가 난다.[55] 문자열, 포인터, 배열 관련해서 프로그래밍 개념적으로는 거의 의미없는 그냥 C 언어만의 저런 함정들이 상당히 많은데다가, 코딩시 자주 접하게 되는 결코 무시할 수 없는 함정들이기 때문에 그냥 넘길수도 없고 거기에 낭비되는 시간도 은근히 많다. 대학 프로그래밍 강의의 목적을 생각하면 사실 무엇이 더 적합한지는 자명하기때문에, Java, Scheme, Python, ML, Haskell(!) 등 해외대학의 경우는 입문 프로그래밍 언어가 고수준 언어로 변경되었다. 한국의 경우는 순수수학이나 논리학같은 분야가 돈이 안되다보니 기반이 매우 약한편이며, 반면 공학은 기형적으로 발달해있기 때문에 전자공학쪽의 입김이 센편이라 여전히 대학에서 C 언어로 입문을 시키는 경우가 많다. 심지어 대학에서도 고급 컴퓨팅 하면 로우레벨쪽부터 상상하는 사람들이 대부분이고, 입문강의를 C 언어에서 다른걸로 바꾸는 이유가 C 언어가 어려워서 잘 못따라오니 쉽게 가르치기 위해서라고 착각하는 사람도 많은편.

4. 점유율과 플랫폼별 지원상황[편집]

2016년 3월을 기준으로 Java와 함께 몇년째 1, 2위를 다투고 있으며[56], 그 이외의 언어와는 넘사벽의 비율을 보여준다. 그야말로 부동의 원투펀치. 다른 언어들이 3위 경쟁을 하는 동안 C와 Java가 넘사벽 양대산맥을 형성하고 있다.

가장 널리 쓰이는 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 운영체제라는 버프도 있고[57], C를 배우고 여러가지 시험해보면서 놀기에 적합한 환경을 제공해준다. 윈도우와 다르게 커널부터 오픈소스로 개발되고, 이 커널이 C 언어로 만들어져 있어 C 언어의 사용도 활발하다. 이쪽 프로그래머들은 개발환경을 vim이나 Emacs로 사용하는 사람들이 많이 있다.

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

5. 다른 언어에 미친 영향[편집]

  • { ... }을 이용한 블럭 (ALGOL/PASCAL 스타일의 begin ... end 보다 간결하다)

  • 대입을 뜻하는 연산자를 =로, 동일함을 뜻하는 연산기호를 ==로 사용한다. 농담 좀 섞어서 초심자의 C언어 코딩 에러의 90%는 여기서 나온다. 나머지 10%는 :이다[59]

  • 다르다를 뜻하는 연산기호를 !=로 사용한다.

  • OR/AND를 ||와 &&로 사용한다.

  • +=, -=, *=, /=등의 직관적인 복합연산자를 지원한다.

  • ++ 와 -- 라는 단항연산자를 사용한다.

  • 그외에 if, for, while 등 많은 예약어의 사용 방식.


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

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

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

"라틴어를 익히면 영어, 프랑스어 모두 쉽게 익힐 수 있어."
라틴어 >>>> 영어+프랑스어인 것은 그냥 넘어가자.

5.1. C++와의 관계[편집]

이름의 유사성 때문에 C++를 C의 확장판 정도로 생각하는 사람들이 많다. 역사적으로 일단 시작은 그러했으니, 어느 정도는 사실이다. 그러나 시간이 지나면서 두 언어가 서로 공유하는 부분에서도 차이가 생기기 시작했다. 즉 이제 C는 C++의 부분집합이 아니게 되었다. 이것이 의미하는 바는, C로 짠 코드를 아무 생각없이 편하게 긁어다 C++에 붙일 수 없게 되었다는 것이다.[62] 자세한 것은 다음 문서를 참고하자. ISO C와 ISO C++의 차이 생각보다 까다로운 문제점들이 많다. 현재의 C11과 C++11에서는 더더욱 많은 차이점이 있을 것이다.

C++17에서는 다시 C가 C++의 부분집합이 될 것으로 보이기 때문에(https://www.reddit.com/r/cpp/comments/4pmlpz/what_the_iso_c_committee_added_to_the_c17_working/) 표준이 갱신되고 C가 C++의 subset이 되면 수정바람.

참고로, Objective-C++의 경우에는 C++와 달리 C를 완전히 포함한다.

6. C 언어용 개발 도구들[편집]

  • Bloodshed Dev-C++ 공식 사이트
    GPL 라이선스를 따르는 오픈 소스 IDE. 2006년 이후로는 소식이 없다. 설치 속도가 빠르고 편리하지만 리소스 에디터가 없다. 설치할 때 기본적으로 같이 깔리는 컴파일러는 MinGW+GCC.

  • Orwell Dev-C++ 공식 사이트
    Orwell이 Bloodshed Dev-C++ 4.9.9.2 소스로 개발하고 있는 IDE이다. 2015년 이후로는 무소식.

  • Borland C 지원 페이지
    볼랜드에서 개발한 유료 IDE. 단순 컴파일러는 무료이다. 볼랜드가 개발언어 쪽만 전담하는 코드기어라는 자회사를 설립하면서 모든 권한을 넘겼는데, 코드기어가 엠바카데로와 합병되면 홈페이지가 여러 차례 이동되었다. 참고로 델파이의 개발도 볼랜드→코드기어→엠바카데로의 순서로 넘어갔다.

  • 비주얼 스튜디오 한국어 공식 사이트
    마이크로소프트에서 만든 IDE. 여기에 포함된 C++ 컴파일러는 MSVC, 또는 Visual C++라고 한다.[63] C/C++이 아니라 C++라는 점에 유의. C는 정식으로 지원하는 언어가 아니다. MSVC가 C를 지원하는 이유는 그저 C++가 C의 문법도 포함하는 언어이기 때문이다. 또한 C99 이후로는 C가 C++와 다른 길을 걷게 되었기 때문에, 비주얼 스튜디오에서는 C99/C11의 일부 또는 전부를 지원하지 않는 것이다. MS는 C를 Internal Language로 규정하여, 윈도우나 기타 MS 소프트웨어 개발에 사용하기는 하지만 엔드 유저(프로그래머 포함)를 위해 지원하지는 않는다. 비주얼 스튜디오 2017에서도 C11은 커녕 C99조차 일부 기능을 지원하지 않는다.[64]

  • CLion 공식 사이트
    IntelliJ IDEA로 유명세를 날리고 있는 JetBrains에서 개발한 C/C++ IDE이다. 인터페이스가 직관적이고 다양한 종류의 컴파일러로 크로스플랫폼 개발이 가능하다는 장점이 있다. CMake 기반으로 프로젝트를 생성하며, 유료 구독권 형식으로 판매되고 있기 때문에 월/년마다 정기 구매해야 한다.

  • Pelles C 공식 사이트
    윈도우 전용의 프리웨어 통합 개발 환경이다. MS와는 다르게 C99는 물론이고, C11도 완벽 지원하는 컴파일러를 제공한다. 게다가 툴이 가볍고 무료다!

  • Watcom C 공식 사이트
    공식 사이트가 위키위키 형식으로 되어 있다. Open Watcom Public License라는 라이선스를 따른다.

  • Wipi C
    관련 문서: WIPI

  • GNEX

  • Xcode 공식 사이트
    애플에서 직접 제작한 IDE. 예전에는 GCC를 그대로 가져다[65] 썼지만, 요즘은 LLVM이라는 새 컴파일러 셋으로 이주하였다.[66] C, C++, Objective-C, Swift 등의 언어를 컴파일할 수 있다. macOS용 애플리케이션이나 iOS용 앱을 제작하는 데는 필수이다.

  • LCC 위키백과 영문
    A Retargetable C Compiler: Design and Implementation라는 책에 소스 코드가 실려있는 C 컴파일러. 이를 바탕으로 한 윈도우용 C 컴파일러인 lcc-win32도 존재한다.

  • ICC (Intel C++ Compiler) 위키백과 영문 공식 사이트
    인텔에서 자기들이 만든 CPU에 최적화된 코드를 만들어 낼 수 있도록 직접 제작한 컴파일러 스위트. 사용 설명서 시작 부분에 '인텔에서 제작하지 않은 CPU에서 구동 시 최적화된 성능을 보장할 수 없습니다'라고 적혀 있다.# C/C++ 및 Fortran 컴파일러를 제공한다. 윈도우에서 설치 시, 비주얼 스튜디오에 애드온 형태로 설치가 돼서 비주얼 스튜디오 내에서 기존 컴파일러 대신에 사용할 수 있다.
    컴파일러 이외에도 어셈블리어 단위로 한땀한땀 손으로 최적화한 수치해석 라이브러리#와 다수의 컴퓨터가 동시에 컴퓨팅을 할 때 사용되는 MPI 라이브러리, 코드의 성능을 분석해주는 도구#, 코드 최적화를 도와주는 도구#, 성능에 악영향을 주는 에러를 찾아주는 도구#, 등을 하나로 묶어서 Intel Parallel Studio라는 이름으로 판매한다. 인텔 CPU와 인텔 가속기를 수만 개씩 사용하는 슈퍼컴퓨터 등에서는 매우 자주 쓰이는 유용한 도구.
    Intel Parallel Studio는 학생 대상으로 무료로 사용 가능(윈도우용은 비주얼 스튜디오 통합 포함)하고, 수치해석 라이브러리는 일반인 대상으로 무료 사용 가능하다.

7. 여담[편집]

7.1. Write in C[편집]

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

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,
LOGO는 이미 죽어서 묻혀버렸어
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로 짜


추종자들은 항상 다음과 같은 소리를 한다.

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

7.2. 포인터[편집]

C의 알파이자 오메가
C에서 지원하는 유도형[68]식의 자료형 중 하나. 중요한 것은 포인터 자체는 integer 타입의 정수라는 점이다. 즉 시스템 상에는 그냥 메모리 주소를 기록한 것이기 때문에 정수로 사용할 수 있다.
C# 과 같은 메니지드 언어는 별도의 "참조(Reference)"라는 형태가 있는 것과는 달리 그냥 정수다. 그래서 C#에는 값과 참조의 구분때문에 Boxing과 Unboxing이 발생한다. C와 C++은 참조형이 없이 모두 정수형 변수이므로 박싱이나 언박싱이 발생하지 않는다.

C를 배우는 사람들은 포인터에서 한번쯤은 골머리를 앓아보았을것이다. 대부분 입문자들은 컴퓨터 구조에 대해 전혀 모르는 상황에서 C를 배우기 때문.[69] 하지만 포인터를 제대로 사용할 수 있게 되면, 그시점에서 컴퓨터 위주로 생각할 수 있게 된다고 볼 수 있다. 그렇기 때문에 사실상 컴퓨터 구조와 함께 배우는 셈이 되는데 기본지식이 없는 일반인의 머리로는 이게 제대로 될 리가 없다. 포인터 덕분에 메모리어셈블리어 수준으로 정밀하게 제어할 수 있지만 그 반작용으로 에러의 90%는 궁극적으로 포인터 문제다.[70] 때문에 C#이나 자바 등은 포인터를 지원 안 한다.[71][72] (결국 구조체와 배열 참조도 전부 포인터 참조로 이루어진다. 다만 사용자에게 제공하지 않을 뿐이다.) 이게 뭐가 문제냐면 시스템이 복잡해지고 입체적으로 확장되다보면 수많은 포인터의 사용문제가 결국 관리비용의 엄청난 증가로 이어지기 때문이다. 프로그래머의 각자 역량에 관리를 맞겨버리는 포인터의 심플함은 결국 대규모 프로젝트의 생산성 저하로 이어지고 알수 없는 오류에 대한 관리비용증가로 직결된다. 때문에 자바나 C#과 같은 메니지드 언어에서는 포인터를 시스템 레벨로 올려서 은폐시키고 프로그래머에게는 GC같은 간접적인 노출만 허용하게 되었다. 프로그래머가 직접적으로 메모리를 다루지 않아 오류 상황에서 OS차원의 샷다운(강제종료)은 없어져서 운용면에서 안정성이 크게 증가되었으나 퍼포먼스 저하와 더불어 엄청난 오버헤드를 감당하게 되었다. 자바나
C#에서 제공하는 GC의 악명은 설명하지 않아도 알게 될것이다. 매우 불편하고 느리며 통제가 쉽지 않다.

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

포인터의 악명은 DOS 시절로 거슬러 올라간다. DOS 시절은 16비트 CPU[75]의 한계로 포인터도 near 포인터(2바이트), far 포인터(4바이트)로 나뉘어져 있었는데 이 시절에는 저 포인터들을 다 컨트롤하기가 쉽지 않았다(주소 계산 방식부터 다르다). tiny, small, medium, compact, large 등등의 메모리 모델이 있었고 각각의 모델마다 사용되는 포인터가 달랐다. data segment 가 한개뿐인 모델에는 near 포인터를 사용하였고[76], 그 외의 보다 큰 메모리 모델에는 far pointer 가 사용되었는데 당연히 near pointer 가 오버헤드가 적기때문에 가급적이면 작은 메모리 모델을 골라서 사용하였다. 게다가 포인터가 깨지기라도 하면 아예 컴퓨터가 맛가는 경우도 많았기에 디버깅 지옥이었다. 본격적으로 32비트 시대가 열리고 난 이후[77]에 near, far 개념이 사라지고 메모리 관리를 OS차원에서 어느정도 관리해주면서 포인터 지옥에서 상당히 해방되었음에도 불구하고 과거의 포인터의 악명이 그대로 이어져 오고 있는 것이다.[78] 물론 드라이버와 같은 Low-Level 프로그램을 짠다면 포인터 한번 잘못 사용하는것으로 블루스크린 을 심심찮게 맛볼 수 있다. 블루스크린이라도 나오면 이는 매우 양호한 경우이고, 조금 안 좋으면 시스템 정지, 최악의 경우는 어떠한 반응도 없이 바로 재부팅이 되는 경우다. 사실 시스템 정지가 일어나는 경우는 동기화 객체를 잘못 다루었을 때에 발생하는 경우가 많다만

참고: 이러한 블루스크린 문제는 최신 OS들이 나오고 고성능 CPU등이 보편화된 2018년 지금 OS레벨에서 관리가 되고 있다. 즉 예전처럼 포인터 오류로
시스템이 셧다운되는 상황은 발생하지 않는다. Virtual Machine이 보편화되었기 때문이다. 16비트 시절 컴퓨터는 시스템 파워가 딸려서 VM같은 고급기술을 사용해서 쓸대없는 퍼포먼스를 발생시키면 안되었다. 그래서 포인터를 관리할 수가 없었고 응용프로그래밍이 메모리를 함부로 건들면 바로 다운이 되었다.
그러나 VM기반의 요즘 OS는 응용프로그래밍의 모든 메모리 입출력은 관리가 되어 있어 부정한 접근이나 잘못된 사용을 검출해 낸다.

C에 입문하는 대학생들에게 필요이상의 심리적 부담을 안겨주어 시작하기도 전에 지레 겁부터 먹게 만드는 악효과를 가져왔다. 여기에 교수들의 실력 부족과 잘못된 커리큘럼은 포인터를 더욱 큰 벽으로 느끼게 만들고 있다. 이 문제는 십여년이 흘렀어도 개선되지 않고 결국 난이도가 좀 더 낮은 언어인 자바로 프로그래밍 교육 커리큘럼이 이동하는 추세다.[79] 자바를 새로 배우기 귀찮아하는 교수들은 이마저도 못하지. 아 대학원생을 족치면 되겠구나! 자바를 비롯한 현대 언어들은 포인터를 아예 없애 메모리에 직접적인 접근을 막고 그 자리에 참조자를 넣어서 자동으로 관리를 한다.[80] 자세한 건 자바 항목 참조.

어렵다는 단점이 있지만 포인터는 강력한 도구임을 알 수 있다. 시스템 내부에서도 굉장히 많이 사용되며 크고 긴 문자열 값을 일일이 넘겨줄 필요 없이 포인터 하나만으로 대체할 수 있으며 알고리즘, 자료구조에도 많이 사용된다. 배열이나 구조체 참조에도 포인터를 사용하게 된다. 결국 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언어의 문화는 한 땀 한 땀 정성들여 저수준의 테크닉을 사용하는 것에 집착하는데, 프로그래머보다 오히려 컴파일러가 CPU를 더 잘 이해하는 지금에 와서는 추상적인 동작에 더 집중하는 것이 현명한 방식이다. 하찮은 닝겐

저수준의 테크닉은 임베디드 프로그래밍이라고 해서 가전제품이나 간단한 장치 제어용 모듈등에 널리 사용하고 있다. 이런 분야에서는 저수준의 테크닉이 절대적으로 중요하나 일반 응용소프트웨어 분야에서는 당연히 객체지향같은 추상화 테크닉에 집중하는게 현명하다.

7.2.1. 배열과 포인터 사이의 관계[편집]

굳이 이 항목을 따로 만든 이유는, 배열과 포인터 사이의 관계를 올바르게 이해하는 것이 포인터를 이해하는 데 있어 큰 비중을 차지하며, 한편으로 포인터 상수라는 부적절한 설명이 C언어 프로그래머들에게 널리 퍼져있기 때문이다. 포인터 상수가 잘못된 개념인 이유 배열과 포인터 사이에 성립하는 정확한 규칙은 다음과 같다.

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이 적용되어 위에서 설명한 변환 규칙이 적용되지 않는다.


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

배열 참조 연산자 arr[0]*(arr + 0)과 동치이며, 후자의 수식은 위의 규칙을 적용받아 해석된다. 배열의 첫번째 원소의 주소값이 곧 배열 자체의 주소값이고, 배열 이름은 그 배열의 첫번째 원소를 나타내므로 arr은 arr[] 배열과 배열 내 첫번째 원소를 가리키는 포인터 주소값이 된다.[81] 여기에 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와 같이 연쇄적으로 변환되지는 않는다는 뜻이다.

실제 예를 들자면, 수식 내에서 쓰인 int arr[3][4][5] 타입은 int (*arr)[4][5] 타입으로 변환되며, int (**arr)[5] 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 도 [ ] 안에 들어갈 수 있지만 이건 또다른 주제라 생략한다.)

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

int f3(int arr[const 3]);
int f4(int * const arr); // 위의 함수 f3과 같다.


이것 또한 다차원 배열의 경우에는 연쇄적으로 적용되지 않고 딱 한번만 적용된다.
int f5(int arr[3][4][5]);
int f6(int (*arr)[4][5]); // 위의 함수 f5 같다.
int f7(int (**arr)[5]); // 위의 함수 f5, f6과 다르다.


이 규칙으로 인해서, C에서는 함수의 매개변수로 배열을 직접 복사해서 넘길 수 없다. 다르게 표현하자면, 함수의 매개변수로 배열을 사용할 수 없다. (이것이 독특한 이유는, 구조체는 되기 때문이다. 심지어 배열을 멤버변수로 갖는 구조체도, 배열 안의 내용을 포함하여 구조체 전체가 그대로 복사되면서 함수 내부로 전달된다.)

7.3. 중괄호 스타일[편집]

C언어는 스코프를 지정할 때 중괄호 쌍을 { } 사용한다. C언어는 whitespace(공백, 탭, 리턴 문자열)가 의미 없는 언어라서[82] 여는 중괄호를 엔터를 쳐서 다음 줄에 놓는 방법과, 이전 표현 바로 옆에 놓는 방식 두 가지 모두 가능하다. 이런 두 가지 스타일을 각각 Allman[83] 스타일과 K&R 스타일[84] 이라고 한다.

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

  • K&R 스타일

아래와 같이 while, if 문 등의 옆에 여는 괄호가 붙어 있다.

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문 밑에 여는 괄호가 있다.

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할은 기타 스타일.[85]

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

7.4. 기타[편집]

수상한 메신저의 Another Story에서 파티 참가자로 초대할 수 있다.(...)

8. 관련 문서[편집]

[1] 데니스 리치와 브라이언 커니핸 지음. #[2] 흔히 C 언어의 공동제작자로 오해되곤 하는 사람들로 켄 톰슨과 브라이언 커니핸이 있는데, C의 탄생 및 발전에 관여하긴 했으나 엄밀하게 따지면 C 언어의 제작자는 아니다. 켄 톰슨은 유닉스 OS와 C 언어의 전신인 B언어를 만들었고, 브라이언 커니핸은 K&R로 불리우는 The C Programming Language라는 책을 썼다.[3] 과거 새로운 표준이 200x년에 나올줄 알고 C0x로 불렸으나, 2011년에 등장하여 C11이 되었다. C++ 역시 같은 이유로 C++0x로 불리다가 2011년에 등장하여 C++11이 되었다.[4] 이들은 다른 언어가 아니라 버전이 다르다.[5] Al Stevens의 책에 나오는 말이다.[6] 또, 동일한 이유로 운영체제 시동을 뜻하는 단어가 bootstraping을 줄여 booting이 되었다. 운영체제가 하는 일이 하드웨어 위에서 소프트웨어를 로드하여 돌릴 수 있도록 하는 것인데, 운영체제 그 자신도 소프트웨어이기 때문에 누가 운영체제를 로드할 것인가에 대한 모순이 생기기 때문.[7] 물론, 금기라 해서 완전히 없던것은 아니다. 당장 유닉스의 전신이던 MULTICS 부터 PL/1 라는 고수준 언어로 작성되었다. 단, 그렇게 만들어진 MULTICS는 망했다. 유닉스는 멀틱스에 대한 반성에서 단순하게 만드는 방향을 추구했으며, 그렇기에 이름부터가 UNI-로 시작하도록 지어졌다.[8] 유닉스는 처음에는 어셈블러로 만들어졌지만 점차 C로 대체되었다. 그리고 그덕분에 이식성을 확보하여 여러 기계로 퍼져나갔다.[9] 당시 프로그래밍 자료를 보면 요즘 사람들의 눈에는 모눈 원고지로밖에 보이지 않을 것이다.[10] 물론 믿으면 곤란. 이쪽은 추상화느님으로 마치 자연어처럼 읽히는 코드를 써서 주석 자체를 코드로 최대한 대체해서 주석이 코드의 변화를 못 따라가는 불상사를 방지하는 것이다.[11] 주의해야 할 것은, 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) 교재이다. 그리고 K&Rea라고 쓰면 곤란하다.[12] 사실 ANSI C 다음 버전부터는 MS에서 표준안을 제대로 지키는 컴파일러를 내놓지 않은 영향이 가장 크다. 거의 대부분의 C 컴파일러는 C99를 따르기 때문이다.[13] MS의 C 지원은 사실상 MS C++ 컴파일러로 컴파일한 코드에 CRT를 억지로 끼워맞추는 수준에 가깝다. 아직 실무에서 잘 안 쓰이는 C11은 둘째치고 1999년에 확정된 C99마저 제대로 지원하지 않는 MS의 심보 이는 MS의 개발 정책 때문.[14] 여기에 관해서도 혼동이 있을 수 있는데, 이후 ANSI가 C 표준 제정에서 손을 떼고 ISO를 받아들였기 때문에, 공식적으로 ANSI C 는 최신표준인 C11 을 가리킨다. 하지만, 실제 사람들이 ANSI C라 지칭할 때는 ANSI가 표준을 직접 제정한 C89/90를 의미한다. 이 페이지에서도 ANSI C는 C89/90를 의미한다.[15] 최근 C++11로 업데이트된 4차 개정판에서 드디어 1000 페이지의 벽을 깨고 1300 페이지를 넘겼다.[16] 실제로 아주 중요한 내용이 구석탱이에 딱 한줄 써있는 경우가 많다.[17] 그렇기에 더더욱 초심자에게는 어울리지 않는다.[18] 저자의 이름인 K. N. King의 앞글자를 땄다.[19] 대표적으로, 멀티/와이드 바이트 문자의 동시 지원을 위한 tchar, tmain 등은 MS가 만든 비표준 함수들이다.[20] 어차피 저런걸 모두 담고 있는 교재가 없긴 하지만, 더하고 덜하고 정도의 차이는 있게 마련이고, 그 차이는 나중의 삽질과 시간낭비로 반드시 뿌린 대로 거두게 된다.[21] 그러나, 웹에 널려있는 정보들은 잘못된 정보일 가능성도 높고 그 설명이 제대로 된 교재 수준을 넘어가는 경우도 드물다. 또한, 가장 결정적인 이유로 같은 수준의 지식이라면 웹서핑보다 교재로 배우는 편이 훨씬 효율적이다. K&R이 나온 지 오래된 책이기는 하지만 중요하고 핵심적인 사항들은 오늘날에도 변함없이 적용되는 내용들이다. 정 K&R을 첫번째 교재로 삼기가 어렵다면 다른 데서 C의 기초를 먼저 배우고 복습 차원으로 K&R을 보는 것도 좋다. 그렇게 하면 K&R의 내용 중 구식인 것과 아닌 것을 스스로 가려낼 수 있게 될 테니까. K&R을 나온 지 오래 된 책이라고 무시하기엔 좋은 내용들이 굉장히 많다.[22] 초기 C 언어는 커다란 규모의 프로그램을 거의 염두에 두지 않고 개발되었다. 당시 IBM 메인프레임에 사용되던 System/360 이 수천명의 프로그래머가 달라붙어 어셈블리어로 수백만 줄이었는데, C 언어로 만들어진 가장 큰 프로그램인 초창기 유닉스의 커널은 고작 만 줄 정도였다.[23] 인력도 인력이지만, 당시 머신은 초고도로 최적화를 해야 만족할 만한 속도를 보여줬다. 덤으로 C언어 그 자체도 컴파일러를 거쳐 기계어 파일이 나오면 그걸 다시 사람이 직접 최적화를 해줘야 했다고 한다. 당시 컴파일러의 최적화 성능이 떨어졌던 이유도 있겠지만.[24] 현재 쓰이는 고수준 개념들 자체는 의외로 오래된 경우가 많다. 예를 들어서 쓰레기 수집은 59년도에 최초로 구상되었고, 타입에러를 컴파일 타임에 모두 잡아낼 수 있는 Hindley-Milner 타입 인터페이스가 70년대 후반에 나와서 Haskell등지에서 쓰이고 있다. 클로저 개념도 60년대~70년대에 나왔다.[25] C99 에서는 struct 마지막 어레이 멤버에 [0] 대신에 그냥 [] 로 써주면 된다.[26] 배열 경계 좀 넘는다고 뭐 그리 대단한 일이 일어나겠냐고 물으신다면, 배열 경계를 넘기면 그 위에 선언한 변수들을 하나하나 덮어쓰기가 되고, 최종적으로 리턴 어드레스(함수의 실행을 끝낸 후 다시 함수를 호출한곳으로 돌아가는 주소값)까지 덮어쓰는 게 가능해진다. 기본적으로 이 취약점을 가진 프로그램을 네트워크에 물리면 원하는 동작을 원격에서 실행시킬 수 있게 되며, 최악의 경우는 루트 권한으로 실행되는 프로그램에서 리턴 어드레스를 쉘을 실행하는 코드가 있는 곳으로 덮어쓰면 루트 계정을 원격으로 탈취당할 수도 있다. 이를 버퍼 오버플로우 취약점이라고 한다. 1998년 전세계 서버를 감염시킨 모리스 웜도, finger 라는 유닉스 유틸리티의 버퍼 오버플로우 취약점을 이용하였다. 이 웜은 쉽게 들통나지 않도록 여러가지 카모플라쥬 전략이 많이 포함되어있었고, 코넬대학은 결국 서버를 다운시키는 등의 조치를 취했지만 전 세계로 퍼져나갔다. 당시 이 웜을 만든 23세의 모리스는 벨 연구소에서 유닉스의 로그인 암호화를 담당했던 사람의 아들이었고, 현재 MIT 대학의 교수다. 2001년 한국에도 사이버 대란을 일으킨 코드레드 의 경우 전세계의 서버 몇십만개를 감염시킨 사례가 있다. 이 취약점 때문에 경계 체크를 안 하는 gets 같은 표준 함수 여러 개를 depreciated(사용 자제) 시켜야 했고, printf의 포맷 문자(%n) 하나가 날라갔다. 심지어 운영체제 자체에서 buffer overflow 방지 메커니즘을 제공하기까지 하는데도 간간히 뚫리는 게 이 취약점이다(...). 물론 공돌이들이 손 놓고 있는게 아니니 방지책도 많이 마련되어있긴 하지만 그럼에도 뚫릴 취약점은 뚫린다. 2014년 OpenSSL에 생긴 재양적인 버그인 하트블리드도 근본적으로는 C언어가 배열의 경계를 체크하지 않는다는 점에서 생겼다.[27] visual studio 2012 버젼부터 함수에 _s 가 붙은 (printf_s, scanf_s 등) 보안을 위한 함수가 추가 되었다. 기존의 printf 같은 함수는 그냥 쓸 수 있지만 scanf와 같은 함수를 쓰려고 하면 보안에 따른 에러 메시지가 뜬다(...) 그리고 이 안전 함수들은 C11에도 추가되었다.[28] 포인터에 익숙하면 오히려 쉽게 느껴질 수도 있다. 포인터 자체가 iterator 패턴과 유사하게 사용 가능하며, 증가, 배열, 참조 연산자를 통해서 짧은 수식으로 여러가지 연산을 한번에 처리할 수 있기 때문이다. 예를 들면 while ( *dst++ = *src++ ) ; 와 같은 짧은 코드만으로 문자열 복사가 가능하다.[29] String literal. "str" 같은 따옴표로 둘러싸인 부분을 뜻함.[30] 정확하게는, 문자열 상수는 변경 불가능한 char형 배열이다. 따라서 마치 배열이름처럼 "abcdefg"[3]와 같이 쓸 수 있으며, 이 수식의 결과값은 'd'이다. 한술 더 떠서 3["abcdefg"]'d'이다. *(3 + "abcdefg)이기 때문.[31] "배열 타입의 결과값을 갖는 모든 수식은, 몇몇 예외상황을 제외하고는 그 배열의 첫 번째 원소가 가리키는 포인터 주소값으로 변환된다." 이것이 흔히 말하는 '포인터 상수'의 진짜 모습이다.[32] 수정하기 전 문서에서는 '문자열'의 주소라고 설명하고 있었는데, 정확히는 그 '문자열의 첫 번째 원소'의 주소값이다. 같은 말 아니냐고? &arr&arr[0]이 다른 것처럼 이 두 가지도 서로 다르다. (주소값은 같게 나오지만 그 타입은 서로 다르다. 전자는 포인터 타입이고 후자는 배열 타입이다.)[33] expression의 l-value에는 수정할 수 있는 값(a와 같은 변수 등)이 와야 하기 때문이다.[34] gcc 4.8 기준으로 'suggest parentheses around assignment used as truth value'(값을 진리값으로 사용하는 assignment는 괄호를 씌우는 것을 권장함)라는 워닝 메시지가 뜬다.[35] 보통 if 문 내부에 의도된 대입 연산자의 경우에는 if ((result = fn(...)) != NULL) { ... } 같은 식으로 대입 표현식을 괄호로 감싼다.[36] C99 에서 _Bool타입이 생기긴 했다. stdbool.h 를 포함할 경우, 그냥 bool로 사용도 가능하긴 하다. 왜 그냥 bool로 안넣었냐면, 기존 프로그램중에 bool 이름을 사용한 코드가 있을 가능성이 있기때문. 반면, 언더스코어 _ 뒤에 대문자가 오는 이름은 C 표준에서 사용하지 말라고 먼저 명시해놨기 때문에 이것을 사용한 _Bool 이 되었다.[37] 음수도 true로 처리된단 뜻이다. 없으면 define 문으로 선언해서 쓰면 된다.[38] int i='a'; char c='b'+i 같은 것들이 매우 자연스럽다.[39] 따라서 char c = 0; 이라고 하면 c에는 '\0' 이 대입된다. 1과 '\1' '\x1' 이 같은 값인 것과 같은 이치.[40] 간단한 예를 들자면 타입과 배열과 포인터가 혼돈의 카오스가 되는 이유인데, 사실 데이터를 실제 저장장치에 저장할 때 이게 정수인지 문자인지 배열 주소인지 포인터 주소인지는 기재되지 않기 때문이다. (그걸 일일이 기재하는 건 시간과 공간의 낭비고, 쓰잘데기도 없는 데다가, 데이터가 그런 단순데이터만 있는 게 아니기 때문에 거의 불가능하기까지 하다.) 마치 메모장에는 32871647200이라고 적어놓고 나중에 메모장을 펼쳐서 이 숫자가 무슨 숫자였지.... 하고 해석하는 것과 같다. 그 데이터가 무슨 데이터였는지 정확히 기억하고 알맞게 사용하는 것은 어셈블리에서는 순전히 사용자의 몫이고, 그때의 전통이 살아있던 C에서는 기본적인 타입 같은 건 컴파일러가 처리해주되 사용자가 그런 세세한 부분을 컨트롤할 수 있도록 허용해주는 것이다.[41] 소켓 프로그래밍이 보통 이렇다. 그리고 사실 구조체, 배열, 공용체는 문법적으로 같은 주소/다른 타입이 가능한 경우이기도 하다.[42] 윈도우, 리눅스, 유닉스 커널의 핵심부는 모두 C로 짜여져 있다.[43] 비트 수준 연산이 많이 사용되고 속도도 빨라야 하기 때문에 많은 언어의 암호학 라이브러리는 거진 C로 구현되어 있다.[44] C의 장점이 이식성이던 시절은 상당히 오래전이기도 해서, 최근에 쓰여진 입문서들은 다른 방향의 서술을 했을 가능성도 있다.[45] 둘째치고 라고 썼지만, 기존의 것들과의 호환성은 C언어가 이리저리 꼬이고 복잡하게 된 가장 큰 이유중 하나다. 부호-음수 표기법이나 1의 보수 표기법까지도 C표준에 들어갈 정도.[46] 숙련된 프로그래머라면 피해갈 수 있는 함정이지만, 그런 수준까지 가기가 쉽지 않다는 점에서는 말장난처럼 느껴질 수도 있다. 그냥 경력이 쌓여서 자동으로 되는 일이 아니고, 임베디드 계통처럼 관련 문제들을 자주 접해 볼 수 있어야 한다[47] 이것에 대해 가장 유명한 문제는 64비트 컴퓨터의 정수형/포인터형 모델에 대한 문제이다. 다음 링크를 참조 http://www.unix.org/version2/whatsnew/lp64_wp.html [48] 사실 C++나 다른 컴파일 언어에 비해서 C언어 자체는 컴파일 속도는 굉장히 빠른 편이다.[49] 정확히 말하면 Java는 구현 언어로만 사용하는 것이며, 라이브러리는 전부 안드로이드 SDK를 사용해야 한다. 따라서 JVM 바이트코드를 생성하는 Kotlin, Scala 같은 언어로 코딩하는 것도 불가능한 것은 아니다. 실제로 현재 안드로이드의 공식 언어는 Kotlin이다.[50] 쉽게 말해서, OS의 특정 기능이 느리면 그 기능을 사용하는 어플리케이션들 전부가 덩달아 느려진다.[51] 다만, C 표준 자체가 많은 부분을 '모든 컴파일러에 동일하게'가 아니라 구현체에 따라(implementation-dependent) 정의하게 하기 때문에 이런 부분들에 대해서는 전부 각 플랫폼마다의 특성을 따로 반영하여 코딩해주어야 한다.[52] 실제로 요즘 나오는 C 교재 중에는 후반부에 OOP 챕터도 넣어놓은 경우가 가끔 있다.[53] 바퀴의 재발명이란 뜻으로, 일반적으로는 이미 다 존재해서 갖다쓰기만 하면 되는것을 괜히 고민하면서 또 만들어내는걸 비판하는데 쓰이는 문구이다. 다만 실전 개발에 있어서는 안좋은 습성이지만 교육용으로는 의도적으로 한번씩은 거치게 하는 편이다. 재발명하면서 기반 시스템의 구조에 대해서 알아갈 수 있기 때문이다.[54] 유닉스의 경우 POSIX API, 윈도우의 경우 Windows API[55] 리터럴 문자열(쌍따옴표로 감싼 문자열)은 컴파일 시 코드 영역(Code Segment)에 모이게 되는데, 메모리로 로드된 이후 이 코드 영역은 보안상 운영체제에 의해 읽기 전용으로 처리된다. 그러니 포인터로 해당 주소에 직접 접근하여 내용을 변경하려고 시도하면 운영체제가 이를 차단하고(Segmentation Fault) 프로그램을 강제로 죽여버리는 것. 반면 char p[] = "aaa";의 경우 "aaa"가 코드 영역에서 스택 영역으로 복사되고 p[]로 접근하게 된다. 따라서 코드 영역의 값을 변경하는 것이 아니므로 런타임 에러가 발생하지 않는다. 참조 다만 OS와 컴파일러에 따라 다른 결과가 나올 수 있다. 예컨대 MS-DOS 시절에 많이 쓰인 Turbo-C에서는 두 번째 코드도 런타임 에러가 나지 않았다.[56] 출처[57] C는 애초에 유닉스 운영체제를 만들기 위해 탄생한 언어이기 때문에 유닉스 운영체제와 시스템 궁합이 좋은 편이다.[58] 사실, Clang은 GCC를 대체하는 것이 목표기 때문에 macOS 외에 FreeBSD 등의 유닉스 계열 운영체제에도 사용된다.[59] 때문에 대부분 언어에서는 if에 bool값이 아닌 int값 등이 들어오면 에러를 내며, 일부 언어에서는 if같이 비교연산이 필요한 곳에서 대입연산을 쓰면 컴파일 에러를 낸다.[60] Python과 그에 영향을 받은 언어들은 이런 영향에서 약간 자유롭다. 이런 언어들은 C와 중괄호가 아닌 들여쓰기로 블럭을 구분하는 특징이 있다. 그리고 Delphi는 중괄호가 주석이어서 C 사용자가 멋모르고 코드를 짜다가 혼돈의 카오스에 빠지는 경우가 많다 [61] fopen() 등의 결과로 반환되는 FILE 포인터를 파일 객체에 대한 포인터로 보면 이해가 쉽다.[62] 단, 소스 단위가 아닌 라이브러리 수준의 호환성은 여전히 좋다.[63] 정확히 말하자면 cl.exe라는 파일이다.[64] https://msdn.microsoft.com/en-us/library/hh409293.aspx[65] 사실은 애플이 GCC를 가져다가 사용하는 대신, 자체적으로 GCC에 추가한 기능 일부를 다시 GCC 측에 돌려주기로 약속했었다.[66] 게다가 LLVM은 이것을 처음으로 고안한 사람을 애플이 스카우트하면서 거의 애플 소유의 프로젝트가 되었다. 하지만 스티브 잡스의 애플 복귀 이후 애플이 진행하는 대부분의 소프트웨어 프로젝트가 오픈 소스인지라 이것도 역시 오픈 소스로 계속 진행중. 오라클 보고 있나?[67] 처리할 데이터의 양이 커지면 처리 시간이 지나치게 증가하는 알고리즘.[68] char, int, float, double과 같은 기본 자료형에서 유도되는 형태의 자료형으로, 다른 유도 자료형으로는 배열이 존재한다.[69] 예를 들어 포인터를 버그 없이 쓰려면 적어도 메모리의 정렬 제한 정도는 이해를 하고 있어야 한다. 그렇지 않으면 잘못된 포인터 테크닉을 남발하는 경우가 생긴다.[70] 특히 연결 리스트를 배우다 보면 내가 무슨 짓을 하고 있는건지 스스로를 자책할 때가 많다.[71] C#이나 자바의 경우 기본적으로 객체 참조 기반으로 돌아간다. 하지만 참조에 관련된 일은 저단계에서 알아서 해주기 때문에 쌩 포인터를 잡고 낑낑거리기 보다는 쉽다.[72] C#은 unsafe 키워드로 정말정말 간절하게 필요한 경우 유사하게 만들어 사용할 수 있다. Java에도 Unsafe 클래스가 있긴 하지만 팩토리 메소드가 막혀있어 리플렉션을 사용해야만 이용할 수 있다. 게다가 Java에서 굳이 포인터를 직접 관리해줄 필요는 없다.[73] OOP 같은 것도 밑바닥은 결국 포인터다.[74] 코어 임베디드 개발 영역에서는 포인터 없이는 아예 못하는 것도 많다.[75] 사실 8비트 CPU도 마찬가지지만.[76] near pointer 는 offset만 저장한다. data segment 가 한개뿐인 상황에서는 offset 이상의 정보가 필요가 없으니.[77] 사실 32비트 시대에서도 DOS는 호환성 문제로 기본적으로 16비트 프로그래밍을 이용했기 때문에 큰 의미가 없었다. 이 부분은 16비트 버전 윈도우도 마찬가지.[78] Intel CPU에서 16비트 모드의 경우, seg:ofs로 나뉘는데, 이가 가리키는 주소는 addr = (seg << 4) | ofs; 로 표현되며 seg, ofs 둘다 16비트이기 때문에 이들의 값이 달라도 같은 주소를 가리키게 되는 경우가 많다. 예를 들어서, 22eeh:0000h = 22e0h:00e0h = 2200h:0ee0h = ...[79] 몇몇 학교는 아예 MS의 지원을 받아서 C#을 입문 단계에서 강의한다.[80] 하지만 포인터를 배울 필요가 없다는 건 아니다. 이를테면 자바의 클래스 변수는 참조변수인데, 포인터 혹은 적어도 비슷한 개념이라도 잡고 있어야 얕은 복사/깊은 복사 등을 이해할 수 있다. 물론 입문자들을 위한 자바 강의에서는 포인터 자체를 언급하지 않고 참조변수 그 자체를 상세히 가르쳐준다. 그러므로 참조변수를 상세히 이해하고 있다면, 사실 포인터의 개념은 자신도 모르는 사이에 대부분 배웠다고 할 수 있다.[81] 참고로 이는 함수에도 똑같이 적용된다. 함수의 이름이 곧 함수의 주소를 나타낸다는 말. 따라서 어떤 함수의 매개변수가 함수 포인터라면, 호출부에서는 그 자리에 괄호와 인자를 생략한 함수의 이름을 넣어야 한다.[82] 정확히는 화이트 스페이스 문자들인 뉴 라인, 탭, 스페이스를 구분하지 않고 아무 화이트 스페이스 하나 이상은 공백 하나로 계산된다. 예외는 C/C++의 문법의 일부가 아닌 매크로로써, 무조건 뉴 라인을 이용해서 매크로의 끝을 구분지어야 하며 여러 줄에 걸쳐 작성할경우 뉴 라인 이전에 역슬래시를 하나 추가해야 한다.[83] 한때 메일서버계의 IE 로 불리던 sendmail 의 저자이며, 게이 프로그래머로 유명한 Eric Allman이라는 사람의 이름에서 따옴. BSD style이라고도 한다.[84] Kernighan&Ritchie의 <The C Programming Language> 에서 쓰여졌기 때문[85] for(i=0;i<n;i++) { work(); } 이런 식으로 쓰는 사람들도 있기는 있다. for과 if 등은 한 함수를 실행시킬거면, 중괄호를 안쳐도 되긴 하다. 다만, 들여쓰기 잘못하면 피바람 분다.(...) 함수가 한 개든 여러 개든 그냥 전부 중괄호를 치는 습관을 들이자.