Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Square brackets after pointer declaration/reference

I am porting code from C to Go, and have encountered something in C I've never seen before. I like to think of myself as being pretty competent and self sufficient when it comes to searching out and learning about new concepts like this, however in this case I have been absolutely unable to do so.

The code I'm porting is declaring and referencing (what I think) are pointers, but they are surrounded by parenthesis and then indexed with square brackets. Some examples:

Struct definition

typedef struct {
    uint32_t *S;
    uint32_t (*S0)[2], (*S1)[2], (*S2)[2];
} example;

Assigning values:

ex.S0 = (uint32_t (*)[2])ex.S;
ex.S1 = ex.S0 + (1 << someInt1) * someInt2;
ex.S2 = ex.S1 + (1 << someInt1) * someInt2;

As is, the naming, declaration and use aren't enough for me to formulate a search that will tell me more. So, for S0, S1, and S2, what are they, and where can I learn more about declaring and using them?

like image 442
Levi Noecker Avatar asked Mar 01 '23 12:03

Levi Noecker


2 Answers

Brackets are part of the grammar for a declarator. They denote arrays.

In C, a declaration is a list of declaration-specifiers followed by a list of declarators with optional initializations.

The declaration-specifiers specify a type like int, double, long long, struct foo, or a typedef name. (They also include additional kinds of specifiers like extern and inline, which are not of consequence in this answer.) Whatever type T this is, the declarators then specify things that have the type T.

Declarators have several forms:

  • A plain name, D, says D has the type T.
  • A declarator in parentheses, (D), means the expression (D) has the type T, which means D also has type T, so this has the same meaning as a plain name in a declarator. However, it groups the declarator for further parsing.
  • A declarator with brackets, D[expression], means the expression D[i] will have the type T, which means D must be an array of the type.
  • A declarator with an asterisk, *D, means the expression *D will have the type T, so D must be a pointer to T.
  • A declarator with parentheses, D(), means the expression D() will have the type T, which means D must be a function that returns T.

These forms can be combined. A declarator (*D)[] says (*D)[] has type T, so (*D) is an array of T, so *D is also an array of T, so D is a pointer to an array of T.

Essentially, a declarator is a picture of how a name will be used in expressions, and the actual type of the name is figured out by working backwards from what the type of that expression is.

(There are additional parts of declarators not discussed above, such as parameter lists for functions and some options for the contents of brackets when declaring arrays.)

like image 90
Eric Postpischil Avatar answered Mar 12 '23 06:03

Eric Postpischil


Suppose you had a two-dimensional array:

uint32_t array[][2] = {
    {1, 2},
    {3, 4},
    {5, 6},
    {7, 8},
    {9, 10},
    {0, 0}
};

We know that this isn't really a "two-dimensional" array. It's really an array of arrays. array is an array of somethings, and what each something is is a little array of two uint32_t's. So array[0] is an array of two uint32_t's, array[1] is an array of two uint32_t's, etc.

Suppose that, for some reason, you wanted to manipulate pointers to rows in this array. Perhaps you want to move down the array using code like this:

sometype p;
p = array;
while(p is not at the end of array) {
    do something with the array pointed to by p;
    p++;
}

Or suppose you wanted to keep track of certain rows in the array, again via pointers:

sometype row2, row4;
row2 = &array[2];
row4 = &array[4];

We know that this makes sense, because array is an array of something, so array[2] is the second (really the third, because 0-based) something, ao &array[2] is a pointer to that something. And, again, in this case the "something" is an array of two uint32_t's.

But the question is, what is sometype? We want p, row2, and row4 to have type "pointer to array of two uint32_t". This is an unusual type, but it does exist, and it's the type you've been asking about:

uint32_t (*p)[2];

This says that p is precisely a pointer to an array of two uint32_t's. The big and obvious subquestion, of course, is: what the heck are those parentheses doing around the (*p) part? And the answer is that without them, we'd have

uint32_t *p[2];        /* not what we want */

and this would declare p as an array of two pointers to uint32_t. In C, there are precedence relationships in declarations just like there are in expressions, and [] binds more tightly than *. So if you want to say that p is a pointer first (and that what it's a pointer to is any array), you need the parentheses:

uint32_t (*p)[2];

So now we can rewrite that loop I wrote earlier that tries to scan the array:

uint32_t (*p)[2];
p = array;
while((*p)[0] != 0 || (*p)[1] != 0) {
    printf("next row: %d, %d\n", (*p)[0], (*p)[1]);
    p++;
}

This works, and I encourage you to compile and run with it. There are some more "funny" parentheses, again because of precedence. Our p is indeed a pointer to a little array of two uint32's. So (*p) is an array of two uint32's. So (*p)[0] is the first element of the pointed-to array, and (*p)[1] is the second. But without the parentheses we'd have *p[0] and *p[1], and those would try to, basically, treat p as an array first and a pointer second, which is not what we want. (Actually, as @sj95126 and I are discussing in the comments, a notation like *p[1] does work — at first I was thinking it would be an error — but it does something different, namely to access the [0] element of the subarray one past p, not the [1] element of the array at p.)

Finally, it may be useful at this point to observe that for "complicated" types such as pointers to arrays, C's typedef mechanism can sometimes make things clearer. Earlier I wrote, as pseudocode,

sometype p;

because we weren't quite sure what type to use. But, now that we know, and if we wanted more convenient declarations for variables like p, row2, and row4, we could write

typedef uint32_2 (*sometype)[2];

This is just like our earlier declaration of p, except that it has that extra keyword typedef out at the front. This changes things: we are not declaring "sometype" as a pointer to an array of two uint32_t's; we are instead declaring "sometype" as an alias or shorthand for the type, "pointer to an array of two uint32_t's". We can then literally say things like

sometype p;

or

sometype row2, row4;

The typedef only helps when declaring p, row2, and row4, though. Down in the expressions where we use them, we'll still need the extra parentheses. And typedefs incorporating pointers can also be confusing (more confusing than they're convenient, that is), so many programmers recommend avoiding pointer typedefs. (Also of course "sometype" is a horrible name, in practice. If you really wrote this you'd want something like typedef uint32_2 (*ptrary2)[2]; ptrary2 p;.)

For a bit more discussion on pointers to arrays, see question 6.13 in the old C FAQ list.

like image 38
Steve Summit Avatar answered Mar 12 '23 08:03

Steve Summit