F.R.I.D.A.Y.

[TIPS 19TH] 05 : 2018.07.09. (월) 본문

외부활동/TIPS 19th

[TIPS 19TH] 05 : 2018.07.09. (월)

F.R.I.D.A.Y. 2018. 7. 10. 10:18
반응형

1. 시프트 연산에서 주의

아래 코드를 보자


char a = 27;
char b = 27;

a = a << 5 >> 3;
b = b << 5;
b = b >> 3;

 위 연산을 마치고 a와 b는 값이 어떻게 나올까? 정확한 값은 모르겠지만 a와 b의 초기값이 같으며 똑같은 연산을 한줄에 했느냐 두줄에 풀어 썼느냐의 차이만 가지므로 결과는 같은 값을 가진다고 느낀다. 하지만, a와 b를 출력하면 다른 값이 나온다.


 이러한 문제가 발생하는 이유는 시프트 연산을 할 때 char 데이터형으로 작업이 이뤄지는 것이 아니라 int로 캐스팅되어 이뤄지기 때문이다[각주:1]. 따라서


a = a << 5 >> 3;

 밑줄 친 부분에서 이미 int로 캐스팅이 이뤄져 [ 0001 1011 ][각주:2]를 5를 넘기면  [ 0110 0000 ]가 되는 것이 아니라

 [ 0000 0000 0000 0000 0000 0011 0110 0000 ]( 864 )가 되고 여기서 다시

 [ 0000 0000 0000 0000 0000 0000 0110 1100 ]이 되어 108이 된다. 즉, 연산과정에서는 [ int ] 형식으로 값이 처리되고 a에 집어 넣을 때 char로 형변환, 정확히는 LSB에 가까운 1 Byte에 해당하는 값만 저장되어 값이 달리 나오게 된다.

 따라서 같은 값을 유도하고 싶다면


a = a << ( 24 + 5) >> (30 - 3); // 1번
a = ((a << 5) & 0xFF) >> 3;     // 2번
a = (char)(a << 5) >> 3;        // 3번


1번처럼 4 Byte의 끝으로 밀어버려서 오버플로우를 발생시키거나

 2번처럼 1 Byte에 들어갈 수 있는 최대 값을 비트연산해버리거나

 3번처럼 캐스팅을 해주는 식으로 해결할 수 있다.



2. 차원 배열

 우리가 받아들이는 정보 중에서 시각은 대부분 2차원에 해당하는 정보를 받아들인다[각주:3]. 사용자가 인간인 만큼 컴퓨터도 2차원 데이터로 표현을 해야하는 경우가 많다. 컴퓨터는 물리적으로 1차원 배열만을 지원하지만, 컴파일러가 개발자들의 편의를 위해 2차원 이상의 다차원 배열을 제공한다. 코드가 2차원 이상인 경우에 이를 1차원 배열로 정렬시키는 식을 통해 다차원 배열을 지원하는 것처럼 만드는 것이다.


int data[2][3];
for (int i = 0; i < 2; i++) {
	for (int k = 0; k < 3; k++) {
		data[i][k] = i * 3 + k;

	}
}

 위 코드는 2차원 배열 변수인 [ data ]에 차례로 0부터 5까지 넣는 코드이다.


 생각하기에는 위 그림처럼 나오겠지만, 실제 메모리에는 아래 그림처럼 저장된다.


2차원 배열로 만들면 복잡해보이지만, 이를 일차원 배열로 만들게 되면 상당히 직관적으로 보인다.


int temp[6];
for (int i = 0; i < 6; i++) {
	temp[i] = i;
}

 똑같은 일을 하는데 아래 코드가 훨씬 보기가 편하고 어디에 무슨 값이 들어갈지 한 눈에 보인다. 컴파일러에 의존하게 되면[각주:4] 간단하게 보일 코드가 복잡해지므로 컴파일러에 의존적이지 않은 개발자가 되기위해 노력해야한다.


1차원 배열( temp[i] ) >> 2차원 배열( data[m][n] ) 변환 공식

>> data[i / n][i % n]

2차원 배열( data[m][n] ) >> 1차원 배열( temp[i] ) 변환 공식

>> temp[k *m + j][각주:5]


 추가로 일반적으로 [x][y]순으로 배열을 생성하는 경우가 많은데, [y][x]순으로 생성하는 것이 메모리 연산 효율에 더 좋다고 한다. 이유는 다음과 같다.



3. 포인터

 포인터는 다른 변수의 메모리 주소를 저장하는 변수이다.

 [ data type ] *[ variables Name ] 형식으로 선언할 수 있다. 여기에서 우리가 일반적으로 별표라고 부르는 * 은 애스터리스크 라고도 불리며, 포인터에서는 다른 일반 변수와 구분되는 포인터 변수임을 컴파일러에게 알려주는 용도이다.

 포인터는 기본적으로 4 Byte 크기를 가진다. 이유는 메모리 주소를 저장하는 변수이기 때문이다. 앞의 [ data type ]에 int나 long등 4 Byte크기의 데이터형식이 작성되지 않더라도 4 Byte를 가진다. 선언부 앞의 [ data type ]은 해당 포인터 변수가 가리키는 변수의 데이터 형을 의미한다.

 OS Bit

 개발 Bit

 프로그램 포인터 크기

 x86

32비트

 x86

 4 Byte

 x64

 실행 불가

 x64

64비트

 x86

 4 Byte

 x64

 8 Byte


 포인터는 포인터 변수 이름 앞에 [ * ]를 붙임으로써 다른 변수에 접근할 수 있다.

 포인터를 처음 배울 때, 선언할 때의 [ * ]와 포인터를 이용해 다른 변수로 접근하는 간접접근의 [ * ]를 혼동하는 경우가 많은데 간단히 생각하면 선언부의 [ * ]는 변수명에 붙어있는 것이 아니라 데이터 형에 붙어있느 것이라고 보면 되고, 간접 전근의 [ * ]는 변수명에 붙어있는 것으로 생각하면 쉽다.

선언부 *    : int* p;

간접접근 * : *p = 4;


 만일 포인터 p의 위치가 100번지이고 var의 위치가 106번지라면 그림은 위와 같다. 


 *p를 사용할 때 프로그램은 아래와 같이 작동한다.



 코드의 8번 줄에 [ a ]변수 앞에 [ & ] 기호가 붙어 있는 것을 볼 수 있는데, 이 &는 주소를 반환하는 번지 계산 연산자라고 한다.

 코드의 9번 줄에 [ p ]변수 앞에 [ * ] 기호는 해당 포인터 변수가 저장하고 있는 주소의 변수를 부르는 번지 지정 연산자라고 한다.


 C언어가 코드를 블럭으로 나누게 되면서[각주:6] 다른 블럭에 있는 변수로 접근할 수 없게 되었다. 그러나 포인터를 사용하면 다른 블럭에 존재하는 변수에 간섭할 수 있게 된다.

 포인터를 배우기 전에는 함수를 사용하고 반환을 통해 값을 전달 했지만, 포인터를 사용하면 간접접근을 통해 콜러(caller)에 있는 변수 값을 제어할 수 있기 때문에 포인터를 배우고 나서 반환(Return)값은 주로 해당 함수의 상태( 성공, 실패 등)를 반환하는데 사용한다.



Etc.

 코드를 작성할 때 느끼지 못하는 비효율 중 가장 큰것이 반복문 안의 if문 등의 조건문이라고 한다. 극강의 퍼포먼스가 필요한 경우에는 필요에 따라 반복문을 둘 이상으로 쪼개는 식의 작업을 한다.


추가 변수 없이 두 변수의 값을 교환하는 방법


int a = 5;
int b = 7;

a ^= b; // a = a ^ b;
b ^= a; // b = a ^ b;
a ^= b; // a = a ^ b;



  1. CPU에서는 연산을 할 때 1 Byte 단위로 연산을 하지 않고 2, 4, 8 Byte 단위로 연산을 한다고 한다. [본문으로]
  2. 27의 2진표기 [본문으로]
  3. 눈 한 쪽에서 받아들이는 위상과 반대 쪽에서 받아들이는 위상의 차이를 이용해 뇌에서 2차원 정보를 3차원으로 보강한다. [본문으로]
  4. 다차원 배열은 컴파일러가 제공하는 형식이다. [본문으로]
  5. k는 외부 반복문의 변수이고, j는 내부 반복문의 변수이다. [본문으로]
  6. 지역화 [본문으로]
728x90
반응형

'외부활동 > TIPS 19th' 카테고리의 다른 글

[TIPS 19TH] 07 : 2018.07.16. (월)  (2) 2018.07.17
[TIPS 19TH] 06 : 2018.07.13. (목)  (2) 2018.07.13
[TIPS 19TH] 04 : 2018.07.05. (목)  (2) 2018.07.06
[TIPS 19TH] 03 : 2018.07.02. (월)  (0) 2018.07.03
[TIPS 19TH] 02 : 2018.06.28. (목)  (4) 2018.06.29
Comments