1. 데이터 타입
중요 포인트
return 은 함수 실행을 종료하고, 결과 값을 EAX 레지스터에 저장한 후 반환합니다.
- 서로 다른 기능을 수행하는 코드 블록 사이에는 공백 줄을 넣어 가독성을 높입니다.
- 코드 내에서 주석 스타일은 일관되게 유지합니다.
/* */ 블록 주석은 중첩하여 사용할 수 없습니다.
long long 자료형은 C99 표준에서 도입되었습니다.
extern int data; 는 컴파일러에게 "이 변수는 int 타입이며, 다른 파일에 정의되어 있다"고 알려주는 선언입니다.
핵심 용어
- 선언 (Declaration): 메모리를 할당하지 않고, 컴파일러에게 변수의 타입과 이름을 알려줍니다.
- 정의 (Definition): 변수를 위해 실제 메모리 공간을 할당합니다.
- 사용 (Use): 변수에 대한 읽기 또는 쓰기 연산을 의미합니다.
- 초기화 (Initialization): 변수를 선언하면서 동시에 초기값을 설정하는 것입니다.
extern int num;
void test()
{
printf("%d\n", num);
}
int num; // 실제 메모리 공간이 할당되는 정의
변수명 규칙
- 영문자, 숫자, 밑줄(
_)을 사용할 수 있으며, 숫자로 시작할 수 없습니다.
- C 언어의 예약어(keyword)는 변수명으로 사용할 수 없습니다.
- 자료형은 메모리 공간의 크기를 결정하고, 변수명은 해당 공간을 식별하는 이름입니다.
문자 자료형
- 소문자
a부터 z는 ASCII 코드 97부터 122까지의 값을 가집니다.
- 대문자
A부터 Z는 ASCII 코드 65부터 90까지의 값을 가집니다.
- 같은 알파벳의 대문자와 소문자는 ASCII 코드 값이 32만큼 차이납니다.
char 타입 변수 초기화의 권장 방법: char ch = '\0';
- 문자 자료형에 저장되는 실제 값은 해당 문자의 ASCII 코드 값입니다. 컴퓨터는 모든 데이터를 숫자로 처리하며, 출력 시에만 문자 형태로 변환하여 보여줍니다.
- 문자 상수(
'a')는 크기가 1바이트이고, 문자열 상수("a")는 문자열 끝의 널 문자(\0)를 포함하여 2바이트의 크기를 가집니다.
- 큰따옴표(
")는 문자열을 나타내며, 해당 문자열의 첫 번째 문자의 주소 값을 의미합니다.
- 작은따옴표(
')는 문자를 나타내며, 해당 문자의 ASCII 코드 값을 의미합니다.
int num = '1234'; 와 같은 다중 문자 상수는 이식성 문제가 있을 수 있으므로 주의해야 합니다.
실수 자료형
- C++에서
f 접미사 없이 실수를 사용하면 기본적으로 double 타입으로 간주됩니다.
float 타입 변수의 권장 초기화 방법: float f = 0.0f;
- C++의
cout은 float와 double 모두 기본적으로 소수점 이하 5자리까지 출력합니다.
이스케이프 시퀀스
- 기본 이스케이프:
'\0' (널 문자), '\n' (줄 바꿈), '\t' (탭), '\r' (캐리지 리턴), '\a' (경고음)
- 8진수 이스케이프
'\ddd': d는 0~7 사이의 숫자이며, 최대 3자리의 8진수 값을 나타냅니다.
- 16진수 이스케이프
'\xhh': h는 0~9, a~f 사이의 문자이며, 최대 2자리의 16진수 값을 나타냅니다.
데이터 타입 변환
- 컴파일러는 정밀도 손실을 방지하기 위해 작은 크기의 데이터 타입을 자동으로 더 큰 크기의 데이터 타입으로 변환합니다. 예:
int → long → float → double
char과 short 타입은 연산 시 자동으로 4바이트(int)로 변환됩니다.
unsigned 타입과 signed 타입이 섞여 연산될 경우, signed 타입이 unsigned 타입으로 변환된 후 연산이 수행됩니다.
구조체
기본 자료형(char, short, int, long, float, double, unsigned, signed) 외의 복합 데이터 타입입니다.
struct Person
{
int age;
char name[32];
}; // 세미콜론 필수
- 구조체 배열 초기화:
memset(arr, 0, sizeof(arr));
- 멤버 오프셋 구하기:
(size_t)&((struct Person*)0)->age
- 같은 타입의 구조체 변수 간 대입(
=)은 내부적으로 memcpy 방식으로 동작합니다.
- 얕은 복사와 깊은 복사: 구조체 내에 포인터 멤버가 있을 경우 발생할 수 있습니다.
- 변수 자체의 내용은 모두 복사되지만, 포인터가 가리키는 동적 할당 메모리 영역은 복사되지 않아 두 구조체가 동일한 메모리를 가리키게 됩니다.
- 이로 인해 동일한 메모리를 두 번 해제하는 오류(double free)가 발생하거나, 데이터가 의도치 않게 덮어씌워질 수 있습니다.
- 메모리 정렬 (Alignment)
- 컴파일러는 구조체의 각 멤버를 특정 단위(alignment unit)에 맞춰 정렬합니다.
- 각 멤버의 시작 주소(offset)는 해당 멤버 타입의 크기의 배수여야 합니다.
- 구조체 전체 크기는 가장 큰 멤버의 크기의 배수여야 합니다.
double 타입 멤버는 8바이트 정렬을 따르지만, 일부 컴파일러 설정에 따라 4바이트 단위로 정렬될 수도 있습니다.
- 중첩 구조체 메모리 정렬: 내부 구조체의 가장 큰 멤버를 기준으로 정렬이 이루어집니다.
- 강제 정렬:
#pragma pack (value) 를 사용하여 정렬 단위를 변경할 수 있습니다.
- 할당 단위는 구조체 내 가장 큰 기본 타입 크기와
value 중 작은 값으로 결정됩니다.
- 이 값을 기준으로 각 멤버의 오프셋이 결정됩니다.
비트 필드 (Bit-field)
- 비트 필드의 주소를 구할 수 없습니다. 메모리는 바이트 단위로 주소가 할당되기 때문입니다.
- 비트 필드의 총 비트 수는 해당 멤버의 타입이 가질 수 있는 비트 수를 초과할 수 없습니다.
- 표준적인 비트 필드의 타입은
unsigned int 이지만, signed/unsigned char, short, int, long 도 사용 가능합니다.
- 비트 필드에 값을 할당할 때는 필드의 크기를 초과하지 않도록 주의해야 합니다. 상위 비트는 무시됩니다.
- 비트 필드가
signed 타입으로 선언되면, 값을 읽을 때 부호 확장(sign extension)이 적용됩니다.
- 인접한 동일 타입의 비트 필드는 하나의 메모리 단위로 합쳐질(압축) 수 있습니다.
unsigned char : 0; 을 사용하면 다음 비트 필드가 새로운 메모리 단위에서 시작하도록 강제할 수 있습니다.
unsigned char : 2; 와 같이 이름 없는 비트 필드는 패딩(padding) 용도로 사용됩니다.
공용체 (Union)
모든 멤버가 동일한 메모리 공간을 공유합니다. 공용체의 전체 크기는 가장 큰 멤버의 크기로 결정됩니다.
열거형 (Enum)
변수가 가질 수 있는 값을 기호 상수(symbolic constant)로 정의합니다. 기본값은 0부터 시작하여 1씩 증가합니다. 중간에 값을 할당하면 이후 값은 그 값부터 증가합니다.
enum Color { RED, GREEN, BLUE };
enum Color myColor = RED; // C++에서는 열거형 이외의 값 할당 시 오류 발생 가능
2. 진법 변환과 입출력
진법별 입출력
표준 C 언어는 2진수 입력을 직접 지원하지 않지만, 일부 최신 컴파일러는 확장 기능을 제공합니다.
| 진법 |
입력 접두사 |
출력 형식 지정자 |
| 2진수 |
0b (컴파일러 확장) |
#include <bitset>, std::bitset<8>(x) |
| 8진수 |
0 |
%o (oct) |
| 10진수 |
(기본값) |
%d (dec) |
| 16진수 |
0x |
%x (hex) |
진법 변환 규칙
- 10진수 → 2/8/16진수: 2, 8, 16으로 나누는 '短除法' (연속 나눗셈)을 사용합니다.
- 2/8/16진수 → 10진수: 각 자리값에 자릿수만큼의 진법의 거듭제곱을 곱하여 더합니다.
- 2진수 ↔ 8진수: 소수점을 기준으로 2진수를 3자리씩 묶어 8진수로, 8진수 각 자리를 3자리 2진수로 변환합니다.
- 2진수 ↔ 16진수: 소수점을 기준으로 2진수를 4자리씩 묶어 16진수로, 16진수 각 자리를 4자리 2진수로 변환합니다.
- 8진수와 16진수 간 직접 변환: 2진수를 거쳐서 변환합니다.
3. 원점수, 1의 보수, 2의 보수
개념
- 원점수 (Sign-Magnitude): 부호 비트(최상위 비트, MSB)와 절대값으로 이루어진 가장 직관적인 표현 방식입니다.
- 1의 보수 (Ones' Complement): 양수는 원점수와 동일하며, 음수는 모든 비트를 반전(0→1, 1→0)하여 표현합니다.
- 2의 보수 (Two's Complement): 1의 보수에 1을 더하여 표현합니다. 현대 컴퓨터에서 음수를 저장하는 표준 방식입니다.
변환 규칙
- 부호 없는 정수: 원점수 = 1의 보수 = 2의 보수
- 양수 (부호 있음): 원점수 = 1의 보수 = 2의 보수
- 음수 (부호 있음):
- 1의 보수 = 원점수의 부호 비트는 그대로, 나머지 비트는 반전
- 2의 보수 = 1의 보수 + 1
2의 보수 표현의 의미
- 뺄셈을 덧셈으로 처리할 수 있게 합니다.
+0과 -0이라는 두 개의 0 표현을 하나로 통일합니다. (2의 보수에서는 0이 하나만 존재, 10000000은 -128을 의미)
데이터 저장
- 컴퓨터는 모든 정수를 2의 보수 형태로 메모리에 저장합니다.
- 8진수, 16진수는 일반적으로 부호 개념이 없으며, 메모리에 저장된 비트 패턴 그대로를 의미합니다.
- 예를 들어,
-10을 32비트 메모리에 저장하면 0xFFFFFFF6 (-10의 2의 보수) 형태로 저장됩니다.
데이터 읽기
- 부호 없는 변수: 메모리에 저장된 값을 그대로 읽어 옵니다.
- 부호 있는 변수: 메모리의 최상위 비트(MSB)를 검사합니다.
- MSB가 0이면: 양수로 간주하여 메모리 값을 그대로 읽습니다.
- MSB가 1이면: 음수로 간주하여 메모리 값에 대해 다시 2의 보수(보수 + 1)를 취한 결과를 읽습니다.
unsigned int data = -10; 의 경우
-10이 메모리에 0xFFFFFFF6로 저장됩니다. 이 값을 unsigned int로 읽으면 4294967286이라는 큰 양수로 출력됩니다. 하지만 signed int로 강제 형변환하여 읽으면 -10이 출력됩니다.
4. 주요 키워드
const
const로 선언된 변수는 읽기 전용(read-only)이 됩니다.
- 변수명을 통해 직접 값을 변경할 수는 없지만, 포인터를 통해 간접적으로 값을 변경할 수 있습니다. (단, 실제로 변경 가능한지는 변수가 저장된 메모리 영역에 따라 다릅니다.)
- 상수로 초기화된
const 변수 (C++): C++에서는 컴파일러가 const int data = 10; 과 같은 경우, 즉시 메모리를 할당하지 않고 값 10을 '상수 테이블'에 등록합니다. 사용자가 &data와 같이 주소를 참조하는 순간에 메모리가 할당됩니다.
- C와 C++의 차이: C 언어의
const 변수는 '읽기 전용 변수'로, 포인터를 통해 값을 변경하는 것이 문법적으로 가능하지만, C++에서는 '진정한 상수'로 취급되어 컴파일 타임에 값이 상수로 사용됩니다.
- 지역
const 변수: 스택 영역에 저장되므로 포인터로 값 변경이 가능할 수 있습니다. 하지만 일부 컴파일러는 최적화를 위해 const 변수 사용 시 메모리에서 값을 읽지 않고 상수 값을 직접 사용하기 때문에, 포인터로 값을 변경해도 반영되지 않는 것처럼 보일 수 있습니다.
- 전역
const 변수: 읽기 전용 데이터(.rodata) 세그먼트에 저장되어 수정이 불가능합니다. 포인터를 통해 값 변경을 시도하면 세그먼테이션 오류(Segmentation fault)가 발생합니다.
- 변수로 초기화된
const 변수: int b = 10; const int data = b; 와 같은 경우, 컴파일러는 data를 위한 메모리를 즉시 할당합니다.
const와 포인터
const int *p 또는 int const *p: p가 가리키는 값을 변경할 수 없습니다. (*p 읽기 전용)
int * const p: 포인터 p 자체를 변경할 수 없습니다. (다른 주소를 가리킬 수 없음, *p 변경 가능)
const int * const p: p가 가리키는 값과 p 자체 모두를 변경할 수 없습니다.
register
- 자주 사용되는 변수를 CPU 레지스터에 저장하도록 컴파일러에 힌트를 주는 키워드입니다.
- 컴파일러가 판단하여 레지스터에 저장하지 않을 수도 있습니다.
- 레지스터 변수의 주소는 구할 수 없습니다. (C++에서는 컴파일러가 자동으로 일반 변수로 변경할 수 있음)
volatile
- 컴파일러의 최적화를 방지하여, 변수를 읽거나 쓸 때마다 항상 메모리에 직접 접근하도록 지시합니다.
- 주로 하드웨어 레지스터나 인터럽트 서비스 루틴(ISR)에서 변경되는 변수에 사용됩니다.
sizeof
sizeof 연산자는 데이터 타입이나 변수의 메모리 크기(바이트 단위)를 반환합니다.
- 문자열의 길이를 반환하는
strlen과는 다릅니다.
sizeof('a') 는 C 언어에서 문자 상수 'a'를 int 타입으로 간주하여 4를 반환합니다.
typedef
- 기존 데이터 타입에 새로운 이름(별칭)을 부여합니다. 새로운 타입을 생성하는 것은 아닙니다.
- 별칭은 보통 대문자로 작성하여 가독성을 높입니다.
- 사용 방법
- 기존 타입을 사용하여 변수를 선언합니다. 예:
int myArray[5];
- 변수명을 원하는 별칭으로 바꿉니다. 예:
int ArrayType[5];
- 맨 앞에
typedef 키워드를 추가합니다. 예: typedef int ArrayType[5];
- 사용 예:
typedef int ARRAY_TYPE[5]; ARRAY_TYPE data = {1,2,3,4,5};
static
- 지역 변수에 사용: 함수 내에서
static으로 선언된 변수는 프로그램이 시작될 때 한 번만 초기화되며, 함수 호출이 끝나도 메모리에 남아 값을 유지합니다.
- 전역 변수/함수에 사용: 선언된 소스 파일 내에서만 접근 가능하도록 범위를 제한합니다. (내부 연결, internal linkage)
5. 연산자
산술 연산자
/: 두 정수 간 연산 시 몫을 반환하고, 실수가 포함되면 일반적인 나눗셈을 수행합니다.
%: 나머지 연산자. 피연산자는 정수만 가능합니다.
+=, -= 등 복합 대입 연산자: a *= b + 5 는 a = a * (b + 5) 와 동일합니다.
관계 연산자
>, <, ==, >=, <=, != 는 값의 크기를 비교합니다. 문자열의 주소 비교가 아닌 내용 비교를 위해서는 strcmp() 함수를 사용해야 합니다.
논리 연산자와 단축 평가 (Short-circuit Evaluation)
&& (논리 AND): 첫 번째 피연산자가 false이면 두 번째 피연산자를 평가하지 않고 false를 반환합니다.
|| (논리 OR): 첫 번째 피연산자가 true이면 두 번째 피연산자를 평가하지 않고 true를 반환합니다.
! (논리 NOT): 0이 아닌 값은 true로 간주합니다.
비트 연산자
| 연산자 |
규칙 |
용도 |
예시 |
& (AND) |
두 비트가 모두 1일 때만 1 |
특정 비트를 0으로 클리어 |
data &= ~(1 << 3); (3번 비트를 0으로) |
| (OR) |
두 비트 중 하나라도 1이면 1 |
특정 비트를 1로 세트 |
data |= (1 << 5); (5번 비트를 1로) |
^ (XOR) |
두 비트가 서로 다르면 1 |
특정 비트를 토글 |
data ^= (1 << 2); (2번 비트를 반전) |
시프트 연산자
- 음수에 대한 오른쪽 시프트(
>>)는 산술 시프트(arithmetic shift)가 적용되어 최상위 비트(부호 비트)가 채워집니다.
- 시프트할 비트 수가 피연산자의 비트 폭보다 크면, 실제 시프트되는 비트 수는 피연산자의 비트 폭으로 나눈 나머지 값입니다.
삼항 연산자
condition ? value_if_true : value_if_false 형태로 사용합니다.
증감 연산자
- 단독 문장으로 사용될 때는
++i와 i++의 결과가 동일합니다.
++i (전위, prefix): 값을 먼저 증가시킨 후, 증가된 값을 식에 사용합니다.
i++ (후위, postfix): 현재 값을 식에 먼저 사용한 후, 값을 증가시킵니다.
쉼표 연산자
가장 낮은 우선순위를 가지며, 왼쪽에서 오른쪽으로 순차적으로 평가되고 마지막 표현식의 결과가 전체 결과가 됩니다.
int num;
num = 10, 20, 30, 40; // num은 10 (대입 연산자가 쉼표 연산자보다 우선)
num = (10, 20, 30, 40); // num은 40 (괄호 안에서 쉼표 연산자가 평가됨)
6. 유용한 표준 라이브러리 함수
난수 생성
| 함수 |
헤더 |
기능 |
time_t time(time_t *tloc) |
<time.h> |
1970년 1월 1일 이후 경과된 초 단위 시간을 반환합니다. |
void srand(unsigned int seed) |
<stdlib.h> |
난수 발생기의 시드(seed) 값을 설정합니다. 루프 안에 두지 않습니다. |
int rand(void) |
<stdlib.h> |
0부터 RAND_MAX 사이의 의사 난수(pseudo-random)를 반환합니다. |
#include <stdlib.h>
#include <time.h>
void generate_random() {
srand(time(NULL)); // 시간을 시드로 사용
int r1 = rand();
int r2 = rand();
printf("Random numbers: %d, %d\n", r1, r2);
}
동적 메모리 관리 (<stdlib.h>)
| 함수 |
기능 |
malloc(size_t size) |
지정된 크기(size 바이트)만큼 메모리를 할당합니다. 초기화되지 않은 상태입니다. |
calloc(size_t nmemb, size_t size) |
nmemb 개의 요소 각각 size 바이트 크기만큼 메모리를 할당하고, 모든 비트를 0으로 초기화합니다. |
realloc(void *ptr, size_t size) |
이전에 할당된 메모리 블록의 크기를 size로 변경합니다. 확장 또는 축소 가능합니다. |
free(void *ptr) |
이전에 할당된 메모리 블록을 해제합니다. |
문자열 처리 함수 (<string.h>)
| 함수 |
기능 |
strlen(str) |
문자열의 길이를 반환합니다. (널 문자 제외) |
strcpy(dest, src) |
문자열을 복사합니다. |
strncpy(dest, src, n) |
문자열의 앞 n개 문자를 복사합니다. |
strcat(dest, src) |
문자열을 이어 붙입니다. |
strncat(dest, src, n) |
문자열의 앞 n개 문자를 이어 붙입니다. |
strcmp(s1, s2) |
두 문자열을 비교합니다. (사전순) |
strchr(str, c) |
문자열에서 특정 문자(c)를 왼쪽부터 검색합니다. |
strrchr(str, c) |
문자열에서 특정 문자(c)를 오른쪽부터 검색합니다. |
strstr(haystack, needle) |
문자열 haystack에서 문자열 needle이 처음 나타나는 위치를 찾습니다. |
메모리 조작 함수 (<string.h>)
| 함수 |
기능 |
memset(ptr, value, num) |
메모리 블록을 특정 값(value)으로 채웁니다. 주로 초기화에 사용됩니다. |
memcpy(dest, src, num) |
메모리 블록을 다른 블록으로 복사합니다. (메모리 영역이 겹치면 안 됨) |
memcmp(ptr1, ptr2, num) |
두 메모리 블록의 첫 num 바이트를 비교합니다. |
문자열과 숫자 변환 (<stdlib.h>)
| 함수 |
기능 |
atoi(str) |
문자열을 int 타입으로 변환합니다. |
atol(str) |
문자열을 long 타입으로 변환합니다. |
atof(str) |
문자열을 double 타입으로 변환합니다. |
문자열 포맷 함수 (<stdio.h>)
sscanf와 %s 포맷은 공백, 개행, 널 문자를 만나면 읽기를 중단합니다.
- 포맷 지정 옵션
%[width]d / %[width]s: 지정된 너비만큼만 데이터를 읽습니다.
%*d / %*s: 데이터를 읽지만, 변수에 저장하지 않고 건너뜁니다.
%*[width]d / %*[width]s: 지정된 너비만큼 읽고 건너뜁니다.
%[a-z]: 주어진 문자 집합(a부터 z)에 포함된 문자만 읽습니다.
%[aBc]: 주어진 문자들(a, B, c) 중 하나와 일치하는 문자만 읽습니다.
%[^aFc]: 캐럿(^) 다음에 나오는 문자(a, F, c)를 제외한 문자를 읽습니다.
%[^a-z]: a부터 z를 제외한 문자를 읽습니다.
| 함수 |
기능 |
sprintf(buffer, format, ...) |
포맷된 문자열을 buffer 배열에 출력(저장)합니다. |
fprintf(stream, format, ...) |
포맷된 문자열을 파일 스트림(stream)에 출력합니다. |
sscanf(buffer, format, ...) |
buffer 배열에서 포맷에 맞게 데이터를 읽어 변수에 저장합니다. |
fscanf(stream, format, ...) |
파일 스트림(stream)에서 포맷에 맞게 데이터를 읽어 변수에 저장합니다. |
파일 입출력 함수 (<stdio.h>)
- 텍스트 모드(
"t")로 파일을 읽을 때는 EOF가 파일의 끝을 나타냅니다.
- 바이너리 모드(
"b")로 파일을 읽을 때는 feof() 함수를 사용하여 파일의 끝을 확인합니다.
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("Error opening file");
return -1;
}
// 파일 읽기/쓰기 작업 수행
fclose(fp); // 파일 닫기
| 함수 |
기능 |
fopen(filename, mode) |
파일을 열고 FILE* 포인터를 반환합니다. |
fclose(fp) |
열린 파일을 닫습니다. |
fputc(c, fp) |
파일에 한 문자를 씁니다. |
fgetc(fp) |
파일에서 한 문자를 읽습니다. |
feof(fp) |
파일 포인터가 파일의 끝에 도달했는지 확인합니다. (도달 시 0이 아닌 값 반환) |
fputs(str, fp) |
파일에 문자열을 씁니다. (성공 시 0, 실패 시 EOF 반환) |
fgets(str, n, fp) |
파일에서 최대 n-1개의 문자를 읽어 str에 저장합니다. (실패 시 NULL 반환) |
fwrite(ptr, size, nmemb, fp) |
메모리 블록(ptr)에서 파일로 데이터를 씁니다. 반환값은 성공적으로 쓴 '항목의 개수'입니다. |
fread(ptr, size, nmemb, fp) |
파일에서 데이터를 읽어 메모리 블록(ptr)에 저장합니다. 반환값은 성공적으로 읽은 '항목의 개수'입니다. |
fprintf(fp, format, ...) |
포맷된 데이터를 파일에 씁니다. |
fscanf(fp, format, ...) |
파일에서 포맷된 데이터를 읽습니다. |
rewind(fp) |
파일 위치 지시자를 파일의 시작으로 되돌립니다. |
ftell(fp) |
현재 파일 위치 지시자의 위치(파일 시작부터의 바이트 단위 오프셋)를 반환합니다. |
fseek(fp, offset, whence) |
파일 위치 지시자를 특정 위치로 이동시킵니다. |
fseek 함수 상세
int fseek(FILE *stream, long offset, int whence);
// whence 값:
// SEEK_SET (0): 파일의 시작점을 기준으로 offset만큼 이동 (offset은 양수)
// SEEK_CUR (1): 현재 위치를 기준으로 offset만큼 이동
// SEEK_END (2): 파일의 끝을 기준으로 offset만큼 이동 (offset은 음수)