今天复习C++中类的构造函数、拷贝构造、移动拷贝构造三个函数,其中也会涉及到 嵌套类和嵌套结构体
push_back(value):在 vector 的末尾添加一个元素。
emplace_back(args...):在末尾直接构造元素,避免临时对象。
起初是在复习vector的时候发现有这么两个函数,都是在末尾添加元素,为什么会有两个??
之后发现



这时候对 移动拷贝构造有了兴趣??
“移动构造”的确涉及在新位置上构造对象,但它的目的是避免传统的拷贝操作,以提高效率。
传统拷贝构造的开销
在传统的拷贝构造中,构造一个新对象时会复制原对象的每一个成员。例如,对于一个大型容器对象(如 std::vector),拷贝构造会将所有元素从原对象中复制到新对象,这会产生较大的性能开销。
移动构造的概念
移动构造则提供了一种更高效的方式来构造对象,尤其是当源对象即将被销毁,且我们只需要其资源时。移动构造的关键点是**“资源转移”**:它将源对象的资源(例如内存、文件句柄、指针等)直接转移到目标对象,而不是复制。这避免了不必要的数据复制。
移动构造的过程
当调用移动构造函数时:
分配目标对象的空间:为新对象分配内存空间。
直接“窃取”资源:将源对象的资源直接“窃取”到新对象中,而不需要复制。例如,在 std::vector 的移动构造中,目标 vector 会直接接管源 vector 的内部指针。
源对象置于“空”状态:源对象通常被置于一个有效但空的状态,具体取决于类型,比如将指针置为 nullptr 或大小设置为 0。这确保源对象可以被安全销毁而不影响目标对象的资源。
所以你可以理解为 移动拷贝构造就是 比如说 有一个对象b,你进行了初始化,然后你又构造了一个对象c,如果使用移动拷贝构造的话,其实就是相当于把b的资源直接拿过来,就是把c指向的地址指向b,然后把b清空掉(直接指针nullptr就可以),其实也就是相当于给b换了个名字c,然后c就顺理成章的构造完成了,这个会大大降低构造的时间,因为不需要额外的空间。
书归正传!
构造函数!
构造函数是一种特殊的成员函数,用于在创建对象时初始化对象的状态。构造函数的名称必须与类名相同,并且没有返回类型。
在 C++ 中,当我们创建一个对象时,系统会自动调用构造函数来完成初始化。
说白了,其实就是为了让初始化更方便。
class MyClass {
public:
int data;
// 构造函数
MyClass(int value) : data(value) {
cout << "Constructor called with value = " << value << endl;
}
};
int main() {
MyClass obj(10); // 自动调用构造函数,将 data 初始化为 10
MyClass obj1;
obj1.data=20;
return 0;
}
这个类里边目前只有一个data,我们自己也能手动初始化,但是如果有很多呢,岂不是很麻烦,所以你写一个构造函数,这个你可以直接通过传参进行初始化,另一方面也是保证你在创建对象之后,你的对象成员都是被初始化的,不会导致不可预测的行为。
当然,如果我们定义的类里边没有写构造函数,那么在运行时编译器会自动生成一个构造函数。
这是官方说法。。
初始化对象成员:
当创建对象时,构造函数可以确保对象的每个成员变量都被初始化。
没有构造函数的情况下,对象的成员变量可能未被初始化,导致不可预测的行为。
封装初始化逻辑:
构造函数可以封装初始化逻辑,使得每次创建对象时都会自动执行,而不需要额外编写初始化代码。
例如,如果对象包含指针或动态内存,构造函数可以负责分配这些内存。
提高代码可读性和安全性:
使用构造函数,创建对象时的初始化过程更加清晰,一目了然。
构造函数能够确保对象创建时状态一致,从而减少由于未初始化变量带来的错误。
自动化初始化流程:
在 C++ 中,创建对象是不可避免的操作,每次创建对象时手动初始化显然不合理。构造函数自动完成初始化流程,可以极大地提高代码效率和安全性。
构造函数的主要作用是初始化对象,而不是计算结果或返回值。创建对象的过程包括为对象分配内存、初始化成员变量等,构造函数是这一步骤的关键部分,但它不会返回一个结果。因此,构造函数没有返回类型。
举例来说,当我们写 MyClass a; 时,编译器自动调用构造函数来初始化 a,并不需要返回值来判断是否初始化成功。
接着我们来了解
拷贝构造函数
拷贝构造函数是用于通过已有对象初始化新对象的构造函数
这是它的定义形式:
ClassName(const ClassName& other);
拷贝构造函数的名字也必须和类名一样,然后 里边的参数 ClassName 是表示这个类型(也就是类名),在 const MyClass& other 中,other 是参数的名字,也就是另一个 ClassName 类型的对象(通常是已经存在的对象),我们将通过拷贝构造函数用它来初始化新的对象。
& 在这里的作用
这里的 & 表示引用,而不是指针。
引用的作用是让 other 作为一种别名引用已有对象,而不是创建新的副本。
使用引用可以避免不必要的数据复制,并提高效率。
const 表示常量性,在这里的作用是:
保护 other 不被修改。因为我们只是用 other 来复制它的数据,而不是修改它。
加上 const 可以使这个拷贝构造函数适用于常量对象,也就是说,即使 other 是一个 const MyClass 对象,也可以调用这个拷贝构造函数。
综上所述,const MyClass& other 的含义就是:
other 是一个对现有 MyClass 对象的常量引用,我们可以读取 other 的数据成员,但不能修改它。
& 表示引用(而非解引用),它提供了对 other 的别名,而不会复制 other 的数据。
const 确保 other 是不可变的,因此在拷贝构造函数内部不能改变 other 的状态。
我现在有些理解了,其实拷贝构造函数,首先当然出现是为了提供一种方便的初始化方法,如果原有一个已经初始化完成的对象a,然后我们定义一个对象b但是要和a一样,那么如果你一个一个用语句复制的话就太麻烦了,所以就出现的拷贝构造,也是为了更方便的初始化对象。但是如果类中只是简简单单的包含一些普通数据变量,其实也无所谓,但是更多时候,类中是有指针的,简单的浅拷贝,可能就是让新的指针和原有的指针指向同一块内存,这就会导致如果a释放了,那么b指针指向的内容可能就没了。所以我们需要通过深拷贝重新为指针申请一块新的空间,然后把原有的指针指向的内容复制过来。
拷贝构造函数的内存变化原理
当调用拷贝构造函数时,新的对象会被创建,其成员变量会逐一从源对象复制过来。对于包含指针的对象,如果不进行深拷贝(即只是浅拷贝),两个对象会共享同一块内存地址,从而导致潜在的内存管理问题。因此,拷贝构造函数的实现和内存变化分为浅拷贝和深拷贝两种情况。
浅拷贝
浅拷贝通过逐一复制每个成员变量的内存内容来实现。如果成员变量是指针,浅拷贝只复制指针的地址,使两个对象共享同一块内存。
内存变化:
直接复制:对于普通成员变量,直接将源对象的值复制到新对象的相应成员内存位置。
指针成员:指针成员的地址被简单地复制,导致两个对象指向同一内存地址。
浅拷贝的问题在于,如果一个对象释放了指针指向的内存,另一个对象的指针会变成悬空指针,可能导致内存错误。
深拷贝
深拷贝通过拷贝构造函数,在堆上为指针成员分配新内存,并将源对象的数据复制到新分配的内存中。这种方式确保每个对象的指针成员独立存在。
内存变化:
普通成员变量:与浅拷贝相同,直接复制数据。
指针成员:为新对象的指针成员重新分配内存,将源对象的指针指向的值复制到新的内存中,确保两个对象的指针各自指向独立的内存。
移动拷贝构造函数
其实就是为了解决上边那个拷贝构造函数的在复杂对象中会非常慢的问题,当然也有其他的好处。
如果是显式调用的话,看我下边的代码就行
他用到了 一个专有的 右值引用 T&& 这个符号
Engine(Engine&& other) noexcept : horsepower(std::move(other.horsepower)) {
cout << "Engine move constructed with horsepower = " << horsepower << endl;
other.horsepower = 0;
}
大概就是这样,然后调用会需要move函数的使用。
下面我主要是想说一下 就是编译器自动调用的几种情况
**1.右值或将亡值赋给对象:**当一个右值(即临时对象)被赋值给一个新对象时,编译器会自动调用移动构造函数,而不是拷贝构造函数。例如:
MyClass obj = MyClass(10); // MyClass(10) 是右值,会自动调用移动构造
**2.函数返回右值:**如果一个函数返回一个对象的右值,接收这个返回值的对象将会调用移动构造函数。例如:
MyClass createObject() {
return MyClass(20); // 返回右值
}
int main() {
MyClass obj = createObject(); // 自动调用移动构造函数
}
**3.std::move 显式转移资源:**当使用 std::move 将一个左值显式转换为右值时,编译器会选择调用移动构造函数。例如:
MyClass obj1(30);
MyClass obj2 = std::move(obj1); // 使用 std::move 将 obj1 转为右值,调用移动构造
介绍一下 临时对象, 将亡值,右值,
1.临时对象
临时对象是指在表达式计算过程中短暂存在的对象,这些对象会在表达式结束后马上销毁。例如:
函数返回值:函数返回一个对象的情况。
类型转换:在类型转换过程中创建的中间对象。
匿名对象:没有绑定到任何变量的对象。
临时对象通常是右值,因此也被称为右值对象。右值对象无法通过普通的左值引用绑定,只能通过右值引用(T&&)进行绑定和操作。
2. 生命周期
临时对象的生命周期非常短暂,通常只在表达式的计算过程中存在。例如:
在函数返回一个对象时,返回的对象是一个临时对象。
在类型转换或运算符重载中产生的匿名对象,也是临时对象。
临时对象的生命周期结束时会自动调用析构函数释放资源。
3.将亡值
**将亡值(xvalue,expiring value)**是指那些即将被销毁的值。例如临时对象(如函数返回的对象)就是将亡值。C++11 引入了右值引用(&&)以便操作这些将亡值,特别是通过移动构造函数和移动赋值运算符来优化对象的资源管理。
#include <iostream>
#include <string>
using namespace std;
class Car {
public:
string brand;
int year;
struct inner_struct {
int inner;
} in_struct;
// 嵌套类 Engine,用于表示引擎信息
class Engine {
public:
int horsepower;
// Engine 的构造函数
Engine(int hp) : horsepower(hp) {
cout << "Engine constructed with horsepower = " << horsepower << endl;
}
// Engine 的拷贝构造函数
Engine(const Engine& other) : horsepower(other.horsepower) {
cout << "Engine copy constructed with horsepower = " << horsepower << endl;
}
// Engine 的移动构造函数
Engine(Engine&& other) noexcept : horsepower(std::move(other.horsepower)) {
cout << "Engine move constructed with horsepower = " << horsepower << endl;
other.horsepower = 0;
}
};
Engine engine; // Car 类包含一个 Engine 对象
// Car 的构造函数
Car(const string& b, int y, int c, int hp) : brand(b), year(y), in_struct{ c }, engine(hp) {
cout << "Car constructed with brand = " << brand << ", year = " << year << endl;
}
// Car 的拷贝构造函数
Car(const Car& other) : brand(other.brand), year(other.year), in_struct{ other.in_struct.inner }, engine(other.engine) {
cout << "Car copy constructed with brand = " << brand << ", year = " << year << endl;
}
// Car 的移动构造函数
Car(Car&& other) noexcept : brand(std::move(other.brand)), year(std::move(other.year)), in_struct{ other.in_struct.inner }, engine(std::move(other.engine)) {
cout << "Car move constructed with brand = " << brand << ", year = " << year << endl;
// 将源对象置为空状态
other.brand.clear(); // 清空品牌名称
other.year = 0; // 设置年份为 0
other.in_struct.inner = 0; // 设置结构体数据为 0
}
void showCarInfo() const {
cout << "Brand: " << brand << ", Year: " << year << ", Number: " << in_struct.inner << ", Horsepower: " << engine.horsepower << endl;
}
};
int main() {
Car car1("Toyota", 2021, 1108, 300); // 调用 Car 的普通构造函数
car1.showCarInfo();
Car car2 = car1; // 调用 Car 的拷贝构造函数
car2.showCarInfo();
Car car3 = std::move(car1); // 调用 Car 的移动构造函数
car3.showCarInfo();
// 注意:car1 被移动后处于“空”状态,内容不确定
car1.showCarInfo();
cout << sizeof(car1) << endl;
return 0;
}
new和delete
1.1. 使用 new 动态分配单个变量
new 可以用于在堆区分配一个基本数据类型或对象,并返回一个指向这块内存的指针。适用于我们只需要动态分配一个单一对象的情况。
int* p = new int(5); // 动态分配一个 int 并初始化为 5
cout << "Value: " << *p << endl; // 输出 5
delete p; // 使用 delete 释放内存
- 使用 new 动态分配数组
new 还可以用于动态分配数组,适用于在程序运行时动态确定数组大小的情况。
int size = 10;
int* arr = new int[size]; // 动态分配一个大小为 10 的 int 数组
// 使用数组
for (int i = 0; i < size; ++i) {
arr[i] = i * 2; // 初始化数组元素
}
// 输出数组元素
for (int i = 0; i < size; ++i) {
cout << arr[i] << " ";
}
cout << endl;
delete[] arr; // 使用 delete[] 释放数组内存
- 使用 new 动态分配对象
在 C++ 中,new 也可以用于动态分配类对象。这在处理包含指针成员的类时尤为常见,因为对象的内存大小通常在编译时确定。
class MyClass {
public:
MyClass() { cout << "MyClass constructor called" << endl; }
~MyClass() { cout << "MyClass destructor called" << endl; }
};
int main() {
MyClass* obj = new MyClass(); // 动态分配一个 MyClass 对象
delete obj; // 使用 delete 释放对象内存
return 0;
}