oynix

于无声处听惊雷,于无色处见繁花

c/c++指针与数组

说一说指针在使用过程中容易混淆不清、出问题的点。

1. 引入

若要说清楚指针,就要先说说内存。内存,每一个字节都有着自己的地址,早年间有个词常说,那就是32位机器,和64位机器(现在32位的机器越来越少),其中的32位和64位,指的便是内存寻址的位数,直白点说就是机器的系统用来存储内存地址的位数。32位,最多能表示2^32个字节,也就是4G个字节,所以在32位机器上运行的程序最多能使用的内存大小就是4G字节。但随着技术发展,4G字节的内存已渐渐不能满足生产生活需求,于是,64位机器应运而生,最多能表示16E个字节,这是一个相当大的数字,虽然目前64位的机器已能满足需求,但也说不定哪天,128位的机器就展露了头脚。

而指针,存储的就是这个地址。

定义一个char类型的变量,它存储的是字符,长度是1个字节。定义一个int类型的变量,它存储的整数,抛开不同类型机器因素,这里认为它的长度是4个字节。定义一个指针类型的变量,在32位的机器上,它存储的就是一个32位的地址,长度为4个字节(1byte=8bits),在64位机器上为8个字节。这里还要说明一点,不管一个变量在内存中占用几个字节,占用4个字节的int也好,占用8个字节的double也罢,它的地址都是低位的第一个地址。比如,一个int占用的4个字节为,0x0000_0001,0x0000_0002,0x0000_0003,0x0000_0004,那么它的地址就是低位的0x0000_0001,double等其他类型,同理。

2. 指针的类型

上面有提到,只要是指针类型的变量,那么它存储的值便是一个内存地址,既然存储的都是地址,那为什么还要有不同的类型呢,比如int型的指针int*、char型的指针char*?主要目的有两个,其一,是用指针类型来限定指针如何解释它所指向的内存,其二,便是限定指针的移动。

先说指针如何解释它所指向的内存。假定32位机器的大顶端机器,

1
2
3
4
内存地址为0x0000_0001的字节,存储的值是0x41
内存地址为0x0000_0002的字节,存储的值是0x41
内存地址为0x0000_0003的字节,存储的值是0x41
内存地址为0x0000_0004的字节,存储的值是0x41

简单说就是4个连续的字节,存储的都是0x41。上面有提,不管什么类型的指针,存储的值都是这个变量的低位字节的地址,现定义一个int类型的指针int* pi令它指向0x0000_0001,再定义一个char* pc,也令它指向0x0000_0001,也就是定义两个不同类型的指针,但是让它们都指向同一个地址。当把它们的值打出来的时候,你会发现pi指向的值为0x4141_4141,pc指向的值为’A’,也就是0x41。指向同样的地址,值却不同,这就是指针类型对内存解释的限定。

再说如何限定指针的移动。就像我们知道的,指针加1就是向后移动,指向下一个值,指针减1就是向前移动,指向前一个值,那么问题来了,每次移动要移动的长度是多少呢,即,每次要移动多少字节呢?指针的类型便会限定指针移动的字节数量,还是上面那个例子,pi加1之后,它会向高位内存移动int的长度,4个字节,随后指向0x0000_0005,而pc加1后,它只会移动1个字节,指向0x0000_0002,因为char的长度为1个字节。关于此,也可以将指针的类型,理解为它的跳跃能力,pi的跳跃能力是4个字节,而pc的跳跃能力是1个字节。

好了,关于指针的类型就说这么多,因为不想写代码,所以假定了一个很直白、也很理想的例子,仅供参考。

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
int arr[5] = {1, 2, 3, 4, 5};
int* pa = arr;
int* pa0 = &arr[0];
int(* parr)[5] = &arr;
cout << "arr = " << arr << endl;
cout << "&arr = " << &arr << endl;
cout << "arr[0] = " << arr[0] << endl;
cout << "&arr[0] = " << &arr[0] << endl;
cout << "pa = " << pa << endl;
cout << "*pa = " << *pa << endl;
cout << "pa的大小:" << sizeof(pa) << endl;
cout << "arr的大小:" << sizeof(arr) << endl;
cout << "*(&arr)[0] = " << *(&arr)[0] << endl;
cout << "*parr[0] = " << *parr[0] << endl;


// 运行结果
arr = 0x7ff7b661e460
arr的地址 = 0x7ff7b661e460
arr[0] = 1
&arr[0] = 0x7ff7b661e460
pa = 0x7ff7b661e460
*pa = 1
pa的大小:8
arr的大小:20
*(&arr)[0] = 1
*parr[0] = 1

像上面这样,一眼看上去,是不是有些迷乱,甚至还有点不知所措?不要担心,路是要一步一步走,让我们逐一击破。

arr是声明的一个长度为5的整型数组,arr的值就是数组第一个元素的地址,也就是arr[0]的地址,第一个元素是个int,所以int型的指针pa是可以指向这个元素的,同时,int型指针pa0,也可以指向arr[0],这个应该不难理解。

用中括号[]从数组中取值的操作,本质上和指针操作,是一样的,也就是说,

1
2
arr[0] = *pa;
arr[1] = *(++pa);

但是,还是有一点区别,数组是有长度的,也就是sizeof(arr)/sizeof(arr[0])的值,这里等于5,指针只要不超过最大内存地址,便可以一直加1向高位移动,但数组一旦超过了声明时的长度,便会报错数组脚标越界。

接下来说容易让人疑惑的对数组取地址:&arr。对数组取地址后,返回的值的类型是数组型指针,如果用一个int型的指针来接收这个值就会报错,而是需要声明一个数组型的指针来接收此值,也就是

1
int(* parr)[5] = &arr;

注意这里的括号,如果不加括号,那么parr的类型就是一个普通数组,里面元素的值是int型指针,要用int型指针来初始化,如果觉得乱,那么横向对比着看就很清晰

1
2
3
4
5
6
7
8
9
10
11
int value = 5;
// int型指针
int* p = &value;

// int型数组,元素类型是int
int arr[5] = {1, 2, 3, 4, 5};
// int*型数组,元素类型是int*
int* parr[5] = {p, p, p, p, p};

// 数组型指针
int(* arrp)[5] = &arr;

int型数组和int*型数组,相同点都是数组,不同点,一个元素类型是int,一个元素类型是int型指针。

int型指针和数组型指针的区别在于,跳跃能力不同,int型指针加1后向高位内存移动一个int的长度,即4个字节,而数组指针在加1后,会向高位内存移动一个数组的长度,即,4x5=20个字节。数组型指针取值,和数组取值相同,这也算对得起名字里的数组,这么看来,数组型指针是不是很像二维数组了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int arr2[2][3] = {{1, 2, 3}, {11, 12, 13}};
// arr2[0]即为数组{1, 2, 3}
int(*arr2p)[3] = &arr2[0];
// arr2p是数组指针,那么(*arr2p)自然是所指向的数组
cout << "二维数组,[0][0]:" << (*arr2p)[0] << endl;
cout << "二维数组,[0][1]:" << (*arr2p)[1] << endl;
cout << "二维数组,[0][2]:" << (*arr2p)[2] << endl;
arr2p++; // 加1之后,移动到下一个数组
cout << "二维数组,[1][0]:" << (*arr2p)[0] << endl;
cout << "二维数组,[1][1]:" << (*arr2p)[1] << endl;
cout << "二维数组,[1][2]:" << (*arr2p)[2] << endl;

// 运行结果
二维数组,[0][0]:1
二维数组,[0][1]:2
二维数组,[0][2]:3
二维数组,[1][0]:11
二维数组,[1][1]:12
二维数组,[1][2]:13

4. const

调用函数时,经常看到返回值或是参数的类型是const char*,还有char const*,关于const,其实有个很好记的小窍门:const修饰谁,谁就不可变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int a = 10;
int b = 20;

// const修饰int,表示cap指向的这个值不能变
const int* cap = &a;
// Yes,可以只声明不初始化赋值,后续再赋值
const int* cap;
// No,不可以,指向地址的值不能变
*cap = 15;
// Yes,可以,cap指向地址的值没变,但是可以指向别的值
cap = &b;

// const不能修饰*,所以,这个时候const修饰的它前面的int,也就是说,
// int const* 和 const int* 两种写法是一样的
int const* acp = &a;

// const修饰指针,表示这个指针指向的地址不能变,所以声明的时候必须初始化赋值
int* const apc = &a;
// No,不可以,声明时必须初始化
int* const apc;
// Yes,可以,虽然改变了a的值,但是apc指向的地址没变,
*apc = 15;
// No,不可以,apc指向的地址不能变
apc = &b;

5. void*

前文写JNI的时候有提到过,类型是void的指针,表示这个指针的跳跃能力未可知,需要在使用的时候手动指定,以便能解释它所指向的内存,多用于通用函数的参数,如内存复制的memcpy,函数本身并不关心传进来的是什么类型的指针,跳跃能力如何,它只负责把从src指针指向的位置开始,复制指定数量的字节的值,到dst指针指向的位置,即可。

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2022/04/b146d2e0d12b/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道