-
[ACES - C ] 데이터 형과 변수의 종류(지역변수, 전역변수, 정적변수)전기전자공학/프로젝트 2017. 7. 11. 11:14
오늘 포스팅은 C언어의 심화적인 부분들에 대해 작성하겠습니다!
Overflow와 Underflow
Data type (데이터형)
지역변수, 전역변수, 정적 변수( 정적 지역변수, 정적 전역변수 )
C 프로그래밍을 하면서 다양한 변수들의 형태을 정하는 데
변수의 데이터형에 따라 컴파일러가 처리하는 과정이 조금 달라집니다.
unsigned / char, short, int, long 을 통해 예를 들어볼게요
overflow(오퍼플로우)란 해당 데이터 형의 제한 숫자 범위를 넘어서는 현상을 말합니다.
예제
#include <stdio.h> int main() { signed char = c1; // signed char(8bit)의 숫자 범위: -128 ~ 127 c1 = 127; c1++; c1 = -128; c1--; }
c1++; // c1은 127에서 더 큰 수가 없기 때문에 -128이 됩니다!
c1--; // c1은 가장 작은 수인 -128에서 1만큼 감소시키는데, 더 작은 수가 없으므로 127이 됩니다
따라서 overflow는 자신의 가장 최대/최소 숫자에서 증가/감소하는 경우 값이 최소/최대로 바뀌는 경우를 말합니다.
overflow은 여러 연산과정이 일어날 경우 생길 수 있는데, 만일 overflow가 생기면 최종값은 자신이 원하던 바와
다르게 나옵니다.
그래서 숫자의 덧셈, 뺄셈, 곱셈, 나눗셈을 할 때, 특히 한꺼번에 많은 곱셈과정이 일어날 때 overflow가 발생할 수 있으므로
나눗셈과 번갈아 가면서 계산해주는 것이 좋습니다!
그렇다면 Underflow(언더플로우)는 무엇일까요?
Underflow란 자신이 나타낼 수 있는 숫자 표현의 범위를 넘어서는 현상을 말합니다.
말로 설명하니까 애매하죠..ㅎ 그렇다면 예제로 보여드릴게요!
#include <stdio.h> int main() { int c; c = 1; printf("%d\n", c); // --- (ㄱ) c = 1.5; printf("%d\n", c); // --- (ㄴ) }
c는 int형으로 선언되었습니다. int는 정수를 표현할 때 쓰는 32bit 데이터 형이죠!
(ㄱ)에서 c는 1로 그대로 출력됩니다.
그렇지만 (ㄴ)의 경우 c는 1.5가 아닌 1을 출력하게 됩니다.
그 이유는 int형이 소수점을 표현하지 못하기 때문인데, 이렇게 표현할 수 없는 부분은 버려지게 되는 현상을 언더플로우라고 합니다.
(여기서 의문! 0에 가까워질 수 있다는 의미는 언더플로우가 발생하기 쉽다는 건가요? 아니면 어렵다는 건가요?)
그럼 어떤 data type을 사용해야 할까요?
상황을 예로 들어봅시다. loop를 100번 돌리고 싶을 때 반복 변수 i의 데이터형을 선택해 보아요
unsigned char i;
unsigned int;
unsigned short;
signed ...
1번 unsigned char 을 선택했다고 합시다!
우리의 컴퓨터 안에 어셈블리어를 만들 때 32bit 팬티엄을 사용한다고 가정합시다.
여기서 32bit의 의미는 펜티엄이라는 기계가 32bit 숫자의 덧셈을 한꺼번에 할 수 있다는 것을 의미합니다.
교수님이 펜티엄이라는 말을 했을 때 무엇인지 알아들을 수 없었는 데, 구글링해보니까
"펜티엄(Pentium)은 인텔에서 만든 개인용 컴퓨터용 마이크로프로세서의 상표명이다."
이네요 ^^ 아마도 제가 마이크로프로세서를 듣지 않아서 몰랐던 것 같아요...
unsigned char의 경우 8bit 데이터 형이므로, 32bit 펜티엄을
의 데이터에서 1 byte(=8bit)만 이용하게 됩니다. 이렇게 1바이트와 1바이트를 ALU에 넣어서 연산을 합니다.8
8
8
8
이 때 해당 연산을 하고 나머지 24bit는 모두 0으로 바꿔주는 과정을 하나 더 거쳐야 합니다.
이는 3번 unsigned short의 경우에도 마찬가지인데요,
unsigned short는 16비트 이므로 프로그램이 원하는 연산을 하고 나머지 16bit를 0으로 바꾸는 연산을 하게 됩니다.
루프가 10만번 돈다고 가정하고 그 안에 덧셈 연산을 한다고 생각하면, 32bit 기계(machine)에서 8bit나 16bit 덧셈 연산을 하는 경우
0으로 바꾸는 연산을 하나 더 하므로 루프를 10만 번 더, 총 20만번 돌게 됩니다.
2번 unsigned int의 경우 32bit이므로 4 byte 모두를 사용하게 되고, 이는 나머지 bit를 0으로 바꾸는 연산과정이 필요없으므로
루프 10만번만 돌면 됩니다.
그렇지만 가끔 작은 데이터 형으로 변수를 사용하는 게 메모리를 절약하기 위해 좋다는 말이 나오는데요,
이는 메모리를 비교적 작게 탑재해야 하는 기계(리모컨 등)에 프로그래밍할 경우 필요하다고 하네요.
많은 메모리를 가지고 있는 pc의 경우에는 이러한 코딩은 사실상 쓸모가 크게는 없다고...
(각자의 분야에 대해 코딩의 스타일이 다르다고 하네요. 자신의 분야에 적합한 코딩으로 짜도록 하세요.
교수님은 해당 기계에 맞지 않는 코딩을 하는 경우 닭잡는 칼로 소를, 소잡는 칼로 닭을.. 이라는 표현을 쓰셨습니다 ㅎ)
그럼 4번 signed의 경우는? 부호가 있는 데이터형의 경우, 이진수로 표현하면,
표현하고자 하는 ??맨 앞 비트가 0이면 0, 맨 앞 비트가 1이면 1로 해줘야 합니다. --- ***
부호비트는 맨 뒤에꺼를 앞으로 ..
그래서 몇몇 컴파일러는 이 연산을 24bit만큼 왼쪽으로 shift했다가 arithmetic right shift를 해주면
맨 앞의 숫자가 1일 때는 right shift된 숫자들이 모두 1로 바뀌고, 0일 때는 모두 0으로 바뀌게 됩니다.
이 또한 한 번더 연산을 하게 되므로,
연산 과정이 가장 많은 최악의 경우는 signed이며 기계의 bit보다 작은 bit의 데이터형을 사용할 때입니다.
따라서, 문자를 쓸 때는 char를 쓰고, 정수를 쓸 때는 그냥 int만 쓰자! 가 오늘의 교훈입니다.
사실 저 위에 데이터 형 말고 더 큰 데이터 형이 존재하죠. long long !
long의 경우는 int와 마찬가지로 32bit인데, long long 은 64bit입니다.
그런데 이 bit 크기 수도 사실 달라질 수 있어요! 이 크기는 컴파일러 만드는 사람이 정하는 거랍니다.
32 bit machine 16 bit machine
char 8 8
short 16 16
int 32 16
long 32 32
long long 64
라고 하네요. (고정적인 숫자는 아니지만, 저 크기가 가장 통용되어 있습니다.)
int를 word로 맞추는 게 기본입니다. 여기서 word의 크기는 정해져 있는 것이 아니라 하나의 '개념'입니다.
word란 마이크로프로세서가 한 번에 처리할 수 있는 기본 단위를 뜻합니다.
32bit machine이라는 말은 32bit를 한 번에 계산한다는 의미로 여기서 word는 32bit이겠지요.
펜티엄 64bit는 64bit의 덧셈 뺄셈을 합니다.
이제는 실수를 표현하는 데이터형 float와 double에 대해 설명해드리겠습니다.
float는 floating point(부동 소수점)이라는 데에서 이름 붙혀졌고 32bit의 크기를 가집니다.
double은 그냥 double floating point라는 의미이고, 크기는 float의 두 배, 64bit입니다.
floating point number라고 해서 모든 연산이 정확하게 되는 건 또 아니에요!
연산을 많이 거치다 보면 오차가 커질 수 있어서, 이 때 underflow를 잘 생각해야 합니다.
1 / 3 * 3 을 사람이 계산하면 금방 1이라는 것을 확인할 수 있죠. 그렇지만 컴퓨터는 다르게 생각합니다.
(float)1 / 3 * 3의 경우 왼쪽에서 오른쪽 연산을 하므로 0.3333 * 3 = 0.9999 가 나오게 됩니다.
이 때는 1과 그렇게 큰 차이가 없다며 별로 신경 쓰지 않을 수 있지만, 큰 프로그램에서 많은 연산을 거치면서 오차는 증가하므로
오류가 없는 연산을 할 수 있도록 주의합시다!
여기서 오퍼플로우도 조심해야 합니다! 보기 편하다고 곱하기를 몰아서 하는 일은 없도록!
곱하기와 나누기를 번갈아서 연산하도록 코드를 짜주세요.
Fixed point number의 경우 소수점 아래의 숫자가 없습니다. 8bit의 경우 3을 표현하기 위해
0000_0011이라고 표현하면 됩니다. 그렇지만 floating number을 표현할 때는 어떻게 해야 할까요?
3.125를 나타내고 싶다면,
0
0
1
1
.
0
0
1
3.125를 이렇게 나타내면 되겠지요. 그렇지만 이런 식이라면 소수점을 표현 할 수가 없습니다.0.125까지 밖에 표현할 수 없죠.
그래서 과학적 표기법 ( 1.xxxx * (밑)^(지수)) 의 형태로 표현합니다.
이진수의 경우 1.xxxx * 2^(yyyy)이고,
부호가 있는 경우 SXXXX YYYY (S: 부호 비트) 로 표현하게 됩니다.
지역변수, 전역변수, 정적 지역변수, 정적 전역변수
이 네가지 변수들을 정확하게 구분하기 위해서 꼭 알아둬야 할 개념 2가지가 있습니다.
'Scope'와 'Life time'
스코프는 범위라는 뜻으로, 어디에서 보이는 가를 의미합니다.
라이프 타임은 언제 생성이되고 언제 소멸되는 지를 명시해주는 시간의 개념입니다.
이는 나중에 저 4가지 변수들을 구분할 때 사용해보면서 이해하도록 하고, 먼저 지역변수에 대해 알아봅시다!
지역변수는 간단히 말하자면, 함수 안에 선언되는 변수입니다.
먼저, 컴파일러는 이 변수들을 어떻게 생성할까요?
프로그램 메모리 부분(?)은 다이내믹, 데이터, 코드로 이루어져 있습니다.
다이내믹은 스택과 힙으로 구성되어 있는데, 지역변수는 스택에서 생깁니다.
지역 변수는 서로 다른 중괄호 안에 있다면 같은 이름으로 정의해도 상관없습니다. 그리고 이 지역변수는
빠르게 쓰기 위해서 그냥 레지스터에 저장해놓고 쓰는 데, 너무 많은 지역변수가 존재할 경우 레지스터가 아닌 ( )곳에 저장됩니다.
데이터 부분에 ( )하고 레지스터에 옯겨서 연산(덧셈, 뺄셈 등)을 하고 다시 데이터에 옮깁니다. ?? 레지스터에 옮겨 놓으면 다른 데서는 레지스터에 있는지 몰라서
그런식으로 저장하면 안됩니다.
이제 전역변수에 대해 설명해보겠습니다.
file: test.c
#include <stdio.h>
int a;
int b = 10;
현재 test.c에는 전역변수 a, b 두 개가 선언되어 있다. a는 초기화되어 있지 않고 b는 초기화 되어 있는데,
초기화 유무에 따라 약간 다른 점이 있다.
만약 메모리 공간을 잡아줘야 하는 초기화 되지 않은 전역변수 100개가 있다 가정하면 한 곳(?)에 모아서 0으로 다 초기화를 해준다.
b는 초기화했으므로 b의 메모리 공간이 있고 그 곳에 10이라고 저장해 두었다.
전역변수를 초기화 하지 않으면 메모리를 조금 아낄 수 있는데, 이 또한 pc같은 큰 용량을 포함하는 기계에서는 신경쓰지 않아도 되는 부분이다.
이제 또다른 예를 들어 보자.
file: test1.c
#include <stdio.h>
main() {
a = 10;
}
test1.c와 test.c을 링커로 합칠 때 test1.c에서 컴파일러는 test.c의 전역변수 a를 볼 수 없다.
즉, 컴파일러는 파일 하나만 본다.
그래서 이 경우에 test1.c에 extern int a; 라는 선언을 해줘야 한다.
이는 컴파일러에게 '어딘가에 a가 있어!'라고 알려주는 것이다. 그래서 링커로 합치고 프로그램 실행을 하면 오류 없이 실행할 수 있다.
그렇지만 만약에 test.c와 test1.c 둘 다 extern int a라고 해버리면 링커가 합칠 때 컴파일러에게 있다고 말했는데, 실제로는 정의된 a가 없으므로
이 때는 링커 에러(unresolved = 있다고 했는 데 없다.)가 발생한다.
전역변수는 어디서나 다 보이는 변수이므로 함수의 이름의 level과 같다.
전역변수의 선언과 함수 선언을 보면 두 가지가 level(?)이 같다고 애매하게 수긍할 수 있다.
int a;
int func();
자, 이제 지역변수(= 자동변수) 다!
지역변수는 중괄호(= { } ) 안 에서 정의되는 변수다. 그리고 이 변수를 쓰려면 반드시 그 전에
초기화를 해야 한다. 그 이유는 전역변수와는 다르게 선언만 하면 쓰레기 값이 할당되기 때문이다.
그리고 중괄호 안에서 맨 처음부분에 지역변수 정의를 해야 한다. (but c++은 나중에 정의해도 됨)
이 지역변수의 스코프는 중괄호로 쌓인 부분(블록)이며, 라이프 타임은 가장 가까운 중괄호 열리는 부분부터
중괄호 닫히는 부분까지다.
정적변수는 global(전역), local(지역) 정적 변수로 나뉜다.
여기서 '정적(static)'이란 프로그램 개발할 때 내가 임의로 '데이터'에 넣을 수 있다는 것을 의미한다.
그리고 변수에다가 static을 붙이면 해당 파일에서만 이 변수를 인식하도록 해준다.
dynamic은 프로그램에서 ?? ..??
- static global 변수
static을 쓰면 scope가 줄어드는데, 그럼 왜 만들었을까? 그 이유는 ' 삽질 할까봐...' 이다.
커다란 프로그램을 만들거나 외부 사람들과 협업하는 경우 전역변수의 이름이 중복될 수 있다. 이 때 내가 산출한 값이
내가 원하는 변수가 아닌 다른 변수에 저장되면 버그를 잡기가 참 힘들어질 수 있다.
이를 막기 위해서 static이 쓰인다.
- static local 변수
이 변수를 설명하기 위해 c 파일을 예로 들겠다.
/* test.c */
#include <stdio.h>
int main() {
static int a = 20;
a++;
}
원래 지역변수는 레지스터에 저장을 한다. 그런데 static이 붙었으므로 a 를 '데이터'에 저장한다.
test.c를 10번 반복해서 실행했을 때 static int는 '데이터'에 초기화를 한 번하고 없어지지 않고 있으므로
a = 20 -> 21 ->22 -> ... -> 30이 된다.
'전기전자공학 > 프로젝트' 카테고리의 다른 글
[ACES] 3rd week - Graph (0) 2017.07.19 [ACES] 메모 (assert, (0) 2017.07.11 #5-6,7 포인터 배열 (0) 2017.06.29 [Kernighan - C] #5 - 5 (0) 2017.06.29 [Kernighan - C] #4-1,2 함수의 기초 (0) 2017.06.26