构造函数与析构函数
  • 构造函数和析构函数不能被继承。正因为如此,派生类对象会先调用基类的构造函数,而且只能调用这一次,析构函数也是同理。

  • 如果基类定义了带参数的构造函数,派生类没有定义任何带参数的构造函数,则不能直接调用基类的带参构造函数,程序编译失败。

  • 类的对象声明会调用构造函数,但类的指针不会。例如MyClass p1, *p2;中p1调用构造函数,p2不会。

  • 如果类A中的成员变量有类B的对象,那么声明A对象时,先运行B的构造,再运行A的构造。不过一般用B的指针比较好

  • 构造函数中的成员列表初始化顺序取决于类中声明的顺序;析构函数销毁数据成员的顺序与声明顺序相反

  • 构造函数不能是虚函数。类有一个指向虚函数表的指针用于调用虚函数,这个指针是在构造函数里初始化的,如果构造函数是虚函数,怎么在没有初始化的情况下调用它?

  • 构造函数中最好不要调用虚函数,某些编译器会报错。因为先运行基类构造函数,再运行派生类的。基类的构造函数时已经出现了虚函数表指针,它指向基类的虚函数表,所以基类的构造函数中调用的虚函数是基类的。这样就无法实现多态了,所以没有意义。参考Effective C++条款 9



  • 如果需要手动实现一个析构函数,通常意味着类实例中有动态分配的内存空间,只有这种情况,需要程序员手动释放分配的内存空间。 同时也需要实现拷贝构造函数和重载赋值运算符。

  • 析构函数无返回类型,只能有一个,也就不能重载。

  • 继承体系中,析构函数执行的顺序: 派生类析构 —- 类成员的析构 —- 基类析构

函数的局部对象是逆序销毁的

1
2
3
4
5
6
int main()
{
Foo f;
Base b;
return 0;
}

运行结果为:

1
2
3
4
Foo construct 
base constructor
base destructor
Foo destruct

构造函数不难理解,当然按顺序,析构是逆序执行的。看完这个例子就会发现,派生类对象在内存中的生存销毁和它很相似:基类构造——派生类构造——派生类析构——基类析构,相当于先有个基类对象,再有派生类对象

更广泛的结论:

  • 全局对象或全局静态对象不管在什么位置定义,它的构造函数都在main()函数之前执行
  • 所有在stack上的对象都比在全局或静态对象早销毁。
  • 不管是在栈上的对象,还是全局或静态对象,都遵循这样的顺序: 越是先产生的对象越是后被销毁

虚析构函数

假如一个类没有派生类,那么析构函数可以不是虚函数。 如果有派生类,析构函数要声明为虚函数,这是为了防止新手犯错误。如果基类指针指向了派生类的对象,析构时,只会调用基类析构函数,没有派生类的析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base
{
public:
Base(){ }
~Base(){ // 应加上virtual声明
qDebug()<<"Base destructor";
}
};

class Derived:public Base
{
public: Derived(){ }
~Derived(){
qDebug()<<"Derived destructor";
}
};

Base* p = new Derived();
delete p;

运行结果是Base destructor。给Base类析构函数加上virtual声明后,运行结果:
1
2
Derived destructor
Base destructor

一开始我没加virtual,仍然调用了派生类的析构函数,后来发现是基类习惯性继承了QObject类,而后者的析构函数是virtual声明的。于是有结论: 对于继承树,只要在根基类上声明虚析构函数就可以,所有的子类的析构函数也就是虚的。

如果析构函数是虚函数,运行时会动态绑定到派生类的析构函数.如果不是虚函数,想安全删除pBase必须delete (Derived*)p. 

可能有人要问了,为什么C++不直接把虚析构函数作为默认值?原因是虚函数表的开销以及和C语言的类型的兼容性。有虚函数的对象增加一个指针

不能随意声明虚析构函数,注意以下原则:

  • 类至少包含一个虚函数的时候,需要虚的析构函数,因为它必然有派生类
  • 类有派生类时,需要虚的析构函数,否则不需要

Sublime

取消自动补全括号引号

1
"auto_match_enabled": "false",

修改 minimap 的颜色

Settings的修改:

1
2
"always_show_minimap_viewport": true,
"draw_minimap_border": true,

.sublime-theme文件部分的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"class": "minimap_control",
"settings": ["always_show_minimap_viewport"],
"viewport_color": [0,191,255],
"viewport_opacity": 0.5,
},

{
"class": "minimap_control",
"settings": ["always_show_minimap_viewport"],
"viewport_color": [0,191,255],
"viewport_opacity": 0.5,
"viewport_opacity": { "target": 0.4, "speed": 3.0, "interpolation": "smoothstep" },
},
{
"class": "minimap_control",
"attributes": ["hover"],
"viewport_color": [0,191,255],
"viewport_opacity": 0.5,
"settings": ["always_show_minimap_viewport"],
"viewport_opacity": { "target": 0.4, "speed": 12.0, "interpolation": "smoothstep" },
},

高级搜索

搜索范例.png
其中的含义:

1
2
3
4
5
6
7
8
*.cs
表示找 cs 后缀的文件,也可以写多个后缀

-/*Debug/*
表示排除 Debug 文件夹内的所有文件

-*.cache
表示排除 cache 后缀的文件, -test.c是不在test.c中搜索

  • 修改sublime的主题文件: /home/user/.config/sublime-text/Packages/One Dark Material - Theme/OneDarkMaterial.sublime-theme

  • sublime 的插件 Bracket Highlighter 设置

参考链接 1链接 2

修改sublime标签的颜色


using关键字

引入命名空间,一个 using 声明一次只引入一个命名空间,这个不用讲了

使用class有时可能会遇到这种情况:基类中的函数func有多个重载版本,但是派生类只对版本1进行了重定义。 结果派生类 隐藏 了基类中所有版本,通过派生类只能访问重定义的版本1,不能再访问基本的版本2,3,4,则派生类要么重定义所有重载版本,或者一个也不重定义。 如果基类中重载版本太多,在派生类中全重定义一遍就太麻烦了。

这就是using关键字适用的场合,可以在派生类中为重载成员名称提供 using 声明,使派生类使用的全是基类的函数版本。在此基础上,再重定义需要的那些函数,其他版本仍是基类的版本。

说的太抽象了,看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base
{
public:
Base() {}
virtual ~Base() {}

virtual void func(int i) { std::cout << "func in base " <<i<< endl;}
virtual void func(float i) { std::cout << "func in base " <<i<< endl;}
};

class Derived : public Base
{
public:
Derived() {}
// using Base::func;
virtual void func(int i) { std::cout << "func in derived " <<i*5<< endl;}
};

Derived d;
d.func(12);
d.func(float(12.20));

不加using时,运行结果是:

1
2
func in derived 60
func in derived 60

也就是对于d.func(float(12.20));,派生类仍然调用了自己的版本,但是我们实际上没有定义float参数的虚函数,这并不是我们想要的。

using后,只有参数是int的重载版本是派生类的,派生类没有定义虚函数的情况,还是调用基类的。运行结果是:

1
2
func in derived 60
func in base 12.2

隐藏对虚函数和非虚函数都是适用的,因为Effective C++说不要在派生类重新定义基类的非虚函数,所以这里我定义成了虚函数,由于多个虚函数对sizeof的计算相当于一个,所以不会扩大占用内存,可以仍按照Effective C++说的声明为虚函数。

using 用做 alias

1
2
3
using Type = std::vector<int>;

Type v={1,2,3,4,5,6};

再探默认构造函数

今天使用派生类时又发现了一个问题,基类和派生类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base
{
public:
Base(int a)
{ }
};

class Derived : public Base
{
public:
Derived()
{ }
};

还没编译,Creator就已经报错了
2
编译时Derived类会报错: error: no matching function for call to ‘Base::Base()’

这个问题的实质还是默认构造函数。我们都知道,如果一个类没有构造函数,编译器会为我们自动创建一个默认构造函数,这个函数没有参数,什么都没做。但是,当我们实现一个构造函数之后,编译器就不会创建,因此Base没有默认构造函数。

默认构造函数就是在调用时不需要explicitly传入实参的构造函数

上面我定义的Base(int a)是构造函数,而不是默认构造函数。 C++中,最多有一个默认构造函数,刻意是编译器生成,也可以是我们自己定义的。我们自己定义的默认构造函数只能是两种:

  1. 无参 2. 有参数,但参数都有默认值

比如下面两种都是默认构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base
{
public:
Base()
{ }
};
// 或者有默认值的有参构造函数
class Base
{
public:
Base(int a=0)
{ }
};

但是不要把这两种构造函数都放入类里,因为最多只能有一个默认构造函数。不实例化不报错,一旦实例化就会报错: 指定了多个默认构造函数,对重载函数调用不明确

派生类构造函数对基类初始化

再回到上面的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base{
public:
Base(int value){}
};

class Derived : public Base
{
public:
Derived()
{ }
private:
int m_value;
};

现在加入派生类,以上的代码会报错: error: constructor for ‘Derived’ must explicitly initialize the base class ‘Base’ which does not have a default constructor

如果基类没有默认构造函数,那么编译器也不会为派生类隐式地定义默认构造函数, 就会出现上面的错误。对这种错误,有两种修改方法:

  1. 对于有参的构造函数,参数赋默认值。也就是改成默认构造函数:把Base(int a)改为Base(int a=0)

  2. 最常用,也是Qt中所用的:在派生类的构造函数后,用列表初始化的方式调用基类构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Derived : public Base
    {
    public:
    Derived():
    Base(m_value)
    { }
    private:
    int m_value;
    };

上面的问题,可以总结如下:

  • 基类没有显式声明构造函数或者有一个无参数的构造函数,派生类构造函数可以不用对基类初始化,即忽略基类的构造函数
  • 基类的构造函数全是有参数的,派生类必须至少实现一个基类的构造函数,例如Qt中常见的:
1
2
3
4
5
explicit MainWindow(QWidget* parent=0);
// QMainWindow构造函数都有参数
MainWindow::Mainwindow(QWidget* parent):
QMainWindow(parent),
ui(new Ui::MainWindow)

这种方式解决的问题是:使用派生类创建一个对象后,怎样初始化从基类中继承过来的数据成员?(基类的构造函数是不能被继承的)

这种代码的具体格式:

1
2
3
4
派生类::派生类构造函数(总参数列表):基类构造函数(参数列表)
{
派生类中的数据成员初始化;
}

注意:如果没有基类和派生的关系,就不能用这种初始化格式,否则报错。

C++11中的default关键字

之前说了,如果我们显式声明构造函数,编译器就不会生成默认构造函数。 但是有时候,我们反而需要一个默认构造函数,比如下面的情况:

1
2
3
4
5
6
7
8
9
10
11
12
class C
{
public:
C(int f)
{
qDebug()<<"construct" ;
}
int a;
long b;
};

C obj;

很显然会报错,因为不存在默认构造函数了。我们可以加一个构造函数:C()=default;,这样编译就通过了

但是以下用法都会报错:

1
2
Base(double value=0)=default;
Base(float value)=default;

這是因为= default只能被加在 沒有默认参数 的special member function后面,Special member function包含: Default constructor, Destructor, Copy constructor, Copy assignment, Move constructor, Move assignment


构造函数的成员初始化列表 (一)

但是C++11规定,类的声明中可以给非静态成员变量赋初值:

1
2
3
4
5
class Base
{
public:
int m = 3;
};

类中的const成员进行初始化必须用这种方式

1
2
3
4
5
6
7
8
9
10
11
12
class Father {
public:
explicit Father(int father):
f(father),
m(10)
{

}
private:
const int f;
const int m;
};

如果不用这种方式初始化,会编译报错:error: C2789:必须初始化常量限定类型的对象

引用成员变量必须在列表中初始化

比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base
{
public:
Base();
explicit Base(int a);
private:
int m=4; // 类体内赋值,只适用于C++11
const int abc;
int& f;
};

Base::Base(int a)
:abc(10), // 常量
f(m) //引用
{
}

如果没有在构造函数成员列表初始化,会报错error: uninitialized reference member in ‘int&’ ‘const int’ [-fpermissive]
也就是上面的1和2的情况

类中的引用必须用列表初始化的方式赋值,不能在类体内或构造函数中用=赋值,否则会报错:operator= is private

两个类没有继承关系,但是类B的对象是类A成员变量,而B的构造函数都是有参数的,而且参数没有默认值,此时必须在类A构造函数中对B初始化

举例:Father类跟上面一样,增加类MyClass如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass
{
public:
explicit MyClass()
:father(s)
{

}
private:
Father father; // 没有默认构造行为的成员
int s;
};
class Father
{
public:
Father(int a){cout<<"Father construct"<<endl;}
};

MyClass构造函数前,先运行Father的构造函数,所以需要对其初始化,否则报错。这里实际上用到了类的隐式转换。
只要Father类有一个函数是无参数的,那就不需要在MyClass类中对father显式初始化,让编译器自动完成即可。其实就是初始化Father构造函数的参数。

我们有更好的优化方法:把father改成指针类型,实现对此规则的优化,这也是Qt中的常见方法
。 因为定义在heap上的类的指针是不会运行构造函数的,类的对象要运行构造函数,然后用成员初始化列表的方法对指针初始化:

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass
{
public:
explicit MyClass()
:father(new Father(s))
{

}
private:
Father* father;
int s;
};

类似的,Qt中的ui指针所指对象的初始化可以放到构造函数里:

1
2
3
4
5
6
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
ui = new Ui::MainWindow;
ui->setupUi(this);
}

构造函数中初始化的顺序

初始化的顺序与列表中的参数顺序无关,而是由类中定义的顺序决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Base::Base(int a)
{
std::cout<<"constructor Base:"<<a<<endl;
}

class Derive : public Base
{
public:
Derive();
Base a;
Base b;
};
Derive::Derive():
b(8),
a(4)
{
}

Derive* d = new Derive();

运行结果是先a后b:
1
2
constructor Base:4
constructor Base:8

与初始化时a和b的顺序无关,只跟类Derive中的声明顺序有关。

构造函数时,初始化成员变量的顺序要与类声明中的变量顺序相对应,若不对应,则出现警告 warning "will be initialized after [-Wreorder]

静态变量不能用构造函数成员列表初始化

静态变量不属于某个对象,而是被类共有,假如构造函数能初始化,会造成变量值不同。编译时报错:error: C2438 无法通过构造函数初始化静态类数据