Operatori, expresii și conversii
Prioritatea operatorului principal, conversiile aritmetice, pericolele amestecării semnate/nesemnate și cele patru modele C++ numite.
Expresii: Elementele de bază ale calculului
O expresie în C++ este o secvență de operatori și operanzi care calculează o valoare. Chiar și o instrucțiune simplă precumx = a + b;conține mai multe sub-expresii:a,b,a + bșix = a + b(atribuirea în sine este o expresie care returnează valoarea atribuită).
Fiecare expresie are două proprietăți: un tip și o categorie de valori (lvalue, rvalue etc.). Înțelegerea acestor proprietăți este esențială deoarece C++ aplică conversii implicite între tipuri, iar regulile pentru aceste conversii pot produce rezultate surprinzătoare dacă nu ești atent.
Operatorul precedența determină modul în care operatorii sunt grupați (ceea ce se leagă mai strâns), în timp ce asociativitatea determină direcția de grupare pentru operatorii la același nivel de precedență. Găsirea acestor erori duce la erori subtile care se compilează fără avertisment.
Surprize de prioritate a operatorului
Precedența operatorului este una dintre cele mai comune surse de erori. Când aveți îndoieli, utilizați paranteze pentru a vă explicita intenția:
#include <iostream>
int main() {
// Precedence trap #1: bitwise vs comparison
int x = 5;
// This does NOT check if bit 1 is set:
if (x & 1 == 0) { // Parsed as: x & (1 == 0) → x & 0 → 0
std::cout << "Never reached!\n";
}
// Correct:
if ((x & 1) == 0) { // Parentheses fix the grouping
std::cout << "Even\n";
}
// Precedence trap #2: ternary vs assignment
int a = 1, b = 2;
int result = a > b ? a : b = 10; // Parsed as: (a > b ? a : b) = 10 — ERROR!
// Correct:
int correct = (a > b) ? a : b; // Parenthesize for clarity
// Precedence trap #3: pointer dereference vs member access
// (*ptr).member vs ptr->member — both are equivalent
// *ptr.member is parsed as *(ptr.member) — usually a compile error
// Short-circuit evaluation
int* ptr = nullptr;
// Safe: the second operand is NOT evaluated if the first is false
if (ptr != nullptr && *ptr > 0) {
std::cout << "Value: " << *ptr << '\n';
}
// The && and || operators guarantee left-to-right evaluation and short-circuit
// Overloaded && and || do NOT short-circuit — avoid overloading them
}Apasă Rulează pentru a compila și rula acest exemplu.
Conversii aritmetice și promovare întregi
Când amestecați tipuri într-o expresie, C++ aplică conversii implicite urmând un set de reguli numit conversii aritmetice obișnuite:
1. Promovare cu numere întregi: tipurile mai mici decâtint(char,short,bool) sunt mai întâi promovate laint(sauunsigned intdacă valoarea nu se poate încadra înint). Aceasta înseamnă căchar + charproduce unint, nu unchar.
2. Echilibrare: Dacă operanzii încă mai diferă ca tip după promovare, tipul „mai mic” este convertit în cel „mai mare” urmând o ierarhie:int→unsigned int→long→unsigned long→long long→unsigned long long→float→double→double→double.
3. Interacțiune cu semnătură/nesemnată: când un tip semnat întâlnește un tip nesemnat de același rang, valoarea semnată este convertită în nesemnată. Aici apar cele mai periculoase conversii implicite.
Amestecarea numerelor întregi semnate și nesemnate este una dintre cele mai comune și periculoase capcane din C++. Când o valoare cu semn negativ este convertită implicit în fără semn, aceasta se înfășoară într-un număr pozitiv foarte mare (deoarece tipurile fără semn folosesc aritmetica modulară).
Acest lucru se întâmplă frecvent atunci când se compară valorileintcusize_t(care esteunsigned) - de exemplu, se repetă peste unstd::vectorcu un contor de buclăintși se compară cuvector.size().
Compilați cu-Wsign-compare(inclus în-Wall) pentru a detecta aceste probleme.
- Un int negativ convertit în nesemnat devine un număr foarte mare (de exemplu, -1 devine 4294967295 pentru 32 de biți)
- Compararea valorilor semnate și nesemnate transformă mai întâi valoarea semnată în nesemnată
- vector.size() returnează size_t (nesemnat) — compararea cu un int negativ dă rezultate greșite
- Utilizați -Wsign-compare pentru a detecta problemele de comparație semnate/nesemnate în timpul compilării
Distribuții cu nume C++ vs distribuții în stil C
C++ oferă patru operatori de turnare numiți care înlocuiesc periculoasa turnare în stil C(Type)expr. Fiecare are un scop specific și niveluri diferite de siguranță:
#include <iostream>
#include <cstdint>
class Base {
public:
virtual ~Base() = default;
virtual void speak() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void speak() override { std::cout << "Derived\n"; }
void only_in_derived() { std::cout << "Special!\n"; }
};
int main() {
// static_cast — compile-time checked, for "natural" conversions
double pi = 3.14159;
int truncated = static_cast<int>(pi); // Explicit: you acknowledge the truncation
std::cout << truncated << '\n'; // 3
// dynamic_cast — runtime-checked, for safe downcasting in class hierarchies
Base* base_ptr = new Derived();
Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr);
if (derived_ptr) { // Check for success!
derived_ptr->only_in_derived(); // Safe
}
// const_cast — adds or removes const (use sparingly!)
const int ci = 42;
// int& mutable_ref = const_cast<int&>(ci); // Legal but modifying ci is UB!
// Only valid use: calling a non-const API with a const object you know won't be modified
// reinterpret_cast — reinterprets the bit pattern (dangerous!)
std::int64_t address = reinterpret_cast<std::int64_t>(base_ptr);
std::cout << "Address: " << address << '\n';
// C-style cast: (int)pi — NEVER use this in C++
// It tries static_cast, then const_cast, then reinterpret_cast —
// you don't know which one was applied!
delete base_ptr;
}Apasă Rulează pentru a compila și rula acest exemplu.
Utilizați modelele numite pentru a comunica intenția și pentru a obține verificarea adecvată a compilatorului:
- static_cast — Pentru conversii bine definite: conversii de tip numeric, upcast-uri în ierarhiile de clasă șivoid*la indicatorul tastat. Acesta este ghipsul pe care îl vei folosi cel mai des.
- dynamic_cast — Pentru downcasting sigur în ierarhii polimorfe. Necesită cel puțin o funcție virtuală în clasa de bază. Returneazănullptr(pentru pointeri) sau aruncăstd::bad_cast(pentru referințe) la eșec. Are un cost de rulare.
- const_cast — Numai pentru interfața cu API-uri vechi care nu sunt corecte în mod constant. Nu îl utilizați niciodată pentru a modifica un obiect cu adevărat const.
- reinterpret_cast — Pentru reinterpretarea biților la nivel scăzut. Aproape întotdeauna indică cod specific platformei. Este rar necesar în codul aplicației.
Nu utilizați niciodată modelele în stil C(Type)exprîn C++ - ele ascund ce conversie se efectuează și ocolesc verificările de siguranță.
- Utilizați static_cast pentru conversii numerice și upcast-uri sigure - este verificat în timpul compilării
- Utilizați dynamic_cast pentru reduceri sigure în ierarhiile polimorfe - este verificat în timpul execuției
- Use const_cast only to interface with non-const-correct legacy APIs
- Never use C-style casts in C++ — they hide which conversion is applied
- Precedența operatorului determină gruparea - atunci când aveți îndoieli, utilizați paranteze pentru a explicita intenția
- Short-circuit evaluation in && and || guarantees left-to-right order and skips unnecessary evaluation
- Promovarea întregului convertește tipurile mici (char, short, bool) în int înainte de aritmetică
- Amestecarea tipurilor semnate și nesemnate transformă valoarea semnată în unsigned - un int negativ devine un număr pozitiv imens
- Folosiți cele patru distribuții numite (static_cast, dynamic_cast, const_cast, reinterpret_cast) în loc de modele în stil C
- Modelele în stil C sunt periculoase, deoarece încearcă în tăcere mai multe tipuri de distribuție fără a vă spune care dintre ele a fost aplicată
Test — Verifică-ți cunoștințele
(15 XP)1. Cum evaluează de fapt expresia`x & 1 == 0`?
2. Ce se întâmplă când compari`-1 < 0u`(unde`0u`este nesemnat)?
3. Ce distribuție C++ ar trebui să utilizați pentru downcasting în siguranță într-o ierarhie de clasă polimorfă?