一、构造函数简介
在 C++ 中,构造函数是一种特殊的成员函数,它会在创建类的新对象时自动被调用。构造函数的主要目的是初始化类的对象。
以下是关于构造函数的一些重要信息:
1、构造函数的名称:构造函数的名称与类名完全相同。
2、自动调用:不需要显式调用构造函数,当创建对象时它会自动被调用。同样,当对象超出其生命周期时,析构函数(名称为类名前有波浪线 ~
的函数)会自动被调用。
3、没有返回类型:构造函数没有返回类型,甚至连 void
都没有。
4、可以被重载:就像其他函数一样,构造函数也可以被重载,只要参数的数量或类型不同即可。
5、可以有默认参数:构造函数可以有默认参数。如果没有提供参数,将使用这些默认值。
6、类型构造函数:如果构造函数只有一个参数,它还可以用于隐式转换。
例如:
class Rectangle {
public:
int width;
int height;
// 这是一个构造函数
Rectangle(int w, int h) {
width = w;
height = h;
}
};
// 使用构造函数创建 Rectangle 对象
Rectangle rect(10, 5);
在这个例子中,Rectangle
类有一个构造函数,该构造函数接受两个参数 w
和 h
,并将这些参数的值分别赋给成员变量 width
和 height
。然后在创建 Rectangle
对象 rect
时,我们使用构造函数并提供了 width
和 height
的值。
二、构造函数:类型构造函数
类型构造函数(也被称为转换构造函数)是一个特殊类型的构造函数,它只接受一个参数。类型构造函数允许对象在初始化或赋值时进行隐式转换为其类的类型。这种转换构造函数定义了一种从一个特定类型到类类型的转换方式。
例如,假设我们有一个名为 Fraction
的类,它表示一个分数,有一个分子和一个分母。我们可能希望能够直接从一个整数创建一个 Fraction
,此时分子是这个整数,分母是 1。我们可以通过定义一个接受一个 int
参数的构造函数来实现这一点:
class Fraction {
private:
int numerator;
int denominator;
public:
// 类型构造函数
Fraction(int num) : numerator(num), denominator(1) {}
// 通常的构造函数
Fraction(int num, int denom) : numerator(num), denominator(denom) {}
// 其他成员函数...
};
有了这个类型构造函数,我们就可以这样初始化一个 Fraction
对象:
Fraction frac1 = 5; // 使用类型构造函数,等价于 Fraction frac1(5);
Fraction frac2(7, 2); // 使用通常的构造函数
在上面的代码中,frac1
是通过一个整数 5
初始化的。编译器使用类型构造函数将这个整数隐式转换为 Fraction
类型。
需要注意的是,这种隐式转换可能会引起一些意料之外的行为,因此如果你不想让构造函数进行隐式转换,你可以在构造函数前加上 explicit
关键字,这样就要求必须显式地进行转换,变为显示构造函数。例如:
class Fraction {
private:
int numerator;
int denominator;
public:
// 显式类型构造函数
explicit Fraction(int num) : numerator(num), denominator(1) {}
// 通常的构造函数
Fraction(int num, int denom) : numerator(num), denominator(denom) {}
// 其他成员函数...
};
在这个例子中,下面的代码将不再被允许,因为我们已经阻止了隐式转换:
Fraction frac1 = 5; // 编译错误:不能隐式转换
如果我们想要从整数创建一个 Fraction
对象,我们必须显式地调用构造函数:
Fraction frac1(5); // OK:显式地调用构造函数
三、构造函数:拷贝构造函数
在 C++ 中,拷贝构造函数是一种特殊的构造函数,用于创建一个对象的新副本。拷贝构造函数接受一个同类型的对象的引用作为参数,然后复制这个对象的值。
以下是关于拷贝构造函数的一些重要信息:
拷贝构造函数的声明:拷贝构造函数的一般形式为 ClassName(const ClassName& obj)
,其中 ClassName
是类的名称,obj
是传递的同类型对象的引用。
何时调用:拷贝构造函数在以下情况下会被调用:当一个新对象作为已存在对象的副本创建时;当一个对象作为函数的参数通过值传递时;当函数返回一个对象时。
默认拷贝构造函数:如果你没有为类定义一个拷贝构造函数,编译器会自动为你生成一个默认的拷贝构造函数。这个默认的拷贝构造函数会执行所有成员变量的浅拷贝。如果类的成员包括指针或者动态分配的内存,你可能需要定义自己的拷贝构造函数,以执行深拷贝。
以下是一个包含拷贝构造函数的简单类的例子:
class MyClass {
private:
int* data;
public:
// 构造函数,用于初始化动态分配的内存
MyClass(int size) {
data = new int[size];
// 初始化数据...
}
// 拷贝构造函数
MyClass(const MyClass& other) {
// 这是一个简单的例子,实际情况可能更复杂
// 你可能需要复制 'other.data' 中的实际数据
data = new int[sizeof(other.data)];
memcpy(data, other.data, sizeof(other.data));
}
// 析构函数,用于释放动态分配的内存
~MyClass() {
delete[] data;
}
// 其他成员函数...
};
在这个例子中,MyClass
有一个动态分配的 int
数组成员 data
。这个类的拷贝构造函数会创建一个新的 int
数组,并将原对象的 data
成员的内容复制到新数组中。这就是所谓的深拷贝,因为它复制了数据本身,而不仅仅是指针。
请注意,如果你的类有动态分配的内存或其他需要特别处理的资源,你可能需要定义自己的拷贝构造函数,以确保正确地复制这些资源。否则,如果你依赖编译器自动生成的默认拷贝构造函数,那么可能只会进行浅拷贝。这可能会导致一些问题,比如内存泄露或悬挂指针。
深拷贝与浅拷贝:在 C++ 中,深拷贝和浅拷贝是两种不同的对象复制方式,它们的主要区别在于如何处理对象的指针成员。
浅拷贝:当我们执行浅拷贝时,只复制对象的所有非静态成员变量到新的对象。如果成员变量中包含指针,则复制的是指针值(也就是地址),而不是指针所指向的内容。这意味着原始对象和复制的对象会共享相同的动态内存。这可能会导致问题,例如,当一个对象被删除时,其析构函数可能会删除共享的内存,从而使另一个对象的指针变为悬挂指针。
深拷贝:当我们执行深拷贝时,不仅复制对象的所有非静态成员变量,还会为每个动态内存分配的指针成员创建新的内存复制,确保新对象获得的是原始对象数据的全新副本,而不是引用同一内存。这样,两个对象就不会共享内存,避免了浅拷贝可能出现的问题。
如果类的成员变量只包含基本类型或其他值语义类型(如 std::string
,std::vector
等),则通常可以依赖编译器生成的默认拷贝构造函数进行浅拷贝。但是,如果类有指向动态分配内存的指针成员,或者包含需要特殊处理的资源(如文件句柄,网络连接等),那么通常需要提供自定义的拷贝构造函数以执行深拷贝。否则,可能会遇到如内存泄露,悬挂指针等问题。
四、析构函数
在 C++ 中,析构函数是一种特殊的成员函数,用于在对象生命周期结束时进行清理工作。当一个对象即将被销毁,无论是因为它离开了其定义的范围,还是因为它是动态分配的内存并被 delete
,都会调用其析构函数。
以下是关于析构函数的一些重要信息:
析构函数的名称:析构函数的名称与类名相同,但前面有一个波浪号 ~
。
自动调用:不需要显式调用析构函数,它会在对象被销毁时自动被调用。
没有返回类型,没有参数:析构函数没有返回类型,也没有参数。这意味着你不能重载析构函数。
用途:析构函数通常用于释放对象可能拥有的资源。例如,如果对象有一个指向动态分配内存的指针成员,那么析构函数可能需要删除这块内存。如果不这样做,就可能导致内存泄漏。
下面是一个包含析构函数的简单类的例子:
class MyClass {
private:
int* data;
public:
// 构造函数,用于初始化动态分配的内存
MyClass(int size) {
data = new int[size];
// 初始化数据...
}
// 析构函数,用于释放动态分配的内存
~MyClass() {
delete[] data;
}
// 其他成员函数...
};
在这个例子中,MyClass
有一个动态分配的 int
数组成员 data
。当一个 MyClass
对象被销毁时,它的析构函数会删除这个数组,从而防止内存泄漏。
请注意,如果你的类有继承关系,析构函数通常应该被声明为 virtual
。这样,当删除一个指向派生类对象的基类指针时,会调用正确的析构函数。如果析构函数不是 virtual
,那么可能只会调用基类的析构函数,从而导致派生类的资源没有被正确清理。
五、构造函数和析构函数调用顺序
构造函数和析构函数的调用顺序在 C++ 中是非常明确的,并且与对象的创建和销毁密切相关。
1、构造函数的调用顺序:
首先,基类的构造函数被调用。
然后,按照它们在类定义中出现的顺序,依次调用派生类的成员变量的构造函数。
最后,调用派生类的构造函数。
2、析构函数的调用顺序:
析构函数的调用顺序与构造函数的调用顺序完全相反。
首先,派生类的析构函数被调用。
然后,按照它们在类定义中出现的顺序的相反顺序,依次调用派生类的成员变量的析构函数。
最后,调用基类的析构函数。
这种顺序确保了在对象的整个生命周期中,资源的获取和释放是正确和有效的。在构造阶段,首先构建基类和成员变量,然后再构建派生类。而在析构阶段,先销毁派生类,再销毁成员变量和基类,以防止在派生类的析构函数中可能仍然需要访问成员变量或基类的情况。
这里有一个简单的例子来说明这个顺序:
#include
class Base {
public:
Base() { std::cout << "Base Constructor\n"; }
virtual ~Base() { std::cout << "Base Destructor\n"; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived Constructor\n"; }
~Derived() { std::cout << "Derived Destructor\n"; }
};
int main() {
Derived d;
return 0;
}
这段代码的输出将是:
Base Constructor
Derived Constructor
Derived Destructor
Base Destructor
这个输出清晰地展示了构造函数和析构函数的调用顺序。