모두의 코드 - 10. 연예인 캐스팅(?) (C 언어에서의 형 변환), 11 - 1. C 언어의 아파트 (배열), 상수, 11 - 2. C 언어의 아파트2 (고차원의 배열)

2024. 9. 25. 23:46Programming Language/C

10. 연예인 캐스팅(?) (C 언어에서의 형 변환)

C언어에서 변수는 고유의 형(type)을 가진다.

동일한 형의 변수끼리 대입, 연산을 하는게 보통인데 가끔씩 프로그래밍을 하다 보면 형이 다른 변수 끼리 대입을 하는 연산이 필요하게 된다.

이렇게 형이 다른 변수끼리 대입이나 연산을 하는건 한국에서 달러로 물건을 구매하는것과 비슷하다.

 

이런 상황을 코드로 확인해보자.

컴파일하면서 

이런 경고를 출력하나 실행은 가능하기에 그 출력의 결과는

\(2\)가 된다.

 

보면 실제로 데이터의 손실이 발생했다.

\( 2.4 \)를 대입했으나 결과는 \(2\)가 나와 소수 부분의 데이터를 소실했다.

이는 각 변수들이 메모리 상에 저장되는 특징이 다르기 때문이다.

int형 변수는 처음 정의되는 시작부터 메모리상에 오직 정수 데이터만 받아들이도록 설계 되있기 때문이다.

 

그러면 컴퓨터는 실수를 어떻게 표현할까?

 

컴퓨터가 실수를 표현하는 원리

컴퓨터는 모든 데이터를 이진수로 표현하고 컴퓨터상에서 실수를 표현하는 방법은 대표적으로 두 가지 방식을 들 수 있다.

고정소수점(Fixed Point)방식부동소수점(Floating Point)방식이다.

float타입의 float가 부동소수점의 float에서 온것이다.

대부분의 컴퓨터의 경우 99.9% 부동소수점 방식을 통해 실수를 표현하고 있을 것이다.

그 이유는 고정 소수점 방식보다 같은수의 비트만 사용해서 표현할 수 있는 수의 범위가 더 넓기 때문이다.

 

이 부동소수점 방식을 통해 수를 표현하는 방법은 국제전기전자기술자협회(IEEE)에서 1985년에 IEEE-754라는 이름으로 표준화했다.

 

IEEE 754

보통 수를 표현하는 방법은

 

\(123, 1234.123, -234\)

 

와 같이 표현된다.

이 수는 

 

\(1.23 \times 10^2, 1.234123 \times 10^{-2}, -2.34 \times 10^2\)

 

와 같은 방식으로도 표현이 가능하다.

 

마찬가지로 컴퓨터 상에서도 소수를

\(\pm f \times b^e\)

와 같이 표현한다.

 

이때 f는 가수, b는 밑, e는 지수이다.

예를들어서 \(123\)의 경우 \(f\)는 \(1.23\), \(b\)는 \(10\), \(e\)는 \(2\)가 된다.

 

컴퓨터 상에서 이진 체계를 이용하기 때문에 \(b\) 의 값은  \(2\) 로 고정되어 있다.

따라서 소수 데이터를 보관할때 \(f, e\)의 값만 저장하면 된다.

그리고 맨 앞에 부호비트를 위해서 1비트를 더 쓰게 된다.(2의 보수 표현법과는 살짝 다르다.)

부호비트의 값이 0이면 양수이고 1이면 음수가 된다.

 

이건 IEEE 754에서 정의한 부동 소수점 표현이다.

float의 경우 가수부분이 23비트를 차지하고, 지수부분이 8비트, 그리고 부호비트가 1비트를 차지하여 총 4바이트를 차지하게 된다.

double의 경우는 가수 부분이 52비트이고 지수 부분이 11비트로 무려 8바이트가 차지하는 거대 자료형이다.

 

메모리상에 실수가 어떻게 저장되는지 확인하기 전에 이진법으로 표현된 실수들을 십진법으로 바꾸고, 십진법으로 표현된 실수들을 어떻게 이진법으로 바꾸는지 살펴보자.

 

소수의 10진법 - 2진법 진법변환

먼저 2진법으로 표시된 소수를 10진법으로 바꾸는 연습을 해보자.

\( 10010.1011_{(2)} \)

 

소수점 이하 부분은 마찬자기로 자리수마다 \(2^{-1}\), \(2^{-2}\) 순으로 쭉쭉 내려간다. 

이는 10진법 체계에서 \(10^{-1}\), \(10^{-2}\)로 내려가는 것과 동일하다.

따라서 

 

\( 10010.1011_{(2)}  = 2^4 + 2^1 + 2^{-3} + 2^{-4} = 18 + 0.5 + 0.125 + 0.0625 = 18.6875 \)

 

와 같이 된다.

 

2진법으로 표시된 소스들은 모두 십진법으로 변환이 가능하다.

그렇다면 십진법 소수도 과연 이진법으로 바꿀 수 있을까? 

이번엔 \( -118.625 \)를 이진소수로 바꿔보자.

 

\( -118.625 = -(64 + 32 + 16 + 4 + 2 + 0.5 + 0.125) =  -1110110_{(2)} - 2^{-1} - 2^{-3} = -1110110.101_{(2)}\)

 

이렇게 바꿔 볼 수 있다.

그런데 사실 모든 10진법으로 표현된 수를 2진법으로 변환할 수 는 없다.

예를 들어 0.1을 이진법으로 바꿔본다고 생각해보자.

 

\( 0.1 = 2^{-4} + 2^{-5} +  2^{-8} + 2^{-9} + ... = 0.0001100110011.._{(2)} \)

 

컴퓨터는 이렇게 무한하게 길어지는 무한 소수들을 모두 메모리에 넣을 수 없기에 일정 부분만 잘라 보관하기에 무조건 오차가 발생하게 된다.

 

IEEE 754 방식으로 소수 저장하기

이제 IEEE 754방식으로 소수가 어떻게 저장되는지 보자.

먼저 부호 비트는 양수일때 0, 음수일때 -1로 저장된다.

위에 -118.625의 경우 부호 비트에 1이 들어간 것을 볼 수 있다.

 

두번째로 변환된 이진수를 정규화(Normalization)한다

정규화란, 어떤 이진수를 1.xxxxx꼴로 만드는 것이다.

-118.625의 경우 이진수 형태인 1110110.101을 1.110110101처럼 바꾸는 것을 말하는 것이다.

그렇다면 가수부분에는 맨 앞 1을 뺀 나머지만 저장이 될것이다.

 

이때 정규화 작업시 얼마나 쉬프트 연산이 일어났는지 계산하여 지수 부분에는 얼마가 와야되는지 알게 된다.

위의 경우는 쉬프트 연산이 오른쪽으로 6번 발생했기에 지수는 6이 오게된다.

 

0.1처럼 무한 소수로 표현되는 수들의 경우는 반올림을 하게 된다.

 

0.1 - 0.00011001 10011001... 으로 나가게 되는데 float에 대입한다고 하면 float의 가수부분이 23비트기에 24번째 비트에서 반올림하게 된다. 그렇기에 0.1은 컴퓨터 상에 0.00011001100110011001101로 보관된다.

 

마지막으로 위에서 계산한 지수에 바이어스(Bias)처리를 해준다.

이건 그냥 지수에 \( 2^{e-1} -1\) 만큼을 더해준다는 의미이다.



float형의 경우는 32비트이고 지수부를 8비트 갖고 있기에

\(2^{8-1}-1\)(127)를 계산한 지수부에 더해주는 거고, double형의 경우는 64비트이고 지수부를 11비트를 갖고 있기에 \(2^{11-1}-1\)(1023)를 지수부에 더해줘야한다.

 

계산한 지수에 바이어스 처리를 해주는 이유는 지수가 언제나 양수가 아니기 때문이다.

-118.625는 정규화시에 지수가 +6이였으나 0.625라는 수를 보면 왼쪽으로 한번 쉬프트 되기 때문에 지수가 -1이 된다.

 

아무튼 그렇게 float의 경우는 지수가 들어가는 값의 범위가 1~254이고 더블의 경우는 1부터 2046까지 가능하게 된다.

이 말은 float의 지수부가 \(2^{-126}\)부터 \(2^{127}\)까지 가능하다는 의미가 된다.

 

여기서 float형 변수를 이용하면 1~ 254까지라 했는데 그럼 기존에 설명했던 0 ~ 255에서 0과 255는 어디로 갔을까?

 

0과 255는 정상적이지 않은 술르 표현하기 위해서 IEEE 754에서 규정해둔 수에 사용된다

* 비정상 수(Denormalized number)

* 무한대

* NaN

 

**이 위에서 설명한 디테일한 부분은 이해가 부족해서 나중에 좀더 찾아보자.

진도를 위해서 일단 넘어가자.

 

형변환(캐스팅)

그럼 우리는 경고가 나오지 않게 대입을 할 수 없을까?

아니다 서로의 형을 맞춰주면 된다.

아래의 코드를 보자.

실행해보면

결론은 아까와 같은 2가 된다.

하지만 

아까 처럼 에러를 출력하지 않는다.

그 이유는 강제로 형변환(캐스팅)했기 때문이다.

 

어떤 변수의 형을 바꾸기 위해선 

 

(바꾸려는 데이터 타입) 변수명

 

과 같이 코드를 작성하면 된다.

 

예를 들어 double로 선언된 b를 int로 바꾸고자 한다면 (int)b 와 같이 작성하면 된다.

이 때 형을 변환한다는건 b의 타입을 아예 바꾼다는 의미가 아니라 그 라인에서 b는 int타입이 될것이라는 일시적인 행위이다.

 

그렇기에 캐스팅 이후에 b를 출력해보면 

double형태로 잘 나오는 것을 볼 수 있다.

 

컴파일러는 아 얘가 형이 다름을 인지하고 넣으려고 하는구나! 라고 인식하게 해준다고 생각하면 될것 같다.

 

하나의 예를 추가로 보자.

 

이렇게 컴파일 하면 단지 형변환을 하고 안하고 차이였는데 하나는 비율이 정확하고 하나는 부정확하다.

여기서 중요한건 a와 b가 정수형 변수라는 점이다.

 

컴퓨터는 a / b 를 두가지의 의미로 인식한다.

a와 b중 어느 하나가 실수형 변수라면 기본적인 나눗셈을 하게된다.

5 / 3 = 1.6666...과 같은 나눗셈 말이다.

 

그런데 a 와 b가 모두 정수형 변수라면 나눗셈연산을 하지 않고 몫만을 반환한다.

그래서 5 /3 = 1이 되는 것이다.

 

이런 형변환은 매우 중요하다.

주로 실수형 변수에서 정수 부분만 추출할때 사용되기도 한다.

 

11 - 1. C 언어의 아파트 (배열), 상수

C언어에서 변수를 통해서 메모리에 값을 읽고 쓸 수 있었다.

변수의 이름 하나가 , 해당 변수 타입에 해당하는 만큼의 공간을 메모리에서 나타내고 있다.

예를 들어 int는 4바이트니까, int a라는 변수가 있다면 a는 메모리상에서 4바이트의 공간을 가지게 될 것이다.

 

C언어에서는 여러개의 변수를 한꺼번에 다루는, 즉 메모리 상에 같은 타입의 변수를 연속적으로 여러개를 한번에 정의할 수 있는 방법을 제공하고 있는데 이게 바로 배열(Array)이다.

 

int형 배열의 경우는 int형 변수들이 연속된 메모리상에 여러개 존재한다고 보면 된다.

 

배열의 기초

이 코드를 실행해보면 

와 같은 결과를 얻게 된다.

 

배열은 말 그대로 특정 타입의 변수들의 집합니다. 전에 변수를 정의할 때는 

 

(변수타입) (변수명);

 

으로 작성 했으나 배열은 비슷하지만 조금 다르다

 

(변수타입) (변수명)[원소의 갯수];

 

와 같이 정의한다.

 

위에 있는 

이 부분을 보면 int형 변수를 10개를 합친것과 같은 배열을 생성하는 것이다.

 

만약 char arr[10]이라고 한다면 원소들 모두 char형으로 선언된다.

 

또한 위와 같이 배열의 정의 옆에 ={value1, value2 ..}를 해준다면 배열의 각각 원소에는 중괄호 속의 각 값들이 순차적으로 들어가게 된다.

물론 이 방법은 배열을 정의할때만 가능하고, 이미 정의된 배열에선 불가능하다.

 

int arr[3] = {1, 2, 3} ==> 문제 없음

 

arr = {4, 5, 6} ==> 불가능

 

그리고 배열을 정의할 때 = {} 로 원소들을 정의했다면 배열의 크기를 생략하면 원소의 갯수에 맞게 알아서 배열을 정의 해준다.

 

그니까 

이렇게 배열의 크기 없이 초기화하는 값들을 넣으면 그 값의 갯수에 맞게 arr의 크기를 정해준다.

위의 경우는 int a[10]의 크기를 갖고 있는 배열이 생성되었다.

 

배열에서 원소 접근

배열에서 원소에 접근하는 방법은 변수명뒤에 []를 붙여주고 []사이에 접근하고자 하는 원소의 index를 넣어주면 된다,

 

arr배열의 1번 요소에 접근하는 법 : arr[0]

arr배열의 5번 요소에 접근하는 법 : arr[4]

 

배열의 index는 0부터 시작이니 실수하지 않도록 하자.

왜 0부터 시작하는지는 포인터를 다룰때 다시보자.

 

만약 크기가 10인 배열에 a[9]가 아닌 a[10]을 통해서 접근하면 어떻게 될까?

실행시켜보면 

알수 없는 값이 나와버렸다.(경우에 따라선 그냥 오류를 내고 종료하는 경우도 있다)

 

배열의 위험성

이걸 이해하기 위해서는 메모리 상에서 배열이 어떻게 있는지를 알아야한다.

 

위 그림은 arr이 메모리 상에서 어떤 모양으로 존재하는지를 보여주는 그림이다.

위 경우 배열의 시작주소는 0x1234이다.

 

C의 배열은 단순히 해당 타입의 변수들의 나열으로 생가각하면 된다.

그렇기에 배열에는 배열에 크기에 관한 정보가 전혀 없다.

다시 말해 arr[3]과 같이 코드를 사용한다면 C언어 상에서는 그냥 아 배열의 처음 위치로부터 4번째 원소이구나 라고 생각하는 것이다.

 

그러면 배열의 끝부분은 어떻게 되어 있을까?

arr배열의 끝을 보면 위 처럼 메모리 상에 arr[9]가 맨 마지막에 있을 것이고 그 뒤에는 다른 변수들의 데이터가 들어 있을 것이다.

하지만 arr[10]을 사용한다면 프로그램입장에선 arr[9]에 더 arr이 존재하는것으로 인식하고 해당 영역의 값을 보여주게 된다.

만약 이 부분이 메모리가 접근하지 못하는 영역이라면 프로그램에서 오류를 내면서 종료될 것이다.

아니라면 해당 부분을 사용하고 있는 데이터의 값이 출력될 것이다.

 

더 문제가 되는 부분은 모르고 arr[10]영역을 다른 값으로 덮어 씌우는 상황이다.

arr[10] = 3 했을때 그 위치에 변수 b가 존재한다면 b = 3 을 한것과 동일한 역할을 하게되는 것인데 이러면 오류를 찾기가 매우 어려워 진다.

따라서 배열을 사용할 때는 우리가 참조하는 원소의 위치가 배열보다 크기가 작은지 확인해봐야한다.

 

배열 가지고 놀기

원래 변수 10개를 선언하고 10번 printf문을 출력해야 했어야할 구문을 배열을 통해서 짧고 이해하기 쉽게 만들수 있다.

 

만약 변수를 10개 입력받는 부분을 추가하게 된다면 배열이 없을 경우엔 코드를 매우 길게 생성할 수 밖에 없을 것이다.

배열을 사용하면 

이렇게 간단하게 표현이 된다.

 

여기서 평균에 따라서 합격 불합격을 출력해주는 프로그램을 만들어 본다면 위 코드에서 합격 불합격을 검사해주는 코드만 추가해주면 된다.

그 결과는 

와 같다.

 

배열의 중요한 특징

그러면 배열의 원소의 수를 숫자로 직접 넣지 않고 변수로 지정할 수 없다.

 

int range =5;

int arr[range];

 

이런 방식은 불가능하다.

 

상수

상수는 변수의 정반대로 처음 정의시 그 값이 바로 주어지고 값이 영원이 변하지 않는다.

상수를 선언하는 방법은 

 

const (타입) (변수명) = (초기화할 값);

 

과 같이 사용한다

 

 

상수라고 해서 특별한건 아니고 그냥 처음 한번 지정된 값은 절대로 변하지 않는다는 점일 뿐이다.

그렇기에 처음 상수를 정의할때 값을 정의해주지 않는다면 

이런 에러를 출력하게 된다.

 

상수는 지정한 값도 변경할 수 없다.

 

즉, 상수는 불변의 데이터이다.

그렇기에 배열의 크기를 상수로 지정할 수 있을까 생각하지만 

이렇게 에러를 발생하면서 안되는걸 알 수 있다.

 

상수로 배열의 크기를 할당 할 수 있다면 그냥 변수로도 크기를 할당할 수 있다는 말이나 마찬가지이기 때문에 불가능하다.

 

상수는 나중에 많이 사용될 것이기 때문에 그냥 일단 알아두자.

 

초기화 되지 않은 값

변수를 초기화 하지 않는다면 변수는 어떤 값을 가질까

아래 코드를 보자.

그냥 보지도 않고 에러라고 던져버린다.

 

그러면 배열의 경우는 어떨까?

arr[0]으로 값을 넣어주고 arr[1]을 출력해보려고 했는데 

(강사님은 아예 에러로 출력을 안시켰는데 여기선 또 된다... 버전의 차이인가 싶다)

 

그렇다면 마지막으로 아래의 코드를 보자.

마지막은 에러도 이상한 값도 아닌 그냥 0으로 출력 되었다.

 

여기서 arr[3] = {1}을 한다면 arr[3] = {1, 0, 0}로 자동으로 컴파일러가 인식하고 넣어주기 때문에 나머지 자리에 0이 들어가 있는 것이다.

arr[5] = {1, 2, 3}을 했을 때도 arr[5] = {1, 2, 3, 0, 0}과 동일하게 컴파일러가 인식해서 값을 할당해준다.

 

11 - 2. C 언어의 아파트2 (고차원의 배열)

2차원 배열에 대해서 이해해보자.

먼저 이차원 배열이란건 배열의 배열을 의미하는 것이다.

조금 더 구체화 해보자면 int형 배열이란 것은 배열 안에 모든 원소가 int형 변수인 것인 것인데 배열의 배열이라고 한다면 배열의 모든 요소가 배열로 존재하는 것을 의미하는 것이고 이를 2차원 배열이라고 말한다.

 

2차원 배열을 선언하는 방법은 아래와 같이 

 

(배열 타입) (배열 이름)[?][?];

 

과 같이 선언한다.

 

 

int arr[3][2];

 

라고 한다면 크기 3의 배열안의 각 배열 요소에 크기가 2인 int형 배열이 존재한다는 의미로 그 모양은 

출처 - https://modoocode.com/20

이런 형태를 띄고 있다.

 

arr[0]의 의미는 int형 원소를 2개 가지는 배열 하나를 의미하는 것이고 그 원소는 arr[0][0]과 arr[0][1]을 의미한다.

 

일차원 배열과 이차원 배열은 그림으로 보면 더 뚜렷하게 이해가 된다.

출처 - https://modoocode.com/20

 

arr[m][n]이라고 한다면 arr[m]이 더큰 부모이고 그 아래에 arr[m][n]이 존재하는 것이다.

 

그럼 2차원 배열을 가지고는 어떤 것을 할 수 있을까?

학생의 점수를 보관한다고 한다면 한 학생의 하나의 점수만 넣을 수 있던 일반 배열과는 달리 한 학생의 arr[m] 여러가지의 과목 arr[m][n]을 보관할 수 있게 된다.

 

그런데 사실 메모리에는 모든 배열이 일차원 배열과 다름없이 들어가게 된다.

그러면 왜 2차원 배열이라고 할까?

 해당 코드를 실행해보면

와 같이 출력이 된다.

 

2차원 배열이나 1차원 배열 모두 메모리 상에서는 연속적으로 존재하게 된다.(메모리는 항상 1차원이다.)

그런데 2차우너 배열을 생각할때는 원소들이 

와 같이 존재한다고 생각할 수 있다.

그런데 사실은 일차원 배열은 한개의 인덱스로 원소에 접근하는 것이고 이차원 배열은 두개의 인덱스로 원소에 접근하는 것이다.

(그렇다고 a[3][3]을 a[8]로 접근할 수 있는 것은 아니다..)

위 코드를 실행해보면

와 같이 실행된다.

 

2 차원 배열 정의하기

기존에는 2차원 배열을

이렇게 선언 했었는데 

이렇게 선언도 가능하다.

 

그리고 기존의 배열에서는 

이런식으로 초기화를 하게 되면 자동으로 갯수를 확인해서 그 크기의 배열을 생성해줬던과 동일하게 

이런 식으로 크기를 작성하지 않아도 배열의 크기를 알아서 설정해주므로 생성이 가능하다.

그러나 역으로 

이렇게는 선언이 불가능하다.

C언어의 다차원배열의 경우 맨 앞의 크기를 제외한 나머지 크기들은 정확히 지정해줘야만 오류가 나오지 않는다.

 

3 차원, 그 이후 차원의 배열들

2차원 배열을 이해했다면 3차원 배열을 이해하는 것도 어렵지 않을 것이다.

보통 3차원 배열을 활용하는 경우는 많지 않은것 같으나 알아두고 넘어가자.

3차원 배열의 선언은 

 

(변수 타입) (변수명) [x][y][z];

 

와 같이 선언이 가능하다.

 

그 모양은 입체적으로 생각해보면 좋다.

위에서 만든 평면상의 2차원 배열(y개의 배열의 모든 요소에 z개의 요소를 가진 배열을 담고 있는 배열)이 x겹으로 겹쳐져 있다고 생각하면 된다.

 

이게 이해가 됐다면 문제는 4차원 배열부터이다.

우리는 3차원 세상에 살고 있기에 4차원에 대해 이해하기가 쉽지 않다.

그런데 위에서 1차원 배열의 경우는 한개의 인덱스로 요소를 찾는것이고 2차원의 경우는 2개의 인덱스로 요소를 찾는 것이라고 했던 것처럼 그냥 4개의 인덱스로 요소에 접근하는 것이라고 생각하는게 좋다.

 

궂이 이미지를 그리려고 할 필요는 없을 것 같다고 생각이 든다...