前言:

因为 C++ 所以指针就有了这篇文章。

一、指针的基本概念

指针在高级语言的编程中发挥着非常重要的作用,它能使得不同区域的代码可以轻易的共享内存数据。

指针使得一些复杂的链接性的数据结构的构建成为可能,有些操作必须使用指针,比如申请堆内存,还有在函数的调用的参数都是按值传递的,如果在函数中修改被传递的对象,就必须通过这个对象指针来完成。

指针就是内存地址,指针变量就是用来存放内存地址的变量,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因为数据类型不同,因此所占的存储空间长度也不同。使用指针不仅可以对数据本身,也可以对存储数据变量的地址进行操作。

简单理解:指针就是内存地址;可以利用指针来保存地址。

二、指针的引入(函数返回中return语句的局限性)

函数的缺陷:

一个函数只能返回一个值,就算我们在函数里面写多了return语句,但是只要执行任何一条return语句,整个函数调用就结束了。

数组:

数组可以帮助我们返回多个值,但是数组是相同数据类型的结合,对于不同数据类型则不能使用数组。

指针:

使用指针可以有效解决这个问题,使用指针我们想反回几个值就能返回几个值,想返回什么类型就可以返回什么类型的值。

简单理解:指针就是内存地址;可以利用指针来保存地址。

在程序设计过程中,存入数据还是取出数据都需要与内存单元打交道,计算机通过地址编码来表示内存单元。指针类型就是为了处理计算机地址数据的,计算机将内存划分为若干个存储空间大小的单元,每个单元大小就是一个字节,即计算机将内存换分为一个一个的字节,然后为每一个字节分配唯一的编码,这个编码即为这个字节的地址。指针就是用来表示这些地址的,即指针型数据不是什么字符型数据,而存的是我们内存中的地址编码。指针可以提高程序的效率,更重要的是能使一个函数访问另一个函数的局部变量,指针是两个函数进行数据交换必不可少的工具。

地址及指针的概念:

程序中的数据(变量,数组等)对象总是存放在内存中,在生命期内这些对象占据一定的内存空间,有确定的存储位置,实际上,每个内存单元都有一个地址,即以字节为单位连续编码。编译器将程序中的对象名转换成机器指令识别的地址,通过地址来存储对象值。

1
int i;   double   f;

计算机为 int 类型数据分配4个字节,为 double 类型分配8个字节。按对象名称存取对象的方式成为对象直接访问,如:i=100; f=3.14; 通过对象地址存取对象的方式成为指针间接访问

三、指针的定义和使用

1、指针变量定义语法:数据类型 * 变量名;

代码举例:

1
2
3
4
5
6
7
8
9
int a = 18;
// 定义指针变量p
int *p;
// 把a的地址给变量p
p = &a;
cout << p << endl;
cout << &a << endl;
// 解引用
cout << *p;

运行结果:

1
2
3
0x7fff93715e1c
0x7fff93715e1c
18

2、指针的初始化,可以在定义指针时对它进行初始化:

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
28
29
30
31
32
33
34
35
// 指针类型* 指针变量名 = 地址初值,......

int a;
// p的初值为变量a 的地址
int * p = &a;
// p1初始化是变量b已有地址值
int b, * p1 = &b;


// 由于指针数据的特殊性,他的初始化和赋值运算是有约束条件的,只能使用以下四种值:
// (1)0值常量表达式:
int a, z = 0;
int p1 = null; //指针允许0值常量表达式
p1 = 0;//指针允许0只常量表达式

// 下面三中形式是错误的:
int* p1 = a;//错误 地址初值不能是变量
int p1 = z;//错误 整形变量不能作为指针,即使值为0
p1 = 4000;//错误,指针允许0值常量表达式

// (2)相同指向类型的对象的地址。
int a, * p1;
double f, * p3;
p1 = &a;
p3 = &f;

p1 = &f;//错误p1和f指向类型不同

// (3)相同指向类型的另一个有效指针
int x, * px = &x;
int* py = px;//相同指向类型的另一个指针

// (4)对象存储空间后面下一个有效地址,如数组下一个元素的地址
int a[10], * px = &a[2];
int* py = &a[++i];

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
28
29
30
31
32
33
34
1)指针加减整数运算
int x[10], n = 3, * p = &x[5];
p + 1 //指向内存空间中x[5]后面的第1个int 型存储单元
p + n //--------------------------n(3)个
p - 1 //-------------------前面-----1个
p - n //

2)指针变量自增自减运算
int x[10], * p = &x[5];
p++ //p指向x[5]后面的第1个int型内存单元
++p //-----------------1--------------
p-- //p指向x[5]前面的第1个int型内存单元
--p //--------------------------------

(3)两个指针相减运算
设p1, p2是相同类型的两个指针,则p2 - p1的结果是两支针之间对象
的个数,如果p2指针地址大于p1则结果为正,否则为负
int x[5], * p1 = &x[0], * p2 = &x[4];
int n;
n = p2 - p1;//n 的值为4 即为他们之间间隔的元素的个数
运算方法:(p2储存的地址编码-p1储存的地址编码)/4 若是double类型则除以8 char类型除以1

1)指针加减整数运算
int x[10], n = 3, * p = &x[5];
p + 1 //指向内存空间中x[5]后面的第1个int 型存储单元
p + n //--------------------------n(3)个
p - 1 //-------------------前面-----1个
p - n //

4)指针的运算关系
设p1、p2是同一个指向类型的两个指针,则p1和p2可以进行关系运算,
用于比较这两个地址的位置关系即哪一个是靠前或者靠后的元素
int x[4], * p1 = &x[0], * p2 = &x[4];
p2 > p1; //表达式为真

通过代码举例,发现是不是跟变量有点相似?

指针变量和普通变量的区别:

  • 普通变量存放的是数据,指针变量存放的是地址。
  • 指针变量可以通过” * “操作符,操作指针变量指向的内存空间,这个过程称为解引用。

总结:

  • 我们可以通过 & 符号 获取变量的地址。
  • 利用指针可以记录地址。
  • 对指针变量解引用,可以操作指针指向的内存数据。

四、指针所占内存空间

  • 指针也是一种数据类型,那么这种数据类型占用多少内存空间呢?
  • 32位操作系统下:指针占4个字节空间大小,不管是什么数据类型;
  • 64位操作系统下:指针占8个字节空间大小,不管是什么数据类型。

代码演示:

1
2
3
4
5
6
7
8
9
int main()
{
// 在64位系统演示下,输出结果都是8位
cout << sizeof(int*) << endl;
cout << sizeof(float*) << endl;
cout << sizeof(double*) << endl;
cout << sizeof(char*) << endl;
return 0;
}

解释:

因为内存是由字节组成的,每个字节都有一个地址编号。指针变量主要是存放相同数据类型的变量的首地址,这里的地址就是指内存中某个字节的编号,而这个编号是由地址总线决定的。

操作系统的位数决定了指针变量所占的字节数:

如果是32位操作系统,也就是地址总线是32位,则它的寻址范围就是0~232-1(4GB),所以每一个字节的编址就会由32个0或1组成。

例:第1个字节的编址是32个0,最后1个的编址是32个1。一个字节有8位,32位则需要4个字节。

如果是64位操作系统,也就是地址总线是64位,则它的寻址范围就是0~264-1(16GB),所以每一个字节的编址就会由64个0或1组成。

例:第1个字节的编址是64个0,最后1个的编址是64个1。一个字节有8位,64位则需要8个字节。

五、空指针和野指针

  • 什么是空指针?空指针的指针变量指向内存中编号为0的空间,它的作用是用来初始化指针变量;空指针指向的内存是不可以访问的。
1
2
3
4
5
6
7
8
int main() {
//指针变量p指向内存地址编号为0的空间
int * p = NULL;
//访问空指针报错
//内存编号0 ~255为系统占用内存,不允许用户访问
cout << *p << endl;
return 0;
}
  • 什么是野指针?野指针就是指针变量指向非法的内存空间。
1
2
3
4
5
6
7
int main() {
//指针变量p指向内存地址编号为0x1100的空间
int * p = (int *)0x1100;
//访问野指针报错
cout << *p << endl;
return 0;
}

空指针和野指针都不是我们申请的空间,因此不要访问。

如果程序中出现野指针的话有可能会导致程序的崩溃,所以我们在定制指针的时候还不知道指向什么的时候先让该指针指向空。

例如:

int *p = NULL;

六、const 修饰指针

const 修饰指针有三种情况:

  • const 修饰指针——常量指针(常量指针只能修改指向,不能修改指向的值)
  • const 修饰常量——指针常量(指针常量只能修改指向的值,不能修改指向)
  • const 即修饰指针,又修饰常量(指向和指向的值都不能修改)

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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; //错误
return 0;
}

小彩蛋:

1
2
3
4
5
6
// 反骨操作:
int const num = 10;
int *p = (int *)&num;
*p = 666;

cout << *p << endl;

七、指针与数组

首先了解一下数组数组名的意义:

1
2
// 例如:
int a[10];

a 是数组名,a 的值是数组的首地址,例如 a 的值是0x00000001,那么a+1表示首地址加 int 大小的地址,即 0x00000005,对a进行解引用才是数组首个元素的值,a=a[0],(a+1)=a[1].

&a 是数组的指针,加一意味着数组首地址加上整个数组的长度(10个int型变量的长度)后的地址,即末尾元素的后一个元素的地址。

那既然有这个规律,我们就可以利用指针来快速的访问数组、遍历数组。

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
cout << "数组的第一个元素:" << arr[0] << endl;
cout << "指针数组的第一个元素:" << *p << endl;

// 指针遍历数组
for (int i = 0; i < sizeof(arr) / sizeof(int); i++) {
cout << *p << " ";
p++;
}
}

八、指针与函数

在普通函数中只是值传递就会出现不可以修改函数实参的情况。

而利用指针作函数参数,可以修改实参的值,以为传入的是一个地址,可以通过改地址去修改值。

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void swap1(int a, int b) {
int temp = a;
a = b;
b = temp;
}

void swap2(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main()
{
int a = 10;
int b = 20;
swap1(a, b);
//运行结果并没有交换
cout << "a = " << a << " b = " << b << endl;

swap2(&a, &b);
//运行结果进行了交换
cout << "a = " << a << " b = " << b << endl;
return 0;
}

C++ 中的指针更多操作正在完善中…..