C++ 指针

344 阅读7分钟

指针(pointer)是一个值为内存地址的变量(或数据对象),而内存地址就类似于身份证号码。

指针的定义和初始化

数据类型 * 指针变量名;

int* ptr_num;   //声明了一个int型的指针ptr_num
char* ptr_name;     //声明了一个char型的指针ptr_name

注意:

  1. int* p 的写法偏向于地址,即 p 就是一个地址变量,表示一个十六进制地址。
  2. int *p 的写法偏向于值,*p 是一个整型变量,能够表示一个整型值。
    • int* ptr1 = #
    • int *ptr2 = nullptr;
  3. 声明中的 * 号和使用中的 * 号意义完全不同

取地址、取值操作:

int num = 1024;
int* ptr_num;
ptr_num = #     // 取地址
*ptr_num = 1111;    // 取值

这里关于 char* 类型的打印有一些需要注意的:

char ch = 'a';
char* ptr_ch = &ch;
cout << ptr_ch << '\t' << (void*)ptr_ch << endl;

输出结果

a      0x61fe17

这里可以发现,ptr_ch 的打印结果并不是一个地址,这是因为 char* 类型在 C++ 中被默认为是一个字符串,所以打印的话是将地址作为一个字符串来打印的。如果想要打印出地址的话,可以将其强制转换成 void* 类型进行输出。

空指针

空指针不指向任何对象,在试图使用一个指针之前可以先检查是否为空。

定义

int *ptr1 = nullptr; // 等价于 int *ptr1 = 0;
int *ptr2 = 0;      // 直接将ptr2初始化为字面常量0
// 或者包含cstdlib头文件,使用NULL
int *ptr3 = NULL

定义指针的时候一定要给一个值,不然会有默认值,是一个随机的内存地址,称为野指针,这是非常危险的。

void* 指针

一种特殊的指针类型,可以存放任意对象的地址。

注意:

  1. void* 指针存放一个内存地址,地址指向的内容是什么类型不能确定,所以不能通过它去修改地址内容。
  2. void* 类型指针一般用来和别的指针比较,作为函数的输入和输出或是赋值给另一个 void* 指针。

指针注意点

  1. 指针可存放任何基本数据类型、数组和其他所有高级数据结构的地址。
  2. 若指针已声明为指向某种类型数据的地址,那么他不能用于存储其他类型数据的地址。
  3. 应为指针指定一个地址之后,才能在语句中使用指针,不然直接使用野指针是很危险的。

引用

所谓的引用就是为对象起了另一个别名。

用法

int& int_value = 1024;
// ref_value指向int_value,是int_value的一个别名
int& ref_value = int_value;
// 错误:引用必须被初始化
int& ref_value2;

注意点

  1. 引用并非对象,只是为一个已经存在的对象起的别名。
  2. 引用只能绑定在对象上,不能与字面值或某个表达式的计算结果绑定在一起。
  3. 引用必须初始化,所以使用引用之前不需要测试其有效性,因此使用引用可能会比使用指针效率高。 例如 *ptr; 想要直接取 ptr 的值,就必须要检验 ptr 这个指针是否初始化了,而引用则不需要。

这里有一句话

指向常量的引用是非法的。

这句话其实是错误的,引用是可以指向常量的,因为有 const 这种常量类型存在。

int& ref_value = 10; // 这种写法是错误的
// 如果想要指向常量可以这样写
const int& ref_value = 10;

指针和引用

  1. 引用其实就是对指针进行了简单封装,底层仍然是指针。
  2. 获取引用地址时,编译器会进行内部转换。

例如:

int num = 108;
int* ptr_num = &num;
int& ref_num = num;
// 上面两个语句是等价的,ref_num == *ptr_num。
cout << boolalpha;
cout << &num << '\t' << &ref_num << '\t' << ptr_num << '\t'
     << (ref_num == *ptr_num) << endl;

输出结果

0x61fe0c        0x61fe0c        0x61fe0c        true

指针和数组

数组:

  • 存储在一块连续的内存空间中
  • 数组名就是这块连续内存空间的首地址

虽然数组名和指针存放的都是地址,使用指针指向数组的首地址就可以和数组名一样正常访问数组,但是看如下代码:

double score[] {11, 22, 33};
double *ptr_score = score;
cout << sizeof(score) << '\t' << sizeof(ptr_score) << endl;

输出结果

24      8

可以发现,数组名和指针还是有区别的,score 代表的是 double[3] 这么一个数组类型,而 ptr_score 是一个指针类型,不是同一个东西。

此外,指针可以进行算术运算修改,但是数组名是不可以修改的。

指针算术运算

  • 指针的递增和递减(++、--)
  • 指针加上或减去某个整数值

注意点

  • 一个类型为T的指针的移动,以sizeof(T)为移动单位。
  • 这种对指针的加减是可能会越界的,如果一个指向数组的指针,超过了数组的存储范围,就会指向未知的内存地址,这是很危险的。

动态分配内存

可以用户自定义需要使用的内存空间,通过关键字 new 来分配内存空间。

int* p = new int;
char* p1 = new char[10];
delete p;
delete [] p1;
  • 指针真正的用武之地:在运行阶段分配未命名的内存空间
  • 在此情况下,只能通过指针来访问内存!

使用关键字 delete 释放内存,new 分配的内存一定要释放,不然有可能造成内存泄漏问题。

  • new 配对使用
  • 不要释放已释放的内存
  • 不能是否声明变量分配的内存

注意: 不要创建两个指向同一内存块的指针,有可能误删两次

int* ptr = new int;
int* ptr1 = ptr;
delete ptr;
delete ptr1;

使用 new 创建动态分配的数组

new 运算符返回第一个元素的地址,数组的释放使用 delete[] 释放内存

int* int_array = new int[10];
delete [] int_array;

关于 newdelete 使用的规则

  1. 不要使用 delete 释放不是 new 分配的内存
  2. 不要使用 delete 释放同一内存两次
  3. 如果使用 new[] 为数组分配内存,则对应 delete[] 释放内存
  4. 对空指针使用 delete 是安全的

程序的内存分配

  • 栈区(stack)
    • 由编译器自动分配释放,一般存放函数的参数值、局部变量的值等
    • 操作方式类似数据结构中的栈-先进后出
  • 堆区(heap)
    • 一般由程序员分配释放,若程序不释放,并且程序结束时操作系统没有回收,那么就会造成内存泄漏
    • 注意: 与数据结构中的堆是两回事,分配方式类似链表
  • 全局区(静态区-static)
    • 全局变量和静态变量是存储在一起的
    • 程序结束后由系统释放
  • 文字常量区
    • 常量字符串就放在这里,程序结束后由系统释放
  • 程序代码区
    • 存放函数体的二进制代码
int num1 = 0;   //全局初始化区
int* ptr1;      //全局未初始化区
int main() {
    //栈区
    int num2;
    //栈区      "xxxx"是在常量区的, str是在栈区的
    char str[] = "xxxx";
    //栈区
    char* ptr2 = nullptr;
    //全局(静态)初始化区
    static int num3 = 1024;
    //分配的内存在堆区
    ptr1 = new int[10];
    ptr2 = new char[20];
    //注意:ptr1和ptr2本身是在栈区中的
    delete [] ptr1;
    delete [] ptr2;
    
    return 0;
}

用指针创建二维数组

int (*p)[2] = new int[4][2];

这里相当于 p 指向了一块大小为4的内存空间首地址,这个空间里存放了4个大小为2的一维数组的首地址。
关于指针创建的二维数组其在内存的分配图如下: