当前位置:首页 > 技术知识 > 正文内容

【C语法硬核20讲】02 数组声明到越界避坑

maynowei5个月前 (10-14)技术知识91

第 01 讲聊了指针。这一讲,我们把“最容易和指针搅在一起”的数组彻底讲清楚:从声明与初始化,到退化(decay)、多维数组、函数形参写法,再到**越界与未定义行为(UB)**的实战避坑。内容偏硬核,但保证能直接落地。


一、数组声明速通:看懂这几行就够了

  • 一维数组:类型 名字[元素个数];
  • int a[4]; // 4 个 int double x[3] = {0}; // 全部置零
  • 多维数组(行主序 row-major):int m[行][列];
  • int m[2][3] = { {1,2,3}, {4,5,6} };
  • 区分“指针数组 vs. 数组指针”
  • int *pa[10]; // 指针数组:10 个 int* 元素 int (*ap)[10]; // 数组指针:指向“含 10 个 int 的数组”的指针
  • 口诀:[] 与 () 谁先“贴”到变量名,谁就先结合。
  • 变长数组(VLA, variable length array,C99 起、C11/C17 为可选特性)
  • void f(size_t n) { int v[n]; // 运行期决定大小,注意栈空间与可移植性 }

二、初始化:指定初始化器、字符串、零填充

  • 部分初始化自动补零
  • int a[5] = {1, 2}; // 等价于 {1,2,0,0,0}
  • 指定初始化器(designated initializer, C99)
  • int a[8] = {[2]=10, [7]=99}; // 其余元素为 0
  • 字符数组 vs. 字符串字面量
  • char s1[] = "abc"; // 含隐含 '\0',长度 4 char *s2 = "abc"; // 指向只读存储(多数实现);修改 → UB // s1[0]='A'; 合法 s2[0]='A'; 未定义行为
  • 要点:想改内容就用数组;char* 指向字符串字面量通常不可改。

三、sizeof 与长度:别把指针当数组

  • 在定义处拿长度(仅在同一作用域有效)
  • int a[5]; size_t n = sizeof a / sizeof a[0]; // 5
  • 指针不是数组
  • int *p = a; sizeof p // 指针大小(64 位常见为 8),不是数组大小
  • 多维数组的宽度
  • int m[3][4]; size_t rows = sizeof m / sizeof m[0]; // 3 size_t cols = sizeof m[0] / sizeof m[0][0]; // 4
  • 字符串字面量长度
  • sizeof "hello" // 6(含结尾 '\0')

结论:凡是离开数组的定义处(比如传给函数),就丢失了长度信息,要么传长度,要么用更安全的形参写法(见下一节)。


四、数组“退化(decay)”与函数形参的三种写法

规则:除 sizeof、_Alignof、& 以及字符串字面量初始化外,数组会退化为指向首元素的指针。这会导致函数里看不到数组的真实长度

1)最常用:指针 + 显式长度

void sum_ints(const int *a, size_t n, long long *out) {
    long long s = 0;
    for (size_t i = 0; i < n; ++i) s += a[i];
    *out = s;
}

2)固定“列数”的二维数组(指针指向数组)

void scale_rows(size_t rows, int (*mat)[4], int k) { // 每行 4 列
    for (size_t i = 0; i < rows; ++i)
        for (size_t j = 0; j < 4; ++j) mat[i][j] *= k;
}

调用:

int m[2][4] = {0};
scale_rows(2, m, 2); // OK:m 自动转为 int (*)[4]

3)使用 VLA 形参(C99,可选特性)

void add_mm(size_t r, size_t c, int A[r][c], const int B[r][c]) {
    for (size_t i=0;i<r;++i)
        for (size_t j=0;j<c;++j)
            A[i][j]+=B[i][j];
}

优点:形参自带“宽度信息”。缺点:受实现与栈大小限制。

常见误区:int ** 不是 int a[M][N] 的等价形参。int a[M][N] 在内存中是一块连续区域;而 int ** 通常是“指向指针的指针”,两者布局与寻址不同。


五、多维数组与内存布局:行主序、等价地址

  • C 的多维数组是数组的数组,采用行主序(row-major)
  • int a[2][3] = { {1,2,3}, {4,5,6} }; // &a[0][0]、&a[0][1]、&a[0][2]、&a[1][0]、...
  • 等价表达式:
  • *(*(a + i) + j) // 等价于 a[i][j]
  • 动态二维数组的正确分配(连续内存版)
  • size_t R=3, C=4; int (*mat)[C] = malloc(sizeof *mat * R); // 连续 R*C 个 int // 使用 mat[r][c] 访问 free(mat);
  • 用 int **p + 多次 malloc 企图模拟 int[R][C] 的连续布局;那是**“锯齿形(jagged)”**,缓存局部性与寻址都不同。

六、越界 = 未定义行为(UB):十个高频坑

  1. <= 写成边界条件
  2. for (size_t i=0; i<=n; ++i) a[i]=0; // 越界(最后一次 i==n)
  3. 负索引或“从 -1 开始”
  4. a[-1] = 7; // UB,即使在某些平台“看起来没事”
  5. 指针走过头
  6. int *end = a + n; // 这是“尾后指针”,可比较,不可解引用 *end = 0; // UB
  7. memcpy 长度写错
  8. memcpy(dst, src, sizeof(src)); // src 已退化为指针?→ 只拷 8 字节(64 位)
  9. 字符串未以 '\0' 结尾
  10. char s[3] = {'o','k'}; puts(s); // UB,未终止
  11. 把“指针数组”当“二维数组”用
  12. int *pa[3]; pa[0] = a; pa[1] = a+3; // pa[i][j] 没有统一的连续布局
  13. 把 int ** 当 int[][N] 用
  14. void f(int **p); // 不能接收 int a[M][N]
  15. 混用 realloc 后的旧指针
  16. p = realloc(p, new_sz); // 失败返回 NULL,成功返回“可能移动过”的新块 // 错误写法:if(realloc(p,...)) { ... 使用 p ... } // 泄漏/悬空
  17. VLA 太大撑爆栈
  18. void g(size_t n){ int a[n]; } // n 来自外部输入 → 风险
  19. 修改字符串字面量
  20. char *s = "abc"; s[0]='A'; // UB(多数实现只读)

七、字符串与字符数组:容量、长度与安全接口

  • 容量(capacity):数组可容纳的元素数
  • 长度(length):实际字符数,不含 '\0'

安全模板:

char buf[16];
snprintf(buf, sizeof buf, "%s-%d", tag, id); // 自动截断 + 结尾 '\0'
size_t len = strnlen(buf, sizeof buf);       // 最多扫到容量大小

避免使用:gets(已移除)、不加边界的 strcpy/strcat/sprintf。


八、实用模板与习惯用法(拿走即用)

  • 计算数组元素个数(仅在定义处使用)
  • #define COUNT_OF(a) (sizeof(a)/sizeof (a)[0])
  • 遍历模板
  • for (size_t i = 0; i < COUNT_OF(a); ++i) { /* ... */ }
  • 函数签名三选一
  • void f(const int *a, size_t n); // 一维 void g(size_t rows, int (*mat)[COLS]); // 固定列数二维 void h(size_t r, size_t c, int A[r][c]); // VLA(实现可选)
  • 动态二维(连续块)
  • int (*m)[C] = malloc(sizeof *m * R); // 使用 m[r][c] free(m);
  • 指定初始化器
  • int z[10] = {[0]=1, [3]=7}; // 其余为 0

九、迷你练习(答案要点在注释)

  1. 下面输出是什么?
int a[3] = {1,2,3};
int *p = a;
printf("%zu %zu\n", sizeof a, sizeof p); // 12(假设 4B int),8(64 位指针)
  1. 哪个是“指向 8 个 int 的数组的指针”?
int (*ap)[8]; // 
int *pa[8];   // 指针数组
  1. 为 3×5 的矩阵申请连续内存并置零?
int (*m)[5] = calloc(3, sizeof *m); // 
  1. 写一个 safe_copy:把 src 拷贝进 dst,保证以 '\0' 结尾
void safe_copy(char *dst, size_t cap, const char *src){
    if (cap==0) return;
    size_t n = strnlen(src, cap-1);
    memcpy(dst, src, n);
    dst[n]='\0';
}

十、收尾与要点回顾

  • 数组 ≠ 指针;离开定义处数组就退化为指针
  • 函数形参:用“指针+长度”、或“指向数组的指针”、或“VLA 形参”
  • 多维数组连续行主序;不要用 int ** 代替
  • 越界 = UB,不要赌运气
  • 字符串安全:容量与长度分离,优先 snprintf/strnlen

把这篇做成你的数组速查表:声明、初始化、sizeof、函数形参、二维分配、越界避坑,一把梭!

相关文章

Flutter 之 ListView(flutter框架)

在 Flutter 中,ListView 可以沿一个方向(垂直或水平方向)来排列其所有子 Widget,常被用于需要展示一组连续视图元素的场景ListView 构造方法ListView:仅适用于列表中...

Shopee新手指南:Shopee卖家中心用户界面介绍

1.Shopee各站点前台网页链接:2.Shopee各站点后台网页链接3.Shopee APP下载:安卓版下载链接:https://pan.baidu.com/s/1eSp8M1k#list/path...

C++11 同步机制:互斥锁和条件变量

前段时间,我研究了 ROS2(Jazzy)机器人开发系统,并将官网中比较重要的教程和概念,按照自己的学习顺序翻译成了中文,进行了整理和记录。到目前为止,已经整理了20多篇文章。如果你想回顾之前的内容,...

如何正确理解Java领域中的并发锁,我们应该具体掌握到什么程度?

苍穹之边,浩瀚之挚,眰恦之美; 悟心悟性,善始善终,惟善惟道! —— 朝槿《朝槿兮年说》写在开头对于Java领域中的锁,其实从接触Java至今,我相信每一位Java Developer都会有这样的一个...

本地配置plsql远程连接oracle数据库

由于Oracle的庞大,有时候我们需要在只安装Oracle客户端如plsql、toad等的情况下去连接远程数据库,可是没有安装Oracle就没有一切的配置文件去支持。最后终于发现一个很有效的方法,O...

ORA-12514 TNS 监听程序当前无法识别连接描述符中请求服务

早上同事用PL/SQL连接虚拟机中的Oracle数据库,发现又报了“ORA-12514 TNS 监听程序当前无法识别连接描述符中请求服务”错误,帮其解决后,发现很多人遇到过这样的问题,因此写着这里。也...