C语言 - 学习笔记
Harvard CS50 计算机科学概论
Harvard CS50 计算机科学概论
  • Prologue
  • WEEK 0 Introduction
  • WEEK 1 C
  • WEEK 2 Arrays
  • WEEK 3 Algorithms
  • WEEK 4 Memory
  • WEEK5 Data Structures
  • WEEK6 Python
  • WEEK7 SQL
  • WEEK8 HTML, CSS, JavaScript
  • WEEK9 Flask
  • C语言总结
Powered by GitBook
On this page
  • 1. C语言程序与编译器
  • 2. 数据类型
  • 2.1 基本类型
  • 2.2 void类型
  • 2.3 stdbool.h
  • 3. 变量
  • 3.1 申明与定义
  • 3.2 const和#define
  • 3.3 储存类
  • 4. 运算符
  • 4.1 算术运算符
  • 4.2 关系运算符
  • 4.3 逻辑运算符
  • 4.4 位运算符
  • 4.5 赋值运算符
  • 4.6 其他
  • 5. 控制语句
  • 5.1 判断
  • 5.2 循环
  • 5.4 循环控制
  • 6. 函数
  • 6.1 printf()
  • 6.2 函数的申明与定义
  • 6.3 return
  • 7. 浮点数的精度
  • 8. 注释
  • 9. 作用域规则
  • 10. 变量初始化

WEEK 1 C

PreviousWEEK 0 IntroductionNextWEEK 2 Arrays

Last updated 2 years ago

需要先安装CS50的C语言库,请参考,手册请参考。

C 语言是一种通用的、面向过程式的计算机程序设计语言。1972 年,为了移植与开发 UNIX 操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言。UNIX 操作系统,C编译器,和几乎所有的 UNIX 应用程序都是用 C 语言编写的。由于各种原因,C 语言现在已经成为一种广泛使用的专业语言。

当前最新的 C 语言标准为 C18 ,在它之前的 C 语言标准有 C17、C11...C99 等。

为什么要使用 C?

C 语言最初是用于系统开发工作,特别是组成操作系统的程序。由于 C 语言所产生的代码运行速度与汇编语言编写的代码运行速度几乎一样,所以采用 C 语言作为系统开发语言。下面列举几个使用 C 的实例:

  • 操作系统

  • 语言编译器

  • 汇编器

  • 文本编辑器

  • 打印机

  • 网络驱动器

  • 现代程序

  • 数据库

  • 语言解释器

  • 实体工具

1. C语言程序与编译器

为了将此代码转换为我们的计算机可以实际运行的程序,我们需要首先将其转换为二进制(0/1)。

样例C代码 1:

#include <stdio.h>

int main(void) {
    printf("Hello World!\n");
}

C 程序主要包括以下部分:

  • 预处理器指令

  • 函数

    main函数是程序的入口,一般来说成功会返回0,失败会返回1。

  • 变量

  • 语句 & 表达式

  • 注释

所有的 C 语言程序都需要包含main()函数。 代码从main()函数开始执行。

stdio.h是一个头文件(Header File)(标准输入输出头文件),#include是一个预处理命令,用来引入头文件。 当编译器遇到printf()函数时,如果没有找到stdio.h头文件,会发生编译错误。

#include <stdio.h>表示包含标准I/O LIbrary。

Source code -> Compiler -> Machine code

编译源代码:

(base) ubuntu@hadoop-node-1:~/C/w1$ ll
total 40
-rwxr-xr-x  2 ubuntu  ubuntu    12K 21 Aug 17:17 HelloWorld
-rw-r--r--  2 ubuntu  ubuntu    68B 21 Aug 17:05 HelloWorld.c

多出来个叫做HellowWorld 的可执行文件。

(base) ubuntu@hadoop-node-1:~/C/w1$ ./HelloWorld 
Hello World!

这和Java是不同的,因为C不需要VM,C代码经过编译后会直接变成可执行文件。

样例C代码 2:

#include <stdio.h>

/*
File: hello1.c
*/

int main(void) {
    string answer = get_string("What's your name? ");
    printf("hello, %s\n", answer);
}

发现编译出错:

(base) ubuntu@hadoop-node-1:~/C/w1$ make hello1
clang -fsanitize=signed-integer-overflow -fsanitize=undefined -ggdb3 -O0 -std=c11 -Wall -Werror -Wextra -Wno-sign-compare -Wno-unused-parameter -Wno-unused-variable -Wshadow    hello1.c  -lcrypt -lcs50 -lm -o hello1
hello1.c:8:5: error: use of undeclared identifier 'string'
    string answer = get_string("What's your name? ");
    ^
hello1.c:9:27: error: use of undeclared identifier 'answer'
    printf("hello, %s\n", answer);   
                          ^
2 errors generated.
make: *** [hello1] Error 1

编译信息格式为:file:row_number:offset。

现象:

在编译工程时,有时会遇到类似ld: library not found for -lxxx的错误提示。

原因:

通常这是由于工程在编译时找不到需要的xxx链接库而导致的。

添加#include <cs50.h>后运行:

(base) ubuntu@hadoop-node-1:~/C$ make hello1
clang -fsanitize=signed-integer-overflow -fsanitize=undefined -ggdb3 -O0 -std=c11 -Wall -Werror -Wextra -Wno-sign-compare -Wno-unused-parameter -Wno-unused-variable -Wshadow    hello1.c  -lcrypt -lcs50 -lm -o hello1
(base) ubuntu@hadoop-node-1:~/C$ ./hello1
What's your name? Ray
hello, Ray

C语言默认是没有String的数据类型,只有字符数组。

C是一种非常Low-level的语言,能够精准控制计算机的硬件,使用库能够有效避免未知错误的发生。

没有Java VM屏蔽系统环境的时候,环境的选择(Linux版本,Kernel版本,clang和gcc版本)特别重要。

编译器是一种计算机程序,它将某种编程语言(高级编程语言)写成的源代码转换成另一种编程语言(低级编程语言)。一个现代编译器的主要工作流程为:源代码 -> 预处理器 -> 编译器 -> 目标代码 -> 链接器 -> 可执行程序,最后打包文件,让计算机运行。编译器的目的是将便于人写的高级编程语言作为源代码,翻译成计算机能够执行的低级机器语言,也就是可执行文件。其中高级语言有C、C++、Java、Python等,低级语言有汇编语言和机器代码。

一个编译器可以按照三段式分为:

  • 前段(Fontend)

  • 优化器(Optimizer)

  • 后端(Backend)

GCC(GNU Compiler Collection)是一套有GNU开发的编程语言编译器,以GPL及LGPL许可证所发行的自由软件。GCC原名为GNU C语言编译器,它原本只能处理C语言,随着技术的发展,GCC很快地得到扩展,变得可以处理C++,之后可以处理的语言扩展到Fortran、Pascal、Objective-C、Java等。

Unix 系统被发明之后,大家用的很爽。但是后来开始收费和商业闭源了。一个叫 RMS 的大叔觉得很不爽,于是发起 GNU 计划,模仿 Unix 的界面和使用方式,从头做一个开源的版本。然后他自己做了编辑器 Emacs 和编译器 GCC。

GNU 是一个计划或者叫运动。在这个旗帜下成立了 FSF,起草了 GPL 等。

接下来大家纷纷在 GNU 计划下做了很多的工作和项目,基本实现了当初的计划。包括核心的 gcc 和 glibc。但是 GNU 系统缺少操作系统内核。原定的内核叫 HURD,一直完不成。同时 BSD(一种 UNIX 发行版)陷入版权纠纷,x86 平台开发暂停。然后一个叫 Linus 的同学为了在 PC 上运行 Unix,在 Minix 的启发下,开发了 Linux。注意,Linux 只是一个系统内核,系统启动之后使用的仍然是gcc和bash等软件。Linus 在发布 Linux 的时候选择了 GPL,因此符合 GNU 的宗旨。

最后,大家突然发现,这玩意不正好是 GNU 计划缺的么。于是合在一起打包发布叫 GNU / Linux。然后大家念着念着省掉了前面部分,变成了 Linux 系统。实际上 Debian,RedHat 等 Linux 发行版中内核只占了很小一部分容量。

LLVM(Low Level Virtual Machine),即底层虚拟机。它是一个由C++编写而成的编译器基础框架,利用虚拟技术创造出编译时期、链接时期、运行时期以及「闲置时期」的最优化框架。从宏观上来讲,LLVM不仅仅是一个编译器或者虚拟机,它是一个众多编译器工具及低级工具技术的统称,它包含了一个前端、优化器、后端以及众多的函数库和模板。从微观上来讲,可以把它看做后端编译器,用来生成目标代码,前端编译器为Clang。Xcode5版本之前,编译器默认使用的是GCC,从Xcode5之后编译器默认使用LLVM。原因后面马上讲到。

CLang是一个由C++编写的编译器前端,能够编译C/C++/Objective等高级语言,属于LLVM的一部分,发布于BSD(自由软件中使用最广发的许可证之一)许可证下,其目的就是为了超越GCC。经过测试证明,Clang编译Objective-C代码的速度为GCC的3倍左右,同时它还能针对用户发生的编译错误准确地给出建议。

Clang和GCC的主要区别如下所示。

  • Clang比GCC编译用的时间更短,包括预处理、语法分析、解析、语义分析、抽象语法树生成的时间。

  • Clang比GCC的内存占用更小。

  • Clang生成的中间产物比GCC更小。

  • Clang的错误提示比GCC更加友好。

  • Clang有静态分析,GCC没有。

  • Clang使用BSD许可证,GCC使用GPL许可证。

  • Clang从一开始就被设计为一个API,允许它被源代码分析工具和IDE集成。GCC被构建成一个单一的静态编译器,这使得它非常难以被作为API并集成到其他工具中。

  • GCC比Clang支持更多的语言,例如Java。

  • GCC比Clang支持更多的平台。

  • GCC比Clang更流行。

2. 数据类型

2.1 基本类型

在 C 语言中,数据类型指的是用于声明不同类型的变量或函数的一个广泛的系统。变量的类型决定了变量存储占用的空间,以及如何解释存储的位模式。

数据类型:

  • bool, a Boolean expression of either true or false

  • char, a single character like a or 2

  • double, a floating-point value with more digits than a float

  • float, a floating-point value, or real number with a decimal value

  • int, integers up to a certain size, or number of bits

  • long, integers with more bits, so they can count higher than an int

  • string, a string of characters(但是不能直接申明,本质上是字符数组)

And the CS50 Library has corresponding functions to get input of various types:

  • get_char

  • get_double

  • get_float

  • get_int

  • get_long

  • get_string

基本类型:

在C里面,一个Boolean类型变量需要使用库stdbool.h,也是1个字节。

注意,各种类型的存储大小与系统位数有关,但目前通用的以64位系统为主。

为了得到某个类型或某个变量在特定平台上的准确大小,您可以使用sizeof运算符。表达式sizeof(type)得到对象或类型的存储字节大小。

示例:

#include <stdio.h>
#include <limits.h>
 
int main()
{
   printf("int 存储大小 : %lu \n", sizeof(int));
   printf("float 存储大小 : %lu \n", sizeof(float));
   printf("double 存储大小 : %lu \n", sizeof(double)); 
}

运行结果:

(base) ubuntu@hadoop-node-1:~/C/w1$ ./typeof 
int 存储大小 : 4 
float 存储大小 : 4 
double 存储大小 : 8 

头文件float.h定义了宏,在程序中可以使用这些值和其他有关实数二进制表示的细节。下面的实例将输出浮点类型占用的存储空间以及它的范围值:

#include <stdio.h>
#include <float.h>
 
int main()
{
   printf("float 存储最大字节数 : %lu \n", sizeof(float));
   printf("float 最小值: %E\n", FLT_MIN);
   printf("float 最大值: %E\n", FLT_MAX);
   printf("精度值: %d\n", FLT_DIG);
   
   return 0;
}

运行结果:

(base) ubuntu@hadoop-node-1:~/C/w1$ ./float 
float 存储最大字节数 : 4 
float 最小值: 1.175494E-38
float 最大值: 3.402823E+38
精度值: 6

2.2 void类型

void类型:

Integer Overflow示例:

// Addition with int

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    // Prompt user for x
    int x = get_int("x: ");

    // Prompt user for y
    int y = get_int("y: ");

    // Perform addition
    printf("%i\n", x + y);
}

编译并运行:

(base) ubuntu@hadoop-node-1:~/C/w1$ ./calculator 
x: 123
y: 345
468
(base) ubuntu@hadoop-node-1:~/C/w1$ ./calculator
x: 2147483646
y: 2
calculator.c:15:22: runtime error: signed integer overflow: 2147483646 + 2 cannot be represented in type 'int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior calculator.c:15:22 in 
-2147483648

发现报错了,因为int类型是32位/4Byte的,对于Unsigned的数字最存储0到4294967295(232−12^{32} - 1232−1),对于Signed的数字能存储-2147483648(−231-2^{31}−231)到+2147483647(231−12^{31} - 1231−1)(用一个Bit表示正负号)。

这种情况下最好使用long类型64位/8Byte的。

// Addition with long

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    // Prompt user for x
    long x = get_long("x: ");

    // Prompt user for y
    long y = get_long("y: ");

    // Perform addition
    printf("%li\n", x + y);
}

编译并运行:

(base) ubuntu@hadoop-node-1:~/C/w1$ ./calculator2
x: 2000000000
y: 2000000000
4000000000

double与int类型的存储机制不同,long int的8个字节全部都是数据位,而double是以尾数,底数,指数的形式表示的,类似科学计数法,因此double比int能表示的数据范围更广。

C 语言也允许定义各种其他类型的变量,比如枚举、指针、数组、结构、共用体等等。

2.3 stdbool.h

布尔类型在stdbool.h中引入。

#include <stdio.h>
#include <stdbool.h>

int main(void) {
    bool keep_going = true;  // 也可以是bool keep_going = 1;
    while(keep_going) {
        printf("本程序会在keep_going为真时持续运行。\n");
        keep_going = false;    // 也可以是 keep_going = 0;`
    }
    printf("停止运行!\n");
}

运行结果:

本程序会在keep_going为真时持续运行.
停止运行!

3. 变量

3.1 申明与定义

C 标识符是用来标识变量、函数,或任何其他用户自定义项目的名称。一个标识符以字母A - Z 或a - z或下划线_开始,后跟零个或多个字母、下划线和数字(0 - 9)。

变量其实只不过是程序可操作的存储区的名称。C 中每个变量都有特定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。

int a;
int i, j, k;
int b = 100;
int c = 100, d = 200;

变量的声明有两种情况:

  • 一种是需要建立存储空间的。例如:int a在声明的时候就已经建立了存储空间。

  • 另一种是不需要建立存储空间的,通过使用extern关键字声明变量名而不定义它。 例如:extern int a其中变量a可以在别的文件中定义的。

3.2 const和#define

在 C 中,有两种简单的定义常量的方式:

  1. 使用#define预处理器。

  2. 使用const关键字。

使用const关键字表示该变量是常量,通常变量名用大写字母。

const int a = 100;

使用const关键字时必须赋值。

下面是使用#define预处理器定义常量的形式:

#define LENGTH 10   
#define WIDTH 5
#define NEWLINE '\n'
#define identifier value

使用#define不需要用;。

3.3 储存类

存储类定义 C 程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们所修饰的类型之前。下面列出 C 程序中可用的存储类:

  • auto:默认的存储类。

  • register:定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个Word),且不能对它应用一元的 & 运算符(因为它没有内存位置)。

    寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义register并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。

  • static:指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用static修饰局部变量可以在函数调用之间保持局部变量的值。

    static修饰符也可以应用于全局变量(方法之外声明的变量)。当static修饰全局变量时,会使变量的作用域限制在声明它的文件内。一般来说,如果只有一个源文件,对于全局变量加不加static没什么影响。

    全局声明的一个static变量或方法可以被任何函数或方法调用,只要这些方法出现在跟static变量或方法同一个文件中。

    #include <stdio.h>
     
    void func1(void);
     
    static int count=10;   
     
    int main()
    {
      while (count--) {
          func1();
      }
    }
     
    void func1(void)
    {             
      static int thingy = 5;
      thingy++;
      printf(" thingy 为 %d , count 为 %d\n", thingy, count);
    }

    运行结果:

     thingy 为 6 , count 为 9
     thingy 为 7 , count 为 8
     thingy 为 8 , count 为 7
     thingy 为 9 , count 为 6
     thingy 为 10 , count 为 5
     thingy 为 11 , count 为 4
     thingy 为 12 , count 为 3
     thingy 为 13 , count 为 2
     thingy 为 14 , count 为 1
     thingy 为 15 , count 为 0

    实例中count作为全局变量可以在函数内使用,thingy使用static修饰后,不会在每次调用时重置。

  • extern:用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您使用extern时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。

    程序的编译单位是源程序文件,一个源文件可以包含一个或若干个函数。在函数内定义的变量是局部变量,而在函数之外定义的变量则称为外部变量,外部变量也就是我们所讲的全局变量。它的存储方式为静态存储,其生存周期为整个程序的生存周期。全局变量可以为本文件中的其他函数所共用,它的有效范围为从定义变量的位置开始到本源文件结束。

    示例:

    #include <stdio.h>
    
    int x = 100, y = 200;
    
    int main(void)
    {
        printf("x + y = %d\n", x + y);
    }

    运行结果:

    (base) ubuntu@hadoop-node-1:~/C/w1$ ./extern
    x + y = 300

    然而,如果全局变量不在文件的开头定义,有效的作用范围将只限于其定义处到文件结束。如果在定义点之前的函数想引用该全局变量,则应该在引用之前用关键字extern对该变量作外部变量声明,表示该变量是一个已经定义的外部变量。有了此声明,就可以从声明处起,合法地使用该外部变量。示例:

    #include <stdio.h>
    
    int main(void)
    {
        extern int x, y;
        printf("x + y = %d\n", x + y);
    }
    
    int x = 100, y = 200;

    运行结果:

    (base) ubuntu@hadoop-node-1:~/C/w1$ ./extern
    x + y = 300

    如果整个工程由多个源文件组成,在一个源文件中想引用另外一个源文件中已经定义的外部变量,同样只需在引用变量的文件中用extern关键字加以声明即可。下面就来看一个多文件的示例:

    第一个文件:main.c

    #include <stdio.h>
     
    int count ;
    extern void write_extern();
     
    int main()
    {
       count = 5;
       write_extern();
    }

    第二个文件:support.c

    #include <stdio.h>
     
    extern int count;
     
    void write_extern(void)
    {
       printf("count is %d\n", count);
    }

    在这里,第二个文件中的extern关键字用于声明已经在第一个文件main.c中定义的count。

4. 运算符

4.1 算术运算符

There are several mathematical operators we can use, too:

  • + for addition

  • - for subtraction

  • * for multiplication

  • / for division

  • % for remainder

  • ++

  • --

4.2 关系运算符

#include <cs50.h>
#include <stdio.h>

int main(void) {
    // Prompt user to agree
    char c = get_char("Do you agree? ");
    // Check whether agreed
    if (c == 'Y' || c == 'y')
    {
        printf("Agreed.\n");
    }
    else if (c == 'N' || c == 'n')
    {
        printf("Not agreed.\n");
    }
}

4.3 逻辑运算符

4.4 位运算符

  • |

  • &

  • ^(异或)

  • ~(取反)

  • >>(右移):正数左边补0,负数补1,右边丢弃。

  • <<(左移):右边补0,左边丢弃。

假设如果 A = 60,且 B = 13,现在以二进制格式表示,它们如下所示:

A = 0011 1100

B = 0000 1101

-----------------

A&B = 0000 1100

A|B = 0011 1101

A^B = 0011 0001

~A = 1100 0011

4.5 赋值运算符

4.6 其他

5. 控制语句

5.1 判断

C 语言把任何非零和非空的值假定为true。

// 格式1:
if (expression)
{
  statement1;
} else if {
  statement2;
}
else if {
  statement3;
}
...
else {
  statement4;
}

// 格式2:
switch(expression)
{
    case const_expr1: statement1;
    case const_expr2: statement2;
    ...
    default: statement_default;
}

可以嵌套。

// Constants

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    // Number of points that I lost
    const int MINE = 2;

    // Prompt user for points
    int points = get_int("How many points did you lose? ");

    // Compare points against mine
    if (points < MINE)
    {
        printf("You lost fewer points than me.\n");
    }
    else if (points > MINE)
    {
        printf("You lost more points than me.\n");
    }
    else
    {
        printf("You lost the same number of points as me.\n");
    }
}

编译与运行:

(base) ubuntu@hadoop-node-1:~/C/w1$ ./points
How many points did you lose? 3
You lost more points than me.

5.2 循环

boolean_expression实际结果是0或者1。

// 格式1:
while (boolean_expression)
{
    // statement
}

// 格式2:
do {
    // statement
}
while (boolean_expression);

// 格式3:
for (initialize; boolean_expression; action) 
{
    // statement
}

for中定义的变量是局部变量。

例子:

for (int i = 0; i < 3; i++)
{
    printf("meow\n");
}

for( ; ; )
   {
      printf("该循环会永远执行下去!\n");
   }
return 0;

5.4 循环控制

6. 函数

6.1 printf()

最简单的函数是printf(),这个函数位于stdio库里面。

f代表Formatted。

第一个参数为String,在C里面String必须用双引号""。

下面是printf()函数的声明。

int printf(const char *format, ...)
  • format:这是字符串,包含了要被写入到标准输出stdout的文本。它可以包含嵌入的标签,标签可被随后的附加参数中指定的值替换,并按需求进行格式化。标签属性是%[flags][width][.precision][length]specifier,具体讲解如下:

  • 附加参数:根据不同的format字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了format参数中指定的每个%标签。参数的个数应与%标签的个数相同。

如果成功,则返回写入的字符总数,否则返回一个负数。

示例1:

#include <stdio.h>
 
int main ()
{
   int ch;
 
   for( ch = 65 ; ch <= 70; ch++ ) {
      printf("ASCII 值 = %d, 字符 = %c\n", ch , ch );
   }
 
   return(0);
}

运行:

(base) ubuntu@hadoop-node-1:~/C/w1$ ./printf 
ASCII 值 = 65, 字符 = A
ASCII 值 = 66, 字符 = B
ASCII 值 = 67, 字符 = C
ASCII 值 = 68, 字符 = D
ASCII 值 = 69, 字符 = E
ASCII 值 = 70, 字符 = F

示例2:

#include <stdio.h>
int main()
{
   char ch = 'A';
   char str[20] = "www.foo.com";
   float flt = 10.234;
   int no = 150;
   double dbl = 20.123456;
   printf("字符为 %c \n", ch);
   printf("字符串为 %s \n" , str);
   printf("浮点数为 %f \n", flt);
   printf("整数为 %d\n" , no);
   printf("双精度值为 %lf \n", dbl);
   printf("八进制值为 %o \n", no);
   printf("十六进制值为 %x \n", no);
   return 0;
}

运行:

(base) ubuntu@hadoop-node-1:~/C/w1$ ./printf 
字符为 A 
字符串为 www.foo.com 
浮点数为 10.234000 
整数为 150
双精度值为 20.123456 
八进制值为 226 
十六进制值为 96 

常用:

  • %c for chars

  • %f for floats or doubles

  • %i for ints

  • %li for long integers

  • %s for strings

6.2 函数的申明与定义

// Abstraction with parameterization

#include <stdio.h>

void meow(int n);

int main(void)
{
    meow(3);
}

// Meow some number of times
void meow(int n)
{
    for (int i = 0; i < n; i++)
    {
        printf("meow\n");
    }
}

函数必须在使用前申明或者定义,如果是申明可以在之后定义。

申明语法:

return_type name(arg_type);

如果不含返回值或者参数,使用void。

6.3 return

使用return返回值。

// Return value, multiple arguments

#include <cs50.h>
#include <stdio.h>

float discount(float price, int percentage);

int main(void)
{
    float regular = get_float("Regular Price: ");
    int percent_off = get_int("Percent Off: ");
    float sale = discount(regular, percent_off);
    printf("Sale Price: %.2f\n", sale);
}

// Discount price
float discount(float price, int percentage)
{
    return price * (100 - percentage) / 100;
}

编译并运行:

(base) ubuntu@hadoop-node-1:~/C/w1$ ./discount
Regular Price: 100
Percent Off: 20
Sale Price: 80.00

7. 浮点数的精度

// Addition with int

#include <cs50.h>
#include <stdio.h>

int main(void)
{ 
    // Prompt user for x
    float x = get_float("x: ");

    // Prompt user for y
    float y = get_float("y: ");

    // Perform addition
    printf("%.50f\n", x / y);
}

编译并运行:

(base) ubuntu@hadoop-node-1:~/C/w1$ ./calculator3
x: 1
y: 10
0.10000000149011611938476562500000000000000000000000

It turns out that this is called floating-point imprecision, the inability for computers to represent all possible real numbers with a finite number of bits, like 32 bits for a float. So, our computer has to store the closest value it can, leading to imprecision.

可以发现因为float只有32位,超过一定的精度也会出现Overflow,只不过不会报错。

什么是Integer Overflow?

  • 假如现在用三个Bit储存数字,111代表7,当再加上1的时候会变成1000,但是由于只是用三个Bit,第一位的1会被忽略,结果变成了000,也就是0。

  • 如果是Signed的情况,一般第一位表示符号,如果是1表示负数,0表示正数或者0。000到011代表0到+3,当再加上1的时候,会变成100表示-4,当继续加1加到111的时候为-1,当再加1变成1000,去掉第一位的1,也就是0。这也是为什么2147483646和2相加是个负数。

计算机使用距离Epoch的秒数来记录时间的,以前是用32位Signed的数字来记,因此当达到(231−12^{31} - 1231−1)秒的时候(2038年1月19日),再加上一秒会变成(−231-2^{31}−231)秒,会造成Overflow,也就是1901年12月13日。

In 2038, we’ll also run out of bits to track time, since many years ago some humans decided to use 32 bits as the standard number of bits to count the number of seconds since January 1st, 1970. But since a 32-bit integer can only count up to about two billion, in 2038 we’ll also reach that limit.

  • The 32 bits of an integer representing 2147483647 look like:

01111111111111111111111111111111
  • When we increase that by 1, the bits will actually look like:

10000000000000000000000000000000
  • But the first bit in an integer represents whether or not it’s a negative value, so the decimal value will actually be -2147483648, the lowest possible negative value of an int. So computers might actually think it’s sometime in 1901.

如果是int除以int会出现Truncation。

可以使用Cast Conversion:x / (float) y。

浮点数的精度也会影响乘法的结果。

#include <cs50.h>
#include <math.h>
#include <stdio.h>

int main(void)
{
    float amount = get_float("Dollar Amount: ");
    int pennies1 = amount * 100;
    printf("Pennies1: %i\n", pennies1);
    int pennies2 = round(amount * 100);
    printf("Pennies2: %i\n", pennies2);
}

编译与运行:

(base) ubuntu@hadoop-node-1:~/C/w1$ ./pennies
Dollar Amount: 4.20
Pennies1: 419
Pennies2: 420

第一个会出现419,是因为计算机使用4.199999999来储存4.20,当乘以100后是419.9999999,向下转化成int类型丢失精度,会直接忽略小数点之后的数据。

round()函数位于math库,可以解决这个问题。

8. 注释

和Java类似:

  • //

  • /* */

  • /** */

9. 作用域规则

任何一种编程中,作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。C 语言中有三个地方可以声明变量:

  1. 在函数或块内部的局部变量

    在某个函数或块的内部声明的变量称为局部变量。它们只能被该函数或该代码块内部的语句使用。局部变量在函数外部是不可知的。

  2. 在所有函数外部的全局变量

    全局变量是定义在函数外部,通常是在程序的顶部。全局变量在整个程序生命周期内都是有效的,在任意的函数内部能访问全局变量。

    全局变量可以被任何函数访问。也就是说,全局变量在声明后整个程序中都是可用的。

    在程序中,局部变量和全局变量的名称可以相同,但是在函数内,如果两个名字相同,会使用局部变量值,全局变量不会被使用。

  3. 在形式参数的函数参数定义中

    函数的参数,形式参数,被当作该函数内的局部变量,如果与全局变量同名它们会优先使用。

全局变量与局部变量在内存中的区别:

  • 全局变量保存在内存的全局存储区中,占用静态的存储单元;

  • 局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。

10. 变量初始化

当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动对其初始化。

数据类型
初始化默认值

int

0

char

'\0'

float

0

double

0

pointer

NULL

正确地初始化变量是一个良好的编程习惯,否则有时候程序可能会产生意想不到的结果,因为未初始化的变量会导致一些在内存位置中已经可用的垃圾值,请参考指针章节。

这里
这里