#include <stdio.h>
static float s_gas = 500.0f;
void honk(void)
{
printf("Honk~ honk~");
}
void reduce_gas(float consumed_gas)
{
s_gas -= consumed_gas;
}
(1) C는 접근 제어자가 없음 -> 기본적으로 모두 전역 함수 (즉, 어디서나 호출 가능)
void print_score(int score)
{
printf("Score: %d\n", score);
}
/*
컴파일 오류
void print_score(float gpa)
{
printf("Score: %f\n", gpa);
}
*/
void print_gpa(float gpa)
{
printf("Score: %f\n", gpa);
}
(2) C는 함수 오버로딩이 없음
함수의 구현체 없이 함수 원형(prototype)만 선언해 주는 것
함수 원형은 다음의 사항들을 명시
(1) 함수의 이름
(2) 반환형
(3) 매개변수들의 자료형
(참고) 함수 정의(definition)는 실제로 함수를 구현해 놓은 것
어떻게 동작할까?
컴파일러는 전방 선언 되어 있는 함수 선언을 보고 실제 어디로 가서 코드를 찾아야 하는지 모르니 구멍으로 남기고, link 단계에서 실제 코드 위치를 찾아서 그 구멍을 메꿔주는 방식으로 작동한다.
int main(void)
{
int num1 = 128;
int num2 = 256;
printf("%d, %d\n", add(num1, num2), subtract(num1, num2));
return 0;
}
표준에 따르면, 함수 매개변수의 평가 순서는 명시되어 있지 않음(unspecified)
즉, 컴파일러에 따라 평가 순서가 달라질 수 있음
printf()가 실제로 실행되기 전에 add()와 subtract()가 호출된다는 것은 보장
그러나 누가 먼저 호출되는지는 컴파일러 마음임
-> 실행 순서가 중요한 경우에는 문제가 될 수 있음
예를 들어 인자가 두 개인 경우 아래와 같이 여러 방법으로 평가가 될 수 있음
(1) 첫 번째 인자가 먼저 평가될 경우
(2) 두 번째 인자가 먼저 평가될 경우
(3) 동시에 평가될 경우
즉, 기본적으로 한 줄에서 동일한 변수를 여러 번 바꾸면 위험함
/* undefined behavior의 예 */
add(++i, ++i);
add(i = -1, i = -1);
add(i, i++);
int main(void)
{
int num1 = 10;
int num2 = 20;
int result = add(num1, num2) + subtract(num1, num2) * divide(num1, num2);
return 0;
}
=, +, * 등의 연산자들은 sequence point가 아님
즉, 연산자 우선순위와 평가 순서는 서로 아무 연관이 없음
평가 순서는 sequence point의 영향만 받는다.
int i = 0;
int j = 0;
int k = 0;
if(++i || ++j && ++k)
{
printf("true!\n");
}
printf("%d, %d, %d\n", i, j, k);
/* 출력 결과
true!
1, 0, 0 */
우선순위 상으로는 &&가 ||보다 높지만, ||와 &&는 sequence point 이므로 || 부터 동작한다.
||는 왼쪽과 오른쪽 중 하나만 참이면 되므로, 뒷부분을 동작시킬 필요가 없다.
-> short circuit
정리
한 줄에 있는 피연산자들은 기본적으로 평가 순서가 보장되지 않음
sequence point : ||, &&, ;, 삼항 연산자 등
블록 범위 {} 안에 선언한 것들은 그 블록 안에서만 사용 가능
블록 안에 또 다른 블록을 넣을 수 있음
- 그러면 안쪽 블록은 바깥 블록에 접근 가능
- 그 반대는 안 됨
/* 컴파일 오류 나는 코드 */
int main(void)
{
int num1 = 10;
printf("num: %d\n", num1);
int num2 = 100;
int result = num1 + num2;
printf("result: %d\n", result);
return 0;
}
/* 컴파일 되는 코드 */
int main(void)
{
int num1 = 10;
printf("num: %d\n", num1);
{
int num2 = 100;
int result = num1 + num2;
printf("result: %d\n", result);
}
return 0;
}
(1) 함수 중간에 블록을 열고 변수 선언 가능
(2) 함수 시작 지점에서 모든 변수를 선언하면 실수할 여지가 있음
- 정확하게 어디서 사용하는 변수인지 파악 불가능
- 중간에 그 값이 바뀔 수도 있음
-> 블록을 이용해서 함수 중간에 선언하는 것도 하나의 방법
코딩 표준 : 변수 가리기(variable shadowing) 금지
모든 변수 이름을 다르게 지어야 함
#include <stdio.h>
/* 안 좋은 코드 */
int main(void)
{
int num = 0;
printf("%d", num); /* 0 */
{
int num = 1;
printf("%d", num); /* 1 */
}
return 0;
}
/* 좋은 코드 */
int main(void)
{
int your_score = 0;
{
int my_score = 1;
printf("%d", my_score);
}
return 0;
}
어떤 블록이나 매개변수 목록에도 안 속하고 파일 안에 있는 것
#include <stdio.h>
static int s_num = 1024;
int add(int op1, int op2);
int main(void)
{
s_num = add(10, 30);
return 0;
}
다른 소스코드 파일에서 링크 가능하고, 프로그램 실행 동안 공간을 차지하므로
스택 메모리가 아닌 데이터 섹션에 들어감 (전역 변수)
유일한 예 : 레이블(lable)
goto 같은 데서 쓰는 것
함수 안에서 선언된 레이블은 함수 어디에서라도 접근 가능
-> 다른 범위들은 위에서 선언된 것만 접근 가능했으나 아래와 같이 밑에 선언된 것을 위에서 사용 가능
#include <stdio.h>
int main(int argc, char** argv)
{
if (argc != 3)
{
goto exit;
}
printf("You have 3 arguments!");
exit:
return 0;
}
함수 선언의 매개변수 목록에 있는 것은 그 목록 안에서 접근 가능
void do_something(double value, char array[10 * sizeof(value)]);
// 상동
void do_something(
double value, /* 함수 선언 범위 */
char array[10 * sizeof(value)] /* value는 첫 번째 매개변수 */
);
상수를 만들 때 사용
기본적으로 모든 변수에 const를 붙이고, 정말 값 변경이 필요한 변수에만 const 생략하는 것이 좋음
/*
const int NUM_CARS = 30;
NUM_CARS = 10; // 컴파일 에러
*/
int caculate_risk(const int id) /* 복사해온 매개 변수 id는 못 바꿔 */
{
int age = db_get_age(id);
int amount;
id *= 2; /* 컴파일 오류 */
amount db_get_deposit_amount(id);
return risk;
}
void updatedimension(int w, int h, int data[])
{
int i = 0;
const int area = w * h;
area = area + 1; /* 컴파일 오류 */
for (i = 0; i < area; ++i)
{
data[i] = 1;
}
}
goto <lable_name>;
...
<lable_name> :
C는 위에서 아래로 순차적으로 코드를 실행함
goto를 쓰면 이 순서를 어기고 다음에 실행할 코드를 마음대로 지정 가능
같은 함수 내에 있는 레이블(lable)로 점프함
그러나 스파게티 코드 방지를 위해 전방(아래쪽)으로만 사용 권장
goto의 나쁜 예
void do_work(void)
{
infinity:
printf("work time!\n");
goto infinity;
}
/* 상동 */
void do_work(void)
{
while (1)
{
printf("work time!\n");
}
}
자료구조 스택과 헷갈리면 안됨
둘 다 작동방법이 동일해서(LIFO) 스택이란 이름을 쓸 뿐
각 함수에서 사용하는 지역 변수 등을 임시적으로 저장하는 공간
스택 메모리의 크기는 프로그램 빌드 시에 결정되고, 위치는 실행 시에 결정됨
(참고)
모든 기본 자료형 변수(char, int, float)를 new없이 사용할 수 있었던 이유는 스택 메모리에 할당됐기 때문임
기본 자료형을 함수 매개변수로 전달하면 스택에 복사본을 만듦(값형)
스택 메모리를 빌리고 반환할 때마다 언제나 빈 공간 없이 차곡차곡 쌓여 있음
new로 만든 데이터는 힙(heap)메모리에 할당됨
int add(const int a, const int b)
{
int res = a + b;
return res;
}
int main(void)
{
int a = 1;
int b = 2;
return add(a, b);
}
스택의 크기가 1MB일 때, 정해진 메모리보다 더 많은 양을 사용하는 경우 스택 오버플로우 발생
int value[30];
size_t array_size = sizeof(values);
/* 방법 1 */
const size_t num_vals = sizeof(values) / sizeof(values[0]);
/* 방법 2 */
/* 함수 밖에서 */
# define ARRAY_LENGTH(arr) (sizeof(arr) / sizeof(arr[0]))
/* 매크로 함수 사용 */
const size_t num_vals2 = ARRAY_LENGTH(values);
배열 전체 바이트 크기를 구하고 그 크기를 첫번째 배열 사이즈로 나누면 배열 크기가 나옴
배열 생성 시 모든 값을 0으로 초기화 하는 방법은? -> 0 뒤에 쉼표를 찍자
int nums[10] = { 0, };
int buffer[2] = { 2, 2 };
int buffer2[2] = { 1, 1 };
size_t i;
for(i = 0; i <= 2; ++i)
{
buffer2[i] = 0;
}
배열의 크기는 2인데 for문 내에서 i를 2 이하로 받음으로서 i가 3까지 증가 가능해짐
-> 쓰레기 값을 불러 들여 버퍼 오버플로우 발생
C 프로그램의 빌드 과정
빌드 : 사람이 읽기 쉬운 소스코드를 기계어 명령어로 변환하는 과정
그리고 그 명령어들을 모아 기계에서 실행 가능한 실행파일로 만드는 과정
C의 빌드는 4단계로 나뉘어져 있음
(1) 전처리 단계
보통 전처리기라는 별도의 프로그램이 담당
- 입력 : c 파일 하나
- 출력 : 확장된 소스코드(컴파일의 기본 단위인 트랜슬레이션 유닛)
- 주석 제거
- 매크로 확장(복붙)
- 인클루드 파일들 확장
(2) 컴파일 단계
컴파일러라는 프로그램이 담당
- 입력 : 트랜스레이션 유닛
- 출력 : 어셈블리어 코드
- 어셈블리어 코드는 아직 정의를 모르는 심볼(함수나 변수의 이름)을 사용할 수 있음
-> 헤더를 통한 선언만으로 컴파일이 가능한 이유임
어셈블리어 코드가 나왔다는 의미는?
이 단계 이후부터 코드는 특정 플랫폼(기계)에서만 동작한다는 이야기
C가 크로스 플랫폼이라는 주장은 컴파일되기 전까지만
(여러 플랫폼에서 돌지만 어셈블리어로 컴파일 되는 순간 그 기계에서만 도는 코드가 됨)
또한 타겟 플랫폼이 몇 비트냐도 이미 결정된 후임(32, 64비트)
(3) 어셈블 단계
어셈블러라는 프로그램이 담당
- 입력 : 어셈블리 코드
- 출력 : 오브젝트 코드(기계가 곧바로 이해 가능한 기계 코드, 즉 이진 코드)
- 어셈블리어 코드와 마찬가지로 여전히 메꿔야 하는 구멍이 있음
(4) 링크 단계
링커라는 프로그램이 담당
- 입력 : 모드 오브젝트 코드들
- 출력 : 최종 실행파일(.exe, .out)
링커는 모든 오브젝트 코드들을 모아다 구멍을 메꾼 뒤 실행파일로 저장
/* adder.h */
int add(const int a, const int b);
/* adder.c */
#include "adder.h"
int add(const int a, const int b)
{
return a + b;
}
/* main.c */
#include "adder.h"
int main(void)
{
const int res = add(1, 2);
return 0;
}
실제 프로그램을 돌게 하는 로직 코드를 저장해 두는 파일
내용물
- 함수 정의(= 함수 구현)
- 전역 변수 등
- 매크로
여러 소스코드 파일에 공통적으로 필요한 것들을 저장해 두는 파일
내용물
- 함수 선언
- 매크로
- extern 변수 선언 등
#include로 포함함 (예) #include "adder.h"
main.c를 컴파일하려면 add() 함수의 원형(함수의 선언)을 알아야 함
"내가 add라는 함수를 어딘가에 만들어 놓을게 필요하면 너가 가져다가 써~"
동일한 함수를 여러 곳에서 써야한다면 복붙을 해야할까?
-> 헤더파일을 사용한다.
#include "action.h"
walk(speed, dir);
/* player.c, monster.c에서 가져다 쓸 수 있음 */
함수 선언만 가지고 어떻게 프로그램이 돌까?
빌드가 여러 단계로 쪼개져 있는 이유가 바로 정의 없이 선언만 가지고도 컴파일이 되게 하기 위해서임
그리고 실제 올바른 기능 호출은 링크 단계가 책임짐
인클루드 하는 법은 두 가지
이 둘의 차이는 디스크 상의 어디에서 헤더 파일을 찾느냐 차이
(1) <>
시스템 경로에서만 헤더 파일을 검색
- 보통 컴파일러가 제공하는 시스템 헤더 파일을 인클루드 할 때 사용
(2) ""
개발자가 구현한 헤더 파일들을 인클루드 할 때 사용
(현재 작업중인 디텍터리에서 헤더파일을 먼저 검색한 뒤 없으면 시스템 경로를 검색)
아까 본 어셈블리 코드에 레이블이 있었죠?
링커가 오브젝트 파일을 다 모아서 하나의 이진 파일로 만들어 주는 도중 함수의 위치를 기억하고 있다가 함수를 호출하려는 코드를 만나면 실행위치에 아까 그 위치(주소)로 점프하는 코드를 넣어줌
만약 선언만 믿고 사용한 함수나 변수가 여전히 구멍으로 남아 있다면?
즉, 다른 오브젝트 코드에서 정의를 못 찾았다면?
- 링커가 못 찾는다며 링커 오류를 뱉음
- 그 함수나 변수가 없어 실행할 방법이 없기에 경고가 아니라 오류
왜 굳이 링크단계가 분리되어 있을까?
사람들은 보통 컴파일(처음 세 단계)과 링크, 두 단계로 나눠서 생각
수많은 구멍을 컴파일 할 때마다 메꾸기엔
조금 전 예에서 추측 가능했겠지만 .c 파일이 많이 있으면 구멍 메꿔주는 일이 매우 복잡
예) c 파일이 수 천개나 있는 프로젝트에서 .c 파일 하나 컴파일 할 때 마다 모든 함수를 찾아서 구멍을 메꿔줘야 하나?
여러 개의 .c파일에서 동일한 외부 함수를 사용할 경우 최종 실행파일에 그 함수 정의가 중복으로 들어가는 것도 막아야 함
위에서 본 함수 등을 기계어로 변환 후 파일 하나로 저장해 놓은 것
나중에 다른 .c 파일에서 이 기능이 필요할 때 같이 링크해서 쓸 수 있음
라이브러리는 두 종류가 있음
- 정적 라이브러리
- 동적 라이브러리
함수들을 만들어놓은 바이너리 덩어리를 사용하는 소스코드가 있으면
그 소스코드 가져와서 컴파일 한다음 마지막에 링크를 함
정적 라이브러리와 링크하는 것을 정적 링킹, 동적 라이브러리와 링크하는 것을 동적 링킹이라고 함
정적 링킹 : 라이브러리 안에 있는 기계어를 최종 실행파일에 가져다 복사함
동적 링킹 : 실행파일 안에 여전히 구멍을 남겨두어 실행파일을 실행할 때 링킹이 일어나게 함
/* monster_repo.h */
void add_monster(void);
/* monster_repo.c */
#include "monster_repo.h"
int g_mob_count = 0;
void add_monster(void)
{
++g_mob_count;
}
/* main.c */
#include <stdio.h>
#include "monster_repo.h"
int main(void)
{
add_monster();
printf("# monsters" %d\n", g_mob_count);
return 0;
}
컴파일러가 각 .c 파일을 따로따로 컴파일 하기 때문
main.c는 monster_repo.c 안에 있는 g_mob_count의 존재를 모름
전역 변수 두개가 서로 같은 이름이면 안됨
다른 방법이 필요
따라서 새로운 전역변수를 만드는게 아니라 monster_repo.c안에 있는 것을
가져다 쓸거다라고 컴파일러에게 말해줘야 함
-> 그래야 컴파일러가 구멍을 쏭쏭 비워 놓음 마치 함수 전방선언이 그랬던 것처럼
그걸 하는것이 extern 키워드
extern int g_mob_count;
void add_monster(void); // monster_repo.h
#include "monster_repo.h" // monster_repo.c
int g_mob_count = 0;
void add_monster(void)
{
++g_mob_count;
}
// main.c
#include <stdio.h>
#include "monster_repo.h"
int main(void)
{
add_monster();
printf("# monsters" %d\n", g_mob_count);
return 0;
}
extern 키워드
- 다른 파일에 있는 전역 변수에 접근하려면 extern 키워드를 사용
전역 변수의 문제
- extern 사용하면 아무데서나 다 확인 가능하고, 심지어는 맘대로 내 파일안의 변수를 바꿔버릴 수 있음 -> 다른데서 내 전역 변수를 못 쓰게 하려면 static 키워드 사용
// monster_repo.h
void add_monster(void);
// monster_repo.c
#include "monster_repo.h"
static int s_mob_count = 0;
void add_monster(void)
{
++s_mob_count;
}
// main.c
#include <stdio.h>
#include "monster_repo.h"
extern int s_mob_count; // extern을 써도 컴파일은 되지만 링커에서 오류 발생됨
// static으로 선언 되어 있기 때문에
int main(void)
{
add_monster();
printf("monster count %d\n", s_mob_count);
return 0;
}
다른 파일에서 전역 변수에 접근 못하게 막는 방법
이 변수의 범위가 파일로 한정됨 (흔히 정적 변수라고 함)
여전히 전역 변수로 프로그램 실행 동안에 실제 공간을 계속 차지하고 있음
static 변수를 다른 파일에서 접근하려고 하면 링커 오류
#include "monster_repo.h"
void add_monster(void)
{
static int mob_count = 0; // 스택에 생성되는 것이 아닌 전역 변수처럼 데이터에 생성
++mob_count;
}
static이 없으면 지역변수, 함수 반환 시 그 변수도 사라짐
static을 쓰면 개념상 전역 변수, 허나 그 함수 안에서만 접근 가능
-> 즉, 함수가 반환돼도 여전히 값은 저장되어 있음
빌드의 4단계가 올바로 돌게 하려면 아래의 기본 원칙을 따라야 함
(1) 헤더 파일에는 선언만 들어간다.
- 함수 선언
- 전역 변수 extern 선언
(2) .c 파일에는 정의가 들어간다
- 함수 정의
- 전역 및 정적 변수 정의
해결법 1.
(1) #include 는 가능하면 .c에서만 하기
(2) b헤더에서 a헤더를 인클루드 하는 대신 a에 정의된 것을 전방선언 하기
해결법 2. 인클루드 가드 사용
#ifndef FOO_H /* 만약 FOO_H가 정의되지 않았다면 */
#define FOO_H /* FOO_H를 정의할 것 */
/* 원래 해더 파일 내용 */ /* 그리고 이 코드를 원래대로 포함시킬 것 */
#endif /* #ifdef 블록의 끝 */
해결법 3. #pragma once
표준 아님
그냥 최신 컴파일러가 대체적으로 다 지원하는 것 -> c++에서 사용
포팅 생각한다면 예전 컴파일러 및 시스템과의 호환을 위해 그냥 인클루드 가드 쓸 것