배움 저장소

홍정모의 따라하며 배우는 C언어 4 문자열과 형식지정자 본문

Programming Language/C

홍정모의 따라하며 배우는 C언어 4 문자열과 형식지정자

시옷지읏 2021. 11. 10. 22:41

4.1 문자열 입출력하기

char fruit;         
char fruit2[40]; /* char type 40개 저장공간을 확보 */

scanf("%c", &fruit);
scanf("%s", fruit2); /* 이때 fruit2는 char type 40개 저장공간의 첫 번째를 
                        가리키는 포인트다. 따라서 & 연산자를 사용할 필요가 없다.*/
printf("%c", fruit);
printf("%s", fruit2);

// 사용공간을 지정해주지 않고 초기화 가능하다
// 자리 계산은 알아서 해준다
char fruit3[] = "banana";

 

4.2 sizeof 연산자

1) sizeof basic types

	int i = 0;
	unsigned int int_size1 = sizeof i;
	unsigned int int_size2 = sizeof(int);
	unsigned int int_size3 = sizeof(i);

	size_t float_size = sizeof(float);

	printf("Size of int    %u bytes \n", int_size1);
	printf("Size of int    %u bytes \n", int_size2);
	printf("Size of int    %u bytes \n", int_size3);
	printf("Size of float %zu bytes \n", float_size);

참고) 출력에 필요한 specifier ( unsigned int : %u  /  size_t : %zu )

 

- size_t 타입을 사용하면 다른 시스템에서 이용하기 용이하다.

// Definitions of common types
#ifdef _WIN64
    typedef unsigned __int64 size_t;
    typedef __int64          ptrdiff_t;
    typedef __int64          intptr_t;
#else
    typedef unsigned int     size_t;
    typedef int              ptrdiff_t;
    typedef int              intptr_t;
#endif

 

2) sizeof arrays

malloc을 사용하려면 stdlib.h 헤더파일을 포함시켜주어야 한다. 

int int_arr[30];
int *int_ptr = NULL;
int_ptr = (int*)maclloc(sizeof(int) * 30);
printf("Size of array   %zu bytes\n", sizeof(intarr)); /* 120bytes  */
printf("Size of pointer %zu bytes\n", sizeof(int_ptr));/*   4bytes  */

- 3번째 줄에서 memory allocation을 어떻게 사용하는지 보여주고 있다.

int type 저장공간 30개를 하나의 저장공간으로 취급하고 int_ptr에 이 저장공간의 주소를 할당한다.

- 이제 두 방법으로 할당한 int_arr과 int_ptr을 모두 출력해보자.

  int_arr을 출력하면 120bytes. int_ptr을 출력하면 4bytes를 출력한다 왜 이런일이 생길까?

=>int_arr은 컴파일 타임에 결정되어 30개의 int type의 저장공간이 한 주소에할당된다. int_ptr은 컴파일 타임에 NULL로 결정되어 이후에 얼마나 큰 사이즈를 담을지 결정하지 못해 컴파일러는 포인터의 크기를 출력한다.

 

malloc 앞의 (int*)의 역할

(int*)는 int*자료형으로 형변환한다.

malloc은 동적으로 메모리를 할당 받은 후 이의 메모리 주소를 void* 형태로 반환한다. sizeof(int) * 30 크기의 메모리를 할당받은 후 이의 주소를 void * 타입으로 리턴한다. 

int_ptr 은 int* 즉 int 를 가리키는 포인터이기에, int_ptr에 할당받은 힙 메모리의 주소를 (int *)로 형변환 하여 int_ptr에 대입시킨다.

 

3) sizeof character array

/* array char는 마지막에 마침표 '\0'을 항상 포함해야 
   하기 때문에 9개의 공간만 사용할 수 있다.           */
char string[10];

printf() 함수는 '/0'를 만나 면 종료된다.

str에 문자를 입력하면, 문자열이 끝나는 지점부터 자동으로 '/0'을 컴퓨터가 입력한다.

다만 str[index]에 직접 값을 입력하는 경우에는 자동으로 입력되지 않으므로 직접 해주어야한다.

 

4.3 문자열이메모리에 저장되는 구조

specifier %hhi를 사용하면 해당 character의 ASCII 코드를 출력한다.

char a = 'a';
char b = 'b';

print("%hhi %hhi\n", a, b);

 

4.4 strlen() 함수

	char str[] = "Hello"; // 컴파일러가 사이즈를 정해준다.

	/* 동적할당을 이용하려면 각 index에 개별 값을 넣어주여야 한다 */
	char* str_malloc = NULL;
	str_malloc = (char*)malloc(sizeof(char) * 100);
	if (str_malloc == NULL) exit(1);

	str_malloc[0] = 'H';
	str_malloc[1] = 'E';
	str_malloc[2] = 'L';
	str_malloc[3] = 'L';
	str_malloc[4] = 'O';
	str_malloc[5] = '\0';

	//strcpy(str_malloc, str);
	//memcpy(str_malloc, str, 6);
	printf("%s \n", str_malloc);


	/* 이때 str_malloc의 사이즈는 주소가 저장되는 공간의 사이즈이다. */
	printf("%zu %zu\n", sizeof(str_malloc), strlen(str_malloc));

이때 32bit 시스템과 64bit 시스템은 주소가 저장되는 공간 크기가 다르기 때문에 다른 값이 나온다.

32bit는 4bytes, 64bit는 8bytes가 주소가 저장되는 공간의 크기이다.

 

4.5 기호적 상수와 전처리기

Symbolic Constant : 기호적 상수, 특정 상수 대신에 기호를 활용할 수 있다. 관습적으로 대문자를 사용한다.

#define PI 3.141592f // Symbolic Constant
#define AI_NAME "Jarvis"

int main()
{
    float radius, area;
    int flag = scanf("%f", &radius);

    area = PI * radius * radius;
    printf("area = %f\n", area);
}

변수와 기호적 상수의 차이는 메모리에 저장 여부이다. 변수는 해당 값을 메모리 공간에 저장하지만 기호적 상수는 전처리 과정(컴파일 타임 이전에 해당함)에 해당 기호적 상수를 그에 맞는 값으로 바꾸어버린다.

 

4.6 명백한 상수들

Manifest Constants: int type에서의 최대값처럼 확실하게 정해져 있어 변하지 않는 상수를 기호에 할당하여 사용하면 명백한 상수이다. INT_MAX, FLT_MIN등이 있다. 

#include <limits.h> // INT_MAX ..
#include <float.h>  // FLT_MAX ...

 

4.7 printf() 함수의 변환 지정자들

Coversion Specifier

변환 출력 사양
%p 포인터의 주소
%x , %X 16진수
%a 부동 소수점 수, 16진수, 소문자 p표기법
%A 부동 소수점 수, 16진수, 대문자 P표기법
%e 부동 소수점 수, 소문자 e-표기법
%E 부동 소수점 수, 대문자 E-표기법
%g %e 사용
지수가 -4보다 작을 때
정밀도 보다 크거나 같을 때
%f 사용
아닐 때
%G 위와 동일하나 %E를 대신하여 사용함

https://docs.microsoft.com/en-us/cpp/c-runtime-library/format-specification-syntax-printf-and-wprintf-functions?view=msvc-170 

 

Format Specification Syntax: `printf` and `wprintf` Functions

Describes the format specifier syntax for the Microsoft C runtime `printf` and `wprintf` functions

docs.microsoft.com

/* string 값이 길 때 \(backslash)를 
   이용하면 다음 줄을 활용할 수 있다. */
printf("Even if there's a small chance, \
we owe this to evertone");

/* %i는 signed int를 출력할 수 있다. unsigned int를
   출력하면 최대 범위를 넘어 overflow */
printf("%i \n", UINT_MAX); // X
printf("%u \n", UINT_MAX); // O

double pi = 3.141592653589793238462643383279502884197;
printf("%f %lf\n", pi, pi); // >> 3.141593 3.141593
printf("%a %A \n", pi, pi); // >> 0x1.921fb54442d18p+1 0X1.921FB54442D18P+1
printf("%e %E \n", pi, pi); // >> 3.141593e+00 3.141593E+00

 

아래 코드에서 123456이 아닌 123457이 나오는 이유는 반올림했기 때문이다.

// 자리수에 따라 표기 방법이 바뀐다.
printf("%g, %g\n", 123456.789, 1234567.89); // 123457, 1.23457e+06
printf("%G, %G\n", 123456.789, 1234567.89); // 123457, 1.23457E+06

// E-04보다 작은 수일 때 표기 방법이 바뀐다.
printf("%g, %g\n, 0.00012345, 0.000012345); // 0.00012345 1.2345e-05
printf("%G, %G\n, 0.00012345, 0.000012345); // 0.00012345 1.2345E-05

 

자릿수 맞추기

printf("%9d \n", 12345);     //     12345 (앞 4자리를 비어준다)
printf("%09d \n", 12345);    // 000012345
printf("%.2f \n", 3.141592); // 3.14                   (소수점 2자리 출력)
printf("%.20f \n", pi);      // 3.14159265358979323800 (소수점 20자리 출력)
                             // double 사이즈의 한계로 뒤에 00은 값이 날라간것

 

printf() 함수에서 float와 dobule(long float)는 동일하게 %f를 사용한다. double을 %lf로 사용해도 이상은 없다.

하지만 scanf() 함수에서는 double type을 사용하려면 %lf를 꼭 사용해야한다.

 

// printf() 함수는 출력 문자의 개수를 반환한다.
int n_printed = printf("Counting");
printf("%u \n", n_printed);

 

4.8 변환 지정자의 수식어

-   => 있으면 왼쪽 없으면 오른쪽
+   => 양수일 때도 '+' 기호가 출력된다
10  => width = 10
.5  => (float)소숫점 5자리에서 반올림
    => (int)  0을 채워놓고 width를 정함
h   => short type
i   => integer

예시

    printf("%+10.5hi\n", 256);

    printf("%+7hi\n", SHRT_MAX);
    printf("%+7hi\n", 0);
    printf("%+7hi\n\n", SHRT_MIN);

    printf("%f\n", FLT_MAX);
    printf("%.50f\n", FLT_MIN);
    printf("%G\n\n", FLT_MIN);

    printf("padding %+60.50f\n", FLT_MAX);
    printf("padding %+60.50f\n\n", FLT_MIN);

    printf("%#o\n", 8);    // 8진수
    printf("%#X\n\n", 16); // 16진수

    printf("%05i \n", 8);   //  width 5자리 중 빈 자리는 0으로 채운다.
    printf("%*i \n", 5, 8); // width 변수를 parameter로 설정한다
                            // width 5자리 중 빈자리는 0으로 채움

 

	/* 아래 123, 1234 모두 동일한 출력결과가 나온다.*/
	printf("%05i\n", 123);
	printf("%05i\n", 1234);

	printf("%.5i\n", 123);
	printf("%.5i\n", 1234);

	printf("%0.5i\n", 123);
	printf("%0.5i\n", 1234);

	printf("%.5s", "hellow there?\n");  // >> hellow there?
	printf("%.s", "hellow there?\n");   // >> 
	//      %.s == %0.s

	// length
	printf("\n\nunsigned char:%hhd\n", 257); // overflow >> 1
	printf("short:%hd\n", 257);          // short    >> 257 
	printf("double:%d\n", 257);          // double   >> 257

	// longlong
	printf("decimal: %d\n", INT_MAX + 1);     // overflow >> INT_MIN
	printf("long long int: %lld\n", (long long)INT_MAX + 1);

%d와 %i 모두 integer type을 출력할 수 있다. %d는 10진법 값을 받고 출력하지만 %i는 0을 붙이면 8진법 x를 붙이면 16진법 0b를 붙이면 2진법을 사용할 수 있다.

printf("%i \n", 0b11); // binary -> decimal >> 3
printf("%i \n", 012);  // octal  -> decimal >> 10
printf("%i", 0x12);    // hexa   -> decimal >> 18

 

4.9 printf() 함수가 인자를 해석하는 과정

	float n1 = 3.14;   // 4bytes
	double n2 = 1.234; // 8bytes
	int n3 = 1024;     // 4bytes

	printf(" %d %d %d \n", n1, n2, n3); // %d는 4bytes 씩 읽는다.
	printf(" %f %f %d \n", n1, n2, n3); // %f는 8bytes 씩 읽는다.

 printf( )함수가 실행되면 n1,n2,n3는 차례대로 스택에 쌓인다. 이때 printf() 함수의 특징이 나타난다. printf()함수는 float와 double 모두 double로 변환해 처리한다. 따라서 스택에 쌓일 때 n1과 n2는 8bytes의 공간을 차지한다. (n3는 4bytes 변함이 없다)

n1의 데이터는 8bytes에 걸쳐 저장되어있다. 첫 번째 출력문에서 %d format specifier를 사용한 것이 문제가 된다.

n1을 읽을 때 %d를 사용하였으므로 0~4bytes의 정보를 불러온다. 이제  스택에 있는 다음 변수를 가져와야 한다. 이 때 4bytes~8bytes를 그 다음 변수로 판단하여 읽어온다. 원하지 않는 입력값이 출력될 것이다.

 

4.10 scanf()함수 사용법

https://www.cplusplus.com/reference/cstdio/scanf/

 

scanf - C++ Reference

function scanf int scanf ( const char * format, ... ); Read formatted data from stdin Reads data from stdin and stores them according to the parameter format into the locations pointed by the additional arguments. The additional arguments should p

www.cplusplus.com

여러 값을 동시에 입력할 때 각 변수 사이에 있는 값을 기준으로 값을 입력한다.

int i;
float f;
char str[20];

// 입력 값 사이에 ','필요
scanf("%i,%f,%s", &i, &f, str); 
// 입력 값 사이에 ' ' 필요
scanf("%i %f %s", &i, &f, str);
// 입력 값 사이에 'a' 필요
scanf("%ia%fa%s", &i, &f, str);

printf("출력: %i %f %s", i, f, str);

 

unsigned int type에 signed int type의 값을 넣고 다양한 specifier로 출력해보자. 어떻게될까?

    unsigned i;
    scanf("%i", &i);     // 음수 값을 입력해보자.

    printf("%u %i", i,i);

1.

signed int는 첫 번째 bit를 양수/음수로 사용하고 unsigned int는 첫 번째 음수까지 수를 세는대 사용한다. 그래서

singed    -2147483648 ~ 2147483647 

unsinged                0 ~ 4294967295

signed 전체를 unsigned/2 만큼 더하면 unsigned 가 된다.

 

2.

 scanf()함수가 실행될 때 -4값을 넣으면 어떻게 될까? 맨 앞에 있는 bit가 1이 되고 보간법을 이용하여 다음과 같은 형태로 저장될 것이다. 

1111 1111 1111 1111 1111 1111 1111 1100

신기한건 signed int 데이터 형태를 2진수로 나타내어 unsigned int에 넣었을 때 오류가 나타나지 않는다는 점이다. 데이터 사이즈가 동일하기만 하면 컴파일러는 아무 신경을 쓰지 않는 것 같다.

 

이 함수가 signed int일 때는 보간법으로 계산하여 -4이지만

이 함수가 unsigned int 일 때는 2진수를 계속 더한 값이 나와 4294967292이 된다.

 

scanf() 함수는 f와 double을 구분한다.

double d = 0.0;
scanf("%f", &d);   // << 200
printf("%f \n",d); // >> 0.000

위 예제에서 200 같이 작은 값을 넣으면 출력 값은 0이된다. 그 이유를 추측해보았다. 컴퓨터는 bit로 데이터를 저장하기 때문에 8bytes에서 4bytes 저장공간을 사용한다고 하면 맨 뒤에있는 4bytes를 사용한다.

그래서 이 값을 double에서 사용하게 되면 fraction(Mantissa)에 들어가게 되어서 아주 작은 값이 되어버린다. 다음 예제를 실행시켜보면 더 명확해진다.

double d = 123456.789;
scanf("%f", &d);     // << 200
printf("%lf \n",d);  // >> 123456.766426

이 때 d의 초기값이 0이 아니라 충분히 큰 값으로 할당한다. 이때 exponent는 변화가 없으므로 아주 작은 값만 변할 것이다. 예측처럼 출력값은 Mantissa의 값만 반영된 것 같다.

 따라서 double에 값을 입력해줄 떄는 꼭 %lf를 사용해주어 8bytes에 데이터를 쓸 수 있도록 하자.

 

scanf()의 다양한 specifier

/* string width */
char str[30];
scanf("%5s", str); // 5글자까지 입력
printf("%s \n", str);

/* chr with %hhd */
char i;
scanf("%hhd", &i);  // short short decimal(-128~127)
printf("%i \n", i); // 큰 값이 들어오면 overflow
더보기

r 데이터는 -128 ~ 127까지 값을 사용할 수 있습니다. 이때

128 값을 할당하면 -128로 overflow

129 값을 할당하면 -127로 overflow

130 값을 할당하면 -126로 overflow

 

overflow된 값을 보간법으로 처리하는 방법을 모르기 때문에 출력값을 기준으로 컴퓨터가 어떻게 동작하고 있는지 추측해봐야 했습니다.

 

binary 계산으로 보면 컴퓨터는 단순히 1을 계속하여 더하고 있습니다.

 

부호| 값

0 | 111 1111 = 127 <= 127

1 | 000 0000 = -128 <= 128

1 | 000 0001 = -127 <= 129

1 | 000 0010 = -126 <= 130

 

그렇다면 컴퓨터가 int 타입의 16이란 숫자를 저장할 때도

0부터 계속해서 1을 더하여 16을 구하는 건가요?

 

int 타입은 그렇다고하면 float나 double같은 경우는 exponent와 fraction이 각각 나누어져 있기 때문에 실제 값을 계산하고 할당할 때 상당히 복잡한 과정이 있을 것 같습니다. 교수님이 알려주셔서 각 파트를 어떻게 계산하는지는 알고 있으나 컴퓨터가 이 과정을 어떻게 계산하고 처리하는지는 막상 상상이 되지 않습니다.

 

scanf()함수를 사용하여 값을 할당할 때 다른 형태의 type이 들어오면 읽어들이기를 멈춘다.

scanf()함수는 입력받은 변수의 개수를 반환한다.

    /* 가장 큰 정수형 타입 */
    intmax_t ll_i;

    /* %i 대신 %ji specifier를 사용 */
    scanf("%ji", &ll_i);
    printf("%ji \n", ll_i);

    /* 다른 타입의 입력값을 함께 읽어올 수 있다. */
    int a, b;
    char c;
    scanf("%d%c%d", &a, &c, &b);
    printf("%d|%c|%d \n", a, c, b);

    /* asterisk * modifier for printf() */
    int i = 123;
    int width = 5;
    scanf("%d", &width);
    printf("%*d \n", width, i); // width를 *specifier를 활용하여 
                                // 동적으로 바꿀 수 있다.


    /* asterisk * modifier for printf() */
    int i_aster;
    scanf("%*d%*d%d", &i_aster);              // * specifier가 있으면 해당 값을 무시한다.
    printf("Your third input = %d", i_aster); // 마지막에 입력한 값만 i에 저장한다.
                                              // 보통 scanf()는 줄바꿈이나 Enter로
                                              // 이전 값과 다음 값을 구분한다
Comments