存档

从单例模式谈起(二):volatile 关键字

2020/01/21 1,453

前一部分(从单例模式谈起(一))我们讨论单例模式时,谈到了 Double-Checked Locking 问题。我们讲, volatile 关键字并不能解决Double-Checked Locking 问题。这一节我们讨论与此相关的问题。
现代编译器为了提高程序的效率而对代码做了很多优化。这些优化大部分是有益的,但是也为多线程编程带来问题。

寄存器缓存

我们来看如下代码:

x++ 的值有锁保护,所以它是独占的,x++ 的行为不会被并发破坏。那么 x 的值必然为 2。 然而现实中并非一定如此:编译器有可能为了提高 x 的访问速度而将 x 放入线程的某个寄存器中,而线程的寄存器是线程私有的。 如果 Thread1 先获得了锁,那么程序的执行过程有可能是这样的:

  1. [Thread1] 读取 x 到寄存器 R1. (R1 = x = 0)
  2. [Thread1] R1++. unlock. 由于之后可能还会访问 x, 所以 Thread1 不将 R1 写回 x . (x = 0)
  3. [Thread2] 读取 x 到寄存器 R2 (R2 = x = 0)
  4. [Thread2] R2++
  5. [Thread2] 将 R2 写回 x (x = R2 =1)
  6. [Thread1] 在某个时候将 R1 写回 x (x = R1 = 1)

由此可见,即使正确的加了锁,也不能保证多线程安全。

继续阅读

Linux Coredump 核心转储

2019/10/14 2,343

什么是 Coredump

coredump 核心转储 ,也称为 核心文件(core file) 是操作系统在进程收到某些 信号 而终止运行时,将此进程的地址空间以内容以及有关进程状态的其他信息写出的一个文件。这种信息往往用于调试。
程序员可以通过工具来分析程序运行过程中哪里出错了:Windows 平台用 userdump 和 WinDBG ,Linux 平台使用gdb, elfdump, objdump 等

Windows WinDBG

关于 windbg, 可以参考以下资料

Linux GDB

有些时候进程在crash的时候会产生 core 文件, 但我们却找不到 core 文件,我们需要使用 ulimit 进行一些设置, 这个命令是用来限制系统用户对shell资源的访问的。
ulimit -a 可以查看当前的设置
ulimit -c 可以设置 core 文件的上限,单位为区块(一般 1 block = 512 bytes) .其值为 0 时不写入 core, 为 unlimited 时不限制 core 文件大小。
需要注意, ulimit 只对当前会话有效。若想对所有会话生效, 需要在 /etc/profile 中进行配置。

源文件如下 test_vec.cpp :

编译运行时可能出现如下现象:

使用 gdb 打开来看:

从 gdb显示的栈信息来看,崩溃发生在 main 函数内的 vector::at 函数内,由 _M_range_check raise 。
如果我们在编译时使用了 -g 选项, 会得到更详细的信息

继续阅读

GDB 常用调试技巧

2019/10/12 1,844

1. 调试宏

宏是预编译的,无法 print 宏的定义。但是如果配合 gcc, 我们还是可以有限地调试宏。
在 GCC 编译程序的时候,加上 -g3 参数,就可以调试宏了。

  • info macro mac_name 可查看宏定义,及位置
  • macro expand mac_expr 可查看宏展开的样子

示例:

调试现场如下:

2. 修改变量

两种方法:

  1. print var_name=x
  2. set var var_name=x

示例:

继续阅读

GDB 常用命令一览

2019/10/11 2,709

命令

GDB 是 Linux 下的命令行调试工具。
启动 GDB 有如下几种方式:

  1. gdb <program> 直接启动执行程序
  2. gdb <program> core 用gdb 同时调试一个可执行程序和core文件。core 是程序非法执行后 core dump 产生的文件
  3. gdb <program> <PID> 指定进程, gdb会自动 attach 上去。program 应该在 PATH 环境变量中可以搜索得到。

常用的 gdb 命令如下

信息 info

info 可以简写成 i

  • info args 列出参数
  • info breakpoints info break i b 列出所有断点
  • info break number i b number 列出序号为 number 的的断点
  • info watchpoints i watchpoints 列出所在 watchpoints
  • info threads 列出所有线程
  • inifo registers 列出寄存器的值
  • info set 列出当前 gdb的所的设置
  • i frame
  • i stack
  • i locals
  • i catch

断点和监视 break & watch

继续阅读

C++ 构造函数漫谈(四)

2019/08/21 1,407

这一章谈 C++11 中引入的两种 “语法糖” .使用它们可以使得我们的代码更为简洁优雅。

委托构造函数

在同一个类中,一个构造函数可以调用另一个构造函数,这叫委托构造函数。这是 C++ 11 的新特性。
委托构造函数可以简化在每个构造函数中的重复代码。

注意一点,委托构造函数在使用时不可以形成环:禁止套娃。

继续阅读

C++ 构造函数漫谈(三)

2019/08/19 1,402

这一章聊一聊在面向对象的C++中,构造函数的调用顺序。

数据成员的构造顺序

一个类的数据成员的初始化顺序只与其在类中的声明顺序相关,与其它无关。

而析构时,如果成员是在堆中,析构顺序正好与构造时相反。

类A的成员的构造顺序为: m1_, m2_, pm1_, pm2_ 。析构时的顺序为 m2_, m1_ ,由于 pm1_, pm2_ 不在堆中,所以它们的析构需要类A自己管理。

继续阅读

C++ 构造函数漫谈(二)

2019/08/18 1,670

三五零法则

我们知道,编译器会为类自动生成几个特别的成员函数:构造函数、复制构造函数、复制赋值运算符、析构函数。后三者比较特殊,我们在下面会频繁提到。

三法则

若一个类需要用户显式定义 析构函数、复制构造函数、复制赋值运算符 中的一个,那么这三个函数都需要显式定义 。如果用户显式定义了其中一个,另外两个还是会被编译器隐式定义,这种混杂的情况容易生产无法预期的错误。
如果一个类中有非基本数据类型或者非类类型的成员(如指针、文件描述符等),则这一法则表现的更为明显:隐式析构函数无法对这种成员进行有效的释放,隐式复制构造函数和隐式复制赋值运算符无法进行深拷贝。

继续阅读

C++ 构造函数漫谈(一)

2019/08/12 2,102

C++ 构造函数有很多有意思的小细节。这里来做一些探讨。这些内容可能会分为几章,这一章来探讨 隐式构造函数,显式空构造函数 和 =default 修饰的构造函数 ,私有构造函数和 =delete 修饰的构造函数 之间的区别。

在开始之前,我们先了解两种特殊的类:

聚合类 与 POD

聚合类 是 C++ 中的一个特殊的类型。当一个类(class, struct, union) 满足以下条件时,它是一个聚合类:

  • 无显式声明的构造函数(可以是 defaultdelete 的)
  • 无基类
  • 无虚成员函数
  • 无私有的或受保护的非静态数据成员
  • 无使用 {}= 直接初始化的非静态数据成员

一个普通数组也是一种聚合类型(如 int[10], char[], double[2][3])

POD ( Plain old data structure ) 则是一种特殊的聚合类,它必须满足聚合类的所有条件,且不具有以下成员:

  • 指针到成员类型的非静态数据成员(包括数组)。
  • 非POD类类型的非静态数据成员(包括数组)。
  • 引用类型的(reference type)非静态数据成员。
  • 用户定义的拷贝与赋值算子。
  • 用户定义的析构函数。

可见,POD类类型就是指class、struct、union,且不具有用户定义的构造函数、析构函数、拷贝算子、赋值算子;不具有继承关系,因此没有基类;不具有虚函数,所以就没有虚表;非静态数据成员没有私有或保护属性的、没有引用类型的、没有非POD类类型的(即嵌套类都必须是POD)、没有指针到成员类型的(因为这个类型内含了this指针)

POD 一般用来在不同的模块之前传递数据使用。如一个 C++ 库向外提供 C 接口,可以使用 POD 作为参数。

隐式构造函数,显式空构造函数 和 =default 修饰的构造函数。

对于 未定义任何构造函数 的类型( struct class or union),编译器会为该为自动生成一个 inline public 的构造函数, 如果这个类型满足 constexpr 类型的要求,则这个构造函数还会被 constexpr 修饰,这个由编译器生成的构造函数,我们称之为 隐式构造函数 或 默认构造函数。在 C++11 以前,如果用户声明了其它构造函数,则编译器不会生成默认构造函数,需要我们显式的声明。而在 C++11 以后,我们仍可用 default 关键字来强制编译器自动生成原本隐式声明的默认构造函数。

继续阅读