Można oczywiście wskaźnikami. Będziesz jednak zajmował się szczegółami które spowodują wprowadzanie wielu ilości błędów (brak dealokacji, zła arytmetyka wskaźników, błędne dostępy). Lepiej zacząć od std::vector zawierające inne std::vector :-) Jeśli będziesz wiedział jakie mają być prawdziwe ograniczenia tej struktury, zmienisz typ danych. Naprawdę na początku programowania 95% zastosowań kontenerów to... std::vector :-)
#include <iostream>
#include <vector>
using container_t = std::vector<std::vector<int>>;
using row_t = typename container_t::value_type;
void makeTriangle(container_t& data) {
for(auto i = 0U; i < data.size(); ++i) {
data[i] = row_t(i + 1);
}
}
void showData(const container_t& data) {
for(const auto& row: data) {
for(const auto& field: row) {
std::cout << field << ' ';
}
std::cout << '\n';
}
}
int main() {
constexpr static size_t DATA_SIZE = 20;
// 20 wierszowy wektor...
container_t data(DATA_SIZE);
makeTriangle(data);
showData(data);
}
Jeśli upierasz się przy wskaźnikach i tablicach dynamicznych, otrzymasz jakiś taki kod:
#include <iostream>
#include <vector>
constexpr static size_t DATA_SIZE = 20;
void makeTriangle(int *** data) {
*data = new int *[DATA_SIZE];
for(auto i = 0U; i < DATA_SIZE; ++i) {
(*data)[i] = new int[i + 1]();
}
}
void showData(int *** data) {
for(auto row = 0U; row < DATA_SIZE; ++row) {
for(auto col = 0U; col < row + 1; ++col) {
std::cout << (*data)[row][col] << ' ';
}
std::cout << '\n';
}
}
void disposeData(int *** data) {
for(auto row = 0U; row < DATA_SIZE; ++row) {
delete [] (*data)[row];
}
delete [] (*data);
}
int main() {
int ** data;
makeTriangle(&data);
showData(&data);
disposeData(&data);
}
Sam oceń czy to jest czytelne i nie popełnisz błędu :-/ Dane do funkcji przekazywane są przez wartość, stąd konieczność przekazania 3-krotnego wskaźnika. Dane są 2-krotnym wskaźnikiem.
Dla maksymalnej wydajności i przy tablicach o regularnej strukturze (np. takiej jak w przykładach), alokuje się ciągłe miejsce w pamięci czyli tablicę 1-wymiarową. Wymaga to później w trakcie dostępów obliczania adresu docelowego. Taka implementacja będzie wydajniejsza ze względu na trafianie w cache procesora.