지난 회차에 이어 교내 멘토링 프로그램을 위해 정리한 내용을 블로그 포스트로도 작성하려고 한다. 사실 멘토링은 진작에 끝났지만, 미루고 미루다가 이제야...
또한, 해당 내용 말고도 자료구조와 C++ 클래스와 관련된 내용도 멘토링을 진행했었지만, 더 좋은 포스트들이 많을 듯해서 이 포스트를 마지막으로 하려고 한다.
Windows에서 Linux 개발환경을 쉽게 만들고, Linux 명령어와 도구를 사용하기 위해 WSL로 Ubuntu(Linux 배포판)를 설치한다. 또한, Linux 환경에서는 복잡하게 MingGW 설치없이 C/C++ 컴파일을 진행할 수 있다.
먼저 WSL을 설치하기 전에 Linux용 Windows 하위 시스템을 활성화해야 한다.

Windows 기능 켜기/끄기에 들어가서 활성화한다.

명령 프롬프트를 관리자 권한으로 실행하고 아래 명령어 입력해, WSL을 설치한다.

wsl --install
계정의 비밀번호를 입력할 때는 화면에 글씨가 표시되지 않는 것이 정상이니 당황하지 않도록 하자.
Microsoft Store에서 Ubuntu를 검색해, Ubuntu 터미널 환경을 설치하고 실행한다.


파일탐색기에서는 Linux > Ubuntu > home > (사용자명)으로 접근할 수 있다. 파일탐색기에서 편하게 파일 이동 및 복붙이 가능하다.



Linux는 GUI(Graphical User Interface) 환경이 아닌 터미널 환경에서 사용하게 되는 경우가 훨씬 많다.
WSL에서는 파일 탐색기를 통해 아래 작업들을 수행할 수도 있지만, SSH를 이용해 원격 서버에 접속할 경우에는 이런 기능을 사용할 수 없다. 따라서 기본적인 Linux 명령어들을 익혀보자.
| 주요 명령어 | 설명 | 부가 설명 |
|---|---|---|
ls | 현재/작업 디렉터리의 내용을 나열한다. | ls -a 처럼 -a 옵션을 사용하면, .으로 시작하는 숨김 파일과 디렉터리도 함께 표시된다. ls -l 처럼 -l 옵션을 사용하면, 파일의 권한, 소유자, 크기 등의 상세 정보를 긴 형식으로 확인할 수 있다. ls -al 로 두 옵션을 모두 적용할 수 있다. |
pwd | 현재/작업 디렉터리의 경로를 절대 경로 형식으로 출력한다. | 절대 경로는 /(루트 디렉터리)부터 시작하는 전체 경로이며, 상대 경로는 ./(현재 디렉터리), ../(부모 디렉터리)를 기준으로 하는 경로이다. |
cd [경로] | 셸의 작업 디렉터리를 변경한다. | |
touch [파일명] | 파일의 접근 시간과 수정 시간을 현재 시각으로 업데이트하며, 만약 지정한 파일이 존재하지 않으면 빈 파일을 새로 생성한다. | |
mkdir [디렉터리명] | 새 디렉터리를 생성한다. | |
echo [문자열] | 문자열이나 변수 값을 출력한다. | echo -e [문자열] 처럼 -e 옵션을 사용하면, 이스케이프 문자를 해석하여 출력한다. |
cat [파일명] | 파일의 내용을 연결하여 표준 출력에 출력한다. | |
mv [src] [dest] | 파일을 이동하거나 이름을 변경한다. 즉, [src] 파일을 [dest] 로 이동시킨다. | |
cp [src] [dest] | 파일과 디렉터리를 복사한다. 즉, [src] 파일/디렉터리를 [dest] 로 복사한다. | cp -r [src] [dest] 처럼 -r 옵션을 사용하면, 재귀적으로 디렉터리 내 모든 파일을 복사한다. |
rm [파일명] | 파일 또는 디렉터리를 삭제한다. | rm -r [파일명]처럼 -r 옵션을 사용하면, 재귀적으로 디렉터리 내 모든 파일을 제거한다. rm -f [파일명]처럼 -f 옵션을 사용하면, 파일을 강제로 삭제한다. ※ 주의: sudo rm -rf / 명령어는 관리자 권한으로(sudo) 루트 디렉터리(/)를 강제로 재귀적으로 삭제한다(rm -rf), 즉 시스템 전체를 삭제한다는 의미로, 절대로 실행해서는 안된다. |
chmod +x [파일명] | 파일에 실행 권한을 부여한다. |
Linux는 터미널 환경에서 작업하게 되는 경우가 많다. Vim은 이러한 터미널 환경에서 사용하는 코드 편집기이다.
본인은 Vim이 싫어서 매번 VSCode 쓰고 있긴 하지만…
sudo apt update
sudo apt install vim
vim --version 명령어로 정상적으로 설치가 완료되었는지 확인한다.
아래 명령어로 파일을 Vim 편집기로 열 수 있다.
vim [파일명, 예) main.c]
*파일이 없는 경우에는 자동으로 파일을 생성한 뒤에 Vim 편집기로 연다.
Vim 편집기는 크게 두 가지 모드, 명령어 모드와 입력 모드로 나뉘어진다. 기본 상태는 명령어 모드이며, i키를 눌러 입력 모드에 진입할 수 있다. 반대로 명령어 모드로 돌아가려면 ESC 키를 누른다.
명령어 모드에서 사용하는 주요 명령어를 소개하겠다.
| 주요 명령어 | 설명 | 부가 설명 |
|---|---|---|
:w | 파일을 저장한다. | |
:q | Vim 편집기를 종료한다. 이때, 수정한 내용이 있지만 저장하지 않은 상태면, 경고 메세지를 출력하고 종료되지 않는다. | :q! 로 강제로 종료할 수 있다. :wq 로 저장하고 종료할 수 있다. |
:[줄 번호] | 해당 줄로 커서가 이동한다. | |
:d | 현재 커서가 위치한 줄을 삭제한다. | :d[삭제할 줄 수]로 현재 줄 포함 [삭제할 줄 수] 만큼 삭제할 수 있다. |
u | 실행을 취소한다. (Undo) | |
. | 마지막 명령을 반복한다. |
간단한 실습을 위해, main.c에 아래 코드를 작성하고 저장한다.
#include <stdio.h>
int main() {
// ANSI 색 코드 배열 (빨 주 노 초 파 남 보)
const char* colors[] = {
"\033[31m", // 빨강
"\033[33m", // 주황
"\033[93m", // 노랑 (밝은 노랑)
"\033[32m", // 초록
"\033[34m", // 파랑
"\033[36m", // 남색 (청록)
"\033[35m" // 보라
};
for (int i = 0; i < 7; i++) {
printf("%s★ ", colors[i]);
}
printf("\033[0m\n");
return 0;
}
작성한 C언어 소스코드를 컴파일하기 위해, 아래 명령어를 실행해 GCC를 설치한다.
sudo apt update
sudo apt install build-essential
이후, gcc --version명령어를 실행하여 제대로 설치가 완료되었는지 확인한다.
gcc -o main main.c 로 컴파일을 진행하며, ./main 으로 실행 파일을 실행한다.

*gcc는 C언어의 컴파일러이며, -o [파일명] 옵션을 사용하여 컴파일 결과로 생성될 실행 파일 이름을 지정할 수 있다.
Makefile은 여러 소스 파일 간의 의존 관계를 정의하여, make라는 단 하나의 명령어만으로 소스 코드 전체를 간단하게 컴파일할 수 있게 해준다. 또한, 변경된 파일만을 다시 컴파일함으로써 빌드 시간을 단축시킬 수 있다.
Makefile은 C/C++ 프로젝트에서 널리 사용되며, gcc, g++, ld 명령어를 반복해서 직접 입력하지 않아도 되기 때문에 자동화된 빌드 관리 도구로 유용하다.
이번에도 간단한 실습을 위해, func.h, func.c, main.c 파일을 생성한다.
func.h 파일의 내용:
(func.c에서 정의된 함수들을 다른 파일에서 사용할 수 있도록 선언해 놓은 헤더 파일)
void printHello();
void printName(char *name);
void printIntro(int age, char *region);
void printThx();
func.c 파일의 내용
#include <stdio.h>
#include "func.h"
void printHello()
{
printf("안녕하세요!\n");
printf("만나서 반갑습니다~\n");
}
void printName(char *name)
{
printf("제 이름은 %s입니다.\n", name);
}
void printIntro(int age, char *region)
{
printf("저는 %d살이고, %s 지역에서 살다가 왔습니다.\n", age, region);
}
void printThx()
{
printf("잘 부탁드립니다!!!\n");
}
main.c 파일의 내용
#include "func.h"
int main()
{
char name[] = "홍길동";
int age = 20;
char region[] = "동에 번쩍 서에 번쩍";
printHello();
printName(name);
printIntro(age, region);
printThx();
return 0;
}

전처리 (Preprocessing)
#include, #define, #ifdef 같은 전처리 지시문을 처리한다.
헤더 파일(<stdio.h>)을 포함하며, 매크로 치환을 진행한다.
#include <stdio.h> 에 printf 함수의 선언이 존재하며 이를 바탕으로 컴파일 단계에서 문법 검사를 진행한다..c 파일을 전처리한 결과로 .i 파일이 생성된다.
컴파일 (Compilation)
.i 파일을 어셈블리 코드(.s)로 변환한다.
문법 검사와 중간 코드를 생성하는 것을 포함한다.
어셈블 (Assembly)
.s)를 기계어(.o, 오브젝트 파일)로 번역한다.링크 (Linking)
여러 .o 파일 또는 라이브러리(.a/.so)를 결합한다.
printf의 정의를 glibc(예: /usr/lib/.../libc.so)에서 찾아 연결한다.프로그램의 시작 주소(entry point)를 확인하고 그 위치를 실행 파일에 기록하기 위해, main 함수의 위치를 확인한다.
소스 코드가 고작 3개 정도일 경우, gcc -o program main.c func.c 와 같은 명령어를 통해 간단히 컴파일해도 크게 문제가 없다.
그러나 컴파일은 많은 자원을 소모하는 작업이며, 소스 코드가 수십 개에 달하는 경우, 단일 명령어로 모든 파일을 매번 다시 컴파일하는 것은 매우 비효율적이다. 특히 수정되지 않은 파일까지 매번 다시 컴파일하게 되면 컴파일 시간이 불필요하게 증가하게 된다.
이때 Makefile로, 변경된 파일만을 골라서 컴파일할 수 있어 전체 컴파일 시간을 크게 줄일 수 있다.
Makefile 기본 문법
<target>: <dependencies>
<recipe>
targetmain.o, program)이거나,clean, all)일 수 있다.make <target> 명령을 실행하면 이 target에 해당하는 작업이 수행된다.dependenciestarget을 만들기 위해 필요한 파일 목록(의존성)이다.target에 해당하는 작업이 다시 수행된다.recipegcc, mv, rm 등)Tab문자여야 한다.Makefile 변수
TARGET = program
CXX = gcc
$(TARGET): main.c func.c func.h
$(CXX) -o $(TARGET) main.c func.c
반복적으로 사용되는 값을 하나의 이름으로 정의하여, 유지보수와 가독성을 높일 수 있도록 도와준다.
변수는 변수이름 = 값 형태로 정의하며, 사용할 때는 $(변수이름) 형태로 참조한다.
Makefile 자동 변수
TARGET = program
CXX = gcc
$(TARGET): main.o func.o
$(CXX) -o $@ $^
main.o: main.c func.h
$(CXX) -c main.c
func.o: main.c func.h
$(CXX) -c func.c
$@: targer의 이름$^: 모든 의존성 목록$?: target보다 더 최근에 수정된 의존성 목록실습에 사용할 Makefile 파일 내용
TARGET = program
CXX = gcc
$(TARGET): main.o func.o
$(CXX) -o $@ $^
main.o: main.c func.h
$(CXX) -c main.c
func.o: func.c func.h
$(CXX) -c func.c
clean:
rm -f *.o $(TARGET)
위의 Makefile이 C 언어 프로젝트에서 일반적으로 사용되는 형태로, 소스 파일들을 오브젝트 파일로 컴파일한 후 링크하여 실행 파일을 생성하는 과정을 간단히 정의하였다.
아래의 Makefile은 C언어 컴파일의 각 단계를 명확히 확인할 수 있도록 작성하였다.
TARGET = program
CXX = gcc
all: $(TARGET)
# 1. 전처리 단계 (.c → .i)
main.i: main.c func.h
$(CXX) -E main.c -o $@
func.i: func.c func.h
$(CXX) -E func.c -o $@
# 2. 컴파일 단계 (.i → .s)
main.s: main.i
$(CXX) -S main.i -o $@
func.s: func.i
$(CXX) -S func.i -o $@
# 3. 어셈블 단계 (.s → .o)
main.o: main.s
$(CXX) -c main.s -o $@
func.o: func.s
$(CXX) -c func.s -o $@
# 4. 링크 단계 (.o → 실행 파일)
$(TARGET): main.o func.o
$(CXX) $^ -o $@
clean:
rm -f *.i *.s *.o $(TARGET)
Makefile 실행

make <target>명령어를 사용하면, 해당 target에 정의된 작업을 수행할 수 있으며, make만 입력하면, Makefile 내에서 가장 먼저 정의된 target 이 기본적으로 실행된다.
우클릭하여 다운로드 링크를 복사하고, Ubuntu 터미널을 열어 아래 명령어를 입력한다.

wget [다운로드 링크]
다운로드 받은 Anaconda sh 파일을 sh로 실행한다.
sh [다운로드 받은 파일, 예) Anaconda3-2024.06-1-Linux-x86_64.sh]
conda 명령어 설정을 하기 위해, vi ~/.bashrc로 .bashrc 파일을 열고, 파일의 맨 마지막에 아래 코드를 작성한다.
export PATH=~/anaconda3/bin:~/anaconda3/condabin:$PATH
source ~/.bashrc로 수정한 내용을 적용하고, conda -V로 버전을 확인한다.

[참고] conda 가상환경 만들기
conda create -n [이름] python=[버전]로 원하는 Python 버전의 가상환경을 만든다.
conda update -n base -c defaults conda로 conda를 업데이트하고,
conda activate [이름]로 가상 환경에 접속한다.
conda deactivate로 가상 환경에서 나갈 수 있다.