类名(){}
构造函数是为了初始化,
构造函数可以用装修来类比,一个类就像一个毛坯房,占据了一块地皮(内存)。默认构造函数就是开发商给它来装修,给它一个默认的初始化。而显式地定义一个构造函数,就是由业主(我们)自己去装修。当然也可以不装修,叙利亚风也是一种装修。那么这就是一种空的构造函数。而在一些情况下,我们必须对一些成员赋初值,如const、引用等类型的成员。构造函数主要是用来初始化对象的非static数据成员,也可以做其他的工作,比如输出一些信息等。
~类名(){}
析构函数那就是拆迁了,销毁对象的非static数据成员。没有返回值,不接受参数。因此也不能重载,对于一个类,只会有唯一一个析构函数。成员按照初始化顺序,逆序销毁。
为什么需要析构函数。
object* o1 = new object();
o1
是一个指向 object
类型对象的指针。它被放置在栈内存中,存储的是 object
类型对象在堆内存中的地址。
*o1
表示通过指针 o1
解引用来访问实际的对象。这个对象的内存是通过 new
运算符从堆上分配的,并在堆内存中创建。堆上的对象会一直存在,直到显式调用 delete
运算符释放内存。
当超出指针 o1
的作用域(比如 }
)时,指针变量 o1
会被释放,即栈上的内存被释放。但是,指针所指向的堆上的对象并不会自动释放。如果不主动释放这块内存(通过 delete
运算符),就会导致内存泄漏,即一块无法访问到的堆内存。而为了避免这一错误,我们常常在析构函数中将它delete
掉。
当我们创建一个新对象并将其初始化为另一个对象的副本时,就会使用拷贝构造函数。
class MyClass {
public:
// 拷贝构造函数
MyClass(const MyClass& obj) {
// 复制 obj 的成员变量到 this 对象
}
};
浅拷贝就是简单的复制,包括指针变量,但是指针所指向的对象不会复制。这就像你有一个钥匙(指针),我拷贝了你的钥匙,也有了一把钥匙。但是这两把钥匙都只能开一个房子的大门(指针指向的对象)。这就会产生一个现象,你通过钥匙对这个房子做了改变,我通过钥匙也会对这个房子做出改变,从而产生问题。如果你把房子卖给了别人(析构函数,内存释放),把钥匙也给了新的户主,但是我下次还可以拿着这把钥匙(悬空指针)进这个房子,这就出现问题了。
深拷贝是一种复杂的复制,它的复制包括了指针以及指针所指向的对象。这就变成了你有一套房子,我也有一套跟你一样的房子。但是之后你和我对房子的装修就是独立的了。而深拷贝也要注意每个对象之间的指针是否被正确地复制和释放,以防止内存泄漏或者重复释放。
object::object(const object& other)
{
m_ptr = new int(*other.m_ptr);
}
object::~object()
{
if (m_ptr != NULL)
{
delete m_ptr;
}
}
为了确保只释放已分配的内存,需要在析构函数中添加判断语句,避免对空指针进行delete
操作。
#include
class MyClass {
public:
int* value;
// 默认构造函数
MyClass() : value(new int(0)) {}
// 拷贝构造函数(浅拷贝)
// MyClass(const MyClass& obj) : value(obj.value) {}
// 拷贝构造函数(深拷贝)
MyClass(const MyClass& obj) : value(new int(*obj.value)) {}
// 析构函数
~MyClass() {
delete value;
}
};
int main() {
MyClass obj1;
*(obj1.value) = 42;
// 浅拷贝
MyClass obj2 = obj1;
std::cout << "obj1.value = " << *(obj1.value) << std::endl;
std::cout << "obj2.value = " << *(obj2.value) << std::endl;
// 深拷贝
MyClass obj3(obj1);
*(obj3.value) = 100;
std::cout << "obj1.value = " << *(obj1.value) << std::endl;
std::cout << "obj3.value = " << *(obj3.value) << std::endl;
return 0;
}
在上面的例子中,我们创建了一个 MyClass 对象,并将其值设置为 42。然后,我们使用浅拷贝和深拷贝分别创建了两个新对象 obj2 和 obj3,并将它们的值设置为 42 和 100。最后,我们输出了三个对象的值,发现浅拷贝后的 obj2 和原始对象 obj1 共享同一个指针所指向的对象,而深拷贝后的 obj3 拥有一个新的指针所指向的新对象。
虚函数在基类中声明,并可以在派生类中被重写。它们被称为虚函数,是因为它们可以通过基类指针或引用来调用,并根据对象的实际类型来动态解析执行的函数。目的是实现运行时多态性,允许在继承关系中根据对象的实际类型来动态解析并调用适当的函数实现。
虚函数相当于给函数打上一个标签(virtual),如果父类函数有这么一个标志,这说明子类大概率会重写这个函数。有时候在声明一个对象的时候,无法立即知道它的真实类型,这个类型可能在程序运行时才能确定下来。因此,在实际的任务中为了方便,我们通常用父类去定义一个指针,而实际类型则是一个子类。
class Person {
public:
virtual void introduce() {
cout << "I am a person." << endl;
}
};
class A : public Person {
public:
void introduce() {
cout << "I am from class A." << endl;
}
};
class B : public Person {
public:
void introduce() {
cout << "I am from class B." << endl;
}
};
Person * p;
int sel=0;
cin>>sel;
if (sel==1){
p =new A;
}
else{
p = new B;
}
C11新特性会在子类重写的函数中也加上一个标签(override),表明该方法已经重写了。
当然,既然是标签(virtual、override),也可以选择不写。只是写上可以增加代码的可读性。
不写virtual,编译器就不会将函数认为是虚函数,在这种情况下,即使通过基类指针或引用调用该函数,也不会发生动态绑定和运行时的多态性。
实际上,将函数声明为虚函数,编译器会生成一个虚函数表(vtable),其中包含用于动态解析的函数指针。通过基类指针或引用调用虚函数时,会根据对象的实际类型查找并调用正确的函数实现。
另外,如果父类的一个函数是虚函数,则子类的这个函数一定是虚函数。这个子类虚函数可以不用写virtual来表明它是虚函数了。
当类中有了纯虚函数,这个类也称为抽象类。抽象类主要是面向对象编程中多态的思想实现。
virtual void func() = 0; //纯虚函数
抽象类特点:
子类继承父类,有时候会新增加一些属性或者功能,而这些属性和功能有可能会开辟在堆区。
那么在使用完后,需要手动删除它们。而前面说到,有时候我们会用父类去定义一个指针,而实际类型则是一个子类。那么就有可能出现把父类在堆区的空间删除了,而子类没删除的问题。针对这一问题,我们通过虚析构来解决通过父类指针释放子类对象。
class Base {
public:
virtual ~Base() {
// 在父类析构函数中执行相应的清理操作
// ...
}
};
class Derived : public Base {
public:
Derived(string name){
m_Name = new string(name);
}
~Derived() {
// 在子类析构函数中执行相应的清理操作
// ...
if (this->m_Name != NULL) {
delete m_Name;
m_Name = NULL;
}
string *m_Name;
};
int main() {
Base* ptr = new Derived(); // 使用父类指针指向子类对象
// 使用ptr进行一些操作
delete ptr; // 通过父类指针释放子类对象,会自动调用虚析构函数
return 0;
}
在上面的示例中,基类 Base
的析构函数被声明为虚函数,而子类 Derived
的析构函数会自动地成为虚函数。因此,当使用父类指针 ptr
删除子类对象时,会调用子类 Derived
的析构函数,从而确保子类对象的资源得以释放。
当指针指向的对象被提前释放或删除,并且指针没有被设置为 nullptr
,就会出现悬空指针错误。这种情况下,指针仍然保持着之前指向的内存地址,但该地址上的对象已经不存在。
int* ptr;
{
int* num = new int(10); // 在堆上分配一个整型变量
ptr = num; // ptr指向了堆上的变量num
delete num; // 提前释放了num指向的内存
} // num超出作用域,但ptr仍然指向先前分配的内存
// 在超出作用域后,ptr成为了悬空指针
// 尝试使用悬空指针
int value = *ptr; // 读取悬空指针的值
// 上述代码会导致未定义的行为,因为ptr指向的内存已经被释放
我们声明了一个指针 ptr
,然后在一个作用域中创建了一个整型变量 num
并将 ptr
指向了 num
的地址。然而,当 num
超出作用域时,其内存被释放,并且 ptr
指向的内存变成了无效的。当我们尝试使用悬空指针 ptr
时,会导致未定义的行为。
RALL原则是资源获取即初始化(Resource Acquisition Is Initialization)的缩写,涉及到对象的生命周期和资源管理。该原则指出,当一个对象被创建时,它应该立即获取所有需要的资源,而当对象被销毁时,应该立即释放这些资源,以确保资源的正确管理和使用。
我们通常使用构造函数和析构函数来实现RALL原则。构造函数用于初始化对象的状态,并获取所需的资源,而析构函数则用于释放这些资源,并清理对象的状态。例如,在使用动态内存分配时,我们可以在构造函数中调用new运算符分配内存,而在析构函数中调用delete运算符释放内存,以确保内存的正确管理。当然,在C11新特性中,可以使用智能指针。
看到一个不错的理解方式,出处:知乎(https://www.zhihu.com/question/396004298/answer/1236427106)
你买了套房,住进去之前得先装修。
你买个硬盘,用之前得先分区。
你买个手机,用之前得先装上sim卡、下载一些必要软件、注册/登录微信/支付宝账户。
创建一个对象也一样:你得到了一块内存;这块内存可能是“二手房”,前任留下的shit什么的都还留在里面,你得先清理(把内容置零)、重新装修(设置一些基础信息)之后才能入住。
过去,C时代,这些都得你自己照应。如果你忘了,那么访问了未初始化存储区、读出乱七八糟的东西,你就自认倒霉吧。
C++时代,人们变聪明了:既然装修是入住前的必要步骤,我干脆把它固定到你的《购房流程指导书》里算了。你交钱买房后,就会有人领你看房、给你谈装修事宜。
这个固定的、执行装修事宜的步骤就是构造函数。
用伪码表示的话,对象创建流程是这样的:
1、用各种奇怪的方式得到一块内存
2、执行构造函数,“装修”这块内存
3、拎包入住
每个人都有自己独特的口味,每个用户自定义对象也有不同的初始化流程。
因此,C++做了一个约定:和类名相同的无返回函数就是它的初始化函数(构造函数),编译器保证在创建一个对象之后、允许你使用它之前,它必定会在这个对象对应的内存上执行构造函数,按你的要求把对象装修好。如果你不写,那么它默认给你个毛坯房(这就是所谓的“默认构造函数”)。
等你有了一定的开发经验,那么一定经常听到“(资源)谁申请谁释放”原则。基于这个原则才能清晰、准确的界定资源的生存期、控制权。
而RALL天然保证了这个原则被严格执行:如果任何类/对象都严格的管好自己申请的资源、并在析构时确保这些资源被无遗漏的归还;那么对一个熟练掌握了RAII的程序员来说,只要一个对象的生存期、所有权、引用关系(计数)在设计之初都理清楚了,资源泄露就是不可能的。
为了清晰表达“所有权转移、复制”等相关语义,C++标准库才提供了shared_ptr、unique_ptr、weak_ptr等“智能指针”;更有趣的是,这些“智能指针”同样是借助于有保障的构造/析构函数的自动调用机制设计的。你必须先透彻理解构造/析构函数,才有可能明白它们的工作原理、甚至自己实现它们(没错,过去那个C++标准化/STL库总是跟不上趟的年代里,很多程序员在自己的工程里手工编写过shared_ptr)。
四个区也是类似这个思想。
构造函数叫“恢复出厂设置”;
拷贝构造函数叫“一键换机”。