构造函数是我们经常需要调用的,他有非常多的知识点,这里罗列一下,也许有很多事你不知道的。

构造函数的组成

构造函数分为两部分,一部分为初始化表,一部分为构造体。

class MyClass{
public:
   MyClass(int a) 
   : m_a(a) //这叫初始化表,构造函数特有的东西
   {
      //构造体
   }
private:
   int m_a;
};

那么问题来了,初始化表和构造体到底有什么区别呢?哪些东西只能写在初始化表?哪些东西只能写在构造体?

首先,我们需要弄明白,这两者的差异,在初始化表对成员变量的初始化,是构造初始化,而在构造体中初始化,

则是赋值拷贝初始化。

int a(3); // 构造初始化

int b;

b = 3;  // 赋值拷贝初始化

可以看出,构造初始化性能更高。其实我们就注意上述的这个对比,作为类的成员变量大部分情况下,既可以放在

初始化表,也可以放在构造体中,什么情况不可以放在构造体呢?或者说必须在初始化表中呢?(这两个问题有一点点差异)。

构造需初始化的东西

引用和常引用的成员,C++11以前初始化列表初始化。

struct Item{ int a; int b;};

class MyClass{

private:

int& m_a;

int m_b[4];

Item m_item;

public:

MyClass(int a)

:m_a(a), m_b({1, 2, 3, 4}), m_item({3, 4})

{

}

};

//其中m_a必须在初始化表中赋值,而m_b和m_item, 在C++11以前,没有初始化列表的一致性的问题

//如果想用初始化列表初始化,就必须在初始化表中使用,c++11以后,就没有这个问题了。

C++98 下述的两种写法。

Item item = {1, 2, 4};//正确

Item item1;

item1 = { 1, 2, 4};   //C++98 是错误的, C++11是正确的。

数组的初始化列表,不管哪个版本都只能构造时使用。

没有无参构造

如果没有在初始化表中进行初始化的成员变量,则默认会在初始化表中调用一个无参构造,如果该成员是自定义的类,

但是该类没有无参数构造,那不好意思,那必须在初始化表中指定构造方法,否则会报错。

其它的区别

上面我们说到,成员变量在初始化表和构造体中初始化的区别,除了类成员,还有其它一些东西,下面我们做一个对比

  • 初始化表

如果该类有基类函数,那么基类函数的构造必须写初始化表中,如果没有,则会初始化表中缺省调用一个基类的无参构造,如果基类没有无参构造,或者没有足够权限调用基类的无参构造,那么则必须显示的调用基类的其他构造函数。

  • 构造体

其实类对象的真正构造发生在初始化表中,而构造体,是绑定咋构造函数中一个普通函数体而已,做为普通函数,可以调用其他的成员函数,初始化表就不能调用(因为,初始化表走完,才表示类对象创建完成,类对象没有创建完成,调用类成员函数,是不安全的)。

类构造的分类

我们已经清楚类的构造函数组成,类构造的分类,是根据类构造函数的参数列表进行区别。

分别分为: 无参构造一参构造多参构造,而一参构造,分为普通的一参构造拷贝构造右值构造

构造函数和普通函数一样,可进行进行重载,默认返回值,为类的对象。

类默认缺省了3个构造函数,无参构造、拷贝构造、右值构造。(有没有缺省的右值构造有待考证)

当写任意的构造函数之后,就没有缺省无参构造函数,而拷贝构造,只有当你写拷贝构造函数之后,

才没有缺省的拷贝构造函数,注意,如果一个类,你只写拷贝构造函数,那么缺省的无参构造,也没了哈。

无参构造

类有没有无参构造的重要性,其实上述中已经阐述到了,很多缺省的调用,就是调用的无参构造。

一参构造

一参构造,就复杂多了,如果一个类有一个参数的构造函数,那么构造该对象,有以下的写法:

一般写法:

A a(3);

赋值写法:

A a = 3;

又或者写成:

A a = A(3);

注意: 上述3种写法都是等价的。只会调用A的一次构造函数。

这是一参构造的特有用法,但是,会带来一些歧义,例如,QWidget的一参构造可以指定一个父窗口指针。

如果写成:

QWidget* parent = new QWidget;

QWidget mywidget = parent; // 这里,有点类似拷贝构造,理解上有歧义。

explicit QWidget(QWidget* parent = Q_NULLPTR);

用explicit修饰后,那么我们只能写成。

QWidget widget(parent);

其中拷贝构造也是一参构造,同样也可以用explicit关键字修饰,但一般拷贝和复制是一码事,所以没见过

explicit关键字修饰拷贝构造的情况。

多参构造

多参构造,其实也没啥好说的,这里说一下,初始化列表的构造,例如:

class MyClass{

public:

MyClass(int a, double b):m_a(a),m_b(b){}

private:

int m_a;

int m_b;

};

MyClass b(1, 3.0);       //正确

MyClass c = {2, 4.1};   //正确,初始化列表构造

初始化列表构造也适合一参构造,同样由于有两种写法,explicit也可以用来限制多参构造,注意,这是C++11的用法。

初始化列表

上面有提到初始化表初始化列表,在C++11以前就有这个概念,用在数组和结构体的初始化,其实这是为兼容C,

但是却与C++所提倡的一致性不统一。

于是C++11增加了初始化列表,那么上面有提到初始化列表的使用。

除了上述中的用法,标准库中还封装了一个玩意。

initialize_list

它是任意个数同类型元素初始化列表的包装,这玩意挺简单,我也不知道咋翻译它,也翻译为初始化列表吧。但此初始化列表

与彼初始化列表,虽然有关联,但不是同一个概念。

它的用法,可以参考标准库中std::list, std::vector等容器的实现,也可以百度一下,还是很好理解的。

std::list<int> a = { 1, 2, 3, 4, 5};  // 这就是initialize_list的应用。

std::map<int, int> b = { {1, 3} , {2, 4} };  // 复合应用。

委派构造

这是C++11新增的用法,就是一个类如果有多个构造函数,我们可以使用委派构造的方法,用过的都说好,也知道它能帮我解决什么的问题。

class MyClass{

MyClass(int a): a(a){}

MyClass() :MyClass(3){}  //委派构造

private:

int  a;

};

委派构造,必须写在初始化表中,且独占,简单的说,初始化表中有了委派构造,那么不好意思,不能其它的任何东西。

上诉这个例子,似乎写的有点多余,实际上当构造体很复杂时,如果有多个构造函数,为了避免构造函数中的内容有多份,通常会选择剥离

成一个函数,构造函数去调用,有了委派构造,就不需要了。

New与构造函数

MyClass* a = new MyClass(3);

这一行代码做了两件事:

  1.  在堆中申请内存。
  2.  调用构造函数。

那么可不可以指定内存构造呢? 可以。

MyClass* a = (MyClass*)malloc(sizeof(MyClass));

new (a)MyClass;

又或者:

char b[sizeof(MyClass)];

new (b) MyClass;

指定内存构造,指定内存可以在栈上, 也可以在堆上,如果你自己实现过std::vector,那么这是你必须知道的事情。

同样析构函数,也分为两部分,调用析构函数和释放内存,同样也可以拆开调用。

构造与派生

当一个类有基类时,构造该的类的对象时, 构造函数的调用顺序是:

基类构造函数→派生类的构造函数, 而析构相反。

这种理解是正确的,但我们得知道为啥。

因为我们调用的是派生的构造函数,咋就先调用基类构造了呢?

其实基类的构造函数是派生类构造函数的一部分,基类的构造必须在派生类构造函数的初始化表中被调用,

且必须是第一个被调用,如果初始化表中没有调用,则缺省的调用基类的无参构造。

缺陷

类的构造函数和运算符重载函数是类成员函数中不能够被修饰为虚函数的函数。构造与派生有一个明显的缺陷,就是我们

必须按照这个流程进行构造,有时候,我们派生的对象,并非想完全的继承自基类。例如:

struct MyData{

int a;

};

class MyView{

public:

MyView(){

data = new MyData;

}

private:

MyData*  data;

};

struct MyData2 : MyData{

double b; // 派生类一些新的数据

}

class MyView2 : pbulic MyView{

MyView2(){

data = new MyData2;

}

};

你会发现一个有内存泄露,因为我们在构造MyView2时,基类构造MyData,  为了修复内存泄露

派生类会选择先释放基类new的MyData, 但是问题来了,new了又立马释放,这不是浪费性能吗?

能不能让基类不做那件事情呢?

有两种写法:

  1.   干脆,不在构造函数构造数据层,例如Filmora中的控制器,都是构造完成之后,主动传入的。
  2.   换一种方式。

显然第一种写法,它不符合设计原则中的内聚性,因为我们构造一个对象,需要写多行代码。

但我们可以换一种方式解决这个问题:

class MyView{

private:

MyView();

virtual void init(){

data = new MyData;

}

public:

template<typename T>

static MyView* create(){

MyView* view = new T;

view→init();

return view;

}

private:

MyData* data;

};

class MyView2{

private:

virtual void init(){

data = new MyData2;

}

};

MyView* view = MyView::create<MyView2>();

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。