指针的三大金刚 —— 字符指针操作字符串、结构体指针操作复杂数据、引用作为变量别名,一次讲透。
用"内存三剑客"的故事建立直觉,用分身术/别名/快递单三大类比理解字符指针、结构体指针与引用,最后给出核心口诀。
小强学完普通指针后,觉得指针已经够用了。直到有一天,他遇到了三个难题:
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* 指向 char 类型的数据。它可以指向字符串常量,也可以指向字符数组。
char *p1 = "Hello"; // p1 指向字符串常量(只读)char p2[] = "Hello"; // p2 是字符数组(可修改)char *p3 = p2; // p3 指向字符数组
char *p = "NOI";// p 指向只读数据段// *p = 'X'; // 错误!段错误!
char s[] = "NOI";// s 在栈上,可修改// s[0] = 'X'; // 合法!s = "XOI"
结构体指针的定义方式与普通指针相同,只是类型是结构体类型。
struct Student { int id; char name[50]; double score;};Student s = {1, "Alice", 95.5};Student *p = &s; // p 指向结构体变量 s
-> 是箭头运算符,用于通过结构体指针访问成员。它是 (*p).member 的简写。
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
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
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
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;}
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; }}
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;}
引用是变量的别名,定义时必须初始化,且一旦绑定就不能再改。
int x = 42;int &r = x; // r 是 x 的引用(别名)// 修改 r 就是修改 xr = 100; // x 变成了 100cout << x; // 输出 100
100
| 对比项 | 指针 | 引用 |
|---|---|---|
| 定义语法 | int *p = &x; | int &r = x; |
| 可以为空 | 可以(nullptr) | 不可以(必须绑定) |
| 重新绑定 | 可以(p = &y) | 不可以(一旦绑定不可改) |
| 访问方式 | *p 或 p->成员 | 直接用 r |
| sizeof 结果 | 8(64位) | 4 或 8(取决于实现) |
| 底层实现 | 存地址 | 通常是指针的别名(编译器处理) |
int main() { char *p = "NOI2024"; int len = 0; while (*p != '\0') { cout << *p++ << " "; len++; } cout << len;}
N O I 2 0 2 4 7
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
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;}
struct Student { int score; };Student& getMax(Student &a, Student &b) { return (a.score > b.score) ? a : b;}
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
通过 3 道综合题巩固字符指针、结构体指针与引用的用法,每题含完整代码、逐行解析和运行结果。
题目:阅读以下代码,写出输出结果。
char *p = "NOI";cout << *++p << " ";cout << *p++ << " ";cout << *p;
O O I
题目:用结构体指针数组存储学生信息,按成绩降序排序后输出。
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
题目:写一个函数,用引用参数同时返回两个值:最大值和最小值。
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
回顾本课核心要点,用一张总结表快速复习,并拓展结构体指针的万能模板。
| 知识点 | 语法/要点 | 说明 |
|---|---|---|
| 字符指针 | 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; }; | 结构体指针的经典应用 |
| 奥赛技巧 | 引用 > 指针(安全优先) | 能用引用就不用指针 |
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. 结构体指针和指向结构体数组的指针有什么区别?(答案:结构体指针指向单个结构体,数组指针指向数组首元素,但两者都可以通过 -> 访问成员)
检测你对字符指针、结构体指针与引用的掌握程度,5 大易错点 + 速查表 + 知识点笔记,帮你查漏补缺。
char *p = "NOI";// *p = 'X'; // 段错误!
char s[] = "NOI";// s[0] = 'X'; // 合法!
Student *p = &s;// p.name // 错误!
Student *p = &s;// p->name // 正确!
int &r; // 错误!
int x = 42;int &r = x; // 正确
int &getRef() { int x = 42; return x; // 返回局部变量引用!
int x = 42;int &getRef() { return x; // 返回全局/静态变量引用
Student *p = &s;// *p.score // 错误!
Student *p = &s;// (*p).score // 正确!
| 操作 | 指针语法 | 引用语法 | 说明 |
|---|---|---|---|
| 定义 | int *p = &x; | int &r = x; | 指针存地址,引用是别名 |
| 访问值 | *p | r | 引用直接像变量一样用 |
| 结构体成员 | p->member | r.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 旗鼓相当的对手:双重循环+结构体指针。
通过分层练习巩固字符指针、结构体指针与引用的知识,从基础到挑战,每题含可展开答案。
完成以下练习,检验你对字符指针、结构体指针与引用的掌握程度。点击"展开答案"查看参考解法。
char *p = "NOI";cout << *++p << " ";cout << *p++ << " ";cout << *p;
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; }
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;}
char *p = "C++";while (*p) cout << *p++;思路:p 指向字符串首字符,while (*p) 遇 '\0' 停止,*p++ 先取当前字符再后移。
易错:想通过 *p 修改字符串字面量会崩溃——字面量存在只读区;条件写成 *p++ 混淆判断和移动也容易出错。
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)——那是传指针的写法,引用参数直接传变量名。
思路:从“必须初始化、能否为空、能否重新绑定”三个角度对比即可得分。
易错:说“引用和指针差不多所以引用可以为空”——引用必须绑定一个已存在的对象。
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,交换的是副本,原数据不变。
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 的赋值带不出来。
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 降序比较并整体交换结构体。
易错:比较方向写反,想降序却排成了升序;交换时忘了整个结构体一起换。
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 用引用带回最高分、最低分和平均分,排序用指针遍历交换结构体。
易错:求平均时整数除法丢小数;或循环里漏累加某个学生的成绩。