처음 시작하는 C언어 03 (fin)

yeonk·2021년 8월 24일
0

C

목록 보기
3/4
post-thumbnail

배열

같은 자료형의 기억 장소를 하나의 이름으로 관리하는 것

  • 같은 자료형의 기억장소가 연속적으로 나열

  • 배열은 0부터 시작하는 인덱스 번호를 통해 관리

  • 배열 속 변수는 배열이 관리하고 있는 기억장소 중 첫번째 기억 장소의 주소 값을 가짐

  • 배열의 주소 값을 담고 있는 변수도 포인터 변수



배열 사용

  • 배열은 변수와 마찬가지로 선언과 동시에 생성
    자료형 배열이름[기억장소개수];

  • 기억장소는 배열을 선언할 때의 개수만큼 만들어지며, 각 기억장소의 크기는 자료형에 따름
    int array1[10];

  • 배열은 0부터 시작하는 인덱스 번호를 가지고 기억장소에 접근 가능
    array[0] = 10;
    array [1] = 20;

  • 만약 배열을 선언할 때 사용한 개수의 범위를 넘어서는 경우 C언어의 종류에 따라 오류가 발생할 수 있음

 int array1[2];
    
    array1[0] = 10;
    array1[1] = 20;

    printf("array1[0] : %d\n", array1[0]);
    printf("array1[1] : %d\n", array1[1]);



for문

  • 배열이 관리하는 기억장소를 처음부터 끝까지 사용할 경우 for문과 같이 사용

  • for문의 반복횟수를 제어하는 변수를 0부터 시작하게 하고 배열의 개수만큼 반복하게 한다면 처음부터 끝가지 모든 기억장소에 접근하는 코드 생성 및 사용 가능


... 생략...

int i;
    for (i = 0; i < 3; i++) {
        printf("array1[%d} : %d\n", i, array1[i]);
    }



배열의 초기화

  • 배열이 관리할 값을 미리 알고 있다면 배열 선언 시 초기화 가능

  • 기억 장소의 개수 설정은 선택 사항



함수에서의 배열

  • 배열 변수: 배열의 주소 값을 가지고 있는 변수

  • 함수를 호출할 때 배열의 주소 값을 전달하면 함수 안에서 배열 사용 가능

void test(int v1[]) {
    int i;
    for (i = 0; i < 10; i++) {
        printf("v1[%d] : %d\n", i, v1[i]);
    }
}


int main()
{
    int array1[] = { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 };
    int array2[] = { 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000 };
    
    test(array1);
    test(array2);
}



배열과 포인터

  • 배열을 담고 있는 변수는 배열의 주소값을 가지고 있기 때문에 포인터 변수와 동일

  • 배열은 []를 통해 관리할 수도 있지만 포인터 변수로도 관리가 가능

  • 배열의 주소값을 가지고 있는 포인터 변수는 배열의 첫번째 기억장소를 가르키며 다음과 같이 배열 요소에 접근 가능

  • 배열을 만들 때 함수안에서 지역 변수로 만들게 되면 함수가 끝난 후 배열이 소멸 됨. 이 문제는 배열 앞에 static을 붙여주면 프로그램이 종료될 때까지 배열이 소멸되지 않음

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

}

int* test2() {
    static int array2[3] = { 1, 2, 3 };
    return array2;
}

int main()
{
    int array1[] = { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 };

    printf("array1[0] : %d\n", array1[0]);
    printf("array1[1] : %d\n", array1[1]);
    printf("array1[2] : %d\n", array1[2]);

    printf("*array1 + 0 : %d\n", *array1);
    printf("*(array1 + 1) : %d\n", *(array1 + 1));
    printf("*(array1 + 2) : %d\n", *(array1 + 2));

    int i;
    for (i = 0; i < 10; i++) {
        printf("array1[i] : %d\n", i, array1[i]);
    }

    for (i = 0; i < 10; i++) {
        printf("*(array1+ %d) : %d\n", i, *(array1 +i));
    }

    test1(array1);

    int* array3 = test2();
    
    for (i = 0; i < 3; i++) {
        printf("*(array3 + %d) : %d\n", i, *(array3 + i));
    }
}





문자열

  • C의 리터럴 중 ""에 묶여 있는 것을 문자열이라고 함
  • C에서는 문자열을 char 타입의 배열로 관리하며 주로 포인터 변수를 사용
  • 'a', 'b', 'c', '\0'(NULL 문자열)
char a1[6] = { 'H', 'e', 'l', 'l', 'o', '\0' }; //문자열을 받아오는 경우, 기억공간 마련
    printf("a1 : %s\n", a1);

    const char *a2 = "Hello"; //문자열을 직접 적는 경우
    printf("a2 : %s\n", a2);

    char a3[11] = { '안', '녕', '하', '세', '요', '\0' };//한글 1글자는 2byte 차지, char 문자열은 1byte
    printf("a3 : %s\n", a3);
   
   //한글은 1글자에 2byte를 차지하기 때문에 배열에 담지 않고 포인터형 변수로 작성해야함

    const char* a4 = "안녕하세요";
    printf("a4 : %s\n", a4);



문자열 함수

C언어에서 대표적으로 많이 사용하는 문자열 함수: strcpy, strcat, cstrlen

  • Strcpy(문자열1, 문자열2): 문자열 1에 문자열2를 복사

  • Strcat(문자열1, 문자열2): 문자열1 + 문자열2

  • Strlen(문자열): 문자열의 바이트 수를 구한다 (한글 2byte).
    참고: C언어는 따로 글자수를 얻어오는 함수가 제공되지 않음

   char str1[12] = "Hello";
    char str2[12] = "World";
    char str3[12];
    int len;

    strcpy(str3, str1);
    printf("strcpy : %s\n", str3);

    strcat(str1, str2);
    printf("strcat : %s\n", str1);

    len = strlen(str1);
    printf("strlen : %d\n", len);

    len = strlen("안녕하세요");
    printf("strlen : %d\n", len);






구조체

배열은 같은 타입의 기억장소를 하나로 묶어서 관리하는 것.
구조체는 다양한 타입의 기억장소를 하나로 묶어서 관리하는 개념.

  • 구조체를 정의하고 구조체 안에 변수들을 선언하면 구조체를 통해 변수들을 관리할 수 있음

  • 구조체의 기억공간 주소값은 변수명에 담김

  • 구조체{
    int a;
    int b;
    int c;
    Char *d;
    int e;
    }변수명;

struct Student1{
    int kor;
    int mat;
    int eng;
    const char* name;
    int age;
}stu;

struct Student2 {
    int kor;
    int mat;
    int eng;
    const char* name;
    int age;
};


int main()
{
    stu.kor = 100;
    stu.mat = 80;
    stu.eng = 70;
    stu.name = "홍길동";
    stu.age = 30;

    printf("stu.kor : %d\n", stu.kor);
    printf("stu.mat : %d\n", stu.mat);
    printf("stu.eng : %d\n", stu.eng);
    printf("stu.name : %s\n", stu.name);
    printf("stu.age : %d\n", stu.age);

    struct Student2 s1;
    struct Student2 s2;

    s1.kor = 90;
    s1.mat = 80;
    s1.eng = 70;
    s1.name = "최길동";
    s1.age = 40;


    printf("s1.kor : %d\n", s1.kor);
    printf("s1.mat : %d\n", s1.mat);
    printf("s1.eng : %d\n", s1.eng);
    printf("s1.name : %s\n", s1.name);
    printf("s1.age : %d\n", s1.age);


    struct Student2 stu_array[3];

    stu_array[0].kor = 10;
    stu_array[1].kor = 20;
    stu_array[2].kor = 30;

    int i;
    for (i = 0; i < 3; i++) {
        printf("kor : %d\n", stu_array[i].kor);
    }
}



구조체 변수

  • 구조체를 하나만 만들어 사용할 경우: 구조체 정의 시 마지막에 변수명을 기술

  • 구조체를 정의하고 다수로 만들어 사용할 경우: 마지막에 변수명을 설명하지 않고 구조체를 필요한 만큼 만들어 사용

  • 구조체도 배열로 만들어 사용 가능

struct test1 {
    unsigned int a1;
    unsigned int a2;
};

struct test2 {
    unsigned int a1:1;
    unsigned int a2:1;
};

int main()
{
    struct test1 t1;
    struct test2 t2;


    printf("t1 : %d\n", sizeof(t1));
    printf("t2 : %d\n", sizeof(t2)); //sizeof는 bit 단위 불가, byte 단위

    t1.a1 = 1;
    t2.a1 = 1;

    printf("t1.a1 : %d\n", t1.a1);
    printf("t2.a1 : %d\n", t2.a1);

    t1.a2 = 2;  //1 0
    t2.a2 = 2;  //  0

    printf("t1.a2 : %d\n", t1.a2);
    printf("t2.a2 : %d\n", t2.a2);

}



비트 필드

  • 비트 필드는 구조체를 정의할 때 구조체의 맴버의 사이즈를 비트 단위로 조절할 수 있는 개념

  • 예를 들어 0과 1만 관리하는 변수가 있다고 하면 1bit만 있으면 관리가 가능하지만, C언어에서 가장 크기가 작은 기억공간은 char(1byte, 8bit) 이다.

  • 이에 비트 단위 크기의 기억 장소를 사용할 수 있도록 비트 필드라는 개념을 제공

  • Struct A{
    Unsigned 자료형 변수명:비트수
    }

  • Unsigned 자료형을 많이 사용하는 이유: 부호가 없어 첫 비트를 일반 값을 기억하는 용도로 사용.



Typedef

  • typedef는 새로운 타입을 정의하는 키워드

  • 주로 구조체에서 많이 사용하며 구조체를 typedef로 정의하면 향후 구조체가 필요할 때 구조체 이름만으로 구조체 선언이 가능

struct test1 {
    int a1;
    int a2;
};

typedef struct  test2{

    int a1;
    int a2;
} test2;

int main()
{
    struct test1 t1;
    t1.a1 = 100;
    t1.a2 = 200;

    printf("t1.a1 : %d\n", t1.a1);
    printf("t1.a2 : %d\n", t1.a2);

    test2 t2;
    t2.a1 = 300;
    t2.a2 = 400;

    printf("t2.a1 : %d\n", t2.a1);
    printf("t2.a2 : %d\n", t2.a2);
}





공용체

공용체는 구조체와 문법은 비슷하지만 개념이 다름.
공용체는 같은 기억장소를 여러 변수들이 공유하는 개념
(구조체는 여러 기억 장소를 하나의 이름으로 관리하는 개념)

  • 실제 개발할 때 많이 쓰여지지는 않지만 필요한 경우가 있을 수 있음

  • union A{
    int a1;
    short a2;
    char a3;
    }

struct Test1 {
    int a1;
    short a2;
    char a3;
};

union Test2 {
    int a1;
    short a2;
    char a3;
};

int main()
{
    struct Test1 t1;
    t1.a1 = 65535;

    printf("t1.a1 : %d\n", t1.a1);

    union Test2 t2;
    t2.a1 = 65535;

    printf("t2.a1 : %d\n", t2.a1);
    printf("t2.a2 : %d\n", t2.a2); // cf. 전체 비트가 모두 1이면 -1로 출력함
    printf("t2.a3 : %d\n", t2.a3);
}





표준 입출력

표준 입출력: 키보드 입력 + 콘솔 출력

  • C언어에서는 키보드 입력 및 콘솔 출력을 위한 함수를 제공

  • Getchar: 문자 하나를 입력 받음

  • Putchar: 문자 하나를 출력

  • Gets: 문자열을 입력 받음

  • Puts: 문자열을 출력

  • Scanf: 다양한 타입의 값을 입력받음

  • Printf: 다양한 타입의 값을 출력

   printf("문자입력 :");
    char a1 = getchar();
    printf("a1 : %c\n", a1);
    putchar(a1);
printf("문자열 입력 :");
    char a2[100];
    gets(a2);
    printf("a2 : %s\n", a2);
    puts(a2);



포멧 문자

Scanf와 printf 에서 값을 출력할 때 사용하는 문자

  • %s: 문자열
  • %c: 문자 1개
  • %d: 부호 있는 정수
  • %u: 부호 없는 정수(0~)
  • %f: 실수
  • %0.3: 실수 (소수점 3자리까지)
char a1[100];
    int a2;
    
    /*
    printf("문자열 입력 :");
    scanf("%s", a1); // 배열값이라 따로 &를 붙이지 않음
    printf("숫자 입력 :");
    scanf("%d", &a2); //정수형 변수이기 때문에 & 붙임
    */

    printf("값 입력:");
    scanf("%s, %d", a1, &a2);
    printf("a1 : %s, a2 : %d\n", a1, a2);





파일 입출력

  • C 언어는 파일에 데이터를 저장하고 읽어올 수 있는 함수를 제공
  • fopen("파일명", 파일모드): 파일을 찾아 파일을 연다.
  • fopen 함수로 파일을 열어주고 읽고 쓰기 한 다음 반드시 fclose로 닫아줘야 함
 FILE* fp;

    /*
    fp = fopen("data1.txt", "w+");
    fprintf(fp, "%s", "abcd");
    fclose(fp);
    */

    fp = fopen("data1.txt", "w+");
    fprintf(fp, "%s", "cdef");
    fclose(fp);

    fp = fopen("data1.txt", "a+");
    fprintf(fp, "%s", "kkkk");
    fclose(fp);



파일모드: r, w, a, r+, w+, a+

  • r: 읽기모드. 파일이 없으면 오류
  • w: 쓰기 모드. 파일이 없으면 생성, 파일이 있으면 삭제하고 다시 생성
  • a: 추가 모드. 파일이 없으면 생성, 파일이 있으면 끝에 내용을 추가
  • r+: 읽고 쓰기 모드. 파일이 있으면 내용을 덮어 씌우고 파일이 없으면 생성
  • w+: 읽고 쓰기 모드. 파일이 있으면 파일을 삭제하고 다시 생성, 파일이 없으면 생성
  • a+: 읽고 쓰기 모드. 파일이 있으면 끝에 내용을 추가하고 파일이 없으면 생성



파일 데이터를 쓰고 읽는 함수

  • fputc: 글자 하나를 파일에 씀
  • fgetc: 글자 하나를 파일에서 읽어옴
  • fputs: 문자열을 파일에 씀
  • fgets: 문자열을 파일에서 읽어옴
  • fprintf: 다양한 타입의 데이터를 파일에 씀
  • fscanf: 다양한 타입의 데이터를 파일에서 읽어옴
  • fclose: 열려진 파일을 닫음
FILE* fp;

    fp = fopen("data2.txt", "w+");
    fprintf(fp, "%s", "abcd kkk"); 
    fclose(fp);

    fp = fopen("data2.txt", "r");
    char a1[255];
    //fscanf(fp, "%s", a1); 문자열에 띄어쓰기가 있는 경우 %[^\n] 함께 기재하여 엔터까지 불러오는 것을 표기
    fscanf(fp, "%[^\n]", a1);

    printf("%s\n", a1);
    fclose(fp);





전처리 명령어

  • 개발자가 작업한 .c 파일은 컴파일 과정 후 컴퓨터가 인식할 수 있는 코드 생성
  • C언어의 경우 전처리기가 전처리 명령어를 분석해 코드를 완성하고 컴파일러에게 전달함
  • 전처리 명령어: 컴파일 전에 개발자가 만드는 코드를 완성
  • 불필요한 코드를 목적 파일에 추가하지 않으므로 수행 속도에 도움
  • #define

    • 코드에서 사용하는 값을 코드에 직접 작성하지 않고 상수로 만드는 전처리 명령어.
    • 개발자는 정의된 상수를 사용하지만 전처리기에 의해 실제 값으로 변경됨
    • 프로그램 실행 중에 변경되지 않는 값을 정의할 때 사용
  • #undef: #define 으로 정의된 상수를 제거

  • #include: 다른 파일에서 작성한 코드를 불러와 사용할 때 사용

  • #ifdef ~ #endif: #define을 통해 정의된 상수가 있다면 내부 코드가 추가되고 그렇지 않으면 제거

  • #ifndef ~ #endif: 지정된 상수가 #define을 통해 정의되어 있지 않다면 내부의 코드가 추가되고 정의되어 있다면 제거

  • #if, #else, #elif: if 문처럼 조건을 통해 코드를 완성 가능

#define DATA1 100
#define DATA2 200


int main()
{
	printf("DATA1 :%d\n", DATA1); //여기서 DATA1은 변수가 아니다
	printf("DATA2 :%d\n", DATA2);

	int a1 = DATA1 + DATA2;
	//	int a1 = 100 + 200;
		printf("DATA1 + DATA2 : %d\n", a1);
#undef DATA1

		//printf("DATA1 : %d\n", DATA1);
}
#define DATA1

int main()
{
	#ifdef DATA1
	printf("DATA1이 정의되어 있습니다.\n");
	#endif	

	#ifdef DATA2
	printf("DATA2이 정의되어 있습니다.\n");
	#endif

#ifndef DATA1
	printf("DATA1이 정의되어 있지않습니다.\n");
#endif	

#ifndef DATA2
	printf("DATA2이 정의되어 있지않습니다.\n");
#endif
}
#define DATA1 100

int main()
{
#if DATA1 < 50
	printf("DATA1은 50보다 작습니다.\n");
#elif DATA1 >= 50 && DATA1 <=100
	printf("DATA1은 50이상이고 100이하 입니다.\n");
#else
	printf("DATA1은 100보다 더 큽니다.\n");
#endif
}



미리 정의되어 있는 상수

C언어에서는 #define을 통해 밀 정의되어 있는 상수들이 있음

  • DATE: 현재 날짜
  • TIME: 현재 시간
  • FILE: 실행된 파일의 이름
  • LINE: 현재 라인 번호
  • STDC: 일반적인 C컴파일로 컴파일 되었을 경우 정의 되어 있는 상수
printf("__DATE__ : % d\n", __DATE__);
	printf("__TIME__ : % d\n", __TIME__);
	printf("__FILE__ : % d\n", __FILE__);
	printf("__LINE__ : % d\n", __LINE__);
	printf("__STDC__ : % d\n", __STDC__);



매크로 함수

  • #define을 통해 함수를 정의할 수 있는데 이를 매크로 함수라고 함

  • 매크로 함수는 전처리기에 의해 실행되고 매크로 함수를 사용하는 곳에 정의된 코드가 사용됨

  • #define은 한 줄로 정의해야 함 (줄바꿈 하고 싶다면 \ 활용)

#define FN1() printf("FN1 호출\n");
#define FN2(a, b) printf("FN2 호출 : %d, %d\n", a, b);
#define FN3(a, b) printf("FN3 호출 : "#a", "#b" \n");
#define FN4(a, b) \
printf("FN4 호출 : \n"); \
printf("a : "#a" \n"); \
printf("b : "#b" \n");

int main()
{
    FN1(); //printf("FN1호출\n);
    FN2(10, 20); //printf("FN2 호출 : %d, %d\n", 10, 20);
    FN3(10, 20); //printf("FN3 호출 : 10, 20\n");
    FN4(10, 20);
}





헤더파일

  • 개발을 하다 보면 상수, 변수, 배열, 함수, 구조체 등을 많이 만들어 사용하는데 한번 만든 것들을 여러 파일에서 사용 가능함

  • 코드를 복사해서 넣으면 수정 시 문제가 발생할 수 있음

  • 헤더 파일: 자주 사용하는 여러 코드들을 한 번만 만들어 사용할 수 있도록 제공

  • 헤더 파일에는 변수, 상수, 배열, 함수, 구조체 등을 선언하고 이를 가져다 사용할 수 있도록 함



헤더 파일의 사용

  • #include<파일명>: C언어 자체에서 제공하고 있는 헤더파일 사용

  • #include"파일이름": 개발자가 만든 파일 이름 사용

  • 헤더파일에는 함수를 선언하고, c소스 파일에서는 코드 구현

#include <iostream>
#include "test.h"

int main()
{
    printf("data1 : %d\n", data1);
    int i;
    for (i = 0; i < 5 ; i++) {
        printf("data2 : %d\n", data2[i]);
    }

    Struct1 st1;
    st1.value1 = 1000;
    st1.value2 = 2000;

    printf("st1.value1 : %d\n", st1.value1);
    printf("st1.value2 : %d\n", st1.value2);

    printf("DATA3 : %d\n", DATA3);
    printf("DATA4 : %d\n", DATA4);

    function1();
}



Auto, extern, register, static

  • Auto

    • 변수의 사용 시점이 끝나면 자동으로 소멸되는 변수를 선언할 때 사용.
    • 모든 지역변수가 여기에 해당.
    • 생략 가능
  • Extern

    • 프로그램이 끝날 때까지 사용되는 변수에는 extern을 붙히며 모든 전역 변수가 해당
    • 생략 가능
  • Register

    • 변수의 접근이 빨라야 하는 경우 사용
    • 변수 선언시 register 키워드를 붙이면 됨
  • Static

    • 변수가 선언된 함수가 끝나더라도 계속 유지가 되는 변수
#include "test1.h"
#include <iostream>

extern int global_a; //다른 곳에서 선언한 전역변수를 가져올 때, 값을 초기화할 수는 없음
extern int global_b;

int main()
{
    int local_a1 = 100;
    int local_a2 = 200;

    printf("local_a1 : %d\n", local_a1);
    printf("local_a2 : %d\n", local_a2);

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

    test2();
    test2();
    test2();

    test3();

    printf("global_a : %df\n", global_a);
    printf("global_b : %df\n", global_b);
}





메모리 관리

정적할당과 동적할당

  • 할당: 개발자가 어떤 값들을 관리하기 위해 기억공간을 마련하는 것

  • 정적할당, 동적할당



정적할당

개발자가 사용하고자 하는 기억공간의 크기가 이미 정해져 있으며 stack 이라는 공간에 만들어짐

  • 우리가 지금까지 사용했던 방식이 모두 정적할당에 해당

  • 자료형이나 구조체 등을 이용하여 변수를 선언하면 기억공간이 마련되고 기억공간을 이름이나 포인터 변수를 통해 접근할 수 있음

  • 정적할당된 기억공간의 사용 범위가 종료되면 자동으로 소멸

typedef struct Struct1 {
    int value1;
    int value2;
} Struct1;

int main()
{
    int data1 = 100;
    int data2[] = {1, 2, 3};


    printf("data1 : %d byte, %d\n", sizeof(data1), data1);

   printf("data2 : %d byte\n", sizeof(data2));

    Struct1 stu1;
    printf("stu1 : %d\n", sizeof(stu1));

}



동적할당

개발자가 원하는 만큼의 용량을 설정하여 기억공간을 할당하는 것.
Heap 영역에 만들어짐
(Cf. Stack vs heap: 메모리 할당량 S<H)

  • 동적 할당의 경우 오로지 포인터 변수를 통해서만 접근이 가능

  • 동적할당의 경우 자동으로 소멸되지 않으므로 개발자가 직접 소멸시켜야 함

  • Calloc(개수, 크기): 크기 만큼의 기억공간을 개수만큼 만듦.
    만들어진 기억공간은 모두 0으로 초기화가 됨

  • Malloc(크기): 크기 만큼의 기억공간이 만들어지지만 초기화 되지 않음

  • Realloc: 기억공간의 크기를 다시 설정

  • Free: 동적할당된 기억공간을 소멸

typedef struct Struct1 {
	int a1;
	int a2;
} Struct1;


int main()
{
   int * pt = (int*)malloc (4); //안정성을 높이기 위해 (int*)를 붙여준 것과 같이 형식을 맞춰 주는 것이 좋다.
   *pt = 100;
   printf("*pt : %d\n", *pt);

   int i;
   int* pt2 = (int*)malloc(4 * 3); //4바이트 공간을 3개 만든다
	   for (i = 0; i < 3; i++){
	   pt2[i] = 100 * (i + 1);
   }

   for (i = 0; i < 3; i++) {
	   printf("pt2 : %d\n", pt2[i]);
   }

   for (i = 0; i < 3; i++) {
	   *(pt + i) = 1000 * (i + 1);
   }
   for (i = 0; i < 3; i++) {
	   printf("pt2 : %d\n", *(pt2 + i));
   }

   Struct1* pt3 = (Struct1*)malloc(sizeof(Struct1));
   (*pt3).a1 = 100;
   (*pt3).a2 = 200;

   printf("a1 : %d\n", (*pt3).a1);
   printf("a2 : %d\n", (*pt3).a2);

   free(pt);
   free(pt2);
   free(pt3);
}





reference

소프트캠퍼스, 처음 시작하는 C언어, 구름EDU, URL, 2021년 8월 23일 수강

0개의 댓글