指针
[TOC]
1. 动态指针
1.1 new 和 delete
在C++中创建动态指针1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义指向动态分配的字符变量的指针
int* val0 = new int(6564);
delete val0;
val0 = nullptr;
// 定义指向动态分配的一维字符数组的指针
// val1指向数组首个元素的地址,即(val1+1)为数组第二个元素的地址
int size1 = 3;
double* val1 = new double[size1]; // new出来的这些都在堆上
for (int i = 0; i < size1; i++)
{// 赋值
val1[i] = 6564;
}
delete[] val1;
val1 = nullptr;
1.2 裸指针的安全使用
为了避免裸指针造成内存泄漏或成为野指针2,推荐以下方法使用:
(1) while(true)包住业务代码
裸指针new出来以后,立刻进入while(true)来处理具体业务代码,可保证一定会执行delete,代码示例如下:
1
2
3
4
5
6
7
8
int nSize = 9; // 定义数组大小
float* fArr = new float[nSize] {0.f}; // new
while(true)
{
/*这里是具体业务代码,有异常就break*/
break; // 最后记得break
}
delete[] fArr; // delete
(2) 交给智能指针
直接把指针交给智能指针,裸指针的生命周期即可自动处理好,代码示例如下:
1
2
3
4
5
6
7
8
int nSize = 9; // 定义数组大小
// 构造智能指针,方法1:
std::unique_ptr<float[]> fArr = std::make_unique<float[]>(nSize);
std::fill_n(fArr.get(), nSize, 0.f); // 初始化元素
// 构造智能指针,方法2:
std::unique_ptr<float[]> fArr(new float[nSize] {0.f});
/*下面该干啥干啥就行*/
但这样也会有麻烦的地方:
-
需要传递指针的时候,仍需要通过
get()来取指针。 -
上述代码中也可以看到,通过
std::make_unique创建的智能指针是不支持直接初始化数组元素的,因此需要使用std::fill_n函数将所有元素初始化为0.f。std::fill_n是一个标准库函数,用于将指定数量的元素设置为给定值。它的函数原型如下:1 2
template< class OutputIt, class Size, class T > void fill_n( OutputIt first, Size count, const T& value );
first:指向要设置值的起始位置的迭代器。count:要设置值的元素数量。value:要设置的值。
2. 二维数组
二维数组可以结合动态指针来创建,以下为几种创建二维数组的方法:[][]345
2.1 数组指针
数组指针,也就是数组的指针,也就是一个指针,指向了一个数组。
1
2
3
4
5
6
// 定义指向动态分配的二维字符数组(3列)的指针
// p指向数组首行的地址,即(p+1)为数组第二行的地址
int size3 = 5;
// new出来的这些内容都在堆上:
char(*pchar1)[3] = new char[size3][3]; // 数组指针
delete[] pchar1;
2.2 指针数组
指针数组,也就是指针的数组,也就是一个数组,里面都是指针。
1
2
3
4
5
6
7
8
9
int s3 = 5;
// 此时tf位于栈上:
float* tf[3]{ nullptr }; // 指针数组:该数组里储存的是指针
// 在堆上申请空间,并把该把指针放入数组tf:
for (int i = 0; i < 3; i++)
tf[i] = new float[s3];
// 卸载资源:
for (int i = 0; i < 3; i++)
delete[] tf[i];
2.3 指针的指针
指针的指针,一个指针可以指向数组,指针的指针自然就可以指向一个二位数组了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1.定义指向动态指针数组的指针:
int size = 3; // 行数
int size2 = 4; // 列数
// 2.动态生成一个包含size个int型指针的数组
// 那么val2也就是指向指针数组首元素,而指针数组首元素是一个int型指针,
// 所以val2就是指向int型指针的指针,即指向指针的指针。
int** val2 = new int* [size] {nullptr}; // 指针val2是位于堆上的
// 3.接下来为val[i]申请空间:
// 有两种方法,一种是在栈上,一种是在堆上:
// (1)在栈上申请空间,但是这样没什么意义,因为很容易就丢了。
int arr1[5]{ 1,2,5,8,4 };
val2[0] = arr1;
// (2)在堆上申请空间:
for (int i = 0; i < size; i++)
val2[i] = new int[size2] {0};
//4. 给二维数组赋值:
for (int i = 0; i < size; i++)
for (int j = 0; j < size2; j++)
val2[i][j] = 6564;
// 5.卸载:
for (int i = 0; i < size; i++)
{
delete[] val2[i];
val2[i] = nullptr;
}
delete[] val2;
val2 = nullptr;
3. 指针常量和常量指针
3.1 指针常量
指针常量是指其所指向的内存地址不能被修改的指针。这意味着一旦指针常量被初始化,它将永远指向同一个地址,无法通过该指针修改所指地址的值。6
1
2
const int x = 10;
int *const ptr = &x;
在这个例子中,ptr是一个指向整数常量 x 的指针常量。
尝试修改 ptr 指向的地址将导致编译错误,因为 ptr 本身是不可变的:
1
2
// 编译错误!
*ptr = &y;
常量指针在函数参数传递和数组声明等场景中发挥着重要作用。
在函数参数中使用指针常量可以确保函数内部不会无意中修改传递进来的数据。
3.2 常量指针:指针指向的内容不可修改
相对于指针常量,常量指针则强调指针指向的内存地址所存储的值是不可变的。这意味着,通过常量指针无法修改所指向地址的值,但可以改变指针指向的地址。
1
2
int y = 5;
const int *ptr_const = &y;
在这个例子中,ptr_const 是一个常量指针,它指向整数变量 y。通过这个指针,我们可以读取 y 的值,但无法通过 ptr_const 修改 y 的值。
1
2
3
4
// 合法
int value = *ptr_const;
// 编译错误!
*ptr_const = 8;
常量指针常常用于保护数据的完整性,确保指针指向的数据不会被意外地修改。
3.3 指针常量 vs 常量指针
理解指针常量和常量指针的区别至关重要,下面我们将通过一些实际的应用场景深入比较这两者。
(1)保护常量数据
假设我们有一个常量数组,我们希望使用指针来访问数组元素,但不希望通过指针修改数组的内容。
1
const int numbers[] = {1, 2, 3, 4, 5};
如果我们使用指针常量来实现,代码可能如下:
1
int *const arr_ptr = numbers; // 编译错误!数组是常量,不可用指针常量指向
而如果使用常量指针,我们可以这样:
1
const int *arr_ptr_const = numbers; // 合法
通过常量指针,我们确保了指针无法修改数组元素的值,同时又可以方便地访问数组。
(2)传递参数
在函数参数传递中,指针常量和常量指针的选择也会影响函数的行为。
考虑以下的函数声明:
1
2
void processArray(const int *arr);
void modifyArray(int *const arr);
通过 processArray 函数,我们传递一个常量指针,确保在函数内部无法修改数组元素的值。而通过 modifyArray 函数,我们传递一个指针常量,确保函数内部无法修改指针指向的数组地址。
3.4 指针常量和常量指针的实际应用
为了更好地理解这两个概念,让我们通过一个实际的应用场景进行解析。
假设我们有一个图书馆管理系统,其中有一本书的信息需要被保护,同时我们也希望能够在需要的时候读取这本书的信息。我们可以通过指针常量和常量指针实现这一目标。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <string>
struct Book {
std::string title;
std::string author;
int year;
};
int main() {
const Book libraryBook = {"The C++ Programming Language", "Bjarne Stroustrup", 1985};
// 使用指针常量,保护书籍信息
const Book *const bookPtrConst = &libraryBook;
// 使用常量指针,可以读取书籍信息,但不能修改
const Book *bookPtr = &libraryBook;
// 读取书籍信息
std::cout << "Book Title: " << bookPtr->title << std::endl;
std::cout << "Author: " << bookPtr->author << std::endl;
std::cout << "Year: " << bookPtr->year << std::endl;
// 编译错误!无法通过常量指针修改书籍信息
// bookPtr->year = 2022;
return 0;
}
在这个例子中,bookPtrConst 是一个指针常量,确保了无法通过该指针修改书籍信息。而 bookPtr 是一个常量指针,允许我们读取书籍信息,但不允许修改。这样,我们在保护书籍信息的同时,还能够方便地访问它。
4. 指针与数组名
见文章7
5. 指针小知识
1)指针+1
在 C/C++ 中,指针加上一个整数 n 表示指针向前移动 n 个该指针类型大小的位置。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
typedef struct example {
// ... 结构体定义 ...
}Example, *pExample;
int main(){
// 分配包含n个Example结构体的数组,用于本文的举例:
pExample pReq = new Example[5];
pExample pPushData = pReq + 1; // 指针向前移动一个 Example 结构体大小的位置,指向第二个Example结构体
// ... 使用 ...
delete[] pReq; // 释放数组
}
- 如上所示,
pReq + 1是一种指针运算,用于获取紧随 Example 结构体之后的数据地址。 - 这种内存布局在协议设计中很常见,通过一个基础结构体加上动态数量的附加数据来传递信息
pReq + 1正好指向了第一个 Example 结构体的起始地址
如上代码等价于:
1
2
// 先计算基础结构体的大小,再通过字节指针移动获取地址
pExample pPushData = (pExample)((char*)pReq + sizeof(Example));
- 两种写法效果相同,但
pReq + 1更简洁,也更能体现出 “紧随其后” 的语义。
参考文章89101112
-
动态指针是:通过使用 new 运算符在堆(heap)中动态分配内存得到的指针。与静态分配的指针(例如在栈上声明的指针变量)不同,动态指针的生命周期由程序员控制,并且需要手动释放分配的内存,以避免内存泄漏。 ↩︎
-
野指针:一个指向无效内存位置的指针。产生原因:(1)声明指针时未初始化。(2)delete 后没有将该指针赋为 nullptr,且后面又继续使用该指针。(这种情况下的指针也被特称为 悬空指针 ) ↩︎
-
CSDN. C++ 指向动态分配的指针数组的指针[DB/OL]. (2019-03-16). https://blog.csdn.net/weixin_43971764/article/details/88605729 ↩︎
-
CSDN. C++ 指针数组与数组指针[DB/OL]. (2016-07-22). https://blog.csdn.net/xumingwei12345/article/details/51994957 ↩︎
-
CSDN. C++ new一个指针数组? [DB/OL]. (2023-01-01). https://blog.csdn.net/qq_73725757/article/details/128444405 ↩︎