C 语言函数

函数基础

#include <stdio.h>

// x 形式参数
double F(double x) {
  return x * x + x + 1;
}

double G(double x, double y, double z) {
  return x * x + y * y + z * z;
}

int main(void) {
  /*
   * <return type> <name> (<parameters>) {
   *    ... statement
   *    return <return value>;
   * }
   */
  puts("HelloWorld");

  // 2.0: 实际参数
  double result_f = F(2.0);
  double result_g = G(3.0, 4.0, 5.0);

  // Add
  // Sum
  // FindNumber

  printf("result of f: %F\n", result_f);
  printf("result of G: %F\n", result_g);

  return 0;
}

函数原型

  1. 在 C 语言的 main 函数定义中,如果未指定任何参数,如int main(){ return 0; },这表明main函数是可变参数的,意味着在调用时可以选择传递任意数量的参数,包括零个,例如 main(), main(1) , 或者 main(1, "hello")
  2. 使用 int main(void) 在C语言中明确指出 main 函数不接受任何参数,这与 C++ 的 int main()不同,在C++中后者表示不接受参数,尝试传递参数会导致编译错误。
  3. puts() 函数用于输出字符串至标准输出,它不接受格式化字符串或变量作为参数。例如,不能使用 puts("结果是%d", i); 这样的语法。正确的用法是分别使用 puts("结果是")printf("%d", i); 来输出字符串和变量。
  4. 在 C 语言中声明函数时,如果只需要指定函数的返回类型和参数类型,可以省略参数名,如 int sub(int, int);,这为函数的声明提供了足够的信息,而无需具体的参数名。
  5. 如果在 C 语言中定义函数时省略了返回值类型,如 add(int a, int b){ ... } ,编译器会根据函数体内的 return 语句推断出返回类型。如果没有 return 语句,则默认返回类型为 int
#include <stdio.h>

void EmptyParamList(void);

/*
 * 1. 函数名
 * 2. 函数返回值类型,如果没写,默认为 int
 * 3. 重要的是函数参数列表,参数类型,和参数的顺序,参数形参名不重要
 */
int Add(int, int);  // 函数声明时可以不写形参名称

int main(void) {
  puts("");
  EmptyParamList();

  int result = Add(1, 2);
  printf("result of add: %d\n", result);
  return 0;
}

void EmptyParamList(void) {
  puts("Hello");
}

变量的类型和作用域

auto 类型

在 C 语言中,使用 auto int i = 1; 声明了一个整型变量 i 并赋予初始值 1。这里的 auto 关键字并非 C 语言标准的一部分,而是某些编译器(如 GCC)中的历史遗留特性,用于声明自动存储期的变量,意味着变量的生命周期与声明它的函数或代码块一致,当函数或代码块执行完毕时,变量会被自动销毁。

值得注意的是,auto 关键字在现代C语言编程实践中很少使用,因为默认情况下,未指定存储类别的局部变量就是自动存储期的。 变量的作用域限制在它被声明的函数或代码块内,一旦退出该作用域,变量就会被销毁,无法再被访问。

至于初始值的处理,不同的编译器可能会有细微的差别。MSVC 编译器下运行,首先会报以下错误,原因是未进行初始化:

点击忽略后销毁该变量,将该地址上的值初始化为 0xcccccccc(十进制为 -858993460)。

GCC 则不会进行初始化。

静态变量类型 static

static 关键字在 C 语言中用于声明具有静态存储期的变量。与 auto 变量不同,static 变量的生命周期贯穿整个程序的运行过程,即使它们所在的函数或代码块已经执行完毕。这意味着,static 变量的内存分配在程序启动时进行,并在程序结束时释放,而不是在函数调用结束后立即销毁。

如果 static 变量未显式初始化,编译器会将其默认初始化为 0。这一点与 auto 变量的行为不同,后者的未初始化行为是未定义的,可能导致不可预测的结果。

此外,static 变量的作用域通常是全局的,但与全局变量(定义在所有函数外部的变量)不同,static 变量只在定义它们的文件内部可见,这使得它们成为实现文件内封装和信息隐藏的有效工具。

void LocalStaticVar(void) {
  // 静态变量
  // 1. 作用域全局,内存不会因函数退出而销毁
  // 2. int 初值默认为 0
  static int static_var;

  // 自动变量
  // 1. 函数、块作用域,随着函数和块退出而销毁
  // 2. 没有默认初值
  int non_static_var;

  printf("static var: %d\n", static_var++);
  printf("non static var: %d\n", non_static_var++);
}

int main(){

  int a[5] = {1,2,3,4,5};

  // TestScope(5, a);

  // TestStatic();

  LocalStaticVar();
  LocalStaticVar();
  LocalStaticVar();

  return 0;
}

GCC 输出结果如下:

0
static var: 0
non static var: 0
static var: 1
non static var: 1
static var: 2
non static var: 2

而 MSVC 输出结果如下:

0
static var: 0
non static var: -858993460
static var: 1
non static var: -858993460
static var: 2
non static var: -858993460

而对于以下代码:

void LocalStaticVar(void) {
  // 静态变量
  // 1. 作用域全局,内存不会因函数退出而销毁
  // 2. int 初值默认为 0
  static int static_var;

  // 自动变量
  // 1. 函数、块作用域,随着函数和块退出而销毁
  // 2. 没有默认初值
  int non_static_var;

  printf("static var: %d\n", static_var++);
  printf("non static var: %d\n", non_static_var++);
}

void CleanMemory() {
  int eraser = -1;
}

int main(){

  int a[5] = {1,2,3,4,5};

  // TestScope(5, a);

  // TestStatic();

  LocalStaticVar();
  CleanMemory();
  LocalStaticVar();
  CleanMemory();
  LocalStaticVar();

  return 0;
}

GCC 输出结果如下:

0
static var: 0
non static var: 0
static var: 1
non static var: -1
static var: 2
non static var: -1

块作用域

块作用域范围内的变量只在块内生效

#include <stdio.h>

int main(){
  {
    int a = 0;
  }
  printf(a);
  return 0;
}

main.c:7:10: error: ‘a’ undeclared (first use in this function)
7 | printf(a);
| ^

寄存器变量类型 register

在 C 语言中,使用 register 关键字声明的变量(例如 register int a)旨在建议编译器尽可能将该变量存储在 CPU 寄存器中,以提高访问速度。与普通变量(如 int a)相比,寄存器变量在汇编层面的操作更为直接和高效。例如,在传递参数时,普通内存变量需要经过额外的步骤:首先将值从寄存器复制到内存的特定位置(如 PTR [rbp-4]),然后再从该内存位置复制到目标寄存器(如 eax)。这一过程涉及两次数据复制,增加了延迟。

void PassByMemory(int parameter) {
  printf("%d\n", parameter);
}
PassByMemory(int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        mov     esi, eax
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        nop
        leave
        ret

而寄存器变量则避免了这一额外步骤,可以直接在寄存器之间传递值,如直接将 edi 寄存器中的值复制到 eax 寄存器,从而减少了指令周期和提高了执行效率。

void PassByRegister(register int parameter) {
  printf("%d\n", parameter);
}
PassByRegister(int):
        push    rbp
        mov     rbp, rsp
        mov     esi, edi
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        nop
        pop     rbp
        ret

函数原型作用域

在 C 语言中,函数原型如 int sort (int size, int array [size]); 展示了一种特定的变量作用域限制。这里的参数 size 仅在该函数声明的上下文中有效,意味着它不能在函数外部被访问或使用。

此外,这种声明方式在不同编译器中的行为也有所差异。在 MSVC编译器中,这样的声明是被允许的,但在 GCC 中则不被接受,并会提示错误,指出需要一个常量表达式,并且不允许数组的大小为零。

进一步地,我们可以区分两种作用域的概念:函数作用域和文件作用域。函数作用域内的参数和变量仅在该函数内部有效,而文件作用域中的变量则在整个文件的范围内都是可访问的,不受特定函数的限制。

函数的变长参数

从本质上讲,可变参数技术涉及将一系列参数作为一个连续的内存块传递给函数。函数接收到这个内存块的起始地址和参数的总数。随后,它根据每个参数的数据类型所占用的字节数 —— 例如,一个整型(int)占用 4 个字节,一个双精度浮点数(double)占用 8 个字节 —— 来解析和提取内存块中的每个参数。通过这种方式,函数能够逐一识别并处理传递给它的所有参数。

#include <stdio.h>
#include <stdarg.h>

/*
 * 实现变长参数
 */
void HandleArgs(int arg_count, ...) {

  // 1. 定义一块空间(va_list),存入所有的变长参数
  va_list args;

  // 2. 将这块空间分割为参数数目个数
  va_start(args, arg_count);

  // 3. 读取每块空间的参数
  for (int i = 0; i < arg_count; ++i) {
    int arg = va_arg(args, int);
    printf("%d: %d\n", i, arg);
  }

  // 4. 这块空间使用结束
  va_end(args);
}

int main() {
  HandleArgs(4, 1, 2, 3, 'a');
  return 0;
}

函数的递归

阶乘 n! 的递归实现:

unsigned int Factorial(unsigned int n) {

  if (n == 0) {
    return 1;
  }
  return n * Factorial(n - 1);
}

for 循环实现:

unsigned int FactorialByIteration(unsigned int n) {
    unsigned int result = 1;
    for (unsigned int i = n; i > 0; --i) {
        result *= i;
    }
    return result 
}

斐波那契数列的递归实现:

unsigned int Fibonacci(unsigned int n) {
  if (n == 0 || n == 1) {
    return n;
  }
  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

for 循环实现:

unsigned int FibonacciByIteration2(int n) {

  if (n == 0 || n == 1) {
    return n;
  }

  int last = 0;
  int current = 1;

  for (int i = 0; i <= n - 2; ++i) {
    int temp = current;
    current = current + last;
    last = temp;
  }
  return current;
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇