Embercode
✕
Character Arrays and StringsLesson 11 of 16

45 min read

Regex basics

Why it matters

A regular expression, usually shortened to regex, is a short pattern that describes a set of strings. The pattern \d{4}-\d{2}-\d{2} describes any text made of four digits, a dash, two digits, a dash and two more digits. It matches 2026-11-05 and does not match 5/11/2026.

Programs use regex to check input, such as whether a value looks like a date. They use it to search logs, pull fields out of a line, and replace text. One pattern can replace many lines of loops and index checks. The same power has a cost. A pattern that looks short can make the program take a very long time on some inputs. The last part of this lesson shows why.

The previous lessons in this module handled text one character at a time with loops, and the CSV lesson split on one fixed character. A regex describes the shape of the text instead of one fixed character.

The idea of a pattern

Most characters in a pattern match themselves. The pattern cat matches the three letters c, a, t in that order. It matches inside concatenate too, because the letters appear there.

Some characters have special meanings. These are the ones you will use most.

PatternMatches
.any one character
\done digit from 0 to 9
\wone letter, digit or underscore
\sone whitespace character, such as a space or tab
[abc]one of the characters a, b or c
[a-z]one lowercase letter
^the start of the text
$the end of the text

A quantifier says how many times the thing before it may repeat.

  • * means zero or more times.
  • + means one or more times.
  • ? means zero or one time.
  • {4} means exactly four times.
  • {2,5} means from two to five times.

A group is part of a pattern written inside round brackets. Groups let you pull out one part of a match later. The bar | means "or", so cat|dog matches either word.

Some patterns are a shape, not a complete check. The pattern [\w.]+@[\w]+\.[a-z]{2,} matches many email addresses, but it does not match every address that the email standard allows. A regex can check the shape of text. It cannot check that a date exists or that a value is correct.

Simple examples

Here are three patterns with what they match.

  • \d+ matches 7, 42 and 2026 inside a longer text. The + means the whole run of digits is one match.
  • ^\d{2}:\d{2}$ matches 09:30 only when the whole text is that value. The ^ and $ anchor the pattern to the start and end.
  • colou?r matches color and colour. The ? makes the u optional.

Escaping special characters

The dot . matches any character, so \d.\d matches 3x5 as well as 3.5. When you want a real dot, write \.. The backslash turns the special character back into an ordinary one. The same rule applies to the other special characters, such as +, ?, ( and [.

std::regex loose(R"(\d.\d)");
std::regex exact(R"(\d\.\d)");
std::cout << std::boolalpha;
std::cout << "3x5 loose: " << std::regex_match("3x5", loose) << "\n";
std::cout << "3x5 exact: " << std::regex_match("3x5", exact) << "\n";
std::cout << "3.5 exact: " << std::regex_match("3.5", exact) << "\n";

The output is:

3x5 loose: true
3x5 exact: false
3.5 exact: true

Reading a pattern piece by piece

Read a pattern from left to right, one piece at a time. Take this pattern for a date written as month, day and year.

^(\d{2})/(\d{2})/(\d{4})$
  • ^ requires the match to start at the beginning of the text.
  • (\d{2}) is group 1. It takes exactly two digits, such as 11.
  • / is an ordinary character. It must be a slash.
  • (\d{2}) is group 2, two more digits, such as 05.
  • / is another slash.
  • (\d{4}) is group 3. It takes exactly four digits, such as 2026.
  • $ requires the match to end at the end of the text.

The pattern matches 11/05/2026. It does not match 11/05/26, because group 3 needs four digits and there are only two. It does not match x11/05/2026, because the ^ requires the match to start at the first character.

When a pattern fails on text you expected it to match, cut the pattern down to the first piece and test that. Add one piece at a time until the test fails. The last piece you added is the one that does not fit the text.

Writing patterns in C++

In a normal C++ string, a backslash starts an escape sequence. The text "\d" is not a valid escape, so the compiler warns about it. A raw string literal avoids this. It starts with R"( and ends with )", and everything between is kept exactly as written. Use raw strings for every regex in C++. The mistakes section shows the warning you get without them.

C++ provides std::regex in the <regex> header. Here are the main tools, with a short example for each.

  • std::regex holds a compiled pattern.
  • std::regex_search looks for the pattern anywhere in the text.
  • std::regex_match returns true only when the pattern matches the whole text.
  • std::regex_replace returns a new string with every match replaced.
  • std::sregex_iterator walks through every match in turn. An iterator is an object that visits the items of a series one at a time.
  • std::smatch holds the result of a search on a std::string. Its element [0] is the whole match and [1], [2] and so on are the groups.

A first program

The program below uses one pattern for dates and one for a name=value pair. It then prints the results.

#include <iostream>
#include <regex>
#include <string>
 
int main() {
    std::regex date(R"(\d{4}-\d{2}-\d{2})");
    std::string text = "Released 2026-11-05, updated 2026-11-06.";
 
    std::smatch found;
    if (std::regex_search(text, found, date)) {
        std::cout << "first: " << found.str() << " at " << found.position() << "\n";
    }
 
    for (std::sregex_iterator it(text.begin(), text.end(), date), end; it != end; ++it) {
        std::cout << "match: " << it->str() << "\n";
    }
 
    std::cout << std::boolalpha;
    std::cout << "whole string 2026-11-05: " << std::regex_match("2026-11-05", date) << "\n";
    std::cout << "with prefix: " << std::regex_match("on 2026-11-05", date) << "\n";
 
    std::regex pair(R"((\w+)=(\w+))");
    std::smatch parts;
    std::string line = "user=chiranjeet";
    if (std::regex_match(line, parts, pair)) {
        std::cout << "key " << parts[1].str() << ", value " << parts[2].str() << "\n";
    }
 
    std::regex digits(R"(\d+)");
    std::cout << std::regex_replace("a1b22c333", digits, "#") << "\n";
}

regex_search stops at the first match and records where it is. found.position() returns the index where the match starts. The sregex_iterator loop goes on from each match to the next, so it prints every date.

The two calls to regex_match show the difference between "anywhere" and "whole text". The first call matches the complete string. The second call fails, because on comes before the date and regex_match needs the pattern to cover all of the text.

Here is the output on g++ 15.2.0 with the C++20 standard.

first: 2026-11-05 at 9
match: 2026-11-05
match: 2026-11-06
whole string 2026-11-05: true
with prefix: false
key user, value chiranjeet
a#b#c#

The parts result has a group for each part of the pattern in round brackets. Group 1 is the name before the equals sign and group 2 is the value after it. Group 0 is the whole match.

Writing patterns in Java

Java has the classes Pattern and Matcher in the java.util.regex package. A Pattern holds a compiled pattern. A Matcher is attached to one piece of text and walks through it.

  • Pattern.compile builds the pattern.
  • pattern.matcher(text) makes a Matcher for that text.
  • matcher.find() moves to the next match and returns true if there is one.
  • matcher.matches() returns true only when the whole text matches.
  • matcher.group(n) returns group n, with group 0 as the whole match.
  • matcher.start() returns the index where the current match begins.

In a Java string literal, a backslash is also an escape character. A regex \d is written as "\\d", with two backslashes in the source code. Java has no raw strings in the form used by C++. A text block, written with three double quotes, can make longer patterns easier to read, but this lesson uses ordinary strings.

import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class RegexDemo {
    public static void main(String[] args) {
        Pattern date = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
        String text = "Released 2026-11-05, updated 2026-11-06.";
 
        Matcher m = date.matcher(text);
        while (m.find()) {
            System.out.println("match: " + m.group() + " at " + m.start());
        }
 
        System.out.println("whole string 2026-11-05: " + date.matcher("2026-11-05").matches());
        System.out.println("with prefix: " + date.matcher("on 2026-11-05").matches());
 
        Matcher pair = Pattern.compile("(\\w+)=(\\w+)").matcher("user=chiranjeet");
        if (pair.matches()) {
            System.out.println("key " + pair.group(1) + ", value " + pair.group(2));
        }
 
        System.out.println("a1b22c333".replaceAll("\\d+", "#"));
    }
}

Compile and run it with OpenJDK 25.0.4 as follows.

javac RegexDemo.java
java RegexDemo

The output is:

match: 2026-11-05 at 9
match: 2026-11-06 at 29
whole string 2026-11-05: true
with prefix: false
key user, value chiranjeet
a#b#c#

The Java and C++ programs print the same matches. The Java program also prints where each match starts, so the second date shows 29, the index of its first character in the sentence.

Matching with find and matches

Two Java methods look similar, and beginners mix them up. find moves through the text and stops at each match, so it can find a date in the middle of a sentence. matches covers the whole text, so "on 2026-11-05" gives false. The C++ regex_search corresponds to find, and the C++ regex_match corresponds to matches.

Groups and replacement

A group is written with round brackets. Groups let you use part of a match without writing a separate pattern. In the name=value example above, the pattern (\w+)=(\w+) has two groups. The first group matches user and the second matches chiranjeet.

Replacement uses the whole match. regex_replace in C++ and replaceAll in Java take a pattern and a replacement string, and return a new string. The pattern \d+ matches each run of digits, so a1b22c333 becomes a#b#c#. The original string does not change, because both functions return a new value.

Finding things in a list of lines

A common job is to read lines of text and pull out one value from each. The pattern below finds the number after the word size= in each line.

#include <iostream>
#include <regex>
#include <string>
#include <vector>
 
int main() {
    std::vector<std::string> lines = {
        "upload ok size=1204 bytes",
        "upload failed",
        "upload ok size=88 bytes",
    };
    std::regex size(R"(size=(\d+))");
    std::smatch m;
    for (const std::string& line : lines) {
        if (std::regex_search(line, m, size)) {
            std::cout << m[1].str() << "\n";
        } else {
            std::cout << "no size\n";
        }
    }
}

The output on g++ 15.2.0 is:

1204
no size
88

The if checks whether the line has the field at all. Lines without it are reported, so the program never reads a group that did not match.

A harder example: a whole log line

A log line often has several fields in a fixed order. This pattern has one group for each field: the date, the time, the level and the rest of the message. The .* at the end matches everything that is left, including spaces and the percent sign.

#include <iostream>
#include <regex>
#include <string>
 
int main() {
    std::regex log_line(R"((\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) (\w+) (.*))");
 
    std::string line = "2026-11-05 10:42:17 WARN disk 91% full";
    std::smatch parts;
    if (std::regex_match(line, parts, log_line)) {
        std::cout << "date: " << parts[1].str() << "\n";
        std::cout << "time: " << parts[2].str() << "\n";
        std::cout << "level: " << parts[3].str() << "\n";
        std::cout << "message: " << parts[4].str() << "\n";
    }
 
    std::string broken = "2026-11-05 10:42 WARN disk";
    std::cout << std::boolalpha << "broken line matched: " << std::regex_match(broken, parts, log_line) << "\n";
}

The output is:

date: 2026-11-05
time: 10:42:17
level: WARN
message: disk 91% full
broken line matched: false

The second line lacks the seconds in the time, so \d{2}:\d{2}:\d{2} fails and the whole match fails. The program then prints false and does not read any group. Use regex_match here, not regex_search, because the groups must cover the whole line. With regex_search, a line that merely contains a date and a level would be accepted.

The cost of backtracking

The matching engine in both languages works by backtracking. It tries one choice. If the rest of the pattern fails, it undoes that choice and tries another. For a simple pattern this is fast. For a pattern with nested repetition on the same characters, the number of choices can grow very fast.

Take the pattern (a+)+b. It means one or more groups, where each group is one or more a, and the text must then end with b. Give it the text aaaa...ac, with many as and no b. The engine has to try every way of splitting the run of as into groups. Each split is tried before the engine gives up. The number of splits doubles each time the text gets one character longer.

The program below measures this with a text of n letters a followed by a c. It uses regex_match, so the engine must try to match the whole text. It also times the simpler pattern a+b on a similar text.

#include <chrono>
#include <iostream>
#include <regex>
#include <string>
 
int main() {
    std::cout << "\nBacktracking cost of (a+)+b on a string with no b\n";
    std::regex bad(R"((a+)+b)");
    for (int n : {16, 18, 20, 22, 24}) {
        std::string s(n, 'a');
        s.push_back('c');
        auto start = std::chrono::steady_clock::now();
        bool ok = std::regex_match(s, bad);
        auto stop = std::chrono::steady_clock::now();
        auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(stop - start).count();
        std::cout << "n = " << n << ", matched: " << ok << ", time: " << ms << " ms\n";
    }
 
    std::regex simple(R"(a+b)");
    std::string long_input(24, 'a');
    long_input.push_back('c');
    std::cout << "a+b on 25 characters, matched: " << std::regex_match(long_input, simple) << "\n";
}

Here is the output on this machine, with g++ 15.2.0 and -std=c++20.

Backtracking cost of (a+)+b on a string with no b
n = 16, matched: 0, time: 15 ms
n = 18, matched: 0, time: 66 ms
n = 20, matched: 0, time: 241 ms
n = 22, matched: 0, time: 951 ms
n = 24, matched: 0, time: 3816 ms
a+b on 25 characters, matched: 0

Each step adds two characters, and the time rises about four times each step. Adding one character doubles the work. The exact times depend on the machine, so your numbers will differ. The shape of the growth is the point. Extending the same doubling to 40 characters would take days, even though the text is still short.

Why the simple pattern is fine

The pattern a+b takes the run of as in one go. It then looks for b, sees c, and gives back one a at a time to try again. Each try is a short check, so the work grows in a straight line with the length of the text. The nested pattern (a+)+b has many ways to split the same run, and each split gets its own try. The difference is the number of ways to match the same text, not the number of characters.

Avoiding the slow case

Three habits keep backtracking under control.

  • Do not put a repeated group inside another repeated group when both repeat the same characters. Write a+b in place of (a+)+b. The two patterns match the same text.
  • Make the parts of the pattern separate. In \d+,\d+, the comma stops the first run of digits, so the engine has one way to split the text.
  • Limit the length of the input before you match it. A check such as if (text.size() > 1000) costs nothing and removes the worst cases for user input.

Neither std::regex nor Java's Pattern has a built-in time limit. A slow pattern will keep the program busy until it finishes. Some regex libraries are built so that the time grows in a straight line with the input length. Those libraries accept fewer features, such as backreferences. The choice depends on the work the program has to do.

Common mistakes

Backslash in a normal C++ string

A normal C++ string treats the backslash as the start of an escape sequence. The \d is not one, so the compiler warns about it. Here is a small program that uses a normal string for the pattern.

#include <iostream>
#include <regex>
 
int main() {
    std::regex date("\d{4}-\d{2}-\d{2}");
    std::cout << std::regex_match("2026-11-05", date) << "\n";
}

With -Wall, g++ 15.2.0 prints this warning for each \d. The first one is shown here.

bad_escape.cpp: In function ‘int main()’:
bad_escape.cpp:5:21: warning: unknown escape sequence: ‘\d’
    5 |     std::regex date("\d{4}-\d{2}-\d{2}");
      |                     ^~~~~~~~~~~~~~~~~~~

The fix is to write the pattern as R"(\d{4}-\d{2}-\d{2})", as shown in the first program.

Forgetting to double the backslash in Java

Java has the same issue but stops the build. The line String p = "\d{4}"; gives this error from javac.

javaerr/Bad.java:3: error: illegal escape character
        String p = "\d{4}";
                     ^
1 error

Write "\\d{4}" instead.

Using regex_search when you need regex_match

regex_search and find succeed when the pattern appears anywhere in the text. A check such as "is this a date?" needs regex_match or matches, or the program will accept "on 2026-11-05" as a date. Test the whole text when the input must be exactly the value.

Forgetting to escape a dot

The pattern \d.\d looks for a digit, any character and a digit. A text such as 1000x5 contains 0x5, so regex_search finds it. Write \. when you mean a dot. The mistake is easy to miss, because the code runs and gives an answer. It is only wrong for input you did not test.

Expecting a pattern to check meaning

The pattern \d{4}-\d{2}-\d{2} accepts 9999-99-99, because it checks the shape of the text and not whether the month is between 1 and 12. A date check that rejects impossible dates needs more code after the pattern.

Practice

Exercise 1: a time pattern

Write a C++ pattern that matches a time written as two digits, a colon and two digits, such as 09:30 or 23:59. Use regex_match to check that 9:30 does not match.

Show the answer

The pattern is R"(\d{2}:\d{2})". The check std::regex_match("09:30", time_of_day) returns true, and std::regex_match("9:30", time_of_day) returns false. The program prints true false.

Exercise 2: find every number

Write a C++ program that prints every run of digits in "order 1234 and 56". Use std::sregex_iterator.

Show the answer

Use the pattern R"(\d+)" with a loop over the matches.

#include <iostream>
#include <regex>
#include <string>
 
int main() {
    std::regex number(R"(\d+)");
    std::string text = "order 1234 and 56";
    for (std::sregex_iterator it(text.begin(), text.end(), number), end; it != end; ++it) {
        std::cout << it->str() << "\n";
    }
}

The output is 1234 on one line and 56 on the next.

Exercise 3: count matches in Java

How many times does matcher.find() return true for the pattern cat on the text concatenate cat? Answer without running the code first, then check.

Show the answer

The answer is 2. The text concatenate contains cat in its middle, and the text ends with a separate cat. The Java loop while (m.find()) n++; prints 2.

Exercise 4: remove a nested repeat

The pattern (x+x+)+y matches the same text as a simpler pattern. Write the simpler pattern and check that both match xxxy.

Show the answer

The simpler pattern is x+y. The output of the check is true true, so both patterns match xxxy. The nested version is the kind that backtracks badly, and the simple one has only one way to split the xs.

Exercise 5: mask phone numbers

Use std::regex_replace to turn each number in the form 555-1234 in the text "call 555-1234 or 555-9876" into XXX-XXXX.

Show the answer

Use the pattern R"(\d{3}-\d{4})" and the replacement "XXX-XXXX".

std::regex phone(R"(\d{3}-\d{4})");
std::cout << std::regex_replace("call 555-1234 or 555-9876", phone, "XXX-XXXX") << "\n";

The output is call XXX-XXXX or XXX-XXXX.

Summary

  • A regex describes the shape of text, using characters, classes, quantifiers, groups and anchors.
  • In C++, write patterns as raw strings such as R"(\d+)", so backslashes are kept.
  • regex_search and find look for a match anywhere. regex_match and matches need the whole text to match.
  • Java Pattern compiles a pattern and Matcher walks through one text. C++ std::regex covers the same jobs with functions and iterators.
  • Backtracking tries each way a pattern can match. Nested repeats on the same characters can make the time double with each extra character.
  • Keep patterns simple, avoid nested repeats on the same characters, and limit the length of input you match.
Check yourself: Words and regex (8 questions) →Practice this topic: coding problems for Character Arrays and Strings →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Reverse words and trim