14장 표준입력함수,15장 배열과포인터, 16장 메모리할당

이성준·2023년 2월 28일
3

C 자료구조

목록 보기
7/12
post-thumbnail

14. 표준입력함수

바로 시작하겠다.
시스템이 가장 기본으로 생각하는 장치를 우리는 표준입력 장치라고 부른다.
C언어는 이런 표준입력장치로부터 데이터를 입력받는 표준입력함수를 제공하는데, 이때 C언어의 표준입력함수는 Enter를 기준으로 데이터를 입력 받는다.
예를 들어, Terminal에 ABC 를 입력한후 Enter를 누르면 ABC 까지 입력이 된것이다. 우리는 이 구조를 그림으로 살펴볼 필요가 있다.

여기서 주의해야할게 만약 ABC를 입력하고 Enter를 누르게 되면 입력버퍼에 ABC가 올라가게 된다. 하지만 뒤에서 배울 값을 하나만 입력 받는 함수의 경우에는 입력버퍼에서 A만을 갖고 온다. 그러면 입력버퍼에는 아직 값이 남아있기 때문에 나중에 출력할때 문제가 된다. 따라서 초기화 해주는 함수를 알고 있어야 하는데 다음과 같다
rewind(stdin)이 함수를 사용하게 되면 입력버퍼가 초기화 된다. 실제 문자열 입력함수를 보고 예시를 들어보자.

  • 표준입력함수
    getchar() → 문자열 하나를 입력받는 함수로써 C언어 표준라이브러리에 저장돼있다.
    getchar()의 함수원형을 살펴보면 int형을 반환하도록 돼있는데 실제로 우리가 문자 한개를 받을때는 보통 char형을 사용하기 때문에 char형 변수로 getchar()의 return값을 받아도 괜찮다.

뒤에 gets(), atoi(), scanf()에 대한 설명이 남았지만 위에서 버퍼의 개념을 먼저 설명하기 위해 잠시 getchar()만을 사용해서 예를 들어보겠다.

void main(void){
    printf("문자를 입력해주세요: ");
    int input_data1 = getchar();
    printf("문자를 입력해주세요: ");
    int input_data2 = getchar();
    printf("문자를 입력해주세요: ");
    int input_data3 = getchar();

    printf("입력데이터 %c\n",input_data1);
    printf("입력데이터 %c\n",input_data2);
    printf("입력데이터 %c\n",input_data3);

}

내가 input_data1의 입력을 할때 ABC를 입력하고 Enter를 쳤다고 해보자 그러면 입력버퍼에는 A,B,C,Enter가 올라가 있다. 따라서 다음과 같은 결과가 나온다.

ABC를 입력한후 추후 입력하지 않았는데 input_data2와 input_data3에 있는 getchar()에서 입력버퍼에 있는 값을 알아서 꺼내와서 출력했음을 볼 수 있다. 따라서 위와 같은 오류가 생기지 않게 하기 위해선 위에서 배운 rewind(stdin)을 사용해서 함수를 구성하면 된다.

void main(void){
    printf("문자를 입력해주세요: ");
    int input_data1 = getchar();
    rewind(stdin);
    printf("문자를 입력해주세요: ");
    int input_data2 = getchar();
    rewind(stdin);
    printf("문자를 입력해주세요: ");
    int input_data3 = getchar();

    printf("입력데이터 %c\n",input_data1);
    printf("입력데이터 %c\n",input_data2);
    printf("입력데이터 %c\n",input_data3);

}


이때 주의해야할 점은 Enter도 입력버퍼에 올라간다는 점이다. 다음 코드를 보자

void main(void){
    printf("문자를 입력해주세요: ");
    int input_data1 = getchar();
    printf("문자를 입력해주세요: ");
    int input_data2 = getchar();

    printf("입력데이터 %c\n",input_data1);
    printf("입력데이터 %c\n",input_data2);
}

getchar()에서 버퍼에서 문자를 가져올때 Enter를 가져왔기 때문에 위와 같은 오류가 발생한다.
따라서 rewind(stdin)으로 초기화 해주거나 getchar()을 그냥 사용해서 Enter를 없애줘야 한다. 다음과 같이 해결하면 된다.

void main(void){
    printf("문자를 입력해주세요: ");
    int input_data1 = getchar();
    getchar();
    printf("문자를 입력해주세요: ");
    int input_data2 = getchar();

    printf("입력데이터 %c\n",input_data1);
    printf("입력데이터 %c\n",input_data2);
}

gets()gets() 는 예상할 수 있듯이 string(문자열)을 받게된다. 이때 문자열을 받는것이기 때문에 배열을 설정해줘야 하는데, getchar() 와 달리 char형으로 배열을 만들어줘야하며 인수로 배열의 시작주소를 넘겨줘야한다. 또한 getchar()와는 달리 gets()Enter까지 입력버퍼에서 읽어와서 입력버퍼에 Enter가 남는일이 생기지 않는다. 따라서 rewind로 초기화를 해줄 필요가 없다. Enter키 값을 읽어와서 문자열의 일부로 포함시키는 것이 아니라 문자열의 끝이라고 인식하기 때문에 실제 문자열에서는 Enter를 입력한 위치에 NULL을 의미하는 0이 들어오게 된다.(파란색이 궁금하면 책의 앞에서 ASCII CODE를 확인해보길 바란다.)\

void main(void){
    char input_data1[10]; //9개의 글자까지 수용가능 한개는 Enter인 0이 들어감
    printf("문자열을 입력해주세요: "); 
    gets(&input_data1); //배열의 주소 = 배열의 시작주소 = 배열의 첫번째 원소의 시작주소(&input_data1[0])
    printf("문자열을 입력해주세요: ");
    char input_data2[10];
    gets(&input_data2);

    printf("입력데이터 %s\n",input_data1);
    printf("입력데이터 %s\n",input_data2);
    printf("NULL확인 %d\n",input_data1[9]); //NULL의 존재를 확인하기 위해서
}

gets()는 입력이 정상적으로 이뤄지면 배열의 시작주소를 반환하고 비정상적으로 이뤄지면 '해당하는 메모리 주소가 없음'을 의미하는 NULL을 반환한다.

  • window cmd

    우리가 프로그램을 실행하면 생기는 cmd창에서 프로그램 실행을 취소하고 싶으면 ctrl+c를 누르면 실행이 취소된다.

atoi()atoi()는 ArrayToInteger 함수로써 이는 stdlib.h 파일에 함수 원형이 정의 돼있다.
위에서 gets()와 getc()를 이용해서 숫자를 입력받을경우 문자열로 인식해서 받아들이는데 따라서 이 문자열을 숫자로 바꿔주는 함수이다. 다음 예를 보자.

void main(void){
    char input_data1[4]; //9개의 글자까지 수용가능 한개는 Enter인 0이 들어감
    printf("세자리 숫자를 입력해주세요: "); 
    gets(&input_data1);
    printf("세자리 숫자를 입력해주세요: ");
    char input_data2[4];
    gets(&input_data2);

    printf("%d",input_data1+input_data2);
}

→ 문자열을 덧셈을 하고자 하니깐 오류가 발생한 것이다.

atoi()를 이용한 수정코드

void main(void){
    char input_data1[4]; //9개의 글자까지 수용가능 한개는 Enter인 0이 들어감
    printf("세자리 숫자를 입력해주세요: "); 
    gets(&input_data1);
    printf("세자리 숫자를 입력해주세요: ");
    char input_data2[4];
    gets(&input_data2);

    int first,second;
    first = atoi(input_data1);
    second = atoi(input_data2);
    printf("%d",first+second);
}

Scanf()

사실 여기까지의 설명은 필요하긴 하겠지만, 각각이 닌텐도 정품칩이라고 하면 scanf()는 R4칩 정도이다. 매우 중요하므로 집중하길 바란다.
scanf()는 다음과 같이 사용한다.

scanf(형식지정자,저장할주소)

printf에 형식지정자를 사용한것처럼 형식지정자를 사용해서 입력을 받는 것이다.

  • 형식지정자
    %dint형 ; 4byte
    %hdshort int형 ; 2byte
    %ffloat형 ; 4byte
    %lfdouble형 ; 8byte
    %c문자 ; 1byte
    %s문자열 ; array의 byte수

위의 scanf에 저장할주소를 입력하는 것을 보고 무언가 떠오르는 것 없나?
scanf()는 포인터를 사용하기 때문에 저장할 주소를 입력해주어야 한다. scanf() 의 동작을 그림으로 알아보자면 다음과 같다

우리가 입력해준 형식지정자를 확인하고 포인터의 모양을 결정해준 다음에 지정한 주소에 형식 지정자의 크기(byte)만큼 저장하는 것이다.
따라서 입력한 값과 형식지정자가 같아야 한다.
다를경우>

void main(void){
    int data;
    printf("값을 입력하세요:");
    scanf("%d",&data);
    printf("%d",data);
}


위와 같이 필요한 정보만을 골라서 받게 된다.
하지만 이때 AB는 그대로 입력버퍼에 남아있기때문에 다음 scanf()에서 문제를 일으킨다

void main(void){
    int data1,data2,data3;
    printf("값을 입력하세요:");
    scanf("%d",&data1);
    printf("%d",data1);
    scanf("%d",&data3);
    printf("%d",data3);
    scanf("%d",&data2);
    printf("%d",data2);
}

따라서 입력버퍼를 rewind(stdin)으로 비워줘야 한다. scanf를 잘못입력 할 경우를 대비한 코드는 0을 반환하는 성질을 이용해 다음과 같이 짤 수 있다.(scanf함수는 실행에 실패할경우 0을 반환하고 성공할경우 1을 반환한다.)

void main(void){
    int data1;
    printf("정수형 자료를 입력하십시오:");
    if(scanf("%d",&data1)==0){
        printf("잘못입력하셨습니다");
    }
    else{
        printf("%d",data1);
    }
}


유사한 코드로 다음과 같이 짤 수 도 있겠다.

void main(void){
    int data1,num;
    printf("정수형 자료를 입력하십시오:");
    num = scanf("%d",&data1);
    if(num==0){
        printf("잘못입력하셨습니다\n");
    }
    else{
        printf("%d",data1);
    }
}

  • scanf()의 다중 입력
    scanf는 한꺼번에 여러 값을 입력 받을 수 있는데 이때 입력값의 구분은 Enter혹은 공백으로 가능하다. 다음과 같이 입력을 받는다.
void main(void){
    int data1,data2,data3;
    printf("값을 입력하세요:");
    scanf("%d%d%d",&data1,&data2,&data3);
    printf("%d",data1);
    printf("%d",data2);
    printf("%d",data3);
}


or

따라서 내가 I go shopping이라고 입력을 하게 되는 순간 3개의 문자열을 입력하게 된 것이다.

void main(void){
    char data1[2],data2[3],data3[9];
    printf("문자열을 입력하세요:");
    scanf("%s%s%s",&data1,&data2,&data3);
    printf("%s",data1);
    printf("%s",data2);
    printf("%s",data3);
}

이경우 어떤 문장을 입력할지 모르기 떄문에 문자열을 입력받을때는 gets()를 입력받는 편이 현명하다
→ gets는 띄어쓰기도 같이 처리해주기 때문이다.

참고

void main(void){
    char data1[2],data2[3],data3[9];
    printf("문자열을 입력하세요:");
    scanf("%s%s%s",&data1,&data2,&data3);
    printf("%s",data1);
    printf("%s",data2);
    printf("%s",data3);
    printf("%d%d%d",data1[1],data2[2],data3[8]);
}


마지막 줄에 배열의 마지막을 출력해본결과 i와 문자열이 끝났다는 NULL값인 0, go와 문자열이 끝났다는 NULL값인 0, shopping과 0값이 같이 배열에 입력됐음을 볼 수 있다. 이는 gets와 마찬가지로 무자열이 입력될때 0값이 같이 입력됐음을 볼 수 있다.

void main(void){
    char data1[100],data2[100],data3[100];
    printf("문자열을 입력하세요:");
    scanf("%s%s%s",&data1,&data2,&data3);
    printf("%s",data1);
    printf("%s",data2);
    printf("%s",data3);
}


배열의 크기를 모르겠으면 아예 크게 100으로 잡아놓으면 문자열을 받았을때 오류는 발생하지 않을 것이지만 메모리의 손실이 너무 커질 것이다.

Quiz> 367page 성적처리 프로그램 만들기를 해보길 바란다.
Hint> 평균을 기준으로 등수처리/ 버블정렬 알고리즘 사용.

15. 배열과 포인터

배열과 포인터는 비슷하게 사용이 가능하다.
먼저 배열과 포인터가 비슷하게 사용이 가능한 이유를 말하자면 int data[5]라는 배열에서 data가 의미하는 바가 이 배열의 시작주소 이기 때문이다.
다음 코드를 보자.

void main(void){
    int data[5];
    int num = (data == &data); //data가 의미하는 바가 data의 시작주소가 맞아?
    printf("%d",num);
}


→ True
int data[5]는 4바이트 짜리 직사각형이 5개 붙어있는 아래와 같은 그림을 떠올리면 된다.

이때 data[1] = 5*(data+1) = 5는 같은 동작을 행한다. 왜냐면 data는 배열의 시작주소 즉 data[0]의 시작주소와 같은데 이때 1을 더하면 1칸을 건너뛰어서 data[1]의 시작주소를 의미하기 때문이다. 이때 이 주소에 번지지정 연산자 *을 이용했기 때문에 위에 말한 둘이 같은ㄴ 동작을 행하는 것이다.
다음 예시를 보자.

void main(void){
    int data[5];
    data[1] = 4;
    printf("%d\n",data[1]);
    *(data+1) = 6;
    printf("%d\n",data[1]); //값입 변했음을 확인 할 수 있다.
    
}

위의 내용은 포인터에서 언급했다

마찬가지의 이유로 포인터를 선언하고 배열의 형태로 값을 집어넣는 것 또한 가능하다.
(p가 의미하는 것이 data의 시작주소 하지만 배열 data가 의미하는것 또한 배열의 시작주소)

void main(void){
    int data[5];
    int*p = &data;
    p[1] = 3; //p가 의미하는 것이 data의 시작주소 하지만 배열 data가 의미하는것 또한 배열의 시작주소
    printf("%d\n",data[1]);
    *(p+1) = 6;
    printf("%d\n",data[1]); //값입 변했음을 확인 할 수 있다. 
}

충격적인 사실>

void main(void){
    int data;
    data = 3;
    printf("%d\n",data);
    *(&data) = 6;
    printf("%d\n",data); //값입 변했음을 확인 할 수 있다.
}


배열이 아닌 일반 변수에 대해서도 번지지정 연산자가 작동함을 볼 수 있다
→ 결국 중요한건 '주소'이다

배열과 포인터의 결합

우리는 여태까지 배열과 포인터를 따로 따로 사용했다 하지만 한번에 사용할 수도 있는데, 한번에 사용할 필요성에 대해서 생각을 해보자면 배열이 나오게된 계기에 대해서 생각을 해보면 쉽다.
배열이 나오게된 계기는 score에 대해서 국어 영어 수학 과학,... 을 변수 여러개로 저장하지 않고 배열이라는 하나의 변수에 저장해놓고 관리하고 싶어서 나오게 됐다.
포인터라고 그런점이 없을까? 라고 생각해 보면 된다.
가령

char*p1;
char*p2;
char*p3;
char*p4;
char*p5;
char*p6;
...

위와 같이 선언해두고 사용한다면 코드가 복잡해지고 프로그래밍을 하는 개발자 또한 헷갈릴 것이다. 이러한 동기에 의해 나온 것이 배열과 포인터의 결합인데 다음과 같이 결합할 수 있다.

char data1,data2,data3,data4,data5;
char*p[5] = {&data1,&data2,&data3,&data4,&data5}

위의 경우를 그림으로 표현하면 다음과 같다.

위와 같이 여러개의 포인터를 배열을 이용해서 선언할 수 있는 반면에 char*p[5]에서 ()를 추가하면 전혀 다른 문법이 된다.
바로 char(*p)[5]인데 이를 해석해보면 다음과 같다
(*p) → 나는 포인터 인데
char[5] → 5바이트배열을 가리켜

포인터를 공부할때 앞에 있는 int, char 등이 포인터가 가리키는 크기라고 배웠다. 이제 배열 또한 가리킬 수 있게 된 것이다.
그림으로 확인하자면 다음과 같다.

이때

void main(void){
    char data[5];
    char(*p)[5]=data;//p 는 data배열의 시작주소를 가리킴
    int num = ((*p)[2] == data[2]);
    printf("%d",num);
}


(*p)[2]data[2]는 같음을 볼 수 있는데 이는 당연한 것인게 *p가 가리키는게 data이기 때문이다.
따라서 (*p)[2]=7을 하게되면 아래의 그림과 같아진다

하지만 이렇게 사용하기에는 배열을 가리킬 수 있다는 장점이 퇴색되는 것 같다.
따라서 다음과 같이 char(*p)[5] 배열을 가리킬 수 있는 포인터는 2차원 배열을 사용하는데 효과적일 것이다.

char data[5][3];
char(*p)[3]=&data;

위의 상황이라고 해보자
Quiz>
(*p)[1] = 3일경우 어디에 3을 넣었는지 아래에 그림에서 찾아봐라
(*p+1)[2] = 3일경우 어디에 3을 넣었는지 아래에 그림에서 찾아봐라
(*p+3)[0] = 3일경우 어디에 3을 넣었는지 아래에 그림에서 찾아봐라

정답:

위의 빈 원을 가르켰다

16장 메모리 할당

우리는 작성한 C소스파일을 Compile작업을 통해 .obj 파일로 만든후 이들을 연결하는 link 작업을 거쳐서 .exe의 실행파일을 만듬을 알 고 있다. 이렇게 만든 실행파일은 컴퓨터의 CPU가 직접 실행할 수 없고 운영체제가 실행파일의 명령들을 읽어서 재구성하게 되는데 이것을 프로세스(process)라고 한다. 프로세스가 구성되면 CPU는 프로세스에 있는 명령들을 실행 할 수 있다.
프로세스는 실행파일에 있는 명령 뿐만 아니라 데이터를 저장할 메모리 공간도 포함하고 있는데, 프로세스는 다음과 같은 구성을 갖는다.

이때 빨간색 부분이 이번 장에서 살펴볼 부분이다.

코드 세그먼트 : 컴파일러는 C언어 소스를 기계어로 된 명령문으로 번역해서 실행파일을 만든다. 이 실행파일들에 있는 기계어명령문을 담는 것이 프로세스의 코드 segment(부분) 이다.

데이터 세그먼트 : 데이터 세그먼트는 문자열상수나, 전역변수, static 변수등 프로그램이 시작해서 끝날 때까지 변하지 않는 데이터를 여기 데이터 세그먼트에서 보관한다

스택 세그먼트 : 스택 세그먼트는 C언어는 함수의 연결체 이기 때문에 지역변수는 함수가 호출될때만 생긴후 함수가 종료(return)되면 메모리 영역에서 지워진다. 따라서 이런 임시 데이터를 저장하는데 사용하는 메모리 영역이다.

  • 정적 메모리 할당
    정적 메모리 할당은 컴파일러가 C언어를 기계어로 번역할때 전역변수와 지역변수를 저장할 메모리를 배정한다. 따라서 번역할때 배정하는 것이기 때문에 프로그램 실행중에는 변수의 크기나 개수를 바꿀 수 없고 바꾸기 위해서는 컴파일을 다시 해야한다.

→ 이후 책에서는 Stack이라는 개념과 Base Pointer Stack Pointer를 사용해서 지역변수가 할당되고 사라지는 과정을 쭉 설명해준다. 읽으면 잘 읽히지만 자료구조의 내용이기 때문에 현재 C언어를 remind 하는 시리즈 보단 추후에 자료구조를 업데이트 할때 올리겠다.

이제 우리는 실질적으로 프로그래밍 해주어야하는 동적 메모리 할당에 대해서 논의해보자
책의 앞에서는 정적 메모리 할당을 설명하면서 지역변수가 할당되는 과정에 관해서 쭉 설명했다. 하지만 위의 그림에서 하나 설명하지 않은 것이 있다. 바로 Heap 영역이다.
프로세스는 지역변수를 저장해놓는 기본스택의 메모리영역으로는 큰 메모리를 감당할 수가 없어서 Heap이라는 영역을 제공하는데, Heap영역에 프로그래머들이 동적으로(즉, 원하는 시점에 원하는 크기로 메모리를 할당하고 원하는 시점에 메모리를 해제하는) 메모리를 할당할 수 있게 했다.

  • malloc() 함수로 동적 메모리 할당하기
    힙에 동적으로 메모리를 할당하기 위해서는 malloc()함수를 사용해야한다.
    이때 malloc()와 밑에서 배울 free()의 함수 원형이 <malloc.h>에 저장돼있으므로 include 해줘야한다

    malloc()사용법: ()안에 메모리 size를 넣으면 heap영역에 메모리가 할당되는데 이 메모리 주소를 void*형으로 반환해준다 따라서 casting을 이용해서 사용 할 수 있다.

    예시를 보자 다음과 같이 사용할 수 있다.
    만약 100바이트를 할당하고 이를 2바이트씩 쪼개서 사용하고 싶은경우.

    void main(void){
       short*data = (short*)malloc(100);
    }

    위와 같이 선언할 경우 다음 그림처럼 메모리가 할당되고 사용할 수 있다

    100바이트의 메모리를 2바이트씩 끊어서 사용함을 볼 수 있다.

  • free()함수로 할당된 메모리 해제하기
    스택에 할당한 지역 변수는 함수 호출이 끝나면 자동으로 메모리가 해제가 되는 반면에 힙에 할당한 메모리는 프로그램이 끝날때까지 자동으로 해제가 되지 않는다. 따라서 너무 많은 메모리를 힙영역에 할당할 경우 힙에도 메모리가 부족해 질수도 있으므로 free()를 사용해서 메모리를 해제해주는 과정이 필요하다. 위의 코드를 이어서 사용하면 다음과 같이 해제할 수 있다.

void main(void){
    short*data = (short*)malloc(100);
    free(data);
}

여기서 주의해야 하는점은 위에 코드에서 설정한 동적할당된 메모리의 주소는 data라는 포인터가 갖고 있다.이때 포인터는 지역변수 이기 때문에 만약 할당을 해제하지 않고 다른 함수로 넘어간다면(지역변수 메모리 영역이 초기화 된다면) 100바이트 가 할당된 메모리 영역을 찾을 방법이 없어서 해당 메모리를 사용할수도 없고 해제할수도 없다.
→ 이런 상태를 메모리가 손실 됐다고 말한다.

이렇게 동적메모리 할당을 한 이후에는 프로그래머에 판단하에 메모리를 해제를 해주어야 한다. 따라서 코드가 복잡해지는 단점이 있다.
하지만 정적할당(즉, 지역변수 선언[ex 배열]) 을 하게되면 자동으로 메모리 해제가 일어나 코드가 단순해지는 장점이 있다.

하지만 동적메모리 할당의 큰 장점중 하나가 배열은 변수를 배열의 크기로 설정할 수 없는 반면에 malloc은 변수를 배열의 크기로 설정 할 수 있다. 다음을 확인해보자.

void main(void){
    int num = 40;
    short*data = (short*)malloc(num);
    free(data);
}

하지만 배열은 위와 같이 변수를 배열의 크기로 설정할 수 없다.

void main(void){
    int num1 = 20;
    int data[num1];
}

문제 없이 진행됐기 때문에 업데이트가 되면서 배열도 변수를 받을 수 있게 바뀌었나 하고 찾아봤는데 다음과 같은 이유가 있었다.

그림링크

이 블로그를 읽는 사람들도 본인 컴파일러가 Variable Length Array를 지원하고 있는지 확인해보길 바란다.

하지만 내생각에는 정적할당을 사용해서 변수를 받는다면 특정 컴파일러에서만 돌아가기 때문에 개발자들끼리 협업을 하는데 방해가 될 것으로 보인다. 따라서 앞으로 가변적인 변수를 메모리의 크기로 받을때에는 동적할당을 사용하면 좋을 것 같다

Quiz> 동적할당으로 숫자를 입력받아 합산하기

위와 같은 결과를 내보길 바란다.
Hint>포인터 주소연산을 활용해라
참고
파이썬과 달리 C언어는 print 이후에 자동으로 \n이 들어가지 않는다.

  • 파이썬 코드





















정답>

void main(void){
    int num=0,max_cnt,cnt=0;
    printf("사용할 숫자의 최대갯수를 입력하시오:");
    scanf("%d",&max_cnt);
    int*p = (int*)malloc(max_cnt);
    while(num!=9999){
        printf("숫자를 입력하세요 9999를 누르면 종료: ");
        scanf("%d",&num);
        cnt++;
        if((num==9999))break;
        else if(cnt>max_cnt){
            printf("최대갯수 이상을 입력하셨습니다 프로그램 종료합니다.\n");
            break;
        }
        else{
            *(p+cnt-1) = num; //인덱스이기 때문에 갯수보다 1개가 적음
            
        }
    }
    printf("입력하신 숫자는 다음과 같습니다");
    for(int i = 0;i<max_cnt;i++){
        printf("%d",*(p+i));
    }
    free(p);
}
profile
Time-Series

0개의 댓글