数组和指针

数组和指针在一定程度上可以互相使用,但是从本质上来说,他们是有本质的区别的。

X=Y的本质

介绍数组和指针之前,首先说一下X=Y这个简单的赋值语句的本质。

对于一个变量,如X或者Y,他是有两种含义的,一是代表着是X的那一块内存空间,二则达标者X的内容。

对于X=Y,左边的X代表的是X的那一块内存空间,而Y代表着Y的内容,可以理解为:将Y变量的内存空间里面的内容复制到X变量的内存空间里面去。

更加专业的角度来说,赋值符的左边成为左值,右边成为右值。编译器为每一个变量都分配一个地址(左值),这个地址是编译器知道的,而且这个变量和这个地址是永久对应不会改变的。也可以说:对于编译器来说,一旦为变量分配了地址,变量名就是这个地址。存储在这个地址的内容(右值)只有在运行的时候才是可以知道的。

综上所述:变量的地址永远不变,变量的内容在运行时可以改变,我们对变量的修改操作对象其实就是变量的内容。

?

数组和指针的访问方式

每个变量(数组和指针)的地址都是编译器可知的,因此数组的存储地址编译器是知道的,每次访问第N(常数)个元素的时候编译器可以直接计算出来这个元素的所在地址。而对于指针来说,编译器虽然知道这个指针变量的存储地址,但是他不知道这个指针变量的内容,及指针指向的地址,所以如果要通过指针访问第N个元素,需要在运行的时候获得指针指向的地址,然后通过计算获得该元素的最终地址。

举例:数组的访问方式

Char a[5] ="abcd";

Char c=a[i];

假如,编译器为数组a分配了5个字节的内存,首地址为9980。

运行时:

步骤1:取i的值,并将其和9980相加。

步骤2:取(9980+i)的内容。

两步读内存过程

举例:指针的访问方式

char p;

Char c=p[i];

假定,编译器为指针变量p分配了地址4624,地址大小为四个字节(32位)。编译器并不知道这个指针指向哪里,因为他不知道里面存储的内容,需要在运行时才知道。

运行时

步骤1:取变量p的值,即地址为4624的内容,假定取出来后为9980.

步骤2:取i的值,将其与9980相加。

步骤3:取地址为(9980+i)的内容。

三步读内存过程。

数组和指针的区别

指针

数组

保存数据的地址

直接保存数据

间接访问数据。首先取指针内容,然后将其作为地址再去取数据。

直接访问数据。根据数组的地址。

通常用于动态数据结构-堆,当然,指向数组也可以。

存储固定数目且数据类型相同的数据,一般是放在栈里面作为临时变量。

使用malloc、free为其分配内存。

隐式分配、删除

指向匿名数据

本身就是数据,数组名就是数据的名字

数组和指针在定义时都可以用字符串常量对其初始化,但是底层机制不同。

定义指针时,编译器为指针分配空间,创建的字符串常量是只读的,并将其地址存放在指针变量中,因此不能对其进行修改。

而通过字符串常量初始化数组,会将每个字符存入到编译器为数组分配的内存空间里面,也就是可以修改的。

数组和指针的使用

声明:

  • extern char a[];不能修改为指针的形式。
  • 定义:char a[10];不能修改为指针形式。
  • 函数参数:f(char a[]);数组形式或指针形式都可以

表达式中的使用:

  • 如c=a[i];数组或指针形式都可以。

数组和指针的可交换性

什么时候数组和指针是相同的,C语言标准中做出了一下的说明:

  • 表达式中的数组名被编译器当作一个指向该数组第一个元素的指针。
  • C语言把下标作为指针的偏移量。
  • 作为函数参数的数组名等同于指针。

可交换性的总结:

  • 用a[i]的形式访问数组,将被编译器解释为(a+i)这样的指针访问。
  • 指针始终为指针,他不可能改写为数组。下标形式访问指针,也是说明指针指向的是一个数组,编译器也将其解释为*(a+i)这样的形式。
  • 在将数组声明为函数的参数时,可以将其看成一个指针,他也具有一个内存空间,也可以修改(数组名没有内存空间也不能修改)。
  • 因此,定义函数参数时,指针和数组对边一起来说是一样的,函数内部其实都是一个指针。
    在其他情况,定义和声明必须匹配。定义一个数组,那在其他文件对其声明也必须声明为数组。

Published: 07 Jul 2013

编译和链接-介绍

编译和连接

过程:预编译,编译,汇编,连接

预编译

将.c文件和.h头文件被编译为一个.i文件,是一个编译单元。


预编译主要是处理一些预编译指令,如将#include的文件放入.c文件等,主要的处理规则:

    <li>#define替换,展开所有宏定义。</li>
    
    <li>处理条件编译指令,如#if等。</li>
    
    <li>处理#include指令,将被包含的文件插入到指令位置(递归,.h文件中也可有include)。</li>
    
    <li>删除注释</li>
    
    <li>添加行号和文件名标识,以便编译器调试。</li>
    

编译和汇编


通过词法分析、语法分析、语义分析及优化产生相应的汇编代码文件。


词法分析:扫描器扫描源代码,根据有限状态机等算法可以识别代码每个单词或符号的物理含义。

语法分析:根据词法分析获得的代码的物理记号,产生语法树。语法书是以表达式为节点的树。进完成表达式语法层面的分析,并不了解这个语句的真实含义。

语义分析:语义分析分析每个表达式的类型。

汇编将汇编代码文件转化为机器可执行的指令(机器码),汇编过程就是一个简单的翻译器。汇编输出的文件叫目标文件(Object File),他是一个通向可执行文件的里程碑。


编译器会将整个语法树转为中间代码,它是语法书的顺序表示。它与机器和运行环境无关,比较常见的有三地址码: x=y op z。根据三地址码可以进行一些优化,比如将常数运算直接求出结果。


基于中间代码,将编译器分为前端和后端。前端生成与机器无关的中间代码,后端根据中间代码生成目标机器代码,后端与机器和运行环境有关。


中间代码通过代码生成器基于具体的机器生成目标代码。这时会出现问题:变量的地址没有确定。如果变量的定义在同一个编译单元,编译器可以为其分配空间,但是如果变量在其他编译模块呢?

这时候就需要链接!


综上所述,编译器是将源代码编译为一个未链接的目标文件,连接器最终将不同的目标文件链接为可执行文件。

链接


当一个代码项目非常大时,我们希望将其分割成不同模块,对这些模块进行编译后,然后通过连接将其拼装。连接主要是把一些指令对其符号地址的引用加以修正。链接过程主要包括:地址和空间分配,符合决议和重定位。

连接可以看成以下过程:当一个模块使用另一个模块的函数foo()时,运行调用时需要知道确切的地址,但在编译阶段因为foo不在该编译单元,所以编译时无法知道确切地址,因此编译器先留出位置,等到连接阶段重新修正为真正的地址。(全局变量机制相同)

Published: 07 Jul 2013

整数表示-signed和unsigned

本文介绍了C语言中int数据类型的存储,以及signed和unsigned类型的区别和联系。

Published: 20 Jun 2013

1的数目

题目:给定一个十进制正整数N,写下从1开始到N的所有整数,数一下其中出现所有1的个数。

例如N=12,写下1,2,3,4,。。。,12,1的个数为5。

问题一

写出函数f(N),表示对于给定的N,计算1到N之间的"1"的个数。

解法一

最直接的解法:从1遍历到N,将其每一个数中含有"1"的个数加起来。

此方法复杂度太高,为O(nlogn)。每个数计算此数的每一个位的数。

解法二

此方法是《编程之美》给的解法。

计算每一位的出现1的个数,如2345,分别计算千位、百位、十位、个位上可能出现1的个数。

以百位为例,可以找到以下规律:

  • 当百位上的数字为0,如12023,百位上出现1的个数由更高位12决定。每一千个数0~999或1000~1999中,百位数出现1的数量为100次。因此,出现的1的个数为更高位(12)*当前位数(100)=1200.
  • 当百位上的数字为1,如12123,百位上出现1的个数由更高位(12)和低位(23)决定。除了上一情况出现的个数,还有12100~12123中百位数中1出现的个数。即:出现的1的个数为更高位(12)*当前位数(100)+低位(23)+1.
  • 当百位上出现的数字>1,如12223和第一种情况类似,但是要将高位+1(因为12***中的那一个也要考虑进去)。即:出现的1的个数为更高位+1(12+1)*当前位数(100)=1300.

[cpp]

int oneCount(int n){

int iCount=0;

    int iFactor=1;

    int iLow=0,iCurrent=0,iHigh=0;

    while(n/iFactor!=0){

        iLow=n%iFactor;

        iCurrent=(n/iFactor)%10;

        iHigh=n/(iFactor*10);

        switch(iCurrent){

        case 0:

            iCount+=iHigh*iFactor;

            break;

        case 1:

            iCount+=(iHigh*iFactor+iLow+1);

            break;

        default:

            iCount+=(iHigh+1)*iFactor;

            break;

        }

        iFactor*=10;

    }

    return iCount;

}

[/cpp]

解法3

此方法是我自己想的解法。

分析发现以下规律:

f(9)=1,f(99)=20,f(999)=300,f(9999)=4000。及f(10^b-1)=b*10^(b-1)。(定理一)

为什么会这样呢。因为对于每个位,出现1的概率为1/10,然后乘以位的数量。如999,个位出现1的概率为1/10,则个位出现100个1,同理,十位100个,百位100个。即300个。

解法:

  1. 将数字2345分解为2000+300+40+5,分别计算他们中含有的1的数量。
  2. 对于每个数字,如2000,我们知道(0~999)中1的个数为300,1000~1999中除了千位1出现的个数同样为300.即2*300.
  3. 在以上基础上,考虑千位出现1的个数,分为以下情况:
    1. 如果千位为0,不会出现。
    2. 如果千位为1(如1234),则千位出现的数量为千位后面的数+1,即234+1.
    3. 如果千位>1,则千位出现1的数量为1000.

[cpp]

int bitCount(int n){

int b=0;

int flag=n/(pow((float)10,b));

while(flag!=0){

    b++;

    int t=pow((float)10,b);

    flag=n/t;

}

return b;

}

int oneCount2(int n){

int count=0;

int b=bitCount(n);

for(int i=b;i&gt;0;i--){

    int tennow=pow((float)10,i-1);

    int tCur=n/tennow;

    n%=tennow;

    if(tCur==0){

        continue;

    }

    if(tCur==1){

        count+=n+1;

    }

    else{

        count+=tennow;

    }

    count+=(tCur*((tennow/10)*(i-1)));

}

return count;

}

[/cpp]

问题二:

满足条件f(N)=N的最大的N是多少?

根据定理一可知,当N的位数b<=10时,N=10^b-1,f(N)=b*10^(b-1),则N>f(N)。

而b=11时,f(N)>N。

当n>10^(11-1)时,能否证明不可能出现f(n)<n?如果可以证明,则在b=10~11中肯定能找到一个最大的N,使f(N)=N.

我证明不太出来,但是大体感觉是不能出现。因此,最大的N应该在b=10~11。

因此,令N=10^11-1=99 999 999 999,使n从N递减,检查是否f(n)=n,第一个就是最大的。

思想:找到N的范围,然后使用穷举法。不能直接使用穷举法,第一,时间复杂度太大。第二,因为要求最大的,因此没有一个终止条件。 当求最大的某个数的时候,首先要去判断一个上界,然后可以从上界朝下去穷举。

扩展问题:

对于其他进制,如二进制,f(1)的计算方法。

类比于问题一中的解法二和解法三,可以有两种解法。

解法一(对应于问题一解法二)

计算每个位出现1的次数。对于每一位:

根据当前位b的值(0或1),如果当前的值为0,则根据此位之前的数乘以当前位数(2^b)。

若当前值为1,此位之前的数乘以当前位数+此位之后的数+1.

解法二(对应问题一中解法三)

分析发现f(1)=1,f(11)=4,f(1…1<b个>)=b*2^(b-1)

将10101分为10000+100+1分别计算1的个数。

f(10101)=f(10000)+count(10000后面的数中该位1的数量)+f(100)+count(100后该位1的数量)+f(1)

根据定义可知f(1000)=f(1000-1)+1;

综上所述:

f(10101)= f(10000)+(101+1) +f(100)+(1+1)+f(1)= f(9999)+1+(101+1) +f(99)+1+(1+1)+f(1)

Published: 06 Jun 2013

Const深入解析

Const关键词的引入主要是为了区分什么可以修改,什么不可以修改。同时,它可以保证代码的安全性并提供访问控制保证。C中使用#define进行值替换,const可以提供这种功能,但是不仅仅限于如此简单的应用。它还可以通过对指针、函数参数、返回值、类和成员函数的修饰,对其进行控制和说明。尽可能使用const是一种良好的代码风格,因为它可以保证减少bug。

我曾经写过一篇简单介绍const用法的文章,这里将更加详细的介绍和解析。主要参考书《Thinking in C++》(英文版)。

1 const基础用法和概念

C语言中,使用#define BUFSIZE 100进行值替换,预处理器将所有的BUFSIZE简单的替换成了100,但没有类型信息往往会出现隐藏的bug。

在C++中,可使用const int bufsize=100;虽然bufsize是一个变量的形式,但是其并没有分配内存,而且编译器在编译阶段可获知bufsize的值并且进行constant folding(就是bufsize+10,编译器直接计算完成)。也可以char buf[bufsize];

Const出了修饰常量以外,它还可以修饰变量。当一个变量通过运行时数据初始化以后,你知道这个变量就不会改变。那也可以使用const对其修饰,以后也就不能改变它的值。这是一个好的变成习惯,而不是要求。

编译器处理const的方式

关键词:内链接,不分配内存

Const变量默认是内链接的,也就是说只能被本编译单元使用。因此,const变量一般定义在.h文件中,需要使用它的编译单元通过#include使用。

对于const常量,编译器一般不为其分配空间,只在符号表(symbol table)中记录常量的定义。即编译器可以知道常量的值,因而能通过它定义数组长度。但是,当const变量显式定义外链接extern时,或者对其使用取地址操作,编译器就会为其分配一个内存空间。除此之外,当const修饰复杂的数据结构时,如数组,编译器不会将其放入符号表,也为其分配内存空间。

不放入符号表也就代表着编译阶段不能使用它的值,如

const int i[] = { 1, 2, 3, 4 };

//! float f[i[3]]; // Illegal

将const变量显式定义为外链接的方法如下:

extern const int x = 1;

在需要使用该const的地方进行声明:

extern const int x;

上面通过初始化区分定义和声明。

Const常量为什么要使用初始化区分定义和声明? 答:在传统的全局变量中,int x;既代表声明,有代表定义,此处分配内存,默认为外连接。在需要使用x的编译单元,通过extern const int x;作为定义,表示在其他编译单元有x的定义,即此处不会为其分配内存。而对于const常量,要使用extern显式定义以此表示外联结,无法通过使用传统的方法区别定义和声明,因此使用初始化区分。

2 const修饰指针

Const修饰指针,可以表示指针指向的地址存储的是常量,也可以表示该指针不能被修改。

指针指向的是const,有两种表示方法:(1) const int* u;(2) int const* v;

Const指针的表示方法为:

int d = 1;

int* const w = &d;

指向const的指针可以指向非const对象,只要保证不通过指针改变该对象。但是,不能将const对象的地址复制给指向非const的指针,因为可能通过这个指针对const对象进行修改。

3修饰函数参数和返回值

用const可以修饰函数参数和返回值,如果按值传递的话,其实const没有啥意义,因为它仅表示传的参数在函数内不能被修改,但是参数即使修改了对函数外也没有任何影响,所以没有意义。但是如果通过按地址传递,const就有很大的作用。

注:在书中有一小节对C++中返回值为对象置为const进行了介绍,但是我觉得有歧义,可能是C++中原本就存在的一些限制和兼容性想法。总之,我没太看懂,就不写了。

这里主要介绍const修饰按地址传递(指针、引用)的函数参数和返回值,也是平时最经常应用的情况。

在按地址传递的函数中,应该尽可能将其声明为const。

在C++中,参数传递的第一选择是使用const引用,尤其是参数为自定义类型。使用const引用有很多好处,比如不用复制对象,使用者的使用格式和按值传递相同。使用const引用还有一个好处就是可以将临时对象作为函数参数,因为临时对象是const类型的。

4 const和Class

当用const修饰和类有关的元素时,他也提供了一些功能。如const成员变量,static const成员变量,const成员函数以及const对象等。

4.1 const成员变量

这里翻译为const成员变量而不是const成员常量,原因是const修饰的成员变量并不是固定的,对于不同的对象可有不同的值,const修饰表示该变量在初始化以后在对象的生命周期内不能被修改。

当const修饰成员变量时,const的使用机制和C中的const相似(C中很少使用const,但是确实存在这个关键词),会为const变量分配内存,确切的说,是为每个对象都分配一个内存存放const成员变量。

const成员变量的初始化和其他成员变量不同,因为在构造函数体中,const应该已经被初始化而且就不能再被修改。因此,const成员变量使用初始化列表初始化,表示在构造函数被执行之前就进行了初始化。

4.2 编译时const常量

Const成员变量并不能作为一个常量让编译器直接使用,因为它仅表示对象生命周期内不能修改,但是每个对象都可以有不同的值。如果想实现编译时常量,需要使用static关键词。

Static const常量必须在声明时初始化,注意,也只有static const常量可以这样初始化,其他变量都不可以。

如:

class StringStack {

static const int size = 100;

const string* stack[size];

};

4.3 const对象和const成员函数

对于const对象,编译器保证在对象的生命周期没有成员变量被修改。编译器可以保证没有public的成员变量被修改,但是如何保证调用的成员函数没有修改成员变量呢。这里就需要用到const成员函数,当用const修饰成员函数时,我们告诉编译器该函数可被const对象调用,而且该函数不会修改对象的成员变量。没有声明为const的成员函数默认为会修改对象,不能被const对象调用。(即使函数没有修改对象,只要没有声明为const,也不能被const对象调用,因此如果函数没有修改成员变量就应该将其声明为const)。

任何被声明为const的成员函数都不能修改对象,编译器要求它不能修改成员变量,也不能调用非const的成员函数。

Const修饰成员函数必须在声明和定义中都显式修饰:

class X {

int i;

public:

X(int ii);

int f() const;

};

X::X(int ii) : i(ii) {}

int X::f() const { return i; }

const成员函数能被const或者非const对象访问,从这个角度,我们也应该尽量将成员函数声明为const。

Published: 04 Jun 2013

类成员变量初始化

1 C++成员变量初始化方式

一般变量 int 初始化列表、构造函数内
静态成员变量 static int 类外初始化
常量 const int 初始化列表
静态常量 static const int 类定义时
成员对象 Class 初始化器
成员对象指针 Class * 初始化列表、构造函数内,和一般变量相同

Example:

[cpp]

include <iostream>

include <string>

using namespace std;

class Test

{

private:

int a;

static int b;

const int c;

static const int d=4;

public:

Test():c(3) //a(1)或者在初始化列表里初始化

{

a=1;

}

};

int Test::b=2;

void main()

{

Test t;

}

[/cpp]

2 C++类成员初始化顺序

  1. 初始化列表优先于构造函数(初始化列表的初始化顺序与声明顺序相同,与初始化列表顺序无关)
  2. 静态成员优先于实例变量
  3. 父类成员变量优先于子类成员变量
  4. 父类构造函数优先于成员对象构造函数优先于子类构造函数

Example:

[cpp]

using namespace std;

class Test

{

public:

Test(string n)

{

    cout&lt;&lt;n&lt;&lt;endl;

}

};

class Base

{

public:

static Test* a;

Test* b;

Test* c;

Base():b(new Test(&quot;b&quot;))

{

    c=new Test(&quot;c&quot;);

}

virtual ~Base()

{

    if(a) delete a;//似乎是很欠妥的做法

    if(b) delete b;

    if(c) delete c;

}

};

Test* Base::a=new Test("a");

class Derived:Base

{

public:

static Test* da;

Test* db;

Test dc;

Derived():dc(&quot;dc&quot;)

{

    db=new Test(&quot;db&quot;);

}

~Derived()

{

    if(da) delete da;//似乎是很欠妥的做法

    if(db) delete db;

}

};

Test* Derived::da=new Test("da");

void main()

{

Derived d;

} ///:~

[/cpp]

结果:

分析:首先根据顺序初始化static成员:a,da,初始化Derived首先初始化基类b,c,然后初始化成员对象dc(初始化器),最后初始化一般的变量db。

3 Java中成员变量初始化顺序

类成员变量优先于类的构造函数

静态成员优先于实例变量

父类成员变量优先于子类成员变量

父类构造函数优先于子类构造函数。

Java不精,就不举例了。

Reference:http://blog.csdn.net/jhj735412/article/details/7520528

Published: 01 Jun 2013

散列表(Hash表)

散列表使用直接寻址的思想,根据key计算地址,然后作为下表去操作元素。最简单的散列表是直接寻址表,它和数组类似,对于每个可能的key分配一个表项,但这种方法要求较大的存储空间为每一个key保留一个位置。因此,散列表往往将多个key映射到同一个表项,这个过程中就牵扯到了如何映射,以及如何解决冲突的问题。而对于散列表的实现,最重要的就是散列函数、冲突解决方法。<!--more-->

直接寻址表和散列表

当关键词的全域比较小时,即所有关键词的个数不大,可采用直接寻址表为每一个关键词预留一个位置,对于每个实际的关键词,只想一个元素。而对于没有出现的关键词,设置为NULL。这种方法要求较大的存储空间,但是方法简单,对于查找、插入和删除操作,也只需要O(1)的时间。

但是key的全域一般较大,而实际的key可能较小,为每个key都保留一个位置不太实际。因此,可以创建大小为m的散列表,使用散列函数h:U->{1,2,…m-1},将所有的key都映射到m个散列表中,从而降低了空间开销。h(key)就是元素的存储位置。

散列函数

散列函数又称hash函数,是将key映射到地址的函数。

好的散列函数应该尽量满足简单一致散列的假设:每个关键词都等可能的散列到m个表项中的任意一个,与其他关键词已被散列到哪个表项无关。

在使用散列函数前,需要将非自然数的关键词解释为自然数。

常用的散列函数:

  • 除法散列法:h(k)=k %m(最常用)
  • 乘法散列法:h(k)=(向下取整)m(kA%1)
  • 直接定址法:即直接寻址
  • 数字分析法:
  • 取平方法
  • 随机数法:随机函数

解决冲突的方法

因为将|U|个关键字映射到大小为m的散列表中,肯定会出现两个关键字映射到同一表项。当现实中出现这种情况时,成为冲突。如何解决冲突,是散列表的另一个重要问题。

1链接法

散列表中存放指针,指向一个链表。将映射到这个散列表项的所有元素都用链表存起来。

2 开放寻址法

在开放寻址法中,所有的元素都存放在散列表中,当出现冲突的时候,就根据探查序列去寻找下一个地址,直至找到为空的地址。

开放寻址的散列函数有两个参数,第一个是关键字,第二个是探查号。h:U*{0,1,…m-1}->{0,1,…,m-1}

对每一个关键词k,探查序列为:<h(k,0), h(k,1), …,h(k,m-1)>

插入时,初始探查号为0,查看散列表表项是否为空,如果不为空,探查号增加直至找到为空的表项将元素插入。

查找时,初始探查号为0,查看表项是否为要查找的元素,探查号增加直至查找到为空的表项。

删除时,先通过查找找到元素,不能简单的将表项置为空,因为查找时需要使用空作为标识。因此将其置为一个特殊的值,比如为DELETE。

探查序列主要有三种:线性探查,二次探查及双重探查

  1. 线性探查
    线性探查就是当发现这个表项不为空时,就去寻找下一个。当寻找到最后一个表项时,再从头查找。
  2. 二次探查
    1^2, -1^2, 2^2,-2^2,3^2, …, ±k^2,(k<=m/2)称二次探查;
  3. 双重探查
    h(k,i)=(h1(k)+ih2(k))mod m

3 再哈希法

再散列法:Hi=RHi(key), i=1,2,…,k RHi均是不同的散列函数,即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生"聚集",但增加了计算时间。

例题

已知一个线性表(38,25,74,63,52,48),假定采用散列函数h(key) = key%7计算散列地址,并散列存储在散列表A【0....6】中,若采用线性探测方法解决冲突,则在该散列表上进行等概率成功查找的平均查找长度为(C)

A、1.5 B、1.7?? C、2.0? D、2.3

下标 0 1 2 3 4 5 6
元素 63 48 38 25 74 52
查找次数 1 3 1 1 2 4

(1+3+1+1+2+4)/6=2

Published: 01 Jun 2013

复杂指针声明解析

指针是C和C++非常常用的东东,但是其复杂性也是非常突出的特点。比如指针的定义,指针可以指向各种元素,比如基本变量、对象、数组或者函数,同时函数返回也可以使指针,数组的元素也可以使指针。因此,有时候指针的定义就很晦涩。一般来说,非常复杂的指针定义在真实的编码时尽量减少使用,但是如果遇到了非常复杂的指针声明,我们也得明白、看得懂。这在一些场合比如找工作,是非常重要的。

如何定义一个指针,或者对于一个复杂定义的指针,我们如何去理解。在《thinking in C++》作者提出了一个方法,我觉得是最易懂也是很有用的。

Start in the middle and work your way out:从中间开始找到声明的指针名,然后通过右、左、右、左的方式审查完所有的元素。

下面举一些例子:

void (*funcPtr)();

这个指针可以这样理解:先从中间开始,找到funcPtr是指针名(funPtr是一个)。然后开始找到我们出去的路:funcPtr右边是括号,因此去左边,发现左边是*,则说明这是一个指针(指针,指向…),朝右看是左右括号并且没有参数列表(一个没有参数的函数),再朝左看,发现是一个void(这个函数返回值为void)。通过以上过程,我们就理解了funcPtr是一个指针,指向一个没有参数的函数,这个函数的返回值为void。

如果没有括号:void *funcPtr();

这是一个普通的函数声明。如果使用我们的方法来分析则是:找到funcPtr朝右,说明funcPtr是一个函数,朝左void,这个函数返回void。说明这个方法不仅适合指针的声明,也适合其他元素的声明。

下面是数组指针和指针数组的例子:

float (**def)[10];

找到def,右看为括号,左看**说明是一个指针A的指针。右看[10],说明指针A指向一个长度为10的数组,朝左看数组的元素为float。所以,def是一个二级指针,它指向一个指向长度为10元素为float数组的指针。

Double* (*def)[10];

Def是一个指针,它指向一个长度为10的数组,数组里面存放的是double*元素。

Double(*f[10])();

F是一个数组,数组里面存放的是指针,这些指针是指向无参返回void函数的指针。

来一个复杂点的例子:

Typedef double (((*fp)())[10] ) ();

fp a;

首先,fp是一个函数指针,这个函数没有参数,返回的是一个指针,这个指针指向一个长度为10的数组,数组里存放的是指针,这个指针是一个无参返回值为double的函数指针。

英文是:An fp is a pointer to a function that takes no arguments and returns a pointer to an array of 10 pointers to functions that take no arguments and return doubles.

嗯,这局英语真是复合句的典型….

注意,我们将这个坑爹的指针通过typedef定义为fp,后面就可以使用fp定义a。

这么复杂的例子我感觉如果谁在现实的开发过程中用到了,谁就要考虑下你写的代码能不能通过审查,你会不会被老板骂了,不过这么复杂的如果都可以理解的话,其他应该就是小case了。

Published: 05 May 2013