嵌入式面试四

10 篇文章 1 订阅

3.HTTP协议格式。

HTTP使用统一资源标识符URI)来传输数据和建立连接。URL(统一资源定位符)是一种特殊种类的URI,包含了用于查找的资源的足够的信息,我们一般常用的就是URL,而一个完整的URL包含下面几部分:

http://www.fishbay.cn:80/mix/76.html?name=kelvin&password=123456#first

1.协议部分

URL的协议部分为http:,表示网页用的是HTTP协议,后面的//为分隔符

2.域名部分

域名是www.fishbay.cn,发送请求时,需要向DNS服务器解析IP。如果为了优化请求,可以直接用IP作为域名部分使用

3.端口部分

域名后面的80表示端口,和域名之间用:分隔,端口不是一个URL的必须的部分。如果端口是80,也可以省略不写

4.虚拟目录部分

从域名的第一个/开始到最后一个/为止,是虚拟目录的部分。其中,虚拟目录也不是URL必须的部分,本例中的虚拟目录是/mix/

5.文件名部分

从域名最后一个/开始到?为止,是文件名部分;如果没有?,则是从域名最后一个/开始到#为止,是文件名部分;如果没有?#,那么就从域名的最后一个/从开始到结束,都是文件名部分。本例中的文件名是76.html,文件名也不是一个URL的必须部分,如果没有文件名,则使用默认文件名

6.锚部分

#开始到最后,都是锚部分。本部分的锚部分是first,锚也不是一个URL必须的部分

7.参数部分

?开始到#为止之间的部分是参数部分,又称为搜索部分、查询部分。本例中的参数是name=kelvin&password=123456,如果有多个参数,各个参数之间用&作为分隔符。

4.TCP/IP协议族中应用层有哪些具体的应用。

HTTP:超文本传输协议,常用于浏览器/Web服务器

Telnet:远程登录协议,常用于终端远程登陆主机

FTP:文本传输协议,常用于文件共享

SMTP:邮件传输协议,常用于邮件发送

5.为什么要用路由表。

路由的原理很简单。市场上所有的路由器,背后都有很多网口,要接入多根网线。路由器内部有一张路由表,规定了 A 段 IP 地址走出口一,B 段地址走出口二,......通过这套"指路牌",实现了数据包的转发。

本机的路由表注明了不同 IP 目的地的数据包,要发送到哪一个网口(interface)。

IP 协议只是一个地址协议,并不保证数据包的完整。如果路由器丢包(比如缓存满了,新进来的数据包就会丢失),就需要发现丢了哪一个包,以及如何重新发送这个包。这就要依靠 TCP 协议。

简单说,TCP 协议的作用是,保证数据通信的完整性和可靠性,防止丢包。

6.当在地址栏输入www.google.com回车之后,描述一下数据到目的主机整个流程。

本机参数

  • 本机的IP地址:192.168.1.100;
  • 子网掩码:255.255.255.0; 
  • 网关的IP地址:192.168.1.1; 
  • DNS的IP地址:8.8.8.8。

 这意味着,浏览器要向Google发送一个网页请求的数据包。

DNS协议

       我们知道,发送数据包,必须要知道对方的IP地址。但是,现在,我们只知道网址www.google.com,不知道它的IP地址。DNS协议可以帮助我们,将这个网址转换成IP地址。已知DNS服务器为8.8.8.8,于是我们向这个地址发送一个DNS数据包(53端口)。

  然后,DNS服务器做出响应,告诉我们Google的IP地址是172.194.72.105。于是,我们知道了对方的IP地址。

子网掩码

       接下来,我们要判断,这个IP地址是不是在同一个子网络,这就要用到子网掩码。已知子网掩码是255.255.255.0,本机用它对自己的IP地址192.168.1.100,做一个二进制的AND运算(两个数位都为1,结果为1,否则为0),计算结果为192.168.1.0;然后对Google的IP地址172.194.72.105也做一个AND运算,计算结果为172.194.72.0。这两个结果不相等,所以结论是,Google与本机不在同一个子网络。因此,我们要向Google发送数据包,必须通过网关192.168.1.1转发,也就是说,接收方的MAC地址将是网关的MAC地址。

应用层协议

浏览网页用的是HTTP协议,它的整个数据包构造是这样的:

HTTP部分的内容,类似于下面这样:

123456789GET / HTTP/1.1Host: [url=http://www.google.com]www.google.com[/url]Connection: keep-aliveUser-Agent: Mozilla/5.0 (Windows NT 6.1) ......Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Encoding: gzip,deflate,sdchAccept-Language: zh-CN,zh;q=0.8Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3Cookie: ... ...

我们假定这个部分的长度为4960字节,它会被嵌在TCP数据包之中。

TCP协议

      TCP数据包需要设置端口,接收方(Google)的HTTP端口默认是80,发送方(本机)的端口是一个随机生成的1024-65535之间的整数,假定为51775。TCP数据包的标头长度为20字节,加上嵌入HTTP的数据包,总长度变为4980字节。

IP协议

       然后,TCP数据包再嵌入IP数据包。IP数据包需要设置双方的IP地址,这是已知的,发送方是192.168.1.100(本机),接收方是172.194.72.105(Google)。IP数据包的标头长度为20字节,加上嵌入的TCP数据包,总长度变为5000字节。

以太网协议

        最后,IP数据包嵌入以太网数据包。以太网数据包需要设置双方的MAC地址,发送方为本机的网卡MAC地址,接收方为网关192.168.1.1的MAC地址(通过ARP协议得到)。

      以太网数据包的数据部分,最大长度为1500字节,而现在的IP数据包长度为5000字节。因此,IP数据包必须分割成四个包。因为每个包都有自己的IP标头(20字节),所以四个包的IP数据包的长度分别为1500、1500、1500、560。

 服务器端响应

经过多个网关的转发,Google的服务器172.194.72.105,收到了这四个以太网数据包。根据IP标头的序号,Google将四个包拼起来,取出完整的TCP数据包,然后读出里面的"HTTP请求",接着做出"HTTP响应",再用TCP协议发回来。本机收到HTTP响应以后,就可以将网页显示出来,完成一次网络通信。
 

9.网络设备和网络层次

 10.数组的本质分析

1.数组的基本概念 

数组是相同类型的变量的有序集合 

数组在—片连续的内存空间中存储元素 

数组元素的个数可以显示或隐式指定 

2.数组地址与数组名

数组名代表数组首元素的地址 

数组的地址需要用取地址符&才能得到 

数组首元素的地址值与数组的地址值相同 

数组首元素的地址与数组的地址是两个不同的概念

 3.数组名与指针

数组名可以看做—个指针常量(int* const p)

数组名“指向”的是内存中数组首元素的起始位置 

数组名不包含数组的长度信息 

在表达式中数组名只能作为右值使用 

只有在下列场合中数组名不能看做指针常量

                -数组名作为sizeof操作符的参数 

                -数组名作为&运算符的参数

3.指针和数组分析

1.指针的运算

指针是一种特殊的变量,与整数的运算规则为 : 

p + n;  ↔  (unsigned int)p + n * sizeof( *p ); 

结论:当指针p指向—个同类型的数组的元素时: p+1 将指向当前元素的下—个元素;

           p -1将指向当前元素的上—个元素。 

指针之间只支持减法运算 , 参与减法运算的指针类型必须相同 ,运算规则为 :

p1 - p2;  ↔ ( (unsigned int)p1 - (unsigned int)p2 )  /  sizeof(type); 

注意: 

     1. 只有当两个指针指向同—个数组中的元素时,指针相减才减有意义,其意义为指针所指元素的下标差 

     2. 当两个指针指向的元素不在同一个数组中时,结果未定义 

指针的比较

                 - 指针也可以进行关系运算(<, <=,>,>=) 

                 - 指针关系运算的前提是同时指向同—个数组中的元素 

                 - 任意两个指针之间的比较运算(==,!=)无限制 

                 - 参与比较运算的指针类型必须相同 

2.数组的访问方式

数组名可以当作指针常量使用, 指针也可以当作数组名来使用

以下标的形式访问数组中的元素  a[0],a[1]

以指针的形式访问数组中的元素  *(a+0),*(a+1)

下标形式 vs 指针形式

    - 指针以固定增量在数组中移动时,效率高于下标形式 

    - 指针增量为1且硬件具有硬件增量模型时,效率更高 

下标形式与指针形式的转换 

 a[n]   ⇿    *(a + n)   ⇿    *(n + a)    ⇿    n[a]   

注意: 

          现代编译器的生成代码优化率已大大提高,在固定增量时,下标形式的效率已经和指针形式相当;

          但从可读性和代码维护的角度来看,下标形式更优。

 虽然指针也可以当作数组名来使用,但数组和指针不同 

3.a和&a的区别 

a为数组首元素的地址 

&a为整个数组的地址 

a和&a的区别在于指针运算 

4.数组参数  

数组作为函数参数时,编译器将其编译成对应的指针

 结论: 一般情况下,当定义的函数中有数组参数时,需要定义另—个参数来标示数组的大小。

5.C语言中的结构体传参

C语言中结构体传参时建议使用指针,可以少了大量的内存复制

指针传参时是将结构体变量地址压栈,通过地址偏移直接获得该变量的各个成员,而非指针传参,会有大量的内存拷贝

 11.字符串

1、字符串的概念

字符串是有序字符的集合 

字符串是程序中的基本元素之一 

C语言中没有字符串的概念 

     - C语言中通过特殊的字符数组模拟字符串 

     - C语言中的字符串是以 '\0' 结尾的字符数组

2、字符数组与字符串 

在C语言中,双引号引用的单个或多个字符是一种特殊的字面量 

              -存储于程序的全局只读存储区 

              -本质为字符数组,编译器自动在结尾加上'\0'字符 

下面那些是字符串的定义?

#include <stdio.h>  
  
int main()  
{  
    char ca[] = {'H', 'e', 'l', 'l', 'o'};  //普通字符数组
    char sa[] = {'W', 'o', 'r', 'l', 'd', '\0'};  //存储于全局只读存储区的\0结尾的字符数组 - 字符串
    char ss[] = "Hello world!";  //存储于全局只读存储区的\0结尾的字符数组 - 字符串
    char* str = "Hello world!";  //存储于全局只读存储区的\0结尾的字符数组 - 字符串
      
    printf("%s\n", ca);  
    printf("%s\n", sa);  
    printf("%s\n", ss);  
    printf("%s\n", str);  
      
    return 0;  
}  

 对于ca的打印会出现不期望的内容(没有\0以字符串打印,一直往后打印)

  • 字符串字面量的本质是一个数组 
  • 字符串字面量可以看作指针常量
  • 字符串字面量中的字符不可改变 
  • 字符串字面量至少包含—个字符(\0) 

3.字符串字面量 

"Hello World ! "是—个无名的字符数组 

#include <stdio.h>  
  
int main()  
{  
    char b = "abc"[0];  
    char c = *("123" + 1);  
    char t = *"";  
      
    printf("%c\n", b);  
    printf("%c\n", c);  
    printf("%d\n", t);  //\0对应的ascii为0
      
    printf("%s\n", "Hello");  
    printf("%p\n", "World");  
      
    return 0;  
}  

4. 字符串的长度
  • 字符串的长度就是字符串所包含字符的个数 
  • 字符串长度指的是第—个'\0'字符前出现的字符个数 
  • 通过'\0'结束符来确定字符串的长度 
  • 函数strlen用于返回字符串的长度
#include <stdio.h>  
#include <string.h>  
  
int main()  
{  
    char s[] = "Hello\0world";  //sizeof(s) = 12 需要加上最后编译器加上的\0
    int i = 0;  
      
    for(i=0; i<sizeof(s)/sizeof(char); i++)  
    {  
        printf("%c\n", s[i]);  //h e l l o \0 w o r l d \0
    }  
      
    printf("%s\n", s);  //Hello
  
    printf( "%d\n", strlen(s) );  //5
    printf( "%d\n", strlen("123") );  //3
   
      
    return 0;  
}  

  1. C语言中通过字符数组模拟字符串 
  2. C语言中的字符串使用'\0'作为结束符 
  3. 字符串字面量的本质为字符数组 
  4. 字符串相关函数都依赖于结束符'\0'

5.问题分析一

#include <stdio.h>  
#include <string.h>  
  
int main()  
{  
    #define STR "Hello, \0D.T.Software\0"  
      
    char* src = STR;  
    char buf[255] = {0};  
      
    snprintf(buf, sizeof(buf), src);  
      
    printf("strlen(STR) = %d\n", strlen(STR));  //7
    printf("sizeof(STR) = %d\n", sizeof(STR));  //22,即便是\0结尾也会编译器也会再加上\0
      
    printf("strlen(src) = %d\n", strlen(src));  //7
    printf("sizeof(src) = %d\n", sizeof(src));  //4
      
    printf("strlen(buf) = %d\n", strlen(buf));  //7
    printf("sizeof(buf) = %d\n", sizeof(buf));  //255
      
    printf("src = %s\n", src);  //Hello
    printf("buf = %s\n", buf);  //Hello
      
    return 0;  
}  

分析:

  • 字符串相关的函数均以第—个出现的 '\0'作为结束符 
  • 编译器总是会在字符串字面量的未尾添加'\0' 
  •  字符串字面量的本质为数组

 1.c和c++的区别

1.C与C++的关系

            C++继承了所有的C特性

            C++在C的基础上提供了更多的语法和特性

            C++的设计目标是运行效率与开发效率的统一

2.C++更强调语言的实用性,所有的变量都可以在需要时候再定义

3.register关键字的加强

     - register关键字请求编译器将局部变量存储于寄存器中

     - 在C++中依然支持register关键字,C++编译器有自己的优化方式

                   ★ C语言中无法获取register变量的地址,C++中可以获得register变量的地址

    - C++编译器发现程序中需要取register变量的地址时,register对变量的声明变得无效

4.全局变量的加强

在C语言中,重复定义多个同名的全局变量是合法的。C语言中多个同名的全局变量最终会被链接到全局数据区的同一个地址空间上

在C++中,不允许定义多个同名的全局变量

5.类型加强

C++中所有的标识符都必须显示的声明类型

C语言中的默认类型在C++中是不合法的           

6.truct关键字的加强

C语言中的struct定义了一组变量的集合。C语言中struct定义的标识符并不是一种新的类型

C++中的struct用于定义一个全新的类型

7.与C语言不同,C++中的const不是只读变量。C++中的const是一个真正意义上的常量

8.bool

9.强制类型转换

2.c语言中的内存分空间

 

3.strcpy

/*
C语言标准库函数strcpy的一种典型的工业级的最简实现 
返回值: 
返回目标串的地址。 
对于出现异常的情况ANSI-C99标准并未定义,故由实现者决定返回值,通常为NULL。 
参数: 
目标串 
    dest
源串 
    str
*/
char* strcpy(char* dst, const char* str)
{
    //1.断言
    assert(dst != NULL && str != NULL);
 
    //2.使用ret指向dst字符串
    char* ret = dst;
 
    //3.复制
    while(*str != '\0')
    {
        *dst = *str;
        src++;
        dst++; 
    }
 
    *dst = '\0';
 
    return ret;
}

 

 4.僵尸进程

  1. 子进程先于父进程结束。子进程结束后父进程此时并不一定立即就能帮子进程“收尸”,在这一段(子进程已经结束且父进程尚未帮其收尸)子进程就被成为僵尸进程。
  2. 子进程除task_struct和栈外其余内存空间皆已清理
  3. 父进程可以使用wait或waitpid以显式回收子进程的剩余待回收内存资源并且获取子进程退出状态。
  4. 父进程也可以不使用wait或者waitpid回收子进程,此时父进程结束时一样会回收子进程的剩余待回收内存资源。(这样设计是为了防止父进程忘记显式调用wait/waitpid来回收子进程从而造成内存泄漏)

5.用递归写出左右子树的对调过程

void exchange(BTree* rt)
{
    BTree *temp = NULL;

    if(rt == NULL)
        return;

    if(rt->left == NULL && rt->right == NULL)
        return;
    else
    {
        temp = rt->left;
        rt->left = rt->child;
        rt->right = temp;
    }

    if(rt->left)
        exchange(rt->right);
    if(rt->right)
        exchange(rt->left);
}

 

6.拷贝构造函数 

  • 参数为const class_name&的构造函数
  • 当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,简单的进行成员变量的值复制
  • 兼容C语言的初始化方式
  • 初始化行为能够符合预期的逻辑

7.深浅拷贝 

比较

                                     优点                                                                                              缺点

浅拷贝只有一份数据,节省空间。因为多个指针指向同一个空间,容易引发同一内存多次释放的问题。
深拷贝每个指针指向不同地址,没有同一内存多次释放的问题。存在多份相同数据,浪费空间。

8.如何判断一个链表有环

class Solution {
public:
    bool hasCycle(ListNode *head) 
	{
        if(NULL == head)
			return false;

		ListNode* fast = head;
		ListNode* slow = head;
		
		while(1)
		{
			fast = fast->next ? fast->next : NULL;
			if(NULL == fast)
				break;

			fast = fast->next ? fast->next : NULL;

			if(NULL == fast)
				break;

			slow = slow->next ? slow->next : NULL;

			if(fast == slow)
				break;
		}
		if(NULL == fast)
			return false;

		return true;
		
    }
	
};

 9.排序算法

选择排序每次选择未排序元素中的最小元素 时间复杂度O(n^2) 不稳定

插入排序每次将第i个元素插入前面i-1个已排元素中 时间复杂度O(n^2) 稳定

冒泡排序每次从后向前将较小的元素交互到位 时间复杂度O(n^2) 稳定

希尔排序通过分组的方式进行多次插入排序 时间复杂度O(n ^ 3/2 ) 不稳定 

归并排序需要额外的辅助空间才能完成 空间复杂度为O(n) 时间复杂度 O(n*logn) 稳定

快速排序通过递归的方式对排序问题进行划分 时间复杂度O(n*logn) 不稳定

10.fork()

fork函数调用一次会返回2次,返回值等于0的就是子进程,而返回值大于0的就是父进程

11.select、poll、epoll

1.select==>时间复杂度O(n)

它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

2.poll==>时间复杂度O(n)

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

3.epoll==>时间复杂度O(1)

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。  

epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现

select:

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

      一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

       当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

poll:

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。                   

2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

epoll:

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无 论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者 遇到EAGAIN错误。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

epoll为什么要有EPOLLET触发模式?

如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLLET这种边沿触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符

epoll的优点:

1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
select、poll、epoll 区别总结:

1、支持一个进程所能打开的最大连接数

select

单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。

poll

poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的

epoll

虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接

2、FD剧增后带来的IO效率问题

select

因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。

poll

同上

epoll

因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

3、 消息传递方式

select

内核需要将消息传递到用户空间,都需要内核拷贝动作

poll

同上

epoll

epoll通过内核和用户空间共享一块内存来实现的。

总结:

综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。

1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善 

12.重写memcpy

/*
memcpy内存拷贝函数
原型:void * memcpy(void* dest, const void* src,unsigned int count);
功能:由src所指内存区域复制count个字节到dest所指内存区域。
说明:src和dest所指内存区域不能重叠,函数返回指向dest的指针。
*/
void memcpy(void* dest, const void* src, unsigned int count)
{
    if(dest == NULL && src == NULL)
        return NULL;
    
    unsigned char* d = (unsigned char*)dest;
    unsigned char* s = (unsigned char*)src;
    
    while(count-- > 0)
    {
        *d++ = *s++;
    }
    
    return dest;
}

13.linux下检查内存的命令

1.top

top命令可实时查看系统的内存使用情况,有多少物理内存已经使用了,多少物理内存空闲,多少缓存,CPU的使用情况,每个进程的进程号是多少,占用的虚拟内存是多少等信息。具体使用方式视嵌入式linux系统的裁剪情况不同而不同。

2.cat /proc/meminfo

 /proc/meminfo文件列出了你想了解的所有的内存使用情况的总的概览。

 

17.回文数

思路 :

通过取整取余操作获取整数中对应的数字进行比较。

1221 

  • 通过计算 1221 / 1000, 得首位1
  • 通过计算 1221 % 10, 可得末位1
  • 进行比较
  • 再将 22 取出来继续比较
bool isPalindrome(int x)
{
    //1.排除负数
    if(x < 0)
        return false;
    
    int div = 1;
    
    while(x / div >= 10)
        div *= 10;//2.eg 1221 div计算之后为1000
    
    while(x > 0)
    {
        int left = x / div;//3.得到最高位1
        int right = x % 10;//4.得到最低位1

        if(left != right)//5.判断
            return false;
        x = (x % div) / 10;//6.把1221的最高位和最低位去掉
        div /= 100;
    }    
    return true;    
} 

21.c++默认函数

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 赋值运算符的重载函数

22.多进程与多线程

一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

对资源的管理和保护要求高,不限制开销和效率时,使用多进程。

要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。

24.两个链表的第一个公共结点

 双指针法:首先遍历两个数组记录其长度,求差,然后移动指针使其在同一个起跑线上,同时移动指针,如果两个指针相等则说明,找到了公共交点

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(headA == NULL || headB == NULL)
            return NULL;
        
        ListNode *p = headA;
        ListNode *q = headB;
        
        int lenA = 0;
        int lenB = 0;
        
        while(p->next != NULL)
        {
            p = p->next;
            lenA++;
        }
        
        while(q->next != NULL)
        {
            q = q->next;
            lenB++;
        }
        
        if(q != p) return NULL;//若最后一个结点都不相同则肯定没有相交
        
        int x = abs(lenA - lenB);
        
        if(lenA > lenB)
        {
            for(int i=0; i<x; i++)
            {
                headA = headA->next;
            }
        }
        else
        {
            for(int i=0; i<x; i++)
            {
                headB = headB->next;
            }
        }
        while(headA != headB)
        {
            headA = headA->next;
            headB = headB->next;
        }

        return headA;
    }
};

 

26.volatile

volatile “易变的”

volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据。

28.堆栈溢出

堆栈溢出就是不顾堆栈中分配的局部数据块大小,向该数据块写入了过多的数据,导致数据越界,结果覆盖了老的堆栈数据。 

29.用C语言定义一个结构体,实现面向对象编程

typedef strct c
{
    char chr;
    char color;
    void (*put)(struct c*, int,int );
    
}ch;

31.删除链表的倒数第K个

给定一个链表: 1->2->3->4->5, 和 n = 2.

当删除了倒数第二个节点后,链表变为 1->2->3->5.

典型的双指针应用:设置快慢指针fast和slow,先让fast走n步,然后两个指针一起前进。当fast->next == nullptr时,slow->next即为倒数第n个节点。需要注意,当删除第一个节点时的特殊情况。

ListNode* removeNthFromEnd(ListNode*, int n)
{
    if(head == NULL || n == 0)
        return head;
    ListNode* fast = head, *slow = head;
    while(n--)
        fast = fast->next;
    if(fast == NULL)
        return head->next;
    while(fast->next)
    {
        fast = fast->next;
        slow = slow->next;
    }
    slow->next = slow->next->next;
    return head;
}

 

32.atoi

int myAtoi(string str)
{
    //1.将字符串转化为字符数组
    const char* ch = str.c_str();
    int len = str.length(), index = 0, sign = 1;
    
    //2.这里不能为int,需要long类型,目的是为了检查结果是否溢出
    long sum = 0;
    
    //3.去掉开头空白部分
    while(index < len && ch[index] == ' ')
        index++;
    
    //4.判断是否为符号位,当为+时,sign=1,当为-时,sign=-1;
    if(index < len && (ch[index] == '+') || (ch[index] == '-'))
    {
        sign = (ch[index] == '+') ? 1 : -1;
        index++;
    }
    while(index < len)
    {
        //5.判断是否为数字
        if(ch[index] >= '0' && ch[index] <= '9')
        {
            sum = sum*10 + ch[index] - '0';
            
            if(sign == 1 && sum > INT_MAX)
            {
                return INT_MAX;
            }
            else if(sign == -1 && (-1 * sum) < INT_MIN)
            {
                return INT_MIN;
            }
            index++;
        }
        else
        {
            break;
        }
    }
    return sum * sign;
}

 

33.x是否是2得整数幂

使用&运算判断一个数是否是2的幂次方

2 & 1 0010 & 0001 = 0 

4 & 3 0100 & 0011 = 0

8 & 7 1000 & 0111 = 0

....

如果n是2的幂次方,则n&(n-1) = 0

int Fun3(int data)
{
    if(data & (data - 1))
        printf("%d不是2的幂次方!\n",data);
    else
        printf("%d是2的幂次方!\n",data);
}

34.判断数字二进制表示中1的个数

eg:4 & 3 0100 & 0011     3 & 2 0011 & 0010

                                          2 & 1 0010 & 0001    

使用 & 运算判断二进制中1的个数

int Fun3(int data)
{
    int count = 0;
    
    while(data)
    {
        data = data & (data - 1);
        count++;     
    }
    return count;
}

 

35. 宏

1.#

在一个宏中的参数前面使用一个#,预处理器会把这个参数转换为一个字符数组。

简化理解:#是“字符串化”的意思,出现在宏定义中的#是把跟在后面的参数转换成一个字符串

2.##

“##”是一种分隔连接方式,它的作用是先分隔,然后进行强制连接。

 36.offset_of

#define offset_of(TYPE, MEMBER) ((size_t) &((TYPE* )0)->MEMBER)

用于计算TYPE结构体中MEMBER成员的偏移位置

编译器做了什么?

  • 编译器清楚的知道结构体成员变量的偏移位置
  • 通过结构体变量首地址偏移量定位成员变量
  • &((TYPE*)0)->MEMBER 根据结构体的首地址和相应成员的偏移量获得成员的具体地址,这里我们将0转化为TYPE类型的指针最终获得绝对地址和偏移地址是一样的
  • 直接使用0地址不会导致程序崩溃吗?不会编译器只是计算成员地址,并没有实际去取成员的值,也没有访问内存只是做了一些计算
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值