目录
循环越界
(1)char类型边界处理
#include <stdio.h>
int main()
{
char a = 127;
printf("%d\n", a);
printf("%d\n", ++a);
a = 127;
printf("%d\n", a + 1);
}
问:输出结果是?
答案:127 -128 128
解析:char的类型是有符号的范围是-128~127(1000 0000~1111 1111)。由于++a是通过a整型提升之后计算的++a再截断放入char类型中的故++a之后变为了-128,而a+1是没有截断放入char a中,并以%d 输出的。用下图演示。计算机运算是通过补码来计算的,127+1 = -128原因如下。
(2)数组越界访问到循环变量
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
问:该题结果是什么?
答案:死循环打印hehe
解析:越界访问
局部变量在内存中创建是由高地址到地址值创建的,而数组的下标的增长在内存中是低到高增长的。那么i在内存中就会在数组的上方。如果i和arr数组中间留的空间合适的话(vs2019空2个)那么数组越界访问就可能访问到i。
那么对i的赋值0就会导致数组进入死循环。(vc6.0没空,gcc空1个,1个空间为4个字节)
2️⃣ 宏
(1)(偏移量计算)offsetof实现
offsetof() 宏返回结构或联合复合中元素名称的偏移量。这提供了一种可移植的方法来确定偏移量。偏移量是成员距结构体或联合体起始位置的距离。
函数的声明:
size_t offsetof(type, member);
#define Offsetof(s,m) ((size_t)&(((s*)0)->m))
typedef struct {
double a;
char str[10];
int b;
char c;
}S;
//验证
int main() {
int a = 0;
S* A = (S*)a;//把0作为起始地址
printf("%d\n", (size_t) & (A->b));//结构体从地址0开始, &(A->b)为b的地址
printf("%d\n", (size_t) & (A->c));
printf("%d\n", Offsetof(S, b));
printf("%d\n", Offsetof(S, c));
return 0;
}
解析:((size_t)&(((s*)0)->m)),首先设类型s结构体,成员为m,对0强转为s*结构体地址,,那么0就是一个结构体的地址,对其指向的成员m取地址就是&(((s*)0)->m)),由于起始位置是0,那么其成员的地址就是偏移量,强转为整型即可。
需要注意的是空结构体也是有地址的,虽然不是具体变量,但是结构体成员的建立就已经形成了各自的偏移量了(参照陈皓老师——C语言结构体里的成员数组和指针https://coolshell.cn/articles/11377.html)
(2)交换整数二进制奇偶位
题目:把一个整型的二进制奇数偶数位交换
例子:10:00000000 00000000 00000000 00001010
变为: 5: 00000000 00000000 00000000 00000101
#define SWAP(x) ( ((x & (0x55555555))<<1) | ((x & (0xAAAAAAAA))>>1))
#include <stdio.h>
int main()
{
int x = 0;
printf("输入一个数:");
scanf("%d", &x);
printf("%d\n", SWAP(x));
return 0;
}
解析:55555555:01010101 01010101 01010101 01010101
AAAAAAAA:10101010 10101010 10101010 10101010
刚好对应奇数偶数位,利用X与55555555按位与即可得到奇数位,同理与AAAAAAAA按位与得到偶数位,分别左移右移一位即可达到交换的目的,再通过按位或结合即可
3️⃣ 内存对齐
题目一:结构体
#pragma pack(4)/*编译选项,表示4字节对齐 平台:VS2013。语言:C语言*/
int main(int argc, char* argv[])
{
struct tagTest1
{
short a;
char d;
long b;
long c; //4个字节
};
struct tagTest2
{
long b;
short c;
char d;
long a;
};
struct tagTest3
{
short c;
long b;
char d;
long a;
};
A.12 12 16
B.11 11 11
C.12 11 16
D.11 11 16
答案:A
解析:默认对齐数是4,成员对齐数是自身大小和默认对齐数间小的那一个,故long的对齐数是4,故tagTest1是0,1(short),2(char),3(空),4,5,6,7(long),8,9,10,11(long)共十二个。剩下两个类似。
题目二:位段
struct A
{
unsigned a : 19;
unsigned b : 11;
unsigned c : 4;
unsigned d : 29;
char index;
}
则sizeof(A)的值为
A 9
B 12
C 16
D 20
答案:C (限于vs环境下,因为位段多余的空间是利用还是舍弃没有标准,vs为舍弃)
解析:
struct A { unsigned a : 19; //19bit ,开辟一个unsigned(32bit),剩13bit unsigned b : 11; //13-11 ,剩下2bit unsigned c : 4; //2bit 不够,舍弃,开辟一个unsigned(32bit),剩28bit unsigned d : 29; //28bit ,不够,舍弃,开辟一个unsigned(32bit),剩3bit char index; //3bit 不够,舍弃,开辟一个char(8bit) } //最后开辟了13个字节,但是要内存对齐,最大对齐数是4,补齐为16个字节
4️⃣ 位运算
题目一:不创建变量交换两值
int main()
{
int a = 10,b = 20;
a = a^b;
b = a^b;
a = a^b;
return 0;
}
解析:这道是热身菜,比较简单,(a^b^b = a);原理是0异或任何整数都为该整数。(a^b^a = b);原理自己和自己异或都为0;相同为0,相异为1
题目二:位运算实现加法
int add(int num1,int num2)
{
while(num2!=0)
{
int c = (num1 & num2)<<1;
num1^=num2;
num2 = c;
}
return num1;
}
解析:利用一点计算机组成的知识,num1&num2得到进位的数,作为进位移到左移一位,num1^num2算出当前位,保留在num1中,当num2为0的时候,说明上一次的c进位是0,也就是num1和num2没有都是1的二进制位了,不需要进位也就计算完毕。
5️⃣ 指针与数组
题目一:
int main()
{
int aa[2][5] = {10,9,8,7,6,5,4,3,2,1};
int *ptr1 = (int *)(&aa + 1);
int *ptr2 = (int *)(*(aa + 1));
printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
A.1, 6
B.10, 5
C.10, 1
D.1, 5
答案:A.1,6
解析:&aa的类型是int (*)[2][5],加一操作会导致跳转一个int [2][5]的长度,直接跑到刚好越界的位置。减一以后回到最后一个位置1处。*(aa + 1)相当于aa[1],也就是第二行的首地址,自然是5的位置。减一以后由于多维数组空间的连续性,会回到上一行末尾的6处。故选A。
题目二
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf("%p, %d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
答案:FFFFFFFC , -4
解析:数组的列数不一样,两者地址相差4,故结果为-4,而%p输出为16进制地址,-4化为32位二进制,转16进制为FFFFFFFC。
6️⃣ sizeof和strlen
要点:
1. sizeof(数组名),这里的数组名是表示整个数组的,计算的是整个数组的大小,单位是字节。
2. &数组名,这里的数组名也表示整个数组,取出的是数组的地址
除上面2中特殊情况外,所有的数组名都是数组首元素的地址
sizeof和strlen区别
sizeof(1)只关注占用空间的大小,单位是字节
(2)不关注类型
(3)是操作符
strlen(1)关注的字符串中\0的为止,计算的是\0之前出现了多少个字符
(2)只针对字符串
(3)库函数
情况一:整型数组的sizeof
int main()
{
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));//数组名单独在sizeof里面是整个数组大小,4*4 = 16
printf("%d\n", sizeof(a + 0));//数组名没有单独放在sizeof里面或&啊,为首元素地址,地址+0还是地址,4/8;
printf("%d\n", sizeof(*a));//数组名不是单独放的,解引用为1,是int故是4
printf("%d\n", sizeof(a + 1));//类比二,这个是int 2的地址,4/8
printf("%d\n", sizeof(a[1]));//a[1]是2,整形是4个字节
printf("%d\n", sizeof(&a)); //整个数组的地址,是个数组指针,还是指针4/8
printf("%d\n", sizeof(*&a));//变成了a,就是单独放了,16
//&a -> int(*)[4]
//&a是数组的地址,它的类型是int(*)[4]数组指针,如果解引用,访问的就是4个int的数组,大小是16个字节
printf("%d\n", sizeof(&a + 1));//&a是数组的地址,&a+1 跳过整个数组后的地址,是地址就是4/8
printf("%d\n", sizeof(&a[0]));//&a[0]取出数组第一个元素的地址,是地址就是4/8
printf("%d\n", sizeof(&a[0] + 1));//&a[0]+1就是第二个元素的地址,是地址大小就是4/8个字节
return 0;
情况二:字符数组
#include <string.h>
int main()
{
字符数组
char arr[] = { 'a','b','c','d','e','f' };//注意这里不是sizeof,只有单独放在sizeof内部才是整个数组
printf("%d\n", strlen(arr));//arr是首元素的地址,但是arr数组中没有\0,计算的时候就不知道什么时候停止,结果是:随机值
printf("%d\n", strlen(arr + 0));//arr是首元素的地址,arr+0还是首元素的地址,结果是:随机值
printf("%d\n", strlen(*arr)); //err,strlen需要的是一个地址,从这个地址开始向后找字符,直到\0,统计字符的个数。
但是*arr是数组的首元素,也就是'a',这是传给strlen的就是'a'的ascii码值97,strlen函数会把97作为起始地址,统计字符串,会形成内存访问冲突
printf("%d\n", strlen(arr[1]));//err 和上一个一样,内存访问冲突
printf("%d\n", strlen(&arr));//&arr是arr数组的地址,虽然类型和strlen的参数类型有所差异,但是传参过去后,还是从第一个字符的
位置向后数字符,结果还是随机值。
printf("%d\n", strlen(&arr + 1));//随机值
printf("%d\n", strlen(&arr[0] + 1));//随机值
printf("%d\n", sizeof(arr));//arr作为数组名单独放在sizeof内部,计算的整个数组的大小,单位是字节,6
printf("%d\n", sizeof(arr + 0));//arr就是首元素的地址,arr+0还是首元素的地址,地址大小就是4/8
printf("%d\n", sizeof(*arr));//arr就是首元素的地址,*arr就是首元素,是一个字符,大小是一个字节,1
printf("%d\n", sizeof(arr[1]));//arr[1]就是数组的第二个元素,是一个字符,大小是1个字节
printf("%d\n", sizeof(&arr));//&arr取出的是数组的地址,数组的地址也是地址,地址就是4/8个字节
printf("%d\n", sizeof(&arr + 1));//&arr取出的是数组的地址,&arr+1,跳过了整个数组,&arr+1还是地址,地址就是4/8个字节
printf("%d\n", sizeof(&arr[0] + 1));//&arr[0]是第一个元素的地址,&arr[0]+1就是第二个元素的地址,地址就是4/8个字节
return 0;
}
情况三:字符指针
int main()
{
字符数组
char arr[] = { 'a','b','c','d','e','f' };//注意这里不是sizeof,只有单独放在sizeof内部才是整个数组
printf("%d\n", strlen(arr));//arr是首元素的地址,但是arr数组中没有\0,计算的时候就不知道什么时候停止,结果是:随机值
printf("%d\n", strlen(arr + 0));//arr是首元素的地址,arr+0还是首元素的地址,结果是:随机值
printf("%d\n", strlen(*arr)); //err,strlen需要的是一个地址,从这个地址开始向后找字符,直到\0,统计字符的个数。
但是*arr是数组的首元素,也就是'a',这是传给strlen的就是'a'的ascii码值97,strlen函数会把97作为起始地址,统计字符串,会形成内存访问冲突
printf("%d\n", strlen(arr[1]));//err 和上一个一样,内存访问冲突
printf("%d\n", strlen(&arr));//&arr是arr数组的地址,虽然类型和strlen的参数类型有所差异,但是传参过去后,还是从第一个字符的
位置向后数字符,结果还是随机值。
printf("%d\n", strlen(&arr + 1));//随机值
printf("%d\n", strlen(&arr[0] + 1));//随机值
printf("%d\n", sizeof(arr));//arr作为数组名单独放在sizeof内部,计算的整个数组的大小,单位是字节,6
printf("%d\n", sizeof(arr + 0));//arr就是首元素的地址,arr+0还是首元素的地址,地址大小就是4/8
printf("%d\n", sizeof(*arr));//arr就是首元素的地址,*arr就是首元素,是一个字符,大小是一个字节,1
printf("%d\n", sizeof(arr[1]));//arr[1]就是数组的第二个元素,是一个字符,大小是1个字节
printf("%d\n", sizeof(&arr));//&arr取出的是数组的地址,数组的地址也是地址,地址就是4/8个字节
printf("%d\n", sizeof(&arr + 1));//&arr取出的是数组的地址,&arr+1,跳过了整个数组,&arr+1还是地址,地址就是4/8个字节
printf("%d\n", sizeof(&arr[0] + 1));//&arr[0]是第一个元素的地址,&arr[0]+1就是第二个元素的地址,地址就是4/8个字节
return 0;
}
题目到这里就结束了,如果你对以上操作熟练于心,说明你的c语言基础非常扎实,要是觉得我写的还行的话,希望你能点赞,关注,收藏 一件三连哦。要是有问题需要探讨,可以评论区留言。我看到将第一时间回复( •̀ ω •́ )✧