引言
小伙伴们,加油呀,上一章我们已经提到了有关指针的学习,本来计划接着学习的,但是想了一下,稍后可能大家会时不时的回来翻看学习,索性我直接把指针有关的所有基础问题一次性全部总结到一章,方便大家到时候翻看复习,对此本章新用了锚点跳转功能,方便大家能快速翻阅自己要看的内容还可以快速回到目录。而且小杨在每一小节都添加了代码部分,为的是帮大家更好的理解和掌握一些片面的定义。小伙伴们也可以跟着小杨一起来敲一敲,试试看有没有理解清楚。
仍然废话不多说,我们开始吧,冲冲冲!!!
指针和引用
目录
1.指针的定义和使用
2.输出较大的数
3.算数运算符
4.关系运算
5.多级指针
6.空指针
7.指针数组
8.指针访问数组
9.指针访问二维数组
10.常量指针和指针常量
11.动态分配内存
12.malloc函数管理内存
13.引用
1.指针的定义和使用
在介绍指针之前,我需要先引入一个概念,那就是内存地址的概念(计算机内存是由一系列连续的存储单元组成的,每个存储单元都有一个唯一的地址)变量就是存储在内存中的某个地址,我们可以通过地址来访问变量的值。
- 指针的定义
那指针是什么呢,指针是一个特殊的变量,它的作用是用来存储另一个变量的地址。指针变量的定义通常包含一个星号*,表示这是一个指针变量。 - 指针的声明和初始化:
声明指针时,需要指定它指向的数据类型,初始化指针时,可以将其设置为NULL,或者指向一个同类型变量的地址。 - 指针的使用
使用&操作符可以获取一个变量的地址。
使用*操作符可以获取指针指向地址的值,这称为解引用操作。 - 代码展示:
#include <iostream>
using namespace std;
int main()
{
// 定义整型指针p1
int* p1;
int i = 10;
// &取地址运算符。获取整型变量i的地址并赋值给p1。
p1 = &i; // 也称为p1指向i
cout << "p1: " << p1 << endl;
// *解引用运算符。通过指针访问变量。
*p1 = 100;
cout << "i: " << i << endl;
return 0;
}
2.输出较大的数
我们既然已了解了指针的简单的用法,我们可不可以试试看,利用指针来判断一下变量的大小,在借助指针输出的一个简单的小问题了,我们一起来试试看吧
- 代码展示:
#include <iostream>
using namespace std;
int main()
{
int a = 20, b = 10;
int* p, * p1, * p2;
// p1保存a的地址,p2保存b的地址
p1 = &a;
p2 = &b;
cout << "p1: " << p1 << " p2: " << p2 << endl;
// 如果a大于b,交换p1和p2的地址
if (*p1 > *p2)
{
p = p1;
p1 = p2;
p2 = p;
}
cout << "a: " << a << " b: " << b << endl;
cout << "max: " << *p2 << endl;
return 0;
}
3.算数运算符
指针的算术运算符在C和C++语言中是指针操作的重要部分。以下是关于指针算术运算符的基本知识:
- 自增运算符(++):
ptr++ 或 ++ptr:将指针移动到下一个数据位置。
对于指向数组的指针,这通常意味着移动到数组的下一个元素。 - 自减运算符(–):
ptr-- 或 --ptr:将指针移动到前一个数据位置。
对于指向数组的指针,这通常意味着移动到数组的上一个元素。 - 加法运算符(+):
ptr + index:将指针向后移动index个数据位置。
这对于数组访问非常有用,可以用来访问数组的特定元素。 - 减法运算符(-):
ptr - index:将指针向前移动index个数据位置。
这也可以用来访问数组的元素,或者计算两个指针之间的距离。 - 赋值运算符(=):
ptr = &value:将指针赋值为变量value的地址。
ptr = ptr1 + index:将指针赋值为另一个指针加上一个整数的值。 - 代码展示:
#include <iostream>
using namespace std;
int main()
{
// 定义整型数组
int a[5] = { 1, 2, 3, 4, 5 };
// 定义整型指针
int* p = &a[0];
for (int i = 0; i < 5; i++)
{
cout << &a[i] << '\t';
}
cout << endl << "(p + 1): " << (p + 1) << endl;
double a1[5] = { 1.1, 1.1, 1.1, 1.1, 1.1};
double* p1 = &a1[0];
for (int i = 0; i < 5; i++)
{
cout << &a1[i] << '\t';
}
cout << endl << "(p1 + 1): " << (p1 + 1) << endl;
}
4.关系运算
我们在进行关系运算的时候,会用到的是比较运算符。
- 比较运算符(==, !=, <, >, <=, >=):
指针之间可以进行大小和等价比较,通常用于数组的遍历和条件判断。 - 代码展示:
#include <iostream>
using namespace std;
int main()
{
int a[5] = {};
for (int i = 0; i < 5; i++)
{
cout << &a[i] << '\t';
}
cout << endl;
int* p1 = &a[0], * p2 = &a[3];
// 比较两个指针的大小,指针地址对应的16进制数字大的指针变量大。
cout << (p1 > p2) << endl; // false
cout << (p2 > p1) << endl; // true
int m = 1, n = 1;
int* p3 = &m, *p4 = &n, *p5 = &n;
cout << "&m: " << &m << " &n: " << &n << endl;
cout << "p3: " << p3 << " p4: " << p4 << " p5: " << p5 << endl;
cout << "&p3: " << &p3 << " &p4: " << &p4 << " &p5: " << &p5 << endl;
cout << (m == n) << endl; // 比较m和n的值,结果为true。
cout << (p3 == p4) << endl; // 比较p3和(m的地址)p4的值(n的地址),结果为false。
cout << (*p3 == *p4) << endl; // 比较p3指向的变量的值和p4指向的变量的值, 结果为true。
cout << (p4 == p5) << endl; // 比较p4和(n的地址)p5的值(n的地址),结果为true。
// 指针不相等时,两个指针指向不同的变量。
*p3 = 100;
cout << "*p3:" << *p3 << " *p4:" << *p4 << endl;
// 指针相等时,两个指针指向同一个变量。
*p4 = 200;
cout << "*p4:" << *p4 << " *p5:" << *p5 << endl;
return 0;
}
5.多级指针
多级指针是指针的指针,即一个指针变量用来存储另一个指针变量的地址。在C和C++中,多级指针的概念是相对直接的。
首先,回顾一下一级指针的概念。一级指针是一个变量,它存储了另一个变量的内存地址。二级指针是一个指针,它存储了一级指针的地址。对于三级指针、四级指针等等,可以依此类推。每增加一级指针,就需要多一个星号*。多级指针的访问需要使用多个解引用操作符*。
例如,要访问二级指针pptr指向的一级指针ptr指向的变量var,需要两次解引用。多级指针在复杂的数据结构(如树、图、链表等)和函数中传递指针参数时非常有用。它们允许函数修改指针本身,而不仅仅是指针指向的值。
-
注意事项:
- 多级指针会增加代码的复杂性,因此在不必要的情况下应避免使用。
- 使用多级指针时,必须确保每一级都是正确初始化和管理的,以防止内存错误。
- 多级指针是C和C++中强大但复杂的特性,理解它们对于深入掌握这些语言至关重要。在实际编程中,多级指针的使用应该谨慎,并确保代码的清晰性和可维护性。
-
代码展示:
#include <iostream>
using namespace std;
int main()
{
int a = 100, * p1, *p3;
// 定义二级指针
int** p2;
p1 = &a;
p2 = &p1;
cout << "&a: " << &a << " p1: " << p1 << " &p1:" << &p1 << " p2:" << p2 << endl;
cout << "*p2: " << *p2 << " **p2:" << **p2 << endl;
// 定义三级指针
int*** p4 = &p2;
cout << "*p4: " << *p4 << " **p4:" << **p4 << " ***p4:" << ***p4 << endl;
double p5;
cout << "sizeof(p1): " << sizeof(p1) << " sizeof(p2): " << sizeof(p2) << " sizeof(p3): " << sizeof(p3)
<< " sizeof(p4): " << sizeof(p4) << " sizeof(p5): " << sizeof(p5) << endl;
return 0;
}
6.空指针
空指针是一个特殊的指针值,它不指向任何有效的内存地址。在C和C++中,空指针的概念非常重要,因为它提供了一种表示指针“无值”或“未初始化”的方法。
- 代码展示:
#include <iostream>
using namespace std;
int main()
{
// NULL 代表空其值为0,值为NULL的指针称为空指针。指针没有指向时可以设置其值为空。
int* p1 = NULL;
if (p1 == NULL)
{
cout << "p1是空指针" << endl;
}
else
{
*p1 = 100;
}
cout << "p1: " << p1 << endl;
// x64选项下,指针变量占用内存是8字节;x86选项下,指针变量占用的内存空间是4字节。
cout << "sizeof(p1): " << sizeof(p1) << endl;
return 0;
}
7.指针数组
指针数组是一个数组,其元素是指针。换句话说,指针数组的每个元素都存储了一个内存地址,这个地址指向某个数据。在C和C++中,指针数组的使用非常普遍,尤其是在处理字符串数组、动态数据结构等方面。
- 指针数组的定义方式是在数组名后面加上指针的星号*,然后指定数组的大小。
- 指针数组可以通过初始化列表来初始化。每个元素都应该是一个指向相应数据类型的指针。
- 指针数组可以通过初始化列表来初始化。每个元素都应该是一个指向相应数据类型的指针。
- 代码展示:
#include <iostream>
using namespace std;
int main()
{
int a = 10, b = 20, c = 30;
// 定义并部分初始化指针数组
int* p1[3] = { &a, &b};
cout << p1[2] << endl; // 部分初始化时,没有对应初值的指针会被置空。
p1[2] = &c; // 指针数组中的元素赋值
int sum = 0;
for (int i = 0; i < 3; i++)
{
sum += *p1[i];
}
cout << "sum: " << sum << endl;
// 数组名是数组第一个元素的地址
cout << "p1: " << p1 << " p1[0]: " << p1[0] << " &a: " << &a << " &p1[0]: " << &p1[0] << endl;
// 通过指针数组名访问指针数组第二个元素
cout << "p1 + 1" << p1 + 1 << " *(p1 + 1)" << *(p1 + 1) << endl;
// 通过指针数组名访问第二个元素指向变量的值
cout << " **(p1 + 1)" << **(p1 + 1) << endl;
return 0;
}
8.指针访问数组
- 可以使用数组索引来访问指针数组的元素,并通过解引用来获取或修改所指向的值。
- 代码展示:
#include <iostream>
using namespace std;
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int* p = a;
// 获取数组a第二个元素的地址
cout << "(p + 1): " << (p + 1) << endl;
// 获取数组a第二个元素的指向的变量的值
cout << "*(p + 1): " << *(p + 1) << endl;
// 指针自增后,指向数组a第二个元素
p++;
// 获取数组a第三个元素的地址
cout << "(p + 1): " << (p + 1) << endl;
// 获取数组a第三个元素的指向的变量的值
cout << "*(p + 1): " << *(p + 1) << endl;
// 获取数组a第一个元素的地址
cout << "(p - 1): " << (p - 1) << endl;
// 获取数组a第一个元素的指向的变量的值
cout << "*(p - 1): " << *(p - 1) << endl;
// 指针指向数组第一个元素
p = a; //相当于p = &a[0]
// 指针运算的方式访问数组元素
for (int i = 0; i < 5; i++)
{
cout << *(a + i) << ", ";
}
cout << endl;
// 数组名[下标]的方式访问数组元素
for (int i = 0; i < 5; i++)
{
cout << a[i] << ", ";
}
cout << endl;
return 0;
}
9.指针访问二维数组
- 指针数组常用于模拟二维数组。例如,可以创建一个指针数组,其中每个元素指向一个一维数组。
- 代码展示:
#include <iostream>
using namespace std;
int main()
{
int a[3][5] = {
{1, 2, 3, 4, 5},
{11, 12, 13, 14, 15},
{21, 22, 23, 24, 25}
};
// 二维数组的数组名是数组第一个元素的地址
cout << "a: " << a << " &a[0][0]" << &a[0][0] << endl;
// 二维数组的名[行位置] 获取该行第一个元素的地址。
cout << "a[0]" << a[0] << " &a[0][0]" << &a[0][0] << endl;
cout << "a[1]" << a[1] << " &a[1][0]" << &a[1][0] << endl;
cout << "a[2]" << a[2] << " &a[2][0]" << &a[2][0] << endl;
//使用指针数组访问二维数组
int* p[3] = { a[0], a[1], a[2] };
// 使用[][]获取指定的数组元素
cout << "p[1][1]: " << p[1][1] << endl;
// 使用[]和指针运算符获取指定的数组元素
cout << "*(p[1] + 1): " <<* (p[1] + 1) << endl;
// 使用指针运算符获取指定的数组元素
cout << "*(*(p + 1) + 1): " << *(*(p + 1) + 1) << endl;
int* p1[3] = { a[0], &a[1][1], a[2]};
// 使用[][]获取指定的数组元素
cout << "p1[1][1]: " << p1[1][1] << endl;
// 使用[]和指针运算符获取指定的数组元素
cout << "*(p1[1] + 1): " << *(p1[1] + 1) << endl;
// 使用指针运算符获取指定的数组元素
cout << "*(*(p1 + 1) + 1): " << *(*(p1 + 1) + 1) << endl;
return 0;
}
10.常量指针和指针常量
- 常量指针
常量指针是指向常量的指针,意味着通过这个指针不能改变它所指向的值,但是指针本身可以改变指向其他常量。在C和C++中,你可以通过以下方式定义一个常量指针:
const int value = 10;
const int *ptr = &value; // 常量指针,不能通过ptr改变value的值
在这个例子中,ptr是一个指向value的常量指针。你不能通过ptr来修改value的值,但是你可以改变ptr指向其他const int类型的变量。
- 指针常量
指针常量是一个其值(即指向的地址)不能被改变的指针,但是你可以通过指针修改它所指向的值。在C和C++中,你可以通过以下方式定义一个指针常量:
int value = 10;
int *const ptr = &value; // 指针常量,不能改变ptr的值(即不能让ptr指向其他变量)
在这个例子中,ptr是一个指向value的指针常量。一旦ptr被初始化为&value,你就不能将ptr改为指向其他变量。不过,你可以通过ptr修改value的值。
-
两者的区别
- 常量指针:指针指向的内容是常量,不能通过指针改变这个内容,但指针本身可以改变指向其他常量。
- 指针常量:指针本身是一个常量,一旦初始化后,就不能改变指向其他变量,但可以改变指向变量的值。
-
代码展示:
#include <iostream>
using namespace std;
int main()
{
// 常量指针
// 定义常量指针
const int* p1;
// 定义指针
int* p2;
// 定义整型变量
int a = 10;
// 定义整型常量
const int b = 20;
p1 = &a; // 常量指针也可以指向变量
p1 = &b;
// p2 = &b; 常量指针才能指向常量,变量指针不能指向常量。
// *p1 = 100; 不能通过常量指针修改指向的变量的值
a = 100;
// 指针常量
int m = 3, n = 5;
// 定义指针常量
int* const p3 = &m;
// p3 = &n; 指针常量的指向不能发生改变
// 指针常量的指向内存地址的内容可以修改
*p3 = 100;
cout << "m: " << m << " *p3: " << *p3 << endl;
char c[] = "abcd";
char* p4 = c;
// "abcd" 是字符串常量,常量指针才能指向常量
const char* p5 = "abcd";
cout << "p5: " << p5 << endl;
cout << "p5: " << *p5 << endl;
cout << "p5[0]: " << p5[0] << endl;
return 0;
}
11.动态分配内存
动态分配内存是指在程序运行过程中,根据需要向操作系统请求分配内存空间的过程。在C语言中,动态内存分配主要通过标准库函数实现,如malloc、calloc、realloc和free。在C++中,除了可以使用这些C语言的函数外,还可以使用new和delete关键字来进行动态内存的管理。
- C++中的动态内存分配
new:分配单个对象的内存,调用构造函数初始化对象。
new[]:分配数组对象的内存,调用构造函数初始化每个元素。
delete:释放单个对象的内存,调用析构函数。
delete[]:释放数组对象的内存,调用每个元素的析构函数。 - 动态内存分配的用途
数据结构:动态创建数据结构,如链表、树、图等。
未知大小的数组:在运行时确定数组的大小。
避免内存浪费:根据需要分配和释放内存,提高内存使用效率 - 动态内存分配的注意事项
内存泄漏:分配内存后未释放会导致内存泄漏,即程序不再使用的内存没有被归还给操作系统。
野指针:释放内存后,指针变为野指针,应将其设置为NULL或重新分配。
内存越界:访问分配的内存之外的地址会导致未定义行为。 - 代码展示:
#include <iostream>
using namespace std;
int main()
{
// 动态分配单个整型存储空间
int* p = new int;
cout << "p: " << p << endl;
*p = 10;
int a = *p;
cout << "a: " << a << " *p: " << *p << endl;
// 释放动态分配的存储空间。
delete p;
p = NULL; // 释放内存空间后,指针置空,避免使用已经释放的内存空间。
cout << "p: " << p << endl;
// 动态分配单个整型存储空间,并初始化为100
p = new int(100);
cout << "*p: " << *p << endl;
delete p;
p = NULL;
// 动态分配连续的整型存储空间
int n = 5;
p = new int[n]; // 申请多少个元素可以使用变量
p[0] = 1;
p[1] = 2;
p[2] = 3;
p[3] = 4;
p[4] = 5;
for (int i = 0; i < 5; i++)
{
cout << p[i] << ", ";
}
cout << endl;
// 释放连续的内存空间。注意[]
delete []p;
p = NULL;
return 0;
}
12.malloc管理内存
当你调用 malloc 时,你需要指定你想要分配的内存大小(以字节为单位)。malloc 会找到足够大的空闲内存块并返回一个指向它的指针。如果分配失败,malloc 返回 NULL。
- 代码展示:
#include <iostream>
using namespace std;
int main()
{
/*
malloc申请动态内存时, 大小由参数决定。
分配成功后,返回void类型指针,一般需要转换为对应数据类型的指针。
常见格式: (指针类型) malloc (sizeof(数据类型));
malloc申请成功时返回申请到的地址,malloc分配失败时,返回NULL;new 分配失败时,抛出异常。
*/
int* p1 = (int*)malloc(sizeof(int));
cout << "p1: " << p1 << endl;
*p1 = 10;
cout << *p1 << endl;
// 释放内存使用free。
free(p1);
cout << "p1: " << p1 << endl;
// 释放后置空指针变量,防止误操作。
p1 = NULL;
// 申请连续存储空间
p1 = (int*)malloc(sizeof(int)*4);
*p1 = 100;
*(p1 + 1) = 200;
p1[2] = 300;
p1[3] = 400;
for (int i = 0; i < 4; i++)
{
cout << p1[i] << ", ";
}
cout << endl;
// 释放连续存储空间
free(p1);
p1 = NULL;
return 0;
}
13.引用
在C++中,引用是一种特殊的别名,它为另一个变量提供了一个替代名称。引用的主要特点是它与其引用的变量共享相同的内存地址,因此对引用的修改实际上就是对引用的变量的修改。C++中的引用与C语言中的指针相似,但引用的语法更为简单,且不需要使用特殊的符号来表示间接访问
- 引用与指针的区别
- 引用必须在声明时初始化,而指针可以在任何时候初始化或重新赋值。
- 引用不可以为空,它必须始终代表一个有效的对象,而指针可以指向空(nullptr)或未初始化。
- 引用不可以改变其引用的对象,而指针可以改变其指向的地址。
引用是C++中一个非常强大的特性,它提供了一种简单而直观的方式来处理变量的别名,同时避免了指针的复杂性。正确使用引用可以使得代码更加清晰和易于维护。
- 代码展示:
#include <iostream>
using namespace std;
int main()
{
int a = 10;
// 定义引用b,初始化为a的别名。引用定义时必须初始化。
int& b = a;
cout << "b: " << b << endl;
// b是a的别名,修改b也会修改a。
b = 100;
cout << "a: " << a << endl;
int c = 20;
// 赋值操作。引用定义以后不能修改为其他变量的引用。
b = c;
cout << "b: " << b << endl;
cout << "a: " << a << endl;
// 指针常量形式
int* const b1 = &a;
*b1 = 100;
cout << "b: " << b << endl;
cout << "a: " << a << endl;
*b1 = c;
cout << "b: " << b << endl;
cout << "a: " << a << endl;
return 0;
}
结语
小伙伴们,指针的基础部分到这里就结束了感觉怎么样呢,是不是小有难度呢,确实小杨在学指针的时候也是,对这个指针甚是头疼,但是也得咬着牙把这个拿下来,不然后边的内容确实没办法学呢,指针就是分水岭,从这部分大家学不会的就不能接着学了,还有畏难的小伙伴们到这里就停下来了,可是指针又是C++能在计算机领域内独领风骚的核心,只有指针是C++独有的一项技能,我们可以可以用指针做到其他高级语言做不到的功能。所以想要学习C+是绕不过的,小伙伴们一定要加油呀,本章已经把有关指针的一部分讲完了,还有指针和数组和函数相结合的部分,这都是需要我们掌握的,一定要加油坚持下来,我们还要在更好的未来相见呢。
好了,为今天努力学习的自己鼓鼓掌吧,加油加油,小伙伴们,你们是最棒的!!!冲冲冲!!!