C++ Crash Course: a fast-Paced Introduction



Yüklə 7 Mb.
Pdf görüntüsü
səhifə35/71
tarix20.09.2023
ölçüsü7 Mb.
#145939
1   ...   31   32   33   34   35   36   37   38   ...   71
C Crash Course A Fast-Paced Introduction by Josh Lospinoso

--snip--
};
int main() {
auto hal = new Hal{}; // Memory is allocated, then constructor is called
delete hal; // Destructor is called, then memory is deallocated
}
I'm completely operational.
Stop, Dave.
Listing 22: A program that creates and destroys a 
Hal
 object
If (for whatever reason) the constructor is unable to achieve a good 
state, it typically throws an exception. As a C programmer, you might have 
dealt with exceptions when programming with some operating system APIs 
(for example, Windows Structured Exception Handling). When an excep-
tion is thrown, the stack unwinds until an exception handler is found, at 
which point the program recovers. Judicious use of exceptions can clean 
up code because you only have to check for error conditions where it makes 
sense to do so. C++ has language-level support for exceptions, as Listing 23 
illustrates.
#include
try {
// Some code that might throw a std::exception 
u
} catch (const std::exception &e) {
// Recover the program here. 
v
}
Listing 23: A 
try
-
catch
 block
You can put your code that might throw an exception in the block 
immediately following 
try
u
. If at any point an exception is thrown, the 
stack will unwind (graciously destructing any objects that go out of scope) 
and run any code that you’ve put after the 
catch
expression 
v
. If no excep-
tion is thrown, this 
catch
code never executes.
Constructors, destructors, and exceptions are closely related to another 
core C++ theme, which is tying an object’s life cycle to the resources it owns. 


An Overture to C Programmers
liii
This is the resource allocation is initialization (RAII) concept (sometimes 
also called constructor acquires, destructor releases). Consider the C++ class in 
Listing 24.
#include
#include
struct File {
File(const char* path, bool write) { 
u
auto file_mode = write ? "w" : "r"; 
v
file_pointer = fopen(path, file_mode); 
w
if (!file_pointer) throw std::system_error(errno, std::system_category()); 
x
}
~File() {
fclose(file_pointer);
}
FILE* file_pointer;
};
Listing 24: A 
File
 class
The constructor of 
File
u
takes two arguments. The first argument cor-
responds with the 
path
of the file, and the second is a 
bool
corresponding to 
whether the file mode should be open for write (
true
) or read (
false
). This 
argument’s value sets 
file_mode
v
via the ternary operator 
? :
. The ternary 
operator evaluates a Boolean expression and returns one of two values 
depending on the Boolean value. For example:
x ? val_if_true : val_if_false
If the Boolean expression 
x
is 
true
, the expression’s value is 
val_if_true

If 
x
is 
false
, the value is 
val_if_false
instead.
In the 
File
constructor code snippet in Listing 24, the constructor 
attempts to open the file at 
path
with read/write access 
w
. If anything goes 
wrong, the call will set 
file_pointer
to 
nullptr
, a special C++ value that’s 
similar to 0. When this happens, you throw a 
system_error
x
. A 
system_error
is just an object that encapsulates the details of a system error. If 
file_pointer
isn’t 
nullptr
, it’s valid to use. That’s this class’s invariant.
Now consider the program in Listing 25, which employs 
File
.
#include
#include
#include
struct File {
--
snip

};
int main() {

u
File file("last_message.txt", true); 
v
const auto message = "We apologize for the inconvenience.";


liv
An Overture to C Programmers
fwrite(message, strlen(message), 1, file.file_pointer);

w
// last_message.txt is closed here!
{
File file("last_message.txt", false); 
x
char read_message[37]{};
fread(read_message, sizeof(read_message), 1, file.file_pointer);
printf("Read last message: %s\n", read_message);
}
}
We apologize for the inconvenience.
Listing 25: A program employing the 
File
 class
The braces 
u
w
define a scope. Because the first 
file
resides within this 
scope, the scope defines the lifetime of 
file
. Once the constructor returns 
v

you know that 
file.file_pointer
is valid thanks to the class invariant; based on 
the design of the constructor of 
File
, you know 
file.file_pointer
must be valid 
for the lifetime of the 
File
object. You write a message using 
fwrite
. There’s 
no need to call 
fclose
explicitly, because 
file
expires and the destructor 
cleans up 
file.file_pointer
for you 
v
. You open 
File
again but this time for 
read access 
x
. As long as the constructor returns, you know that last_message 
.txt was opened successfully and continue on reading into 
read_message
. After 
printing the message, the destructor of 
file
is called, and the 
file.file_pointer
is again cleaned up.
Sometimes you need the flexibility of dynamic memory allocation, but 
you still want to lean on the object life cycle of C++ to ensure that you don’t 
leak memory or accidentally “use after free.” This is exactly the role of smart 
pointers, which manage the life cycle of dynamic objects through an ownership 
model. Once no smart pointer owns a dynamic object, the object destructs. 
One such smart pointer is 
unique_ptr
, which models exclusive owner-
ship. Listing 26 illustrates its basic usage.
#include
struct Foundation{
const char* founder;
};
int main() {
std::unique_ptr second_foundation{ new Foundation{} }; 
u
// Access founder member variable just like a pointer:
second_foundation->founder = "Wanda";

v
Listing 26: A program employing a 
unique_ptr
You dynamically allocate a 
Foundation
, and the resulting 
Foundation*
pointer is passed into the constructor of 
second_foundation
using the 


An Overture to C Programmers
lv
braced-initialization syntax 
u
. The 
second_foundation
has type 
unique_ptr

which is just an RAII object wrapping the dynamic 
Foundation
. When 
second 
_foundation
is destructed 
v
, the dynamic 
Foundation
destructs appropriately.
Smart pointers differ from regular, raw pointers because a raw pointer 
is simply a memory address. You must orchestrate all the memory manage-
ment that’s involved with the address manually. On the other hand, smart 
pointers handle all these messy details. By wrapping a dynamic object with 
a smart pointer, you can rest assured that memory will be cleaned up appro-
priately as soon as the object is no longer needed. The compiler knows that 
the object is no longer needed because the smart pointer’s destructor is 
called when it falls out of scope.

Yüklə 7 Mb.

Dostları ilə paylaş:
1   ...   31   32   33   34   35   36   37   38   ...   71




Verilənlər bazası müəlliflik hüququ ilə müdafiə olunur ©azkurs.org 2024
rəhbərliyinə müraciət

gir | qeydiyyatdan keç
    Ana səhifə


yükləyin