21

One of the interview questions asked me to "write the prototype for a C function that takes an array of exactly 16 integers" and I was wondering what it could be? Maybe a function declaration like this:

void foo(int a[], int len);

Or something else?

And what about if the language was C++ instead?

M.M
  • 138,810
  • 21
  • 208
  • 365
sriks
  • 563
  • 2
  • 6
  • 17

6 Answers6

44

In C, this requires a pointer to an array of 16 integers:

void special_case(int (*array)[16]);

It would be called with:

int array[16];
special_case(&array);

In C++, you can use a reference to an array, too, as shown in Nawaz's answer. (The question asks for C in the title, and originally only mentioned C++ in the tags.)


Any version that uses some variant of:

void alternative(int array[16]);

ends up being equivalent to:

void alternative(int *array);

which will accept any size of array, in practice.


The question is asked - does special_case() really prevent a different size of array from being passed. The answer is 'Yes'.

void special_case(int (*array)[16]);

void anon(void)
{

    int array16[16];
    int array18[18];
    special_case(&array16);
    special_case(&array18);
}

The compiler (GCC 4.5.2 on MacOS X 10.6.6, as it happens) complains (warns):

$ gcc -c xx.c
xx.c: In function ‘anon’:
xx.c:9:5: warning: passing argument 1 of ‘special_case’ from incompatible pointer type
xx.c:1:6: note: expected ‘int (*)[16]’ but argument is of type ‘int (*)[18]’
$

Change to GCC 4.2.1 - as provided by Apple - and the warning is:

$ /usr/bin/gcc -c xx.c
xx.c: In function ‘anon’:
xx.c:9: warning: passing argument 1 of ‘special_case’ from incompatible pointer type
$

The warning in 4.5.2 is better, but the substance is the same.

Community
  • 1
  • 1
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • Would it actually prevent the user to do a array[17] or array[15]? when I think what exactly is int [], in the background its a int pointer to a non dynamic array. So would the program/compiler automatically reject a different length, or would it accept it and try to handle it as best as it can ? – Jason Rogers Jan 17 '11 at 06:25
  • 1
    This is exactly right, I deleted my answer after testing. I thought I could get tricky with another use for static (AUFS) and go with C99's `void foo(int a[static 16]);` but I just realized that guarantees *at least* 16 members not *exactly* as wanted by the OP – SiegeX Jan 17 '11 at 06:42
  • So if I pass an array that is not of size 16, it will not compile ? let's say I have int intArray[20]; special_case(&intArray). Will that compile? – santahopar Apr 15 '14 at 20:13
  • @armanali: It depends on your compiler and the compiler options. You should get a warning about the type mismatch; you might not get an error unless you make all warnings into errors. As shown in the question, GCC warns but compiles the code unless you include `-Werror`. – Jonathan Leffler Apr 15 '14 at 20:16
  • Is this construction `void f(int (*array)[16])` valid with C90, or it requires C99 or beyond ? – Cyan Jun 29 '15 at 07:19
  • @Cyan: AFAICR, it is part of C90 — not a new feature in C99 or C11. To the extent that compiling the code with `gcc -O3 -g -std=c90 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition -Werror -pedantic -c special.c` proves anything, the code is OK under C90 (where `special.c` contained the `anon()` function plus an `extern void anon(void);` declaration, and the compilation failed with mismatched types for the `array18` definition/call and succeeded when they were made into comments). – Jonathan Leffler Jun 29 '15 at 07:24
  • Excellent and complete answer. Thanks you ! – Cyan Jun 29 '15 at 07:31
  • `special_case(&array);` : does that mean there is an additional indirection level to access data within `array` ? `array` being a pointer to where the array content is, it seems the function argument is a pointer to a pointer. – Cyan Jun 29 '15 at 07:58
  • @Cyan: Succinctly, yes. It takes a pointer to an array, so you have to dereference the pointer inside the function. – Jonathan Leffler Jun 29 '15 at 08:04
  • @Cyan: No, these are equivalent. The type ``int[5]`` is a bit messy. In the case of an argument ``f(int[5])``, it is equivalent to ``f(int *)`` and so you have a pointer to the first int. In the case of an argument ``f(int(*)[5])``, you have a pointer to 5 consecutive ints. These two are the same (for the implementation). – Armin Rigo Jun 29 '15 at 08:59
  • @ArminRigo: I disagree with your statements. I believe they are wrong. – Jonathan Leffler Jun 29 '15 at 09:08
  • It seems to behave like a 2D arrays. It's a pointer to arrays of size 16. In order to access individual elements, it's necessary to do `array[0][i]` instead of the more usual `array[i]`. Nevertheless, in such case, the pointer points at the beginning, hence it has the same value. See http://ideone.com/1qA7T9 . – Cyan Jun 29 '15 at 09:35
  • Also notable : it seems a function like `int read(const int (*array)[16])` wouldn't accept an argument of type `int (*array)[16]` without triggering a warning. From a compiler perspective, they are different types. Which introduces a question : how to specify `const` operations in a compatible way ? – Cyan Jun 29 '15 at 10:11
  • @JonathanLeffler: try with a compiler looking at the assembler (e.g. ``gcc -O2 -S input.c``). They are exactly the same. – Armin Rigo Jun 29 '15 at 15:38
12

There are several ways to declare array-parameters of fixed size:

void foo(int values[16]);

accepts any pointer-to-int, but the array-size serves as documentation

void foo(int (*values)[16]);

accepts a pointer to an array with exactly 16 elements

void foo(int values[static 16]);

accepts a pointer to the first element of an array with at least 16 elements

struct bar { int values[16]; };
void foo(struct bar bar);

accepts a structure boxing an array with exactly 16 elements, passing them by value.

Christoph
  • 164,997
  • 36
  • 182
  • 240
  • 1
    In the `void foo(int values[static 16]);` case, the C standard does not require that the compiler verify that the argument given is correct. And, AFAIK, no compilers even attempt to do this. So while it is nice in theory, at the current time it doesn't actually get you any safety -- there'll be no warning issued if you call it with a smaller array. Hopefully compilers will get better. – M.M Jun 29 '15 at 07:11
  • Actually, clang gives a warning if you pass an array that is too short. It does not give any warning if you pass ``&local_int``, though, even though this means you're in effect giving an array of length 1. – Armin Rigo Jun 29 '15 at 08:53
6

& is necessary in C++:

void foo(int (&a)[16]); // & is necessary. (in C++)

Note : & is necessary, otherwise you can pass array of any size!


For C:

void foo(int (*a)[16]) //one way
{
}

typedef int (*IntArr16)[16]; //other way
void bar(IntArr16 a)
{
}

int main(void) 
{
        int a[16];
        foo(&a); //call like this - otherwise you'll get warning!
        bar(&a); //call like this - otherwise you'll get warning!
        return 0;
}

Demo : http://www.ideone.com/fWva6

Nawaz
  • 353,942
  • 115
  • 666
  • 851
  • 2
    References are not part of the C language. – Stephen Canon Jan 17 '11 at 06:20
  • 1
    @Stephen: you're right - but while the title asks for C, the tags include C++. – Jonathan Leffler Jan 17 '11 at 06:24
  • @Jonathan: you are rite but when we are taking about concepts like these c or c++ its the same..isn't it.. now i added c++ in the Que – sriks Jan 18 '11 at 02:02
  • 2
    @Srikanth: No, for concepts like this, C and C++ are not identical because C++ has references and C does not have references. And the answer using references therefore does not apply to C. They are two distinct, albeit related, languages. – Jonathan Leffler Jan 18 '11 at 03:04
  • Doing an operation on `a` from within `foo()` or `bar()` is not as straightforward as if it was a simple table pointer, due to the additional indirection level. It's not possible to do `a[5]=1;`, but the working equivalent seems to be `a[0][5]=1;`. Not a huge deal, but still slightly less clear to read. More importantly, I'm not sure to understand how it's possible to enforce `const` property. It seems that something like this wouldn't work : ` void foo(const int (*a)[16]) { (...) } void bar (int (*a)[16]) { foo(a); }` The pointers are considered of different type. – Cyan Jun 29 '15 at 08:06
  • @Nawaz You say to use foo(&a); to call this as otherwise you'll get a warning. I get an error from gcc when I use that syntax ("error: invalid initialization of non-const reference of type 'unsigned int (&)[8]' from a temporary of type 'unsigned int (*)[8]") but it works fine without. Can you elaborate a bit on that? – cosimo193 Jun 01 '17 at 16:48
  • @Nawaz No :-) Asking me to post my code made me realize that I was using your suggested C code, with the C++ foo prototytpe. When we use the C++ prototype (void foo(int (&a)[16]);) we need to call foo(a);, i.e. without the &. Thanks for your reply though; it helped! – cosimo193 Jun 02 '17 at 21:48
2

I think the simplest way to be typesafe would be to declare a struct that holds the array, and pass that:

struct Array16 {
  int elt[16];
};


void Foo(struct Array16* matrix);
Chris Becke
  • 34,244
  • 12
  • 79
  • 148
1

You already got some answers for C, and an answer for C++, but there's another way to do it in C++.

As Nawaz said, to pass an array of N size, you can do this in C++:

const size_t N = 16; // For your question.

void foo(int (&arr)[N]) {
    // Do something with arr.
}

However, as of C++11, you can also use the std::array container, which can be passed with more natural syntax (assuming some familiarity with template syntax).

#include <array>

const size_t N = 16;

void bar(std::array<int, N> arr) {
    // Do something with arr.
}

As a container, std::array allows mostly the same functionality as a normal C-style array, while also adding additional functionality.

std::array<int, 5> arr1 = { 1, 2, 3, 4, 5 };
int arr2[5] = { 1, 2, 3, 4, 5 };

// Operator[]:
for (int i = 0; i < 5; i++) {
    assert(arr1[i] == arr2[i]);
}

// Fill:
arr1.fill(0);
for (int i = 0; i < 5; i++) {
    arr2[i] = 0;
}

// Check size:
size_t arr1Size = arr1.size();
size_t arr2Size = sizeof(arr2) / sizeof(arr2[0]);

// Foreach (C++11 syntax):
for (int &i : arr1) {
    // Use i.
}
for (int &i : arr2) {
    // Use i.
}

However, to my knowledge (which is admittedly limited at the time), pointer arithmetic isn't safe with std::array unless you use the member function data() to obtain the actual array's address first. This is both to prevent future modifications to the std::array class from breaking your code, and because some STL implementations may store additional data in addition to the actual array.


Note that this would be most useful for new code, or if you convert your pre-existing code to use std::arrays instead of C-style arrays. As std::arrays are aggregate types, they lack custom constructors, and thus you can't directly switch from C-style array to std::array (short of using a cast, but that's ugly and can potentially cause problems in the future). To convert them, you would instead need to use something like this:

#include <array>
#include <algorithm>

const size_t N = 16;

std::array<int, N> cArrayConverter(int (&arr)[N]) {
    std::array<int, N> ret;

    std::copy(std::begin(arr), std::end(arr), std::begin(ret));

    return ret;
}

Therefore, if your code uses C-style arrays and it would be infeasible to convert it to use std::arrays instead, you would be better off sticking with C-style arrays.

(Note: I specified sizes as N so you can more easily reuse the code wherever you need it.)


Edit: There's a few things I forgot to mention:

1) The majority of the C++ standard library functions designed for operating on containers are implementation-agnostic; instead of being designed for specific containers, they operate on ranges, using iterators. (This also means that they work for std::basic_string and instantiations thereof, such as std::string.) For example, std::copy has the following prototype:

template <class InputIterator, class OutputIterator>
OutputIterator copy(InputIterator first, InputIterator last,
                    OutputIterator result);
// first is the beginning of the first range.
// last is the end of the first range.
// result is the beginning of the second range.

While this may look imposing, you generally don't need to specify the template parameters, and can just let the compiler handle that for you.

std::array<int, 5> arr1 = { 1, 2, 3, 4, 5 };
std::array<int, 5> arr2 = { 6, 7, 8, 9, 0 };
std::string str1 = ".dlrow ,olleH";
std::string str2 = "Overwrite me!";

std::copy(arr1.begin(), arr1.end(), arr2.begin());
// arr2 now stores { 1, 2, 3, 4, 5 }.

std::copy(str1.begin(), str1.end(), str2.begin());
// str2 now stores ".dlrow ,olleH".
// Not really necessary for full string copying, due to std::string.operator=(), but possible nonetheless.

Due to relying on iterators, these functions are also compatible with C-style arrays (as iterators are a generalisation of pointers, all pointers are by definition iterators (but not all iterators are necessarily pointers)). This can be useful when working with legacy code, as it means you have full access to the range functions in the standard library.

int arr1[5] = { 4, 3, 2, 1, 0 };
std::array<int, 5> arr2;

std::copy(std::begin(arr1), std::end(arr1), std::begin(arr2));

You may have noticed from this example and the last that std::array.begin() and std::begin() can be used interchangeably with std::array. This is because std::begin() and std::end() are implemented such that for any container, they have the same return type, and return the same value, as calling the begin() and end() member functions of an instance of that container.

// Prototype:
template <class Container>
auto begin (Container& cont) -> decltype (cont.begin());

// Examples:
std::array<int, 5> arr;
std::vector<char> vec;

std::begin(arr) == arr.begin();
std::end(arr) == arr.end();

std::begin(vec) == vec.begin();
std::end(vec) == vec.end();

// And so on...

C-style arrays have no member functions, necessitating the use of std::begin() and std::end() for them. In this case, the two functions are overloaded to provide applicable pointers, depending on the type of the array.

// Prototype:
template <class T, size_t N>
T* begin (T(&arr)[N]);

// Examples:
int arr[5];

std::begin(arr) == &arr[0];
std::end(arr) == &arr[4];

As a general rule of thumb, if you're unsure about whether or not any particular code segment will have to use C-style arrays, it's safer to use std::begin() and std::end().

[Note that while I used std::copy() as an example, the use of ranges and iterators is very common in the standard library. Most, if not all, functions designed to operate on containers (or more specifically, any implementation of the Container concept, such as std::array, std::vector, and std::string) use ranges, making them compatible with any current and future containers, as well as with C-style arrays. There may be exceptions to this widespread compatibility that I'm not aware of, however.]

2) When passing a std::array by value, there can be considerable overhead, depending on the size of the array. As such, it's usually better to pass it by reference, or use iterators (like the standard library).

// Pass by reference.
const size_t N = 16;

void foo(std::array<int, N>& arr);

3) All of these examples assume that all arrays in your code will be the same size, as specified by the constant N. To make more your code more implementation-independent, you can either use ranges & iterators yourself, or if you want to keep your code focused on arrays, use templated functions. [Building on this answer to another question.]

template<size_t SZ> void foo(std::array<int, SZ>& arr);

...

std::array<int, 5> arr1;
std::array<int, 10> arr2;

foo(arr1); // Calls foo<5>(arr1).
foo(arr2); // Calls foo<10>(arr2).

If doing this, you can even go so far as to template the array's member type as well, provided your code can operate on types other than int.

template<typename T, size_t SZ>
void foo(std::array<T, SZ>& arr);

...

std::array<int, 5> arr1;
std::array<float, 7> arr2;

foo(arr1); // Calls foo<int, 5>(arr1).
foo(arr2); // Calls foo<float, 7>(arr2).

For an example of this in action, see here.


If anyone sees any mistakes I may have missed, feel free to point them out for me to fix, or fix them yourself. I think I caught them all, but I'm not 100% sure.

Community
  • 1
  • 1
0

Based on Jonathan Leffler's answer

#include<stdio.h>
void special_case(int (*array)[4]);
void anon(void){
    int array4[4];
    int array8[8];
    special_case(&array4);
    special_case(&array8);
}
int main(void){
    anon(); 
    return 0;
}
void special_case(int (*array)[4]){
    printf("hello\n");
}

gcc array_fixed_int.c &&./a.out will yield warning:

array_fixed_int.c:7:18: warning: passing argument 1 of ‘special_case’ from incompatible pointer type [-Wincompatible-pointer-types]
    7 |     special_case(&array8);
      |                  ^~~~~~~
      |                  |
      |                  int (*)[8]
array_fixed_int.c:2:25: note: expected ‘int (*)[4]’ but argument is of type ‘int (*)[8]’
    2 | void special_case(int (*array)[4]);
      |                   ~~~~~~^~~~~~~~~ 

Skip warning:

gcc  -Wno-incompatible-pointer-types  array_fixed_int.c &&./a.out 
jian
  • 4,119
  • 1
  • 17
  • 32