C언어

C언어의 기초 - 변수와 상수

머리큰개발자 2021. 2. 1. 19:41

*주의 - 개인적으로 학습한 내용이므로 틀린 부분을 알려주시면 진심으로 감사드리겠습니다.

가. 변수(Variables)와 상수(Constants)

C는 기계를 직접 다룰 수 있는 어셈블리어(Assembly language)에 가장 가까운 언어이다.
UNIX 체제에서 사용하기 위해 개발된 언어이며 절차지향적이므로
코드의 순서에 굉장히 유념해서 봐야한다.
또한 C는 한 번에 모든 코드를 목적 프로그램을 바꿔주는 컴파일 언어이기 때문에
한 번 빌드에 성공하면 빠르게 실행이 가능한 장점이 있어
알고리즘 시험을 볼 때도 C로 보곤 한다.

기계어에 가장 가까운 언어이기 때문에 하드웨어에 직접 접근도 가능한데,
우선 기초부터 쌓기 위해 C언어가 어떤 방식으로 작동하는지부터 차근차근 학습하기로 했다.
이 글은 이진수와 8진수, 16진수에 대한 이해와 음수의 표현을 위한 2의 보수,
실수형 데이터를 표현하는 부동소수점을 이용한 방식 등을 알고 있다고 생각하고 작성한다.

개인적으로 가장 빠르게 학습하는 방법은 일단 부딪혀 보면서
무언갈 끝까지 만들어 보는 경험이라고 생각한다.
그렇기 때문에 차례차례 모든 정보를 모아가면서 학습하기보다,
기초부터 알아가되 필요한 것을 그때그때 조금 더 파고 들어가는 방식으로 학습하려고 한다.

상수와 변수를 공부하고 어떤 방식으로 컴퓨터가 인식하고 있는지를 위해
우선 기본적인 서식 개념으로 코드를 짜 둔다.

#if 1
#include <stdio.h>
void main(void){
	printf("Hello, world!");
}
#endif

코드를 한 번이라도 다뤄본 사람은 모두 다 한다는 hello world...
standard input output 파일에 있는 함수 printf 를 사용하기 위해
import (include) 해주고 함수를 가져와 사용한다.

문장은 ""로 감싸서 내보낼 수 있고, 모든 명령어의 끝에는 ; 를 붙여
끝임을 컴퓨터가 인식하게 해주어야 한다.
void 는 Specifier 로 나중에 다루기로 하고,
모든 파일에는 하나의 main block 이 필요하고 {} 블럭으로 main 을 구성한다.

또한, 한 파일에서 여러가지의 main 을 테스트하기 위해
#if 1 .. #endif 를 붙여 #if 1 일 때는 컴퓨터가 인식하도록,
#if 0 일 때는 컴퓨터가 인식하지 못하도록하여 여러개의 main 함수를 써도 문제가 없도록 한다.
안 쓸 때는 반드시 0으로 바꿔 에러가 뜨지 않도록 주의한다.(2개 이상 main 이 발견될 시 오류)

문장을 표현할 때는 여러가지 특수 기능의 문자를 사용할 수 있는데, \로 시작하며 출력에 영향을 준다.

#if 1
#include <stdio.h>
void main(void){
	printf("aaaa \n bbbb \r cc \t dddd \b eeee \a ffff \0 gggg");
}

위 코드를 실행해보면
aaaa
ccbb dddeeeeffff
가 나올 것이다. 이를 통해 생각해보면,

\n \r \t \b \a \0
다음 줄 처음으로 tab (공백) 앞으로 한 글자 알림음 출력 종료

\a 는 되는 컴퓨터도 있고 아닌 컴퓨터도 있다는데,
내 2020그램 + 2019 Visual Studio 는 소리가 잘났다.
눈 여겨볼 것은 출력되는 위치가 겹치도록 바뀔 경우(\r \b)
원래 있던 글자가 사라지면서 입력한 뒤의 글자가 나온다는 것이다.
추가로 \0 은 후술하겠지만 문자열의 종료를 의미하므로
뒤의 gggg를 출력하지 않고 종료하게 된다.

이 printf 함수를 이용하여 여러가지 변수들을 출력할 수 있다.
이 기능을 포맷 지시자를 이용해 수행하는데, 뒤에 들어오는 변수가
정수인지, 실수인지, 글자인지, 문장인지 등을 알려주고
그에 해당하는 값을 출력하게 해주는 고마운 녀석이다.

변수를 확인하기 위해 가장 많이 사용하는 포맷 지시어는 다음과 같다.

%d %f %x or %X or %#x %o or %#o %c %s
정수 실수 16진수 8진수 문자형 문자열

자잘하게 응용할 수 있는 포맷 지시어 형식들이 있지만 뒤에서 사용하면서 보도록 한다.

#if 1
#include <stdio.h>
void main(void){
	pritnf("%d\n", 100); //100
    printf("%d\n", 200-100); //100
    printf("%s\n", "Hi Im handsome"); //Hi Im handsome
    printf("%c\n", 65); //A
    printf("%c\n", 'A'); //A
    printf("%f\n", 3.15); //3.150000
    printf("%x %X %#x\n", 90, 90,90); //5a 5A 0x5a
    printf("%o %#o\n", 100,100); // 144 0144
}
#endif

//는 컴퓨터가 인식하지 못하게 하고 싶은 말을 적는 주석이다.
// 뒤의 모든 문장이 해당되며 한 줄만 주석처리를 한다.
만약 여러 줄을 주석처리하고 싶다면 줄마다 //를 입력하던가
혹은 주석 시작에 /* 주석 끝에 */ 를 입력하면 안에 있는 모든 문장이 주석처리가 된다.

%d 를 사용할 경우 정수형으로 출력되게 되며, 연산식이 들어가있어도 그 결과가 출력됨을 볼 수 있다.
%s 의 경우 "" 안에 적혀있는 문자열이 그대로 출력되는 것을 볼 수 있다.
%c 의 경우 문자형(character)으로 65에 해당하는 글자인 A가 출력되는 것을 볼 수 있다. 이것은 후에 ASCII 코드를 다룰 때 다시 본다.
%f 의 경우 실수형(float)를 출력하며 소수점 6자리까지 기본적으로 출력하고, 이것이 유효범위이다.
%x의 경우 16진수로 바꿔서 출력해준다. 90의 경우 16진수로 5a가 되는데 x로 쓰면 a, X로 쓰면 A, #을 붙이면 앞에 16진수를 뜻하는 0x 를 붙여서 출력해준다.
%o 의 경우도 마찬가지이지만 8진수로 바꿔서 출력해준다.

하지만 상수만으로는 계산하고 나서 저장해둘 곳이 없다.

그래서 필요한 것이 "변수"이다.

변수는 사용하기 전에, 컴퓨터가 변수를 저장할 위치를
미리 마련할 수 있도록 "선언"이라는 과정이 필요하다.
변수의 선언은 Specifier + Identifier = Initializer 의 형태를 사용하며,
Initializer는 나중에 해줘도 된다.

Specifier 는 그게 어떤 유형인지, 어떤 방식인지 등을 특정해주는 역할을 하고,
Identifier는 선언한 변수의 이름을 뜻하고,
Initializer는 변수를 선언할 때 어떤 값을 갖게 하고 싶은지 넣어주는 역할을 한다.

이는 뒤에서 대입하는 것과 사뭇 다를바 없어 보이지만,
대입과 다른 경우가 몇 가지 있기 때문에 구분해준다.

Specifier 은 여러가지가 있지만 크게 세 가지로 나뉘어지지만
우선 데이터의 유형(Type)을 선언하는 것부터 본다.
숫자를 저장하는 방식에 따라 정수형과 실수형으로 나눈다.

정수형 char (1B) short (2B) int, long int (4B) long long (8B)
실수형     float (4B) double, long double (8B)

괄호는 차지하는 크기를 의미하며 1B(Byte) = 1 bit 를 의미하고,
1bit 는 0과 1 중 하나를 의미한다. 즉, 1B=1111 1111(2) 같은 방식으로 2진수 8자리를 의미한다.
short 는 16자리, int 는 32자리로 생각하면 된다.
여기서 long int 나 double 운영체제가 32bit 냐 64bit 냐에 따라 다르지만,
대부분의 컴퓨터에서 각각 4B와 8B를 가진다고 생각하면 된다.

또한 정수형의 경우(실수형은 해당 없음) 음수를 이진수로 표현하느냐 안하느냐에 따라
부호별로도 Type을 나눌 수 있다.
이 때 음수를 가질 수 있는 수가 기본적으로 설정(default)되었기 때문에
선언할 때 따로 말을 붙여주지 않아도 되지만,
양수만 다루고 싶을 때는 unsigned 라는 말을 붙여 선언해야한다.

양수, 음수 char short int, long long long
양수만 unsigned char unsigned short unsigned int, unsigned long unsigned long long

이렇게 나누는 이유는 같은 이진수여도 선언한 타입에 따라서 컴퓨터가 해석하는 값이 달라지기 때문이다.
이 부분은 이진수와 음수의 표현(2의 보수)을 공부해야한다.
여기서는 안다고 가정하고 (지금 알고 있는 것 같아서) 계속 기록해보도록 한다.

양수만 저장하느냐, 음수도 저장하느냐에 따라
해당 타입의 변수가 표현할 수 있는 수의 영역도 달라지게 된다.

비트의 경우의 수와 데이터 타입의 표현 가능한 수가 같다고 생각하면 된다.

예를 들어 Char 형 데이터일 경우 1B = 8bit의 공간을 가지는데,
8bit는 2^8 개의 경우의 수를 가지므로 256개의 숫자를 표현할 수 있으므로
signed의 경우 -128~127 (1000 0000(2) 를 -128로 약속한다.),
unsigned char의 경우 0~255까지의 숫자를 표현할 수 있다.

마찬가지로 signed int 의 경우 4B = 32bit 이고 2^32 의 경우의 수를 가지므로
약 43억개의 수를 표현 할 수 있다. 그래서 -2,147,483,648 ~ 2,147,483,647 까지 표현이 가능하며
unsigned 의 경우 0~4,294,967,295 까지의 숫자가 표현 가능하다.

이렇게 표현 가능한 수의 범위가 정해져있기 때문에 연산을 하다가
이 범위를 넘어갈 경우 내가 예상하지 못한 전혀 다른 숫자가 나올 수 있기 때문에
C언어를 사용할 때는 항상 주의해야한다.

가령 30억 + 20억을 해야하는데 int 로 선언했을 경우
오히려 7억과 같은 어이없는 결과를 얻을 수 있으므로
long long a 로 선언하여 미리 예상하고 사고를 방지해야한다.

그리고 인식표를 붙여 상수값을 구분할 수 있도록 한다.
물론 붙이지 않아도 specifier 로 선언했다면 문제는 없다. 다만 헷갈릴 수 있다.

int, char, short 는 별도로 타입을 지정하는 인식표(Suffix)를 붙이지 않는다.

Type int, char, short, double unsigned int signed long unsigned long long long unsigned long long float long double
Suffix x u, U l, L ul,UL ll, LL ull, ULL f, F l, L

변수를 선언하면 각 유형별로 메모리 상에 이진수로 저장되며,
인텔 포멧을 쓰는 컴퓨터에서는 메모리 한 칸에 1B를 Little Endian 방식으로 저장한다.

출력과 선언을 알았기 때문에, 이제 선언한 변수에 숫자를 직접 입력받을 수 있도록 해본다.
입력을 받기 위해선 마찬가지로 <stdio.h> 에 들어있는 scanf 함수를 사용한다.

#if 1
#include <stdio.h>

void main(void){
	int i,j,k;
    scanf("%d", &i); //콘솔에 4 입력
    printf("%d\n", i); // 4 출력됨
    scanf("%d%d%d", &i, &j, &k);// 1,2,3 입력
    printf("i=%d, j=%d, k=%d", i, j, k); //i=1, j=2, k=3 출력
}

입력받을 때 가장 중요한 것은 변수의 주소에 직접 접근하여 저장한다는 것이다.
변수의 주소는 변수 이름 앞에 &을 붙여서 표현한다.
즉 scanf("%d", &i); 에서 &i 는 i의 주소값을 전달해 준 것이고
%d는 정수형 숫자를 입력받겠다는 뜻이다.
또한 여러개를 한 번에 입력받고 한 번에 출력하는 것도 가능하며
이때도 포맷 지시어를 사용하여 입력, 출력한다.

scanf 함수는 입력받을 때 공백이나 엔터키를 기준으로 입력받으므로 한 번에 입력시에 123 으로 쓰면 안되고 1 2 3 혹은
1
2
3
처럼 입력해야 한다.
또한 char 형 데이터를 입력받을 때, char형은 앞에 공백이나 엔터도 char형으로 인식하기 때문에
scanf(" %c", &변수이름); 처럼 입력받을 때 %c 앞에 한 칸 띄워줘서
space bar 나 엔터를 하나 거르고 받아야 이상한 값이 저장되지 않는다.

가장 자주쓰는 포맷 지시어는 다음과 같다.

포맷 지시어 d,D u,U e,E f lf s c
데이터 10진 정수 양수 10진 정수 지수로 표현하는 실수 실수 실수 문자열 문자
데이터 타입 int, long unsigned int, unsigned long float float double char char

여기서 printf 의 포맷지시어와 다른 것이 하나 있는데,
printf의 경우 double type 의 변수를 출력할 때 printf("%f")를 사용했던 것에 비해,
scanf시에는 %lf 를 써야 double형 데이터를 입력받을 수 있다는 것이다.
그냥 %f로 입력받을시에 유효숫자가 6자리에 불과한 float형을 입력받는다는 것에 유의하자.

scanf 말고도 공백을 포함해 입력받을 수 있는 gets() 함수
(엔터키까지 입력받으며 엔터를 0x0 null 값으로 바꿔서 저장한다)와 한 글자만
엔터로 입력받는 getchar() 함수, 키를 누르자마자 입력받는 getch() 함수도 있다.
마찬가지로 한 글자만 출력하는 putchar() 함수와 putch() 함수도 있다.


나. 연산자


함수를 이용하기 위해 가장 기초적인 연산자들을 먼저 짚고 간다.

일반적인 수학과는 다른 연산자들을 사용하며,
컴퓨터라는 특수한 환경이 결과에 반영되기 때문에
항상 어떤 수를 입력하고 어떤 수를 출력하고 싶은지 염두에 두어야한다.

기호 + - * / % =
역할 덧셈 뺄셈 곱셈 나머지 대입
  addition subtraction multiplication division remainder assignment

수학의 연산자와는 다르게 정수의 나눗셈은 두 가지의 방식으로 따로 결과값을 얻는다.
예를 들어 7 / 3 의 결과를 알고 싶으면 7 / 3 (몫)과 7%3 (나머지)를 따로 계산해봐야한다.
7/3 의 결과는 2, 7%3의 결과는 1이 된다.(7 = 3 * 2 + 1)

특히 수학과 가장 다른 점은 '=' 이다.
수학에서의 등식은 좌변과 우변의 값이 같다는 뜻의 등호이지만,
컴퓨터 언어에서의 =은 대입의 기능만을 한다.
즉 a=3; 일 경우, a가 3을 가지고 있다는 뜻이 아닌 a에 3을 넣으라는 의미가 된다.

이 연산자들의 계산 순서는 수학과 동일하며,
() 를 쳐서 먼저 계산하고 싶은 요소를 계산하는 것도 동일하다.
즉 +- 보다 */%가 먼저 수행되며 가장 마지막에 =이 수행된다.

또한 부호의 기능을 하는 +- 와, 컴퓨터 상에서 몇 바이트(B, Byte)를 차지하는지 알 수 있는 sizeof(a) 함수,
컴퓨터의 메모리에 저장되어 있는 물리적인 주소를 알 수 있는 &a 연산자는
사칙연산보다 우선적으로 처리되며,
숫자 1개만 있으면 수행이 가능한 연산자들이므로 단항 연산자라고 부른다.

단항 연산자가 여러 개일 경우, 예를 들어 -sizeof(a) 등의 경우 변수에 가장 가까운 항부터 차례대로 처리된다. 즉 sizeof 의 결과가 먼저 나오고 그 값에 - 부호를 붙이는 식으로 수행된다.

단항 연산자 중 증가와 감소 연산자도 있다. 변수에 붙여 사용할 수 있는데,
1을 더해주는 a++, ++a 와 1을 빼주는 a--, --a가 있다.
단항 연산자는 변수의 뒤에 있으면 후치, 앞에 있으면 전치라고 부르는데 증가,
감소 연산자의 경우 후치는 그 줄의 계산이 모두 끝나고 1을 증가시켜주고,
전치의 경우 계산을 하기 전에 1을 증가시킨 후에 계산을 진행한다.

예를 들어서, int a=3, b=0; 일 경우 b= 3+ a++ 로 계산하면 b= 3+3 = 6 이 되고 , a는 1 증가하여 4가 된다.
하지만 b= 3+ ++a 의 경우, a는 먼저 4가 되고 b= 3+4 = 7 가 된다.

또한 대입자 =의 활용으로 복합 대입 연산자가 있다.
a = a+3 의 식은 수학에서는 말도 안되는 등식이다.
하지만 컴퓨터에서는 단순히 대입으로 인식하기 때문에,
기존에 a가 갖고 있던 값에 3을 더한 값을 a에다가 덮어 씌운다.
즉 a=1일 경우, a+3 의 결과인 4가 a값에 대입되어 연산 결과 a=4로 바뀌게 된다.

이 복합 대입 연산자는 사칙연산 모두에 적용될 수 있다.
즉 a+=3 일 경우 a=a+3; 과 동일한 뜻이고, a*=3; 이면 a=a*3; a/=3; 이면 a=a/3; a-=b+1; 의 경우 a= a- (b+1) 처럼 간단하게 만들어 식을 만들 수 있다.

연산자의 우선순위는 다음과 같다.
단항 연산자 > 이항 연산자 > 삼항 연산자 순으로 우선순위가 높으므로,
단항부터 계산을 수행하는 것을 눈여겨 봐야한다.
자세한 사용법은 앞으로 계속 나올테니 우선 이런 것이 있구나만 확인하고,
사칙연산과 단항 연산자만 확인하고 넘어간다.

1 () [] -> . :: Grouping, scope, array/member access
2 ! ~ - + * & sizeof  type cast ++x --x (most) unary operations, sizeof and type casts
3 * / % Multiplication, division, modulo
4 + - Addition and subtraction
5 << >> Bitwise shift left and right
6 < <= > >= Comparisons: less-than, ...
7 == != Comparisons: equal and not equal
8 & Bitwise AND
9 ^ Bitwise exclusive OR
10 | Bitwise inclusive (normal) OR
11 && Logical AND
12 || Logical OR
13 ?: Conditional expression (ternary operator)
14 = += -= *= /= %= &= |= ^= <<= >>= Assignment operators
15 , Comma operator

(출처- 위키백과 연산의 우선순위)