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

[TIPS 19TH] 04 : 2018.07.05. (목) 본문

외부활동/TIPS 19th

[TIPS 19TH] 04 : 2018.07.05. (목)

F.R.I.D.A.Y. 2018. 7. 6. 11:30
반응형


1. 비트 연산자

 비트 연산자란 어떤 값을 비트 단위로 계산하는 연산자로, 일반적인 연산자와 달리 각 비트끼리 연산을 행한다.

 비트 연산자는 다음으로 이루어져 있다.

 연산자

 의미

 예시 [식 - 결과]

 &

 비트 AND : 두 값 모두 1이어야 1 반환

 0101 & 1111

 0101

 |

비트 OR : 하나라도 1이면 1 반환 

 0101 | 1111

 1111

 ^

비트 XOR : 서로 반대인 경우에 1 반환 

 0101 ^ 1111

 1010

 비트 연산자는 단항 연산자처럼 번역이 되기 때문에 이항 연산자에 비해 속도면에서 이득을 볼 수 있다.

 비트 연산자를 사용하면 다음과 같은 문제도 풀 수 있다.


 Q. 몇 번째 비트가 켜져 있는가? (켜짐 : 1, 꺼짐 : 0)


 다음과 같은 코드를 통해 몇번 비트가 켜졌는지 알 수 있는데, getBitState() 함수에서 어떠한 값과 몇번 비트를 판단할것인지 값을 전달받으면 해당 값을 판단해 bool[각주:1] 형태로 전달해주고 삼항 연산자를 통해 켜지고 꺼지는 것을 출력한다.


 한가지 더 참신한 것은 비트 XOR을 이용하면 굳이 0을 대입하지 않더라도 0으로 초기화 할 수 있다.

a라는 변수가 초기화되지 않았다면


int a = a ^ a;
// a에는 0이 들어간다.


 a에 어떠한 값이 들어가더라도 비트 XOR 연산자는 서로 반대되는 비트를 연산했을 때만 1을 반환하므로 같은 값을 XOR 연산하게 되면 무조건 0이 될 수밖에 없다.



2. 시프트 연산자

 이 시프트 연산자 또한 비트 연산의 일종이다. 그러나 시프트 연산자만으로 충분히 한 단락을 만들 가치가 있다고 판단해 비트 연산자에서 빼왔다. 먼저 비트 연산자는 아래처럼 두 개가 존재하며 기능은 표와 같다.

 연산자

 의미

 예시

 A << B

 A의 모든 비트를 B만큼 왼쪽으로 옮긴다.

 4 << 2

 16

 A >> B

 A의 모든 비트를 B만큼 오른쪽으로 옮긴다.

 8 >> 2

 2

 왼쪽 시프트( << )의 경우 주어진 수[각주:2]에 2^n[각주:3]을 곱해준다. 역으로 오른쪽 시프트( >> )의 경우에는 주어진 수를 2^n만큼 나누어버린다.


이 시프트 연산에서 가장 유의해야할 점은 시프트 연산을 계속 한다고 해서 값이 그만큼 증가하는 것은 아니라는 점이다.

예로 [ 0x01 ]을 왼쪽 시피트 연산으로 30번 하면[각주:4] 값은 1,073,741,824[각주:5]이지만, 여기서 한번 더 왼쪽 시프트 연산을 하면 값은 -2,147,483,648[각주:6]이 된다. 이유는 연산 과정에서 오버플로우가 발생했기 때문이다.


오버플로우를 하기 전에, 먼저 (signed) int의 범위를 알아볼 필요가 있는데 int의 범위는 다음과 같다.

-2,147,483,648 ~ 0 ~ 2,147,483,647

그럼 다시, 0x01 << 31을 해보자. 단, 이번엔 signed가 아니라 unsigned int로.


printf("%u\n", (unsigned int)0x01 << 31);
// 출력 : 2,147,483,648

 그리고 값을 보면 정상적으로 [ 0x01 << 30 ]의 두배가 되는 것을 확인할 수 있다.

 signed int는 메모리상에서 이렇게 저장된다.

 signed 타입의 데이터들은 음수를 포함한 정수 부분을 표현해야하기 때문에 MSB 한 비트를 부호를 결정하는 데 사용한다. 따라서 MSB를 제외한 31비트가 숫자를 결정하게 되는데, 양수의 최대값은 [ 2,147,483,648 - 1 ]로 결정된다. 21억에서 1을 빼는 이유는 컴퓨터에서는 0이 양수로 들어가기 때문이다. 컴퓨터 상에서는 양수와 음수는 약 21억개로 동일한 개수를 가지지만, 실제 수학에서 가지는 의미와 상이하기 때문에 이러한 오류를 범할 수 있다.


 이런 특성을 이용해 보수라는 개념이 도입되었다. unsigned char 크기에 들어있는 255에 1을 더하면 0이 되어버리는데 [ 1111 1111 ]에 1을 더하니 [ 1 0000 0000 ]가 되고, 1 Byte만 저장할 수 있으므로 [ 0000 0000 ]가 된다.

 보수는 여러 종류가 있는데 일반적으로 1의 보수와 2의 보수를 자주 사용한다.

>> 1의 보수 : 각 자리의 비트를 반전시킨 것

>> 2의 보수 : 1의 보수에서 1을 더한 것.



3. 변수 타입

 변수는 어디에 작성했느냐에 따라 해당 변수의 수명이 달라진다.

 타입

 수명 및 특징

전역 변수 

프로그램의 생명주기와 같이 함

프로그램의 모든 부분에서 사용 가능

지역 변수 

변수를 선언한 부분[각주:7]과 생명주기를 같이함

해당 변수를 선언한 부분과 해당 부분의 하위에서만 사용 가능 


 변수 데이터 타입 앞, 혹은 뒤에 다음과 같은 단어를 넣을 수 있다. 

단어 

특징 

const 

상수 선언 

static 

 전역 변수의 성격과 지역 변수의 성격의 혼합혼종

extern 

실제 존재하지는 않지만 다른 곳에서 선언됨을 알려줌 

  const를 사용하면 해당 변수는 선연과 동시에 값을 줘야 하고, 변수는 값으로 대입[각주:8]할 수 없다. 또, 한번 값을 받고 난 후에는 값을 변경할 수 없다.


int main(void) {
	const int constant = 13;
	constant = 6;            //error

}

위 코드를 작성해보면 [ constant ]를 변경하려고 하니까 오류가 발생한다. [ const int constant ] 이렇게만 작성해도 오류가 발생한다. 값이 할당되지 않았기 때문이다.


#include <stdio.h>

int a(void) {
	static int arb;
	
	return ++arb;
}

int main(void) {

	for(int i =0; i < 10;i++) printf("%d\n", a());

	return 0;
}

 위 코드를 작성하면 1부터 10까지 차례로 값이 출력된다. 이유는 static이라는 [ a() ]함수의 [ arb ]라는 변수가 생명주기를 프로그램과 함께하기 때문이다. static을 붙이면 지역변수처럼 선언을 해도 전역변수 성질을 가지게 된다.

 사용 가능 범위

지역 변수를 따름 

 생명 주기

전역 변수를 따름 


 extern 의 경우에는 코드를 여러 문서로 나누었을 때, 컴파일러는 서로 다른 문서에 있는 전역변수를 통일시키지 못한다. 따라서 다른 문서에서 선언한 전역변수를 현재 사용중인 문서에서 사용할 수 없는데, 컴파일러에게 이미 선언되어있으니 통일시키라는 식으로 알리기 위해 변수 데이터 형 앞에 extern 을 붙여준다.

사용법은 다음과 같다.


// a.c 

int a = 3;

int main(void){ ...

// b.c

extern int a;


이 때, extern을 사용한 전역변수에 값을 대입할 수 있으나, 그렇게 되면 extern은 무시된다. 또, 이미 선언된 전역변수 명과 같은 전역변수를 선언하고 그 앞에 extern을 붙여주면 에러가 발생한다. 존재하지 않는 전역변수가 있다고 extern [ data type ] [ variables name ] 이런 식으로 작성해도 에러를 발생시킨다.

// a.c

int a;
int b;
int c;

// b.c

int a;            // error : a.c 파일에 이미 a가 선언되어있음
extern int b;     // 정상
extern int c = 4; // error : a.c에 이미 c가 존재하기 때문에 중복으로 선언됨 (extern 키워드가 무시됨)
extern int d = 5; // 문제 없음



4. 배열

 원래는 포인터를 사용해야하지만 C의 창시자가 입문자를 위해 만든 문법이라고 한다. 프로그램은 메모리 주소를 가지고 작업을 하는데 개발자가 메모리 주소를 외우는 것이 비효율적이기 때문에 변수를 생성했다. 그런데 변수는 메모리 주소로 코딩을 했을때와 달리 varName+1 등과 같이 메모리 주소를 변경시키는 등의 행위를 할 수가 없기 때문에 같은 종류의 데이터를 넣기 힘들었다. 그래서 그 보완책으로 만들어진 것이 바로 배열이다. 배열은 다음과 같은 형식을 가지고 있다.


int arr[5];


 대괄호는 해당 배열을 몇만큼의 길이로 만들 것인지 묻는 것이다. 여기서 대괄호를 사용하는 이유는 간단히 말하면 사용할 수 있는것이 대괄호뿐이었기 때문이다. 이미 괄호[각주:9]와 중괄호[각주:10]는 사용중이었기 때문이다.

 배열 연산자( [ ] )는 가장 높은 연산 순서를 가지기 때문에 굳이 연산순서를 생각하지 않아도 된다.


 IDE로 Visual Studio를 사용하는 개발자는 해당사항에 없지만 배열 대괄호 사이에 가변인자를 넣을 수 있다.


int arrSize = 5;
int arr[arrSize];

이런 식으로. C99에서 표준화 되었다. 그러나 이런 식으로 가변배열을 사용하는 것은 퍼포먼스 측면에서 부정적이다. 스택프레임, 간단히 메모리 관리 측면에서 최적화를 잘 해주지 못하기 때문이다.

 C에서 배열은 제로베이스, 즉 인덱스가 0부터 시작한다. 위 코드에서 길이 5인 배열을 선언했기 때문에 색인(인덱스)이 1부터 5까지 아니라 0부터 4까지의 색인을 가진다.


int arr1[5] = { 0, };        // 1번, Length : 5

int arr2[5] = { 0,0,0,0,0 }; // 2번, Length : 5

int arr3[] = { 0,0,0,0,0 };  // 3번, Length : 5

 배열의 초기화는 위 코드처럼 세가지 방법이 있다. 1번, 2번처럼 길이를 정한 후에 1번처럼 뒤를 [ { 0, } ]를 입력하거나, 2번처럼 각 색인자리에 하나씩 넣어주는 방법이 있다. 3번처럼 색인의 길이는 명시적으로 선언하지 않지만 초기화를 2번과 같이 해주는 방법도 있다.

 3번은 주의해야할 것이 초기화를 해준 개수만큼만 배열 길이가 책정이 되는 것이다.


int arr3[] = ( 0,0,0,0 };


 이렇게 작성하면 [ arr3 ]의 배열 길이는 4가 된다. 명시적으로 길이를 선언해주지 않으면 맨 마지막 뒤에 ' , '도 작성하면 안된다.

 배열을 선언한 뒤에는 중괄호로 묶어서 대입할 수 없다.


int arr[5];
arr = { 0,0,0,0,0 }; //error


이유는 저렇게 하면 중괄호가 복합문으로 작용하게되고, 복합문은 값을 반환하지 않기 때문이다.


int arr[5] = {3, };


 이렇게 초기화할 수도 있는데, 이렇게 출력하면 arr[0]에만 3이 들어가고 나머지 [ 1부터 4 색인 ]까지는 0이 들어가게 된다. 명시적으로 선언되지 않은 부분은 0으로 초기화된다. 만일 3으로 모든 색인을 초기화하고싶다면 반복문을 사용하자.


참고

>> 배열은 Built-in Data type이 아니라 User-defined Data type이다. 빌트인에 존재하지 않는 3 Byte 데이터크기를 만들 수 있는 것도 이 이유 때문이다.

     >> 배열 초기화 { 0, }에서 ' , '는 작성하지 않아도 상관없다. 이전의 규칙이 현재에 와서 사라졌지만 관례처럼 따라붙는것일 뿐이다.



5. 문자열

 C에서 문자열은 char 타입 데이터의 집합으로 나타난다. 참고로 요즘 프로그램 언어는 String으로 문자열 데이터타입을 따로 제공한다.

 C가 탄생한 시대에는 메모리 크기가 크지 않았기 때문[각주:11]에 현시대의 String 데이터타입처럼 데이터를 저장할 수 없었다. 따라서 메모리 사용량을 최대한으로 줄이기 위해 문자열을 저장한 후, 맨 마지막에 0을 넣음으로써 문자열의 끝을 판단했다.


char data1[6] = {'h','e','l','l','o'};  // 1번
char data2[] = "hello";                 // 2번
char data3[6] = "hello";                // 3번

 이런 식으로 값을 저장할 수 있다. 그러나 1번과 2,3번은 차이가 있는데 1번의 경우엔 [ data1 ]에 일일이 대입을 진행하는 것이지만, 2번이나 3번같은 식은 "hello"라는 문자열이 프로그램 실행시에 데이터세그먼트[각주:12]에 저장되고 그 주소가 data에 저장된다. 따라서 1번의 경우에는 스택영역에서 6바이트를 사용하지만, 2번이나 3번같은 경우에는 데이터 영역에서 6바이트, 스택영역에서 6바이트로 총 12바이트를 사용한다. 그렇다고 1번처럼 코드를 작성하라는 말은 아니다. 램이 64KB이던 시절이면 몰라도 지금같이 기본 8GB 장착으로 컴퓨터가 돌아가는 환경에서 1번처럼 작성하면 오히려 사소한 곳에 시간을 사용함으로써 생산성을 떨어뜨릴 수 있다.

 또, 1번이나 3번의 경우에는 2번과 달리 길이가 정해져있기 때문에 주의해야 할 것이 있다. 배열의 길이를 [ 문자열 길이 + 1 ]로 지정해야 한다. C언어의 문자열은 데이터 형식 특성상 맨 마지막에 0을 넣는데, 배열의 길이때문에 0을 못넣게 되면 뒤에 이상한 문자가 출력되기도 한다. 따라서 문자열을 배열에 저장할 때는 길이 계산을 정확히 해야한다.


참고

>> printf("%s", var ); 로 하면 문자열이 출력되는데 이 때, var에 0이 존재하지 않으면 해당 위치 이후의 데이터를 지속적으로 읽어오기 때문에 이상한 글자가 포함되는 것이다.

>> 매개변수에서 char data[] 이렇게 해도 컴파일러에서는 포인터[각주:13]로 변환된다. 또 대괄호 안의 숫자는 무시[각주:14]된다.



Etc.

 전역, 지역, 매개변수 등 모든 영역에서 데이터 타입을 작성하지 않고 변수, 함수를 작성할 수 있다. 여기서 데이터 타입을 작성하지 않으면 기본 값인 [ int ]로 지정된다.


NULL은 특별히 구분해줘야한다. 다 같은 0이지만 그 쓰임새가 다르다.

>> ASCII의 0번 : NULL 문자(즉 문자 자체가 없다)

>> 포인터의 NULL : 해당 포인터에 들어있는 주소가 0번[각주:15]이다.

>> cpp의 포인터 null : 해당 포인터에 들어 있는 주소가 0번이다. C와 달리 소문자 null로 구성한다.



  1. C언어에서는 초기에는 없다가 나중에 필요에 의해 새롭게 헤더 파일이 만들어진 자료형으로, 참과 거짓 둘만 존재하는 자료형이다. C언어의 헤더에서는 true는 1, false는 0으로 선언되어있다.헤더 명은 stdbool.h이다 [본문으로]
  2. 위 표 연산자 부분에서 A 에 해당하는 값 [본문으로]
  3. 이 때 n은 위 표 연산자 부분에서 B에 해당한다 [본문으로]
  4. 0x01 << 30 [본문으로]
  5. 약 10억 7천만 [본문으로]
  6. 약 -21억 4천만 [본문으로]
  7. 함수, 복합문 등 [본문으로]
  8. 즉, 프로그램을 컴파일하는 단위에서 이미 값이 정해져있어야 하므로, 상수값만 넣을 수 있다. [본문으로]
  9. ' ( ' , ' ) ' [본문으로]
  10. ' { ', ' } ' [본문으로]
  11. 1970년대 메모리는 주로 64kb 이런 식으로 존재했다고 한다. [본문으로]
  12. 프로그램 실행시에 프로그램이 적재된는 곳을 메모리인데, 적재된 구역을 세그먼트라고 한다. 세그먼트는 세부 갈래로 나뉘는데, hello와 같은 문자열은 데이터세그먼트라고 불리는 곳에 저장된다. 자세한 사항은 조금더 심도있게 배워야 알 수 있으나 관심있는 사람은 찾아보자 [본문으로]
  13. char *data [본문으로]
  14. 단일 배열에서만 가능하고 이중배열 이상에서는 맨 마지막만 없앨 수 있다. [본문으로]
  15. OS상에서 0번을 가진 포인터는 무시시켜버리는 등으로 오류를 을으키지 않기 때문이다. [본문으로]
728x90
반응형

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

[TIPS 19TH] 06 : 2018.07.13. (목)  (2) 2018.07.13
[TIPS 19TH] 05 : 2018.07.09. (월)  (2) 2018.07.10
[TIPS 19TH] 03 : 2018.07.02. (월)  (0) 2018.07.03
[TIPS 19TH] 02 : 2018.06.28. (목)  (4) 2018.06.29
[TIPS 19TH] 01 : 2018.06.25. (월)  (2) 2018.06.26
Comments