常见嵌入式C问题

进程与线程的区别

进程,是对操作系统正在运行程序的一个抽象。操作系统会把每个运行中的程序封装成独立的实体,分配各自所需要的资源,在根据调度算法切换执行,这个抽象的实体就是进程。因此进程是操作系统机型资源分配和调度的一个基本单位。

进程是任务调度的最小单位,每个进程有自己的独立代码和数据空间,使得各个进程之间内存地址相互隔离。

随着应用程序功能设计的越来越复杂,应用程序中的某种活动可能被阻塞,自然而然的想着能不能把这些应用程序分解成更细的粒度,能“并行”的执行多个执行实体,并且这些细粒度能够可以共享程序的地址空间,也可以共享程序代码、数据及内存空间,线程就被引入了。
另外一个引入线程的原因,每个进程都有独立的代码和数据空间(程序上下文),程序之间切换会开销很大,而线程之间共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间的切换开销小。

引入线程模型后,进程负责分配和管理系统资源,线程负责调度运算,也是CPU切换时间片的最小单位,创建进程后会有默认一个主线程的。

区别:

  • 功能:进程是OS资源(CPU、内存)分配的基本单位,而线程是任务调度和执行的基本单位。

  • 开销:每个进程都有独立的内存空间,存放代码和数据段等,进程切换回有较大开销;线程可以看做轻量级进程,共享内存空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。

  • 运行:OS中能运行多个进程,同一进程中有多个线程同时执行(CPU调度,每个时间片中只有一个线程执行)。

  • 创建:创建进程,系统调用的fork,会将父进程的task_struct(五大结构file_struct/fs_struct/sighand_struct/signal_struct/mm_struct)都复制一份并初始化,形成自己的内存空间数据;进程创建,调用clone,五大结构仅仅是引用计数器加一,也就共享进程的数据结构。

  • 通讯:进程需要跨进程边界,适合小数据量传送,线程间适合大数据量传送。进程间:管道、信号、共享内存、消息队列、限号量、socket,线程间:信号量、读写锁、条件变量、互斥锁。

  • 上下文切换:有进程上下文、中断上下文,需要保存当前任务的运行环境,恢复将要运行任务的运行环境(程序指令运行的位置,寄存器、堆栈)。

交叉编译

交叉编译:在当前编译平台下,编译出来的程序能运行在体系结构不同的另一种目标平台上,但是编译平台本身却不能运行该程序。如在 x64 平台上,编写程序并编译成能运行在ARM平台的程序,编译得到的程序在x64平台上是不能运行的,必须放到ARM平台上才能运行。

交叉编译是相对复杂的,必须考虑如下几个问题:

  • CPU架构:比如ARM,x86,MIPS等等;
  • 字节序:大端(big-endian)和小端(little-endian);
  • 浮点数的支持;
  • 应用程序二进制接口(Application Binary Interface,ABI);

为什么要使用交叉编译:

  • 交叉编译的目标系统一般都是内存较小、显示设备简陋甚至没有,没有能力在其上进行本地编译;
  • 有能力进行源代码编译的平台CPU架构或操作系统与目标平台不同;

RAM中运行和ROM中运行

RAM(random access memory):运行内存,CPU可以直接访问,读写速度非常快,但是不能掉电存储。
ROM(read only memory):存储性内存,可以掉电存储,如Flash(机械磁盘也可以简单的理解为ROM)。
正常低速的MCU处理直接从norflash中运行,高速系统需要将程序从ROM中复制到RAM中执行。

Typedef

Typedef在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如,思考一下下面的例子:

#define dPS struct s *
typedef struct s * tPS;

以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构s指针。哪种方法更好呢?(如果有的话)为什么?
这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是:typedef更好。

dPS p1,p2;
tPS p3,p4;
//第一个扩展为
struct s * p1, p2;

上面的代码定义p1为一个指向结构的指,p2为一个实际的结构,这也许不是你想要的。第二个例子正确地定义了p3 和p4 两个指针。

  1. #define之后不带分号,typedef之后带分号。typedef 定义是语句,因为句尾要加上分号。而define 不是语句,千万不能在句尾加分号。
  2. #define可以使用其他类型说明符对宏类型名进行扩展,而 typedef 不能这样做。
    #define INT1 int
    unsigned INT1 n;  //没问题
    typedef int INT2;
    unsigned INT2 n;  //有问题
    // INT1可以使用类型说明符unsigned进行扩展,而INT2不能使用unsigned进行扩展。
    
  3. 在连续定义几个变量的时候,typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证。
    #define PINT1 int*;
    P_INT1 p1,p2;  //即int *p1,p2;
    typedet int* PINT2;
    P_INT2 p1,p2;  //p1、p2 类型相同
    
    // PINT1定义的p1与p2类型不同,即p1为指向整形的指针变量,p2为整形变量;PINT2定义的p1与p2类型相同,即都是指向 int 类型的指针。
    

宏定义

// 写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个
#define MIN(A,B) ((A) <= (B) ? (A) : (B))

#define MAX(x,y) ((x) > (y) ? (x) : (y))

// 交换两个参数值的宏定义
#define SWAP(a,b)\
                (a)=(a)+(b);\
                (b)=(a)-(b);\
                (a)=(a)-(b);

// 已知一个数组table,用一个宏定义,求出数据的元素个数
#define NTBL (sizeof(table)/sizeof(table[0]))
  1. 标识#define在宏中应用的基本知识。这是很重要的。因为在 嵌入(inline)操作符 变为标准C的一部分之前,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。
  2. 懂得在宏中小心地把参数用括号括起来
  3. 我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事? c least = MIN(*p++, b);

用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)

#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL

注意几件事情:

  1. #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)
  2. 懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
  3. 意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。
  4. 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。
#include <stdio.h>
#define debugPrintf(...) printf("DEBUG: " __VA_ARGS__);
int main(int argc, char** argv)
{
 debugPrintf("Hello World!\n");
 return 0;
}

/* ...表示所有剩下的参数,__VA_ARGS__被宏定义中的...参数所替换。
   这在c语言的GNU扩展语法里是一个特殊规则:当__VA_ARGS__为空时,会消除前面这个逗号。 */

define 与 typedef

  • 原理:#define 作为预处理指令,在编译预处理时进行替换操作,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef 是关键字,在编译时处理,有类型检查功能,用来给一个已经存在的类型一个别名,但不能在一个函数定义里面使用 typedef 。
  • 功能:typedef 用来定义类型的别名,方便使用。#define 不仅可以为类型取别名,还可以定义常量、变量、编译开关等。
  • 作用域:#define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。
  • 指针的操作:typedef 和 #define 在处理指针时不完全一样。
    const INTPTR1 p5 = &var; // 相当于 const int * p5; 常量指针,即不可以通过 p5 去修改 p5 指向的内容,但是 p5 可以指向其他内容。
    const INTPTR2 p6 = &var; // 相当于 int * const p6; 指针常量,不可使 p6 再指向其他内容。
    

const与#define

Const作用:定义常量、修饰函数参数、修饰函数返回值三个作用。被Const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。

  1. const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
  2. 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。

编译预处理

预编译又称为预处理,是做些代码文本的替换工作。处理#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等,就是为编译做的预备工作的阶段,主要处理#开始的预编译指令,预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置,通常用于指示编译哪些代码,简单的判断编译条件。

c提供的预处理功能主要有以下三种:1)宏定义 2)文件包含 3)条件编译

  1. 总是使用不经常改动的大型代码体。
  2. 程序由多个模块组成,所有模块都使用一组标准的包含文件和相同的编译选项。在这种情况下,可以将所有包含文件预编译为一个预编译头。

#error 指令让预处理器发出一条错误信息,并且会中断编译过程。
#warning 生成一个编译错误事件并停止编译/发出警告信息,message 可以不需要双引号。
#undef 用于把前面的宏定义名取消。
#ifndef/define/endif 用在头文件中的,防止该头文件被重复引用。

用C编写死循环

// 这个问题用几个解决方案。我首选的方案是:
while(1)
{
}

for(;;)
{
}

/* 这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们这样做的基本原理。如果他们的基本答案是:"我被教着这样做,但从没有想到过为什么。"这会给我留下一个坏印象。*/

// 第三个方案是用 goto
Loop:
...
goto Loop;
//应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的BASIC/FORTRAN程序员。

数据声明(Data declarations)

用变量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

关键字static

这个简单的问题很少有人能回答完全。在C语言中,关键字static有限定作用域的作用,三个种情况:

  1. 在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
  2. 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
  3. 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。

大多数能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。

关键字const

Dan Saks已经在他的文章里完全概括了const的所有 用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.如果你从没有读到那篇文章,只要能说出const意味着"只读"就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。
下面的声明都是什么意思?

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

一个定义为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;
    }
    

位操作(Bit manipulation)

嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。

  1. 用bit fields。Bit fields是被扔到C语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可重用的。最近不幸看到 Infineon为其较复杂的通信芯片写的驱动程序,它用到了bit fields因此完全对我无用,因为我的编译器用其它的方式来实现bit fields的。从道德讲:永远不要让一个非嵌入式的家伙粘实际硬件的边。
  2. 用 #defines 和 bit masks 操作。这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:
#define BIT3 (0x1 << 3)
static int a;

void set_bit3(void)
{
    a |= BIT3;
}
void clear_bit3(void)
{
    a &= ~BIT3;
}

一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数,这也是可以接受的。我希望看到几个要点:说明常数、|=和&=~操作。

给一个32bit数据的位置1,怎么用宏来实现?

#define SET_BIT(x, bit) (x |= (1 << bit)) /* 置位第bit位 */

访问固定的内存位置(Accessing fixed memory locations)

嵌入式系统经常具有要求去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。
这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换(typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同而不同。典型的类似代码如下:

int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;

// A more obscure approach is:
// 一个较晦涩的方法是:

*(int * const)(0x67a9) = 0xaa55;

即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。
奇数地址会不会死机还是一个需要特别注意的问题。

中断(Interrupts)

中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字__interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。

__interrupt double compute_area (double radius)
{
    double area = PI * radius * radius;
    printf("\nArea = %f", area);
    return area;
}
  1. ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。
  2. ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
  3. 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
  4. 与第三点一脉相承,printf()经常有重入和性能上的问题。

代码例子(Code examples)

下面的代码输出是什么,为什么?

void foo(void)
{
    unsigned int a = 6;
    int b = -20;
    (a+b > 6) ? puts("> 6") : puts("<= 6");
}

这个问题涉及C语言中的类型转换问题,这无符号整型问题的答案是输出是 “>6”。
当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。 这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。

评价下面的代码片断:

unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1's complement of zero */

// 对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:

unsigned int compzero = ~0;

这一问题涉及处理器字长问题。嵌入式程序员应非常准确地明白硬件的细节和它的局限。

请问以下代码有什么问题:

int main()
{
    char a;
    char *str=&a;
    strcpy(str,"hello");
    printf(str);
    return 0;
}

没有为str分配内存空间,将会发生异常,问题出在将一个字符串复制进一个字符变量指针所指地址。虽然可以正确输出结果,但因为越界进行内在读写而导致程序崩溃。

动态内存分配(Dynamic memory allocation)

嵌入式系统还是有从堆(heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能造成内存碎片,碎片收集的问题,变量的持行时间等等。
下面的代码片段的输出是什么,为什么?

char *ptr;
if ((ptr = (char *)malloc(0)) == NULL)
    puts("Got a null pointer");
else
    puts("Got a valid pointer");

这是一个有趣的问题。最近在我的一个同事不经意把0值传给了函数malloc,得到了一个合法的指针之后,我才想到这个问题。这就是上面的代码,该代码的输 出是"Got a valid pointer"。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正确。得到正确的答案固然重要, 但解决问题的方法和你做决定的基本原理更重要些。

晦涩的 C 语法

C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?

int a = 5, b = 7, c;
c = a+++b;

//上面的例子是完全合乎语法的。问题是编译器如何处理它?根据最处理原则,编译器应当能处理尽可能所有合法的用法。因此,上面的代码被处理成:

c = a++ + b;

因此, 这段代码持行后a = 6, b = 7, c = 12。
这个问题的最大好处是这是一个关于代码编写风格,代码的可读性,代码的可修改性的好的话题。

死锁

多个并发进程因争夺系统资源而产生相互等待的现象即为死锁。即:一组进程中的每个进程都在等待某个事件发生,而只有这组进程中的其他进程才能触发该事件,这就称这组进程发生了死锁。

产生死锁的本质原因为:

  1. 系统资源有限。
  2. 进程推进顺序不合理。

死锁的4个必要条件:

  1. 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
  2. 占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
  3. 不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
  4. 循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。那么,解决死锁问题就是相当有必要的了。

死锁的处理方式主要从预防死锁、避免死锁、检测与解除死锁这四个方面来进行处理。

预防死锁:

  1. 资源一次性分配:(破坏请求和保持条件)
  2. 可剥夺资源:即当某进程新的资源未满足时,释放已占有的资源(破坏不可剥夺条件)
  3. 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

避免死锁:
预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。

检测死锁:

  • 首先为每个进程和每个资源指定一个唯一的号码;
  • 然后建立资源分配表和进程等待表

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

  1. 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
  2. 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

TCP与UDP

TCP和UDP是OSI模型中的运输层中的协议。TCP提供可靠的通信传输,而UDP则常被用于广播和细节控制交给应用的通信传输,两者主要的不同体现在一下几个方面:

  1. TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。
  2. TCP提供可靠的服务。它通过校验和,丢包时的重传控制,序号标识,滑动窗口、确认应答,次序乱掉的分包进行顺序控制实现可靠传输。即通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达; UDP尽最大努力交付,即不保证可靠交付。
  3. UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高要求的通信或广播通信场景。
  4. 每一条TCP连接只能是点到点的; UDP支持一对一,一对多,多对一和多对多的交互通信方式。
  5. TCP对系统资源要求较多,UDP对系统资源要求较少。

UDP有时比TCP更有优势:
UDP以其简单、传输快的优势,在越来越多场景下取代了TCP, 如实时游戏。

  1. 网速的提升给UDP的稳定性提供可靠网络保障,丢包率很低,如果使用应用层重传,能够确保传输的可靠性。
  2. TCP为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程,由于TCP在内置的系统协议栈中,极难对其进行改进。
    采用TCP,一旦发生丢包,TCP会将后续的包缓存起来,等前面的包重传并接收到后再继续发送,延时会越来越大。
    基于UDP对实时性要求较为严格的情况下,采用自定义重传机制,能够把丢包产生的延迟降到最低,尽量减少网络问题造成的影响。
  • TCP 三次握手流程

    • 客户端发个请求“开门呐,我要进来”给服务器
    • 服务器发个“进来吧,我去给你开门”给客户端
    • 客户端有很客气的发个“谢谢,我要进来了”给服务器
  • TCP 四次挥手流程

    • 客户端发个“时间不早了,我要走了”给服务器,等服务器起身送他
    • 服务器听到了,发个“我知道了,那我送你出门吧”给客户端,等客户端走
    • 服务器把门关上后,发个“我关门了”给客户端,然后等客户端走(尼玛~矫情啊)
    • 客户端发个“我知道了,我走了”,之后自己就走了

sizeof和strlen

sizeof是运算符,在编译时即计算好了; 而strlen是函数,要在运行时才能计算。

switch()的参数类型

C/C++中支持byte,char,short,int,long,bool,整数类型和enum(枚举)类型,不支持float,double,string。

全局变量可不可以定义在可被多个.C文件包含的头文件中?为什么?

可以,在不同的C文件中以static形式来声明同名全局变量。
可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错。

程序的内存分配

一个由c/C++编译的程序占用的内存分为以下几个部分

  1. 栈区(stack)—由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  2. 堆区(heap)—一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表
  3. 全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
  4. 文字常量区—常量字符串就是放在这里的。程序结束后由系统释放。
  5. 程序代码区—存放函数体的二进制代码

Heap与Stack

Heap是堆,Stack是栈。
Stack的空间由系统自动分配/释放,Heap上的空间手动分配/释放。
Stack空间有限,Heap是很大的自由存储区,malloc函数分配的内存空间即在堆上

程序的局部变量存在于哪里,全局变量存在于哪里,动态申请数据存在于哪里。

程序的局部变量存在于栈区;全局变量存在于静态区;动态申请数据存在于堆区。

数组与指针的区别

数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。

  1. 修改内容上的差别
    char a[] = “hello”;
    a[0] = ‘X’;
    char *p = “world”; // 注意p 指向常量字符串
    p[0] = ‘X’; // 编译器不能发现该错误,运行时错误
    
  2. 用运算符sizeof可以计算出数组的容量(字节数)。sizeof§为指针得到的是一个指针变量的字节数,而不是p所指的内存容量。C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。

C语言中各进制表示法

八进制数 octal number
二进制数binary number
十进制数Decimal Number
十六进制数hexadecimal number
%d 代表十进制
%o 代表八进制
%x 代表十六进制
%u 无符号十进制数
%e 以科学记数法表示
%#o 代表带前缀o的八进制
%#x 代表待前缀ox的十六进制
\0oo 八进制值(o表示一个八进制数字)
\xhh 十六进制值(h表示一个十六进制数字)

16进制0x234这样的(如24就是0x018,凡是以0X或0x开头的数字序列) 8进制01111这样的(凡是16进制0x234这样的(如24就是0x018,凡是以0X或0x开头的数字序列) 8进制01111这样的(凡是以0开头的数字序列)以0开头的数字序列)

Nand Flash与Nor Flash

  1. 访问接口差异
    NorFlash有数据总线和地址总线,所以NorFlash的读取只要传送线性的地址就可以把数据送到数据线上了,对于NorFlash的编程同样需要先擦除然后再编程操作,不过NorFlash以扇区为单位进行擦除。现在的存储要求越来越大,而NorFlash就需要更多的地址线来寻址,所以NorFlash的使用相对NandFlash使用要少。
    而nand flash没有这类的总线,只有IO接口,只能通过IO接口发送命令和地址,对nand flash内部数据进行访问。
    相比之下,nor flash就像是并行访问,nand flash就是串行访问,所以相对来说,前者的速度更快些。

  2. 读写速度和成本
    nor的成本相对高,具体读写数据时候,不容易出错。总体上,比较适合应用于存储少量的代码。Nand flash相对成本低。会有坏块出现,使用中数据读写容易出错,所以一般都需要有对应的软件或者硬件的数据校验算法,统称为ECC。由于相对来说,容量大,价格便宜,因此适合用来存储大量的数据。其在嵌入式系统中的作用,相当于PC上的硬盘,用于存储大量数据。

参考 Nand Flash 和Nor Flash的区别NAND flash和NOR flash的区别详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值