目录
初识构造/析构函数
OK,我相信大部分小伙伴认识构造函数是源于课堂,取自老师。
让我们一同回忆课堂,面对对象编程(object-oriented programing)的老师都是如何讲述构造/析构函数。
无论你就读于哪里,我想老师大致都讲过这些:
1.构造函数在类被创建时被调用。
2.如果你从没写过一个构造函数,编译器会送你一个 “默认构造函数”。
3.析构函数在类被销毁时调用。
4.同样的,如果你没有定义过析构函数,编译器会送你一个。
5.它们没返回值类型
好了,我想大部分老师都会讲述这些了。但是...如果构造/析构函数如此粗浅的话,要他有何用?本杰明是傻了吗?还有它构造/析构函数什么档次?胆敢与众函数不同?
我不知道大家有没有这样的问题,反正我的心中有太多太多疑惑了。
时间解答了我的问题,让我来聊聊一些“题外话”放松以下。
一些“题外话”
我曾有幸见到了以下这样有趣的代码,这是你在java中所不能看见的。
class Fraction {
public:
Fraction(int numerator, int denominator){
this->numerator = numerator;
this->denominator = denominator;
}
operator double() { return static_cast<double>(numerator) / denominator; }
private:
int numerator;
int denominator;
};
OK,以上是我们对“分数类”的简单定义。对,这很简单;但是,如果配合上以下的代码,阁下又如何理解、释义呢?
int main() {
Fraction fra_num = Fraction(1, 2);
std::cout << fra_num;
return 0;
}
当初我看到这段代码的时候,我的反应:“WTF,are you kidding me?”
这是妥妥的不能通过编译啊,为什么呢?你没对<<重载啊!这能用吗?
现在回头看看自己可真傻!哦,这段代码是可以通过编译的!!!
编译会穷尽一切手段使你的代码合法话!!!因为它的职责是让程序跑起来,它可不管是怎么跑的!!!因为考虑代码如何运行是你的职责!
对对对!我们是没有重载 操作符<< 但是,我们重载过double() 也就是转换函数。
于是你可爱的编译器是这样理解的:
哦,亲爱的合作伙伴。我猜你一定是要把fra_num转变成double类型,然后输出。一定!
so!你在屏幕上获得了 0.5;
我们再把代码改一改,倘若是这样的,阁下如何理解、释义?
class Fraction {
public:
Fraction(int numerator, int denominator = 1){
this->numerator = numerator;
this->denominator = denominator;
}
operator double() { return static_cast<double>(numerator) / denominator; }
private:
int numerator;
int denominator;
};
int main() {
Fraction fra_num = 1;
std::cout << fra_num;
return 0;
}
我想大部分同学会 “c语言”连连, fra_num = 1;是个什么玩意儿?
哦~不妨我们这样看,
我们定义了一个构造函数 其中denominator 的默认值为1,如果你什么都不给的话,denominator = 1;
记住!编译会不择手段的让你的代码合法!
编译器开始发现了你的“意图”:
老兄,你真会写代码,你一定是想写 Fraction fra_num = Fraction(1);吧!哦好像少一个参数,哦~你还放了个默认值啊,这回我可算懂了;
你是想写 Fraction fra_num = Fraction(1, 1);
于是...你的代码通过编译,并在屏上获得了 1。
这是些让人哭笑不得的代码释义!也许你是想这样做,但也有可能这完全不在你的意料之中!
再识构造/析构函数
抱歉,或许我有点“跑题”了。接下来,让我们闲言少叙,书归正传。
我不知道大家是否从那些 “题外话” 中看到了些什么东西。
这些令哭笑不得的代码另一番滋味,别有一番天地!
没错!你应该发现了,它们:
1.隐式转换函数 也没有 返回值
2.单参数的构造函数起到了类型转换的作用
本杰明对构造函数是这样解释得:构造函数的价值在于让对象实例化,赋予数据意义;
你想起了一个问题吗?编译器会送一个“默认构造函数”
没错,编译器的职责在于让对象实例化,赋予数据意义;而你的责任是初始化,决定数据拥有怎样的含义。
相同的,你应该学会揣摩析构函数诞生的缘由了。
析构函数毁灭对象的数据,让其不具意义;注意是毁灭数据,而不是释放内存归还地址;
所以你可能会在一些vector编码看到这样的段落与设计
template<class T>
void my_vector<T>::pop_back() {
--m_size;
m_data[m_size].~T();
return;
}
这里就摧毁了最后一个结点的对象数据,是对象数据,如果是基本类型可没什么作用;
好吧,我们从语法层面上,更深一步的理解了构造/析构函数了。
但这仅仅是语法层面,如果知道这,那我们可对构造函数理解可还不够深刻。
接下来,我将从语义层面尽可能消除你对构造函数的误解。
误解1:认为任何时候编译器都会提供构造函数;
哦~,我知道,我知道。你们一定会说“这可没什么误解!这道我会解答,当我们给出构造函数时,编译器便不会给出默认构造函数”。
嗯,你们说得对,但是我所说的并非如此,它还包括扩展;或者说,我们在大多数设计时,并不会给出默认构造函数,因为我们需要避免无意义的对象出现,或者说可以让我偷个小懒。但是,现实很骨感。在特定情况下,缺失默认构造函数那是令人痛苦的;
(1)应当给“类成员”设计默认构造函数
当你的程序中,出现了一个类内含另一个类。我们称被内含的类为“成员类”。
如果,你的成员类没有设计 “默认构造函数”,那是糟糕的。因为他会导致,内含类也无法构造。
例如上图,A并没有“默认构造函数”,但是当我们想要构建B类对象时,我们一定要调用A类的构造函数。因为构造函数的语义是让对象实例化,赋予数据意义,所以编译器一定会想办法在B类创建时就创建完成A类。没错!我们并没有任何数据传入,或者说就算设计B(int , int),B(A a, int b)也是无用的。因为在进入构造函数B()的那一刻,A a就应该被创建完成。
你可以想想,构造函数内的语句几乎就是赋值!怎么样才能赋值?那得先有让我存放东西的地方啊!连存放物品的位置都不知道,谈何存放?
没错,此时因为类A中没定义默认构造函数 从而导致类B也无法构造,原因不在于B,而在于A;
如果我们在进入B时就应该创建好类A,那么问题就好理解了。此时我们是没有办法传入参数供编译器创建A类对象的,这也就意味着A类的构造函数必须有一个空参数列表的构造函数!而这就是默认构造函数!
同样的我们能够理解
(2)应当给父类设计默认构造函数
除此以外,
(3)应该给带有虚函数的类创建默认构造函数
(4)应该给虚继承的父类创建默认构造函数
这两种情况只需要简单了解C++的类设有虚函数表、虚基类表。当然我必须承认这得看编译器。
如果是Microsoft C++那虚基类表是独立的,但如果是cfront那虚基类表是含于虚函数表内的。
但无论如何在创建时,如若它们中存有数据,那么在创建类时需要按照具体情况放置指针类。
如何设计构造/析构函数
构造/析构函数并非天生安全。然而,一旦错误来自它们,那将是痛苦的寻找过程。
总得来说,我的设计应当遵循以及秉持以下态度和理念
(1)万不得已不提供默认构造函数
(2)谨防单参数构造函数带来的影响(可避免 如使用关键字 explicit)
(3)严防构造函数创对象不完全的情况出现(需要初始化成员列表+初始化函数 、 用类封装)
(4)严禁析构函数流出异常(抓取所有异常)