文章目录
- 前言
- 一、由for( ; ;)引出的一系列问题
- 二、sizeof和strlen的误区
- 三、字符串转义字符带来的问题
- 四、结构体概念模糊
- 五、!和~概念分不清
- 六、switch case误区
- 七、短路运算
- 八、复合赋值运算符带来的问题
- 九、什么是左值什么是右值
- 十、struct和union
- 总结
前言
最近我打算出一套笔试刷题的总结,帮助大家解决一些笔试的经典和容易出错的题目,并且将这些知识点讲解明白。
我将会在牛客网上刷题,节省大家的时间将最值得关注的题目呈现给大家。
一、由for( ; ;)引出的一系列问题
在C/C++的for循环中,我们可以省略循环语句的各个参数,包括初始化语句、循环条件和增量表达式。当省略这些参数时,编译器会按照一定的规则进行默认处理。
下面是省略循环参数时的默认处理规则:
1.省略初始化语句:如果省略初始化语句,那么循环开始之前的初始化操作就会被忽略。通常情况下,我们会在循环外部或者在循环之前对循环变量进行初始化。
2.省略循环条件:如果省略循环条件,那么循环将会一直执行,直到遇到 break 或 return 等跳出循环的语句。
3.省略增量表达式:如果省略增量表达式,需要确保循环体内部有适当的语句改变循环变量的值,以避免出现无限循环。否则,循环可能会无限执行,造成程序异常。
下面是一个例子来说明for循环省略参数的情况:
for (;;) {
// 无限循环,没有显式的初始化、条件和增量表达式
// 需要在循环体内部通过其他方式改变循环变量的值或者使用跳出循环的语句来终止循环
}
值得注意的是,虽然可以省略for循环的各个参数,但是在实际编码中,建议根据具体情况选择合适的方式来书写循环语句,以增加代码的可读性和易于维护性。
涉及到的题目:
int i = 0;
for(i = 0; ; i++)
{
printf("Hello\n");
}
根据上面我们讲解到的规则,这里是省略了for循环中的条件,那么代码执行的次数就是无数次,这将是一个死循环。
二、sizeof和strlen的误区
这一道题目其实是没有什么难度的,主要就是要搞清楚sizeof和strlen的原理。
sizeof是计算字符串占内存的大小,而strlen是计算字符串的长度,并且只计算\0前字符串的个数。
根据这个原理我们可以轻松解决这个题目。
三、字符串转义字符带来的问题
要得到下面这道题目的答案我们得先了解字符串转义字符的概念。
在C语言中,转义字符是一种特殊的字符序列,用于表示在普通字符中无法直接表示的特殊字符或者控制字符。转义字符以反斜杠(\)作为前缀,后面跟着一个或多个字符。
下面是一些常见的字符串转义字符及其含义:
':单引号。用于表示普通的单引号字符。
":双引号。用于表示普通的双引号字符。
\:反斜杠。用于表示普通的反斜杠字符。
\n:换行符。用于表示换行操作,在字符串中添加一个换行符。
\t:制表符。用于表示水平制表操作,在字符串中添加一个制表符。
\r:回车符。用于表示回车操作,在字符串中添加一个回车符。
\b:退格符。用于表示退格操作,在字符串中添加一个退格符。
\f:换页符。用于表示换页操作,在字符串中添加一个换页符。
\0:空字符。用于表示空字符,常用于字符串的结尾,表示字符串的结束。
\xhh:十六进制转义字符。用于表示一个字符的十六进制值,hh为两个十六进制数字。
那么现在我们来分析一下答案为什么是9吧。
\:这是转义字符\的转义表示,表示一个反斜杠字符\。
141:普通字符串
\141:同样是八进制转义字符,也表示ASCII码为141的字符。
abc:这是普通的字符序列,包含字符a、b和c。
\t:这是转义字符,表示一个水平制表符。
1 + 3 + 1 + 3 + 1 = 9。
四、结构体概念模糊
这里需要注意的点就是定义结构体类型是不会分配内存的。只有当定义结构体变量时才会分配内存。
结构体类型定义:
typedef struct {
int x;
int y;
} Point;
定义一个结构体变量,这里才会分配内存空间。
Point p;
五、!和~概念分不清
在C语言中,~和!是两个不同的运算符,分别表示按位取反和逻辑取反。
这里很容易将他们混淆。
1.~按位取反:
~是一个一元位运算符,用于对操作数的每个位执行逻辑取反操作。它适用于整数类型(如int、char等)和无符号整数类型(如unsigned int、unsigned char等)。当对一个二进制数进行按位取反运算时,每个位的0会变为1,1会变为0。
示例:
unsigned int x = 10; // 二进制为 0000 1010
unsigned int result = ~x; // 结果为 1111 0101,即4294967285
在进行按位取反运算时需要注意,结果的位数与操作数的位数相同,因此它不会改变操作数的类型。
2.!逻辑取反:
!是一个一元逻辑运算符,用于对操作数执行逻辑取反操作。它适用于任何可以转化为布尔值的表达式,返回值为int类型,结果为1(真)或0(假)。!运算符会将其操作数的值为0(假)时返回1(真),非0值为(真)时返回0(假)。
示例:
int x = 10;
int result = !x; // 结果为0,因为x的值为非0,逻辑取反结果为0(假)
在使用!进行逻辑取反时,非零值会被视为真,而0会被视为假。
总结:
~按位取反运算符用于对整数类型进行按位取反操作,逐位翻转0和1。
!逻辑取反运算符用于对表达式求逻辑反,将非0值取反为0,将0取反为1。
六、switch case误区
现在我们看到下面这个题目,很多同学都会犯错认为输出的结果就是1,其实这是错误的。
当没有遇到break时,无论case中的条件是否符合都会执行,只有当遇到break时才会退出。
所以这题输出的结果是3。
#include <stdio.h>
int main(void)
{
int x = 1;
int i = 0;
switch (x)
{
case 1:
i++;
case 2:
i++;
case 3:
i++;
break;
}
printf("i = %d\n",i);
}
七、短路运算
我们来看到下面的一道题目:
看起来这一题是非常简单的,但是却又是非常容易出问题的。有的同学可能是会认为结果为3,但是最后的结果却是2,这是为什么呢?
根据短路规则,在逻辑或运算符||中,如果左侧表达式为真,那么整个表达式的结果就是真,并且不会再计算右侧的表达式。只有当左侧表达式为假时,才会计算右侧表达式。
在这个代码中,首先判断i > 4的结果是否为真,即比较i是否大于4。由于i的值为5,条件成立,即左侧表达式为真。因此,右侧的表达式j++ > 1不会被计算,也就是不会执行j++的操作。
因此,即使j的初始值为2,右侧的自增操作j++也不会执行。所以,j的值保持为2,并且程序会打印出j的值,即输出结果为j : 2。
#include <stdio.h>
#include <string.h>
int main(void)
{
int i = 5;
int j = 2;
if((i > 4) || (j++ > 1))
{
printf("j : %d\n", j);
}
return 0;
}
那么下面我们再来看一个&&的短路运算:
根据短路规则,在逻辑与运算符 && 中,如果左侧的表达式为假,整个表达式的结果就是假,并且不再计算右侧的表达式。只有当左侧表达式为真时,才会计算右侧的表达式。
#include <stdio.h>
int main(void)
{
int i = 1;
int j = 2;
if ((i == 1) && (j++ > 1))
{
printf("j: %d\n", j);
}
return 0;
}
在这个代码中,首先判断 i == 1 的结果是否为真,即比较 i 是否等于 1。由于 i 的值为 1,条件成立,即左侧表达式为真。因此,接下来会计算右侧的表达式 j++ > 1。
在右侧的表达式中,会先判断 j++ 是否大于 1。由于 j 的初始值是 2,所以 j++ 的结果为 2,不大于 1。因此,该表达式的结果为假。
根据短路规则,因为左侧表达式为真但右侧表达式为假,整个逻辑与的结果也为假。所以,printf 语句不会被执行。
因此,在该代码中,j 的值仍然是初始值 2,而不是 3。
八、复合赋值运算符带来的问题
这一题有很多同学可能会将x *= i + 3等价为 x =x * i + 3,这就会导致错误的结果,实际上x = i + 3是等价为x = x(i+3)的。
#include <stdio.h>
#include <string.h>
int main(void)
{
int x = 5;
int i = 3;
x *= i + 3;
printf("%d\n", x);
return 0;
}
九、什么是左值什么是右值
下面这个表达式是正确的吗?很多小伙伴可能都会犯错,其实这是一个错误的表达式。
++(a++);
在一个表达式中,将同一个变量作为左值(左侧操作数)和右值(右侧操作数)进行修改,会导致未定义行为。
在 C 语言中,左值(lvalue)和右值(rvalue)是用于描述表达式中的值的属性。
左值(lvalue):
左值是指可以出现在赋值运算符左侧的表达式。
左值可以是变量、数组元素、结构体成员等,它们在内存中有对应的存储位置。
左值可以被修改(赋值)。
左值可以出现在引用和取址操作符(如*和&)的操作数中。
例如:
int x = 10; // x 是左值,可以被赋值
int arr[5]; // arr 是左值,数组名可以作为左值
arr[0] = 5; // arr[0] 是左值,数组元素可以作为左值
struct Point { int x; int y; } p;
p.x = 1; // p.x 是左值,结构体成员可以作为左值
右值(rvalue):
右值是指可以出现在赋值运算符右侧的表达式。
右值可以是常量、字面值、表达式的结果等。
右值没有对应的存储位置。
右值不能被直接修改。
右值不能出现在引用和取址操作符的操作数中。
例如:
int a = 5; // 5 是右值,常量可以作为右值
int b = a + 3; // a + 3 是右值,表达式的结果可以作为右值
int c = func(); // func() 是右值,函数调用表达式的结果可以作为右值
需要注意的是,有一些表达式既可以作为左值又可以作为右值,例如变量自身的标识符。根据上下文,同一个表达式可以被视为左值或右值。
例如:
int x = 10; // x 是左值,在赋值语句中作为左操作数
int y = x; // x 是右值,在赋值语句中作为右操作数
int *ptr = &x; // &x 是右值,取址运算符的操作数
总结一下,左值表示可被赋值和具有存储位置的表达式,而右值则表示不可被直接赋值和没有存储位置的表达式。
十、struct和union
结构体 (struct) 和联合体 (union) 都要遵循字节对齐规则。字节对齐是指在内存中分配存储空间时,按照一定的规则将数据对齐到特定的内存地址。
对于结构体和联合体中的成员,编译器通常会根据其类型和对齐规则要求进行对齐。对齐要求通常取决于平台和编译器的设置,可以通过编译器选项或预处理指令进行配置。
常见的对齐规则包括按字节对齐、按类型对齐以及按照默认对齐规则对齐。例如,对于 int 类型的成员,通常要求按照 4 字节对齐,而 double 类型的成员通常要求按照 8 字节对齐。
结构体的大小必须是成员中最大对齐要求的倍数。
所以这题的结果为16 + 8 = 24。
总结
下篇文章继续为大家讲解笔试好题。