1. const
1.1 const int * p; int const * p;
以上两个语句中的const都在*之前,都是用来修饰*p的,等价。
*p代表指针p所指向的变量。
不能修改*p就意味着不能通指针p来修改p所指向变量的值。
但是指针p的值可以修改,即可以改变p指向的变量(指针p里面保存的地址)。
const int *p; /* 可以不必初始化p,p的值,即p的指向,可以改变 */
int a = 1;
p = &a; /* 可以,*p为常变量,但p仍是普通变量 */
*p = 2; /* 错误 */
1.2 int * const p = &a;
此时,const直接修饰p,即指针p为常变量,p的值不能改变,即指针p所指向的位置不能改变,但是*p可以改变,即p指向的变量的值可以通过p来修改。
int * const p; /* 错误,p为常变量,必须初始化 */
int a = 1;
int * const q = &a;
*q = 2; /* 正确 */
类似于定义的数组:
int p[10] = { 0 }; // p不能改变指向的位置
2 大端/小端
在x86的windows下,小端模式:
long long int a = 0X0102030405060708;
for(int i=0; i<8; i++)
{
printf("%d ", *((unsigned char*)(((unsigned char*)&a)+i)));
}
输出结果:
8 7 6 5 4 3 2 1
union:
例1:
union ldshape {
long double f;
struct {
uint64_t m;
uint16_t se;
} i;
};
long double x = ...;
union ldshape u = {x};
u.i.se &= 0x7fff;
一个内存块的一体两面,但是编码规则中一般会避免union的使用。
例2:
/* Get the more significant 32 bit int from a double. */
#define GET_HIGH_WORD(hi,d) \
do { \
union {double f; uint64_t i;} __u; \
__u.f = (d); \
(hi) = __u.i >> 32; \
} while (0)
例3:
double x = ...;
union {double f; uint64_t i;} u = {.f = x};
do{…}while(0):
在定义宏函数时,如果有多条语句,
那么不妨使用do{…}while(0)将其包围。
因为如果使用if , 有两种可能:
if(bSomething) DO_SOMETHING(x,k);
或者
if(bSomething) { DO_SOMETHING(x,k); }
如果是第二种,那么没有什么影响。
如果是第一种,只能使用do while,否则if后面只会接受一条语句,
就算使用{}将几条语句包起,使用宏函数一般会在后面追加分号“;”,
又给分成了两条语句,else无法匹配,发生语法错误。
#pragma :
它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作。
#pragma message("hello")
#pragma code_seg( ["section-name" [, "section-class"] ] )
它能够设置程序中函数代码存放的代码段,当我们开发驱动程序的时候就会使用到它。
#pragma once
只要在头文件的最开始加入这条指令就能够保证头文件被编译一次
restrict :
restrict,C语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。
restrict是c99标准引入的,它只可以用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式.即它告诉编译器,所有修改该指针所指向内存中内容的操作都必须通过该指针来修改,而不能通过其它途径(其它变量或指针)来修改;这样做的好处是,能帮助编译器进行更好的优化代码,生成更有效率的汇编代码.如 int *restrict ptr, ptr 指向的内存单元只能被 ptr 访问到,任何同样指向这个内存单元的其他指针都是未定义的,直白点就是无效指针。restrict 的出现是因为 C 语言本身固有的缺陷,C 程序员应当主动地规避这个缺陷,而编译器也会很配合地优化你的代码.
在vc中,使用__restrict
前缀L :
在字符或字符串前加上前缀L,表示使用的是宽字节
如 sizeof(L’\0’)的值为2,而不是1
如 wchar_t c = L'a';
#if ‘\0’-1 > 0 :
signed char为有符号字符类型
unsigned char为无符号字符类型
但是char没有规定是有符号还是无符号
#if '\0'-1 > 0 /* 用来测试char是有符号还是无符号 */
#define CHAR_MIN 0
#define CHAR_MAX 255
#else
#define CHAR_MIN (-128)
#define CHAR_MAX 127
#endif
在codeWarrior中,可以在编译器的配置中指定char是有符号或是无符号的
变参(Variable arguments)分析 :
/* 在考虑字对齐的情况下x在内存栈中所占的空间 */
#define __VA_ALIGNED_SIZE(x) ((sizeof(x) + sizeof(int) - 1) & ~(sizeof(int) - 1))
typedef char * va_list; // 一般情况下的定义
/* C99标准中void va_start(va_list ap, parmN);
* last是...之前的最后一个参数,va_start返回的是last之后参数的起始地址,
* last及其之前是固定的参数,last之后就是可变参数了,其个数和类型都不确定,
* 所以最好要在固定参数中隐含了可变参数的个数及类型信息,
* 例如int printf(const char *restrict fmt, ...),
* 在fmt中隐含了后面可变参数的个数和类型,即对可变参数的解析考虑的fmt中包含的信息,
* 而与真正传入的参数类型及个数无关,比如printf("%d%d\n,1,2,3);可以输出前面两个。
* 可以想象该函数肯定会遍历fmt来获得参数类型信息,然后会去变参列表中依次抓出该类型的数据。
*/
#define va_start(ap, last) ((ap) = (void *)(((char *)&(last)) + __VA_ALIGNED_SIZE(last)))
/* 此处va_end有修改,请注意,原为#define va_end(ap) ((void)0) */
/* C99标准中void va_end(va_list ap); 将ap清零,如果没有影响也可以不执行该操作*/
#define va_end(ap) (ap = (va_list)0)
/* 此处va_end有修改,请注意,原为#define va_end(ap) ((void)0) */
/* C99标准中void va_copy(va_list dest, va_list src); 两个可变参数表之间的传值 */
#define va_copy(dest, src) ((dest) = (src))
/* C99标准中type va_arg(va_list ap, type);
* ap代表的是在内存栈中参数列表中的位置,type则是对应存储的数据类型,
* va_arg返回的是存储在地址ap的类型为type的数据,与此同时移动ap的位置,
* 所以va_arg的作用是按顺序从参数表中取下一个参数的值
*/
#define va_arg(ap, type) \
( ((ap) = (va_list)((char *)(ap) + __VA_ALIGNED_SIZE(type))), \
*(type *)(void *)((char *)(ap) - __VA_ALIGNED_SIZE(type)) )
注意,x86系列的处理器使用栈来传参,而arm、powerpc等处理器一般优先使用寄存器来传参,参数很多时用栈传递多余的参数。
C库中NULL宏定义 :
#ifdef __cplusplus
#define NULL 0L
#else
#define NULL ((void*)0)
#endif
NULL在C/C++标准库中被定义为一个宏,一般为:
#define NULL ((void*)0) /C中的“标准”写法,NULL被替换为一个void类型的指针右值,值等于0;由于是void*类型,可以隐式转化为其它类型的指针。
#define NULL 0 //C++,相对C简化定义的原因是C++中void*无法自由隐式转换为其它类型的指针,而字面量0可以隐式转换为指针类型;尽管它实际上可以被作为整数0使用而不引起编译问题,但要注意,从设计目的上而言,NULL应当总是只作为空指针使用。
char c = NULL相当于char c = 0,也就是把c初始化为0。0在这里并不是空指针的意思,而是隐式转换成的字符’/0’——值为0的char字符。在C/C++的一般实现中,这样的行为(值为0的数值类型,无论是void*或int,转换为char的值还是0)是明确的,但这种用法并不恰当。一如LS的错误,ASCII及兼容字符集中数值0对应的字符为null character(null terminator),应该用NUL(没有在标准库中定义,语言中char类型用’/0’,wchar_t类型用L’/0’)而不是NULL表示。NUL的一个重要应用是作为C风格字符串的结尾标志字符。
VC调试时按Alt+8、Alt+7、Alt+6和Alt+5,
打开汇编窗口、堆栈窗口、内存窗口和寄存器窗口
看每句C对应的汇编、单步执行并观察相应堆栈、内存和寄存器变化
int&long:
一般在32位或64位机器上,int的长度更稳定,一般为4个字节,但有时可能是2个字节。
而通过long的长度一般可以看出机器的字长,
32位机上long为4个字节,64位机上long为8个字节,
这样通过sizeof(long)我们可以知道该机器上指针的长度
const int p & int const p :
const int *p; const修饰的是*p,即p指针指向的变量的值不能通过p改变
int * const p; const修饰的是指针p,即p指针不能随便移动指向的位置
运算符的优先级:
后缀++与后缀–的优先级最高
取地址的&的优先级较高,高于加减乘除和各种比较运算符
但是位与的&的优先级较低,低于加减乘除和各种比较运算符
这些你记牢了么?
所以不要嫌多层括号看着烦,这样也许是最清晰的
位运算:
#define ALIGN (sizeof(size_t))
#define ONES ((size_t)-1/UCHAR_MAX) // 00000001000000010000000100000001
#define HIGHS (ONES * (UCHAR_MAX/2+1))
10000000100000001000000010000000
#define HASZERO(x) ((x)-ONES & ~(x) & HIGHS)
0xffff00ff - ones fefefffe ff 1000
char *__strchrnul(const char *s, int c)
{
size_t *w, k;
c = (unsigned char)c;
if (!c) return (char *)s + strlen(s);
for (; (uintptr_t)s % ALIGN; s++)
if (!*s || *(unsigned char *)s == c) return (char *)s;
k = ONES * c; // 注意此处k的妙用,22 -> 22222222
for (w = (void *)s; !HASZERO(*w) && !HASZERO(*w^k); w++);
for (s = (void *)w; *s && *(unsigned char *)s != c; s++);
return (char *)s;
}
HASZERO中的x应该是四个ascii字符组成的一个一个字(32位机上),
用来判断这四个字符里有没有’\0’
gcc -fno-builtin
该选项禁用gcc的内建函数,特别是在使用gcc测试自己开发的C库函数的时候,非常有用,
如果不用,那么自己写的同标准C同名的函数不会被使用,而是引用内建函数。
char *strchr(const char *s,char c)
这样的接口形式,在实现时肯定会有一次从const指针到非const指针的强转,
而uc的C库也是这样实现的,但在某些安全编码规则中是不允许的,要记违背
.bss .sbss
我们知道,映像文件中bss段或sbss段是虚的,但是在系统的映像文件被加载器加载时,
会将bss段实体化,而且要将其初始化为0,但是有的加载器却不会主动初始化为0,
这是很值得关注的,因为即使赋值为0的全局变量也会进入bss段,从而可能会导致错误。
sbss段是古老的事物,但是可能仍在被使用。
此时可以在脚本中做bss段和sbss段的位置标记,在初始化代码中加入这两段的清零初始化。
整数除法,如果除数为0,那么出错。
还要注意一种情况:32位除法,除数为-1,被除数为-2147483648,结果为2147483648,
但是这个结果没法被32位的有符号整数所表示,这时也是出错。
同理,-2147483648的取绝对值也会出错。
这些会在单元测试的边界值测试中被发现,所以不要轻视边界值测试。
在32位的平台上实现64位的整数四则运算是很麻烦的,特别是除法,
所以,如果编译器的库中没有提供这些,你要认真考虑是否要自己手写,
而如果编译器提供了这些,你可以直接使用,但是注意现成的已经编译好的库和你当前的编译是否使用了相同的eabi规则,
简单的直接调用真的可能会发生错误,是遇到过这样的问题的,64位整数运算的代码量也是不小的,
而且这对编译器有了更多的依赖,甚至有的场合要验证编译器提供的算法是否正确全面也是不小的工作量。
如果你所要实现的函数会返回错误码给用户,那么最好通过返回值的形式给出错误码,
如果以参数指针的形式给错误码,那么如果指针为NULL,错误码就无法发送出去,这意味着错误无法反馈了,
只能寻找更高级的、系统级的渠道来返回错误码,这是你所期望的么?
代码文件的头部说明要给出所使用的字符编码,比如utf-8或gb2312
注意:在这两个不同的编码规则下,空格都会有不同的编码,所以当你以为自己只用了ascii码而不会发生乱码时,
空格会给你当头棒喝
overlap:
内存块的overlap没什么争议,但是字符串处理函数的overlap的定义就有争议了
strcpy(s1,s2) strcat(s1,s2) 在判断overlap的时候可以确立一个基本原则,即不改变s2的内容,
这未必是最好的原则,但是胜在要给定的规则简单,也易于理解
中断与调度之间的关系:如果已经关了中断,还能发生调度么?
m4下推荐使用PendSV中断来切换上下文,如果使用PendSV来进行上下文切换,那么关中断后是不会发生调度的,即使触发了调度,也要等开中断才能触发。
另外,也有不使用中断来进行的调度的,这种情况是直接使用当前的context进行任务调度,这时,关中断并不会影响调度,当前的关中断状态会被保存起来,知道该任务继续运行时恢复为关中断状态。
不管是哪种情况,调度切换上下文的时候都要管中断,切换上下文的过程不能够被打断的。
提供可移植的接口时,外部依赖如果不能集中管理,那么不要隐藏太深,在源码中尽早的暴露
注释:
一般只提供战略级的注释,即以注释说明函数(或是类型等)的作用及实现的大体思路(如果思路不简单直接的话)。
而战术级的注释不要随意添加,要根据你所假设的读者而审慎添加,太详细的注释未必是好事,可能会妨碍读者的阅读和思考,有时甚至是对读者的不尊重。最好给变量、函数等起一个合适的名字,通过这些名字来透露出代码在干什么。
对结构体的初始化也要小心
struct div_t tmp = {0, 0};
如果是裁剪的精简版编译器,最好不要这样初始化,它对编译器的某个库有依赖,如果没有正确的链接某个libxxx.a库,
可能会导致编译失败:没有定义__clear等符号
对cache的操作有enable、disable、invalidate、flush
其中invalidate操作是不会先将对应cache块的内容写回内存,
所以改操作可能会导致修改数据丢失。
cache块的常见大小32B,这也是cache操作的基本单位
当时测试的时候,如果取一个static变量的地址进行invalidate cache,结果可能会清零其他的全局变量。