Wskaźniki i tablice¶
Wskaźniki¶
Dla typu T
, typ T*
jest typem „wskaźnik do T”. Zmienna typu T*
przechowuje adres obiektu typu T
.
char c = 'a';
char* p = &c; // p przechowuje adres c
Przykład deklaracji wykorzystujących typy wskaźnikowe:
int* ptrInt; // wskaźnik do int
char* pc; // wskaźnik do char
char** ppc; // wskaźnik do wskaźnika do p
int* ap[15]; // tablica 15 wskaźników do int
int (*fp)(char*); // wskaźnik do funkcji przyjmującej jako argument
// wskaźnik do char i zwracającej wartość typu int
int* f(char*); // funkcja przyjmująca jako argument wskaźnik do char i
// zwracająca wskaźnik do int
Dereferencja wskaźników¶
Z użyciem wskaźników związane są operatory:
Operator
&
zwraca adres wartości lub wskazanej zmiennejOperator
*
umożliwia dereferencję wskaźnika – zwraca to, na co wskazuje wskaźnikOperator
->
umożliwa dostęp do składowych obiektu wskazywanego przez wskaźnik
char c = 'a';
char* p = &c;
char c2 = *p;
std::string word = "Szkolenie C++";
std::string* ptr_str = &word;
int word_length = ptr_str->length();
Stałe i wskaźniki¶
Typy wskaźnikowe mogą być dekorowane modyfikatorem const
:
void f1(char* p)
{
char s[] = "Hello";
const char* pc = s; // wskaźnik do stałej
pc[3] = 'g'; // błąd: pc wskazuje na stałą
pc = p; // ok.
char* const cp = s; // stały wskaźnik
cp[3] = 'a'; // ok.
cp = p; // błąd: cp jest stałą
const char* const cpc = s; // stały wskaźnik do stałej
cpc[3] = 'a'; // błąd
cpc = p; // błąd
}
Wskaźniki funkcji¶
Funkcje mogą być wywołane pośrednio z wykorzystaniem wskaźników do funkcji.
int factorial(int x) // deklaracja + definicja funkcji
{
if (x == 0)
return 1;
else
return x * factorial(x-1);
}
int result = factorial(4); // bezpośrednie wywołanie funkcji
int (*ptrFun)(int); // ptrFun – wskaźnik na funkcje typu int xxx(int)
ptrFun = &factorial; // przypisanie wskaźnikowi adresu funkcji
result = ptrFun(7); // pośrednie wywołanie funkcji z wykorzystaniem
// wskaźnika
Dla funkcji dozwolone jest:
jej wywołanie
pobranie jej adresu
#include <iostream>
#include <string>
void error(const std::string& s)
{
std::cout << "Error: " << s << std::endl;
exit(1);
}
int div(int a, int b, void (*on_error)(const std::string&))
{
if (b == 0) // jeśli dzielnik == 0
on_error("dzielenie przez 0"); // wywołanie zwrotne
return a / b;
}
int main()
{
int x = 10;
int y = 0;
std::cout << x << "/" << y << " = " << div(x, y, &error) << std::endl;
}
Puste wskaźniki¶
Stan wskaźnika niezainicjowanego jest nieokreślony – może on wskazywać na cokolwiek.
Tworząc zmienną wskaźnikową zawsze powinniśmy zainicjować jej wartość adresem obiektu lub wartością
nullptr
(w kodzie legacy - wartością 0
lub NULL
).
nullptr
- uniwersalny pusty wskaźnik¶
Nowe słowo kluczowe w C++11 - nullptr
.
wartość dla wskaźników, które na nic nie wskazują
bardziej czytelny i bezpieczniejszy odpowiednik stałej
NULL/0
posiada zdefiniowany przez standard typ -
std::nullptr_t
(zdefiniowany w pliku nagłówkowym<cstddef>
)
Istnieje niejawna konwersja z wartości nullptr
do pustej (zerowej) wartości dowolnego typu wskaźnikowego (lub do wskaźnika do składowej).
int* ptr = nullptr;
namespace std
{
typedef decltype(nullptr) nullptr_t;
}
int* p = nullptr;
int* p1 = NULL;
int* p2 = 0;
p1 == p; // true
p2 == p; // true
int* p {}; // p is set to nullptr
nullptr
rozwiązuje problem z przeciążeniem funkcji przyjmujących jako argument wskaźnik lub typ całkowity:
void foo(int);
foo(0); // wywołuje foo(int)
foo(NULL); // wywołuje foo(int)
foo(nullptr); // błąd kompilacji
void bar(int);
void bar(void*);
void bar(nullptr_t);
bar(0); // wywołuje bar(int)
bar(NULL); // wywołuje bar(int) jeśli NULL jest zdefiniowane przez 0
// błąd dwuznaczności jeśli NULL jest zdefiniowane przez 0L
bar(nullptr); // wywołuje bar(void*) lub bar (nullptr_t) (jeśli jest dostępne)
Testowanie wskaźników¶
Stan wskaźnika niezainicjowanego jest nieokreślony – może on wskazywać na cokolwiek. Literał nullptr służy do oznaczenia wskaźnika, który niczego nie wskazuje.
int* px1 = nullptr;
if (px1 != nullptr)
{
//...
}
int* px2{}; // odpowiednik int* px2 = nullptr;
if (px2)
{
//...
}
if (!px2)
{
//...
}
Wskaźniki do void
¶
Wskaźnik do dowolnego typu może zostać przypisany do typu void*
. Wskaźnik void*
może być przypisany do innego wskaźnika typu void*
. Dozwolone są porównania między tymi wskaźnikami (==
, !=
). Inne operacje są niedozwolone. Wskaźnik do typu void reprezentuje wskaźnik do fragmentu pamięci, ale bez znajomości typu (wskaźnik do „surowej” pamięci).
void f(int* pi)
{
void* pv = pi; // niejawna konwersja z int* do void*
*pv; // błąd!
pv++; // błąd! nieznany rozmiar
int* ptr2pi = static_cast<int*>(pv);
(*ptr2pi)++;
}
Tablice¶
Tablica – sekwencja elementów określonego typu o ustalonej długości i zajmująca sąsiadujące ze sobą komórki pamięci. Liczba elementów musi zostać określona podczas tworzenia tablicy. Dostęp do elementów tablicy odbywa się za pomocą operatora indeksu []. Elementy są indeksowane od 0 do rozmiar-1.
int values[10]; // tablica 10 elementów typu int
values[0] = 77;
values[9] = values[0];
for (int i = 0; i < 10; i++)
{
values[i] = 42;
}
Inicjalizacja tablic¶
Tablice mogą być inicjalizowane przy pomocy listy wartości.
int v1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
char v2[] = {'a', 'b', 'c', 'd', 0};
Jeśli na liście inicjalizującej znajduje się za mało wartości pozostałe elementy przyjmują wartość zero.
int v1[8] = {1, 2, 3, 4};
int v2[8] = {1, 2, 3, 4, 0, 0, 0, 0};
Taki sposób inicjalizacji jest dozwolony tylko w momencie deklaracji.
Tablice i wskaźniki¶
Dostęp do elementów tablicy może odbywać się za pomocą wskaźników. Nazwa tablicy może być używana jako wskaźnik do pierwszego elementu tablicy.
int values[10]; // tablica 10 elementów typu int
*values = 88; // values[0] = 88
int* vp = values; // równoważne vp = &values[0]
Arytmetyka wskaźników¶
Wskaźniki mogą udostępniać dostęp do dowolnego elementu tablicy.
Na wskaźnikach można wykonywać operacje arytmetyczne.
Jeśli do wskaźnika dodamy wartość całkowitą n, przesunie się o n elementów; operator
++
umożliwia przesunięcie wskaźnika o jeden element.Wyrażenie będące sumą wskaźnika i wartości n wskazuje n-ty element za elementem wskazywanym przez wskaźnik.
Jeśli odejmiemy od siebie wartości dwóch wskaźników, uzyskamy liczbę całkowitą reprezentującą odległość między elementami wskazywanymi przez te wskaźniki.
int values[10]; // tablica 10 elementów typu int
for (int* p = values; p < values+10; ++p)
{
std::cout << "index: " << p-values
<< " value: " << *p << std::endl;
}
int x = values[5];
*(values+5) = 10;
Tablice wielowymiarowe¶
Tablice wielowymiarowe reprezentowane są jako tablice tablic.
int d2[10][20]; // d2 jest tablicą 10 tablic 20 wartości typu int
int value = d2[0][19];
// zerowanie tablicy
for (int i = 0; i < 10; i++)
for (int j = 0; j < 20; j++)
d2[i][j] = 0;
Tablice array<T, N>
¶
Tablica o stałym rozmiarze (fixed size array) specyfikowanym w momencie kompilacji.
Pamięć dla tablicy array
może być alokowna na stosie, w bloku pamięci statycznej lub wewnątrz obiektu (jako składowa). Spełnia wszystkie wymagania dla kontenera standardowego.
Plik nagłówowy:
<array>
Implementacja typu
array
nie ma żadnego narzutu w runtimieTyp
std::array
jest agregatem, więc możliwa jest inicjalizacja agregatowa.Posiada w interfejsie metody zwracające iteratory:
T* begin()
lubconst T* begin() const
- zwracają iterator wskazujący na początek tablicyT* end()
lubconst T* end() const
- zwracają iterator wskazujący na następny element za ostatnim elementem tablicy
Metoda
data()
zwraca wskaźnik do typu danych przechowywanych w tablicy.std::array<int, 4> arr1 = {1, 2, 3, 4}; for(size_t i = 0; i < arr1.size(); ++i) cout << arr1[i] << endl; int* buffer = arr1.data(); *(buffer+2) = -1; for(const auto& item : arr1) cout << item << " "; cout << endl;
Metoda
swap()
umożliwia wymianę danych w tablicachstd::array<int, 4> arr1 = {1, 2, -1, 4}; std::array<int, 4> arr2 = {}; print(arr1, "arr1: "); print(arr2, "arr2: "); arr1.swap(arr2); cout << "\nSwap:\n"; print(arr1, "arr1: "); print(arr2, "arr2: ");
arr1: [ 1 2 -1 4 ] arr2: [ 0 0 0 0 ] Swap: arr1: [ 0 0 0 0 ] arr2: [ 1 2 -1 4 ]
vector<T> - tablica dynamiczna¶
Klasa std::vector<T> stanowi alternatywę dla natywnych tablic C++.
Zalety:
Dynamiczny rozmiar możliwy do zmiany w trakcie działania programu
Przechowuje informację o swoim rozmiarze – metoda
size()
Można używać indeksów
[]
Metoda push_back()
dodaje do wektora nowy element. Element jest wstawiany na końcu kolekcji.
#include <vector>
std::vector<int> numbers = { 1, 2, 3 }; // wektor zainicjowany wartościami { 1, 2, 3}
numbers.push_back(4); // [1, 2, 3, 4]
numbers.push_back(5); // [1, 2, 3, 4, 5]
numbers.push_back(6); // [1, 2, 3, 4, 5, 6]
for(size_t i = 0; i < numbers.size(); ++i)
std::cout << numbers[i] << "\n";
std::vector<std::string> names(5); // utworzenie wektora zawierającego 5
names[0] = "Jan"; // elementów typu string
names[1] = "Ala";
names[2] = "Aleksandra";
names[3] = "Zyta";
names[4] = "Katarzyna";, 5, 6
names.push_back("Krzysztof");
for(const auto& name : names)
std::cout << name << "\n";
Pętla for
dla zakresów¶
Range-Based for
iteruje po wszystkich elementach zakresu. Jest kompatybilna z:
zakresami/kontenerami, które posiadają w interfejsie metody
begin()
iend()
listami inicjalizacyjnymi
iteratorami
tablicami natywnymi (C-arrays)
std::vector<int> vec;
//... inserting items
for(int item : vec)
cout << item << endl;
for(int& item : vec) // using ref to modify each element
item *= 2;
Kopiowanie elementów w trakcie iteracji może obniżyć wydajność (np. dla typów string
, shared_ptr
) lub może być zabronione np. unique_ptr
.
Można uniknąć kopiowania elementów w trakcie iteracji dodając referencję. Opcjonalnie można również stosować modyfikator const
lub volatile
.
std::vector<std::shared_ptr<Gadget>> shared_gadgets;
// ...
for(const auto& ptr : shared_gadgets)
ptr->do_something();
std::vector<std::unique_ptr<Gadget>> unique_gadgets;
// ...
for(auto ptr : unique_gadgets) // compilation error
ptr->do_something();
for(const auto& ptr : unique_gadgets) // ok
ptr->do_something();
Mechanizm pętli Range-Based for
¶
Wyrażenie
for( decl : coll )
{
statement;
}
jest rozwijane do pętli:
for(auto _pos = coll.begin(), _end = coll.end(); _pos != _end; ++_pos)
{
decl = *_pos;
statement;
}
lub z niższym priorytetem do pętli:
for(auto _pos = begin(coll), _end = end(coll); _pos != _end; ++_pos)
{
decl = *_pos;
statement;
}