数组和指针的关系非常紧密,所以这本书将他们放到一章来学习。下面我们一起来看一下。
我们通常将只存储一个值的变量叫做标量scalar,将存储多个值的叫做vector
int fix = 1; //标量
int power[8] = {1, 2, 3, 4, 5, 6, 7, 8};//vector
const int power2[8] = {1, 2, 3, 4, 5, 6, 7, 8};//只读数组,内容不能被修改
int power[] = {1, 2, 3, 4, 5, 6, 7};//使用初始化列表,可以不用指定元素数量
int power[6] = {[5]=22}; //部分初始化,其余值默认初始化为0,(C99)
#include
#define SIZE 4
int main(void)
{
int no_data[SIZE]; //只声明未初始化的数组
int i;
printf("%2s%14s\n",
"i", "no_data[i]");
for (i = 0; i < SIZE; i++)
printf("%2d%14d\n", i, no_data[i]); //会打印随机值
return 0;
}
使用未初始化值的数组,会打印内存上的现有垃圾值。
#include
#define SIZE 4
int main(void)
{
int no_data[SIZE]={3,5}; //初始化部分值
int i;
printf("%2s%14s\n",
"i", "no_data[i]");
for (i = 0; i < SIZE; i++)
printf("%2d%14d\n", i, no_data[i]); //只有前两个数打印输入,后面会被默认赋为0
return 0;
}
如果部分初始化,剩余值初始化为0.
int no_data[5]={3,5,4,5,6};
int data[5];
data = no_data;//不允许
data[5] = {3,4,5,7,7}; //不起作用,因为已经声明了,不能再用初始化方式赋值
编译器不会检查数组下标是否使用得当,所以数组下标越界使用,会导致程序改变其他变量的值,从而导致程序异常终止。
C为何不检查数组越界?
在程序运行之前,数组下标值尚未确定,编译器如果需要再运行时添加额外代码检查数组每个下标,就会降低程序运行速度。因此编译器不检查下标越界,而信任程序员。
float rain[5][10]; //声明5行10列的数组,第一个参数代表元素所在行,第二个代表所在列。
数组表示法其实实在变相的使用指针。
int power[4];
power
和 &power[0]
都表示数组首元素的地址。
两个都是常量,不会改变。但是可以把他赋值给指针变量。
#include
#define SIZE 4
int main(void)
{
short dates [SIZE];
short * pti;
short index;
double bills[SIZE];
double * ptf;
// 数组首元素地址
pti = dates;
ptf = bills;
printf("%23s %15s\n", "short", "double");
//打印poiter+index的地址
for (index = 0; index < SIZE; index ++)
printf("pointers + %d: %10p %10p\n",
index, pti + index, ptf + index);
return 0;
}
上面程序是打印数组中每个元素的地址。
第一行是打印每个数组开始的地址,下一行是指针+1后的地址。
可以看到C中指针+1,增加的是一个存储单元,也就是+1后的地址是数组中下一个元素的地址,而不是下一个字节的地址。
dates+2 == &date[2]
元素首元素地址后移两位,也就是第三个元素的地址
所以可以推导出某个元素下标的值也可以表示为arr[i] == *(arr+i)
下面看一个区别:
*(dates + 2) //dates数组中第三个元素的值,他与dates[2]等价
*dates + 2 //dates数组中第一个元素的值+2
所以,我们在声明指针时,必须要指定指针的类型。这样指针+1移动的实际上是该指针类型的存储单元。比如int指针,每+1,指针实际移动是4个字节。
我们想把一个数组传入函数,如何做?
可以这样,使用数组形参
#include
#define SIZE 10
int sum(int ar[], int n);
int main(void)
{
int marbles[SIZE] = {20,10,5,39,4,16,19,26,31,20};
long answer;
answer = sum(marbles, SIZE);
printf("The total number of marbles is %ld.\n", answer);
printf("The size of marbles is %zd bytes.\n",
sizeof marbles);
return 0;
}
int sum(int ar[], int n) // how big an array?
{
int i;
int total = 0;
for( i = 0; i < n; i++)
total += ar[i];
printf("The size of ar is %zd bytes.\n", sizeof ar);
return total;
}
我们把一个函数
#include
#
define SIZE 10
int sum(int ar[], int n);
int main(void)
{
int marbles[SIZE] = {20,10,5,39,4,16,19,26,31,20};
long answer;
answer = sum(marbles, SIZE);
printf("The total number of marbles is %ld.\n", answer);
printf("The size of marbles is %zd bytes.\n",
sizeof marbles);
return 0;
}
int sum(int *arr, int n)
{
int i;
int total = 0;
for( i = 0; i < n; i++)
total += ar[i]; //也写成total += *(ar++);
printf("The size of ar is %zd bytes.\n", sizeof ar);
return total;
}
记住:数组名是该数组首元素的地址。
我们的数组marbles就是数组名,也就是该数组首元素的地址。
而sum函数要接收的形参是一个int型指针,也就是一个地址。那么我们将该数组首元素地址传入到函数中。这时,arr就表示的marbles这个数组的数组名了,所以在函数中我们直接使用arr[4]
所以使用函数接收数组的时候下面两种形式声明等价
int sum(int ar[], int n);
int sum(int *arr, int n);
*和++ 运算符优先级相同,但是结合律是从左往右
#include
int data[2] = {100, 200};
int moredata[2] = {300, 400};
int main(void)
{
int * p1, * p2, * p3;
p1 = p2 = data;
p3 = moredata;
printf(" *p1 = %d, *p2 = %d, *p3 = %d\n",
*p1 , *p2 , *p3); // 100 100 300
printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n",
*p1++ , *++p2 , (*p3)++); // 100 200 300
printf(" *p1 = %d, *p2 = %d, *p3 = %d\n",
*p1 , *p2 , *p3); // 200 200 301
return 0;
}
指针初始化时,“=”的右操作数必须为内存中数据的地址,不可以是变量。
指针的初始化主要有两大类方法:
一是给指针变量初始化一个在内存中已经存在的地址;二是通过指针变量申请一块新的内存并赋初值。
对于一个不确定要指向何种类型的指针,在定义它之后最好把它初始化为NULL,并在解引用这个指针时对它进行检验,防止解引用空指针。另外,为程序中任何新创建的变量提供一个合法的初始值是一个好习惯,它可以帮你避免一些不必要的麻烦。
在使用指针之前,必须要用已分配的地址来初始化他。
int *pt;
在声明创建了一个指针时,系统只分配了存储指针本身的内存,而并未分配存储数据的内存。
比如
int *pt;
*pt = 5; //严重错误
严重错误,因为该句意思是把5存储在pt指向的位置。而此时pt只声明了,还没有指向一个位置,所以不知道将5存在何处,程序可能擦除其他数据,或者导致程序崩溃。
// ptr_ops.c -- pointer operations
#include
int main(void)
{
int urn[5] = {100,200,300,400,500};
int * ptr1, * ptr2, *ptr3; //声明指针
ptr1 = urn; // 把一个地址赋值给指针,等价于ptr1 = &urn[0];
ptr2 = &urn[2]; // 把一个地址赋值给指针
// 解引用指针以及获得地址
printf("pointer value指针的值, dereferenced pointer解引用指针, pointer address指针的地址:\n");
printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n",
ptr1, *ptr1, &ptr1);//ptr1 = 0x7ffd7c103ad0, *ptr1 =100, &ptr1 = 0x7ffd7c103ac8
// pointer addition指针加法
ptr3 = ptr1 + 4;
printf("\nadding an int to a pointer:\n");
printf("ptr1 + 4 = %p, *(ptr4 + 3) = %d\n",
ptr1 + 4, *(ptr1 + 3));//ptr1 + 4 = 0x7ffd7c103ae0, *(ptr4 + 3) = 400
// increment a pointer 指针++
ptr1++;
printf("\nvalues after ptr1++:\n");
printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n",
ptr1, *ptr1, &ptr1); //ptr1 = 0x7ffd7c103ad4, *ptr1 =200, &ptr1 = 0x7ffd7c103ac8 指针本身会移动至下一个元素
// decrement a pointer 指针--
ptr2--;
printf("\nvalues after --ptr2:\n");
printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n",
ptr2, *ptr2, &ptr2); //ptr2 = 0x7ffd7c103ad4, *ptr2 = 200, &ptr2 = 0x7ffd7c103ac0
--ptr1; // restore to original value
++ptr2; // restore to original value
printf("\nPointers reset to original values:\n");
printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2); //ptr1 = 0x7ffd7c103ad0, ptr2 = 0x7ffd7c103ad8
// subtract one pointer from another 指针减指针
printf("\nsubtracting one pointer from another:\n");
printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n",
ptr2, ptr1, ptr2 - ptr1);//ptr2 = 0x7ffd7c103ad8, ptr1 = 0x7ffd7c103ad0, ptr2 - ptr1 = 2 (表示的是元素之间下标位置距离
// subtract an integer from a pointer 指针减一个数
printf("\nsubtracting an int from a pointer:\n");
printf("ptr3 = %p, ptr3 - 2 = %p\n",
ptr3, ptr3 - 2); //ptr3 = 0x7ffd7c103ae0, ptr3 - 2 = 0x7ffd7c103ad8
return 0;
}
函数传参两种方式,一种是按值传递,一种是指针传递。如果传递的是数组,则我们都要使用按指针传递。因为值传递数组函数栈必须有足够空间来接收原数组的副本,而且效率低下。所以我们一般都使用指针传递。
但是按指针传递有一个安全隐患就是可能会修改原数组中的数据,但有时我们不想让函数修改我们数组中的数据,以保证数据的完整性。我们可以使用const关键字。
/* displays array contents */
void show_array(const double ar[], int n)
{
int i;
for (i = 0; i < n; i++)
printf("%8.3f ", ar[i]);
putchar('\n');
}
/* multiplies each array member by the same multiplier */
// 如果这里使用const修饰,那么编译就会报错
void mult_array(double ar[], int n, double mult)
{
int i;
for (i = 0; i < n; i++)
ar[i] *= mult;
}
//修饰数组
const double dip[SIZE] = {20.0, 17.66, 8.2, 15.3, 22.22};
dip[2] = 3.4; //编译报错
double *pt = dip; //不允许,因为dip是const数组,同理不要把const数组传给普通形参指针
//修饰指针类型:不允许改变指向的值
double rates[4] = {20.0, 17.66, 8.2, 15.3, 22.22};
const double *pd = rates;// 指向元素首地址
*pd = 4.5; //不允许
pd[2] = 4; // 不允许
double rates2[2] = {23,4};
pd = rates2;//允许,pd指针可以指向其他位置
//修饰单独指针:指针不能指向其他位置
double arr[4] = {20.0, 17.66, 8.2, 15.3, 22.22};
double * const pt2 = arr;
pt2 = &arr[2]; //不允许,不能指向其他位置
*pt = 33.1; //允许,可以修改指向的值arr[0]
//修饰指针类型+单独指针:不能指向其他位置也不能修改值
double arr2[4] = {20.0, 17.66, 8.2, 15.3, 22.22};
const double* const pc = arr2;
pc = &arr2[2]; //不允许,不能指向其他位置
*pc = 33.1; //不允许
指针之间的赋值比数值之间的类型要严格。
也就是不同类型指针之间不能赋值,不然会报编译错误。
比如int和double之间不能赋值,int** 和int*之间也不能相互赋值。