22.智能指针(下)

2024-06-23 15:28
文章标签 22 指针 智能

本文主要是介绍22.智能指针(下),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

标题

  • 五、引用计数智能指针
    • 5.1 共享引用计数智能指针共享数据
    • 5.2 使用Box定义三个共享链表
    • 5.3 使用Rc代替Box
    • 5.4 引用计数增加实验
  • 六、RefCell和内部可变性模式
    • 6.1 通过RefCell在运行时检查借用规则
    • 6.2 内部可变性:不可变值的可变借用
      • 1)内部可变性的用例:mock对象
      • 2)创建测试场景
    • 6.3 RefCell在运行时记录借用
    • 6.4 结合Rc和RefCell拥有多个可变数据所有者
  • 七、引用循环会导致内存泄漏
    • 7.1 概念
    • 7.2 制造引用循环
    • 7.3 解决方案
      • 1)避免引用循环:将Rc变成Weak
      • 2)Strong VS Weak
      • 3)创建带有父子结点的树形数据结构

五、引用计数智能指针

  • 引用计数Rc<T>通过引入的数量记录着某个变量是否仍在被使用;
  • 如果引用计数为0,则代表没有任何有效引用,此时可以被清理;
  • Rc<T>用于堆上分配的内存被程序的多个部分读取,且无法确定哪一部分最后结束使用;
  • Rc<T>只能用于单线程;

5.1 共享引用计数智能指针共享数据

  • 创建两个共享第三个列表所有权的示例
    在这里插入图片描述
  • 列表a从5开始;
  • 列表bc分别从3和4开始,然后共享a的部分;

5.2 使用Box定义三个共享链表

enum List {Cons(i32, Box<List>),Nil,
}use crate::List::{Cons, Nil};fn main() {let a = Cons(5,Box::new(Cons(10,Box::new(Nil))));let b = Cons(3, Box::new(a));let c = Cons(4, Box::new(a));
}
  • 代码无法编译通过
  • 当创建列表b时,a的所有权被移进了b;
  • 当创建列表c时,a的所有权已经被移动,所以c无法创建成功;

5.3 使用Rc代替Box

enum List {Cons(i32, Rc<List>),Nil,
}use std::rc::Rc;
use crate::List::{Cons, Nil};fn main() {let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));let b = Cons(3, Rc::clone(&a));let c = Cons(4, Rc::clone(&a));
}
  • 在List中,使用Rc<T>代替Box<T>
  • 创建b和c时,克隆a所包含的Rc<List>,这使引用计数分别加1并允许a和b以及a和c之间共享Rc<List>中数据的所有权;
  • 也可以将Rc::clone(&a)换成a.clone()Rc::clone只会增加引用计数而不会执行深拷贝;

5.4 引用计数增加实验

  • 打印引用计数值,以便直接看着引用计数的变化;
fn main() {let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));println!("count after creating a = {}", Rc::strong_count(&a));let b = Cons(3, Rc::clone(&a));println!("count after creating b = {}", Rc::strong_count(&a));{let c = Cons(4, Rc::clone(&a));println!("count after creating c = {}", Rc::strong_count(&a));}println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
  • 运行结果如下
    在这里插入图片描述
  • 初始计数为1,每调用一个克隆都会使计数加1;
  • 离开作用域后会自动减少计数;

通过不可变引用,Rc<T>允许在程序的多个部分之间只读的共享数据;

六、RefCell和内部可变性模式

  • 内部可变性(Interior mutability)是Rust的一个设计模式,可让使用者在有不可变引用时也可以改变数据(这通过是规则所禁止的);
  • 为了改变数据,该模式在数据结构中使用unsafe代码来模糊Rust通常的可变性和借用规则;
  • 当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的unsafe代码将被封装进安全的API中,而外部类型仍然是不可变的;

6.1 通过RefCell在运行时检查借用规则

  • RefCell<T>代表其数据的唯一所有权;
  • RefCell<T>的借用规则的不可变性作用于运行时
  • RefCell<T>只能用于单线程场景;

6.2 内部可变性:不可变值的可变借用

  • 当有不可变值时,不能可变地借用它;
  • 如下的编译报错
fn main() {let x = 5;let y = &mut x;
}
  • 可以在特定情况下,令一个值在其方法内部能够修改自身,而在其他代码中仍然视为不可变;
  • RefCell<T>是一个获得内部可变性的方法;

1)内部可变性的用例:mock对象

  • 测试替身(test double) 代表一个测试中替代某个类型的类型;
  • mock对象是特定类型的测试替身,它们记录测试过程中发生了什么以便可以说明操作是否正确;
  • Rust没有内建mock对象功能,可以自己创建一个与mock对象有着相同功能的结构体;

2)创建测试场景

  • 编写一个记录某个值与最大值的差距的库;
  • 根据当前值与最大值的差距来发送消息;
  • 例如:可以记录用户所允许的API调用数量限额;

src/lib.rs

pub trait Messenger {fn send(&self, msg: &str);
}pub struct LimitTracker<'a, T: Messenger> {messenger: &'a T,value: usize,max: usize,
}impl<'a, T> LimitTracker<'a, T>where T: Messenger {pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {LimitTracker {messenger,value: 0,max,}}pub fn set_value(&mut self, value: usize) {self.value = value;let percentage_of_max = self.value as f64 / self.max as f64;if percentage_of_max >= 1.0 {self.messenger.send("Error: You are over your quota!");} else if percentage_of_max >= 0.9 {self.messenger.send("Urgent warning: You've used up over 90% of your quota!");} else if percentage_of_max >= 0.75 {self.messenger.send("Warning: You've used up over 75% of your quota!");}}
}
  • Message trait拥有send方法,这是定义的mock对象所需要拥有的接口;
  • set_value方法可以改变传递的value参数的值;
#[cfg(test)]
mod tests {use super::*;struct MockMessenger {sent_messages: Vec<String>,}impl MockMessenger {fn new() -> MockMessenger {MockMessenger { sent_messages: vec![] }}}impl Messenger for MockMessenger {fn send(&self, message: &str) {self.sent_messages.push(String::from(message));}}#[test]fn it_sends_an_over_75_percent_warning_message() {let mock_messenger = MockMessenger::new();let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);limit_tracker.set_value(80);for item in &mock_messenger.sent_messages{println!("{}", item);}assert_eq!(mock_messenger.sent_messages.len(), 1);}
}
  • 测试代码定义了一个MockMessenger结构体,其要发送的字段为一个String值的Vec;
  • MockMessenger结构体实现Messenger trait,这样就可以为LimitTracker提供一个MockMessenger;
  • 测试用例中
    1. 创建一个MockMessager实例,send_message存储着空vector;
    2. 创建LimitTracker,传入MockMessenger实例和最大值100;
    3. 调用LimitTracker的set_value方法,并传入80,这将运行self.messenger.send("Warning: You've used up over 75% of your quota!");代码;
    4. 最后断言sent_messages的长度为1,打印其值;

编译报错
在这里插入图片描述

  • 由于send方法获取了self的不可变引用,因此不能修改MockMessenger来记录消息;
  • 也不能按照信息使用&mut self替代,否则send的参数与Messenger trait的参数不符合;
  • 将这两项也修改;
pub trait Messenger {fn send(&mut self, msg: &str);
}impl Messenger for MockMessenger {fn send(&mut self, message: &str) {self.sent_messages.push(String::from(message));}
}
  • 还是如下报错;

在这里插入图片描述

  • 这就使得内部可变性有了用武之地!
  • 通过RefCell来储存sent_messages就可以解决这个问题;

修改的部分如下,采用注释进行对比

#[cfg(test)]
mod tests {use super::*;use std::cell::RefCell;struct MockMessenger {// sent_messages: Vec<String>,sent_messages: RefCell<Vec<String>>,}impl MockMessenger {fn new() -> MockMessenger {// MockMessenger { sent_messages: vec![] }MockMessenger { sent_messages: RefCell::new(vec![])}}}impl Messenger for MockMessenger {fn send(&self, message: &str) {// self.sent_messages.push(String::from(message));self.sent_messages.borrow_mut().push(String::from(message));}}#[test]fn it_sends_an_over_75_percent_warning_message() {let mock_messenger = MockMessenger::new();let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);limit_tracker.set_value(80);for item in mock_messenger.sent_messages.borrow().iter(){println!("{}", item);}assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);}
}
  • sent_messages字段的类型由Vec<String>改为RefCell<Vec<String>>
  • 在new函数中新建了一个RefCell<Vec<String>>实例替代空vector;
  • 在send方法中,使用RefCell的borrow_mut方法获取RefCell中值的可变引用(是一个vector);
  • 最后的打印以及断言中,使用RefCell的borrow以获取vector的不可变引用;
  • 最后输出了vector中的字符串并通过了测试;
    在这里插入图片描述

6.3 RefCell在运行时记录借用

  • 一般情况下使用&&mut分别创建不可变和可变引用;
  • 对于RefCell<T>,分别使用borrowborrow_mut方法创建不可变和可变引用;
  • borrow方法返回Ref<T>类型的智能指针,borrow_mut方法返回RefMut<T>类型的智能指针;
  • RefCell<T>记录当前有多少个活动的Ref<T>RefMut<T>智能指针;
  • 每次调用borrow,RefCell<T>将活动的不可变借用计数加1,当Ref<T>值离开作用域时,不可变借用计数减1;
  • RefCell<T>的实现会在运行时出现panic;
impl Messenger for MockMessenger {fn send(&self, message: &str) {let mut one_borrow = self.sent_messages.borrow_mut();let mut two_borrow = self.sent_messages.borrow_mut();one_borrow.push(String::from(message));two_borrow.push(String::from(message));}
}
  • 代码在send函数里相同的作用域创建两个可变借用;
  • 编译能正常通过,运行测试时失败;

在这里插入图片描述

  • 错误提示already borrowed: BorrowMutError,就是RefCell<T>在运行时处理违反借用规则时的报错;

6.4 结合Rc和RefCell拥有多个可变数据所有者

  • RefCell<T>的一个常见用法是与Rc<T>结合;
  • Rc<T>允许对相同数据有多个所有者,只能提供数据的不可变访问;
  • 一个储存了RefCell<T>Rc<T>,可以得到有多个所有者且可以修改的值;
#[derive(Debug)]
enum List {Cons(Rc<RefCell<i32>>, Rc<List>),Nil,
}use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;fn main() {let value = Rc::new(RefCell::new(5));let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));*value.borrow_mut() += 10;println!("a after = {:?}", a);println!("b after = {:?}", b);println!("c after = {:?}", c);
}
  • 创建Rc<RefCell<i32>>实例并存储在变量value中;
  • 在a中用包含value的Cons成员创建了一个List;
  • 克隆value后a和value 以便a和value都能拥有其内部值 5 的所有权;
  • 将列表a封装进Rc<T>,当创建列表b和c时,他们都可以引用a;
  • 对value调用borrow_mut解引用Rc<T>以获取内部的RefCell<T>值;
  • borrow_mut方法返回RefMut<T>智能指针,可以对其使用解引用运算符并修改其内部值;
    在这里插入图片描述
  • 输出a,b,c时,可以看到他们都拥有修改后的值15;
  • 通过使用RefCell<T>可以拥有一个表面上不可变的List;
  • 可以使用RefCell<T>中提供内部可变性的方法来在需要时修改数据;
  • RefCell<T>的运行时借用规则检查也确实保护我们免于出现数据竞争;

标准库中也有其他提供内部可变性的类型,比如 Cell,它类似 RefCell 但有一点除外:它并非提供内部值的引用,而是把值拷贝进和拷贝出 Cell。还有 Mutex,其提供线程间安全的内部可变性。

七、引用循环会导致内存泄漏

7.1 概念

  • Rust的内存安全可以保证很难发生内存泄露,但并非完成不可能!
  • 使用Rc<T>RefCell<T>创造出循环引用,引用数量不会减少为0,就会发生内存泄漏;

7.2 制造引用循环

use std::rc::Rc;
use std::cell::RefCell;
use crate::List::{Cons, Nil};#[derive(Debug)]
enum List {Cons(i32, RefCell<Rc<List>>),Nil,
}impl List {fn tail(&self) -> Option<&RefCell<Rc<List>>> {match self {Cons(_, item) => Some(item),Nil => None,}}
}fn main() {let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));println!("a initial rc count = {}", Rc::strong_count(&a));println!("a next item = {:?}", a.tail());let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));println!("a rc count after b creation = {}", Rc::strong_count(&a));println!("b initial rc count = {}", Rc::strong_count(&b));println!("b next item = {:?}", b.tail());if let Some(link) = a.tail() {*link.borrow_mut() = Rc::clone(&b);}println!("b rc count after changing a = {}", Rc::strong_count(&b));println!("a rc count after changing a = {}", Rc::strong_count(&a));// Uncomment the next line to see that we have a cycle;// it will overflow the stack// println!("a next item = {:?}", a.tail());
}
  • Cons成员的第二个元素是RefCell<Rc<List>>,因此能够修改Cons成员所指向的List;
  • tail方法方便在有Cons成员的时候访问第二项;
  • 在main函数中创建了一个链表以及一个指向a中链表的b链表;
  • if let语句中使用a.tail()取出a的第二个元素,将它指向b,形成一个引用循环:两个List互相指向彼此;

在这里插入图片描述

  • 开始创建a时的引用数量为1,a的下一个元素是Nil;
  • b创建之后a的引用个数变为2,b的引用数量为1,b的下一个元素就是a;
  • 修改a的第二个元素的指向后,a和b都有两个引用。

在这里插入图片描述

  • 将main函数的最后一个println!的注释取消,就会发现在循环打印;

7.3 解决方案

  • 开发者采用自动化测试、代码评审等;
  • 重新组织数据结构,使得一部分引用拥有所有权而另一部分没有;

1)避免引用循环:将Rc变成Weak

  • Rc::cloneRc<T>实例的strong_count加1,Rc<T>的实例只有在strong_count为0时才会被清理;
  • Rc<T>实例通过调用Rc::downgrade方法可以创建值的Weak Reference(弱引用);
  • 调用Rc::downgrade时会得到Weak<T>类型的智能指针;
  • 调用Rc::downgrade时会将weak_count加1;
  • weak_count无需计数为0就能使Rc<T>实例被清理;

2)Strong VS Weak

  • 强引用(Strong Reference)是关于如何共享Rc<T>实例的所有权;
  • 弱引用(Weak Reference)并不表示所有权关系;
  • 当强引用数量为0时,弱引用会自动断开,因此弱引用不会创建循环引用;
  • Weak<T>实例上调用upgrade方法,返回Option<Rc<T>>,以此保证指向弱引用的值仍然存在;

3)创建带有父子结点的树形数据结构

加入子节点

use std::rc::Rc;
use std::cell::RefCell;#[derive(Debug)]
struct Node {value: i32,children: RefCell<Vec<Rc<Node>>>,
}fn main() {let leaf = Rc::new(Node {value: 3,children: RefCell::new(vec![]),});let branch = Rc::new(Node {value: 5,children: RefCell::new(vec![Rc::clone(&leaf)]),});
}
  • 在Node结构体中:
    • Rc<Node>保证了所有的子结点共享所有权;
    • RefCell保证了能修改其他节点的子结点;
  • 在main函数中:
    • 创建了值为3且没有子节点的Node实例leaf
    • 创建了值为5且以leaf作为子节点的实例branch
    • 这就可以通过branch.children从branch中获得leaf,反过来不行(可以通过添加parent解决);

加入父结点

  • 要使能够从leaf中获得branch,需要再加一个parent;
  • parent的类型如果是Rc<T>则会产生循环引用;
  • 换个思路:
    • 父节点应该拥有子节点:父节点被销毁了,子节点也应该被销毁;
    • 子节点不应该拥有父节点:子节点被销毁,父节点应该依然存在;
    • 因此应该使用弱引用类型Weak<T>,具体的是RefCell<Weak<Node>>
use std::rc::{Rc, Weak};
use std::cell::RefCell;#[derive(Debug)]
struct Node {value: i32,parent: RefCell<Weak<Node>>,children: RefCell<Vec<Rc<Node>>>,
}fn main() {let leaf = Rc::new(Node {value: 3,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![]),});println!("leaf parent = {:#?}", leaf.parent.borrow().upgrade());let branch = Rc::new(Node {value: 5,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![Rc::clone(&leaf)]),});*leaf.parent.borrow_mut() = Rc::downgrade(&branch);println!("leaf parent = {:#?}", leaf.parent.borrow().upgrade());
}
  • 对应Node结构体
    • 添加引向父节点的结构parent: RefCell::new(Weak::new()),
  • 对于main函数
    • 创建leaf结点:值为3,由于没有父结点,因此创建了空的weak引用实例;
    • 创建branch结点:值为5,父结点也为空,leaf仍作为子结点;
    • 第一个打印语句通过父结点的borrow()方法获取不可变引用,然后用upgrade()方法打印出来(为空);
    • 通过leaf.parent.borrow_mut()获取leaf的父结点的可变引用,然后通过解引用将其指向branch
    • 接着打印leaf的父结点,没有无限的输出也表示没有造成循环引用;

在这里插入图片描述

可视化strong_count和weak_count的改变

  • 创建新的内部作用域并放入branch的创建;
  • 观察Rc<Node>实例的strong_countweak_count值变化;
fn main() {let leaf = Rc::new(Node {value: 3,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![]),});println!("leaf strong = {}, weak = {}",Rc::strong_count(&leaf),Rc::weak_count(&leaf),);{let branch = Rc::new(Node {value: 5,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![Rc::clone(&leaf)]),});*leaf.parent.borrow_mut() = Rc::downgrade(&branch);println!("branch strong = {}, weak = {}",Rc::strong_count(&branch),Rc::weak_count(&branch),);println!("leaf strong = {}, weak = {}",Rc::strong_count(&leaf),Rc::weak_count(&leaf),);}println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());println!("leaf strong = {}, weak = {}",Rc::strong_count(&leaf),Rc::weak_count(&leaf),);
}

运行结果
在这里插入图片描述

  • 创建leaf之后,其Rc<Node>的强引用为1,没有弱引用;
  • 进入内部作用域
    • branch的强引用计数为1,弱引用计数也为1(leaf.parnet引向了branch);
    • leaf的弱引用计数为2(一个是本身的,另一个是branch的branch.children储存了leaf的Rc<Node>的拷贝),弱引用计数仍然为0;
  • 离开内部作用域后
    • branch的作用域也就结束了,leaf的Rc<Node>强引用减少为0,因此相应的branch被丢弃;
    • 来自leaf.parent的弱引用计数为1,这与Node是否被丢弃无关,因此没有产生任何内存泄漏!
  • 离开内部作用域后访问leaf的父节点,再次得到None。
  • 到程序结尾,由于leaf又是Rc<Node>唯一的引用了,因此最后打印leafRc<Node>的强引用计数为1,弱引用计数为0;

管理计数和值的逻辑都内建于Rc<T>Weak<T>以及它们的Drop trait实现中。通过在Node定义中指定从子节点到父节点的关系为一个Weak<T>引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。

这篇关于22.智能指针(下)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1087539

相关文章

22.手绘Spring DI运行时序图

1.依赖注入发生的时间 当Spring loC容器完成了 Bean定义资源的定位、载入和解析注册以后,loC容器中已经管理类Bean 定义的相关数据,但是此时loC容器还没有对所管理的Bean进行依赖注入,依赖注入在以下两种情况 发生: 、用户第一次调用getBean()方法时,loC容器触发依赖注入。 、当用户在配置文件中将<bean>元素配置了 lazy-init二false属性,即让

智能客服到个人助理,国内AI大模型如何改变我们的生活?

引言 随着人工智能(AI)技术的高速发展,AI大模型越来越多地出现在我们的日常生活和工作中。国内的AI大模型在过去几年里取得了显著的进展,不少独创的技术点和实际应用令人瞩目。 那么,国内的AI大模型有哪些独创的技术点?它们在实际应用中又有哪些出色表现呢?此外,普通人又该如何利用这些大模型提升工作和生活的质量和效率呢?本文将为你一一解析。 一、国内AI大模型的独创技术点 多模态学习 多

C语言入门系列:探秘二级指针与多级指针的奇妙世界

文章目录 一,指针的回忆杀1,指针的概念2,指针的声明和赋值3,指针的使用3.1 直接给指针变量赋值3.2 通过*运算符读写指针指向的内存3.2.1 读3.2.2 写 二,二级指针详解1,定义2,示例说明3,二级指针与一级指针、普通变量的关系3.1,与一级指针的关系3.2,与普通变量的关系,示例说明 4,二级指针的常见用途5,二级指针扩展到多级指针 小结 C语言的学习之旅中,二级

基于 Java 实现的智能客服聊天工具模拟场景

服务端代码 import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.PrintWriter;import java.net.ServerSocket;import java.net.Socket;public class Serv

利用结构体作为函数参数时结构体指针的定义

在利用结构体作为函数的参数进行传递时,容易犯的一个错误是将一个野指针传给函数导致错误。 #include <stdio.h>#include <math.h>#include <malloc.h>#define MAXSIZE 10typedef struct {int r[MAXSIZE]; //用于存储要排序的数组,r[0]作为哨兵或者临时变量int length;

江西电信联合实在智能举办RPA数字员工培训班,培养“人工智能+”电信人才

近日,江西电信与实在智能合作的2024年数字员工开发应用培训班圆满闭幕。包括省公司及11个分公司的核心业务部门,超过40名学员积极报名参与此次培训,江西电信企业信息化部门总监徐建军出席活动并致辞,风控支撑室主任黄剑主持此次培训活动。 在培训会开幕仪式上,徐建军强调,科创是电信企业发展的核心动力,学习RPA技术是实现数字化转型的关键,他阐述了RPA在提高效率、降低成本和优化资源方面的价值,并鼓励学

isa指针的理解

D3实例isa指向D3类对象。D3类的话isa指向D3元类对象。D3元类保存类中的方法调度列表,包括类方法和对象方法

深度神经网络:解锁智能的密钥

深度神经网络:解锁智能的密钥 在人工智能的浩瀚星空中,深度神经网络(Deep Neural Networks, DNNs)无疑是最耀眼的那颗星。它以其强大的学习能力、高度的适应性和广泛的应用场景,成为了我们解锁智能世界的一把密钥。本文将带你走进深度神经网络的神秘世界,探讨其原理、应用以及实用操作技巧。 一、深度神经网络概述 深度神经网络,顾名思义,是一种具有多个隐藏层的神经网络。与传统的神经

C/C++语言基础知识 之 引用和指针

关于引用 引入是C++引入的新语言特性。 1 int &rn = a;-----------------------------------------------2 int* p = &a;3 int* &pa = p;4 (*pa)++;5 pa = &b;6 (*pa)++; L1:声明rn为变量a的一个引用,不需要为rn另外开辟内存单元。rn和a占

秋招突击——6/22——复习{区间DP——加分二叉树,背包问题——买书}——新作{移除元素、实现strStr()}

文章目录 引言复习区间DP——加分二叉树个人实现 背包问题——买书个人实现参考实现 新作移除元素个人实现参考思路 找出字符串中第一个匹配项的下标个人实现参考实现 总结 引言 今天做了一个噩梦,然后流了一身汗,然后没起来,九点多才起床背书。十点钟才开始把昨天那道题题目过一遍,然后十一点才开始复习题目,为了不耽误下午的时间,所以这里的就单纯做已经做过的题目,主打一个有量,不在学