/* *** CRITICAL NOTES ON SMART POINTERS AND HEAP MEMORY in C++ *** */ #include #include #include #include using namespace std; /* A smart pointer is a class/struct designed to encapsulate the management of heap memory. I will explain how to use them first, then describe what they are underneath. Let's start with reviewing regular (raw) pointers and how they work: int x; int *px = &x; // px holds the memory address of x ("pointer to x") int *mx = new int(1); // new allocates an int on the heap, returns a pointer int *qx = px; // multiple pointers can point to the same location. *qx += 1; // de-references qx and assign it a value. cout << *qx << *px << x; // prints 222, *qx dereferences qx, refers to x mx = new int(2); // pointers can change where they're pointing to There are no restrictions to where in memory (heap or stack) that raw pointers can point to. There are also no restrictions to how many pointers are pointing to the same data, and there are no restrictions to changing a pointer to point somewhere else. These lack of restrictions mean that we can get a variety of memory errors, including: o Returning a dangling pointer to a value that will be popped from the stack o Reassigning a pointer to heap to point somewhere else (memory leak) o Deleting the wrong data, or deleting too early o Deleting the same memory twice o Deleting memory not on the heap. The following function contains many of these errors, yet IT COMPILES: only warnings for a few very obvious errors can be expected. */ int* verybad() { int x = 1; // 1 allocated on stack int *y = new int(2); // 2 allocated on heap y = &x; // reassigns y to point to x on stack (memory leak) delete y; // deletes stack-allocated data, x will be deleted twice return y; // returns dangling pointer } // function with lots of memory errors /* When software consist of thousands of lines of code, it will be difficult even for experienced programmers to keep track of all raw pointers in the code and make sure that these errors are avoided. This is a main weakness of programming in traditional C/C++. ******** std:unique_ptr and std:make_unique ********* New solutions to memory management in C++ were introduced in 2011 and 2014. Once you #include, you will have access to a class/struct called std::unique_ptr and a special operation/function std::make_unique. Unique pointers (unique_ptr) replace raw pointers and make_unique replaces the 'new' operation. unique_ptr y = make_unique(2); // replaces int* y= new int(2); cout << *y; // prints 2, the * operator also dereferences unique pointers Unique pointers are demonstrated in the following function, which allocates two arrays of integers on the heap but contains no memory leaks or other memory errors: */ void notbad() { unique_ptr A = make_unique(10); //allocates int[10] on heap for(int i=0;i<10;i++) A[i] = 10*i; // use A like a "normal" pointer unique_ptr B = make_unique(20); for(int i=0;i<20;i++) B[i] = 20-i; //A = B; not allowed as it destroys uniqueness (won't compile) A = move(B); // transfer of ownership }//leaktest /* Please note that make_unique(10) creates an integer with value 10 while make_unique(10) creates an array of 10 integers. A unique_ptr is a "smart" pointer because, unlike raw pointers, it tries to respect the rules of *lifetime* and *ownership* ******** LIFETIME AND OWNERSHIP ********* The lifetime of a piece of data is how long it's guaranteed to stay in memory. If the data is allocated globally, then its lifetime is 'static', meaning that it's valid for the entire duration of the program. The lifetime of something allocated on the stack ends when it's popped off from the stack when the function that allocated it returns. The lifetime of something allocated on the heap ends when delete is called on a pointer pointing to it. Most memory errors come from a *mismatch of lifetimes* between pointers and the data that they point to. If the pointer outlives the data, it becomes a dangling or invalid pointer. If the data outlives all of its pointers, that's a memory leak. To equalize the lifetimes of pointers and what they point to, we should first make pointers unqiue: allow exactly one pointer to same heap location to be alive. This means that the heap memory can only be deallocated by its unique pointer. It also means that when the lifetime of the pointer ends, we can also end (deallocate) the lifetime of the data that it points to at the same time. A std::unique_ptr *owns* the data that it points to. The ownership is exclusive. In contrast, a raw pointer does not "own" the data that it points to because it's not necessarily unique. A unique_ptr is *uniquely responsible for managing the data that it points to.* A unique_ptr is intended to only point to heap-allocated data, never to global or stack-allocated data, because such data do not require special management. A unique_ptr is itself a struct that's allocated on the stack or heap (usually stack). A piece of data owned by a unique_ptr cannot outlive the unique_ptr: once the unique_ptr is deallocated, the data it owns is also deallocated. A unique_ptr should never be created on the heap with 'new', only with std::make_unique: 'new' returns a raw pointer while make_unique returns a unique_ptr. NEVER CALL delete on a unique_ptr: delete can only be called on raw pointers. ******** TRANSFER OF OWNERSHIP ********* You cannot assign a unique_ptr to another unique_ptr as that would violate uniqueness by making a copy of the same pointer: unique_ptr y = make_unique(2); unique_ptr z = make_unique(3); //x = y; // won't compile x = move(y); // TRANSFERS OWNERSHIP Instead of x=y, which won't compile, you must call x=move(y); During such a "move assignment", the following occurs: 1. the data that x previous owned is deallocated: the lifetime of that data ends. 2. x is now pointing to the data that y pointed to. 3. y is set to nullptr: y now owns nothing This sequence of events means that ownership is transferred from y to x: the data cannot be owned by two unique pointers, and a unique pointer can only own one location at a time. After such an assignment, the unique pointer that was "moved" can no longer be referred to, as it is now equivalent to a nullptr: you will get a runtime memory error (segmentation fault and/or coredump) if you continue to use y after the move. Transfer of ownership also occurs when a unique_ptr is passed to a function and when a unqiue_ptr is returned from a function: */ unique_ptr u1(unique_ptr p) { // do stuff with p... *p += 1; unique_ptr q = make_unique(3); return q; } void q1() { unique_ptr y = make_unique(2); unique_ptr z = u1(move(y)); cout << *z; // should print 3 cout << *y; // runtime memory error: y doesn't own anything! (lifetime ended). } /* When move(y) is passed to u1, the local unique pointer p, which is allocated on the u1 function's runtime stack frame, now owns the data previously owned by y. When u1 returns, p is deallocated by being popped from the stack, and at that point, the heap data it owns is also deallocated. We cannot use it again from the calling function (q1). However, a function can also transfer back a locally owned memory location by returning a unique pointer. Note that "move" was necessary when passing a unique pointer to a function, though not when assigning to a unique pointer returned from a function. This is because a value returned from a function is an "R-value", meaning that it exists temporarily and will die if not assigned to an "L-value", which is something that can appear on the left-hand side of the "=" operator. This also applies to the unique_ptr returned by make_unique. Essentially, 'move' casts an L-value into an R-value and triggers the "move assignment operator" or the "move constructor". The detailed mechanics of how this works are demonstrated when we implement our own smart pointer. Here's another example of unique pointers, this time pointing to structs. */ struct point { int x; int y; point(int a, int b) {x=a;y=b;} }; unique_ptr notbadatall() { unique_ptr p1 = make_unique(3,5); cout << p1->x << "," << p1->y << endl; // prints 3,5 p1 = make_unique(8,2); cout << p1->x << "," << p1->y << endl; // and no leak unique_ptr p2; p2 = move(p1); // transfer of ownership //cout << "p1->: " << p1->x << endl; // will crash, p1 now nullptr if (p2) cout << p2->x << "," << p2->y << endl; // overloads bool() return make_unique(99); // returns unique_ptr to 99 on heap }//notbadatall /* In order to take full advantage of smart pointers, you should **not mix them with raw pointers**. */ unique_ptr stillbad() { int x; int A[5]; unique_ptr p1(&x); // creating a unique_ptr from a raw pointer unique_ptr p2(&x); // duplicate pointer to x ("uniquely not unique") unique_ptr p3(A); // raw pointer not pointing to heap! int* px = p2.get(); // gets the raw pointer inside the unique pointer unique_ptr* pu = &p3; // LOL! way to make things worse! return p3; // uniquely dangling .. } /* The problem with the above function is that unique pointers can lose all their effectiveness when they're mixed with raw pointers. The expressions '&x' and 'A' are both raw pointers. The "unique" pointers p1 and p2 are pointing to the same x, and they're pointing to stack allocated data, not heap-allocated data. If you return such unique pointer, you'll still get a dangling pointer. You can even force a unique pointer to "give it up" with the call to .get(). Mixing raw pointers with smart pointers can entirely defeat their purpose and make matters even worse than before they were invented. **** NEVER USE RAW POINTERS AGAIN! **** Thus, in order for unique_ptr to be most effective, you should never use them in conjunction with raw pointers. ***** unique_ptr must be used in conjunction with make_unique ***** ALWAYS CREATE UNIQUE POINTERS WITH make_unique, not by giving its constructor a raw pointer like in the 'stillbad' function. make_unique always allocates memory on the heap, not the stack, so you can't force a unique_ptr to point to stack memory, which it cannot "own". ***The recommended way to use heap-allocated memory in modern C++ is to create a unique_ptr with make_unique.*** But since C++ doesn't enforce this rule, you will just have to observe it yourself. EASIER SAID THAN DONE ... Observing the protocol of using unique_ptr/make_unique exclusively can be rather restrictive at times, and changes significantly your approach to writing programs. Sometimes, you may need to call built-in functions such as 'memset' and 'memcpy' from old C, which require raw pointers to be passed to them. Using these functions will require you to "get" the raw pointer from inside the unique_ptr, which is why C++ provides such an operations. C++ is an "expert" programming language, which means it never stops you from doing anything. It's entirely up to you to be as careful as possible when mixing unique pointers with raw pointers, and to minimize and localize such mixtures. If you're a beginner, it's best to just observe the strict protocal. Don't use raw pointers yourself. Call built-in functions that "experts" have defined for you instead: */ template void memcpy_unique(unique_ptr& dst,unique_ptr& src,int count) noexcept { memcpy(dst.get(),src.get(),count*sizeof(TY)); }//manually define memcpy_unique /* This function, defined by me, uses an l-value references in order to be efficient. Unlike another pointer, a reference *does not count* against uniqueness. Ownership is *not* transfered when you assign (or pass) a unique_ptr to a reference. If you have two unique_ptr p and q, that are pointing to contiguous data, you can call 'memcpy_unique(p,q,count)' without using any raw pointers yourself, because that usage was isolated in the function. However, be aware that references can also cause problems: */ unique_ptr& bad_reference() { unique_ptr u1 = make_unique(1); return u1; } /* Why is the function bad? Because it's a dangling reference: neither the unique_ptr struct nor the data it pointed to are alive after the return. But at least you will get a compiler warning here. Sometimes, not having multiple pointers pointing to the same data can be too restricting. Consider the following struct for a linked-list of integers and a function to print every value in the list */ struct node { int item; node* next; }; void printnodes(node* n) { node* current = n; while (current!=nullptr) { cout << current->item << " "; current = current->next; } }//printnodes /* The printnodes function relies on having ANOTHER POINTER, 'current', which is set to every node in the list. Had we created this list using unique pointers instead of raw pointers, then having this other pointer would violate uniqueness. */ struct unode { int item; unique_ptr next; }; void printunodes(unique_ptr& n) { unique_ptr current = move(n); while (current) { // this means current is not nullptr cout << current->item << endl; current = move(current->next); } } /* The printunodes function will indeed print the list, but ONLY once. The list would be *destroyed by the printing*: onwership of each node would be transferred one by one to current and are destroyed when current is reassigned, and current itself is destroyed when the function returns. Using a reference here is not an option because a reference, once set, cannot be changed to reference something else: it is always just an alias of what it's originally defined to reference. The best solution here? Don't create a linked list using unique pointers. Linked lists shouldn't be used much in modern programming anyway. Use std::vector instead. If you need a data structure that contains unique pointers to other data, use a std::vector and the built-in "for-each-reference" loop */ void u2() { vector> V; V.push_back(make_unique(2)); V.push_back(make_unique(4)); for(unique_ptr& x:V) cout << *x << endl; } /* This form of for-each loop creates a separate reference to each unique pointer in the vector, without violating uniqueness. The reference x is updated to refer to successive elements of the vector. MEMORY LEAKS ARE STILL POSSIBLE. Unfortunately, even if we used exclusively unique_ptr/make_unique, memory leaks are still sometimes possible. We can illustrate this using the unode struct defined above. */ void u3() { unique_ptr first = make_unique(); unique_ptr second = make_unique(); unique_ptr third = make_unique(); third->item = 3; third->next = nullptr; second->item = 2; second->next = move(third); first->item = 1; first->next = move(second); first->next->next = move(first); // this will still create a memory leak }//u3 /* The last line of function u3 creates a memory leak despite the fact that this function uses exclusively unique_ptr/make_unique. There are no raw pointers (except for the nullptr, which is not really "raw" either). The data owned by second and third were moved into the list pointed to by first. Without the last line of the function there'd be no leak: when first is popped from the stack it will also destroy its 'next', which in turn destroys its 'next'. The last instruction attempts to create a circular reference to first: but the 'move' will destroy first as it does this, which means that there will be nobody left who can manage the nodes with 2 and 3. The origin of this problem stems from the fact that C++ does not control how something is changed (mutated). You can either designate something as 'const', which means it can't be changed at all, or allow any changes to be made at any time. There's no finer way to control how things change. As a general programming principle, ** we should not read and change something at the same time ** Let's give a simple example of this principal: */ void bad_forloop() { vector V {2,4,6,8}; // make and initialize a vector for(int x:V) V.push_back(x); // add it to the vector again! for(auto x:V) { cout << x<< " "; } cout << " ... (bad_forloop output)\n"; } /* The problem with this loop is that we are iterating (reading) over the vector V while changing it at the same time. In some languages like Python, such a loop will run until you're out of memory. In other lanuages such as C# and Java, it will throw a runtime exception that the iterator cannot continue because the collection's been modified. C++ will compile and run this code, but look for its output when you run this program ... This is the painful side of "zero overhead abstraction", at least in C++. The 'u3' function is a bit more subtle, but the root of the problem is the same. We need read access through `first` in order to get to `first->next->next`, but before this is complete, `first` is destroyed, as it is moved. Mutation includes being moved as well as being reassigned. When we mutate (change) a piece of data, there shouldn't be anything that is still dependent on the original form of the data. This "rule of mutability" is just as important as the ownership and lifetime principles, but it is not recognized by unique pointers. Reading the data and changing it should happen in different phases. There's no way that the C++ compiler can check if this is being violated at compile time, and the cost of checking it at runtime is too high to be consistent with the principle of zero-overhead abstraction. Unfortunately, unique_ptr and make_unique do not guarantee complete memory safety. However, using them exclusively will still make it much easier to write programs that are free of memory errors. You won't have to worry about pointers pointing to the stack and you won't have to worry about calling delete exactly once at exactly the right time. And if you remember to respect the principle of mutability described above, you shouldn't have to worry about deleting something too early. */ /* ******** std:shared_ptr and std:make_shared ********* Some algorithms require that there are multiple pointers to the same memory location. Sometimes this is done for efficiency: it's much less expensive to copy the pointer than to copy the entire structure. For example, we can have a data structure where all instances shared a common core of values that are immutable: this data should be shared instead of being replicated. Modern C++ provides another kind of smart pointer for such situations called 'shared_ptr', with accompanying operation 'make_shared'. A shared pointer is sometimes called a **reference counter**, although they're not C++ references. Unlike a unique pointer, multiple shared pointers can point to the same data. However, shared pointers also maintain a **shared counter** that counts how many pointers are currently pointing to the same data. Each time a copy (share) of the pointer is made, the counter is incremented. Each time a shared_ptr is reassigned to something else, or goes out of scope (popped) the counter is decremented. The last live pointer to the data is responsible for deallocating the data. That is, only when the counter goes to zero will be data be deallocated. You can check the current value of the reference counter with .use_count(): */ void s1() { shared_ptr origin = make_shared(0,0); shared_ptr p1 = origin; // no need to call move shared_ptr p2 = origin; shared_ptr p3 = origin; shared_ptr p4 = make_shared(1,2); cout << "current reference counter: " << origin.use_count() << endl; //3 p1 = p4; // re-assigns p1 cout << "counter after reassignment:" << origin.use_count() << endl; p2 = p3; // this has no effect on the reference counter p2 = move(p3); // this decreases the counter, p3 nolonger points to origin cout << "counter after move assignment:" << p2.use_count() << endl; }//s1 /* When a shared pointer is assigned to another shared pointer, calling "move" is optional, but it still has an effect. Once move is called on a shared_ptr, the pointer is no longer pointing to the data it was pointing to (it's now nullptr): thus the reference counter is decreased by move. But making an assignment without move creates another pointer and increments the counter. Note that the line 'p2 = p3' has no cumulative effect on the counter, although technically it was decremented and then incremented. If a shared pointer is passed to a function, the counter is also incremented. &-references do not affect the count. shared_ptr and unique_ptr cannot mix: moving one into the other is not allowed (compiler error). Using shared_ptr/make_shared is easier than unique_ptr/make_unqiue. You won't accidently destroy the data by moving it. You normally won't have to call move. But it's very important that you understand that there are two major problems with shared pointers: 1. They incur significantly greater runtime overhead than unique_ptr 2. They don't work when structures are pointing to each other. Zero-overhead abstraction is not just a principle of the programming language; it must be followed by programmers. The programming language allows us to avoid costly abstractions when they're unnecessary, but we must avoid them. Using shared pointers throughout our program might make programming a little easier but it violates this principle if they're not actually needed. If you have two structures, each with a pointer to the other, but there's nothing else pointing to either of these structures, then their reference counters will never go to zero and they never get deallocated, resulting in a memory leak. In such situations, you have to also use std::weak_ptr. A weak_ptr is a "downgraded" shared_ptr. Having a weak_ptr to a shared structure does not affect the reference counter. If you have structures with pointers pointing to eachother, one of those pointers must be a weak_ptr. In complex situations, when your memory allocations is an arbitrary graph, figuring out which pointers to make weak could be tricky: not enough will result in memory leaks while too many may result in things getting deleted too early. It's possible to assign a weak_ptr to a shared_ptr (there's no make_weak). It's possible to upgrade a weak_ptr to a shared_ptr with the function .lock() Here is an example of a doubly-linked list, where each "node" in the list has a pointer to the 'next' node, as well as a pointer to the previous node. Each node in the middle of such a list will have a pointer pointing to it from both directions. But if nothing points to the head of this list, then none of the nodes will be deallocated. This situation calls for one of the two pointers to be "weak". */ struct dnode { // doubly linked list using shared_ptr and weak_ptr int item; shared_ptr next; // next node weak_ptr previous; // previous node dnode(int x, shared_ptr& next, shared_ptr& rdc) { item=x; next=next; previous=rdc; // assigning shared_ptr to weak_ptr is ok } // function to get the first element of the list using the back pointer int first() { weak_ptr w = previous; shared_ptr upgraded = w.lock(); // upgrade to shared_ptr weak_ptr downgraded = upgraded; // downgrade to weak_ptr while (w.lock()->previous.lock()) { w = w.lock()->previous; } return w.lock()->item; //.lock() converts weak_ptr to a shared_ptr } // must use .lock() before dereferening weak_ptr }; // dnode /* When a weak_ptr is assigned to a shared_ptr, the reference counter of the shared data is *not* incremented. Before we can use a weak_ptr to access data we have call .lock() on it to upgrade it to a shared_ptr. In this example, the calls to w.lock() create shared_ptr objects local to the function 'first'. When this function returns, all the tempoary shared pointers will be deallocated, decreasing the reference counter back to what it was before the function call. So in principle, we should: 1. use unique_ptr if possible 2. use shared_ptr only when they're really necessary 3. use weak_ptr to prevent cyclic shared pointers. The reference counter approach to memory management is the default for some programming languages, including Apple's Swift language. Other languages have active garbage collectors. These languages do not respect the principle of zero-overhead abstraction: we have to always pay the overhead of their way of managing memory whether we need to or not. */ /* L-Value and R-Value References References become especially important with smart pointers. A reference does not count against the uniqueness of unique_ptr, nor does it increase the pointer count of shared_ptr. A reference is not a value while a pointer is. But there are two kinds of references. Recall that an "l-value" is whatever can appear on the left-hand side of the assignment operator (=). An r-value can appear on the right-hand side of =. R-values that are not also l-values include constants and "temporaries", which are values returned from functions before they're assigned to l-values. A type such as `int&` is an "l-value reference" while `int&&` is an "r-value reference". int y = 2; int& x = y; // allowed because y is an l-value int& x = 2; // not allowed because 2 is an r-value, not an l-value int&& x = 2; // allowed because this x is an r-value reference. int&& x = y; // not allowed because y is an l-value (not strictly r-value) int&& x = move(y); // allowed: move invokes the "move assignment" operator R-value reference are principally used to implement "move semantics". The "move" operation actually doesn't do much but to trigger the "move assignment", as opposed to the "copy assignment" operation. It's the move assignment operations that performs the transfer of ownership. The move assignment operation is something that can be manually defined for a class. We will do that to define our own version of unique pointers. DEFINING OUR OWN SMART POINTER To help you better understand smart pointers and how to effectively use them, we can define our own version of unique_ptr. While doing so, we'll also provide it with an additional feature: the ability to temporarily lock it to prevent it from being moved. We will have to call the .lock() (not to be confused with .lock on a weak_ptr) function ourselves in the right places. There will also be a increase in runtime cost compared to unique_ptr. The leaktest1 and leaktest1m functions illustrate how they're used. The lock feature is designed to help us prevent memory leaks that can still occur even if we completely avoid raw pointers. */ template class managed_ptr { private: TY* ptr; // internal pointer to something of type TY bool locked; // prevent moving (runtime check, with overhead***) public: managed_ptr(TY* p) {ptr=p; locked=0;} managed_ptr() { ptr=nullptr; locked=0; } ~managed_ptr() { if (ptr!=nullptr) delete ptr; } void lock() {locked=1;} void unlock() {locked = 0; } bool islocked() { return locked; } // overload operators so that a managed_ptr looks like a real pointer TY& operator *() { return *ptr; } TY* operator ->() { return ptr; } TY& operator [](int i) { return *(ptr+i); } operator bool() { return ptr==nullptr; } // overload the move assignment operator to TRANSFER OWNERSHIP. void operator=(managed_ptr&& other) { // other is an R-Value Reference if (other.locked) { cerr << "Memory mutation violation on managed_ptr\n"; throw -1; } if (ptr) delete ptr; // prevent memory leaks before assignment ptr = other.ptr; // takes over "ownership" of heap value other.ptr = nullptr; // ownership must be unqiue to prevent wrong deletes }// this is called the "move assignment" operator // traditional copy constructor must accept a 'const' argument: // managed_ptr(const managed_ptr& mptr) {}// defines how to copy // overload the "move constructor" so ownership is transferred when // when passing smartpointer to function, return from function managed_ptr(managed_ptr&& other) { if (other.locked) { cerr << "Memory mutation violation on managed_ptr\n"; throw -1; } if (ptr) delete ptr; // prevent memory leaks before assignment ptr = other.ptr; // takes over "ownership" of heap value other.ptr = nullptr; }// copy constructor, or more accurately, "move" constructor // the move constructor nullifies the copy constructor }; // managed_ptr managed_ptr leaktest1() { managed_ptr A(new int[10]); // managed pointer to heap data for(int i=0;i<10;i++) A[i] = 10*i; managed_ptr B(new int[20]); for(int i=0;i<20;i++) B[i] = 20-i; //A = B; not allowed (won't compile): && means must call move on L-value A = move(B); // move and &&-references where added in C++ 2011 managed_ptr C2(new int[40]); //int* C3 = new int[20]; use available memory return A; }//leaktest1 (no memory leak) // The following function, which uses an advanced C++ template with // "variadic parameter packs", isolates the creation of managed pointers: // it makes sure that they're only allocated on the heap. Essentially // it's the same as std::make_unique template managed_ptr make_managed(Args&&... args) { return managed_ptr(new TY(std::forward(args)...)); // forward retains original l/r-value status of args }// make_managed struct mnode { // linked list using managed_ptr's int item; managed_ptr next; mnode() {next=nullptr; item = {}; } // {} means default value }; void leaktest1m() { managed_ptr m1 = make_managed(2,4); managed_ptr m2 = make_managed(2,4); m1 = move(m2); managed_ptr first = make_managed(); managed_ptr second = make_managed(); managed_ptr third = make_managed(); // if we lose first, we won't be able to access the list, including // deleting it. So we "lock" it to prevent it from being "moved" first.lock(); // not ->lock(), because then it's looking for mnode.lock third->item = 3; third->next = nullptr; second->item = 2; second->next = move(third); first->item = 1; first->next = move(second); // add node to front of the list first.unlock(); managed_ptr mewnode = make_managed(); mewnode-> item = 4; mewnode->next = move(first); // move ok after .unlock first = move(mewnode); // first changes. first.lock(); first->next->next = move(first); // this becomes a runtime error } // (no memory leaks) int main() { managed_ptr C = leaktest1(); for(int i=0;i<20;i++) cout << C[i] << endl; //while (1) leaktest1(); // will there be memory leaks? No //verybad(); //stillbad(); //will core dump notbad(); auto x = notbadatall(); cout << "x is " << *x << endl; //while (1) u3(); // still will leak //while (1) leaktest1m(); // no leaks s1(); bad_forloop(); return 0; }//main