C语言基础学习笔记

C语言的应用场景

  • 操作系统
  • 语言编译器
  • 汇编器
  • 文本编辑器
  • 打印机
  • 网络驱动器
  • 现代程序
  • 数据库
  • 语言解释器
  • 实体工具

第一个C程序

#include <stdio.h>

int main(){
    printf("朋");
    return 0;
}

C标准,最新为C11。

开发环境搭建

  • 文本编辑器
  • C 编译器

主流的C编辑器 gcc

gcc -v

C 程序是如何执行的

  1. 找到 你编辑好的 C 程序,如 main.c
  2. 执行 gcc main.c
  3. 如果上一步没有错误,会生成一个 a.out 的可执行文件
  4. 输入 ./a.out 即可执行

Token

token 可以简单地翻译为单词 。

在编程语言编译执行过程中,有一个不可忽略的概念(其实我们都忽略了),那就是词法分析器。

词法分析是计算机科学中将字符序列转换为单词(Token)序列的过程。

词法分析器便是为了具体实现上述过程的。词法分析器一般依函数的形式存在,供语法分析器调用。

例如下面一段代码

printf("朋");

词法分析器会生成这样的五个token

printf
(
"朋"
)
;

关键字

关键字 说明
auto 声明自动变量
break 跳出当前循环
case 开关语句分支
char 声明字符型变量或函数返回值类型
const 声明只读变量
continue 结束当前循环,开始下一轮循环
default 开关语句中的”其它”分支
do 循环语句的循环体
double 声明双精度浮点型变量或函数返回值类型
else 条件语句否定分支(与 if 连用)
enum 声明枚举类型
extern 声明变量或函数是在其它文件或本文件的其他位置定义
float 声明浮点型变量或函数返回值类型
for 一种循环语句
goto 无条件跳转语句
if 条件语句
int 声明整型变量或函数
long 声明长整型变量或函数返回值类型
register 声明寄存器变量
return 子程序返回语句(可以带参数,也可不带参数)
short 声明短整型变量或函数
signed 声明有符号类型变量或函数
sizeof 计算数据类型或变量长度(即所占字节数)
static 声明静态变量
struct 声明结构体类型
switch 用于开关语句
typedef 用以给数据类型取别名
unsigned 声明无符号类型变量或函数
union 声明共用体类型
void 声明函数无返回值或无参数,声明无类型指针
volatile 说明变量在程序执行中可被隐含地改变
while 循环语句的循环条件

空格

必须的空格

int height;

int 和 height 之间的空格是必须的。

非必须的空格

height = 10;

等号左右的空格为非必须的,但是为了增强代码的可读性,会在一些位置插入空格,代码先是给人阅读的,然后才是给机器执行的。

数据类型

  1. 基本类型:整数型和浮点型
  2. 枚举类型
  3. void类型
  4. 派生类型:指针类型、数组类型、结构类型、共用体类型和函数类型。

    #include <stdio.h>
    #include <limits.h>
    
    int main(){
    printf("sizeof int %lu", sizeof(int));
    return 0;
    }
    

基本类型占用空间(64位机)

  • char : 1个字节
  • int :4个字节
  • float:4个字节
  • double:8个字节

void 类型

void 类型指定没有可用的值。它通常用于以下三种情况下:

  1. 函数返回为空
  2. 函数参数为空
  3. 指针指向void

不理解没关系,后面慢慢学。

变量

变量,即程序可操作的存储区的名称。

C 中每个变量都用特定的类型。

变量的声明和定义

extern int i; //声明,不是定义
int i; //声明,也是定义

左值和右值

左值(lvalue):指向内存位置的表达式被称为左值(lvalue)表达式。左值可以出现在赋值号的左边或右边。

右值(rvalue):术语右值(rvalue)指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。

例如变量是左值,数值型的字面值为右值。

复制语句左边只能是左值,故

20 = 40;

在编译时就会报错。

常量

在执行期间不会改变的量,又叫字面量。

定义常量

  1. #define
  2. const

    #include <stdio.h>
    
    #define  H  10
    
    int main(){
    const W = 20;
    printf("H %i", H);
    printf("\n");
    printf("W %i", W);
    return 0;
    }
    

注: 把常量定义为大写字母形式,不是必须的,但是是很好的编程习惯。

存储类

存储类定义C中变量/函数的可见范围和生命周期

auto:只能修饰局部变量

register:用于定义存储在寄存器中而不是 RAM 中的局部变量。

寄存器只用于需要快速访问的变量,比如计数器。

static:指示编译器在程序的生命周期内保持局部变量的存在。

使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。

static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。

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

当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。

运算符

算术运算符 + - * 、 % ++ –

关系运算符 == != > < >= <=

逻辑运算符 && || !

位运算符 & | ^ ~ << >>

赋值运算符 = += -= *= /= %= <<= >>= &= ^= |=

其他运算符 sizeof & * ?:

运算符优先级: 记不住。

判断

if (a == 1){
}else{
  
}
switch(expression){
  case exp1:
    statement;
    break;
	default :
    statement;
}
exp1 ? exp2 : exp3;

循环

while(condition){
  
}

for(init; condition; increment){
  
}

do{
  
}while(condition);

函数

return type function_name(params list){
  /*function body*/
}

默认情况下,C 使用传值调用来传递参数。一般来说,这意味着函数内的代码不能改变用于调用函数的实际参数。

作用域规则

  1. 在函数或块内部的局部变量
  2. 在所有函数外部的全局变量
  3. 形式参数的函数参数定义中

全局变量和局部变量

  1. 全局变量保存在内存的全局存储区中,占用静态的存储单元;
  2. 局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。

初始化默认值

数据类型 初始化默认值
int 0
char ‘\0’
float 0
double 0
pointer NULL

数组

C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。

double balance[10]

枚举 enum

#include <stdio.h>

enum ABC {
    A=100, B, C
};

int main() {
    enum ABC abc;
    abc = B;
    printf("%i", abc);
}

输出:101

指针

指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。

#include <stdio.h>

int main ()
{
    int  var1;
    char var2[2];

    printf("var1 变量的地址: %p\n", &var1  );
    printf("var2 变量的地址: %p\n", &var2  );

    return 0;
}

输出

var1 变量的地址: 0x7ffeed397458
var2 变量的地址: 0x7ffeed397456
int    *ip;    /* 一个整型的指针 */
double *dp;    /* 一个 double 型的指针 */
float  *fp;    /* 一个浮点型的指针 */
char   *ch;     /* 一个字符型的指针 */

null 指针

如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为指针。


#include <stdio.h>

int main() {
    int *ptr = NULL;

    printf("ptr 的地址是 %p\n", ptr);

    return 0;
}

输出:

ptr 的地址是 0x0

指针的指针

指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。

C 语言允许您传递指针给函数,只需要简单地声明函数参数为指针类型即可。

回调函数

在JS中有回调函数的概念,如何在C中使用回调函数呢?

借用函数指针。


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


void gen_rand_array(int *array, int array_size, int(*get_random)(void)) {
    for (int i = 0; i < array_size; i++) {
        array[i] = get_random();
    }
}

int get_random_value(void) {
    return rand();
}

int main() {
    int *ptr = NULL;
    int arr1[8];
    gen_rand_array(arr1, 8, get_random_value);
    for (int i = 0; i < 8; ++i) {
        printf("arr %d %d\n", i, arr1[i]);
    }
    return 0;
}

字符串

C 中,字符串实际上是使用 null 字符 ‘\0’ 终止的一维字符数组。

char name[] = "peng";

不需要把 null 字符放在字符串常量的末尾。C 编译器会在初始化数组时,自动把 ‘\0’ 放在字符串的末尾。

字符串操作函数

序号 函数 & 目的
1 strcpy(s1, s2); 复制字符串 s2 到字符串 s1。
2 strcat(s1, s2); 连接字符串 s2 到字符串 s1 的末尾。
3 strlen(s1); 返回字符串 s1 的长度。
4 strcmp(s1, s2); 如果 s1 和 s2 是相同的,则返回 0;如果 s1s2 则返回大于 0。
5 strchr(s1, ch); 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。
6 strstr(s1, s2); 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置

结构体

一种用户自定义的可用的数据类型。

struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
} book;

如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明,如下所示:

struct B;    //对结构体B进行不完整声明
 
//结构体A中包含指向结构体B的指针
struct A
{
    struct B *partner;
    //other members;
};
 
//结构体B中包含指向结构体A的指针,在A声明完后,B也随之进行声明
struct B
{
    struct A *partner;
    //other members;
};

为了使用指向该结构的指针访问结构的成员,您必须使用 -> 运算符,如下所示:

struct_pointer->title;

位域

些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0 和 1 两种状态,用 1 位二进位即可。为了节省存储空间,并使处理简便,C 语言又提供了一种数据结构,称为”位域”或”位段”。

struct bs{
    int a:8;
    int b:2;
    int c:6;
}data;

说明 data 为 bs 变量,共占两个字节。其中位域 a 占 8 位,位域 b 占 2 位,位域 c 占 6 位。

位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:

struct k{
    int a:1;
    int  :2;    /* 该 2 位不能使用 */
    int b:3;
    int c:2;
};

共同体

共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。

可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。

union Data
{
   int i;
   float f;
   char  str[20];
} 

共用体占用的内存应足够存储共用体中最大的成员。

typedef

可以使用它来为类型取一个新的名字。

typedef int Int;

typedef vs #define

#definde 是 C 指令,用于为各种数据类型定义别名。

typedef 仅限于为类型定义符号名称;

typedef 由编辑器执行解释的,#define由预编译器进行处理的。

#include <stdio.h>

#define TRUE  1
#define FALSE 0

int main() {
    typedef int Int;
    Int a;
    a = 20;
    printf("TRUE 的值: %d\n", TRUE);
    printf("FALSE 的值: %d\n", FALSE);
    printf("Int a %d\n", a);

    return 0;
}

输出

TRUE 的值: 1
FALSE 的值: 0
Int a 20

输入和输出

标准文件 文件指针 设备
标准输入 stdin 键盘
标准输出 stdout 屏幕
标准错误 stderr 您的屏幕

输入和输出函数:

  1. 1int scanf(const char *format, …) 函数从标准输入流 stdin 读取输入,并根据提供的 format 来浏览输入。
  2. int printf(const char *format, …) 函数把输出写入到标准输出流 stdout ,并根据提供的格式产生输出。

format 可以是一个简单的常量字符串,但是您可以分别指定 %s、%d、%c、%f 等来输出或读取字符串、整数、字符或浮点数。

printf() 用于格式化输出到屏幕。printf() 函数在 “stdio.h” 头文件中声

明。

getchar() & putchar() 函数

  1. int getchar(void) 函数从屏幕读取下一个可用的字符,并把它返回为一个整数。
  2. int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。

gets() & puts() 函数

char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。

int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout

文件读写

#include <stdio.h>

int main()
{
    FILE *fp = NULL;
    fp = fopen("test.txt", "w+");
    fprintf(fp, "写入文件 1 \n");
    fputs("写入文件 2\n", fp);
    fclose(fp);
}

test.txt 中具体内容

写入文件 1 
写入文件 2

读取文件

#include <stdio.h>

int main()
{
    FILE *fp = NULL;
    char buff[255];

    fp = fopen("test.txt", "r");
    fscanf(fp, "%s", buff);
    printf("1: %s\n", buff );

    fgets(buff, 255, (FILE*)fp);
    printf("2: %s\n", buff );

    fgets(buff, 255, (FILE*)fp);
    printf("3: %s\n", buff );
    fclose(fp);

}

输出

1: 写入文件
2:  1 

3: 写入文件 2

预处理器

C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。

C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。

所有的预处理器命令都是以井号(#)开头。

所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:

指令 描述
#define 定义宏
#include 包含一个源代码文件
#undef 取消已定义的宏
#ifdef 如果宏已经定义,则返回真
#ifndef 如果宏没有定义,则返回真
#if 如果给定条件为真,则编译下面代码
#else #if 的替代方案
#elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个 #if……#else 条件编译块
#error 当遇到标准错误时,输出错误消息
#pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

预定义宏

描述
DATE 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
TIME 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
FILE 这会包含当前文件名,一个字符串常量。
LINE 这会包含当前行号,一个十进制常量。
STDC 当编译器以 ANSI 标准编译时,则定义为 1。

头文件

头文件是扩展名为 .h 的文件。

包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。

引用头文件相当于复制头文件的内容。

建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。

强制类型转换

(type_name) expression

错误处理

C 语言不提供对错误处理的直接支持。那怎么办?

可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。

开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。

errno.h

递归

递归指的是在函数的定义中使用函数自身的方法。

#include <stdio.h>
 
double factorial(unsigned int i){
   if(i <= 1)
   {
      return 1;
   }
   return i * factorial(i - 1);
}
int  main(){
    int i = 15;
    printf("%d 的阶乘为 %f\n", i, factorial(i));
    return 0;
}
#include <stdio.h>

int fibonaci(int i) {
    if (i == 0) {
        return 0;
    }
    if (i == 1) {
        return 1;
    }
    return fibonaci(i - 1) + fibonaci(i - 2);
}

int main() {
    int i;
    for (i = 0; i < 10; i++) {
        printf("%d\t\n", fibonaci(i));
    }
    return 0;
}

可变参数


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

double average(int num, ...) {

    va_list valist;
    double sum = 0.0;
    int i;

    /* 为 num 个参数初始化 valist */
    va_start(valist, num);

    /* 访问所有赋给 valist 的参数 */
    for (i = 0; i < num; i++) {
        sum += va_arg(valist, int);
    }
    /* 清理为 valist 保留的内存 */
    va_end(valist);

    return sum / num;
}

int main() {
    printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2, 3, 4, 5));
    printf("Average of 5, 10, 15 = %f\n", average(3, 5, 10, 15));
}

内存管理

序号 函数和描述
1 void *calloc(int num, int size); 在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。
2 void free(void *address); 该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。
3 void *malloc(int num); 在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。
4 void *realloc(void *address, int newsize); 该函数重新分配内存,把内存扩展到 newsize

命令行参数

命令行参数是使用 main() 函数参数来处理的,其中,argc 是指传入参数的个数,argv[] 是一个指针数组,指向传递给程序的每个参数。

排序算法

  1. 冒泡排序
  2. 选择排序
  3. 插入排序
  4. 希尔排序
  5. 归并排序
  6. 快速排序

语言实例

  1. https://www.runoob.com/cprogramming/c-examples.html

经典 100 题

  1. https://www.runoob.com/cprogramming/c-100-examples.html