Pointers and Strings

Hi everyone ✋

In the previous post we learned the deep and beautiful relationship between pointers and arrays. We saw that an array name is really an address, that marks[i] is just *(marks + i), and that arrays decay into pointers all the time.

If you haven’t read the earlier posts in this series, please go through them first ─ today’s topic sits directly on top of everything we learned about arrays.

Today we are going to talk about pointers and strings. And here’s a little spoiler to get us excited ─ in C++, the classic C-style string is really just an array of characters. And since arrays are secretly pointers… well, you can already guess where this is going 😉

Let’s take a deep dive 🦴

First, what even is a C-style string?

Before pointers enter the picture, let’s remember what a string is at the lowest level.

In C++ we have the lovely std::string class (we’ll come back to it at the end), but underneath, the original C-style string is simply this ─ a bunch of characters stored side by side in memory, ending with a special invisible character called the null terminator, written as '\0'.

That '\0' is the secret hero of this whole post. It marks “the string ends here.” Without it, nobody would know where the string stops.

Let’s make one.

#include <iostream>
using namespace std;

int main()
{
    char name[6] = {'i', 'm', 'r', 'a', 'n', '\0'};

    cout << name << endl;

    return 0;
}

The output will be ─

imran

Notice the array has 6 slots for a 5-letter word. That extra slot holds the '\0'. When we cout the array, C++ prints character by character and stops the moment it hits '\0'.

Writing '\0' by hand is tiresome, so C++ lets us write the same thing with double quotes ─

#include <iostream>
using namespace std;

int main()
{
    // the compiler automatically adds '\0' at the end for us
    char name[] = "imran";

    cout << name << endl;

    return 0;
}

The output is again ─

imran

Even though we typed only 5 letters, this array is actually 6 characters long, because the compiler quietly added the '\0' for us. Let’s prove it 🥳

#include <iostream>
using namespace std;

int main()
{
    char name[] = "imran";

    cout << "sizeof(name): " << sizeof(name) << endl;

    return 0;
}

The output will be ─

sizeof(name): 6

Six, not five! That invisible '\0' is taking up the sixth slot. Always remember this ─ a C-style string needs one extra byte for its null terminator 🤔

A string lives in memory like this

Let’s picture our name array in memory. Each character sits in its own box, side by side, exactly like the integer arrays from last post.

Index name[0] name[1] name[2] name[3] name[4] name[5]
Value ‘i’ ‘m’ ‘r’ ‘a’ ‘n’ ‘\0’

This should look very familiar! It’s just an array ─ only this time, the elements are char instead of int. And as we learned last post, an array name is really the address of the first element. So name is basically a pointer to name[0], the letter 'i'.

Everything we learned about pointers and arrays applies here too 🥳

Pointing a pointer at a string

Since a string is just a char array, and an array name is an address, we can point a char pointer at it.

#include <iostream>
using namespace std;

int main()
{
    char name[] = "imran";

    // name is the address of the first character
    char *ptr = name;

    cout << "first character: " << *ptr << endl;

    return 0;
}

The output will be ─

first character: i

Just like before, no & was needed ─ name already decays into a pointer to its first character. And *ptr dereferences it to give us 'i'. So far this is exactly the array story from last post, just with letters 😄

The interesting part ─ printing a char pointer

Now here’s something special that happens only with char pointers, and it surprises a lot of beginners.

When we put a normal int pointer into cout, we get an address. But when we put a char pointer into cout, C++ does something different ─ it assumes we want the whole string, and prints characters starting from that address until it hits '\0'.

Let’s see both behaviors side by side.

#include <iostream>
using namespace std;

int main()
{
    int number = 42;
    int *iPtr = &number;

    char name[] = "imran";
    char *cPtr = name;

    cout << "int pointer  : " << iPtr << endl;   // prints an address
    cout << "char pointer : " << cPtr << endl;   // prints the string!

    return 0;
}

The output will be something like ─

int pointer  : 0x7ffe5a3c
char pointer : imran

😲 See the difference?
The int pointer printed a hexadecimal address, but the char pointer printed imran.

This is a special rule baked into cout for char * ─ it treats a char pointer as “the start of a string” and keeps printing until the null terminator. This single rule is why C-style strings and pointers feel so tangled together.

If we ever actually want the address stored in a char pointer, we have to trick cout by casting it to a void *.

#include <iostream>
using namespace std;

int main()
{
    char name[] = "imran";
    char *cPtr = name;

    cout << "as string  : " << cPtr << endl;
    cout << "as address : " << (void *)cPtr << endl;

    return 0;
}

The output will be something like ─

as string  : imran
as address : 0x7ffe5a3c

See how the same pointer printed the string first, then the raw address after casting?

A void * is simply a “typeless” pointer ─ a pointer that just holds an address without any idea what kind of value lives there. Because it has no type, cout can’t apply its special “this is a string” rule to it, so it falls back to printing the plain address. We won’t go deeper here, but void * is powerful enough to deserve its own discussion, so we’ll explore it properly in a later post 😉

Walking through a string with pointer arithmetic

Since a string is an array of characters, we can walk through it with pointer arithmetic ─ exactly like we walked through the integer array last post. The only difference is how we know when to stop.

With the int array, we knew the size was 5. With a string, we don’t need the size at all ─ we just walk forward until we meet '\0' 🦴

#include <iostream>
using namespace std;

int main()
{
    char name[] = "imran";

    char *ptr = name;

    // keep going until we hit the null terminator
    while (*ptr != '\0')
    {
        cout << *ptr << " ";
        ptr++;   // move to the next character
    }
    cout << endl;

    return 0;
}

The output will be ─

i m r a n

🥳🥳 We printed every letter without ever knowing the length in advance!

Let’s understand what happened. Each time we did ptr++, the pointer moved forward by sizeof(char), which is exactly 1 byte ─ so it landed on the very next character. And *ptr gave us the character living there. The loop kept going until *ptr became '\0', at which point we stopped.

Step ptr points to *ptr keep going?
1 name[0] ‘i’ yes
2 name[1] ‘m’ yes
3 name[2] ‘r’ yes
4 name[3] ‘a’ yes
5 name[4] ‘n’ yes
6 name[5] ‘\0’ STOP

This little while (*ptr != '\0') pattern is the heartbeat of almost every classic C string function. Once you recognize it, you’ll see it everywhere 👀

Let’s build our own strlen 🥳

To really feel the power of what we just learned, let’s build something useful ─ our own version of strlen, the function that measures a string’s length.

The idea is simple ─ start a pointer at the beginning, walk forward counting steps, and stop at '\0'.

#include <iostream>
using namespace std;

// our own string-length function
int myStrlen(char *str)
{
    int length = 0;

    while (*str != '\0')
    {
        length++;
        str++;
    }

    return length;
}

int main()
{
    char name[] = "imran";

    cout << "length: " << myStrlen(name) << endl;

    return 0;
}

The output will be ─

length: 5

🥳 Five, not six! Notice that our function counts the letters only and does not include the '\0', because the loop stops right when it reaches it. This is exactly how the real strlen behaves ─ it gives you the visible length, not the storage size.

Two small lessons hide here, both from earlier posts ─

First, str is a char * parameter, which means the array decayed to a pointer when passed in. So our function only received an address, just like all the array functions from last post.

Second, this is why strlen("imran") gives 5 but sizeof("imran") gives 6. The sizeof counts the storage (including '\0'), while strlen counts the letters (stopping before '\0'). Beginners mix these two up constantly, so keep them apart in your mind 🤔

Let’s build more C string tools 🛠️

Now that we’ve built myStrlen, we’re basically fluent in the while (*ptr != '\0') pattern. So let’s use it to rebuild a few more classic C string functions from scratch. Every one of them is just pointers walking through characters ─ nothing more. Once you see them built by hand, the whole <cstring> library stops feeling like magic 🥳

1. myStrcpy ─ copying a string

strcpy copies one string into another. The idea ─ walk through the source with a pointer, copy each character into the destination, and don’t forget to place the '\0' at the end.

#include <iostream>
using namespace std;

// copy src into dest, character by character
void myStrcpy(char *dest, char *src)
{
    while (*src != '\0')
    {
        *dest = *src;   // copy one character
        dest++;         // move both pointers forward
        src++;
    }

    *dest = '\0';       // don't forget the null terminator!
}

int main()
{
    char source[] = "imran";
    char destination[20];   // must be big enough!

    myStrcpy(destination, source);

    cout << destination << endl;

    return 0;
}

The output will be ─

imran

🥳 Notice two important things. First, we manually added *dest = '\0'; after the loop ─ because our while stops before copying the '\0', so we have to place it ourselves. Forgetting this is one of the most common C bugs ⚠️

Second, destination had to be big enough to hold the copy. C does not check this for us. If the destination is too small, we write past the end of the array ─ a dangerous bug called a buffer overflow. This is exactly why modern C++ prefers std::string, which grows on its own.

2. myStrcat ─ joining two strings

strcat (short for concatenate) glues the second string onto the end of the first. The trick ─ first walk to the end of the destination, then copy the source from there.

#include <iostream>
using namespace std;

void myStrcat(char *dest, char *src)
{
    // step 1: walk to the end of dest (stop at its '\0')
    while (*dest != '\0')
    {
        dest++;
    }

    // step 2: now copy src onto the end, just like strcpy
    while (*src != '\0')
    {
        *dest = *src;
        dest++;
        src++;
    }

    *dest = '\0';   // terminate the joined string
}

int main()
{
    char full[20] = "imran";
    char last[] = " shah";

    myStrcat(full, last);

    cout << full << endl;

    return 0;
}

The output will be ─

imran shah

🥳 See how the first loop just finds the end, and the second loop is really our strcpy again? We’re reusing the same walking pattern twice. That’s the beauty of pointers ─ once you learn the walk, everything is built from it.

3. myStrcmp ─ comparing two strings

strcmp checks whether two strings are equal. It walks both strings together, comparing character by character. If it ever finds a mismatch, they’re different. If both reach '\0' at the same time, they’re equal.

The real strcmp returns 0 when the strings are equal, so we’ll follow the same convention.

#include <iostream>
using namespace std;

// returns 0 if equal, non-zero if different
int myStrcmp(char *a, char *b)
{
    while (*a != '\0' && *b != '\0')
    {
        if (*a != *b)
        {
            return *a - *b;   // characters differ
        }
        a++;
        b++;
    }

    // if we reached here, check if BOTH ended together
    return *a - *b;
}

int main()
{
    char first[]  = "imran";
    char second[] = "imran";
    char third[]  = "shah";

    cout << "imran vs imran : " << myStrcmp(first, second) << endl;
    cout << "imran vs shah  : " << myStrcmp(first, third) << endl;

    return 0;
}

The output will be something like ─

imran vs imran : 0
imran vs shah  : -10

🥳 A 0 means the strings matched perfectly. A non-zero result means they differ ─ and the sign tells us which one is “bigger” alphabetically (based on the character codes). Here 'i' (105) minus 's' (115) gives -10, which is why "imran" is considered “less than” "shah". This is exactly how dictionary-style sorting works under the hood 🤔

4. myStrtok ─ splitting a string into tokens

This one is the trickiest and the most fun 🥳 strtok breaks a string into smaller pieces (tokens) wherever it finds a separator character ─ like splitting "imran,shah,coder" by commas.

The clever idea is this ─ strtok modifies the original string, replacing each separator with a '\0' so each piece becomes its own little string. And it uses a static pointer to remember where it stopped last time, so calling it again continues from where it left off.

Let’s build a simplified version that splits on a single separator character.

#include <iostream>
using namespace std;

// splits a string on a single separator character
// pass the string the first time, then pass NULL to keep going
char* myStrtok(char *str, char separator)
{
    // this pointer REMEMBERS where we stopped last time
    static char *current = nullptr;

    // if a new string is given, start from it; otherwise continue
    if (str != nullptr)
    {
        current = str;
    }

    // if there's nothing left, return nullptr
    if (current == nullptr || *current == '\0')
    {
        return nullptr;
    }

    // remember where this token starts
    char *tokenStart = current;

    // walk until we hit the separator or the end
    while (*current != '\0' && *current != separator)
    {
        current++;
    }

    // if we stopped on a separator, cut the string here
    if (*current == separator)
    {
        *current = '\0';   // replace separator with a terminator
        current++;         // move past it for next time
    }
    else
    {
        current = nullptr; // we reached the end
    }

    return tokenStart;
}

int main()
{
    char text[] = "imran,shah,coder";

    char *token = myStrtok(text, ',');

    while (token != nullptr)
    {
        cout << token << endl;
        token = myStrtok(nullptr, ',');   // pass nullptr to continue
    }

    return 0;
}

The output will be ─

imran
shah
coder

🥳🥳 We split one string into three pieces using nothing but pointers and null terminators!

This is the most advanced example in the whole post, so let’s slow down on the two clever tricks ─

The static char *current is the key. A normal local variable would reset every time the function is called, but a static variable keeps its value between calls. That’s how myStrtok remembers where it left off, so the next call continues from the right spot. This is why we call it the first time with the string, and every next time with nullptr.

And notice *current = '\0'; ─ we are literally overwriting the separator in the original string with a null terminator. That chops the string into standalone pieces in place. This is also why strtok can only be used on a modifiable char array, never on a const char * string literal ─ remember the read-only trap we’re about to discuss 😉

Don’t worry if strtok feels heavy ─ it’s genuinely the trickiest function in the standard C string library. Just seeing how it works with pointers is a big achievement 🥳

A dangerous trap ─ char array vs char pointer literal ⚠️

Now we reach the part that causes real bugs in real programs, so let’s go slowly.

There are two very common ways to create a string, and they look almost the same but behave very differently.

char a[] = "imran";   // a modifiable array copy
char *b = "imran";    // a pointer to a read-only string literal

The first one, char a[], creates a fresh array and copies the letters into it. We own this array, and we can freely change its characters.

The second one, char *b, does not make a copy. It points b straight at a string literal ─ a block of characters the compiler stores in read-only memory. We do not own it, and trying to change it is dangerous.

Let’s see the safe one first.

#include <iostream>
using namespace std;

int main()
{
    char a[] = "imran";

    a[0] = 'I';   // totally fine, we own this array

    cout << a << endl;

    return 0;
}

The output will be ─

Imran

No problem ─ we changed the first letter to a capital 'I' and it worked perfectly.

But now watch the dangerous version ─

#include <iostream>
using namespace std;

int main()
{
    char *b = "imran";

    b[0] = 'I';   // DANGER ─ trying to modify a read-only string literal!

    cout << b << endl;

    return 0;
}

This might crash your program or cause undefined behavior 😱 Because b points to a read-only string literal, trying to overwrite b[0] is not allowed. The compiler may even warn you about it.

So here’s the golden rule to remember ─

  • If you want a string you can modify, use a char array ─ char a[] = "imran";
  • If you only want to read a fixed string, a const char * is the honest, safe choice ─ const char *b = "imran";

That const makes your intention clear and lets the compiler protect you from accidental writes. Modern C++ compilers actually require the const for string literals, for exactly this safety reason.

A quick comparison

Let’s put the two side by side so the difference is crystal clear.

char a[] = “imran” const char *b = “imran”
What it is a brand-new array a pointer to a literal
Makes a copy? Yes No
Can we modify it? Yes No (read-only)
Memory used the full array just a pointer
Safe to change letters? Yes No ─ may crash

So where does std::string fit in? 🤔

After all this careful pointer work, you might wonder ─ “why do we ever bother with raw char arrays?”

Great question. In modern C++, most of the time we don’t. We use the friendly std::string class, which handles the memory, the null terminator, the resizing, and the safety for us.

#include <iostream>
#include <string>
using namespace std;

int main()
{
    string name = "imran";

    name[0] = 'I';          // safe and easy
    name += " shah";        // grows automatically!

    cout << name << endl;
    cout << "length: " << name.length() << endl;

    return 0;
}

The output will be ─

Imran shah
length: 10

So much easier 🥳 No null terminator to worry about, no read-only traps, no manual length counting. std::string grows on demand and protects us at every step.

But here’s the beautiful part ─ even std::string is secretly built on top of a character buffer, and it still lets us reach the underlying C-style string whenever we need it, using .c_str()

#include <iostream>
#include <string>
using namespace std;

int main()
{
    string name = "imran";

    // get the underlying const char* (C-style string)
    const char *raw = name.c_str();

    cout << raw << endl;

    return 0;
}

The output will be ─

imran

So even the modern, safe std::string is, deep down, still a char pointer wearing a very nice suit 😄 Understanding the raw pointer version helps you understand what std::string is doing behind the scenes ─ and that’s exactly why we walked through all of this.

So what should we remember? 🤔

Let’s wrap up the key takeaways ─

  • A C-style string is just a char array ending in a null terminator '\0'.
  • The '\0' is what tells C++ where the string stops ─ that’s why "imran" needs 6 bytes, not 5.
  • A string name decays to a char *, so all our pointer-and-array knowledge applies directly.
  • cout treats a char * specially ─ it prints the whole string, not the address. Cast to void * to see the address.
  • We can walk a string with pointer arithmetic using the while (*ptr != '\0') pattern ─ no length needed.
  • char a[] makes a modifiable copy, but char *b = "literal" points to read-only memory ─ use const char * for literals.
  • In modern C++, prefer std::string for safety and convenience ─ but know that .c_str() reveals the char * hiding underneath.
  • Classic C string functions like strlen, strcpy, strcat, strcmp, and strtok are all just pointers walking through characters until '\0'.
  • When copying or joining strings by hand, always leave room for the '\0' and make sure the destination is big enough ─ otherwise you get a buffer overflow.
  • strtok modifies the original string in place, so it only works on a modifiable char array, not a read-only literal.

Once these click, the mysterious world of C strings stops being scary and starts making perfect sense 👀✨

Congratulations 🥳🥳🥳
We now understand the deep connection between pointers and strings.

In the next post we will talk about pointers and functions ─ how we can store a function’s address in a pointer and call it through that pointer, which opens the door to some seriously powerful C++ tricks 😉

Happy Coding 💻 🎵

Leave a Comment

Your email address will not be published. Required fields are marked *