null로 끝나는 문자열의 근거는 무엇입니까?


281

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을 붙이고 길이 접두사는 접두사에서 뺍니다.)


110
나는 항상 모든 C ++ 프로그래머가 자신의 문자열 라이브러리를 작성하는 것이 통과의 의식이라고 생각했습니다.
Juliet

31
합리적 설명을 기대하는 것에 대해 이것은 무엇입니까? 다음에 x86 또는 DOS에 대한 이론적 근거를 듣고 싶습니까? 내가 아는 한 최악의 기술이 이깁니다. 매번 그리고 최악의 문자열 표현.
jalf

4
왜 길이 접두사 문자열이 우수하다고 주장합니까? 결국 C는 null로 끝나는 문자열을 사용하여 다른 언어와 차별화되어 인기를 얻었습니다.
Daniel C. Sobral

44
@Daniel : C는 Von Neumann 머신에서 실행 가능한 프로그램을 간단하고 효율적이며 이식 가능하게 표현하고 Unix에 사용 되었기 때문에 인기를 얻었습니다. null로 끝나는 문자열을 사용하기로 결정했기 때문은 아닙니다. 좋은 디자인 결정이라면 사람들은 그 결정을 복사했을 것입니다. 그들은 확실히 C에서 거의 모든 것을 복사했습니다.
Billy ONeal

4
Concat은 문자열 중 하나를 파괴하면 길이 접두사가있는 O (m)입니다. 그렇지 않으면 같은 속도입니다. 가장 일반적으로 사용되는 C 문자열은 인쇄 및 스캔이었습니다. 이 두 가지 모두에서 널 종료는 하나의 레지스터를 저장하기 때문에 더 빠릅니다.
Daniel C. Sobral

답변:


195

로부터 말의 입

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

Dennis M Ritchie, C 언어 개발


12
또 다른 관련 인용문 : "... 문자열의 의미론은 모든 배열에 적용되는보다 일반적인 규칙에 의해 완전히 포괄되므로 결과적으로 언어를 설명하는 것이 더 간단합니다 ..."
AShelly

151

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

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

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

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


29
char *temp = "foo bar";C에서 유효한 진술입니다 ... 야! 그거 문자열이 아니야? null로 종료되지 않습니까?
Yanick Rochon

56
@ Yanick : 그것은 컴파일러에게 마지막에 null이있는 char 배열을 만들도록 지시하는 편리한 방법입니다. 그것은 '문자열'이 아닙니다
Robert S Ciaccio

28
@calavera : 그러나 단순히 "이 문자열 내용과 2 바이트 길이 접두사를 가진 메모리 버퍼 만들기"를 의미 한 것처럼,
Billy ONeal

14
@Billy : 'string'은 실제로 char에 대한 포인터이므로 바이트에 대한 포인터와 동일합니다. 처리하는 버퍼가 실제로 'string'이라는 것을 어떻게 알 수 있습니까? 이를 나타내려면 char / byte * 이외의 새로운 유형이 필요합니다. 아마도 구조체?
Robert S Ciaccio

27
@ calavera가 맞다고 생각합니다 .C에는 문자열에 대한 데이터 유형이 없습니다. 좋아, 문자열과 같은 문자 배열을 고려할 수 있지만 이것이 항상 문자열임을 의미하는 것은 아닙니다 (문자열의 경우 명확한 의미의 문자 시퀀스를 의미합니다). 이진 파일은 문자의 배열이지만 그 문자는 인간에게 아무런 의미가 없습니다.
BlackBear

106

질문은 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와 호환되어야합니다).


7
... 계속 ... 내가 생각하는 몇 가지 요점, 즉 "모든 것이 파일입니다"라는 주장이 잘못되었습니다. 파일은 순차적 액세스이며 C 문자열은 그렇지 않습니다. 최소한의 구문 설탕으로 길이 접두어를 수행 할 수도 있습니다. 여기서 합리적인 주장은 작은 (즉, 8 비트) 하드웨어에서 32 비트 접두사를 관리하려는 것입니다. 길이의 크기가 구현에 의해 결정된다고 말하면 간단히 해결할 수 있다고 생각합니다. 결국, 그것이하는 일 std::basic_string입니다.
Billy ONeal

3
@ 빌리 ONeal : 실제로 내 대답에는 두 가지 다른 부분이 있습니다. 하나는 '핵심 C 언어'의 일부에 관한 것이고 다른 하나는 표준 라이브러리가 제공해야하는 것에 관한 것입니다. 문자열 지원과 관련 하여 핵심 언어에는 하나의 항목 만 있습니다. 큰 따옴표로 묶인 바이트 수의 의미입니다. 나는 C 행동에 당신보다 행복하지 않습니다. 매번 닫는 바이트 묶음 끝에 0을 추가하면 마술처럼 느껴집니다. \0프로그래머가 암시적인 것이 아니라 그것을 원할 때 마지막에 선호하고 명시 적 입니다. 앞에 붙는 길이가 훨씬 나쁩니다.
kriss

2
@Billy ONeal : 그것은 사실이 아니며, 용도는 핵심과 라이브러리가 무엇인지에 관심을 갖습니다. 가장 큰 요점은 C를 사용하여 OS를 구현하는 것입니다. 해당 레벨에서는 사용 가능한 라이브러리가 없습니다. C는 임베디드 컨텍스트 나 종종 같은 종류의 제한이있는 프로그래밍 장치에도 자주 사용됩니다. 많은 경우에 Joes의의는 아마 오늘날의에서 C를 사용하지 않아야합니다 : "OK, 당신이 콘솔에서 원하는 아니오 너무의 나쁜 ... 콘솔이 있습니까???"
한국 표준 과학 연구원

5
@Billy "글쎄, 운영 체제를 구현하는 C 프로그래머의 .01 %에게는 좋습니다." 다른 프로그래머는 하이킹을 할 수 있습니다. C는 운영 체제를 작성하기 위해 만들어졌습니다.
Daniel C. Sobral

5
왜? 그것이 범용 언어라고 말했기 때문에? 그것을 쓴 사람들이 그것을 만들 때 무엇을하고 있 었는가? 생애 첫 몇 년 동안 무엇을 사용 했습니까? 그래서, 저와 동의하지 않는다고 말하는 것은 무엇입니까? 운영 체제를 작성하기 위해 작성된 범용 언어 입니다 . 그것을 거부합니까?
Daniel C. Sobral

61

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

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


2
@muntoo 흠 ... 호환성?
khachik

19
@muntoo : 엄청난 양의 기존 C 및 C ++ 코드를 손상시킬 수 있기 때문입니다.
Billy ONeal

10
@muntoo : 패러다임이왔다 갔다하지만 레거시 코드는 영원히 있습니다. C의 모든 미래 버전은 0으로 끝나는 문자열을 계속 지원해야합니다. 그리고 구식 방법을 사용할 수있는 한, 사람들이 잘 알고 있기 때문에 사람들이 계속 사용할 것입니다.
John Bode

8
@muntoo : 날 믿어, 때로는 할 수 있으면 좋겠다. 그러나 나는 여전히 파스칼 문자열보다 0으로 끝나는 문자열을 선호합니다.
John Bode

2
레거시에 대해 이야기하십시오 ... C ++ 문자열은 이제 NUL로 끝나야합니다.
Jim Balter

32

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 종료면 충분합니다.


2
1. +1. 2. 언어의 기본 동작이 길이 접두사를 사용하여 만들어 졌다면 더 쉽게 할 수있는 다른 것들이 있었을 것입니다. 예를 들어, 거기에있는 모든 캐스트는 strlen대신 전화를 걸 거나 친구에게 숨겨져 있었을 것 입니다. "구현하기까지 남겨 두는"문제에 관해서는, 접두사가 short대상 상자에있는 것이 무엇이든 말할 수 있습니다. 그러면 모든 캐스팅이 여전히 작동합니다. 3. 하루 종일 한 가지 또는 다른 시스템이 나빠 보이도록 고안된 시나리오를 생각 해낼 수 있습니다.
Billy ONeal

5
@Billy C가 라이브러리 사용을 최소화하거나 전혀 사용하지 않도록 설계되었다는 사실 외에도 라이브러리는 충분히 사실입니다. 예를 들어 프로토 타입의 사용은 초기에 일반적이지 않았습니다. 접두사를 말하면 short문자열의 크기 가 효과적으로 제한됩니다. 이것은 예리하지 않은 것 같습니다. 8 비트 BASIC 및 Pascal 문자열, 고정 크기 COBOL 문자열 및 이와 유사한 작업을 수행 한 나 자신은 무제한 크기 C 문자열의 거대한 팬이되었습니다. 오늘날 32 비트 크기는 실제 문자열을 처리하지만 해당 바이트를 조기에 추가하는 것은 문제가되었습니다.
Daniel C. Sobral

1
@ 빌리 : 우선, 다니엘 고마워요. 둘째, 빌리, 나는 아직도 당신이 여기서하고있는 요점을 놓치고 있다고 생각합니다. 나는 문자열 데이터 유형 접두어의 장단점을 장단점으로 주장하지 않습니다 . 내가 말하고 다니엘이 매우 분명하게 강조한 것은 C의 구현 에서 그 주장을 전혀 다루지 않기로 한 결정이 있었다는 것이다 . 기본 언어에 관한 한 문자열은 존재하지 않습니다. 문자열을 처리하는 방법에 대한 결정은 프로그래머에게 맡겨져 있으며 null 종료가 대중화되었습니다.
Robert S Ciaccio

1
나 +1 추가하고 싶은 한 가지 더; 당신이 제안하는 구조체는 실제 string타입을 향한 중요한 단계를 놓칩니다 . 그것은 문자를 알지 못합니다. "char"의 배열입니다 (기계 링고의 "char"는 "단어"가 인간이 문장에서 단어라고 부르는 것만 큼 많은 문자입니다). 문자열은 상위char 개념으로 인코딩 개념을 도입 한 경우 배열 위에 구현 될 수 있습니다 .
Frerich Raabe

2
@ DanielC.Sobral : 또한 언급 한 구조체에는 두 개의 할당이 필요하지 않습니다. 스택에있는 그대로 buf사용하거나 (할당 만 필요로 함) struct string {int len; char buf[]};유연한 배열 구성원으로 하나의 할당으로 전체를 사용 및 할당하고로 전달하십시오 string*. (또는 틀림없이 struct string {int capacity; int len; char buf[]};명백한 성능상의 이유로)
Mooing Duck

20

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

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


+1. 길이 접두사와 같은 것을 원하는 사람들이 어디서나 많은 "접착제 코드"를 쓰지 않아도되도록 길이를 저장할 표준 장소를 갖는 것이 좋을 것입니다.
Billy ONeal

2
문자열 데이터와 관련하여 가능한 표준 장소는 없지만 물론 별도의 지역 변수를 사용할 수 있습니다 (물론 후자가 편리하지 않고 전자가 너무 낭비가 아닌 경우 전달하지 않고 다시 계산) 또는 포인터가있는 구조 구조에 할당 목적으로 포인터를 "소유"하는지 또는 다른 곳에서 소유 한 문자열에 대한 참조인지 여부를 나타내는 플래그로 구성 할 수 있습니다. 당신을 맞는 구조의 문자열입니다.
R은 .. GitHub의 STOP은 ICE 돕기

13

모든 언어, 특히 어셈블리 위의 한 단계 인 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도 문제가 있지만 여기서 제기 한 문제의 영향은 거의 없습니다.


2
@deemoowoor : Concat : O(m+n)nullterm 문자열로, O(n)다른 곳 에서는 일반적입니다. 다른 곳에서는 O(n)널 용어 문자열의 길이 입니다 O(1). 조인 : 다른 곳에서는 O(n^2)nullterm 문자열로 O(n). null로 끝나는 문자열이 더 효율적인 경우가 있습니다 (즉, 포인터 케이스에 하나만 추가하십시오). concat과 길이가 가장 일반적인 작업입니다 (길이는 형식, 파일 출력, 콘솔 표시 등에 필요합니다). . 상각하기 위해 길이를 캐시하는 경우 길이를 O(n)문자열과 함께 저장해야한다고 주장했습니다.
Billy ONeal

1
오늘 코드 에서이 유형의 문자열은 비효율적이며 오류가 발생하기 쉽지만 콘솔 표시는 실제로 문자열을 효율적으로 표시하기 위해 문자열의 길이를 알 필요가 없으며 파일 출력은 실제로 문자열에 대해 알 필요가 없습니다 길이 (이동 중에 클러스터 만 할당) 및 현재 문자열 형식은 대부분 고정 문자열 길이에서 수행되었습니다. 어쨌든 C에서 O (n ^ 2) 복잡도를 갖는 경우 잘못된 코드를 작성해야합니다. O (n) 복잡도에서 코드를 작성할 수 있습니다
dvhh

1
@dvhh : 나는 n ^ 2를 말하지 않았다-나는 m + n이라고 말했다-여전히 선형이지만 연결을 수행하기 위해 원래 문자열의 끝을 찾아야하지만 길이 접두어는 탐색하지 않는다 필요합니다. (이것은 실제로 선형 시간을 요구하는 길이의 또 다른 결과입니다)
Billy ONeal

1
@ 빌리 ONeal : 단지 호기심에서 문자열 조작 함수 호출을 위해 현재 C 프로젝트 (약 50000 줄의 코드)를 grep했습니다. strlen 101, strcpy 및 variant (strncpy, strlcpy) : 85 (메시지, 묵시적 사본에 수백 개의 리터럴 문자열이 사용됨), strcmp : 56, strcat : 13 (6은 길이가 0 인 문자열로 연결되어 strncat을 호출 함) . 접두사 길이가 strlen에 대한 호출 속도를 높이지만 strcpy 또는 strcmp에 대한 호출 속도를 높이는 데 동의합니다 (strcmp API가 공통 접두사를 사용하지 않는 경우). 위의 의견과 관련하여 가장 흥미로운 것은 strcat이 매우 드물다는 것입니다.
kriss

1
@ supercat : 실제로는 아닙니다. 일부 구현을보십시오. 짧은 문자열은 짧은 스택 기반 버퍼 (힙 할당 없음)를 사용하고있을 때만 힙을 사용합니다. 그러나 아이디어를 실제로 라이브러리로 구현할 수 있습니다. 일반적으로 문제는 전체 디자인이 아닌 세부 사항에 도달 할 때만 나타납니다.
kriss

9

여러면에서 C는 원시적이었습니다. 그리고 나는 그것을 좋아했습니다.

어셈블리 언어보다 한 단계 높았 기 때문에 작성 및 유지 관리가 훨씬 쉬운 언어와 거의 동일한 성능을 제공합니다.

널 종료자는 간단하며 언어의 특별한 지원이 필요하지 않습니다.

되돌아 보면 그렇게 편리하지 않은 것 같습니다. 그러나 나는 80 년대에 어셈블리 언어를 사용했고 당시에는 매우 편리해 보였습니다. 소프트웨어가 지속적으로 발전하고 있으며 플랫폼과 도구가 점점 더 정교 해지고 있다고 생각합니다.


null로 끝나는 문자열에 대해 더 이상 원시적 인 것이 무엇인지 알 수 없습니다. 파스칼은 C보다 우선하며 길이 접두사를 사용합니다. 물론 문자열 당 256 자로 제한되었지만 16 비트 필드를 사용하면 대부분의 경우 문제를 해결했을 것입니다.
Billy ONeal

문자 수를 제한한다는 사실은 정확히 그런 일을 할 때 고려해야 할 문제 유형입니다. 예, 더 길게 만들 수 있지만 바이트는 중요합니다. 그리고 모든 경우에 16 비트 필드가 충분히 길어야합니까? 당신은 null-terminate가 개념적으로 원시적임을 인정해야합니다.
Jonathan Wood

10
문자열의 길이를 제한하거나 내용 (널 문자 없음)을 제한하거나 4-8 바이트 수의 추가 오버 헤드를 허용합니다. 무료 점심은 없습니다. 처음에는 null로 끝나는 문자열이 완벽하게 이해되었습니다. 어셈블리에서 때로는 문자열의 끝을 표시하기 위해 문자의 최상위 비트를 사용하여 1 바이트를 더 절약했습니다!
Mark Ransom

정확히, Mark : 무료 점심은 없습니다. 항상 타협입니다. 오늘날 우리는 같은 종류의 타협을 할 필요가 없습니다. 그러나 그 당시에는이 접근 방식이 다른 방법과 비슷해 보였습니다.
Jonathan Wood

8

C가 파스칼 방식으로 문자열을 길이로 접두어로 구현 한 순간을 가정하면 7 자 길이의 문자열은 3 자 문자열과 동일한 DATA TYPE입니까? 대답이 '예'인 경우 전자를 후자에 할당 할 때 컴파일러는 어떤 종류의 코드를 생성해야합니까? 문자열이 잘 리거나 자동으로 크기가 조정되어야합니까? 크기가 조정되면 스레드 안전을 위해 잠금으로 해당 작업을 보호해야합니까? C 접근 방식은 이러한 모든 문제를 다음과 같이 강화했습니다. :)


2
Err .. 아뇨. C 접근법에서는 7 자 긴 문자열을 3 자 긴 문자열에 할당 할 수 없습니다.
Billy ONeal

@ 빌리 ONeal : 왜 안돼? 이 경우 이해하는 한 모든 문자열은 동일한 데이터 유형 (char *)이므로 길이는 중요하지 않습니다. 파스칼과 달리. 그러나 그것은 길이 접두사 문자열의 문제보다는 파스칼의 한계였습니다.
Oliver Mason

4
@ 빌리 : 방금 크리스티안의 요점을 되풀이했다고 생각합니다. C는 이러한 문제를 전혀 다루지 않음으로써 이러한 문제를 처리합니다. 실제로 C의 관점에서 실제로 문자열의 개념을 포함하고 있다고 생각하고 있습니다. 포인터 일 뿐이므로 원하는대로 지정할 수 있습니다.
Robert S Ciaccio

2
** 매트릭스 : "문자열이 없습니다"와 같습니다.
Robert S Ciaccio

1
@ calavera : 그것이 어떻게 증명되는지 모르겠습니다. 길이 접두사를 사용하여 같은 방식으로 해결할 수 있습니다. 즉, 할당을 전혀 허용하지 않습니다.
Billy ONeal

8

어떻게 든 질문에 C에서 길이 접두사 문자열에 대한 컴파일러 지원이 없음을 암시한다는 것을 이해했습니다. 다음 예제는 적어도 다음과 같은 구문으로 문자열 길이가 컴파일 타임에 계산되는 자체 C 문자열 라이브러리를 시작할 수 있음을 보여줍니다.

#define PREFIX_STR(s) ((prefix_str_t){ sizeof(s)-1, (s) })

typedef struct { int n; char * p; } prefix_str_t;

int main() {
    prefix_str_t string1, string2;

    string1 = PREFIX_STR("Hello!");
    string2 = PREFIX_STR("Allows \0 chars (even if printf directly doesn't)");

    printf("%d %s\n", string1.n, string1.p); /* prints: "6 Hello!" */
    printf("%d %s\n", string2.n, string2.p); /* prints: "48 Allows " */

    return 0;
}

그러나 문자열 포인터를 구체적으로 해제 할 때와 정적으로 할당 될 때 (리터럴 char배열) 조심해야하므로 문제가 발생하지 않습니다 .

편집하다: 질문에 대한 더 직접적인 대답으로, 내 견해는 C가 필요한 경우 문자열 길이를 사용할 수있는 (컴파일 시간 상수로) 지원 할 수있는 방법이지만 여전히 사용하려는 경우 메모리 오버 헤드가없는 것입니다 포인터 만 제로 종료.

물론 표준 라이브러리는 문자열 길이를 인수로 사용하지 않으며 길이 추출이 코드처럼 간단하지 않기 때문에 제로 종료 문자열로 작업하는 것이 좋습니다 char * s = "abc".


문제는 라이브러리가 구조체의 존재를 알지 못하고 임베드 된 null과 같은 것을 여전히 잘못 처리한다는 것입니다. 또한 이것은 실제로 내가 묻는 질문에 대답하지 않습니다.
Billy ONeal

1
사실입니다. 따라서 더 큰 문제는 일반 오래된 0으로 끝나는 문자열보다 문자열 매개 변수를 인터페이스에 제공하는 더 좋은 표준 방법이 없다는 것입니다. 필자는 여전히 포인터 길이 쌍으로 피드를 지원하는 라이브러리가 있다고 주장합니다 (적어도 적어도 C ++ std :: string을 구성 할 수 있음).
Pyry Jahkola

2
길이를 저장하더라도 널이 포함 된 문자열을 허용해서는 안됩니다. 이것은 기본적인 상식입니다. 데이터에 null이있는 경우 문자열이 필요한 함수와 함께 사용해서는 안됩니다.
R .. GitHub STOP HELPING ICE

1
@ supercat : 보안의 관점에서 그 중복성을 환영합니다. 그렇지 않으면 무지 (또는 수면 박탈) 프로그래머는 이진 데이터와 문자열을 연결하고 기대하는 것들로 전달 결국 [널 종료] 문자열 ...
R .. GitHub의 STOP 돕기 ICE

1
@R .. : null로 끝나는 문자열을 기대하는 메소드는 일반적으로을 기대하지만 null로 끝나지 char*않는 많은 메소드는을 기대합니다 char*. 형식을 분리하면 더 큰 이점은 유니 코드 동작과 관련이 있습니다. 문자열 구현에 문자열이 특정 종류의 문자를 포함하는 것으로 알려져 있거나 포함하지 않는 것으로 알려진 플래그를 유지하는 것이 좋습니다 (예 : 포함하지 않는 것으로 알려진 백만 자 문자열에서 999,990 번째 코드 포인트 찾기) 기본 다국어 비행기 이외의 문자는

6

"32 비트 시스템에서도 문자열을 사용 가능한 메모리의 크기로 허용하면 접두어가 붙은 길이는 널 종료 문자열보다 3 바이트 만 넓습니다."

첫째, 짧은 문자열의 경우 3 바이트가 추가 될 수 있습니다. 특히 길이가 0 인 문자열은 이제 4 배 많은 메모리를 사용합니다. 우리 중 일부는 64 비트 컴퓨터를 사용하므로 길이가 0 인 문자열을 저장하려면 8 바이트가 필요하거나 문자열 형식이 플랫폼이 지원하는 가장 긴 문자열을 처리 할 수 ​​없습니다.

처리해야 할 정렬 문제가있을 수도 있습니다. "solo \ 0second \ 0 \ 0four \ 0five \ 0 \ 0seventh"와 같이 7 개의 문자열을 포함하는 메모리 블록이 있다고 가정합니다. 두 번째 문자열은 오프셋 5에서 시작합니다. 하드웨어에서 32 비트 정수를 4의 배수 인 주소에 정렬해야 할 수도 있으므로 패딩을 추가하여 오버 헤드를 더 증가시켜야합니다. C 표현은 비교할 때 매우 메모리 효율적입니다. (메모리 효율성은 우수합니다. 예를 들어 캐시 성능에 도움이됩니다.)


나는이 모든 문제를 해결했다고 생각합니다. 예, x64 플랫폼에서 32 비트 접두사는 모든 가능한 문자열에 맞지 않을 수 있습니다. 다른 한편으로, 당신은 널 종료 문자열만큼 큰 문자열을 원하지 않습니다. 왜냐하면 당신이 할 수있는 거의 모든 작업의 ​​끝을 찾기 위해 40 억 바이트를 모두 검사해야하기 때문입니다. 또한 null로 끝나는 문자열이 항상 악하다고 말하는 것은 아닙니다. 이러한 블록 구조 중 하나를 구축하고 특정 응용 프로그램이 그러한 종류의 구성에 의해 속도가 빨라지면 갈수록 좋습니다. 나는 언어의 기본 행동이 그렇게하지 않기를 바랍니다.
Billy ONeal

2
내 견해로는 효율성 문제를 과소 평가했기 때문에 귀하의 질문에 해당 부분을 인용했습니다. 이중 또는 4 배의 메모리 요구 사항 (각각 16 비트 및 32 비트)은 큰 성능 비용이 될 수 있습니다. 긴 문자열은 느릴 수 있지만 최소한 지원되며 여전히 작동합니다. 정렬에 관한 나의 다른 요점은 전혀 언급하지 않습니다.
Brangdon

UCHAR_MAX 이외의 값은 바이트 액세스 및 비트 시프 팅을 사용하여 압축 및 압축 해제 된 것처럼 동작하도록 지정하여 정렬을 처리 할 수 ​​있습니다. 적절하게 설계된 문자열 유형은 0으로 끝나는 문자열과 본질적으로 유사한 스토리지 효율성을 제공하면서 추가 메모리 오버 헤드없이 버퍼에 대한 경계 검사를 허용 할 수 있습니다 (버퍼에 "비트가 가득 참"인지 말하기 위해 접두사에서 1 비트 사용). 그렇지 않고 마지막 바이트가 0이 아닌 경우 해당 바이트는 나머지 공간을 나타냅니다 버퍼가 가득
차지

...이 공간 내에 사용되지 않은 바이트의 정확한 수를 저장할 수 있으며 추가 메모리 비용은 없습니다.) 접두사로 작업하는 비용은 문자열 길이를 전달하지 않고도 fgets ()와 같은 메서드를 사용할 수있는 기능으로 인해 상쇄됩니다 (버퍼가 얼마나 큰지 알고 있으므로).
supercat

4

널 종료는 빠른 포인터 기반 작업을 허용합니다.


5
응? 길이 접두사와 함께 작동하지 않는 "빠른 포인터 작업"은 무엇입니까? 더 중요한 것은 길이 접두사를 사용하는 다른 언어는 C wrt 문자열 조작보다 빠릅니다.
Billy ONeal

12
@ billy : 길이 접두사 문자열을 사용하면 문자열 포인터를 가져 와서 4를 추가 할 수 없으며 길이 접두사가 없으므로 유효한 문자열이 될 것으로 기대합니다 (어쨌든 유효하지 않음).
Jörgen Sigvardsson

3
@j_random_hacker : asciiz 문자열 (연결 가능성 O (n) 대신 O (m + n))의 연결이 훨씬 나쁘고 concat은 여기에 나열된 다른 작업보다 훨씬 일반적입니다.
Billy ONeal

3
null로 끝나는 문자열로 더 비싸지는 작고 작은 작업이 있습니다 strlen. 나는 그것이 약간의 단점이라고 말합니다.
jalf

10
@ 빌리 ONeal : 다른 사람 도 정규식을 지원합니다. 그래서 무엇? 그들이 만든 라이브러리를 사용하십시오. C는 배터리를 포함하지 않고 최대 효율과 미니멀리즘에 관한 것입니다. C 도구를 사용하면 구조체를 사용하여 길이 접두사 문자열을 매우 쉽게 구현할 수 있습니다. 또한 길이와 문자 버퍼를 관리하여 문자열 조작 프로그램을 구현하는 것을 금지하는 것은 없습니다. 그것은 일반적으로 효율성을 원하고 C를 사용할 때하는 일이지만 char 버퍼의 끝에서 0을 기대하는 소수의 함수를 호출하는 것은 문제가되지 않습니다.
kriss

4

한 가지 언급되지 않은 점 : C가 설계 될 때 'char'가 8 비트가 아닌 많은 기계가있었습니다 (오늘날 DSP 플랫폼이없는 경우도 있음). 문자열의 길이를 접두사로 결정하면 몇 개의 'char'길이의 접두사를 사용해야합니까? 2를 사용하면 8 비트 문자 및 32 비트 주소 공간이있는 시스템의 문자열 길이에 인공 제한이 적용되는 반면 16 비트 문자 및 16 비트 주소 공간이있는 시스템의 공간은 낭비됩니다.

임의의 길이의 문자열을 효율적으로 저장하고 싶고 'char'가 항상 8 비트 인 경우 속도와 코드 크기가 약간의 비용으로 스키마가 짝수로 시작되는 문자열을 정의 할 수 있습니다 N은 N / 2 바이트 길이이고 홀수 값 N과 짝수 값 M (뒤로 읽기)이 앞에 붙는 문자열은 ((N-1) + M * char_max) / 2 등이 될 수 있습니다. 문자열을 보유하기 위해 일정한 양의 공간을 제공한다고 주장하면 해당 공간 앞에 충분한 바이트가 있어야 최대 길이를 처리 할 수 ​​있습니다. 그러나 'char'가 항상 8 비트는 아니라는 사실은 문자열의 길이를 유지하는 데 필요한 'char'의 수가 CPU 아키텍처에 따라 다르기 때문에 이러한 체계를 복잡하게 만듭니다.


접두사는 그대로 구현 정의 크기 일 수 있습니다 sizeof(char).
Billy ONeal

@BillyONeal : sizeof(char)하나입니다. 항상. 접두사가 구현 정의 크기 일 수는 있지만 어색합니다. 또한 "올바른"크기가 무엇인지 알 수있는 실제적인 방법은 없습니다. 4 개의 문자열을 많이 보유하고 있다면, 제로 패딩은 25 % 오버 헤드를, 4 바이트 길이 접두사는 100 % 오버 헤드를 부과합니다. 또한 4 바이트 길이 접두어를 패킹하고 압축을 푸는 데 소요되는 시간은 0 바이트에 대해 4 바이트 문자열을 스캔하는 비용을 초과 할 수 있습니다.
supercat

1
아 예. 네가 옳아. 접두사는 char이 아닌 다른 것이 될 수 있습니다. 대상 플랫폼에서 정렬 요구 사항을 충족시키는 모든 것이 좋습니다. 나는 거기에 가지 않을 것입니다-나는 이것을 이미 죽음으로 주장했습니다.
Billy ONeal

문자열의 길이가 접두사로 가정되면 가장 어리석은 일이 접두사 일 것입니다 size_t(메모리 낭비 는 손상 수 있습니다. 메모리 가 손상 수있는 가능한 모든 길이의 문자열을 허용합니다). 사실, 그건 종류의 D가하는 일; 배열은 struct { size_t length; T* ptr; }이고 문자열은 단지 배열입니다 immutable(char).
Tim Čas

@ TimČas : 문자열을 단어로 정렬해야하는 경우가 아니라면, 짧은 문자열로 작업하는 데 드는 비용은 길이를 포장하고 포장을 풀어야하는 요구 사항에 따라 많은 플랫폼에서 사용됩니다. 나는 그것이 실용적이라고 생각하지 않습니다. 문자열이 내용에 독립적 인 임의 크기의 바이트 배열이되도록하려면 길이를 문자 데이터에 대한 포인터와 분리하고 언어를 사용하여 리터럴 문자열에 대한 두 가지 정보를 얻을 수있는 것이 좋습니다 .
supercat

2

C를 둘러싼 많은 디자인 결정은 원래 구현되었을 때 매개 변수 전달이 다소 비싸다는 사실에서 비롯됩니다. 예를 들어

void add_element_to_next(arr, offset)
  char[] arr;
  int offset;
{
  arr[offset] += arr[offset+1];
}

char array[40];

void test()
{
  for (i=0; i<39; i++)
    add_element_to_next(array, i);
}

void add_element_to_next(ptr)
  char *p;
{
  p[0]+=p[1];
}

char array[40];

void test()
{
  int i;
  for (i=0; i<39; i++)
    add_element_to_next(arr+i);
}

후자는 두 개가 아닌 하나의 매개 변수 만 전달하면되므로 약간 저렴했습니다. 호출되는 메소드가 배열의 기본 주소 나 그 안의 인덱스를 알 필요가 없으면 두 포인터를 결합하는 단일 포인터를 전달하는 것이 값을 개별적으로 전달하는 것보다 저렴합니다.

C가 문자열 길이를 인코딩 할 수있는 합리적인 방법이 많이 있지만, 그 때까지 발명 된 접근법에는 문자열의 기본 주소를 수락하기 위해 문자열의 일부를 사용하여 작동 할 수있는 모든 필요한 기능이 있습니다. 두 개의 개별 매개 변수로 원하는 색인. 0 바이트 종료를 사용하면 해당 요구 사항을 피할 수있었습니다. 오늘날의 머신에서는 다른 접근법이 더 좋을 수도 있지만 (현대 컴파일러는 종종 레지스터의 매개 변수를 전달하고, memcpy는 strcpy ()와 동등한 기능을 사용할 수없는 방식으로 최적화 할 수 있습니다) 충분한 프로덕션 코드는 0 바이트로 끝나는 문자열을 사용하여 다른 것으로 변경하기 어렵습니다.

PS-- 일부 작업에서 약간의 속도 저하와 긴 문자열에서 약간의 추가 오버 헤드가 발생하는 대신, 문자열에서 작동하는 메서드가 문자열, 바운드 검사 문자열 버퍼에 대한 포인터를 직접 받거나 다른 문자열의 하위 문자열을 식별하는 데이터 구조 "strcat"과 같은 함수는 [modern syntax]와 비슷했을 것입니다.

void strcat(unsigned char *dest, unsigned char *src)
{
  struct STRING_INFO d,s;
  str_size_t copy_length;

  get_string_info(&d, dest);
  get_string_info(&s, src);
  if (d.si_buff_size > d.si_length) // Destination is resizable buffer
  {
    copy_length = d.si_buff_size - d.si_length;
    if (s.src_length < copy_length)
      copy_length = s.src_length;
    memcpy(d.buff + d.si_length, s.buff, copy_length);
    d.si_length += copy_length;
    update_string_length(&d);
  }
}

K & R strcat 방법보다 약간 크지 만 K & R 방법에서는 지원하지 않는 범위 검사를 지원합니다. 또한 현재 방법과 달리 임의의 하위 문자열을 쉽게 연결할 수 있습니다.

/* Concatenate 10th through 24th characters from src to dest */

void catpart(unsigned char *dest, unsigned char *src)
{
  struct SUBSTRING_INFO *inf;
  src = temp_substring(&inf, src, 10, 24);
  strcat(dest, src);
}

temp_substring에 의해 반환 된 문자열의 수명을 가진 자에 의해 제한 될 수 있습니다 ssrc방법에 필요한 이유를 적이다 (짧은이었다,inf - 그것은 지역 인 경우 메소드가 돌아 왔을 때, 그것은 죽을 것 건네지).

메모리 비용과 관련하여 최대 64 바이트의 문자열 및 버퍼는 1 바이트의 오버 헤드를 갖습니다 (0으로 끝나는 문자열과 동일). 더 긴 문자열은 약간 더 많을 것입니다 (두 바이트 사이에 허용되는 오버 헤드 양과 필요한 최대 시간 / 공간 상충 관계 여부). 길이 / 모드 바이트의 특수 값은 문자열 함수에 플래그 바이트, 포인터 및 버퍼 길이 (다른 문자열로 임의로 색인 할 수있는)를 포함하는 구조가 제공되었음을 나타내는 데 사용됩니다.

물론 K & R은 그러한 것을 구현하지 않았지만, 오늘날 많은 언어가 빈약 해 보이는 문자열 처리에 많은 노력을 기울이고 싶지 않았기 때문일 가능성이 높습니다.


단일 매개 변수로 여전히 전달 될 수 char* arr있는 양식 struct { int length; char characters[ANYSIZE_ARRAY] };또는 유사한 구조를 가리 키지 못하게하는 것은 없습니다 .
Billy ONeal

@BillyONeal :이 접근 방식의 두 가지 문제 : (1) 문자열 전체를 전달할 수 있지만 현재 접근 방식은 문자열의 꼬리를 전달할 수도 있습니다. (2) 작은 줄과 함께 사용하면 상당한 공간을 낭비하게됩니다. 만약 K & R이 문자열에 시간을 보내고 싶었다면 훨씬 더 강력 해졌지만 10 년 후 새로운 언어가 사용될 것이라고 생각하지 않습니다.
supercat

1
전화 컨벤션에 관한이 내용은 실제와 무관 한 공정한 이야기입니다 ... 디자인에서는 고려되지 않았습니다. 그리고 레지스터 기반 호출 규칙은 이미 "발명"되었습니다. 또한 두 포인터와 같은 접근 방식은 구조체가 일류가 아니기 때문에 옵션이 아니 었습니다 ... 기본 요소 만 할당하거나 전달할 수있었습니다. struct 복사는 UNIX V7까지 도착하지 않았습니다. 문자열 포인터를 복사하기 위해 memcpy (존재하지 않은)가 필요하다는 것은 농담입니다. 언어 디자인을 예쁘게 만들고 있다면 격리 된 함수뿐만 아니라 전체 프로그램을 작성해보십시오.
Jim Balter

1
"문자열 처리에 많은 노력을 기울이고 싶지 않았기 때문일 것입니다." 초기 UNIX의 전체 응용 프로그램 도메인은 문자열 처리였습니다. 그렇게하지 않았다면, 우리는 결코 들어 보지 못했을 것입니다.
짐 발터

1
"문자 버퍼가 길이를 포함하는 정수로 시작하는 것은 더 이상 마술 적이라고 생각하지 않습니다."- str[n]올바른 문자를 참조하려는 경우입니다. 이것들은 이것을 논의하는 사람들이 생각하지 않는 종류의 것입니다 .
짐 발터

2

Joel Spolsky에 따르면 이 블로그 게시물 ,

UNIX와 C 프로그래밍 언어가 발명 된 PDP-7 마이크로 프로세서에 ASCIZ 문자열 유형이 있기 때문입니다. ASCIZ는 "끝에 Z가 0 인 ASCII"를 의미했습니다.

여기에있는 다른 모든 대답을 본 후에, 이것이 사실이더라도 C가 null로 끝나는 "문자열"을 갖는 이유의 일부 일 뿐이라고 확신합니다. 그 포스트는 문자열과 같은 간단한 것들이 실제로 어려울 수있는 방법에 대해 잘 설명하고 있습니다.


2
봐요, 나는 많은 것을 요엘에게 존경합니다. 그러나 이것은 그가 추측하는 곳입니다. Hans Passant의 답변은 C의 발명가들로부터 직접 온 것입니다.
Billy ONeal

1
그렇습니다. 그러나 Spolsky의 말이 사실이라면, 그들이 언급 한 "편의"의 일부 였을 것입니다. 이것이 부분적으로 내가이 답변을 포함시킨 이유입니다.
BenK

AFAIK .ASCIZ는 일련의 바이트를 빌드하기위한 어셈블러 명령문 일뿐 0입니다. 그것은 그 당시 제로 종료 문자열 이 잘 확립 된 개념이라는 것을 의미합니다 . 그것은 0으로 끝나는 문자열이 PDP- *의 아키텍처와 관련이 있다는 것을 의미 하지는 않습니다 . 단 MOVB, BNE(바이트 복사 ) 및 (마지막으로 복사 된 바이트가 0이 아닌 경우 분기 ) 로 구성된 단단한 루프를 작성할 수 있습니다.
Adrian W

C가 오래되고 연약하고 낡은 언어임을 보여줍니다.
purec

2

반드시 이론적 근거는 아니지만 길이로 인코딩 된 대응점

  1. 특정 길이의 동적 길이 인코딩은 메모리에 관한 한 정적 길이 인코딩보다 우수하며 모두 사용에 따라 다릅니다. 증명을 위해 UTF-8을 살펴보십시오. 본질적으로 단일 문자를 인코딩하기위한 확장 가능한 문자 배열입니다. 확장 된 각 바이트마다 단일 비트를 사용합니다. NUL 종료는 8 비트를 사용합니다. 길이 접두사 64 비트를 사용하여 무한 길이라고도 할 수 있다고 생각합니다. 여분의 비트를 얼마나 자주 치는가가 결정 요인입니다. 1 개의 매우 큰 줄만? 8 비트 또는 64 비트를 사용중인 경우 누가 신경 쓰나요? 많은 작은 문자열 (즉, 영어 단어의 문자열)? 그런 다음 접두사 비용이 큰 비율입니다.

  2. 시간을 절약 할 수있는 길이 접두사 문자열 은 실제가 아닙니다 . 제공된 데이터의 길이를 제공해야하는지 여부, 컴파일 타임을 세거나 문자열로 인코딩해야하는 동적 데이터가 제공되고 있습니다. 이 크기는 알고리즘의 어느 시점에서 계산됩니다. 널 종료 문자열의 크기를 저장하는 별도의 변수를 제공 할 수 있습니다. 시간 절약에 대한 비교가 무의미합니다. 하나는 끝에 여분의 NUL을 가지고 있지만 길이 인코딩에 해당 NUL이 포함되어 있지 않으면 둘 사이에 아무런 차이가 없습니다. 알고리즘 변경이 전혀 필요하지 않습니다. 프리 패스는 컴파일러 / 런타임 대신 수동으로 직접 디자인해야합니다. C는 주로 수동으로 일하는 것입니다.

  3. 선택적인 길이 접두사는 판매 포인트입니다. 알고리즘에 대한 추가 정보가 항상 필요한 것은 아니므로 모든 문자열에 대해 알고리즘을 수행해야하므로 사전 계산 + 계산 시간이 O (n) 아래로 떨어질 수 없습니다. (즉, 하드웨어 난수 생성기 1-128. "무한 문자열"에서 가져올 수 있습니다. 문자가 너무 빨리 생성된다고 가정 해 봅시다. 따라서 문자열 길이는 항상 변경됩니다.하지만 데이터를 어떻게 사용하든 상관 없습니다. 내가 가지고있는 많은 임의의 바이트. 요청 후 얻을 수있는 즉시 사용 가능한 다음 바이트를 원합니다. 장치를 기다리고있을 수 있지만 미리 읽은 문자 버퍼를 가질 수도 있습니다. 길이 비교는 불필요한 계산 낭비입니다 .null 검사가 더 효율적입니다.)

  4. 길이 접두사는 버퍼 오버 플로우를 방지하는 좋은 방법입니까? 라이브러리 함수와 구현의 올바른 사용법입니다. 잘못된 데이터를 전달하면 어떻게됩니까? 내 버퍼는 2 바이트 길이이지만 함수에 7이라고 말합니다! 예는 : 만약이 도착 () 는 버퍼와 컴파일 테스트 된 내부 버퍼 확인 했어 수 알려진 데이터를 사용하기위한 한 ()의 malloc을통화하고 여전히 사양을 따릅니다. 알 수없는 STDIN이 알 수없는 버퍼에 도달하기위한 파이프로 사용되도록 의도 된 경우 길이 arg가 무의미하다는 것을 의미하는 버퍼 크기를 알 수는 없지만 분명히 카나리아 검사와 같은 다른 것이 필요합니다. 그 문제에 대해서는 일부 스트림과 입력의 길이를 접두사로 붙일 수 없으며 단지 할 수 없습니다. 즉, 길이 검사는 알고리즘에 내장되어야하며 타이핑 시스템의 마술 부분이 아닙니다. TL; DR NUL- 종결은 안전하지 않아야했으며, 오용으로 인해 결국 막을 내 렸습니다.

  5. 카운터 카운터 포인트 : NUL 종료는 바이너리에서 성가시다. 여기서 길이 접두사를 사용하거나 이스케이프 코드, 범위 다시 매핑 등의 방식으로 NUL 바이트를 변환해야합니다. 물론 더 많은 메모리 사용 / 정보 감소 / 바이트 당 작업 수를 의미합니다. 길이 접두사는 대부분 여기서 전쟁에서 승리합니다. 변환의 유일한 단점은 길이 접두사 문자열을 다루기 위해 추가 함수를 작성할 필요가 없다는 것입니다. 이는보다 최적화 된 sub-O (n) 루틴에서 더 많은 코드를 추가하지 않고도 자동으로 O (n) 동등 물로 작동하도록 할 수 있음을 의미합니다. 단점은 물론 NUL 무거운 줄에 사용될 때 시간 / 메모리 / 압축 폐기물입니다. 바이너리 데이터에서 작동하기 위해 중복되는 라이브러리의 양에 따라 길이 접두사 문자열로만 작동하는 것이 좋습니다. 즉, 길이 접두사 문자열로도 동일한 작업을 수행 할 수 있습니다 ...- 1 길이는 NUL 종료를 의미 할 수 있으며 길이 종료 내부에서 NUL 종료 문자열을 사용할 수 있습니다.

  6. Concat : "O (n + m) vs O (m)" 나는 당신이 m을 연결 후 문자열의 총 길이로 언급한다고 가정하고 있습니다. 둘 다 최소한 그 수의 연산을 가져야하기 때문입니다 (단지 압정 할 수는 없습니다) 문자열 1에-를 다시 할당해야한다면 어떻게해야합니까?). 그리고 n은 사전 계산 때문에 더 이상 할 필요가없는 신화적인 양의 작업이라고 가정합니다. 그렇다면 대답은 간단합니다. 사전 계산. 만약항상 재 할당 할 필요가없는 충분한 메모리를 가지고 있다고 주장하고 있으며, 이것이 큰 O 표기법의 기초이며 대답은 훨씬 간단합니다. 문자열 1의 끝 부분에 할당 된 메모리에서 이진 검색을 수행하십시오. 우리가 realloc에 ​​대해 걱정하지 않도록 문자열 1 다음에 무한한 0의 견본. 거기에서 쉽게 n을 log (n)으로 가져 가면 간신히 시도했습니다. 만약 당신이 log (n)을 기억한다면 실제 컴퓨터에서 기본적으로 64만큼이나 큽니다. 그것은 본질적으로 O (m +)라고 말하는 O (64 + m)와 같습니다. (그렇습니다 . 오늘날 사용중인 실제 데이터 구조에 대한 런타임 분석에 로직이 사용되었습니다. 제 머릿속에서 헛소리가 아닙니다.)

  7. Concat () / Len () 다시 : 결과를 메모합니다. 쉬운. 가능한 경우 모든 계산을 사전 계산으로 전환합니다. 이것은 알고리즘 결정입니다. 언어의 강제 제약이 아닙니다.

  8. NUL 종료를 사용하면 문자열 접미사 전달이 더 쉬워집니다. 길이 접두사를 구현하는 방법에 따라 원래 문자열을 손상시킬 수 있으며 때로는 불가능할 수도 있습니다. 사본이 필요하고 O (1) 대신 O (n)을 전달하십시오.

  9. 인수 통과 / 역 참조는 NUL 종료 대 길이 접두사에 비해 적습니다. 더 적은 정보를 전달하기 때문에 분명히. 길이가 필요하지 않으면 발자국을 많이 절약하고 최적화 할 수 있습니다.

  10. 당신은 속일 수 있습니다. 정말 포인터 일뿐입니다. 누가 그것을 문자열로 읽어야한다고 말합니까? 단일 문자 또는 부동 소수점으로 읽으려면 어떻게해야합니까? 반대로하고 플로트를 문자열로 읽으려면 어떻게해야합니까? 조심하면 NUL 종료 로이 작업을 수행 할 수 있습니다. length-prefix를 사용 하여이 작업을 수행 할 수 없으며 일반적으로 포인터와 구별되는 데이터 유형입니다. 바이트 단위로 문자열을 작성하고 길이를 가져와야 할 것입니다. 물론 전체 부동 소수점 과 같은 것을 원한다면 (아마도 NUL이있을 것입니다) 어쨌든 바이트 단위로 읽어야하지만 세부 사항은 결정해야합니다.

TL; DR 바이너리 데이터를 사용하고 있습니까? 그렇지 않다면 NUL 종료는 더 많은 알고리즘의 자유를 허용합니다. 그렇다면 코드 수량 대 속도 / 메모리 / 압축이 주요 관심사입니다. 두 가지 접근 방식 또는 메모가 혼합 된 것이 가장 좋습니다.


9 는 다소 기반이 맞지 않거나 잘못 표현되었습니다. 길이 접두사에는이 문제가 없습니다. Lenth 는 별도의 변수로 전달 합니다. 우리는 pre-fiix에 대해 이야기하고 있었지만 나는 멀리 나갔습니다. 아직도 생각하기 좋은 점은 그대로 두겠습니다. : d
블랙

1

"C에 문자열이 없습니다"라는 답변을 구입하지 않습니다. 사실, C는 기본 제공되는 고급 유형을 지원하지 않지만 C로 데이터 구조를 나타낼 수 있으며 이것이 바로 문자열입니다. 문자열이 C에서 포인터라는 사실은 첫 번째 N 바이트가 길이로 특별한 의미를 가질 수 없다는 것을 의미하지는 않습니다.

Windows / COM 개발자는 실제 문자 데이터가 바이트 0에서 시작하지 않는 길이 접두사 C 문자열과 정확히 같은 BSTR형식에 매우 익숙합니다 .

따라서 null 종료를 사용하기로 한 결정은 단순히 언어의 필요성이 아니라 사람들이 선호하는 것 같습니다.


-3

gcc는 아래 코드를 수락합니다 :

char s [4] = "abcd";

우리가 문자열이 아닌 문자 배열로 취급해도 괜찮습니다. 즉, s [0], s [1], s [2] 및 s [3] 또는 memcpy (dest, s, 4)를 사용하여 액세스 할 수 있습니다. 그러나 puts로 시도하거나 strcpy (dest, s)로 더 나쁜 문자를 얻을 수 있습니다.


@Adrian W. 유효한 C입니다. 정확한 길이의 문자열은 특수한 경우 NUL은 생략됩니다. 이것은 일반적으로 현명하지 않지만 FourCC "문자열"을 사용하는 헤더 구조체를 채우는 경우에 유용 할 수 있습니다.
Kevin Thibedeau

네 말이 맞아 이것은 유효한 C이며, 설명과 같이 컴파일되고 동작합니다. 다운 보트 (내 것이 아니라 ...)의 이유는 아마도이 답변이 어떤 식 으로든 OP의 질문에 대답하지 않기 때문일 것입니다.
Adrian W
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.