第七章 数组与指针

提示:以下是本篇文章正文内容,下面案例可供参考


只储存单个值的变量有时也称为标量变量(scalar variable)

数组

数组(array) 是按顺序储存的一系列相同类型元素(element) 的集合。元素个数称为数组的长度,元素在数组中的序号称为下标(subscript) ,又叫做索引(indice)偏移量(offset)

程序一般通过下标访问数组元素,数组下标必须是从0开始计数的整数。为提高执行速度,C编译器不会检查数组的下标是否正确,如果访问数组时下标越界可能导致程序异常中断。

float debts[20]; //声明数组
// 方括号[]表明 debts 是一个数组,20 是数组长度,float 是每个元素的类型。

// 最好是在声明数组时使用符号常量来表示数组的长度
#define SIZE 20
float debts[SIZE];

debts[8]=3.14; 
// 通过下标8访问数组第九个元素

数组初始化

未初始化的数组元素和未初始化的普通变量一样,其中储存的值是无意义的。

声明时初始化数组需要将初始化值用 “{ }” 括起来以 “,” 分隔。

// 指定数组长度,从首元素开始按列表中的值初始化。
int powers[7] = { 1, 2, 3, 4, 5 };
/* 如果初始化列表中的值少于指定的数组长度,编译器会把剩余的元素初始化为0。
   如果初始化列表中的值多于指定的数组长度,编译器会将其视为错误。 */

// 不指定数组长度,编译器根据列表中值的个数确定数组长度。
int arr[] = { 1,2,3,4 };  //数组长度 4

// 数组长度计算
int length = (sizeof arr)/(sizeof arr[0])
/* sizeof a :数组总字节数
   sizeof a[0] :数组元素的字节数 */

// 有时需要把数组设置为只读。使用 const 关键字。
const int days[MONTHS] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
#include<stdio.h>
int main(void) {
	int a[4]; //未初始化数组,数组元素的值是无意义的
	for (int i = 0; i < 4; i++) 
		printf("a[%d] = %d\n", i, a[i]);
		
	int b[4] = { 0 }; //初始化所有数组元素为0
	for (int i = 0; i < 4; i++) 
		printf("b[%d] = %d\n", i, b[i]);
	return 0;
}

指定初始化器(C99)

C99 增加了一个新特性:指定初始化器(designated initializer),可以在初始化列表中使用带方括号的下标初始化指定位置的元素。

// 传统初始化,要初始化arr[5],必须先初始化它前面的元素
int arr[6] = {0, 0, 0, 0, 0, 212}; 
// 指定初始化器
int arr[6] = { [5]=212 };  

先将列表中初始化器前面的值按顺序初始化为数组元素,不足的位置初始化为0,再将初始化器指定的位置初始化。 如果初始化器指定位置已经初始化,那么初始化器以及初始化器后面的值将按顺序覆盖已经初始化的数组元素值。

int arr[] = {1,[3]=3,4,5,[0]=6,7,8,[3]=9,10};   //{6,7,8,9,10,5} 

初始化:
[3]=3: {1,0,0,3,4,5}
[0]=6: {6,7,8,3,4,5}
[3]=9: {6,7,8,9,10,5}

多维数组

声明并初始化二维数组

//声明一个二维数组
int arr[3][4];
/* 理解:二维数组相当于数组的数组,可以看成一张二维表,实际上在内存中数组元素是顺序存储的。
   arr是一个内含3个元素的数组,arr[0]是数组首元素。
   arr[0]是一个内含4个元素的数组,arr[0][0]是数组首元素 */

// 初始化二维数组建立在初始化一维数组基础上
int arr[3][4]=
{
    {0, 1, 2, 3},
    {4, 5, 6, 7},
    {8, 9, 10, 11}
};
/* 初始化时也可省略内部的花括号,只保留最外面的一对花括号。
   编译器会从第一行开始初始化,不足的位置初始化为0 */

声明时带初始化的多维数组,可以省略第一个方括号中的长度值,编译器会根据初始化值的个数确定数组的具体长度。

其他多维数组如 int box[10][20][30]
可以把一维数组想象成一行数据,把二维数组想象成数据表,把三维数组组想象成一叠数据表。把box 看作数组的数组。也就是说,box 内含10个元素,每个元素是内含20个元素的数组,这20个数组元素中的每个元素是内含30个元素的数组。


指针简介

取地址运算符 & 用于获取变量在内存中的地址。pooh是一个变量,&pooh就是pooh的内存地址。

什么是指针?指针就是内存地址,指针变量是用来存放内存地址的变量(指针变量和指针通常混用)

间接运算符 *(indirection operator) 又叫 解引用运算符(dereferencing operator) ,用于解引用指针。

int * pi;  
/* 定义一个指向int类型变量的指针变量pi
   * 表明这是一个指针类型,int是pi指向的变量的类型 */

int pooh=5; //定义一个int类型变量pooh

pi=&pooh; 
/* &pooh是取地址操作。&pooh返回pooh在内存中的地址
  将pooh的地址赋值给指针变量pi,则称pi指向pooh */

// 下面两个表达式等价
pooh+=1;
*pi+=1;
/* *pi 对指针pi解引用。解引用是指通过指针,访问或修改指针指向的内存内容。
   *pi 返回的是一个数据对象,它是可修改的左值 */

/* 对指针的一些描述:
   pi的类型:int*,指向int类型(变量)的指针变量。 
   pi指向的类型:int类型 
   pi指向的地址:就是pi的值(一个地址)
   pi指向的变量:pooh
   pi指向的对象:pooh
   pi指向的值:pooh的值 */

* 和指针名之间的空格可有可无。通常声明时使用空格,解引用时省略空格。

数组和指针

内存中的每个字节按顺序编号,称之为内存地址。一个较大对象的地址(如double 类型的变量)通常是该对象第一个字节的地址

指针存储了对象在内存中的第一个字节的地址。当 *pt 要获取指针pt指向的对象的值时,除了知道内存地址外,还要知道返回多少字节?这时指针的类型告诉了计算机应该返回sizeof(type)个字节(type是指针指向的类型),例如 int 型返回4字节,double 型返回8字节。

对指针加1,指针的值将递增 sizeof(type) 个字节(type是指针指向的类型)。例如,对 double 型指针 ptf 加1,实际上 ptf 的值递增了8。如果 ptf 指针指向的是数组首元素的地址,ptf+1 就是数组第二个元素的地址(数组存储在一段连续编号的内存空间),*(ptf+1)就是第二个元素的值。

在C中数组名就是数组首元素的地址,它就是一个指向数组首元素的指针,而且是一个常量。例如, int arr[10],arr就是arr[0]的地址,arr==&arr[0]。和普通指针一样,arr+index 就是 arr[index]的地址,*(arr+index) 的值就是arr[index]的值。

注意:数组的地址 和 数组首元素的地址,值一样,概念不一样

#include <stdio.h>
#define SIZE 4

int  main(void) {
	short dates[SIZE];
	double bills[SIZE];  
	short * pti = dates; //pti指向dates数组首元素的地址
	double * ptf = bills; //ptf指向bills数组首元素的地址

	short index;
	for (index = 0; index < SIZE; index++) 
		printf("p + %d:  %10p %10p %10p\n", 
		index, pti + index, dates + index, &dates[index]);
	printf("----------------------------------------------\n");
	for (index = 0; index < SIZE; index++) 
		printf("p + %d:  %10p %10p %10p\n", 
		index, ptf + index, bills + index, &bills[index]);
	return 0;
}

p + 0:    00AFF95C   00AFF95C   00AFF95C
p + 1:    00AFF95E   00AFF95E   00AFF95E
p + 2:    00AFF960   00AFF960   00AFF960
p + 3:    00AFF962   00AFF962   00AFF962
----------------------------------------------
p + 0:    00AFF928   00AFF928   00AFF928
p + 1:    00AFF930   00AFF930   00AFF930
p + 2:    00AFF938   00AFF938   00AFF938
p + 3:    00AFF940   00AFF940   00AFF940
dates == &dates[0];  //dates, &dates[0]都表示数组首元素的内存地址,且都是常量
dates + 2 == &date[2];  //相同的地址
*(dates + 2) == dates[2]; //相同的值
/* 操作数组时 *(dates+2)的形式称为指针表示法, 
             dates[2]的形式称为数组表示法。
   可以自由选择使用数组表示法还是指针表示法。至于C语言,
   无论 dates 是数组名还是指针变量,dates[i]和*(dates+i)这两个表达式都是等价的。
   但是,只有当dates是指针变量时,才能使用 dates++ 这样的表达式。

   指针表示法(尤其与递增运算符一起使用时)更接近机器语言,因此一些编译器在编译时能生成效率更高的代码  */

实际上,C语言会将数组表示法转化为指针表示法。dates是数组首元素的地址,dates[index] 的意思是 * (dates + index ),可以认为* (dates + index )的意思是 " 到内存的dates位置,然后移动index个单元,检索储存在那里的值 "。指针表示法和数组表示法是等效的,编译器编译这两种写法生成的代码相同。


函数、数组和指针

要在函数间传递数组只需把数组首元素的地址和数组长度传递给被调函数形参。

//计算数组元素值之和
#include <stdio.h>
#define SIZE 10

int sum1(int*, int); //函数原型
int sum2(int * start, int * end);

int  main(void) {
	int marbles[SIZE] = { 1,2,3,4,5,6,7,8,10 };
	int total = sum1(marbles, SIZE);
	// int total = sum2(marbles, marbles+SIZE);
	printf("%d\n", total);
	return 0;
}

// 1 直接传递长度
int sum1(int * ar, int n) {
	int total = 0;
	for (int i = 0; i < n; i++) 
		total += ar[i];  //数组表示 ar[i] 与 指针表示*(ar + i) 等效
	return total;
}

// 2 传递两个指针,第1个指向数组首元素,第2个指向数组最后一个元素的下一个位置。
int sum2(int * start, int * end) {
	int total = 0;
	while(start < end) 
		total += *start++; 
	return total;
}

声明函数原型可以省略变量名,所以下面4种原型都是等价的:

int sum(int * ar , int n);
int sum(int * , int);
int sum(int ar[], int n);
int sum(int [], int);
/* 在函数原型声明和函数定义头中,int * ar 和 int ar[] 都是声明一个指向int类型的指针。
   使用int ar[] 告诉使用者指针ar指向的不仅一个int类型变量,还是一个int类型数组的元素。
   只有在函数原型和函数定义头中,int ar[] 才会表示声明一个指针形参(而不是数组) */

指针操作

#include <stdio.h> 
int main(void) { 
	int urn[5] = { 100, 200, 300, 400, 500 }; 
	int * ptr1, *ptr2, *ptr3; 
	ptr1 = urn;     // 把一个地址赋给指针 
	ptr2 = &urn[2]; // 把一个地址赋给指针 
	
	   // 解引用指针, 以及获得指针的地址 
	printf("pointer value, dereferenced pointer, pointer address:\n"); 
	printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1); 
	
	ptr3 = ptr1 + 4;  // 指针加法
	printf("\nadding an int to a pointer:\n"); 
	printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4)); 
	
	ptr1++;  // 递增指针 
	printf("\nvalues after ptr1++:\n"); 
	printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
	
	ptr2--;  // 递减指针 
	printf("\nvalues after --ptr2:\n"); 
	printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2); 
	
	--ptr1;  // 恢复为初始值 
	++ptr2;  // 恢复为初始值 
	printf("\nPointers reset to original values:\n"); 
	printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2); 
	// 一个指针减去另一个指针 
	printf("\nsubtracting one pointer from another:\n"); 
	printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n", ptr2, ptr1, ptr2 - ptr1); 
	// 一个指针减去一个整数 
	printf("\nsubtracting an int from a pointer:\n");
	printf("ptr3 = %p, ptr3 - 2 = %p\n", ptr3, ptr3 - 2);
	return 0;
}

在这里插入图片描述
如果编译器不支持%p 转换说明, 可以用%u 或%lu 代替%p; 如果 编译器不支持用%td转换说明打印地址的差值, 可以用%d或%ld来代替。

赋值:
可以把地址赋给指针。例如,用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。地址应该和指针类型兼容。

解引用:
通过 * 运算符获取或修改指针指向的地址上存储的值。不要解引用未初始化的指针

取址:
和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。因此&ptr1是指向ptr1的指针,而ptr1是指向urn[0]的指针

指针求差:
可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离

比较:
使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。

递增递减指针:
在递增或递减指针时要注意一些问题。编译器不会检查指针是否仍指向数组元素。C 只能保证指向数组任意元素的指针和指向数组后面第 1 个位置的指针有效。


指针和多维数组

int  zippo[4][2];   /// 数组的数组

zippo[0]、zippo[1]、zippo[2]、zippo[3] 都是内含两个int整数的数组。
数组名 zippo[0] 代表首元素地址,zippo[0] 是一个指向 int 型的指针
zippo[0] == &zippo[0][0]
*zippo[0] == zippo[0][0]
*(zippo[0]+1) == zippo[0][1]
*zippo[2] == zippo[2][0]
*(zippo[2]+1) == zippo[2][1]

zippo[0]、zippo[1]、zippo[2]、zippo[3] 4个数组构成一个新的数组 zippo

数组 zippo 首元素地址 是一个指向( 内含2 int整数)数组的指针
zippo == &zippo[0]
*zippo == zippo[0]
*(zippo+2) == zippo[2]
*(*(zippo+2)+1) == zippo[2][1]

**zippo == zippo[0][0]

简而言之
zippo[0]是一个占用一个int大小对象的地址,
zippo是一个占用两个int大小对象的地址。
由于这个整数和内含两个整数的数组都开始于同一个地址,所以zippo和zippo[0]的值相同。
int main(void) { 
	int zippo[4][2] = { { 2, 4 }, { 6, 8 }, { 1, 3 }, { 5, 7 } };
	printf(" zippo = %p,  zippo+1 = %p\n",zippo, zippo + 1); 
	printf(" zippo[0] = %p,  zippo[0]+1 = %p\n",zippo[0], zippo[0] + 1); 
	printf(" *zippo = %p,  *zippo+1 = %p\n",*zippo, *zippo + 1); 
	printf(" zippo[0][0] = %d\n", zippo[0][0]);
	printf(" *zippo[0] = %d\n", *zippo[0]); 
	printf(" **zippo = %d\n", **zippo); 
	printf(" zippo[2][1] = %d\n", zippo[2][1]);
	printf(" *(*(zippo+2)+1) = %d\n", *(*(zippo + 2) + 1)); 
	return 0; 
} 

 zippo = 004FFD54,  zippo+1 = 004FFD5C
 zippo[0] = 004FFD54,  zippo[0]+1 = 004FFD58
 *zippo = 004FFD54,  *zippo+1 = 004FFD58
 zippo[0][0] = 2
 *zippo[0] = 2
 **zippo = 2
 zippo[2][1] = 3
 *(*(zippo+2)+1) = 3

数组地址、数组内容和指针之间的关系
在这里插入图片描述

指向多维数组的指针

int zippo[4][2] = {0}; // 声明一个二维数组

// []的优先级高于 *
int * pax[2];  // pax 是一个内含两个指针元素的数组,每个元素都是指向int的指针
int (*pz)[2];  // pz 是一个 指向内含两个int值的数组的 指针

// 可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名:
pz = zippo;
zippo[m][n] == *(*(zippo + m) + n)
pz[m][n] == *(*(pz + m) + n)

函数和多维数组

三种函数原型

// 传递二维数组 COLS 代表列数 rows 代表行数,下面三个原型等价
int sum(int (*ar)[COLS], int rows); // 声明一个 指向内含COLS个int值的数组的 指针
int sum(int ar[][COLS], int rows);  // 第一个 [] 表明 ar 是一个指针
int sum(int [][COLS], int);         // 声明原型忽略参数名 

//也可以在第1对方括号中写上大小, 但是编译器会忽略该值
int sum(int ar[3][COLS], int rows); // 有效声明, 但是3将被忽略

int sum2(int ar[][], int rows);     // 错误的声明,不能省略第二个方括号的值
/* 编译器会把数组表示法转换成指针表示法。
   例如,编译器会把 ar[1]转换成 ar+1。编译器对ar+1求值,要知道ar所指向的对象大小 
   所以第二对方括号中的值不可省略 */

一般而言, 函数原型或函数头中声明一个指向N维数组的指针时, 只能省略最左边方括号中的值。因为第1对方括号只用于表明这是一个指针, 而其他的方括号则用于描述指针所指向数据对象的类型。

使用 typedef 声明数组类型

typedef int arr4[4];    // arr4是一个内含4 个int的数组 
typedef arr4 arr3x4[3]; // arr3x4 是一个内含3个arr4的数组

int sum(arr3x4 ar, int rows);    // 与下面的声明相同 
int sum(int ar[3][4], int rows); // 与下面的声明相同 
int sum(int ar[][4], int rows);  // 标准形式

const 和 指针

常变量

由const 创建的只读变量不可更改值,一般称为常变量,它跟其它常量有本质区别。虽然用 #define 指令可以创建类似功能的符号常量,但是 const 的用法更加灵活。可以创建 const 数组、const 指针和指向 const 的指针。

const 创建常变量

double pi = 3.14159; // 普通变量
const double PI = 3.14159; // 常变量
// 等同 double const PI = 3.14159; 
// 必须在声明时初始化且之后不可再通过 PI 修改值

PI = 3.14;   //编译错误,不能直接通过PI修改值

// 但可以通过指针间接修改PI的值,这样C编译器会发出警告
double * pd = &PI; 
/* 编译警告,将一个普通指针指向 const 变量是不安全的 */
   
*pd = 3.14;  // const 变量 PI 已经被修改

普通指针指向 const 变量时,意味着后续可能会通过这个指针修改const变量,但是修改 const 数据可能是不安全的。所以C 编译器会给出警告,而C++中编译会发生编译错误。 事实上,如果已经把一个变量定义为const数据,就不应该再修改它。标准规定了通过普通指针更改const数据是未定义的。

const创建只读数组

const int days[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
days[9] = 44; // 编译错误, 数组元素的每个值都是常变量

const 修饰 一级指针

const 修饰哪部分主要看 const 的位置,规则适用更多级指针。

指向const的指针
const double * pc 等同 double const * pc
const 修饰 *pc,*pc 的值不可修改,pc 的值可以修改

double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * pc = rates;  // pc指向数组的首元素
double const * pc = rates;  // 与上面等同

*pc = 29.89;       // 编译错误
pc[2] = 222.22;    // 编译错误
rates[0] = 99.99;  // 没问题,因为rates未被const限定

// 虽然不能修改pc指向的对象的值,但是可以让pc指向别处:
pc++;  // 让pc指向rates[1] 

// 指向 const 的指针通常用于函数形参中,表明该函数不会使用指针改变数据,例如:
int sum(const int * ar, int n);
int sum(const int ar[], int n);  

const 指针
double * const pc
const 修饰 pc,pc 的值不可修改,*pc 的值可以修改

double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
double * const pc = rates;  // pc指向数组的首元素

pc = &rates[2]; // 编译错误
*pc = 92.99;     // 可以修改rates[0]的值

//可以用这种指针修改它所指向的值,但是它只能指向初始化时设置的地址。

指向 const 的 const 指针
const double * const pc
第一个const 修饰 *pc,第二个const 修饰 pc,*pc 和 pc 的值都不可修改

double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * const pc = rates;  //  pc指向数组的首元素

pc = &rates[2];  //编译错误
*pc = 92.99;     //编译错误

const 修饰 二级指针

const 修饰哪部分主要看 const 的位置,类似规则适用更多级指针。

const int ** p
int const ** p 与上面等同
const 修饰 **p ,**p的值不可修改,*p 和 p 的值可以修改

int * const * p
const 修饰 *p ,*p 的值不可修改, **p 和 p 的值可以修改

int ** const p
const 修饰 p ,p的值不可修改,**p 和 *p 的值可以修改

int * const * const p
第一个 const 修饰 *p,第二个 const 修饰 p

const int * const * const p
第一个 const 修饰 **p,第二个 const 修饰 *p,第三个 const 修饰 p

int main(void) {
	 
	int a = 10;      int b = 20; 
	int * pa = &a;   int *pb = &b;
	
	const int * const * const ppa = &pa; //限定
	**ppa = 30;  // 编译错误
	*ppa = pb;   // 编译错误
	ppa = &pb;   // 编译错误

	printf("%d\n", **ppa);
	return 0;
}

指针之间的赋值

指针之间的赋值比数值类型之间的赋值要严格。例如, 不用类型转换就可以把 int 类型的值赋给double类型的变量, 但是两个类型的指针不能这样做。

普通指针之间赋值

int n = 5;
double x;
int * pi = &n;
double * pd = &x;
x = n;   // 隐式类型转换
pd = pi; // 编译警告
pd = (double *)pi; // 没有警告
int * pt;
int (*pa)[3];
int ar1[2][3];
int ar2[3][2];
int **ppt;   	 // 一个指向指针的指针

pt = &ar1[0][0]; // 都是指向int的指针
pt = ar1[0]; 	 // 都是指向int的指针
pt = ar1;		 // 编译警告

pa = ar1; 		 // 都是指向内含3个int类型元素数组的指针
pa = ar2;		 // 编译警告

ppt = &pt; 		 // both pointer-to-int *
*ppt = ar2[0]; 	 // 都是指向int的指针
ppt = ar2;		 // 编译警告

指向const的指针赋给普通指针

把指向const的指针赋给普通指针不安全, 因为这样可以使用新的指针改变原指针指向的数据。 编译器在编译代码时, 可能会给出警告。C标准规定了通过普通指针更改const数据是未定义的。

普通指针赋给指向const的指针

把普通指针赋给指向const的指针没有问题。

// 一级引用
int x = 20;
const int y = 23;

int * p1 = &x; //普通指针
const int * p2 = &y; // 指向const的指针

p1 = p2; // 把指向const的指针赋给普通指针
/* 编译警告, p2 指向的const数据可能通过p1被修改,而我们不应该修改const数据。
   这样做导致的结果是未定义的 */

p2 = p1;   // 没问题 -- 

指向const的二级指针

// 二级引用
	int *p1;
	const int **pp2;  
	const int n = 13; 

	pp2 = &p1;  // 编译警告 
/*  warning: assignment to 'const int **' from incompatible pointer type 'int **'  */	

	*pp2 = &n;  
/* 这将 间接修改 p1 , 使 p1 指向 n,这样一个普通指针p1指向了const数据n,不安全 
   warning: assignment to 'const int **' from incompatible pointer type 'int **'  */

	*p1 = 10;   
/* 这样就可以通过 p1 修改 n,但是 n 是不应该被修改的,因为它被const限定 */
	printf("%d\n", n);
	
/* Terminal中(OS X对底层UNIX系统的访问)使用gcc编译包含以上代码的小程序,导致n最终的值是13,
   但是在相同系统下使用clang来编译,n最终的值是10。两个编译器都给出指针类型不兼容的警告。
   当然,可以忽略这些警告,但是最好不要相信该程序运行的结果,这些结果都是未定义的 */

C const和C++ const

C和C++中const的用法很相似, 但是并不完全相同。

  1. C++允许在声明数组大小时使用const整数, 而C却不允许。C++把const修饰的变量当做常量,而C不是。
  2. C++的指针赋值检查更严格
const int y; 
const int * p2 = &y; 
int * p1; 
p1 = p2;  // C++中不允许这样做, 但是C可能只给出警告 

变长数组VLA C99

在C99标准之前,声明数组时只能在方括号中使用整型常量表达式。所谓整型常量表达式,是由整型常量构成的表达式。sizeof 表达式被视为常量,而 const 值不是常量(与C++不同)。

int a1[5*2+1];
int a2[sizeof(int)]; //sizeof(int)是常量

C99新增了变长数组(variable-length array, VLA) ,允许使用变量表示数组的维度。声明VLA时不能进行初始化。

int quarters = 4; 
const int regions = 5; 
double sales[regions][quarters]; // 一个变长数组(VLA)

变长数组必须是自动存储类别, 这意味着无论在函数中声明还是作为函数形参声明,都不能使用static或extern 存储类别说明符 。 而且,不能在声明中初始化它们。 最终, C11把变长数组作为一个可选特性, 而不是必须强制实现的特性。

int sum(int rows, int cols, int ar[rows][cols]) 
/* 注意前两个形参(rows和cols)用作第3个形参二维数组ar的两个维度。 因为ar的声明要使用rows和cols, 
   所以在形参列表中必须在声明ar之前先声明这两个形参。 因此, 下面的原型是错误的:*/
int sum(int ar[rows][cols], int rows, int cols); 
/* C99/C11标准规定, 可以省略原型中的形参名, 但是必须用星号来代替省略的维度 */
int sum2d(int, int, int ar[*][*]); 

需要注意的是, 在函数定义的形参列表中声明的变长数组并未实际创建数组。 和传统的语法类似, 变长数组名实际上是一个指针。 这说明带变长数组形参的函数实际上是在原始数组中处理数组, 因此可以修改传入的数组。


复合字面量 C99

C99新增了 复合字面量(compoundliteral)字面量是除符号常量外的常量。发布C99标准的委员会认为, 如果有代表数组和结构内容的复合字面量, 在编程时会更方便。

对于数组, 复合字面量类似数组初始化列表, 前面是用括号括起来的类型名。

 int diva[2] = {10, 20}; //一个普通数组

 (int [2]) m{10, 20};
// 创建一个和diva数组相同的匿名数组,int[2]即是复合字面量的类型名。

初始化有数组名的数组时可以省略数组大小, 复合字面量也可以省略大小,编译器会自动计算数组当前的元素个数。

 (int []){50, 20, 90} // 内含3个元素的复合字面量 

因为复合字面量是匿名的, 所以不能先创建然后再使用它, 必须在创建的同时使用它。 使用指针记录地址就是一种用法。

int * pt1; 
pt1 = (int [2]) {10, 20};

还可以把复合字面量作为实际参数传递给带有匹配形式参数的函数:

 int sum(const int ar[], int n);
  ... int total3;
  total3 = sum((int []){4,4,4,5,5,5}, 6); 

这里, 第1个实参是内含6个int类型值的数组, 和数组名类似, 这同时也是该数组首元素的地址。 这种用法的好处是, 把信息传入函数前不必先创建数组, 这是复合字面量的典型用法。

可以把这种用法应用于二维数组或多维数组。 例如, 下面的代码演示了如何创建二维 int 数组并储存其地址:

int (*pt2)[4]; 
/* 声明一个指向二维数组的指针, 该数组内含2个数组 元素, 每个元素是内含4个int类型值的数组 */

pt2 = (int [2][4]) { {1,2,3,-9}, {4,5,6,-8} }; 
// 该复合字面量的类型是int [2][4],即一个2×4的int数组。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值