数组和指针

数组和指针

数组和指针的统一性C语言的长处之一,用指针可以很方便地访问数组和模拟动态分配的数组。

多数的数组引用都会退化为数组第一个元素的指针,这是C语言中数组和指针等价的基础。因此,数组在C语言中是个二等公民:你永远也不能作为一个整体操作数组(例如:复制或将它们传入数组),因为一旦你提到数组的名字,你所得到的就是一个指针而不是一个数组了。

       关于char a[]=”hello”char *p=”world”的区别:

clip_image002[4]

       其中a为数组预留了6个字符的位置,而p只是提供了一个指针。本质区别为:一旦出现类似a的数组和类似p的指针,以后的操作就会按照不同的方法计算。

       数组和指针的区别

数组是一个由(同一类型的)连续元素组成的预先分配的内存块;

指针是一个对任何位置的(特定类型的)数据元素的引用;

数组自动分配空间,但是不能重分配或改变大小;

指针必须被赋值以指向分配的空间(可能使用malloc),但是可以随意重新赋值(即指向不同的对象),同时除了表示一个内存块的基址之外,还有许多用途;

数组和指针的联系

指针可以模拟数组;

几乎没有所谓数组的东西,下标操作符实际上是个指针操作符;

从更高的层次上看,指向一块内存的指针本质上也就是一个数组;

 

如果a是数组int a[10],那么对a的引用就是“int型的指针,而&a“10int的数组的指针

 

       sizeof操作符如果能够判断出数组的大小,就会返回数组的大小,如果数组的大小未知或者数组已经退化为指针,就不能提供数组的大小了。

 

结构和联合

结构和联合

结构基础知识

       聚合数据类型(aggregate data type)能够同时存储超过一个的单独数据。C提供了两种类型的聚合数据类型:数组和结构

       数组:是相同类型的元素的集合,它的每个元素时通过下标引用或指针间接访问来选择的;

       结构:也是一些值的集合,这些值成为它的成员member,但一个结构的各个成员可能具有不同的类型。

结构声明

       结构声明为:

       struct tag {member-list} variable-list;

       红色部分为可选部分,可选部分不能全部省略,至少要出现两个

 

struct {

       int a;

       char b;

       float c;

}x;

 

struct {

       int a;

       char b;

       float c;

}y[10],*z;

如上面所示,即使两个结构的成员列表完全相同,但是编译器仍然会把它们当做完全不同的两种类型来对待。

结构成员的直接访问

       直接访问通过点操作符.访问。点操作符结构两个操作数,左操作数就是结构变量的名字,右操作数就是需要访问的成员的名字。

结构成员的间接访问

对于结构

struct data {

       int a;

       char b;

       float c;

};

那么struct data *x,我们应该使用(*x).a来访问a,但是这样写太繁琐。C语言提供了一种更方便的操作符,即箭头操作符。使用x->a即可访问a

结构的自引用

       在一个结构内部包含一个类型为该结构本身的成员是非法的clip_image001,比如:

struct data {

       int a;

       char b;

       float c;

       struct data x;

};

其中的struct data会无限递归。我们可以通过包含一个该结构的指针来解决

struct data {

       int a;

       char b;

       float c;

       struct data *x;

};

正确的原因是,编译器在结构的长度确定之前就已经知道了指针的长度,所以这种类型的自引用是合法的。

如果你觉得一个结构内部包含一个指向该结构本身的指针有些奇怪,请记住它事实上所指向的是同一种类型的不同结构,比如更高级的数据结构,链表和树,都是采用这种技巧实现的。

警惕陷阱

typedef struct {

       int a;

       char b;

       float c;

       DATA *x;

}DATA;

上面这个结构是失败的,因为类型名DATA直到声明的末尾才定义,所以在结构声明的内部它尚未定义。解决方法为定义一个结构标签:

typedef struct DATA_TAG {

       int a;

       char b;

       float c;

       struct DATA_TAG *x;

}DATA;

不完整的声明

       当两个结构互相引用时,肯定存在一些依赖的结构。此时应该首先声明哪一个呢?我们可以使用不完整声明(incomplete declaration)来解决这个问题,它声明一个作为结构标签的标识符。然后,我们可以把这个标签用在不需要知道这个结构的长度的声明中,如声明指向这个结构的指针。接下来的声明把这个标签与成员列表联系在一起。例如:

struct B;

 

struct A {

       struct B *partner;

       int a;

       float b;

}

 

struct B {

       struct A *partner;

       int a;

       float b;

}

访问结构成员

       比如对结构:

struct data {

       int a;

       char b;

       float c;

       struct data *x;

};

我们定义一个struct data m

pa = &m

pi = &(m.a);

我们可以知道papi的值其实是相同的,因为pa是整个结构的地址,而pia的地址,而a是结构的第一个元素,所以值相同。但是两个是不等同的,因为pa是指向结构的指针,而pi是指向int的指针。可以通过强制类型转换pi = (int *)pa来令两者相等,但是有很大危险性。

作为函数参数的结构

       当把结构作为参数产地给函数时,最好使用结构的指针,而不是结构本身。比如:

struct data {

       int a;

       char b;

       float c;

       struct data *x;

}DATA;

 

l  void test(DATA x);   这种方法是不提倡的;复制DATA,如果DATA结构十分庞大,会占用很大的空间;

l  void test(DATA *x);   这种方法是提倡的clip_image002只是传递一个指针。

l  void test(register DATA const*x);  这种方法是大力提倡的clip_image002[1]clip_image002[2]clip_image002[3],这种模式可以提高速度,并且可以防止指针被修改。

 

什么时候应该向函数传递一个结构而不是指针呢?很少有这种情况。只有当一个结构特别的小(长度和指针相同或更小)时,结构传递方案的效率才不会输给指针传递方案。但是对于绝大多数的结构,传递指针显然效率更高。如果你希望修改结构的任何成员,也应该使用指针传递方案。

位段

       位段的声明和结构类似,但它的成员时一个或多个位的字段,这些不同长度的字段实际上存储于一个或多个整型变量中

       位段的声明和任何普通的结构成员声明相同,但有两个例外:

l  首先,位段成员必须声明为intsigned intunsigned int类型;

l  其次,在成员们的后面是一个冒号和一个整数,这个整数指定该位段所占用的位的数目。

 

Mark:用signedunsigned整数显式地声明位段是个好主意,因为int在不同的机器上解释可能不同,指明了signedunsigned类型有利于移植。

       使用位段的好理由是,位段能够把长度为奇数的数据包装在一起,节省存储空间;使用位段还可以很方便地访问一个整型值的部分内容。

联合

       联合的所有成员引用的是内存中的相同位置。当你想在不同的时刻把不同的东西存储于同一个位置时,就可以使用联合。

clip_image004

       上述联合在我的机器上共占用8个字节。

l  如果成员f被使用,这个字就作为浮点型访问;

l  如果成员i被使用,这个字就作为整型值访问;

l  如果成员d被使用,这个字就作为双精度浮点型访问;

如果联合的各个成员具有不同的长度,联合的长度就是它最长成员的长度。比如,上述的联合fid的联合长度就应该是double类型做占用的字节数。这样的话,联合就足以容纳它最大的成员,但是如果这些成员的长度相差悬殊,当存储长度较短的成员时,浪费的空间是相当可观的。在这种情况下,更好的方法是在联合中存储指向不同成员的指针而不是直接存储成员本身,这样就解决了内存浪费的问题,当它需要使用哪个成员时,就分配正确数量的内存来存储它。

联合的初始化

       联合可以被初始化,但这个初始值必须是第一个成员的类型(如果被赋予其他值,有可能会被强制类型转换),而且它必须位于一对花括号里面。例如:

clip_image006

总结

l  不同的结构声明即使它们的成员列表相同也被认为是不同的类型;

l  结构不能包含类型也是这个结构的成员,但是它的成员可以是一个指向这个结构的指针,这个技巧长用于链式数据结构中;

l  为了声明两个结构,每个结构都包含一个指向对方的指针的成员,我们需要使用不完整的声明来定义一个结构标签名;

l  结构可以作为参数传递给函数,也可以作为返回值从函数返回。但是,向函数传递一个指向结构的指针往往效率更高。在结构指针参数的声明中可以加上const关键字防止函数修改指针所指向的结构。

l  向函数传递结构参数是低效的,最好传递指针。

 

数组

数组

一维数组

       数组名的值是一个指针常量,也就是数组第一个元素的地址。

       注意数组名为指针常量,而不是指针变量,不能改变。

下标引用

       对于已知的数组长度,编译器将不检查它的下标,因为涉及到位置和长度方面的信息,将会影响程序的执行效率。

       2[array]在有些编译器中的含义为*(array + 2)

指针与下标

       下标绝不会比指针更有效率,但指针有时会比下标更有效率

       当然,指针的高效率前提是被正确地使用。

指针的效率

clip_image002

clip_image004

       从上面的一个数组复制到另一个数组的总结为:

l  当你根据某个固定数目的增量在一个数组中移动时,使用指针变量将比使用下标产生更高的代码,当这个增量是1并且机器具有地址自动增量模型时,这点表现更为突出;

l  声明为寄存器变量的指针通常比位于静态内存和堆栈中的指针效率更高(具体提高的幅度取决于你所使用的机器);

l  如果你可以通过测试一些已经初始化并经过调整的内容来判断循环是否应该终止,那么你就不需要使用一个单独的计数器;

对于使用指针或者数组的判断:

Ø  程序的维护是软件产品的主要成本所在,所以要多考虑一下维护方便;

Ø  有些机器在设计时使用了特殊的指令,用于执行数组下标的操作,目的就是为了使这种极为常用的操作更加快速,在这种机器上,编译器将使用这些特殊的指令来实现下标表达式,但编译器并不一定会用这些指令来实现指针表达式,所以在这种机器上,下标表达式可能会比指针更有效率;

Ø  关于使用哪种方式,最主要的还是要先确认程序中到底是那段代码占用了绝大部分的运行时间,然后把精力用在这些代码上,努力改进它们。

数组和指针

 

 

 

 

 

int a[5];       

      

       会直接分配内存空间。

 

 

?

int *b;       

 

并未被初始化为指向任何现有的内存空间,甚至根本不会被初始化。

作为函数参数的数组名

void strcpy(char *buffer, char const *string);

       该函数中const的优点:

Ø  这是一个良好的文档习惯,通过函数原型就可以发现该数据不会被修改,而不用阅读完整的函数定义;

Ø  编译器可以捕捉到任何试图修改该数据的意外错误;

Ø  这类声明允许向函数传递const参数。

声明数组参数

int strlen(char *string);

int strlen(char string[]);

这两个其实含义是一样的,调用函数时实际传递的是一个指针,所以函数的形参实际上是个指针。但是为了使程序员新手更容易上手一些,编译器也接受数组形式的函数形参。

       但是两个定义中,指针的声明更加准确一些

字符数组的初始化

       当用于初始化一个字符数组时,字符串为一个初始化列表,在其他任何地方,表示一个字符串常量

指向数组的指针

       int matrix[3][10];

       int (*p)[10] = matrix;–指向数组的指针

 

作为函数参数的多维数组

void func(int (*mat)[10]);         指向整型数组的指针

void func(int **mat);             指向整型指针的指针

初始化

       多维数组的初始化最好加上花括号,比如:

int a[2][3][4] =

{

{

{},

{1,2,3,4},

{}

},

{

{},

{1,1,1,1},

{}

}

};

原因如下;

l  利于显示数组的结构;

l  对于不完整的初始化列表,相当有用,比如上面的只初始化其中的部分数据,如果不加花括号,要好好数对位置才行;

数组长度自动计算

       要求是只有第一维可以缺省提供,其余的几维必须显示写出。但其实也可以缺省,主要是由于必须要把子维的数组要达到最长要求才行(即子初始化列表中必须有一个完整的形式),否则还是不对。

总结

       在绝大多数表达式中,数组名的值是指向数组第一个元素的指针。这个规则只有两个例外

l  sizeof返回整个数组占用的字节而不是一个指针所占用的字节;

l  单目操作符&返回一个指向数组的指针,而不是一个指向数组第一个元素的指针的指针。

 

指针和数组并不相等:

l  当我们声明一个数组时,它同时也分配了一些内存空间,用于容纳数组元素;

l  当我们声明一个指针时,它只分配了用于容纳指针本身的空间。

 

只要有可能,函数的指针形参都应该声明为const

Python 列表

列表

       列表是一个任意类型的对象的位置相关的有序集合,它没有固定的大小,不像字符串,其大小是可变的,通过对偏移量进行赋值以及其他各种列表的方法进行调用,确实能够修改列表的大小。

       Python的列表与其他语言中的数组有些类似,但是列表要强大得多,其中一个方面就是,列表没有固定类型的约束

       我们可以使用append方法来扩充列表的大小,用pop方法移除给定的一项,用insert插入元素,用remove来移除元素。因为列表时可变的,大多数列表的方法都会改变列表对象,而不是创建一个新的列表。