What is a C string?

Real-World Analogy

Imagine a string as a row of labelled boxes in a warehouse. Each box holds one letter. At the end of the row there is a special red box labelled STOP — this is the null terminator \0. Every C string function keeps walking along the boxes until it hits that red stop box. If the stop box is missing, the function keeps walking into random warehouse areas — that is a buffer overrun, and it is one of the most dangerous bugs in C programming.

In Python, a string is a full-featured object — it knows its own length, can be sliced, concatenated with +, and garbage-collected automatically. In Java, String is an immutable object with built-in bounds checking. In C, there is no string type. A string is simply a char array that the programmer promises ends with the special character '\0' (ASCII value 0, also called the null terminator or NUL character).

This is both powerful and dangerous. Powerful because a C string is just memory — fast, portable, compatible with everything. Dangerous because C never checks whether your buffer is large enough, and if you write past the end you corrupt whatever happens to live in the next memory location.

The null terminator \0 is not the digit zero. The character '0' has ASCII value 48. The null terminator has ASCII value 0. They are completely different. When you write "hello" in your source code, the compiler stores 6 bytes: 'h','e','l','l','o','\0'. The \0 is added automatically by the compiler for string literals — but NOT when you fill a char array byte by byte yourself.

Python
# Python strings are objects
s = "hello"
print(len(s))        # 5 — built-in
t = s + " world"     # safe concatenation
print(s[0:3])        # "hel" — safe slicing
# No buffer, no null terminator needed
Java
// Java String is immutable object
String s = "hello";
System.out.println(s.length()); // 5
String t = s + " world";        // safe
String sub = s.substring(0, 3); // "hel"
// IndexOutOfBoundsException guards you
C
/* C strings are char arrays */
#include <string.h>
char s[6] = "hello";  /* 5 chars + \0 */
printf("%zu\n", strlen(s)); /* 5 */
char t[20];
strcpy(t, s);         /* manual copy */
strncat(t, " world", 14); /* careful! */
/* No bounds checking — your responsibility */
The Most Common C Bug: Buffer Overflow

When you declare char name[10], you have exactly 10 bytes. If you copy an 11-character string into it, the 11th byte overwrites whatever is at name[10] in memory — which might be another variable, a return address, or heap metadata. This is a buffer overflow — it causes crashes, undefined behavior, and is the root cause of countless security vulnerabilities. Always size buffers with at least one extra byte for \0.

String Declaration Cheat-Sheet

DeclarationWhat happensModifiable?Notes
char s[] = "hello"; Array of 6 bytes on stack, initialized from literal Yes Most common. Size inferred by compiler.
char s[20] = "hello"; 20 bytes, first 6 used, rest zero-filled Yes Safe — extra room for growth.
char s[3] = "hello"; Only 3 bytes — truncated! No null terminator! Danger Buffer too small. Undefined behavior.
const char *s = "hello"; Pointer to string literal in read-only memory No Writing to it is undefined behavior.
char s[20]; s[0] = '\0'; Empty string manually initialized Yes Always set \0 at index 0 for empty string.
Mental Model: Strings Are Pointers to Char Arrays

When you pass a string to a function like strlen(s), you are passing a pointer to the first character. The function then walks forward one byte at a time until it finds \0. This is why strlen returns the wrong answer (or crashes) if there is no \0 — it never knows where to stop.

string.h functions — signatures and annotations

Include #include <string.h> for all functions below. The n variants (strncpy, strncat, strncmp) are the safe alternatives — always prefer them for production code.

/* strlen — count characters before \0 */
size_t strlen(const char *s);
 ^      ^                 ^ const = won't modify the string
 |      └── unsigned type for sizes (never negative)
 └── returns number of chars BEFORE \0 (not including \0)

/* strcpy — copy src into dst (including \0) */
char *strcpy(char *dst, const char *src);
 ^                ^ dst must be large enough — NO CHECKING!
 └── returns dst (allows chaining, rarely used)

/* strncpy — copy at most n bytes (SAFE, but caveat: may not add \0!) */
char *strncpy(char *dst, const char *src, size_t n);
                                               ^ always add: dst[n-1] = '\0';

/* strcat — append src to end of dst */
char *strcat(char *dst, const char *src);
 dst must have strlen(dst)+strlen(src)+1 bytes available!

/* strncat — append at most n bytes of src (SAFE) */
char *strncat(char *dst, const char *src, size_t n);
 always adds \0 after the n chars (unlike strncpy)

/* strcmp — compare two strings lexicographically */
int strcmp(const char *a, const char *b);
 returns: 0 if equal, <0 if a<b, >0 if a>b
 WARNING: NEVER use == to compare strings! "abc" == "abc" compares POINTERS!

/* strncmp — compare only first n bytes */
int strncmp(const char *a, const char *b, size_t n);
 great for prefix checks: strncmp(cmd, "GET", 3) == 0

/* sprintf — format into a string buffer */
int sprintf(char *buf, const char *fmt, ...);
 like printf but writes to buf instead of stdout

/* sscanf — parse from a string buffer */
int sscanf(const char *buf, const char *fmt, ...);
 like scanf but reads from a string instead of stdin
Key rule for strncpy: It copies at most n bytes and pads with \0 if src is shorter — but if src is n or more bytes long, the result is NOT null-terminated. Always force-terminate: dst[n-1] = '\0';

Memory layout of "cat"

char s[] = "cat";   /* compiler allocates 4 bytes */

Address:  0x1000  0x1001  0x1002  0x1003
Bytes:    'c'     'a'     't'     '\0'
ASCII:     99      97     116       0

strlen(s) == 3    /* counts c, a, t — stops at \0 */
sizeof(s) == 4    /* includes the \0 byte */
strlen vs sizeof: strlen counts until \0 (runtime). sizeof returns the byte size of the array (compile-time, includes \0). For a pointer (char *p), sizeof(p) gives the pointer size (8 bytes on 64-bit) — NOT the string length!

sprintf and sscanf

/* sprintf: build a string from formatted data */
char buf[64];
int score = 42;
sprintf(buf, "Score: %d / 100", score);
/* buf now contains: "Score: 42 / 100\0" */

/* sscanf: parse fields from a string */
char line[] = "Alice 21 88.5";
char name[20]; int age; float score2;
sscanf(line, "%s %d %f", name, &age, &score2);
/* name="Alice", age=21, score2=88.5 */
Use case: sscanf is perfect for parsing lines read with fgets. Read the whole line first, then parse fields with sscanf — much safer than using fscanf directly on a file.

strtod — safe string-to-double conversion

/* strtod — the safe alternative to atof (from stdlib.h) */
double strtod(const char *str, char **endptr);
 ^      ^         ^ input string    ^ if not NULL, *endptr points to first unconverted char
 |      └── returns the converted double value
 └── #include <stdlib.h>

/* Basic usage — ignore trailing characters */
double x = strtod("3.14abc", NULL);   /* x == 3.14 */

/* Error detection: if endptr == original str, no conversion happened */
char *end;
double val = strtod(argv[1], &end);
if (end == argv[1]) {
    fprintf(stderr, "Not a number\n");
}
strtod vs atof: atof("abc") silently returns 0.0 on failure — you cannot distinguish failure from a genuine zero. strtod sets *endptr == str when no conversion occurs, giving you a reliable error signal. Use the same pattern as strtol: pass &end and check end == str. For overflow/underflow, check errno after the call (set errno = 0 beforehand).

ctype.h — character classification and conversion

Include #include <ctype.h> for these functions. All take an int argument (pass the char cast to unsigned char to avoid undefined behavior with non-ASCII characters) and return an int. Used extensively in Week 2 "reverse" and Week 3 "Shout" exercises.

Function Returns true (nonzero) when c is...
isalpha(c)a letter (a–z, A–Z)
isdigit(c)a decimal digit (0–9)
isalnum(c)a letter or digit
isspace(c)whitespace (space, tab, newline, \r, \f, \v)
isupper(c)an uppercase letter
islower(c)a lowercase letter
isprint(c)a printable character (includes space)
iscntrl(c)a control character (ASCII 0–31 and 127)
toupper(c)returns uppercase version of c (non-letter: returns c unchanged)
tolower(c)returns lowercase version of c (non-letter: returns c unchanged)
/* Convert a string to uppercase using toupper */
#include <ctype.h>
char s[] = "Hello World";
for (int i = 0; s[i]; i++) {
    s[i] = (char)toupper(s[i]);  /* cast needed: toupper returns int */
}
/* s is now "HELLO WORLD" */

/* Count digits in a string */
int digits = 0;
for (int i = 0; s[i]; i++)
    if (isdigit((unsigned char)s[i])) digits++;
Important safety note: Always pass (unsigned char)c or cast appropriately — passing a signed char directly can cause undefined behavior for non-ASCII characters (values 128–255 appear as negative when char is signed). The cast to unsigned char is the portable, correct approach. toupper and tolower return int, so cast the result back to char when storing in a char array.

Complete programs you can compile and run

Example 1 — String declarations, strlen, strcpy, strcat Week 3 Lecture
#include <stdio.h>
#include <string.h>

int main(void) {
    /* Declaration styles */
    char hobby[] = "rowing";        /* size = 7 (6 chars + '\0') */
    char buf[64] = "";              /* empty string: buf[0] = '\0' */
    char greeting[64];              /* uninitialized — must write before reading */

    /* strlen: count characters before '\0' */
    printf("hobby    = \"%s\"\n", hobby);
    printf("strlen   = %zu\n",   strlen(hobby));   /* 6 */
    printf("sizeof   = %zu\n",   sizeof(hobby));   /* 7 (includes \0) */

    /* strcpy: copy a string into a buffer */
    strcpy(greeting, "Hello, ");   /* buf must fit "Hello, \0" = 8 bytes */
    printf("After strcpy: \"%s\"\n", greeting);

    /* strcat: append one string to another */
    strcat(greeting, hobby);       /* greeting = "Hello, rowing" */
    strcat(greeting, "!");         /* greeting = "Hello, rowing!" */
    printf("After strcat: \"%s\"\n", greeting);
    printf("Final strlen = %zu\n", strlen(greeting));

    /* sprintf: build a string with formatting */
    int score = 95;
    sprintf(buf, "Grade: %d%%", score);   /* %% prints literal % */
    printf("%s\n", buf);

    return 0;
}
Output
hobby = "rowing"
strlen = 6
sizeof = 7
After strcpy: "Hello, "
After strcat: "Hello, rowing!"
Final strlen = 14
Grade: 95%
Example 2 — Safe string manipulation with strncpy, strncat, strcmp Week 3 Tutorial — Strings
#include <stdio.h>
#include <string.h>

#define BUFSIZE 16

int main(void) {
    char dst[BUFSIZE];
    const char *src = "tiger";       /* 5 chars */
    const char *long_src = "superlongstring";  /* 15 chars — fits exactly */

    /* strncpy: copy at most n bytes */
    strncpy(dst, src, BUFSIZE);
    dst[BUFSIZE - 1] = '\0';         /* ALWAYS force-terminate! */
    printf("strncpy: \"%s\"\n", dst);

    /* What if src is too long? strncpy truncates but may omit '\0' */
    strncpy(dst, long_src, BUFSIZE);
    dst[BUFSIZE - 1] = '\0';         /* critical: ensures null terminator */
    printf("truncated: \"%s\"\n", dst);

    /* strncat: safe concatenation — always adds '\0' */
    char result[32] = "Hello";
    strncat(result, ", World", sizeof(result) - strlen(result) - 1);
    printf("strncat: \"%s\"\n", result);

    /* strcmp: string equality — NEVER use == on strings! */
    const char *a = "apple";
    const char *b = "banana";
    const char *c = "apple";

    printf("strcmp(apple, banana) = %d  (negative = a comes first)\n",
           strcmp(a, b));
    printf("strcmp(apple, apple)  = %d  (zero = equal)\n",
           strcmp(a, c));
    printf("strcmp(banana, apple) = %d  (positive = b comes after)\n",
           strcmp(b, a));

    /* strncmp: compare only first n chars — great for command parsing */
    const char *cmd = "GET /index.html";
    if (strncmp(cmd, "GET", 3) == 0) {
        printf("HTTP method is GET\n");
    }

    return 0;
}
Output
strncpy: "tiger"
truncated: "superlongstrin"
strncat: "Hello, World"
strcmp(apple, banana) = -1 (negative = a comes first)
strcmp(apple, apple) = 0 (zero = equal)
strcmp(banana, apple) = 1 (positive = b comes after)
HTTP method is GET
Example 3 — strtol: safe string-to-integer conversion Week 3B Tutorial
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main(void) {
    /* atoi: UNSAFE — silently returns 0 on failure, no error info */
    int bad = atoi("abc");         /* returns 0 — you cannot tell it failed */
    int also_bad = atoi("0");      /* also returns 0 — indistinguishable! */
    printf("atoi(\"abc\") = %d  (silent failure!)\n", bad);

    /* strtol: SAFE — preferred in COMP2017 */
    /* Signature: long strtol(const char *s, char **endptr, int base); */
    char *endptr;
    const char *s1 = "42abc";

    long val = strtol(s1, &endptr, 10);
    /* val = 42, endptr points to "abc" (the first non-digit) */
    printf("strtol(\"%s\", ..., 10): val=%ld, rest=\"%s\"\n", s1, val, endptr);

    /* Detect complete conversion: *endptr should be '\0' */
    if (*endptr != '\0') {
        printf("  Warning: partial conversion, trailing: \"%s\"\n", endptr);
    }

    /* Detect no digits at all: endptr == original string */
    const char *s2 = "xyz";
    long val2 = strtol(s2, &endptr, 10);
    if (endptr == s2) {
        printf("strtol(\"%s\"): no digits found!\n", s2);
    }

    /* Detect overflow using errno */
    errno = 0;                          /* reset before the call */
    long big = strtol("99999999999999999999", &endptr, 10);
    if (errno != 0) {
        printf("Overflow detected via errno!\n");
    }

    /* Base 0: auto-detect hex (0x) and octal (0) prefixes */
    printf("0x1F  = %ld\n", strtol("0x1F",  NULL, 0));  /* 31 */
    printf("017   = %ld\n", strtol("017",   NULL, 0));  /* 15 (octal) */
    printf("255   = %ld\n", strtol("255",   NULL, 0));  /* 255 (decimal) */

    return 0;
}
Output
atoi("abc") = 0 (silent failure!)
strtol("42abc", ..., 10): val=42, rest="abc"
Warning: partial conversion, trailing: "abc"
strtol("xyz"): no digits found!
Overflow detected via errno!
0x1F = 31
017 = 15
255 = 255
Example 3b — strtod: safe string-to-double conversion Week 3B Tutorial
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main(void) {
    /* atof: UNSAFE — silently returns 0.0 on failure, no error info */
    double bad = atof("xyz");          /* returns 0.0 — indistinguishable from atof("0") */
    printf("atof(\"xyz\") = %f  (silent failure!)\n", bad);

    /* strtod: SAFE — preferred alternative */
    /* Signature: double strtod(const char *str, char **endptr); */
    char *end;
    const char *s1 = "3.14abc";

    double val = strtod(s1, &end);
    /* val = 3.14, end points to "abc" (first unconverted char) */
    printf("strtod(\"%s\", ...): val=%f, rest=\"%s\"\n", s1, val, end);

    /* Detect complete conversion: *end should be '\0' */
    if (*end != '\0') {
        printf("  Warning: partial conversion, trailing: \"%s\"\n", end);
    }

    /* Detect no digits at all: end == original string pointer */
    const char *s2 = "not-a-number";
    double val2 = strtod(s2, &end);
    if (end == s2) {
        printf("strtod(\"%s\"): no valid number found!\n", s2);
    }
    (void)val2;  /* suppress unused-variable warning */

    /* Using strtod for command-line argument validation */
    const char *arg = "2.718";
    errno = 0;
    double e = strtod(arg, &end);
    if (end == arg) {
        fprintf(stderr, "Not a number\n");
    } else if (errno != 0) {
        fprintf(stderr, "Overflow/underflow\n");
    } else {
        printf("Parsed: %f\n", e);
    }

    return 0;
}
Output
atof("xyz") = 0.000000 (silent failure!)
strtod("3.14abc", ...): val=3.140000, rest="abc"
Warning: partial conversion, trailing: "abc"
strtod("not-a-number"): no valid number found!
Parsed: 2.718000
Example 4 — sscanf for line parsing Week 3 — File I/O pattern
#include <stdio.h>
#include <string.h>

int main(void) {
    /* sscanf parses a string the same way scanf parses stdin */
    char line[] = "Alice 21 88.5";
    char name[20];
    int  age;
    float gpa;

    int n = sscanf(line, "%19s %d %f", name, &age, &gpa);
    /* %19s: read at most 19 chars into name (leaves room for \0) */

    if (n == 3) {
        printf("Name: %s\n", name);
        printf("Age:  %d\n", age);
        printf("GPA:  %.1f\n", gpa);
    } else {
        printf("Parse error: only got %d fields\n", n);
    }

    /* sprintf round-trip: format then parse */
    char buf[64];
    sprintf(buf, "x=%d y=%d", 10, 20);
    int x, y;
    sscanf(buf, "x=%d y=%d", &x, &y);
    printf("Parsed: x=%d, y=%d\n", x, y);

    return 0;
}
Output
Name: Alice
Age: 21
GPA: 88.5
Parsed: x=10, y=20

Practice problems with solutions

P1 — What does this print? Trace through carefully. Week 3 Tutorial

Without running the code, predict the output of each printf statement. Pay close attention to the difference between strlen and sizeof.

#include <stdio.h>
#include <string.h>
int main(void) {
    char s1[] = "hello";
    char s2[20] = "hello";
    const char *s3 = "hello";

    printf("%zu\n", strlen(s1));
    printf("%zu\n", sizeof(s1));
    printf("%zu\n", strlen(s2));
    printf("%zu\n", sizeof(s2));
    printf("%zu\n", strlen(s3));
    printf("%zu\n", sizeof(s3));  /* on 64-bit system */
    return 0;
}
5
6
5
20
5
8
Line by line: strlen(s1) = 5 (counts h,e,l,l,o). sizeof(s1) = 6 (the array has 6 bytes: 5 chars + 1 for \0). strlen(s2) = 5 (still only 5 chars despite the array being 20 bytes). sizeof(s2) = 20 (the declared array size). strlen(s3) = 5 (pointer walks until \0). sizeof(s3) = 8 on a 64-bit system — it returns the pointer size, not the string length. This is the classic pointer-vs-array trap.
P2 — Spot the bug: buffer overflow and missing null terminator Week 3 Lecture + Tutorial

The code below has three bugs. Find and fix each one. This is exactly the kind of code that appears in COMP2017 exams.

#include <stdio.h>
#include <string.h>
int main(void) {
    char dst[5];
    const char *src = "Hello, World!";

    /* Bug 1 */
    strcpy(dst, src);

    char first[3];
    char second[3];
    /* Bug 2 */
    strncpy(first, "AB", 3);
    strncpy(second, "CDEEEE", 3);
    printf("%s%s\n", first, second);  /* Bug 3 */

    return 0;
}
#include <stdio.h>
#include <string.h>
int main(void) {
    /* Fix 1: dst must be large enough to hold src + \0 */
    char dst[14];  /* strlen("Hello, World!") = 13, +1 for \0 */
    const char *src = "Hello, World!";
    strcpy(dst, src);
    printf("%s\n", dst);

    char first[3];
    char second[3];
    strncpy(first, "AB", 3);
    /* Fix 2: force null-terminate after strncpy */
    first[2] = '\0';

    strncpy(second, "CDEEEE", 3);
    /* Fix 3: second is NOT null-terminated because src length >= n */
    /* strncpy copies "CDE" but writes no \0 when src is 6 chars and n=3 */
    second[2] = '\0';  /* must force-terminate */
    printf("%s%s\n", first, second);  /* now safe: "ABCD" */

    return 0;
}
Bug 1: strcpy copies 14 bytes (13 chars + \0) into a 5-byte buffer. This is a classic buffer overflow — it corrupts the stack.
Bug 2: first gets "AB\0" correctly (src shorter than n). But second gets "CDE" with NO null terminator because src ("CDEEEE", 6 chars) is longer than n (3). strncpy never adds \0 when src >= n.
Bug 3: Printing second without a null terminator reads past the array into garbage memory — undefined behavior.
P3 — Implement my_strlen using a pointer Week 5 Tutorial

Write your own version of strlen that uses a pointer (not array indexing) to walk through the string. Then write a version using array indexing. Both must return the number of characters before '\0'.

#include <stdio.h>
#include <stddef.h>

/* Version 1: pointer arithmetic */
size_t my_strlen_ptr(const char *s) {
    const char *p = s;      /* save start address */
    while (*p != '\0') {    /* advance until null terminator */
        p++;
    }
    return (size_t)(p - s); /* difference = number of chars */
}

/* Version 2: array indexing */
size_t my_strlen_idx(const char *s) {
    size_t len = 0;
    while (s[len] != '\0') {
        len++;
    }
    return len;
}

int main(void) {
    const char *test = "Systems Programming";
    printf("ptr:   %zu\n", my_strlen_ptr(test));  /* 19 */
    printf("index: %zu\n", my_strlen_idx(test));  /* 19 */
    return 0;
}
Pointer version: p - s gives the number of bytes between two pointers to the same array — which equals the number of characters walked. Index version: simpler to read. Both are O(n) — they must scan the entire string since C strings don't store their length. This is why repeated strlen calls in loops are expensive.
P4 — strcmp return value trap: what does this print? Week 3 Tutorial

A student writes this code to check if two strings are equal. What is wrong with it? What will it print for the inputs shown?

#include <stdio.h>
#include <string.h>
int main(void) {
    char a[] = "network";
    char b[] = "netflix";

    /* Incorrect equality check */
    if (strcmp(a, b)) {
        printf("strings are different\n");
    } else {
        printf("strings are equal\n");
    }

    /* Another incorrect check */
    if (strcmp(a, b) == 1) {
        printf("a comes after b\n");
    } else {
        printf("a comes before or equals b\n");
    }
    return 0;
}
/* Output: */
strings are different      /* correct by accident */
a comes before or equals b /* WRONG: should say "a comes after b" */

/* Correct versions: */
if (strcmp(a, b) != 0) { printf("different\n"); }  /* equality check */
if (strcmp(a, b) > 0)  { printf("a after b\n"); }  /* ordering check */
Bug 1: if (strcmp(a, b)) tests if the result is non-zero, which happens to detect "not equal" — but this is confusing and not idiomatic. Use != 0 explicitly.
Bug 2: strcmp(a, b) == 1 is WRONG. The C standard only guarantees the sign of the result — not the exact value. strcmp may return any negative number for "less than" and any positive number for "greater than". Some implementations return -1/0/+1 but others return the actual character difference. Always check < 0, == 0, > 0.

For "network" vs "netflix": at index 3, 'w' (119) > 'f' (102), so the result is positive (a > b). The check == 1 may be false even though a comes after b.
P5 — Write a safe string concatenation function Week 5 Tutorial

Write a function safe_concat(char *dst, const char *src, size_t dst_size) that appends src to dst, ensuring the result is never larger than dst_size - 1 bytes and is always null-terminated. Return the length of the resulting string.

#include <stdio.h>
#include <string.h>

size_t safe_concat(char *dst, const char *src, size_t dst_size) {
    if (dst_size == 0) return 0;

    size_t dst_len = strlen(dst);
    if (dst_len >= dst_size) return dst_len;  /* already full */

    size_t space_left = dst_size - dst_len - 1; /* -1 for \0 */
    strncat(dst, src, space_left);
    /* strncat always adds \0 after the n chars */
    return strlen(dst);
}

int main(void) {
    char buf[20] = "Hello";
    size_t len;

    len = safe_concat(buf, ", World!", sizeof(buf));
    printf("Result: \"%s\"  len=%zu\n", buf, len);

    /* Try to overflow — should truncate safely */
    len = safe_concat(buf, "OVERFLOW_ATTEMPT", sizeof(buf));
    printf("After overflow attempt: \"%s\"  len=%zu\n", buf, len);
    return 0;
}
Key insight: Calculate space_left = dst_size - dst_len - 1 to know how many bytes we can safely append. Pass this to strncat which always appends a \0 after writing. The result is always null-terminated and never exceeds dst_size. This pattern is essentially what strlcat (BSD extension) does.
P6 — Implement my_strcat without using string.h Wk3B/4A Tutorial

Implement char *my_strcat(char *dest, const char *src) that appends src to dest. Do NOT use any string.h functions. Return dest.

char *my_strcat(char *dest, const char *src) {
    char *d = dest;
    while (*d) d++;          /* walk to end of dest */
    while ((*d++ = *src++)); /* copy src including \0 */
    return dest;
}
Step 1 — find the end of dest: while (*d) d++; advances d until it points at the null terminator of dest. d now sits exactly where the new characters should be written.
Step 2 — copy src: while ((*d++ = *src++)) copies one character per iteration — including the final \0 — and stops when the assigned value is \0 (falsy). This is the idiomatic compact copy loop in C.
Return value: returning the original dest pointer (saved before walking) mirrors the real strcat signature and allows call chaining. Danger: dest must have enough space for its existing content plus all of src — no bounds checking is performed.
P7 — Implement my_substring with malloc Wk3B/4A Tutorial

Implement char *my_substring(const char *str, int start, int len) that returns a newly malloc'd string containing len characters starting at index start. Return NULL if start is out of bounds. Do NOT use any string.h functions.

#include <stdlib.h>

char *my_substring(const char *str, int start, int len) {
    int slen = 0;
    while (str[slen]) slen++;          /* manual strlen */
    if (start < 0 || start >= slen) return NULL;
    if (start + len > slen) len = slen - start;  /* clamp to end */
    char *result = malloc(len + 1);
    if (!result) return NULL;
    for (int i = 0; i < len; i++) result[i] = str[start + i];
    result[len] = '\0';
    return result;
}
Measure the string: a manual while loop replaces strlen since we cannot use string.h.
Bounds check: return NULL when start is negative or at/past the end. This satisfies the requirement and avoids reading garbage memory.
Clamp length: if start + len would reach past the end of str, reduce len to only take what is available.
malloc: allocate len + 1 bytes — one extra for the null terminator. The caller is responsible for calling free() on the returned pointer.
Copy loop: a simple index loop copies len characters; then result[len] = '\0' null-terminates the new string.

Key concepts to memorize

Card 1 of 10
Question — click to flip
Answer
Click card to flip • Use buttons to navigate

Test your understanding

Topic 09 Quiz — Strings Score: 0 / 6
1
What is the null terminator in C strings?LO1
multiple choice
2
True or False: strncpy(dst, src, n) always null-terminates the destination.LO1
true / false
3
Given char s[] = "cat"; on a 64-bit system, what does sizeof(s) return?LO1
multiple choice
4
Fill in the blank: To compare two strings a and b for equality in C, you write _______(a, b) == 0.LO1
fill in the blank
5
Spot the bug: what is wrong with this string comparison?LO1
char input[20];
scanf("%19s", input);
if (input == "quit") {
    printf("Exiting...\n");
}
spot the bug — multiple choice
6
Which function writes formatted text into a character buffer (instead of stdout)?LO8
multiple choice
0/6
Quiz complete!