[TIL]20210626

박창현·2021년 6월 26일
0

TODAY I LEARNED

목록 보기
5/53

C언어

#include <stdio.h>
printf 함수나 다른 함수들을 사용하기 위해서는 stdio.h 라이브러리가 사용된다.
stdio.h는 헤더 파일로 C언어로 작성되어 있으며 파일명이 .h로 끝나는 파일이다.
이 파일에는 printf 함수의 프로토타입이 있어서 Clang 컴파일러가 프로그램을 컴파일할때 printf가 무엇인지 알려주는 역할을 한다.

알아두면 괜찮을 내용인 것 같아 부스트코스의 내용을 그대로 첨언한다.

make나 clang을 사용해서 프로그램을 실행할 때 아래 네 개의 단계를 거칩니다.전처리
컴파일링
어셈블링
링킹
우리가 명령어를 실행할 때 정확히 어떤 일이 일어나는지 알아보도록 하겠습니다.

전처리(Precompile)
컴파일의 전체 과정은 네 단계로 나누어볼 수 있습니다. 그 중 첫 번째 단계는 전처리인데, 전처리기에 의해 수행됩니다. # 으로 시작되는 C 소스 코드는 전처리기에게 실질적인 컴파일이 이루어지기 전에 무언가를 실행하라고 알려줍니다.
예를 들어, #include는 전처리기에게 다른 파일의 내용을 포함시키라고 알려줍니다. 프로그램의 소스 코드에 #include 와 같은 줄을 포함하면, 전처리기는 새로운 파일을 생성하는데 이 파일은 여전히 C 소스 코드 형태이며 stdio.h 파일의 내용이 #include 부분에 포함됩니다.

컴파일(Compile)
전처리기가 전처리한 소스 코드를 생성하고 나면 그 다음 단계는 컴파일입니다. 컴파일러라고 불리는 프로그램은 C 코드를 어셈블리어라는 저수준 프로그래밍 언어로 컴파일합니다.
어셈블리는 C보다 연산의 종류가 훨씬 적지만, 여러 연산들이 함께 사용되면 C에서 할 수 있는 모든 것들을 수행할 수 있습니다. C 코드를 어셈블리 코드로 변환시켜줌으로써 컴파일러는 컴퓨터가 이해할 수 있는 언어와 최대한 가까운 프로그램으로 만들어 줍니다. 컴파일이라는 용어는 소스 코드에서 오브젝트 코드로 변환하는 전체 과정을 통틀어 일컫기도 하지만, 구체적으로 전처리한 소스 코드를 어셈블리 코드로 변환시키는 단계를 말하기도 합니다.

어셈블(Assemble)
소스 코드가 어셈블리 코드로 변환되면, 다음 단계인 어셈블 단계로 어셈블리 코드를 오브젝트 코드로 변환시키는 것입니다. 컴퓨터의 중앙처리장치가 프로그램을 어떻게 수행해야 하는지 알 수 있는 명령어 형태인 연속된 0과 1들로 바꿔주는 작업이죠. 이 변환작업은 어셈블러라는 프로그램이 수행합니다. 소스 코드에서 오브젝트 코드로 컴파일 되어야 할 파일이 딱 한 개라면, 컴파일 작업은 여기서 끝이 납니다. 그러나 그렇지 않은 경우에는 링크라 불리는 단계가 추가됩니다.

링크(Link)
만약 프로그램이 (math.h나 cs50.h와 같은 라이브러리를 포함해) 여러 개의 파일로 이루어져 있어 하나의 오브젝트 파일로 합쳐져야 한다면 링크라는 컴파일의 마지막 단계가 필요합니다. 링커는 여러 개의 다른 오브젝트 코드 파일을 실행 가능한 하나의 오브젝트 코드 파일로 합쳐줍니다. 예를 들어, 컴파일을 하는 동안에 CS50 라이브러리를 링크하면 오브젝트 코드는 GetInt()나 GetString() 같은 함수를 어떻게 실행할 지 알 수 있게 됩니다.

이 네 단계를 거치면 최종적으로 실행 가능한 파일이 완성됩니다.

bool: 불리언, 1바이트
char: 문자, 1바이트
int: 정수, 4바이트
float: 실수, 4바이트
long: (더 큰) 정수, 8바이트
double: (더 큰) 실수, 8바이트
string: 문자열, ?바이트

char과 string을 사용할 때의 주의사항.

char 을 입력할 때는 ' ' 작은 따옴표를 쓰고,
string 을 입력 할때는 " " 큰 따옴표를 사용해야 한다.

char c1='H';
int i1=72;
printf("%c-%i ",c1,c1);
printf("%c-%i ",c1,(int)c1);
printf("%c-%i ",i1,i1);
printf("%c-%i ",(char)i1,i1);

H-72 H-72 H-72 H-72 라는 결과가 나온다. (int)c1(char)i1에서 (int)와 (char)는 형변환 이라는 것이다. char문자열이지만 int취급을 하라는 것 또는 그 반대로 해석하면 된다. 하지만 %i %c을 통해 int와 char값으로 출력하라고 했기때문에 굳이 형변환 하지않더라도 72와 H라는 값이 출력된 것을 볼 수 있다.

배열

int 에서 배열을 쓴다면 int arrays[배열_크기]; 를 통해 만들 수 있다.
여기서 주의할 점은 배열의 크기와 배열의 번호는 같지 않다는 점이다.

int arrays[10]; //10개의 요소(인덱스 0~9)를 가질 수 있는 배열을 만듬.
arrays[0];    // 첫 번째 요소, 인덱스 0
arrays[5];    // 여섯 번째 요소, 인덱스 5
arrays[9];    // 열 번째 요소, 인덱스 9
arrays[10];   // 열한 번째 요소, 인덱스 10 임으로 배열의 크기보다 큼으로 값을 배정할 수 없다.

arrays[10]은 열한 번째요소 임으로 배열의 크기를 벗어났다는 점을 유의하자.
arrays[11]을 이용하면 메모리 한칸을 낭비해서라도 눈에 보이는 시각적인 숫자와 인덱스를 맞출 수 있다. 0번째 점수 라는 표현대신 낭비를 감안하더라도 1번째 점수 로 출력할 수 있다. (물론, i+1등을 이용해 시각적인 눈속임을 줄 수 있지만, 매번 +1을 더하는 것보다 메모리 한칸을 낭비하는 것이 더 나은 방법이라 생각한다.)

여기서 좋은 코딩습관을 하나 배울 수있는데,

const int N = 3;

int main(void)
{
    // 점수 배열 선언 및 값 저장
    int scores[N];
    scores[0] = 72;
    scores[1] = 73;
    scores[2] = 33;

    // 평균 점수 출력
    printf("Average: %i\n", (scores[0] + scores[1] + scores[2]) / N);
}

이 코드에서 N대신 각각에 3을 대입해놓은 상태였다면 나중에 코드를 볼 때 각각의 3들이 같은 값을 가져야 한다는 사실을 깨닫기 쉽지 않다는 것을 명심해야한다.
그렇기에 하나의 변수를 지정해주고 변수를 대입함으로서 문제를 해결할 수 있다.
그리고 이때 const int는 전역 변수를 설정하는 것이다.

전역 변수
만약 N이 고정된 값(상수)이라면 그 값을 선언할 때 함수 밖에서 const를 앞에 붙여서 전역 변수, 즉 코드 전반에 거쳐 바뀌지 않는 값임을 지정해줄 수 있다.
관례적으로 이런 전역 변수의 이름은 대문자로 표기 한다.

하면서 알게된 사소한 것.

 {
    // 사용자로부터 점수의 갯수 입력
    int n = get_int("Scores:  ");

    // 점수 배열 선언 및 사용자로부터 값 입력
    int scores[n];
    for (int i = 0; i < n; i++)
    {
        scores[i] = get_int("Score %i: ", i + 1);
    }

    // 평균 출력
    printf("Average: %.1f\n", average(n, scores));
}

//평균을 계산하는 함수
float average(int length, int array[])
{
    int sum = 0;
    for (int i = 0; i < length; i++)
    {
        sum += array[i];
    }
    return (float) sum / (float) length;
}

이렇게 float 함수에 lenghth array[] 2개의 인자를 입력받고 이를 이용해 내부에서 계산 후 return을 통해 값을 돌려줄 수 있다.
array[]에서 [ ]을 통해 입력받는 것이 배열임을 알려줘야한다.

또한, C언어의 배열은 스스로의 길이를 기억하지 않는다.

(파이썬이나 자바와 다르게) 그러므로 함수에 배열의 길이 또한 넣어줘야한다.
--> 이는 아직 무슨 소리인지 잘 이해를 못하겠는데 이해하면 수정하기.!
지금 느낀바로는 파이썬에서 배열을 for i in ~을 통해 넣으면 배열이 끝난 후 알아서 for문을 벗어났는데 c는 스스로의 배열의 길이를 알지 못해 끝나지 않는다? 이런 거같음.

\0 [널문자]

string은 문자를 적는 만큼 크기를 가지기 때문에 정해진 크기가 없다. 그러므로 끝을 알려주는 문자가 필요한데 그것이 바로 \n [널문자] 이다. 이는 8비트 모두 0인 상태이다. char 한 개당 1바이트(8비트)가 필요했기에 이 널문자도 8비트를 가지고 모두 0인 상태이다. c에서는 string(문자열)을 표시하면 항상 마지막에 널문자가 붙는다. 즉 HI!를 표시하기 위해서 3바이트가 아닌 4바이트가 필요하다!

문자열의 끝을 파악하기.

첫번째 방법은 해당하는 인덱스의 문자가 널 종단 문자, 즉 ‘\0’와 일치하는지 검사하는 것이다.

즉, s라는 문자열이 있다고 할 때 for (int i = 0; s[i] != ‘\0’; i++) {...} 과 같은 루프를 사용한다.

여기서 \0은 문자열이 아니고 문자로 취급하기때문에 ' '을 이용한다.

두번째 방법은 #include<string.h>strlen() 을 이용하는 것이다. strlen()은 문자열의 길이를 반환해 준다.

이때 for을 쓴다면 알아둬야할 점이 strlen을 조건식안에 넣는다면 strlen의 길이는 변하지 않지만 길이가 몇이냐고 계속 물어봄으로 리소스를 낭비하는 경우가 발생한다. 이를 해결하고자 n=strlen(~) 으로 메모리에 int값을 미리 적어놓고 n을 이용해 조건식을 완성하면 낭비를 줄일 수 있다.(하지만 이는 리소스 대신 메모리를 낭비한다. 이에 대한 논쟁은 다음번에.)

명령행 인자

#include <cs50.h>
#include <stdio.h>

int main(int argc, string argv[])
{
    if (argc == 2)
    {
        printf("hello, %s\n", argv[1]);
    }
    else
    {
        printf("hello, world\n");
    }
}

여기서 main 안에 void 대신 argc와 argv[]가 들어갔다. main 함수가 실행될 때 값을 받아오게 된다. 컴퓨터개론에서 리눅스를 배울 때 argc와 argv를 배워놨기에 이해가 빨랐다.
콘솔에 "./argc”로 실행해보면 “hello, world”라는 값이 출력된다.
명령행 인자에 주어진 값이 프로그램 이름(/argc)밖에 없기 때문이다.
하지만 “./argc David”로 실행해보면 “hello, David”라는 값이 출력된다.
명령행 인자에 David라는 값이 추가로 입력되었고, 따라서 argc 는 2, argv[1] 은 “David”가 된다. 이 명령행 인자를 통해 콘솔로 실행할 때 여러가지를 입력시킬 수 있다.

profile
개강했기에 가끔씩 업로드.

0개의 댓글