跳转至

2.2 Functions

Wed Lecture

  1. 函数的存在是为了可读性,可移植性,可维护性等,也可在软件工程重构过程中用于减少重复代码量与内存,提供其他模块重复调用,同时将语句抽象化
  2. 操作系统内核采用以下函数定义明确的入口点,从用户编写的代码到内核中,此类函数称为系统调用。 在 main 函数中 return 0; 就是在告诉系程序执行完毕,和早期的时钟机制有关
  3. 库封装再分发,这里教授提到 Apple 的标准库中默认隐式得帮你调用了部分 math 库所以你不用 include, 但是在 linux 需要确保每个程序都正确链接
  4. 在足够先进的操作系统中,多个正在运行的进程可以共享一个函数的单个实例, 比如 printf() - 前提是函数的代码不能被任何进程修改,并且该函数引用每个进程的不同 data 和传递给函数的参数。
  5. main 函数在正式编写时只做参数异常处理

main() should be constrained to:

  • receive and check the program's command-line arguments,
  • report errors detected with command-line arguments, and then call exit(EXIT_FAILURE)
  • call functions from main(), typically passing information requested and provided by the command-line arguments, and finally call exit(EXIT_SUCCESS) if all went well.
  • All error messages printed to the stderr stream.
  • All 'normal' output printed to the stdout stream
Application domain (a sample of) 3rd-party libraries
operating system services (files, directories, processes, inter-process communication) OS-specific libraries, e.g. glibc, System32, Cocoa
web-based programming libcgi, libxml, libcurl
data structures and algorithms the generic data structures library (GDSL)
GUI and graphics development OpenGL, GTK, Qt, wxWidgets, UIKit, Win32, Tcl/Tk
image processing (GIFs, JPGs, etc) GD, libjpeg, libpng
networking Berkeley sockets, AT&T's TLI
security, cryptography openssl, libmp
scientific computing NAG, Blas3, GNU scientific library (gsl)
concurrency, parallel and GPU programming OpenMP, CUDA, OpenCL, openLinda (thread support is defined in C11, but not in C99)

虽然 C 语言本身没有太多实现的内置方法,但仍然有大量的共享库

The datatype of a function

void

不作 return 只做 printf

#include <stdio.h>
#include <stdlib.h>

void output(char ch, int n)        
{
    for(int i=1 ; i<=n ; i=i+1) {
        printf("%c", ch);
    }
}

int main(int argc, char *argv[])
{
    output(' ', 19);
    output('*',  1);
    output('\n', 1);
    return 0;
}

extern

#include <stdio.h>
#include <stdlib.h>

extern double sqrt(double x);

float square(float x)
{
    return x * x;
}

int main(int argc, char *argv[])
{
    if(argc > 2) {
      float a, b, sum;

      a   = atof(argv[1]);
      b   = atof(argv[2]);

      sum = square(a) + square(b);
      printf("hypotenuse = %f\n",
             sqrt(sum) );
    }
    return 0;
}

header file 里并不会包含代码实现,只会 declare 有这个 sqrt 方法存在这叫 declaration,你需要自己去用 extern 重新书写其 definition 避免其默认使用 int 对待 args. double 的原因为在 Linux 中实际上我们并不会使用太多 float, 精度不够时系统一会自动进行下一级的上升

Common mistakes

  1. 函数不需要定义参数也可以运行
void backup_files( void )            
{
    .....
}
  1. 在编写 C 语言代码时,不要依赖特定的参数求值顺序,因为编译器优化以及不同架构下的优化策略,不同架构下运行的相同代码可能会导致参数求值顺序不同
int square( int a )
{
    printf("calculating the square of %i\n", a);
    return a * a;
}

void sum( int x, int y )
{
    printf("sum = %i\n", x + y );
}

....

    ....
    sum( square(3), square(4) );

以上这段代码在不同的机器下执行出来的结果不同:

    calculating the square of 3     // the output on PowerPC Macs
    calculating the square of 4
    sum = 25

or

    calculating the square of 4     // the output on Intel Macs
    calculating the square of 3
    sum = 25

static

对于 function 来说类似于 Java 的 private 禁止外部访问,私有化

#include <stdio.h>
#include <stdlib.h>

//  myfunction IS ONLY VISIBLE WITHIN THIS FILE, AND IS CALLED BY main
static void myfunction(void)
{
    static int count = 1;    //  retains its value between function calls
    int        local = 0;    //  is re-initialised on each function call

    printf("count=%i  local=%i\n", count, local);
    ++count;
    ++local;
}

//  main IS NOT DECLARED AS static BECAUSE THE OPERATING SYSTEM MUST BE ABLE TO CALL IT
int main(int argc, char *argv[])
{
    for(int i=0 ; i < 5 ; ++i) {
        myfunction();
    }
    exit(EXIT_SUCCESS);
}

这里的 count 作为静态关键字,整个生命周期只会被分配一次内存,无论函数调用多少次该变量也不会初始化,而 local 会在每次调用的时候会重新被初始化成 0 作用域为函数内部

count=1  local=0
count=2  local=0
count=3  local=0
count=4  local=0
count=5  local=0

Functions receiving a variable number of arguments

这里简要介绍了 printf 这样的参数,多个可变变量能够绑定 string 中,就必须提供 %d %s %i 这种参数

有时候我们需要给系统做日志记录的时候甚至不知道参数数据类型的时候就需要调用第三方库比如 stdarg.h 去支持

Readings

#include <stdio.h>

// This function takes no arguments and returns no value:

void hello(void)
{
    printf("Hello, world!\n");
}

int main(void)
{
    hello();  // Prints "Hello, world!"
}

function prototype 是对函数的一种声明,它向编译器提供了函数的名称、参数类型以及返回类型,但不包含函数的实际实现(函数体)。函数原型通常出现在程序的开头或头文件中,用于在函数调用之前告诉编译器函数的基本信息。

void foo();
void foo(void);  // Not the same!

以上两行完全不一样,后者向编译器表明没有参数后有效关闭了类型检查,因此推荐在空参数时使用 void 声明。

Increment & Value Copy

int a = 5;
int b = a++;

a++ 为后置自增,return 原数值后再加 1,因此 a 结果为 6,b 为 5

int a = 5;
int b = ++a;

// b = 6, a =6

++a 为前置自增,给原数值先加上 1 后再 return 该值

#include <stdio.h>

void increment(int a)
{
    a++;
}

int main(void)
{
    int i = 10;

    increment(i);

    printf("i == %d\n", i);  // 10
}

Pass by value

在 C 语言中,当你将一个变量传递给函数时,函数接收的是该变量的副本,而不是该变量的引用。这意味着在函数 increment 中,a 是 i 的副本,对 a 的任何修改不会影响 main 函数中的 i。并且 void 函数本身无法 return 任何值,因此答案为 10,要想在 void 函数的时候加 1 就要用到指针概念:

Pass by reference

#include <stdio.h>

void increment(int *a) {
    (*a)++;
}

int main(void) {
    int i = 10;

    increment(&i);  // 传递 i 的地址

    printf("i == %d\n", i);  // 现在输出是 11
}

在这个修改后的代码中,increment 函数接收 i 的地址,并直接对 i 进行操作,因此 i 的值会被真正增加,输出会是 11。