C/C++를 가르치기 전에 생각해 보기

개발 업계의 변화가 매섭습니다. 파이썬이 배우기 쉽다는 인식과 AI의 급부상을 등에 업고 "첫 프로그래밍 언어"로서 큰 인기를 누리고 있습니다. 파이썬 전에는 자바가 취업 깡패로 유명했습니다. 메모리 안정성을 이유로 C/C++를 몰아내고 러스트를 써야 한다는 목소리도 큽니다. 웹 생태계의 중심 자바스크립트도 빼놓을 수 없습니다. 코틀린, 스칼라, C#, 고, 타입스크립트, 리스크립트, 엘릭서, 하스켈, OCaml 등등등 학교에서 잘 가르치지 않는 언어에 대한 수요도 상당합니다.

이 와중에 여러분은 C나 C++를 가르치기로 하셨습니다. 본인의 취향일 수도 있고, 엉겁결에 맡게 된 자리일 수도 있습니다. 어쨌든 지금도 수많은 학교와 학원에서 C/C++을 가르칩니다. 책도 꾸준히 나옵니다. 나온지 50년도 더 된 C를, 40년이 다 되어 가는 C++을 가르치는데, 크게 고민할 게 있겠습니까? 자료를 술술 적어 나갑니다.

그렇지만 C/C++는 잘못 가르치기가 너무 쉽습니다. 보통 다른 프로그래밍 수업들은 교수자가 가르치는 기술이 미숙해서, 또는 요즘 트렌드를 따라가지 못해서 문제가 되는 경우가 많습니다만, C/C++ 수업의 경우에는 교수자가 언어에 대한 기본적인 이해가 부족한 경우가 대부분입니다. 대학 교수, 정보 교사, 학원 강사, 책을 집필하는 현직자, 자격증 문항 출제자 등등 예외가 거의 없습니다.

물론 모든 걸 정확하게 가르치는 게 최우선 목표는 아닙니다. 너무 복잡하고 어려운 내용은 대충 넘어가고, 나중에 학생이 경험을 쌓으면서 새로 정확하게 배우기를 기대하는 것도 방법이지요. 그러나 이건 교육 목적을 생각해서 주의 깊게 내용을 다듬을 때 의미가 있는 것이지, 교수자가 자신의 무지를 뽐내고 있어도 된다는 뜻은 아닙니다.

이런 문제 의식의 결과로, 여러 강의 및 교재에서 흔히 발견할 수 있는 오개념들과, 기타 C/C++ 교육에 도움이 될만한 조언들을 모아 문서를 만들고자 했습니다. 부디 "한국에서 C/C++ 배우면 안 된다"는 말을 그만할 수 있게 도와 주세요.

UB, UsB, IdB를 공부하라

undefined behavior, unspecified behavior, implementation-defined behavior가 무엇인지 아시나요? 각각의 예시를 대실 수 있나요?

엉겁결에 C/C++을 가르치게 되셨다면 UB가 뭔지 들어보지 못하셨을 수 있습니다. 그런 경우, 가장 좋은 해결책은 C/C++를 가르치지 않는 것입니다. 자바나 파이썬이 수요도 많고 쉬운 대안일 것입니다.

피치 못할 사정이 있어 그래도 C/C++를 가르쳐야 한다면, UB⋅UsB⋅IdB에 대한 문서를 꼼꼼하게 읽으세요. 관련 주제에 익숙한 조수나 대학원생을 구해서 검토를 맡기는 것도 하나의 방법입니다.

UB는 전설의 1시험 7출제오류에도 일조한 바 있습니다.

C/C++를 가르칠 것인지 유사 C/C++를 가르칠 것인지 결정하라

그냥 C 또는 C++라고 하면 ISO/IEC가 주기적으로 갱신하는 표준 C와 표준 C++를 의미합니다. 이 조언들도 여러분이 기본적으로 표준 C/C++를 가르칠 것이라 생각하고 쓰였습니다.

그러나 프로그래밍 초보자들에게 표준 C/C++의 몇몇 요소는 설명하기 어려울 수 있습니다. 또, 교육 목적 자체가 C/C++를 가르치는 것이 아니라 컴퓨터 구조 입문일 경우, C/C++를 제대로 가르치는 것은 너무 번잡하기도 합니다. 여기저기 숨어 있는 UB가 특히 그렇습니다.

이 경우에 C/C++와 아주 유사하지만 본질적으로는 다른 교육용 C/C++를 가르치는 방법이 있습니다. 교육에 걸림돌이 되는 부분들은 적절히 바꿔서 새 언어라고 말하는 것입니다. 컴파일러는 여러분의 유사 언어를 인식하지 못하겠지만, 최적화를 끄고 컴파일하면 아마도 문제가 되지는 않을 것입니다.

여기서 중요한 점은, 진짜 C/C++가 아님을 명시하는 것입니다. 무엇을 왜 수정했는지 설명할 수 없다면 그건 그냥 오개념입니다. 많은 학생들은 별로 깊이 생각하지 않고, 딱 여러분이 가르친대로 현업에서 코드를 짭니다. 그러다 UB나 보안 취약점이 생기면 골치가 아파지는 것입니다. 최소한 현업에서 C/C++를 써야 하는 학생들은 나중에 표준을 공부해 볼 수 있도록 언급해 주도록 합시다.

커널 C나 임베디드에 들어가는 C 등 표준에서 벗어난 C 환경을 가르치는 경우에도, 해당 환경에 한정된 내용임을 말해 주세요. 그러나 교육 목표와는 하등 상관 없이 본인의 편의를 위해 "Dev-C++ 6.1 기준으로 풀이한다" 같은 말을 문제지에 적지는 맙시다.

독자연구 하지 마라

Do it! C언어 입문의 저자이신 김성엽 님의 어셈블리 뜯기입니다. 이런 거 하지 마세요.

컴파일 결과를 어셈블리로 확인해서, 그걸 바탕으로 어떤 사실을 끌어내려고 노력하시는 분들이 있습니다. 물론 컴파일 결과를 확인하는 건 좋은 실험입니다. 실제로 컴퓨터가 코드를 어떻게 실행하는지 이해하는 데 도움이 되지요.

그러나, 고교 교사가 학교 과학실에서 실험 몇 번 하고 수업 자료에 넣을 새로운 과학적 사실을 발견할 수는 없는 것처럼, 컴파일러 몇 번 돌려보고 "이 코드가 더 빠르게 뽑히네", "이런 코드는 무조건 이런 방식으로 변환되네" 같은 결론을 내리면 안 됩니다. 보통 이런 기초적인 발견은 스택 오버플로에 비슷한 질문이 있으니 결론을 내리기 전에 먼저 검색을 해 보세요.

어셈블리 뿐만 아니라 다른 독자연구에 대해서도 같은 조언이 적용됩니다.

변수는 첫 사용 시점에 선언하라

C99 전에는 블록 범위 변수를 블록 시작 부분에 몰아서 선언해야 했습니다. 그 제한이 풀린지 20년이 넘게 지났습니다. 그러지 마세요.

변수는 첫 사용 시점에, 가장 좁은 범위로 선언하라는 것은 C/C++뿐만 아니라 많은 언어에 적용되는 지침입니다.

너무 복잡한 식을 가르치지 마라

여러분은 십중팔구 C/C++의 식 계산을 이해하고 계시지 못합니다.

간단하게 퀴즈 드리겠습니다. 변수는 int 타입, 언어는 C를 가정합니다. (네, C와 C++의 계산 규칙은 다릅니다!) 오버플로는 없다고 가정합니다.

  • (a+b)+(c+d)의 계산 순서는 어떻게 될까요? 가장 왼쪽의 더하기, 그 다음이 가장 오른쪽, 마지막으로 가운데일까요? 왼쪽과 오른쪽의 순서가 바뀔 수는 없을까요? 왼쪽과 오른쪽이 동시에 계산될 수는 없을까요?
    정답왼쪽 더하기와 오른쪽 더하기의 순서는 바뀔 수 있고, 동시에 계산될 수도 있습니다.
  • f()+g()는 f와 g가 동시에 호출될 수 있을까요? f와 g 중 어느 쪽이 먼저 호출될까요?
    정답f와 g 사이의 순서는 정해져 있지 않습니다. 그러나 둘이 동시에 호출되지는 않습니다.
  • i++ + ++i; 후 i는 몇이 증가했을까요?
    정답시퀀스 포인트 없는 부작용 두 개로, UB입니다.
  • i = ++i + 1; 후 i는 몇이 증가했을까요?
    정답역시 시퀀스 포인트 없는 부작용 두 개로, UB입니다. ++i의 값 계산이 끝났다고 부작용이 끝난 건 아닙니다! 그러니까 ++i를 "증가시킨 뒤, 그 값을 반환"처럼 설명하시면 엄밀히는 틀립니다. 한편, C++에서는 그 설명이 맞습니다.
  • i = ++i && 0; 후 i는 몇이 되었을까요?
    정답0이 됩니다. 여기서는 &&가 시퀀스 포인트를 만들기 때문에 UB가 아닙니다.

저라면 이런 규칙을 설명하려고 애쓰지는 않을 것 같습니다. 예쁘고 깔끔한 식만 가르치시는 걸 권장합니다. 특히, "식 하나에는 부작용 최대 하나까지"라는 규칙을 지키지 않으면 문제를 오류 없이 내기가 너무 힘듭니다.

부작용의 순서를 물어 보는 문제를 정말 내고 싶은데, 더하기의 양변이 동시에 계산되면서 값 계산과 부작용의 시점이 불일치하는 기괴한 방식이 싫으시면, 자바나 파이썬을 쓰세요.

배열과 포인터는 별개다

배열은 포인터가 아니고, 포인터는 배열이 아닙니다. 그저 배열이 많은 경우에 포인터로 암시적 형변환될 뿐입니다. 이 점을 두리뭉술하게 설명할수록 혼란스러워집니다.

포인터는 그냥 메모리 주소가 아니다

아래의 자격증 기출 문제를 봐 주세요.

포인터로 배열의 경계를 넘어가서 접근했기 때문에 UB입니다.  pointer provenance라는 개념과 관련되어 있습니다.

컴퓨터 구조 입문을 목적으로 하는 강의에서는 교육용 C라고 하고 provenance 없는 평평한 메모리 모델을 가정하는 것도 방법입니다.

메모리 재해석은 자유롭지 못하다

아래의 코드를 봐 주세요. int는 32비트, short는 16비트를 가정합시다.

6줄에서 잘못된 타입의 포인터로 객체에 접근하였으므로, 역시 UB입니다. strict aliasing rule이라고 합니다. 잘못된 타입의 포인터를 만드는 것까지는 UB가 아니고, char* 같은 몇몇 타입은 이 규칙에 예외입니다. 공용체를 쓴다면 C와 C++ 사이에 규칙이 다릅니다.

현업에서는 잘못된 타입의 포인터를 이리저리 넘기다가 오작동해서 낭패를 볼 수 있습니다만, 컴퓨터 구조 입문을 목적으로 하는 강의에서는 strict aliasing rule 없는 교육용 언어를 가정하는 것도 좋겠습니다.

(C++) 배열 대신 std::vector만 가르치는 것을 고려하라

C를 가르치지 않고 C++을 가르친다면, 정적 배열 대신 동적 배열 std::vector만 가르칠 수도 있습니다. 이 경우 장점으로는 포인터로의 자동 형변환을 가르치지 않아도 된다는 점, 더 고수준인 언어들과 비슷한 경험을 줄 수 있다는 점이 있습니다. 알고리즘 문제 풀이 정도의 수업에 적합합니다.

(C++) 3/5/0의 규칙을 지키는 좋은 클래스들만 다루라

소유권 개념은 C++ 객체의 근간입니다. 그리고 소유권을 잘 모델링하는 객체는 3/5/0의 규칙을 지킵니다. 이 규칙들을 꼭 설명하시고, 이것들을 따르는 좋은 클래스들만 다루세요.


우선 생각나는 조언은 여기까지입니다. 경험한 지엽적인 오개념들은 더 많지만, 그런 것들은 레퍼런스나 스펙을 바탕으로 검토를 더 꼼꼼히 하면 해결될 문제로 믿습니다. cppreference는 이 기괴한 언어 둘을 탐험하는 우리들의 친구입니다.

의견이나 질문은 댓글과 이메일([email protected])로 받습니다.