C/C++

函数

请写个函数在main函数执行前先运行

attribute可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。

gnu对于函数属性主要设置的关键字如下:

        alias:      设置函数别名。
        aligned:    设置函数对齐方式。
        always_inline/gnu_inline: 
                    函数是否是内联函数。
        constructor/destructor:
                    主函数执行之前、之后执行的函数。
        format:
                    指定变参函数的格式输入字符串所在函数位置以及对应格式输出的位置。
        noreturn:
                    指定这个函数没有返回值。
                    请注意,这里的没有返回值,并不是返回值是void。而是像_exit/exit/abord那样
                    执行完函数之后进程就结束的函数。
        weak:指定函数属性为弱属性,而不是全局属性,一旦全局函数名称和指定的函数名称
             命名有冲突,使用全局函数名称。

完整示例代码如下:

#include <stdio.h>

void before() __attribute__((constructor));
void after() __attribute__((destructor));

void before() {
    printf("this is function %s\n",__func__);
    return;
}

void after(){
    printf("this is function %s\n",__func__);
    return;
}

int main(){
    printf("this is function %s\n",__func__);
    return 0;
}

// 输出结果
// this is function before
// this is function main
// this is function after

为什么析构函数必须是虚函数?

将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。

为什么C++默认的析构函数不是虚函数?

C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

C++中析构函数的作用?

如果构造函数打开了一个文件,最后不需要使用时文件就要被关闭。析构函数允许类自动完成类似清理工作,不必调用其他成员函数。

析构函数也是特殊的类成员函数。简单来说,析构函数与构造函数的作用正好相反,它用来完成对象被删除前的一些清理工作,也就是专门的扫尾工作。

静态函数和虚函数的区别?

静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。

重载和覆盖有什么区别?

  1. 覆盖是子类和父类之间的关系,垂直关系;重载同一个类之间方法之间的关系,是水平关系。
  2. 覆盖只能由一个方法或者只能由一对方法产生关系;重载是多个方法之间的关系。
  3. 覆盖是根据对象类型(对象对应存储空间类型)来决定的;而重载关系是根据调用的实参表和形参表来选择方法体的。

虚函数表具体是怎样实现运行时多态的?

原理

虚函数表是一个类的虚函数的地址表,每个对象在创建时,都会有一个指针指向该类虚函数表,每一个类的虚函数表,按照函数声明的顺序,会将函数地址存在虚函数表中,当子类对象重写父类的虚函数的时候,父类的虚函数表中对应的位置会被子类的虚函数地址覆盖。

作用

在用父类的指针调用子类对象成员函数时,虚函数表会指明要调用的具体函数是哪个。

C语言是怎么进行函数调用的?

大多数CPU上的程序实现使用栈来支持函数调用操作,栈被用来传递函数参数、存储返回信息、临时保存寄存器原有的值以备恢复以及用来存储局部变量。

函数调用操作所使用的栈部分叫做栈帧结构,每个函数调用都有属于自己的栈帧结构,栈帧结构由两个指针指定,帧指针(指向起始),栈指针(指向栈顶),函数对大多数数据的访问都是基于帧指针。下面是结构图:

栈指针和帧指针一般都有专门的寄存器,通常使用ebp寄存器作为帧指针,使用esp寄存器做栈指针。

帧指针指向栈帧结构的头,存放着上一个栈帧的头部地址,栈指针指向栈顶。

请你说一说select

  1. select函数原型
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
  1. 文件描述符的数量

单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量;(在linux内核头文件中定义:#define __FD_SETSIZE 1024)

  1. 就绪fd采用轮询的方式扫描

select返回的是int,可以理解为返回的是ready(准备好的)一个或者多个文件描述符,应用程序需要遍历整个文件描述符数组才能发现哪些fd句柄发生了事件,由于select采用轮询的方式扫描文件描述符(不知道那个文件描述符读写数据,所以需要把所有的fd都遍历),文件描述符数量越多,性能越差

  1. 内核 /用户空间内存拷贝

select每次都会改变内核中的句柄数据结构集(fd集合),因而每次调用select都需要从用户空间向内核空间复制所有的句柄数据结构(fd集合),产生巨大的开销

  1. select的触发方式

select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次调用select还是会将这些文件描述符通知进程。

  1. 优点

a. select的可移植性较好,可以跨平台;

b. select可设置的监听时间timeout精度更好,可精确到微秒,而poll为毫秒。

  1. 缺点

a. select支持的文件描述符数量上限为1024,不能根据用户需求进行更改;

b. select每次调用时都要将文件描述符集合从用户态拷贝到内核态,开销较大;

c. select返回的就绪文件描述符集合,需要用户循环遍历所监听的所有文件描述符是否在该集合中,当监听描述符数量很大时效率较低。

请你说说fork,wait,exec函数

父进程产生子进程使用fork拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用写实拷贝机制分配内存,exec函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。fork从父进程返回子进程的pid,从子进程返回0.调用了wait的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回-1。exec执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1。

数组

以下代码表示什么意思?

*(a[1]+1)、*(&a[1][1])、(*(a+1))[1]

第一个: 因为a[1]是第2行的地址,a[1]+1偏移一个单位(得到第2行第2列的地址),然后解引用取值,得到a[1][1]

第二个:[]优先级高,a[1][1]取地址再取值。

第三个:a+1相当于&a[1],所以* (a+1)=a[1],因此*(a+1)[1]=a[1][1]

数组下标可以为负数吗?

可以,因为下标只是给出了一个与当前地址的偏移量而已,只要根据这个偏移量能定位得到目标地址即可。下面给出一个下标为负数的示例:

数组下标取负值的情况:

#include <stdio.h>
int main()
{
    int i:
    int a[5]={0,1,2,3,4};
    int *p=&a[4]
    for(i=-4;i<=0;i++)
    printf("%d %x\n", p[i], &p[i]);
    return O.
}
//输出结果为
//0 b3ecf480
//1 b3ecf484
//2 b3ecf488
//3 b3ecf48c
//4 b3ecf490

从上例可以发现,在C语言中,数组的下标并非不可以为负数,当数组下标为负数时,编译可以通过,而且也可以得到正确的结果,只是它表示的意思却是从当前地址向前寻址.

位操作

如何求解整型数的二进制表示中1的个数?

程序代码如下:

#include <stdio.h>
int func(int x)
{
    int countx = 0;
    while(x)
    {
        countx++;
        x = x&(x-1);
    }
    return countx;
}
int main()
{
    printf("%d\n",func(9999));
    return 0;
}

程序输出的结果为8。

在上例中,函数func()的功能是将x转化为二进制数,然后计算该二进制数中含有的1的个数。首先以9为例来分析,9的二进制表示为1001,8的二进制表示为1000,两者执行&操作之后结果为1000,此时1000再与0111(7的二进制位)执行&操作之后结果为0。

为了理解这个算法的核心,需要理解以下两个操作:

1)当一个数被减1时,它最右边的那个值为1的bit将变为0,同时其右边的所有的bit都会变成1。

2)每次执行x&(x-1)的作用是把ⅹ对应的二进制数中的最后一位1去掉。因此,循环执行这个操作直到ⅹ等于0的时候,循环的次数就是x对应的二进制数中1的个数。

如何求解二进制中0的个数

int CountZeroBit(int num)
{
    int count = 0;

    while (num + 1)
    {
        count++;
        num |= (num + 1);    //算法转换
    }
    return count;
}

int main()
{
    int value = 25;
    int ret = CountZeroBit(value);
    printf("%d的二进制位中0的个数为%d\n",value, ret);
    system("pause");
    return 0;
}

交换两个变量的值,不使用第三个变量。即a=3,b=5,交换之后a=5,b=3;

有两种解法, 一种用算术算法, 一种用^(异或)。

a = a + b;
b = a - b;
a = a - b; 
a = a^b;// 只能对int,char..
b = a^b;
a = a^b;
or
a ^= b ^= a;

给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。

#define BIT3 (0x1<<3) 
static int a; 
void set_bit3(void) 
{ 
    a |= BIT3; 
} 
void clear_bit3(void) 
{ 
    a &= ~BIT3; 
} 

联系作者

关于作者

作者在准备秋招的过程中,凭借这份资料,最后顺利拿到了oppo,小米,兆易创新,全志科技,海康威视等十余家家公司的offer。现将这部分资料分享出来,希望能对大家有帮助!

关于嵌入式软件工程师笔试面试指南

嵌入式软件工程师笔试面试指南,详细分成了简历书写,面试技巧,面经总结,笔试面试八股文总结等四个部分。

其中,八股文又分成了C/C++,数据结构与算法分析,Arm体系与架构,Linux驱动开发,操作系统,网络编程,名企笔试真题等七个部分。

无论是做嵌入式应用层还是嵌入式底层,一定会对你有帮助的(没有帮助来找我领红包)。这些内容均会同步更新到github仓库中(内含PDF版本的获取方式)。

因个人能力有限,可能会有一些错误。大家发现错误,可以在github提交issues,勘误我也会整理出来,第一时间通知大家。

这些内容都是我熬夜整理的,创作不易,大家不要忘了点击「赞」支持下,也算没有白白熬夜,对得起我掉的一根根头发。

github:https://github.com/ZhongYi-LinuxDriverDev/EmbeddedSoftwareEngineerInterview