面试嵌入式工程师常问的问题(面试嵌入式都会问那些问题呢)
下面是好好范文网小编收集整理的面试嵌入式工程师常问的问题(面试嵌入式都会问那些问题呢),仅供参考,欢迎大家阅读!
「目录」
一、C/C++编程基础
二、计算机网络编程
三、操作系统
四、数据结构及算法
参考
本篇参考网上及自身的面试经验,总结一些高频考察的Linux C/C++知识点,方便后续查阅总结。
一、C/C++编程基础
C++多态的实现
virtual关键字修饰基类的成员函数,派生类中重写此函数,实现多态
C++四种强制类型转换[1]
static_cast
static_cast<type>(expression)
该运算符把 expression 转换为 type 类型,主要用于基本数据类型之间的转换,如把 uint 转换为 int,把 int 转换为 double 等。
另外,static_cast 还可用于类层次结构中,基类和派生类之间指针或引用的转换,但也要注意:
static_cast 进行上行转换是安全的,即把派生类的指针转换为基类的;
static_cast 进行下行转换是不安全的,即把基类的指针转换为派生类的。
注:static_cast 没有运行时类型检查来保证转换的安全性,需要程序员来判断转换是否安全。
uint x = 1;int y = static_cast<int>(x); // 转换正确int x = 1;double y = static_cast<double>(x); // 转换正确// 上行转换,派生类→基类Derive* d = new Derive();Base* b = static_cast<Base*>(d);// 下行转换,基类→派生类Base* b = new Base();Derive* d = static_cast<Derive*>(b);
const_cast
const_cast<type>(expression)
主要是用来去除复合类型中const和volatile属性(没有真正去除)。expression 和 type 的类型需要保持一致。
dynamic_cast
dynamic_cast<type>(expression)
主要用于类层次间的上行转换或下行转换。在进行上行转换时,dynamic_cast 和 static_cast 的效果是一样的,但在下行转换时,dynamic_cast 具有类型检查的功能,比 static_cast 更安全。
reinterpret_cast
reinterpret_cast<type>(expression)
该运算符可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针。
static的作用[2]
限制变量或函数作用域
被static关键字修饰的全局函数或者变量具有文件作用域,即只在当前文件中可见。
保持变量内容的持久
被static修饰的变量会被存储在静态存储区,生命周期也为从定义直至程序结束。对于局部变量,即使在函数退出后该静态变量依然存在,然而却也无法访问。此外,static修饰的变量一生只会被初始化一次。
默认初始化为0
因为被static修饰的变量会被存储在静态存储区,所以才有了这个一条。因为静态存储区的变量会被默认初始化为0。
除此之外,在C++中,static也可以类成员变量和类成员函数。
类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致 了它仅能访问类的静态数据和静态成员函数。
静态成员函数不含有this指针,所以可以作为回调函数。但同时为了可以访问类的成员变量可以将对象的this指针当做实参传入回调函数中。
静态成员函数在类定义体外定义时不能加static关键字修饰,因为成员函数本是类作用域,而在类外用static修饰会将其作用于扩大为文件作用域,所以是不合理的。
静态成员变量并不像一般的成员变量在构造函数中初始化,而是在类的实现文件中初始化,即必须在.cpp文件中初始化,否则在程序链接时会出错,重定义,且初始化时无需再使用static关键字修饰。
static修饰的const成员变量可以再类中被定义时初始化。
利用static只会被初始化一次的特性,可以实现单例对象。
extern 有什么作用
extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。extern声明既不是定义,也不分配存储空间。
说一说const关键字[3]
const关键字告诉了编译器,它修饰的目标值不能被改变,如果代码中发现有类似改变该变量的操作,那么编译器就会捕捉这个错误。修饰函数时,
好的编程习惯告诫程序员,当不需要改变的变量,最好使用const修饰,例如非头文件定义的常量,最好用const而非define。
sizeof 和 strlen 的区别
sizeof会将空字符 计算在内,而strlen不会将空字符 计算在内;
sizeof会计算到字符串最后一个空字符 并结束,而strlen如果遇到第一个空字符 的话就会停止并计算遇到的第一个空字符 前面的长度。
#define和const的区别[4]
#define 只是在预处理阶段简单的做字符替换,其可以实现宏函数和变量等;const是在编译、运行阶段起作用,修饰变量。
const 定义的常数是变量也带类型,#define 定义的只是个常数 不带类型。
#define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误。
#define属于预处理,会占用代码空间;const+类型定义属于变量定义,占用数据段空间。
一个指针可以是 volatile 吗
可以。volatile关键词修饰的变量意思为值可能会改变,指针是可以改变的,与const关键词相反。
定义和声明的区别
声明是将一个名称引入程序,定义提供了一个实体在程序中的唯一描述。
什么是野指针
访问一个已销毁或者访问受限的内存区域的指针。野指针的使用,可能会引起程序崩溃,且无法用NULL检测野指针。
指针和引用的区别[5]
指针是一个变量,存储的是一个地址,指向内存的一个存储单元,指针变量占用内存;
引用是原变量的一个别名,跟原来的变量实质上是同一个东西,引用变量不占用内存。
简述指针常量与常量指针区别[6]
指针常量:本质是一个常量,而用指针修饰它。指针常量的值是指针,这个值因为是常量,所以不能被赋值。
常量指针:又叫常指针,可以理解为常量的指针,也即这个是指针,但指向的是个常量,这个常量是指针的值(地址),而不是地址指向的值。
引用作为函数参数以及返回值的好处[7]
在内存中不产生被返回值的副本。(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtime error!)
谈谈C++内存管理[8]
「stack栈区」:专门用来实现函数调用-栈结构的内存块。相对空间下(可以设置大小,Linux 一般默认是8M,可通过 ulimit –s 查看),系统自动管理,从高地址往低地址,向下生长。
「内存映射区」:包括文件映射和匿名内存映射, 应用程序的所依赖的动态库,会在程序执行时候,加载到内存这个区域,一般包括数据(data)和代码(text);通过mmap系统调用,可以把特定的文件映射到内存中,然后在相应的内存区域中操作字节来访问文件内容,实现更高效的IO操作;匿名映射,在glibc中malloc分配大内存的时候会用到匿名映射。这里所谓的“大”表示是超过了MMAP_THRESHOLD 设置的字节数,它的缺省值是 128 kB,可以通过 mallopt() 去调整这个设置值。还可以用于进程间通信IPC(共享内存)。
「heap堆区」:主要用于用户动态内存分配,空间大,使用灵活,但需要用户自己管理,通过brk系统调用控制堆的生长,向高地址生长。
「BBS段和DATA段」:用于存放程序全局数据和静态数据,一般未初始化的放在BSS段(统一初始化为0,不占程序文件的空间),初始化的放在data段,只读数据放在rodata段(常量存储区)。
「text段」:主要存放程序二进制代码。
谈谈new、delete、malloc、free[9]
共同点:
都是从堆上申请空间,并且需要用户手动释放。
不同点:
malloc和free是函数,new和delete是操作符
malloc申请的空间不会初始化,new可以初始化
malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可。
malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型。
malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。
申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。
delete和delete的区别
delete只会调用一次析构函数,而delete 会调用每一个成员的析构函数。
简述 strcpy、sprintf 与 memcpy 的区别
操作对象不同,strcpy 的两个操作对象均为字符串; sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串; memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
C++的空类有哪些成员函数
一个默认构造函数、一个拷贝默认构造函数、一个默认拷贝赋值操作符和一个默认析构函数。这些函数只有在第一次被调用时,才会被编译器创建。所有这些函数都是inline和public的。
构造函数为什么不能是虚函数[10]
构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。。。
虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。
深拷贝和浅拷贝的区别
浅拷贝: 与拷贝对象共享同一片内存。只复制对象的基本类型,对象类型,仍属于原来的引用。
深拷贝: 申请新的内存,并将目标对象复制到新的内存。
知道STL吗,挑两个你最常用的容器说一说
容器分为两大类: 顺序容器和关联容器
顺序容器
顺序容器有以下三种:可变长动态数组 vector、双端队列 deque、双向链表 list。
之所以被称为顺序容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置(尾部、头部或中间某处)插入,元素就会位于什么位置。
「vector」:
vector 实现的是一个动态数组,单向开口的连续性空间,支持内部元素随机访问,能够自动根据需要动态扩容。[11]
「deuque」:
deque是双向开口的连续线性空间,支持内部元素的随机访问。擅长在序列尾部添加或删除元素(时间复杂度为O(1)),而不擅长在序列中间添加或删除元素。且支持头部添加和删除元素的成员。[12]
「list」:
双向链表容器,即该容器的底层是以双向链表的形式实现的。非连续空间、通过指针来连接每一个小空间、插入和删除都是O(1)操作,元素访问效率较低等等,不支持随机访问。
关联容器
关联容器有以下四种:set、multiset、map、multimap。关联容器内的元素是排序的。插入元素时,容器会按一定的排序规则将元素放到适当的位置上,因此插入元素时不能指定位置。
「set」:
含有 Key 类型对象的已排序集。用比较函数 比较 (Compare) 进行排序。搜索、移除和插入拥有对数复杂度。set 通常以红黑树实现。[13]
「multiset」:
是排序好的集合(元素已经进行了排序),并且允许有相同的元素。[14]
「map」:
是有序键值对容器,它的元素的键是唯一的。[15]
「multimap」:multimap也是存储两个元素之间的映射关系的容器,不相同的是,multimap的key值可以重复出现。[16]
STL中的vector的实现,是怎么扩容的
vector 为空的时候没有预分配空间,每次添加一个元素时,会判断当前是否还有剩余可用空间,如果没有则进行试探性扩容,并且把内存拷贝到新申请的内存空间上,并且释放原先的内存。
C++中vector和list的区别
「vector」是动态数组,内部存储是一片连续性的空间,支持通过下标随机访问。
「list」是双向链表,内部存储可能是不连续的空间,通过指针链接这些不连续的空间,不支持随机访问。
怎么确定一个程序是C编译的还是C++编译的
如果编译器在编译cpp文件那么__cplusplus
就会被定义 如果是一个c文件在被编译那么__STDC__
就会被定义。
说一下什么是内存泄漏,如何避免
是指程序在申请内存后,无法释放已申请的内存空间,称之为内存泄露。无法释放的内存会一直被无效占用,且无法被再次使用,累计下来会导致进程占用内存越来越大,直至无内存资源可用,导致进程崩溃。
C++中内存泄漏的几种情况[17]
在类的构造函数和析构函数中没有匹配的调用new和delete函数
没有正确地清除嵌套的对象指针
在释放对象数组时在delete中没有使用方括号
指向对象的指针数组不等同于对象数组
缺少拷贝构造函数
缺少重载赋值运算符
没有将基类的析构函数定义为虚函数
一个文件从源码到可执行文件所经历的过程
① 预处理,产生.ii文件
② 编译,产生汇编文件 (.s文件)
③ 汇编,产生目标文件 (.o或.obj文件)
④ 链接,产生可执行文件
聊聊C++11新特性[18]
C++构造函数和析构函数的调用顺序[19]
「构造函数顺序」: 基类构造函数、对象成员构造函数、派生类本身的构造函数。
「析构函数顺序」: 派生类本身的析构函数、对象成员析构函数、基类析构函数(与构造顺序正好相反)。
用 C++设计一个不能被继承的类
将自身构造函数和析构函数声明为private。
什么是纯虚函数
基类中声明的虚函数,仅有声明无实现。要求派生的子类必须定义自身的实现方法,达到多态效果。
构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因[20]
构造函数不能声明为虚函数
因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等
虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了
析构函数最好声明为虚函数
首先析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。
析构函数的作用
对象消亡时,自动被调用,用来释放对象占用的空间。
栈和堆的区别,什么时候必须使用堆
栈:为函数分配的一块内存,函数内部声明的所有局部变量都将占用栈内存。函数执行完毕后,占用的栈会被销毁回收,内部定义的变量均会销毁。效率很高,但是分配的内存容量有限。
堆:程序中未使用的内存,在程序运行时可用于动态分配内存。由程序员自己维护申请和回收,若使用完毕未回收会导致内存泄漏。
栈溢出(stack overflow)的原因以及解决方法[21]
栈溢出原因
① 局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出。
② 递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。
③ 指针或数组越界。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。
解决方法
增大栈空间; 变量过大时,改用动态分配,使用堆(heap)而不是栈(stack)。
编码实现某一变量某位清 0 或置 1
// 将第index为置1. index: 0~7unsigned char set_bit(unsigned char value, int index){ return value | (1 << index);}//将第index位清0, index: 0~7unsigned char clear_bit(unsigned char value, int index){ return value & (~(1 << index));}
头文件<>和""的区别
<>: 表示引用标准库头文件,编译器会从系统默认库环境路径查找。
"": 一般表示用户自己定义使用的头文件,编译器默认会优先从当前路径中寻找;若未找到,再从系统默认库环境路径中去查找。
注:linux下C和C++默认库环境路径:/usr/include
静态绑定和动态绑定的介绍
把一个方法与其所在的类/对象关联起来叫做方法的绑定。[22]
「静态类型」:对象在声明时采用的类型,在编译期既已确定。
「动态类型」:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的。
「静态绑定」:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期。
「动态绑定」:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期。
非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)
引用是否能实现动态绑定,为什么引用可以实现
可以。引用在创建的时候必须初始化,在访问虚函数时,编译器会根据其所绑定的对象类型决定要调用哪个函数。注意只能调用虚函数。
什么情况下会调用拷贝构造函数(三种情况)
拷贝构造函数从来不显示调用,而是由编译器隐式地调用。
① 用类的一个对象去初始化另一个对象时;
② 当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用;
③ 当函数的返回值是类的对象或引用时。
extern "C"作用
extern "C"
的 主要 作用 就 是 为了能够正确实现C++代码调用其他C语言代码。加上extern "C"
后,编译器会按照C语言语法进行编译。
typedef和define的区别
typedef和define都是替一个对象取一个别名,以此增强程序的可读性。[23]
①作用阶段不同
「typedef」: 编译阶段有效, 有类型检查的功能。
「define」: 预处理阶段有效, 编译前, 只进行简单而机械的字符串替换, 不进行任何检查。
② 功能不同
「typedef」: 用来定义类型(内部或自定义类型)的别名, 起到使类型易于记忆的功能。
「define」: 不止可以为类型取别名, 还可以定义常量, 变量, 编译开关等。
③ 作用域不同[24]
「typedef」: 与变量生命期类似。放在函数外,作用域从定义开始到文件尾;若放在函数内,定义域从定义处到该函数结尾。
「define」没有作用域的限制,从定义开始到文件结尾。
//源自 typedef …//此处开始到文件结尾 #define …//此处开始到文件结尾 int negate(int num) { … typedef …//此处开始到该函数结束。注意,该函数内,此定义前,也不能用 #define …//此处开始到文件结尾 … } typedef …//此处开始到文件结尾 #define …//此处开始到文件结尾 void show() { typedef …//此处开始到该函数结束。 #define …//此处开始到文件结尾 }
④ 对指针的操作:
二者修饰指针类型时, 作用不同。
Typedef int * pint; #define PINT int * const pint p;//p不可更改,p指向的内容可以更改,相当于 int * const p; const PINT p;//p可以更改,p指向的内容不能更改,相当于 const int *p;或 int const *p; pint s1, s2; //s1和s2都是int型指针 PINT s3, s4; //相当于int * s3,s4;只有一个是指针。
友元函数和友元类[25]
友元函数
类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
友元类
在类A中,用friend
修饰另一个已经定义的类B,类B就属于类A的友元类,类B中的所有成员函数都是类A的友元函数,类B可以访问类A的所有成员,包括 public
、protected
、private
属性的。
提高c++性能,你用过哪些方式去提升[26]
空间足够时,可以将经常需要读取的资源,缓存在内存中。
尽量减少大内存对象的构造与析构,考虑缓存暂时不用的对象,等待后续继续使用。
尽量使用C++11的右值语义,减少临时对象的构造。
简单的功能函数可以使用内联。少用继承,多用组合,尽量减少继承层级。
在循环遍历时,优化判断条件,减少循环次数。
优化线程或进程的同步方式,能用原子操作的就不用锁。能应用层同步的就不用内核对象同步。
优化堆内存的使用,如果有内存频繁的申请与释放,可以考虑内存池。
优化线程的使用,节省系统资源与切换造成的性能损耗,线程使用频繁的可以考虑线程池。
尽量使用事件通知,谨慎使用轮循或者sleep函数。
界面开发中,耗时的业务代码不要放在UI线程中执行,使用单独的线程去异步处理耗时业务,提高界面响应速度。
经常重构、优化代码结构。优化算法或者架构,从设计层面进行性能的优化。
二、计算机网络编程
select、poll、epoll的区别[27]
select、poll、epoll是多路复用主要用到的三种技术,主要区别如下:
① 支持一个进程所能打开的最大连接数
「select」
单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
「poll」
poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。
「epoll」
虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。
② FD剧增后带来的IO效率问题
「select」
因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
「poll」
同上。
「epoll」
因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。
③ 消息传递方式
「select」
内核需要将消息传递到用户空间,都需要内核拷贝动作。
「poll」
同上。
「epoll」
epoll通过内核和用户空间共享一块内存来实现的。
TCP和UDP的区别[28]
TCP是面向连接的,UDP是无连接的
TCP是可靠的,UDP是不可靠的
TCP是面向字节流的,UDP是面向数据报文的
TCP只支持点对点通信,UDP支持一对一,一对多,多对多
TCP报文首部20个字节,UDP首部8个字节
TCP有拥塞控制机制,UDP没有
TCP协议下双方发送接受缓冲区都有,UDP并无实际意义上的发送缓冲区,但是存在接受缓冲区
TCP三次握手[29]
① 第一次握手
客户端给服务器发送一个SYN段(在 TCP 标头中 SYN 位字段为 1 的 TCP/IP 数据包), 该段中也包含客户端的初始序列号(Sequence number = J)。
② 第二次握手
服务器返回客户端 SYN +ACK 段(在 TCP 标头中SYN和ACK位字段都为 1 的 TCP/IP 数据包), 该段中包含服务器的初始序列号(Sequence number = K);同时使 Acknowledgment number = J + 1来表示确认已收到客户端的 SYN段(Sequence number = J)。
③ 第三次握手
客户端给服务器响应一个ACK段(在 TCP 标头中 ACK 位字段为 1 的 TCP/IP 数据包), 该段中使 Acknowledgment number = K + 1来表示确认已收到服务器的 SYN段(Sequence number = K)。
三次握手的原因[30]
为了实现可靠数据传输,TCP 协议的通信双方,都必须维护一个序列号,以标识发送出去的数据包中,哪些是已经被对方收到的。三次握手的过程即是通信双方相互告知序列号起始值,并确认对方已经收到了序列号起始值的必经步骤。
TCP四次挥手[31]
① 第一次挥手:Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入FIN_WAIT_1状态,这表示Client端没有数据要发送给Server端了。
② 第二次挥手:Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段,ack设为seq加1,Client端进入FIN_WAIT_2状态,Server端告诉Client端,我确认并同意你的关闭请求。
③ 第三次挥手:Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入LAST_ACK状态。
④ 第四次挥手 :Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段,然后Client端进入TIME_WAIT状态。Server端收到Client端的ACK报文段后,关闭连接。此时,Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,Client端也关闭连接。
四次挥手的原因[32]
其实是客户端和服务端的两次挥手,也就是客户端和服务端分别释放连接的过程。客户端在发送完最后一次确认之后,还要等待2MSL的时间。有两个原因: 一是为了让B能够按照正常步骤进入CLOSED状态,二是为了防止已经失效的请求连接报文出现在下次连接中。
① 由于客户端最后一个ACK可能会丢失,这样B就无法正常进入CLOSED状态。于是B会重传请求释放的报文,而此时A如果已经关闭了,那就收不到B的重传请求,就会导致B不能正常释放。而如果A还在等待时间内,就会收到B的重传,然后进行应答,这样B就可以进入CLOSED状态了。
② 在这2MSL等待时间里面,本次连接的所有的报文都已经从网络中消失,从而不会出现在下次连接中。
编写socket套接字的步骤
服务器端程序的编写步骤
① 调用socket()函数创建一个用于通信的套接字。
② 第二步:给已经创建的套接字绑定一个端口号,这一般通过设置网络套接口地址和调用bind()函数来实现。
③ 调用listen()函数使套接字成为一个监听套接字。
④ 调用accept()函数来接受客户端的连接,就可以和客户端通信了。
⑤ 处理客户端的连接请求。
⑥ 终止连接。
客户端程序编写步骤
① 调用socket()函数创建一个用于通信的套接字。
② 通过设置套接字地址结构,说明客户端与之通信的服务器的IP地址和端口号。
③ 调用connect()函数来建立与服务器的连接。
④ 调用读写函数发送或者接收数据。
⑤ 终止连接。
三、操作系统
如何不用sizeof()
判断系统是16位还是32位?
可通过内存地址地址长度判断,定义一个指针变量,通过打印其地址即可判断。
进程和线程间的通信方式
匿名管道(pipe)、高级管道(popen)、有名管道(fifo)、消息队列、信号量、信号(sinal)、共享内存、套接字(socket)。
线程安全和线程不安全
线程安全指支持多线程访问,多线程访问时不会导致共享数据污染。反之,为线程不安全。
死锁产生的原因和死锁的条件[33]
死锁的定义
两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
死锁的产生原因
通常是源于多个进程对资源的争夺,不仅对不可抢占资源进行争夺或引起死锁,而且对可消耗资源进行争夺也会引起死锁。总结如下:
① 系统资源不足;
② 进程运行推进的顺序不当;
③ 资源分配不当;
死锁产生的必要条件
① 互斥条件:
进程在运行中对资源进行排他性使用,即一个资源仅能被一个进程使用,此时其他进程请求资源时,只能等待其释放。
② 请求与保持条件:
某进程已经保持了一个资源,但又请求另一个资源,若该资源被其他进程占有,此时请求阻塞,且对已经占有的资源不释放;
③ 不可抢占条件:
进程获得的资源在未使用完时不可被抢占,只能在进程使用完时自己释放;
④ 循环等待条件
发生死锁时,必然存在这样一个循环,一个进程p1等待p2占有的资源进程p2等待p3占有的资源...进程pn等待p1占有的资源。
死锁的处理方法
① 预防死锁:事先预防策略,容易实现,通过实现设置限制,破坏产生死锁的四个条件之一。(如对资源采用按序分配策略)
② 避免死锁:事先预防策略,在资源的动态分配过程中,用某些方法防止系统禁图不安全状态。常见的方法有银行家算法。
③ 检测死锁:通过检测机构等及时检测出死锁,采取适当措施,把进程从死锁中解脱。
④ 解除死锁:检测出死锁后,采取措施解决。比如剥夺资源,撤销进程。
这四种方法对死锁的防范逐渐减弱,但对应的是资源利用率的提高。
如何采用单线程处理高并发
采用非阻塞,异步编程的思想。
① IO多路复用技术
② 采用事件驱动模型,基于异步回调来处理事件
线程的状态
运行期、挂起、死亡、正常退出、和线程阻塞。
进程的状态
① 运行(running)态:进程占有处理器正在运行。
② 就绪(ready)态:进程具备运行条件,等待系统分配处理器以便运行。
③ 等待(wait)态:又称为阻塞(blocked)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成。
系统调用brk和mmap[34]
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。
brk是将数据段(.data)的最高地址指针_edata往高地址推;
mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
进程和线程的区别
① 地址空间
线程共享本进程的地址空间,而进程之间是独立的地址空间。
② 资源
线程共享本进程的资源如内存、I/O、cpu等,不利于资源的管理和保护,而进程之间的资源是独立的,能很好的进行资源管理和保护。
③ 健壮性
多进程要比多线程健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。
④ 执行过程
每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口,执行开销大。
但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,执行开销小。
⑤ 可并发性
两者均可并发执行。
⑥ 切换时
进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
⑦ 其他
线程是处理器调度的基本单位,但是进程不是。
工作常用到的Linux命令
find、grep、ps、top、ls(比较简单,不展开)
gdb
参考网上gdb常用的调试手法。
gcc/g++
C和C++的编译工具。
什么是虚拟内存
《操作系统》:虚拟存储技术的基本思想时利用大容量外存来扩充内存,产生一个比有限的实际内存空间大得多的、逻辑的虚拟空间,简称虚存,以便能够有效地支持多道程序系统的实现和大型程序运行的需要,从而增强系统的处理能力。
常见的内存管理机制[35]
简单的分为连续分配管理方式和非连续分配管理方式这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间,比如块式管理。非连续分配管理方式允许一个程序内存分散,比如页式管理、段式管理和段页式管理。
块式管理
远古时代的计算机操作系统的内存管理方式,将内存分为几个固定大小的块,每个块只包含一个进程,如果程序运行需要内存,操作系统就给它分配一块,如果程序运行只需要很小的空间,则分配的这块内存很大一部分就浪费了,这些在每个块中未被利用的空间,我们称为碎片。
页式管理
把主存分为大小相等且固定的一页一页的形式,页比较小,相对于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。
段式管理
页式管理虽然提高了内存利用率,但是其中的页没有任何实际意义,段式管理把主存分为一段一段 ,每一段的空间又比一页的空间小很多。但是段有实际意义,段式管理通过段表对应逻辑地址和物理地址。
段页式管理
段页式管理机制结合了段式管理和页式管理的优点,就是把主存分为若干段,每个段又分为若干页,也就是说段页式管理机制中段和段之间以及段的内部都是离散的。
大端和小端,用C++代码怎么确定
指针法
int JudgeSystem(void) { int a = 1; char *p = (char *)&a; // 如果是小端则返回 1,如果是大端则返回 0 return *p;}
联合体法
int JudgeSystem(void) { union { char c; int i; } un; // 匿名联合体 un un.i = 1; // 如果是小端则返回 1,如果是大端则返回 0 return un.c;}
四、数据结构及算法
几种常见的排序算法[36]
① 冒泡排序
② 选择排序
③ 插入排序
④ 希尔排序
⑤ 快速排序
⑥ 归并排序
⑦ 堆排序
链表的特点和操作
特点
① 采用动态存储分配,不会造成内存浪费和溢出。
② 链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素。
常见操作
① 创建
② 插入
③ 修改
④ 查找
⑤ 删除
常见的查找算法
① 顺序查找
② 二分查找
③ 插值查找
④ 斐波那契查找
⑤ 树表查找
⑥ 分块查找
⑦ 哈希查找
如何判断链表是否为环形[37]
通过快慢指针。慢指针每次走1步,快指针每次走2步。当快慢指针相遇,说明链表为循环链表。
bool IsRingList(SNode* pHead){ bool ret = false; SNode *pSlow = pHead, *pFast = pHead; while(pFast && pFast->next) { pSlow = pSlow->next; pFast = pFast->next->next; if (pSlow == pFast) { return true; } } return ret;}
计算环形链表的长度
通过快慢指针。慢指针每次走1步,快指针每次走2步,两者速度差为1步。当两指针相遇时,快指针比慢指针多走1圈。
int GetRingListLength(SNode* pHead){ int length = 0; SNode *pSlow = pHead, *pFast = pHead; while(pFast && pFast->next) { pSlow = pSlow->next; pFast = pFast->next->next; length++; if (pSlow == pFast) { break; } } if (pSlow != pFast) { length = 0; printf("It is not ring list.n"); } return length;}
查找单链表倒数第n个节点
通过快慢指针,快指针先走n步,随后慢指针与快指针同步走,当快指针到达链表尾部时,慢指针即为倒数第n个节点。