目录
1.Qt基础知识
1. Qt信号槽机制的优势和不足
2. static和const的使用
3. 指针常量、常量指针,常指针常量
4. 指针和引用的异同
5. 如何理解多态
6. 虚函数表
7. 常用数据结构
8.Tcp
2. C++ 基础
1. C语言和C++有什么区别?
2. struct 和class 有什么区别?
3. extern "C"的作用?
4. 函数重载和覆盖有什么区别?
5. 谈一谈你对多态的理解,运行时多态的实现原理是什么?
6. 对虚函数机制的理解,单继承、多继承、虚继承条件下虚函数表的结构
7. 如果虚函数是有效的,那为什么不把所有函数设为虚函数?
8. 为什么要虚继承?
9. 构造函数可以是虚函数吗?析构函数可以是虚函数吗?
10. 为什么虚函数表中有两个析构函数?
11. 为什么基类析构函数要是虚函数?
12. 什么场景需要用到纯虚函数?纯虚函数的作用是什么?
13. 了解RAII 吗?介绍一下?
14. 类的大小怎么计算?
15. volatile 关键字的作用?什么时候需要使用volatile 关键字
16. 了解各种强制类型转换的原理及使用?
17. 指针和引用有什么区别?什么情况下用指针,什么情况下用引用?
18. 一般什么情况下会出现内存泄漏?怎么用C++在编码层面尽量避免内存泄漏。
19. unique_ptr 如何转换所有权?
20. 谈一谈你对面向对象的理解
21. 什么场景下使用继承方式,什么场景下使用组合?
22. new 和malloc 有什么区别?
23. malloc的内存可以用delete释放吗?
24. malloc出来20字节内存,为什么free不需要传入20呢,不会产生内存泄漏吗?
25. new[]和delete[]一定要配对使用吗?new[]和delete[]为何要配对使用?
3. C++11
1. 了解auto 和decltype 吗?
2. 谈一谈你对左值和右值的了解,了解左值引用和右值引用吗?
3. 了解移动语义和完美转发吗?(这个大家自己百度吧,有点复杂)
4. 了解列表初始化吗?
5. 平时会用到function、bind、lambda 吗,都什么场景下会用到?
6. 对C++11 的mutex 和RAII lock 有过了解吗?
7. 对C++11 的智能指针了解多少,可以自己实现一个智能指针吗?
8. enum 和enum class 有什么区别?
4. STL
1. C++直接使用数组好还是使用std::array 好?std::array 是怎么实现的?
2. std::vector 最大的特点是什么?它的内部是怎么实现的?resize 和reserve 的区别是什么?clear 是怎么实现的?
3. deque 的底层数据结构是什么?它的内部是怎么实现的?
4. map 和unordered_map 有什么区别?分别在什么场景下使用?
5. list 的使用场景?std::find 可以传入list 对应的迭代器吗?
6. string 的常用函数
5. 设计模式
1. 分别写出饿汉和懒汉线程安全的单例模式
优点:类型安全,松散耦合。缺点:同回调函数相比,运行速度较慢。
优点:
缺点:
原因:
static:静态变量声明,分为局部静态变量,全局静态变量,类静态成员变量。也可修饰类成员函数。有以下几类:
const:常量声明,类常成员函数声明。 const和static不可同时修饰类成员函数,const修饰成员函数表示不能修改对象的状态,static修饰成员函数表示该函数属于类,不属于对象,二者相互矛盾。const修饰变量时表示变量不可修改,修饰成员函数表示不可修改任意成员变量。
int main()
{
int x = 10;
int y = 20;
// 指针常量(常指针):数据类型 * const 指针 = 变量
int* const p1 = &x; // 指针常量,p1不可更改
// p1 = &y; 错误
*p1 = 30; // 正确,变量的值可以更改
cout << "x: " << x << endl; // x: 30
// 常量指针:const 数据类型 * 指针 = 变量
// 或:数据类型 cosnt * 指针 = 变量
const int* p2 = &x; // 常量指针,*p2不可变
// *p2 = y; 错误
p2 = p1; // 正确,p2可变
// 常指针常量:const 数据类型 * cosnt 指针 = 变量
const int * const p3 = &x; // p3和*p3均不可更改
// p3 = p2; 错误
// *p3 = 20; 错误
}
指针:是一个变量,但是这个变量存储的是另一个变量的地址,我们可以通过访问这个地址来修改变量。
引用:是一个别名,还是变量本身。对引用进行的任何操作就是对变量本身进行的操作。
相同点:二者都可以对变量进行修改。
不同点:指针可以不必须初始化,引用必须初始化。指针可以有多级,但是引用只有一级(int&& a不合法, int** p合法)。指针在初始化后可以改变,引用不能进行改变,即无法再对另一个同类型对象进行引用。sizeof指针可以得到指针本身大小,sizeof引用得到的是变量本身大小。指针传参还是值传递,引用传参传的是变量本身。
定义:同一操作作用于不同的对象,产生不同的执行结果。C++多态意味着当调用虚成员函数时,会根据调用类型对象的实际类型执行不同的操作。
实现:通过虚函数实现,用virtual声明的成员函数就是虚函数,允许子类重写。声明基类的指针或者引用指向不同的子类对象,调用相应的虚函数,可以根据指针或引用指向的子类的不同从而执行不同的操作。
Overload(重载):函数名相同,参数类型或顺序不同的函数构成重载。
Override(重写):派生类覆盖基类用virtual声明的成员函数。
Overwrite(隐藏):派生类的函数屏蔽了与其同名的基类函数。派生类的函数与基类函数同名,但是参数不同,隐藏基类函数。如果参数相同,但是基类没有virtual关键字,基类函数将被隐藏。
多态是由虚函数实现的,而虚函数主要是通过虚函数表实现的。如果一个类中包含虚函数,那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。该类的每个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),需指针指向虚函数表。注意:对象不包含虚函数表,只有需指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表。
1.三次握手:建立一个TCP连接时,需要客户端服务端总共发送三个包以确认连接的建立。在这一过程中由客户端执行connect来触发,流程如下:
2.四次挥手:断开一个Tcp连接时,需要客户端和服务端总共发送四个包以确认连接的端口。在socket编程中,这一过程由客户端或服务端任一方执行close来触发,流程如下:
C语言是面向过程的,抽象化的通用设计语言,主要用于底层开发,C++是C的超集,继承并扩展了C语言,C++即可以进行C语言的过程化程序设计,又可以进行以面向对象为主要特点的程序设计。
extern "C" 的主要作用就是为了能够正确实现C++代码调用其它C语言代码。加上extern "C"后,会提示编译器这部分代码按C语言(而不是C++)的方式进行编译。
由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名(而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包含函数名)。也就是说,C++和C对生成的函数名称的处理是不易的,extern "C" 的目的就是主要实现C和C++的相互调用问题。
多态:就是多种形态,C++的多态分为静态多态和动态多态。静态多态就是重载,因为在编译器决议确定,所以成为静态多态。在编译时就可以确定函数地址。动态多态即运行时多态是通过继承重写基类的虚函数实现的多态,因为在运行时决议确定,所以称为动态多态,也叫运行时多态。运行时在虚函数表中寻找调用函数的地址。在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是子类,就调用子类的函数。如果对象类型是父类,就调用父类的函数,(即指向父类调父类,指向子类调子类)此为多态的表现。
运行时多态实现原理:
虚函数就是类中用virtual声明的成员函数,利用虚函数可以实现运行时多态。 带虚函数的对象布局如下:
单继承:将基类虚表中的内容拷贝一份到子类虚表中,如果派生类重写了基类某个虚函数,就用派生类自己的虚函数替换掉原先基类虚函数的入口地址。对象布局如下:
多继承:多重继承会有多个虚函数表,几重继承就会有几个虚函数表。这些表按照派生的顺序一次排列,如果子类改写了父类的虚函数,那么就会用子类自己的虚函数覆盖虚函数表相应的位置,如果子类有新的虚函数,那么就添加到第一个虚函数表的末尾。对象布局如下:
虚继承:对象布局和普通继承不同,普通继承下子类和基类公用一个虚表地址,而在虚继承下,子类和虚基类分别有一个虚表地址的指针。对象布局如下:
虚函数是有代价的,由于每个虚函数的对象都要维护一个虚函数表,因此在使用虚函数的时候会产生一定的系统开销,这是没有必要的。另外,虚函数的调用相对于普通函数要更慢一些,因此每次都要查找虚函数表,有一定的时间开销。
如图:
非虚继承时,显然D会继承两次A ,内部就会存储两份A的数据,浪费空间,而且还会有二义性,D调用A的方法时,由于有两个A,究竟调用哪个A的方法呢,编译器也不知道,就会报错,所以有了虚继承,解决了空间浪费以及二义性问题。在虚继承下,只有一个共享的基类对象被继承,而无论该基类在派生层次中出现多少次。共享的基类子对象被称为虚基类,在虚继承下,基类对象的复制以及由此引起的二义性被消除了。
构造函数不能是虚函数,析构函数可以是虚函数。理由如下:构造函数就是为了在编译阶段确定对象的类型以及为对象分配空间,如果类中有虚函数,那就会在构造函数中初始化虚函数表,虚函数的执行缺需要依赖虚函数表。如果构造函数是虚函数,那么它就需要依赖虚函数表才可执行,而只有在构造函数中才会初始化虚函数表,鸡生蛋蛋生鸡的问题,很矛盾,所以构造函数不能是虚函数。而析构函数不存在这个问题,所以析构函数可以是虚函数。
这是因为对象有两种构造方式,栈构造和堆构造,所以在对应的实现上,对象也有两种析构方式,其中堆上对象的析构和栈上对象的析构不同之处在于,栈内存的析构不需要执行delete函数,会自动被回收。
一般基类的析构函数都要设置成虚函数,因为如果不设置成虚函数,咋析构的过程中只会调用到基类的析构函数而不会调用到子类的析构函数,可能会产生内存泄漏。
在许多情况下,在基类中不能对虚函数给出有意义的的实现,而把它定义为纯虚函数,它的实现留给派生类去做。作用:1.为了方便使用多态特性。2.在很多情况下,基类本身生成对象时不合情理的。
RAII: Resource Acquisition Is Initialization,资源获取即初始化,将资源的生命周期与一个对象的生命周期绑定,举例来说就是,把一些资源封装在类中,在构造函数中请求资源,在析构函数中释放资源且绝不抛出异常,而一个对象在生命周期结束时会自动调用析构函数,即资源的生命周期和一个对象的生命周期绑定。
空类的大小是一个特殊情况,空类的大小为1。sizeof计算。
volatile关键字告诉编译器其修饰的变量是易变的,它会确保修饰的变量每次读操作都从内存里读取,每次写操作都将值写到内存里。volatile关键字就是给编译器做个提示,告诉编译器不要对修饰的变量做过度的优化,提示编译器该变量的值可能会以其它形式被改变。
volatile用于读写操作不可以被优化掉的内存,用于特种内存中。
区别:
相同点:
何时使用:
内存泄漏是指程序向系统申请分配内存使用(new),用完以后却没有归还(delete)。结果申请的那块内存程序不再使用,而系统也无法再讲它分配给需要的程序。
造成内存泄漏的几种情况:
如何避免内存泄漏:
利用std::move,例如:
std::unique_ptr uniquePtr = std::make_unique(10);
// std::shared_ptr sharedPtr = std::move(unique);
std::unique_ptr uniquePtr2 = std::move(unique);
继承:通过扩展已有的类来获得新功能的代码重用方法
组合:新类由现有类的对象合并而成的类的构造方式
使用场景:
可以,但是一般不这么用。malloc和free是C语言中的函数,C++为了兼容C语言保留下来这一对函数。简单来说,new可以理解为,先执行malloc来申请内存,后调用构造函数来初始化对象,delete是先执行析构函数,后使用free来释放内存。若先new再使用free来释放空间的话,可能会出现一些错误。而先使用malloc,再使用delete的话没有问题。
因为不能保证程序员使用free时传入的参数是和malloc一致的,从而导致内存泄漏等问题。现在free的解决方式是让free函数自己确定要释放多少内存,可以使用的方式是在申请内存时多申请一些空间来存储内存大小,在free时再获取这个大小进行释放。
1.不一定,当类型为int、float等内置类型时,可以不配对使用,但是建议还是配对使用。
2.new[]为一个数组申请内存时,编译器还会悄悄地在内存中保存一个整数,用来表示数组中元素的个数。因为在delete一块内存时,我们不仅要知道指针指向多大的内存,更重要的是要知道指针指向的数组中对象的个数。因为只有知道了对象数量才能一一调用它们的析构函数,完成对数组中所有对象的清理。如果使用的是delete,则编译器只会将指针所指的对象当作单个对象来处理。所以对于数组,需要使用delete[]来处理,符号[]会告诉编译器在delete这块内存时,先去获取保存的那个元素数量值,然后再进行一一清理。
auto:可以让编译器在编译时就推导出变量的类型,代码如下:
auto a = 10; // 10是int型,自动推导出a是int
int i = 10;
auto b = i; // b是int
auto d = 2.0; // d是double
decltype:用于推导表达式类型,代码如下:
int func() { return 0; };
decltype(func()) i; // i是int
int x = 0;
decltype(x) y; // y是int
decltype(x + y) z; // z是int
左值:在内存中有确定存储地址、有变量名、表达式结束依然存在的值。
左值引用:绑定到左值的引用,通过&来获得左值引用。
右值:在内存中没有确定存储位置、没有变量名,表达式结束就会销毁的值。
右值引用:绑定到右值的引用,通过&&来获得右值引用。
int a1 = 10; // 非常量左值
const int a2 = 10; // 常量左值
int& b1 = a1; // 非常量左值引用
const int& b2 = a2; // 常量左值引用
int&& c1 = 10; // 非常量右值引用
const int&& c2 = 10; // 常量右值引用,10是非常量右值
移动语义:可以理解为转移所有权,拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用,通过C++11新增的移动语义可以省去很多拷贝负担,如何利用移动语义,主要通过移动构造函数。
完美转发:指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参。转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数也是右值。
列表初始化:可以直接在变量名后面加上初始化列表来进行对象的初始化。
std::function:是可调用对象的封装器,可以看做是一个函数对象,用于表示函数这个抽象概念。std::function的实例可以存储、复制和调用任何可调用对象,存储的可调用对象称为std::function的目标,若std::function不含目标,则称它为空,调用空的std::function的目标会抛出std::bad_function_call异常。
#include
#include
using namespace std;
struct Foo
{
Foo(int num) : num_(num) {}
void print_add(int i) const { cout << num_ + i << endl; }
int num_;
};
void print_num(int i) { cout << i << "\n"; }
struct PrintNum
{
void operator()(int i) const { cout << i << "\n"; }
};
int main()
{
// 存储自由函数
function f1 = print_num;
f1(10);
// 存储 lambda
function f2 = []() { print_num(20); };
f2();
// 存储 std::bind 的返回值
function f3 = bind(print_num, 30);
f3();
// 存储成员函数的调用
function f4 = &Foo::print_add;
const Foo foo(10);
f4(foo, 30);
f4(10, 30);
// 存储数据成员
function f5 = &Foo::num_;
cout << f5(Foo(50)) << "\n";
// 存储成员函数及对象的调用
function f6 = bind(&Foo::print_add, foo, placeholders::_1);
f6(50);
// 存储成员函数和对象指针的调用
function f7 = bind(&Foo::print_add, &foo, placeholders::_1);
f7(60);
// 存储函数对象的调用
function f8 = PrintNum();
f8(80);
return 0;
}
std::bind:使用std::bind可以将可调用对象和参数一起绑定,绑定后的结果使用std::function进行保存,并延迟调用到任何我们需要的时候。
lambda表达式:定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用。
mutex:互斥量,是一种线程同步的手段,用于操作多线程同时读写的共享数据。
RAII lock:可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁。
三种智能指针:
std::shared_ptr:使用引用计数,每一个shared_ptr的拷贝都指向相同的内存,每次拷贝都会触发引用计数+1,每次生命周期结束析构的时候引用计数-1,在最后一个shared_ptr析构的时候,内存才会释放。
std::weak_ptr:用来监视shared_ptr的生命周期,它不管理shared_ptr内部的指针,它的拷贝析构都不会影响引用计数,纯粹是作为一个旁观者监视shared_ptr中管理的资源是否存在,可以用来返回this指针和解决循环引用问题。
std::unique_ptr:独占型的智能指针,它不允许其它智能指针共享其内部指针,也不允许unique_ptr的拷贝和赋值。
std::array好。std::array除了有传统数组支持随机访问、效率高、存储大小固定等特点外,还支持迭代器访问、获取容量、获得原始指针等高级功能。而且它还不会退化成指针T*给开发人员造成困惑。
实现原型如下:
namespace std
{
template
class array;
}
特点:
内部实现:底层采用的数据结构非常简单,就只是一段连续的线性内存。内部使用三个迭代器来表示,分别指向vector容器对象的起始字节位置,当前最后一个元素的末尾字节,整个容器所占用内存空间的末尾字节。因此通过这三个迭代器,就可以计算出容器的size和capacity。当vector的大小和容量相等时(size==capacity)时,如果再向其添加元素,那么vector就需要扩容。扩容的过程主要是一下三步:1.完全弃用现有的内存空间,重新申请更大的内存空间(一般为原有空间大小的2倍或1.5倍,不同编译器扩容策略可能不同。)。2.将旧内存空间中的数据,按原有顺序移动到新的内存空间中。3.最后将旧的内存空间释放。
resize和reserve区别:
clear实现:clear只是将vector的size置0,并不保证capacity为0,因此clear并不能释放vector已经申请的内存。
deque是双端队列,非常适合在头部和尾部添加或删除数据。deque容器存储数据的空间是由一段一段等长的连续空间构成,各段空间之间并不一定是连续的,可以位于内存的不同区域。为了管理这些连续空间,deque容器用数组(数组名假设为map)存储着各个连续空间的首地址。也就是说,map数组中存储的都是指针,指向那些真正用来存储数据的各个连续空间。如下图所示:
通过建立map数组,deque容器中申请的这些分段的连续空间就能实现整体连续的效果。换句话说,当deque需要在头部或尾部增加存储空间时,它会申请一段新的连续空间,同时在map数组的开头或结尾添加指向该空间的指针,由此该空间就接到了deque容器的头部或尾部。deque容器的分段存储结构,提高了在序列两端添加或删除元素的效率。
map是有序的,unorder_map是无序的,map适用于有顺序要求的问题,unorder_map适用于查找问题,unordered_map的查找效率优于map。map的内部是红黑树,unordered_map的内部是哈希表。
list是双向链表容器,底层是以双向链表的的形式实现的。这意味着,list容器中的元素可以分散存储在内存空间里,而不是必须存储在一整块连续的内存空间。如下图所示:
可以看到,list容器中各个元素的前后顺序是靠指针来维系的,每个元素都配备了两个指针,分别指向它的前一个元素和后一个元素,其中第一个元素的前向指针为null,因为它前面没有元素;同样,尾部元素的后向指针也为null。因此,它可以在序列已知的任何位置快速插入或删除元素,因此在实际场景中,如果需要对序列进行大量添加或删除元素的操作,而直接访问元素的需求却很少,这种情况建议使用list容器存储序列。
可以。
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。根据单例对象创建时间,可分为两种模式:懒汉模式和饿汉模式。
#include
#include
using namespace::std;
// 懒汉模式一:多线程不安全
template
class Singleton
{
public:
static T* getInstance()
{
if (instance_ == nullptr)
{
instance_ = new T();
}
return instance_;
}
private:
Singleton() = delete;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
static T* instance_;
};
template
T* Singleton::instance_ = nullptr;
// 懒汉模式二:多线程安全
template
class Singleton2
{
public:
static T* getInstance()
{
if (instance_ == nullptr)
{
mutex_.lock();
if (instance_ == nullptr)
{
instance_ = new T();
}
mutex_.unlock();
}
return instance_;
}
private:
Singleton2() = delete;
Singleton2(const Singleton2&) = delete;
Singleton2& operator=(const Singleton2&) = delete;
private:
static T* instance_;
static mutex mutex_;
};
template
T* Singleton2::instance_ = nullptr;
template
mutex Singleton2::mutex_;
class Printer
{
friend class Singleton;
friend class Singleton2;
private:
Printer() = default;
Printer(const Printer&) = delete;
Printer& operator=(const Printer&) = delete;
public:
void print() { cout << "Printer" << endl; }
};
int main(int argc, char* argv[])
{
Singleton::getInstance()->print();
Singleton2::getInstance()->print();
}
#include
#include
using namespace::std;
// 饿汉模式
template
class Singleton
{
private:
Singleton() = delete;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static T* getInstance()
{
return instance_;
}
private:
static T* instance_;
};
template
T* Singleton::instance_ = new T();
class Printer
{
friend class Singleton;
private:
Printer() = default;
Printer(const Printer&) = delete;
Printer& operator=(const Printer&) = delete;
public:
void print() { cout << "Printer" << endl; }
};
int main(int argc, char* argv[])
{
Singleton::getInstance()->print();
}
定义了对象之间一对多的依赖,令多个观察者对象同时监听某一个主题对象,当主题对象发生改变时,所有的观察者都会收到通知并更新。
优点:
缺点:
优点:
缺点:
定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。简单工厂模式可以决定在什么时候创建哪一个产品类的实例。工厂方法模式有非常良好的扩展性。抽象工厂模式降低了模块间的耦合性,提高了团队开发效率。
构造者模式是较为复杂的创建型模式,它将客户端与包含多个组成部分的复杂对象的创建过程分离。客户端无需知道具体的构造过程,只需要与构造器打交道即可,构建与表示分离。
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能在一起工作的那些类一起工作。
进程的定义:一个具有一定独立功能的程序在一个数据集合上依次动态执行的过程。进程是一个正在执行程序的实例,包括程序计数器、寄存器和程序变量的当前值。简单来说,进程就是一个程序的执行流程,内部保存程序运行所需的资源。在操作系统中可以有多个进程在运行,可对于CPU来说,同一时刻,一个CPU只能运行一个进程,但在某一时间段内,CPU将这一时间段拆分成更短的时间片,CPU不停的在各个进程间游走,这就给人一种并行的错觉,像CPU可以同时运行多个进程一样,这就是伪并行。
线程的定义:线程是进程当中的一条执行流程,这几乎就是进程的定义,一个进程内可以有多个子执行流程,即线程。从资源组合的角度看,进程把一组相关的资源组合起来,构成一个资源平台环境,包括地址空间(代码段,数据段),打开的文件等各种资源。从运行的角度看:进程是代码在这个资源平台上的执行流程,然而线程貌似也是这样,但是进程比线程多了资源内容列表样式:进程 = 线程 + 共享资源。
进程是操作系统分配资源的单位,线程是调度的基本单位,线程之间共享进程资源。
这里就不得不提到一个数据结构:进程控制块(PCB),操作系统为每个进程都维护一个PCB,用来保存与该进程有关的各种状态信息。进程可以抽象理解为就是一个PCB,PCB是进程存在的唯一标志。操作系统用PCB来描述进程的基本情况及运行变化的过程,进程的任何状态变化都会通过PCB来体现。
PCB包含进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、打开文件的状态、账号和调度信息,以及其它在进程由运行态切换到就绪态或阻塞态时必须保存的信息,从而保证该进程随后能再次启动,就像从未中断过一样。
提到进程管理,有一个概念我们必须知道,就是中断向量,中断向量是指中断服务程序的入口地址。一个进程在执行过程中可能会被中断无数次,但是每次中断后,被中断的进程都要返回到与中断发生前完全相同的状态。
进程的每次变化都会有相应的状态,而操作系统维护了一组队列,表示系统中所有进程的当前状态。不同的状态有不同的队列,有就绪队列阻塞队列等,每个进程的PCB都根据它的状态加入到相应的队列中,当一个进程的状态变化时,它的PCB会从一个状态队列中脱离出来加入到另一个状态队列。
上下文切换指的是操作系统停止当前运行进程(从运行态改变成其它状态)并且调度其它进程(就绪态转变成运行态)。操作系统必须在切换之前存储许多部分的进程上下文,必须能够在之后恢复他们,所以进程不能显示它曾经被暂停过,同时切换上下文这个过程必须快速,因为上下文切换操作是非常频繁的。那上下文指的是什么呢?指的是任务所有共享资源的工作现场,每一个共享资源都有一个工作现场,包括用于处理函数调用,局部变量分配以及工作现场保护的栈顶指针,和用于指令执行等功能的各种寄存器。
私有:每个线程都有独立的,私有的栈区,程序计数器,栈指针以及函数运行使用的寄存器。
共有:代码区,堆区
死锁:如果一组进程中的每一个进程都在等待仅有该组进程中的其它进程才能引发的事件,此时这组进程就被称为死锁。
产生死锁的必要条件:
避免死锁:系统对进程发出每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配。这是一种保证系统不进入死锁状态的动态策略。
大端字节:将数据的低位字节存放在内存的高位地址,高位字节存放在低位地址。
小端字节:将数据的高位字节存放在内存的高位地址,低位字节存放在低位地址。
信号:一种处理异步事件的方式。信号是比较复杂的通信方式,用于通知接收进程有某种事件发生,除了用于进程外,还可以发送信号给进程本身。
信号量:进程间通信处理同步互斥的机制。是在多线程环境下使用的一种设施,它负责协调各个线程,以保证它们能够正确,合理的使用公共资源。
预处理-》编译-》汇编-》链接
不一定,可以指定程序入口,main是默认的程序入口。
静态库在程序的链接阶段被赋值到了程序中;动态库在链接阶段没有被复制到程序中,而是程序在运行时动态加载到内存中供程序调用。使用动态库系统只需载入一次,不同的程序可以得到内存中相同的动态库的副本,因此节省了很多内存,而且使用动态库也便于模块化更新程序。