第七章 指针
关键考点总览
跳转到:关键考点总览- 指针的定义与初始化(取地址
&、解引用*、空指针nullptr/NULL) - 指针运算与数组名的关系(
p+k的计算规则、数组名在多数表达式中会“衰变”为指向首元素的指针) - 字符串与字符指针的使用
- 动态内存分配(
new/delete、内存泄漏) - 指针作为函数形参(实现数据共享、多返回值)
- 引用类型与函数参数传递
- 指针数组与多级指针(二级指针、动态二维数组)
- 函数指针(定义、调用、作为参数传递)
- 常量指针与指针常量的区别
void*通配指针与强制类型转换
详细知识点
跳转到:详细知识点7.1 指针的概念
跳转到:7.1 指针的概念定义/语法
跳转到:定义/语法-
指针:存储地址的变量,可用于间接访问对象、在函数间共享/修改数据,并配合动态内存分配管理运行期对象(如动态数组)。 [来源:第7章 p8,指针介绍]
-
指针定义格式:
类型 *指针名;需告诉编译器:(1) 该变量是一个指针;(2) 指针指向的数据是什么类型。 [来源:第7章 p10,定义指针]
-
定义示例:
int *intp;double *doublep;int *p, x, *q; // p和q是指针,x是普通int变量[来源:第7章 p10,定义指针]
规则/规范
跳转到:规则/规范-
取地址操作符
&:返回变量的地址,&x返回变量 x 的地址。&只能用于“有确定存储位置”的对象(例如具名变量等左值);对字面量/临时对象通常非法,如&2非法。 [来源:第7章 p11,指针的操作]
-
解引用操作符
*:通过指针间接访问它所指向的单元,*intp表示 intp 指向的变量。int x, *intp = &x;*intp = 5; // 等价于 x = 5;[来源:第7章 p11,指针的操作]
-
空指针初始化:推荐使用
nullptr表示“不指向任何对象”的空指针常量;NULL在 C++ 中通常是0/0L的宏,可能与整数重载混淆,不建议优先使用。 [来源:第7章 p12,指针的初始化]
易错点与辨析
跳转到:易错点与辨析-
⚠️ 未初始化指针的危险:未初始化指针的值不确定;对其解引用属于未定义行为(可能崩溃、数据破坏或表现“看似正常”)。
int *p;*p = 5; // 严重错误!p未初始化[来源:第7章 p12,指针的初始化]
-
⚠️ 指针初始化与赋值的区别:
- 初始化:
int *p = &x;(解引用符*与取址符&配对) - 赋值:
p = &y;(指针变量名与取址符&配对,不加*) [来源:第7章 p13,Notes]
- 初始化:
例题结论
跳转到:例题结论-
【习题分析】 以下代码的问题:
int *p = 10;→ 错误:不能存 int 类型给指针int *p = &10;→ 错误:常量没有内存地址int *p = *10;→ 错误:解引用*只能对地址使用*p = &x;→ 错误:*p是 int 类型,不能赋地址值 [来源:第7章 p14,习题 Notes答案]
-
【正确写法】 定义指针指向变量并间接修改:
int num = 10;int *p = #*p += 5; // num变成15[来源:第7章 p14,习题 Notes答案]
7.2 void* 通配指针
跳转到:7.2 void* 通配指针定义/语法
跳转到:定义/语法- 通配指针定义:当定义指针但不关心被指向变量类型时使用。
[来源:第7章 p15,void*通配指针类型]void *指针变量名;
规则/规范
跳转到:规则/规范- 其他类型的指针可以赋值给 void 类型指针。
- 把 void 类型指针赋给其他类型指针时,必须做强制类型转换。
[来源:第7章 p15,void*通配指针类型]void *pointer;int *x, y;x = &y;pointer = x; // 合法x = static_cast<int*>(pointer); // C++ 推荐写法
易错点与辨析
跳转到:易错点与辨析- ⚠️ 不同类型指针之间不能直接赋值:
[来源:第7章 p15,void*通配指针类型]int *x;double *tem;tem = x; // 错误:cannot convert 'int*' to 'double*'
7.3 指针运算和数组名
跳转到:7.3 指针运算和数组名定义/语法
跳转到:定义/语法-
指针加/减法:若 p 指向数组元素,则
p±k会按元素大小移动(等价于地址偏移k * sizeof(*p)字节);该运算仅在同一数组对象范围内(含 one-past-the-end)才有定义。 [来源:第7章 p17,指针运算] -
数组名与指针的关系:数组名本质是数组对象;在多数表达式中会衰变为指向首元素的指针(因此不能对数组名赋值或自增)。
int a[5] = {1,2,3,4,5}, *p;p = a; // p指向数组首地址[来源:第7章 p18,指针与数组名]
规则/规范
跳转到:规则/规范-
指针访问数组元素的等价写法:
p[i]等价于*(p+i)[来源:第7章 p18,指针与数组名] -
合法的数组遍历方式:
for (i=0; i<5; ++i) cout << a[i] << ' ';for (i=0; i<5; ++i) cout << p[i] << ' ';for (i=0; i<5; ++i) cout << *(a+i) << ' ';for (i=0; i<5; ++i) cout << *(p+i) << ' ';for (p=a; p<a+5; ++p) cout << *p << ' ';[来源:第7章 p18,指针与数组名]
-
非法遍历方式:数组名是常量,不能对其进行自增操作。
for (i=0; i<5; ++i) {cout << *a << ' ';++a; // 错误!数组名是常量地址}[来源:第7章 p18,指针与数组名]
-
数组作为函数形参:实质上是指针作为形参,以下两种写法等价:
int fun(int p[], int size);int fun(int *p, int size);传递的是数组起始地址,数组大小需要另外一个参数指定。 [来源:第7章 p20,数组传递的进一步讨论]
例题结论
跳转到:例题结论-
【习题分析】 假设
int arr[5] = {1, 2, 3, 4, 5}:arr→ 首地址arr[2]→ 第三个元素(值为3)&arr[2]→ 第三个元素的地址arr + 2→ 第三个元素的地址*(arr+2)→ 第三个元素(值为3)*(arr+2)*2→ 第三个元素的两倍(值为6) [来源:第7章 p19,习题 Notes答案]
-
【执行结果分析】
int arr[] = {10, 20, 30, 40, 50};int *p = arr + 2;cout << *p << " " << p[2];输出:
30 50(*p是 arr[2]=30,p[2]是 *(p+2)=arr[4]=50) [来源:第7章 p19,习题 Notes答案] -
【执行结果分析】 排序函数调用:
#include <algorithm>// 只对前5个元素排序int a1[10] = {9,8,7,6,5,4,3,2,1,0};std::sort(a1, a1 + 5);// 结果:5 6 7 8 9 4 3 2 1 0(前5个有序)// 只对后5个元素排序int a2[10] = {9,8,7,6,5,4,3,2,1,0};std::sort(a2 + 5, a2 + 10);// 结果:9 8 7 6 5 0 1 2 3 4(后5个有序)[来源:第7章 p20,数组传递的进一步讨论]
7.4 字符串再讨论
跳转到:7.4 字符串再讨论定义/语法
跳转到:定义/语法- 字符指针引用字符串:常用一个字符指针来引用一个字符串,从该指针所指向的字符开始,直到
'\0'字符结束。[来源:第7章 p22,字符串再讨论]char bstr[] = "201811122119";char *p = bstr;cout << p << endl; // 输出整个字符串cout << *p << endl; // 输出第一个字符 '2'
规则/规范
跳转到:规则/规范-
字符串以
'\0'结束,只需确定字符串开始的地址即可,不需要指定长度。 [来源:第7章 p35,Notes] -
字符串字面量具有静态存储期,通常放在只读存储区;尝试通过指针修改其内容属于未定义行为(应使用
const char*接收)。 [来源:第7章 p23,Notes]
易错点与辨析
跳转到:易错点与辨析- ⚠️ 字符串操作错误示例:
[来源:第7章 p23,字符串常量]const char *pt1 = "1234"; // 字符串字面量应以 const char* 接收char pt2[] = "12"; // pt2 是字符数组(容量为3,含 '\0')const char *pt3 = "34";pt3 = pt2; // A: 合法,指针可以重新指向别处// pt2 = pt1; // B: 错误!数组名不可赋值strcpy(pt2, pt1); // C: 错误!数组溢出(pt2 只有3字节)
例题结论
跳转到:例题结论-
【执行结果分析】 strcpy 返回值应用:
char bString[80], aString[80] = "201811122119";strcpy(bString, aString)[2] = 'A'; // bString = "20A811122119"strcpy(bString, "Hello, world!"); // bString = "Hello, world!"strcpy(bString, aString+5); // bString = "1122119"[来源:第7章 p22,Notes答案]
-
【填空题答案】 将数字字符串转换为整数:
int func(char* str) {int num = 0, digital;while (*str >= '0' && *str <= '9') { // 填空1digital = *str - '0'; // 填空2num = num * 10 + digital;str++;}return num;}[来源:第7章 p25,习题 Notes答案]
7.5 动态内存分配
跳转到:7.5 动态内存分配定义/语法
跳转到:定义/语法-
new 运算符:动态分配空间并返回首地址。
int n = 10;int *array1 = new int[n]; // 动态分配整型数组int *array2 = new int[n](); // 归零初始化int *array3 = new int[n]{1,2,3,4,5}; // 初始化赋值[来源:第7章 p27,内存的动态分配]
-
delete 运算符:回收动态分配的空间。
delete p; // 回收指针指向的单个变量空间delete [] array; // 回收动态数组空间[来源:第7章 p28,动态内存释放]
规则/规范
跳转到:规则/规范-
动态分配的空间需要显式用 delete 释放,delete 需要该动态分配空间的首地址。 [来源:第7章 p28,动态内存释放]
-
内存泄漏:如果一个动态分配的空间没有被任何指针变量引用,则无法用 delete 释放。 [来源:第7章 p28,动态内存释放]
-
⚠️ 不要重复释放同一块内存。 [来源:第7章 p28,动态内存释放]
-
动态分配可能失败:当内存用完时,new 会抛出
bad_alloc异常。使用new(std::nothrow)不会抛出异常,而是返回空指针nullptr(需要#include <new>)。p = new(std::nothrow) int;if (!p) {cout << "allocation failure\n";return -1;}[来源:第7章 p29,动态分配可能会失败]
-
assert 宏:判断表达式真假,为假则终止程序。
#include <cassert>int *p = new(std::nothrow) int;assert(p != nullptr); // p 为 nullptr 则退出程序[来源:第7章 p29,动态分配可能会失败]
执行逻辑
跳转到:执行逻辑- 内存分配区域(概念分类;具体地址布局因平台/实现而异):
- 静态分配:全局变量和静态变量,在整个程序运行期间都存在
- 动态分配(堆):需要时向系统申请,不再使用时显式释放
- 自动分配(栈):函数内定义的自动变量,函数结束后自动释放 [来源:第7章 p30,内存分配的进一步介绍]
7.6 指针和函数
跳转到:7.6 指针和函数定义/语法
跳转到:定义/语法-
指针形参实现数据共享:用指针作为参数可以在被调用函数中修改调用函数的变量。
void swap_p(int *pa, int *pb) {int c;c = *pa;*pa = *pb;*pb = c;}// 调用:swap_p(&x, &y);[来源:第7章 p33,指针变量作为形参:输入]
-
指针形参实现多返回值:
void add_on(int a, int *add1, int *add2, int *add3) {*add1 = a + 1;*add2 = a + 2;*add3 = a + 3;}[来源:第7章 p36,指针变量作为形参:输出]
-
返回指针的函数:
返回类型 *函数名(形参列表);返回的指针在其被使用时必须有效,不能返回局部变量的地址。 [来源:第7章 p38,返回指针的函数]
规则/规范
跳转到:规则/规范-
常量指针(
const int *p):不能通过解引用更改变量的值。int x = 5;const int *p = &x;*p = 20; // 错误:assignment of read-only location[来源:第7章 p34,防止数据被间接引用修改]
-
指针常量(
int * const p):指针本身不能改变,定义时必须初始化。int x = 5, y = 6;int * const p = &x;*p = 20; // 合法,x变成20p = &y; // 错误:assignment of read-only variable[来源:第7章 p34,防止数据被间接引用修改]
【整理自 PPT】常量指针 vs 指针常量 记忆口诀:const 在 * 左边,指向内容不可变;const 在 * 右边,指针本身不可变。
(由[来源:第7章 p34] 整理)
例题结论
跳转到:例题结论- 【填空题答案】 函数返回动态数组:
[来源:第7章 p40,习题 Notes答案]int *input(int n) {int *buf; // 填空1buf = new int[n]; // 填空2for (int i = 0; i < n; i++) cin >> buf[i];return buf;}int main() {int num;int *data; // 填空3cin >> num;data = input(num);for (int i = 0; i < num; i++) cout << data[i] << ' ';delete [] data; // 填空4return 0;}
7.7 引用类型和函数
跳转到:7.7 引用类型和函数定义/语法
跳转到:定义/语法- 引用:给一个变量取一个别名,使一个内存单元可以通过不同的变量名来访问。
[来源:第7章 p42,引用]类型 &引用名 = 变量名;int i = 1;int &j = i;j = 2; // i == 2
规则/规范
跳转到:规则/规范-
必须在定义引用变量时就初始化,之后不能再改变绑定。 [来源:第7章 p42,引用]
-
引用在语义上是别名;实现上编译器通常用指针实现,但标准不保证其占用空间一定等同于指针,也不应把引用当作“可为 null 的指针”。 [来源:第7章 p42,引用]
-
引用实现函数参数传递(比指针更简洁):
void swap(int &a, int &b) {int c;c = a;a = b;b = c;}// 调用:swap(x, y); // 不需要&[来源:第7章 p43,引用参数传递与函数返回]
例题结论
跳转到:例题结论- 【执行结果分析】 引用与指针混合使用:
输出:int swap(int &x, int *y, int z=1) {int *a = &x;static int b = x; // 静态变量,只初始化一次b += 2;*a = (*a) - z + b;return b;}int main() {int a = 8, b = 5;cout << swap(a, &b) << " " << a << " " << b << endl; // 10 17 5cout << swap(a, &b) << " " << a << " " << b << endl; // 12 28 5return 0;}[来源:第7章 p44,习题 Notes答案]10 17 512 28 5
7.8 指针数组和多级指针
跳转到:7.8 指针数组和多级指针定义/语法
跳转到:定义/语法-
指针数组:多个指针组织成的数组。
类型名 *数组名[数组长度];int *a[5]; // 包含5个int指针的数组[来源:第7章 p46,指针数组和多级指针]
-
多级指针:指向指针的指针。
二级指针:类型名 **变量名;三级指针:类型名 ***变量名;多级指针需要逐层解引用:若
T** pp指向一个T*,则*pp为T*,**pp才是T对象。 [来源:第7章 p46,指针数组和多级指针]
代码模式
跳转到:代码模式-
动态创建 M×N 二维数组 - 方法一(二级指针 + 指针数组):
int **a, i, j, k = 0;a = new int *[3]; // 分配指针数组for (i = 0; i < 3; ++i)a[i] = new int[4]; // 每个指针指向一维数组// 使用:a[i][j]// 释放:for (i = 0; i < 3; ++i) delete [] a[i];delete [] a;[来源:第7章 p48,方法一示例]
-
动态创建 M×N 二维数组 - 方法二(一维数组模拟):
int *a, i, j, k = 0;a = new int[12]; // 分配3×4=12个元素// 访问第i行第j列:a[i*4+j]// 释放:delete [] a;[来源:第7章 p50,方法二示例]
-
字符串数组使用指针数组存储:
const char *p[2] = {"Beijing", "Shanghai"};for (int i = 0; i < 2; i++) cout << p[i];[来源:第7章 p51,字符串数组]
例题结论
跳转到:例题结论-
【填空题答案】 字符串搜索函数:
bool search(const char * const *dept, int n, const char *s) { // 填空1for (int i = 0; i < n; i++)if (strcmp(dept[i], s) == 0)return true; // 填空2return false; // 填空3}int main() {const char *deptName[4] = {"CS", "AU", "EE", "SE"}; // 填空4// ...}[来源:第7章 p52,习题 Notes答案]
-
【执行结果分析】 指针数组排序后输出:
const char *str[3] = {"morning", "afternoon", "apple"};// 按字典序排序后:{"afternoon", "apple", "morning"}for (int i = 0; i < 3; i++)cout << str[i] + i << endl; // 注意 +i 偏移输出:
afternoon // str[0]+0,完整输出pple // str[1]+1,跳过第一个字符'a'rning // str[2]+2,跳过前两个字符"mo"[来源:第7章 p53,习题]
-
【执行结果分析】 二级指针操作:
char text[][20] = {"Hello world!", "I love SJTU!", "How are you?"};char *str[3] = {text[0], text[1], text[2]};char **p = str + 1; // p指向str[1]**p = 'U'; // text[1][0]变成'U'cout << *p << endl; // U love SJTU!cout << *(p+1) << endl; // How are you?cout << *(*(p-1)+11); // ! (text[0][11])[来源:第7章 p54,习题]
7.9 程序接收运行时参数
跳转到:7.9 程序接收运行时参数定义/语法
跳转到:定义/语法- main 函数参数:程序运行时的参数作为 main 函数的参数传入。
[来源:第7章 p55,程序接收运行时参数]int main(int argc, char *argv[]) {// argc: 参数个数// argv: 参数字符串数组}
例题结论
跳转到:例题结论- 【执行结果分析】 命令行参数:
输出:c:\> myprogram.exe -d -c new.txt[来源:第7章 p55,程序接收运行时参数]argc=4argv[0]=myprogram.exeargv[1]=-dargv[2]=-cargv[3]=new.txt
7.10 函数指针
跳转到:7.10 函数指针定义/语法
跳转到:定义/语法-
函数指针:保存函数入口地址的指针。
返回类型 (*指针名)(形参列表);int my_isdigit(int n, int k); // 示例函数:避免与 <cctype> 的 isdigit 冲突int (*p)(int, int) = my_isdigit; // 或:p = my_isdigit;[来源:第7章 p57,指向函数的指针]
-
通过函数指针调用函数(三种等价写法):
int x = 0;my_isdigit(12, x);(*p)(12, x);p(12, x);[来源:第7章 p57,指向函数的指针]
代码模式
跳转到:代码模式- 函数指针作为参数(实现通用排序):
[来源:第7章 p58-59,函数指针的应用]void quicksort(int data[], int low, int high,int (*comp)(int, int)) {// ...mid = divide(data, low, high, comp);// ...}// 定义比较函数int intcmp(int a, int b) {if (abs(a) == abs(b)) return 0;if (abs(a) < abs(b)) return -1;else return 1;}// 调用int b[] = {7, 9, -4, 3, 8, -1, 2, 5, -6, 0};quicksort(b, 0, 9, intcmp);
总结(来自 PPT 原文)
跳转到:总结(来自 PPT 原文)- 本章介绍了指针的概念,指针变量的定义、运算
- 采用指针,可以使数组有多种访问方式
- 将指针作为形式参数可以使一个函数与其调用函数共享数据
- 动态分配内存
*:多级指针、数组指针、函数指针
[来源:第7章 p62,总结]