3G 移动应用开发实验室 2025 纳新面试题

46 阅读14分钟

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

  1. 把“日志策略”抽象成统一接口:任何日志函数只要签名匹配,

都能被当成 log_func 使用,调用方无需知道实现细节。

typedef void (*log_func)(const char *msg);
  1. 具体策略 A:错误日志,带 [ERROR] 前缀
static void error_log(const char *msg) { printf("[ERROR] %s\n", msg); }
  1. 具体策略 B:调试日志,带 [DEBUG] 前缀
static void debug_log(const char *msg) { printf("[DEBUG] %s\n", msg); }
  1. 策略调度器:只依赖抽象接口,不依赖具体实现。 新增日志方式(比如写入文件、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会帮你拼,但用微码陷阱把性能拖慢一个数量级。

大小端

一、处理器端:一般使用小端,硬件实现更简单

  1. 低字节在低地址 → 地址运算简单 8/16/32/64 位变量做加/减/乘时,进位都是从低字节向高字节走。小端把低字节放在地址0,硬件只需要一个递增的地址计数器就能按序取完所有字节;大端则要“先取最高字节,地址再递减”或额外加偏移,早期没有高速缓存的年代要多一路加法器。

  2. 早期 8 位 → 16 位扩展平滑 8086 是 16 位,但要兼容 8 位 8080。 小端保证:mov al, [0] 拿到的是同一个 8 位值,不管后面再接 16 位指令 mov ax,[0]。大端就必须在指令里再加“字节交换”或地址回绕,硅片面积立刻上去。

  3. 乘法/除法/变长整数省力 做 32 × 32 → 64 时,硬件从最低部分积开始算,边算边写回内存;小端可以顺序写,大端要“写完再倒序 store”,多一次循环或缓冲。

RISC-V 把 endian 做成可选,但实现者 99 % 选小端——硅片小、功耗低、IP现成。

二、网络端:一般是用大端

  1. RFC 1700(1994)之前,IETF 先拿 Motorola 68k 做路由器原型——它是 big-endian。 协议头必须“先在纸上能读”,于是规定: “最高位在前” 的 big-endian 作为网络字节序。之后考虑兼容性更重要保留了大端序。

  2. 协议头都是逐字节人工调试抓包看 0x4500... 一眼就知道 Version=4, IHL=5; 如果用小端,WireShark 里得先翻转 16/32 位才能读,排错效率直线下降。

  3. 网络字节序只在包头,量极小路由器对 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;
}