文章目录

  • 1、 字符串数组打印(指针的步长)
    • 1.1 指针变量+1
    • 1.2 字符串数组的步长
    • 1.3 跨行加⭐⭐⭐⭐⭐
      • 例子[1]
      • 例子[2]
  • 2、大端小端
  • 3、异步IO和同步IO区别
  • 4、变量a的不同定义
  • 5、关于char越界的数值
  • 6、利用移位、与实现模
  • 7、无符号与有符号相加结果为无符号类型
  • 8、实现某一位置0或置1操作,保持其它位不变
  • 9、设置一绝对地址为0x67a9的整型变量的值为0xaa66
  • 10、中断函数中的注意问题
    • [10.1 什么是不可重入函数](https://blog.csdn.net/DP29syM41zyGndVF/article/details/112342763)
    • 10.2 如何写出可重入的函数?⭐⭐⭐⭐⭐
  • 11、malloc内存分配
    • 11.1malloc申请大小问题
    • 11.2 malloc底层实现原理
      • 具体分析
        • 1)brk 是将数据段(.data)的最高地址指针 _edata 往高地址推
        • 2)mmap 是在进程的虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空闲的虚拟内存。
  • 12、变量全置0与全置1
  • 13、你真的了解数组吗?
    • 数组作为形参声明
    • 变长数组
    • 字符串与字符数组
    • 数组名不可以++,指针可以
    • 字符串的地址
  • 14、写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个
  • 15、说明关键字volatile有什么含意,并给出例子
    • 15.1 外围设备的特殊功能寄存器
    • 15.2 在中断服务函数中修改全局变量
    • 15.3 线程之间共享变量(在多线程中修改全局变量)
    • 15.4 volatile的顺序性
  • 16、位反转
  • 17、字符串翻转
  • 18、引用和指针的区别
  • 19、-1,2,7,28,,126请问28和126中间那个数是什么?为什么?
  • 20、一级指针无效传参
    • 解决方法:改用二级指针或者返回char*
  • 21、写出float x 与“零值”比较的if语句
  • 22、修改野指针
  • 23、void* 类型
  • 24、sizeof的返回类型
  • 25、typedef的作用
  • 26、浮点数
  • 27、i++操作
    • 27.1 i++的操作顺序
    • 27.2 可以 &(i++) 吗?
  • 28、什么是左值与右值
    • 28.1 举例:
    • 28.2 i++为什么不能作为左值
  • 29、union、struct结构体对齐总结(超全)
    • 29.1 union对齐
    • 29.2 struct结构体对齐
      • (1)自然规则对齐
      • (2)加入#pragma pack(n)
      • (3)通过指定元素位数
      • (4)柔型数组
        • 柔型数组实例:
      • (5)不进行对齐
  • 30、实现strcpy
    • 30.1 代码实现
    • 30.2 strcpy能把strSrc的内容复制到strDest,为什么还要char *类型的返回值?
    • 30.3 strcpy和strncpy
  • 31、手写memcpy, memcpy与strcpy的区别, memmove
    • restrict关键字
  • C++部分
  • 1、C++异常
    • 代码示例:
    • 不可以滥用异常,销毁数据会造成性能消耗!
  • 2、引用
  • 2.1 什么是引用?
    • 2.2 引用 VS 指针
    • 2.3 函数传参用到引用
    • 2.4 引用的优点和需要遵守的规则
  • 3、 C++函数中的默认值
  • 4、register存储类
  • 5、内联函数inline
  • 6、const和指针
    • 6.1 char * const cp;
    • 6.2 const char * p;
    • 6.3 char const * p;
    • 6.4 char const * const p;
  • 7、构造函数
    • 初始化列表注意事项
    • explicit构造函数
    • initializer_list的用法
  • 8、拷贝构造函数
    • 8.1 浅拷贝
    • 8.2 实现拷贝构造函数
      • 如何去做:
    • 8.3 拷贝构造函数被调用的情况
    • 8.4 深拷贝与浅拷贝⭐⭐⭐⭐⭐
    • 8.5 不会进入到拷贝构造函数,而是进入赋值构造函数的情况⭐⭐⭐⭐
  • 9、拷贝赋值构造函数
  • 10、左值、右值引用是什么?
    • 10.1 左值引用
    • 10.2 右值引用
    • 10.3 为什么要有右值引用呢?
      • 10.3.4 移动语义
        • **首先实现string类⭐⭐⭐⭐⭐**
  • 11、移动构造函数
  • 12、拷贝构造函数的参数为什么必须是引用?
  • 13、赋值构造函数为什么返回值是引用?
  • 14、const在成员对象不同位置的含义
    • c++在函数后加const的意义
    • 在函数前面加cosnt代表返回值不可修改。
  • 15、 静态成员与静态成员函数
  • 16、菱形继承与虚继承
    • 17、智能指针
      • (1)auto_ptr
      • (2)unique_ptr
      • (3)shared_ptr
      • (4)weak_ptr
  • 操作系统部分
  • 0、什么是内核?
    • 0.1 Linux内核系统体系结构
      • 0.1.1虚拟文件系统:
    • 0.2 Linux内核结构体
  • 1、[cache](https://www.cnblogs.com/jokerjason/p/10711022.html)
    • 1.1 cache是什么
    • 1.2 为什么需要cache
    • 1.3 cpu与cache 内存交互的过程
    • 1.4 cache写机制
    • 1.5 cache 一致性⭐⭐⭐⭐⭐
  • 2、Norflash与Nandflash的区别
  • 3、反码、补码
  • 4、内存管理MMU的作用
  • 5、SRAM、DRAM、SDRAM
  • 6、[主宰操作系统的经典算法](https://www.cnblogs.com/cxuanBlog/p/13372092.html)
  • 7、同步与异步
    • 7.1 中断的同步与异步
    • 7.2 同步与异步信号
  • 8、[实时操作系统和非实时操作系统的区别](https://blog.csdn.net/u013752202/article/details/53649047)
  • 9、ioremap实现物理地址到虚拟地址的映射
  • 10、I/O模型
    • 1.阻塞IO
    • 2.非阻塞IO(通过fcntl控制文件描述符非阻塞)
    • 3.IO多路复用
  • 11、同步与互斥
    • 什么是进程同步:
    • 什么是进程互斥
    • 进程互斥的四种软件方法
      • 1、单标志法
      • 2、双标志先检查法
      • 3、双标志后检查法
      • 4、Peterson算法
    • 进程互斥的硬件实现方法
      • 1、中断屏蔽方法
  • 12、死锁
    • 12.1 什么是死锁?
    • 12.2 死锁、饥饿、死循环的区别
    • 12.3 死锁产生的必要条件⭐⭐⭐⭐⭐
      • 1、互斥条件
      • 2、不可剥夺条件
      • 3、请求和保持条件
      • 4、循环等待条件
    • 什么时候会发生死锁
    • 死锁的处理策略
      • 1、预防死锁。破坏死锁产生的四个必要条件中的一个或几个
      • 2.避免死锁。用某种方法防止系统进入不安全状态,从而避免死锁(银行家算法)![在这里插入图片描述](https://img-blog.csdnimg.cn/20210622143617230.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTU2Njc2NQ==,size_16,color_FFFFFF,t_70)
      • 3.死锁的检测和解除。允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后采取某种措施解除死锁。
  • 13、内部总线协议
    • 什么是内部总线
    • 什么是外部总线
    • 1、UART
    • 2、I2C
  • 14、线程安全方法
    • 14.0、同步与互斥
    • 14.1、重提volatile
    • 14.2、竞争与原子操作
      • 14.2.1 屏蔽中断方式
      • 14.2.2 不同arm架构下原子操作处理方式
    • 14.3、同步与锁
      • 14.3.0 自旋锁
        • 单CPU下的三种情况
        • 多核CPU的情况
      • 14.3.1 信号量
      • 14.3.2 互斥量
      • 14.3.3 条件变量
      • 14.3.4 读写锁
      • 14.3.5 临界区
    • 14.4、重提可重入与线程安全
    • 15、冯诺依曼架构和哈弗架构
      • (1) 冯·诺依曼结构
      • (2) 哈佛结构
      • (3) 改进的哈佛结构

1、 字符串数组打印(指针的步长)

1.1 指针变量+1

char *p = NULL;
printf("%d\n",p); // 0
printf("%d\n",p+1); // 1int *p2 = NULL;
printf("%d\n",p2);  // 0
printf("%d\n",p2+1);  // 4

1.2 字符串数组的步长

main() { char *str[]={"ab","cd","ef","gh","ij","kl"}; char *t; t=(str+4)[-1]; printf("%s",t); }

则显示"gh"

为什么呢:

首先要知道存放的是char* 类型的数组,所以str + 4 也就是数组的第5个元素:“ij”,最后得到的是第5个元素的首地址,我们在去数组的[-1]索引也就是“gh”的首地址,最终打印的就是gh

1.3 跨行加⭐⭐⭐⭐⭐

main()
{//例子[1]int a[5]={1,2,3,4,5};int *ptr=(int *)(&a+1);//&a相当于变成了行指针,加1则变成了下一行首地址printf("%d,%d,%d",*(a+1),*(ptr-1));//例子[2]int * ptr1 = (int *)( (int)a + 1);int * ptr2 = (int *)( (int)a + 4);printf("%d,%d\n", ptr[-1],*ptr2);
}

例子[1]

1. *(a+1)就是a[1],执行结果是2因为a是int*类型,a+1步长为4
2.*(ptr-1)就是a[4],结果为5首先我们得到的是&a的地址,而&a是一个含有5个int类型的数组,所以&a+1的步长就是a数组整个的大小,加到a数组的末尾后面,(int *)(&a+1)这一句话,把它转换成int *类型的指针,步长又为4了,后面给它-1,即*(ptr-1) 相当于减了一个int* 的步长,结果为5

例子[2]

首先看里面的语句  (int)a + 1 、(int)a + 4
这个意思是我们把a的地址得到,然后把a的地址+1和+4,
那么ptr1肯定是一个乱的值,因为取的是不对的地址
而ptr2得到的是数组第二个元素的地址,再转换成(int *)类型
这样我们又可以进行后续的+-操作进行指针的引用了。

2、大端小端

小端:低位字节数据存储在低地址
大端:高位字节数据存储在低地址
例如:int a=0x12345678;(a首地址为0x2000)
0x2000 0x2001 0x2002 0x2003
0x12 0x34 0x56 0x78 大端格式

3、异步IO和同步IO区别

如果是同步IO,当一个IO操作执行时,应用程序必须等待,直到此IO执行完,相反,异步IO操作在后台运行,
IO操作和应用程序可以同时运行,提高系统性能,提高IO流量; 在同步文件IO中,线程启动一个IO操作然后就立即进入等待状态,直到IO操作完成后才醒来继续执行,而异步文件IO中,
线程发送一个IO请求到内核,然后继续处理其他事情,内核完成IO请求后,将会通知线程IO操作完成了。

4、变量a的不同定义

一个整型数 int a;
一个指向整型数的指针 int *a;
一个指向指针的指针,它指向的指针式指向一个整型数 int **a;
一个有10个整型数的数组 int a[10];
一个有10指针的数组,该指针是指向一个整型数 int *a[10];
一个指向有10个整型数数组的指针 int (*a)[10];
一个指向函数的指针,该函数有一个整型数参数并返回一个整型数 int ( *a)(int);

一个有10个指针的数组,该指针指向一个函数,该函数有一个整型数参数并返回一个整型 int (*a[10])(int);

5、关于char越界的数值

int foo(void)
{int i;char c=0x80;i=c;if(i>0)return 1;return 2;
}

返回值为2;因为i=c=-128;如果c=0x7f,则i=c=127

6、利用移位、与实现模

a=b*2;a=b/4;a=b%8;a=b/8*8+b%4;a=b*15;实现效率最高的算法a=b*2 -> a=b<<1;a=b/4 -> a=b>>2;a=b%8 -> a=b&7;a=b/8*8+b%4 -> a=((b>>3)<<3)+(b&3)a=b*15 -> a=(b<<4)-b

7、无符号与有符号相加结果为无符号类型

int main(void){unsigned int a = 6;int b = -20;char c;(a+b>6)?(c=1):(c=0);}

c=1,但a+b=-14;如果a为int类型则c=0。
原来有符号数和无符号数进行比较运算时(==,<,>,<=,>=),有符号数隐式转换成了无符号数(即底层的补码不变,但是此数从有符号数变成了无符号数),
比如上面 (a+b)>6这个比较运算,a+b=-14,-14的补码为1111111111110010。此数进行比较运算时, 被当成了无符号数,它远远大于6,所以得到上述结果。

如果a = 1,b = -2 结果为2^32 -1

8、实现某一位置0或置1操作,保持其它位不变

 #define BIT3 (0x1<<3)static int a;void set_bit3(void){a |= BIT3;}void clear_bit3(void){a &= ~BIT3;}实现多位置1与置0a &= ~( 1 << 3 | 1 << 4); //置0a |= (1<< 3 | 1 << 4); //置1

9、设置一绝对地址为0x67a9的整型变量的值为0xaa66

int *ptr;ptr = (int *)0x67a9;*ptr = 0xaa66;(建议用这种)一个较晦涩的方法是:
*(int * const)(0x67a9) = 0xaa66;

10、中断函数中的注意问题

__interrupt void compute_area (void) { double area = PI * radius * radius; printf(" Area = %f", area); return area; }

1、 ISR不可能有参数和返回值的!
2、 ISR尽量不要使用浮点数处理程序,浮点数的处理程序一般来说是不可重入的,而且是消耗大量CPU时间的!!

10.1 什么是不可重入函数

  • 函数体内使用了静态(static)的数据结构;

  • 函数体内调用了 malloc() 或者 free() 函数;

  • 函数体内调用了标准 I/O 函数;

    printf函数一般也是不可重入的,UART属于低速设备,printf函数同样面临大量消耗CPU时间的问题!

不可重入函数在实现时候通常使用了全局的资源,在多线程的环境下,如果没有很好的处理数据保护和互斥访问,就会发生错误,常见的不可重入函数有:

  • printf --------引用全局变量stdout

  • malloc --------全局内存分配表

  • free --------全局内存分配表

  • 满足下列条件的函数多数是不可重入的:

    (1)函数体内使用了静态的数据结构;

    (2)函数体内调用了malloc()或者free()函数;

    (3)函数体内调用了标准I/O函数。

10.2 如何写出可重入的函数?⭐⭐⭐⭐⭐

  • 在函数体内不访问那些全局变量;
  • 如果必须访问全局变量,记住利用互斥信号量来保护全局变量。或者调用该函数前关中断,调用后再开中断;
  • 不使用静态局部变量;
  • 坚持只使用缺省态(auto)局部变量;
  • 在和硬件发生交互的时候,切记关闭硬件中断。完成交互记得打开中断,在有些系列上,这叫做“进入/退出核心”或者用 OS_ENTER_KERNAL/OS_EXIT_KERNAL 来描述;
  • 不能调用任何不可重入的函数;
  • 谨慎使用堆栈。最好先在使用前先 OS_ENTER_KERNAL;

11、malloc内存分配

11.1malloc申请大小问题

#include <stdio.h>
#include <malloc.h>
int main()
{char *ptr;if((ptr = (char *)malloc(0)) == NULL)puts("got a null pointer\n");elseputs("got a valid pointer\n");int a =  malloc_usable_size(ptr);printf("size = %d\n", a);return 0;
}

malloc申请一段长度为0的空间,malloc依然会返回一段地址,还有一段地址空间,所以ptr不等于NULL。
malloc这个函数,会有一个阈值,申请小于这个阈值的空间,那么会返回这个阈值大小的空间。
如阈值为24,那么申请小于24的值就会返回24

结果如下图所示

这个阈值会随着编译器的不同而不同

如果申请一个负数,那么返回的是0,如下图

这是因为malloc规定不可以申请一个负数

参考博客

11.2 malloc底层实现原理

1)当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)

2)当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。

具体分析

从操作系统角度看,进程分配内存有两种方式,分别由两个系统调用完成:brk 和 mmap (不考虑共享内存)
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

1)brk 是将数据段(.data)的最高地址指针 _edata 往高地址推

将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:

1,进程启动的时候,其(虚拟)内存空间的初始布局如图1所示

2,进程调用A=malloc(30K)以后,内存空间如图2:

malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配

你可能会问:难道这样就完成内存分配了?

事实是:_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的

3,进程调用B=malloc(40K)以后,内存空间如图3

2)mmap 是在进程的虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空闲的虚拟内存。


4,进程调用C=malloc(200K)以后,内存空间如图4

默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存

这样子做主要是因为:

brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,因为只有一个_edata 指针,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放。

当然,还有其它的好处,也有坏处,再具体下去,有兴趣的同学可以去看glibc里面malloc的代码了。

5,进程调用D=malloc(100K)以后,内存空间如图5

6,进程调用free( C )以后,C对应的虚拟内存和物理内存一起释放

7,进程调用free(B)以后,如图7所示

B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了
  这里是因为
:malloc是从堆里面申请内存,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

8,进程调用free(D)以后,如图8所示

B和D连接起来,变成一块140K的空闲内存

9,默认情况下:

当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示

参考博客:
malloc底层实现及原理
linux malloc内存申请相关参数设置

12、变量全置0与全置1

unsigned int zero = 0;
unsigned int compzero = 0xFFFF;

对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:
unsigned int compzero = ~0; //这样才是全是置位1

13、你真的了解数组吗?

数组作为形参声明

int add(int a[])
int add[int a[][]] //错误的
int add(int a[][4])

声明一个指向N维数组的指针时,只能省略最左边方括号中的值。
因为第一对方括号只用于表明这是一个指针,而其他方括号则用于描述所指向数据对象的类型。

//等价的写法
int a[][3][4][5];
int (*a)[3][4][5];

变长数组

int sum(int ar[][col],int  row)
{}

可以输入不同的row行的值,表明输入的行有多少,不用一开始就确定了大小

字符串与字符数组

char str[5] = {'H' , 'E' ,'L' , 'L', 'O' };//字符数组
char str[5] = {'H' , 'E' ,'L' , 'L', '\0' };//字符串
char str[] = "hello";
sizeof(str) = 6
strlen(str) = 5
char src[5] = "hello"; //编译器会报错,还需要一个空间存放\0",需要改成6

数组名不可以++,指针可以

char head[] = “I love”;
const char *phead = “I love”;

*(phead++)可以,但是 * (head++)错误的,可以 * ( head + 1)。因为head是常量,而phead是变量

字符串的地址

const char* a = "abc";
const char* b = "abc";
printf("%s  %p  %p\n",a,&a,a);
printf("%s  %p  %p\n",b,&b,b);

结果

abc  0x7fffc8fd1920  0x4006e4
abc  0x7fffc8fd1928  0x4006e4

表明a和b的地址是不同的,但是他们里面指向的内容是一样的。

14、写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个

#define Min(a,b) (a) <= (b) ? (a) : (b)

15、说明关键字volatile有什么含意,并给出例子

(深入理解计算机操作系统P536页)

volatile表示被修饰的符号是易变的。告诉编译器不要随便优化我的代码!!

简要的说法:volatile关键字定义一个 变量的时候,是需要告诉编译器不要缓存这个变量到寄存器,每次从内存中读取该变量的值。可以应用在外围设备的特殊功能寄存器或者需要一个进程需要利用该变量判断条件,另一个进程改变该变量的状态。

15.1 外围设备的特殊功能寄存器

在嵌入式偏硬件方面的程序,我们经常要控制一些外围硬件设备,就拿I/O端口来说,我们会去操作映射到对应IO端口的寄存器。假设某一个寄存器的地址为0x1234,在C语言中,我们可以定义一个指针pRegister指向这个地址:

unsigned int *pRegister = (unsigned int *)0x1234;

实际应用中:我们经常会去判断一个寄存器中的值(或者寄存器中某一位)为‘0’还是‘1’。例如下面程序:

unsigned int *pRegister = (unsigned int *)0x1234;  //wait
while(*pRegister == 0){//不改变*pRegister的值
}  //Code...

我们的代码目的是不断的判断 *pRegister的值是否为‘0’。如果 *pRegister的值(值由硬件改变)在中途变为‘1’,则跳出死循环。
因为上面的循环中,*pRegister的值并没有发生改变,因为我们的编译器会对上述代码进行优化,如下:

unsigned int *pRegister = (unsigned int *)0x1234;  //wait
if (*pRegister == 0){while(1){//不改变*pRegister的值}
}//Code...

经过优化后,在上面的循环中,*pRegister的值不会发生改变,所以循环中就不再判断 *pRegister的值了,运行效率提升。但是pRegister指向的特殊功能寄存器,其值是由硬件改变的,而软件却不再判断 *pRegister的值了,那么就进入死循环了,即使 *pRegister的值发生了改变,软件也察觉不到了。

15.2 在中断服务函数中修改全局变量

static int flag = 1;  void main(void){  while (flag == 1){  //code ...  }  //code ...
}  void do_interrupt(void){  //中断服务程序//code...  flag = 0;
}

中断程序中修改了全局变量,上面的代码简单,只要flag的值为‘1’,就会一直运行循环里面的程序。刚才我们已经讲了,因为flag值在循环里没有改变,编译器就将对其优化。如下:

static int flag = 1;  void main(void){  if (flag == 1){  while (1){  //code ...   }  }  //code ...
}  void do_interrupt(void){  //code...  flag = 0;
}  

15.3 线程之间共享变量(在多线程中修改全局变量)

int  cnt;  void task1(void){  cnt = 0;  while (cnt == 0) {  sleep(1);  }
}  void task2(void){  cnt++;  sleep(10);
}

同理对while进行了优化

15.4 volatile的顺序性

对于两个volatile的变量在一起的时候编译器不会优化他们的顺序
而一个volatile和非volatile在一起的时候可能会改变他们的执行顺序。

16、位反转

实现一个8位数据反转

位 8  7  6  5  4  3  2  1
数 v8 v7 v6 v5 v4 v3 v2  v1转换后:位 8  7  6  5  4  3  2  1
数 v1 v2 v3 v4 v5  v6 v7 v8
unsigned char bit_reverse(unsigned char c)
{unsigned char buf;int bit = 8;while(bit){bit--;//最后需要移动0位,所以要先bit--,最后才能到0,如果在后面最后0的时候就会退出了buf |= ( (c&1) << bit);c >>= 1;}return buf;
}

17、字符串翻转

18、引用和指针的区别

(1). 指针是一个实体,而引用仅是个别名;
(2). 引用使用时无需解引用(*),指针需要解引用;
(3). 引用只能在定义时被初始化一次,之后不可变;指针可变;
(4). 引用没有 const,指针有 const,const 的指针不可变;
(5). 引用不能为空,指针可以为空;
(6). “sizeof 引用”得到的是所指向的变量(对象)的大小,
而“sizeof指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
(7). 指针和引用的自增(++)运算意义不一样;

19、-1,2,7,28,126请问28和126中间那个数是什么?为什么?

答案应该是4^3-1=63 规律是n3-1(当n为偶数0,2,4)n3+1(当n为奇数1,3,5)

20、一级指针无效传参

先看这个例子:

进入test02后栈上给s分配了一个空间,然后进入getstring,首先常量区给helloworld开辟了空间,然后str这个局部变量,在栈上也存放了helloworld内容,最后返回str指针的地址0x002,但是栈区的空间在执行完getstring后已经被回收了,所以打印的是乱码的东西。

答:结果可能是乱码。因为getstring返回的是指向“栈内存”的指针,该指针的地址不是 NULL,
但其原现的内容已经被清除,新内容不可知。

#include<stdio.h>
#include<memory.h>void allo(char *p)
{p = malloc(100);memset(p,0,100);strcpy(p,"hello word");
}int main()
{char *p = NULL;allo(p);printf("%d\n",p);system("pause");return 0;
}

结果为0;

同理这里进入allo函数后函数为char *p形参在栈上分配空间,然后p指向堆中一块内存,再把helloworld内容拷到p指向内存的地址,但是这个函数执行完后,p内容处的内容就释放掉了,并不会打印。

解决方法:改用二级指针或者返回char*

21、写出float x 与“零值”比较的if语句

if(x>0.000001&&x<-0.000001)

22、修改野指针

void Test(void){char *str = (char *) malloc(100);strcpy(str, “hello”);free(str);     if(str != NULL){strcpy(str, “world”); printf(str);}}

篡改动态内存区的内容,后果难以预料,非常危险。因为free(str);之后,str成为野指针, if(str != NULL)语句不起作用。

野指针不是NULL指针,是指向被释放的或者访问受限内存指针。

造成原因:指针变量没有被初始化,任何刚创建的指针不会自动成为NULL;
指针被free或delete之后,没有置NULL;
指针操作超越了变量的作用范围,比如要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

23、void* 类型

任何数据都是有类型的,告诉编译器分配多少内存

void val; //错误的

但是void* 可以 ,四个字节

void* 是所有类型指针的祖宗

int * pInt = NULL;
char *PChar = pInt;

从int* 到char* 系统会警告,可以使用类型转换:

int * pInt = NULL;
char *PChar = (char *)pInt;

或者用void * 接收

void * pVoid = pInt;

任何类型的指针,都可以不经过强制转换。转换成void* 类型,所以可以理解为void* 是所有类型指针的祖宗。

  • void * 主要用于数据结构的封装

  • 在取 指针的内容的时候,比如int *p,或者char *cp 可以取地址的内容 * cp ,但是如果是一个void *类型是不可以取内容的,因为未知目标类型。

  • void * 可以作为函数的返回值,函数中接收void * 返回值类型的函数返回的值时,需要对数据进行强制类型转换

24、sizeof的返回类型

(0)sizeof是操作符,sizeof测量的实体大小在编译阶段就已经确定

(1)sizeof返回的是unsigned int 所以

sizeof(int) - 5 其实结果是 > 0的

(2)当数组作为参数传入的时候,退化为首元素的指针

结果为28,4

25、typedef的作用

26、浮点数

首先我们如何表示一个浮点数,例子:

1011.01

8 4 2 1 . 0.5 0.25 按照这个规则,得到上面二进制的结果为11.25

单精度一共32位,我们也可以按照这个规律去定义

其中最高位为1为S符号位,中间8位为指数(E),表示小数点在第几位上,后面23位小数(M)表示二进制

S |-> 8位 <-| |-> 23位 <-|

结果为:

浮点数:0X41360000

0100 0001 0011 0110 000 0 0000 0000 0000

M = 011 0110 0000 0000 0000 0000 ,E = 100 0001 0

  1. M * 2 ^( E-127) = 1.0110110 * 2^3 相当于小数点往后移动3位 —> 1011.0110 = 11.375

double类型:

27、i++操作

27.1 i++的操作顺序

int a = 5, b = 7, c;c = a+++b;

c = 12;

++优先级比+高,所以先++后+

27.2 可以 &(i++) 吗?

为什么见:34

28、什么是左值与右值

左值就是出现在表达式左边的值(等号左边),可以被改变,他是存储数据值的那块内存的地址,也称为变量的地址;

右值是指存储在某内存地址中的数据,也称为变量的数据。

左值可以作为右值,但右值不可以是左值。

因此也只有左值才能被取地址。

一句话概括就是:左值就是可以被寻址的值

28.1 举例:

int i = 0;(i++)+=i; //错误(++i)+=i; //正确 int *ip = &(i++); //错误int *ip = &(++i); //正确

28.2 i++为什么不能作为左值

我们来看i++和i++的实现

// 前缀形式:
int& int::operator++() //这里返回的是一个引用形式,就是说函数返回值也可以作为一个左值使用
{//函数本身无参,意味着是在自身空间内增加1的*this += 1;  // 增加return *this;  // 取回值
}
//后缀形式:
const int int::operator++(int) //函数返回值是一个非左值型的,与前缀形式的差别所在。
{//函数带参,说明有另外的空间开辟int oldValue = *this;  // 取回值++(*this);  // 增加return oldValue;  // 返回被取回的值
}

简单得到理解,就是i++返回的是一个临时变量,函数返回后不能被寻址得到,它只是一个数据值,而非地址,因此不能作为左值。

更简单的代码解释

// i++:
{
int tmp;
tmp=i;
i=i+1;
return tmp;
}// ++i:
{
i=i+1;
return i;
}

29、union、struct结构体对齐总结(超全)

29.1 union对齐

对齐规则:
1、占用的内存空间大小需要是结构体中占用最大内存空间的类型的整数倍
2、共用内存大小
例子1:

union example {  int a[5];  char b;  double c;
};
int result = sizeof(example);

结果为: 24不是20
虽然 int a[5]的大小为20,但是需要以最大内存空间类型的整数倍对齐,doube为8字节,所以结果为24;

29.2 struct结构体对齐

(1)自然规则对齐

规则1:自然对齐的时候,存放地址空间的时候需要将存放的地址与数据类型的字节对齐
例子2:

struct example {int a[4];char b;double c;
}test_struct;

4个空间大小的int 占用16个字节,从0地址开始存,存到15地址
现在16地址存放char,
那double该存放到哪里呢?
double占用8个字节,所以需要以8地址为整数存放,所以下一个8地址对齐的地址是24
所以最终占用空间为32。

规则2:占用的内存空间大小需要是结构体中占用最大内存空间的类型的整数倍

例子3:

struct example {  char b;  double c;  int a;
}test_struct;
int result = sizeof(test_struct);

结果是24
为什么呢?你可能在想 第一个char 存放地址0
double存放地址8
int存放地址16
所以最终是20字节,但是需要按照最大的数据类型字节数的整数倍,即最大的double8字节的整数倍为24。

(2)加入#pragma pack(n)

其中n可以为1、2、4、8、16
规则:对齐字节数 = min(成员起始地址应是n的倍数时填充的字节数, 自然对齐时填充的字节数)

例子4:

#pragma pack(4)
using namespace std;
struct example{ char a; double b; int c;
}test_struct;

结果为16。
a存放的地址为0;
b存放的地址在自然对齐的情况下需要为double 8字节的倍数,是在8地址的时候存放,但是现在是4字节对齐,所以可以在4地址的位置存放double b;这里就用到了上述的规则,需要存放min(pack中n的倍数,自然对齐下数据类型的大小的倍数)结果的地址
c再存放在12地址的位置。
最终结果是16个字节大小。

例子5:

#pragma pack(8)
using namespace std;
struct example{  int a;  char b;  short int c;  int d;
}test_struct;

如果对齐的大小大于了本身数据类型的字节数,那么取最取最小的倍数,存放应该放置的地址。
所以这里数据类型的大小都比8小,所以还是按照(1)自然规则对齐中的规则进行对齐

(3)通过指定元素位数

例子6:

struct test
{char a : 7;int b : 11;int c : 4;int d : 10;char index;};

比如a,只占char类型的7位,并没有占8位,后面的b,c,d也是只占int的几个位,因此可以共用,11+4+10=25,没有超过32位,因此占4个字节就够了。这里的规则是如果是int占32位,只要后面的元素还能够放下,则共用一个4字节空间,然后前后的两个char根据对齐规则各占一个字节。结果为12字节

例子7:

#pragma pack(1)
struct MyStruct
{uint32_t i : 24;//char c;//char c : 8;//uint32_t c : 8;double a;char b;};

加上pack(1)后结果为13,即4 + 8 + 1 = 13;
uint32_t 中指定i为24位,剩下8位double没有指定位数无法填入,占4个字节,如果我中间加入一个char c呢,正好8位,会填入吗?
结果为 4 + 1 + 8 + 1 = 14;
如果加入char c : 8 呢 ,结果还是4 + 1 + 8 + 1 = 14;
如果加入uint32_t c : 8; 结果就是4 + 8 + 1 = 13;这里发现只有同类型的才可以位数填充。

那现在我把pack去掉

struct MyStruct
{uint32_t i : 24;//char c;//char c : 8;//uint32_t c : 8;double a;char b;
};

此时结果为 24;
因为0地址放i,
8地址放double,
16地址放char
但是空间大小要以最大的数据类型大小的倍数所以结果为24。
参考博客

(4)柔型数组


typedef struct _SoftArray{int len;int array[];
}SoftArray;sizeof(SoftArray));

结果为4.后面的数组并没有占用空间

1、什么是柔性数组?

柔性数组既数组大小待定的数组, C语言中结构体的最后一个元素可以是大小未知的数组,也就是所谓的0长度,所以我们可以用结构体来创建柔性数组。

2、柔性数组有什么用途 ?

它的主要用途是为了满足需要变长度的结构体,为了解决使用数组时内存的冗余和数组的越界问题。

3、用法 :在一个结构体的最后 ,申明一个长度为空的数组,就可以使得这个结构体是可变长的。对于编译器来说,此时长度为0的数组并不占用空间,因为数组名本身不占空间,它只是一个偏移量, 数组名这个符号本身代 表了一个不可修改的地址常量 (注意:数组名永远都不会是指针! ),但对于这个数组的大小,我们可以进行动态分配,对于编译器而言,数组名仅仅是一个符号,它不会占用任何空间,它在结构体中,只是代表了一个偏移量,代表一个不可修改的地址常量!

对于柔性数组的这个特点,很容易构造出变成结构体,如缓冲区,数据包等等:

柔型数组实例:

#include<stdio.h>
#include<malloc.h>
typedef struct _SoftArray{int len;
int array[];
}SoftArray;
int main()
{int len=10,i=0;SoftArray *p=(SoftArray*)malloc(sizeof(SoftArray)+sizeof(int)*len);p->len=len;for(i=0;i<p->len;i++){p->array[i]=i+1;}for(i=0;i<p->len;i++){  printf("%d\n",p->array[i]);}free(p);return 0;
}

可以实现动态的扩展结构体的大小,而不是在一开始就分配确定好大小。

参考博客

(5)不进行对齐

struct { char b; double c; int a;
}__attribute__((packed)) test_struct;

结果为13,取消了对齐。

30、实现strcpy

已知strcpy函数的原型是:

char * strcpy(char * strDest,const char * strSrc);

1.不调用库函数,实现strcpy函数。

2.解释为什么要返回char *。

30.1 代码实现

char * strcpy(char * strDest,const char * strSrc){if ((NULL==strDest) || (NULL==strSrc)) //[1]throw "Invalid argument(s)"; //[2]char * strDestCopy = strDest; //[3]while ((*strDest++=*strSrc++)!='\0'); //[4]return strDestCopy;
}

错误的做法[1]:

(A) 不检查指针的有效性,说明答题者不注重代码的健壮性。

(B) 检查指针的有效性时使用((!strDest)||(! strSrc))或(!(strDest&&strSrc)),说明答题者对C语言中类型的隐式转换没有深刻认识。在本例中char *转换为bool即是类型隐式转换,这种功能虽然灵活,但更多的是导致出错概率增大和维护成本升高。所以C++专门增加了bool、true、false三个关键字以提供更安全的条件表达式。

© 检查指针的有效性时使用((strDest0)||(strSrc0)),说明答题者不知道使用常量的好处。直接使用字面常量(如本例中的0)会减少程序的可维护性。0虽然简单,但程序中可能出现很多处对指针的检查,万一出现笔误,编译器不能发现,生成的程序内含逻辑错误,很难排除。而使用NULL代替0,如果出现拼写错误,编译器就会检查出来。

错误的做法[3]:

(A)忘记保存原始的strDest值,说明答题者逻辑思维不严密。

错误的做法[4]:

(A)循环写成while ( * strDestCopy++ = * strSrc++);,同[1](B)。

(B)循环写成while (*strSrc!=‘\0’) * strDest++ = * strSrc++;,说明答题者对边界条件的检查不力。循环体结束后,strDest字符串的末尾没有正确地加上’\0’。

30.2 strcpy能把strSrc的内容复制到strDest,为什么还要char *类型的返回值?

返回strDest的原始值使函数能够支持链式表达式,增加了函数的“附加值”。同样功能的函数,如果能合理地提高的可用性,自然就更加理想。

​ 链式表达式的形式如:

​ int iLength=strlen(strcpy(strA,strB));

​ 又如:

​ char * strA=strcpy(new char[10],strB);

30.3 strcpy和strncpy

strcpy并不能检查目标空间是否能够容纳字符串,用strncpy更安全。
多了一个拷贝的最大字符数,但是字符最后不一定会有‘\0’

31、手写memcpy, memcpy与strcpy的区别, memmove

将由src指向地址为起始地址的连续n个字节的数据复制到以dest指向地址为起始地址的空间内,函数返回一个指向dest的指针
特别说明(参考博客):

1.src和dest所指内存区域不能重叠,所以用restrict关键字修饰

2.与strcpy相比,memcpy遇到‘\0’并不会结束,而是一定会拷贝完n个字节

3.memcpy可以拷贝任何数据类型的对象,可以指定拷贝的数据长度

4.如果dest本身就有数据,执行memcpy()后会覆盖原有的数据

5.dest和src都不一定是数组,任意的可读写的空间均可

6.如果要追加数据,则每次执行memcpy后,要将目标数组地址增加到所要追加数据的地址

restrict关键字

该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于(base on)该指针的,即不存在其它进行修改操作的途径

void *memcpy( void * restrict dest , const void * restrict src, size_t n)

这是一个很有用的内存复制函数,由于两个参数都加了restrict限定,所以两块区域不能重叠,即dest指针所指的区域,不能让别的指针来修改,即src的指针不能修改. 相对应的别一个函数 memmove(void *dest,const void *src, size_t)则可以重叠。


#include<stdio.h>
#include<stdlib.h>
void * my_memcpy(void * restrict dest,const void * restrict src,unsigned count )
{if (dest == NULL || src == NULL){return NULL;}char* pdest =(char*) dest;char* psrc = (char*)src;while (count--){*pdest++ = *psrc++;}return dest;
}
int main()
{char src[] = "hello";char dest[] = "world";my_memcpy(dest, src, strlen(src));printf("%s", dest);system("pause");return 0;
}

C++部分

1、C++异常

抛出异常throw 和

捕获异常try catch

try 块中的代码有可能抛出异常。它后面通常跟着一个或多个catch 块在想要处理问题的地方,通过异常处理程序捕获异常

foo()
{//[1].......A..B.//[2]throw//[3].........//[4]
}

如果在throw处发生了异常,会干两件事:

1、程序首先会回到foo()函数的调用处,[3]底下的代码都不去运行了,

2、上面[1] - [2]之前的代码都会进行清理,一个回转的操作,如果调用了A或B,则回去调用A和B的析构函数,避免资源泄露 也可以理解成代码调转到[4]上结束运行这个函数,栈上的数据都销毁把A和B进行析构。

代码示例:

通过抛出异常返回一个 字符串"my exception" 在catch中捕捉到了这个字符串打印输出。

不可以滥用异常,销毁数据会造成性能消耗!

只有你实在没有办法解决的时候,比如一个除法,分母为0的时候,我不知道该怎么,我不能返回0和-1,因为结果并不是0和-1,我就可以抛出一个异常 const *str的字符串,并且你可以catch这个const *str

2、引用

2.1 什么是引用?

别名:同样的内存地址

2.2 引用 VS 指针

引用的内存意义:同样的内存地址!!!

而指针是int i = 5;

int * p = & i;

明显需要开辟一个新的地址存放 i 的地址,指向 i

主要有三个不同:

1、不存在空引用!引用必须连接到一块合法的地址

2、一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。

3、引用必须在创建时被初始化。指针可以在任何时间被初始化。

2.3 函数传参用到引用

foo( int a,int *b ,int& c)
{cout << a << b << c << endl;
}main()
{//[1]int a = 5;int *b = & a;int &c = a;//[2]foo();return 0;
}

执行到[1] - [2]之间的时候,堆栈上为 a ,b分配空间,c指向a ,运行到foo()后,堆栈会继续分配一个a的空间,以及一个b的空间,而c并不占空堆栈空间,直接指向了第一次堆栈上为a分配空间的地址2000上

​ 堆栈:

2000 a
1996 b
1992 a
1988 b

2.4 引用的优点和需要遵守的规则

将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?
好处:在内存中不产生被返回值的副本
注意:
1.不能返回局部变量的引用,主要是局部变量会在函数返回后被销毁,因此被返回的引用就成了无所指的引用,程序会进入未知状态
2.不能返回函数内部new分配的内存的引用(容易造成内存泄漏)

string& foo()
{string* str = new string("abc");return *str;
}
void main()
{//[1] 不会内存泄漏string& str1=foo();delete &str1;//[2] 内存泄漏string str = foo();}

​ str1是局部变量、出了作用域就没了。你申请的内存还在、但是“载体”没了。引用是“载体”的引用、 它本身不是“载体”

3.可以返回类成员的引用,但最好是const

3、 C++函数中的默认值

foo( int a,int *b ,int& c,int d = 200)
{cout << a << b << c << endl;
}
main()
{int a = 5;int *b = & a;int &c = a;foo(a,b,c); //正确foo(100,b, c,200); //错误,默认值只能放在最后面,几个都可以,但是不能放在第一个
}

4、register存储类

register定义存储在寄存器中而不是内存中的局部变量(只是希望而已)

然而,完全取决于编译器!

Best Practice:主要为了优化性能。注意,不要过早优化不要因为C++可以做什么就做什么!

优化什么呢:我想频繁的使用这个变量,从寄存器里面读肯定会快!

但是也有问题:

register int b;
int * a = & b;

这个时候b肯定不能放在寄存器里面,因为你想取地址b,寄存器哪有地址,编译器就不会这么做。

5、内联函数inline

如何一个函数比较小巧,经常调用,可以减少函数调用用到堆栈的开销

优点:

  • 没有了调用的开销,效率也很高。
  • 编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确
  • 然后进行一系列的相关检查,就像对待任何一个真正的函数一样
  • 这样就消除了它的隐患和局限性。(宏替换不会检查参数类型,安全隐患较大:对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患。)
  • 可以作为一个类的成员函数,与类的普通成员函数作用相同

缺点:

  • 内联函数的函数体一般来说不能太大,如果内联函数的函数体过大,一般的编译器会放弃内联方式
  • 如果函数体变大的话,会造成很多的cache miss 发生具体原理可以去看一下。

6、const和指针

C++之父推荐的读法是:从右往左读

6.1 char * const cp;

//example 1:
char * const cp;
// cp is cosnt pointer to char 说明他是一个cosnt指针,指向的内容为char
// 因此它的指针指向是不可以更改的,如果 cp = &x;
//试图将 cp的指针指向x地址这是不可以的,因为它不可以修改。

6.2 const char * p;

//example 2:
const char * p
// cp is pointer to cosnt char 说明他是一个指针,指向的内容为 const char
// 因此它的指针指向的内容是不可以更改的,如果 *p = x;
//试图将 p的指针指向的内容进行修改这是不可以的,因为它不可以修改。

6.3 char const * p;

char const跟 const char是一样的

//example 3:
const char * p
// cp is a pointer to char that is cosnt  说明他是一个指针,指向的内容为 char const
// 因此它的指针指向的内容是不可以更改的,如果 *p = x;
//试图将 p的指针指向的内容进行修改这是不可以的,因为它不可以修改。

6.4 char const * const p;

//example 3:
const char * p
// cp is a const pointer to cosnt char
//说明他是一个cosnt指针,指向的内容为 const char

7、构造函数

1、一个构造函数包含构造函数和析构函数这是最基本的

2、构造函数没有返回值,构造函数初始化成员在后面用:给成员初始化(使用初始化列表的构造函数是显式的初始化类的成员),而不是{ sz = size};在构造函数里面,因为如果这个数据类型比较大的话,开销就大了。

初始化列表注意事项

当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通
过成员初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。因为const对象不允许重新赋值,同时引用需要被初始化。

class Vector{private:size_t sz; // size_t = unsigned intdouble *elem;Vector(size_t size) : sz(size). elem(new double[sz]){}
public:Vector(initializer_list<double> lst);  //类内声明double& operator[](int i){return elem[i]; }size_t size(){return sz;}~Vector(){};
}Vector::Vector(initializer_list<double> lst) //可以类内声明,类外定义函数:elem(new double[lst.size()])
{std::copy(lst.begin(),lst.end(),elem);
}

3、构造函数可以有很多个,但是会有一个默认的构造函数

Vector(){}

explicit构造函数

解决隐式类型转换问题

Vector v1 = 7; // OK ,V1 有7个元素如果写成explicit Vector(int s); // no implicit  不可以隐式的
Vector v1(7); // OK ,V1 有7个元素
Vector v1 = 7; // error,不可以隐式转换

initializer_list的用法

Vector(initializer_list<double> lst):elem(new double[lst.size()])
{std::copy(lst.begin(),lst.end(),elem);
}Vector v2 {1,2,3,4,5}; //写一个这样的构造函数就可以实现使用列表初始化

8、拷贝构造函数

接着7中的例子:

8.1 浅拷贝

void bad_copy*(Vector v1)
{Vector v2 = v1;v1[0] = 2;v1[1] = 3;v2[0] = 10;v2[1] = 20; //此时你以为v1 v2是两个不同的东西,实际上是两个相同的东西
}

此时v2 = v1;

在栈中v2和v1分别在两个地方保存, v1中st和v2中的st存在于不同的地址空间,但是指针他们指向的确实同一块空间。

8.2 实现拷贝构造函数

classname (const classname &obj){...}
//在...中实现自己的拷贝构造函数
  • 通过使用另一个同类型的对象来初始化新创建的对象
  • 复制对象把它作为参数传递给函数
  • 复制对象,并从函数返回这个对象
  • 如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数⭐⭐⭐⭐⭐

如何去做:

​ 首先建立对象,并调用其构造函数,然后成员被拷贝。

​ 用A初始化B的完成方式是内存拷贝,复制所有成员的值

指针虽然复制了,但所指向的空间并没有复制,而是由两个对象共用了

Vector::Vector(const Vector& other):sz(other.size()),:elem(new double[other.size()])
{for(int i = 0; i!=sz; i++)elem[i] = other.elem[i];
}

8.3 拷贝构造函数被调用的情况

带有指针的类必须要有拷贝构造和拷贝赋值

  • 对象初始化拷贝赋值 Mytype B = A或者 Mytype B(A)

  • 定义新对象,并用已有对象初始化新对象时,即执行语句“MyType B=A;”时(定义对象时使用赋值初始化)

  • 当对象直接作为参数传给函数时,函数将建立对象的临时拷贝,这个拷贝过程也将调同拷贝构造函数

    void func(Mytype t) //传入A时,会有t = p;t会调用拷贝构造函数
    {}
    
  • 函数返回时,函数栈区的对象会复制一份到函数的返回去

    Mytype foo()
    {
    Myype A;return A; //堆栈上开辟临时变量,复制给B
    }
    Mytype B = foo();// 匿名对象被扶正,他们是同一个对象
    Mytype C;
    C = foo(); // 用匿名对象给C,匿名对象被析构
    

8.4 深拷贝与浅拷贝⭐⭐⭐⭐⭐

浅拷贝是增加了一个指针,指向原来已经存在的内存。而深拷贝是增加了一个指针,并新开辟了一块空间让指针指向这块新开辟的空间。浅拷贝在多个对象指向一块空间的时候,释放一个空间会导致其他对象所使用的空间也被释放了,再次释放便会出现错误。

8.5 不会进入到拷贝构造函数,而是进入赋值构造函数的情况⭐⭐⭐⭐

Mytype B;
B = A;

此时就不会进入拷贝构造函数!!!

9、拷贝赋值构造函数

如何进入赋值构造函数,见8.4 ,将一个已有的对象赋值给另一个已有的对象如B = A就进入赋值构造函数;
如果是Mytype B = A就进入拷贝构造函数。

拷贝赋值函数:
本来就有的两个东西,需要把原来的东西清空,分别配一个和原来一样大的空间,把数据拷贝过来

实现思路:
1、检测是否是自我赋值
2、释放原有自己的空间
3、为自己分配新的空间
4、完成值的拷贝

Vector& Vector::operator=(const Vector& a)
{if( &a == this )return *this;delete[] elem;double *p = new double[a.size()];for(int i = 0; i!=sz;++i)p[i] = a.elem[i];       //记住C++中分配空间和初始化写在一起。elem = p;sz = a.sz;return *this;
}

10、左值、右值引用是什么?

左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式)

int a;
int b; a = 3;
b = 4;
a = b;
b = a; // 以下写法不合法。
3 = a;
a+b = 4;

右值指的则是只能出现在等号右边的变量(或表达式)。如运算操作(加减乘除,函数调用返回值等)所产生的中间结果。

10.1 左值引用

MyType &引用名 = 左值表达式;
int main(){ int a = 0; int &b = a; b = 11; return 0;
}

10.2 右值引用

右值的引用,就是给右值取别名

Type &&引用名 = 右值表达式;

10.3 为什么要有右值引用呢?

左值引用我们知道可以用于函数传参,减少不必要的对象拷贝,提升效率;或者用于替代指针的使用

C++11引入右值引用主要是为了实现移动语义完美转发

10.3.4 移动语义

首先实现string类⭐⭐⭐⭐⭐

#include <iostream>
#include <cstring>
#include <vector>
using namespace std; class MyString{
public: static size_t Ctor; //统计调用构造函数的次数 static size_t CCtor; //统计调用拷贝构造函数的次数
public: //构造函数 MyString(const char* str = nullptr) { ++Ctor; if (str != nullptr) { m_data = new char[strlen(str) + 1]; strcpy(m_data, str); } else { m_data = new char[1]; *m_data = '\0'; } } // 拷贝构造函数 MyString(const MyString& other) { ++CCtor; m_data = new char[strlen(other.m_data) + 1]; strcpy(m_data, other.m_data); } // 拷贝赋值函数 =号重载 MyString& operator=(const MyString& other) { if (this == &other) // 避免自我赋值!! return *this; delete[] m_data;  // 先释放原来的空间m_data = new char[strlen(other.m_data) + 1]; strcpy(m_data, other.m_data); return *this; } ~MyString() { delete[] m_data; m_data = NULL; }
private: char* m_data;
};

测试一下代码:

size_t MyString::Ctor = 0;
size_t MyString::CCtor = 0; int main(){ vector<MyString> vecStr; vecStr.reserve(1000); //先分配好1000个空间 for (int i = 0; i < 1000; i++) { vecStr.push_back(MyString("hello")); } cout << "构造次数:" << MyString::Ctor << endl; cout << "拷贝构造次数:" << MyString::CCtor << endl;
}

结果:

构造次数:1000
拷贝构造次数:1000

我们每次拷贝一个临时变量都需要深拷贝即进入拷贝构造函数中,这样会降低效率,因此拷贝构造函数每次都是重新分配一块新的空间,同时将要拷贝的对象复制过来。

要是我们能够减少这个拷贝的次数,效率就提升了,所以这个时候右值引用就派上用场了

右值引用可以引用并修改右值,但是通常情况下,修改一个临时值是没有意义的。然而在对临时值进行拷贝时,我们可以通过右值引用来将临时值内部的资源移为己用,从而避免了资源的拷贝

总结一下移动语义就是说:我将一个临时的对象或者马上就不需要的对象传给一个新的对象的时候,因为之前的对象我们不需要的,正常的话会调用拷贝构造函数,但是拷贝构造函数会涉及深拷贝,我需要新建立一个空间给自己然后把临时变量的内容拷贝过来,但是临时对象马上就要销毁了呀,所以我们只需要将自己资源指向原来临时变量已经存在的资源,然后把原来已经存在的资源进行销毁即可,下面的代码可以看到我们不需要原来的资源后,把原来的指针指向一个空指针
代码如下:增加移动构造函数:

size_t MyString::MCtor = 0;  //统计调用移动构造函数的次数
// 移动构造函数
MyString(MyString&& str)  :m_data(str.m_data) {  ++MCtor;  str.m_data = nullptr; //不再指向之前的资源了
}

测试结果:

构造次数:1000
拷贝构造次数:0
移动构造次数:1000

成功减少了临时对象拷贝的次数。

11、移动构造函数

移动构造函数拷贝构造函数的区别是,拷贝构造的参数是const Mytype& str,是常量左值引用,而移动构造的参数是Mytype&& str,是右值引用

移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间同时将要拷贝的对象复制过来,而是"拿"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,那么就没有“拿”过来。

拷贝构造函数中,对于指针,我们一定要采用深层复制

而移动构造函数中,对于指针,我们采用浅层复制

注意:指针的浅层复制危害性极大!

之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了,所以我们要重设指针为nullptr(因为a我们不再使用了,如果不设为nullptr那么指针还指向新创建的空间,一旦出现内存被释放了以后,指针却还指向该内存,就会出现“野指针”的问题,容易出现内存非法访问错误。)

用a初始化b后,a我们就不需要了,最好是初始化完成后就将a析构(不能用了)所以移动构造函数,专门处理这种,用a初始化b后,就将a析构的情况

移动构造函数 在初始化的时候就是作了一个浅拷贝

然后把指针指向了空。

关于move的使用调用移动构造函数⭐⭐⭐⭐⭐

move方法来将左值转换为右值,从而调用移动构造函数而不是拷贝构造函数。

使用移动构造函数会提升函数的效率,见下面代码中return z;

Vector foo()
{Vector x(2000);Vector y(2000);Vector z(2000);z = x;  // 执行赋值构造函数y = std::move(x); //执行移动构造函数return z; // 我们返回z,再想一个问题之前提到8.3的第四条中,在栈中返回,它是一个将亡之人,如果我们调用拷贝构造函数的话会效率很低,因为这个z我后面并不需要它了,因此我们用浅拷贝(移动构造函数)就可以了。
}

12、拷贝构造函数的参数为什么必须是引用?

拷贝构造函数的参数为什么必须是引用?_nwd0729的专栏-CSDN博客

如果不用引用的话,赋值传参的时候又会调用拷贝构造函数,这样就会无限循环的调用拷贝构造函数。

13、赋值构造函数为什么返回值是引用?

#include <iostream>
using namespace std;
class Test
{public:Test();~Test();Test(const Test& t);//Test& operator=(const Test& t);Test operator=(const Test& t);
private:int t1;
};
Test::Test()
{cout<<"调用构造函数"<<endl;
}
Test::~Test()
{cout<<"调用析构函数"<<endl;
}
Test::Test(const Test& t)
{cout<<"调用拷贝构造函数"<<endl;
}
//Test& Test::operator =(const Test& t)
Test Test::operator =(const Test& t)
{cout<<"调用赋值构造函数"<<endl;t1 = t.t1;return *this;
}
int main()
{Test t1,t2,t3;cout << "-----"<<endl;t1 = t2 = t3;return 0;
}

结果为:

调用赋值构造函数
调用拷贝构造函数
调用赋值构造函数
调用拷贝构造函数

如果把改成赋值构造函数返回值改为Test &
结果为:

调用赋值构造函数
调用赋值构造函数

这里存在连续赋值,这是符合C++的语法规范的。如果赋值操作符返回一个引用类型,倒不是说 b=c 返回的引用变量直接赋值给a,毕竟a不是引用类型。

该过程实际上是 b=c 返回一个引用temp,然后 a=temp 再次调用赋值操作符。这里存在两次调用赋值操作符。

如果赋值操作符不是返回的一个引用那么 在b=c调用复制操作符之后就会再次调用拷贝构造函数返回一个临时对象temp 然后 a=temp 再调用赋值操作符。增加了一次拷贝的代价。

参考链接

14、const在成员对象不同位置的含义

c++在函数后加const的意义

class String
{public:                                 String(const char* cstr=0);                     String(const String& str);
//我们返回值本来就有了不是在operator里面创建的所以返回类型为String&   String& operator=(const String& str);         ~String();                                    char* get_c_str() const { return m_data; }
private:char* m_data;
};

对于一个string类中,可以看到get_c_str我们需要返回一个值,但是这个值我是只读的并不需要修改,这个时候我们就可以在函数后加cosnt表明:
1、C++编译器在实现const的成员函数时,为了确保该函数不能修改类的实例状态,会在函数中添加一个隐式的参数const this*。
2、但当一个成员为static的时候,该函数是没有this指针的,也就是说此时const的用法和static是冲突的;
3、如果成员变量是mutuable的话则在上述后面加的const成员函数中可以进行修改。

在函数前面加cosnt代表返回值不可修改。

15、 静态成员与静态成员函数

  • 静态成员函数可以把函数与类的任何特定对象独立开来

  • 静态成员函数即使在类对象不存在的情况下也能被调用,只要使用类名加范围解析运算符::就可以访问

    class A{int sz;static void foo(){}
    };
    A a;
    A::foo();
    a.foo();
    

创建了一个对象a,但是foo并不属于a

  • 静态成员函数只能访问静态成员数据、其他静态成员函数和类外部函数静态成员函数不能访问类的 this 指针,都需要对象所以就没有this指针

    static int foo()
    {return 2 * sz;
    }
    

    这就是错误的了,不能访问某个对象的成员。

静态成员函数总结如下:

1、静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员, 完成对静态数据成员的封装
2、静态成员函数只能访问静态数据成员。原因:非静态成员函数,在调用时this 指针被当作参数传进。而静态成员函数属于类,而不属于对象,没有 this 指针。

静态变量总结如下:

  • static 成员变量实现了同类对象间信息共享。
  • static 成员类外存储,求类大小,并不包含在内。
  • static 成员是命名空间属于类的全局变量,存储在 data 区。
  • static 成员只能类外初始化
  • 可以通过类名访问(无对象生成时亦可),也可以通过对象访问

16、菱形继承与虚继承

class Grandfather{int obj;
}
class father1 : public Grandfather {}
class father2 : public Grandfather {}
class son : public father1 ,public  father2 {}

产生菱形继承的时候,son会有两份obj分别从father1 和father2继承而来。
son s;
s.obj = 5;//会报错
s.father1:: obj = 5; //这样才可以
s.father2:: obj = 5;
这时我们可以通过虚继承,防止儿子在多继承的时候,出现爷爷中的变量拷贝多份。

class Grandfather{int obj;
}
class father1 : virtual  public Grandfather {}
class father2 : virtual public Grandfather {}
class son : public father1 ,public  father2 {}

这样son得到的就是独一份Grandfather的obj

17、智能指针

C++里面有四个指针:auto_ptr、unique_ptr、shared_ptr、weak_ptr

(1)auto_ptr

auto_ptr<int> pint1;
auto_ptr<int> pint2;
pint2 = pint1;

两个指针指向了同一个地方,释放的时候会释放两次,存在这个bug。所以C++11中弃用了auto_ptr

(2)unique_ptr

unique指针规定一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存,编译报错。

unique_ptr<int> pint1;
unique_ptr<int> pint2;
pint2 = pint1; //报错

原理是通过将拷贝构造函数生命为private,赋值函数声明为delete。

那现在既不能赋值也不能拷贝,只能通过move转发实现从一个指针转换到另外一个指针。move的原理就是将自己的指针指向别人的资源,然后将别人的指针修改为nullptr。

unique_ptr<int> pInt(new int(1));
// move转移
unique_ptr<int> pInt1 = std::move(pInt);

(3)shared_ptr

auto存在多个指针指向相同的对象,会造成多次释放相同空间,而unique只能指向一个对象。
shared_ptr可以多个指针指向一个对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。

原理:通过一个引用计数的指针类型变量m_count,专门用于引用计数,使用拷贝构造函数和赋值拷贝构造函数时,引用计数加1,当引用计数为0时,释放资源。

但是shared_ptr存在循环引用计数问题,因此将其中一个变量改为weak_ptr即可。

(4)weak_ptr

weak_ptr就是为了解决shared_ptr存在循环引用计数问题才设计的。
weak_ptr的构造和析构不会引起引用计数的增加或减少

操作系统部分

0、什么是内核?

0.1 Linux内核系统体系结构

5大模块:进程调度模块、内存管理模块、文件系统模块、进程间通信模块和网络接口模块。

**进程调度模块:**控制进程被CPU资源的使用,采取的策略是不同进程能够公平合理的访问CPU,同时保证内存能够及时的执行硬件操作

**内存管理模块:**保证所有的进程能够安全的来共享机器上的内存区,同时这个内存管理的模块还支持虚拟内存的管理方式,能够使得支持进程使用比实际使用内存的空间更大,并且可以利用文件系统把这些暂时不用的内存数据块交换到外部的存储设备上。

**文件系统模块:**支持对外部设备的驱动和一些存储,虚拟文件系统VFS(这里面的回答很重要⭐⭐⭐⭐⭐)这个模块通过所有的外部设备提供一个通用的文件接口,他隐藏了各种各样硬件设备以及实现细节

0.1.1虚拟文件系统:

​ 虚拟文件系统是一套代码框架(framework),它处于文件系统的使用者与具体的文件系统之间,将两者隔离开来。这种引入 一个抽象层次的设计思想,即“上层不依赖于具体实现,而依赖于接口;下层不依赖于具体实现,而依赖于接口”,就是著名的“依赖反 转”,它在 Linux内核中随处可见。
​ VFS框架的设计,需要满足如下需求:

​ 1、 为上层的用户提供统一的文件和目录的操作接口,如 open, read, write

​ 2、 为下层的具体的文件系统,定义一系列统一的操作“接口”, 如 file_operations, inode_operations, dentry_operation,而 具体的 文件系统必须实现这些接口,才能融入VFS框架中。

**进程间通信模块:**用于多种进程间通信

**网络接口模块:**支持各种网络通信的标准

0.2 Linux内核结构体

0.3

内核的内存管理的基本单位是页(page),源码下面 /include/linux/mm_types

1、cache

1.1 cache是什么

Cache存储器:电脑中为高速缓冲存储器,是位于CPU和主存储器DRAM之间,规模较小,但速度很高的存储器,通常由SRAM静态存储器组成。

高速缓冲存储器最重要的技术指标是它的命中率CPU要访问的数据在Cache中有缓存,称为“命中” (Hit),反之则称为“缺失” (Miss)。

现在 CPU 的 Cache 又被细分了几层,常见的有 L1 Cache, L2 Cache, L3 Cache,其读写延迟依次增加,实现的成本依次降低。

现代系统采用从 Register ―> L1 Cache ―> L2 Cache ―> L3 Cache ―> Memory ―> Mass storage的层次结构,是为解决性能与价格矛盾所采用的折中设计。

下图描述的就是CPU、Cache、内存、以及DMA之间的关系。程序的指令部分和数据部分一般分别存放在两片不同的cache中,对应指令缓存(I-Cache)和数据缓存(D-Cache)。

1.2 为什么需要cache

CPU缓存(Cache Memory)位于CPU与内存之间的临时存储器,它的容量比内存小但交换速度快。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可避开内存直接从缓存中调用,从而加快读取速度。

使用Cache改善系统性能的依据是程序的局部性原理,包括时间局部性和空间局部性。即最近被CPU访问的数据,短期内CPU 还要访问(时间);被 CPU 访问的数据附近的数据,CPU 短期内还要访问(空间)。因此如果将刚刚访问过的数据缓存在Cache中,那下次访问时,可以直接从Cache中取,其速度可以得到数量级的提高。

1.3 cpu与cache 内存交互的过程

CPU接收到指令后,它会最先向CPU中的一级缓存(L1 Cache)去寻找相关的数据,然一级缓存是与CPU同频运行的,但是由于容量较小,所以不可能每次都命中。这时CPU会继续向下一级的二级缓存(L2 Cache)寻找,同样的道理,当所需要的数据在二级缓存中也没有的话,会继续转向L3 Cache、内存(主存)和硬盘.

1.4 cache写机制

Cache写机制分为write through和write back两种。

Write-through(直写模式)在数据更新时,同时写入缓存Cache和后端存储。此模式的优点是操作简单;缺点是因为数据修改需要同时写入存储,数据写入速度较慢。

Write-back(回写模式)在数据更新时只写入缓存Cache。只在数据被替换出缓存时,被修改的缓存数据才会被写到后端存储。此模式的优点是数据写入速度快,因为不需要写存储;缺点是一旦更新后的数据未被写入存储时出现系统掉电的情况,数据将无法找回。

读机制

贯穿读出式(Look Through)

该方式将Cache隔在CPU与主存之间,CPU对主存的所有数据请求都首先送到Cache,由Cache自行在自身查找。如果命中。 则切断CPU对主存的请求,并将数据送出;不命中。则将数据请求传给主存。

该方法的优点是降低了CPU对主存的请求次数,缺点是延迟了CPU对主存的访问时间。

旁路读出式(Look Aside)

在这种方式中,CPU发出数据请求时,并不是单通道地穿过Cache。而是向Cache和主存同时发出请求。由于Cache速度更快,如果命中,则Cache在将数据回送给CPU的同时,还来得及中断CPU对主存的请求;不命中。则Cache不做任何动作。由CPU直接访问主存。它的优点是没有时间延迟,缺点是每次CPU对主存的访问都存在,这样。就占用了一部分总线时间。

1.5 cache 一致性⭐⭐⭐⭐⭐

DMA和cache一致性问题

2、Norflash与Nandflash的区别

(1)、NAND闪存的容量比较大
(2)、由于NandFlash没有挂接在地址总线上,所以如果想用NandFlash作为系统的启动盘,就需要CPU具备特殊的功能,
如s3c2410在被选择为NandFlash启动方式时会在上电时自动读取NandFlash的4k数据到地址0的SRAM中。
(3)、NAND Flash一般地址线和数据线共用,对读写速度有一定影响。NOR Flash闪存数据线和地址线分开,
所以相对而言读写速度快一些。

3、反码、补码

反码:对原码除符号位外的其余各位逐位取反就是反码

补码:负数的补码就是对反码加1

正数的原码、反码、补码都一样

4、内存管理MMU的作用

  • 内存分配和回收
  • 内存保护
  • 内存扩充
  • 地址映射

5、SRAM、DRAM、SDRAM

SRAM:CPU的缓存就是SRAM,静态的随机存取存储器,加电情况下,不需要刷新,数据不会丢失
DRAM,动态随机存取存储器最为常见的系统内存,需要不断刷新,才能保存数据
SDRAM:同步动态随机存储器,即数据的读取需要时钟来同步。

6、主宰操作系统的经典算法

7、同步与异步

7.1 中断的同步与异步

  • 同步中断:异常

    指令执行完毕后才会发生中断,而不是代码指令执行期间,如系统调用[时钟同步]

  • 异步中断:中断
    打断指令执行,键盘中断[时钟不同步]

    • 可屏蔽中断
    • 不可屏蔽中断

7.2 同步与异步信号

  • 同步信号:提前约定好

  • 异步信号:并没有通知,突然访问

    这里的同步跟异步是不是跟上面的中断有点像,同步中断得等一个执行指令执行完了,才可以进行中断,由时钟控制,这里的同步信号通过提前一个时钟约定好什么时候该干什么事;
    异步就是突然就要访问。

常见的IIC跟SPI就是同步信号
串口就是异步信号:可以一根数据线,通过高低电平的持续时间来双方设定协议通信。

差别:

同步传输 异步传输
信号线 多:时钟信号、数据信号 少:只需要数据信号
速率 可变,提高时钟信号频率即可 双方提前约定
抗干扰能力

8、实时操作系统和非实时操作系统的区别

这篇文章总结的很好:

Linux是分时操作系统,不是实时的

可剥夺型内核与不可剥夺型内核的区别

9、ioremap实现物理地址到虚拟地址的映射

函数原型

虚拟地址 = ioremap(物理地址,size)

作用:把物理地址开始的一段空间大小为 (size),映射为虚拟地址;返回值是该段虚拟地址的首地址。

实际上,它是按页4096字节进行映射的,是整页整页地映射的。
假设物理地址= 0x10002 ,size=4;
ioremap 的内部实现是:
1、物理地址 按页取整,得到地址 0x10000
2、 size 按页取整,得到 4096
3、把起始地址 0x10000 ,大小为4096的这一块物理地址空间,映射到虚拟地址空间,假设得到的虚拟空间起始地址为 0xf0010000
4、那么 phys_addr = 0x10002 对应的 虚拟地址 = 0xf0010002
5、不再使用该段虚拟地址时,要 iounmap(virt_addr)

10、I/O模型

先前知识:以网络编程为例,我们知道套接字编程通信文件的fd中read和write读写两端都有缓冲区,且在内核空间中

在TCP/IP编程中:
1、连接的建立:accept,connect
2、连接的断开:

主动断开:close,shutdown(fd)
被动断开:read = 0(对方断开);write = -1,errno = EPIPE;

3、消息的到达

read,recv
非阻塞IO:read = -1

IO模型主要五种:

1.阻塞IO

阻塞IO要等到数据从缓冲区拷贝完成才能继续,否则一直阻塞当前的线程;

2.非阻塞IO(通过fcntl控制文件描述符非阻塞)

1、非阻塞IO没有数据准备会立即返回,不会阻塞线程
2、非阻塞IO需要我们主动探测数据是否准备,而阻塞IO会一直等待数据拷贝完毕

阻塞与非阻塞IO的区别:
数据未到达时是否立即返回。

通过上面的内容我们知道此时没有IO多路复用
直接通过read/write读取的状态直接操作网络数据 得到网络状态
非阻塞IO :

首先我们知道需要我们主动探测,数据是否准备
但是会有两个问题:
(1)我们可以利用线程做其他的事情,但是我们不知道数据什么时候准备好,
(2) 这个数据已经准备很久了,但是我们去做其他任务了

阻塞IO

问题:当前我只能处理一条连接的IO

因此可以引出IO多路复用模型了

3.IO多路复用

上面非阻塞IO我们知道,需要主动的探测,探测问题交由我们IO多路复用

也就是一个线程去检测多个IO连接的事件

11、同步与互斥

什么是进程同步:

同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。

什么是进程互斥

进程的“并发”需要“共享”的支持。各个并发执行的进程不可避免的需要共享一些系统资源(比如内存,又比如打印机、摄像头这样的I/O设备)

两种资源共享方式:
1、互斥共享方式
某些资源提供多个进程使用,但是一个时间段只允许一个进程访问该资源
2、同时共享方式
允许时间段内多个进程“同时”对他们进行访问

我们把一个时间段内只允许一个进程使用的资源称为临界资源。许多物理设备(比如摄像头、打印机)都属于临界资源。此外还有许多变量、数据、内存缓冲区等都属于临界资源。
对临界资源的访问,必须互斥地进行。互斥,亦称间接制约关系。进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。当前访问临界资源的进程访问结束,释放该资源之后,另一个进程才能去访问临界资源。

为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以下4个原则:

进程互斥的四种软件方法

1、单标志法

2、双标志先检查法


检查和上锁不是原子操作

3、双标志后检查法

4、Peterson算法


Peterson算法用软件方法解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待三个原则,但是依然未遵循让权等待的原则。

进程互斥的硬件实现方法

1、中断屏蔽方法


只适用于内核进程,不适用于用户进程。

12、死锁

12.1 什么是死锁?

在并发环境下,各进程因竞争资源而造成的一种互相等待对方手里的资源,导致各进程都阻塞,都无法向前推进的现象,就是“死锁”

12.2 死锁、饥饿、死循环的区别

12.3 死锁产生的必要条件⭐⭐⭐⭐⭐

产生死锁必须同时满足以下四个条件,只要一条不成立,死锁就不会产生。

1、互斥条件

互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁(如哲学家的筷子、打印机设备)。像内存、扬声器这样可以同时让多个进程使用的资源是不会导致死锁的(因为进程不用阻塞等待这种资源)。

2、不可剥夺条件

不可剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。

进程获得资源只能自己释放

3、请求和保持条件

请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放

就是我已经有一个资源了,但是我还去请求另外一个资源,而另外一个资源被其他进程占有,导致自己的也释放不了

4、循环等待条件

循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。

注意!
发生死锁时一定有循环等待,但是发生循环等待时未必死锁(循环等待是死锁的必要不充分条件)
如果同类资源数大于1,则即使有循环等待,也未必发生死锁。但如果系统中每类资源都只有一个,那循环等待就是死锁的充分必要条件了。
举个例子:在哲学家进餐中每个人左手都拿起筷子,大家都去等待右手的筷子这就是同类资源数都等于1,如果来一个神父给其中一个人一只筷子那这时就是同类资源数大于1。

什么时候会发生死锁

1.对系统资源的竞争。各进程对不可剥夺的资源(如打印机)的竞争可能引起死锁,对可剥夺的资源(CPU)的竞争是不会引起死锁的。
2.进程推进顺序非法。请求和释放资源的顺序不当,也同样会导致死锁。例如,并发执行的进程P1,P2分别申请并占有了资源R1、R2,之后进程P1又紧接着申请资源R2,而进程P2又申请资源R1,两者会因为申请的资源被对方占有而阻塞,从而发生死锁。
3.信号量的使用不当也会造成死锁。如生产者-消费者问题中,如果实现互斥的P操作在实现同步的P操作之前,就有可能导致死锁。(可以把互斥信号量、同步信号量也看做是一种抽象的系统资源)

死锁的处理策略

1、预防死锁。破坏死锁产生的四个必要条件中的一个或几个





2.避免死锁。用某种方法防止系统进入不安全状态,从而避免死锁(银行家算法)

3.死锁的检测和解除。允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后采取某种措施解除死锁。

13、内部总线协议

什么是内部总线

内部总线是微机内部各外围芯片与处理器之间的总线,用于芯片一级的互连。内部总线就是我们很熟悉的了。I2C、SPI都是内部总线。

什么是外部总线

外部总线则是微机和外部设备之间的总线,例如RS-232、RS485、USB总线

1、UART

在串口通讯的协议层中,规定了数据包的内容,它由起始位,主体数据,校验位以及停止位组成,通讯双方的数据包格式以及波特率要约定一致才能正常收发数据。数据通信格式如下图:

其中各位的意义如下:

起始位:先发出一个逻辑”0”信号,表示传输字符的开始。
数据位:可以是5~8位逻辑”0”或”1”。如ASCII码(7位),扩展BCD码(8位)。小端传输
校验位:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验)比如发送字符为A = 0b01000001,如果是奇校验,现在A中有两个1了,所以我们奇校验这一位需要为1,这样加上前面两个1一共是3个,现在是奇数了。如果是偶校验,为0即可因为前面已经有两个1了是偶数个1,不需要再把校验位设为1了。
停止位:它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。
空闲位:处于逻辑“1”状态,表示当前线路上没有资料传送。

我们接收端如何知道对方要发送数据给我呢?
首先默认情况下这条线上的电平是高电平,我们要发送数据,就把起始位拉低,保持1bit时间,后面要接收8个bit数据,那我们如何能够准确的获取到呢?那就需要通过双方约定波特率才能准确的收到,并且从开始位开始的时候计时
如果使用115200波特率,8位数据位,没有校验位,1个停止位可以写成115200,8n1(n就是没有);
每一位需要的时间 t = 1 / 115200;
传输一个字节需要10位: t = 10 / 115200
每秒钟能传输 1 / t = 115200 / 10 = 11520byte

2、I2C

14、线程安全方法

14.0、同步与互斥

本章节11中介绍了同步与互斥的概念,但是更形象一点的比喻:

比如A、B两个人去上厕所;
同步可以理解成:你用完了,我才可以去用,别人用的很慢你只能等他
互斥:谁抢到了厕所就谁先用,中途不可以被打断,我用完了你才可以用,只能有一个人使用。
临界区:厕所就可以理解成临界区访问的对象。

14.1、重提volatile

在上面C语言部分介绍过volatile的内容,这里对volatile 再深入的探究一下。
volatile指示编译器每次访问变量的时候重新从主存中获得,而不是通过寄存器中的变量别名来进行访问,从而确保数据每次都重新真正的装载了。

14.2、竞争与原子操作

14.2.1 屏蔽中断方式

我们通常对于单CPU范围内避免竞态的简单有效方法就是通过关闭中断来实现,但是在驱动程序中这种方式并不推荐。
中断屏幕是的中断与进程之间的并发不再发生,并且进程调度都依赖中断来实现,因此内核的抢占与进程之间的并发也得以避免。

对于ARM处理器,通常是先原理是将CPSR中I位进行屏蔽

长时间的屏蔽中断会导致数据丢失乃至系统崩溃,并且通过进制和使能中断都只是本CPU中的,对于多核的CPU并不能去禁止另外一个核的CPU去执行中断,因此可以通过原子操作解决单一变量的互斥问题。

14.2.2 不同arm架构下原子操作处理方式

在ARMV6架构以下的处理器不支持SMP多处理器,他只需要关闭中断(防止内贼,即本CPU的工作),而>=ARMV6就涉及SMP多处理器。

SMP就是Symmetric Multi-Processors,对称多处理器;UP即Uni-Processor,系统只有一个单核CPU。

在单核下原子操作确实也就是关闭中断操作,而对于多核情况下原子操作是通过LDREX和STREX指令,可以看到多了一个EX后缀,表示独占的意思。

在ARMv6及以上的架构中,原子操作不再需要关闭中断,关中断的花销太大了。并且关中断并不适合SMP多CPU系统,你关了CPU0的中断,CPU1也可能会来执行些操作啊。
代码如下:

通常ldrex和strex配对使用,用于监控ldrex和strex之间有无其他实体存取该变量的地址。
具体例子:

① 读出:ldrex r0, [r1]
读取r1所指内存的数据,存入r0;并且标记r1所指内存为“独占访问”
如果有其他程序再次执行“ldrex r0, [r1]”,一样会成功,一样会标记r1所指内存为“独占访问”。
② 修改r0的值
③ 写入:strex r2, r0, [r1]:
如果r1的“独占访问”标记还存在,则把r0的新值写入r1所指内存,并且清除“独占访问”的标记,把r2设为0表示成功。
如果r1的“独占访问”标记不存在了,就不会更新内存,并且把r2设为1表示失败。
再通过bne 1b再次进入ldrex,最终总有一次会成功。

原子的操作可以实现单核以及多核之间的并发。

14.3、同步与锁

14.3.0 自旋锁

自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远 高于互斥锁。虽然它的效率比互斥锁高,但是也存在缺点:

  • 1、自旋锁一直占用CPU,他在未获得锁的情况下,一直运行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低
  • 2、在用自旋锁时有可能造成死锁
    当递归调用时有可能造成死锁;
    调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等都有可能引起阻塞,因此在自旋锁的占用期间不能调用这些函数。。

自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。

互斥锁与自旋锁的区别⭐⭐⭐⭐⭐

互斥锁属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和 Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞 (blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而自旋锁则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。

单CPU下的三种情况

单CPU下spin_lock退化成了禁止抢占(preempt)

1、对于单CPU系统,没有“其他CPU”;如果内核不支持preempt,当前在内核态执行的线程也不可能被其他线程抢占,也就“没有其他进程/线程”。所以,对于不支持preempt的单CPU系统,spin_lock是空函数,不需要做其他事情。

2、 如果单CPU系统的内核支持preempt,即当前线程正在执行内核态函数时,它是有可能被别的线程抢占的。这时spin_lock的实现就是调用“preempt_disable()”:你想抢我,我干脆禁止你运行。

3、既有中断抢断又支持preempt
3.1 对于spin_lock_irq(),在支持在UP系统中就退化为local_irq_disable()和preempt_disable(),如下图所示:

假设程序A要访问临界资源,可能会有中断也来访问临界资源,可能会有程序B也来访问临界资源,那么使用spin_lock_irq()来保护临界资源:先禁止中断防止中断来抢,再禁止preempt防止其他进程来抢。

3.2 对于spin_lock_bh(),在UP系统中就退化为禁止软件中断和preempt_disable();

3.3 对于spin_lock_irqsave,它跟spin_lock_irq类似,只不过它是先保存中断状态再禁止中断

多核CPU的情况

要让多CPU中只能有一个获得临界资源,使用原子变量就可以实现。但是还要保证公平,先到先得。比如有CPU0、CPU1、CPU2都调用spin_lock想获得临界资源,谁先申请谁先获得。

14.3.1 信号量

多线程同步用的,一个线程完成了某一个动作就通过信号告诉别的线程,别的线程再进行某些动作。
信号量比较简单, 不能解决优先级反转问题。见我的文章:里面讲述了优先级翻转以及互斥信号量的优先级继承问题

14.3.2 互斥量

这是多线程互斥用的,比如说,一个线程占用了某一个资源,那么别的线程就无法访问,知道这个线程离开,其他的线程才开始可以利用这个资源。进程A获得锁,就必须要进程A释放,实现多进程间的互斥,而信号量可以实现不同进程间的同步。

互斥量只能0和1,1代表资源可用。
信号量可以为非负整数。

14.3.3 条件变量

根据应用场景,互斥锁解决的是同步问题的互斥,而条件变量解决的是同步问题的等待。即生产者消费者模型,通常和互斥锁一起使用。
参考博客:注意信号量与条件变量的区别

14.3.4 读写锁

14.3.5 临界区

14.4、重提可重入与线程安全

15、冯诺依曼架构和哈弗架构

冯诺依曼架构是 数据和代码放在一起;
哈佛架构是数据和代码分开存放。

arm7:冯诺依曼架构-三级流水线
arm9:哈佛架构-五级流水线
arm11:哈佛-7级流水线

(1) 冯·诺依曼结构

冯·诺依曼结构的处理器使用同一个存储器,经由同一个总线传输。
在典型情况下,完成一条指令需要3个步骤,即:取指令、指令译码和执行指令。从指令流的定时关系也可看出冯·诺依曼结构与哈佛结构处理方式的差别。举一个最简单的对存储器进行读写操作的指令,指令1至指令3均为存、取数指令,对冯·诺依曼结构处理器,由于取指令和存取数据要从同一个存储空间存取,经由同一总线传输,因而它们无法重叠执行,只有一个完成后再进行下一个。

(2) 哈佛结构

哈佛结构是一种将程序指令存储和数据存储分开的存储器结构。

中央处理器首先到程序指令存储器中读取程序指令内容,解码后得到数据地址,再到相应的数据存储器中读取数据,并进行下一步的操作(通常是执行)。程序指令存储和数据存储分开,可以使指令和数据有不同的数据宽度。

哈佛结构是指程序和数据空间独立的体系结构, 目的是为了减轻程序运行时的访存瓶颈。
例如最常见的卷积运算中, 一条指令同时取两个操作数, 在流水线处理时, 同时还有一个取指操作, 如果程序和数据通过一条总线访问, 取指和取数必会产生冲突, 而这对大运算量的循环的执行效率是很不利的。哈佛结构能基本上解决取指和取数的冲突问题。而对另一个操作数的访问, 就只能采用Enhanced哈佛结构了, 例如像TI那样,数据区再split, 并多一组总线。 或向AD那样,采用指令cache, 指令区可存放一部分数据。

arm7系列的CPU有很多款,其中部分CPU没有内部cache的,比如arm7TDMI,就是纯粹的冯·诺依曼结构,其他有内部cache且数据和指令的cache分离的cpu则使用了哈弗结构。

(3) 改进的哈佛结构

改进型的哈佛结构与哈佛体系结构差别
与冯.诺曼结构处理器比较,哈佛结构处理器有两个明显的特点:
1).使用两个独立的存储器模块,分别存储指令和数据,每个存储模块都不允许指令和数据并存;
2).使用独立的两条总线,分别作为CPU与每个存储器之间的专用通信路径,而这两条总线之间毫无关联。

==后来,又提出了改进的哈佛结构,其结构特点为: ==
1).使用两个独立的存储器模块,分别存储指令和数据,每个存储模块都不允许指令和数据并存;
2).具有一条独立的地址总线和一条独立的数据总线,利用公用地址总线访问两个存储模块(程序存储模块和数据存储模块)。即公用数据总线被用来完成程序存储模块或数据存储模块与CPU之间的数据传输;
(3).两条总线由程序存储器和数据存储器分时共用。

嵌入式面试总结(持续更新)相关推荐

  1. JAVA面试大全(持续更新中...)

    本文旨在收集Java面试过程中出现的问题,力求全面,仅作学习交流,欢迎补充,持续更新中-,部分段落选取自网上,部分引用文章已标注,部分已记不清了,如侵权,联系本人 Java基础 1.面向对象的概述 面 ...

  2. 面试问题,持续更新...

    1.<label></label>标签在IE下无法使用,只需要在显示的标签后加上disabled="disabled"就好了; 2.被Native修饰的方法 ...

  3. 万能的 JS(万字、基础、原理、面试、持续更新。。。)

    万能的 JS 心无杂念,行路也将势如破竹. 万能的 JS JS(JavaScript 轻量级动态脚本语言) 面向对象思想: 作用域 预解释 (变量提声) 浏览器天生自带 JS 中内存的分类 JS 数据 ...

  4. 计算机基础面试(持续更新中)

    一.计算机网络 TCP/UDP TCP/IP即传输控制协议,是面向连接的协议,发送数据前要先建立连接,TCP提供可靠的服务,也就是说,通过TCP连接传输的数据不会丢失,没有重复,并且按顺序到达.(类似 ...

  5. 嵌入式 c语言 面试题,嵌入式面试题-持续更新

    1.用预处理指令#define声明一个常数,用以表示1年中有多少秒(忽略闰年问题). #define  SECONDS_PER_YEAR  (60 * 60 * 24 * 365)UL  //最后的U ...

  6. 2021React面试精选——持续更新

    目录 React的请求应该放在哪个⽣命周期中 jsx的本质是什么 React组件通信如何实现 React最新的⽣命周期是怎样的 setState到底是异步还是同步 React中keys的作用是什么 受 ...

  7. Java开发面试(持续更新)

    文章目录 一.集合 1.数组和集合的区别 2.List.Set.Map的区别 二.Vue 1.Vue常用的组件有哪些 三.数据库 1.多表查询 2.事务 四.框架 1.SpringBoot常用的注解 ...

  8. 我在深圳面试汇总(--持续更新中)

    第一家:拓保软件有限公司(福田区)(无笔试) 1.statement与preparestatement的区别! 2.你用过哪些设计模式? 3.hibernate与ibatis的区别? 4.对sprin ...

  9. java高级面试视频,持续更新~

    Java高级工程师面试:Java中反射机制的理解!反射机制的使用原理深入理解Java中的反射反射的概念反射的原理反射的主要用途反射的运用获得Class对象判断是否是某个类的实例创建实例获取方法获取构造 ...

最新文章

  1. 完整的Blender三维课程:素描到三维艺术的初学者
  2. 微软全球 AKS 女掌门人,这样击破云原生“怪圈”!
  3. 《第二章:深入了解超文本》
  4. Shell 自定义函数
  5. Intel 64/x86_64/x86/IA-32处理器操作模式/运行模式
  6. 从0基础学Python:装饰器及练习(基础)
  7. Apache+Php+Mysql配置
  8. powerdesigner安装之后会自动加载到word中怎么去除??
  9. tcp/ip通信第5期之客户机端程序
  10. 爱与光 android4.0学习
  11. 【工具使用】AI帮你写代码
  12. 路飞学城python开发ftp_路飞学城-Python开发集训-第1章
  13. 为什么手机网速太慢_为什么苹果手机的网速变慢了_苹果手机上网速度慢的解决方法-系统城...
  14. 李开复给中国大学生的第五封信—你有选择的权利
  15. SqlServer配置身份验证登录教程
  16. 设计模式笔记——代理模式
  17. 我们的指纹是如何形成的,科学家找到主导指纹形成原因
  18. 通用漏洞评分系统 (CVSS)系统入门指南
  19. 【 无线网络技术 】实验一、构建无线网络实验环境
  20. HDU 1287.破译密码

热门文章

  1. 人人都是产品经理 - 苏杰 读书笔记
  2. python实现自动化登录测试
  3. c++二分法求平方根
  4. 如何根据vin码查询_汽车VIN码是什么,怎么查询Vin码?
  5. Springboot+POI通用Excel表格导出表头样式设置方法
  6. 在线语音识别引擎及识别方法与流程
  7. Go实战--golang中使用echo框架中JSONP(labstack/echo)
  8. matlab7 fig exe 阴影,Matlab 生成完全独立运行的 EXE文件的问题请教
  9. [51单片机]按键部分(软件消抖)
  10. MongoDB磁盘空间碎片化问题排查指南