08.C和C++指针和引用
C/C++ 指针和引用基础
原始指针 (裸指针) 基础
指针声明
- 指针概念:指针就是一个内存地址
- 声明指针 (
类型 *变量名
)
1
2
3
4
int d = 1;
int *i = &d; // 最标准
int* i1 = &d;
int * i2 = &d;
- 取地址 (
&
) - 取指针 (地址的值) 用
* 指针变量
- 通过
*
操作指针的值 - 指针支持自增 (
++
) 和自减 (--
),偏移指针,里面的值不可预知
1
2
int m = 10;
int *j = &m;
1
2
3
4
5
6
7
8
9
10
11
int i1 = 10;
int *p1 = &i1;
// %#x格式化为十六进制,&取地址,*取地址中的值
printf("i1的地址:%#x\n", &i1); // 取i1的地址
printf("p1存的值:%#x\n", p1); // p1存的是i1的地址
printf("p1的地址:%#x\n", &p1);
printf("p1存的地址的值:%d\n", *p1); // *p1,取p1地址的值
*p1 = 100;
printf("通过*指针操作操作内存,修改值:%d\n", i1);
指针理解
指针对于管理和操纵内存非常重要。
一个指针只是一个地址,指针是一个整数,它是保存内存地址的整数。
类型是完全没有意义的:指针和类型无关,它只是存储了内存地址的整数。
示例 1:无类型的指针
1
2
3
4
5
// 无类型的指针,只存储内存地址,无关心内存存储的是什么数据类型
void* ptr = 0; // 0是无效的内存地址,这是个无效的指针
// C++ 11
void* ptr = nullptr;
int 变量的指针
每创建一个变量都有一个内存地址,因为我们需要一个地方来存储这个变量,通过 &
取出变量的地址
1
2
3
4
5
int var = 5; // var值为5
// ptr保存了变量var的内存地址
void* ptr = &var; // ptr地址为:0x000000016fdfe49c
// 也可以写成下面,内存地址不变
int* ptr1 = &var; // ptr1地址为:0x000000016fdfe49c
指针所占内存空间
指针有多大,取决于其指向的数据的大小,可能是 32位
的整数,也可能是 64位
的整数,也可能是 16位
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
int a = 10;
int * p;
p = &a; //指针指向数据a的地址
cout << *p << endl; //* 解引用
cout << sizeof(p) << endl;
cout << sizeof(char *) << endl;
cout << sizeof(float *) << endl;
cout << sizeof(double *) << endl;
system("pause");
return 0;
}
读取指针
指针的 *
运算符通常被称为 dereference
运算符。
1
2
3
4
5
int var = 5; // var值为5
// ptr保存了变量var的内存地址
void* ptr = &var;
// 逆向引用ptr指针,访问指针指向的数据,报错了,取指针的值,需要是具体类型的指针
int var2 = *ptr; // expression must be a pointer to a complete object type
下面:
1
2
3
4
5
6
7
8
9
int main()
{
int var = 8;
int *ptr = &var;
*ptr = 10;
int var2 = *ptr;
std::cout << "var=" << var;
std::cin.get();
}
指针分类
- 原始指针
- 智能指针(Smart Pointer)[[C++ 智能指针]]
悬挂指针、空指针和野指针
悬挂指针 Dangling Pointers
悬挂指针是指之前指向有效对象但该对象已被删除或释放的指针。例如,如果你释放了一个指向动态分配内存的指针,但没有将指针设置为 nullptr,则它会变成悬挂指针。由于原来的内存可能被重新分配并用于其他用途,解引用悬挂指针是危险的,因为它可能导致意外改变其他数据或崩溃。
示例:
1
2
3
int* ptr = new int(42);
delete ptr; // ptr 现在是悬挂指针
// 应该做的是:ptr = nullptr;
空指针 Null Pointer
空指针是指明确指向内存地址 “0
” 的指针,也就是没有指向任何有效内存的指针。在 C++11 之前,通常使用 NULL 表示空指针。从 C++11 开始,建议使用 nullptr 关键字来表示空指针。空指针不指向任何的对象或函数,因此解引用一个空指针会导致未定义行为,通常是程序崩溃。
示例:
1
2
3
4
5
6
7
8
9
10
11
int main() {
//指针变量p指向内存地址编号为0的空间
int * p = NULL;
//访问空指针报错
//内存编号0 ~255为系统占用内存,不允许用户访问
cout << *p << endl;
system("pause");
return 0;
}
NULL
和 nullptr
在 C++ 中,NULL 和 nullptr 都用于表示指针不指向任何对象,但它们之间有一些重要的区别:
- NULL:
- 在 C++ (和 C) 中,
NULL
通常定义为0
或 ((void*) 0
),表示空指针常量。 - 它在 C 标准中就已存在,并被 C++ 继承。
NULL
被多种上下文解释,有时可能会引起歧义,如NULL
可以和整数0
相互转换,这可能在函数重载时导致不明确的调用。
- 在 C++ (和 C) 中,
- nullptr:
nullptr
是 C++11 引入的一个关键字,专门代表空指针。- 它是一个
std::nullptr_t
类型的字面量,这使得nullptr
不能隐式转换为整数类型,且类型安全性比NULL
更强。 - 在 C++11 及其之后版本中,推荐使用
nullptr
初始化空指针,因为它提供了更明确的意图,并且能够更好地配合 C++ 的类型系统。
因此,在新的 C++ 代码中,你应该使用 nullptr
而不是 NULL
来初始化空指针。
1
2
int *p = NULL; // 旧的 C++ 风格
int *p = nullptr; // 推荐的 C++11 风格
野指针 Wild Pointer
野指针是指没有被初始化或者随意赋值的指针。这种指针不是空指针,也不是悬挂指针,因为它可能指向任意的、随机的内存地址。这是非常危险的,因为你无法预测它指向何处和它的行为,解引用野指针通常会导致未定义行为。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 示例1:
int main() {
// 指针变量p指向内存地址编号为0x1100的空间
int * p = (int *)0x1100;
// 访问野指针报错
cout << *p << endl;
system("pause");
return 0;
}
// 示例2:
int* ptr; // 野指针,它没有初始化
*ptr = 42; // 危险操作,因为 ptr 可能指向任何地方
空指针和野指针都不是我们申请的空间,因此不要访问。
指针和数组
指针遍历数组,数组名代表了数组的首地址
1
2
3
4
5
6
int arr[] = {1, 2, 3, 4, 5};
int *arr1 = arr;
for(size_t i = 0; i < 5; i++)
{
printf("数组值:%d\n",*(arr1+i));
}
数组指针
数组的指针,指针指向的是一块连续的数组内存首地址
1
2
3
4
5
6
7
8
9
// 数组指针
// 二维数组
int arr_demo[2][3] = \{\{11,22,33\},\{44,55,66\}\};
int (*p_arr_demo)[3] = arr_demo; // 存放的是,{11,22,33}和{44,55,66}的地址
printf("数组指针%#x\n",p_arr_demo); // {11,22,33}的地址
printf("数组指针%#x\n",p_arr_demo+1); // {44,55,66}的地址
printf("取出存放{44,55,66}的地址:%#x\n",*(p_arr_demo+1));
// 取出55
printf("取出55:%d\n",*(*(p_arr_demo+1)+1));
指针数组
指针的数组,表示一个存放地址的数组,里面存放的全是地址
1
2
3
4
// 指针数组
int mm = 10;
int *p_mm[3] = {&mm,&mm,&mm};
printf("%d\n",**p_mm); // 10,取出指针数组第一个值,为一个地址,再去该地址中的值
const 和 指针
const 修饰指针有三种情况
- const 修饰指针 — 常量指针:指针指向可以改,指针指向的值不可以更改
- const 修饰常量 — 指针常量:指针指向不可以改,指针指向的值可以更改
- const 即修饰指针,又修饰常量:指针指向和指针指向的值都不可以更改
技巧: 看 const 右侧紧跟着的是指针还是常量, 是指针就是常量指针,是常量就是指针常量
示例 1:
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
int main() {
int a = 10;
int b = 10;
// const修饰的是指针,指针指向可以改,指针指向的值不可以更改
const int * p1 = &a;
p1 = &b; //正确
//*p1 = 100; 报错
// const修饰的是常量,指针指向不可以改,指针指向的值可以更改
int * const p2 = &a;
//p2 = &b; //错误
*p2 = 100; //正确
// const既修饰指针又修饰常量
const int * const p3 = &a;
//p3 = &b; //错误
//*p3 = 100; //错误
system("pause");
return 0;
}
示例 2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//从右往左看 const 修饰谁 谁就不可变
//const final
char tmp[] = "hello";
//不能修改char对应地址的值 指针p2的值是可以变的
const char *p2 = tmp;
p2 = "DONGNAO";
//和p2是一样的
char const *p3 = tmp;
char * const p4 = tmp;
p4[0] = 'a';
//不能修改指针的值 p4是不可变的
//p4 = "11"
// p5和p6,指针不可变,指针指向的地址内容也不可变
const char * const p5 = tmp;
char const * const p6 = tmp;
多级指针
1
2
3
4
5
6
7
8
//多级指针
int i2 = 10;
//p7存 i2的地址
int *p7 = &i2;
//p7这个变量自己的地址给 p8
int **p8 = &p7;
printf("p8的值:%d\n", **p8);
指针其他概念
- 野指针:没有初始化的指针
1
int *p; // 没有初始化的指针,就是野指针
- 悬空指针:指针指向的内存被释放了
函数和指针
函数指针 Pointer to Function
函数指针是指向函数的指针。它用于存储函数的地址,可以像调用普通函数那样使用函数指针调用函数。函数指针通常用于回调函数、函数表或命令模式设计等场景。函数指针的定义包含了函数的返回类型和参数列表。
函数指针的格式
1
2
3
4
5
6
type (*ptr)(type1, type2)
void(*p)(char*) // p是函数指针的名称
// void 返回值
// (*p) p变量用来表示这个函数
// (char*) 函数的参数列表
类似于 Kotlin 中的高阶函数,
1
add: (a:Int, b:Int) -> Int
下面是 C 语言函数指针的一个示例:
1
2
3
4
5
6
7
8
9
10
// 先定义一个普通的函数
int add(int a, int b) {
return a + b;
}
// 然后定义一个函数指针,指向具有相同签名的函数
int (*functionPtr)(int, int) = add;
// 使用函数指针调用函数
int result = functionPtr(2, 3); // 结果为 5
在上面的代码中,functionPtr
是一个指向接受两个 int 参数并返回 int 的函数的指针。
C++ 无参数的函数指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Print() {
std::cout << "hello,world" << std::endl;
}
void testCPointer()
{
//void(*function)() = Print(); // error,不要带()
void(*pr1)() = Print; // ok
printf("pr1存的值:%#x\n", pr1); // pr1存的值:0x331172
void(*pr2)() = &Print; // ok
printf("pr2存的值:%#x\n", pr2); // pr2存的值:0x331172
//auto function = Print(); // error,auto无法识别void类型
auto function = Print; // ok,去掉括号就不是在调用这个函数,而是在获取函数指针,得到了这个函数的地址。就像是带了&取地址符号一样"auto function = &Print;""(隐式转换)。
printf("function存的值:%#x\n", function); // function存的值:0x331172
function();//调用函数 //这里函数指针其实也用到了解引用(*),这里是发生了隐式的转化,使得代码看起来更加简洁明了!
}
 |
C++ 有参数函数指针
1
2
3
4
5
6
7
void Print(int a) {
std::cout << a << std::endl;
}
int main() {
auto temp = Print; //正常应该是 void(*temp)(int) = Print,太过于麻烦,用auto即可
temp(1); //在用函数指针的时候也传参数进去就可以正常使用了
}
Typedef 或 using 定义函数指针
Typedef 定义函数指针
1
2
3
4
5
6
7
8
9
void say(int (*m)(char*, char*), char *msg) {
m(msg, NULL);
}
也可以通过typedef定义别名
typedef int(*Func)(char*,char*);
void say(Func func, chat *msg){
m(msg,NULL);
}
案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef void(*Success)(char*);
typedef void(*Failure)(char*);
void http(int i, Success success, Failure f) {
if (i == 1) {
success("成功");
} else {
f("失败");
}
}
void httpOk(char* msg) {
printf("请求成功了:%s\n", msg);
}
void httpFailure(char* msg) {
printf("请求失败了:%s\n", msg);
}
void main() {
http(1, httpOk, httpFailure);
http(0, httpOk, httpFailure);
}
Using 定义函数指针
为什么要首先使用函数指针?
如果需要将一个函数作为另一个函数的形参,那么就要需要函数指针 .
这里用 using
来定义函数指针:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Print(int val) {
std::cout << val << std::endl;
}
//下面就将一个函数作为形参传入另一个函数里了
void ForEach(const std::vector<int>& values, void(*function)(int)) {
for (int temp : values) {
function(temp); //就可以在当前函数里用其他函数了
}
}
int main() {
std::vector<int> valus = { 1, 2, 3, 4, 5 };
ForEach(values, Print); //这里就是传入了一个函数指针进去!!!!
}
指针函数(Function Returning a Pointer)
指针函数并非一个专业术语,但如果你看到它,它通常指的是一个返回指针的函数。指针函数的返回类型是某种类型的指针。这里是一个指针函数的例子:
1
2
3
4
5
// 这是一个返回 int 指针的函数
int* getArray() {
static int arr[10]; // 静态局部数组,可以作为返回的指针使用
return arr;
}
在这个例子中,getArray
函数返回一个指向 int 类型的静态局部数组的指针。
总之,函数指针
描述的是一个变量,这个变量存储了一个函数的地址,而 指针函数
实际上是描述的一个返回指针的函数。两者语法上相似,但它们的用途和含义完全不同。弄清楚声明中哪部分是类型修饰符、哪部分是函数/变量的名字,有助于理解它们在 C/C++ 程序中的作用。
引用 Reference
引用介绍
引用和指针是两个东西 引用:变量名是附加在内存位置中的一个标签, 可以设置第二个标签 简单来说引用变量是一个别名,表示一个变量的另一个名字
根本上,引用通常只是指针的伪装,他们只是在指针上的语法糖,让它更容易阅读和理解,引用必须引用已经存在的变量,引用本身并不是新的变量,它们并不占用内存,它们没有真正的存储空间。
语法:
1
数据类型 &别名 = 原名
引用能做的,指针都能做;能用引用做的事,就用引用做
1
2
int a = 5;
int& ref = a;
引用注意事项
- 引用必须初始化
- 引用在初始化后,不可以改变
1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
int a = 10;
int b = 20;
//int &c; //错误,引用必须初始化
int &c = a; //一旦初始化后,就不可以更改
c = b; //这是赋值操作,不是更改引用
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
return 0;
}
引用作为函数参数
值传递:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 值传递
void Increment(int value)
{
value++;
}
int main()
{
int var = 8;
Increment(var); // 值传递,var不会变
int &ref = var;
std::cout << "ref=" << ref << std::endl;
ref = 10;
std::cout << "var=" << var << std::endl;
}
// 输出:
// ref=8
// var=10
引用传递:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 引用传递
void Increment(int& value)
{
value++;
}
int main()
{
int var = 8;
Increment(var);
int &ref = var;
std::cout << "ref=" << ref << std::endl;
ref = 10;
std::cout << "var=" << var << std::endl;
}
// 输出:
// ref=9
// var=10
指针传递:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Increment(int* value)
{
// value++; // 这样是错误的,因为value是指针,++操作符不能直接作用于指针
(*value)++; // 这样是正确的,先解引用,再自增
}
int main()
{
int var = 8;
Increment(&var);
int &ref = var;
std::cout << "ref=" << ref << std::endl;
ref = 10;
std::cout << "var=" << var << std::endl;
}
// 输出:
// ref=9
// var=10
int& ref
ref 是引用int* ptr = &var
取 var 变量的地址,赋值给 ptr,是一个指针*ptr
取 ptr 指针指向的内存地址中内容
引用作为函数返回值
作用:引用是可以作为函数的返回值存在的
注意:不要返回局部变量引用
用法:函数调用作为左值
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
// 返回局部变量引用
int& test01() {
int a = 10; //局部变量
return a;
}
// 返回静态变量引用
int& test02() {
static int a = 20;
return a;
}
int main() {
//不能返回局部变量的引用
int& ref = test01();
cout << "ref = " << ref << endl;
cout << "ref = " << ref << endl;
//如果函数做左值,那么必须返回引用
int& ref2 = test02();
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
test02() = 1000;
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
return 0;
}
引用的本质
本质:引用的本质在 C++ 内部实现是一个指针常量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 发现是引用,转换为 int* const ref = &a;
void func(int& ref){
ref = 100; // ref是引用,转换为*ref = 100
}
int main() {
int a = 10;
// 自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
int& ref = a;
ref = 20; // 内部发现ref是引用,自动帮我们转换为: *ref = 20;
cout << "a:" << a << endl;
cout << "ref:" << ref << endl;
func(a);
return 0;
}
C++ 推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了。
常量引用
作用: 常量引用主要用来修饰形参,防止误操作
在函数形参列表中,可以加==const 修饰形参==,防止形参改变实参。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 引用使用的场景,通常用来修饰形参
void showValue(const int& v) {
//v += 10;
cout << v << endl;
}
int main() {
// int& ref = 10; 引用本身需要一个合法的内存空间,因此这行错误
// 加入const就可以了,编译器优化代码,int temp = 10; const int& ref = temp;
const int& ref = 10;
// ref = 100; //加入const后不可以修改变量
cout << ref << endl;
// 函数中利用常量引用防止误操作修改实参
int a = 10;
showValue(a);
return 0;
}
指针使用注意
安全的使用指针
使用指针前,应该始终检查指针是否为 nullptr
,这是一个表示空指针的特殊值,代表指针不指向任何对象。
提高代码安全性的最佳实践包括:
- 始终初始化指针(最好初始化为
nullptr
)。 - 在解引用之前检查指针是否为
nullptr
。 - 避免使用裸指针,可以考虑使用 C++ 的智能指针(如
std::unique_ptr
、std::shared_ptr
)管理资源。
检查空指针
在解引用之前,总是检查指针是否为 nullptr
或 NULL
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1、if判断
void safeFunc(int* ptr) {
if (ptr != nullptr) {
// ptr 不是空指针,可以安全地解引用
int value = *ptr;
// 使用 value...
} else {
// ptr 是空指针,适当处理
}
}
// throw异常
void safeDereference(int* ptr) {
if (ptr == nullptr) {
// 处理错误情况,如返回值或抛出异常
throw std::invalid_argument("Received nullptr");
}
int value = *ptr;
// 使用 value...
}
使用断言
如果函数的合约规定指针参数不得为空,你可以使用 assert
断言来捕获违反合约的情况。这在调试过程中非常有用,但请记住,在非调试版本的程序中(即释放(release)版本),断言可能不会被执行。
1
2
3
4
5
6
#include <cassert>
void safeDereference(int* ptr) {
assert(ptr != nullptr && "ptr should not be null");
int value = *ptr;
// 使用 value...
}
使用智能指针
在可能的情况下,使用 C++ 标准库中的智能指针(std::unique_ptr
、std::shared_ptr
),这可以帮助管理对象的生命周期并防止悬挂指针(dangling pointers)的情况发生。
1
2
3
4
5
6
#include <memory>
void safeDereference(std::unique_ptr<int>& ptr) {
int value = *ptr; // 不需要检查 nullptr,unique_ptr 保证了它的有效性
// 使用 value...
}
如果必须使用原始指针,请慎用并确保你完全控制了指针的来源和生命周期。尽可能限制原始指针的作用域,并在不需要时尽快释放。