面试嵌入式工程师常问的问题(面试嵌入式都会问那些问题呢)

2024-05-10 00:35:00 来源 : haohaofanwen.com 投稿人 : admin

下面是好好范文网小编收集整理的面试嵌入式工程师常问的问题(面试嵌入式都会问那些问题呢),仅供参考,欢迎大家阅读!

面试嵌入式工程师常问的问题

「目录」

一、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的所有成员,包括 publicprotectedprivate属性的。

提高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个节点。


相关文章

专题分类