(欢迎纠错)
1 基本概念
CPU:运算器+控制器(向内储存器发送地址流数据)
1 Byte=8 Bit
,1 K=1024 Byte
,每个Byte有一个唯一的地址,一个指针4 Byte
预处理:
.c->.i
编译:.i/.c->.s
汇编:.s->.o
链接:.o->可执行程序
。通常预处理器与编译器集成在一起编译:转化成目标代码,但还需要标准启动代码和库代码合并起来(链接)才能形成可执行代码
小端存储法: 高地址存高字节、低地址存低字节;(手机ARM、ipad、x86结构、intel/ARM和DSP…)(IBM大型机、网络字节序用大端存储法)
Gcc常用编译选项:
-Wall
生成警告信息(配合-O
使用),-W
额外的警告信息,-pedantic
根据C标准生成警告、避免使用非标准特性,-ansi
禁用非标准C特性,-std=c99
指定编译器版本泛型编程(generic programming),指没有特定类型、但一旦指定一种类型就可以转换成指定类型的代码
2 格式化输入输出
printf的转换说明:
%(-)m.pX
m
为要显示字符最少数量(不够会自动扩充)、p
为精度,有负号时左对齐(空格会放在右边、+
表示始终加正负号);常用转换说明符:d
十进制整数,e
指数,f
十进制浮点数,g
指数或浮点数、适合范围变化大或无法预知大小的数读取
double
用%lf
,显示用%f
3 表达式与循环
运算符优先级: 最高的是非正式意义运算符(数组下标、成员选择等)、其次是单目运算符(
括号
>++
>*
),双目运算符(算术
>移位
>关系
>逻辑
>赋值
>条件(a?b:c)
),最后是逗号运算符注意各种运算符的优先级、结合性,但没必要记
%
取余数只用于整数,其他的用fmod
函数x+=y
与x=x+y
的区别: 后者对x
进行了两次求值,可能引起的副作用也都会出现2次while(i++ < n)
这种写法使程序更简洁形式参数:formal parameter,实际参数:actual argument
switch
语句记得加break
,否则会从匹配标签开始执行到switch
末尾可以接受的一种
goto
用法: 出现问题时想要从一组循环嵌套中跳出(用break只能跳出一个循环,比较麻烦)goto label; ... label: statement;
- 把常量放在判断相等的比较表达式左侧,减少出错:
while('\t'==c||''==c||'\n'==c){;} //一旦出错会马上捕获
4 基本类型
C99的
<stdbool.h>
提供了bool宏:bool flag=true;
这样使用typedef
可提高程序的可移植性sizeof(short)
函数返回size_t
类型,是无符号整型unsigned int
的别名。相比于strlen()
、sizeof
把字符串末尾不可见空字符也计算在内指针的类型:决定指针移动一位时的距离(
sizeof(指针类型)
)sizeof(a=b+1)
并没有向a赋值
5 数组
获得数组长度:
sizeof(a)/sizeof(a[0])
, 注意返回的是一个无符号数复制数组的一个方式:
memcpy(a,b,sizeof(a))
,来自<string.h>
头的内存复制函数复合字面量建立无名数组:
(int []){1,2,3,4,5};
一维数组中:
&a[i]
等价于a+i
,二维数组中:&a[i][j]
等价于a[i]+j
一维数组
a[i]
中,&a+1
代表跨越整个数组的长度,不同于a+1
(跨越数组一个元素的长度)绝大多数情况下,数组名的值是指向数组第1个元素的指针,除了两个例外:
sizeof
返回整个数组占用的字节、&
返回一个指向数组的指针
多维数组作为函数参数:
int mat[3][4]; void func(int (*mat)[4]); //指向数组的指针 //也可以写成: void func(int mat[][4]); //但不能写成: void func(int **mat); func(mat);
多维数组定义:
//正确: int a[][3]={{1,2,3},{4,5,6}}; //报错: int a[2][]={{1,2,3},{4,5,6}}; //只有第一维能根据初始化列表缺省地提供,其余的必须显式写出
6 函数
exit
导致程序退出,return
导致函数退出return 0;
控制权交给上一级(直到最初一级、结束程序);exit(0);
直接结束程序函数名(不带括号)返回该类型函数的地址
递归的工作原理: 函数调用时、变量存在堆栈上,每一次递归、之前调用的变量仍留在堆栈上但不能被访问,直到函数返回前一次调用
递归往往是用简洁性弥补其运行开销(冗余计算增长极快),一些情况用迭代实现更好
调用在后面定义的函数,必须在调用之前声明其函数原型;函数定义时结尾可不加分号、但声明结束时必须加,推荐全都加
用
stdarg
实现可变参数:#include <stdarg.h> double avg(int n, ...) { // n为省略号中参数个数 va_list var_arg; //定义一va_list类型 int count; double sum = 0; va_start(var_arg, n); //初始化va_list for (count = 0; count < n; count++) { sum += va_arg(var_arg, double); //按顺序读取va_list中的参数,注意类型一定要一致 } va_end(var_arg); //结尾要调用 return sum / n; } avg(3,1.2,2.3,3.4); //使用方式
7 指针
二维数组中
a
和a[0]
的区别: 类型不同,前者为(拿int举例)int (*)[num_cols]
,后者为int *
*p++
等价于*(p++)
;相当于a[i++]
。(*p)++
表示p所指元素值加1*(a+i)
代表a[i]
,*(a[i]+j)
和*(*(a+i)+j)
都代表a[i][j]
*p[4]
指针数组,元素全为指针的数组;(*p)[4]
是一个指针,指向一个4元素数组字符指针:
char *s="Hello";
等价于:char *s; s="Hello";
而字符数组不可以分开这样做,并且字符数组间不能进行赋值(s
输出的是Hello
、而*s
输出的是H
)
8 字符串
单引号:字符常量(代表一个整数); 双引号:字符串(指向无名数组起始字符的指针)
延续字符串到下一行: 前一行用
\
结尾C语言把字符串字面量作为字符数组(结尾为
\0
)来处理,把字符常量(单引号)作为整数处理getchar、putchar
每次处理一个字符。getchar
不会跳过空格、制表符和换行符,而scanf
会#include<stdio.h> ... char ch; while((ch=getchar())!=EOF){ //stdio.h中定义EOF putchar(ch); } //该程序会每次按下`Enter`输出缓冲区字符、当遇到文件结尾模拟(`Ctrl+D`或`Ctrl+Z`)时停止
puts()
函数只输出字符串,会自动在末尾加上换行符(类似还有fputs()
和printf()
);gets()
读取整行输入直到换行符(尽量别用、已淘汰、可以用fgets()
、而scanf()
适合读取单词)字符串最后要有
\0
,否则为字符数组常见创建字符串的方式:
// 数组形式中,数组名st1是地址常量,不能更改st1(不能++st1这样的操作);数组元素是变量、但数组名是常量 char st1[]="hello world."; // 指针形式,字符串字面量为const数据、不能修改st2指向的数据,但可以改变st2的值(可以++st2) const char *st2="Hello world too.";
atoi()
stdlib.h
中将字符串转成int,同理有atof()
(double)、atol()
(long)。另外strtol()
也可以
所谓的文本文件不过是二进制文件按照一定规则解码得到的(如ASCII一字节一字节解读)?
字符只是小型整数,所以用一个整型变量容纳字符值不会引起任何问题
两个问号放一起的时候注意“三字母词(trigrph)”
signed
关键字多用于char
类型,因为其他整形类型默认为有符号数,只有char
因编译器而异常用字符串函数:
size_t strlen(char const *string) //返回字符串(末尾必须为\0)长度,size_t为无符号数 //用strlen(x)>=strlen(y)而不是strlen(x)-strlen(y)>=0,无符号数必非负 char *strcpy(char *dst,char const *src) //复制字符串并返回一份拷贝,注意内存不能重叠或空间不足 char *strcat(char *dst,char const *src) //连接字符串到dst后面 int strcmp(char const *s1,char const *s2) //字符串比较、若相等返回0 //长度受限的字符串函数:strncpy、strncat、strncmp,多了一个参数size_t len
9 预处理器
#define PI 3.1415
编译时替换、末尾不用加分号#if 0 #endif
可以用来屏蔽含注释的代码预处理中的
#
运算符和黏合剂##
运算符:#define PSQR(x) printf("The square of " #x " is %d.\n",((x)*(x))) #define XNAME(n) x ## n int y=5,x1=2; PSQR(y); //输出为: "The square of y is 25" printf(XNAME(1)); //输出为2
带参数的宏:(若是表达式一定记得加括号,否则相邻操作符之间会产生不可预料的作用)
#define PRINT(FORMAT,VALUE) \ printf("The vaule of " #VALUE " is " FORMAT "\n",VALUE) //注意#argument的用法 #define ADD(num,value) \ sum##num+=value x=1; PRINT("%d",x+3); ADD(5,25); //把sum5这个变量加25
#undef
移除一个宏定义条件编译:
#if constant-expression statements #elif ... #endif //是否被定义: #if defined(symbol) //等价于#ifdef symbol #if !defined(symbol2) //等价于#ifndef symbol2
10 大型程序编写要点
include
中使用宏替换#if defined(IA32) #define CPU_FILE "ia32.h" #elif defined(IA64) #define CPU_FILE "ia64.h" #endif #include CPU_FILE
#error
检查不该包含的头文件//避免把头文件用于旧的非标准编译器 #ifndef _STDC_ #error This header requires a Standard C compiler #endif
echo_eof < words
,<
为重定向运算符、使程序用文件而不是键盘输入,同样>
表示输出到文件(覆盖)、而>>
表示附加到文件
宏可以完成一些函数做不到的事:
#define MALLOC(n,type) \ ((type*)malloc((n)*sizeof(type)) ) pi=MALLOC(25,int);
模块的客户(main、使用者),模块的接口(头文件),模块的实现(.c文件)。模块的好处:抽象、可复用性、可维护性;模块的特点:高内聚性、低耦合性;模块的类型:数据池、库、抽象对象、抽象数据类型(ADT)
11 结构、联合和枚举
允许把一个结构赋值给另一个结构,也允许把一个结构初始化为相同类型的另一个结构
结构中比较特殊的一种初始化方式:
struct book a={.value=10.9};
即只初始化部分成员(注意声明的结尾加分号)结构内部不能包含该结构本身,但可以是指向该结构的指针(构造链表、树常用)
利用结构来复制数组:
struct {int a[10];} a1,a2; a1=a2;
复合字面量,创建一个临时的结构:
part1=((struct part){528,"drive",10});
结构中储存字符串不要用字符指针而要用字符数组(字符指针将字符串储存在编译器储存常量的地方),如果要用请先用
malloc()
分配储存空间结构名不是地址(不同于函数、数组),访问地址要加上
&
两结构即使成员列表完全相同、也被编译器当做两种截然不同的类型
联合 类型,混合数据类型:
//该union可储存一个int或一个double或一个char union hold{ int digit; double bigfl; char letter; }
枚举类型(enum)是整数类型,目的是为了提高程序的可读性和可维护性
enum suit {CLUBS,DIAMONDS,HEARTS,SPADES}; typedef enum {FALSE,TRUE} Bool; enum suit2 {CLUBS=1,DIAMONDS=2,...}; //也可以选择不同数字,推荐加尾逗号
12 动态储存分配和函数指针
Segmentation fault
错误,说明程序试图访问未分配的内存void *malloc( unsigned int size )
,size是申请字节的大小,常用示例:int* p=(int*) malloc(5* sizeof(int) ) if(p==NULL) { printf("内存申请失败,退出"); return;
动态分配内存详解:
#include <stdlib.h> //使用malloc、exit必加 double *ptd; int n = 30; ptd = (double *)malloc( n * sizeof(double)); //创建一个数组(n可以用变量),分配了一块连续的内存 if (ptd == NULL) { //分配失败时返回NULL指针,NULL在stdio.h、stdlib.h、string.h、time.h等中均有定义 exit( EXIT_FAILURE); // EXIT_FAILURE表示程序异常终止、EXIT_SUCCESS表普通的程序结束(相当于0) free(ptd); //释放malloc分配了的内存 // calloc和malloc类似,不过还会自动把指针初始化为0: double *pt = calloc(n, sizeof(double)); // realloc能修改分配的内存块大小
灵活数组成员
struct vstring{ int len; char chars[]; //C99 only , flexible array member} }; struct vstring *str= malloc(sizeof(struct vstring)+n); str->len=n;
- 函数指针的使用(常作为其他函数的参数):
int f(int x); //声明函数 int (*pf)(int)=&f; //创建并初始化函数指针pf,&可以去掉、因为函数名总是被编译器转换为函数指针 //函数的调用: int ans=f(25); int ans=pf(25); // 直接这样调用也行,函数名就是个地址 int ans=(*pf)(25); // int *g() , ()优先级较高,故是个函数,返回类型是int * // int (*g)() , 是个指针,指向一个返回int类型的函数
13 声明、存储类型、作用域、变量类型
- 声明的语法:
extern const unsigned int a[10]; //1. extern、储存类型,有auto、static、extern、register四种 //2. 类型限定符,const、volatile两种 //3. 类型说明符,如int、char...
变量的3个性质:
- 存储期限。自动存储期:块终止时释放,静态存储期:整个运行期间占有同一个单元
- 作用域。 块作用域、文件作用域
- 链接(不同部分共享此变量的范围)。 外部链接:可以被程序中几个(或全部)文件共享,内部链接:只能属于单独一个文件,无链接:属于单独一个函数,不能共享
五种存储类别:
自动变量:自动存储期(函数自动分配的、运行时堆栈)、块作用域、无链接,声明在块或函数头(函数参数部分)中,最常见的类型
int main(voidl){ auto int plex;//显示声明成自动变量,注意auto的用法C和C++很不一样 }
寄存器变量:不储存在内存而是CPU寄存器(最快可用内存)中,无法获得变量地址,其他特性与自动变量相似(可以对指针用、这样就不必复制指针值、但必须被声明为局部变量?)
register int quick; //只是一个请求,可能被忽略而变成自动变量
块作用域的静态变量(静态无链接):和自动变量的作用域相同、但离开后变量不消失(静态存储期,对其他函数隐藏、对当前函数保留),声明在块内
void trystat(void){//不能在函数形参中用static static int stay=1; }
外部链接的静态变量: 有文件作用域、外部链接,未初始化的外部变量会自动设为0,并且只能用常量表达式初始化
extern int out;//在main之外声明外部变量,若变量定义在别的源文件中则需要加extern(不会分配存储空间,只提示定义在别处),否则不要加 int main(void){}
内部链接的静态变量:只能用于同一个文件中的函数、声明在函数外、用
static
关键词
- 内存的3个部分:
- 静态变量(外部链接、内部链接、无链接)、内存在编译时确定,程序开始时创建、结束时销毁
- 自动变量:作为栈处理、进入定义变量的块时存在、离开块时消失;(按创建顺序加入、相反顺序销毁)
- 动态分配的内存:“支离破碎”地分布在已用内存块之间(堆)、比栈内存要慢,malloc时分配free后释放
标识符声明
static
或external
只有第一次声明有效,多次声明无法更改其链接属性如何理解复杂声明:从内往外读、
[]
和()
还有.
(于是有了->
)优先*
从名字开始读取,按优先级顺序排序
优先级从高到低是: a. 声明中被括号括起来的部分 b. 后缀操作符、小括号表示函数、方括号表示数组 c. 前缀操作符:*表示指针
const后面若紧跟类型说明符(如int)、则作用与它,否则作用于它左边的星号
例子:
char * const *(*next)()
首先*next
说明是个指针,看外面有个括号说明是函数指针,再有个*
得出指针所指内容;再看函数返回类型,是个char * const
例子:
int *f()
,()
优先级较高、说明是个函数、*
表示返回值是个指针;int (*f)()
: 是个指向函数的指针、函数返回值是int;int *(*f)()
: 指向函数的指针、函数返回值为整形指针;int *f[]
: 是个数组、元素是整形指针;int (*f[])()
: 是个数组、数组元素为指针、指针类型为返回int的函数的指针;int *(*f[])()
: 是个数组、数组元素为指针、指针类型为返回int*的函数的指针复杂的类型转换符: 把声明中的变量名和末尾的分号去掉,剩余部分用括号“封装”即可
- 通过
-lname
选项告知编译器链接到libname.so
函数库;始终将-l
函数库选项放在命令行的最右端(首先需要让文件包含未解析的引用)
14 位运算符等
用处:编写系统程序(编译器、操作系统)、加密程序、图形程序、需要高执行速度或高效利用空间的程序
C语言中其他进制的表示方式:
移位运算符:
<< 左移
、>> 右移
:(为了可移植性,最好只对无符号数移位运算)unsigned int i=13; //1101 unsigned int j=i<<13; //110100 unsigned int j=i>>13; //11 i<<=2; //会改变操作数
~
按位取反、一元运算符(一定要弄清有多少位)&
按位与、^
按位异或、|
按位或
15 标准库
求余数和商:
#include <stdlib.h> //定义了div函数 #include <iostream> int main(int argc, char *argv[]) { auto result = div(25, 3); //返回div_t类型的一个结构 std::cout << "quot(商): " << result.quot << " , rem(余数): " << result.rem << std::endl; return 0; }
程序运行时间获取:
#include <time.h> clock_t t1=clock()/CLOCKS_PER_SEC; //开始运行到这个位置处花的时间,并转换为s //只是近似值
16 输入输出(高级、stdio.h)
文件指针:定义在
stdio.h
中的派生类型,C语言会自动打开stdin
、stdout
、stderr
三个标准文件。#include <stdio.h> #include <stdlib.h> FILE *fp; // 返回空指针则说明打开文件失败 if((fp=fopen("filename","r"))==NULL){ fprintf(stdout,"Can't open file.\n"); exit(EXIT_FAILURE); } while((ch=getc(fp))!=EOF){ //判断是否到了文件末尾 ;} if(fclose(fp)!=0){ // 成功关闭返回0,否则返回EOF fprintf(stderr,"Error closing file\n"); }
rewind(fp)
可以让程序回到文件开始处
windows下使用
fopen
等函数时注意路径的写法:使用/
或者\\
作为目录分隔符通过
argv
传递命令行参数:// argc为argv长度,argv是指针数组,*argv[0]为程序名,之后如*argv[1]为命令行参数 int main(int argc, char *argv[]){}