Java面试宝典:MongoDB实战技巧 作者:忆遂愿
https://cloud.tencent.com/developer/article/2466159?shareByChannel=link
文章对 MongoDB 知识的全面阐述,质量很高。从基本概念、Java 驱动使用、数据操作、安全性能问题与解决、数据一致性事务处理,到数据模型设计、技术集成和存储图片优势等方面讲解详细、条理清晰,体现出作者深入的理解。
如果一个类中什么成员都没有,简称为空类。
空类大小是1,占一个字节
class Date//空类
{
};
空类中真的什么都没有吗?
并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数(也就是说我们不写,编译器也会自动生成和调用)

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数的实际用处大吗?我们不妨先来看一下这样一段代码
#include<iostream>
using namespace std;
class Date
{
private:
int year, month, day;
public:
void SetDate()//SetDate方法用来给日期设置值
{
cin >> year >> month >> day;
}
void Show()//显示日期
{
cout << year << "/" << month << "/" << day << endl;
}
};
int main()
{
Date d1, d2,d3;//创建对象
d1.SetDate();
d1.Show();
d2.SetDate();
d2.Show();
d3.SetDate();
d3.Show();
return 0;
}
上面这段代码中的日期类对象的初始化赋值我们调用了类的成员函数SetDate(),上面只创建了3个对象,而每个对象都需要调用一次SetDate()方法才能完成赋值操作,那如果创建了N个对象呢,岂不是要调用N次的SetDate()方法。
我们再来看一下有显示构造函数的日期类,有什么不同?
#include<iostream>
using namespace std;
class Date
{
private:
int year, month, day;
public:
Date(int y = 0, int m = 0, int d = 0)//构造函数
//完成初始化类对象
{
year = y;
month = m;
day = d;
}
void SetDate()//SetDate方法用来给日期设置值
{
cin >> year >> month >> day;
}
void Show()//显示日期
{
cout << year << "/" << month << "/" << day << endl;
}
};
int main()
{
Date d1(2024,5,20), d2(2023, 5, 20), d3(2022, 5, 20);//创建对象
d1.Show();
d2.Show();
d3.Show();
return 0;
}
我们可以发现构造函数直接给我们创建的对象进行了赋初值,而且我们并不需要调用构造函数,在我们创建对象时,编译器会自动调用构造函数,我们不再需要每次都调用SetDate()方法才能进行日期的赋值。
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任
务并不是开空间创建对象,而是初始化对象。
特征如下
(1)函数名与类名相同
(2)没有返回值
(3)对象实例化时自动调用
(4)可重载(详细了解可阅读文章缺省参数和函数重载)
(5)初始化对象,不开空间
(6) 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成(不传参数的构造函数为默认构造函数),默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。
(7) 默认生成的构造函数,内置类型(如int、char、double)没有规定要处理(是否处理取决于编译器),自定义类型调用默认构造函数
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
#include<iostream>
using namespace std;
class Date
{
private:
int year, month, day;
public:
// 1.无参构造函数
Date() { ; }
// 2.带参构造函数
Date(int y, int m, int d )//构造函数
//完成初始化类对象
{
year = y;
month = m;
day = d;
}
void Show()//显示日期
{
cout << year << "/" << month << "/" << day << endl;
}
};
int main()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
d1.Show();
d2.Show();
//Date d3();
//d3.Show();
return 0;
}
在上面这段代码中无参构造函数是默认生成的构造函数,编译器没有处理内置类型,所以显示出来的日期是随机值
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
例如
class Date
{
public:
Date(int year, int month, int day)
{
year_ = year;
month_ = month;
day_ = day;
}
private:
int year_;
int month_;
int day_;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体 内可以多次赋值。
初始化列表:类对象成员定义初始化,可与函数体混合使用(先列表后函数体)。以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
//MyQueue类的构造函数
MyQueue(int n)
:_pushst(n)
,_Popst(n)
{//函数体}
为什么要把这一部分单独拿出来讲?是因为有些成员变量必须要在初始化列表进行初始化。
(1)必须要在初始化列表进行初始化的类成员 1、const成员变量,因为const只有在定义的时候有唯一一次初始化机会 2、&引用,引用必须初始化 3、没有默认构造的自定义类型成员(因为必须显示传参调用构造函数) (2)初始化列表,不管写没写,每个成员变量都会先走一遍。 自定义类型成员调用默认构造函数 内置类型有缺省值用缺省值,没有缺省值看编译器是否处理 (3)成员变量在类中声明的次序才是初始化列表中成员变量初始化的顺序 注:初始化列表中成员变量初始化的顺序,与成员变量在初始化列表中的先后位置无关
代码演示
#include <iostream>
using namespace std;
// 自定义类型,没有默认构造函数
class CustomType {
public:
//explicit 关键字作用阻止隐式类型转换
explicit CustomType(int value) : value_(value) {}
void print() const { cout << "CustomType value: " << value_ << endl; }
private:
int value_;
};
class MyClass {
public:
// 构造函数,使用初始化列表
MyClass(int a, int b, int c)
: const_member_(a) // const成员变量
,ref_member_(b) // 引用成员变量
,custom_member_(c) // 没有默认构造的自定义类型成员
{
// 函数体
}
// 成员函数
void printMembers() const
{
cout << "const_member_: " << const_member_ << endl;
cout << "ref_member_: " << ref_member_ << endl;
custom_member_.print();
}
private:
const int const_member_; // const成员变量
int& ref_member_; // 引用成员变量
CustomType custom_member_; // 没有默认构造的自定义类型成员
};
int main()
{
int x = 10;
MyClass obj(1, x, 20); // 使用初始化列表中的参数
obj.printMembers();
return 0;
}
再来看这样一段代码来验证:初始化列表中成员变量初始化的顺序,与成员变量在初始化列表中的先后位置无关
class A {
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
先用_a1用来给_a2赋值,由于之前我们并没有给_a1赋初值,所以_a2打印出来是随机值;而a也就是传过来的实参1用来给_a1赋值,所以_a1打印出来是1。
一般构造函数需要我们自己显示实现,只有少数可以让编译器自动。如MyQueue(队列),成员变量全是自定义类型(编译器自动生成)
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
(1) 析构函数名是在类名前加上字符 ~。
(2)无参数、无返回值类型。
(3) 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
注意:析构函数不能重载
(4)对象生命周期结束时,C++编译系统系统自动调用析构函数。
(5)析构函数与构造函数类似:内置类型不一定处理,自定义类型调用它的析构函数。
代码示例:
class Date
{
private:
int year, month, day;
public:
// 1.无参构造函数
Date() { ; }
// 2.带参构造函数
Date(int y, int m, int d )//构造函数
//完成初始化类对象
{
year = y;
month = m;
day = d;
}
void Show()//显示日期
{
cout << year << "/" << month << "/" << day << endl;
}
~Date(){}//析构函数
};
(1)有资源清理的需要显示实现析构函数,如Stack(栈)、List(链表)
(2)有两种场景不需要显示实现析构函数
a.没有资源需要清理,如Date类
b.内置类型成员没有资源需要清理,剩下的成员都是自定义类型成员。如MyQueue
初阶版
代码演示:
#include<iostream>
using namespace std;
class Date
{
private:
int year, month, day;
public:
Date(int y, int m, int d )//构造函数
//完成初始化类对象
{
year = y;
month = m;
day = d;
cout << "构造";
cout << year << "/" << month << "/" << day << endl;
}
~Date()
{
cout << "析构~Date()";
cout << year << "/" << month << "/" << day << endl;
}
};
int main()
{
Date d1(2023, 10, 10);
Date d2(2015, 1, 1);
return 0;
}
通过上面例子可知:先构造的对象后析构,遵循栈的先进后出原则
进阶版
静态全局对象、静态局部对象、全局对象、局部对象,我们再来看一下这些对象的构造和析构顺序会是怎么样?
代码演示
#include<iostream>
using namespace std;
class Date
{
private:
int year, month, day;
public:
Date(int y, int m, int d )//构造函数
//完成初始化类对象
{
year = y;
month = m;
day = d;
cout << "构造";
cout << year << "/" << month << "/" << day << endl;
}
~Date()
{
cout << "析构~Date()";
cout << year << "/" << month << "/" << day << endl;
}
};
Date d1(2014, 9, 9);//全局对象
static Date d2(2015, 10, 10);//静态全局对象
int main()
{
Date d3(2016, 11, 11);//局部对象
Date d4(2017, 12, 12);//局部对象
static Date d5(2018, 1, 1);//静态局部对象
return 0;
}

通过上面示例我们发现,这里的对象析构顺序与我们之前所说的先构造的先析构有所出入。原本按照我们之前案例得出的结论最先析构的应该是d5,最后析构的应该是d1才对,是哪里出了问题呢?
其实问题出在这些对象的生命周期上,下面简要介绍一下各种对象的生存期,以及调用构造函数、析构函数的时机。
1、局部对象 其生存期为函数被调用期间。在建立对象时,调用其构造函数,当函数调用结束时调用其析构函数。 2、静态局部对象 其生存期为函数被首次调用至程序结束期间。当函数被首次调用建立对象时,调用其构造函数,当主函数执行完毕前调用其析构函数。 3、全局对象 全局对象在程序一开始时,其构造函数就会被执行(这通常比程序进入点更早)。当程序即将结束前,全局对象的析构函数会被执行。4、静态全局对象 静态全局对象与全局对象类似,其构造函数也是在程序一开始时被执行。当程序结束时,静态全局对象的析构函数也会被执行,但需要注意的是,它可能比全局对象的析构函数早一步执行。
补充知识
全局对象和静态全局对象生命周期的对比: 静态全局对象和全局对象都在程序开始时创建,并在程序结束时销毁。它们的生命周期实际上是一样长的,都是贯穿整个程序的执行过程。 主要的区别在于,静态全局对象的析构函数可能在全局对象的析构函数之前执行,但这并不意味着静态全局对象的生命周期更长。 链接性的区别: 除了生命周期外,静态全局对象和全局对象还有一个重要的区别:链接性。一般的全局对象在程序的其他文件中可以通过关键字extern来访问,而静态全局对象则只能在本文件中使用。 综上所述,静态全局对象和全局对象的生命周期实际上是相同的,都是贯穿整个程序的执行过程。它们的主要区别在于链接性和析构函数的执行顺序。
所以在上面的例子中局部对象应该是最先被析构的,局部对象d4比局部对象d3先析构(因为d3比d4先构造),然后到静态局部对象d5析构,再到静态全局变量,最后是全局变量。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。