解构和析构——基于C++面向对象编程(英)

S5 Constructor and destructor

#OOP

文章目录

Definition

Constructor

A constructor function is a special member function in a class that:

  1. Has the same name as the class - The constructor must be named identically to the class it belongs to.
  2. Has no return type - Unlike regular functions, constructors don’t specify a return type, not even void.
  3. Is automatically called - When an object of the class is created, the appropriate constructor is automatically invoked.
  4. Initializes object data members - The primary purpose is to initialize the object’s member variables to ensure the object starts in a valid state.
  5. Can be overloaded - A class can have multiple constructors with different parameter lists, allowing objects to be initialized in different ways.
  6. Can accept parameters - Unlike regular member functions, constructors can take parameters to initialize member variables with specific values.
  7. Can have access modifiers - Constructors are typically public, but can also be private or protected for specific design patterns.
  8. Can call other constructors - In C++, one constructor can call another constructor of the same class (known as delegating constructors).
  9. Can have initialization lists - C++ constructors can use initialization lists to initialize members directly rather than through assignment.
  10. Cannot be inherited - Derived classes do not inherit constructors from base classes (although C++11 introduced constructor inheritance with the “using” keyword).
class Student {
public:
    // Default constructor
    Student() {
        name = "Unnamed";
        age = 0;
        score = 0.0;
        cout << "Default constructor called" << endl;
    }
    
    // Constructor with one parameter
    Student(const char* studentName) {
        name = studentName;
        age = 0;
        score = 0.0;
        cout << "Constructor with name parameter called" << endl;
    }
    
    // Constructor with two parameters
    Student(const char* studentName, int studentAge) {
        name = studentName;
        age = studentAge;
        score = 0.0;
        cout << "Constructor with name and age parameters called" << endl;
    }
    
    // Constructor with three parameters
    Student(const char* studentName, int studentAge, float studentScore) {
        name = studentName;
        age = studentAge;
        score = studentScore;
        cout << "Constructor with all parameters called" << endl;
    }
    
    // Method to display student information
    void display() {
        cout << "Name: " << name << ", Age: " << age << ", Score: " << score << endl;
    }
    
private:
    const char* name;  // Student name
    int age;           // Student age
    float score;       // Student score
};

int main() {
    // Creating objects using different constructors
    Student s1;                         // Calls default constructor
    Student s2("Zhang San");            // Calls constructor with one parameter
    Student s3("Li Si", 18);            // Calls constructor with two parameters
    Student s4("Wang Wu", 20, 92.5);    // Calls constructor with three parameters
    
    // Display information for each student
    cout << "Student 1 info: "; s1.display();
    cout << "Student 2 info: "; s2.display();
    cout << "Student 3 info: "; s3.display();
    cout << "Student 4 info: "; s4.display();
    
    return 0;
}
Default constructor called
Constructor with name parameter called
Constructor with name and age parameters called
Constructor with all parameters called
Student 1 info: Name: Unnamed, Age: 0, Score: 0
Student 2 info: Name: Zhang San, Age: 0, Score: 0
Student 3 info: Name: Li Si, Age: 18, Score: 0
Student 4 info: Name: Wang Wu, Age: 20, Score: 92.5

Notes:

  1. If any constructor is defined manually, the system will no longer provide a default parameterless constructor.
    class Student{
    public:
    /*
    	Student(string pName=“”,int i=0)
     	{   cout<<"姓名学时"<<endl;   
         name = pName;
         semesHours=i;
      	}
    	//Error turns to ok.
    */   
      	Student(string pName){ 
    		cout <<"姓名" <<pName <<endl;
        	name = pName;
    	}
     	Student(string pName,int i){
    	   cout<<"姓名学时"<<endl;   
    	   name = pName;
           semesHours=i;
    	}
    	//Student(){};	//Error turns to ok.
    private:
      string name; int semesHours; 
    };
    int main()
    {  Student tt("Tom",60);  Student ss("Jenny"); 
        Student f;      //Error: No suitable constructor function.
       return 0; 
    }
    

Destructor function

A destructor function is a special member function in a class that:

  1. Has the same name as the class preceded by a tilde (~) - For example, if the class is named Rectangle, the destructor is named ~Rectangle.
  2. Has no return type - Like constructors, destructors don’t specify a return type, not even void.
  3. Cannot take parameters - Unlike constructors, destructors cannot be parameterized or overloaded; a class can have only one destructor.
  4. Is automatically called - When an object goes out of scope or is explicitly deleted, its destructor is automatically invoked.
  5. Performs cleanup operations - The primary purpose is to release resources allocated by the object during its lifetime, such as dynamically allocated memory, open files, database connections, etc.
  6. Executes in reverse order of construction - In a hierarchy, the derived class destructor is called first, followed by the base class destructor.
  7. Can be explicitly called - Though rare and generally not recommended, destructors can be explicitly called using the syntax object.~ClassName().
  8. Is crucial for preventing memory leaks - Proper implementation of destructors is essential in resource management, especially when dealing with raw pointers.
  9. Can be virtual - In polymorphic classes, destructors should typically be declared as virtual to ensure proper cleanup of derived class resources when deleted through a base class pointer.
  10. Completes the object’s lifecycle - It represents the final phase in an object’s life, cleaning up what the constructor set up.
class Student{
public:
	Student();
	~Student();
protected:
	char *name;
	int age;
};
Student::Student(){ age=5;Student::~Student()
{ cout<<“Object ends.;}

Student a;
a.Student(); //Error
a.~Student(); //Not recommended

Composition Classes

Definition

  • Composition: A phenomenon where structure variables and objects are embedded as members in another structure or class declaration.
  • Member Objects: The embedded object members (class members).
  • Composition Class: The class that contains these member objects (also called container class).

Creation of Composition Class Objects

// Example of class definitions
class Student {
public:
    Student() {
        cout << "constructing student.\n";
        semesHours = 100;
        gpa = 3.5;
    }
    ~Student() { cout << "~Student\n"; }
protected:
    int semesHours;
    float gpa;
};

class Teacher {
public:
    Teacher() {
        cout << "constructing teacher.\n";
    }
    ~Teacher() { cout << "~Teacher\n"; }
private:
    char name[20];
};
//Example of composion class
class TutorPair {
public:
    TutorPair() {
        cout << "constructing tutorpair.\n";
        noMeetings = 0;
    }
    // Cannot access Student and Teacher members directly
    ~TutorPair() { cout << "~TutorPair\n"; }
protected:
    Student student;   // Member object
    Teacher teacher;   // Member object
    int noMeetings;
};

Constructor Execution Order

int main() {
    TutorPair tp;
    cout << "back in main.\n";
    return 0;
}

Output:

constructing student.
constructing teacher.
constructing tutorpair.
back in main
~TutorPair
~Teacher
~Student

Note: In a class, the division of labor is very clear; each class is only responsible for initializing its own objects.

Initialization Confusion in Composition Classes

class StudentID {
public:
    StudentID(int id = 0) {
        value = id;
    }
    ~StudentID() {}
protected:
    int value;
};

class Student {
public:
    Student(string pName = "noName", int ssID = 0) {
        name = pName;
    }
private:
    string name;
    StudentID id;
};

int main() {
    Student s("Randy", 9818);
    return 0;
}

Problem: How to pass ssID to initialize the id member?

Incorrect Approach

class Student {
public:
    Student(string pName = "noName", int ssID = 0) {
        name = pName;
        StudentID id(ssID);  // This only creates a temporary variable
    }
private:
    string name;
    StudentID id;
};

What actually happens in sequence:
1.The Student object is created
2.The member id is default-initialized using StudentID’s default constructor
3.Inside the constructor, a temporary StudentID object named id is created and initialized with ssID
4.The temporary id is destroyed when the constructor finishes
5.The class member id remains unchanged (still has its default value)

Correct Solution: Member Initialization List

class Student {
    string name;
    StudentID id;
public:
    Student(string pName = "noName", int ssID = 0) : id(ssID) {
        name = pName;
    }
};

What actually happens in sequence for the initialization list approach:

  1. Memory for the Student object is allocated
  2. The initialization list is processed: id(ssID) invokes the StudentID constructor with parameter ssID
  3. The member id is initialized directly with the value ssID
  4. The constructor body executes: name = pName assigns a value to the name member
  5. The fully initialized Student object is ready for use
    Colon Syntax:
  • Introduces the constructor’s initialization list
  • Calls constructors with parameters specified in the constructor parameter list

Example with Multiple Member Objects

class Student {
    int semesHours;
    float gpa;
public:
    Student(int i) {
        cout << "constructing student.\n";
        semesHours = i;
        gpa = 3.5;
    }
    ~Student() { cout << "~Student\n"; }
};

class Teacher {
    string name;
public:
    Teacher(string p) {
        name = p;
        cout << "constructing teacher.\n";
    }
    ~Teacher() { cout << "~Teacher\n"; }
};
class TutorPair {
public:
    // The order of initialization is determined by the declaration order,
    // not the order in the initialization list
    TutorPair(int i, int j, string p) : teacher(p), student(j) {
        cout << "constructing tutorpair.\n";
        noMeetings = i;
    }
    ~TutorPair() { cout << "~TutorPair\n"; }
protected:
    Student student;   // Initialized first
    Teacher teacher;   // Initialized second
    int noMeetings;
};

int main() {
    TutorPair tp(5, 20, "Jane");
    cout << "back in main.\n";
    return 0;
}

Initializing Constant Data Members and References

  • References: Must be attached to another independent variable and initialized.
  • Constant data members: Data members that are initialized only once and their values cannot be changed.
  • Both can only be initialized using the member initialization list syntax.
class Student {
public:
    Student(int s, int &k) : i(s), j(k) {}  // Cannot use direct assignment
    void p() { cout << j << endl; }
protected:
    const int i;  // Constant data member
    int &j;       // Reference member
};

C++11 In-place Initialization of Class Members

const int i = 20;
int a1 = 2;      // Traditional initialization
int a2 = {2};    // Uniform initialization with =
int a3{3};       // Uniform initialization without =

Key Points to Remember

  1. Construction Order:
    • Member objects are constructed before the containing class
    • Construction order follows declaration order, not initialization list order
  2. Destruction Order:
    • Destruction occurs in reverse order of construction
    • Containing class destructor executes first, then member objects
  3. Initialization List:
    • Required for: reference members, const members, member objects without default constructors
    • Preferred for all member initialization (more efficient than assignment in body)
  4. C++11 Feature:
    • In-place initialization provides alternative ways to initialize class members

String Class

The C++ Standard Library provides a string class that offers a more convenient and safer way to handle strings compared to traditional C-style character arrays. Here’s an explanation of the key features:

Basic Features of C++ string

  1. Automatic Memory Management
    • When you declare string name;, the string class automatically handles memory allocation and deallocation
    • Dynamically adjusts the memory space as the string grows or shrinks
    • Prevents buffer overflow issues common with C-style strings
  2. No Null Terminator Requirement
    • C++ strings don’t rely on the ‘\0’ null terminator character internally
    • The class keeps track of the string length separately
  3. Operator Overloading
    • Assignment (=): Easily copy strings with str1 = str2;
    • Equality comparison (==): Compare string content with if(str1 == str2)
    • Concatenation (+): Join strings with str3 = str1 + str2;
  4. Using the string Class
    • Include the proper header: #include <string> (not string.h which is for C-style strings)
    • The string class is defined in the std namespace, so you may need to use std::string or using namespace std;
    string str1, str2 = "abcd"; 
    str1 = str2;                 
    str2 = str1 + "123"; 
    cout << str2 << " " << str2.length() << " " << str2.insert(0, "8");
    
    The output would be:
    abcd123 7 8abcd123

Converting C++ string to C-style string

Sometimes you need to work with functions that require C-style strings. The string class provides two methods for this:

  1. data() Method
    • Returns the string contents as a character array
    • Does NOT append a null terminator (‘\0’)
    • Example: char s[20]; strcpy(s, str1.data());
    • Note: This is potentially dangerous since C functions expect null-terminated strings
  2. c_str() Method
    • Returns a const char pointer to a null-terminated character array
    • Safely usable with C functions that expect C-style strings
    • Example: char s[20]; strcpy(s, str2.c_str());
    • This is the preferred method for C++ to C string conversion

Important Considerations

  1. When using data(), make sure you manually add a null terminator if needed for C functions
  2. The pointer returned by c_str() is valid only until the next operation that modifies the string
  3. When copying to char arrays, ensure the destination array is large enough to avoid buffer overflows
  4. Modern C++ code should prefer string objects over C-style strings whenever possible

Object construct order

Categories of Objects and Their Construction Order

  1. Local and Static Local Objects
    • Local objects are constructed each time the execution flow reaches their definition
    • Static local objects (with static lifetime) are constructed only once during the first function call
    • Class members follow this construction order (declared in textual order)
  2. Static Objects
    • Constructed once at first definition
    • Destructed when the program ends
  3. Global Objects
    • Constructed before main() executes
    • Destructed when the program ends
    • If global objects are distributed across different files, their construction order is unpredictable

Example Code with Output Analysis

class A {
public:
    A(int k) { s = k; cout << k << "A is constructing\n"; }
    ~A() { cout << s << "~A\n"; }
    int s;
};

int main() {
    cout << "main is running\n";
    A s(1);
    void f();
    f();
    f();
    return 0;
}

void f() {
    cout << "f is running\n";
    A s(2);
    static A t(3);
}

A t(4);  // Global object
Output:
4A is constructing        // Global object t constructed before main
main is running
1A is constructing        // Local object s in main
f is running
2A is constructing        // Local object s in f
3A is constructing        // Static local object t in f (first call only)
2~A                       // Local s in f destroyed when f ends
f is running
2A is constructing        // Another local s in f for second call
2~A                       // Local s in f destroyed again
1~A                       // Local s in main destroyed
3~A                       // Static local t destroyed at program end
4~A                       // Global t destroyed at program end
  1. Local Objects:
    • Constructed every time execution passes their definition
    • Destructed when execution leaves their scope
  2. Static Local Objects:
    • Constructed only during the first function call (note object t(3) is constructed only once)
    • Destructed at program end
  3. Global Objects:
    • Constructed before main() starts
    • Destructed after main() completes

Cross-File Global Object Issues

The order of construction for global objects across different compilation units is undefined, which can lead to problems:

// A.h
class A {
public:
    A(int r = 0) { s = r; }
private:
    int s;
};

// B.h
class B {
public:
    B(A t) { x = t; }
private:
    A x;
};

// l.cpp
#include "A.h"
A h(6);
void f() {
    // ...
}

// z.cpp
#include "A.h"
#include "B.h"
extern A h;
B k(h);  // Potential problem: h may not be defined.
int main() { /* ... */ }

Notes:
Whatextern A h; means in C++:
The extern keyword is used to declare a variable that is defined elsewhere (typically in another file). It tells the compiler:

  1. “There exists a variable h of type A that is defined in another translation unit”
  2. “Don’t allocate memory for h here; just use the one defined elsewhere”
    Actual porcess:
  3. In l.cpp: A h(6); is the definition that allocates memory and initializes the global object h
  4. In z.cpp: extern A h; is just a declaration that refers to the h defined in l.cpp
  5. Then B k(h); creates a global object k that depends on h being already constructed

Const Objects and Const Member Functions

Const Objects

A const object is an object whose data members cannot be changed throughout the object’s entire lifetime. Const objects must be initialized when they are defined, and cannot be modified thereafter.
Syntax for declaring const objects:
const ClassName objectName; or ClassName const objectName;
Example:

class A {
public:
    A(int i, int j) { x = i; y = j; }
    void reset() { x++; y++; }
private:
    int x, y;
};

const A a(3, 4);  // Const object declaration with initialization
a.reset();        // Error: cannot modify a const object

Const Member Functions

Const member functions are methods that guarantee they will not modify the object’s state (data members). They are declared with the const keyword after the function parameter list.
Example:

class R {
public:
    R(int r1, int r2) { R1 = r1; R2 = r2; }
    void add() { R2++; }               // Regular member function
    void print();                      // Regular member function
    void print() const;                // Const member function (valid overload)
    void p() const;                    // Const member function
private:
    int R1, R2;
};

void R::print() {
    cout << R1;
    p();  // OK: Regular member functions can call const member functions
}

void R::print() const {  // 'const' suffix cannot be omitted
    R1++;     // Error: Const member functions cannot modify data members
    add();    // Error: Const member functions cannot call non-const member functions
    cout << R1;
    p();      // OK: Const member functions can call other const member functions
}

void R::p() const {
    cout << R2 << endl;
}
int main() {
    // Regular object
    R a(5, 4);
    a.p();        // Regular objects can call const member functions
    a.print();    // For regular objects, non-const versions are preferred when both exist
    
    // Const object
    const R b(20, 52);
    b.print();    // Const objects can only call const member functions
    b.add();      // Error: Const objects cannot call non-const member functions
    b.p();        // OK: This is a const member function
    
    // Const reference to regular object
    const R& ra = a;
    ra.p();       // Const references can only call const functions
    ra.print();   // Calls the const version of print()
}

Key Points

  1. Const Objects:
    • Must be initialized when defined
    • Cannot modify their data members
    • Can only call const member functions
  2. Const Member Functions:
    • Declared with const keyword after the parameter list
    • Cannot modify data members
    • Cannot call non-const member functions
    • Can be called by both const and non-const objects
  3. Overloading with Const:
    • A member function can be overloaded based on its const-ness
    • For non-const objects, the non-const version is preferred
    • For const objects, only the const version can be called
  4. Constructors and Destructors:
    • There are no const constructors or const destructors
    • Both const and regular objects use the same constructors and destructors

Dynamic Memory Allocation for Objects

Introduction to Dynamic Allocation

Dynamic memory allocation allows programs to request memory during runtime. In C++, there are two main approaches:

  1. C-style allocation:

    • malloc() and free()
    • Does not call constructors or destructors
    • Only allocates raw memory
  2. C++ allocation:

    • new and delete operators
    • Properly calls constructors and destructors
    • Both allocates memory and initializes objects
  3. Dynamic allocation is necessary when:

    • The required memory size is not known at compile time
    • Memory needs to persist beyond the current scope
    • Large objects that exceed the stack size limit

Comparing malloc/free vs new/delete

class aa {
public:
    aa(int a = 1) { id = a; }
    int id;
    ~aa() { cout << "aa is completed."; }
};

int main() {
    // Using new/delete
    aa *p = new aa(9);
    cout << p->id;         // 9 (constructor was called)
    
    // Using malloc/free
    aa *q = (aa *)malloc(sizeof(aa));
    cout << q->id;         // Random value (constructor was NOT called)
    
    delete p;              // Destructor is called
    free(q);               // Destructor is NOT called
    
    return 0;
}

Common Memory Leaks

Memory leaks occur when dynamically allocated memory is not properly freed. Here are some examples:
Example 1: Returning a reference to a dynamically allocated object without freeing it

class aa {
public:
    aa(int a = 1) { id = a; }
    int id;
};

aa& m() {
    aa *p = new aa(9);
    return (*p);           // Memory leak: no way to delete this later
}

int main() {
    aa& s = m();
    cout << s.id;          // 9 (object exists, but memory is leaked)
    return 0;
}

In main():

  • aa& s = m() gets a reference to the object that exists on the heap
  • We can access the object through this reference (s.id works correctly)
  • But we have no way to free the memory because we no longer have the pointer value(destroyed p)

Example 2: Deleting before returning a reference (dangerous!)

class aa {
public:
    aa(int a = 1) { id = a; }
    int id;
};

aa& m() {
    aa *p = new aa(9);
    delete p;              // Deleting memory
    return (*p);           // Dangerous: returning a reference to deleted memory
}

int main() {
    aa& s = m();
    cout << s.id;          // Random value (memory has been freed)
    return 0;
}

Heap space is not dynamically released with functions, and programmers need to manage it independently.
Example 3: Exhausting memory due to repeated allocation without deallocation

class aa {
public:
    aa(int a = 1) { id = a; }
    int id;
    ~aa() { cout << "aa is completed."; }
};

void p() {
    aa *s = new aa[9999];  // Allocate large array but never delete it
}

int main() {
    for(;;)                // Infinite loop
        p();               // Memory will eventually be exhausted
    return 0;
}

Importance of Proper Memory Management

Common errors in dynamic memory management:

  • Failing to release memory that was dynamically allocated
  • These unreleased memory blocks are called “memory leaks”
  • Small leaks may not be problematic as the OS reclaims memory when the program exits
  • However, for long-running programs or programs that allocate large amounts of memory, leaks can exhaust available memory
  • Memory exhaustion can cause program crashes and, in extreme cases, system instability
  • For programs that run for months or years without restarting, or on resource-constrained devices (like phones), avoiding memory leaks is critical

Destructors and Dynamic Memory

class aa {
public:
    aa(int a = 9) { age = new int[a]; }  // Constructor allocates dynamic memory
    int* age;
    ~aa() { delete[] age; }              // Destructor frees the allocated memory
};

int main() {
    aa* s = new aa;                      
    s->age[0] = 20;
    int* t = s->age;
    delete s;                            // Calls destructor, which frees age array
    cout << t[0];                        // Dangerous: accessing freed memory
    return 0;
}

Object Arrays

Creating arrays of objects with dynamic allocation:

class Student {
public:
    Student() {
        cin >> value;
    }
    ~Student() {
        cout << value << endl;
    }
protected:
    int value;
};

int main() {
    Student *p = new Student[5];         // Creates 5 Student objects, calling constructor for each
    cout << "Delete Begin" << endl;
    delete[] p;                          // Calls destructor for each object in array
    return 0;
}

Important notes about object arrays:

  • Cannot be initialized through parameter passing
  • Requires either a default constructor or constructors with default parameters
  • Student a[10] (); is incorrect syntax

Creating Arrays of Pointers to Objects

int nextStudentID = 1;

class StudentID {
public:
    StudentID() {
        value = nextStudentID++;
    }
    ~StudentID() {
        --nextStudentID;
    }
protected:
    int value;
};

class Student {
public:
    Student(string pName = "noName") {
        name = pName;
    }
protected:
    string name;
    StudentID id;
};

int main() {
    int i, j;
    string temp;
    cin >> i;
    
    // Create array of pointers to Student objects
    Student **p = new Student*[i];
    
    // Create each Student object dynamically
    for(j = 0; j < i; j++) {
        cin >> temp;
        p[j] = new Student(temp);
        cout << nextStudentID;
    }
    
    // Proper cleanup: delete each object, then delete the array of pointers
    for(j = i - 1; j >= 0; j--)
        delete p[j];
    delete[] p;
    
    cout << nextStudentID;
    return 0;
}

Detailed Explanation of the Pointer Concepts

  1. Student **p = new Student*[i];
    • This is a two-level pointer structure:
    • Student** means “pointer to a pointer to Student”
    • new Student*[i] allocates an array of i pointers (not Student objects yet)
    • Each element in this array will be a pointer to a Student object
    • Memory digram:
      p ---→ [ptr1][ptr2][ptr3]...[ptrN] (array of pointers, initially uninitialized)
  2. p[j] = new Student(temp);
    • For each position in the array, we allocate a new Student object
    • new Student(temp) creates a Student object on the heap and returns its address
    • p[j] = … stores this address in the j-th slot of our pointer array
    • After this loop completes, memory looks like:
      p ---→ [ptr1][ptr2][ptr3]...[ptrN]
      		↓     ↓     ↓       ↓
      	   [Student][Student]...[Student]  
      (actual Student objects on the heap)
      

Cleanup process:

  1. We have two levels of dynamic allocation:
    • The array of pointers: p
    • Each individual Student object: p[0], p[1], etc.
  2. for(j = i - 1; j >= 0; j–) delete p[j];
    • This loop deletes each Student object that we created with new Student(temp)
    • It frees the memory where the actual Student objects reside
    • After this loop, memory looks like:
      p ---→ [ptr1][ptr2][ptr3]...[ptrN]  
      
      (array still exists, but pointers point to freed memory)
  3. delete[] p; (Note: The original example incorrectly had delete p;)
    • This frees the array of pointers itself
    • We use delete[] because p points to an array (created with new[])
    • After this, all dynamically allocated memory has been freed

Best Practices

1 Always match allocation and deallocation properly:

  • new with delete
  • new[] with delete[]
  • malloc() with free()
    2 Consider using smart pointers (C++11 and later):
  • std::unique_ptr for single objects
  • std::shared_ptr for shared ownership
  • These automatically manage memory, reducing the risk of leaks
    3 Release memory in the correct order:
  • When dealing with arrays of pointers to objects, delete the pointed objects first
  • Then delete the array of pointers
    4 Implement proper destructors for classes that allocate dynamic memory
    5 Avoid returning references or pointers to local dynamically allocated objects:
  • This often leads to memory leaks or dangling references

Copy Constructors

Object Assignment vs. Object Copying

In C++, there are two different ways to make one object have the same data as another:

  1. Object Assignment: Using an existing object to assign values to another
Student a("Jenny");
Student b;      // b already exists
b = a;          // Assignment - uses overloaded assignment operator
  1. Object Copying (Copy Construction): Using an existing object to initialize a new object.
Student s1("Jenny");
Student s2 = s1;   // Equivalent to: Student s2(s1);

When Copy Constructors Are Called

Copy constructors are invoked in several scenarios:

  1. Direct initialization of a new object
Student s1("Jenny");
Student s2(s1);    // Copy constructor called
  1. Assignment-like initialization
Student s1("Jenny");
Student s2 = s1;   // Copy constructor called, not assignment
  1. Passing objects by value to functions
void func(Student s) { /* ... */ }
Student m;
func(m);           // Copy constructor called
  1. Returning objects by value from functions
Student func() {
    Student k;
    // ...
    return k;      // Copy constructor called
}
  1. Creating objects using dynamic allocation
Student s1("Jenny");
Student *p = new Student(s1);  // Copy constructor called

Default Copy Constructor

If you don’t define a copy constructor, the compiler provides a default one that performs a member-by-member copy (shallow copy):

class Student {
public:
    Student(int k) : i(k) {}
    Student() {}
    void p() { cout << i << endl; }
protected:
    int i;
};

int main() {
    Student s(9818);
    s.p();           // Output: 9818
    
    Student t(s);    // Default copy constructor called
    t.p();           // Output: 9818
    
    Student m;
    m = t;           // Assignment operator called, not copy constructor
    m.p();           // Output: 9818
    ·
    return 0;
}

Custom Copy Constructor

You can define your own copy constructor to control how objects are copied:

class Student {
public:
    Student(int k) : i(k) {}
    Student() {}
    
    // Custom copy constructor
    Student(const Student &m) {
        i = m.i * (-1);  // Negate the value during copying
    } 
    
    void p() { cout << i << endl; }
protected:
    int i;
};

int main() {
    Student s(9818);
    s.p();             // Output: 9818
    
    Student t(s);      // Custom copy constructor called
    t.p();             // Output: -9818
    
    Student k = s;     // Custom copy constructor called
    k.p();             // Output: -9818
    
    Student *p = new Student(s);  // Custom copy constructor called
    p->p();            // Output: -9818
    
    Student m;
    m = s;             // Assignment operator called, not copy constructor
    m.p();             // Output: 9818 (assuming default assignment operator)
    
    return 0;
}

Shallow Copy vs. Deep Copy

The Problem with Shallow Copy
When objects contain pointers to dynamically allocated memory, the default copy constructor creates a shallow copy, which can lead to problems:

class aa {
public:
    aa() { f = new char[10]; }
    ~aa() { delete[] f; }
    char* f;
};

int main() {
    aa p;
    strcpy(p.f, "Computer");
    cout << p.f << endl;      // Output: Computer
    
    aa q(p);                  // Shallow copy - q.f points to the same memory as p.f
    cout << q.f << endl;      // Output: Computer
    
    strcpy(p.f, "Software");
    // Both p.f and q.f now point to "Software" because they share the same memory
    cout << q.f << p.f << endl;  // Output: SoftwareSoftware
    
    return 0;
}  // PROBLEM: Double deletion - both destructors try to delete the same memory

Key issues with shallow copy:

  • Multiple objects point to the same memory
  • When one object modifies the memory, it affects all objects sharing that memory
  • When objects are destroyed, multiple attempts to delete the same memory can cause crashes

The Solution: Deep Copy

Implement a custom copy constructor that creates independent copies of dynamically allocated resources:

class aa {
public:
    aa() { f = new char[10]; }
    
    // Deep copy constructor
    aa(const aa& s) {
        f = new char[10];   // Allocate new memory
        strcpy(f, s.f);     // Copy the contents
    }
    
    ~aa() { delete[] f; }
    char* f;
};

int main() {
    aa p;
    strcpy(p.f, "Computer");
    cout << p.f << endl;      // Output: Computer
    
    aa q(p);                  // Deep copy - q.f points to a new, independent memory block
    cout << q.f << endl;      // Output: Computer
    
    strcpy(p.f, "Software");
    // Only p.f is changed, q.f remains unchanged
    cout << p.f << q.f << endl;  // Output: SoftwareComputer
    
    return 0;
}  // No problem: each destructor deletes its own memory
  1. It allocates completely new memory with f = new char[10];
    • Instead of just copying the pointer value (which would be a shallow copy)
    • The new object gets its own separate, independent memory block
    • This is the crucial step that makes it a deep copy
  2. It copies the actual content with strcpy(f, s.f);
    • Takes the contents from the source object’s memory
    • Duplicates that content byte-by-byte into the newly allocated memory
  3. The result is two separate copies of the data
    • Each object manages its own memory block
    • Changes to one object’s data won’t affect the other
    • When objects are destroyed, each one safely deletes its own memory

Unnamed Objects

C++ allows creating unnamed (temporary) objects:

#include <iostream>
using namespace std;

class Student {
public:
    Student(int i) {
        value = i;
        cout << "Student" << value << endl;
    }
    ~Student() {
        cout << "~Student" << value << endl;
    }
protected:
    int value;
};

int main() {
    Student(9);  // Creates an unnamed object, destroyed at the end of the statement
    
    //Student *p = &Student(8);  // Incorrect - although it compiles, it's meaningless
                              // The unnamed object is destroyed immediately
    
    // Student &q = Student(7);  // Error - cannot bind non-const reference to a temporary
    
    Student const &q = Student(7);  // Valid - extends lifetime of temporary until reference goes out of scope
    
    Student i(6);
    i = Student(5);  // Valid - creates temporary, then uses assignment operator
    
    return 0;
}  // Destructors called for all objects

Why this worksStudent const &q = Student(7);
This is allowed because:

  • C++ specifically permits binding const references to temporary objects
  • This triggers “lifetime extension” - the temporary lives until the reference goes out of scope
  • The “const” qualifier guarantees you won’t modify the temporary
  • This makes the pattern safe and useful

Rules for unnamed objects:

  1. They are temporary and typically destroyed at the end of the full expression
  2. Cannot bind non-const references to them
  3. Can bind const references, which extends their lifetime to match the reference
  4. Can be used in assignments and other operations

Key Points to Remember

  1. Copy constructor definition: ClassName(const ClassName& obj);
  2. When you should define a custom copy constructor:
    • When the class manages resources (pointers, file handles, etc.)
    • When you want special behavior during copying
    • When the default member-wise copying is inappropriate
  3. Difference between assignment and copy construction:
    • Copy construction initializes a new object
    • Assignment gives values to an existing object
  4. Rule of Three:
    • If you need to define a custom destructor, copy constructor, or assignment operator, you typically need all three
    • This is because all three deal with resource management
  5. Shallow vs. Deep Copy:
    • Shallow copy duplicates pointers but not what they point to
    • Deep copy duplicates both pointers and what they point to
    • For classes with dynamically allocated memory, deep copy is usually necessary

Constructors for Type Conversion

Implicit Type Conversion with Constructors

In C++, constructors that can be called with a single parameter (or where all parameters except the first have default values) can be used for implicit type conversion from the parameter type to the class type.
Basic Example

class aa {
public:
    aa(int a = 1) { id = a; cout << "Constructor" << endl; }
    aa(aa & s) { id = s.id; cout << "Copy"; }
    int & Getid() { return(id); }
private:
    int id;
};

int main() {
    aa m;         // "Constructor"
    aa n(m);      // "Copy"
    aa o = m;     // "Copy" (equivalent to aa o(m))
    aa s = 9;     // "Constructor" (equivalent to aa s(9))
    aa t;         // "Constructor"
    t = 9;        // t = aa(9); "Constructor" followed by assignment operation
    return 0;
}

In this example:

  • aa s = 9; uses the constructor aa(int a = 1) to convert 9 to an aa object
  • t = 9; first creates a temporary aa object from 9 using the constructor, then assigns it to t

Type Conversion in Function Arguments

Constructors enable implicit conversion when passing arguments to functions:

class aa {
public:
    aa(int a = 1) { id = a; }
    char * p;
    int id;
};

void m(aa a) {
    cout << a.id << endl;
}

int main() {
    m(9);  // Equivalent to m(aa(9));
    return 0;
}

Here, m(9) works because 9 is implicitly converted to an aa object using the constructor.
Note: If there exists a function m(int), it would be preferred over the implicit conversion to aa.

Ambiguity in Type Conversion

When multiple classes have constructors that could convert from the same type, ambiguity can occur:

class aa {
public:
    aa(int a = 1) { id = a; }
    int id;
};

class bb {
public:
    bb(int a = 2) { id = a; }
    int id;
};

void m(aa a) {
    cout << a.id << endl;
}

void m(bb a) {
    cout << a.id << endl;
}

int main() {
    m(9);  // Ambiguity - could be m(aa(9)) or m(bb(9))
    return 0;
}

This code won’t compile because the compiler can’t determine whether to convert 9 to aa or bb.

Limitations of Constructor Conversions

1 No multi-step conversions: The conversion must be direct; C++ won’t perform multiple conversions in sequence
2 Single parameter requirement: For implicit conversion, constructors must have a single parameter, or all but one parameter must have default values
3 Not too complex: Conversion operations shouldn’t be too complex to maintain code clarity

Converting Between User-Defined Types

Constructors can convert between different class types:

class HighWorker {
    float income;
public:
    HighWorker(float a = 2000.0) { income = a; }
    float Getincome() { return(income); }
};

class Worker {
    float income;
public:
    Worker(float a = 1200.0) { income = a; }
    Worker(HighWorker b) { income = 0.75 * b.Getincome(); }
    float Getincome() { return(income); }
};

void m(Worker a) { cout << a.Getincome() << endl; }

int main() {
    Worker a;
    m(a);             // Passes Worker directly
    
    HighWorker b;
    m(b);             // Converts HighWorker to Worker using Worker(HighWorker)
                      // Equivalent to m(Worker(b));
    return 0;
}

In this example:

  • m(b) works because the Worker class has a constructor that takes a HighWorker as a parameter
  • This allows a HighWorker object to be implicitly converted to a Worker object

Controlling Implicit Conversions with explicit

To prevent unwanted implicit conversions, C++ provides the explicit keyword:

class HighWorker {
 float income;
public:
    HighWorker(float a = 2000.0){income = a;}
    float Getincome(){return(income);}
};
class Worker {
    float income;
public:
    Worker(float a = 1200.0) { income = a; }
    Worker(HighWorker b) { income = 0.75 * b.Getincome(); }
    float Getincome() { return(income); }
};

void m(Worker a) { cout << a.Getincome() << endl; }

int main() {
    HighWorker b;
    // m(b);          // Error: no implicit conversion allowed
    m(Worker(b));     // OK: explicit conversion
    return 0;
}

With explicit:

  • The constructor still works for explicit conversions
  • Implicit conversions are prohibited
  • This helps prevent unexpected behavior and makes code intentions clearer

Key Points to Remember

  1. Implicit conversion constructors:
    • Must have a single parameter (or all but one have default values)
    • Allow automatic conversion from parameter type to class type
  2. When implicit conversions occur:
    • Initialization: ClassName obj = value;
    • Assignment: obj = value;
    • Function arguments: func(value) when func expects class type
    • Return values: return value; when function returns class type
  3. Ambiguity issues:
    • Multiple conversion paths can lead to compilation errors
    • Function overloading can interact with conversions in complex ways
  4. Control mechanisms:
    • Use explicit to prevent implicit conversions
    • Create explicit conversion functions when needed
  5. Best practices:
    • Be mindful of implicit conversions for code clarity
    • Use explicit conversions where intentions should be clear
    • Avoid creating multiple implicit conversion paths

Friends

Introduction to Friends

Friends in C++ provide a mechanism to access private and protected members of a class from outside that class. They are a controlled exception to the principles of encapsulation and information hiding.

Friend Functions

A friend function is a non-member function that has access to a class’s private and protected members.
Problem Without Friends
Consider a class and a function that needs to operate efficiently on its private members:

class aa {
    float a;
    float b;
public:
    float &aaa() { return(a); }
    float &bbb() { return(b); }
};

aa sum(aa a, aa b) {
    aa c;  
    c.aaa() = a.aaa() + b.aaa();
    c.bbb() = a.bbb() + b.bbb();
    return(c);
}

void main() {
    aa a, b;   
    a.aaa() = 1; a.bbb() = 2;
    b.aaa() = 3; b.bbb() = 4;
    aa c = sum(a, b);
    cout << c.aaa() << endl << c.bbb();
    
    // Performance concern for repeated access
    for(long i = 0; i < 100000; i++)
        cout << a.aaa() << endl;
}

In this code:

  • We need to use accessor methods (aaa() and bbb()) to get or set private data
  • Each call involves function invocation overhead
  • For operations requiring many accesses, this overhead can be significant

Solution with Friend Functions

By declaring the sum function as a friend, we can access private members directly:

class aa {
    float a;
    float b;
public:
    friend aa sum(aa, aa);  // Friend function declaration
    friend void main();     // Friend function - gives main() access to private members
    float &aaa() { return(a); }
    float &bbb() { return(b); }
};

aa sum(aa s, aa t) {
    aa c;
    c.a = s.a + t.a;    // Direct access to private members
    c.b = s.b + t.b;    // Direct access to private members
    return(c);
}

void main() {
    aa a, b;   
    a.a = 1; a.b = 2;   // Direct access to private members
    b.a = 3; b.b = 4;   // Direct access to private members
    aa c = sum(a, b);
    cout << c.a << endl << c.b;  // Direct access to private members
    
    // More efficient with direct access
    for(long i = 0; i < 100000; i++)
        cout << a.a << endl;
}

In this improved code:

  • sum and main are declared as friend functions of class aa
  • They can directly access private members without accessor methods
  • This improves performance, especially for repeated operations

Friend Classes

A friend class has access to the private and protected members of the class that declares it as a friend.
Example of Friend Classes

class Teacher;  // Forward declaration to resolve cross-referencing

class Student {
    friend class Teacher;            // Friend class - all Teacher members can access Student private data
    // The following declarations are redundant if the entire class is already a friend
    // friend void Teacher::p(int);     // Redundant - already covered by friend class
    // friend void Teacher::q(float);   // Redundant - already covered by friend class
    int age;
    float score;
};

class Teacher {
public:
    void p(int a) { Sooc.age = a; }       // Access private member of Student
    void q(float a) { Sooc.score = a; }   // Access private member of Student
    Student Sooc;
private:
    char name[9];
};

void main() {
    Teacher a;
    a.p(20);
    a.q(80.9);
}

In this example:

  • Teacher is declared as a friend of Student with friend class Teacher;
  • This single declaration grants all member functions of Teacher access to private members of Student
  • The individual member function friend declarations are redundant and unnecessary
  • You would only need individual member function friend declarations if you wanted to grant access to specific functions rather than the entire class

Function Friends vs. Friend Class

  1. Use individual member function friends when:
    • You want to limit access to only specific functions of another class
    • You want fine-grained control over which external functions can access private data
    • The principle of least privilege is important for your design
  2. Use friend class when:
    • Two classes are tightly coupled and need extensive access to each other’s private members
    • Most or all member functions of one class need access to another class’s private data
    • The classes form a logical unit in terms of their relationship

Important Notes About Friends

1 No Symmetry: Friend relationships are not symmetric. If class B is a friend of class A, class A is not automatically a friend of class B.
2 No Transitivity: Friend relationships are not transitive. If class B is a friend of class A, and class C is a friend of class B, class C is not automatically a friend of class A.
3 Declaration Position: Friend declarations can be placed in either the public, protected, or private sections of a class. The placement does not affect the accessibility of the friend.
4 Not Class Members: Friend functions are not member functions of the class. They are normal functions with special access privileges.
5 Multiple Friendships: A function can be a friend of multiple classes by being declared as a friend in each class.

Benefits and Drawbacks of Friends

Benefits:

  • Improves performance by allowing direct access to private members
  • Simplifies code that needs to work with multiple objects of a class
  • Allows non-member functions to participate in the implementation of a class
  • Enables certain C++ features (like operator overloading) to be implemented efficiently

Drawbacks:

  • Breaks encapsulation, potentially leading to less maintainable code
  • Can make the code harder to understand if overused
  • Increases coupling between classes, which can make code more difficult to modify

Best Practices

1 Use Sparingly: Use friends only when necessary, typically for operators and functions that need efficient access to multiple objects.
2 Consider Alternatives: Before declaring a friend, consider whether adding member functions or restructuring the code could achieve the same goal.
3 Balance: Find a balance between data sharing and information hiding. Friends should be a conscious trade-off between encapsulation and performance/convenience.
4 Document: Clearly document any friend relationships to make the design intent clear to future maintainers.
5 Group Together: Keep friend functions close to the class definition, typically in the same file, to maintain logical cohesion.

Static Members

The Need for Static Members

Static members in C++ provide a way to share data or functions among all instances of a class, rather than having separate copies for each object.
Problem Without Static Members
Consider a scenario where we need to track IDs across multiple objects:

// StudentID.h
class StudentID {
  int value;
public:
  StudentID();
  ~StudentID();
};

// StudentID.cpp
int ID = 0;  // Global variable
StudentID::StudentID() {
  value = ++ID;
}
StudentID::~StudentID() {
  --ID;
}

// aa.cpp
#include "StudentID.h"
extern int ID;  // External reference to global variable
void main() {
  void xx();  
  StudentID s, t;
  cout << ID;
  xx();
  cout << ID;
}

// bb.cpp
#include "StudentID.h"
extern int ID;
void xx() {
  ID++;  // Directly modifying the global variable
}

Problems with this approach:

  • ID is a global variable accessible to any code
  • No protection or encapsulation
  • Anyone can modify ID without using the class interface
  • Potential naming conflicts with other global variables

Static Data Members

Static data members solve these problems by making the data part of the class, but shared among all instances.
Declaration and Initialization

// StudentID.h
class StudentID {
  int value;
public:
  static int ID;  // Static data member declaration
  StudentID();
  ~StudentID();
};

// StudentID.cpp
#include "StudentID.h"
int StudentID::ID = 0;  // Static data member initialization (no static keyword here)
StudentID::StudentID() {
  value = ++ID;
}
StudentID::~StudentID() {
  --ID;
}

// aa.cpp
#include "StudentID.h"
void main() {
  void xx();
  StudentID s, t;  // s and t's object space doesn't include ID
                  // The static data member has only one copy
  cout << StudentID::ID;  // Access via class name
  xx();
  cout << StudentID::ID;
}

// bb.cpp
#include "StudentID.h"
void xx() {
  StudentID::ID++;  // Modify via class name
}

Accessing Static Data Members
Static members can be accessed in three ways:

// StudentID.h
class StudentID {
public:
  static int ID;
  StudentID();
  ~StudentID();
protected:
  int value;
};

// aa.cpp
#include "StudentID.h"
void main() {
  void xx();
  StudentID s, t;
  cout << s.ID;              // Access via object
  cout << t.ID;              // Access via different object (same value)
  cout << StudentID::ID;     // Access via class name (preferred)
  xx();    
}

All three methods access the same shared variable.

Properties of Static Data Members

1 Shared among all objects: Only one copy exists regardless of how many objects are created
2 Memory allocation: Not part of individual objects’ memory space
3 Lifetime: Allocated at program start, similar to global variables
4 Initialization: Must be initialized outside class definition (in a cpp file)
5 Access control: Follows class access rules (public, protected, private)

Static Member Functions

Static member functions are class functions that don’t operate on a specific instance of the class.

class Student {
public:
  static int Get();  // Static member function declaration
private:
  static int noOfStudents;
  int x;
};

int Student::Get() {
  return(noOfStudents);
}

int Student::noOfStudents = 0;

void main() {
  cout << Student::Get();  // Call without object (correct)
  Student ss;
  cout << ss.Get();       // Call with object (works but not necessary)
}

Restrictions on Static Member Functions
Static member functions cannot:

  1. Access non-static data members directly
  2. Call non-static member functions directly
  3. Use the this pointer
class aa {
  int b;
  int GetB() { return b; }
public:
  static int a;
  void o() { cout << a; cout << this->a; cout << b; }  // Non-static, can access everything
  static void p() { cout << a; /* cout << this->a; */ }  // Error: no 'this' in static function
  static void q() { /* cout << b << GetB(); */ }  // Error: can't access non-static members
  static void r(aa &t) { t.b = 6; cout << t.GetB(); }  // OK: accessing non-static through parameter
};

Common Uses for Static Members

1. Counting Objects

class Student {
public:
  Student() { noOfStudents++; }
  ~Student() { noOfStudents--; }
  static int GetCount() { return noOfStudents; }
private:
  static int noOfStudents;
};

int Student::noOfStudents = 0;

2. Singleton Pattern
The Singleton pattern ensures a class has only one instance and provides a global point to access it.

class Singleton {
  static Singleton* instance;  // Static pointer to the single instance
  Singleton() {}              // Private constructor prevents direct instantiation
public:
  static Singleton* getInstance() {  
    if(instance == nullptr)   // Check if instance exists
      instance = new Singleton;  // Create instance if it doesn't exist
    return instance;          // Return the existing or newly created instance
  }
};

Singleton* Singleton::instance = nullptr;  // Initialize the static pointer to null

void main() {
  Singleton* s = Singleton::getInstance();  // Get a pointer to the instance
  Singleton* t = Singleton::getInstance();  // Get another pointer to the SAME instance
  // s and t now point to the same object
  
  // Singleton u;  // Error: constructor is private, can't create object directly
}

Detailed explanation of pointer usage in the Singleton pattern:

  1. Static pointer declaration:
    • static Singleton* instance; declares a static pointer variable that will hold the address of the single instance.
    • Being static means there’s only one copy of this pointer shared by all code that uses this class.
    • This shared pointer is what allows us to maintain and access a single object.
  2. Pointer initialization:
    • Singleton* Singleton::instance = nullptr; initializes the static pointer to null (no object yet).
    • This is done outside the class definition as required for static members.
    • nullptr (or NULL in older code) indicates that no memory has been allocated yet.
  3. Lazy initialization using the pointer:
    • The first time getInstance() is called, instance is null, so a new object is created with new Singleton.
    • new Singleton allocates memory on the heap for a Singleton object and returns its address.
    • This address is stored in the instance pointer.
    • On subsequent calls, since instance is no longer null, no new object is created.
  4. Returning the pointer:
    • return instance; returns the address of the single Singleton object.
    • Callers receive this address as a pointer (Singleton*).
    • Every caller gets the same address, meaning they all access the same object.
  5. Client code usage:
    • Singleton* s = Singleton::getInstance(); stores the address of the single instance in pointer s.
    • Singleton* t = Singleton::getInstance(); stores the same address in pointer t.
    • Since both pointers contain the same address, s and t point to the same object in memory.
    • Users can’t create their own instances because the constructor is private.

This pointer-based approach allows the Singleton class to:

  • Control its own instantiation (through the private constructor)
  • Ensure only one instance exists (by checking the static pointer)
  • Provide global access to that instance (through the static getInstance method)
  • Defer creation until the instance is actually needed (lazy initialization)

Note: This basic implementation has some limitations, including potential issues with thread safety and object destruction. In production code, more sophisticated implementations might be needed.
3. Maintaining Linked Lists of Objects

class Student {
public:
  Student(char* pName);
  ~Student();
  static Student* pFirst;  // Points to first student in list
  Student* pNext;          // Points to next student in list
  char name[40];
};

Student* Student::pFirst = nullptr;

Student::Student(char* pName) {
  strcpy(name, pName);
  pNext = pFirst;  // Link to existing list
  pFirst = this;   // Make this the new first item
}

Student::~Student() {
  if(pFirst == this) {
    pFirst = pNext;  // If first in list, update pFirst
    return;
  }
  
  // Find this in the list and update links
  for(Student* pS = pFirst; pS; pS = pS->pNext)
    if(pS->pNext == this) {
      pS->pNext = pNext;
      return;
    }
}

void print() {
  for(Student* pS = Student::pFirst; pS; pS = pS->pNext)
    cout << pS->name << endl;
}

4. Reconstructing Relationships
Static members can help reconstruct relationships between objects, as in this example that transforms a bidirectional relationship to a unidirectional one:

// Original bidirectional relationship
class Female;
class Male {
public:
  Female* &getWife() { return pWife; }
private:
  Female* pWife;
};

class Female {
public:
  Male* &getHusband() { return pHusband; }
private:
  Male* pHusband;
};

// Transformed to unidirectional with static members
class Female;
class Male {
public:
  Male() { m_buffer[m_sum++] = this; }
  Female* &getWife() { return pWife; }
  static Male* m_buffer[100];  // Stores all Male objects
  static int m_sum;
private:
  Female* pWife;
};

int Male::m_sum = 0;
Male* Male::m_buffer[100] = {0};

class Female {
public:
  Male* getHusband();  // No longer stores husband pointer
};

Male* Female::getHusband() {
  for (int i = 0; i < Male::m_sum; i++)
    if (Male::m_buffer[i]->getWife() == this)
      return Male::m_buffer[i];
  return nullptr;
}

在这里插入图片描述

Static Class Objects

Static objects of a class are constructed before main() begins and are destroyed after main() ends:

class B {
public:
  int i;
  B(int i = 0) : i(i) {
    cout << i << "B is constructing\n";
  }
  B(B& b) { i = b.i;
    cout << i << "copying B\n";
  }
  ~B() { cout << i << " destructing B\n"; }
};

class A {
  int s;
  static B b;  // Static class member
  B bb;        // Regular class member
public:
  A(int k = 0, int j = 0) : s(k), bb(j) {
    cout << s << "A is constructing\n";
  }
  ~A() { cout << s << " destructing A\n"; }
};

B A::b;  // Initialize static member (constructed before main)
A s3(3, 3);  // Global object (constructed before main)

int main() {
  cout << "main is running\n";
  void f(B);
  A s1(1, 1);  // Local object (constructed when declaration is reached)
  B b1(8);
  f(b1);
  return 0;
}

void f(B b1) {
  cout << "f is running\n";
  b1 = B(9);
}

Output would show B class member construction before main() starts, and object destruction after main() ends.

Key Points About Static Members

  1. Static Data Members:
    • Only one copy exists for all objects of the class
    • Not part of object memory space
    • Allocated at program start
    • Must be initialized outside the class definition
    • Can be accessed via class name or objects (class name preferred)
  2. Static Member Functions:
    • Don’t operate on a specific object (no this pointer)
    • Can only access static data members directly
    • Can be called without creating any objects
    • Useful for operations that don’t depend on object state
  3. Use Cases:
    • Object counting
    • Shared resources
    • Singleton pattern
    • Global flags or states
    • Linked lists of class objects
    • Maintaining relationships between objects
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值