I'm writing a language interpreter in C, and my
struct String slice(struct String in, size_t begin, size_t end)
struct String out;
out.characters = in.characters + begin;
out.length = end - begin;
char* slice(char* in, size_t begin, size_t end)
char* out = malloc(end - begin + 1);
for(int i = 0; i < end - begin; i++)
out[i] = in[i + begin];
out[end - begin] = '\0';
The usual solution is to do both - keep the length and maintain the null terminator. It's not much extra work and means that you are always ready to pass the string to any function.
Null-terminated strings are often a drain on performance, for the obvious reason that the time taken to discover the length depends on the length. On the plus side, they are the standard way of representing strings in C, so you have little choice but to support them if you want to use most C libraries.