new与delete

对类类型而言,new运算是先分配内存再执行构造函数,delete是先执行析构函数再释放内存

对array的用法

我们都知道new与delete经常这样用:

1
2
3
4
5
int* a = new int(10);
delete a;

int* b = new int[10];
delete []b;

如果第二种情况改用delete b;会不会有内存泄漏? 答案是仍然不会,分配简单类型内存时,内存大小已经确定,系统可以记忆并且进行管理,在析构时,系统并不会调用析构函数。它直接通过指针可以获取实际分配的内存空间,哪怕是一个数组内存空间。

但是对于类对象就不能这样用了,看下面的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base
{
public:
Base()
{
std::cout<<"基类构造 "<<this<<endl;
}
virtual ~Base()
{
std::cout<<"基类析构"<<this<<endl;
}
};
Base* pb = new Base[5];
delete pb;

运行结果如下,有5个构造,但只有1个析构函数,而且从this指针来看,是数组第一个元素的析构函数:
1
2
3
4
5
6
7
基类构造 0xe118d4
基类构造 0xe118ec
基类构造 0xe11904
基类构造 0xe1191c
基类构造 0xe11934

基类析构 0xe118d4

delete pb只用来释放pb指向的内存和一个析构。delete[] pb用来释放指针指向的内存,还逐一调用数组中每个对象的析构。 所以为了编程规范,不管对简单类型还是类类型,都要用delete []pb的形式。

与malloc/free的区别

  • new/delete用于C++中的动态内存分配; malloc/free仅用于C环境,用于类类型时,不会运行构造析构函数

  • new/delete不必指定分配内存大小,malloc/free必须指定

  • new返回的是指定对象的指针,而malloc返回的是void*malloc的返回值一般都需要进行类型转化。

  • new是一个操作符可以重载,malloc是一个库函数

比如:

1
2
Base* b = (Base*)malloc(12);
free(b);

12是随便指定的,结果不运行构造和析构函数,而且如果中间运行成员函数,程序会崩溃。 所以这种代码没有任何意义


函数在main之前或之后运行
  • C++中,全局对象的构造函数在main之前运行,析构函数在main之后运行。
  • 类静态变量的初始化在main之前,静态函数不行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Base
{
public:
Base()
{
std::cout<<"基类构造"<<endl;
}
~Base()
{
std::cout<<"基类析构"<<endl;
}
static int get()
{
std::cout<<"get()"<<endl;
return 55;
}
static int count;
}

// main.cpp
Base b;
// int Base::count = Base::get(); 错误,这里不能调静态函数
int main()
{
std::cout<<"main "<<endl;
return 0;
}

运行结果:

1
2
3
4
基类构造
get()
main
基类析构

gcc中使用attribute关键字,声明constructor和destructor函数:

1
2
3
4
5
6
7
__attribute__((constructor)) void before_main() { 
printf("before main\n");
}

__attribute__((destructor)) void after_main() {
printf("after main\n");
}


运算符重载(二)

以一个Point类为例,重载几个运算符,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Point
{
public:
Point(int x=0,int y=0)
{
arr[0] = x;
arr[1] = y;
}
void print()
{
cout<<"x: "<<arr[0]<<" y: "<<arr[1]<<endl;
}

int& operator [](int num)
{
assert(num==1 || num==0);
if(num==0)
return arr[0];
else if(num==1)
return arr[1];
}
Point& operator -()
{
return Point(-arr[0],-arr[1]);
}
Point& operator --()
{
return Point(arr[0]-1,arr[1]-1);
}
Point& operator ++()
{
return Point(arr[0]+1,arr[1]+1);
}
Point& operator +(Point& p)
{
return Point(arr[0]+p.arr[0], arr[1]+p.arr[1]);
}
Point& operator -(Point& p)
{
return Point(arr[0] - p.arr[0], arr[1] - p.arr[1]);
}
bool operator ==(const Point& p)
{
return ((arr[0]==p.arr[0]) && (arr[1]==p.arr[1]) )
}
private:
int arr[2];
};

有一个数组做成员变量,构造函数给数组赋值。
首先是一元运算符- -- ++,显然是无参数的。返回值应当是Point&,函数也容易理解。
二元运算符加法和减法及相等也简单,有一个参数而已。其实这三个运算符由于是双目的,最好按友元重载。

下标运算符重载的声明必须是返回类型& operator [](参数),只能作为类成员函数,不能做友元。

调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
Point p(1,4);
//改变 p
p[0] = 7;
p[1] = 9;
Point p1 = -p;
Point p2(4,12);
Point p3 = p - p2;
if(p2==p3)
cout<<"equal"<<endl;
else
cout<<"not equal"<<endl;
Point p4 = ++p3;
Point p5 = --p3;

参考:C++ 运算符重载


运算符重载(一)

不能重载的运算符:sizeof .(类成员访问) .*(类成员指针访问) :: ?:(三元运算符)。记住只有sizeof和带.的运算符不能重载。

运算符重载有两种方式:成员函数和友元函数。成员函数的形式比较简单,就是在类里面定义了一个与操作符相关的函数。友元函数因为没有this指针,所以形参会多一个。

对运算符重载通常是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Base
{
public:
Base();
explicit Base(int a)
{
m = a;
}
Base operator +(Base& b) //可以是成员函数,也可以是友元
{
Base temp;
temp.m = this->m + b.m;
return temp;
}
friend std::ostream& operator<<(ostream& out, Base& b); //只能是友元
private:
int m;
};

std::ostream& operator<<(ostream& out, Base& b)
{
out<<b.m;
return out;
}

调用:
1
2
3
4
Base b1(3);
Base b2(9);
Base b3 = b1+b2;
std::cout<<b3<<endl;

输入输出运算符重载不能做类的成员函数, 因为平时的输出命令是cout<<b;,实际是cout.operator<<b形式的,如果做成员函数,就变成了b.operator<<cout的形式。

C++ Primer的解释是:假设输入输出运算符是某个类的成员,则它们也必须是istream或ostream的成员。然而这两个类属于标准库,并且我们无法给标准库中的类添加任何成员。

Qt的<<重载也是类似的方法:

1
2
3
4
5
6
//输出QLineF的两点坐标
QDataStream &operator<<(QDataStream &stream, const QLineF &line)
{
stream << line.p1() << line.p2();
return stream;
}

  • 单目运算符最好重载为类的成员函数;双目运算符则最好重载为类的友元函数。
  • 以下一些双目运算符不能重载为类的友元函数:=、()、[]、->。
  • 类型转换运算符只能以成员函数方式重载
  • 流运算符只能以友元的方式重载

拷贝构造函数

基本规则

  • copy构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,没有返回值。它必须的一个参数是本类型的一个引用变量,如果形参是对象做值传递, 将实参传进函数时, 我们实际是拷贝一个副本,这样又要调用拷贝构造函数, 层层递归, 会把栈堆满。类中可以存在多个copy构造函数。

  • 编译器会自动生成默认copy构造函数,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员逐个进行赋值,也就是浅拷贝

  • 默认copy构造函数不处理静态变量。如果静态成员变量在构造、析构实例的时候需要修改,那么通常需要手工实现copy构造函数和重载赋值运算符。

  • 如果对象存在了动态成员,那么需要手动实现析构函数,也就需要手动实现copy构造函数,因为默认copy构造函数使用的是浅拷贝,要改用深拷贝。

  • 如果派生类没有自定义拷贝构造函数,它在拷贝时,会调用基类的copy构造函数。如果两个类都自定义copy构造函数,那么只调用派生类的。

  • copy构造函数也要对常成员变量进行列表初始化

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

深拷贝

浅拷贝实际是对变量的引用,深拷贝是对类成员复制并重新分配内存, 二者的最大区别在于是否手动分配内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Base
{
public:
Base();
explicit Base(int a);
virtual ~Base();
Base(const Base& obj);
private:
int m;
// 最好用智能指针
int *p;
};
Base::Base(int a):
m(a)
{
p = new int(100); // p内存分配在stack
}

Base::~Base()
{
if(p)
{
delete p;
p = NULL;
}
}
Base::Base(const Base &obj):
m(0),
p(new int(100))
{
std::cout<<"copy构造函数"<<endl;
m = obj.m;
// 动态分配的内存必须自定义copy构造函数,浅拷贝不会处理
int* temp = new int;
*temp = *(obj.p);
this->p = temp;
}
//调用
Base b1(15);
Base b2(b1);

不能不处理p。假如copy构造函数中没有对p分配内存,编译正确,但运行时b2析构会出现问题。因为默认拷贝执行浅拷贝,把b2里的p也指向了b1里的p,二者地址相同,结果会出现二次析构,内存泄漏。

我用Creator试验二次析构,发现程序结束时报错 program has unexpectedly finished ,再次运行时Qt先报信息Fault tolerant heap shim applied to current process.,这就是内存泄漏造成的,按照这个方法解决

标准化容器使用insert、push、assign等成员增加元素的时候也会调用拷贝构造函数

禁用拷贝

禁用原因主要是两个:

  1. 浅拷贝问题,也就是上面提到的二次析构。
  2. 自定义了基类和派生类的copy构造函数,但派生类对象拷贝时,调用了派生类的拷贝,没有调用自定义的基类拷贝而是调用默认的基类拷贝。这样可能造成不安全,比如出现二次析构问题时,因为不会调用我们自定义的基类深拷贝,还是默认的浅拷贝。

Effective C++条款6规定,如果不想用编译器自动生成的函数,就应该明确拒绝。方法一般有三种:

  1. C++11对函数声明加delete关键字:Base(const Base& obj) = delete;,不必有函数体,这时再调用拷贝构造会报错
  2. 最简单的方法是将copy构造函数声明为private
  3. 条款6给出了更好的处理方法:创建一个基类,声明copy构造函数,但访问权限是private,使用的类都继承自这个基类。默认copy构造函数会自动调用基类的copy构造函数,而基类的copy构造函数是private,那么它无法访问,也就无法正常生成copy构造函数。

Qt就是这样做的,QObject定义中有这样一段,三条都利用了:

1
2
3
4
5
6
private:
Q_DISABLE_COPY(QMainWindow)

#define Q_DISABLE_COPY(Class) \
Class(const Class &) Q_DECL_EQ_DELETE;\
Class &operator=(const Class &) Q_DECL_EQ_DELETE;

类的不可拷贝特性是可以继承的,凡是继承自QObject的类都不能使用copy构造函数和赋值运算符

有没有定义派生类copy构造函数的情况的不同结果

先是没有定义派生类copy构造函数的情况:

1
2
Derived f;
Derived ff(f);

运行结果是这样:

1
2
3
4
5
6
7
8
Base constrct      0x75fd20
Derived construct 0x75fd20
Base copy constrct 0x75fd10
*************
Derived deconstruct 0x75fd10
Base deconstrct 0x75fd10
Derived deconstruct 0x75fd20
Base deconstrct 0x75fd20

对于副本的对象,只调用了基类copy构造函数。


然后是定义的情况,运行结果是这样:

先是f的基类构造和派生类构造,然后进入ff,这里的对象是个副本,所以this指针的地址不同了, 先是基类构造然后是派生类拷贝构造, 销毁时倒没什么特别。

这样看来, 派生类的copy构造函数可以尽量不定义。

参考:
详解copy构造函数
为什么很多人禁用拷贝(复制)构造函数


结构体与类的字节对齐(终极方案,简单易懂)

先记住常用类型在32和64位的字节

类型 32位 64位
char 1 1
int 4 4
short 2 2
float 4 4
double 8 8
指针 4 8

只有指针在64位时不同,是8。函数指针的typedef声明不参与计算。枚举类型占内存4字节。
另外注意:gcc中没有要求结构体大小是最大对象的整数倍。

字节对齐

终于搞清楚结构体的字节对齐怎么计算了,看了那么多国内博客,大部分都不靠谱,要不然就是不知所云,最后看了一个印度三哥的视频讲解,没用3分钟就明白了。其实就一条规则:计算时按最大成员的大小进行逐个判断,有需要就补位

直接看几个例子:

1
2
3
4
5
6
typedef struct bb
{
int i; //4
double w; //8
float h; //4
};

我们以为它在内存中是这样的: iiii wwwwwwww hhhh 其实是这样的:iiii ---- wwwwwwww hhhh ----
最大的double占8个,从左向右,每8个为一组,编译器无法把iiii wwww一起处理,那样就把double截断了,所以给int补4位。同理float补4位。总共24.

1
2
3
4
5
6
struct s1{
char c; //1
int i; // 4
short f; // 2
double v; // 8
};

原本是这样:c iiii ff vvvvvvvv,从左向右按8补齐,应当是:c--- iiii ff------ vvvvvvvv。c和i总共5,给c补3位就行。f没法和v组合,只能补6位,总共24.

1
2
3
4
5
6
struct s1{
short f; //2
char c[3]; //3
int i; //4
double v; //8
};

原来是:ff ccc iiii vvvvvvvv,2+3不足8,2+3+4超过了8,所以给f补3位,然后i补4位,也就是2+3+3+4+4+8=24

最后来个特殊的,计算N的大小:

1
2
3
4
5
6
7
8
9
struct Node{
char c;
int i;
char p;
};
struct N{
Node n; //12
int x; //4
};

按上面的方法可知Node占12,那么按上面的方法,N是不是该占24?错了,在N里的Node应该按cccc iiii pppp处理,这样N就占16.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct s1{
char c; //1
int i; // 4
short f; // 2
double v; // 8
};

struct s2{
char c; //1
short f; // 2
int i; // 4
double v; // 8
};

第一个的大小是24,第二个是16

#pragma pack (n)

这条预处理命令也好理解了,只要把上面规则中最大变量的大小换成n就行,注意假设结构体中最大元素占内存m,当n如果超过m是不起作用的。

类的sizeof计算

  1. 类的大小为类的非静态成员数据的类型大小之和,也就是说静态成员数据不作考虑。
  2. 普通成员函数和构造函数不影响sizeof的计算
  3. 虚函数由于虚指针的存在,所以要占据一个指针大小,也就是4字节,无论多少个虚函数
  4. 类的总大小也遵守字节对齐规则。

将类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Base
{
public:
Base();
explicit Base(int a);
virtual ~Base();
void test();
virtual void test_virtual();
private:
void foo();
protected:
int m;

执行sizeof的结果是8,如果把虚析构函数去掉就变成4,再把int m去掉就变成了1,实际是空类,但是空类实例化也要在内存占用地址,由编译器添加一个字节以区分不同对象。


回调函数

之前对回调函数理解一直不到位,需要深入分析一下。
一般函数都是系统提供或程序员自定义的,让程序员使用的。但回调函数恰恰相反,它是程序员定义(注册),在特定条件(常常是用户触发)发生时由系统API调用的,是通过函数指针实现调用的。函数定义在高层,调用在底层。

Linux信号处理机制就是利用回调函数实现的,例如signaction某个形参就有一个成员是函数指针。

1
2
3
4
5
6
7
8
9
10
11
12
void func(int n)
{
printf("signal %d catched !\n",n);
}
int main()
{
struct sigaction act;
act.sa_handler = func;
sigaddset(&act.sa_mask,SIGQUIT);
act.sa_flags = 0;
sigaction(SIGINT,&act,NULL);
}

当程序运行后,按Ctrl+C会发送SIGINT信号,然后内核调用函数func,输出文本。

类的成员函数做回调函数

由于this指针的作用,使得将一个CALLBACK型的成员函数作为回调函数安装时就会因为隐含的this指针使得函数参数个数不匹配,从而导致回调函数安装失败。定义类成员函数时,在该函数前加CALLBACK即可将其定义为回调函数。

  1. 类的静态成员函数实现回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void func(int sig)
{
cout << endl;
cout << "signal: "<<sig<<endl;
if(sig == 15)
cout << "SIGTERM"<<endl;
else
cout << "SIGINT"<<endl;
exit(0);
}

class Foo
{
public:
static void func(int sig)
{
cout << endl;
cout << "signal: "<<sig<<endl;
if(sig == 15)
cout << "receive signal SIGTERM"<<endl;

exit(0);
}
};

int main()
{
signal(SIGINT, func);
signal(SIGTERM, Foo::func); // 第二个参数也可以是func
while(1) // 设法阻塞
sleep(1);
return 0;
}

从终端启动程序,会阻塞。按下Ctrl+C,会触发SIGINT信号。执行pkill命令,会触发SIGTERM信号。


this指针

this指针指向对象的地址,本身是一个常量指针MyClass *const this,也就是不能改变指向的对象。

this指针是在创建对象前就有了,在编译时刻已经确定,this指针放在栈上。当一个对象创建后,整个程序运行期间只有一个this指针.

我是这样理解类和this指针的:类相当于房子的户型,根据这个户型可以造出很多房子,这就相当于对象。每个房子的地址不同,这就相当于内存地址。当你进入一个房子后,你可以看见桌子、椅子、地板等,但是房子你是看不到全貌了。this就是房子里面的一个标识,说明了房子的地址,但这个标识又不占房子空间。

this指针不属于对象本身的一部分,不会影响sizeof作用。 顺便一提,一个空的类,sizeof的大小是1

this指针是编译器默认传给类中非静态函数的隐含形参,其作用域在非静态成员函数的函数体内。
在类的赋值运算符重载函数中,我们可以一般使用*this作为当前对象返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base
{
public:
Base();
void test();
void test_pub();
}
//相当于void Base::test(Base* const this)
void Base::test()
{
std::cout<<this<<endl;
cout<<"base test"<<endl;
}
void Base::test_pub()
{
std::cout<<this<<endl;
cout<<"base test_pub"<<endl;
}

调用两函数的结果:

1
2
3
4
00DDF9A0
base test
00DDF9A0
base test_pub

也就是说运行时只有一个this指针。


类的静态变量和静态函数

如果有N个同类的对象,那么每一个对象都分别有自己的成员变量,不同对象的成员变量各自有值,互不相干。但是有时我们希望有某一个或几个成员变量为所有对象共有,这样可以实现数据共享。

可以使用全局变量,但用全局变量的安全性得不到保证,由于在各处都可以自由地修改全局变量的值,很有可能偶然失误,因此在实际开发中很少使用全局变量。

静态变量

  • 对于类的静态成员变量,只有static const int类型和 enum 类型能在类里面初始化,其他只能在.cpp里初始化,但不能在类体内初始化,因为静态数据成员为类的各个对象共享,否则每次创建一个类的对象则静态数据成员都要被重新初始化。

  • 静态成员函数和静态变量在类体外初始化时,无须加static关键字,否则是错误的。

  • static成员变量的内存空间是在初始化时分配,程序结束才释放,跟类对象的销毁无关。

  • 静态变量不参与sizeof计算,因为它不占用对象的内存

  • 静态成员仍然遵循public,private,protected访问准则。

静态函数

  • 静态成员函数只能调用静态变量,因为没有this指针。 设计它的初衷是把类名当成namespace用,控制类内的static变量。

  • 静态成员函数仍然遵循访问等级,但最好是public,否则不能直接用类名::调用

  • 非静态成员函数可以任意地访问静态成员函数和静态数据成员。

  • 静态成员函数不能声明为虚函数,编译直接报错

  • 静态成员函数与成员函数不能同名同参数,也就是静态和非静态函数不能重载,否则编译器不知调用哪个

举例:

1
2
3
4
5
6
7
8
9
10
11
12
// 头文件
class Obj
{
private:
Obj() {}
static Obj* instance;

public:
static Obj* getInstance();
static const int n = 12;
static int n;
};

1
2
3
4
5
6
7
8
9
10
11
//源文件
Obj* Obj::instance=0; //类外定义
int Obj::n = 4; //类外定义
Obj* Obj::getInstance()
{
if(!instance)
{
instance = new Obj();
}
return instance;
}

参考:
Essential C++ 115页


单例模式的多种形态

应用实例:线程池,日志类,windows系统的任务管理器

一个单例模式应具备以下特征:

  1. 只能实例化同一个对象
  2. 可以全局访问
  3. 禁止拷贝
  4. 线程安全

针对第一条,可以将构造函数权限设为private,如果是public那么每次实例化调用构造函数,对象的内存地址都不同,也就是说只准调用一次构造函数。那问题来了,构造函数都private了,怎么实例化对象?显然要用某个public方法来调用,这又有问题:都没实例化对象,怎么调用public方法?所以这个public方法只能是静态的了。

针对第二条,全局性很容易想到静态函数,它是属于类的,而不是属于某个对象的。

第三条很容易,将拷贝构造函数和复制运算符声明为private即可。

综上,单例类的雏形应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton
{
public:
// 单例方法

private: //构造函数或析构函数为私有函数,所以该类是无法被继承的,
Singleton(){std::cout<<"单例构造"<<endl; }
~Singleton(){std::cout<<"单例析构"<<endl; }
Singleton(const Singleton &);
Singleton& operator=(const Singleton&);
static Singleton* m;
};

关键就是怎么实现public static方法。

测试1

首先想到这样的方法:

1
2
3
4
5
static Singleton instance()
{
Singleton s;
return s;
}

结果报错构造函数和析构函数是private,第一行就编译不过

测试2

测试下面这种方法

1
2
3
4
5
6
7
8
9
static Singleton* instance1()
{
Singleton *s = new Singleton();
return s;
}
Singleton* s1 = Singleton::instance1();
Singleton *s2 = Singleton::instance1();
delete s1;
delete s2;

结果发现有两个构造函数,而且无法析构。

有缺陷的懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Singleton
{
public:
static Singleton* LazyInstance()
{
if(!m)
m = new Singleton();
return m;
}
~Singleton(){std::cout<<"单例析构"<<endl; }

private:
Singleton(){std::cout<<"单例构造"<<endl; }
Singleton(const Singleton &)=delete;
Singleton& operator=(const Singleton&)=delete;

static Singleton* m;
};

测试:

1
2
3
4
5
6
7
// 在类外要初始化静态变量: SingleTon* SingleTon::instance = nullptr;
Singleton* s = Singleton::LazyInstance();
std::cout<<s<<endl;
Singleton* s2 = s;
std::cout<<s2<<endl;
delete s;
// delete s2; 不能这样,否则二次析构

s和s2的内存地址相同,说明是单例。但类中只负责new出对象,却没有负责delete对象,结果发现只调用一次构造函数,还需要手动delete s。可以用智能指针修正

如果有两个线程,假设pthread_1刚判断完 intance 为NULL 为真,准备创建实例的时候,切换到了pthread_2, 此时pthread_2也判断intance为NULL为真,创建了一个实例,再切回pthread_1的时候继续创建一个实例返回,那么此时就不再满足单例模式的要求

双检锁+智能指针的懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SingleTon
{
public:
typedef boost::unique_ptr<SingleTon> Ptr;
static Ptr getInstance()
{
if(instance==nullptr)
{
std::lock_guard<std::mutex> lk(m_mutex);
if(instance==nullptr)
instance = boost::shared_unique<SingleTon>(new SingleTon);
}
return instance;
}
~SingleTon(){ qDebug()<<"destruct"; }
private:
SingleTon(){ qDebug()<<"construct"; }
SingleTon(const SingleTon& s){}
SingleTon& operator=(const SingleTon& s){}

static std::mutex m_mutex;
static Ptr instance;
};

改用智能指针做静态类型,

1
2
3
4
5
6
// 在类外要初始化静态变量: SingleTon::Ptr SingleTon::instance = nullptr;
SingleTon::Ptr s = SingleTon::getInstance();
qDebug() << s.get();

SingleTon::Ptr s1 = SingleTon::getInstance();
qDebug() << s1.get();

结果二者地址相同,也运行了析构函数。 加了锁,使用互斥量来达到线程安全。这里使用了两个if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用getInstance的方法都加锁,锁的开销毕竟还是有点大的。

缺点:使用智能指针会要求用户也得使用智能指针;使用锁也有开销; 同时代码量也增多了;在某些平台,双检锁会失效

饿汉模式

优点:不需要加锁,执行效率高,线程安全的

缺点:初始化即实例化,浪费内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Singelton
{
private:
Singelton(){}
static Singelton *single;
public:
static Singelton *GetSingelton();
};
// 饿汉模式的关键:初始化即实例化
Singelton *Singelton::single = new Singelton;

Singelton *Singelton::GetSingelton()
{
// 不再需要进行实例化
//if(single == nullptr){
// single = new Singelton;
//}
return single;
}

Meyers模式

1
2
3
4
5
6
#define App SingleTon::MeyersInstance()
static SingleTon& MeyersInstance()
{
static SingleTon s;
return s;
}

这种方式所用到的特性是在C++11标准中的Magic Static特性。如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。

在 MeyersInstance() 函数内定义局部静态变量的好处是,构造函数只会在第一次调用MeyersInstance() 时被初始化, 保证了成员变量和 Singleton 本身的初始化顺序。
它还有一个潜在的安全措施, MeyersInstance() 返回的是对局部静态变量的引用, 如果返回的是指针, MeyersInstance() 的调用者很可能会误认为他要检查指针的有效性, 并负责销毁。

Qt中的全局指针

Qt里有一个全局指针qApp,在任意地方都能使用,看看是不是单例模式。

QApplication中:

1
#define qApp (static_cast<QApplication *>(QCoreApplication::instance()))

QCoreapplication中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//头文件中
#define qApp QCoreApplication::instance()

static QCoreApplication *instance() { return self; }

static QCoreApplication *self;

//源文件中
QCoreApplication *QCoreApplication::self = 0;
// 构造函数
QCoreApplication::QCoreApplication()
{
d_func()->q_ptr = this;
d_func()->init();
QCoreApplicationPrivate::eventDispatcher->startingUp();
}

// 就是 d_func()->init();
void QCoreApplicationPrivate::init()
{
......
Q_ASSERT_X(!QCoreApplication::self, "QCoreApplication", "there should be only one application object");
QCoreApplication::self = q;
......
}

从QCoreapplication来看,qApp是个宏,实际是函数QCoreApplication::instance(),QCoreApplication这个类十分关键,构造函数肯定不能是private。从这个self来看,特别像懒汉模式,self是在QCoreApplication构造函数里赋值,赋给它的q指针实际就是QCoreApplication的this指针。

但是在程序里使用qApp,你会发现其地址都一样,也就是同一个全局指针,这就在于Q_ASSERT_X这句限定了只能有一个QCoreApplication对象,再加上拷贝构造函数和赋值运算符都在QObject限定为private,因此qApp也是一种单例模式。所以我们可以说单例模式不一定限定构造和析构是private,这个使用了Qt特有的d指针和q指针,技巧性太高,还是用meyers模式吧

ROS中的单例模式

看ROS源码中的类TopicManager,它用到了单例模式,我模仿写了一个类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SingleTon;
typedef boost::shared_ptr<SingleTon> SingleTonPtr;

#define Ptr SingleTon::getInstance()

class SingleTon
{
public:
SingleTon() {}
static const SingleTonPtr& getInstance()
{
static SingleTonPtr f = boost::make_shared<SingleTon>();
return f;
}
void out() { cout<<" out put "<<endl; }
private:
int m_num;
};

结果发现构造函数只能是public,如果是private,就会报错,原因在make_shared中。这样一来就不能实现单例了,看来这种做法不可行。


参考:探究 C++ Singleton(单例模式)