[C] C lang 기본문법2
함수
- C의 함수 선언은 C#과 거의 동일
- 다만 C는 접근 제어자(예: public, private 등)가 없음.
public void honk (void) /* 컴파일 오류 */
{
printf("hlloe")
}
- C의 함수는 기본적으로 모두 전역(global)함수
- 즉, C의 함수는 어디서든 호출할 수 있음.
- 물론 이걸 제한할 수 있는 키워드도 있다.
함수의 오버로딩
- C언어는 그런거 없다.
- 따라서 함수 명을 다르게 만들어야 한다.
만약 A, B 아래 코드가 있다고 치면
A.
#include <stdio.h>
int main(void)
{
foo();
getchar();
return 0;
}
void foo(void) /* 컴파일 오류 */
{
printf("foo called");
}
B.
#include <stdio.h>
int main(void)
{
int x = foo();
getchar();
return 0;
}
void foo(void) /* 컴파일 오류 */
{
printf("foo called");
}
-> A는 컴파일이 안되고 B만 컴파일이 가능하다.
- C는 언제나 위에서 아래로 코드를 훑음.
- 함수 정의가 등장하기 전에, 즉 함수를 구현해 놓은거. 실제 중괄호 열고 거기다 코드 집어 넣고 이런 함수를 보기전에 이 함수를 호출하려고 하면 원칙상은 그함수를 모르므로 나 몰라라고 하는게 정상. 근데 C89에서 컴파일러가 이런 경우를 마주치면 그냥 가정을 자기 멋대로 생각해 봄.
- ANSI C (C89)에서 함수 정의(definition, 구현부와 같은 의미)가 등장 하기 전에 그 함수를 호출하면 컴파일러가 다음과 같이 가정
- 반환형은 int
- 그 매개변수는 아무거나 올 수 있다.
- 따라서 나중에 컴파일러가 int 가 아닌 다른것을 반환하는 함수를 찾으면 컴파일 오류를 뱉음.
해결법
- 호출 전에 함수 정의를 위치시키면 아무 문제가 없다.
컴파일 실패 코드
#include <stdio.h>
int main(void)
{
foo();
getchar();
return 0;
}
void foo(void) /* 컴파일 오류 */
{
printf("foo called");
}
올바른 코드
void foo(void) /* 컴파일 오류 */
{
printf("foo called");
}
int main(void)
{
foo();
getchar();
return 0;
}
함수 정의
- 그럼 함수 만들때 마다 호출하는 코드 위에 다 위치해야 하나?
- 함수 100개면 다 위에 넣어야 함? 그럼 코드가 되게 지저분해지는데.
- 함수에서 다른 함수 호출하는 경우는?
요즘 컴파일러나 새로운 코드 컴파일러나 툴처럼 소스코드 분석해서 아래에서 봤으니까 호출해줄게 이런식이 아님.
이걸 해결하기 위한게 함수 선언 이다.
함수 선언
- 함수의 구현체 없이 함수원형(prototype)만 선언해주는 것
- 함수의 원형은 다음의 사항들을 명시
- 함수의 이름
- 반환형
- 매개변수들의 자료형
- 비교: 함수 정의(definition)은 실제로 함수를 구현해 놓은 것.
- 함수 정의는 그 자체로 함수 선언이기도 하다.(당연)
함수 선언과 정의를 분리
#include <stdio.h>
void foo(void) /* 함수 선언, 전방선언 */
int main(void)
{
foo();
getchar();
return 0;
}
void foo(void) /* 함수 정의 */
{
printf("foo called");
}
함수 사용 전에 그 함수를 선언한다. 보통 함수 선언은 파일의 제일 위에 선언한다.
함수 선언과 정의가 하나
#include <stdio.h>
void foo(void) /* 함수 정의 */
{
printf("foo called");
}
int main(void)
{
foo();
getchar();
return 0;
}
전방선언의 작동 원리
- 컴파일러가 함수 이름과 반환형, 매개변수를 알 수 있음.
- 컴파일 다음 단계인 링크(link) 단계에서 실제 코드 위치 찾아서 그 구멍에 넣어주겠다는 것.
foo 함수 구현부가 0x01234면 그 곳에 컴파일러가 인식하고 넣어줌.
그럼 함수선언은 int를 반환하면 선언 안해도 되나?
C89/90에선 맞는 얘기 . 그래도 함수 선언은 언제나 하자
C99부터는 int 가정을 하지 않기 때문
- 그러나 어떤 컴파일러는 경고만 주고 컴파일 허용은 할 수도 있음.
- 모든 컴파일러가 그렇단 보장이 없으므로 반드시 선언할 것.
함수 매개변수 평가순서, 피 연산자 평가 순서
int num1 =128
int num2 = 256;
printf(""%d %d", add(num1, num2), subtract(num1, num2))
- 표준에 따르면, 함수 매개변수 평가순서는 명시되어 있지 않다.(unspeicified)
- 즉, 컴파일러에 따라 평가 순서가 달라질 수 있다.
printf()가 실행 되기 전 , add, subtract호출은 보장
- 그러나 누가 먼저 실행되는 지는 컴파일러따라 다르다.(컴파일러 맘)
명시되지 않은 피연산자 평가 순서
if(find_next () + spawn()==2)
{
~~
}
- find_next가 먼저 호출되는 보장이 없다.
- 명시 되지 않음.
- 한 줄에 있는 함수 호출 순서에 의존해 코드 작성하지 말것
- 해법은 두 함수를 두줄에 따로 호출하는 것.
기본적으로 한 줄에서 동일한 변수를 여러번 바꾸면 위험하다
이런 코드들도 많이 쓰는데
int main(void)
{
int num =10;
num = ++num + num++;
printf(num);
}
이 경우도 한 줄인가? 어떤 컴파일러는 한줄로 볼 수도 있으므로 이런 경우도 별로 좋은건 아님
한 표현식에서 같은 변수를 여러번 바꾸지 말 것
- 앞서 봤듯 + 연산자는 피연산자의 평가순서를 강제하지 않음.
- = 연산자도 마찬가지.
- 이럴떄 한 줄에서 같은 변수를 여러 번 수정시 정의되지 않은 결과가 나온다.
i = ++i + i++; /* 어떤 일이 정의 될지 모른다.*/
i = i++ + 1; /* 어떤 일이 정의 될지 모른다.*/
array[i] = i++; /* 어떤 일이 정의 될지 모른다.*/
위 셋 다 어떤 일이 정의 될지 모른다.
뭐든 간 가독성도 안 좋은 코드니 안 쓰는게 좋다
- 좋은 습관도 아니고 동작 정의 된 거도 확인하는게 귀찮은 일이다.
연산자 우선순위와 평가 순서
- 연산자 우선순위와 평가순서는 서로 아무런 연관이 없음
int result = add(num1, num2) + subtract(num1, num2) *divide(num1, num2);
if(++i | ++j && ++k) |
++i ||(++j && ++k) 여기서 ++i가 참이면 뒤에 괄호가 먼저든 뭐든 앞이 참이므로(왼쪽 피연산자) 오른쪽 피연산자를 검사할 필요도 없고 이걸 short circuit 평가라고 한다.
&& 과 는 평가 순서를 강제하는 연산자다.
범위(scope)
블록 범위와 변수 선언 위치
컴파일 오류 나는 코드
int main(void)
{
int num1 = 10;
print(num1);
int num2 = 100; /* error */
int result = num1 + num2; /* error */
printf(result);
}
컴파일 되는 코드
int main(void)
{
int num1 = 10;
print(num1);
{
int num2 = 100; /* error */
int result = num1 + num2; /* error */
}
/* num2, result 접근 못함 */
printf(result);
}
- 함수 중간에 블록을 열고 변수 선언 가능
- 함수 시작지점에서 모든 변수 선언시 실수할 여지 있음
- 정확히 어디서 사용하는 변수인지 파악 불가
- 중간에 값 바뀔 수도
- 블록 이용해서 함수 중간에 선언하는 것도 방법
- 이래나 저래나 만족스럽지 않다.
파일범위
- 어떤 블록이나 매개변수 목록에 안 속하고 파일 안에 있느느 거
#include <stdio.h>
static int s_num = 1024; /* 여기 */
int add(int op1, int op2);
int main(void)
{
s_num = add(10,30);
return 0 ;
}
파일 범위에 있는 변수의 메모리 위치
파일 범위에 있는 변수
- 다른 소스코드 파일에서 링크 가능
- 프로그램 실행 동안 공간 차지
- 즉, 스택 메모리에 들어가는 게 아님.
- 이들은 데이터 섹션에 들어감.
이게 전역변수
데이터라는 메모리 따로 있고 운영체제는 같은 메모리.
데이터 섹션 부분은 전역변수 들어감. 작성하는 코드 부분은 코드섹션이 있음.
힙은 나중에 배움. 함수에서 쓰는 건 이런 스택 메모리에 들어감.
메모리 주소가 증가한다는 의미는 , 메모리 주소가 메모리 위치니까 0부터 큰 숫자 이런식으로 증가. 작은 수 부터 큰 숫자. 우리가 생각하기엔 보통 작은 숫자가 위, 큰 숫자가 아래라고 생각할 수 있지만, 컴퓨터 구조에서 메모리 보여줄 떄는 위 처럼 보여주는 경우가 많다.(특히 스택을 얘기할 때는)
알아야 할 건 전역변수는 함수 메모리에 속한 게 아니다.
함수 범위
- 유일한 예 : label
- goto 같은 데서 쓰는 것.
- 함수 안에서 선언된 레이블은 함수 어디서라도 접근 가능.
- 다른 범위들은 위에서 선언된거만으로 접근 가능
- goto는 별로 안좋다는데..
함수 선언 범위
함수 선언의 매개변수 목록에 있는 건 그 목록 안에서 접근 가능.
많이 쓸 일은 없음
다음과 같은 예는 괜찮음.
void do_something(double value, char array[10*sizeof(value)]);
->
void do_something( double value, /* 함수 선언 범위 / char array[10 * sizeof(value)] / value는 첫 번째 매개변수 */ )
const 키워드
- 기본적으로 모든 변수에 const를 붙이자.
- 정말 값 변경이 필요한 변수에만 const를 생략하자
- 원칙적으로 말하면 언어의 기본 동작이 바뀌어야 함
- 아무것도 안 붙이면 const
- 굳이 프로그래머가 바뀌는 걸 원하면 앞에 뭔가 붙이기
- Rust가 이런거 잘한다.
goto 문
goto <label_name>;
...
<label_name>:
- C는 위에서 아래로 순차적으로 코드를 실행함.
- goto를 쓰면 이 순서를 다 어기고 다음에 실행할 코드를 맘대로 지정 가능.
반복문은 결국 goto를 사용한다.
- 어셈블리어는 반복문이 없다.
- 원래 어셈블리어로 쓰다 C로 넘어와서 초기엔 goto를 많이 썼다.
- 당연히 안전한건 일반 반복문을 쓰는 것.
- 근데 goto를 악마라고 아예 쓰지 말라는 것도 문제였긴하다.(물론 요새는 안쓰는게 정석)
goto는 정말 쓰면 안되나?
- 쓰면 안됨.
- 엄청난 길이의 코드가 만들어지기 때문
- 그래도 유용하게 쓰이는 경우도 있기는 하다.
goto 없이 for문에서 탈출하려면 if문 여러개 써야한다. 근데 goto는 한번에 탈출 가능
goto가 좋지는 않지만 쓰이는 곳들이 있기는 하다.
C가 아니라 C#이든 다른 언어든 수행할 수 있는 경우도 있기는 함.
goto 대신 함수를 따로 만들면 된다. 근데 꼭 함수 만드는 게 좋은거 아니다.
함수 만드는 순간 거기에 대한 과부화도 걸리고 오버헤드도 있기 떄문. 그래서 함수 안 쓰는게 성능에 있어서 만큼은 좋다.
그럼 일반 프로그래밍 or 유지보수 문제에서는 함수호출 쓰는게 좋긴 한데 결과적으로는 함수호출만으로 해결 못하는 걸 여기서 처리함.
에러 있으면 어디로 점프함. out나감. 만약 A에러 있으면 A했던거 뒤집어라 그리고 나감. A가 되고 B도 되는데 B가 문제생기면 out_b로 가서 B뒤집고 이런식. C로 가면 out_c가서 C 뒤집고
이런식으로 1,2,3,4 연산 있을때 뒤집어갈떄는 goto가 유리함. 이런걸 함수 만들려면 굉장히 힘들다.
당장 MacOS의 소스코드에서도 goto를 많이 사용한다.
c#은 goto를 지원하기도 한다.
goto 베스트 프렉티스는
- goto문은 언제나 전방(아래쪽)으로만 점프
- 위로 점프시 스파게티 코드 됨
내포된 루프에서 빠져나올 땐 자유로이 쓴다.
- 한 함수 안에 여러개 조건문이 공통된 코드 실행해야 할 떄도 안 써도 됨.
- 예: 함수 마지막에 성공/오류 조건 처리
- 근데 회사에서 쓰지 말라면 안쓰면 됨.
스택 메모리
C는 어떻게 new를 안 만들고 도나?
- new넣은 애는 값형이 아니라 참조형이라 했다.
- 그래서 복사도 안된다고했다.
C는 값 형으로도 배열을 만들 수 있다.
- 사실 모든 자료형은 참조형으로도 , 값형으로도 만들 수 있다.
- 이걸 알려면 스택 메모리를 알아야 한다.
스택 메모리란
- 자료 구조인 스택과는 다른 개념
- 둘다 작동 방법이 동일해 LIFO 스택이란 이름 쓸 뿐
스택은 함수 호출할 떄 사용. 함수에 보면 함수마다 각각 지역변수들이 있는데 그 지역변수를 어딘가에는 저장해 놔야 스택 메모리에서 공간 잡고 여기에 지역변수 넣는다.
- 각 함수에서 사용하는 지역변수 등을 임시적으로 저장하는 공간
- 스택 메모리의 크기는 프로그램 빌드 시에 결정됨.
- 스택 메모리의 위치는 실행시에 결정됨. (실행할 때 마다 달라지므로)
컴파일을 할 떄 컴파일을 하지만 exe파일을 가져다 다른 컴퓨터에서 실행함.
기억해야 할 것은 스택 메모리가 얼마나 크냐 정의하는 것은 프로그램 빌드 시에 정의를 한다.
- 그 크기 값이 이제 실행파일에 보면 거기에 헤더파일이 있다.(실행파일이 뭔지 설명하는) 거기에 프로퍼티 중 하나로 들어간다. 스택사이즈가 얼마이닞 적혀있고 스택 위치는 실행할 때 마다 달라지기 때문에 실행할 때 결정된다.
스택 메모리는 결국
함수가 호출 될 떄마다 그 함수에서 필요한 공간을 스택에서 떼었다가 그 함수가 반환시 흔들어 지워버리는 개념.
- 실제 지우지는 않는다.(쓰레기 값으로 놔둠)
load를 제일 먼저 호출 메모리 빌려옴. 아래로 쭉 실행하면서 메모리 빌려옴.
기본 자료형 변수는 스택 메모리를 차지
여태 모든 기본 자료형 변수를(char, float, int) new 없이 쓸 수 있던 거도 스택 메모리에 할당 됐기 떄문이다.
기본 자료형을 함수 매개변수로 전달시, 스택에 복사본을 만듬 -> 이게 값형.
스택 메모리를 빌리고 반환시마다 언제나 빈 공간 없이 차곡차곡 쌓음.
new 로 만든 데이터는 힙(heap) 메모리에 할당됨.
- 이 경우 메모리에 구멍이 뚫릴수 있음.
스택 메모리에 대해서 간단히 알아보자
- 스택이 큰 주소에서 작은 주소로 쌓임.
- ESP(Extended Stack Pointer)
- 현재 스택 포인터
- EBP(Extended Base Pointer)
- 현재 스택 프레임의 기본(첫) 주소. (입구). 현재 어디까지 차 있는지 보여주는 포인터.
- 스택 프레임(Stack frame)
- 각 함가 사용하는 스택 메모리의 범위
스택 메모리 안의 배열, 스택 오버플로우
- 스택의 크기는 한정적.
- 타겟 플랫폼 따라 달라짐. 심지어 컴파일 시 프로그래머가 스택 크기 정해줄 수도 있음.
- clang window에서 아무 설정 없으면 1mb 정도.
스택 오버플로우는 이 스택을 넘어가는 경우에 생긴다
- 스택의 크기가 1mb일떄 아래 코드 실행하면
int add (const int a, const int b)
{
char buffer [1024 * 1024];
int res = a+b;
return res;
}
너무 큰 데이터는 스택에 넣으면 안된다.
너무 큰 데이터는 스택에 못 넣음
이럴 경우에 쓰는 게 동적 메모리 할당(c나 자바에서의 new)
- OS에게 메모리 달라고 요청하는 것.
재귀함수를 깊이 쓰면 스택 오버플로우 나는 이유도 같은 이유
함수 한번 호출할 때 마다 그 함수 스택 프레임 만큼 바이트를 더 먹음.
그 함수가 반환하지 않고 계속 다른 함수를 호출하며 스택 올리면 언젠가 1mb를 다 쓴다.
int values[30];
size_t array_size = sizeof(values); //120
sizeof(values) 는 values 배열이 차지하는 총 바이트 수를 반환
- 그 이유는?
- 이 배열이 스택에서 몇 바이트 차지하는 지 컴파일 중 알기 때문
꼼수
배열의 요소 개수 구하는 방법
- 방법1:
const size_t num_vals = sizeof(values) /sizeof(values[0]);
- 방법2:
함수 밖에서
define array_length(arr) sizeof(values) /sizeof(values[0]);
매크로 함수 사용
const size_t num_vals2 = array_length(values);
sizeof(매개변수) 와 배열의 총 바이트 수
sizeof()가 매개변수로 들어온 배열의 총 바이트 수를 반환할 수 있으려면 그 배열의 모든 요소가 스택에 다 복사되어 있어야함
사용하는 스택 크기가 달라야 한다
- 즉 , 다음 두 함수 호출에서 매개변수 전달에 사용하는 스택 크기가 달라야함.
왼쪽은 매개변수 20개, 오른쪽은 50개
process()라는 함수는 어셈블리어가 이미 컴파일 된건데, 하나는 20개 불러온 어셈블리어, 하나는 50개
ebp 20, ebp 50인 함수가 있다(같은 함수인데?)
=> 불가능
함수 스택 메모리 사용량은 고정.
함수는 호출자가 누구든 딱 정해진 수와 크기의 매개변수 가 들어온다는 가정으로 동작
- 함수가 먼저 결정되고 호출자는 그 함수를 호출할 뿐.
즉, 함수는 호출자가 뭐 하는 놈인지 모른다.
- 함수의 스택 메모리 사용량은 고정이 되어있는거.
sizeof(매개변수)가 4를 반환한 이유는?
결국 포인터의 주소 크기였던 것(주소 알려주는게 전부)
길이가 명시된 매개변수 배열
void process(int nums[5])
{
size_t i;
for (int i = 0; i<5 ; i++){
nums[i] *= 2;
}
}
이건 배열 5 길이인걸 넣어준게 아닌가?
-> 프로그래머 편의를 위함. 50이건 50000이건 컴파일은 nums[]와 동일하게 함. 대신 개발자가 보고 쉽게 판단 가능.
위 코드는 복사본이 아니라 원본을 바꿈
- 이걸 “참조에 의한 호출”이라고 함.(원본을 바꾸기 떄문)
- 어떤 사람은 “값에 의한 호출” 이라고 함(위치를 복사하는거라)
- 어떤 사람들은 C는 참조에 의한 호출이 없고 그냥 주소를 전달하는 법을 통해 참조에 의한 호출 을 시뮬레이션 하는 방법.
- 중요하지 않다. 원본이 바뀐다는 거만 알자.
매개변수의 길이 , 배열요소의 초기값
- 매개변수는 들어오는 길이 알 방법은 없음.
- 배열 자체에서 크기를 알아올 수 있는 방법은 있음.
예외: 아까 array_length() 꼼수(매크로)를 쓸 수 있는 경우
즉, 배열의 크기는 따로 기억해둬야 한다.
void process(size_t n, int nums[5])
{
size_t i;
for (int i = 0; i<5 ; i++){
nums[i] *= 2;
}
}
이런식으로 바꿔야 함.
배열 요소의 초기값
C는 배열 요소의 값을 초기화 해주지 않음.
- 따라서 그 전 메모리 남아있던 값을 그대로 사용
- 이건 변수도 마찬가지.
int nums[30]; /* 속에 뭔 값이 들어있는 지 모름 (쓰레기값)*/ int val; /* 마찬가지 */
- 변수 초기화는 했고 배열 초기화 방법은?
배열의 초기화
가장 좋은 방법 : 배열의 모든 값을 0으로
배열의 모든 값 0으로 초기화 하는 방법은?
int nums[10] ={0,}
0 뒤에 쉼표를 찍자
이를 통해 초기화 목록 모든 값을 직접 초기화 해주지 지만 쉼표 뒤가 모두 0으로 초기화 됨을 보여줌.
이래서 C는 위험하다.
버퍼 오버플로가 나면 다른 곳들 까지 0으로 바꿈.
내가 가진 공간이 아닌 다른 애가 가진 공간을 덮어씌우는데 C는 이 버퍼 오버플로우를 체크 안해줘서 만약 이게 나면 전에 덮어쓴 데이터 떄문인걸 모를 수 있다. 이 경우 크래시가 나야하는데.
이런 버그를 메모리 stomp라고 한다.
이건 가장 고치기 힘든 문제(메모리를 밟고 지나간 상황)
이걸 찾아서 고치는 게 프로그래머의 자질.
다차원 배열
2차원 배열 , 3차원 배열
2차원 배열의 경우, C# 에서는 int[,] , C에서는 int[][]
여기까지가 C 만의 문법 보기 전 까지 기본 문법 및 작동원리는 얼추 알아봄.
위까지 이해하면
- 다른 언어에서 배웠던것들은 C언어에서 어떻게 작용하는지.
C에서 다르게 동작하는 것은 무엇인지.
- 다음은 내 소스코드가 어떻게 실행코드로 바뀌는지 보자.