I've been writing a lot of C. Whilst doing so I have been questioning my sanity. Am I an awful programmer? You know what, I think I might be.
Kudos to all those devs that created monumental feats with C. Because it is not an easy tool to use. That said, I do enjoy writing C, just as long as I don't have a deadline or any business critical software to deliver. But when I say enjoy, I mean enjoyment in the sense of using a sycthe to cut a lawn, whilst my lawnmower watches on. It's lovely using the sycthe, but at the end of the day when I am exhausted and cut only a quarter of the grass that I could have cut with a mower in half the time, I do feel a great sense of achievement. I'm exhausted and broken, but happy.
StringBuffer
Anyway here is a string buffer implementation that I extracted from my project. You can see the up to date source code at BrianDouglasIE/c_string_buffer.
The replace and remove were particularly tricky to implement. My brain was on fire trying to figure out how to juggle the memory about correctly.
Header
#ifndef STRING_BUFFER_H
#define STRING_BUFFER_H
#include <stddef.h>
/// @brief A dynamic string buffer that supports common string operations.
typedef struct {
size_t size; ///< The current size (length) of the string.
size_t capacity; ///< The total allocated capacity of the buffer.
char *data; ///< Pointer to the character data.
} StringBuffer;
/// @brief Represents all positions where a match was found.
typedef struct {
size_t *positions; ///< Array of match positions.
size_t count; ///< Number of matches found.
} MatchResult;
/// @brief Represents the result of splitting a string.
typedef struct {
char **parts; ///< Array of substrings resulting from the split.
size_t count; ///< Number of parts.
} SplitResult;
// Lifecycle
/// @brief Initializes a new, empty StringBuffer.
/// @return A pointer to the newly allocated StringBuffer.
StringBuffer *StringBuffer_init();
/// @brief Frees memory used by the given StringBuffer.
/// @param buf Pointer to the StringBuffer to be freed.
void StringBuffer_free(StringBuffer *buf);
/// @brief Clears the contents of the StringBuffer without freeing the object.
/// @param buf Pointer to the StringBuffer to be cleared.
void StringBuffer_clear(StringBuffer *buf);
// Operations
/// @brief Prints the contents of the StringBuffer to stdout.
/// @param buf Pointer to the StringBuffer to be printed.
void StringBuffer_print(const StringBuffer *buf);
/// @brief Appends text to the end of the StringBuffer.
/// @param buf Pointer to the StringBuffer.
/// @param text Null-terminated string to append.
void StringBuffer_append(StringBuffer *buf, const char *text);
/// @brief Prepends text to the beginning of the StringBuffer.
/// @param buf Pointer to the StringBuffer.
/// @param text Null-terminated string to prepend.
void StringBuffer_prepend(StringBuffer *buf, const char *text);
/// @brief Removes the first occurrence of text from the buffer starting from a given index.
/// @param buf Pointer to the StringBuffer.
/// @param text Null-terminated string to remove.
/// @param from Index to start the search from.
void StringBuffer_remove(StringBuffer *buf, const char *text, size_t from);
/// @brief Replaces the first occurrence of a substring with another string, starting from a given index.
/// @param buf Pointer to the StringBuffer.
/// @param original Substring to be replaced.
/// @param update Replacement string.
/// @param from Index to start the search from.
void StringBuffer_replace(StringBuffer *buf, const char *original,
const char *update, size_t from);
// Search & Split
/// @brief Finds the index of the first occurrence of text starting from a given index.
/// @param buf Pointer to the StringBuffer.
/// @param text Substring to search for.
/// @param from Index to start the search from.
/// @return The index of the first occurrence, or -1 if not found.
int StringBuffer_index_of(const StringBuffer *buf, const char *text,
size_t from);
/// @brief Finds all occurrences of a substring starting from a given index.
/// @param buf Pointer to the StringBuffer.
/// @param text Substring to match.
/// @param from Index to start the search from.
/// @return A pointer to a MatchResult containing all match positions.
MatchResult *StringBuffer_match_all(const StringBuffer *buf, const char *text,
size_t from);
/// @brief Splits the buffer into parts using the given delimiter.
/// @param buf Pointer to the StringBuffer.
/// @param delimiter String delimiter to split by.
/// @return A pointer to a SplitResult containing the parts.
SplitResult *StringBuffer_split(const StringBuffer *buf, const char *delimiter);
// Cleanup
/// @brief Frees the memory used by a MatchResult.
/// @param matches Pointer to the MatchResult to free.
void MatchResult_free(MatchResult *matches);
/// @brief Frees the memory used by a SplitResult.
/// @param split Pointer to the SplitResult to free.
void SplitResult_free(SplitResult *split);
#endif
Implementation
#include "string_buffer.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define INITIAL_CAPACITY 16
static int ensure_capacity(StringBuffer *buf, size_t required) {
if (required <= buf->capacity)
return 1;
size_t new_capacity = buf->capacity ? buf->capacity : INITIAL_CAPACITY;
while (new_capacity < required) {
new_capacity *= 2;
}
char *new_data = realloc(buf->data, new_capacity);
if (!new_data) {
perror("realloc");
return 0;
}
buf->data = new_data;
buf->capacity = new_capacity;
return 1;
}
StringBuffer *StringBuffer_init() {
StringBuffer *buf = malloc(sizeof(StringBuffer));
if (!buf) {
perror("malloc");
return NULL;
}
buf->size = 0;
buf->capacity = 0;
buf->data = NULL;
return buf;
}
void StringBuffer_free(StringBuffer *buf) {
if (!buf)
return;
free(buf->data);
free(buf);
}
void StringBuffer_clear(StringBuffer *buf) {
if (!buf || !buf->data)
return;
buf->data[0] = '\0';
buf->size = 0;
}
void StringBuffer_print(const StringBuffer *buf) {
if (!buf || !buf->data)
return;
printf("size: %zu\n", buf->size);
printf("data: %s\n", buf->data);
}
void StringBuffer_append(StringBuffer *buf, const char *text) {
if (!buf || !text)
return;
size_t text_len = strlen(text);
if (!ensure_capacity(buf, buf->size + text_len + 1))
return;
memcpy(buf->data + buf->size, text, text_len + 1);
buf->size += text_len;
}
void StringBuffer_prepend(StringBuffer *buf, const char *text) {
if (!buf || !text)
return;
size_t text_len = strlen(text);
if (!ensure_capacity(buf, buf->size + text_len + 1))
return;
memmove(buf->data + text_len, buf->data,
buf->size + 1);
memcpy(buf->data, text, text_len);
buf->size += text_len;
}
int StringBuffer_index_of(const StringBuffer *buf, const char *text,
size_t from) {
if (!buf || !text || !buf->data || from >= buf->size)
return -1;
char *match = strstr(buf->data + from, text);
return match ? (int)(match - buf->data) : -1;
}
MatchResult *StringBuffer_match_all(const StringBuffer *buf, const char *text,
size_t from) {
if (!buf || !text || !buf->data || from >= buf->size)
return NULL;
MatchResult *matches = malloc(sizeof(MatchResult));
if (!matches)
return NULL;
matches->positions = NULL;
matches->count = 0;
int index = StringBuffer_index_of(buf, text, from);
while (index != -1) {
size_t *new_positions =
realloc(matches->positions, sizeof(size_t) * (matches->count + 1));
if (!new_positions) {
MatchResult_free(matches);
return NULL;
}
matches->positions = new_positions;
matches->positions[matches->count++] = index;
index = StringBuffer_index_of(buf, text, index + 1);
}
return matches;
}
void StringBuffer_remove(StringBuffer *buf, const char *text, size_t from) {
if (!buf || !text || !buf->data || from >= buf->size)
return;
size_t text_len = strlen(text);
int index = StringBuffer_index_of(buf, text, from);
while (index != -1) {
size_t tail_len = buf->size - (index + text_len);
memmove(buf->data + index, buf->data + index + text_len, tail_len + 1);
buf->size -= text_len;
index = StringBuffer_index_of(buf, text, index);
}
}
void StringBuffer_replace(StringBuffer *buf, const char *original,
const char *update, size_t from) {
if (!buf || !original || !update || !buf->data || from >= buf->size)
return;
size_t original_len = strlen(original);
size_t update_len = strlen(update);
if (original_len == 0 || update_len == (size_t)-1)
return;
int index = StringBuffer_index_of(buf, original, from);
while (index != -1) {
if (update_len > original_len) {
if (!ensure_capacity(buf, buf->size + (update_len - original_len) + 1))
return;
}
size_t tail_len = buf->size - (index + original_len);
memmove(buf->data + index + update_len, buf->data + index + original_len,
tail_len + 1);
memcpy(buf->data + index, update, update_len);
buf->size = buf->size - original_len + update_len;
index = StringBuffer_index_of(buf, original, index + update_len);
}
}
SplitResult *StringBuffer_split(const StringBuffer *buf,
const char *delimiter) {
if (!buf || !delimiter || !buf->data)
return NULL;
char *copy = strdup(buf->data);
if (!copy)
return NULL;
SplitResult *result = malloc(sizeof(SplitResult));
if (!result) {
free(copy);
return NULL;
}
result->parts = NULL;
result->count = 0;
char *token = strtok(copy, delimiter);
while (token) {
char **tmp = realloc(result->parts, sizeof(char *) * (result->count + 1));
if (!tmp) {
SplitResult_free(result);
free(copy);
return NULL;
}
result->parts = tmp;
result->parts[result->count] = strdup(token);
if (!result->parts[result->count]) {
SplitResult_free(result);
free(copy);
return NULL;
}
result->count++;
token = strtok(NULL, delimiter);
}
free(copy);
return result;
}
void MatchResult_free(MatchResult *matches) {
if (!matches)
return;
free(matches->positions);
free(matches);
}
void SplitResult_free(SplitResult *split) {
if (!split)
return;
for (size_t i = 0; i < split->count; i++) {
free(split->parts[i]);
}
free(split->parts);
free(split);
}
Tests
#include "string_buffer.h"
#include <assert.h>
#include <string.h>
int main(void) {
StringBuffer *buf = StringBuffer_init();
assert(buf != NULL);
StringBuffer_append(buf, "hello");
assert(buf->size == 5);
assert(strcmp(buf->data, "hello") == 0);
StringBuffer_append(buf, " world");
assert(buf->size == 11);
assert(strcmp(buf->data, "hello world") == 0);
assert(StringBuffer_index_of(buf, "hello", 0) == 0);
assert(StringBuffer_index_of(buf, "world", 0) == 6);
assert(StringBuffer_index_of(buf, "mars", 0) == -1);
assert(StringBuffer_index_of(buf, "llo", 2) == 2);
assert(StringBuffer_index_of(buf, "hello", 99) == -1);
assert(StringBuffer_index_of(buf, "hello", buf->size) == -1);
assert(StringBuffer_index_of(buf, "d", buf->size - 1) ==
(int)(buf->size - 1));
assert(StringBuffer_index_of(buf, "h", 1) == -1);
MatchResult *mr = StringBuffer_match_all(buf, "l", 0);
if (!mr) {
return -1;
}
assert(mr->count == 3);
assert(mr->positions[0] == 2);
assert(mr->positions[1] == 3);
assert(mr->positions[2] == 9);
MatchResult_free(mr);
SplitResult *sr = StringBuffer_split(buf, " ");
assert(sr->count == 2);
assert(strcmp(sr->parts[0], "hello") == 0);
assert(strcmp(sr->parts[1], "world") == 0);
SplitResult_free(sr);
StringBuffer_prepend(buf, "hello ");
assert(buf->size == 17);
assert(strcmp("hello hello world", buf->data) == 0);
StringBuffer_remove(buf, " ", 0);
assert(buf->size == 15);
assert(strcmp("hellohelloworld", buf->data) == 0);
StringBuffer *remove_overlap_test = StringBuffer_init();
StringBuffer_append(remove_overlap_test, "lalalalala");
StringBuffer_remove(remove_overlap_test, "ala", 0);
assert(strcmp(remove_overlap_test->data, "llla") == 0);
assert(remove_overlap_test->size == 4);
StringBuffer_free(remove_overlap_test);
StringBuffer_replace(buf, "hellohello", "hello ", 0);
assert(buf->size == 11);
assert(strcmp("hello world", buf->data) == 0);
StringBuffer *replace_overlap_test = StringBuffer_init();
StringBuffer_append(replace_overlap_test, "lalalalala");
StringBuffer_replace(replace_overlap_test, "ala", "", 0);
assert(strcmp(replace_overlap_test->data, "llla") == 0);
assert(replace_overlap_test->size == 4);
StringBuffer_free(replace_overlap_test);
StringBuffer_clear(buf);
assert(buf->size == 0);
assert(strlen(buf->data) == 0);
StringBuffer_free(buf);
return 0;
}