面试题汇总

文章目录

PART1——Linux应用相关

1.什么是进程和线程?有何区别?

进程和线程都是操作系统中的概念,用于管理程序的执行。

进程是指正在运行的程序的实例,它包含了程序代码、数据和执行状态等信息。每个进程都有自己的地址空间、堆栈和文件描述符等资源,它们之间相互独立,互不干扰。进程是操作系统中最基本的资源分配单位,它可以由操作系统创建、调度和销毁。

线程是进程中的一个执行单元,它是进程中的一个分支,共享进程的地址空间和资源。线程可以看作是轻量级的进程,它们之间的切换比进程之间的切换更快,因为线程共享了进程的资源,所以线程之间的通信和同步更加方便和高效。

区别在于:

  1. 进程是操作系统中最基本的资源分配单位,而线程是进程中的一个执行单元。
  2. 进程之间相互独立,互不干扰,而线程共享进程的地址空间和资源。
  3. 进程之间的切换比线程之间的切换更慢,因为进程之间需要切换地址空间和资源,而线程之间只需要切换执行上下文。
  4. 进程之间的通信和同步比线程之间的通信和同步更加复杂和耗费资源,因为进程之间需要使用进程间通信机制,而线程之间可以使用共享内存等高效的通信方式。

2.内核有哪几种方式访问硬件设备?

内核可以通过以下几种方式访问硬件设备:

  1. I/O端口访问:内核可以通过读写I/O端口的方式来访问硬件设备,这种方式通常用于访问一些简单的设备,如键盘、鼠标等。
  2. 内存映射I/O访问:内核可以将硬件设备的寄存器映射到内核地址空间中,通过读写内存的方式来访问硬件设备,这种方式通常用于访问一些复杂的设备,如网卡、显卡等。
  3. 中断访问:内核可以通过注册中断处理函数的方式来访问硬件设备,当硬件设备发生中断时,内核会调用相应的中断处理函数来处理中断事件。
  4. DMA访问:内核可以通过DMA(Direct Memory Access)方式来访问硬件设备,这种方式可以让硬件设备直接访问内存,从而提高数据传输的效率。

3.简述BootLoader,Linux内核,根文件系统之间的关系以及各自的作用

BootLoader、Linux内核和根文件系统是构成Linux操作系统的三个重要组成部分,它们之间的关系如下:

  1. BootLoader:BootLoader是启动加载器,它是在计算机启动时运行的程序,负责将操作系统从存储设备(如硬盘、U盘等)中加载到内存中,并将控制权交给操作系统。BootLoader的主要作用是初始化硬件设备、加载内核和根文件系统等。

  2. Linux内核:Linux内核是操作系统的核心部分,它是操作系统的主要组成部分,负责管理计算机的硬件资源、提供系统调用接口、管理进程和线程、提供文件系统等功能。Linux内核是操作系统的核心,它的作用是将硬件资源和软件资源有效地组织起来,为用户提供一个稳定、高效、安全的操作环境。

  3. 根文件系统:根文件系统是操作系统的基础文件系统,它包含了操作系统所需的所有文件和目录,包括设备文件、配置文件、库文件、可执行文件等。根文件系统是操作系统的基础,它的作用是提供操作系统所需的所有文件和目录,为操作系统的正常运行提供支持。

在Linux系统启动时,BootLoader首先会加载内核文件到内存中,然后将控制权交给内核。内核会初始化硬件设备、加载根文件系统,并启动系统服务和用户进程,最终进入用户界面。因此,BootLoader、Linux内核和根文件系统是Linux操作系统启动过程中不可或缺的三个组成部分。

4.死锁的条件

死锁是指在并发系统中,两个或多个进程或线程因竞争资源而陷入互相等待的状态,无法继续执行下去。死锁的4个必要条件如下:

  1. 互斥条件:至少有一个资源必须处于非共享模式,即一次只能被一个进程使用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能被其他进程强行剥夺,只能由该进程自己释放。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源的关系。即进程A等待进程B占用的资源,进程B等待进程C占用的资源,进程C等待进程A占用的资源。

5.IPC(进程间通信)机制,包括?

1.管道(Pipe):管道是一种半双工的通信方式,只能在具有亲缘关系的进程之间使用。一个进程向管道中写入数据,另一个进程从管道中读取数据。

2.命名管道(Named Pipe):命名管道也是一种半双工的通信方式,但可以在不具有亲缘关系的进程之间使用。命名管道在文件系统中有一个名字,进程可以通过该名字打开管道进行通信。

3.消息队列(Message Queue):消息队列是一种消息传递机制,可以在不具有亲缘关系的进程之间传递消息。消息队列可以实现进程间异步通信,发送方将消息发送到队列中,接收方从队列中读取消息。

4.信号量(Semaphore):信号量是一种计数器,用于控制多个进程对共享资源的访问。进程可以通过信号量进行同步和互斥操作,避免竞争条件。

5.共享内存(Shared Memory):共享内存是一种高效的IPC机制,可以在多个进程之间共享同一块物理内存。多个进程可以直接访问共享内存中的数据,避免了数据复制的开销。

6.套接字(Socket):套接字是一种网络通信机制,可以在不同主机之间进行进程间通信。套接字可以实现进程间的网络通信,包括TCP和UDP等协议。

这些IPC机制各有特点,可以根据具体的需求选择合适的机制进行进程间通信。

6.如何访问两个Linux线程的全局变量?

在Linux中,可以使用互斥锁(mutex)或信号量(semaphore)来实现两个线程之间对全局变量的访问。互斥锁可以确保同一时间只有一个线程可以访问全局变量,而信号量可以控制同时访问全局变量的线程数量。此外,还可以使用条件变量(condition variable)来实现线程之间的同步,以便在全局变量发生变化时通知其他线程。需要注意的是,在使用这些机制时,必须避免死锁(deadlock)和竞态条件(race condition)等问题。

7."select"和"poll"的主要区别在于:

  1. 参数类型:select使用fd_set结构体来表示文件描述符集合,而poll使用pollfd结构体数组来表示。

  2. 可扩展性:select的文件描述符集合大小有限,通常为1024,而poll没有这个限制。

  3. 效率:在文件描述符数量较少的情况下,select的效率比poll高,但在文件描述符数量较多时,poll的效率更高。

总的来说,select和poll都是用于I/O多路复用的系统调用,它们的选择取决于具体的应用场景和需求。

8. 请绘制一个Linux进程的内存布局。

Linux进程的内存布局通常可以分为以下几个部分:

  1. 代码段(text segment):存放程序的可执行代码,通常是只读的。

  2. 数据段(data segment):存放程序的全局变量和静态变量,通常是可读写的。

  3. 堆(heap):存放动态分配的内存,通常是可读写的。

  4. 栈(stack):存放函数调用时的局部变量和函数调用的上下文信息,通常是可读写的。

  5. 内核空间(kernel space):存放操作系统内核的代码和数据,只能被内核访问。

以上是Linux进程内存布局的基本结构,不同的进程可能会有一些特殊的内存区域,例如共享内存、动态链接库等。

9. 如何避免Linux线程之间的死锁?如何测试你的代码?

死锁是指两个或多个线程在互相等待对方释放资源的情况下无法继续执行的情况。为了避免死锁,可以采取以下措施:

  1. 避免使用多个锁:使用一个锁来保护多个资源,而不是使用多个锁来保护每个资源。

  2. 避免锁的嵌套:在持有一个锁的情况下,不要尝试获取另一个锁。

  3. 使用超时机制:在等待锁的时候,可以设置一个超时时间,如果超时则放弃等待。

  4. 避免循环依赖:如果多个线程需要获取多个锁,需要按照相同的顺序获取锁,以避免循环依赖。

测试代码时,可以采用以下方法:

  1. 单元测试:编写针对每个函数或模块的测试用例,以确保代码的正确性。
  2. 集成测试:将多个模块或组件集成在一起进行测试,以确保它们能够正确地协同工作。
  3. 压力测试:模拟大量并发请求,测试系统的性能和稳定性。
  4. 调试工具:使用调试工具来检查代码中的问题,例如内存泄漏、死锁等。

10. 创建编译命令

假设有一个test.c文件,需要/home/acornfinclude里的 acorn.h文件,且需要/home/acorn/ib里的 libacorn.so.1.10动态库,写出一条编译命令生成可执行文件 test (test需要带调试信息)

编译命令如下:

gcc -g -o test test.c -I/home/acorn/include -L/home/acorn/lib -lacorn

其中:

  • -g 表示生成可执行文件时带有调试信息。
  • -o test 表示生成的可执行文件名为 test。
  • test.c 是需要编译的源代码文件。
  • -I/home/acorn/include 表示头文件 acorn.h 在 /home/acorn/include 目录下。
  • -L/home/acorn/lib 表示动态库 libacorn.so.1.10 在 /home/acorn/lib 目录下。
  • -lacorn 表示链接 libacorn.so.1.10 动态库。注意,这里的参数名是去掉前缀 lib 和后缀 .so 的部分。

11. 怎样设置才能产生core文件?

要产生core文件,需要在程序崩溃时生成一个核心转储文件。在Linux系统中,可以通过以下步骤设置:

  1. 确认系统内核参数core文件大小限制是否为0,可以使用以下命令查看:

    ulimit -a
    

    如果core文件大小限制为0,需要使用以下命令将其设置为无限制:

    ulimit -c unlimited
    
  2. 确认程序是否被编译时开启了core文件生成选项,可以在编译时加上以下选项:

    -g -Wall -Wextra -Werror -Wno-unused-parameter -Wno-missing-field-initializers -Wno-unused-function -Wno-unused-variable -Wno-unused-but-set-variable -Wno-format-truncation -Wno-stringop-truncation -Wno-implicit-fallthrough -Wno-sign-compare -Wno-maybe-uninitialized -Wno-unused-result -Wno-format-security -Wno-strict-aliasing -Wno-unknown-pragmas -Wno-strict-overflow -Wno-overflow -fno-omit-frame-pointer
    
  3. 确认程序是否在崩溃时会产生core文件,可以使用以下命令查看:

    ulimit -c
    

    如果输出结果为0,则需要使用以下命令将其设置为非0值:

    ulimit -c unlimited
    

设置完成后,当程序崩溃时,就会在当前工作目录下生成一个名为“core”的文件,该文件包含程序崩溃时的内存转储信息,可以使用调试工具进行分析。

12.怎样建立一个软连接,并说明软连接和硬连接的区别?

在 Linux 系统中,可以使用 ln 命令来创建软链接和硬链接。

创建软链接的命令格式如下:

ln -s 源文件 目标文件

其中,-s 表示创建软链接,源文件 是要链接的文件,目标文件 是链接后的文件名。

在这个问题中,如果要为 libz.50.1.1 创建一个软链接,可以使用以下命令:

ln -s libz.50.1.1 libz.so

这将在当前目录下创建一个名为 libz.so 的软链接,指向 libz.50.1.1 文件。

软链接和硬链接的区别如下:

  1. 硬链接是指向同一个 inode 的不同文件名,而软链接是一个特殊的文件,其中包含指向另一个文件的路径。
  2. 硬链接只能在同一个文件系统中创建,而软链接可以跨越文件系统。
  3. 删除原始文件时,硬链接仍然可以访问该文件的内容,而软链接将无法访问。
  4. 硬链接不能用于目录,而软链接可以用于目录。

13.如何编写可重入函数,写一个可重入函数

可重入函数是指在多线程或多任务环境下,能够安全地被多个线程或任务同时调用的函数。为了实现可重入,需要避免使用全局变量、静态变量、非线程安全的库函数等可能导致数据竞争的元素。

以下是一个简单的可重入函数示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void reverse(char *str, char *result, int len) {
    int i;
    for (i = 0; i < len; i++) {
        result[i] = str[len - i - 1];
    }
    result[len] = '\0';
}

int main() {
    char str[] = "Hello, world!";
    char result[strlen(str) + 1];
    reverse(str, result, strlen(str));
    printf("%s\n", result);
    return 0;
}

在这个示例中,我们定义了一个 reverse 函数,用于将一个字符串反转。为了实现可重入,我们避免使用全局变量和静态变量,并且使用了参数传递的方式来传递需要操作的字符串和结果字符串。这样,即使有多个线程或任务同时调用该函数,也不会发生数据竞争。

需要注意的是,可重入函数并不一定是线程安全的,因为线程安全还需要考虑到锁的使用等问题。但是,可重入函数是线程安全的基础。

14.中断服务子程序相关错误

__interrupt double compute_area(double r)
{
	double area PI*r*r;
	printf("%f\r\n",area);
	return area;
}
  • 不能有返回值
  • 不能传参
  • 不能做浮点运算
  • pirntf有重入和性能的问题

15.请说明Linux系统调用的概念与实现方式

Linux系统调用是指用户空间程序通过系统调用接口向内核发出请求,请求内核执行某些特权操作或获取某些系统资源。系统调用是用户程序与内核之间的接口,用户程序通过系统调用接口向内核发出请求,内核则根据请求执行相应的操作并返回结果给用户程序。

Linux系统调用的实现方式是通过软中断来实现的。软中断是一种特殊的中断,它是由用户程序通过软中断指令触发的,触发软中断后,CPU会从用户态切换到内核态,并跳转到软中断处理程序中执行。在Linux系统中,软中断的编号为0x80,用户程序通过int 0x80指令触发软中断,内核会根据传入的系统调用号来执行相应的系统调用。

Linux系统调用的概念和实现方式是操作系统学习中非常重要的内容,理解系统调用的概念和实现方式可以帮助我们更好地理解操作系统的工作原理。

16.请说明Linux系统中的信号处理机制

Linux系统中的信号处理机制是指在进程间通信时,一个进程可以向另一个进程发送信号,以通知该进程发生了某个事件。Linux系统中有多种信号,每种信号都有一个唯一的编号,例如SIGINT表示中断信号,SIGTERM表示终止信号等等。

当一个进程接收到一个信号时,它可以采取不同的行动,例如忽略该信号、执行默认操作或者执行自定义操作。Linux系统中的信号处理机制可以通过以下步骤实现:

  1. 发送信号:一个进程可以通过系统调用kill()向另一个进程发送信号。

  2. 接收信号:当一个进程接收到一个信号时,它会暂停当前的执行,转而执行信号处理函数。

  3. 信号处理函数:每个信号都有一个默认的处理函数,但进程也可以通过系统调用signal()或sigaction()来注册自定义的信号处理函数。

  4. 信号屏蔽:进程可以通过系统调用sigprocmask()来屏蔽某些信号,以避免在执行关键代码时被中断。

  5. 信号队列:如果一个进程接收到多个相同类型的信号,这些信号会被放入一个信号队列中,等待进程处理。

总的来说,Linux系统中的信号处理机制是一种非常重要的进程间通信方式,它可以帮助进程及时地响应外部事件,保证系统的稳定性和可靠性。

17.请说明Linux系统用户空间C程序中的线程同步机制

Linux系统用户空间C程序中的线程同步机制包括以下几种:

  1. 互斥锁(Mutex):互斥锁是一种最基本的线程同步机制,它可以保证在同一时刻只有一个线程能够访问共享资源。当一个线程获取到互斥锁时,其他线程就必须等待该线程释放锁后才能继续访问共享资源。
  2. 条件变量(Condition Variable):条件变量是一种线程同步机制,它可以让线程在特定条件下等待或唤醒。当一个线程需要等待某个条件满足时,它可以调用条件变量的等待函数,这会使线程进入阻塞状态,直到其他线程发出信号通知该线程条件已经满足。
  3. 读写锁(Read-Write Lock):读写锁是一种特殊的互斥锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。当一个线程获取到读锁时,其他线程也可以获取读锁,但不能获取写锁;当一个线程获取到写锁时,其他线程就必须等待该线程释放锁后才能继续访问共享资源。
  4. 自旋锁(Spin Lock):自旋锁是一种特殊的互斥锁,它不会使线程进入阻塞状态,而是会一直尝试获取锁,直到获取成功为止。自旋锁适用于锁的持有时间很短的情况,因为它不会引起线程的上下文切换,从而提高了程序的性能。
  5. 信号量(Semaphore):信号量是一种计数器,它可以用来控制多个线程对共享资源的访问。当一个线程需要访问共享资源时,它必须先获取信号量,如果信号量的值大于0,则线程可以继续访问共享资源;如果信号量的值等于0,则线程必须等待其他线程释放信号量后才能继续访问共享资源。当一个线程释放共享资源时,它必须将信号量的值加1,以便其他线程可以继续访问共享资源。

18.请说明Linux系统中驱动程序中的同步机制与区别

在Linux系统中,驱动程序中的同步机制主要包括信号量、自旋锁、读写锁和互斥锁等。它们的区别如下:

  1. 信号量:信号量是一种计数器,用于控制对共享资源的访问。当信号量的值为正数时,表示资源可用;当值为零时,表示资源已被占用,需要等待;当值为负数时,表示有进程在等待资源。信号量可以用于进程间同步和互斥。

  2. 自旋锁:自旋锁是一种轻量级的锁,它不会使线程进入睡眠状态,而是在一个循环中不断地检查锁是否可用。如果锁被占用,则线程会一直循环等待,直到锁被释放。自旋锁适用于锁的持有时间很短的情况。

  3. 读写锁:读写锁是一种特殊的锁,它允许多个线程同时读共享资源,但只允许一个线程写共享资源。读写锁适用于读操作远远多于写操作的情况。

  4. 互斥锁:互斥锁是一种最常用的锁,它保证同一时刻只有一个线程可以访问共享资源。当一个线程获得互斥锁后,其他线程必须等待该线程释放锁后才能访问共享资源。互斥锁适用于锁的持有时间较长的情况。

总的来说,不同的同步机制适用于不同的场景,开发者需要根据具体情况选择合适的同步机制来保证程序的正确性和性能。

19.shell脚本实现

  1. 使用shell脚本实现,读取文件名,并判断是否为字符设备文件,如是铂贝本文件到当前目录下,不是则输出当前终端提示。

    可以使用以下的shell脚本实现:

    #!/bin/bash
    
    # 读取文件名
    read -p "请输入文件名: " filename
    
    # 判断是否为字符设备文件
    if [ -c "$filename" ]; then
        # 是字符设备文件,拷贝到当前目录下
        cp "$filename" .
    else
        # 不是字符设备文件,输出提示信息
        echo "输入的文件不是字符设备文件"
    fi
    

    解释一下脚本的实现:

    1. 使用read命令读取用户输入的文件名,并保存到filename变量中。
    2. 使用-c选项判断$filename是否为字符设备文件,如果是,则使用cp命令将其拷贝到当前目录下。
    3. 如果不是字符设备文件,则输出提示信息。

    注意:该脚本只能判断文件是否为字符设备文件,不能判断是否为块设备

  2. 使用shell脚本实现,定义一个数组 其内容是 one two three four five fix

    可以使用以下代码定义一个包含这些元素的数组:

    #!/bin/bash
    
    # 定义数组
    arr=("one" "two" "three" "four" "five" "fix")
    
    # 打印数组元素
    echo "${arr[@]}"
    

    输出结果为:

    one two three four five fix
    
  3. 使用shell脚本实现,定义一个函数 其功能是输出传入参数到终端

    可以使用以下的shell脚本实现:

    #!/bin/bash
    
    # 定义函数
    function print_arg {
        echo "传入的参数是:$1"
    }
    
    # 调用函数
    print_arg "Hello World!"
    

    在上面的脚本中,我们定义了一个名为print_arg的函数,它接受一个参数,并将其输出到终端。然后我们调用这个函数,并传入一个字符串作为参数。当脚本运行时,它会输出传入的参数是:Hello World!到终端。

  4. 使用shell脚本实现,定义一个函数 其功能是循环输出传入参数到终端

    可以使用以下的shell脚本实现:

    #!/bin/bash
    
    # 定义函数
    function loop_output() {
        # 循环输出传入参数
        for arg in "$@"
        do
            echo "$arg"
        done
    }
    
    # 调用函数
    loop_output "hello" "world" "!"
    

    在上面的脚本中,我们定义了一个名为loop_output的函数,它接受任意数量的参数。在函数内部,我们使用for循环遍历传入的参数,并使用echo命令将它们输出到终端。最后,我们在脚本的最后调用了loop_output函数,并传入了三个参数:“hello”、“world"和”!"。当脚本运行时,它会输出这三个参数到终端。你可以根据需要修改传入的参数。

20.描述嵌入式多任务系统中任务切换的原理

嵌入式多任务系统中,任务切换是指在多个任务之间进行切换,以实现多任务并发执行的过程。任务切换的原理如下:

  1. 任务调度器:嵌入式多任务系统中的任务调度器负责管理和调度所有任务的执行。任务调度器根据任务的优先级和调度算法来决定哪个任务应该被执行。

  2. 上下文切换:当任务调度器决定切换到另一个任务时,它会保存当前任务的上下文信息,包括程序计数器、寄存器和堆栈指针等。然后,它会恢复下一个任务的上下文信息,并将控制权转移到该任务。

  3. 中断处理:在嵌入式多任务系统中,中断是一种常见的事件,如定时器中断、外部设备中断等。当中断事件发生时,任务调度器会暂停当前任务的执行,并保存其上下文信息。然后,它会执行中断处理程序,处理中断事件。处理完成后,任务调度器会恢复当前任务的上下文信息,并继续执行该任务。

  4. 时间片轮转:在一些嵌入式多任务系统中,任务调度器会采用时间片轮转的方式来实现任务切换。每个任务被分配一个时间片,当时间片用完时,任务调度器会切换到下一个任务。

总之,任务切换是嵌入式多任务系统中实现多任务并发执行的关键。通过任务调度器的管理和调度,以及上下文切换和中断处理等机制,可以实现多个任务之间的高效切换和并发执行。

21.fork出的子进程会继承父进程的哪些属性?

fork出的子进程会继承父进程的以下属性:

  1. 进程的代码段、数据段和堆栈段;
  2. 进程的文件描述符;
  3. 进程的信号处理方式;
  4. 进程的当前工作目录;
  5. 进程的用户ID和组ID;
  6. 进程的进程组ID和会话ID;
  7. 进程的资源限制;
  8. 进程的环境变量;
  9. 进程的共享内存和信号量等IPC资源。

需要注意的是,子进程会继承父进程的这些属性的副本,也就是说,子进程对这些属性的修改不会影响父进程。同时,子进程会拥有自己的进程ID和父进程ID。

22.TCPSocket编程中常用的函数有哪些?哪些函数会产生阻塞?

在TCPSocket编程中,常用的函数包括:

  1. socket():创建一个套接字。
  2. bind():将套接字绑定到一个地址和端口。
  3. listen():将套接字设置为监听状态。
  4. accept():接受客户端的连接请求。
  5. connect():连接到服务器端。
  6. send():发送数据。
  7. recv():接收数据。
  8. close():关闭套接字。

在这些函数中,accept()、recv()和connect()函数会产生阻塞。当调用这些函数时,如果没有数据可读或连接请求可接受,程序会一直等待,直到有数据可读或连接请求可接受为止。为了避免阻塞,可以使用非阻塞I/O或者多线程技术。


PART2——C语言相关

1.详述C++引用与指针的区别,并通过C++举例说明

C++中,指针和引用都是用来间接访问变量的。它们的主要区别如下:

  1. 定义方式不同:指针使用*和&符号来定义,而引用使用&符号来定义。

  2. 空指针和空引用的区别:指针可以被赋值为NULL或nullptr,表示空指针;而引用必须在定义时初始化,不能为NULL或nullptr。

  3. 操作符的不同:指针使用*和->操作符来访问指针所指向的对象,而引用使用.和->操作符来访问引用所绑定的对象。

  4. 内存分配方式不同:指针可以通过new运算符动态分配内存,而引用只能绑定已经存在的对象。

  5. 传递参数的方式不同:指针可以作为函数参数传递,可以通过指针修改函数外部的变量;而引用也可以作为函数参数传递,但是它可以直接修改函数外部的变量,不需要通过指针间接访问。

总的来说,引用是指针的一种更加安全和方便的替代品,它可以避免指针的一些潜在问题,如空指针和野指针。但是在某些情况下,指针仍然是必需的,比如需要动态分配内存或者需要使用指针算术运算等。

C++中的指针和引用都是用来间接访问内存中的数据的。它们的主要区别在于以下几个方面:

  1. 指针可以被重新赋值,而引用一旦被初始化就不能再指向其他对象。

  2. 指针可以为空,而引用必须总是指向某个对象。

  3. 指针可以进行算术运算,而引用不支持。

  4. 指针可以指向空间未知的内存,而引用必须总是指向已知的内存。

下面通过一个简单的例子来说明指针和引用的区别:

#include <iostream>
using namespace std;

int main() {
    int a = 10;
    int* p = &a; // 定义指针p并指向a
    int& r = a;  // 定义引用r并指向a

    cout << "a = " << a << endl;
    cout << "*p = " << *p << endl;
    cout << "r = " << r << endl;

    *p = 20; // 修改指针p所指向的值
    r = 30;  // 修改引用r所指向的值

    cout << "a = " << a << endl;
    cout << "*p = " << *p << endl;
    cout << "r = " << r << endl;

    p = nullptr; // 将指针p置为空
    // r = nullptr; // 引用不能置为空

    return 0;
}

在上面的例子中,我们定义了一个整型变量a,并分别用指针p和引用r来访问它。我们可以看到,指针p可以被重新赋值,可以指向空间未知的内存,而引用r一旦被初始化就不能再指向其他对象,也不能指向空间未知的内存。此外,指针p可以进行算术运算,而引用r不支持。

2.详述浅拷贝与深拷贝的区别,并通过C++举例说明

浅拷贝和深拷贝都是在C++中进行对象拷贝时的概念。

浅拷贝是指将一个对象的值复制到另一个对象中,这两个对象共享同一块内存空间。也就是说,当其中一个对象的值发生改变时,另一个对象的值也会随之改变。这种拷贝方式适用于简单的数据类型,如int、float等。

深拷贝是指将一个对象的值复制到另一个对象中,但是这两个对象拥有不同的内存空间。也就是说,当其中一个对象的值发生改变时,另一个对象的值不会受到影响。这种拷贝方式适用于复杂的数据类型,如数组、结构体等。

下面通过C++代码举例说明:

#include <iostream>
using namespace std;

class Person {
public:
    int age;
    char* name;
    Person(int age, char* name) {
        this->age = age;
        this->name = new char[strlen(name) + 1];
        strcpy(this->name, name);
    }
    // 拷贝构造函数
    Person(const Person& p) {
        this->age = p.age;
        this->name = new char[strlen(p.name) + 1];
        strcpy(this->name, p.name);
    }
    // 析构函数
    ~Person() {
        delete[] name;
    }
};

int main() {
    // 浅拷贝
    Person p1(18, "Tom");
    Person p2 = p1;
    cout << "p1.name: " << p1.name << endl;
    cout << "p2.name: " << p2.name << endl;
    p1.name[0] = 'J';
    cout << "p1.name: " << p1.name << endl;
    cout << "p2.name: " << p2.name << endl;

    // 深拷贝
    Person* p3 = new Person(20, "Jerry");
    Person* p4 = new Person(*p3);
    cout << "p3->name: " << p3->name << endl;
    cout << "p4->name: " << p4->name << endl;
    p3->name[0] = 'K';
    cout << "p3->name: " << p3->name << endl;
    cout << "p4->name: " << p4->name << endl;

    delete p3;
    delete p4;
    return 0;
}

输出结果为:

p1.name: Tom
p2.name: Tom
p1.name: Jom
p2.name: Jom
p3->name: Jerry
p4->name: Jerry
p3->name: Kerry
p4->name: Jerry

可以看到,浅拷贝的两个对象共享同一块内存空间,当其中一个对象的值发生改变时,另一个对象的值也会随之改变。而深拷贝的两个对象拥有不同的内存空间,当其中一个对象的值发生改变时,另一个对象的值不会受到影响。

3.指针引用与对象传参有什么区别,并通过C++举例说明

指针引用和对象传参都是C++中常用的参数传递方式,它们的区别在于传递的方式和对原始数据的修改方式。

指针引用是将指针作为参数传递给函数,函数内部可以通过指针修改原始数据的值。指针引用使用起来比较方便,可以直接修改原始数据,但是需要注意指针为空的情况。

对象传参是将对象作为参数传递给函数,函数内部对对象的修改不会影响原始数据的值。对象传参使用起来比较安全,不会对原始数据造成影响,但是需要注意对象的拷贝构造函数和析构函数的调用。

下面通过C++代码举例说明:

#include <iostream>
using namespace std;

class Person {
public:
    int age;
    Person(int age) {
        this->age = age;
    }
};

void changeByPointer(Person* p) {
    p->age = 20;
}

void changeByObject(Person p) {
    p.age = 20;
}

int main() {
    Person p1(10);
    cout << "p1 age: " << p1.age << endl; // 输出:p1 age: 10

    changeByPointer(&p1);
    cout << "p1 age after changeByPointer: " << p1.age << endl; // 输出:p1 age after changeByPointer: 20

    changeByObject(p1);
    cout << "p1 age after changeByObject: " << p1.age << endl; // 输出:p1 age after changeByObject: 20

    return 0;
}

在上面的代码中,我们定义了一个Person类,包含一个age属性。然后我们定义了两个函数changeByPointer和changeByObject,分别使用指针引用和对象传参的方式修改Person对象的age属性。最后在main函数中,我们创建了一个Person对象p1,并分别使用两种方式修改p1的age属性。从输出结果可以看出,使用指针引用的方式可以直接修改原始数据,而使用对象传参的方式不会对原始数据造成影响。

4.编程语言中关键字volatile有什么含义,并列出三个不同的例子

关键字volatile在编程语言中通常用于修饰变量,其含义是告诉编译器该变量可能会被其他线程或者外部因素修改,因此编译器不应该对该变量进行优化,以保证程序的正确性。

以下是三个不同的例子:

  1. 多线程编程中,一个线程修改了一个volatile变量的值,其他线程可以立即看到这个变化,而不是等待缓存同步或者其他操作。
  2. 嵌入式系统中,使用volatile修饰的变量通常是硬件寄存器的映射,这些寄存器的值可能会被硬件或者其他外部因素修改,因此需要使用volatile来确保程序正确地读取这些值。
  3. 在某些编译器中,使用volatile修饰的变量可以防止编译器对该变量进行优化,例如将变量缓存到寄存器中,从而确保程序正确地读取该变量的值。

5.编程语言中关键字const有什么含义,并列出两个不同的例子

const是一个关键字,用于定义常量。常量是指在程序运行期间不可更改的值。const关键字可以用于变量、函数参数和函数返回值。

以下是两个不同的例子:

  1. 声明一个常量变量
const int MAX_VALUE = 100;

在这个例子中,MAX_VALUE被定义为一个常量,其值为100。由于MAX_VALUE是一个常量,因此在程序运行期间不能更改它的值。

  1. 声明一个常量指针
int value = 10;
const int* ptr = &value;

在这个例子中,ptr被定义为一个指向常量的指针。由于ptr指向的是一个常量,因此不能通过ptr来修改value的值。但是,可以通过其他方式来修改value的值。

6.在C语言中,static关键字有三种使用位置和作用:

  1. 函数内部的static变量:在函数内部定义的static变量,其作用域仅限于该函数内部,不会被其他函数访问到。该变量在程序运行期间只会被初始化一次,而不是每次函数被调用时都会被初始化。

  2. 函数外部的static变量:在函数外部定义的static变量,其作用域仅限于该文件内部,不会被其他文件访问到。该变量在程序运行期间只会被初始化一次,而不是每次程序启动时都会被初始化。

  3. 函数声明中的static函数:在函数声明中使用static关键字,表示该函数仅限于本文件内部使用,不会被其他文件调用。这种函数也被称为静态函数。

总的来说,static关键字的作用是限制变量或函数的作用域,使其仅限于当前文件或函数内部使用,同时也可以控制变量或函数的生命周期。

7.算术运算符、赋值运算符和关系运算符他们的优先级高低?

在算术运算符、赋值运算符和关系运算符中,优先级高低如下:

  1. 算术运算符的优先级最高,包括乘法、除法、取模、加法和减法。

  2. 赋值运算符的优先级次之,包括等号、加等、减等、乘等、除等和取模等。

  3. 关系运算符的优先级最低,包括小于、大于、小于等于、大于等于、等于和不等于等。

需要注意的是,如果表达式中有括号,则括号内的运算优先级最高,会先被计算。同时,如果有多个同级别的运算符,会按照从左到右的顺序进行计算。

8.用C语言实现strcpy的代码:

#include <stdio.h>

char* strcpy(char* dest, const char* src) {
    char* p = dest;
    while (*src != '\0') {
        *p++ = *src++;
    }
    *p = '\0';
    return dest;
}

int main() {
    char str1[20] = "Hello";
    char str2[20];
    strcpy(str2, str1);
    printf("str2: %s\n", str2);
    return 0;
}

在上面的代码中,我们定义了一个strcpy函数,它接收两个参数:目标字符串dest和源字符串src。函数的作用是将源字符串src复制到目标字符串dest中,并返回目标字符串的指针。

在函数内部,我们定义了一个指针p,它指向目标字符串的起始位置。然后我们使用一个while循环,将源字符串中的每个字符复制到目标字符串中,直到遇到字符串结束符\0。最后,我们在目标字符串的末尾添加一个字符串结束符\0,并返回目标字符串的指针。

main函数中,我们定义了两个字符串str1str2,并将str1复制到str2中。最后,我们打印出str2的值,以验证strcpy函数的正确性。

9.用C语言实现strcat的代码:

#include <stdio.h>
#include <string.h>

char* my_strcat(char* dest, const char* src) {
    char* p = dest + strlen(dest);
    while (*src != '\0') {
        *p++ = *src++;
    }
    *p = '\0';
    return dest;
}

int main() {
    char str1[20] = "Hello, ";
    char str2[] = "world!";
    my_strcat(str1, str2);
    printf("%s\n", str1);
    return 0;
}

在上面的代码中,我们定义了一个名为my_strcat的函数,它接受两个参数:目标字符串dest和源字符串src。该函数的实现方式是,先找到dest字符串的末尾,然后将src字符串中的每个字符依次拷贝到dest字符串的末尾,直到src字符串的结尾。最后,将dest字符串的末尾设置为\0,以表示字符串的结束。

main函数中,我们定义了两个字符串str1str2,并将它们传递给my_strcat函数。最后,我们打印出拼接后的字符串str1

10.用C语言实现memcpy

在C语言中,可以使用memcpy函数来实现内存拷贝。memcpy函数的原型如下:

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

其中,dest表示目标内存地址,src表示源内存地址,n表示要拷贝的字节数。

下面是一个简单的实现:

void *my_memcpy(void *dest, const void *src, size_t n) {
    char *d = dest;
    const char *s = src;
    while (n--) {
        *d++ = *s++;
    }
    return dest;
}

这个实现使用了指针的自增运算符来逐个拷贝字节,直到拷贝完n个字节为止。注意,这个实现没有考虑内存重叠的情况,如果需要处理内存重叠,需要使用memmove函数。

10.字符串逆序

在C语言中,可以使用两个指针来实现字符串的逆序。以下是使用C语言实现字符串逆序的示例代码:

#include <stdio.h>
#include <string.h>

void reverseString(char* str) {
    int left = 0;
    int right = strlen(str) - 1;

    while (left < right) {
        // 交换左右指针所指向的字符
        char temp = str[left];
        str[left] = str[right];
        str[right] = temp;

        // 移动左右指针
        left++;
        right--;
    }
}

int main() {
    char str[] = "Hello, World!";
    printf("原始字符串:%s\n", str);

    reverseString(str);
    printf("逆序字符串:%s\n", str);

    return 0;
}

在上述示例代码中,我们定义了一个reverseString函数,该函数接收一个字符串作为参数,并将字符串逆序。函数内部使用两个指针leftright,初始时分别指向字符串的首尾字符。然后,通过交换左右指针所指向的字符,实现字符串的逆序。最后,输出逆序后的字符串。

运行以上代码,输出结果为:

原始字符串:Hello, World!
逆序字符串:!dlroW ,olleH

10.字符串转换成整数

在C语言中,可以使用sprintf函数将整数转换为字符串。以下是使用C语言实现整数转换成字符串的示例代码:

#include <stdio.h>

void intToString(int num, char* str) {
    sprintf(str, "%d", num);
}

int main() {
    int num = 12345;
    char str[20];
    intToString(num, str);
    printf("转换结果:%s\n", str);

    return 0;
}

在上述示例代码中,我们定义了一个intToString函数,该函数接收一个整数和一个字符数组作为参数,并将整数转换为字符串存储到字符数组中。函数内部使用sprintf函数,将整数格式化为字符串,并将结果存储到指定的字符数组中。

运行以上代码,输出结果为:

转换结果:12345

10. 字符串转换成整数

在C语言中,可以使用atoi函数将字符串转换为整数。以下是使用C语言实现字符串转换成整数的示例代码:

#include <stdio.h>
#include <stdlib.h>

int stringToInt(const char* str) {
    int result = 0;
    int sign = 1;
    int i = 0;

    // 处理符号位
    if (str[0] == '-') {
        sign = -1;
        i++;
    }

    // 遍历字符串中的每个字符
    while (str[i] != '\0') {
        // 判断字符是否为数字
        if (str[i] >= '0' && str[i] <= '9') {
            // 将字符转换为数字并累加到结果中
            result = result * 10 + (str[i] - '0');
            i++;
        } else {
            // 如果遇到非数字字符,则跳出循环
            break;
        }
    }

    // 返回最终结果
    return result * sign;
}

int main() {
    const char* str = "12345";
    int num = stringToInt(str);
    printf("转换结果:%d\n", num);

    return 0;
}

在上述示例代码中,我们定义了一个stringToInt函数,该函数接收一个字符串作为参数,并返回转换后的整数。函数内部使用循环遍历字符串中的每个字符,判断字符是否为数字,然后将其转换为对应的整数并累加到结果中。最后,根据符号位返回最终的结果。

运行以上代码,输出结果为:

转换结果:12345

11.C语言实现将A、B两个链表的元素交叉归并

下面是C语言实现将A、B两个链表的元素交叉归并的代码:

#include <stdio.h>
#include <stdlib.h>

typedef struct node {
    int data;
    struct node *next;
} Node;

Node *merge(Node *a, Node *b) {
    Node *head = NULL, *tail = NULL;
    while (a && b) {
        if (head == NULL) {
            head = tail = a;
            a = a->next;
            tail->next = b;
            tail = b;
            b = b->next;
        } else {
            tail->next = a;
            a = a->next;
            tail = tail->next;
            tail->next = b;
            b = b->next;
            tail = tail->next;
        }
    }
    if (a) {
        tail->next = a;
    }
    if (b) {
        tail->next = b;
    }
    return head;
}

int main() {
    Node *a = (Node *)malloc(sizeof(Node));
    a->data = 1;
    a->next = (Node *)malloc(sizeof(Node));
    a->next->data = 3;
    a->next->next = (Node *)malloc(sizeof(Node));
    a->next->next->data = 5;
    a->next->next->next = NULL;

    Node *b = (Node *)malloc(sizeof(Node));
    b->data = 2;
    b->next = (Node *)malloc(sizeof(Node));
    b->next->data = 4;
    b->next->next = (Node *)malloc(sizeof(Node));
    b->next->next->data = 6;
    b->next->next->next = NULL;

    Node *head = merge(a, b);
    while (head) {
        printf("%d ", head->data);
        head = head->next;
    }
    printf("\n");

    return 0;
}

这里定义了一个Node结构体表示链表节点,包含一个整数data和一个指向下一个节点的指针nextmerge函数接收两个链表ab,返回交叉归并后的链表头节点。在函数中,使用headtail两个指针分别指向交叉归并后的链表的头和尾,然后遍历ab两个链表,依次将节点插入到交叉归并后的链表中。最后,如果ab中还有剩余的节点,将其直接插入到交叉归并后的链表的尾部。最后返回交叉归并后的链表的头节点。

main函数中,创建两个链表ab,然后调用merge函数将它们交叉归并,并打印输出结果。

12.C语言实现将字符串中的指定字符全部删掉

可以使用C语言中的字符串处理函数来实现将字符串中的指定字符全部删除。以下是一个示例代码:

#include <stdio.h>
#include <string.h>

void delete_char(char *str, char c) {
    int len = strlen(str);
    int i, j;
    for (i = 0, j = 0; i < len; i++) {
        if (str[i] != c) {
            str[j++] = str[i];
        }
    }
    str[j] = '\0';
}

int main() {
    char str[100];
    char c;
    printf("请输入字符串:");
    scanf("%s", str);
    printf("请输入要删除的字符:");
    scanf(" %c", &c);
    delete_char(str, c);
    printf("删除后的字符串为:%s\n", str);
    return 0;
}

在上面的代码中,delete_char函数接收两个参数:一个字符串和一个字符。它遍历字符串中的每个字符,如果当前字符不等于指定字符,则将其复制到新的字符串中。最后,将新字符串的结尾设置为\0,以确保它是一个有效的C字符串。

main函数中,我们从用户输入中获取一个字符串和一个字符,并将它们传递给delete_char函数。最后,我们打印出删除指定字符后的字符串。

13.C语言实现将一个char组成的字符串循环右移n位

以下是C语言实现将一个char组成的字符串循环右移n位的代码:

#include <stdio.h>
#include <string.h>

void rightShift(char* str, int n) {
    int len = strlen(str);
    n %= len; // 取模,防止n大于字符串长度
    if (n == 0) return; // 如果n为0,直接返回
    char tmp[n];
    strncpy(tmp, str + len - n, n); // 将后n个字符复制到tmp中
    memmove(str + n, str, len - n); // 将前len-n个字符向右移动n位
    strncpy(str, tmp, n); // 将tmp中的n个字符复制到str的前n个位置
}

int main() {
    char str[] = "abcdefg";
    int n = 3;
    rightShift(str, n);
    printf("%s\n", str); // 输出结果为 "efgabcd"
    return 0;
}

代码思路:

  1. 先计算字符串长度len,然后对n取模,防止n大于字符串长度。
  2. 如果n为0,直接返回。
  3. 定义一个长度为n的字符数组tmp,将原字符串中后n个字符复制到tmp中。
  4. 使用memmove函数将原字符串中前len-n个字符向右移动n位。
  5. 最后将tmp中的n个字符复制到原字符串的前n个位置。
  6. 在主函数中测试函数rightShift的功能。

14.C语言实现检查一个整数是否是对称数字

可以通过将整数转换为字符串,然后判断字符串是否对称来实现检查一个整数是否是对称数字。

具体实现如下:

#include <stdio.h>
#include <string.h>

int is_symmetric(int num) {
    char str[20];
    sprintf(str, "%d", num); // 将整数转换为字符串
    int len = strlen(str);
    for (int i = 0; i < len / 2; i++) {
        if (str[i] != str[len - i - 1]) {
            return 0; // 如果不对称,返回0
        }
    }
    return 1; // 如果对称,返回1
}

int main() {
    int num;
    printf("请输入一个整数:");
    scanf("%d", &num);
    if (is_symmetric(num)) {
        printf("%d是对称数字\n", num);
    } else {
        printf("%d不是对称数字\n", num);
    }
    return 0;
}

在上面的代码中,我们使用sprintf函数将整数转换为字符串,然后使用循环判断字符串是否对称。如果字符串对称,返回1,否则返回0。最后在main函数中调用is_symmetric函数判断整数是否是对称数字。

10.C语言实现单链表节点的定义、插入、查询和删除的示例代码:

#include <stdio.h>
#include <stdlib.h>

// 定义单链表节点结构体
typedef struct ListNode {
    int val;
    struct ListNode* next;
} ListNode;

// 插入节点
void insertNode(ListNode** head, int val) {
    ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
    newNode->val = val;
    newNode->next = NULL;

    if (*head == NULL) {
        *head = newNode;
    } else {
        ListNode* cur = *head;
        while (cur->next != NULL) {
            cur = cur->next;
        }
        cur->next = newNode;
    }
}

// 查询节点
ListNode* searchNode(ListNode* head, int val) {
    ListNode* cur = head;
    while (cur != NULL) {
        if (cur->val == val) {
            return cur;
        }
        cur = cur->next;
    }
    return NULL;
}

// 删除节点
void deleteNode(ListNode** head, int val) {
    ListNode* cur = *head;
    ListNode* prev = NULL;

    while (cur != NULL) {
        if (cur->val == val) {
            if (prev == NULL) {
                *head = cur->next;
            } else {
                prev->next = cur->next;
            }
            free(cur);
            return;
        }
        prev = cur;
        cur = cur->next;
    }
}

// 打印链表
void printList(ListNode* head) {
    ListNode* cur = head;
    while (cur != NULL) {
        printf("%d ", cur->val);
        cur = cur->next;
    }
    printf("\n");
}

int main() {
    ListNode* head = NULL;

    // 插入节点
    insertNode(&head, 1);
    insertNode(&head, 2);
    insertNode(&head, 3);
    insertNode(&head, 4);

    // 打印链表
    printList(head);

    // 查询节点
    ListNode* node = searchNode(head, 3);
    if (node != NULL) {
        printf("Found node with value %d\n", node->val);
    } else {
        printf("Node not found\n");
    }

    // 删除节点
    deleteNode(&head, 2);

    // 打印链表
    printList(head);

    return 0;
}

在这个示例代码中,我们定义了一个单链表节点结构体,包含一个整数值和一个指向下一个节点的指针。我们还定义了插入节点、查询节点和删除节点的函数,并在主函数中演示了它们的使用。

11.如何区别指针数组和数组指针

指针数组和数组指针是两种不同的概念。

指针数组是一个数组,其中的每个元素都是一个指针。例如,int *arr[10]就是一个指针数组,其中有10个元素,每个元素都是一个指向int类型的指针。

数组指针是一个指针,它指向一个数组。例如,int (*arr)[10]就是一个数组指针,它指向一个有10个int类型元素的数组。

可以通过以下方式来区分它们:

  1. 声明方式不同:指针数组的声明方式是指针类型后面跟一个方括号,而数组指针的声明方式是一个指针类型,后面跟一个括号和一个方括号。

  2. 使用方式不同:指针数组可以用于存储指向不同类型的指针,而数组指针只能指向一个特定类型的数组。

  3. 访问方式不同:指针数组可以通过下标访问每个元素,而数组指针需要使用指针运算符来访问数组中的元素。

例如,以下代码声明了一个指针数组和一个数组指针,并演示了它们的使用方式:

int *ptr_arr[10]; // 指针数组
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int (*ptr)[10]; // 数组指针

ptr_arr[0] = &arr[0]; // 将数组的第一个元素的地址存储在指针数组的第一个元素中
ptr = &arr; // 将数组的地址存储在数组指针中

printf("%d\n", *ptr_arr[0]); // 输出数组的第一个元素
printf("%d\n", (*ptr)[0]); // 输出数组的第一个元素

12.详解函数指针

函数指针是指向函数的指针变量。它可以用来存储函数的地址,也可以用来调用函数。函数指针的语法如下:

返回值类型 (*指针变量名)(参数列表);

其中,指针变量名是一个标识符,用来表示指向函数的指针变量的名称。返回值类型是函数的返回值类型,参数列表是函数的参数列表。

例如,下面的代码定义了一个函数指针变量p,它指向一个返回值为int,参数为两个int类型的函数:

int (*p)(int, int);

函数指针的使用有两种方式:

  1. 将函数指针作为参数传递给其他函数。这种方式可以实现函数回调,即在一个函数中调用另一个函数,并将其作为参数传递给第一个函数。

  2. 直接调用函数指针所指向的函数。这种方式可以实现动态调用函数,即在程序运行时根据需要调用不同的函数。

下面是一个简单的例子,演示了函数指针的使用:

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int sub(int a, int b) {
    return a - b;
}

int main() {
    int (*p)(int, int);
    int a = 10, b = 5;

    p = add;
    printf("add(%d, %d) = %d\n", a, b, p(a, b));

    p = sub;
    printf("sub(%d, %d) = %d\n", a, b, p(a, b));

    return 0;
}

在上面的代码中,我们定义了两个函数add和sub,它们分别实现了加法和减法运算。然后我们定义了一个函数指针变量p,它可以指向任何一个返回值为int,参数为两个int类型的函数。在main函数中,我们先将p指向add函数,然后调用p(a, b)实现了加法运算;接着将p指向sub函数,然后调用p(a, b)实现了减法运算。这样,我们就可以通过函数指针动态调用不同的函数,实现了代码的灵活性和可扩展性。

13.变长数组(也称为动态数组)特性包括:

  1. 可以动态增加或减少数组的大小,不需要预先指定数组的大小。

  2. 可以使用与普通数组相同的方式访问元素,即使用下标索引。

  3. 可以在数组的末尾添加元素,也可以在数组的中间插入元素。

  4. 可以使用指针来访问变长数组的元素。

  5. 变长数组的大小可以在运行时动态改变,因此可以根据实际需要灵活地分配内存空间。

  6. 变长数组的内存空间是连续的,因此可以通过指针算术运算来访问数组元素。

  7. 变长数组的实现通常使用堆内存,因此可以避免栈溢出的问题。

需要注意的是,变长数组的动态扩展操作可能会导致内存分配失败或者性能下降,因此在使用变长数组时需要谨慎考虑其实际需求和性能表现。

14.给出布尔、整型、浮点和指针类型变量与“零值”的if比较

布尔类型变量的零值为false,可以用if语句进行比较:

bool b = false;
if (b == false) {
    // do something
}

整型变量的零值为0,可以用if语句进行比较:

int i = 0;
if (i == 0) {
    // do something
}

浮点类型变量的零值为0.0,但是由于浮点数的精度问题,不建议直接用等于号进行比较,可以使用一个误差范围进行比较:

float f = 0.0;
if (fabs(f) < 0.00001) {
    // do something
}

指针类型变量的零值为NULL,可以用if语句进行比较:

int* p = NULL;
if (p == NULL) {
    // do something
}

15.数组与链表的优缺点

数组和链表都是常见的数据结构,它们各有优缺点,下面是它们的比较:

数组的优点:

  1. 随机访问:可以通过下标直接访问数组中的任意元素,时间复杂度为O(1)。
  2. 内存连续:数组中的元素在内存中是连续存储的,因此可以利用CPU的缓存机制,提高访问效率。
  3. 空间利用率高:数组中的元素大小相同,不会浪费空间。

数组的缺点:

  1. 大小固定:数组一旦创建,大小就固定了,无法动态扩展或缩小。
  2. 插入和删除操作效率低:在数组中插入或删除元素时,需要移动其他元素,时间复杂度为O(n)。
  3. 内存浪费:如果数组中的元素数量比较少,会浪费一部分内存空间。

链表的优点:

  1. 动态扩展:链表的大小可以动态扩展或缩小,不会浪费内存空间。
  2. 插入和删除操作效率高:在链表中插入或删除元素时,只需要修改指针,时间复杂度为O(1)。
  3. 内存利用率高:链表中的元素大小可以不同,可以灵活利用内存空间。

链表的缺点:

  1. 随机访问效率低:链表中的元素在内存中不是连续存储的,因此无法利用CPU的缓存机制,访问效率较低。
  2. 需要额外的空间存储指针:链表中的每个元素都需要一个指针来指向下一个元素,因此需要额外的空间存储指针。
  3. 不支持随机访问:链表只能从头开始遍历,无法直接访问任意位置的元素。

16.请描述C程序内存布局,以及进程使用堆、栈的过程

C程序内存布局通常分为以下几个部分:

  1. 代码段(text segment):存放程序的可执行代码,通常是只读的。

  2. 数据段(data segment):存放程序中已经初始化的全局变量和静态变量,通常是可读写的。

  3. BSS段(bss segment):存放程序中未初始化的全局变量和静态变量,通常也是可读写的。

  4. 堆(heap):动态分配的内存空间,通常是由程序员手动申请和释放的,大小不固定。

  5. 栈(stack):存放函数调用时的局部变量、函数参数、返回地址等信息,大小固定,由系统自动分配和释放。

进程使用堆的过程通常是通过调用C标准库中的malloc()函数来实现的。该函数会在堆中分配一块指定大小的内存空间,并返回该空间的首地址。程序员可以在该空间中存储任意类型的数据,并在使用完毕后调用free()函数将其释放。

进程使用栈的过程通常是在函数调用时,系统会为该函数分配一块栈空间,用于存储该函数的局部变量、函数参数、返回地址等信息。当函数执行完毕后,系统会自动释放该栈空间。如果函数调用过程中栈空间不足,会导致栈溢出错误。

17.详述C++的this指针

在C++中,this指针是一个指向当前对象的指针。它是一个隐式参数,可以在成员函数中使用,用于访问当前对象的成员变量和成员函数。

当一个成员函数被调用时,编译器会将当前对象的地址作为this指针传递给函数。在函数内部,可以使用this指针来访问当前对象的成员变量和成员函数。

例如,假设有一个名为Person的类,其中有一个成员函数printName(),它打印出当前对象的名字。在printName()函数中,可以使用this指针来访问当前对象的名字:

class Person {
public:
    void printName() {
        cout << "My name is " << this->name << endl;
    }
private:
    string name;
};

int main() {
    Person p;
    p.printName(); // 输出 "My name is "
    return 0;
}

在上面的例子中,this指针被用来访问当前对象的成员变量name。注意,this指针是一个指向当前对象的指针,因此可以使用箭头运算符->来访问成员变量和成员函数。

此外,this指针还可以用于返回当前对象的引用,例如:

class Person {
public:
    Person& setName(string name) {
        this->name = name;
        return *this;
    }
private:
    string name;
};

int main() {
    Person p;
    p.setName("Tom").setName("Jerry"); // 连续调用setName()函数
    return 0;
}

在上面的例子中,setName()函数返回当前对象的引用,以便可以连续调用该函数。在函数内部,使用this指针来访问当前对象的成员变量name,并返回*this,即当前对象的引用。

18.C语言回调函数的本质是什么?描述回调函数的定义和使用方法

C语言中的回调函数本质上是一种函数指针,它允许我们将一个函数作为参数传递给另一个函数,并在需要的时候调用它。

回调函数的定义通常包括两个部分:回调函数本身的定义和调用回调函数的函数的定义。回调函数的定义与普通函数的定义相同,只是它的参数列表和返回值类型必须与调用它的函数所期望的相匹配。调用回调函数的函数通常会将回调函数作为一个参数传递,并在需要的时候调用它。

回调函数的使用方法如下:

  1. 定义回调函数,包括参数列表和返回值类型。

  2. 在需要使用回调函数的函数中,将回调函数作为参数传递。

  3. 在需要的时候,调用回调函数。

例如,下面的代码演示了如何使用回调函数来计算一个数组中的元素总和:

#include <stdio.h>

int sum(int *array, int size, int (*callback)(int)) {
    int total = 0;
    for (int i = 0; i < size; i++) {
        total += callback(array[i]);
    }
    return total;
}

int add_one(int x) {
    return x + 1;
}

int main() {
    int array[] = {1, 2, 3, 4, 5};
    int size = sizeof(array) / sizeof(int);
    int result = sum(array, size, add_one);
    printf("The sum is %d\n", result);
    return 0;
}

在上面的代码中,sum函数接受一个整数数组、数组大小和一个回调函数作为参数。回调函数add_one将传入的整数加1并返回结果。sum函数在遍历数组时调用回调函数,并将回调函数的返回值累加到总和中。最后,main函数调用sum函数并输出结果。

19.写几个简单的类,描述C++的继承、虚拟和多态

  1. 继承

继承是C++中面向对象编程的重要特性之一,它允许我们创建一个新的类,该类可以从现有的类中继承属性和方法。下面是一个简单的继承示例:

class Animal {
public:
    void eat() {
        cout << "Animal is eating." << endl;
    }
};

class Dog : public Animal {
public:
    void bark() {
        cout << "Dog is barking." << endl;
    }
};

在上面的示例中,我们定义了一个Animal类和一个Dog类。Dog类从Animal类继承,因此它可以使用Animal类中的所有方法和属性。例如,Dog类可以使用Animal类中的eat()方法。

  1. 虚拟函数

虚拟函数是C++中实现多态的一种方式。虚拟函数是在基类中声明的,但是可以在派生类中重新定义。当我们使用基类指针或引用调用虚拟函数时,将根据实际对象类型调用相应的函数。下面是一个简单的虚拟函数示例:

class Shape {
public:
    virtual void draw() {
        cout << "Drawing a shape." << endl;
    }
};

class Circle : public Shape {
public:
    void draw() {
        cout << "Drawing a circle." << endl;
    }
};

class Square : public Shape {
public:
    void draw() {
        cout << "Drawing a square." << endl;
    }
};

在上面的示例中,我们定义了一个Shape类和两个派生类Circle和Square。Shape类中的draw()方法是虚拟的,因此它可以在派生类中重新定义。当我们使用基类指针或引用调用draw()方法时,将根据实际对象类型调用相应的函数。

  1. 多态

多态是C++中面向对象编程的另一个重要特性。多态允许我们使用基类指针或引用来调用派生类中的方法。这样可以使代码更加灵活和可扩展。下面是一个简单的多态示例:

void drawShape(Shape* shape) {
    shape->draw();
}

int main() {
    Circle circle;
    Square square;

    drawShape(&circle);
    drawShape(&square);

    return 0;
}

在上面的示例中,我们定义了一个drawShape()函数,该函数接受一个Shape类的指针。我们可以将Circle类和Square类的对象传递给该函数,因为它们都是Shape类的派生类。当我们调用drawShape()函数时,将根据实际对象类型调用相应的draw()方法。这就是多态的实现方式。

20.写一个简单的类实现几个常用的操作符重载

以下是一个简单的类,实现了加法、减法、乘法和输出运算符的重载:

#include <iostream>

class MyNumber {
public:
    MyNumber(int num) : m_num(num) {}

    MyNumber operator+(const MyNumber& other) const {
        return MyNumber(m_num + other.m_num);
    }

    MyNumber operator-(const MyNumber& other) const {
        return MyNumber(m_num - other.m_num);
    }

    MyNumber operator*(const MyNumber& other) const {
        return MyNumber(m_num * other.m_num);
    }

    friend std::ostream& operator<<(std::ostream& os, const MyNumber& num) {
        os << num.m_num;
        return os;
    }

private:
    int m_num;
};

int main() {
    MyNumber num1(10);
    MyNumber num2(5);

    MyNumber sum = num1 + num2;
    MyNumber diff = num1 - num2;
    MyNumber prod = num1 * num2;

    std::cout << "Sum: " << sum << std::endl;
    std::cout << "Difference: " << diff << std::endl;
    std::cout << "Product: " << prod << std::endl;

    return 0;
}

在这个例子中,我们定义了一个名为 MyNumber 的类,它包含一个整数成员变量 m_num。我们重载了加法、减法和乘法运算符,使得我们可以像操作普通整数一样操作 MyNumber 类型的对象。我们还重载了输出运算符,以便我们可以使用 std::cout 输出 MyNumber 类型的对象。在 main 函数中,我们创建了两个 MyNumber 对象,并使用重载的运算符对它们进行操作。最后,我们使用 std::cout 输出了结果。

21.简单写一个c++单例的实现

以下是一个简单的C++单例模式的实现:

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }

    void doSomething() {
        // do something
    }

private:
    Singleton() {} // 构造函数私有化,防止外部实例化
    Singleton(const Singleton&) = delete; // 禁止拷贝构造函数
    Singleton& operator=(const Singleton&) = delete; // 禁止赋值运算符

    ~Singleton() {} // 析构函数私有化,防止外部删除实例
};

在这个实现中,我们使用了静态成员函数 getInstance() 来获取单例实例。这个函数中使用了一个静态局部变量 instance,它只会在第一次调用 getInstance() 时被初始化,之后每次调用都会返回同一个实例。这种实现方式保证了线程安全,因为静态局部变量的初始化是线程安全的。

为了防止外部实例化和删除单例,我们将构造函数和析构函数都设为私有。此外,我们还禁止了拷贝构造函数和赋值运算符,以防止单例被复制。

22.解释下char *p=malloc(0)

在C语言中,malloc函数用于在堆上分配一块指定大小的内存空间,并返回该内存空间的首地址。当传入的参数为0时,malloc函数会尝试分配一个大小为0的内存块。这种情况下,malloc函数的行为是不确定的,可能会返回NULL指针,也可能会返回一个非NULL的指针。

在代码中,char *p=malloc(0)表示分配一个大小为0的内存块,并将其首地址赋值给指针变量p。由于分配的内存块大小为0,因此p指向的内存空间是无效的,不能进行读写操作。这种情况下,p指针的值可能是NULL,也可能是一个非NULL的指针值,具体取决于malloc函数的实现。因此,使用malloc(0)分配内存空间是没有意义的,应该避免这种用法。

PART3——计算机网络相关

1.应用层的协议有哪些

应用层协议是计算机网络中的一种协议,用于定义应用程序之间的通信规则。常见的应用层协议有:

1. HTTP(超文本传输协议):用于在Web浏览器和Web服务器之间传输数据,是Web应用程序的基础。

2. FTP(文件传输协议):用于在计算机之间传输文件,支持上传和下载。

3. SMTP(简单邮件传输协议):用于在邮件客户端和邮件服务器之间传输电子邮件。

4. POP3(邮局协议版本3):用于从邮件服务器上下载电子邮件。

5. IMAP(互联网消息访问协议):用于在邮件客户端和邮件服务器之间传输电子邮件,支持在线邮件管理。

6. DNS(域名系统):用于将域名转换为IP地址,以便在Internet上进行通信。

7. Telnet(远程终端协议):用于在本地计算机上远程控制远程计算机。

8. SSH(安全外壳协议):用于在本地计算机上远程控制远程计算机,提供加密和安全性。

9. DHCP(动态主机配置协议):用于自动分配IP地址和其他网络配置信息。

10. SNMP(简单网络管理协议):用于管理和监控网络设备和系统。

2.ISO网络七层模型的每个层次及其作用:

  1. 物理层:负责传输数据的物理介质,如电缆、光纤等。

  2. 数据链路层:负责将数据分成帧并进行错误检测和纠正。

  3. 网络层:负责路由选择和数据包转发,使数据能够在不同的网络之间传输。

  4. 传输层:负责数据传输的可靠性和流量控制,如TCP和UDP协议。

  5. 会话层:负责建立、管理和终止会话,如远程登录和文件传输。

  6. 表示层:负责数据格式的转换和加密解密,如压缩和加密。

  7. 应用层:负责应用程序之间的通信,如电子邮件、文件传输和网页浏览。

ISO网络七层模型的作用是提供一个标准化的网络通信协议模型,使不同的计算机和网络设备能够相互通信。它还有助于网络协议的开发和实现,以及网络故障的诊断和解决。

3.TCP三次握手,四次挥手过程。

三次握手过程:

  1. 客户端向服务器发送SYN(同步)报文,请求建立连接,此时客户端进入SYN_SENT状态。

  2. 服务器收到SYN报文后,回复一个SYN+ACK(同步+确认)报文,表示收到请求并同意建立连接,此时服务器进入SYN_RCVD状态。

  3. 客户端收到SYN+ACK报文后,回复一个ACK(确认)报文,表示已经收到服务器的确认,此时客户端和服务器都进入ESTABLISHED状态,连接建立成功。

四次挥手过程:

  1. 客户端向服务器发送FIN(结束)报文,请求关闭连接,此时客户端进入FIN_WAIT_1状态。
  2. 服务器收到FIN报文后,回复一个ACK报文,表示已经收到请求,此时服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态。
  3. 服务器完成数据传输后,向客户端发送FIN报文,请求关闭连接,此时服务器进入LAST_ACK状态。
  4. 客户端收到FIN报文后,回复一个ACK报文,表示已经收到请求,此时客户端进入TIME_WAIT状态,等待2MSL(最长报文段寿命)后关闭连接,服务器收到ACK报文后进入CLOSED状态,连接关闭成功。

4.TCP协议通过以下方式保证可靠性:

  1. 应用数据被分割成TCP报文段,每个报文段都有一个序号和确认号,确保数据的有序传输和接收。

  2. TCP协议使用滑动窗口机制,控制发送方和接收方之间的数据流量,避免数据的丢失和拥塞。

  3. TCP协议使用三次握手建立连接,确保双方的通信能力和可靠性。

  4. TCP协议使用超时重传机制,当发送方没有收到确认信息时,会重新发送数据,确保数据的可靠传输。

  5. TCP协议使用累计确认机制,接收方只需要确认已经接收到的数据,避免重复传输和浪费带宽。

    通过以上机制,TCP协议可以保证数据的可靠传输,确保数据的完整性和正确性。

5.TCP和UDP特性:

TCP:

  • 面向连接:在数据传输前,需要建立连接,传输完成后需要断开连接。
  • 可靠性高:TCP会对数据进行校验和确认,确保数据的可靠性。
  • 有序性:TCP会对数据进行排序,保证数据的有序性。
  • 流量控制:TCP会根据网络状况进行流量控制,避免网络拥塞。
  • 慢启动:TCP会在连接建立时进行慢启动,逐渐增加传输速率,避免网络拥塞。

UDP:

  • 无连接:UDP不需要建立连接,直接传输数据。
  • 可靠性低:UDP不会对数据进行校验和确认,数据传输过程中可能会出现丢失或重复。
  • 无序性:UDP不会对数据进行排序,数据传输过程中可能会出现乱序。
  • 无流量控制:UDP不会进行流量控制,可能会导致网络拥塞。
  • 快速:UDP传输速度快,适合实时性要求高的应用场景,如视频、音频等。

6.UDP和TCP的共同之处有哪些?

UDP和TCP都是互联网传输层协议,它们的共同之处包括:

  1. 都是面向通信的协议,用于在网络中传输数据。

  2. 都使用IP协议作为底层协议。

  3. 都支持端口号,用于标识不同的应用程序。

  4. 都使用校验和来保证数据的完整性。

  5. 都支持多路复用和分用,可以在同一个连接上传输多个数据流。

  6. 都可以通过网络套接字API进行编程实现。

  7. 都可以在应用层上实现可靠性和流量控制等功能。

尽管UDP和TCP有许多共同点,但它们的工作方式和特点也有很大的不同。UDP是无连接的、不可靠的协议,适用于实时性要求高、数据传输量小的应用场景;而TCP是面向连接的、可靠的协议,适用于数据传输量大、要求可靠性高的应用场景。


PART4——机器内存、架构相关

1.堆和栈的区别?

堆和栈都是计算机内存中的存储区域,但它们有以下区别:

  1. 内存分配方式不同:栈是由编译器自动分配和释放的,而堆是由程序员手动分配和释放的。

  2. 内存分配效率不同:栈的内存分配和释放速度非常快,因为它是由编译器自动管理的,而堆的内存分配和释放速度相对较慢,因为它需要程序员手动管理。

  3. 内存分配大小不同:栈的内存分配大小是固定的,通常在编译时就已经确定了,而堆的内存分配大小是动态的,可以根据程序的需要进行调整。

  4. 内存分配方式不同:栈是一种后进先出(LIFO)的数据结构,而堆是一种无序的数据结构。

  5. 内存使用方式不同:栈主要用于存储局部变量、函数参数和返回值等,而堆主要用于存储动态分配的内存,如数组、对象等。

总的来说,栈和堆都是计算机内存中的重要存储区域,它们各自有不同的特点和用途,程序员需要根据实际情况选择合适的存储方式。

2.给一个绝对地址为0xA9687894的变量赋值为0xCC66

下面是C语言的代码示例:

unsigned int *ptr = (unsigned int *)0xA9687894; // 将指针指向绝对地址为0xA9687894的变量
*ptr = 0xCC66; // 给该变量赋值为0xCC66

需要注意的是,直接操作绝对地址可能会导致系统崩溃或数据损坏,应该谨慎使用。

3.什么是内存泄漏?造成内存泄漏的原因有哪些?

内存泄漏指的是程序在运行过程中,申请的内存空间没有被及时释放,导致系统中的可用内存越来越少,最终可能导致系统崩溃或者运行缓慢。

造成内存泄漏的原因有以下几种:

  1. 程序中存在未释放的动态内存:程序在运行过程中,通过malloc等函数动态申请内存空间,但是在使用完毕后没有及时释放,导致内存泄漏。
  2. 循环引用:在使用面向对象编程语言时,如果两个对象之间相互引用,且没有及时解除引用,就会导致内存泄漏。
  3. 文件描述符未关闭:在程序中打开文件时,如果没有及时关闭文件,就会导致文件描述符泄漏,从而导致内存泄漏。
  4. 程序中存在死循环:如果程序中存在死循环,就会导致程序一直占用内存,从而导致内存泄漏。
  5. 程序中存在资源未释放:如果程序中使用了其他资源,如数据库连接、网络连接等,但是在使用完毕后没有及时释放,也会导致内存泄漏。

PART5——算法相关

1.常见的排序算法有以下几种:

  1. 冒泡排序(Bubble Sort):比较相邻的元素,如果前一个比后一个大,就交换它们的位置。时间复杂度为O(n^2),是一种稳定的排序算法。

  2. 选择排序(Selection Sort):每次从未排序的元素中选择最小的元素,放到已排序的末尾。时间复杂度为O(n^2),不稳定。

  3. 插入排序(Insertion Sort):将未排序的元素插入到已排序的合适位置。时间复杂度为O(n^2),是一种稳定的排序算法。

  4. 快速排序(Quick Sort):选择一个基准元素,将小于基准元素的放在左边,大于基准元素的放在右边,再对左右两边递归进行快速排序。时间复杂度为O(nlogn),不稳定。

  5. 归并排序(Merge Sort):将数组分成两个子数组,分别进行归并排序,再将两个有序的子数组合并成一个有序的数组。时间复杂度为O(nlogn),是一种稳定的排序算法。

  6. 堆排序(Heap Sort):将数组构建成一个最大堆,每次将堆顶元素与最后一个元素交换,然后重新构建最大堆。时间复杂度为O(nlogn),不稳定。

排序算法的快慢取决于算法的时间复杂度,一般来说,时间复杂度越低,排序速度越快。但是在实际应用中,还需要考虑算法的稳定性、空间复杂度等因素。

2. 二分查找(Binary Search)

一种在有序数组中查找特定元素的搜索算法。其基本思想是将数组分成两部分,取中间值与目标值进行比较,如果中间值大于目标值,则在左半部分继续查找;如果中间值小于目标值,则在右半部分继续查找;如果中间值等于目标值,则查找成功。

以下是C语言实现二分查找的代码:

#include <stdio.h>

int binary_search(int arr[], int n, int target) {
    int left = 0, right = n - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
}

int main() {
    int arr[] = {1, 3, 5, 7, 9};
    int n = sizeof(arr) / sizeof(arr[0]);
    int target = 5;
    int index = binary_search(arr, n, target);
    if (index == -1) {
        printf("Target not found.\n");
    } else {
        printf("Target found at index %d.\n", index);
    }
    return 0;
}

在上面的代码中,binary_search函数接收一个有序数组、数组长度和目标值作为参数,返回目标值在数组中的下标。如果目标值不存在于数组中,则返回-1。

binary_search函数中,使用leftright两个指针分别指向数组的左右两端。在每次循环中,计算中间位置mid,并将目标值与中间值进行比较。如果中间值等于目标值,则查找成功,返回中间位置mid;如果中间值小于目标值,则在右半部分继续查找,将left指针移到mid + 1的位置;如果中间值大于目标值,则在左半部分继续查找,将right指针移到mid - 1的位置。如果最终未找到目标值,则返回-1。

main函数中,定义一个有序数组arr,并计算数组长度n。定义目标值target为5,调用binary_search函数查找目标值在数组中的下标,并将结果输出到控制台。

3.C语言实现,使用位运算实现二进制数的逆序:

#include <stdio.h>

unsigned int reverseBits(unsigned int num) {
    unsigned int reversed = 0;
    int bits = sizeof(num) * 8; // 计算整数的位数

    while (bits--) {
        reversed <<= 1; // 左移一位
        reversed |= num & 1; // 取出最低位并加入到逆序数中
        num >>= 1; // 右移一位
    }

    return reversed;
}

int main() {
    unsigned int num = 34520;
    printf("原数的二进制表示:%08x\n", num);
    unsigned int reversed = reverseBits(num);
    printf("逆序后的二进制表示:%08x\n", reversed);
    return 0;
}

输出结果为:

原数的二进制表示:000086d8
逆序后的二进制表示:0000001b0061

其中,reverseBits函数实现了二进制数的逆序,bits变量计算整数的位数,reversed变量存储逆序后的二进制数,num变量存储原数。在循环中,每次将reversed左移一位,然后将num的最低位加入到reversed中,最后将num右移一位。循环结束后,reversed中存储的就是逆序后的二进制数。

PART6 试卷

笔试试卷:

一、 简答题(每题2分,共计40分)

  1. 什么是嵌入式系统?
  2. 嵌入式系统和通用计算机之间有哪些区别?
  3. 请简述Linux内核的体系结构。
  4. 什么是驱动程序?
  5. 请简述字符设备驱动和块设备驱动的区别。
  6. 什么是中断?中断的作用是什么?
  7. 请简述进程与线程的区别。
  8. 什么是进程调度?请列举几种进程调度算法。
  9. 什么是虚拟内存?它的作用是什么?
  10. 什么是信号?信号的作用是什么?
  11. 什么是进程间通信?请列举几种进程间通信的方式。
  12. 什么是文件系统?请简述典型的Linux文件系统。
  13. 请简述Linux启动过程。
  14. 什么是交叉编译?为什么要使用交叉编译?
  15. 什么是Makefile?请简述Makefile的语法规则。
  16. 请简述gcc的编译选项。
  17. 什么是交叉工具链?请简述交叉工具链的组成部分。
  18. 请简述Linux开发板的启动过程。
  19. 什么是设备树?请简述设备树的作用。
  20. 请简述Linux系统调用的作用。

二、 填空题(每题1分,共计40分)

  1. 嵌入式系统通常具有 __________、 __________、 __________ 的特点。
  2. Linux内核由 __________ 和 __________ 两部分组成。
  3. Linux内核主要分为 __________、 __________、 __________ 三层。
  4. Linux内核中的 __________ 提供了驱动程序的接口。
  5. 字符设备驱动是针对 __________ 的操作,而块设备驱动是针对 __________ 的操作。
  6. Linux中 __________ 和 __________ 是两种定时器机制。
  7. 进程是 __________ 的基本单位,线程是 __________ 的基本单位。
  8. 进程调度需要保证 __________ 和 __________ 两个方面的公平性。
  9. 虚拟内存技术可以提供 __________ 和 __________ 两种优势。
  10. kill命令可以向进程发送 __________。
  11. 进程间通信的方式包括 __________、 __________、 __________、 __________ 四种。
  12. 文件系统可以分为 __________、 __________、 __________ 三类。
  13. Linux启动过程可以分为 __________、 __________、 __________ 三个阶段。
  14. 交叉编译是指在一台主机上编译出在另一种 __________ 上运行的程序。
  15. Makefile中的变量以 __________ 开头。
  16. gcc的编译选项包括 __________、 __________、 __________ 三类。
  17. 交叉工具链的组成部分包括 __________、 __________、 __________ 三部分。
  18. Linux开发板的启动过程可以分为 __________、 __________、 __________ 三个阶段。
  19. 设备树是一种描述 __________ 的数据结构。
  20. Linux系统调用为用户提供了 __________ 的编程接口。

笔试试卷答案:

一、 简答题

  1. 嵌入式系统指嵌入在其他设备中,通常面向特定应用场景,功能单一,资源有限的计算机系统。
  2. 区别如下:
    • 嵌入式系统通常是面向特定应用场景,以完成特定任务为目的;通用计算机则是面向大众,用于广泛的数据处理和通讯。
    • 嵌入式系统资源有限,包括处理器性能、存储容量和带宽等;通用计算机资源相对丰富。
    • 嵌入式系统往往需要保证实时性,即保证任务能够在规定时间内完成,而通用计算机则不需要保证实时性。
  3. Linux内核包括两部分:内核空间代码和用户空间代码。内核空间代码包括系统调用接口、进程管理、文件系统、网络协议栈等。用户空间代码包括shell和应用程序等。
  4. 驱动程序指对硬件设备进行操作的系统软件。它提供了操作系统与硬件设备之间的接口,允许软件和硬件之间进行通信和交互。
  5. 字符设备驱动是对字符设备进行操作的驱动程序,而块设备驱动是对块设备进行操作的驱动程序。区别在于字符设备是基于字符或字节流的设备,而块设备则是基于块的设备。
  6. 中断是由硬件或软件触发的一种事件,可以打断当前程序的执行,转而执行指定的处理程序。中断的作用是提高系统的响应速度,以及处理外部设备的请求和异常情况。
  7. 进程是操作系统中的一个执行单元,具有独立的地址空间和控制信息。线程是进程内部的一个执行单元,共享同一地址空间和控制信息。
  8. 进程调度是操作系统对进程分配CPU时间的方式。常用的算法包括先来先服务、最短作业优先、时间片轮转、多级反馈队列等。
  9. 虚拟内存是一种技术,将实际的物理内存抽象成逻辑上连续的内存空间。它可以提供更高的内存利用率和更大的地址空间,同时也能够实现内存保护和进程间隔离等功能。
  10. 信号是一种异步的事件通知机制,用于向进程发送通知并请求其进行相应的处理。信号的作用是在进程间进行通信和事件处理。
  11. 进程间通信(IPC)是指在不同进程之间进行数据传输和同步的机制。常见的方式包括管道、信号量、消息队列和共享内存等。
  12. 文件系统是指对计算机上的文件和目录进行管理和组织的一种软件。典型的Linux文件系统包括EXT2、EXT3和EXT4等。
  13. Linux启动过程包括三个阶段:BIOS启动、BootLoader启动和内核启动。
  14. 交叉编译是针对其他平台而非当前平台的编译过程,可以在一台主机(开发机)上编译出在目标机上运行的程序。
  15. Makefile是一个用于自动化编译的脚本文件,它包含了编译规则、依赖关系和构建命令等信息。Makefile的语法规则包括变量、目标、依赖和命令四个要素。
  16. gcc的编译选项包括预处理选项(-E)、优化选项(-O)、调试选项(-g)、输出选项(-o)等。
  17. 交叉工具链是一套在主机上运行的工具集,用于生成在目标机上运行的可执行二进制文件。其组成部分包括交叉编译器、交叉链接器、交叉汇编器等。
  18. Linux开发板的启动过程包括三个阶段:硬件初始化、BootLoader加载和内核启动。
  19. 设备树(Device Tree)是一种描述系统中硬件设备、驱动程序和中断等信息的数据结构,用于解决固化的硬件资源分配问题。
  20. Linux系统调用提供了一组API,用于访问底层硬件资源、操作文件系统和进行进程间通信等。

二、 填空题

  1. 实时性、功能单一、资源有限
  2. 内核空间代码、用户空间代码
  3. 系统调用接口层、进程管理层、虚拟文件系统层
  4. 设备驱动
  5. 字符设备、块设备
  6. 定时器、软定时器
  7. 资源、公平性
  8. 活跃性、公平性
  9. 内存保护、进程隔离
  10. 信号
  11. 管道、信号量、消息队列、共享内存
  12. FAT、NTFS、EXT2/3/4
  13. BIOS启动、BootLoader启动、内核启动
  14. 平台
  15. $开头的变量名
  16. 常用选项、优化选项、输出选项
  17. 交叉编译器、交叉链接器、交叉汇编器
  18. 硬件初始化、BootLoader加载、内核启动
  19. 硬件设备
  20. 底层硬件资源
  • 在ps aux命令中,a、u、x是三个选项,分别代表以下含义:

    • a:显示所有用户的进程,包括其他用户的进程。
    • u:以用户为主的格式来显示进程信息,包括进程的所有者、CPU占用率、内存占用率等。
    • x:显示没有控制终端的进程,通常是守护进程或后台进程。

    因此,ps aux命令可以列出所有用户的进程,并以用户为主的格式来显示进程信息,包括没有控制终端的进程

  • 在Linux系统中,可以使用ps命令结合grep命令来查找某个进程的详细信息。具体命令如下:

    ps aux | grep <进程名或进程ID>
    

    其中,ps aux命令用于列出所有进程的详细信息,而grep命令用于过滤出包含指定进程名或进程ID的行。这样就可以找到指定进程的详细信息了。

    如果想要查看进程的启动参数、环境变量等信息,可以使用以下命令:

    cat /proc/<进程ID>/cmdline cat /proc/<进程ID>/environ
    

    其中,/proc/<进程ID>/cmdline文件包含了进程的启动参数信息,而/proc/<进程ID>/environ文件包含了进程的环境变量信息。可以使用cat命令来查看这些文件的内容。需要注意的是,这些文件只能被进程本身或者root用户访问。
    进行操作的驱动程序,而块设备驱动是对块设备进行操作的驱动程序。区别在于字符设备是基于字符或字节流的设备,而块设备则是基于块的设备。

  1. 中断是由硬件或软件触发的一种事件,可以打断当前程序的执行,转而执行指定的处理程序。中断的作用是提高系统的响应速度,以及处理外部设备的请求和异常情况。
  2. 进程是操作系统中的一个执行单元,具有独立的地址空间和控制信息。线程是进程内部的一个执行单元,共享同一地址空间和控制信息。
  3. 进程调度是操作系统对进程分配CPU时间的方式。常用的算法包括先来先服务、最短作业优先、时间片轮转、多级反馈队列等。
  4. 虚拟内存是一种技术,将实际的物理内存抽象成逻辑上连续的内存空间。它可以提供更高的内存利用率和更大的地址空间,同时也能够实现内存保护和进程间隔离等功能。
  5. 信号是一种异步的事件通知机制,用于向进程发送通知并请求其进行相应的处理。信号的作用是在进程间进行通信和事件处理。
  6. 进程间通信(IPC)是指在不同进程之间进行数据传输和同步的机制。常见的方式包括管道、信号量、消息队列和共享内存等。
  7. 文件系统是指对计算机上的文件和目录进行管理和组织的一种软件。典型的Linux文件系统包括EXT2、EXT3和EXT4等。
  8. Linux启动过程包括三个阶段:BIOS启动、BootLoader启动和内核启动。
  9. 交叉编译是针对其他平台而非当前平台的编译过程,可以在一台主机(开发机)上编译出在目标机上运行的程序。
  10. Makefile是一个用于自动化编译的脚本文件,它包含了编译规则、依赖关系和构建命令等信息。Makefile的语法规则包括变量、目标、依赖和命令四个要素。
  11. gcc的编译选项包括预处理选项(-E)、优化选项(-O)、调试选项(-g)、输出选项(-o)等。
  12. 交叉工具链是一套在主机上运行的工具集,用于生成在目标机上运行的可执行二进制文件。其组成部分包括交叉编译器、交叉链接器、交叉汇编器等。
  13. Linux开发板的启动过程包括三个阶段:硬件初始化、BootLoader加载和内核启动。
  14. 设备树(Device Tree)是一种描述系统中硬件设备、驱动程序和中断等信息的数据结构,用于解决固化的硬件资源分配问题。
  15. Linux系统调用提供了一组API,用于访问底层硬件资源、操作文件系统和进行进程间通信等。

二、 填空题

  1. 实时性、功能单一、资源有限
  2. 内核空间代码、用户空间代码
  3. 系统调用接口层、进程管理层、虚拟文件系统层
  4. 设备驱动
  5. 字符设备、块设备
  6. 定时器、软定时器
  7. 资源、公平性
  8. 活跃性、公平性
  9. 内存保护、进程隔离
  10. 信号
  11. 管道、信号量、消息队列、共享内存
  12. FAT、NTFS、EXT2/3/4
  13. BIOS启动、BootLoader启动、内核启动
  14. 平台
  15. $开头的变量名
  16. 常用选项、优化选项、输出选项
  17. 交叉编译器、交叉链接器、交叉汇编器
  18. 硬件初始化、BootLoader加载、内核启动
  19. 硬件设备
  20. 底层硬件资源
  • 在ps aux命令中,a、u、x是三个选项,分别代表以下含义:

    • a:显示所有用户的进程,包括其他用户的进程。
    • u:以用户为主的格式来显示进程信息,包括进程的所有者、CPU占用率、内存占用率等。
    • x:显示没有控制终端的进程,通常是守护进程或后台进程。

    因此,ps aux命令可以列出所有用户的进程,并以用户为主的格式来显示进程信息,包括没有控制终端的进程

  • 在Linux系统中,可以使用ps命令结合grep命令来查找某个进程的详细信息。具体命令如下:

    ps aux | grep <进程名或进程ID>
    

    其中,ps aux命令用于列出所有进程的详细信息,而grep命令用于过滤出包含指定进程名或进程ID的行。这样就可以找到指定进程的详细信息了。

    如果想要查看进程的启动参数、环境变量等信息,可以使用以下命令:

    cat /proc/<进程ID>/cmdline cat /proc/<进程ID>/environ
    

    其中,/proc/<进程ID>/cmdline文件包含了进程的启动参数信息,而/proc/<进程ID>/environ文件包含了进程的环境变量信息。可以使用cat命令来查看这些文件的内容。需要注意的是,这些文件只能被进程本身或者root用户访问。

07-03 06:27