1、static关键字了解么,它的作用是什么?

1、static修饰局部变量时:①对其存储位置进行改变,存储在静态区;②改变其生命周期,为整个源程序,因此它只被初始化一次,并且被声明为静态的变量在这一函数被调用过程中维持其值不变。

2、static修饰全局变量时:改变其作用域,在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问,它是一个本地的全局变量(只能被当前文件使用)。

3、static修饰函数时:改变其作用域,一个被声明为静态的函数只可被这一模块内的其它函数调用。即,这个函数被限制在声明它的模块的本地范围内使用(只能被当前文件使用)

修饰局部变量实例:

#include <stdio.h>

void nonStaticVarFunction() {
    // 没有使用static修饰的局部变量
    int var = 0;
    var++;
    printf("Non-static var: %d\n", var); // 输出的值总是 1
}

void staticVarFunction() {
    // 使用static修饰的局部变量
    static int staticVar = 0;
    staticVar++;
    printf("Static var: %d\n", staticVar); // 输出的值会递增
}

int main() {
    for (int i = 0; i < 3; i++) {
        nonStaticVarFunction();
    }
    for (int i = 0; i < 3; i++) {
        staticVarFunction();
    }
    return 0;
}

输出结果:

Non-static var: 1
Non-static var: 1
Non-static var: 1
Static var: 1
Static var: 2
Static var: 3

解析:这个例子展示了没有使用static修饰的局部变量var每次调用nonStaticVarFunction函数时都会被重新初始化为0,然后增加1并打印出来,因此输出总是1。而static修饰的局部变量staticVar在第一次调用staticVarFunction函数时初始化为0,之后每次调用函数都会保留上次调用结束时的值,并继续递增,所以输出是递增的。这说明了static`修饰符对局部变量生命周期的影响,使变量跨函数调用保持状态。

修饰函数实例:假设我们有两个源文件:main.chelp.c。我们在help.c中定义了一个static函数,这意味着这个函数只能在help.c中被调用,尝试在main.c中调用它将导致链接错误。

#include <stdio.h>

// static修饰的函数,仅在本文件内可见
static void printHello() {
    printf("Hello from helper.c\n");
}

// 全局函数,可以被其他文件调用
void callPrintHello() {
    printHello();
}

输出结果:

#include <stdio.h>

// 函数声明
void callPrintHello();

int main() {
    // 调用helper.c中定义的函数
    callPrintHello();  // 正确,因为callPrintHello是全局的
    // printHello();    // 错误,无法编译,因为printHellomain.c中不可见
    return 0;
}

解析:在这个例子中,尽管main.c尝试调用printHello函数将失败,但我们可以通过调用callPrintHello间接调用printHello。这是因为printHello函数是静态的,它的作用域被限制在help.c文件内。这样的设计提高了程序的模块化。


2、select的作用是什么,它和epoll的区别?

select函数的原理:

1、文件描述符的数量:单个进程监视的文件描述符的数量有上限(通常由FD_SETSIZE宏定义,经常是1024)。

2、处理机制、效率:select采用轮询的方式对文件描述符进行扫描,由于需要对所有的文件描述符fd进行遍历),文件描述符越多,效率越低。

3、内核、用户空间内存拷贝:由于select每次都会改变内核中的句柄数据结构集(fd集合),因此每次调用select都需要从用户空间向内核空间复制所有的句柄数据结构(fd集合),开销比较大

4、触发方式:select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次调用select还是会将这些文件描述符通知进程。

优点:

1、select的可移植性比较好,几乎在所有系统上都有支持

2、select可设置的监听时间timeout精度更好,可精确到微妙

缺点:

1、监听的文件描述符数量存在上限1024,不能根据用户需求进行更改

2、select需要在内核和用户空间之间复制整个文件描述符,开销较大

3、轮询的处理方式,当文件描述符数量很大时效率较低

epoll的原理及优势:

1、处理效率更高:epoll能够调用epoll_wait不断轮询就绪链表,期间可能多次睡眠和唤醒交替,但是它是在设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进行睡眠的进程。select需要遍历整个fd集合,而epoll只需判断就绪链表是否为空即可,节省了大量CPU的时间。这就是回调机制带来的性能提升。

2、节省开销:select在每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把 current 往设备等待队列中挂一次,而 epoll 只要一次拷贝,而且把 current 往等待队列上挂也只挂一次(在 epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列),这节省不少的开销。


3、map与set了解么,有什么区别

map和set是C++标准库(STL)中的两种重要的容器类型,其底层实现都是红黑树

map的原理:

1、map是一个关联容器,它存储的是键值对(key-value pair),其中每个键都是唯一的,而且每个键都映射到一个值。map允许根据键来快速检索、删除或修改对应的值。

set的原理:

1、set也是一个关联容器,但它仅存储唯一的键(或元素),没有“值”的概念,每个元素都是唯一的。set主要用于快速检查一个元素是否存在于集合中。

两者区别:

1、存储内容:map存储的是键值对,每一个键映射到一个特定的值,而set仅存储值(或称为元素),并不关联任何值

2、用途:map支持下标操作,而set不支持下标操作,如果你需要建立一个元素到另一个元素的映射关系,应使用map;如果你只需要维护一个元素集合来快速检查某个元素是否存在,使用set更合适


4、GDB调试的基本操作,以及如何去追踪变量、查看堆栈信息

编译程序以便于调试

gcc -g program.c -o program

启动GDB

gdb  program

GDB的基本操作

1、使用run运行程序

(gdb) run

2、设置断点

(gdb) break main  # main函数开始处设置断点
(gdb) break file.c:10  # file.c的第10行设置断点

3、继续执行(当程序在断点处停止后,使用continue命令继续执行

(gdb) continue

4、单步执行(使用step执行下一行代码)

(gdb) step

5、查看变量值(使用print命令查看变量值)

(gdb) print variableName

6、退出GDB

(gdb) quit

7、设置条件断点(如果想在变量达到特定值时停止程序,可以设置条件断点)

(gdb) break file.c:20 if variableName==value

8、追踪变量,观察点(GDB的watch命令允许你设置观察点,程序在变量值改变时会自动停下来

(gdb) watch variableName

9、查看堆栈信息(backtrace命令查看当前调用堆栈)

(gdb) backtrace

(gdb) backtrace full

backtrace full不仅显示函数调用的序列,还会显示每个栈中的局部变量和参数的值

(gdb) frame 2

使用frame命令能够切换到特定的栈帧中,可以查看在那个特定栈帧的上下文的变量值和状态。上面命令为切换到栈帧2,可以在这个栈帧的上下文执行命令。比如查看变量等

实例:

#include <stdio.h>

void print_hello(int times) {
    for (int i = 0; i < times; i++) {
        printf("Hello, world!\n");
    }
}

void test() {
    char *ptr = NULL;
    *ptr = 10;  // 故意造成段错误
}

int main() {
    print_hello(3);
    test();
    return 0;
}

在这个程序中,test函数会导致一个段错误。你可以编译这个程序并使用GDB来调试它。当程序崩溃时,你可以使用backtrace命令来查看导致错误的函数调用序列。

通过查看堆栈信息,能够更容易地找到程序出错的地方以及程序执行的路径。

常用的GDB调试命令

gcc -g test.c -o test   #编译程序以便于调试
gdb test     #启动调试
help        #查看命令帮助,具体命令查询在gdb中输入help+命令,简写h
run        #重新开始运行文件,简写r
start       #单步执行,运行程序,停在第一执行语句
list   #查看原代码(list-n,从第n行开始查看代码。1ist+函数名:查看具体函数),简写1set#设置变量的值
next    #单步调试((逐过程,函数直接执行),简写n
step    #单步调试(逐语句:跳入自定义函数内部执行),简写sbacktrace #查看函数的调用的栈帧和层级关系,简写bt
frame    #切换函数的栈帧,简写f
info     #查看函数内部局部变量的数值,简写ifinish #结束当前函数,返回到函数调用点
continue   #继续运行,简写c
print    #打印值及地址,简写pquit #退出gdb ,简写q
break+num   #在第num行设置断点,简写b
info breakpoints  #查看当前设置的所有断点
delete breakpoints num   #删除第num个断点,简写d
display      #追踪查看具体变量值
undisplay    #取消追踪观察变量
watch    #被设置观察点的变量发生修改时,打印显示i 
watch    #显示观察点
enable breakpoints   #启用断点
disable breakpoints  #禁用断点
set fo11ow-fork-mode child #Makefile项目管理:选择跟踪父子进程(fork())

5、了解死锁么,它是咋么产生的以及如何解决?

死锁产生原因:

多个并发进程因争夺系统资源而产生相互等待的现象。

死锁的必要条件:

1、互斥:一个资源每次只能被一个进程(线程)使用,如果另一个进程(线程)请求该资源,那么它必须等待直到该资源被释放

2、占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其它进行释放该资源

3、不可抢占:资源不能被抢占,即只能由持有它的进程(线程)主动释放。

4、循环等待:存在一个进程(线程)的资源需求序列{P1, P2, ..., Pn},其中P1持有P2所需的资源,P2持有P3所需的资源,以此类推,Pn持有P1所需的资源,形成了一个循环等待的情况。

解决死锁:

1、预防死锁:

  • 破坏互斥条件:允许多个进程(线程)同时访问共享资源。
  • 破坏请求和保持条件:要求进程(线程)在获取所有资源之前不保持任何资源。
  • 破坏不可抢占条件:允许系统抢占进程(线程)持有的资源。
  • 破坏循环等待条件:对资源进行排序,强制所有进程按照相同的顺序请求资源。

2、避免死锁:

通过仔细地资源分配和进程调度,使系统永远不会进入死锁状态。常见的避免死锁的算法包括银行家算法等。

3、检测死锁:

周期性地检查系统中是否存在死锁,一旦检测到死锁,就采取措施解除死锁。常见的死锁检测算法包括资源分配图算法等。

4、解除死锁:

当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常用方法:

①剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,解除死锁状态

②撤销进程:可以直接撤销死锁进程或撤销代价最小的进程,直至有足够的资源可以使用,死锁状态消除为止


6、物理地址与虚拟地址,如何映射

物理地址:物理地址是内存条上真实存在的地址,是实际硬件上的地址,每个存储单元中都有一个唯一的物理地址,

虚拟地址:虚拟地址是由程序生成的地址。当程序访问内存时,它使用的是虚拟地址。

目的:通过使用虚拟地址,操作系统可以让每个程序都认为自己在使用一大块连续的内存空间,这块虚拟的内存空间由操作系统管理。虚拟内存技术允许多个程序并发运行,而不会相互干扰,提高了内存的使用效率和系统的安全性。

映射方式(从物理地址到虚拟地址的映射):

1、使用内存管理接口

操作系统通常提供了一系列的内存管理接口,允许内核代码请求页面映射,修改页表等。如,在Linux中,函数如vmalloc可以用于分配连续的虚拟地址空间,而底层的物理内存可能是非连续的。这些接口更多地被用于管理虚拟内存,关系到物理地址的管理和映射。

2、通过文件系统接口

某些操作系统提供了特殊的文件系统或文件系统接口,允许用户空间程序以文件的形式访问硬件设备或物理内存。例如,Linux的/dev/mem设备文件允许直接访问物理内存地址空间。通过打开并mmap这个设备文件,用户空间程序可以将特定的物理内存区域映射到其虚拟地址空间中。

int fd = open("/dev/mem", O_RDWR);
void *vaddr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, phys_addr);

3、Linux中映射物理地址到虚拟地址

Linux内核提供几种机制来将物理地址映射到内核的虚拟地址空间,最常用的是ioremap函数,ioremap用于映射物理内存地址到虚拟地址空间,通常用于设备驱动程序中以访问映射硬件的寄存器。

实例:假设有一个设备寄存器位于物理地址0x1FC00000,我们需要从Linux内核模块中访问这个寄存器

#include <linux/io.h>

void __iomem *my_reg;

// 映射物理地址0x1FC00000到虚拟地址空间
my_reg = ioremap(0x1FC00000, PAGE_SIZE);
if (!my_reg) {
    printk("Cannot map register address\n");
    return -ENOMEM;
}

// 使用ioread32(), iowrite32()等函数访问映射的寄存器
unsigned int val = ioread32(my_reg);

// 完成后解除映射
iounmap(my_reg);

ioremap函数将指定的物理地址范围映射到内核虚拟地址空间,返回一个指向这段虚拟地址的指针。这样,内核代码就可以通过这个虚拟地址来访问物理内存或设备寄存器。


7、TCP和UDP的区别以及TCP如何保证数据传输的稳定性

TCP与UDP的区别:

1、TCP是面向连接的,UDP是面向无连接的

2、TCP提供可靠的数据传输,UDP不提供可靠性保证

3、TCP能够使用滑动窗口机制来控制数据的发送速率,接收方通过窗口大小来告知发送方自己的接收能力,从而避免了发送过快导致接收方无法处理的情况,且还有拥塞控制机制,通过拥塞窗口和慢启动等算法来控制网络拥塞,而UDP没有流量控制和拥塞控制机制,可能会导致网络拥塞或丢包。

TCP的原理及数据传输稳定性

1、连接导向

TCP是面向连接的协议,通信双方在传输数据之前需要先建立连接。连接的建立包括三次握手的过程,确保通信双方都能够正常通信。

2、可靠性

TCP提供可靠的数据传输,通过序号、确认和重传机制确保数据的可靠性。每个数据包都有一个序号,接收方收到数据后会发送确认消息,发送方在一定时间内没有收到确认消息则会重传数据包。

3、流量控制

TCP使用滑动窗口机制来控制数据的发送速率,接收方通过窗口大小来告知发送方自己的接收能力,从而避免了发送过快导致接收方无法处理的情况。

4、拥塞控制

TCP通过拥塞窗口和慢启动等算法来控制网络拥塞。当网络出现拥塞时,TCP会减小发送窗口,降低发送速率,从而避免网络拥塞进一步恶化。


8、虚函数与纯虚函数了解么

虚函数:在基类中声明为虚拟的成员函数,允许在派生类中重新定义(override)该函数以实现多态性。通过使用虚函数,程序可以根据对象的实际类型调用适当的函数。

虚函数特点:

1、虚函数在基类中使用关键字 virtual 声明,并且在派生类中可以被重写(覆盖)。

2、虚函数在基类中有一个实现,但可以在派生类中重新实现(覆盖)。

3、如果派生类中未定义某个虚函数的实现,那么将使用基类中的实现。4、虚函数允许基类的指针或引用根据指向的实际对象类型调用相应的函数。

实例:

class Base {
public:
    virtual void foo() {
        cout << "Base::foo() called" << endl;
    }
};

class Derived : public Base {
public:
    void foo() override {
        cout << "Derived::foo() called" << endl;
    }
};

纯虚函数:在基类中声明为虚拟的成员函数,但没有在基类中提供实现。它只是一个函数原型,留给派生类去实现。纯虚函数使得基类成为抽象类,不能被实例化,但可以作为接口定义使用。

纯虚函数特点:

1、纯虚函数在基类中使用 virtual 关键字声明,并且用 =0表示没有实现。2、派生类必须实现纯虚函数,否则派生类也会变成抽象类。

3、抽象类(包含至少一个纯虚函数的类)不能被实例化,只能作为接口使用。

实例:

class AbstractBase {
public:
    virtual void pureVirtualFunction() = 0; // 纯虚函数声明
};

class ConcreteDerived : public AbstractBase {
public:
    void pureVirtualFunction() override {
        cout << "ConcreteDerived::pureVirtualFunction() called" << endl;
    }
};

在纯虚函数中,= 0是用来告诉编译器,该函数没有实现,需要在派生类中实现。因此,派生类必须提供实现,否则它也会变成抽象类。

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐