(笔试 & 面试)嵌入式C语言基础知识

把姿态放低,你会发现看不惯的人和事越来越少,琐碎的生活不再是一地鸡毛,你和世界的关系也越来越好。 —- 人民日报 《[夜读] 格局越大,姿态越低》

通过最近几家公司的笔试及面试,发现他们不仅关注你是否知道存在这个东西,同时还需要你说出这些东西是怎么高效实现以及它们之间的区别。虽然我也答出七七八八,但能够明显发现自己有些基础不牢固,细节掌握不清楚,编码能力有点弱。做技术需要踏踏实实,一步一步脚印,从小事做起,从基础积累,细节决定成败,所以就此在这总结一下最近遇到所有问题及一些基础知识。

strcpy及相关函数的实现

1
2
3
4
5
6
7
8
9
10
11
/*
* 前提是dest具有足够的空间
*/
char *strcpy(char *dest, const char *src)
{
char *tmp = dest;

while ((*dest++ = *src++) != '\0')
/* nothing */;
return tmp;
}

这个函数为什么要返回char *类型指针?
答: 林锐的《高质量C++编程指南》给出原文如下:

有时候函数可能不需要返回值,但为了增加灵活性如支持链式表达式,可以附加返回值。
例如字符串复制函数strcpy 的原型:
char *strcpy(char *dest,const char *src);
strcpy 函数将src 拷贝至输出参数sdest 中,同时函数的返回值又是dest。
这样做并非多此一举,可以获得如下灵活性:
char str[20];
int length = strlen( strcpy(str, “Hello World”) );

memcpy及相关函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
/*
* 前提是dest具有足够的空间, 没有考虑重叠问题
*/
void *memcpy(void *dest, const void *src, size_t count)
{
char *tmp = dest;
const char *s = src;

while (count--)
*tmp++ = *s++;
return dest;
}

如果dest与src指向的内存区域有重叠,怎么实现memcpy?
答:代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* memmove实现考虑到了内存区域重叠问题
*/
void *memmove(void *dest, const void *src, size_t count)
{
char *tmp;
const char *s;

if (dest <= src) {
tmp = dest;
s = src;
while (count--)
*tmp++ = *s++;
} else {
tmp = dest;
tmp += count;
s = src;
s += count;
while (count--)
*--tmp = *--s;
}
return dest;
}

由于memcpy不需要判断重叠,所以它的运行速度比memmove快,在确定dst和src不会重叠的情况下,可以使用memcpy

strcpy与memcpy区别

  • 复制的内容不同,strcpy只能复制字符串,但memcpy可以复制任意内容,比如字符数组、整数、浮点数、结构体等
  • 复制的方法不同, strcpy不需要指定长度,直到它遇到被复制字符串的结束符\0才会停止,所以容易溢出。但memcpy则是根据其第三个参数决定复制的长度
  • 用途不同,通常在复制字符串时使用strcpy,而需要复制其他类型数据时一般用memcpy

最大公约数(Greatest common divisor)和最小公倍数(Least Common Multiple)

欧几里德法 (辗转相除法)

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
#include<stdio.h>

#ifdef RECURSION
/*
* 递归实现
*/
int gcd(int a, int b)
{
if (b == 0) /* 注意这个结束条件 */
return a;
else gcd(b, a % b);
}
#else
/*
* 循环实现
*/
int gcd(int a, int b)
{
int tmp;

while(b != 0) /* 注意这个结束条件 */
{
tmp = a % b;
a = b;
b = tmp;
}
return a;
}
#endif

int main()
{
int a, b;
while(~scanf("%d %d", &a, &b))
{
printf("gcd = %d\n", gcd(a, b)); /* 最大公约数 */
printf("lcm = %d\n", a*b/gcd(a,b)); /* 最小公倍数 */
}

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
#include<stdio.h>

int gcd(int a, int b)
{
while(a != b) /* 注意这个结束条件 */
{
if (a > b)
a = a - b;
else
b = b - a;
}

return a;
}

int main()
{
int a, b;
while(~scanf("%d %d", &a, &b))
{
printf("gcd = %d\n", gcd(a, b));
printf("lcm = %d\n", a*b/gcd(a,b));
}

return 0;
}

两者异同

  1. 都是求最大公因子的方法,前者以除法为主,后者以减法为主。特别当两个数相差较大时,前者运算次数较少。
  2. 结束条件不同,前者是当相除余数为零时,后者当减数与差相等时。

static、const和volatile

const关键字

作用:

  1. 给读你代码的人传达非常有用的信息,声明一个参数为常量是为了告诉用户这个参数的应用目的;
  2. 通过给优化器一些附加信息,添加关键字const可能产生更紧凑的代码;
  3. 合理使用关键字const可以使编译器很自然地保护那些不希望被修改的参数,防止无意的代码修改,可以减少bug的出现。

如下声明:

1
2
3
4
5
int  const a;
const int a;
const int *a;
int *const a;
const int * const a;
  1. 前两个的作用一样,a是一个常整型数
  2. a是一个指向常整型数的指针,整型数不可修改,指针变量可以修改
  3. a是一个指向整数的常指针,指针指向的整型数可以修改,但是指针变量不可以修改
  4. a是一个指向常整数的常指针,指针指向的整型数不可以修改,指针变量也不可以修改

应用:

  • 阻止一个变量被修改,可使用const,在定义const变量时,需要先初始化,不然以后没有机会修改
  • 修饰指针,指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const
  • 在一个函数声明中,const修饰形参指示在函数内部不可以改变该形参值

static关键字作用

  1. 在函数体内,一个声明为static变量在该函数被调用过程中维持其值不变。注意:静态局部变量存放位置为.data段,如果没有初始化,编译器会自动为其赋值0。
  2. 在编译单元内但在函数提外,一个声明为static变量可以被这个编译单元内所以函数访问,但不能被其他编译单元中函数访问,也就是说它是一个本地全局变量。注意:静态全局变量和普通全局变量存储位置要么.data段要么.bss段,这个关键字影响语法层面变量的作用域,不影响变量的生命期。
  3. 在编译单元中,一个声明为static函数只能被这个编译单元内的其它函数调用。也就是这个函数被限制在声明它的编译单元的本地范围内使用。

volatile关键字