Week 05 주차를 마무리 하며 이번주 공부 한것을 적어 내려가려 한다.
이번주 부터는 C언어를 시작하고 RB트리(Red-Black Tree)를 구현하는 과제가 주어졌다. 이 과제를 진행하면서 우선 내가 C언어를 처음 시작하고 부족한 부분을 모두 채워서 RB트리를 구현해내는건 현실적으로 불가능에 가깝다,, 하지만 C언어를 처음 하는 내 입장에선 단순히 언어를 배운다기 보단 정말 베이직한 부분들을 다루는게 정말 많다. 그래서 난 최대한 절대적인 시간을 들여서 C언어를 공부 할거 같다. 최소한 C를 알고 있다면 언어를 그만큼 알고 익숙하다는 뜻이니 개념 공부를 하면서 C언어 문법이나 자료구조등을 구현하는 연습을 같이한다면 개념공부도 하면서 구현에 들이는 시간과 익숙해지는 시간을 최대한 단축시키면서 할수 있을것 같다 라는 생각이 들었다. 직접적인 구현에 들어 가기 앞서 간단하게 노션에 정리된 3분PT- C언어를 보면서 개념들을 정리 하고 추가적인 참고 자료들도 같이 넣을 예정이다. 이번주도 내가 최선을 다하고 있는지 다시 한번 돌아보고 계속 발전해 나가는 개발자가 되고 싶다.
이번주 C언어 공부 내용 목차를 정리해보자면 다음과 같다:
3분PT - C언어참고: https://www.notion.so/kraftonjungle/3-PT-C-4994f6fe77b74e98a126e861990d40ba#84e3f1b668184562b1e94677f53218c8
프로그램 구조
다음은 "Hello World"를 출력하는 간단한 C코드입니다.
#include <stdio.h>
/* 여러줄 커맨트는
이렇게 적으세요 */
int main() {
// 한줄 커맨트는 이렇게 적으세요
printf("Hello, World! \n");
return 0;
}
리눅스에서 C소스 파일을 컴파일을 하고, 실행하는 방법입니다.
$ gcc hello.c
$ ./a.out
Hello, World!
# 실행파일명 지정하기
$ gcc hello.c **-o hello.out**
다음은 두개의 명령문 형태는 의미적(semantic)으로 같습니다.
// 명령문1
printf("Hello, World! \n");
return 0;
// 명령문2
printf("Hello, World! \n"); return 0;
다음은 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
자료형
C의 다양한 정수형들은 각기 그 크기가 다르다. int
형은 주로 32비트지만, 오래된 CPU에서는 16비트일 수도 있습니다. 몇몇 프로그램은 int
에서 저장할 수 없을 만큼 큰 숫자를 필요로 할 때가 있습니다. 이를 위한 C 언어의 자료형이 바로 long
정수형입니다. 반대로 컴파일러에게 int
보다 메모리 공간을 덜 먹는 자료형을 사용하게 지시해줘야할 때가 있습니다. 이럴 때 사용하는 정수형은 short
정수형입니다.
타입 | 크기(bytes) | 값의 범위 |
---|---|---|
char | 1 | unsigned OR signed |
unsigned char | 1 | 0 to 2^8-1 |
signed char | 1 | -2^7 to 2^7-1 |
int | 2 / 4 | unsigned OR signed |
unsigned int | 2 / 4 | 0 to 2^16-1 OR 2^31-1 |
signed int | 2 / 4 | -2^15 to 2^15-1 OR -2^31 to 2^32-1 |
short | 2 | unsigned OR signed |
unsigned short | 2 | 0 to 2^16-1 |
signed short | 2 | -2^15 to 2^15-1 |
long | 4 / 8 | unsigned OR signed |
unsigned long | 4 / 8 | 0 to 2^32-1 OR 2^64-1 |
signed long | 4 / 8 | -2^31 to 2^31-1 OR -2^63 to 2^63-1 |
long long | 8 | unsigned OR signed |
unsigned long long | 8 | 0 to 2^64-1 |
signed long long | 8 | -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) | 값의 범위 |
---|---|---|
float | 4 | ±1.2×10^-38 to ±3.4×10^38 |
double | 8 / 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
)
타입 | 크기(bytes) | 값의 범위 |
---|---|---|
int8_t | 1 | -2^7 to 2^7-1 |
uint8_t | 1 | 0 to 2^8-1 |
int16_t | 2 | -2^15 to 2^15-1 |
uint16_t | 2 | 0 to 2^16-1 |
int32_t | 4 | -2^31 to 2^31-1 |
uint32_t | 4 | 0 to 2^32-1 |
int64_t | 8 | -2^63 to 2^63-1 |
uint64_t | 8 | 0 to 2^64-1 |
bool | 1 | true / false or 0 / 1 |
일반적으로 void는 없다 또는 알수없는 상태를 의미합니다. void를 쓸수 있는 3가지 경우는 다음과 같습니다.
프로그램 실행중에 고정되어 변하지 않는 값을 리터럴이라고 합니다.
0b1111 // 이진수 1111 -> 리눅스계열에서만 가능
0B1111 // 이진수 1111 -> 리눅스계열에서만 가능
0377 // 8진수 377
255 // 10진수
0xff // 16진수 FF
0xFF // 16진수 FF
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;
// 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
식별자는 변수, 함수등의 이름을 말합니다. 식별자는 A..Z, a..z, 로 시작할수 있으며, 그다음은 A..Z, a..z, , 0..9 문자로 구성될 수 있습니다.
// 가능한 식별자
mohd
zara
abc
move_name
a_123
myname50
_temp
j
a23b9
retVal
// 불가능한 식별자
2022abc // 숫자로 시작할수 없음
while // 예약어는 식별자가 될수 없음
스토리지 클래스는 변수와 함수의 스쿠프(scope - visibility)와 생명주기(life-time)을 정의합니다.
auto는 모든 지역 변수를 위한 디폴트 스토리지 클래스입니다.
auto는 함수내에서만 사용될수 있으면, 생명주기가 함수의 호출/종료까지로 제한됩니다.
함수내의 변수는 스택에 위치하므로, 함수호출 종료시 팝되게 되어 있습니다
다음 두가지 변수정의는 동일합니다
{
int mount;
auto int month;
}
register는 지역변수가 RAM이 아니라, 레지스터에 정의하는데 사용합니다.
따라서 해당 변수의 크기는 레지스터 크기와 동일합니다(일반적으로는 word).
또한 '&'와 같이 메모리 주소를 참조하는 연산을 사용할수 없습니다.
레지스터는 카운터와 같이 빠른 액세스가 필요한 변수에 대해서 유용합니다.
{
register int miles;
}
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
#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
int a = 10;
int b = 2;
a + b
a - b
a * b
a / b
a % b // a를 b로 나눈 나머지
a++
b--
++a
--b
4 > 2 # true 크다
5 < 1 # false 작다
6 >= 5 # true 크거나 같다
4 <= 4 # true 작거나 같다
3 == 5 # false 같다
4 != 7 # true 같지 않다
p | q | p & q | p | q |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
a = true;
b = false;
(a && b) # false 모두 참이어야 참을 반환한다.
(a || b) # true 둘 중 하나만 참이면 참이다.
!(a && b) # true 모두 참인게 아니면(not)
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
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)* & sizeof | Right 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 |
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
여러개의 타입을 하나의 메모리 공간에 위치시킬때 사용합니다. 이때 메모리의 크기는 가장 큰 타입의 크기와 같습니다.
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
#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
숫자형 상수들을 하나의 데이터 타입으로 표현할때 사용합니다.
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
길어진 타입 정의 또는, 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;
모든 변수는 메모리에 위치합니다. 이때, 메모리 위치를 메모리주소(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값을 담고 있는 메모리라고 알려줌. 에러 안남
int* ptr = NULL; // ptr은 NULL 아무 주소도 할당되어 있지 않다
// NULL = 0
if (ptr) { ... } // ptr에 주소값이 NULL이 아닌값이 할당된 경우
if (!ptr) { ... } // ptr에 NULL로 할당되어 있는 경우
배열은 같은 타입의 요소가 연속적으로 고정된 길이만큼 존재하는 자료구조를 말합니다.
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차원 배열이며, 항상 마지막이 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개의 문자만 비교
// 리턴타입 함수명(인자리스트)
int max(int num1, int num2) {
// 지역 변수 정의
int result;
...
return result;
}
다음은 전방 선언의 예입니다.
#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는 아규먼트(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함수의 원형
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
// if
if( a > 20 ) {
...
}
// if - else
if (a > 20) {
...
} else {
...
}
// if - else if - else
if (a > 20) {
...
} else if (a > 10) {
...
} else {
...
}
switch (a) {
case 10: // a가 10일때
...
break; // 만약 break를 안쓰면 실행이후에도 다음을 실행
case 20: // a가 20일때
...
break; // 만약 break를 안쓰면 실행이후에도 다음을 실행
default: // optional, 그외의 경우
statement(s);
}
int i = 1;
while (i <= 10) {
...
i++;
}
for (int i = 1; i <= 10; i++) {
...
}
int i = 1;
do {
...
i++;
} while (i <= 10);
int i = 0;
while (1) {
if (x == 10) // 조건을 만족하면 루프를 탈출
break;
...
i++;
}
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) // 조건에 만족하면, 다음스텝 실행
continue;
...
}
LOOP: do {
if(a == 15) { // 조건에 만족한다면
a = a + 1;
goto LOOP; // LOOP레이블이 있는 위치로 점프
}
...
a++;
} while( a < 20 );
for( ; ; ) {
printf("This loop will run forever.\n");
}
함수내에서 정의된 변수를 지역변수라고 부릅니다. 지역변수들은 정의된 함수의 내부에서만 보여지고(visibility), 함수 외부에는 보여질수 없습니다. 또한 함수가 종료되면, 지역변수를 위해서 할당된 메모리 공간도 사라지게 됩니다. 만약 지역변수를 static으로 선언하게 되면, 함수가 종료되더라도 메모리 공간은 그대로 유지되게 됩니다.
전역변수는 정의된 파일의 전체 구간에서 접근할수 있는 변수입니다. 기본적으로는 정의된 파일에서만 접근이 가능하며, 다른 파일에서 해당 전역변수를 extern으로 선언하면, 다른파일에서도 해당 전역변수를 접근할 수 있습니다.
함수의 선언에 사용되는 인자(argument)와 실제 호출시에 전달되는 인자(parameter)중에서 파라미터는 함수의 호출시 값이 복사되며, 복사된 값은 함수실행중에만 함수 내부에서만 보여지고, 함수종료시 해당 메모리는 사라집니다.
C프로그램이 메모리에 적재(load)되면, 위의 그림처럼 메모리가 구성됩니다. 그림에서 위쪽으로 갈수록 주소값이 커집니다.
더 자세한 내용은 메모리구조, 데이터블럭을 구글링 해봅시다. 다음은 변수 사용 사례별, 메모리 위치를 커맨트로 설명하고 있습니다.
#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;
...
}
프로그램에서 배열을 정의할때, 배열의 정확한 크기를 모를때, 포인터와 메모리할당(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);
}
// 기존에 할당된 배열의 크기를 재조정합니다
name2 = realloc( name2, 300 * sizeof(char) );
...
// malloc으로 할당한 메모리가, 더이상 필요없어질때 메모리에서 소거(해제)합니다
free(name2);
C프로그래밍에서 다음 장치(키보드,화면출력,에러등)는 파일로 취급됩니다. 이들 파일(장치)는 프로그램이 실행될때, 자동으로 파일이 열려있고, 다음의 파일포인터를 이용해서 접근할수 있습니다.
Standard File | File Pointer | Device |
---|---|---|
Standard input | stdin | Keyboard |
Standard output | stdout | Screen |
Standard error | stderr | Your screen |
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;
}
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;
}
// 파일열기
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. |
long ftell(fptr); // 파일에서 position을 반환
fseek(fptr, offset, origin); // 파일의 origin 위치에서 offset만큼 위치로 이동
fseek(fptr, -10L, SET_END); // 파일의 끝에서 10번째 이전 위치로 position 이동
origin:
- SEEK_SET 파일의 시작
- SEEK_CUR 파일에서 현재 위치
- SEEK_END 파일의 끝
C 전처리는 컴파일러의 부분은 아닙니다. 컴파일 과정중의 하나입니다. 컴파일러가 컴파일하기전에 단순한 텍스트를 교체 과정으로 생각하면 됩니다.
전처리 명령어(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))
여러 파일에서 #include가 되면, 같은 내용들이 중복으로 포함될수 있습니다. 이를 방지하기 위해서 다음과 같은 방식으로 한번만 포함되도록 할수 있습니다.
#ifndef MYHEADER_H
#DEFINE MYHEADER_H
...
#endif
#include "myheader.h". // 여기에서만 한번 myheader.h의 내용이 포함됩니다
// 이때 MYHEADER_H 를 define합니다
...
#include "myheader.h" // MYHEADER_H가 define 되어 있으므로, 다시 포함되지 않습니다
여러개의 조건에 맞게 헤더파일들이 포함될수 있도록 할수 있습니다.
#if ANDROID_13
#include "support_lib_13.h"
#elif ANDROID_12
#include "support_lib_12.h"
#elif ANDROID_11
...
#endif
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 을 반환
}
함수에 인자의 개수가 가변적으로 변하는 경우에 대해서, 가변적 부분은 …로 선언합니다. 그리고, 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));
}