在C++中,类与对象是面向对象编程的基本概念。类是一种抽象的数据类型,用于描述对象的属性和行为。而对象则是类的实例,具体化了类的属性和行为。本文将介绍C++中类与对象的对象特征,并重点讨论了对象的引用。
文章目录
- 一、构造函数和析构函数
- 二、函数的分类和调用
- 1. 分类
- 2. 调用方式
- 3. 示例代码
- 4. 输出结果:
- 5. 代码解释
- 三、拷贝构造函数的时机
- 四、构造函数调用规则
- 五、深拷贝和浅拷贝
- 六、初始化列表
- 七、类对象作为类成员
- 八、静态成员
相关链接:
一文搞懂类与对象的封装
一文搞懂C++中的引用
函数的高级应用
一、构造函数和析构函数
当我们创建一个类时,它可能具有一些成员变量和成员函数。构造函数和析构函数是类的特殊成员函数,用于初始化对象和清理对象。
-
构造函数的作用是在创建对象时初始化对象的成员变量。它的名称与类名相同,没有返回类型,并且可以有参数。构造函数可以有多个重载版本,根据传入的参数的类型和数量来确定使用哪个构造函数。构造函数在对象创建时自动调用。
-
析构函数的作用是在对象销毁时清理对象的资源。它的名称与类名相同,前面加上一个波浪号(~),没有返回类型,并且不接受任何参数。析构函数只能有一个,不能重载。析构函数在对象销毁时自动调用。
示例代码:
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;
}
输出结果:
Default constructor called
Name: Unknown, Age: 0
Parameterized constructor called
Name: John, Age: 25
Destructor called
Destructor called
代码解释:
在上述示例中,我们定义了一个名为Person
的类,它具有两个成员变量:name
和age
。我们使用构造函数和析构函数来初始化和清理这些成员变量。
首先,我们定义了一个默认构造函数,它没有参数。在默认构造函数中,我们将name
设置为Unknown
,将age
设置为0,并打印一条消息来表示构造函数被调用。
接下来,我们定义了一个带参数的构造函数,它接受一个字符串参数和一个整数参数。在带参数的构造函数中,我们将传入的参数值分别赋给name
和age
成员变量,并打印一条消息来表示构造函数被调用。
在主函数中,我们首先使用默认构造函数创建一个名为p1
的对象。由于没有传入任何参数,因此默认构造函数被调用。然后,我们打印出p1
对象的name
和age
成员变量的值,它们分别为Unknown
和0。
接下来,我们使用带参数的构造函数创建一个名为p2
的对象。我们传入字符串"John"
和整数25
作为参数,因此带参数的构造函数被调用。然后,我们打印出p2
对象的name
和age
成员变量的值,它们分别为John
和25。
在程序结束时,对象p1
和p2
超出了它们的作用域,因此它们被销毁。在对象销毁的过程中,析构函数被自动调用。在示例中,我们在析构函数中打印了一条消息来表示析构函数被调用。
二、函数的分类和调用
1. 分类
-
默认构造函数:如果类没有显式定义构造函数,则编译器会自动生成一个默认构造函数。默认构造函数没有参数,也不执行任何初始化操作。它在创建对象时被隐式调用。
-
带参数的构造函数:带参数的构造函数接受一个或多个参数,并用这些参数来初始化对象的成员变量。它们在对象创建时被调用,并且根据传入的参数的类型和数量来确定使用哪个构造函数。
-
拷贝构造函数:拷贝构造函数是一种特殊的构造函数,它接受一个同类对象的引用作为参数,并使用该对象的值来初始化新创建的对象。拷贝构造函数在以下情况下被调用:
- 使用一个对象初始化另一个对象时
- 将对象作为函数参数传递给函数
- 从函数返回对象
2. 调用方式
-
直接调用:可以通过类名加括号的方式直接调用构造函数。例如:
MyClass obj(10);
-
隐式调用:构造函数在创建对象时隐式调用。例如:
MyClass obj = MyClass(10);
,这里会调用带参数的构造函数来创建对象。 -
拷贝初始化:使用一个对象初始化另一个对象时,会调用拷贝构造函数。例如:
MyClass obj1(10); MyClass obj2 = obj1;
-
函数参数传递:将对象作为函数参数传递给函数时,会调用拷贝构造函数。例如:
void func(MyClass obj);
,在调用函数func(obj)
时会调用拷贝构造函数。 -
函数返回对象:从函数返回对象时,会调用拷贝构造函数。例如:
MyClass func() { return MyClass(10); }
3. 示例代码
#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. 输出结果:
Parameterized constructor called
Parameterized constructor called
Copy constructor called
Copy constructor called
Destructor called
Destructor called
Destructor called
Destructor called
5. 代码解释
在上述示例中,我们定义了一个名为MyClass
的类,它包含一个成员变量num
和多个构造函数。我们创建了多个对象,并通过不同的方式调用构造函数。
首先,我们通过直接调用构造函数创建了对象obj1
,它会调用带参数的构造函数。然后,我们通过隐式调用构造函数创建了对象obj2
,它也会调用带参数的构造函数。
接下来,我们通过拷贝初始化创建了对象obj3
,它使用对象obj1
的值来初始化。在拷贝初始化过程中,会调用拷贝构造函数。
然后,我们定义了一个函数func
,它接受一个MyClass
对象作为参数。在调用函数func(obj1)
时,会调用拷贝构造函数来将对象obj1
传递给函数。
最后,我们定义了一个函数func
,它返回一个MyClass
对象。在调用函数func()
并将返回的对象赋值给obj4
时,会调用拷贝构造函数。
在程序结束时,所有对象超出了它们的作用域,因此它们被销毁。在对象销毁的过程中,析构函数被自动调用。在示例中,我们在析构函数中打印了一条消息来表示析构函数被调用。
三、拷贝构造函数的时机
- 使用一个对象初始化另一个对象时:当使用一个已经存在的对象来初始化一个新对象时,会调用拷贝构造函数。例如:
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; // 调用拷贝构造函数
- 将对象作为函数参数传递给函数:当将对象作为函数参数传递给函数时,会调用拷贝构造函数。例如:
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); // 调用拷贝构造函数
- 从函数返回对象:当从函数返回对象时,会调用拷贝构造函数。例如:
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; // 调用拷贝构造函数
}
- 在使用类对象进行赋值操作时,也会调用拷贝构造函数。例如:
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; // 调用拷贝构造函数
需要注意的是,编译器有时会进行优化,避免不必要的拷贝构造函数的调用。这种优化称为“拷贝消除”(copy elision)。在某些情况下,编译器可能会直接将对象的值从一个位置移动到另一个位置,而不是进行拷贝构造函数的调用。这可以提高性能,但是不会调用拷贝构造函数。
四、构造函数调用规则
构造函数调用规则如下:
-
默认构造函数:如果没有显式定义构造函数,编译器会自动生成一个默认构造函数。默认构造函数没有参数,并且执行默认的初始化操作。当创建对象时,如果没有提供参数,会调用默认构造函数。
-
参数化构造函数:参数化构造函数接受一个或多个参数,并用这些参数来初始化对象的成员变量。当创建对象时,如果提供了参数,会调用对应的参数化构造函数。
-
拷贝构造函数:拷贝构造函数接受一个同类型的对象作为参数,并使用该对象的值来初始化新对象。拷贝构造函数可以用于对象的拷贝初始化、函数参数传递和函数返回对象等场景。
-
移动构造函数:移动构造函数是C++11引入的新特性,它接受一个右值引用作为参数,并使用该参数的值来初始化新对象。移动构造函数通常用于在对象的资源所有权转移时提高性能。
构造函数的调用规则如下:
- 当创建对象时,会根据提供的参数类型和数量来选择合适的构造函数进行调用。如果没有提供参数,则会调用默认构造函数。
- 当使用一个对象来初始化另一个对象时,会调用拷贝构造函数。
- 当将对象作为函数参数传递给函数时,会调用拷贝构造函数。
- 当从函数返回对象时,会调用拷贝构造函数。
- 在使用类对象进行赋值操作时,也会调用拷贝构造函数。
- 在某些情况下,编译器会进行优化,避免不必要的拷贝构造函数的调用。这种优化称为“拷贝消除”(copy elision)。
五、深拷贝和浅拷贝
浅拷贝是指将一个对象的值复制到另一个对象,包括对象的成员变量。这意味着两个对象共享相同的内存地址,对其中一个对象的修改会影响到另一个对象。浅拷贝只复制了对象的表面层次,没有复制对象所拥有的资源。
深拷贝是指将一个对象的值复制到另一个对象,并且为新对象分配独立的内存空间。这样两个对象就拥有了彼此独立的内存空间,对其中一个对象的修改不会影响到另一个对象。深拷贝会递归地复制对象的所有成员变量,包括对象所拥有的资源。
示例代码:
#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: 25person2.printInfo(); // 输出:Name: Alice, Age: 25person2.printInfo(); // 输出:Name: Alice, Age: 25person2.printInfo(); // 输出:Name: Alice, Age: 25person1.printInfo(); // 输出:Name: Bob, Age: 30person2.printInfo(); // 输出:Name: Alice, Age: 25return 0;
}
在上面的示例中,我们定义了一个Person
类,其中包含一个字符串类型的成员变量m_name
和一个整型的成员变量m_age
。在构造函数中,我们使用new
运算符为m_name
分配了独立的内存空间,并将字符串复制到该内存空间中。
然后,我们创建了一个person1
对象,并将其值赋给person2
对象。由于默认的拷贝构造函数是浅拷贝,所以person2
对象的m_name
指针指向了与person1
对象相同的内存空间。当我们修改person2
对象的m_name
时,实际上也会修改person1
对象的m_name
。这就是浅拷贝的特点。
为了实现深拷贝,我们需要自定义拷贝构造函数,并在其中为m_name
分配独立的内存空间,并将字符串复制到该空间中。这样,person2
对象就拥有了自己独立的m_name
内存空间,对其进行修改不会影响到person1
对象。
总结起来,浅拷贝只复制对象的表面层次,而深拷贝会递归地复制对象的所有成员变量,包括对象所拥有的资源。深拷贝需要自定义拷贝构造函数来实现。
六、初始化列表
初始化列表是一种在构造函数中初始化成员变量的方法,可以用于实现深拷贝。
在上面的示例中,我们可以使用初始化列表来实现深拷贝,而不需要在拷贝构造函数中手动分配内存和复制字符串。
下面是使用初始化列表实现深拷贝的示例:
#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: 25person2.printInfo(); // 输出:Name: Alice, Age: 25person2.printInfo(); // 输出:Name: Alice, Age: 25person2.printInfo(); // 输出:Name: Alice, Age: 25person1.printInfo(); // 输出:Name: Alice, Age: 25person2.printInfo(); // 输出:Name: Alice, Age: 25return 0;
}
在上面的示例中,我们在构造函数的初始化列表中分配了独立的内存空间,并将字符串复制到该空间中。这样,person2
对象就拥有了自己独立的m_name
内存空间,对其进行修改不会影响到person1
对象。
使用初始化列表可以简化代码,并且可以确保在对象构造时成员变量已经正确初始化。这对于实现深拷贝非常有用。
七、类对象作为类成员
当一个类的成员变量是另一个类的对象时,我们需要在拷贝构造函数中正确地拷贝这些成员变量。
以下是一个示例,其中Person
类的一个成员变量是Address
类的对象:
#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: Broadwayperson2.printInfo(); // 输出:Name: Alice, Age: 25, City: New York, Street: Broadwayreturn 0;
}
在上面的示例中,Person
类的一个成员变量是Address
类的对象。在Person
类的拷贝构造函数中,我们使用拷贝构造函数来正确地拷贝Address
对象。这样,当我们拷贝一个Person
对象时,Person
对象和其成员变量Address
对象都会进行深拷贝。
需要注意的是,在Person
类的析构函数中,我们只需要释放m_name
成员变量的内存空间,因为m_address
成员变量的内存空间会在Address
类的析构函数中释放。
总结起来,当一个类的成员变量是另一个类的对象时,我们需要在拷贝构造函数中正确地拷贝这些成员变量,以实现深拷贝。
八、静态成员
静态成员变量是属于类本身而不是类的实例的。因此,在拷贝构造函数中不需要拷贝静态成员变量,因为它们在所有类的实例之间是共享的。
以下是一个示例,其中Person
类有一个静态成员变量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: 2return 0;
}
在上面的示例中,Person
类有一个静态成员变量count
,用于记录创建的Person
对象的数量。在构造函数中,我们通过递增count
来跟踪对象的数量,在析构函数中通过递减count
来更新对象的数量。
在拷贝构造函数中,我们不需要拷贝静态成员变量count
,因为它是属于类本身而不是类的实例。因此,在拷贝构造函数中只需要拷贝非静态成员变量即可。
总结起来,静态成员变量不需要在拷贝构造函数中进行拷贝,因为它们是属于类本身而不是类的实例。