0%

由于一个字节8bit最多只能表示 256 种字符,用来表示英文字符绰绰有余,想覆盖非英文字符便捉襟见肘了。为了表示众多的非英文字符(比如汉字),计算机先驱们发明了 多字节编码 ——通过 多个字节来表示一个字符。由于 原始字节序列不维护编码信息,操作不慎便导致各种乱码现象。

Python 提供的解决方案是 Unicode 字符串 ( str )对象, Unicode 可以表示各种字符,无需关心编码。然而存储或者网络通讯时,字符串对象不可避免要 序列化 成字节序列。为此, Python 额外提供了 字节序列对象—— bytes

如上图, str 对象统一表示一个 字符串 ,不需要关心编码;计算机通过 字节序列 与存储介质和网络介质打交道,字节序列由 bytes 对象表示;存储或传输 str 对象时,需要将其 序列化 成字节序列,序列化过程也是 编码 的过程。

对象结构#

bytes 对象用于表示由若干字节组成的 字节序列 以及相关的 操作 ,并不关心字节序列的 含义 。因此, bytes 应该一种 变长 、 不可变 对象 ,内部由 C 数组 实现。如下图:

  1. ob_sval 字节序列对象 PyBytesObject 中,确实藏着一个字符数组 ob_sval 。注意到 ob_sval 数组长度定义为 1 ,这是 C 语言中定义 变长数组 的技巧(ob_sval存储的是地址)。

  2. ob_snash ob_shash ,它用于保存字节序列的 哈希值 。 由于计算 bytes 对象哈希值需要遍历其内部的字符数组,开销相对较大。因此, Python 第一次计算 哈希值时,选择 将哈希值缓存到 ob_shash字段中,以 空间换时间,避免重复计算。

  3. ob_size 每个PyVarObject内部都有个 ob_size字段,PyBytesObject使用此字段存储大小信息以 保持len()操作的O(1)时间复杂度,并跟踪非ascii字符串的大小(内部可以为空字符)

空对象样例#

Python 为待存储的字节序列 额外分配一个字节,用于在末尾处保存 \0 ,以便兼容 C 字符串。从上图可以看出,就算空 bytes 对象( b'' )也是要占用内存空间的,至少变长对象 公共头部 是少不了的。

1
2
>>> sys.getsizeof(b'')
33

bytes 对象占用的内存空间可分为以下个部分进行计算:

  • PyVarObject公共头部 24 字节,ob_refcnt 、 ob_type 、 ob_size 每个字段各占用 8 字节;
  • 哈希值 ob_shash 占用 8 字节;
  • 字节序列本身,假设是 n 字节;
  • 额外 1 字节用于存储末尾处的 \0 ;

因此,bytes 对象空间计算公式为 24+8+n+124+8+n+1,即 33+n33+n,其中 n 为字节序列长度(也是len的取值)。 经过上面的学习,我们可以知道 len(byte对象) = n,len显示的只是 ob_size字段的值,而bytes对象真实占用内存量还 需要加 33.

ascii样例#

对象行为#

对象的行为由对象的 类型 决定,因而我们需要到 bytes 类型对象(PyBytes_Type)中寻找答案。在 Objects/bytesobject.c 源码文件中,我们找到 bytes 类型对象 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
PyTypeObject PyBytes_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"bytes",
PyBytesObject_SIZE,
sizeof(char),
// ...
&bytes_as_number, /* tp_as_number 保存着 数值运算 处理函数的指针*/
&bytes_as_sequence, /* tp_as_sequence */
&bytes_as_mapping, /* tp_as_mapping */
(hashfunc)bytes_hash, /* tp_hash */
// ...
};
bytes 对象居然支持数据操作?bytes_as_number 结构体中只定义了一个操作—— 模运算 ( % ):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static PyNumberMethods bytes_as_number = {
0, /*nb_add*/
0, /*nb_subtract*/
0, /*nb_multiply*/
bytes_mod, /*nb_remainder*/
}
static PyObject *
bytes_mod(PyObject *self, PyObject *arg)
{
if (!PyBytes_Check(self)) {
Py_RETURN_NOTIMPLEMENTED;
}
//实现字符串格式化
return _PyBytes_FormatEx(PyBytes_AS_STRING(self), PyBytes_GET_SIZE(self),
arg, 0);
}
由此可见, bytes 对象只是 借用 % 运算符实现字符串格式化,谈不上支持数值运算,虚惊一场:
1
2
>>> b'msg: a=%d b=%d' % (1, 2)
b'msg: a=1 b=2'

序列型操作#

众所周知, bytes 是 序列型对象 ,序列型操作才是研究重点。我们在 bytes_as_sequence 结构体中找到相关定义:

1
2
3
4
5
6
7
8
9
10
static PySequenceMethods bytes_as_sequence = {
(lenfunc)bytes_length, /*sq_length*/
(binaryfunc)bytes_concat, /*sq_concat*/
(ssizeargfunc)bytes_repeat, /*sq_repeat*/
(ssizeargfunc)bytes_item, /*sq_item*/
0, /*sq_slice*/
0, /*sq_ass_item*/
0, /*sq_ass_slice*/
(objobjproc)bytes_contains /*sq_contains*/
};
由此可见, bytes 支持的 序列型操作 包括以下 5 个: - sq_length ,查询序列长度; - sq_concat ,将两个序列合并为一个; - sq_repeat ,将序列重复多次; - sq_item ,取出给定下标序列元素; - sq_contains,包含关系判断;

长度#

最简单的序列型操作是 长度查询 ,直接返回 ob_size 字段即可:

1
2
3
4
5
static Py_ssize_t
bytes_length(PyBytesObject *a)
{
return Py_SIZE(a);
}

合并#

1
2
>>> b'abc' + b'cba'
b'abccba'

合并操作将两个 bytes 对象拼接成一个,由 bytes_concat 函数处理:

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
static PyObject *
bytes_concat(PyObject *a, PyObject *b)
{
Py_buffer va, vb; //定义局部变量 va 、 vb 用于维护缓冲区
PyObject *result = NULL; //新建临时变量,保存合并结果

va.len = -1;
vb.len = -1;
//获取字节序列所在缓冲区
if (PyObject_GetBuffer(a, &va, PyBUF_SIMPLE) != 0 ||
PyObject_GetBuffer(b, &vb, PyBUF_SIMPLE) != 0) {
PyErr_Format(PyExc_TypeError, "can't concat %.100s to %.100s",
Py_TYPE(b)->tp_name, Py_TYPE(a)->tp_name);
goto done;
}

/* Optimize end cases */
if (va.len == 0 && PyBytes_CheckExact(b)) { //如果第一个对象长度为 0 ,第二个对象就是结果
result = b;
Py_INCREF(result);
goto done;
}
if (vb.len == 0 && PyBytes_CheckExact(a)) { //第二个对象长度为 0 ,第一个对象就是结果
result = a;
Py_INCREF(result);
goto done;
}

if (va.len > PY_SSIZE_T_MAX - vb.len) { //长度超过限制则报错
PyErr_NoMemory();
goto done;
}

result = PyBytes_FromStringAndSize(NULL, va.len + vb.len); //临时 bytes 对象用于保存合并结果,长度为待合并对象长度之和
if (result != NULL) {
memcpy(PyBytes_AS_STRING(result), va.buf, va.len);
memcpy(PyBytes_AS_STRING(result) + va.len, vb.buf, vb.len);
}

done:
if (va.len != -1)
PyBuffer_Release(&va);
if (vb.len != -1)
PyBuffer_Release(&vb);
return result; //返回结果
}
bytes_concat 函数逻辑很直白,将两个 bytes 对象的缓冲区拷贝到一起形成新 bytes 对象。

数据拷贝的陷阱#

考察以下表达式——合并 3 个 bytes 对象:

1
>>> result = a + b + c
这个语句执行时,分成两步进行合并:先将 a 和 b 合并,得到临时结果 t ,再将 t 和 c 合并得到最终结果 result :
1
2
>>> t = a + b
>>> result = t + c
这个过程中,a 和 b 的数据需要被拷贝两遍

合并 n 个 bytes 对象,头两个对象需要拷贝 n-1 次,只有最后一个对象不需要重复拷贝。平均下来,每个对象大约要拷贝 n/2 次!

内建方法 join#

bytes 对象提供了一个内建方法 join ,可高效合并多个 bytes 对象:
1
>>> result = b''.join(segments)
join 方法对数据拷贝进行了优化:先遍历待合并对象计算总长度;然后根据总长度 创建目标对象;最后再 遍历待合并对象,逐一拷贝数据。这样一来,每个对象均只需拷贝一次,解决了重复拷贝的陷阱。

字符缓冲池#

为了优化单字节 bytes 对象(也可称为 字符对象 )的创建效率, Python 内部维护了一个 字符缓冲池 :

1
static PyBytesObject *characters[UCHAR_MAX + 1];
Python 内部 创建单字节 bytes 对象时,先检查目标对象是否已在缓冲池中。PyBytes_FromStringAndSize 函数是负责创建 bytes 对象的通用接口,同样位于 Objects/bytesobject.c 中:
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
PyObject *
PyBytes_FromStringAndSize(const char *str, Py_ssize_t size)
{
PyBytesObject *op;
if (size < 0) {
PyErr_SetString(PyExc_SystemError,
"Negative size passed to PyBytes_FromStringAndSize");
return NULL;
}

//如果目标对象为 单字节对象 且 已在字符缓冲池 中,直接返回已缓存对象
if (size == 1 && str != NULL &&
(op = characters[*str & UCHAR_MAX]) != NULL)
{
#ifdef COUNT_ALLOCS
one_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}

//创建新 bytes 对象并拷贝字节序列
op = (PyBytesObject *)_PyBytes_FromSize(size, 0);
if (op == NULL)
return NULL;
if (str == NULL)
return (PyObject *) op;

memcpy(op->ob_sval, str, size);
/* share short strings */

//如果创建的对象为单字节对象,将其放入字符缓冲池
if (size == 1) {
characters[*str & UCHAR_MAX] = op;
Py_INCREF(op);
}
return (PyObject *) op;
}

由此可见,当 Python 程序 开始运行时字符缓冲池是空的。随着 单字节 bytes 对象的创建,缓冲池中的对象慢慢多了起来。

字符对象 首次创建后便在缓冲池中缓存起来;后续再次使用时, Python 直接从缓冲池中取,避免重复创建和销毁。与 小整数 一样,字符对象 只有为数不多的 256 个,但使用频率非常高。缓冲池技术作为一种 以空间换时间 的优化手段,只需 较小的内存为代价,便可明显提升执行效率。

reference#

  1. Python 源码深度剖析/09 bytes 对象,不可变的字节序列
  2. bytes

构造函数#

  1. 函数名与类名相同
  2. 不能定义返回值,也不能有return语句
  3. 可以有形式参数,也可以没有
  4. 可以是内联函数
  5. 可以重载
  6. 可以带默认参数值
  7. 对象被创建时自动调用
  8. 如果不定义构造函数,编译器编译阶段会生成默认构造函数
  9. 如果此时希望编译器隐含生成默认构造函数,只需加上=default
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Clock {
public:
Clock(int h, int m, int s);
Clock(); //默认构造函数
void setTime(int h, int m, int s);
void showTime();
private:
int hour, minute, second;
};

Clock::Clock() : hour(0), minute(0), second(0) {}//默认构造函数,用初始化列表,但是没用形参
Clock::Clock(int h,int m,int s):hour(h),minute(m),second(s){}
//用初始列表初始化hour,minute,second三个成员变量,效率高,简单初始化无需写在结构体里面

int main(){
Clock c1(0,1,8); //调用构造函数
Clock c2; //调用无参构造函数
}

委托构造函数#

1
2
3
4
5
Clock::Clock(int h,int m,int s):hour(h),minute(m),second(s){} 
Clock::Clock() : hour(0), minute(0), second(0) {}//默认构造函数,用初始化列表,但是没用形参

委托构造函数不仅可以简洁,而且保证代码一致性
Clock::Clock():Clock(0,0,0){} //默认构造函数用委托构造函数构造

拷贝构造函数#

特殊的构造函数,其形参为本类对象的引用, 用一个已存在的对象去初始化同类型的新对象 1. 定义一个对象,以本类 另一个对象作为初始值,发送拷贝构造函数 2. 如果函数的形参时类的对象,调用函数时,将使用实参对象初始化形参对象 3. 如果函数的 返回值是类的对象,函数执行完返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调i函数 4. C++11用"=delete"指示编译器不生成默认复制构造函数 5. 拷贝构造函数是一种特殊的构造函数,具有 单个形参,该形参(常用const修饰)是 对该类类型的引用。 6. 当定义一个新对象并用一个同类型的对象对它进行初始化时,将 显示使用拷贝构造函数。当该类型的对象传递给函数或从函数返回该类型的对象时,将 隐式调用拷贝构造函数**。

C++支持两种初始化形式:复制初始化(int a = 5;)和直接初始化(int a(5);)对于类 类型直接初始化直接调用实参匹配的构造函数复制初始化总是调用拷贝构造函数,也就是说:

1
2
A x(2);  //直接初始化,调用构造函数
A y = x;  //复制初始化,调用拷贝构造函数

下面使用上面定义的类对象来说明各个构造函数的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 1 void main()
2 {
3 // 调用了无参构造函数,数据成员初值被赋为0.0
4 Complex c1,c2;
5
6 // 调用一般构造函数,数据成员初值被赋为指定值
7 Complex c3(1.0,2.5);
8 // 也可以使用下面的形式
9 Complex c3 = Complex(1.0,2.5);
10
11 // 把c3的数据成员的值赋值给c1
12 // 由于c1已经事先被创建,故此处不会调用任何构造函数
13 // 只会调用 = 号运算符重载函数
14 c1 = c3;
15
16 // 调用类型转换构造函数
17 // 系统首先调用类型转换构造函数,将5.2创建为一个本类的临时对象,然后调用等号运算符重载,将该临时对象赋值给c1
18 c2 = 5.2;
19
20 // 调用拷贝构造函数( 有下面两种调用方式)
21 Complex c5(c2);
22 Complex c4 = c2; // 注意和 = 运算符重载区分,这里等号左边的对象不是事先已经创建,故需要调用拷贝构造函数,参数为c2
23
24 }
参考:http://www.cnblogs.com/xkfz007/archive/2012/05/11/2496447.html

深拷贝和浅拷贝:#

  1. 所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。

  2. 在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间

如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝

上面提到,如果没有自定义拷贝构造函数,则系统会创建默认的拷贝构造函数,但系统创建的 默认拷贝构造函数只会执行“浅拷贝”,即将被拷贝对象的数据成员的值一一赋值给新创建的对象,若该类的数据成员中 有指针成员,则会使得新的对象的指针所指向的地址与被拷贝对象的指针所指向的地址相同,delete该指针时则会导致两次重复delete而出错。下面是示例:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
 1 #include <iostream.h>
2 #include <string.h>
3 class Person
4 {
5 public :
6
7 // 构造函数
8 Person(char * pN)
9 {
10 cout << "一般构造函数被调用 !\n";
11 m_pName = new char[strlen(pN) + 1];
12 //在堆中开辟一个内存块存放pN所指的字符串
13 if(m_pName != NULL)
14 {
15 //如果m_pName不是空指针,则把形参指针pN所指的字符串复制给它
16 strcpy(m_pName ,pN);
17 }
18 }
19
20 // 系统创建的默认复制构造函数,只做位模式拷贝
21 Person(Person & p)
22 {
23 //使两个字符串指针指向同一地址位置
24 m_pName = p.m_pName;
25 }
26
27 ~Person( )
28 {
29 delete m_pName;
30 }
31
32 private :
33 char * m_pName;
34 };
35
36 void main( )
37 {
38 Person man("lujun");
39 Person woman(man);
40
41 // 结果导致 man 和 woman 的指针都指向了同一个地址
42
43 // 函数结束析构时
44 // 同一个地址被delete两次
45 }
46
47
48 // 下面自己设计复制构造函数,实现“深拷贝”,即不让指针指向同一地址,而是重新申请一块内存给新的对象的指针数据成员
49 Person(Person & chs);
50 {
51 // 用运算符new为新对象的指针数据成员分配空间
52 m_pName=new char[strlen(p.m_pName)+ 1];
53
54 if(m_pName)
55 {
56 // 复制内容
57 strcpy(m_pName ,chs.m_pName);
58 }
59
60 // 则新创建的对象的m_pName与原对象chs的m_pName不再指向同一地址了
61 }

一个空的class类里有什么?#

定义一个空类

1
2
3
class Empty
{
};
默认会生成以下几个函数 1. 无参的构造函数
1
2
3
Empty()
{
}
2. 拷贝构造函数
1
2
3
Empty(const Empty& copy)
{
}
3. 赋值运算符
1
2
3
Empty& operator = (const Empty& copy)
{
}
4. 析构函数(非虚)
1
2
3
~Empty()
{
}
这些函数只有在第一次使用它们的时候才会生成,他们都是inline并且public的。如果想禁止生成这些函数,可以将它们定义成private函数,如果有很多类都有这种需求,那么可以定义一个基类,然后让其他类继承这个类。 ### 空class用法

先假设我们有个很傲娇的类,它不希望通过构造函数生成,也不希望别的对象对它赋值。 然而更加高级的做法是定义一个空类,将空类的复制构造函数和赋值操作符声明为私有,然后让SomeClass继承它。像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Empty
{
protected:
Empty(){} //允许derived class调用
~Empty(){}
private:
Empty(const Empty&); //阻止了copying
Empty& operator = (const Empty&);
};

class SomeClass: private Empty
{
...
};
这些函数只有在第一次使用它们的时候才会生成,他们都是inline并且public的。如果想禁止生成这些函数,可以将它们定义成private函数,如果有很多类都有这种需求,那么可以定义一个基类,然后让其他类继承这个类。

虚函数的作用以及实现原理#

链接:https://www.nowcoder.com/questionTerminal/1f67d4e2b6134c298e993e622181b333 #### 虚函数的作用:简单讲即实现多态。 基类定义了虚函数,子类可以重写该函数,当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态地调用属于子类的该函数,且这样的函数调用是无法在编译器期间确认的,而是在运行期确认,也叫做迟绑定。

对于虚函数的支持则分两步完成:

1.每一个class产生一堆指向虚函数的指针,放在表格之中。这个表格称之为虚函数表(virtual table,vtbl)。

2.每一个对象被添加了一个指针,指向相关的虚函数表vtbl。通常这个指针被称为vptr。vptr的设定(setting)和重置(resetting)都由每一个class的构造函数,析构函数和拷贝赋值运算符自动完成。

另外,虚函数表地址的前面设置了一个指向type_info的指针,RTTI(Run Time Type Identification)运行时类型识别是有编译器在编译器生成的特殊类型信息,包括对象继承关系,对象本身的描述,RTTI是为多态而生成的信息,所以只有具有虚函数的对象在会生成。

C++中虚函数使用虚函数表和 虚函数表指针实现,虚函数表是一个类的虚函数的地址表,用于索引类本身以及父类的虚函数的地 址,假如子类的虚函数重写了父类的虚函数,则对应在虚函数表中会把对应的虚函数替换为子类的 虚函数的地址;虚函数表指针存在于每个对象中(通常出于效率考虑,会放在对象的开始地址处), 它指向对象所在类的虚函数表的地址;在多继承环境下,会存在多个虚函数表指针,分别指向对应 不同基类的虚函数表。

简单来讲是多态,也就是允许派生类对象指向基类指针在运行时调用调用派生类的同名函数。 原理:含有虚函数的类对象,在啊创建时会再头部创建一个指针,指向一个虚表,虚表内保存着虚函数的地址,当调用虚函数时,调用指针指向虚表,子啊虚表中找到虚函数的地址。从而实现运行时多态,普通的成员函数地址是固定的,直接调用即可。

Overload、Overwrite及Override的区别#

Overload(重载):在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。 1. 相同的范围(在同一个类中); 2. 函数名字相同; 3. 参数不同; 4. virtual 关键字可有可无。

Override(覆盖):是指派生类函数覆盖基类函数,特征是: 1. 不同的范围(分别位于派生类与基类); 2. 函数名字相同; 3. 参数相同; 4. 基类函数必须有virtual 关键字。

Overwrite(重写):是指派生类的函数屏蔽了与其同名的基类函数,规则如下: 1. 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。 2. 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

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
49
50
51
52
53
54
#include <stdio.h>
#include <iostream>
class Parent
{
public:
void F()
{
printf("Parent.F()/n");
}
virtual void G()
{
printf("Parent.G()/n");
}
int Add(int x, int y)
{
return x + y;
}
//重载(overload)Add函数
float Add(float x, float y)
{
return x + y;
}
};

class ChildOne:Parent
{
//重写(overwrite)父类函数
void F()
{
printf("ChildOne.F()/n");
}
//覆写(override)父类虚函数,主要实现多态
void G()
{
printf("ChildOne.G()/n");
}
};

int main()
{
ChildOne childOne;// = new ChildOne();
Parent* p = (Parent*)&childOne;
//调用Parent.F()
p->F();
//实现多态
p->G();
Parent* p2 = new Parent();
//重载(overload)
printf("%d/n",p2->Add(1, 2));
printf("%f/n",p2->Add(3.4f, 4.5f));
delete p2;
system("PAUSE");
return 0;
}

overload的话,只有函数返回值类型不同,会重载吗?#

  1. 在使用重载时只能通过不同的参数样式。例如,不同的参数类型,不同的参数个数,不同的参数顺序(当然,同一方法内的几个参数类型必须不一样,例如可以是fun(int, float), 但是不能为fun(int, int));

  2. 不能通过访问权限、返回类型、抛出的异常进行重载;

  3. 方法的异常类型和数目不会对重载造成影响;

  4. 对于继承来说,如果某一方法在父类中是访问权限是priavte,那么就不能在子类对其进行重载,如果定义的话,也只是定义了一个新方法,而不会达到重载的效果。

一个结构体中有一个int,一个char,一个static int,问这个结构体占多少内存#

结构体 内存对齐规则 结构体所占用的内存 与其成员在结构体中的声明顺序有关,其成员的内存对齐规则如下:

  1. 每个成员分别按自己的对齐字节数和PPB(指定的对齐字节数,32位机默认为4)两个字节数最小的那个对齐,这样可以最小化长度。

  2. 复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度。

  3. 结构体对齐后的长度必须是成员中最大的对齐参数(PPB)的整数倍,这样在处理数组时可以保证每一项都边界对齐。

  4. 计算结构体的内存大小时,应该列出每个成员的偏移地址,则其长度=最后一个成员的偏移地址+最后一个成员数的长度+最后一个成员的调整参数

STL中有什么类#

https://blog.csdn.net/chuanzhouxiao/article/details/51902786

1
2
3
4
5
6
7
8
9
10
11
12
vector 向量 
string 字符串
list 列表
queue 队列
map 映射
set 集合
stack 栈

map,vector和unordered_map区别及实现原理
红黑树 hash表
mysql索引
tcp三次握手 重传机制
## 进程数据栈堆

1.进程和线程的定义#

  1. 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
  2. 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

2.进程和线程的区别#

  1. 进程在执行过程中拥有独立的内存单元,而该进程的多个线程共享内存,从而极大地提高了程序的运行效率。
  2. 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  3. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
  4. 在很多现代操作系统中,一个进程的(虚)地址空间大小为4G,分为系统(内核?)空间和用户空间两部分,系统空间为所有进程共享,而用户空间是独立的,一般WINDOWS进程的用户空间为2G。
  5. 一个进程中的所有线程共享该进程的地址空间,但它们有各自独立的(/私有的)栈(stack),Windows线程的缺省堆栈大小为1M。堆(heap)的分配与栈有所不同,一般是一个进程有一个C运行时堆,这个堆为本进程中所有线程共享,windows进程还有所谓进程默认堆,用户也可以创建自己的堆。 用操作系统术语,线程切换的时候实际上切换的是一个可以称之为线程控制块的结构(TCB?),里面保存所有将来用于恢复线程环境必须的信息,包括所有必须保存的寄存器集,线程的状态等。

3.多线程的适用场景是什么?为啥要用多线程?#

使用多线程是为了 提高程序运行的效率。假如有一个程序,要求用户输入多个算式,计算出结果,并分别打印到屏幕上。如果用户一直没有输入,那么无法计算,更无法打印。如果用户输入了,必须要全部输入完,才能计算出结果,再打印到屏幕。 使用线程的话,一个线程用来等待用户输入,一个用来计算结果,一个用来打印。用户在输入算式3的时候,计算线程在计算算式2,打印线程在打印算式1,三个线程同时进行,减少了等待,这样就提高了运行效率

4.堆和栈#

:是 共有的空间,分全局堆和局部堆。全局堆就是 所有没有分配的空间,局部堆就是 用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得 用完了要还给操作系统,要不然就是内存泄漏

:是个 线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 thread safe的。操作系统在切换线程的时候会自动的切换栈,就是切换 SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。 函数调用栈堆

Top N排序 拷贝构造函数 复制构造函数 static 关键字

源文件cpp到可执行文件exe的过程#

从一个cpp文件到一个exe文件,大概经历了以下过程:

  1. 预处理(preprocessor) 根据预处理命令组装成新的C/C++程序,常以i为扩展名。这个过程包括:宏的替换、删除注释、处理预处理指令(如#include、#ifdef)。

  2. 编译(complier) 将得到的i文件翻译成汇编代码,即.s文件。

  3. 汇编(assembler) 将汇编文件翻译成机器指令,并打包成可重定位目标程序的o文件。该文件是二进制文件,字节编码是机器指令。编译器把一个cpp编译汇编得到目标文件时,除了要在目标文件里写入cpp里包含的数据和代码,还要至少提供3个表:

  • 未解决符号表(unresolved symbol table):提供所有在编译单元里引用但定义不在本编译单元里的符号及其出现的地址;
  • 导出符号表(export symbol table):提供本编译单元具有定义,且愿意提供给其它编译单元使用的符号及其地址(全局作用域);
  • 地址重定向表(address redirect table):提供本编译单元所有对自身地址的引用的记录。
  1. 链接(linker) 由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或函数调用)或程序中可能调用了某个库文件中的函数。将引用的其它o文件并入到我们程序所在的o文件中并进行处理,方可得到最终的可执行文件。 链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定向表,对其中记录的地址进行重定向(即加上该编译单元实际在可执行文件里的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实际的地址(也要加上拥有该符号定义的编译单元实际在可执行文件里的起始地址)。最后把所有的目标文件的内容写在各自的位置上,再做一些别的工作,即得到一个可执行文件。 PS:实际链接的时候更为复杂,因为实际的目标文件里把数据或代码分为好几个区,重定向等要按区进行,但原理一样。
  • 内部链接:一个名称对编译单元(cpp文件)来说是局部的,在链接的时候其它的编译单元无法链接到它;
  • 外部链接:一个名称对编译单元来说不是局部的,在链接的时候其它的编译单元可以访问它,即它可以和别的编译单元交互。

const和define#

const定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个拷贝。 #define宏是在预编译阶段进行替换,而const修饰的只读变量是在编译的时候确定其值。 #define宏没有类型,而const修饰的只读变量具有特定的类型

1
2
3
4
const int *p;   //p可变,p指向的对象不可变
int const*p; //p可变,p指向的对象不可变
int *const p; //p不可变,p指向的对象可变
const int *const p; //指针p和p指向的对象都不可变
总的来说: const:有数据类型,编译进行安全检查,可调试 define:宏,不考虑数据类型,没有安检,不能调试

这里有一个记忆和理解的方法: 先忽略类型名(编译器解析的时候也是忽略类型名),我们看const离哪个近。"近水楼台先得月",离谁近就修饰谁。 判断时忽略括号中的类型

1
2
3
4
const (int) *p;   //const修饰*p,*p是指针指向的对象,不可变
(int) const *p; //const修饰*p,*p是指针指向的对象,不可变
(int)*const p; //const修饰p,p不可变,p指向的对象可变
const (int) *const p; //前一个const修饰*p,后一个const修饰p,指针p和p指向的对象都不可变
## C++ 异常机制分析 http://www.cnblogs.com/QG-whz/p/5136883.html

new和malloc的区别#

https://www.cnblogs.com/engraver-lxw/p/8600816.html 1. 申请的内存所在位置   new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。 2. 返回类型安全性   new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是**返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型**。

  1. 内存分配失败时的返回值   new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。

在使用C语言时,我们习惯 在malloc分配内存后判断分配是否成功

1
2
3
4
5
6
7
8
9
int *a  = (int *)malloc ( sizeof (int ));
if(NULL == a)
{
...
}
else
{
...
}
但是 对于new实际上这样做一点意义也没有,因为new根本不会返回NULL,而且 程序能够执行到if语句已经说明内存分配成功了,如果失败早就抛异常了。正确的做法应该是使用异常机制
1
2
3
4
5
6
7
8
try
{
int *a = new int();
}
catch (bad_alloc)
{
...
}

  1. 是否需要指定内存大小 使用 new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而 malloc则需要显式地指出所需内存的尺寸
    1
    2
    3
    class A{...}
    A * ptr = new A;
    A * ptr = (A *)malloc(sizeof(A)); //需要显式指定所需内存大小sizeof(A);
  2. 是否调用构造函数/析构函数 使用new操作符来分配对象内存时会经历三个步骤:
  • 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
  • 第二步:编译器运行 相应的构造函数以构造对象,并为其传入初值
  • 第三步:对象构造完成后,返回一个指向该对象的指针

使用delete操作符来释放对象内存时会经历两个步骤: - 第一步:调用 对象的析构函数。 - 第二步:编译器 调用operator delete(或operator delete[])函数释放内存空间

总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而 malloc则不会

  1. 对数组的处理 C++提供了new[]与delete[]来专门处理数组类型:
    1
    A * ptr = new A[10];//分配10个A对象
    使用new[]分配的内存必须使用delete[]进行释放:
    1
    delete [] ptr;
    new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数。注意delete[]要与new[]配套使用,不然会找出数组对象部分释放的现象,造成内存泄漏。至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:
    1
    int * ptr = (int *) malloc( sizeof(int)* 10 );//分配一个10个int元素的数组
  2. new与malloc是否可以相互调用 operator new /operator delete的实现可以基于malloc,而 malloc的实现不可以去调用new。下面是编写operator new /operator delete 的一种简单方式,其他版本也与之类似:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     void * operator new (sieze_t size)
    {
    if(void * mem = malloc(size)
    return mem;
    else
    throw bad_alloc();
    }
    void operator delete(void *mem) noexcept
    {
    free(mem);
    }

指针对齐#

以下代码打印的结果是(假设运行在 64 位计算机上):

1
2
3
4
5
6
7
8
struct st_t {
int status;
short *pdata;
char errstr[32];
};
st_t st[16];
char *p=(char *)(st[2].esstr+32);
printf(“%d”,(p-(char *)(st)));

根据字节对齐,在64位系统下struct st_t 结构体占用的字节为48个。

1
2
3
4
5
6
7
struct st_t { 
int status; //占用8个(后面的4个为对齐位)
short *pdata;//占用8个
char errstr[32];//占用32个
};
char *p=(char *)(st[2].esstr+32),p实际指向了st[3]
则p-(char *)(st)),即为&st[3]-&st[0],占用空间为3个结构体的大小,即3*48=144

空类的sizeof大小,有一个虚函数的类的sizeof#

https://blog.csdn.net/foreverhuylee/article/details/39320977 题目(二):运行下面的代码,输出是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A
{
};

class B
{
public:
B() {}
~B() {}
};

class C
{
public:
C() {}
virtual ~C() {}
};

int _tmain(int argc, _TCHAR* argv[])
{
printf("%d, %d, %d\n", sizeof(A), sizeof(B), sizeof(C));
return 0;
}

  1. 答案是1, 1, 4。class A是一个空类型,它的实例不包含任何信息,本来求sizeof应该是0。但当我们声明该类型的实例的时候,它必须在内存中占有一定的空间,否则无法使用这些实例。至于占用多少内存,由编译器决定。Visual Studio 2008中每个空类型的实例占用一个byte的空间。

  2. class B在class A的基础上添加了构造函数和析构函数。由于构造函数和析构函数的调用与类型的实例无关(调用它们只需要知道函数地址即可),在它的实例中不需要增加任何信息。所以sizeof(B)和sizeof(A)一样,在Visual Studio 2008中都是1。

  3. class C在class B的基础上把析构函数标注为虚拟函数。C++的编译器一旦发现一个类型中有虚拟函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。在32位的机器上,一个指针占4个字节的空间,因此sizeof(C)是4。

值传递,引用传递#

值传递---单向传递 swap(int a,int b) 引用传递 ----双向传递 swap(int &a,int &b)
引用即别名,必须初始化

内联函数 inline#

作用:可避免函数调用的开销 注:内联函数只是对编译器发送一个请求,编译器可以忽略该请求 一般用于优化规模小,流程直接,频繁调用的函数

  1. 内联函数体内不能有循环语句和switch语句
  2. 内联函数定义必须在内联函数第一次调用之前
  3. 对内联函数不能进行异常接口声明

constexpr函数#

指能用于常量表达式的函数

1
2
3
4
5
6
constexpr int getsize() {
return 20;
}
int main() {
constexpr int foo = getsize(); //foo是常量表达式
}
## 常量表达式 值不会改变并且编译过程就能得到计算结果的表达式
1
2
3
const int max_files=20; //常量表达式
const int limit=max_files+1; //常量表达式
int staff_size=27; //不是常量表达式

默认参数值#

有默认参数的形参必须列在函数形参列表的最右端 调用实参与形参结合的次序是从左到右

1
2
3
4
5
int add(int x,int y=5,int z=6);//正确
add(1)=12;
add(1,2)=9;

int add(int x=1,int y=5,int z);//错误

浏览器的GET和POST#

这里特指浏览器中 非Ajax的HTTP请求,即从 HTML和浏览器诞生就一直使用的HTTP协议中的GET/POST。浏览器用 GET请求来获取一个html页面/图片/css/js等资源;用 POST来提交一个< form>表单,并 得到一个结果的网页

GET#

“读取“一个资源。比如Get到一个html文件。 反复读取不应该对访问的数据有副作用。比如”GET一下,用户就下单了,返回订单已受理“,这是不可接受的。没有副作用被称为 “幂等“(Idempotent)。因为 GET是读取,就可以 对GET请求的数据做缓存。这个缓存可以做到浏览器本身上(彻底 避免浏览器发请求),也可以做到代理上(如 nginx),或者 做到server端(用Etag,至少可以减少带宽消耗)

POST#

在页面里 < form> 标签会定义 一个表单。点击其中的 submit元素会发出一个 POST请求让服务器做一件事。这件事往往是 有副作用的,不幂等的。不幂等也就意味着 不能随意多次执行。因此也就 不能缓存。比如通过POST下一个单,服务器创建了新的订单,然后返回订单成功的界面。这个页面不能被缓存。试想一下,如果POST请求被浏览器缓存了,那么下单请求就可以不向服务器发请求,而直接返回本地缓存的“下单成功界面”,却又没有真的在服务器下单。那是一件多么滑稽的事情。因为POST可能有副作用,所以浏览器实现为 不能把POST请求保存为书签。想想,如果点一下书签就下一个单,是不是很恐怖?。此外如果尝试重新执行POST请求,浏览器也会弹一个框提示下这个刷新可能会有副作用,询问要不要继续。

改造GET和POST#

当然,服务器的 开发者完全可以把GET实现为有副作用;把POST实现为没有副作用。只不过这和浏览器的预期不符。把GET实现为有副作用是个很可怕的事情。 我依稀记得很久之前百度贴吧有一个因为 使用GET请求可以修改管理员的权限而造成的安全漏洞。反过来,把没有副作用的请求用POST实现,浏览器该弹框还是会弹框,对用户体验好处改善不大。但是后边可以看到,将HTTP POST作为接口的形式使用时,就没有这种弹框了。于是 把一个POST请求实现为幂等就有实际的意义POST幂等能让很多业务的前后端交互更顺畅,以及避免一些因为前端bug,触控失误等带来的重复提交。将一个有副作用的操作实现为幂等必须得从业务上能定义出怎么就算是“重复”。如提交数据中增加一个dedupKey在一个交易会话中有效,或者 利用提交的数据里可以天然当dedupKey的字段。这样万一用户强行重复提交,服务器端可以做一次防护。

GET和POST数据的格式区别#

GET和POST 携带数据的格式也有区别。 当浏览器发出一个GET请求时,就意味着 要么是用户自己在浏览器的地址栏输入,要不就是 点击了html里a标签的href中的url。所以其实 并不是GET只能用url,而是 浏览器直接发出的GET只能由一个url触发。GET上要在url之外带一些参数就 只能依靠url上附带querystring。请求参数和对应的值附加在URL后面,利用一个 "?"代表URL的结尾与请求参数的开始,多个 参数用 "&"连接

1
2
3
http://server/action?id=a&id=b 

https://server/action/?info\=''\&abc\=c6cebb78a7be\&server\=52300 (这里加了转义字符'\',待验证是否必要)

但是HTTP协议本身并没有这个限制。浏览器的POST请求都来自表单提交。每次提交,表单的数据被浏览器用编码到HTTP请求的body里。浏览器发出的POST请求的body主要有有两种格式,一种是 application/x-www-form-urlencoded用来传输简单的数据,大概就是"key1=value1&key2=value2"这样的格式。另外一种是 传文件,会采用multipart/form-data格式。采用后者是因为application/x-www-form-urlencoded的编码方式对于文件这种二进制的数据非常低效。浏览器在POST一个表单时,url上也可以带参数,只要< form action="url" >里的url带querystring就行。只不过表单里面的那些用< input> 等标签经过用户操作产生的数据都在会在body里。因此我们一般会泛泛的说“GET请求没有body,只有url,请求数据放在url的querystring中;POST请求的数据在body中“。但这种情况仅限于浏览器发请求的场景。

参考 四种常见的 POST 提交数据方式

接口中的GET和POST#

这里是指通过浏览器的Ajax api,或者iOS/Android的App的http client,java的commons-httpclient/okhttp或者是curl,postman之类的工具 发出来的GET和POST请求。此时GET/POST不光能用在 前端和后端的交互中,还能用在 后端各个子服务的调用中(即当一种 RPC协议使用)。尽管RPC有很多协议,比如thrift,grpc,但是http本身 已经有大量的现成的支持工具可以使用,并且对人类很友好,容易debug。HTTP协议在 微服务中的使用是相当普遍的。当用 HTTP实现接口发送请求时,就 没有浏览器中那么多限制了,只要是 符合HTTP格式的就可以发。HTTP请求的格式,大概是这样的一个字符串:

1
2
3
4
5
6
7
<METHOD> <URL> HTTP/1.1\r\n
<Header1>: <HeaderValue1>\r\n
<Header2>: <HeaderValue2>\r\n
...
<HeaderN>: <HeaderValueN>\r\n
\r\n
<Body Data....>
其中的“"可以 是 GET或POST,或者其他的HTTP Method,如 PUT、DELETE、OPTION……。HTTP是 基于TCP/IP的关于数据如何在万维网中如何通信的协议。HTTP的底层是TCP/IP。所以GET和POST的底层也是 TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样的。因此从协议本身看,并 没有什么限制说GET一定不能没有bodyPOST就一定不能把参放到的querystring上。因此其实 可以更加自由的去利用格式。比如Elastic Search的_search api就 用了带body的GET;也可以自己开发接口 让POST一半的参数放在url的querystring里另外一半放body里**;你甚至还可以让所有的参数都放Header里——可以做各种各样的定制,只要请求的客户端和服务器端能够约定好。

当然,太自由也带来了另一种麻烦,开发人员不得不每次讨论确定参数是 放url的path里,querystring里,body里,header里这种问题。于是就有了 一些列接口规范/风格。其中名气最大的当属 REST。REST充分运用 GET、POST、PUT和DELETE,约定了这4个接口分别获取、创建、替换和删除“资源”,REST最佳实践还推荐在 请求体使用json格式。这样仅仅通过看HTTP的method就可以明白接口是什么意思,并且解析格式也得到了统一。

GET和POST还有一个重大区别:#

GET产生一个TCP数据包;POST产生两个TCP数据包。

对于GET方式的请求,浏览器会把 http header和data一并发送出去,服务器响应200(返回数据);

而对于POST,浏览器 先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。 注: 1. GET与POST都有自己的语义,不能随便混用。

  1. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。

  2. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

json相对于x-www-form-urlencoded的优势:#

  1. 可以有 嵌套结构
  2. 可以支持 更丰富的数据类型。通过一些框架,json可以直接被服务器代码映射为业务实体。用起来十分方便。但是 如果是写一个接口支持上传文件,那么还是 multipart/form-data格式更合适

安全性#

我们常听到GET不如POST安全,因为 POST用body传输数据,而 GET用url传输,更加容易看到。但是 从攻击的角度,无论是GET还是POST都不够安全,因为 HTTP本身是明文协议。每个 HTTP请求和返回的每个byte都会在网络上明文传播,不管是url,header还是body。这完全不是一个“是否容易在浏览器地址栏上看到“的问题。为了避免传输中数据被窃取,必须做从客户端到服务器的端端加密。业界的通行做法就是 https——即 用SSL协议协商出的密钥加密明文的http数据。这个 加密的协议和HTTP协议本身相互独立。如果是利用HTTP开发公网的站点/App,要保证安全,https是最最基本的要求。当然,端端加密并不一定非得用https。比如国内金融领域都会用私有网络,也有GB的加密协议SM系列。但除了军队,金融等特殊机构之外,似乎并没有必要自己发明一套类似于ssl的协议。

回到HTTP本身,的确 GET请求的参数更倾向于放在url上,因此 有更多机会被泄漏。比如携带私密信息的url会展示在地址栏上,还可以分享给第三方,就非常不安全了。此外,从客户端到服务器端,有大量的中间节点,包括网关,代理等。他们的access log通常会输出完整的url,比如nginx的默认access log就是如此。如果url上携带敏感数据,就会被记录下来。但请注意,就算私密数据在body里,也是可以被记录下来的,因此如果请求要经过不信任的公网,避免泄密的唯一手段就是https。这里说的“避免access log泄漏“仅仅是指避免可信区域中的http代理的默认行为带来的安全隐患。

reference#

  1. 知乎--GET 和 POST 到底有什么区别?
  2. 都 2019 年了,还问 GET 和 POST 的区别
  3. 四种常见的 POST 提交数据方式

curl 是常用的命令行工具(其实并非linux下的,windows cmd也有,只是我在linux下使用),用来请求 Web 服务器。它的名字就是 客户端(client)的 URL 工具的意思。

不带有任何参数时,curl 就是发出 GET 请求。

1
$ curl https://www.baidu.com

-d#

-d参数用于发送 POST 请求的数据体。

1
$ curl -d'login=2333&password=2333'-X POST https://google.com/login

-X#

-X参数 指定 HTTP 请求的方法。

1
$ curl -X POST https://google.com

-H#

-H参数 添加 HTTP 请求的标头

1
$ curl -H 'Accept-Language: en-US' https://google.com
上面命令添加 HTTP 标头Accept-Language: en-US。

1
$ curl -H 'Accept-Language: en-US' -H 'Secret-Message: xyzzy' https://google.com

上面命令 添加两个 HTTP 标头

1
$ curl -d '{"login": "emma", "pass": "123"}' -H 'Content-Type: application/json' https://google.com/login
上面命令 添加 HTTP 请求的标头是Content-Type: application/json,然后用-d参数发送 JSON 数据。

-v#

  • -v参数输出通信的整个过程,用于调试。
    1
    $ curl -v https://www.baidu.com
  • -trace参数也可以用于调试,还会输出原始的二进制数据。
1
$ curl --trace - https://www.baidu.com

-o#

-o参数将服务器的回应 保存成文件,等同于wget命令。

1
$ curl -o baidu.html https://www.baidu.com

上面命令将www.baidu.com保存成baidu.html。

报错curl: (35) Unknown SSL protocol error in connection to localhost:27183#

1
2
3
4
5
6
7
8
9
10
11
$ curl -v https://localhost:27183/abc
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 27183 (#0)
* successfully set certificate verify locations:
* CAfile: none
CApath: /etc/ssl/certs
* SSLv3, TLS handshake, Client hello (1):
* Unknown SSL protocol error in connection to localhost:27183
* Closing connection 0
curl: (35) Unknown SSL protocol error in connection to localhost:27183

Possible reason:没有证书,本地用 http不能用https

要了解装饰器,首先我们来看看嵌套函数 ## 嵌套函数

1
2
3
4
def adder(n):
def handler(x):
return n+x
return handler #返回的是一个函数的引用
像 adder 函数和 handler 这样,在一个函数的函数体内定义另一个函数,就构成了 嵌套函数 。adder-handler 这段简单的代码包含了 3 个不同的作用域:

作用域是一个 静态概念,由 Python 代码语法决定,与编译后产生的 代码对象 一一对应。作用域规定了能够被某个代码块访问的变量有哪些,但对变量具体的值则一概不关心。

一旦 Python 程序开始运行,虚拟机需要为 作用域中的变量 分配一定的 存储空间,这就是 名字空间 。名字空间依照作用域规则实现,它 决定了某个变量在运行时的取值,可以看做是 作用域在运行时的动态表现方式。

adder 函数执行时,作用域 A 在虚拟机中表现为 全局 名字空间,作用域 B 表现为 局部 名字空间:

1
2
globals: adder
locals: n, handler
handler 函数执行时,例如调用 adder(10) 时,作用域 A 在虚拟机中表现为 全局 名字空间,作用域 B 表现为 闭包 名字空间:作用域 C 表现为 局部 名字空间
1
2
3
globals: adder
locals: x
enclosure: n

闭包#

什么是闭包呢?#

闭包 ( closure )词法闭包 ( Lexical Closure ) 的简称,指 延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。简单来说就是 嵌套函数引用了外层函数的变量。这些被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

首先我们来看看adder(10)示例:

1
2
3
4
5
>>> add10 = adder(10)
>>> add10(10)
20
>>> add10(15)
25
观察上面的例子,当函数 adder 返回,局部变量 n 应该就被回收了,为什么 handler 函数还能访问到它呢?这正是由于 闭包空间的存在。

以 adder(10) 为例,它是一个 handler 函数对象,闭包变量 n 值总是 10 。那么,内层函数是如何访问闭包作用域的呢?我们对函数代码对象进行反汇编,从中可以看出端倪:

1
2
3
4
5
6
7
8
9
10
>>> add10 = adder(10)
>>> add10
<function adder.<locals>.handler at 0x10dc2b6a8>
>>> add10.__code__
<code object handler at 0x10dbe5150, file "<stdin>", line 2>
>>> dis.dis(add10.__code__)
3 0 LOAD_DEREF 0 (n) # 执行了闭包变量(藏于 PyFrameObject)查找工作
2 LOAD_FAST 0 (x) # 局部变量(栈帧对象中)查找
4 BINARY_ADD
6 RETURN_VALUE

PyFrameObject 结构体最后部分是不固定的,依次存放着 静态局部名字空间、闭包名字空间以及临时栈。以 add10(1) 为例,函数运行时 PyFrameObject 状态如下如下:

由于函数 局部变量、闭包变量个数编译阶段 就能 确定运行时并不会增减,因此 无须用 dict 对象来保存。相反,将这些变量依次排列 保存在数组中,然后通过数组下标来访问即可。这就是所谓的 静态名字空间

对于局部变量 n ,数组对应的槽位保存着整数对象 1 的地址,表示 n 与 1 绑定。而闭包变量 x 则略有差别,槽位 不直接保存整数对象 10 ,而是通过一个 PyCellObject 间接与整数对象 10 绑定。

闭包变量如何初始化#

函数对象 PyFunctionObject 中有一个字段 func_closure ,保存着函数 所有闭包变量。我们可以通过名字 closure 可以 访问 到这个底层结构体字段:

add10.__closure__ (<cell at 0x10dc09e28: int object at 0x10da161a0>,)

这是一个由 *PyCellObject 组成的 元组,PyCellObject 则 保存着闭包变量的值。当函数调用发生时,Python 虚拟机创建 PyFrameObject 对象,并从 函数对象取出该元组,依次填充相关静态槽位

为什么闭包变量要通过 PyCellObject 间接引用?#

最新的 Python(3.7+) 提供了 nonlocal 关键字,支持 修改闭包变量。如果没有 PyCellObject ,函数在运行时 直接修改 PyFrameObject函数返回就被回收了。借助 PyCellObject ,函数在运行时修改的是 ob_ref 。这样一来,就算函数返回,修改还是随函数而存在

示例理解#

1
2
3
4
5
6
7
8
9
lst = []
for i in arange(5):
def f ():
print (i)
print (f)
lst.append(f)

for f in lst:
f()

输出为

1
2
3
4
5
6
7
8
9
10
<function f at 0x000001E36692FE58>  //函数对象是动态生成的
<function f at 0x000001E366949048>
<function f at 0x000001E36694E798>
<function f at 0x000001E36694E4C8>
<function f at 0x000001E36694E708>
4
4
4
4
4
这是因为 闭包变量是通过 PyCellObject 间接引用,PyCellObject中的 ob_ref指针指向了i这个对象,而i最后变成了4。

若要正常输出0,1,2,3,4,应该怎么修改呢?

1
2
3
4
5
6
7
8
lst = []
for i in arange(5):
def f (i = i):
print (i)
lst.append(f)

for f in lst:
f()

1
2
3
4
5
0
1
2
3
4

这里的i是局部变量

装饰器#

前面我们了解了 嵌套函数和闭包,我们可以 让函数具备搭积木的魔法,例如:打印函数的执行时间。

事不宜迟,我们来实践一下,实现 timer 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs) #此处拿到了被装饰的函数func
time.sleep(2)#模拟耗时操作
long = time.time() - start
print(f'共耗时{long}秒。')
return wrapper #返回内层函数的引用

@timer
def add(a, b):
print(a+b)

add(1, 2) #正常调用add
timer被我们改造成了 装饰器,它接受被装饰函数为入参,返回内部 嵌套函数的引用(注意:此处并未执行函数),内部嵌套函数 wrapper持有被装饰函数的引用即func

“@”是Python的语法糖,它的作用类似于:

1
2
add = timer(add) #此处返回的是timer.<locals>.wrapper函数引用
add(1, 2)

下载文件到本地目录#

1
2
3
svn checkout path(path是服务器 上的目录)
例如:svn checkout svn://192.168.1.1/pro/domain
简写:svn co

查看当前当前项目地址#

1
2
3
svn info (这里返回的是root路径)

svn info filename (返回的时特定文件filename的路径)

切换分支(svn sw)#

1
2
3
4
svn sw <branch_full_url>

sample:
svn sw https://mysvn.cn/svn/repo/project/branches/version-xxx

查看日志#

1
2
svn log path
sample:svn log test.php (显示这个文件的所有修改记录,及其版本号的变化)

比较差异#

1
2
3
4
5
6
7
svn diff path(将修改的文件与基础版本比较)
sample:svn diff test.php

svn diff -r m:n path(对版本m和版本n比较差异)
sample:svn diff -r 200:201 test.php

简写:svn di

虽然一般情况下python无需像cpp一样需要手动 内存申请和释放,但是某些情况,我们需要自己去管理对象的销毁,让它的生命周期符合我们的预期。weakref就是一种方式。

1. 概念#

首先需要了解的是在 Python 里每个对象都有一个 引用计数,当这个引用计数为 0 时,Python 的garbage collection(GC)是可以 安全销毁这个对象的, 比如对一个对象创建引用则计数加 1,删除引用则计数减 1 。

weakref 模块允许 对一个对象创建弱引用,弱引用不像正常引用, 弱引用不会增加引用计数,也就是说 当一个对象上只有弱引用时,GC是可以销毁该对象的

2 weakref.ref#

2.1 weakref.ref用法#

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
>>> import weakref
>>> import sys
>>> class DBO(object):
... pass

>>> dbo1 = DBO() # 创建对象,引用+1
>>> sys.getrefcount(dbo1) # 调用sys.getrefcount,该函数引用了dbo1,对象引用+1
2
>>> weakref_dbo = weakref.ref(dbo1) # 创建弱引用
>>> sys.getrefcount(dbo1) # 弱引用没有增加引用计数
2
>>> weakref_dbo # 弱引用指向的对象
<weakref at 0x7f9b0316d3c0; to 'DBO' at 0x7f9b03166ed0>
>>> dbo2 = weakref_dbo() # 获取弱引用指向的对象
>>> dbo1 is dbo2 # dbo1和dbo2引用的是同一个对象
True
>>> sys.getrefcount(dbo1) # 对象上的引用计数加 1
3
>>> sys.getrefcount(dbo2)
3
>>> dbo1 = None # 删除引用
>>> sys.getrefcount(dbo1) # 这个数字是None对象的引用次数
2545

>>> weakref_dbo
<weakref at 0x7f9b0316d3c0; to 'DBO' at 0x7f9b03166ed0>
>>> dbo2 = None # 删除引用
>>> weakref_dbo # 当对象引用计数为0时,弱引用失效
<weakref at 0x7f9b0316d3c0; dead>
>>> sys.getrefcount(dbo1)
2546
>>>

2.1 weakref.ref源码#

接下来我们看看它的源码套餐,定义在objects/weakrefObjects.c中

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
PyObject *
PyWeakref_NewRef(PyObject *ob, PyObject *callback)
{
/*PyWeakReference是一个弱引用对象,result做临时存储*/
PyWeakReference *result = NULL;
PyWeakReference **list;
PyWeakReference *ref, *proxy;
/*检查当前对象类型是否支持弱引用*/
if (!PyType_SUPPORTS_WEAKREFS(Py_TYPE(ob))) {
PyErr_Format(PyExc_TypeError,
"cannot create weak reference to '%s' object",
Py_TYPE(ob)->tp_name);
return NULL;
}
/*获取弱引用链表指针*/
list = GET_WEAKREFS_LISTPTR(ob);
/*根据*list,给 ref 和 proxy 赋值*/
get_basic_refs(*list, &ref, &proxy);
if (callback == Py_None)
callback = NULL;
if (callback == NULL)
/* return existing weak reference if it exists */
result = ref;
if (result != NULL)
/*给当前weak reference引用加1*/
Py_INCREF(result);
else {
/* Note: new_weakref() can trigger cyclic GC, so the weakref
list on ob can be mutated. This means that the ref and
proxy pointers we got back earlier may have been collected,
so we need to compute these values again before we use
them.
使用之前,先确认ob对象没有被析构
*/
result = new_weakref(ob, callback);
if (result != NULL) {
get_basic_refs(*list, &ref, &proxy);
if (callback == NULL) {
if (ref == NULL)
insert_head(result, list);
else {
/* Someone else added a ref without a callback
during GC. Return that one instead of this one
to avoid violating the invariants of the list
of weakrefs for ob. */
Py_DECREF(result);
Py_INCREF(ref);
result = ref;
}
}
else {
PyWeakReference *prev;

prev = (proxy == NULL) ? ref : proxy;
if (prev == NULL)
insert_head(result, list);
else
insert_after(result, prev);
}
}
}
return (PyObject *) result;
}

  1. weakref.proxy proxy 像是弱引用对象,它们的行为就是它们所引用的对象的行为,这样就 不必 首先调用弱引用对象来访问背后的对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    >>> from socket import *
    >>> s = socket(AF_INET, SOCK_STREAM)
    >>> ref_s = weakref.ref(s)
    >>> ref_s
    <weakref at 0x7f9b0316d3c0; to '_socketobject' at 0x7f9b0310b910>
    >>> s
    <socket._socketobject object at 0x7f9b0310b910>
    >>> proxy_s = weakref.proxy(s)
    >>> proxy_s
    <weakproxy at 0x7f9b03117208 to _socketobject at 0x7f9b0310b910>
    >>>
    >>> ref_s.close() # 不能直接调用对象方法
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    AttributeError: 'weakref' object has no attribute 'close'
>>> ref_s().close()              #  不能直接调用对象方法,要加上()
>>>
>>> proxy_s.close()              #  可以直接调用对象方法
>>> 
>>> sys.getrefcount(s)
2
>>> ref_s
<weakref at 0x7f9b0316d3c0; to '_socketobject' at 0x7f9b0310b910>
>>> r = ref_s()
>>> r.close()
>>> sys.getrefcount(s)
3
>>> ref_s
<weakref at 0x7f9b0316d3c0; to '_socketobject' at 0x7f9b0310b910>
>>> del ref_s
>>> ref_s
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'ref_s' is not defined
>>>

最近打算读一下计算机网路领域的圣经,并将一些重要知识点总结记录下来。

1.1 引言#

链接到Internet的设备都必须有一个 IP地址。基于TCP/IP协议的专用网络中 使用的设备 也具有IP地址。 IP路由器实现的转发程序使用IP地址来 识别流量去向;IP地址也表示流量的来源。 IP地址与电话号码类似,但人们知道电话号码,而 IP地址通常被Internet中的 DNS屏蔽在用户视野之外, DNS实现大多数人使用的是 名字而非数字化的IP地址。

个人用户通常由Internet 服务商(ISP)分配 地址,通过支付费用来获得地址和执行路由。

1.2 表示IP地址#

  • IPv4地址,通常采用 点分四组或者点分十进制表示法(192.168.0.1)。点分四组表示法由 四个用点分隔的十进制数组成。每个数字都是 [0,255]的非负整数(8位二进制可以表示),代表整个IP地址的四分之一(共占32位大小)。

  • IPv6,地址长度为 128位,是IPv4的四倍,它通常采用成为块或者字段的四个十六进制数(一个块大小为16位),这些数用冒号分隔。 例如一个包含8个块的IPv6可以写成 5f05:2000:80ad:5800:0058:0800:2023:1d71。虽然不像用户熟悉的十进制数,但将十六进制转换为二级制更容易。另外IPv6可以简化成标准化的 [RFC4291]:

    1. 一个块的前导零不必书写,如上面可以写成:5f05:2000:80ad:5800:58:800:2023:1d71
    1. 全零块可以省略,用::代替,例如 0:0:0:0:0:0:0:1可以写成 ::1。为了避免歧义,一个IPv6中::只能使用一次
    1. IPv6格式嵌入IPv4地址可使用混合符号形式,如IPv6地址::ffff:10.0.0.1可以表示IPv4地址为10.0.0.1。
    1. IPv6的 低32位通常用点分四组法。Ipv6地址::0102::f001相当于地址::1.2.240.1。它被称为IPv4兼容的IPv6地址。

后面[RFC5952]做了命名新的优化,这里不细讲了。

1.3 基本的IP地址结构#

IPv4 地址空间中有 4 294 967 296(2^32)个可能的地址,而 IPv6地址个数为(2^128):(不列了,实在太大了,根本用不完)。由于拥有大量地址,可以方便将地址空间 分为一个一个块

1.3.1 单播地址#

大多IPv4地址块被 最终细分为一个地址,用于 识别链接Internet或某些专用的内联网计算机网络接口。这些就是单播地址(IPv4 大部分都是单播地址空间)

除了单播地址,还有 广播,组播和任播地址

1.3.2 分类寻址#

  • 每个IP单播地址 = 网络部分(识别接口使用的iP地址在哪个网络可被发现) + 主机地址(识别网络部分下的特定主机)。 因此地址中一些连续位称为 网络号,其余称为 主机号
  • 现实中 不同网络下主机数量不一, 每台主机都需要一个唯一IP。 -- 方案1:基于当前或预计主机数量,将不同大小的IP地址空间分配给不同的站点。

整数溢出#

c语言中,32位机器的int 长度为32 位,表示的范围: [-2147483648, 2147483647], 超过这个范围就会溢出了。 由于整数溢出现象的存在,程序员需要结合业务场景,谨慎选择数据类型

而在python中,就没有整数溢出的烦恼。 Python 可以计算十的一百次方,这在其他语言是不可想象的:

1
2
>>> 10 ** 100
10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
也许我们过去都接触过 c语言大整数的实现,接下来我们来看看python如何实现大整数。

int 对象#

int 对象在 Include/longobject.h 头文件中定义:

1
typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */
顺着注释去 Include/longintrepr.h 中,找到了实现 int 对象的结构体:
1
2
3
4
struct _longobject {
PyObject_VAR_HEAD /*可变长对象都具有的公共头部*/
digit ob_digit[1]; /*这里存储int的整数值*/
};
在 Include/longintrepr.h 头文件,可以找到 digit 字段的定义:
1
2
3
4
5
6
7
#if PYLONG_BITS_IN_DIGIT == 30
typedef uint32_t digit;
// ...
#elif PYLONG_BITS_IN_DIGIT == 15
typedef unsigned short digit;
// ...
#endif
由此可知digit 就是一个 C 语言整数,因此 int 对象是通过整数数组来实现大整数的。至于整数数组用什么整数类型来实现, Python 提供了两个版本,一个是 32 位的 uint32_t ,一个是 16 位的 unsigned short ,编译 Python 解析器时可以 通过宏定义指定选用的版本

Python 作者为什么要这样设计呢?这主要是 出于内存方面的考量:对于范围不大的整数,用 16 位整数表示即可,用 32 位就有点浪费。

整数对象| 对象大小(16位)| 对象大小(32位) -:-|-:-|-:- 1|24 + 2 * 1 = 26|24 + 4 * 1 = 28 1000000 |24 + 2 * 2 = 28|24 + 4 * 1 = 28 10000000000 |24 + 2 * 3 = 30|24 + 4 * 2 = 32


Q:ob_digit 数组长度可能大于1,而为什么在结构体定义中, ob_digit 数组长度却固定为 1?#

由于 C 语言中 数组长度不是类型信息,我们可以 根据实际需要为 ob_digit 数组分配足够的内存,并将其当成长度为 n 的数组操作。这也是 C 语言中一个常用的编程技巧。长度信息在 PyVarObject(PyVarObject比PyObjcet多了个ob_size字段,详细定义可以看[python源码分析] 1.对象)中的ob_size中

实现大整数#

整数分为 正数 、 负数 和 零 , Python 规定不同整数在 int 对象中的存储方式,要点可以总结为 3 条:

整数 绝对值 根据实际情况分为若干部分,保存于 ob_digit 数组中; ob_digit 数组长度 保存于 ob_size 字段,对于 负整数 的情况,ob_size 为负(这里可以说就很精妙了); 整数 零 以 ob_size 等于 0 来表示ob_digit 数组为空; 接下来,我们以 5 个典型的例子详细介绍这几条规则:

  1. 对于整数 0 , ob_size 字段等于 0 , ob_digit 数组为空,无需分配。
  2. 对于整数 10 ,其绝对值保存于 ob_digit 数组中,数组长度为 1 , ob_size 字段等于 1 。
  3. 对于整数 -10 ,其绝对值同样保存于 ob_digit 数组中,但由于 -10 为负数, ob_size 字段等于 -1
  4. 对于整数 1073741824 ( 2 的 30 次方),由于 Python 只使用 32 整数的后 30 位,因此 需要另一个整数才能存储,整数数组长度为 2 。绝对值这样计算:\(2^{30}*1+2^0*0=10737418242\)
  5. 对于整数 -4294967297 (负的 2 的 32 次方加 1 ),同样要长度为 2 的 ob_digit 数组,但 ob_size 字段为负。绝对值这样计算:\(2^{30}*4+2^0*1=42949672972\)

为什么 Python 只用 ob_digit 数组整数的后 30 位?#

这跟 加法进位有关。如果全部 32 位都用来保存绝对值,那么为了保证加法不溢出(产生进位),需要先强制转换成 64 位类型后在进行计算。但 牺牲最高 1 位后,加法运算便不用担心进位溢出了。那么,为什么 Python 牺牲最高 2 位呢?应该是 为了和 16 位整数方案统一起来:如果选用 16 位整数作为数组, Python 则只使用其中 15 位

小整数静态对象池#

小整数对象池在 Objects/longobject.c 中实现,关键代码如下:

1
2
3
4
5
6
7
8
9
/*小整数池的范围通过宏来定义的, 默认是-5-257,我们可以通过修改此处的宏来调整小整数池的大小, 但是需要对python进行重新编译*/
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257 /*该宏规定了对象池 正数个数 (从 0 开始,包括 0 ),默认 257 个*/
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5 /*该宏规定了对象池 负数个数 ,默认 5 个*/
#endif

static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS]; /*一个整数对象数组,保存预先创建好的小整数对象*/

如果在[-5, 257)范围内,会直接返回存于small_ints的对象,所以小整数只会存在一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// longobject.c
static PyObject *
get_small_int(sdigit ival)
{
PyObject *v;
assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);
#ifdef COUNT_ALLOCS
if (ival >= 0)
quick_int_allocs++;
else
quick_neg_int_allocs++;
#endif
return v;

至于为什么选择静态缓存从 -5 到 256 之间的小整数,主要是出于某种 权衡 :这个范围内的整数使用 频率很高 ,而缓存这些小整数的 内存开销相对可控 。很多程序开发场景都没有固定的正确答案,需要根据实际情况平衡利弊。

理解了静态对象池,如下现象就很好理解了:

1
2
3
4
5
6
7
8
9
>>> a = 1 + 0
>>> b = 1 * 1
>>> id(a), id(b)
(4408209536, 4408209536)

>>> c = 1000 + 0
>>> d = 1000 * 1
>>> id(c), id(d)
(4410298224, 4410298160)

由于整数对象是 不可变对象 ,任何整数运算结果都以新对象返回,而对象创建销毁开销却不小。为了优化整数对象的性能, Python 在启动时将使用 频率较高 的小整数预先创建好,这就是 小整数缓存池 。默认情况下,小整数缓存池缓存 从 -5 到 256 之间的整数

数学运算#

根据我们在 PyTypeObject 中学到的知识,对象的行为由对象的 类型 决定。因此,整数对象 数学运算的秘密藏在整数类型对象中。在 Objects/longobject.c 中找到整数类型对象( PyLong_Type ),其定义如下所示:
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
PyTypeObject PyLong_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"int", /* tp_name */
offsetof(PyLongObject, ob_digit), /* tp_basicsize */
sizeof(digit), /* tp_itemsize */
0, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
long_to_decimal_string, /* tp_repr */
&long_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
(hashfunc)long_hash, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
Py_TPFLAGS_LONG_SUBCLASS, /* tp_flags */
long_doc, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
long_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
long_methods, /* tp_methods */
0, /* tp_members */
long_getset, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
long_new, /* tp_new */
PyObject_Del, /* tp_free */
};
类型对象中, tp_as_number 是一个关键字段。该字段指向一个 PyNumberMethods 结构体,结构体保存了 各种数学运算的 函数指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static PyNumberMethods long_as_number = {
(binaryfunc)long_add, /*nb_add*/
(binaryfunc)long_sub, /*nb_subtract*/
(binaryfunc)long_mul, /*nb_multiply*/
long_mod, /*nb_remainder*/
long_divmod, /*nb_divmod*/
long_pow, /*nb_power*/
(unaryfunc)long_neg, /*nb_negative*/
(unaryfunc)long_long, /*tp_positive*/
(unaryfunc)long_abs, /*tp_absolute*/
(inquiry)long_bool, /*tp_bool*/
(unaryfunc)long_invert, /*nb_invert*/
long_lshift, /*nb_lshift*/
(binaryfunc)long_rshift, /*nb_rshift*/
long_and, /*nb_and*/
long_xor, /*nb_xor*/
long_or, /*nb_or*/
long_long, /*nb_int*/
// ...
};
下图展示了 整数对象 、 整数类型对象 以及 整数数学运算处理函数 之间的关系:

加法#

如何为一个由数组表示的大整数实现加法?问题答案得在 long_add 函数中找,该函数是整数对象 加法处理函数 。我们再接再厉,扒开 long_add 函数看个究竟(同样位于 Objects/longobject.c ):

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
static PyObject *
long_add(PyLongObject *a, PyLongObject *b)
{
/*定义变量 z 用于临时保存计算结果*/
PyLongObject *z;

CHECK_BINOP(a, b);

/*
如果参与运算的整数对象底层数组长度均不超过 1 ,直接用 MEDIUM_VALUE 宏将整数对象转化成 C 整数类型进行运算,
性能损耗极小。满足这个条件的整数范围在 -1073741823~1073741823 之间,足以覆盖程序运行时的绝大部分运算场景
*/

if (Py_ABS(Py_SIZE(a)) <= 1 && Py_ABS(Py_SIZE(b)) <= 1) {
return PyLong_FromLong(MEDIUM_VALUE(a) + MEDIUM_VALUE(b));
}
if (Py_SIZE(a) < 0) {
if (Py_SIZE(b) < 0) {
/*如果两个整数均为 负数 ,调用 x_add 计算两者绝对值之和,再将结果符号设置为负( 16 行处)*/
z = x_add(a, b);
if (z != NULL) {
assert(Py_REFCNT(z) == 1);
Py_SIZE(z) = -(Py_SIZE(z));
}
}
/*如果 a 为负数, b 为正数,调用 x_sub 计算 b 和 a 的绝对值之差即为最终结果*/
else
z = x_sub(b, a);
}
else {
if (Py_SIZE(b) < 0)
z = x_sub(a, b);
else
/*如果两个整数均为正数,调用 x_add 计算两个绝对值之和即为最终结果*/
z = x_add(a, b);
}
return (PyObject *)z;
}
### x_add x_add 用于计算两个整数对象绝对值之和,源码同样位于 Objects/longobject.c :
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
static PyLongObject *
x_add(PyLongObject *a, PyLongObject *b)
{
/*取ob_size的绝对值*/
Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));


/*用变量z 临时存储计算结果*/
PyLongObject *z;
Py_ssize_t i;
/*临时进位*/
digit carry = 0;

/* Ensure a is the larger of the two: */
if (size_a < size_b) {
/*如果 a 数组长度比较小,将 a 、 b 交换,数组长度较大的那个在前面*/
{ PyLongObject *temp = a; a = b; b = temp; }
{ Py_ssize_t size_temp = size_a;
size_a = size_b;
size_b = size_temp; }
}
/*创建新整数对象,用于保存计算结果(注意到长度必须比 a 和 b 都大一,因为可能有进位)*/
z = _PyLong_New(size_a+1);

if (z == NULL)
return NULL;
/*遍历 b 底层数组,与 a 对应部分相加并保存到 z 中,需要特别注意进位计算*/
for (i = 0; i < size_b; ++i) {
carry += a->ob_digit[i] + b->ob_digit[i];
z->ob_digit[i] = carry & PyLong_MASK;
carry >>= PyLong_SHIFT;
}
/*遍历 a 底层数组剩余部分,与进位相加后保存到 z 中,同样需要特别注意进位计算*/
for (; i < size_a; ++i) {
carry += a->ob_digit[i];
z->ob_digit[i] = carry & PyLong_MASK;
carry >>= PyLong_SHIFT;
}
/*将进位写入 z 底层数组最高位单元中*/
z->ob_digit[i] = carry;
/*去除计算结果 z 底层数组中前面多余的零,因为最后的进位可能为零*/
return long_normalize(z);
}

reference#

  1. Python 源码深度剖析/07 int 对象,永不溢出的整数
  2. Python 源码深度剖析/08 int 源码解析:如何实现大整数运算?
  3. Python3源码—整数对象

进程相关指令 ps#

参考自 ps 进程查看器

进程状态#

ps工具标识进程的5种状态码: - D 不可中断 uninterruptible sleep (usually IO) - R 运行 runnable (on run queue) - S 中断 sleeping - T 停止 traced or stopped - Z 僵死 a defunct (”zombie”) process

命令参数#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
a       显示 所有进程(显示状态码)
-a 显示同一终端下的所有程序
-A 所有进程
e 环境变量
c 进程的 真实名称

r 当前 终端的进程
T 当前 终端的所有程序
u 用户的所有进程
-au 较详细的资讯
-aux 所有包含其他使用者的行程

-N 反向选择
-e 等于“-A”
f 显示程序间的关系
-H 显示树状结构

-C<命令> 列出指定命令的状况
–lines<行数> 每页显示的行数
–width<字符数> 每页显示的字符数
–help 显示帮助信息
–version 显示版本显示

输出列含义#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
F           代表这个程序的旗标 (flag), 4 代表使用者为 super user
S 代表这个程序的状态 (STAT),关于各 STAT 的意义将在内文介绍
UID 程序被该 UID 所拥有
PID 进程的ID
PPID 则是其 上级父程序的ID
C CPU 使用的资源百分比
PRI 这个是 Priority (优先执行序) 的缩写,详细后面介绍
NI 这个是 Nice 值,在下一小节我们会持续介绍
ADDR 这个是 kernel function,指出该程序在内存的那个部分。如果是个 running的程序,一般就是 “-“
SZ 使用掉的内存大小
WCHAN 目前这个程序是否正在运作当中,若为 - 表示正在运作
TTY 登入者的终端机位置
TIME 使用掉的 CPU 时间。
CMD 所下达的指令为何

其它常用指令#

linux环境中英文切换配置以及乱码问题#

基础配置#

本质就是修改系统的LANG变量

LANG是language的简称,稍微有英语基础的用户一看就看出来这个变量是决定系统的默认语言的,即系统的菜单、程序的工具栏语言、输入法默 认语言等。

查看当前用户的LANG变量#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
liuw@12:~locale

LANG=zh_CN
LANGUAGE=
LC_CTYPE="zh_CN"
LC_NUMERIC="zh_CN"
LC_TIME="zh_CN"
LC_COLLATE="zh_CN"
LC_MONETARY="zh_CN"
LC_MESSAGES=en_US
LC_PAPER="zh_CN"
LC_NAME="zh_CN"
LC_ADDRESS="zh_CN"
LC_TELEPHONE="zh_CN"
LC_MEASUREMENT="zh_CN"
LC_IDENTIFICATION="zh_CN"
LC_ALL=

配置自己的LANG#

如果你希望修改整个系统的编码和语言信息,可以修改系统的配置文件修改LANG,而如果不希望影响其他用户直接在 自己的~/.bashrc中配置LANG即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# use english
export LANG=en_US.UTF-8

LANG=en_US.UTF-8
LANGUAGE=
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES=en_US
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=
配置后如图:

SVN: Can't convert string from 'UTF-8' to native encoding问题#

上图可以看到,原本的'LC_ALL'为空,对中文因此出现了这个问题

1
LC_ALL=
解决方案:
1
export LC_ALL=zh_CN.UTF-8     # 配置~/.bashrc中 LC_ALL

查询linux系统类型#

1
lsb_release -a

这个命令适用于所有遵守LSB规范的的linux,包括Redhat、SuSE、Debian、Ubuntu、Centos等发行版

显示系统核心信息#

1
2
3
4
5
uname

uname -r

uname -a

内存信息#

1
cat /proc/meminfo

CPU信息#

1
cat /proc/cpuinfo

查看当前路径#

1
pwd

重命名文件 mv#

例子:将目录A重命名为B

1
mv A B
例子:将/a目录移动到/b下,并重命名为c
1
mv /a /b/c

搜索文件 find#

1
2
3
4
5
6
7
8
9
10
11
find / -mtime 0   #0代表当前时间,即从现在到24小时前,有改动过内容的文件都会被列出来

find /etc -newer /etc/passwd #寻找/etc下面的文件,如果文件日期比/etc/passwd新就列出

find / -name file #/代表全文搜索

find /home -user Anmy #查找/home下属于Anmy的文件

find / -nouser #查找系统中不属于任何人的文件,可以轻易找出那些不太正常的文件

find / -name passed #查找文件名为passed的文件