0% found this document useful (0 votes)
5 views

Week 11_ Dynamic Memory

The document provides a comprehensive overview of dynamic memory management in C++, focusing on pointers, their declaration, initialization, and operations. It explains the importance of pointers for efficient memory use and includes examples of pointer arithmetic, dereferencing, and swapping values using pointers. Additionally, it addresses dynamic memory allocation using 'new' and 'delete' operators, as well as the prevention of memory leaks.

Uploaded by

lujainhesham04
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views

Week 11_ Dynamic Memory

The document provides a comprehensive overview of dynamic memory management in C++, focusing on pointers, their declaration, initialization, and operations. It explains the importance of pointers for efficient memory use and includes examples of pointer arithmetic, dereferencing, and swapping values using pointers. Additionally, it addresses dynamic memory allocation using 'new' and 'delete' operators, as well as the prevention of memory leaks.

Uploaded by

lujainhesham04
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 25

Dynamic Memory

Agenda
1. Pointers
○ Introduction to Pointers

■ What is a pointer?
■ Why are pointers important in C++?

○ Declaring and Initializing Pointers

■ Syntax for declaring pointers


■ Initializing pointers with addresses
■ Assigning values to pointers

○ Working with Pointers

■ Accessing the value pointed to by a pointer


■ Dereferencing pointers
■ Pointer arithmetic

○ Pointer Operations and Manipulations

■ Swapping values using pointers


■ Returning pointers from functions

2. Iterators
○ What is an Iterator?
○ Types of Iterators

■ Forward Iterator
■ Reverse Iterator
■ Random Access Iterator

3. Dynamic Memory
○ Introduction to dynamic memory allocation
○ Allocating memory using 'new' and 'delete' operators
○ Memory leaks and how to avoid them
1. Pointers
● Introduction to Pointers

What is a pointer?

Unlike a normal variable which may hold a value of a type like int or char, a
C++ pointer is a variable that stores the memory location of another
variable. It might be helpful to think of a pointer as a shortcut on a desktop.
A pointer stores the address of another variable and thus also gives us a way
to access and change the contents of the pointed-to variable.

Pointer variables contain memory addresses as their values. Normally, a


variable directly contains a specific value. A pointer contains the memory
address of a variable that, in turn, contains a specific value. In this sense, a
variable name directly references a value, and a pointer indirectly references
a value. Referencing a value through a pointer is called indirection.
Diagrams typically represent a pointer as an arrow from the variable that
contains an address to the variable located at that address in memory.

Why are pointers important in C++?

Pointers in C++ are important for several reasons. A big consideration for
any programmer is the efficient use of memory. Pointers make it possible to
dynamically access and manipulate memory. With pointers, we can structure
memory access in custom ways. For example, we can use custom data
structures that do not otherwise fit into standard variable declarations.
Another example is efficient memory access for high-performance
applications.

Pointers are valuable because they allow us to increase program execution


speed. This results in simpler code, especially as the size of our code
increases.

For example, we can use pointer variables to modify and return by function,
a much better alternative to passing-by-value. Without pointers, we would
have to manually copy values, thus ending up with bulkier code. This works
fine when working with a single integer but can be more complex when
working with a more advanced data structure. Let’s take a look at an
example:

void createArray(int *array_name, int num) {


for (int x = 0; x < num; x++)
array_name[x] = x+1;
}

int main() {
int array[4];
int* foo = array;
createArray(array, 4);
for (int i = 0; i < 4; ++i) {
cout << *foo[i] << " ";
}
return 0;
} //Output: 1 2 3 4

As we saw above, pointers can also be used to point to, access and
manipulate the contents of a dynamic data structure like an array. While
pointers and arrays are not the same, an array name functions as a pointer.
Here, foo points to the first element of the array.

Pointers also uniquely allow for pointer operations or pointer arithmetic. You
can compare pointers not only for equality but also perform arithmetic
operations like < and <=. You can even add integers to pointers and subtract
one pointer from another.

● Declaring and Initializing Pointers

Syntax for Declaring Pointers


Pointers, like any other variables, must be declared before they can be used.
For example, for the pointer countPtr that stores an integer variable memory
address, the declaration

int* countPtr, count;

declares the variable countPtr to be of type int* (i.e., a pointer to an int


value) and is read (right to left), “countPtr is a pointer to int.” Variable count
in the preceding declaration is declared to be an int, but not a pointer to an
int. The * in the declaration applies only to the first variable. Each variable
being declared as a pointer must be preceded by an asterisk (*). When *
appears in a declaration, it’s not an operator; rather, it indicates that the
variable being declared is a pointer. Pointers can be declared to point to
objects of any data type.

Initializing pointers with addresses

Pointers should be initialized to nullptr (added in C++11) or to a memory


address either when they’re declared or in an assignment. A pointer with the
value nullptr “points to nothing” is known as a null pointer. From this point
forward, when we refer to a “null pointer” we mean a pointer with the value
nullptr.

In earlier versions of C++, the value specified for a null pointer was 0 or
NULL. NULL is defined in several standard library headers to represent the
value 0. Initializing a pointer to NULL is equivalent to initializing a pointer
to 0, but prior to C++11, 0 was used by convention. The value 0 is the only
integer value that can be assigned directly to a pointer variable without first
casting the integer to a pointer type (normally via a reinterpret_cast).
An example for pointer initialization:

int count = 10;


int* countPtr = &count;

The address operator (&) is a unary operator that obtains the memory
address of its operand.

So, we first get the memory address of the integer variable count and then
initialize the pointer variable countPtr with this address.

And it is often a good practice to initialize any pointer variables in your


program either to a real memory address of a variable like in the previous
example or to nullptr.

Assigning values to pointers

Assuming the declarations:

int y = 5; // declare variable y


int* yPtr = nullptr; // declare pointer variable yPtr
the statement:
yPtr = &y; // assign address of y to yPtr

assigns the address of the variable y to pointer variable yPtr. Then variable
yPtr is said to “point to” y. Now, yPtr indirectly references variable y’s value
(5). The use of the & in the preceding statement is not the same as its use in
a reference variable declaration, where it’s always preceded by a data-type
name. When declaring a reference, the & is part of the type. In an expression
like &y, the & is the address operator. The following figure shows a
representation of memory after the preceding assignment. The “pointing
relationship” is indicated by drawing an arrow from the box that represents
the pointer yPtr in memory to the box that represents the variable y in
memory.
The following figure shows another pointer representation in memory with
integer variable y stored at memory location 600000 and pointer variable
yPtr stored at location 500000. The operand of the address operator must be
an lvalue—the address operator cannot be applied to literals or to
expressions that result in temporary values (like the results of calculations).

● Working with Pointers

Accessing the value pointed to by a pointer

Using yPtr from the previous example, you can actually determine the value
of y, which in this case is 5. Accessing the value stored in a variable by using
a pointer is called indirection because you are indirectly accessing the
variable by means of the pointer. For example, you can use indirection with
the yPtr pointer to access the value in y. Indirection means accessing the
value at the address held by a pointer. The pointer provides an indirect way
to get the value held at that address.

Note:

With a normal variable, the type tells the compiler how much memory is
needed to hold the value. With a pointer, the type does not do this; all
pointers are the same size—it depends on the compiled machine code that
you produce either 32-bit or 64-bit (the default is 32-bit) you can choose this
setting during the compilation process and it depends on the specific
compiler you use, if you use 32-bit then all pointers will be 4-bytes in size
and if you use 64-bit then all pointers will be 8-bytes in size. The type tells
the compiler how much memory is needed for the object at the address, that
the pointer holds! In the declaration

unsigned short int * yPtr = 0; // make a pointer

yPtr is declared to be a pointer to an unsigned short integer. This tells the


compiler that the pointer (which needs four bytes to hold an address) will
hold the address of an object of type unsigned short int, which itself requires
two bytes

Dereferencing pointers

The indirection operator (*) is also called the dereference operator. When a
pointer is dereferenced, the value at the address stored by the pointer is
retrieved.

Normal variables provide direct access to their own values. If you create a
new variable of type unsigned short int called yourAge, and you want to
assign the value in howOld to that new variable, you write:

unsigned short int yourAge;


yourAge = howOld;

A pointer provides indirect access to the value of the variable whose address
it stores. To assign the value in howOld to the new variable yourAge by way
of the pointer pAge, you write

unsigned short int yourAge;


yourAge = *pAge;

The indirection operator (*) in front of the pointer variable pAge means “the
value stored at.” This assignment says, “Take the value stored at the address
in pAge and assign it to yourAge.” If you didn’t include the indirection
operator:
yourAge = pAge; // bad!!

you would be attempting to assign the value in pAge, a memory address, to


YourAge. Your compiler would most likely give you a warning that you are
making a mistake.

Different Uses of the Asterisk:

The asterisk (*) is used in two distinct ways with pointers: as part of the
pointer declaration and also as the dereference operator.

When you declare a pointer, the * is part of the declaration and it follows the
type of the object pointed to. For example,

// make a pointer to an unsigned short


unsigned short * pAge = 0;

When the pointer is dereferenced, the dereference (or indirection) operator


indicates that the value at the memory location stored in the pointer is to be
accessed, rather than the address itself.

// assign 5 to the value at pAge


*pAge = 5;

Also note that this same character (*) is used as the multiplication operator.
The compiler knows which operator to call based on how you are using it
(context).

Pointer arithmetic

This section describes the operators that can have pointers as operands and
how these operators are used with pointers. C++ enables pointer
arithmetic—a few arithmetic operations may be performed on pointers.
Pointer arithmetic is appropriate only for pointers that point to built-in array
elements.

A pointer may be incremented (++) or decremented (--), an integer may be


added to a pointer (+ or +=) or subtracted from a pointer (- or -=), or one
pointer may be subtracted from another of the same type—this particular
operation is appropriate only for two pointers that point to elements of the
same built-in array.

Assume that int v[5] has been declared and that its first element is at
memory location 3000. Assume that pointer vPtr has been initialized to point
to v[0] (i.e., the value of vPtr is 3000). The following figure illustrates this
situation for a machine with four-byte integers. Variable vPtr can be
initialized to point to v with either of the following statements (because a
built-in array’s name implicitly converts to the address of its zeroth
element):

int* vPtr = v;
int* vPtr = &v[0];

Adding Integers to and Subtracting Integers from Pointers:

In conventional arithmetic, the addition 3000 + 2 yields the value 3002. This
is normally not the case with pointer arithmetic. When an integer is added to,
or subtracted from, a pointer, the pointer is not simply incremented or
decremented by that integer, but by that integer times the size of the memory
object to which the pointer refers. The number of bytes depends on the
memory object’s data type. For example, the statement:

vPtr += 2;
would produce 3008 (from the calculation 3000 + 2 * 4), assuming that an
int is stored in four bytes of memory. In the built-in array v, vPtr would now
point to v[2]. If an integer is stored in eight bytes of memory, then the
preceding calculation would result in memory location 3016 (3000 + 2 * 8).

If vPtr had been incremented to 3016, which points to v[4], the statement:

vPtr -= 4;

would set vPtr back to 3000—the beginning of the built-in array. If a pointer
is being incremented or decremented by one, the increment (++) and
decrement (--) operators can be used. Each of the statements:

++vPtr;
vPtr++;

increments the pointer to point to the built-in array’s next element. Each of
the statements:

--vPtr;
vPtr--;

decrements the pointer to point to the built-in array’s previous element.


Subtracting Pointers:

Pointer variables pointing to the same built-in array may be subtracted from
one another. For example, if vPtr contains the address 3000 and v2Ptr
contains the address 3008, the statement:

x = v2Ptr - vPtr;

would assign to x the number of built-in array elements from vPtr to


v2Ptr—in this case, 2. Pointer arithmetic is meaningful only on a pointer that
points to a built-in array. We cannot assume that two variables of the same
type are stored contiguously in memory unless they’re adjacent elements of
a built-in array.

● Pointer Operations and Manipulations


Swapping values using pointers

In this section, we will learn how to swap or exchange two numbers using
pointers in C++. Pointers are used to keep the memory address of variables.
We will write one function that will take two pointers of two integer
variables and swap these integer variables by using the pointers.

Passing pointers to a function is a better way to exchange or change any


value because we are passing the memory address of a variable. If we
change anything that is stored in that memory address, we don’t have to
assign any other value to the variable, it will be changed immediately.

Algorithm to use in the program:

We will use the following algorithm:

1. Take the numbers as inputs from the user. Store these numbers in two
different integer variables.

2. Pass the pointers of these variables to a different function.


3. Inside the function, exchange the content of these variables using a
third variable. We need to follow the following steps to swap two
numbers:

a. Assign the value of the second variable to the third variable.

b. Assign the value of the first variable to the second variable.

c. Assign the value of the third variable to the first variable.

4. With these three steps, the values of the variables will be swapped.

5. Once done, print the numbers again to the user.

Below is the complete C++ program that swaps two user input numbers
using pointers:

#include <iostream>
using namespace std;

void swap(int *first, int *second) {


int temp;

temp = *first;
*first = *second;
*second = temp;
}

int main() {
int a, b;

cout << "Enter the numbers : " << endl;


cin >> a;
cin >> b;

cout << "Before swap, a = " << a << ", b = " << b << endl;
swap(&a, &b);
cout << "After swap, a = " << a << ", b = " << b << endl;
}
Here,

1. a and b are two integer variables.

2. We are taking the numbers as inputs from the user and these values
are assigned to the variables a and b respectively.

3. The method swap is used to swap two numbers using pointers. It takes
two integer pointers as the parameters and swaps the values stored in
the addresses pointed by these pointers.

a. The variable temp is used as a temporary variable.

b. It first assigns the value pointed by the variable first to the temp
variable.

c. Then it assigns the value pointed by the second variable to the


first variable.

d. Finally, it assigns the value pointed by the temp variable to the


second variable. With this step, both numbers are swapped.

4. This program is printing the values of the integers before and after the
numbers are swapped.

Returning pointers from functions

C++ allows passing pointers to the function as well as returning a pointer


from a function. This can be achieved by declaring the return type of the
function as a pointer. See the syntax below:

pointer_return_type function_name(parameters) {
statements;
}

//for example
int * MyFunction() {
statements;
}
Please note that, it is not a good idea to return the address of a local variable
outside the function as it goes out of scope after function returns. However,
this feature is useful if the local variable is defined as a static variable. A
static variable does not go out of scope even after the function returns and
preserves its values after each function call.

Example:

In the example below, a static variable is created inside the function and the
function returns the address of that variable.

#include <iostream>
using namespace std;

int * MyFunction() {
static int count = 0;
count++;
return &count;
}

int main () {
int * p;
for(int i = 1; i <= 3; i++) {
p = MyFunction();
cout << "Function returns: " << p << "\n";
cout << "Value of count: " << *p << "\n";
}
return 0;
}

The output of the above code will be:

Function returns: x555944f53158


Value of count: 1
Function returns: x555944f53158
Value of count: 2
Function returns: x555944f53158
Value of count: 3
Example:

Consider one more example, where an array of 5 random variables is created


inside a function and returns the address of first array element (remember
that the array name decays to a pointer to the first element in the array).

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

int * MyFunction() {
static int rand_num[5];

//initialize random seed


srand (time(NULL));

//generating 5 random number between 1 and 100


for(int i = 0; i < 5; i++) {
rand_num[i] = rand() % 100 + 1;
}

return rand_num;
}

int main () {
int * p;

p = MyFunction();
for(int i = 0; i < 5; i++) {
cout << "(p + "<<i<<") = " << p+i << "\n";
cout << "*(p + "<<i<<") = " << *(p+i) << "\n";
}
return 0;
}
The output of the above code will be:

(p + 0) = 0x55beb3d2c170
*(p + 0) = 16
(p + 1) = 0x55beb3d2c174
*(p + 1) = 98
(p + 2) = 0x55beb3d2c178
*(p + 2) = 34
(p + 3) = 0x55beb3d2c17c
*(p + 3) = 87
(p + 4) = 0x55beb3d2c180
*(p + 4) = 56
2. Iterators

● What is an Iterator?
As we have observed, data structures have their unique ways of storing data,
so when applying algorithms to a particular data structure, it's essential to
consider how the data is stored inside the container and its ordering.
Unfortunately, attempting to implement an algorithm that works for all
containers can be frustrating. It requires adjusting the implementation for
each container, which can be a waste of time and energy, and it goes against
the concepts of reusability and extendibility.

To address this issue, C++ developers introduced iterators. They realized


that all algorithms only need to access and iterate through data, so they
designed an interface for each container that provides access to the data
inside the container. The interface hides the container, so algorithms don't
need to work with it. Instead, they work with an iterator, which enables them
to access, manipulate, and iterate through the elements inside the container.

By using iterators, developers can save significant effort. When developing a


new container, all they need to do is define an iterator for it. Since the
iterator subscribes to all other iterators in the interface, all algorithms can be
applied to this container without the need to re-implement or refactor them.
Similarly, when developing a new algorithm, all that is required is for the
algorithm to work with the interface of the iterator. It won't need to know
what container it's working with. Therefore, iterators act as a mediator that
allows algorithms to work with all containers and vice versa.

● Types of Iterators
Iterators are divided into several types:

Forward Iterator

iterator = container.begin();

iterator++; // Go to next
element

iterator--; // Go to previous
element
Reverse Iterator

iterator = container.rbegin();

iterator++; // Go to previous
element

iterator--; // Go to next
element

Random access (iterator):

The most powerful iterator that allows access to the elements at arbitrary
positions in constant time.

To understand how the random-access works let's imagine the memory as


the following figure:

memory(bytes) arr[0] arr[1] arr[2] arr[3] arr[4]

10 14 18 22 26

arr[0] => 10 + 0 * 4 = 10
arr[1] => 10 + 1 * 4 = 14
arr[2] => 10 + 2 * 4 = 18
int arr[5]; arr[3] => 10 + 3 * 4 = 22
arr[4] => 10 + 4 * 4 = 26

Consider that each cell represents a byte, and the gray cells have already
been allocated. We wish to allocate an array with 5 elements, which will
require 20 free bytes (5 elements x 4 bytes per element). It is important that
these 20 free bytes are consecutive, with no allocated bytes in between them.
This is beneficial because if we have the address of the first byte, we can
directly access any other element in the array.
3. Dynamic Memory
● Introduction to dynamic memory allocation
In the section, five areas of memory are mentioned:

○ Global namespace
○ The free store
○ Registers
○ Code space
○ The stack

Local variables are on the stack, along with function parameters. Code is in
code space, of course, and global variables are in the global namespace. The
registers are used for internal housekeeping functions, such as keeping track
of the top of the stack and the instruction pointer. Just about all of the
remaining memory is given to the free store, which is often referred to as the
heap.

Local variables don’t persist; when a function returns, its local variables are
destroyed. This is good because it means the programmer doesn’t have to do
anything to manage this memory space, but is bad because it makes it hard
for functions to create objects for use by other objects or functions without
generating the extra overhead of copying objects from stack to return value
to destination object in the caller. Global variables solve that problem at the
cost of providing unrestricted access to those variables throughout the
program, which leads to the creation of code that is difficult to understand
and maintain. Putting data in the free store can solve both of these problems
if that data is managed properly.

You can think of the free store as a massive section of memory in which
thousands of sequentially numbered cubbyholes lie waiting for your data.
You can’t label these cubbyholes, though, as you can with the stack. You
must ask for the address of the cubbyhole that you reserve and then stash
that address away in a pointer.
One way to think about this is with an analogy: A friend gives you the 800
number for Acme Mail Order. You go home and program your telephone
with that number, and then you throw away the piece of paper with the
number on it. If you push the button, a telephone rings somewhere, and
Acme Mail Order answers. You don’t remember the number, and you don’t
know where the other telephone is located, but the button gives you access
to Acme Mail Order. Acme Mail Order is your data on the free store. You
don’t know where it is, but you know how to get to it. You access it by using
its address—in this case, the telephone number. You don’t have to know that
number; you just have to put it into a pointer (the button). The pointer gives
you access to your data without bothering you with the details.

The stack is cleaned automatically when a function returns. All the local
variables go out of scope, and they are removed from the stack. The free
store is not cleaned until your program ends, and it is your responsibility to
free any memory that you’ve reserved when you are done with it. This is
where destructors are absolutely critical because they provide a place where
any heap memory allocated in a class can be reclaimed.

The advantage of the free store is that the memory you reserve remains
available until you explicitly state you are done with it by freeing it. If you
reserve memory on the free store while in a function, the memory is still
available when the function returns.

● Allocating memory using 'new' and 'delete' operators


You allocate memory on the free store in C++ by using the new keyword.
new is followed by the type of the object that you want to allocate so that the
compiler knows how much memory is required. Therefore, new unsigned
short int allocates two bytes in the free store, and new long allocates four,
assuming your system uses a two-byte unsigned short int and a four-byte
long.

The return value from new is a memory address. Because you now know
that memory addresses are stored in pointers, it should be no surprise to you
that the return value from new should be assigned to a pointer. To create an
unsigned short on the free store, you might write

unsigned short int * pPointer;


pPointer = new unsigned short int;

You can, of course, do this all on one line by initializing the pointer at the
same time you declare it:

unsigned short int * pPointer = new unsigned short int;

In either case, pPointer now points to an unsigned short int on the free store.
You can use this like any other pointer to a variable and assign a value into
that area of memory by writing:

*pPointer = 72;

This means “Put 72 at the value in pPointer,” or “Assign the value 72 to the
area on the free store to which pPointer points.”

Note:

If new cannot create memory on the free store (memory is, after all, a
limited resource), it throws an exception, However, you can use the nothrow
object when you are requesting memory using the new keyword to return a
null pointer instead of throwing an exception if new cannot find enough
memory on the free store.

The delete Keyword

When you are finished with an area of memory, you must free it back to the
system. You do this by calling delete on the pointer. delete returns the
memory to the free store.

It is critical to remember that memory allocated with new is not freed


automatically. If a pointer variable is pointing to memory on the free store
and the pointer goes out of scope, the memory is not automatically returned
to the free store. Rather, it is considered allocated and because the pointer is
no longer available, you can no longer access the memory. This happens, for
instance, if a pointer is a local variable. When the function in which that
pointer is declared returns, that pointer goes out of scope and is lost. The
memory allocated with new is not freed—instead, it becomes unavailable.

This situation is called a memory leak. It’s called a memory leak because
that memory can’t be recovered until the program ends. It is as though the
memory has leaked out of your computer.

To prevent memory leaks, you should restore any memory you allocate back
to the free store. You do this by using the keyword delete. For example:

delete pPointer;

When you delete the pointer, you are really freeing up the memory whose
address is stored in the pointer. You are saying, “Return to the free store the
memory that this pointer points to.” The pointer is still a pointer, and it can
be reassigned.

Most commonly, you will allocate items from the heap in a constructor, and
deallocate them in the destructor. In other cases, you will initialize pointers
in the constructor, allocate memory for those pointers as the object is used,
and, in the destructor, test the pointers for null and deallocate them if they
are not null.

Note:

When you call delete on a pointer, the memory it points to is freed. Calling
delete on that pointer again crashes your program! When you delete a
pointer, set it to nullptr. Calling delete on a null pointer is guaranteed to be
safe.
● Memory leaks and how to avoid them
Memory leaks are one of the most serious issues and complaints about
pointers. You have seen one way that memory leaks can occur. Another way
you might inadvertently create a memory leak is by reassigning your pointer
before deleting the memory to which it points.

Consider this code fragment:

1: unsigned short int * pPointer = new unsigned short int;


2: *pPointer = 72;
3: pPointer = new unsigned short int;
4: *pPointer = 84;

Line 1 creates pPointer and assigns it the address of an area on the free store.
Line 2 stores the value 72 in that area of memory. Line 3 reassigns pPointer
to another area of memory. Line 4 places the value 84 in that area. The
original area—in which the value 72 is now held—is unavailable because
the pointer to that area of memory has been reassigned. No way exists to
access that original area of memory, nor is there any way to free it before the
program ends.

The code should have been written like this:

1: unsigned short int * pPointer = new unsigned short int;


2: *pPointer = 72;
3: delete pPointer;
4: pPointer = new unsigned short int;
5: *pPointer = 84;

Now, the memory originally pointed to by pPointer is deleted, and thus


freed, on line 3.

Note:
For every time in your program that you call new, there should be a call to
delete. It is important to keep track of which pointer owns an area of
memory and to ensure that the memory is returned to the free store when
you are done with it.

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy