C语言入门

c语言入门学习

C语言基础

C语言特点

作用

  1. 所有大学理工科必修课
  2. 名企,外企,高薪程序员面试必考
  3. 写外挂,做黑客必须掌握的语言
  4. 学习[[数据结构]]和C++的基石
  5. 绝对重要的基石语言,Unix,Linux,Windows,JAVA,C++,C#底层实现都靠C

image-20200909140723886

1)优点

  1. 代码量效
  2. 执行速度快
  3. 功能强大
  4. 编程自由

2)缺点

  1. 写代码实现周期长
  2. 可移植性较差
  3. 过于自由,经验不足易出错
  4. 对于平台依赖较多

中央处理器(CPU)的主要模块

  1. 运算器
  2. 控制器
  3. 寄存器

内存—-(内)存储器

外部设备

  1. 输入设备
  2. 输出设备
  3. 外存—-(外)存储器

C语言关键字

C语言仅有32个关键字,9种控制语句,34种运算符

32个关键字

1
auto,break,case,char,const,continue,default,do,double,else,enum,extern,float,for,goto,if,int,long,register,return,short,signed,sizeof,static,struct,switch,typedef,unsigned,union,void,volatile,while

9种控制语句

1
2
3
4
5
6
7
8
9
if()else{}
for(){}
while(){}
do{}while()
continue
break
switch
goto
return

34种运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
算术运算符:  +  -  *  /  %  ++  --  
关系运算符: < <= == > >= !=
逻辑运算符: ! && ||
位运算符: << >> ~ | ^ &
赋值运算符: =及其拓展
条件运算符: ?=
逗号运算符: ,
指针运算符: * &
求字节数: sizeof
强制类型转换: (类型)
分量运算符: . ->
下标运算符: []
其他: ()

算数运算符:先 * / % 后 + -

运算符优先级:

[]() > ++ -- (后缀高于前缀) (强转) sizeof > 算数运算(先乘除取余,后加减)> 

比较运算 > 逻辑运算 > 三目运算(条件运算)> 赋值运算 > 逗号运算

解决控制台一闪而过:

  1. 在return 0之前添加system(“pause”)
  2. 在项目上–》右键–》属性–》配置属性–》链接器–》系统–》子系统–》在下拉框中选择“控制台”(/SUBSYSTEM:CONSOLE)

导入头文件()

使用C语言库函数需要提前包含库函数对应的头文件

1
2
3
4
5
#:关键标识符,表示引入头文件
include:引入头文件关键字
stdio.h:系统标准输入输出库对应的头文件
<>:表示系统直接按系统指定的目录检索
"":表示系统先在 "" 指定的路径(没写路径代表当前路径)查找头文件,如果找不到,再按系统指定的目录检索

gcc编译器

我们用编辑器编写程序,由编译器编译后才可以运行!

gcc(GNU Compiler Collection,GNU 编译器套件),是由 GNU 开发的编程语言编译器。gcc原本作为GNU操作系统的官方编译器,现已被大多数类Unix操作系统(如Linux、BSD、Mac OS X等)采纳为标准的编译器,gcc同样适用于微软的Windows。

gcc最初用于编译C语言,随着项目的发展gcc已经成为了能够编译C、C++、Java、Ada、fortran、Object C、Object C++、Go语言的编译器大家族。

格式:

1
2
gcc [-option1] ... <filename>
g++ [-option1] ... <filename>
  • 命令、选项和源文件之间使用空格分隔
  • 一行命令中可以有零个、一个或多个选项
  • 文件名可以包含文件的绝对路径,也可以使用相对路径
  • 如果命令中不包含输出可执行文件的文件名,可执行文件的文件名会自动生成一个默认名,Linux平台为a.out,Windows平台为a.exe

gcc、g++编译常用选项

1
2
3
4
-o file	指定生成的输出文件名为file
-E 只进行预处理
-S(大写) 只进行预处理和编译
-c(小写) 只进行预处理、编译和汇编

Windows平台下gcc环境配置

windows命令行界面下,默认是没有gcc编译器,我们需要配置一下环境。由于我们安装了[[Qt]],[[Qt]]是一个集成开发环境,内部集成gcc编译器,配置一下环境变量即可使用gcc。

C语言编译过程

C代码编译成可执行程序经过4步:

  1. 预处理:宏定义展开、头文件展开、条件编译等,同时将代码中的注释删除,这里并不会检查语法
  2. 编译:检查语法,将预处理后文件编译生成汇编文件
  3. 汇编:将汇编文件生成目标文件(二进制文件)
  4. 链接:C语言写的程序是需要依赖各种库的,所以编译之后还需要把库链接到最终的可执行程序中去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gcc编译4步骤:【重点】
1. 预处理 -E xxx.i 预处理文件
gcc -E xxx.c -o xxx.i
1) 头文件展开。 --- 不检查语法错误。 可以展开任意文件。
2)宏定义替换。 --- 将宏名替换为宏值。
3)替换注释。 --- 变成空行
4)展开条件编译 --- 根据条件来展开指令。
...
2. 编译 -S xxx.s 汇编文件
gcc -S hello.i -o hello.s
1)逐行检查语法错误。【重点】 --- 整个编译4步骤中最耗时的过程。
2)将C程序翻译成 汇编指令,得到.s 汇编文件。
3. 汇编 -c xxx.o 目标文件
gcc -c hello.s -o hello.o
1)翻译:将汇编指令翻译成对应的 二进制编码。
4. 链接 无 xxx.exe 可执行文件。
gcc hello.o -o hello.exe
1)数据段合并
2)数据地址回填
3)库引入

img

1
2
3
4
预处理:gcc -E hello.c -o hello.i
编 译:gcc -S hello.i -o hello.s
汇 编:gcc -c hello.s -o hello.o
链 接:gcc hello.o -o hello
选项 含义
-E 只进行预处理
-S(大写) 只进行预处理和编译
-c(小写) 只进行预处理、编译和汇编
-o file 指定生成的输出文件名为 file
文件后缀 含义
.c C 语言文件
.i 预处理后的 C 语言文件
.s 编译后的汇编文件
.o 编译后的目标文件

记忆方式:iso ESC

预处理

文件包含处理

img

#incude<>和#include***””***区别

  • “” 表示系统先在file1.c所在的当前目录找file1.h,如果找不到,再按系统指定的目录检索。
  • < > 表示系统直接按系统指定的目录检索。

宏定义

无参数的宏定义(宏常量)
1
#define num 100

说明

  1. 宏名一般用大写,以便于与变量区别;

  2. 宏定义可以是常数、表达式等;

  3. 宏定义不作语法检查,只有在编译被宏展开后的源程序才会报错;

  4. 宏定义不在行末加分号;

  5. 宏名有效范围为从定义到本源文件结束;

  6. 可以用#undef命令终止宏定义的作用域;

  7. 在宏定义中,可以引用已定义的宏名;

带参数的宏定义(宏函数)
1
2
//仅仅只是做文本替换,由于宏函数没有普通函数参数压栈、跳转、返回等的开销,可以调高程序的效率。
#define SUM(x,y) (( x )+( y ))

注意:

  1. 宏的名字中不能有空格,但是在替换的字符串中可以有空格。ANSI C允许在参数列表中使用空格;
  2. 用括号括住每一个参数,并括住宏的整体定义。
  3. 用大写字母表示宏的函数名。
  4. 如果打算宏代替函数来加快程序运行速度。假如在程序中只使用一次宏对程序的运行时间没有太大提高。

在C和C++中,# 预处理操作符是一种用于将参数转换为字符串的特殊操作符。当在预处理阶段使用 # 操作符时,它会把后面的参数转换为一个字符串。这个功能通常用于宏定义中,允许在代码中以字符串形式表示参数的名称。

1
#define PRINT_VAR(x) printf("Variable name: %s\n", #x)

条件编译

img

1
2
3
4
5
6
7
8
9
//防止头文件被重复包含引用
#ifndef _SOMEFILE_H
#define _SOMEFILE_H

//需要声明的变量、函数
//宏定义
//结构体

#endif

一些特殊的预定宏

C编译器,提供了几个特殊形式的预定义宏,在实际编程中可以直接使用,很方便。

1
2
3
4
5
6
7
8
9
10
11
12
//	__FILE__			宏所在文件的源文件名 
// __LINE__ 宏所在行的行号
// __DATE__ 代码编译的日期
// __TIME__ 代码编译的时间

void test()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
}

system函数

  • 功能:在已经运行的程序中执行另外一个外部程序
  • 参数:外部可执行程序名字
  • 返回值:成功:0 失败:任意数字
  • 参数如:pause,cmd,calc,mspaint,notepad,cls。。。。。。

注释

  • 单行注释://(注意是斜杠,\叫反斜杠,要区分开)
  • 多行注释:/* */

CPU 内部结构与寄存器

CPU对外是通过总线(地址,控制,数据)来和外部设备交互的,总线的宽度是8位,同时cpu的寄存器也是8位的,那么这个cpu就叫8位cpu

存储器按与CPU远近来分:

寄存器 — 缓存 —(CPU缓存) — 内存 — 外存

为什么要有缓存呢?

因为如果经常操作内存中的同一个地址的数据,就会影响速度,因此在内存和寄存器中设置一个缓存

IDE用法(VS+QT)

IDE 集成开发环境,[[QT]]就是一个轻量级的集成开发环境,vs也是IDE

QT常用快捷键

快捷键 含义
Ctrl + i 自动格式化代码
Ctrl + / 注释/取消注释
Alt + Enter 自动完成类函数定义
F4 .h 文件和对应.cpp 文件切换
F9 设置断点
F5 调试运行
Ctrl + r 编译,但不调试运行
Ctrl + b 编译,不运行
F10 next调试(单步步过)
F11 step调试(单步步入)

VS的快捷键

快捷键 含义
Alt+Shift 多光标选择
Ctrl + k,Ctrl + f 自动格式化代码
Ctrl + k,Ctrl + c 注释代码
Ctrl + k,Ctrl + u 取消注释代码
F9 设置断点
F5 调试运行
Ctrl + F5 不调试运行
Ctrl + Shift + b 编译,不运行
F10 next调试
F11 step调试

VS的代码快捷导入

新建文件后缀为xxx.snippet

VS –》 工具–》 代码片段管理器 –》 Visual C++

VS2013的C4996错误

由于微软在VS2013中不建议再使用C的传统库函数scanf,strcpy,sprintf等,所以直接使用这些库函数会提示C4996错误:

VS建议采用带_s的函数,如scanf_s、strcpy_s,但这些并不是标准C函数。

要想继续使用scanf,strcpy,sprintf等,需要在源文件中添加以下指令就可以避免这个错误提示:

1
2
3
#define _CRT_SECURE_NO_WARNINGS  //这个宏定义最好要放到.c文件的第一行

#pragma warning(disable:4996) //或者使用这个

如果是使用vs,更好的解决方式是在项目属性页-配置属性-C/C++-预处理器中添加_CRT_SECURE_NO_WARNINGS

vs注意点:另一个文件中的函数,未声明直接用会默认当成**int 函数名()**这样的函数处理

数据类型

img

既能读又能写的内存对象,称为变量

若一旦初始化后不能修改的对象则称为常量

变量特点:

  1. 变量在编译时为其分配相应的内存空间
  2. 可以通过其名字和地址访问相应内存

声明和定义区别:

  1. 不需要建立存储空间,如:extern;
  2. 定义变量需要建立存储空间,如:int b;
  • int b 它既是声明,同时又是定义
  • 对于 extern int a来讲它只是声明不是定义

一般的情况下,把建立存储空间的声明称之为“定义”,而把不需要建立存储空间的声明称之为“声明”。

变量标识符命名规则:

  • 标识符不能是关键字
  • 标识符只能由字母、数字、下划线组成
  • 第一个字符必须为字母或下划线
  • 标识符中字母区分大小写

整形:int

整型变量的定义和输出

*打印格式* *含义*
%d 输出一个有符号的10进制int类型
%o(字母o) 输出8进制的int类型
%x 输出16进制的int类型,字母以小写输出
%X 输出16进制的int类型,字母以大写输出
%u 输出一个10进制的无符号数

short、int、long、long long

*数据类型* *占用空间*
short(短整型) 2字节
int(整型) 4字节
long(长整形) Windows为4字节,Linux为4字节(32位),8字节(64位)
long long(长长整形) 8字节

注意点:

需要注意的是,整型数据在内存中占的字节数与所选择的操作系统有关。虽然 C 语言标准中没有明确规定整型数据的长度,但 long 类型整数的长度不能短于 int 类型, short 类型整数的长度不能长于 int 类型。

l当一个小的数据类型赋值给一个大的数据类型,不会出错,因为编译器会自动转化。但当一个大的类型赋值给一个小的数据类型,那么就可能丢失高位。

整型常量 所需类型
10 代表int类型
10l, 10L 代表long类型
10ll, 10LL 代表long long类型
10u, 10U 代表unsigned int类型
10ul, 10UL 代表unsigned long类型
10ull, 10ULL 代表unsigned long long类型
打印格式 含义
%hd 输出short类型
%d 输出int类型
%ld 输出long类型
%lld 输出long long类型
%hu 输出unsigned short类型
%u 输出unsigned int类型
%lu 输出unsigned long类型
%llu 输出unsigned long long类型

有符号数和无符号数区别

有无符号位

数据类型 占用空间 取值范围
short 2字节 -32768 到 32767 (-215 ~ 215-1)
int 4字节 -2147483648 到 2147483647 (-231 ~ 231-1)
long 4字节 -2147483648 到 2147483647 (-231 ~ 231-1)
unsigned short 2字节 0 到 65535 (0 ~ 216-1)
unsigned int 4字节 0 到 4294967295 (0 ~ 232-1)
unsigned long 4字节 0 到 4294967295 (0 ~ 232-1)

比如说内存中的32个1二进制存储的整形读取可能是-1,也可能是2147483647 ,看你以什么格式读取

有符号数和无符号的数值溢出的区别

符号位溢出会导致数的正负发生改变,但无符号最高位的溢出会导致最高位丢失。

注意了,补码的补码还是其本身

类型转换

隐式类型转换:

由编译器自动完成。

由赋值产生的类型转换。 小–》大 没问题。 大 –》 小 有可能发生数据丢失。

强制类型转换:

语法: (目标类型)带转换变量

​ (目标类型)带转换表达式

大多数用于函数调用期间,实参给形参传值。

sizeof关键字

  • sizeof不是函数,而是操作符,所以不需要包含任何头文件,它的功能是计算一个数据类型的大小,单位为字节
  • sizeof的返回值为size_t
  • size_t类型在32位操作系统下是unsigned int,是一个无符号的整数

sizeof后跟变量名或类型名的区别:

  • 如果后跟的是变量名的话,sizeof d或sizeof(d)都成立
  • 但后跟的是类型名的话,sizeof必须加括号,即sizeof(double)

字符型:char

字符型变量用于存储一个单一字符,在 C 语言中用 char 表示,其中每个字符变量都会占用 1 个字节。在给字符型变量赋值时,需要用一对英文半角格式的单引号(‘ ‘)把字符括起来。

字符变量实际上并不是把该字符本身放到变量的内存单元中去,而是将该字符对应的 ASCII 编码放到变量的存储单元中。char的本质就是一个1字节大小的整型。

ASCII对照表

ASCII值 控制字符 ASCII值 字符 ASCII值 字符 ASCII值 字符
0 NUT 32 (space) 64 @ 96
1 SOH 33 ! 65 A 97 a
2 STX 34 66 B 98 b
3 ETX 35 # 67 C 99 c
4 EOT 36 $ 68 D 100 d
5 ENQ 37 % 69 E 101 e
6 ACK 38 & 70 F 102 f
7 BEL 39 , 71 G 103 g
8 BS 40 ( 72 H 104 h
9 HT 41 ) 73 I 105 i
10 LF 42 * 74 J 106 j
11 VT 43 + 75 K 107 k
12 FF 44 , 76 L 108 l
13 CR 45 - 77 M 109 m
14 SO 46 . 78 N 110 n
15 SI 47 / 79 O 111 o
16 DLE 48 0 80 P 112 p
17 DCI 49 1 81 Q 113 q
18 DC2 50 2 82 R 114 r
19 DC3 51 3 83 S 115 s
20 DC4 52 4 84 T 116 t
21 NAK 53 5 85 U 117 u
22 SYN 54 6 86 V 118 v
23 TB 55 7 87 W 119 w
24 CAN 56 8 88 X 120 x
25 EM 57 9 89 Y 121 y
26 SUB 58 : 90 Z 122 z
27 ESC 59 ; 91 [ 123 {
28 FS 60 < 92 / 124 |
29 GS 61 = 93 ] 125 }
30 RS 62 > 94 ^ 126 `
31 US 63 ? 95 _ 127 DEL

ASCII 码大致由以下两部分组成:

  1. ASCII 非打印控制字符: ASCII 表上的数字 0-31 分配给了控制字符,用于控制像打印机等一些外围设备。
  2. ASCII 打印字符:数字 32-126 分配给了能在键盘上找到的字符,当查看或打印文档时就会出现。数字 127 代表 Del 命令。
转义字符 含义 ASCII码值(十进制)
\a 警报 007
\b 退格(BS) ,将当前位置移到前一列 008
\f 换页(FF),将当前位置移到下页开头 012
\n 换行(LF) ,将当前位置移到下一行开头 010
\r 回车(CR) ,将当前位置移到本行开头 013
\t 水平制表(HT) (跳到下一个TAB位置) 009
\v 垂直制表(VT) 011
\ 代表一个反斜线字符”" 092
' 代表一个单引号(撇号)字符 039
" 代表一个双引号字符 034
? 代表一个问号 063
\0 数字0 000
\ddd 8进制转义字符,d范围0~7 3位8进制
\xhh 16进制转义字符,h范围09,af,A~F 3位16进制

实型(浮点型):float、double

单精度浮点数(float)、 双精度浮点数(double), 但是double型变量所表示的浮点数比 float 型变量更精确。

由于浮点型变量是由有限的存储单元组成的,因此只能提供有限的有效数字。在有效位以外的数字将被舍去,这样可能会产生一些误差。因此做浮点数的判断是否相等时候最好用两浮点数之差和某个很小的精度小数坐标,具体看你要什么精度,比如0.000001

不以f结尾的常量是double类型,以f结尾的常量(如3.14f)是float类型。

long double是C++中的长浮点数数据类型,表示具有更高精度的双精度浮点数。在C++中,long double通常提供比double类型更高的精度,通常占用更多的存储空间。具体的精度和存储空间取决于编译器和计算机架构。

进制

当前的计算机系统使用的基本上是二进制系统,数据在计算机中主要是以补码的形式存储的。

2进制与十进制转换图

imgimg

C语言如何表示相应进制数

十进制 以正常数字1-9开头,如123
八进制 以数字0开头,如0123
十六进制 以0x开头,如0x123
二进制 C语言不能直接书写二进制数

在计算机系统中,数值一律用补码来存储

类型限定符

限定符 含义
extern 声明一个变量,extern声明的变量没有建立存储空间。extern int a;//变量在定义的时候创建存储空间
const 定义一个常量,常量的值不能修改。const int a = 10;
Volatile 防止编译器优化代码
register 定义寄存器变量,提高效率。register是建议型的指令,而不是命令型的指令,如果CPU有空闲寄存器,那么register就生效,如果没有空闲寄存器,那么register无效。

Volatile详解

volatile的本意是“易变的”,volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

volatile表示不想被意想不到的改变,const表示程序不应该试图去修改它

1
2
3
4
volatile int i=10;
int a = i;
//其他代码,并未明确告诉编译器,对i进行过操作
int b = i;

volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。

注意,在vc6中,一般调试模式没有进行代码优化,所以这个关键字的作用看不出来。下面通过插入汇编代码,测试有无volatile关键字,对程序最终代码的影响。首先用classwizard建一个win32 console工程,插入一个voltest.cpp文件,输入下面的代码:

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
#include <stdio.h>
void main()
{
int i=10;
int a = i;
printf("i= %d\n",a);
//下面汇编语句的作用就是改变内存中i的值,但是又不让编译器知道
__asm {
mov dword ptr [ebp-4], 20h
}
int b = i;
printf("i= %d\n",b);
}
/*然后,在调试版本模式运行程序,输出结果如下:
i = 10
i = 32
然后,在release版本模式运行程序,输出结果如下:
i = 10
i = 10
输出的结果明显表明,release模式下,编译器对代码进行了优化,第二次没有输出正确的i值。下面,我们把 i的声明加上volatile关键字,看看有什么变化:*/
#include <stdio.h>
void main()
{
volatile int i=10;
int a = i;
printf("i= %d\n",a);
__asm {
mov dword ptr [ebp-4], 20h//这个在VS2015中debug版本不一定设置的ebp-4。
}
int b = i;
printf("i= %d\n",b);
}
/*分别在调试版本和release版本运行程序,输出都是:
i = 10
i = 32
这说明这个关键字发挥了它的作用!*/

下面是volatile变量的几个例子:

  1. 并行设备的硬件寄存器(如:状态寄存器)
  2. 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
  3. 多线程应用中被几个任务共享的变量

这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。

字符串常量

img

每个字符串的结尾,编译器会自动的添加一个结束标志位’\0’,即 “a” 包含两个字符’a’和’\0’。

printf函数和putchar函数

printf是输出一个字符串,putchar输出一个char。、

printf格式字符:

*打印格式* *对应数据类型* *含义*
%d int 接受整数值并将它表示为有符号的十进制整数
%hd short int 短整数
%hu unsigned short 无符号短整数
%o unsigned int 无符号8进制整数
%u unsigned int 无符号10进制整数
%x,%X unsigned int 无符号16进制整数,x对应的是abcdef,X对应的是ABCDEF
%f float 单精度浮点数
%lf double 双精度浮点数
%e,%E double 科学计数法表示的数,此处”e”的大小写代表在输出时用的”e”的大小写
%c char 字符型。可以把输入的数字按照ASCII码相应转换为对应的字符
%s char * 字符串。输出字符串中的字符直至字符串中的空字符(字符串以’\0‘结尾,这个’\0’即空字符)
%p void * 以16进制形式输出指针
%% % 输出一个百分号

printf附加格式:

*字符* *含义*
l(字母l) 附加在d,u,x,o前面,表示长整数
- 左对齐
m(代表一个整数) 数据最小宽度
0(数字0) 将输出的前面补上0直到占满指定列宽为止不可以搭配使用-
m.n(代表一个整数) m指域宽,即对应的输出项在输出设备上所占的字符数。n指精度,用于说明输出的实型数的小数位数。对数值型的来说,未指定n时,隐含的精度为n=6位。

image-20200922183129000

scanf函数与getchar函数

  • getchar是从标准输入设备读取一个char。
  • scanf通过%转义的方式可以得到用户通过标准输入设备输入的数据。
  • 借助“正则表达式”, scanf获取带有空格的字符串:scanf(“%[^\n]”, str);

scanf格式字符:

格式 作用
%*s或%*d 跳过数据(遇到空格或\t代表结束忽略)
%[width]s 读指定宽度的数据,此处[]括号并不需要真写
%[a-z] 匹配a到z中任意字符(尽可能多的匹配)(只要有一个字符匹配失败,就不继续匹配了)
%[aBc] 匹配a、B、c中一员,贪婪性
%[^a] 匹配非a的任意字符,贪婪性
%[^a-z] 表示读取除a-z以外的所有字符

p.s.上面所有都是可以加星表示跳过的,并且都可以指定读指定宽度的数据

字符串的输入输出

fgets:从stdin获取一个字符串, 预留 \0 的存储空间。空间足够读 \n, 空间不足舍弃 \n 【安全】

用户输入

函数 终止条件 用户输入时候是否包含结尾的“\n”
scanf 遇到空格或换行符
gets 遇到换行符
fgets 遇到换行符或读到size-1个字符

输出显示

函数 输出内容后是否多输出一个“\n” 输出内容结尾的‘0’是否输出
printf
puts
fputs

由于fgets安全,因此正经编写代码的时候用fgets

strlen()

计算指定指定字符串s的长度,不包含字符串结束符‘\0’

函数的声明

如果使用用户自己定义的函数,而该函数与调用它的函数(即主调函数)不在同一文件中,或者函数定义的位置在主调函数之后,则必须在调用此函数之前对被调用的函数作声明。

所谓函数声明,就是在函数尚在未定义的情况下,事先将该函数的有关信息通知编译系统,相当于告诉编译器,函数在后面定义,以便使编译能正常进行。

注意:一个函数只能被定义一次,但可以声明多次。

p.s.在一个文件中(比如a.c)定义一个全局变量int a = 10;然后在另一个代码文件(比如main.c)中需要使用变量a,可以写int a;单独看main.c文件时就会出现二义性,一个含义是当其他文件中没有定义过全局变量a,则这里定义一个变量a。另一个含义是当其他文件中包含声明全局变量a,则这里声明一个变量a。所以当a.c中定义了全局变量a时,在main.c中最好使用:extern int a;

main函数与exit函数

在main函数中调用exit和return结果是一样的,但在子函数中调用return只是代表子函数终止了,在子函数中调用exit,那么程序终止。

随机数:

  1. 播种随机数种子: srand(time(NULL));
  2. 引入头文件 #include <stdlib.h> <time.h>
  3. 生成随机数: rand() % 100;

exit函数:

#include <stdlib.h>
return关键字:

    返回当前函数调用,将返回值返回给调用者。

exit()函数:

    退出当前程序。

头文件的意义(重点)

1
2
3
4
5
6
7
8
9
10
11
//文件名 First.c
main()
{
printStr();
}


printStr()
{
printf(“Hello world!”);
}

如上图,编译报错,这里涉及到一个顶层作用域的问题:

顶层作用域就是从声明点延伸到源程序文本结束

因此在没有声明的前提下,main必须放在最后面。然后如果遇到两函数互相嵌套的情况,就会发生,谁放前面都没用的情况,因此在一切都还没开始之前进行顶层声明就是最通用的解决方式了。我们将这些顶层声明拿出来单独管理,组织成一个所谓的头文件

因此test1.h头文件的目的是为了使test2可以通过test1.h快速建立对test2的函数声明,以此使用test2的函数


声明和定义的表示区别:

变量的声明和定义:

1
2
3
4
int a;//需要建立存储空间      既是定义,也是声明
extern int a;//不需要建立存储空间 只是声明,不是定义
//如果声明有初始化,就被当作定义,即使前面有extern,例如:
extern int a = 5; //定义

函数的声明和定义:

带有{ }的都是定义,否则就是声明。


C语言的存储类说明符有以下几个:

说明符 用 法
Auto 只在块内变量声明中被允许, 表示变量具有本地生存期.
Extern 出现在顶层或块的外部变量函数与变量声明中,表示声明的对象具有静态生存期, 连接程序知道其名字.
Static 可以放在函数与变量声明中,在函数定义时,只用于指定函数名,而不将函数导出到链接程序,在函数声明中,表示其后边会有定义声明的函数,存储类型static.在数据声明中,总是表示定义的声明不导出到连接程序.
  1. 说明符auto表明一个变量具有自动存储时期.该说明符只能用在具有代码块作用域的变量声明中,而这样的变量已经拥有自动存储时期,因此它主要是用来明确指明意图,使程序更易读.
  2. 说明符register也只能用于具有代码块作用域的变量.它将一根变量归入寄存器存储类,这相当于请求将该变量存储在一个寄存器内,以更快的存取.它的使用也使你不能获得该变量的地址.
  3. 说明符static在用于具有代码块作用域的变量的声明时,使该变量具有静态存储时期,从而得以在程序运行期间(即使在包含该变量的代码块并没有运行时)存在并保留其值.变量仍具有代码块作用域和空链接.static用于具有文件作用域的变量的声明时,表明该变量具有内部链接.(静态函数只能在声明他的文件中可见,其他文件不能引用该函数,不同的文件可以使用相同名字的静态函数,互不影响)
  4. 说明符extern表明你在声明一个已经在别处定义了的变量.如果包含extern的声明具有代码块作用域,所指向的变量可能具有外部链接也可能具有内部链接,这取决于该变量的定义声明

由上面可知,extern修饰的变量可以供其他文件使用。

但为什么头文件中的函数大多不需要extern,因为C语言中有默认的存储类标志符. C99中规定, 所有顶层的默认存储类标志符都是extern

但为了区分是否别的文件定义,因此有了人为规范:在.h文件中声明的函数,如果在其对应的.c文件中有定义,那么我们在声明这个函数时,不使用extern修饰符, 如果反之,则必须显示使用extern修饰符.

注意:并不能用数组来声明指针,反之亦然。(很明显呀。。。)

注意:在同一个工程的不同的需要包含a.h的文件当中,你只能定义AAA一次,否则在连接这些目标文件时会出现
重复定义的错误,即使你的单独目标文件编译没有任何的问题.因此:在头文件中使用宏定义实现对整个头文件的防止重复包含就很重要(防止重复包含的目的是为了解决整个项目中同一个东西定义超过一次报错的问题)


最后给出一点点大型编程时候全局变量使用需要注意的问题,这也仅仅是个建议,或者说一种编程习惯

  1. 所有全局变量全部以g_开头,并且尽可能声明成static类型.
  2. 尽量杜绝跨文件访问全局变量.如果的确需要在多个文件内访问同一变量,应该由该变量定义所在文件内提供GET/PUT函数实现.
  3. 全局变量必须要有一个初始值,全局变量尽量放在一个专门的函数内初始化.
  4. 如调用的函数少于三个,请考虑改为局部变量实现.

多文件(分文件)编程

  • 把函数声明放在头文件xxx.h中,在主函数中包含相应头文件
  • 在头文件对应的xxx.c中实现xxx.h声明的函数

防止头文件重复包含

头文件重复包含错误

当一个项目比较大时,往往都是分文件,这时候有可能不小心把同一个头文件 include 多次,或者头文件嵌套包含。

为了避免同一个文件被include多次,C/C++中有两种方式,一种是 #ifndef 方式

1
2
3
4
5
6
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__

// 声明语句

#endif

一种是 #pragma once 方式。

1
2
3
#pragma once

// 声明语句

头文件放

  1. #include头文件
  2. 函数声明
  3. 类型定义
  4. 宏定义

源文件中只要加一个#include自己同名头文件。

对于函数来说,默认为extern。

不需要额外在声明时加extern,加不加是等价的。

内存

  • 存储器:计算机的组成中,用来存储程序和数据,辅助CPU进行运算处理的重要部分。
  • 内存:内部存贮器,暂存程序/数据——掉电丢失 SRAM、DRAM、DDR、DDR2、DDR3。
  • 外存:外部存储器,长时间保存程序/数据—掉电不丢ROM、ERRROM、FLASH(NAND、NOR)、硬盘、光盘。

只读存储器(ROM)

里面的内容是厂家生产时预先录制(烧制/烧写)好的信息,如BIOS信息,断电后数据不消失,可多次读取。

随机存储器(RAM)

断电后数据消失

有关内存的两个概念:物理存储器和存储地址空间

物理存储器:实际存在的具体存储器芯片。

  • 主板上装插的内存条
  • 显示卡上的显示RAM芯片
  • 各种适配卡上的RAM芯片和ROM芯片

存储地址空间:对存储器编码的范围。我们在软件上常说的内存是指这一层含义。

  • 编码:对每个物理存储单元(一个字节)分配一个号码
  • 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写

指针注意点:

指针定义注意:

1
2
char* p,p2;//这里p才是char指针类型,p2只是char类型
char* p,*p2;//这样才是两者均为指针

一级指针传参:

1
2
3
4
5
6
7
8
9
10
11
12
13
int c = 3;
void hello(int* i)
{
i = &c;
}
void main()
{
int b = 5;
int* a = &b;
hello(a);
printf("%d", *a);
}
//输出结果为5,因为该参数传递是值传递,不对本身指针造成影响

改成地址传递应该如下:(二级指针传参)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int c = 3;

void hello(int** i)
{
*i = &c;
}
void main()
{
int b = 5;
int* a = &b;
hello(&a);
printf("%d", *a);
}
//输出结果为3,函数成功对指针进行修改

用n级指针形参,去间接修改了n-1级指针(实参)的值。

指针的步长体现在:

  • +1之后跳跃的字节数
  • 解引用 解出的字节数

结构体中算偏移量的函数

通过 offsetof( 结构体名称, 属性) 找到属性对应的偏移量

offsetof 引入头文件 #include<stddef.h>

1
2
3
//实际定义如下:
typedef unsigned int size_t
#define offsetof(s,m) (size_t)&(((*s)0)->m)

指针做函数参数,具备输入和输出特性:

  • 输入:主调函数分配内存
  • 输出:被调用函数分配内存

指针改变后的释放出错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void test(){
char *p = (char *)malloc(50);
char buf[] = "abcdef";
int n = strlen(buf);
int i = 0;

for (i = 0; i < n; i++)
{
*p = buf[i];
p++; //修改原指针指向
}

free(p);//此处出错,因为此时的p已经不是指向原来位置的指针了
}

出错显示:

image-20200925162410473

野指针,空指针,哑指针,垂悬指针

指针变量也是变量,是变量就可以任意赋值,不要越界即可(32位为4字节,64位为8字节),但是,任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。

指针操作应该记住:1. 初始化时置NULL 2. 释放时置NULL

空指针

  • 不能向NULL或者非法内存拷贝数据

往空指针里塞东西:报错如下:

image-20200921222715218

往野指针里塞东西也是报上图错误,仅红线部分不为0地址而已

野指针

导致野指针的三种情况

  • 指针变量未初始化

任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

  • 指针释放后未置空

有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是告诉系统 ,指针指向的内存可以回收了,以此把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。

空指针可以重复释放、野指针不可以重复释放

  • 指针操作超越变量作用域

不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int* doWork()
{
int a=10;
int*p=&a;
return p;
}


void main()
{
int *p=doWork();
printf("%d\n",*p);//输出10,成功输出但实际上已经指向不合法的空间
printf("%d\n",*p);//输出乱七八糟的数据,因为10已经被覆盖了
}
//会产生无法预测的值(第一次往往能正确输出)

1
2
//但是,野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用),C语言中,可以把NULL赋值给此指针,这样就标志此指针为空指针,没有任何指针
int *p = NULL;

NULL是一个值为0的宏常量:

1
#define NULL    ((void *)0)

p.s. int* p;—–windows int *p; ———Linux

哑指针和垂悬指针

垂悬指针:指向曾经存在的对象,但该对象已经不再存在了

哑指针:除了指向,没有其他任何动作的指针

二级指针的输入输出特性

二级指针的输出特性

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
37
38
//被调函数,由参数n确定分配多少个元素内存
void allocate_space(int **arr,int n){
//堆上分配n个int类型元素内存
int *temp = (int *)malloc(sizeof(int)* n);
if (NULL == temp){
return;
}
//给内存初始化值
int *pTemp = temp;
for (int i = 0; i < n;i ++){
//temp[i] = i + 100;
*pTemp = i + 100;
pTemp++;
}
//指针间接赋值
*arr = temp;
}
//打印数组
void print_array(int *arr,int n){
for (int i = 0; i < n;i ++){
printf("%d ",arr[i]);
}
printf("\n");
}
//二级指针输出特性(由被调函数分配内存)
void test(){
int *arr = NULL;
int n = 10;
//给arr指针间接赋值
allocate_space(&arr,n);
//输出arr指向数组的内存
print_array(arr, n);
//释放arr所指向内存空间的值
if (arr != NULL){
free(arr);
arr = NULL;
}
}

二级指针的输入特性

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
//打印数组
void print_array(int **arr,int n){
for (int i = 0; i < n;i ++){
printf("%d ",*(arr[i]));
}
printf("\n");
}
//二级指针输入特性(由主调函数分配内存)
void test(){

int a1 = 10;
int a2 = 20;
int a3 = 30;
int a4 = 40;
int a5 = 50;

int n = 5;

int** arr = (int **)malloc(sizeof(int *) * n);
arr[0] = &a1;
arr[1] = &a2;
arr[2] = &a3;
arr[3] = &a4;
arr[4] = &a5;

print_array(arr,n);

free(arr);
arr = NULL;
}

万能指针void *

void *指针可以指向任意变量的内存空间

void 常用于数据类型的封装*

const关键字

修饰变量:

1
2
3
4
const int a = 20;//依然可以通过指针修改a的值
int *p = &a;
*p = 650;
printf("%d\n", a);

修饰指针:

  • const int *p;(指向常量的指针)

    • 可以修改p
    • 不可以修改*p
  • int const *p;(指向常量的指针)

    同上。

  • int * const p;(常量指针)

    • 可以修改*p
    • 不可以修改p
  • const int *const p;

    • 不可以修改*p
    • 不可以修改p

总结const 向右修饰,被修饰的部分即为只读。

常用:在函数形参内,用来限制指针所对应的内存空间为只读。

  • 全局变量

直接修改 失败 ,间接修改 语法通过,运行失败,受到常量区保护

  • 局部变量

直接修改 失败 , 间接修改 成功,放在栈上

主要作用:修饰形参防止误操作

结构体作为参数时,往往会用指针传值,但用指针会有副作用,可能会不小心修改原数据,因此const诞生

p.s.定义const变量最好初始化,因为定义后无法赋值

注意:C中的const依然是变量,而C++中const修饰的东西,实际上是常量

位运算

用法(n未知项)

1
2
3
4
5
6
//打开位
00000100 | n//打开第二位
//打开所有位
n | ~n
//关闭所有位
n & ~n

左移 <<

左移一位相当于原值*2.

右移 >>

对于有符号类型,结果依赖于机器。空出的位可能用0填充,或者使用符号(最左端)位的副本填充。

移位运算符能够提供快捷、高效(依赖于硬件)对2的幂的乘法和除法。

number << n number乘以2的n次幂
number >> n 如果number非负,则用number除以2的n次幂

位域

C语言提供了一种数据结构,称为“位域”或者“位段”。所谓位域,就是把一个字节中的二进制位划分为不同的区域,并说明每个区域的位数。每个域都有一个域名,允许程序中按照域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

位域的本质就是一种结构类型,不过其成员是按二进制位分配的

使用位域的主要目的在于压缩内存

1
2
3
4
5
6
struct <位域结构名>
{
...
<类型说明符> <位域名> : <位域长度> // 位域列表
...
};

注意事项:

  • 一个位域必须存储在同一个字节中,不能跨字节存储。如一个字节所剩空间不能存储下一个位域的时候,应从下一个字节开始存储。也可以有意使某个位域从下一单元开始
  • 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说位域的不能超过8bit;
  • 位域可以无位域名,这时它只用作填充或调整位置。无名的位域是不能使用的

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Demo
{
int a : 4;
int : 0;//空域
int b : 6;//从第二个字节开始存放
};
//在这个位域定义中,a占第一个字节的4bit,这个字节的另4bit填0表示不使用,b从第二个字节开始,占4bit。

struct Demo
{
int a : 4;
int : 2; // 这2bit不能使用
int b : 2;
};

位域的使用

位域的使用与结构体相同,其一般形式为:<位域变量名>.<位域名>,位域允许各种格式的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Demo
{
unsigned int a : 1;
unsigned int b : 4;
unsigned int c : 3;
}demo1,*pdemo;

void main()
{
demo1.a = 1;
demo1.b = 15;
demo1.c = 7;
printf("demo1.a = %d, demo1.b = %d, demo1.c = %d\n",demo1.a,demo1.b,demo1.c);

pdemo = &demo1;//位域也是可以使用指针的
pdemo->a = 0;
pdemo->b &= 1;//位与运算
pdemo->c |= 3;//位或运算
printf("pdemo->a = %d, pdemo->b = %d, pdemo->c = %d\n",pdemo->a,pdemo->b,pdemo->c);
}
  • 位域结构的成员不能单独被取sizeof值
  • C99规定int、unsigned int、bool可以做位域的类型,但编译器几乎都对此做了扩展,允许其他类型的存在。

数组名

在C中,在几乎所有数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。它的类型取决于数组元素的类型:如果他们是int类型,那么数组名的类型就是“指向int的常量指针”;如果它们是其他类型,那么数组名的类型也就是“指向其他类型的常量指针”。

数组名字是数组的首元素地址,但它是一个地址常量,不可以做任何和赋值有关的操作

  • sizeof(数组)=数组的实际字节数
  • sizeof(指针)=4/8

p.s.要注意数组名和指针变量的区别。通常情况下,我们总觉得数组名和指针变量差不多,但是在用sizeof的时候差别很大,对数组名用sizeof返回的是整个数组的大小,而对指针变量进行操作的时候返回的则是指针变量本身所占得空间,在32位机的条件下一般都是4。而且当数组名作为函数参数时,在函数内部,形参也就是个指针,所以不再返回数组的大小;

数组做函数函数参数,将退化为指针,在函数内部不再返回数组大小

数组和指针的区别

但实际上,数组名和指针并不是等价的

答案是否定的。数组名在表达式中使用的时候,编译器才会产生一个指针常量。那么数组在什么情况下不能作为指针常量呢?在以下两种场景下:

当数组名作为sizeof操作符的操作数的时候,此时sizeof返回的是整个数组的长度,而不是指针数组指针的长度。n 当数组名作为&操作符的操作数的时候,此时返回的是一个指向数组的指针,而不是指向某个数组元素的指针常量。

1
2
3
4
5
int arr[10];
//arr = NULL; //arr作为指针常量,不可修改
int *p = arr; //此时arr作为指针常量来使用
printf("sizeof(arr):%d\n", sizeof(arr)); //此时sizeof结果为整个数组的长度
printf("&arr type is %s\n", typeid(&arr).name()); //int(*)[10]而不是int*

声明一个数组时,编译器根据声明所指定的元素数量为数组分配内存空间,然后再创建数组名,指向这段空间的起始位置。声明一个指针变量的时候,编译器只为指针本身分配内存空间,并不为任何整型值分配内存空间,指针并未初始化指向任何现有的内存空间。

因此,表达式*a是完全合法的,但是表达式*b却是非法的。*b将访问内存中一个不确定的位置,将会导致程序终止。另一方面b++可以通过编译,a++却不行,因为a是一个常量值。

除了两种特殊情况外,都是指向数组第一个元素的指针

  • 特殊情况1 sizeof 统计数组长度
  • 特殊情况2 对数组名取地址,数组指针,步长整个数组长度

作为函数参数的数组名

1
2
int print_array(int *arr);
int print_array(int arr[]);

我们可以使用任何一种声明,但哪一个更准确一些呢?答案是指针。因为实参实际上是个指针,而不是数组。同样sizeof arr值是指针的长度,而不是数组的长度。

​ 现在我们清楚了,为什么一维数组中无须写明它的元素数目了,因为形参只是一个指针,并不需要为数组参数分配内存。另一方面,这种方式使得函数无法知道数组的长度。如果函数需要知道数组的长度,它必须显式传递一个长度参数给函数。

数组下标可为负值

1
2
3
4
int arr[] = { 5, 3, 6, 8, 2, 9 };
int *p = arr + 2;
printf("*p = %d\n", *p);//6
printf("*p = %d\n", p[-1]);//3

二维数组名

二维数组的3种形式参数

1
2
3
void PrintArray01(int arr[3][3])
void PrintArray02(int arr[][3])
void PrintArray03(int(*arr)[3])

多维数组名间的区别

1
2
3
4
5
6
int a[5]={0};
int b[5][5]={0};
int c[5][5][5]={0};
//a的类型为int* &a的类型为int(*)[5]
//b的类型为int(*)[5] &b的类型为int(*)[5][5]
//c的类型为int(*)[5][5] &b的类型为int(*)[5][5][5]

数组指针

注意:对于一个数组定义 int arr[100]; arr这个数组名与&arr这个数组指针在内存中是同一个值.&arr并是一个二级指针,他是一个特殊的类型叫数组指针.

数组的类型由元素类型数组大小共同决定:int array[5] 的类型为 int[5];

定义数组指针有一下三种方式:

  1. 先定义数组类型,再用数组类型定义数组指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void test01(){

//先定义数组类型,再用数组类型定义数组指针
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
//有typedef是定义类型,没有则是定义变量,下面代码定义了一个数组类型ArrayType
typedef int(ArrayType)[10];
//int ArrayType[10]; //定义一个数组,数组名为ArrayType

ArrayType myarr; //等价于 int myarr[10];
ArrayType* pArr = &arr; //定义了一个数组指针pArr,并且指针指向数组arr
for (int i = 0; i < 10;i++){
printf("%d ",(*pArr)[i]);
}
printf("\n");
}
  1. 直接定义数组指针类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void test02(){

int arr[10];
//定义数组指针类型
typedef int(*ArrayType)[10];
ArrayType pArr = &arr; //定义了一个数组指针pArr,并且指针指向数组arr
for (int i = 0; i < 10; i++){
(*pArr)[i] = i + 1;
}
for (int i = 0; i < 10; i++){
printf("%d ", (*pArr)[i]);
}
printf("\n");

}
  1. 直接定义数组指针类型的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void test03(){

int arr[10];
int(*pArr)[10] = &arr;

for (int i = 0; i < 10; i++){
(*pArr)[i] = i + 1;

}
for (int i = 0; i < 10; i++){
printf("%d ", (*pArr)[i]);
}
printf("\n");
}

字符串注意点:

  • vs 将多个相同字符串常量看成一个
  • 不可以修改字符串常量
  • ANSI并没有制定出字符串是否可以修改的标准,根据编译器不同,可能最终结果也是不同的

指针加减运算

  • 指针计算不是简单的整数相加减
  • 如果是一个int *,+1的结果是增加一个int的大小
  • 如果是一个char *,+1的结果是增加一个char大小

多级指针

1
2
3
4
5
6
7
8
9
10
11
12
int a = 10;
int *p = &a; //一级指针
*p = 100; //*p就是a

int **q = &p;
//*q就是p
//**q就是a

int ***t = &q;
//*t就是q
//**t就是p
//***t就是a

指针数组做为main函数的形参

1
int main(int argc, char *argv[]);
  • main函数是操作系统调用的,第一个参数标明argc数组的成员数量,argv数组的每个成员都是char *类型
  • argv是命令行参数的字符串数组
  • argc代表命令行参数的数量,程序名字本身算一个参数

在VS中。项目名称上 –》右键–》属性–》调试–》命令行参数 –》将 命令行写入。

数组做函数返回值

C语言,不允许!!!只能写成指针形式(指针做返回值时候注意不要返回局部变量地址)

字符串格式化注意点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   //字符数组只能初始化5个字符,当输出的时候,从开始位置直到找到0结束
char str1[] = { 'h', 'e', 'l', 'l', 'o' };
printf("%s\n",str1);

//字符数组部分初始化,剩余填0
char str2[100] = { 'h', 'e', 'l', 'l', 'o' };
printf("%s\n", str2);

//请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少?
char str5[] = "hello\0world";
printf("%s\n",str5);
printf("sizeof str5:%d\n",sizeof(str5));//12
printf("strlen str5:%d\n",strlen(str5));//5

//再请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少?
char str6[] = "hello\012world";
printf("%s\n", str6);
printf("sizeof str6:%d\n", sizeof(str6));//12
printf("strlen str6:%d\n", strlen(str6));//11

八进制和十六进制转义字符:

在C中有两种特殊的字符,八进制转义字符和十六进制转义字符,八进制字符的一般形式是’\ddd’,d是0-7的数字。十六进制字符的一般形式是’\xhh’,h是0-9或A-F内的一个。八进制字符和十六进制字符表示的是字符的ASCII码对应的数值。

比如 :

  • ‘\063’表示的是字符’3’,因为’3’的ASCII码是30(十六进制),48(十进制),63(八进制)。
  • ‘\x41’表示的是字符’A’,因为’A’的ASCII码是41(十六进制),65(十进制),101(八进制)。

字符串

1
2
3
4
5
6
char str1[] = {'h', 'i', '\0'};			 //变量,可读可写

char str2[] = "hi"; //变量,可读可写

char *str3 = "hi"; //常量,只读
//str3中存的是常量地址

字符串处理函数 #include <string.h>

strcpy()

把src所指向的字符串复制到dest所指向的空间中,’\0’也会拷贝过去

1
2
3
char *strcpy(char *dest, const char *src);
//参数: dest:目的字符串首地址 src:源字符首地址
//返回值: 成功:返回dest字符串的首地址 失败:NULL

strncpy()

功能:把src指向字符串的前n个字符复制到dest所指向的空间中,是否拷贝结束符看指定的长度是否包含’\0’。

1
char *strncpy(char *dest, const char *src, size_t n);

strcat()

功能:将src字符串连接到dest的尾部,‘\0’也会追加过去

strncat()

功能:将src字符串前n个字符连接到dest的尾部,‘\0’也会追加过去

strcmp()

功能:比较 s1 和 s2 的大小,比较的是字符ASCII码大小。

strncmp()

只比较 s1 和 s2 前n个字符的大小,比较的是字符ASCII码大小。

strchr()

1
2
3
4
5
6
7
8
9
#include <string.h>
char *strchr(const char *s, int c);
//功能:在字符串s中查找字母c出现的位置
//参数:
// s:字符串首地址
// c:匹配字母(字符)c
//返回值:
// 成功:返回第一次出现的c地址
// 失败:NULL

strstr()

1
2
3
4
5
6
7
8
9
#include <string.h>
char *strstr(const char *haystack, const char *needle);
//功能:在字符串haystack中查找字符串needle出现的位置
//参数:
// haystack:源字符串首地址
// needle:匹配字符串首地址
//返回值:
// 成功:返回第一次出现的needle地址
// 失败:NULL

strtok()

功能:来将字符串分割成一个个片段。当strtok()在参数s的字符串中发现参数delim中包含的分割字符时, 则会将该字符改为\0 字符,当连续出现多个时只替换第一个为\0。

1
2
3
char *strtok(char *str, const char *delim);
//参数:str:指向欲分割的字符串 delim:分隔符字符(如果传入字符串,则传入的字符串中每个字符均为分割符)
//返回值:成功:分割后字符串首地址 失败:NULL
  • 在第一次调用时:strtok()必需给予参数s字符串

  • 往后的调用则将参数s设置成NULL,每次调用成功则返回指向被分割出片段的指针

    strtok函数会破坏被分解字符串的完整,调用前和调用后的s已经不一样了。如果要保持原字符串的完整,可以使用strchr和sscanf的组合等。

strtok是一个线程不安全的函数,因为它使用了静态分配的空间来存储被分割的字符串位置,线程安全的函数叫strtok_r,ca

字符串处理函数 #include <stdio.h>

sprintf()

功能:根据参数format字符串来转换并格式化数据,然后将结果输出到str指定的空间中,直到出现字符串结束符 ‘\0’ 为止。...可变参数列表

1
2
3
int sprintf(char *str, const char *format, ...);
//参数:str:字符串首地址 format:字符串格式,用法和printf()一样
//返回值:成功:实际格式化的字符个数 失败: - 1

sscanf()

功能:从str指定的字符串读取数据,并根据参数format字符串来转换并格式化数据。

1
2
3
int sscanf(const char *str, const char *format, ...);
//参数:str:指定的字符串首地址 format:字符串格式,用法和scanf()一样
//返回值:成功:参数数目,成功转换的值的个数 失败: - 1
格式 作用
%*s或%*d 跳过数据(遇到空格或\t代表结束忽略)
%[width]s 读指定宽度的数据,此处[]括号并不需要真写
%[a-z] 匹配a到z中任意字符(尽可能多的匹配)(只要有一个字符匹配失败,就不继续匹配了)
%[aBc] 匹配a、B、c中一员,贪婪性
%[^a] 匹配非a的任意字符,贪婪性
%[^a-z] 表示读取除a-z以外的所有字符

p.s.上面所有都是可以加星表示跳过的,并且都可以指定读指定宽度的数据

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
//例子1
char *ip="127.0.0.1";
int num1=0,num2=0,num3=0,num4=0;
sscanf(ip,"%d.%d.%d.%d",&num1,&num2,&num3,&num4);
//例子2
char *str="abcdef#helloworld@123456";//提取字符串#和@间的数据
char buf[1024]={0};
sscanf(str,"%*[^#]#%[^@]",buf);
//例子3
char *str="helloworld@itcast.cn";//提取helloworld和itcast.cn
char buf1[1024]={0};
char buf2[1024]={0};
sscanf(str,"%[^@]@%s",buf1,buf2);

字符串处理函数 #include <stdlib.h>

atoi()

功能:把一个整形形式的字符串转化为一个整数。atoi()会扫描nptr字符串,跳过前面的空格字符,直到遇到数字或正负号才开始做转换,而遇到非数字或字符串结束符(‘\0’)才结束转换,并将结果返回返回值。

1
2
int atoi(const char *nptr);
//参数:nptr:待转换的字符串 返回值:成功转换后整数

类似的函数还有以下

atof()

功能:把一个小数形式的字符串转化为一个浮点数。

atol()

功能:将一个字符串转化为long类型

atof() 将字符串转换为双精度浮点型值
atoi() 将字符串转换为整型值
atol() 将字符串转换为长整型值
strtod() 将字符串转换为双精度浮点型值,并报告不能被转换的所有剩余数字
strtol() 将字符串转换为memset*(*buf*,* 0*,* 1024*);*

​ sprintf*(*buf*,* “%-8d”*,* num*);*

​ printf*(*“buf:%s\n”*,* buf****);****长整值,并报告不能被转换的所有剩余数字
strtoul() 将字符串转换为无符号长整型值,并报告不能被转换的所有剩余数字

自己实现字符串拷贝:

1
2
3
4
void copy_string03(char* dest, char* source){
//判断*dest是否为0,0则退出循环
while (*dest++ = *source++){}
}

strtol()

功能:将字符串转换为指定进制的数字

1
2
3
4
long int strtol(const char* str, char** endptr, int base)
//参数str:要转换的字符串 如:"123","0x12","123abc"
//参数endptr:二级指针,返回非数字部分的字符串指针,测试的时候使用,一般指定为NULL(下面有案例)
//进制数base:指定转换的进制(如果str是0x12,这里不是16进制就会报错)

案例

1
2
3
4
char* p = "123ABC";
char* pt = NULL;
strtol(p,&pt,10);
//strtol函数返回123.如果打印pt的值为"abc"

内存管理

作用域

C语言变量的作用域分为:

  • 代码块作用域(代码块是{}之间的一段代码)
  • 函数作用域
  • 文件作用域

局部变量

局部变量也叫auto自动变量(auto可写可不写),一般情况下代码块{}内部定义的变量都是自动变量

它有如下特点:

  • 在一个函数内定义,只在函数范围内有效

  • 在复合语句中定义,只在复合语句中有效

  • 随着函数调用的结束或复合语句的结束局部变量的声明声明周期也结束

  • 如果没有赋初值,内容为随机

  • 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用。

静态(static)局部变量

  • static局部变量的作用域也是在定义的函数内有效
  • static局部变量的生命周期和程序运行周期一样,同时staitc局部变量的值只初始化一次,但可以赋值多次
  • static局部变量若未赋以初值,则由系统自动赋值:数值型变量自动赋初值0,字符型变量赋空字符

全局变量

  • 在函数外定义,可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,须用extern声明
  • 全局变量的生命周期和程序运行周期一样
  • 不同文件的全局变量不可重名

静态(static)全局变量

  • 在函数外定义,作用范围被限制在所定义的文件中
  • 不同文件静态全局变量可以重名,但作用域不冲突
  • static全局变量的生命周期和程序运行周期一样,同时staitc全局变量的值只初始化一次

静态全局变量和普通全局变量的区别,说明:

全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量。
全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。
这两者的区别在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。

extern全局变量声明

extern int a;声明一个变量,这个全局变量在别的文件中已经定义了,这里只是声明,而不是定义。

全局函数和静态函数

在C语言中函数默认都是全局的,使用关键字static可以将函数声明为静态,函数定义为static就意味着这个函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数都没用。

对于不同文件中的staitc函数名字可以相同。

p.s.所有的函数默认都是全局的,意味着所有的函数都不能重名,但如果是staitc函数,那么作用域是文件级的,所以不同的文件static函数名是可以相同的。

总结

类型 作用域 生命周期
auto变量 一对{}内 当前函数
static局部变量 一对{}内 整个程序运行期
extern变量 整个程序 整个程序运行期
static全局变量 当前文件 整个程序运行期
extern函数 整个程序 整个程序运行期
static函数 当前文件 整个程序运行期
register变量 一对{}内 当前函数
全局变量 整个程序 整个程序运行期

内存布局

内存分区

可执行文件结构

C代码经过预处理、编译、汇编、链接4步后生成一个可执行程序。

在 Windows 下,程序是一个普通的可执行文件,以下列出一个二进制可执行文件的基本情况:

img

通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)。

代码区(text segment)

存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。

全局初始化数据区/静态数据区(data段)

该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。

未初始化数据区(又叫 bss 区)

存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。

程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。

img

img

  • .text   代码段
  • .rodata 存储字符串常量
  • .data  存储已初始化的全局/静态变量
  • .bss    存储为初始化/初始化为0的全局/静态变量(在可执行文件中只占一个占位符,程序加载的时候才分配空间)
进程内存结构
代码区(text segment)(翻译:文本部分)

加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。

未初始化数据区(BSS)

加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。

全局初始化数据区/静态数据区(data segment)

加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。

全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量静态变量常量

示例代码:

1
2
3
4
5
6
7
8
9
10
int v1 = 10;//全局/静态区
const int v2 = 20; //常量,一旦初始化,不可修改
static int v3 = 20; //全局/静态区
char *p1; //全局/静态区,编译器默认初始化为NULL

//那么全局static int 和 全局int变量有什么区别?

void test(){
static int v4 = 20; //全局/静态区
}
栈区(stack)

栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。

img

img

正如上图所示的那样,随着函数被逐级调用,编译器会为每一个函数建立自己的栈框架,栈空间逐渐消耗。随着函数的逐级返回,该函数的栈框架也将被逐级销毁,栈空间得以逐步释放。顺便说一句,递归函数的嵌套调用深度通常也是取决于运行时栈空间的剩余尺寸。

img

如图为:内存生长方向(小端模式)

函数的调用方和被调用方对于函数是如何调用的必须有一个明确的约定,只有双方都遵循同样的约定,函数才能够被正确的调用,这样的约定被称为调用惯例/调用约定(Calling Convention)

调用惯例包含如下几点:

  • 函数参数的传递顺序和方式
  • 栈的维护方式

注意: cdecl不是标准的关键字,在不同的编译器里可能有不同的写法,例如gcc里就不存在_cdecl这样的关键字,而是使用__attribute_((cdecl)).

调用惯例 出栈方 参数传递 名字修饰
cdecl 函数调用方 从右至左参数入栈 下划线+函数名
stdcall 函数本身 从右至左参数入栈 下划线+函数名+@+参数字节数
fastcall 函数本身 前两个参数由寄存器传递,其余参数通过堆栈传递。 @+函数名+@+参数的字节数
pascal 函数本身 从左至右参数入栈 较为复杂,参见相关文档

堆区(heap)

堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

注意:

全局静态存储区内的常量分为常变量和字符串常量,一经初始化,不可修改。静态存储内的常变量是全局变量,与局部常变量不同,区别在于局部常变量存放于栈,实际可间接通过指针或者引用进行修改,而全局常变量存放于静态常量区则不可以间接修改。

数据区包括:堆,栈,全局/静态存储区。
全局/静态存储区包括:常量区,全局区、静态区。
常量区包括:字符串常量区、常变量区。
代码区:存放程序编译后的二进制代码,不可寻址区。

可以说,C/C++内存分区其实只有两个,即代码区和数据区。

存储类型总结
类型 作用域 生命周期 存储位置
auto变量 一对{}内 当前函数 栈区
static局部变量 一对{}内 整个程序运行期 初始化在data段,未初始化在BSS段
extern变量 整个程序 整个程序运行期 初始化在data段,未初始化在BSS段
static全局变量 当前文件 整个程序运行期 初始化在data段,未初始化在BSS段
extern函数 整个程序 整个程序运行期 代码区
static函数 当前文件 整个程序运行期 代码区
register变量 一对{}内 当前函数 运行时存储在CPU寄存器
字符串常量 当前文件 整个程序运行期 data段

1568374636_120805

内存操作函数

#include <string.h>

memset()

1
2
3
4
5
6
7
void *memset(void *s, int c, size_t n);
//功能:将s的内存区域的前n个字节以参数c填入
//参数:
// s:需要操作内存s的首地址
// c:填充的字符,c虽然参数为int,但必须是unsigned char , 范围为0~255
// n:指定需要设置的大小
//返回值:s的首地址

memcpy()

1
2
3
4
5
6
7
void *memcpy(void *dest, const void *src, size_t n);
//功能:拷贝src所指的内存内容的前n个字节到dest所值的内存地址上。
//参数:
// dest:目的内存首地址
// src:源内存首地址,注意:dest和src所指的内存空间不可重叠,可能会导致程序报错
// n:需要拷贝的字节数
//返回值:dest的首地址

memmove()

memmove()功能用法和memcpy()一样,区别在于:dest和src所指的内存空间重叠时,memmove()仍然能处理,不过执行效率比memcpy()低些。

memcmp()

1
2
3
4
5
6
7
8
9
10
int memcmp(const void *s1, const void *s2, size_t n);
//功能:比较s1和s2所指向内存区域的前n个字节
//参数:
// s1:内存首地址1
// s2:内存首地址2
// n:需比较的前n个字节
//返回值:
// 相等:=0
// 大于:>0
// 小于:<0

堆区内存分配和释放#include <stdlib.h>

malloc()

1
2
3
4
5
6
7
void *malloc(size_t size);
//功能:在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型。分配的内存空间内容不确定,一般使用memset初始化。
//参数:
// size:需要分配内存大小(单位:字节)
//返回值:
//成功:分配空间的起始地址
//失败:NULL

free()

1
2
3
4
5
void free(void *ptr);
//功能:释放ptr所指向的一块内存空间,ptr是一个任意类型的指针变量,指向被释放区域的首地址。对同一内存空间多次释放会出错。
//参数:
//ptr:需要释放空间的首地址,被释放区应是由malloc函数所分配的区域。
//返回值:无

calloc()

1
2
3
4
5
6
7
8
9
void *calloc(size_t nmemb, size_t size);
//功能:
//在内存动态存储区中分配nmemb块长度为size字节的连续区域。calloc自动将分配的内存 置0。
//参数:
//nmemb:所需内存单元数量
//size:每个内存单元的大小(单位:字节)
//返回值:
// 成功:分配空间的起始地址
//失败:NULL

realloc()

1
2
3
4
5
6
7
8
9
10
void *realloc(void *ptr, size_t size);
//功能:
//重新分配用malloc或者calloc函数在堆中分配内存空间的大小。
//realloc不会自动清理增加的内存,需要手动清理,如果指定的地址后面有连续的空间,那么就会在已有地址基础上增加内存,如果指定的地址后面没有空间,那么realloc会重新分配新的连续内存,把旧内存的值拷贝到新内存,同时释放旧内存。
//参数:
//ptr:为之前用malloc或者calloc分配的内存地址,如果此参数等于NULL,那么和realloc与malloc功能一致
//size:为重新分配内存的大小, 单位:字节
//返回值:
//成功:新分配的堆内存地址
//失败:NULL

复合类型(自定义类型)

结构体

定义结构体变量的方式:

  • 先声明结构体类型再定义变量名
  • 在声明类型的同时定义变量
  • 直接定义结构体类型变量(无类型名)

img

1
2
3
4
5
6
7
8
9
struct test{
//...
};
typedef struct test myTest;
//给struct test起别名为myTest,下面为等价简化版本
typedef struct test{
//...
}myTest;
//即结构体定义时候,有typedef的话,myTest是类型别名,没有typedef的情况下,myTest直接就是结构体test的变量了

结构体变量的初始化

1
2
3
4
5
6
7
8
9
10
11
struct Person{
char name[64];
int age;
}p1 = {"john",10}; //定义类型同时初始化变量

struct{
char name[64];
int age;
}p2 = {"Obama",30}; //定义类型同时初始化变量

struct Person p3 = {"Edward",33}; //通过类型直接定义

结构体赋值

深拷贝和浅拷贝

重点:

  • 系统提供的赋值操作是 浅拷贝 – 简单值拷贝,逐字节拷贝
  • 如果结构体中有属性 创建在堆区,就会出现问题,在释放期间,一段内存重复释放,一段内存泄露

解决方案:自己手动去做赋值操作,提供深拷贝

堆创建并释放带指针的结构体数组的案例:(要求如下图:)

img

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
37
38
39
struct Person {
char* name;
int age;
};

//顺序创建
struct Person** pArray = (struct Person**)malloc(sizeof(struct Person*) * 3);
for (size_t i = 0; i < 3; i++)
{
pArray[i] = (struct Person*)malloc(sizeof(struct Person));
pArray[i]->age = i + 100;
pArray[i]->name = (char*)malloc(sizeof(char)*64);
}
memcpy(pArray[0]->name, "小明",strlen("小明")+1);
memcpy(pArray[1]->name, "小方", strlen("小方") + 1);
memcpy(pArray[2]->name, "王晓东", strlen("王晓东") + 1);
for (size_t i = 0; i < 3; i++)
{
printf("%d号同学 名称:%s 年龄:%d\r\n", i, pArray[i]->name, pArray[i]->age);
}
//倒序释放
for (size_t i = 0; i < 3; i++)
{
if (pArray[i]->name!=NULL)
{
free(pArray[i]->name);
pArray[i]->name = NULL;
}
if (pArray[i]!=NULL)
{
free(pArray[i]);
pArray[i] = NULL;
}
}
if (pArray!=NULL)
{
free(pArray);
pArray = NULL;
}

创建和释放可封装成函数,创建的话需要传出地址,释放要修改地址为NULL,因此有两种方案解决如下:

  1. 创建释放时通过参数传递进去,此时由于需要改变指针,因此应该传进指针的指针
  2. 创建的时候可以通过返回值,返回创建的指针,避免多级指针的使用

结构体字节对齐

  1. 从第一个属性开始,从0开始偏移
  2. 从第二个往后的属性开始,要放在min(当前成员的大小,#pargama pack(n))的整数倍地址上
  3. 所有属性都计算完后,结构体总的大小,也就是sizeof的结果必须是min(结构体内部最大成员(子结构体需要拆解来看),#pargama pack(n))的整数倍,不足要补齐。
  4. 注意,结构体嵌套结构体的时候,子结构体只需要放在子结构体中最大类型和对齐模数比的最大值的整数倍上
1
2
//显示当前packing alignment的字节数,生成项目的时候以warning message的形式被显示。
#pragma pack(show)

image-20201010154522488

案例如下:

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
#pragma pack(show)//==8
struct student
{
int a;//0~3
char b;//4
double c;//8~15
float d;//16~19
};
struct teacher
{
int a ;//0~3
struct student b;//8~31
int c;//32~35

};
int main(int argc, char *argv[])
{
struct student stu1;
stu1.a = 10;
stu1.b = 'b';
stu1.c = 100;
stu1.d = 200;
struct teacher teac1;
teac1.a = 10;
teac1.b = stu1;
teac1.c = 10;
printf("%d", sizeof(struct student));
printf("%d", sizeof(struct teacher));
return 0;
}

stu1内存占用如下:

image-20201010164139876

其他字节对齐命令:

  • #pragma pack(push):

英文单词push是“压入”的意思。编译器编译到此处时将保存对齐状态(保存的是push指令之前的对齐状态)。

  • #pragma pack(pop):

英文单词pop是”弹出“的意思。编译器编译到此处时将恢复push指令前保存的对齐状态(请在使用该预处理命令之前使用#pragma pack(push))。

push和pop是一对应该同时出现的名词,只有pop没有push不起作用,只有push没有pop可以保持之前对齐状态(但是这样就没有使用push的必要了)。

  • #pragma pack() 能够取消自定义的对齐方式,恢复默认对齐。

共用体(联合体)

  • 联合union是一个能在同一个存储空间存储不同类型数据的类型;
  • 联合体所占的内存长度等于其最长成员的长度倍数,也有叫做共用体;
  • 同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用;
  • 共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖;
  • 共用体变量的地址和它的各成员的地址都是同一地址。
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
#include <stdio.h>

//共用体也叫联合体
union Test
{
unsigned char a;
unsigned int b;
unsigned short c;
};

int main()
{
//定义共用体变量
union Test tmp;

//1、所有成员的首地址是一样的
printf("%p, %p, %p\n", &(tmp.a), &(tmp.b), &(tmp.c));

//2、共用体大小为最大成员类型的大小
printf("%lu\n", sizeof(union Test));

//3、一个成员赋值,会影响另外的成员
//左边是高位,右边是低位
//低位放低地址,高位放高地址
tmp.b = 0x44332211;

printf("%x\n", tmp.a); //11
printf("%x\n", tmp.c); //2211

tmp.a = 0x00;
printf("short: %x\n", tmp.c); //2200
printf("int: %x\n", tmp.b); //44332200

return 0;
}

枚举

枚举:将变量的值一一列举出来,变量的值只限于列举出来的值的范围内。

1
2
3
4
5
//枚举类型定义:
enum 枚举名
{
//枚举值表
};
  • 在枚举值表中应列出所有可用值,也称为枚举元素。
  • 枚举值是常量,不能在程序中用赋值语句再对它赋值。
  • 举元素本身由系统定义了一个表示序号的数值从0开始顺序定义为0,1,2 …

示例如下:

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
#include <stdio.h>

enum weekday
{
sun = 2, mon, tue, wed, thu, fri, sat
} ;

enum bool
{
flase, true
};

int main()
{
enum weekday a, b, c;
a = sun;
b = mon;
c = tue;
printf("%d,%d,%d\n", a, b, c);

enum bool flag;
flag = true;

if (flag == 1)
{
printf("flag为真\n");
}
return 0;
}

typedef

typedef为C语言的关键字,作用是为一种数据类型(基本类型或自定义数据类型)定义一个新名字,不能创建新类型

  • 与#define不同,typedef仅限于数据类型,而不是能是表达式或具体的值
  • #define发生在预处理,typedef发生在编译阶段

void数据类型

void字面意思是”无类型”,void* 无类型指针,无类型指针可以指向任何类型的数据。void定义变量是没有任何意义的,当你定义void a,编译器会报错。

void真正用在以下两个方面:

  • 对函数返回的限定;
  • 对函数参数的限定;

void的使用

  • 不可以利用void创建变量 无法给无类型变量分配内存
  • 用途:限定函数返回值,函数参数
  • void * 万能指针 可以不通过强制类型转换就转成其他类型指针

文件操作

磁盘文件和设备文件

磁盘文件

指一组相关数据的有序集合,通常存储在外部介质(如磁盘)上,使用时才调入内存。

设备文件

在操作系统中把每一个与主机相连的输入、输出设备看作是一个文件,把它们的输入、输出等同于对磁盘文件的读和写。

磁盘文件的分类

计算机的存储在物理上是二进制的,所以物理上所有的磁盘文件本质上都是一样的:以字节为单位进行顺序存储。

从用户或者操作系统使用的角度(逻辑上)把文件分为:

  1. 文本文件:基于字符编码的文件
  2. 二进制文件:基于值编码的文件

重点:

  1. fwrite/fread将以二进制形式写入/读取文件,例如像例子中的int类型,将会以数值形式保存。若使用记事本等程序打开将会看到无法识别的内容。
  2. fprintf/fscanf把数据内容格式化为字符串,实际写入文件的内容为该字符串每一个字符的ASCII码。若用记事本打开则显示文本内容。

文本文件和二进制文件

文本文件

  • 基于字符编码,常见编码有ASCII、UNICODE等
  • 一般可以使用文本编辑器直接打开
  • 数5678的以ASCII存储形式(ASCII码)为:00110101 00110110 00110111 00111000

二进制文件

  • 基于值编码,自己根据具体应用,指定某个值是什么意思
  • 把内存中的数据按其在内存中的存储形式原样输出到磁盘上
  • 数5678的存储形式(二进制码)为:00010110 00101110

二者区别:当对文件使用文本方式打开的时候,读写的windows文件中的换行符\r\n会被替换成\n读到内存中,当在windows下写入文件的时候,\n被替换成\r\n再写入文件。如果使用二进制方式打开文件,则不进行\r\n和\n之间的转换。 那么由于Linux下的换行符就是\n,所以文本文件方式和二进制方式无区别。

理解:

  1. r 读到\r\n会改为\n,读到\x1a会返回EOF
  2. rb 读到什么返回什么,读到文件末尾才会返回EOF

文件I/O的特点:

  1. 程序为同时处于活动状态的每个文件声明一个指针变量,其类型为FILE*。这个指针指向这个FILE结构,当它处于活动状态时由流使用。
  2. 流通过fopen函数打开。为了打开一个流,我们必须指定需要访问的文件或设备以及他们的访问方式(读、写、或者读写)。Fopen和操作系统验证文件或者设备是否存在并初始化FILE。
  3. 根据需要对文件进行读写操作。
  4. 最后调用fclose函数关闭流。关闭一个流可以防止与它相关的文件被再次访问,保证任何存储于缓冲区中的数据被正确写入到文件中,并且释放FILE结构。

**p.s.**标准I/O更为简单,因为它们并不需要打开或者关闭。

I/O函数家族:

家族名 目的 可用于所有流 只用于stdin和stdout
getchar 字符输入 fgetc、getc getchar
putchar 字符输出 fputc、putc putchar
gets 文本行输入 fgets gets
puts 文本行输出 fputs puts
scanf 格式化输入 fscanf scanf
printf 格式化输出 fprintf printf

文件指针

在C语言中用一个指针变量指向一个文件,这个指针称为文件指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
//ANSI C并未规定FILE的成员,不同编译器可能有不同的定义。
typedef struct
{
short level; //缓冲区"满"或者"空"的程度
unsigned flags; //文件状态标志(读到文件尾巴,该标志为true,否则false)
char fd; //文件描述符
unsigned char hold; //如无缓冲区不读取字符
short bsize; //缓冲区的大小
unsigned char *buffer;//数据缓冲区的位置
unsigned ar; //指针,当前的指向
unsigned istemp; //临时文件,指示器
short token; //用于有效性的检查
}FILE;

FILE是系统使用typedef定义出来的有关文件信息的一种结构体类型,结构中含有文件名、文件状态和文件当前位置等信息。

声明FILE结构体类型的信息包含在头文件“stdio.h”中,一般设置一个指向FILE类型变量的指针变量,然后通过它来引用这些FILE类型变量。通过文件指针就可对它所指的文件进行各种操作。

img

C语言中有三个特殊的文件指针由系统默认打开,用户无需定义即可直接使用:

  1. stdin: 标准输入,默认为当前终端(键盘),我们使用的scanf、getchar函数默认从此终端获得数据。
  2. stdout:标准输出,默认为当前终端(屏幕),我们使用的printf、puts函数默认输出信息到此终端。
  3. stderr:标准出错,默认为当前终端(屏幕),我们使用的perror函数默认输出信息到此终端。

文件缓冲区

ANSI C标准采用“缓冲文件系统”处理数据文件。

所谓缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。

如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量) 。

磁盘文件的存取

img

img

  • 磁盘文件,一般保存在硬盘、U盘等掉电不丢失的磁盘设备中,在需要时调入内存
  • 在内存中对文件进行编辑处理后,保存到磁盘中
  • 程序与磁盘之间交互,不是立即完成,系统或程序可根据需要设置缓冲区,以提高存取效率

更新缓冲区

1
2
3
4
5
6
7
8
#include <stdio.h>
int fflush(FILE *stream);
//功能:更新缓冲区,让缓冲区的数据立马写到文件中。
//参数:
//stream:文件指针
//返回值:
//成功:0
//失败:-1

缓冲区的优势

  1. 提高硬件寿命(减少读写硬盘的次数)
  2. 提高读写效率,直接从内存进行读写

文件的打开和关闭

文件的打开

任何文件使用之前必须打开:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
FILE * fopen(const char * filename, const char * mode);
//功能:打开文件
//参数:
// filename:需要打开的文件名,根据需要加上路径
// mode:打开文件的模式设置
//返回值:
// 成功:文件指针
// 失败:NULL

第一个参数的几种形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FILE *fp_passwd = NULL;

//相对路径:
//打开当前目录passdw文件:源文件(源程序)所在目录
FILE *fp_passwd = fopen("passwd.txt", "r");

//打开当前目录(test)下passwd.txt文件
fp_passwd = fopen(". / test / passwd.txt", "r");

//打开当前目录上一级目录(相对当前目录)passwd.txt文件
fp_passwd = fopen(".. / passwd.txt", "r");

//绝对路径:
//打开C盘test目录下一个叫passwd.txt文件
fp_passwd = fopen("c:/test/passwd.txt","r");

第二个参数的几种形式(打开文件的方式):

打开模式 含义
r或rb 以只读方式打开一个文本文件(不创建文件,若文件不存在则报错)
w或wb 以写方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件)
a或ab 以追加方式打开文件,在末尾添加内容,若文件不存在则创建文件
r+或rb+ 以可读、可写的方式打开文件(不创建新文件)
w+或wb+ 以可读、可写的方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件)
a+或ab+ 以添加方式打开可读、可写的文件。若文件不存在则创建文件;如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留。
方式 含义
“r” 打开,只读,文件必须已经存在。
“w” 只写,如果文件不存在则创建,如果文件已存在则把文件长度截断(Truncate)为0字节。再重新写,也就是替换掉原来的文件内容文件指针指到头。
“a” 只能在文件末尾追加数据,如果文件不存在则创建
“rb” 打开一个二进制文件,只读
“wb” 打开一个二进制文件,只写
“ab” 打开一个二进制文件,追加
“r+” 允许读和写,文件必须已存在
“w+” 允许读和写,如果文件不存在则创建,如果文件已存在则把文件长度截断为0字节再重新写 。
“a+” 允许读和追加数据,如果文件不存在则创建
“rb+” 以读/写方式打开一个二进制文件
“wb+” 以读/写方式建立一个新的二进制文件
“ab+” 以读/写方式打开一个二进制文件进行追加

注意:

  • b是二进制模式的意思,b只是在Windows有效,在Linux用r和rb的结果是一样的

  • Unix和Linux下所有的文本文件行都是\n结尾,而Windows所有的文本文件行都是\r\n结尾

  • 在Windows平台下,以“文本”方式打开文件,不加b:

    • 当读取文件的时候,系统会将所有的 “\r\n” 转换成 “\n”
    • 当写入文件的时候,系统会将 “\n” 转换成 “\r\n” 写入
    • 以”二进制”方式打开文件,则读写都不会进行这样的转换
  • 在Unix/Linux平台下,“文本”与“二进制”模式没有区别,”\r\n” 作为两个字符原样输入输出

P.S.

“\“这样的路径形式,只能在windows使用

“/“这样的路径形式,windows和linux平台下都可用,建议使用这种

文件的关闭

任何文件在使用后应该关闭:

  • 打开的文件会占用内存资源,如果总是打开不关闭,会消耗很多内存
  • 一个进程同时打开的文件数是有限制的,超过最大同时打开文件数,再次调用fopen打开文件会失败
  • 如果没有明确的调用fclose关闭打开的文件,那么程序在退出的时候,操作系统会统一关闭。
1
2
3
4
5
6
7
8
#include <stdio.h>
int fclose(FILE * stream);
//功能:关闭先前fopen()打开的文件。此动作让缓冲区的数据写入文件中,并释放系统所提供的文件资//源。
//参数:
// stream:文件指针
//返回值:
// 成功:0
// 失败:-1

文件的顺序读写

  • 按照字符读写文件:fgetc(), fputc()
  • 按照行读写文件:fputs(), fgets()
  • 按照块读写文件:fread(), fwirte()
  • 按照格式化读写文件:fprintf(), fscanf()
  • 按照随机位置读写文件:fseek(), ftell(), rewind()

首先一定要记住fread函数只用于读二进制文件(即fopen打开方式必须带b),而fscanf可以读文本也可以读二进制。

按照字符读写文件fgetc、fputc、feof

写文件
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int fputc(int ch, FILE * stream);
//功能:将ch转换为unsigned char后写入stream指定的文件中
//参数:
// ch:需要写入文件的字符
// stream:文件指针
//返回值:
// 成功:成功写入文件的字符
// 失败:返回-1
文件结尾

在C语言中,EOF表示文件结束符(end of file)。在while循环中以EOF作为文件结束标志,这种以EOF作为文件结束标志的文件,必须是文本文件。在文本文件中,数据都是以字符的ASCII代码值的形式存放。我们知道,ASCII代码值的范围是0~127,不可能出现-1,因此可以用EOF作为文件结束标志。

1
#define EOF    (-1)

当把数据以二进制形式存放到文件中时,就会有-1值的出现,因此不能采用EOF作为二进制文件的结束标志。为解决这一个问题,ANSI C提供一个feof函数,用来判断文件是否结束。feof函数既可用以判断二进制文件又可用以判断文本文件。

1
2
3
4
5
6
7
8
#include <stdio.h>
int feof(FILE * stream);
//功能:检测是否读取到了文件结尾。判断的是最后一次“读操作的内容”,不是当前位置内容(上一个内容)。
//参数:
// stream:文件指针
//返回值:
// 非0值:已经到文件结尾
// 0:没有到文件结尾
读文件
1
2
3
4
5
6
7
8
#include <stdio.h>
int fgetc(FILE * stream);
//功能:从stream指定的文件中读取一个字符
//参数:
// stream:文件指针
//返回值:
// 成功:返回读取到的字符
// 失败:-1

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char ch;
#if 0
while ((ch = fgetc(fp)) != EOF)//读到的不是EOF则循环
{
printf("%c", ch);
}
printf("\n");
#endif

while (!feof(fp)) //文件没有结束,则执行循环
{
ch = fgetc(fp);
printf("%c", ch);
}
printf("\n");

按照行读写文件fgets、fputs

写文件
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int fputs(const char * str, FILE * stream);
//功能:将str所指定的字符串写入到stream指定的文件中,字符串结束符 '\0' 不写入文件。
//参数:
// str:字符串
// stream:文件指针
//返回值:
// 成功:0
// 失败:-1
读文件
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
char * fgets(char * str, int size, FILE * stream);
//功能:从stream指定的文件内读入字符,保存到str所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了size - 1个字符为止,最后会自动加上字符 '\0' 作为字符串结束。
//参数:
// str:字符串
// size:指定最大读取字符串的长度(size - 1)
// stream:文件指针
//返回值:
// 成功:成功读取的字符串
// 读到文件尾或出错: NULL

格式化文件fprintf、fscanf

写文件

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int fprintf(FILE * stream, const char * format, ...);
//功能:根据参数format字符串来转换并格式化数据,然后将结果输出到stream指定的文件中,直到出现字符串结束符 '\0' 为止。
//参数:
// stream:已经打开的文件
// format:字符串格式,用法和printf()一样
//返回值:
// 成功:实际写入文件的字符个数
// 失败:-1

fprintf(fp, "%d %d %d\n", 1, 2, 3);

读文件

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int fscanf(FILE * stream, const char * format, ...);
//功能:从stream指定的文件读取字符串,并根据参数format字符串来转换并格式化数据。
//注意:遇到空格和换行时结束
//参数:
// stream:已经打开的文件
// format:字符串格式,用法和scanf()一样
//返回值:
// 成功:参数数目,成功转换的值的个数
// 失败: - 1

按照块读写文件fread、fwrite

写文件

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
//功能:以数据块的方式给文件写入内容
//参数:
// ptr:准备写入文件数据的地址
// size: size_t 为 unsigned int类型,此参数指定写入文件内容的块数据大小
// nmemb:写入文件的块数,写入文件数据总大小为:size * nmemb
// stream:已经打开的文件指针
//返回值:
// 成功:实际成功写入文件数据的块数目,此值和 nmemb 相等
// 失败:0
读文件
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
//功能:以数据块的方式从文件中读取内容
//参数:
// ptr:存放读取出来数据的内存空间
// size: size_t 为 unsigned int类型,此参数指定读取文件内容的块数据大小(单位是字节)
// nmemb:读取文件的块数,读取文件数据总大小为:size * nmemb
// stream:已经打开的文件指针
//返回值:
// 成功:实际成功读取到内容的块数,如果此值比nmemb小,但大于0,说明读到文件的结尾。
// 失败:0
// 0: 表示读到文件结尾。(feof())

文件的随机读写

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
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
功能:移动文件流(文件光标)的读写位置。
参数:
stream:已经打开的文件指针
offset:根据whence来移动的位移数(偏移量),可以是正数,也可以负数,如果正数,则相对于whence往右移动,如果是负数,则相对于whence往左移动。如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了文件末尾,再次写入时将增大文件尺寸。
whence:其取值如下:
SEEK_SET:从文件开头移动offset个字节
SEEK_CUR:从当前位置移动offset个字节
SEEK_END:从文件末尾移动offset个字节
返回值:
成功:0
失败:-1

#include <stdio.h>
long ftell(FILE *stream);
功能:获取文件流(文件光标)的读写位置。
参数:
stream:已经打开的文件指针
返回值:
成功:当前文件流(文件光标)的读写位置
失败:-1

#include <stdio.h>
void rewind(FILE *stream);
功能:把文件流(文件光标)的读写位置移动到文件开头。
参数:
stream:已经打开的文件指针
返回值:
无返回值

负数移动光标:

image-20201010182023642

Windows和Linux文本文件区别

判断文本文件是Linux格式还是Windows格式:

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
#include<stdio.h>

int main(int argc, char **args)
{
if (argc < 2)
return 0;

FILE *p = fopen(args[1], "rb");
if (!p)
return 0;

char a[1024] = { 0 };
fgets(a, sizeof(a), p);

int len = 0;
while (a[len])
{
if (a[len] == '\n')
{
if (a[len - 1] == '\r')
{
printf("windows file\n");
}
else
{
printf("linux file\n");
}
}
len++;
}

fclose(p);

return 0;
}

获取文件状态

1
2
3
4
5
6
7
8
9
10
#include <sys/types.h>
#include <sys/stat.h>
int stat(const char *path, struct stat *buf);
//功能:获取文件状态信息
//参数:
//path:文件名
//buf:保存文件信息的结构体
//返回值:
//成功:0
//失败-1

stat结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat {
dev_t st_dev; //文件的设备编号
ino_t st_ino; //节点
mode_t st_mode; //文件的类型和存取的权限
nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为1
uid_t st_uid; //用户ID
gid_t st_gid; //组ID
dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号
off_t st_size; //文件字节数(文件大小)
unsigned long st_blksize; //块大小(文件系统的I/O 缓冲区大小)
unsigned long st_blocks; //块数
time_t st_atime; //最后一次访问时间
time_t st_mtime; //最后一次修改时间
time_t st_ctime; //最后一次改变时间(指属性)
};

删除文件、重命名文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int remove(const char *pathname);
功能:删除文件
参数:
pathname:文件名
返回值:
成功:0
失败:-1

#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
功能:把oldpath的文件名改为newpath
参数:
oldpath:旧文件名
newpath:新文件名
返回值:
成功:0
失败: - 1

文件操作注意点

  1. feof()函数有滞后性,不要用feof按照字符方式读文件,会读出EOF,EOF打印出来是“ ”,比空格长的一段空白间距
  2. 如果属性开辟到堆区,不要存指针到文件中,要将指针指向的内容存放在文件中

打印错误:

1
perror("文件打开失败\n");//会在"打印文件打开失败\n"之后,继续打印系统自带的错误显示

函数指针

函数指针

注意:通过函数类型定义的变量是不能够直接执行,因为没有函数体。只能通过类型定义一个函数指针指向某一个具体函数,才能调用。

函数指针没有步长

函数指针和指针函数的区别

  • 函数指针:指向函数的指针
  • 指针函数:返回值为指针的函数

指针函数数组(重点)

返回值和参数均为void的函数指针的长度为10的数组类型定义方式如下:

1
2
3
4
5
6
7
typedef void(*函数指针的数组类型[10])();
//定义该类型变量
函数指针的数组类型 a;
//赋值(testFunc为函数名,即函数指针)
a[0]=testFunc;
//通过a调用testFunc
a[0]();

回调函数(函数指针做函数参数)(难点)

提供一个函数实现对任意类型数组进行排序,排序规则利用选择排序,排序的顺序可以用户自己指定

案例如下:

随机数

srand函数是随机数发生器的初始化函数。修改随机数种子:(修改为当前时间戳)

1
srand((unsigned)time(NULL));

srand设置产生一系列伪随机数发生器的起始点,要想把发生器重新初始化,可用1作seed值。任何其它的值都把发生器匿成一个随机的起始点。rand检索生成的伪随机数。在任何调用srand之前调用rand与以1作为seed调用srand产生相同的序列。

获取随机数:

1
cout<<rand();

参数seed是rand()的种子,用来初始化rand()的起始值。

可以认为rand()在每次被调用的时候,它会查看:

1) 如果用户在此之前调用过srand(seed),给seed指定了一个值,那么它会自动调用srand(seed)一次来初始化它的起始值。

2) 如果用户在此之前没有调用过srand(seed),它会自动调用srand(1)一次。

理解上:srand会产生一个随机数序列,种子决定了其起始点,rand为往后取。同样的种子,rand往后取多次产生的随机序列相同。

静态库的创建

  1. 配置项目属性。因为这是一个静态链接库,所以应在项目属性的“配置属性”下选择“常规”,在其下的配置类型中选择“静态库(.lib)。
  2. 编译生成新的解决方案,在Debug文件夹下会得到mylib.lib (对象文件库),将该.lib文件和相应头文件(头文件是给用户阅读知道库提供了什么函数)给用户,用户就可以使用该库里的函数了。

静态库的使用

3种方法:

  1. 配置项目属性
1
2
3
A、添加工程的头文件目录:工程---属性---配置属性---c/c++---常规---附加包含目录:加上头文件存放目录。
B、添加文件引用的lib静态库路径:工程---属性---配置属性---链接器---常规---附加库目录:加上lib文件存放目录。
C 然后添加工程引用的lib文件名:工程---属性---配置属性---链接器---输入---附加依赖项:加上lib文件名。
  1. 使用编译语句
1
#pragma comment(lib,"./mylib.lib")
  1. 直接添加到工程中(2,3步骤等价)
1
2
就像你添加.h和.c文件一样,把lib文件添加到工程文件列表中去.
切换到"解决方案视图",--->选中要添加lib的工程-->点击右键-->"添加"-->"现有项"-->选择lib文件-->确定.

静态库优缺点

  • 静态库对函数库的链接是放在编译时期完成的,静态库在程序的链接阶段被复制到了程序中,和程序运行的时候没有关系;
  • 程序在运行时与函数库再无瓜葛,移植方便。
  • 浪费空间和资源,所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

p.s.一旦程序中有任何模块更新,整个程序就要重新编译链接、发布给用户,用户要重新安装整个程序。

p.s.静态链接的方式对于计算机内存和磁盘空间浪费非常严重。特别是多进程操作系统下,静态链接极大的浪费了内存空间。在现在的linux系统中,一个普通程序会用到c语言静态库至少在1MB以上,那么如果磁盘中有2000个这样的程序,就要浪费将近2GB的磁盘空间。

动态库的创建

简单地讲,就是不对哪些组成程序的目标程序进行链接,等程序运行的时候才进行链接。也就是说,把整个链接过程推迟到了运行时再进行,这就是动态链接的基本思想。

  1. 创建一个新项目,在已安装的模板中选择“常规”,在右边的类型下选择“空项目”,在名称和解决方案名称中输入mydll。点击确定。
  2. 在解决方案资源管理器的头文件中添加,mydll.h文件,在源文件添加mydll.c文件(即实现文件)。
  3. 在test.h文件中添加如下代码:
1
2
3
//想导出的函数声明必须加,定义可加可不加__declspec(dllexport)
__declspec(dllexport) int myminus(int a, int b);
//字面翻译declare special声明特殊(dllexport动态链接库导出)
  1. 配置项目属性。因为这是一个动态链接库,所以应在项目属性的“配置属性”下选择“常规”,在其下的配置类型中选择“动态库(.dll)。
  2. 编译生成新的解决方案,在Debug文件夹下会得到mydll.dll (对象文件库),将该.dll文件、.lib文件和相应头文件给用户,用户就可以使用该库里的函数了。

导出方式有以下两种:

  1. *.def 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//*.def 文件必须至少包含下列模块定义语句:

//文件中的第一个语句必须是 LIBRARY 语句。此语句将 .def 文件标识为属于 DLL。LIBRARY 语句的后面是 DLL 的名称。链接器将此名称放到 DLL 的导入库中。

//EXPORTS 语句列出名称,可能的话还会列出 DLL 导出函数的序号值。通过在函数名的后面加上 @ 符和一个数字,给函数分配序号值。当指定序号值时,序号值的范围必须是从 1 到 N,其中 N 是 DLL 导出函数的个数。

//注释语句,在语句前面加分号 “;” 。

//例如:
//;DLLTest.def : Declares the module parameters for the DLL.
LIBRARY "DLLTest"
EXPORTS
add @1
fun @2

如果是VS平台,必须要在连接器中添加.def文件

img

  1. __declspec(dllexport) 关键字

重要理解:

  1. 动态链接库中定义有两种函数:**导出函数(export function)和内部函数(internal function)**。 导出函数可以被其它模块调用,内部函数在定义它们的DLL程序内部使用。
  2. 动态库的lib文件和静态库的lib文件的区别?

在使用动态库的时候,往往提供两个文件:一个引入库(.lib)文件(也称“导入库文件”)和一个DLL(.dll)文件。虽然引入库的后缀名也是“lib”,但是,动态库的引入库文件和静态库文件有着本质的区别,对一个DLL文件来说,其引入库文件(.lib)包含该DLL导出的函数和变量的符号名,而.dll文件包含该DLL实际的函数和数据。在使用动态库的情况下,在编译链接可执行文件时,只需要链接该DLL的引入库文件,该DLL中的函数代码和数据并不复制到可执行文件,直到可执行程序运行时,才去加载所需的DLL,将该DLL映射到进程的地址空间中,然后访问DLL中导出的函数。

动态库的使用

方法一:隐式调用

创建主程序TestDll,将mydll.h、mydll.dll和mydll.lib复制到源代码目录下。

(P.S:头文件Func.h并不是必需的,只是C++中使用外部函数时,需要先进行声明)

在程序中指定链接引用链接库 : #pragma comment(lib,”./mydll.lib”)

#pragma comment(lib,”./mydll.lib”)和直接把mydll.lib添加进项目等价

方法二:显式调用

1
2
3
4
5
HANDLE hDll; //声明一个dll实例文件句柄
hDll = LoadLibrary("mydll.dll"); //导入动态链接库
MYFUNC minus_test; //创建函数指针
//获取导入函数的函数指针
minus_test = (MYFUNC)GetProcAddress(hDll, "myminus");

可变参数列表

头文件:<stdarg.h>

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdarg.h>

void printNumbers(int count, ...) {
va_list args;
va_start(args, count);
for (int i = 0; i < count; i++) {
int num = va_arg(args, int);
printf("%d ", num);
}
va_end(args);
}

int main() {
printNumbers(3, 1, 2, 3);
return 0;
}
//打印如下
1
2
3

面向接口编程

以函数指针为接口,双方分割来开发

技术层次

img

里奇最早的C语言是K&R C

1989第一套标准是:ANSI C/C89标准(C语言的第一个官方标准)

C90标准,C94,C95(与C89完全等同)

C99标准(C语言的第二个官方标准)

  • 增加了新关键字 restrict,inline,_Complex,_Imaginary,_Bool
  • 支持 long long,long double _Complex,float _Complex 这样的类型
  • 支持了不定长的数组。数组的长度就可以用变量了。声明类型的时候呢,就用 int a[*] 这样的写法。不过考虑到效率和实现,这玩意并不是一个新类型。

编程提示

  • 源代码的可读性几乎总是比程序的运行时效率更为重要
  • 只要有可能,函数的指针形参都应该声明为const
  • 在多维数组的初始值列表中使用完整的多层花括号提高可读性

常见报错:

fatal error LNK1120: 1 个无法解析的外部命令

重点,.c后缀文件和.cpp文件后缀混着用,会导致此报错

待理解部分

浮点数转换为定点数的汇编表达方式

通过setwindowshookex hook onpaint的尾部消息

小知识点

如果调用的是另外一个DLL的导出函数,与正常调用没有差别。
如果调用的是被注入的EXE中的内部函数,则需要知道函数的地址。

可变参数详解

C语言新标准

C99引入

C99 标准引入了许多特性,包括内联函数(inline functions)、可变长度的数组、灵活的数组成员(用于结构体)、复合字面量、指定成员的初始化器、对IEEE754浮点数的改进、支持不定参数个数的宏定义,在数据类型上还增加了 long long int 以及复数类型。

聚合类型

断言

windows下开发环境部署

MSYS2

MSYS2通过集成化工具链、高效的包管理、多环境隔离,彻底解决了Windows下C/C++环境配置繁琐的问题。无论是开发原生应用、移植跨平台项目,还是管理第三方库,其便捷性远超传统方案(如MinGW或Cygwin).对于现代C/C++开发者,MSYS2已成为Windows平台的首选开发环境

完整的类Unix开发环境

  • 提供bash/grep/make等Unix命令行工具,弥补Windows原生终端的不足。
  • 支持POSIX兼容层,可直接运行Linux脚本或工具

强大的包管理(Pacman)

  • 集成Arch Linux的pacman包管理器,支持一键安装、更新和卸载软件包

  • 自动解决依赖关系,例如安装GCC编译器只需一条命令:

    1
    pacman -S mingw-w64-x86_64-toolchain  # 安装64位GCC工具链[9,10](@ref)

多环境支持

提供独立子系统环境,灵活适配不同开发需求:

  • MSYS:POSIX兼容环境,运行Unix工具。
  • MINGW64:编译原生64位Windows程序。
  • UCRT64:使用Universal C Runtime的工具链

其对C/C++环境安装的关键帮助如下:

  1. 无需手动配置,通过pacman直接安装:
    • 编译器:GCC、Clang(支持C++11/14/17标准)
    • 构建工具:CMake、Make、Meson
    • 调试器:GDB
  2. 提供3200+预编译开源库(如OpenSSL、FFmpeg),避免从源码编译的复杂性
  3. 跨平台开发支持
    • 在Windows上模拟Unix环境,方便编译Linux/macOS兼容的项目
    • 支持生成原生Windows程序(如.exe文件),性能优于Cygwin
  4. 环境隔离与兼容性
    • 不同环境(如MINGW32/MINGW64)独立存在,避免库冲突
    • 路径自动映射(如/c/对应C:\),解决Windows路径格式问题

安装方式

MSYS2下载网址

官网下载安装包(64位系统选msys2-x86_64.exe),安装路径避免空格(如C:\msys64

首次启动后更新系统

1
2
pacman -Syu  # 更新核心包
pacman -Su # 升级其余组件

安装开发环境

1
2
3
4
5
pacman -S mingw-w64-ucrt-x86_64-toolchain  # 安装 GCC 工具链

pacman -S mingw-w64-ucrt-x86_64-cmake

pacman -S mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-make

注意,安装好以后是使用mingw32-make 来替代 make 命令,可以创建链接ln -s /ucrt64/bin/mingw32-make.exe /ucrt64/bin/make.exe

使用方式

MSYS2 自动将 Windows 驱动器映射为根目录下的虚拟路径,格式为:**/盘符/目录路径(注意:路径分隔符为斜杠 /,且不区分大小写**)

1
2
3
4
5
# 进入 C 盘 Users 目录
cd /c/Users

# 进入 D 盘 Project 文件夹
cd /d/Project/src

库盘点

ncurses库

mac上自带ncurses库

以下是一些常用的ncurses库函数和用法的简要说明

初始化和清理

  • initscr():初始化ncurses库,启动终端模式。
  • endwin():清理并退出ncurses库,恢复终端原始设置。

屏幕输出

  • printw(const char *format, ...):在当前光标位置打印格式化的字符串。
  • mvprintw(int y, int x, const char *format, ...):在指定位置打印格式化的字符串。
  • refresh():刷新屏幕,将输出显示在终端上。

键盘输入

  • getch():获取用户按下的键盘字符。
  • mvgetch(int y, int x):在指定位置获取用户按下的键盘字符。

光标控制

  • move(int y, int x):将光标移动到指定位置。
  • getyx(WINDOW *win, int y, int x):获取当前光标位置。

窗口和面板

  • WINDOW *newwin(int nlines, int ncols, int begin_y, int begin_x):创建一个新的窗口。
  • delwin(WINDOW *win):删除窗口。
  • wprintw(WINDOW *win, const char *format, ...):在窗口中打印格式化的字符串。
  • wrefresh(WINDOW *win):刷新窗口,将输出显示在终端上。
  • PANEL *new_panel(WINDOW *win):创建一个新的面板。
  • del_panel(PANEL *panel):删除面板。

颜色和属性

  • start_color():启用颜色功能。
  • init_pair(short pair, short foreground, short background):初始化颜色对。
  • attron(int attrs):启用指定的属性。
  • attroff(int attrs):禁用指定的属性。