C_Primer_Plus10.array_and_pointer
数组和指针
- 要点
static
&,*(一元)
创建并初始化数组
指针和数组的关系
编写处理数组的函数
二维数组
- 把模块化编程的优势应用到数组
- 讲明白数组和指针的关系
数组
数组是由数据类型相同的一系列元素组成。普通变量可以使用的类型,数组元素都可以用
自动存储类别:普通变量,相对静态存储类别而言。
静态存储类别存储在静态存储区,生存周期是整个程序的生存周期。其值是在程序编译时初始化,程序运行时不执行初始化操作。因此函数中的静态变量在改变其值后会一直保留,再次调用函数时静态变量为上次调用结束时的状态。
自动存储类别为默认类别,生存周期一般是当前代码块。函数中的自动变量的初始化发生在函数调用时,每次调用都会执行初始化,若未指定初始化值,则其值不固定,随着分配到不同的内存地址而变化。
还有一种常见存储类型是动态存储类型,是 new 或 malloc 分配的区域,(一般在堆区),调用 delete 或 free 来手动释放内存。生命周期一般与自动类型相同
另外,还有其他几种存储类型:寄存器(register),外部链接(extern)。义如其名,寄存器类型一般放在寄存器中,常用于经常使用的变量,当寄存器数量不够时,会存储在内存中。而现在的编译器一般会自动识别出常用变量,所以一般不需要声明为寄存器类型。外部链接一般用于跨文件访问变量。
初始化数组时,若初始化列表中的值少于数组元素个数时,编译器会把剩余的元素都初始化为0.也就是说,如果不初始化数组,数组元素和未初始化的普通变量一样,存储的都是垃圾值,如果是部分初始化数组,剩余的元素就会被初始化为0.
int a[4] = {5,12};
但是如果初始化列表项数多余数组元素个数,编译器会将其视为错误。此时可以省略数组元素个数,让编译器自动匹配数组大小和初始化列表中的项数。
const int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
指定初始化器
designated initializer, C99 特性。可以只初始化数组中的指定元素,这样做的好处是可以跳过前面的元素,直接初始化某个位置的元素:
int arr[6] = [[5] = 212]; // 0, 0, 0, 0, 0, 212
int days[MONTHS] = {31, 28, [4] = 30, 31, 30, 31, [1] = 29}; // 31,29,0,0,30,31,30,31, 0, 0, 0, 0
int staff[] = {1, [3] = 4, 9, 10}; // 1, 0, 0, 4, 9, 10
给数组元素赋值
使用中括号下标的方式访问数组元素,可以对其赋值。
C 不允许把数组作为一个单元赋给另一个数组变量。除了初始化以外,不允许使用花括号列表的形式赋值。
int a[4] = {1, 2, 3};
int b[4];
b = a; // 不允许
b[4] = a[4]; // 下标越界
b[4] = {4,5,6}; // 不起作用
在 C 中,编译器不会检查数组下标是否使用正确。
C 未定义使用越界下标的结果。
这意味着程序看上去可以运行,但是运行结果很奇怪,或异常中止。
若对越界元素进行操作,可能会导致其他变量的值发生变化。
C 相信程序员能编写正确的代码,这样的程序运行速度更快,但不是所有程序员都能做到这一点,所以就出现了下标越界的问题。
所以最好是在声明数组时使用符号常量来表示数组的大小。
变长数组
VLA (variable-length array)
用于指定数组长度的量可以是变量,而不必是常量。
这是 C99 标准,而后 C11 放弃了这一创新举措,把 VLA 设定为了可选,而不是语言必备特性。
int n = 5;
int a[n];
多维数组
float rain[5][12];
数组的存储是顺序存储,先存储第一个12个元素的数组,然后是第二个12个元素的数组。。。多维数组的内花括号可以省略,只保留最外层花括号,这样的话,编译器会逐行赋值,如果元素数不够用,则剩余的值初始化为0.
初始化
const float rain[YEARS][MONTHS] =
{
{ 4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6 },
{ 8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.3 },
{ 9.1, 8.5, 6.7, 4.3, 2.1, 0.8, 0.2, 0.2, 1.1, 2.3, 6.1, 8.4 },
{ 7.2, 9.9, 8.4, 3.3, 1.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 6.2 },
{ 7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 5.2 }
};
- 如果某个子数组元素个数不够,则默认初始化为0
- 若数量超出,则出错,但不会影响其他行的初始化
- 初始化时也可只保留最外面一对花括号
- 如果初始化的数值不够,按照先后顺序逐行初始化,知道用完所有的值,后面没有值的元素统一初始化为0
- 初始化时第一个方括号中的元素数可以省略,后面的不能省略
int ar[][3]
多维数组
int box[3][4][5];
指针和数组
计算机的硬件指令非常依赖地址,所以指针在某种程度上把程序员想要传达的指令以更接近机器的方式表达。因此,使用指针的程序更有效率
指针能够有效地处理数组,使用数组其实就是变相的使用指针
数组名实际上是数组首元素的地址:
arr == &arr[0]; // true
两者都表示数组首元素的内存地址,都是常量,程序运行中不会改变,但可以把他们赋值给指针变量。
利用指针遍历数组
#include <stdio.h>
#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");
for (index = 0; index < SIZE; ++index){
printf("pointers + %d: %10p %10p\n", index, pti+index, ptf+index);
}
return 0;
}
output:
short double
pointers + 0: 0x7ffc7c7dba20 0x7ffc7c7dba30
pointers + 1: 0x7ffc7c7dba22 0x7ffc7c7dba38
pointers + 2: 0x7ffc7c7dba24 0x7ffc7c7dba40
pointers + 3: 0x7ffc7c7dba26 0x7ffc7c7dba48
地址是用16进制表示的,按照字节编址,short 类型占用2个字节16位,double 类型是8字节64位。在C中,指针加1表示增加一个存储单元。对数组而言,就是指向下一个元素地址,而不是指向下一个字节的地址。这是为什么必须声明指针所指向对象类型的原因之一。只知道地址还不够,还要知道数据类型,这样系统才能知道存储对象需要多少字节(即使指针指向的是标量变量,也要知道变量的类型,否则 *pt 就无法正确地取回地址上的值)。
- 指针的值是它所指向对象的地址。
- 地址的表示方式依赖于计算机内部的硬件。许多计算机都是按字节编址。
- 一个较大对象的地址通常是该对象第一个字节的地址。
- 指针前使用 * 运算符可以获得该指针指向对象的值
- 指针加1,指针的值递增它所指向类型的大小
使用指针的灵活性:
dates + 2 == &dates[2]; // 地址相同
*(dates + 2); // 等价于 dates[2]
*dates + 2; // 先获得 dates[0]的值,再加2; * 运算符优先级高于 +
函数、数组和指针
比如,写一个函数,求一个数组的元素之和,可以把数组的首元素指针传递给函数:
int sum(int * arr);
但函数不知道数组的元素个数,有两种方法处理元素个数的问题。一是在函数中写固定的数组大小;另一种是加一个参数,指定数组元素个数:
int sum(int * arr, int n);
由于数组名本质上是一个指针,而且函数原型可以省略参数名,所以可以写成如下形式:
int sum(int *, int);
int sum(int arr[], int);
int sum(int [], int);
函数的定义以下两种形式等价:
int sum(int * arr, int n){
// some code
}
int sum(int arr[], int n){
// some code
}
使用指针形参
如上求和函数,可以传递两个指针:
int sump(int * start, int * end) {
int total = 0;
while (start < end) {
total += * start++;
}
}
*和++优先级相同,但结合律是从右往左,所以先计算 start++,再取值
虽然这种写法比较常用,但写成*(start++)更清楚
使用该函数:
total = sump(arr, arr + SIZE);
这种写法既不简洁也不好记,而且很容易导致编程错误。因为虽然 C 保证了 arr[SIZE] 有效,但并不保证这个位置有值,所以程序不能访问该位置。
如果 end 指向数组的最后一个元素而不是数组末尾的下一个位置,则必须这样写:
total = sump(arr, arr + SIZE - 1);
- 利用指针改变值:
int data[2] = {100, 200};
int * p;
p = data;
(* p)++;
*p; // 101
ps: 另一种遍历数组的方法:
int sum(int arr[]){
int len = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < len; ++i){
// some code
}
}
也可以用指针代替:
int sum(int * pt){
int len = sizeof(pt) / sizeof(*pt);
for (int i = 0; i < len; ++i){
// some code
}
}
指针操作
- 赋值:把数组名、带地址运算符的变量名、另一个指针进行赋值
- 解引用:
*运算符,比如:*pt - 取值:
&a - 指针与整数相加相减:
pt+4等价于arr[4] - 指针递增递减:增加对应的数据类型占用的字节数
- 指针求差:得到的是字节数除以单个元素占用的字节数,即元素个数
- 比较:使用关系运算符比较,前提是两个指针指向的对象类型相同
- 超出范围:编译器不会检查指针是否仍指向数组元素,C只能保证指向数组人艺元素的指针和指向数组后面第一个位置的指针有效。如果运算后指针超出范围,则是未定义的
- 打印:指针用
%p 或 %u, %lu打印,指针差用%td 或 %d, %ld打印
千万不要解引用未初始化的指针:
int *pt;
*pt = 5;
未初始化的指针,它的值是一个随机值,所以不知道 5 将存储在何处。这可能不会出什么错,也可能会擦写数据或代码,或者导致程序崩溃。
创建一个指针时,系统只分配了储存指针本身的内存,并未分配存储数据的内存。
其他类型的指针:
- 指针数组
- 函数指针
- 指向指针的指针数组
- 指向函数的指针数组
保护数组中的数据
通常,只有程序需要在函数中改变数值时,才会传递指针。而对于数组,别无选择,必须传递指针,因为这样做效率高。如果一个函数按值传递数组,则必须分配足够的空间来存储原数据的副本,然后把原数组所有的数据拷贝至新的数组中。如果把数组的地址传递给函数,让函数直接处理原数组则效率要高。
对形参使用 const
如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字 const:
int sum(const int arr[], int n){
int total = 0;
for (int i = 0; i < n; ++i){
total += arr[i];
}
return total;
}
以上代码的 const 告诉编译器,该函数不能修改 arr 指向的数组中的内容。如果在函数中不小心使用类似 arr[i]++ 的表达式,编译器会捕捉这个错误,并生成一条错误信息。
这样使用 const 并不要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。
const 的其他内容
const 数组
用 const 修饰的变量,其内容不可更改,包括数组中的元素。
const int days[MONTHS] = {31, 28, 31};
days[9] = 44; // 编译错误
指向 const 的指针
指向 const 的指针不能用于改变值:
double rates[5] = {10.3, 5.2, 3.14, 2.8, -.5, 100.1};
const double *pd = rates[1];
不能用 pd 指针来更改它所指向的值,也不能用指针加减的方式改变邻近的元素的值:
*pd = 29.8 // 不允许
pd[2] = 222.2 // 不允许
rates[0] = 99.9; // 允许,因为 rates 未被 const 限定
无论使用指针表示法还是数组表示法,都不允许使用 pd 修改它所指向数据的值。但是 rates 可以修改,而且,可以让 pd 指向别处:
pd++; // 允许
pd = &rates[3]; // 允许
指向 const 的指针通常用于函数形参中,表明该函数不会使用指针改变数据。
参数传递
可以把非 const 变量作为实参,用 const 变量作为形参,这样函数不会改变变量内部的值。
另外,不应该把 const 变量作为实参。C 标准规定,使用非 const 形参修改 const 的实参数据导致的结果是未定义的。
const 指针
定义一个不能指向别处的指针,关键是 const 关键字的位置:
double * const pc = rates;
这样的指针只能指向初始化时的位置,也不能修改它所指向的值:
pc = $rates[2]; // 不允许
*pc = 92.2; // 不允许
指针和多维数组
int zippo[4][2];
以 zippo 为例,讨论二维数组与一位数组的区别:
- 因为 zippo 是数组首元素的地址,所以 zippo 的值和 zippo[0] 的值相同。而 zippo[0] 又是一个数组,它与它的首元素 zippo[0][0] 的地址相同。即:
- zippo[0] 是一个占用一个 int 大小对象的地址
- zippo 是一个占用两个 int 大小对象的地址
- 由于这个整数和内含两个整数的数组都始于同一个地址,所以 zippo 和 zippo[0] 的值相同。
- 给指针或地址加 1,其值会增加对应类型大小的数值。因为 zippo 和 zippo[0] 代表的对象占用大小不同,zippo 占两个 int,zippo[0] 占1个 int,所以 zippo+1 和 zippo[0]+1 的值不同
- 解引用一个指针,或在数组名后加下标,得到引用对象代表的值。
*(zippo[0])等价于zippo[0][0]**zippo等价于zippo[0][0]- zippo 是地址的地址,必须解引用两次才能得到原始值。地址的地址或指针的指针是 双重间接 (double indirection) 的例子。
#include <stdio.h>
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;
}
output:
zippo = 0x7ffd035c39e0, zippo + 1 = 0x7ffd035c39e8
zippo[0] = 0x7ffd035c39e0,zippo[0] + 1 = 0x7ffd035c39e4
*zippo = 0x7ffd035c39e0, *zippo + 1 = 0x7ffd035c39e4
zippo[0][0] = 2
*zippo[0] = 2
**zippo = 2
zippo[2][1] = 3
*(* (zippo+2)+1) = 3
指向多维数组的指针
以上述二维数组 zippo 为例,如何声明一个指向一个二位数组的指针变量 pz?
pz 必须指向一个内含两个 int 类型值的数组,而不是指向一个 int 类型值:
int (* pz)[2]; // 指向一个内含两个 int 类型值的数组
pz = zippo; // pz 与 zippo 指向同一个对象 zippo[0]
pz+2; // 等价于 zippo[2]
pz[2][1]; // 等价于 zippo[2][1]
上述声明中,有个圆括号,是因为方括号 [] 的优先级比 * 高。如果没写圆括号:
int * pax[2]; // pax 是一个内含两个指针元素的数组,每个元素指向一个 int 值
由于 [] 比 * 优先级高,所以 pax 会成为一个包含两个指针元素的数组,每个元素指向一个 int 值
指针的兼容性
指针的兼容性比数据类型间的赋值要严格。例如,不用类型转换就可以把 int 类型的值赋给 double 类型的变量,但是两个类型的指针不能这样做:
int n = 5;
double x;
int * p1 = &n;
double * pd = &x;
x = n; // 隐式类型转换
pd = p1; // 编译时错误
更复杂的情况:
int * pt;
int (* pa)[3];
int ar1[2][3];
int ar2[3][2];
int ** p2;
pt = &ar1[0][0]; // 有效,都是指向 int 的指针
pt = ar1[0]; // 无效
pa = ar1; // 有效,都是指向内含3个 int 类型元素数组的指针
pa = ar2; // 无效
*p2 = ar2[0]; // 有效,都是指向 int 的指针
p2 = ar2; // 无效,p2 是指向 int 的二级指针,ar2 指向的是内含2个 int 元素数组的指针
对于 const 限制的变量和指针:
int x = 20;
const int y = 30;
int * p1 = &x;
const int * p2 = &y;
const int ** pp2;
p1 = p2; // 不安全 -- 把 const 指针赋给非 const 指针
p2 = p1; // 安全
pp2 = &p1; // 不安全 -- 嵌套指针类型赋值
把 const 指针赋给非 const 指针不安全,编译时可能会给出警告,执行这样的代码是未定义的。但把非 const 指针赋给 const 指针没问题,前提是只进行一级解引用,对于二级解引用则不安全,比如:
const int ** pp2;
int * p1;
const int n = 13;
pp2 = &p1; // 允许,但会导致 const 限定符失效
*pp2 = &n; // 有效,两者都声明为 const,但这将导致 p1 指向 n
*p1 = 10; // 有效,但这样会改变 n 的值(但 n 是 const 的)
C 标准规定了通过非 const 指针更改 const 数据是未定义的,即不同编译器对它的反应不同。比如在 terminal 中(unix)使用 gcc 编译上述代码,导致 n 的最终值是 13,但在相同系统下用 clang 来编译,n 最终的值是 10。
C const 和 C++ const 区别
用法相似,但不完全相同
1, C++ 允许在声明数组大小时使用 const 整数,而 C 却不允许const int n = 5; const int arr[n];2, C++ 的指针赋值检查更严格:
const int y; const int * p2 = &y; int * p1; p1 = p2; // C++ 不允许这样做,但 C 可能只给出警告C++ 不允许把 const 指针赋给非 const 指针,而 C 允许,但如果通过非 const 指针改变 const 指针指向的值,则其行为是未定义的
函数和多维数组
要编写处理二位数组的函数,首先要能正确理解指针才能写出声明函数的形参。而在函数体中,通常使用数组表示法进行相关操作。
声明形参方法:
void somefunction0(int (* pt)[4]); // 指向一个包含4个 int 元素数组的指针
void somefunction1(int pt[][4]); // 空的方括号表示 pt 是个指针
void somefunction2(int [][4]); // 函数声明时可省略形参
例:
#include <stdio.h>
#define ROWS 3
#define COLS 4
void sum_col(int (* )[COLS], int);
void sum_row(int [][COLS], int);
void sum_all(int [][COLS], int);
int main(void){
int junk[ROWS][COLS] = {
{2,4,6,8},
{3,5,7,9},
{12,10,8,6}
};
sum_row(junk, ROWS);
sum_col(junk, ROWS);
sum_all(junk, ROWS);
return 0;
}
void sum_row(int pt[][COLS], int rows){
int sum;
for (int i = 0; i < rows; ++i){
sum = 0;
for (int j = 0; j < COLS; ++j){
sum += pt[i][j];
}
printf("sum of row %d is %d\n", i, sum);
}
}
void sum_col(int (* pt)[COLS], int rows){
int sum;
for (int i = 0; i < COLS; ++i){
sum = 0;
for (int j = 0; j < rows; ++j){
sum += pt[j][i];
}
printf("sum of col %d is %d\n", i, sum);
}
}
void sum_all(int (*pt)[COLS], int rows){
int sum;
for (int i = 0; i < rows; ++i){
for (int j = 0; j < COLS; ++j){
sum += pt[i][j];
}
}
printf("sum of pt is %d\n", sum);
}
output:
sum of row 0 is 20
sum of row 1 is 24
sum of row 2 is 36
sum of col 0 is 17
sum of col 1 is 19
sum of col 2 is 21
sum of col 3 is 23
sum of pt is 103
函数声明中传递二维数组时,最后维度的数量不能空着:
int sum1(int ar[][], int rows); // 错误的声明
int sum2(int ar[3][4], int rows); // 有效,但3会被省略
sum2 中的 ar 指向一个内含4个 int 类型值的数组,ar+1 表示该地址加上16个字节
使用 typedef 更加方便:
typedef int arr4[4]; // arr4 是一个内含4个 int 的数组
typedef arr4 arr3x4[3]; // arr3x4 是一个内含3个 arr4 的数组
// 以下三种声明方式相同
int sum1(arr3x4 ar, int rows);
int sum2(int ar[3][4], int rows);
int sum3(int ar[][4], int rows); // 标准形式
一般而言,声明一个指向N维数组的指针时,只能省略最左边方括号中的值:
// ar 指向一个 10*20*30 的 int 数组
int sum4d(int ar[][10][20][30], int rows);
变长数组(VLA)
为了与 FORTRAN 接轨,试图比 FORTRAN 更有竞争力,增加了该特性
C99 新增了变长数组(variable-length array, VLA),允许使用变量表示数组的维度:
int quarters = 4;
int regions = 5;
double sales[quaters][regions];
之前说到过,变长数组有一些限制,因为它的长度是不能事先确定的,所以只能动态分配空间,即自动存储类别。这意味着无论在函数中声明还是作为函数形参声明,都不能使用 static 或 extern 存储类别说明符。而且,不能在声明中初始化他们。最终 C11 把变长数组作为一个可选特性,而不是必须强制实现的特性。
另外,在声明和定义时,要注意参数的顺序,要先写维度数,再写数组变量名:
// 有效
int sum2d(int rows, int cols, int ar[rows][cols]);
// 无效:
int sum2d(int ar[rows][cols], int rows, int cols);
// 省略变量名时,用 * 代替
int sum2d(int, int, int [*][*]);
// 定义:
int sum2d(int rows, int cols, int ar[rows][cols]){
// some code
}
普通数组都是静态内存分配,即在编译时确定数组大小
变长数组允许动态内存分配,即在运行时指定数组的大小
符合字面量
compound literal
比如,5是 int 类型的字面量,'Y' 是 char 类型的字面量,"abcd" 是字符串字面量,81.2 是 double 的字面量。
数组的字面量(复合字面量):
(int [2]){10, 20};
初始化有数组名的数组时可以省略数组大小,复合字面量也可以省略,编译器会自动计算数组的元素个数:
(int []){10, 20};
因为字面量是匿名的,所以必须在创建的同时使用它,使用指针记录地址就是其中一种用法:
int * p1;
p1 = (int []){10, 20};
还有一种用法,是复合字面量的典型用法,即调用函数时使用,这样做的好处是把信息传入函数前不必先创建数组:
int total;
total = sum((int []){10,20,30}, 3);
二维数组的复合字面量:
int (* p2)[4];
p2 = (int [2][4]){{1,2,3,4}, {5,6,7,8}};
符合字面量是提供只临时需要的值的一种手段,其作用域是块,即一旦离开复合字面量所在的块,程序无法保证该字面量是否存在。
小结
编写一个函数来处理数组,有助于实现模块化。把数组名作为实际参数时,传递的不是整个数组,而是数组的地址。为了处理数组,函数必须知道从何处开始读取数据和要处理多少个数组元素。
C99/C11 新增了变长数组,可以用变量表示数组大小。这意味着变长数组的大小延迟到程序运行时才确定。
如果函数没有修改数组的意图,应在声明函数的形参时使用关键字 const。在被调函数中使用数组表示法或指针表示法,无论用哪种表示法,实际上使用的都是指针变量。数组名等同于指针,用法相同,ar[i] 和 *(ar+i) 等价(ar 是数组名或指针变量)
多维数组的形参,除了第一个维度,其他维度的元素数都要指定。传递的第一个维度元素数通常是作为第二个参数。
void display(double ar[][2], int rows);
对于变长数组,数组维度作为参数传递:
// 声明
void display(int rows, int cols, double ar[rows][cols]);
// 调用
display(5, 12, sales);
字符串本质上是字符数组,所以对于字符串的参数传递,不用传递数组大小,函数中通过检测字符串的末尾(字符串末尾是空字符串 '\0')也知道在何处停止。
关于运行时变量的存储位置
来自百度百科:
1、auto(自动的)
例:auto int a;定义的整形变量a的存储方式是自动存储的,也就是说动态的分配存储空间和释放存储空间。比如说,在一个调用函数里定义的变量,当我们调用这个函数时,CPU在动态存储区分配一个存储空间,这个存储空间用来放变量a的值,当调用函数执行完之后,CPU会释放这个存储空间(也可以这样理解,之前定义a时,CPU把一个存储空间给了a,a对这个存储空间有了使用权,其他的人无法去占用这个存储空间,当CPU释放这个存储空间时,也就意味着这个存储空间是无主的,其他的人可以占用它)。还有需要注意的是,当我们再次调用这个函数时,CPU分配的存储空间的地址并不一定是之前的地址,也就有了这样的一种情况,当我们定义一个变量时,不给它初始值,它的值是不确定的。
我们之前编写程序的时候很少用到auto定义变量,其实我们之前定义的变量前面没有加static,编译系统会默认为是auto的存储方式,都会把变量存放在动态存储区。
2、static(静态的)
例:static int a;定义的整形变量a的存储方式是静态存储的,静态局部变量是放在静态存储区内分配存储单元的,在整个程序运行期间都不释放,跟全局变量一样长期占用内存。但是静态变量和全局变量还是不一样的,比如说,静态变量只能在所定义的函数内引用,静态局部变量在函数调用结束后是仍然存在的,但不能被其他函数引用。
静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已经有了初值,以后每次调用函数时不再对其重新复制,而只是保留上次函数调用结束时的值。在定义静态局部变量时,如不赋初值,则编译时自动赋初值为0。
有时在程序设计中定义一个全局变量,只限于被所在源文件引用,而不想让其他文件引用,则可在定义全局变量前面加static。
3、register(寄存器的)
对于一些频繁使用的变量,程序在执行的过程中,每次用到这些变量的时候,都要从内存取出来,运算完了之后还要写到内存中去,循环执行的次数越多,花费的时间就越多,为提高效率,C++允许将局部变量放在CPU的寄存器上,需要用到时直接从寄存器上取出参加运算,就不用再到内存中取;
例:register int i,sum=0;
for(i=0;i<10000;i++)
sum+=i;
i和sum都是频繁使用的变量,所以将他们定义为寄存器变量。
当今的优化编译系统能够识别使用频繁的变量,从而自动将这些变量放在寄存器中,而不需要设定为register。因此,用register声明变量是不必要的。
4、extern(外部的)
要理解extern的作用,前提要对全局变量有所了解。
全局变量的作用域只限于从定义那一行开始,到文件最后一行,终究只限于所在源文件中。如若想在其他源文件引用其他源文件的全局变量,则要在其他源文件声明该变量是extern的(来自外部的,其他源文件的),这样扩大了全局变量的作用域。
例:char flag=1;
extern char falg;
赋初值是在定义时进行,外部变量声明不能赋初值。
参考:
https://jingyan.baidu.com/article/a65957f4eed8d424e67f9b2d.html
https://zhuanlan.zhihu.com/p/100158885
ex001:
int * ptr;
int torf[2][2] = {12, 14, 16};
ptr = torf[0];
*ptr 和 *(ptr+2) 分别是什么?
*ptr 是 12,*(ptr+2) 是16
ex002:
int (* ptr)[2];
int torf[2][2] = {12, 13, 14};
ptr = torf;
**ptr 和 **(ptr+1) 分别是什么?
**ptr 是 12,**(ptr+1) 是14
ex003:
声明一个指向内含字符串的数组的指针,每个字符串包含20个 char:
char * ptrs[20];
ex004:
void show(const double ar[][3], int n);
编写一个函数调用,把一个2行3列的复合字面量传递给 show() 函数。
show(int [][3]{{1,2,3},{4,5,6}}, 2);
ex02:
编写一个程序,初始化一个double类型的数组,然后把该数组的内容拷贝至3个其他数组中(在main()中声明这4个数组)。使用带数组表示法的函数进行第1份拷贝。使用带指针表示法和指针递增的函数进行第2份拷贝。把目标数组名、源数组名和待拷贝的元素个数作为前两个函数的参数。第3个函数以目标数组名、源数组名和指向源数组最后一个元素后面的元素的指针。也就是说,给定以下声明,则函数调用如下所示:
double source[5] = {1.1, 2.2, 3.3, 4.4, 5.5};
double target1[5];
double target2[5];
double target3[5];
copy_arr(target1, source, 5);
copy_ptr(target2, source, 5);
copy_ptrs(target3, source, source + 5);
#include <stdio.h>
void copy_arr(double [], const double [], int);
void copy_ptr(double *, const double *, int);
void copy_ptr2(double *, const double *, const double *);
void show(const double *, int);
int main(void){
double source[5] = {1.1, 2.2, 3.3, 4.4, 5.5};
double target1[5];
double target2[5];
double target3[5];
copy_arr(target1, source, 5);
copy_ptr(target2, source, 5);
copy_ptr2(target3, source, source+5);
printf("target1: ");
show(target1, 5);
printf("target2: ");
show(target2, 5);
printf("target3: ");
show(target3, 5);
return 0;
}
void copy_arr(double target[], const double source[], int len){
while (--len >= 0){
target[len] = source[len];
}
}
void copy_ptr(double * target, const double * source, int len){
for (int i = 0; i < len; ++i){
* (target++) = * (source++);
}
}
void copy_ptr2(double * target, const double * source, const double * end){
while (source < end){
* (target++) = * (source++);
}
}
void show(const double * ar, int len){
for (int i = 0; i < len; ++i){
printf("%10.2f", ar[i]);
}
printf("\n");
}
output:
target1: 1.10 2.20 3.30 4.40 5.50
target2: 1.10 2.20 3.30 4.40 5.50
target3: 1.10 2.20 3.30 4.40 5.50
ex05:
编写一个函数,返回储存在double类型数组中最大值和最小值的差值,并在一个简单的程序中测试该函数。
#include <stdio.h>
double calc_max_min(const double *, int);
int main(void){
double ar[] = {1, 2.2, 3.3, 4.4, -9, 100.2};
double max_min = calc_max_min(ar, sizeof(ar)/sizeof(ar[0]));
printf("max - min: %.2f\n", max_min);
return 0;
}
double calc_max_min(const double * ar, int len){
double max = ar[0];
double min = max;
for (int i = 0; i < len; ++i){
if (ar[i] > max){
max = ar[i];
}
if (ar[i] < min){
min = ar[i];
}
}
return max-min;
}
output:
max - min: 109.20
ex06:
编写一个函数,把double类型数组中的数据倒序排列,并在一个简单的程序中测试该函数。
#include <stdio.h>
void inverse(double *, int);
void show(const double *, int);
int main(void){
double ar[] = {1, 2.2, 3.3, 4.4, -9, 100.2};
int len = sizeof(ar)/sizeof(ar[0]);
printf("before: ");
show(ar, len);
inverse(ar, len);
printf("after: ");
show(ar, len);
return 0;
}
void inverse(double * pt, int len){
int loop = len / 2;
double tmp;
for (int i = 0; i < loop; ++i){
tmp = pt[i];
pt[i] = pt[len-i-1];
pt[len-i-1] = tmp;
}
}
void show(const double * ar, int len){
for (int i = 0; i < len; ++i){
printf("%10.2f", ar[i]);
}
printf("\n");
}
output:
before: 1.00 2.20 3.30 4.40 -9.00 100.20
after: 100.20 -9.00 4.40 3.30 2.20 1.00
ex08:
使用编程练习2中的拷贝函数,把一个内含7个元素的数组中第3~第5个元素拷贝至内含3个元素的数组中。该函数本身不需要修改,只需要选择合适的实际参数(实际参数不需要是数组名和数组大小,只需要是数组元素的地址和待处理元素的个数)。
#include <stdio.h>
void copy_ptr(double *, const double *, int);
void show(const double *, int);
int main(void){
double ar[] = {1, 2.2, 3.3, 4.4, -9, 100.2, 6.7};
double ar1[3];
int len = 3;
copy_ptr(ar1, &ar[2], len);
show(ar1, len);
return 0;
}
void copy_ptr(double * target, const double * source, int len){
for (int i = 0; i < len; ++i){
* (target++) = * (source++);
}
}
void show(const double * ar, int len){
for (int i = 0; i < len; ++i){
printf("%10.2f", ar[i]);
}
printf("\n");
}
output:
3.30 4.40 -9.00
ex09:
编写一个程序,初始化一个double类型的3×5二维数组,使用一个处理变长数组的函数将其拷贝至另一个二维数组中。还要编写一个以变长数组为形参的函数以显示两个数组的内容。这两个函数应该能处理任意N×M数组(如果编译器不支持变长数组,就使用传统C函数处理N×5的数组)。
#include <stdio.h>
void copy2d(int rows, int cols, double [rows][cols], const double [*][*]);
void show2d(int, int, const double ar[*][*]);
int main(void){
double ar[3][5] = {
{1.1, 2.2, 3.3, 4.4, 5.5},
{1, 2, 3},
{11., 21, -3.3, 14.4, -5.5},
};
double ar1[3][5];
int rows = sizeof(ar) / sizeof(ar[0]);
int cols = sizeof(ar[0]) / sizeof(ar[0][0]);
copy2d(rows, cols, ar1, ar);
show2d(rows, cols, ar1);
return 0;
}
void copy2d(int rows, int cols, double target[rows][cols], const double (* source)[cols]){
for (int i = 0; i < rows; ++i){
for (int j = 0; j < cols; ++j){
target[i][j] = source[i][j];
}
}
}
void show2d(int rows, int cols, const double (* ar)[cols]){
for (int i = 0; i < rows; ++i){
for (int j = 0; j < cols; ++j){
printf("%10.2f", ar[i][j]);
}
printf("\n");
}
}
output:
1.10 2.20 3.30 4.40 5.50
1.00 2.00 3.00 0.00 0.00
11.00 21.00 -3.30 14.40 -5.50
ex10:
编写一个函数,把两个数组中相对应的元素相加,然后把结果储存到第 3 个数组中。也就是说,如果数组1中包含的值是2、4、5、8,数组2中包含的值是1、0、4、6,那么该函数把3、4、9、14赋给第3个数组。函数接受3个数组名和一个数组大小。在一个简单的程序中测试该函数。
#include <stdio.h>
void add(double * target, const double *, const double [], int);
void show(const double *, int);
int main(void){
double ar0[] = {1, 2, 3, 4, 5};
int len = sizeof(ar0) / sizeof(ar0[0]);
double * ar1;
ar1= (double []){7, 8, 9, 10, 11};
double target[len];
add(target, ar0, ar1, len);
show(target, len);
}
void add(double * target, const double * source0, const double * source1, int len){
for (int i = 0; i < len; ++i){
target[i] = source0[i] + source1[i];
}
}
void show(const double * ar, int len){
for (int i = 0; i < len; ++i){
printf("%10.2f", ar[i]);
}
printf("\n");
}
output:
8.00 10.00 12.00 14.00 16.00
ex14:
编写一个程序,提示用户输入3组数,每组数包含5个double类型的数(假设用户都正确地响应,不会输入非数值数据)。该程序应完成下列任务。
a.把用户输入的数据储存在3×5的数组中
b.计算每组(5个)数据的平均值
c.计算所有数据的平均值
d.找出这15个数据中的最大值
e.打印结果
每个任务都要用单独的函数来完成(使用传统C处理数组的方式)。完成任务b,要编写一个计算并返回一维数组平均值的函数,利用循环调用该函数3次。对于处理其他任务的函数,应该把整个数组作为参数,完成任务c和d的函数应把结果返回主调函数。
#include <stdio.h>
#include <stdbool.h>
void read2d(int, int, double [*][*]);
void calc_mean0(int, int, const double [*][*], double *);
double calc_mean1(int, int, const double [*][*]);
double calc_max1(int, int, const double [*][*]);
void show2d(int, int, const double [*][*]);
void show(const double *, int);
int main(void){
int rows = 3, cols = 5;
double ar[rows][cols];
read2d(rows, cols, ar);
show2d(rows, cols, ar);
double mean[cols];
calc_mean0(rows, cols, ar, mean);
double mean1 = calc_mean1(rows, cols, ar);
double max = calc_max1(rows, cols, ar);
show(mean, cols);
printf("mean: %.5f\n", mean1);
printf("max: %.5f\n", max);
return 0;
}
void read2d(int rows, int cols, double ar[rows][cols]){
printf("input 15 numbers for 3*5 array:\n");
int i = 0, j = 0;
for (; i < rows; ++i){
for (j = 0; j < cols; ++j){
scanf("%lf", &ar[i][j]);
while('\n' != getchar()){
continue;
}
}
}
}
void calc_mean0(int rows, int cols, const double ar[rows][cols], double mean[cols]){
for (int i = 0; i < cols; ++i){
for (int j = 0; j < rows; ++j){
mean[i] += ar[j][i] / (rows*1.);
}
}
}
double calc_mean1(int rows, int cols, const double ar[rows][cols]){
double mean;
for (int i = 0; i < rows; ++i){
for (int j = 0; j < cols; ++j){
mean += ar[i][j] / (rows*cols*1.);
}
}
return mean;
}
double calc_max1(int rows, int cols, const double ar[rows][cols]){
double max;
for (int i = 0; i < rows; ++i){
for (int j = 0; j < cols; ++j){
if (max < ar[i][j]){
max = ar[i][j];
}
}
}
return max;
}
void show2d(int rows, int cols, const double (* ar)[cols]){
for (int i = 0; i < rows; ++i){
for (int j = 0; j < cols; ++j){
printf("%10.2f", ar[i][j]);
}
printf("\n");
}
}
void show(const double * ar, int len){
for (int i = 0; i < len; ++i){
printf("%10.2f", ar[i]);
}
printf("\n");
}
output:
input 15 numbers for 3*5 array:
1
2
3
4
5
9
8
7
6
5
3
4
5
6
7
1.00 2.00 3.00 4.00 5.00
9.00 8.00 7.00 6.00 5.00
3.00 4.00 5.00 6.00 7.00
4.33 4.67 5.00 5.33 5.67
mean: 5.00000
max: 9.00000

浙公网安备 33010602011771号