1,volatile
关键字volatile有什么含意 ? 并举出三个不同的例子?
并行设备的硬件寄存器
存储器映射的硬件寄存器通常加volatile,因为寄存器随时可以被外设硬件修改。当声明指向设备寄存器的指针时一定要用volatile,它会告诉编译器不要对存储在这个地址的数据进行假设。
一个中断服务程序中修改的供其他程序检测的变量
volatile提醒编译器,它后面所定义的变量随时都有可能改变。因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
多线程应用中被几个任务共享的变量
简单地说就是防止编译器对代码进行优化.比如如下程序
XBYTE[2]=0x55:
XBYTE[2]=0x56:
XBYTE[2]=0x57;
XBYTE[2]=0x58;
对外部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作,但是编译器却会对上述四条语句进行优化,认为只有XBYTE[2]=0x58( 即忽略前三条语句,只产生一条机器代码)。
如果键入volatile,编译器会逐一的进行编译并产生相应的机器代码(产生四条代码)。
2,关键字static的作用是什么 ?
2.1 修饰局部变量
结论:static修饰局部变量只会被初始化一次并且改变了变量的生命周期,让静态局部变量出了作用域依然存在,到程序结束, 生命周期才结束。
在C语言中,为什么 static变量只初始化一次 ?
对于所有的对象( 不仅仅是静态对象 ),初始化都只有一次,而由于静态变量具有"记忆"功能,初始化后,一直都没有被销毁,都会保存在内存区域中,所以不会再次初始化。存放在静态区的变量的生命周期一般比较长,它与整个程序"同生死、共存亡”,所以它只需初始化一次。而auto查量,即自动变量由于它存放在栈区,一旦函数调用结束,就会立刻被销毁
void test()
{
int j = 0;
static int i = 0; // 修饰局部变量
i++;
j++;
printf("i[%d]j[%d]\t", i, j);
}
int main()
{
int i = 0;
for (i = 0; i < 3; i++)
{
test();
}
return 0;
}
// 输出
i[1]j[1] i[2]j[1] i[3]j[1]
2.2 static 修饰全局变量/函数
结论:一个全局变量/函数被static修饰,变量/函数只能在自己所在的源文件内部使用,生命周期不变。
static修饰全局变量与函数作用相同,因为未加static时全局变量与函数都具有外部链接属性,使他们在其他源文件内用extern声明后即可使用。
使用static后,全局变量与函数的外部链接属性->内部链接属性,让他们无视了extern的作用,只能在他们所在的源文件内使用。
3,sizeof 简单实现
#define mySizeof(Value) ((char *)(&Value + 1) - (char *)(&Value))
int main()
{
int a;
char b;
char *c;
double d;
int *e;
printf("[%d][%d][%d][%d][%d]\n", mySizeof(a), mySizeof(b), mySizeof(c), mySizeof(d), mySizeof(e));
printf("[%d][%d][%d][%d][%d]\n", sizeof(a), sizeof(b), sizeof(c), sizeof(d), sizeof(e));
printf("====================\n");
return 0;
}
输出:
[4][1][8][8][8]
[4][1][8][8][8]
4,C 内存分配的三种方式
4.1 从静态存储区域分配。
内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
4.2 在栈上创建。
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。
栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
4.3 从堆上分配,
亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。
动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
5,堆与栈有什么区别 ?
5.1.申请方式
栈的空间由操作系统自动分配/释放,堆上的空间手动分配/释放
5.2.申请大小的限制
栈空间有限。
在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是 一个编译时就确定的常数 ),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小
堆是很大的自由存储区。
堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
5.3.申请效率
栈由系统自动分配,速度较快。但程序员是无法控制的
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便
栈的分析
栈是数据结构一个非常特殊的数据类型
栈作为一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据,最后一个数据被第一个读出来。栈具有记忆作用,对栈的插入与删除操作中,不需要改变栈底指针。
栈是允许在同一端进行插入和删除操作的特殊线性表。允许进行插入和删除操作的一端称为栈顶,另一端为栈底;栈底固定,而栈顶浮动;栈中元素个数为零时称为空栈。插入一般称为进栈,删除则称为退栈。栈也称为后进先出表。
6,C语言函数压栈方向
自右向左
为何要自右向左,是为了支持C语言的不定参数,否则左右都可以
首先,在函数调用过程中,最先入栈的是调用函数调用处的下一条指令的地址,这样调用完成后返回到该地址继续执行,这个地址是很重要的,然后才是函数的入参,函数的内部局部变量。调用结束,将栈内容一一出栈,最后栈顶指向了开始存储的指令地址,主函数继续从这里开始还行。
其次,函数调用入参的入栈顺序和C语言支持变长参数有关,比如printf函数就支持变长参数,也即void printf(const char *fmt,……),这个时候并不知道参数有多少个,如果从左向右入栈,那么fmt就在栈底,该参数的位置无法通过栈顶指针偏移来确定,因为不知道栈顶和栈底之间有多少个参数,大小是多少,无法确定,但是,如果从右向左入栈的话,fmt参数就在栈顶的位置,通过这个固定的普通参数就可以通过偏移逐一寻址到后续参数的地址。
7,野指针、悬空指针是什么 ?
7.1.野指针是指向不可用内存的指针,当指针被创建时,指针不可能自动指向NULL,这时,默认值是随机的,此时的指针成为野指针。
7.2.当指针被free或delete释放掉时,如果没有把指针设置为NULL,则会产生悬空指针,因为释放掉的仅仅是指针指向的内存,并没有把指针本身释放掉。
7.3.第三个造成野指针的原因是指针操作超越了变量的作用范围。
#include <iostream>
using namespace std;
int main()
{
int *p; // 野指针
int *q = NULL; // 非野指针
p = new int(5); // p 现在不再是野指针
q = new int(10);
cout<<"*p = "<<*p<<endl;
cout<<"*q = "<<*q<<endl;
free(p); // p 在释放后成为悬空指针
p = NULL; // 非悬空指针
free(q);
return 0;
}
8,#define 和 const 使用建议
两者之间的区别
- define在预处理阶段进行替换,const常量在编译阶段使用;
- define不做类型检查,只进行替换,const常量有数据类型,会执行类型检查;
- define不能调试,const常量可以调试;
- define定义的常量在替换后运行过程中,会不断占用内存,而const定义的常量存储在数据段,只有一份拷贝,效率更高;
- define可以定义函数,const不可以。
9,位操作
位操作运算优先级低于算数运算,所以写代码谨记位运算加括号
c语言中存在6个位操作运算符,且它们只能用于整形操作数。
位操作符 | 名称 |
& | 按位与 |
| | 按位或 |
^ | 按位异或 |
<< | 按位左移 |
>> | 按位右移 |
~ | 按位取反 |
"&"、" |"、"^"真值表,与顺序无关,所以只用展示三种情况的运算
位1 | 位2 | & | | | ^ |
0 | 1 | 0 | 1 | 1 |
0 | 0 | 0 | 0 | 0 |
1 | 1 | 1 | 1 | 0 |
位运算常用操作片段
1、位基础操作
清零 & (与)零, 置位 |(或)一
================================================================
2、 对称加密
其实现原理是一个数异或同一个数两次还是原数
================================================================
3、乘除幂运算
//计算n*2
int mulTwo(int n) {
return n << 1;
}
//除以2,负奇数的运算不可用
int divTwo(int n) {
return n >> 1;//除以2
}
//计算n*(2^m),即乘以2的m次方
int mulTwoPower(int n,int m) {
return n << m;
}
//计算n/(2^m),即除以2的m次方
int divTwoPower(int n,int m) {
return n >> m;
}
================================================================
4、奇偶判断
//判断数值的奇偶性
boolean isOddNumber(int n){
return (n & 1) == 1;
}
================================================================
5、不增加新变量的情况下交换两个变量的值
//常规交换方式
int temp;
temp = a;
a = b;
b = temp;
//基于位运算的方式
a = a ^ b;
b = a ^ b; //实际上是(a^b)^b,也就是a异或了b两次,等号右边是a的值。
a = a ^ b;
================================================================
6、十进制转十六进制
public static String decimalToHex(int decimal) {
String hex = "";
while (decimal != 0) {
int hexValue = decimal % 16;
hex = toHexChar(hexValue) + hex;
decimal = decimal / 16;
}
return hex;
}
//将0~15的十进制数转换成0~F的十六进制数
public static char toHexChar(int hexValue) {
if(hexValue <= 9 && hexValue >= 0) {
return (char)(hexValue + '0');
} else {
return (char)(hexValue - 10 + 'A');
}
}
================================================================
7、求平均数
求平均值,比如有两个int类型变量x、y,首先要求x+y的和,再除以2,但是有可能x+y的结果会超过int的最大表示范围,所以位运算就派上用场啦。
(x&y)+((x^y)>>1);
================================================================
8、32位数获取单字节
#define GET_LOW_BYTE0(x) ((x >> 0) & 0x000000ff) /* 获取第0个字节 */
#define GET_LOW_BYTE1(x) ((x >> 8) & 0x000000ff) /* 获取第1个字节 */
#define GET_LOW_BYTE2(x) ((x >> 16) & 0x000000ff) /* 获取第2个字节 */
#define GET_LOW_BYTE3(x) ((x >> 24) & 0x000000ff) /* 获取第3个字节 */
================================================================
9、清零某个字节
#define CLEAR_LOW_BYTE0(x) (x &= 0xffffff00) /* 清零第0个字节 */
#define CLEAR_LOW_BYTE1(x) (x &= 0xffff00ff) /* 清零第1个字节 */
#define CLEAR_LOW_BYTE2(x) (x &= 0xff00ffff) /* 清零第2个字节 */
#define CLEAR_LOW_BYTE3(x) (x &= 0x00ffffff) /* 清零第3个字节 */
================================================================
10、获取int最大最小值
int getMaxInt()
{
return (1 << 31) - 1;//2147483647, 由于优先级关系,括号不可省略
}
int getMinInt()
{
return 1 << 31;//-2147483648
}
================================================================
11、pow()方法实现
//自己重写的pow()方法
int pow(int m , int n){
int sum = 1;
while(n != 0){
if(n & 1 == 1){
sum *= m;
}
m *= m;
n = n >> 1;
}
return sum;
}
================================================================
12、位图操作
#define clr_bit(x, n) ( (x) &= ~(1 << (n)) ) // 某位清零
#define set_bit(x, n) ( (x) |= (1 << (n)) ) // 某位置1
#define get_bit(x, n) ( ((x)>>(n)) & 1 ) // 获得某位值
=================================================================
13、置位
置位特定位n:
#define SET_NTH_BIT(x, n) ( x | ((1U)<<(n)) )
置位n到m位:
#define SET_BIT_N_TO_M(x,n,m)(x | (~((~0U)<<(m-n+1)))<<(n))
分析:
第一步:((~0U)<<(m-n+1) ) 产生(m-n)到31 位为 1 的数
第二步:(~((~0U)<<(m-n+1)))产生0~m-n位为1的数
第三步:(~((~0U)<<(m-n+1)))<<(n) 产生
第四步:采用位或,将特定位置为1
=================================================================
14、复位
复位特定位:
#define CLEAR_NTH_BIT(x, n) ( x & ~((1U)<<(n)) )
复位n到m位:
#define CLEAR_BIT_N_TO_M(x,n,m) (x & ((~0U)<<(m-n+1))<<(n))
=================================================================
15、截取变量的部分连续位
#define GETBITS(x, n, m) ((x & ~(~(0U)<<(m-n+1))<<(n)) >> (n) )
=================================================================
16、给定一个整数a , 设置a的bit3 , 保证其他位不变。
int a = 0xAB; // 假设a的初始值是0xAB(二进制为10101011)
int mask = 1 << 3; // 构造掩码,设置第4位为1(二进制为00001000)
a = a | mask; // 使用按位或运算设置bit3
这种设定某位或者清零某位的值都可以分为两步,1构造掩码、2与原数进行位运算
=================================================================
下面是一些未整理的例子
2. 给定一个整数a , 设置a的bit3~bit7 , 保证其他位不变。
a |= (0x1f << 3)
3. 给定一个整数a , 清除a的bit3 , 保证其他位不变。
a &= (~(1 << 3)) ;
4. 给定一个整数a , 清除a的bit3~bit7 , 保证其他位不变。
a &= (~(0x1f << 3))
5. 给定一个整数a , 取出a的bit3~bit7 。
思想:
(1)将其他位清零
(2)右移3位
代码:
a = (a & (0x1f << 3)) >> 3 ;
6. 给寄存器a的bit3~bit7赋值12
思想:
(1)将bit3~bit7清零
(2)将12左移3位 ,写入a
代码:
a = (a & ~(0x1f << 3)) | (12 << 3) ;
7. 给寄存器a的bit3~bit7加上12
思想:
(1)先取出bit3~bit7
(2)将取出的数加上12
(3)将a的bit3~bit7 清零
(4)将取出的数写入a
代码:
tmp = a&0x1f << 3) >> 3 ;
tmp += 12 ;
a = a & ~(0x1f << 3) ;
a |= tmp << 3 ;
8. 给寄存器a的bit3~bit7 赋值4和 bit 8 ~ bit 12 赋值7
写法一:(只是简单的重复赋值运算)
a = (a & (~0x1f << 3)) | (4 << 3) ;
a = (a & (~0x1f << 8)) | (7 << 8) ;
写法二:
a &= ~((0x1f << 3) | (0x1f << 8)) ;
10,链表
10.1、给你一个单向链表,实现一个算法,判断这个链表中是否存在环;
/*
设置两个速度不同的指针同时从链表的第一个节点开始遍历链表,一个快指针 fp 每次移动两个节点,
一个慢指针sp每次移动一个节点,若两个指针能相遇,则存在环
fast = fast->next - next, slow = slow->next;
*/
bool IsExitsLoop(llink head)
{
llink slow = head, fast = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
break;
}
return !(fast == NULL || fast->next == NULL);
}
10.2、如果存在环,返回环的起始结点;
10.3、如果这个时候你给出的实现是用的快慢指针,而且步长是2)步长选择3,4,5,或者其它的可以解决以上两个问题吗?如果也可以,那么为什么你选择的步长是2?
计算:
慢指针步长为1
快指针的步长为b
已经走的步数x
环长为len
步长差 = x*b-x*1= x*(b-1)
解决思路:进入环之后可以把环看做一个无限长的单链表,当快慢指针步长差为环长度的整数倍时既相遇
x*(b-1) % len == 0
那么这里的关键在于b的取值,事实上,len的长度我们是不知道的,而x的步数实际上会因为b的取值随之变化,所以我们不用管x带来的影响。
所以,当b为2时,即b-1为1时,无论len为多少都会产生一个对应的x值保证x*(b-1)为len的整数倍。
那么如果b为3的时候,b-1为2,首先,len的长度至少要为偶数才能保证为len的整数倍
10.4、求有环单链表的环长
在环上相遇后,记录第一次相遇点为Pos,之后指针slow继续每次走1步,fast每次走2步。在下次相遇的时候fast比slow正好又多走了一圈,也就是多走的距离等于环长。
设从第一次相遇到第二次相遇,设slow走了len步,则fast走了2*len步,相遇时多走了一圈:
10.5、列表反转
借助3个指针 beg、mid、end,遍历整个链表
1)mid的指针指向beg
2)三个指针整体后移一位
3)退出条件 end == NULL
/*
迭代反转链表
该算法的实现思想非常直接,就是从当前链表的首元节点开始,一直遍历至链表的最后一个节点,这期间会逐个改变所遍历到的节点的指针域,另其指向前一个节点。
迭代反转法,head 为无头节点链表的头指针
*/
llink iteration_reverse(llink head)
{
if(!head || !head->next)
{
return head;
}
else
{
llink begin = NULL;
llink mind = head;
llink end = head->next;
while(1)
{
// 反转指针域
mind->next = begin;
// 反转结束
if(NULL == end)
{
break;
}
// 三个指针整体向后移动一个节点
begin = mind;
mind = end;
end = end->next;
}
return mind;
}
}
11,C语言中函数参数入栈的顺序
C程序栈底为高地址,栈顶为低地址
参数入栈顺序是和具体编译器实现相关的,C语言要选择从右至左
C方式参数入栈顺序(从右至左)的好处就是可以动态变化参数个数。通过栈堆分析可知,自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。
因此,C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参数形式。换句话说,如果不支持这个特色,C语言完全和Pascal一样,采用自左向右的参数入栈方式。