정신과 시간의 방
카테고리
작성일
2022. 10. 1. 23:31
작성자
risehyun
  • 매개 변수 전달하기
    A 함수 내부에서 B 함수를 호출하는 코드가 있다면, A는 호출자이고 B는 피호출자이다.
    이 두 함수가 호출/피호출 관계로 묶이는 것을 바인딩(Binding)이라고 한다.
    두 함수가 서로 연결되는 인터페이스는 매개변수와 반환 자료이다.

    호출자와 피호출자 관계가 성립할 때 두 함수 중에서 호출자에 해당하는 함수는 피호출자 함수의 매개 변수 초기값을 인자로 넘겨줘야 한다는 의무가 있다.이때 함수 호출 과정에서 매개변수(인자)로 전달되는 정보가 무엇이냐에 따라 매개변수의 전달 방법이 달라진다.

    이러한 매개변수 전달방법은 전달할 매개변수가 값인지 주소인지에 따라 Call by value와 Call by reference로 나뉜다.

    - Call by value
    다음은 전형적인 call by value 예제로, 사용자 정의 함수 Add() 함수의 매개변수로 두 값을 전달하고 두 수의 합산 결과를 반환받아 출력한다.
    여기서 인수로 명시된 3과 4는 그 값 자체로 의미가 있기 때문에, 피호출함수가 이 값을 매개변수로 받아 내부 연산을 수행하고 적절한 정보를 반환할 수 있게 된다.
#include <stdio.h>

int Add(int a, int b)
{
	return a + b;
}

int main(int argc, char* argv[])
{
    printf("%d\n", Add(3, 4));

    return 0;
}

 

 

    - Call by reference
이 예제는 앞서 살펴본 call by value 형식으로 구현된 Add() 함수를 call by reference 형식으로 바꾼 것이다.
call by reference의 핵심은 매개변수가 포인터라는 점이다. 따라서 호출자는 반드시 메모리의 주소를 인수로 넘겨야 하고,
피호출자 함수는 이 매개변수를 간접 지정함으로써 포인터가 가리키는 대상에 접근할 수 있다.

따라서 예제에서는 int에 대한 포인터인 a와 b를 간접지정하였기 때문에 호출자 함수(main())의 지역변수에 접근해 정보를 읽어올 수 있었다. 예제엔 반영되지 않았지만 단순 대입을 시도할 수도 있다.
이전의 call by value 형식의 함수에서는 피호출자 함수가 호출자 함수에 속한 메모리에 접근할 수 없었다.
그러나 call by reference 형식에서는 주소를 인자로 전달했기 때문에 그 주소를 사영해 피호출자 함수가 호출자 함수에 속한 메모리를 대상으로 하여 접근할 수 있게 되었다.

 

#include <stdio.h>

int Add(int* a, int* b)
{
	return *a + *b;
}

int main(int argc, char* argv[])
{
	int x = 3, y = 4;

	// Add() 함수를 호출할 때 지역변수의 주소를 실인수로 기술
	printf("%d\n", Add(&x, &y));

	return 0;

}

 

call by reference 방식을 call by value 방식과 비교했을 때 가장 큰 차이점은 주소를 통해 호출자 메모리에 접근할 수 있는 방법을 제시함으로써 두 함수가 좀 더 강력하게 결합될 수 있다는 것이다.
또한 call by reference 방식의 가장 큰 장점은 배열처럼 크기가 큰 메모리를 매개변수로 전달할 수 있다는 점이다.

  • 매개변수가 포인터일 때 대상 메모리 크기를 인수로 함께 받기
    보안, 설계면에서 매개 변수를 포인터로 받을 때 포인터와 함께 대상 메모리 크기를 인수로 함께 받는 것이 유용하다.
    그 이유는 포인터의 가장 큰 문제를 방지하기 위해서이다.
    포인터 자체는 단순히 주소를 가리키기만 하기 때문에, 실제로 들어온 값이 실제로 어떤 크기를 가지고 있는지 알 방법이 없다.

#include <stdio.h>

// 주소를 매개변수로 받을 때는 대상 메모리의 크기를 함께 받는 것이 좋다.
void GetName(char* pszName, int nSize)
{

	printf("이름을 입력하세요. :");
	// 매개변수로 전달받은 주소를 다시 gets_s() 함수의 인자로 넘긴다.
	gets_s(pszName, nSize);

}

int main(int argc, char* argv[])
{
	char szName[32] = { 0 };

	// 배열(주소)과 배열의 크기를 함수의 매개변수로 전달한다.
	GetName(szName, sizeof(szName));
	printf("당신의 이름은 %s입니다. \n", szName);

	return 0;
}

 

이 예제에서는 GetName 함수를 정의할 때 char *pszName을 사용했다. 하지만 이것은 char pszName[] 과 동일한 의미이기 때문에 이렇게 바꿔 적어주어도 상관없다.
오히려 후자처럼 배열 기호를 직접 써두는 편이 별다른 주석 없이도 "매개변수로 배열의 이름이 전달"된다는 것을 추론하기 쉽게 해준다.

 

  • 피호출자 함수가 동적으로 할당한 메모리를 호출자 함수에서 해제하는 경우
    아래 예제는 메모리를 할당하는 함수와 해제하는 함수가 서로 다른 상황을 설명한다.
    몇몇개의 함수로만 이뤄진 작은 규모의 프로그램일 때는 동적으로 할당한 메모리를 바로 그 영역에서 해제하는 경우가 많지만, 규모가 큰 프로그램에서는 피호출자 함수가 동적으로 할당한 메모리를 호출자 함수에서 해제해줘야하는 상황이 벌어질 수 있다.
#include <stdio.h>
#include <stdlib.h>


// 주소를 반환하는 사용자 정의 함수 선언 및 정의
char* GetName(void)
{
	char* pszName = NULL;

	// 메모리를 동적 할당한다.
	pszName = (char*)calloc(32, sizeof(char));
	printf("이름을 입력하세요. : ");

	// 동적 할당한 메모리에 사용자가 입력한 문자열을 저장한다.
	gets_s(pszName, sizeof(char) * 32);
	// 동적 할당한 메모리의 주소를 호출자 함수에 반환한다.
	return pszName;

}

int main(int argc, char* argv[])
{
	char* pszName = NULL;
	// 이름이 저장된 동적 할당된 메모리의 주소를 반환받는다.
	pszName = GetName();
	printf("당신의 이름은 %s 입니다.\n", pszName);

	// 호출자 함수가 메모리를 해제해야 한다.
	free(pszName);


	return 0;
}

예제에서 각각 다른 함수에 선언된 변수 pszName은 이름은 동일하지만 선언된 스코프가 다르므로 서로 다른 변수임을 알 수 있다.
처음 GetName() 함수에서 pszName을 NULL로 초기화 하기 전에 이 변수 안에는 당연히 쓰레기값이 들어가 있게 되는데, calloc 함수를 사용해 이 함수가 반환한 메모리를 모두 0으로 초기화한다.
이후 동적 할당된 pszName 변수가 함수의 결과값으로 리턴되면, 메인 함수에 있던 동일한 이름의 다른 변수인 pszName이 이것을 받아 printf() 함수를 통해 출력한다.
이후 호출자 함수인 main()함수에서 GetName() 함수르 통해 받았던 동적할당된 pszName 변수의 메모리를 해제해주었다.

이처럼 추후에 C언어 프로그램을 작성하는 과정에서 어떤 오류가 발생한다면 메모리의 변화를 하나하나 추적하여 문제를 해결할 수 있어야 한다.

이어지는 예제는 Call by reference가 필요한 가장 대표적인 상황으로, 매개변수의 값을 포인터를 사용해 교환하는 것이다. 상당히 중요한 예제이므로 이해가 안되면 머리로 외우기라도 해야 한다.

 

#include <stdio.h>

void Swap(int* pLeft, int* pRight)
{
	// 주소가 가리키는 대상의 메모리 값을 교환한다.
	int nTmp = *pLeft;
	*pLeft = *pRight;
	*pRight = nTmp;
}

int main(int argc, char* argv[])
{
	int x = 10;
	int y = 20;

	// 호출자 함수에 선언된 지역변수의 주소를 전달한다.
	// 따라서 함수가 반환한 후 x와 y값은 서로 교환된다.

	Swap(&x, &y);
	printf("%d, %d\n", x, y);

	return 0;
}

 

Swap() 함수 내부는 함수이름처럼 int에 대한 포인터 젼수를 간접지정하고 대상 메모리에 대한 교환을 수행한다.
또한 main() 함수에서 Swap() 함수를 호출할 때, main() 함수의 지역변수인 x와 y의 주소를 인수로 전달한다.
따라서 이 각각을 간접지정한 대상은 당연히 main() 함수의 x와 y가 된다.

 

  • 잘못된 주소를 전달하는 경우
    call by reference 방식은 함수의 매개변수(포인터)로 주소를 받는 형식이다.
    이 경우에는 주소를 구체적으로 기술하는 일은 전적으로 호출자 함수의 책임이다.
    반면 피호출자 함수는 자신이 반환하는 주소가 가리키는 대상 메모리가 유효한지를 검증하고 보장해야할 책임이 있다.

    즉, 운영체제에 반환했거나(지역변수라서 스코프가 끝나면 반환되는 경우 등) 곧 사라질 메모리(메모리 할당을 해제하는 등)에 대한 주소를 반환하는 일은 절대 없어야 하며, 반드시 유효한 주소를 반환해야 한다.

    특히 함수 내부에 선언된 자동변수의 주소를 반환하는 경우를 주의해야 한다.

    동적 할당된 메모리나 정적변수는 함수의 호출이나 반환과는 아무런 관련이 없다.
    동적 할당된 메모리는 free() 함수로 해제하기 전까지 유효하며 정적 변수의 경우 프로그램이 종료되는 순간까지 유효하다.

    그러나 자동변수의 경우 스택 영역을 사용하기 때문에 스코프가 닫히면 그 내부에서 선언된 것이 사실상 사라진다(정말로 사라진다기 보다는, 가용범위가 줄어든 것-지정해제-가 된다).
    따라서 자동변수의 주소를 반환하는 경우에 결과적으로 이 주소는 유효하지 않은 주소가 되므로 오류를 일으킨다.
    혹 이 상황에서 프로그램이 정상적으로 작동된다고 해도 추후 여기에서 문제가 발생하면, 이미 사라진 주소이기 때문에 어디에서 이런 문제가 발생했는지 알아내는 것이 매우 어렵다.

    이처럼 심각한 오류가 있는데도 불구하고 프로그램이 정상적으로 작동된다면, 프로그램이 작동되지 않는 경우보다 더욱 경계하여 꼼꼼히 디버깅을 해 원인을 알아내야 한다.
    정상적으로 작동하지 말아야 할 코드가 작동한다는 것은 정반대인 경우보다 더 심각한 상황이며 절대 그냥 넘어가선 안된다.


  • 재귀호출
    - 재귀호출의 개념
    재귀호출(Recursive function call)은 함수가 내부에서 다시 자기 자신을 호출하는 것이다.
    영문표기로는 Recursion(반복)이라고도 하는데, '반복'이라는 말 그대로 '반복문과 스택 자료구조를 합친 것'이 바로 재귀호출이다.

    논리적인 코드의 구조는 반복문과 같지만, 반복 과정에서 선형 자료구조인 스택이 필요한 경우 바로 이 재귀호출을 사용한다.

    ※ 스택은 가장 먼저 Push한 정보가 가장 나중에 Pop되고, 가장 나중에 Push한 것이 가장 먼저 Pop되는 Last In First Out(LIFO) 구조이다. 이런 구조는 되돌리기(Undo) 기능과 같은 것을 구현하기 좋다.
    또한 어떤 정보를 다루는 과정에서 로그를 남기고자 할 때도 스택 형식으로 만드는 것이 좋다.
    시기적으로 가장 최근에 벌어진 일이 스택의 최상단에 존재하므로 접근하기가 쉽기 때문이다.

    - 재귀호출의 특징
    재귀 호출을 사용하는 가장 흔한 경우는 비선형 자료구조를 다룰 때이다.
    비선형 자료구조에는 대표적으로 트리(Tree)가 있다.

    트리의 특징은 자료를 계층적 구조로 만드는 것으로, 윈도우 탐색기 왼쪽에 있는 폴더들의 계층 구조를 표현한 화면과 같다. 따라서 재귀 호출은 트리구조를 배울 때 반드시 등장하게 된다.


    - 재귀호출의 장단점
    앞서 정의한 것처럼 재귀 호출은 "반복문과 스택 자료구조의 조합"이다.
    따라서 재귀호출로 구현된 코드를 반복문으로 수정하고자 한다면, 스택 형식의 선형자료구조를 한가지 구현하거나 구현된 라이브러리를 가져다 사용하여 고칠 수도 있다.

    그럼에도 굳이 재귀호출을 사용하는 이유는, 함수 호출 스택이 이미 존재하기 때문이다.
    함수가 함수를 호출하는 과정에 이미 스택 프레임이 존재하기 때문에 개발자가 이를 별도로 구현하지 않고 활용할 수 있어 재귀호출을 사용하는 것이다.

    이를 통해 개발자는 편의를 도모할 수 있고, 코드가 간편해질 수도 있다. 그러나 재귀 호출의 대가는 꽤 비싸다.
    바로 다음과 같은 이유 때문이다.

    1. 스택에는 자동변수나 매개변수 말고도 스택 프레임을 관리하기 위한 여러 정보들이 포함되며
    2. 함수 호출에 의해 프로그램의 흐름도 변경되고
    3. 매개변수를 복사하는 연산도 수행해야 한다.

    따라서 반복문에 비해 훨씬 더 많은 연산이 수행된다.
    또한 가장 큰 단점은 기본 설정을 유지했을 때 1MB 정도에 불과한 스택 메모리를 순식간에 대량으로 소모할 가능성이 높다는 것이다. 평균적으로 10~20%의 메모리를 사용한다고 하면, 잘못 사용된 재귀호출은 80~90%의 메모리를 사용하게 된다. 만약 스택 메모리를 모두 소진한다면, 스택 오버플로우 오류에 의해 프로그램이 비정상 종료된다.

    따라서 재귀호출을 사용하는데는 신중해야 하며, 반복문으로 할 수 있다면 반복문을 쓰는 것이 맞다.
    단, 비선형 자료구조인 트리를 다룰 때는 대부분 재귀호출을 사용한다. 


  • 문자/문자열 처리 함수
    CRL(C-Runtime Libraray)는 많은 표준함수를 제공한다. 공무를 목적으로 표준함수를 직접 구현할 수는 있지만 실무에서 이미 표준함수에 있는 기능을 직접 구현하려는 실수를 해선 안된다.
    표준함수는 호환성이나 안정성 면에서 이미 오랜 시간 검증된 좋은 코드이지만 직접 구현한 함수는 그런 검증이 되지 않았기 때문이다. 따라서 표준함수에 어떤 것들이 있는지 알아두고 해당 기능이 필요할 때 잘 활용하면 된다.

    - 문자 처리 함수
    CRL 함수 중 많은 것들이 문자와 문자열을 처리하는 함수이다. 이 중 '문자 처리 전용 함수'에는 어떤 것들이 있는지 다음의 표를 통해 간단히 알아보자.
함수이름 기능
isalpha() A~Z, a~z에 속하는 문자인지 검사하는 함수이다.
isdigit() 0~9에 속하는 문자(char)인지 검사하는 함수이다.
isxdigit() 0~9, A~F, a~z에 속하는 문자(char)인지 검사하는 함수이다.
isalnum() 0~9, A~Z, a~z에 속하는 문자(char)인지 검사하는 함수이다.
islower() 영문 소문자인지 검사하는 함수이다.
isupper() 영문 대문자인지 검사하는 함수이다.
isspace() 0x09~0x0D 혹은 0x20에 속하는 화이트 스페이스 문자인지 검사하는 함수이다.
toupper() 영문 소문자를 영문 대문자로 변환해주는 함수이다.
tolower() 영문 대문자를 소문자로 변환하는 함수이다.

 

     - 문자열 처리 함수
gets(), puts(), printf(), scanf() 함수들은 문자열 처리 함수에 속한다. 이 함수들 외에 자주 쓰이는 함수를 덧붙이면 다음과 같다.

 

  • strcat(), strncat() 함수를 이용한 문자열 붙이기

    - char *strcat(char *strDestination, const char *strSource);
    인자 : strDestination - 문자열을 추가(append)하여 저장할 메모리 주소
              strSource        - 추가할 문자열이 저장된 메모리 주소
    반환값 : strDestination - 인자로 주어진 주소 반환
    설명     : 첫 번째 인자로 전달된 주소에 저장된 문자열에 두 번째 인자로 전달된 문자열을 추가해주는 함수이다.
                  따라서 첫 번째 인자로 전달된 주소에 저장된 문자열의 길이가 늘어난다.

    * strcat() 함수의 경우 대상 메모리의 경계를 넘기는 오류가 발생하기 쉽고 이로 인한 보안 결함도 있으므로 윈도우에서는 strcat_s() 함수를 사용하는 것이 좋다.

    또한 strcat() 함수는 문자열을 뒤에 붙여넣기 위해서 대상 메모리에 저장된 문자열의 길이를 측정(strlen()) 할 수밖에 없다. 내부적으로 while문을 이용해 이러한 작업을 하는데, 그렇기 때문에 문자열의 길이가 늘어날 수록 길이를 측정해야 하는 반복 횟수가 늘어나 효율을 떨어트린다.

    따라서 가장 확실한 대안으로 직접 새로운 strcat() 함수를 만드는 방법이 있다.
    새로 만드는 함수는 첫 번째 매개변수로 받은 주솟값을 그대로 반환하는 것이 아닌, 두 번째 매개변수로 전달된 문자열을 이어 붙인 뒤 맨 마지막 문자('\0'이 아닌 문자)가 저장된 메모리의 주소를 반환한다.
    그러면 두 번째로 이어 붙일 때는 문자열의 길이를 처음부터 측정하지 않을 수 있기 때문이다.

#include <stdio.h>
#include <string.h>

char* mystrcat(char* pszDst, char* pszSrc)
{

	// 대상 메모리에 저장된 문자열의 끝을 찾는다.
	while (*pszDst != '\0')
	{
		++pszDst;
	}
	
	// 그 뒤에 새로운 문자열을 이어 붙인다.
	while (*pszSrc != '\0')
	{
		*pszDst++ = *pszSrc++;
	}

	// 맨 끝을 NULL문자로 마무리한다.
	*++pszDst = '\0';

	// 이어 붙인 문자열의 마지막 글자가 저장된 메모리의 주소를 반환한다.
	return --pszDst;
}

int main(void)
{
	char szPath[128] = { 0 };
	char* pszEnd = NULL;

	// 대상 메모리에 문자열을 붙인다.
	pszEnd = mystrcat(szPath, "C:\\Program Files\\");

	// 앞서 반환받은 주소를 첫 번째 인수로 호출해 문자열을 붙인다.
	pszEnd = mystrcat(pszEnd, "CHS\\");
	pszEnd = mystrcat(pszEnd, "C Programming");

	puts(szPath);
	return 0;
}

 

- char *strncat(char *strDestination, const char *strSource, size_t count);
인자 : strDestination - 문자열을 추가하여 저장할 메모리 주소
          strSource        - 추가할 문자열이 저장된 메모리 주소
          count               - 추가할 문자열 길이
반환값 : strDestination - 인자로 주어진 주소 반환 
설명     : 첫 번째 인자로 전달된 주소에 저장된 문자열에 두 번째 인자로 전달된 문자열을 주어진 길이만큼만 추가해주는 함수이다.

  • sprintf() 함수를 이용한 문자열 붙이기
    strcat() 함수 외에도 sprintf() 함수를 이용해서 문자열을 붙여넣거나 조립하는 방법이 있다.
    sprintf() 함수의 기능은 printf() 함수와 거의 같은데, 문자열을 콘솔 화면이 아니라 메모리에 출력한다는 점에서 차이가 있다. 자주 사용되는 함수는 아니지만 알아두면 유용하다.
    하지만 sprintf() 함수 역시 보안상 결함이 있으므로 sprintf_s()나 snprintf() 함수를 쓰는 것이 좋다.

  • strbrk() 함수를 이용한 구문분석
    strbrk() 함수는 strstr() 함수와 달리 대상 문자열에서 특정 '문자열'이 아닌, 문자들 중 하나가 있는지 검색하는 함수이다. 예를 들어 검색 대상 문자열에 'ABC'를 검색하면 'ABC'라는 문자열 전체를 검색하는 것이 아니라, 'A', 'B' 혹은 'C'가 있는지 대상 문자열에서 검색한다.
    만약 한 글자라도 일치하는 것을 찾으면 그 주소를 반환하는데, 전반적인 사용 방법은 strstr() 함수와 매우 유사하다.

    - char *strpbrk(const char *string, const char *strCharSet);
    인자 : string - 검색 대상 문자열이 저장된 메모리 주소
              strCharSet : 검색할 문자집합
    반환값 : 찾으면 해당 문자가 저장된 메모리 주소 반환, 찾지 못하면 NULL 반환
    설명     : 임의의 대상 문자열에서 특정 문자집합을 검색하는 함수
#include <stdio.h>
#include <string.h>

void main(void)
{
	char szBuffer[128] = { 0 };
	char szSet[128] = { 0 };

	char* pszStart = szBuffer;

	// 검색 대상 문자열을 입력받는다.
	printf("Input String : ");
	gets(szBuffer);

	// 찾을 문자들을 입력받는다.
	printf("Input character set : ");
	gets(szSet);

	// 대상 문자열에 일치하는 문자가 있는지 검색한다.
	while ((pszStart = strpbrk(pszStart, szSet)) != NULL)
	{
		printf("[%p] Index : %d, %c\n", pszStart, pszStart - szBuffer, *pszStart);
		
		// 일치하는 하나를 찾았으므로 다음으로 이동하고 계속 찾는다.
		pszStart++;
	}

}

위의 예제처럼 strpbrk() 함수를 활용하면 문자열에서 특정 구문을 분석할 때 유용하게 사용할 수 있다.

  • strtok() 함수를 이용한 구문분석
    구문 분석에는 앞서 사용한 strpbrk() 함수 외에도 strtok() 함수도 자주 사용된다.
    하지만 이 함수에는 몇가지 문제점이 존재하기 때문에 가급적 사용을 자제해야 한다.

    - char *strtok(char *strToken, const char *strDelimit);
    인자 : strToken - 토큰화 할 문자열이 저장된 메모리 주소
              strDelimit - 토큰의 기준이 되는 구분자 문자집합
    반환값 : 두 번째 인자로 전달된 문자집합 중 하나라도 찾으면 해당 문자가 저장된 메모리의 내용을 근거로 NULL로 바꾸고, 문자열의 시작 주소를 반환한다.
    설명     : 임의의 문자열을 구분자를 근거로 토큰화 하는 함수이다. 이 함수는 내부적으로 정적 변수(static)를 사용하므로 주의해야 한다.

#include <stdio.h>
#include <string.h>

int main(void)
{
	// 토큰화 할 대상 문자열
	char szBuffer[128] = { "nData = x + y;\nnResult = a * b" };

	// 토큰화의 기준이 되는 구분자 문자열
	char *pszSep = " *+=;\n";
	char* pszToken = NULL;

	// 구분자 문자열을 근거로 첫 번째 토큰화를 시도한다.
	pszToken = strtok(szBuffer, pszSep);
	while (pszToken != NULL)
	{
		// 찾은 토큰을 출력한다.
		puts(pszToken);

		// 그 다음 토큰을 이어서 검색한다.
		pszToken = strtok(NULL, pszSep);
	}

	// 변경된 원본 문자열을 출력해본다.
	printf("\nszBuffer: %s\n", szBuffer);

	return 0;
}

 

strrtok() 함수를 호출할 때는 첫 번째 인자로 토큰화를 시작할 문자열이 저장된 메모리의 주소를 명시했다.
그러나 이후 행에서부터는 첫번째 인자를 NULL로 명시한다.

이처럼 NULL로 명시하면 앞에서 토큰화를 완료한 다음에 '이어서' 토큰화를 한다.

여기서 토큰화란, 긴 문자열을 규칙에 따라 잘게 자르는 것을 의미하는데 구체적으로는 문자열 중간에 NULL을 집어넣어 문자를 자르는 것을 말한다. 기본적으로 함수의 작동 원리는 strstr(), strpbrk() 함수와 유사하다.

그러나 strtok() 함수는 검색 대상 메모리에 '쓰기'를 시도(NULL 삽입)하는 데다, 내부적으로 정적 변수를 사용하고 있기 때문에 멀티스레드 환경에서는 문제가 생길 수 있다.

 

따라서 strtok() 함수로 검색을 수행하는 대상 메모리는 반드시 쓰기 가능한 메모리여야 한다. 또한 함수가 반환한 후로 대상 메모리의 내용이 수정되었다는 사실을 감안해야한다. 보안 결함도 존재하기 때문에 이런 제약들을 생각하면 strtok() 함수 대신에 가급적이면 strpbrk() 함수를 사용하는 것이 좋다.

 

  • 유니코드 문자열
    C 언어에서 문자열은 두 종류로 구별 할 수 있는데, 첫 번째는 MBCS(Muiti-bytes Character Sets) 문자열이고, 두 번째는 유니코드(UNICODE) 문자열이다. 일반적으로 예제에서 다룬 문자열은 모두 MBCS 문자열이며, MBCS에서 영문 한 글자는 1바이트, 한글 한 글자는 2바이트를 사용한다. 그리고 char 형으로 문자를 표현한다. 문자열의 끝을 명시하는 NULL 문자 역시 char 형이다.

    그런데 이와 같은 영문, 한글, 한자 표현의 차이는 프로그램 문자열 길이 계산에도 큰 영향을 준다.
    예를 들어, "String"이라는 문자열의 길이는 6이고 실제로 문자의 개수도 6이며, 이 문자열을 저장하려면 char[7]이 필요하다.
    그러나 "문자열"이라는 한글 문자열은 길이가 6인데(MBCS에서 한글은 한 글자당 2바이트 이므로 2*3), 문자의 개수는 3개이다. 즉, 문자열의 문자 개수와 길이가 서로 다르다는 문제가 발생한다. 이 외에도 인코딩 규칙에 따른 여러 문제가 있을 수 있다.

    따라서 이런 문제를 극복하기 위해 유니코드가 등장했다.
    유니코드는 문자 하나를 표현하기 위해 16비트 혹은 32비트를 사용하는데 윈도우 운영체제에서는 16비트 자료형이다. 따라서 유니코드 형식을 나타내는 wchar_t형은 2바이트이다.

    유니코드는 상수 형식으로 표기할 때 문자열 앞에 'L'을 붙여서 L"String" 형식으로 표기한다. 따라서 L"String"의 자료형은 wchar[7]가 된다. 이때 바이트 메모리 단위 크기는 14(널 문자 포함 7개*2바이트)가 된다.
    유니코드에서는 영문/한글 관계없이 필요한 메모리 크기가 '(문자열 길이 + 1) * sizeof(wchar_t)'로 통일 된다.
    따라서 한글을 사용하는 L"문자열"의  자료형은 wchar[4]가 되고, 메모리 단위 크기는 4*2 = 8바이트가 된다.

    MBCS 문자열과 유니코드 문자열은 구조가 다르므로 유니코드 문자열을 MBCS 문자열 처리 함수로 출력하면 첫 글자만 출력되는 현상이 발생할 수 밖에 없다.
    유니코드 문자열의 경우 영문자마다 NULL이 들어간다. 그래서 printf(L"Hello");라고 출력하면 화면에는 'H'만 나온다.
    따라서 유니코드 문자열은 유니코드 문자열 전용 함수를 사용해야 한다.

  • wprintf(), wcscpy() 함수
    유니코드 문자열 전용 함수에는 대표적으로 printf() 대신 wprintf()를 사용하고, strcpy() 대신 wcscpy()를 사용한다. 뿐만 아니라 모든 문자열 처리 함수에 대해 MBCS 버전과 UNICODE 버전이 함께 존재한다.
    하지만 wcscpy() 함수의 경우 strcpy() 함수처럼 보안 결함이 존재하므로, 대체 함수로 wcscpy_s()를 사용하거나 wcsncpy()함수가 있다.

#include <stdio.h>
#include <string.h>

int main(void)
{
	wchar_t* pszData = L"String";
	wchar_t wszData[32];
	
	wcscpy(wszData, pszData);
	wprintf(L"%s\n", wszData);

	return 0;


}

 

  • wcstombs(). mbstowcs() 함수
    wcstombs() 함수는 유니코드 문자열을 MBCS 문자열로 변환한다. 반대로 mbstowcs() 함수는 MBCS 문자열을 유니코드로 바꾼다.

    - size_t wcstombs(char* *mbstr, const wchar_t *wcstr, size_t count);
    인자 : mbstr - MBCS로 변환한 문자열을 저장할 메모리 주소
              wcstr - MBCS로 변환할 유니코드 문자열이 저장된 메모리 주소
              count - MBCS로 변환할 문자열의 최대 크기
    반환값 : MBCS로 변환된 문자열 길이로, 만일 mbstr 인수가 NULL이면 변환을 위해 필요한 메모리 길이 반환
    설명     : 유니코드 문자열을 MBCS 문자열로 변환하는 함수

    -size_t mbstowcs(wchar_t *wcstr, const char *mbstr, size_t count);
    인자 : wcstr - 유니코드로 변환한 문자열을 저장할 메모리 주소
              mbstr - 유니코드로 변환할 MBCS 문자열이 저장된 메모리 주소
              count -  유니코드로 변환할 문자열의 최대 크기
    반환값 : 유니코드로 변환된 문자열의 길이(개수)
                  만일 wcstr 인수가 NULL이면 변환을 위해 필요한 메모리 길이 반환
    설명     : MBCS 문자열을 유니코드 문자열로 변환하는 함수


  • 유틸리티 함수
    유틸리티 함수란 프로그램을 개발하는 과정에서 자주 사용되는 다양한 기능을 함수로 구현해 놓은 것을 말한다. 예를 들어서, 시스템의 시간을 알아내는 일은 어떤 프로그램이든 필요할 수가 있다. 혹은 문자열을 정수 자료형으로 변환하거나 임의의 난수를 발생 시키는 기능도 사용 빈도가 높은 편이다.

    - atoi(), atol(), atof() 함수
    atoi() 함수는 아스키 코드 문자열로 된 숫자 문자열을 int형 자료로 변환해주는 기능을 가진 함수이다.
    같은 방식으로 atol() 함수는 long형, atof() 함수는  double 형식으로 자료를 변환해준다.
    이 함수들의 유니코드 기반 함수는 _wtoi(), _wtol(), _wtof() 함수이다.

    1. int atoi(const char *string);
    인자 : string - 변환할 문자열이 저장된 메모리의 주소
    변환값 : 변환된 int 값, 변환에 실패할 경우 0
    설명     : 정수 문자열을 실제 정수로 변환하는 함수

    2. long atol(const char *string);
    인자 : string - 변환할 문자열이 저장된 메모리의 주소
    변환값 : 변환된 long 값, 변환에 실패할 경우 0L
    설명     : long형 문자열을 실제 long 숫자로 변환하는 함수

    3. long atof(const char *string);
    인자 : string - 변환할 문자열이 저장된 메모리의 주소
    변환값 : 변환된 double 값, 변환에 실패할 경우 0.0
    설명     : 실수 문자열을 실제 실수로 변환하는 함수

    만약 변환값이 표현범위를 벗어나는 경우에는 어떻게 되는지 예제로 확인해보자. 
#include <stdio.h>
#include <stdlib.h>

void main(void)
{
	printf("%d\n", atoi("2147483647")); // int형 자료가 표현할 수 있는 최대 크기의 양수
	printf("%d\n", atoi("2147483648")); // 최대 크기를 벗어나면 정수로 표현할 수 있는 최댓값을 반환한다.
	printf("%e\n", atof("1.7e+308"));   // double형 자료가 표현할 수 있는 최대 크기 실수
	printf("%e\n", atof("1.7e+309"));   // 값을 제대로 표시할 수 없으므로 프로그램은 멀쩡하게 돌아가지만 연산 결과가 올바르지 못하게 됨.
}


정수의 경우 표현 범위를 벗어나면 정수가 표현할 수 있는 최댓값을 반환하지만, 실수의 경우 값을 제대로 표시할 수 없어 연산 결과가 바르지 못하게 되므로 주의해야 한다.

 - time(), localtime(), ctime() 함수
컴퓨터가 시간을 표시할 때는 UTC라고 하는 협정 시계시를 기준으로 한다.
time() 함수는 C언어 표준 라이브러리 함수로, 1970년 1월 1일 자정부터 현재까지 흘러간 시간을 초 단위로 계산해주는 함수이다. 다만 자신의 컴퓨터에 설정된 시간을 말하는 것이기 때문에 실제 시간과는 아무런 관련이 없다.

함수의 인자로는 결과가 저장될 변수의 주소를 받는데, 어차피 반환값으로도 결과를 알 수 있기 때문에 대부분 NULL을 인자로 호출한다.

하지만 반환된 시간을 알아볼 수 있는 시간으로 변환하는 것은 별도의 함수를 이용해야만 가능하다.
나누기 및 나머지 연산을 사용하는 방법도 있겠지만 C언어에서는 localtime()이나 ctime() 함수를 이용해 계산하는 것이 좋다. 단, 두 함수 모두 보안 문제가 있기 때문에 대체 함수로 localtime_s(), ctime_s() 함수를 사용할 수 있다.
그리고 ctime() 함수의 경우 문자열의 주소를 반환하기 때문에 유니코드 기반의 _wctime()함수가 별도로 존재한다. ctime_s() 함수의 경우에는 _wctime_s()함수가 있다.

- time_t time(time_t *timer);
인자 : timer - 결과를 저장할 time_t 변수의 주소
반환값 : 1970년 1월 1일 자정부터 현재까지 흐른 시간을 초 단위로 반환
설명     : 에러를 반환하는 경우는 없다. 단 2038년 1월 19일 03시 14분 07초 이후에는 32비트(long 형)의 범위를 벗어난다.

- struct tm *localtime(const time_t *timer)

인자 : timer - time() 함수로 알아낸 시간 값이 저장된 변수의 주소
반환값 : tm 구조체의 주소
설명 : tm 구조체는 년/월/일 시/분/초 등의 정보를 멤버로 가진다. 즉, 이 함수는 시간 값(초 단위)를 계산하여 구조체로 반환해주는 함수이다.

- char *ctime(const time_t *timer);

인자 : timer - timer() 함수로 알아낸 시간 값이 저장된 변수의 주소
반환값 : 시간을 형식에 맞는 문자열로 반환하여 그 주소를 반환한다.
설명 : 로컬 타임 존(local time zone) 설정에 맞춰 시간을 문자열로 변환하는 함수

 

  • srand(), rand() 함수
    rand() 함수는 난수를 발생하는 함수로, 호출할 때마다 임의의 숫자(0~0x7FFF)를 불규칙적으로 반환한다. 참고로 0x7FFF를 십진수로 변환하면 32767이다. 그리고 보통 난수의 최댓값인 0x7FFF라는 표현 대신 RAND_MAX라는 상수를 사용한다. 이 상수는 #define 전처리기를 이용하여 정의된 상수이다.

    rand() 함수를 호출하기 앞서서 반드시 srand() 함수를 호출하여 초깃값을 설정해야 프로그램이 처음 시작되어 호출되는 rand() 함수부터 늘 다른 값을 반환할 수 있게 된다. 만일 srand() 함수를 호출하지 않는다면 프로그램을 시작할 때마다 매번 같은 순서의 난수가 발생할 것이다.

    - void srand(unsigned int seed);
    인자 : seed - 난수를 발생시키기 위한 초깃값
    반환값 : 없음
    설명 : rand() 함수를 이용하여 난수를 발생시키기에 앞서, 임의의 초깃값을 설정해 최초 rand() 함수가 반환하는 값의 근거가 되는 초깃값을 명시하는 함수이다.

    - int rand(void);
    인자 : 없음
    반환값 : 난수
    설명 : O~RAND_MAX(231-1) 사이에 속하는 임의의 난수
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void main()
{
	int i = 0;
	srand((unsigned)time(NULL));

	for (i = 0; i < 10; i++)
	{
		printf("%6d\n", rand());
	}

	for (i = 0; i < 10; i++)
	{
		printf("%6d\n", rand() % 10);
	}

}


위 예제는 총 20개의 난수를 발생시키는데, 상단의 10개는 RAND_MAX 범위 안에서 난수 10개를 발생시키지만,

두 번째 10개는 나머지 연산자를 사용해 출력되는 난수의 범위를 0~10 미만으로 제한하였다.
이를 활용하여 만약 가위바위보 게임을 개발한다고 하면 난수 범위를 0~2로 제한 하는 방법도 있을 것이다.

 

  • system(), exit() 함수
    system() 함수는 명령 프롬프트를 통해 명령을 내리는 것과 같은 기능을 제공한다.
    이 함수를 이용하면 다른 외부 응용 프로그램이나 명령을 실행 할 수 있게 된다.
    이 함수의 유니코드 기반 함수는 _wsystem() 함수이다.

    exit() 함수는 프로그램을 즉시 종료하는 함수이다. C언어에서 프로그램의 종료는 main() 함수의 반환을 의미하는데, exit() 함수를 호출하면 이것과 상관 없이 프로그램을 즉시 종료시킨다.

    - int system(const char *command);
    인자 : command 명령 콘솔에서 실행할 문자열이 저장된 메모리 주소
    반환값 : 성공하면 0이 아닌 값 반환
                  그러나 Command interpreter가 0을 반환하면 똑같이 0을 반환한다.
                   만일 에러가 발생한다면 -1을 반환한다.
    설명    : Command interpreter에 명령어를 전달해 실행하고, 그 결과를 반환해주는 함수다. 해당 명령의 수행이 끝나기 전에는 함수를 반환하지 않는다.

#include <stdio.h>
#include <stdlib.h>

void main()
{
	char szCommand[512] = { 0 };
	printf("Input command : ");
	gets(szCommand);

	system(szCommand);


}

위 예제에서 콘솔창에 notepad라고 입력하면 윈도우 메모장이 실행된다.

- void exit(int status);

인자 : status - 응용 프로그램의 종료 상태 값
반환값 : 없음
설명 : 인수로 전달된 상태 값을 반환하고, 프로그램을 완전히 종료한다.

'C,C++' 카테고리의 다른 글

구조체 포인터  (0) 2023.01.20
문자, 문자열  (0) 2023.01.19
[C/C++] 기본 제어문  (0) 2022.09.25
[C] 입출력  (0) 2022.09.23
[C] C 프로그래밍의 기본 요소  (0) 2022.09.19