概述
C 语言的数组是一种将标量数据聚集成更大数据类型的方式。其实现的方式非常简单,很容易翻译为机器代码。C 语言中一个不同寻常的特点是可以产生指向数组中元素的指针,并对这些指针进行运算。
一、数组
1.1、基本原则
数组是一组数目固定、类型相同的数据项,数组中的数据项被称为元素。数组的声明方式如下:
T A[N];
其中 T 为数据类性,N 为整形常数。首先,它在内存中分配一个 L*N 字节的连续区域,其次它引入了标识符 A,可以用 A 来作为指向数组开头的指针。
1.2、指针运算
C 语言允许对指针进行运算,计算出来的值会根据该指针类型的大小进行伸缩。例如,p 是一个指向类型为 T 的数据的指针,p 的值为 x,那么表达式 p+i 的值为
x+L*i,这里 L 为数据类型 T 的大小。
单操作数操作符“&”和“*”可以产生指针和间接引用指针。
1.3、数组初始化
可以给数组元素指定初始值,例如:
double values[7] = {1.0,1.2,1.5};
这里初始值个数小于数组元素个数,那么没有初始值的元素就设置为0。如果初始值个数大于数组元素个数,那么就会报错。
1.4、多维数组
当我们创建数组的数组时,即创建多维数组,数组分配和引用的一般原则也是成立的。例如声明以下多维数组:
int A[5][3];
等价于下面的声明:
typedef int row3_t[3];
row3_t A[5];
整个数组的大小为:4×5×3=60字节。
数组A可以看做一个5行3列的矩阵,数组元素按照“行优先”的顺序排列,这就就意味着第0行的所有元素,可以写做A[0]。这种排列顺序是嵌套声明的结果。将 A 看作一个5个元素的数组,每个元素都是3个 int 的数组。
要访问多维数组的元素,编译器会以数组启始为基地址,偏移量为索引(需要经过伸缩),产生计算期望的偏移量。通常来说,对于一个声明如下的数组:
T D[R][C];
它的元素 D[i][j] 的内存地址为:
&D[i][j] = x + L(C × i + j) // x 为多维数组首地址
1.5、变长数组
历史上 C 语言只支持在编译时就能确定的多维数组(对一维可能有例外)。ISO C99 引入了一种功能,允许数组的维度是表达式,在数组被分配的时候才计算出来。
看下面这个例子:
size_t size = 0;
printf("Enter the number of elements you want to store: ");
sacanf("%zd",&size)
float values[size];
printf("The size of variable array, values is %zu bytes.\n",sizeof values);
执行结果如下:
Enter the number of elements you want to store: 5
The size of variable array, values is 20 bytes.
上面代码逻辑很简单,就是把从键盘上读到的值放到 size 中,接着使用 size 的值指定数组的长度。这里要注意的是,因为 size_t 是用实现代码定义的整数类型,所以如果使用 %d 读取这个值,就会得到一个编译错误。%zd 中的 z 告诉编译器,它应该是 size_t,所以无论整数类型 size_t 是什么,编译器都会使说明符合使用于读取操作。
二、指针
指针对于 C 语言来说太重要,然而,想要全面理解指针,除了要对 C 语言有熟练的掌握外,还要有计算机硬件以及操作系统等方方面面的基本知识。所以指针对初学不是十分友好,所以对于初学者选择好的入门参考书非常重要,这里推荐一下 Ivor Horton 的《Beginning C》,Kenneth A. Reek 的《Pointers on C》和 K&R 的《The C Programming Language》(这本其实不适合初学者)。
指针是一种保存变量地址的变量。在 C 语言中,指针的使用非常广泛,原因之一是,指针常常是表达某个计算的惟一途径,另一个原因是,同其它方法比较起来使用指针通常可以生成更高效、更紧凑的代码。
一、指针与地址
指针是能够存放一个地址的一组存储单元,指针的值实质是内存单元(即字节)的编号,所以指针单独从数值上看,也是整数,他们一般用16进制表示。指针的值(虚拟地址值)使用一个机器字的大小来存储,也就是说,对于一个机器字为 w 位的电脑而言,它的虚拟地址空间是0~2的w次方 - 1,程序最多能访问2的w次方个字节。这就是为什么 32 位系统最大支持 4GB 内存的原因了。
在 Beginning C 中有如下描述“可以存储地址的变量称为指针(pointer),存储在指针中的地址通常是另一个变量。”结合下图可以更直接的理解这句话。
上图中,指针 pnum 含有另一个变量 num 的地址,变量 num 是一值为 99 的整数变量。存储在 pnum 中的地址是 num 的第一个字节的地址。“指针”这个词也用于
表示一个地址。但是仅仅知道 pnum 是一个指针是不够的,编译器必须要知道指针所指向的变量的类型,才能正确的处理指针指向内存的内容。从上一篇文章我们知道,
一个 char 类型占用一个字节,而1个 int 类型或占用4个字节。所以说,每个指针都和具体的变量类型相关联,并且也只能用于指向该类型的变量。
一般给定义类型的指针写成 type,其中 type 是任意给定的类型。这里要注意,类型名 void 表示没有指定类型,所以 void 类型的指针可以包含任意类型的数据
项的地址。
2.1、声明指针
指针的声明形式和变量的声明形式类似,声明一个指向 int 类型的变量的指针声明如下:
int* pnumber; 或 int *pnumber;
pnumber 变量的类型为 int*,可以存储任意 int 类型变量的地址。在编写代码时最好统一指针的声明方式。看下面这个例子:
int *p, q;
上述语句声明了一个指针 p 和一个变量 q,两者都是 int 类型。
上面的例子中虽然创建了 pnumber 变量,但是并没有对它进行初始化,这种做法很危险,因为未初始化的内存中可能存在垃圾值,在程序执行可能会带来意想不到的问
题。所以应该总是在声明指针时对他进行初始化。以下代码表示 pnumber 不指向任何对象:
int *pnumber = NULL;
NULL 是在标准库中定义的一个常量,对于指针它表示 0。
2.2 寻址运算符
可以使用寻址运算符 & 来获取变量的地址。例如:
int number = 99;
int *pnumber = &number;
2.3、通过指针访问值
使用间接运算符“*”可以访问指针的变量值,该操作符也称为“取消引用运算符”(dereferencing operator),它用于取消对指针的引用。看下面示例:
int number = 1024;
int *pointer = &number;
printf("The value of number is: %d.\n", number);
printf("The value of number is: %d.\n", *pointer);
printf("The address of number is: %p.\n", pointer);
printf("The address of pointer is: %p.\n", &pointer);
printf("The size_t of address is: %zd bytes.\n", sizeof(pointer));
执行结果:
The value of number is: 1024.
The value of number is: 1024.
The address of number is: 0x7fffc066958c.
The address of pointer is: 0x7fffc0669590.
The size_t of address is: 8 bytes.
2.4、指向常量的指针
声明指针时可以使用关键字 const 进行指定,表示该指针指向的值不能被修改。如下所示:
int value = 100;
const int* pvalue = &value;
以上语句是将指针 pvalue 所指向的值声明为常量,编译器会检查是否有语句试图改变 pvalue 指向的值,并将这些语句标记为错误。以下语句就会产生这样一个错误:
*pvalue = 101; // error: assignment of read-only location ‘*pvalue’
但是可以通过以下语句修改 value 的值:
value = 101; // 合法
由于指针不是常量,可以修改指针 pvalue 的值:
int number = 0;
pvalue = &number;
但是仍旧不能使用指针改变该变量的值,总结来说就是指向常量的指针不能通过指针来改变该指针指向的值。
2.5、常量指针
也可以使指针中储存的地址不能被改变,其声名方式如下:
int value = 100;
int *const pvalue = &value; // 声明常量指针
编译器会检查是否有语句试图改变 pvalue 的值,并将这些语句标记为错误。如下所示:
pvalue = &number; // error: assignment of read-only variable ‘pvalue’
但是可以修改指针指向的值:
*pvalue = 101; // 合法
value = 101; // 合法
由以上可以看出,在对常量指针声明时需要为该指针指定一个有效的地址,以避免出错。
可以创建一个常量指针,它指向一个常量值:
const int *const pvalue = &value;
以上语句既不能改变指针的值,也不能通过指针改变 value 的值,但是可以直接修改 value 的值。
三、数组和指针
- 数组是一个相同类型的对象集合,可以用一个名称引用;
- 指针是一个变量,它的值是给定类型的另一个变量或常量的地址;
- 数组和指针关系密切,有时可以互换使用。
3.1、一维数组
考虑下面这个例子,这里使用标准函数 scanf():
char single = 0;
scanf("%c", &single);
如果需要输入字符串,可以使用以下代码:
char single[] = "hello c";
scanf("%s", single);
可以发现这里并没有使用取地址符,而是直接使用数组名,就像使用指针。如果以这种方式使用数组名称使用数组名,而没有带索引值它就引用数组的第一个元素得到地址。
但是数组和指针之间有一个重要区别:可以改变指针包含的地址,但是不能改变数组名称引用的地址。
以下例子展示了将一个整数值加到指针(p + 1)产生的效果:
char exp[] = "this is a example.";
char* p = exp;
for(int i = 0; i < strlen(exp); ++i) {
printf("exp[%d] = %c * (p+%d) = %c &exp[%d] = %p p+%d = %p\n", i,exp[i], i, *(p+i), i, &exp[i], i, p+i);
}
输出结果如下:
exp[0] = t * (p+0) = t &exp[0] = 0x7fff0c3df580 p+0 = 0x7fff0c3df580
exp[1] = h * (p+1) = h &exp[1] = 0x7fff0c3df581 p+1 = 0x7fff0c3df581
exp[2] = i * (p+2) = i &exp[2] = 0x7fff0c3df582 p+2 = 0x7fff0c3df582
exp[3] = s * (p+3) = s &exp[3] = 0x7fff0c3df583 p+3 = 0x7fff0c3df583
可以看出通过 &exp[i] 获取的地址和通过 (p+i) 获取的地址相同,这也是预期的结果。
3.1 多维数组
在多维数组中数组名和指针之间的差异更加明显,以一个二维数组为例:
char board[3][3] = {
{'1','2','3'},
{'4','5','6'},
{'7','8','9'}
};
printf("address of board : %p\n", board);
printf("address of board[0][0] : %p\n", &board[0][0]);
printf("address of board[0] : %p\n", board[0]);
输出结果:
address of board : 0x7fff66b67c2f
address of board[0][0] : 0x7fff66b67c2f
address of board[0] : 0x7fff66b67c2f
以上三个输出结果相同,由此可以得到推论:声明一维数组时 x[n1] 时,[n1] 放在数组名称之后,告诉编译器这是一个有 n1 个元素的数组。声明二维数组时y[n1][n2]时,编译器就会创建一个大小为 n1 的数组,它的每一个元素是大小为 n2 的数组。
虽然 board、 board[0] 和 &board[0][0] 的数值相同,但是它们并不是相同的东西:board 是 char 型二维数组的地址,board[0] 是 char 型一维数组的地址,&board[0][0] 是 char 型数组元素的地址。
用指针记号获取数组中的数值时,必须使用间接运算符,看下面这个例子:
char board[3][3] = {
{'1','2','3'},
{'4','5','6'},
{'7','8','9'}
};
printf("value of board[0][0] : %c\n", board[0][0]);
printf("value of *board[0] : %c\n", *board[0]);
// board 是 char** 类型,是指针的指针
printf("value of **board : %c\n", **board);
输出结果:
value of board[0][0] : 1
value of *board[0] : 1
value of **board : 1
注意:尽管可以把二维数组看成是一维数组的数组,但是在内存中并不是以这种形式存储二维数组,其存储方式为存储一个很长的一维数组,编译器确保可以像一维数组那样访问它。如下所示:
char board[3][3] = {
{'1','2','3'},
{'4','5','6'},
{'7','8','9'}
};
for(int i = 0; i < 9; ++i) {
// *board 得到二维数组的第一个元素(首个一维数组),*board +i
// 就是对第一个一维数组进行偏移,在执行解引用就可以得到二维数组
// 元素值。
printf(" board: %c\n", *(*board +i));
}
输出结果:
board: 1
board: 2
board: 3
board: 4
board: 5
board: 6
board: 7
board: 8
board: 9
四、内存的使用
C 语言中内存划分如下:
栈区:栈内存,存放局部变量,自动分配和释放,里面函数的参数,方法里面的临时变量
堆区:动态内存分配,由程序员手动分配,最大值为操作系统的 80%
全局区或静态区
常量区(字符串)
程序代码区
C 语言有一个功能:动态分配内存,它依赖指针的概念,为在代码中使用的指针提供了很强的激励机制,它允许在程序执行时动态分配内存。只有使用指针才能动态分配内存。
4.1、动态分配内存:malloc() 函数
在运行时分配内存的最简单的标准库函数是 malloc() 函数,使用该函数时,需要在程序中包含头文件
动态分配内存代码如下:
int* pNumber = (int*)malloc(100); // 这里需要进行类型强转
以上代码可以分配 100 个字节,也就是 25 个 int 值,该语句假定 int 占 4 个字节,但是不同的系统对 int 的大小规定可能不同,因此最好取消这种假设,而使用以下方式分配内存:
int* pNumber = (int*)malloc(25*sizeof(int));
需要注意的是:
- malloc() 返回类型为 void*,所以需要进行类型转换。许多编译器会将 malloc 返回的地址自动转化为赋值语句左边的指针类型,但是加上显示的类型转换是无害的;
- 在 32 位模式中,malloc() 返回的地址总是 8 的倍数;在 64 位模式系统中,该地址总是 16 的倍数;
- 如果 malloc() 遇到问题,那么它就会返回 NULL,并设置 errno,所以在使用前最好先判断内存是否已经分配;
- malloc() 不初始化它所分配的内存。
4.2、释放动态分配的内存
在使用动态分配内存时,应该总是在不需要该内存时释放它们。释放动态分配的内存必须要能够访问引用内存块的地址,释放内存语句如下:
free(pNumber);
pNUmber = NULL;
注意:在释放指针指向的堆内存时,必须确保它不被另一个地址覆盖。
示例代码,列举指定个数的质数:
#include
#include
#include
int main(void) {
unsigned long long *pPrimes = NULL;
unsigned long long trial = 0;
bool found = false;
int total = 0;
int count = 0;
printf("How many primes would you like - you'll get at least 4? ");
scanf("%d", &total);
total = total < 4 ? 4 : total;
pPrimes = (unsigned long long*)malloc(total*sizeof(unsigned long long));
if(!pPrimes) {
printf("Not enough memory. It's the end I'm afraid.\n");
return 1;
}
*pPrimes = 2ULL; // first prime
*(pPrimes + 1) = 3ULL; // second prime
*(pPrimes + 2) = 5ULL; // third prime
count = 3;
trial = 5ULL;
while(count < total) {
trial += 2ULL;
for(int i = 1; i < count; ++i) {
if(!(found = (trial % *(pPrimes + i)))) break;
}
if(found) *(pPrimes + count++) = trial;
}
for(int i = 0; i < total; ++i) {
printf("%12llu", *(pPrimes + i));
if(!((i+1) % 5)) printf("\n");
}
printf("\n");
free(pPrimes);
pPrimes = NULL;
return 0;
}
4.3、使用 calloc() 函数分配内存
在头文件
- 它将内存分配为指定大小的数组;
- 它初始化了分配的内存,所有的位均为 0。
calloc() 需要两个参数,数组元素个数和数组元素所占字节数,两个参数类型都是 size_t,函数返回类型为 void*。示例代码如下:
int* pNumber = (int*)calloc(75, sizeof(int));
如果不能分配,那么函数将返回 NULL,也可以让编译器执行类型转换:
int* pNumber = calloc(75, sizeof(int));
4.4、扩展动态分配的内存
realloc() 函数可以重用或扩展之前使用 malloc() 或 calloc() (或 realloc())分配的内存。realloc() 需要两个参数:指针和要分配的新内存字节数。
参考
Beginning C
Pointer on C
The C Programming