前言
根据各方大佬总结的 c 语言面试问题进行了收集,并根据自己的理解进行整理,本篇章属于知识点汇总,如果有需要的内容可以根据目录跳转。
另外八股文等知识梳理的文章,在下整理都要花费十数个小时,若觉得不错的话还请点赞、收藏、关注在下文章吧,感谢感谢!
一、运算符
(一)运算符优先级
问:运算符的优先级
答:成员运算符 > 单目运算符 >= 算数运算符 > 移位运算符 > 关系运算符 > 逻辑运算符 > 赋值运算符
- () 和 [] 优先级最高
- 成员运算符:. 成员选择(对象)、 -> 成员选择(指针)
- 单目运算符:~ 按位取反、- 负号、++ 自增、-- 自减、& 取值、! 逻辑非、(类型) 强制类型转换、sizeof 长度运算符
! 表示取非,对于整形变量,非 0 数字取非为 0,0 取非为 1
~ 表示取反,代表位取反& 表示取地址符
* 作用于指针时表示访问指针所指向的对象
#include <stdio.h>
int main()
{
/* Write C code in this online editor and run it. */
int a = 20;
printf("%d\n",a); // 20
printf("%d\n",&a); // a的地址值,比如0093F968
int* b = &a;
printf("%d\n",b); // a的地址值
printf("%d\n",*b); // 20
int c = !a;
printf("%d\n",c); // 0
int d = ~a;
printf("%d\n",d); // -21
return 0;
}
- 算数运算符:+ 加法、- 减法、* 乘法、/ 除法、% 取余、++ 自增、-- 自减
- 移位运算符:<< 左移、>> 右移
- 关系运算符:> 大于、< 小于、== 等于、 != 不等于、 >= 大于等于、<= 小于等于
- 逻辑运算符:&& 逻辑与、|| 逻辑或、& 位与、| 位或、^ 异或
- 赋值运算符:? : 三目运算符、= 赋值、/= 除后赋值、*= 乘后赋值、%= 取模后赋值、+= 加后赋值、-= 减后赋值、<<= 左移后赋值、>>= 右移后赋值、 &= 位与后赋值、|= 位或后赋值、^=异或后赋值
% 操作要求左右的值均为整数
(二)原码、反码、补码
问:请说说原码、反码、补码
答: 整形数值在计算机的存储中,最左边的一位代表符号位,0 正 1 负
- 原码:二进制的数,比如 10 的原码为 0000 1010
- 反码:正数的反码与原码相同,比如 10 的原码为 0000 1010,反码则为 0000 1010;
负数的反码 0 变 1,1 变 0(符号位不变),比如 -10 的原码为 1000 1010,反码则为 1111 0101。 - 补码:正数的补码与原码相同,比如 10 的原码为 0000 1010,补码则为 0000 1010
负数的补码为反码加1,比如 -10 反码为 1111 0101,补码为 1111 0110
二、关键字
(一)continue
- 作用: 跳过本次循环体中余下未执行的语句,立即进行下一次循环条件判定。
- 注意: continue 只能用在循环语句中,且并不会终止循环
(二)break
- 作用: 跳出本层循环
- 注意:
- break 能用在循环语句和 switch 语句中,其作用是跳出当前循环或跳出当前 switch 语句体
- 如果存在多重循环比如for(){switch case: break},则仅跳出 switch 语句,for循环不受影响
- 使用 switch 一定要加 break,除非有特殊需要
(三)return
结束当前循环,退出函数,用在函数体中则返回特定值
(四)goto
无条件跳转,几乎不使用。
(五)volatile
- 作用: 用于修饰变量和指针,告诉编译器该变量是容易发生变化的,不能对该变量进行优化,每次取值必须从内存中取值而不是直接去取之前在寄存器中的值
- 注意: 首先要清楚在 c 语言中,变量是存储在内存中的,而编译器会将对内存的多次读取操作替换为单次读取操作,以减少对内存的访问次数。比如下列代码:
volatile int a = 20, b, c;
b = a;
c = a;
上述代码如果不加 volatile 编译器的执行流程如下:
- b=a; 先从a的内存中取值存放到寄存器,再把寄存器的值给存到b的内存
- c=a;把寄存器的值给存到b的内存
这里可以看出编译器对 c=a;进行了优化,不再执行从 a 的内存中取值,而是直接从寄存器中取值 如果这段时间内 a 发生了变化(比如中断触发、多线程共享变量修改了a的值),那么 c 就不能得到最新的值
这时候就需要使用 volatile 告诉编译器,不要对变量 a 进行优化,每次都是从内存中取出 a 的值
- 使用场景:
- 多线程使用共享变量:因为多线程是多核操作,同时进行
- 中断:中断程序会修改其他程序中使用的变量
- 硬件寄存器:因为寄存器随时会被修改,好比AD转换的寄存器,随时会因 为电压变化而修改
- 外部任务会修改变量
(六)struct 结构体
问:struct 与 typedef struct 的区别
答:typedef 是类型定义的意思,用在结构体中是给结构体取别名,定义方式如下
#include <stdio.h>
#include <string.h>
typedef struct Student
{
char name[20];
int age;
int sex;
} stu; // stu 即结构体别名
struct Teacher
{
char name[20];
int age;
} Li; // 结构体成员
int main() {
stu Zhangsan = {"zhangsan", 18, 1}; // 使用typedef struct可以直接使用结构体别名创建新的结构体成员
stu Lisi;
strcpy(Lisi.name, "lisi"); // 使用strcpy复制字符串
Lisi.age = 17;
Lisi.sex = 0;
struct teacher Zhang; // 直接使用struct创建结构体成员
return 0;
}
问:结构体大小如何计算
答:
- 对于不同的操作系统,个别数据类型的大小有区别,比如 long 在 32 位操作系统中为 4 字节,在 64 位中就是 8 字节
- 计算大小时需要考虑字节对齐问题:
a. 结构体成员的每个变量的首地址,必须是成员变量中“最大基本数据类型成员所占字节数”的整数倍(对齐)
b. 每个变量类型的偏移量必须是该变量类型的整数倍(对齐)
//最大基本数据类型为 double,占 8 字节,其他的结构体变量占用空间均为 8 的整数倍
struct data {
double d1; // 大小为8字节,从地址0开始,占用到地址8
int d2[5]; // 大小为 4x5=20 字节,首地址为8,为8的倍数,占用到地址28
char d3; // 大小为 1,首地址为28,不是8的倍数,偏移地址到32,占用到地址33
}; // 当前结构体大小为33,不是8的倍数,取8的倍数,所以结构体大小为40
(七)union 联合体
union 的使用:
union 的用法和 struct 差不多,区别是同一个 union 成员的不同的成员变量共享同一块内存空间(起始地址相同),同一时刻只能存在一个成员变量
这边可以理解为张三和李四是穿同一条裤子的,张三穿了裤子,李四就没裤子穿了,同一时刻张三和李四最多只有一个人能穿裤子。
举例个代码:
#include <stdio.h>
union Age
{
int zhangsan;//成员变量张三
int lisi;//成员变量李四
};
int main() {
//union Age age = {18, 20};//同一时刻只能存在一个成员变量,所以这里运行会报错
union Age age;
age.zhangsan = 18;
age.lisi = 22;
printf("%d, %d\n", age.zhangsan, age.lisi); //运行结果 22, 22,说明后赋的值会将先赋的值覆盖
printf("%p, %p\n", &age.zhangsan, &age.lisi);//获取成员变量的地址值,结果0x7ffc6b832f5c, 0x7ffc6b832f5c,说明起始地址相同
printf("%d\n", sizeof(union Age));//获取联合体大小,结果为4,说明该联合体只有一个int类型大小
return 0;
}
问:union 联合体和 struct 结构体的区别
答:
- 对于联合体同一成员的所有成员变量都共享一块内存,而结构体是将所有成员变量内存的叠加,需要考虑字节对齐问题。
- 对于联合体来说,只要你修改了里面的成员变量,其他的成员变量也会被修改,而结构体间的成员变量是互不影响的。
问:联合体一般可以用来做什么?
答:
- 获取数据的高低位,或检查大小端问题
在实际的编程当中,为了确定当前计算机使用的存储模式是大端存储还是小端存储,可以使用union进行简单高效的判断
大端存储模式:高字节存放在低位地址,低字节存放在高位地址
小端存储模式:低字节存放在低位地址,高字节存放在高位地址
ps:x86架构和多数ARM、DSP都为小端存储模式,keil C51则为大端存储模式,有些架构还可以自主切换
#include <stdio.h>
union GetByte
{
short twoByte; //short 类型占 2 字节
char myByte[2];//myByte[0] 和 myByte[1] 分别占用 1 字节
}getbit;
int main() {
getbit.twoByte = 0x1122;
printf("%x, %x\n", getbit.myByte[0], getbit.myByte[1]); //运行结果 22, 11;说明本计算机是低字节存放在低位,高字节存放在高位,为小端存储
return 0;
}
- 节省内存空间(计算机早期内存寸土寸金时才考虑的事情)
如果有不理解的地方,可以看看孤烟大佬的视频
(八)enum
int main()
{
enum { a,b=5, c, d=4, e };
printf("%d, %d, %d, %d, %d",a, b, c, d, e);
//输出结果0, 5, 6, 4, 5
}
- 在枚举变量的值默认为前一个变量的值+1
- 而如果第一个枚举变量没有被赋值,默认值为 0
- b == e == 5 说明枚举的值是可以重复的
(九)typedef
问:typedef 和 #define 的区别
答:
- #define 是 c 语言中定义的语法,是预处理指令,在预处理时进行简单而机械的字符串替换,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。可作用于类型定义、常量、变量等。
- typedef 是关键字,在编译时处理,有类型检查功能。它在自己的作用域内给一个已存在的类型一个别名,但不能在一个函数定义里面使用 typedef。用 typedef 定义数组、指针、结构等类型能够使程序书写简单,也能使意义明确,增强可读性。可作用于类型定义上。
#define | typedef |
---|---|
预处理指令 | 关键字 |
在编译前不作检查,编译时可能检查到错误 | 有类型检查功能 |
无作用域限制 | 有作用域限制 |
可定义类型、常量、变量等的别名 | 可定义类型的别名 |
- 定义指针区别
#define myptr int* p
myptr a,b;//a 是 p 的指针变量,b 是 int 变量
typedef int* myptr;
myptr a,b;//a 是 int* a, b是 int* b
(十)const
- const 定义变量,表示该变量是个常量(并不是真正意义上的常量),不允许被修改,如果存在修改会在编译时报错
const int a = 100;
int const b = 10;
a = 200; //报错
char str[b] = "abcdefg";//报错,因为被const修饰的变量b并不是真正意义上的常量,无法分配准确的大小给str
char num[b];//数组不初始化是不会报错的
- const 修饰函数参数,表示函数体内不能修改该参数的值
void test1(const int x, int y)
{
x = 10; //编译时报错
y = 10;
}
- const 修饰函数返回值,赋值返回值的变量需要被 const 修饰
const int * test(void)
{
return 1;
}
int main(){
const int *num1 = test();
int *num2 = test();//编译时报错
}
- const 修饰指针
int main(){
//*在const后,即常量指针,表示指针指向的内容不可以修改,但是指针的地址可以更改
const * int a;
int const *b;
//*在const前,即指针常量,表示指针指向的内容可以修改,但是指针的地址不可以更改
int * const c;
//指针的地址和内容都不可以被修改
const int * const d;
}
- const 修饰的变量存放位置
对于const修饰的局部变量:存放在栈中,代码结束就会释放,在C语言中可以通过指针修改里面的值
对于const修饰的全局变量(已初始化的)存放在只读数据段,不可以通过指针修改里面的值,未初始化的存放在.bss
(十一)extern 链接阶段
- extern 声明外部变量:(保存在数据段)
在文件 a.c 定义变量并声明全局变量:int a = 10; 这时会建立存储空间
在文件 b.c 对变量 a 进行声明,注意声明时不能初始化。
extern int a;
extern int a = 30; //错误,生命时不能初始化
注意:全局变量最好不要写在 .h 头文件中,如果写在了 .h 文件中,多个 .c 文件对这头文件进行包含,编译器会报错 multiple define 多个定义。
如果有些全局变量经常在多个文件中被需要,普遍的做法如下:
// file1.c 文件
int globalVariable = 10; // 定义全局变量
// golbal.h 文件
#ifndef GLOBAL_H_
#define GLOBAL_H_
extern int globalVariable; // 声明全局变量
#endif /* GLOBAL_H_ */
//file2.c 文件
#include "global.h"
#include <stdio.h>
int main() {
printf("%d\n", globalVariable);
return 0;
}
如上,在golbal.h文件中将常出现的变量进行声明,在需要全局变量的文件中包含即可,如果需要增加代码可读性和防止特殊错误,你也可以在 file2.c 中再进行一次声明。
extern 的作用主要是告诉编译器我在其他文件定义了个全局变量,并且已经分配了空间,不需要再为该变量申请空间
- extern 声明外部函数:方式和声明外部变量一样。
extern void function1(int x, int y);
- extern “C”
该作用是实现在 c++ 中调用 c 语言代码,告诉编译器这部分代码要是有 c 编译
(十二)register
register 只能用于修饰变量(且不能是浮点数),被修饰的变量会被存储在 CPU 内部的寄存器中,而不是内存中。
寄存器有两个特点,一个是运算速度快,一个是不能取地址。
被 register 修饰的变量无法取地址,取地址会编译报错
(十三)auto
一般情况下,我们没有特别声明的局部变量,都默认为 auto 类型,存储在栈中
(十四)static
- static 修饰变量
静态全局变量:作用域当前文件中,其他文件无法访问这个变量,程序运行时被初始化,有且只初始化一次。
静态局部变量:作用域在当前函数中,函数外无法访问,函数被调用时被初始化,有且只初始化一次,生命周期不会随函数结束而结束,直到程序结束
#include <stdio.h>
int staticNum()
{
static int num = 0;
return ++num;
}
int main()
{
int sum = 0;
for(int i=0; i<10; i++)
{
sum = staticNum();
}
printf("%d\n", sum); //打印结果为 10
return 0;
}
问:静态全局变量与静态局部变量的区别
答:作用域不同,静态全局变量的作用域在当前文件中,静态局部变量的作用域在当前函数体中。
问:普通全局变量和静态全局变量的去呗
答:作用域不同,静态全局变量的作用域在当前文件中,普通全局变量的作用域为当前整个项目。
- static 修饰函数
在函数返回值前加上 static 关键字,函数即被定义为静态函数,静态函数的作用域为当前文件,在其他文件也可使用同名的静态函数
(十五) switch case
注意:
- switch里面不能是浮点数、double,可以是表达式,但是结果不能是浮点数或double
- 要注意 case 语句后面是否有break,如果没有,就会从找到的case语句一直执行到停止
- case不能是 “abcdef” 这样的字符串,但是可以是 ‘a’ 这样的单字符,因为字符最终也是整数
(十六)do while
先执行 do {} 中的内容,在执行 while() 里的条件判断是否符合。
(十七) sizeof
问: sizeof() 和 strlen() 的区别
答:
1. sizeof 是关键字,strlen 是函数
2. sizeof 用于计算占用内存大小,strlen 用来计算字符串的长度
3. sizeof 会计算 \0 的空间,strlen 不需要
#include <stdio.h>
#include <string.h>
int main()
{
char c[5] = {1,2,3};
printf("%d\n", sizeof(c)); //5
printf("%d\n", strlen(c)); //3
int i[5] = {1,2,3};
printf("%d\n", sizeof(i)); //20
return 0;
}
4. sizeof 在编译时计算,strlen 在运行时计算
问:如何不适用 sizeof 求数据类型字节的大小
答:
//(char*)强转,便于在指针运算中以字节为单位进行计算。
//&value+1 则能够获取value结束后的地址
//&value 获取value的起始地址
#define mysizeof(value) (char*)(&value+1)-(char*)(&value)
问:strlen(“\0”) == sizeof(“\0”); 是否正确?
答:
printf("%d\n", sizeof("\0")); //2
printf("%d\n", strlen("\0")); //0
printf("%d\n", sizeof('\0')); //4
printf("%d\n", strlen('\0')); //报错空指针
问: sizeof(a++); a最后的结果会改变吗?
答:不会,sizeof 只会求所占内存大小,不会进行表达式运算
int a = 2;
printf("%d\n", sizeof(a++)); //4
printf("%d\n", a); //2
问:计算数组大小
答:
char ch[] = "hello";
char str[10] = {'h','e','l','l','o'};
printf("%d\n",sizeof(ch));//6
printf("%d\n",strlen(ch));//5
printf("%d\n",sizeof(str));//10
printf("%d\n",strlen(str));//5
问:sizeof(void); 结果
答:报错,或者为1