null로 끝나는 문자열의 근거는 무엇입니까? 측면에서)를 위해 쓰여 졌음을 알고 있지만 효율성

C와 C ++를 좋아하는 한, null로 끝나는 문자열을 선택할 때 머리를 긁을 수는 없습니다.

  • C 앞에 존재하는 길이 접두사 (즉, 파스칼) 문자열
  • 길이 접두사 문자열은 일정한 시간 길이 조회를 허용하여 여러 알고리즘을 더 빠르게 만듭니다.
  • 접두사가 붙은 길이의 문자열은 버퍼 오버런 오류를 발생시키기 어렵습니다.
  • 32 비트 시스템에서도 문자열이 사용 가능한 메모리의 크기가되도록 허용하면 접 두부 길이가 null로 끝나는 문자열보다 3 바이트 만 넓습니다. 16 비트 시스템에서는 단일 바이트입니다. 64 비트 시스템에서 4GB는 적절한 문자열 길이 제한이지만,이를 기계어 크기로 확장하려는 경우에도 64 비트 시스템에는 일반적으로 여분의 7 바이트를 널 인수로 만드는 충분한 메모리가 있습니다. 나는 원래의 C 표준이 미친 듯이 가난한 기계 (메모리 측면에서)를 위해 쓰여 졌음을 알고 있지만 효율성 주장은 나를 여기에 팔지 않습니다.
  • 다른 모든 언어 (예 : Perl, Pascal, Python, Java, C # 등)는 접두사가 붙은 문자열을 사용합니다. 이러한 언어는 문자열 조작 벤치 마크에서 문자열보다 효율적이기 때문에 일반적으로 C를 능가합니다.
  • C ++은 std::basic_string템플릿으로 이것을 약간 수정 했지만 null로 끝나는 문자열을 기대하는 일반 문자 배열은 여전히 ​​널리 퍼져 있습니다. 힙 할당이 필요하기 때문에 불완전합니다.
  • 널 종료 문자열은 문자열에 존재할 수없는 문자 (즉, 널)를 예약해야하며, 접 두부 길이의 문자열에는 널이 포함될 수 있습니다.

이러한 것 중 일부는 C보다 최근에 밝혀 졌으므로 C가 알지 못하는 것이 합리적입니다. 그러나 C가 나오기 전에는 몇 가지가 분명했습니다. 명백하게 우수한 길이 접두사 대신 널 종료 문자열이 선택된 이유는 무엇입니까?

편집 : 일부는 위의 효율성 점에 대해 사실을 요구하고 (내가 이미 제공 한 것을 좋아하지 않기 때문에 ) 몇 가지 사항에서 비롯됩니다.

  • 널 종료 문자열을 사용하는 Concat에는 O (n + m) 시간 복잡성이 필요합니다. 길이 접두사는 종종 O (m) 만 필요합니다.
  • 널 종료 문자열을 사용하는 길이에는 O (n) 시간 복잡성이 필요합니다. 길이 접두사는 O (1)입니다.
  • 길이와 연결은 가장 일반적인 문자열 연산입니다. 널 종료 문자열이 더 효율적일 수있는 몇 가지 경우가 있지만 훨씬 덜 자주 발생합니다.

아래 답변에서 null로 끝나는 문자열이 더 효율적인 경우가 있습니다.

  • 문자열의 시작 부분을 잘라 내야하며 어떤 방법으로 전달해야 할 때. 길이 접두어가 정렬 규칙을 따라야하기 때문에 원래 문자열을 제거 할 수 있더라도 길이 접두사가있는 일정한 시간에 실제로이 작업을 수행 할 수 없습니다.
  • 문자열 문자를 문자별로 반복하는 경우 CPU 레지스터를 저장할 수 있습니다. 이것은 문자열을 동적으로 할당하지 않은 경우에만 작동합니다 (그런 다음 문자열을 해제해야하기 때문에 원래 malloc 및 친구들로부터 얻은 포인터를 유지하기 위해 저장 한 CPU 레지스터를 사용해야합니다).

위의 어느 것도 길이와 concat만큼 일반적이지 않습니다.

아래 답변에 더 많은 주장이 있습니다.

  • 줄 끝을 잘라 내야합니다

그러나 이것은 잘못되었습니다-null로 끝나고 길이가 앞에 붙은 문자열과 동일한 시간입니다. (널로 끝나는 문자열은 새 끝을 원할 경우 null을 붙이고 길이 접두사는 접두사에서 뺍니다.)



답변

로부터 말의 입

BCPL, B 또는 C는 언어에서 문자 데이터를 강력하게 지원하지 않습니다. 각각은 문자열을 정수 벡터처럼 취급하고 몇 가지 규칙으로 일반적인 규칙을 보완합니다. BCPL과 B 모두에서 문자열 리터럴은 문자열로 문자로 초기화 된 정적 영역의 주소를 셀로 묶습니다. BCPL에서 첫 번째 묶음 바이트에는 문자열의 문자 수가 포함됩니다. B에는 카운트가 없으며 문자열은 특수 문자로 끝나고 B는 철자가
*e됩니다. 이 변경은 8 비트 또는 9 비트 슬롯에 카운트를 유지함으로써 발생하는 문자열 길이의 제한을 피하기 위해 부분적으로 이루어졌으며, 일부에서는 카운트를 유지하는 것이 터미네이터를 사용하는 것보다 편리하지 않은 것으로 보였습니다.

Dennis M Ritchie, C 언어 개발


답변

C에는 언어의 일부로 문자열이 없습니다. C의 ‘문자열’은 문자에 대한 포인터 일뿐입니다. 어쩌면 당신은 잘못된 질문을하고있을 것입니다.

“문자열 유형을 제외하는 이유는 무엇입니까?”가 더 관련이있을 수 있습니다. 이를 위해 C는 객체 지향 언어가 아니며 기본 값 유형 만 있음을 지적합니다. 문자열은 다른 유형의 값을 결합하여 구현해야하는 상위 레벨 개념입니다. C는 더 낮은 추상화 레벨에 있습니다.

아래의 격렬한 스쿼시에 비추어 볼 때 :

나는 이것이 어리 석거나 나쁜 질문이라고 말하려고하지 않거나 문자열을 나타내는 C 방식이 최선의 선택이라고 지적하고 싶습니다. C에 문자열을 바이트 배열과 데이터 유형으로 구별하는 메커니즘이 없다는 사실을 고려하면 질문이 더 간결하게 표현 될 것임을 분명히하려고합니다. 오늘날 컴퓨터의 처리 및 메모리 성능 측면에서 이것이 최선의 선택입니까? 아마 아닙니다. 그러나 후시는 항상 20/20이며 그 모든 것 🙂


답변

질문은 Length Prefixed Strings (LPS)vs 로 요청됩니다zero terminated strings (SZ) 되지만 길이 접두사 문자열의 이점을 대부분 노출합니다. 압도적으로 보일지 모르지만 솔직히 말하면 LPS의 단점과 SZ의 장점도 고려해야합니다.

내가 이해하는 것처럼,이 질문은 “제로 종료 문자열의 장점은 무엇인가?”를 묻는 편견으로 이해 될 수도 있습니다.

Zero Terminated Strings의 장점 (나는 본다) :

  • 매우 간단하고 언어로 새로운 개념을 도입 할 필요가 없으며 char 배열 / char 포인터가 할 수 있습니다.
  • 핵심 언어에는 최소한의 구문 설탕이 포함되어있어 큰 따옴표 사이의 문자를 많은 문자 (실제로 많은 바이트)로 변환합니다. 경우에 따라 텍스트와 전혀 관련이없는 것을 초기화하는 데 사용할 수 있습니다. 예를 들어 xpm 이미지 파일 형식은 문자열로 인코딩 된 이미지 데이터를 포함하는 유효한 C 소스입니다.
  • 그건 그렇고, 당신 문자열 리터럴에 0을 넣을 수 있습니다 . 컴파일러는 리터럴 끝에 다른 것을 추가 할 것입니다 : "this\0is\0valid\0C". 문자열입니까? 또는 네 줄? 또는 많은 바이트 …
  • 플랫 구현, 숨겨진 간접, 숨겨진 정수 없음.
  • 숨겨진 메모리 할당이 필요하지 않습니다 (strdup과 같은 악명 높은 비표준 함수는 할당을 수행하지만 대부분 문제의 원인입니다).
  • 작거나 큰 하드웨어에는 특별한 문제가 없습니다 (8 비트 마이크로 컨트롤러에서 32 비트 접두사 길이를 관리 해야하는 부담 또는 문자열 크기를 256 바이트 미만으로 제한하는 제한을 상상해보십시오.
  • 문자열 조작의 구현은 아주 간단한 라이브러리 함수입니다.
  • 문자열의 주요 사용에 효율적 : 알려진 시작부터 순차적으로 읽은 상수 텍스트 (대부분 사용자에게 메시지).
  • 0을 종료하는 것도 필수는 아니며, 많은 바이트와 같이 문자를 조작하는 데 필요한 모든 도구를 사용할 수 있습니다. C에서 배열 초기화를 수행 할 때 NUL 종료자를 피할 수도 있습니다. 올바른 크기를 설정하십시오.char a[3] =
    "foo";
    유효한 C (C ++ 아님)이며 a에 마지막 0을 넣지 않습니다.
  • stdin, stdout과 같은 고유 길이가없는 “파일”을 포함하여 “모든 것이 파일입니다”라는 유닉스 관점과 일치합니다. 개방형 읽기 및 쓰기 프리미티브는 매우 낮은 수준으로 구현됩니다. 라이브러리 호출이 아니라 시스템 호출입니다. 바이너리 또는 텍스트 파일에 동일한 API가 사용됩니다. 파일 읽기 프리미티브는 버퍼 주소와 크기를 가져 와서 새 크기를 리턴합니다. 그리고 문자열을 버퍼로 쓸 수 있습니다. 다른 종류의 문자열 표현을 사용하면 출력하기 위해 버퍼로 리터럴 문자열을 쉽게 사용할 수 없거나 캐스팅 할 때 매우 이상한 동작을해야합니다.char* . 즉, 문자열의 주소를 반환하지 않고 실제 데이터를 반환합니다.
  • 쓸데없는 버퍼 사본없이 파일에서 내부에서 읽은 텍스트 데이터를 매우 쉽게 조작하고 올바른 위치에 0을 삽입하십시오 (물론 현대 C에서는 큰 따옴표로 묶인 문자열이 오늘날 일반적으로 수정할 수없는 데이터로 유지되는 const 문자 배열이므로 실제로 C는 아닙니다) 분절).
  • 어떤 크기의 int 값을 앞에 추가하면 정렬 문제가 발생합니다. 초기 길이는 정렬되어야하지만 문자 데이터에 대해 그렇게 할 이유는 없습니다 (다시 말해서 문자열을 강제로 정렬하면 바이트 묶음으로 처리 할 때 문제가 있음을 의미합니다).
  • 상수 리터럴 문자열 (sizeof)의 길이는 컴파일 타임에 알려져 있습니다. 그렇다면 왜 실제 데이터 앞에 추가하여 메모리에 저장하고 싶습니까?
  • C가 다른 사람처럼 (거의)하는 방식으로 문자열은 char 배열로 간주됩니다. 배열 길이는 C에 의해 관리되지 않으므로 논리 길이는 문자열에 대해서도 관리되지 않습니다. 유일한 놀라운 점은 끝에 0 항목이 추가되었지만 큰 따옴표 사이에 문자열을 입력 할 때 핵심 언어 수준입니다. 사용자는 길이를 통과하는 문자열 조작 함수를 완벽하게 호출하거나 대신 일반 memcopy를 사용할 수 있습니다. SZ는 단지 시설 일뿐입니다. 대부분의 다른 언어에서는 배열 길이가 관리되며 문자열과 동일한 논리입니다.
  • 어쨌든 1 바이트 문자 세트로는 충분하지 않으며 종종 문자 수가 바이트 수와 매우 다른 인코딩 된 유니 코드 문자열을 처리해야합니다. 사용자는 아마도 “크기”이상을 원할뿐 아니라 다른 정보도 원할 것입니다. 길이를 유지하면 이러한 다른 유용한 정보와 관련하여 아무 것도 사용하지 마십시오 (특히 보관할 자연 장소가 아님).

즉, 표준 C 문자열이 실제로 비효율적 인 드문 경우에는 불평 할 필요가 없습니다. 사지가 가능합니다. 그 추세를 따랐다면 표준 C에는 정규 표현식 지원 기능이 포함되어 있지 않다고 불평해야하지만 실제로는 그 목적으로 사용할 수있는 라이브러리가 있기 때문에 실제로는 문제가 아니라는 것을 알고 있습니다. 따라서 문자열 조작 효율성을 원할 때 bstring 과 같은 라이브러리를 사용하지 않는 이유는 무엇입니까? 또는 C ++ 문자열?

편집 : 최근에 D 문자열을 보았습니다. . 선택한 솔루션이 크기 접두사도 아니고 제로 종료도 아니라는 것을 알면 흥미 롭습니다. C에서와 같이 큰 따옴표로 묶인 리터럴 문자열은 변경 불가능한 문자 배열의 짧은 축약 형이며 언어는 또한 의미를 갖는 문자열 키워드를 갖습니다 (불변 문자 배열).

그러나 D 배열은 C 배열보다 훨씬 풍부합니다. 정적 배열의 경우 길이는 런타임에 알려져 있으므로 길이를 저장할 필요가 없습니다. 컴파일러는 컴파일 타임에 그것을 가지고 있습니다. 동적 배열의 경우 길이를 사용할 수 있지만 D 설명서에는 보관 위치가 나와 있지 않습니다. 우리가 아는 한, 컴파일러는 레지스터를 문자 데이터와 멀리 떨어진 일부 레지스터 또는 변수에 유지하도록 선택할 수 있습니다.

일반 문자 배열 또는 리터럴이 아닌 문자열에는 최종 0이 없으므로 프로그래머는 D에서 C 함수를 호출하려는 경우 자체적으로 입력해야합니다. 리터럴 문자열의 경우에는 D 컴파일러가 여전히 0을 입력합니다. 각 문자열의 끝 (C 문자열로 쉽게 캐스트하여 C 함수를 쉽게 호출 할 수 있도록)하지만이 0은 문자열의 일부가 아닙니다 (D는 문자열 크기로 계산하지 않습니다).

나를 다소 실망시킨 유일한 것은 문자열이 utf-8이어야한다는 것입니다. 그러나 멀티 바이트 문자를 사용할 때도 length는 여전히 많은 바이트 수를 반환합니다 (적어도 컴파일러 gdc에서는 사실입니다). 컴파일러 버그인지 또는 목적인지는 확실하지 않습니다. (OK, 아마 무슨 일이 일어 났는지 알았을 것입니다. D 컴파일러에게 소스를 사용하려면 utf-8을 사용하십시오. 처음에는 바보 같은 바이트 순서 표시를 넣어야합니다. 특히 UTF-8에 대해 편집기를하지 않는 것을 알고 있기 때문에 바보를 씁니다. 8은 ASCII와 호환되어야합니다).


답변

나는 역사적인 이유가 있으며 위키 백과 에서 이것을 발견했다고 생각합니다 .

C (및 그 언어가 파생 된 언어)가 개발 될 때 메모리는 극히 제한적 이었으므로 문자열 길이를 저장하는 데 1 바이트의 오버 헤드 만 사용하는 것이 매력적이었습니다. 당시에는 “Pascal string”(BASIC의 초기 버전에서도 사용됨)이라고하는 유일한 인기있는 대안은 문자열의 길이를 저장하기 위해 선행 바이트를 사용했습니다. 이렇게하면 문자열에 NUL이 포함될 수 있으며 길이에 단 하나의 메모리 액세스 (O (1) (일정) 시간) 만 있으면됩니다. 그러나 1 바이트는 길이를 255로 제한합니다.이 길이 제한은 C 문자열의 문제보다 훨씬 제한적이므로 일반적으로 C 문자열이 우세합니다.


답변

Calavera옳지 만 사람들이 요점을 알지 못하는 것처럼 코드 예제를 제공합니다.

먼저, C가 무엇인지 고려해 봅시다. 모든 언어가 기계 언어로 직접 변환되는 간단한 언어입니다. 모든 유형은 레지스터와 스택에 적합하며 운영 체제 또는 큰 런타임 라이브러리가 필요하지 않습니다. 이러한 것들을 작성 하기위한 것이기 때문에 (실행 하기에 가장 적합한 작업) 오늘날에는 경쟁자가 될 수 없습니다).

C가 있었다면 string같은 유형 int또는char , 그것은 레지스터 나 스택에 맞지 않았고, 어떤 방법으로 처리 할 수 (모든과의 지원 인프라를) 메모리 할당을 필요로 유형이 될 것입니다. 이 모든 것들은 C의 기본 원칙에 위배됩니다.

따라서 C의 문자열은 다음과 같습니다.

char s*;

자, 이것이 길이 접두사라고 가정 해 봅시다. 두 개의 문자열을 연결하는 코드를 작성해 봅시다.

char* concat(char* s1, char* s2)
{
    /* What? What is the type of the length of the string? */
    int l1 = *(int*) s1;
    /* How much? How much must I skip? */
    char *s1s = s1 + sizeof(int);
    int l2 = *(int*) s2;
    char *s2s = s2 + sizeof(int);
    int l3 = l1 + l2;
    char *s3 = (char*) malloc(l3 + sizeof(int));
    char *s3s = s3 + sizeof(int);
    memcpy(s3s, s1s, l1);
    memcpy(s3s + l1, s2s, l2);
    *(int*) s3 = l3;
    return s3;
}

또 다른 대안은 구조체를 사용하여 문자열을 정의하는 것입니다.

struct {
  int len; /* cannot be left implementation-defined */
  char* buf;
}

이 시점에서 모든 문자열 조작에는 두 가지 할당이 필요합니다. 실제로 라이브러리를 처리하기 위해 라이브러리를 통과한다는 의미입니다.

재미있는 것은입니다 … 같은 구조체 할이 C에 존재! 그것들은 사용자 처리에 메시지를 표시하는 데 사용되지 않습니다.

Calavera가 만드는 요점은 다음과 같습니다 .C에는 문자열 유형이 없습니다. . . 그것으로 무엇이든하려면 포인터를 가져 와서 두 가지 유형의 포인터로 디코딩해야합니다. 그런 다음 문자열의 크기와 관련이 있으며 “구현 정의”로 남길 수 없습니다.

이제 C 어쨌든 메모리를 처리 할 수mem 있으며 라이브러리 의 함수 ( <string.h>조차도!)는 메모리를 한 쌍의 포인터와 크기로 처리하는 데 필요한 모든 도구를 제공합니다. C에서 소위 “문자열” 은 단 하나의 목적, 즉 텍스트 터미널 용으로 운영 체제를 작성하는 맥락에서 메시지를 표시하기 위해 만들어졌습니다. 그리고이를 위해 null 종료면 충분합니다.


답변

분명히 성능과 안전을 위해 반복적으로 수행 strlen하거나 동등한 문자열을 사용하는 대신 작업하는 동안 문자열의 길이를 유지해야 합니다. 그러나 문자열 내용 바로 앞에 고정 된 위치에 길이를 저장하는 것은 매우 나쁜 디자인입니다. 요르겐이 Sanjit의 대답에 코멘트에서 지적했듯이, 그것은 예를 들어 같은 일반적인 작업을 많이하게 문자열로 문자열의 꼬리를 치료 배제 path_to_filename하거나 filename_to_extension새 메모리를 할당하는 (그리고 실패와 오류 처리의 가능성을 초래)없이 불가능 . 물론 문자열 길이 필드가 차지해야하는 바이트 수에 대해서는 아무도 동의 할 수없는 문제가 있습니다 (많은 잘못된 “파스칼 문자열”

C가 프로그래머가 길이를 저장할 위치 / 방법 / 방법을 훨씬 유연하고 강력하게 선택할 수 있도록 설계했습니다. 물론 프로그래머는 똑똑해야합니다. C는 충돌하거나 정지하거나 적에게 뿌리를 내리는 프로그램으로 어리 석음을 처벌합니다.


답변

모든 언어, 특히 어셈블리 위의 한 단계 인 C (따라서 많은 어셈블리 레거시 코드를 상속 함)의 어셈블리 내장을 고려하여 게으름, 레지스터 절약 및 이식성. ASCII 일에는 널 문자가 쓸모가 없다는 데 동의합니다 (아마도 EOF 제어 문자만큼 좋습니다).

의사 코드로 보자

function readString(string) // 1 parameter: 1 register or 1 stact entries
    pointer=addressOf(string)
    while(string[pointer]!=CONTROL_CHAR) do
        read(string[pointer])
        increment pointer

총 1 개의 레지스터 사용

사례 2

 function readString(length,string) // 2 parameters: 2 register used or 2 stack entries
     pointer=addressOf(string)
     while(length>0) do
         read(string[pointer])
         increment pointer
         decrement length

사용 된 총 2 개의 레지스터

당시에는 근시안적인 것처럼 보이지만 코드와 레지스터 (이때 PREMIUM, 알고있는 시간, 펀치 카드 사용)의 절약을 고려하십시오. 따라서 프로세서 속도를 kHz 단위로 계산할 수있는 속도가 빠를수록이 “해킹”은 등록이 필요없는 프로세서를 쉽게 처리 할 수있을 정도로 훌륭하고 휴대 성이 뛰어납니다.

인수를 위해 2 개의 공통 문자열 연산을 구현합니다.

stringLength(string)
     pointer=addressOf(string)
     while(string[pointer]!=CONTROL_CHAR) do
         increment pointer
     return pointer-addressOf(string)

복잡성 O (n) 여기서 대부분의 경우 PASCAL 문자열은 O (1)입니다. 문자열의 길이는 문자열 구조에 미리 추가되기 때문입니다 (이 작업은 이전 단계에서 수행되어야 함을 의미 함).

concatString(string1,string2)
     length1=stringLength(string1)
     length2=stringLength(string2)
     string3=allocate(string1+string2)
     pointer1=addressOf(string1)
     pointer3=addressOf(string3)
     while(string1[pointer1]!=CONTROL_CHAR) do
         string3[pointer3]=string1[pointer1]
         increment pointer3
         increment pointer1
     pointer2=addressOf(string2)
     while(string2[pointer2]!=CONTROL_CHAR) do
         string3[pointer3]=string2[pointer2]
         increment pointer3
         increment pointer1
     return string3

복잡성 O (n) 및 문자열 길이 앞에 추가해도 작업의 복잡성은 변경되지 않지만 시간이 3 배 단축됩니다.

반면 PASCAL 문자열을 사용하는 경우 레지스터 길이와 비트 엔디안을 고려하여 API를 다시 디자인해야합니다 .PASCAL 문자열은 길이가 1 바이트 (8 비트)에 저장되어 있기 때문에 255 자 (0xFF)의 알려진 제한을 얻었습니다 ), 더 긴 문자열 (16 비트-> 모든 것)을 원한다면 코드의 한 계층에서 아키텍처를 고려해야합니다. 더 긴 문자열을 원할 경우 대부분 호환되지 않는 문자열 API를 의미합니다.

예:

하나의 파일은 8 비트 컴퓨터에서 앞에 문자열 api로 작성 된 다음 32 비트 컴퓨터에서 읽어야합니다. 게으른 프로그램은 4 바이트가 문자열의 길이라고 생각한 다음 그 많은 메모리를 할당합니다. 그런 다음 그 많은 바이트를 읽으십시오. 또 다른 경우는 x86 (big endian)으로 PPC 32 바이트 문자열 read (little endian)가 될 것입니다. 물론 다른 하나가 작성되었음을 알지 못하면 문제가 발생합니다. 1 바이트 길이 (0x00000001)는 1 바이트 문자열을 읽기위한 16MB 인 16777216 (0x0100000)이됩니다. 물론 사람들은 하나의 표준에 동의해야하지만 16 비트 유니 코드조차도 거의 큰 엔디안을 얻지 못했다고 말할 것입니다.

물론 C도 문제가 있지만 여기서 제기 한 문제의 영향은 거의 없습니다.