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

数据成员的构造顺序
一个类的数据成员的初始化顺序只与其在类中的声明顺序相关,与其它无关。
而析构时,如果成员是在堆中,析构顺序正好与构造时相反。
1 | class M { |
类A的成员的构造顺序为: m1_, m2_, pm1_, pm2_ 。析构时的顺序为 m2_, m1_ ,由于 pm1_, pm2_ 不在堆中,所以它们的析构需要类A自己管理。
这一章聊一聊在面向对象的C++中,构造函数的调用顺序。

一个类的数据成员的初始化顺序只与其在类中的声明顺序相关,与其它无关。
而析构时,如果成员是在堆中,析构顺序正好与构造时相反。
1 | class M { |
类A的成员的构造顺序为: m1_, m2_, pm1_, pm2_ 。析构时的顺序为 m2_, m1_ ,由于 pm1_, pm2_ 不在堆中,所以它们的析构需要类A自己管理。

我们知道,编译器会为类自动生成几个特别的成员函数:构造函数、复制构造函数、复制赋值运算符、析构函数。后三者比较特殊,我们在下面会频繁提到。
若一个类需要用户显式定义 析构函数、复制构造函数、复制赋值运算符 中的一个,那么这三个函数都需要显式定义 。如果用户显式定义了其中一个,另外两个还是会被编译器隐式定义,这种混杂的情况容易生产无法预期的错误。
如果一个类中有非基本数据类型或者非类类型的成员(如指针、文件描述符等),则这一法则表现的更为明显:隐式析构函数无法对这种成员进行有效的释放,隐式复制构造函数和隐式复制赋值运算符无法进行深拷贝。
C++ 构造函数有很多有意思的小细节。这里来做一些探讨。这些内容可能会分为几章,这一章来探讨 隐式构造函数,显式空构造函数 和 =default 修饰的构造函数 ,私有构造函数和 =delete 修饰的构造函数 之间的区别。
在开始之前,我们先了解两种特殊的类:

聚合类 是 C++ 中的一个特殊的类型。当一个类(class, struct, union) 满足以下条件时,它是一个聚合类:
无显式声明的构造函数(可以是 default 或 delete 的)
无基类
无虚成员函数
无私有的或受保护的非静态数据成员
无使用 {} 或 = 直接初始化的非静态数据成员
一个普通数组也是一种聚合类型(如 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 关键字来强制编译器自动生成原本隐式声明的默认构造函数。
项目中发现使用个别机器的 IE 访问某些个别页面出现 500 错误。分析发现出现错误的机器,其 IE 的 HTTP RequestHeader Accept-Language 是 zh-Hans-CN , 而正常的机器是 zh-CN . 由于某些原因无法修改服务器程序,那只能强制修改 IE 的 header.

方式是修改注册表:
计算机\HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\International ,
添加或修改项 AcceptLanguage 为 zh-CN , 重启 IE 即可。
zh-Hans 简体中文
zh-Hans-CN 大陆地区使用的简体中文
zh-Hans-HK 香港地区使用的简体中文
zh-Hans-MO 澳门使用的简体中文
zh-Hans-SG 新加坡使用的简体中文
zh-Hans-TW 台湾使用的简体中文
zh-Hant 繁体中文
zh-Hant-CN 大陆地区使用的繁体中文
zh-Hant-HK 香港地区使用的繁体中文
zh-Hant-MO 澳门使用的繁体中文
zh-Hant-SG 新加坡使用的繁体中文
zh-Hant-TW 台湾使用的繁体中文
今天在看 cpprestsdk 的源码的时候发现一个方法的定义是这样的:
1 | template<typename _Ty> |
语法为:
1 | auto fun_name(argument-declarations...) -> return_type{} |
我们知道 C++11 的 lambda 表达式可以使用 -> 指定返回参数的类型, 没想到声明方法也可以这么用。这种写法和传统的方法有什么区别呢?
在泛型编程中,一个常见的例子如下:
1 | template <typename T> |
如果 a 和 b 的类型不同,需要怎么写呢?
1 | template<typename R, typename T1, typename T2> |
我们在调用此方法时必须显示指定 R 的类型, 或者使用 decltype 运算符推导返回值类型。如果让调用的方式简单点呢?把返回值类型放到定义中去, 像这样:
1 | template <typename T1, typename T2> |
然而这么写是编译不过的。因为 a, b 在参数列表中,编译器解析返回值的时候,它们还没有定义。那么我们可以这样写:
1 | template <typename T1, typename T2> |
不过这太晦涩了。
C++ 11 允许将返回值类型后置,前面使用 auto 占位:
简单地说: enable_shared_from_this 是为了解决 在类的内部获取自己的 shared_ptr 这件事情而存在的。
众所周知, 每一个对象都能通过this 指针来访问自己的地址。this 指针也是所有成员函数的隐含参数。然而有些时候,我们需要的不仅是 this,而是一个 “this的智能指针”。
这里有一个常见的场景:
1 | class A { |
代码如上:在异步方法 DoSth_Async() 中调用了成员方法 OnDo(bool) . 这里存在一个问题: 当 OnDo() 被调用的时候,该类的是否还在生存中:
1 | int main(){ |
智能指针 ptr 在出作用域后立即被释放。所以当 OnDo() 被调用的时候,其所在的对象实际已经被释放了。如果确保在 OnDo() 被调用的时候,该对象仍然在生命周期内呢?一个方便的方法便上在构建线程的时候,将该对象的 shared_ptr 传入到线程。在该线程的生命周期内,该对象就会一直存在。这是一种利用 shared_ptr 的 保活机制 。
设置 IE 代理有多种方法。常用的是修改注册表和调用 InternetSetOption API 。
IE 设置在注册表中所在的位置是
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings
其中:
ProxyServer
Value Type: REG_SZ
Value Data: “ProxyServerName:Port”
ProxyEnable
Value Type: REG_DWORD
Value Data: 0 = Disable 1 = Enable
ProxyOverride 不使用代理的地址的列表
Value Type: REG_SZ
Value Data: “list_of_exclusion” 。可以使用通配符 * . 使用分号 ; 分隔, 如 192.168.1.*;*.domain.com;<local>
示例代码如下 :
1 | INTERNET_PER_CONN_OPTION_LIST List; |
关于更多的信息请 参考这里 docs.microsoft.com
在 IE9 之前, 以上步骤之后可能需要重启 IE 才能使设置生效。或者在打开 “Internet 属性” 面板时设置不生效。此时需要:
1 | InternetSetOption(0, INTERNET_OPTION_SETTINGS_CHANGED, 0, 0); |
来通知其它应用IE属性被修改。
Rundll32.exe shell32.dll,Control_RunDLL inetcpl.cpl,,4
Rundll32 可以做很多事情。具体可以看这里 List of Rundll32 Commands in Windows 10
单例模式可能是大家最为熟知的一种设计模式,本身没什么好谈的。但是在 C++ 中,由单例模式可以引出一系列的问题,可能会比较有意思,这里探讨一下。
1.使用 static 实现
1 | class S1 { |
其中 S2 是要避免的。因为 1. 类的静态成员变量的初始化时间一般早于 main 函数 2. 如果静态成员的初始化里调用了其它类,可能出现未定义的错误。
2.使用指针判断是否初始化
1 | class S3{ |
这两种方式在单线程程序中使用都是可以的,但如果在多线程中,就会出现问题。
对于 S1 要注意, C++ 局部静态变量的初始化可能不是线程安全的 , 这就是 Magic Static ,是指 返回一个静态局部变量的引用 的用法,在某些情况下,如S1 可能会被编译器这样解析:
TCP 协议错综复杂, 很容易出现错误,错误码非常之多。这里探究一些在编程中比较常见的错误码。tcp 错误码 在不同的操作系统中的值不同。这里取用 Linux 与 Windows 两种,如 connection_refused 在windows 中的值为10054, 在 Linux 中为54 .
eof :2 : End of fileeof 标志着流的结束。当对端关闭连接(调用了 shutdown 或 close) 后,处于 CLOSE_WAIT 状态。 本端会收到 FIN, ACK 。此时如果本端再试图 read ,则会读到 eof
connection_refused61 : Connection refused
10061 : No connection could be made because the target machine actively refused it . 由于目标计算机积极拒绝,无法连接
在客户端试图与服务端建立连接的时候发生。一般是服务端没有处于监听状态。 客户端发送发 SYN , 但是收到了 RST, ACK
connection_reset54 : Connection reset by peer
10054 : An existing connection was forcibly closed by the remote host . 远程主机强迫关闭了一个现有的连接。
对端对处于连接状态的socket 进行了异常断开, 如进程中断等。此时如果本端进行读操作,可能会得到此错误。
最近一段时间在解决一个网络方面的 BUG,发现自己对 TCP 的状态了解得不够,于是复习了一遍 TCP 的各种状态与状态间的转换,并做了一个整理,以加深自己的理解。
网络上的两台设备想要一起工作,就必须使用相同的网络协议。像 TCP 这种复杂的协议,我们很难简洁地描述其各种确切的操作。所以我们试图使用有限状态机来解释这个复杂的协议。
有限状态机 (FSM : Finite State Machine) 又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。FSM 的四个基本概念如下:
状态 state: 描述机器在特定时间上处于的 环境(circumstance)或状况(status)
转换 Transition: 从一种状态到另一种状态的行为(act)
事件 Event: 导致状态发生变化的事情
动作 Action: 机器从一种状态转换之前对事件所做的响应
FSM通过解释协议可以处于的所有不同状态、可以在每个状态中发生的事件、针对事件采取的操作以及结果发生的转换来描述协议。协议通常在第一次运行时以特定的开始状态启动。尔后,它遵循一系列步骤使其进入常规操作状态,并根据特定类型的输入或其他情况移动到其他状态。状态机之所以称为有限状态机,是因为只有有限数量的状态。
对于 TCP 来说,可以使用 FSM 来描述一个连接的生命周期 : 一个 TCP 设备和另一个 TCP 设备之间的每个连接,都从一个空状态(null state) 开始,经过一系列的状态变化,直到建立了(established)连接。然后它将保持这种状态,直到遇到某种事件,它将进行一系列状态直到回到关闭状态。
我们使用三个缩写词来表示状态间转换的三种类型消息,它们对应于 TCP 头的标志。(关于 TCP 头的内容可以参见我之前的博客 《TCP 协议概述》) :