# 3.1 为什么栈空间很小?

C的栈在 x86-64 上是直接绑定到CPU指令的,实现上极其精简,因此它与堆不同,没有动态增长、动态缩小的功能,一旦分配出来就会永远占用相应的空间。每个线程都会占用独立的栈空间,这样对于线程数很多的进程来说,如果栈空间分配得过多,就会很浪费内存空间。相反,堆空间可以一开始分配得很小,然后不停向上增长,释放相应的空间之后还可以归还给操作系统,因此适合处理比较大的空间。对于一定要占用比较大的栈空间的情况,可以指定栈空间大小。其他语言的栈并不一定是这样的,比如Python的栈就是假的,用堆空间模拟的,只有递归深度限制,而没有容量限制。

# 3.2 什么时候使用 short?

通常 int 设置为自然大小,是硬件处理效率最高的整数形式。

short 在参与运算时会被转成 int,会影响处理short整型运算速度。

C/C++中,编译器对int类型数据的执行效率最高。一般在符合int条件的情况下优先选择int

# 3.3 unsigned 陷阱

有符号数和无符号数相加时,有符号数的类型被提升到无符号数,最高位符号位变成数据位。

# 3.4 形参设为 const 指针有什么好处?

将形参设置为const将把形参限定为常量,使得我们不能修改它。

这样做的好处有2点:

  • 第一,保证了实参不能被修改,增加了安全性。
  • 第二,扩大了该函数的参数的接收范围,使得函数更具通用性

# 3.5 scanf里的空白符

空白字符能匹配输入中任意数量的空白符,所以scanf("\n")里的 \n 能匹配任意数量的空白符。scanf会一直等待输入,直到输入非空白符为止。

# 3.6 floating point exception

通常是逻辑错误,如对 0 取余。

int32_t common_multiple(int32_t x, int32_t y)
{
    int32_t a = x;
    int32_t b = y;
    int32_t r = a % b;

    while (r) {
        a = b;
        b = a % b;
    }
    kdebug("最小公因数:%d\n",b);
    return (x * y) / b;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.7 C语言编写的程序在无操作系统的情况下运行时如何支持其堆栈空间?

栈是一种CPU硬件支持的数据结构;通过CPU内部的栈寄存器SP就可以访问栈顶元素;使用push指令把数据压入栈,SP自动增加一个元素的偏移量。(对x86就是sp-2,因为x86的栈是反向增长);使用pop从栈中弹出一个数据,SP自动减少一个元素的偏移量。

# 3.8 volatile 一般用处

  1. 存储器映射的硬件寄存器通常要加 volatile,因为每次读写可能有不同意义。
  2. 中断服务程序中修改的供其他程序检测的变量,因为在中断程序中被修改,main 函数中并未修改 所以可能只从内存中读取一次放到寄存器中,后续只会从寄存器中读取变量副本,使得中断操作对变量的修改无用。
  3. 多任务环境下各任务间共享的标志,如在某个线程里,读取某个变量,编译器优化时有时会先把变量 读取到寄存器中,如果当前线程没有修改变量值就一直从寄存器中读取,而如果该变量在别的线程改变了值后,寄存器里的值却并不会改变,从而造成读取变量值错误。

# 3.9 为什么全局区要分为 bss 段 和 data 段?

bss 存储未初始化的全局变量,data 存储初始化的全局变量。

因为对于初始化的变量,所有的数据必须保存到目标文件中,在 os 加载程序的时候,复制到对应 内存。

而未初始化的变量,编译器就不会在目标文件保存值,只需要记录一个字节数,告诉 os 多少个字节要在加载到内存的时候初始化为 0。

bss 段主要为了节省程序的目标文件或者可执行文件所占的磁盘空间。

注意:全局变量 int arr[1024] = {0}; 等价于 int arr[1024]

# 3.10 有了互斥锁为什么还要条件变量?

互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送 信号的方法弥补了互斥锁的不足,它常和互斥锁一起配合使用。使用时,条件变量被用来阻塞一个线程,当条件 不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相 应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满 足。一般说来,条件变量被用来进行线程间的同步。

两个线程操作同一临界区时,通过互斥锁保护,若A线程已经加锁,B线程再加锁时候会被阻塞,直到A释放锁,B再获得锁运行,进程B必须不停的主动获得锁、检查条件、释放锁、再获得锁、再检查、再释放,一直到满足运行的条件的时候才可以(而此过程中其他线程一直在等待该线程的结束),这种方式是比较消耗系统的资源的。而条件变量同样是阻塞,还需要通知才能唤醒,线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,该线程就休眠了,应该仍阻塞在这里,等待条件满足后被唤醒,节省了线程不断运行浪费的资源。这个过程一般用while语句实现。当线程B发现被锁定的变量不满足条件时会自动的释放锁并把自身置于等待状态,让出CPU的控制权给其它线程。其它线程 此时就有机会去进行操作,当修改完成后再通知那些由于条件不满足而陷入等待状态的线程。这是一种通知模型的同步方式,大大的节省了CPU的计算资源,减少了线程之间的竞争,而且提高了线程之间的系统工作的效率。这种同步方式就是条件变量。

总而言之,为了避免因条件判断语句与其后的正文或wait语句之间的间隙而产生的漏判或误判,用一个mutex来保证。对于某个cond的判断,修改等操作某一时刻只有一个线程在访问。条件变量本身就是一个竞争资源,这个资源的作用是对其后程序正文的执行权,于是用一个锁来保护。

使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。

# 3.11 内存中的栈是怎么增长的?

在计算机的内存中,栈(Stack)是一种数据结构,用于存储函数调用和局部变量等临时数据。栈的增长方向通常是由高地址向低地址,也被称为向下生长。

当程序执行函数调用时,会将函数的参数、返回地址和局部变量等数据压入栈中。栈指针(Stack Pointer)指示了当前栈顶的位置。栈指针的初始位置通常是在栈的最高地址。

当执行函数调用时,栈指针会减小(向低地址移动),将数据压入栈中。这个过程称为入栈(Push)。每次入栈操作都会将栈指针向下移动一定的字节量,以便为新的数据腾出空间。

相反,当函数执行完毕返回时,会从栈中弹出数据,栈指针会增加(向高地址移动),将数据从栈中移出。这个过程称为出栈(Pop)。每次出栈操作都会将栈指针向上移动一定的字节量,以释放之前压入栈的数据空间。

栈的增长是按照先进后出(Last-In-First-Out,LIFO)的原则进行的。最后入栈的数据首先被出栈,即最新的函数调用先被执行完毕。

需要注意的是,栈的大小是有限的,计算机系统会为栈分配一定的内存空间。当栈的大小超过了分配的空间,或者发生栈溢出(Stack Overflow)时,会导致程序崩溃或异常。

总结起来,栈在内存中是一种向下生长的数据结构,通过栈指针的移动来实现数据的入栈和出栈操作。这种机制使得函数调用和局部变量的管理变得简单而高效。

# 3.12 栈帧内的地址增长方向是什么样的?

在计算机的内存中,每个函数调用都会创建一个栈帧(Stack Frame),也称为活动记录(Activation Record)或函数帧(Function Frame),用于存储函数的局部变量、参数和其他相关信息。栈帧的内部地址增长方向是由高地址向低地址,也被称为向下生长。

栈帧内部的地址增长方向是由编译器或计算机体系结构决定的,具体实现可能有所不同。然而,通常情况下,栈帧内的地址增长方向是从高地址向低地址。

当一个函数被调用时,会为该函数创建一个新的栈帧。栈帧通常由以下几个部分组成:

  1. 返回地址(Return Address):指向函数调用后将要执行的下一条指令的地址。
  2. 参数(Arguments):传递给函数的参数。
  3. 局部变量(Local Variables):在函数内部声明的局部变量。
  4. 其他相关信息:如函数调用的上下文和寄存器的保存等。

这些数据在栈帧内部被依次存储,根据栈帧内地址的增长方向,它们会从高地址到低地址的顺序被存放。

栈帧的大小是由函数的参数和局部变量的数量以及它们的大小决定的。每当一个新的函数调用发生时,栈指针会减小(向低地址移动),为新的栈帧腾出空间。

需要注意的是,栈帧的增长方向是相对于栈顶指针(Stack Pointer)而言的,而不是相对于内存的整体地址。整个栈的增长方向通常是由高地址向低地址,但栈帧内部的相对地址是按照从高地址到低地址的方式进行的。

总结起来,栈帧内的地址增长方向是由高地址向低地址,根据栈帧内数据的存储顺序,数据会依次从高地址到低地址被存放。这种机制使得函数的局部变量和参数可以方便地被存取和管理。

# 3.13 UL 后缀有什么用?

数字常量会被隐形定义为 int 类型,两个 int 类型相加的结果可能会发生溢出。

UL 会强制转化为 unsigned long 类型。

# 3.14 为什么在使用条件变量进行线程同步时,阻塞线程之前要先上锁?

如下代码:

    mtx_lock(&mutex);  // 上锁
    cnd_wait(&condition, &mutex);  // 等待条件变量信号
    sharedVariable--;
    printf("线程2:共享变量的值:%d\n", sharedVariable);
    mtx_unlock(&mutex);  // 解锁
1
2
3
4
5

为了确保线程在等待条件变量时的原子性和正确性。

cnd_wait() 的两步操作必须保证是原子性的,必须把把调用线程放到条件等待队列上和释放 mutex 两个步骤捆绑到一起。 不然上面的两个步骤中间就可能插入其它操作。比如,如果先释放mutex,这时候生产者线程向队列中添加 数据,然后signal,之后消费者线程才去『把调用线程放到等待队列上』,signal信号就这样被丢失了,所以必须对 cnd_wait() 操作上锁。

  1. 原子性:条件变量的等待操作是一个原子操作。这意味着当线程调用cnd_wait函数时,它会原子性地释放互斥锁并阻塞在条件变量上,以等待其他线程发送信号。如果不先上锁,其他线程可能在此线程阻塞之前就发送了信号,而阻塞的线程会错过这个信号,导致逻辑错误。
  2. 正确性:通过先上锁,可以确保等待条件变量的线程能够正确处理条件的更改。在线程上锁后,它可以安全地检查条件是否满足。如果线程在上锁之前等待条件变量,可能会遇到竞态条件,即其他线程在此线程检查条件之前改变了条件的状态,导致线程处理过时或不正确的条件。

总结来说,先上锁再阻塞线程是为了保证条件变量等待的原子性和正确性。通过先上锁,确保线程在等待之前能够正确检查条件,并能够在阻塞期间正确处理其他线程发送的信号。这样可以避免竞态条件和线程间的数据不一致性问题。

#include <stdlib.h>
#include <stdbool.h>
#include <inttypes.h>
#include <threads.h>
#include <stdio.h>

struct data{
    volatile int32_t num; //< 多个线程之间共享的变量。
    mtx_t mtx;
    cnd_t cnd;
};

extern int func1(void *);
extern int func2(void *);

int main(int argc, const char* argv[])
{
    // * 初始化互斥锁为普通互斥锁。
    mtx_t mtx;          //< 互斥对象
    mtx_init(&mtx, mtx_plain);

    // * 初始化条件变量
    cnd_t cnd;          //< 条件变量
    cnd_init(&cnd);

    // * 创建两个线程。
    struct data data = {
        .num = 0,
        .mtx = mtx,
        .cnd = cnd
    };

    thrd_t tid1, tid2;  //< 线程1, 2
    thrd_create(&tid1, func1, (void *)&data);
    thrd_create(&tid2, func2, (void *)&data);

    // * 等待线程结束
    thrd_join(tid1, NULL);
    thrd_join(tid2, NULL);

    // 销毁互斥量
    mtx_destroy(&mtx);
    // 销毁条件变量
    cnd_destroy(&cnd);

    return EXIT_SUCCESS;
}

int func1(void *arg)
{
    struct data *dat = (struct data *)arg;

    printf("I'm %d\n", 1);
    // 阻塞直到解锁。
    mtx_lock(&dat->mtx);

    // 等待 dat->num 的值为 100
    // 阻塞线程,自动解锁 mtx,等待信号。
    // 线程变为非阻塞时,锁住线程。
    cnd_wait(&dat->cnd, &dat->mtx);
    dat->num = 0;

    printf("%d\n", dat->num);
    mtx_unlock(&dat->mtx);
    printf("quit thread1\n");

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# 3.15 线程局部变量有什么用?

TODO: TLS(Thread Local Stroge)

让每个线程都有自己的全局变量,如 errno 的实现。

# 3.16 互斥锁的作用是什么?

对非原子性操作不加同步手段保护公共资源,会导致数据混乱,无法得到准确的数据。

互斥锁(Mutex)是一种同步机制,用于保护共享资源在多线程环境下的互斥访问。它的主要作用是确保在任意给定的时间点只有一个线程可以访问被保护的共享资源,从而避免数据竞争和不一致的结果。

互斥锁通过提供两个基本操作来实现互斥访问:

上锁(Locking):一个线程可以尝试获得互斥锁的所有权,如果锁当前未被其他线程持有,则该线程成功获取锁并继续执行。如果锁已被其他线程持有,则请求锁的线程将被阻塞,直到锁被释放为止。

解锁(Unlocking):持有锁的线程可以释放锁,使其他线程有机会获得锁并访问共享资源。

互斥锁的作用主要体现在以下方面:

保护共享资源:在多线程环境中,多个线程可能同时访问共享资源,如果没有互斥锁进行保护,可能会导致数据竞争和不确定的结果。互斥锁确保在任意给定的时间点只有一个线程可以访问共享资源,从而避免竞态条件。

实现临界区(Critical Section):互斥锁可以将代码块或函数标记为临界区,只允许一个线程在任意时间点执行临界区的代码。这对于需要保证原子性操作或特定顺序执行的代码段非常重要。

同步线程:互斥锁可以用于实现线程之间的同步,使得线程能够按照特定的顺序执行或等待其他线程完成特定操作。

需要注意的是,互斥锁的过度使用可能会导致性能问题,因为它会引入线程之间的竞争和阻塞。因此,在设计并发程序时,需要仔细评估共享资源的访问模式,避免不必要的互斥锁使用,并考虑其他同步机制,如读写锁、条件变量等,以提高程序的性能和可伸缩性。

# 3.17 malloc 和 calloc 的区别是什么?

malloc与calloc在内存分配时,前者分配一整块,后者分配n块,并且后者在分配时会将内存置为0, 前者内存里是垃圾数据。

void *calloc(size_t num, size_t size);
void *malloc(size_t num);
1
2

# C 程序中,栈的大小是多少?

在C语言程序中,栈的大小是由系统和编译器决定的。栈是用来存储局部变量、函数调用信息以及其他程序运行时需要的数据的一种内存区域。栈的大小在不同的操作系统和编译器中可能会有所不同。

一般来说,栈的大小通常是有限的,因为操作系统为每个进程分配了一定的虚拟地址空间用于栈。这个限制可以在不同的操作系统和编译器中有所不同。例如,在Windows操作系统中,默认情况下,32位应用程序的栈大小为1MB,而64位应用程序的栈大小为2MB。

在编写C程序时,要注意避免使用过多的递归调用或者过多的局部变量,以防止栈溢出问题。如果需要更大的栈空间,可以通过调整编译器选项或操作系统参数来进行设置,但这可能需要谨慎处理,以免影响系统的稳定性和性能。

总之,栈的大小是一个复杂的主题,受到多个因素的影响,包括操作系统、编译器、硬件架构等。

# C 程序最大能占用多大内存?

操作系统和硬件限制: 操作系统和硬件平台会限制单个进程能够使用的最大内存。例如,32位操作系统通常限制单个进程的地址空间为4GB,而64位操作系统可以支持更大的地址空间。不过,实际可用的内存可能会受到操作系统本身和其他正在运行的进程的影响。

物理内存和虚拟内存: 现代操作系统使用虚拟内存管理,这意味着每个进程都有自己的虚拟地址空间。物理内存(RAM)和虚拟内存的关系由操作系统和硬件决定。当物理内存不足时,操作系统可以将部分数据移到磁盘上的交换文件中,这称为页面交换。因此,虽然程序可能具有大量的虚拟内存空间,但实际可用的物理内存可能会有限。

编译器优化和代码结构: 编译器可以对代码进行优化,以减少内存使用。合理的代码结构和内存管理实践可以最大程度地减少内存占用。同时,避免内存泄漏和不必要的内存分配也是重要的。

堆和栈的使用: 堆和栈是程序运行时使用的两个主要内存区域。堆用于动态内存分配(例如使用malloc和free函数),栈用于存储局部变量和函数调用信息。栈通常较小,而堆的大小可以受到系统和编译器的限制。

# 怎么比较两个浮点数的大小?

浮点数在内存中的储存方式与整型相差甚远,浮点数在内存中的储存,并不是我们所想的是完整存储的, 在十进制转化为二进制,是可能有精度的损失的。注意这里的损失,不是一味的减少了,还有可能增多。

可以使用数学库里的 isgreater() 函数比较两个浮点数的大小,也可以使用 float.h 文件里的 FLT_EPSILON/DBL_EPSILON 作为精度进行比较。

# 怎么在 main 函数后再执行代码?

使用 _onexit() 注册回调函数。

这个函数可以实现在main主函数执行完毕之后,才执行的代码。

(1)使用格式:_onexit(int fun()) ,其中函数fun()必须是带有int类型返回值的无参数函数;

(2)_onexit() 包含在头文件 stdlib.h 中。

(3)无论函数_onexit() 放到main中任意位置,它都是最后执行。

#include <iostream>
#include <cstdlib>
using namespace std;
int func1(),func2(),func3();

int main(){

   _onexit(func2);
   _onexit(func1);
   _onexit(func3);
    cout<<"First Line"<<endl;
    cout<<"Second Line"<<endl;
}

int func1()
{
    cout<<"fun1()  executed!"<<endl;
    return 0;
}

int func2()
{
    cout<<"fun2()  is"<<endl;
    return 0;
}
int func3()
{
    cout<<"fun3()  This "<<endl;
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 怎么确定程序是 C 语言编写还是 C++ 编写?

#include <cstdio>
#include <cstdlib>

int func(void) {
#ifdef __cplusplus
    printf("cpp\n");
#else
    printf("c\n");
#endif
    return 0;
}

int main(int argc, const char* argv[])
{
    _onexit(func);

    printf("main end.\n");
    return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 怎么根据给定类型的变量,得出类型转换符?

float h(); -> (float (*)())
int arr[3][4] -> (int (*)[4])
void (*signal(int, void(*)(int)))(int)
1
2
3

# extern 指针和数组不同

extern char *hello;
extern char hell[];
1
2

# 如何判断两数相加会溢出?

if (a > (INT32_MAX - b)) {
    // 溢出
}
1
2
3

# strcpy 和 memcpy 有什么区别?

strcpymemcpy 是两个在 C 和 C++ 编程中用于复制数据的函数,但它们有一些关键区别:

  1. 用途:

    • strcpy 主要用于复制字符串,它以 null 终止字符 ('\0') 作为结束标志。因此,它通常用于复制 C 风格的字符串(以 null 结尾的字符数组)。
    • memcpy 用于在内存块之间复制一定数量的字节,它不关心数据是否是字符串,也不关心 null 终止字符。
  2. 参数类型:

    • strcpy 的参数是字符指针(通常用于字符串):char* strcpy(char* destination, const char* source)
    • memcpy 的参数是指针和字节数:void* memcpy(void* destination, const void* source, size_t num)
  3. 安全性:

    • strcpy 不会检查源字符串的长度,因此容易导致缓冲区溢出,可能引发安全漏洞,尤其是当源字符串比目标缓冲区更长时。
    • memcpy 需要明确指定要复制的字节数,因此更加灵活和安全,不容易导致缓冲区溢出,但需要确保目标缓冲区足够大以容纳要复制的数据。
  4. 性能:

    • strcpy 需要遍历源字符串直到遇到 null 终止字符,这可能会导致性能开销,特别是对于长字符串。
    • memcpy 只是简单地按字节数复制数据,通常比 strcpy 更快,特别是在大数据块的情况下。

在使用这两个函数时,需要根据具体的需求和数据类型选择合适的函数。如果处理的是字符串,且要确保 null 结尾,那么 strcpy 可能更方便。如果要处理二进制数据或不关心 null 结尾,那么 memcpy 更适合。在任何情况下,都要谨慎处理目标缓冲区的大小,以避免缓冲区溢出问题。

# 线程的分离和非分离有什么区别?

在线程编程中,线程的分离(detached)和非分离(joinable)是两种不同的线程状态,它们有以下关键区别:

  1. 生命周期管理:

    • 非分离线程(joinable):非分离线程是默认状态,也就是说,如果你创建一个线程,它会处于非分离状态。这意味着主线程可以等待(join)这个线程的结束,以获取线程的返回值或确保线程执行完毕。非分离线程的资源(如线程控制块)在线程退出后不会被立即释放,需要等待主线程显式调用 pthread_join(在 POSIX 线程中)或类似的函数来回收资源。

    • 分离线程(detached):分离线程是一种状态,它表示主线程不会等待这个线程的结束,也不能再次 join。一旦线程被设置为分离状态,它的资源会在线程退出后立即被释放,而无需主线程显式调用 pthread_join 或类似函数。分离线程通常用于一些独立的、不需要与主线程同步的任务,或者是不需要获取线程返回值的情况。

  2. 线程的属性:

    • 在 POSIX 线程(pthread)中,可以使用 pthread_attr_setdetachstate 函数来设置线程的分离属性,将线程设置为分离线程。
  3. 使用场景:

    • 非分离线程 适用于需要等待线程执行完毕并获取其返回值的情况。例如,当你启动多个线程来执行一些计算任务,并希望在所有线程完成后收集结果时,非分离线程是合适的。

    • 分离线程 适用于一些独立的、不需要主线程等待的任务,特别是在主线程不关心线程的结果,或者线程的生命周期与应用程序的生命周期无关时。分离线程可以提供更好的性能,因为不需要等待线程结束,从而避免了主线程的阻塞。

在使用线程时,需要根据具体需求和场景选择线程的分离性质。确保正确管理线程的状态和资源是编写多线程应用程序的重要部分,以避免潜在的问题和资源泄漏。

# 互斥锁的作用是什么?

互斥锁和原子操作

互斥锁(Mutex)确保了被锁住的代码块在多线程环境下的互斥执行,也就是说,同一时刻只有一个线程可以进入被锁住的代码块。这可以防止多个线程同时访问共享资源,从而避免了竞态条件(Race Condition)。

虽然互斥锁确保了代码块的互斥执行,但它本身并不提供原子操作的语义。原子操作是指不可分割的操作,即使在多线程环境下,也不会被其他线程中断。互斥锁只是保护了进入临界区(被锁住的代码块)的线程互斥执行,但临界区内部的操作仍然可以是多个步骤。

如果需要执行原子操作,通常需要使用特定的原子操作函数或CPU提供的原子指令。这些函数或指令确保在执行期间不会被其他线程中断。在C语言中,您可以使用标准C11中提供的原子操作库 <stdatomic.h> 来执行原子操作。这个库提供了一系列的原子操作函数,比如 atomic_store, atomic_load, atomic_fetch_add, atomic_fetch_sub 等。

要注意的是,互斥锁是一种相对较重的同步机制,因为它涉及到内核态和用户态的切换,所以在性能要求高的情况下,可能需要谨慎选择是否使用互斥锁,或者是否可以使用更轻量级的同步机制,如自旋锁或原子操作。原子操作通常比互斥锁更高效,因为它们避免了内核态和用户态的切换。

互斥锁的作用

互斥锁(Mutex,全名为 Mutual Exclusion)是一种用于多线程编程中的同步机制,其主要作用是确保在多个线程之间对共享资源的互斥访问,以防止数据竞争和不确定性行为。互斥锁的主要作用包括:

  1. 保护共享资源: 互斥锁用于保护共享资源,例如共享内存区域、全局变量、文件等,以确保在任何给定时刻只有一个线程可以访问这些资源。这可以防止多个线程同时修改共享资源,从而避免了数据的不一致性和竞态条件。

  2. 确保数据一致性: 通过互斥锁,您可以确保在多线程环境下对共享数据的读写操作是原子的,即不会被其他线程中断。这有助于确保数据的一致性,避免数据损坏或不正确的计算结果。

  3. 实现线程安全: 互斥锁是实现线程安全的重要工具之一。线程安全意味着多个线程可以同时访问某个代码块或数据结构,而不会导致不正确的结果或崩溃。互斥锁可以用于使关键部分的代码成为临界区,确保在任何时候只有一个线程可以进入这个临界区。

  4. 避免死锁: 互斥锁的良好使用可以帮助避免死锁情况,因为它们确保了线程在访问资源时按照一定的顺序获取锁,从而降低了发生死锁的可能性。

  5. 协调线程: 互斥锁还可以用于协调线程之间的操作,例如等待某个条件满足后再执行特定操作,这通常涉及到条件变量的使用,结合互斥锁来等待和通知线程。

需要注意的是,虽然互斥锁是一种重要的同步机制,但过度使用它们可能会导致性能下降,因为互斥锁涉及到线程的阻塞和唤醒操作,这会引入一些开销。因此,在设计多线程程序时,需要仔细考虑如何使用互斥锁,以确保同时满足线程安全性和性能需求。

# static 的生命周期是?

生命周期是整个程序的运行周期,存放于静态/全局区。

# C 中 空字符串怎么表示?

空字符串就是只一个 0 的字符串,也就是 0。

# 数组名 + 1 和 数组名取地址 + 1 的区别?

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

int main(int argc, const char* argv[])
{
    int arr[5] = {
        1,2,3,4,5
    };

    int *ptr = (int *) (&arr + 1);

    // 2, 5
    printf("%d,%d\n", *(arr + 1), *(ptr-1));
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 0 和 NULL 和 \0 的区别?

在C语言中,NULL和0、‘\0’的值都是一样的,但是它们的实质并不相同,NULL用于指针和对象,0是指数值,‘\0’用于字符串的结束。 在不同的系统中,NULL并非总是和0等同,NULL仅仅代表空值,也就是指向一个不被使用的地址,在大多数 系统中,都将0作为不被使用的地址,所以就有了类似这样的定义:#define NULL 0

但并非总是如此,也有些系统不将0地址作为NULL,而是用其他的地址,所以说,千万别将NULL和0等价起来, 特别是在一些跨平台的代码中,这更是将给你带来灾难。

# 只声明不定义函数可以吗?

可以,函数只有声明没有定义,不调用它,就不会链接出错。

# 怎么对字符串进行换行?

  • 利用双引号对长字符串进行换行。编译器在编译处理时会自动的拼接这些子字符串。
  • 利用反斜杠对长字符串进行换行。但是反斜杠拆分的字符串会把下一行的空格也计算在内。