char、short( int)、int、long( int)、long long (int)、float、double、long doulbe,括号内内容表示可省略。除了上述几种外,前5中还有对应的unsigned类型。3u、3ul、1.2f、1.2L。
常量:整型常量、浮点型常量、符号常量(用#define定义)、字符常量、字符串常量、枚举常量
字符常量中有一些比较特别,成为转义字符,ANSI C中的全部转义字符序列如下:
\a 响铃符 \\ 反斜杠
\b 回退符 \? 问号
\f 换页符 \' 单引号
\n换行符 \" 双引号
\r 回车符 \ooo八进制数,ooo代表1~3个八进制数字(0~7)
\t 横向制表符 \xhh 十六进制数,hh为一或多个十六进制数字(0~9,a~f,A~F)
\v 纵向制表符 \0
运算符优先级及结合性:
函数与程序结构
如果函数中省略了返回值类型,则默认为int类型。
函数间的通信可以通过参数、返回值、外部变量进行。
1、struct与typedef struct
struct Student{int a;int b}stu1; //定义名为Student的结构体,及一个Student变量stu1
struct {int a;int b;}stu1; //只定义了一个结构体变量stu1,未定义结构体名,无法再定义其他变量
typedef struct Student{int a;int b;}Stu1,Stu2; //定义结构体类型为Student,别名为Stu1或Stu2,此时有三种定义相应变量的方式:
struct Student st; 或 Stu1 st; 或 Stu2 st;
typedef struct{int a;int b;}Stu1,Stu2; //未指定结构体类型名,但给了别名Stu1或Stu2,可用之定义变量
原则1、数据成员对齐规则:结构(struct或联合union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)
原则2、结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储)
原则3、收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。
获取结构体成员在结构体内的偏移量:(将0强制转换成这个结构体的指针,来访问这个结构体中的成员,然后,取这个成员的地址,得到的就是成员相对结构体起始的偏移)
3、typedef struct node{int a; struct node*lchild; struct node* rchild;}BNode,*BTree;
这里:
BNode为struct node 的别名,相当于 typedef struct node BNode
BTree为struct node*的别名,相当于typedef struct node* BTree
4、*p++,(*p)++,*++p
5、中缀表达式转后缀表达式:
手算:
用手算的方式来计算后序式相当的简单,将运算子两旁的操作数依先后顺序全括号起来,然后将所有的右括号取代为左边最接近的运算子(从最内层括号开始),最后去掉所有的左括号就可以完成后序表示式,例如:
a+b*d+c/d => ((a+(b*d))+(c/d)) -> abd*+cd/+
中缀转前缀类似
计算机算:用堆栈
6、C/C++中的 a和&a
7、预处理的妙用
8、关于swap,C中可以通过指针实现,C++中除了用指针外,还可以通过引用&实现
另,不用临时变量的swap实现方法(理论上,用满足互逆操作的一对操作即可,如加减、乘除、异或):
用异或: x^=y; y^=x; x^=y;
用加减: x-=y; y=x+y; x-=y; 或 x=y-x; y=y-x; x=x+y; 或 x+=y; y=x-y; x-=y; 。把x、y想成一维数轴上的两点,通过x、y的大小关系容易得到前两种实现:x>y、x>y
用乘除法:理论可以,实际有精度问题,不推荐。
表达式的利用: x= x+y - (y=x) ,依赖于编译器对表达式的求值顺序,若从右到左求值则结果不正确,故不推荐用。
需要特别注意的是:
加减实现中,第三种第一步求和可能溢出,此时根据不同语言对溢出的处理,结果可能不同(Java下可得正常结果),故更推荐用减法。
注意排序场景下,若自己和自己交换,这时所有方法都会得到错误的结果即都会变成0,这种情况在数组交换元素时很可能遇到。因此应注意判断是不是同一个数组元素。看如下例子:若left和last相等,则结果执行后两者皆为0
还有一种方法,用宏替换:
9、关于结构体成员访问
10、负数二进制原码补码转换(负数在计算机内为什么用补码?只有这样才能正确实现加减法,计算机其实只能做加法,1-2变为1+(-2),1和-2只有用补码表示,所加结果才正确):
符号位不变,然后:
1、原码求补码:(1)a 取反加一得b 或 (2)a减一取反得b
2、补码求原码:(逆2)b取反加一得a 或 (逆1)b减一取反得a
即不论原码求补码还是补码求原码,都可用取反加一或减一取反这两种方法(注:当然,符号位不参加运算)
注:移码——移码(又叫增码)是对真值补码的符号位取反(不管补码是正是负),一般用作浮点数的阶码,引入的目的是便于浮点数运算时的对阶操作。
11、一个负整数与它的补码间的相互转换:
求补码表示的整数:
关于2.2),实际意义可以联系现实中的时钟,补码的作用就是把倒退x格表示为进128-x格。
负数x->补码十进制值(不包括负号):x+128。(退|x|格等于进128+|x|格)
补码十进制值x(不包括负号)->负数:x-128。(进x格等于进x-128格,即退128-x格)
如补码10000111表示的数为7-128=-121(进7等于退121),-7的补码为11111001(退7等于进121),特别地,补码10000000表示的数为0-128=-128
12、printf
%5d:右对齐,左边不够不空格
%-5d:左对齐,右边不够不空格
int(4.7):4,强转直接截断;%.0f,4.7——>5,四舍五入
13、limits.h中定义了关于整型变量的取指范围
结果:
14、产生随机数:int rand(void),void srand(int seed)
15、c语言的 atol 函数与 strtol函数
16、负整数转正整数:
通常直接取反即可:n=-n;
但是,对于最大负整数转不了,如对于一个字节的有符号整数,范围为[-128,127],若果n=-128,则-n为128,超过了n能表示的范围,这点应注意
17、负整数取余运算%,与正整数一样,即不论正负,商的绝对值尽可能小(尽可能接近0)。如-5/2结果到底是-2还是-3,可以通过位操作验证:-5右移2位结果显然是-2
如-126%10=-12......-6 而不是 =-13......4;
8%3=2......2;
-8%3=-2......-2而不是-3......1
-8%-3=2......-2而不是3......1
18、一排数,里面除了一个数出现奇数次外其他均出现偶数次,如何快速找出这个独特的数?——所有数异或即可
19、parseInt函数可以将字符串转为整数
javascript:parseInt(str,base)
base意为 str为base进制数,函数结果为该base进制数对应的十进制值
C中有类似的函数:strtol(*nptr,**endptr,base)转成长整数,endptr指向未被识别的第一个字符。如 char *endptr;strtol("1213ad",&endptr,4);——>103
21、当表达式中存在有符号数和无符号数类型时,所有的操作都自动转换为无符号类型。可见无符号数的运算优先级高于有符号数。
22、extern的使用:
23、C中的static。(局部变量存在栈中,动态申请的空间在堆中)
1、与extern相反,static限定所修饰的变量或函数的作用域,使其仅在定义它们的源文件内有效,其他文件内无法访问它们,这相当于面向对象中的private的功能!!
2、修饰局部变量时可提升局部变量的生存周期,使其在整个程序的生成周期内有效,但作用域仍只在所在的函数内。如果用户未初始化,则自动被初始化未0(这点与全局变量一样)示例如下:
24、寄存器变量:如register int x
register声明只适用于自动变量及形参,告诉编译器它所声明的变量在程序中使用频率较高,其思想是讲register变量放在机器的寄存器中,但编译器可以视情况忽略次选项。无论寄存器实际上是不是放在寄存器中,它的地址都是不能访问的。
25、C中变量一般分为四种:外部变量、静态变量、局部变量(自动变量)、寄存器变量。前两者初始化表达式必须是常量表达式。
C预处理器:
宏定义#define、#undef
文件包含#include
条件包含#if、#elif、#else、#endif、#ifdef、#inndef等
(typedef也有文本替换功能,但其功能不是由预处理器而是由编译器完成的,其文本替换功能远超#define:1)可以为类型取别名提高可读性,如tree节点;2)可以使程序参数化以提高可移植性,如果typedef声明的数据类型与机器有关,当移植到其他机器时只需改变typedef定义即可,如标准库的size_t类型, typedef unsigned __int64 size_t; )
****第五章、指针与地址****
26、
(1)指针中&为取址运算符,&a得到a类型的指针,指向a;*为间接寻址(或称间接引用)运算符,如*p就是以指针变量值(地址)为地址的内存区域的值,即指针所指向的值。
(2)指针与数组:
数组名表示数组首元素的地址,一个通过数组名和下标实现的表达式可等价地用指针加偏移量实现,如 a[1]<=>*(a+1) , &a[1]<=>a+1 。通过数组下标能完成的所有操作都可以通过指针完成,且一般前者的执行速度比后者快。 C语言内部在取数组元素值时就是通过转换成指针加偏移量再取值的,因此a[-1]在语法上也没错,等价于*(a-1)。
把数组名传给函数实际上传的是该数组首元素的地址,所以不论形参是数组还是指针,实参只要传递地址语法上就没有错误。
数组名和指针的区别:数组名表示数组首元素的地址,而指针则是一个变量,变量的内容为某个元素的地址。可通过如下两图看出区别:
(3)有效的指针运算:指针同整数加减;指向相同数组中元素的指针间的减法或比较;指针赋0或地址或指针与0比较(指针与整数不能相互转换,0除外,0代表NULL,C语言保证0永远不是有效的数据地址)。其他,如两指针加减乘除等运算都是非法的。
如下示例,本意是希望通过交换指针存的地址值实现swap,但是编译不能通过;再者,就算编译通过,函数也达不到期望的功能,因为地址也是传值传递进来的,在函数内部交换地址值堆外面没影响
再例:设low、high为指向数组首末元素的指针,求指向中点元素的指针,(low+high)/2是错的,因为两指针不允许加法运算,解决:low+(high-low+1)/2
(4)指针数组与数组指针:
指针数组(知道有几行):int * p[13]:是个一维数组,每个元素类型为 int * 。int a[3][4]; int *p[3]={a[0],a[1],a[2]};
数组指针(知道有几列):int (*p)[13](可以看成int p[][13]),是一个指针,指针类型为数组 int [13]。int a[3][4]; int (*p)[4]; p=a;(如果二维数组作为函数参数,则函数声明中必须指明该二维数组的列数,如void swap(char v[][10],int i,int j),可见相当于数组指针char (*v)[10])
由上可见,指针数组、数组指针都与二维数组有某些关联,示例如下:
实际上,C语言数组访问方式本质就是指针访问,在C语言内部都转换为指针访问。对于二维来说,最后都转为上面的*(*(p+i)+j),*(*(q+i)+j)
(5)二维数组与指针数组
对于 int a[10][20] 与 int *b[10]; ,它们有相通之处如都可以通过a[2][3]、b[2][3]访问元素,但有本质区别:前者分配了10*20个int空间,后者分配了10个 int * 空间,若后者每个元素指向20个元素的int数组,则还分配10*20个int空间。
可见若用后者表示前者,则会多分配一些空间;后者的优点在于每个元素不必指向相同长度的数组,这在用于存储多个字符串时很有用,如char * argv[10]
(6)函数指针与函数:(任何类型的指针都可以转化为void * 类型,并且在将它转换回原来的类型时不会丢失信息)
函数:char *cmp(char *,char *);cmp是一个函数,返回值类型为char 指针
函数指针:int (*cmp)(void *,void *):cmp是个指向函数的指针,该函数具有两个void*类型的参数,其返回值类型为int。使用函数指针的一个示例是作为排序算法里排序规则参数,示例如下(在传 函数指针 形参对应的函数实参时不用取址运算符&):
(7)其他
printf的格式化参数可以是表达式:printf(a[0][0]>0 ? "yes:%d\n" : "no\n",a[0][0]);
字符串常量可整体赋给字符指针: char *str; str="hello world"; 实际上是将指向字符串常量第一个元素的指针赋给str,而若str是字符数组str[]则不能整体赋值。此外,str没法改变指向的常量字符串的个别字符值,这点也与字符数组不同。
****第六章、结构****
结构初始化:
27、联合、位字段是以结构为基础的数据结构,可以节省存储空间
结构体定义及内存空间分配原则见1、2、3
(1)在所有运算符中,结构运算符 “.” 和 “->” 、函数调用的 “()” 、用于下标的 “[]” 优先级最高
(2)sizeof是C语言提供的一个编译时一元运算符 而不是函数,返回无符号整型。条件编译语句#if中不能使用sizeof,因预处理器不对类型名进行分析;#define中则可
确定 int数组 data[]元素个数:
1)、sizeof(data)/sizeof(int):适用于不确定是否至少有一个元素;需要知道类型
2)、sizeof(data)/sizeof(data[0]):适用于至少有一个元素;不需要知道类型
注意sizeof的陷阱,常用它来计算数组的元素个数,但在函数里使用时实际上得到的是数组变量(指针)的大小而不是元素个数,示例:
注意:
2.对字符串使用sizeof包含'\0',而strlen遇到'\0'时停止并且长度不包含'\0'。
(3)自引用:包含有自身实例的结构是非法的,但是可以有指向自身结构类型的指针,也可以两个结构相互引用(指针)
(4)联合:联合与结构类似,不同的是其各成员共享内存,相对于基地址的偏移量都为0;其所占大小要大到足够容纳最“宽”的成员;且对齐方式要适合于联合中的所有类型的成员(即不是单纯地取最大字节的变量就完事)。
可以通过联合判断处理器的大端或小端:[ 大端小端取决于CPU架构,power pc,aix等是大端,arm,Intel(x86系列等),amd是小端 ]
(在windows上测试如下)
(5)位字段:与结构一样,不同的是在定义时需要指定成员所占的位数,与基本数据类型类似,可以把位字段看成特殊的一种数据类型,只不过其占落干位。(实际上,位字段所提供的功能也可以通过位操作自己实现)
主要用于一些使用空间很宝贵的程序中,如嵌入式程序。对于位字段中的成员不能用位操作符进行运算也不能使用&取址,因为它们没有地址。考虑到字节存放的大端小端的问题,使用位字段定义的数据不能在不同字节顺序的机器之间移动。因此,从理论上来说,使用位字段的程序是不可移植的。
****第七章、输入与输出****
28、
(1)printf、fprintf 格式输出:%[flags][width][.perc][ d|i|u|o|x|c|s|f ]。 (int printf(char *format, arg1, arg2, ...),返回值为打印的字符数,包括'\0',数有多位算多个字符)
总:flags:负左对齐正右对齐;width:至少占width位;perc:精度要perc位,小数为位数,整数为位数不够补前0,字符串为字符个数。
i与d无异,是旧式写法;输出时不管是float还是double都用f,输入则分别为f、lf;可以用"%%"打印"%"自身。
printf的格式%a.b_(a表示至少占a位,负左对齐正右对齐;b对于_的不同有不同意义。_处可为d、i、u、o、x,c、s、f 等)
a、b也可以用*代替,然后动态确定:printf("%*.*d",a,b,num);位数不够自动补前导0的方法:printf("%.5d %05d\n",12,4);//00012 00004
示例:
结果:
(2)scanf、fscanf、sscanf 格式输入:% [*] [2] [l] [ d|i|u|o|x|c|s|f|集合 ]。 (int scanf(char *format, ...),返回正确读入的个数,若到达文件末尾,返回EOF;format后的参数都必须是指针)
与格式输出printf等的交集在于d、f...几个字符,此外无。有各自的特别之处,printf如上述,scanf 如下(注意,printf、fprintf并没有如下这些特性):
1)%nd 表示读入不超过n位。如scanf("%2d",&a),输入为1234时a为12
2)读入字符串支持集合操作:
%[a-z]:从首字符起的连续字符串,仅包含字母 a 到 z
%[az]:从首字符起的连续字符串,仅包含字母 a 和 z
%[^a-z]:从首字符起的连续字符串,遇到字母 a 到 z 之一停止。这里a-z也可以为若干个字符,如 a 或 abcd
3)* 表示跳过此数据不读入,也就是不把此数据读入参数中
合理使用这三者,可以当“正则”使用,甚至可以通过scanf、sscanf、printf等实现复杂的功能,如后缀表达式计算等。一些例子如下:
(3)其他
读入一行时,gets会删掉结尾的'\n'、puts会在写入字符串时在结尾添加一个'\n';fget、fputs则原样读入、原样输出。对比如下:
(4)变长参数表:参数格式和类型可变。stdarg.h中包含一组宏定义,对如何遍历参数表进行定义。C的变长参数表应至少有一个有名参数,省略号只能出现在尾部
以下在不同的编译器有不同的实现,但接口一致:
以vc6.0编译器为例:
29、ungetc(int c,FILE *fp):将读出的数据放回到缓冲区去,下一次读数据时,再次读出来。可压入到缓冲区的字符数与缓冲区中已输出字符数有关?
30、存储函数管理:malloc、realloc、calloc、free、memset、memcpy/memmove、memcmp等
1)void *malloc(size_t n)
从堆中动态分配n字节大小的空间,成功返回首地址,失败返回NULL;
由于malloc函数值的类型为void型指针,因此,可以将其值类型转换后赋给任意类型指针,这样就可以通过操作该类型指针来操作从堆上获得的内存空间;
分配得到的内存空间未初始化,可以用 C++中的memset(见后面)来初始化,或用会自动初始化的calloc
2)void * realloc(void * p,int n)
将p指向的对象空间的大小调节为n字节,成功返回首地址,失败返回NULL;
p应为指向堆空间的指针(即指向动态分配空间对象),若p为空,则重新分配一块新的区域;
分配得到的空间也是未初始化的
3)void *calloc(int n,int size)
分配n个大小为size字节的空间,相当于malloc(n*size),成功返回首地址,失败返回NULL;p=(int *)calloc(SIZE,sizeof(int));
自动初始化为0
4)memset(void *p,int ch,size_t n):将指针p指向的对象的前面n个字节的每个字节以ch代替
5)free(*p)释放指针p指向的动态申请的空间,free后p仍指向原来指向的地方,但不能再访问p指向的对象,否则系统会报错,指向的对象空间(堆中)被OS回收,值有没有被改看系统的实现。这时p为野指针,一般还要置空:p=NULL
6)memcpy、memmove 的功能见名知意,其区别:源地址区域和目标地址区域有重叠时,后者可按预期正常复制、而前者则可能会错乱覆盖。
31、整数位运算
概括而言,几种位运算的定义及其典型用途:(注:下面说的“任何位值”指“0或1”)
按位与运算:
定义:0和任何位值的与得0、1和任何位值的与得原位值,即两位都为1时相与得1否则得0;
用途:将一个数的若干位置为0 ; 取一个数的若干位(也即将该数除了这些位外的其他位置为0,所以本质上作用也是置0);
按位或运算:
定义:0和任何位值的或得原位值、1和任何位值的或得1,即两位至少有一个1时相或得1否则得0;
用途:将一个数的若干位置为1;
按位异或运算:
定义:0和任何位值的异或得原位值、1和任何位值的异或得原位值的反,即两位不同则异或得1否则得0;
用途:将一个数的若干位取反;也可用于 将数若干位置0(当确定异或的两数的各位都相同时)或 将数若干位置1(当确定异或的两数的各位都相反时);
1)取反操作:
规律:-x=~x+1=~(x-1)。有趣但没啥用的推论:-(~n)、~(-n)分别表示 n+1、n-1 操作。
x-1 的含义:对于整数x,将 最后一个1及其右边的所有位 取反:x-1。如 24(011000)-1 -> 23(010111)
-x 的含义:对于整数x,将 最后一个1左边的所有位 取反:~(x-1),将上例按位取反即可(结合上面的规律可知,-x 或 ~x+1 也符合)。
总结:以这两个基本规律及前述与、或、异或运算的用途为基础,得到针对x的各种骚应用(有些在后面详细介绍,这里先总结):
x & (x-1):最后一个1及其后面的所有位置为0,即去掉最后一个1;体现了与运算的置0作用。
x & (-x):最后一个1之前的所有位置为0,即只保留最后一个1;体现了与运算的置0作用。
x | (x-1):最后一个1及其后面的所有位置为1;体现了或运算的置1作用。
x | (-x):最后一个1之前的所有位置为1;体现了或运算的置1作用。
x ^ (x-1):最后一个1及其后面的所有为置为1,左边的所有为置为0;体现了异或运算的置0和置1作用。
x ^ (-x):最后一个1及其后面的所有为置为0,左边的所有为置为1;体现了异或运算的置0和置1作用。可见 x ^ (-x) = ~( x ^ (x-1) )
2)去掉最后一个1:x&(x-1) 可以用来去掉整数x的二进制形式的最后一个1,其原理借助上面 x-1的含义可得。
以之为基础,有个妙用:判断一个正数x(x>0)是不是2的n次方:return (x&(x-1)==0)。
3)只保留最后一个1:将整数x的二进制形式的除最后一个1外其他位都置为0: x & (-x),如当x为6时候该式结果为2。其原理借助上面 -x 的含义可得。
以之为基础,有个妙用:判断一个正数x(x>0)是不是2的n次方:return (x&(-x)==x);
另外,数据结构“树状数组”中的lowbit函数就是这个
4)只保留最左边的1:将整数x二进制形式的除最左边一个1外其他位置都置为0:
原理:将最左边1的右边的所有位置都改为1得到新数x1,再由x1右移1位得到x2,则x1-x2即为所求。java代码示例:
当然,也可通过循环“去掉最后一个1”来实现
以之为基础,当a为非负数时,该结果即为不大于整数a的最大的2的整数次幂。
以之为基础,当a为非负数时,该结果左移1位即为不小于整数a的最小的2的整数次幂。
5) 整数移位规律:x >> -n = ~(x << n) , x << -n = ~(x >> n)。即无符号右移-n位等于无符号左移n位按位取反,反之同理。注意这里都是指无符号移位。
使用场景示例:创建一个最低5位为1的整数:-1的补码各位均为1,故直接 -1 >> -5 即可。更直觉的方法是左移5位后按位取反,即 ~(-1 << 5),可见上述规律是成立的。
Java JDK中EnumSet的实现类RegularEnumSet里迭代addAll、complement方法就用了此技巧。
6)除数是2的整数倍时的求余操作:a%b 等于 a &(b-1),注意只有当b为2的整数倍时才成立。
7)英文字母(a-z、A-Z)大小写转换:
大写转小写:与空格字符或运算,如 'a' | ' ' = 'a' , 'A' | ' ' = 'a'
小写转大写:与下划线字符与运算,如 'a' & '_' = 'A' , 'A' & '_' = 'A'
大小写转换:与空格异或运算,如 'a' ^ ' ' = 'A' , 'A' ^ ' ' = 'a'
由于小写字母的ascll码比大写的多32,故 “大写转小写时加32、小写转大小时减32” 即可。但由于大小写字母的ascll码及32的二进制形式的特殊性,可演化为如上位运算。
原因分析:
1、空格、下划线 的ascll 码及二进制分别为32(0010 0000)、95(0101 1111),前者二进制中除了第六位为1其他位均为0、后者则除了第六位为0其他位均为1
2、'A' ~ 'Z'、'a' ~ 'z' 的ascll码和二进制分别为:65(0100 0001) ~ 90(0101 1010)、97(0110 0001) ~ 122(0111 1010),所有ascll码的第八位均为0、所有大写字母的第六位都为0、所有小写字母的第六位都为1。
故:
大写转小写:“大写字母加(32)10” <=> “字母二进制形式的第6位置为1” <=> “字母二进制形式 | (10 0000)2 ” <=> “字母 | (0x20)16 ” <=> “字母 | 空格 ”
小写转大小:“小写字母减(32)10” <=> “字母二进制形式的第6位置为0” <=> “字母二进制形式 & (101 1111)2 ” <=> “字母 & (0x5f)16 ” <=> “字母 & 下划线 ”
字母反转(若是大写则转小写、若是小写则转大写):“字母二进制形式的第6位置为0则置1、为1则置0” <=> “字母二进制形式 ^ (0010 0000)2 ” <=> “字母 ^ (0x20)16 ” <=> “字母 ^ 空格 ”
实际上,大小写字母二进制只差1位不是巧合,是故意这样设计的,方便早期的电子设备编码转换。
8)判断两个整数是否异号:若a ^ b < 0则a、b异号。也可用积或商来判断,但开销更大且可能溢出。
9)无分支取绝对值:
10)两整数取均值,一般是(x+y)/2,但可能溢出。可用位运算代之,但注意,其和不能为负,否则结果有误:
11)异或运算的特点:
一个整数与0异或得原数:如 1 ^ 0 = 1 、0 ^ 0 = 0;
一个数与1异或得原数的反:如 1 ^ 1= 0 、0 ^ 1 = 1。
根据此两个特点,有:
一个数A与各bit全1的另一数B 异或 的结果等于 ~A 。如 1011 ^ 1111 = 0100;
要对一个数的若干位取反,只需要将数与该若干位为1的另一个数异或即可。如 要对 1011 的1、3位取反,只需要 1011 ^ 0101 =1110 即可。
应用示例,求一个有n bit的无符号整数能表示的最大值:-1 ^ ( -1 << n ) 或 ~( -1 << n ),即各bit全为1的数左移n位然后按位取反。
12) 获取程序运行所在处理器的位数: a = 32 << (^uint(0) >> 63) // 32 or 64
32、void qsort(void *base, size_t n,size_t size, int (*cmp)(const void *,const void *))
对base[n]进行排序,每个元素的大小siezeof(base[0])为size
33、(&a得到a类型的指针,指向a。如对于int a; &a得到 int *类型的指针)
把浮点数在计算机内部的表示当成整数对应的值
打印各种数据对象的字节表示:(long和指针占的字节数为机器字长,long long为8B)
34、以O(lgn)复杂的计算a^n
35
位运算求模:对于b%a ,当a为2^k的倍数时,b/a=b>>k、b%a=b&(a-1)移位运算:(假设b为正整数)
a << b 相当于a *2^b:左移操作(java中亦称为左移操作,即 << )
a >> b相当于a/(2^b);这里:
当a是无符号数时为逻辑右移,右移时a的左边补0;(java中称为无符号右移操作,即 >>> )
当a是有符号数时为算术右移,右移时左边补充1还是0根据a移动前最左边是1还是0而定。(java中称为右移操作,即 >> )
java中没有无符号数的概念,都是有符号数,其用 >>、>>>分别表示右移操作、无符号右移操作。后者在c中相当于"有符号数的逻辑右移"操作,可以通过把数强转成无符号数并逻辑右移完成,即对于整数a:java中的 a>>>3 相当于c中的 (unsigned int)a >>3
进程临界区互斥的实现:
使用:
进程同步(同步是一种更特殊的互斥,也就是生产者消费者问题)的实现:信号量
37、C/C++中没有逻辑右移符号“>>>”(Java有),对于右移只有符号“>>”,其既可表示算术右移又可表示逻辑右移。C标准没有明确定义应该使用哪种右移,但一般来说编译器/机器都对有符号数使用算术右移,对无符号数使用逻辑右移。如:
38、递归版整数转字符串(可以处理最大负数)
39、关于传值、传址
所谓传值、传址,是指调用函数时传入的实参是 变量的内容(或称变量的值) 还是 变量的位置(或称变量的地址)。对于前者,在被调用函数里是没法改变实参变量的值的,而后者则可以。
对Java来说,只有传值这一方式,因此无论如何都没有办法在被调用的函数里改变实参变量的值。
根据参数类型的不同,传的“值”的含义也不同,具体来说:
1、若参数是基本数据类型,则传的为变量的值。如int、long等
2、若参数是对象类型,则传的为引用(即对象的地址)。此时在被调用函数里虽然没法改变实参变量的值(即让该实参变量指向其他对象),但却可通过该引用改变实参变量指向的对象的内容。
对于String、基本数据类型包装类如Integer 等immutable类型,虽然也为对象类型,但因为其没有提供自身修改的函数,每次操作都是新生成一个对象,所以要特殊对待。可以认为是传值,因此没办法在函数里改变引用对象的内容。
示例:
Java对变量值的解析方式:
若变量是基本数据类型,则直接使用该值进行加减等操作;
若变量是对象类型,则把该值当做地址,需要一次据之进行一次寻址才能拿到要进行加减等操作的值。
对C来说,除了传值外,还有传址方式,因此能在被调用函数里改变实参变量的值。
1、若参数是基本数据类型,则为传值,如int、long等
2、若参数是指针类型,且实参是一个指针变量,则在被调用函数里无法改变实参即该变量的内容但可以改变该变量指向的对象的内容。这相当于上述Java里的引用情形。
特别地,若实参是形如 &a 这样取值运算符加变量的形式,则实参的值会被改变,此即传址。实际上,对变量取址就隐含得到了指向该变量的指针,所以函数里能够改变该指针指向的对象的内容即变量的值。
示例:如下所示,传指针变量a时调用前后指针变量a的内容不变但其指向的对象的内容变了;传&d2前后d2的内容变了。
函数调用时 传值 or 传址 的理解。
40、C99标准允许数组动态定义,即动态确定数组大小,如 int foo(int n) { int a[n]; return 1; } ,注意需要编译器支持。
41、关于C/C++中的内存分配:(全局变量、静态变量、局部变量等)
1、静态存储区:静态变量、常量、全局变量分配在静态存储区,这块内存在程序编译时就已经分配好,且在程序的整个运行期间都存在。(对于一个具体程序而言,包括data段和bss段)
2、栈区:函数(包括main函数)内部的变量及其存储单元分配在栈上(任何局部变量都处于栈区,如int a[]={1,2};,变量a及1、2都处于栈区),这块内存在运行时随着函数调用和调用结束而自动分配和释放。栈内存分配运算置于处理器的指令集中,效率很高,但可分配容量有限。
3、堆区:程序在运行时用malloc或new申请的内存(即动态内存分配)在堆区。这块内存由程序员负责申请并在适当时用free或delete释放,生存期完全由程序员决定,若没释放则在程序结束时自动释放。
(上述分配位置与Java中的方法区、栈、堆类比:Java中全局变量、静态变量、局部变量等都在栈区,变量指向的对象在堆区、动态分配的对象在堆区)
由此引申出char *a与char a[]的区别:(分配位置、读写能力、存取效率)
char *a="abc"; 中"abc"是常量,存放在静态存储区,因此内存大小在编译时就分配好,且通过指针a只能读而不能改变每个字符;
char a[]="abc"; 中"abc"存放在栈中,在运行时分配,可以通过指针a来读取和修改每个字符;由于存于栈上,效率比上者快。
(20240612补充)
C、Go等支持指针的语言,局部变量并不都是分配在栈上的:1 动态分配(malloc等)内存的局部变量自身位于栈,但指向的内存区域位于堆。这点无疑问。例如 int* p = (int*)malloc(sizeof(int));2 非动态分配的局部变量通常也在栈,但少数情况下却也可能指向堆!!!例如 func()*int{ a:=2; return &a; }
原因:在函数内将非动态创建的局部变量自身的地址返回给函数调用者使用,这种情况编译器会将该局部变量分配在堆上,以防止变量随函数栈帧的销毁而消亡。根源就在于支持指针。像Java不支持指针,就无法返回变量自身的地址故不存在上述这种“奇葩”特例。
42、Linux中C/C++程序的内存布局:(5段)
1、text段(代码段/正文段):存放程序的二进制执行代码,只读,在程序运行前即已确定,是共享的(如在父子进程中共享代码段)。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
2、data段(初始化数据段):存放已初始化的静态变量和已初始化全局变量,在程序运行前即已确定。
3、bss段(未初始化数据段):存放未初始化的静态变量和未初始化的全局变量,在程序运行前即已确定。
4、stack段(栈段)
5、heap段(堆段)
在Linux下可以用size命令看一个c/c++程序中前三段的大小,示例:
43、enum类型占用的内存空间大小由枚举值列表里的最大值确定。
1、浮点数的表示(三部分):
a)符号位S:1为负,0为正;
b)阶码E(非负,[0,255] 或 [0,2047]):等于指数x的移码-1,也即x+2n-1=x+127或x+1023。(移码即补码符号位取反,引入的目的是便于浮点数运算时的对阶操作)。
这里127或1023是偏移量,阶码为指数值加偏移量,故通过与偏移量比较就可知指数正负:若存储的值大于偏移量则意味着指数是正的,否则若存储的值小于偏移量则意味着指数是负的,否则若存储的值等于偏移量则意味着指数为0。
因此,一个规格化的32位浮点数的机器码对应的真值为:x=(-1)S× 2(E-127)× (1.M),一个规格化的64位浮点数的机器码对应的真值为:x=(-1)S× 2(E-1023)× (1.M)。可见,可表示的阶码范围为 [0,255] 或 [0, 2047];幂次范围为[-127,128]或[-1023, 1024];可表示的尾数范围为 [0,223-1]/223 或 [0,252-1]/252 。
如对于单精度(0.5)10=(0.1)2,符号位为0、指数为-1、规格化后的尾数为1.0。故阶码为-1+127=126=[0111 1110]2、尾数为[000...00]2,所以0.5=[0 01111110 000...00]2;
对于单精度的(2.25)10=(10.01)2,符号位为0、指数为1、规格化后尾数为1.001。故阶码为1+127=128=[1000 0000]2、尾数为[0010...00]2,所以2.25=[0 10000000 0010...00]2;
对于单精度的(-12.5)10=(-1100.1)2,符号位为1、指数为3、规格化后的尾数为1.1001。故阶码为3+127=130=[1000 0010]2、尾数为[10010...00]2,所以-12.5=[1 10000010 10010...00]2。验证: float a= -12.5f; int * pa=( int *)&a; printf("%x\n",*pa);//c1480000
2、浮点数的范围(下面以32位单精度浮点数为例,由于x=(-1)S× 2(E-127)× (1.M),且阶码E范围为[0,28-1]、尾数M范围为[0,223-1]/223):
a)特殊数的表示:从上述机器码对应的真值可以看出,IEEE754标准无法精确表示0。因此,IEEE754标准规定阶码为0或255的特殊情况来表示这些数:
若阶码E=0且尾数M=0,则规定此数表示±0(正负号和数符位有关),即+0的机器码为0 00...00,-0的机器码为1 00...00。故计算机内部无法表示精确的浮点数0,其实际值分别为1.0*2-127、-1.0*2-127。
若阶码E=255且尾数M=0,则规定此数表示±无穷。即正无穷的机器码为0 11111111 00...00,负无穷的机器码为1 11111111 00...00。故正负无穷的实际值分别为1.0*2128、-1.0*2128。
若阶码E=255且尾数M!=0,则这不是一个数(NaN)。对负数开根号等非法操作称为浮点数异常(floating-point exception)操作,其结果用NaN表示。
b)范围(除去E为0和255这两种特殊情况):
最大正数:符号位S=0,阶码E=254,尾数M=11..11,即机器码为0 11111110 11...11,故浮点数最大正数为(1.11...11)2*2127≈3.402832e+38;
最小正数:符号位S=0,阶码E=1,尾数M=00..00,即机器码为0 00000001 00...00,故浮点数最大正数为(1.0)2*2-126≈1.175494e-38;
最小负数、最大负数与最大正数、最小正数对应,只是符号位S=1。即最小负数、最大负数分别为-3.402832e+38、-1.175494e-38;
3、浮点数的精度
浮点数的精度是指浮点数的小数位所能表达的位数。
以32位浮点数为例,尾数域有23位。那么浮点数以二进制表示的话精度是23位,23位所能表示的最大数是223−1=8388607,所以十进制的尾数部分最大数值是8388607,也就是说尾数数值超过这个值,float将无法精确表示,所以float最多能表示小数点后7位,但绝对能保证的为6位,也即float的十进制的精度为为6位。
64位双精度浮点数的尾数域52位,因252−1=4,503,599,627,370,495,所以双精度浮点数的十进制的精度最高为16位,绝对保证的为15位,所以double的十进制的精度为15位。
注意:精度为6或15位,并不表示小数长度小于该数的都可以精确用单精度或双精度浮点数表示,因为并不是所有的小数都可以表示为有限个2的负整数次幂的和。故浮点数的判等通过等号或差值是否小于阈值来进行也可。
浮点数的表示范围由阶码(用整数形式表示,阶码指明小数点在数据中的位置,决定了浮点数的表示范围)确定、精度由尾数(用定点小数形式表示,尾数部分给出有效数字的位数,决定了浮点数的表示精度)确定。
为什么浮点数有精度问题而整数没有?简单理解,计算机内部用来表示数的二进制位是有限的,而整数类型下的数据个数是有限的而浮点数类型下的数据个数是无限的,用有限位数表示无限个数显然不可能。
45、关于传值还传址的通用理解(不限语言)
变量的本质:一个变量本质上是一个可以装东西(数据)的盒子,这里的“盒子”具体而言就是内存里的若干个连续的字节块。它有几个属性:
1,盒子位置就是变量地址。盒子自身有个位置信息,称为“地址”,可通过地址找到盒子。
2,盒子名字就是变量名
3,盒子装的东西
的值就是变量值
的类型(也即数据的类型)就是变量类型,即变量类型就是盒子中东西的类型。注意这里的“东西”是指直接放在盒子里的数据。例如 int a 、 int* a Person a 三者分别表示盒子a中的数据的类型是int、int *、Person。变量类型(与数据类型对应)可归纳为三类:
4.1,基本数据类型(如int、char等,不同语言下大同小异)的变量。盒子里放的就是数据本身。如 int a=2 说明盒子a里放的是数字2。
4.2,复杂数据类型的变量,包括:
3.2.1,存放变量位置的变量,如C、C++语言中的指针和数组变量(注,Java里没有这种类型的变量,即使Java中的数组也不属于这种而是属于下面介绍的那种),盒子里放的是另一个变量的位置。以C语言为例,如 int* b=&a; 中盒子b里放的是盒子a的位置值、 int c[]={1,2}; int* d=c; 中盒子d里放的是c的位置值。注意C、C++中数组变量存的是数组首元素的地址,故例子中d赋值时不是 int* d=&c;
3.2.2,存放对象位置的变量,如C中的结构体和联合、Java中除基本数据原子类型外的其他类型如数组类型、引用类型(如String类型)等。盒子里放的是对象的位置。以Java为例,如 Personn p=new Person() 、 String name="zhangsan" 、 int[] ages={22,23} 中盒子里放的是复杂对象的位置。
注:
4,盒子占用内存的大小就是变量大小,它取决于变量类型:复杂数据类型的变量例如指针变量占用的内存大小为内存地址空间表示的长度,即4B或8B(32位或64位),其他类型跟语言的定义有关例如C里int为4B。变量类型决定了往各自里放/读东西时写/读的字节数。
从上面我们知道了变量的本质和变量类型后,关于传值及传址的区别就好理解了:
1,发生的场景:变量赋值或函数调用时的传参,后者实际上也是赋值。
2,区别:传值是直接将源变量盒子的内容复制到目标变量盒子里;传址是将源变量的位置值复制到目标变量盒子里。
总结:
1,在赋值时应保证源值的类型与目标变量类型兼容,可见,要么一致(如int值赋给int变量;上述C语言中c直接赋给d;还有类型强制转换等),要么是子类型(如int值赋给long)。
2,从上述区别可看出,某种程度上可认为是“存放变量位置的变量”这种变量的存在使得产生了“传址”方式。故对具体语言而言,根据是否有这种变量,可以知道C语言中两种方式都有,Java中只有传值没有传址方式。
46、进制转换
十进制和n进制间的相互转换,这里n进制可以包括整数、小数部分,如十进制的10.32、二进制的101.011。
1 n进制数转为十进制:各位上有权重,各位上的值乘以该位的权重,然后求和即可。如 (101.011)2 = (1*22+0*21+1*20 + 0*2-1+1*2-2+1*2-3)10 = 5.375。
2 十进制转为n进制:(为啥是下面这样算?看上面第二个式子转为第一个式子的过程,想象n进制转为十进制是怎么转的,整数、小数都是位权乘以位值结果求和。)
十进制整数部分:“除以n取余数,直到商为0,余数逆序排列” 即为结果。因为整数部分提公因子n,剩下的就是n进制整数部分的最低位。
十进制小数部分:"乘以n取整数,直到积为0,整数顺序排列"即为结果。因为小数部分提公因子n-1,剩下的就是n进制小数部分的最高位。由于小数部分可能无法由若干个2的负整数次幂求和精确表示,故可能无法达到积为0的停止条件,此时视精度要求看算几位即可。