一.指针。

它的本质是地址的类型。在许多语言中根本就没有这个概念。但是它却正是C灵活,高效,在面向过程的时代所向披靡的原因所在。因为C的内存模型基本上对应了现在von Neumann(冯·诺伊曼)计算机的机器模型,很好的达到了对机器的映射。不过有些人似乎永远也不能理解指针【注1】。

注1:Joel Spolsky就是这样认为的,他认为对指针的理解是一种aptitude,不是通过训练就可以达到的

指针可以指向值、数组、函数,当然它也可以作为值使用。

看下面的几个例子:

int* p;//p是一个指针,指向一个整数

int** p;//p是一个指针,它指向第二个指针,然后指向一个整数

int (*pa)[3];//pa是一个指针,指向一个拥有3个整数的数组

int (*pf)();//pf是一个指向函数的指针,这个函数返回一个整数

后面第四节我会详细讲解标识符(identifier)类型的识别。

1.指针本身的类型是什么?

先看下面的例子:int a;//a的类型是什么?

对,把a去掉就可以了。因此上面的4个声明语句中的指针本身的类型为:

int*

int**

int (*)[3]

int (*)()

它们都是复合类型,也就是类型与类型结合而成的类型。意义分别如下:

point to int(指向一个整数的指针)

pointer to pointer to int(指向一个指向整数的指针的指针)

pointer to array of 3 ints(指向一个拥有三个整数的数组的指针)

pointer to function of parameter is void and return value is int (指向一个函数的指针,这个函数参数为空,返回值为整数)

2.指针所指物的类型是什么?

很简单,指针本身的类型去掉 “*”号就可以了,分别如下:

int

int*

int ()[3]

int ()()

3和4有点怪,不是吗?请擦亮你的眼睛,在那个用来把“*”号包住的“()”是多余的,所以:

int ()[3]就是int [3](一个拥有三个整数的数组)

int ()()就是int ()(一个函数,参数为空,返回值为整数)【注2】

注2:一个小小的提醒,第二个“()”是一个运算符,名字叫函数调用运算符(function call operator)。

3.指针的算术运算。

请再次记住:指针不是一个简单的类型,它是一个和指针所指物的类型复合的类型。因此,它的算术运算与之(指针所指物的类型)密切相关。

int a[8];

int* p = a;

int* q = p + 3;

p++;

指针的加减并不是指针本身的二进制表示加减,要记住,指针是一个元素的地址,它每加一次,就指向下一个元素。所以:

int* q = p + 3;//q指向从p开始的第三个整数。

p++;//p指向下一个整数。

double* pd;

……//某些计算之后

double* pother = pd – 2;//pother指向从pd倒数第二个double数。

4.指针本身的大小。

在一个现代典型的32位机器上【注3】,机器的内存模型大概是这样的,想象一下,内存空间就像一个连续的房间群。每一个房间的大小是一个字节(一般是二进制8位)。有些东西大小是一个字节(比如char),一个房间就把它给安置了;但有些东西大小是几个字节(比如double就是8个字节,int就是4个字节,我说的是典型的32位),所以它就需要几个房间才能安置。

注3:什么叫32位?就是机器CPU一次处理的数据宽度是32位,机器的寄存器容量是32位,机器的数据,内存地址总线是32位。当然还有一些细节,但大致就是这样。16位,64位,128位可以以此类推。

这些房间都应该有编号(也就是地址),32位的机器内存地址空间当然也是32位,所以房间的每一个编号都用32位的二进制数来编码【注4】。请记住指针也可以作为值使用,作为值的时候,它也必须被安置在房间中(存储在内存中),那么指向一个值的指针需要一个地址大小来存储,即32位,4个字节,4个房间来存储。

注4:在我们平常用到的32位机器上,绝少有将32位真实内存地址空间全用完的(232 = 4G),即使是服务器也不例外。现代的操作系统一般会实现32位的虚拟地址空间,这样可以方便运用程序的编制。关于虚拟地址(线性地址)和真实地址的区别以及实现,可以参考《Linux源代码情景分析》的第二章存储管理,在互联网上关于这个主题的文章汗牛充栋,你也可以google一下。

但请注意,在C++中指向对象成员的指针(pointer to member data or member function)的大小不一定是4个字节。为此我专门编制了一些程序,发现在我的两个编译器(VC7.1.3088和Dev-C++4.9.7.0)上,指向对象成员的指针的大小没有定值,但都是4的倍数。不同的编译器还有不同的值。对于一般的普通类(class),指向对象成员的指针大小一般为4,但在引入多重虚拟继承以及虚拟函数的时候,指向对象成员的指针会增大,不论是指向成员数据,还是成员函数。【注5】。

注5:在Andrei Alexandrescu的《Modern C++ Design》的5.13节Page124中提到,成员函数指针实际上是带标记的(tagged)unions,它们可以对付多重虚拟继承以及虚拟函数,书上说成员函数指针大小是16,但我的实践告诉我这个结果不对,而且具体编译器实现也不同。一直很想看看GCC的源代码,但由于旁骛太多,而且心不静,本身难度也比较高(这个倒是不害怕^_^),只有留待以后了。

还有一点,对一个类的static member来说,指向它的指针只是普通的函数指针,不是pointer to class member,所以它的大小是4。

5.指针运算符&和*

它们是一对相反的操作,&取得一个东西的地址(也就是指针),*得到一个地址里放的东西。这个东西可以是值(对象)、函数、数组、类成员(class member)。

其实很简单,房间里面居住着一个人,&操作只能针对人,取得房间号码;

*操作只能针对房间,取得房间里的人。

参照指针本身的类型以及指针所指物的类型很好理解。

小结:其实你只要真正理解了1,2,就相当于掌握了指针的牛鼻子。后面的就不难了,指针的各种变化和C语言中其它普通类型的变化都差不多(比如各种转型)。

二.数组。

在C语言中,对于数组你只需要理解三件事。

1.C语言中有且只有一维数组。

所谓的n维数组只是一个称呼,一种方便的记法,都是使用一维数组来仿真的。

C语言中数组的元素可以是任何类型的东西,特别的是数组作为元素也可以。所以int a[3][4][5]就应该这样理解:a是一个拥有3个元素的数组,其中每个元素是一个拥有4个元素的数组,进一步其中每个元素是拥有5个整数元素的数组。

是不是很简单!数组a的内存模型你应该很容易就想出来了,不是吗?:)

2.数组的元素个数,必须作为整数常量在编译阶段就求出来。

int i;

int a;//不合法,编译不会通过。

也许有人会奇怪char str[] = “test”;没有指定元素个数为什么也能通过,因为编译器可以根据后面的初始化字符串在编译阶段求出来,

不信你试试这个:int a[];

编译器无法推断,所以会判错说“array size missing in a”之类的信息。不过在最新的C99标准中实现了变长数组【注6】

注6:如果你是一个好奇心很强烈的人,就像我一样,那么可以查看C99标准6.7.5.2。

3.对于数组,可以获得数组第一个(即下标为0)元素的地址(也就是指针),从数组名获得。

比如int a[5]; int* p = a;这里p就得到了数组元素a[0]的地址。

其余对于数组的各种操作,其实都是对于指针的相应操作。比如a[3]其实就是*(a+3)的简单写法,由于*(a+3)==*(3+a),所以在某些程序的代码中你会看到类似3[a]的这种奇怪表达式,现在你知道了,它就是a[3]的别名。还有一种奇怪的表达式类似a[-1],现在你也明白了,它就是* (a-1)【注7】。

注7:你肯定是一个很负责任的人,而且也知道自己到底在干什么。你难道不是吗?:)所以你一定也知道,做一件事是要付出成本的,当然也应该获得多于成本的回报。

我很喜欢经济学,经济学的一个基础就是做什么事情都是要花成本的,即使你什么事情也不做。时间成本,金钱成本,机会成本,健康成本……可以这样说,经济学的根本目的就是用最小的成本获得最大的回报。

所以我们在自己的程序中最好避免这种邪恶的写法,不要让自己一时的智力过剩带来以后自己和他人长时间的痛苦。用韦小宝的一句话来说:“赔本的生意老子是不干的!

但是对邪恶的了解是非常必要的,这样当我们真正遇到邪恶的时候,可以免受它对心灵的困扰!

对于指向同一个数组不同元素的指针,它们可以做减法,比如int* p = q+i;p-q的结果就是这两个指针之间的元素个数。i可以是负数。但是请记住:对指向不同的数组元素的指针,这样的做法是无用而且邪恶的!

对于所谓的n维数组,比如int a[2] [3];你可以得到数组第一个元素的地址a和它的大小。*(a+0)(也即a[0]或者*a)就是第一个元素,它又是一个数组int[3],继续取得它的第一个元素,*(*(a+0)+0)(也即a[0][0]或者*(*a)),也即第一个整数(第一行第一列的第一个整数)。如果采用这种表达式,就非常的笨拙,所以a[0][0]记法上的简便就非常的有用了!简单明了!

对于数组,你只能取用在数组有效范围内的元素和元素地址,不过最后一个元素的下一个元素的地址是个例外。它可以被用来方便数组的各种计算,特别是比较运算。但显然,它所指向的内容是不能拿来使用和改变的!

关于数组本身大概就这么多,下面简要说一下数组和指针的关系。它们的关系非常暧昧,有时候可以交替使用。

比如 int main(int args, char* argv[])中,其实参数列表中的char* argv[]就是char** argv的另一种写法。因为在C语言中,一个数组是不能作为函数引数(argument)【注8】直接传递的。因为那样非常的损失效率,而这点违背了C语言设计时的基本理念——作为一门高效的系统设计语言。

注8:这里我没有使用函数实参这个大陆术语,而是运用了台湾术语,它们都是argument这个英文术语的翻译,但在很多地方中文的实参用的并不恰当,非常的勉强,而引数表示被引用的数,很形象,也很好理解。很快你就可以像我一样适应引数而不是实参。

dereferance,也就是*运算符操作。我也用的是提领,而不是解引用。

我认为你一定智勇双全:既有宽容的智慧,也有面对新事物的勇气!你不愿意承认吗?:)

所以在函数参数列表(parameter list)中的数组形式的参数声明,只是为了方便程序员的阅读!比如上面的char* argv[]就可以很容易的想到是对一个char*字符串数组进行操作,其实质是传递的char*字符串数组的首元素的地址(指针)。其它的元素当然可以由这个指针的加法间接提领(dereferance)【参考注8】得到!从而也就间接得到了整个数组。

但是数组和指针还是有区别的,比如在一个文件中有下面的定义:

char myname[] = “wuaihua”;

而在另一个文件中有下列声明:

extern char* myname;

它们互相是并不认识的,尽管你的本义是这样希望的。

它们对内存空间的使用方式不同【注9】。

对于char myname[] = “wuaihua”如下

myname

w

u

a

i

h

u

a

\0

对于char* myname;如下表

myname

\|/

w

u

a

i

h

u

a

\0

注9:可以参考Andrew Konig的《C陷阱与缺陷》4.5节。

改变的方法就是使它们一致就可以了。

char myname[] = “wuaihua”;

extern char myname[];

或者

char* myname = “wuaihua”;//C++中最好换成const char* myname = “wuaihua”。

extern char* myname;

C之诡谲(下)

三.类型的识别。

基本类型的识别非常简单:

int a;//a的类型是a

char* p;//p的类型是char*

……

那么请你看看下面几个:

int* (*a[5])(int, char*); //#1

void (*b[10]) (void (*)()); //#2

doube(*)() (*pa)[9]; //#3

如果你是第一次看到这种类型声明的时候,我想肯定跟我的感觉一样,就如晴天霹雳,五雷轰顶,头昏目眩,一头张牙舞爪的狰狞怪兽扑面而来。

不要紧(Take it easy)!我们慢慢来收拾这几个面目可憎的纸老虎!

1.C语言中函数声明和数组声明。

函数声明一般是这样int fun(int,double);对应函数指针(pointer to function)的声明是这样:

int (*pf)(int,double),你必须习惯。可以这样使用:

pf = &fun;//赋值(assignment)操作

(*pf)(5, 8.9);//函数调用操作

也请注意,C语言本身提供了一种简写方式如下:

pf = fun;// 赋值(assignment)操作

pf(5, 8.9);// 函数调用操作

不过我本人不是很喜欢这种简写,它对初学者带来了比较多的迷惑。

数组声明一般是这样int a[5];对于数组指针(pointer to array)的声明是这样:

int (*pa)[5]; 你也必须习惯。可以这样使用:

pa = &a;// 赋值(assignment)操作

int i = (*pa)[2]//将a[2]赋值给i;

2.有了上面的基础,我们就可以对付开头的三只纸老虎了!:)

这个时候你需要复习一下各种运算符的优先顺序和结合顺序了,顺便找本书看看就够了。

#1:int* (*a[5])(int, char*);

首先看到标识符名a,“[]”优先级大于“*”,a与“[5]”先结合。所以a是一个数组,这个数组有5个元素,每一个元素都是一个指针,指针指向“(int, char*)”,对,指向一个函数,函数参数是“int, char*”,返回值是“int*”。完毕,我们干掉了第一个纸老虎。:)

#2:void (*b[10]) (void (*)());

b是一个数组,这个数组有10个元素,每一个元素都是一个指针,指针指向一个函数,函数参数是“void (*)()”【注10】,返回值是“void”。完毕!

注10:这个参数又是一个指针,指向一个函数,函数参数为空,返回值是“void”。

#3. doube(*)() (*pa)[9];

pa是一个指针,指针指向一个数组,这个数组有9个元素,每一个元素都是“doube(*)()”【也即一个指针,指向一个函数,函数参数为空,返回值是“double”】。

现在是不是觉得要认识它们是易如反掌,工欲善其事,必先利其器!我们对这种表达方式熟悉之后,就可以用“typedef”来简化这种类型声明。

#1:int* (*a[5])(int, char*);

typedef int* (*PF)(int, char*);//PF是一个类型别名【注11】。

PF a[5];//跟int* (*a[5])(int, char*);的效果一样!

注11:很多初学者只知道typedef char* pchar;但是对于typedef的其它用法不太了解。Stephen Blaha对typedef用法做过一个总结:“建立一个类型别名的方法很简单,在传统的变量声明表达式里用类型名替代变量名,然后把关键字typedef加在该语句的开头”。可以参看《程序员》杂志2001.3期《C++高手技巧20招》。

#2:void (*b[10]) (void (*)());

typedef void (*pfv)();

typedef void (*pf_taking_pfv)(pfv);

pf_taking_pfv b[10]; //跟void (*b[10]) (void (*)());的效果一样!

#3. doube(*)() (*pa)[9];

typedef double(*PF)();

typedef PF (*PA)[9];

PA pa; //跟doube(*)() (*pa)[9];的效果一样!

3.const和volatile在类型声明中的位置

在这里我只说const,volatile是一样的【注12】!

注12:顾名思义,volatile修饰的量就是很容易变化,不稳定的量,它可能被其它线程,操作系统,硬件等等在未知的时间改变,所以它被存储在内存中,每次取用它的时候都只能在内存中去读取,它不能被编译器优化放在内部寄存器中。

类型声明中const用来修饰一个常量,我们一般这样使用:const在前面

const int;//int是const

const char*;//char是const

char* const;/

int offset;

} va_list;

其它的定义类似。

经常在Windows进行系统编程的人一定知道函数调用有好几种不同的形式,比如:

__stdcall,__pascal,__cdecl。在Windows下_stdcall,__pascal是一样的,所以我只说一下__stdcall和__cdecl的区别。

(1)__stdcall表示被调用端自身负责函数引数的压栈和出栈。函数参数个数一定的函数都是这种调用形式。

例如:int fun(char c, double d),我们在main函数中使用它,这个函数就只管本身函数体的运行,参数怎么来的,怎么去的,它一概不管。自然有main负责。不过,不同的编译器的实现可能将参数从右向左压栈,也可能从左向右压栈,这个顺序我们是不能加于利用的【注15】。

注15:你可以在Herb Sutter的《More Exceptional C++》中的条款20:An Unmanaged Pointer Problem, Part 1:Parameter Evaluation找到相关的细节论述。

(2)__cdecl表示调用端负责被调用端引数的压栈和出栈。参数可变的函数采用的是这种调用形式。

为什么这种函数要采用不同于前面的调用形式呢?那是因为__stdcall调用形式对它没有作用,被调用端根本就无法知道调用端的引数个数,它怎么可能正确工作?所以这种调用方式是必须的,不过由于参数参数可变的函数本身不多,所以用的地方比较少。

对于这两种方式,你可以编制一些简单的程序,然后反汇编,在汇编代码下面你就可以看到实际的区别,很好理解的!

重载函数有很多匹配(match)规则调用。参数为“…”的函数是匹配最低的,这一点在Andrei Alexandrescu的惊才绝艳之作《Modern C++ Design》中就有用到,参看Page34-35,2.7“编译期间侦测可转换性和继承性”。

c语言指针b 和b j,c语言精华 ------ 指针和数组相关推荐

  1. 指针在c语言中的运用,怎么理解C语言中的指针,如何运用?

    恰好我之前写了一系列介绍 C 语言的文章,介绍了什么是指针,以及为什么要使用指针,下面摘录一部分,感兴趣的话,可以点我了解更多. 什么是 C语言指针? 不同的数据类型的主要区别在于占用的存储空间不同. ...

  2. 【C语言重点难点精讲】C语言指针

    文章目录 一:指针入门 二:数组入门 (1)数组的内存空间布局 (2)区分&arr[0]和&arr 三:指针和数组的关系 (1)以指针的形式访问和以数组形式访问 (2)为什么C语言要这 ...

  3. 按照c语言规首字母只能是,C语言--指针 - osc_nbqoh20k的个人空间 - OSCHINA - 中文开源技术交流社区...

    [TOC] #知识内容总结 ##为什么要学习指针? 我们已经学习了如何用数组存放多个相同类型的数据并进行运算,但数组的长度在定义时必须给定以后不能再改变.如果事先无法确定需要处理数据数量,应该如何处理 ...

  4. c语言两个指针变量不可以,2.C语言指针变量

    # 2.C语言指针变量 变量的指针就是变量的地址.存放变量地址的变量是指针变量.即在C语言中,允许用一个变量来存放指针,这种变量称为指针变量.因此,一个指针变量的值就是某个变量的地址或称为某变量的指针 ...

  5. c语言指针的应用实验七,C语言实验程序总结实验七指针.doc

    C语言实验程序总结实验七指针 实验七 指针 一 实验目的 1 了解指针的概念,学会定义和使用指针变量. 2 掌握指针.变量和数组的关系及使用方法. 3 学会使用指向函数的指针变量. 二 实验内容 1 ...

  6. 嵌入式C语言基础知识查漏补缺--内存管理函数指针数组

    内存管理: 堆和栈的理解和区别 局部变量 静态局部变量 全局变量 静态全局变量 作用域 在定义变量的{}之内有效 在定义变量的{}之内有效 整个工程,所有文件 当前文件 生命周期 程序运行至变量定义处 ...

  7. c语言获取指针分配的字节数,c语言指针知识点总结(共6篇).docx

    c语言指针知识点总结(共6篇) C语言指针教学中的知识点分析与总结 摘要:分析指针的基本概念及指针在数组.函数.字符串.动态存储分配等方面的应用,提出指针教学过程中易混淆概念及注意事项,对初学者深入理 ...

  8. char类型怎么输入 c语言_还没搞懂C语言指针?这里有最详细的纯干货讲解(附代码)...

    21ic综合自网络信息 指针对于C来说太重要.然而,想要全面理解指针,除了要对C语言有熟练的掌握外,还要有计算机硬件以及操作系统等方方面面的基本知识.所以本文尽可能的通过一篇文章完全讲解指针. 为什么 ...

  9. 【C 语言】结构体 ( 结构体中嵌套二级指针 | 为 结构体内的二级指针成员 分配内存 | 释放 结构体内的二级指针成员 内存 )

    文章目录 一.结构体中嵌套二级指针 1.结构体中嵌套二级指针 类型声明 2.为 结构体内的二级指针成员 分配内存 3.释放 结构体内的二级指针成员 内存 二.完整代码示例 一.结构体中嵌套二级指针 1 ...

最新文章

  1. Oracle 11g dataguard三种模式以及实时查询(Real-time query)功能设置
  2. Android 使用GridView+仿微信图片上传功能(附源代码)
  3. windows 10配置VS+MPI编程环境
  4. jzoj6801-NOIP2020.9.19模拟patrick【树状数组】
  5. [转]完美解决IE(IE6/IE7/IE8)不兼容HTML5标签的方法
  6. mysql Error_code: 1593
  7. G-Sensor 8452驱动及相关
  8. Object.defineProperty 接口浏览器实现的bug.和疑惑
  9. Backbox Linux简介与配置内网IP
  10. 获取url中带的参数
  11. 线性表——链表(含代码)
  12. [RK3288][Android6.0] 调试笔记 --- 开机提示mmc rescan错误
  13. 安全文章研读:深度学习对抗攻击防御策略的一些实现
  14. chr计算机语言,Chr,ChrW函数
  15. 美国生活——考取实习驾照(Permit)
  16. float转int 四舍五入问题
  17. 单词数 HDU - 2072
  18. 5.0 数据库完整性详解(PRIMARY KEY、REFERENCES、CHECK、CONSTRAINT、DOMAIN、TRIGGER)
  19. 流量为王时代,APP推广需要解决难题,如何解决分享绑定上下级关系。
  20. 计算机cmd如何设置路由,Windows 系统cmd设置添加静态路由方式

热门文章

  1. 总结了这67个pandas函数,完美解决数据处理,拿来即用!
  2. 阅读笔记:What Uncertainties Do We Need in Bayesian Deep Learning for Computer Vision?
  3. 双非渣本小Android四年磨一剑,秋招大厂(字节、腾讯、B站)面经分享
  4. 垃圾去哪里系列之可回收物
  5. Flutter 集成腾迅 IM Demo 时不同的编译配置的连环坑 2021-12-17
  6. .net精美书籍大检阅
  7. 风丘科技为您提供电动汽车热管理方案
  8. SAP CTRL加逗号,句号后注释和取消注释不起作用的解决方法
  9. 黑马程序员_MongoDB笔记
  10. 移动端地图技术分享 百度高德SDK