I have an array of bytes, if I just want X number of bytes in a specific location of that array, I am wondering if this is valid.
Also my code is crashing here, the following piece of code was provided to me.. but it crash on my system and don't know why.
rawDataPtr is my array of bytes, as you can guess the value that i am interested on starts on positon 4.
float TempFloat = COMMON_ConvertByteArrayToFloat(&rawDataPtr[3]);
Now the function.
union {
uint8_t tmpArray[4];
float tmpFloat;
}value;
float COMMON_ConvertByteArrayToFloat(uint8_t *data) {
value.tmpArray[0] = data[3];
value.tmpArray[1] = data[2];
value.tmpArray[2] = data[1];
value.tmpArray[3] = data[0];
return value.tmpFloat;
}
I have an array of bytes, if I just want X number of bytes in a specific location of that array, I am wondering if this is valid.
Supposing that "array" rawDataPtr has at least four elements, &rawDataPtr[3] is a valid pointer to the fourth. If the element type is different from uint8_t then you technically ought to cast to uint8_t * when you pass it to your function:
float TempFloat = COMMON_ConvertByteArrayToFloat((uint8_t *) &rawDataPtr[3]);
... but in practice that is unlikely to make any difference.
Your function is fine in itself. In particular,
there is no problem accessing data[0] ... data[3] as you do, provided that data is a valid pointer, and that referencing data[3] does not constitute an attempt to read outside the bounds of the object (in)to which data points. In the usage you present, that corresponds to rawDataPtr being an array of at least 7 bytes.
it is permissible to write to one element of a union and subsequently read from another, though there are some caveats. In particular, your code assumes that floats are four bytes in size; if they are longer on your system then your code has unspecified behavior.
Additional notes:
I assume it is intentional that you reverse the byte order from your array to form the value of your float. Whether that is appropriate is a function of your particular data.
If you are going to perform the bytes -> float conversion via a union, then it is advisable to use a union object that is local to the function. You presently appear to be using a file-scoped instance instead.
Your function is entirely capable of producing a trap representation in value.tmpFloat, and then attempting to read and return that value. If it does so then the resulting behavior is undefined.
Overall, if the function is crashing your program then it is because you are feeding it bad data. In that case, it is likely that either the caller's rawDataPtr is not a valid pointer (and note that it's name suggests that it is a pointer not an array), or it points to fewer than four bytes before the end of an object, or the bytes to which it points form a trap representation when converted.
Related
I've recently been working on wifi sniffer using ESP8266, in order to sniff wifi packets there is a function called wifi_set_promiscuous_rx_cb(wifi_sniffer_packet_handler) it takes a callback function as a parameter and passes buffer pointer, which has the packet info and length of the packet as parameters to the callback function , wifi_sniffer_packet_handler(uint8_t *buff, uint16_t len) is the call back function. i am not understanding what these two statements are doing
const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buff;
const wifi_ieee80211_packet_t *ipkt = (wifi_ieee80211_packet_t *)ppkt->payload;
wifi_promiscuous_pk_t is a structure
typedef struct{
wifi_pkt_rx_ctrl_t rx_ctrl; /**< metadata header */
uint8_t payload[0]; /**< Data or management payload. Length of payload is described by rx_ctrl.sig_len. Type of content determined by packet type argument of callback. */
} wifi_promiscuous_pkt_t;
and wifi_ieee80211_packet_t is another structure
typedef struct
{
wifi_ieee80211_mac_hdr_t hdr;
uint8_t payload[2]; /* network data ended with 4 bytes csum (CRC32) */
} wifi_ieee80211_packet_t;
how the data in the *buff is assigned to these structures, are the above statements responsible for the assignment
i've seen many stackoverflow questions and many other threads regarding this but none of the posts clarified my doubt
The packet coming from the radio presumably has a format like this:
| metadata | mac hdr | mac payload |
This is stored initially as a uint8_t array pointed to by buff.
Now, you typecast the buffer into the first structure type:
const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buff;
This is what our buffer conceptually looks like now:
|<---------------------- buff ---------------------->| buff
|<-- rx_ctrl -->|<------- payload ------------------>| ppkt
The structure is kind of overlaid on top of the byte array and since the metadata is always a fixed size, ppkt->rx_ctrl will now contain this metadata header.
But what about payload? The declaration indicates payload to be a zero sized array. This is special syntax to tell the compiler that this is a 'placeholder' for an arbitrarily large array of user-managed memory. (Please note that this is an outdated GCC extension and might produce compile errors on most modern compilers. The post C99 approach is to simply leave out the array dimension like this: uint8_t payload[])
Hence ppkt->payload is now an array starting with the first byte beyond the metadata. Since C doesn't bother with bounds checking, you can access any byte of this payload with ppkt->payload[i]
Now, we need to get to the mac payload by parsing out the mac header, which is again of a fixed size. This is accomplished by:
const wifi_ieee80211_packet_t *ipkt = (wifi_ieee80211_packet_t *)ppkt->payload;
In this case, we discard the header (rx_ctrl) and overlay the wifi_ieee80211_packet_t struct on top of the payload portion of the buffer.
Looking at our buffer again, this is what has happened to it:
|<---------------------- buff ---------------------->| buff
|<-- rx_ctrl -->|<------- payload ------------------>| ppkt
|<-- hdr -->|<----- payload -------->| ipkt
Now you can easily get to the mac payload in the original packet with ipkt->payload.
This is a very common idiom used when parsing packets in networking protocol stacks.
PS: However, please note that typecasting between incompatible types is risky business. Firstly, you have to understand and account for the alignment of structure members. Also, depending on how your structures have been defined, this might result in violation of strict aliasing rules and hence, invoke undefined behaviour on certain compilers.
The goal of converting from a pointer to bytes that have been read from the network to a pointer to a structure is to interpret the bytes as that structure type. This relies on compiler features that are not required by the C standard.
The code (wifi_promiscuous_pkt_t *)buff converts the pointer buff to a pointer to the type wifi_promiscuous_pkt_t. This relies on buff being aligned as required for the structure,1 and it requires that the conversion produce a pointer to the same place in memory but with a different type.2
Then const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buff; defines a new pointer ppkt and initializes it to point to the same memory buff points to. The goal is to have references such as ppkt->rx_ctrl interpret the data as the data for the wifi_promiscuous_pkt_t structure instead of as the raw uint8_t bytes.
The C standard does not define the behavior here.3 Compilers may support this by defining the behavior themselves. Notably, the compiler needs to support aliasing memory: Accessing memory using a type other than the original type used to define or store data there.
For illustration, let’s say the type of rx_ctrl, wifi_pkt_rx_ctrl_t, is a four-byte int. Then, when these requirements above are met, ppkt->rx_ctrl_t will access the first four bytes at buff and interpret them as if they were an int.
(If those requirements are not met, then a C-standard way to reinterpret the bytes is to make a new object and copy the data into it: wifi_promiscuous_pkt_t temp; memcpy(temp, buff, sizeof temp + extra for the payload length);, after which temp.rx_ctrl will provide the int value. However, depending on circumstances and compiler quality, that can cause a lot of extra memory copying we would like to avoid. Accessing the memory directly is preferable, if it is supported. Another alternative is to read the data directly from the network into the target object instead of into a buffer of bytes.)
After this, const wifi_ieee80211_packet_t *ipkt = (wifi_ieee80211_packet_t *)ppkt->payload; does the same thing with ppkt->payload instead of buff. Once ppkt has been set up, ppkt->payload points to the bytes starting after the rx_ctrl member.4 This pointer is then converted to point to the type wifi_ieee80211_packet_t, and ipkt is initialized with the new pointer.
As before, the intent is that ipkt->hdr and ipkt->payload will refer to the data in the first structure’s payload area as if that data contained a wifi_ieee80211_packet_t structure. However, there is another problem here. As noted above, buff may pointed to well-aligned memory if it was allocated with malloc. But ipkt does not point to the same place as buff. It points to ppkt->payload, which is some number of bytes after the start because it follows the rx_ctrl member. So we do not know, from this code alone, that ipkt is properly aligned for a wifi_ieee80211_packet_t. Possibly it is, if the size of the rx_ctrl member is a multiple of the alignment requirement for the hdr member.
Without comments in the code speaking to this, this requirement may have been neglected. The code might work because rx_ctrl is an okay size for this to work, but it might generate an alignment trap. If not now, then possibly in the future when the types involved change.
Footnotes
1 If buff points to the start of memory allocated with malloc or another standard memory allocation routine, it will be correctly aligned for any fundamental type. Or, if it was defined as an array, correct alignment can be requested with the standard _Alignas keyword or possibly with a compiler extension. If it was merely defined as an ordinary array of uint8_t, is not guaranteed to be aligned as required for this use.
2 For general pointer-to-object conversions, the standard only requires that a pointer converted to another pointer-to-object type and then back to its original type points to the original object. It does not guarantee the pointer is otherwise usable in the other type. But this is common in C implementations.
3 In general, accessing an array of uint8_t using other types violates the rule in C 2018 6.5 7, which says that memory which has been defined with a certain type, or, for dynamic memory, assigned with a certain type, shall be accessed only by certain “matching” types or by a “character” (byte) type.
4 Formally, ppkt->payload designates an array, and that array is automatically converted to a pointer. Also, the array is declared to have zero elements. That is not defined by the C standard, but GCC and other compilers supported it as a way to allow variable length data at the end of a structure. Since C 1999, the standard way to do this is to declare the member with [] instead of [0].
When I used the structure:
|<-- rx_ctrl -->|<------- payload ------------------>|
I get a misalignment in the payload data by a shift of 2 bytes to the left.
When I replace rx_ctrl part with a field e.g. unsigned frame_ctrl:16 I do not see this shift. I do have to typecast frame_ctrl to rx_ctrl to get the details.
I assume this is because I use esp32-S2 which assumes teh rx_ctrl is 32 bits regardless of its definition as 16 bits.
As I understand it, all of the cases where C has to handle an address involve the use of a pointer. For example, the & operand creates a pointer to the program, instead of just giving the bare address as data (i.e it never gives the address without using a pointer first):
scanf("%d", &foo)
Or when using the & operand
int i; //a variable
int *p; //a variable that store adress
p = &i; //The & operator returns a pointer to its operand, and equals p to that pointer.
My question is: Is there a reason why C programs always have to use a pointer to manage addresses? Is there a case where C can handle a bare address (the numerical value of the address) on its own or with another method? Or is that completely impossible? (Being because of system architecture, memory allocation changing during and in each runtime, etc). And finally, would that be useful being that addresses change because of memory management? If that was the case, it would be a reason why pointers are always needed.
I'm trying to figure out if the use pointers is a must in C standardized languages. Not because I want to use something else, but because I want to know for sure that the only way to use addresses is with pointers, and just forget about everything else.
Edit: Since part of the question was answered in the comments of Eric Postpischil, Michał Marszałek, user3386109, Mike Holt and Gecko; I'll group those bits here: Yes, using bare adresses bear little to no use because of different factors (Pointers allow a number of operations, adresses may change each time the program is run, etc). As Michał Marszałek pointed out (No pun intended) scanf() uses a pointer because C can only work with copies, so a pointer is needed to change the variable used. i.e
int foo;
scanf("%d", foo) //Does nothing, since value can't be changed
scanf("%d", &foo) //Now foo can be changed, since we use it's address.
Finally, as Gecko mentioned, pointers are there to represent indirection, so that the compiler can make the difference between data and address.
John Bode covers most of those topics in it's answer, so I'll mark that one.
A pointer is an address (or, more properly, it’s an abstraction of an address). Pointers are how we deal with address values in C.
Outside of a few domains, a “bare address” value simply isn’t useful on its own. We’re less interested in the address than the object at that address. C requires us to use pointers in two situations:
When we want a function to write to a parameter
When we need to track dynamically allocated memory
In these cases, we don’t really care what the address value actually is; we just need it to access the object we’re interested in.
Yes, in the embedded world specific address values are meaningful. But you still use pointers to access those locations. Like I said above, a pointer is an address for our purposes.
C allows you to convert pointers to integers. The <stdint.h> header provides a uintptr_t type with the property that any pointer to void can be converted to uintptr_t and back, and the result will compare equal to the original pointer.
Per C 2018 6.3.2.3 6, the result of converting a pointer to an integer is implementation-defined. Non-normative note 69 says “The mapping functions for converting a pointer to an integer or an integer to a pointer are intended to be consistent with the addressing structure of the execution environment.”
Thus, on a machine where addresses are a simple numbering scheme, converting a pointer to a uintptr_t ought to give you the natural machine address, even though the standard does not require it. There are, however, environments where addresses are more complicated, and the result of converting a pointer to an integer may not be straightforward.
int i; //a variable
int *p; //a variable that store adres
i = 10; //now i is set to 10
p = &i; //now p is set to i address
*p = 20; //we set to 20 the given address
int tab[10]; // a table
p = tab; //set address
p++; //operate on address and move it to next element tab[1]
We can operate on address by pointers move forward or backwards. We can set and read from given address.
In C if we want get return values from functions we must use pointers. Or use return value from functions, but that way we can only get one value.
In C we don't have references therefore we must use pointers.
void fun(int j){
j = 10;
}
void fun2(int *j){
*j = 10;
}
int i;
i = 5; // now I is set to 5
fun(i);
//printf i will print 5
fun2(&i);
//printf I will print 10
I'm learning about pointers and have been told this: "The purpose of pointers is to allow you to manually, directly access a block of memory."
Say I have int var = 5;. Can't I use the variable 'var' to access the block of memory where the value 5 is stored, since I can change the value of the variable whenever I want var = 6;? Do I really need a pointer when I can access any variable's value just by using its variable, instead of using a pointer that points to the address where the value is stored?
"The purpose of pointers is to allow you to manually, directly access a block of memory."
This is not always true. Consider
*(int*)(0x1234) = some_value;
this is "direct" memory access. Though
int a = some_value, *ptr = &a;
*ptr = some_other_value;
you are now accessing a indirectly.
Can't I use the variable 'var' to access the block of memory where the
value 5 is stored, since I can change the value of the variable
whenever I want var = 6; ?
Surely; but the semantics is different.
Do I really need a pointer when I can access any variable's value just by using its variable, instead of using a pointer that points to the address where the value is stored?
No, you don't. Consider the first example: within the scope where a has been declared, modifying its value through ptr is rather pointless! However, what if you are not within the scope of a? That is
void foo(int x)
{
x = 5;
}
int main(void)
{
int x = 10;
foo(x);
}
In foo, when you do x = 5, there is an ambiguity: do you want to modify foo::x or main::x? In the latter case that has to be "requested" explicitly and the fact that happens through pointers -or, better, through indirection- is a coincidence and a language choice. Other languages have others.
Pointer types have some traits that make them really useful:
It's guaranteed that a pointer will be so large that it can hold any address that is supported by the architecture (on x86, that is 32 bits a.k.a. 4 bytes, and an x64 64 bits a.k.a. 8 bytes).
Dereferencing and indexing the memory is done per object, not per byte.
int buffer[10];
char*x = buffer;
int*y = (int*)buffer;
That way, x[1] isn't y[1]
Both is not guaranteed if you use simple ints to hold your values. The first trait is at least guaranteed by uintptr_t (not by size_t though, although most of the time they have the same size - except that size_t can be 2 bytes in size on systems with segmented memory layout, while uintptr_t is still 4 bytes in size).
While using ints might work at first, you always:
have to turn the value into a pointer
have to dereference the pointer
and have to make sure that you don't go beyond certain values for your "pointer". For a 16 bit int, you cannot go beyond 0xFFFF, for 32 bit it's 0xFFFF FFFF - once you do, your pointer might overflow without you noticing it until it's too late.
That is also the reason why linked lists and pointers to incomplete types work - the compiler already knows the size of the pointers you are going to you, and just allocates memory for them. All pointers have the same size (4 or 8 bytes on 32-bit/64-bit architectures) - the type that you assign them just tells the compiler how to dereference the value. char*s take up the same space as void*s, but you cannot dereference void*s. The compiler won't let you.
Also, if you are just dealing with simple integers, there's a good chance that you will slow down your program significantly do to something called "aliasing", which basically forces the compiler to read the value of a given address all the time. Memory accesses are slow though, so you want to optimized these memory accesses out.
You can compare a memory address to a street address:
If you order something, you tell the shop your address, so that they can send you what you bought.
If you don't want to use your address, you have to send them your house, such that they can place the parcel inside.
Later they return your house to you. This is a bit more cumbersome than using the address!
If you're not at home, the parcel can be delivered to your neighbor if they have your address, but this is not possible if
you sent them your house instead.
The same is true for pointers: They are small and can be transported easily, while the object they point to
might be large, and less easily transportable.
With pointer arithmetics, pointers can also be used to access other objects than the one they originally pointed to.
You call a function from main() or from another function, the function you called can only return 1 value.
Let say you want 3 values changed, you pass them to the called function as pointers. That way you don't have to use global values.
One possible advantage is that it can make it easier to have one function modify a variable that will be used by many other functions. Without pointers, your best option is for the modifying function to return a value to the caller and then to pass this value to the other functions. That can lead to a lot of passing around. Instead, you can give the modifying function a pointer where it stores its output, and all the other functions directly access that memory address. Kind of like global variables.
So far, I have dealt a bit with pointers and structs, but I'm not sure how to allocate an array of a structure at runtime - see below.
N.B. "user_size" is initialized at runtime.
typedef struct _COORDS
{
double x;
double y;
double area;
double circumference;
int index;
wchar_t name[16];
} COORDS, *PCOORDS;
PCOORDS pCoords = (PCOORDS)malloc(sizeof(COORDS)* user_size);
// NULL ptr check omitted
After that, can I just access pCoords[0] to pCoords[user_size-1] as with an ordinary array of ints?
More to the point: I don't understand how the compiler superimposes the layout of the structure on the alloc'ed memory? Does it even have to or am I overthinking this?
The compiler does not super-impose the structure on the memory -- you tell it to do so!
An array of structures is accessed by multiplying the index of one element by its total size. pCoords[3], for example, is "at" pCoords + 3*sizeof(COORDS) in memory.
A structure member is accessed by its offset (which is calculated by the sizes of the elements before it, taking padding into account). So member x is at an offset 0 from the start of its container, pCoords plus sizeof(COORDS) times the array element index; and y is sizeof(x) after that.
Since you tell the compiler that (1) you want a contiguous block of memory with a size for user_size times the size of a single COORD, and (2) then access this through pCoords[2].y, all it has to do is multiply and add, and then read the value (literally) in that memory address. Since the type of y is double, it reads and interprets the raw bytes as a double. And usually, it gets it right.
The only problem that can arise is when you have multiple pointers to that same area of memory. That could mean that the raw bytes "at" an address may need interpreting as different types (for instance, when one pointer tells it to expect an int and another a double).
With the provisio that the valid range is acutally 0..user_size - 1, your code is fine.
You are probably overthinking this. The compiler does not "superimpose" anything on the malloc'ed memory - that is just a bunch of bytes.
However, pointers are typed in C, and the type of the pointer determines how the memory is interpreted when the pointer is derefenced or used in pointer artihmetic. The compiler knows the memory layout of the struct. Each field has a defined size and an offset, and the overall size of the struct is known, too.
In your case, the expression pCoords[i].area = 42.0 is equivalent to
char *pByte = (char*)pCoords + sizeof(COORDS) * i + offsetof(COORDS, area);
double *pDouble = (pDouble*)pByte;
*pDouble = 42.0;
I've just started to learn C so please be kind.
From what I've read so far regarding pointers:
int * test1; //this is a pointer which is basically an address to the process
//memory and usually has the size of 2 bytes (not necessarily, I know)
float test2; //this is an actual value and usually has the size of 4 bytes,
//being of float type
test2 = 3.0; //this assigns 3 to `test2`
Now, what I don't completely understand:
*test1 = 3; //does this assign 3 at the address
//specified by `pointerValue`?
test1 = 3; //this says that the pointer is basically pointing
//at the 3rd byte in process memory,
//which is somehow useless, since anything could be there
&test1; //this I really don't get,
//is it the pointer to the pointer?
//Meaning, the address at which the pointer address is kept?
//Is it of any use?
Similarly:
*test2; //does this has any sense?
&test2; //is this the address at which the 'test2' value is found?
//If so, it's a pointer, which means that you can have pointers pointing
//both to the heap address space and stack address space.
//I ask because I've always been confused by people who speak about
//pointers only in the heap context.
Great question.
Your first block is correct. A pointer is a variable that holds the address of some data. The type of that pointer tells the code how to interpret the contents of the address being held by that pointer.
The construct:
*test1 = 3
Is called the deferencing of a pointer. That means, you can access the address that the pointer points to and read and write to it like a normal variable. Note:
int *test;
/*
* test is a pointer to an int - (int *)
* *test behaves like an int - (int)
*
* So you can thing of (*test) as a pesudo-variable which has the type 'int'
*/
The above is just a mnemonic device that I use.
It is rare that you ever assign a numeric value to a pointer... maybe if you're developing for a specific environment which has some 'well-known' memory addresses, but at your level, I wouldn't worry to much about that.
Using
*test2
would ultimately result in an error. You'd be trying to deference something that is not a pointer, so you're likely to get some kind of system error as who knows where it is pointing.
&test1 and &test2 are, indeed, pointers to test1 and test2.
Pointers to pointers are very useful and a search of pointer to a pointer will lead you to some resources that are way better than I am.
It looks like you've got the first part right.
An incidental thought: there are various conventions about where to put that * sign. I prefer mine nestled with the variable name, as in int *test1 while others prefer int* test1. I'm not sure how common it is to have it floating in the middle.
Another incidental thought: test2 = 3.0 assigns a floating-point 3 to test2. The same end could be achieved with test2=3, in which case the 3 is implicitly converted from an integer to a floating point number. The convention you have chosen is probably safer in terms of clarity, but is not strictly necessary.
Non-incidentals
*test1=3 does assign 3 to the address specified by test.
test1=3 is a line that has meaning, but which I consider meaningless. We do not know what is at memory location 3, if it is safe to touch it, or even if we are allowed to touch it.
That's why it's handy to use something like
int var=3;
int *pointy=&var;
*pointy=4;
//Now var==4.
The command &var returns the memory location of var and stores it in pointy so that we can later access it with *pointy.
But I could also do something like this:
int var[]={1,2,3};
int *pointy=&var;
int *offset=2;
*(pointy+offset)=4;
//Now var[2]==4.
And this is where you might legitimately see something like test1=3: pointers can be added and subtracted just like numbers, so you can store offsets like this.
&test1 is a pointer to a pointer, but that sounds kind of confusing to me. It's really the address in memory where the value of test1 is stored. And test1 just happens to store as its value the address of another variable. Once you start thinking of pointers in this way (address in memory, value stored there), they become easier to work with... or at least I think so.
I don't know if *test2 has "meaning", per se. In principle, it could have a use in that we might imagine that the * command will take the value of test2 to be some location in memory, and it will return the value it finds there. But since you define test2 as a float, it is difficult to predict where in memory we would end up, setting test2=3 will not move us to the third spot of anything (look up the IEEE754 specification to see why). But I would be surprised if a compiler would allow such thing.
Let's look at another quick example:
int var=3;
int pointy1=&var;
int pointy2=&pointy1;
*pointy1=4; //Now var==4
**pointy2=5; //Now var==5
So you see that you can chain pointers together like this, as many in a row as you'd like. This might show up if you had an array of pointers which was filled with the addresses of many structures you'd created from dynamic memory, and those structures contained pointers to dynamically allocated things themselves. When the time comes to use a pointer to a pointer, you'll probably know it. For now, don't worry too much about them.
First let's add some confusion: the word "pointer" can refer to either a variable (or object) with a pointer type, or an expression with the pointer type. In most cases, when people talk about "pointers" they mean pointer variables.
A pointer can (must) point to a thing (An "object" in standards parlance). It can only point to the right kind of thing; a pointer to int is not supposed to point to a float object. A pointer can also be NULL; in that case there is no thing to point to.
A pointertype is also a type, and a pointer object is also an object. So it is allowable to construct a pointer to pointer: the pointer-to-pointer just stores the addres of the pointer object.
What a pointer can not be:
It cannot point to a value: p = &4; is impossible. 4 is a literal value, which is not stored in an object, and thus has no address.
the same goes for expressions: p = &(1+4); is impossible, because the expression "1+4" does not have a location.
the same goes for return value p = &sin(pi); is impossible; the return value is not an object and thus has no address.
variables marked as "register" (almost distinct now) cannot have an address.
you cannot take the address of a bitfield, basically because these can be smaller than character (or have a finer granularity), hence it would be possible that different bitmasks would have the same address.
There are some "exceptions" to the above skeletton (void pointers, casting, pointing one element beyond an array object) but for clarity these should be seen as refinements/amendments, IMHO.