Embrace Range-v3 (or whatever will be introduced in C++20) and write a solution in (almost) a single line:
auto flattenedRange = ranges::views::join(someClassVectors);
this gives you a range in flattenedRange, which you can loop over or copy somewhere else easily.
This is a possible use case:
#include <iostream>
#include <vector>
#include <range/v3/view/join.hpp>
int main()
{
    std::vector<std::vector<int>> Ints2D = {
        {1,2,3},
        {4},
        {5,6}
    };
    auto Ints1D = ranges::views::join(Ints2D);
    // here, going from Ints1D to a C-style array is easy, and shown in the other answer already
    for (auto const& Int : Ints1D) {
        std::cout << Int << ' ';
    }
    std::cout << '\n';
    // output is: 1 2 3 4 5 6 
}
In case you want to get a true std::vector instead of a range, before writing it into a C-style array, you can include this other header
#include <range/v3/range/conversion.hpp>
and pipe join's output into a conversion function:
    auto Ints1D = ranges::views::join(Ints2D) | ranges::to_vector;
    // auto deduces std::vector<int>
In terms of standard and versions, it doesn't really require much. In this demo you can see that it compiles and runs just fine with
- compiler GCC 7.3
- library Range-v3 0.9.1
- C++14 standard (option -std=c++14tog++)
As regards the copies
- ranges::views::join(Ints2D)is only creating a view on- Ints2D, so no copy happens; if view doesn't make sense to you, you might want to give a look at Chapter 7 from Functional Programming in C++, which has a very clear explanation of ranges, with pictures and everything;¹
- even assigning that output to a variable, auto Ints1D = ranges::views::join(Ints2D);, does not trigger a copy;Ints1Din this case is not astd::vector<int>, even though it behaves as one when we loop on it (behaves as a vector because it's a view on it);
- converting it to a vector, e.g. via  | ranges::to_vector, obviously triggers a copy, because you are no more requesting a view on a vector, but a true one;
- passing the range to an algorithm which loops on its elements doesn't trigger a copy.
Here's an example code that you can try out:
// STL
#include <iostream>
#include <vector>
// Boost and Range-v3
#include <boost/range/algorithm/for_each.hpp>
#include <range/v3/view/join.hpp>
#include <range/v3/range/conversion.hpp>
struct A {
    A() = default;
    A(A const&) { std::cout << "copy ctor\n"; };
};
int main()
{
    std::vector<std::vector<A>> Ints2D = {
        {A{},A{}},
        {A{},A{}}
    };
    using boost::range::for_each;
    using ranges::to_vector;
    using ranges::views::join;
    std::cout << "no copy, because you're happy with the range\n";
    auto Ints1Dview = join(Ints2D);
    std::cout << "copy, because you want a true vector\n";
    auto Ints1D = join(Ints2D) | to_vector;
    std::cout << "copy, despite the refernce, because you need a true vector\n";
    auto const& Ints1Dref = join(Ints2D) | to_vector;
    std::cout << "no copy, because we movedd\n";
    auto const& Ints1Dref_ = join(std::move(Ints2D)) | to_vector;
    std::cout << "no copy\n";
    for_each(join(Ints2D), [](auto const&){ std::cout << "hello\n"; });
}
¹ In an attempt to try giving a clue of what a range is, I would say that you can imagine it as a thing wrapping two iterators, one poiting to the end of the range, the other one pointing to the begin of the range, the latter being incrementable via operator++; this opearator will take care of the jumps in the correct way, for instance, after viewing the element 3 in Ints2D (which is in Ints2D[0][2]), operator++ will make the iterator jump to view the elment Ints[1][0].