一、指针基础核心语法

1. 指针的本质

指针:我们知道内存基本存储单位是byte,而内存中每个byte都有个唯一编号,称为他的地址,具体讲就是个十六进制数,也称为指针。
指针变量:存放变量内存地址的特殊变量,核心作用是通过地址间接访问、修改目标变量的值。

2. 指针的定义与初始化

定义语法

指向某类型变量的指针,定义格式为:类型名* 指针名;类型名 *指针名;

  • 示例:int* ptr;(定义一个指向int型变量的指针ptr)
  • 真题高频错误写法:*int ptr;int ptr*;ptr int;

初始化规则

指针必须用同类型变量的内存地址初始化,未初始化的指针为「野指针」,禁止直接解引用。

  • 正确示例:

    int a = 10;
    int* p = &a; // 用变量a的地址&a初始化指针p,p指向a
  • 高频错误写法:

    1. 用变量值直接初始化指针:int a=5; int *p=a;(错误,把变量值当内存地址)
    2. 用普通变量存指针:int a=5; int p=&a;(错误,内存地址无法存入普通变量)
    3. 未初始化就解引用赋值:int* ptr; *ptr = 10;(错误,野指针,访问非法内存)

3. 核心运算符:取地址符& 与 解引用符*

  • &变量名:取该变量在内存中的起始地址,示例:&a 得到变量a的地址。
  • *指针名:解引用,访问指针指向的内存空间,可读取/修改目标变量的值。
  • 真题高频核心考点:通过解引用修改原变量的值

    int a = 42;
    int* p = &a;
    *p = *p + 1; // 等价于 a = a + 1,最终a的值为43

    解引用操作修改的是指针指向的原变量,而非指针本身。
    这里特别注意对p和 p 的理解,修改p是修改它指向谁,而修改 p则是修改它指向地址里面的内容

4. 同类型指针的赋值

同类型的指针可以直接互相赋值,赋值后多个指针指向同一块内存空间,修改任意一个指针的解引用值,都会影响原变量。

  • 真题示例(第4题):

    int a = 5;
    int* p1 = &a;
    int* p2 = p1; // p2和p1指向同一个变量a
    *p2 = 10; // 等价于 a=10,最终a、*p1、*p2的值均为10

二、指针与函数传参

1. 三种参数传递方式对比

传递方式函数形参定义实参传递能否修改实参能否避免大对象拷贝
值传递void func(int x)直接传变量func(a)❌ 仅修改副本,不影响实参❌ 会生成完整副本
指针传递void func(int* p)传变量地址func(&a)✅ 通过解引用*p修改实参✅ 仅传递地址,无拷贝
引用传递void func(int& x)直接传变量func(a)✅ 直接修改实参✅ 仅传递别名,无拷贝

2. 核心考点

  1. 值传递的本质:函数接收的是实参的副本,函数内对形参的任何修改,都不会影响外部实参。

    • 示例:

      void increaseA(int x) { x++; }
      int main() {
          int a = 5;
          increaseA(a); // 值传递,仅修改副本x,a的值仍为5
          return 0;
      }
  2. 指针传递的用法:形参为指针类型,实参必须传递变量的地址;函数内通过*解引用,直接修改实参的值。

    • 示例:

      void increaseB(int* p) { (*p)++; }
      int main() {
          int a = 5;
          increaseB(&a); // 传a的地址,解引用修改a,最终a=6
          return 0;
      }
    • 易错点:必须加括号(*p)++,若写成*p++,会因运算符优先级问题,先移动指针再解引用,不会修改原变量的值。
  3. 引用传递的用法
    引用: 也叫别名,本身不占内存,声明后直接绑定到某个现有同类型数据,俩名字代表一个东西。
    形参为变量的引用(别名),函数内对形参的修改直接作用于实参,用法比指针更简洁。
  4. 其他考点:想要避免大型对象的拷贝,或在函数内修改外部实参,指针传递和引用传递均可实现,值传递无法实现。

三、指针与一维数组(核心考点)

1. 数组名的本质

数组名是数组首元素的内存地址,是一个常量指针,不能修改其指向。

  • 核心等价关系:arr == &arr[0],数组名arr直接代表数组第一个元素的地址。

2. 数组与指针的等价访问规则

数组的下标访问,完全可以用指针的解引用实现,核心等价公式:
arr[i] == *(arr + i)

  • 示例:arr[2] 等价于 *(arr + 2),代表数组第3个元素的值。

3. 指针的算术运算

指针加减整数,不是简单的地址数值加减,而是按指针指向的类型大小进行偏移

  • 示例:int* p;p+1 代表地址向后偏移 sizeof(int)(4字节),指向数组的下一个元素,而非地址十六进制数值+1(实际相当于+4)。
  • 示例:

    int arr[4] = {0,1,2,3};
    int* p = arr;
    p += 1;
    // 最终p的值是arr[1]的地址,而非数值1

4. 指针的下标访问

指向数组的指针,可以直接使用数组的[]下标语法,等价于解引用操作:
p[i] == *(p + i)

  • 示例:

    double* p_arr = new double[3];
    p_arr[0] = 0.2; p_arr[1] = 0.5; p_arr[2] = 0.8;
    p_arr += 1; // 指针向后偏移1个元素,指向原p_arr[1]
    cout << p_arr[0] << endl; // 等价于*(p_arr+0),输出0.5

四、指针与二维数组

1. 二维数组的内存布局

C++中二维数组采用行优先存储,所有元素在内存中是连续排列的。

  • 示例:int arr[2][3] = {{1,2,3},{4,5,6}};
    实际内存存储顺序为顺序的:arr[0][0] → arr[0][1] → arr[0][2] → arr[1][0] → arr[1][1] → arr[1][2],也就是说如果arr0后面的地址里就是arr1。

2. 二维数组的地址等价转换

arr[i][j] == *(*(arr + i) + j)

  • 公式解释:

    1. arr + i:偏移i行,指向第i行的一维数组;
    2. *(arr + i):取第i行的首地址,等价于arr[i]
    3. *(arr + i) + j:在第i行中偏移j列,指向arr[i][j]的地址;
    4. *(*(arr + i) + j):解引用,得到arr[i][j]的值。
  • 示例:int arr[2][3] = {{1,2,3},{4,5,6}};*(*(arr + 1) + 2) 等价于arr[1][2],值为6。

3. 二维数组名的本质

二维数组名是指向一维数组的指针(数组指针)

  • 示例:int arr[3][4];,arr的类型为int (*)[4],指向一个长度为4的int型一维数组;arr + 1 会偏移一整行的大小(4*4=16字节),直接指向第二行的起始位置。

4. 用一级指针遍历二维数组

利用二维数组内存连续的特性,可通过一级指针遍历所有元素,核心公式:
arr[i][j] == *(p + i * 列数 + j),其中p = &arr[0][0](数组首元素地址)

  • 真题示例(第10题):

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

5. 二维数组的内存地址计算

已知数组首地址、元素类型大小,计算arr[i][j]的内存地址

  • 示例:
    32位int占4字节,二维数组int array[2][3]首地址为0x7ffee4065820,则&array[1][1]的地址为:
    0x7ffee4065820 + 4 * 4 = 0x7ffee4065820 + 0x10 = 0x7ffee4065830

    • 4 * 4 : 顺序考虑,ar1距离ar0隔着四个元素,每个占四字节。
    • 16进制下的16就是0x10

6. 静态二维数组 vs 动态二维数组

类型定义方式内存位置释放方式内存连续性
静态二维数组int arr[3][4];栈内存函数结束自动释放连续
动态一维数组模拟二维int* arr = new int[12];堆内存手动delete[] arr;释放连续
  • 静态数组在栈上分配,自动释放;动态数组在堆上分配,必须手动释放,无法自动释放

五、指针与结构体(常考考点)

1. 结构体指针的定义与初始化

// 定义结构体
struct Point {
    int x, y;
};
struct Student {
    string name;
    int age;
};

// 结构体指针初始化
Point p = {10, 20};
Point* ptr = &p; // 用结构体变量的地址初始化指针

2. 结构体指针的成员访问

核心访问运算符为->,等价于解引用后用.访问成员:
ptr->成员名 == (*ptr).成员名

  • 真题示例(第16题):

    struct Rectangle {
        Point topLeft;
        Point bottomRight;
    };
    Rectangle rect = {{10,10}, {20,20}};
    Point* p = &rect.bottomRight;
    p -> y = 5; // 等价于 (*p).y =5,修改rect.bottomRight.y的值为5

3. 结构体数组与指针

  • 静态结构体数组:Student students[20];
  • 动态结构体数组:Student* students = new Student[20];(合法写法,真题第19题判断题考点)
  • 错误写法:Student students = new Student[20];(错误,new返回指针,不能赋值给结构体变量)

六、堆内存动态分配(new/delete)

除了我们熟悉的声明变量、数组来申请内存,还可以用new关键字直接申请内存,并把返回地址存到指针里。

1. new运算符:申请堆内存

  • 申请单个变量:int* p = new int; // 分配1个int占用的内存,返回首地址
  • 申请数组:int* p = new int[10];// 分配10个int占用的内存,返回首地址
  • 申请结构体数组:Student* p = new Student[20];// 分配20个Student占用的内存,返回首地址

2. delete运算符:释放堆内存

  • 释放单个变量:delete p;
  • 释放数组:delete[] p;(数组释放必须加[]

3. 核心考点与易错点

  1. 堆内存申请后必须手动释放,否则会造成内存泄漏;栈内存会自动释放,无需手动操作。
  2. 释放内存时,指针必须指向申请时的首地址;若指针已发生偏移,必须先回到首地址再释放,否则会导致程序崩溃。

    double* p_arr = new double[3];
    p_arr += 1; // 指针偏移
    p_arr -= 1; // 回到首地址
    delete p_arr; // 正确释放
  3. 不能释放栈内存的地址,只能释放new申请的堆内存。

七、易错判断题汇总

核心:分清楚类型,是指针还是普通变量

int a, b;
int *p = &a; // 正确 指针赋值给指针变量
b = p; // 错误,指针赋值给普通变量
b = *p; // 正确,解引用得到普通int,赋值给整数变量
a = &b; // 错误,指针赋值给普通变量
int * q = p; // 正确,指针赋值给指针
题目说法正确/错误核心原因
int x=5; int *p=&x; *p=*p+3; 运行后x的值变成8正确解引用修改指针指向的原变量x的值
int a=5; int *p=a; 能正确初始化指针错误不能用变量值初始化指针,必须用变量地址&a
struct Student {...}; Student* students = new Student[20]; 代码合法正确正确定义动态结构体数组,new返回指针赋值给结构体指针
数组arr首地址为0x7ffee4065820,int* p=arr; p+=1;后p的值是1错误指针算术运算按类型大小偏移,p的值是arr[1]的地址,而非数值1
void add(int &x){ x += 10; } int a=5; add(a); 运行后a的值变成15正确引用传递直接修改实参的值
void func(int* p) { *p = 10; } int a=5; func(&a); cout<<a; 输出10正确指针传递通过解引用修改实参a的值
int* ptr; *ptr = 10; 可以正确定义并初始化指针错误指针未初始化(野指针),直接解引用访问非法内存

标签: none

评论已关闭