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

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

maynowei6个月前 (10-14)技术知识116

第 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、函数形参、二维分配、越界避坑,一把梭!

相关文章

高效办公,你值得拥有之原型工具AXURE篇

简介 Axure RP是美国Axure Software Solution公司旗舰产品,是一个专业的快速原型设计工具,让负责定义需求和规格、设计功能和界面的专家能够快速创建应用软件或Web网站的线框图...

一文弄懂 GO 的 互斥锁 Mutex !(互斥锁的使用方法)

在 Go 语言并发编程中,互斥锁(Mutex)是一个非常重要的同步原语。本文将深入介绍 Mutex 的使用方法、实现原理以及最佳实践。1. 什么是 Mutex?Mutex(互斥锁)是一种用于多线程编程...

Google前工程主管“入住”Oracle(google公司前台)

ZDNet至顶网服务器频道 10月11日 新闻消息:Oracle 已聘用了前 Snapchat 和 Google 工程部主管 Peter Magnusson,其主要的职责是运行一个被重新调整过的 of...

Oracle 不是有效的导出文件,标头验证失败 解决方法

第一种:网上搜索到的大多解决方法是说导出文件时使用的Oracle版本不一致问题,需要修改dmp文件的版本号。如果确定版本号确实不一样,请自行搜索一下解决方法。第二种:备份dmp文件时,备份的语句可能使...

你可能疏忽的plsql和navicat连接Oracle注意点

在日常开发中,我们总是少不了要连接数据库,你是否遇到过填写的账号、密码、连接地址都对,但就是连接不上Oracle的情况?这里说一下其中一种连接不上Oracle的原因,这种情况简单,但很可能被疏忽。记下...

Oracle 11g安装教程完整版(oracle 11g 安装教程)

由于工作需要,将安装的经验分享给大家。第一步:首先准备安装文件包:Oralce 11.2.0.4 64bit和plsqldev1405x64如图所示:第二步:将2个文件解压到同一个目录,如图所示:第三...