The standard input and standard output (and standard error) streams can point to basically any I/O device.  This is commonly a terminal, but it can also be a file, a pipe, a network socket, a printer, etc.  What exactly those streams direct their I/O to is usually determined by the process that launches your process, be that a shell or a daemon like cron or apache, but a process can redirect those streams itself it it would like.
I'll use Linux as an example, but the concepts are similar on most other OSes.  On Linux, the standard input and standard output stream are represented by file descriptors 0 and 1.  The macros STDIN_FILENO and STDOUT_FILENO are just for convenience and clarity.  A file descriptor is just a number that matches up to some file description that the OS kernel maintains that tells it how to write to that device.  That means that from a user-space process's perspective, you write to pretty much anything the same way: write(some_file_descriptor, some_string, some_string_length) (higher-level I/O functions like printf or cout are just wrappers around one or more calls to write).  To the process, it doesn't matter what type of device some_file_descriptor represents.  The OS kernel will figure that out for you and pass your data to the appropriate device driver.
The standard way to launch a new process is to call fork to duplicate the parent process, and then later to call one of the exec family of functions in the child process to start executing some new program.  In between, it will often close the standard streams it inherited from its parent and open new ones to redirect the child process's output somewhere new.  For instance, to have the child pipe its output back to the parent, you could do something like this in C++:
int main()
{
    // create a pipe for the child process to use for its
    // standard output stream
    int pipefds[2];
    pipe(pipefds);
    // spawn a child process that's a copy of this process
    pid_t pid = fork();
    if (pid == 0)
    {
        // we're now in the child process
        // we won't be reading from this pipe, so close its read end
        close(pipefds[0]);
        // we won't be reading anything
        close(STDIN_FILENO);
        // close the stdout stream we inherited from our parent
        close(STDOUT_FILENO);
        // make stdout's file descriptor refer to the write end of our pipe
        dup2(pipefds[1], STDOUT_FILENO);
        // we don't need the old file descriptor anymore.
        // stdout points to this pipe now
        close(pipefds[1]);
        // replace this process's code with another program
        execlp("ls", "ls", nullptr);
    } else {
        // we're still in the parent process
        // we won't be writing to this pipe, so close its write end
        close(pipefds[1]);
        // now we can read from the pipe that the
        // child is using for its standard output stream
        std::string read_from_child;
        ssize_t count;
        constexpr size_t BUF_SIZE = 100;
        char buf[BUF_SIZE];
        while((count = read(pipefds[0], buf, BUF_SIZE)) > 0) {
            std::cout << "Read " << count << " bytes from child process\n";
            read_from_child.append(buf, count);
        }
        std::cout << "Read output from child:\n" << read_from_child << '\n';
        return EXIT_SUCCESS;
    }
}
Note: I've omitted error handling for clarity
This example creates a child process and redirects its output to a pipe.  The program run in the child process (ls) can treat the standard output stream just as it would if it were referencing a terminal (though ls changes some behaviors if it detects its standard output isn't a terminal).
This sort of redirection can also be done from a terminal.  When you run a command you can use the redirection operators to tell your shell to redirect that commands standard streams to some other location than the terminal.  For instance, here's a convoluted way to copy a file from one machine to another using an sh-like shell:
gzip < some_file | ssh some_server 'zcat > some_file'
This does the following:
- create a pipe
- run gzipredirecting its standard input stream to read from "some_file" and redirecting its standard output stream to write to the pipe
- run sshand redirect its standard input stream to read from the pipe
- on the server, run zcatwith its standard input redirected from the data read from the ssh connection and its standard output redirected to write to "some_file"