"C语言-指针和函数"

  "C语言基础复习三"

Posted by Xu on January 14, 2018

C语言–指针和函数

pointer_func

指针

未初始化和非法的指针:

int *a;
*a = 12;//指针a并没有被初始化,存储整数12的地址往往是一个非法地址,会出现内存错误

NULL指针

char ch = 'a';
char *cp = &ch;

表达式:

ch,&ch,cp,&cp,*cp,*cp+1,*(cp+1)这些表达式中只有表达式的值存储在确定位置才能作为左值,否则是非法的

++cp,和cp++作为左值也是非法的,因为它拷贝后的值的存储位置不能确定,其实++cp的值为内存中ch后面那个值的地址的一份拷贝,但并不知道这个拷贝存放在哪个确定的内存地址中,同样cp++也是cp的地址的一份拷贝,并不知道这个拷贝的具体内存地址。所以都不能作为左值

  • *++cp明确指向内存中ch后面的值,这个值的存储地址也就是内存变量ch后面值的地址,是确定的,可以作为左值
  • *cp++表达式的值明确指向ch存储的值,这个值的地址也就是变量ch的地址,也是确定的,可以作为左值

据此分析:++*cp,(*cp)++,++*++cp,++*cp++作为左值,由右向左的结合性可知,最后都是以++作为前缀,相当于++ex,该表达式的值就是ex自增后的一份拷贝,但该拷贝的值的存储位置不确定,所以均无法作为左值

指针运算:

  1. 算术运算只限于两种形式:

  • 指针+,-整数
  • 指针-指针:只有当两个指针都指向同一个数组,得到的结果为一种有符号整数类型,也就是数组中相差元素的个数

2. 关系运算:<,<=,>,>= 同样执行关系运算的两个指针也需要指向同一数组的元素

函数

向编译器提供函数的特定信息(参数的类型,数量及返回值类型)有两种方式:

  • 在使用该函数时,该函数在该代码之前的同一源文件中进行函数定义</br> 函数定义:</br> 类型 函数名(形参)</br> 代码块</br>

  • 函数原型:</br> 类型 函数名(形参);</br>

一种好的函数使用方式:使用函数原型,在头文件构建好函数原型,在需要使用该函数时则#include该头文件。同时函数原型需与函数定义相匹配。

int *func(void);提示没有任何参数,而不是表示它有一个类型为void的参数。

函数的缺省认定:当程序调用一个无法见到原型的函数时,编译器会缺省认为该函数返回一个整型值。这会引发错误,如

float f;
...
f = xyz();//xyz函数返回一个浮点值

由于xyz没有原型(xyz在该代码之后定义?),编译器默认xyz返回整型值,所以会将这个返回值转换为float后返回给f,比如返回3.14,转换指令会将该返回值解释为整型值1078523331,然后将该整型值转换为float返回给f。

黑盒的实现:

  • C语言实现黑盒式模块(对外只提供数据的接口,不能直接访问数据),可以通过合理使用static来限制数据的作用域,使得外部访问这部分的数据必须通过访问提供的函数接口来获取。
  • 一般的模式是使用头文件定义好黑盒函数接口的原型及数据特征
  • 然后在一源文件中#include该接口模块头文件,再通过static定义想要访问的数据,并实现访问接口,所以要访问该数据需要调用该源文件中实现的函数接口来获取

递归与迭代

递归的两大特性:

  • 存在限定条件,当满足该限定条件时递归结束
  • 每一次递归越来越接近该限定条件

示例:将一个整数以字符的形式打印出来,比如打印4267,首先将4267对10求余得到7,将7以字符的形式打印,商为426,继续对10求余…

程序:

binary_to_char(unsigned int value){
   unsigned int quotient;
   quotient = value / 10;
   if (quotient! = 0){
      binary_to_char(quotient);
   }
   putchar(value%10 + '0');
}

追踪一个递归函数的关键在于函数中声明的变量是如何存储的。当函数被调用时,它的变量的空间是创建在运行时堆栈上的,上一步递归调用的函数的变量仍保留在堆栈上,但被新调用函数的变量所掩盖,堆栈如下图所示:

Stack

递归虽然是一种强有力的技巧,但它的误用也有可能造成资源的浪费,比如计算阶乘:n!

//递归
long factorial (int n){
     if (n<=0)
         return 1;
     else return n*factorial(n-1);
}
//循环
long factorial (int n){
     int result;
     while (n>1){
        result *=n;
        n -= 1;
     }
     return result;
}

相比之下循环可能方式更直接一点,递归显得更聪明一点,但是实际上递归解决该问题浪费了更多的资源,因为每一步递归调用都需要生成新的变量,这些新的变量需要占用堆栈内存空间。而循环只有两个变量,显然造成的资源开销更小。

体会:在使用递归时,只有当递归过程中产生的结果需要保存(4267每一次递归产生的字符都参与到最后的结果需要保存),并直接参与到最后的结果时,递归显然更有效,当递归过程中的结果只是临时结果(求阶乘的每一步递归都是作为临时变量,不直接参与到结果),不需要保存时,循环会更有效果并节约资源。

分析菲波拉契数列:

       n <=1: 1
f(n)=  n = 2: 1
       n > 2: f(n-1) + f(n-2)

如果使用递归来解决这个问题话,资源的浪费就将非常严重,我们可以看到f(n-1)会冗余调用f(n-2),f(n-2)会冗余调用f(n-3)…如此下去一个函数可以能会被冗余计算很多次,比如计算f(10)时,f(3)的值被计算了21次。使用循环来解决这个问题的话,效率可以提高几十万倍!

由我之前的体会来分析,该递归每次产生的中间值都没有直接参与到最后的结果,而是一个临时的结果去对最后的结果起作用,所以不适合使用递归。这种循环其实也叫迭代。

可变参数列表

当函数的参数数量不确定时,这种可变参数列表是通过宏来实现的,这些宏定义在stdarg.h头文件,该头文件声明了一个类型va_list和三个宏va_start,va_arg,va_end。

  • va_list: 为可变参数列表
  • va_start: 函数处理可变参数列表的起始位置,该函数接受两个参数:va_list变量名和可变参数列表前的第一个参数名(也就是省略号前的第一个参数)
  • va_arg: 用于处理可变参数列表的下一个参数,该函数接受两个参数:va_list变量名和下一个参数的类型
  • va_end:当可变参数列表访问完毕后,需要调用该函数,接受可变参数列表变量为唯一参数
#include <stdarg.h>

float average(int n_values,...){
   va_list var_arg;//可变参数列表变量名
   int count;
   float sum = 0;
   va_start(var_arg,n_values);//设置可变参数列表的起始位置为参数n_values之后的所有参数
   for(count = 0;count<n_values;count++){
     sum += va_arg(var_arg,int);//访问下一个参数的值,这里所有的参数类型均为int
   }
   va_end(var_arg);//可变参数列表访问完毕,结束访问
   
}