在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: 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;
}
在上面的示例中,我们定义了一个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: 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;
}
在上面的示例中,我们在构造函数的初始化列表中分配了独立的内存空间,并将字符串复制到该空间中。这样,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: Broadway
person2.printInfo(); // 输出:Name: Alice, Age: 25, City: New York, Street: Broadway
return 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: 2
return 0;
}
在上面的示例中,Person类有一个静态成员变量count,用于记录创建的Person对象的数量。在构造函数中,我们通过递增count来跟踪对象的数量,在析构函数中通过递减count来更新对象的数量。
在拷贝构造函数中,我们不需要拷贝静态成员变量count,因为它是属于类本身而不是类的实例。因此,在拷贝构造函数中只需要拷贝非静态成员变量即可。
总结起来,静态成员变量不需要在拷贝构造函数中进行拷贝,因为它们是属于类本身而不是类的实例。




















