/*其下知识点是本人在某培训公司帮学员所总结,想对C语言有所了解的同学可以看看!有问题也可以在下方留言。我会适时给与大家回答。
希望对大家有用!谢谢!
红色字体的知识点为重要熟练掌握,并能敲出相应代码。蓝色字体的为熟悉,下次使用时查下相应知识点能用出即可。一般黑色为了解。*/
第一课:
一、C语言程序的开发周期
1.编辑:编写源程序,保存退出。hello.c
2.预编译:处理包含文件和宏扩展。
3.编译:生成目标模块。hello.o
4.链接:将多个目标模块和库链接成一个可执行程序。a.out
5.载入:将可执行文件读入内存,形成进程映像。
6.运行:处理器执行进程映像中的代码。从main函数开始。
二、gcc常用选项
-o 输出文件名:指定生成的文件名(可执行文件)
-E:只做预编译
-c:只做编译,不做链接 会生成(.o)
-S:产生汇编文件 会生成(.s)
-l:指定链接库 例子:gcc a.c–l库名
-L:指定库的目录 -L目录名
-std=c89/c99:指定语言标准
三、#include指令
用<>包含的头文件,到标准头文件路径(usr/include)下寻找。
用" "包含的头文件,优先到当前目录下寻找。
一般情况下,建议用<>包含标准头文件,用""包含自定义头文件。如果头文件既不在当前目录下,也不在标准头文件路径中,可以通过-I选项告诉编译器到哪里寻找这样的头文件。
四、C语言程序的注释
1.以/*开始,以*/结束,中间的部分为注释,没有行数限制,不能嵌套。
2.//单行注释,从//开始到本行末尾是注释。
a /* b c
d */ e
f
b c d是注释
a // b c
d // e
f
b c e是注释
五、编码规范
1.语句尽量分在多行中书写。
int main (void) {printf ("helloword"); return 0;} // 不好
int main (void) {
printf ("hello world\n");
return 0;
}
2.空格可以使代码更加清晰。
3.缩进可以增强代码的层次感。
4.必要空行可以增强代码的逻辑性。
5.命名风格
1)匈牙利命名
int nAge;
CreateWindow
2)下划线方式
create_window
六、变量
1.变量就是内存中的一个区域,其值可以改变,通过变量名加以标识和区分,变量有类型和值。
2.变量类型包括char(1字节)、int(4字节)、short(2字节)、long(4字节)、long long(8字节)、float(4字节)、double(8字节),等等。另外还包括复合类型。
3.变量的定义
类型变量名 =初值;
int a = 10;
int b; // 未初始化的变量,其值不确定
4.可以printf函数输出变量的值
printf ("%d",a);
printf ("%d%d", a, b);
printf ("a=%d,b=%d\n", a, b);
a=10, b=1434
整型:%d
字符:%c
浮点数:%f/ 单精度 %lf双精度
5.变量名必须是合法标识符
1)必须以字母或下划线开头
int abc; // ok
int _abc; // ok
int 2abc; // error !
int * abc; // error !
2)可以包含字母、下划线和数字
int a_2; // ok
int a-2; // error !
3)大小写敏感
int a, A; // 两个变量
4)不能与关键字冲突
int float; // error !
5)理论上变量名的长度没有限制,但是具体的编译器往往会截断。
6)变量命名最好有意义。
int age;
float salary;
int max;
int ttt , zyu; // 不好
七、基本数据类型
C语言的基本数据类型包括:
char:字符/单字节整数,1字节
unsigned char:非负的单字节整数,1字节
short:双字节整数,2字节
unsigned short:非负的双字节整数,2字节
int:四字节整数,4字节
unsigned int:非负的四字节整数,4字节
long:四字节整数,4字节
unsigned long:非负的四字节整数,4字节
long long:八字节整数,8字节
unsigned long long:非负的八字节整数,8字节
float:浮点数,4字节
double:双精度浮点数,8字节
long double:长双精度浮点数,12字节
1.字符型
1)字符型变量的底层存储就是整数,对于字符而言存储的是该字符在ASCII表中的代码。
'A' –> 65
'a' - > 97
'0' - > 48
2)字符常量通过一对单引号('')表示。
char c = 'A'; // 实际上c中存放的是65
3)用printf显示字符的时候,如果用%c显示的是字符,如果用%d显示的就是ASCII码。
4)转义字符
\n:换行,光标移到下一行行首
\r:回车,光标移到当前行行首 //一般用不到
\t:制表
\v:垂直制表
\b:退格
\\:\
\':'
\":"
%%:%
5)取值范围
char:8位,1字节,有符号,-128 ~127(-2^7 ~ 2^7-1)
unsigned char:8位,1字节,无符号,0 ~ 255 (0~ 2^8 - 1)
2.整型
1)int代表16位或32位整数,可以用short/long进行修饰,short (int)用16位表示,long (int)用32位表示。有符号的int有正负之分,无符号的int (unsigned int)只有0和整数。
2)整型常数
100:默认为int,十进制
100L:long
100LL:long long
100u:unsigned int
100UL:unsigned long
0100:八进制,64
0x100:十六进制,256
3.浮点数
Float
/double :一般我们使用浮点型数据时都会使用double来声明变量
/long double
1.23:默认为double
1.23f:float
1.23L:long double
浮点类型在计算机内部存储都是近似值。
十、三个常用函数
printf (格式化串,变量表);
scanf (格式化串,地址表);
从键盘读取数据到变量中。
int a;
scanf ("%d",&a);
一次调用输入多个数据,各个数据以空白字符(空格、制表、回车)分隔。
sizeof ():获取一个变量或类型的字节数。
第二课
一、十进制与二进制
十进制:逢十进一
二进制:逢二进一
0 0000 0000
1 0000 0001
2 0000 0010
3 0000 0011
4 0000 0100
5 00000101
6 0000 0110
7 00000111
8 0000 1000
9 0000 1001
二、进制转换
1.二进制与十进制的转换
1)位与权
93 = 9 * 10 + 3 * 1
位 权 位 权
整数93在一个字节中的二进制形式:
0 1 0 1 1 1 0 1 - 位
128 64 32 16 8 4 2 1 - 权
2)零和正数:符号位(最高位)为0
A.二转十:一位加权,零位不见
01011101=64+16+8+4+1=93
01101101=64+32+8+4+1=109
B.十转二:有权添一,无权补零
93-64=29-16=13-8=5-4=1-> 01011101
109-64=45-32=13-8=5-4=1-> 01101101
127-64=63-32=31-16=15-8=7-4=3-2=1-> 01111111
3)负数:符号位(最高位)为1
A.二转十:取反加一,转十添负
10100011取反01011100加一01011101转十
64+16+8+4+1=93添负-93
10010011取反01101100加一01101101转十
64+32+8+4+1=109添负-109
B.十转二:去负转二,取反加一
-93去负93转二93-64=29-16=13-8=5-4=1->01011101取反10100010加一10100011
-109去负109转二109-64=45-32=13-8=5-4=1->01101101取反10010010加一10010011
2.二进制和十六进制的转换
四位二进制对应一位十六进制
十进制 二进制 十六进制
0 0000 0
1 0001 1
2 0010 2
3 0011 3
4 0100 4
5 0101 5
6 0110 6
7 0111 7
8 1000 8
9 1001 9
10 1010 A
11 1011 B
12 1100 C
13 1101 D
14 1110 E
15 1111 F
一个字节对应两位十六进制
93 0101 1101 0x5D
109 0110 1101 0x6D
-93 1010 0011 0xA3
-109 1001 0011 0x93
-191 1111 1111 01000001 0xFF41
3.二进制和八进制的转换
三位二进制对应一位八进制
十进制 二进制 八进制
0 000 0
1 001 1
2 010 2
3 011 3
4 100 4
5 101 5
6 110 6
7 111 7
93 01 011 101 0135
109 01 101 101 0155
-93
-109
-191
三、运算符与表达式
1.算数运算
1)+、-、*
2)/、%
整数相除,取整数部分。
/和%不能对整数0进行操作,否则会中断程序。但是可以对浮点0进行操作,但是得到的结果是inf,表示无穷大。
%不能对浮点数使用。
/向零取整(取更接近0的数字)。
%的结果与其左操作数的符号一致。
2.赋值运算
1)将赋值运算符右边的值付给左边的变量。
int a;
a = 10; // 将10赋给a
2)赋值表达式的值就是赋值表达式左操作数的值。
int a, b, c;
a = b = c = 10; // a = (b= (c = 10));
printf("%d,%d,%d\n", a, b, c); // 10,10,10
3)赋值运算符的左操作数必须是一个左值——可被赋值的对象——变量,常量或表达式不能被赋值。
100 = 200; // error
100 = a; // error
a + b = c; // error
4)赋值运算符是“=”而不是“==”。
5)赋值运算符还可和其它运算符结合起来使用,构成复合赋值。
a += b; // a = a + b
a %= b; // a = a % b
6)初始化与赋值是不同的。
int a = 10; // 初始化
a = 20; // 赋值
3.自增减运算
1)++/--:变量自增/减1。
int a = 10;
a++; // a:11
a--; // a:10
++a; // a:11
--a; // a:10
2)不能用于常量。
100++; // error
--100; // error
3)可以用于所有的整型类变量,以及浮点类,但是多数情况下用于int。
4)前后缀自增减的区别:
前缀:先自增减,在计算表达式。
后缀:先计算表达式,再自增减。
a++表达式和++a表达式的值都是从操作数a的内存空间中得到的。二者的区别在于,一个是先取表达式的值再自增,另一个是先自增在取表达式的值。
5)忠告:自增减运算最好不要在一个表达式中使用多次。
4.关系运算
>/ >=/ </ <=/==/ !=
关系运算表达式的值是整数:1或者0。
逻辑真:1
逻辑假:0
5.逻辑运算
1)逻辑与(&&)、逻辑或(||)、逻辑非(!)
A && B:只有当A和B的值都为真(非零)时,表达式的值才为真。
A || B:只有当A和B的值都为假(零)时,表达式的值才为假。
! A:当A的值为真时,表达式的值为假,当A为假时,表达式的值为真。
2)短路运算
A && B:如果A的值为假,则B不处理。
A || B:如果A的值为真,则B不处理。
6.位运算
1)位与(&)/位或(|)/位异或(^)/位反( ~)
位与:参与运算的两位都是1结果为1,否则为0。位与可以置某一位为0,也可以得到某一位的值。
10110001 A
11101111 B
&)-----------
10100001 C
^
10110001 A
00010000 B
&)----------- 若A&B==B,则A中此位为1
00010000 C
^
位或:参与运算的两位都是0结果为0,否则为1。位或可以置某一位为1,也可以得到某一位的值。
10100001 A
00010000 B
|)------------
10110001 C
^
10100001 A
11101111 B
|)------------ 若A|B==B,则A中此位为0
11101111
^
异或:参与运算的两位相异为1,相同为0。异或可以翻转某一位,想翻哪位哪位就是1,其余为0。
10101010 A
00100100 B
^)------------
10001110 C
^ ^
A^B->C
C^B->A
C^A->B
异或还可用于在不增加第三变量的前提下,交换两个变量的值。
int a = 10, b = 20;
int c = a;
a = b;
b = c;
--------------------
a : 1011
b : 0110
a = a ^ b; // a : 1101
b = a ^ b; // b : 1011
a = a ^ b; // a : 0110
位反:按位取反
01100001
~)-----------
10011110
2)左右移位
A.有符号数:左移补零,右移补符
10101010 << 1 ->01010100
-86 84
10101010 >> 1 ->11010101
-86 -43
B.无符号数:左移右移全补零
10101010 << 1 ->01010100
170 84
10101010 >> 1 ->01010101
170 85
在不发生溢出的情况下,左移一位即乘2,右移一位即除2。
00000001 << 1 ->00000010
1 2
7.取地址和解引用(取目标)运算
取地址&:获取一个变量的地址,即其首字节的地址。
解引用*:根据地址获得变量的值。
8.类型转换
1)类型升级:由低级类型转换为高级类型。浮点高于整型,长整型高于短整型(long long > long > int > short >char)。若有符号类型能够表示无符号类型的值,则取有符号类型,否则按无符号处理。
2)强制转换:目标类型变量 = (目标类型)源类型变量。
double f = 1.23;
int n = (int)f; // n : 1
无论哪种形式的类型转换对于源变量都不会产生任何影响。
9.条件运算
条件表达式 ? 表达式1 :表达式2
若条件表达式为真,则整个表达式的值取表达式1的值,否则整个表达式的值取表达式2的值。
10.逗号运算
逗号表达式:表达式1, 表达式2, ..., 表达式n
从左至右依次计算各个子表达式的值,以表达式n的值作为整个逗号表达式的值。
int a=2, b=4, c=6, x,y;
y = (x = a+b, b+c);
// x = a + b;
// y = b + c;
printf(%d,%d\n", x, y); // 6,10
不是所有出现逗号的地方都是逗号表达式,如在变量定义/声明、函数的参数表等场合,逗号仅仅是作为变量或参数的分隔符。
11.运算符的优先级和结合序
按优先级从高到低排列:
初等运算:()、[]、.、->
单目运算:!、~、++、--、类型转换、&(取地址)、*(解引用)、sizeof
算数运算:*、/、%高于+、-
移位运算:>>、<<
关系运算:>、>=、<、<=高于==、!=
位与运算:&
异或运算:^
位或运算:|
逻辑运算:&&高于||
条件运算:?:
赋值运算:=、复合赋值
逗号运算:,
多数运算符具有左结合性,单目/三目/赋值运算符具有右结合性。
左结合:
a - b + c <==> (a -b) + c
右结合:
-----a <==>-(--(--a)) // g++
a > b ? a : c > d ?c : d <==> a > b ? a : (c > d ? c : d)
a = b = c <==> a =(b = c)
括号不怕冗余。
第三课
流程控制
结构化程序设计,就是以顺序、分支、循环三种基本控制结构构建任意复杂的单入口单出口的程序。
一、条件分支
1.语法形式
if (表达式1) {
当表达式1为真(值非零)时执行的语句;
}
else if (表达式2) {
当表达式2为真(值非零)时执行的语句;
}
...
else {
当表达式1-N都不为真时执行的语句;
}
2.if只能出现1次,else if可以出现0-N次,else可以出现0-1次。
3.if-else结构应用于需要根据不同的条件执行不同代码的场合。
4.if-else结构最多只能执行1个语句块。若有else分支,则必选其一执行,若无else分支,则可选其一执行。
5.如果{}中只有一条语句,或者一个独立的控制结构,那么可以省略该{}。
6.else和else if总是和最近的if配对。
y=f(x)
x y
<0 x - 1
=0 x
>0 x + 1
y = x + 1;
if (x >= 0) {
if (x == 0)
y = x;
}
else
y = x - 1;
二、开关分支
1.语法形式
switch (控制表达式) {
case 常量表达式1:
语句块1;
break;
case 常量表达式2:
语句块2;
break;
...
default:
语句块X;
break;
}
2.控制表达式被当做整数处理,可以是字符,但是不能是浮点数和字符串。常量表达式必须是常量,如:3、'A'、2+5。不允许有重复的分支。
3.default不一定在最后,但是如果default出现在其它case之前,其最后的break不能省略。
4.一般而言所有能够使用switch-case结构的场合都可以用if-else替代,反之不行。
三、循环
1.while循环
while (循环控制表达式) {
循环体语句块;
}
S1:计算循环控制表达式,若为真则执行循环体语句块,否则退出循环;
S2:执行循环体语句块后,执行S1。
1)如果循环控制表达式恒为真,则构成无限循环;
2)while循环的循环体可能一次都不执行。
2.do-while循环
do {
循环体语句块;
} while (循环控制表达式); //分号一定要有
S1:执行循环体语句块;
S2:计算循环控制表达式,若为真则执行S1,否则退出循环。
do-while循环的循环体至少会被执行一次。
通过break语句可以提前退出循环。break只能退出包含该语句的最内层循环。
3.for循环
for (表达式1;表达式2; 表达式3) {
循环体语句块;
}
S1:计算表达式1;
S2:计算表达式2,若为真则执行循环体语句块,否则退出循环;
S3:计算表达式3,执行S2。
T
1 -> 2 -> 体
F/ \ /
出 3
通常通过表达式1初始化循环变量,通过表达式2判断循环终止条件,通过表达式3更新循环变量。
continue语句:中断本次循环,继续下一次循环。对于while/do-while循环,continue ->计算循环控制表达式 -> ...,对于for循环,continue ->计算表达式3 -> 计算表达式2 ->
第四课
函数
一、函数的概念
int max = 0, i;
for (i = 0; i < 10; i++)
if (i != max && scores[i] > scores[max])
max = i;
scores[max]就是最高分
函数就是一系列语句的组合,用以实现一些相对独立且具有一定通用性的功能。
y = f(x)
y = kx+b
二、函数的定义
1.语法
返回类型函数名 (形参表) {
函数体语句;
}
int main (void) {
// ...
return 0;
}
三、函数的声明
声明语法:
返回类型函数名 (形参表);
1.函数在使用前最好进行声明,如果没有给出函数的显示声明,则编译器会采用如下隐式声明:
int func ();
返回int,且参数表任意。
2.如果函数返回类型不是int,或者希望编译器对函数调用的参数匹配性做检查,那么就要显示地对函数进行声明。
3.函数声明中的参数表,空括号“()”表示可以接受任意参数,而void,表示不接受任何参数。
4.如果函数的定义在函数调用之前,定义本身即是声明。
四、函数的调用
调用语法:
函数名(实参表);
1.有参数的函数在定义时使用的参数叫形参,当调用该函数时需要传入的参数叫实参。将实参传递给形参的过程,相当于将实参的值赋值给形参变量。从内存的角度看,实参属于调用者的栈区,形参属于被调用者的栈区。
2.当以数组名作为实参调用函数时,实际传给形参的是该实参数组的首地址(而非实参数组的副本),因此在函数中对形参数组的修改也就是对实参数组的修改。
在函数中对形参数组不用通过sizeof(arr)/sizeof(arr[0])获得该数组的长度。如果函数有必要了解数组中有效数据的个数,应该通过另一个附加参数传入该函数。
3.return可以用于退出函数,exit()用于退出整个程序,只有在main函数中,二者才相当。
五、递归与递推
1.递归就是函数自己调用自己。
n! = n*(n-1)(n-2)...1
n!=n*(n-1)!
使用递归的时机:需要进行的操作就是正在进行的操作。
注意:递归一定要有终止条件,否则就会形成无限递归。
2.递推就是一种迭代算法。
a(n+1) = a(n) + d;
费氏数列:f(n) = f(n-2)+ f(n-1), f(1)=f(2) = 1
1 1 2 3 5 8 13 ...
第12项的值?
递归函数可以简化算法,但是效率不高。如果不是十分必要的话,尽可能避免使用递归。
汉诺塔问题:递归。
A B C
N
第一步:通过某种操作将N-1个盘子借助C移到B上。
第二步:将A上第N个盘子移到C。
第三步:通过某种操作将B上的N-1个盘子借助于A移到C上。
循环递归:func_a -> func_b _> func_c -> func_a
交叉递归:func_a -> func_b _> func_a
自递归:func_a -> func_a
第五课
作用域与可见性
一、变量和作用域
1.局部变量:定义在函数或语句块(被一对{}括起来的若干条语句)中的变量(包括形参)。局部变量的可见性仅局限在定义该变量的函数或语句块中。其生命期由函数栈的压入和弹出决定。内层作用域的标识符会隐藏外层作用域的同名标识符。
2.全局变量:定义在所有函数之外的变量。其生命期与进程一致。在程序中居于最外层作用域。全局变量全局可见。
全局变量的外部声明:在一个源文件(b.c)中访问定义在另一个源文件(a.c)中的全局变量,需要在访问之前,对其进行外部声明。
// a.c
int g = 100; // 定义于a.c
// b.c
extern int g; // 外部声明于b.c
void foo (void) {
g++;
}
全局变量的外部声明一定要和改变量被定义的类型相一致。
3.静态局部变量:作用域和生命期与全局变量一致,但可见性与局部变量一致。
4.静态全局变量:将全局变量的可见性限制在定义该变量的源文件中。
5.全局函数:即通常所言函数。在C语言所有的函数都是全局函数。
6.静态全局函数:只有在定义该函数的源文件中才能调用该函数。
作用域和生命期 可见性
局部变量函数/块 函数/块
静态局部变量进程 函数/块
全局变量进程 所有文件
静态全局变量进程 定义文件
全局函数进程 所有文件
静态全局函数进程 定义文件
指针与字符串
一、指针
1.指针就是存放地址的变量。在32位系统上,一个指针变量占用4个字节。在64位系统上,一个指针变量占用8个字节。
2.指针类型、取地址、解引用
1)指针类型
int* pa;
int *pa;
int * pa;
语义:pa是一个指针,该指针指向一个int型的数据,即pa存放一个int型数据的地址。
int* pa, pb; // pa是int*,pb是int
int *pa, *pb;
2)取地址——&
int a;
int* pa = &a; // pa指向a,a是pa的目标,pa是a的指针,pa中存放着a的地址。
3)解引用(取目标)——*
*pa = 100; // 将100赋值给pa的目标,即赋值给a
3.指针的用法
1)将指针作为函数的参数,传递变量的地址,进而在多个函数中访问相同的内存数据。
2)指针也可以作为函数的返回值,但是不要返回指向局部变量的指针。因为函数返回以后,其局部变量所占用的内存将随函数栈一起被释放,所得到的指针为野指针。可以返回全局变量、静态局部变量、实参变量以及成员变量的指针。因为这些变量在函数返回以后依然有效。
int foo (void) {
return 10;
}
a = foo ();
int* foo (void) {
...
return p;
}
int* foo (void); // 函数声明,声明一个没有参数,返回int*的foo反函数
int (*foo) (void); // 函数指针
void foo (void) {
}
没有返回值的函数。
4.指针计算
1)一个指针型数据可以和一个整数做加减法计算,表示地址计算。所得结果与指针的类型有关。
int型指针+1:地址+4
short型指针+1:地址+2
char型指针+1:地址+1
...
X型指针+/-N:地址+/-N*sizeof(X)
2)通过指针计算可以访问数组中的元素。所谓的下标运算“[]”,其本质就是指针计算。
3)相同类型的指针之间可以做减法运算,所得为两个指针之间的距离,以指针类型为单位。
5.数组和指针
1)数组名就是数组的首地址,即首元素的地址,其本质就是一个指针,但其类型为常量。
2)以数组为参数的函数,所传递的实参是数组的首地址,而形参就是数组元素类型的指针。
6.常量、常量指针和指针常量
1)const可以修饰普通变量,该变量就会被当做常量来处理,即其值一经初始化就再不能被修改。
2)常量指针:指向常量的指针,即无法通过该指针修改其目标。
const int* p;
int const* p;
3)指针常量:指针型的常量,即无法修改其值的指针。
int* const p;
const int* const p; // 常量指针常量,p本身不能改,其目标亦不能改
二、字符串
1.在C语言中没有专门的字符串类型,通常情况下,字符串可以用三种方式表示:
1)字面值方式:"hello world"。不能修改,但是可以被赋值给变量,并在变量中修改。字面值方式的字符串后面自动追加'\0'结束符。字面值可以拼接:
"hello""world"
" china"==> "hello world china"
2)字符数组方式:char str[] = {...};以char型数组表示字符串,注意结尾符'\0',必须手动加入数组中。
3)字符指针方式:char* psz = ...;可以指向字面值字符串,也可以指向字符数组字符串。同样有效字符串必须以'\0'结尾。
char str[] ="abcdefg";
str[1] = '1';
*(str+2) = '2';
str[4] = 0; // = '\0';
printf ("%s\n",str);
输出:a12d
str[4] = '0';
printf ("%s\n",str);
输出:a12d0fg
-------------
char str1[] ="ABC";
char str2[] ={'A','B','C'};
sizeof (str1)>sizeof(str2)
-------------
int a;
scanf ("%d",&a);
printf ("%d",a);
char str[128];
scanf ("%s",str);
2.字符串I/O函数
char* psz;
scanf ("%s",psz); // 错误!
char* gets (char* str);
从标准输入读取字符串保存到str所指向的内存中,直到遇到\n为止,并将\n转换为\0。返回str。
int puts (char* str);
向标准输出打印str所指向的字符串,并将\0转换为\n。成功返回非0,失败返回0。
3.其它字符串常用函数
string.h
size_t strlen (char*str);
返回str的长度。
char* strcpy (char* to,const char* from);
将from所指向的字符串拷贝到to所指向的内存中。返回to。
char* strcat (char* str1,const char* str2);
将str2所指向的字符串连接到str1所指向字符串的末尾。返回str1。
使用strcpy/strcat的时候,to/str1所指向的内存空间一定要足够大,否则会导致内存溢出。
int strcmp (const char*str1, const char* str2);
str1 < str2:返回值小于0
str1 = str2:返回值等于0
str1 > str2:返回值大于0
4.字符串数组
1)二维数组
char sa[][128] ={"beijing", "tianjin", "shanghai","chongqing"};
printf ("%s\n",sa[0]);
// beijing
2)字符指针数组
数组首地址:数组首元素的地址,数组名。
指针数组:数组中的每个元素都是指针。
int a, b, c;
int* pa[] = {&a,&b, &c};
char* ps[] ={"hello", "world", "mw"};
第六课
编译预处理与大型程序
一、包含文件指令
编辑:hello.c,高级语言。
预处理(预编译):包含文件扩展、宏扩展等。
编译:汇编文件。
汇编:目标文件。
链接:可执行程序。
#include <stdio.h>
包含文件扩展:用被#include指令包含的文件内容替换该#include指令行。
#include<...>:从标准包含文件目录(/usr/include)找包含文件。常用于标准包含文件。
stdio.h
stdlib.h
string.h
stdbool.h
#include"...":首先找当前目录,没找到则取标准包含文件目录找,还没找到,根据-I选项指定的目录找。常用于自定义包含文件。
gcc -E可以查看预处理结果。
gcc -E xxx.c -o xxx.pc
二、宏定义指令
#define
1)常量宏
提高代码的可读性,可维护性。
#define 宏名 宏值
如:
#define PAI 3.14
预处理器将代码中出现的宏名全部替换为宏值。这个过程就叫做宏扩展或宏替换。
2)参数宏(宏函数)
#define 宏名(宏参数) 宏值
预处理器将宏参数替换为实际值后,代码中的宏名全部替换为宏值。
注意:小括号不要少。参数和被替换的宏值。
#define SQUARE(X) ((X)*(X))
3)语言扩展
4)#和##
#:表示将其后的宏参数作为字符串字面值进行替换。
##:表示将其后的宏参数替换以后与其前面的部分粘连在一起。
5)预定义宏
A.标准预定义宏
__FILE__:当前文件
__LINE__:当前行
__DATE__:编译日期
__TIME__:编译时间
__STDC__:是否支持标准C
B.编译器预定义宏
GNU编译器:__GNUC__
微软编译器:_MSC_VER
C.用户预定义宏
通过gcc的-D选项设置预定义宏。
如果宏是一个字符串的字面值,那么-D后面的宏值需要用\"引起来。
gcc -DVER=1.0
gcc -DCOPYRIGHT=\"小红\"
程序中的VER被宏替换为1.0,COPYRIGHT被宏替换为"小红"。
三、条件编译指令
#if // 如果,#if VER==1
#ifdef // 如果定义了...
#ifndef // 如果没有定义...
#elif // 否则如果...
#else // 否则
#endif // 和#if/#ifdef/#ifndef配对使用
#undef // 取消定义,和#define相反
满足条件(条件表达式的值非零)的代码参加编译,否则不参加编译。
四、头文件卫士
com.h
/ \
net.h des.h
/ \ / \
net.c app.c des.c
头文件卫士:在xxx.h中
#ifndef _XXX_H
#define _XXX_H
// 头文件代码...
#endif // _XXX_H
防止同一个.h文件被沿着不同路径包含到一个.c文件中产生重定义冲突。
五、构建脚本
makefile三要素:
目标: 依赖
<制表符>规则,从依赖获得目标的方法
练习:实现安全版本的strcpy_s和strcat_s。
char* strcpy_s (
char* dest, // 目标缓冲区
size_t size, // 目标缓冲区的大小(最多容纳的字符个数)
const char* src // 源字符串
);
char* strcat_s (
char* dest,
size_t size,
const char* src
);
练习:实现字符串拆分函数:
void split (
const char* psz, // 原始串
const char* sep // 分隔符
);
依次打印出原始串被分隔符分隔而成的各个子串。
split ("...192...168..1.2..",".");
192
168
1
2
split ("gwjxl@sina.com","@.");
gwjxl
sina
com
最好支持中文。
split ("2012年8月10日","年月日");
2012
8
10
#*.
###abc***xxx\0##*uiyt...
^
2
^
1
char* p = strchr ("hello", 'e');
p指向hello中的e字符
char* p = strchr ("hello", 'w');
p == NULL
复合类型与高级指针
一、结构体
1.在C语言中可以使用结构体定义用户自己的数据类型,类似于数组,但是结构体中的成员可以是不同的数据类型。
1)直接定义结构体形式的变量
struct {若干成员;} 变量名;
例如:
struct {
char name[128]; // 成员
intage; // 成员
float score; // 成员
} student; // 结构体变量
struct {
char name[128];
intage;
float score;
} student2;
2)先定义结构体类型,然后用该类型定义变量。
struct 结构体类型名 {
若干成员;
};
struct 结构体类型名 变量名;
例如:
struct Student {
char name[128];
intage;
float score;
}; // 定义了一个名为Student的数据类型,该类型是一个结构,包括name、age、score三个成员
struct Student student; // 定义了一个名为student的变量,该变量是structStudent类型的
struct Student student2;
struct Student student3;
3)先用typedef定义结构体类型别名,再用该别名定义变量
typedef 原始类型 类型别名;
typedef unsigned int UINT;
UINT a; // 等价于unsigned int a;
typedef int BOOL;
typedef struct Student {
char name[128];
intage;
float score;
} STU;
STU student;
STU student2;
STU student3;
4)嵌套结构:一个结构体类型中的某个成员也是结构体类型的。例如:
struct Student {
char name[128];
intage;
float score;
struct Date {
int year;
int month;
int day;
}birthday;
};
2.结构型变量及数组的初始化
通过{}对结构型变量进行初始化s。
3.访问结构体的成员
1)通过变量访问成员:用.运算符,亦称成员访问运算符。
2)通过指针访问成员:用->运算符,亦称间接成员访问运算符。
4.结构类型的变量可以作为函数的参数,但是与基本类型参数的情况一样,虚实结合的过程只是值的复制,因此在函数内部对形参所做的修改,不会影响实参。如果希望函数能够改变实参的值,应该传入变量的地址。
5.即使是以读方式访问形参的函数,仅仅出于性能的考虑,也可以以地址方式传参,避免结构复制所带来的开销。为了防止在函数中意外地修改实参,可以用常量指针定义参数。
6.内存对齐与补齐
1)对齐:结构中的每个成员在内存中的起始位置必须是其自身字节长度的整数倍,超过4字节的按4字节算。
2)补齐:结构体的字节长度必须是其字节数最多的成员的字节长度的整数倍,超过4字节的按4字节算。
7.位域
struct X {
inta : 1;
intb : 2;
intc : 5;
};
1)位域可以指定每个成员的大小(以位为单位),用以节省内存。
2)位域用法和结构一样,但是不能取成员的地址。
3)位域成员的类型必须是整型类(char/short/int/long/longlong及其unsigned版本)
二、共用体(联合)
1.联合与结构的定义语法一致,只是其关键为union而不是struct。
2.联合中的成员在内存是共享的。
struct A {
inti;
char c[4];
double d;
};
IIIICCCCDDDDDDDD
----....~~~~~~~~
i c[4] d
union B {
inti;
char c[4];
double d;
};
3.联合的字节长度就是其字节数最大的成员的字节长度。
三、枚举
1.枚举是一个整型常量的列表,一般而言,其值为有限个,每个值都是枚举常量(整型)。枚举默认从0开始,但是也可以人为指定,没有显式指定的枚举常量,取上一个值加1。
2.C语言中的枚举类型其本质就是一个整型,可以接受任何针对整型变量的赋值,不做与枚举值域有关的检查。
3.增加代码的可读性,避免重复定义常量。
四、指针数组与数组指针
严格区分三个容易混淆的概念:
数组元素的指针
int a[10] = {...};
int* p = a;
printf ("%d", *p);
*p = 100;
p++;
指针组成的数组
int a, b, c;
int* arr[] = {&a, &b, &c};
*arr[0] = 100; // 100 -> a
printf ("%d", *arr[1]); // b
指向数组的指针
int a[10] = {...};
int (*p)[10] = &a;
五、二维数组与指针
int a[2][3] = {1,2,3,4,5,6};
a[0] a[0]+1 a[0]+2
| | |
v v v
a->1 2 3
a+1->4 5 6
a的类型:int (*)[3]
a[0]的类型:int*
对于以上二维数组以下表达式的值是相等的:
a+i
*(a+i)
a[i]
&a[i]
&a[i][0]
-------------
x[y] = *(x+y)
a[i][j]
= *(a[i]+j)
= *(*(a+i)+j)
六、多级指针
二级指针:指向一级指针的指针,其中存放一个一级指针的地址。
int a, b, c;
int* pa[] = {&a, &b, &c};
char* pc[] = {"hello","world", "tarena"};
int** ppa = pa;
七、空指针(void*)
1.代表任意类型的指针。
2.一切类型的指针都可以隐式转换为空指针。
3.对空指针不能解引用。
4.空指针支持计算,但是统一按单字节处理。
八、动态内存分配与释放
#include <stdlib.h>
void* malloc (
size_t size // 预分配字节数
);
成功返回所分配内存的起始地址,失败返回NULL。不初始化。
void* calloc (
size_t nmemb, // 元素数
size_t size // 元素字节数
);
成功返回所分配内存的起始地址,失败返回NULL。初始化为0。
所有通过动态内存分配所得到内存都要通过free()函数释放。
void free (
void* ptr // 内存地址
);
void* realloc (
void* ptr, // 原地址
size_t size // 新字节数
);
成功返回调整后的内存地址,失败返回NULL。新增部分不做初始化。
如果ptr为NULL,则与malloc()等效。
如果size为0,则与free()等效。
可能会分配新的内存空间,如果发生这种情况,原内存会自动释放,其内容会拷贝到新内存中,但是如果分配失败,原内存不会释放。
char* p = (char*)malloc (10);
char* p2 = realloc (p, 20);
if (p2)
p =p2;
else {
free (p);
return -1;
}
举例:录入多行文本,每行不超过255个字符,行数不限,输入!表示输入结束,将之前输入的所有内容拼成一个字符串输出。
输入> 小红
输入> 有理
输入> 由
输入> !
输出> 小红有理由
九、函数指针及其解引用
1.用指针存储代码区中的函数地址。所有的函数都有地址,而函数名实际上就是该函数的符号地址。
2.定义语法
函数:返回类型函数名 (形参表) {函数体;}
函数指针:
返回类型 (*函数指针名) (形参表);
例如:
int foo (int a, double b){...}
int bar (int a, double b)
{...}
int (*pfoo) (int, double) = foo;
pfoo = bar;
3.解引用语法
函数指针名 (实参表);
pfoo (10, 3.14); // 调用bar
----------------
pfoo = &bar;
(*pfoo) (10, 3.14);
第七课
IO流与标准库
一、IO流的打开和关闭
FILE* fopen (
const char* filename, //文件路径
const char* mode // 打开模式
);
打开模式:
r: 读
w: 写
a: 追加
r+: 读写,文件必须存在,从头开始
w+: 写读,文件不存在就创建,文件存在就清空
a+: 读写,文件不存在就创建,文件存在就追加
b: 二进制方式
UNIX/Linux:二进制方式作用不大
Windows:二进制方式,对读写内容不做任何转换。非二进制方式,写入\n,实际写入\r\n,文件中\r\n,读到的\n。
返回值是IO流结构体指针,用户调用其他IO流函数。
int fclose (
FILE* fp // fopen的返回值
);
成功返回0,否则返回EOF。
进程一启动,以下标准IO流是默认打开的:
stdout: 标准输出
stdin: 标准输入
stderr: 标准出错
二、格式化IO
int fprintf (
FILE* stream, // IO流指针
const char* format,// 格式字符串
...// 输出数据项
);
printf(...)实际就是fprintf (stdout,...)
格式字符串:
%[标志][宽度][.精度][h|l|ll]类型符
标志:
- 左对齐
+ 输出正负号
# 输出进制标识
0 用0填充
宽度.精度:123.34,宽度6,精度2,%6.2f
h short
l long/double
ll long long
类型符:
d 十进制整数
c 字符
s 空字符结尾的字符串
f 浮点数
x/X 十六进制整数
o 八进制整数
p 地址(十六进制)
u 无符号十进制整数
g 浮点数(自动选择最短形式)
e 浮点数(科学计数法)
int fscanf (
FILE* stream, // IO流指针
const char* format, // 格式字符串
...// 地址表
);
1)以空白字符(空格、制表、换行)作为数据分隔符。
2)模式匹配:根据格式化字符串的模式进行匹配,从中提取数据。
3)禁止赋值:*
4)字符集表示:只读取某个特定字符集中的字符,只要遇到字符集以外的字符即刻返回。
三、非格式化IO
int fputc (
intc, // 字符
FILE* stream // IO流指针
);
成功返回c,失败返回EOF。
int fgetc (
FILE* stream // IO流指针
);
成功返回读到的字符,失败或者遇到文件尾返回EOF。
四、二进制IO
size_t fread (
void* buf, // 缓冲区指针
size_t size, // 期望读取的数据单元字节数
size_t count, // 期望读取的数据单元数
FILE* stream // IO流指针
);
成功返回实际读取的数组单元数。失败或遇到文件尾返回0。
size_t fwrite (
void* buf, // 缓冲区指针
size_t size, // 期望写入的数据单元字节数
size_t count, // 期望写入的数据单元数
FILE*stream // IO流指针
);
成功返回实际写入的数据单元数,失败返回0。