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
And now for stuff to keep in mind:
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.
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.