Site logo
y42 software
Objective-C++ development
Abstract

Dos and Don'ts in C in reference to Objective-C

Quite often an Objective-C developer has contact with pure C which should not make you wonder as Objective-C is based on C and therefore has the same restrictions - and the same traps.

Based on a very good article of Andrew König's "C Traps and Pitfalls" I'd like to give you a very short overview of what to keep in mind when programming in Objecitive-C (and C). This article should not be a substitute for König's essay, think about it as more or less a reminder or reiteration.

Assuming that you will work with GCC 4.0 or later I will leave out topics which are not current any more or rather distracting.

Before I delve into details, let me state a few general rules of

 

Good programming practice

  • Adhere to Objective-C rules. Always. Think about memory management (object ownership). Follow naming conventions (plus prefix your classes so they don't clash with others as there is no concept of namespaces in C).
  • Name your methods accordingly to what the do. Don't abbreviate, your compiler and linker can treat more than eight symbols!
  • The same is true for variables. The more global they get in scope the better (more meaningful) their name should be. Your can prefix iVars with a letter like "i" for iVar or "m" for member (C++-style) - this helps you to determine their scope. And use CamelBack.
  • Always make clear what your intention was. Use explicit casts and write code (within a methode etc) in a meaningful order (like define local variables when they are needed and not at the top of the method like you had to a while back).
  • Compilers are really good at optimizing your code. Therefore don't start to optimize your code to soon. Especially if that means that reading your code gets more and more complicated. Avoid assigning and testing a value at the same time in a while or if statement. This avoids possible errors due to operator precedence. Plus it is easier to debug when you can see the value first before it gets used.
  • Turn on all warnings that your compiler has. He might inform you of possible errors. Look at them and correct them.

And now for stuff to keep in mind:

 

GENERAL

1) Beware that "=" is assignment and "==" is comparison. They are not the same :-) As mentioned before if you want to avoid mistakes don't write:

if( x = y > 2)

even though this is legal, use parenthesis at least (GCC 4 will emit a warning about missing parenthesis anyway)

if( (x = y) > 2)

or even better (because it is clearer and easier to understand:

x = y;
if( x > 2)

2) multi-charcter tokens:

Remember there are tokens which consists of two characters.
The compiler will analyse your code and operate with the tokens you provide. To delimit you tokens you can use spaces. But you don't have to. You can write:

a=b/2;

This is fine. Let a and b be ints and think of this:

int *p;
...
a=b/*p

This is not what you had in mind. There will never be a devision and a lot of code won't compile. /* marks the beginning of a comment.
Use spaces or brackets here:

a = b / *p or a=b/(*p)

I prefer spaces - I use them always because code is more readable. Other two-charakter tokens are >>, >< (shift) and ->


3) 'a' and "a" are not the same!

'a' is an int of the ACSII-Char-Number for a.
"a" is shorthand for a nameless array of chars ending with a zero.
Thus 'abcd' is a number consisting of the numeric values of a plus b plus c plus d. This is a multi-character constant which you can sometimes see being used - look at Apples CarbonCore/Folders.h where you can find enumerations with lines like:

kPreferencesFolderType = 'pref'

Keep in mind that 'pref' is not a string, but an int.


4) Operator-Precedence

Again - as a good rule of practice always state the meaning that you intend by using brackets. Think of flags in bitfields. To check wether or not a flag is set you use a filter expression which consists of a bitwise AND.
Now think of this unhappy example:

if ( someBitfield & 1 << 3 == 1 )

This is not: Check someBitfield against 1 shifted 3 to the left and see if this value was set. The shift operator has the strongest precedence and will be evaluated first.
Next is isEqualTo which gives us 0.
Then the bitwise AND is applied to now someBitfield & 0 which is true only if all bits in the bitfield are zero.

Even though it might look stupid (or better: like LISP) write:

if ( (someBitfield & (1 << 3)) == 1 )

4b) Also be careful with function pointers:

*f() doesn't dereference the function pointer and calls the function f, but will try to call the function f which will be expected to return a pointer! Use (*f)() in this case.

Other famous pifalls are *p++. If you have a pointer p which should get dereferenced and the the value increased by one, write (*p)++.


5) The compiler can not always warn you if you misplace semicolons. Sometimes a missing or wrong set semicolon still is legal code - but the meaning is totally different. (Well, this is actually what this article is all about...)
Consider an if-statement where you put a semicolon afterwards (must be late at night, but still)

if (a > b);
a = b;

The last line will always be executed because the if-clause has an empty statement (;).
If you are more involved and define your own structs in Obj-C and happen to have pure C-functions, watch out to end the struct declaration with a semicolon because the compiler will accept this:

struct somewhat {
 int a;
 int b;
}

function()
{
 ...
}

as a function that returns the struct somewhat.


6) C has a special deal with switch-statements: the program flow is orchestrated in a slightly different way than in non-C languages. You as a programmer have to explicitly stop execution with "break" if a case statement is met.
The probably well known syntax for switch is:

switch(expression)
{
 case 1: doSomething(); break;
 ...
 default: doDefault(); break;
}

Leaving out the "break" can be done - the result is that every case-statement from the first met case-statement on will be executed until a break is met.
You can use this for your advantage. Example:

char *someChar...;
//go through all the char and check them
for(...)
{
 switch(*someChar)
 {
   case 'a':
   case 'b':
   case 'c':
   ....
   case 'z': convertToUpper(*someChar); break;
   case 'A':
   case 'B':
   ...
 }
}

You get the picture. Sometimes this is really useful (not like in my example).
Say you have 3 textfields in a vertical row and depending on some condition you want to display only the first, only the first two or all fields. one way you could do this is that you hide all fields and then - you guessed it - use a switch-statement like this:

switch(startRowToHide)
{
 case 1: [field1 setHidden: YES];
 case 2: [field2 setHidden: YES];
 case 3: [field3 setHidden: YES]; break;
 default: break;
}

So depending on the startRowToHide program flow jumps to the right case-statement and from there on hides all following textfields.
But beware if you don't want this kind of behaviour!


7) To call a function in C you use the function name followed by parenthesis. Like:

functionToCall();

This actually calls the function.
But if you just write:

functionToCall

nothing happens. Well, actually what happens is that the address of the function is fetched. Where do you need this?
Take NSArray, for example.
NSArray has a method called:

- (NSArray *)sortedArrayUsingFunction:(int (*)(id, id, void *))comparator context:(void *)context

That means you have to supply a function with the signature

(int (function)(id, id, void *))

like:

int mySort(id value1, id value2, void *context)

What the method "sortedArrayUsingFunction:context:" expects is the address of a sort function - so we use it like this:

[someArray sortedArrayUsingFunction: mySort context: nil];

Since you don't want to call it you don't use mySort() in that case.


8) As I mentioned above: don't be lazy about those parenthesis. Especially in program-flow statements like if().
Parenthesis make your intention clear. It's easier for others to read and maintain your code. Think about this:

if( a == 0 )
  if( b == 0 ) error();
else c = a / b;
if( c == 2 ) callEven(c);
else callUneven(c);

This is erroneous code. And not easy to read. And it shows another problem: The dangling else problem. "else" is always bound to the last (or innermost) if-statement. You know it for sure.
Give your code some structure and some air to breathe.

Things like:

if (x.y.f() && (x.z = g(x.a, b, x.c)) != err)
    return h(x, x.z, k);

are hard to read and understand and debug. This was real code, certainly I changed the names of the variables, still if you want to debug it you really have to pick carefully where and when to jump in/out go over etc.
Remember: compilers will optimize this for you.


9) Don't confuse the bitwise operators "&" and "|" with the logical counterparts "&&" and "||". I don't think I have to go into lengthy explanations here, just one example:

int x = 2; int y = 1; if( x && y ) getsExecuted(); // both are true (greater than zero)
if( x & y) doesnotGetExecuted();

x is ...0010
y is ...0001
binary "&" results in (remember: only is 1 if both bits at the same place are 1)
x&y ...0000


10) Ah yes, this is my favorite:
Subscripts of array start with 0.
That so C-style that you even start counting sheep with zero.
This is so flesh and blood after a few years and we all write:

int i = 0; for (i=0; i < [somearray count]; i++)...

Hopefully!
(Mind: not i <= [someArray count] as count is the total number of members in the array!)
Even though König doesn't mention it: It isn't a good idea to create your own collection classes with index access starting at 1. I came across it and could not believe it when I realized that 0 was the end of the collection. C programmers start with 0.


11) A word on casting: do it. And think about it while doing.
Pay attention to your numbers in calculations.
The compiler will not always do the right thing.
Consider this:

-(int) resize: (int) oldSize
{
float sizeFactor = 0.721;
return oldSize * sizeFactor;
}

This could always return 0!
Don't expect the compiler to first cast oldSize to float, then multiply by sizeFactor, do some rounding and cast back to int.
GCC will try to do its best but never count on it. Tell the compiler what you had in mind.

And pay attention to signed/unsigned comparison.
The [array count] returns an unsigned int value.
Imagine you compare it to an int, which can be negative.

if ( intValue < [array count] ) NSBeep();

Think about it...
What should the compiler do?
Casting intValue to unsigned will lead to a very large positive number if intValue is a small negative number, so you will hear nothing. Casting the array-count to an int will probably do what you want to do as long as there are less than INT_MAX members in the array. Now make that an unsigned short...

Closely related to this are overflows:
Think about money. You have a class that stores money values by cents (thus giving it the accuracy of integer math).
A customer buys 100 pieces of your fabulous product which is each $15.
No problem. You export your class to turkey where a sandwich is about 1 million Turkey Lira (you class saves this value by factor 100).
Now think about cars...
Even though you might say that this is unlikely to happen, there where smart people declaring that 64k are enough as an application memory.

Before you undertake operations on numbers that will increase them test whether it is possible to do so:

short value = 750;
short factor = 100;
if( (SHRT_MAX / factor) < value )
{
 alertOverflow();
}
else
{
 value *= factor;
}

12) Relatively new is NSError which is usually used as an indirect return value if something went wrong within a method. You supply the address of a local NSError declaration to this method like:

-(NSObject*) doSomething: (NSError **) error;

Certainly the caller of this method can supply nil in case he isn't interested in the error. So you have to be careful in the design of the doSomething: method: Check to see if the caller supplied an address of an error-object and then assign your error to the dereferenced input address:

-(NSObject*) doSomething: (NSError **) error
{
 ....
 //something went wrong...
 if(error != nil)
 {
   *error = [NSError errorWithDomain: ...];
 }
}

If you don't check you will be dereferencing nil which will crash your app.


 

PREPROCESSOR

13) In Objective-C you will find yourself using the preprocessor really often and all that was said about it is true in Objective-C.
But consider one thing: if you define a value you will define it without a type. So:

#define MAX_NUMBERS 10

is not really defined to be an int, unsigned int or short or whatever. You may be better of by using an extern-declaration:

in xyz.h write:

extern const short MAX_NUMBERS;

in xyz.m write:

const short MAX_NUMBERS = 10;

(both outside of @implementation or @interface).

14) Also remember to put parenthesis around the used value of your defines like in the probably most common example:

#define max(x,y) ((x) > (y) ? (x) : (y))

x or y could be an expression and then you might find yourself in a conflict of operator precedence.
Plus remember that macros hide how many times they (need to) evaluate an expression. In our example at least one of the expressions (x or y) is evaluated twice - which is ok if it is a plain value. Just consider x is array[i++] and gets evaluated twice...
In such cases you should rather provide a real method instead of using a macro. They are easier to debug plus computers are getting faster all the time. If you find that your profiler shows a noticeable performance hit in your method then (and only then) optimize it.