学习内容分享
C/C++部分
关键字
1、C语言中的内存,栈区和静态区
静态区(static):全局变量存储(在程序的整个生命周期都存在),
栈区(stack):局部变量存储(自动的连续内存),
堆区(heap):动态存储(内存池,非连续分配)。
2、new/delete与malloc/free区别
new、delete是C++中的操作符,而malloc和free是标准库函数。
new返回的是特定类型的指针,并且可以自动计算所申请内存的大小,而malloc需要我们计算申请内存的大小,并且在返回是强行转换为实际类型的指针。
3、sizeof和strlen有什么区别
sizeof是操作符,在计算字符串的空间大小时,包含了结束符'\0',而strlen是一个计算字符串长度的库函数,使用时需要引入头文件#include<string.h>,不包含'\0',只计算结束符'\0'之前的字符串长度。
函数
1、重载和重写有什么区别?
1、重写是子类和父类之间的关系,垂直关系;重载是同一个类方法之间的关系,水平关系;
2、重写对于函数名,参数要求是一致的;重载则是在同一个函数名下参数顺序、个数和类型有不同点;
3、重写由一个方法或者一对方法产生关系;重载则是多个方法之间的关系。
2、说一下fork, wait, exec函数?
父进程产生子进程使用fork拷贝出来一个父进程的副本,拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用写拷贝机制分配内存,exec函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。fork从父进程返回子进程的pid,从子进程返回0,调用了wait的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,执行失败则返回-1,exex执行成功则子进程从新的程序开始,无返回值,执行失败则返回-1。
数组
1、数组下标可以是负数吗?
可以,因为下标只是给出了一个当前地址的偏移量而已,只要根据这个偏移量能定位到有效目标地址就行,举个栗子:
cpp
#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 0;
}
// 输出结果为
// 0 b3ecf480
// 1 b3ecf484
// 2 b3ecf488
// 3 b3ecf48c
// 4 b3ecf490
指针
1、常量指针和指针常量区别(易混淆)
常量指针:指向常量的指针,指针所指的值不变,指向地址可变
指针常量:指针是常量,指向地址不变,指向的值可变
简而言之,const char*
限制了指针所指向的内容的修改,而 char* const
限制了指针本身的修改。你可以通过检查const
关键字的位置来确定是哪种限制:如果const
在*
之前,那么指针指向的内容是常量;如果const
在*
之后(即在指针类型之后),那么指针本身是常量。
容器和算法
1、vector和list的区别是什么?
1 vector底层实现是数组;list是双向链表;
2 vector支持随机访问,list不支持;
3 vector在中间结点进行插入删除会导致内存拷贝,list不会;
4 vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好 。
2、STL中迭代器有什么作用?有指针为何还要迭代器?
1、迭代器(Iterator)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素,而又不需要暴露该对象的内部表示,迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用*取值后的值而不是输出其本身。
2、迭代器和指针的区别:迭代器不是指针,是类模板,表现的像指针,迭代器封装了指针,重载了指针的一些操作符,->、*、++、--等,可遍历容器内全部或部分元素的对象,相当于一种智能指针,可以根据不同类型的数据结构来实现不同++,--的操作。
裸机开发
STM32中HAL库和寄存器开发区别?
在STM32中,使用寄存器编程和使用HAL(硬件抽象层)库编程是两种常见的编程方式,它们各有优缺点。
使用寄存器编程是指直接对微控制器的寄存器进行操作以控制硬件。这意味着开发者需要直接读取和写入硬件的寄存器地址。
优点:
- 性能高:由于直接操作硬件,减少了中间层的开销,因此代码执行速度更快。
- 灵活性:提供了对硬件的完全控制,可以实现特定的硬件优化。
缺点:
- 开发难度大:需要深入了解硬件的结构和寄存器地址,编写代码时容易出错。
- 可移植性差:不同的微控制器可能有不同的寄存器结构和地址,因此代码不易移植到其他平台。
使用HAL库编程,HAL库提供了一种硬件抽象的方式,使开发者可以通过函数调用来操作硬件,而无需关心具体的寄存器地址。
优点:
- 开发简单:提供了易于使用的API,降低了开发难度。
- 可移植性好:HAL库通常支持多种微控制器,因此代码可以很容易地移植到其他平台。
- 易于维护:由于使用了抽象层,当硬件发生变化时,只需要修改底层实现,而无需修改上层代码。
缺点:
- 性能开销:由于使用了中间层,相对于直接操作寄存器,可能会有一定的性能开销。
- 灵活性受限:HAL库提供的是通用的API,可能无法实现某些特定的硬件优化。
举个例子
假设我们要在STM32上设置一个GPIO引脚的输出值。
使用寄存器编程:
c
// 假设GPIOA的基址是GPIOA_BASE
#define GPIOA_ODR (*((volatile uint32_t *)(GPIOA_BASE + 12)))
void set_gpio_output(uint16_t pin, uint8_t value) {
if (value) {
GPIOA_ODR |= (1 << pin);
} else {
GPIOA_ODR &= ~(1 << pin);
}
}
使用HAL库编程:
c
#include "stm32f1xx_hal.h"
void set_gpio_output(GPIO_TypeDef* GPIOx, uint16_t pin, uint8_t value) {
if (value) {
HAL_GPIO_WritePin(GPIOx, pin, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(GPIOx, pin, GPIO_PIN_RESET);
}
}
在上面的例子中,使用寄存器编程需要直接操作硬件的寄存器地址,而使用HAL库编程则通过调用HAL_GPIO_WritePin
函数来实现相同的功能。可以看到,使用HAL库编程的代码更加简洁和易于理解,同时也提高了代码的可移植性。然而,如果你对硬件有深入的了解,并且需要实现特定的硬件优化,那么使用寄存器编程可能更加合适。
驱动开发
说说bootloader?
BootLoader是在操作系统内核运行之前运行的一段小程序,主要用于初始化硬件设备、建立内存空间映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核准备好正确的环境。在嵌入式系统中,由于通常并没有像BIOS那样的固件程序,因此整个系统的加载启动任务就完全由BootLoader来完成。
BootLoader的启动过程可以是单阶段的,也可以是多阶段的。多阶段的BootLoader通常能提供更为复杂的功能和更好的可移植性。它通常分为stage1和stage2两大部分,stage1一般使用汇编语言编写,以达到短小精悍的目的;而stage2则常用C语言编写,以提高代码的可读性和可移植性。
另外,BootLoader的具体实现还依赖于CPU的体系结构。例如,在一个基于ARM7TDMI core的嵌入式系统中,系统在上电或复位时通常都从地址0x00000000处开始执行,而在这个地址处安排的通常就是系统的BootLoader程序。
总的来说,BootLoader在嵌入式系统和计算机启动过程中扮演着至关重要的角色,负责初始化硬件、设置环境,并最终加载并启动操作系统或特定的程序。
bootloader启动为什么要开启icache和关闭dcache和tlb?
在bootloader启动阶段,需要开启icache(指令缓存)和关闭dcache(数据缓存)以及tlb(转换后备缓冲器)的原因与它们各自的功能和特性有关。
首先,icache用于存储CPU最近访问过的指令,使得CPU在再次需要这些指令时能够直接从缓存中快速获取,而不是从主存中读取,从而大大提高了指令的访问速度。在bootloader启动阶段,CPU需要频繁地访问指令来执行各种初始化操作,因此开启icache可以显著提高启动速度。
其次,dcache用于存储CPU最近访问过的数据,它的主要目的是加速数据的访问。然而,在bootloader启动阶段,由于内存尚未完全初始化,数据缓存可能会导致数据访问异常,因此通常需要关闭dcache以避免潜在的问题。
至于tlb,它是用于缓存虚拟地址到物理地址的映射关系的。然而,在bootloader阶段,地址映射尚未建立,因此tlb并无实际作用,而且开启tlb可能会增加系统复杂性并降低效率。因此,关闭tlb是合理的选择。
总的来说,开启icache和关闭dcache、tlb在bootloader启动阶段是为了优化启动性能、避免潜在问题并简化系统配置。这些设置确保了CPU能够高效、稳定地执行启动过程中的各项任务。
初始化一个串口、检测系统内存映射、将内核映像和根文件系统映像从flash读到SDRAM空间中、为内核设置启动参数以及调用内核是Bootloader在执行其第二阶段任务时的主要步骤。
- 初始化串口
目的:串口(通常称为UART,即通用异步收发器)是一种用于调试和系统日志输出的常见接口。初始化串口是为了让Bootloader能够通过串口输出信息,从而方便开发者了解启动过程中的状态和错误信息。
步骤:
- 配置串口通信参数,如波特率、数据位、停止位和校验位。
- 启用串口中断(如果需要的话)。
- 测试串口是否工作正常。
- 检测系统内存映射
目的:确定系统的物理内存布局和大小,以便Bootloader和内核知道哪些地址范围是可用的RAM空间。
步骤:
- 读取存储在特定位置的内存映射信息(这些信息通常由硬件平台或引导程序提供)。
- 解析这些信息,获取RAM的起始地址和大小。
- 将这些信息保存在合适的数据结构中,供后续使用。
- 将内核映像和根文件系统映像从Flash读到SDRAM空间中
目的:在启动过程中,操作系统内核和根文件系统通常存储在非易失性存储器(如Flash)中。为了执行这些程序,需要将它们加载到RAM(通常是SDRAM)中。
步骤:
- 读取Flash中的内核映像和根文件系统映像。
- 将这些数据块复制到SDRAM中的适当位置。
- 验证数据是否正确复制(通过校验和或其他机制)。
- 为内核设置启动参数
目的:内核在启动时需要一系列参数,如命令行选项、内存布局、设备信息等。这些参数通常由Bootloader提供。
步骤:
- 创建一个参数结构或表,包含内核所需的各种参数。
- 将这些参数放置在内存中一个预定的位置(通常是内核启动后能够访问到的地址)。
- 确保内核启动时能够找到并解析这些参数。
- 调用内核
目的:将控制权从Bootloader转移到操作系统内核,开始执行内核的初始化代码。
步骤:
- 设置CPU到正确的模式或状态,以准备执行内核代码。
- 跳转到内核的入口点(即内核映像在内存中的起始地址)。
- 一旦跳转到内核,Bootloader的任务就完成了,接下来的工作由内核负责。
这些步骤是Bootloader在启动操作系统之前必须完成的关键任务。它们确保了硬件得到适当的配置,操作系统映像被正确加载,并且内核能够接收到必要的信息来成功启动。每个步骤的正确执行都是系统稳定启动的关键。
网络编程
管道(Pipe)
管道(Pipe):管道是一种进程间通信的方式,可以在不同进程之间传递数据。可以使用管道函数如pipe()、read()和write()等来实现进程间通信。
在Unix和Linux中,管道通常通过以下系统调用进行创建和使用:
pipe()
:这个函数用于创建一个管道。它接受一个文件描述符数组作为参数,并返回两个文件描述符:一个用于读取(通常称为读端),另一个用于写入(通常称为写端)。read()
和write()
:这两个函数分别用于从管道中读取数据和向管道中写入数据。read()
函数从指定的文件描述符(在这种情况下是管道的读端)读取数据,而write()
函数则将数据写入指定的文件描述符(在这种情况下是管道的写端)。
需要注意的是,管道是半双工的,也就是说,数据只能在一个方向上流动。此外,管道只能用于具有共同祖先的进程之间(例如,一个父进程和它的子进程)。
对于需要全双工通信或在不相关的进程之间进行通信的场景,可能需要使用其他类型的IPC机制,如套接字(Sockets)、共享内存(Shared Memory)或消息队列(Message Queues)等。
总的来说,管道是一种简单而有效的进程间通信方式,特别适用于需要在具有共同祖先的进程之间传递数据的场景。
套接字(Socket)
套接字的工作原理可以类比为电话系统中的电话线和电话机,每个套接字都类似于一个电话机,而网络则类似于电话线。通过套接字,进程可以发送和接收数据,就像通过电话机进行通话一样。
在使用套接字进行通信时,通常涉及到以下几个关键步骤和函数:
- 创建套接字 :使用
socket()
函数创建一个新的套接字。这个函数接受三个参数:地址族(如AF_INET表示IPv4)、套接字类型(如SOCK_STREAM表示TCP套接字)和协议类型(通常为0,表示使用默认协议)。 - 绑定地址和端口 :对于服务器端套接字,需要使用
bind()
函数将其绑定到一个特定的地址和端口上。这样,客户端就可以通过这个地址和端口来连接到服务器。 - 监听连接请求 :对于服务器端套接字,还需要使用
listen()
函数来监听来自客户端的连接请求。这个函数指定了最大连接队列长度,即可以同时等待多少个客户端的连接请求。 - 建立连接 :对于客户端套接字,使用
connect()
函数来建立与服务器的连接。这个函数接受服务器的地址和端口作为参数。 - 发送和接收数据 :一旦连接建立,就可以使用
send()
和recv()
(或sendto()
和recvfrom()
对于无连接套接字)等函数来发送和接收数据了。这些函数允许在连接的双方之间传输字节流。 - 关闭套接字 :通信完成后,使用
close()
函数来关闭套接字,释放资源。
需要注意的是,套接字的通信模式可以是面向连接的(如TCP),也可以是无连接的(如UDP)。面向连接的套接字在发送和接收数据之前需要先建立连接,而无连接的套接字则不需要。此外,套接字还支持多种不同的通信选项和配置,可以根据具体需求进行设置。
应用开发
进程等于程序吗?
进程(Process)并不完全等同于运行的程序。进程是操作系统进行资源分配和调度的基本单位,是程序执行时的一个实例。换句话说,进程是程序的一次执行过程,是动态的概念,是程序在执行过程中分配和管理资源的基本单位。
当我们运行一个程序时,操作系统会为其分配相应的资源(如内存空间),并创建一个进程来执行这个程序。这个进程会包含程序的代码、数据以及执行上下文等信息。因此,进程可以看作是程序在操作系统中的一次具体执行。
然而,程序本身只是一组静态的指令和数据,它存储在硬盘等存储介质中,只有当它被加载到内存中并由操作系统创建为一个进程后,它才能被执行。所以,进程是程序执行的一个动态实体,而程序是进程执行的静态基础。
总结来说,进程是运行的程序的一个实例,是程序在执行过程中的一个动态表现。进程和程序在概念上是有所区别的,但它们是密切相关的。