[C] C lang 기본문법

#include, stdio.h

  • C#에서 using 지시문(directive)과 비슷한 일을 한다.
  • 다른 파일에 구현한 함수나 변수를 사용할 수 있게 해줌.(다만, C#처럼 알아서 함수나 변수를 찾아주지는 않음.)
  • #include는 전처리기 지시문 중 하나.
  • 전처리기
    • 컴파일을 하기 전 텍스트를 복붙해주는 역할을 함.

#include<>

  • #include에서 <> 안의 단어는 실제로 디스크상에 존재하는 파일 이름이다.

20221006_105658

c의 헤더가드 include는 헤더 파일을 열어서 (*h) 그 내용을 복사한 뒤, - #include 라는 부분을 없애고 복사한 내용으로 바꾸는 것이다.

  • include /* 컴파일 */
  • include ‘stdio.h’ /* 컴파일 오류 */
  • include “stdio.h” /* 컴파일 */

-> include “stdio.h” 는 사용이 되나 권장하지 않는다.(사용 하지 말것)

  • include 는 C 표준라이브러리 중 일부이다. (C standard Library)
C 표준 라이브러리란.

다음에 필요한 매크로 , 자료형 (data type) , 함수 등을 모아 둔 것.

  • 문자열 처리
  • 수학 계산
  • 입출력 처리
  • 메모리 관리
stdio.h의 역할
  • libc에서 표준 입출력 (Standard Input and Output)을 담당.
  • 스트림 입출력에 관한 함수들을 포함.
  • C#의 네임스페이스와 비슷한 역할을 지닌 라이브러리.
  • 안에 있는 함수의 예로 printf(), scanf(), fopen(), fclose() 등이 있다.

main(void) 함수

int main(void)
{
  return 0;
}
  • 프로그램의 진입점 (entry point)
  • C 코드를 빌드해서 나온 실행파일 (.exe 또는 .out) 을 실행하면 main 함수가 자동적으로 실행 됨.

main() 함수의 이름과 반환형

C#

static void Main(string[] args)
{

}
C#에서 봤던 Main()함수와 같은 역할
  • 하지만 C는 대문자 ‘M’ 대신 소문자 ‘m’ 사용
  • 약속된 함수명이라 바꾸는 건 불가능 함.
반드시 int를 반환함.
  • 0: 프로그램이 문제가 없었다는 뜻.(정상종료 판단) 근데 이걸 어겨도 누가 때리거나 그러지는 않는다.
#include <stdio.h>

int main(void)
{
  printf("hello");
  return 0;
}

clang -std=c89 -W -Wall -pedantic-errors main.c

를 clang 설치 후 컴파일 하면 윈도우면 a.exe, 유닉스 계열은 a.out 생성.

main(void) 함수 : (void)

int main(void){
  return 0;
}

다른 언어와 달리 void를 생략한다고 매개변수가 없다는 뜻이 아님 C에는 함수 선언과 함수 정의가 있음.

  • 함수 선언에서 void를 생략하면 매개 변수를 받는다는 의미
    • 단, 아직 매개변수의 개수와 자료형을 모를 뿐.
  • 함수정의에서 void를 생략하면 매개변수가 없다는 뜻.

따라서 언제나 void를 넣는 습관을 기르자.

예시

int sum(void); /* 함수 선언: 매개 변수가 없음 / int sum(); / 함수 선언: 매개 변수가 있는데, 아직 뭔지 모름. */

int sum(void) /함수정의: 매개변수가 없음 */ { /코드 생략 */ }

int sum() /* 함수 정의 : 매개변수가 없음 / { /코드 생략 */ }

int sum(const int num1, const int num2 ) /* 함수 정의: 매개변수가 2개 있음 / { /코드 생략 */ }

커맨드라인 인자 받아오는 법

JAVA나 C#은 커맨드라인 인자를 main에서 받아온다.(main함수에 바로 넣어버림) public static void main(String args[]) { //args[4] : “a” , “b” , “c” , “d” }

main() 함수와 커맨드라인 인자
  • C에도 같은 코드가 있다.
int main(int argc, char * argv[])
{
  /* 코드 생략 */
  return 0;
}


혹은

int main(int argc, char ** argv)
{
  return 0;
}

위는 배열형태로 받고 아래는 문자열 형태로 받는다 보면 된다. argc는 인자, argv는 받는 형태(내용), **argv는 포인터 받는다 보면 된다.


printf()

printf(“hello”);

  • 화면에 데이터 출력하는 함수.
  • printf()는 ‘print formatted’(서식에 맞게 출력하다.) 그냥 이쁘게 출력한다 보면 됨.

printf() 함수로 문자열 출력하기

  • C#이나 다른 언어들은 ‘+’나 string.format() 혹은 문자열 보간 ($”{}”) 을 이용.
  • C는 그런거 없다..

    • ‘%s’ 라는 서식문자가 (format specifier) 문자열이 들어갈 위치를 알려줌.
const char* name = "Blood Mary";
printf("hello, %s\n", name);

printf() 함수로 정수 출력하기

  • C#이나 다른 언어들은 ‘+’나 string.format() 혹은 문자열 보간 ($”{}”) 을 이용.
  • C는 그런거 없다..

    • ‘%d’ 라는 서식문자가 (format specifier) 정수가 들어갈 위치를 알려줌.
int num1 = 90;
int num2 = 80;

printf("%d %d", num1, num2);

각 ‘%s’, ‘%d’ 가 데이터를 서식하게 됨.


주석(comments)

/* entry point for the program */

  • /* */ 만 지원
  • 주석이 여러줄이든 한줄이든 다 /* */
    • vsc나 다른 ide에서 //를 치면 주석처리 되는 경우가 많은데 vsc는 c++과 c 혼용해서 적혀서 그런점도 있으며 ide도 그랜식으로 되어있는게 많다.
  • 다른 언어는 하나는 //, 여러줄이면 /* */가 일반적이다.
  • C는 그딴거 없다. 🤔
#include <stdio.h>

int main(void)
{
  //Lets try it
  printf("hello");
  return 0;
}

20221006_141231

주석 부분에서 에러가 난다.



test)

void print_number(void); /* 함수 선언: 매개변수 목록에 void를 사용 */
void print_number();     /* 함수 선언: 매개변수 목록에 void를 생략 */
  • C에서 함수를 선언할 때 매개 변수 목록에 void를 사용하는 것은 무엇을 의미하나?
  1. 이 함수는 매개변수를 받지만, 매개변수의 개수가 한정된다.
  2. 이 함수는 매개변수가 없다.
  3. 이 함수는 매개변수를 받는다. 단지, 아직 자료형과 개수를 모를 뿐 이다.
  4. 이 함수는 void 형 매개변수를 받는다.
  • C89 표준에서 허용하는 주석 형태는?

C언어만의 기본 문법. C는 절차적 언어

  • 절차적 언어와 개체지향언어(OOP) 차이는 다들 안다.
  • C#, JAVA같은 언어들은 OOP언어지만 절차적으로도 어느정도 사용이 가능하다.
  • 하지만 C는 순수하게 절차적 언어로만으로 사용이 가능하다.


  • 즉, C로 작성한 코드는 데이터보다 프로세스에 맞춰져 있다.
  • 이게 반드시 나쁜 것은 아니다.
    • OOP에서도 종종 절차적 언어 스타일로 코드를 작성한다.
  • 또한 절차적 언어는 이해하기 쉽다.
    • 매뉴얼에 적힌 대로 따라하는 느낌.

절차적 언어는 데이터 보다 프로세스를 중시 하기 떄문에 클래스, 절차적 OOP 이런 개념들도 없다.(문법 자체는 간단해 보일수도 라고 느낄수 있다.)

  • 클래스(Class): 그런거 없다.

  • 함수
    • 모두 전역(global) 변수
    • 기본적으로 어디서나 호출 가능
  • 변수
    • 함수 밖에 선언되어 있으면 전역변수
    • 함수 안이면 지역변수



자료형 (data type)

  • char, short, int, long, float, double, long double 이런 건 다 안다는 가정하에

unsigned 와 signed

  • C#에서는 부호없는(unsigned) 자료형 앞에 접두사 u를 붙임.
    • 단. byte는 기본이 ‘부호 없음’
    • 부호 있는 byte를 쓰려면 접두사 s를 붙였음.
  • C는 unsigned라는 단어를 자료형 이름 앞에 넣어줘야 함.
    • 예: unsigned char, unsigned int
    • ‘부호 있음’을 명확히 보여주기 위해 signed를 붙일 수도 있음.
      • 예) signed int, signed char
    • unsigned/signed 를 생략하면 부호 있음(char)이 기본이다.
      • 단, char은 예외다.

위 둘(char, byte)가 다른 이유는 둘 다 어떤 데이터를 표현하는 가장 작은 단위이다.(컴퓨터에 접근 가능한 가장 작은 단위) 데이터를 읽어오는 가장 작은 단어가 byte인데, 바이트이기 때문에 바이트를 순수한 1바이트 이진 숫자로 생각하면 signed으로 보기 어려운 점도 있고 아스키 코드와도 관련이 없다고는 할 수 없다.


Char형

char a = 'a';
char b = 'a+1' /* 98,b */
char c = 99;  /* 99, c */

최소 8 비트인 정수형

최소 8비트? -> ‘표준은 8비트 이상’ 이라고만 정의한다. 그럼 몇십만 비트 적용해도 표준에 맞나? -> ㅇㅇ 표준에 맞다.

물론 그런 컴파일러는 없긴 하다. 표준은 기본적으로 비트로 얘기한다.

char가 몇 비트인지 찾는 법

  • 헤더를 include 한 뒤, CHAR_BIT을 보면 몇 비트인지 알 수 있음.(define 된 정수 볼 수 있다.)
#include <limit.h>
int main(void)
{
    char char_size = CHAR_BIT;
    return 0;
}

char가 몇 비트인지 찾는 이유 = 컴파일러에 따라 char의 비트가 다를 수 있다. 어떤건 16비트, 어떤건 9비트 어떤건 90비트. 이런 식일 수 있기 때문.

윈도우 , vsc 등 char은 기본적으로 8비트

원래 C표준은 기본 자료형들의 정확한 바이트 수를 강요하지 않는다.

  • 컴파일러마다 알아서 함.

더 나아가 1바이트를 CHAR_BIT 만큼이라고 한다.

왜냐하면? 소형기기에 따라 특정 크기를 적용하는 게 어려울 수 있기 때문이다.



헷갈리는 점

1바이트는 비트 8개가 모인 것. 근데 그건 일반 컴퓨터에서 얘기하는 점이고

C기준에서는 1바이트가 8비트가 아닐 수 있다.

C 표준은 CHAR_BIT 이 8 비트이면 그게 1바이트이다. 이게 16비트면 1바이트이다.

??? 🤔 ???

char는 내가 돌리는 기계에서 표현이 가능한 가장 작은 단위의 메모리. 내가 접근하고 데이터를 저장하는 단위의 작은 메모리.

그게 어떤 기기냐에 따라서 1 바이트일 수도 있고, 어떤 기기는 2 바이트에 저장을 할 수도 있다.

여기서 말하는 2바이트는 일반적으로 얘기하는 8비트. 8비트로 저장할 수도 있고 16비트로 저장할 수도 있다.

이게 char라는 자료형이다. 그럼 C언어에서의 바이트라고 하는 개념은 char의 크기이다.

-> 컴퓨터의 가장 적게 저장할 수 있는 단위. 지금 사용하고 있는 기기에, 그 단위가 2바이트라면 실제 16비트라면 그 16비트를 1바이트로 한다.


즉, char 사이즈 1바이트. 그게 C표준이 말하는 것. 위에서 계속 8비트, 8비트 적었는데. 비트는 바뀌지 않기 때문. 그래서 비트로 얘기했다.

결국 소형기기에 따라, 구조를 어떻게 잡았냐에 따라, 기계가 어떻게 되어있는지에 따라 기본적으로 저장소는 단위가 다를 수 있다.

하드웨어 설계자의 마음대로이기 때문에, C는 과거에 많은 하드웨어에 쓰였던 것. 그 하드웨어가 지금도 많이 쓰이기 떄문에

8비트 = 1바이트로 굳어졌다.

생각해야 될 건 그 바이트는 char의 크기이다.



char과 signed/unsigned

  • 정수형이니 signed(부호 있음)와 unsigned(부호 없음)가 있음.
  • signed/unsigned 를 생략하면?
  • signed이라고 보통 생각 (왜? 보통 정수형은 다 그러기 때문)
  • 그럼 unsigned 인가? -> ㄴㄴ
  • C 표준은 그런걸 정하지 않았다.
  • 컴파일러 구현 따라 정해짐.
    • 어디서는 signed, 어디서는 unsigned(clang으로 윈도우에서 확인해보면 signed 출력)
    • clang window에서는 signed.
    • char sign_test = 255;
      • 주고 -1을 확인해보면 signed가 나온다.


char의 기본 부호가 지정 안 된 이유?

  • 아스키의 범위는 0~127 이므로 부호 여부는 상관이 없음.
    • 기본 부호 지정 안 된 이유는 char를 아스키 char만 사용한다고 가정하면 부호는 상관이 없음. 그게 0이든 1이든 간에 뒤에는 0,1 쓸 일이 없으니까 아예 첫 번째 비트, 최상위 비트 쓸 일이 없고 하위 일곱 비트만 쓰니까 상관이 없음
  • 단, 8비트 정수형으로 쓰려고 할 때는 반드시 char 앞에 signed나 unsigned를 붙이는 게 좋음.

  • 즉, 정수형 쓸 때는 컴파일러에 따라 자기 마음대로 선택이 가능하다.

  • 이걸 글자를 표현하는게 아니라 숫자를 표현하기 위해 C언어에서 쓴다면, signed, unsigned를 둘다 명시적으로 써 줘야한다.

  • 무시하고 그냥 char a; 이런식으로 쓰면? => 포팅이라는게 이걸 가져다가 다른 컴파일러에 컴파일 해서 다른 플랫폼(기계와 운영체제 합쳐둔걸 플랫폼이라고 여기서는 일단 지칭하자) 에서 돌게하려면? => 아무 문제 없이 코드 작성하려면 7비트에서 표현가능한거로 끝이 나야한다.


그것은 마지막 비트 7비트, 최상위 비트 뺀 최하위 7비트. 0~127밖에 안됨. 이 사이에서만 포팅시 문제가 안나고 이 이상 가려면 signed, unsigned를 다 지정해야 한다.

signed char signed_char = -1;
unsigned char unsigned_char = 255;

안 그러면 포팅해도 문제 없는 정수 범위는 0~127 뿐.

  • 0000 0000(2) ~ 0111 1111(2)



char의 부호를 판단하는 방법

limit.h의 헤더파일에서 CHAR_MIN을 보면

부호 식별자가 없는 char가 signed인지 unsigned인지 알 수 있음.


char로 표현 가능한 숫자의 범위

포팅 문제 없는 범위
unsigned charcharsigned char
0~2550~127-127~127

왜 -128이 아니라 -127일까? 2의 보수와 1의 보수가 있는데 2의 보수는 0이 하나. 근데 1의 보수는 -0, +0이 존재한다.(즉 0이 2개 가진 셈.)

만약 2의 보수를 기준으로 짰는데 1의 보수를 가진 컴파일러를 가진 기계나 OS를 만났다.

그럼 -128이 없어진다. 그렇기 때문에 1의 보수를 표준으로 본다(아주아주 옛날기계면 1의 보수 쓸지도).

그래서 일단 표준은 -127~127이 포팅에 문제 없는 범위.

사실 -127~127인 기계는 별로 없으나 혹시라도 만약을 위해 -127~127을 안전한 포팅범위로 설정해둔 것.


Char로 표현 가능한 숫자의 범위(보통)

  • 표준은 표준. 뭐 일단 이렇게 쓰자의 느낌.
  • 실제 보통(일반의 데스크탑 개발시) 안전하게 생각해도 되는 것.
    1. 크기 : 8비트
    2. 부호(unsigned/signed)를 생략할 경우 : signed
      • char signed_char = -1;
      • 근데 안드로이드 경우 gcc로 컴파일 해보면 unsigned 나옴. 🤔 뭐 그렇다고.
    3. 범위
      • 부호 없는 경우 (unsigned char): 0~255;
      • 부호 있는 경우 (signed char): -128~127;



short

short num = -30000; /* 기본 : signed */
unsigned short unsigned_short = 65535;
signed short signed_short = -32767;
  • 최소 16비트이고, char의 크기 이상인 정수형.(char가 몇비트든 최소 16비트는 되어야 함)
  • 포팅 문제 없는 값의 범위
    • 부호 없는 short(unsigned short): 0~ 65535
    • 부호 있는 short(short, signed short): -32767~32767



int

int num = -32767 /* 기본: signed */
unsigned int unsigned_int = 65535;
signed int signed_int = -32767

표준에 따르면 최소 16비트 그리고 short 이상인 정수형

왜 16비트? short와 길이가 같은데?



int는 기본 정수

  • int 는 그냥 정수라는 의미.

  • 따라서 앞 뒤 생략하 정수 처리하라고 하면 CPU는 딱 맞는 크기여야 한다.

  • 그게 무엇의 크기인가.
    • CPU의 산술 논리 장치 (ALU, Arimetic Logic Unit) 가 사용하는 기본 데이터
    • 이 데이터를 워드(word)라 하고, 그 크기를 워드 크기라고 함.
    • 워드 크기는 레지스터 크기랑 일치.
  • 즉, CPU 따라 다름.
  • 예전엔 16비트 CPU가 많았음 -> 그래서 최소 16비트



int 와 64bit 플랫폼

  • 그 이후 32비트가 나오면서 int의 크기는 32비트가 됨.
  • 그러나 이제 64비트 컴퓨터인데? 그래도 32비트로 머묾.
    • 원칙적으로는 C 표준을 어긴 것.(원칙적으로는 C표준은 CPU가 이해하는 사이즈이므로)
      • 그러나 64비트로 올리면 32비트 정수를 어떻게 쓰지? 또 32비트로 알고 그렇게 작성된 코드들도 어어마 무시하게 많다. 그걸 올리면 망가질 코드들이 너무 많다. 그래서 int 32비트라고 무조건 쓰게된 것.
      • 너무 오랫동안 32비트를 int 크기로 사용
    • 32비트에서 64비트로 바꾼다고 성능이 무조건 빨라지지도 않음.(이유: 캐시 메모리 등). 빨라질 수도 있고 그대로일 수 도 있다는 점.
      • 64 비트가 필요할 경우에만

      • 32비트 쓰다가 크기 큰 용량 같은 경우들에만 64비트 사용.

    • ‘int’를 64비트로 올리면? short는 32비트가 되나?



int로 표현 가능한 숫자 범위

  • 포팅에 안전한 숫자 범위: short와 같음.
  • 표준에 상관 없이 보통 안전하게 생각해도 되는 것.
    1. 크기 : 32비트
    2. 범위
      • 부호 없는 경우 (unsigned) : 0 ~ 4,294,967,295
      • 부호 있는 경우 (signed) : -2,147,483,648 ~ 2,147,483,647
      • 근데 이제 웬만한 플랫폼은 문제가 되지 않음.



int의 리터럴

int signed_int = -1024
unsigned_int unsigned_int1 = 394;
unsigned_int unsigned_int2 = 2147483648; /* 경고 */
unsigned_int unsigned_int3 = 2147483648u; /* 경고 없음. 대문자 U도 가능 */
리터럴
  • ‘u’ 또는 ‘U’ : 부호 없는 (unsigned) 수를 표현하는 접미사.
    • 부호 있는 수의 최대값 보다 큰 값을 unsigned_int에 대입할 경우 ‘u’ 또는 ‘U’를 붙여야 함.
    • 안 붙이면 경고 발생

“integer literal is too large to be represented in a signed integer type …..”



long

long num = -214783648;
unsigned long unsigned_long = 2147483647
signed long signed_long= -2147483648
  • int가 16비트일 때 그거보다 2배 큰 자료형 필요함
  • 따라서 long은 최소 32비트이고, int 이상의 크기

    • 다른 언어는 long 64비트가 기본적.
    • C언어 기준 당시에는 32비트만 해도 굉장히 긴거였다.
  • 그럼 최소 64비트인 정수형은?
    • C89엔 없다.
    • 포팅 안전한 범위 : -2147483647 ~ 2147483647
    • 표준에 상관없이 안전하다고 하는 것.(int와 동일)

long의 리터럴

long signed_long = -200000000l; /* 대문자 L도 됨 */
unsigned long unsigned_long1 = 2147483647;
unsigned long unsigned_long2 = 2147483648; /* 경고 */
unsigned long unsigned_long3 = 2147483648ul; /* 경고 없음 */
리터럴
  • ‘l’ 혹은 ‘L’: long 을 의미하는 접미사
  • ‘u’ 혹은 ‘U’: 부호 없는 (unsigned) 수를 표현하는 접미사
  • 두 접미사를 같이 쓸 수 있음. unsigned long이라는 의미가 됨.

ex) 2147483648ul; 2147483648LU;



float

float num = 3.14f; /* 컴파일 */
unsigned float unsigned_float = 3.14f; /* 컴파일 오류 */
signed float signed_float = - 3.14f; /* 컴파일 오류 */
  • 소프트웨어 공학에서 부동 소수점은 IEEE 754로 대동단결 되었다고 함.
    • float는 IEEE 754 single(32비트)
    • double은 IEEE 754 double(64비트)
  • 하지만 C는 CPU가 IEEE 754를 지원하는 실수 계산 장치를 장착하기 전 부터 쓰임

  • 표준에 따르면 C의 float은 IEEE754가 아닐 수 있음.
    • IEEE 754일수도 아닐 수도 있음.
    • 역시 컴파일러 구현에 따라 다름.(최소 C89에선)
    • 크기는 char 이상이기만 하면 됨.
  • unsigned 형 없음.
  • 표준에 상관 없이 보통 안전하게 생각해도 되는 것.
    • 크기: 32비트
    • 범위: IEEE 754 single과 동일
  • 관련 헤더 파일: float.h



float의 리터럴

float pi1= 3.14f /* f도 됨 */
float pi2= 3.14uf /* 컴파일 오류, float는 접미사 u를 안 씀 */
  • 리터럴
    • ‘f’ 또는 ‘F’ : float를 의미하는 접미사.



double

double num = 3.14f; /* 컴파일 */
unsigned double unsigned_double = 3.14f; /* 컴파일 오류 */
signed double signed_double = - 3.14f; /* 컴파일 오류 */
  • 표준에 따르면 CPU가 계산에 사용하는 ‘기본 데이터’ 크기
    • 크기는 float 이상이면 됨.
    • float은 그저 double 보다 빠르게 연산하기 위해 만든 부동소수점.
  • 역시 컴파일러 구현따라 다름.
    • IEEE 754 double이라는 보장이 없음.
  • unsigned형 없음.
  • 즉, 요즘 있는 일반적인 모던 CPU의 경우 32비트가 원칙상은 맞는 거일 수 있음. 근데 다른 언어는 float의 2배, float이 기본적으로 32비트 이렇게 가니까 좀 달라지긴 한다.

  • 컴파일러 따라 구현이 다르고, IEEE754의 double이라는 보장이 없었다.



long double

long double num = 3.14; /* 컴파일 */
unsigned long double unsigned_long_double = 3.14; /* 컴파일 오류 */
signed double signed_double = - 3.14f; /* 컴파일 오류 */
  • double 보다 정밀도가 높음
  • double 이상의 크기.
  • 다른 부동소수점들과 마찬가지로 unsigned 형이 없음.
  • 관련 헤더 파일 : float.h



  • 데스크톱에선 다른 언어와 비슷하게 사용 가능.
    • 예외: long(32비트)
  • 소형 기기를 다룰 때는?
    • 매뉴얼에서 자료형 크기 확인 후 사용
  • 여기저기 사용할 코드라면?
    • 포팅이 보장되는 범위의 값으로만 사용
    • float/double은 플랫폼 사이에 값이 정확히 일치 안 할수도 있음.


20221007_142742


bool 형

  • C89에 없다.
  • C99에서 (좀 이상한 형태로) 새로 들어옴
  • 대부분의 C 프로그래머들은 bool을 쓰지 않음.

bool형을 안 쓰는 이유

  • 정수로 대신 쓸 수 있음.
  • 0이면 false, 0이 아님 true.
  • 하드웨어에서도 실제 bool이 없음.
    • 0이냐 아니냐 만 있음.
#include <stdio.h>

int main(int argc, char ** argv)

{

  if(argc !=0){
    printf("%s\n", argv[0]);
  }
  return 0;
}

20221007_141218

오른쪽 위는 어셈블리어 코드가 저장된 메모리 주소. 실제 코드가 cmp, compare의 약자. 비교하다. je, jump equal. 같으면 점프하다.

xor ,pop(stack에서의 push,pop) , ret : return

bool을 안쓰다 보니

while문의 조건으로 숫자도 가능하다(…)

int count = 5;
while(counter --){ /* 나쁜 조건식 */
  printf('%d\n', counter);
}

코딩 표준: 참/거짓을 반환해야 할 때는?

  • C에서 참/거짓을 반환해야 하는 함수의 경우 보통 이렇게 함
    • 거짓일 때는 0을 반환
    • 참 일때는 1을 반환

ex) JAVA

public static void main(String args[])
{
  if(/* 조건 */){
    return true;
  }
  return false;
}

C

public static void main(String args[])
{
  if(/* 조건 */){
    return 1;
  }
  return 0;
}

열거형

C에서 열거형은 형을 강제한다기보다 그냥 정수에 별명 붙이는 정도의 수준임.

C#

enum EDay{Monday, Tuesday, Wednesday, /* 생략 */ }
enum EMonth{January, Februrary, March, /* 생략 */ }

EDay humpDay = EDay.Wednesday;
EMonth birthMonth = humpDay; //컴파일 오류

위 C#은 day, month가 있는데 알듯 월~일 ,1~12월까지 있다 근데 month에 요일을 넣으니 당연히 컴파일 오류

C

enum EDay{DAY_Monday, DAY_Tuesday, DAY_Wednesday, /* 생략 */ }
enum EMonth{MONTH_January, MONTH_Februrary, MONTH_March, /* 생략 */ }

enum day hump_Day = DAY_Wednesday;
enum month birth_Month = hump_Day; /* 컴파일 됨. 태어난 날이 수요일 */

C는 위와 달리 month에 day를 넣어도 컴파일이 된다. enum을 만들든 다른 int나 enum을 만들든 결과적으로 다 int. 그리고 대입이 다 자유롭게 가능하다.

그래서 이걸 꺠끗하게 나눴다기 보다는 정수형, int, monday는 0. DAY_Tuesday는 1. 이렇게 정해놓으면 보기도 어렵고 같은 그룹인지도 모름.

그룹 지어서 보기 쉽게 해놓자. 단 실제 작동은 int하고 똑같이 돌게 하자.

여기서 진화해서 타입까지 강요해주게 된 enum이 C#이다.



변수 선언 위치

  • 변수 선언은 반드시 블럭의 시작에서만 해야 함.
  • 코드 중간에 사용하는 변수는 블록 시작에서 선언만 하고 뒤에 대입.
#include <stdio.h>

int get_sum(int a, int b)
{
    return a + b;
}

int get_difference(int a, int b)
{
    return a - b;
}

int main(void)
{
    int a = 3;
    int b = 10;
    int sum = get_sum(a, b);
    printf("sum: %d\n", sum);

    int difference = get_difference(a, b);
    printf("difference: %d", difference);

    return 0;
}

위와 같은 코드도 실제 ide에서 실행시키면 잘 돌아가지만 c89기준에선 변수는 블록안에 선언되야 하므로 컴파일 안됨. 블록 안이라는게

int difference = get_difference(a, b) 를 실행하기 전에 printf()문을 출력했다. 이 경우 변수 선언보다 먼저 다른 행동을 한거라 c89에선 컴파일 시 에러를 띄운다.



C에서 새로 만나는 연산자

일반적인 연산자 및 연산자 우선순위는 구글링 하면 나온다. ex)http://www.tcpschool.com/codingmath/priority

C에서만 있는 연산자들이 있는데

  • sizeof
  • 역 참조 연산자
  • 주소 연산자
  • 구조체와 공용체 멤버 접근자 . 과 ->



sizeof()

char ch = 'a';
int num = 100;
char char_array[30];

size_t size_char = sizeof(ch);  /* 1 */
size_t size_char = sizeof(ch);  /* 4 */
size_t size_char = sizeof(ch);  /* 4 */
size_t size_char = sizeof(ch);  /* 30 */

20221007_152758

  • 피연산자의 크기를 바이트로 반환해주는 연산자
  • sizeof()는 함수가 아니다. 키워드라고 보면 된다.



sizeof()는 컴파일 중에 평가된다.

  • 실행 중이 아니라 컴파일 도중에 크기를 찾음.
  • 바꿔 말하면 컴파일 시 모르는 크기는 찾아 줄 수 없음.
  • char형을 넣으면 반드시 1이 반환
    • byte가 char과 같다.
  • 이 연산자가 반환하는 값은 부호 없는 정수형의 상수 (unsigned int)로 size_t형
    • 우리가 알던 unsigned int, unsigned short가 아닌 size_t형을 반환한다.


size_t

  • 부호 없는 정수형이나 실제 데이터형은 아님.
  • _t는 typedef를 했다는 힌트
    • typedef는 다른 자료형에 별칭을 붙이는 것.
    • 플랫폼에 따라 다른 자료형을 쓰기 위해서 size_t를 typedef한 것.
    • 예를들어 clang window는 아래와 같다.

20221007_153613



size_t의 크기

  • C89 표준은 size_t의 크기를 딱히 명시하지 않음.
  • 단, 배열을 만들면 그 배열의 바이트 크기를 얻을 수 있다고 명시함.
    • 여기서 추측할 수 있는 건 size_t는 최소 그 정도는 담을 수 있는 크기.
    • 그럼 배열이 얼마나 커지냐인데 2^8-1(=255)바이트는 너무 작고, 최소 2^16-1(=65535) 는 되어야 할 것 같음.
    • 다행히도, C99에서 확실히 최소 16비트를 요구함.
  • size_t를 쓰는 이유는 하드웨어에 따라 표현할 수 있는 최상의 사이즈가 있다는 것. 그래서 하드웨어에서 기본적으로 16비트로 unsigned 16비트 뭔가가 될거 32비트면 unsigned 32비트가 될거고 이건 알아서 하드웨어에 따라 컴파일러가 정의하라고 주는게 size_t
  • 보통은 unsigned int를 사용
    • typedef unsigned int size_t



size_t의 용도

  • 어떤 것의 크기를 나타내기 위해 사용.
  • 좋은 예: 반복문이나 배열에 접근할 때 사용.
    • 반복문의 카운터 변수에 음수가 필요 없을 때
    • 배열의 경우 길이가 음수가 될 수 없으니
      • my_array[-1] 이런거 없으니까.
int int_array[30];
size_t i;

for(int i =0; i<30; ++i){
  int_array[i]=i
}

이런식 위에 int도 size_t형으로도 줄 수 있고 원래는 그게 바람직 함.



size_t와 -1

  • C# String의 IndexOf() 함수는 문자를 못 찾으면 -1을 반환했음.
  • size_t 를 가지고도 같은 일을 할 수 있음.
    • 이걸 size_t로 보면 최대값. -> 왜냐하면 -1의 비트패턴을 보면 답이 나옴.
      • signed int -1 과 unsigned int 4,294,967,295는 같은 비트패턴을 가짐.
size_t get_students_index(const char* name)
{
    if(!/* 조건 */){
      return (size_t)-1;
    }
    return 올바른 인덱스;
}

20221007_160920

사진 보면 -1 signed int와 unsigned int 의 최고값은 동일하다. 음수는 근데 유효한 색인값이 아니라서 -1 반환하면 양수를 딱 알았는데, -1을 반환했을 떄 signed int를 쓴다. 그럼 signed int는 unsigned int에 비해 표현 가능한 양수 부분이 반쪽임.

음수 반쪽은 아예 안 쓰고 -1만 쓰는 거고, 비트 하나를 줄이면서 그렇게 된 것. 실제 int를 반환하는 함수를 썼을 때에는 배열에 크기가 size_t 로 표현할 수 있는 거 보다 절반이라는 의미.


그럼 그 상황에서도 unsigned int 만큼의 배열 요소를 가질 수가 없다. 그게 넘어가면 오버플로우가 되서 음수값이 이상하게 돌아올 수 밖에 없다.

그래서 더 큰 값에서 제일 긴 값, max값, 안 들어올법한 값을 잘못된거라 표현하는 게 문제가 있는 것은 아니다.


한 쪽 방향으 가면 확실히 음수가 표현되지만, 그 만큼 배열에 들어갈 수 있는 요소의 수가 절반이고, 실제 그 배열에 들어갈 수 있는 요소가 2배로 늘었는데, 그 2배까지 간다는 게 걱정이 되면 절반을 줄였을 떄 그걸 넘어가는 걱정부터 해야한다.

결국 두 방식을 비교하자면

그냥 눈에 보이기로는 음수 반환하는게 좋고,
실질적으로 생각하면 배열에 넣을 수 있는 공간이 2배보다 하나 작은 것.(-1개).

왜냐하면 제일 끝에 큰 값은 안 넣기 때. 그래도 2배 큰 게 사실은 실용적으로 더 효용성이 있는 것이다.


이거는 양쪽 방향이 틀린건 아닌데 , 어느 쪽으로 가든 상관이 없다.

size_t로 가서 제일 최고값을 잘못된 색인을 표현하는 반환 값으로 써도 그건 일반적으로 하는 일이다.



  • 그럼 위 경우에 다른 언어들 처럼 예외처리 exception, try~catch 구문을 쓰면 되지 않나?

-> C언어는 예외처리 ‘따위’ 존재하지 않는다. 🤔 exception, try, catch 전부 존재하지 않음.

그렇다고 exception 없다고 좋은 제품이 안나오건 아니다.

당장 윈도우 OS, 리눅스 OS들 얼마나 튼실한지 보면 알 것.

그래도 일단 예외는 C언어에 존재하지 않는다.



sizeof(): 배열의 크기가 다르다?

  • 배열의 경우, 함수 인자로 받을 경우 다른 결과가 나옴.
size_t get_char_array_size(char data[])
{
  return sizeof(data);
}

int main(int argc, char **argv)
{
  char char_array[30];
  size_t size_array = sizeof(char_array); /* 30 */
  size_t size_array_2 =  get_char_array_size(char_array); /* 4 */

}

20221007_164516

sizeof()라 해서 반드시 배열에 모든 byte가 나오는 건 아니다. 실행해보면 매개변수로 들어온건 4가 나온다. 64비트에서 컴파일 하면 8이 나옴.



역참조 연산자 *

int num = 10;
int *p = &num; /* 역참조 연산자 (X). 포인터 변수 선언 */
int num1 = *p; /* 역참조 연산자 (O) */

int result = num1 * num2; /* 역참조 연산자(X), 곱셈 연산자 */

  • *의 피연산자가 하나면 역 참조(indirection, derefence) 연산자.
    • 피 연산자가 둘인 곱셈연산자가 아님.
  • 포인터형 변수에만 사용 가능.
  • 주소 연산자와 더불어, 메모리를 들었다 놨다 하는 연산자.
  • ‘포인터형 변수에 저장된 주소’의 위치에 들어있는 값에 접근.



주소 연산자 &

int num = 10;
int *p = &num;

int still_num = num & num; /* 주소 연산자(X), 비트 연산자(O) */

  • 역시 피연산자가 하나일 떄는 주소 연산자.
    • 피연산자가 2개일 떄 쓰는 비트연산자가 아님.
  • 어떤 변수의 메모리 주소를 반환

&로 변수의 주소를 가져오고 역참조 연산자를 통해 그 변수의 주소를 저장한다.(포인터 변수)



구조체와 공용체 멤버 접근자 . 과 ->

. 연산자

  • C#이런데서 썼지만 여기는 클래스가 없어서 함수 호출에 쓸 수 없다.
  • 단, 구조체와 공용체는 있으므로, 그들의 멤버변수에 접근 시 사용한다.

    • 원래 구조체는 순수한 데이터만 모아 둔 것(DTO 느낌).
    • 한단계 발전해서 구조체에 함수를 넣었는데 이게 클래스이다.
    • 클래스가 있고 개체가 있으면 거기서 변수 접근할 수 있다.


-> 연산자(화살표 연산자)

  • 2개의 연산 ‘.’ 과 ‘*‘를 합친 것
  • 이 또한 구조체와 공용체의 멤버변수에 접근 할 시 사용.



조건, 반복문.

if문과 bool 표현식(Boolean Expression)

  • C의 비교/조건 연산자 사용한 반환식은 참이면 1, 거짓이면 0 반환
  • 만약 b>10이라는 식이 있으면 true/false반환이 아닌 C언어에서는 1, 0을 반환한다.

  • 숫자를 if문의 조건식에 넣어도 곧바로 판단이 가능하다.
    • false:0
    • true:1
  • 메모리 주소(포인터) 나 float형(예: 3.14f)도 마찬가지이다.

    • 모든 비트 패턴이 0이면 false, 아니면 true

20221007_171458

숫자를 if문에 넣어도 곧바로 판단이 가능하다.-> if문은 숫자가 내부에 올 것을 기대함. 비트 패턴을 보면 float 0.0은 IEEE 754. 32에 의하면 모든 비트가 0이여야 한다. 0이 오면 거짓으로 판단하고 출력 안해야 하고.

실제 num 주소에 가보면 주소에 num 파일이 저장되어 있다고 나온다. 그 주소를 쳐서 가본다. 가봤더니 0.0이라고 했었는데, 실제 00 00 00 00 4바이트. 그래서 0이 저장되어 있다.

오른쪽은 3.14f인데, 비트패턴이 0은 아니다. 실제로 보면 3.14 저장된 메모리가 있고, 가져와 보면 0은 아니다. 0이 아닌게 들어왔기 떄문에 true로 판단해서 출력한 것.

이런식으로 if문이 돈다.



switch/case문

int num =1 ;
switch(num){
  case 0:
      ~
      break;
  case 1:
      ~
      break;
  case 2:
      ~
      break;
}

자바나 다른 언어의 경우 switch(num)의 num자리(변수자리)에 String 형태도 가능했었다.


근데 C언어는 “정수형(int, char, enum)” 의 형태만 가능하다.

const char* name = "banana";
switch(name) /*  컴파일 오류 */
{
  case "naver": /* 컴파일 오류 */
    ~
    break;
  default:
    ~
    break;

}



case 안에서 break 문을 빼먹으면?

  • switch문을 곧바로 탈출하지 않고 아래 코드 계속 실행
    • 보통 다른 case나 default 레이블에 있는 코드
    • 다른 곳에서 break를 만나거나, switch 블록의 끝에 도착하면 탈출
  • 이렇게 아래 코드를 계속 실행하는 걸 fall-through라고 한다.
    • 의도적으로 fall-through를 만드는 경우도 있는데 이 경우 /* intention fallthrough */를 반드시 붙여야 한다.

Case 레이블은 반드시 상수만.

  • 또한 이 상수는 반드시 컴파일 시에 결정되어야 함.
    • 컴파일러가 자동으로 if문 만들어준다 생각하면 된다.



반복문

for

  • C언어에는 foreach는 존재하지 않는다.
  • for 문의 초기화 코드에 size_t i =0; 을 못 씀.
    • 변수 선언은 제일 위에서 하기 때문.
  • 컴파일 실패나는 코드 :
int sum = 0;

for (size_t i=0; i<10; i++){ /* 오류 */
  sum +=i;
}
  • 올바른 코드 :
int sum = 0;
size_t i;
for (i=0 ; i<10; i++){ /* 오류 */
  sum +=i;
}

while

  • while문은 특별히 다른 언어와 다른 점은 없다.
    • 반복에서 탈출하려면 break
    • 다음 회차 가려면 continue
      • 이 2개는 for, do while에서도 사용 가능.

코딩 표준 while문

  • 조건식은 bool 형 (true/false) 대신, 1/0을 반환.
  • 그래서 counter 변수를 넣는 경우가 있는데 좋은 경우는 아니다.
  • ’==0’ 이나 ‘!=0’ 을 넣는게 좋다.

  • 안 좋은 코드:
int day = 5;
while(day--){
  printf("%d\n", day);
}
  • 권장하는 코드:
int day = 5;
while(day-- != 0 ){
  printf("%d\n", day);
}

do while

  • while과 마찬가지로 특별한 점은 없다
int day = 5;
do{
  printf("%d\n", day);
}while(day-- != 0 )





© 2021.03. by yacho

Powered by github