본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Computer Science

C 기초: 컴파일, 자료형, 연산자, 제어문, 함수, 배열과 문자열

작성 2026. 10. 13.추가
목차26개 항목
출처와 참고

하버드 CS50(부스트코스 「모두를 위한 컴퓨터 과학」) 1~3강과 고무오리 CS 스터디의 CS50 정리 노트를 바탕으로 재구성한 노트다. 연산자와 수식, 제어문, 함수, 배열 초기화 규칙, 문자열 라이브러리 함수는 고려사이버대학교 「프로그래밍 기초와 실습(C언어)」(2024-2학기) 강의 내용으로 보강했다.

컴퓨터가 정보를 0과 1로 표현하는 방법, C 코드가 실행 파일이 되는 과정, 배열과 문자열이 메모리에 놓이는 방식을 다룬다. 이어지는 메모리 이야기는 포인터와 메모리에 있다.

컴퓨팅 사고: 정보의 표현

  • 비트는 0 또는 1, 바이트는 비트 8개다. 1바이트로 2⁸ = 256가지 값을 표현할 수 있다.
  • 문자는 ASCII, Unicode 같은 표준에 따라 숫자로 바꿔 표현한다(진법·보수·부동 소수점은 데이터의 표현).
  • 그림은 픽셀마다 RGB 값을 숫자로 저장한다.
  • 어떤 정보든 결국 0과 1의 조합이다.

알고리즘과 의사 코드

알고리즘은 입력을 원하는 출력으로 바꾸는 처리 과정, 즉 규칙의 순서적 나열이다. 정확성과 효율성을 함께 따져야 한다. 전화번호부에서 이름을 찾는 의사 코드에는 프로그래밍의 기본 요소가 모두 들어 있다.

요소역할
함수무엇을 할지 알려 주는 동사 (예: "책을 펼친다")
조건여러 갈래 중 하나를 고름
불리언 표현참/거짓으로 답이 나오는 질문 (예: "찾는 이름이 이 페이지에 있는가?")
루프같은 일을 반복

컴파일 과정

make나 clang으로 "컴파일한다"고 하지만, 실제로는 네 단계를 거친다.

단계하는 일결과
1. 전처리 (Preprocessing)#으로 시작하는 지시문을 처리. 예: #include <stdio.h>는 헤더 파일 내용을 그 자리에 붙여 넣음전처리된 C 코드
2. 컴파일 (Compiling)C 코드를 어셈블리 코드로 변환어셈블리 코드
3. 어셈블 (Assembling)어셈블러가 어셈블리 코드를 CPU가 실행할 수 있는 0과 1의 오브젝트 코드(기계어)로 변환오브젝트 파일
4. 링크 (Linking)링커가 여러 오브젝트 파일(직접 짠 코드, stdio 같은 라이브러리)을 하나의 실행 파일로 합침실행 파일

소스 파일이 하나뿐이어도 프로그램 시작 코드와 printf 같은 C 표준 라이브러리를 붙여야 하므로, 실행 파일을 만들려면 링크 단계를 반드시 거친다. 정적·동적 링킹의 차이는 GCC 컴파일과 링크에 정리했다. 어셈블리 언어가 기계어로 바뀌는 과정은 기본 컴퓨터 프로그래밍에서 볼 수 있다.

디버깅과 코드 디자인

  • printf로 중간값을 출력해 보는 것이 가장 기본적인 디버깅이다. 한 줄씩 실행하며 변수를 보려면 디버거(gdb·lldb, IDE 디버거, CS50 환경의 debug50)를 쓴다.
  • 여러 사람이 함께 작업할수록 자동 검사 도구가 도움이 된다. 포매터·린터(clang-format, CS50의 style50)는 코드가 일관된 스타일로 작성되었는지 검사한다.
  • 고무 오리 디버깅: 앞에 놓인 고무 오리(아무 물체)에게 코드를 한 줄씩 말로 설명하다 보면 문제를 스스로 발견하게 된다.

자료형과 크기

C의 자료형은 차지하는 메모리 크기가 정해져 있다. (64비트 리눅스·macOS 기준. C 표준은 최소 크기만 정하므로 환경마다 다를 수 있고, 예를 들어 64비트 Windows에서는 long이 4바이트다. bool은 C99~C17에서는 <stdbool.h>를 포함해야 쓸 수 있고, C23부터는 bool·true·false가 키워드라 헤더 없이 쓴다.)

자료형의미크기
bool불리언1바이트
char문자1바이트
int정수4바이트
float실수4바이트
long더 큰 정수8바이트
double더 정밀한 실수8바이트
string문자열 (CS50 라이브러리)길이에 따라 다름

연산자와 수식

수식(expression)은 값을 내는 코드 조각이다. 상수 10, 변수 x 하나도 수식이고, 연산자로 피연산자를 엮어 더 큰 수식을 만든다. 끝에 ;를 붙이면 문장이 된다. C의 연산자 체계는 C++·Java·JavaScript·Go가 거의 그대로 물려받아서, 아래 함정 대부분이 그 언어들에도 있다.

분류연산자
산술+ - * / %, 단항 + -
증감++ --
대입=, += -= *= /= %= &= |= ^= <<= >>=
관계== != < > <= >=
논리&& || !
비트& | ^ ~ << >>
기타?:, ,, sizeof, 형 변환 (type), 주소 &, 역참조 *, 멤버 . ->

나눗셈과 나머지

  • 정수끼리 나누면 몫도 정수이고 소수부는 0 쪽으로 버린다(C99부터 표준). 7 / 2는 3, -7 / 2는 -3이다. 실수 결과가 필요하면 한쪽을 실수로 만든다(7 / 2.0, (double)a / b).
  • %는 정수 전용이고 결과의 부호는 왼쪽 피연산자를 따른다(-7 % 3은 -1). 그래서 홀수 판별을 x % 2 == 1로 쓰면 음수 홀수를 놓친다. x % 2 != 0으로 쓴다.
  • 정수를 0으로 나누면 미정의 동작이고 대부분 프로그램이 죽는다.
  • 거듭제곱 연산자는 없다. ^는 XOR이다. 제곱은 x * x, 일반적인 경우는 <math.h>의 pow를 쓴다.
  • 몫과 나머지는 단위 변환의 기본 도구다. 초를 분·초로 나누면 min = sec / 60; s = sec % 60;이고, 거스름돈처럼 큰 단위부터 차례로 몫과 나머지를 구한다.

타입이 다른 피연산자가 섞일 때의 정수 승격과 부호 없는 정수 함정은 C의 정수 자료형과 형 변환에 있다.

증감 연산자

형태수식의 값변수
++x (전위)증가한 뒤의 값1 증가
x++ (후위)증가하기 전의 값1 증가

변수에만 쓸 수 있어서 10++, (x + 1)++은 컴파일 오류다. 한 수식 안에서 같은 변수를 두 번 바꾸거나, 바꾸면서 따로 읽으면 미정의 동작이다. 피연산자의 평가 순서를 표준이 정하지 않았기 때문이다.

i = i++ + 1;              /* 미정의 동작 */
a[i] = i++;               /* 미정의 동작 */
printf("%d %d", i, i++);  /* 함수 인자의 평가 순서도 정해져 있지 않다 */

결과가 컴파일러와 최적화 옵션마다 달라지므로 증감은 독립된 문장으로 뗀다. GCC는 -Wsequence-point(-Wall에 포함), Clang은 -Wunsequenced로 경고한다.

대입도 수식이다

x = 3은 값이 3인 수식이라 y = x = 3;은 오른쪽부터 묶여 둘 다 3이 된다. 왼쪽에는 메모리 위치를 나타내는 lvalue만 올 수 있다(100 = x;는 오류). 이 성질 때문에 if (x = 0)은 비교가 아니라 대입이 되어 절대 실행되지 않고 x까지 바꿔 놓는다. -Wparentheses(-Wall에 포함)가 경고하며, 일부러 대입하면서 검사할 때는 while ((c = getchar()) != EOF)처럼 괄호로 감싸고 비교를 적는다.

복합 대입 x op= e는 x = x op (e)라서 x *= y + 1은 x = x * (y + 1)이다.

관계·논리 연산자와 단락 평가

  • 관계 연산의 결과는 int 1 또는 0이다. (3 >= 2) + 5는 6이다.
  • 2 < x < 5는 (2 < x) < 5로 묶여 0이나 1을 5와 비교하므로 늘 참이다. 2 < x && x < 5로 쓴다.
  • 실수는 ==로 비교하지 않는다. 0.1 + 0.2 == 0.3은 거짓이다. 차이의 절댓값이 허용 오차보다 작은지 본다(fabs(a - b) < 1e-9, 데이터의 표현).
  • &&, ||는 왼쪽부터 평가하고 결과가 정해지면 오른쪽을 건너뛴다(단락 평가, short-circuit evaluation). 그 지점이 순서점(sequence point)이라 왼쪽의 부수 효과는 오른쪽 평가 전에 끝난다. if (p != NULL && p->count > 0), if (n == 0 || total / n > 10)처럼 가드로 쓰는 것이 정상 관용구다. 반대로 꼭 실행돼야 하는 호출을 오른쪽에 두면 빠질 수 있다.

비트 연산자

플래그 묶음, 마스크, 2의 거듭제곱 곱셈·나눗셈에 쓴다. 활용 기법은 비트 조작에 있다.

함정설명
&와 && 혼동&는 단락 평가가 없다. 1 & 2는 0, 1 && 2는 1
음수 오른쪽 시프트부호 비트를 채울지 0을 채울지 구현 정의다. 비트 연산은 부호 없는 타입으로 한다
폭 이상 시프트32비트 int를 32 이상 시프트하면 미정의 동작
부호 있는 값 왼쪽 시프트음수이거나 결과가 범위를 넘으면 미정의 동작

조건, 쉼표, sizeof

  • 조건 연산자 c ? a : b는 둘 중 하나만 평가한다. 값을 고르는 짧은 식(x = (c) ? a : b;)에만 쓰고, 중첩되면 if로 바꾼다.
  • 쉼표 연산자 a, b는 a를 평가해 버리고 b의 값을 낸다. 실제로는 for (i = 0, j = n - 1; i < j; i++, j--)에서나 쓴다. 함수 인자 사이의 쉼표는 이 연산자가 아니다.
  • sizeof는 타입이나 수식의 바이트 수(size_t)를 컴파일 시점에 낸다. 피연산자를 실행하지 않으므로 sizeof(i++)에서 i는 그대로다. 함수 안에서 배열 매개변수에 쓰면 포인터 크기가 나온다(포인터와 메모리).

우선순위 함정

표 전체를 외울 필요는 없다. 대략 단항 > 산술 > 시프트 > 관계 > 비트 > 논리 > 조건 > 대입 > 쉼표 순이고, 단항·조건·대입만 오른쪽부터 묶인다. 실수는 정해진 곳에서 난다.

코드실제 해석흔한 의도
flags & MASK == 0flags & (MASK == 0)(flags & MASK) == 0
x << 1 + 1x << 2(x << 1) + 1
*p++*(p++): 값을 읽고 포인터를 다음 칸으로(*p)++: 가리키는 값을 증가
!x == y(!x) == y!(x == y)

비트 연산자가 비교 연산자보다 낮은 것은 &&가 없던 초기 C에서 &를 논리 AND로 쓰던 흔적이다. 비트 연산이 비교·산술과 섞이면 괄호를 친다.

제어문

프로그램의 흐름은 순차·선택·반복 세 구조의 조합이다. C에는 불리언 타입이 늦게 들어와 조건식은 0이면 거짓, 0이 아닌 모든 값이 참이고, 관계·논리 연산의 결과는 int 1 또는 0이다. =와 ==를 헷갈리는 함정과 단락 평가는 위 「연산자와 수식」 절에 있다.

선택: if와 switch

  • 조건이 참일 때 실행할 문장이 둘 이상이면 중괄호로 묶은 복합문(블록) 으로 쓴다. 중괄호 없이 들여쓰기만 맞추면 첫 문장만 조건에 걸린다.
  • 매달린 else(dangling else): else는 들여쓰기와 상관없이 가장 가까운, 아직 짝이 없는 if 와 짝을 이룬다. 바깥 if에 붙이려면 안쪽 if를 중괄호로 감싼다.
if (score > 80)
    if (score >= 90) grade = 'A';
else grade = 'B';          /* 들여쓰기와 달리 안쪽 if(score >= 90)의 else다 */

switch는 제어식의 값에 맞는 case로 점프한다. case는 진입점일 뿐이라 break가 없으면 아래로 계속 실행된다(fallthrough, C++ 기초의 fallthrough 절). 여러 값이 같은 처리를 공유할 때는 일부러 case를 붙여 쓴다(case 4: case 6: case 9: case 11: days = 30; break;).

case 라벨에가능 여부
정수 상수, 문자 상수 'a', 열거 상수, 상수만으로 된 식 2 + 3된다
변수, 변수가 들어간 식안 된다. 컴파일 시점에 값이 정해진 정수 상수식이어야 한다
실수 0.5, 문자열 "abc"안 된다. 제어식도 정수 계열이어야 한다. 문자열 분기는 if와 strcmp로 한다
같은 값 두 번안 된다(컴파일 오류)

범위 조건(score >= 90)은 switch로 표현할 수 없으므로 if-else if 사슬을 쓴다.

반복: while, do-while, for

반복문조건 검사맞는 곳
while (조건) { }들어가기 전. 처음부터 거짓이면 한 번도 안 돈다횟수를 모르고 조건으로 끝날 때. 끝을 알리는 특수 값(센티널, 예: -1)이 나올 때까지 입력받기
do { } while (조건);한 번 실행한 뒤. 최소 한 번은 돈다. 끝에 세미콜론이 필요하다메뉴 출력 후 입력받기, 올바른 값이 들어올 때까지 다시 묻기
for (초기식; 조건식; 증감식) { }들어가기 전. 초기식은 한 번, 증감식은 매 회 끝에 실행횟수가 정해진 반복, 배열 순회
  • for는 while로 그대로 옮길 수 있다. 세 부분은 모두 생략할 수 있고 for (;;)는 무한 루프다. 쉼표로 변수를 둘 이상 초기화할 수 있다(for (i = 0, k = 0; ...)).
  • for (i = 0; i < 10; i++);처럼 괄호 뒤에 세미콜론을 붙이면 빈 문장이 반복되고 본문은 한 번만 실행된다. while (i < 10) 안에서 i를 바꾸지 않으면 무한 루프다.
  • break는 가장 안쪽 반복문 하나(또는 switch)만 빠져나온다. continue는 남은 본문을 건너뛰고 while이면 조건식으로, for면 증감식으로 간다.
  • 의도한 무한 루프 while (1)은 탈출 조건이 복잡할 때 안에서 break로 끝낸다.

goto

goto 레이블;은 같은 함수 안의 레이블로 조건 없이 점프한다. 흐름을 따라가기 어려워 일반 분기·반복에는 쓰지 않는다. 다만 중첩 반복문을 한 번에 빠져나오거나, 오류가 나면 할당한 자원을 역순으로 해제하는 정리 코드로 모으는 용도로는 C 코드(리눅스 커널 등)에서 관용적으로 쓴다.

if (!(a = malloc(n))) goto fail;
if (!(b = malloc(n))) goto free_a;
/* ... */
return 0;
free_a: free(a);
fail:   return -1;

함수

함수는 이름 붙은 작업 단위다. 같은 코드를 한 곳에 모아 중복을 없애고, 프로그램을 작은 모듈로 나눠 읽고 디버깅하기 쉽게 만든다.

double c_to_f(double c);        /* 함수 원형: 헤더 + 세미콜론, 매개변수 이름은 생략 가능 */
 
int main(void) { printf("%f\n", c_to_f(36.5)); }   /* 36.5가 인수 */
 
double c_to_f(double c)         /* 정의. c가 매개변수 */
{
    return c * 9.0 / 5.0 + 32.0;
}
용어뜻
인수(argument)호출할 때 실제로 넘기는 값
매개변수(parameter)그 값을 받는 함수 쪽 변수. 인수 값으로 초기화되는 지역 변수다
반환값return 식;으로 돌려주는 값. 하나만 돌려줄 수 있다. 여럿이 필요하면 포인터 매개변수나 구조체를 쓴다
함수 원형(prototype)정의보다 호출이 먼저 나올 때 컴파일러에게 이름·매개변수 타입·반환형을 미리 알린다. 보통 헤더 파일에 모은다
  • C는 위에서 아래로 한 번 읽으며 컴파일하므로, 원형 없이 정의보다 먼저 호출하는 것은 C99부터 표준에서 허용되지 않는다(C89는 int를 반환한다고 가정했고, GCC 14부터는 기본으로 오류다). 정의를 main 위에 두면 원형이 없어도 되지만 main이 맨 아래로 밀린다.
  • 매개변수가 없으면 int f(void)로 쓴다. C17까지 int f()는 「매개변수 정보 없음」이라 아무 인수나 받아도 컴파일러가 검사하지 않는다. C23부터는 f()도 f(void)와 같다.
  • C의 인수 전달은 항상 값에 의한 호출이다. 함수 안에서 매개변수를 바꿔도 호출한 쪽 변수는 그대로이고, 바꾸려면 주소를 넘긴다(포인터와 메모리). 배열은 첫 원소의 주소가 넘어가므로 함수 안에서 원소를 바꾸면 원본이 바뀐다.
  • 표준 라이브러리 함수는 헤더를 포함해 원형을 얻는다. rand()는 0~RAND_MAX 사이의 의사 난수를 주고, 시드가 같으면 매번 같은 수열이 나오므로 srand((unsigned)time(NULL))로 한 번 시드를 정한다. 범위는 rand() % n으로 줄인다(작은 편향이 있어 암호용으로는 쓰지 않는다).
  • 함수가 자기 자신을 부르는 재귀는 멈추는 조건(기저 사례)과 문제를 줄여 자신을 부르는 부분으로 이뤄진다. 원리와 꼬리 재귀는 재귀에 있다.

변수가 어디서 보이고(스코프) 언제까지 사는지(저장 기간), static·extern의 뜻은 C의 스코프와 링키지에 있다.

배열

같은 자료형의 값들을 메모리에 연속으로 저장하고, 하나의 이름과 인덱스로 다루는 구조다.

int scores[5];   // int 5개짜리 배열, 20바이트가 연속으로 잡힌다
규칙예
중괄호로 초기화한다. 초깃값이 원소 수보다 적으면 나머지는 0 이 된다int a[5] = {1, 2}; → 1 2 0 0 0, int z[100] = {0};은 전부 0
초깃값이 있으면 크기를 생략할 수 있다. 개수는 sizeof a / sizeof a[0]으로 구한다int a[] = {1, 2, 3}; → 크기 3
초깃값 없는 지역 배열은 쓰레기 값이다. 전역·static 배열은 0으로 채워진다-
배열끼리 대입·비교할 수 없다. 원소별로 복사하거나 memcpy를 쓴다a = b; 컴파일 오류
인덱스 범위를 검사하지 않는다. 벗어난 접근은 컴파일도 되고 실행도 될 수 있지만 다른 변수를 덮어쓰는 정의되지 않은 동작이다int s[5]; s[5] = 60;

2차원 배열 int m[2][3]은 행 우선(row-major) 으로 m[0][0] m[0][1] m[0][2] m[1][0] ... 순서로 한 줄에 놓인다. 그래서 바깥 반복을 행, 안쪽 반복을 열로 돌아야 메모리를 순서대로 읽어 캐시에 유리하다. 함수 매개변수로 받을 때는 첫 번째 크기만 생략할 수 있다(void f(int m[][3], int rows)). 한 행의 길이를 알아야 m[i][j]의 주소를 계산할 수 있기 때문이다.

크기를 코드에 고정하면 점수 개수가 바뀔 때마다 코드를 고쳐야 한다. 크기를 사용자에게 입력받아 그만큼 배열을 만들면 더 유연하다.

#include <cs50.h>
#include <stdio.h>
 
int main(void)
{
    int n = get_int("Number of scores: ");
    int scores[n];                       // 입력받은 크기의 배열 (가변 길이 배열, VLA)
 
    for (int i = 0; i < n; i++)
    {
        scores[i] = get_int("Score %i: ", i + 1);
    }
 
    int sum = 0;
    for (int i = 0; i < n; i++)
    {
        sum += scores[i];
    }
    printf("Average: %f\n", sum / (float) n);
}

가변 길이 배열(VLA)은 스택에 잡히므로 n이 크면 스택 오버플로우가 난다. C11부터는 컴파일러가 지원하지 않아도 되는 선택 기능이라 MSVC에서는 쓸 수 없다. 크기가 실행 중에 정해지고 클 수 있다면 malloc으로 힙에 잡는 것이 안전하다(포인터와 메모리).

문자열은 char의 배열

문자열 "CS50"은 메모리에 다음처럼 저장된다.

인덱스01234
값'C''S''5''0''\0'
  • 맨 끝의 널 종단 문자 \0(모든 비트가 0인 바이트)이 문자열의 끝을 표시한다. 그래서 4글자 문자열이 5바이트를 차지한다.
  • 문자열의 길이는 \0을 만날 때까지 세어서 알아낸다. \0은 문자열 중간에 올 수 없고 끝 표시로만 쓰인다.
  • 배열 문자열과 문자열 리터럴의 차이, 리터럴이 읽기 전용인 이유는 포인터와 메모리에 있다.

문자 처리: ctype.h

ASCII에서 소문자는 대문자보다 32 크다('a' = 97, 'A' = 65). 그래서 문자를 숫자처럼 계산해 대소문자를 바꿀 수도 있지만, 보통은 <ctype.h>의 함수를 쓴다. 검사 함수는 참이면 0이 아닌 값(1이라는 보장은 없다), 거짓이면 0을 돌려준다.

함수참이 되는 문자
isalpha, isdigit, isalnum영문자, 0~9, 둘 중 하나
isxdigit16진수 숫자 (0-9a-fA-F)
isupper, islower대문자, 소문자
isspace공백, \t, \n, \v, \f, \r
ispunct, isprint, iscntrl구두점, 출력 가능 문자, 제어 문자
toupper, tolower바꾼 값을 돌려준다. 해당 없으면 그대로
  • 인자는 unsigned char 범위의 값이나 EOF여야 한다. char가 부호 있는 플랫폼에서 UTF-8 한글 바이트(0x80 이상)를 그대로 넘기면 음수가 되어 미정의 동작이다. isalpha((unsigned char)c)처럼 넘긴다.
  • 결과는 로케일을 따르고 기본 "C" 로케일은 ASCII만 판별한다. 유니코드 분류는 <wctype.h>나 ICU 같은 라이브러리로 한다.

문자열 처리: string.h

문자열 함수는 모두 '\0'을 만날 때까지 나아갈 뿐 길이 정보를 따로 받지 않는다. 그래서 함수는 목적지 배열이 얼마나 큰지 모른다. 아래 함정 대부분이 여기서 나온다. 배열에는 대입할 수 없으므로(s = "World";는 오류) 내용을 바꾸려면 복사 함수를 쓴다.

함수하는 일함정
strlen(s)'\0' 앞까지의 길이매번 끝까지 센다(O(n)). for (i = 0; i < strlen(s); i++)는 O(n²)
strcpy(d, s)s를 d에 복사d의 크기를 검사하지 않아 버퍼 오버플로
strcat(d, s)d 끝에 이어 붙임같은 문제에, d의 끝을 찾느라 매번 처음부터 센다
strncpy(d, s, n)최대 n바이트 복사s가 n 이상이면 '\0'을 붙이지 않고, 짧으면 나머지를 '\0'으로 채운다
strncat(d, s, n)최대 n글자를 덧붙이고 '\0'을 붙임n은 d의 크기가 아니라 덧붙일 글자 수다
strcmp(a, b), strncmp사전순 비교, 앞 n글자만 비교같으면 0, 다르면 음수·양수일 뿐 -1·1 보장이 없다
strchr, strrchr, strstr문자의 처음·마지막 위치, 부분 문자열 위치없으면 NULL. 역참조 전에 검사한다
strtok(s, delim)구분자로 토큰 분리원본에 '\0'을 써 넣고 내부 정적 상태를 써서 중첩·멀티스레드에서 깨진다. POSIX strtok_r을 쓴다
memcpy, memmove, memset바이트 단위 복사·채우기영역이 겹치면 memmove (포인터와 메모리)

if (strcmp(a, b))는 "다르면"이라는 뜻이라 헷갈린다. strcmp(a, b) == 0처럼 비교를 적는다. 같은 작업을 Python은 len(s)·s.upper(), JavaScript는 s.length·s.toUpperCase()로 하고 길이를 객체가 들고 있어 이런 함정이 없다.

strncpy는 안전한 strcpy가 아니다

strncpy는 고정 길이 레코드 필드를 채우려고 만든 함수라 문자열 복사에 맞지 않는다. char name[8]에 strncpy(name, "Christopher", sizeof name)을 하면 8바이트가 꽉 차고 '\0'이 없어, 이어지는 printf("%s")가 배열 밖까지 읽는다. 길이를 제한한 복사·이어 붙이기는 다음 중 하나로 한다.

방법특징
snprintf(d, sizeof d, "%s", s)표준 C99. 항상 '\0'으로 끝내고 원래 필요했던 길이를 돌려줘 잘림을 검사할 수 있다
strlcpy, strlcatBSD에서 시작. 항상 '\0'으로 끝내고 필요 길이를 돌려준다. glibc 2.38과 POSIX.1-2024에 들어갔다
strcpy_s 등 (C11 부록 K)표준의 선택 사항이라 구현이 드물고 사실상 MSVC 전용이다
int need = snprintf(path, sizeof path, "%s/%s.txt", dir, name);
if (need < 0 || (size_t)need >= sizeof path) {
    /* 잘렸다: 오류 처리 */
}

한 줄 입력: fgets

gets는 목적지 크기를 받지 않아 긴 입력이면 무조건 넘치고, C11에서 표준에서 빠졌다. fgets를 쓴다.

char line[128];
if (fgets(line, sizeof line, stdin) != NULL) {
    line[strcspn(line, "\n")] = '\0';   /* 남은 줄바꿈 제거 */
}

fgets는 최대 size - 1글자를 읽고 '\0'을 붙이며, 줄바꿈까지 읽었으면 그것도 문자열에 들어 있다. 문자열에 '\n'이 없으면 줄이 버퍼보다 길어 잘린 것이고 나머지는 다음 fgets가 읽는다. scanf("%s")의 폭 지정 문제는 C 입력과 파일 입출력에 있다.

문자열과 숫자 변환

문자열 "36.5"와 double 36.5는 전혀 다른 바이트라 변환 함수가 필요하다.

함수방향오류 감지
atoi, atol, atof문자열 → 숫자없다. "abc"도 0이고, 범위를 넘으면 미정의 동작
strtol, strtoul, strtod문자열 → 숫자멈춘 위치(endptr)와 범위 초과(errno == ERANGE)를 알려 준다
sscanf(s, "%d", &n)문자열 → 여러 값성공한 항목 수를 돌려준다. 범위 초과는 미정의 동작
snprintf(buf, n, "%d", x)숫자 → 문자열필요 길이를 돌려준다. sprintf는 크기 검사가 없다
int parse_port(const char *s, long *out) {
    char *end;
    errno = 0;
    long v = strtol(s, &end, 10);
    if (end == s || *end != '\0') return -1;          /* 숫자가 아니거나 뒤에 쓰레기 */
    if (errno == ERANGE || v < 1 || v > 65535) return -1;
    *out = v;
    return 0;
}

사용자 입력이나 파일 내용처럼 믿을 수 없는 문자열은 strtol 계열로 바꾸고, atoi는 이미 검증된 값에만 쓴다.

명령행 인자

프로그램을 실행할 때 명령과 함께 넘기는 추가 정보다.

#include <cs50.h>
#include <stdio.h>
 
int main(int argc, string argv[])
{
    if (argc != 2)
    {
        printf("missing command-line argument\n");
        return 1;                 // 0이 아닌 종료 코드 = 오류
    }
    printf("hello, %s\n", argv[1]);
    return 0;
}
  • argc: 인자의 개수(프로그램 이름 포함), argv: 인자 문자열 배열
  • argv[0]은 프로그램 이름이다. ./hello David라면 argv[1]이 "David".
  • main의 반환값은 종료 상태(exit status) 다. 0은 성공, 그 외는 오류를 뜻한다.
  • Python에서는 sys.argv로 같은 정보를 얻는다.

핵심 정리

  • 컴파일 = 전처리 → 컴파일(어셈블리) → 어셈블(기계어) → 링크(실행 파일).
  • 정수 나눗셈은 0 쪽으로 자르고 %의 부호는 왼쪽을 따른다. 한 수식에서 같은 변수를 두 번 바꾸면 미정의 동작이고, &&·||는 단락 평가로 가드에 쓴다. 비트 연산이 비교와 섞이면 괄호를 친다.
  • else는 가장 가까운 짝 없는 if에 붙고, case 라벨은 정수 상수식만 된다. break는 가장 안쪽 반복문 하나만 빠져나온다.
  • 함수는 정의 전에 원형을 보여야 하고, 인수는 항상 값으로 복사된다.
  • 배열은 같은 자료형을 메모리에 연속으로 저장한다. 부분 초기화하면 나머지는 0이고, 범위 검사는 없다.
  • C 문자열은 \0으로 끝나는 char 배열이라, n글자 문자열은 n + 1바이트를 쓴다. 문자열 함수는 목적지 크기를 모르므로 snprintf·fgets·strlcpy처럼 크기를 받는 함수를 고르고, 숫자 변환은 strtol로 한다.
  • main(int argc, string argv[])로 명령행 인자를 받고, 반환값은 종료 상태다.

관련 노트: GCC 컴파일과 링크, Makefile과 make, C의 정수 자료형과 형 변환, 포인터와 메모리, 비트 조작, C 입력과 파일 입출력

원본 기록2

댓글

GitHub 계정으로 댓글을 남길 수 있어요.