Syntax Error12
Syntax Error12

Reputation: 88

Why is there a disparity b/w array of chars and string in C?

I noticed that if you define a string in C you need to take into consideration the nul terminator, which made intuitive sense that the computer need someway to check that the string has hit its end.

But then when you do something like anytype arr[] = {val1, val2, ...}, you suddenly don't need to take into consideration the nul terminator.

How does the computer know that it has hit the end of the array without any special identifier.

i tried searching but the answers were not specific to how does the computer know the end of an array.

Upvotes: -1

Views: 124

Answers (3)

Yimin Rong
Yimin Rong

Reputation: 2027

Unlike other languages, in C, arrays don't include their inherent sizes. Without any checks, it's entirely possible to read or write outside of an array bounds. In modern systems, there can be some protection provided by segmentation and paged virtual memory, but it will still let you read outside of the array within the memory owned by the process.

To answer your question:

The \0 character automatically added to the end of strings is a standard terminator used by many C functions. It's so important that the UTF-8 standard was designed to be compatible with it by explicitly forbidding overlong encodings.

However, there is no standard terminator for the ends of other types of arrays like integers, floating point, structures, etc. In these cases, you have to provide the size of the array so that the called function won't access out of bounds.

In some cases, you may know that certain values cannot exist, so for example, you could use -1 to indicate the end of an array of integers. Any design decisions here would be specific to the application.

Upvotes: 3

Dúthomhas
Dúthomhas

Reputation: 10103

There are a couple of misconceptions going on in other answers.

In C, an array is a special object. An array in C has:

  • an element type
  • a length

For example, int xs[7]; declares an array of element type int and length 7 elements.

⟶ This is information the compiler keeps about your array.


The problem is that you cannot pass arrays around like you can other objects. In order to reference an array, you must use a pointer. You see this most commonly when trying to pass an array as argument to a function. You cannot pass the array, but you can pass a pointer to it:

#include <stdio.h>

int sum( int * xs, int n )
{
  int total = 0;
  while (--n) total += xs[n];
  return total;
}

int main( void )
{
  int primes[7] = { 2, 3, 5, 7, 11, 13, 17 };
  printf( "%d\n", sum( primes, 7 ) );
  return 0;
}

We see that the function does not get the array itself, but a pointer to the (first element of the) array. Since pointers do not have an associated number of elements, we must also pass that information to the function to know when the array ends.


The trick here is that the array name “decays” to a pointer to the first element. This behavior is part of the C language. You could easily write &(primes[0]) instead, but that is the long way around when just writing primes is the same thing. This is called “array-to-pointer decay”.


The next trick is that the [] array subscript operator is syntactic sugar for a pointer offset lookup:

  xs  [2] = 42;  // A
*(xs + 2) = 42;  // B

In this example, lines A and B are exactly the same thing! In fact, line A is rewritten by the compiler as line B behind the scenes.

⟶ That is why it is possible to write the array indexing weirdly as: 2[xs] = 42; — it is the same as *(2 + xs) = 42!


We can get the compiler to tell us the number of elements in an array using a trick: (size in bytes of the array) divided by (size in bytes of a single element). This is expressed in code as:

sizeof(array) / sizeof(array[0])

We can use, through it is often most convenient via a macro:

#define ARRAY_SIZE(array) (sizeof(array) / sizeof(array[0]))
...
printf( "%d\n", sum( primes, ARRAY_SIZE(primes) ) );

We often use fewer elements in an array than available. We then need to track both the number of elements used and the total number of elements available. Again, in C, the compiler knows the total number of elements available — until you pass a pointer to the array somewhere.

We do this when doing things like adding to an array:

#define MAX_XS 100
int xs[MAX_XS];
int n_xs = 0;

// Add an x to xs
if (n_xs < MAX_XS)
{
  xs[n_xs++] = x;
}

All right, what does any of this have to do with strings?


A string in C is an array! But it has an additional characteristic: the string data itself is terminated with a “nul character” (a zero, or '\0'). This makes it easy to pass strings around without also having to pass around either their length or their capacity.

In other words, I can write:

char s[100];
strcpy( s, "Hello " );
strcat( s, username );
strcat( s, "!" );

As long as username is fewer than (100TOTAL - 6strlen("Hello ") - 1strlen("!") - 1NUL) characters long, everything will fit. But I don’t have to mess with ARRAY_SIZE() or s_length or anything like that.


Sentinel characters appear in other contexts as well. For example, the second argument to main() is an array of pointers to strings. The array itself is terminated with a NULL pointer!

In other words:

int main( int argc, char * argv[] )
{
  assert( argv[argc] == NULL );

Using special characters is not always a very good idea though. In fact, strings in C cannot contain nul characters for this very reason: it is impossible to embed a nul in a C string, simply because the nul indicates the end of the string!


tl;dr

  • The compiler knows the length of an array.
  • A pointer indexes an unknown number of elements — it can be indexed like an array but it is not.
  • Arrays do not need to be terminated with a sentinel value.
  • C strings are terminated by the sentinel nul character for convenience.
  • A C string is data stored in a (potentially larger) array.

Upvotes: 2

John Bode
John Bode

Reputation: 123578

How does the computer know that it has hit the end of the array without any special identifier.

It doesn't. Arrays in C don't store any runtime metadata regarding their length, type, or anything else (this is true of all types in C). An array is just a sequence of objects of a given type.

Under most circumstances array expressions decay to pointers to their first element; when you pass an array expression as an argument to a function:

int arr[10] = { ... };
foo( arr ); // == &arr[0]
...

all the function receives is a pointer:

void foo( int *a )
{
  ...
}

A pointer points to a single object of the given type; you cannot know from that pointer value alone whether that object is part of an array or how many elements follow it.

You must either pass the number of elements as a separate parameter or use a sentinel value to mark the end of the data (which may not be the last element of the array). This is why you have to use the 0 terminator for strings, because that's the only way the various str* routines know where the end of the string is. 0 works because it doesn't correspond to a printable value or control character; it's an "out of band" value for that particular use case.

However, under other circumstances, a 0-valued byte may be valid data and can't be used as a sentinel (hence all strings are arrays of character type, but not all arrays of character type are strings). In that case you'd have to store the size in a separate variable.

Upvotes: 1

Related Questions