【C++进阶】指针:从基础到实践

目录

一、指针基础回顾

1.1. 指针的概念

1.2. C 与 C++ 共通之处

二、C++ 指针的独特之处

2.1 默认值与初始化

2.2 运算符重载

2.3. 指针类型转换

2.4. 引用类型(Reference)

2.5 智能指针:内存管理的革新

2.5.1 std::unique_ptr(独占指针)

2.5.2 std::shared_ptr(共享指针)

2.5.3 std::weak_ptr(观察指针)

2.6  指针与面向对象编程

2.6.1 指向类成员的指针

2.6.2  多态与指针

三、指针的高级应用

3.1 函数指针

3.2 指针与模板

3.3  动态内存分配与释放

3.4  指针数组与数组指针

3.5 多级指针

四、性能优化技巧

4.1 内存对齐优化

4.2 指针别名优化

五、指针使用的注意事项

5.1 避免空指针和野指针

5.2 防止内存泄漏

5.3 指针运算的合法性

六、总结

七、参考资料


指针是 C 和 C++ 中非常重要且强大的特性,它提供了直接操作内存的能力。在 C++ 里,指针的概念与 C 语言有诸多相似之处,但也存在一些显著的不同。本文深入探讨 C++ 指针,详细介绍其与 C 语言指针的差异。

一、指针基础回顾

1.1. 指针的概念

指针是一个变量,它存储的是另一个变量的内存地址。通过指针,我们可以直接访问和操作内存中的数据。在C语言中,指针主要承担以下职责:

  • 动态内存管理

  • 函数参数传递优化

  • 数据结构的实现(链表、树等)

  • 直接硬件访问

传统C指针的基本形式:

int var = 10;
int *ptr = &var;
printf("%d", *ptr);  // 输出10

1.2. C 与 C++ 共通之处

①指针的定义与初始化

在 C 和 C++ 中,指针的定义和初始化方式基本相同。

#include <iostream>

int main() {
    int num = 10;
    // 定义一个指向 int 类型的指针,并初始化为 num 的地址
    int* ptr = &num; 

    std::cout << "num 的值: " << num << std::endl;
    std::cout << "num 的地址: " << &num << std::endl;
    std::cout << "ptr 存储的地址: " << ptr << std::endl;
    std::cout << "ptr 指向的值: " << *ptr << std::endl;

    return 0;
}

定义了一个整数变量 num,并创建了一个指向 num 的指针 ptr。通过 & 运算符获取变量的地址,使用 * 运算符解引用指针以访问其指向的值。

②指针的算术运算

指针可以进行算术运算,如加法和减法。在 C 和 C++ 中,指针算术运算的规则是一致的。

#include <iostream>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int* ptr = arr;  // 指向数组首元素

    std::cout << "第一个元素: " << *ptr << std::endl;
    ptr++;  // 指针向后移动一个 int 类型的位置
    std::cout << "第二个元素: " << *ptr << std::endl;

    return 0;
}

定义了一个整数数组 arr,并让指针 ptr 指向数组的首元素。通过 ptr++ 操作,指针向后移动一个 int 类型的位置,从而指向数组的下一个元素。 

二、C++ 指针的独特之处

2.1 默认值与初始化

在C语言中,如果声明了一个未初始化的指针,它将指向一个不确定的位置(通常为NULL)。而在C++中,如果没有明确赋值,所有指针会被隐式初始化为nullptr,表示“无有效值”。使用nullptr可以避免将0(整数)错误地解释为指针的情况,提高了代码的安全性和可读性。

int* p; // C++中,p默认为nullptr
int* q; // C语言中,q可能是任意地址

2.2 运算符重载

C++允许对指针进行自定义运算,如加减操作可以改变指针地址。而C语言的指针运算则更为简单,主要是算术运算和比较运算。

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int* p = arr;
    p++; // 指针向后移动一个元素
    std::cout << *p << std::endl; // 输出: 2
    return 0;
}

2.3. 指针类型转换

C++提供了一些更安全的指针转换函数,如reinterpret_cast,用于在不同类型之间转换指针。而C语言中,类型转换通常依赖于类型标识符(*&)。

int main() {
    void* ptr = new int(10);
    int* intPtr = reinterpret_cast<int*>(ptr);
    std::cout << *intPtr << std::endl; // 输出: 10
    delete intPtr;
    return 0;
}

2.4. 引用类型(Reference)

C++引入的引用类型是对指针的重要补充,它与指针的主要区别:

特性 指针 引用
空值 允许nullptr 必须初始化
重新绑定 可以改变指向 不能改变绑定
内存占用 显式存储地址 通常由编译器优化
访问方式 显式解引用(*) 隐式解引用
多级间接访问 支持多级指针 只支持单级

引用类型可以看作是变量的别名。与指针相比,引用更加安全和易用。

#include <iostream>

void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 5;
    int y = 10;

    std::cout << "交换前: x = " << x << ", y = " << y << std::endl;
    swap(x, y);
    std::cout << "交换后: x = " << x << ", y = " << y << std::endl;

    return 0;
}

swap 函数使用引用作为参数,避免了使用指针的复杂性。引用在定义时必须初始化,并且一旦绑定到一个对象,就不能再绑定到其他对象。 

2.5 智能指针:内存管理的革新

C++ 标准库提供了智能指针,用于自动管理动态分配的内存,避免了手动管理内存带来的内存泄漏问题。这是 C++ 与 C 语言指针的一个重要区别。

2.5.1 std::unique_ptr(独占指针)

std::unique_ptr 是一种独占式智能指针,它确保同一时间只有一个指针可以指向该对象。

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass 构造函数" << std::endl; }
    ~MyClass() { std::cout << "MyClass 析构函数" << std::endl; }
    void doSomething() { std::cout << "MyClass 执行操作" << std::endl; }
};

int main() {
    // 创建一个 std::unique_ptr 并指向 MyClass 对象
    std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
    ptr->doSomething();

    // 当 ptr 离开作用域时,MyClass 对象会自动被销毁
    return 0;
}

std::unique_ptr 会在其生命周期结束时自动释放所指向的对象,无需手动调用 delete

2.5.2 std::shared_ptr(共享指针)

std::shared_ptr 是一种共享式智能指针,多个 std::shared_ptr 可以指向同一个对象,并且会维护一个引用计数。当引用计数为 0 时,对象会被自动销毁。

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass 构造函数" << std::endl; }
    ~MyClass() { std::cout << "MyClass 析构函数" << std::endl; }
    void doSomething() { std::cout << "MyClass 执行操作" << std::endl; }
};

int main() {
    // 创建一个 std::shared_ptr 并指向 MyClass 对象
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
    std::shared_ptr<MyClass> ptr2 = ptr1;  // 共享所有权

    ptr1->doSomething();
    ptr2->doSomething();

    // 当 ptr1 和 ptr2 都离开作用域时,MyClass 对象会自动被销毁
    return 0;
}

ptr1 和 ptr2 共享同一个 MyClass 对象的所有权,引用计数会在它们创建和销毁时自动更新。 

2.5.3 std::weak_ptr(观察指针)

std::weak_ptr 是一种弱引用智能指针,它不拥有对象的所有权,而是对 std::shared_ptr 所管理的对象的一种弱引用。主要用于解决 std::shared_ptr 可能出现的循环引用问题。

#include <iostream>
#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A 析构函数" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> a_ptr;  // 使用 std::weak_ptr 避免循环引用
    ~B() { std::cout << "B 析构函数" << std::endl; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->b_ptr = b;
    b->a_ptr = a;

    return 0;
}

如果 B 类中的 a_ptr 也使用 std::shared_ptr,就会形成循环引用,导致对象无法正常销毁。使用 std::weak_ptr 可以避免这种情况。

2.6  指针与面向对象编程

C++ 是一种面向对象的编程语言,指针在面向对象编程中有着独特的应用。

2.6.1 指向类成员的指针

C++ 允许定义指向类成员的指针,包括指向成员变量和成员函数的指针。

#include <iostream>

class MyClass {
public:
    int value;
    void printValue() { std::cout << "Value: " << value << std::endl; }
};

int main() {
    MyClass obj;
    obj.value = 10;

    // 指向成员变量的指针
    int MyClass::* ptrToMember = &MyClass::value;
    std::cout << "通过指针访问成员变量: " << obj.*ptrToMember << std::endl;

    // 指向成员函数的指针
    void (MyClass::* ptrToFunction)() = &MyClass::printValue;
    (obj.*ptrToFunction)();

    return 0;
}

定义了指向 MyClass 类的成员变量 value 和成员函数 printValue 的指针,通过这些指针访问成员变量和调用成员函数。

2.6.2  多态与指针

C++ 的多态性使得通过基类指针可以调用派生类的成员函数。

#include <iostream>

class Base {
public:
    virtual void print() { std::cout << "Base 类的 print 函数" << std::endl; }
};

class Derived : public Base {
public:
    void print() override { std::cout << "Derived 类的 print 函数" << std::endl; }
};

int main() {
    Base* basePtr;
    Derived derivedObj;

    basePtr = &derivedObj;
    basePtr->print();  // 调用 Derived 类的 print 函数

    return 0;
}

Base 类的 print 函数被声明为虚函数,通过基类指针 basePtr 指向派生类对象 derivedObj,并调用 print 函数,实际调用的是派生类的 print 函数,实现了多态性。 

三、指针的高级应用

3.1 函数指针

函数指针是指向函数的指针,它可以用来实现回调机制等高级功能。

#include <iostream>

// 定义一个函数类型
typedef int (*MathFunction)(int, int);

// 加法函数
int add(int a, int b) {
    return a + b;
}

// 减法函数
int subtract(int a, int b) {
    return a - b;
}

// 执行数学运算的函数,接受一个函数指针作为参数
int performOperation(int a, int b, MathFunction func) {
    return func(a, b);
}

int main() {
    int x = 5;
    int y = 3;

    // 使用函数指针调用加法函数
    int result1 = performOperation(x, y, add);
    std::cout << "加法结果: " << result1 << std::endl;

    // 使用函数指针调用减法函数
    int result2 = performOperation(x, y, subtract);
    std::cout << "减法结果: " << result2 << std::endl;

    return 0;
}

定义了一个函数指针类型 MathFunction,并使用它来实现一个通用的 performOperation 函数,该函数可以接受不同的数学函数作为参数。

3.2 指针与模板

C++ 的模板机制可以与指针结合使用,实现更灵活的代码。

#include <iostream>

// 模板函数,交换两个指针所指向的值
template <typename T>
void swapPointers(T*& a, T*& b) {
    T* temp = a;
    a = b;
    b = temp;
}

int main() {
    int num1 = 10;
    int num2 = 20;
    int* ptr1 = &num1;
    int* ptr2 = &num2;

    std::cout << "交换前: *ptr1 = " << *ptr1 << ", *ptr2 = " << *ptr2 << std::endl;
    swapPointers(ptr1, ptr2);
    std::cout << "交换后: *ptr1 = " << *ptr1 << ", *ptr2 = " << *ptr2 << std::endl;

    return 0;
}

定义了一个模板函数 swapPointers,用于交换两个指针所指向的值。该函数可以处理不同类型的指针。

3.3  动态内存分配与释放

使用newdelete运算符可以在运行时动态地分配和释放内存。

int main() {
    int* p = new int(10); // 动态分配一个整数并初始化为10
    std::cout << *p << std::endl; // 输出: 10
    delete p; // 释放动态分配的内存
    p = nullptr; // 避免悬挂指针
    return 0;
}

对于动态分配的数组,需要使用delete[]来释放内存。

int main() {
    int* arr = new int[5] {1, 2, 3, 4, 5};
    for (int i = 0; i < 5; i++) {
        std::cout << arr[i] << " "; // 输出: 1 2 3 4 5
    }
    std::cout << std::endl;
    delete[] arr; // 释放动态分配的数组内存
    return 0;
}

3.4  指针数组与数组指针

指针数组:数组的元素是指针类型。

int main() {
    int* ptrArray[3]; // 声明一个包含3个整型指针的数组
    int a = 1, b = 2, c = 3;
    ptrArray[0] = &a;
    ptrArray[1] = &b;
    ptrArray[2] = &c;
    for (int i = 0; i < 3; i++) {
        std::cout << *ptrArray[i] << " "; // 输出: 1 2 3
    }
    std::cout << std::endl;
    return 0;
}

 数组指针:指针指向整个数组。

int main() {
    int arr[3] = {1, 2, 3};
    int (*ptr)[3] = &arr; // 声明一个指向包含3个整数的数组的指针
    for (int i = 0; i < 3; i++) {
        std::cout << (*ptr)[i] << " "; // 输出: 1 2 3
    }
    std::cout << std::endl;
    return 0;
}

3.5 多级指针

多级指针是指向指针的指针,可以用于更复杂的内存管理和数据结构。

int main() {
    int a = 10;
    int* p = &a;
    int** pp = &p; // pp是一个指向指针p的指针
    std::cout << **pp << std::endl; // 输出: 10
    return 0;
}

、性能优化技巧

4.1 内存对齐优化

alignas关键字的使用:

struct alignas(64) CacheLine {
    int data[16];
};  // 确保结构体按64字节对齐

4.2 指针别名优化

restrict关键字(C++未正式支持,但编译器扩展):

void addArrays(int* __restrict a, 
              const int* __restrict b, 
              int size) {
    for(int i=0; i<size; ++i) {
        a[i] += b[i];  // 编译器可做更好优化
    }
}

五、指针使用的注意事项

5.1 避免空指针和野指针

空指针指向内存地址0,通常用于初始化指针变量。野指针则指向非法的内存空间,使用空指针和野指针都会导致未定义行为。

#include <iostream>

int main() {
    int* p = nullptr; // 初始化指针为空指针
    // int* p = (int*)0x1111; // 野指针,不要这样做
    if (p != nullptr) {
        std::cout << *p << std::endl; // 如果p不是空指针,则解引用
    } else {
        std::cout << "Pointer is nullptr" << std::endl;
    }
    return 0;
}

5.2 防止内存泄漏

动态分配的内存必须在使用完后及时释放,否则会导致内存泄漏。

#include <iostream>

int main() {
    int* p = new int(10);
    // 使用p指向的内存
    delete p; // 释放内存
    p = nullptr; // 避免悬挂指针
    return 0;
}

使用智能指针可以有效避免这个问题。 

#include <iostream>
#include <memory>

int main() {
    // 使用智能指针管理动态分配的内存
    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    std::cout << *ptr << std::endl;

    // 当 ptr 离开作用域时,内存会自动释放
    return 0;
}

使用 std::unique_ptr 来管理动态分配的整数,避免了手动调用 delete 可能带来的内存泄漏问题。 

5.3 指针运算的合法性

在进行指针运算时,要确保指针的合法性和有效性,避免出现越界访问等问题。

#include <iostream>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int* p = arr;
    for (int i = 0; i < 5; i++) {
        std::cout << *(p + i) << " "; // 合法访问数组元素
    }
    std::cout << std::endl;
    // *(p + 5) 是越界访问,不要这样做
    return 0;
}

六、总结

C++ 指针继承了 C 语言指针的强大功能,同时引入了许多新的特性,如引用类型、智能指针、指向类成员的指针等。这些特性使得 C++ 指针在内存管理、面向对象编程等方面更加安全和灵活。在使用指针时,要注意空指针和内存泄漏等问题,合理利用智能指针可以有效避免这些问题。


七、参考资料

  •  《C++ Primer(第 5 版)》这本书是 C++ 领域的经典之作,对 C++ 的基础语法和高级特性都有深入讲解。
  • 《Effective C++(第 3 版)》书中包含了很多 C++ 编程的实用建议和最佳实践。
  • 《C++ Templates: The Complete Guide(第 2 版)》该书聚焦于 C++ 模板编程,而using声明在模板编程中有着重要应用,如定义模板类型别名等。
  • C++ 官方标准文档:C++ 标准文档是最权威的参考资料,可以查阅最新的 C++ 标准(如 C++11、C++14、C++17、C++20 等)文档。例如,ISO/IEC 14882:2020 是 C++20 标准的文档,可从相关渠道获取其详细内容。
  • cppreference.com:这是一个非常全面的 C++ 在线参考网站,提供了详细的 C++ 语言和标准库文档。
  • LearnCpp.com:该网站提供了系统的 C++ 教程,配有丰富的示例代码和清晰的解释,适合初学者学习和理解相关知识。