04 ~ 10 - 기초 문법

유현수·2024년 1월 15일
post-thumbnail

해당 챕터들은 새롭게 알게된 내용에 한해서만 요약하여 작성했습니다.

04 - 표현식

할당 연산자

대부분의 프로그래밍 언어에서 할당은 구문(statement)이다. 이와 달리 C 언어에선 할당이 +와 같은 연산이다. 할당 v = e의 값은 할당 이후 v의 값이다.

int i;
float f;

i = 72.99f;    /* i의 값은 72이다    */
f = 136;       /* f의 값은 136.0이다 */

05 - 선택문

논리 표현식

많은 프로그래밍 언어에선 i < j와 같은 표현식은 "불리언"이나 "논리"라는 특수한 타입을 띨 것이다. 이러한 타입은 참과 거짓을 유일한 값으로 갖는다. C에서는 이와는 다르게 i < j와 같은 비교식의 결과값은 1 혹은 0인 정수다. 1이면 참을 의미하고, 0은 거짓을 의미한다.

관계 연산자

C에서 사용되는 관계 연산자는 수학에서 사용되는 <, >, ≤, ≥와 기본적으로 동일하지만, 표현식에서 사용되었을 때 결과값으로 0 또는 1이 나온다. 10 < 11의 값은 1이고, 11 < 10의 값은 0이다.

동등 연산자

관계 연산자처럼 동등 연산자도 결과값이 0 또는 1이다.

switch문

계단식 if문보다 switch문이 더 가독성이 좋고 특히 조건이 많을수록 실행속도도 더 빠르다.

대부분의 경우 switch문은 다음과 같은 구조를 갖는다.

switch (expression) {
case constant-expression:
    statements
…
case constant-expression:
    statements
default:
    statements
}
  • 제어식controlling expression : switch라는 단어 다음엔 반드시 괄호로 둘러싸인 정수 표현식이 나와야한다. C에서는 문자를 정수로 취급하므로 switch문에 사용할 수 있다. 하지만 고정소수는 사용할 수 없다.
  • 조건 부호case label : 각 조건은 다음 형식으로 부호가 붙여져야한다.
    • case constant-expression : 상수식은 일반 표현식과 똑같으나, 변수나 함수 호출을 포함할 수 없다. 5는 상수식이고 5 + 10은 상수식이지만 n + 10은 상수식이 아니다(n이 매크로정의된 상수가 아니라면 말이다). 조건부호에서 상수식은 반드시 정수(문자도 포함한다)여야한다.
  • 구문statements : 각 조건부호 다음엔 한 개 이상의 구문이 올 수 있다. 굳이 복합문처럼 중괄호를 써줄 필요가 없다. 각 조건부호의 구문은 주로 break문으로 끝난다.

아쉽게도 다른 프로그래밍 언어처럼 일정 범위를 조건부호로 나타낼 수는 없다. case 다음엔 반드시 한 개의 상수식만 올 수 있지만 여러 개의 조건부호들이 다음 예시처럼 같은 구문을 공유할 수 있다. (역자: 이때 / intentional fallthrough /라는 주석으로 의도적임을 드러내는 것이 좋다)

switch (grade) {
case 4:
    /* intentional fallthrough */
case 3:
    /* intentional fallthrough */
case 2:
    /* intentional fallthrough */
case 1:
    printf("좋지 않아요!");
    break;
case 0:
    printf("좀 더 노력하세요!");
    break;
default:
    printf("잘못된 학점입니다");
    break;
}

switch문은 반드시 default 조건을 필수로 하지 않는다. 만약 default가 존재하지 않는데 제어식의 값이 그 어떤 조건부호와도 만족하지 않는다면 프로그램은 단순히 switch문 다음 구문으로 넘어간다.

06 - 루프

do문

do문은 사실상 while문과 동일하다고 할 수 있다. 다만 제어식을 루프 본문 실행 이후에 판별할 뿐이다. do문의 일반형은 다음과 같다.

do {
    // statement
} while ( expression );

아래 예시를 살펴보자.

i = 10;
do {
    printf("%d\n", i);
    --i;
} while (i > 0);

// 10
// 9 
// ...
// 1

for문

C89와 C99 for문의 차이점

C89에서는 for문의 첫번째 표현식에서 변수를 선언할 수 없다.

int main(void)
{
    int i;
    for (i = 0; i < 10; i++) {
        printf("%d\n", i);
    }
}

C99에서는 for문의 첫번째 표현식에서 변수를 선언할 수 있다.

int main(void)
{
    for (int i = 0; i < 10; i++) {
        printf("%d\n", i);
    }
}

쉼표연산자

for문 내에 두 개 (이상)의 표현식을 초기화하거나 루프가 실행될 때마다 여러 변수를 증감시켜야 할 때가 있다. 이는 for문의 첫번째와 세번째 표현식 내에서 쉼표연산자를 통해 구현할 수 있다.

쉼표 연산자를 사용해

sum = 0;
for (i = 1; i <= N; ++i) {
    sum += i;
}

위와 같이 작성하지 않고 아래처럼 작성할 수 있다.

for (sum = 0, i = 1; i <= N; ++i) {
    sum += i;
}

루프에서 벗어나기

루프를 종료시키는 구문으로는 break, continue, goto가 있다. break문과 continue문만으로도 많은 것이 가능하기에 goto문은 잘 사용되지 않는 구문이다.

goto문

goto문은 해당ㅇ 구문에 표식(label)이 있는 한 함수 내의 그 어느 구문으로든 도약할 수 있다. (C99의 경우 goto뭉네 약간의 범위를 부여한다. 가변 크기를 갖는 배열의 선언을 우회할 때 사용할 수 없다.)

표식이란 구문의 시작에 표시한 식별자이다.

identifier : statement

구문은 두 개 이상의 식별자를 가질 수 있다. goto문 자체는 다음과 같은 구조를 갖는다.

goto identifier;

구문 goto L;을 실행한다면 프로그램의 제어를 표식 L이 있는 구문으로 옮긴다. 여기서 표식 L은 반드시 goto문이 있는 함수 내에 있어야 한다. 또한 goto문에서 표식은 반드시 goto문 이후에 있어야 한다.

루프를 도중에 강제로 종료할 때 break문이 아니라 goto문을 이용할 수도 있다.

for (d = 2; d < n; ++d) {
    if (n % d == 0) {
        goto done;
    }
}

done:
    if (d < n) {
        printf("%d is divisible by %d\n", n, d);
    } else {
        printf("%d is a prime\n", n);
    }

goto문은 옛날 프로그래밍 언어들의 잔재이기 때문에 자주 사용하지 않는다. break, continue, return, exit이 대부분의 경우 goto를 대체할 수 있기 때문이다.

물론 goto문이 유용하게 사용될 때도 있다. 반복문이 여러겹이거나 반복문 내에 switch문이 있는 상황에서 goto문을 사용해 전체 반복문을 손쉽게 벗어날 수 있다.

while   ()
    switch   ()   {goto loop_done;    /* 이 상황에선 break가 먹히지 않는다 */}
}
loop_done:

null문

구문은 무형(null)의 형태를 띌 수 있다. 이는 세미콜론을 제외하고 아무 기호도 없는 구문을 의미한다.

예를 들어 아래 반복문을 살펴보자.

for (d = 2; d < n; d++) {
  if (n % d == 0) {
    break;
  }
}

여기서 n & d ==0 조건을 루프의 제어식으로 옮긴다면 사실상 루프의 본문은 비게 된다.

for (d = 2; d < n && n % d != 0; d++) {
  /* 비어있는 루프의 본문 */ ;
}

07 - 기본형

정수형

유부호와 무부호 정수형

정수형은 유부호(signed)와 무부호(unsigned) 두 가지로 나뉜다.

유부호(signed)와 무부호(unsigned) 정수형
유부호(signed) 정수형의 맨 좌측 비트는 부호 비트(sign bit)이다. 부호 비트는 정수의 값이 0이거나 양수이면 0이고 음수이면 1이 된다. 따라서 유부호 비트는 무부호 비트에 비해 1비트 작은 크기만큼 값을 표현할 수 있다.

무부호 정수형은 unsigned를 붙여 선언한다. 부무호 숫자는 시스템 프로그래밍, 저수준 및 기계와 밀접한 어플리케이션 개발에 적합하다.

정수형의 크기

int형은 CPU에 따라 그 크기가 달라진다. 만약 int에 저장할 수 없을 만큼 큰 숫자를 할당해야 한다면 long 정수형을 사용한다. 반대로 메모리가 덜 필요한 숫자를 사용한다면 short 정수형을 사용할 수 있다.

정수형의 종류

long, short, signed, unsigned 등의 규격자들을 혼용해서 표현할 수 있는 서로 다른 정수형은 다음 총 여섯 가지이다.

  • short int
  • unsigned short int
  • int
  • unsigned int
  • long int
  • long unsigned int

규격자들을 선언할 때 딱히 순서가 있는 것은 아니다. unsigned short int이든 short unsigned int이든 둘 다 같은 의미이다.

int를 생략해줄 수도 있다. unsigned short intunsigned short로, long intlong으로 축약할 수 있다.

자신의 환경에서 정수형의 최대/최소값 확인하기

표준 라이브러리 중 하나인 <limits.h> 헤더 파일을 이용해 각 정수값의 최대/최소값을 확인할 수 있다.

#include <stdio.h>
#include <limits.h>

int main(void)
{
    printf("%d\n", INT_MIN);
    printf("%d\n", INT_MAX);
}

정수형 상수

C에서는 정수형 상수를 세 가지 방법으로 작성할 수 있다.

  • 10진수(decimal) 상수는 0에서 9까지의 숫자를 사용하지만, 0으로 시작하지 않는다.
    • 15, 255, 32767
  • 8진수(octal) 상수는 0에서 7까지의 숫자를 사용하며, 반드시 0으로 시작해야한다.
    • 017, 0377, 077777
  • 16진수(hexadecimal) 상수는 0에서 9까지의 숫자와 a에서 f까지의 알파벳을 사용하며 반드시 0x로 시작해야한다. 알파벳은 대소문자 구분을 하지 않아도 된다.
    • 0xf, 0xff, 0x7fff

8진수와 16진수는 그저 숫자를 바라보는 방식이 다를 뿐이지, 실제 숫자가 저장되는 방식에는 아무런 영향을 끼치지 않는다. 정수, 8진수, 16진수, 어떤 방식으로 표현하든 언제나 2진수로 저장된다.

컴파일러가 해당 상수를 강제로 긴 정수형으로 취급하게 만들어주고 싶다면 상수 뒤에 L혹은 l을 적어주면 된다.
15L 0377L 0x7fffL

상수를 무부호로 만들어주고 싶다면 U 혹은 u를 뒤에 적어주면 된다.
15U 0377U 0x7fffU

LU는 혼용할 수 있어 상수가 긴 정수형이면서 무부호로 만들어 줄 수 있다.
0xffffffffUL (LU 간에는 순서가 없을 뿐더러 대소문자 구분도 없다.)

정수 초과

정수 간에 산술 연산 시 해당 정수형의 범위를 벗어날 때가 있다.
만약 유부호(signed) 정수간 연산에서 초과(overflow)가 발생하면 프로그램은 불능행동을 한다.
무부호(unsigned) 정수간 연산에서 초과가 발생 시에는 행동이 정의되어 있다. n이 결과값을 저장하는데 사용된 비트의 수일 때, 모듈로 2^n에 따른 정답을 얻을 것이다. 예를 들어 무부호 16비트 숫자 65,535에 1을 더한다면 결과값은 반드시 0이다.

숫자 입출력

정수형 입출력 시 %d 변환 규격자는 int 한테만 작동한다. 따라서 unsigned, short, long 정수형을 읽고 쓰려면 다른 변환 규격자를 사용해야 한다.

무부호(unsigned)를 읽거나 쓸 땐 d 대신 u, o, x를 사용해야 한다.

  • u : 무부호 10진수
  • o : 무부호 8진수
  • x : 무부호 16진수
unsigned int u;

scanf("%u", &u);    /* reads  u in base 10 */
printf("%u", u);    /* writes u in base 10 */
scanf("%o", &u);    /* reads  u in base 8 */
printf("%o", u);    /* writes u in base 8 */
scanf("%x", &u);    /* reads  u in base 16 */
printf("%x", u);    /* writes u in base 16 */

short 정수형을 읽거나 쓸 땐 h 문자를 d, o, u, x 앞에 추가해준다.

ed!short s;

scanf("%hd", &s);
printf("%hd", s);

long 정수형을 읽거나 쓸 땐 l 문자를 d, o, u, x 앞에 추가해준다.

long l;

scanf("%ld", &l);
printf("%ld", l);

long long 정수형을 읽거나 쓸 땐 ll을 문자를 d, o, u, x 앞에 추가해준다.

long long l;

scanf("%lld", &ll);
printf("%lld", ll);

소수형

C에는 세 가지 소수형(floating type)이 있다.

  • float : 단일 정밀도 소수점
  • double : 2배 정밀도 소수점
  • long double : 확장 정밀도 소수점

float은 정확성이 그렇게 필요하지 않은 경우(소수점 이하 한 자리 등)에 적합하다. double은 대부분의 프로그램에 적합한 수준의 정밀함을 제공한다. long double은 상당한 정밀함을 제공하지만 거의 사용하지 않는다.

C 표준은 float, double, long double 자료형이 어느 수준의 정밀성을 제공하는지 정의하지 않는다. 이는 각 컴퓨터마다 소수점을 저장하는 방식이 다르기 때문이다. 최근 컴퓨터들의 경우 전기전자기술자협회 IEEE 표준 754(IEC 60559라고도 알려짐)을 따르기 때문에 우리는 이 표준을 예시로 공부할 것이다.

아래 표는 IEEE 표준을 기준으로 소수점을 구현했을 때 소수점이 갖는 특성들을 보여준다. (표에 나온 최소 양수값은 정규화normalization된 값이다. 비정규 값은 더 작을 수도 있다.) 표에는 long double 자료형의 길이는 기계마다 다르므로 반영되지 않았다. 통상적으로 80비트나 128비트이긴 하다.

자료형최소 양수값최대값정밀도
float1.17549 * 10^-383.40282 * 10^38소수점 이하 6자리
double2.22507 * 10^-3081.79769 * 10^308소수점 이하 15자리

IEEE 표준을 준수하지 않는 컴퓨터에서는 표의 내용이 적용되지 않는다. 사실 몇몇 기기에선 floatdouble과 같은 특성을 갖거나 doublelong double과 같은 특성을 가질 수도 있다. 이러한 특성이 정의된 매크로들은 <float.h> 헤더에서 확인할 수 있다.

C99에선 소수점이 두 가지 부분으로 나뉜다. float, doublelong double은 실소수형(real floating type)으로 분류하고, float _Complex, double _Complex, long double _Complex는 복소수형(complex type)으로 분류한다. 후자에 속한 세 가지 자료형들은 C99에 새로 추가된 자료형들이다.

소수형 상수

소수형 상수는 다양한 방법으로 표현할 수 있다. 다음은 57.0을 표현하는 다양한 방법이다:
57.0 57. 57.0e0 57E0 5.7e1 5.7e+1 .57e2 570.e-1

소수점 상수는 반드시 소수점 혹은 지수를 포함해야한다. 지수는 숫자에 곱해질 10의 제곱수를 의미한다. 지수를 사용하기 위해선 반드시 그 앞에 E 혹은 e를 넣어주어야한다. E 혹은 e 이후에 부가적으로 + 혹은 - 부호를 추가해줄 수 있다.

기본적으로 소수점 상수는 2배 정밀도 숫자로 저장된다. 다시 말해 C 컴파일러는 상수 57.0을 double 변수와 같은 서식으로 메모리에 저장해준다. 필요시에 double 값들은 float으로 변환되기에 크게 문제될 부분은 없다.

가끔 컴파일러에게 소수점 상수를 강제로 float 또는 long double 서식에 저장하게 만들어줄 때가 있다. 만약 단일 정밀도로 사용하고 싶다면 F 혹은 f를 상수 말미에 추가해주어야 한다. (57.0F) 상수를 long double 서식으로 저장하고 싶다면 말미에 L 혹은 l을 추가해주어야 한다. (57.0L)

C99에서는 소수점 상수를 16진수로 작성하게 해줄 수 있다. 16진수 소수는 16진수 정수 상수와 마찬가지로 0x 혹은 0X로 시작한다. 허나 거의 사용하지 않는다.

소수 입력 및 출력

전에서도 다루었듯이, %e, %f%g가 단일 정밀도 숫자를 읽고 쓸 때 사용하는 변환 규격자다. doublelong double 자료형의 경우 이와 다른 변환이 필요하다.

double형의 값을 읽을 때 e, f, g 앞에 l을 추가해준다.

double d;

scanf("%lf", &d);
  • 주의: printf가 아닌 scanf의 서식 문자열에서만 l을 작성해주어야 한다. printf에서 e, f, g 변환은 floatdouble 둘 다 혼용해서 사용할 수 있게 해준다. (C99에서는 printf에서 %le, %lf, %lg를 사용할 수 있게 해준다. 물론 l은 실질적으로 아무런 영향을 끼치지는 않는다)

  • long double형의 값을 읽거나 쓸 땐 e, f, g 앞에 L을 넣어주어야 한다.

    long double ld;
    scanf("%Lf", &ld);
    printf("%Lf", ld);

문자형

컴퓨터마다 갖고 있는 문자 집합(charset)이 다르기 때문에 char형의 값은 컴퓨터마다 다르다.
char형 변수에는 문자 하나를 할당할 수 있다.

char ch;

ch = 'a';    /* 소문자 a */
ch = 'A';    /* 대문자 A */
ch = '0';    /* 영      */
ch = ' ';    /* 띄어쓰기 */

문자형 상수는 큰따옴표가 아닌 작은따옴표로 표시한다.

문자 연산

C에서 문자는 값이 작은 정수와 같다. ASCII를 예로 들면 문자 코드는 0000000에서 11111111까지 있는데 이걸 정수로 읽으면 그냥 0에서 127까지다. C에서 문자와 정수 간에는 상당히 밀접한 관계가 있어 문자형 상수들은 사실 char형이 아니라 int형으로 취급한다.

계산할 때 문자가 있으면 C는 그냥 문자를 정수 취급한다.

유부호와 무부호 문자

C에서는 문자를 정수처럼 다룰 수 있게 해주기 때문에 char형 또한 정수형처럼 유부호 / 무부호 여부를 결정해줄 수 있다. 유부호 문자형의 값은 일반적으로 -128에서 127까지이고, 무부호 문자형의 값은 0에서 255까지이다.

대부분의 경우 솔직히 char이 유부호인지 무부호인지 궁금하지 않다. 하지만 문자형 변수에 정수를 저장할 때는 고려를 해줘야한다. 이 때문에 C는 char 앞에 signed 혹은 unsigned를 작성해줄 수 있게 해준다

signed char sch;
unsigned char uch;

C89에선 이 문자형과 정수형간 긴밀한 관계를 이용해 정수형과 문자형을 대정수형(integral type)이라고 부른다. 열거형 또한 대정수형이다. C99에선 "대정수형"이라는 용어를 사용하지 않고, "정수형"의 의미를 확장해서 문자형과 열거형 또한 정수형이라고 부른다. C99의 _Bool형 또한 무부호 정수형으로 취급한다.

산술형

정수형과 소수형을 합쳐서 보통 산술형(arithmetic type)이라고 부른다.

확장 비트열

문자형 상수는 보통 작은 따옴표로 둘러 싸인 하나의 문자를 의미한다. 여기서 개행문자와 같은 예외사항이 등장한다. 개행문자와 같은 예외적인 문자들은 출력을 하지 않는 비가시적인 문자이기 때문에 키보드로 입력할 수 없다. 그렇기 때문에 프로그램이 이러한 문자를 문자 집합에서 처리해줘야한다. C에서는 이를 확장 비트열(escape sequence)로 불리는 특수한 표기법으로 해결한다.

확장 비트열은 확장 문자(character escape)와 확장 숫자(numeric escape) 두 가지로 나뉜다. 아래 표에서는 확장 문자 전체를 나열한다.

이름확장 비트열
알림(비프음)\a
백스페이스\b
폼 피드\f
개행\n
개행 복귀\r
수평 탭\t
수직 탭\v
역슬래시\\
물음표\?
작은따옴표\'
큰따옴표\"

형변환

C는 암시적 변환(implicit conversion)과 명시적 변환(explicit conversion)을 모두 제공한다.

암시적 변환은 다음 상황에서 발생한다.

  • 산술 혹은 논리표현식에서 피연산자들이 같은 형을 갖지 않을 때 (C에서는 기본산술변환usual arithmetic conversion이라는 것을 수행한다)
  • 할당에서 우항에 있는 표현식의 형이 좌항의 변수와 다를 때
  • 함수 호출의 입력변수의 형이 이에 대응하는 입력값의 형과 다를 때
  • return문의 표현식의 형이 함수의 반환형과 다를 때 우선 위의 두 가지를 본 단원에서 설명하고, 나머지 두 개는 9단원에서 설명하겠다.

기본산술변환

기본산술변환은 산술, 관계, 동등 연산자들과 같은 대부분의 이항연산자들의 피연산자에 적용된다.

기본산술변환의 기본적인 전략은 피연산자를 두 가지 값을 모두 안전하게 포함할 수 있는 가장 "좁은" 형으로 변환하는 것이다. (단순히 말하자면 한 형이 다른 형보다 더 적은 바이트를 요구한다면 더 좁다고 할 수 있다) 피연산자들의 형을 갖게 만드는 방법은 더 좁은 형을 갖는 피연산자를 나머지 피연산자에 맞추어 형변환을 해주는 것이다(이를 승진promotion이라고 부른다). 가장 흔하게 일어나는 승진 중 하나는 정수형 승진integral promotion이다. 대표적으로 문자형 혹은 short 정수를 int형(혹은 unsigned int)로의 변환이 있다.

기본산술변환을 할 때의 규칙은 다음과 같이 두 가지 경우로 나눌 수 있다.

  • 소수형 피연산자가 존재할 때
    • 다음 다이어그램과 같이 더 좁은 형을 승진시킨다.
    • float > double > long double
    • 만약 가장 형이 큰 피연산자가 long double형이라면, 나머지 피연산자들을 전부 long double로 변환해주어야한다. double의 경우 나머지 피연산자들을 double으로 통일해주어야한다. float일 때도 마찬가지이다. 이 규칙은 정수형과 소수형을 섞을 때도 적용한다: 만약 한 피연산자가 long int를 갖고 나머지 피연산자가 double형을 갖는다면 long int 피연산자를 double로 변환시킨다.
  • 소수형 피연산자가 존재하지 않을 때
    • 우선 양 피연산자들 정수형 승진해준다(둘 다 문자형 혹은 short 정수가 아니도록 보장해준다). 그 다음 아래의 다이어그램과 같이 더 좁은 형을 갖는 피연산자를 승진시킨다.
    • int > unsigned int > long int > unsigned long int
    • 여기서 만약 long intunsigned int가 같은 크기(32비트)를 가질 때 발생한다. 이 경우 longunsigned int 둘 다 unsigned long int로 변환한다.

아래는 기본산술연산의 실제 예시이다.

char c;
short int s;
int i;
unsigned int u;
long int l;
unsigned long int ul;
float f;
double d;
long double ld;

i = i + c;      /* c는 정수로 변환됨                 */
i = i + s;      /* s는 정수로 변환됨                 */
u = u + i;      /* i는 무부호 정수로 변환됨          */
l = l + u;      /* u는 장정수로 변환됨               */
ul = ul + l;    /* l은 무부호 장정수로 변환됨         */
f = f + ul;     /* ul은 소수형으로 변환됨            */
d = d + f;      /* f는 2배소수형으로 변환됨          */
ld = ld + d;    /* d는 장2배소수형으로 변환됨        */

할당 중 형변환

할당은 통상적인 산술변환과는 다르게 작동한다. C에서는 매우 간단한 규칙을 따르는데, 할당의 우항에 있는 표현식은 좌항의 변수의 형으로 변환이 된다. 만약 변수의 형이 표현식의 형만큼 최소한 충분히 "넓다면" 아무런 문제 없이 작동한다. 예시를 보도록 하자.

char c;
int i;
float f;
double d;

i = c;    /* c는 정수형으로 변환됨    */
f = i;    /* i는 소수형으로 변환됨    */
d = f;    /* f는 2배소수형으로 변환됨 */

하지만 반대의 경우 문제가 발생한다. 소수를 정수 변수에 할당하는 경우 소수의 소수점 이하를 버리게 된다.

int i;

i = 842.97;     /* i의 값은 842다 */
i = -842.97;    /* i의 값은 -842다 */

더욱이 더 좁은 형에 범위를 벗어나는 값을 할당하면 의미 없는 결과를 내보낼 거나, 더 최악의 결과를 내보낼 것이다.

c = 10000;      /*** 잘못됨!! ***/
i = 1.0e20;     /*** 잘못됨!! ***/
f = 1.0e100;    /*** 잘못됨!! ***/

이런 식으로 더 넓은 값을 "좁은" 변수에 할당하는 행위는 컴파일러나 lint와 같은 외부 도구에서 경고할 것이다.

2단원에서도 그랬듯이, float형 변수에 고정 소수점 상수를 할당할 때 말미에 f를 넣어주는 것이 좋다:

f = 3.14159f;

접미사 f를 생략한 3.14159의 경우 double형을 띠고 있어 경고 문구를 출력할 수도 있다.

형치환

C의 암시적 형변환이 편리하기는 해도, 형변환에 대한 직접적인 제어가 때때로 필요하다. 이러한 이유 때문에 C에서는 형치환cast이라는 것을 제공한다. 형치환식은 다음과 같은 구조를 갖는다.

( type-name ) expression

type-name은 해당 표현식이 어떤 형으로 변환될 것인지를 명시해준다.

다음 예제에서는 형치환식을 이용해 float 값의 소수부를 계산하는 방법을 보여준다.

float f;
float frac_part;

frac_part = f - (int) f;

형정의

형정의(type definition)은 다음과 같이 사용한다.

typedef int bool_t;

bool_ttypedef를 이용해 정의해주게되면 컴파일러는 자신이 이해할 수 있는 형들 목록에 bool_t을 추가하게 된다. bool_t은 이제 변수 선언, 변환식 등지에 내장된 형들처럼 사용할 수 있다. bool_t로 직접 변수를 선언해보겠다:

bool_t flag;    /* int flag;와 같은 구문 */

컴파일러는 bool_tint의 동의어로 취급을 한다. 즉, flag는 그저 평범한 int에 불과하다.

형정의의 장점

예를 들어 cash_incash_out이라는 변수를 사용해 달러 값을 저장한다고 가정하자. 그렇다면 dollars_t를 다음과 같이 정의할 수 있다.

typedef float dollars_t;

dollars_t cash_in, cash_out;

위와 같은 구문은 아래보다 더 나은 구문이다.

float cash_in, cash_out;

형정의는 프로그램의 유지보수성을 높여주기도 한다. 나중에 사실 dollars_t가 본래 float이 아니라 double을 사용해야한다면 그저 형정의 부분만 바꿔주면 된다:

typedef double dollars_t;

Dollars 변수의 선언부는 바꿔줄 필요가 없다는 의미다. 만약 형정의를 해주지 않았다면 달러값을 저장하는 모든 float 변수들을 전부 double로 선언부를 바꿔주는, 전혀 쉽지 않은 작업을 진행해야한다.

형정의와 호환성

형정의는 호환성이 중요한 프로그램을 작성할 때 매우 중요하게 작용한다. 프로그램을 한 컴퓨터에서 다른 컴퓨터로 옮길 때 발생하는 가장 큰 문제 중 하나는 컴퓨터별로 자료의 범위가 다르다는 것이다. 만약 int형 변수 i를 다음과 같이 정의했다고 하자.

i = 100000;

위와 같은 정의는 정수가 32비트인 컴퓨터에선 아무 문제 없이 돌아가지만, 정수가 16비트인 컴퓨터에서는 문제가 생긴다.

C99에선 <stdint.h> 헤더에서 특정 비트를 갖는 정수형들을 typedef를 이용해 정의해놓았다. 예를 들어 int32_t는 정확하게 32비트인 유부호정수형이다. 이러한 형들을 사용하면 프로그램의 호환성을 훨씬 높여줄 것이다.

sizeof 연산자

sizeof 연산자는 프로그램이 특정 형의 값을 저장하는데 필요한 메모리가 얼마인지 알려준다.

sizeof ( 형이름 )

sizeof 값을 출력할 때는 여러가지를 고려해야한다. sizeof 표현식의 형은 구현정의된 형인 size_t형이다. C89에서는 해당 표현식을 우리가 알고 있는 값으로 우선 치환을 한 다음에 출력하는 것이 좋다. size_t는 무부호 정수형이기 때문에 sizeof를 가장 안전하게 형치환하는 방법은 C89의 제일 큰 무부호형인 unsigned long으로 치환한 다음 %lu 규격자를 이용해 출력하는 것이다.

printf("int형의 크기: %lu\n", (unsigned long) sizeof(int));

C99에서 size_t형은 unsigned long보다 크기가 더 커질 수 있다.하지만 C99의 printf 함수는 size_t를 형변환 없이 직접 출력하는 것이 가능하다. 바로 규격자 앞에 z 문자를 붙여준 다음, 그 뒤에 아무 정수형 규격자(보통 u를 사용)를 적는 것이다.

printf("int형의 크기: %zu\n", sizeof(int));    /* C99에서만 가능 */

08 - 배열

지금까지 다뤘던 변수는 스칼라scalar 변수다. 즉, 한 가지 자료만 저장할 수 있다. C에서는 집합aggregate 변수를 제공하는데, 이름대로 여러 개의 값을 저장할 수 있다. C에는 배열과 구조체 두 가지 집합이 있다. 본 단원에서는 배열을 선언하고 사용하는 방법을 배운다.

1차원 배열

1차원 배열은 다음과 같이 사용한다.

int a[10];    /* 길이가 10인 int형 배열 */

for (int i = 0; i < 10; i++) {
    a[i] = i;
}

int b[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};    /* 배열 초기화 */

int c[10] = {1, 2, 3, 4, 5, 6};
/* c의 초기값은 {1, 2, 3, 4, 5, 6, 0, 0, 0, 0}이다 */

int d[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};    /* 초기화 시 배열 길이 생략 가능 */


/*
* C99
* 지정 초기자designated initializer 사용 가능
* 지정하지 않은 나머지 원소는 0으로 초기화
*/
int e[15] = {[2] = 29, [9] = 7, [14] = 48};

다차원 배열

다차원 배열은 다음과 같이 사용한다.

/* 선언 */
int m[5][9];

/* 초기화 */
int n[5][9] = {{1, 1, 1, 1, 1, 0, 1, 1, 1},
               {0, 1, 0, 1, 0, 1, 0, 1, 0},
               {0, 1, 0, 1, 1, 0, 0, 1, 0},
               {1, 1, 0, 1, 0, 0, 0, 1, 0},
               {1, 1, 0, 1, 0, 0, 1, 1, 1}};

/*
* C99
* 다차원 배열도 지정 초기자 사용 가능
*/
double ident[2][2] = {[0][0] = 1.0, [1][1] = 1.0};

상수 배열

차원과 무관하게 배열을 const를 선언에 추가해주어 "상수"로 만들어 줄 수 있다:

const char hex_chars[] =
  {'0'. '1'. '2'. '3'. '4'. '5'. '6'. '7'. '8'. '9'.
   'A'. 'B'. 'C'. 'D'. 'E'. 'F'};

const로 선언된 배열은 프로그램에 의해 수정될 수 없다. 컴파일러는 배열의 원소를 수정하려는 모든 시도를 감지할 것이다.

const는 단순히 배열에만 국한된 것이 아니라, 모든 변수에 가능한다. 이에 대한 내용은 나중에 다룰 것이다.

가변크기 배열 (C99)

/* Reverses a series of numbers using a variable-length array - C99 only */

#include <stdio.h>

int main(void) {
    int i;
    int reverse_count;

    printf("How many numbers do you want to reverse?: ");
    scanf("%d", &reverse_count);

    int numbers[reverse_count];   /* C99 only - length of array depends on n */

    printf("Enter %d numbers: ", reverse_count);
    for (i = 0; i < reverse_count; ++i) {
        scanf("%d", &numbers[i]);
    }

    printf("In reverse order:");
    for (i = reverse_count - 1; i >= 0; --i) {
        printf(" %d", numbers[i]);
    }
    printf("\n");

    return 0;
}

위 코드의 배열 numbers가 바로 가변크기 배열variable-length array(VLA)을 사용한 예제다. VLA의 크기는 프로그램 컴파일 도중이 아닌, 실행 도중에 정해진다. VLA의 최대 장점은 프로그래머가 배열을 선언할 때 대충 아무 값이나 때려넣지 않고, 프로그램 자체에서 정확하게 필요한 만큼을 할당할 수 있게 해준다는 점이다. 만약 프로그래머가 배열의 크기를 정해준다면, 아마 배열의 크기가 과도하게 크거나(메모리 낭비) 작을 것이다(프로그램이 오류를 낼 것).

09 - 함수

함수 정의 및 호출

C언어에서 함수는 다음과 같이 정의한다.

return-type function-name ( parameters )
{
    declarations
    statements
}

함수의 반환형과 관련해 다음과 같은 규칙들이 있다.

  • 함수는 배열을 제외한 모든 형을 반환할 수 있다.
  • C89에서 반환형을 생략하면 int를 반환한다고 간주한다.
  • C99에서 함수의 반환형을 생략할 수 없다.

함수 선언

함수 정의 부분을 함수 호출 이후에 배치한다고 가정해보자.

#include <stdio.h>

int main(void)
{
    int x;
    int y;
    
    printf("더할 숫자 두 개를 입력해주세요: ");
    scanf("%d%d", &x, &y);
    printf("sum result: %d", get_sum(x, y));

    return 0;
}

int get_sum(int left, int right)
{
    return left + right;
}

컴파일러는 get_sum을 호출할 때 함수에 대한 정보를 모른다. 따라서 get_sum의 기본 반환형이 int라고 가정한다. 이런 경우 컴파일러가 함수에 대해 암시적 선언을 했다고 말한다. 컴파일러는 함수에 대한 정보를 모르므로 기본매개변수승진을 수행하고 최대한 그게 맞기를 빌 뿐이다.

모든 함수를 호출하기 이전에 정의할 수도 있지만 다른 해결 방법이 있다. 함수를 호출하기 이전에 선언하는 것이다. 함수 선언은 함수 정의의 첫번째 줄에 세미콜론을 붙인 것처럼 생겼다.

/* 함수 선언 */
return-type function-name ( parameters ) ;

이제 함수 선언을 위의 예제를 수정하면 에러 없이 함수가 호출되는 것을 확인할 수 있다.

#include <stdio.h>

int get_sum(int left, int right);   /* 선언 */

int main(void)
{
    int x;
    int y;
    
    printf("더할 숫자 두 개를 입력해주세요: ");
    scanf("%d%d", &x, &y);
    printf("sum result: %d", get_sum(x, y));

    return 0;
}

int get_sum(int left, int right)    /* 정의 */
{
    return left + right;
}

입력변수

  • 매개변수: 함수 정의 시 작성
  • 입력변수: 함수 호출 시 작성

C에서 입력변수는 값에 의해 전달pass by value된다. 함수가 호출되면 각 입력변수가 평가되고, 그 결과값이 이에 대응하는 매개변수에 할당된다. 매개변수는 입력변수의 값의 복사본을 갖고 있기 때문에 매개변수에 가해진 모든 수정사항은 입력변수에 어떠한 영향도 주지 않는다.

배열 입력변수

함수의 매개변수가 일차원 배열이라면 배열의 크기를 입력해주지 않아도 된다.

int f(int a[])
{
    int len = sizeof(a) / sizeof(a[0]);
       /*** 잘못됨: a의 원소의 개수가 아님 ***/}

하지만 C에서는 이렇게 함수를 작성하면 배열의 길이가 얼마인지 알 수 없다. (이에 대해서는 12.3 단원에서 설명한다.) 따라서 별도 매개변수로 배열의 크기를 전달해주어야 한다.

만약 매개변수가 다차원 배열이라면 열의 개수는 생략해도 되나 행의 개수를 명시해줘야 한다.

int foo(int arr[][10], int row_size)
{
  ...
}

다차원 배열에서 행의 크기를 정해줘야한다는 점이 번거롭게 느껴질 수도 있다. 다행히도, 이를 포인터의 배열로 해결해줄 수 있다. C99의 가변크기 배열 매개변수는 심지어 더 나은 해결책을 제공한다.

가변크기 배열 매개변수 (C99)

C99에선 가변크기 배열도 매개변수가 될 수 있다.

int get_sum_of_array(int arr[], int size)
{}

위 함수에서는 배열 arr의 크기와 size 사이엔 아무런 직접적인 연결관계가 없다. 함수 본문이 sizearr의 크기처럼 간주하지만 실제 배열의 크기는 size보다 클 수도 있다(혹은 더 작을 수도 있다. 이 경우 함수가 제대로 작동하지 않을 것이다).

가변크기 배열 매개변수를 사용하게 되면 arr의 크기가 size임을 명시적으로 정해줄 수 있다. 단, size가 먼저 와야한다. 가변크기 배열 매개변수를 사용할 땐 순서가 매우 중요하다.

int get_sum_of_array(int size, int arr[size])
{}

새로운 버전의 get_sum_of_array의 원형을 작성하는 방법은 여러 가지가 있다.

int get_sum_of_array(int size, int arr[size]);    /* 버전 1 */
int get_sum_of_array(int size, int arr[*]);    /* 버전 2-1 */
int get_sum_of_array(int, int arr[*]);    /* 버전 2-2 */

* 표현을 써준 이유는 매개변수의 이름은 함수 선언에서 선택 사항이기 때문이다. 만약 첫번째 매개변수의 이름이 생략된다면 배열의 크기가 size 임을 정해줄 수 없지만 *을 통해 배열의 크기가 이전 매개변수와 관련되어있다는 힌트를 줄 수 있다.

배열 매개변수 선언에서 static 사용하기 (C99)

C99는 static이라는 키워드를 배열 매개변수에서 사용할 수 있게 해준다. (이 키워드는 C99 이전에도 존재하던 키워드다. 18.2 단원에서 C89 기준으로 이 키워드를 설명한다.)

다음 예시에서 숫자 3 앞에 static을 적어놓는다면 배열 arr의 크기가 최소한 3이 될 수 있도록 강제해준다.

int get_sum_of_two_dimensional_array(int arr[static 3], int size)
{}

static을 위와 같이 작성한다고 해서 프로그램의 행동에 영향을 주지는 않는다. static의 존재는 그저 C 컴파일러가 배열에 접근하는 규칙을 좀 더 빨리 생성할 수 있도록 "힌트"를 줄 뿐이다. (만약 컴파일러가 배열이 언제나 최소 크기로 특정된다고 판단하면 함수 호출 이전에 이러한 원소들을 메모리로부터 실질적으로 함수의 구문에서 사용하기 전에 "미리 받아올" 수 있게 해준다).

복합 리터럴

C99에서는 복합 리터럴compound literal을 사용해 익명의 배열을 만들어 입력변수로 넘겨줄 수 있다.

total = get_sum_of_array((int []) {3, 0, 3, 4, 1}, 5);

return문

void 함수가 아닌 함수는 반드시 return문을 통해 구체적으로 어떤 값을 반환할지를 명시해줘야한다. return문은 다음과 같은 형태를 띤다.

return expression ;

return문은 반환형이 void인 함수에서도 사용될 수 있다. 다만, 이 경우 표현식을 작성해주지 않는다.

return;    /* void 함수에서 반환(종료)한다 */

프로그램 종료

main은 함수이기 때문에 반드시 반환형을 가져야 한다. 일반적으로 main 함수의 반환형은 int이다. 그렇기 때문에 지금까지의 예제에서 main 함수는 다음과 같이 작성했었다:

int main(void)
{}

예전 C 프로그램들은 main 함수의 반환형을 생략해줘서 옛 방식으로, 자동으로 반환형이 int가 되게 함수를 정의했다.

main()
{}

C99에선 함수의 반환형을 생략하는 것은 불가능하기 때문에 위와 같이 반환형을 생략하는 것은 피하는 게 좋다. main의 매개변수 목록에서 void를 생략하는 건 가능하지만, 언제나 코딩 스타일 상 main 함수가 매개변수가 없음을 드러내주기 위해 명시적으로 void를 작성해주는 것이 좋다. (나중에 main 함수가 사실은 매개변수 두 개를 가짐을 배운다. 보통 그 둘을 argcargv라고 부른다.)

main 함수에서 반환하는 값은 상태코드다. 몇몇 운영체제는 프로그램이 종료될 때 반환하는 이 상태코드를 갖고 판별을 해준다. 프로그램이 정상적으로 종료됐다면 main0을 반환할 것이다; 비정상적 종료라면 0이 아닌 값을 main이 반환해줘야 한다. (사실 우리가 반환값을 어떤 목적으로 사용해야하는지에 대한 규칙은 없다.) 당장 상태코드를 어딘 가에 쓸 계획이 없더라도 프로그램이 종료될 때 상태코드를 반환해주는 것은 좋은 습관이다.

exit 함수

main 함수에서 return문을 실행하는 것도 프로그램을 종료하는 한 방법이다. 다른 방법으로는 <stdlib.h> 라이브러리의 exit 함수를 사용하는 것이다. exit 함수에 전달하는 입력변수는 main 함수의 반환값과 같은 의미를 갖는다. 둘 다 프로그램 종료 시의 상태를 의미한다. 정상종료를 표시해주려면 0을 전달해주면 된다:

exit(0);    /* 정상 종료 */

0 이라는 값이 한 눈에 이해하기는 어렵기 때문에, C에선 0 대신 EXIT_SUCCESS를 전달해줄 수 있다(기본적으로 결과는 같다).

exit(EXIT_SUCCESS);    /* 정상 종료 */

반대로 EXIT_FAILURE의 경우 비정상적인 종료를 의미한다.

exit(EXIT_FAILURE);    /* 비정상 종료 */

EXIT_SUCCESSEXIT_FAILURE 둘 다 <stdlib.h>에서 정의된 매크로들이다. EXIT_SUCCESSEXIT_FAILURE의 값은 구현정의되어있다. 보통 순서대로 01의 값을 갖는다.

프로그램을 종료하는 방법으로서 returnexit은 밀접한 관계를 갖는다.

return expression ;

사실 위와 같은 main의 구문은 아래와 같다.

exit(expression);

returnexit의 차이점은 exit은 현재 어떤 함수가 이 함수를 호출하든 프로그램을 종료시킨다는 점이다. return문은 오로지 main 함수에서만 프로그램을 종료시킨다. 몇몇 프로그래머들은 아예 exit만 사용해서 프로그램의 모든 종료 지점을 특정해줄 수 있게 한다.

10 - 프로그램 설계

지역변수

정적지역변수

지역변수의 선언부 앞에 static을 적어주게되면 자동저장기간 대신 정적저장기간static storage duration을 갖게 된다. 정적저장기간을 갖는 변수는 영구적인 메모리 공간을 가지므로 프로그램 실행 내내 그 값을 유지한다. 다음 함수의 예시를 보라.

void f(void)
{
    static int s_i;    /* 정적 지역변수. 역자: 정적 변수는 앞에 s_를 붙여주는 것이 좋은 코딩 습관이다 */}

지역변수 s_istatic으로서 선언되었으므로 프로그램 실행 내내 같은 메모리 공간에 위치하게 된다. f가 반환될 때 s_i는 그 값을 잃지 않는다.

정적지역변수이더라도 블록 스코프를 가지므로 다른 함수에서는 보이지 않는다. 간단히 말해 정적변수는 다른 함수로부터 그 자료를 숨기면서 동시에 같은 함수가 나중에 호출되더라도 같은 값을 유지하는 변수다.

프로그램 설계하기

지금까지 배운 프로그램의 주요 요소는 다음과 같다.

  • #include#define 같은 전처리 지시자
  • 형정의
  • 외부변수의 선언
  • 함수 원형
  • 함수 정의

위와 같은 규칙을 따르기 위해 프로그램을 구조화하는 방식은 여러가지이다. 그 중 한 가지 예시를 들어보겠다.

  • #include 지시자
  • #define 지시자
  • 형정의
  • 외부변수의 선언
  • main 함수 외의 함수들의 원형
  • main 함수 정의
  • 나머지 함수들의 정의
profile
"Life isn't about finding yourself. Life is about creating yourself."

0개의 댓글