Зачем использовать двойной указатель? или Зачем использовать указатели на указатели?

когда следует использовать двойной указатель в C? Может ли кто-нибудь объяснить на примере?

Я знаю, что двойной указатель-это указатель на указатель. Зачем мне указатель на указатель?

19 ответов


если вы хотите получить список символов (слово), вы можете использовать char *word

если вы хотите список слов (предложение), вы можете использовать char **sentence

если вам нужен список предложений (монолог), Вы можете использовать char ***monologue

если вам нужен список монологов (биография), вы можете использовать char ****biography

если вам нужен список биографий (био-библиотека), вы можете использовать char *****biolibrary

Если вам нужен список био-библиотек (a ??лол), вы можете использовать char ******lol

... ...

да, я знаю, что это может не быть лучшим структуры данных!--23-->


одна из причин-вы хотите изменить значение указателя, переданного в функцию в качестве аргумента функции, для этого вам требуется указатель на указатель.

простыми словами, использовать ** Если вы хотите сохранить (или сохранить изменение) выделение или назначение памяти даже вне вызова функции. (Итак, передайте такую функцию с двойным указателем arg.)

Это может быть не очень хороший пример, но покажет вам основное использование:

void allocate(int** p)
{
  *p = (int*)malloc(sizeof(int));
}

int main()
{
  int* p = NULL;
  allocate(&p);
  *p = 42;
  free(p);
}

вот простой ответ!!!!

  • допустим, у вас есть указатель, что его значением является адрес.
  • но теперь вы хотите изменить адрес.
  • вы могли бы, выполнив pointer1 = pointer2, и pointer1 теперь будет иметь адрес pointer2.
  • но! если вы хотите, чтобы функция сделала это для вас, и вы хотите, чтобы результат сохранялся после завершения функции, вам нужно выполнить дополнительную работу, вам нужен новый pointer3, чтобы указать pointer1 и передать pointer3 функции.

  • вот забавный пример (сначала взгляните на выходной сигнал, чтобы понять!):

#include <stdio.h>

int main()
{

    int c = 1;
    int d = 2;
    int e = 3;
    int * a = &c;
    int * b = &d;
    int * f = &e;
    int ** pp = &a;  // pointer to pointer 'a'

    printf("\n a's value: %x \n", a);
    printf("\n b's value: %x \n", b);
    printf("\n f's value: %x \n", f);
    printf("\n can we change a?, lets see \n");
    printf("\n a = b \n");
    a = b;
    printf("\n a's value is now: %x, same as 'b'... it seems we can, but can we do it in a function? lets see... \n", a);
    printf("\n cant_change(a, f); \n");
    cant_change(a, f);
    printf("\n a's value is now: %x, Doh! same as 'b'...  that function tricked us. \n", a);

    printf("\n NOW! lets see if a pointer to a pointer solution can help us... remember that 'pp' point to 'a' \n");
     printf("\n change(pp, f); \n");
    change(pp, f);
    printf("\n a's value is now: %x, YEAH! same as 'f'...  that function ROCKS!!!. \n", a);
    return 0;
}

void cant_change(int * x, int * z){
    x = z;
    printf("\n ----> value of 'a' is: %x inside function, same as 'f', BUT will it be the same outside of this function? lets see\n", x);
}

void change(int ** x, int * z){
    *x = z;
    printf("\n ----> value of 'a' is: %x inside function, same as 'f', BUT will it be the same outside of this function? lets see\n", *x);
}
  • и вот результат:
 a's value: bf94c204

 b's value: bf94c208 

 f's value: bf94c20c 

 can we change a?, lets see 

 a = b 

 a's value is now: bf94c208, same as 'b'... it seems we can, but can we do it in a function? lets see... 

 cant_change(a, f); 

 ----> value of 'a' is: bf94c20c inside function, same as 'f', BUT will it be the same outside of this function? lets see

 a's value is now: bf94c208, Doh! same as 'b'...  that function tricked us. 

 NOW! lets see if a pointer to a pointer solution can help us... remember that 'pp' point to 'a' 

 change(pp, f); 

 ----> value of 'a' is: bf94c20c inside function, same as 'f', BUT will it be the same outside of this function? lets see

 a's value is now: bf94c20c, YEAH! same as 'f'...  that function ROCKS!!!. 

добавлять к Аша response, если вы используете один указатель на пример ниже (например, alloc1 ()), вы потеряете ссылку на память, выделенную внутри функции.

void alloc2(int** p) {
   *p = (int*)malloc(sizeof(int));
   **p = 10;
}

void alloc1(int* p) {
   p = (int*)malloc(sizeof(int));
   *p = 10;
}

int main(){
   int *p;
   alloc1(p);
   //printf("%d ",*p);//value is undefined
   alloc2(&p);
   printf("%d ",*p);//will print 10
   free(p);
   return 0;
}

причина, по которой это происходит, заключается в том, что в alloc1 указатель передается по значению. Итак, когда он переназначается на результат malloc звонок внутри alloc1, изменение не относится к коду в другой области.


1. Основная Концепция -

когда вы заявляете следующее : -

1. char *ch - (называется символьным указателем)
- ч содержит адрес одного символа.
- (*ch) будет разыменование значения символа..

2. char * * ch -
'ch' содержит адрес массива символьных указателей. (как в 1)
'*ч' содержит адрес одного символа. (Заметить что это отличается от 1, из-за разницы в декларации).
(**ch) будет разыменование на точное значение символа..

добавление дополнительных указателей расширяет размерность типа данных, от символа к строке, к массиву строк и так далее... Вы можете связать его с 1D, 2d, 3D-матрицей..

таким образом, использование указателя зависит от того, как вы объявите его.

вот простой код..

int main()
{
    char **p;
    p = (char **)malloc(100);
    p[0] = (char *)"Apple";      // or write *p, points to location of 'A'
    p[1] = (char *)"Banana";     // or write *(p+1), points to location of 'B'

    cout << *p << endl;          //Prints the first pointer location until it finds ''
    cout << **p << endl;         //Prints the exact character which is being pointed
    *p++;                        //Increments for the next string
    cout << *p;
}

2. Еще одно применение двойных указателей -
(это также будет охватывать pass by reference)

Предположим, вы хотите обновить символ из функции. Если вы попробуете следующее : -

void func(char ch)
{
    ch = 'B';
}

int main()
{
    char ptr;
    ptr = 'A';
    printf("%c", ptr);

    func(ptr);
    printf("%c\n", ptr);
}

выход будет AA. Это не работает, так как вы" передали значение " функции.

правильным способом сделать это было бы -

void func( char *ptr)        //Passed by Reference
{
    *ptr = 'B';
}

int main()
{
    char *ptr;
    ptr = (char *)malloc(sizeof(char) * 1);
    *ptr = 'A';
    printf("%c\n", *ptr);

    func(ptr);
    printf("%c\n", *ptr);
}

теперь расширьте это требование для обновления строки вместо характер.
Для этого необходимо получить параметр в функции в виде двойного указателя.

void func(char **str)
{
    strcpy(str, "Second");
}

int main()
{
    char **str;
    // printf("%d\n", sizeof(char));
    *str = (char **)malloc(sizeof(char) * 10);          //Can hold 10 character pointers
    int i = 0;
    for(i=0;i<10;i++)
    {
        str = (char *)malloc(sizeof(char) * 1);         //Each pointer can point to a memory of 1 character.
    }

    strcpy(str, "First");
    printf("%s\n", str);
    func(str);
    printf("%s\n", str);
}

в этом примере метод ожидает, что двойной указатель в качестве параметра обновит значение строки.


Я видел очень хороший пример сегодня, с этот блог, как я описываю ниже.

представьте, что у вас есть структура для узлов в связанном списке, который, вероятно, является

typedef struct node
{
    struct node * next;
    ....
} node;

теперь вы хотите реализовать remove_if функция, которая принимает критерий удаления rm как один из аргументов и пересекает связанный список: если запись удовлетворяет критерию (что-то вроде rm(entry)==true), его узел будет удален из списка. В конце концов, remove_if возвращает заголовок (который может отличаться от исходного заголовка) связанного списка.

можно писать

for (node * prev = NULL, * curr = head; curr != NULL; )
{
    node * const next = curr->next;
    if (rm(curr))
    {
        if (prev)  // the node to be removed is not the head
            prev->next = next;
        else       // remove the head
            head = next;
        free(curr);
    }
    else
        prev = curr;
    curr = next;
}

в своем for петли. Сообщение такое:без двойных указателей, вы должны поддерживать prev переменная для повторной организации указателей, и обрабатывать два разных случая.

но с двойными указателями вы можете написать

// now head is a double pointer
for (node** curr = head; *curr; )
{
    node * entry = *curr;
    if (rm(entry))
    {
        *curr = entry->next;
        free(entry);
    }
    else
        curr = &entry->next;
}

тебе не нужен prev вот так вы можете напрямую изменить что prev->next указал на.

чтобы сделать вещи яснее, давайте проследим код немного. Во время удаления:

  1. если entry == *head: это будет *head (==*curr) = *head->next -- head теперь указывает на указатель нового узла заголовка. Вы делаете это, непосредственно изменяя headсодержание нового указателя.
  2. если entry != *head: аналогично, *curr это prev->next указал на, А теперь указывает на entry->next.

независимо от того, в в этом случае вы можете повторно организовать указатели единым способом с помощью двойных указателей.


указатели на указатели также пригодятся как " ручки "для памяти, где вы хотите передать" ручку " между функциями для повторного размещения памяти. Это в основном означает, что функция может изменить память, на которую указывает указатель внутри переменной дескриптора, и каждая функция или объект, который использует дескриптор, правильно укажет на вновь перемещенную (или выделенную) память. Библиотеки любят делать это с "непрозрачными" типами данных, то есть типами данных, которых у вас нет чтобы беспокоиться о том, что они делают с указанной памятью, вы просто передаете "ручку" между функциями библиотеки для выполнения некоторых операций над этой памятью ... библиотечные функции могут выделять и де-выделять память под капотом без необходимости явно беспокоиться о процессе управления памятью или о том, куда указывает дескриптор.

например:

#include <stdlib.h>

typedef unsigned char** handle_type;

//some data_structure that the library functions would work with
typedef struct 
{
    int data_a;
    int data_b;
    int data_c;
} LIB_OBJECT;

handle_type lib_create_handle()
{
    //initialize the handle with some memory that points to and array of 10 LIB_OBJECTs
    handle_type handle = malloc(sizeof(handle_type));
    *handle = malloc(sizeof(LIB_OBJECT) * 10);

    return handle;
}

void lib_func_a(handle_type handle) { /*does something with array of LIB_OBJECTs*/ }

void lib_func_b(handle_type handle)
{
    //does something that takes input LIB_OBJECTs and makes more of them, so has to
    //reallocate memory for the new objects that will be created

    //first re-allocate the memory somewhere else with more slots, but don't destroy the
    //currently allocated slots
    *handle = realloc(*handle, sizeof(LIB_OBJECT) * 20);

    //...do some operation on the new memory and return
}

void lib_func_c(handle_type handle) { /*does something else to array of LIB_OBJECTs*/ }

void lib_free_handle(handle_type handle) 
{
    free(*handle);
    free(handle); 
}


int main()
{
    //create a "handle" to some memory that the library functions can use
    handle_type my_handle = lib_create_handle();

    //do something with that memory
    lib_func_a(my_handle);

    //do something else with the handle that will make it point somewhere else
    //but that's invisible to us from the standpoint of the calling the function and
    //working with the handle
    lib_func_b(my_handle); 

    //do something with new memory chunk, but you don't have to think about the fact
    //that the memory has moved under the hood ... it's still pointed to by the "handle"
    lib_func_c(my_handle);

    //deallocate the handle
    lib_free_handle(my_handle);

    return 0;
}

надеюсь, это поможет,

Джейсон


строки являются отличным примером использования двойных указателей. Сама строка является указателем, поэтому каждый раз, когда вам нужно указать на строку, вам понадобится двойной указатель.


ниже приведен очень простой пример C++, который показывает, что если вы хотите использовать функцию для установки указателя на объект, вам нужен указатель на указатель. В противном случае, указатель будет продолжать возвращаться к null.

(ответ на C++, но я считаю, что это то же самое в C.)

(также для справки: Google ("pass by value c++") = " по умолчанию аргументы в C++ передаются по значению. Когда аргумент передается по значению, значение аргумента копируется в параметр функции.")

Итак, мы хотим установить указатель b равна строке a.

#include <iostream>
#include <string>

void Function_1(std::string* a, std::string* b) {
  b = a;
  std::cout << (b == nullptr);  // False
}

void Function_2(std::string* a, std::string** b) {
  *b = a;
  std::cout << (b == nullptr);  // False
}

int main() {
  std::string a("Hello!");
  std::string* b(nullptr);
  std::cout << (b == nullptr);  // True

  Function_1(&a, b);
  std::cout << (b == nullptr);  // True

  Function_2(&a, &b);
  std::cout << (b == nullptr);  // False
}

// Output: 10100

что происходит на линии Function_1(&a, b);?

  • "ценности"&main::a (адрес) копируется в параметр std::string* Function_1::a. Поэтому Function_1::a является указателем на (т. е. адрес памяти) строки main::a.

  • "ценности"main::b (адрес в памяти) копируется в параметр std::string* Function_1::b. Поэтому теперь в памяти есть 2 из этих адресов, оба нулевых указателя. На линии b = a; локальная переменная Function_1::b затем изменяется на равно Function_1::a ( = &main::a), но переменная main::b остается неизменной. После звонка в Function_1, main::b по-прежнему является нулевым указателем.

что происходит на линии Function_2(&a, &b);?

  • лечение a переменная такая же: внутри функции, Function_2::a - это адрес строки main::a.

  • но переменная b теперь передается как указатель на указатель. "Значение"&main::b (the адрес указателя main::b) копируется в std::string** Function_2::b. Поэтому в Function_2, разыменовав это как *Function_2::b получит доступ и изменит main::b . Итак, линия *b = a; на самом деле параметр main::b (адрес), равной Function_2::a (= адрес main::a) что мы хотеть.

если вы хотите использовать функцию, чтобы изменить вещи, будь то объект или адрес (указатель), вы должны передать указатель на эту вещь. то, что вы на самом деле pass in не может быть изменен (в области вызова), потому что локальная копия сделана.

(исключение, если параметр является ссылкой, например,std::string& a. Но обычно это const. Как правило, если вы называете f(x), если x - это объект вы должны иметь возможность предположить, что f не изменить x. Но если ... --34--> является указателем, то вы должны предположить, что f может изменить объект, на который указывает x.)


простой пример, который вы, вероятно, видели много раз раньше

int main(int argc, char **argv)

во втором параметре у вас есть: указатель на указатель на char.

обратите внимание, что обозначение указателя (char* c) и индекс массива (char c[]) взаимозаменяемы в аргументах функции. Таким образом, вы также можете написать char *argv[]. Другими словами char *argv[] и char **argv взаимозаменяемы.

то, что выше представляет собой на самом деле массив последовательностей символов (командная строка аргументы, которые даются программе при запуске).

см. также ответ для получения более подробной информации о вышеуказанной сигнатуре функции.


например, вы можете убедиться, что, когда вы освобождаете память о чем-то, вы устанавливаете указатель на null впоследствии.

void safeFree(void** memory) {
    if (*memory) {
        free(*memory);
        *memory = NULL;
    }
}

когда вы вызываете эту функцию, вы вызываете ее с адресом указателя

void* myMemory = someCrazyFunctionThatAllocatesMemory();
safeFree(&myMemory);

Теперь myMemory имеет значение NULL, и любая попытка повторного использования будет очень очевидно неправильной.


например, если вы хотите случайный доступ к несмежным данным.

p -> [p0, p1, p2, ...]  
p0 -> data1
p1 -> data2

-- in C

T ** p = (T **) malloc(sizeof(T*) * n);
p[0] = (T*) malloc(sizeof(T));
p[1] = (T*) malloc(sizeof(T));

вы храните указатель p, что указывает на массив указателей. Каждый указатель указывает на элемент данных.

если sizeof(T) большой невозможно выделить непрерывный блок (т. е. с помощью malloc) из sizeof(T) * n байт.


одна вещь, для которой я использую их постоянно, - это когда у меня есть массив объектов, и мне нужно выполнить поиск (двоичный поиск) на них по разным полям.
Я сохраняю исходный массив...

int num_objects;
OBJECT *original_array = malloc(sizeof(OBJECT)*num_objects);

затем создайте массив отсортированных указателей на объекты.

int compare_object_by_name( const void *v1, const void *v2 ) {
  OBJECT *o1 = *(OBJECT **)v1;
  OBJECT *o2 = *(OBJECT **)v2;
  return (strcmp(o1->name, o2->name);
}

OBJECT **object_ptrs_by_name = malloc(sizeof(OBJECT *)*num_objects);
  int i = 0;
  for( ; i<num_objects; i++)
    object_ptrs_by_name[i] = original_array+i;
  qsort(object_ptrs_by_name, num_objects, sizeof(OBJECT *), compare_object_by_name);

вы можете сделать столько отсортированных массивов указателей, сколько вам нужно, а затем использовать двоичный поиск в отсортированном массиве указателей для доступа к объекту, который вам нужен, по данным, которые у вас есть. Исходный массив объектов может оставайтесь несортированными, но каждый массив указателей будет отсортирован по заданному полю.


как сказано, одним из приложений double poinnter является обновление строки, чтобы сделанные изменения отражались обратно.

#include <iostream>
#include <cstring>  // for using strcpy
using namespace std;

void change(char **temp)
{
   strcpy(temp[0],"new");
   strcpy(temp[1],"value");
}

int main()
{
   char **str;
   str = (char **)malloc(sizeof(char *)*3);
   str[0]=(char *)malloc(10);
   str[1]=(char *)malloc(10);
   strcpy(str[0],"old");
   strcpy(str[1],"name");
   char **temp = str;  // always use the temporary variable
   while(*temp!=NULL)
{
    cout<<*temp<<endl;
    temp++;
}
temp = str;   // making it point it to the original head because we have changed the address in while loop above
change(str);
while(*temp!=NULL)
{
   cout<<*temp<<endl;
   temp++;
}

free(temp);
free(str[0]);
free(str[1]);
free(str);

Я использовал двойные указатели сегодня, когда я программировал что-то для работы, поэтому я могу ответить, почему мы должны были использовать их (это первый раз, когда я действительно должен был использовать двойные указатели). Нам пришлось иметь дело с кодированием кадров в реальном времени, содержащихся в буферах, которые являются членами некоторых структур. В кодере нам пришлось использовать указатель на одну из этих структур. Проблема заключалась в том, что наш указатель был изменен, чтобы указать на другие структуры из другого потока. Для использования тока структура в кодер, мне пришлось использовать двойной указатель, чтобы указать на указатель, который был изменен в другом потоке. Поначалу не было очевидно, по крайней мере для нас, что мы должны принять такой подход. В процессе было напечатано много адресов :)).

вы должны использовать двойные указатели при работе с указателями, которые изменяются в других местах вашего приложения. Вы также можете найти двойные указатели, чтобы быть необходимостью, когда вы имеете дело с оборудованием, которое возвращает и адрес вы.


почему двойные указатели?

цель состоит в том, чтобы изменить, на что указывает studentA, используя функцию.

#include <stdio.h>
#include <stdlib.h>


typedef struct Person{
    char * name;
} Person; 

/**
 * we need a ponter to a pointer, example: &studentA
 */
void change(Person ** x, Person * y){
    *x = y; // since x is a pointer to a pointer, we access its value: a pointer to a Person struct.
}

void dontChange(Person * x, Person * y){
    x = y;
}

int main()
{

    Person * studentA = (Person *)malloc(sizeof(Person));
    studentA->name = "brian";

    Person * studentB = (Person *)malloc(sizeof(Person));
    studentB->name = "erich";

    /**
     * we could have done the job as simple as this!
     * but we need more work if we want to use a function to do the job!
     */
    // studentA = studentB;

    printf("1. studentA = %s (not changed)\n", studentA->name);

    dontChange(studentA, studentB);
    printf("2. studentA = %s (not changed)\n", studentA->name);

    change(&studentA, studentB);
    printf("3. studentA = %s (changed!)\n", studentA->name);

    return 0;
}

/**
 * OUTPUT:
 * 1. studentA = brian (not changed)
 * 2. studentA = brian (not changed)
 * 3. studentA = erich (changed!)
 */

надеюсь, следующий пример очистит некоторые понятия, касающиеся указателей и двойных указателей , их различий и использования в общих сценариях.

    int* setptr(int *x)
    {
        printf("%u\n",&x);
        x=malloc(sizeof(int));
        *x=1;
        return x;
    }

In the above function setptr we can manipulate x either
1. by taking fn arg as int *x , doing  malloc and setting value of x and return x 
Or
    2. By taking arg as int ** and malloc and then set  **x value to some value.
Note: we cant set any general pointer directly without doing  malloc.Pointer indicates that it is a type of variable which can hold address of any data type.Now either we define a variable and give reference to it or we declare a pointer(int *x=NULL) and allocate some memory to it inside the called function where we pass x or a reference to it .. In either case we need to have address of a memory in the  pointer and in the case pointer initially points  to NULL or it is defined like int *x where it points  to any random address then we need to assign a valid memory address to pointer 

    1. either we need to allocate memory to it by malloc

    int *x=NULL means its address is 0.
    Now we need to either o following
    1.



    void main()
        {
            int *x;
            x=malloc
            *x=some_val;
        }
        Or
        void main()
        {
            int *x
            Fn(x);
        }

        void Fn(int **x)
        {
            *x=malloc;
            **x=5;
        }
        OR
        int * Fn(int *x)
        {
            x=malloc();
            *x=4;
            Return x;
        }


        2. Or we need to point it to a valid memory like a defined variable inside the function where pointer is defined.


        OR
        int main()
        {
            int a;
            int *x=&a;
            Fn(x);
            printf("%d",*x);
        }
        void Fn(int *x)
        {
            *x=2;
        }


     in both cases value pointed by x is changed inside fn

    But suppose if we do like


    int main()
    {
        int *x=NULL;
        printf("%u\n",sizeof(x));
        printf("%u\n",&x);
        x=setptr(x);
        //*x=2;
        printf("%d\n",*x);
        return 0;
    }

/* output
4
1
*/

#include<stdio.h>
void setptr(int *x)
{
    printf("inside setptr\n");
    printf("x=%u\n",x);
    printf("&x=%u\n",&x);
    //x=malloc(sizeof(int));
    *x=1;
    //return x;
}
int main()
{
    int *x=NULL;
    printf("x=%u\n",x);
    printf("&x=%u\n",&x);
    int a;
    x=&a;
    printf("x=%u\n",x);
    printf("&a=%u\n",&a);
    printf("&x=%u\n",&x);
    setptr(x);
    printf("inside main again\n");

    //*x=2;
    printf("x=%u\n",x);
    printf("&x=%u\n",&x);
    printf("*x=%d\n",*x);
    printf("a=%d\n",a);
    return 0;
}

применение двойной указатель, как показано на Матур Bhavuk кажется неправильным. Вот следующий пример является допустимым

void func(char **str)
{
     strcpy(str[0],"second");
}

int main(){

    char **str;
    str = (char **)malloc(sizeof(char*)*1); // allocate 1 char* or string
    str[0] = (char *)malloc(sizeof(char)*10);      // allocate 10 character
    strcpy(str[0],"first");            // assign the string
    printf("%s\n",*str);
    func(str);            
    printf("%s\n",*str);           
    free(str[0]); 
    free(str);
}

следующий пример, который я даю, даст понимание или интуицию о том, как работают двойные указатели, я пройду через шаги

1) try to understand the following statements
   char **str ;

   a) str is of type char ** whose value is an address of another pointer.
   b) *str is of type char * whose value is an address of variable or (it is a string itself).
   c) **str is of type char ,gives the value stored, in this case a character.

ниже приведен код, к которому вы можете относиться к вышеуказанным пунктам (a,b,c), чтобы понять

str = (char **)malloc(sizeof(char *) *2); // here i am assigning the mem for two char *
       str[0]=(char *)"abcdefghij"; // assignin the value
       str[1]=(char *)"xyzlmnopqr"; 

теперь для печати значения i.строки e в массиве, просто посмотрите на точку b, В случае строки значение, а также адрес одинаковы, поэтому нет необходимости разыменовать его снова.

cout<<*str<<endl;   // abcdefghij;

теперь печатать следующая строка, выходите из одного разыменования i.e ( * ) от *str до str , а затем приращение, как показано ниже

str++;

теперь печатать строку

cout<<*str<<endl;        //xyzlmnopqr

теперь, чтобы напечатать только символы в строке, обратитесь к пункту c)

cout<<**str<<endl;  // prints the first character i.e "a"

теперь, чтобы напечатать следующий символ строки i.e " b" выйдите из 1 оператора разыменования и увеличьте его i.e переход от * * str к * str и do * str++

*str++;

теперь выведите символ

cout<<**str<<endl;  // prints the second character i.e "b"

С два массива ("abcdefghij", "xylmnopqr") хранятся в непрерывном блоке памяти, если то же самое делается для увеличения адреса, все символы двух строк будут напечатаны