CS50 - 프로그래밍 응용
[프로그래밍 응용]
1. 컴파일
[컴파일 과정(Compile)]
전처리(Precompile) : 컴파일의 네 단계 중 첫 번째 단계는 전처리인데, 전처리기에 의해 수행됩니다. # 으로 시작되는 C 소스 코드는 전처리기에게 실질적인 컴파일이 이루어지기 전에 무언가를 실행하라고 알려줍니다. 예를 들어, #include는 전처리기에게 다른 파일의 내용을 포함시키라고 알려줍니다. 프로그램의 소스 코드에 #include 와 같은 줄을 포함하면, 전처리기는 새로운 파일을 생성하는데 이 파일은 여전히 C 소스 코드 형태이며 stdio.h 파일의 내용이 #include 부분에 포함됩니다.
컴파일(Compile) : 전처리기가 전처리한 소스 코드를 생성하고 나면 그 다음 단계는 컴파일입니다. 컴파일러라고 불리는 프로그램은 C 코드를 어셈블리어라는 저수준 프로그래밍 언어로 컴파일합니다.소스 코드(프로그래밍 언어로 작성한 코드)를 오브젝트 코드(0과 1)로 변환시키는 과정입니다. 여기서 소스 코드는 여러분이 C언어와 같은 프로그래밍 언어로 작성한 코드이고, 오브젝트 코드는 기계 코드라고도 알려져 있는데, 0과 1로 이루어져 있으며 컴퓨터에게 프로그램이 어떻게 실행되어야 하는지 알려주는 코드입니다. make 명령어 자체는 컴파일러가 아니고, clang이라는 컴파일러를 호출해서 C 소스 코드를 오브젝트 코드로 컴파일 하도록 합니다.어셈블리는 C보다 연산의 종류가 훨씬 적지만, 여러 연산들이 함께 사용되면 C에서 할 수 있는 모든 것들을 수행할 수 있습니다. C 코드를 어셈블리 코드로 변환시켜줌으로써 컴파일러는 컴퓨터가 이해할 수 있는 언어와 최대한 가까운 프로그램으로 만들어 줍니다. 컴파일이라는 용어는 소스 코드에서 오브젝트 코드로 변환하는 전체 과정을 통틀어 일컫기도 하지만, 구체적으로 전처리한 소스 코드를 어셈블리 코드로 변환시키는 단계를 말하기도 합니다.
어셈블(Assemble) : 소스 코드가 어셈블리 코드로 변환되면, 다음 단계인 어셈블 단계로 어셈블리 코드를 오브젝트 코드로 변환시키는 것입니다. 컴퓨터의 중앙처리장치가 프로그램을 어떻게 수행해야 하는지 알 수 있는 명령어 형태인 연속된 0과 1들로 바꿔주는 작업이죠. 이 변환작업은 어셈블러라는 프로그램이 수행합니다. 소스 코드에서 오브젝트 코드로 컴파일 되어야 할 파일이 딱 한 개라면, 컴파일 작업은 여기서 끝이 납니다. 그러나 그렇지 않은 경우에는 링크라 불리는 단계가 추가됩니다.
링크(Link) : 만약 프로그램이 (math.h나 cs50.h와 같은 라이브러리를 포함해) 여러 개의 파일로 이루어져 있어 하나의 오브젝트 파일로 합쳐져야 한다면 링크라는 컴파일의 마지막 단계가 필요합니다. 링커는 여러 개의 다른 오브젝트 코드 파일을 실행 가능한 하나의 오브젝트 코드 파일로 합쳐줍니다. 예를 들어, 컴파일을 하는 동안에 CS50 라이브러리를 링크하면 오브젝트 코드는 GetInt()나 GetString() 같은 함수를 어떻게 실행할 지 알 수 있게 됩니다.
2. 버그와 디버깅
버그 : 코드에 들어있는 오류.
디버깅(debugging) : 코드에 있는 버그를 식별하고 고치는 과정입니다. 프로그래머는 디버거라고 불리는 프로그램을 사용하여 디버깅을 하게 됩니다. 프로그램은 일반적으로 인간보다 훨씬 빠르게 연산을 수행합니다. 그래서 프로그램을 실행시켜보는 것만으로는 무엇이 잘못됐는지 찾아내기 어렵습니다. 디버거는 프로그램을 특정 행에서 멈출 수 있게 해주기 때문에 버그를 찾는데 도움이 됩니다. 프로그래머는 멈춰진 그 지점에서 무슨 일이 일어나는지 볼 수 있습니다. 프로그램이 멈추는 특정 지점을 중지점이라고 합니다. 또한 프로그래머가 프로그램을 한번에 한 행씩 실행할 수 있게 해줍니다. 이로써 프로그래머는 프로그램이 내리는 모든 결정들을 단계별로 따라갈 수 있게 됩니다.
3. 형 변환
형변환(Typecasting) :자료형에서 다른 자료형으로 변환. 형변환할 때 정밀도(표현 범위)가 더 높은 자료형으로 바꿀 경우 값에 오차가 발생할 수 있다는 점 유의.
명시적 형변환 : 이미 존재하는 변수 앞에 새로운 자료형을 넣어 다른 자료형으로 바꿔주는 것
암묵적 형변환 : ASCII 표준에 해당하는 정수를 %c에 전달하면 컴파일러는 그 값을 자동으로 문자로 해석
4.함수와 리턴
함수 : 특정 목적을 위해 만들어진 재사용 가능한 코드입니다. 함수는 입력값과 출력값을 가지며 프로그램 내의 어디에서든 재사용될 수 있습니다. 프로그램을 여러 개의 함수로 만들면 코드를 조직화하고 간단하게 만들 수 있습니다. 이것이 추상화의 한 예입니다. 함수를 한번 작성하면 그 함수가 구체적으로 어떻게 만들어졌는지 몰라도 함수를 사용할 수 있습니다. 그리고 함수를 만들지 않은 사람도 저수준의 상세 내용을 신경 쓰지 않고 함수를 사용(호출)할 수 있습니다.
함수의 첫 행은 세 부분으로 나누어져 있습니다. 첫째는 반환 자료형인데, 함수를 호출한 쪽에 출력값으로 반환해줄 값의 자료형입니다. 반환할 값이 없을 경우 반환할 값이 비어있다는 의미로 void를 씁니다. 둘 째는 함수의 이름입니다. 공백이 있으면 안되고, C에서 이미 사용하는 키워드는 쓸 수 없습니다. 셋 째는, 괄호 안에 인자라고도 알려진 함수의 매개변수(parameter)를 넣어줍니다. 이것은 함수의 입력값이며 입력값이 없으면 void를 씁니다. 이 첫 행 뒤에는 중괄호로 묶인 함수 코드가 작성됩니다.
반환자료 함수명(파라미터)
void sayHi(void){}
int main(void){}
int square(int x){}
반환(return) : return이 쓰여진 행이 보통 함수의 마지막 행입니다
범위(Scope) : 함수 안에서 변수를 정의하거나 함수의 파라미터로 전달된 것들은 그 함수 안(지역{중괄호 사이} )에서만 유효합니다. 이러한 변수들을 지역변수라고 부릅니다. 지역변수는 그 함수 안에서만 사용될 수 있고, 다른 곳에서는 존재하지 않습니다. <코드 2>의 main 함수에서 x 변수를 참조하려고 한다면, 컴파일러는 오류를 만들어낼 것입니다. x라는 변수는 main함수의 범위(Scope) 안에 존재하지 않고, 오직 square 함수의 범위 안에서만 존재하기 때문입니다. 같은 논리로, main 함수에 정의된 어떤 변수도 square 함수 내에서 접근할 수 없습니다.
5. 배열과 문자열
배열 : 같은 자료형으로 된 여러 개의 변수를 연속적으로 저장. 변수와 마찬가지로, 배열을 선언할 때는 첫째로 배열에 저장되는 자료의 유형을 명시합니다. 그리고 배열의 이름을 지정합니다. 이름 뒤에 오는 대괄호[] 안에는 배열의 크기가 들어갑니다. 배열의 크기는 배열에 얼마나 많은 값을 넣어둘 수 있는지를 정의.
인덱스 : 배열을 일련의 연속된 칸으로 생각할 수 있는데, 각 칸에는 값이 있고, 숫자 인덱스가 있습니다. 숫자 인덱스는 배열 안에 들어있는 특정 값에 접근하기 위해 쓰이며, 몇 번째 값인지를 가리킵니다. C에서 배열의 인덱스는 0으로 시작. 배열의 각 값은 인덱스 숫자로 참조하기 때문에 배열을 반복문으로 돌리기 쉽습니다.
문자배열(문자열): 문자 값을 연속적으로 저장. 문자열은 char 값들의 배열을 나타냅니다. C에서 문자열의 마지막 인덱스는 널로 끝나며, ‘\0’으로 나타냅니다. 널 종단(null-terminator)은 문자열에게 문자열이 끝났고 더 이상의 문자가 남아있지 않다고 말하는 문자입니다. 문자열은 하나의 배열이기 때문에, 다른 배열에서 인덱스를 이용해 각 원소에 접근하는 것과 마찬가지로 문자열의 인덱스를 통해 특정 문자 값에 접근할 수 있습니다. 배열을 사용하므로 문자열에 반복문을 돌려 각 문자에 연산을 하기가 쉽습니다. 보통 반복문의 카운터는 0으로 초기화하여 시작되고, 문자열의 마지막 인덱스까지 반복합니다.
6.커맨드 라인
커맨드라인(프로그램을 명령줄) : 실행시킬 때, 보통 ./프로그램_이름 같은 명령어로 실행했을 것입니다. C에서는 프로그램의 명령행 인자(command-line arguments)들을 명시할 수 있고, 명령줄에 인자들을 명시하여 사용자가 프로그램의 main 함수에 인자들을 전해줄 수 있게 합니다. 이 기능은 GetString처럼 프로그램이 실행 중일 때 입력값을 전달받는 방법 대신 사용할 수 있습니다.
argc, argv : make, cd, clang, mkdir와 같은 명령어 프로그램들은 모두 명령행 인자들을 받습니다. C에서 명령행 인자들은 main 함수에 입력값으로 전달됩니다. 하지만 우리가 이전에 작성했던 main 함수에서는 void를 써주었기 때문에 아무런 인자도 받지 않았습니다.
명령행 인자를 받기 위해서는, main 함수가 정수형인 argc와 문자열 배열인 argv라는 두 인자를 받아야 합니다. argc는 argument count의 약자로, 명령줄에 전달되는 인자의 수를 나타냅니다. 공백으로 구분되는 각 단어는 하나의 인자로 인식되고, ./hello와 같이 프로그램을 호출하는 것도 하나의 인자로 인식됩니다. argv는 argument vector의 약자로, 인자들 그 자체를 나타내는 배열입니다. 배열의 각 값은 문자열입니다.
7.종료 코드
종료 코드 : main 함수 안에서 아무 것도 반환하지 않으면 기본적으로 컴파일러는 자동으로 main 함수가 0을 반환한다고 추정합니다. main 함수가 반환하는 값은 종료 코드라고 불립니다. 프로그램이 길어지고 더 복잡해질수록, 종료 코드가 더 값지게 쓰일 수 있습니다.
종료코드 사용하기 : 관례적으로, 프로그램이 문제없이 성공적으로 끝나면 종료 코드 0을 반환해야 합니다. 그래서 main 함수 끝에 반환이 명시되지 않으면 컴파일러는 프로그램이 0을 돌려주었다고 추정합니다. 0이 아닌 종료 코드(보통 1이나 -1)는 일반적으로 프로그램이 실행되는 동안 프로그램이 성공적으로 끝나지 못하게 하는 어떤 오류가 있었다는 것을 의미합니다.
입력 유효성(input validation) : 사용자가 제공한 입력값이 유효한지 프로그램이 확인하는 것입니다. 예를 들어 프로그램이 두 개의 명령행 인자를 받는 것으로 알고 있는데, 한 개만 받았다면, 0이 아닌 종료 코드를 반환해 오류를 알립니다.
종료 코드 확인하기 : 보통 명령줄로 프로그램을 실행시키면, main 함수가 반환하는 반환 값을 볼 수 없습니다. 그러나 프로그래머들이 코드에서 어떤 문제가 있는지 찾을 수 있도록 도와주는 프로그램인 디버깅 도구를 사용하면 main 함수가 어떤 종료 코드로 종료됐는지 볼 수 있습니다. 종료 코드를 확인하는 것은 프로그램이 왜 실패했는지 알아낼 수 있는 좋은 방법입니다. 각 오류에 따라 다른 종료 코드가 반환되기 때문에 입력 유효성을 많이 확인해야 하는 프로그램을 작성할 때 큰 도움이 됩니다.
8. 라이브러리
라이브러리 : 프로그래머들이 이미 만들어진 코드를 다시 개발하지 않아도 되게 하고, 서로 함께 작업할 수 있도록 만들어주는 함수의 모음입니다. 라이브러리는 문자를 대문자에서 소문자로 변환하거나, 문자열을 정수로 변환하는 것 등 C에서 자주 쓰이는 연산들의 함수를 포함합니다. 여러분의 프로그램에서 비슷한 기능을 수행해야 한다면, 이미 만들어져 있는 함수를 또 만드는 대신 이미 존재하는 라이브러리를 그대로 사용할 수 있습니다.
라이브러리 사용하기 : 라이브러리를 불러오기 위해서는 #include를 사용해 해당 라이브러리의 헤더 파일을 포함해주면 됩니다. 라이브러리를 불러오면 라이브러리 함수들을 사용할 수 있습니다.
9. 구조체와 캡슐화
구조체와 캡슐화 : 프로그래밍을 할 때, 일반적인 자료형들을 사용하기에 적합하지 않을 수 있습니다. 이런 상황에서 우리는 데이터를 캡슐화하여 어떤 개체에 연관되는 정보들을 한 덩어리로 묶을 수 있게 됩니다. 예를 들어 학생은 이름(string형), 나이(int형), 평점(float형)와 같은 정보들을 갖고 있는데, 이 정보들은 단독으로는 큰 의미를 갖고 있지 않습니다. 이 정보들이 모여 학생이라는 개체를 이룰 때 의미를 갖게 되고 C에서는 구조체(structure)라는 방법을 사용합니다.
배열과 구조체 : 데이터를 묶어 효율적으로 활용하기 위하여 배열을 사용하였습니다. 배열의 장점은 각 인덱스를 알 때, 모든 학생들에게 임의 접근할 수 있으며, 각 배열 원소를 순환하면서 확인할 수 있다는 것입니다. 하지만 배열은 같은 데이터형의 변수들을 하나로 묶을 수 있지만, 서로 다른 데이터형의 변수를 묶어서 사용할 수 없습니다. 그리고 사용 전에 배열의 크기를 선언해야만 합니다.
구조체 : 데이터를 묶어주는 또 다른 방법은 구조체입니다. 구조체를 사용함으로써 서로 다른 자료형의 변수를 하나로 묶어 새로운 자료형을 만들 수 있습니다. 구조체에는 연관된 정보들이 string형, int형, float형으로 구성되어 있습니다. 이러한 정보들을 각각 멤버라고 부릅니다. 구조체 자료형의 특정 멤버에 접근하고 싶다면 구조체명.멤버명 (student.name)으로 하면 됩니다. 데이터를 구조체로 저장할 때의 장점은 서로 다른 자료형의 데이터들을 하나로 묶을 수 있다는 것입니다. 구조체를 사용했을 때의 또 다른 장점은 더 이상 학생이 몇 명이 있는지 선언할 필요가 없다는 것입니다. 구조체에서는 학생 수를 정의하지 않고 원하는 수만큼 만들 수 있습니다. 하지만 배열이 인덱스를 사용하여 각 멤버들을 순환하는 것과 달리 구조체는 멤버를 순환할 수는 없습니다.
10. 재귀
재귀(Recursion) : 함수가 본인 스스로를 호출해서 사용
시그마 : 주어진 수 n 부터 1까지의 연속된 수를 모두 합한 값.
스택 : 재귀 함수에서 동일한 함수를 계속해서 호출될 때마다 함수를 위한 메모리가 계속해서 할당됩니다. 함수가 호출될 때 마다 사용되는 메모리를 스택이라고 부릅니다. 컴퓨터가 일을 처리하는데 관리를 하는 역할인 운영체제는 함수를 실행할 수 있도록 일정량의 바이트를 주고, 그 공간에 함수의 변수나 다른 것들을 저장할 수 있도록 합니다. 그래서 재귀함수를 이용하다 보면 함수가 종료되지 않고, 함수가 계속해서 호출되는 경우가 발생하기도 합니다. 이 경우 스택 공간은 초과되고 프로그램 충돌이 발생됩니다. 그렇기 때문에 재귀를 사용할 때는 과도하게 스택 메모리가 사용되지 않도록 주의해야 합니다. 메모리 사용 문제때문에 재귀는 매우 유의 해야 하지만, 특정 자료구조를 다룰 때 매우 유용하게 사용됩니다.
11. 파일 입출력
프로그램은 한 번 실행하고 나면 그 안에 입력했던 데이터들이 모두 사라집니다. 프로그램이 변수를 만들고 그 안에 데이터를 넣어두거나 할 때 사용하는 메모리는 영구적으로 데이터를 저장할 수 있는 작업공간이 아니기 때문입니다. 데이터를 영구적으로 저장해두고 싶다면 프로그램이 실행될 때 파일에 저장해두면 됩니다. 이 파일에 저장된 데이터는 다음 번에 프로그램이 실행될 때 다시 읽어올 수 있습니다. C에는 파일 입출력(File IO) 기능이 있어서 파일에 데이터를 저장하거나 읽어오도록 만들 수 있습니다.
- 파일 열기 : C의 표준 입출력 라이브러리인 stdio.h에서 파일을 읽고 쓰는 기능을 제공합니다. 먼저 파일에 접근해야 파일의 데이터를 읽어오거나 쓸 수 있습니다. 파일은 FILE이라고 하는 구조체로 구현되어 있습니다. FILE* file; 코드를 이용해 변수를 선언할 수 있습니다. fopen 함수를 호출할 때 전달하는 첫 번째 인자는 파일 이름이고 두 번째 인자는 모드입니다. 즉, ‘r’은 읽기 모드, ‘w’는 쓰기 모드, ‘a’는 추가 모드입니다.
FILE* file; file = fopen(“file.txt”, “r”);
파일 열고 쓰기 : 파일을 열고 나면 파일의 내용을 읽거나 쓸 수 있습니다. 파일의 내용 중에서 한 글자만 읽어오고 싶다면 fgetc 함수를 사용합니다. 만약 한 줄을 읽어오고 싶다면 fgets 함수를 사용할 수 있고 정해진 길이만큼의 데이터를 읽어오고 싶다면 fread 함수를 사용할 수 있습니다. 형식문자열을 사용하여 데이터를 읽어오기 위해서는 fscanf를 이용합니다. 파일에 데이터를 한 글자만 쓰고 싶다면 fputc 함수를 사용하고 한 줄을 쓰고 싶다면 fputs 함수를 사용할 수 있습니다. fprintf 함수를 사용하면 printf 함수처럼 형식문자열을 이용해 데이터를 쓸 수 있습니다. fwrite 함수는 배열에 들어있는 데이터를 파일에 쓸 수 있도록 합니다.
파일 닫기 : 파일을 한 번 열면 그 파일은 반드시 닫아야 합니다. 왜냐하면 파일을 계속 열기만하면 파일을 열 수 있는 시스템 리소스가 소진되어 더 이상 파일을 열 수 없게 될 수도 있기 때문입니다. UNIX 시스템에서는 프로그램이 종료될 때 열려있는 파일이 있으면 자동으로 닫아주기도 합니다. 하지만 파일을 사용하고 나면 항상 파일을 닫는습관을들이는것이좋습니다.
fclose(file);