코딩 공부/C, C++

5. define과 비트연산자

갬성꿈돌이 2024. 1. 19. 22:55
반응형
반응형

목차

    전처리기

     

     

    입력 데이터를 처리하여 다른 프로그램에 대한 입력으로서 사용되는 출력물을 만들어내는 프로그램을 전처리기라고 하는데 컴파일러가 실행되기 직전에 단순히 텍스트를 조작하는 치환 역할을 하기도 하고, 디버깅에도 도움을 주며 헤더 파일의 중복 포함도 방지하는 것으로 #이라는 기호로 시작한다. 첫날에도 설명했지만 우리가 처음 쓰는 #include <stdio.h>도 C언어 표준 라이브러리의 헤더파일을 포함하겠다고 선언하는 전처리기이다.

     

    전처리기의 종류는 아래와 같다.

    지시어 의미 지시어 의미
    #define 매크로 정의 #endif 조건 처리 문장 종료
    #include 파일 포함 #ifdef 매크로가 정의되어 있는 경우
    #undef 매크로 정의 해제 #ifndef 매크로가 정의되어 있지 않은 경우
    #if 조건이 참일 경우 #line 행번호 출력
    #else 조건이 거짓일 경우 #pragma 시스템에 따라 의미가 다름

     

     

     

    Define

     

     

    그 중 Define 전처리기에 대해 설명한다.

     #define 은 상수(숫자, 기호, 문자열 등)나 심지어 함수까지 특정 문자로 치환할 수 있는 기능으로 정의하는 것으로 생각하면 되며, 이는 '매크로'라고 불린다.

     

    매크로의 장점은 호출 사용이 아니라 컴파일 직전에 코드 자리에 대입되기 때문에, 수행 속도가 빠르다!

    그러나 코드가 길수록 가시성이 떨어져 보통 3줄이상은 쓰지 않고 매크로가 들어가야 할 코드가 발견될 때마다 대입 해주어야 하므로 실제 실행 시 코드의 대입이 반복되어 프로그램의 크기가 늘어나게 된다.

     

    따라서 프로그램의 크기와 실행 속도 중 중요한 것이 무엇인지 판단하고 매크로를 쓰거나 함수로 쓰는 것을 선택해야한다..

     

     

    캐릭터의 상태를 예시로 들어보자.

    아래처럼 #define을 통해 Hungry, Stress, Thirsty를 미리 매크로 정의로 선언하면 우리는 세 상태의 숫자값을 기억하지 않고 관련 상태를 텍스트로 적을 수 있고, 상태가 들어가야할 모든 코드를 찾아서 정의된 숫자로 치환해준다. 아래 내용은 define만 설명하기 위해 대충 만든 것이니 내용을 신경쓰지 마십시오.

    #include <stdio.h>;
    
    #define Hungry 1
    #define Stress 2
    #define Thirsty 3
    
    int main() {
    
    	int MyStatus = Hungry + Stress + Thirsty;
    
    	printf("현재 과로 상태는 %d입니다.\n", MyStatus);
    
    
    	int NeedFeed = Hungry + Thirsty;
    
    	printf("현재 섭취 부족 상태는 %d입니다.\n", NeedFeed);
    
    	return 0;
    }

     

     

     

     

    비트연산자(bitwise operator)

     

    꽤 중요한 내용인데 상대적으로 실제 사용 빈도가 다른 연산자들에 비해 비교적 적어서(실제로는 정말 많이 쓰지만 다른 것들에 비해 비교적 적다) 시간이 지나면 잘 까먹어서 자주 보고 계속 상기시켜야한다.

     

    비트 연산자는 비트(bit) 단위로 논리 연산을 할 때 사용하는 연산자로 전체 비트를 왼쪽이나 오른쪽으로 이동시킬 때도 사용한다. 

    & 대응되는 비트가 모두 1이면 1을 반환함. (비트 AND 연산)
    | 대응되는 비트 중에서 하나라도 1이면 1을 반환함. (비트 OR 연산)
    ^ 대응되는 비트가 서로 다르면 1을 반환함. (비트 XOR 연산)
    ~ 비트를 1이면 0으로, 0이면 1로 반전시킴. (비트 NOT 연산)
    << 지정한 수만큼 비트들을 전부 왼쪽으로 이동시킴. (left shift 연산)
    >> 부호를 유지하면서 지정한 수만큼 비트를 전부 오른쪽으로 이동시킴. (right shift 연산)

     

     

     

     

    비트간의 연산

     

    &(AND)

    이처럼 비트 AND 연산자는 대응되는 두 비트가 모두 1일 때만 1을 반환하며, 다른 경우는 모두 0을 반환.

     

    |(OR) : Shift + \로 누르면 |가 입력

    비트 OR 연산자는 대응되는 두 비트 중 하나라도 1이면 1을 반환하며, 두 비트가 모두 0일 때만 0을 반환.

    ^(XOR)

    비트 XOR 연산자는 대응되는 두 비트가 서로 다르면 1을 반환하고, 서로 같으면 0을 반환.

    ~(NOT)

    비트 NOT 연산자는 해당 비트가 1이면 0을 반환하고, 0이면 1을 반환.

     

     

     

     

    shift 연산

     

     

    위의 그림에서 <<(Left Shift, 왼쪽으로 쉬프트), >>(Right Shift, 오른쪽으로 이동)에 해당하며 비트를 화살표가 가리키는 방향으로 한 비트(한 칸이라고 표현하겠다)씩 민다.

     

    (0,0,0,0,0,0,0,1)의 1Byte크기의 1을 예시로 들어보자.

    unsigned char byte =1;
    
    byte <<= 1 ;

    실행결과 : (0,0,0,0,0,0,0,1)=1을 왼쪽 방향으로 한 칸씩 밀어 (0,0,0,0,0,0,1,0)=2가 된다.

     

    왼쪽 방향으로 비트를 2칸씩 밀어낸 결과

     

    ​결과적으로 보면

     

    byte <<= n; 는 2^n의 배수.

    byte >>= n; 는 2^n으로 나눈 몫(어차피 뒤에 비트는 탈락이니 나머지는 없다).

     

     

     

     

    Define+비트연산 예시

     

     

    제대로 예를 들어주면 보통 16진수(0x를 앞에 써주면 16진수 표현법)로 캐릭터의 상태를 모두 비트 안에 넣어준다.

     

     

    그러면 이제 비트의 자리를 밀어내서 코드 한줄로 캐릭터의 위독 중첩 상태를 표현할 수 있다.

    일일이 참/거짓을 판단하지 않고 모든 상태의 변수를 비트에 넣어 하나의 값으로 표현이 가능하다. Goat다.

     

     

     

     

     

     

     

    아래는 비트연산에 대해 더욱 자세하게 설명된 네이버 블로그 글. 이해 안될 때마다 다시 보고 공부하자.

     

    https://blog.naver.com/PostView.naver?blogId=yuyyulee&logNo=221114544260

     

     

    [아두이노 강좌] 56. Bit 연산 (5) - <<, >> (Shift)

    >> 지난 강좌 - Bit 연산 (1) ~ (4) 드디어 Bit 연산의 마지막 시간, '<<(Left Shift...

    blog.naver.com

     

    '<<(Left Shift)' 연산은 모든 비트가 왼쪽으로 정해진 수만큼 이동하는 기능을 수행한다. 이항 연산자로, "A<<B"라고 표시하여 'A'를 왼쪽으로 'B'만큼 비트 이동하라는 의미로 사용한다. 예로 보는 것이 빠를 듯. 우리의 숫자 '100'.

    100 << 2 = 144

    2진수로 '01100100'인 숫자 '100'을 '2'만큼 <<(Left Shift) 연산을 한 결과이다. 모든 비트가 왼쪽으로 두 자리씩 이동한 것을 알 수 있다.

    여기서 주의 깊게 봐야 할 곳은 최상위 비트와 최하위 비트 부분.

    100 << 2 = 144

    숫자 '100'의 최상위 비트 2자리는 <<(Left Shift) 연산 후 소멸되고, 결과 값의 최하위 비트 2자리는 '0'으로 채워진 것을 알 수 있다.

    만일 <<(Left Shift) 연산의 대상이 2바이트 크기를 가진다면? 다음은 Int 타입의 숫자 '100'을 3만큼 <<(Left Shift) 연산한 결과이다.

    100 << 2 = 400 (int Type)

    1바이트 크기였다면 소멸했을 6, 7번 비트 값이지만, 결과 값이 2바이트 크기를 가지게 되어 8, 9번 비트로 이동되고 결과 값은 '400'이 된다. 역시 최하위 비트는 '0'으로 채워지고.

    기억해야 할 세 가지.

    1. 결과 값의 크기는 피연산자의 타입에 의해 결정된다.

    2. 결과 값의 범위 밖으로 넘치는(Overflow) 비트는 소멸, 새로운 비트는 '0'으로 채워진다.

    3. 피연산자에는 정수 형태의 값(byte, int, long)만 사용할 수 있다.

    또 하나, 알아둬야 할 <<(Left Shift) 연산의 중요한 특징.

    다음은 숫자 '100'을 1만큼 <<(Left Shift), 2만큼 <<(Left Shift), 3만큼 <<(Left Shift) 연산한 결과 값을 각각 나타낸 것이다.

    규칙이 눈에 보이는가?

    숫자 '100'을 1만큼 <<(Left Shift) 연산한 결과는 '200', 2만큼 <<(Left Shift) 연산한 결과는 '400', 3만큼 <<(Left Shift) 연산한 결과는 '800'이다. 그럼 4만큼 <<(Left Shift) 연산한 결과는??

    그렇다. 1만큼 <<(Left Shift) 할 때마다 값이 정확히 2배씩 증가하여, 4만큼 <<(Left Shift) 한 결과는 '1600'이 될 것이다.

    <<(Left Shift)에서 기억해둬야 할 마지막 사항.

    4. "A << B"의 연산 시 결과 값은 "A * (2의 B제곱)" 한 결과와 같다. 단, 결과 값의 범위 내에서.

    중요한 것은 결과 값의 범위 내에서, 라는 것. Overflow가 발생하여 상위 비트가 소멸되면 결과 값은 달라진다. byte 타입의 숫자 '100'을 2만큼 <<(Left Shift) 연산했을 때 결과 값이 '144'였다는 것을 다시 한번 생각하자.

    >> (Right Shift) 연산

    <<(Left Shift) 연산을 살펴봤으니, '>>(Right Shift)' 연산은 쉽게 이해할 수 있을 것이다.

    다음은 숫자 '100'(byte 타입)을 2만큼 >>(Right Shift) 연산한 결과이다.

    100 >> 2 = 25

    모든 비트가 오른쪽으로 2칸씩 이동하여 결과 값이 '25'가 되는 것을 볼 수 있다. <<(Left Shift) 연산과 마찬가지로, 새롭게 채워지는 최상위 비트들은 '0'으로 채워지며, 오른쪽으로 밀려나는 최하위 비트들은 소멸된다. 단, 최상위 비트가 '0'으로 채워지는 이유는 예의 '100'이라는 값이 현재 byte 타입이기 때문인데, 여기에 대한 자세한 내용은 아래에서 다시 설명하겠다.

    어, 그럼 <<(Left Shift) 연산에서 '2의 B제곱'만큼 값이 증가하던 규칙이 있었던 것처럼 >>(Right Shift) 연산에도 규칙이 있는걸까?

    있다.

    다음은 숫자 '100'을 '1'만큼 >>(Right Shift) 연산한 결과와 '2'만큼 >>(Right Shift) 연산한 결과, '3'만큼 >>(Right Shift) 연산한 결과를 나타낸 것이다.

    1씩 >>(Right Shift) 될 때마다 1/2씩 감소하는 것을 볼 수 있다. 정수 타입이므로 '12.5'가 나올 수 없다는 건 말 안해도 알겠지요?

    자, 이번엔 >>(Right Shift) 연산에서 가장 유의해야 할 사항.

    바로 피연산자의 부호이다. >>(Right Shift) 연산이 실행될 때 아두이노는 제일 먼저 피연산자의 타입을 확인한다. 만일 피연산자의 타입이 int나 long으로 부호를 가지는(signed) 정수 타입이라면 최상위 비트가 '0'인지, '1'인지를 다시 확인한다.

    int나 long 타입에서 최상위 비트가 '1'이라는 것은 해당 값이 음수(-) 값임을 나타낸다. 아두이노는 피연산자가 부호를 가지는 타입이고 최상위 비트가 '1'인 음수 값일 경우, >>(Right Shift) 연산 시 최상위 비트를 '1'로 채운다.

    예를 들어보자. 다음은 int 타입의 숫자 '100'과 '-100'을 각각 2 비트 씩 >>(Right Shift) 연산한 결과이다.

    100 >> 2 = 25

    -100 >> 2 = -25

    음? 진짜로 저렇게 되냐구? 직접 찍어보자.

    void setup() { // put your setup code here, to run once: Serial.begin(19200); } void loop() { // put your main code here, to run repeatedly: int positive_i = 100; int negative_i = -100; Serial.print("100 : "); Serial.println(positive_i, BIN); Serial.print("100 >> 2 : "); Serial.println(positive_i >> 2, BIN); Serial.print("-100 : "); Serial.println(negative_i, BIN); Serial.print("-100 >> 2 : "); Serial.println(negative_i >> 2, BIN); while(1) ; }

    여기서 "-100"이 4바이트로 찍힌 건, print() 함수 내부에서 long 타입으로 자동 변환되기 때문.

    그럼, 그냥 단순히 비트 이동만을 위해 최상위 비트를 '0'으로 채우고 싶을 때는 어떻게 해야 할까? 그때는 형변환을 사용해야 한다. int 타입은 unsigned int 타입으로, long 타입은 unsigned long 타입으로 형변환한 뒤 >>(Right Shift) 연산을 실행하면, 이 값들은 음수(-) 값을 가질 수 없는 타입이므로 최상위 비트가 '0'으로 채워진다.

    >>(Right Shift) 연산의 경우 언어마다 조금씩 다르게 실행될 수 있으므로 주의가 필요하다. 예를 들어 자바의 경우 최상위 비트를 부호 비트로 채우는 >>(Right Shift) 연산 외에, 무조건 '0'으로 채우는 '>>>(Unsigned Right Shift)' 연산 기호가 따로 있다.

    정리하면,

    5. "A >> B"의 연산 시 결과 값은 "A / (2의 B제곱)" 한 결과와 같다. 단, 결과 값은 정수.

    6. 피연산자가 부호를 가지는 정수 타입이고 최상위 비트가 '1'일 때, >>(Right Shift) 연산 실행 시 최상위 비트는 '1'로 채워진다.

    반응형

    '코딩 공부 > C, C++' 카테고리의 다른 글

    6. 함수와 변수  (1) 2024.01.23
    4. 조건문  (0) 2024.01.17
    3. 연산자  (0) 2024.01.17
    2. 자료형  (0) 2024.01.17
    1. C언어 기초  (0) 2024.01.16