C++/Zmienne

Z Wikibooks, biblioteki wolnych podręczników.

< C++

Zanim przystąpisz do czytania tego rozdziału upewnij się, że opanowałeś już wiedzę z podręcznika C. Jest tu wykorzystywanych wiele odniesień i pojęć z tego języka.

Spis treści

[edytuj] Deklarowanie zmiennych

W języku C zmienne deklarowało się na początku bloku kodu (zwykle przed pierwszą instrukcją). W przeciwieństwie do C++ nie można było natomiast deklarować zmiennych np. w nagłówku pętli for. Poniższy przykład bez problemu powinien zadziałać w kompilatorach języka C++, natomiast starsze kompilatory C mogą go uznać za błędny:

int main ()
{
   for (int i = 0; i <= 10; ++i ) {
      // instrukcje...
   }
}

W C++ deklaracje zmiennych mogą znajdować się w dowolnym miejscu kodu w funkcji, nie obowiązuje już zasada z C nakazująca ich deklarowanie przed właściwym kodem funkcji:

#include <iostream>
using namespace std;
 
int main ()
{
   int i;
   cin >> i;
   int j = i*i;
   cout << j;
   return 0;
}

[edytuj] Kontrola typów

W C++ w stosunku do C została zaostrzona kontrola typów. Teraz za każdym razem, gdy przekażemy funkcji zmienną o innym typie dostaniemy błąd od kompilatora. Główna zmiana dotyczy wskaźników na typ void*. W C były one zupełnie bezkarne i można było przydzielać wskaźniki void* do każdych innych, w C++ są na równi z innymi typami. Teoretycznie kod napisany w C powinien zostać bez problemu skompilowany w kompilatorze C++, lecz istnieje kilka rozbieżności, które czasami to uniemożliwiają. Jedna z nich dotyczy właśnie typu void*. Kod w C, bez problemu skompilowany w kompilatorze tegoż języka:

int* wskaznik = malloc(sizeof(int));

nie zostanie skompilowany w kompilatorze C++, z powodu zaostrzonej kontroli typów. Aby sprawić, że ten kod będzie się kompilować musimy go odrobinę zmodyfikować:

int* wskaznik = (int*)malloc(sizeof(int));

Problem został rozwiązany przy użyciu rzutowania. Co to takiego? Odpowiedź znajdziesz w dziale poniżej.

[edytuj] Rzutowanie

W języku C rzutowanie wyglądało w następujący sposób:

int zmienna_calkowita = (int)zmienna_rzeczywista;

W C++ nadal można używać takiego rzutowania, jest ono nazywane "rzutowaniem w stylu C". Oprócz tego C++ oferuje "rzutowanie w stylu funkcyjnym":

int zmienna_calkowita = int(zmienna_rzeczywista);

które działa dokładnie tak samo.

Oba zapisy mają jedną istotną wadę - ciężko wypatrzeć je w kodzie. Każde rzutowanie jest potencjalnym miejscem wystąpienia błędów. Jeśli byśmy chcieli przejrzeć kod źródłowy w poszukiwaniu wszystkich rzutowań, nie byłoby to łatwe, przez co usuwanie błędów z programu w stylu języka C jest utrudnione.

C++ wprowadza inny sposób zapisu, który od razu rzuca się w oczy. Dodatkowo rzutowanie podzielono na cztery typy:

static_cast 
proste rzutowanie
const_cast 
rzutowanie ze zmiennych z modyfikatorem const i volatile na zmienne bez tych modyfikatorów
reinterpret_cast 
niebezpieczne rzutowania, które zmieniają zupełnie sens interpretacji bitów w zmiennych
dynamic_cast 
rzutowanie wskaźników na obiekty

Ostatnie z tych rzutowań będzie opisane później, w rozdziale Funkcje wirtualne.

Powodem takiego podziału jest potrzeba zwiększenia bezpieczeństwa przez wyeliminowanie pomyłek. Jak to działa? Jeśli chcielibyśmy dokonać pewnego rodzaju rzutowania operatorem, który nie jest do niego przewidziany, kompilator zgłosi nam błąd. Dodatkowo, jeśli podejrzewamy, że jakiś błąd w działaniu programu wynika z rzutowania, najczęściej chodzi nam o rzutowanie konkretnego rodzaju, zatem podział rzutowań ułatwia znajdywanie takich błędów.

Nowych operatorów rzutowania używa się w następujący sposób:

int zmienna_całkowita = static_cast<int>(zmienna_rzeczywista);

podając w nawiasach ostrych typ, na który rzutujemy.

Omówimy teraz dłużej pierwsze trzy z nowych rzutowań.

Do zrobienia Do zrobienia:
dopisać dla każdego - jak używać, kiedy stosować, kiedy nie stosować, konsekwencje, przykład


[edytuj] Static_cast


Operator static_cast zapewnia wysoki poziom bezpieczeństwa, gdyż widząc static_cast kompilator używa całej swojej mądrości, żeby zagwarantować jak najsensowniejszy rezultat rzutowania, w razie potrzeby zmieniając reprezentację wartości poddanej rzutowaniu. Przykładowo przy rzutowaniu zmiennej typu int na float, bity wewnętrznej reprezentacji zostaną zmienione, tak aby reprezentowały tę samą wartość matematyczną, ale według formatu używanego dla float.

[edytuj] Static_cast służy w szczególności do:

  • Konwersji podstawowych typów liczbowych, np. int na float.
  • Konwersji zdefiniowanych przez użytkownika.
  • Konwersji wskaźnika na obiekt klasy pochodnej na wskaźnik na obiekt klasy podstawowej (tak zwane rzutowanie do góry hierarchii dziedziczenia).
  • Konwersji wskaźnika na obiekt klasy podstawowej na wskaźnik na obiekt klasy pochodnej (tak zwane rzutowanie w dół hierarchii).

Są też inne zastosowania, np. rzutowanie zmiennej za pomocą wyrażenia static_cast<void>(nazwa_zmiennej), które na niektórych kompilatorach pozwala uniknąć ostrzeżenia o nieużywaniu tej zmiennej.

Nie przejmuj się, jeżeli trzy ostatnie punkty powyższej listy są niezrozumiałe. Staną się zrozumiałe po przeczytaniu rozdziału o dziedziczeniu i definiowaniu konwersji typów. Ważny jest morał z przytoczenia tych zastosowań, a mianowicie fakt, że static_cast służy do najczęściej wykorzystywanych, zdefiniowanych przez standard języka i bezpiecznych rzutowań. Czwarty punkt na powyższej liście przypomina jednak o tym, że nie zawsze rzutowanie static_cast jest bezpieczne w czasie wykonania programu.

Wyjaśnienie dla zaawansowanych:

Jeśli wykonamy rzutowanie w dół na typ, który nie jest zgodny z rzeczywistym (dynamicznym) typem obiektu, rezultatem może być wysypanie się programu.

[edytuj] Do czego static_cast nie służy:

  • Do rzutowania wskaźników na różne typy, jeśli nie ma specjalnie zdefiniowanej konwersji między tymi wskaźnikami. Przykładowo nie skompiluje się static_cast<int*>(i), jeśli zmienna i jest typu unsigned int* Nie uda się też rzutowanie ze wskaźnika na typ stały (z modyfikatorem const) na wskaźnik na typ niestały.
  • Do dynamicznego sprawdzania, czy rzutowanie mogłoby się powieźć (czy ma sens). Nie miałoby to sensu, bo dla static_cast sposób rzutowania jest ustalany w czasie kompilacji. Zresztą nie ma żadnej informacji o błędzie, którą można by było sprawdzić.

[edytuj] Przykłady poprawnego użycia static_cast:

Do zrobienia Do zrobienia:
przykłady


[edytuj] Przykłady niepoprawnego użycia static_cast:

Do zrobienia Do zrobienia:
przykłady


[edytuj] Inne Cechy static_cast

Standard języka stwierdza również, że wyrażenia, które nie dokonują żadnej konwersji mogą być również opisane operatorem static_cast, np. int i=static_cast<int>(8);. Takie static_cast może być bezpiecznie usunięte z kodu, należy jednak uważać na usuwanie go z kodu generycznego, korzystającego z szablonów.

W powyższym wstępie i przykładach wszędzie, gdzie jest mowa o wskaźnikach, można by również mówić o referencjach. Obowiązują je te same reguły.

Należy pamiętać, że działanie rzutowania static_cast zależy tylko od takich informacji o typach, które są dostępne czasie kompilacji. Stąd słowo "static" w "static_cast". Kompilator nie dodaje "z własnej inicjatywy" kodu binarnego, więc static_cast można używać również w tzw. wąskich gardłach programu. Poprzednie zdanie celowo używa wyrażenia w cudzysłowie, bo jakiś kod oczywiście jest dodawany przez kompilator. Zazwyczaj jest to jednak tylko zmiana reprezentacji liczby lub wywołanie zdefiniowanej przez użytkownika (czyli z naszej inicjatywy) funkcji konwertującej.

[edytuj] const_cast

[edytuj] reinterpret_cast

[edytuj] Ćwiczenia

#include <stdio.h>
 
int main(int argc, char *argv[])
{
  int liczba, liczba2;
  scanf("%d %d", &liczba, &liczba2);
  double wynik = liczba / liczba2;
  printf("%d / %d = %0.1f\n", liczba, liczba2, wynik);
 
  return 0;
}

Po uruchomieniu powyższego programu i podaniu wejścia

5 2

Dlaczego jako wynik wyświetlana jest liczba 2.0 a nie 2.5? Rozwiąż problem przy użyciu rzutowania.