[C++] 문자열 함수 - strlen(), strcat_s(), strcmp(), strcpy_s()

2024. 3. 27. 18:23Programming Language/C++

아래 링크 클릭 시 해당 본문으로 이동

strlen()

strcat_s()

strcmp()

strcpy_s()

`strcat()`, `strcpy()`는 버퍼 오버플로우를 일으킬 가능성이 있어, 안전하지 않은 함수로 간주되어 오류가 발생한다.

그래서 좀 더 안전한 함수인 각각 `strcat_s()`, `strcpy_s()`를 사용하기를 권장한다.


strlen(), strcat_s(), strcmp(), strcpy_s()

헤더 파일

// strlen(), strcat_s(), strcmp(), strcpy_s()를 사용하려면 아래 헤더 파일을 불러와야 한다.
#include <string.h>  // C 방식
#include <cstring> // C++ 방식

 

strlen()

• 문자열의 길이를 반환하는 함수

▶ 함수 원형

함수 원형(Function Prototype)
• 함수에 대한 정보를 컴파일러에게 미리 알리는 것
• 실제 구현이 아닌 컴파일러에 함수 이름, 반환 유형 및 매개변수를 알려주는 함수 선언
컴파일러가 함수를 정의하기 전에 함수를 호출하는 방법을 알려줄 수 있게 도와준다.
size_t strlen(const char* _String);

// strlen()이 char* 타입을 요구하는 이유
	// 문자열의 시작 주소에 접근해서 배열 안에 저장되어 있는 문자가 몇 개인지 체크하고 알려주는 함수이다.
	// 문자열은 배열의 초기화로써 사용되어야 하기 때문에 읽기 전용 메모리(ROM)에 존재한다.
	// 배열의 요소가 char_t이고, 배열의 이름이 곧 시작 주소이기 때문에 시작 주소는 char* 타입이다.
    
// strlen()이 const 포인터 타입을 요구하는 이유
	// 문자열의 주소를 전달하면 혹여나 값이 변경될 수도 있다는 불안 요소가 존재한다.
	// 이 함수는 문자열의 길이를 알려줄 뿐, 값을 수정할 의도가 없기 때문에 앞에 const가 붙는다.

 

▷ 예시

#include <cstring>

int main()
{
	// char 배열
	char cName1[10] = "Alice";
    int iLen1 = strlen(cName1); // 5
    
    char cName2[10] = "Al\0ice";
    iLen2 = strlen(cName2); // 5
    // **배열의 크기 ≠ 문자열의 길이**
    
    // const char*
    const char* cName3 = "Alice";
    iLen3 = strlen(cName3); // 5

	return 0;
}

`strlen()` 직접 구현해 보기

// 문자열의 주소값을 변경하지 않는다는 의미로 _nL 앞에 const를 붙인다.
unsigned int myStrlen(const char* _nL)
{
	int i = 0;
    while (true)
    {
    	// *(_nL + i)에서 i가 1일 때, 주소가 한 단계 증가하면 char형이므로 실제로는 1byte가 더해진다.
    	char c = *(_nL + i);  // char c = _nL[i];
        // 칸마다 접근하면서(한 칸, 두 칸, 세 칸, ⋯) 접근한 칸이 NULL이면 반복문을 빠져나오게 한다.
    	if ('\0' == c)
        {
        	break;
        }
        ++i;
    }
    
    return i;
}

int main()
{
	char cName[10] = "Alice";  // myStrlen()의 _nL[]와 main()의 cName[]은 "Alice"에 접근한다.
    int iLen = myStrlen(cName);

	return 0;
}

※ 비교 연산자 사용할 때 `rvalue == 변수` 형태로 코드 작성하는 습관 들이기
`c = '\0'`같은 형태로 작성하면 비교 연산자를 대입 연산자로 잘못 작성했을 때 오류가 발생하지 않아서 실수인지 모르고 넘어가는 일이 발생할 수 있다.

for문이 아닌 while문을 사용한 이유

unsigned int myStrlen(const char* _nL)
{
	int i = 0;
    while (true)
    {
		// 조건식이 참이므로 무조건 실행
    }
}

증감식으로 얼마를 증가시킬 건지, 무슨 연산자를 사용할 것인지와 같이 미리 정할 수 없는 부분이 있다.

반복문의 경우 while문 안에 조건식을 적지 않고 true를 사용하면 조건식이 항상 참이기 때문에 무조건 반복문이 실행된다.

덕분에 복잡한 부분을 배제한 채로 쉽게 반복문의 틀을 잡아나갈 수 있다.

▷ while문 수정

unsigned int myStrlen(const char* _nL)
{
	int i = 0;
    // NULL이면 반복문을 탈출하고, 접근한 칸이 NULL이 아니면 i를 증가시켜서 다음 칸으로 접근한다.
    while ('\0' != _nL[i])
    {
        ++i;
    }
    
    return i;
}
가독성이나 이해하기 쉬운 건 과정을 풀어서 작성한 코드인데 어차피 같은 의미이기 때문에 본인이 편한 방법을 선택하면 된다.

strcat()_s

• 문자열을 이어 붙여주는 함수이다.

• `strcat()_s`은 오버로딩된 함수로, 2종류가 있다.

함수 오버로딩 : 같은 함수명임에도 매개 변수(파라미터)의 자료형이 다르거나 자료형은 같아도 개수가 다른 함수를 만들 수 있는 기능

함수 오버로딩 시 컴파일러가 어떤 함수인지 구별을 할 수 있기 때문에 오류가 발생하지 않는다.

(※ 클래스에서 함수 오버라이딩과 전혀 다르지만 이름 때문에 헷갈리는 경우가 있으니 주의하기)

▶ 함수 원형

// 1. 인자를 3개 받는 경우
// strcat_s() 원형
errno_t strcat_s(char* _Destination, rsize_t _SizeInBytes, const char* _Source);
// strcat()과 달리 추가로 버퍼 크기를 매개변수로 받아 오버플로우를 방지한다.
// 배열 크기를 초과하면 오류를 반환한다.

// 2. 인자를 2개 받는 경우
// 템플릿 기반 strcat_s() 원형
errno_t strcat_s<_Size>(char (&_Destination)[_Size], const char* _Source);

 

1) 인자를 3개 받는 경우

함수 `(`에 커서를 놓고 단축키 <Ctrl> + <Shift> + <Space>를 누르면 함수를 사용하기 위해 필요한 요소들을 보여주는 창이 뜬다.

• 크기가 얼마인 목적지 공간에 어떤 문자열을 이어 붙인다.

Destination : 목적지
Source : 소스, 원본

① `char*_Destination` : 어디에 (목적지, 이어 붙임을 당하는 쪽)

• 다른 문자나 문자열을 붙이려면 이어 붙여지는 쪽은 수정 가능해야 한다.

 

② `rsize_t _SizeInWords` : 배열의 최대 개수(이어 붙이려는 목적지 공간의 크기)

• 배열의 최대 개수를 받는 이유 : 이어 붙일 쪽 배열의 최대 개수를 초과하면 안 되기 때문에 이를 방지하기 위해 복사받을 곳의 최대 공간을 입력받아서 확인해 준다.

배열 초과 시 예외 처리가 발생하므로 이어 붙여지는 쪽을 생각해서 공간 자체가 넉넉하게 잡아야 한다.

#include <cstring>

int main()
{
	char cString[10] = "abc";
	strcat_s(cString, 10, "defghijklmno");  // 배열 초과
    // abc 뒤에 NULL까지 생각해서 배열의 공간을 설정해야 한다.

	return 0;
}

 

배열의 최대 개수를 잘못 적으면 예외 처리나 오류가 발생하지 않고 경고만 발생한다.

시작 주소만 받아가서 주소로부터 몇 칸까지 괜찮은지 알 방법이 없기 때문에 컴파일러는 작성한 칸까지 가능하다고 믿고 그대로 실행한다.

이때 문법적으로는 오류가 발생하지 않으므로 주의해야 한다.

#include <cstring>

int main()
{
	char cString[10] = "abc";
	strcat_s(cString, 100, "defghijklmno");  // 배열 초과
    // abc 뒤에 NULL까지 생각해서 배열의 공간을 설정해야 한다.

	return 0;
}

 

③ `const char*_Source` : 무엇을 (이어 붙일 것)

• 이어 붙일 쪽은 원본을 수정하지 않고 읽기만 해야 되므로 const가 붙는다.

 

▷ 이어 붙여지는 쪽의 공간을 여유 있게 잡아놓은 경우

char cString[100] = "abc";
strcat_s(cString, 100, "def");  // abc 뒤에 def를 붙인다.

`abc` 뒤에 나머지는 전부 0으로 채워져 있지만 0은 문자로 채워진 것이 아니라 실제로는 빈 공간이므로 `abc` 바로 뒤에 `def`가 붙는다.

 

2) 인자를 2개 받는 경우

배열의 크기를 템플릿으로 받아가서 초과하는지 아닌지 자동으로 인식해 주는 방법이다.

직접 배열의 크기를 적는 것보다 알아서 배열의 개수가 초과되는지 체크해 주는 방법이 실수를 줄여줘서 더 좋다.

▷ 예시

#include <cstring>

int main()
{
    char cString[10] = "abc";

    // 템플릿 기반 strcat_s() 호출
    strcat_s(cString, "def");

    return 0;
}

`strcat_s()` 직접 구현해 보기

// 예외 처리가 발생하면 뜨는 창처럼 배열의 최대 개수를 초과했을 때 경고를 발생시키는 기능이다.
// 이는 특정 함수를 호출하도록 되어있는 매크로이다.
#include <assert.h>  // 경고 발생시키기
#include <iostream>

using namespace std;

unsigned int myStrlen(const char* _nL)
{
    int i = 0;
    while (true)
    {
        char c = *(_nL + i);  // char c = _nL[i];
        if ('\0' == c)
        {
            break;
        }
        ++i;
    }
    
    return i;
}

// 이어 붙인 최종 문자열의 길이가 목적지의 저장 공간을 넘어서면 예외 처리가 뜨게 해야 된다.
void myStrcat_s(char* _pDest, unsigned int _iBufferSize, const char* _pSrc)
{
    int iDestLen = myStrlen(_pDest);
    int iSrcLen = myStrlen(_pSrc);
    
    // 예외 처리 => 이어 붙인 최종 문자열의 길이가 목적지 저장 공간을 넘어서려는 경우
    if (_iBufferSize < iDestLen + iSrcLen + 1)  // Null 문자 공간까지 계산
    {
        assert(nullptr);
    }

    // 문자열 이어 붙이기
    // 1. Dest 문자열의 끝을 확인 (문자열이 이어 붙은 시작 위치)
    // abc 뒤에 NULL 문자('\0') 자리부터 문자를 붙여야 되므로 목적지 문자열의 끝을 확인해야 한다.
    //iDestLen;  // Dest 문자열의 끝 인덱스

    // 2. 반복적으로 Src 문자열을 Dest 끝 위치에 복사하기
    // 3. Src 문자열의 끝을 만나면 반복 종료
    // 반복문을 진행하면서 Src 문자열을 Dest 끝 위치에 복사하고, 반복 횟수는 _Source 문자열의 개수만큼이다.
    for (int i = 0; i < iSrcLen + 1; ++i)
    {
        _pDest[iDestLen + i] = _pSrc[i];
    }
}

int main()
{
    char cString[10] = "abc";
    myStrcat_s(cString, 10, "def");
    
    cout << cString;

    return 0;
}

▽ 출력 결과

abcdef

strcmp()

두 문자열을 비교하여 값을 반환하는 함수이다.

왼쪽의 우선순위가 높으면 -1을, 두 문자열이 일치하면 0을, 오른쪽의 우선순위가 높으면 1을 반환한다.

마치 저울처럼 -1, 0, 1 중에서 각각 상황에 맞게 값을 반환한다.

wcscmp값

우선순위가 높고 낮음은 사전과 아스키 코드를 생각해 보면 된다.

더 먼저 오는 것이 우선순위가 높다.

ex. `가` > `가나`, `a` > `b`

 

▶ 함수 원형

int strcmp(const char* _Str1, const char* _Str2)

▷ 예시

#include <cstring>

int main()
{
	// 1. 두 문자열이 일치할 때
	int iRet = strcmp("abc", "abc"); // 0

	// 2. 두 문자열의 길이가 다를 때
	// 길이가 짧은 쪽이 우선순위가 더 높다.
	iRet = strcmp("ab", "abc");  // -1
	iRet = strcmp("abc", "ab");  // 1

	// 3. 두 문자열의 길이가 같을 때
	// 아스키 코드를 보면 a가 b보다 우선순위가 높다.
	iRet = strcmp("abc", "cbc");  // -1
	iRet = strcmp("cbc", "abc");  // 1

	return 0;
}

'strcmp()` 직접 구현해보기

#include <assert.h>

unsigned int myStrlen(const char* _nL)
{
    int i = 0;
    while (true)
    {
        char c = *(_nL + i);  // char c = _nL[i];
        if ('\0' == c)
        {
            break;
        }
        ++i;
    }

    return i;
}

int myStrcmp(const char* _left, const char* _right)
{
    int leftLen = myStrlen(_left);
    int rightLen = myStrlen(_right);

    // 두 문자열 중 하나를 저장해놓는다.
    int iLoop = leftLen;  // 두 문자열이 같을 때 아래 if문을 실행시킬 필요가 없어진다.
    int iReturn = 0;

    // 두 문자열의 길이가 다를 때
    // 왼쪽 문자열이 더 작으면 -1을 반환한다.
    if(leftLen < rightLen)
    {
        iLoop = leftLen;
        iReturn = -1;
    }
    // 오른쪽 문자열이 더 작으면 -1을 반환한다.
    else if(leftLen > rightLen)
    {
        iLoop = rightLen;
        iReturn = 1;
    }

    // 반복문은 두 문자의 반복 횟수에 맞게 반복된다.
    for (int i = 0; i < iLoop; ++i)
    {
        // 왼쪽 문자가 오른쪽보다 작다면 -1을, 그렇지 않다면 1을 반환한다.
        if (_left[i] < _right[i])
        {
            return -1;
        }
        else if(_left[i] > _right[i])  // ※ else로 작성하면 작다를 제외한 크거나 "같은" 경우도 포함되어버린다.
        {
            return 1;
        }
    }

    return iReturn;  // -1, 0, 1 중에서 해당하는 것을 반환한다.
}

int main()
{
    int iRet = myStrcmp("abc", "ab");  // 1
    iRet = myStrcmp("abc", "abcdef");  // -1

    return 0;
}

strcpy_s()

• 문자열을 복사하는 함수

▶ 함수 원형

errno_t strcpy_s<_Size>(char* (&_Destination)[_Size], const char* _Source)

▷ 예시

#include <cstring>
#include <iostream>

using namespace std;

int main()
{
    char c[] = "Hello World!";
    char dest[30];
    strcpy_s(dest, c);

    cout << dest;

    return 0;
}

▽ 출력 결과

Hello World!

`strcpy_s()` 직접 구현해 보기

#include <iostream>

void myStrcpy_s(char* _pDest, const char* _pSrc)
{
    int i = 0;
    // _pSrc 문자열을 끝까지 순차적으로 복사한다.
    while (_pSrc[i] != '\0')
    {
        _pDest[i] = _pSrc[i];
        ++i;
    }
    // 마지막에 NULL 문자 추가
    _pDest[i] = '\0';
}

int main()
{
    char cSrc[] = "Hello";
    char cDest[50];  // 복사본을 저장할 배열
	
    cout << myStrcpy_s(cDest, cSrc);

    return 0;
}

▽ 출력 결과

Hello