1972 年,为了移植与开发 UNIX 操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言。C 语言是一种通用的、面向过程式的计算机程序设计语言,它与 Java 编程语言一样普及,二者在现代软件程序员之间都得到广泛使用。强烈推荐郝斌老师的C语言自学教程,在入门 C 语言时给了我很大的启发和帮助。以下为我在学习和实战练习过程中所做的笔记,可供参考。
C编程预备
整数: int
,short int
,long int
。
浮点数(实数):float
,double
。
字符 :char
。
变量:本质就是内存中一段存储空间,变量必须的初始化。
定义变量: 数据类型变量名 = 要赋的值;
。
常量的表示:
- 整数:十进制传统写法,十六进制前面加 Ox 或 0X,八进制前面加 0。
- 浮点数:
float x = 3.2
orfloat x = 123.45e-2;
。 - 字符:单个字符用单引号括起来,字符串用双引号括起来。
整数是以补码的形式转化为二进制代码存储在计算机中的。实数是以 IEEE754 标准转化为二进制代码存储在计算机中的。
字符的存储本质上与整数的存储方式相同。字符的本质实际也是与整数的存储方式相同。
字节就是存储数据的单位,并且是硬件所能访问的最小单位,1字节 = 8位。
不同类型数据之间相互赋值:(数据类型)(表达式)
。
变量不能被重复定义。
输入和输出函数
输入函数:printf("输出控制符", 输出参数);
。
逻辑运算符:!=
==
&&
。
输出控制符:%d
, %ld
, %c
, %lf
, %x
, %o
, %s
。
输出函数:scanf("输入控制符", 输入参数);
。
处理非法输入:
1 | char ch; |
流程控制
流程控制:程序代码执行的顺序。流程控制的分类:顺序、选择、定义。
if 语句:if...else...
- C语言对真假的处理:非零是真,真用1表示,假用零表示。
else
后不跟判断句。
switch
:电梯程序。
循环:某些代码会被重复执行
for
语句;- 三目运算符
(A? B: C)
; while
语句:for 和 while 可以相互转换,但 for 的逻辑性更强,更不容易出错。do...while
:do…while…并不等价于 for,当然也不等价于 while,主要用于人机交互。- break 用来终止循环。
- continue 用于跳过本次循环佘下的语句。
数组
数组:为了解决大量同类型数据的存储和使用间题,为了模拟现实世界。
一维数组:为n个变量连续分配存储空间,所有的变量数据类型必须相同,所有变量所占的字节大小必须相等,int a[5];
。一维数组名不代表数组中所有的元素,一维数组名代表数组第一个元素的地址。
二维数组,int a[3][4];
总共是12个元素, 可以当做3行4列看待。a[i][j]
表示第 i+1 行第 j+1 列的元素。
int a[m][n];
: 该二维数组右下角位置的元素只能是 a[m-l][n-l]
。
C 语言中不存在多维数组,因为内存是线性一维的,n 维数组可以当做每个元素是 n-1 维数组的一维数组。
函数
函数:避免了重复性操作,有利于程序的模块化。逻辑上:函数是能够完成特定功能的独立的代码块。物理上:函数能够接收数据,能够对接受的数据进行处理能够将数据处理的结果返回。函数返回值的类型也称为函数的类型。
定义函数:函数定义的本质是详细描述函数之所以能够实现某个特定功能。
return 表达式:终止被调函数, 向主调函数返回表达式的值。break 是用来终止循环和 switch 的, return 是用来终止函数的。
函数的分类:
- 有参函数和无参函数;
- 有返回值函数和无返回值函数;
- 库函数和用户自定函数;
- 值传递函数和地址传递函数;
- 普通函数和主函数(main 函数)。
一个程序必须有且只能有一个主函数。主函数可以调用普通函数普通函数不能调用主函数。普通函数可以相互调用。
主函数是程序的入口,也是程序的出口。
函数调用和函数定义的顺序如果函数调用写在了函数定义的前面, 则必须加函数前置声明。
函数是 C 语言的基本单位,类是 Java, C#, C++ 的基本单位。
常用的系统函数:
double sqrt(double x);
:求的 x 的平方根。int abs(int x)
ordouble fabs(double x)
:求 x 的绝对值。
递归和栈。
变量的作用域和存储方式
全局变量:在所有函数外部定义的变量叫全局变量。全局变量使用范围:从定义位置开始到整个程序结束。
局部变量:在一个函数内部定义的变量或者函数的形参都统称为局部变量。局部变量使用范围: 只能在本函数内部使用。
在一个函数内部如果定义的局部变量的名字和全局变量名一样时, 局部变量会屏蔽掉全局变量。
静态变量;自动变量;寄存器变量。
指针
指针:表示一些复杂的数据结构,快速的传递数据,减少内存的耗用,使函数返回一个以上的值,能直接访问硬件,能够方便的处理字符串,是理解面向对象语言中引用的基础,是C语言的灵魂。
1 | typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型 |
地址:内存单元的编号,从零开始的非负整数,范围: 4G [0—4G-1]
。指针就是地址, 地址就是指针。
指针变量就是存放内存单元编号的变量,或者说指针变量就是存放地址的变量。
指针和指针变量是两个不同的概念。指针的本质就是一个操作受限的非负整数。
基本类型指针:int * p;
。
指针运算符:放在已经定义好的指针变量的前面。如果 P 是一个已经定义好的指针变量,则 *p
表示以 p 的内容为地址的变量。
如何通过被调函数修改主调函数普通变量的值:
- 实参必须为该普通变量的地址
- 形参必须为指针变量
- 在被调函数中通过
*形参名 =
的方式就可以修改主调函数相关变量的值。
指针和一维数组:
- 一维数组名 a[0] 是个指针常量,存放的是一维数组第一个元素的地址。
- 下标和指针的关系:如果 P 是个指针变量, 则
p[i]
永远等价于*(P+i)
。 - 确定一个一维数组需要两个参数:数组第一个元素的地址和数组的长度。
- 指针变量不能相加不能相乘也不能相除。如果两个指针变量指向的是同一块连续空间中的不同存储单元,则这两个指针变量才可以相减。
sizeof(数据类型)
:返回值就是该数据类型所占的字节数。指针变量,无论它指向的变量占几个字节,该指针变量本身只占四个字节。
一个变量的地址是用该变量首字节的地址来表示。
指针和二维数组:
- 指针和函数;
- 指针和结构体;
- 多级指针。
动态内存分配
传统数组也叫静态数组。
传统数组的缺点:数组长度必须事先制定,且只能是常整数,不能是变量,一旦定义, 其长度就不能在更改。传统数组的内存程序员无法手动释放,在一个函数运行期间,系统为该函数中数组所分配的空间会一直存在,直到该函数运行完毕时,数组的空间才会被系统释放。传统方式定义的数组不能跨函数使用。
int 类动态数组:int *p = (int *)malloc(int len);
。本语句分配了两块内存, 一块内存是动态分配的,总共 len 个字节, 另一块是静态分配的,并且这块静态内存是P变量本身所占的内存, 总共 4 个字节。malloc 只有一个 int 型的形参,表示要求系统分配的字节数。
malloc 函数的功能是请求系统 len 个字节的内存空间, 如果请求分配成功,则返回第一个字节的地址, 如果分配不成功, 则返回 NULL。
malloc 函数能且只能返回第一个字节的地址, 所以我们需要把这个无任何实际意义的第一个字节的地址(俗称干地址)转化为一个有实际意义的地址,因此 malloc 前面必须加(数据类型 *
), 表示把这个无实际意义的第一个字节的地址转化为相应类型的地址。
int *p = (int *)malloc(50);
表示将系统分配好的 50 个字节的第一个字节的地址转化为 int *
型的地址。更准确的说是把第一个字节的地址转化为四个字节的地址,这样 P 就指向了第一个的四个字节,P+1 就指向了第2个的四个字节,p+i 就指向了第 i+1 个的4个字节。p[0] 就是第一个元素,p[i] 就是第 i+1 个元素。
free(p)
表示把P所指向的内存给释放掉。
静态内存是由系统自动分配,由系统自动释放,静态内存是在栈分配的。动态内存是由程序员手动分配,手动释放,动态内存是在堆分配的。静态内存不可以跨函数使用。
函数 | 描述 |
---|---|
void *calloc(int num, int size); | 在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。 |
void free(void *address); | 释放 address 所指向的内存块,释放的是动态分配的内存空间。 |
void *malloc(int num); | 在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。 |
void *realloc(void *address, int newsize); | 重新分配内存,把内存扩展到 newsize。 |
结构体
结构体:为了表示一些复杂的事物,而普通的基本类型无法满足实际要求,把一些基本类型数据组合在一起形成的一个新的复合数据类型,这个叫做结构体。
1 | struct tag { |
定义结构体:结构体变量定义的同时可以整体赋初值,如果定义完之后, 则只能单个的赋初值。
取出结构体变量中的每一个成员:
结构体变量名.成员名
;指针变量名->成员名
。
使用结构体指针变量作为函数参数来传递。结构体变量不能相加减乘除,但结构体变量可以相互赋值。
共用体:
1 | union [union tag] |
位域声明:
1 | struct |
typedef:
1 | typedef unsigned char byte; |
枚举和补码
枚举:把一个事物所有可能的取值一一列举出来。
1 | enum 枚举名 {枚举元素1,枚举元素2,……}; |
原码:也叫符号-绝对值码,最髙位 0 表示正,1表示负,其余二进制位是该数字的绝对值的二进制位。原码简单易懂,加减运算复杂,存在加减乘除四种运算,增加了CPU的复杂度,零的表示不唯一。
反码:反码运算不便,没有在计算机中应用。
移码:移码表示数值平移 n 位,n 称为移码量。移码主要用于浮点数的阶码的存储。
补码:
- 求正整数的二进制:除 2 取余,直至商为零,余数倒叙排序。
- 求负整数的二进制:先求与该负数相对应的正整数的二进制代码,然后将所有位取反,末尾加1,不够位数时, 左边补1。
- 零的二进制全是零。
- 如果首位是 0,表明是正整数, 按普通方法来求。
- 如果首位是 1, 则表明是负整数,将所有位取反,末尾加1,所得数字就是该负数的绝对值。
- int 类型变量所能存储的最大正数用十六进制表示是:7FFFFFFF。
- int 类型变量所能存储的绝对值最大的负整数用十六进制表示是:80000000。
链表
算法:对存储数据的操作,对不同的存储结构,要完成某一个功能所执行的操作是不一样的。算法是依附于存储结构的,不同的存储结构, 所执行的算法是不一样的。广义的算法也叫泛型,无论数据是如何存储的,对该数据的操作都是一样的。
数组查找快,增删慢;链表查找慢,增删快,占位小。
首节点是存放第一个有效数据的节点。尾节点是存放最后一个有效数据的节点。头结点是首节点前面的那个节点。头结点的数据类型和首节点的类型是一摸一样的。头结点并不存放有效数据设置头结点的目的是为了方便对链表的操作。
确定一个链表需要一个参数:头指针,头指针是存放头结点地址的指针变量。
二进制全部为零的含义 —0000000000000
的含义:
- 数值零;
- 字符串结束标记符
\0
; - 空指针
NULL
。NULL
本质也是零,但这个零不代表数字零,而表示的是内存单元的编号零。以零为编号的存储单元的内容不可读,不可写。
位运算符:
- 按位与
&
; - 按位或
|
; - 按位取反
~
; - 按位异或
^
; - 按位左移
<<
; - 按位右移
>>
。
C 输入输出
标准文件 | 文件指针 | 设备 |
---|---|---|
标准输入 | stdin | 键盘 |
标准输出 | stdout | 屏幕 |
标准错误 | stderr | 您的屏幕 |
getchar()
&putchar()
函数;gets()
&puts()
函数;scanf()
和printf()
函数。
文件
文件指针fp
:fopen(文件名, 使用方式);
、fclose(文件名, 使用方式);
。
文件读取:
fgets(str,n,fp)
、fputs(str,fp)
;fprintf(文件指针,格式字符串,输出表列)
、fscanf(文件指针,格式字符串,输入表列)
;fread(buffer,size,count,fp)
、fwrite(buffer,size,count,fp)
;ferror(fp);
、clearerr(fp)
。
fopen()
函数原型:
1 | FILE *fopen( const char * filename, const char * mode ); |
C 预处理器(C Preprocessor)
C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤,它们会指示编译器在实际编译之前完成所需的预处理。
所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。
指令 | 描述 |
---|---|
#define | 定义宏 |
#include | 包含一个源代码文件 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个 #if……#else 条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
C 错误处理
C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。
在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno
,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h
头文件中找到各种各样的错误代码。
C 语言提供了 perror()
和 strerror()
函数来显示与 errno
相关的文本消息。
C 命令行参数
执行程序时,可以从命令行传值给 C 程序,这些值被称为命令行参数,它们对程序很重要,特别是当您想从外部控制程序,而不是在代码内对这些值进行硬编码时,就显得尤为重要了。命令行参数是使用 main() 函数参数来处理的,其中,argc
是指传入参数的个数,argv[]
是一个指针数组,指向传递给程序的每个参数。
对命令行参数解析:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37int main(int argc, char *argv[])
{
char *optstr = "p:n:m:c:";
struct option opts[] = {
{"path", 1, NULL, 'p'},
{"name", 1, NULL, 'n'},
{"mtime", 1, NULL, 'm'},
{"ctime", 1, NULL, 'c'},
{0, 0, 0, 0},
};
int opt;
while((opt = getopt_long(argc, argv, optstr, opts, NULL)) != -1){
switch(opt) {
case 'p':
strcpy(path, optarg);
break;
case 'n':
strcpy(targetname, optarg);
break;
case 'm':
modifiedtime = atoi(optarg);
break;
case 'c':
changetime = atoi(optarg);
break;
case '?':
if(strchr(optstr, optopt) == NULL){
fprintf(stderr, "unknown option '-%c'\n", optopt);
}else{
fprintf(stderr, "option requires an argument '-%c'\n", optopt);
}
return 1;
}
}
findInDir(path);
return 0;
}
C 库函数
C 标准库是一组 C 内置函数、常量和头文件,比如 <stdio.h>
、<stdlib.h>
、<math.h>
,等等。