C语言再学习 -- 数组和指针

861人阅读 评论(0) 收藏 举报
分类:

一、数组

1、数组介绍

数组(array)由一系列类型相同的元素构成。例如:

int arr[10];

float candy[25];

char code[12];

以上数组声明中包括数组元素的数目和元素的类型。数组元素按顺序存储于内存中,通过使用整数索引(或偏移量)来访问。在C中,首元素的索引值为0,因此包含n个元素的数组的末元素的索引为n-1。


2、数组初始化

A、可以使用花括号括起来的一系列数值来初始化数组。数组之间用逗号隔开,在数值和逗号之间可以使用空格符。例如:

int arr[5] = {1, 2, 3, 4, 5};

B、当数值数目少于数组元素数目时,多余的数组元素被初始化为0。也就是说,如果不初始化数组,数组元素和未初始化的普通变量一样,其中存储的是无用的数值,但是如果部分初始化数组,未初始化的元素则被设置为0。例如:

int arr[10] = {1, 2, 3, 4, 5}; 默认后五位未初始化元素为0。

C、如果初始化列表中项目的个数大于数组大小,编译器会毫不留情地认为这是一个错误。你可以省略括号中的数字,从而让编译器自动匹配数组大小和初始化列表中的项目数目。例如:

int arr[] = {1, 2, 3, 4, 5};

需要注意:

当使用空的方括号对数组进行初始化时,编译器会根据列表中的数值数目来确定数组大小。

可通过sizeof计算数组元素数目,例如:

int arr[] = {1, 2, 3, 4, 5}; 

数组元素数目 = sizeof (arr) / sizeof (int);

D、字符串数组初始化,例如:

char code[] = "hello world";

char code[] = {'h' ,'e' ,'l' ,'l' ,'o' ,' ' ,'w' ,'o' ,'r' ,'l' ,'d' ,'\0'};

注意标志结束的空字符,如果没有它,得到的就只是一个字符数组而不是一个字符串。指定数组大小时,一定要确保数组元素比字符串长度至少多1 (多出来的1个元素用于容纳空字符),未被使用的元素均被自动初始化为0。这里的0是char形式的空字符,而不是数字字符0。

E、采用宏定义#define指令创建一个指定数组大小的明显常量 (SIZE),可以在定义数组和设置循环限制时使用这个常量,以后更改数组大小的时候方便处理,例如:

#define SIZE 5

int arr[SIZE];

for (int n = 0; n < SIZE; n++) {....};

F、指定初始化项目(C99)

例如:int arr[6] = {[5] = 2};  //把arr[5]初始化为2   

等于:int arr[6] = {0, 0, 0, 0, 0, 2};

说明:对于通常的初始化,在初始化一个或多个元素后,未经初始化的元素都将被设置为0.

再如:int day[12] = {31, 28, [4] = 31, 30, 31, [1] = 29};

等于:int day[12] = {31, 29, 0, 0, 31, 30, 31, 0, 0, 0, 0, 0};

说明:

第一,如果在一个指定初始化项目后跟有不止一个值,,例如在序列 [4] = 31, 30, 31中这样,则这些数值将用来后续的数组元素初始化。

第二,如果多次对一个元素进行初始化,则最后的一次有效。录入在序列中前面days[1]初始化为28,而后面的指定初始化 [1] = 29覆盖了前面的数值,于是day[1]的数值最终为29.

3、数组赋值

声明完数组后,可以借助数组的索引(即下标)对数组成员进行赋值。C不支持把数组作为一个整体来进行赋值的,也不支持用花括号括起来的列表形式进行赋值 (初始化的时候除外)。可使用循环对元素逐个赋值。

#include <stdio.h>
#define SIZE 5
int main (void)
{
	int n;
	int arr[SIZE] = {1, 2, 3, 4, 5}; /"数组初始化"/
	int code[SIZE];  
	code = arr;   /"不允许"/
	code[SIZE] = arr[SIZE];  /"不正确"/
	code[SIZE] = {1, 2, 3, 4, 5};   /"不起作用"/

	for (n = 0; n < SIZE; n++)   /"循环对元素逐个赋值"/
		arr[n] = 2*n;
	return 0;
}

4、数组边界

使用数组的时候,需要注意数组索引不能超过数组的边界,例如:

int arr[5];

那么在使用数组索引的时候,要确保它的范围在0和4之间,因为编译器不会检测出这种错误,例如:

arr[5] = 3; //没有这个数组元素

arr[6] = 4; //没有这个数组元素

在标准C中并不检查是否使用正确的下标,不检查边界能够让C程序的运行速度更快。当程序运行时,这些语句把数据放在可能由其他数据使用的位置上,因而可能破坏程序的结果甚至使程序崩溃。

5、指定数组大小

直到C99标准出现之前,声明数组时在方括号内只能使用整数常量表达式。整数常量表达式是由整数常量组成的表达式。sizeof表达式被认为是一个整数常量,而(和C++不同)一个const值却不是整数常量。并且该表达式的值必须大于0.

#define SIZE 5
int n = 5;
float a1[5]; 			//可以
float a2[5*2 + 1];		//可以
float a3[sizeof (int) +1]; 	//可以
float a4[-4];			//不可以,数组大小必须大于0
float a5[0];			//不可以,数组大小必须大于0
float a6[2.5];			//不可以,数组大小必须大于0
float a7[(int)2.5];		//可以,把float类型指派为int类型
float a8[n];			//C99之前不允许
float a9[SIZE];			//可以

6、数组函数

使用数组名作为实际参数时,主要的一点是要知道并不是把整个数组传递给函数,而是传递它的地址;因此相应的形式参量是一个指针(数组做形参即指针)。处理数组时,函数必须知道数组的地址和元素的个数。数组地址直接传递给函数,而数组元素的个数信息需要内建于函数内部或被作为独立的参数传递给函数。后者更为通用,因为这种方法可以处理不同大小的数组。

下列4中原型都是等价的:

int sum (int *ar, int n);

int sum (int *, int);

int sum (int ar[], int n);

int sum (int [], int);

定义函数时,名称是不可以省略的:

int sum (int *ar, int n) {...} 等价于

int sum (int ar[], int n) {...}

7、多维数组

A、int arr[5][12]; //5个有12个整数组成的宿主的数组,我们称之为二维数组。

int arr[行][列];

arr[0]是数组,那么它的首元素是arr[0][0],第二个元素为arr[0][1],依此类推其他元素。它包含5行,每行12列。

注意:使用for嵌套循环结构处理二维数组时,“行”为外循环,“列”为内循环,长循环为内部循环,短循环为外部循环,那么“行”应该小于“列”。

B、初始化二维数组

例如:arr[2][3]={{1,2,3},{4,5,6}};

初始化使用了2个数值列表,每个数值列表都用花括号括起来。如果第一个列表有2个数值,则第一行只有前2个元素得到赋值,最后1个元素被默认初始化为0.如果列表中的数值多余3个,则报告错误;而且这些数值不会影响到下一行的赋值。

初始化的时候也可以省略内部的花括号,只保留最外面的一对花括号,例如:

arr[2][3]={1,2,3,4,5,6};

C、二维数组作形参

在传统的C向函数传递多维数组的方法是把数组名(也就是地址)传递给相应类型的指针变量。指针声明需要指定各维的大小(注意第一个方括号是空的,不需要明确指出大小);第一个参量的维数大小通常作为第二个参数来传递,例如:

void display (int arr[][12], int rows);等价于

void display (int (* arr)[4], int rows);

请注意下面的声明是不正确的:

void display (int arr[][], int rows); //错误的声明

编译器会把数组符号转换成指针符号,如果是空括号,这样转换的时候会不知道ar所指向对象的数据大小,编译器将不能正确处理。

也可以如下这样在另一对括号填写大小,但编译器将忽略:

void display (int arr[8][12], int rows);

一般声明N维数组的指针时,除了最左边的方括号可以留空外,其他都需要填写数据,这是因为首方括号表示这是一个指针,而其他方括号描述的是所指向对象的数据类型,例如:

void display (int arr[][5][8][9], int rows); 等价于

void display (int (*arr)[5][8][9], int rows);

8、变长数组(VLA)

int n = 4,m = 5;

int arr[n][m];//一个变长数组 

变长数组有一些限制:

变长数组必须是自动存储类的,这意味着它们必须在函数内部或作为函数参量声明,而且声明时不可以进行初始化。变长数组中的“变”并不表示在创建数组后,可以修改其大小。变长数组的大小在创建后就是保持不变的。“变”的意思是说起维大小可以用变量来指定。

声明一个二维变长数组参数的函数:

int sum (int row, int cols, int arr[row][cols]);

请注意钱两个参量(row和cols)用作数组参量arr的维数。因为arr的声明中使用了row和cols,所以在参量列表中,它们两个的声明需要早于arr。因此,下面的原型是错误的:

int sum (int arr[row][cols], int row, int cols); //顺序不正确

也可以省略函数原型中的名称,但是如果省略名称,则需要使用星号来代替省略的维数:

int sum (int, int, int arr[*][*]);  //省略了维数参量的名称

二、指针

1、指针介绍

一般来讲,指针是一个其数值为地址的变量指针变量的数值表示的是地址,例如:

int n = 5;

int *ptr = &n;   

*ptr = 5;        

这时ptr的值是n的地址,*ptr是存储在地址&n中的数值。

一元运算符&:

假设p是一个变量的名字,那么&p就是该变量的地址,%p是输出地址的占位符,通常以十六进制形式显示值,例如:

printf ("%d, %p\n", p, &p);

说明:如果你的编译器不支持%p,可以使用%u或%lu作为代替。

间接运算符*:

当后面跟一个指针名或地址时,* 给出存储区在被指向地址中的数值.

在 32 位系统下,不管什么样的指针类型,其大小都为 4byte。可以测试一下 sizeof( void *)。


2、指针声明

int *pi;/*pi是指向一个整数变量的指针*/

char *pc;/*pc是指向一个字符变量的指针*/

float *pf;/*pf是指向一个浮点数变量的指针*/

指针声明,需要说明指针所指向的变量的类型,星号(*)表明该变量为一指针。星号(*)和指针之间的空格式可选的,通常程序员在声明中使用空格,而在指向变量时将其省略。

切记:当创建一个指针时,系统只会分配了用来存储指针本身的内存空间,并不会分配用来存储数据的内存空间。因此在使用指针之前,必须给它赋予一个已分配的内存地址。

可以将一个已存在的变量地址赋给指针,或者使用函数malloc()来首先分配内存,例如:

int arr[5];

int *ptr = arr;     或者

int *ptr = (*int)malloc (5*sizeof (int));

扩展部分:

int* b, c, d;

人们很自然地以为这条语句把所有三个变量声明为指向整型的指针,但事实上并非如此。我们被它的形式愚弄了。星号实际上是表达式 *b 的一部分,只对这个标识符有用。b 是一个指针,但其余两个变量只是普通的整型。要声明三个指针,正确的语句如下:

int *b, *c, *d;


3、指针和数组

A、数组标记实际上是一种变相的使用指针的形式,比如,数组名同时也是该数组首元素的地址。

int arr[5];

arr = &arr[0]; //数组名是该数组首元素的地址。

int *p = arr;    //将数组首元素地址,赋值给指针

两者都是常量,因为在程序的运行过程中它们保持不变。可以将它们赋值给指针变量的值,然后进行修改。

B、用指针标识数组的每一个元素,可以得到每一个元素的数值,对指针加1,等久对指针的值加上它指向的对象的字节大小,例如:

int n[10];

n + 2 = &n[2];     /*相同的地址*/

*(n + 2) = n[2];    /*相同的值*/

注意区分 *(n + 2)和*n + 2。间接运算符(*)的优先级高于+,因此后者等价于 *(n) + 2

*(n + 2)    /*n的第3个元素的值*/

*n +2       /*第1个元素的值和2相加*/

4、函数、数组和指针

int sum (int *arr, int n);   

int sum (int arr[], int n);

这里第一个参数把数组地址和数组类型的信息传递给函数,第二个参数把数组中的锇元素个数传递给函数。此外,关于函数参量还需要注意,在函数原型或函数定义的场合中(并且也只有在这两种场合),可以用int *arr代替int arr[]。

无论在任何情况下,形式int* arr都表示arr是指向int的指针。形式int arr[]也可以表示arr是指向int的指针,但只是在声明形式参量时才可以这样使用。使用第二种形式说明了arr不仅指向一个int数值,而且它指向的这个int是一个数组中的元素。

5、指针操作

A、赋值,可以把一个地址赋给指针。通常使用数组名或地址运算符&来进行地址赋值。注意地址应该和指针类型兼容。

B、求值或取值,运算符*可取出指针指向地址中存储的数值。

C、取指针地址,指针变量同其他变量一样具有地址和数值,使用运算符&可以得到存储指针本身的地址。

D、将一个整数加给指针,可以使用+运算符来把一个整数加给一个指针,或者把一个指针加给一个正数。如果相加的结果超出了初始指针指向的数组的范围,那么这个结果不确定。

E、增加指针的值,可以通过一般的加法或增量运算符来增加一个指针的值。

F、从指针中减去一个整数,可以使用-运算符来从指针中减去一个整数。

G、减小指针的值,指针做减量运算。

H、求差值,可以求出两个指针间的差值。

I、比较,可以使用关系运算符来比较两个指针的值,前提是两个指针具有相同的类型。

#include <stdio.h>
int main (void)
{
	int arr[5] = {100, 200, 300, 400, 500};
	int *ptr1, *ptr2, *ptr3;
	ptr1 = arr;
	ptr2 = &arr[2];
	printf ("ptr1 = %p, *ptr1 = %d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
	ptr3 = ptr1 +4;
	printf ("ptr1 +4 = %p, *(ptr1 + 3) = %d\n", ptr1 + 4, *(ptr1 + 3));
	ptr1++;
	printf ("ptr1 = %p, *ptr1 = %d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
	ptr2--;
	printf ("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2);
	--ptr1;
	++ptr2;
	printf ("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);
	printf ("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %d\n", ptr2, ptr1, ptr2 - ptr1);
	printf ("ptr3 = %p, ptr3 - 2 = %p\n", ptr3, ptr3 - 2);
	return 0;
}
输出结果:
ptr1 = 0xbf89c5f0, *ptr1 = 100, &ptr1 = 0xbf89c604
ptr1 +4 = 0xbf89c600, *(ptr1 + 3) = 400
ptr1 = 0xbf89c5f4, *ptr1 = 200, &ptr1 = 0xbf89c604
ptr2 = 0xbf89c5f4, *ptr2 = 200, &ptr2 = 0xbf89c608
ptr1 = 0xbf89c5f0, ptr2 = 0xbf89c5f8
ptr2 = 0xbf89c5f8, ptr1 = 0xbf89c5f0, ptr2 - ptr1 = 2
ptr3 = 0xbf89c600, ptr3 - 2 = 0xbf89c5f8

一些合法和非法的语句:

int arr[5];

int *ptr1, ptr2;

合法:                     非法:

ptr++;                     arr++;

ptr2 = ptr1 + 2;       ptr2 = ptr2 + ptr1;

ptr2 = arr + 1;         ptr2 = arr * ptr1;

说明只有指针变量,才可以使用ptr++这样的表达式。

6、const修饰指针、数组

const定义的变量具有只读性,const修饰的只读变量必须在定义的时候初始化。

A、修饰数组

定义或说明一个只读数组可采用如下格式:
int const a[5]={1, 2, 3, 4, 5};或
const int a[5]={1, 2, 3, 4, 5};

B、修饰指针

这里给出一个记忆和理解的方法:
先忽略类型名(编译器解析的时候也是忽略类型名),我们看 const 离哪个近。“近水楼先得月”,离谁近就修饰谁。

int arr[5];
const int *p = arr; //const 修饰*p,p 是指针,可变; *p 是指针指向的对象,不可变。
int const *p = arr; //const 修饰*p,p 是指针, 可变;*p 是指针指向的对象,不可变。
int *const p = arr; //const 修饰 p, p 是指针,不可变; p 指向的对象可变。
const int *const p= arr; //前一个 const 修饰*p,后一个 const 修饰 p,指针 p 和 p 指向的对象都不可变。

7、指针数组和数组指针

指针数组:首先它是一个数组,数组的元素都是指针,例如:int *ptr1[10];

数组指针:首先它是一个指针,它指向一个数组,例如:int (*ptr2)[10];

这里需要明白一个符号之间优先级的问题,"[ ]"的优先级比"*"要高。p1 先与“ []”结合,构成一个数组的定义,数组名为 p1, int *修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含 10 个指向 int 类型数据的指针,即指针数组。

至于 p2 就更好理解了,在这里"( )"的优先级比"[ ]"高,"*"号和 p2 构成一个指针的定义,指针变量名为 p2, int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚 p2 是一个指
针,它指向一个包含 10 个 int 类型数据的数组,即数组指针。


三、指针和数组的区别

指针就是指针,指针变量在 32 位系统下,永远占 4 个 byte,其值为某一个内存的地址。指针可以指向任何地方,但是不是任何地方你都能通过这个指针变量访问到。

数组就是数组,其大小与元素的类型和个数有关。定义数组时必须指定其元素的类型和个数。数组可以存任何类型的数据,但不能存函数。

他们之间没有任何关系!只是他们经常穿着相似的衣服来逗你玩罢了。


0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    友情链接

       

           


        不安分的小宝带你暴走全世界
        脚步从未停止,旅途永无止境

        喜欢户外旅行的,等你来!!


    个人资料
    • 访问:521421次
    • 积分:7247
    • 等级:
    • 排名:第3211名
    • 原创:253篇
    • 转载:139篇
    • 译文:0篇
    • 评论:127条
    博客专栏