文章

08.C和C++指针和引用

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;

示例 2:

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;
}
NULLnullptr

在 C++ 中,NULLnullptr 都用于表示指针不指向任何对象,但它们之间有一些重要的区别:

  1. NULL
    • 在 C++ (和 C) 中,NULL 通常定义为 0 或 ((void*) 0),表示空指针常量。
    • 它在 C 标准中就已存在,并被 C++ 继承。
    • NULL 被多种上下文解释,有时可能会引起歧义,如 NULL 可以和整数 0 相互转换,这可能在函数重载时导致不明确的调用。
  2. 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 修饰指针有三种情况

  1. const 修饰指针 — 常量指针:指针指向可以改,指针指向的值不可以更改
  2. const 修饰常量 — 指针常量:指针指向不可以改,指针指向的值可以更改
  3. 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();//调用函数 //这里函数指针其实也用到了解引用(*),这里是发生了隐式的转化,使得代码看起来更加简洁明了!
}
![image.png200500](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidianobsidian202404162317818.png)
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_ptrstd::shared_ptr)管理资源。

检查空指针

在解引用之前,总是检查指针是否为 nullptrNULL

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_ptrstd::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...
}

如果必须使用原始指针,请慎用并确保你完全控制了指针的来源生命周期。尽可能限制原始指针的作用域,并在不需要时尽快释放。

本文由作者按照 CC BY 4.0 进行授权