1. 주석
// 주석
// 설명 역할, 코드로 인식되지 않는다. 존재하지만 존재하지 않는 것처럼 여기는 마치 깍두기 같은 존재.
주석에 대한 자세한 설명은 이전에 작성한 글 (https://risehyun.tistory.com/37)의 5번 문항을 참고하자.
2. 변수
변수(variable)란 데이터를 일시적으로 저장할 목적으로 사용하는 메모리 공간이다.
간단히 말하자면 구체화 하지 않았거나 앞으로 변경될 가능성이 있는 수를 변수라고 할 수 있다.
변수는 데이터 값을 일시적으로 저장하는 역할을 하는 일종의 데이터 임시 저장용 상자로,
이 상자는 컴퓨터 내부의 메모리 안에 만들어 진다.
- 변수가 필요한 이유
크게 아래의 두 가지로 정리할 수 있다.
1. 사용자에게서 받는 데이터를 저장할 수 있다.
2. 프로그램 코드에 직접 값을 넣는 것 보다 변수를 사용하는 것이 프로그램을 더 유연하게 만든다. (유지보수가 쉬워진다)
- 변수의 종류
다음과 같이 특징에 따라 4가지로 나눌 수 있다.
// 변수
// 1. 전역변수 -> 함수 밖에 선언된 변수
int global = 0;
int data = 0;
// 2. 지역변수 (함수 안에 선언된 변수)
// 함수 내부의 스코프가 중첩되면서 지역 변수의 지역변수 생성 가능.
// 변수명 규칙 : 지역변수끼리는 변수명이 겹쳐도 스코프 밖에 가까운 변수 부터 우선 순위 부여함.
// 따라서 겹치는 변수명도 선언 가능. 하지만 완전히 같은 영역 안에서는 중복 되는 이름 줄 수 X
// 또한 전부 변수와 지역변수가 동시에 존재하는 경우 호출시 전역 변수가 인식 된다.
//3. 정적변수
//4. 외부변수
- 변수에 대한 사실
- 변수는 이름으로 식별한다.
각각의 변수는 자신만의 이름을 가지고 있으므로 이름을 이용해 변수들을 구별한다.
-변수는 그릇(자료를 담을 공간)이고 상수는 그릇에 담을 음료수(이미 상표가 정해진 물건)이다.
예를 들어 종이컵(=그릇, 변수)에 어떤 음료가 담기냐에 따라 커피 한잔이 될 수 있고 주스 한잔이 될 수도 있다.
- 변수의 개수는 적을 수록 좋다.
변수가 많아지면 관리가 힘들고 프로그램을 복잡하게 만든다.
- 변수는 메모리에서 구현되며, 메모리는 주소를 가진다.
메모리 사용량이 적은 프로그램이 효율적인 프로그램이다.
앞서 살펴본 많은 변수가 프로그램을 복잡하게 만드는 데는 이러한 이유도 있다.
- 변수 선언
- 변수를 사용하려면 먼저 선언을 해야 한다.
변수 선언이란 컴파일러에게 지금부터 어떤 변수를 할 건지,
어떤 타입의 데이터가 그 변수에 저장되는지 컴파일러에게 미리 알려주는 것이다.
int a;
int a, b, c; // 하나의 라인에서 여러 개의 변수를 선언
- 변수 이름 짓기
앞서 변수는 이름으로 식별한다고 배웠다. 따라서 반드시 변수에는 이름이 있어야 한다.
변수의 이름과 함수의 이름은 모두 식별자의 일종이다.
식별자(identifer)란 변수 이름, 함수 이름 등에 사용되어 다른 것들과 식별할 수 있게 해주는 것이다.
변수의 이름은 프로그래머가 마음대로 지을 수 있지만 몇 가지 식별자를 만드는 규칙이 있어 이것을 따른다.
식별자를 만드는 규칙은 다음과 같다.
- 식별자는 영문자와 숫자, 밑줄 문자 _로 이루어진다.
- 식별자의 중간에 공백이 들어가면 안 된다.
- 식별자의 첫 글자는 반드시 영문자 또는 밑줄 기호 _ 이여야 한다. 식별자는 숫자로 시작 할 수 없다.
- 대문자와 소문자는 구별된다. 따라서 변수 include와 Index, INDEX는 모두 서로 다른 변수이다.
- C언어의 키워드(※)와 똑같은 식별자는 허용되지 않는다.
※ 키워드는 C언어에서 고유한 의미를 가지고 있는 특별한 단어이다.
키워드는 컴파일러가 지정한 예약어(reserved words)라고도 한다.
이러한 키워드는 사용자가 다시 정의하거나 사용하는 것이 금지 되어 있다.
- 변수가 선언되면 변수의 값은 아직 정의되지 않은 상태가 된다.
변수를 선언과 동시에 값을 넣는 방법은 변수 이름 뒤에 = 를 붙이고 초기값을 적는 것이다.
이것을 변수의 초기화(initialization)이라고 한다.
변수를 선언 동시에 초기화하는 것은, 먼저 선언 한 후에 값을 대입하는 것과 동일하게 작동한다.
동일 타입의 변수는 같은 줄에서 선언과 동시에 변수를 초기화 할 수 있다.
int width = 100, height = 200;
다음과 같은 초기화는 문법적인 오류는 아니지만 width는 초기화되지 않고 height만 초기화 되므로 피해야 한다.
※ 변수를 초기화 하지 않으면 변수 안에 어떤 값이 들어갈까?
변수가 어디에 선언되느냐에 따라 다르지만, 일반적으로 초기화 되지 않은 변수엔
아무 의미가 없는 쓰레기 값이 들어가게 된다.
따라서 변수를 제대로 초기화 하지 않으면 연산 과정에서 의도하지 않은 값이 들어가는 등 문제가 발생할 수 있으므로,
변수는 가능한 선언과 동시에 초기화까지 진행해주는 것이 좋다.
- 변수값 출력
printf()와 같은 라이브러리 함수를 사용하려면 함수를 호출하면 된다.
이를 이용해 변수값을 출력하려면 다음과 같은 문장을 사용하면 된다.
printf("두수의 합 : %d", sum);
여기서 "%d"는 %d를 출력하라는 의미가 아니고 출력 형식을 지정한다.
%d는 변수의 값을 10진수 형태로 출력하라는 것을 의미한다.
여기서 변수 sum의 값이 %d의 위치에서 정수로 출력 된다.
"%d"를 형식 지정자라고 한다. 기본적인 형식 지정자는 다음과 같은 것들이 있다.
형식 지정자 |
의미 |
실행 결과 |
%d |
10진 정수로 출력 |
10 |
%f |
실수로 출력 |
3.14 |
%c |
문자로 출력 |
a |
%s |
문자열로 출력 |
Hello |
형식 지정자와 변수들은 1개 이상일 수도 있고 중간에 문자열이 있는 등 섞어서 사용할 수 있다.
이 경우 형식 지정자의 자리에 각 변수들이 대응된다.
+) 변수의 반대되는 개념으로 상수라는 것이 존재한다.
상수는 구체적인 값이 확정되어 앞으로 변할 가능성이 없는 수를 의미한다.
프로그램에서 데이터를 저정하는 공간은 두 가지로 나눌 수 있는데,
변수의 경우 프로그램 실행 중에 저장된 값이 언제든지 변경될 수 있는 공간에 생성된다.
컴퓨터가 계산을 하기 위해서는 모든 데이터가 메모리에 존재해야 하므로 상수도 변수처럼 메모리에 저장된다.
하지만 상수의 경우 변경이 불가하다는 태그가 붙어있다는 차이점이 존재한다.
이렇듯 상수도 메모리에 존재하기 때문에 자료형이 존재한다.
- 변수의 입력
지정한 값을 출력하는 printf()와 반대로 키보드로부터 입력된 데이터를 지정된 형식으로 변환하여 저장하는 scanf() 함수가 있다.
scanf("%d", &x);
scanf()가 호출되면 컴퓨터는 사용자가 숫자 입력을 마칠 때까지 기다리게 된다.
사용자가 정수를 입력하고 엔터키를 누르면 그때 비로소 정수가 변수에 저장되어서 scanf() 함수의 호출이 끝나게 된다.
첫 번째 인수인 "%d"는 형식 지정자로서 정수형의 데이터를 지정된 형식으로 변환하여 변수에 저장하는 라이브러리 함수이다. scanf()에서 형식 지정자는 printf()에서의 형식 지정자와 그 의미가 같다. 두 번째 인수인 &x은 입력을 받을 변수의 주소를 나타낸다.
이때 변수 이름 앞에 &(앰퍼샌드) 기호가 있다는 점을 유의해야 한다.
변수는 메모리에 생성되기 때문에 각각의 변수는 고유한 주소를 가지고 있다.
변수 이름 앞에 &를 붙이면 해당 변수의 주소를 의미하게 된다.
예를 들어 &i라고 하면 i라는 변수의 주소를 의미하게 된다.
이렇듯 scanf() 함수는 단순히 변수의 값을 받아 출력하는 printf()와 달리 변수의 주소를 받는다는 특징이 있다.
3. 자료형
자료형(data type)이란 변수가 저장할 데이터가 정수인지 실수인지, 아니면 또 다른 데이터인지를 지정하는 것이다.
일정 크기(혹은 길이)의 메모리에 저장된 정보(혹은 자료)를 해석하는 방법,
즉 정수, 실수, 문자 등을 구별하기 위한 키워드로서 자료형이라는 하위분류가 존재한다고 할 수 있다.
기본적으로 컴퓨터라는 기계가 처리할 수 있는 정보는 오로지 숫자 뿐이다.
겉으로는 그림이나 글자의 모습을 하고 있어도 본질은 숫자라는 사실을 잊지 말아야 한다.
자료형에는 정수형, 실수형, 문자형 등이 있다.
// 자료형 (크기단위, byte, 형태를 설명해주는 역할 = Data Type)
// 정수형 : char(1), short(2), int(4), long(4), long long(8)
// 실수형 : float(4), double(8)
int main()
{
int i = 0;
// Q. 정수형 자료형이 1바이트이고 양수만 취급하고자 할 때
// 0에서 최대 255까지 들어갈 수 있는 변수 c를 선언해보자.
// 1 바이트로 양수만 표현
// 256가지 -> 0 ~ 255
// A. unsigned 형의 char 변수를 선언한다.
unsigned char c = 0;
c = 0;
c = 255;
// Q. 만약 자료형의 크기에 벗어나는 값을 할당하면 어떻게 될까?
// A. 값을 할당할 공간이 더 이상 없기 때문에 오버플로우가 발생하여 변수에 0이 할당되게 된다.
c = 256;
// Q. 1바이트로 양수, 음수를 모두 표현하려면 어떻게 해야 할까?
// -128 ~ 0 ~ 127
char c1 = 0;
c1 = -1;
c2 = 255;
// 음의 정수 찾기(2의 보수법)
// 대응되는 양수의 부호를 반전 후, 1을 더한다.
return 0;
}
분류 |
자료형 |
크기 |
해석방법 혹은 특징 |
정수형 |
char |
1바이트 (8비트) |
1비트 부호비트와 나머지 전체를 데이터 비트로 해석하는 부호가 있는 정수형 |
short |
2바이트 (16비트) |
int |
4바이트 (32비트) |
long |
4바이트 (32비트) |
long long int |
8바이트 (64비트) |
unsigned 조합 |
- |
부호 비트도 데이터 비트로 해석하는, 부호가 없는 정수형 |
실수형 |
float |
4바이트 |
부호비트, 지수, 가수 등 세 부분으로 구성된 실수형.
지수부와 가수부를 연산한 결과로 값을 표현 |
double |
8바이트 |
long double |
8바이트 이상 |
유도형 |
* (에스터리스크) |
4바이트 (32비트 App) 또는 8바이트 (64비트 App) |
포인터 |
[ ] |
- |
배열, 동일 요소의 집합체 |
구조체 |
서로 다른 요소들의 집합체 |
공용체 |
동일 메모리에 다양한 해석방법을 부여하는 방법 |
함수형 |
- |
포인터와 동일 |
반환 형식과 매개변수 구조를 가진 형식으로 호출 연산의 대상이 될 수 있는 방식 |
- |
void |
|
무치형. 길이 및 해석방법 없는 자료형 |
- short, wchar_t
short형은 부호가 있는 16비트 정수형이다. 정수를 담기 위한 자료형이긴 하지만, 표현 범위가 작기 때문에 자주 사용되지 않는다. wchar_t는 윈도우 환경에서 short형과 같은 16비트 자료형으로 문자 하나를 저장하는 공간이다.
정수를 표현하는 가장 보편적인 형식은 int형이지만 소스코드에서 상수를 표현할 때 int와 short는 별도로 구별하지 않는다. 또한 wchar_t형은 문자 상수는 char형과 달리 상수 앞에 L이 붙는다.
short s = 1234;
wchar_t w = L'A';
- int와 long
int형은 부호가 있는 32비트 정수형이며 가장 많이 사용되는 자료형 중 하나로
일반적인 정수, 특히 상수를 표시할 때 int형을 사용한다/
long도 int형과 동일하게 32비트를 가지는데, 둘의 표현 범위가 같기 때문에 서로 차이가 없는 것처럼 보이지만 분명히 다르다.
int는 운영체제 환경과 상관없이 늘 동일한 4바이트 크기를 유지한다.
즉 64비트 운영체제와 32비트 운영체제 중 어디에서 int 자료형을 사용하더라도 같은 값을 가진다.
반면 long의 경우 32비트 운영체제에선 4바이트 크기를 가지지만 64비트 운영체제에서는 8바이트 크기를 가진다.
따라서 둘의 차이는 운영체제 환경에 따라 크기가 달라지는지 여부에 있다.
- unsigned, signed 수식자
두 키워드는 정수형(short, int, long, long long) 앞에서 선언할 수 있다.
unsigned는 음수가 아닌 값만을 나타낸다는 것을 의미한다.
unsigned를 붙이면 표현 범위에서 음수가 제외 되기 때문에 더 넓은 범위의 양수를 나타낼 수 있다.
printf()로 unsigned 값을 출력하고 싶을 때는 %u를 사용한다.
signed는 반대로 해당 자료형이 음수도 가질 수 있음을 명백히 하는데 사용된다.
따라서 signed int와 일반 int는 서로 같은 의미이며 보통의 경우 signed 수식자가 생략된다.
- 오버플로우
변수가 나타낼 수 있는 범위를 넘는 숫자를 저장하려 하면 그 결과가 정수형이 나타낼 수 있는 범위를 넘어갈 수 있다. 이러한 현상을 오버플로우(overflow)라고 한다.
오버플로우가 발생하면 전체적으로 부정확한 결과가 계산될 수 있다.
그러나 정수형에서 오버플로우가 발생하더라도 컴파일러는 아무런 경고를 하지 않으므로,
오버플로우가 발생하지 않도록 프로그래머 스스로 주의해야 한다.
- 이형자료 간의 연산과 형승격
가능한 서로 자료형이 같은 자료형을 가진 변수끼리 계산하는 것이 바람직하지만,
이형자료 간 연산을 진행하는데 문법적으로 전혀 문제가 없는 경우 이형자료를 가진 변수끼리 연산이 가능한 경우가 있다.
예를 들어, double + int는 double 형식이 된다. 따라서 해당 결과 값은 실수가 되므로 %d 형식으로 출력할 수 없다.
%d로 출력하려하면 실제 결과 값이 아닌 0이 출력된다.
왜 실수를 정수로 출력할 수 없는지 헷갈린다면 아래 5번 항목에서 실수와 정수의 차이를 확인해보자.
간략하게 여기서 이유만 언급하자면 실수와 정수는 비트 상에서의 표현 방식이 다르기 때문이다.
다시 본론으로 돌아가서, 이처럼 서로 자료형 크기가 같은 int와 double뿐 만 아니라 자료형 크기가 다른 변수도 연산 가능하다.
다만 이 케이스에서는 한 가지 유의해야 할 점이 있다.
임시 결과의 자료형은 연산자가 참여한 피연산자 중 정보 표현 범위가 더 넓은 자료형이 된다.
EX. char형과 int형 간의 연산
: char타입은 2비트 정수형 자료형이고 int는 4비트 정수형 자료형이다.
따라서 연산의 결과로 얻는 임시 결과의 자료형은 표현 범위가 더 넓은 int이다.
같은 이치로 float와 double형 간의 연산은 표현 범위가 더 넓은 double형을 결과로 얻게 된다.
이렇게 연산 결과가 피연산자의 자료형보다 표현범위가 더 넓은 형식으로 변경되는 현상을 형승격(type promotion)이라 한다.
4. 비트와 바이트
1 Byte = 8 Bit
1bit => 더 이상 쪼갤 수 없는 메모리의 최소 단위로, 있다(1) 없다(0)의 상태값만 나타낸다.
2^10 = 1024개 묶음 = 1Kb
1024 Kb => 1MB (약 1천배)
1byte(=8bit) 자료형의 경우 최대 256개의 상태를 나타낼 수 있다.(2^8=256)
숫자로는 0~255까지의 수를 표현할 수 있다.
2진수에 이를 대입해보면, 모든 메모리가 0으로 채워진 경우가 있을 수 있다. 이를 정수 0이라고 한다.
전부 0인데 첫 번째 메모리에 1이 들어있는 경우 이를 정수 1이라고 한다.
1 바이트 (부호비트) |
1 바이트 (자료비트) |
1 바이트 (자료비트) |
1 바이트 (자료비트) |
1 바이트 (자료비트) |
1 바이트 (자료비트) |
1 바이트 (자료비트) |
1 바이트 (자료비트) |
양수의 경우 8bit로 이뤄진 메모리라고 할 때, 7개의 메모리인 2^7 = 128개의 값(0~127)을 나타내며,
맨 왼쪽 부호 비트에 0이 할당된다.
음수의 경우에도 동일하게 128개의 값(-1~-128)을 나타내나 부호 비트에 1이 할당된다.
5. 정수와 실수의 차이
메모리가 작을 수록 비트가 작아지므로 그만큼 표현할 수 있는 수의 가짓수가 줄어든다.
실수의 경우, 0~1사이에 무한대에 가까운 많은 숫자가 존재할 수 있다.
따라서 존재할 수 있는 모든 실수를 메모리에 매칭한다는 것은 불가능하다.
한정된 메모리로 이러한 숫자를 나타내는데 한계가 있기 때문이다.
그러므로 실수를 표현할 때는 부동소수점이라는 새로운 체계를 따른다.
부동소수점이란 소수점 이하의 숫자 정보까지 표현하는 것을 말한다.
즉, 정수와 실수는 메모리를 다루는 방식, 즉 표현하는 방식 자체가 다르다.
그렇기 때문에 정수와 실수를 함께 연산할 때도 둘의 표현방식이 다르므로 어느 한쪽의 기준에 맞춰서 계산해야 한다.
// 정수와 실수를 함께 사용하는 예
int a = 4 + 4.0;
이 과정에서 형변환등에 실수가 있다면 연산 결과에 문제가 생길 수 있으므로
둘을 혼재해서 쓰는 경우는 특별한 의도가 있는 상황이 아니라면 가급적 피한다.
실수의 표현에서는 특정 실수에 딱 맞는 비트가 존재하기 보다는 해당 실수를
표현하기 위한 가장 근사한 값으로 비트를 잡아주는 것에 가깝다.
그렇기 때문에 실수를 다룰 때 중요한 것은 실수가 정밀하게 표현되는 타입이 아니므로
연산 전에 예상했던 결과값과 실제 연산 최종값이 다르게 나타날 수 있다.
따라서 특정 조건에 충족을 할때도 있고 아닐 때도 있게 된다.
결론적으로 조건식에 실수의 연산 결과값을 사용하면
이러한 실수의 정밀도 차이로 인한 문제가 발생할 수 있음을 인지해야 한다.
// 정수 표현 방식과 실수 표현 방식은 다르다.
// 실수 표현 방식은 정밀도에 의존한다.
// 따라서 double(8) 자료형이 flaot(4)보다 더 아래의 소수점까지 정확하게 표현이 가능하다.
// 가능한 정수는 정수끼리, 실수는 실수끼리 연산하되, 두 표현방식의 피 연산자가 연산될 경우
// 명시적으로 형을 변환하는 것이 좋다.
// EX) 형변환을 명시적으로 적어서 연산하기
float f = 10.2415f + (float) 20;
// 캐스팅을 하지 않아도 연산은 진행되지만 이 경우 도출된 결과가 의도를 한 것인지 아닌지 알 수 없으므로
// 명시적으로 확실하게 변환을 시켜주자.
위에서 서술했듯이 float는 유효 자릿수가 소수점 이하 6자리까지 이므로 정밀도가 비교적 낮은 편에 속해 오류가 발생할 위험성이 높다. 따라서 대부분 실수 자료는 float형 보다는 double형을 사용하는 것이 좋다.
실수상수를 표기할 때, f를 붙이지 않으면 자동으로 double형으로 처리된다.
5.5 상수
상수의 종류에는 정수 상수와 기호 상수가 있다.
- 정수 상수
12나 100과 같이 숫자로 표기한다.
정수 상수는 기본적으로 int형으로 간주되지만 만약 int의 범위를 넘게 되면 컴파일러가 알아서 long형으로 취급한다.
long형으로도 부족한 경우 unsigned long형으로 변경될 수도 있다.
이처럼 컴파일러는 상수값을 처리할 때 가급적 자료형에서 가장 작은 자료형을 선택하는 특성이 있다.
명시적으로 상수의 자료형을 지정하고 싶을 때는 접미사를 붙여 이를 나타낼 수 있다.
예를 들어 아래와 같은 경우는 접미사 L을 붙여 123이라는 상수를 long으로 나타낸다.
sum = 123L;
123만 있는 경우 int형이 16비트인 시스템에서는 16비트의 123이 된다.
하지만 123L이라고 하면 32비트의 123이 된다.
6. 대입 연산자
= 연산자를 대입 연산자라고 부른다. = 의 좌변에는 항상 변수가 위치하고 우변에는 값이 위치한다.
= 연산자는 우변의 값을 좌변의 변수에 저장한다.
정확히 말하자면, 변수는 메모리 주소의 이름이므로 대입 연산자는 특정 주소에 정보를 담는(복사하는) 역할을 한다.
따라서 대입 연산자가 수행되면 왼쪽 피연산자의 정보에 새로운 정보를 덮어쓰게 되므로, 기존 정보는 유실된다.
대입 연산자의 왼쪽 피연산자를 l-value라고 하고, 오른쪽 피연산자를 r-value라고 한다.
l-value의 l은 Left의 L도 되지만 Locator(위치 지정자)라는 의미도 가지고 있다.
따라서 왼쪽 피연산자는 '정보를 담을 수 있는 변수'라는 뜻이다.
정보를 담을 수 있는 변수가 아니라면 l-value가 될 수 없다.
7. 산술 연산자
// 연산자
// 대입 연산자, =
// 산술 연산자
// +, -, *, /, %(모듈러스, 나머지, 피연산자가 모두 정수일 때만 가능
// 실수의 경우 어느 소수점에서 끊어서 계산할지 모호하기 때문에 연산이 성립되지 않음)
// ++, --
int data = 10 * 10;
//data = data + 20;
data += 20;
// data가 int형이므로 double 타입의 실수를 넣을 수 없음. int로 캐스팅 해주어야함
data = (10.f / 3.f);
// int로 결과값을 캐스팅 해준 경우
data = (int)(10.f / 3.f);
- 나눗셈 연산에서 주의해야 할 점
- 정수를 정수로 나눈 결과는 정수이다. 따라서 int / int 결과값은 소수점 이하가 절사된 채 정수 부분만 가지게 되므로,
%f로 출력할 수 없다. (출력하려 하면 실제 결과값 대신 0이 출력된다. 정수와 실수의 표현 체계가 다르기 때문이다.)
- 나눗셈 연산시 어떤 값이든 절대 0으로 나눌 수 없다. 만약 0으로 나누려고 시도하게 되면 어떤 연산 장치도 0으로 나눌 수 없게 되므로 값을 구할 수 없어진다. 결과적으로 연산이 영원히 끝나지 않게 되므로 프로그램이 죽게 된다.
- 나머지 연산자에서 주의해야 할 점
나머지 연산자 역시 앞서 살펴본 나눗셈 연산처럼 값을 0으로 나눌 수 없다.
또한 나머지 연산자의 피연산자로 실수가 사용될 수 없다.
따라서 '12.3 % 3' 혹은 '12.3F % 3' 같은 연산식은 문법적으로 허용되지 않는다.
나머지 연산자의 우선순위는 곱셈, 나눗셈 연산자와 동일하다.
그러므로 사칙 연산 중에서 덧셈과 뺼셈보다 우선 연산된다.
나머지 연산자는 보통의 연산자 이상의 의미를 가진다.
나머지 연산의 결과는 제수(※)보다 클 수 없기 때문에 나머지 연산은 제수만큼 경우의 수를 갖는다.
이를 활용하면 일정 범위로 경우의 수를 강제할 수 있다.
※ 나눗셈에서의 제수와 피제수
- 제수 : 나눗셈에서 어떤 수를 나누는 수.
EX. 10 / 5 = 2 에서 '5'가 제수가 된다.
- 피제수 : 어떤 수나 식을 다른 수나 식으로 나눌 때 처음의 수나 식.
EX. 6 / 3 = 2에서 '6'이 피제수가 된다.
- 복합 대입 연산자 (+=, -= 등)
복합 대입 연산자란 기존의 단순 대입 연산자에 산술 연산자나 비트 연산자를 조합해 새로운 하나의 연산자로 만드는 것이다.
따라서 연산을 두 번 실행한다.
일종의 누적을 실행하는데, 이 때 누적할 변수는 누적 연산에 앞서 반드시 0으로 초기화를 해야 한다.
그렇지 않으면 쓰레기 값이 들어가 오류가 발생하게 된다.
- 단항 증감 연산자
단항 증감 연산자는 '--'와 "++"로 표시되는 연산자를 뜻한다.
피연산자에 저장된 정보를 1씩 증가 또는 감소시키는 역할을 하는데, 이때 기본 전제로 피연산자가 l-value여야 한다.
즉, 단순 대입 연산자의 왼쪽에 올 수 있는 쓰기 가능한 변수여야 연산이 실행된다.
단항 증감 연산에서 문법적으로 주의해야 할 부분은 전위식 표기(++x)인 경우에는 연산자 우선 순위가 높지만,
후위식 표기(x++)일 때는 우선순위가 사실상 최하위가 된다는 것이다.
따라서 단항 증감 연산자를 후위식으로 표현하면 대입 연산자보다도 나중에 실행되므로
마치 그 단항 연산이 없는 것처럼 생각하는 것이 낫다.
대입까지 모든 구문이 다 끝나야만 비로소 단항 증감 연산이 수행되기 때문이다.
이 우선순위가 바뀜에 따라서 후에 클래스나 구조체의 오퍼레이터를 호출할 때 의도치 않은 문제가 발생할 수 있다.
따라서 습관적으로 전위 연산자를 사용하는 것이 좋다.
< 단항 증감 연산과 복합 대입 연산>
단항 증감 연산은 기존의 복합 대입 연산자와 비슷해보이는 점이 있다.
실제로 x += 1 와 ++x는 의미와 실행 결과가 같다.
그러나 문법적으로는 단항 연산이란 점에서 다르며 연산자의 우선순위가 전혀 다르다.
또한 결과적으로 보면 ++x는 x에 1을 누적하는 연산이지만, 정확히 말하자면 누적이라기 보다는 계수(counting)다.
이 계수 역시 누적에 기반을 두기 때문에, 복합 대입 연산자로 누적을 구현할 때 그러했듯이
계수기 변수도 연산 전 반드시 변수를 0으로 초기화 해야만 쓰레기값으로 인한 오류가 발생하지 않는다.
8. 논리 연산자
// 논리 연산자
// !(역), &&(And), ||(Or)
// 참(True), 거짓(False)
// 참 : 1 (0을 제외한 값, 주로 1로 표기)
// 거짓 : 0
bool truefalse = false; // 디버깅 해보면 false이므로 0이 나옴
bool IsTrue = 100; // bool 자료형에 정수를 넣으면 이것을 true 상태로 받아들여 디버깅시 1이 된다.
// 따라서 bool 자료형은 0,1만 다루는 1byte 자료형이므로 숫자를 표현하기에는 적합하지 않다.
int iTrue = true; // 1이 할당됨
iTrue = !iTrue; // !(역 연산자)로 인해 1(true)의 반대인 0(false)이 할당된다.
// 원래 c에선 true, false를 전처리기에서 따로 처리했어야했는데 c++부터 기본 연산자가 되었다.
// 논리곱으로 연산해보기
// 둘 다 0이 아닌 수이기 때문에 참이므로 iTrue는 1(True)가 된다.
iTrue = 100 && 200;
// 앞의 숫자가 0이므로 거짓이기 때문에 iTrue는 0(false)가 된다.
iTrue = 0 && 200;
// 논리합으로 연산해보기
// ||(OR) 연산은 둘 중 하나만 참이면 참이 되므로 iTrue는 1(True)가 된다.
iTrue = 0 || 300;
// 두 개 모두 0(false)이므로 iTrue는 0(False)가 된다.
iTrue = 0 || 0;
// 주의! 1만 True가 아니라 연산자에서는 0을 제외한 모든 값을 True라고 본다.
// bool 자료형은 값을 0 또는 1로만 구분하기 때문에 숫자 100, 200 등을 넣으면 모두 1로 간주한다.
// 이런 특성을 활용하는 경우가 많다. (조건문의 조건으로 쓴다던지)
// 이 때 값을 유지한 채로 값이 참인지 거짓인지를 즉시 활용할 수 있다.
// (700 등의 값을 넣으면서 0이 아니므로 참으로 판별한다던지)
// (비교) 구문
// if, else
// switch case -> if, else와 결은 다르지만 if else문과 같이 비교 구문이다.
// 삼항 연산자
// :?
- 논리 연산의 특징
1. 논리 연산은 왼쪽에서 오른쪽으로 진행한다.
2. 경우에 따라 나중에 수행될 연산을 수행하지 않고도 전체 결과를 확정할 수 있다.
이처럼 앞에서 연산 결과가 확정되면 뒤의 연산은 생략하여 논리식의 효율을 꾀하는 것을 쇼트서킷(short circuit)이라고 한다.
<쇼트서킷의 특징>
1. 논리 연산식은 반드시 왼쪽에서 오른쪽으로 수행한다.
2. OR 논리식은 조건에 만족하면 이후 연산을 생략한다.
3. AND 논리식은 조건에 부합하지 않으면 이후 연산은 생략한다.
4. 혼합 논리식에서 AND는 한 덩어리로 묶고 OR의 연속으로 살핀다.
9. 비트 연산자
- 비트 연산자는 자료형을 근거로 해석된 정보가 아닌, 일정 길이의 메모리에 담긴 2진수 정보를 비트 단위로 계산하는 연산자를 뜻한다.
만일 8비트 정보라면 여덟 개의 비트 각각에 대해 여덟 번 연산하는 것과 같다.
C에서는 비트 연산자로 &(AND), OR(|), XOR(^), NOT(~), Shift Left (<<), Shift right(>>)가 있다.
이중 NOT(!)만 단항 연산자이고, 나머지는 모두 이항 연산자이다.
비트 단위의 상수를 기술할 땐 보통 16진수를 사용한다.
unsigned char byte = 13;
byte <<= 3; // 2^n 배수
byte >>= 2; // 2^n 나눈 몫
// 초기값이 홀수인 13이지만 비트 이동을 하고 나면 나머지는 소실되고 2의 배수인 몫만 남게 된다.
// 비트 곱(&), 합(|), xor(^), 반전(~)
// 비트 단위로 연산을 진행하며,
// 곱 연산(&)은 두 비트가 모두 1인 경우 1
// 합 연산(|)은 두 비트중 하나라도 1인 경우 1
// XOR 연산(^)은 두 비트가 서로 같으면 0이고 다르면 1
// 반전 연산 (~)은 0은 1로, 1은 0으로 변경
- 마스크 연산
상수 0과 변수의 특정 비트열에 대해 비트 AND 연산으로 일정 구간이 모두 0이 되도록 의도한 연산을 마스크(mask) 연산이라고 한다. 이 연산을 통해 불필요한 정보를 가리고 필요한 정보만 골라낼 수 있으므로 마스크라 표현한 것이다.
만일 비트 AND 연산의 한쪽 피연산자의 모든 비트가 1이면, 다른 한쪽 변수의 값이 무엇이든 그 결과는 변수 값 그대로다. 하지만 상수 0인 경우에는 결과가 무조건 0이 된다.
정보를 잘라내기 위해 시프트 연산을 사용할 수도 있지만, 꼭 필요한 정보만 걸러내거나 특정 스위치를 켜거나 끄기 위한 연산 등을 쉽게 해낼 수 있으므로 마스크 연산이 활용된다.
10. 형변환 연산자
int x = 5;
printf("%f\n", (double)(x/2)); // 결과값이 double이 된다.
형변환 연산자(type cast)는 피연산자의 자료형을 강제로 새로운 자료형으로 변경하는 단항 연산자이다.
이전에 배운 연산자들보다 우선 순위가 높은 연산이기도 하다.
따라서 덧셈 또는 곱셈과 같은 산술 연산자와 함께 사용하면 이 형변환 연산자가 먼저 수행된다.
형변환 연산자는 강제로 자료형을 변경하기 때문에 적절한 변환이 아닌 경우에도 연산이 수행되버린다.
따라서 심각한 논리 오류나 정보의 손실이 발생할 가능성이 있기 때문에 사용시 주의해야 한다.
11. 연산자 응용
앞서 살펴본 기본 연산자들처럼 직접적인 숫자 계산처럼 보이진 않지만 명백히 연산자가 맞는 연산자들이 있다.
이번에는 이러한 연산자들에 대해 알아보려 한다.
- sizeof 연산자
sizeof 연산자는 피연산자의 자료형에 대한 연산이다.
즉, 피연산자가 자료형이라는 것이다.
따라서 sizeof(5)라는 연산은 정수 5를 뜻하는 것이 아닌, sizeof(int)라는 연산을 의미한다.
int형의 바이트 단위 메모리 크기가 4바이트이므로, sizeof(int)의 연산 결과는 4가 된다.
그러므로 sizeof(5)의 연산 결과도 동일하게 4가 된다.
또한 sizeof 연산자는 프로그램을 빌드하고 CPU가 실행하는 런타임 연산자가 아니라 컴파일러가 컴파일타임에 수행하는 연산자이다. 그렇기 때문에 아무리 많이 사용하더라도 프로그램의 수행능력이 떨어지지 않는다.
프로그램을 작성하다 보면 변수의 메모리 크기를 생각하고 거기에 맞춰서 코드를 작성할 일이 많다.
예를 들어 다음과 같은 코드가 있다고 가정하자.
int aList[5];
wchar_t wch;
int는 4바이트 크기를 가지므로 int 5개가 한 묶음인 aList는 20바이트 크기를 가진다.
그러므로 소스코드에서 aList 배열의 크기를 명시할 때 20이라는 숫자를 직접 기입할 수도 있다.
하지만 이는 매우 부적절한 코딩 방식이다.
배열 요소의 개수는 추후에 변경될 수 있는 여지가 있기 때문이다.
그러므로 20이라고 직접 숫자를 기입하기 보다 sizeof(aList)로 처리하는 것이 적절하다.
또한 wchar_t 역시 환경에 따라 2바이트 또는 4바이트가 될 수 있기 때문에, 바로 숫자로 크기 값을 표기하기보다
sizeof(wchar_t)로 쓰는 것이 다양한 환경에 대응할 수 있는 좋은 코드가 된다.
- 관계 연산자
관계 연산자는 내부적으로 상등/부등, 비교 연산자로 분류할 수 있으며,
두 피연산자의 값을 비교해 참 또는 거짓의 결과를 내는 연산자이다.
피연산자로 변수나 상수를 사용하는 것이 일반적이지만 연산식이 오는 경우도 많다.
A == B : 상등 연산으로, A는 B와 같다. (즉, 둘의 뺄셈 결과가 0이다.)
A != B : 부등 연산으로, A는 B와 같지 않다.
A > B : 비교 연산으로, A는 B보다 크다.
A < B : 비교 연산으로, A는 B보다 작다.
A >= B : 비교 연산으로, A는 B보다 크거나 같다. (B 이상이다)
A <= B : 비교 연산으로, A는 B보다 작거나 같다. (B 이하이다)
<C언어에서의 참의 정확한 의미>
참 (true)를 %d로 출력하면 1이 되고, 거짓 (false)를 출력하면 0이 된다.
그렇기 때문에 참 (true)는 1이라고 생각하기 마련이다.
하지만 c언어에서 '참'의 진정한 의미는 "0이 아닌 모든 값" 이다.
따라서 값이 1이든, 400이든 모두 0이 아니므로 bool 연산자로 받아보면 참으로 표시된다.
비교 관계 연산자에서는 두 값의 비교를 위해서 내부적으로 뺄셈을 실행한다.
정확한 값을 알 수 없는 임의의 두 숫자가 서로 같은 숫자인지 여부를 사칙연산으로 알아보려면
가장 직관적이고 명백한 방법이 바로 두 수를 빼서 결과가 0인지를 확인해보는 것이기 때문이다.
따라서 둘의 뺄셈을 진행하여 결과가 0이 아니면 다시 양수인지 음수인지를 확인한다.
이때 A - B 연산의 결과가 양수라면 A는 B보다 크다는 것을 알 수 있다.
이런 식으로 관계 연산은 뺄셈 연산이 응용된다.
만일 문자열처럼 배열 형태의 정보에 대해 관계 연산을 수행하고 싶다면 각 요소 하나하나를 일일이 비교해야 한다.
따라서 대부분 비교를 위한 전용 함수를 사용한다. 함수를 사용할 때도 연산자를 사용할 때와 마찬가지로,
두 대상이 같다면 함수가 0을 반환한다. 이런 함수로는 memcmp()와 strcmp() 함수가 있다.
이전 연산자들과 마찬가지로 관계 연산 또한 정수와 실수의 조합으로 상등 연산을 수행할 경우
전혀 엉뚱한 결과를 얻을 수 있기 때문에 피연산자의 자료형이 매우 중요하다.
12. if / else 구문
// 비교 연산자
// ==, !=, <, >, <=, >=
// 참, 거짓
// 구문
// if, else
data = 0;
// 조건식이 참이므로 스코프 안의 구문이 실행 됨.
if (100 && 200)
{
data = 100;
}
// 위 조건식이 참이고 스코프 안에서 data 변수에 100을 할당하였으므로 참이 된다.
// 하지만 조건식이 거짓이 되게 된다면 아래 조건식 역시 거짓이 된다.
if (data == 100)
{
}
// if, else, else if 문
if (data == 100)
{
// if가 참인 경우 수행
}
else if()
{
// 위의 조건이 거짓일 때 이 조건문 체크 후 참일 시 수행
// 이 조건식이 거짓이 경우 아래의 else문 스코프 안에 있는 내용을 수행하도록 넘어감
}
else
{
// 위의 조건에서 if가 다 거짓인 경우 수행
}
// else if문과 각자 따로 수행되는 여러개의 if문 중 어느 것을 사용할지 잘 생각해봐야한다.
// 예를 들어서 wasd로 캐릭터를 이동시키는 movement 로직을 만든다고 할 때,
// 키보드가 눌리는 순서를 w->a->s->d 순으로 else if문을 이용해 조건 검사를 하면
// w와 a를 동시에 누르는 경우 대각선으로 이동하는 것이 아니라
// 조건 검사를 위해 w 이동을 건너 뛰어버리기 때문에 위쪽 방향으로 이동할 수 없다.
// 이런 경우에는 else if문이 아닌 각각 4개의 if문을 사용하여 조건 검사를 개별적으로 하도록 해야 한다.
13. switch 구문 / 삼항 연산자
switch (10) // 원하는 특정 변수 또는 값을 넣어준다
{
// 위에서 사용한 변수와 케이스 안의 조건 값을 비교해서 해당 케이스와 일치하는 쪽 구문을 실행한다.
case 10:
// 여기서 변수 값과 조건 값이 일치하므로 break문을 만나기 전까지의 로직을 실행한다.
break;
case 20:
break;
// 여기까지 조건을 검사했을 때 조건에 맞는 경우가 없을 때 아래의 디폴트문을 수행하게 된다.
default:
break;
// 만약 케이스문에 break가 없는 경우 그 아래로 지정된 케이스문 로직도 함께 작동되므로
// break문의 사용에 유의해야한다. 이를 활용해 의도적으로 break문을 안 쓸 수도 있게 된다.
}
int iTest = 10;
if (iTest == 10)
{
}
else if (iTest == 20)
{
}
else
{
}
// 3항 연산자
// 조건식 ? 조건식이 참인 경우 실행되는 것 : 조건식이 거짓인 경우 실행되는 것
iTest == 20 ? iTest = 100 : iTest = 200;
// 삼항연산자로 나타낸 다음 식을 if else문으로 동일하게 나타내면 다음과 같다.
if (iTest == 10 || iTest == 20 || iTest == 30)
{
}
else
{
}
- 조건 연산자 (= 삼항 연산자)
조건 연산자는 C 언어에서의 유일한 삼항 연산자이다. 사용방법과 문법구조 면에서 이전에 배운 연산자들과 다르게 피연산자로 조건식을 필요로 한다. 또한 그 조건의 결과로 피연산자인 두 개 항에서 하나를 '선택' 한다.
조건식 ? A : B
조건식이 참이면 A를 선택하지만, 그렇지 않으면(거짓이면) B를 선택한다. 따라서 A와 B가 동시에 선택될 수 없다.
또한 삼항 연산자의 두 번째, 세 번째 피연산자는 변수나 상수 하나(항)이어야 한다. 따라서 선택 대상 피연산자가 연산식이라면 아래의 예시처럼 반드시 괄호로 묶어야 한다.
nInput <= 10 ? (nSelect = 10) : (nSelect = 20);
sizeof()연산이나 주소 연산자(&)와 같은 컴파일 타임 연산을 제외하고, 연산에 참여하는 피연산자는 변수 그 자체가 아니라 그 안에 담긴 값이라는 점을 유의하도록 하자.