目录
python3.6中下列程序的输出是什么,具体的题目记不清楚了,在这里补充一下python2 和python3在输出上的区别
C语言中的malloc/free与C++中的new/delete的区别
sizeof/strlen?在对字符串数组求sizeof的时候算不算最后的\0?
用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个。
嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。
进程通信有哪些,如果我需要传输大量数据使用那种方式效率最高。
项目介绍需要复习的点
任务:
1、了解一下安卓系统的input子系统, led子系统
2、自己做的基于安卓开发的应用(智能家居)流程,需要更细致的了解
3、图像方面jni调用在熟悉一下
4、飞控PID算法需要了解一下
4、最后就是串口通信这块I^c/uart需要在巩固一下
1、“挑战杯”科技作品竞赛-智能窗户控制系统
简介:通过传感器获取室内外环境情况,控制智能窗户的状态,远程视频监控、
定时开关窗户,实时记录用户对窗的使用习惯,智能调节窗户开闭。
负责在 RT5350 平台,运行 openwrt 系统,接收来自 zigbee 网关获取温湿度 /CO2 传感器/人体感应信息,使用 mjpg_streamer 开源项目将摄像头采集到的 视频数据通过网络传输到客户端,实现视频监控。(原理:从摄像头采集图 像,并把他们已流的形式,通过基于 IP 的网络传输到安卓端)。在 Android stdio 平台开发安卓应用,接收远程服务器的数据,实现远程监控家庭情况;
问题:mjpg_streamer项目介绍/数据怎么传输到服务器/
概念:在Linux上运行的视频服务器,可以将摄像头采集到的视频数据通过网络传输到客户端,实现视频监控
参考:https://blog.csdn.net/yi412/article/details/37649641
输出插件的实现是一个http服务器
主要结构:mjpg_streamer主要由三部分构成,主函数mjpg_streamer.c和输入、输出组件,其中输入、输出组件通常是input_uvc.so和output_http.so,他们是以动态链接库的形式在主函数中调用的。
主函数的主要功能有:
1.解析命令行的各个输入参数。2.判断程序是否需要成为守护进程,如果需要,则执行相应的操作。3.初始化global全局变量。4.打开并配置输入、输出插件。5.运行输入、输出插件。
输入插件将采集到的视频送到全局缓存中,输出插件则从全局缓存中读取数据并发送。输出插件的实现是一个http服务器,这里就不介绍了。输入插件的视频采集功能主要是通过Linux的V4L2接口( https://blog.csdn.net/Jfuck/article/details/8169352)实现的,主要是4个函数input_init()、 input_stop()、 input_run()和 input_cmd()。其中iniput_run()函数创建了线程cma_thread(),这个线程很重要,该函数的作用是抓取一帧的图像,并复制到全局缓冲区。
zigbee
ZigBee技术是一种近距离、低复杂度、低功耗、低速率、低成本的双向无线通讯技术。
主要用于距离短、功耗低且传输速率不高的各种电子设备之间进行数据传输以及典型的有周期性数据、间歇性数据和低反应时间数据传输的应用。
zigbee网络提供3种网络设备类型,分别是协调器(Coordinator)、路由器(Router)和终端节点(EndDevice)。协调器是一个全功能设备,在整个网络中的权限最高,功能最强大。它是一个网络的建立者,维护整个zigbee网络。在协议栈中,每个源文件都可以定义多个功能设备的实现函数。它们都通过编译来实现各个设备的需要
- 基本概念
ZigBee技术是一种近距离、低复杂度、低功耗、低速率、低成本的双向无线通讯技术。
主要用于距离短、功耗低且传输速率不高的各种电子设备之间进行数据传输以及典型的有周期性数据、间歇性数据和低反应时间数据传输的应用。
zigbee网络提供3种网络设备类型,分别是协调器(Coordinator)、路由器(Router)和终端节点(EndDevice)。协调器是一个全功能设备,在整个网络中的权限最高,功能最强大。它是一个网络的建立者,维护整个zigbee网络。在协议栈中,每个源文件都可以定义多个功能设备的实现函数。它们都通过编译来实现各个设备的需要
- Zstack系统结构
- 程序运行框图
安卓端开发
- WIFI通信( FlashActivity.java)
(1)使用WIFI传输数据之前,首先要到AndroidManifest.xml文件里面声明wifi权限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
(2)获得wifi id和port
ipEdt = (AutoCompleteTextView) findViewById(R.id.ip);
portEdt = (EditText) findViewById(R.id.port);
wifi = (WifiManager) getSystemService(Context.WIFI_SERVICE);//获得系统wifi服务
(3)通过使用SharedPreferences存储录入的ip地址以及端口号
sp = getSharedPreferences("config", MODE_PRIVATE);
/* 创建好配置文件后,以后就可以用它的edit来操作配置文件了 */
editor = sp.edit();
String names[] = sp.getString("ip", "").split(":");//获得上次输入的ip地址
//将数组或List集合的多个值包装成多个列表项,使用的android自带的//android.R.layout.simple_list_item_1
ArrayAdapter<String> adapter = new ArrayAdapter<String>(mContext,
android.R.layout.simple_dropdown_item_1line, names);
ipEdt.setAdapter(adapter);
2、图像jni调用流程
使用 NDK,通过 JNI 的方式来调用 C++ 的方法。
流程:
Gradle 调用您的外部构建脚本 CMakeLists.txt。
CMake 按照构建脚本中的命令将 C++ 源文件 native-lib.cpp 编译到共享的对象库中,并命名为 libnative-lib.so,Gradle 随后会将其打包到 APK 中。
运行时,应用的 MainActivity 会使用 System.loadLibrary() 加载原生库。现在,应用可以使用库的原生函数 stringFromJNI()。
MainActivity.onCreate() 调用 stringFromJNI(),这将返回“Hello from C++”并使用这些文字更新 TextView。
要手动配置 Gradle 以关联到您的原生库,需要将 externalNativeBuild {} 块添加到模块级 build.gradle 文件中,并使用 cmake {} 或 ndkBuild {} 对其进行配置。
概念:
JNI:JNI是一套编程接口,用来实现Java代码与本地的C/C++代码进行交互;
NDK: NDK是Google开发的一套开发和编译工具集,可以生成动态链接库,主要用于Android的JNI开发
首先是将写好的C/C++代码编译成对应平台的动态库(windows一般是dll文件,linux一般是so文件等),
流程:
1、使用C++11扩展:采用cmake编译.so文件的方法,所以相当于用java接口去调用c++,使用NDK技术, 多个cpp文件,下面也多了一个CMakeLists.txt
2、MobileNetssd.cpp:加载转成ncnn的MobileNetSSD_deploy.id.h等模型参数/把像素转换成data,并指定通道顺序/根据提供的*id.h文件加载ncnn网络,输出结果xywh/
3、CMakeLists.txt修改:添加ncnn for android 的包/在add library中 MobileNetssd为生成.so的文字最好直接和.cpp名字一样,需要更改/在target_link_librarie以下三个都要添加MobileNetssd 、ncnn_lib这个ncnn的lib的add、jnigraphics#这个jni也需要add/
4、build.gradle修改:cmake添加C++11多线程和手机硬件架构armeabi-v7a/需要添加 把 .a文件导入"src/main/jniLibs"src/cpp
5、编写java接口:命名为和.cpp文件一样的名称这里就是MobileNetssd.java
package com.example.che.mobilenetssd_demo;
import android.graphics.Bitmap;
/**
* MobileNetssd的java接口,与本地c++代码相呼应 native为本地 此文件与 MobileNetssd.cpp相呼应
*/
public class MobileNetssd {
public native boolean Init(byte[] param, byte[] bin); // 初始化函数
public native float[] Detect(Bitmap bitmap); // 检测函数
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("MobileNetssd");//最后
}
}
- public native boolean Init(byte[] param, byte[] bin); // 初始化函数 对应于NDK编写的.cpp文件中的JNIEXPORT jboolean JNICALL
- Java_com_example_che_mobilenetssd_demo_MobileNetssd_Init(JNIEnv *env, jobject obj, jbyteArray param, jbyteArray bin)
- public native float[] Detect(Bitmap bitmap); // 检测函数 对应于NDK编写的.cpp文件中的JNIEXPORT jfloatArray JNICALL Java_com_example_che_mobilenetssd_demo_MobileNetssd_Detect(JNIEnv* env, jobject thiz, jobject bitmap)
其实有规律可寻以第二个为例子
函数前三个都是JNIEXPORT +函数返回类型(NDK形式)+JNICALL
Java_com_example_che_mobilenetssd_demo_MobileNetssd_Detect(JNIEnv* env, jobject thiz, jobject bitmap)
对于这个命名先看下面这个图你可能就懂了
相当于绝对路径那种感觉了显示java文件夹,再是com.example.che.mobilenetssd_demo这个包再是MobileNetssd.java文件最后是 java接口文件中的public native float[] Detect(Bitmap bitmap); 函数的文件名,即Java_com_example_che_mobilenetssd_demo_MobileNetssd_Detect,后面参数则为
(JNIEnv* env, jobject thiz,+原函数的函数参数(NDK类型格式))
其实正常来讲NDK这种不应该是先写cpp再写java接口而是应该先写java接口再利用IDE本身带有的NDK的开发环境可以直接双点写的java接口的函数(函数格式如上需要加一个native)之后快捷键按ALT+ENTER即可直接在cpp文件中添加成功,具体请看NDK技术即可,这里不赘余了。
接下来就是我的其他.java文件以及UI的XML文件了
6、MainActivity.java:java接口实例化,利用java函数调用NDK c++函数/MobileNetssd初始化,也就是把model文件进行加载,用io流读取二进制文件,最后存入到byte[]数组中/将文件传入java的NDK接口(c++ 代码中的init接口)/图像输入处理,调用mobileNetssd.Detect(input_bmp);方法得到目标坐标xywh,画出目标框/build之后得到自己的这个文件夹的对应的编译硬件架构的文件夹下成功生成.so文件,名字为libMobileNetssd.so
3、飞控PID算法
控制中心单片机通过IMU陀螺仪加速度计(MPU6050等等)获取四旋翼的角度(俯仰、横滚和偏航)的相对基准角度变化、然后滤波(卡尔曼滤波等等)处理获得方向余弦矩阵和四元数得到欧拉角、使用PID控制或者PI,PD控制(P比例I积分D微分)将系统反馈值和期望值进行比较、并根据偏差不断修复、直至达到期望的预定值。P的作用是加快系统达到预期的速度;I的作用是消除净差;D有阻尼的作用、就是阻止系统突变。
通过PID自动控制算法处理、输出期望的PWM波给四个电调、控制四个无刷电机的转速、从而得到一个期望的力控制四旋翼的前后左右上下飞行。
卡尔曼滤波原理:https://blog.csdn.net/tiandijun/article/details/72469471
四元数原理:https://www.zhihu.com/question/23005815
4、安卓系统的input子系统, led子系统
input子系统:https://www.cnblogs.com/haiming/p/3318614.html
led子系统:https://blog.csdn.net/qq_23327993/article/details/86520216
5、串口通信
串口通信
参考:https://blog.csdn.net/huwei2003/article/details/36418471
串口是串行接口(serial port)的简称,也称为串行通信接口或COM接口。
串口通信是指采用串行通信协议(serial communication)在一条信号线上将数据一个比特一个比特地逐位进行传输的通信模式。串口按电气标准及协议来划分,包括RS-232-C、RS-422、RS485等。
IIC
参考:https://blog.csdn.net/u010650845/article/details/73467586
概念:由数据线 SDA和时钟线SCL两根线构成的串行总线,可发送和接收数据。在CPU与被控IC之间、IC与IC之间进行双向传送
IIC数据传输速率有标准模式(100 kbps)、快速模式(400 kbps)和高速模式(3.4 Mbps),另外一些变种实现了低速模式(10 kbps)和快速+模式(1 Mbps)
uart串口通信
参考:https://blog.csdn.net/weixin_43664511/article/details/103038303
UART是一种通用串行数据总线,用于异步通信。该总线双向通信,可以实现全双工传输和接收。串口的通信方式为串行通信,按位发送和接收字节,将并行数据转换为串行数据流发送出去,将接受的串行数据流转换为并行数据。完成串口通信,只需要三根线,接收(rx)、发送(tx)和地线。确定通信双方的波特率和数据格式一致,是实现串口通信的前提。=0 BY-SA版权协议
算法题
给你一个单链表的链表头,实现链表的排序,说出具体过程
参考:https://blog.csdn.net/baidu_30000217/article/details/77823084
交换节点:插入排序,冒泡排序,简单选择排序
交换数据:快速排序
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
//节点结构
struct node
{
int val;
struct node * next;
};
typedef struct node node, * list;
//打印函数
void printList(list mylist);
//排序函数
//插入排序
void insertSort(list mylist);
//冒泡排序
void bubbleSort(list mylist);
//简单选择
void selectSort(list mylist);
//快速排序
void quickSort(list mylist);
int main(void)
{
int arr[] = {5, 1, 7, 4, 2, 9, 6, 3, 8};
//程序都是针对有头结点的单向链表
list mylist = (list)malloc(sizeof(node));
mylist -> val = 0;
mylist -> next = NULL;
int len = sizeof(arr) / sizeof(arr[0]);
int i = 0;
node * cur = mylist;
while(i < len)
{
node * newNode = (node *)malloc(sizeof(node));
newNode -> val = arr[i];
newNode -> next = NULL;
cur -> next = newNode;
cur = cur -> next;
i ++;
}
//insertSort(mylist);
//bubbleSort(mylist);
//selectSort(mylist);
quickSort(mylist);
printList(mylist);
return 0;
}
void printList(list mylist)
{
node * cur = mylist -> next;
while(cur != NULL)
{
printf("%d ", cur -> val);
cur = cur -> next;
}
printf("\n");
}
插入排序
//=============插入排序====================
void insertSort(list mylist)
{
if((mylist -> next == NULL) || (mylist -> next -> next == NULL))
{
return;
}
node * head, * p1, * prep1, * p2, * prep2, * temp;
head = mylist;
prep1 = head -> next;
p1 = prep1 -> next;
//prep1和p1是否需要手动后移
bool flag;
while(p1 != NULL)
{
flag = true;
temp = p1;
//由于是单向链表,所以只能从头部开始检索
for(prep2 = head, p2 = prep2 -> next; p2 != p1; prep2 = prep2 -> next, p2 = p2 -> next)
{
//发现第一个较大值
if(p2 -> val > p1 -> val)
{
p1 = p1 -> next;
prep1 -> next = p1;
prep2 -> next = temp;
temp -> next = p2;
flag = false;
break;
}
}
//手动后移prep1和p1
if(flag)
{
prep1 = prep1 -> next;
p1 = p1 -> next;
}
}
}
//=============插入排序====================
冒泡排序
//=============冒泡排序====================
void bubbleSort(list mylist)
{
if((mylist -> next == NULL) || (mylist -> next -> next == NULL))
{
return;
}
node *head, * pre, * cur, *next, * end, * temp;
head = mylist;
end = NULL;
//从链表头开始将较大值往后沉
while(head -> next != end)
{
for(pre = head, cur = pre -> next, next = cur -> next; next != end; pre = pre -> next, cur = cur -> next, next = next -> next)
{
//相邻的节点比较
if(cur -> val > next -> val)
{
cur -> next = next -> next;
pre -> next = next;
next -> next = cur;
temp = next;
next = cur;
cur = temp;
}
}
end = cur;
}
}
//=============冒泡排序====================
快速排序:https://blog.csdn.net/u012658346/article/details/51141288
C/C++实现strcpy和strcat两个功能
strcpy实现
char* myStrcpy(char* pre, const char* next)
{
if (pre == nullptr || next == nullptr) //空指针直接返回
{
return nullptr;
}
if (pre == next) // 两者相等也无需拷贝了
return pre;
while ((*pre++ = *next++) != '\0'); // 依次赋值给主字符数组
return pre;
}
//另一种形式,这种标准点
char* _strcpy(char* dest, const char* src) {
assert(dest != NULL && src != NULL);
char* temp = dest;
while (*src != '\0')
{
*dest = *src;
dest++;
src++;
}
*dest = '\0';
return dest;
}
int main() {
char s2[] = "efieji";
char s1[] = "123";
_strcpy(s2, s1);
cout << s2 << endl;
cout << strlen(s2) << endl;
}
strcat实现
char* myStrcat(char* pre, const char* next)
{
if (pre == nullptr || next == nullptr) // 如果有一个为空指针,直接返回pre
return pre;
char* tmp_ptr = pre + strlen(pre); //strlen计算字符数,需要包含都文件string.h,当然也可以自己实现
while ( (*tmp_ptr++ = *next++) != '\0'); // 依次接着赋值
return pre;
}
#include<iostream>
#include<string>
#include<string.h>
using namespace std;
char* myStrcat(char* pre, const char* next)
{
if (pre == nullptr || next == nullptr)
return pre;
char* tmp_ptr = pre + strlen(pre);
while ( (*tmp_ptr++ = *next++) != '\0');
return pre;
}
char* myStrcpy(char* pre, const char* next)
{
if (pre == nullptr || next == nullptr)
{
return nullptr;
}
if (pre == next)
return pre;
while ((*pre++ = *next++) != '\0');
return pre;
}
int main()
{
char str1[100] = "12345";
char str2[20] = "hello world";
myStrcat(str1, str2);
myStrcpy(str1, str2);
printf("%s\n", str1);
return 0;
}
求矩阵对角线的和
求n阶方阵的对角线元素之和。编写主程序,用户输入矩阵的阶数n,动态申请n*n的存储空间,再输入n行、n列的元素,调用函数求矩阵的对角元素之和,在主函数中输出这个和。设元素均为整数。n>=1。
//编写一个程序输入一个n*n的矩阵,求出两条对角线元素值之和
#include <iostream>
using namespace std;
int main() {
int num;
cout << "请输入对角矩阵的大小" << endl;
cin >> num;
int **p = new int*[num];//数组大小动态,二维
for (int i = 0; i <num; i++)
p[i] = new int[num];
int sum = 0;
int number;
for (int i = 0; i < num; i++) {
cout << "请输入矩阵元素";
cout << "第" << (i + 1) << "行" << endl;
for (int j = 0; j < num; j++){
cout << "第" << (j + 1) << "列" << endl;
cin >> number;
p[i][j] = number;
}
}
for (int i = 0; i < num; i++) {
sum += p[i][i] + p[i][num-i-1];//两个对角线元素相加
}
cout << "sum=" << sum << endl;
return 0;
}
C/C++/Python
python中set dict list tuple的区别
序列是Python中最基本的数据结构。序列中的每个元素都分配一个数字 - 它的位置,或索引,第一个索引是0,第二个索引是1,依此类推。Python有6个序列的内置类型,但最常见的是列表list和元组tuple。具体的不想讲了,就总结一下吧,已经很晚了呢。
各自的特点:
list(列表):有序集合,随时增删
set(集合):无序集合、key不重复
tuple(元组):有序列表,一旦初始化,无法修改
dict(字典):键值对(key-value)方式存储,查找速度快
总结:
1、list、tuple是有序列表;dict、set是无序列表
2、list元素可变、tuple元素不可变
3、dict和set的key值不可变,唯一性
4、set只有key没有value
5、set的用途:去重、并集、交集等
6、list、tuple:+、*、索引、切片、检查成员等
7、dict查询效率高,但是消耗内存多;list、tuple查询效率低、但是消耗内存少
一个C++源文件从文本到可执行文件经历的过程
1).预处理,产生.ii文件
2).编译,产生汇编文件(.s文件)
3).汇编,产生目标文件(.o或.obj文件)
4).链接,产生可执行文件(.out或.exe文件)
C/C++内存分布?堆栈的区别?
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。
堆,就是那些由 new 分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个 new 就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。堆可以动态地扩展和收缩。
自由存储区,就是那些由 malloc 等分配的内存块,他和堆是十分相似的,不过它是用 free 来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的 C 语言中,全局变量又分为初始化的和未初始化的(初始化的全局变量和静态变量在一块区域,未初始化的全局变量与静态变量在相邻的另一块区域,同时未被初始化的对象存储区可以通过 void* 来访问和操纵,程序结束后由系统自行释放),在 C++ 里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多
堆和栈的区别
首先是管理方式不同,堆由程序员负责申请和释放,栈是编译器负责的,然后是结构不同,堆是一种从底部往顶部扩展的结构,栈是从顶部到底部刚好相反的结构,最后是效率有很大的不同,堆的申请和释放都需要经过算法计算,因为要减少内存碎片和提高内存使用率,而栈由编辑器负责,速度非常快,在这点上堆的效率比较低
python3.6中下列程序的输出是什么,具体的题目记不清楚了,在这里补充一下python2 和python3在输出上的区别
1.python3中print是一个内置函数,有多个参数,而python2中print是一个语法结构;
2.Python2打印时可以不加括号:print 'hello world', Python3则需要加括号 print("hello world")
3.Python2中,input要求输入的字符串必须要加引号,为了避免读取非字符串类型发生的一些行为,不得不使用raw_input()代替input()
C语言中的malloc/free与C++中的new/delete的区别
new/delete是C++的操作符,而malloc/free是C中的函数。new做两件事,一是分配内存,二是调用类的构造函数;同样,delete会调用类的析构函数和释放内存。而malloc和free只是分配和释放内存。new建立的是一个对象,而malloc分配的是一块内存;new建立的对象可以用成员函数访问,不要直接访问它的地址空间;malloc分配的是一块内存区域,用指针访问,可以在里面移动指针;new出来的指针是带有类型信息的,而malloc返回的是void指针。new/delete是保留字,不需要头文件支持;malloc/free需要头文件库函数支持。
具体看下面:
1.所属语言
new是C++特性,malloc是C的。C++一般使用的new,但也可以使用malloc,而C用malloc、realloc、calloc。
2.申请释放方式
new和delete,malloc和free配对使用。new的使用比malloc简单,内部已经实现了大小的计算、类型转换等工作,而malloc使用时需要计算大小及进行类型转换。
3.malloc是标准库函数,new是C++的运算符。
new可以被重载,但malloc不可以,malloc需要库函数的支持,new不需要。
4.构造与析构
new和delete会自动调用构造函数和析构函数,但是malloc和free不会。
5.申请内存失败
申请内存失败,默认new抛出异常,malloc返回NULL。
6.重新分配内存
malloc可利用realloc重新分配内存,new不可以。
7.类型安全性
new会检查类型是否对应,如果不对应会保存,但malloc只关注申请内存的多少,不会检查类型。
8.类型转换
malloc返回的类型是void,所以在调用malloc时要进行显式的类型转换,将void转换成所需的指针类型,new不需要。
9.数组分配
new有明确的方式处理数组的分配,即new[],释放也有delete[],malloc没有。
10.设置内存分配器
new可以设置自己的内存分配器,malloc不可以。
堆栈是什么?局部变量和全局变量的存放位置
申请方式
栈:由系统自动分配,例如,生命在函数中一个局部变量 int b;系统自动在栈中为b开辟空间;
堆:由程序员自己申请,并指明大小。例如:
在C中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。
申请后系统的反应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
申请效率的比较
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。
存储内容上
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
sizeof/strlen?在对字符串数组求sizeof的时候算不算最后的\0?
sizeof
a---数组名单独出现,代表整个数组。int a[5] = {1,2,3,4};printf("%d\n",sizeof(a));//20
&a—表示的是整个数组的地址(地址的大小为4,单位为字节。)printf("%d\n",sizeof(a+0));//4 printf("%d\n",sizeof(&a+1));//4
解引用 *&a---代表整个数组的内容printf("%d\n",sizeof(*&a));//20,*&a---代表整个数组的内容,所以为5*4
strlen
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));//随机值
因为在此数组中没有’ \0 ’,函数无法知道在哪里停下来。printf("%d\n",strlen(&arr+1));//随机值-6.但&数组名加上其他操作符—代表整个数组的地址。再接着执行。跳过整个数组,所以大小差6.
char arr[] = "abcdef";
printf("%d\n", strlen(arr));//6
printf("%d\n", strlen(arr+0));//6
二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));//48 a---数组名单独出现,代表整个数组。一共有12个元素,每个元素为4字节。12*4
printf("%d\n",sizeof(*a));//16二维数组降级变为一维数组,成为第一行。所以第一行内容的大小为4*4
printf("%d\n",sizeof(a[3]));//16表示第三行的数组长度,元素个数为4,4*4
在对字符串数组求sizeof的时候算不算最后的\0?答案是:算。
sizeof 运算符能得出静态数组的长度。与'\0'没有关系的。
与'\0' 相关的是strlen函数 遇到'\0'就结束了。可以查看strlen实现。
举个栗子:
char c[] = "abcde"
这是一个字符串常量,会隐式的在末尾加上空字符'\0';
sizeof 计算出来的是 6
strlen 算出来是5
函数指针
参考:https://blog.csdn.net/qq_28386947/article/details/73480831?depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-4&utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-4
函数指针: 是指向函数的指针变量
在程序运行中,函数是程序算法指令部分,他们和数组一样也占用内存空间,也都有相应的地址。我们程序员可以使用指针变量指向数组的首地址,同样,也可以使用指针变量指向函数代码的首地址, 指向函数代码首地址的指针变量,称为函数指针。
函数指针有两个用途:调用函数和做函数的参数
函数指针和指针函数的区别:函数指针是一个指向函数的指针。他的本质是一个指针,而指针函数只是说明他是一个返回值为指针的函数,它本质是一个函数,
类与结构体的区别
参考:https://blog.csdn.net/yuechuxuan/article/details/81673953
类是一种“引用类型”。创建类的对象时,对象赋值到的变量只保存对该内存的引用。将对象引用赋给新变量时,新变量引用的是原始对象。通过一个变量做出的更改将反映在另一个变量中,因为两者引用同一数据。
结构是一种值类型。创建结构时,结构赋值到的变量保存该结构的实际数据。将结构赋给新变量时,将复制该结构。因此,新变量和原始变量包含同一数据的两个不同的副本。对一个副本的更改不影响另一个副本。
类通常用于对较为复杂的行为建模,或对要在创建类对象后进行修改的数据建模。结构最适合一些小型数据结构,这些数据结构包含的数据以创建结构后不修改的数据为主。结构与类共享大多数相同的语法,但结构比类受到的限制更多。
C++中的struct对C中的struct进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了,它已经获取了太多的功能。
struct能包含成员函数吗? 能!struct能继承吗? 能!!struct能实现多态吗? 能!!!既然这些它都能实现,那它和class还能有什么区别?最本质的一个区别就是默认的访问控制:默认的继承访问权限,struct是public的,class是private的。
struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的
我依旧强调struct是一种数据结构的实现体,虽然它是可以像class一样的用。我依旧将struct里的变量叫数据,class内的变量叫成员,虽然它们并无区别。
“class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数。这一点在Stanley B.Lippman写的Inside the C++ Object Model有过说明。
从上面的区别,我们可以看出,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。
main的参数的意义
main()函数称之为主函数,一个C程序总是从main()函数开始执行的
main()函数的返回值类型是int型的,而程序最后的 return 0; 正与之遥相呼应,0就是main()函数的返回值。那么这个0返回到那里呢?返回给操作系统,表示程序正常退出。因为return语句通常写在程序的最后,不管返回什么值,只要到达这一步,说明程序已经运行完毕。而return的作用不仅在于返回一个值,还在于结束函数。
C编译器允许main()函数没有参数,或者有两个参数(有些实现允许更多的参数,但这只是对标准的扩展)。这两个参数,一个是int类型,一个是字符串类型。第一个参数是命令行中的字符串数。按照惯例(但不是必须的),这个int参数被称为argc(argument count)。大家或许现在才明白这个形参为什么要取这么个奇怪的名字吧,呵呵!至于英文的意思,自己查字典吧。第二个参数是一个指向字符串的指针数组。命令行中的每个字符串被存储到内存中,并且分配一个指针指向它。按照惯例,这个指针数组被称为argv(argument value)。系统使用空格把各个字符串格开。一般情况下,把程序本身的名字赋值给argv[0],接着,把最后的第一个字符串赋给argv[1],等等。
TYPEDEF的用法
用来声明自定义数据类型,主要是为了达到简便,还有就是在换编译环境时这东西比较有用.因为不同的编译环境数据类型占的位数有可能不一样的.
全局变量,局部变量,静态局部变量各是存放在哪个位置?
全局变量静态局部变量: 存放在静态RAM中.
局部变量:存放在动态区.而这东西在函数调用时会压栈的.他可能这一次运行存放在一个地址,下一次运行时就在另一个地址了,是一个不固定的.写过汇编的都知道.
回调函数以及用处,什么时候执行?
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
ASSETR是什么?什么情况下需要用
提示你的代码在错误的情况执行了.在你认为正常情况下不可能执行到的地方就得放这东西..这东西写代码的人要记得经常用,多用.没了
TYPEDEF的用法
用来声明自定义数据类型,主要是为了达到简便,还有就是在换编译环境时这东西比较有用.因为不同的编译环境数据类型占的位数有可能不一样的.
全局变量,局部变量,静态局部变量各是存放在哪个位置?
全局变量静态局部变量: 存放在静态RAM中.
局部变量:存放在动态区.而这东西在函数调用时会压栈的.他可能这一次运行存放在一个地址,下一次运行时就在另一个地址了,是一个不固定的.写过汇编的都知道.
两个函数怎么共享资源
可以用全局变量,不够由于没有约束,会使得函数变得不可重入,也可以使用指针,以参数的形式传递一个指针,就可以共享指针所指向的资源
const和define的区别,分别在什么情况下使用
在编译器的角度下,const其实给出了地址,而define给出了立即数,然后const是有类型的,表达式运算的时候会进行类型安全检查,而define没有,最后const只在第一次使用的时候访问内存,往后的使用都是访问符号表,define则是普通的字符串替换,具有多个副本。
const一般用于函数的参数保护,以免函数内部不小心修改了只读变量,define其实比较自由,按照Linux的风格,define是有崇高的地位,很多短小精悍的功能都由define完成,如果就定义常量而言,const和define都可以,不过我个人认为const定义的常量比define要高效
下面的声明都是什么意思?
const int a;
int const a;
const int *a;
int *const a;
int const *a const;
前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:
1). 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)
2). 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
3). 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。
volatile的作用
编译器优化常用的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用CPU。
volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据(脏数据就是在物理上临时存在过,但在逻辑上不存在的数据)。当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)
关键字volatile有什么含意 并给出三个不同的例子?
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1). 并行设备的硬件寄存器(如:状态寄存器)
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 多线程应用中被几个任务共享的变量
硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑这否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
1). 一个参数既可以是const还可以是volatile吗?解释为什么。
2). 一个指针可以是volatile 吗?解释为什么。
3). 下面的函数有什么错误:
int square(volatile int *ptr)
{ return *ptr * *ptr;
}
下面是答案:
1). 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2). 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3). 这段代码的有个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
static关键字
静态局部变量使用static修饰符定义,即使在声明时未赋初值,编译器也会把它初始化为0。且静态局部变量存储于进程的全局数据区,即使函数返回,它的值也会保持不变。
1)在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
2) 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
3) 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
指针和引用的区别
对象是指一块能存储数据并具有某种类型的内存空间,一个对象a,它有值和地址&a,运行程序时,计算机会为该对象分配存储空间,来存储该对象的值,我们通过该对象的地址,来访问存储空间中的值
指针p也是对象,它同样有地址&p和存储的值p,只不过,p存储的数据类型是数据的地址。如果我们要以p中存储的数据为地址,来访问对象的值,则要在p前加解引用操作符"*",即*p。
对象有常量(const)和变量之分,既然指针本身是对象,那么指针所存储的地址也有常量和变量之分,指针常量是指,指针这个对象所存储的地址是不可以改变的,而指向常量的指针的意思是,不能通过该指针来改变这个指针所指向的对象。
引用可以理解成变量的别名。定义一个引用的时候,程序把该引用和它的初始值绑定在一起,而不是拷贝它。计算机必须在声明r的同时就要对它初始化,并且,r一经声明,就不可以再和其它对象绑定在一起了。引用的一个优点是它一定不为空,因此相对于指针,它不用检查它所指对象是否为空,这增加了效率。
C++多态的实现方式
1、覆盖
覆盖是指子类重新定义父类的虚函数的做法。当子类重新定义了父类的虚函数后,父类指针根据赋值给它的不同的子类指针,动态的调用属于子类的该函数,这样函数调用在编译期间是无法确定的,这样的函数地址在运行期间绑定称为动态联编。
2、重载
重载是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数。
智能指针
shared_ptr、unique_ptr、weak_ptr
shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
- 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的
- 拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
- get函数获取原始指针
- 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
- 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍。
int main()
{
shared_ptr<Test> ptest(new Test("123"));
shared_ptr<Test> ptest2(new Test("456"));
cout<<ptest2->getStr()<<endl;
cout<<ptest2.use_count()<<endl;
ptest = ptest2;//"456"引用次数加1,“123”销毁
ptest->print();
cout<<ptest2.use_count()<<endl;//2
cout<<ptest.use_count()<<endl;//2
ptest.reset();
ptest2.reset();//此时“456”销毁
cout<<"done !\n";
return 0;
}
unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout<<"A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout<<"B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
理解智能指针需要从下面三个层次:
从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
智能指针还有一个作用是把值语义转换成引用语义。
用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个。
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
用变量a给出下面的定义
a) 一个整型数(An integer)
b)一个指向整型数的指针( A pointer to an integer)
c)一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer to an intege)r
d)一个有10个整型数的数组( An array of 10 integers)
e) 一个有10个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers to integers)
f) 一个指向有10个整型数数组的指针( A pointer to an array of 10 integers)
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions that take an integer argument and return an integer )
答案是:
a) int a; // An integer
b) int *a; // A pointer to an integer
c) int **a; // A pointer to a pointer to an integer
d) int a[10]; // An array of 10 integers
e) int *a[10]; // An array of 10 pointers to integers
f) int (*a)[10]; // A pointer to an array of 10 integers
g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer
h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer
单片机嵌入式
中断与异常有何区别
异常在处理的时候必须考虑与处理器的时钟同步,实际上异常也称为同步中断,在处理器执行到因编译错误而导致的错误指令时,或者在执行期间出现特殊错误,必须靠内核处理的时候,处理器就会产生一个异常;所谓中断是指外部硬件产生的一个电信号从CPU的中断引脚进入,打断CPU的运行。所谓异常是指软件运行过程中发生了一些必须作出处理的事件,CPU自动产生一个陷入来打断CPU的运行。
简述SPI,UART,I2C三种传输方式
SPI:高速同步串行口,首发独立,可同步进行
SPI接口主要应用在EEPROM,Flash,实时时钟,A/D转化器,数字信号处理,是一种全双工同步通讯总线,该接口一般使用四条线:串行时钟线(sck),主出从入线,主入从出线,低电平有效地的从机选择线。
I2C协议:是单片机与其他芯片进行通讯的协议:
A、只要求两条总线线路,一条是串行时钟线,一条是串行数据线;
B、通过软件设定地址
C、是一个多主机总线,如果两个或更多主机同时初始化数据传送可通过冲突检测和仲裁防止数据破坏;
D、I2C总线传输的是数据的总高位
UART:主要是由一个modem(调制解调器),可以将模拟信号量转化成数字信号量。
嵌入式操作系统和通用操作系统有什么差别?
答案:多优先级,抢占型,实时操作系统。嵌入式操作系统一般没有UI,体积小,实时性强,对稳定性要求更高。嵌入式操作系统强调实时性,并且可裁减。要求系统资源的消耗要尽可能的小。
ARM指令体系
arm和Thumb
什么叫波特率?和比特率有什么区别?
波特率是传输一个码元的时间,而比特率是传输一个二进制位的时间,区别在于,一个码元不一定是一个二进制位(虽然经常都是),比如当传输的状态有4种的时候,需要2个二进制位表示全部状态,此时发送一个状态为一个码元,一个码元其实就是2个二进制位,此时波特率是比特率的两倍,一个通用的公式是比特率=波特率*一个码元对应的二进制位数
PWM有占空比的概念,讲一讲占空比
占空比就是在一个脉冲宽度中,通电时间或者高电平时间所占的比例
单片机怎么实现占空比
可以用定时功能实现,设置好脉冲宽度和占空比后,计算定时的溢出时间,这段时间就作为高电平的输出时间,其余时间输出低电平
怎么实现定时功能
单片机都会有定时器,用定时器和中断就可以了
讲一讲中断的概念
中断是用于处理紧急事件的,系统只要预先设置好中断源、中断触发方式、中断服务函数、中断屏蔽位等等就可以使用中断了,当中断源满足中断条件的时候就会触发中断,此时CPU会停止当前工作,然后保护现场,接着跳转到中断服务函数执行,最后恢复现场
中断触发方式有哪些
外部中断一般是上升沿、下降沿或者两者都触发,而内部中断是由程序行为触发的,比如未定义行为像数组越界、除数是0等等
假设时钟频率128MHz,那么1us可以运行多少条指令?
时钟周期:时钟周期也称为振荡周期,定义为时钟脉冲的倒数(时钟周期就是单片机外接晶振的倒数,例如12M的晶振,它的时钟周期就是1/12us),是计算机中的最基本的、最小的时间单位。在一个时钟周期内,CPU仅完成一个最基本的动作。时钟脉冲是计算机的基本工作脉冲,控制着计算机的工作节奏。时钟频率越高,工作速度就越快。8051单片机把一个时钟周期定义为一个节拍(用P表示),二个节拍定义为一个状态周期(用S表示)。
机器周期:计算机中,常把一条指令的执行过程划分为若干个阶段,每一个阶段完成一项工作。每一项工作称为一个基本操作,完成一个基本操作所需要的时间称为机器周期。8051系列单片机的一个机器周期由6个S周期(状态周期)组成。 一个S周期=2个节拍(P),所以8051单片机的一个机器周期=6个状态周期=12个时钟周期。例如外接24M晶振的单片机,他的一个机器周期=12/24M 秒;
指令周期:执行一条指令所需要的时间,一般由若干个机器周期组成。指令不同,所需的机器周期也不同。
简述SPI,UART,I2C三种传输方式
SPI:高速同步串行口,首发独立,可同步进行
SPI接口主要应用在EEPROM,Flash,实时时钟,A/D转化器,数字信号处理,是一种全双工同步通讯总线,该接口一般使用四条线:串行时钟线(sck),主出从入线,主入从出线,低电平有效地的从机选择线。
I2C协议:是单片机与其他芯片进行通讯的协议:
A、只要求两条总线线路,一条是串行时钟线,一条是串行数据线;
B、通过软件设定地址
C、是一个多主机总线,如果两个或更多主机同时初始化数据传送可通过冲突检测和仲裁防止数据破坏;
D、I2C总线传输的是数据的总高位
UART:主要是由一个modem(调制解调器),可以将模拟信号量转化成数字信号量。
嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量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;
}
linux操作系统
linux使用过哪些命令?
ls;cd;pwd;mkdir;rm;cp;chmod;ll;ifconfig;grep;ps;df;kil
linux调度算法有哪些?
SCHED_OTHER 分时调度策略,
SCHED_FIFO实时调度策略,先到先服务
SCHED_RR实时调度策略,时间片轮转
多线程为什么要设置属性,为什么要设置栈大小
我答的不够用所以要分配更多的空间…后来百度了一下,linux线程默认分配8M的调用栈,那么太多的线程会导致栈溢出的问题,所以可以调用pthread_attr_setstack自动分配合理的大小防止栈溢出。当然线程还有其他的一些属性。
进程通信有哪些,如果我需要传输大量数据使用那种方式效率最高。
管道,命名管道,消息队列,共享内存,信号量,socket unix域,数据很大的时候,用共享内存是最好的,不用从用户态到内核态的频繁切换和拷贝数据,直接从内存中读取就可以,速度最快,但是共享内存开辟空间就必须要使用同步机制,这是他的缺点。交叉编译链
gcc
在一种计算机环境(称为host machine)中运行的编译程序,能编译出在另外一种环境(称为target machine)下运行的代码,叫做交叉编译。实现这个交叉编译的一系列工具,包括C函数库,内核文件,编译器,链接器,调试器,二进制工具……称为交叉编译工具链。
实际上在进行嵌入式开发时,我们通常都会在主机上(host machine)使用开发板厂商提供的编译器,调试器。比如在windows上装环境调试51,61单片机,在Linux上用arm-gcc写arm开发板的程序……
搭建交叉编译环境是一个非常繁琐并细致的过程。笔者就已嵌入式Linux交叉编译工具链的搭建为例,介绍下交叉编译工具的创建过程和原理。
线程和进程的区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
2) 线程的划分尺度小于进程,使得多线程程序的并发性高。
3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
进程之间相互通信的方式
1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
4. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
5. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
6. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
7. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
8. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
网络IO模式(select、epoll)
I/O 多路复用( IO multiplexing):select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。
poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
Linux的进程调度(优先级、时间片轮转调度)
1、先来先服务调度算法
先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。
2、短作业(进程)优先调度算法
短作业(进程)优先调度算法,是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。
3、时间片轮转法
在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU分配给队首进程,并令其执行一个时间片。时间片的大小从几ms到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。换言之,系统能在给定的时间内响应所有用户的请求。
能说一下具体操作系统移植的过程么?
答:内核启动流程:uboot->kernel->rootfs uboot基本都不会做修改,直接拿来烧在板上即可,uboot主要在调试模式修改一些环境变量,包括tftp的下载地址(serverip、ipaddr),包括bootcmd传给内核的参数,传给根文件系统的参数bootargs,并熟悉相应的命令使用(nand scrub、set pri、boot);内核源码编译,内核参考开发板的参数进行修改(开发板和母版的参数差异),make menuconfig进行模块选择,ulmage的制作;然后内核利用bootargs参数进行根文件系统启动,再就是根文件目录树的创建及其基本文件的创建。
(以上基本就能应付,很少有面试官深入,因为应届毕业生做arm移植的太少了=-=,更深的问题:为什么内核初始化地址是0x30008000,ulmage和zlmage的差别,make menuconfig添加模块的依据,)
移植遇到比较困难的问题
我说我当时移植QT的库时 ,发现版子上分区内存不够,当时也不知道怎么解决 就去学习分区挂载的知识,用df查看分区,发现有分区但是没挂载,导致空间不够,但是又不想在系统启动每次手动去挂载,于是就研究根文件启动init的过程,在配置文件inittab添加挂载命令,系统就会自启动并挂载分区了。
接着问,如果开发板上只有十兆空间,但是你的移植的QT库有二十兆,你会怎么做?
我说的扩容,他说除了扩容呢,我接着说移植的文件中有一个stripped属性,用strip命令去除库文件中的符号表,会小很多,他说QT编译一般会做这个工作,还有别的方法么…我就答不上来了。查了一下看了别人的方法https://blog.csdn.net/yming0221/article/details/6548349
shell脚本
#!/bin/sh
cd ~
mkdir shell_tut
cd shell_tut
for ((i=0; i<10; i++)); do
touch test_$i.txt
done
示例解释
第1行:指定脚本解释器,这里是用/bin/sh做解释器的
第2行:切换到当前用户的home目录
第3行:创建一个目录shell_tut
第4行:切换到shell_tut目录
第5行:循环条件,一共循环10次
第6行:创建一个test_0…9.txt文件
第7行:循环体结束
mkdir, touch都是系统自带的程序,一般在/bin或者/usr/bin目录下。for, do, done是sh脚本语言的关键字。
计算机网络
OSI七层网络协议和TCP/IP协议
OSI是Open System Interconnection的缩写,意为开放式系统互联。是设计和描述计算机网络通信的基本框架。OSI模型把网络通信的工作分为7层
应用层(Application):提供网络与用户应用软件之间的接口服务
表示层(Presentation):提供格式化的表示和转换数据服务,如加密和压缩
会话层(Session):提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制
传输层(Transimission): 提供建立、维护和取消传输连接功能,负责可靠地传输数据(PC)
网络层(Network):处理网络间路由,确保数据及时传送(路由器)
数据链路层(DataLink) :负责无错传输数据,确认帧、发错重传等(交换机)
物理层(Physics) : 提供机械、电气、功能和过程特性(网卡、网线、双绞线、同轴电缆、中继器)
TCP/IP协议简单介绍
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,是互联网的基础,也是每个程序员必备的基本功。TCP/IP参考模型分为四层,从上到下分别是:应用层、传输层、网络互连层、网络接口层。
TCP功能
将数据进行分段打包传输
对每个数据包编号控制顺序
运输中丢失、重发和丢弃处理
流量控制避免拥塞
TCP粘包、拆包及解决办法
参考:https://zhuanlan.zhihu.com/p/108822858
UDP 是基于报文发送的,UDP首部采用了 16bit 来指示 UDP 数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。而 TCP 是基于字节流的,虽然应用层和 TCP 传输层之间的数据交互是大小不等的数据块,但是 TCP 并没有把这些数据块区分边界,仅仅是一连串没有结构的字节流;另外从 TCP 的帧结构也可以看出,在 TCP 的首部没有表示数据长度的字段,基于上面两点,在使用 TCP 传输数据时,才有粘包或者拆包现象发生的可能。
什么是粘包、拆包?
假设 Client 向 Server 连续发送了两个数据包,用 packet1 和 packet2 来表示,那么服务端收到的数据可以分为三种情况,现列举如下:
第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象。
第二种情况,接收端只收到一个数据包,但是这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。
第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。
为什么会发生 TCP 粘包、拆包?
要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包。
要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包。
接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
粘包、拆包解决办法
由于 TCP 本身是面向字节流的,无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:
消息定长:发送端将每个数据包封装为固定长度(不够的可以通过补 0 填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
设置消息边界:服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如 FTP 协议。
将消息分为消息头和消息体:消息头中包含表示消息总长度(或者消息体长度)的字段。
更复杂的应用层协议比如 Netty 中实现的一些协议都对粘包、拆包做了很好的处理。