pointers don't have ends. you are responsible for knowing how big it is.
pointers can be null, if you chain them (eg, in a linked list), and you can use THAT to stop but it takes effort to set that up.
here,
p0 simply points to a. It is the address, or 'array index into the giant array we call computer ram', of the variable a.
p1 is the address of p0. Nothing more, nothing special. The fact that p0 is also a pointer is irrelevant **
the 'end', so to speak, -- these both have 1 item only.
so p1[0] is fine, p1[1] is off the end (that being 2, and you only have 1)
po[0] is fine, p0[1] or p0[100] are bad as above.
p0++ will make p0 invalid.
** it makes the syntax a little odd looking until you get used to seeing * as part of a type. But syntax aside, its not special.
After p0++, becomes a pointer past the end of an object.
ie p0 points to a fictitious object (it can't be dereferenced), but it is still valid (it is a well defined pointer).
For purposes of pointer arithmetic and comparison, a pointer past the end of the last element of an array x of n elements is considered to be equivalent to a pointer to a hypothetical array element n of x and an object of type T that is not an array element is considered to belong to an array with one element of type T. https://eel.is/c++draft/basic.compound#3.4