NOI C++ 课程体系 / 语法入门 / 第8讲
CSP-J 入门级 · 语法入门 · 指针进阶

C++ 字符指针、结构体指针与引用 完全指南

指针的三大金刚 —— 字符指针操作字符串、结构体指针操作复杂数据、引用作为变量别名,一次讲透。

6 大教学模块 3 种指针类型 5 个精讲案例 5 大易错点 1 张速查表 15 道分层练习
本讲 · 第 22/56 讲字符指针、结构体指针与引用
模块 01

情景导航

用"内存三剑客"的故事建立直觉,用分身术/别名/快递单三大类比理解字符指针、结构体指针与引用,最后给出核心口诀。

📖 故事引入:小强的"内存三剑客"

小强学完普通指针后,觉得指针已经够用了。直到有一天,他遇到了三个难题:

1. 他想修改一个字符串,但发现字符串常量不能被修改 —— 这时候需要字符指针来帮忙。
2. 他定义了一个结构体学生,想写一个函数修改学生的成绩 —— 用普通指针太麻烦,需要结构体指针和->运算符。
3. 他写函数想返回两个值,但return只能返回一个 —— 这时候引用派上用场了!

于是,小强决定学习指针的三大金刚:字符指针、结构体指针和引用。

💡 三大类比的智慧
📄

字符指针 = 快递单上的地址

字符串常量存在内存的某个位置,字符指针就是写着那个位置地址的"快递单",可以按地址找到并读取字符。

char* p = "NOI"; p 指向 'N'
📦

结构体指针 = 档案袋标签

结构体是一个"档案袋",里面装了很多信息。结构体指针就是档案袋上的标签,用 -> 可以打开标签找到里面的信息。

p->name 等价于 (*p).name
🔑

引用 = 小名/别名

你叫"王心俊",朋友叫你"心俊哥"。两个名字指向同一个人。引用就是变量的"小名",改了别名就等于改了原名。

int &r = x; r 就是 x 的别名
🛣 生活类比:分身与别名
📂

字符指针遍历字符串

就像你沿着街道一家家看门牌号,字符指针从字符串首字符开始,逐个字符往后走,直到遇到 '\0'。

while(*p++) 逐个字符遍历
📦

结构体指针操作成员

就像你拿到一个员工的工牌(指针),用 -> 就可以查看他的姓名、工号、部门等信息。

emp->salary 查看工资
🔑

引用交换变量

就像两个人互换外套。你给他你的外套(值),他给你他的外套,表面上看你们互换了,实际上外套没变,只是"名字"换了。

void swap(int &a, int &b)
📄 字符指针

字符指针是最简单的指针类型之一,它指向一个 char 类型的变量或字符串常量。

char * p = "NOI2024"
char 类型 指针声明符 指针变量名 字符串常量地址

注意:字符串常量存储在只读数据段,不能通过指针修改它的内容!

📦 结构体指针

结构体指针就是指向结构体变量的指针。访问结构体成员时,使用 -> 运算符(箭头运算符)。

struct Student { int id; char name[50]; };
Student s; Student *p = &s;
结构体类型 结构体指针 结构体变量 取地址
🔑 引用

引用是变量的别名,定义时必须初始化。引用一旦绑定到某个变量,就不能再改绑。

int x = 42;
int &r = x; // r 是 x 的别名
int 类型 引用声明符 引用名 绑定到 x
🔑 核心口诀
指针三剑客口诀 char* p 指向字符串,*p 逐个读字符;
struct* p 指向结构体,p-> 成员取个遍;
int& r 是变量别名,改 r 就是改原值;
引用必须初始化,指针可以空着来。
模块 02

知识探究

深入字符指针的内存模型、字符串常量与字符数组的区别、结构体指针的 -> 运算符、引用的本质,以及引用与指针的对比。

📄 字符指针定义与内存布局

字符指针 char* 指向 char 类型的数据。它可以指向字符串常量,也可以指向字符数组。

C++字符指针定义
char *p1 = "Hello";  // p1 指向字符串常量(只读)char p2[] = "Hello";  // p2 是字符数组(可修改)char *p3 = p2;        // p3 指向字符数组
"Hello" 字符串常量 → 存储在 只读数据段
p1 存的是 "Hello" 的首地址
p2[] 在栈上创建了一份副本,可修改
p3 指向 p2 的首地址
⚠ 字符串常量 vs 字符数组
字符串常量(不可修改)
char *p = "NOI";// p 指向只读数据段// *p = 'X';  // 错误!段错误!
字符数组(可修改)
char s[] = "NOI";// s 在栈上,可修改// s[0] = 'X';  // 合法!s = "XOI"
关键区别:字符串常量存在只读段,通过指针不能修改;字符数组存在栈上,可以通过指针修改。
📦 结构体指针定义

结构体指针的定义方式与普通指针相同,只是类型是结构体类型。

C++结构体指针定义
struct Student {    int id;    char name[50];    double score;};Student s = {1, "Alice", 95.5};Student *p = &s;  // p 指向结构体变量 s
→ -> 运算符详解

-> 是箭头运算符,用于通过结构体指针访问成员。它是 (*p).member 的简写。

p->name == (*p).name
p->id == (*p).id
p->score == (*p).score
箭头运算符(推荐) 等价于 解引用 + 点运算符
C++-> 运算符使用
Student s = {1, "Alice", 95.5};Student *p = &s;cout << p->id << " ";cout << p->name << " ";cout << p->score;// 输出: 1 Alice 95.5
运行结果
1 Alice 95.5
📂 结构体指针案例

案例 1:用结构体指针修改成员

C++指针修改结构体成员
struct Student {    int id; char name[50]; double score;};void updateScore(Student *p, double newScore) {    p->score = newScore;  // 通过指针修改}int main() {    Student s = {1, "Alice", 95.5};    updateScore(&s, 98.0);    cout << s.score;  // 输出 98}
运行结果
98

案例 2:结构体指针数组遍历

C++结构体指针数组
struct Student { int id; char name[50]; double score; };int main() {    Student arr[3] = {        {1, "Alice", 95.5},        {2, "Bob", 87.0},        {3, "Charlie", 92.3}    };    Student *p = arr;  // 指向首元素    for (int i = 0; i < 3; i++) {        cout << (p+i)->id << " "             << (p+i)->name << " "             << (p+i)->score << "\n";    }}
运行结果
1 Alice 95.5
2 Bob 87
3 Charlie 92.3

案例 3:结构体指针排序(洛谷 P5740 最厉害的学生)

C++结构体指针排序
struct Student { int id; char name[50]; int score; };int main() {    int n; cin >> n;    Student *a = new Student[n];    for (int i = 0; i < n; i++)        cin >> a[i].id >> a[i].name >> a[i].score;    // 用指针排序:按分数降序    for (int i = 0; i < n-1; i++)        for (int j = 0; j < n-i-1; j++)            if (a[j].score < a[j+1].score) {                Student t = a[j]; a[j] = a[j+1]; a[j+1] = t;            }    for (int i = 0; i < n; i++)        cout << a[i].id << " " << a[i].name << " " << a[i].score << "\n";    delete[] a;}

案例 4:结构体指针实现链表节点

C++链表节点
struct Node {    int val;    Node *next;};Node *head = nullptr;void insert(int val) {    Node *p = new Node{val, head};    head = p;}void print() {    Node *p = head;    while (p) {        cout << p->val << " ";        p = p->next;    }}

案例 5:结构体指针返回最大值

C++返回指向最大元素的指针
struct Student { int id; char name[50]; int score; };Student* findMax(Student a[], int n) {    Student *maxP = a;    for (Student *p = a + 1; p < a + n; p++) {        if (p->score > maxP->score) maxP = p;    }    return maxP;}
🔑 引用定义与本质

引用是变量的别名,定义时必须初始化,且一旦绑定就不能再改。

C++引用基本操作
int x = 42;int &r = x;     // r 是 x 的引用(别名)// 修改 r 就是修改 xr = 100;        // x 变成了 100cout << x;     // 输出 100
运行结果
100
⚖ 引用 vs 指针对比
对比项指针引用
定义语法int *p = &x;int &r = x;
可以为空可以(nullptr)不可以(必须绑定)
重新绑定可以(p = &y)不可以(一旦绑定不可改)
访问方式*p 或 p->成员直接用 r
sizeof 结果8(64位)4 或 8(取决于实现)
底层实现存地址通常是指针的别名(编译器处理)
奥赛建议:函数传参时,大对象用引用(更简洁安全),需要可选参数时用指针(可以为空)。
📂 综合案例

案例 1:字符指针遍历字符串

C++指针遍历字符串
int main() {    char *p = "NOI2024";    int len = 0;    while (*p != '\0') {        cout << *p++ << " ";        len++;    }    cout << len;}
运行结果
N O I 2 0 2 4 7

案例 2:引用交换两个数

C++引用交换
void swap(int &a, int &b) {    int t = a; a = b; b = t;}int main() {    int x = 3, y = 5;    swap(x, y);    cout << x << " " << y;}
运行结果
5 3

案例 3:结构体指针 + 引用混合使用

C++结构体指针与引用
struct Student { int id; char name[50]; int score; };void printRef(Student &s) {  // 引用传参    cout << s.id << " " << s.name << " " << s.score;}void printPtr(Student *p) {  // 指针传参    cout << p->id << " " << p->name << " " << p->score;}

案例 4:引用返回结构体引用

C++返回引用
struct Student { int score; };Student& getMax(Student &a, Student &b) {    return (a.score > b.score) ? a : b;}

案例 5:字符指针 + 结构体指针综合

C++综合应用
struct Student { int id; char name[50]; int score; };int main() {    Student arr[3] = {        {1, "Alice", 95}, {2, "Bob", 87}, {3, "Charlie", 92}    };    Student *p = arr;    char *msg = "Top student: ";    cout << msg;    for (int i = 0; i < 3; i++) {        if ((p+i)->score > 90)            cout << (p+i)->name << " ";    }}
运行结果
Top student: Alice Charlie
模块 03

实践应用

通过 3 道综合题巩固字符指针、结构体指针与引用的用法,每题含完整代码、逐行解析和运行结果。

⛽ 题目 1:字符指针追踪输出

题目:阅读以下代码,写出输出结果。

C++字符指针追踪
char *p = "NOI";cout << *++p << " ";cout << *p++ << " ";cout << *p;
查看解析与答案
解析:
1. p 初始指向 'N'("NOI" 首字符)
2. *++p:先 p++(p 指向 'O'),再解引用 → 输出 O
3. *p++:先解引用 *p(值 'O'),再 p++(p 指向 'I') → 输出 O
4. *p:p 当前指向 'I' → 输出 I
运行结果
O O I
⛽ 题目 2:结构体指针数组排序

题目:用结构体指针数组存储学生信息,按成绩降序排序后输出。

C++结构体指针排序
struct Student { int id; char name[50]; int score; };int main() {    Student arr[3] = {        {1, "Alice", 95}, {2, "Bob", 87}, {3, "Charlie", 92}    };    Student *p = arr;    // 冒泡排序(用指针访问)    for (int i = 0; i < 2; i++)        for (int j = 0; j < 2-i; j++)            if ((p+j)->score < (p+j+1)->score) {                Student t = *(p+j); *(p+j) = *(p+j+1); *(p+j+1) = t;            }    for (int i = 0; i < 3; i++)        cout << (p+i)->name << " " << (p+i)->score << "\n";}
运行结果
Alice 95
Charlie 92
Bob 87
思路:用结构体指针 p 指向数组首元素,通过 (p+j)->score 访问每个学生的成绩进行比较和交换。
⛽ 题目 3:引用实现多返回值

题目:写一个函数,用引用参数同时返回两个值:最大值和最小值。

C++引用多返回值
void getMinMax(int a[], int n, int &mx, int &mn) {    mx = mn = a[0];    for (int i = 1; i < n; i++) {        if (a[i] > mx) mx = a[i];        if (a[i] < mn) mn = a[i];    }}int main() {    int a[] = {3, 7, 2, 9, 5};    int mx, mn;    getMinMax(a, 5, mx, mn);    cout << "Max:" << mx << " Min:" << mn;}
运行结果
Max:9 Min:2
模块 04

总结提升

回顾本课核心要点,用一张总结表快速复习,并拓展结构体指针的万能模板。

📊 核心要点总结
知识点语法/要点说明
字符指针char* p = "str";指向字符串常量(只读)
字符串常量 vs 数组char* p vs char s[]常量不可修改,数组可修改
结构体指针struct* p = &s;指向结构体变量的指针
-> 运算符p->member等价于 (*p).member
引用定义int& r = x;r 是 x 的别名,必须初始化
引用 vs 指针引用不能为空,指针可以引用更安全,指针更灵活
引用传参void f(int& a)修改参数即修改原变量
结构体指针数组(p+i)->member指针遍历结构体数组
链表节点struct Node { int val; Node* next; };结构体指针的经典应用
奥赛技巧引用 > 指针(安全优先)能用引用就不用指针
📦 拓展案例:结构体指针万能模板
C++结构体指针遍历模板
struct Student { int id; char name[50]; int score; };void processStudents(Student a[], int n) {    Student *p = a;    for (int i = 0; i < n; i++) {        // 用指针访问每个结构体成员        cout << (p+i)->id << " "             << (p+i)->name << " "             << (p+i)->score << "\n";    }}// 调用方式int main() {    Student data[] = {        {1, "Alice", 95}, {2, "Bob", 87}    };    processStudents(data, 2);}
万能模板记忆法:Student *p = a; for (int i=0; i<n; i++) (p+i)->member —— "指针从开头走到末尾,(p+i)-> 就是当前结构体的成员"
💡 思考与启发

💡 思考题

1. 为什么字符串常量不能通过指针修改?(答案:字符串常量存储在只读数据段,修改会导致段错误)

2. 引用和指针在底层实现上有什么区别?(答案:引用通常被编译器实现为指针的别名,但语法上更简洁安全)

3. 结构体指针和指向结构体数组的指针有什么区别?(答案:结构体指针指向单个结构体,数组指针指向数组首元素,但两者都可以通过 -> 访问成员)

模块 05

学习检测

检测你对字符指针、结构体指针与引用的掌握程度,5 大易错点 + 速查表 + 知识点笔记,帮你查漏补缺。

⚠ 5 大易错点
ERROR 01

修改字符串常量

错误写法
char *p = "NOI";// *p = 'X';  // 段错误!
正确写法
char s[] = "NOI";// s[0] = 'X';  // 合法!
ERROR 02

-> 与 . 混淆

错误写法
Student *p = &s;// p.name  // 错误!
正确写法
Student *p = &s;// p->name  // 正确!
ERROR 03

引用未初始化

错误写法
int &r;    // 错误!
正确写法
int x = 42;int &r = x;  // 正确
ERROR 04

悬空引用

错误写法
int &getRef() {    int x = 42;    return x;  // 返回局部变量引用!
正确写法
int x = 42;int &getRef() {    return x;  // 返回全局/静态变量引用
ERROR 05

结构体指针解引用优先级

错误写法
Student *p = &s;// *p.score  // 错误!
正确写法
Student *p = &s;// (*p).score  // 正确!
📊 速查表
操作指针语法引用语法说明
定义int *p = &x;int &r = x;指针存地址,引用是别名
访问值*pr引用直接像变量一样用
结构体成员p->memberr.member指针用 ->,引用用 .
可以为空p = nullptr;不允许引用必须绑定有效变量
重新绑定p = &y;不允许引用一旦绑定不可更改
传参void f(int *p)void f(int &r)都能修改原变量
数组遍历*(p+i)r[i]引用可直接用下标
📌 知识点笔记
📄 字符指针

char* 指向字符串常量时不可修改内容。用 char s[] 定义字符数组可修改。遍历字符串用 while(*p++)。

📦 结构体指针

struct* p = &s; 用 -> 访问成员。p->name 等价于 (*p).name。指向数组时用 (p+i)->member 遍历。

🔑 引用

int &r = x; r 是 x 的别名。必须初始化,不能重新绑定。函数传参用引用可避免拷贝大对象。

⚠ 安全原则

字符串常量不修改,指针用 nullptr 初始化,引用必须初始化,delete 后置空指针。

📏 奥赛技巧

结构体排序用指针遍历更高效,引用传参避免大对象拷贝,-> 运算符比 (*p).member 更简洁。

📚 洛谷真题

P5740 最厉害的学生:结构体指针排序。P5744 培训:指针遍历更新。P5741 旗鼓相当的对手:双重循环+结构体指针。

模块 06

练习作业

通过分层练习巩固字符指针、结构体指针与引用的知识,从基础到挑战,每题含可展开答案。

课后练习

练习作业

完成以下练习,检验你对字符指针、结构体指针与引用的掌握程度。点击"展开答案"查看参考解法。

⛽ 总结练习(3 题)
基础字符指针第 1 题

阅读以下代码,写出输出结果

C++字符指针追踪
char *p = "NOI";cout << *++p << " ";cout << *p++ << " ";cout << *p;
展开答案
答案:O O I
*++p:先 p++ 指向 'O',解引用输出 O;*p++:先解引用 *p('O'),再 p++ 指向 'I';*p 输出 I。
提高结构体指针第 2 题

用结构体指针数组存储 3 个学生信息,按成绩降序排序并输出

展开答案
struct Student { int id; char name[50]; int score; };Student arr[3] = {{1,"A",95},{2,"B",87},{3,"C",92}};Student *p = arr;for (int i=0; i<2; i++)    for (int j=0; j<2-i; j++)        if ((p+j)->score < (p+j+1)->score) {            Student t = *(p+j); *(p+j) = *(p+j+1); *(p+j+1) = t;        }
挑战引用综合第 3 题

用引用参数实现一个函数,同时返回数组的最大值、最小值和平均值

展开答案
void getStats(int a[], int n, int &mx, int &mn, double &avg) {    mx = mn = a[0]; double sum = a[0];    for (int i = 1; i < n; i++) {        if (a[i] > mx) mx = a[i];        if (a[i] < mn) mn = a[i];        sum += a[i];    }    avg = sum / n;}
📦 A 组 · 基础题(4 题)
基础字符指针

定义一个字符指针 p,让它指向字符串 "C++",然后逐个字符输出

展开答案
char *p = "C++";while (*p) cout << *p++;

思路:p 指向字符串首字符,while (*p) 遇 '\0' 停止,*p++ 先取当前字符再后移。

易错:想通过 *p 修改字符串字面量会崩溃——字面量存在只读区;条件写成 *p++ 混淆判断和移动也容易出错。

基础结构体指针

定义一个结构体 Student,用指针访问其成员并输出

展开答案
struct Student { int id; char name[50]; };Student s = {1, "Alice"};Student *p = &s;cout << p->id << " " << p->name;

思路:Student *p = &s; 后用 p->id 访问成员,等价于 (*p).id。

易错:写成 p.id(指针不能用点号)或 *p.id(优先级错),都会编译报错。

基础引用

用引用实现一个函数,将传入的整数翻倍

展开答案
void doubleVal(int &r) { r *= 2; }// 调用:doubleVal(x);

思路:int& r 是实参的别名,r *= 2 直接修改原变量,调用时写 doubleVal(x) 即可。

易错:调用写成 doubleVal(&x)——那是传指针的写法,引用参数直接传变量名。

基础引用 vs 指针

写出引用和指针在定义、访问、重新绑定方面的 3 个区别

展开答案
答案:1. 引用必须初始化,指针可以后赋值;2. 引用不能为空,指针可以为 nullptr;3. 引用不能重新绑定,指针可以指向其他变量。

思路:从“必须初始化、能否为空、能否重新绑定”三个角度对比即可得分。

易错:说“引用和指针差不多所以引用可以为空”——引用必须绑定一个已存在的对象。

📂 B 组 · 提高题(4 题)
提高结构体指针

用结构体指针数组实现学生成绩管理系统(输入 n 个学生,输出最高分学生信息)

展开答案
struct Student { int id; char name[50]; int score; };Student *a = new Student[n];Student *maxP = a;for (Student *p = a+1; p < a+n; p++)    if (p->score > maxP->score) maxP = p;cout << maxP->name << " " << maxP->score;

思路:new Student[n] 开数组,指针遍历比较 p->score,记录最高分学生的指针。

易错:遍历条件写成 p <= a+n,会多访问一个越界元素。

提高字符指针

用字符指针实现字符串的反转(原地修改,使用头尾指针)

展开答案
void reverse(char s[]) {    int n = strlen(s);    char *left = s, *right = s + n - 1;    while (left < right) {        char t = *left; *left = *right; *right = t;        left++; right--;    }}

思路:头尾双指针向中间走,交换 *left 与 *right,直到 left >= right。

易错:循环条件写成 left != right——偶数长度时两指针交叉后会再交换一次,等于没反转。

提高引用传参

用引用实现一个函数,交换两个结构体变量的值

展开答案
void swap(Student &a, Student &b) {    Student t = a; a = b; b = t;}

思路:参数用 Student& 传引用,借助临时变量 t 完成两个结构体的整体交换。

易错:参数写成传值 Student a, Student b,交换的是副本,原数据不变。

提高综合

用结构体指针和引用混合实现:输入 n 个学生,用引用返回最高分和最低分的学生指针

展开答案
Student* findBest(Student a[], int n, Student* &worst) {    Student *best = a, *p = a;    worst = a;    for (p = a+1; p < a+n; p++) {        if (p->score > best->score) best = p;        if (p->score < worst->score) worst = p;    }    return best;}

思路:Student* &worst 是“指针的引用”,函数内修改 worst,主函数的实参指针跟着变。

易错:少写一个 &,变成传指针的值,函数里对 worst 的赋值带不出来。

💡 C 组 · 挑战题(4 题)
挑战链表

用结构体指针实现一个简单的单向链表,支持插入、遍历和删除操作

展开答案
struct Node { int val; Node *next; };Node *head = nullptr;void insert(int val) {    Node *p = new Node{val, head};    head = p;}void removeFirst() {    if (head) { Node *t = head; head = head->next; delete t; }}

思路:头插法:新节点 next 指向原 head,再让 head 指向新节点,插入、删除注意维护指针顺序。

易错:先改 head 再接 next 会丢失原链表,顺序不能反;删除节点后要 delete 防泄漏。

挑战指针数组

用指针数组实现多个字符串的排序输出(按字典序)

展开答案
const char *names[] = {"NOI", "CSP", "IOI", "USACO"};int n = 4;for (int i=0; i<n-1; i++)    for (int j=0; j<n-i-1; j++)        if (strcmp(names[j], names[j+1]) > 0) {            const char *t = names[j]; names[j] = names[j+1]; names[j+1] = t;        }

思路:交换的是指针(地址),比较字符串内容用 strcmp 决定是否对调。

易错:用 > 直接比较两个 char*,比较的是地址而不是字典序。

挑战结构体指针+引用

用结构体指针和引用实现一个学生成绩管理系统:支持添加学生、按分数排序、输出排名

展开答案
struct Student { int id; char name[50]; int score; };void sortStudents(Student *a, int n) {    for (int i=0; i<n-1; i++)        for (int j=0; j<n-i-1; j++)            if (a[j].score < a[j+1].score) {                Student t = a[j]; a[j] = a[j+1]; a[j+1] = t;            }}

思路:传数组首地址 Student *a 和长度 n,冒泡按 score 降序比较并整体交换结构体。

易错:比较方向写反,想降序却排成了升序;交换时忘了整个结构体一起换。

挑战综合应用

用结构体指针实现一个简易的学生成绩管理系统(输入 n 个学生成绩,用引用返回最高分、最低分和平均分,用指针排序输出)

展开答案
struct Student { int id; char name[50]; int score; };void analyze(Student *a, int n, int &mx, int &mn, double &avg) {    mx = mn = a[0].score; double sum = a[0].score;    for (int i=1; i<n; i++) {        if (a[i].score > mx) mx = a[i].score;        if (a[i].score < mn) mn = a[i].score;        sum += a[i].score;    }    avg = sum / n;}

思路:analyze 用引用带回最高分、最低分和平均分,排序用指针遍历交换结构体。

易错:求平均时整数除法丢小数;或循环里漏累加某个学生的成绩。