指针操作
指针变量的 8种基本操作
赋值: 可以把地址赋给指针。用数组名、 带地址运算符(&) 的变量名、 另一个指针进行赋值。
解引用: *运算符给出指针指向地址上储存的值。
取址: 和所有变量一样, 指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。
指针与整数相加: 可以使用+运算符把指针与整数相加, 或整数与指针相加。无论哪种情况, 整数都会和指针所指向类型的大小(以字节为单位)相乘, 然后把结果与初始地址相加。
递增指针: 递增指向数组元素的指针可以让该指针移动至数组的下一个元素。ptr1++相当于把ptr1的值加上4(我们的系统中int为4字节) ,ptr1指向urn[1]
指针减去一个整数: 可以使用-运算符从一个指针中减去一个整数。 指针必须是第1个运算对象, 整数是第 2 个运算对象。 该整数将乘以指针指向类型的大小(以字节为单位) , 然后用初始地址减去乘积。
递减指针: 当然, 除了递增指针还可以递减指针。
指针求差: 可以计算两个指针的差值。 通常, 求差的两个指针分别指向同一个数组的不同元素, 通过计算求出两元素之间的距离。 差值的单位与数组类型的单位相同。
使用关系运算符可以比较两个指针的值, 前提是两个指针都指向相同类型的对象。
这里的减法有两种。 可以用一个指针减去另一个指针得到一个整数, 或者用一个指针减去一个整数得到另一个指针。
编译器不会检查指针是否仍指向数组元素。 C 只能保证指向数组任意元素的指针和指向数组后面第 1 个位置的指针有效。 但是, 如果递增或递减一个指针后超出了这个范围, 则是未定义的。 另外, 可以解引用指向数组任意元素的指针。 但是, 即使指针指向数组后面一个位置是有效的, 也能解引用这样的越界指针。
创建一个指针时, 系统只分配了储存指针本身的内存, 并未分配储存数据的内存。 因此,在使用指针之前, 必须先用已分配的地址初始化它。
保护数组中的数据
传递int类型的值还是传递指向int的指针。 通常都是直接传递数值, 只有程序需要在函数中改变该数值时, 才会传递指针。如果一个函数按值传递数组, 则必须分配足够的空间来储存原数组的副本, 然后把原数组所有的数据拷贝至新的数组中。 如果把数组的地址传递给函数, 让函数直接处理原数组则效率要高。
处理数组的函数通常都需要使用原始数据, 因此这样的函数可以修改原数组。
add_to(prices, 100, 2.50);
该函数修改了数组中的数据。函数通过指针直接使用了原始数据。
对形式参数使用const
如果函数的意图不是修改数组中的数据内容, 那么在函数原型和函数定义中声明形式参数时应使用关键字const。
int sum(const int ar[], int n); /* 函数原型 */
int sum(const int ar[], int n) /* 函数定义 */
{
int i;
int total = 0;
for( i = 0; i < n; i++)
total += ar[i];
return total;
}
const告诉编译器, 该函数不能修改ar指向的数组中的内容。 如果在函数中不小心使用类似ar[i]++的表达式, 编译器会捕获这个错误, 并生成一条错误信息。
这样使用const并不是要求原数组是常量, 而是该函数在处理数组时将其视为常量, 不可更改。使用const可以保护数组的数据不被修改, 就像按值传递可以保护基本数据类型的原始值不被改变一样。
一个函数显示数组的内容, 另一个函数给数组每个元素都乘以一个给定值。 因为第1个函数不用改变数组, 所以在声明数组形参时使用了const; 而第2个函数需要修改数组元素的值, 所以不使用const。
comst的其他内容
const double PI = 3.14159;
#define指令可以创建类似功能的符号常量, 但是const的用法更加灵活。 可以创建const数组、 const指针和指向const的指针。
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * pd = rates; // pd指向数组的首元素
第2行代码把pd指向的double类型的值声明为const, 这表明不能使用pd来更改它所指向的值
*pd = 29.89; // 不允许
pd[2] = 222.22; //不允许
rates[0] = 99.99; // 允许, 因为rates未被const限定
无论是使用指针表示法还是数组表示法, 都不允许使用pd修改它所指向数据的值。 但是要注意, 因为rates并未被声明为const, 所以仍然可以通过rates修改元素的值。
指向 const 的指针通常用于函数形参中, 表明该函数不会使用指针改变数据。
关于指针赋值和const需要注意一些规则。 首先, 把const数据或非const数据的地址初始化为指向const的指针或为其赋值是合法的
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double locked[4] = {0.08, 0.075, 0.0725, 0.07};
const double * pc = rates; // 有效
pc = locked; //有效
pc = &rates[3]; //有效
只能把非const数据的地址赋给普通指针
show_array()函数可以接受普通数组名和 const数组名作为参数, 因为这两种参数都可以用来初始化指向const的指针
show_array(rates, 5); // 有效
show_array(locked, 4); // 有效
对函数的形参使用const不仅能保护数据, 还能让函数处理const数组。
C标准规定, 使用非const标识符(如, mult_arry()的形参ar) 修改const数据(如, locked) 导致的结果是未定义的。
const还有其他的用法。 例如, 可以声明并初始化一个不能指向别处的指针, 关键是const的位置
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两次, 该指针既不能更改它所指向的地址, 也不能修改指向地址上的值。
指针和多维数组
int zippo[4][2]; /* 内含int数组的数组 */
数组名zippo是该数组首元素的地址。 在本例中, zippo的首元素是一个内含两个int值的数组, 所以zippo是这个内含两个int值的数组的地址。
zippo是数组首元素的地址, 所以zippo的值和&zippo[0]的值相同。而zippo[0]本身是一个内含两个整数的数组, 所以zippo[0]的值和它首元素(一个整数) 的地址(即&zippo[0][0]的值) 相同。
给指针或地址加1, 其值会增加对应类型大小的数值。
解引用一个指针(在指针前使用*运算符) 或在数组名后使用带下标的[]运算符, 得到引用对象代表的值。
zippo是地址的地址, 必须解引用两次才能获得原始值。 地址的地址或指针的指针是就是双重间接(double indirection) 的例子。
该输出显示了二维数组zippo的地址和一维数组zippo[0]的地址相同。 它们的地址都是各自数组首元素的地址, 因而与&zippo[0][0]的值也相同。
对二维数组名解引用两次, 得到储存在数组中的值。 使用两个间接运算符(*) 或者使用两对方括号([]) 都能获得该值(还可以使用一个*和一对[], 但是我们暂不讨论这么多情况) 。
如果程序恰巧使用一个指向二维数组的指针, 而且要通过该指针获取值时, 最好用简单的数组表示法, 而不是指针表示法。
指向多维数组的指针
int (* pz)[2]; // pz指向一个内含两个int类型值的数组
把pz声明为指向一个数组的指针, 该数组内含两个int类型值。[]的优先级高于*
int * pax[2]; // pax是一个内含两个指针元素的数组, 每个元素都指向int的指针。
由于[]优先级高, 先与pax结合, 所以pax成为一个内含两个元素的数组。 然后*表示pax数组内含两个指针。这行代码声明了两个指向int的指针。 而前面有圆括号, *先与pz结合, 因此声明的是一个指向数组(内含两个int类型的值) 的指针。
pz是一个指针, 不是数组名, 但是也可以使用 pz[2][1]这样的写法。 可以用数组表示法或指针表示法来表示一个数组元素, 既可以使用数组名, 也可以使用指针名。
指针的兼容性
指针之间的赋值比数值类型之间的赋值要严格。编译器在编译代码时, 可能会给出警告, 执行这样的代码是未定义的。
const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1; // 允许, 但是这导致const限定符失效(根据第1行代码,不能通过*pp2修改它所指向的内容)
*pp2 = &n; // 有效, 两者都声明为const, 但是这将导致p1指向n(*pp2已被修改)
*p1 = 10;//有效, 但是这将改变n的值(但是根据第3行代码, 不能修改n的值)
标准规定了通过非const指针更改const数据是未定义的。
C const和C++ const
C和C++中const的用法很相似, 但是并不完全相同。 区别之一是,C++允许在声明数组大小时使用const整数, 而C却不允许。 区别之二是,C++的指针赋值检查更严格
const int y;
const int * p2 = &y;
int * p1;
p1 = p2; // C++中不允许这样做, 但是C可能只给出警告
C++不允许把const指针赋给非const指针。 而C则允许这样做, 但是如果通过p1更改y, 其行为是未定义的。
函数和多维数组
如果要编写处理二维数组的函数, 首先要能正确地理解指针才能写出声明函数的形参。
void somefunction( int pt[][4] );
第1个方括号是空的。 空的方括号表明pt是一个指针。 这样的变量稍后可以用作相同方法作为junk。
程序把数组名junk(即, 指向数组首元素的指针, 首元素是子数组) 和符号常量ROWS(代表行数3) 作为参数传递给函数。 每个函数都把ar视为内含数组元素(每个元素是内含4个int类型值的数组) 的数组。 列数内置在函数体中, 但是行数靠函数传递得到。
ar和main()中的junk都使用数组表示法。 因为ar和junk的类型相同, 它们都是指向内含4个int类型值的数组的指针。
编译器会把数组表示法转换成指针表示法。int sum2(int ar[][4], int rows); // 有效声明。
表示ar指向一个内含4个int类型值的数组(在我们的系统中, ar指向的对象占16字节) , 所以ar+1的意思是“该地址加上16字节”。
变长数组
变长数组有一些限制。 变长数组必须是自动存储类别, 这意味着无论在函数中声明还是作为函数形参声明, 都不能使用static或extern存储类别说明符。而且, 不能在声明中初始化它们。 最终,C11把变长数组作为一个可选特性, 而不是必须强制实现的特性。
变长数组不能改变大小
变长数组中的“变”不是指可以修改已创建数组的大小。 一旦创建了变长数组, 它的大小则保持不变。 这里的“变”指的是: 在创建数组时, 可以使用变量指定数组的维度。
在函数定义的形参列表中声明的变长数组并未实际创建数组。 和传统的语法类似, 变长数组名实际上是一个指针。 这说明带变长数组形参的函数实际上是在原始数组中处理数组, 因此可以修改传入的数组。
数组的大小必须是给定的整型常量表达式, 可以是整型常量组合,C实现可以扩大整型常量表达式的范围, 所以可能会允许使用const, 但是这种代码可能无法移植。
C99/C11 标准允许在声明变长数组时使用 const 变量。 所以该数组的定义必须是声明在块中的自动存储类别数组。
变长数组还允许动态内存分配, 这说明可以在程序运行时指定数组的大小。 普通 C数组都是静态内存分配, 即在编译时确定数组的大小。
复合字面量
字面量是除符号常量外的常量。对于数组, 复合字面量类似数组初始化列表, 前面是用括号括起来的类型名。
初始化有数组名的数组时可以省略数组大小, 复合字面量也可以省略大小, 编译器会自动计算数组当前的元素个数。
因为复合字面量是匿名的, 所以不能先创建然后再使用它, 必须在创建的同时使用它。 使用指针记录地址就是一种用法。
复合字面量的类型名也代表首元素的地址, 所以可以把它赋给指向int的指针。
复合字面量是提供只临时需要的值的一种手段。 复合字面量具有块作用域这意味着一旦离开定义复合字面量的块, 程序将无法保证该字面量是否存在。 也就是说, 复合字面量的定义在最内层的花括号中。
关键概念
数组用于储存相同类型的数据。 C 把数组看作是派生类型, 因为数组是建立在其他类型的基础上。 也就是说, 无法简单地声明一个数组。 在声明数组时必须说明其元素的类型, 如int类型的数组、 float类型的数组, 或其他类型的数组。 所谓的其他类型也可以是数组类型, 这种情况下, 创建的是数组的数组(或称为二维数组) 。
在把数组名作为实际参数时, 传递给函数的不是整个数组, 而是数组的地址(因此, 函数对应的形式参数是指针) 。数组地址提供了“地址”, “元素个数”可以内置在函数中或作为单独的参数传递。
数组和指针的关系密切, 同一个操作可以用数组表示法或指针表示法。它们之间的关系允许你在处理数组的函数中使用数组表示法, 即使函数的形式参数是一个指针, 而不是数组。
本章小结
数组是一组数据类型相同的元素。 数组元素按顺序储存在内存中, 通过整数下标(或索引) 可以访问各元素。 在C中, 数组首元素的下标是0, 所以对于内含n个元素的数组, 其最后一个元素的下标是n-1。
C把数组名解释为该数组首元素的地址。 换言之, 数组名与指向该数组首元素的指针等价。 概括地说, 数组和指针的关系十分密切。
C 语言, 不能把整个数组作为参数传递给函数, 但是可以传递数组的地址。 然后函数可以使用传入的地址操控原始数组。 如果函数没有
修改原始数组的意图, 应在声明函数的形式参数时使用关键字const。 在被调函数中可以使用数组表示法或指针表示法, 无论用哪种表示法, 实际上使用的都是指针变量。
指针加上一个整数或递增指针, 指针的值以所指向对象的大小为单位改变。
C 语言传递多维数组的传统方法是把数组名(即数组的地址) 传递给类型匹配的指针形参。 声明这样的指针形参要指定所有的数组维度, 除了第1个维度。 传递的第1个维度通常作为第2个参数。
变长数组提供第2种语法, 把数组维度作为参数传递。