자료형

자료형은 데이터를 표현하는 기준(데이터를 표현하는 방법)이다. 때문에 변수도 상수도 자료형에 근거한다.

  • 변수는 값이 메모리 공간에 저장 및 참조되는 방식에 따라서 정수형과 실수형으로 나뉜다.

  • C표준에서는 자료형 별 크기를 정확히 제한하고 있지 않다. 즉, 컴파일러마다 차이가 있으니까 조심해야 한다. 기본 자료형 종류와 표현범위

  • 컴퓨터는 2진수를 기반으로 데이터를 표현하고 연산도 진행한다.

정수형

  1. 가장 왼쪽의 비트(MSB)는 부호비트로 사용
  2. 음의 정수를 표현할 때에는 2의 보수를 취한다.
    1. 1의 보수를 취한다.(0과 1 반전)
    2. 1을 더한다.
  3. +n에다가 2의 보수를 취하면 -n이 되고, -n에 다가 2의 보수를 취하면 +n이 된다. 즉, 양수일때만 2의 보수를 취하는게 아님
  4. 일반적으로 CPU가 처리하기에 가장 적합한 크기의 정수 자료형을 int로 정의한다. 따라서 int형 연산의 속도가 다른 자료형의 연산속도에 비해서 동일하거나 더 빠르다.
  5. 데이터의 양이 많아서 연산속도보다 데이터의 크기를 줄이는 것이 더 중요한 경우 short와 같은 자료형을 활용한다.
  6. 정수 자료형에만 unsigned를 붙일 수 있다.
  7. 보통은 signed가 붙으나 안 붙으나 같은 의미이지만 컴파일러에 따라 signed char != char일 수 있다.
  8. char은 정수형이다. 즉, 문자도 결국 정수를 저장한 것이다. ex. char ch = 'A';은 컴파일러에 의해 char ch = 65;로 바뀐다.

실수형

  1. 컴퓨터가 실수를 표현하는 방식에는 넓은 범위의 실수를 표현할 수 있지만, 오차가 존재한다.(=부동 소수점 오차) 그래서 아래와 같이 근사치를 계속 더하다 보면 오차가 생기게 되므로 주의해야 한다.
#include <float.h> // FLT_EPSILON : 부동 소수점에서 발생할 수 있는 가장 큰 오차
#include <math.h> // fabsf()

int main() {
    float num = 0.0f;

    for (int i = 0; i < 100; i++) {
        num += 0.01f;
    }

    if (fabsf(num - 1.0f) <= FLT_EPSILON) { 
        // num == 1.0f : 이 정도 오차는 없는 것으로 계산한다. 
    } 
    else { 
        // num != 1.0f : 오차 발생!
    } 

    return 0;
}
  1. 실수 자료형에서는 보편적으로 double을 선택한다. 즉, 특별한 선언이 없다면 소수점을 포함한 소수는 double 자료형으로 인식된다.

리터럴 상수

이름이 없고 변경이 불가능한 데이터로, 리터럴 상수도 자료형이 존재한다.

  1. int inum = 5; 5int형으로 메모리 공간에 저장하기로 되어 있다.
  2. double dnum = 7.15; 7.15double형으로 메모리 공간에 저장하기로 약속되어 있다.
    ex. float num1 = 5.123; double형 상수를 float 변수에 넣는 것이기 때문에 자동 형변환이 발생하여 데이터 손실 경고가 뜬다.
  3. 0xA : 16진수 ‘A’(=10) / 012 : 8진수 ‘12’(=10)
  4. 상수의 표현을 위한 접미사 상수의 표현을 위한 접미사
  5. char* str ="Text";처럼 문자열은 포인터 상수(즉 주소값을 반환)로 표현된다.

자동 형변환의 종류

  1. 대입연산의 전달과정에서 발생

    1. double num1= 245; 데이터 손실은 없으나 부동소수점 오차 발생
    2. int num2 = 3.14; 소수점 이하 값 손실
    3. int num3=129; char ch = num3; 상위 바이트 단순 소멸. 부호가 바뀔수 있음
  2. 정수의 승격 : 연산을 빠르게 하기 위해서 int보다 작은 크기의 정수형 데이터를 int형 데이터로 형 변환이 되어서 연산이 진행됨

    1. short num1 = 15, num2 = 25; short num3 = num1 + num2; num1과 num2가 int형으로 형 변환
  3. 피연산자의 자료형 불일치

    1. double num = 5.15 + 19; 19가 19.0으로 형변환
    2. int -> long -> long long -> float -> double -> long double
    3. char, short의 경우 정수의 승격에 의해서 int로 변환되기 때문에 위 규칙에서 없음

특수문자

특수문자

키워드

아래의 경우, 변수나 함수의 이름으로 쓸 수 없다.

  1. auto
  2. _Bool
  3. break
  4. case
  5. char
  6. _Complex
  7. const
  8. continue
  9. default
  10. do
  11. double
  12. else
  13. enum
  14. extern
  15. float
  16. for
  17. goto
  18. if
  19. _Imaginary
  20. return
  21. restrict
  22. short
  23. signed
  24. sizeof : 이건 함수가 아닌 연산자이다.
  25. static
  26. struct
  27. switch
  28. typedef
  29. union
  30. unsigned
  31. void
  32. volatile
  33. while

연산자

연산자 우선순위 및 결합방향

  • 결합 방향이란, 우선순위가 동일한 두 연산자가 하나의 수식에 존재하는 경우, 어떠한 순서대로 연산하느냐를 결정해 놓은 것
  • 결합 방향이 왼쪽인 경우는 연산자를 피연산자의 왼쪽에 놓는 경우와 조건연산, 대입연산이다. ex. sizeof str, &num, …
  1. ++num / --num : 값을 1 증가/감소 후, 속한 문장의 나머지를 진행(선 증가/감소, 후 연산)
  2. num++ / num-- : 속한 문장 전체를 먼저 진행한 후, 값을 1 증가/감소(선 연산, 후 증가/감소)
  3. 소괄호도 연산자이다. 즉, 2번 연산자의 경우 소괄호와 상관없이 다음 문장으로 넘어가야만 비로소 값의 증가 및 감소가 이뤄진다.
  4. 곱셈과 나눗셈이 비트의 이동부다 부담스러운 연산이다.

변수의 종류

  1. 지역변수 : 중괄호 내에 선언된 모든 변수(반복문, 조건문, 함수의 매개변수 등)
    • 중괄호를 나오면 다 해제
    • 초기화 하지 않으면 쓰레기값
    • 같은 중괄호 내에서만 접근 가능
  2. 전역변수 : 어떤 중괄호에도 포함되지 않음
    • 프로그램 시작과 동시에 메모리 공간(Data 영역)에 할당되어 종료시까지 존재
    • 별도의 값 초기화하지 않으면 0으로 초기화
    • 프로그램 전체 영역 어디서든 접근 가능
  3. static 지역변수 : 선언된 함수에서만 접근 가능한 전역변수
    • 선언된 함수 내에서만 접근 가능
    • 별도의 값 초기화하지 않으면 0으로 초기화
    • 딱 1회 초기화되고 프로그램 종료 시까지 메모리 공간(Data 영역)에 존재
  4. static 전역변수 : 선언된 파일 내에서만 접근 가능한 전역변수(다른 외부 파일에서 접근을 허용하지 않는다.)
    • static void increment() {} : 함수를 static 선언을 하면 선언된 파일 내에서만 접근이 가능하다. 이로서 코드 안전성을 부여한다.
  5. register 변수 : CPU 내 레지스터에 저장될 확률이 높은 변수
    • register에 할당될지는 컴파일러가 결정
    • 지역변수에만 적용하는 것이 유의미(전역변수로 죽치고 있으면 큰 손해이므로)

Miscellaneous

General

  1. int main(int argc, char* argv[]) { return 0; }

    1. argc는 자기 자신을 포함한 매개변수의 개수이다.
    2. argv는 자기 자신을 포함해서 매개변수를 띄워쓰기를 기준으로 받은 문자열 배열이다.
    3. ex. ./helloProgram I Love You : argc == 4, argv == {"./helloProgram", "I", "Love", "You"}
    4. ex. ./byeProgram "Good work." : argc == 2, argv == {"./byeProgram", "Good work."} 이와 같이 큰따옴표로 묶으면 공백을 포함한 하나의 문자열 매개변수로 처리한다.
  2. 스트림 : 프로그램상에서 모니터와 키보드를 대상으로 데이터를 입출력하기 위해서 연결시켜 주는 다리

    1. 운영체제가 제공하는 소프트웨어적인 상태
    2. 한 방향으로 흐르는 데이터의 흐름
    3. 콘솔 입출력을 위한 입력 스트림, 출력 스트림은 프로그램이 실행되면 자동으로 생성되고, 프로그램이 종료되면 자동으로 소멸되는 스트림이다.(stdin, stdout, stderr)
  3. 프로그램 실행시 운영체제에 의해서 마련되는 메모리의 구성

    1. 코드 영역 : 실행할 프로그램의 코드가 저장된다. CPU는 이 영역에 저장된 명령문을 하나씩 가져가서 실행한다.
    2. 데이터 영역 : 전역변수와 static 변수 할당. 프로그램 시작과 동시에 메모리 공간에 할당되어 프로그램 종료 시까지 남아있게 된다.
    3. 스택 영역 : 지역변수, 매개변수 할당. 함수를 빠져나가면 소멸된다.
    4. 힙 영역 : 프로그래머가 원하는 시점에 malloc(), free() 등을 통해 변수를 할당하고 소멸할 수 있도록 지원되는 영역. 프로그래머가 따로 해제해주지 않으면 프로그램 종료시 운영체제에 의해 해제된다.

  1. 프로그램이 종료되면 운영체제에 의해서 할당된 메모리 공간 전체를 반환하는데 이때, 전역변수가 소멸된다.

  2. C 프로그램의 생성 과정

  3. 컴파일러는 파일 단위로 컴파일 진행한다. 즉, 다른 파일의 정보를 참조하여 컴파일을 진행하지 않는다. 그러므로 외부에 함수, 변수 등이 정의되어 있다면 컴파일러에게 알려줘야 한다. 컴파일러에게 알려주는 것은 몇 번이고 중복되도 상관없다.

    1. extern int num; : num 변수가 다른 파일에 전역변수로 정의되어 있음을 알려준다.
    2. extern void increment(); , void increment(); : 함수가 다른 파일에 정의되어 있음을 알려준다. 여기서는 extern이 생략 가능
  4. 헤더파일 선언 : 헤더파일에 있는 변수, 함수, 매크로를 쓰는 소스파일에만, 구조체 정의를 쓰는 소스/헤더파일에만 추가하면 된다.

    1. #include <stdio.h> : 표준 헤더파일이 저장되어 있는 디렉터리에서 파일을 찾는다.
    2. #include "myheader.h" : 이 소스파일이 저장된 디렉터리에서 헤더파일을 찾는다.
    3. #include "Release/header.h" : 소스파일이 있는 디렉터리의 하위폴더 Release에서 헤더파일을 찾는다.
    4. #include "../header.h" : 소스파일이 있는 디렉터리의 상위폴더에서 헤더파일을 찾는다.
  5. 헤더파일에 포함해야 하는 것

    1. 외부에 선언된 변수/함수에 접근/호출하기 위한 선언들 : 소스파일이 2개 이상이면 생길 수 밖에 없음

      // arith.h
      #ifndef __ARITH_H__ // 구조체 정의가 포함되어 있는 "stdiv.h"을 포함함으로 헤더파일 중복삽입이 문제가 될 수 있기 때문에 미연에 방지하는게 좋다. 
      #define __ARITH_H__
      
      #include "stdiv.h"
      extern int num;
      Div add(int, int);
      
      #endif
      
    2. 매크로 : 매크로의 명령문도 파일 단위로만 유효하다.

    3. 구조체의 정의 : 구조체의 정의는 그 구조체를 필요로 하는 모든 파일에 존재해야 한다. 그러나 구조체를 중복 정의하면 컴파일 에러 메시지가 뜨므로, 이를 조건부 컴파일을 이용해서 해결해야 한다. 구조체의 정의는 구조체의 정의만을 포함하거나 그 구조체와 관련된 함수들만이 포함된 파일로 만드는게 좋겠다.

      // stdiv.h
      #ifndef __STDIV_H__ // 이와 같이 조건부 컴파일을 하면 자유롭게 #include "stdiv.h" 해도 괜찮다.
      #define __STDIV_H__
      
      typedef struct {
        int quotient;
        int remainder;
      } Div;
      
      #endif
      

선행처리

컴파일러가 컴파일 하기 이전에 선행처리기가 먼저 처리하여 소스파일로 반환한다. 컴파일러가 아니기 때문에 선행처리 명령문들은 #으로 시작하여 끝에 세미콜론(;)을 붙이지 않는다. 그리고 보통은 매크로의 이름을 대문자로 정의한다. 여러 줄에 걸쳐서 정의할 때는 \문자를 활용하여 줄이 바뀌었음을 알려줘야 한다. 선행처리기가 파일 단위로 선행처리를 하기 때문에 매크로의 명령문도 파일단위로만 유효하다.

  1. #define PI 3.14 : PI라는 문자를 볼 때마다 3.14로 치환한다.

  2. #define PRINT_HELLO puts("HELLO"); : PRINT_HELLO를 볼 때마다 해당 함수로 치환한다.

  3. #define SQUARE(X) ((X) *(X)), `#define DIFF_ABS(x, y) ((x) > (y) ? (x) - (y) : (y) - (x)) : 함수형 매크로의 경우 소괄호를 남발할정도로 많이 써야 한다.

    • 문자열 내에서 매크로의 매개변수를 치환하고 싶으면 매크로 연산자 #을 써야 한다.

      #define STRING_JOB(A, B) #A "의 직업은 " #B "입니다. "
      
    • 필요한 형태대로 단순하게 결합하려면 매크로 연산자 ##을 써야 한다.

      #define CON(UPP, LOW) UPP ## 00 ## LOW
      
      int num = CON(22, 77); // 220077이 대입된다. 
      
  4. #define ADD 처럼 매크로의 몸체를 생략해서 정의해도 된다. 이렇게 정의하면 ADD라는 문구는 다 공백으로 대체가 된다.

  5. #include <stdio.h> : stdio.h 파일의 모든 내용을 여기에다가 그대로 복사한다.

  6. #if A ... #elif B !!! #else ,,, #endif : A가 참이라면 ... 컴파일, B가 참이라면 !!! 컴파일, 둘 다 아니라면 ,,, 컴파일

    #define ADD 1
    
    #if ADD // ADD가 참이면 아래 줄을 컴파일함
    	printf("+");
    #endif
    
  7. #ifdef A ... #else ,,, #endif : A가 정의(#define)되어 있다면 ... 컴파일, 아니면 ,,,컴파일

    • #ifndef A ... #else ,,, #endif : 정의되어 있지 않다면

연속적인 문장

  1. int num1 = 30, num2 = 40; 여러 변수의 선언과 동시에 초기화를 할 때 comma를 쓴다.
  2. int* ptr1 = NULL, * ptr2 = NULL; 여러 개의 포인터변수를 쓸 때는 이렇게 쓴다.
  3. num1 = num2 = 0; 여러 개의 변수에 같은 값을 대입할 때 이렇게 쓴다.
  4. printf("Name: "); scanf("%s", name); 뭘 입력할지 안내하고 입력을 받을 때 이와 같이 한 줄로 입력하면 좋다.

반복문

  1. do ~ whilewhile는 실제로 body가 1회 이상 실행되고, 조건문이 같고, body가 조건문에 영향을 주지 않는다면, 실행횟수는 똑같다.
  2. 즉, 반복조건의 검사위치가 달라서 do ~ while은 최소 1회 이상 실행한다는 점이 while과의 유일한 차이점
  3. 반복문의 반복횟수가 정해져 있다면, for를 쓰는것이 유리하다.
  4. break;는 가장 가까이서 감싸고 있는 반복문 하나를 빠져나오는 것이다.

조건문

  1. switch(n) ~ case문에서 n은 정수형 변수이므로 char형도 포함된다.
  2. 두 수 중 큰 수 혹은 작은 수를 계산하는 것은 ? :을 잘 활용하자.

함수

  1. 함수의 선언에서는 int increment(int); 처럼 매개변수의 이름을 생략할 수 있다.
  2. 함수가 호출되면 해당 함수의 복사본을 만들어서 실행한다고 생각해야함
  3. 함수가 호출될 때 메모리 공간 내 stack영역에서 새로 공간이 할당되어서 해당 지역변수들이 생성된다.
  4. 인자 전달의 기본방식은 **값의 복사(call-by-value)**이다. 즉, 복사가 되는 것 뿐이기 때문에, 함수가 호출되고 나면, 전달되는 인자와 매개변수는 별개가 된다.
    • call-by-reference : 그냥 단순하게 주소 값을 매개변수로 전달하는 경우
  5. 매개변수로 배열을 선언할 수 없다. 매개변수가 넘겨질 때 그만큼 새로 메모리공간을 할당하는데, 배열의 사이즈가 크면 stack을 초과할 수 있기 때문이다.
  6. 매개변수로 1차원 배열을 넘길 때에는 다음과 같이 배열의 시작주소값을 넘겨야 한다.
    1. void showArrayElem(int * param, int len);
    2. void shoWArrayElem(int param[], int len);
    3. 매개변수에서만 int param[]int* param은 완전히 동일한 선언이다.
    4. 주소값만을 넘겨서는 그 배열의 사이즈를 알 수 없으므로 항상 배열 사이즈와 함께 넘겨야 한다.
    5. sizeof(param)param이 포인터 변수이므로 단순히 포인터 변수의 크기인 8을 반환한다.
    6. 배열의 크기는 보통 sizeof(param) / sizeof(param[0])으로 넘기면 된다.
  7. 매개변수로 2차원 배열을 넘길 때에는 다음과 같이 배열 포인터와 행 사이즈를 넘겨야 한다.
    1. void show2DArray(int (*arr)[4], int column);
    2. void show2DArray(int arr[][4], int column);
    3. 매개변수에서만 int (*arr)[4]int arr[][4]은 완전히 동일한 선언이다.
    4. 배열 포인터만을 넘겨서는 그 2차원배열의 행 개수를 알 수 없으므로 항상 행 사이즈와 함께 넘겨야 한다.
    5. sizeof(arr)arr이 포인터 변수이므로 단순히 포인터 변수의 크기인 8을 반환한다.
    6. 배열의 크기는 보통 sizeof(arr) / sizeof(arr[0])으로 넘기면 된다.(배열의 전체 크기 / 한 행의 크기)
void show2DArray(int (*arr)[4], int column) {
  for (int i = 0; i < column; i++) {
    for (int j = 0; j < 4; j++) {
      printf("%d ", arr[i][j]);
    }
    puts("");
  }
}
  1. 함수에서 지역적으로 선언된 변수의 주소값을 반환하면 안된다. 함수가 반환되면 거기에 속해 있는 모든 지역변수들이 해제되기 때문이다. 이를 해결하려면 동적할당을 통해 힙의 영역에서 변수를 선언하고 이를 반환해야 한다.
  2. 함수에서 반환할 때에도 리턴값을 복사해서 반환한다.

구조체(struct)

  1. 구조체 선언 방식
// 구조체 정의 방식 #1
struct Point {
   int xpos;
   int ypos;
};

struct Point x;

// 구조체 정의 방식 #2 - 1
struct point {
   // ...
};
typedef struct point Point;

// 구조체 정의 방식 #2 - 2
typedef struct point {
   // ...
} Point;

struct Point x; // 이렇게 둘 다 사용 가능
Point y;

// 구조체 정의 방식 #3
typedef struct {
   // ...
} Point;

Point x; // struct 키워드로 선언 불가능
  1. 구조체 변수의 주소값은 구조체 변수의 첫 번째 멤버의 주소 값과 동일
  2. typedef 키워드로 재정의할 경우 구조체 변수의 이름을 보통 대문자로 설정
  3. sizeof(struct)를 계산하면 struct의 모든 멤버의 sizeof 결과를 더한것과 같다.

공용체(union)

typedef struct {
   unsigned short upper;
   unsigned short lower;
} DBShort;

typedef union {
   int iBuf;
   char bBuf[4];
   DBShort sBuf;
} RDBuf;
  1. 위 사례처럼 union은 같은 메모리를 다양하게 해석하고, 접근할 수 있다.
    1. 4bytes 메모리 공간을 rdBuf.iBuf로 접근하면 하나의 int 정수로 접근
    2. rdBuf.bBuf로 접근하면 크기가 4인 문자 배열로 접근
    3. rdBuf.sBuf로 접근하면 상위 2bytes, 하위 2bytes로 short 2개로 접근할 수 있다.

열거형(enum)

변수에 저장이 가능한 정수 값들을 나열한것으로 정수로 인식됨

enum syllable {
   Do=1, Re=3, Mi, Fa=8, So // 1, 3, 4, 8, 9
}

enum { // 자료형의 이름을 생략한 형태로 정의
   Do, Re, Mi, Fa, So // 0, 1, 2, 3, 4
}

문자열

  1. \0'은 ascii값이 0으로 이를 문자의 형태로 출력할 경우, 아무런 출력이 발생하지 않는다.(공백(' ')이 출력되는 것과 다름)
  2. 문자열 != 문자배열 : 문자열을 구분할 때 마지막에 항상 \0이 있어야 한다. 그래야만 정상적으로 문자열이 출력된다.
    • str[strlen(str) - 1] = 0 : str 문자열의 마지막에 null문자 대입, '\0'0을 넣는 것은 같은 의미(ascii 값이 같으므로)
    • null 문자가 없으면 단순한 문자 배열이다.
  3. C언어에서는 개행을 \n으로 표시하기로 약속한다. 이는 C언어에서만 해당한다.
    1. MS-DOS(Windows) : \r\n
    2. Mac OS : \r
    3. Unix 계열: \n
  4. 문자열이 파일에 저장될 때에는 문자열의 끝을 의미하는 널 문자는 저장되지 않는다. 때문에 파일에서는 개행을 기준으로 문자열을 구분한다.
  5. 문자열을 나란히 선언하면 하나의 문자열로 간주된다. 즉 "ABC" "DEF""ABCDEF"와 같다.

배열

1차원 배열

  1. int arr[3] = {1 }; : 나머지 arr[1], arr[2]는 0으로 초기화
  2. int arr[] = {4, 5, 6}; : arr의 사이즈는 3으로 자동 결정
  3. M 사이즈인 1차원 배열 동적할당: int * ptr = (int*)malloc(sizeof(int) * M);

2차원 배열

  1. int arr2d[2][4] = {1, 2, 3, 4, 5, 6, 7, 8}; : 1차원 배열처럼 초기화 가능
  2. int arr2d[][4] = {{1, 2, 3}, {5, 6}}; : arr[0][3], arr[1][2], arr[1][3]은 0으로 초기화
  3. int arr2d[][4] = {1, 2, 3, 4, 5, 6, 7, 8}; : 세로의 길이만 생략 가능
  4. 위 2차원 배열의 이름인 arr2d이중 포인터가 아니라 배열 포인터다!
    • arr2d&arr2d[0][0]을 뜻하면서도 배열 전체를 의미하고, int (*ptr1)[4]의 타입이다.
    • arr2d[i]&arr2d[0][0]을 뜻하면서도 배열 i번째 행 전체를 의미하고, int* ptr2의 타입이다.
    • sizeof(arr2d)는 2 * 4 * sizeof(int)를 반환한다.
    • sizeof(arr2d[i])는 4 * sizeof(int)를 반환한다.
    • 고로, arr2d != arr2d[i]
  5. int (*ptr1)[4] = matrix;에서 배열포인터 변수 ptr1은 int형 변수를 가리키면서 포인터 연산시 sizeof(int) * 4의 크기 단위로 값이 증가 및 감소한다.
  6. 배열 포인터 != 포인터 배열
    1. int* a[4]; : 포인터 배열 = int형 포인터 4개를 담고 있는 배열
    2. int (*b)[4]; : 배열 포인터 = int변수를 가리키고 포인터 연산시 sizeof(int) * 4만큼 증감하는 포인터
  7. MxN 사이즈인 2차원 배열(행 = M, 열 = N) 동적할당 및 해제 :
int** arr2d = (int**)malloc(sizeof(int*) * M);
for (int i = 0; i < M; i++) {
  arr2d[i] = (int*)malloc(sizeof(int) * N);
}

// 해제 
for (int i = 0; i < M; i++) {
  free(arr2d[i]);
}
free(arr2d);

Pointer

포인터 변수 vs 포인터 상수

포인터 변수

메모리의 주소 값을 저장하기 위한 변수

  1. 64bit OS, 64bit으로 컴파일했을 때 sizeof(ptr) == 8로 계산된다.
  2. &연산자는 변수만이 피연산자가 될 수 있다.
  3. 포인터의 형(type)은 메모리 공간을 참조하는 기준이 되어서 *연산할 때 메모리 공간의 접근 기준이 된다.
    • 즉, *pnumpnum의 포인터 자료형에 따라서 해당하는 주소에서 몇 바이트를 읽을지, 정수/실수형으로 해석할지 판단한다.
  4. const int* ptr;은 포인터 변수 ptr을 이용해서 ptr이 가리키는 변수에 저장된 값을 변경하는 것을 허용하지 않겠다는 뜻
  5. int* const ptr;은 포인터 변수 ptr에 저장된 주소값을 변경하는 것을 허용하지 않겠다는 뜻
    • const int인지 const ptr인지로 구분하면 좋겠다.
  6. void*는 형(type)이 존재하지 않는 포인터다.
    1. 함수포인터, 배열포인터 등등 무엇이든 그것의 주소만을 담을 수 있는 포인터
    2. 포인터 연산(값의 변경 및 참조 등)을 하려면 캐스팅한 후 진행해야 한다.

포인터 상수(상수 형태의 포인터)

메모리의 주소 값이지만 그 주소값을 변경할 수 없는 상수

  1. 배열의 이름은 배열의 시작 주소값을 의미하며, 그 형태는 값의 저장이 불가능한 상수이다.
  2. 배열의 이름과 포인터 변수는 변수냐 상수냐의 특성적 차이만 있을 뿐, 둘 다 포인터이기 때문에 포인터 변수로 할 수 있는 연산은 배열의 이름으로도 할 수 있고, 배열의 이름으로 할 수 있는 연산은 포인터 변수로도 할 수 있다.
    • 즉, int* ptr; 이면 ptr[2]은 int 배열에서 3번째 원소를 가리킨다.
int arr[3] = {15, 25, 34};
int* ptr = &arr[0]; // int* ptr = arr;

printf("%d %d\n", arr[0], ptr[0]); // 15 15
printf("%d %d\n", arr[1], ptr[1]); // 25 25
printf("%d %d\n", arr[2], ptr[2]); // 34 34
printf("%d %d\n", *arr, *ptr); // 15 15
  1. 문자열을 두 가지 형태로 선언할 수 있다.
    1. char str1[] = "My String"; : str1은 계속 문자열이 저장된 위치를 가리켜야 한다.
    2. char * str2 = "My String"; : str2는 다른 문자열을 가리킬 수 있다.
  2. 함수의 이름은 함수가 저장된 메모리공간의 주소값을 의미한다.
    • int (*fptr) (int); 매개변수가 int 하나 있고, 반환형이 int인 함수 포인터
    • void (*fptr2) (char*, int); 매개변수가 char*, int이고, 반환형이 없는 함수포인터

포인터 연산

  • int형 포인터를 대상으로 n의 크기만큼 값을 증가 및 감소 시, n * sizeof(int) 의 크기만큼 주소 값이 증가 및 감소
  • double형 포인터를 대상으로 n의 크기만큼 값을 증가 및 감소 시, n * sizeof(double) 의 크기만큼 주소 값이 증가 및 감소
  • 1차원배열 arr에서 arr[i] == *(arr + i), &arr[i] == arr + i
  • 2차원배열 arr2d에서 *(arr2d[i] + j) == (*(arr+i))[j] == *(*(arr+i)+j) == arr[i][j]
  • sizeof(ptr)은 포인터 변수 ptr의 크기인 8을 반환하지만, sizeof(arr)와 같이 포인터 상수는 배열 arr의 크기를 반환한다. 즉, sizeof를 갖고 배열의 크기를 반환하고 싶으면 포인터 상수를 피연산자로 넣어야 한다.

포인터 배열

포인터 변수로 이루어진 배열

  1. char* str[3];은 문자열을 3개 저장할 수 있는 char형 포인터 배열이다.

이중 포인터(더블 포인터)

포인터를 가리키는 포인터 변수

  1. int* arr[3];에서 arr은 포인터 배열의 첫주소를 가리키므로 int**이다.

자주 쓰는 함수 정리

<stdio.h>

서식지정 입출력 : printf() vs fprintf() vs sprintf() vs scanf() vs fscanf() vs sscanf()

함수 콘솔 파일 문자열 호출 성공시 호출 실패시 파일의 끝에 도달시 비고
int printf(const char* formatString, ...); O X X 출력된 문자의 수 반환 EOF 반환
int fprintf(FILE* stream, const char* formatString, ...); O O X 출력된 문자의 수 반환 EOF 반환
int sprintf(char* buffer, const char* formatString, ...); X X O 끝에 \0을 뺀 작성된 바이트 수 반환
int scanf(const char* formatString, ...); O X X 입력된 문자의 수 반환 EOF 반환 EOF 반환
int fscanf(FILE* stream, const char* formatString, ...); O O X 입력된 문자의 수 반환 EOF 반환 EOF 반환
int sscanf(const char* src, const char* formatString, ...); X X O 성공적으로 변환된 필드 수 반환 EOF 반환 EOF 반환(문자열이 끝날 시)
  • float, double, long double의 데이터 출력에 사용되는 서식문자는 %f, %f, %Lf이다.
  • float, double, long double의 데이터 입력에 사용되는 서식문자는 %f, %lf, %Lf이다.

printf(), fprintf(), sprintf()

각 필드들을 입력하여 서식지정을 통해서 새롭게 만들어낸 문자열을 콘솔/파일/문자열에 출력하는 함수들이다.

  1. 서식문자

서식문자

  • %8d : 필드 폭을 8칸 확보후 오른쪽 정렬
  • %-8d : 필드 폭을 8칸 확보후 왼쪽 정렬
  1. printf(...)fprintf(stdout, ...)와 똑같다.
  2. sprintf()는 포맷의 형식으로 문자열을 버퍼(char*)에 출력한다.
    • 이를 이용하여 숫자를 문자열로 바꿀 수 있다. ex.sprintf(str, "%d", 240);

scanf(), fscanf(), sscanf()

콘솔/파일/문자열로부터 문자열을 서식지정된 패턴으로 입력받아 파싱하여 각 필드에 저장하는 함수들이다.

  1. 공백(, \t, \n)을 기준으로 데이터 구분하고, 공백 문자를 입력버퍼에 남겨두고 그 앞까지 받아들인다. (%d이든, %s이든 상관없이)
  2. 그러므로 보통 공백을 포함하는 문장은 scanf()로 입력받는 것은 적절치 못하다. (fgets()로 받고 후속조치할 것)
  3. 함수 호출 시 변수의 주소값을 넘기는 call-by-reference를 하는 이유는 스트림으로부터 입력을 받아서 해당 변수의 주소값에 직접 접근해서 채워넣기 위함이다.
  4. 서식 문자 : printf()와 비슷하면서 다르므로 별도로 기억해야 한다.
    1. %d : 10진수 정수
    2. %o : 8진수 양의 정수
    3. %x : 16진수 양의 정수
    4. %f, %e, %g : float형 데이터
    5. %lf : double형 데이터
    6. %Lf : long double형 데이터
    7. %s : 문자열(공백 이전까지)
  5. scanf(...)fscanf(stdin, ...)와 똑같다.
char name[10];
char sex;
int age;
int ret = fscanf(fp, "%s %c %d", name &sex, &age);
if (ret == EOF) {
  // 함수 오류 혹은 파일의 끝에 도달
  
  if (feof(fp) != 0) {
    // 파일의 끝에 도달
  }
}

문자 입출력 : putchar() vs fputc() vs getchar() vs fgetc()

함수 콘솔 파일 호출 성공시 호출 실패시 파일의 끝에 도달시 비고
int putchar(int ch); O X ch 반환 EOF 반환
int fputc(int ch, FILE* stream); O O ch 반환 EOF 반환
int getchar(void); O X 버퍼로부터 문자 1개 EOF 반환 EOF 반환
int fgetc(FILE* stream); O O 버퍼로부터 문자 1개 EOF 반환 EOF 반환
  1. getchar()fgetc() 의 반환형이 char가 아닌 int인 이유는 EOF가 -1이기 때문이다.
  2. 서식지정할 필요없이 문자 하나 단순 입력/출력하는 것이라면 scanf()printf()보다 메모리공간을 덜 차지하고, 속도가 빠른 위 함수를 쓰자.

문자열 입출력 : puts() vs fputs() vs gets() vs fgets()

함수 콘솔 파일 호출 성공시 호출 실패시 파일의 끝에 도달시 비고
int puts(const char* str); O X 음수가 아닌 값 EOF 반환 항상 끝에 자동적으로 개행
int fputs(const char* str, FILE* stream); O O 음수가 아닌 값 EOF 반환 자동적으로 개행 안함
char* gets(char* str); O X str NULL 반환 NULL 반환 쓰지 말것(오버플로우 위험)
char* fgets(char* str, int n, FILE* stream); O O str NULL 반환 NULL 반환 \n을 만날 때까지 또는 \0를 포함한 n개만큼 읽되, 공백문자와 \n을 포함해서 읽는다.
  1. 아래의 사례와 같이 문자열을 입력 받으면 문자열의 끝에 자동으로 \0 문자가 추가된다.
char str[7];
fgets(str, sizeof(str), stdin); // "123456789" 입력

puts(str); // "123456" 출력 : 널 문자를 포함하여 7개이므로, 6개 문자를 버퍼로부터 입력받음
  1. 아래의 사례와 같이 \n을 만날 때까지 문자열을 읽어 들이는데, \n을 제외시키거나 버리지 않고 문자열의 일부로 받아들인다.
char str[7];
fgets(str, sizeof(str), stdin); // "1234" 입력 후 엔터칠 때 입력버퍼로 "1234\n"이 삽입됨

puts(str); // "1234\n" 출력 : 개행문자를 비롯한 공백문자도 문자열의 일부로 받아들임
str[strlen(str) - 1] = 0; // 개행문자가 포함된 경우 개행문자를 `\0`으로 바꿈
  1. 서식지정할 필요없이 문자열을 단순 입력/출력하는 것이라면 scanf()printf()보다 메모리공간을 덜 차지하고, 속도가 빠른 위 함수를 쓰자.

파일관련 : fopen(), fclose(), fflush(), feof(), fseek(), ftell()

함수 호출 성공시 호출 실패시 파일의 끝에 도달시 비고
FILE* fopen(const char* filename, const char* mode); FILE* 반환 NULL 반환
int flose(FILE* stream); 0 반환 EOF반환
int fflush(FILE* stream); 0 반환 EOF 반환
int feof(FILE* stream); 0이 아닌 값 반환 파일의 끝이 아닐 경우 0 반환
int fseek(FILE* stream, long offset, int wherefrom); 0 반환 0이 아닌값 반환
long ftell(FILE* stream); 파일 위치 지시자의 offset 반환

fopen()

  1. 읽기만 가능할 때 파일이 없으면 에러 발생하여 NULL 반환한다.
  2. 쓰기 -> 읽기, 읽기 -> 쓰기로 작업을 변경할 때 메모리 버퍼를 비워줘야 하고 잘못 사용될 수 있기 때문에 웬만하면 r, w, a 중에서 선택하는 것이 좋다.
  3. 텍스트 모드(t)와 바이너리 모드(b)
    1. 기본값은 텍스트 모드이다.
    2. w+twt+는 같은 의미이다.
  4. 텍스트 모드로 개방하면 아래의 변환이 자동적으로 이루어진다.(ex. Windows)
    1. C 프로그램에서 \n을 파일에 저장하면 \r\n으로 변환되어 저장됨
    2. 파일에 저장된 \r\n을 C프로그램 상에서 읽으면 \n으로 변환되어 읽혀짐
    3. 즉, 텍스트모드로 개방하면 운영체제 별로 개행 문자가 다른 것을 신경 쓸 필요가 없어진다.
모드 스트림 성격 파일이 없으면?
r 읽기 가능 에러
w 쓰기 가능 생성
a 파일의 끝에 덧붙여 쓰기 가능 생성
r+ 읽기/쓰기 가능 에러
w+ 읽기/쓰기 가능 생성
a+ 읽기/덧붙여 쓰기 가능 생성

fclose()

  1. 운영체제가 할당한 자원의 반환
  2. 출력 버퍼에 버퍼링 되었던 데이터의 출력 및 출력버퍼를 비움
    • 즉, fclose()를 호출할 때 그제서야 파일 저장을 한다는 뜻이다.

fflush()

  1. 출력버퍼의 비워짐 = 출력버퍼에 저장된 데이터가 버퍼를 떠나서 목적지로 이동된다.
  2. 입력버퍼의 비워짐 = 입력버퍼의 데이터 소멸
    1. fflush(stdin);은 컴파일러에 따라 다른 결과를 보이므로 하면 안된다.
    2. while (getchar() != '\n');\n를 만날 때까지 \n을 포함해서 문자를 읽어들여 입력버퍼를 비울 수 있다.
if (fflush(stdout) == EOF) { 
  // 실패
  exit(-1);
} 
else {
  //성공
} 

feof()

파일의 마지막까지 저장된 데이터를 모두 읽어들일 때 반드시 파일의 끝을 확인해야 한다.

다음의 경우일때 feof()를 통해서 파일의 끝인지 확인해야 한다.

  1. getchar(), fgetc()의 경우에는 파일의 끝에 도달했거나 오류났을 경우에 (문자 하나 이므로) EOF를 반환한다.
  2. gets(), fgets()의 경우에는 파일의 끝에 도달했거나 오류났을 경우에 (문자열 이므로) NULL을 반환한다.
  3. fread()의 경우에는 파일의 끝에 도달했거나 오류났을 경우에 매개변수 count보다 작은 값을 반환한다.
if (feof(fp) != 0) {
  // 파일의 끝에 도달했다
} else {
  // 파일의 끝이 아니다
}

fseek(), ftell()

  1. 파일의 끝은 파일의 마지막 데이터가 아니라 파일의 끝을 표시하기 위해서 삽입이 되는 EOF를 의미한다.
  2. fseek()의 매개변수 wherefrom에 전달되는 상수
    1. SEEK_SET : 파일 맨 앞(첫 번째 바이트)에서부터 이동을 시작
    2. SEEK_CUR : 현재 위치에서부터 이동을 시작
    3. SEEK_END : 파일 맨 끝(EOF)에서부터 이동을 시작
  3. fseek()offset이 음수인 경우에 파일 앞쪽으로 이동한다.
  4. fgetc(), fgets() 등을 통해서 파일로부터 입력을 진행하면 그만큼 파일 위치 지시자는 이동한다.
  5. ftell()을 이용해서 파일 위치 지시자를 다시 이전 위치로 되돌릴 수 있다.
putchar(fgetc(fp));
fpos = ftell(fp); // 현재 파일 위치(offset) 저장
fseek(fp, -1, SEEK_END); // 파일 끝에서 첫번째 바이트를 가리킨다.(즉, 파일의 마지막 데이터)
putchar(fgetc(fp));
fseek(fp, fpos, SEEK_SET); // 이전 파일 위치로 복귀

fread() vs fwrite()

함수 호출 성공시 호출 실패시 파일의 끝에 도달시 비고
size_t fread(void* buffer, size_t size, size_t count, FILE* stream); count 반환 count보다 작은 값 반환 count보다 작은 값 반환
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream); count 반환 count보다 작은 값 반환
  1. fread()은 읽어 들인 바이트 수가 아니라 데이터 개수를 반환한다.
  2. 매개변수 size는 한 데이터의 크기를 뜻하고, 매개변수 count는 그 데이터의 개수를 뜻한다. 즉, 총 size * count bytes 크기 만큼 바이너리 파일로 입출력한다.
  3. 구조체는 바이너리 데이터로 인식하여 fread()fwrite() 함수로 파일 입출력을 처리한다.

<string.h>

함수 return 비고
size_t strlen(cosnt char* s); 전달된 문자열의 길이를 반환 널 문자(\0)는 길이에 포함하지 않음
char* strcpy(char* dest, const char* src); dest 값 반환
char* strncpy(char* dest, const char* src, size_t n); dest 값 반환 src의 문자열을 dest에 복사하되, src의 길이가 매우 길다면 n만큼의 길이만큼 복사
\0문자를 고려하지 않으므로 마지막에 널 문자를 따로 넣어줘야 한다.(아래 1번 참조)
char* strcat(char* dest, const char* stc); dest 값 반환
char* strncat(char* dest, const char* src, size_t n); dest 값 반환 src의 문자열 중 최대 n개만큼 덧붙이고, \0를 반드시 자동으로 넣어준다. 그러므로 destn+1개 만큼의 여유공간이 있어야 함
int strcmp(const char* s1, const char* s2); 두 문자열의 내용이 같으면 0, 아니면 0이 아닌 값 반환 \0을 포함해서 ascii값을 비교한다. s1s2보다 사전편찬 순서상 뒤에 위치하면 양수 반환, 그 반대면 음수 반환 ex) s1 = “Zebra”, s2= “Apple” -> 양수 반환
int strncmp(const char* s1, const char* s2, size_t n); 두 문자열의 내용이 같으면 0, 아니면 0이 아닌 값 반환 \0을 포함해서 ascii값을 비교한다. s1s2보다 사전편찬 순서상 뒤에 위치하면 양수 반환, 그 반대면 음수 반환 ex) s1 = “Zebra”, s2 = “Apple” -> 양수 반환
  1. strncpy()는 널 문자 삽입을 따로 고려해줘야 한다.
strncpy(dest, src, sizeof(dest) - 1); // NULL문자를 뺀 sizeof(dest)-1 만큼 복사(최대한 복사해서 넣어도 널문자를 위한 공간 하나 빼고 복사해야 하므로)
dest[strlen(dest) - 1] = 0; // 문자열의 마지막 끝부분 다음에 널 문자 삽입
  1. strcat(), strncat()에서는 src의 첫부분을 dest의 널문자부터 덮어씌운다.
char* src = "World";
char dest[8] = "Hello"; // 널문자가 차지한 공간 포함해서 3개 만큼 빈 공간이 있다.

strncat(dest, src, 2); // 널 문자를 반드시 마지막에 넣어줘야 하므로 최대 2개만큼 복사해서 붙여넣을 수 있다.
puts(dest); // "HelloWo"

<stdlib.h>

문자열을 숫자로 변환 : atoi(), atol(), atof()

  1. char* -> int : int atoi(const char* str);
  2. char* -> long : long atol(const char* str);
  3. char* -> double : double atof(const char* str);

동적할당 및 해제 : malloc(), calloc(), realloc(), free()

void* malloc(size_t size);

void* calloc(size_t elt_count, size_t elt_size); : 블록크기(elt_size) * 블록개수(elt_count)만큼 할당하고 모든 비트를 0으로 초기화한다.

void* realloc(void* ptr, size_t size); : ptr이 카리키는 메모리의 크기를 size만큼 조절한다.

void free(void* ptr);

  1. 힙에 할당된 메모리공간은 포인터(즉, 주소값)를 이용해 접근할 수 밖에 없다.

  2. 메모리 공간의 할당이 실패할 경우 NULL을 반환하므로 반드시 이를 체크해줘야 한다.

    int* ptr = (int*)malloc(sizeof(int) * 3);
    if (ptr == NULL) {
      // 메모리 할당 실패에 따른 오류 처리
    }
    
  3. void*로 반환되는 것은 주소값만을 갖고 있다는 의미이므로 이를 이용해서 참조하기 위해서는 포인터의 형변환을 해줘야 한다.

  4. realloc()은 확장할 영역이 넉넉치 못할 경우, 새로운 장소에 별도로 할당하여 이전 배열에 저장된 값을 복사해서 옮겨놓고 그 메모리 주소값을 반환하기도 한다. 이 경우에는 알아서 데이터를 옮겨주고, 기존 장소는 메모리 해제해주니까 신경쓸 필요가 없다.