Question

I have a

LS_Led* LS_vol_leds[10];

declared in one C module, and the proper externs in the other modules that access it.

In func1() I have this line:

/* Debug */
LS_Led led = *(LS_vol_leds[0]);

And it does not cause an exception. Then I call func2() in another C module (right after above line), and do the same line, namely:

/* Debug */
LS_Led led = *(LS_vol_leds[0]);`

first thing, and exception thrown!!!

I don't think I have the powers to debug this one on my own.

Before anything LS_vol_leds is initialized in func1() with:

LS_vol_leds[0] = &led3;
LS_vol_leds[1] = &led4;
LS_vol_leds[2] = &led5;
LS_vol_leds[3] = &led6;
LS_vol_leds[4] = &led7;
LS_vol_leds[5] = &led8;
LS_vol_leds[6] = &led9;
LS_vol_leds[7] = &led10;
LS_vol_leds[8] = &led11;
LS_vol_leds[9] = &led12;

My externs look like

extern LS_Led** LS_vol_leds;

So does that lead to disaster and I how do I prevent disaster?

Thanks.

Was it helpful?

Solution

This leads to disaster:

extern LS_Led** LS_vol_leds;

You should try this instead:

extern LS_Led *LS_vol_leds[];

If you really want to know why, you should read Expert C Programming - Deep C Secrets, by Peter Van Der Linden (amazing book!), especially chapter 4, but the quick answer is that this is one of those corner cases where pointers and arrays are not interchangeable: a pointer is a variable which holds the address of another one, whereas an array name is an address. extern LS_Led** LS_vol_leds; is lying to the compiler and generating the wrong code to access LS_vol_leds[i].

With this:

extern LS_Led** LS_vol_leds;

The compiler will believe that LS_vol_leds is a pointer, and thus, LS_vol_leds[i] involves reading the value stored in the memory location that is responsible for LS_vol_leds, use that as an address, and then scale i accordingly to get the offset.

However, since LS_vol_leds is an array and not a pointer, the compiler should instead pick the address of LS_vol_leds directly. In other words: what is happening is that your original extern causes the compiler to dereference LS_vol_leds[0] because it believes that LS_vol_leds[0] holds the address of the pointed-to object.

UPDATE: Fun fact - the back cover of the book talks about this specific case:

So that's why extern char *cp isn't the same as extern char cp[]. I knew that it didn't work despite their superficial equivalence, but I didn't know why. [...]

UPDATE2: Ok, since you asked, let's dig deeper. Consider a program split into two files, file1.c and file2.c. Its contents are:

file1.c

#define BUFFER_SIZE 1024
char cp[BUFFER_SIZE];
/* Lots of code using cp[i] */

file2.c

extern char *cp;
/* Code using cp[i] */

The moment you try to assing to cp[i] or use cp[i] in file2.c will most likely crash your code. This is deeply tight into the mechanics of C and the code that the compiler generates for array-based accesses and pointer-based accesses.

When you have a pointer, you must think of it as a variable. A pointer is a variable like an int, float or something similar, but instead of storing an integer or a float, it stores a memory address - the address of another object.

Note that variables have addresses. When you have something like:

int a;

Then you know that a is the name for an integer object. When you assign to a, the compiler emits code that writes into whatever address is associated with a.

Now consider you have:

char *p;

What happens when you access *p? Remember - a pointer is a variable. This means that the memory address associated with p holds an address - namely, an address holding a character. When you assign to p (i.e., make it point to somewhere else), then the compiler grabs the address of p and writes a new address (the one you provide it) into that location.

For example, if p lives at 0x27, it means that reading memory location 0x27 yields the address of the object pointed to by p. So, if you use *p in the right hand side of an assignment, the steps to get the value of *p are:

  1. Read the contents of 0x27 - say it's 0x80 - this is the value of the pointer, or, equivalently, the address of the pointed-to object
  2. Read the contents of 0x80 - this finally gives you *p.

What if p is an array? If p is an array, then the variable p itself represents the array. By convention, the address representing an array is the address of its first element. If the compiler chooses to store the array in address 0x59, it means that the first element of p lives at 0x59. So when you read p[0] (or *p), the generated code is simpler: the compiler knows that the variable p is an array, and the address of an array is the address of the first element, so p[0] is the same as reading 0x59. Compare this to the case for which p is a pointer.

If you lie to the compiler, and tell it you have a pointer instead of an array, the compiler will (wrongly) generate code that does what I showed for the pointer case. You're basically telling it that 0x59 is not the address of an array, it's the address of a pointer. So, reading p[i] will cause it to use the pointer version:

  1. Read the contents of 0x59 - note that, in reality, this is p[0]
  2. Use that as an address, and read its contents.

So, what happens is that the compiler thinks that p[0] is an address, and will try to use it as such.

Why is this a corner case? Why don't I have to worry about this when passing arrays to functions?

Because what is really happening is that the compiler manages it for you. Yes, when you pass an array to a function, a pointer to the first element is passed, and inside the called function you have no way to know if it is a "real" array or a pointer. However, the address passed into the function is different depending on whether you're passing a real array or a pointer. If you're passing a real array, the pointer you get is the address of the first element of the array (in other words: the compiler immediately grabs the address associated to the array variable from the symbol table). If you're passing a pointer, the compiler passes the address that is stored in the address associated with that variable (and that variable happens to be the pointer), that is, it does exactly those 2 steps mentioned before for pointer-based access. Again, note that we're discussing the value of the pointer here. You must keep this separated from the address of the pointer itself (the address where the address of the pointed-to object is stored).

That's why you don't see a difference. In most situations, arrays are passed around as function arguments, and this rarely raises problems. But sometimes, with some corner cases (like yours), if you don't really know what is happening down there, well.. then it will be a wild ride.

Personal advice: read the book, it's totally worth it.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top