7.malloc(0)的返回值是什么?会发生什么?
malloc(0)是C标准中的实现定义行为,不同库实现不一样,Linux glibc
不会返回null,会返回一个合法的地址,这个指针可以被free释放,
但是也有个问题,这个指针指向的内存不允许被读写和解引用,没有可用的存储空间。
部分库会有一些返回null;
不要写malloc(0)没有实际业务,没有实际用途;
判断malloc申请成功,不能简单地进行判空处理返回不等于null的,要意识到malloc(0)也有可能会导致返回非null的指针。
拿到这个指针还可以被free ,但是绝对不可以被解引用。
8.什么是内存碎片?分为那俩类?分别怎么产生?
内存碎片分为外部碎片和内部碎片,
外部碎片:产生于堆中频繁申请和释放内存。多次malloc/free后会导致堆里面会有很多分散的,不连续的空闲小块。
这个和上一次发的和相似,brk方式更容易产生这样的外部碎片。
内部碎片:分配给程序的内存块,大于实际程序需要的字节数,多出来的那部分空间被占用,但是程序不会使用造成浪费。
产生的来源有俩处:mmap分配的时候按4kb页进行对齐的,申请一块小于一页的内存,业内剩余的空间浪费。
glibc malloc 为了对齐,分配块字节的时候会向上对齐,多出来的字节无法使用
9.char *p和char arr\[\] 的根本区别?作为函数参数传参的时候会发生什么?
在定义层面 char *p是个指针变量,存字符串字面量的首地址;字符串字面量存放在只读常量区,p变量存在于栈中,不可以修改为p0啥的,会发生段错误
char arr\[\] 字符数组,在栈上面开辟了连续内存,把字符串hello拷贝到数组栈内存里,内存可读可写,可以修改为arr0;
当作为函数参数时:
cpp
void func(char *p){}
void func(char arr[]){}//编译器等价为void
func (char *arr);
数组名作为函数参数时会发生退化,退化为指针。
传入数组后,实际上拷贝的是数组首元素的地址;函数内部拿到的也是指针,不再具备数组大小的信息,sizeof(arr) 得到的指针大小,不是总的字节数
指针传参:拷贝指针变量里面保存的地址值。
定义的时候char*p是指针,指向只读常量;char arr\[\] 是栈上面的副本。传参的时候数组会退化为指针,俩个写法本质完全一致。
10. strlen 和sizeof的区别?手写简易的strlen实现思路说一下?
strlen()库函数,运行时执行,统计字符串有效的字符数量,不包含末尾'\0';依赖'\0'作为结束的标志,参数是必须合法的以'\0'结尾的字符串;如果不以'\0'结尾的话会变成一个随机值;
sizeof:编译器运算符,计算整个变量/数组所占用的内存字节数,包含末尾'\0';不需要'\0'
接受char* 指针,做非空判断后直接进行循环 while 判断*p!='\0'指针后移,让计数器++;循环结束返回数据;
11.指针常量和常量指针。const int *p;int *const p;const int *const p,分别解释。
const int *p;常量指针;
const 修饰*p ,不能修改*p(指针所指向的内容只读) 可以修改p,p可以指向别的地址变量,
int *const p:指针常量 const 修饰 p
不能修改p(指针所指向固定,不能再指向别的地址) 可以修改*p,p可以修改他指向内存里面的数据;
const int * const p; 两边都被const修饰。不能修改p(指向不能变)不能修改 *p (指向的内容也不能变)
cpp
int a=1,b=2;
const int *p1 = &a;
p1 = &b; //合法,改变指向
*p1 = 100; //错误,不能修改内容
int * const p2 = &a;
//p2 = &b; //错误,不能改变指向
*p2 = 100; //合法,可以修改内容
const int * const p3 = &a;
p3 = &b; //报错
*p3 = 100; //报错
12.宏定义#define 和 inline内联函数的区别?宏有什么典型坑?
#define宏:预处理阶段,纯粹的文本替换,没有语法语义的检测,还有类型的检测
inline内联函数:编译阶段,编译器会把函数代码直接展开到调用处,省去函数调用栈开销;本质还是函数,有类型检查。
主要区别
类型:宏无类型检查;inline函数做严格参数类型检查。
递归:宏不能递归;inline函数可以递归(但递归一般不会展开)。
参数副作用:宏只是文本替换,会出现参数重复计算;inline函数参数只计算一次。
作用域:宏全局生效,undef才能取消;inline遵循C作用域规则。宏的典型坑
宏定义多个变量: #define P int * , P a,b ,展开为 int *a,b ,a是指针,b是普通int。
宏参数不加括号: #define ADD(a,b) a+b , ADD(1,2)*3 展开变成 1+2*3 ,优先级出错;所以宏参数全部要套括号。
参数带自增等副作用: MAX(i++, 5) ,i会被执行两次自增。
宏不能调试,没有函数栈,gdb难以跟踪;inline可以调试。
13.说一下结构体的内存对齐第三条规则?为什么需要内存对齐?不对齐会发生什么?
结构体内存对齐三条标准规则(面试背诵)
每个成员的起始偏移,必须是该成员自身大小的整数倍。不满足就填充空字节。
结构体整体对齐:结构体整体大小,必须等于结构体中最大基础数据成员大小的整数倍。不足末尾填充padding。
如果存在嵌套结构体:嵌套结构体成员,对齐模数取它内部最大成员的大小。
cpp
struct Test{
char c; //1
int i; //4
double d; //8
};
c:偏移0,占用 0
i:自身4字节,偏移需要是4倍数 →跳到偏移4;占用4‑7;1‑3填充。
d:自身8字节,偏移需要是8倍数 →偏移8;占用8‑15
总大小16,是最大成员double(8)的整数倍,满足整体对齐
cpp
struct Inner {
double d; //8字节,Inner内部最大成员是double(8)
int x;
};
struct Outer {
int num; //4字节
struct Inner in;
};
struct Inner 内部最大成员是 double(8) 。
所以成员 in (Inner类型),它在外层结构体中的起始偏移,必须是 8 的整数倍。
现在看外层 Outer 全部成员: int num 、 struct Inner in 。
Inner 里面含有 double(8) ,所以Outer全部成员里最大基础成员大小是8。
于是:Outer结构体的总大小,必须是8的整数倍,而不是int的4。
CPU硬件访问内存,是按字块读取(比如一次读8字节)。
对齐的地址,CPU一次就可以读完一个变量;
如果不对齐,一个变量横跨两个内存块,CPU要读两次,再拼接数据,性能下降。
平台差异:
-
x86平台:支持非对齐访问,只是性能变差,不会崩溃。
-
ARM架构:非对齐访问直接程序崩溃。
14:二级指针什么时候使用? char **p ,讲讲它的内存布局。
函数内部修改外部一级指针(最常用)
比如函数内malloc,想要修改外面的指针变量。
cpp
void func(char **pp)
{
*pp = malloc(100); //修改外部一级指针的值
}
如果只用一级指针,C值传递,只能修改指向内容,不能修改指针本身存的地址。
字符串数组, char *arr\[\] ,数组每一个元素是char*;数组名传参退化为 char ** 。
bash
char *strs[] = {"aaa","bbb"}; //字符串指针数组
char **pp
pp本身是指针变量,栈上,存一级指针变量的地址。
*pp :解引用pp,拿到一级指针(char*)的值。
**pp :再解引用,拿到真正char字符
bash
pp(栈) → 存放一级指针p的地址 → p(栈/堆) → 存放字符的地址 → 真正字符数据
为什么想要在函数里面给外部指针malloc,需要传二级指针,传一级指针为什么不行?口述作答。
C语言函数参数是值传递,传指针的时候,只是拷贝一份指针的值。
cpp
void bad(char *p)
{
p = malloc(10); //修改的是形参p,只是函数内部副本
}
int main(){
char *ptr = NULL;
bad(ptr);
// main里ptr依旧是NULL!malloc的地址丢了,内存泄漏
}
这里 bad(ptr) ,把ptr里面存的值(NULL)拷贝给形参p。
p 和 ptr 是两个完全独立的指针变量。函数内修改形参 p ,只会改变副本,不会影响main函数外面的ptr。
想要修改main中ptr这个指针变量本身(把malloc返回的地址写入ptr),就需要传入ptr的地址,也就是二级指针。
cpp
void good(char **pp)
{
*pp = malloc(10); //*pp 等价于 main里面的ptr变量,直接改写外部指针
}
int main(){
char *ptr = NULL;
good(&ptr);
// ptr拿到malloc地址,生效
}
pp 保存外部ptr的地址; *pp 就是外部ptr变量本身,赋值就直接修改外面的指针。
一句话总结:
一级指针传参:可以修改指针指向的内容,不能修改指针变量本身存什么地址。
二级指针传参:可以修改外部一级指针变量本身的值。
15. static关键字在C语言里面三处使用场景分别是什么,每一处作用?
静态局部变量(函数内部 static int a;)
存储位置:不在栈,放在静态存储区(.data/.bss)。
生命周期:整个程序运行全程存在,函数退出不会销毁。
初始化:只初始化一次,第一次调用函数的时候初始化;后续再次调用函数保留上一次的值
作用域:依旧只能在本函数内部使用,外面访问不到。
静态全局变量
普通全局变量:所有 .c 文件都可以extern声明访问。
加 static 之后:作用域限定在当前的.c源文件,其他源文件无法访问;生命周期整个程序。
静态函数
普通函数:外部其他.c可以声明调用。
加 static :该函数仅本 .c 文件内部可用,其他源文件不能调用,对外隐藏。
16. extern关键字的作用?extern "C"是干什么的?(简单口述)
extern
用于声明外部的变量或者函数,告诉编译器这个变量/函数在别的源文件中已经定义,此处仅做引用,不分配内存。
普通全局变量(不加static),其他文件可以通过extern声明来使用;static全局变量,extern也访问不到。
extern "C"
仅C++中使用。
C++为了支持函数重载,编译时会修改函数名字;C语言不会修改函数名。
extern "C" 作用:让C++编译器对包裹的函数,使用C语言的编译命名规则,不做名字改编,保证C++可以链接调用C编译出来的函数。
17. 说一说回调函数是什么?简单说下使用场景
通俗理解:把函数A的地址,当作参数传给另一个函数B;由函数B在内部去调用函数A,A就叫回调函数。
不是我们直接调用A,是B帮我们调用A。
普通调用:你的代码主动调用函数。
回调:把函数指针传进去,别人内部回过头来调用你的函数。
cpp
#include <stdio.h>
//回调函数
void my_print(int data)
{
printf("得到数据:%d\n", data);
}
//接收函数指针作为参数
void handle(int arr[], int len, void (*cb)(int))
{
for(int i=0;i<len;i++)
{
//在这里,内部调用传进来的回调函数
cb(arr[i]);
}
}
int main()
{
int nums[]={1,2,3};
//把my_print函数地址传进去
handle(nums,3, my_print);
return 0;
}
handle 不知道打印逻辑是什么,打印逻辑交给外面传入的 my_print ;handle循环内部回过头调用 my_print , my_print 就是回调函数。
库/工具函数写通用逻辑,具体业务逻辑交给使用者自己写回调。
比如:库只负责遍历数组,打印、求和、过滤这些业务由回调实现。
事件驱动:Linux信号处理、网络编程,收到事件之后系统调用你写好的回调。
排序库函数 qsort !非常经典。 qsort 接收一个比较回调函数指针,qsort内部调用你的比较函数完成
18.qsort函数参数有哪四个?简单说下比较回调怎么写。
cpp
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*));
base :要排序数组的起始地址(void*)nitems :数组元素个数 size :每一个元素占的字节大小
compar :比较回调函数指针
你刚才把个数、元素大小顺序颠倒,面试写出来直接扣分。
回调函数规则回调原型:
int compar(const void *a, const void *b);
返回值规则: 返回 <0:a排在b前面(升序) 返回 >0:b排在a前面 返回 0:两者相等
cpp
int cmp_int(const void *a, const void *b)
{
//void*强转为int*,再解引用拿到数值
int x = *(const int*)a;
int y = *(const int*)b;
return x - y; //升序;如果 y‑x 就是降序
}
19. 什么是段错误segment fault?说出至少4种产生段错误的场景,请作答。
段错误是程序已经编译成功,运行的时候才出现的运行时错误。
底层本质:进程访问不允许访问的内存地址,操作系统内核发送SIGSEGV信号,程序直接崩溃。
段错误常见四大场景
解引用野指针 / 空指针局部指针未初始化,拿着随机垃圾地址解引用。
内存访问越界
数组越界读写,访问不属于本进程的内存地址。
内存不对齐访问
比如 int* 指针,指向不是4字节对齐的地址;ARM硬件直接报段错误;x86只是性能低。
修改只读常量区内存
cpp
char *str = "hello";
str[0] = 'X'; //字符串字面量放在只读常量区,写操作触发段错误
free栈上的地址:free普通局部变量地址。 int a; free(&a);
重复free同一块堆内存。