In C++, classes and objects are the basic concepts of object-oriented programming. A class is an abstract data type used to describe the properties and behavior of an object. An object is an instance of a class, embodying the properties and behavior of the class. This article will introduce the object characteristics of classes and objects in C++, with a focus on object references.
Article directory
Related links:
One article to understand the encapsulation of classes and objects, one article to understand the advanced application of reference functions
in C++
1. Constructor and destructor
When we create a class, it may have some member variables and member functions. Constructors and destructors are special member functions of a class that are used to initialize and clean up objects .
-
The function of the constructor is to initialize the member variables of the object when creating the object. It has the same name as the class, has no return type, and can have parameters. A constructor can have multiple overloaded versions, and which constructor is used depends on the type and number of parameters passed in. The constructor is automatically called when the object is created.
-
The role of the destructor is to clean up the object's resources when the object is destroyed. It has the same name as the class, preceded by a tilde (~), has no return type, and accepts no parameters. There can only be one destructor and cannot be overloaded. The destructor is automatically called when the object is destroyed.
Sample code:
class Person {
public:
string name;
int age;
// 默认构造函数
Person() {
name = "Unknown";
age = 0;
cout << "Default constructor called" << endl;
}
// 带参数的构造函数
Person(string n, int a) {
name = n;
age = a;
cout << "Parameterized constructor called" << endl;
}
// 析构函数
~Person() {
cout << "Destructor called" << endl;
}
};
int main() {
// 使用默认构造函数创建对象
Person p1;
cout << "Name: " << p1.name << ", Age: " << p1.age << endl;
// 使用带参数的构造函数创建对象
Person p2("John", 25);
cout << "Name: " << p2.name << ", Age: " << p2.age << endl;
return 0;
}
Output result:
Default constructor called
Name: Unknown, Age: 0
Parameterized constructor called
Name: John, Age: 25
Destructor called
Destructor called
Code explanation:
In the above example, we defined a Person
class named which has two member variables: name
and age
. We use constructors and destructors to initialize and clean up these member variables.
First, we define a default constructor, which takes no parameters. In the default constructor, we will set name
to 0 and print a message to indicate that the constructor was called.Unknown
age
Next, we define a parameterized constructor that accepts a string parameter and an integer parameter. In the constructor with parameters, we assign the passed parameter values to name
the age
member variables and print a message to indicate that the constructor has been called.
In the main function, we first create an p1
object named using the default constructor. Since no parameters are passed in, the default constructor is called. Then, we print out the values of p1
the object's name
and member variables, which are and 0 age
respectively .Unknown
Next, we create an object named using a constructor with parameters p2
. We pass in strings "John"
and integers 25
as parameters, so the constructor with parameters is called. Then, we print out the values of p2
the object's name
and member variables, which are and 25 age
respectively .John
At the end of the program, the objects p1
and p2
out of their scope, so they are destroyed. During object destruction, the destructor is automatically called. In the example, we printed a message in the destructor to indicate that the destructor was called.
2. Classification and calling of functions
1. Classification
-
Default constructor: If a class does not explicitly define a constructor, the compiler automatically generates a default constructor. The default constructor has no parameters and does not perform any initialization operations. It is called implicitly when creating an object.
-
Parameterized constructor: A parameterized constructor accepts one or more parameters and uses these parameters to initialize the member variables of the object. They are called when the object is created, and which constructor is used is determined based on the type and number of parameters passed in.
-
Copy constructor: The copy constructor is a special constructor that accepts a reference to a similar object as a parameter and uses the value of the object to initialize the newly created object. The copy constructor is called when:
- When using one object to initialize another object
- Pass object to function as function parameter
- Return object from function
2. Calling method
-
Direct call: You can call the constructor directly by adding parentheses to the class name. For example:
MyClass obj(10);
-
Implicit calling: The constructor is called implicitly when creating an object. For example:
MyClass obj = MyClass(10);
, where the constructor with parameters is called to create the object. -
Copy initialization: When one object is used to initialize another object, the copy constructor is called. For example:
MyClass obj1(10); MyClass obj2 = obj1;
-
Function parameter passing: When an object is passed to a function as a function parameter, the copy constructor is called. For example:
void func(MyClass obj);
, the copy constructor will be called when calling the functionfunc(obj)
. -
Function returns object: When an object is returned from a function, the copy constructor is called. For example:
MyClass func() { return MyClass(10); }
3. Sample code
#include <iostream>
using namespace std;
class MyClass {
public:
int num;
// 默认构造函数
MyClass() {
num = 0;
cout << "Default constructor called" << endl;
}
// 带参数的构造函数
MyClass(int n) {
num = n;
cout << "Parameterized constructor called" << endl;
}
// 拷贝构造函数
MyClass(const MyClass& obj) {
num = obj.num;
cout << "Copy constructor called" << endl;
}
// 析构函数
~MyClass() {
cout << "Destructor called" << endl;
}
};
int main() {
// 直接调用构造函数
MyClass obj1(10);
// 隐式调用构造函数
MyClass obj2 = MyClass(20);
// 拷贝初始化
MyClass obj3(obj1);
// 函数参数传递
void func(MyClass obj);
func(obj1);
// 函数返回对象
MyClass func();
MyClass obj4 = func();
return 0;
}
4. Output results:
Parameterized constructor called
Parameterized constructor called
Copy constructor called
Copy constructor called
Destructor called
Destructor called
Destructor called
Destructor called
5. Code explanation
In the above example, we have defined a MyClass
class named which contains one member variable num
and multiple constructors. We create multiple objects and call the constructor in different ways.
First, we create the object by calling the constructor directly obj1
, which calls the constructor with parameters. We then create the object by implicitly calling the constructor obj2
, which also calls the constructor with parameters.
Next, we create the object via copy-initialization obj3
, which obj1
initializes it with the object's value. During the copy initialization process, the copy constructor will be called.
Then, we define a function func
that accepts an MyClass
object as a parameter. When a function is called func(obj1)
, the copy constructor is called to obj1
pass the object to the function.
Finally, we define a function func
that returns an MyClass
object. The copy constructor is called when a function is called func()
and the returned object is assigned to.obj4
At the end of the program, all objects go out of their scope, so they are destroyed. During object destruction, the destructor is automatically called. In the example, we printed a message in the destructor to indicate that the destructor was called.
3. Timing of copy constructor
- When an object is used to initialize another object : When an existing object is used to initialize a new object, the copy constructor is called. For example:
class MyClass {
public:
MyClass(int value) : m_value(value) {
}
MyClass(const MyClass& other) : m_value(other.m_value) {
std::cout << "Copy constructor called" << std::endl;
}
private:
int m_value;
};
MyClass obj1(10);
MyClass obj2 = obj1; // 调用拷贝构造函数
- Passing an object to a function as a function argument : When an object is passed to a function as a function argument, the copy constructor is called. For example:
class MyClass {
public:
MyClass(int value) : m_value(value) {
}
MyClass(const MyClass& other) : m_value(other.m_value) {
std::cout << "Copy constructor called" << std::endl;
}
private:
int m_value;
};
void func(MyClass obj) {
// Do something with obj
}
MyClass obj1(10);
func(obj1); // 调用拷贝构造函数
- Returning an object from a function : When an object is returned from a function, the copy constructor is called. For example:
class MyClass {
public:
MyClass(int value) : m_value(value) {
}
MyClass(const MyClass& other) : m_value(other.m_value) {
std::cout << "Copy constructor called" << std::endl;
}
private:
int m_value;
};
MyClass func() {
MyClass obj(10);
return obj; // 调用拷贝构造函数
}
- When using a class object for assignment, the copy constructor will also be called . For example:
class MyClass {
public:
MyClass(int value) : m_value(value) {
}
MyClass(const MyClass& other) : m_value(other.m_value) {
std::cout << "Copy constructor called" << std::endl;
}
private:
int m_value;
};
MyClass obj1(10);
MyClass obj2;
obj2 = obj1; // 调用拷贝构造函数
It should be noted that the compiler sometimes performs optimizations to avoid unnecessary calls to the copy constructor. This optimization is called "copy elision". In some cases, the compiler may move an object's value directly from one location to another instead of making a copy constructor call. This improves performance but does not call the copy constructor.
4. Constructor calling rules
The constructor calling rules are as follows:
-
Default constructor : If no constructor is explicitly defined, the compiler will automatically generate a default constructor. The default constructor has no parameters and performs default initialization operations. When an object is created, if no parameters are provided, the default constructor is called.
-
Parameterized constructor : The parameterized constructor accepts one or more parameters and uses these parameters to initialize the member variables of the object. When an object is created, if parameters are provided, the corresponding parameterized constructor is called.
-
Copy constructor : The copy constructor accepts an object of the same type as a parameter and uses the value of the object to initialize the new object. The copy constructor can be used in scenarios such as object copy initialization, function parameter passing, and function return objects.
-
Move constructor : The move constructor is a new feature introduced in C++11. It accepts an rvalue reference as a parameter and uses the value of the parameter to initialize a new object. Move constructors are often used to improve performance when resource ownership of an object is transferred.
The calling rules of the constructor are as follows:
- When an object is created, the appropriate constructor is selected and called based on the type and number of parameters provided. If no parameters are provided, the default constructor is called.
- The copy constructor is called when one object is used to initialize another object.
- The copy constructor is called when an object is passed to a function as a function argument.
- When an object is returned from a function, the copy constructor is called.
- When using a class object for assignment, the copy constructor will also be called.
- In some cases, the compiler will perform optimizations to avoid unnecessary calls to the copy constructor. This optimization is called "copy elision".
5. Deep copy and shallow copy
Shallow copy refers to copying the value of one object to another object, including the object's member variables. This means that both objects share the same memory address, and modifications to one of them will affect the other. Shallow copy only copies the surface level of the object and does not copy the resources owned by the object.
Deep copy refers to copying the value of one object to another object and allocating independent memory space for the new object. In this way, the two objects have independent memory spaces, and modifications to one object will not affect the other object. A deep copy recursively copies all member variables of an object, including resources owned by the object.
Sample code:
#include <iostream>
#include <cstring>
class Person {
public:
Person(const char* name, int age) {
m_name = new char[strlen(name) + 1];
strcpy(m_name, name);
m_age = age;
}
// 拷贝构造函数
Person(const Person& other) {
m_name = new char[strlen(other.m_name) + 1];
strcpy(m_name, other.m_name);
m_age = other.m_age;
}
// 析构函数
~Person() {
delete[] m_name;
}
// 打印信息
void printInfo() {
std::cout << "Name: " << m_name << ", Age: " << m_age << std::endl;
}
private:
char* m_name;
int m_age;
};
int main() {
Person person1("Alice", 25);
Person person2 = person1; // 浅拷贝
person1.printInfo(); // 输出:Name: Alice, Age: 25
person2.printInfo(); // 输出:Name: Alice, Age: 25
person2.printInfo(); // 输出:Name: Alice, Age: 25
person2.printInfo(); // 输出:Name: Alice, Age: 25
person1.printInfo(); // 输出:Name: Bob, Age: 30
person2.printInfo(); // 输出:Name: Alice, Age: 25
return 0;
}
In the above example, we defined a Person
class that contains a member variable of string type m_name
and an integer member variable m_age
. In the constructor, we use new
operators to m_name
allocate a separate memory space and copy the string into that memory space.
Then, we create an person1
object and assign its value to person2
the object. Since the default copy constructor is a shallow copy, the pointer person2
of the object m_name
points to the person1
same memory space as the object. When we modify person2
the object m_name
, we actually modify person1
the object m_name
. This is the characteristic of shallow copy.
m_name
In order to implement deep copy, we need to customize the copy constructor, allocate a separate memory space in it , and copy the string into this space. In this way, person2
the object has its own independent m_name
memory space, and modifications to it will not affect person1
the object.
To sum up, shallow copy only copies the surface level of the object, while deep copy recursively copies all member variables of the object, including the resources owned by the object. Deep copy requires a custom copy constructor to implement.
6. Initialization list
The initialization list is a method of initializing member variables in the constructor and can be used to implement deep copying.
In the above example, we can use an initialization list to implement a deep copy without manually allocating memory and copying the string in the copy constructor.
Here is an example of using an initialization list to implement a deep copy:
#include <iostream>
#include <cstring>
class Person {
public:
Person(const char* name, int age) : m_age(age) {
m_name = new char[strlen(name) + 1];
strcpy(m_name, name);
}
// 拷贝构造函数
Person(const Person& other) : m_age(other.m_age) {
m_name = new char[strlen(other.m_name) + 1];
strcpy(m_name, other.m_name);
}
// 析构函数
~Person() {
delete[] m_name;
}
// 打印信息
void printInfo() {
std::cout << "Name: " << m_name << ", Age: " << m_age << std::endl;
}
private:
char* m_name;
int m_age;
};
int main() {
Person person1("Alice", 25);
Person person2 = person1; // 深拷贝
person1.printInfo(); // 输出:Name: Alice, Age: 25
person2.printInfo(); // 输出:Name: Alice, Age: 25
person2.printInfo(); // 输出:Name: Alice, Age: 25
person2.printInfo(); // 输出:Name: Alice, Age: 25
person1.printInfo(); // 输出:Name: Alice, Age: 25
person2.printInfo(); // 输出:Name: Alice, Age: 25
return 0;
}
In the above example, we allocate a separate memory space in the constructor's initialization list and copy the string into that space. In this way, person2
the object has its own independent m_name
memory space, and modifications to it will not affect person1
the object.
Using an initialization list can simplify the code and ensure that member variables have been correctly initialized when the object is constructed. This is useful for implementing deep copies.
7. Class objects as class members
When the member variables of one class are objects of another class, we need to copy these member variables correctly in the copy constructor.
Here is an example where Person
one of the member variables of a class is Address
an object of the class:
#include <iostream>
#include <cstring>
class Address {
public:
Address(const char* city, const char* street) {
m_city = new char[strlen(city) + 1];
strcpy(m_city, city);
m_street = new char[strlen(street) + 1];
strcpy(m_street, street);
}
Address(const Address& other) {
m_city = new char[strlen(other.m_city) + 1];
strcpy(m_city, other.m_city);
m_street = new char[strlen(other.m_street) + 1];
strcpy(m_street, other.m_street);
}
~Address() {
delete[] m_city;
delete[] m_street;
}
void printInfo() {
std::cout << "City: " << m_city << ", Street: " << m_street << std::endl;
}
private:
char* m_city;
char* m_street;
};
class Person {
public:
Person(const char* name, int age, const Address& address) : m_age(age), m_address(address) {
m_name = new char[strlen(name) + 1];
strcpy(m_name, name);
}
Person(const Person& other) : m_age(other.m_age), m_address(other.m_address) {
m_name = new char[strlen(other.m_name) + 1];
strcpy(m_name, other.m_name);
}
~Person() {
delete[] m_name;
}
void printInfo() {
std::cout << "Name: " << m_name << ", Age: " << m_age << std::endl;
m_address.printInfo();
}
private:
char* m_name;
int m_age;
Address m_address;
};
int main() {
Address address("New York", "Broadway");
Person person1("Alice", 25, address);
Person person2 = person1; // 深拷贝
person1.printInfo(); // 输出:Name: Alice, Age: 25, City: New York, Street: Broadway
person2.printInfo(); // 输出:Name: Alice, Age: 25, City: New York, Street: Broadway
return 0;
}
In the above example, Person
one of the member variables of the class is Address
an object of the class. In Person
the copy constructor of the class, we use the copy constructor to copy the object correctly Address
. In this way, when we copy an Person
object, Person
the object and its member variable Address
objects will be deeply copied.
It should be noted that in Person
the destructor of the class, we only need to release m_name
the memory space of the member variables, because m_address
the memory space of the member variables will be Address
released in the destructor of the class.
To sum up, when the member variables of one class are objects of another class, we need to correctly copy these member variables in the copy constructor to implement deep copy.
8. Static members
Static member variables belong to the class itself rather than to instances of the class. Therefore, there is no need to copy static member variables in the copy constructor because they are shared among all instances of the class.
Here is an example where the Person
class has a static member variable count
:
#include <iostream>
class Person {
public:
Person(const char* name, int age) : m_age(age) {
m_name = new char[strlen(name) + 1];
strcpy(m_name, name);
count++;
}
Person(const Person& other) : m_age(other.m_age) {
m_name = new char[strlen(other.m_name) + 1];
strcpy(m_name, other.m_name);
count++;
}
~Person() {
delete[] m_name;
count--;
}
static int getCount() {
return count;
}
private:
char* m_name;
int m_age;
static int count;
};
int Person::count = 0;
int main() {
Person person1("Alice", 25);
Person person2 = person1; // 深拷贝
std::cout << "Count: " << Person::getCount() << std::endl; // 输出:Count: 2
return 0;
}
In the above example, Person
the class has a static member variable count
that records the number of objects created Person
. In the constructor, we count
keep track of the number of objects by incrementing it, and in the destructor we count
update the number of objects by decrementing it.
In the copy constructor, we do not need to copy the static member variable count
because it belongs to the class itself rather than an instance of the class. Therefore, only non-static member variables need to be copied in the copy constructor.
To sum up, static member variables do not need to be copied in the copy constructor because they belong to the class itself rather than instances of the class.