本篇内容:
1. 程序的内存分配
-
①栈(stack):由编译器进行管理,自动分配和释放,存放的是函数调用过
程中的各种参数,局部变量,返回值以及函数返回地址。 -
②堆(heap):用于程序动态申请分配和释放空间( malloc和free),程序员
申请的空间在使用结束后应该释放,则程序自动收回。
✔ 如何记忆?自己申请的东西不要了就丢垃圾堆,垃圾堆需要自己收拾;客栈的东西出钱了由老板收拾,不需要自己动手。(嘿嘿嘿,生动形象吧!!!)
-
③数据段:主要存放全局变量和静态变量。分成了以下几部分
-
已初始化数据段.data:已初始化的全局变量和静态变量
-
未初始化数据段.bss:未初始化的全局变量和静态变量
-
只读数据段.rodata:只读常量
✔ 这里可能有误,百度的各种说明中,个人感觉很多分类得不是很清晰,比较容易混淆,这里给出的是整理之后比较清晰的版本,如有错误,敬请读者批评指正。详细见下图。
-
⑤代码段: 存放程序的二进制代码。
【2024面试真题·广州某100-499公司】
2. 堆栈溢出的原因?
(递归层次深、动态申请内存未释放、数组越界、指针非法访问)
答:①递归函数调用层次太深,栈中需要保存每次递归调用的局部变量,当递归调用层次太深,会导致栈溢出;
②动态申请的变量保存在堆中,需要在调用结束后手动释放,否则也会导致堆溢出;
③数组下标越界;
④指针访问非法的内存地址。
3. 关键字const的理解?
答:作用:限定或者说限制变量和常量为只读,常量必须在定义的时候同时被初始化
使用和理解如下:
#include <stdio.h>
int main(){
int val = 100;
int valNew = 200;
int *p1 = &val;
int const *p2 = &val; //p2的值只读(*p2),不能修改
int *const p3 = &val; //p3只读,不能修改
int const *const p4 = &val; //p4和*p4只读,不能修改
printf("p1=%d,p2=%d,p3=%d\n",*p1,*p2,*p3);
/** p1的值和指向可以被修改 **/
p1 = &valNew; /* OK */
*p1 = 300; /* OK */
/** p2的值只读不能修改,指针指向可以修改 **/
p2 = &valNew; /* OK */
*p2 = 300; /* ERROR */
/** p3只读不能修改,p3的值(*p3)可以修改 **/
p3 = &valNew; /* ERROR */
*p3 = 300; /* OK */
/** p4的的值和指向均只读不能修改 **/
p4 = &valNew; /* ERROR */
*p4 = 300; /* ERROR */
printf("p1=%d,p2=%d,p3=%d\n",*p1,*p2,*p3);
return 0;
}
4. 关键字static的作用?
答:①static修饰全局变量:只初始化一次,且防止该变量在其他文件中被引用,此时该全局变量的作用域是当前文件;
②static修饰局部变量:普通的局部变量在函数执行完毕后会被销毁,而使用 static 修饰的局部变量内存空间会一直存在直到程序结束。这意味着每次调用函数时,该变量的值是不会被覆盖或重新初始化,可以实现变量值的持久化。
③static修饰函数:表示该函数作用域在当前声明文件中。
5.关键字extern的作用?
答:声明一个定义在外部文件的变量(引入)。如果extern声明全局变量,编译器会自动到本工程其他.c源文件引入该变量; extern也可以引入外部定义的函数,相当于包含对应的头文件。(引入外部变量、外部函数)
6. sizeof和strlen的区别?
- sizeof是C语言中的关键字,用于计算数据类型或变量所占内存的字节数。
- strlen是C语言中的函数,用于计算字符串的长度,即有效字符的个数。
- 计算字符串的大小时,sizeof包含字符串的结束符‘\0’,strlen不包含’\0’。
- sizeof 运算符在编译时期完成,返回的是一个常量表达式,它的结果是在编译时确定的;strlen函数在运行时期根据传入的字符串动态计算其长度,返回的是字符串的有效字符个数。
7. strlen(“\0”) 和sizeof(“\0”)的“陷阱”
写出下面程序的输出结果:
a = 0,b = 2,a1段错误,b1 = 4
8. 什么是预编译,什么时候需要预编译
答:①编译前处理以#开头的指令,例如:#include编译器会拷贝已包含的头文件代码;#define宏定义的替换;以及#ifndef进行的条件处理。预编译指令可以放在程序中的任何位置。
②对于总是经常使用但不经常改动的大型代码,程序由多个模块组成,所有模块都使用相同的编译选项,这种情况下可以将包含文件预编译成一个预编译头。
9. C语言中的预编译运算符#和##的区别?
- #(字符串化运算符):
- 用于将宏参数转换为字符串常量。
- 示例:假设有一个宏定义 #define STR(x) #x,当使用该宏时,STR(test) 将被展开为 “test” 字符串常量。
- ##(连接运算符):
- 用于将两个标识连接在一起形成一个新的标识。
- 示例:假设有一个宏定义 #define CONCAT(x, y) x##y,当使用该宏时,CONCAT(a, b) 将替换为标识符ab。
- 如以下代码:
#include <stdio.h>
#define TEST1(S) (#S)
#define TEST2(S1,S2) (S1 ## S2)
int main(){
char *ab = "HelloWorld";
printf("%s\n",TEST1(1));
printf("%s\n", TEST2(a,b));
return 0;
}
10. 如何避免头文件被重复包含?
- 使用#ifndef、#define和#endif
11. 局部变量和全部变量能否重名?
答:可以,局部变量会屏蔽全局变量。当局部变量和全局变量重名,在函数内引用该变量时,会用到同名的局部变量,而不会用到全局变量。(在C++中可使用作用域解析运算符 ::界定区分)
12. 指针在内存中占多少个字节?
答:指针占多少个字节与地址总线的位数有关,32位的地址总线指针所占大小位4个字节;64位的地址总线指针占8个字节
13. 请写一个标准宏MIN,实现传入两个参数返回较小的一个。
- #define MIN(A,B) ((A) <= (B) ? (A) : (B))
14. 给出下列变量的定义
定义一个整型数 int a;
定义一个指向整型数的指针 int *a;
定义一个指向指针的指针,它指向的指针指向一个整型数 int **a;
定义一个有10个整型数的数组 int a[10];
定义一个有10个指针的数组,每个指针指向一个整型数 int *p[10];
定义一个指向有10个整型数的数组的指针 int (*p)[10];
定义一个指向指针的指针,被指向的指针指向一个有10个整型数的数组 int *(*p)[10];(错) / int (**a)[10];(对)
定义一个指向数组的指针,数组中有10个整型指针 int *((*p1)[10]);(我的答案) / int *(*p2)[10];(参考答案)
定义一个指向函数的指针,该函数只有一个整型参数且返回一个整型数 int (*p)(int);
定义一个有10个指针的数组,每个数组指向一个函数,该函数只有一个整型参数且返回一个整型数 int (*p[10])(int);
定义一个函数指针,指向的函数有两个整型参数且返回一个函数指针,返回的函数指针指向有一个整型参数且返回整型数的函数 int(* (*p)(int,int))(int)
15. 结构体字节对齐
- 结构体变量的首地址是最长成员变量的整数倍
- 每个成员变量的地址空间必须对齐