知识汇总:网络、操作系统、docker、协程、C++

网络:https://coolshell.cn/articles/11564.html

https://coolshell.cn/articles/11609.html

UDP可靠性实现:https://www.infoq.cn/article/how-to-make-udp-reliable

epoll:https://www.nowcoder.com/discuss/26226

https://mp.weixin.qq.com/s/O_QVxhyS7C3gJluqaLerWQ

Io_uring:https://zhuanlan.zhihu.com/p/62682475?utm_source=wechat_timeline

https://developer.aliyun.com/article/764720

https://zhuanlan.zhihu.com/p/31852168

wireshark捕获过滤器语法:https://biot.com/capstats/bpf.html

Alpn:https://tools.ietf.org/html/rfc7301

Http2:https://tools.ietf.org/html/rfc7540#section-6.5.1

HTTP3:https://tools.ietf.org/html/draft-ietf-quic-http-27

TCP性能和发送接收Buffer的关系:https://yq.aliyun.com/articles/720202?spm=ata.13261165.0.0.733571cdHp5EHT

SACK 优化会引起拒绝服务攻击吗?https://www.ibm.com/developerworks/cn/linux/l-tcp-sack/

密钥协商机制:https://www.jianshu.com/p/b1d6996d2f51

协程:https://zhuanlan.zhihu.com/p/27409164

https://github.com/chenyahui/AnnotatedCode/tree/master/libco

https://github.com/chenyahui/AnnotatedCode/tree/master/coroutine

https://www.cyhone.com/articles/analysis-of-libco/

https://www.cyhone.com/articles/analysis-of-cloudwu-coroutine/

https://mthli.xyz/stackful-stackless/

Stl源码分析:https://www.kancloud.cn/digest/stl-sources/177264

迭代器和traits:https://blog.csdn.net/terence1212/article/details/52287762?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EsearchFromBaidu%7Edefault-1.pc_relevant_baidujshouduan&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EsearchFromBaidu%7Edefault-1.pc_relevant_baidujshouduan

LD_PRELOAD环境变量:https://blog.csdn.net/haoel/article/details/1602108

docker参考资料:

https://coolshell.cn/articles/18190.html 

https://coolshell.cn/articles/17010.html

https://coolshell.cn/articles/17029.html

https://coolshell.cn/articles/17049.html

https://coolshell.cn/articles/17061.html

https://coolshell.cn/articles/17200.html

容器,其实是一种特殊的进程而已,多了一些隔离与限制。

Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统。这些都是我这台机器当前可以被 Cgroups 进行限制的资源种类。

Namespace 技术则是用来修改进程视图的方法。用 clone() 系统调用创建一个新进程时,可以在参数中指定 CLONE_NEWPID等,以限制进程能够看到的资源。Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是Linux 操作系统里的第一个 Namespace。而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。

操作系统:

在线查看Linux源码网站 :https://elixir.bootlin.com/

内核初始化

- 内核初始化, 运行 `start_kernel()` 函数(位于 init/main.c), 初始化做三件事

    - 创建样板进程, 及各个模块初始化

    - 创建管理/创建用户态进程的进程

    - 创建管理/创建内核态进程的进程

---

- 创建样板进程,及各个模块初始化

    - 创建第一个进程, 0号进程. `set_task_stack_end_magic(&init_task)` and `struct task_struct init_task = INIT_TASK(init_task)`

    - 初始化中断, `trap_init()`. 系统调用也是通过发送中断进行, 由 `set_system_intr_gate()` 完成.

    - 初始化内存管理模块, `mm_init()`

    - 初始化进程调度模块, `sched_init()`

    - 初始化基于内存的文件系统 rootfs, `vfs_caches_init()`

        - VFS(虚拟文件系统)将各种文件系统抽象成统一接口

    - 调用 `rest_init()` 完成其他初始化工作

---

- 创建管理/创建用户态进程的进程, 1号进程

    - `rest_init()` 通过 `kernel_thread(kernel_init,...)` 创建 1号进程(工作在用户态).

    - 权限管理

        - x86 提供 4个 Ring 分层权限

        - 操作系统利用: Ring0-内核态(访问核心资源); Ring3-用户态(普通程序)

    - 用户态调用系统调用: 用户态-系统调用-保存寄存器-内核态执行系统调用-恢复寄存器-返回用户态

    - 新进程执行 kernel_init 函数, 先运行 ramdisk 的 /init 程序(位于内存中)

        - 首先加载 ELF 文件

        - 设置用于保存用户态寄存器的结构体

        - 返回进入用户态

        - /init 加载存储设备的驱动

     - kernel_init 函数启动存储设备文件系统上的 init

---

- 创建管理/创建内核态进程的进程, 2号进程

    - `rest_init()` 通过 `kernel_thread(kthreadd,...)` 创建 2号进程(工作在内核态).

    - `kthreadd` 负责所有内核态线程的调度和管理

9.系统调用

32 位的系统调用是如何执行的

64位

10.进程(*)

可重定位目标文件

ELF 文件的头是用于描述整个文件的。这个文件格式在内核中有定义,分别为 struct elf32_hdr 和 struct elf64_hdr。

接下来我们来看一个一个的 section,我们也叫节。

.text:放编译好的二进制可执行代码

.data:已经初始化好的全局变量

.rodata:只读数据,例如字符串常量、const 的变量

.bss:未初始化全局变量,运行时会置 0

.symtab:符号表,记录的则是函数和变量

.strtab:字符串表、字符串常量和变量名

可执行文件

这个格式和.o 文件大致相似,还是分成一个个的 section,并且被节头表描述。只不过这些 section 是多个.o 文件合并过的。但是这个时候,这个文件已经是马上就可以加载到内存里面执行的文件了,因而这些 section 被分成了需要加载到内存里面的代码段、数据段和不需要加载到内存里面的部分,将小的 section 合成了大的段 segment,并且在最前面加一个段头表(Segment Header Table)。在代码里面的定义为 struct elf32_phdr 和 struct elf64_phdr,这里面除了有对于段的描述之外,最重要的是 p_vaddr,这个是这个段加载到内存的虚拟地址。e_entry,也是个虚拟地址,是这个程序运行的入口。

编译生成动态链接库

gcc -shared -fPIC -o libdynamicprocess.so process.o

当运行一个程序的时候,首先寻找动态链接库,然后加载它。默认情况下,系统在 /lib 和 /usr/lib 文件夹下寻找动态链接库。如果找不到就会报错,我们可以设定 LD_LIBRARY_PATH 环境变量,程序运行时会在此环境变量指定的文件夹下寻找动态链接库。

基于动态链接库创建出来的二进制文件格式还是 ELF,但是稍有不同。首先,多了一个.interp 的 Segment,这里面是 ld-linux.so,这是动态链接器,也就是说,运行时的链接动作都是它做的。另外,ELF 文件中还多了两个 section,一个是.plt,过程链接表(Procedure Linkage Table,PLT),一个是.got.plt,全局偏移量表(Global Offset Table,GOT)。它们是怎么工作的,使得程序运行的时候,可以将 so 文件动态链接到进程空间的呢?dynamiccreateprocess 这个程序要调用 libdynamicprocess.so 里的 create_process 函数。由于是运行时才去找,编译的时候,压根不知道这个函数在哪里,所以就在 PLT 里面建立一项 PLT[x]。这一项也是一些代码,有点像一个本地的代理,在二进制程序里面,不直接调用 create_process 函数,而是调用 PLT[x]里面的代理代码,这个代理代码会在运行的时候找真正的 create_process 函数。去哪里找代理代码呢?这就用到了 GOT,这里面也会为 create_process 函数创建一项 GOT[y]。这一项是运行时 create_process 函数在内存中真正的地址。如果这个地址在 dynamiccreateprocess 调用 PLT[x]里面的代理代码,代理代码调用 GOT 表中对应项 GOT[y],调用的就是加载到内存中的 libdynamicprocess.so 里面的 create_process 函数了。但是 GOT 怎么知道的呢?对于 create_process 函数,GOT 一开始就会创建一项 GOT[y],但是这里面没有真正的地址,因为它也不知道,但是它有办法,它又回调 PLT,告诉它,你里面的代理代码来找我要 create_process 函数的真实地址,我不知道,你想想办法吧。PLT 这个时候会转而调用 PLT[0],也即第一项,PLT[0]转而调用 GOT[2],这里面是 ld-linux.so 的入口函数,这个函数会找到加载到内存中的 libdynamicprocess.so 里面的 create_process 函数的地址,然后把这个地址放在 GOT[y]里面。下次,PLT[x]的代理函数就能够直接调用了。

exec 这个系统调用最终调用的 load_elf_binary。

包含 p 的函数(execvp, execlp)会在 PATH 路径下面寻找程序;

不包含 p 的函数需要输入程序的全路径;

包含 v 的函数(execv, execvp, execve)以数组的形式接收参数;

包含 l 的函数(execl, execlp, execle)以列表的形式接收参数;

包含 e 的函数(execve, execle)以数组的形式接收环境变量。

进程树

ps -ef 会发现,PID 1 的进程就是我们的 init 进程 systemd(所有用户进程的父进程),PID 2 的进程是内核线程 kthreadd(所有内核进程的父进程),这两个我们在内核启动的时候都见过。其中用户态的不带中括号,内核态的带中括号。

总结

readelf 工具用于分析 ELF 的信息,objdump 工具用来显示二进制文件的信息,hexdump 工具用来查看文件的十六进制编码,nm 工具用来显示关于指定文件中符号的信息。

11.线程

对于任何一个进程来讲,即便我们没有主动去创建线程,进程也是默认有一个主线程的。

线程的创建和运行:

设置属性 PTHREAD_CREATE_JOINABLE。这表示将来主线程程等待这个线程的结束,并获取退出时的状态。

线程的数据:

1 线程栈上的本地数据:栈的大小可以通过命令 ulimit -a 查看,默认情况下线程栈大小为 8192(8MB)。我们可以使用命令 ulimit -s 修改。

可以通过下面这个函数 pthread_attr_t,修改线程栈的大小。

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

2 在整个进程里共享的全局数据

3 线程私有数据

数据保护:

1 mutex

总结:

12 task_struct 

任务ID

pid_t pid;

pid_t tgid;

struct task_struct *group_leader;

线程有自己的 pid,tgid 就是进程的主线程的 pid,group_leader 指向的就是进程的主线程。

任务状态

 volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */

 int exit_state;

 unsigned int flags;

state(状态)可以取的值:

/* Used in tsk->state: */

#define TASK_RUNNING                    0

#define TASK_INTERRUPTIBLE              1

#define TASK_UNINTERRUPTIBLE            2

#define __TASK_STOPPED                  4

#define __TASK_TRACED                  8

/* Used in tsk->exit_state: */

#define EXIT_DEAD                      16

#define EXIT_ZOMBIE                    32

#define EXIT_TRACE                      (EXIT_ZOMBIE | EXIT_DEAD)

/* Used in tsk->state again: */

#define TASK_DEAD                      64

#define TASK_WAKEKILL                  128

#define TASK_WAKING                    256

#define TASK_PARKED                    512

#define TASK_NOLOAD                    1024

#define TASK_NEW                        2048

#define TASK_STATE_MAX                  4096

-TASK_INTERRUPTIBLE,可中断的睡眠状态。这是一种浅睡眠的状态,也就是说,虽然在睡眠,等待 I/O 完成,但是这个时候一个信号来的时候,进程还是要被唤醒。只不过唤醒后,不是继续刚才的操作,而是进行信号处理。

-TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。这是一种深度睡眠状态,不可被信号唤醒,只能死等 I/O 操作完成。

TASK_KILLABLE,可以终止的新睡眠状态。进程处于这种状态中,它的运行原理类似 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号。

-TASK_WAKEKILL 用于在接收到致命信号时唤醒进程,而 TASK_KILLABLE 相当于这两位都设置了。——  #define TASK_KILLABLE          (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)

总结

13 task_struct 

运行统计信息

u64        utime;//用户态消耗的CPU时间

u64        stime;//内核态消耗的CPU时间

unsigned long      nvcsw;//自愿(voluntary)上下文切换计数

unsigned long      nivcsw;//非自愿(involuntary)上下文切换计数

u64        start_time;//进程启动时间,不包含睡眠时间

u64        real_start_time;//进程启动时间,包含睡眠时间

进程亲缘关系

如果在 bash 上使用 GDB 来 debug 一个进程,这个时候 GDB 是 parent,bash 是这个进程的 real_parent。

内存管理

struct mm_struct                *mm;

struct mm_struct                *active_mm;

进程权限

/*谁能操作我 Objective and real subjective task credentials (COW): */

const struct cred __rcu        *real_cred;

/*我能操作谁Effective (overridable) subjective task credentials (COW): */

const struct cred __rcu        *cred;

struct cred {

......

        kuid_t          uid;            /* real UID of the task */

        kgid_t          gid;            /* real GID of the task */

        kuid_t          suid;          /* saved UID of the task */

        kgid_t          sgid;          /* saved GID of the task */

        kuid_t          euid;          /* effective UID of the task */

        kgid_t          egid;          /* effective GID of the task */

        kuid_t          fsuid;          /* UID for VFS ops */

        kgid_t          fsgid;          /* GID for VFS ops */

......

        kernel_cap_t    cap_inheritable; /* caps our children can inherit */

        kernel_cap_t    cap_permitted;  /* caps we're permitted */

        kernel_cap_t    cap_effective;  /* caps we can actually use */

        kernel_cap_t    cap_bset;      /* capability bounding set */

        kernel_cap_t    cap_ambient;    /* Ambient capability set */

......

} __randomize_layout;

第一个是 uid 和 gid,注释是 real user/group id。一般情况下,谁启动的进程,就是谁的 ID。但是权限审核的时候,往往不比较这两个,也就是说不大起作用。第二个是 euid 和 egid,注释是 effective user/group id。一看这个名字,就知道这个是起“作用”的。当这个进程要操作消息队列、共享内存、信号量等对象的时候,其实就是在比较这个用户和组是否有权限。第三个是 fsuid 和 fsgid,也就是 filesystem user/group id。这个是对文件操作会审核的权限。一般说来,fsuid、euid,和 uid 是一样的,fsgid、egid,和 gid 也是一样的。因为谁启动的进程,就应该审核启动的用户到底有没有这个权限。

我们可以通过 chmod u+s program 命令,给这个游戏程序设置 set-user-ID 的标识位,把游戏的权限变成 rwsr-xr-x。这个时候,用户 A 再启动这个游戏的时候,创建的进程 uid 当然还是用户 A,但是 euid 和 fsuid 就不是用户 A 了,因为看到了 set-user-id 标识,就改为文件的所有者的 ID,也就是说,euid 和 fsuid 都改成用户 B 了,这样A就能够将通关结果保存。

文件&文件系统

/* Filesystem information: */

struct fs_struct                *fs;

/* Open file information: */

struct files_struct            *files;

13.task_struct

用户态函数栈  32 位

64 bit

64 位操作系统的寄存器数目比较多。rax 用于保存函数调用的返回结果。栈顶指针寄存器变成了 rsp,指向栈顶位置。堆栈的 Pop 和 Push 操作会自动调整 rsp,栈基指针寄存器变成了 rbp,指向当前栈帧的起始位置。改变比较多的是参数传递。rdi、rsi、rdx、rcx、r8、r9 这 6 个寄存器,用于传递存储函数调用时的 6 个参数。如果超过 6 的时候,还是需要放到栈里面。然而,前 6 个参数有时候需要进行寻址,但是如果在寄存器里面,是没有地址的,因而还是会放到栈里面,只不过放到栈里面的操作是被调用函数做的。

内核态函数栈

Linux 给每个 task 都分配了内核栈,也就是stack成员。在 32 位系统上8K。在 64 位系统上 16K,并且要求起始地址必须是 8192 的整数倍

pt_regs:系统调用从用户态到内核态的时候,首先要做的第一件事情,就是将用户态运行过程中的 CPU 上下文保存起来,其实主要就是保存在这个结构的寄存器变量pt_regs里。这样当从内核系统调用返回的时候,才能让进程在刚才的地方接着运行下去。系统调用的时候,压栈的值的顺序和 struct pt_regs 中寄存器定义的顺序是一样的。

总结:

在用户态,应用程序进行了至少一次函数调用。32 位和 64 的传递参数的方式稍有不同,32 位的就是用函数栈,64 位的前 6 个参数用寄存器,其他的用函数栈。

在内核态,32 位和 64 位都使用内核栈,格式也稍有不同,主要集中在 pt_regs 结构上。

在内核态,32 位和 64 位的内核栈和 task_struct 的关联关系不同。32 位主要靠 thread_info,64 位主要靠 Per-CPU 变量。

36.IPC

1 管道

命令行中用到的“|” 表示的管道称为匿名管道

另外一种类型是命名管道。这个类型的管道需要通过 mkfifo 命令显式地创建。

mkfifo hello

# ls -l  # 文件的类型是 p,就是 pipe 的意思。

prw-r--r--  1 root root        0 May 21 23:29 hello

# echo "hello world" > hello

# cat < hello 

hello world

2 共享内存加信号量是常用的模式

3 信号

37 38.信号

如何通过 API 注册一个信号处理函数,整个过程如下

在用户程序里面,有两个函数可以调用,一个是 signal,一个是 sigaction,推荐使用 sigaction。

用户程序调用的是 Glibc 里面的函数,signal 调用的是 __sysv_signal,里面默认设置了一些参数,使得 signal 的功能受到了限制,sigaction 调用的是 __sigaction,参数用户可以任意设定。

无论是 __sysv_signal 还是 __sigaction,调用的都是统一的一个系统调用 rt_sigaction。

在内核中,rt_sigaction 调用的是 do_sigaction 设置信号处理函数。

在每一个进程的 task_struct 里面,都有一个 sighand 指向 struct sighand_struct,里面是一个数组,下标是信号,里面的内容是信号处理函数。

信号的发送与处理步骤:

假设我们有一个进程 A,main 函数里面调用系统调用进入内核。

按照系统调用的原理,会将用户态栈的信息保存在 pt_regs 里面,也即记住原来用户态是运行到了 line A 的地方。

在内核中执行系统调用读取数据。

当发现没有什么数据可读取的时候,只好进入睡眠状态,并且调用 schedule 让出 CPU,这是进程调度第一定律。

将进程状态设置为 TASK_INTERRUPTIBLE,可中断的睡眠状态,也即如果有信号来的话,是可以唤醒它的。

其他的进程或者 shell 发送一个信号,有四个函数可以调用 kill、tkill、tgkill、rt_sigqueueinfo。

四个发送信号的函数,在内核中最终都是调用 do_send_sig_info。

do_send_sig_info 调用 send_signal 给进程 A 发送一个信号,其实就是找到进程 A 的 task_struct,或者加入信号集合,为不可靠信号,或者加入信号链表,为可靠信号。

do_send_sig_info 调用 signal_wake_up 唤醒进程 A。进程 A 重新进入运行状态 TASK_RUNNING,根据进程调度第一定律,一定会接着 schedule 运行。

进程 A 被唤醒后,检查是否有信号到来,如果没有,重新循环到一开始,尝试再次读取数据,如果还是没有数据,再次进入 TASK_INTERRUPTIBLE,即可中断的睡眠状态。

当发现有信号到来的时候,就返回当前正在执行的系统调用,并返回一个错误表示系统调用被中断了。

系统调用返回的时候,会调用 exit_to_usermode_loop。

这是一个处理信号的时机。

调用 do_signal 开始处理信号。

根据信号,得到信号处理函数 sa_handler,然后修改 pt_regs 中的用户态栈的信息,让 pt_regs 指向 sa_handler。

同时修改用户态的栈,插入一个栈帧 sa_restorer,里面保存了原来的指向 line A 的 pt_regs,并且设置让 sa_handler 运行完毕后,跳到 sa_restorer 运行。

返回用户态,由于 pt_regs 已经设置为 sa_handler,则返回用户态执行 sa_handler。

sa_handler 执行完毕后,信号处理函数就执行完了,接着根据第 15 步对于用户态栈帧的修改,会跳到 sa_restorer 运行。

sa_restorer 会调用系统调用 rt_sigreturn 再次进入内核。

在内核中,rt_sigreturn 恢复原来的 pt_regs,重新指向 line A。

从 rt_sigreturn 返回用户态,还是调用 exit_to_usermode_loop。

这次因为 pt_regs 已经指向 line A 了,于是就到了进程 A 中,接着系统调用之后运行,当然这个系统调用返回的是它被中断了,没有执行完的错误。

40.共享内存与信号量

无论是共享内存还是信号量,创建与初始化都遵循同样流程,通过 ftok 得到 key,通过 xxxget 创建对象并生成 id;

生产者和消费者都通过 shmat 将共享内存映射到各自的内存空间,在不同的进程里面映射的位置不同;

为了访问共享内存,需要信号量进行保护,信号量需要通过 semctl 初始化为某个值;

接下来生产者和消费者要通过 semop(-1) 来竞争信号量,如果生产者抢到信号量则写入,然后通过 semop(+1) 释放信号量,如果消费者抢到信号量则读出,然后通过 semop(+1) 释放信号量;

共享内存使用完毕,可以通过 shmdt 来解除映射。

56.容器

无论是容器,还是虚拟机,都依赖于内核中的技术,虚拟机依赖的是 KVM,容器依赖的是 namespace 和 cgroup 对进程进行隔离。

为了运行 Docker,有一个 daemon 进程 Docker Daemon 用于接收命令行。为了描述 Docker 里面运行的环境和应用,有一个 Dockerfile,通过 build 命令称为容器镜像。容器镜像可以上传到镜像仓库,也可以通过 pull 命令从镜像仓库中下载现成的容器镜像。通过 Docker run 命令将容器镜像运行为容器,通过 namespace 和 cgroup 进行隔离,容器里面不包含内核,是共享宿主机的内核的。对比虚拟机,虚拟机在 qemu 进程里面是有客户机内核的,应用运行在客户机的用户态。

58.cgroup

cgroup 的工作机制:

第一步,系统初始化的时候,初始化 cgroup 的各个子系统的操作函数,分配各个子系统的数据结构。第二步,mount cgroup 文件系统,创建文件系统的树形结构,以及操作函数。第三步,写入 cgroup 文件,设置 cpu 或者 memory 的相关参数,这个时候文件系统的操作函数会调用到 cgroup 子系统的操作函数,从而将参数设置到 cgroup 子系统的数据结构中。第四步,写入 tasks 文件,将进程交给某个 cgroup 进行管理,因为 tasks 文件也是一个 cgroup 文件,统一会调用文件系统的操作函数进而调用 cgroup 子系统的操作函数,将 cgroup 子系统的数据结构和进程关联起来。第五步,对于 CPU 来讲,会修改 scheduled entity,放入相应的队列里面去,从而下次调度的时候就起作用了。对于内存的 cgroup 设定,只有在申请内存的时候才起作用。

C++:

1 堆、栈、RAII

C++ 标准里一个相关概念是自由存储区,英文是 free store,特指使用 new 和 delete 来分配和释放内存的区域。一般而言,这是堆的一个子集:new 和 delete 操作的区域是 free storemalloc 和 free 操作的区域是 heap但 new 和 delete 通常底层使用 malloc 和 free 来实现,所以 free store 也是 heap。

值语义和引用语义的区别,是 C++ 的特点,也是它的复杂性的一个来源

Rall:把资源(比如内存、文件、锁等)和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。借助构造函数、析构函数和栈,自动构造和析构对象,避免资源泄露。

2 智能指针

unique_ptr

unique_ptr ptr1(new int(10));      // int智能指针

assert(*ptr1 == 10);                    // 可以使用*取内容

assert(ptr1 != nullptr);                // 可以判断是否为空指针

unique_ptr ptr2(new string("hello"));  // string智能指针

assert(*ptr2 == "hello");                // 可以使用*取内容

assert(ptr2->size() == 5);              // 可以使用->调用成员函数

正如它的名字,表示指针的所有权是“唯一”的,不允许共享,任何时候只能有一个“人”持有它。为了实现这个目的,unique_ptr 应用了 C++ 的“转移”(move)语义,同时禁止了拷贝赋值,所以,在向另一个 unique_ptr 赋值的时候,要特别留意,必须用 std::move() 函数显式地声明所有权转移。

auto ptr1 = make_unique(42);    // 工厂函数创建智能指针

assert(ptr1 && *ptr1 == 42);        // 此时智能指针有效

auto ptr2 = std::move(ptr1);        // 使用move()转移所有权

assert(!ptr1 && ptr2);              // ptr1变成了空指针

shared_ptr

shared_ptr ptr1(new int(10));    // int智能指针

assert(*ptr1 = 10);                    // 可以使用*取内容

shared_ptr ptr2(new string("hello"));  // string智能指针

assert(*ptr2 == "hello");                      // 可以使用*取内容

auto ptr3 = make_shared(42);  // 工厂函数创建智能指针

assert(ptr3 && *ptr3 == 42);      // 可以判断是否为空指针

auto ptr4 = make_shared("zelda");  // 工厂函数创建智能指针

assert(!ptr4->empty());                  // 可以使用->调用成员函数

与 unique_ptr 的最大不同点:它的所有权是可以被安全共享的

也就是说支持拷贝赋值,允许被多个“人”同时持有,就像原始指针一样。

auto ptr1 = make_shared(42);    // 工厂函数创建智能指针

assert(ptr1 && ptr1.unique() );    // 此时智能指针有效且唯一

auto ptr2 = ptr1;                  // 直接拷贝赋值,不需要使用move()

assert(ptr1 && ptr2);              // 此时两个智能指针均有效

assert(ptr1 == ptr2);            // shared_ptr可以直接比较

// 两个智能指针均不唯一,且引用计数为2

assert(!ptr1.unique() && ptr1.use_count() == 2); 

assert(!ptr2.unique() && ptr2.use_count() == 2); 

shared_ptr 支持安全共享的秘密在于内部使用了“引用计数”。

因为 shared_ptr 具有完整的“值语义”(即可以拷贝赋值),所以,它可以在任何场合替代原始指针,而不用再担心资源回收的问题

shared_ptr 的注意事项

对象的析构函数,不要有非常复杂、严重阻塞的操作。一旦 指向这个对象的shared_ptr 在某个不确定时间点析构释放资源,就会阻塞整个进程或者线程

shared_ptr 的引用计数也导致了一个新的问题,就是“循环引用”

class Node final

{

public:

    using this_type    = Node;

    using shared_type  = std::shared_ptr;

public:

    shared_type    next;      // 使用智能指针来指向下一个节点

};

auto n1 = make_shared();  // 工厂函数创建智能指针

auto n2 = make_shared();  // 工厂函数创建智能指针

assert(n1.use_count() == 1);    // 引用计数为1

assert(n2.use_count() == 1);

n1->next = n2;                // 两个节点互指,形成了循环引用

n2->next = n1;

assert(n1.use_count() == 2);    // 引用计数为2

assert(n2.use_count() == 2);    // 无法减到0,无法销毁,导致内存泄漏

要从根本上杜绝循环引用,要用到weak_ptr

它专门为打破循环引用而设计,只观察指针,不会增加引用计数(弱引用),但在需要的时候,可以调用成员函数 lock(),获取 shared_ptr(强引用)。

class Node final

{

public:

    using this_type    = Node;

    // 注意这里,别名改用weak_ptr

    using shared_type  = std::weak_ptr;

public:

    shared_type    next;    // 因为用了别名,所以代码不需要改动

};

auto n1 = make_shared();  // 工厂函数创建智能指针

auto n2 = make_shared();  // 工厂函数创建智能指针

n1->next = n2;            // 两个节点互指,形成了循环引用

n2->next = n1;

assert(n1.use_count() == 1);    // 因为使用了weak_ptr,引用计数为1

assert(n2.use_count() == 1);  // 打破循环引用,不会导致内存泄漏

if (!n1->next.expired()) {    // 检查指针是否有效

    auto ptr = n1->next.lock();  // lock()获取shared_ptr

    assert(ptr == n2);

}

3 右值和移动语义

左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:变量、函数或数据成员的名字返回左值引用的表达式,如 ++x、x = 1、cout << ' '字符串字面量如 "hello world"

反之,纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。最常见的情况有:返回非引用类型的表达式,如 x++、x + 1、make_shared(42)除字符串字面量之外的字面量,如 42、true

类型是右值引用的变量是一个左值

值类型:与引用类型相对的

值类别:左值右值的概念

一句话总结,移动语义使得在 C++ 里返回大对象(如容器)的函数和运算符成为现实,因而可以提高代码的简洁性和可读性,提高程序员的生产率。

如何实现移动?

要让你设计的对象支持移动的话,通常需要下面几步:

1 你的对象应该有分开的拷贝构造和移动构造函数(除非你只打算支持移动,不支持拷贝——如 unique_ptr)。

2 你的对象应该有 swap 成员函数,支持和另外一个对象快速交换成员。

3 在你的对象的名空间下,应当有一个全局的 swap 函数,调用成员函数 swap 来实现交换。支持这种用法会方便别人(包括你自己在将来)在其他对象里包含你的对象,并快速实现它们的 swap 函数。

4 实现通用的 operator=。

5 上面各个函数如果不抛异常的话,应当标为 noexcept。这对移动构造函数尤为重要。

引用坍缩和完美转发

我们已经讲了对于一个实际的类型 T,它的左值引用是 T&,右值引用是 T&&。

那么:是不是看到 T&,就一定是个左值引用?是不是看到 T&&,就一定是个右值引用?

对于前者的回答是“是”,对于后者的回答为“否”。

关键在于,在有模板的代码里,对于类型参数的推导结果可能是引用。我们可以略过一些繁复的语法规则,要点是:

1 对于 template foo(T&&) 这样的代码,如果传递过去的参数是左值,T 的推导结果是左值引用;

2 如果传递过去的参数是右值,T 的推导结果是参数的类型本身。

3 如果 T 是左值引用,那 T&& 的结果仍然是左值引用——即 type& && 坍缩成了 type&。

4 如果 T 是一个实际类型,那 T&& 的结果自然就是一个右值引用。

4 容器

xeus-cling 

String 

string作为参数:

1 如果不修改字符串的内容,使用 const string& 或 C++17 的 string_view 作为参数类型。后者是最理想的情况,因为即使在只有 C 字符串的情况,也不会引发不必要的内存复制。

2 如果需要在函数内修改字符串内容、但不影响调用者的该字符串,使用 string 作为参数类型(自动拷贝)。

3 如果需要改变调用者的字符串内容,使用 string& 作为参数类型(通常不推荐)

vector

可以使用 emplace 在指定位置构造一个元素,可以使用 emplace_back 在尾部新构造一个元素

当 push_back、insert、reserve、resize 等函数导致内存重分配时,或当 insert、erase 导致元素位置移动时,vector 会试图把元素“移动”到新的内存区域。vector 通常保证强异常安全性,如果元素类型没有提供一个保证不抛异常的移动构造函数,vector 通常会使用拷贝构造函数。因此,对于拷贝代价较高的自定义元素类型,我们应当定义移动构造函数,并标其为 noexcept,或只在容器中放置对象的智能指针。

C++11 开始提供的 emplace… 系列函数是为了提升容器的性能而设计的。你可以试试把 v1.emplace_back() 改成 v1.push_back(Obj1())。对于 vector 里的内容,结果是一样的;但使用 push_back 会额外生成临时对象,多一次(移动或拷贝)构造和析构。如果是移动的情况,那会有小幅性能损失;如果对象没有实现移动的话,那性能差异就可能比较大了。

deque

由于元素的存储大部分仍然连续,它的遍历性能是比较高的。

由于每一段存储大小相等,deque 支持使用下标访问容器元素,大致相当于 index[i / chunk_size][i % chunk_size],也保持高效。

list

因为某些标准算法在 list 上会导致问题,list 提供了成员函数作为替代:

merge、remove、remove_if、reverse、sort、unique

forward_list

为什么会需要这么一个阉割版的 list 呢?原因是,在元素大小较小的情况下,forward_list 能节约的内存是非常可观的;在列表不长的情况下,不能反向查找也不是个大问题。提高内存利用率,往往就能提高程序性能,更不用说在内存可能不足时的情况了。

queue

依赖于某个现有的容器,因而被称为容器适配器(container adaptor),queue 缺省用 deque 来实现

stack

stack 缺省也是用 deque 来实现

在这里下面是低地址,向上则地址增大,与内存管理的方向正好相反

1.俩个函数对象less 和 hash

2.less 二元函数,执行对任意类型的比较值,返回布尔类型,调用运算符 ( operator() )而且缺省行为是指定类型对象 < 的比较

3.sort 排序默认使用 less 从大到小使用 greater

4.hash 目的是把一个某种类型的值转化为一个无符号的整数 hash 值 

auto hp = hash();

priority_queue

1.不遵守一般 queue 规则,有序的队列,可以 less(顺排) 和 greater(逆排)

关联性容器

Rb-tree

1.关联性容器有set(集合)、map(映射)、multiset(多重集)、multimap(多重映射)。C++外这些都是无序的,C++里这些都是有序的

2.关联性容器带 mult i的字段是允许重复键,不带是不允许

3.关联系容器没有 insert、emplace等成员函数,但都提供find(找到等价键值元素)、lower_bound(找到第一个不小于查询键值元素)、upper_bound(找到第一个不大于查询键值元素)等查询的函数。

4.在multimap 里精确查找满足某个区间使用 equal_range

无序关联容器

Hash table

1.C++11开始每一个关联容器都有一个无序关联容器他们是

unordred_set、unordered_map、unordered_multiset、unordered_multimap

2.有序的关联容器包括(priority_queue)的插入和删除操作以及关联性容器查找操作复杂度都是O(log(n)) 而无序可以平均达到O(1)(必须使用恰当)

array

1.C数组没有begin 和 end 成员函数(可以使用全局的)

2.C数组没有size成员函数

3.C数组作为参数传递时,不会获取C数组长度和结束位置

课后思考

1.为什么大部分容器都提供了begin、end等方法

答:不同容器内部实现方式不同,实现的遍历方式不同,都能提供begin、end的方法也是为了提供统一的调用方法

2.为什么容器没有继承一个公用的基类

答:不同容器内部实现方式不同(包括存储方式),虽然外部接口都是相同的方法调用,但是接口内部实现机制都是不同的,如果非要使用基类,那基类也只能定义虚函数,还不如现在,在实现的时候就做了统一接口,还少一层构造

const volatile

1.const它是一个类型修饰符,可以给任何对象附加上“只读”属性,保证安全;

它可以修饰引用和指针,“const &”可以引用任何类型,是函数入口参数的最佳类型;

它还可以修饰成员函数,表示函数是“只读”的,const 对象只能调用 const 成员函数。

const int MAX_LEN  = 1024;

auto ptr = (int*)(&MAX_LEN);

*ptr = 2048;

cout << MAX_LEN << endl;      // 输出1024

编译器看到 const 定义,就会采取一些优化手段,比如把所有 const 常量出现的地方都替换成原始值。所以,对于没有 volatile 修饰的 const 常量来说,虽然你用指针改了常量的值,但这个值在运行阶段根本没有用到,因为它在编译阶段就被优化掉了。

2.volatile它表示变量可能会被“不被察觉”地修改,禁止编译器优化,使用的时候都必须“老老实实”地去取值,影响性能,应当少用。

// 需要加上volatile修饰,运行时才能看到效果

const volatile int MAX_LEN  = 1024;

auto ptr = (int*)(&MAX_LEN);

*ptr = 2048;

cout << MAX_LEN << endl;      // 输出2048

3.mutable它用来修饰成员变量,允许 const 成员函数修改,mutable 变量的变化不影响对象的常量性,但要小心不要误用损坏对象。

异常:用还是不用,这是个问题

没有异常时,使用错误码,有大量需要判断错误的代码,零散分布在代码各处

什么是“异常安全”:异常安全是指当异常发生时,既不会发生资源泄漏,系统也不会处于一个不一致的状态。

谷歌为什么不使用异常?使用异常的代价要比在全新的项目中使用异常大一些;当时他们使用的编译器在异常上工作得很糟糕;使用异常和不使用异常比,二进制文件大小会有约百分之十到二十的上升

异常有哪些问题

只要开启了异常,即使不使用异常你编译出的二进制代码通常也会膨胀。

异常比较隐蔽,不容易看出来哪些地方会发生异常和发生什么异常。

一、使用异常

1.异常处理并不意味着需要写显示的try和catch。异常安全的代码,可以没有任何try和catch

2.适当组织好代码,利用好RAII,实现矩阵的代码和使用矩阵的代码都可以更短、更清晰,处理异常一般情况会记日志或者向外界用户报告错误。

二、使用异常的理由

1.vector C++标准容器中提供了at成员函数,能够在下标不存在的时候抛出异常(out_of_range),作为一种额外的帮助调试手段

2.强异常保证,就是一旦异常发生,现场会恢复到调用异常之前的状态。(vector在元素类型没有提供保证不抛异常的移动构造函数的情况下,在移动元素时会使用拷贝构造函数,一旦某操作发生异常,就可以恢复原来的样子)

3.只要使用标准容器就都的处理可能引发的异常bad_alloc

4。可以使用异常,也可以使用assert

迭代器和新for循环

自动类型推断和初始化

自动类型推断auto

使用 auto 的变量(或函数返回值)的类型仍然是编译时就确定了,只不过编译器能自动帮你填充而已。

decltype

decltype 的用途是获得一个表达式的类型,结果可以跟类型一样使用。

它有两个基本用法:decltype(变量名) 可以获得变量的精确类型。

decltype(表达式) (表达式不是变量名,但包括 decltype((变量名)) 的情况)可以获得表达式的引用类型;除非表达式的结果是个纯右值(prvalue),此时结果仍然是值类型。

如果我们有 int a;,那么:

decltype(a) 会获得 int(因为 a 是 int)。

decltype((a)) 会获得 int&(因为 a 是 lvalue)。

decltype(a + a) 会获得 int(因为 a + a 是 prvalue)

decltype(auto)

根据类型推导规则,auto 是值类型,auto& 是左值引用类型,auto&& 是转发引用(可以是左值引用,也可以是右值引用)。使用 auto 不能通用地根据表达式类型来决定返回值的类型。不过,decltype(expr) 既可以是值类型,也可以是引用类型。因此,我们可以这么写:

decltype(expr) a = expr;

这种写法明显不能让人满意,特别是表达式很长的情况(而且,任何代码重复都是潜在的问题)。为此,C++14 引入了 decltype(auto) 语法。对于上面的情况,我们只需要像下面这样写就行了。

decltype(auto) a = expr;

这种代码主要用在通用的转发函数模板中:你可能根本不知道你调用的函数是不是会返回一个引用。这时使用这种语法就会方便很多。

函数返回值类型推断

auto foo(参数) -> 返回值类型声明

{

  // 函数体

}

类模板的模板参数推导

如果你用过 pair 的话,一般都不会使用下面这种形式:pair pr{1, 42};

使用 make_pair 显然更容易一些:auto pr = make_pair(1, 42);

这是因为函数模板有模板参数推导,使得调用者不必手工指定参数类型;但 C++17 之前的类模板却没有这个功能,也因而催生了像 make_pair 这样的工具函数。在进入了 C++17 的世界后,这类函数变得不必要了。现在我们可以直接写:pair pr{1, 42};

int a1[] = {1, 2, 3};

array<int, 3> a2{1, 2, 3}; // 啰嗦

主要缺点就是不能像 C 数组一样自动从初始化列表来推断数组的大小

在 C++17 里也是基本不存在的

array a{1, 2, 3};

// 得到 array<int, 3>

结构化绑定

在讲关联容器的时候我们有过这样一个例子:

multimap::iterator lower, upper;

std::tie(lower, upper) = mmp.equal_range("four");

这个例子里,返回值是个 pair,我们希望用两个变量来接收数值,就不得不声明了两个变量,然后使用 tie 来接收结果。在 C++11/14 里,这里是没法使用 auto 的。

好在 C++17 引入了一个新语法,解决了这个问题。

目前,我们可以把上面的代码简化为:

auto [lower, upper] = mmp.equal_range("four");

列表初始化

现在我们初始化容器也可以和初始化数组一样简单了:vector v{1, 2, 3, 4, 5};

这不是对标准库容器的特殊魔法,而是一个通用的、可以用于各种类的方法。从技术角度,编译器的魔法只是对 {1, 2, 3} 这样的表达式自动生成一个初始化列表,在这个例子里其类型是 initializer_list。程序员只需要声明一个接受 initializer_list 的构造函数即可使用。从效率的角度,至少在动态对象的情况下,容器和数组也并无二致,都是通过拷贝(构造)进行初始化。

统一初始化

类数据成员的默认初始化

C++11 增加了一个语法,允许在声明数据成员时直接给予一个初始化表达式。这样,当且仅当构造函数的初始化列表中不包含该数据成员时,这个数据成员就会自动使用初始化表达式进行初始化。

class Complex {

public:

  Complex() {} // 类数据成员的初始化全部由默认初始化完成

  Complex(float re) : re_(re) {} // im_ 由默认初始化完成

  Complex(float re, float im)

    : re_(re) , im_(im) {} // 完全不使用默认初始化

private:

  float re_{0};

  float im_{0};

};

自定义字面量

二进制字面量

从 C++14 开始,对于二进制也有了直接的字面量:unsigned mask = 0b111000000;

#include <bitset>

cout << bitset<9>(mask) << endl;

数字分隔符

C++14 开始,允许在数字型字面量中任意添加 ' 来使其更可读

unsigned mask = 0b111'000'000;

long r_earth_equatorial = 6'378'137;

double pi = 3.14159'26535'89793;

const unsigned magic = 0x44'42'47'4E;

静态断言

C++11 直接从语言层面提供了静态断言机制。

比如编译时就检查 alignment 是不是二的整数次幂

static_assert((alignment & (alignment - 1)) == 0,

  "Alignment must be power of two");

default 和 delete 成员函数

override 和 final 说明符

override 显式声明了成员函数是一个虚函数且覆盖了基类中的该函数

final 则声明了成员函数是一个虚函数,且该虚函数不可在派生类中被覆盖。如果有一点没有得到满足的话,编译器就会报错。final 还有一个作用是标志某个类或结构不可被派生。同样,这时应将其放在被定义的类或结构名后面。

静态多态

需要使用的模板参数类型,不能完全满足模板的要求,应该怎么办?

1 添加代码,让那个类型支持所需要的操作(对成员函数无效)。

2 对于函数模板,可以直接针对那个类型进行重载。

3 对于类模板和函数模板,可以针对那个类型进行特化。

Constexpr

const 用来表示一个运行时常量

在 C++11 引入、在 C++14 得到大幅改进的 constexpr 关键字,它的字面意思是 constant expression,常量表达式。存在两类 constexpr 对象:

constexpr 变量:编译时完全确定的常数

constexpr 函数:至少对于某一组实参可以在编译期间产生一个编译期常数

lambda表达式

首先你要知道,C++ 没有为 lambda 表达式引入新的关键字,并没有“lambda”这样的词汇,而是用了一个特殊的形式“[]”,术语叫“lambda 引出符”(lambda introducer)。

C++ 也鼓励程序员尽量“匿名”使用 lambda 表达式

vector v = {3, 1, 8, 5, 0};    // 标准容器

cout << *find_if(begin(v), end(v),  // 标准库里的查找算法

            [](int x)                // 匿名lambda表达式,不需要auto赋值

            {

                return x >= 5;        // 用做算法的谓词判断条件 

            }                        // lambda表达式结束

        )

     << endl;                        // 语句执行完,lambda表达式就不存在了

“[=]”表示按值捕获所有外部变量,表达式内部是值的拷贝,并且不能修改;

“[&]”是按引用捕获所有外部变量,内部以引用的方式使用,可以修改;

[this] 标明按引用捕获外围对象

[*this] 标明按值捕获外围对象

变量名 = 表达式 标明按值捕获表达式的结果,如[*this, count = get_count()]

&变量名 = 表达式 标明按引用捕获表达式的结果[*this, &count = get_count()]

也可以在“[]”里明确写出外部变量名,指定按值或者按引用捕获,C++ 在这里给予了非常大的灵活性。

int x = 33;              // 一个外部变量

auto f1 = [=]()          // lambda表达式,用“=”按值捕获

{

    //x += 10;            // x只读,不允许修改

};

auto f2 = [&]()        // lambda表达式,用“&”按引用捕获

{

    x += 10;            // x是引用,可以修改

};

auto f3 = [=, &x]()      // lambda表达式,用“&”按引用捕获x,其他的按值捕获

{

    x += 20;              // x是引用,可以修改

};

泛型的lambda

auto f = [](const auto& x)        // 参数使用auto声明,泛型化

{

    return x + x;

};

cout << f(3) << endl;            // 参数类型是int

cout << f(0.618) << endl;        // 参数类型是double

string str = "matrix";

cout << f(str) << endl;          // 参数类型是string

总结

lambda 表达式是一个闭包,能够像函数一样被调用,像变量一样被传递;可以使用 auto 自动推导类型存储 lambda 表达式,但 C++ 鼓励尽量就地匿名使用,缩小作用域;lambda 表达式使用“[=]”的方式按值捕获,使用“[&]”的方式按引用捕获,空的“[]”则是无捕获(也就相当于普通函数);捕获引用时必须要注意外部变量的生命周期,防止变量失效;C++14 里可以使用泛型的 lambda 表达式,相当于简化的模板函数。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容