C++ 指针

C++ 中的指针是一个变量,用于存储内存地址。指针可以指向任何类型的数据,并且可以用于动态内存分配、数组操作、函数参数传递等。C++中的一个重要概念及其特点,也是掌握C++比较困难的部分。指针也就是内存地址,指针变量是用来存放内存地址的变量,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。本文主要介绍C++中的指针。

1、C++指针

指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,必须在使用指针存储其他变量地址之前,对其进行声明。

& 是一元运算符,返回操作数的内存地址。例如,如果 var 是一个整型变量,则 &var 是它的地址。该运算符与其他一元运算符具有相同的优先级,在运算时它是从右向左顺序进行的。

*是一元运算符,返回操作数所指定地址的变量的值。指针声明示例如下:

int    *ip;    /* 一个整型的指针 */
double *dp;    /* 一个 double 型的指针 */
float  *fp;    /* 一个浮点型的指针 */
char   *ch;    /* 一个字符型的指针 */

2、指针的使用

变量定义的时候给变量初始化,没有给指针初始化,就会出现野指针,该指针的指向并不是我们所希望的,一旦错误的释放了这个指针,就会发生内存的访问。指针使用之后,如果不释放指针所使用的内存,就会造成内存的泄露,这样就会有大量内存由于没能释放,别的程序不可以使用这部分内存,如果一个程序不停申请内存而不去释放内存,很快就会造成系统的崩溃。在使用指针时一定要判断指针是否为空,如果为空,则做相应的操作。如果不做判断,则可能错误的使用空指针。

例如,

#include <iostream>
using namespace std;
 
int main ()
{
   int  var = 20;   /* 实际变量的声明 */
   int  *ip;        /* 指针变量的声明 */
 
   ip = &var;  /* 在指针变量中存储 var 的地址 */
 
   cout << "var 变量的地址: " << &var << endl;
 
   /* 在指针变量中存储的地址 */
   cout << "ip 变量存储的地址: " << ip << endl;
 
   /* 使用指针访问值 */
   cout << "*ip 变量的值: " << *ip << endl;
   /*使用空指针初始化*/ 
   int *pIntegerVal=NULL;
   /*用变量初始化指针*/
   int length=5; 
   int *pIntegerTemp=&length;
   /*分配内存初始化指针*/ 
   int *pInteger=(int*)malloc(10*sizeof(int));
   //为指针分配大小为10个整数的内存空间。
   /*内存申请和释放*/
   if(pInteger != NULL)
  {
  free(pInteger);
  pInteger=NULL;//指针释放之后并不为空,要设置其为空
  }
  pInteger=(int*)malloc(10*sizeof(int));
  if(pInteger == NULL)
  {
   cout << "内存申请没有成功\n!";
  }
   return 0;
}

 3、C++ 中的 NULL 指针

变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值。赋为 NULL 值的指针被称为空指针。

NULL 指针是一个定义在标准库中的值为零的常量。这样能很好的避句野指针。

例如,

#include <iostream>
using namespace std;
int main ()
{
   int  *ptr = NULL;
   cout << "ptr 的地址是 " << ptr << endl;
   return 0;
}

注意:

大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。

4、nullptr 关键字

nullptr关键字就是表示空指针,C++11之前空指针都是NULLnullptr表示空指针,不能转换为整型类型。为了向后兼容,C++11仍允许用0(NULL)来表示空指针。nullptr相比0(NULL)具有更高的类型安全,推荐使用nullptr

#include <iostream>
using namespace std;

void foo(int* ptr) {
    if (ptr == nullptr) {
        std::cout << "Received null pointer" << std::endl;
    } else {
        std::cout << "Received non-null pointer" << std::endl;
    }
}

int main() {
    int* ptr = nullptr;  // 使用 nullptr 初始化指针
    foo(ptr);  // 传递 nullptr

    return 0;
}

5、C++ 智能指针

C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。智能指针的作用是管理一个指针。因为可能申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,智能指针就是一个类,当超出了类的作用域,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

1)auto_ptr

std::auto_ptr 是 C++98/03 中的一种智能指针类型,它用于自动管理动态分配的内存。但从 C++11 开始,std::auto_ptr 被弃用,推荐使用 std::unique_ptrstd::shared_ptr 来替代。

#include <iostream>
#include <memory>  // 包含 auto_ptr
using namespace std;

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass Constructor\n";
    }
    ~MyClass() {
        std::cout << "MyClass Destructor\n";
    }
    void show() {
        std::cout << "Hello from MyClass!\n";
    }
};

int main() {
    // 使用 auto_ptr
    // 创建并初始化 auto_ptr
    std::auto_ptr<MyClass> ptr1(new MyClass); 
    
    // 通过 auto_ptr 调用成员函数
    ptr1->show();  

    // 转移所有权到 ptr2
    std::auto_ptr<MyClass> ptr2 = ptr1;

    // ptr1 已经变为 NULL,不能再使用
    if (ptr1.get() == nullptr) {
        std::cout << "ptr1 is null.\n";
    }
    // ptr2 拥有对象的所有权
    ptr2->show();

    // ptr2 超出作用域时,自动销毁 MyClass 对象
    return 0;
}

2)shared_ptr

shared_ptr采用引用计数的方式管理所指向的对象。当有一个新的shared_ptr指向同一个对象时(复制shared_ptr等),引用计数加1。当shared_ptr离开作用域时,引用计数减1。当引用计数为0时,释放所管理的内存。

#include <iostream>
#include <memory>  // 包含 shared_ptr
using namespace std;

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor\n";
    }
    ~MyClass() {
        std::cout << "MyClass destructor\n";
    }
    void greet() {
        std::cout << "Hello from MyClass!\n";
    }
};

int main() {
    // 创建一个 shared_ptr,指向一个动态分配的 MyClass 对象
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
    // 使用 shared_ptr 调用成员函数
    ptr1->greet();
    {
        // 创建另一个 shared_ptr,指向同一个对象
        std::shared_ptr<MyClass> ptr2 = ptr1;
        // 使用 ptr2 调用成员函数
        ptr2->greet();     
        // 当 ptr2 离开作用域时,它会自动释放所管理的对象
    }

    // ptr1 仍然持有指向 MyClass 对象的引用
    // 当 ptr1 离开作用域时,MyClass 的析构函数会被调用,内存会被释放
    return 0;
}

3)weak_ptr

weak_ptr一般和shared_ptr配合使用。它可以指向shared_ptr所指向的对象,但是却不增加对象的引用计数。当出现weak_ptr所指向的对象实际上已经被释放了的情况。weak_ptr有一个lock函数,尝试取回一个指向对象的shared_ptrstd::weak_ptr 是 C++11 引入的智能指针,它用于解决 std::shared_ptr 引起的循环引用问题。std::weak_ptr 不会增加对象的引用计数,它只是观察 shared_ptr 指向的对象。当 shared_ptr 对象被销毁时,weak_ptr 自动失效。

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

class MyClass {
public:
    MyClass(int val) : value(val) {
        cout << "MyClass(" << value << ") created.\n";
    }
    ~MyClass() {
        cout << "MyClass(" << value << ") destroyed.\n";
    }
    void display() {
        cout << "Value: " << value << endl;
    }
private:
    int value;
};

void demonstrateWeakPtr() {
    shared_ptr<MyClass> sp1 = make_shared<MyClass>(10);
    // weak_ptr 指向 sp1 管理的对象
    weak_ptr<MyClass> wp1 = sp1; 
    if (auto sp2 = wp1.lock()) { // lock() 检查对象是否仍然存在
        cout << "Weak pointer is valid.\n";
        sp2->display();
    } else {
        cout << "Weak pointer is expired.\n";
    }
    sp1.reset(); // 释放资源
    if (auto sp2 = wp1.lock()) {
        cout << "Weak pointer is valid.\n";
        sp2->display();
    } else {
        cout << "Weak pointer is expired.\n";
    }
}

int main() {
    demonstrateWeakPtr();
    return 0;
}

4)unique_ptr

unique_ptr对于所指向的对象, 指向的对象是独占的。不可以对unique_ptr进行拷贝、赋值等操作,但是可以通过release函数在unique_ptr之间转移控制权。

#include <iostream>
#include <memory> // 包含 unique_ptr
using namespace std;

class Test {
public:
    Test() {
        std::cout << "Test 构造函数被调用!" << std::endl;
    }
    ~Test() {
        std::cout << "Test 析构函数被调用!" << std::endl;
    }
    void display() {
        std::cout << "Test::display() 被调用!" << std::endl;
    }
};

int main() {
    // 手动分配并传递给 unique_ptr
    std::unique_ptr<Test> ptr(new Test());

    // 使用 ptr 访问 Test 对象
    ptr->display();

    // unique_ptr 自动释放内存
    return 0;
}

6、动态内存分配 (new 和 delete)

C++ 的动态内存分配允许在程序运行时动态地分配内存,而不是在编译时就固定分配。通过 newdelete 操作符,可以在堆区(heap)上分配和释放内存。避免双重释放(Double Free),同一个内存块不能被 delete 两次,否则会导致未定义行为。通常在删除指针后,最好将其置为 nullptr,以防止误用。

#include <iostream>

int main() {
    // 动态分配一个整数
    int* ptr = new int;  
    *ptr = 100;
    std::cout << "Value: " << *ptr << std::endl;  // 输出 100

    // 释放内存
    delete ptr;

    // 动态分配一个整数数组
    int* arr = new int[3];  
    arr[0] = 10;
    arr[1] = 20;
    arr[2] = 30;

    std::cout << "Array elements: ";
    for (int i = 0; i < 3; ++i) {
        std::cout << arr[i] << " ";  // 输出 10 20 30
    }
    std::cout << std::endl;

    // 释放数组内存
    delete[] arr;

    return 0;
}

7、指向指针的指针 (双重指针)

C++ 中的指向指针的指针(通常称为双重指针)是一个指针,它存储另一个指针的地址。双重指针的基本概念是,通过两层间接访问来操作存储在内存中的值。

#include <iostream>
using namespace std;

int main() {
    int num = 10;
    int* ptr = &num;   // ptr 是指向 num 的指针
    int** ptr2 = &ptr;  // ptr2 是指向 ptr 的指针

     // 输出 num: 10
    cout << "num: " << num << endl;      
     // 输出 ptr: 10
    cout << "ptr: " << *ptr << endl;  
     // 输出 ptr2: 10
    cout << "ptr2: " << **ptr2 << endl;  

    // 修改 num 的值通过双重指针
    **ptr2 = 20;
    cout << "After modification:" << endl;
    // 输出 num: 20
    cout << "num: " << num << endl;     
    // 输出 ptr: 20
    cout << "ptr: " << *ptr << endl;     
    // 输出 ptr2: 20
    cout << "ptr2: " << **ptr2 << endl;   

    return 0;
}

1)双重指针与数组

双重指针经常用于处理二维数组(动态分配的数组)。二维数组实际上是一个数组的数组,可以通过双重指针访问每个元素。

#include <iostream>
using namespace std;

int main() {
    int rows = 3, cols = 3;
    
    // 动态分配二维数组
    int** arr = new int*[rows];  // 创建指向 int* 的指针数组
    for (int i = 0; i < rows; i++) {
        arr[i] = new int[cols];  // 为每一行分配列
    }

    // 初始化数组
    int value = 1;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            arr[i][j] = value++;
        }
    }

    // 打印数组
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            cout << arr[i][j] << " ";
        }
        cout << endl;
    }

    // 释放内存
    for (int i = 0; i < rows; i++) {
        delete[] arr[i];  // 删除每一行
    }
    delete[] arr;  // 删除行指针数组

    return 0;
}

2)双重指针作为函数参数

双重指针常用于函数中,以便修改指针本身或通过指针修改数据。

#include <iostream>
using namespace std;

void changePointer(int** ptr) {
    // 修改 ptr 指向的指针的值
    *ptr = new int(100);  // 分配新的内存并赋值
}

int main() {
    int* ptr = new int(10);  // 初始指针指向值 10
    cout << "Before: " << *ptr << endl;  // 输出 10

    changePointer(&ptr);  // 传递 ptr 的地址

    cout << "After: " << *ptr << endl;  // 输出 100

    delete ptr;  // 释放内存
    return 0;
}

8、常量指针和指针常量

 C++ 中,常量指针和指针常量都是与指针相关的概念,区别在于它们限制了指针或指向的数据是否可以被修改。常量指针是指指向常量数据的指针,也就是说,通过常量指针不能修改指向的值,但指针本身(即指向的地址)是可以修改的。指针常量是指指针本身是常量的,即指针的值(即指向的地址)不能修改,但可以通过指针修改指向地址的内容。

#include <iostream>
using namespace std;

int main() {
    int num1 = 10, num2 = 20;
    
    // 常量指针(指向常量数据)
    // ptr1 是一个常量指针,指向一个常量整数
    const int* ptr1 = &num1;  
    std::cout << "Value pointed to by ptr1: "
    << *ptr1 << std::endl;  // 输出 10
    
    // *ptr1 = 30;  // 错误:不能修改 ptr1 指向的内容
    
    ptr1 = &num2;  // 可以改变 ptr1 的指向
    std::cout << "Now ptr1 points to: "
    << *ptr1 << std::endl;  // 输出 20
    
    // 指针常量(常量指针,指向非常量数据)
    int* const ptr2 = &num1;  // ptr2 是一个常量指针,指向一个可修改的整数
    std::cout << "Value pointed to by ptr2: " 
    << *ptr2 << std::endl;  // 输出 10
    
    *ptr2 = 30;  // 可以修改 ptr2 指向的内容
    std::cout 
    << "After modifying the value pointed to by ptr2: "
    << *ptr2 << std::endl;  // 输出 30
    
    // ptr2 = &num2;  
    // 错误:不能修改 ptr2 的指向
    
    return 0;
}

推荐阅读
cjavapy编程之路首页