天天看点

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

点击蓝字关注,创智助你长姿势

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

C 和 C++ 中最强大的功能莫过于指针(pointer)了,但是对于大多数人尤其是新手来说,指针是一个最容易出错、也最难掌握的概念了。本文将从指针的方方面面来讲述指针的概念和用法,希望对大家有所帮助。

内存模型

为了更好地理解指针,让我们来看一下计算机的内存模型。

内存分为物理内存和虚拟内存,物理内存对应计算机中的内存条,虚拟内存是操作系统内存管理系统假象出来的。由于这些不是我们本文的重点,下面不做区分。

在不考虑 cpu 缓存的情况下,计算机运行程序本质上就是对内存中的数据的操作,通俗地来说,就是将内存条某些部分的数据搬进搬出或者搬来搬去,其中“搬进搬出”是指将内存中的二进制数据搬入 cpu 寄存器及运算器中进行相应的加减运算或者将寄存器中的数据搬回内存单元中,而“搬来搬去”是指将内存中的数据由这个位置搬到另外一个位置(当然,一般不是直接搬,而是借助寄存器作为中间存储区)。如下图所示:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

计算机为了方便管理内存,将内存的每个单元用一个数字编号,如下图所示:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

图中所示,是一个大小为128个字节的内存空间,其中每一个空格代表一个字节,所以内存编号是0~127。

对于一个 32 位的操作系统来说,内存空间中每一个字节的编号是一个 32 位二进制数,所以内存编号从 0000 0000 0000 0000 0000 0000 0000 0000 至 1111 1111 1111 1111 1111 1111 1111 1111,转换成 16 进制也就是 0x00000000 至  0xFFFFFFFF,由于是从 0 开始的,所以化成 10 机制就是从  0 至 2 的 32 次方减 1;对于 64 位操作系统,内存编号也就是从 64 个 0 至 64 个 1。

大家需要注意的是,从上面两个图我们可以发现,我们一般将编号小的内存单元画在上面,编号大的画在下面,也就是说从上至下,内存编号越来越大。

指针与指针变量

指针的本意就是内存地址,我们可以通俗地理解成内存编号,既然计算机通过编号来操作内存单元,这就造就了指针的高效率。

那么什么是指针变量呢?指针变量可通俗地理解成存储指针的变量,也就是存储内存地址(内存编号)的变量。首先指针变量和整型变量、字符型变量以及其他数据类型的变量一样都是变量类型;但是,反过来,我们不应该按这样的方式来分类,即:整型指针变量、字符型指针变量、浮点型指针变量等等。为什么不推荐这样的分类方法呢?首先,指针变量就是一个数据类型,指针数据类型,这种数据类型首先是一个变量数据类型,那么它的大小是多少呢?很多同学理所当然地认为整型指针变量和一个字符指针变量的大小是不一样的,这种认识是错的。指针变量也是一个变量,它是一个用来存储其他变量的内存地址的,更准确地说,指针变量时用来存储其他变量的内存首地址的,因为不同的数据类型所占的内存大小不一样。举个例子,在 32 位机器上,假如 a 是 int 型变量,pa 是指向 a 的指针变量,b 是一个 double 型变量,pb 是指向 b 的指针变量,那么 a 在内存中占四个字节,b 在内存中占 8 个字节,假如 a 在内存中分布是从 0x11111110~0x11111113,而 b 在内存中分布是 0x11112221 至 0x11112228,那么指针变量 pa 中存储的内容是 0x11111110,而 pb 中存储就是 0x11112221,看到了吧,也就是说,pa 和 pb 中存储的都是地址,而且都是  32 位的二进制地址;再者,因为存储这样的地址需要 4 个字节,所以无论是 int 型指针变量 pa 或者是 double 型指针变量  pb,它们所占的内存大小都是四个字节,从这点来说,不管什么类型的指针都是一样的,所以不论按整型指针变量、字符型指针变量、浮点型指针变量等等来区分指针变量。总结起来,指针变量和 int、float、char 等类型一样同属变量类型,指针变量类型占四个字节(32 位机器下),存储的是 32 位的内存地址。下面的代码证明这一点:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针
c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

上面介绍的是指针变量的一个方面,指针变量还有另外一层含义:在 C/C++ 中星号(*)被定义成取内容符号,虽然所有指针变量占的内存大小和存储的内存地址大小都是一样的,但是由于存储的只是数据的内存首地址,所以指针变量存储的内存地址所指向的数据类型决定着如何解析这个首地址,也就是说对于 int 型指针变量,我们需要从该指针变量存储的(首)地址开始向后一直搜索 4 个字节的内存空间,以图中的变量 a 为例就是从 0x12ff60~0x12ff63,对于变量 b 就是  0x12ff44~0x12ff4b。所以从这个意义来上讲,当我们使用 *pa,必须先知道 pa 是一个整型的指针,这里强调“整型”,而 a 的值 1 也就存储在从 0x12ff60~0x12ff63 这四个字节里面,当我们使用*pb,必须先知道 pb 是一个 double 型指针,这里强调"double",也就是说值 2.0000 存储在 0x12ff44~0x12ff4b  这八个字节里面。因此,我们对指针变量进行算术运算,比如  pa + 2,pb + + 之类的操作,是以数据类型大小为单位的,也就是说 pa+2,相当于 0x12ff60+sizeof(int) * 2 = 0x12ff60 + 4 * 2 = 0x12ff68,不是0x12ff60 + 2哦;而pb - -相当于 0x12ff44 +  sizeof(double) * 1 = 0x12ff44 + 8 * 1 = 0x12ff4c。理解这一点很重要。同理 &a + 2 和 &b - 1 也是一样(注意由于 &b 是一个指针常量,所以写成 &b - - 是错误的)。

指针变量和指针常量

指针变量首先是一个变量,由于指针变量存储了某个变量的内存首地址,我们通常认为“指针变量指向了该变量”,但是在这个时刻指针变量 pa 指向变量 a,下个时候可能不存储变量 a  的首地址,而是存储变量 c 的首地址,那么我们可以认为这个时候,pa 不再指向 a,而是指向 c。请别嫌我啰嗦,为了帮助你理解,我是故意说得这么细的,后面我们讨论高级主题的时候,当你觉得迷糊,请回来反复咀嚼一下这段话。也就是说指针变量是一个变量,它的值可以变动的。

相反,指针常量可通俗地理解为存储固定的内存单元地址编号的“量”,它一旦存储了某个内存地址以后,不可再改存储其他的内存地址了。所以指针常量是坚韧,因为它“咬定青山不放松”;说是“痴情”,因为它“曾经沧海难为水”。我这里讲的指针常量对应的是 const 关键字定义的量,而不是指针字面量。像  &a,&b,&a + 2 等是指针字面量,而 const int *p = &a;中的 p 才算是真正的指针常量,指针常量一般用在函数的参数中,表示该函数不可改变实参的内容。来看一个例子吧:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

上面的函数由于修改了一个常指针(多数情况下等同指针常量),因而会编译出错:error C3892: “x” :不能给常量赋值。

指针变量与数组

记得多年以前,我在学生会给电子技术部和地理信息系统专业的同学进行 C 语言培训时,这是一个最让他们头疼和感到一头雾水的话题,尤其是指针变量与二维数组的结合,我永远忘不了胡永月那一脸迷惑与无助的表情。今天我这里给大家深入地分析一下。先看一个例子:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

如果你能得出下面这样的结果,说明你已经基本上对数组与指针的概念理解清楚了:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

通过上图,我们可以知道*(a + 1) = 2, *(ptr - 1) = 5。且不说很多同学根本得不到这样的结果,他们看到 int *ptr = (int*)(&a+1);这样的语句就已经懵了,首先,我们知道 C 语言中规定数组名表示这个数组的首地址,而这里竟然出现了 &a 这样的符号,本来 a 就是一个指针常量了,这里对 &a 再次取地址难道不是非法操作吗?哈哈,当你有这样的疑问的时候,说明你对二维数组相关知识理解不深入。我这里先给你补充下知识点吧:

看这样一个二维数组:int arr[3][4],这个数组布局如下:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

这是一个3行4列的数组,它在内存中的分布如下:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

这里每一个数组元素占 4 字节空间,我们知道 C 语言规定,数组名 arr 是整个数组元素的首地址,比如是 0x0012ff08,而像 arr[0]、arr[1]、arr[2] 分别是数组第一行、第二行、第三行的首地址,也就是 0x0012ff08、0x0012ff18、0x0012ff28。

我们把 arr、arr[0] 和 &arr[0][0] 单独拿出来分析,因为数组的首地址也是第一列的首地址,同时也是第一个元素的首地址,所以 arr 和 arr[0] 和 &arr[0][0] 表示的都是同一个地址,但是这三个首地址在进行算术运算时是有区别的。如果 &arr[0][0] + 1,这里的相当于跳一个元素的内存字节数,也就是 4 个;但是 arr[0] + 1,移动的内存字节数是一列元素所占的字节数,也就是 4 * 4 = 16 个;最后,也是最让人迷惑的的就是 arr + 1,这个时候移动的内存数目是整个数组占的内存字节数,也就是  48 个字节数,所以 a + 1 所表示的内存地址已经不属于这个数组了,这个地址位于数组最后一个元素所占内存空间的下一个字节空间。

光有这些知识还是不能解决上面的问题,我们再补充一个知识点。

C++ 是一种强类型的语言,其中有一种类型叫 void 类型,从本质上说 void 不是一种类型,因为变量都是“有类型”的,就好像人的性别,不是男人就是女人,不存在无性别的人,所以  void 更多是一种抽象。在程序中,void 类型更多是用来“修饰”和“限制”一个函数的:例如一个函数如果不返回任何类型的值,可以用 void 作返回类型;如果一个函数无参数列表,可以用 void 作为参数列表。

跟 void 类型”修饰“作用不同,void 型指针作为指向抽象数据的指针,它本质上表示一段内存块。如果两个指针类型不同,在进行指针类型赋值时必须进行强制类型转换,看下面的例子:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

但是可以将任何指针类型赋值给 void 类型而无须进行强制类型转换:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

当然,如果把 void 型指针转换成并不是它实际指向的数据类型,其结果是不可预测的。试想,如果把一个 int 型指针赋给  void 型,然后再把这个 void 型指针强制转换成 double 型指针,这样的结果是不可预测的。因为不同数据类型所占内存大小不一样,这样做可能或截断内存数据或者会增加一些未知的额外数据。所以,最好是将 void 类型指针转换成它实际数据类型指针。

有了上面的说明,你应该能看懂 C 函数库中下面这个函数的签名含义了吧?

void *memcpy(void *dest,const void *src,size_t len);
           

在这里,任何数据类型的指针都可以传给这个函数,所以这个函数成为了一个通用的内存复制函数。

好了,说了这么多,回答最初的那个问题上:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

我们来分析一下。首先,我们可以将这个数组看成是一个特殊的二维数组,也就是 1 行 5 列的二维数组,现在 a 表示的是第一个元素的首地址,那么 a + 1 指向的就是下一个元素的内存首地址,所以*(a + 1) = 2;而 &a 则是表示整个数组的首地址,那么 &a + 1 移动的内存数目就是整个数组所占字节数,假如这里我们量化来说明,假如原先数组中第一个元素的首地址是 1,那么 &a + 1 表示的就是 21,而这个地址已经不属于数组了,接着通过(int*)(&a + 1)将数组指针转换成整型指针,这样原先 &a + 1 表示的数据范围是 21~40 一下缩小到 21~24,正好是一个 int 型的大小,所以 ptr - 1 的存储的地址就是 17 了,表示的数据内存范围是 17~20,这样*(ptr - 1)正好就是最后一个元素 5 了。

但是话说回来,首先这样的转换安全与否尚有争议,再次,这样的程序晦涩难懂,难于理解,所以建议不要写出这样的程序。

上面的例子,只是通过一些简单的数据类型来说明内存分布,但是实际对于一些复杂的数据类型,尤其是一些自定义的类或者结构体类型,内存分布必须还要充分考虑到字节对齐。比如下面的代码:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

这是输出结果:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

由于结构体 s1 中存在字节对齐现象(以 sizeof(double) = 8 个字节对齐),所以 s1 占据 24 字节内存,而 s2 只占 16 个字节。知道这点,我们平常在设计结构体字段的时候,就可以合理安排字段顺序来使用更少的内存空间了。

函数指针

函数指针是指向函数的指针变量。因而“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。C/C++ 程序在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是一致的。函数指针有两个用途:调用函数和做函数的参数。

我们先来先使用函数指针调用函数。如下图所示:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

上面的代码首先是定义了一个函数f,然后是定义一个函数指针 pf,接着在主函数里面将函数f的地址赋值给函数指针,这样 pf 就指向了函数 f,这样使用*pf 就可以直接调用函数了。但是上面的例子定义函数指针的方法在某些编译器中是无法通过的,最好通过 typedef 关键字定义函数指针,推荐的写法如下:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

通过上面的例子,我们来总结下函数指针的定义和使用方法:

首先,通过 typedef 关键字定义一个函数指针类型,然后定义一个该函数指针类型变量,接着将函数的入口地址赋值给该函数指针类型变量,这样就可以通过这个函数指针变量调用函数了。

需要注意的是,定义函数指针类型时的函数签名(包括函数返回值和函数参数列表的类型、个数、顺序)要将赋值给该类型变量的函数签名保持一致,不然可能会发生很多无法预料的情况。还有一点,就是 C/C++ 规定函数名就表示函数入口地址,所以,函数名赋值时函数名前面加不加取地址符 & 都一样,也就是说 PF pf = f 等价于 PF pf = &f。这个 & 是可以省略的。但是这是单个函数的情况,在 C++ 中取类的方法函数的地址时,这个 & 符号式不能省略的,见下面的例子:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

函数指针的另外一个用处,而且是用的最多的,就是作为一个函数的参数。也就是说某个函数的某个参数类型是一个函数,这在 windows 编程中作为回调函数(callback)尤其常见。我们来看一个例子:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

上图中,函数 f2 第一个参数类型是一个函数,我们传入函数 f1 作为参数。这种函数参数是函数类型的用法很重要,建议大家掌握。

指针变量的定义方法

先插播一段广告,说下main函数的返回值问题,如下图:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

这种main函数无返回值的写法,在国内各大C/C++教材上屡见不鲜,这种写法是错误的!

有一点你必须明确:C/C++ 标准中从来没有定义过 void main()这样的代码形式。C++ 之父 Bjarne Stroustrup 在他的主页 FAQ 中明确地写了这样一句话:

“在C++中绝对没有出现过void main(){ } 这样的函数定义,在C语言中也是。”

main 函数的返回值应该定义为 int 型,在 C/C++ 标准中都是这样规定的。在 C99 标准规定,只有以下两种定义方式是正确的:

int main(void);int main(int argc,char *argv[]);
           

虽然在 C 和 C++ 标准中并不支持 void main(),但是在部分编译器中 void main()依旧是可以通过编译并执行的,比如微软的 VC++。由于微软产品的市场占有率和影响力很大,因为在某种程度上加剧了这种不良习惯的蔓延。不过,并非所有犯人编译器都支持 void main(),gcc 就站在 VC++ 的对立面,它是这一不良习气的坚定抵制者,它会在编译时明确地给出一个错误。

广告播完,我们回到正题上来。我们来看下如何定义一个指针,首先看一个例子:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

我来替你回答吧,你肯定认为 a 是一个指针变量,b 是一个整型变量,c 和 d 都是一个指针变量。好吧,恭喜你,答错了!

其实定义指针变量的时候,星号(*)无论是与数据类型结合还是与变量名结合在一起都是一样的!但是,为了便于理解,还是推荐大家写成第一种形式,第二种形式容易误导人,不是吗?而且第一种形式还有一个好处,我们可以这样看:

int *a;    //将*a 看成一个整体,它是一个 int 型数据,那么 a 自然就是指向*a 的指针了。

说完定义指针的方法,下面我们来看下如何初始化一个指针变量,看下面的代码:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

上面的代码有错误吗?

错误在于我们不能这样写:int *p = 1; 由于 p 是一个匿名指针,也就是说 p 没有正确的初始化,它可能指向一个不确定的内存地址,而这个内存地址可能是系统程序内存所在,我们将数值 1 装入那个不确定的内存单元中是很危险的,因为可能会破坏系统那个内存原来的数据,引发异常。换另一个方面来看,将整型数值 1 直接赋值给指针型变量 p 是非法的。

这样的指针我们称为匿名指针或者野指针。和其他变量类型一样,为了防止发生意料之外的错误,我们应该给新定义的指针变量一个初始值。但是有时候,我们又没有合适的初始值给这个指针,怎么办?我们可以使用 NULL 关键字或者 C++ 中的  nullptr。代码如下:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

通过上面的写法就告诉编译器,这两个指针现在不会指向不确定的内存单元了,但是目前暂时不需要使用它们。 

C++中的引用

C++ 中不仅有指针的概念,而且还存在一个引用的概念,看下面的代码:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

我开始在接触这个概念的时候,老是弄错。当时这么想的,既然 b 是 a 的引用,那么 &b 应该等于 a 吧?也就是说,在需要使用变量 a 的时候,可以使用 &b 来代替。

上面的这种认识是错误的!所谓引用,使用另外一个变量名来代表某一块内存,也就是说 a 和 b 完全是一样,所以任何地方,可以使用 a 的,换成 b 也可以,而不是使用 &b,这就相当于同一个人有不同的名字,但是不管哪个名字,指的都是同一个人。

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

新手在刚接触引用的使用,还有一个地方容易出错,就是忘记给引用及时初始化,注意这里的“及时”两个字,C++ 规定,定义一个引用时,必须马上初始化。看下面的代码:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

传值还是传引用(by value or by reference)

看下面的伪代码:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

在涉及到利用一个已有初值的变量给另外一个变量赋值时,必须考虑这样的情况。图中变量 a 已经有了初值,然后利用 a 来给 b 赋初值,那么最后改变 b 的值,a 的值会不会受影响呢?这就取决于 b 到底是 a 的副本还是和 a 同时指向同一内存区域,这就是我们常说的赋值时是传值还是传引用。各大语言都是这样规定的,也就是说不局限于 C/C++,同时 Java、C#、php、javascript 等都一样:

如果变量类型是基元数据类型(基础数据类型),比如 int、float、bool、char 等小数据类型被称为基元数据类型(primitive data type),那么赋值时传的是值。也就是说,这个时候 b 的值是 a 的拷贝,那么更改 b 不会影响到 a,同理更改 a 也不会影响到 b。

但是,如果变量类型是复杂数据类型(complex data type),不如数组、类对象,那么赋值时传的就是引用,这个时候,a 和 b 指向的都是同一块内存区域,那么无论更改 a 或者 b 都会相互影响。

让我们来深入地分析下,为什么各大语言都采取这种机制。对于那些基元数据类型,由于数据本身占用的内存空间就小,这样复制起来不仅速度快,即使这样的变量数目很多,总共也不会占多大空间。但是对于复杂数据类型,比如一些类对象,它们包含的属性字段就很多,占用的空间就大,如果赋值时,也是复制数据,那么一个两个对象还好,一旦多一点比如 10  个、100 个,会占很大的内存单元的,这就导致效率的下降。

最后,提醒一点,在利用 C++ 中拷贝构造函数复制对象时需要注意,基元数据类型可以直接复制,但是对于引用类型数据,我们需要自己实现引用型数据的真正复制。

C/C++中的new关键字与Java、C#中的关键字对比

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

我大学毕业的时候痴迷于于网页游戏开发,使用的语言是 flash 平台的 actionscript 3.0(简称 as3,唉,如今已经没落),我刚开始由 as3 转行至 C/C++,对于 C/C++ 中 new 出来的对象必须通过指针对象来引用它非常不习惯。上图中,Object 是一个类(class),在 Java 或者 C# 等语言中,通过 new 关键字定义一个对象,直接得到 Object 的实例,也就是说后续引用这个对象,我们可以直接使用 obj.property 或者 obj.method()等形式,但是在 C++ 中不行,比如用一个指针去接受这个 new  出来的对象,我们引用这个对象必须使用指针引用运算符 ->,也就是我们需要这样写:pObj->property 或 pObject->method()。代码如下:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

当然 C++ 中还有一种不需要使用指针就可以实例化出来类对象的方法,从 Java、C# 等转向 C++ 的程序员容易误解为未初始化对象变量的定义,看下列代码:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

这是 C++ 中利用 Object 类实例化两个对象 obj1 和 obj2,obj2 因为调用构造函数传了两个参数 param1,param2 还好理解一点,对于 obj1 很多 Java 或者 C# 的程序员开始很难接受这种写法,因为如果放在 Java 或者 C# 中,obj1 根本就没有被实例化嘛,在他们看来,obj1 只是一个简单的类型申明。希望Java、C# 等程序员要转换过思维来看待 C++ 中的这种写法。

还有一点也容易出错,在 C++ 中,this 关键字是一个指针,而不是像在 Java、C# 中是一个类实例。也就是说,在 C++ 中*this 才等价于 Java、C# 中的 this。所以写法也就不一样了:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针
c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

Windows编程中的指针

windows 是操作系统是用 C 语言写出来的,所以尽管你在  windows 中看到很多不认识的数据类型,但是这些数据类型也是通过基本的 C 语言类型组装起来的。我们这里只介绍  windows 中指针型数据。

定义指针数据类型必须使用星号(*),但是 windows 为了开发的方便,通过宏定义将指针“隐藏起来”,严格地说应该是将星号隐藏起来了,下面给出一些例子:

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

原文链接:https://mp.weixin.qq.com/s/Ua1xi_K2GuYusM0e8HRNhg

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

创智俱乐部

微信:sziitlSA

c定义一个整型数组_转载 | 深入理解 C/C++ 中的指针

一个让你涨姿势的社团

长按二维码关注