openlang

getline은 실패해도 버퍼를 호출자에게 남긴다

POSIX getline은 넘겨받은 포인터를 통해 버퍼를 할당하거나 키우며, 그 버퍼는 첫 호출부터 호출자의 것입니다. -1을 반환하는 호출도 예외가 아닙니다. 명세는 해제를 애플리케이션에 맡기고, Linux 매뉴얼 페이지는 getline이 실패하더라도 버퍼를 해제해야 한다고 직접 적어 둡니다.

따라서 루프에는 해제가 정확히 한 번, 루프가 끝난 뒤에 있습니다. 끝난 이유와는 상관없습니다.

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

int main(void)
{
	char *line = NULL;
	size_t cap = 0;
	ssize_t n;
	long lines = 0;

	while ((n = getline(&line, &cap, stdin)) != -1)
		lines++;
	if (ferror(stdin))
		perror("getline");
	free(line);
	printf("%ld\n", lines);
	return 0;
}

여기서 두 가지가 따라옵니다. 첫째, -1은 파일 끝과 읽기 오류를 모두 뜻하므로 둘을 구분하려면 ferror나 feof를 봐야 하고, 어느 경우든 line을 누가 해제하는지는 바뀌지 않습니다. 둘째, line과 cap은 널 포인터와 0으로 시작하거나, malloc으로 얻은 버퍼와 그 실제 크기로 시작해야 합니다. 스택 배열은 안 됩니다. getline이 그것을 realloc에 넘길 수 있기 때문입니다.

가장 자주 보는 실수는 오류 분기 안에만 free(line)이 있고 파일 끝 경로에는 없는 경우입니다.

이어지는 대화 4

서로의 근거를 따라
  1. 고유 링크

    getline은 ISO C에 없다. POSIX다. 올린 코드는 기본 기능 매크로가 마침 그 선언을 열어 주기 때문에 컴파일될 뿐이다. glibc에서 -std=c11로 빌드하면 선언이 없다. 선언되지 않은 함수 호출은 C99부터 유효한 C가 아니고, 요즘 GCC와 Clang은 이를 기본으로 오류 처리하니 루프는 그냥 빌드되지 않는다. 첫 include 위에 #define _POSIX_C_SOURCE 200809L을 두면 해결된다. 사람들은 루프만 복사하고 빌드 플래그는 복사하지 않으니 이 줄은 예제 안에 있어야 한다.

  2. 고유 링크

    맞습니다. 저는 기본 상태에서 getline을 노출하는 헤더로만 확인했고, 그것은 glibc에 대해 아무것도 보여 주지 않습니다. 이 정의는 예제의 첫 줄에 있어야 합니다.

  3. 고유 링크

    Lua에 줄을 넘기다가 걸렸던 작은 거 하나요. 길이는 strlen 말고 꼭 반환값을 쓰세요. getline은 스트림에 있던 바이트를 중간의 0 바이트까지 그대로 담아요. 그래서 입력이 b\0c\n이면 반환값은 4인데 strlen(line)은 1이에요. lua_pushlstring(L, line, n)을 쓰면 스크립트가 네 바이트를 다 받고, lua_pushstring을 쓰면 조용히 "b"만 받아요. 찾고 나서 좀 웃었어요.

  4. 고유 링크

    그렇습니다. C 쪽에서도 루프 뒤에 line을 문자열로 다루는 모든 코드에 같은 말이 적용됩니다. 반환값은 읽은 바이트 수이고, 구분자가 있었다면 구분자도 포함합니다. 개행 없이 끝나는 마지막 줄은 개행 없이 돌아오므로, 무조건 line[n - 1]을 지우는 코드는 그 줄의 진짜 문자 하나를 지웁니다. 말씀하신 입력을 테스트 파일에 넣고, 개행 없는 마지막 줄도 함께 넣겠습니다.

같은 질문의 다른 갈래

  1. 댕글링 포인터 경고는 사라졌고 포인터는 남았다

    래퍼를 통해 C++ 라이브러리에 이름을 넘기는데, 처음엔 한 줄로 CString::new(name).unwrap().as_ptr()라고 썼어요. rustc가 바로 임시 CString이 문장 끝에서 사라지니 포인터가 댕글링이…

    답글 1rustcpp
  2. 같은 인스턴스를 가리키는 인터페이스 참조와 객체 참조

    오래된 Delphi 코드에서 제가 가장 자주 만나는 수명 실수는 한 인스턴스에 두 가지 소유 규칙을 섞는 데서 나옵니다. TInterfacedObject를 상속한 클래스는 인터페이스 참조 카운트가 0이 될 때 해제됩니다. 클…

  3. 같은 sink 호출인데 한 번은 복사, 한 번은 이동

    Nim 2는 복사냐 이동이냐를 호출 지점마다 정한다. proc 단위로 정하지 않는다. 같은 인자, 같은 proc. 첫 호출은 a가 아직 쓰이니 복사한다. 둘째 호출은 이동한다. 호출 지점에는 어느 쪽인지 아무 표시가 없다.…

    답글 2nim