嵌入式LInux之C语言提升篇---Skr Skr Up Up

 嵌入式C语言提升

致敬:每一个奋斗的人! Up Up Up
C语言常见编译错误
1.预处理错误 -E
    1-1 找不到源文件错误  
       自己定义头文件 使用 “xxx.h” 搜索的目录 ./
       系统头文件     使用 <xxx.h> 搜索的目录 系统环境变量
       解决办法
       gcc -I跟查找头文件的目录
       举例
       gcc -o build -I./include xxx.c
2.编译错误 -c (语法错误)


3.链接错误 -o 原材料不够,或者多了
       collect2 -undefined (原材料缺少)
       寻找标签是否实现。

预处理的使用
#include  包含头文件 使编译程序将另一源文件嵌入到带#include 的源文件中
#define   宏定义 (替换)
   #define 宏名 宏体 (不进行语法检查)
   #define ABC 3+5
   printf("the %d ",ABC*5); ----?
   #define ABC (3+5)    ---?
   #define ABC(x) (5+(x))
#undef    撤销已定义过的宏名

条件编译     003.c
#if          #if的一般含义是如果#if后面的表达式为TRUE(真)
             则编译它与#endif之间的代码,否则跳过这些代码。
#else        命令#else的功能与C语言的else一样,#else建立另一个选择(#if失败的情况)
#elif        命令#elif 类似于 else if 形成一个if else-if 阶梯状语句
             可进行多种编译选择
#endif       命令#endif 标识 一个#if块的结束

#ifdef       如果有定义执行编译代码
#ifndef      如果没有定义执行编译代码
可以使用GCC 编译选项添加预处理宏定义
              gcc -D[宏名] 不需要空格
其他
#line       改变当前的行号和文件名,他们是在编译中预处理定义的标识符
            命令的基本形式如下:
           #line number["filename"]
#error      编译程序时,只要遇到 #error 就会产生一个编译错误提示信息 ,并停止编译
            #error 错误信息
#pragma     为实现时定义的命令,它允许向编译程序传送各种指令
             编译程序可能有一种选择,它支持对程序执行的追踪。
预定义宏
__FUNCTION__: 函数名
__LINE__    :行号
__FILE__    : 文件名
__DATA__    : 表示编译时刻的日期字符串,“25 Dec 2018”
__TIME__    : 表示编译时刻的时间字符串, "12:30:44"


宏展开下的 # ##  004.c
#  字符串化
## 连接符号


C 语言关键字 32 个
关键字:编译器 预先定义好的有一定意义的 字符串

杂项:
     sizeof:计算对象所占内存大小 sizeof(xxx) --不是函数--
     编译器给我们查看内存容量的一个工具
     return:子程序返回语句(可以带参数,也可不带参数)
     用于终止一个函数并返回其后面跟着的值
     用法: return (Val)
    -规则-
     :return语句不能返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁
     例子:
     char *func(void){
         char str[30];
         .
         ...
         return str;
     }
数据类型:
  C语言操作的对象---> 资源/内存
  资源的属性【大小】---> 数据类型--->”尺子“
 
  char
  硬件芯片操作的最小单位: bit 1 / 0 位
  软件操作的最小单位:     8bit == 1 Byte 4M  --> 4Mbit Kbit/s KB/s
  char = 1Byte = 8bit == 2^8 = 256
  ==================================================================
  int
  编译器最优处理大小:
  系统一个周期,所能接受的最大处理单元,int 32位
  int = 4Byte = 4*8bit = 32bit == 2^32   (与编译器有关)
 
  short = 2Byte = 16bit
  long = 4Byte = 32bit
 
  unsigned 声明无符号类型变量
  signed   声明有符号类型变量
  int a ---展开--- signed int
  区别:内存空间的最高是符号位还是数据
  有符号: 数字
  无符号: 数据 (采集数据)
  区别 :
  char a  = -1;  0xff;
  a>>1
  unsigned char b = 0xff;
  b>>1;
  ==================================================================
  浮点数
  float:声明浮点型数据  4Byte = 32bit
  double:声明双精度变量 8Byte = 64bit
  浮点型常量
  1.0f
  =================================================================
  void 空类型 用于占位 到时候强制转换
===================================================================
  自定义数据类型
  struct :声明结构体变量
          元素之间的集合
  struct myabc{
     unsigned int a;
     unsigned int b;
     unsigned int c;
     unsigned int d;
  }; -------->这里只是告诉编译器,有定义该结构体的能力(声明);
  struct myabc mybuf;划分内存 (定义);
  顺序有要求
  -----------------
  |               |         |——|———————————————|
  |---------------|   ----》|--|---------------|
  |---------------|
  |---------------|
    (结构体内存模型)        (共用体内存模型)
  union 声明联合数据类型
    共用起始地址的一段内存(占用的内存为最大数据类型,但是指定好后就是指定数据类型)
    技巧型代码
  union myabc{
      char a;
      int b;
  }; (声明)
  union myabc abcbuf;

  enum:声明枚举类型 【人和人交流限制】
  enumerate 一一列举
  用法
  enum [枚举名称]{常量列表};
 
  typedef:用以给数据类型取别名
  int a ;         a是一个int 类型的变量
  typedef int a ; a是一个int 类型的外号(声明 没有申请内存)
------------------------------------------------------------------------------------------------
逻辑结构:改变程序执行的方式 (提高逻辑思维)
CPU 顺序执行程序
分支 ---> 选择
循环 ---> 重复

条件:
if(条件表达式)
    xxxx
else
    xxxx
if 使用规制:
先处理正常情况,在处理异常情况;
理解:
if(NULL != p);
    func();
和:if(NULL != p)
       func();

多分支 switch case default
switch(整形数字)
    case :
        break
    default
使用规则:
1.每一个case语句的结尾绝对不要忘记加break,否则将导致多个分支重叠(除非有意为止)
2.最后必须使用default分支,即使程序真的不需要default处理,也要保留。
3.case后面只能是整型或者字符型的常量或者常量表达式。
================================================================================================
 do while for
for   次数
while 条件 先判断while后面括号的值,如果是真则执行其后面的代码,否则不执行;
    while(0)
     {
         printf("0 ");
         break;
     }
     while(-1){
        printf("-1");
        break;
    }
    while(1)
    {
        printf("1 ")
        break;
    }
do (先执行在判断)
while

continue break goto
continue :终止本次循环(本轮循环)
break    :终止本层循环(消除一次循环体)
===============================================================================================
类型修饰符
   对内存资源存放位置的限定
   资源属性【位置的限定】
资源位置:
auto: 声明自动变量 默认情况 --- > 分配的内存是可读可写的区域 (区域如果在{ }里面,就是栈空间内)
register: 限制变量定义在寄存器上的修饰   内存(存储器) 寄存器  定义一些快速访问的变量
(编译器会尽量安排CPU的寄存器去存放这个值,&对register不起作用)
  使用规则:register变量必须是能被CPU寄存器所接受的类型。
static:
    静态
应用场景:
1.函数内部的变量
    int fun()
  {
     int a ===> static int a
  }
2.函数外部的变量
 int a ===> static int a
 int fun()
{

}
3.修饰函数
int fun() === > static int fun();

const: 常量的定义 修饰只读变量
extern: 外部申明
volatile:告知编译器编译方法的关键字,不优化编译 给硬件打交道
修饰的变量的值的修改,不仅仅可以通过软件,也可以通过其他方式(硬件外部的用户)
----------------------------------------------------------------------------------------------
运算符
算术操作运算符
+  加
-  减
*  乘
/  除
%  求余数 n%m = res [0 ~ m-1]
   应用场景:
         1.取一个范围的数
         2.得到M进制的个位数
         3.得到循环数据结构的下标
==============================================================================================
逻辑运算
  真 假
  返回结果 1(真) 0 (假)
|| 逻辑或 有一真即可
&& 逻辑与 都真才为真
> 大于
< 小于
>= 大于等于
<= 小于等于
!  不等于
? : 三目运算
=============================================================================================
位运算符
 1. 移位操作 (注意:数据 与数字 操作是不同的)
<< 左移位 乘法 *2 (2进制下的移位) m<<n == m*2^n
>> 右移位
&  按位与  
应用场景:
   1.屏蔽 int a = 0x1234; a & 0xff00 = 0x1200
   2.取出 int a = 0x12; a & 0xff = 0x12
   3.硬件清零器   clr
|  按位或
应用场景:
     1.设置高点频的方法 设置器 set
^  按位异或  相当与减
~  取反
=============================================================================================
赋值运算符
=
+=
-=
&=
|=
============================================================================================
内存访问符
() :限制符 (a+b)*c 函数访问 fun()
[] :数组 内存访问的ID符号
{} :函数体限制符
--------------------------------------------------------------------------------------------
->
.
--------------------------------------------------------------------------------------------
& :取地址
* :指针
=============================================================================================

2-3:C语言内存空间的使用(指针 数组 结构体,共用体 内存分布图 段错误分析)
指针:一种保存变量地址的变量: 首先--变量 作用:保存变量的地址 pointer = 指向
指针: 内存类型资源地址,门牌号的代名词
指针变量:存放指针这个概念的盒子。
 int  a ; ----> |  |  |  |  a 相当与 盒子
 *P
C语言编译器对指针这个特殊概念,有2个疑问?
1.分配一个盒子,盒子多大?
      在32bit系统中,指针就是4个Byte
2.盒子里要存放的地址,所指向 内存的读取方法是什么?
      看前面的数据类型,看在内存读取方式。如果char *p_ch 取读 1Byte
      int *p_int 4Byte
   & 取地址符

指针与修饰符
const:常量(只读性质)
内存属性:
1.内存操作的大小
2.内存的变化性,可读可写
       理解如下:
             char const *p/const char *p[T]
解析:  const char *p :所指向的位置可以改变,但是位置里面的内容不可变。 字符串 “hello world”
                char *const p[T]/char *p const
解析:char *const p:所指向的位置不能改变,所指向位置里面的内容可变。    硬件资源  LCD
解析: const char *const p
const char*const P:指针所指向的位置不能改变,同时位置里面的内容也不能改变。 ROM 空间

voliatile:防止编译器优化指向内存的优化,(硬件修改的内容,编译器可能将其去掉了)
char *p
voliatile char *p

typedef:取别名
什么类型  变量名称;
 xxxxx   neam
int (*p[10](int ,void(*p)(int)));
typedef char* name_t; 声明一个name_t类型,其类型是指的是char * 类型
name_t my_name; == char * my_name;
====================================================================================================
指针+运算符
加减运算 ++,--,+,-:
int a =12;
a+1 = 13
int *p = 0x12;
p+1 = [0x12+1*(sizeof(*p))] = 0x16;
指针的加/减法运算,实际上加/减的是一个单位,单位的大小可以使用sizeof(p[0]);

[]:
变量名[n]
n:ID标签
地址里面的内容的标签访问方式
*(p+n) = p[n]
================================================================================================
逻辑操作符 (> >= < <= == !=)
对地址操作一般只要的就是与特殊位置的比较
1.跟一个特殊值进行比较 0x0 地址的无效值,结束标志 NULL
  int *p
  if(p == 0x0)/ if(p==NULL)
2.指针必须是同类型的比较才有意义(编译器操作时使用)
================================================================================================
多级指针(关系)地址表
int **p;
存放地址的地址空间
char **p;
p[0] ,p[1].....p[n]
p[m] = NULL 结束了

int main(int argc, char **argv)
================================================================================================
数组 = 地址常量的标签
   内存分配的一种形式  越界问题
数组名是一个常量符号,一定不能放在=的左边 相当于标签
char buff[100] ;
     buff = "hello world! ";  XXXXXXX错误使用
     buff = {"hello world! "};
定义一个空间:
   1.大小
   2.读取方式
数据类型 数组名[m]
解析: [] == 告诉编译器申请一个连续内存空间 大小 m个区域(小方格) 数据类型取内存的方式 一个小方格的大小Byte
int a[100]: 100个int类型的连续空间   

数组空间的初始化
1.空间的赋值:按照标签逐一赋值(程序员工作量太大,能不能用编译器进行一些自动处理,帮组赋值)
---》空间定义时,就告知编译器初始化情况,空间的第一次赋值,初始化操作
int a[10] = 空间; 第一次赋值
C语言本身,CPU内部本身一般不支持空间与空间的拷贝;
{} == 空间限制块
int a[10] = {10,20,30,40,50,60,70,80,90,100};

#include <stdio.h>
void main()
{
    char buf[100];
#ifdef A
    buf = "hello wolrd! ";
    printf("buf:%s",buf);
#endif
#ifdef B
    char buf1[100] = {"helo wolrd "};
    printf("buf1:%s",buf1);
#endif
#ifdef C
    buf = {"===== "};
    printf("buf:%s",buf);
#endif
}
演示代码:
[root@localhost lesson1]# gcc 005.c -o 005-1 -D A
005.c: 在函数‘main’中:
005.c:7: 错误:将‘char *’赋值给‘char[100]’时类型不兼容
[root@localhost lesson1]# gcc 005.c -o 005-1 -D B
[root@localhost lesson1]# ./005-1
buf1:helo wolrd
[root@localhost lesson1]# gcc 005.c -o 005-1 -D C
005.c: 在函数‘main’中:
005.c:15: 错误:expected expression before ‘{’ token
数组空间的初始化和变量的初始化本质不同,尤其是嵌入式的裸机开发中,空间的初始化往往需要库函数的辅助

软件的最小空间char
char buf[10] = {'a','b','c'};
buf当成普通内存来看,没有问题
buf当成一个字符串来看,最后加上一个''或者0
字符串的重要属性,结尾一定有一个‘'
char buf[10] = {"abc"};
区别:char buf[10] = "abc"  char *buf = "abc";
解析: “abc” === 地址

第二次内存初始化,赋值只能逐一处理;
strcpy ,strncpy
一块空间,当成字符空间,提供了一套字符拷贝函数
字符拷贝函数的原理:
    内存空间和内存空间的逐一赋值的功能的一个封装体
    一旦空间出现了0给个特殊值,函数即将结束。
#include <string>
char *strcpy(char *dest,const char * stc); == 非常严重的内存泄露函数
char *strncpy(char *dest,const char *stc,size_t n); (推荐使用)
字符空间
    可以使用ASCII码来解码的空间,===人能看懂的数值 ''作为结束标志
===============================================================================================
非字符空间(不能使用strcpy /strncpy)
    数据采集 0x00 ~ 0xff 8bit
    开辟一个存储这些数据的盒子
    unsigned char buf[10]; ----> 数据
拷贝三要素(非字符串空间):使用memcpy()
1.源资源
2.目标
3.拷贝的个数
#include <string>
void *memcpy(void *dest,const void*str,size_t n); n单位是字节 Byte

int buf[10];
int sensor_buf[100];
memcpy(buf,sensor_buf,(10*sizeof(int)));
========================================================================================
指针与数组
int a[100];
char * a_p[100];  sizeof(a_p) = 100*4
1.指针数值(二维指针):数组里面保存的地址,地址在内存的取地址方式就是前面的数据类型。
数组名的保存
定义一个指针,指向int a[10]的首地址;
定义一个指针,指向int b[5][6]的首地址;
 *p p这个变量被*修饰了,只能是一个地址的变量了。
int *p[5]与int (*p)[5]区别?
解析:
    int *p[5] : 5元素的数组,修饰数组* 说明是数组里面装的是用来存放地址,这些地址在内存
中取地址的方式 int类型(一次取走4Byte);
    int (*p)[5]:里面有一个()修饰 先翻译()里面的内容,*修饰的是地址 p表示一个存放的是地址变量
    但是在内存中取地址的方式 int[5] 5*sizeof(int) = 5*4 =20 Byte;


int b[2][3][4] == int (*p)[3][4];
========================================================================================
结构体(打包数据类型)
1.字节对齐
struct abc{
    char ch; //1Byte
    int   i; //4Byte
};

printf("sizeof:%lu ",sizeof(abc));  == ???
效率,希望空间换时间的效率。最终打包的大小一定是4的倍数;
结构体里面的变量的位置不一致,打包的大小可能不一样。
============================================================================================
内存分配图
内存属性:
1.大小
2.在哪儿(位置)
int a ---》在哪儿?

编译----》汇编-----》连接

int main()
{
   int a;
    a =0x10;
    
    printf("a&:%p ",&a);
    printf("main:%p",main);
}
全局的数据空间(初始的,未初始化的 全局变量)static RW
只读数据段 "hello world " 字符串常量  R 静态空间,整个程序结束时释放内存 生存周期最长
   常见错误: char *p = "helao world"
代码段: code                         R
 
常用的查看函数内存的命令
size  :查看函数内存大小
strings :查看"" 只读数据段
nm :查看全局数据空间
=======================================================================================
栈空间:
'{}'里面的除了有特殊修饰的变量 或者函数
运行时,函数内部使用的变量,函数一旦结束,就释放空间。
=======================================================================================
堆空间:
运行时,可以自由,自我管理的分配和释放的空间。生存周期是由程序员决定。
分配:
    malloc(),一旦成功,返回分配好的地址给我们,只需要接受。对于新地址读法,
由程序员制定。输入参数制定分配大小,单位Byte
    char *p;
    p = (char *)malloc(100);
    int *p ;
    p=(int *)malloc(5*sizeof(int));
    
释放:
   free(p);    
========================================================================================
3-4:C语言函数的使用
1.函数的概述:
     一堆代码的集合,用一个标签去描述它 (优点:复用化)
    访问:标签--- 函数名
    函数与数组的区别:
    函数具备3要素
    char *p:
    int a[100]:
函数3要素:1.函数名(地址) 2.输入参数 3.返回值
  1.函数名:
在定义函数标签 是,必须将3要素告知编译器
 int fun(int strm,char ch){};
 
 如何用指针保存函数?
 char *p;
 char (*p)[10];
 int (*p)(int char) 保存函数地址
定义函数,调用函数。
int func(int a,int b)
{
    xxxx
}
int main()
{
    func(10,2);
}
举例:lesson2/002.c
void main()
{
    void *p;
     p = (void *)printf;
    int(*myshow)(const char *,...);
    printf("printf() addr:%p ",p);
    myshow = (int (*)(const char *,...))p;
    myshow("myshow ");
    myshow("P:%p,M:%p ",printf,myshow);
}
2.输入参数(值得传递,地址的传递)
承上启下的功能

调用者:
     函数名(要传递的数据)  //实参

背调者:
   函数的具体实现
   函数的返回值 函数名(接受的数据)//形参 只开辟空间 用于放数据
   {
       具体的实现
   }
数据流向 : 实参 -----》 形参
传递的形式:拷贝;
举例:lesson2/003.c
void myswap(int buf)
{
   char str[20];
    strncpy(str,(char *)(buf),20);
    printf("The buf is:%x ",buf);
   printf("str:%s",str);
}

void main()
{
   int a = 0x20;
   char *p = "hello world! ";
   myswap(p);
   void (*func)(int);
   func = myswap;
   char *T = "My is func ! ";
   func(T);
}
--------值传递------------------------------
#include <stdio.h>
void swap(int a ,int b)
{
    int  temp;
    temp = a;
    a = b;
    b = temp;
}

void main()
{
   int a =30, b =10;
   int c ;
   printf("This a :%d,b:%d ",a,b);
#ifdef  A
    c =a;
    a =b;
    b =c;
    printf("This a :%d,b:%d ",a,b);
#endif
#ifdef B
   swap(a,b);
   printf("This a :%d,b:%d ",a,b);
#endif
}
结果:004-1 / -D A  a:30,b:10 a:10 b:30
      004-2 / -D B  a:30,b:10 a:30 b:10
上层调用者保护自己的空间不被修改的能力
---------地址传递----------------------------
#include <stdio.h>
void swap(int *a ,int*b)
{
    int  temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

void main()
{
   int a =30, b =10;
   int c ;
   printf("This a :%d,b:%d ",a,b);
#ifdef  A
    c =a;
    a =b;
    b =c;
    printf("This a :%d,b:%d ",a,b);
#endif
#ifdef B
   swap(&a,&b);
   printf("This a :%d,b:%d ",a,b);
#endif
}
结果:005-1 / -D A  a:30,b:10 a:10 b:30
      005-2 / -D B  a:30,b:10 a:10 b:30
上层,调用者让下层子函数 修改自己的空间值得方式
      连续空间的传递的方式使用地址传递
      注意修饰符 const 只读空间
int a
scanf("%d",&a);
-------------连续空间的传递------------------------
1.数组
  数组名 --- 标签(地址)
  实参:
  int abc[10];
  fun(abc);
  形参:
  void fun(int *p)(推荐) <====> void fun (int p[10])
 
2.结构体
  结构体变量
  struct abc{
      int a,b,c;
  };
struct abc buf;
实参:
   fun(buf);         fun(&buf)
形参:
void fun(struct abc buf_t)---->浪费空间   void fun(struct abc *buf_p)---->节约空间
  连续空间的传递的方式使用地址传递

  sprintf(char *str ,const char*format,...)
空间传递:
1.子函数只看空间里面的内容情况 —--+ const  void func(const char *string);
2.子函数修改上层空间里面的内容
空间与空间操作注意要素:1.首地址 2.结束标志

结束标志:内存里面存放了0x00(1B),针对于字符空间的结束标志 。
          非字符空间0x00不是结束标志,只是表示当前状态。
          
字符空间操作模板:

void fun(char *p)
{
    int i;
    while(p[i]){
        p[i] 操作 p[i] = x   a = p[i] + -
        i++
    }
}
非字符空间(不已0x00作为结束标志 )
int *p unsigned char *p short *p struct abc *p  void *p(任何类型)
结束标志:数量(Byte)
void fun(unsigned char *p ,int size)      ===升级====>  void fun(void *p,int size)  
{
    int i;
    for(i=0;i<size;i++){                   
        p[i] = ;
        a =p[i]
        ++ -- + -
    }
}
//内存拷贝
void *memcpy(void *dest,const void *str,size_t size);
ssize_t recv(int sockfd,void *buf,size_t len,int flags);
ssize_t send(int sockfd ,const void *buf,size_t len,int flags);
======================================================================
函数三要素之返回值(出口参数---启下作用(返回值 地址传递))
基本语法:
        返回类型 函数名称(输入列表)
        {
            
            return (返回信息)
        }
调用者:
       返回值接受 = fun();

被调者:
      int fun ()
      {
         int ret;
         xxxxx
        
         return ret ;
      }
拷贝
返回数据类型:
      基本类型
      指针类型(空间)
指针作为空间返回的唯一数据类型
int *fun()
地址:指向的合法性
作为函数的设计者,必须保证函数返回的地址所指向的空间是合法。(不是局部变量)
使用者:
int *fun()
int *p = fun();

函数内部实现
基本数据类型返回:
基本数据类型 fun()
{
     基本数据类型 ret
     ret = xxx;
     return ret;
}    

返回数据类型为地址空间:
char *fun()                       =====> 错误 函数返回局部变量地址  ====》改变将局部变量转化成静态区
{
   char buf[] = "hello wolrd ";
   return buf;
    
}
升级 (局部变量地址转化到)=== 静态区 只读区 堆区
char * fun(void)
{
   char *str = (char *)malloc(sizeof(char)*100);
   strcpy(str,"hello wolrd ");
   return str;
}

======================================================================================
常见面试题解析:
预处理器(Preprocessor)
1 . 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
  #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
我在这想看到几件事情:
1) #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)
2)懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
3) 意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。
4) 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。

2 . 写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个。
  #define MIN(A,B) ((A) <= (B) ? (A) : (B)) 这个测试是为下面的目的而设的:
1) 标识#define在宏中应用的基本知识。这是很重要的。因为在  嵌入(inline)操作符 变为标准C的一部分之前,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。
  2)三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的。
3) 懂得在宏中小心地把参数用括号括起来
4) 我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事?
  least = MIN(*p++, b);

3. 预处理器标识#error的目的是什么?
  如果你不知道答案,请看参考文献1。这问题对区分一个正常的伙计和一个书呆子是很有用的。只有书呆子才会读C语言课本的附录去找出象这种问题的答案。当然如果你不是在找一个书呆子,那么应试者最好希望自己不要知道答案。

死循环(Infinite loops)
  4. 嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢? 这个问题用几个解决方案。   
我首选的方案是:
  while(1)
  {

  }
  一些程序员更喜欢如下方案:
  for(;;)
  {

  }
  这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们这样做的基本原理。如果他们的基本答案是:"我被教着这样做,但从没有想到过为什么。"这会给我留下一个坏印象。
  第三个方案是用 goto
  Loop:
  ...
  goto Loop;
应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的BASIC/FORTRAN程序员。

数据声明(Data declarations)
5. 用变量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
  6. 关键字static的作用是什么?
  这个简单的问题很少有人能回答完全。在C语言中,关键字static有三个明显的作用:
1)在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
  2)  在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
3)  在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处和重要性。

Const
7.关键字const有什么含意?
我只要一听到被面试者说:"const意味着常数",我就知道我正在和一个业余者打交道。去年Dan
  Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems
  Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.如果你从没有读到那篇文章,只要能说出const意味着"只读"就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks的文章吧。)
  如果应试者能正确回答这个问题,我将问他一个附加的问题:下面的声明都是什么意思?
  const int a;
  int const a;
  const int *a;
  int * const a;
  int const * a const;
  /******/
  前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 ,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:
  1) 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)
2) 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
  3) 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。

Volatile
8. 关键字volatile有什么含意?并给出三个不同的例子。
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1) 并行设备的硬件寄存器(如:状态寄存器)
2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3) 多线程应用中被几个任务共享的变量
  回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、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)
9. 嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。 对这个问题有三种基本的反应
1)不知道如何下手。该被面者从没做过任何嵌入式系统的工作。
  2) 用bit fields。Bit  fields是被扔到C语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可重用的。我最近不幸看到 Infineon为其较复杂的通信芯片写的驱动程序,它用到了bit fields因此完全对我无用,因为我的编译器用其它的方式来实现bit  fields的。从道德讲:永远不要让一个非嵌入式的家伙粘实际硬件的边。
3) 用 #defines 和 bit masks  操作。这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:
  #define BIT3 (0x1 << 3)
  static int a;
void set_bit3(void)
  {
  a |= BIT3;
  }
  void clear_bit3(void)
  {
  a &= ~BIT3;
  }

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

访问固定的内存位置(Accessing fixed memory locations)
10. 嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。
在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换(typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同而不同。典型的类似代码如下:
  int *ptr;
  ptr = (int *)0x67a9;
  *ptr = 0xaa55;
  A more obscure approach is: (   一个较晦涩的方法是):
  *(int * const)(0x67a9) = 0xaa55;
  即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。

    

中断(Interrupts)
  11.
中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字 __interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。
__interrupt double compute_area (double radius)
  {
  double area = PI * radius * radius;
  printf(" Area = %f", area);
  return area;
  }
这个函数有太多的错误了,以至让人不知从何说起了:
1)ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。
2) ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
3) 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
4) 与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。

代码例子(Code examples)
12 . 下面的代码输出是什么,为什么?
  void foo(void)
  {
  unsigned int a = 6;
  int b = -20;
  (a+b > 6) ? puts("> 6") : puts("<= 6");
  }
  这个问题测试你是否懂得C语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是 ">6"。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。如果你答错了这个问题,你也就到了得不到这份工作的边缘。

13. 评价下面的代码片断:
  unsigned int zero = 0;
  unsigned int compzero = 0xFFFF;
  /*1's complement of zero */
  对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:
unsigned int compzero = ~0;
  这一问题真正能揭露出应试者是否懂得处理器字长的重要性。在我的经验里,好的嵌入式程序员非常准确地明白硬件的细节和它的局限,然而PC机程序往往把硬件作为一个无法避免的烦恼。
到了这个阶段,应试者或者完全垂头丧气了或者信心满满志在必得。如果显然应试者不是很好,那么这个测试就在这里结束了。但如果显然应试者做得不错,那么我就扔出下面的追加问题,这些问题是比较难的,我想仅仅非常优秀的应试者能做得不错。提出这些问题,我希望更多看到应试者应付问题的方法,而不是答案。不管如何,你就当是这个娱乐吧...

动态内存分配(Dynamic memory allocation)
14.
尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发生的问题是什么?这里,我期望应试者能提到内存碎片,碎片收集的问题,变量的持行时间等等。这个主题已经在ESP杂志中被广泛地讨论过了(主要是 P.J. Plauger, 他的解释远远超过我这里能提到的任何解释),所有回过头看一下这些杂志吧!让应试者进入一种虚假的安全感觉后,我拿出这么一个小节目:下面的代码片段的输出是什么,为什么?
  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"。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正确。得到正确的答案固然重要,但解决问题的方法和你做决定的基本原理更重要些。
Typedef
  15 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 两个指针。

晦涩的语法
  16 . C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?
  int a = 5, b = 7, c;
  c = a+++b;
  这个问题将做为这个测验的一个愉快的结尾。不管你相不相信,上面的例子是完全合乎语法的。问题是编译器如何处理它?水平不高的编译作者实际上会争论这个问题,根据最处理原则,编译器应当能处理尽可能所有合法的用法。因此,上面的代码被处理成:c = a++ + b;
因此, 这段代码持行后a = 6, b = 7, c = 12。
  如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题。我发现这个问题的最大好处是这是一个关于代码编写风格,代码的可读性,代码的可修改性的好的话题。







 















原文地址:https://www.cnblogs.com/Jack-Tango/p/9788861.html