Question

How to Convert std::string to const char* or char* in C++

cppstringcharstdstring

Question

How can I convert a std::string to a char* or a const char* in C++?

Short Answer

By the end of this page, you will understand how std::string stores text, how to get a C-style string from it, when to use c_str() or data(), and what to do if you truly need a writable char*. You will also learn the lifetime rules that make this conversion safe or unsafe.

Concept

In C++, std::string and C-style strings are related but not the same thing.

  • std::string is a C++ string class that manages memory for you.
  • A C-style string is typically a const char* or char* pointing to a null-terminated character array.

If you have a std::string, the most common way to get a read-only C-style string is:

std::string s = "hello";
const char* p = s.c_str();

c_str() returns a pointer to the string's internal character data as a const char*.

This matters because many older C libraries and some system APIs expect const char* instead of std::string.

If you need writable character data, things are different:

  • You usually should not try to directly treat a std::string as a writable char* unless you understand the rules.
  • If an API only reads the string, use const char* from c_str().
  • If an API needs to modify the characters, create a writable buffer.

In modern C++, you may also use data():

std::string s = "hello";
const char* p = s.data();

For const std::string, data() gives read-only access. For non-const strings in modern C++, data() can provide writable access to the internal buffer, but this is only appropriate when you know exactly what the API expects and you preserve valid string state.

The key idea is: use std::string as your main string type, and only convert when an API requires it.

Mental Model

Think of std::string as a smart container that owns and manages a text buffer for you.

A const char* from c_str() is like borrowing a read-only view of that buffer.

  • You can look at it.
  • You can pass it to functions that read text.
  • You do not own it.
  • You should not try to modify it.

A char* is different: it suggests writable memory.

That is like asking for a marker so you can edit the label directly. Sometimes the buffer can be edited safely, but only under specific rules. If you need guaranteed writable memory for another API, it is often safer to create your own separate buffer.

Take Quiz

Syntax and Examples

Read-only conversion with c_str()

#include <iostream>
#include <string>

int main() {
    std::string name = "Alice";
    const char* cstr = name.c_str();

    std::cout << cstr << '\n';
}

Use this when the function only needs to read the string.

Using data()

#include <iostream>
#include <string>

int main() {
    std::string text = "Hello";
    const char* p = text.data();

    std::cout << p << '\n';
}

In modern C++, data() is often similar to c_str() for read-only use.

Creating a writable char* buffer

If you truly need a writable char*, make your own copy:

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::string text = "Hello";

    std::vector<char> buffer(text.begin(), text.end());
    buffer.push_back('\0');

    char* writable = buffer.data();
    writable[0] = 'Y';

    std::cout << writable << '\n';
}

This prints:

Yello

This approach is safer when another function wants to modify the characters.

Passing to a C-style function

#include <cstdio>
#include <string>

int main() {
    std::string message = "Hello from std::string";
    std::puts(message.c_str());
}

puts expects a C-style string, so c_str() is the correct choice.

Step by Step Execution

Consider this example:

#include <iostream>
#include <string>

int main() {
    std::string s = "cat";
    const char* p = s.c_str();

    std::cout << p << '\n';
}

Step by step:

  1. std::string s = "cat";

    • A C++ string object named s is created.
    • It stores the text cat and manages its own memory.
  2. const char* p = s.c_str();

    • c_str() returns a pointer to the internal character data.
    • p now points to a null-terminated character sequence representing cat.
    • The pointer is const char*, so the data should not be modified through p.
  3. std::cout << p << '\n';

    • cout sees a C-style string and prints characters until it reaches the null terminator.
    • The output is cat.

Important lifetime rule:

const char* p;
{
    std::string s = "cat";
    p = s.c_str();
}
// s is destroyed here
// p is now dangling

After s is destroyed, p is no longer valid. That is one of the most important things to remember.

Real World Use Cases

This conversion is common when C++ code interacts with older APIs or libraries.

Common scenarios

  • Calling C library functions such as puts, fopen, or printf
  • Passing file paths to platform APIs that expect const char*
  • Interfacing with third-party libraries written in C
  • Logging systems that accept C-style strings
  • Working with embedded systems or low-level code

Example: opening a file

#include <cstdio>
#include <string>

int main() {
    std::string filename = "data.txt";
    FILE* file = std::fopen(filename.c_str(), "r");

    if (file) {
        std::fclose(file);
    }
}

fopen expects const char*, so c_str() bridges the gap.

Example: formatting output

#include <cstdio>
#include <string>

int main() {
    std::string user = "Sam";
    std::printf("User: %s\n", user.c_str());
}

Again, printf expects a C-style string for %s.

Take Quiz

Real Codebase Usage

In real projects, developers usually follow a simple pattern:

Keep std::string as the main type

Use std::string for storing, returning, and passing text inside your C++ code.

std::string buildMessage() {
    return "Operation completed";
}

Convert only at the boundary where an API requires a C-style string.

std::string msg = buildMessage();
external_log(msg.c_str());

Use conversion at the call site

This keeps ownership clear and reduces bugs.

send_to_c_api(configPath.c_str());

Use guard clauses for empty input

if (path.empty()) {
    return;
}
legacy_open(path.c_str());

Use a separate writable buffer when mutation is required

std::vector<char> buffer(text.begin(), text.end());
buffer.push_back('\0');
legacy_mutating_api(buffer.data());

Avoid storing raw pointers from temporary strings

Bad pattern:

const char* p = makeName().c_str();

The temporary string is destroyed immediately, so p becomes invalid.

Better:

std::string name = makeName();
const char* p = name.c_str();

This is a common code review point in production C++ code.

Common Mistakes

1. Modifying the result of c_str()

Broken code:

std::string s = "hello";
char* p = const_cast<char*>(s.c_str());
p[0] = 'H';

Why this is a problem:

  • c_str() gives read-only access.
  • Forcing it to char* with const_cast is unsafe.
  • Modifying through that pointer can cause undefined behavior.

Avoid it by using a writable buffer instead.

2. Using the pointer after the string changes

Broken code:

std::string s = "hello";
const char* p = s.c_str();
s += " world";
std::cout << p << '\n';

Why this is a problem:

  • Changing the string may reallocate its internal buffer.
  • p may no longer point to valid data.

Fix:

std::string s = "hello";
s += " world";
const char* p = s.c_str();
std::cout << p << '\n';

3. Using a pointer after the string is destroyed

Broken code:

const char* p;
{
    std::string s = "hello";
    p = s.c_str();
}
std::cout << p << '\n';

Why this is a problem:

  • p points into s.
  • When s is destroyed, the pointer becomes dangling.

4. Assuming char* and const char* are interchangeable

Broken code:

std::string s = "hello";
char* p = s.c_str();

This does not compile because c_str() returns const char*.

5. Forgetting the null terminator in a manual buffer

Broken code:

std::string s = "hello";
std::vector<char> buffer(s.begin(), s.end());
char* p = buffer.data();

Why this is a problem:

  • Many C APIs expect a null-terminated string.
  • Without \0, the API may read past the end.

Fix:

std::vector<char> buffer(s.begin(), s.end());
buffer.push_back('\0');

Comparisons

ApproachReturnsWritable?Null-terminated?Best use
s.c_str()const char*NoYesPass to APIs that only read the string
s.data()const char* or char* depending on constness and C++ versionSometimesTypically yes in modern use, but use carefullyAccess internal buffer when you understand the rules
&s[0]char*YesNot the main intentDirect buffer access for non-empty strings; less clear than data()
std::vector<char> copychar* via .data()YesOnly if you add \0Safe writable buffer for mutating C APIs

c_str() vs data()

  • c_str() is the clearest choice for read-only C-string access.
  • data() is more general and can be writable for non-const strings in modern C++.
  • For beginners, c_str() is usually the best answer when a function expects const char*.

std::string vs C-style string

TypeMemory managementSafer?Easy to resize?
std::stringAutomaticYesYes
char* / const char*Manual or externalLess safeNo

Prefer std::string unless an API specifically requires a C-style string.

Take Quiz

Cheat Sheet

Quick reference

Get const char* from std::string

std::string s = "hello";
const char* p = s.c_str();

Read-only access with data()

const char* p = s.data();

Writable buffer copy

std::vector<char> buffer(s.begin(), s.end());
buffer.push_back('\0');
char* p = buffer.data();

Rules to remember

  • c_str() returns const char*
  • Do not modify the result of c_str()
  • The pointer stays valid only while the string exists and is not modified in a way that invalidates it
  • If the API only reads, use c_str()
  • If the API writes, use a separate writable buffer unless you truly know internal buffer access is safe

Common safe pattern

std::string path = "file.txt";
legacy_read_only_api(path.c_str());

Common unsafe pattern

const char* p = makeString().c_str();

The temporary string dies immediately, so p becomes invalid.

FAQ

When should I use c_str() in C++?

Use c_str() when a function expects a const char* and only needs to read the string.

Can I convert std::string directly to char*?

Not safely in the general case. If you need writable characters, create a writable buffer such as a std::vector<char> copy.

What is the difference between c_str() and data()?

c_str() is specifically for getting a C-style string. data() gives access to the underlying character buffer and may be writable for non-const strings in modern C++.

Is the pointer from c_str() always valid?

No. It is only valid while the std::string object still exists and its internal buffer has not been invalidated by certain modifications.

Can I store the result of c_str() for later use?

Only if the original std::string stays alive and unchanged in a compatible way. Otherwise, the pointer may become invalid.

Why does char* p = s.c_str(); fail to compile?

Because c_str() returns const char*, not char*. The compiler prevents you from removing read-only protection.

What should I use in modern C++: std::string or char*?

Use std::string for normal C++ code. Convert to const char* only when calling APIs that require it.

Take Quiz

Mini Project

Description

Build a small C++ program that stores a filename and a user message in std::string, then passes them to functions that simulate old C-style APIs. This project demonstrates the practical difference between passing read-only text with c_str() and preparing a writable buffer when a function needs to modify characters.

Goal

Create a program that safely passes std::string data to both read-only and writable C-style functions.

Requirements

  • Create one function that accepts const char* and prints the text.
  • Create one function that accepts char* and changes the first character.
  • Store the original text in std::string variables.
  • Use c_str() for the read-only function.
  • Use a writable buffer copy for the mutating function.
Take Quiz

Keep learning

Related questions

View more