[知识点] 1.1 C 语言基础

总目录 > 1  语言基础 > 1.1  C 语言基础

前言

  这篇文章对 C 语言里一些最基础的编程知识进行简要介绍。因为对 C / C++ 大部分入门内容都轻车熟路了,有些地方可能就不恰当地略过了。

更新日志

  20200713 - 分支与循环部分施工完毕。

  20200716所有内容全部施工完毕。

  20200901 - 1.2 C++ 入门基础知识 现更改为 1.1 C 语言基础,仅介绍 C 语言中存在的基础知识。

  20200903 - 部分内容移动至 1.2 C 语言数据类型,介绍变量,常量,数组,结构体与函数等内容。

  20211016 - 配合考研复习,重新整理了一遍结构,新增部分内容,优化缩进排版。

子目录列表

  1.1.1 计算机语言的发展

  1.1.2 C 语言的编译

  1.1.3 C 语言程序的结构

  1.1.4 常量、变量与表达式

  1.1.5 输入与输出

  1.1.6 运算符

  1.1.7 选择结构与循环结构

 

1.1  C 语言基础

1.1.1 计算机语言的发展

  计算机语言经历了几个阶段:

    > 机器语言:基于二进制;

    > 汇编语言:英文字母 + 数字表示指令;

    > 高级语言:接近自然语言与数学语言,依赖编译程序将其转换为目标程序,其发展阶段为:

      > 首个高级语言:FORTRAN;

      > 非结构化语言:FORTRAN / Basic / ALGOL;

      > 结构化语言:QBasic / C

      > 面向对象语言:C++ / C# / Visual Basic / JAVA / Python。

  下面开始介绍 C 语言。

 

1.1.2 C 语言的编译

  ① 编译的概念

    众所周知,C / C++ 作为计算机高级语言,是开发者和计算机的沟通方式,是两者之间的桥梁;

    同样众所周知的是,计算机本身并不能识别这些由英文单词和数字组成的话,而只能理解由 0 和 1 构成的机器指令;

    那么,要将 C / C++ 翻译成二进制数,则需要一位翻译员进行翻译,在计算机领域里,翻译的过程被称作 —— 编译(compile),而担任翻译员的,则是编译器

  ② 编译器

    C 语言使用的编译器为 GCC(GNU Compiler Collection,GNU 编译器套件)

    Windows 需要自行下载;

    macOS 在终端执行:xcode-select --install;

    Linux 在终端执行:sudo apt update && sudo apt install gcc。

  ③ 编译与运行过程

    

   ④ IDE

    集成开发环境(IDE, Integrated Development Environment),一种将上述的程序编辑、编译、连接与运行等操作全部集中在一起的开发环境;

    现有的常用的 C / C++ 语言 IDE 有:

      > Dev-C++(最基础的 C / C++ IDE,适合初学者使用,功能相对有限)

      > CLion(JetBrain 家族,同系列还有 Intellij IDEA(JAVA IDE)、PyCharm(Python IDE)等)

      > Emacs(自由度相当高的 IDE 与文本编辑器)

      > Visual Studio

      > Code:Blocks

      > Eclipse

  ⑤ 命令行执行

    如果不想被 IDE 的框架受限,或者想显得更高端的话,可以在命令行直接进行编译,格式为(C++):

      g++ *.cpp -o *,* 表示要编译的文件名,-o 用来指定输出文件名(C 格式类似,此处不介绍)

    除此之外,还有许多可以添加的编译选项,列举几个常用的:

      -O / -O1 优化生成代码;-O2 进一步优化;-O3 再进一步优化;

      -w 关闭所有警告提醒;-Wall 显示所有警告提醒;

      -lm 链接数学库(这个一般竞赛编译都会加上,但其实不加上也能正常运行)

 

1.1.3 C 语言程序的结构

  先演示一个最经典的 C 语言示例代码。

1 #include <stdio.h>
2  
3 int main() {
4     printf("Hello, world!"); // output 'Hello, world!'
5     return 0;
6 } 

  > 头文件

    第一行(L1)为头文件格式:

      #include <头文件名>

    头文件的作用是什么?一般头文件包含了各类功能函数或者数据接口的声明;

    任何函数都不是与生俱来的,而是保存在 C / C++ 里内置的各种 .h 文件中,如果不事先声明,编译器并不能识别这是什么;

    比如,L4 的 printf 函数是来自内置的 <stdio.h> 中,故需要在最开始表明,接下来的代码需要用到这里面的函数,那么编译器在执行主程序之前就会先进行预处理;

    一般而言,只需要写上你需要的头文件即可,但许多时候你并不清楚你会需要哪些头文件中的哪些函数,有时候可能出现遗漏,而编译器并不会在所有这类情况下报错,所以你可以选择:

      ① 提前写好常用的头文件作为模板,每次编写时直接复制上去;

      ② 多检查几次;

      ③ 使用万能头文件(仅适用于 C++)

#include <bits/stdc++.h>

    它包含了 C++ 绝大多数头文件。但是请注意,首先 C 语言不能使用,MSVC 编译器不支持;

    其次并不是所有情况下都允许使用,多加留意考试 / 比赛规则;

    最后,头文件声明越多,对编译时间影响越大,尽管一般不至于导致 TLE。

    头文件也可以自己编写,比如自行定义了 “bebe.h”,并在其中定义了一些函数,那么在声明头文件时可以这样写:

#include "bebe.h"

    注意,自定义头文件使用 " " 而不是 < >

    声明头文件本身是一种预处理语句。关于预处理,请参见 1.2 C 语言进阶

  > 主函数 main

    L3 定义了一个主函数:int main();

    函数定义由三部分组成:int 是函数类型,main 是函数名称,() 内写函数参数,可以为空;

    对于任何 .c / .cpp 文件,可以有许多个函数,但有且仅有一个主函数,且为整型(int)函数;

    注意,请不要使用各类古老的教材教辅上写的 void main(),实际上这种写法早已被废弃,是不会被编译通过的;

    L6 有一句 “return 0”,表示主函数的返回值为 0,作为主函数的结束标识,尽管可以不写,但还是建议写上,这是规范,某些情况下不写会出现错误;

    主函数是一个特殊的函数。关于函数,请参见 1.2 C 语言进阶

   > 注释

    上面的示例代码中的 L5 中,后面有个 “//” 符号,表示注释,对程序编译无任何影响,一般用于让某段代码暂时不执行,或者解释程序,便于日后维护或者供他人阅读。

    两种注释的方法:

      ① 行内注释,以 “//” 开头,后面所有内容全部为注释;

        举例:如上示代码;

        过去只有 C++ 支持这种注释形式,C 语言则是在 C99 标准中新增对其的支持。

      ② 注释块,以 “/*” 开头,以 “*/” 结尾,可以跨行,中间内容全部为注释;

        举例:

/*
---------------------
    Hello, world!
---------------------
*/

 

1.1.4 常量、变量与表达式

  ① 基本数据类型

    程序执行的基本逻辑是输入数据 -> 处理数据 -> 输出结果,数据是程序在运算处理时最核心的对象。

    数据被分为不同类型,称为数据类型,它是程序分配和使用存储单元的基本技术。下面介绍 C / C++ 中的几大基本数据类型:

    > int 整型

      整数类型,简称整型,占用 4 个字节,表示范围为 [-2 ^ 31, 2 ^ 31 - 1 ( = 2147483647)]。支持类型前缀限定符,如:

        > short [int] - 短整型,占用 2 个字节,表示范围为 [-2 ^ 15, 2 ^ 15 - 1 ( = 32767)];

        > unsigned int - 无符号整型,占用 4 个字节,表示范围为 [0, 2 ^ 32 - 1 ( = 4294967295)];

        > long [int] - 长整型,完全同 int(在 32 位机器上);

        > long long [int] - 超长整型,占用 8 个字节,表示范围为 [-2 ^ 63, 2 ^ 63 - 1 ( = 9223382036854775807)](C++ 11 正式加入标准);

      还有 unsigned short, unsigned long, unsigned long long 等。

      【P44 补码表示法 补充】

    > float / double 浮点型

      > float - 单精度浮点型,表示实数,占用 4 个字节,表示范围大致为 [-3.4 * 10 ^ 38, 3.4 * 10 ^ 38];

      > double - 双精度浮点型,表示实数,占用 8 个字节,表示范围大致为 [-1.7 * 10 ^ 308, 1.7 * 10 ^ 308];

    > char 字符型

      表示字符,占用 1 个字节,表示范围为 [0, 255];

      不是所有字符都可以被识别, 目前普遍采用 ASCII 字符集,其中包括全体大小写英文字母、数字、29 个特殊符号、空格符、制表符、不能显示的若干字符等;

      比如大写字母 'A' 对应的 ASCII 代码为 65,空格 ' ' 对应的 ASCII 代码为 32;

      另外还有 wchar_t,表示宽字符,占用 2 个字节;char16_t, char32_t,表示 Unicode 字符,分别占用 2, 4 个字节。

    > bool 布尔逻辑型

      表示真(true, 1)假(false, 0),占用 1 个字节,表示范围为 [0, 1];

    > void 无类型

      int, short, char 前缀说明符缺省时,默认为 signed 类型,即有符号型。double 同样支持 long 前缀。

      在 16 / 32 位的机器上,int 的长度分别为 2, 4 字节,如果需要提高可移植性,尽可能用 short / long 等前缀进行限定。

    除了上述基本数据类型,还有诸多更为复杂的类型,比如:数组、结构体、共用体、指针、函数,请参见 1.2 C 语言进阶

  ② 常量

    (以前根据 OI Wiki 总结时,其将常量被狭隘地视作常变量,其实是不合理的)

    常量指不能被改变的数据。常见的常量类型如下:

      > 整型常量:比如 1, 123, -741, 0;

        在一个数后面加上 'L' 或 'l',可以表示它为 long 型;加上 'LL' 或 'll',可以表示它为 long long 型;

      > 实型常量:十进制形式比如 3.14, -0.1428, 0.0, 10.086,指数形式比如 12.34e3, -346.87e-25;

        所有实型常量默认为双精度浮点型 double。

      > 字符常量:用单引号 ' 囊括,可以划分为:

        > 普通字符,比如 'a', 'B', '3', '?';

        > 转义字符,由 '\' 加上一个字符构成,表示将该字符转换成其他的意义,而非其本身比如:

          > '\n',表示换行,将当前位置移动到下一行起始;

          > '\t',表示制表符,将当前位置移到一个制表符后;

          > '\\',表示输出字符 '\';

          > '\?',表示输出字符 '?';

          > '\...',其中 ... 为一个八进制数,表示 ... 对应的字符,比如 '\101' 表示 ASCII 为八进制数 101(即十进制数 65)的字符 'A';

          > '\x...',其中 ... 为一个十六进制数,定义与 '\...' 类似。

          转义字符最常用的场景在下面输入与输出部分会提及。

      > 字符串常量,用双引号 " 囊括,比如 "nd", "Dota2", "o", "..?";

        可以注意到,上述基本数据类型中并没有字符串,C 语言本身不直接提供 C++ 中才有的字符串类型 String,而是通过字符数组实现。

      > 符号常量,用宏定义指令 #define 实现,指定一个符号名称代表一个常量,更多关于宏定义的内容请参见 1.2 C 语言进阶 中的预处理器部分。

      > 常变量,在 C99 标准加入的具有变量属性的常量 —— 有名字,占存储单元,但不能改变值;只能在声明时被赋值。

        声明常量的一般格式:

          const 常量数据类型 常量名 = 常量值;

        常量名属于标识符(identifier)的一类,其名称由字母、数字和下划线字符组成,且必须以字母和下划线开头,在计算机语言中是最关键的一类数据;

        比如:

const int a = 2;
const char b = 'A';  

  ③ 变量

    变量指可以被改变的一类数据,与常量相对应;

    变量必须先声明后使用,声明变量的一般格式:

      变量数据类型 变量名, ...

    和上面的常量名相同,变量名也是标识符,也需要遵从标识符的命名规则;

    比如:

int a; // 声明一个 int 类型的变量 a
double b, c; // 声明两个 double 类型的变量 b, c

    变量名只是程序可操作的存储区的名称,其可以存储所属数据类型对应的内容,比如:

int a;
a = 5; // 将整数 5 赋值给 a

    或者:

char d = 'a'; // 声明一个 char 类型的变量 d,且赋值为 'a'

    声明的同时可以赋值;

    同样地,变量除了可以是基本数据类型,也可以是复杂的数据类型,下面会进行介绍。

  ④ 数据类型转换

    类型转换指将一种数据类型转换为另一种数据类型,分为隐式类型转换显式类型转换

    > 隐式类型转换

      如下 4 种情况,编译器会自动对参与运算的数据类型进行恰当地转换而无需程序猿参与,这样的类型转换称为隐式类型转换

      在同一个算术表达式中,如果出现了一种以上的不同数据类型,编译器会自行进行类型转换,再进行计算;

      转换原则是尽可能避免损失精度,由窄数据类型(占用存储空间小的类型)向宽数据类型转换,具体情况大致如下:

        bool -> char -> short -> (unsigned)int -> long -> long long -> float -> double -> long double

      将一种类型的数据复制给不同类型的变量,会发生隐式类型转换,将右值结果转换为左值类型。比如:

int a = 2;
double b = 3.4, c = 2.2;
b = a; // 将 int 类型变量 a 转换为 double 类型,值为 2.0,并赋值给 b
a = c; // 将 double 类型变量 c 转换为 int 类型,精度损失,值为 2,并赋值给 a

      函数调用时,如果实参表达式形参类型不同,则将实参类型转换为形参;

      函数返回时,如果返回表达式值函数声明返回类型不同,则将结果转换为声明返回类型,比如:

double min(int a, int b) {
    return a < b ? a : b; 
}

      由于形参 a, b 均为 int 类型,其返回表达式显然也是 int 类型,但声明返回类型为 double,则会将返回值从 int 转换为 double。

      (关于这两条涉及到的函数会在下面介绍)

    > 显式类型转换

      显式类型转换也称为强制转换,指通过程序猿强行更改数据的类型,一般格式如下:

        (强制转换类型) 变量 / 表达式名

      举例:

int a = 1;
double b = (double)a; // 将 int 变量 a 强制转换成 double,值为 1.0 ,赋值给 b 
a = (int)2.33; // 将浮点数 2.33 强制转换成 int,损失精度,值为 2,赋值给 a 

    > 互通性

      尽管我们提到 int 家族是用来表示整数,float / double 表示实数,char 表示字符,bool 表示真假逻辑,但 C / C++ 对这些类型之间的转换包容性极高,许多情况下可以直接相互转换,比如:

char x = 65;
int y = 'a';

      两者都是合法的,且数值是相等的;

bool a = 1;
bool b = true;

      两者也是合法的,且数值是相等的;

      这一点,在比如 Java 等其他语言上要严格许多。C++ 的这种类型互通在方便编写的同时也降低了容错率。

 

1.1.5 输入与输出

  ① scanf / printf

    在 C 语言中,使用 <stdio.h> 库中的 scanf / printf 函数分别进行数据的输入与输出,一般格式为:

      scanf("A", B);

      printf("A", B);

    "A" 内填写字符或者格式说明符,B 填写变量。比如:

scanf("%d", &a);  // 表示输入一个变量 a
scanf("%d %d", &a, &b);  // 表示输入两个变量 a, b
printf("1");  // 表示输出一个数 1,等同于 printf("%d", 1);
printf("%d\n%d", a, b);  // 表示输出两个变量 a, b,中间会换行

    格式看起来很复杂(确实比 C++ 的 cin / cout 流输入输出函数要复杂许多),进行一些解释:

    > %d 是什么?

      格式说明符的一种。使用 scanf / printf 时,需要先声明该变量的格式,下面是常用的格式说明符:

        %d - 十进制有符号整型变量,对应 int 类型等;

        %c - 字符变量,对应 char 类型;

        %f - 单精度浮点型变量,对应 float 类型;

        %lf - 双精度浮点型变量,对应 double 类型;

        %s - 字符串(字符数组)变量,对应 char[](字符数组);

        %lld - 长整型变量(Windows 下使用 %I64d),对应 long long 类型。

      > 在 '%' 与字母之间添加一个数字,表示设置输出域宽(即输出数据占用的字符个数);

        可以等价于在设定域宽的范围内进行右对齐;

        这个数字小于数本身位数则没有影响;大于本身位数将自行向左侧填充空格。

        举例:

printf("%3d", 5);  // 将输出 “  5”

      > 如果在域宽前再添加一个 '-',则表示左对齐,并在域宽大于本身位数时向右侧填充空格。

        举例:

printf("%-3d", 5);  // 将输出 “5  ”

      > 在 '%' 与字母之间添加一个 '.' 和一个数字,表示设置浮点数精度(即小数保留位数),仅适用于浮点型变量,即 %f, %lf。

        举例:

printf("%.2lf", 2.077);  // 将输出 “2.08”

    > \n 是什么?

      转义字符的一种,表示换行,在上面的字符常量中已经提到。在日常使用中,转义字符大多是用于数据输出。

    > scanf 中的 & 是什么?

      & 表示取址运算符,表示变量在内存中的地址,具体请参见:1.4.3 指针与引用 中的 指针的声明 部分。

  ② getchar / putchar 字符数据的输入输出

    分别是输入和输出单个字符,举例略。

  ③ get / gets / put / puts

    

 

1.1.6 运算符

  ① 算术运算符

    + 正/加法;- 负/减法;* 乘法;/ 除法;% 取模。

  ② 位运算符

    ~ 非;& 与;| 或;^ 异或;<< 左移;>> 右移。

    关于位运算更详细的介绍,请参见:6.1 位运算与进位制

  ③ 赋值运算符

    = 赋值,前面还可以加上算术运算符位运算符形成复合赋值运算符,比如:

    i += 2,等价于 i = i + 2;

    i %= j,等价于 i = i & j。

  ④ 自加自减符

    ++ 自加:i++ 等价于 i = i + 1;

    -- 自减:i-- 等价于 i = i - 1;

  ⑤ 比较运算符

    < 小于;<= 小于等于;> 大于;>= 大于等于;== 等于;!= 不等于。

    注意,== 表示等于,而 = 表示赋值,初学者常见问题之一。

  ⑥ 逻辑运算符

    && 逻辑与;|| 逻辑或;! 逻辑非。

    注意和前面的位运算符区分开来:位运算符是返回与 / 或 /... 操作的值,而逻辑运算符只返回其值是否为 0,为 0 时返回 0,其余情况就返回 1,用于各类判断而非运算。

  ⑦ 分号与逗号

    ; 分号:表示一个表达式的结束;

    , 逗号:用于将若干个表达式分隔开,有时候等价于分号,但实际有区别,这里不赘述。

  ⑧ 成员访问运算符

    [ ] 下标符;. 对象成员;& 取地址 / 引用类型符;* 寻址 / 解引用符;-> 指针成员;

  ⑨ 关于优先级

    所有运算符都有运算的先后顺序,就像我们平常计算的时候知道乘除优先于加减;

    一般需要关注的是如下几个优先级关系(优先级从低到高):

      > 算法运算符 > 关系运算符 > 赋值运算符

      > 与 > 或

      > 逻辑与 > 逻辑或

    下面给出 C++ 大部分运算符的优先级总表(第一级优先级最高):

      

  ⑩ 关于混合运算

    不同类型的数据允许混合运算。前面已经提及了数据类型的转换,如果一个运算符两侧数据类型不同,同样也会进行隐式类型转换以进行运算,同样遵循上述转换原则。

 

1.1.7 选择结构与循环结构

  ① 选择结构

    > if 和 if - else 语句

if (条件) {
    主体1;
}
else {
    主体2; 
} 

      众多主流语言中必学的第一条语句 —— if 语句,基本判断语句。最基本结构为 if (...) ...,和英文一样,表示“如果……则……”;还可以在语句后加上 else ...,表示“如果……则……否则……”。

      if - else 语句还可以嵌套使用,举个例子:

if (a == 1)    
    b = 10;
else if (a == 2)
    b = 100;
else if (a == 3)
    b = 1000;
else
    b = 0;

    > switch 语句

switch (选择句) {
    case 标签1:
        主体1; break;
    case 标签2:
        主体2; break;
    ... 
    default:
        主体n; 
}

      其含义为:当选择句的返回值为标签 1 时,执行主体 1 语句;为标签 2 时,执行主体 2 语句,等等;如果不等于任何标签,则执行 default 中的主体 n 语句。

      标签本身没有数量限制。

      其中,选择句必须是整数类型表达式,标签必须是整型常量,所以相比之下局限性比 if - else 大很多。

      举个例子:

switch (a) {
    case 1:
        b = 10;
        break;
    case 2:
        b = 100;
        break;
    case 3:
        b = 1000;
        break;
}

      这段代码和上面的 if-else 代码是等价的。

      注意每一个 case 后都写上了 break,从 switch 的功能来看是必要的,但是如果不加,编译是没有问题的,只是会出现不一一对应执行的情况,比如:

      在第二个 case 中不加 break,且选择句返回值为标签 2,则将会执行主体 2, 3, ... 的语句,直到出现 break。

      举个例子:

switch (a) {
    case 1:
        b = 10;
    case 2:
        b = 100;
    case 3:
        b = 1000;
        break;
    }
    return 0;

      那么,其实 a 不管是等于 1 / 2 / 3,最后 b 的值均为 1000。

  ② 循环

    顾名思义,许多时候我们要反复做一件相同的事情,比如我们需要求出 2 ^ 20,写二十次 * 2 显然是不合理的,这时候需要使用循环语句

    > for 语句

for (初始化; 判断条件; 更新) {
    循环体;
}

      最基本的循环语句,使用最广泛,功能最强大。就以上述求 2 ^ 20 为例,使用 for 循环可以这样写:

ans = 1;
for (int i = 1; i <= 20; i++) {
    ans = ans * 2;
}

      初始化定义一个变量 i 并赋值为 1,其作用为计数器,当 i <= 20,即计数器计数到 20 之前,执行花括号内包含的循环体语句 ans = ans * 2;再执行更新语句 i++ 以进行累加。故 for 循环语句的执行顺序为:初始化语句 -> 判断条件 -> 循环体语句 -> 更新语句

      上述是 for 循环最基本的使用方式,而其实其灵活性极高。初始化、判断条件、更新和循环体四大部分,理论上都可以进行省略 —— 省略初始化语句,即不进行初始化;省略判断条件,即判断条件永远为真,一般会在循环体内在一定条件下通过某种方式跳出循环,如果没有,则会进入死循环;省略更新语句,即不进行更新,一般在循环体内包含了更新内容,否则也会进入死循环;省略循环体没有意义,但同样可以编译。

      C 语言中不允许在初始化语句里定义变量,所以需要在循环语句前就进行定义。

    > while 语句

while (判断条件) {
    循环体; 
}

      不能初始化的循环语句(初始化只能在循环前完成)。如果将上述 if 的结构套用到 while 中,则相当于:

初始化; 
while (判断条件) {
    循环体;
    更新; 
}

      不同的是,判断条件不能省略。如果需要永远为真,则需要填入 true(或 1)。

    > do - while 语句

do {
    循环体;
} while (判断条件);

      和 while 语句类似,唯一区别是 do - while 语句先执行循环体再进行判断。

      三种循环一般情况下都是互通的,根据实际需要和个人习惯进行选择即可。

    > break 和 continue

      循环语句中很关键的两个语句。有时我们的中断循环的判断条件并非那么明朗而需审时度势,break 和 continue 能够满足我们的需求。

      break 的作用是立即退出当前循环。比如我们需要 1 + 2 + ... 累加到刚好大于 100,则可以:

for (int i = 1; ; i++) {
    ans += i; 
    if (ans > 100) break;
}

      continue 的作用是跳过本轮循环,直接准备下一轮循环。对于 for 循环是执行更新语句,对于 whlie 循环则直接判断。比如我们需要 1 + 3 + 5 + ... + 21,可以这样写:

for (int i = 1; i <= 21; i++) {
    if (i % 2 == 0) continue;
    ans += i;
}

 

posted @ 2020-04-22 20:46  jinkun113  阅读(805)  评论(0编辑  收藏  举报