这些C题目你都会了吗?C语言题集

目录

1️⃣ 循环越界

2️⃣ 宏

3️⃣ 内存对齐

4️⃣ 位运算

5️⃣ 指针与数组

6️⃣ sizeof和strlen


循环越界

(1)char类型边界处理

#include 
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原因如下。

这些C题目你都会了吗?C语言题集_第1张图片

(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

解析:越界访问

这些C题目你都会了吗?C语言题集_第2张图片

局部变量在内存中创建是由高地址到地址值创建的,而数组的下标的增长在内存中是低到高增长的。那么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 
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

这些C题目你都会了吗?C语言题集_第3张图片

解析:&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

这些C题目你都会了吗?C语言题集_第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 

	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语言基础非常扎实,要是觉得我写的还行的话,希望你能点赞,关注,收藏 一件三连哦。要是有问题需要探讨,可以评论区留言。我看到将第一时间回复( •̀ ω •́ )✧

这些C题目你都会了吗?C语言题集_第5张图片

你可能感兴趣的