指针,数组,变量

指针基础概念

在讲解指针之前,首先对变量有个概念。

变量由三个部分组成:1.类型。2.变量值.3.变量的起始地址。

int a=10;这里面就包含第1,2点。那么第三点呢?

获取变量的起始地址:&变量。(这里要注意,这个地址是起始地址,后面的数组会有所关联)

现在可以引进指针的概念了,所谓的指针是一种特殊的变量,专用于存放变量在内存中的起始地址

指针语法:数据类型*变量名(从另一个角度理解,指针=变量,类型为(数据类型*))

指针赋值,使用&。传入的是地址。

声明指针变量后,在没有赋值之前,里面是乱七八糟的值,这时候不能使用指针。

指针存放变量的地址,因此,指针名表示的是地址(就像变量名可以表示变量的值一样)

*运算符被称为间接值 或**解除引用(解引用)**运算符,将它用于指针,可以得到该地址的内存中存储的值,*也是乘法符号,C++根据上下文来确定所指的是乘法还是解引用。

int *ptr=&a;

cpp 复制代码
#include <iostream>
using namespace std;


int main()
{
    SetConsoleOutputCP(65001);
    int a = 10; int b = 20;
    cout << "a = " << a << endl;
    cout << "&a =:" << &a << endl;

    int* ptr = &a;
    cout << "ptr = " << ptr << endl;
    cout << "*ptr = " << *ptr << endl;

    ptr = &b;
    cout << "b = " << b << endl;
    cout << "&b =:" << &b << endl;
    cout << "ptr = " << ptr << endl;
    cout << "*ptr = " << *ptr << endl;
}

指针可以用于函数的参数

如果把函数的形参声明为指针,调用的时候把实参的地址传进去,形参中存放的是实参的地址,在函数中通过解引用的方法直接操作内存中的数据,可以修改实数的值,这种方法被通俗的称为地址传递传地址

简单来说,在函数定义的时候,形参存放地址(数据类型*变量a),而函数内部调用的时候通过解引用进行使用(*变量a)。

cpp 复制代码
#include <windows.h>
#include <iostream>
using namespace std;

void add( int* num1, int* num2)
{
    int sum = *num1 + *num2;
    cout << "Sum = " << sum << endl;
    *num1 = 100; // 修改num1所指向的值
}
int main()
{
    SetConsoleOutputCP(65001);
    int a = 10; int b = 20;
    cout << "a = " << a << endl;
    cout << "&a =:" << &a << endl;

    int* ptr = &a;
    cout << "ptr = " << ptr << endl;
    cout << "*ptr = " << *ptr << endl;

    ptr = &b;
    cout << "b = " << b << endl;
    cout << "&b =:" << &b << endl;
    cout << "ptr = " << ptr << endl;
    cout << "*ptr = " << *ptr << endl;

    add(&a, &b);
    cout << "a = " << endl;


}

现在我们基本上对指针有个概念了,指针就是指向变量的起始地址。他就向一个箭头,那么这个箭头还是有自由度进行操作的,例如:指向谁?能否通过指针进行更改变量值?

这时候引入了const

1.常量指针 const 数据类型*变量名

这个东西是规定这个指针指向的变量值无法更,但是可以更改它的指向。这时候的指针就相当于只能读取,而无法更改。一般用于修饰函数的形参,表示不希望在函数里修改内存地址中的值。

2.指针常量 数据类型*const 变量名

这个东西是规定这个指只能指向某个变量,无法更换指向,但是可以读取以及更改变量值。

3.常指针常量 const 数据类型 * const 变量名

指向的变量(对象)不可改变,不能通过解引用的方法修改内存地址中的值。


这时候我们考虑另外的问题,指针除了用来指向单纯的变量还能做什么?还能指向什么?

我们先看这个c++的内存空间图

栈: 存储局部变量、函数参数和返回值。

堆: 存储动态开辟内存的变量。

数据段: 存储全局变量和静态变量。

代码段: 存储可执行程序的代码和常量(例如字符常量),此存储区不可修改。

栈是系统管理的,无需我们来操作,但是堆不同,这块是我们动态分配,需要自己创建以及销毁,不销毁的话会消耗资源的。

这时候为了在堆上变量进行跟踪,确保生成以及消耗,我们使用指针来跟踪它。什么意思呢?

我们在堆上申请一块内存空间,然后用指针指向它,当我们不用的时候将内存中的信息清空,指针消失,以此来管理堆,以此完成动态分配(new,detlete)

步骤:

1)声明一个指针;

2)用new运算符向系统申请一块内存,让指针指向这块内存;

3)通过对指针解引用的方法,像使用变量一样使用这块内存;

4)如果这块内存不用了,用delete运算符释放它。

cpp 复制代码
	int* p = new int(5);
	cout << "*p=" << *p << endl;
	*p = 8;
	cout << "*p=" << *p << endl;
	delete p;

int* p = new int(5);

重点理解这个:

  1. 在堆(heap)上动态分配一块足够存放一个 int 类型的内存
  2. 将这块内存中的值初始化为 5
  3. 把这块内存的地址赋值给指针变量 p

指针上面讲的是指向某个变量的起始位置,那如果我们想让指针改变指向的话我们可以通过改变地址对吧,例如int*ptr=&a。但是在函数值传递的时候不行,为什么?

举个例子:

cpp 复制代码
#include <iostream>
using namespace std;

void badFunction(int* p) {
    p = new int(100);   // 想让主函数的指针指向新内存
    cout << "badFunction  *p = " << *p << endl;  // 输出 100
}

int main() {
    int* ptr = nullptr;
    badFunction(ptr);

    // 问题来了:ptr 改变了吗?
    if (ptr == nullptr) {
        cout << "main ptr = nullptr!" << endl;  // 会输出这行!
    }

    delete ptr;
    // 如果这里 delete ptr; 就会出错(因为是 nullptr)
    return 0;
}

这时候我们运行发现是不改变的,为什么?因为是值传递!

  • 调用 badFunction(ptr) 时,ptr 的值(即地址)复制给了形参 p
  • 所以 pptr 的一个副本
  • 在函数里修改 p = new int(100); 只是改了副本,不影响主函数的 ptr

那么如果我们形参是地址的话确实是可以改变的

cpp 复制代码
#include <iostream>
using namespace std;

void badFunction(int* p) {
    p = new int(100);   // 想让主函数的指针指向新内存
    cout << "badFunction  *p = " << *p << endl;  // 输出 100
}
void goodFunction(int* &p) {
    p = new int(100); // 直接修改主函数的指针
}

int main() {
    int* ptr = nullptr;
    badFunction(ptr);

    // 问题来了:ptr 改变了吗?
    if (ptr == nullptr) {
        cout << "main ptr = nullptr!" << endl;  // 会输出这行!
    }

    goodFunction(ptr);
    cout << *ptr << endl;
    delete ptr;
    // 如果这里 delete ptr; 就会出错(因为是 nullptr)
    return 0;
}

我们看看这个形参int* &p

这个像什么?是不是相当于一个指针指向了指针的地址啊,所以引入了二级指针

二级指针: 数据类型** 指针名;

注意几个点,形参使用数据类型**变量名,而在调用的时候输入的实参是地址&指针名。

cpp 复制代码
#include <iostream>
using namespace std;

void badFunction(int* p) {
    p = new int(100);   // 想让主函数的指针指向新内存
    cout << "badFunction  *p = " << *p << endl;  // 输出 100
}
void goodFunction(int*&p) {
    p = new int(100); // 直接修改主函数的指针
}
void Function(int** p) {
    *p = new int(100); // 直接修改主函数的指针
}



int main() {
    int* ptr = nullptr;
    badFunction(ptr);

    // 问题来了:ptr 改变了吗?
    if (ptr == nullptr) {
        cout << "main ptr = nullptr!" << endl;  // 会输出这行!
    }

    goodFunction(ptr);
    cout << *ptr << endl;

    Function(&ptr);
    cout << *ptr << endl;
    delete ptr;
    // 如果这里 delete ptr; 就会出错(因为是 nullptr)
    return 0;
}

野指针就是指针指向的不是一个有效(合法)的地址。

在程序中,如果访问野指针,可能会造成程序的崩溃。

出现野指针的情况主要有三种:

1)指针在定义的时候,如果没有进行初始化,它的值是不确定的(乱指一气)。

cpp 复制代码
int* p;          // 危险!p 的值是随机垃圾地址
*p = 10;         // ❌ 试图写入未知内存 → 崩溃 or 隐蔽错误

2)如果用指针指向了动态分配的内存,内存被释放后,指针不会置空,但是,指向的地址已失效。

cpp 复制代码
int* p = new int(42);
delete p;
// 此时 p 仍保存着原地址,但该内存已归还系统
cout << *p;      // ❌ 读取已释放内存 → 野指针!

3)指针指向的变量已超越变量的作用域(变量的内存空间已被系统回收),让指针指向了函数的局部变量,或者把函数的局部变量的地址作为返回值赋给了指针。

cpp 复制代码
int* getPtr() {
    int x = 100;
    return &x;   // ❌ x 是局部变量,函数结束即销毁
}

int main() {
    int* p = getPtr();
    cout << *p;  // ❌ 野指针!x 的内存已被回收
}

规避方法:

1)指针在定义的时候,如果没地方指,就初始化为nullptr。

2)动态分配的内存被释放后,将其置为nullptr。

3)函数不要返回局部变量的地址。

注意:野指针的危害比空指针要大很多,在程序中,如果访问野指针,可能 会造成程序的崩溃。是可能,不是一定,程序的表现是不稳定,增加了调试程序的难度。


我们不仅通过指针调用变量,还想使用指针调用函数。

函数指针是一个变量,它存储的是"函数在代码段中的入口地址" 。因为函数的二进制代码存放在内存四区中的代码段,函数的地址是它在内存中的起始地址。如果把函数的地址作为参数传递给函数,就可以在函数中灵活的调用其它函数。

使用函数指针的三个步骤:

a)声明函数指针;

cpp 复制代码
返回类型 (*指针名)(参数类型列表);

b)让函数指针指向函数的地址;

cpp 复制代码
函数名就是函数的地址。
函数指针的赋值:函数指针名=函数名;

c)通过函数指针调用函数。

cpp 复制代码
(*函数指针名)(实参);
函数指针名(实参);
场景 使用方式 声明方式
普通 int 指针 *p 得到 int 值 int *p;
函数指针 callback(5) 调用函数 int (*callback)(int);
cpp 复制代码
void process(int x, int (*callback)(int)) {
    int result = callback(x);
    cout << "Processed: " << result << endl;
}

int square(int n) { return n * n; }

int main() {
    process(5, square);  // 把 square 作为回调传入
}

讲完变量以及指针,就引入数组。为什么要数组,原因很简单,因为实用,可以存储信息等等。

一维数组:是一组数据类型相同的变量,可以存放一组数据。

1.创建数组:数据类型 数组名[数组长度]

数组长度必须是整数,可以是常量,也可以是变量和表达式

2.数组的使用 数组名[数组下标]

有两种方式:

第一种: 下标访问 下标从 0size-1

cpp 复制代码
arr[0] = 10;        // 第一个元素
arr[i] = arr[i-1];  // i 是变量也可以

第二种:指针访问,指针指向数组的起始位置

cpp 复制代码
int arr[5] = { 1,2,3,4,5 };
int* p = arr;       // arr 自动转为 &arr[0]
cout << *(p + 2) << endl;  // 输出 3
//注意!!!
cout << sizeof(arr) << endl;  // 返回 5 * sizeof(int) → 整个数组大小
cout << sizeof(p) << endl;    // 返回 sizeof(int*) → 指针大小(8 字节)

3.内存布局

  • 连续存储;
  • &arr[0], &arr[1], ... 地址依次递增 sizeof(元素)
  • arr == &arr[0](值相等,但类型不同:int* vs int(*)[5])。

4.数组初始化

数据类型 数组名[数组长度] = { 值1,值2,值3, ...... , 值n};

数据类型 数组名[ ] = { 值1,值2,值3, ...... , 值n};

数据类型 数组名[数组长度] = { 0 }; // 把全部的元素初始化为0。

数据类型 数组名[数组长度] = { }; // 把全部的元素初始化为0。

5.清空 & 复制数组

清空:memset()函数

cpp 复制代码
#include <cstring>  // C++ 中推荐 <cstring>,而非 <string.h>

int arr[100];
memset(arr, 0, sizeof(arr));  // 全部字节设为 0 → 对 int/char 等有效

复制: memcpy 复制数组

cpp 复制代码
int src[5] = {1,2,3,4,5};
int dest[5];
memcpy(dest, src, sizeof(src));  

在这里浅说一下,最好使用stl的array以及vector

容器 适用场景 特点
std::array<T, N> 大小固定且已知 (编译期确定) 例如:坐标 {x,y,z}、RGB 颜色、月份名等 - 栈上分配(快) - 零额外开销(和传统数组性能一样) - 支持所有容器接口
std::vector<T> 大小未知或会变化 例如:读取用户输入的数字列表、动态链表缓存等 - 堆上分配 - 自动管理内存 - 支持 push_back, resize

详细讲讲一维数组和指针的关系

1.指针的算术

将一个整型变量加1后,其值将增加1。

但是,将指针变量(地址的值)加1后,增加的量等于它指向的数据类型的字节数。

2.数组的地址

a)数组在内存中占用的空间是连续的。

b)C++将数组名解释为数组第0个元素的地址。

c)数组第0个元素的地址和数组首地址的取值是相同的。

d)数组第n个元素的地址是:数组首地址+n

e)C++编译器把 数组名[下标] 解释为 *(数组首地址+下标)

3数组的本质

数组是占用连续空间的一块内存,数组名被解释为数组第0个元素的地址。C++操作这块内存有两种方法:数组解释法和指针表示法,它们是等价的。

4.数组名不总是被解释为地址

两种例外情况(数组名不会退化为指针):

情况 行为
1. sizeof(数组名) 返回整个数组的字节数,不是指针大小
2. &数组名 返回"整个数组的地址",类型是 int(*)[N](指向数组的指针),不是 int*

5.一维数组用于函数参数

一、核心事实

当一维数组作为函数参数传递时,它会自动退化为指向首元素的指针。

因此,函数无法知道原数组的长度

二、参数声明的两种等价写法

复制代码
void func(int* arr, int len);      // 明确是指针
void func(int arr[], int len);     // 语义更清晰(推荐)
  • 两者在编译器眼中完全相同

  • 即使写成 int arr[100]100 也会被忽略!

三、必须显式传递数组长度

  • 函数内部不能用 sizeof(arr) 获取数组大小(它返回的是指针大小,如 8 字节);

  • 正确做法:由调用者传入长度:

    cpp 复制代码
    int data[5] = {1,2,3,4,5};
    func(data, 5);  // 显式传长度

四、函数内部使用方式

  • 可以用数组语法arr[i]

  • 也可以用指针语法*(arr + i)

  • 两者完全等价,任选其一。

五、例外情况(无需传长度)

仅当数组有结束标志时可省略长度,例如:

  • C 风格字符串(以 \0 结尾):

    cpp 复制代码
    void printStr(const char* s);  // 遇到 '\0' 停止

用new动态创建一维数组

为什么需要动态数组?

类型 分配位置 大小限制 生命周期 是否需手动释放
普通数组(int arr[N] 栈(stack) 小(通常几 MB) 函数结束自动销毁 ❌ 不需要
动态数组(new int[N] 堆(heap) 大(可达 GB 级) 手动控制 ✅ 必须 delete[]

二、动态创建与释放语法

  1. 创建
cpp 复制代码
数据类型* 指针 = new 数据类型[数组长度];

示例:

cpp 复制代码
int* p = new int[10];        // 分配 10 个 int
double* q = new double[n];   // n 是变量(运行时确定)
  1. 释放
cpp 复制代码
delete[] 指针;
指针 = nullptr;  // 避免悬空指针(good practice)

⚠️ 必须用 delete[],不能用 delete


二维数组:

二维数组的本质

二维数组是"数组的数组" ------ 它是一个一维数组,其中每个元素又是一个一维数组。

二、创建二维数组

  1. 基本语法
cpp 复制代码
数据类型 数组名[行数][列数];

示例:

cpp 复制代码
int matrix[3][4];  // 3 行 4 列的整数矩阵

三、内存布局:连续存储(行优先)

cpp 复制代码
int arr[2][3] = {
    {10, 20, 30},
    {40, 50, 60}
};

内存布局(地址递增):

cpp 复制代码
[10][20][30][40][50][60]
 ↑
 &arr[0][0]
  • 总字节数:行数 × 列数 × sizeof(元素)

  • sizeof(arr) 返回整个数组大小(仅在定义处有效)

四、访问元素

cpp 复制代码
arr[i][j]  // 第 i 行、第 j 列(i ∈ [0, 行数-1], j ∈ [0, 列数-1])

等价于指针运算:

cpp 复制代码
*(*(arr + i) + j)

因为:

  • arrint[3] 类型的数组(假设列数=3);

  • arr + i 指向第 i 行(类型为 int(*)[3]);

  • *(arr + i) 是第 i 行(类型为 int[3],退化为 int*);

  • 再加 j 得到具体元素。

五、初始化方式

  1. 按行初始化(推荐)
cpp 复制代码
int mat[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};
  1. 扁平初始化(按内存顺序)
cpp 复制代码
int mat[2][3] = {1, 2, 3, 4, 5, 6};  // 效果同上
  1. 自动推导行数
cpp 复制代码
int mat[][3] = {{1,2,3}, {4,5,6}};  // 行数自动为 2
  1. 全零初始化
cpp 复制代码
int mat[3][4] = {0};   // 全部为 0
int mat[3][4] = {};    // C++11 起,更推荐

⚠️ 注意:如果初始化列表不足,剩余元素自动补 0

六、清空与复制(传统方式)

  1. 清零(仅适用于 POD 类型)
cpp 复制代码
#include <cstring>
memset(mat, 0, sizeof(mat));  // 安全,因为 mat 是连续内存
  1. 复制
cpp 复制代码
int src[2][3] = {{1,2,3},{4,5,6}};
int dest[2][3];
memcpy(dest, src, sizeof(src));  // 安全,因为内存连续

优势:二维数组是连续内存,所以 memset/memcpy 可以直接作用于整个数组(不像指针数组)。

七、二维数组作为函数参数

二维数组传参时,必须让函数知道"每行有多少列" ,因为编译器需要据此计算元素地址。

因此,列数必须在参数中显式指定,而行数可以省略(通过额外参数传入)。

为什么需要"行指针"?

  1. 二维数组的本质
cpp 复制代码
int bh[2][3] = {{11,12,13}, {21,22,23}};
  • bh 是一个包含 2 个元素 的数组;

  • 每个元素是 int[3] 类型(即长度为 3 的整型数组);

  • 所以 bh 的类型是:int[2][3]

  1. 数组名的退化规则
  • 在表达式中,bh 退化为 指向第一个"行"的指针

  • 第一个"行"是 int[3],所以 bh 退化为:int (*)[3](读作:"指向含3个int的数组的指针")

因此:

cpp 复制代码
int (*p)[3] = bh;  // 正确!p 是行指针
int* q = bh;       // ❌ 错误!类型不匹配(除非强制转换)

💡 int (*)[3]int*:前者知道"每行3个int",后者只知道"一个int"。

二维数组传参的两种等价写法

写法 1:显式使用行指针(更清晰底层机制)

cpp 复制代码
void func(int (*p)[3], int rows);

写法 2:数组语法(更易读,推荐)

cpp 复制代码
void func(int p[][3], int rows);
// 或
void func(int p[2][3], int rows);  // 2 会被忽略,实际仍是 int (*)[3]

🔍 编译器视角:两者完全等价!都表示参数 p 的类型是 int (*)[3]

为什么列数不能省略?

因为地址计算依赖列数!

访问 p[i][j] 时,编译器实际计算:

cpp 复制代码
*( *(p + i) + j )
  • p + i 需要知道 一行占多少字节3 * sizeof(int)

  • 如果不知道列数(即不知道每行大小),就无法定位第 i 行!

📌 类比:你知道书有若干页,但不知道每页多少行,就无法快速跳到第 N 行。

cpp 复制代码
#include <iostream>

// 方式1:使用行指针(显式指针语法)
void printMatrix1(int (*p)[3], int rows) {
    std::cout << "=== Using row pointer int (*)[3] ===\n";
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < 3; ++j) {
            std::cout << p[i][j] << " ";  // 数组表示法
            // 或:std::cout << *(*(p + i) + j) << " "; // 指针表示法
        }
        std::cout << "\n";
    }
}

// 方式2:使用数组语法(更易读,推荐)
void printMatrix2(int p[][3], int rows) {
    std::cout << "=== Using array syntax int p[][3] ===\n";
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < 3; ++j) {
            std::cout << p[i][j] << " ";
        }
        std::cout << "\n";
    }
}

int main() {
    // 定义一个 2x3 的二维数组
    int bh[2][3] = {
        {11, 12, 13},
        {21, 22, 23}
    };

    // 验证地址关系
    std::cout << "Address verification:\n";
    std::cout << "bh          = " << bh << "\n";           // 行地址(类型 int(*)[3])
    std::cout << "&bh[0]      = " << &bh[0] << "\n";       // 第0行地址(同上)
    std::cout << "bh[0]       = " << bh[0] << "\n";        // 第0行首元素地址(类型 int*)
    std::cout << "&bh[0][0]   = " << &bh[0][0] << "\n";    // 首元素地址(同上)
    std::cout << "*bh         = " << *bh << "\n";           // 等价于 bh[0]
    std::cout << "\n";
    //(*bh) 指向的是二维数组的第0行(即第0行的首地址)
    //(*bh)[1] 就是第0行第1列的元素,也就是 bh[0][1]
	std::cout << "(*bh)[1]=" << (*bh)[1] << std::endl; // 12
    std::cout << "(*bh)[2]=" << (*bh)[2] << std::endl; // 13
    //(*bh + 1) 是第0行首地址加1,指向第0行的第1个元素,也就是 bh[0] + 1,类型还是 int*。
    //(*bh + 1)[1] 实际上是先移动到 bh[0][1],再取下标为1的元素,也就是 bh[0][2]。
    std::cout << "(*bh+1)[1]=" << (*bh + 1)[1] << std::endl; // 21
    //二维数组在内存中是这样排的:
    //11 12 13 21 22 23
    //(*bh + 1)[2] 就是 *( (*bh + 1) + 2 ),也就是 *(bh[0] + 3),即 bh[1][0],值为 21
    std::cout << "(*bh+1)[2]=" << (*bh + 1)[2] << std::endl; // 21
    //*(bh+1) 把这个指针解引用,得到第1行本身,类型是 int[3],等价于 bh[1]。
    //(*(bh+1))[1] 就是第1行第1列的元素,也就是 bh[1][1]。
    std::cout << "(*(bh+1))[1]=" << (*(bh+1))[1] << std::endl; // 22
   
	std::cout << "*(bh[0]+1)=" << *(bh[0] + 1) << std::endl; // 12
    std::cout << "*(bh[1]+1)=" << *(bh[1] + 1) << std::endl; // 22


    // 调用函数(两种方式完全等价)
    printMatrix1(bh, 2);  // 传 bh,自动退化为 int(*)[3]
    printMatrix2(bh, 2);

    return 0;
}
相关推荐
yqsnjps74658ocz2 小时前
如何检查Visual Studio是否支持C++14?
c++·ide·visual studio
漠然&&2 小时前
实战案例:用 Guava ImmutableList 优化缓存查询系统,解决多线程数据篡改与内存浪费问题
java·开发语言·缓存·guava
buvsvdp50059ac2 小时前
如何在Visual Studio中启用C++17或C++20?
c++·c++20·visual studio
yqsnjps74658ocz2 小时前
如何在Visual Studio中设置项目为C++14?
java·c++·visual studio
buvsvdp50059ac2 小时前
如何在Visual Studio中启用C++14的特性?
java·c++·visual studio
沐怡旸2 小时前
【穿越Effective C++】条款17:以独立语句将newed对象置入智能指针——异常安全的智能指针初始化
c++·面试
zhangkaixuan4562 小时前
Flink 写入 Paimon 流程:Checkpoint 与 Commit 深度剖析
java·开发语言·微服务·flink·paimon
程序猿本员2 小时前
1. 让自己习惯cpp
c++
Wild_Pointer.2 小时前
简析单目相机模型中的针孔模型
c++·数码相机·opencv