一、C语言和C++有什么区别
- C语言是面向过程,强调用函数将问题分解为多个子任务,按顺序逐步进行。数据和操作分开
- C++则是面向对象,面向对象是一种基于对象和类的编程范式,关注如何利用对象来抽象和模拟现实世界的实体。因此引入了类,封装,继承,多态。将数据和操作紧密绑定。
- C++的标准库丰富,包含了STL(标准模版库),且有命名空间,避免命名冲突。
- 在动态内存管理方面也有不同
- C语言使用malloc和free进行动态内存管理。
- C++提供了new和delete运算符,支持对象的构造和析构。
二、C语言里的结构体和C++里的类有什么区别?
- 默认访问控制
- 在C语言中,结构体中的成员默认是公有的(没有访问控制)。
- 在C++中,类中的成员默认是私有的,即外部代码不能直接访问类的成员,除非显式声明为public。
- 函数成员
- C语言的结构体只能包含数据成员(变量),不能包含函数成员。如果你想在结构体中关联函数,只能通过函数指针的方式实现。
- C++的类可以包含函数成员(即方法),这允许类不仅存储数据,还可以直接操作数据,提供更丰富的行为。
- 继承和多态
- C语言的结构体不支持继承,也不支持多态。
- C++类支持继承(一个类可以从另一个类派生),并通过虚函数实现多态性,允许你通过基类指针或引用调用派生类的行为。这是面向对象编程的核心特性。
三、什么是运算符重载
- 运算符重载(Operator Overloading)是面向对象编程中的一种技术,它允许程序员重新定义或扩展已有的运算符(如 +、-、*、/ 等)在自定义类或对象上的行为。
#include <iostream>
using namespace std;
class Complex {
public:
int real, imag;
Complex(int r = 0, int i = 0) : real(r), imag(i) {}
// 重载加法运算符(+)
Complex operator + (const Complex& other) {
return Complex(this->real + other.real, this->imag + other.imag);
}
void display() {
cout << real << " + " << imag << "i" << endl;
}
};
int main() {
Complex c1(3, 4), c2(1, 2);
Complex c3 = c1 + c2; // 使用重载的 + 运算符
c3.display(); // 输出 4 + 6i
return 0;
}
四、 同类型设备使用网线直连对线序的要求
- 在网络中,如果要将同类型的设备(如两台电脑、两台交换机)通过网线直连,使用的是交叉线(crossover cable)。
- 交叉线的线序要求是:
- 一端的1和2脚与另一端的3和6脚交叉连接
一端的1号针脚(发送数据 +)连接到另一端的3号针脚(接收数据 +)
一端的2号针脚(发送数据 -)连接到另一端的6号针脚(接收数据 -)
- 现代网络设备一般都支持Auto-MDIX,可以自动检测网线的类型并调整发送和接收线路,因此可以直接使用普通的直通线(Straight-through Cable)连接。
五、进程间的通信方式
- 管道:用于父子进程间的数据传递,分为匿名管道和命名管道(FIFO)。
- 信号(Signal):一种异步通知机制,用于向进程发送信号。
- 消息队列(Message Queue):通过消息的方式在进程间传递数据,按顺序存储消息。
- 共享内存(Shared Memory):多个进程共享同一块内存,直接读写内存中的数据,速度快。
- 信号量(Semaphore):用于控制进程对共享资源的访问,通常与共享内存结合使用。
- 套接字(Socket):通过网络协议进行进程间通信,支持跨主机通信。
六、什么是嵌入式或你对嵌入式的理解是什么?
- 嵌入式系统是一种专门为执行特定任务或功能设计的计算机系统,通常嵌入到更大的设备或系统中。
- 嵌入式系统的特点包括资源有限、对实时性的要求高、功耗低以及高度定制化。
七、C语言里的volatile关键字有什么作用
- volatile关键字用于告诉编译器不要对标记为volatile的变量进行优化。
- 读取volatile变量时,编译器会直接从内存中读取,而不是使用寄存器中的缓存值。
八、对多态的理解
- 多态是面向对象编程的特性之一,允许使用相同的函数名调用不同的函数,实现不同的功能。
- C++中多态通过虚函数来实现,基类中的虚函数在子类中可以被重写。
class Animal {
public:
virtual void sound() {
cout << "Some animal sound" << endl;
}
};
class Dog : public Animal {
public:
void sound() override {
cout << "Bark" << endl;
}
};
Animal* a = new Dog();
a->sound(); // 输出 "Bark",因为调用的是Dog类的sound函数。
九、如果一个类中含有纯虚函数能不能实例化该类?
- 不能。包含纯虚函数的类称为抽象类,不能实例化。如果一个类包含一个或多个纯虚函数,它只能作为基类供其他类继承,不能直接创建对象。
十、C++里的新特性有哪些?
- 智能指针,自动类型推导(auto关键字),lambda表达式,范围for循环:for(auto& element : container)。
十一、什么是僵尸进程?
- 僵尸进程是指一个已经终止但仍然存在于进程表中的进程。它的父进程还没有读取它的退出状态,因此系统仍然保留它的进程表项。父进程需要调用**wait()或waitpid()**来回收子进程的资源,避免系统资源浪费。
十二、管道的通信原理是什么?
- 管道(pipe)是一种用于进程间通信(IPC)的机制,它允许一个进程将数据写入管道,另一个进程从管道中读取数据。
- 数据在管道中以先进先出(FIFO)的方式传输。管道是单向的,如果需要双向通信,则需要使用两个管道。
十三、什么是端口号?
- 端口号是计算机网络中用于标识特定进程或服务的数字,用于区分同一设备上不同的网络应用。
十四、 IP地址和MAC地址有什么区别?
- IP地址:是一种逻辑地址,用于标识网络中的设备。可变
- MAC地址:是设备的物理地址,硬编码在网卡中,用于局域网内设备的标识,通常不会改变。
十五、常见的排序方法和原理
- 冒泡排序(Bubble Sort):通过重复比较相邻元素并交换,直到整个数组有序。时间复杂度为O(n²)。
- 选择排序(Selection Sort):每次从未排序部分选择最小(或最大)元素,放到已排序部分末尾。时间复杂度为O(n²)。
- 插入排序(Insertion Sort):将数组分为已排序和未排序部分,逐一将未排序元素插入到已排序部分的正确位置。时间复杂度为O(n²)。
- 归并排序(Merge Sort):采用分治法,将数组递归分为两半,分别排序后合并。时间复杂度为O(n log n)。
- 快速排序(Quick Sort):选择一个基准元素,将数组分为比基准小和大的两部分,递归排序。平均时间复杂度为O(n log n)。
- 堆排序(Heap Sort):利用堆数据结构将数组构建为最大堆或最小堆,然后逐步取出堆顶元素。时间复杂度为O(n log n)。
- 基数排序(Radix Sort):将数字分为若干位,从低位到高位依次进行计数排序。时间复杂度为O(n * k),k为数字的位数。
十六、编写实现数组排序的一种算法。说明为什么你会选择用这样的方法?
- 快速排序的实现
#include <iostream>
using namespace std;
// 快速排序的分区函数
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准
int i = low - 1; // 小于基准的元素的索引
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
swap(arr[i], arr[j]); // 将小于基准的元素放到前面
}
}
swap(arr[i + 1], arr[high]); // 将基准元素放到正确位置
return i + 1; // 返回基准元素的位置
}
// 快速排序的主函数
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high); // 获取分区点
quickSort(arr, low, pi - 1); // 对左半部分排序
quickSort(arr, pi + 1, high); // 对右半部分排序
}
}
int main() {
int arr[] = {10, 7, 8, 9, 1, 5};
int n = sizeof(arr) / sizeof(arr[0]);
quickSort(arr, 0, n - 1);
cout << "Sorted array: \n";
for (int i = 0; i < n; i++)
cout << arr[i] << " ";
cout << endl;
return 0;
}
十七、请实现strcpy函数
- 参数使用const修饰,防止函数内部修改. 参数需要检测,测量你的代码规范问题.
- 在 while ((*dest++ = *src++)); 这段代码中,并不需要显式地加入 \0(空字符),因为这段代码在复制字符串时,实际上已经处理了 \0 的情况。
- 当 *src++ 取到 \0 后,条件变为 0(因为 \0 的值为 0),循环结束。\0被复制了
#include <stdio.h>
char *strcpy(char *dest, const char *src) {
char *original_dest = dest;//保存目标字符串 dest 的原始地址。
// 逐个字符复制,直到遇到字符串结束符 '\0'
while ((*dest++ = *src++));
return original_dest;
}
十八、memcpy、strcmp、strlen、strstr(看看就好)
- memcpy
#include <stdio.h>
// 实现memcpy函数,将源内存区域的n个字节复制到目标内存区域
void *memcpy(void *dest, const void *src, size_t n) {
// 将void指针转换为字符指针,便于逐字节操作
char *d = dest;
const char *s = src;
// 按字节逐个复制,直到n个字节被复制完
while (n--) {
*d++ = *s++;
}
// 返回目标内存区域的指针
return dest;
}
- strcmp
#include <stdio.h>
// 实现strcmp函数,比较两个字符串,返回差值
int strcmp(const char *str1, const char *str2) {
// 当两个字符串的字符相等且未到字符串末尾时,继续比较
while (*str1 && (*str1 == *str2)) {
str1++; // 移动到下一个字符
str2++; // 移动到下一个字符
}
// 比较不相等的字符或到达字符串末尾,返回它们的差值
return *(unsigned char *)str1 - *(unsigned char *)str2;
}
- strlen
#include <stdio.h>
// 实现strlen函数,计算字符串的长度
size_t strlen(const char *str) {
const char *s = str; // 保存原始指针
// 遍历字符串直到遇到字符串结束符 '\0'
while (*s) {
s++; // 移动到下一个字符
}
// 返回字符串的长度,等于 s - str
return s - str;
}
- strstr
#include <stdio.h>
// 实现strstr函数,在主字符串中查找子字符串
char *strstr(const char *haystack, const char *needle) {
// 如果子字符串为空,返回主字符串的起始地址
if (!*needle) return (char *)haystack;
// 遍历主字符串
for (; *haystack; haystack++) {
const char *h = haystack;
const char *n = needle;
// 比较主字符串和子字符串的字符
while (*h && *n && (*h == *n)) {
h++; // 移动到主字符串的下一个字符
n++; // 移动到子字符串的下一个字符
}
// 如果子字符串遍历结束,表示找到匹配
if (!*n) return (char *)haystack;
}
// 如果没有找到匹配,返回NULL
return NULL;
}
十九、中断是什么,简述中断工作流程. 请描述中断的边沿触发和电平触发的区别,中断函数处理需要注意什么?
- 中断是指计算机系统中,当某些外部或内部事件发生时,处理器暂时中断当前正在执行的任务,转而处理该事件的一个机制。
- 中断的目的是让处理器可以响应外部设备或内部异常情况,避免轮询等待,提升系统效率。
- 工作流程
中断请求
处理中断请求
跳转中断服务程序
处理中断
恢复执行
- 边沿触发和电平触发的区别
- 边沿触发(Edge-triggered):中断信号由电平变化(从低到高或从高到低的边沿)引发
- 电平触发(Level-triggered): 中断信号由电平状态(高电平或低电平)引发。适合需要持续响应某些信号的场景(如设备准备好数据)。
- 中断函数处理需要注意的事项
- 中断处理要尽量简短
- 避免阻塞操作
- 中断优先级和嵌套
- 共享资源的同步问题
- 清除中断标志
- 保护上下文
- 简述 串口 i2c spi 总线的区别
串口通常指的是串行通信接口(UART),它是一种异步通信协议,两根信号线TX、RX,仅支持一对一通信
I2C 是一种用于芯片之间的低速、短距离通信的同步串行总线,广泛用于传感器、EEPROM 和小型外围设备。
同步通信,需要时钟信号。主设备通过时钟控制数据传输。仅需两根线:数据线(SDA)和时钟线(SCL)
支持多主多从通信,一条 I2C 总线上可以挂接多个设备。每个设备都有唯一的地址,主设备根据地址选择从设备进行通信。
SPI 是一种高速、全双工、同步的串行通信协议,常用于高速数据传输,如存储器、显示设备、AD/DA 转换器等。
同步通信,需要时钟信号。支持全双工通信(数据可以同时发送和接收)。
引脚数量:
主设备输出的数据(MOSI:Master Out Slave In)
从设备输出的数据(MISO:Master In Slave Out)
时钟信号(SCK:Serial Clock)
片选信号(CS/SS:Chip Select/Slave Select,用于选择从设备)
单主多从
SPI:用于高速、短距离的通信,如存储设备、显示屏、ADC/DAC 等设备。
二十、简述adc原理,adc是如何测量电压的. 有时候会出计算题,求解电压数值.
- ADC(Analog-to-Digital Converter)是一种将模拟信号(如电压)转换为数字信号的电子设备。
- 工作步骤:抽样–>保持–>量化–>编码
- 公式:
二十一、定时器, pwm原理是什么?
-
定时器是一种用于计时或生成特定时间间隔的硬件模块,广泛应用于微控制器中。定时器的工作原理基于计数器,通过对时钟信号进行计数来实现定时功能。
-
溢出/中断:当计数器达到预设的最大值时,定时器会发生溢出,重新开始计数。在溢出时,定时器可以触发中断
-
PWM(Pulse Width Modulation,脉宽调制)是一种调节信号占空比的方法,通过调节脉冲信号中高电平持续时间与周期的比例来控制输出功率或模拟信号。
-
PWM用于电机控制、LED亮度调节、模拟信号生成
二十二、友元函数
- 友元函数是 C++ 中的一种机制,允许特定函数访问类的私有和保护成员。
- 可以实现类之间的紧密合作,而不需要通过公共接口访问数据。
二十三、接口和虚函数的区别
- 接口在 C++ 中并没有专门的语法,它通常通过纯虚函数(pure virtual function)来实现。
- 接口的特点
- 一个类如果所有的成员函数都是纯虚函数,并且没有数据成员,我们可以把它看作接口。
- 接口类不能实例化,只能被继承。
- 任何派生类必须实现所有的纯虚函数,才能被实例化。
- 区别:
- 接口用于定义一组行为或功能,任何实现该接口的类必须提供这些行为的具体实现。接口是为了强制派生类实现某些功能。
- 虚函数用于实现运行时多态,使得基类指针或引用可以在派生类中调用重写的函数。
- 一个类可以继承多个接口(多继承),这在定义类的行为时非常有用。
- 接口用于定义行为规范,使得不同类可以实现相同的功能。
二十四、Makefile的作用
- Makefile 是一个自动化构建工具的配置文件,主要用于管理项目的编译过程。它通过定义目标(target)、依赖(dependency)和命令(command),来描述如何从源代码生成可执行文件或其他目标。
- 在 Makefile 中,@ 符号用于抑制命令的回显。当你在 Makefile 中使用命令时,默认情况下,Make 会在执行命令之前将该命令打印到终端。如果你希望执行命令时不显示该命令,可以在命令前加上 @ 符号。
二十五、shell通道符的作用
- 在 Shell 中,通道符(|)用于将一个命令的输出作为另一个命令的输入。这个功能称为“管道”,可以将多个命令组合在一起,形成一个更复杂的操作。
ls -l | grep ".txt"
这个命令列出当前目录下的所有文件,然后通过 grep 筛选出包含 .txt 的文件。
二十六、链接库的参数
-l
- 用于链接库文件。格式为 -l<库名>,其中 <库名> 是去掉前缀 lib 和后缀 .a 或 .so 的库名。
gcc main.c -o my_program -lm # 链接数学库 libm.so
-L
- 指定库文件搜索路径。默认情况下,链接器会在系统的标准库路径中查找库文件,使用 -L 可以添加自定义路径。
gcc main.c -o my_program -L/path/to/lib -lmylib # 链接自定义路径中的 libmylib.so
二十七、open和fopen
- open是一个系统调用,用于打开文件并返回一个文件描述符(file descriptor),这是一个用于标识打开文件的非负整数。
- fopen 是一个标准库函数,用于打开文件并返回一个指向 FILE 结构的指针,提供更高级别的文件操作接口。
- 对于简单的文件操作,通常使用 fopen 更为方便。
- fopen有缓冲区
二十八、fopen的第一个参数
- fopen函数的第一个参数是文件名(路径),它指定了要打开或创建的文件的名称和位置。这个参数可以是一个相对路径或绝对路径,具体取决于你的需求。
二十九、多重继承会有什么问题
- 可能导致歧义:当两个父类有共同的祖先时,子类继承了这两个父类,可能导致歧义。
- 命名冲突
- 内存开销
每个父类的实例变量会被包含在子类中,这可能会导致子类占用更多的内存空间。
三十、如何实现服务器并发
- 实现服务器并发可以通过多种方法,常见的有多线程、多进程和异步 I/O 等.