目录
5). 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
前言
c++类与对象(二)(上)
一、构造函数
1. 构造函数的引入
class Data
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
//d1.Init(2022, 9, 27);
d1.Print();//忘记初始化的话输出随机值
return 0;
}
对于Data类,可以通过 Init 公有方法给对象设置日期,如果不进行初始化,将输出随机值,写代码的人很容易忘记进行初始化,且如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
在函数F中,本地变量a和b的构造函数(constructor)和析构函数(destructor)的调用顺序是: ( )
Class A;
Class B;
void F() {
A a;
B b;
}
A.b构造 a构造 a析构 b析构
B.a构造 a析构 b构造 b析构
C.b构造 a构造 b析构 a析构
D.a构造 b构造 b析构 a析构
2. 构造函数的特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1). 函数名与类名相同。
2). 无返回值。
class Data
{
public:
Data()//函数名和类名相同,无返回值,自动调用的构造函数,保证初始化
{
_year = 1;
_month = 1;
_day = 1;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
d1.Print();
return 0;
}
3). 对象实例化时编译器自动调用对应的构造函数。
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
Data d1();会报错
注意以下代码,存在歧义,Data d1,会不知道调用谁
经典面试题:
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// 以下测试函数能通过编译吗?
void Test()
{
Date d1;
}
4). 构造函数可以重载。
5). 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
注意:推荐构造函数自己写,推荐全缺省或者半缺省
6).默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理,其中c++把变量分成两种,内置类型/基本类型:int/char/double/指针 ,自定义类型:class/struct去定义类型对象。
下面将分几个情形进行对第六点特征进行理解:
情形一:MyQueue使用默认构造函数时:
对于q进行了初始化!
class Stack
{
public:
Stack()//构造函数,函数名为类名Stack,无返回值
{
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
void push(int x)
{
}
int pop()
{
}//注意:函数都在公共区
private:
Stack _st1;
Stack _st2;
};
int main()
{
MyQueue q;
return 0;
}
情形二:
当Stack类也使用默认的构造函数的时候:
class Stack
{
public:
/*Stack()//构造函数,函数名为类名Stack,无返回值
{
_a = nullptr;
_top = _capacity = 0;
}*/
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
public:
void push(int x)
{
}
int pop()
{
}//注意:函数都在公共区
private:
Stack _st1;
Stack _st2;
};
int main()
{
MyQueue q;
return 0;
}
为什么Stack使用默认构造函数会使得q是随机值???
因为当MyQueue中成员变量只有自定义成员变量,其不写构造函数很合理,(注意函数在公共代码区) 当其调用其默认的构造函数,其再去调用Stack的构造函数,而Stack也没写构造函数的时候,默认的构造函数不会对内置变量进行初始化,故会导致_st1/_st2为随机值
思考一个问题?
情形一的情况,是因为MyQueue中成员变量只有自定义变量,故其在调用其默认的构造函数的时候,默认构造函数会对自定义类型进行初始化,会再去调用Stack的构造函数,进行初始化,但是如果MyQueue的成员变量中有内置类型的时候怎么办???见下图。
所以注意:
1).如果一个类的成员全是自定义类型,我们就使用默认生成的函数
2).如果有内置成员,最好还是自己去写构造函数,或者需要参数的构造函数,也自己写
注意:构造函数只有三种:
1).默认生成的
其中MyQueue默认生成构造函数
2).自己写的无参的
见之前所写的日期的构造函数
3).自己写的全缺省的
注意:以下这种情形不构成构造函数
二、析构函数
1. 概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2. 特性
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
class Stack
{
public:
Stack()//构造函数,函数名为类名Stack,无返回值
{
_a = nullptr;
_top = _capacity = 0;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;//可写可不写
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st;
return 0;
}
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数,如下图所示。
但是下图代码没必要写析构函数,因为不需要资源清理,因为像这些day、month、year出了作用域,栈帧就自动销毁了。
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,
比如上图的Data类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
class Stack
{
public:
Stack()//构造函数,函数名为类名Stack,无返回值
{
_a = nullptr;
_top = _capacity = 0;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;//可写可不写
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st;
return 0;
}
3. 笔试题-析构构造的顺序问题
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
A.D B A C
B.B A D C
C.C D B A
D.A B D C
分析:1、类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象
2、全局对象先于局部对象进行构造
3、局部对象按照出现的顺序进行构造,无论是否为static
4、所以构造的顺序为 c a b d
5、析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部 对象之后进行析构
6、因此析构顺序为B A D C
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为? ( )
C c;
void main()
{
A*pa=new A();
B b;
static D d;
delete pa;
}
A.A B C D
B.A B D C
C.A C D B
D.A C B D
分析:首先手动释放pa, 所以会先调用A的析构函数,其次C B D的构造顺序为 C D B,因为先构造全局对象,在构造局部静态对象,最后才构造普通对象,然而析构对象的顺序是完全按照构造的相反顺序进行的,所以答案为 B
4. 最后,我们再详细理解一下析构函数。
关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
因为:在main函数中创建了Data对象d,而d中包含4个成员变量,
其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;
而_t是Time类对象,是自定义类型,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以需要调用Time类的析构函数进行销毁。
但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Data类对象,所以编译器会调用Data类的析构函数,而Data类中没有显式的析构函数,则编译器会给Data类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,再进行销毁。
而Time类的析构函数是自己写的,则调用cout << "~Time()" << endl;该语句,输出~Time(),
即当Data对象销毁时,要保证其内部每个自定义对象都可以正确销毁。
注意:创建哪个类的对象则调用该类的析构函数,销毁哪个类的对象则调用哪个类的析构函数。
总结
构造函数与析构函数的正确使用。