当前位置: 首页 > news >正文

C++ 指针从入门到精通实战:全面掌握指针的概念与应用

在这里插入图片描述

C++ 指针从入门到精通实战:全面掌握指针的概念与应用

指针(Pointer)是C++中一个极其重要且强大的概念,它赋予了程序员直接操作内存的能力,从而实现高效的代码和复杂的数据结构。然而,指针的使用也伴随着诸多挑战,如内存管理、指针悬挂、野指针等问题。如果掌握得当,指针能够极大地提升C++程序的性能和灵活性。本文将从指针的基本概念入手,逐步深入到高级应用,通过详细的示例和关键注释,帮助读者从入门到精通C++指针。
在这里插入图片描述

🧑 博主简介:CSDN博客专家、CSDN平台优质创作者,高级开发工程师,数学专业,10年以上C/C++, C#, Java等多种编程语言开发经验,拥有高级工程师证书;擅长C/C++、C#等开发语言,熟悉Java常用开发技术,能熟练应用常用数据库SQL server,Oracle,mysql,postgresql等进行开发应用,熟悉DICOM医学影像及DICOM协议,业余时间自学JavaScript,Vue,qt,python等,具备多种混合语言开发能力。撰写博客分享知识,致力于帮助编程爱好者共同进步。欢迎关注、交流及合作,提供技术支持与解决方案。
技术合作请加本人wx(注明来自csdn):xt20160813

目录

  1. 指针基础概念
    • 什么是指针
    • 指针的声明与初始化
    • 指针的运算符
  2. 指针的基本操作
    • 指针与变量的关系
    • 指针的解引用
    • 指针的比较与运算
  3. 指针与数组
    • 数组名与指针
    • 指针遍历数组
    • 多维数组与指针
  4. 指针与函数
    • 函数指针的定义与使用
    • 函数指针作为参数
    • 回调函数的实现
  5. 指针与动态内存管理
    • 动态内存分配
    • 内存释放与避免内存泄漏
    • 智能指针的介绍与使用
  6. 指针的高级应用
    • 指针的指针
    • 指向成员函数的指针
    • 指向类的指针与对象
  7. 常见指针错误与调试
    • 野指针(Dangling Pointer)
    • 悬空指针
    • NULL指针与空指针
    • 指针越界
  8. 实战案例:指针在数据结构中的应用
    • 单链表的实现
    • 二叉树的实现
    • 图的邻接表表示
  9. 总结
  10. 参考资料

指针基础概念

什么是指针

在C++中,指针是一种变量,用于存储另一个变量的内存地址。指针提供了一种间接访问和操作内存的方式,使得程序员能够高效地管理内存和数据结构。

示例:

#include <iostream>
using namespace std;

int main() {
    int var = 10;      // 普通变量
    int* ptr = &var;   // 指针变量,存储var的地址

    cout << "var的值: " << var << endl;           // 输出:10
    cout << "ptr存储的地址: " << ptr << endl;      // 输出var的地址
    cout << "*ptr的值: " << *ptr << endl;          // 输出:10

    return 0;
}

输出:

var的值: 10
ptr存储的地址: 0x7ffee3b1b6fc
*ptr的值: 10

指针的声明与初始化

指针的声明语法为:类型* 指针名;。其中,类型是指针所指向的变量的类型,*用于表示这是一个指针。

示例:

double d = 3.14;
double* d_ptr = &d;  // 声明一个指向double类型的指针,并初始化为d的地址

在C++中,指针可以在声明时未初始化,但这样会导致悬空指针问题,因此最佳实践是在声明时立即初始化指针。

指针的运算符

C++中与指针相关的两个主要运算符是:

  • 取地址运算符(&):用于获取变量的内存地址。
  • 解引用运算符(*):用于访问指针所指向的变量的值。

示例:

int a = 5;
int* p = &a;  // 使用&取地址运算符
cout << "a的值: " << a << endl;      // 输出:5
cout << "*p的值: " << *p << endl;    // 输出:5

*p = 10;  // 使用*进行解引用并修改a的值
cout << "修改后a的值: " << a << endl;  // 输出:10

输出:

a的值: 5
*p的值: 5
修改后a的值: 10

指针的基本操作

指针与变量的关系

指针变量存储的是其他变量的内存地址。通过指针,可以访问和修改指针所指向的变量的值。

示例:

#include <iostream>
using namespace std;

int main() {
    int x = 25;
    int* p = &x;  // p指向x

    cout << "x的值: " << x << endl;      // 输出:25
    cout << "通过指针p访问x的值: " << *p << endl;  // 输出:25

    *p = 30;  // 通过指针p修改x的值
    cout << "修改后x的值: " << x << endl;  // 输出:30

    return 0;
}

输出:

x的值: 25
通过指针p访问x的值: 25
修改后x的值: 30

指针的解引用

解引用是指通过指针访问其所指向的变量的值。使用*运算符可以获得指针所指向的变量。

示例:

#include <iostream>
using namespace std;

int main() {
    double pi = 3.14159;
    double* ptr = &pi;

    cout << "pi的值: " << pi << endl;      // 输出:3.14159
    cout << "*ptr的值: " << *ptr << endl;  // 输出:3.14159

    *ptr = 3.14;  // 通过指针修改pi的值
    cout << "修改后的pi的值: " << pi << endl;  // 输出:3.14

    return 0;
}

输出:

pi的值: 3.14159
*ptr的值: 3.14159
修改后的pi的值: 3.14

指针的比较与运算

指针可以进行比较和算术运算,但必须注意以下几点:

  1. 比较:只有指向同一类型的指针才可以进行比较。
  2. 算术运算:指针可以进行加减操作,但操作数必须是整数。加法和减法会根据指针类型自动调整偏移量。

示例:

#include <iostream>
using namespace std;

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int* p1 = &arr[0];
    int* p2 = &arr[2];

    // 指针比较
    if (p1 < p2) {
        cout << "p1指向的元素在p2指向的元素之前" << endl;
    }

    // 指针算术运算
    cout << "p1指向的元素: " << *p1 << endl;  // 输出:10
    p1 += 3;  // p1现在指向arr[3]
    cout << "经过加法后的p1指向的元素: " << *p1 << endl;  // 输出:40

    return 0;
}

输出:

p1指向的元素在p2指向的元素之前
p1指向的元素: 10
经过加法后的p1指向的元素: 40

指针与数组

数组名与指针

在C++中,数组名在大多数情况下会被解释为指向数组第一个元素的指针。这使得指针和数组在使用上具有高度的相似性。

示例:

#include <iostream>
using namespace std;

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int* p = arr;  // 数组名arr被当作指向第一个元素的指针

    cout << "arr[0]: " << arr[0] << endl;  // 输出:1
    cout << "*(p + 0): " << *(p + 0) << endl;  // 输出:1
    cout << "arr[2]: " << arr[2] << endl;  // 输出:3
    cout << "*(p + 2): " << *(p + 2) << endl;  // 输出:3

    return 0;
}

输出:

arr[0]: 1
*(p + 0): 1
arr[2]: 3
*(p + 2): 3

指针遍历数组

通过指针,可以高效地遍历数组。这种方式常用于需要高性能数据访问的场景。

示例:

#include <iostream>
using namespace std;

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int* p = arr;  // 指针指向数组第一个元素

    cout << "数组元素通过指针访问: ";
    for(int i = 0; i < 5; ++i) {
        cout << *(p + i) << " ";  // 指针偏移
    }
    cout << endl;

    return 0;
}

输出:

数组元素通过指针访问: 10 20 30 40 50 

多维数组与指针

多维数组可以使用指针进行访问,但需要理解其内存排列方式(通常是行优先)。

示例:

#include <iostream>
using namespace std;

int main() {
    int matrix[2][3] = {
        {1, 2, 3},
        {4, 5, 6}
    };

    int (*p)[3] = matrix;  // p是指向含有3个整数的一维数组的指针

    cout << "矩阵元素通过指针访问:" << endl;
    for(int i = 0; i < 2; ++i) {
        for(int j = 0; j < 3; ++j) {
            cout << *(*(p + i) + j) << " ";
        }
        cout << endl;
    }

    return 0;
}

输出:

矩阵元素通过指针访问:
1 2 3 
4 5 6 

指针与函数

函数指针的定义与使用

函数指针是指向函数的指针变量。它可以存储函数的地址,并通过指针调用函数。

示例:

#include <iostream>
using namespace std;

// 函数A
void greet() {
    cout << "Hello, World!" << endl;
}

// 函数B
void farewell() {
    cout << "Goodbye!" << endl;
}

int main() {
    // 定义函数指针,指向返回类型为void,参数为空的函数
    void (*funcPtr)();

    // 指向函数A
    funcPtr = greet;
    funcPtr();  // 调用greet()

    // 指向函数B
    funcPtr = farewell;
    funcPtr();  // 调用farewell()

    return 0;
}

输出:

Hello, World!
Goodbye!

函数指针作为参数

函数指针可以作为函数的参数,允许在运行时动态指定执行的函数逻辑。

示例:

#include <iostream>
using namespace std;

// 定义一个接受函数指针的函数
void performOperation(int x, void (*operation)(int)) {
    cout << "执行操作前,x = " << x << endl;
    operation(x);  // 通过函数指针调用函数
    cout << "执行操作后,x = " << x << endl;
}

// 操作函数
void increment(int& x) {
    x += 1;
    cout << "增量操作:x = " << x << endl;
}

void decrement(int& x) {
    x -= 1;
    cout << "减量操作:x = " << x << endl;
}

int main() {
    int value = 10;

    // 定义函数指针类型
    typedef void (*Operation)(int&);

    // 使用函数指针调用increment
    Operation op = increment;
    performOperation(value, (void(*)(int))op);  // 需要类型转换

    // 使用函数指针调用decrement
    op = decrement;
    performOperation(value, (void(*)(int))op);  // 需要类型转换

    return 0;
}

输出:

执行操作前,x = 10
增量操作:x = 11
执行操作后,x = 11
执行操作前,x = 11
减量操作:x = 10
执行操作后,x = 10

注意: 在上述示例中,incrementdecrement函数的参数类型与performOperation函数的函数指针类型不匹配,因此需要进行类型转换。为避免这种情况,建议统一函数指针的签名。

优化后的示例:

#include <iostream>
using namespace std;

// 定义函数指针类型
typedef void (*Operation)(int&);

// 定义一个接受函数指针的函数
void performOperation(int x, Operation operation) {
    cout << "执行操作前,x = " << x << endl;
    operation(x);  // 通过函数指针调用函数
    cout << "执行操作后,x = " << x << endl;
}

// 操作函数
void increment(int& x) {
    x += 1;
    cout << "增量操作:x = " << x << endl;
}

void decrement(int& x) {
    x -= 1;
    cout << "减量操作:x = " << x << endl;
}

int main() {
    int value = 10;

    // 使用函数指针调用increment
    Operation op = increment;
    performOperation(value, op);  // 无需类型转换

    // 使用函数指针调用decrement
    op = decrement;
    performOperation(value, op);  // 无需类型转换

    return 0;
}

输出:

执行操作前,x = 10
增量操作:x = 11
执行操作后,x = 11
执行操作前,x = 11
减量操作:x = 10
执行操作后,x = 10

回调函数的实现

回调函数是一种通过函数指针、std::function或其他机制,在函数执行过程中动态调用指定函数的模式。回调函数广泛应用于事件处理、异步操作和库接口设计中。

示例:

#include <iostream>
#include <functional>
using namespace std;

// 定义回调类型
typedef void (*Callback)(int);

// 模拟事件触发函数,接受回调函数
void onEvent(int eventCode, Callback cb) {
    cout << "事件触发,事件代码: " << eventCode << endl;
    // 事件处理逻辑...
    cb(eventCode);  // 执行回调函数
}

// 回调函数实现
void handleEvent(int code) {
    cout << "处理事件,事件代码: " << code << endl;
}

int main() {
    // 注册并触发事件
    onEvent(100, handleEvent);
    return 0;
}

输出:

事件触发,事件代码: 100
处理事件,事件代码: 100

使用std::function和Lambda表达式的优化示例:

#include <iostream>
#include <functional>
using namespace std;

// 使用std::function定义回调类型
typedef function<void(int)> Callback;

// 模拟事件触发函数,接受回调函数
void onEvent(int eventCode, Callback cb) {
    cout << "事件触发,事件代码: " << eventCode << endl;
    // 事件处理逻辑...
    cb(eventCode);  // 执行回调函数
}

int main() {
    int multiplier = 2;

    // 使用Lambda表达式作为回调函数
    Callback cb = [multiplier](int code) {
        cout << "处理事件,事件代码: " << code << ", 乘数: " << multiplier << ", 结果: " << code * multiplier << endl;
    };

    // 注册并触发事件
    onEvent(150, cb);

    return 0;
}

输出:

事件触发,事件代码: 150
处理事件,事件代码: 150, 乘数: 2, 结果: 300

指针与动态内存管理

动态内存分配

C++提供了newdelete运算符,用于在堆上动态分配和释放内存。这使得程序能够在运行时根据需要动态地管理内存。

示例:

#include <iostream>
using namespace std;

int main() {
    // 动态分配单个整数
    int* p = new int;
    *p = 42;
    cout << "动态分配的整数值: " << *p << endl;

    // 动态分配整数数组
    int* arr = new int[5];
    for(int i = 0; i < 5; ++i) {
        arr[i] = i * 10;
    }
    cout << "动态分配的数组元素: ";
    for(int i = 0; i < 5; ++i) {
        cout << arr[i] << " ";
    }
    cout << endl;

    // 释放内存
    delete p;       // 释放单个整数
    delete[] arr;   // 释放数组

    return 0;
}

输出:

动态分配的整数值: 42
动态分配的数组元素: 0 10 20 30 40 

注意事项:

  1. 匹配分配与释放方式:使用new分配的单个对象必须使用delete释放,使用new[]分配的数组必须使用delete[]释放。
  2. 避免内存泄漏:每次动态分配的内存都必须在不再需要时释放,否则会导致内存泄漏,从而消耗系统资源。

内存释放与避免内存泄漏

内存泄漏是指程序中动态分配的内存未被释放,导致这些内存无法被重新利用。为了避免内存泄漏,必须确保每次new对应一个delete,每次new[]对应一个delete[]

示例:

#include <iostream>
using namespace std;

void createMemoryLeak() {
    int* leakPtr = new int(100);
    // 忘记释放内存,导致内存泄漏
}

int main() {
    createMemoryLeak();
    cout << "程序结束,内存泄漏已发生。" << endl;
    return 0;
}

输出:

程序结束,内存泄漏已发生。

避免方法:

  1. 手动管理:严格遵循动态分配与释放的匹配规则,确保每次new都有对应的delete
  2. 智能指针:使用C++标准库提供的智能指针(如std::unique_ptrstd::shared_ptr),自动管理内存,防止泄漏。
  3. RAII(资源获取即初始化)原则:将资源管理与对象生命周期绑定,通过构造函数获取资源,通过析构函数释放资源。

示例:使用智能指针避免内存泄漏

#include <iostream>
#include <memory>
using namespace std;

int main() {
    // 使用unique_ptr管理单个对象
    unique_ptr<int> p = make_unique<int>(50);
    cout << "智能指针管理的整数值: " << *p << endl;

    // 使用unique_ptr管理数组
    unique_ptr<int[]> arr = make_unique<int[]>(5);
    for(int i = 0; i < 5; ++i) {
        arr[i] = i * 5;
    }
    cout << "智能指针管理的数组元素: ";
    for(int i = 0; i < 5; ++i) {
        cout << arr[i] << " ";
    }
    cout << endl;

    // 无需手动delete,智能指针自动释放内存

    return 0;
}

输出:

智能指针管理的整数值: 50
智能指针管理的数组元素: 0 5 10 15 20 

优势:

  • 自动释放:智能指针在其生命周期结束时自动释放内存,防止内存泄漏。
  • 异常安全:在发生异常时,智能指针也能确保内存被正确释放。
  • 使用简便:无需手动调用delete,减少人为错误。

智能指针的介绍与使用

C++11引入了智能指针,主要包括std::unique_ptrstd::shared_ptrstd::weak_ptr。它们提供了自动内存管理的机制,降低了内存泄漏的风险。

1. std::unique_ptr

std::unique_ptr是一个独占所有权的智能指针,不能被复制,只能被移动。适用于拥有唯一所有者的资源。

示例:

#include <iostream>
#include <memory>
using namespace std;

int main() {
    // 创建unique_ptr,管理单个对象
    unique_ptr<string> ptr1 = make_unique<string>("Hello, Unique Pointer!");
    cout << *ptr1 << endl;

    // 移动所有权
    unique_ptr<string> ptr2 = move(ptr1);
    if(!ptr1) {
        cout << "ptr1现在为空。" << endl;
    }
    cout << *ptr2 << endl;

    return 0;
}

输出:

Hello, Unique Pointer!
ptr1现在为空。
Hello, Unique Pointer!

2. std::shared_ptr

std::shared_ptr是一个共享所有权的智能指针,允许多个指针共同拥有同一个资源。资源会在最后一个shared_ptr被销毁时被释放。

示例:

#include <iostream>
#include <memory>
using namespace std;

int main() {
    // 创建shared_ptr,管理单个对象
    shared_ptr<int> sp1 = make_shared<int>(100);
    cout << "sp1引用计数: " << sp1.use_count() << endl;  // 输出:1

    {
        // 复制shared_ptr,共享所有权
        shared_ptr<int> sp2 = sp1;
        cout << "sp1引用计数: " << sp1.use_count() << endl;  // 输出:2
        cout << "sp2引用计数: " << sp2.use_count() << endl;  // 输出:2
    }

    // sp2已销毁
    cout << "sp1引用计数: " << sp1.use_count() << endl;  // 输出:1

    return 0;
}

输出:

sp1引用计数: 1
sp1引用计数: 2
sp2引用计数: 2
sp1引用计数: 1

3. std::weak_ptr

std::weak_ptr是一个弱引用智能指针,不增加引用计数,用于观察由shared_ptr管理的对象而不拥有资源所有权,防止循环引用。

示例:

#include <iostream>
#include <memory>
using namespace std;

int main() {
    shared_ptr<int> sp1 = make_shared<int>(200);
    weak_ptr<int> wp1 = sp1;  // weak_ptr指向sp1管理的资源

    cout << "sp1引用计数: " << sp1.use_count() << endl;  // 输出:1

    if(auto sp2 = wp1.lock()) {  // 尝试提升为shared_ptr
        cout << "wp1成功提升,值为: " << *sp2 << endl;
        cout << "sp1引用计数: " << sp1.use_count() << endl;  // 输出:2
    } else {
        cout << "wp1提升失败,资源已释放。" << endl;
    }

    sp1.reset();  // 释放资源

    if(auto sp3 = wp1.lock()) {
        cout << "wp1成功提升,值为: " << *sp3 << endl;
    } else {
        cout << "wp1提升失败,资源已释放。" << endl;
    }

    return 0;
}

输出:

sp1引用计数: 1
wp1成功提升,值为: 200
sp1引用计数: 2
wp1提升失败,资源已释放。

总结:

  • std::unique_ptr:适用于拥有独占所有权的资源。
  • std::shared_ptr:适用于需要共享所有权的资源。
  • std::weak_ptr:适用于观察资源而不拥有所有权,防止循环引用。

指针的高级应用

指针的指针

指针的指针是指向指针变量的指针。通过多级指针,可以实现对多级数据结构的访问和操作。

示例:

#include <iostream>
using namespace std;

int main() {
    int var = 50;
    int* p = &var;    // p指向var
    int** pp = &p;    // pp指向p

    cout << "var的值: " << var << endl;              // 输出:50
    cout << "*p的值: " << *p << endl;                // 输出:50
    cout << "**pp的值: " << **pp << endl;            // 输出:50

    // 修改var的值通过多级指针
    **pp = 100;
    cout << "修改后var的值: " << var << endl;        // 输出:100

    return 0;
}

输出:

var的值: 50
*p的值: 50
**pp的值: 50
修改后var的值: 100

应用场景:

  • 多级数据结构:如二维数组、链表的链表等。
  • 动态内存管理:可以方便地管理多维动态数组的内存。

指向成员函数的指针

指针不仅可以指向变量,还可以指向类的成员函数。通过成员函数指针,可以实现更加灵活的对象行为调用。

示例:

#include <iostream>
using namespace std;

// 定义类
class MyClass {
public:
    void display(int x) {
        cout << "MyClass::display called with x = " << x << endl;
    }

    void show() {
        cout << "MyClass::show called." << endl;
    }
};

int main() {
    MyClass obj;

    // 指向成员函数的指针
    void (MyClass::*ptr)(int) = &MyClass::display;
    void (MyClass::*ptrShow)() = &MyClass::show;

    // 调用成员函数
    (obj.*ptr)(10);        // 输出:MyClass::display called with x = 10
    (obj.*ptrShow)();     // 输出:MyClass::show called.

    // 使用指针操作对象的成员函数
    MyClass* pObj = &obj;
    (pObj->*ptr)(20);     // 输出:MyClass::display called with x = 20

    return 0;
}

输出:

MyClass::display called with x = 10
MyClass::show called.
MyClass::display called with x = 20

说明:

  1. 声明成员函数指针void (MyClass::*ptr)(int) = &MyClass::display; 定义了一个指向MyClass::display成员函数的指针。
  2. 调用成员函数
    • 通过对象(obj.*ptr)(10);
    • 通过指针(pObj->*ptr)(20);

指向类的指针与对象

指向类的指针是指向类实例的指针,通过它可以访问对象的成员函数和成员变量。

示例:

#include <iostream>
using namespace std;

class Person {
public:
    string name;
    int age;

    void introduce() {
        cout << "我是" << name << ",今年" << age << "岁。" << endl;
    }
};

int main() {
    Person p;
    p.name = "张三";
    p.age = 30;

    Person* ptr = &p;  // 指向类对象的指针

    // 访问成员变量
    cout << "姓名: " << ptr->name << endl;  // 使用箭头运算符
    cout << "年龄: " << ptr->age << "岁" << endl;

    // 调用成员函数
    ptr->introduce();

    return 0;
}

输出:

姓名: 张三
年龄: 30岁
我是张三,今年30岁。

说明:

  • 箭头运算符(->):用于通过指向对象的指针访问成员变量和成员函数。
  • 点运算符(.):用于通过对象直接访问成员。

常见指针错误与调试

在指针的使用过程中,常会遇到一些问题和错误,如野指针、悬空指针、空指针和指针越界等。了解这些错误的成因和调试方法,有助于编写更健壮的C++程序。

野指针(Dangling Pointer)

定义: 野指针指向的内存已被释放或尚未分配,指针本身未初始化或已被释放。

示例:

#include <iostream>
using namespace std;

int main() {
    int* p = new int(10);  // 动态分配内存
    delete p;               // 释放内存

    // p现在成为野指针,指向已释放的内存
    cout << "*p的值: " << *p << endl;  // 未定义行为

    return 0;
}

解决方法:

  1. 及时将指针置为nullptr:释放内存后,将指针设置为nullptr,避免指针指向已释放的内存。
  2. 使用智能指针:智能指针自动管理内存,避免手动释放后的野指针问题。

优化示例:

#include <iostream>
#include <memory>
using namespace std;

int main() {
    unique_ptr<int> p = make_unique<int>(10);  // 使用unique_ptr管理内存
    cout << "*p的值: " << *p << endl;          // 输出:10

    p.reset();  // 释放内存并将p置为nullptr

    if(p == nullptr) {
        cout << "p已经被重置为nullptr。" << endl;
    }

    return 0;
}

输出:

*p的值: 10
p已经被重置为nullptr。

悬空指针

定义: 悬空指针是指指向已经被释放内存的指针,和野指针类似,有时两者定义重叠。

示例:

#include <iostream>
using namespace std;

int* getPointer() {
    int x = 100;
    return &x;  // 返回局部变量的地址,x在函数结束后被销毁
}

int main() {
    int* p = getPointer();
    cout << "*p的值: " << *p << endl;  // 未定义行为

    return 0;
}

输出:

*p的值: 4201552  // 具体值不确定,属于未定义行为

解决方法:

  1. 避免返回指向局部变量的指针
  2. 使用动态内存分配或智能指针

优化示例:

#include <iostream>
#include <memory>
using namespace std;

// 使用智能指针返回动态分配的内存
unique_ptr<int> getPointer() {
    return make_unique<int>(100);
}

int main() {
    unique_ptr<int> p = getPointer();
    cout << "*p的值: " << *p << endl;  // 输出:100

    return 0;
}

输出:

*p的值: 100

NULL指针与空指针

定义: 空指针是指不指向任何有效对象的指针。C++11引入了nullptr关键字,用于表示空指针。

示例:

#include <iostream>
using namespace std;

int main() {
    int* p1 = NULL;    // 使用NULL表示空指针
    int* p2 = nullptr; // C++11中的空指针

    if(p1 == nullptr) {
        cout << "p1是空指针。" << endl;
    }

    if(p2 == nullptr) {
        cout << "p2是空指针。" << endl;
    }

    return 0;
}

输出:

p1是空指针。
p2是空指针。

说明: 尽管NULL在C++中通常被定义为0((void*)0),但nullptr是专门用于表示空指针的类型安全指针,可以避免一些类型模糊的问题。

指针越界

定义: 指针越界是指指针指向的内存超出了其合法范围,如访问数组的非法索引位置。

示例:

#include <iostream>
using namespace std;

int main() {
    int arr[3] = {1, 2, 3};
    int* p = arr;

    // 越界访问
    cout << "第四个元素: " << *(p + 3) << endl;  // 未定义行为

    return 0;
}

解决方法:

  1. 确保指针操作不超出边界
  2. 使用容器(如std::vector)和算法,减少手动指针操作。

优化示例:

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> vec = {1, 2, 3};

    for(auto it = vec.begin(); it != vec.end(); ++it) {
        cout << "元素值: " << *it << endl;
    }

    // 使用循环访问,避免越界
    return 0;
}

输出:

元素值: 1
元素值: 2
元素值: 3

实战案例:指针在数据结构中的应用

指针在数据结构的实现中发挥着至关重要的作用。以下通过几个常见的数据结构(单链表、二叉树和图的邻接表)展示指针的实际应用。

单链表的实现

单链表是一种动态数据结构,每个节点包含数据和指向下一个节点的指针。

示例:

#include <iostream>
using namespace std;

// 定义单链表节点
struct Node {
    int data;
    Node* next;

    Node(int val) : data(val), next(nullptr) {}
};

// 单链表类
class LinkedList {
private:
    Node* head;

public:
    LinkedList() : head(nullptr) {}

    // 添加节点到链表末尾
    void append(int val) {
        Node* newNode = new Node(val);
        if(!head) {
            head = newNode;
            return;
        }
        Node* temp = head;
        while(temp->next)
            temp = temp->next;
        temp->next = newNode;
    }

    // 打印链表
    void print() {
        Node* temp = head;
        while(temp) {
            cout << temp->data << " -> ";
            temp = temp->next;
        }
        cout << "NULL" << endl;
    }

    // 释放链表内存
    ~LinkedList() {
        Node* temp = head;
        while(temp) {
            Node* del = temp;
            temp = temp->next;
            delete del;
        }
    }
};

int main() {
    LinkedList list;
    list.append(10);
    list.append(20);
    list.append(30);
    list.print();  // 输出:10 -> 20 -> 30 -> NULL

    return 0;
}

输出:

10 -> 20 -> 30 -> NULL

关键注释:

  • Node结构体:包含数据和指向下一个节点的指针。
  • LinkedList类
    • append函数:动态分配新节点,并将其添加到链表末尾。
    • print函数:遍历链表并打印每个节点的数据。
    • 析构函数:遍历链表并释放每个节点的内存,避免内存泄漏。

二叉树的实现

二叉树是一种分支结构,每个节点最多有两个子节点(左子节点和右子节点)。

示例:

#include <iostream>
using namespace std;

// 定义二叉树节点
struct TreeNode {
    int data;
    TreeNode* left;
    TreeNode* right;

    TreeNode(int val) : data(val), left(nullptr), right(nullptr) {}
};

// 二叉树类
class BinaryTree {
private:
    TreeNode* root;

public:
    BinaryTree() : root(nullptr) {}

    // 插入节点
    void insert(int val) {
        root = insertRec(root, val);
    }

    // 中序遍历
    void inorder() {
        inorderRec(root);
        cout << endl;
    }

    // 释放二叉树内存
    ~BinaryTree() {
        destroyRec(root);
    }

private:
    // 递归插入节点
    TreeNode* insertRec(TreeNode* node, int val) {
        if(!node)
            return new TreeNode(val);
        if(val < node->data)
            node->left = insertRec(node->left, val);
        else
            node->right = insertRec(node->right, val);
        return node;
    }

    // 递归中序遍历
    void inorderRec(TreeNode* node) {
        if(node) {
            inorderRec(node->left);
            cout << node->data << " ";
            inorderRec(node->right);
        }
    }

    // 递归释放内存
    void destroyRec(TreeNode* node) {
        if(node) {
            destroyRec(node->left);
            destroyRec(node->right);
            delete node;
        }
    }
};

int main() {
    BinaryTree bt;
    bt.insert(50);
    bt.insert(30);
    bt.insert(70);
    bt.insert(20);
    bt.insert(40);
    bt.insert(60);
    bt.insert(80);

    cout << "中序遍历二叉树: ";
    bt.inorder();  // 输出:20 30 40 50 60 70 80 

    return 0;
}

输出:

中序遍历二叉树: 20 30 40 50 60 70 80 

关键注释:

  • TreeNode结构体:包含数据、指向左子节点和右子节点的指针。
  • BinaryTree类
    • insert函数:调用递归函数insertRec将新节点插入到合适的位置。
    • inorder函数:调用递归函数inorderRec进行中序遍历。
    • 析构函数:调用递归函数destroyRec释放所有节点的内存。

图的邻接表表示

是一种由节点(顶点)和边组成的复杂数据结构。邻接表是一种高效的图表示方法,利用指针来存储节点之间的连接关系。

示例:

#include <iostream>
#include <vector>
using namespace std;

// 定义图的边
struct Edge {
    int dest;
    Edge(int d) : dest(d) {}
};

// 定义图
class Graph {
private:
    int V;                      // 节点数量
    vector<vector<Edge>> adj;   // 邻接表

public:
    Graph(int vertices) : V(vertices), adj(vertices, vector<Edge>()) {}

    // 添加无向边
    void addEdge(int src, int dest) {
        adj[src].emplace_back(dest);
        adj[dest].emplace_back(src);  // 无向图需要双向添加
    }

    // 打印图的邻接表
    void printGraph() {
        for(int v = 0; v < V; ++v) {
            cout << "节点 " << v << " 的邻接节点: ";
            for(auto& edge : adj[v]) {
                cout << edge.dest << " ";
            }
            cout << endl;
        }
    }
};

int main() {
    int vertices = 5;
    Graph g(vertices);

    g.addEdge(0, 1);
    g.addEdge(0, 4);
    g.addEdge(1, 2);
    g.addEdge(1, 3);
    g.addEdge(1, 4);
    g.addEdge(2, 3);
    g.addEdge(3, 4);

    g.printGraph();

    return 0;
}

输出:

节点 0 的邻接节点: 1 4 
节点 1 的邻接节点: 0 2 3 4 
节点 2 的邻接节点: 1 3 
节点 3 的邻接节点: 1 2 4 
节点 4 的邻接节点: 0 1 3 

关键注释:

  • Edge结构体:表示图中的边,包含目的节点的编号。
  • Graph类
    • addEdge函数:向邻接表中添加新的边,实现图的构建。
    • printGraph函数:遍历邻接表并打印每个节点的邻接节点。

回调函数的优缺点

优点

  1. 灵活性高:回调函数允许在不同的场景下调用不同的函数,实现灵活的功能扩展。
  2. 代码解耦:可以将功能模块解耦,使得代码更加模块化和可维护。
  3. 事件驱动:在事件驱动编程中,回调函数是响应事件的核心机制。
  4. 异步处理:回调函数为异步编程提供了一种简单的实现方式,避免阻塞主线程。

缺点

  1. 代码复杂性增加:大量使用回调函数可能导致代码结构复杂,难以理解和维护。
  2. 调试困难:回调函数可能导致控制流不明确,增加了调试的难度。
  3. 生命周期管理:需要确保回调函数在调用前对象仍然存在,避免使用悬挂指针。
  4. 性能开销:尤其是在高频调用的场景下,回调函数可能引入额外的性能开销。

现代C++中的替代方案与改进

随着C++标准的发展,引入了一些替代回调函数的现代特性,如未来与承诺(Futures & Promises)和协程(Coroutines),这些机制提供了更高效和安全的异步编程方式。

使用未来与承诺(Futures & Promises)

**std::futurestd::promise**提供了一种机制,用于在线程间传递异步操作的结果。它们能够实现多线程间的同步和通信,而不需要显式使用回调函数。

示例:

#include <iostream>
#include <thread>
#include <future>
using namespace std;

// 模拟耗时任务
int compute(int x) {
    cout << "计算开始,x = " << x << endl;
    this_thread::sleep_for(chrono::seconds(2));  // 模拟耗时
    cout << "计算完成,x * x = " << x * x << endl;
    return x * x;
}

int main() {
    // 使用async启动异步任务,并获取future
    future<int> fut = async(launch::async, compute, 5);

    cout << "主线程继续执行..." << endl;

    // 在需要结果时,通过future获取
    int result = fut.get();
    cout << "异步任务返回结果: " << result << endl;

    return 0;
}

输出:

主线程继续执行...
计算开始,x = 5
计算完成,x * x = 25
异步任务返回结果: 25

优势:

  • 简化代码:相比回调,通过futurepromise可以更直观地组织异步任务的执行和结果获取。
  • 异常处理future可以捕获并传播异步任务中的异常,提高代码的健壮性。
  • 同步与异步结合:可以灵活地在需要时同步获取异步任务的结果。

协程(Coroutines)的引入

C++20引入了协程,一种轻量级的线程机制,能够实现更高效的异步编程。协程使得异步代码看起来像同步代码,提升了代码的可读性和可维护性。

示例:

#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
using namespace std;

// 协程返回类型
struct ReturnObject {
    struct promise_type {
        ReturnObject get_return_object() { return {}; }
        suspend_never initial_suspend() { return {}; }
        suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

// 协程函数
ReturnObject asyncTask() {
    cout << "协程任务开始..." << endl;
    this_thread::sleep_for(chrono::seconds(2));  // 模拟耗时
    cout << "协程任务结束。" << endl;
}

int main() {
    cout << "主线程继续执行..." << endl;
    auto task = asyncTask();  // 启动协程
    cout << "主线程等待协程完成..." << endl;
    this_thread::sleep_for(chrono::seconds(3));  // 等待协程完成
    cout << "主线程结束。" << endl;
    return 0;
}

输出:

主线程继续执行...
协程任务开始...
主线程等待协程完成...
协程任务结束。
主线程结束。

优势:

  • 简洁的异步代码:协程使得异步操作的代码结构简洁,类似于同步代码,提升了可读性。
  • 高效的上下文切换:协程的上下文切换开销低于传统线程,适合高并发场景。
  • 更好的可维护性:协程帮助维护更清晰的控制流,减少了回调嵌套。

注意事项:

  • 编译器支持:确保所使用的编译器支持C++20协程特性。
  • 理解协程机制:协程的概念相对复杂,需要深入理解其工作原理和使用场景。

总结

C++指针是掌握C++编程的基石,深入理解指针的概念、操作和应用对于编写高效、灵活的程序至关重要。本文从指针的基础概念入手,逐步探讨了指针的基本操作、与数组和函数的结合、动态内存管理、以及高级应用如多级指针和成员函数指针。通过丰富的实战案例,展示了指针在数据结构中的应用和指针相关错误的调试方法。

随着C++11及以后的标准引入了智能指针、Lambda表达式、std::function以及C++20的协程等现代特性,指针的使用变得更加安全和高效。智能指针的引入极大地减少了内存管理的复杂性,而协程则为异步编程提供了更为简洁和高性能的实现方式。

关键要点:

  • 指针基础:理解指针的声明、初始化、解引用等基本操作,熟悉指针的运算和类型转换。
  • 动态内存管理:熟练使用newdelete进行动态内存分配,避免内存泄漏和野指针问题。
  • 智能指针:利用std::unique_ptrstd::shared_ptrstd::weak_ptr自动管理内存,提高代码安全性。
  • 指针与数据结构:掌握指针在链表、树和图等数据结构中的应用,构建高效的动态数据结构。
  • 错误处理:识别和解决常见的指针错误,如野指针、悬空指针和指针越界,提升程序的稳定性。
  • 现代特性:结合C++的现代特性,如Lambda表达式、std::function和协程,编写更加灵活和高性能的代码。

指针虽然强大,但也需要谨慎使用。良好的指针管理习惯和现代C++特性的合理运用,能够帮助开发者有效地利用指针的优势,避免常见的陷阱和错误,编写出高效、安全的C++程序。


参考资料

  1. C++ Reference
  2. C++ Primer - Stanley B. Lippman 等
  3. Effective Modern C++ - Scott Meyers
  4. C++ Concurrency in Action - Anthony Williams
  5. The C++ Programming Language - Bjarne Stroustrup
  6. C++20 Coroutines
  7. Boost Libraries
  8. Smart Pointers Explained
  9. Memory Management in C++
  10. Modern C++ Design Patterns

标签

C++、指针、智能指针、动态内存管理、数据结构、函数指针、Lambda表达式、回调函数、协程、内存泄漏

版权声明

本文版权归作者所有,未经允许,请勿转载。

相关文章:

  • 第六讲 | vector的使用及其模拟实现
  • 绿算轻舟系列FPGA加速卡:驱动数字化转型的核心动力
  • 敏感数据触发后怎么保障安全?
  • Windows10 微软五笔 造词造句
  • 矩阵求导 Ref 0
  • 跨境电商中的几种支付方式——T/T、L/C、D/P、D/A、O/A
  • 【新能源汽车压力采集与数据处理技术方案:从传感器到智能分析的硬核实战指南】
  • The first day of vue
  • openGauss新特性 | 自动参数化执行计划缓存
  • 三层架构与分层解耦:深入理解IOC与DI设计模式
  • 微信小程序实现table样式,自带合并行合并列
  • 网络中的基本概念
  • 虚幻引擎 Anim To Tex| RVT | RT
  • CTF web入门之文件上传
  • 【STL】set
  • 判断一棵树是不是另一棵树的子树
  • 容器实战高手课笔记 ----来源《极客时间》
  • 【C到Java的深度跃迁:从指针到对象,从过程到生态】第一模块·认知转型篇 —— 第二章 开发环境全景搭建:从gcc到JVM的范式迁移
  • 聊聊价值投资
  • 【Qt】Qt Creator开发基础:项目创建、界面解析与核心概念入门
  • 吕国范任河南省人民政府副省长
  • 新加坡选情渐热:播客、短视频各显神通,总理反对身份政治
  • 中国航天员乘组完成在轨交接,神十九乘组将于29日返回地球
  • 没有雷军的车展:老外扎堆,萌车、机器狗谁更抢镜?| 湃客Talk
  • 重新认识中国女性|婚姻,古代传统家庭再生产的根本之道
  • ​王毅会见塔吉克斯坦外长穆赫里丁