it depends if you are OK to mess with your buffer's content or if you want a new buffer.
if you are fine with trashing the buffer then just replace the character at position end by '\0' and read from the address of the character starting at start
If you can use C++17, use std::string_view::substr. It'll be easy to use an more efficient than creating unnecessary copies.
Without C++17, you can still do something similar using a pair of pointers or a pointer and a size.
To create a copy, you can use strncpy. Don't forget the terminating null, and watch out for buffer overflows.
It's a wrapper around standard C-style arrays with some added features, for example, you can slice it, and you can return it from functions, which is not something you can do with normal arrays.
For example:
Array<char, 15> array {"Hello World 123"};
auto subarray_view = array.slice<1, 10>(); // Note that 10 is the end index, not the length
Example with different data types:
#include <Arduino_Helpers.h>
#include <AH/Containers/Array.hpp>
// Helper that prints all elements of any iterable
template <class T>
void print(const T &arr) {
for (const auto &el : arr) {
Serial.print(el); Serial.print(", ");
}
Serial.println();
}
void setup() {
Serial.begin(115200);
while (!Serial);
{
// Create an array of 6 floats.
Array<float, 6> array { 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, };
// Create a view on a portion of the array, from index 1 to 4.
auto subarray_view = array.slice<1, 4>();
// Create a new array that's a copy of the view.
auto subarray = subarray_view.asArray();
// You can also be more explicit about the type, but `auto` is
// handy because then you don't have to specify the length.
Array<float, 4> subarray_explicit = subarray_view; // identical to previous line
Serial.print("Full array: ");
print(array);
Serial.print("View: ");
print(subarray_view);
Serial.print("Sub-array: ");
print(subarray);
// Changing a value in the original array ...
Serial.println("Changing the second element");
array[1] = 3.14;
// ... changes the view ...
Serial.print("View: ");
print(subarray_view);
// ... but not the new sub-array.
Serial.print("Sub-array: ");
print(subarray);
Serial.println();
}
{ // Your original example:
Array<char, 15> array {"Hello World 123"};
Serial.print("Full array: ");
print(array);
auto subarray_view = array.slice<1, 10>();
Serial.print("View: ");
print(subarray_view);
}
}
void loop() {}
Output:
Full array: 0.00, 0.10, 0.20, 0.30, 0.40, 0.50,
View: 0.10, 0.20, 0.30, 0.40,
Sub-array: 0.10, 0.20, 0.30, 0.40,
Changing the second element
View: 3.14, 0.20, 0.30, 0.40,
Sub-array: 0.10, 0.20, 0.30, 0.40,
Full array: H, e, l, l, o, , W, o, r, l, d, , 1, 2, 3,
View: e, l, l, o, , W, o, r, l, d,
Array sizes have to be known at compile-time, so you'll have to use a function template if you want your function to accept different array sizes (or different types):
#include <Arduino_Helpers.h>
#include <AH/Containers/Array.hpp>
template <class T>
void print(const T &arr) {
for (const auto &el : arr) {
Serial.print(el);
Serial.print(", ");
}
Serial.println();
}
template <class T, size_t N>
Array<T, 3> get_last_3_elems(const Array<T, N> &array) {
return array.template slice<N - 3, N - 1>();
}
void setup() {
Serial.begin(115200);
while (!Serial);
// Create an array of 6 floats.
Array<float, 6> array { 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, };
// Create a new array containing the last 3 elements.
auto last_3 = get_last_3_elems(array);
Serial.print("Full array: ");
print(array);
Serial.print("Last 3: ");
print(last_3);
}
void loop() {}
Output:
Full array: 0.00, 0.10, 0.20, 0.30, 0.40, 0.50,
Last 3: 0.30, 0.40, 0.50,
// Here is your subArray function..
byte* subArray(byte* theArray,int itemSize,int startItem,int numItems) {
int trace;
int numBytes;
byte* newArray;
numBytes = itemSize * numItems;
newArray = (byte*)malloc(numBytes);
if (newArray) {
trace = startItem * itemSize;
for (int i=0;i<numBytes;i++) {
newArray[i] = theArray[trace];
trace++;
}
}
return newArray;
}
// This is a couple examples how to use it.
void setup(void) {
float myArray[20]; // An array that can hold 20 floats.
float* newArray; // A pointer to a non-allocated float array.
char myStr[] = "Hello this is a big 'Ol string that goes on and on.";
char* newStr; // A pointer to an non-allocated c-string.
Serial.begin(9600); // Fire up serial for display.
for(int i=0;i<20;i++) { // For ever char in the float array.
myArray[i] = rand(); // We pop in a random number.
}
for(int i=0;i<20;i++) { // Lets have a look at what we got.
Serial.print(myArray[i]);
Serial.print(",");
}
Serial.println();
Serial.println("-----");
Serial.println();
// Ok this is how you set up the call for a array of floats..
newArray = (float*)subArray((byte*)myArray,sizeof(float),3,5);
for(int i=0;i<5;i++) { // Lets have a look at what the result was..
Serial.print(newArray[i]);
Serial.print(",");
}
Serial.println();
Serial.println("-----");
Serial.println();
// !!!
free(newArray); // IMPORTANT, when done with the new array, FREE IT!
// !!!
Serial.println(myStr); // Now have a look at our string..
Serial.println("-----");
Serial.println();
// Ok this is how you set up the call for a array of chars.
newStr = (char*)subArray((byte*)myStr,sizeof(char),14,16);
for(int i=0;i<16;i++) { // We can't use string stuff because, this doesn't add the '\0'
Serial.print(newStr[i]); // So we print out all the chars one by one.
}
// !!!
free(newStr); // IMPORTANT, when done with the new array, FREE IT!
// !!!
}
void loop() {
// put your main code here, to run repeatedly:
}
You should never return a raw owning pointer (e.g. allocated by malloc). It is the number one reason for memory leaks. Users will mess up and forget to deallocate it.
Instead, use the (RAII) containers or smart pointers that abstract away memory management (or just return by value instead of a pointer). C++ Core Guidelines: I.11: Never transfer ownership by a raw pointer (T*) or reference (T&)
You shouldn't use malloc in C++, it's only supported for backwards compatibility with C. It doesn't (explicitly) create any objects and it doesn't call any constructors. You can only use it with (structs of) trivial types types, if a user tries to use your code with a struct or class that doesn't satisfy these requirements, it's undefined behavior and your program ends up in an invalid state. The compiler doesn't even warn you about this.
If you must allocate memory manually (in this case and in many other cases, you don't), prefer new, it does everything malloc does with a cleaner interface, and it doesn't have any unexpected limitations that break your program. C++ Core Guidelines: R.10: Avoid malloc() and free()
Similarly, you cannot copy general objects byte-by-byte, you must use the copy assignment operator (=). Try using your function on an array of String, for example ...
This is really sketchy, you're using malloc to implicitly create an array of byte, and then reinterpret_cast it to an array of floats, which invokes undefined behavior.
C-style casts are dangerous, they get converted to const_cast or reinterpret_cast without any warnings. If that happens, it's almost certainly undefined behavior. C++ Core Guidelines: ES.48: Avoid casts C++ Core Guidelines: ES.49: If you must use a cast, use a named cast
The C-style cast is dangerous because it can do any kind of conversion, depriving us of any protection from mistakes (now or in the future).
Even if you would decide to ignore all previous guidelines, why the raw loop instead of memcpy?
You shouldn't rely on the user to provide the size of the element type. The compiler already knows this size, and can check it or pass it to your function for you. If you rely on the user to supply it manually, he/she will eventually mess up. For example, one might want to refactor this code to use double instead of float, and an unsuspecting user might simply replace the types until the compiler is happy: newArray = (double*)subArray((byte*)myArray,sizeof(float),3,5);
Good luck debugging that ...
Similarly, the compiler cannot help you check that the types of your arrays make sense. You could pass in an array of char, with the sizeof float as element size, and then cast the result to a pointer to Servo, and the compiler will happily generate complete nonsense without even warning you.
A much safer alternative with the same functionality as yours, but without the caveats could be: (view and run code on Godbolt)
#include <vector>
#include <cassert>
// For vectors
template <class T>
std::vector<T> subArray(const std::vector<T> &theArray, size_t startItem, size_t numItems) {
assert(startItem < theArray.size());
assert(startItem + numItems <= theArray.size());
return std::vector<T>(std::begin(theArray) + startItem,
std::begin(theArray) + startItem + numItems);
}
// For C-style arrays
template <class T, size_t N>
std::vector<T> subArray(const T (&theArray)[N], size_t startItem, size_t numItems) {
assert(startItem < N);
assert(startItem + numItems <= N);
return std::vector<T>(std::begin(theArray) + startItem,
std::begin(theArray) + startItem + numItems);
}
// This is a couple examples how to use it.
void setup() {
float myArray[20]; // And array that can hold 20 floats.
for(float &el : myArray) // For ever element in the array. (Range-based for loop)
el = rand(); // Please don't use rand(), it's terrible at generating random numbers ...
for(float el : myArray) { // Lets have a look at what we got.
Serial.print(el); Serial.print(", ");
}
Serial.println();
// No need to manually specify the size of the elements,
// no need to manually do an unsafe cast to the correct type,
// the compiler handles all of this for you.
// The function also works as expected for any type of array, not just
// primitive types like int, char, float etc.
auto newArray = subArray(myArray, 3, 5);
for(float el : newArray) { // Lets have a look at what the result was.
Serial.print(el);
Serial.print(", ");
}
Serial.println();
// No need worry about deallocation here, the compiler makes sure
// the memory is always freed correctly, even if we would return
// early or throw an exception.
}
That still leaves the question whether dynamic allocation is really necessary for the OP's use case ... All arrays and sub-arrays in his example have constant sizes known at compile time. In that case, it's better to use fixed-size arrays instead of dynamically allocating storage, especially on embedded systems where certain dynamic memory allocation patterns cause heap fragmentation.
Which is why it is important to point out and discuss unsafe practices
Not like I’m writing code for a Boeng 737.
Why should that matter? Safe and correct code is beneficial to everyone, in any kind of code base.
Besides, Arduino is a common gateway drug to more serious kinds of programming, making sure beginner's don't pick up bad practices is in everyone's best interest.
You're right, I should probably have mentioned that.
The main goal of that piece of code was to demonstrate that it's easy to write safe, readable code with the same functionality as the example posted by JimLee.
To be able to run my example, you need access to the C++ standard library, which is not available on AVR-based Arduinos. For AVR, you can either install a 3rd party port of the standard library, or use another library that provides functionality similar to std::vector: a type-safe dynamic array class that handles memory allocation for you.
On AVR, I'd probably recommend starting with the Array::slice code I posted earlier, since it doesn't require any dynamic memory allocation.
The Arduino Helpers library I used in that example also includes a standard library port for AVR that you can use for the dynamic std::vector example. To get it to actually compile and run on an AVR Arduino: include the Arduino Helpers library, replace #include <vector> with #include <AH/STL/vector>, replace <cassert> with assert.h, add a call to Serial.begin and an empty loop function.
The following code was tested on an Arduino Leonardo:
#include <Arduino_Helpers.h>
#include <AH/STL/vector>
#include <assert.h>
// For vectors
template <class T>
std::vector<T> subArray(const std::vector<T> &theArray, size_t startItem, size_t numItems) {
assert(startItem < theArray.size());
assert(startItem + numItems <= theArray.size());
return std::vector<T>(std::begin(theArray) + startItem,
std::begin(theArray) + startItem + numItems);
}
// For C-style arrays
template <class T, size_t N>
std::vector<T> subArray(const T (&theArray)[N], size_t startItem, size_t numItems) {
assert(startItem < N);
assert(startItem + numItems <= N);
return std::vector<T>(std::begin(theArray) + startItem,
std::begin(theArray) + startItem + numItems);
}
// This is a couple examples how to use it.
void setup() {
Serial.begin(115200);
while (!Serial);
float myArray[20]; // An array that can hold 20 floats.
for(float &el : myArray) // For ever element in the array. (Range-based for loop)
el = rand(); // Please don't use rand(), it's terrible at generating random numbers ...
for(float el : myArray) { // Lets have a look at what we got.
Serial.print(el); Serial.print(", ");
}
Serial.println();
// No need to manually specify the size of the elements,
// no need to manually do an unsafe cast to the correct type,
// the compiler handles all of this for you.
// The function also works as expected for any type of array, not just
// primitive types like int, char, float etc.
auto newArray = subArray(myArray, 3, 5);
for(float el : newArray) { // Lets have a look at what the result was.
Serial.print(el);
Serial.print(", ");
}
Serial.println();
// No need worry about deallocation here, the compiler makes sure
// the memory is always freed correctly, even if we would return
// early or throw an exception.
}
void loop() {}
Absolutely, which is why my first posts focused on much simpler approaches that build on the OP's understanding of substrings, such as strncpy and the standard library string_view::substr() function that allows you to slice character arrays (and not just Strings or std::strings).
If you are going to be returning arrays from functions, I think the Array::slicecode I posted here
is quite useful and elegant. It shouldn't be too hard to understand if you're familiar with with String.substr(), for example.
The index arguments passed between angled brackets (<>) instead of parentheses might be new, but that's just something you can accept as a given without having to understand how templates work, all of the complexity is hidden inside of the library. The angled brackets are just as arbitrary as using parentheses, square brackets or curly braces for other operations.
The std::vector-based approach was only in response to JimLee's code, which I would argue is not at all something you would want to give to a beginner. Apart from the bad practices you're teaching them, you're basically handing them a loaded gun, aimed at their foot.
If you need dynamic allocation, I believe std::vector is absolutely the right thing to be teaching. In a real teaching session, you would probably leave out the templates at first so the need for them arises naturally. I included them here because OP explicitly asked for arrays of any type.
Other than that, vectors are much easier to explain than pointers, casts, malloc, free, sizeof ... both as a concept and in practice.
And most importantly, they won't blow off the legs of unsuspecting beginners.
Simple, naive, and resource-saving
It's not a second array, it's a pointer into the existing array.
void setup()
{
Serial.begin(9600);
char array[] = "Hello World 123"; // At least one char plus the zero
char* const second {array+1};
Serial.println(array);
Serial.println(second);
strcpy(array,"Onioncake");
Serial.println(array);
Serial.println(second);
}
void loop()
{
}