参考书籍:《C Prime Plus》
注意:
1.存储类别警告
数组可以创建成不同的存储类别,不过此处描述的数组属于自动存储类别(即在函数内部进行声明,且声明时未使用关键字static),所以必须进行初始化,否则将会生成的是随机的垃圾数据(对一些其他的存储类别,未进行初始化将会把值自动设置为0)
2.使用const声明数组
有时需要把数组设置成已读,这样程序只能从数组中检索值,不能把新值写入数组。要创建只读数组,应该用const声明和初始化数组,一旦声明后就不能给他赋值。如:const int day[3]={4,5,6};
一.数组
1.指定初始化器(C99)
可以初始化指定的数组元素,可以在初始化列表中使用带方括号的下标指明待初始化的元素,例: int arr[12]={[5]=456};其中,把arr[5]初始化为456,其他未初始化的元素都会被设置为0。
注:
1.1.如果指定初始化器后面有更多的值,那么后面的这些值将被用于初始化指定元素后面的元素。如;[4]=33,44,55,则day[5],day[6]也将分别被初始化为44和55。
1.2.如果再次初始化指定元素,那么最后的初始化将会取代之前的初始化。
2.给数组元素赋值
C不允许把数组作为一个单元赋给另一个数组,除初始化以外不允许使用花括号列表的形式赋值
,只能对应一个个赋值。
3.数组的边界
为什么编译器不会检查数组下标是否使用得当?
因为检查下标会影响运行速度,C语言有信任程序员的原则
tips:
1.初始化值的数量少于数组元素个数时,会把剩余个数都初始化为0;
2.其实,省略方括号中的数字,编译器会自动以初始化列表中的项数来确定数组的大小。
二.指针和数组
使用指针的程序更有效率:在某种程度上传达的指令更接近机器的方式
1.数组名是数组首元素的地址。
即arr是一个数组,那么
arr==&arr[0];
两者都表示数组首元素的内存地址(&为地址运算符)。两者都是常量,在程序的运行过程中不会改变。但是可以把他们赋给指针变量,可以修改指针变量的值。
假设一地址变量为ptr:
在C中,指针加1指的是增加1个存储单元——对数组而言,意味着是下一个元素的地址,而不是下一个字节的地址。这是为什么必须声明指针所指向对象类型的原因之一,这能让计算机得知存储单元占多少字节。
2.指针对较大对象的地址通常是该对象第一个字节的地址。
3.数组表示法和指针表示法可相互转换,即两者等效。
即定义arr[n]的意思是*(arr+n)。可以认为*(arr+n)的意思是“到内存的arr的位置,然后移动n个单元,检索存储那里的值”。
这里arr是数组首元素的地址,arr+n是元素arr[n]的地址
注意:不要混淆*(arr+3)和*arr+3。因为间接运算符(*)的优先等级高于+,所以:
*(arr+3) //arr第四个元素的值
*arr+3 //arr第一个元素的值加3
三.函数,数组和指针
1.尽管int * arr形式和int arr[]形式是等价的,但是,只有在函数原型和函数定义头中,才能使用后者代替前者。本质是两者的语法规则和场景限制,后者的实质是:“数组退化为指针”的特殊写法,离开函数形参场景就不合法。
2.因为函数原型可以省略参数名,则由1.可得下列原型是等价的:
int sum(int *arr, int n);
int sum(int *, int );
int sum(int arr[], int n);
int sum(int [], int );
但是,函数定义中不能省略参数名,则由1.可得下列函数定义是等价的:
int sum(int *arr, int n);
{
}
int sum(int arr[],int n);
{
}
3.已知int arr1[5]={5,6,7,8,9};那么printf("%d",sizeof arr1);输出的结果为多少?
答案是8,那是因为arr1不是数组本身,它是指向数组首元素的指针,而本电脑系统中使用的是8字节存储地址,所以结果为8。
4. 为处理数组内的值,可以选择传递开始指针和一个整型形参来表明待处理数组的元素个数的方法;也可以传递开始指针和结束指针的方法。
#include <stdio.h>
int main(void)
{
int size=5;
int arr2[5]={5,6,7,8,9};
answer=sump(arr2,arr2+size);
}
int sump(int *start,int *end)
{
int total=0;
while(start<end)
{
total+=*start;
start++;
}
return total;
}
注意:“arr+2”为数组最后一个元素的后面一个单位(已经超出了数组的范围);他的作用是标定数组的界限,仅此而已;该位置是属于未被程序显式管理的区域,任何方法都不能对此处进行访问和赋值。
5.地址的运算:total+=*start++;
一元运算符*和++的优先级相同,但是结合律是从右到左,所以
total+=*start++为先把start指向的值加到total上,再对start++;
total+=*++start为先对start++,在对改变了的start指向的值加到total上;
6.对指针表示法和数组表示法,两者都是等价的。但是,只有当arr2是指针变量时,才能使用arr2++这样的表达式。
四.指针操作
C语言提供了9种基本的不同操作。
(如果编译器不支持%p的转换说明,可以用%u或%lu代替%p;如果编译器不支持用%zd转换说明打印地址的差值,那么可以使用%d或%ld来代替)
1.赋值:注意地址应该和指针类型兼容。也就是说,不能把double类型的地址赋给指向int的指针。
2.解引用:*运算符给出指针指向地址存储的值。
3.取值:和所有变量一样,指针变量也有自己的地址和值。
4.指针与整数相加:如果相加结果超出了初始指针指向的数组范围,则计算结果是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。目的是不改变原指针的指向,一次性跳过多个元素,直接定位到目标元素。
5.递增指针:递增指向数组元素的指针可以让指针移动至数组的下一个元素。目的是改变原指针指向,每次只跳过一个元素,便于在循环中逐个遍历数组的元素。方法
6.指针减去一个整数
7.递减指针
8.指针求差:可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。例如arr数组是int类型数组,那&arr[3]-arr[0]得2,意思就是这两个指针所指向的两个元素相隔两个int,而不是2个字节。
9.比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向数组元素。
注意:无论如何,在使用指针时一定要注意,不要解引用未初始化的指针!
因为在创建指针时,系统只分配了存储指针本身的内存,并未分配存储数据的内存。
五.保护数组中的数据
基本
一般函数都是传递数值,只有程序需要在函数中改变该数值时,才会传递指针。对于数组来说,必须传递指针,因为这样效率高。
不过,传递地址会导致一些问题,因为传递地址相当于是传递原始数据,那么这样,函数就会随意更改原数组;对于一般非数组的函数而言,形参即为原始数组的副本,就不会意外更改数据
注意:因为有“数组退化为指针”的说法,所以处理数组时,通常都是使用原始数据。
事实上,是否更改原始数据需要依据实际问题来进行判断。
对形参使用const:如果函数不是意图修改数组中的内容,那么在函数原型和函数定义中声明形式参数时应尽量使用关键字const。
const的更多应用:虽然#define指令可以创建类似功能的符号常量,但是const更灵活,可以创建对应数组,指针和指向const的指针。
const限制通过地址变量来修改地址上的值。
//假设有以下代码
int art[5]={6,7,8,9,5};
const int ptr = art;
*pd = 44; //不允许
pd[2] = 55; //不允许
art[0] = 99; //允许,因为art未被const限定
pd++; //没问题,允许
这里的const是表明不能使用pd来更改它所指向的值,与通过art[]来更改值,以及更改pd指向的地址无关。
const用于形参中,表明该函数不会使用指针改变数据。
指针赋值和const的一些规则。
以防止通过指针改变const数组中的函数
1.把const数据或非const数据的地址初始化为指向const的指针或为其赋值是合法的:
int arr1[5] = {4,5,6,7,8};
const int arr2[4] = {55,66,77,88};
const int *pd = arr1; //有效
pd = arr2; //有效
pd = arr1[3]; //有效
2.然而,只能把非const数据的地址赋给普通地址:(若把const数据赋给普通指针,将会通过指针将原数据修改)
int arr1[5] = {4,5,6,7,8};
const int arr2[4] = {55,66,77,88};
int *pd = arr1; //有效
pd = arr2; //无效
pd = arr1[3]; //有效
函数传参
若把函数实参类比成数据(带const或不带const),把函数形参类比成指针(带const或不带consrt),就可以得到函数实参和形参的类似关系。

使用非const标识符(形参)修改const数据时,会导致结果时未定义的。
六.指针和多维数组
基本
为了更好的描指针和多维数组的关系,这里假定有下列声明:int zip[4][2];
数组名时该数组首元素的地址,实质上是不断“退化”的结果。由此,也就可以理解了zip[2]与zip[2][0]所指的地址的值其实是相同的,尽管前者是指一整个一维数组的地址(地址是取开头的),后者是指一个int变量。
注意:尽管zip[2]和zip[2][0]所指的地址的值是相同的,但是他们并不一样。
对zip和zip[0],尽管有如上所述的情况,但是他们有区别。我们说zip是一个占用两个int大小对象的地址;后者是占用一个int大小对象的地址。
怎么判断呢?一般来说通过zip+1及zip[0]+1来辨别,前者每次递增跳过了1个“包含2个的数组”;后者每次递增跳过了1个int。由此,我们可以通过递增的方法,来判断其是占用几个大小对象的地址。
通常,*符号和[]符号作为解引用的作用作用于多维数组中,例如:**zip与*&zip[0][0]等价。
也可易得zip必须得要解引用两次才能获得初始值。
如能很好地理解1.的话,那么就可以理解zip[2][1]等价的指针表示法是*(*(zip+2)+1)了。
zip //二维数组的首元素的地址(每个元素都是内含两个int的一维数组)
zip+2 //二维数组的第三个元素(即第三个一维数组)的地址
*(zip+2) //二维数组的第三个元素(即第三个一维数组)的首元素(一个int的值)的地址
*(zip+2)+1 //二维数组的第三个元素(即第三个一维数组)的第二个元素(也是一个int的值)的地址
*(*(zip+2)+1) //二维数组的第三个元素的第二个元素(也是一个int的值)的值,即zip[2][1]
1.指向多维数组的指针
当声明一个指针变量pz指向一个二维数组时,需要考虑声明类型,占用对象大小以及维度数。
例如:int (*pz)[2]; //pz指向一个包含两个int类型值的数组
1.1.当需要声明N维数组时,只需要在后面多加[]即可,如:int (*pz)[2][5];
1.2.以上代码中,为什么需要加()呢?是因为[]符号的运算符优先等级高于*运算符。
例如:int * pz[2]; //pz是一个内含两个指针元素的数组,每个数组都指向int指针
由于[]优先级高,先结合,所以pz成为一个内含两个元素的数组,然后*表示pz数组内含两个指针。
2.指针兼容性
指针之间的赋值比数值类型之间的赋值要严格。
2.1.不用类型转换就可以把int类型的赋值给double类型的变量,但是两个类型的指针不可以这样做。
2.2.指针间赋值时,需要注意指向相同类型。
2.3.指针的指针间的const法则,与数据和指针,实参和实参的const法则相似
注意:C++不允许把const指针赋给非const指针。而c则允许这样做,如果要以此来修改指针,这样的结果时未定义的。
3.函数与多维数组
在函数体中,通常使用数组表示法进行操作。
下列使用的表示法等价:
void sum(int arr[][cols], int a);
void sum_max(int [][cols], int );
void sum_min(int(*arr)[cols], int cows);
其中,第一个括号是空的。空的括号表明是一个指针。
其实,正如指针声明一样,数组表示法中除第一个括号是空的,其他都有对应的大小,分别对应着各维度占用对象的大小。
七.变长数组
我们希望函数能够更加普适各种情况,所以有了变长数组,允许使用变量表示数组的维度。
注意:1.变长数组有限制,他必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用static或extern存储类别说明符。并且,不能在声明中初始化他们。
2.一旦创建了变长数组,他的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用变量指定数组维度。
3.在形参列表中变量必须声明在数组前,如:int sum_2(int arr[rows][cols],int rows, int cols);——无效顺序
和传统的语法类似,变量长数组名实际上是一个指针,这说明带变长数组形参的函数实际上是在原始数组中处理数组的。
变长数组还允许动态内存分配,这说明可以在程序运行时指定数组大小。
684

被折叠的 条评论
为什么被折叠?



