크래프톤 정글 WIL_Week05

이승우·2023년 5월 11일
0

크래프톤 정글

목록 보기
6/14
post-thumbnail

Week 05 주차를 마무리 하며 이번주 공부 한것을 적어 내려가려 한다.

이번주 부터는 C언어를 시작하고 RB트리(Red-Black Tree)를 구현하는 과제가 주어졌다. 이 과제를 진행하면서 우선 내가 C언어를 처음 시작하고 부족한 부분을 모두 채워서 RB트리를 구현해내는건 현실적으로 불가능에 가깝다,, 하지만 C언어를 처음 하는 내 입장에선 단순히 언어를 배운다기 보단 정말 베이직한 부분들을 다루는게 정말 많다. 그래서 난 최대한 절대적인 시간을 들여서 C언어를 공부 할거 같다. 최소한 C를 알고 있다면 언어를 그만큼 알고 익숙하다는 뜻이니 개념 공부를 하면서 C언어 문법이나 자료구조등을 구현하는 연습을 같이한다면 개념공부도 하면서 구현에 들이는 시간과 익숙해지는 시간을 최대한 단축시키면서 할수 있을것 같다 라는 생각이 들었다. 직접적인 구현에 들어 가기 앞서 간단하게 노션에 정리된 3분PT- C언어를 보면서 개념들을 정리 하고 추가적인 참고 자료들도 같이 넣을 예정이다. 이번주도 내가 최선을 다하고 있는지 다시 한번 돌아보고 계속 발전해 나가는 개발자가 되고 싶다.

이번주 C언어 공부 내용 목차를 정리해보자면 다음과 같다:

  1. 프로그램 구조
  2. 자료형
  3. 변수
  4. 연산자
  5. struct(구조체)
  6. union(공용체)
  7. enum and typedef(열거형 및 타입 정의(축약형 이름 부여))
  8. 포인터(pointer)
  9. 배열(array)
  10. 문자와 문자열
  11. 함수
  12. 조건문
  13. 반복문
  14. Scope (스코프 - visibility / life-time)
  15. 메모리 관리
  16. 입력과 출력
  17. 파일 입출력
  18. 전처리기 (preprocessor)
  19. 헤더파일
  20. 에러 처리
  21. 가변인자

3분PT - C언어참고: https://www.notion.so/kraftonjungle/3-PT-C-4994f6fe77b74e98a126e861990d40ba#84e3f1b668184562b1e94677f53218c8


  1. 프로그램 구조

    Hello World 예제

    다음은 "Hello World"를 출력하는 간단한 C코드입니다.

	#include <stdio.h>

	/* 여러줄 커맨트는 
  	이렇게 적으세요 */

	int main() {
 
   	// 한줄 커맨트는 이렇게 적으세요
   	printf("Hello, World! \n");

   	return 0;
	}
  • #include<stdio.h>는 stdio.h 파일을 포함하라는 명령어입니다.
  • 프로그램이 시작되면 무조건 int main()이라는 함수를 실행합니다.
  • 커맨트는 / ... / 안에 적습니다. 한줄 커맨트는 // 로 시작합니다.
  • printf(...)는 출력을 하는 함수입니다.
  • return 0는 main() 함수를 끝내고, main함수의 결과로 0으로 반환한다는 의미입니다.

C파일 컴파일과 프로그램 실행

리눅스에서 C소스 파일을 컴파일을 하고, 실행하는 방법입니다.

  • 위의 소스코드를 편집기에 입력한 후에 hello.c 이름으로 저장합니다.
  • 쉘커맨드에서 해당 파일이 있는 폴더로 이동합니다.
  • gcc hello.c 를 입력하여, 소스파일을 컴파일합니다.
  • 만약 코드에 에러가 없다면, 컴파일러는 a.out 실행 파일을 생성합니다.
	$ gcc hello.c
	$ ./a.out
	Hello, World!

	# 실행파일명 지정하기
	$ gcc hello.c **-o hello.out**

명령문(statement)과 세미콜론

  • 세미콜론은 명령문의 끝을 의미합니다.
  • 각 명령문은 반드시 세미콜론으로 끝나야 합니다.
  • 라인의 변경이 아니라 세미콜론에 의해서 명령문이 나누어 집니다.

다음은 두개의 명령문 형태는 의미적(semantic)으로 같습니다.

	// 명령문1
	printf("Hello, World! \n");
	return 0;

	// 명령문2
	printf("Hello, World! \n"); return 0;

예약어 (reserved word)

다음은 C에서 특별한 의미로 사용되는 키워드이며, 예약어라고 한다. 이것들은 상수명, 변수명, 식별자명으로 사용할수 없습니다.

	auto        double      int         struct 
	break       else        long        switch 
	case        enum        register    typedef 
	char        extern      return      union 
	const       float       short       unsigned 
	continue    for         signed      void 
	default     goto        sizeof      volatile 
	do          if          static      while

  1. 자료형

    정수형

    C의 다양한 정수형들은 각기 그 크기가 다르다. int형은 주로 32비트지만, 오래된 CPU에서는 16비트일 수도 있습니다. 몇몇 프로그램은 int에서 저장할 수 없을 만큼 큰 숫자를 필요로 할 때가 있습니다. 이를 위한 C 언어의 자료형이 바로 long 정수형입니다. 반대로 컴파일러에게 int보다 메모리 공간을 덜 먹는 자료형을 사용하게 지시해줘야할 때가 있습니다. 이럴 때 사용하는 정수형은 short 정수형입니다.

    타입크기(bytes)값의 범위
    char1unsigned OR signed
    unsigned char10 to 2^8-1
    signed char1-2^7 to 2^7-1
    int2 / 4unsigned OR signed
    unsigned int2 / 40 to 2^16-1 OR 2^31-1
    signed int2 / 4-2^15 to 2^15-1 OR -2^31 to 2^32-1
    short2unsigned OR signed
    unsigned short20 to 2^16-1
    signed short2-2^15 to 2^15-1
    long4 / 8unsigned OR signed
    unsigned long4 / 80 to 2^32-1 OR 2^64-1
    signed long4 / 8-2^31 to 2^31-1 OR -2^63 to 2^63-1
    long long8unsigned OR signed
    unsigned long long80 to 2^64-1
    signed long long8-2^63 to 2^63-1

    정수형 상수를 새 정수형 long long int로 강제해주고 싶다면 상수 뒤에 LL 혹은 ll을 적어주면 됩니다. 단, 여기서는 둘 다 소문자거나 대문자여야합니다. LL 혹은 ll 앞에 혹은 뒤에 U 혹은 u 를 추가해주면 해당 상수는 unsigned long long int가 됩니다.

    소수형

    소수점 이하의 값도 다뤄야하거나, 엄청 크거나 작은 숫자를 다뤄야할 때가 있습니다. 이러한 숫자들은 소수점 형식에 저장합니다. C에는 각기 다른 소수점 형식에 따라 세 가지 소수형floating type이 있습니다.

    float은 정확성이 그렇게 필요하지 않은 경우(소수점 이하 한 자리 등)에 적합합니다. double은 대부 분의 프로그램에 적합한 수준의 정밀함을 제공합니다. long double의 경우 상당한 정밀함을 제공하지만 거의 사용하지 않습니다.

    타입크기(bytes)값의 범위
    float4±1.2×10^-38 to ±3.4×10^38
    double8 / 4±2.3×10^-308 to ±1.7×10^308 OR alias to float for AVR.

    가끔 컴파일러에게 소수점 상수를 강제로 float 또는 long double 서식에 저장하게 만들어줄 때가 있 습니다. 만약 단일 정밀도로 사용하고 싶다면 F 혹은 f를 상수끝에 추가해주어야 합니다. (57.0F) 상수를 long double 서식으로 저장하고 싶다면 말미에 L 혹은 l을 추가해주어야 합니다. (57.0L)

    C99에서 확장된 자료형

    타입크기(bytes)값의 범위
    int8_t1-2^7 to 2^7-1
    uint8_t10 to 2^8-1
    int16_t2-2^15 to 2^15-1
    uint16_t20 to 2^16-1
    int32_t4-2^31 to 2^31-1
    uint32_t40 to 2^32-1
    int64_t8-2^63 to 2^63-1
    uint64_t80 to 2^64-1
    bool1true / false or 0 / 1

    void

    일반적으로 void는 없다 또는 알수없는 상태를 의미합니다. void를 쓸수 있는 3가지 경우는 다음과 같습니다.

    • 함수의 결과로 void를 리턴
      - 함수 수행의 결과로 아무것도 리턴하지 않는다는 것을 의미합니다.
      - 예: void foo()
    • 함수의 인자로 void를 받기
      - 함수에 인자로 아무것도 받지 않는다는 것을 의미합니다.
      - 예: int bar(void)
    • void의 포인터
      - void*는 "void 포인터"라고 읽습니다.
      - 객체의 메모리주소를 의미합니다.

    숫자형 리터럴

    프로그램 실행중에 고정되어 변하지 않는 값을 리터럴이라고 합니다.

    0b1111  // 이진수 1111 -> 리눅스계열에서만 가능
    0B1111  // 이진수 1111 -> 리눅스계열에서만 가능
    
    0377    // 8진수 377
    
    255     // 10진수
    
    0xff    // 16진수 FF 
    0xFF    // 16진수 FF 

  1. 변수

변수 정의와 초기화

	int x;          // x 변수는 int 타입이라고 선언합니다
	char x = 'C';   // x 변수는 char 타입이며, 'C'문자값으로 초기화합니다
	float x, y, z;  // 여러개의 변수 x,y,z 를 float타입으로 선언합니다

	const int x = 1; // x는 선언이후, 절대로 다른 값으로 변경될수 없습니다

	int x, y, z;
	y = 1;
	z = 2;
	x = y + z;

선언 vs 정의

  • 변수의 선언은 컴파일러에게 변수가 어떻게 생겼는지 정보를 알려주는 것입니다.
  • 변수의 정의는 컴파일러에게 실제로 어디에, 얼마만큼의 메모리를 생성할지 알려주는 것입니다.
	// x라는 int 타입의 변수가 다른파일 어딘가에 있다는 것을 컴파일러에게 알려줍니다
	extern int x;   
 
	int main () {
 
   	int a, b;  // a, b라는 int 타입 변수를 이 파일에 위치시킵니다
 
   	// 변수에 값을 할당합니다 
   	a = 10;
   	b = 20;
   
   	c = a + b;

   	printf("value of c : %d \n", c);
  
   	return 0;
	}
 
	>> 출력결과
	value of c : 30

네이밍 - 식별자 (identifier)

식별자는 변수, 함수등의 이름을 말합니다. 식별자는 A..Z, a..z, 로 시작할수 있으며, 그다음은 A..Z, a..z, , 0..9 문자로 구성될 수 있습니다.

	// 가능한 식별자
	mohd
	zara
	abc
	move_name
	a_123
	myname50
	_temp
	j
	a23b9
	retVal

	// 불가능한 식별자
	2022abc   // 숫자로 시작할수 없음
	while     // 예약어는 식별자가 될수 없음

스토리지 클래스 (storage class)

스토리지 클래스는 변수와 함수의 스쿠프(scope - visibility)와 생명주기(life-time)을 정의합니다.

  • auto storage class
    • auto는 모든 지역 변수를 위한 디폴트 스토리지 클래스입니다.

    • auto는 함수내에서만 사용될수 있으면, 생명주기가 함수의 호출/종료까지로 제한됩니다.

    • 함수내의 변수는 스택에 위치하므로, 함수호출 종료시 팝되게 되어 있습니다

      다음 두가지 변수정의는 동일합니다
      	{
         	int mount;
         	auto int month;
      	}
  • register storage class
    • register는 지역변수가 RAM이 아니라, 레지스터에 정의하는데 사용합니다.

    • 따라서 해당 변수의 크기는 레지스터 크기와 동일합니다(일반적으로는 word).

    • 또한 '&'와 같이 메모리 주소를 참조하는 연산을 사용할수 없습니다.

    • 레지스터는 카운터와 같이 빠른 액세스가 필요한 변수에 대해서 유용합니다.

      	{
         	register int  miles;
      	}
  • static storage class
    • static은 컴파일러에게 해당 변수의 스쿠프를 벗어낫을때 파괴하지말고, 프로그램의 종료시점까지 생명주기를 같게 하도록 합니다.

    • 따라서, 함수내의 지역변수가 매번 호출될때마다, 기존 지역변수의 값을 그대로 유지하고 있게 됩니다.

    • static이 전역변수에 적용되게 되면, 해당 변수의 스쿠프는 파일에 한정되게 합니다.

    • 전역변수가 파일내에서만 사용 가능한 private 변수가 됩니다.

      	static int count = 5; // count는 현재의 파일 내에서만 사용 가능한 전역 변수로 정의합니다
      
      	void func( void ) {
       
         	static int i = 5; // i는 프로그램 시작시 5로 초기화됩니다
         	i++;              // 함수 호출이 종료하더라도, i는 메모리에 계속 유지됩니다
       
         	printf("i:%d, count:%d\n", i, count);
      	}
      
      	int main() {
       
         	while(count--) {
            	func();
         	}
           
         	return 0;
      	}
       
      	>>> 실행결과
      	i:6, count:4
      	i:7, count:3
      	i:8, count:2
      	i:9, count:1
      	i:10, count:0
  • extern storage class
    • extern은 전역변수가 모든 프로그램 파일에서 보여질수 있도록 합니다.
    • 여러 파일을 가지고 있고, 한개의 파일에서 전역 변수 또는 함수를 정의하고,
    • extern을 이용해서 다른 파일이 해당 전역 변수와 함수를 접근할수 있게 합니다. 다음은 두개의 파일에서 같은 전역 변수와 함수를 extern을 통해서 공유하는 예입니다.
       	#include <stdio.h>
        
       	int count ;
       	extern void write_extern();  // write_extern 함수가 다른 파일에 정의되어 있음
        
       	main() {
           	count = 5;
           	write_extern();
       	}
      main.c
       	#include <stdio.h>
         
       	extern int count;  // int타입 count 변수는 다른 파일에 정의되어 있음
         
       	void write_extern(void) {
          	printf("count is %d\n", count);
       	}
      support.c

타입캐스팅

타입캐스팅은 다른 타입으로 전환하는 것을 말합니다.

	char x = 1; 
	float y = (float) x;  // (float)x  -> x를 float타입으로 타입캐스팅 -> 1.0

  1. 연산자

산술연산자 (arithmatic operators)

int a = 10;
int b = 2;
 
a + b
a - b
a * b
a / b
a % b    // a를 b로 나눈 나머지
 
a++
b--
++a
--b

비교연산자 (relational operators)

  • && → and
  • || → or
  • ! → not
4 > 2      # true  크다
5 < 1      # false 작다
6 >= 5     # true  크거나 같다
4 <= 4     # true  작거나 같다
3 == 5     # false 같다
4 != 7     # true  같지 않다

논리연산자 (logical operators)

  • & → and
  • | → or
  • ^ → exclusive (서로 다르면)
  • ~ → not
pqp & qpq
00000
01011
11110
10011
a = true;
b = false;
 
(a && b)    # false    모두 참이어야 참을 반환한다.
(a || b)    # true     둘 중 하나만 참이면 참이다.
!(a && b)   # true     모두 참인게 아니면(not)

비트연산자 (bitwise operators)

A = 60  // 0011 1100
B = 13  // 0000 1101
-----------------
 
A&B = 0000 1100
A|B = 0011 1101
A^B = 0011 0001
~A =  1100 0011
 
A << 2  = 1111 0000
B >> 2  = 0000 0011

할당연산자 (assignment operators)

a = b
c += a    // c = c + a
c -= a    // c = c - a
c *= a    // c = c * a
c /= a    // c = c / a
c %= a    // c = c % a
c <<= a    // c = c << a
c &= a    // c = c & a
c ^= a    // c = c ^ a
c |= a    // c = c | a

기타연산자

sizeof(int)  // int타입의 크기 -> 4
&a           // a의 주소
*a = 1       // 포인터a가 참조하는 메모리에 1을 넣기
b = *a       // 포인터a가 참조하는 메모리의 값을 가져와서, b에 넣기

연산자의 우선순위

카테고리연산자적용방향
Postfix() [] -> . ++ - -Left to right
Unary+ - ! ~ ++ - - (type)* & sizeofRight to left
Multiplicative* / %Left to right
Additive+ -Left to right
Shift<< >>Left to right
Relational< <= > >=Left to right
Equality== !=Left to right
Bitwise AND&Left to right
Bitwise XOR^Left to right
Bitwise OR
Logical AND&&Left to right
Logical OR
Conditional?:Right to left
Assignment= += -= *= /= %=>>= <<= &= ^==
Comma,Left to right

  1. struct(구조체)

구조체는

struct는 여러개의 데이터 아이템을 가지는 새로운 데이터 타입을 정의할 때 사용합니다.

// struct person 타입 선언
struct person { 
  char gender;  // 성별 - 멤버변수
  int  age;     // 나이 - 멤버변수
};              // 반드시 세미콜론으로 끝나야함. 가끔 빠뜨리는 실수함

struct person man1, man2;   // 변수 man1, man2는 person타입임

// 재귀형태로 struct 사용
struct item {
  int value;
  struct item* next;   // 멤버변수 next는 struct item의 포인터 타입임
};

구조체 멤버 접근

struct person { 
  char gender;  // 성별 - 멤버변수
  int  age;     // 나이 - 멤버변수
};              // 반드시 세미콜론으로 끝나야함. 가끔 빠뜨리는 실수함

struct person man1;

// 멤버변수 접근
man1.gender = 'M';
man1.age = 21;

struct person* man2;
...

// struct 포인터의 멤버 접근
man2->gender = 'F';
man2->age = 22;

멤버별 비트설정

struct person {
   unsigned int age : 3;     // 멤버 age는 3비트 저장공간을 할당
   unsigned int height : 5;  // 멤버 height는 5비트 저장공간을 할당 
} man;

int main( ) {

   man.age = 4;
   printf( "Sizeof( man ) : %d\n", sizeof(man) );
   printf( "man.age : %d\n", man.age );

   man.age = 7;
   printf( "man.age : %d\n", man.age );

   man.age = 8;  // 8은 0b1000 4비트 -> 3비트 이상부분은 저장안됨
   printf( "man.age : %d\n", man.age );

   return 0;
}

>>> 실행결과
Sizeof( Age ) : 4
man.age : 4
man.age : 7
man.age : 0

  1. union(공용체)

union은

여러개의 타입을 하나의 메모리 공간에 위치시킬때 사용합니다. 이때 메모리의 크기는 가장 큰 타입의 크기와 같습니다.

union Data {
   char c;
   int i;
   char str[20];
};

int main( ) {

   union Data data;   // data변수는 20 byte 크기를 가지게 된다      

   printf( "size: %d\n", sizeof(data));

   

   return 0;
}

>>> 실행결과
size: 20

union 멤버에 접근

#include <stdio.h>
#include <string.h>
 
union Data {
   int i;
   float f;
   char str[20];
};
 
int main( ) {

   union Data data;        

	 // union의 멤버 접근하기
   data.i = 10;
   data.f = 220.5;
   strcpy( data.str, "C Programming");

   // 아래의 data.i, data.f, data.str은 같은 공간을 공유
   // 따라서 마지막 data.str의 설정값으로 overwrite된 상태임
   printf( "data.i : %d\n", data.i);
   printf( "data.f : %f\n", data.f);
   printf( "data.str : %s\n", data.str);

   return 0;
}

>>> 실행결과
data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming

  1. enum and typedef(열거형 및 타입 정의(축약형 이름 부여))

enum은

숫자형 상수들을 하나의 데이터 타입으로 표현할때 사용합니다.

enum status1 {
   ON,              // ON 상수를 정의. 디폴트는 0부터 시작. ON = 0
   OFF,             // OFF 상수를 정의. 이전 상수값+1. OFF = 1
};

enum status2 {
   STATE_1 = 2,    // 상수값을 2로 정의
   STATE_2 = 7,    // 상수값을 7로 정의
};

int main( ) {

   enum status1 a = ON;
   printf("a: %d\n", a);

   enum status1 b = STATE_2;
   printf("b: %d\n", b);

   return 0;
}

>>> 실행결과
a: 0
b: 7

typedef는

길어진 타입 정의 또는, struct 타입 정의에 축약형 이름을 부여하는 것입니다.

// unsigned short타입을 myuint 타입으로 정의
typedef unsigned short myuint;
myuint a = 10;                   // a를 myunint 타입으로 정의

// struct person {...}을 person_type으로 정의
typedef struct person {
  char gender;
  int age;
} person_type;       

person_type man1;    // man 변수는 person_type임
man1.age = 12;

// enum status를 myenum 타입으로 정의
typedef enum status {
   ON,
   OFF,
} myenum;

myenum st = OFF;

  1. 포인터(pointer)

포인터는

모든 변수는 메모리에 위치합니다. 이때, 메모리 위치를 메모리주소(address)라고 합니다. 포인터는 이러한 메모리주소를 담을수 있는 변수를 말합니다.

i = 3;
int* j = &i;    // 65524

포인터 선언

int a = 1;
float b = 2.0f;

int* x;     // int형 변수의 주소를 담을수 있는 변수(포인터)
x = &a;     // &는 해당위치의 주소를 가져오는 연산자임

float* y;   // float형 변수의 주소를 담을수 있는 변수(포인터)
y = &b;

struct person { ... } c;
struct person* w = &c;   // struct person형 변수의 주소를 담는 변수(포인터)

void* z;    // 모호한 타입의 주소를 담는 변수
z = x;
z = y;
z = w;

포인터 접근

int a = 1;
int* x = &a;   // &a : a의 주소를 가져오기

int b = *x;    // x포인터가 가르키는 주소에 들어있는 값을 b에 할당 -> 1

x              // 포인터가 가르키는 주소값 -> a의 주소
*x             // 포인터가 가르키는 주소의 메모리에 들어있는 값 -> a의 값 -> 1
*x = 10;       // 포인터가 가르키는 주소의 메모리에 10으로 할당 -> a = 10

struct person { 
  char gender;
  int  age;
};
struct person man;
man.gender = 'M';
man.age = 21;

struct person* pman = &man;
pman->age = ...;   // 포인터가 가르키는 struct의 멤버 접근

void* y = &a;

*y = 10;          // y가 가르키는 타입이 무엇인지 y는 몰라서 오류 발생함
                  // error: incomplete type 'void' is not assignable

*(int*)y = 10;    // y가 가르키는 주소가 int값을 담고 있는 메모리라고 알려줌. 에러 안남

NULL포인터

  • 변수가 아무런 주소값을 가지고 있지 않을때, NULL 이라는 값을 할당하고
  • 변수를 널포인터라고 부릅니다.
int* ptr = NULL;       // ptr은 NULL 아무 주소도 할당되어 있지 않다
                       // NULL = 0

if (ptr) { ... }       // ptr에 주소값이 NULL이 아닌값이 할당된 경우
if (!ptr) { ... }      // ptr에 NULL로 할당되어 있는 경우

  1. 배열(array)

배열은

배열은 같은 타입의 요소가 연속적으로 고정된 길이만큼 존재하는 자료구조를 말합니다.

배열 선언

int values[10];   // values변수는 10개의 int를 담을수 있는 변수 공간(배열)
int values[10] = {1,2,3,4,5,6,7,8,9,10};  // values 요소들을 값으로 초기화

int values[] = {1,2,3};  // values를 초기화한 값들의 개수만큼만 배열을 만듦

배열의 차원

int values[10];    // 10개의 요소를 가진 1차원 배열

// 3 * 2개의 요소를 가진 2차원 배열
int values[3][2] = { 
	{1,2}, 
	{3,4},
	{5,6}
};

for (int y=0; y<3; y++)
  for (int x=0; x<2; x++)
    printf("values[%d][%d]: %d\n", y, x, values[y][x]);

배열의 접근

int values[10];
values[0]     // 배열의 첫번째 요소 접근

values[2]     // 배열의 3번째 요소 접근
*(values + 2) // 배열의 3번째 요소 접근

&values[2]    // 배열의 3번째 요소의 주소값
values+2      // 배열의 3번째 요소의 주소값 

배열의 크기 구하기

int values[10];

sizeof(values) / sizeof(int)          // 약간 unsafe
sizeof(values) / sizeof(values[0])    // safe

  1. 문자와 문자열

사용사례

문자열은 문자타입의 1차원 배열이며, 항상 마지막이 NULL로 끝납니다. 다음 예는 문자열을 선언하고 초기화하는 3가지 방법을 보여줍니다.

// a변수에 문자 'X'값을 할당
char a = 'X';

// greeting이라는 6개의 요소로 구성된 배열을 정의
char greeting[] = "hello";
char greeting[6] = {'h', 'e', 'l', 'l', 'o', '\0'};

// hello라는 문자열을 가르키는 포인터 greeting
char* greeting = "hello"; 

문자열 관련 함수

char s1[12] = "Hello";
char s2[12] = "World";
char s3[12];

strcpy(s3, s2)      // s2를 s3으로 복사

strcat(s1, s2)      // s1의 끝에 s2를 concat(이어붙이기) -> HelloWorld

strlen(s1);         // s1의 길이 -> 10

strcmp(s1, s2)      // s1과 s2를 비교
                    // if (s1 == s2) -> 0을 반환
                    // if (s1 < s2) -> 0보다 작은값 반환
                    // if (s1 > s2) -> 0보다 큰값 반환

strchr(s1, ch)      // s1문자열내에서 ch 문자가 처음 발견되는 포인터를 반환
strstr(s1, s2)      // s1문자열내에서 s2문자열이 처음 발견되는 포인터를 반환

strncpy(s1, s2, n)  // s2의 n개의 문자만 s1으로 복사 
strncat(s1, s2, n)  // s2의 n개의 문자만 s1의 끝으로 concat
strncmp(s1, s2, n)  // s1,s2의 처음 n개의 문자만 비교

  1. 함수

함수 정의 (definition)

// 리턴타입 함수명(인자리스트) 
int max(int num1, int num2) {
 
   // 지역 변수 정의
   int result;
   
   ...
  
   return result;
}

함수 선언 (declaration)

  • 함수의 선언은 컴파일러에게 함수의 모양을 알려주는 역할을 합니다.
  • 즉, 함수의 실제 구현(정의)과 선언을 분리한다는 것입니다.
  • 파일내에서 다른 함수에서 뒤에서 구현될 함수를 호출하기 위해서는, 함수의 모양을 미리 알고 있어야 하므로,
  • 전방선언(forward declaration)을 통해서 컴파일러에게 함수의 모양을 알려줍니다.

다음은 전방 선언의 예입니다.

#include <stdio.h>
  
// 함수의 전방선언 - 구현없이 선언만
int max(int num1, int num2);
  
int main () {
 
   int a = 100;
   int b = 200;
   int ret;
  
   // 함수 호출
   ret = max(a, b);
  
   printf( "Max value is : %d\n", ret );
  
   return 0;
}
  
// int 2개를 인자로 받아서, int를 리턴하는 함수
int max(int num1, int num2) {
 
   int result;
  
   if (num1 > num2)
      result = num1;
   else
      result = num2;
  
   return result;
}

extern을 이용해서 함수를 선언하면, 다른 파일에서 함수를 사용할 수 있게 할수도 있습니다.

다음은 extern 선언으로 함수를 공유하는 경우입니다.

#include <stdio.h>
 
extern void write_extern();
 
main() {
    write_extern();
}
#include <stdio.h>
  
void write_extern(void) {
   printf("count is %d\n", count);
}

call by value와 call by reference

함수를 호출할때, 함수에 인자를 넘겨주는 방법에 두가지 방법이 있습니다.

  • call by value
  • call by reference

call by value는 아규먼트(argument)값을 파라미터(parameter)에 복사하여 넘겨주는 방식입니다. 이것은 함수 내부에서 파라미터값을 수정하더라도 아규먼트에는 전혀 영향이 없습니다.

call by reference는  아규먼트(argument)의 주소값을 파라미터(parameter)에 복사하여 넘겨주는 방식입니다. 주소가 넘어갔으므로, 함수내부에서 파라미터가 가르키고 있는 주소의 내용을 변경하면, 아규먼트에 영향을 주게 됩니다.

int x = 2;

// call by value
void f(int v);
f(x);   // 값을 인자에 전달

// call by reference
void f(int* v);
f(&x);  // 주소를 인자에 전달

다음 call by value로 swap 함수를 구현할때의 효과를 보여줍니다. 호출시 사용한 아규먼트 a, b는 호출후에도 전혀 영향을 받지 않았습니다.

#include <stdio.h>
  
void swap(int x, int y) {
 
   int temp;
 
   temp = x;
   x = y;   
   y = temp;
   
   return;
}
  
int main () {

   int a = 100;
   int b = 200;
  
   printf("swap전, a : %d\n", a );
   printf("swap전, b : %d\n", b );
  
   // call by value로 함수호출
   swap(a, b);
  
   printf("swap후, a : %d\n", a );
   printf("swap후, b : %d\n", b );
  
   return 0;
}
 
>>> 실행결과
swap전, a : 100
swap전, b : 200
swap후, a : 100
swap후, b : 200

다음 call by reference로 swap 함수를 구현할때의 효과를 보여줍니다. 호출시 사용한 아규먼트 a, b의 주소를 넣고 호출하고, 함수내부에서 주소 위치의 값을 변경하였으므로, 호출후에는 아규먼트가 바뀌게 된다.

#include <stdio.h>
  
void swap(int* x, int* y) {
 
   int temp;
 
   temp = *x; 
   *x = *y;   
   *y = temp; 
   
   return;
}
  
int main () {

   int a = 100;
   int b = 200;
  
   printf("swap전, a : %d\n", a );
   printf("swap전, b : %d\n", b );
  
   // call by reference로 함수 호출
   swap(&a, &b);
  
   printf("swap후, a : %d\n", a );
   printf("swap후, b : %d\n", b );
  
   return 0;
}
 
>>> 실행결과
swap전, a : 100
swap전, b : 200
swap후, a : 200
swap후, b : 100

main 함수 - command line 인자

// main함수의 원형
int main(int argc, char* argv[]) {

    // argc: 커맨드라인에서 받은 인자의 개수
    // argv: 인자의 값
    // argv[0] -> 프로그램 이름
    // argv[1] -> 실제 프로그램의 첫번째 인자

    printf("argc: %d\n", argc);
    for (int i=0; i<argc; i++)
        printf("argv[%d]: %s\n", i, argv[i]);
	
   return 0;  // 정수값을 반환
}

# 컴파일해서 실행파일명을 test.out으로 했을 경우
$ ./test.out 123 456
argc: 1
argv[0]: ./test.out
argv[1]: 123
argv[2]: 456

  1. 조건문

if문

// if
if( a > 20 ) {
     ...
}

// if - else
if (a > 20) {
     ...
} else {
     ...
}

// if - else if - else 
if (a > 20) {
    ...
} else if (a > 10) {
    ...
} else {
    ...
}

switch-case문

switch (a) {
 
   case 10:  // a가 10일때
      ...
      break; // 만약 break를 안쓰면 실행이후에도 다음을 실행
     
    case 20: // a가 20일때
       ...
       break; // 만약 break를 안쓰면 실행이후에도 다음을 실행
     
   default: // optional, 그외의 경우
     statement(s);
}
  1. 반복문

while 루프

int i = 1;
while (i <= 10) {
    ...
    i++;
}

for 루프

for (int i = 1; i <= 10; i++) {
    ...
}

do-while 루프

int i = 1;
do {
    ...
		i++;
} while (i <= 10);

break

int i = 0;
while (1) {
    if (x == 10) // 조건을 만족하면 루프를 탈출
        break;
    ...
    i++;
}

continue

for (int i = 1; i <= 10; i++) {
    if (i % 2 == 0)  // 조건에 만족하면, 다음스텝 실행
        continue;
    ...
}

goto

LOOP: do {
    if(a == 15) {   // 조건에 만족한다면
       a = a + 1;
       goto LOOP;  // LOOP레이블이 있는 위치로 점프
    }
		...

    a++;
} while( a < 20 );

무한반복(infinite)

for( ; ; ) {
      printf("This loop will run forever.\n");
}

  1. Scope (스코프 - visibility / life-time)

지역변수 (local variables)

함수내에서 정의된 변수를 지역변수라고 부릅니다. 지역변수들은 정의된 함수의 내부에서만 보여지고(visibility), 함수 외부에는 보여질수 없습니다. 또한 함수가 종료되면, 지역변수를 위해서 할당된 메모리 공간도 사라지게 됩니다. 만약 지역변수를 static으로 선언하게 되면, 함수가 종료되더라도 메모리 공간은 그대로 유지되게 됩니다.

전역변수 (global variables)

전역변수는 정의된 파일의 전체 구간에서 접근할수 있는 변수입니다. 기본적으로는 정의된 파일에서만 접근이 가능하며, 다른 파일에서 해당 전역변수를 extern으로 선언하면, 다른파일에서도 해당 전역변수를 접근할 수 있습니다.

파라미터의 스코프

함수의 선언에 사용되는 인자(argument)와 실제 호출시에 전달되는 인자(parameter)중에서 파라미터는 함수의 호출시 값이 복사되며, 복사된 값은 함수실행중에만 함수 내부에서만 보여지고, 함수종료시 해당 메모리는 사라집니다.

지역변수, 전역변수, 파라미터, 상수 - 메모리 구조

C프로그램이 메모리에 적재(load)되면, 위의 그림처럼 메모리가 구성됩니다. 그림에서 위쪽으로 갈수록 주소값이 커집니다.

  • 스택 (stack)
  • 힙 (heap)
  • unintialized data(.bss)
  • initialized data(.data)
  • code segment(.text 또는 .css)

  • 지역변수와 파라미터는 메모리에서 스택 영역에 위치합니다. 스택영역은 일시적인 메모리로 사용됩니다. 함수 호출시 할당되었다가, 함수호출이 종료되면 사라집니다.
  • 초기화된 전역변수와 static 변수는 initialized data 영역에 위치합니다.
  • 초기화 되지 않은 전역변수와 static 변수는 unintialized data 영역에 위치합니다.
  • const(read only)로 선언된 변수들은 code segment영역에 위치합니다.
  • 코드의 내용은 code segment에 위치합니다.

더 자세한 내용은 메모리구조, 데이터블럭을 구글링 해봅시다. 다음은 변수 사용 사례별, 메모리 위치를 커맨트로 설명하고 있습니다.

#include <stdio.h>
#include <stdlib.h>

int x;                   // x: uninitialized data
int y = 0;               // y: initialized data
const int z = 1;         // z: code segment

void func(int v)         // v: stack
{
    int a = 10;          // stack
    static int b = 20;   // intialized data
    ...
}

void main()
{
    static int i;        // uninitialized data
    const int k = 1;     // code segment

    func(20);

    char* hp = malloc(10);   // hp: heap
    printf("heap 메모리주소 : %p\n",hp);
    free(hp);
    hp = NULL;

    ...
}

  1. 메모리 관리

malloc - 동적으로 메모리 할당하기

프로그램에서 배열을 정의할때, 배열의 정확한 크기를 모를때, 포인터와 메모리할당(malloc)함수를 이용해서 원하는 시점에 필요한 만큼의 배열을 생성할수 있습니다. malloc 대신에 calloc으로도 메모리를 할당할수 있습니다. (자세한 것은 calloc으로 구글링 해보세요)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {

   char name[100];
   char *name2;   // char배열을 가르킬수 있는 포인터 변수만 선언
                  // 배열을 크기는 아직 모름

   // 200개의 문자를 담을수 있는 배열을 동적으로 만듦(할당)
   // 만약 메모리를 동적으로 할당할수 없었다면, malloc이 NULL을 반환
   name2 = (char*) malloc( 200 * sizeof(char) );  
	
   if( name2 == NULL ) {
      printf("에러 - 요청된 메모리가 할당되지 않았습니다\n");
   } else {
      strcpy(name2, "좀 엄청긴 200자 정도의 이름을 여기에...");
   }
   
   printf("name: %s\n", name);
   printf("name2: %s\n", name2);
}

realloc / free - 메모리 크기재조정과 해제

// 기존에 할당된 배열의 크기를 재조정합니다
name2 = realloc( name2, 300 * sizeof(char) );

...

// malloc으로 할당한 메모리가, 더이상 필요없어질때 메모리에서 소거(해제)합니다 
free(name2);

  1. 입력과 출력

표준입출력 파일

C프로그래밍에서 다음 장치(키보드,화면출력,에러등)는 파일로 취급됩니다. 이들 파일(장치)는 프로그램이 실행될때, 자동으로 파일이 열려있고, 다음의 파일포인터를 이용해서 접근할수 있습니다.

Standard FileFile PointerDevice
Standard inputstdinKeyboard
Standard outputstdoutScreen
Standard errorstderrYour screen

getchar(), putchar(), gets(), puts() 함수

int getchar(void)       // 키보드(stdin)으로부터 문자한개를 입력 받습니다
char* gets(char* s)     // 인자로 받은 s포인터가 가르키는 메모리에, 키보드(stdin)으로부터 문자열을 입력 받습니다

int putchar(int c)      // c 변수에 담긴 문자를 화면(stdout)에 출력합니다
int pus(const char* s)  // s포인터가 가르키는 메모리에 담긴 문자열을, 화면(stdout)에 출력합니다

#include <stdio.h>
int main( ) {

   char str[100];

   printf( "문자열을 입력하세요 :");
   gets( str );

   printf( "\n입력된 문자열: ");
   puts( str );

   return 0;
}

scanf(), printf() 함수

int scanf(const char *format, ...)    // stdin으로부터 format의 형태로 입력을 받습니다
int printf(const char *format, ...)   // stdout에 format의 형태로 출력합니다

#include <stdio.h>
int main( ) {

   char str[100];
   int i;

   printf( "값을 입력하세요 :");

   scanf("%s %d", str, &i);  // str과 i를 stdin에서 입력받기

   printf( "\nYou entered: %s %d ", str, i);  // str, i를 stdout에 출력하기

   return 0;
}

  1. 파일 입출력

파일 입출력 함수

// 파일열기
FILE *fopen( const char * filename, const char * mode );

// 파일닫기
int fclose( FILE *fp );

// 파일에 문자한개 쓰기
int fputc( int c, FILE *fp );

// 파일로부터 문자하개 읽어오기
int fgetc( FILE * fp );

// 파일로부터 문자열 읽어오기
char *fgets( char *buf, int n, FILE *fp );
int fscanf(FILE *fp, const char *format, ...)

// 파일에 문자열 쓰기
int fputs( const char *s, FILE *fp );
int fprintf(FILE *fp,const char *format, ...);

// 파일에 데이터 쓰기
#include <stdio.h>

main() {
   FILE *fp;

   // 파일을 쓰기모드로 열기 /tmp/test.txt
   fp = fopen("/tmp/test.txt", "w+");

   // 파일에 문자열을 출력  
   fprintf(fp, "This is testing for fprintf...\n");
   fputs("This is testing for fputs...\n", fp);

   // 파일닫기
   fclose(fp);
}

// 파일에서 데이터 읽어오기
#include <stdio.h>

main() {

   FILE *fp;
   char buff[255];

   fp = fopen("/tmp/test.txt", "r");
   fscanf(fp, "%s", buff);
   printf("1 : %s\n", buff );

   fgets(buff, 255, (FILE*)fp);
   printf("2: %s\n", buff );
   
   fgets(buff, 255, (FILE*)fp);
   printf("3: %s\n", buff );
   fclose(fp);
}

바이너리 파일입출력 함수

// 바이너리 데이터를 파일로부터 읽어오기
size_t fread(void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);

// 바이너리 데이터를 파일로 쓰기              
size_t fwrite(const void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);

파일모드

모드접근 방식
"r" / "rb"Read existing text/binary file.
"w" / "wb"Write new/over existing text/binary file.
"a" / "ab"Write new/append to existing text/binary file.
"r+" / "r+b" / "rb+"Read and write existing text/binary file.
"w+" / "w+b" / "wb+"Read and write new/over existing text/binary file.
"a+" / "a+b" / "ab+"Read and write new/append to existing text/binary file.

랜덤 접근 (random access)

long ftell(fptr);    // 파일에서 position을 반환

fseek(fptr, offset, origin);  // 파일의 origin 위치에서 offset만큼 위치로 이동 

fseek(fptr, -10L, SET_END);   // 파일의 끝에서 10번째 이전 위치로 position 이동

origin: 
- SEEK_SET 파일의 시작
- SEEK_CUR 파일에서 현재 위치
- SEEK_END 파일의 끝

  1. 전처리기 (Preprocessor)

전처리란

C 전처리는 컴파일러의 부분은 아닙니다. 컴파일 과정중의 하나입니다. 컴파일러가 컴파일하기전에 단순한 텍스트를 교체 과정으로 생각하면 됩니다.

전처리 명령어 (directives, macro)

전처리 명령어(directive, macro)는 어떻게 전처리를 수행할지 전처리기에 설명합니다.

#define MAX_VAL 20    // MAX_VAL이라는 상수값을 정의합니다

//--------------------------
// 현재 파일에 명시된 파일을 포함시킵니다
#include <stdio.h>
#include "myheader.h"

//--------------------------
#define FILE_SIZE 30
#undef  FILE_SIZE        // 기존에 정의된 상수값 정의를 해제합니다. 이후에 다시 정의가능합니다
#define FILE_SIZE 42

//--------------------------
// myheader.h 파일의 시작
#ifndef MYHEADER
   #DEFINE MYHEADER
   ...
#endif
// myheader.h 파일의 끝
//--------------------------

// test.c
#include "myheader.h".  // 여기에서만 한번 myheader.h의 내용이 포함됩니다
...
#include "myheader.h"   // MYHEADER가 정의되어 있으므로, 다시 포함되지 않습니다

//--------------------------
#ifdef DEBUG
   // 만약 DEBUG가 정의되어 있으면, 이 부분을 포함
#endif

//--------------------------
// 여러문장으로 매크로가 이어질때 \ 를 사용
#define  message_for(a, b)  \
   printf(#a " and " #b ": We love you!\n")

//--------------------------
// square(10) ->  ((10)*(10)) 으로 교체
#define square(x) ((x) * (x))

  1. 헤더파일

헤더파일은

  • 헤더파일은 .h 확장자를 가집니다.
  • #include 파일명.h 로 기술하면, 전처리기가 해당 파일을 컴파일전에 포함한후에 컴파일합니다.
  • 헤더 파일에는 보통 constant, macro, 시스템 수준의 전역변수 선언, 함수의 선언을 포함합니다.
  • 구현(정의) 파일들이 다른 파일과 공유하거나 반복적으로 사용되는 상수를 헤더에 주로 위치시킵니다.

header파일 한번만 include하기

여러 파일에서 #include가 되면, 같은 내용들이 중복으로 포함될수 있습니다. 이를 방지하기 위해서 다음과 같은 방식으로 한번만 포함되도록 할수 있습니다.

#ifndef MYHEADER_H
   #DEFINE MYHEADER_H
   ...
#endif
#include "myheader.h".  // 여기에서만 한번 myheader.h의 내용이 포함됩니다
                        // 이때 MYHEADER_H 를 define합니다
...
#include "myheader.h"   // MYHEADER_H가 define 되어 있으므로, 다시 포함되지 않습니다

computed include

여러개의 조건에 맞게 헤더파일들이 포함될수 있도록 할수 있습니다.

#if ANDROID_13
  #include "support_lib_13.h"
#elif ANDROID_12
  #include "support_lib_12.h"
#elif ANDROID_11
  ...
#endif

  1. 에러 처리

errono, perror(), strerror()

errno                             // 현재 발생한 에러 번호
char * strerror ( int errno );    // 에러 번호를 설명하는 에러 문자열을 반환하는 함수
void perror(const char *string);  // 오류 메세지를 stderr로 출력합니다

#include <stdio.h>
#include <errno.h>
#include <string.h>

extern int errno ;

int main () {

   FILE * pf;
   int errnum;
   pf = fopen ("unexist.txt", "rb");  // 일부러 잘못된 경로의 파일을 열기
	
   if (pf == NULL) {
   
      errnum = errno;  // errno: 마지막 발생한 에러번호 
      fprintf(stderr, "Value of errno: %d\n", errno);
      perror("Error printed by perror");
      fprintf(stderr, "Error opening file: %s\n", strerror( errnum ));
   } else {
   
      fclose (pf);
   }
   
   return 0;
}

>>> 실행결과
Value of errno: 2
Error printed by perror: No such file or directory
Error opening file: No such file or directory

프로그램 종료 상태

프로그램이 오류없이 완료될 경우에는, 프로그램은 EXIT_SUCCESS 값으로 반환하는 것이 일반적입니다. 여기서 EXIT_SUCCESS는 값이 0으로 정의된 매크로입니다.

만약 프로그램에 오류 조건이 있고 종료되는 경우 -1로 정의된 상태 EXIT_FAILURE로 종료해야 합니다.

다음은 위의 상태값을 반환하는 소스코드입니다.

#include <stdio.h>
#include <stdlib.h>

main() {

   int dividend = 20;
   int divisor = 5;
   int quotient;
 
   if( divisor == 0) {
      fprintf(stderr, "0으로 나눔! 종료중...\n");
      exit(EXIT_FAILURE);  // -1 을 반환
   }
	
   quotient = dividend / divisor;
   fprintf(stderr, "몫 : %d\n", quotient );

   exit(EXIT_SUCCESS);  // 0 을 반환
}

  1. 가변인자

가변인자는

함수에 인자의 개수가 가변적으로 변하는 경우에 대해서, 가변적 부분은 …로 선언합니다. 그리고, va_list, var_start, va_arg, va_end를 통해서 입력받은 인자를 접근할수 있습니다.

예제

#include <stdio.h>
#include <stdarg.h>

double average(int num,...) { // ... 인자가 가변적으로 들어옴

   va_list valist;   // 가변인자를 담는 리스트 
   double sum = 0.0;
   int i;

   va_start(valist, num);  // 가변인자 바로 앞의 고정인자로부터 인자 목록을 초기화

   for (i = 0; i < num; i++) {
      arg = va_arg(valist, int);  // 차례대로 인자를 읽어오기
      sum += arg;
   }
	
   // 가변인자를 모두 가져온후 메모리 클리어
   va_end(valist);

   return sum/num;
}

int main() {
   printf("AVE: 2, 3, 4, 5 = %f\n", average(4, 2, 3, 4, 5));
   printf("AVE: 5, 10, 15 = %f\n",  average(3, 5, 10, 15));
}


0개의 댓글