1.1 基本概念
【必问】一个C程序编译步骤是什么?每一个步骤做了什么?
C代码编译成可执行程序经过4步:
-
预处理:宏定义展开、头文件展开、条件编译等,同时将代码中的注释删除,这里并不会检查语法。
-
编译:检查语法,将预处理后文件编译生成汇编文件。
-
汇编:将汇编文件生成目标文件(二进制文件)。
-
链接:C语言写的程序是需要依赖各种库的,所以编译之后还需要把库链接到最终的可执行程序中去。
链接阶段具体会做哪些工作?
链接器把多个目标文件和库文件组合起来,解析所有符号引用,分配内存地址,修正代码中的地址引用,最终生成一个完整的可执行程序或库。
具体包括以下关键任务:
-
符号解析 —— 找到每个函数/变量引用的定义。
-
地址分配 —— 给各个代码段、数据段分配地址空间。
-
重定位 —— 修正代码中对函数、变量的地址引用,使其指向正确位置。
-
库链接 —— 处理静态库或动态库的依赖关系,决定如何包含或引用它们。
整形常量和浮点型常量的默认类型是什么?
-
整数型常量,默认为int类型。后面加上一个L表示long类型常量。当一个默认int常量赋值给long类型时发生转换。
-
浮点型常量,默认为 double 类型。常量后加F表示float类型,否则自动转换。long double的后缀也是L。
C语言的数据类型有哪几种?string是基本类型吗?
基本数据类型:整型、浮点型、字符型、布尔型。
其他数据类型:构造类型、指针类型、空类型 void。
string不是基本类型。
(追问)C语言的布尔类型跟C++的布尔类型一样吗?
相似但不一样:
C++:直接内置 bool 类型(关键字),无需额外头文件。true 和 false 是内置的布尔文字量(关键字)。
C:类型 _Bool 表示布尔值。与此同时,c99还提供了一个头文件 stdbool.h,文件中定义了 bool代表 _Bool,并且定义了 true 代表1、false 代表0。
char类型、short类型之间做运算(例如加法),得到的是什么类型?
系统自动将 字节宽度较小 的类型转换为 字节宽度较大 的数据类型,它是由系统自动转换完成的。
C 标准规定,在表达式中,所有比 int 小的整型(如 char、short,包括有符号和无符号版本)都会被提升为 int 或 unsigned int。
所以char类型、short类型之间做运算,运算的结果是int类型。
C语言将宽类型赋值给窄类型,会报错吗?
不会。但会导致精度损失。
将浮点数转换为整数时,将舍弃浮点数的小数部分,只保留整数部分。
将很宽的数转成很窄得数,正数都可能变成负数(溢出)。
文件中的全局变量可以被每一个函数使用吗?
不是。
是在全局变量之后定义的函数才可以使用。
如果全局变量与函数中定义的局部变量重名,会发生什么?
在函数内部调用此同名的局部变量(就近原则)。
静态存储方式和动态存储方式有什么区别?
从存储位置看,静态存储变量通常存放在静态存储区,像全局变量和用static修饰的局部变量就属于静态存储,它们在程序编译时就分配好内存空间,整个程序运行期间一直存在;而动态存储变量存放在栈区和堆区,栈区用于存放函数的形参、局部变量等,堆区则是通过动态内存分配函数如malloc、calloc等手动申请的内存,使用完后需手动释放。
从生命周期来讲,静态存储变量的生命周期是整个程序的运行期,从程序开始运行就分配内存,到程序结束才释放;动态存储变量的生命周期则取决于我们的操作,比如在栈上分配的局部变量,函数调用结束后就释放,堆上的变量在我们调用free等函数释放后才结束生命周期。
静态存储区一般存放什么数据?全局变量使用static修饰存放在什么区?
static的局部变量+全局变量。
全局变量使用static修饰依然是全局变量,依然在静态存储区。
【必问】一个局部变量被加上static修饰,会发生什么?
首先,它的存储位置会从栈区转移到静态存储区,这意味着它的内存会在程序启动时就分配好,并且在程序整个运行期间都不会被释放,即使所在的函数已经执行完毕。
其次,它的生命周期延长了,从原本函数调用时创建、函数返回时销毁,变成了和全局变量一样,从程序开始运行就存在,直到程序结束才释放。不过它的作用域并没有改变,仍然只能在定义它的函数内部访问。
另外,由于它存储在静态存储区,每次函数调用时不会重新初始化,而是保留上一次函数调用结束时的值,除非显式重新赋值。这在某些需要记录函数调用状态的场景下非常有用,比如统计函数被调用的次数。
【必问】全局变量使用static修饰,会发生什么?
全局变量使用static修饰,则称为静态全局变量,静态全局变量 仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响。定义不需要与其他文件共享的全局变量时,加上static关键字能够有效地降低程序模块之间的耦合,避免不同文件同名变量的冲突,且不会误使用。
如果在定义静态局部变量时不赋初值的话,会发生什么?
如果在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符‘\0’(对字符变量)。而对自动变量来说,它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另分配存储单元,而所分配的单元中的内容是不可知的。
C语言里,假设我有多个文件例如main.c,f1.c,f2.c文件,已知定义在全局变量前的函数访问不了这个全局变量,那么我在f1.c里定义一个全局变量,f2.c里的哪些函数能访问到这个全局变量?
如果你在 f1.c 中定义了一个全局变量,那么这个全局变量默认具有外部链接,即它的作用域可以被其他文件访问,前提是这些文件要声明它。在 f2.c 中,如果你想访问 f1.c 中定义的全局变量,你需要在 f2.c 中使用 extern 关键字声明它。只要 f2.c 中的函数包含了 extern int global_var; 声明,它就能访问 f1.c 中定义的全局变量。
(追问)如果f2.c的文件里面有两个函数,我在第一个函数后、第二个函数前使用extern int global_var;声明,那么第一个函数能访问到这个全局变量吗?
不能。
(追问)那如果我在f2.c里,只在某一个函数内部写了extern int global_var;语句,会发生什么?
如果你在 f2.c 文件中,只在某一个函数内部写了 extern int global_var;,那么该全局变量 global_var 只对该函数内部可见,而对于该函数外部的其他代码来说,extern 声明是不可见的。
我在f1.c里定义一个全局变量,在f2.c里也定义一个重名全局变量,会发生什么?
在 C 语言中,如果你在 f1.c 和 f2.c 中分别定义了同名的全局变量,这将导致命名冲突,并且在链接阶段会发生错误。(都加上static即可)
我在f1.c里的一个函数内的变量,我需要让他在f2.c也能够使用,能否用static 或extern 修饰符解决?
不能。
(追问)static和extern同时作用于一个变量,会发生什么?
报错。
被const关键字修饰的变量存储在什么区?
const变量的存储位置不固定,但大多数情况下,全局/静态const变量在只读数据段,局部const变量在栈上。
不过,如果这个局部const变量被取了地址或者用extern声明,编译器可能会把它提升到静态存储区(类似全局变量的处理方式)。另外,如果const修饰的是类成员变量,它的存储位置取决于对象的存储位置——如果是栈上的对象,成员变量就在栈上;如果是堆上分配的对象,成员变量就在堆上。
【必问】const修饰指针,写在前面和后面有什么区别?
const int *ptr;,const 修饰指针所指向的数据(数据不可修改)
int * const ptr = &x;,const 修饰指针本身(指针不可修改)
const int * const ptr = &x;,const 修饰指针本身和数据(指针和数据都不可修改)
volatile 关键字有什么作用?
volatile 是 C 语言中的一个关键字,主要用于告诉编译器该变量的值可能会被外部因素(如硬件、操作系统、其他线程等)修改,因此禁止编译器对该变量进行优化。
volatile 与 const 的组合有什么用?
volatile 和 const 可以同时修饰变量,这样变量被声明为常量且不可修改,同时其值仍然可以被外部因素修改(例如硬件或信号中断)。
restrict 关键字有什么作用?
restrict 关键字告诉编译器,在某个作用域内指针所指向的内存区域不被其他指针访问,从而允许编译器进行更加激进的优化。它的使用有助于提高程序的性能,尤其是在处理指针和数组时。
const修饰的数据加上static、extern分别会发生什么?
当const修饰全局变量时,默认它是内部链接属性,也就是只能在当前文件使用。加上static,进一步明确了它的内部链接属性,还是只能在定义它的源文件里访问,这和单纯const修饰全局变量的默认行为一致,不过显式加static能让代码意图更清晰。
要是const修饰全局变量时加上extern,那就改变了它的链接属性,让它变成外部链接属性,意味着可以在其他源文件里通过extern声明来使用这个变量。
对于const修饰的局部变量,它默认是常量,存储在栈上,加上static会改变它的存储位置到静态区,生命周期变为整个程序运行期间,但作用域还是局部的;而const局部变量一般不加extern,因为extern主要用于扩展全局标识符的链接性,对局部变量没意义。
已知a=1,那么在C语言里b=a++ + a++,那么b=?(自增笑传之拆拆步)
int temp1 = a++; // temp1 = 1, a = 2
int temp2 = a++; // temp2 = 2, a = 3
b = temp1 + temp2; // b = 1 + 2 = 3
b = 3
那如果有一个表达式a+++a,那么代表a++ + a还是a + ++a?
表达式 a+++a 在 C 语言中是一个合法的语法,并且它的含义是 a++ + a,而不是 a + ++a。这背后的原因是 C 语言的运算符优先级和结合性规则。
sizeof是关键字,运算符还是函数?
sizeof 是 C++ 中的一个关键字,同时它也是一种运算符。它不是函数。
switch使用时有哪些需要注意的点?
-
如果没有break,就会直接往下执行语句块,忽视下面的所有case判断。
-
case子句中的值必须是常量,不能是变量名或不确定的表达式值或范围。
-
同一个switch语句,所有case子句中的常量值互不相同。
-
switch中的表达式可以是整型或字符类型。不可以使用浮点类型,否则编译失败。
-
default位置是灵活的。
typedef vs #define的区别是什么?
用typedef只是对已经存在的类型指定一个新的类型名,而没有创造新的类型。typedef与#define是不同的。#define是在预编译时处理的,它只能作简单的字符串替换,而typedef是在编译阶段处理的,且并非简单的字符串替换。
inline是干嘛的?
inline 是 C 语言中的一个关键字,通常用于函数声明和定义中,目的是提示编译器将函数的代码直接嵌入到调用函数的地方,从而减少函数调用的开销,提高程序的执行效率。inline 关键字用于建议编译器将函数体的代码在调用的地方进行替换(内联展开)。
inline 函数通常用于简单且频繁调用的函数,比如计算、获取值等,而不适用于复杂的函数(比如包含循环、大量逻辑判断等)。
inline 是在编译阶段处理的。
【必问】inline vs #define的区别是什么?
inline 是在编译阶段处理的,内联函数是有类型的,会进行类型检查,更安全且容易调试。
宏是在预编译时处理的,宏只是简单的文本替换,不进行类型检查。
怎么判断两个浮点数是否相等?为什么?
正确做法:使用一个很小的误差范围(epsilon)进行比较。由于浮点数存在精度误差,我们通常不判断它们是否完全相等,而是判断它们是否“足够接近”。
怎么比较两个结构体是否相等?直接使用memcmp比较==0可能出现什么问题?
C语言中的结构体在内存中布局时,编译器为了提高访问效率,通常会对成员进行 内存对齐(alignment),这就会在结构体成员之间插入一些 不可见的填充字节(padding bytes),用来对齐下一个成员的地址。
如果你用 memcmp() 比较两个结构体,这些 padding 字节的差异会导致 memcmp() 返回非零,即使你关心的成员(如 a 和 b)是完全相同的。
正确做法:逐成员比较结构体。
取余运算如何优化成位运算?
只有当除数是 2 的整数幂(即 1, 2, 4, 8, 16, 32, ...)时,a % n可以优化为 a & (n - 1)。
1.2 数组
对数组部分元素赋初值,如果大括号里面的值少于数组的成员数量,会发生什么?多于数组的成员数量呢?
未赋值的成员自动初始化为0。如果多于数组的成员数量则会报错。
数组被创建在内存的哪个位置?
一般的数组在内存的栈空间,malloc创建的数组在堆空间。
int *arr2 = arr1;能复制数组吗?
不能,两个指针指向同一内存。
可以使用循环语句创建新的数组然后一个一个把元素复制过去。
也可以使用 memcpy()函数。
字符串声明时赋值的元素长于定义长度,会发生什么?短于定义长度呢?
字符串声明时赋值的元素长于定义长度,例如char str[2] = "aaaaaaaaaaaaaa";,编译器可能会给与警告,但也可能报错。
若赋值的元素短于定义长度,则这个字符串的多余部分都是\0。
【必问】sizeof vs strlen对字符串操作的差别在哪?
sizeof(str4)/sizeof(char)字符数组长度包含\0。
strlen(str)字符串长度不包含\0(但无论如何存储在内存中时\0总是在的)。
1.3 指针
*p++, *(p++), *++p, *(++p), ++*p, ++(*p)的区别?
-
x = *p++
先使得p++,然后指针指出。但是因为后++最后才会执行所以x == *p,随后p才会真正++。
-
x = *(p++)
和上面的那个1一模一样。
-
x = *++p
先使得++p,然后指针指出。由于是前++,p会先+1,x == *(p+1)。
-
*(++p)
和上面的那个3一模一样。
-
x = ++*p
先指针指出,得到一个int值,再把这个值+1,x == *(p)+1。
-
x = ++(*p)
和上面的那个5一模一样。
【必问】野指针的成因有哪些?
-
指针使用前未初始化。
-
指针越界访问。
-
指针指向已释放的空间。
【必问】(追问)野指针如何避免?
-
指针初始化。
-
小心指针越界。
-
避免返回局部变量的地址。
-
指针指向空间释放,及时置NULL。
-
指针使用之前检查有效性。
(追问)野指针和悬垂指针的区别?
-
野指针:指针声明后没有赋初值,它的值是“垃圾值”(可能是之前内存中的残留数据)。
-
悬垂指针:指针曾经指向一个合法的对象或内存块,但该对象已经被销毁(比如函数返回了局部变量的地址,或者动态分配的内存被释放)。
arr+1和&arr+1结果相同吗?
完全不同,arr+1只是真加了1变成arr[1],&arr+1就加了一整个数组的大小。
【必问】数组指针 vs 指针数组有什么不同?
数组指针和指针数组有着本质区别。数组指针它本质上是一个指针,只不过这个指针指向的是一个数组。比如说 int (*p)[10] ,这里的 p 就是一个数组指针,它指向一个包含 10 个整数的数组,我们可以通过 p 来访问这个数组里的元素。
而指针数组呢,它本质上是一个数组,数组里的每个元素都是指针。例如 int *p[10] ,这里 p 就是一个包含 10 个元素的数组,每个元素都是一个 int 类型的指针,这些指针可以分别指向不同的整数或者整数数组。在使用的时候,数组指针主要用于操作特定的数组,而指针数组更灵活,能存储多个不同对象的地址,在处理字符串数组等情况时经常会用到。
1.4 函数
【必问】值传递和地址传递有什么不同?那些数据默认使用值传递?
值传递,又称传值方式、数据复制方式,就是把主调函数的实参值 复制 给被调用函数的 形参,使形参获得初始值。接着在函数内对形参值的修改,不影响实参值。
地址传递,又称传地址方式、地址复制方式、指针传递,就是把实参地址常量进行复制,传送给形参。实参将地址传递给形参,二者地址值相同。
默认传递值的类型: 基本数据类型 (整型类型、浮点类型,字符类型)、结构体、共用体、枚举类型。
默认传递地址的类型: 指针、数组。
【必问】指针函数 vs 函数指针有什么不同?
指针函数和函数指针确实容易混淆。指针函数其实就是一个返回指针类型的函数,比如int* func(),这个函数执行完后返回的是一个指针。
函数指针则是指向函数的指针变量,它存储的是函数的地址,可以通过这个指针去调用对应的函数,比如int (*funcPtr)(),这里funcPtr就是一个函数指针,它可以指向返回值为int且无参数的函数。
简单来说,指针函数是“返回指针的函数”,而函数指针是“指向函数的指针”。在实际使用中,函数指针经常用于回调函数或者实现多态等高级功能,而指针函数则更多是在需要返回动态分配内存或者复杂数据结构时使用。
内部函数 vs 外部函数有什么不同?
如果在一个源文件中定义的函数只能被本文件中的函数调用,而不能被同一源程序其他文件中的函数调用,这种函数称为内部函数。此时,内部函数需要使用static修饰。
外部函数在整个源程序中都有效,只要定义函数时,在前面加上 extern 关键字即可。因为函数与函数之间都是并列的,函数不能嵌套定义,所以函数在本质上都具有外部性质。因此在定义函数省去extern说明符时,则隐含为外部函数。
你是否使用过dlopen这类API?他们是做什么的?
这样的 API,它们属于 动态链接(Dynamic Linking) 相关的系统编程接口,主要用于在程序运行时(而非编译或启动时)动态加载共享库(动态库)并调用其中的函数或访问变量。这在很多高级场景中非常有用,比如插件系统、模块化设计、延迟加载等。
简单来说,dlopen 允许程序在运行时按需加载某个动态库,而不是在编译时或程序启动时就固定链接好所有的库。这为程序提供了极大的灵活性,比如:
-
实现插件机制:主程序不关心有哪些插件,插件可以在运行时被加载进来。
-
实现模块化架构:根据配置或用户选择,动态加载不同的功能模块。
-
延迟加载某些功能,优化程序启动性能。
-
支持热更新:替换动态库后,无需重新编译主程序即可生效。
(追问)当你更新一个被dlopen加载的.so文件时,整个服务需要重启吗?需要注意什么ABI兼容问题?
如果及时重新用 dlopen 加载新版本的 .so 文件,并且正确管理旧库的生命周期和符号引用,那就不用重启(但是这很难)。
但如果你的程序一直使用的是旧的句柄(handle),那么即使你替换了磁盘上的 .so 文件,程序依然调用的是旧版本代码,除非你显式地重新加载新 .so。
如何实现“热更新”?
一些复杂系统会采用“双缓冲”或“版本切换”设计,保证切换的原子性与安全性。
技术上,你可以同时保留多个 dlopen 的句柄,指向不同版本的 .so,然后根据需要使用其中某一个。
什么是 ABI(Application Binary Interface)?
ABI 是二进制层面的接口规范,包括:
-
函数调用约定(参数如何传递、栈如何管理)
-
数据类型的大小和对齐(如
int、struct、指针大小) -
函数符号的名称修饰(name mangling,C++ 更复杂)
-
全局变量的布局与访问方式
-
动态库中导出的符号(函数/变量)的版本与兼容性
如果新旧版本的 .so ABI 不兼容,会导致什么后果?
-
程序崩溃(比如函数参数传递错误、结构体布局变化导致内存访问越界)
-
逻辑错误(函数行为不符合预期,但不会立即崩溃)
-
数据损坏(比如新旧版本对某个数据结构的理解不一致)
如何尽量保证 ABI 兼容性?(最佳实践)
如果你计划使用 dlopen 做热加载或插件机制,强烈建议遵循以下原则:
-
保持稳定的 C 接口(使用 extern "C"):C++ 的符号修饰复杂、易变,建议用
extern "C"包裹导出的函数 -
使用版本控制策略
-
设计良好的插件接口(抽象层)
1.5 结构体
以下结构体的sizeof是多大?
结构体占用的存储空间,不是各个属性存储空间的总和。为了计算效率,c语言的内存占用空间一般来说都必须是 int 类型存储空间的整数倍。如果 int 类型的存储是4字节,那么 struct 类型的存储空间就总是4的倍数。
typedef struct A
{
char a[31];
int b;
} A;
占用大小:36(a:31补齐到32,再加上b占4)
typedef struct A
{
char a;
int b;
char c;
} A;
占用大小:12(a:1补齐到:4,b占4,c:1补齐到:4)
typedef struct A
{
int b;
char a;
char c;
} A;
占用大小:8(b占4,a:1但是c也只占1,他俩可以组合,1+1再补2)
typedef struct A {
int b; // 4字节
char a[33]; // 33字节
double c; // 8字节(对齐要求为8字节)
} A;
占用大小:基础对齐值=double8,所以占用48((int4+a的前4字节)+36(33-4=29)+8)
typedef struct A
{
char a[33];
double c;
int b;
} A;
占用大小:基础对齐值=double8,所以占用56(40(33补齐)+8+8)
将结构体用赋值运算符(=)赋值给另一个变量,他们会共用一个地址吗?直接用等于号,会造成什么问题?
不共用,结构体是值传递,并非地址传递。
但是,但是结构体内部如果有类似于地址的东西(一个字符串),那么赋值后的这两个结构体内部指向同一个字符串,造成问题。
以下共用体的sizeof多大?
共用体变量所占的内存长度等于最长的成员的长度,但也需要对齐。
typedef union A
{
int b;
char a[31];
} A;
占用32字节,其中a需要向int类型对齐。
typedef union A
{
int b;
char a[33];
double c;
} A;
占用40字节,其中a需要向double类型对齐。
2648

被折叠的 条评论
为什么被折叠?



