자료형
자료형은 데이터를 표현하는 기준(데이터를 표현하는 방법)이다. 때문에 변수도 상수도 자료형에 근거한다.
-
변수는 값이 메모리 공간에 저장 및 참조되는 방식에 따라서 정수형과 실수형으로 나뉜다.
-
C표준에서는 자료형 별 크기를 정확히 제한하고 있지 않다. 즉, 컴파일러마다 차이가 있으니까 조심해야 한다.
-
컴퓨터는 2진수를 기반으로 데이터를 표현하고 연산도 진행한다.
정수형
- 가장 왼쪽의 비트(MSB)는 부호비트로 사용
- 음의 정수를 표현할 때에는 2의 보수를 취한다.
- 1의 보수를 취한다.(0과 1 반전)
- 1을 더한다.
- +n에다가 2의 보수를 취하면 -n이 되고, -n에 다가 2의 보수를 취하면 +n이 된다. 즉, 양수일때만 2의 보수를 취하는게 아님
- 일반적으로 CPU가 처리하기에 가장 적합한 크기의 정수 자료형을
int
로 정의한다. 따라서int
형 연산의 속도가 다른 자료형의 연산속도에 비해서 동일하거나 더 빠르다. - 데이터의 양이 많아서 연산속도보다 데이터의 크기를 줄이는 것이 더 중요한 경우
short
와 같은 자료형을 활용한다. - 정수 자료형에만
unsigned
를 붙일 수 있다. - 보통은
signed
가 붙으나 안 붙으나 같은 의미이지만 컴파일러에 따라signed char
!=char
일 수 있다. char
은 정수형이다. 즉, 문자도 결국 정수를 저장한 것이다. ex.char ch = 'A';
은 컴파일러에 의해char ch = 65;
로 바뀐다.
실수형
- 컴퓨터가 실수를 표현하는 방식에는 넓은 범위의 실수를 표현할 수 있지만, 오차가 존재한다.(=부동 소수점 오차) 그래서 아래와 같이 근사치를 계속 더하다 보면 오차가 생기게 되므로 주의해야 한다.
#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;
}
- 실수 자료형에서는 보편적으로
double
을 선택한다. 즉, 특별한 선언이 없다면 소수점을 포함한 소수는double
자료형으로 인식된다.
리터럴 상수
이름이 없고 변경이 불가능한 데이터로, 리터럴 상수도 자료형이 존재한다.
int inum = 5;
5
는int
형으로 메모리 공간에 저장하기로 되어 있다.double dnum = 7.15;
7.15
는double
형으로 메모리 공간에 저장하기로 약속되어 있다.
ex.float num1 = 5.123;
double
형 상수를float
변수에 넣는 것이기 때문에 자동 형변환이 발생하여 데이터 손실 경고가 뜬다.0xA
: 16진수 ‘A’(=10) /012
: 8진수 ‘12’(=10)- 상수의 표현을 위한 접미사
char* str ="Text";
처럼 문자열은 포인터 상수(즉 주소값을 반환)로 표현된다.
자동 형변환의 종류
-
대입연산의 전달과정에서 발생
double num1= 245;
데이터 손실은 없으나 부동소수점 오차 발생int num2 = 3.14;
소수점 이하 값 손실int num3=129; char ch = num3;
상위 바이트 단순 소멸. 부호가 바뀔수 있음
-
정수의 승격 : 연산을 빠르게 하기 위해서
int
보다 작은 크기의 정수형 데이터를int
형 데이터로 형 변환이 되어서 연산이 진행됨short num1 = 15, num2 = 25; short num3 = num1 + num2;
num1과 num2가 int형으로 형 변환
-
피연산자의 자료형 불일치
double num = 5.15 + 19;
19가 19.0으로 형변환int
->long
->long long
->float
->double
->long double
char
,short
의 경우 정수의 승격에 의해서int
로 변환되기 때문에 위 규칙에서 없음
특수문자
키워드
아래의 경우, 변수나 함수의 이름으로 쓸 수 없다.
auto
_Bool
break
case
char
_Complex
const
continue
default
do
double
else
enum
extern
float
for
goto
if
_Imaginary
return
restrict
short
signed
sizeof
: 이건 함수가 아닌 연산자이다.static
struct
switch
typedef
union
unsigned
void
volatile
while
연산자
- 결합 방향이란, 우선순위가 동일한 두 연산자가 하나의 수식에 존재하는 경우, 어떠한 순서대로 연산하느냐를 결정해 놓은 것
- 결합 방향이 왼쪽인 경우는 연산자를 피연산자의 왼쪽에 놓는 경우와 조건연산, 대입연산이다. ex.
sizeof str
,&num
, …
++num
/--num
: 값을 1 증가/감소 후, 속한 문장의 나머지를 진행(선 증가/감소, 후 연산)num++
/num--
: 속한 문장 전체를 먼저 진행한 후, 값을 1 증가/감소(선 연산, 후 증가/감소)- 소괄호도 연산자이다. 즉, 2번 연산자의 경우 소괄호와 상관없이 다음 문장으로 넘어가야만 비로소 값의 증가 및 감소가 이뤄진다.
- 곱셈과 나눗셈이 비트의 이동부다 부담스러운 연산이다.
변수의 종류
- 지역변수 : 중괄호 내에 선언된 모든 변수(반복문, 조건문, 함수의 매개변수 등)
- 중괄호를 나오면 다 해제
- 초기화 하지 않으면 쓰레기값
- 같은 중괄호 내에서만 접근 가능
- 전역변수 : 어떤 중괄호에도 포함되지 않음
- 프로그램 시작과 동시에 메모리 공간(Data 영역)에 할당되어 종료시까지 존재
- 별도의 값 초기화하지 않으면 0으로 초기화
- 프로그램 전체 영역 어디서든 접근 가능
- static 지역변수 : 선언된 함수에서만 접근 가능한 전역변수
- 선언된 함수 내에서만 접근 가능
- 별도의 값 초기화하지 않으면 0으로 초기화
- 딱 1회 초기화되고 프로그램 종료 시까지 메모리 공간(Data 영역)에 존재
- static 전역변수 : 선언된 파일 내에서만 접근 가능한 전역변수(다른 외부 파일에서 접근을 허용하지 않는다.)
static void increment() {}
: 함수를static
선언을 하면 선언된 파일 내에서만 접근이 가능하다. 이로서 코드 안전성을 부여한다.
- register 변수 : CPU 내 레지스터에 저장될 확률이 높은 변수
- register에 할당될지는 컴파일러가 결정
- 지역변수에만 적용하는 것이 유의미(전역변수로 죽치고 있으면 큰 손해이므로)
Miscellaneous
General
-
int main(int argc, char* argv[]) { return 0; }
argc
는 자기 자신을 포함한 매개변수의 개수이다.argv
는 자기 자신을 포함해서 매개변수를 띄워쓰기를 기준으로 받은 문자열 배열이다.- ex.
./helloProgram I Love You
:argc
== 4,argv
=={"./helloProgram", "I", "Love", "You"}
- ex.
./byeProgram "Good work."
:argc
== 2,argv
=={"./byeProgram", "Good work."}
이와 같이 큰따옴표로 묶으면 공백을 포함한 하나의 문자열 매개변수로 처리한다.
-
스트림 : 프로그램상에서 모니터와 키보드를 대상으로 데이터를 입출력하기 위해서 연결시켜 주는 다리
- 운영체제가 제공하는 소프트웨어적인 상태
- 한 방향으로 흐르는 데이터의 흐름
- 콘솔 입출력을 위한 입력 스트림, 출력 스트림은 프로그램이 실행되면 자동으로 생성되고, 프로그램이 종료되면 자동으로 소멸되는 스트림이다.(
stdin
,stdout
,stderr
)
-
프로그램 실행시 운영체제에 의해서 마련되는 메모리의 구성
- 코드 영역 : 실행할 프로그램의 코드가 저장된다. CPU는 이 영역에 저장된 명령문을 하나씩 가져가서 실행한다.
- 데이터 영역 : 전역변수와 static 변수 할당. 프로그램 시작과 동시에 메모리 공간에 할당되어 프로그램 종료 시까지 남아있게 된다.
- 스택 영역 : 지역변수, 매개변수 할당. 함수를 빠져나가면 소멸된다.
- 힙 영역 : 프로그래머가 원하는 시점에
malloc()
,free()
등을 통해 변수를 할당하고 소멸할 수 있도록 지원되는 영역. 프로그래머가 따로 해제해주지 않으면 프로그램 종료시 운영체제에 의해 해제된다.
-
프로그램이 종료되면 운영체제에 의해서 할당된 메모리 공간 전체를 반환하는데 이때, 전역변수가 소멸된다.
-
C 프로그램의 생성 과정
-
컴파일러는 파일 단위로 컴파일 진행한다. 즉, 다른 파일의 정보를 참조하여 컴파일을 진행하지 않는다. 그러므로 외부에 함수, 변수 등이 정의되어 있다면 컴파일러에게 알려줘야 한다. 컴파일러에게 알려주는 것은 몇 번이고 중복되도 상관없다.
extern int num;
:num
변수가 다른 파일에 전역변수로 정의되어 있음을 알려준다.extern void increment();
,void increment();
: 함수가 다른 파일에 정의되어 있음을 알려준다. 여기서는extern
이 생략 가능
-
헤더파일 선언 : 헤더파일에 있는 변수, 함수, 매크로를 쓰는 소스파일에만, 구조체 정의를 쓰는 소스/헤더파일에만 추가하면 된다.
#include <stdio.h>
: 표준 헤더파일이 저장되어 있는 디렉터리에서 파일을 찾는다.#include "myheader.h"
: 이 소스파일이 저장된 디렉터리에서 헤더파일을 찾는다.#include "Release/header.h"
: 소스파일이 있는 디렉터리의 하위폴더Release
에서 헤더파일을 찾는다.#include "../header.h"
: 소스파일이 있는 디렉터리의 상위폴더에서 헤더파일을 찾는다.
-
헤더파일에 포함해야 하는 것
-
외부에 선언된 변수/함수에 접근/호출하기 위한 선언들 : 소스파일이 2개 이상이면 생길 수 밖에 없음
// arith.h #ifndef __ARITH_H__ // 구조체 정의가 포함되어 있는 "stdiv.h"을 포함함으로 헤더파일 중복삽입이 문제가 될 수 있기 때문에 미연에 방지하는게 좋다. #define __ARITH_H__ #include "stdiv.h" extern int num; Div add(int, int); #endif
-
매크로 : 매크로의 명령문도 파일 단위로만 유효하다.
-
구조체의 정의 : 구조체의 정의는 그 구조체를 필요로 하는 모든 파일에 존재해야 한다. 그러나 구조체를 중복 정의하면 컴파일 에러 메시지가 뜨므로, 이를 조건부 컴파일을 이용해서 해결해야 한다. 구조체의 정의는 구조체의 정의만을 포함하거나 그 구조체와 관련된 함수들만이 포함된 파일로 만드는게 좋겠다.
// stdiv.h #ifndef __STDIV_H__ // 이와 같이 조건부 컴파일을 하면 자유롭게 #include "stdiv.h" 해도 괜찮다. #define __STDIV_H__ typedef struct { int quotient; int remainder; } Div; #endif
-
선행처리
컴파일러가 컴파일 하기 이전에 선행처리기가 먼저 처리하여 소스파일로 반환한다. 컴파일러가 아니기 때문에 선행처리 명령문들은 #
으로 시작하여 끝에 세미콜론(;
)을 붙이지 않는다. 그리고 보통은 매크로의 이름을 대문자로 정의한다. 여러 줄에 걸쳐서 정의할 때는 \
문자를 활용하여 줄이 바뀌었음을 알려줘야 한다. 선행처리기가 파일 단위로 선행처리를 하기 때문에 매크로의 명령문도 파일단위로만 유효하다.
-
#define PI 3.14
: PI라는 문자를 볼 때마다 3.14로 치환한다. -
#define PRINT_HELLO puts("HELLO");
: PRINT_HELLO를 볼 때마다 해당 함수로 치환한다. -
#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이 대입된다.
-
-
#define ADD
처럼 매크로의 몸체를 생략해서 정의해도 된다. 이렇게 정의하면 ADD라는 문구는 다 공백으로 대체가 된다. -
#include <stdio.h>
:stdio.h
파일의 모든 내용을 여기에다가 그대로 복사한다. -
#if A ... #elif B !!! #else ,,, #endif
:A
가 참이라면...
컴파일,B
가 참이라면!!!
컴파일, 둘 다 아니라면,,,
컴파일#define ADD 1 #if ADD // ADD가 참이면 아래 줄을 컴파일함 printf("+"); #endif
-
#ifdef A ... #else ,,, #endif
:A
가 정의(#define
)되어 있다면...
컴파일, 아니면,,,
컴파일#ifndef A ... #else ,,, #endif
: 정의되어 있지 않다면
연속적인 문장
int num1 = 30, num2 = 40;
여러 변수의 선언과 동시에 초기화를 할 때 comma를 쓴다.int* ptr1 = NULL, * ptr2 = NULL;
여러 개의 포인터변수를 쓸 때는 이렇게 쓴다.num1 = num2 = 0;
여러 개의 변수에 같은 값을 대입할 때 이렇게 쓴다.printf("Name: "); scanf("%s", name);
뭘 입력할지 안내하고 입력을 받을 때 이와 같이 한 줄로 입력하면 좋다.
반복문
do ~ while
과while
는 실제로 body가 1회 이상 실행되고, 조건문이 같고, body가 조건문에 영향을 주지 않는다면, 실행횟수는 똑같다.- 즉, 반복조건의 검사위치가 달라서
do ~ while
은 최소 1회 이상 실행한다는 점이while
과의 유일한 차이점 - 반복문의 반복횟수가 정해져 있다면,
for
를 쓰는것이 유리하다. break;
는 가장 가까이서 감싸고 있는 반복문 하나를 빠져나오는 것이다.
조건문
switch(n) ~ case
문에서 n은 정수형 변수이므로char
형도 포함된다.- 두 수 중 큰 수 혹은 작은 수를 계산하는 것은
? :
을 잘 활용하자.
함수
- 함수의 선언에서는
int increment(int);
처럼 매개변수의 이름을 생략할 수 있다. - 함수가 호출되면 해당 함수의 복사본을 만들어서 실행한다고 생각해야함
- 함수가 호출될 때 메모리 공간 내 stack영역에서 새로 공간이 할당되어서 해당 지역변수들이 생성된다.
- 인자 전달의 기본방식은 **값의 복사(call-by-value)**이다. 즉, 복사가 되는 것 뿐이기 때문에, 함수가 호출되고 나면, 전달되는 인자와 매개변수는 별개가 된다.
- call-by-reference : 그냥 단순하게 주소 값을 매개변수로 전달하는 경우
- 매개변수로 배열을 선언할 수 없다. 매개변수가 넘겨질 때 그만큼 새로 메모리공간을 할당하는데, 배열의 사이즈가 크면 stack을 초과할 수 있기 때문이다.
- 매개변수로 1차원 배열을 넘길 때에는 다음과 같이 배열의 시작주소값을 넘겨야 한다.
void showArrayElem(int * param, int len);
void shoWArrayElem(int param[], int len);
- 매개변수에서만
int param[]
과int* param
은 완전히 동일한 선언이다. - 주소값만을 넘겨서는 그 배열의 사이즈를 알 수 없으므로 항상 배열 사이즈와 함께 넘겨야 한다.
sizeof(param)
은param
이 포인터 변수이므로 단순히 포인터 변수의 크기인 8을 반환한다.- 배열의 크기는 보통
sizeof(param) / sizeof(param[0])
으로 넘기면 된다.
- 매개변수로 2차원 배열을 넘길 때에는 다음과 같이 배열 포인터와 행 사이즈를 넘겨야 한다.
void show2DArray(int (*arr)[4], int column);
void show2DArray(int arr[][4], int column);
- 매개변수에서만
int (*arr)[4]
과int arr[][4]
은 완전히 동일한 선언이다. - 배열 포인터만을 넘겨서는 그 2차원배열의 행 개수를 알 수 없으므로 항상 행 사이즈와 함께 넘겨야 한다.
sizeof(arr)
은arr
이 포인터 변수이므로 단순히 포인터 변수의 크기인 8을 반환한다.- 배열의 크기는 보통
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("");
}
}
- 함수에서 지역적으로 선언된 변수의 주소값을 반환하면 안된다. 함수가 반환되면 거기에 속해 있는 모든 지역변수들이 해제되기 때문이다. 이를 해결하려면 동적할당을 통해 힙의 영역에서 변수를 선언하고 이를 반환해야 한다.
- 함수에서 반환할 때에도 리턴값을 복사해서 반환한다.
구조체(struct)
- 구조체 선언 방식
// 구조체 정의 방식 #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 키워드로 선언 불가능
- 구조체 변수의 주소값은 구조체 변수의 첫 번째 멤버의 주소 값과 동일
typedef
키워드로 재정의할 경우 구조체 변수의 이름을 보통 대문자로 설정sizeof(struct)
를 계산하면struct
의 모든 멤버의 sizeof 결과를 더한것과 같다.
공용체(union)
typedef struct {
unsigned short upper;
unsigned short lower;
} DBShort;
typedef union {
int iBuf;
char bBuf[4];
DBShort sBuf;
} RDBuf;
- 위 사례처럼
union
은 같은 메모리를 다양하게 해석하고, 접근할 수 있다.- 4bytes 메모리 공간을
rdBuf.iBuf
로 접근하면 하나의 int 정수로 접근 rdBuf.bBuf
로 접근하면 크기가 4인 문자 배열로 접근rdBuf.sBuf
로 접근하면 상위 2bytes, 하위 2bytes로 short 2개로 접근할 수 있다.
- 4bytes 메모리 공간을
열거형(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
}
문자열
- ‘
\0'
은 ascii값이 0으로 이를 문자의 형태로 출력할 경우, 아무런 출력이 발생하지 않는다.(공백(' '
)이 출력되는 것과 다름) - 문자열 != 문자배열 : 문자열을 구분할 때 마지막에 항상
\0
이 있어야 한다. 그래야만 정상적으로 문자열이 출력된다.str[strlen(str) - 1] = 0
: str 문자열의 마지막에 null문자 대입,'\0'
과0
을 넣는 것은 같은 의미(ascii 값이 같으므로)- null 문자가 없으면 단순한 문자 배열이다.
- C언어에서는 개행을
\n
으로 표시하기로 약속한다. 이는 C언어에서만 해당한다.- MS-DOS(Windows) :
\r\n
- Mac OS :
\r
- Unix 계열:
\n
- MS-DOS(Windows) :
- 문자열이 파일에 저장될 때에는 문자열의 끝을 의미하는 널 문자는 저장되지 않는다. 때문에 파일에서는 개행을 기준으로 문자열을 구분한다.
- 문자열을 나란히 선언하면 하나의 문자열로 간주된다. 즉
"ABC" "DEF"
는"ABCDEF"
와 같다.
배열
1차원 배열
int arr[3] = {1 };
: 나머지arr[1]
,arr[2]
는 0으로 초기화int arr[] = {4, 5, 6};
:arr
의 사이즈는 3으로 자동 결정M
사이즈인 1차원 배열 동적할당:int * ptr = (int*)malloc(sizeof(int) * M);
2차원 배열
int arr2d[2][4] = {1, 2, 3, 4, 5, 6, 7, 8};
: 1차원 배열처럼 초기화 가능int arr2d[][4] = {{1, 2, 3}, {5, 6}};
:arr[0][3]
,arr[1][2]
,arr[1][3]
은 0으로 초기화int arr2d[][4] = {1, 2, 3, 4, 5, 6, 7, 8};
: 세로의 길이만 생략 가능- 위 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]
int (*ptr1)[4] = matrix;
에서 배열포인터 변수ptr1
은 int형 변수를 가리키면서 포인터 연산시 sizeof(int) * 4의 크기 단위로 값이 증가 및 감소한다.- 배열 포인터 != 포인터 배열
int* a[4];
: 포인터 배열 = int형 포인터 4개를 담고 있는 배열int (*b)[4];
: 배열 포인터 = int변수를 가리키고 포인터 연산시 sizeof(int) * 4만큼 증감하는 포인터
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 포인터 상수
포인터 변수
메모리의 주소 값을 저장하기 위한 변수
- 64bit OS, 64bit으로 컴파일했을 때
sizeof(ptr) == 8
로 계산된다. &
연산자는 변수만이 피연산자가 될 수 있다.- 포인터의 형(type)은 메모리 공간을 참조하는 기준이 되어서
*
연산할 때 메모리 공간의 접근 기준이 된다.- 즉,
*pnum
은pnum
의 포인터 자료형에 따라서 해당하는 주소에서 몇 바이트를 읽을지, 정수/실수형으로 해석할지 판단한다.
- 즉,
const int* ptr;
은 포인터 변수ptr
을 이용해서ptr
이 가리키는 변수에 저장된 값을 변경하는 것을 허용하지 않겠다는 뜻int* const ptr;
은 포인터 변수ptr
에 저장된 주소값을 변경하는 것을 허용하지 않겠다는 뜻const int
인지const ptr
인지로 구분하면 좋겠다.
void*
는 형(type)이 존재하지 않는 포인터다.- 함수포인터, 배열포인터 등등 무엇이든 그것의 주소만을 담을 수 있는 포인터
- 포인터 연산(값의 변경 및 참조 등)을 하려면 캐스팅한 후 진행해야 한다.
포인터 상수(상수 형태의 포인터)
메모리의 주소 값이지만 그 주소값을 변경할 수 없는 상수
- 배열의 이름은 배열의 시작 주소값을 의미하며, 그 형태는 값의 저장이 불가능한 상수이다.
- 배열의 이름과 포인터 변수는 변수냐 상수냐의 특성적 차이만 있을 뿐, 둘 다 포인터이기 때문에 포인터 변수로 할 수 있는 연산은 배열의 이름으로도 할 수 있고, 배열의 이름으로 할 수 있는 연산은 포인터 변수로도 할 수 있다.
- 즉,
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
- 문자열을 두 가지 형태로 선언할 수 있다.
char str1[] = "My String";
: str1은 계속 문자열이 저장된 위치를 가리켜야 한다.char * str2 = "My String";
: str2는 다른 문자열을 가리킬 수 있다.
- 함수의 이름은 함수가 저장된 메모리공간의 주소값을 의미한다.
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
를 갖고 배열의 크기를 반환하고 싶으면 포인터 상수를 피연산자로 넣어야 한다.
포인터 배열
포인터 변수로 이루어진 배열
char* str[3];
은 문자열을 3개 저장할 수 있는 char형 포인터 배열이다.
이중 포인터(더블 포인터)
포인터를 가리키는 포인터 변수
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()
각 필드들을 입력하여 서식지정을 통해서 새롭게 만들어낸 문자열을 콘솔/파일/문자열에 출력하는 함수들이다.
- 서식문자
%8d
: 필드 폭을 8칸 확보후 오른쪽 정렬%-8d
: 필드 폭을 8칸 확보후 왼쪽 정렬
printf(...)
는fprintf(stdout, ...)
와 똑같다.sprintf()
는 포맷의 형식으로 문자열을 버퍼(char*)에 출력한다.- 이를 이용하여 숫자를 문자열로 바꿀 수 있다. ex.
sprintf(str, "%d", 240);
- 이를 이용하여 숫자를 문자열로 바꿀 수 있다. ex.
scanf(), fscanf(), sscanf()
콘솔/파일/문자열로부터 문자열을 서식지정된 패턴으로 입력받아 파싱하여 각 필드에 저장하는 함수들이다.
- 공백(,
\t
,\n
)을 기준으로 데이터 구분하고, 공백 문자를 입력버퍼에 남겨두고 그 앞까지 받아들인다. (%d
이든,%s
이든 상관없이) - 그러므로 보통 공백을 포함하는 문장은 scanf()로 입력받는 것은 적절치 못하다. (fgets()로 받고 후속조치할 것)
- 함수 호출 시 변수의 주소값을 넘기는 call-by-reference를 하는 이유는 스트림으로부터 입력을 받아서 해당 변수의 주소값에 직접 접근해서 채워넣기 위함이다.
- 서식 문자 : printf()와 비슷하면서 다르므로 별도로 기억해야 한다.
%d
: 10진수 정수%o
: 8진수 양의 정수%x
: 16진수 양의 정수%f
,%e
,%g
: float형 데이터%lf
: double형 데이터%Lf
: long double형 데이터%s
: 문자열(공백 이전까지)
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 반환 |
getchar()
와fgetc()
의 반환형이char
가 아닌int
인 이유는EOF
가 -1이기 때문이다.- 서식지정할 필요없이 문자 하나 단순 입력/출력하는 것이라면
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 을 포함해서 읽는다. |
- 아래의 사례와 같이 문자열을 입력 받으면 문자열의 끝에 자동으로
\0
문자가 추가된다.
char str[7];
fgets(str, sizeof(str), stdin); // "123456789" 입력
puts(str); // "123456" 출력 : 널 문자를 포함하여 7개이므로, 6개 문자를 버퍼로부터 입력받음
- 아래의 사례와 같이
\n
을 만날 때까지 문자열을 읽어 들이는데,\n
을 제외시키거나 버리지 않고 문자열의 일부로 받아들인다.
char str[7];
fgets(str, sizeof(str), stdin); // "1234" 입력 후 엔터칠 때 입력버퍼로 "1234\n"이 삽입됨
puts(str); // "1234\n" 출력 : 개행문자를 비롯한 공백문자도 문자열의 일부로 받아들임
str[strlen(str) - 1] = 0; // 개행문자가 포함된 경우 개행문자를 `\0`으로 바꿈
- 서식지정할 필요없이 문자열을 단순 입력/출력하는 것이라면
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()
- 읽기만 가능할 때 파일이 없으면 에러 발생하여
NULL
반환한다. - 쓰기 -> 읽기, 읽기 -> 쓰기로 작업을 변경할 때 메모리 버퍼를 비워줘야 하고 잘못 사용될 수 있기 때문에 웬만하면
r
,w
,a
중에서 선택하는 것이 좋다. - 텍스트 모드(
t
)와 바이너리 모드(b
)- 기본값은 텍스트 모드이다.
w+t
와wt+
는 같은 의미이다.
- 텍스트 모드로 개방하면 아래의 변환이 자동적으로 이루어진다.(ex. Windows)
- C 프로그램에서
\n
을 파일에 저장하면\r\n
으로 변환되어 저장됨 - 파일에 저장된
\r\n
을 C프로그램 상에서 읽으면\n
으로 변환되어 읽혀짐 - 즉, 텍스트모드로 개방하면 운영체제 별로 개행 문자가 다른 것을 신경 쓸 필요가 없어진다.
- C 프로그램에서
모드 | 스트림 성격 | 파일이 없으면? |
---|---|---|
r |
읽기 가능 | 에러 |
w |
쓰기 가능 | 생성 |
a |
파일의 끝에 덧붙여 쓰기 가능 | 생성 |
r+ |
읽기/쓰기 가능 | 에러 |
w+ |
읽기/쓰기 가능 | 생성 |
a+ |
읽기/덧붙여 쓰기 가능 | 생성 |
fclose()
- 운영체제가 할당한 자원의 반환
- 출력 버퍼에 버퍼링 되었던 데이터의 출력 및 출력버퍼를 비움
- 즉,
fclose()
를 호출할 때 그제서야 파일 저장을 한다는 뜻이다.
- 즉,
fflush()
- 출력버퍼의 비워짐 = 출력버퍼에 저장된 데이터가 버퍼를 떠나서 목적지로 이동된다.
- 입력버퍼의 비워짐 = 입력버퍼의 데이터 소멸
fflush(stdin);
은 컴파일러에 따라 다른 결과를 보이므로 하면 안된다.while (getchar() != '\n');
로\n
를 만날 때까지\n
을 포함해서 문자를 읽어들여 입력버퍼를 비울 수 있다.
if (fflush(stdout) == EOF) {
// 실패
exit(-1);
}
else {
//성공
}
feof()
파일의 마지막까지 저장된 데이터를 모두 읽어들일 때 반드시 파일의 끝을 확인해야 한다.
다음의 경우일때 feof()
를 통해서 파일의 끝인지 확인해야 한다.
getchar()
,fgetc()
의 경우에는 파일의 끝에 도달했거나 오류났을 경우에 (문자 하나 이므로)EOF
를 반환한다.gets()
,fgets()
의 경우에는 파일의 끝에 도달했거나 오류났을 경우에 (문자열 이므로)NULL
을 반환한다.fread()
의 경우에는 파일의 끝에 도달했거나 오류났을 경우에 매개변수 count보다 작은 값을 반환한다.
if (feof(fp) != 0) {
// 파일의 끝에 도달했다
} else {
// 파일의 끝이 아니다
}
fseek(), ftell()
- 파일의 끝은 파일의 마지막 데이터가 아니라 파일의 끝을 표시하기 위해서 삽입이 되는
EOF
를 의미한다. fseek()
의 매개변수wherefrom
에 전달되는 상수SEEK_SET
: 파일 맨 앞(첫 번째 바이트)에서부터 이동을 시작SEEK_CUR
: 현재 위치에서부터 이동을 시작SEEK_END
: 파일 맨 끝(EOF
)에서부터 이동을 시작
fseek()
의offset
이 음수인 경우에 파일 앞쪽으로 이동한다.fgetc()
,fgets()
등을 통해서 파일로부터 입력을 진행하면 그만큼 파일 위치 지시자는 이동한다.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 보다 작은 값 반환 |
fread()
은 읽어 들인 바이트 수가 아니라 데이터 개수를 반환한다.- 매개변수
size
는 한 데이터의 크기를 뜻하고, 매개변수count
는 그 데이터의 개수를 뜻한다. 즉, 총size * count
bytes 크기 만큼 바이너리 파일로 입출력한다. - 구조체는 바이너리 데이터로 인식하여
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 를 반드시 자동으로 넣어준다. 그러므로 dest 는 n+1 개 만큼의 여유공간이 있어야 함 |
int strcmp(const char* s1, const char* s2); |
두 문자열의 내용이 같으면 0, 아니면 0이 아닌 값 반환 | \0 을 포함해서 ascii값을 비교한다. s1 이 s2 보다 사전편찬 순서상 뒤에 위치하면 양수 반환, 그 반대면 음수 반환 ex) s1 = “Zebra”, s2 = “Apple” -> 양수 반환 |
int strncmp(const char* s1, const char* s2, size_t n); |
두 문자열의 내용이 같으면 0, 아니면 0이 아닌 값 반환 | \0 을 포함해서 ascii값을 비교한다. s1 이 s2 보다 사전편찬 순서상 뒤에 위치하면 양수 반환, 그 반대면 음수 반환 ex) s1 = “Zebra”, s2 = “Apple” -> 양수 반환 |
strncpy()
는 널 문자 삽입을 따로 고려해줘야 한다.
strncpy(dest, src, sizeof(dest) - 1); // NULL문자를 뺀 sizeof(dest)-1 만큼 복사(최대한 복사해서 넣어도 널문자를 위한 공간 하나 빼고 복사해야 하므로)
dest[strlen(dest) - 1] = 0; // 문자열의 마지막 끝부분 다음에 널 문자 삽입
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()
char*
->int
:int atoi(const char* str);
char*
->long
:long atol(const char* str);
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);
-
힙에 할당된 메모리공간은 포인터(즉, 주소값)를 이용해 접근할 수 밖에 없다.
-
메모리 공간의 할당이 실패할 경우
NULL
을 반환하므로 반드시 이를 체크해줘야 한다.int* ptr = (int*)malloc(sizeof(int) * 3); if (ptr == NULL) { // 메모리 할당 실패에 따른 오류 처리 }
-
void*
로 반환되는 것은 주소값만을 갖고 있다는 의미이므로 이를 이용해서 참조하기 위해서는 포인터의 형변환을 해줘야 한다. -
realloc()
은 확장할 영역이 넉넉치 못할 경우, 새로운 장소에 별도로 할당하여 이전 배열에 저장된 값을 복사해서 옮겨놓고 그 메모리 주소값을 반환하기도 한다. 이 경우에는 알아서 데이터를 옮겨주고, 기존 장소는 메모리 해제해주니까 신경쓸 필요가 없다.