3G_mobile_lab_entry_c.c
3G 移动应用开发实验室 2025 纳新面试题(纯 C 版)
3G 移动应用开发实验室
2025-10
说明:为避免 main 函数重名,采用“main_题号”形式。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
题 1 Hello World & 3G
sizeof / strlen 的区别,字符数组与字符串字面量。
预期输出:Hello 3G 19 25
int main_01(void) {
int a = sizeof(int) - sizeof(char); /* 4-1 = 3 */
char str[] = "Good";
char G = str[strlen(str) - 4]; /* 'G' */
int b = strlen("Hello world and hello 3G!"); /* 25 */
int c = sizeof("Welcome to 3G lab!!"); /* 20(含尾\0) */
printf("Hello %d%c %d%d\n", a, G, c, b);
return 0;
}
题 2 常量不常 局部不局
const 的作用域、static 生命周期、局部变量屏蔽。
预期输出:
MAX_VALUE: 200
static=1, local=11
scope1: local_var = 50
Hello 3g
static=2, local=11
scope2: local_var = 11
void test_function(void) {
/* static 变量:只初始化一次,内存位于数据段,
函数退出后仍然保持值不变,下次调用继续累加。 */
static int call_count = 0;
/* 普通局部变量:每次调用重新在栈上创建,初始化为 10,
函数返回后自动销毁。 */
int local_var = 10;
++call_count;
++local_var; // 先自增,再打印
printf("static=%d, local=%d\n", call_count, local_var);
/* --------- 作用域隐藏演示 --------- */
if (call_count == 1) {
/* 新定义的局部变量把外层的 local_var 完全遮住,
这里访问不到上一行的 local_var(11)。 */
int local_var = 50; // 仅在本 {} 块内可见
printf("scope1: local_var = %d\n", local_var);
printf("Hello 3g\n");
} else {
/* 第二次进入此分支时,里层 {} 已经结束,隐藏解除,
这里看到的是外层 local_var(11)。 */
printf("scope2: local_var = %d\n", local_var);
}
} // call_count 仍存活;local_var 栈空间被回收
主函数——同名局部常量会隐藏全局常量
int main_02(void) {
/* 局部常量 MAX_VALUE 把全局的 100 隐藏掉,
此处打印的是 200。 */
const int MAX_VALUE = 200;
printf("MAX_VALUE: %d\n", MAX_VALUE);
test_function(); // 第 1 次调用:call_count == 1
test_function(); // 第 2 次调用:call_count == 2
return 0;
}
题 3 我本是魔丸,定要学习无符号整形助我破鼎
浮点精度差异:float vs double,字面量后缀对运算的影响。
预期输出:
f1 = 670.3333129883
d1 = 670.33333333333337122895
d1 == d2
int main_03(void) {
/* 1. 单精度运算:2011.0f / 3.0f
* 两个 float 做除法,结果先按 IEEE-754 单精度舍入,
* 再赋给 float 变量 f1。精度大约 7 位十进制有效数字。 */
float f1 = 2011.0f / 3.0f;
/* 2. 双精度运算:2011.0 / 3.0
* 字面量不带后缀默认是 double,整个表达式按 double 运算,
* 结果再赋给 double d1。精度大约 15~16 位十进制有效数字。 */
double d1 = 2011.0 / 3.0;
/* 3. 混合精度运算:2011.0f / 3.0
* 左边是 float,右边是 double,编译器把 float 提升成 double 后再相除,
* 所以运算全程按 double 精度走,结果与 d1 完全相同。 */
double d2 = 2011.0f / 3.0;
/* 4. 按不同格式打印,直观看出精度差异 */
printf("f1 = %.10f\n", f1); // 10 位小数,约 7 位有效数字后开始出现“噪音”
printf("d1 = %.20lf\n", d1); // 20 位小数,double 的有效位全部露出
/* 5. 验证 d1 与 d2 是否按位相等
* 由于第 2、3 步全程都是 double 精度,结果一致,
* 所以输出 “d1 == d2” */
printf("%s\n", (d1 == d2) ? "d1 == d2" : "d1 != d2");
return 0;
}
题 4 别犯傻了,赫里内勒多,代码不进 if 很正常
逻辑与短路:i++ 为 0 时右侧 ++j 不会执行。
预期输出:else branch: i=4, j=0
int main_04(void) {
int i = 0, j = 0;
if (i++ && ++j) /* i 先取 0 判定为假,短路 */
{
j = j >> 2;
printf("if branch: i=%d, j=%d\n", i, j);
} else {
i = i << 2; /* i 已自增为 1,左移 2 位 -> 4 */
printf("else branch: i=%d, j=%d\n", i, j);
}
return 0;
}
题 5 虽然都是小 case,但到底应该落在哪个 case 里呢
位运算 + switch 的 fall-through 行为。
起始 num=0,0-> 51-> 50-> 25-> 24-> 12-> 6-> 3-> 4, 最终返回 1<<5 = 32
预期输出:answer = 32
int process(int num) {
while (1) {
switch (num) {
case 0:
num = (num ^ 123 ^ 87) | 16; /* 60; fall through */
case 1:
num = num ^ 15; /* 60^15=51 */
break;
case 2:
--num; /* fall through */
case 3:
num = num + 1; /** 4 */
break;
case 4:
num = num + 1; /**5 */
case 5:
return (1 << num); /* 1<<5 = 32;返回时num一定为5 */
default:
num = (num % 2) ? (num - 1) : (num / 2); /**50 25 24 12 6 3*/
}
}
}
int main_05(void) {
int answer = process(0);
printf("answer = %d\n", answer);
return 0;
}
题 6 递哩哩哩哩哩哩归
递归填表,类似“网格路径”计数。
strange(3,2) = 15
0 1 2 3
1 2 4 7
2 4 8 15
int strange(int n, int m) {
/**最上边或最左边直接返回 */
if (n == 0)
return m;
if (m == 0)
return n;
return strange(n - 1, m) + strange(n, m - 1);
}
int main_06(void) {
printf("%d\n", strange(3, 2));
return 0;
}
题 7 数组指针 指针数组
多维数组与多级指针的内存布局、解引用方式。
预期输出:
(arr[1] + 2) = 6
arrPtr[1][1] = 5
(*arrPtr + 3) = 4
(ptr + 2) = 3
arr[0][1] = 2
ptrArr[0][0] = 1
int main_07(void) {
/* 1. 定义一个 2×3 的二维数组,内存里连续排布 6 个 int:
* 低地址 -> 高地址
* 1 2 3 4 5 6
* arr 的类型是 int[2][3],可退化为 int(*)[3] */
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
/* 2. 让 int* 直接指向首元素。
* arr[0] 是 int[3],可退化为 int*,指向元素 1。
* ptr 按单个 int 步进。 */
int *ptr = arr[0];
/* 3. 数组指针:arrPtr 是指向“长度为 3 的 int 数组”的指针。
* 初始化后它指向 arr 第 0 行,类型为 int(*)[3]。
* 步进单位是一整行(3 个 int)。 */
int(*arrPtr)[3] = arr;
/* 4. 指针数组:ptrArr 是含 2 个 int* 的数组。
* 我们手动把两个行首地址填进去,相当于把二维拆成两个一维。 */
int *ptrArr[2];
ptrArr[0] = arr[0]; // 指向第 0 行首元素 1
ptrArr[1] = arr[1]; // 指向第 1 行首元素 4
/* 5. 各种解引用验证 --------------------------- */
/* 5.1 arr[1] 拿到第 1 行首地址(int*),+2 再解引用 -> 元素 6 */
printf("*(arr[1] + 2) = %d\n", *(arr[1] + 2));
/* 5.2 arrPtr 指向第 0 行,[1] 步进一行到第 1 行,再 [1] 取该行第 1 列 -> 5 */
printf("arrPtr[1][1] = %d\n", arrPtr[1][1]);
/* 5.3 *arrPtr 先拿到第 0 行首地址(int*),+3 跨到第 1 行第 0 列,再解引用 ->
* 4 */
printf("*(*arrPtr + 3) = %d\n", *(*arrPtr + 3));
/* 5.4 ptr 指向首元素,+2 直接数到第 3 个元素 -> 3 */
printf("*(ptr + 2) = %d\n", *(ptr + 2));
/* 5.5 最朴素二维下标写法 -> 2 */
printf("arr[0][1] = %d\n", arr[0][1]);
/* 5.6 通过指针数组访问 -> 1 */
printf("ptrArr[0][0] = %d\n", ptrArr[0][0]);
return 0;
}
题 8 函数指针也玩 cosplay
通过函数指针实现“策略模式”,提高代码扩展性。
预期输出:
[DEBUG] Hello 3G!
[ERROR] YOLO
- 把“日志策略”抽象成统一接口:任何日志函数只要签名匹配,
都能被当成 log_func 使用,调用方无需知道实现细节。
typedef void (*log_func)(const char *msg);
- 具体策略 A:错误日志,带 [ERROR] 前缀
static void error_log(const char *msg) { printf("[ERROR] %s\n", msg); }
- 具体策略 B:调试日志,带 [DEBUG] 前缀
static void debug_log(const char *msg) { printf("[DEBUG] %s\n", msg); }
- 策略调度器:只依赖抽象接口,不依赖具体实现。 新增日志方式(比如写入文件、syslog、网络)时, 这里一行不改,符合“对扩展开放,对修改关闭”。
static void print_log(log_func func, const char *msg) { func(msg); }
int main_08(void) {
const char *msg = "Hello 3G!";
/* 5. 运行期动态切换策略,同一份调用代码产生不同行为
* → 实现“多态”效果,却零虚函数表、零运行时开销。 */
print_log(debug_log, msg);
print_log(error_log, "YOLO");
/* 6. 如果以后想打日志到文件,只需:
* static void file_log(const char *msg) { ... }
* print_log(file_log, "whatever");
* 主流程、框架代码完全不用动,重新编译即可。 */
return 0;
}
题 9 宏魔法的隐秘交换术
宏展开副作用、字符串化运算符 # 的演示。
预期输出:
i = 4, j = 3
Welcome to 3G 2025!!
3G_mobile_lab_entry_c.c:242:14: warning: multiple unsequenced
modifications to 'i' [-Wunsequenced]
242 | if (SQUARE(++i) == SQUARE(--j))
| ^~
3G_mobile_lab_entry_c.c:229:21: note: expanded from macro 'SQUARE'
229 | #define SQUARE(x) ((x) (x))
| ^ ~
3G_mobile_lab_entry_c.c:242:29: warning: multiple unsequenced
modifications to 'j' [-Wunsequenced]
242 | if (SQUARE(++i) == SQUARE(--j))
| ^~
3G_mobile_lab_entry_c.c:229:21: note: expanded from macro 'SQUARE'
229 | #define SQUARE(x) ((x) (x))
error: Expression '(++i)(++i)' depends on order of evaluation of side
effects [unknownEvaluationOrder]
if (SQUARE(++i) == SQUARE(--j))
error: Expression '(--j)(--j)' depends on order of evaluation of side
effects [unknownEvaluationOrder]
if (SQUARE(++i) == SQUARE(--j))
#define SQUARE(x) ((x) * (x)) // 纯文本替换,无“求值”概念
#define SWAP(a, b, t) \
do { \
t = a; \
a = b; \
b = t; \
} while (0) \
// 使用do{...}while(0)构造后的宏定义不会受到大括号、分号等的影响,应该会按你期望的方式运行。
#define TO_STR(x) #x // 字符串化运算符
#define MAGIC_CAL(a, b) (SQUARE(a) + SQUARE(b))
#define WELCOME(x) printf("Welcome to " TO_STR(3G) " %d!\n", x)
int main_09(void) {
int i = 2, j = 4, tmp = 0;
/* 问题核心:宏展开后变成
if (((++i) * (++i)) == ((--j) * (--j)))
同一个标量 i 在同一个 full-expression 里被无序列地修改两次,
产生未定义行为(UB)。(C11 §6.5 ¶2 明文禁止)。 */
if (SQUARE(++i) == SQUARE(--j))
SWAP(i, j, tmp); // 宏继续展开成 do{...}while(0)
printf("i = %d, j = %d\n", i--, j);
/* 宏继续套娃:
WELCOME(SQUARE(i*i) * MAGIC_CAL(i,4))
-> printf("Welcome to " "3G" " %d!\n",
((i*i)*(i*i)) * (((i)*(i)) + ((4)*(4))) );
此时 i 已经是 3(上一步 i-- 之后),所以打印 2025。 */
WELCOME(SQUARE(i * i) * MAGIC_CAL(i, 4));
return 0;
}
题 10 到底是几?
位域、联合体、结构体内存对齐、指针别名修改。
输出随平台略有差异,典型 64-bit 结果:
Size of MyStruct: 40
bit (3-bit unsigned): 0
unionSon.val (int): 256
After ++(*s1.ptr) & (*s2.ptr)++ : 9 9
Final number value: 10
为什么要对齐
32 位总线一次抓 4 字节,64 位一次抓 8 字节。若变量起始地址是 4/8的倍数,正好落在同一物理行;否则要发两次内存事务,性能掉一倍。
大多数架构保证“自然对齐”的读/写是原子的。跨行跨页需要两条指令,中间可能被中断,并发程序就要自己加锁。
缓存行、MMU 页、DDR burst 都以对齐块为单位。不对齐的访问会触发 bank冲突、cache split、甚至流水线 replay,延迟从 4 cycles 变成 30+ cycles。
如果非要从不对齐的位置读内容,ARM-M0/M3、MIPS32、SPARC等直接抛BusError;x86会帮你拼,但用微码陷阱把性能拖慢一个数量级。
大小端
一、处理器端:一般使用小端,硬件实现更简单
-
低字节在低地址 → 地址运算简单 8/16/32/64 位变量做加/减/乘时,进位都是从低字节向高字节走。小端把低字节放在地址0,硬件只需要一个递增的地址计数器就能按序取完所有字节;大端则要“先取最高字节,地址再递减”或额外加偏移,早期没有高速缓存的年代要多一路加法器。
-
早期 8 位 → 16 位扩展平滑 8086 是 16 位,但要兼容 8 位 8080。 小端保证:
mov al, [0]拿到的是同一个 8 位值,不管后面再接 16 位指令mov ax,[0]。大端就必须在指令里再加“字节交换”或地址回绕,硅片面积立刻上去。 -
乘法/除法/变长整数省力 做 32 × 32 → 64 时,硬件从最低部分积开始算,边算边写回内存;小端可以顺序写,大端要“写完再倒序 store”,多一次循环或缓冲。
RISC-V 把 endian 做成可选,但实现者 99 % 选小端——硅片小、功耗低、IP现成。
二、网络端:一般是用大端
-
RFC 1700(1994)之前,IETF 先拿 Motorola 68k 做路由器原型——它是 big-endian。 协议头必须“先在纸上能读”,于是规定: “最高位在前” 的 big-endian 作为网络字节序。之后考虑兼容性更重要保留了大端序。
-
协议头都是逐字节人工调试抓包看
0x4500...一眼就知道 Version=4, IHL=5; 如果用小端,WireShark 里得先翻转 16/32 位才能读,排错效率直线下降。 -
网络字节序只在包头,量极小路由器对 1 G 包/秒做转发,真正瓶颈是查表、DMA、缓存, 做一次
ntohl只花 1 cycle(现代 CPU 有专用指令bswap),
对齐
平台:arm64
基本类型:alignof(char)=1,alignof(int)=4,alignof(double)=8 …
数组:alignof(T[N]) = alignof(T)。
结构体/联合体:alignof(struct S) = max(alignof(每个成员))
只看成员里最严格的那一个,与结构体总大小毫无关系。
struct SonStruct {
char ch; // 0:1 , 对齐 1
// Type: char
// Offset: 0 bytes
// Size: 1 byte (+3 bytes padding), alignment 1 byte
int val; // 4:4 , 对齐 4 → 前面需填充 3 字节
// Type: int
// Offset: 4 bytes
// Size: 4 bytes, alignment 4 bytes
}; // Size: 8 bytes, alignment 4 bytes
struct MyStruct {
/* ---- 1. 位域 ---- */
unsigned int bit : 3; // 底层类型是 unsigned int(4),但位域不占地址
// 它跟下一个“真正的地址成员”一起算对齐
// 因此编译器把它当成一个 4 字节“容器”来看待,
// 但本身不增加大小,只决定后面从哪儿开始
// Type: unsigned int
// Offset: 0 bytes
// Size: 3 bits (+61 bits padding), alignment 4 bytes
/* ---- 2. 指针 ---- */
int *ptr; // 对齐 8 → 必须从 8 的倍数开始
// 前面留 4 字节空洞(bit 的 4 字节容器占 0~3,
// 4~7 填充,ptr 从 8 开始占 8~15)
// Type: int *
// Offset: 8 bytes
// Size: 8 bytes, alignment 8 bytes
/* ---- 3. 匿名union ---- */
union { // 联合体大小 = 9(char arr[9]),
char arr[9]; // 但联合体对齐值 = 最大成员对齐 = 4(int)
// Type: char[9]
// Size: 9 bytes (+3 bytes padding), alignment 1 byte
int val; // Type: int
// Size: 4 bytes (+8 bytes padding), alignment 4 bytes
} unionSon; // 因此 unionSon 对齐 4,实际占 9 字节,
// 但后面要按 4 对齐 → 编译器把它放到 16
// 16..24 共 9 字节,然后 25~27 填充到 28
// 使得下一个成员能满足 4 对齐
// Type: union (unnamed)
// Offset: 16 bytes
// Size: 12 bytes, alignment 4 bytes
/* ---- 4. 子结构体 ---- */
struct SonStruct son; // 自身大小 8,对齐 4
// 直接从 28 开始,28~35
// Type: struct SonStruct
// Offset: 28 bytes
// Size: 8 bytes (+4 bytes padding), alignment 4 bytes
};
总大小计算:
35 是最后一个字节索引,索引从 0 开始 → 实际用了 36 字节
但结构体本身对齐值 = 最大成员对齐 = 8(ptr 带来的)
因此总大小必须向上取整到 8 的倍数 → 40
Size: 40 bytes, alignment 8 bytes
int main_10(void) {
struct MyStruct s1 = {0}, s2 = {0};
int number = 8;
s1.bit = number; /* 8 超出 3-bit,截断为 0 */
s1.ptr = &number;
s1.unionSon.arr[1] = 1; /* little-endian 下 val 变为 256
* 00000000 10000000 00000000 00000000
* 0b100000000 = 256 */
s2 = s1; /* 结构体整体拷贝 */
printf("Size of MyStruct: %zu\n\n", sizeof(s1));
printf("bit (3-bit unsigned): %d\n", s1.bit);
printf("unionSon.val (int): %d\n\n", s1.unionSon.val);
printf("After ++(*s1.ptr) & (*s2.ptr)++ : %d %d\n\n", ++(*s1.ptr),
(*s2.ptr)++); // 与编译器有关,可能从左往右计算,也可能从右往左。
printf("Final number value: %d\n", number);
return 0;
}
题 11 水壶问题 —— 递归版 gcd
有两个水壶,容量分别为 x 和 y 升。水的供应是无限的。确定是否有可能使用这两个壶准确得到 target 升。
递归求最大公约数(欧几里得算法)
static int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
判断能否用容量 x, y 的水壶量出 target 升。
static bool canMeasureWater(int x, int y, int target) {
if (target < 0 || target > x + y)
return false;
if (x == 0 || y == 0)
return (target == 0 || target == x + y);
int g = gcd(x, y);
return (target % g == 0);
}
static int main_11(void) {
int x = 3, y = 5, target = 4;
printf("canMeasureWater(%d,%d,%d) = %s\n", x, y, target,
canMeasureWater(x, y, target) ? "true" : "false");
return 0;
}
题 12 原地快速排序
static void __quick_sort(int *a, int left, int right) {
if (left >= right)
return;
int pivot = a[left + (right - left) / 2];
int i = left, j = right;
while (i <= j) {
while (a[i] < pivot)
i++;
while (a[j] > pivot)
j--;
if (i <= j) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
i++;
j--;
}
}
if (left < j)
__quick_sort(a, left, j);
if (i < right)
__quick_sort(a, i, right);
}
static void my_qsort_int(void *base, size_t nmemb) {
__quick_sort((int *)base, 0, (int)nmemb - 1);
}
static int main_12(void) {
int arr[] = {5, 2, 9, 1, 5, 6};
size_t n = sizeof(arr) / sizeof(arr[0]);
my_qsort_int(arr, n);
printf("my_qsort_int result: ");
for (size_t i = 0; i < n; ++i)
printf("%d ", arr[i]);
putchar('\n');
return 0;
}
总入口:按需运行各题目演示
int main(void) {
puts("=== 3G 移动应用开发实验室 2025 纳新题 演示 ===");
puts("1. Hello World & 3G");
main_01();
puts("");
puts("2. 常量不常 局部不局");
main_02();
puts("");
puts("3. 浮点精度差异");
main_03();
puts("");
puts("4. 逻辑短路");
main_04();
puts("");
puts("5. 位运算 + switch fall-through");
main_05();
puts("");
puts("6. 递归填表");
main_06();
puts("");
puts("7. 数组指针 指针数组");
main_07();
puts("");
puts("8. 函数指针策略模式");
main_08();
puts("");
puts("9. 宏魔法");
main_09();
puts("");
puts("10. 位域/联合/对齐");
main_10();
puts("");
puts("11. 水壶问题(数学)");
main_11();
puts("");
puts("12. 手写 qsort");
main_12();
puts("");
puts("=== 演示结束,欢迎加入 3G 移动应用开发实验室! ===");
return 0;
}