C指针系列二
一、函数调用——传址
函数调用中将数组作为参数进行传递,此时,它和指针的关系变得很微妙。我们应该要弄明白调用时到底传递的是什么。首先分析下面的简单代码:
1 int array[5] = {1, 2, 3, 4, 5};
2 void print( int a[] ) {
3 int i = 0;
4 for(; i < sizeof(a)/sizeof(a[0]); ++i)
5 printf(“%d ”, a[i]);
6 }
7 int main()
8 {
9 print(array);
10 return 0;
11 }
调用print(array)时打印出来的结果是1(而我们的预想结果是1 2 3 4 5), n = sizeof(a)/sizeof(a[0])是一种常用的表示数组元素个数的方法。之所以没有打印预料的结果,是因为将数组作为实参调用函数时,所做的工作仅仅是在栈中开辟一个4字节的空间,用来存放数组首元素地址的副本。故此时的a仅是个指向array[0]的指针,sizeof(a) /sizeof(a[0])=1,ok…问题解释完毕!!!
刚才啰嗦的半天得到的结论是:数组作为实参传递时,传递的只是一个指针(数组首元素地址的副本),不管你的函数形式是如下哪一种:
void print( int a[] ) void print( int *a )---------两种形式等价
这就决定了我们在C/C++中传递数组时必须带上一个n(表示数组元素个数),故函数print正确的形式是void print( int *a, int n)或void print(int a[], int n)。
谈到这里我们马上想到了一个例外——如果我们想将一个字符串中的所有字符转换为大写的(这里假设字符串的字符全是小写字符),代码应该是这样:
1 void UpperCase( char str[] ){
2 int i = 0;
3 while( str[i] != ‘\0’ )
4 str[i++] -= (‘a’-‘A’);
5 }
这里没有添加一个形参n去表示字符串长度,难道传递的是整个数组而不是指针???其实,这里传递的也只是字符数组首元素的地址而已,之所以不需要指定数组的长度,是因为字符数组有个结尾标识符’\0’,即隐含的传递了数组大小的信息。基于这个思想我们可以修改上面的情况——假设要打印出5个童鞋的英语成绩(不可能为负,故添加-1作为结束符):
1 int array[5] = {1, 2, 3, 4, 5, -1};//末尾添加了一个标识
2 void print( int a[] ) {//这里不用传递数组大小n
3 int i = 0;
4 while( a[i] != -1)
5 printf(“%d ”, a[i]);
6 }
上面谈到的情形——将数组作为实参进行传递的时候,具体传递了什么——虽然不复杂,但还是应该弄清楚的好,至少它体现了C/C++设计思想的一个很小的部分。java和pyhton舍弃了指针,用引用替代之;java中的数组或是python中的列表都被视为对象,即它们都对数组或列表进行了封装,并提供了一些有用的属性(如数组的长度信息),加上以引用的方式进行传递,使得操作更加便利(至少不需要传递一个表示数组大小的参数)。C++中的数组是内置型,操作方式和C中的一样,故大家更倾向于使用vector。
二、指针的应用
(1)指针数组
下面先列出几个看起来有点复杂的声明,再聊聊指针数组及多级指针
int *a[5];//声明a为指针数组
int **b[5];//声明b为指针数组
int ***c[5];//声明c为指针数组
a,b和c都是指针数组,只是数组元素不同;a中的元素是指向int型变量的指针,b中的元素是指向int*型变量的指针,同理,c中的元素是指向int**型变量的指针。谈到二级指针或是更高级的指针时,很抽象的赶脚啊!!!其实多级指针没有多维数组来的抽象,不管是多少级的指针,它都只是一个4个字节的变量,用来存放其它变量的地址而已,不要将其视为一个多么可怕的潘多拉盒子。下面的赋值完全正确:
int ***p = NULL;
int *pi = &p;//ok…编译器最多就是给出一个警告,不想看到的警告的话就强制性转换吧!!!
一点也不神秘嘛。。。只要让类型匹配(逃过编译器的法眼),指针随你怎么操作,前提是你的脑子里面必须有张很清晰的内存分布图。下面摆出一个动态创建二维数组的模板程序(更高维的依此类推)
1 int main()
2 {
3 int **p = NULL;//记得初始化,Don’t be a slacker!!!
4 int i, j;
5 int m, n;
6 scanf(“%d %d”, &m, &n);//动态创建m*n二维数组
7 if((p = (int **)malloc( sizeof(int*)*m )) != NULL){//作为一个合格的程序员不要忘了这步!!!
8 for(i=0; i<m; ++i){
9 if((p[i] = (int*)malloc( sizeof(int)*n))!= NULL){
10 memset(p[i], 0, sizeof(int)*n);//如果最后的结果全是垃圾数据时应该想起这步!!!
11 }
12 }
13 for(i=0; i<m; ++i){
14 for(j=0; j<n; ++j)
15 scanf("%d", &p[i][j]);
16 }
17 //具体的数据处理操作
18 //...
19 for(i=0; i<m; ++i)
20 free(p[i]);//忘了这步真是活该没有妹子要!!!
21 free(p);
22 }
23 return 0;
24 }
(2)数组指针
先摆上一个程序题,来引出后面的话题:
1 #include <stdio.h>
2 int main( void )
3 {
4 int a[5][10];
5 printf(“%d %d\n”, a+1, &a+1 );//假设a的值是1310000!!!
6 return 0;
7 }
暂且搁置一个这个题目,下面来讨论数组指针和二维数组的关系,了解这层关系之后,使用二维数组就会容易的多。。。就从数组指针的声明形式开始吧!!!
int (*a)[5];//声明a为数组指针
int p[5][5];//作对比之用
两者都是定义了元素个数为5的数组(每个元素又是一个数组),可以将a和p视为等价, 看看下面两者的关系图。
要弄明白一个指针指向的数据类型,最好的办法是对其进行加减操作。a进行加1操作跳过的字节数=sizeof(int)*5, a加1跳过的是一个元素个数为5的数组耶,故a是一个数组指针,指向的是一个数组而不是一个int型变量;同样的方法来分析&p, p和*p。三者的值是一样的(存放的是相同的地址),但是三者代表的含义不同。&p加1操作后跳过的字节数=sizeof(int)*5*5,表示&p指向的是一个5行5列的int型二维数组;而p加1操作后跳过的字节数=sizeof(int)*5,表示p指向的是一个元素个数为5的int型一维数组(和a一样);*p加1后跳过的字节数=sizeof(int),表示*p指向的是一个int型变量。同样的数据类型可以直接赋值咯!!!ok…下面这么写绝对没有错:
int (*a)[5];
int p[5][5];
a = p;//都是指向一个拥有5个元素的int型数组
到这里就可以轻松的将上面那道程序题解出来了。。。。
遵循上面谈到的内容,考虑二维数组作为实参进行传递的时候,传递的是什么东东???传递的也是p的一个副本(也可以说是*p或&p的值的一个副本,因为三者的值都相同),告诉起始地址还不够,必须加上元素个数n,是一维的个数还是二维的个数呢???先看看下面代码之后在继续闹吧…
1 #include <stdio.h>
2 void print( int (*a)[5], int n ){//写成print( int a[][], int m, int n)行不???
3 int i, j;
4 for(i=0; i<n; ++i){
5 for(j=0; j<5; ++j)
6 printf("%d ", a[i][j]);
7 printf("\n");
8 }
9 }
10 int main()
11 {
12 int p[2][5] = {{1,2,3,4,5}, {6,7,8,9,10}};
13 print(p, 2);
14 return 0;
15 }