MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 指针使用需要注意的关键点

2021-09-277.7k 阅读

指针的基本概念与定义

在C++ 中,指针是一种特殊的变量类型,它存储的是内存地址。通过指针,我们可以直接访问和操作内存中的数据,这赋予了我们强大的编程能力,但同时也带来了一些需要特别注意的地方。

指针变量的定义形式为:类型 *指针变量名。例如:

int num = 10;
int *ptr; // 定义一个指向int类型的指针
ptr = # // 将num的地址赋给ptr

在这里,int *ptr 定义了一个名为 ptr 的指针变量,它可以指向 int 类型的数据。& 是取地址运算符,&num 获取了变量 num 在内存中的地址,并将其赋给了指针 ptr

指针的初始化

  1. 初始化的重要性 指针在使用前必须初始化,否则它将指向一个未定义的内存位置,这可能导致程序崩溃或产生难以调试的错误。例如:
int *ptr; // 未初始化的指针
// 尝试使用ptr,这是危险的
// std::cout << *ptr << std::endl; // 未定义行为

上述代码中,ptr 是一个未初始化的指针,若试图解引用(使用 *ptr)它,将导致未定义行为。程序可能会崩溃,也可能产生一些看似随机的结果。

  1. 正确的初始化方式
    • 可以在定义指针时就进行初始化:
int num = 10;
int *ptr = &num; // 定义并初始化指针
  • 也可以先定义,再在合适的地方初始化:
int num = 10;
int *ptr;
ptr = &num;
  1. 指向常量的指针与常量指针
    • 指向常量的指针: 指向常量的指针不能通过指针修改所指向的值,但指针本身可以指向其他地址。定义形式为 const 类型 *指针变量名。例如:
const int num = 10;
const int *ptr = &num;
// *ptr = 20; // 错误,不能通过指向常量的指针修改值
int otherNum = 20;
ptr = &otherNum; // 合法,指针可以指向其他地址
  • 常量指针: 常量指针是指针本身是常量,一旦初始化后,它不能再指向其他地址,但可以通过指针修改所指向的值(如果所指向的值不是常量)。定义形式为 类型 * const 指针变量名。例如:
int num1 = 10;
int num2 = 20;
int * const ptr = &num1;
// ptr = &num2; // 错误,常量指针不能再指向其他地址
*ptr = 30; // 合法,通过常量指针修改所指向的值
  1. 指向常量的常量指针 这是一种结合了上述两种特性的指针,定义形式为 const 类型 * const 指针变量名。一旦初始化,指针既不能指向其他地址,也不能通过指针修改所指向的值。例如:
const int num = 10;
const int * const ptr = &num;
// *ptr = 20; // 错误,不能通过指针修改值
// ptr = &otherNum; // 错误,指针不能再指向其他地址

指针运算

  1. 指针的算术运算 指针可以进行一些算术运算,如加法、减法。指针的算术运算与指针所指向的数据类型的大小密切相关。
    • 指针加法: 当对指针进行加法运算时,指针实际增加的字节数是其指向数据类型大小乘以所加的整数。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
ptr = ptr + 2; // ptr现在指向arr[2]

在上述代码中,int 类型通常占用4个字节(在32位系统中),ptr + 2 使得 ptr 增加了 2 * sizeof(int) = 8 个字节,从而指向了 arr[2]

  • 指针减法: 指针减法可以计算两个指针之间的距离(以所指向数据类型的个数为单位)。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr1 = &arr[0];
int *ptr2 = &arr[3];
int distance = ptr2 - ptr1; // distance为3

这里 ptr2 - ptr1 的结果是3,因为 ptr2ptr1 之间相差3个 int 类型的数据。

  1. 指针比较 指针可以进行比较运算,如 ==!=<> 等。指针比较是基于它们所存储的内存地址进行的。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr1 = &arr[0];
int *ptr2 = &arr[2];
if (ptr1 < ptr2) {
    std::cout << "ptr1指向的地址小于ptr2指向的地址" << std::endl;
}

在上述代码中,由于数组在内存中是连续存储的,arr[0] 的地址小于 arr[2] 的地址,所以比较结果为真。

指针与数组

  1. 数组名作为指针 在C++ 中,数组名在大多数情况下可以被看作是一个指向数组首元素的常量指针。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 等同于 int *ptr = &arr[0];

这里 arr 就像一个常量指针,指向 arr[0]。我们可以通过指针的方式来访问数组元素:

int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
for (int i = 0; i < 5; i++) {
    std::cout << *(ptr + i) << " "; // 等同于 std::cout << arr[i] << " ";
}
  1. 指针与多维数组 对于多维数组,情况会稍微复杂一些。以二维数组为例,二维数组可以看作是数组的数组。例如:
int arr[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

这里 arr 是一个指向包含4个 int 类型元素的数组的指针。arr[0]arr[1]arr[2] 分别是指向每一行首元素的指针。如果要访问 arr[1][2],可以通过以下指针方式:

int arr[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};
int *ptr = &arr[0][0];
int value = *(ptr + 1 * 4 + 2); // 等同于 arr[1][2]

在上述代码中,1 * 4 + 2 计算出了 arr[1][2] 在一维线性内存中的偏移量。

  1. 动态数组与指针 我们可以使用指针来创建动态数组,即运行时才分配内存的数组。使用 new[] 运算符来分配动态数组,使用 delete[] 运算符来释放内存。例如:
int size = 5;
int *dynamicArr = new int[size];
for (int i = 0; i < size; i++) {
    dynamicArr[i] = i * 2;
}
// 使用完动态数组后,释放内存
delete[] dynamicArr;

在上述代码中,new int[size] 分配了一块连续的内存,大小为 sizeint 类型的空间,并返回指向这块内存首地址的指针 dynamicArr。使用完后,通过 delete[] dynamicArr 释放内存,防止内存泄漏。

指针与函数

  1. 函数参数中的指针 函数可以接受指针作为参数,这使得函数能够直接操作调用者提供的数据,而不是操作数据的副本。例如:
void changeValue(int *ptr) {
    *ptr = 100;
}
int main() {
    int num = 50;
    changeValue(&num);
    std::cout << "num的值为: " << num << std::endl; // 输出 100
    return 0;
}

在上述代码中,changeValue 函数接受一个 int 类型的指针 ptr。通过解引用 ptr,函数可以修改 num 的值。

  1. 函数返回指针 函数也可以返回指针,但需要特别注意返回的指针所指向的内存的生命周期。例如:
int * createArray() {
    int *arr = new int[5];
    for (int i = 0; i < 5; i++) {
        arr[i] = i;
    }
    return arr;
}
int main() {
    int *result = createArray();
    // 使用result数组
    for (int i = 0; i < 5; i++) {
        std::cout << result[i] << " ";
    }
    // 记得释放内存
    delete[] result;
    return 0;
}

在上述代码中,createArray 函数创建了一个动态数组并返回指向它的指针。在 main 函数中,我们接收这个指针并使用数组,最后要记得释放内存,以避免内存泄漏。

  1. 指针与函数指针 函数指针是指向函数的指针,它存储了函数在内存中的入口地址。函数指针的定义形式为 返回类型 (*指针变量名)(参数列表)。例如:
int add(int a, int b) {
    return a + b;
}
int main() {
    int (*funcPtr)(int, int) = add;
    int result = funcPtr(3, 5);
    std::cout << "结果为: " << result << std::endl; // 输出 8
    return 0;
}

在上述代码中,int (*funcPtr)(int, int) 定义了一个函数指针 funcPtr,它可以指向返回 int 类型且接受两个 int 类型参数的函数。funcPtr = addfuncPtr 指向了 add 函数,然后可以通过 funcPtr 调用 add 函数。

指针与类

  1. 类中的指针成员 类可以包含指针成员变量,这在实现动态数据结构(如链表、树等)时非常有用。例如,实现一个简单的链表节点类:
class ListNode {
public:
    int data;
    ListNode *next;
    ListNode(int value) : data(value), next(nullptr) {}
};

在上述代码中,ListNode 类包含一个 int 类型的数据成员 data 和一个指向 ListNode 类型的指针成员 nextnext 指针用于链接下一个节点,nullptr 表示链表的末尾。

  1. this指针 在类的成员函数中,this 指针是一个隐含的指针,它指向调用该成员函数的对象。例如:
class Rectangle {
private:
    int width;
    int height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    int getArea() {
        return this->width * this->height;
    }
};

getArea 成员函数中,this->widththis->height 用于访问对象的成员变量。this 指针使得成员函数能够明确操作哪个对象的成员变量。

  1. 指针与对象生命周期 当使用指针来管理对象时,需要特别注意对象的生命周期。例如,使用 new 运算符创建对象时,要记得使用 delete 运算符释放内存:
class MyClass {
public:
    MyClass() {
        std::cout << "MyClass对象被创建" << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass对象被销毁" << std::endl;
    }
};
int main() {
    MyClass *obj = new MyClass();
    // 使用obj
    delete obj;
    return 0;
}

在上述代码中,new MyClass() 创建了一个 MyClass 对象,并返回指向它的指针 obj。使用完后,通过 delete obj 释放内存,从而调用对象的析构函数,确保资源被正确释放。

指针相关的常见错误与避免方法

  1. 空指针引用 空指针是指值为 nullptr(在C++ 11及以后)或 NULL(在C++ 11之前)的指针,它不指向任何有效的内存地址。解引用空指针会导致未定义行为,通常会使程序崩溃。例如:
int *ptr = nullptr;
// std::cout << *ptr << std::endl; // 未定义行为,空指针引用

为避免空指针引用,在解引用指针前,一定要检查指针是否为 nullptr

int *ptr = nullptr;
if (ptr != nullptr) {
    std::cout << *ptr << std::endl;
}
  1. 野指针 野指针是指向已释放或未初始化内存的指针。例如:
int *ptr = new int(10);
delete ptr;
// 此时ptr成为野指针
// std::cout << *ptr << std::endl; // 未定义行为,野指针引用

为避免野指针,在释放内存后,将指针赋值为 nullptr

int *ptr = new int(10);
delete ptr;
ptr = nullptr;
  1. 内存泄漏 内存泄漏发生在动态分配的内存没有被释放的情况下。例如:
void memoryLeak() {
    int *ptr = new int(10);
    // 没有释放ptr所指向的内存
}

为避免内存泄漏,确保在使用完动态分配的内存后,使用相应的 deletedelete[] 运算符释放内存。如果在函数中分配了内存,可以考虑使用智能指针(如 std::unique_ptrstd::shared_ptr 等)来自动管理内存,它们会在适当的时候自动释放内存。例如:

#include <memory>
void noMemoryLeak() {
    std::unique_ptr<int> ptr(new int(10));
    // 当ptr超出作用域时,内存会自动释放
}
  1. 指针类型不匹配 指针类型不匹配可能导致难以调试的错误。例如:
int num = 10;
int *intPtr = &num;
double *doublePtr = reinterpret_cast<double*>(intPtr); // 不推荐,指针类型不匹配
// 使用doublePtr可能导致未定义行为

在进行指针类型转换时,要确保转换是合理的并且符合程序的逻辑。尽量避免使用 reinterpret_cast,除非有非常明确的需求。如果需要进行类型转换,可以考虑使用 static_castdynamic_cast(用于多态类型转换)。例如:

int num = 10;
int *intPtr = &num;
// 假设我们有一个函数接受void* 类型指针
void someFunction(void *ptr) {
    // 这里如果要使用intPtr,可以使用static_cast进行转换
    int *newIntPtr = static_cast<int*>(ptr);
    std::cout << *newIntPtr << std::endl;
}
someFunction(intPtr);

智能指针

  1. std::unique_ptr std::unique_ptr 是C++ 11引入的一种智能指针,它拥有对所指向对象的唯一所有权。当 std::unique_ptr 被销毁时,它会自动释放所指向的对象。例如:
#include <memory>
int main() {
    std::unique_ptr<int> ptr(new int(10));
    std::cout << *ptr << std::endl;
    // 当ptr超出作用域时,内存会自动释放
    return 0;
}

std::unique_ptr 不能被复制,但可以被移动。例如:

#include <memory>
void takeUniquePtr(std::unique_ptr<int> newPtr) {
    std::cout << *newPtr << std::endl;
}
int main() {
    std::unique_ptr<int> ptr(new int(10));
    takeUniquePtr(std::move(ptr));
    // 此时ptr不再拥有对象,对象的所有权转移到takeUniquePtr函数中的newPtr
    return 0;
}
  1. std::shared_ptr std::shared_ptr 也是C++ 11引入的智能指针,它允许多个 std::shared_ptr 指向同一个对象,通过引用计数来管理对象的生命周期。当最后一个指向对象的 std::shared_ptr 被销毁时,对象的内存会被释放。例如:
#include <memory>
int main() {
    std::shared_ptr<int> ptr1(new int(10));
    std::shared_ptr<int> ptr2 = ptr1; // ptr2和ptr1指向同一个对象
    std::cout << "ptr1的引用计数: " << ptr1.use_count() << std::endl; // 输出 2
    std::cout << "ptr2的引用计数: " << ptr2.use_count() << std::endl; // 输出 2
    // 当ptr1和ptr2超出作用域时,对象的内存会被释放
    return 0;
}
  1. std::weak_ptr std::weak_ptr 是一种弱引用,它指向由 std::shared_ptr 管理的对象,但不会增加对象的引用计数。std::weak_ptr 主要用于解决 std::shared_ptr 可能产生的循环引用问题。例如:
#include <memory>
class B;
class A {
public:
    std::shared_ptr<B> bPtr;
    ~A() {
        std::cout << "A对象被销毁" << std::endl;
    }
};
class B {
public:
    std::weak_ptr<A> aWeakPtr;
    ~B() {
        std::cout << "B对象被销毁" << std::endl;
    }
};
int main() {
    std::shared_ptr<A> aPtr(new A());
    std::shared_ptr<B> bPtr(new B());
    aPtr->bPtr = bPtr;
    bPtr->aWeakPtr = aPtr;
    // 这里不会产生循环引用,因为bPtr->aWeakPtr不会增加aPtr的引用计数
    // 当aPtr和bPtr超出作用域时,A和B对象的内存会被正确释放
    return 0;
}

通过深入理解和正确使用指针,我们可以充分发挥C++ 语言的强大功能,编写出高效、健壮的程序。同时,注意避免指针使用中的常见错误,并合理使用智能指针,能有效提高程序的可靠性和可维护性。