news 2026/10/21 3:29:22

C 语言基础教程(我的C之旅开始了)[四]

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C 语言基础教程(我的C之旅开始了)[四]

前言


这是「我的 C 之旅」系列的第四篇。前三篇假定你已经掌握了变量、基本类型、运算符,以及if/for/while这些控制流。第四篇把注意力转向 C 语言里最能体现「贴近内存」特色的三样东西:数组、字符串和指针,并把它们放进函数里组合使用。


先说明两点。第一,这个系列讲的是C 语言,不是 C++。文中用printf/scanf、char数组、malloc/free,不出现std::string、std::vector、引用、类这些 C++ 设施。第二,C 标准以 C17(ISO/IEC 9899:2018)为基准;gets在 C11 就已从标准中移除,本文不会使用它。


初学者最常见的误解是「数组会记住自己的长度」「字符串就是字符数组」「指针和数组是同一个东西」。这三句话都只对了一半,而错的那一半正是缓冲区溢出和越界访问的根源。本文会把边界讲清楚,所有代码都按标准语义推演,不声称经过编译或运行。


一、数组:一块连续的内存


1.1 声明、初始化与长度


/* 适用于 C17 */
#include <stdio.h>

int main(void) {
int scores[5] = {90, 85, 77, 60, 95};
int zeros[5] = {0}; /* 全部元素为 0 */
int partial[5] = {1, 2}; /* 后三个元素为 0 */
char name[] = "C"; /* 长度为 2:'C' 和 '\0' */

printf("%zu\n", sizeof scores); /* 整个数组的字节数 */
printf("%zu\n", sizeof scores[0]); /* 单个元素的字节数 */
printf("%zu\n", sizeof scores / sizeof scores[0]); /* 元素个数 */
printf("%zu\n", sizeof name); /* 2 */
return 0;
}

sizeof返回size_t,所以用%zu打印。数组长度必须是编译期常量(C99 起支持变长数组,但其大小在运行时确定,且不是所有实现都完整支持;C11 把它改为可选特性)。用sizeof求元素个数只对真正的数组有效,一旦数组作为函数参数传递,它会退化为指针,sizeof得到的是指针大小。


1.2 下标从 0 开始,且不做边界检查


/* 适用于 C17 */
int a[5];
for (int i = 0; i < 5; ++i) { /* 正确:0..4 */
a[i] = i * i;
}

a[5]越界。C 标准不要求运行时检查,越界读写是未定义行为(undefined behavior, UB):可能读到别的变量、可能崩溃、也可能看起来正常。这与 C++ 的std::vector::at()不同,C 没有「抛异常」这一层保护,边界完全由程序员负责。


二维数组是「数组的数组」,内存中仍连续存放:


/* 适用于 C17 */
int grid[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
/* grid[r][c] 等价于 *(*(grid + r) + c) */

把二维数组传给函数时,除第一维外的长度必须写进参数类型,因为编译器需要它来计算行跨度:


/* 适用于 C17 */
void printGrid(int rows, int cols, int g[][4]) {
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < cols; ++c) {
printf("%d ", g[r][c]);
}
printf("\n");
}
}

写成int **g是另一个类型,不能直接接收二维数组,这是初学者常见的类型不匹配。


二、字符串:以'\0'结尾的字符数组


2.1 两种写法,差别很大


/* 适用于 C17 */
char a[] = "hello"; /* 可修改的数组,长度 6 */
const char *b = "hello"; /* 指向字符串字面量,不要修改 */


  • char a[]在栈上分配 6 个字节,内容是h e l l o \0,可以逐字符修改。

  • const char *b指向只读区域中的字面量。通过b修改内容是 UB(很多平台上表现为段错误)。加const是让编译器帮你拦住这类修改。

  • 字符串的长度由'\0'决定,而不是数组容量。char buf[100] = "hi";的strlen是 2,但sizeof buf是 100。


2.2 常用函数与它们的边界




函数作用风险



strlen(s)数到'\0'为止s无终止符时越界读

strcpy(dst, src)复制含终止符不检查dst容量,溢出

strncpy(dst, src, n)最多复制 n 字节源串过长时可能不写'\0'

strcat(dst, src)追加不检查容量

snprintf(dst, n, fmt, ...)有界格式化返回值可能大于n,需判断截断

fgets(dst, n, stream)有界读一行可能保留换行符;n 为容量



安全写法示例:


/* 适用于 C17 */
#include <stdio.h>
#include <string.h>

int main(void) {
char dst[16];
const char *src = "a rather long string";

/* strncpy 不保证终止,手动补 */
strncpy(dst, src, sizeof dst - 1);
dst[sizeof dst - 1] = '\0';

/* 更推荐 snprintf:一定有界,且会终止 */
int n = snprintf(dst, sizeof dst, "%s", src);
if (n < 0) {
/* 编码错误 */
} else if ((size_t)n >= sizeof dst) {
/* 发生截断,dst 中是前 sizeof dst - 1 个字符 */
}
return 0;
}

snprintf的返回值是「假如空间足够,本应写入的字符数(不含'\0')」,因此它大于等于缓冲区大小时表示发生了截断,而不是写入失败。这个细节经常被误解。


读取输入时,scanf("%s", buf)不限制长度,等价于埋了一个溢出点;fgets(buf, sizeof buf, stdin)才有界。gets已从 C11 标准中删除,任何新代码都不应使用。


三、函数:值传递与声明


3.1 原型、定义与调用


/* 适用于 C17 */
#include <stdio.h>

int add(int a, int b); /* 声明(原型) */

int main(void) {
printf("%d\n", add(2, 3));
return 0;
}

int add(int a, int b) { /* 定义 */
return a + b;
}

C 要求函数在调用前可见声明。把声明放在头文件里、定义放在.c文件里,是分离编译的基础。main的标准形式是int main(void)或int main(int argc, char *argv[]),void main()从来不是标准写法。


3.2 值传递


C 只有值传递。函数收到的是一份副本,修改形参不影响实参:


/* 适用于 C17 */
void badSwap(int x, int y) {
int t = x;
x = y;
y = t; /* 只改了副本 */
}

void goodSwap(int *x, int *y) {
int t = *x;
*x = *y;
*y = t;
}

要让函数修改调用者的变量,必须传地址并用指针解引用。数组是例外:数组名作为实参会退化为指向首元素的指针,所以函数里对数组元素的修改会反映到原数组,但「数组长度」这个信息不会一起传过去,必须另加参数。


3.3 作用域与static



  • 块作用域的自动变量在离开块时生命周期结束,地址随之失效。

  • 文件作用域的static函数/变量只在本翻译单元可见,是 C 里实现「私有」的手段。

  • 函数内的static局部变量只初始化一次,生命周期贯穿整个程序:


/* 适用于 C17 */
int nextId(void) {
static int id = 0; /* 只初始化一次 */
return ++id;
}

static局部变量在多次调用之间保留值,但它不是线程安全的;多线程环境下对它的自增是数据竞争。C11 提供了<threads.h>,但是否可用取决于实现,实际项目更常用平台线程库。


四、指针初步:地址与解引用


4.1 取地址与解引用


/* 适用于 C17 */
#include <stdio.h>

int main(void) {
int value = 42;
int *p = &value; /* p 保存 value 的地址 */

printf("%d\n", *p); /* 42 */
*p = 100; /* 通过指针修改 */
printf("%d\n", value); /* 100 */
return 0;
}

*在声明里表示「指针类型」,在表达式里表示「解引用」,两者含义不同但相关。&取地址,不能作用于位域,也不能作用于寄存器变量。


4.2 指针与数组的关系


数组名在大多数表达式里会转换为指向首元素的指针,所以下面两种写法等价:


/* 适用于 C17 */
int a[5] = {10, 20, 30, 40, 50};
int *p = a; /* 等价于 &a[0] */
printf("%d\n", a[2]); /* 30 */
printf("%d\n", *(p + 2)); /* 30 */
printf("%d\n", p[2]); /* 30 */

但数组名不是指针:sizeof a是 20(假设int为 4 字节),sizeof p是指针大小;数组名不能被赋值。指针算术以元素为单位,p + 2前进2 * sizeof(int)字节。


指针相减只有在两个指针指向同一数组(或数组尾后位置)时才有定义,其他情况是 UB。


4.3 空指针与const


/* 适用于 C17 */
#include <stddef.h>

int *p = NULL; /* 空指针常量 */
const int *pc = NULL; /* 指向常量的指针:不能通过 pc 改值 */
int *const cp = NULL; /* 常量指针:cp 本身不能改指向 */

C 里用NULL(定义在<stddef.h>等头文件),C++ 才用nullptr。不要把NULL当作整数 0 使用;判断空指针写if (p == NULL)或if (!p)。


五、完整示例:统计并反转一行文本


下面把数组、字符串、函数、指针串起来:读一行、去掉换行、统计字符与单词数、原地反转。


/* 适用于 C17 */
#include <ctype.h>
#include <stdio.h>
#include <string.h>

static void stripNewline(char *s) {
size_t len = strlen(s);
if (len > 0 && s[len - 1] == '\n') {
s[len - 1] = '\0';
}
}

static size_t countWords(const char *s) {
size_t count = 0;
int inWord = 0;
for (size_t i = 0; s[i] != '\0'; ++i) {
unsigned char ch = (unsigned char)s[i];
if (isspace(ch)) {
inWord = 0;
} else if (!inWord) {
inWord = 1;
++count;
}
}
return count;
}

static void reverseInPlace(char *s) {
size_t i = 0;
size_t j = strlen(s);
while (i + 1 < j) {
--j;
char t = s[i];
s[i] = s[j];
s[j] = t;
++i;
}
}

int main(void) {
char line[128];

if (fgets(line, (int)sizeof line, stdin) == NULL) {
return 1;
}
stripNewline(line);

printf("length: %zu\n", strlen(line));
printf("words: %zu\n", countWords(line));

reverseInPlace(line);
printf("reversed: %s\n", line);
return 0;
}

逐点说明:



  • fgets最多读sizeof line - 1个字符,并保证写入'\0';输入超长时不会溢出,只是分多次读完。

  • stripNewline处理末尾换行;若输入恰好填满缓冲区,换行可能留在下一次读取里,这是fgets的正常行为。

  • countWords把char先转成unsigned char。<ctype.h>的函数只接受EOF或unsigned char范围内的值;直接传负的char是 UB。这个细节在处理非 ASCII 文本时尤为重要。

  • reverseInPlace用i + 1 < j避免在长度为偶数时多交换一次;j先自减再访问,边界是正确的。

  • 反转对多字节字符(如 UTF-8 中文)不适用,会按字节打乱。C 的char是字节,不是字符;处理 Unicode 需要专门的库或按编码规则解码。


常见坑点



  1. ❌char buf[10]; strcpy(buf, "0123456789");


✅ 目标容量不足,写出界是 UB;用snprintf(buf, sizeof buf, "%s", src)并检查截断。



  1. ❌ 认为strncpy(dst, src, n)一定会补'\0'。


✅ 源串长度不小于 n 时不会终止;手动写dst[n-1] = '\0',或改用snprintf。



  1. ❌char *s = "hello"; s[0] = 'H';


✅ 字面量通常位于只读段,修改是 UB;要改就用char s[] = "hello";,只读则写const char *。



  1. ❌ 函数里用sizeof arr求数组长度。


✅ 数组形参会退化为指针,sizeof得到指针大小;把长度作为参数显式传入。



  1. ❌while (!feof(fp)) { fgets(...); ... }


✅feof只在读失败后才置位,会多处理一次旧数据;直接判断fgets的返回值。



  1. ❌scanf("%s", buf)读取用户输入。


✅ 不限制宽度会溢出;用fgets,或用scanf("%99s", buf)明确上限。



  1. ❌ 把'a'与"a"混用。


✅'a'是字符常量(int类型),"a"是含'a'与'\0'的字符串字面量,二者不能互换。



  1. ❌ 返回局部数组的地址。


✅ 函数返回后自动存储期结束,指针悬垂;改为由调用者传入缓冲区,或使用malloc并由调用者free。


总结




主题关键规则常见错误



数组下标 0..n-1,不做边界检查越界读写(UB)

数组传参退化为指针,长度另传在函数内sizeof求长度

字符串以'\0'结尾忘记终止符、缓冲区不足

输入fgets有界用gets或无限宽%s

函数只有值传递以为形参能改实参

指针&取地址、*解引用悬垂指针、空指针解引用

const区分指向常量的指针与常量指针通过const指针改写

生命周期局部变量离开作用域即失效返回局部数组地址



C 的数组、字符串、指针本质上是同一件事的三个视角:一段连续内存、一个终止约定、一个地址。把「谁拥有这块内存、它有多长、什么时候失效」这三个问题随时问自己,大部分崩溃都能在写代码时避免。下一讲会在这个基础上讨论结构体与动态内存,那才是 C 程序组织数据的真正起点。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 3:25:04

用11个MCP服务器把Claude Desktop变成一体化创作工作台

我搭这个一体化创作站&#xff0c;起因其实特别俗&#xff1a;一天要在对话框、浏览器、文件管理器、待办清单、修图工具之间来回切几十次。某天我在 Claude Desktop 里问它能不能帮我整理一下本地散落的资料&#xff0c;它沉默了一会儿&#xff0c;回了一句让我印象很深的话&a…

作者头像 李华
网站建设 2026/10/21 3:25:04

计算机二级C语言备考指南:从语法基础到上机编程的完整复习路线

简介&#xff1a;这份资源面向零基础或刚入门的编程学习者&#xff0c;围绕计算机二级C语言考试与Go语言基础学习两条主线整理&#xff0c;帮助读者在备考二级的同时建立对Go语言语法、模板渲染、变量与切片结构等内容的系统认知。压缩包共收录471个文件&#xff0c;整体约90.4…

作者头像 李华
网站建设 2026/10/21 3:24:50

Agentic AI落地智能制造:提示工程架构师的实践指南

最近收到了一份关于Agentic AI在智能制造落地的案例集&#xff0c;整理得很扎实&#xff0c;里面的场景拆分、提示词框架和踩坑记录都有实操参考价值。我花了两个晚上挨个跑了一遍核心场景&#xff0c;挑了些能直接迁移到自有业务里的思路&#xff0c;结合这些年做提示工程的经…

作者头像 李华
网站建设 2026/10/21 3:24:35

多智能体流水线:3小时从需求文档生成架构图的实操指南

从需求文档到架构图&#xff0c;过去我至少要磨三天&#xff1b;最近用一套多智能体流水线&#xff0c;直接把首版架构图的时间压到了3小时。这事不是我拍脑袋吹牛&#xff0c;而是我把流程拆开、把每个环节的耗时算明白之后&#xff0c;跑出来的实操套路。如果你是架构师、技术…

作者头像 李华
网站建设 2026/10/21 3:23:34

Windows加载DLL报0x800711C7?应用程序控制策略拦截排查指南

做Windows系统维护久了&#xff0c;几乎都会撞上这么一堵墙&#xff1a;某个软件启动到一半&#xff0c;或者某个插件加载的时候&#xff0c;程序僵住&#xff0c;紧接着要么弹一个错误窗口&#xff0c;要么直接看事件日志&#xff0c;里面躺着一条HRESULT:0x800711C7&#xff…

作者头像 李华
网站建设 2026/10/21 3:22:51

海康威视Python移动侦测对接:ISAPI事件订阅与自动抓图实践

简介&#xff1a;这份压缩包面向海康威视网络摄像头二次开发初学者与有 Python 基础的开发者&#xff0c;聚焦通过官方 SDK 实现移动侦测事件订阅、报警回调与告警数据解析&#xff0c;对应博客教程“直接使用”部分&#xff0c;能快速绕过环境配置与接口联调障碍。包内共15个文…

作者头像 李华