跳转到内容

第七章 指针

  1. 指针的定义与初始化(取地址&、解引用*、空指针nullptr/NULL)
  2. 指针运算与数组名的关系(p+k 的计算规则、数组名在多数表达式中会“衰变”为指向首元素的指针)
  3. 字符串与字符指针的使用
  4. 动态内存分配(new/delete、内存泄漏)
  5. 指针作为函数形参(实现数据共享、多返回值)
  6. 引用类型与函数参数传递
  7. 指针数组与多级指针(二级指针、动态二维数组)
  8. 函数指针(定义、调用、作为参数传递)
  9. 常量指针与指针常量的区别
  10. void* 通配指针与强制类型转换

  • 指针:存储地址的变量,可用于间接访问对象、在函数间共享/修改数据,并配合动态内存分配管理运行期对象(如动态数组)。 [来源:第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答案]


  • 通配指针定义:当定义指针但不关心被指向变量类型时使用。
    void *指针变量名;
    [来源:第7章 p15,void*通配指针类型]
  • 其他类型的指针可以赋值给 void 类型指针。
  • 把 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章 p15,void*通配指针类型]

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,数组传递的进一步讨论]


  • 字符指针引用字符串:常用一个字符指针来引用一个字符串,从该指针所指向的字符开始,直到 '\0' 字符结束。
    char bstr[] = "201811122119";
    char *p = bstr;
    cout << p << endl; // 输出整个字符串
    cout << *p << endl; // 输出第一个字符 '2'
    [来源:第7章 p22,字符串再讨论]
  • 字符串以 '\0' 结束,只需确定字符串开始的地址即可,不需要指定长度。 [来源:第7章 p35,Notes]

  • 字符串字面量具有静态存储期,通常放在只读存储区;尝试通过指针修改其内容属于未定义行为(应使用 const char* 接收)。 [来源:第7章 p23,Notes]

  • ⚠️ 字符串操作错误示例:
    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字节)
    [来源:第7章 p23,字符串常量]
  • 【执行结果分析】 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') { // 填空1
    digital = *str - '0'; // 填空2
    num = num * 10 + digital;
    str++;
    }
    return num;
    }

    [来源:第7章 p25,习题 Notes答案]


  • 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,动态分配可能会失败]

  • 内存分配区域(概念分类;具体地址布局因平台/实现而异):
    1. 静态分配:全局变量和静态变量,在整个程序运行期间都存在
    2. 动态分配(堆):需要时向系统申请,不再使用时显式释放
    3. 自动分配(栈):函数内定义的自动变量,函数结束后自动释放 [来源:第7章 p30,内存分配的进一步介绍]

  • 指针形参实现数据共享:用指针作为参数可以在被调用函数中修改调用函数的变量。

    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变成20
    p = &y; // 错误:assignment of read-only variable

    [来源:第7章 p34,防止数据被间接引用修改]

【整理自 PPT】常量指针 vs 指针常量 记忆口诀:const 在 * 左边,指向内容不可变;const 在 * 右边,指针本身不可变。 (由[来源:第7章 p34] 整理)

  • 【填空题答案】 函数返回动态数组:
    int *input(int n) {
    int *buf; // 填空1
    buf = new int[n]; // 填空2
    for (int i = 0; i < n; i++) cin >> buf[i];
    return buf;
    }
    int main() {
    int num;
    int *data; // 填空3
    cin >> num;
    data = input(num);
    for (int i = 0; i < num; i++) cout << data[i] << ' ';
    delete [] data; // 填空4
    return 0;
    }
    [来源:第7章 p40,习题 Notes答案]

7.7 引用类型和函数

跳转到:7.7 引用类型和函数
  • 引用:给一个变量取一个别名,使一个内存单元可以通过不同的变量名来访问。
    类型 &引用名 = 变量名;
    int i = 1;
    int &j = i;
    j = 2; // i == 2
    [来源:第7章 p42,引用]
  • 必须在定义引用变量时就初始化,之后不能再改变绑定。 [来源:第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 5
    cout << swap(a, &b) << " " << a << " " << b << endl; // 12 28 5
    return 0;
    }
    输出:
    10 17 5
    12 28 5
    [来源:第7章 p44,习题 Notes答案]

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) { // 填空1
    for (int i = 0; i < n; i++)
    if (strcmp(dept[i], s) == 0)
    return true; // 填空2
    return 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 函数的参数传入。
    int main(int argc, char *argv[]) {
    // argc: 参数个数
    // argv: 参数字符串数组
    }
    [来源:第7章 p55,程序接收运行时参数]
  • 【执行结果分析】 命令行参数:
    c:\> myprogram.exe -d -c new.txt
    输出:
    argc=4
    argv[0]=myprogram.exe
    argv[1]=-d
    argv[2]=-c
    argv[3]=new.txt
    [来源:第7章 p55,程序接收运行时参数]

  • 函数指针:保存函数入口地址的指针。

    返回类型 (*指针名)(形参列表);
    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,指向函数的指针]

  • 函数指针作为参数(实现通用排序):
    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);
    [来源:第7章 p58-59,函数指针的应用]

总结(来自 PPT 原文)

跳转到:总结(来自 PPT 原文)
  1. 本章介绍了指针的概念,指针变量的定义、运算
  2. 采用指针,可以使数组有多种访问方式
  3. 将指针作为形式参数可以使一个函数与其调用函数共享数据
  4. 动态分配内存
  5. *:多级指针、数组指针、函数指针

[来源:第7章 p62,总结]