一.编译链接运行
1. 虚拟地址空间的内存布局
Linux内存管理 | 二、虚拟地址空间布局 - 知乎 (zhihu.com)
2. 说下 C++的内存管理
代码段、数据段、堆、栈、bss段、文件映射和匿名映射区
3. 全局变量和局部变量的区别(生命周期、作用域、默认值、内存区域、链接属性)
1). 作用域不同:局部变量的作用域限定在其定义的块或函数内,而全局变量的作用域是整个程序。
2). 生存周期不同:局部变量的生命周期随着其所在的作用域结束而结束,全局变量的生命周期从程序开始到程序结束。
3). 访问范围不同:局部变量只能在其所在的作用域内访问,无法被其他作用域中的代码访问;而全局变量可以在程序的任何地方都能访问。
4). 内存占用不同:全局变量在内存中的占用空间较大,而局部变量的空间较小。
5).链接属性不同:非静态全局变量可以对外链接,局部变量不能对外链接
4. 堆和栈的区别
栈由编译器管理,堆由程序员管理。
堆在系统分配的内存比栈多得多(数量级G和m)。
堆区内存碎片化,栈不会。
堆区内存向地址增大方向增长,栈向地址较少方向
堆一般动态分配,栈动静态分配
(a)管理方式:栈的管理是编译器来进行分配和管理,堆的管理一般是程序员通过new和delete来对内存进行分配和释放。
(b)空间大小:堆在系统中一般可以分配几个G大小的内存,而栈一般分配几M大小的内存。
(c)碎片问题:堆碎片化,栈没有碎片化问题。堆在不断的执行new和delete操作中,内存逐渐被碎片化,使得程序的执行效率变低;栈采用后进先出的策略,所以不会出现碎片化的问题。
(d)增长方向:(堆增大,栈减少)堆的增长方向是向着地址增大的方向进行的,而栈采用的是后进先出的策略,所以元素的增长方向是朝着地址减少的方向进行的。
(e)分配方式:(堆动态,栈动静态)堆一般是动态分配的;而栈既有动态分配的方式也有静态分配的方式,静态分配使用alloc函数执行,也是由编译器分配,动态由编译器分配。
(f)分配效率:一般情况下,栈的分配效率高于堆,因为栈分配的时候由编译器和操作系统底层将临时变量和相关地址放在特定的寄存器中,读写效率高,而堆有上层复杂的数据结构会造成效率低的问题。
原文链接:https://blog.csdn.net/qq_34796146/article/details/104139121
5. 编译时,代码优化都做了哪些事
常量传播,常量折叠,复写传播,公共子表式消除,无用代码消除,数组范围检查消除,方法内联,逃逸分析
编译器常用的8种优化方法 - 知乎 (zhihu.com)
6. 链接过程分哪些,说下动态链接
静态链接,动态链接
静态链接——依赖静态库(libxxxx.a)
链接时,把所有需要的函数二进制代码都包含在可执行程序中,完成所有符号引用
动态链接——依赖动态库(libxxxx.so)
不直接拷贝相应的代码,而是通过一系列符号或者参数在程序运行时或加载时把这些信息传递给操作系统,由操作系统负责将需要的依赖库加载到内存中,然后程序在运行指定代码时,去执行内存中已经加载的依赖库的代码
静态库和动态库区别
1.方式不同:静态链接把静态库编译进目标文件;动态链接运行时加载已经在内存中的代码
2.占用空间:静态库存在多个副本(内存和磁盘),比较浪费系统资源;动态库只有一个副本
3.使用方式:静态库所在程序是直接运行;动态库所在程序是动态链接(程序环境需要指定动态库查找环境)
4.库文件发生改变时:静态库当接口(函数名、参数等)发生改变时,需要重新编译,依赖其的程序需要重新链接;动态库发生改变只需要重新编译动态库就行
7. 编译链接过程,在什么平台上
gcc。
8. 讲下函数调用堆栈过程(有点小难,还没学会)
二.C/C++区别
1. 讲下函数重载,C/C++函数生成符号的规则
1)extern最简单的用法就是在一个文件中引用另一个文件的变量或函数(例如:在 A.cpp 文件中引用 B.cpp 中的全局变量 )
2)作为一种面向对象的语言,C++支持函数重载,而过程式语言 C 则不支持。函数被 C++编译后在符号库中的名字与 C 语言的不同。C++的编译中,会把函数参数类型也加到编译后的代码中而不仅仅是函数名,但C语言只有函数名。
假设某个函数的原型为:
int foo(int a, int b);
该函数被 C 编译器编译后在符号库中的名字为_foo,而 C++编译器则会产生像_foo_int_int 之类的名字 ,这样的名字包含了函数名、函数参数数量及类型信息。C++就是靠这种机制来实现函数重载的。 例如,在 C++中,函数void foo( int x, int y )与 void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。
比如说你用 C 开发了一个 DLL 库,为了能够让 C ++语言也能够调用你的 DLL 输出(Export)的函数,你需要用 extern "C"来强制编译器不要修改你的函数名。
2. 内联函数的使用场景
小型函数:对于短小的函数,如取值、设置值或简单的数学运算,内联函数非常合适。
频繁调用的函数:如果一个函数在代码中被频繁调用,将其声明为内联函数可以显著提高性能。 头文件中的函数:通常将内联函数的定义放在头文件中,以便在多个源文件中重复使用。
3. static 关键字 C、C++方面的用法
在C语言中,主要定义全局静态变量,定义局部静态变量,定义静态函数。
1.定义全局静态变量:在全局变量前面加上关键字static
特点:在全局数据区内分配内存;如果没有初始化,默认值为0;
该变量从定义到本文本结束可见;
2.定义局部静态变量:在局部变量前面加上关键字 static
特点:在全局数据区内分配内存;如果不显示初始化,会被隐式初始化为0;
它始终驻留在全局数据区,直到程序结束;
其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束
3.定义静态函数:函数前面加上static
特点:静态函数只能在本源文件使用;在文件作用域中声明的inline 函数默认为 static
在C++语言中新增两种作用:定义静态数据成员,定义静态成员函数
静态成员数据特点:在程序的全局数据区分配内存;
静态数据成员定义时要分配空间,所以不能在类声明里定义;
静态成员函数特点:静态成员函数与类相联系,不与类的对象相联系。
静态成员函数不能访问非静态数据成员。原因很简单,非静态数据成员属于特 定的类实例,主要用于对静态数据成员的操作。
静态成员函数和静态数据成员都没有 this 指针。
4. 指针、引用区别,指针传值和引用传值的区别
指针声明的时候可以不初始化,引用声明的时候必须初始化。
指针有空指针,但引用不存在空引用。
指针还可以指向别的(除了const情况),引用不能再引用别的。
指针大小四个字节,引用的大小是所引用的变量的大小。
传指针:值传递的过程中,被调函数的形式参数作为被调函数的局部变量处理,是实参的副本。被调函数对形参的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值(这里是在说实参指针本身的地址不会变)
传引用:被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。被调函数对形参做的任何操作都影响了主调函数中的实参变量。
5. C++中的 const 是为了干什么
定义常量,作为函数返回值,提高函数的安全性和可靠性。
常成员函数,当前函数中不能修改本类数据成员的值。
可以定义函数参数,定义指针。
6. const 和 define 的异同
define在预处理阶段,是预处理指令;而const是关键字,在程序编译、运行时起作用。
define只是简单的字符串替换,没有类型检查;而const有对应的数据类型
7. 指针常量和常量指针区别,如何定义写一下
指针常量就是一个常量,由指针修饰,int* const p,p指向的地址不能的改变但是*p的值可以改变
常量指针就是指向常量的指针,const int *p
8. new 和 malloc 区别
new和delete是C++关键字,需要编译器支持;malloc和free是库函数,需要头文件支持
new会自动计算对象大小来申请空间;而malloc要显式指出所需大小
new和delete分配内存和调用类的构造函数(析构函数);malloc和free只是分配和释放内存
new内存分配失败时,会抛出bac_alloc异常(要用try-catch);malloc分配内存失败时返回NULL
9. new 失败会怎样,不想抛出异常该怎么办
会抛出异常。
返回一个空指针,这种情况下,程序需要检查指针是否为空,以确定内存是否分配成功。
auto p = new (std::nothrow) type;
if ( nullptr == p )
10. delete 释放单个对象和数组的区别
delete用于释放单个对象的内存,而delete[]用于释放数组对象的内存。 使用delete释放数组对象的内存会导致未定义的行为,因为delete只会释放数组的第一个元素的内存,而不会释放整个数组的内存。 因此,如果要释放数组对象的内存,必须使用delete[]。
11. 野指针怎么来的,怎么预防野指针
定义时没初始化,释放内存后没置为nullptr
(1)没初始化的时候,即没指向时把它置为NULL
(2)指针分配完内存后,不使用时再用free()函数清空内存空间(清空原来的缓冲区),并再将指针指为NULL。
12.C、C++区别
面向过程:把问题分为几个步骤,再一个一个解决
面向对象:把问题分成不同的对象,分析对象的行为
优缺点:
面向过程:性能比面向对象好,比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
面向对象:调用时需要实例化,开销比较大。易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
三.C++基础
1. 说说 OOP 思想,举个例子说明面向过程和面向对象的区别
oop指的是面向对象编程,oop思想就是封装继承多态。C++——封装、继承、多态_c++封装,继承,多态的概念-CSDN博客
面向过程:做蛋炒饭的过程
面向对象:米饭和盖浇
封装:该隐藏的数据私有化,该公开的接口公有化,目的就是为了分工合作,更加方便安全,防止不必要的扩展。
继承:实现代码和数据的复用和扩展
多态:
重载多态——函数重载 运算符重载
强制多态——static_cast、const_cast、reinterpret_cast和dynamic_cast
包含多态--virtual
参数多态--模板。方法的参数可以接受不同类型的对象,并根据实际传递的对象类型来执行不同的操作。
2. struct 和 class 区别
C的struct和C++struct的区别:C不能写函数,不能为空,C++可以
C结构体名就是结构体名,C++作为类型名,可以直接定义变量
class默认的是private,strcut默认的是public。
3. C++中的空类,你知道吗?对空类求 sizeof,结果怎样?哪些情况会对它有影响,
static 变量对其有影响吗
sizeof(空类)=1
类的大小
1.类里面数据成员大小之和
2.普通函数不占空间大小
3.static数据成员不在类中分配空间
4.virtual函数占一个指针大小
4. private 和 protect 的区别
private表示私有的,只能在当前类中访问,其他类无法访问。 protected表示受保护的,可以在当前类和其子类中访问,但是在其他类中无法访问。 因此,private主要用于隐藏类的实现细节,保护数据的安全性;而protected则用于继承和多态等特性的实现,提高代码的可扩展性和复用性
5. 什么是友元关系,为什么要有友元
在类的定义中用friend声明了一个外部函数或其他类的成员函数(public和private均可)后,这个外部函数称为类的友元函数。 友元函数声明的基本格式为: friend 函数原型; 友元函数中可访问类的private成员。(方便编程,将要访问私有或保护的函数或类作为要访问的那个类的朋友)
注意:友元不区分访问权限
1.一个普通函数作为一个类的友元可以在此函数中通过对象访问当前类的私有数据成员
2.一个类的成员函数作为另一个类的友元,可以在此函数中通过对象访问另一个类的私有数据成员
3.一个类作为另一个类的友元类,则此类中的所有成员函数都可以通过另一个类的对象
访问其私有数据成员
友元的特性:
1.友元是单向的
2.友元不具有传递性
3.友元不能继承
友元破坏了信息隐藏机制,但是也让类的借口扩展更加灵活
6. 虚继承——防止菱形继承出现的两种数据的情况
7. 函数指针,在哪用过
函数指针可以作为参数传递,也可以作为函数返回值
void compareNumberFunction(int *numberArray, int count, int compareNumber, BOOL (*p)(int, int))
{for (int i = 0; i < count; i++) {if (p(*(numberArray + i), compareNumber)) //通过函数指针调用比较函数{printf("%d\n", *(numberArray + i));}}
}
8. 如果析构函数里面抛出异常会怎么样?
会内存泄漏,所以别写try catch
9. 如果构造函数里面抛出异常呢?会造成内存泄漏吗
会的。不要抛异常,用别的办法判断new成功没
10. 深拷贝 浅拷贝,写时拷贝的特点
系统默认的拷贝是浅拷贝,浅拷贝深拷贝最根本的区别就是,浅拷贝拷贝前后都在同一片内存,深拷贝产生了新的内存。
浅拷贝:只复制对象的指针,两个对象同时指向一个地址空间,任意一个对象改变另一个也会改变,而析构函数需要执行两次,因而会对内存空间进行两次析构。
深拷贝:复制一个一模一样的对象,不共享内存,修改新对象,旧对象不变
深拷贝更加安全, 是完整的数据拷贝, 数据是完全的另外一个备份, 但是相应的拷贝性能会下降, 占用 内存 / CPU 资源更多 ; 浅拷贝 缺少安全性, 但是性能很高, 执行效率高 ;
写时拷贝,就是在写的时候(即改变字符串的时候)才会真正开辟空间(深拷贝),如果只对数据读取时,就对数据进行浅拷贝,也称写时拷贝。 写时拷贝技术是通过“引用计数”实现的,在分配空间的时候多分配4个字节,用来存放引用计数器,记录这块空间的引用次数。 当有新的指针指向这块空间时,引用计数加一,当要释放这块空间时,引用计数减一(假装释放),直到引用计数减为0时才真正释放掉这块空间。
11. 成员访问权限问题
public 公有成员 基类、派生类、友元、外部都可以访问
protected 保护成员 基类、派生类、友元可以访问
private 私有成员 基类、友元可以访问
12. C++的组合说一下
组合是将一个对象(部分)放到另一个对象里(组合)。相比"聚合",组合是一种强所属关系,组合关系的两个对象往往具有相同的生命周期,被组合的对象是在组合对象创建的同时或者创建之后创建,在组合对象销毁之前销毁。一般来说被组合对象不能脱离组合对象独立存在,而且也只能属于一个组合对象。在C++语法中,使用在一个类中包含另外一个类类型的成员来实现组合。
(好比你和你的灵魂,你无了,你的灵魂也无了)
原文链接:https://blog.csdn.net/qq_39551987/article/details/80730664
13. 说下重载 隐藏 覆盖
重载——函数名相同,参数列表不同,作用域相同
隐藏——父子类同名同参非虚函数,父子类同名不同参虚函数
覆盖——父类虚函数,父子类同名同参
14. 怎么实现一个不可继承的类
用final关键字限制某个类不能被继承,或某个虚函数不能被重写。如果修饰函数,只能修饰虚函数,要放在类或函数后面。
#if 0
class A final
{
public:A() { cout << "A" << endl; }
};
#endif#if 0
class A
{
public:virtual void fn()final {}
};
class B :public A
{
public:virtual void fn() //fn无法被子类重写{cout << "B::fn" << endl;}
};
#endif
15. 私有继承是 is-a 还是 has-a 关系
私有继承has a(午餐有水果但是水果不是午餐);公有继承is a(橘子是水果)
16. C++中的四种类型转换
static_cast——相似类型转换,比如int,double
reinterpret_cast——不同类型转换,比如,int,int*
const_cast——去掉const属性,方便修改值
dynamic_cast——父类对象指针或引用转换为子类的指针或引用
格式:B* b=dynamic_cast<B*>(a);(<>为目标类型)
17. C++的动多态实现原理
最常见的动态多态的实现思路:对于一组相关的数据类型,抽象出它们之间共同的功能集合,在基类中将共同的功能声明为多个公共虚函数接口,子类通过重写这些虚函数,实现各自对应的功能。在调用时,通过基类的指针来操作这些子类对象,所需执行的虚函数会自动绑定到对应的子类对象上。
上述可以看出,动态多态的实现关键在于虚函数,基类通过virtual关键字声明和实现虚函数,此时基类会拥有一张虚函数表,虚函数表会记录其对应的函数指针。当子类继承基类时,子类也会获得一张虚函数表,不同之处在于,子类如果重写了某个虚函数,其在虚函数表上的函数指针会被替换为子类的虚函数指针。
18. 讲下虚函数,纯虚函数,纯虚函数有什么用
虚函数是指用 virtual 关键字修饰的类成员函数。 虚函数的作用是允许使用基类的指针来调用子类的函数,从而实现“多态”。 编译器在编译代码时,无法确定被调用函数来自基类还是派生类,所以称作“虚”函数。
纯虚函数:virtual xxxxxx=0;
在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
19. 什么函数不能声明为虚函数
只有类的成员函数才能声明为虚函数;
静态成员函数不能是虚函数;
内联函数不能为虚函数; (内联函数就是为了在代码中直接展开,减少函数调用话费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的)
构造函数不能是虚函数;
20. 虚函数的实现原理
虚函数存在只读数据段,虚表中虚函数的代码存在代码段。虚表存着虚函数的地址,基类可以调用
21. 虚函数表中存的都有什么内容
虚函数表包含了指向每个虚函数的函数指针以供类对象调用
22. 说一下 RTTI
23. 虚析构有什么用
虚析构函数是为了避免内存泄露,而且是当子类中会有指针成员变量时才会使用得到的。 也就说虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的。
24. 析构函数一定要写成虚函数吗
是
25. 重载和重写的区别
(1)范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。
(2)参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。
(3)virtual的区别:重写的基类函数必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。
26.单例模式
只能生成一个实例的模式
1)将构造函数定义为private或protected
2)static不需要对象调用,里面没有隐含的this指针
3)如果要求只能创建一个对象,要将拷贝构造函数和赋值删除,static成员函数要加引用
class A
{
protected:A(){ cout << "A" << endl; }A(const A&) = delete;//删除拷贝构造
public:A& operator=(const A&) = delete;//删除赋值重载
public:static A& fn(){static A a;return a;}
};
void main()
{A& b = A::fn();A& bb = A::fn();//没有构造成功
}
需要频繁实例化然后销毁的对象。
创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
有状态的工具类对象。
频繁访问数据库或文件的对象。
27.对于“设计模式“这个概念的理解
软件设计模式,又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。 它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。 也就是说,它是解决待定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以重复使用。 其目的是为了提高代码的可重用性、代码的可读性和代码的可靠性。
四.智能指针
1. 怎么去设计一个智能指针
智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放
2. 说下智能指针 强智能指针,引用计数这块
强指针(shared_ptr)负责资源回收,而弱指针(weak_ptr)负责线程安全
3. shared_ptr 什么时候引用计数加 1
用一个shared_ptr初始化另一个shared_ptr
用一个shared_ptr给另一个shared_ptr赋值
将shared_ptr作为参数传递给一个函数
shared_ptr作为函数的返回值
4. 熟悉哪些智能指针? weak_ptr 的作用是什么,应用场景有哪些
weak_ptr是shared_ptr的伴侣指针,它是一个观察者,它没有管理对象的权利甚至需要访问对象都要先临时创建一个shared_ptr(把weak_ptr转换成shared_ptr)。
weak_ptr<int> wp;
shared_ptr<int> sp=make_shared<int>(100);
wp=sp;
auto look=wp.lock();
if(look) //资源还在
当使用shared_ptr出现环形依赖的时候,使用weak_ptr的lock检测资源是否还在
5. 智能指针的使用场景
std::shared_ptr: 内存由多个指针变量共同使用,共同拥有内存的所有权。不要用delete。 但是必须杜绝循环拷贝、杜绝和裸指针一起使用(交叉引用)——当所有的共享指针都被摧毁,对应的资源也会被摧毁,而这时候裸指针还指向着资源,裸指针就悬空了。
std::unique_ptr :内存的所有者或者说管理者必须是唯一的。 如果进入不同的模块或者调用者,那么执行所有权转移。它也可以自动管理对象,当它被销毁了那么它绑定的资源也会被销毁(reset()释放或转移资源,release()释放资源delete并且置nullptr)。它不能拷贝构造或者赋值,但是使用过程面以避免传值,有以下三种方式实现:
1.move()
unique_ptr<int> up1=make_unique<int>(100);
unique_ptr<int> up2(up1.release());
unique_ptr<int> up2=move(up1);
2.传左值
auto up=make_unique<int>(100);
fun(up.get());
3.传引用
std::weak_ptr: 作为shared_ptr的伴侣对内存的使用仅仅是访问而已,不涉及其生命周期的管理
五.STL
1. STL 中 vector 和 list 区别,使用场景,内存管理
vector是顺序表,list是链表。查找,增删效率不同。
如果大量访问适合vector,如果大量增删适合list。
2.STL 中 map 和 unordered_map 区别?哪个效率高
unordered_map与map的区别主要在于排序和性能方面。 unordered_map是无序的,即元素的顺序是不确定的,而map是有序的,元素按照键的顺序排列。 当需要有序地遍历元素时,map的性能比unordered_map更好。
3. vector 容器底层实现,扩容方式,如果自己实现,如何实现(如果存放自定义类型, 需要干哪些事情)
一段连续的线性内存空间。vector容器的底层实现机制是通过使用三个迭代器来表示。 这三个迭代器分别是start、finish和end_of。 其中,start指向vector容器对象的起始字节位置,finish指向当前最后一个元素的末尾字节,end_of指向整个vector容器所占用内存空间的末尾字节。 通过这三个迭代器,vector容器可以实现对元素的访问和操作。
当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。 vector 容器扩容的过程需要经历以下 3 步: 完全弃用现有的内存空间,重新申请更大的内存空间; 将旧内存空间中的数据,按原有顺序移动到新的内存空间中; 最后将旧的内存空间释放。
4. 详细说一下迭代器使用和分类
- p+=i:使得 p 往后移动 i 个元素。
- p-=i:使得 p 往前移动 i 个元素。
- p+i:返回 p 后面第 i 个元素的迭代器。
- p-i:返回 p 前面第 i 个元素的迭代器。
- p [i]:返回 p 后面第 i 个元素的引用。
- InputIterator :输入迭代器。 支持对容器元素的逐个遍历,以及对元素的读取(input);
- OutputIterator :输出迭代器。 支持对容器元素的逐个遍历,以及对元素的写入(output)。
- ForwardIterator :前向迭代器。 向前逐个遍历元素。 可以对元素读取;
- BidirectionalIterator :双向迭代器。 支持向前向后逐个遍历元素,可以对元素读取。
- RandomAccessIterator :随机访问迭代器。 支持O (1)时间复杂度对元素的随机位置访问,支持对元素的读取。
5. map 中[]运算符和 find 运算符有什么区别?
1) map的下标运算符 []的作用是:将关键码作为下标去执行查找,并返回对应的值;如果不存在这个关键码,就将一个具有该关键码和值类型的默认值的项插入这个map。
2) map的find函数:用关键码执行查找,找到了返回该位置的迭代器;如果不存在这个关键码,就返回尾迭代器。
6.详细解释 deque 的底层原理
deque的迭代器是一个指针,它指向deque中的一个元素。 我们可以使用迭代器来访问deque中的元素,也可以使用迭代器来修改deque中的元素。 deque是一种双端队列,它的底层实现是一个双向链表。
7. 迭代器失效
当需要增加元素而容量不够需要扩容的时候,数据转移到了新的内存区域,迭代器就会失效,就让它重新从begin开始。