본문 바로가기
C

배열과 포인터

by curious week 2025. 11. 11.

1차원/다차원 배열, 문자열, 함수와 배열 인수

학습 목표

  • 배열(array) 의 메모리 특성과 선언/초기화/순회 방법을 정확히 이해한다.
  • 2차원/3차원 배열 의 행·열(면) 개념과 초기화 규칙을 익힌다.
  • 문자열과 char 배열 의 관계(널 문자 '\0')를 정확히 다룬다.
  • 함수 매개변수로 배열 전달 시의 규칙(첫 차원 생략, 포인터로의 decay)을 이해한다.

1. 배열 개념 핵심

  • 배열(array): 동일 자료형의 값들을 연속적 메모리에 저장하고 하나의 이름으로 관리하는 변수.
  • 요소 접근: 0부터 시작하는 인덱스로 접근 (a[0], a[1], …).
  • 연속 저장이므로 순차 처리(합계, 최댓값, 탐색, 정렬)에 캐시 친화적이고 효율적.

주의: 범위를 벗어난 접근(Out-of-Bounds)미정의 동작(UB) 이며, 다른 데이터 파손/보안 취약점으로 이어질 수 있음.


2. 1차원 배열

2.1 선언 형식

자료형 배열명[개수];   // 개수는 양의 정수 상수식
  • 첨자 범위: 0 ~ 개수-1
  • 배열명만 사용할 때는 배열 시작 주소의 의미(표현식 맥락에서 포인터로 decay)

예:

int a[5];      // a[0]..a[4]
double v[100]; // v[0]..v[99]

2.2 초기화 방법

(1) 선언 후 대입

int arr[4];
arr[0]=10; arr[1]=20; arr[2]=30; arr[3]=40;

(2) 선언과 동시에 초기화

int a1[4] = {10, 20, 30, 40}; // 정확히 4개
int a2[]  = {10, 20, 30, 40}; // 크기 생략 → 4로 결정
int a3[4] = {10, 20};         // 나머지는 0으로 초기화 → {10,20,0,0}
int a4[4] = {0};              // 전부 0
// int e[3] = {1,3,5,7};      // 컴파일 에러: 원소 초과

(3) 전역/정적 배열의 디폴트 초기화

  • 파일 전역 또는 static 배열은 명시 초기화 없으면 0으로 초기화.

2.3 순회·사용 예

#include <stdio.h>
int main() {
    int data[10] = {10,23,5,9,22,48,12,10,55,31};
    int max = data[0];
    for (int i=1; i<10; ++i) {
        if (max < data[i]) max = data[i];
    }
    printf("max=%d\n", max); // 55
}

3. 다차원 배열

3.1 2차원 배열

선언 형식

자료형 이름[행][열];
  • 총 크기 = 행 × 열
  • C는 행 우선(row-major): 한 행(row)이 메모리에서 연속.

예:

int a[2][3];           // 2행 3열
int b[2][3] = {1,2,3,4,5,6};       // 평평하게 나열해도 OK
int c[2][3] = {{1,2},{4,5,6}};     // 부족분은 0 채움 → {{1,2,0},{4,5,6}}
int d[][3]  = {{1,2,3},{4,5,6}};   // 행 수 생략 가능(초깃값 개수로 결정)

사용 예

#include <stdio.h>
int main() {
    int score[4][3] = {
        {90,90,90},{80,80,80},{70,70,70},{60,60,60}
    };
    for (int i=0;i<4;i++){
        int sum=0;
        for (int j=0;j<3;j++){ sum += score[i][j]; }
        printf("%d행 합계=%d\n", i, sum);
    }
}

3.2 3차원 배열

선언 형식

자료형 이름[면][행][열];
  • 총 크기 = 면 × 행 × 열
  • 생략 규칙: 첫째 차원(면)만 생략 가능.
int a[2][3][4] = { /* ... 24개 값 ... */ };
int b[][3][4]  = { /* ... */ };      // OK
// int c[2][][4] = { ... };          // 에러
// int d[2][3][] = { ... };          // 에러

4. 문자열과 char 배열

4.1 문자열 표현

  • C 문자열 = 문자들의 연속 + 종료 널문자 '\0'
  • 배열 크기는 문자열 길이 + 1 이상이어야 함.

4.2 선언·초기화

문자열 리터럴:

char name[]  = "HONG GIL DONG";     // 자동으로 \0 포함, 크기는 14
char name2[16] = "HONG GIL DONG";   // 여유 공간 확보 권장

문자 나열:

char addr1[] = {'S','E','O','U','L','\0'}; // 문자열
char addr2[] = {'S','E','O','U','L'};      // \0 없음 → 문자열 아님(주의)

잘못된 예(버퍼 부족):

// char name[10] = "HONG GIL DONG"; // 문자열 길이 13 + '\0' → 14 필요 → 에러/오버런

4.3 입력과 출력 주의

#include <stdio.h>
int main() {
    char s[50];
    // scanf("%s", s);     // 공백에서 끊김, 버퍼 오버런 위험
    // 안전 대안(한 줄): fgets
    if (fgets(s, sizeof(s), stdin)) {
        // 개행 제거 등 후처리 가능
        printf("%s", s);
    }
}
  • scanf("%s", s) 는 공백 전까지만 읽고 길이 제한 없음오버플로우 위험.
  • 실무에선 fgets 권장(필요 시 개행 처리).

5. 함수와 배열 인수 전달

5.1 1차원 배열 전달

  • 함수 매개변수에서 배열은 첫 요소 포인터로 decay.
  • 보통 배열 크기 n 을 함께 전달.
#include <stdio.h>

double average(int arr[], int n) {   // int *arr 와 동일 시그니처
    int sum = 0;
    for (int i=0; i<n; ++i) sum += arr[i];
    return sum / (double)n;
}

int main() {
    int score[4] = {90,85,100,88};
    printf("평균=%.1f\n", average(score, 4));
}

5.2 2차원 배열 전달

  • 두 번째 차원(열의 크기) 는 반드시 고정되어야 함(컴파일러가 오프셋 계산 가능해야 하므로).
void score_sum(int gr[][5], int row, int column) { // 열=5 고정
    int sum[2] = {0};
    for (int i=0;i<row;i++)
        for (int j=0;j<column;j++)
            sum[i] += gr[i][j];
    for (int i=0;i<row;i++) printf("sum[%d]=%d\n", i, sum[i]);
}

int main() {
    int score[2][5] = {{10,20,30,40,50},{10,10,10,10,10}};
    score_sum(score, 2, 5);
}

5.3 요약 규칙

  • 1차원: int f(int a[], int n) 또는 int f(int *a, int n)
  • 2차원: void g(int a[][C], int r, int c) 처럼 두 번째 차원 C는 상수
  • 3차원: double h(double a[][Y][X], int n) 처럼 뒤쪽 차원들은 상수
  • 범위 체크 책임: C는 배열 크기를 몰라서 호출/함수 내부에서 직접 체크해야 함.

6. 실전 주의사항·베스트 프랙티스

  • 경계 체크: 모든 인덱스 접근 전후로 범위 확인 습관화.
  • 초기화: 자동변수 배열은 초기화하지 않으면 쓰레기 값. 필요 시 {0} 로 초기화.
  • 문자열 버퍼: 항상 +1(널 문자) 고려, 입력은 fgets 선호.
  • 다차원 전달: 마지막 차원부터 크기 확정(컴파일러 오프셋 계산 규칙).
  • 성능: 중첩 루프는 행 우선(row-major) 순서로 접근(캐시 효율 ↑).

7. 확인 문제(짧게)

  1. 아래 선언에서 합법/불법?
int a[][3] = {{1,2,3},{4,5,6}}; // ?
int b[2][]  = {{1,2},{3,4}};    // ?
  1. 문자열 버퍼 길이 문제를 찾고 고치기:
char name[8] = "SEOUL KOREA";  // ?
  1. 2차원 배열 인수를 받는 올바른 시그니처는?
  • void f(int m[][], int r, int c);
  • void f(int (*m)[4], int r, int c);
  • void f(int m[][4], int r, int c);

정답 힌트:

  1. 첫 줄 OK, 둘째 줄 에러. 2) 길이 12+1 필요 → 버퍼 확장. 3) 두 번째/세 번째가 올바름.

포인터 개념, 선언/참조, 포인터 연산

학습 목표

  • 포인터(pointer) 를 “값이 아니라 주소를 저장하는 변수”로 정확히 이해한다.
  • 주소 연산자 &, 간접참조(역참조) * 의 의미와 사용 위치를 구분한다.
  • 초기화되지 않은 포인터의 위험과 자료형 일치의 중요성을 익힌다.
  • 배열 ↔ 포인터 관계, 포인터 산술(증감·차) 규칙을 체화한다.

1) 포인터 기본 개념

  • 일반 변수: “데이터 값”을 저장.
  • 포인터 변수: “데이터가 저장된 메모리 주소”를 저장.
    • 포인터가 가리키는 대상에 접근하려면 *pointer (간접참조, dereference).
int n;        // 일반 변수: 값 저장
int *p = &n;  // 포인터: n의 주소 저장
*p = 200;     // p가 가리키는 주소에 200 저장 (즉, n = 200)
printf("%d\n", n); // 200

포인터를 사용하면 “주소를 통해 간접 접근”을 하므로, 참조 대상이 정확히 무엇인지(어떤 주소인지) 초기화가 매우 중요합니다.


2) 선언/참조와 연산자

2-1. 선언 형식

자료형 *이름;  // 이 포인터는 '자료형' 크기/방식으로 메모리를 해석
  • 포인터의 자료형은 “가리키는 대상의 자료형”과 일치해야 안전하게 동작합니다.

예:

int   *pi;   // int를 가리키는 포인터
float *pf;   // float를 가리키는 포인터
void  *pv;   // 형 미정(범용) 포인터

2-2. 주소 연산자 & 와 간접참조 *

int a = 10;
int *p = &a;   // &a: a의 주소
*p = 20;       // *p: p가 가리키는 "그 메모리 칸" (a)에 기록
printf("%d\n", a); // 20

주의: * 는 선언에서 “포인터형”을 뜻하고, 표현식에서 “간접참조”를 뜻합니다.


3) 안전한 포인터 사용 패턴

3-1. 반드시 초기화

초기화되지 않은 포인터는 쓰레기 주소를 가리켜 미정의 동작(UB) 을 유발합니다.

int *p;      // 초기화 안 함: 위험
// *p = 10;  // UB: 임의 메모리 파손 가능

int x = 0;
p = &x;      // 안전한 초기화
*p = 10;     // OK

3-2. 자료형 일치

포인터의 형과 대상의 형이 다르면 읽기/쓰기 크기와 정렬이 달라져 오동작합니다.

float c;
int *ip = (int*)&c; // 피치 못할 캐스팅은 피하자 (정렬/표현 차이로 위험)

정상 예시:

float c, b = 20.0f;
float *fp = &c;
int a = 10;
*fp = (float)a * b; // 포인터형과 대상형 일치 → 안전

3-3. void * (범용 포인터)

  • 어떤 형의 주소든 담을 수 있지만 직접 역참조 불가. 역참조 시 명시적 캐스팅 필요.
int a = 100;
char ch = 'b';
void *pv = &a;
printf("%d\n", *(int*)pv);
pv = &ch;
printf("%c\n", *(char*)pv);

4) 배열과 포인터의 관계

  • 1차원 배열에서 배열명은 대부분의 표현식에서 첫 요소의 주소decay(자동 변환)합니다.
int a[5] = {10,20,30,40,50};
int *p = a;      // == &a[0]
printf("%d\n", p[2]);   // == *(p+2) == a[2] == 30

배열명 자체는 좌변이 될 수 없고(상수 주소), 함수 인수로 넘어갈 때는 대개 포인터로 변환됩니다.


5) 포인터 산술(Arithmetic)

5-1. 증감/덧셈/뺄셈

  • p+1 은 바이트 1이 아니라 “가리키는 형의 크기”만큼 주소가 이동합니다.
    • char* 는 1바이트씩, int* 는 sizeof(int) 바이트씩 이동.
char *pc = (char*)0x100; // 가정
pc+1 → 0x101

int *pi = (int*)0x100;   // 가정, 32-bit int
pi+1 → 0x104

5-2. 배열 순회와 증감 연산자의 차이

int a[] = {10,20,30,40,50};
int *p = &a[0];

printf("%d\n", *p);    // 10
printf("%d\n", *p++);  // 10  (사용 후 p가 다음 요소로 이동)
printf("%d\n", *++p);  // 30  (p를 먼저 이동한 뒤 역참조)
  • *p++ : 후위 — *(p++) (역참조 후 포인터 증가)
  • *++p : 전위 — *(++p) (포인터 증가 후 역참조)

5-3. 포인터 간 차이(difference)

  • 같은 배열 내 요소를 가리키는 포인터끼리만 차이를 구하는 것이 정의됨.
int a[10], *p=&a[6], *q=&a[2];
printf("%ld\n", p - q); // 4 (요소 개수 단위의 차이)
  • 포인터 합산은 불가, 전혀 무관한 영역끼리의 차이도 미정의 동작/의미 없음.

6) 자주 틀리는 포인트(실전 체크리스트)

  1. 초기화 안 된 포인터 사용 금지
    • 선언 즉시 NULL 또는 유효한 주소로 초기화.
  2. 자료형 일치
    • int* 로 float 를 다루지 말 것. 꼭 필요한 캐스팅은 최소화하고 구조를 재설계.
  3. 배열 경계 준수
    • p+i 사용 시 항상 0 <= i < 길이를 보장. 범위 체크는 호출자/피호출자 중 한쪽에서 확실히.
  4. postfix vs prefix
    • *p++와 *++p의 의미 차이를 정확히 구분.
  5. void*
    • 역참조 전 정확한 형으로 캐스팅. (라이브러리 콜백/메모리 풀/제너릭 컨테이너에서 흔히 사용)

7) 예제 모음(슬라이드 재구성)

7-1. 포인터로 값 복사/쓰기

#include <stdio.h>
int main(void) {
    int a=5000, b=0, c=0;
    int *p = &a;   // p → a
    b = *p;        // b = a
    p = &c;        // p → c
    *p = 100;      // c = 100
    printf("a=%d, b=%d, c=%d\n", a,b,c); // 5000,5000,100
}

7-2. 포인터와 형 일치

#include <stdio.h>
int main(void) {
    int a = 10; float b = 20.0f, c;
    float *pf = &c;
    *pf = (float)a * b; // OK
    printf("%f\n", c);  // 200.000000
}

7-3. void* 사용

#include <stdio.h>
int main(void) {
    int n = 100; char ch = 'b';
    void *pv = &n;
    printf("%d\n", *(int*)pv);
    pv = &ch;
    printf("%c\n", *(char*)pv);
}

7-4. 배열 + 포인터 산술

#include <stdio.h>
int main(void) {
    int a[] = {10,20,30,40,50};
    int *p = a;        // &a[0]
    printf("%d\n", *p);      // 10
    printf("%d\n", *p++);    // 10, p → a[1]
    printf("%d\n", *++p);    // p → a[2], 출력 30
    printf("%ld\n", (&a[6])-(&a[2])); // 정의된 동작 아님(경계 밖) → 실제 코드는 금지
    return 0;
}

위 마지막 줄처럼 배열 경계 밖 주소로 연산/비교는 금지(실전 코드에서는 하지 마세요).


8) 용어 메모

  • UB: Undefined Behavior(미정의 동작)
  • dereference: 간접참조(포인터가 가리키는 주소의 메모리에 접근)
  • decay: 배열이 표현식 맥락에서 첫 요소 포인터로 변환되는 규칙
  • row-major: 행 우선 메모리 배치

9) 빠른 확인 문제

  1. 아래 중 정상 동작만 고르세요.
    a. 초기화 안 한 int *p; *p=7;
    b. int a; int *p=&a; *p=7;
    c. float f; int *p=(int*)&f; *p=7;
  2. 결과를 예측하세요.
int a[]={1,2,3}; int *p=a;
printf("%d ", *p++); // ?
printf("%d ", *++p); // ?
  1. void *pv = &x; 를 역참조하려면 어떤 조치가 필요한가?

정답 체크:

  1. b만 안전. a는 UB, c는 형 불일치로 위험.
  2. *p++ → 1 출력 후 p가 a[1]로, *++p → p가 a[2]로 이동 후 3 출력.
  3. 정확한 형으로 캐스팅 후 역참조(예: *(int*)pv).

포인터·배열 심화, 포인터 배열, 이중 포인터

학습 목표

  • 문자열 리터럴과 char* 의 불변성을 명확히 이해한다.
  • 1차원/2차원 배열을 포인터 산술로 순회·접근하는 법을 익힌다.
  • 포인터 배열(배열의 각 요소가 포인터) 의 활용처(“가변 길이 행들”)를 이해한다.
  • 이중 포인터(T**) 의 의미와 대표적 사용 패턴(출력 매개변수, argv, 2D 가변 컨테이너)을 익힌다.

1) 문자열과 포인터

핵심

  • "TEXT" 는 문자열 리터럴(읽기 전용 영역) 입니다. 이를 가리키는 char *p = "TEXT"; 에서 *p = 't'; 와 같은 수정은 금지(미정의 동작).
  • 수정 가능한 문자열이 필요하면 배열에 담아야 합니다.
// (권장 X) 리터럴을 가리키는 포인터: 읽기 전용
const char *p = "COMPUTER";    // const 붙이면 의도가 명확
// *p = 'c';  // 금지!

// (권장) 수정 가능 버전: 배열에 복사
char s[] = "COMPUTER";         // 스택에 'C','O',...,'\0' 저장
s[0] = 'c';                    // OK

용어 메모

  • literal: 소스에 직접 적힌 상수 데이터
  • read-only segment: 실행 중 수정 불가 메모리 영역

2) 1차원 배열과 포인터

배열명 ↔ 포인터

  • 1차원 배열에서 배열명은 첫 요소의 주소로 decay 합니다(표현식 맥락).
  • a[i] ≡ *(a + i) ≡ *(p + i) ≡ p[i] (단, p == a 또는 p == &a[0] 인 경우)
#include <stdio.h>

int main(void) {
    int a[] = {10,20,30,40,50};
    int *p = a;          // == &a[0]
    int b, c, d;

    b = *p + *(p + 3);   // 10 + 40 = 50
    p++;                 // p → a[1]
    c = *p + *(p + 3);   // 20 + 50 = 70
    d = *p + 3;          // 20 + 3  = 23

    printf("b=%d, c=%d, d=%d\n", b, c, d);
}

안전 규칙

  • 경계 체크: 0 <= i < len 보장.
  • 형 일치: int* 로 float 영역을 읽거나 쓰지 않기(미정의 동작).
  • 초기화: int *p = NULL; 후 유효한 주소로 세팅.

3) 2차원 배열과 포인터

물리 메모리 배치

  • C의 2차원 배열은 행 우선(row-major) 으로 1차원처럼 연속 배치됩니다.
  • int a[rows][cols]; 에서 &a[i][j] == (&a[0][0]) + (i*cols + j).
#include <stdio.h>

int main(void) {
    static int a[2][3] = { {1,2,3}, {4,5,6} };
    int *p = &a[0][0];            // 평탄화 시작 주소
    int rows = 2, cols = 3;

    // 포인터 산술로 2D 순회
    for (int i=0; i<rows; ++i) {
        for (int j=0; j<cols; ++j) {
            printf("%d ", *(p + i*cols + j));
        }
        printf("\n");
    }
}

올바른 포인터 초기화 예

  • int *pt = &a[0][0]; 또는 int *pt = a[0]; 는 OK
  • pt = (int*)a; 는 경고 유발(타입 의도 모호). 명확한 주소로 초기화 권장.

4) 포인터와 배열의 관계 (호환적 표기와 차이)

  • 호환적 표기: a[i] 와 *(a+i) 는 동치, p[i] 와 *(p+i) 도 동치.
  • 차이: “배열명”은 좌변 불가(상수 주소), “포인터 변수”는 증감 가능.
int A[5] = {1,2,3,4,5};
int *p = A;

p++;     // OK (다음 요소로)
*A;      // OK (읽기)
A++;     // 에러: 배열명은 상수 주소
  • 메모리 확보 관점
    • 배열: 정적/고정 크기를 한 번에 확보.
    • 포인터: 필요 시점에 malloc/free 등으로 유동적 확보가 가능.

5) 포인터 배열 (Array of Pointers)

개념과 장점

  • “포인터들을 요소로 가지는 배열”.
  • 서로 길이가 다른 문자열들을 담을 때 2차원 배열보다 메모리 효율↑.
#include <stdio.h>

int main(void) {
    // (2D 배열) 모든 행이 10칸씩 고정 → 짧은 문자열도 10칸 점유
    char fixed[3][10] = {"KNOU", "Computer", "123456789"};

    // (포인터 배열) 각 문자열 길이만큼만 실제 메모리 사용
    const char *dyn[3] = {"KNOU", "Computer", "123456789"};

    for (int i=0; i<3; ++i) {
        // '\0' 만나면 종료
        for (int j=0; dyn[i][j]; ++j) putchar(dyn[i][j]);
        putchar('\n');
    }
}

언제 쓰나

  • “행 길이 가변” 데이터(문자열 목록, 파일 경로 목록 등).
  • 함수로 문자열 리스트를 반환(또는 전달)할 때 인터페이스가 단순.

6) 이중 포인터(T**)

의미

  • 포인터를 가리키는 포인터”.
  • char **pp → pp는 “char* 를 저장하는 메모리”의 주소, *pp는 char*, **pp는 char.
#include <stdio.h>

int main(void) {
    char a = 'A';
    char *p = &a;     // p: &a
    char **pp = &p;   // pp: &p

    printf("**pp = %c\n", **pp);  // A
}

대표 사용처 3가지

  1. 출력 매개변수(out-param): 함수가 포인터를 “설정”해 주어야 할 때
#include <stdlib.h>
#include <stdbool.h>

// 역할: 버퍼를 할당하여 결과 포인터를 돌려준다
// 매개변수:
//   out_buf (out) : 할당된 메모리의 주소를 되돌려줄 포인터의 주소 (char**)
//   out_n   (out) : 길이
// 반환: 성공 여부
bool make_buffer(char **out_buf, size_t *out_n) {
    const char *src = "hello";
    size_t n = 6; // "hello\0"
    char *buf = (char*)malloc(n);
    if (!buf) return false;
    for (size_t i=0;i<n;i++) buf[i]=src[i];
    *out_buf = buf;   // 호출자 포인터를 '설정'
    *out_n = n;
    return true;
}
  1. argv 스타일: 프로그램 인자 목록
    • int main(int argc, char **argv) 에서 argv[i] 는 i번째 문자열(포인터).
    • 즉, “문자열들에 대한 포인터들의 배열”을 가리키는 포인터.
  2. 가변 2D 컨테이너: char **lines 처럼 “행 포인터 배열”을 동적으로 구성.

용어 메모

  • out-param: 호출자가 넘긴 포인터의 내용을 함수가 채워서 결과를 “돌려주는” 매개변수
  • heap: 동적 메모리 영역(malloc/free)

7) 실전 체크리스트(오류 방지)

  1. 문자열 리터럴 수정 금지
    • const char *p = "abc"; 로 선언해 의도 명확히. 수정 필요 시 char s[] = "abc";.
  2. 포인터 초기화 필수
    • T *p = NULL; → 유효 주소로 세팅 후 사용.
  3. 경계 검증
    • 1D: 0 <= i < n, 2D: 0 <= i < rows, 0 <= j < cols.
  4. 형 일치
    • int* 로 float 메모리 접근 금지. 필요 시 안전한 변환·복사를 설계.
  5. 이중 포인터 out-param
    • 함수 내부에서 *out = malloc(...) 했다면 호출자가 free(*out) 책임.

8) 빠른 연습 문제

  1. 다음 중 정상/권장만 고르세요.
    a) char *p = "ABC"; p[0] = 'a';
    b) const char *p = "ABC";
    c) char s[] = "ABC"; s[0] = 'a';
  2. 2차원 배열 int a[3][4]; 에서 &a[2][1] 과 동치인 포인터 산술 식을 쓰시오.
    (단, p = &a[0][0]; 로 가정)
  3. 이중 포인터를 out-param 으로 받아 버퍼를 할당하는 함수의 “호출자 코드”에서 빠지기 쉬운 실수를 적고, 올바른 호출 예를 간단히 쓰시오.

정답 예시

  1. b, c (a는 리터럴 수정 시도라 금지)
  2. p + 2*4 + 1
  3. 실수: free를 안 함, 또는 NULL 검사를 안 함
    정답 호출 예:
char *buf = NULL;
size_t n = 0;
if (make_buffer(&buf, &n)) {
    // 사용
    free(buf);
}

'C' 카테고리의 다른 글

함수와 기억 클래스  (0) 2025.11.11
선택 제어문과 반복 제어문  (0) 2025.11.11
입.출력 함수와 연산자  (0) 2025.10.22
자료형과 선행처리기  (0) 2025.10.13
C 언어의 개요  (1) 2025.08.27