一、指针的基本概念

变量的地址

获取 变量在内存中的 起始地址,使用 &变量名

1
2
3
int a;
cout << (void*)&a << '\n'; // 十六进制
cout << (long long)&a << '\n'; // 十进制

指针变量

简称 指针存放 变量在内存中的起始地址,使用 数据类型* 变量名数据类型* 也是一种数据类型,即 x 型指针。

对指针赋值

使用 指针 = &地址,称为指向某变量,被指向的类型称为基类型。

1
2
3
4
5
int a;
int* pa = &a;
cout << (long long)&a << '\n';
cout << (long long)pa << '\n';
// 以上两行的输出结果相同

二、使用指针

注意:声明指针变量后必须初始化。

* 运算符(即 解引用)用于获得该地址的内存中存储的值。

变量和指向变量的指针就像是同一枚硬币的两面,即 &apa 相同,表示的是地址;a*pa 相同,表示的是变量的值。

1
2
3
4
5
6
7
int a = 8;
int* pa = &a;
cout << a << '\n'; // 8
cout << *pa << '\n'; // 8
*pa = 6;
cout << a << '\n'; // 6
cout << *pa << '\n'; // 6

对于普通变量,系统在内部跟踪该内存单元;对于指针变量,系统直接访问该内存单元。

三、指针用于函数的参数

1
2
3
4
5
6
7
8
9
void fun(int *a) {
*a = 6;
}

int main() {
int a = 8;
fun(&a);
cout << a; // 6
}

在函数中修改地址,将修改变量的值,称为 传址。除此之外,可以减少内存拷贝,提升性能。

四、用 const 修饰指针

常量指针

const 数据类型* 变量名,不能解引用以修改内存地址的值,但可以修改指向的变量。

1
2
3
4
int a = 8, b = 6;
const int* pa = &a;
*pa = 6; // Error
*pa = b; // OK

应用:一般用于修饰函数的形参,表示不希望在函数里修改内存地址中的值。

指针常量

数据类型* const 变量名,指向的变量不能改变,但可以解引用以修改内存地址的值。

应用:一般无用。

常指针常量

const 数据类型* const 变量名,指向的变量不能改变,也不能解引用以修改内存地址的值。

应用:一般无用。

五、void 关键字

函数的形参用 void*,表示接受数据类型的指针,如 memset() 中第一个参数。

注意:不能对 void* 指针直接解引用,需要先转换成其它类型的指针。

1
2
3
4
5
6
7
8
9
10
11
void fun(void* p) {
cout << p << '\n';
cout << *p << '\n'; // Error
cout << *(char*)p << '\n'; // OK
}
int main() {
int a = 80;
char c = 'X';
fun(&a);
fun(&b);
}

六、动态分配内存

内存空间

地址从低到高依次为:代码段(可执行代码、常量区)、数据段(全局变量和静态变量)、堆(动态开辟内存的变量,内存很大,需手动管理)、栈(很小,局部变量、函数参数和返回值)、内核空间。

动态分配内存

申请内存:int* p = new 数据类型(初始值)

释放内存:delete 地址

1
2
3
int* p = new int(5);
cout << *p << '\n';
delete p;

七、二级指针

指针用于存放普通变量的地址,二级指针用于存放指针变量的地址,使用 数据类型** 指针名

1
2
3
4
5
6
7
8
9
10
int a = 8;
cout << &a << '\t' << a << '\n';
int* pa = &a;
cout << &pa << '\t' << pa << '\t' << *pa << '\n';
int** ppa = &pa;
cout << &ppa << '\t' << ppa << '\t' << *ppa << '\t' << **ppa << '\n';
// output:
// 000000608ADFF474 8
// 000000608ADFF498 000000608ADFF474 8
// 000000608ADFF4B8 000000608ADFF498 000000608ADFF474 8

在函数中,如果传递普通变量的地址,形参用指针;传递指针的地址,形参用二级指针。

1
2
3
4
5
6
7
8
9
10
void fun(int** pp) {
*pp = new int(8);
cout << pp << *pp << '\n';
delete *pp;
}

int main() {
int* p = NULL;
fun(&p);
}

八、空指针

0NULL 表示空指针(在 C++11 中有更安全的 nullptr),它指向空,不能解引用。

1
2
int* p = nullptr;
cout << *p << '\n'; // Error

为避免程序崩溃,应当判断函数形参是否为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void fun(int *a) {
cout << *a;
}
int main() {
int* p = nullptr;
fun(p);
}
// return ugly

void fun(int *a) {
if (a == 0) return;
cout << *a;
}
int main() {
int* p = nullptr;
fun(p);
}
// OK

九、无效指针

访问无效的指针可能造成程序崩溃,可能原因有:

  • 手动赋值;

  • 未初始化;

  • 释放内存后指针失效:

    1
    2
    3
    4
    5
    int* p = new int(8);
    cout << *p << '\n';
    delete p;
    cout << *p << '\n';
    // return ugly
  • 变量的内存空间已被回收(让指针指向局部变量,将局部变量的地址作为返回值赋给指针):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int* fun() {
    int a = 8;
    return &a;
    }
    int main() {
    int p = fun();
    cout << *p << '\n';
    }
    // return ugly

规避方法:

  • 定义时初始化为空;
  • 释放内存后,置为空;
  • 函数不返回局部变量的地址。

十、一维数组和指针

指针的算术

指针变量加 1,增加量等于 sizeof 数据类型

数组的地址

数组在内存中占用的空间是连续的,数组的地址即数组的第 0 个元素的地址:

1
2
3
4
int a[5] = { 1, 2, 3, 5, 8 };
cout << (long long)a << '\n';
cout << (long long)&a << '\n';
cout << (long long)&a[0] << '\n';

数组的第 $n$ 个元素的地址是 数组首地址 + n,编译器将 a[1] 解释为 *(a + 1)

使用 sizeof 数组名,得到的是整个数组占用内存空间的字节数。

十一、一维数组用于函数的参数

指针的数组表示

编译器将 数组名[下标] 解释为 *(数组名 + 下标),将 指针[下标] 解释为 *(指针 + 下标)

1
2
3
4
5
6
int a[5] = { 1, 2, 3, 5, 8 };
cout << a[2] << '\n';
cout << *(a + 2) << '\n';
int* p = a;
cout << *(p + 2) << '\n';
cout << p[2] << '\n';

于是:

1
2
3
4
5
6
7
int a[5] = { 1, 2, 3, 5, 8 };
cout << a[2] << '\n'; // 3
cout << (&a[2])[0] << '\n'; // 3
cout << (&a[2])[1] << '\n'; // 5
int* p = &a[2];
cout << p[0] << '\n'; // 3
cout << p[1] << '\n'; // 5

又:

1
2
3
4
5
6
7
8
char a[20];
int* p = (int*)a;
for (int i = 0; i < 5; ++i) {
p[i] = i + 3427;
}
for (int i = 0; i < 5; ++i) {
cout << *(p + i) << '\n';
} // 3427 3428...

表明,内存指定了操作方法,但可通过修改指针类型改变操作内存的方法。

一维数组用于函数的参数

1
2
void fun(int* p, int len);
void fun(int p[], int len);

传参时,必须指定数组的长度。因为在函数内,使用 sizeof a 获取的是指针占用内存空间的字节数(8 字节),而不是数组的。

十二、动态创建一维数组

在堆上分配内存。

申请内存:int* a = new 数据类型[数组长度]

释放内存:delete[] 指针

1
2
3
4
5
6
7
8
int* a = new int[8];
for (int i = 0; i < 8; ++i) {
a[i] = i;
}
for (int i = 0; i < 8; ++i) {
cout << a[i] << '\n';
}
delete[] a;

注意:

  • 动态创建的数组没有数组名,使用 sizeof 运算符得到的是指针占用内存空间的字节数。

  • 为避免申请失败造成程序崩溃,使用

    1
    2
    3
    4
    5
    6
    7
    int* a = new(std::nothrow) int[8];
    if (a == nullptr) {
    cout << "Ugly";
    return;
    }
    a[5] = 8;
    delete[] a;

十三、二维数组用于函数的参数

1
2
3
4
int* p; // 整型指针
int* p[3]; // 一维整型指针数组,元素是三个整型指针
int* p(); // 函数 p 的返回值类型是整型的地址
int (*p)(int, int); // p 是函数指针,返回值是整型

行指针(数组指针)

数据类型 (*行指针名)[行的大小]; 用于指向数组长度为 行的大小数据类型 型数组。

一维数组名被解释为数组第 0 个元素的地址,对一维数组名取地址得到的是数组的地址,即行地址。

1
2
3
4
5
6
7
8
int a[10];
cout << a << '\n';
cout << &a << '\n';
cout << a + 1 << '\n'; // 地址增量为4
cout << &a + 1 << '\n'; // 地址增量为40

int* p1 = a;
int (*p2)[] = &a;

二维数组名是行地址

1
2
int a[2][3] = {{ 2,3,5 }, { 3,5,8 }};
int (*p)[3] = a;

二维数组 a 有 2 个元素,每个元素是一个数组长度为 3 的整型数组。

a 被解释为数组长度为 3 的整型数组类型的行地址。

(*p)[3] 是数组长度为 3 的整型数组类型的行指针。

二维数组用于函数的参数

1
2
void fun(int (*p)[3], int len);
void fun(int p[][3], int len);

十四、多维数组

多维数组

1
2
int a[2][3][4];
int (*p)[3][4] = a;

三维数组 a 有 2 个元素,每个元素是一个 3 行 4 列的整型二维数组。

a 被解释为 3 行 4 列的整型二维数组类型的行地址。

(*p)[3][4] 是 3 行 4 列的整型二维数组类型的行指针。

多维数组用于函数的参数

1
2
void fun(int (*p)[3][4], int len);
void fun(int p[][3][4], int len);

十五、函数指针与函数回调

把函数的地址作为参数传递给函数,可以在函数中灵活调用其它函数。

1
2
3
4
5
6
7
8
9
10
11
void fun(int n) {

}
int main() {
int a = 8;
fun(a);
void (*pfun)(int);
pfun = fun;
(*pfun)(a); // C 语言写法
pfun(a); // C++ 写法
}

应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void fun1(int a) {
cout << a;
}
void fun2(int a) {
cout << a * a;
}
void fun(void (*pfun)(int), int b) {
int c = 6;
pfun(c);
}
int mian() {
fun(fun1, 3);
fun(fun2, 4);
}