0. Prologue
“컴퓨터가 좋으면 프로그래밍 언어를 배워보면 어때. C언어 같은거”
아버지가 툭 던진 한마디에 아무것도 모르는 체로 무작정 C언어를 공부해야겠다고 마음먹었다. 초등학교 6학년 때였나. 그렇게 그냥 눈에 띄는 아무 책 하나 사서 저자가 시키는 대로 따라하기 시작했다.
다들 그렇듯, 필자의 첫 IDE는 Visual Studio 2010이었다. 그때 비트연산을 보면서 ‘이런 건 왜 하는 거야’라고 생각하고, 포인터를 보고서 ‘얘는 어디다 써먹지’ 하다가 결국 중간에 접었던 걸로 기억한다. 서술된 설명도 잘 이해하고 문제도 곧 잘 이해했으나 무슨 의미가 있는지를 이해하지 못했고, 목표의식도 없었다.
그렇게 흘러가다 고등학교 정보수업시간때 다시 C언어를 마주치게 되었다. 3~4년은 되었기 때문에 많이 잊어버렸으나 프로그래밍 자체가 생소해서 많은 친구들이 헤매던 것과 달리 곧잘 이해하여 수월히 수업을 들었던 기억이 난다.
그때 처음 visual studio를 탈출했었다. 당시 선생님이 개발환경 구성방법을 설명하면서 CodeBlocks를 소개했고, 수업도 CodeBlocks로 진행했기에 자연스럽게 코드블럭을 IDE로 쓰게 되었었다. 켜는 데만 30초이상이 걸리는 visual studio와 달리 코드블럭은 매우 가벼워 매우 좋아했던걸로 기억한다.
그러다 (무슨 이유였는지 잘 기억이 나지 않는다) 코드블럭에서 Dev-C++로 갈아탔다. 심지어 dev-c++은 진작에 개발이 중단됐고 지금보니 코드블럭도 별로 나쁠게 없어보이는데 진짜 왜 그랬는지는 모르겠다.
어쨌든 그렇게 Dev-C++을 쓰면서 대학교 수업에서 VS를 권장해서 가끔씩 써오면서 욕한게 전부다. 그러다 Visual Studio Code가 출시되었고, 많은 사람들이 호평을 내길래 나도 따라 써봤다. 귀찮은거 싫어해서 당시엔 바로 돌아왔었으나 그게 시작이었지…
1. GCC, 그리고 MinGW
저렇게 나도 모르는 사이에 GNU의 세계에 들어왔다.
1-1. GCC: GNU Compiler Collection
GCC는 리처드 스톨만의 자유소프트웨어 재단에서 진행중인 GNU 프로젝트의 일환으로 개발되고 있는 오픈소스 컴파일러 컬렉션으로 사실상 리눅스계열의 표준 컴파일러나 다름 없다.
그리고 우리가 잘 아는 Visual Studio 컴파일러가 있겠다. 하지만 이렇게 정식으로 지원하지 않거나, 호환성에 대한 우려가 있어 많이들 “GCC를 써야한다”고 하고 귀가 닳도록 들어왔다. Clang은… 잘 모르겠다…
gcc를 설치후
>gcc -c foo.c
>gcc -c bar.c
>gcc -o test foo.o bar.o 와 같이 명령어를 입력하면, foo.c와 bar.c를 컴파일해 오브젝트파일 생성 후, 링크를 해 test라는 실행파일을 출력해준다.
GCC는 C,C++를 비롯한 여러 언어를 지원하는데, gcc 명령어로 C++ 빌드는 한계가 있다.
(저 명령어 gcc는 gnu c compiler인 것이다…) C++ 빌드를 위해선 g++ 명령어를 사용하면 되며, 사용법은 비슷하다. 추가로, >gcc -c -g foo.c 와 같이 정의된 다양한 옵션을 적용해 줄 수 있다.
1-2. MinGW
그런데 GCC는 결국 저쪽 세상, UNIX 호환 운영체제의 이야기이고, 윈도우에서는 그런거 없다. Visual Studio의 C 빌드도 결국엔 C++ 컴파일러로 그냥 어찌저찌 해결한 것 뿐이고, 앞서 이야기한 여러 문제로 사용하지 않기로 했다. 그래서 Windows로 GCC를 fork해오는 여러 프로젝트들이 있다. Cygwin은 윈도우에 posix레이어를 얻는 방식이어서 무거운 반면, MinGW는 posix를 포기하고 윈도우 환경에 네이티브로 접근하기 때문에 윈도우 환경에서 오픈소스 C 컴파일러로 많이 사용되는 듯하다.
뭐 새로울 것도 없는게 코드블럭, 그리고 Dev-C++을 사용하면서 자동으로 깔리고 그동안 사용해왔었다.
SourceForge: MinGW로부터 MinGW를 다운받아 설치후, bin폴더를 찾아 아래와 같이 시스템 환경변수 path에 추가해주어 어느 폴더이든지 사용할 수 있도록 해주면 된다.
시스템 속성(sysdm.cpl)에서 고급 탭 → 환경변수로 들어가, 시스템 변수의 Path를 편집해 mingw의 bin폴더 경로를 추가해주면 된다.
2. VS Code C/C++ Build 자동화
webnautes님의 블로그 글을 많이 참고 했다.
Visual Studio Code는 Visual Studio보다 가벼우면서도, visual studio와 같이 IntelliSense도 지원하면서 자체적으로 git도 지원하고 extension도 많다. 세상에 이런 물건을 만들다니 사람들이 왜 그렇게 마소를 칭찬했는지 이제 좀 알 것 같았다. 진짜 생각만 해도 눈물이 난다. ㅠㅠ
하지만 앞서 잠깐 언급했듯, 이건 그냥 IDE 비슷한 수준으로 구현이 가능할 정도로 코딩에 최적화 된 (텍스트 에디터 치고)무겁고 기능많은 텍스트 에디터이다. mingw가 내장되어있던 codeblocks, dev-c++과 달리 mingw 설치 및 경로 설정과 여러 귀찮은 설정들을 거쳐야 한다.
2-1. VS Code C/C++ 환경구성
하지만 앞서 mingw 설치와 환경변수 설정은 끝냈으니, 이제 C/C++ Extension을 설치하자.
프로젝트 폴더 아래 .vscode 폴더가 있고 프로젝트 설정 정보들이 json형태로 들어있다. 나는 여기서 json을 처음 만져봤다.
아래는 내가 설정한 c_cpp_properties.json이다. 컴파일러 경로를 포함해 c standard등은 상황에 따라 적절히 입력해주면 된다.
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"windowsSdkVersion": "10.0.18362.0",
"compilerPath": "C:/Program Files/mingw-w64/x86_64-8.1.0-posix-seh-rt_v6-rev0/mingw64/bin/gcc.exe",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "gcc-x64"
}
],
"version": 4
}
json이 불편하면, ctrl+shift+p 로부터 C/C++: Edit Configuration (UI)를 찾아 UI환경으로 설정을 진행할 수도 있다.
2-2. VS Code Build 자동화 (tasks.json)
앞서 mingw의 bin폴더를 시스템 환경변수 path에 등록하여 gcc를 어디서든지 사용할 수 있도록 만들었다. 어느 폴더에 있던 사용가능하단 소리지 vs code에서 버튼하나 누르면 빌드가 된다는 소리는 아니다. 여전히 명령어를 일일이 입력해주어야 한다. 그나마 vs code에 내장된 터미널이 있으니 다행이지…
하지만 한참 바쁘게 구현중인데 gcc 명령어와 파일 이름을 줄줄히 입력하면 화가 날 듯하다. 코드블럭이나 dev-c++, 아니면 visual studio처럼 빌드 및 실행 작업을 구성해서 버튼하나로 해결하고 싶다.
vs code의 terminal 탭에서 configure task로 해결 가능하다. 결국에는 .vscode 폴더 아래에 tasks.json을 작성해야 한다.
{
"version": "2.0.0",
"runner": "terminal",
"type": "shell",
"echoCommand": true,
"presentation" : { "reveal": "always" },
"tasks": [
{
"label": "Build",
"command": "gcc",
"args": [
"${file}",
"-o",
"${fileDirname}/${fileBasenameNoExtension}"
],
"group": "build",
//컴파일시 에러를 편집기에 반영
//참고: https://code.visualstudio.com/docs/editor/tasks#_defining-a-problem-matcher
"problemMatcher": {
"fileLocation": [
"relative",
"${workspaceRoot}"
],
"pattern": {
// The regular expression.
//Example to match: helloWorld.c:5:3: warning: implicit declaration of function 'prinft'
"regexp": "^(.*):(\\d+):(\\d+):\\s+(warning error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
}
},
{
"label": "Execute",
"command": "cmd",
"group": "test",
"args": [
"/C", "${fileDirname}\\${fileBasenameNoExtension}"
]
}
]
}
위 tasks.json이 내가 webnautes님의 글을 보고 처음 구성한 베낀 task 였다.
json이 아직도 조금 생소해서 그럴뿐이지 보다보면 각각 무슨의미인지 대충 알것같긴하다. problem matcher의 정규표현식이 좀 험악하긴한데, 그거야 뭐 주석에 나온 문서만 잘 읽고 사용하면 되겠다.
뭐 쨌든 그럼 이제 Terminal 탭에서 Run Task 또는 Rund Build Task를 눌러 버튼하나만으로 빌드와 실행이 가능하다.
3. make로 다중파일 빌드작업 구성
webnautes님의 글 끝부분에도 나와있고, tasks.json을 자세히 살펴봐도 알겠지만, 소스파일 여러개를 빌드하는게 불가능하다. 아니 정확히 한 소스파일에 한 실행파일이 나온다. 우리가 원하는건 이게 아닐텐데. 한 소스파일에 모든 코드를 다 때려넣기에 부담스러우니 모듈별로 파일을 나누어 컴파일 후 링크 과정을 거쳐 하나의 실행파일이 출력되길 원하는데 이게 안 된다.
빌드가 단순한 경우에는 그냥 명령어를 쳐주는게 빠를 수도 있겠다만, 프로젝트가 복잡하고 의존성이나 뭐 각종 옵션이 달라지는 경우 매번 그걸 그대로 쳐주기는 힘들다. 그래서 이런 경우를 위한 빌드 자동화 도구들이 있다.
- make (Makefile)
- CMake (Cross Platform Make)
등등등…
make는 Unix 계열 운영체제의 프로그램 빌드도구이다. 하지만 mingw에 포함되어 있어 mingw32-make 명령어를 사용하면 된다.
CMake는 make처럼 빌드작업을 하진 않고, Makefile 또는 윈도우의 경우에는 솔루션 파일을 작성해준다. 크로스플랫폼이란 장점 뿐 아니라 의존성 문제때문에 빌드가 꼬이는 등의 문제를 방지할 수 있단다.
근데 아직은 잘 체감을 못하겠어서 그냥 make를 사용하기로 했다. 항상 소 잃고 외양간을 고친다.
장상배님의 Make 강좌가 많은 도움이 됐다.
CC = gcc
ifeq ($(RELEASE),1)
CFLAGS = -O2 -DNDEBUG -c
LDFLAGS = -O2 -DNDEBUG -o
else
CFLAGS = -c -g -O0 -DDEBUG -W -Wall
LDFLAGS = -O0 -DDEBUG -W -Wall -o
endif
LDLIBS = -lm
SRCS = $(notdir $(wildcard *.c))
OBJS = $(SRCS:.c=.o)
TARGET = bmputill
$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) $(TARGET) $(OBJS) $(LDLIBS)
%.o: %.c %.h
$(CC) $(CFLAGS) $<
run:
./$(TARGET) -e
clean:
del *.o
del $(TARGET)
3-1. Makefile을 바탕으로 수정한 tasks.json
{
"version": "2.0.0",
"runner": "terminal",
"type": "shell",
"echoCommand": true,
"presentation": { "reveal": "always" },
"tasks": [
{
"label": "Debug Build",
"type": "shell",
"command": "mingw32-make",
"args":[],
"options":{},
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$gcc"
]
},
{
"label": "Release Build",
"type": "shell",
"command": "mingw32-make RELEASE=1",
"group": "build",
"problemMatcher": [
"$gcc"
]
},
{
"label": "make clean",
"type": "shell",
"command": "mingw32-make clean",
"group": "none",
"problemMatcher": [
"$gcc"
]
},
{
"label": "run w/o build",
"type": "shell",
"command": "mingw32-make run",
"group": {
"kind": "test",
"isDefault": true
},
"problemMatcher": [
"$gcc"
]
}
]
}
4. gcc 옵션중 내가쓰는옵션에 대한 추가 설명
나중에 읽어보기 위한 자세한 설명을 적기 너무 귀찮아서 내일의 나에게로 미루겠다. gcc 옵션중 내가쓰는옵션들도 설명해야할듯
작성중…
5. Linux 또는 WSL환경에서
WSL에서 VSCode를 많이 쓰게 되면서 앞서 구현한 것들을 거의 그대로 가져와 조금만 수정하였다.
5-1. c_cpp_properties.json
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [],
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "gcc-x64",
"compilerPath": "/usr/bin/gcc"
}
],
"version": 4
}
5-2. tasks.json
{
"version": "2.0.0",
"runner": "terminal",
"type": "shell",
"echoCommand": true,
"presentation": { "reveal": "always" },
"tasks": [
{
"label": "Debug Build",
"type": "shell",
"command": "make",
"args":[],
"options":{},
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$gcc"
]
},
{
"label": "Release Build",
"type": "shell",
"command": "make RELEASE=1",
"group": "build",
"problemMatcher": [
"$gcc"
]
},
{
"label": "make clean",
"type": "shell",
"command": "make clean",
"group": "none",
"problemMatcher": [
"$gcc"
]
},
{
"label": "run w/o build",
"type": "shell",
"command": "make run",
"group": {
"kind": "test",
"isDefault": true
},
"problemMatcher": [
"$gcc"
]
}
]
}
5-3. Makefile
CC = gcc
ifeq ($(RELEASE),1)
CFLAGS = -O2 -DNDEBUG -c
LDFLAGS = -O2 -DNDEBUG -o
else
CFLAGS = -c -g -O0 -DDEBUG -W -Wall
LDFLAGS = -O0 -DDEBUG -W -Wall -o
endif
SRCS = $(notdir $(wildcard *.c))
OBJS = $(SRCS:.c=.o)
TARGET = test
$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) $(TARGET) $(OBJS)
%.o: %.c %.h
$(CC) $(CFLAGS) $<
run:
./$(TARGET)
clean:
rm *.o $(TARGET)