第一节 初始C语言
一 什么是C语言
1 计算机语言
计算机语言(包括:C、C++、Java、Python等) 是用于实现人与计算机交流的语言,而汉语、英语、日语等,适用于实现人与人之间交流的语言。
C语言,是面向过程的计算机编程语言,广泛应用于底层开发,虽然也可用于上层软件开发,但存在一些缺陷,比如:内存安全问题、编写复杂度高、缺乏内置类型、代码可读性差、可移植性较差等问题,因此,在现代软件开发中,C语言已经逐渐被更加安全、高效、易用的编程语言所替代,如Java、Python、C#等。
![](https://i-blog.csdnimg.cn/blog_migrate/a717d2c1f11a5e4fbe88c43d0d9bbcdd.png)
2 ANSIC国际标准
为了避免各个开发厂商用的C语言语法产生差异而制定的(用来保证不同企业之间可以相互使用)
3 计算机语言的发展
机器语言(二进制由0/1组成)--->汇编语言---------------------->B语言---------->C语言
低级语言-------------------------------------------------------------------------------------->高级语言
4 实现第一个C语言程序
(1)新建项目
(2)点击空项目,输入项目名称,点击创建
(3)在源文件处,右键,添加新建项
4.选择C++文件,书写名称,并将后缀改为.c文件,点击添加,方可输入代码
第二节 数据类型
一 计算机单位
bit(比特):一个二进制位(0或1)
byte(字节):1byte = 8bit
KB(千字节):1KB = 1024byte
GB(吉字节):1GB = 1024KB
TB(太字节):1TB = 1024GB
PB(拍字节):1PB = 1024TB
二 内置类型
不同的地方使用恰当的类型可以达到节省空间的效果(选择的类型决定了为变量开辟空间的字节数)
1 整型家族
(1)通过signed/unsigned来控制整型有无符号,除去char类型默认是不确定,由编译器来决定外,其余类型均默认为有符号型
char:字符类型
int:整型
short:短整型
long:长整型
long long:更长的整型
(2)有无符号数的取值范围
1.char类型的区别
(无符号数:255+1 == 0)
(有符号数:-1+1 == 0)
2.int类型的区别
结果为:4294967286和-10
(3)Limits.h,定义了整型数值的取值范围的相关信息(通过宏,定义了不同类型的最大最小值)
(4)无符号数最好还是少用,容易出现死循环的风险,因为0-1等于最大值(无负数),可以想象为一个圆形
2 浮点型家族
(1)浮点数没有原反补的概念,且浮点数之间比较不可用 ==来比较 ((BV1og411P7Rf)视频中有讲解)
float:单精度浮点型(4字节)
double:双精度浮点型(8字节)
long double(8/12/16字节,c语言未明确声明)
(2) Float.h:定义了浮点数的取值范围的相关信息(宏定义了不同类型的最大最小值)
(3) 浮点型在内存中的存储方式
![](https://i-blog.csdnimg.cn/blog_migrate/02fef13ad815d0e1806ae7ed933d3f3d.png)
(4)浮点型在32位(4字节)存储形式:1+8+23
64位(8字节)存储形式:1+11+52
(5) 演示示例
3 构造类型
数组: int arr[10],此时arr的类型:int [10]
int arr[ ] = {0,1,2};此时arr的类型:int [3]
结构体类型:struct
枚举类型:enum
联合类型:union
4 指针类型
所有指针类型的大小均为4字节长度
Int * pa;
char * pr;
float * pf;
void * pv;
5 空类型/无类型
void,主要应用于参数、指针、返回值类型
第三节 变量和常量
一 变量
用来描述变化的数据
1 定义变量的方式
short age = 15;
float weight = 55.5f;(55.5默认为double类型,加上f使其类型变为float)
char ch = 'c';(单引号--字符;双引号--字符串)
2 局部变量和全局变量
局部变量 | 全局变量 | |
作用域 | 局部变量只在其定义的作用域内可见,出了这个作用域就无法访问 | 全局变量的作用域跨越整个程序,可以在程序的任何地方进行访问 |
生命周期 | 局部变量的生命周期是从其定义处开始,到其所在作用域结束时结束 | 全局变量的生命周期与程序的运行时间相同,从程序启动到结束 |
可见性 | 局部变量的可见性也是有限制的,只能在其作用域内访问和修改 | 全局变量对整个程序都是可见的,可以在任何函数内部进行访问和修改 |
默认初始化 | 不会进行默认初始化,随机值 | 数值类型的全局变量会被初始化为0 指针类型的全局变量会被初始化为nullptr 类对象的全局变量会调用默认构造函数进行初始化 |
(1)同一项目的,其他文件中的全局变量不可直接使用,要加声明外部符号extern
(2)当全局变量和局部变量中有相同名称的变量时,若都可以使用的话,保持局部优先原则
3 变量的命名规则
二 常量
用来描述不变的数据
1 常量的类型
字面常量:可以直接写出来的,其中"abcd"为字符串常量
const修饰的常变量,只是具有常属性,但本质上还是变量
#define定义的标识符常量,命名时都为大写字母(无等于号;无分号)
#define STR “love”---也可以是字符串
#define ADD(x,y) (x+y)---定义宏---不要吝啬括号,其实最好写为((x)+(y))
枚举常量:可一一列举的(一种自定义类型的方法)
其中若使用printf("%d",red);//则此时打印结果为0
使用示例:enum Color ch = red;
第四节 字符
一 字符串
由双引号引起来的一串字符,其中\0为字符串结束的标志,若在字符串中间添加\0,则会提前结束
二 转义字符
转义字符本质上为字符,打印时使用%c
(1)一些重要的转义字符
\\:用来表示一个\字符
\':用来表示一个'字符
\":用来表示一个"字符
\t:水平制表符,等价于tab键位的使用
\n:换行符
\ddd:其中d为八进制数字,最多为三位,例如\130代表为大写X
\xdd:x为前缀必须要有,dd为16进制数字,无上限要求,例如\x42代表为大写B
(2)需要记忆的ASCII码
\0:0
0:48
A:65
a:97
(3)并非所有字符均可打印,其中包含不可打印字符
第五节 语句
包含表达式语句、函数调用语句、控制语句、复合语句以及空语句(其中{}称之为代码块)
一 控制语句
分支语句(选择语句)
循环执行语句(循环语句)
转向语句
二 选择语句
1 if else语句
(1)不加大括号的情况下,只能控制一条语句
(2)悬空else的问题:else总是和相距最近的if匹配
2 switch语句
(1)表达式中的值必须为整型
(2)可以嵌套使用
(3)常用于多分支情况
三 循环语句
1 while语句
while(1)为死循环
2 do...while语句
不适用于一次都不执行的场景(do...while循环体必执行一次)
3 for语句
(1)不要在for循环体中修改循环变量的值,否则for循环可能会失控
(2)建议采用前闭后开区间写法(for(int i = 0;i < 10 ; i++))
(3)continue在for循环语句中,可能因为i++在上面,不会跳过循环变量的变更
(4)三部分均可省略
for(;;)
此时由于判断部分为空,导致恒为真,等价于while(1)---死循环
(5)循环变量可能不止一个for(int i = 0 , int j = 0 ; i < 5 && j > 3 ; i++ , ++j)
4 break和continue
(1)break是直接跳出循环,但是若是多层嵌套循环,只能跳出一层循环
(2)continue是跳过continue之后的步骤,回到循环的判断部分
四 goto语句
(1)goto语句容易出现问题,通常不使用
(2)在跳出多层循环时使用goto,虽然也可使用多次break,但是会比较麻烦
第六节 函数
函数的作用:简化代码,反复使用(且功能尽量单一)
一 库函数
必须包含对应函数的头文件
1 I/O函数
1.1 标准输入函数scanf
scanf()函数用于从标准输入设备读取格式化的数据
scanf("%d%d",&a,&b);,若输入成功一个则返回1,输入成功两个则返回2输入失败返回EOF(-1)(EOF == 文件结束标志 == ctrl + z)
scanf(“%d/%d”,&a,&b);此时在使用的时候,输入时中间必须使用(/)
1.2 标准输出函数printf
printf()函数用于向标准输出设备打印格式化的字符串
printf ("%d", num);,成功打印几个字符,返回值就是几
%c | %d | %u | %hd | %ld | %lld |
字符型 | 整型形式打印 | 无符号整型 | 短整型 | 长整型 | 更长的整型 |
%f | %lf | %x | %02x | %#x | %p |
单精度浮点型 | 双精度浮点型 | 16进制形式打印 | 按照16进制形式打印,对齐数为2,不足之处补0 | 按照16进制形式打印,但是会在开始加上0x | 按照地址形式打印(16进制)(且前面的0也都会打印出来) |
1.3 标准输入函数getchar()
getchar()函数用于从标准输入设备读取单个字符
(1)ctrl+z会让getchar()和scanf返回一个EOF
(2)使用scanf("%s",str);只会读取 空格 之前的字符
(3)在需要消耗掉换行的时候使用getchar();
1.4 标准输出函数putchar()
putchar()函数用于向标准输出设备打印单个字符
2 字符串操作函数
2.1 求字符串长度的函数strlen(str)
从起始地址,向后查找一直到\0结束
(1)返回值类型为:unsigned int
(2)头文件:string.h
(3)模拟实现(3种方式)
2.2.1 字符串拷贝函数strcpy(str1, str2)
拷贝str2的内容到str1中,拷贝到str2的\0位置处终止
(1)会将str2中的\0拷贝到str1中
(2)str1空间大小一定要够用(但也能运行,因为该函数会忽略这一点)
(3)返回值类型:char*(作用:为了实现链式访问printf(“%s”, strcpy(arr1,arr2)));
(4)模拟实现
2.2.2 字符串拷贝函数strncpy(arr1, arr2, n)
拷贝str2中n个字节长度的内容到str1中
(1)会相对安全一些,有了字节大小的限制
(2)拷贝大小超过str2字符串长度的时候,以\0代替
2.3.1 字符串追加函数strcat(arr1, arr2)
找到原字符str1中第一个\0,并从此处开始追加,追加至str2的\0处
(1)原字符串空间要足够大,可以容纳追加的
(2)模拟实现
2.3.2 字符串追加函数strncat(str1, str2, n)
找到原字符str1中第一个\0,并从此处开始追加,追加str2中n个字节长度的内容
(1)追加结束后自动追加一个\0
(2)n大于str2长度时,只追加str2的所有内容,并在最后追加\0
(3)可以自己追加自己
2.4.1 字符串比较函数strcmp函数(str1, str2)
根据ASCII对对应位置处进行比较,相同则后移,直至不同或到\0处
(1)规定的是<0 / >0 / ==0(vs中返回的是1/0/-1)
(2)模拟实现
2.4.2 字符串比较函数strncmp(str1, str2, n)
只比较前n个字节看是否相同
(1)”abcd” 与 “aaaa”的比较,比较的是两者的地址(前者a和后者a的地址)
2.5 查找子串的函数strstr(str1, str2)
看arr2是否是arr1的字串,若找到了返回str2第一次出现时的地址,找不到返回nullptr
(1)模拟实现
2.6 分割字符串的函数strtok(arr1,arr2)
(1)函数的执行过程如下:
①在第一次调用时,传入待分割的字符串作为第一个参数,并且返回被分割的第一个子字符串的指针。
②后续的调用中,第一个参数传入NULL,函数会继续返回被分割的下一个子字符串的指针,直到没有更多的子字符串为止。
(2)在每次调用strtok函数之后,原字符串会被修改,被分割的子字符串处会被'\0'替代
2.7 strerror函数
将errno(全局变量,表示最近一次发生的错误码)转换为相应的错误信息字符串
以上代码会尝试打开一个名为“file.txt”的文件。如果打开失败,就输出相应的错误信息字符串,否则就对文件进行操作并关闭它
(1)与perror作比较:
perror会自动打印,且不能不打印,自动在字符串后加上冒号(输出结果:错误原因:+错误信息)
strerror可以自行选择打印或者不打印
3 字符操作函数
3.1 字符分类函数
isalpha() | 判断字符是否为字母 |
isdigit() | 判断字符是否为数字 |
isalnum() | 判断字符是否为字母或数字 |
isxdigit() | 判断字符是否是十六进制数字(0-9、A-F以及a-f) |
isspace() | 判断字符是否为空格、制表符或换行符 |
isupper() | 判断字符是否为大写字母 |
islower() | 判断字符是否为小写字母 |
ispunct() | 判断字符是否为标点符号 |
isprint() | 判断字符是否可打印(是否为可见字符),包含空格 |
isgraph() | 判断字符是否是可打印字符,并且不是空格 |
iscntrl() | 判断字符是否是控制字符 |
3.2 字符转换函数
(1)大写转小写:int tolower(int c);
(2)小写转大写 :int toupper(int c);
4 内存操作函数
4.1 拷贝函数memcpy(str1, str2, n)
拷贝str2中n个字节长度的内容到str1中
(1)可以拷贝任意类型的数据
(2)不可以实现重叠内存拷贝memcpy(str1+2, str1, 20);//1,2,3,4,5,6,7,8,9
(会产生的结果:1,2,1,2,1,2,1,8,9)
但实际很多编译器下memcpy也可以实现重叠拷贝(memmove >= memcpy)
4.2 拷贝函数memmove(str1, str2, n)
(1)可以实现重叠内存拷贝memmove(str1+2, str1, 20);//1,2,3,4,5,6,7,8,9
(产生的结果:1,2,1,2,3,4,5,8,9)
(2)模拟实现:(str+m, str, n)从后向前拷贝
(str, str+m, n)从前向后拷贝
4.3 比较函数memcmp(str1, str2, n)
①比较n个字节,从两个地址依次向后比较,以字节为单位,直至不同处或比较结束
②返回值为:<0 / >0 / 0
4.4 内存设置函数memset(str, 0 ,20)
从str地址处开始,每个字节都设置为0,共设置20字节
(1)第二个参数应该是unsigned char
类型(范围:0-255)
5 时间/日期函数
6 数学函数
7 其他函数
二 自定义函数
三 函数的应用
1 参数
(1)形参只是实参的一份临时拷贝,形参的修改不影响实参
(2)实参(变量、常量、表达式(a+b)、函数),不论是哪种都必须为确定的值
(3)形参:在函数为被调用时,形参不占用内存,形参在函数调用时创建,调用结束时销毁
2 函数的调用
(1)传值调用(不可更改实参数值)
(2)传址调用(可更改实参数值)
3 函数的嵌套使用/链式访问
(1)嵌套使用:例如,在main函数中使用printf函数
(2)链式访问:printf(“%d” , add(a,b) );
4 函数声明
int ADD (int num1, int num2);
int ADD (int, int);
(1)函数要先声明后使用
(2)在实际应用中,一般在头文件中创建 .h文件,用来存放函数声明
5 函数定义
函数具体实现方式
(1)一般在源文件中创建 .c 文件,存放函数定义(实际应用中)
(2)在属性-常规-配置类型-静态库中,可以隐藏 .c 文件
确保别人不会知晓函数具体实现(会生成 .lib 文件,替换掉 .c 文件)
(3)在分享给别人时,只需分享给 .h 和 .lib 文件即可
使用时需(# pragma comment ( lib , “ xxx.lib”))和(#include “ xxx.h ”)
(4)(2)和(3)增加了保密性
6 递归函数
把大型问题转化为与原问题相同的小规模问题来求解(大事化小),从而只需用少量的程序就可描述出所需要的多次重复计算,大大减少了代码量
(1)两个必要条件
存在限制条件
每次递归后,越来越接近于这个限制条件
(2)每次递归都是完整的独立的函数调用,不会因为其中某一次的return而结束
(3)每次函数的调用,都会在栈上开辟空间
第七节 数组
一组相同类型元素的集合
一 一维数组
1 初始化
(1)整型数组初始化
(2)字符数组初始化
2 数组名和指针
(1)数组名只在以下两种情况代表整个数组,其余情况均为首元素地址(不论几维数组)
计算整个数组的长度:sizeof(arr);
取地址符后直接跟数组名:&arr;
(2)arr+1 == &arr[0]+1 == &arr[1]
(3)&arr+1,向后跳过整个数组大小个字节
(4)大小计算
数组的大小:sizeof(arr) / sizeof(arr[0])
区分:strlen(),遇到\0停止
3 其余注意事项
(1)数组长度不可为变量,只能为常量
c99标准中增加了变长数组的概念,此时数组长度可以为变量,但并非所有编译器均可,且变长数组不可初始化
(2)一维数组在内存中是连续存放的,地址由低到高存放
(3)输入输出要结合循环语句进行(for/while/do..while)
二 二维数组
可以理解为 存放元素为一维数组 的一维数组,例如arr[2][3],可以看作为存放2个arr[3]的一维数组
1 初始化
2 数组名和指针
(1)arr代表首行地址,arr == &arr[0]
(2)&arr[0]+1 == &arr[1],第二行的地址
(2)大小计算
数组大小:sizeof(arr) / sizeof(arr[0][0])
行数计算:sizeof(arr) / sizeof(arr[0])
列数计算:sizeof(arr[0]) / sizeof(arr[0][0])
第一行大小:sizeof(arr[0])
3 其余注意事项
(1)二维数组在内存中是连续存放的,地址由低到高存放
三 数组的应用
1 作为函数参数的时候
(1)一维数组
非指针形式:void fun(int arr[])
一维数组(数组长度可以省略,长度错误也没错(不推荐))
指针形式:void fun(int* arr)
(2)二维数组
非指针形式:void test(int arr[][5]);
二维数组(数组行数可以省略)
指针形式:void test(int (*parr)[5]);
第八节 操作符
计算时都使用的是补码
一 算术操作符
(1)加法:+
(2)减法:-
(3)乘法:*
(4)除法:/
(5)取余:%(两端都必须为整数)
二 移位操作符
必须为整数(负整数结果不确定,不推荐使用)
(1)左移操作符:<<(左边(符号位)丢弃,右边补0)
(2)右移操作符:>>(右边丢弃,左边补上)
算数右移:左边补符号位
逻辑右移:左边补0(与编译器相关)
三 位操作符
(1)按位与:&(有0为0,同1为1)
(n & (n - 1),每使用一次就会减少一个二进制中的1)
(2)按位或:|(有1为1,同0为0)
(3)按位异或:^(相同为0,不同为1)
四 赋值操作符
(1)=
(2)+=
(3)-=
(4)/=
(5)&=
(6)^=
(7)|=
(8)>>=
(9)<<=
五 单目操作符
(1)逻辑反操作符:!
非0为真,!5 == 0,!0 == 1
(2)取地址操作符:&
(3)sizeof操作符:sizeof(a) == sizeof a == sizeof(int)
因为可以写为siezof a,所以证明sizeof非函数,函数的()不能省去
(4)按位取反:~
符号位也会被按位取反
while( ~scanf( “%d” , &num)) { },当返回EOF(-1)的时候,再按位取反等于0,所以此时会终止while循环
(5)++
(6)--
不要过于困难化,例如b=(++a)+(++a)+(++a),导致在不同编译器结果不同,所以是错误代码
(7)强制类型转换
(8)解引用操作符:*
六 关系操作符
(1)>
(2)>=
(3)<
(4)<=
(5)!=
(6)==
七 逻辑操作符
(1)&& 逻辑与(一假为假)
(2)|| 逻辑或(一真为真)
八 条件操作符
exp1 ? exp2 : exp3(若exp1为真,则计算exp2,为假则计算exp3)
九 逗号表达式
从左向右依次计算,但整个表达式只保留最后一个的结果
(b=(a=3+1,c=1+1,a-c)),则a=4,c=2,d=2,但是b的值=(a-c),只看最后一个
十 下标引用、函数调用和结构成员
(1)下标引用[]
2个操作数:数组名+数组个数
(2)函数调用()
至少一个操作数:函数名+未知个数参数 ,Add()
(3)结构成员访问操作符
.
->
十一 原反补
(1)原码
(2)反码:符号位不变,其他位按位取反
(3)补码:反码+1(可能影响符号位,先不管)
补码->原码:
①补码-1再按位取反
②补码按位取反再+1(补码的补码=原码)
(4)正整数:原码 == 反码 == 补码
(5)最高位:0表示正数,1表示负数
(6)原码反码补码存在的意义
整数在内存中存储的形式:补码的二进制形式
整数打印出来的形式:原码的二进制形式
存放补码的原因:CPU中只有加法计算器
(7)原码计算1-1 == -2
补码计算1-1 == 0(最高位放不下,直接舍弃,最终为0)
十二 隐式类型转换(整型提升)
(1)长度小于整型(int型)的,在进行计算前,先提升为int再去计算
(2)有符号数:高位按符号数提升
(3)无符号数:高位补0
十三 算术转换
(1)当 >= int时,计算时按照最长的类型进行计算
long double > double > float > unsigned long int > long int > unsined int > int
十四 操作符的属性
(1)优先级
(2)结合性
(3)是否控制求值顺序(&& || 逗号 三目操作符)
(4)注意事项
①并非依靠三者可以确定最终计算顺序(根据编译器而定),若有结果不同,则表达式存在问题
②少写过于复杂的++/--运算
第九节 关键字
关键字是C语言中本身存在的,不可以创造关键字
一 关键字
auto
char / int / short / long / double / float / signed /unsigned
if / else / do / while / for / switch / case / break / continue / default
const / static / typedef / sizeof / union / enum / struct
goto / extern / return / void
volatile / register
二 注意事项
include/difine:都不是关键字,都是预处理指令
第十节 静态变量
一 修饰局部变量
(1)改变了存储类型,本身在栈区,修饰后存储在静态区
(2)生命周期延长,与程序生命周期相同
(3)作用域不会改变,只在原本作用域生效,作用于外使用要使用指针
(4)static修饰的变量可以更改
(5)不初始化的话默认为0
while(1)
{
Static int num = 0;
num++;
printf("%d",num);
}
//此时打印结果为:1,2,3...,因为只执行一次
二 修饰全局变量
全局变量本身有外部连接属性,所以可以使用extern
static修饰后改为内部连接属性,只能在本.c文件中使用(只想自己使用时加上static)
三 修饰函数
同全局变量,本身具有外部连接属性,可用extern
static修饰后改为内部连接属性,只能在本.c文件中使用static int ADD (int a,int b){ }
第十一节 指针
指针 = 地址 = 内存编号
一 指针简介
(1)每个内存单元为1字节(8bit)
(2)指针变量的大小
任何类型的指针大小均是4字节(32位机器是4字节;64位机器是8字节)
(3)指针类型
决定了解引用时的访问权限(int以4个字节为单位进行访问)
决定了指针+1或-1所走的步伐大小( +n = n * sizeof( int ) )
注:整数和浮点数在内存中存储方式不同,虽然有的都是占用4个字节,但解引用的话结果不可知
(4)指针运算
指针 ± 整数:根据指针类型(向前/向后)跳转相应的字节
指针的关系运算:&arr[0] < &arr[1]
指针-指针:(必须是同一块空间的指针)&arr1[5] - &arr1[2],得到中间的数据个数3
错误示例:&arr1[5] - &arr2[0],arr1
二 一二级指针的使用
(1)int * pa = &a;
*表示:是指针变量
int表示:pa指向的对象是int类型
(2)*pa = 20;
*表示:解引用操作
*pa = 20;表示:将a 的值改为20
(3)二级指针
(4)字符指针注意事项
*p1 = ‘w’; //会出错,因为不可修改
arr中的 abcdef 不是常量字符串,相当于创建好空间来存放字符串abcdef
p1和p2,是指针,指向该常量字符串abcdef
三 指针数组
存放指针的数组
整型数组:int arr1[5] = {1 , 2, 3 , 4, 5 }; //类型:int
指针数组:int* arr2[3] = { &a , &b , &c}; //类型:int*
(1)传参形式:
非指针形式:同数组,void test(int *arr[]),数组长度可以不写
指针形式:void test(int** arr);
四 数组指针
指向数组的指针
(1)int (*p)[10] = &arr; //指向:int[10]([10]不可省略)的指针
(2)二维数组:int (*p)[3][5] = &arr;
(3)*p == arr ==&arr[0](首元素地址)
(4)数组指针不常用于一维数组,使用起来太过冗余
五 函数指针
(1)示例演示,假设有一个Add函数(fun和&fun不仅数值相同,含义也相同,不同于数组名arr)
①int (*p) (int , int) = Add;
②int (*p) (int , int) = &Add;
(2)函数指针类型的重命名:
typedef int (*pA)(int , int); //原类型为:int(*)(int , int)
使用 typedef 定义函数指针类型可以简化代码并提高可读性
六 函数指针数组
(1)int (*arr[4])(int , int);
数组名:arr
数组大小:4
数组元素类型:int(*)(int , int)
(2)应用示例
七 指向函数指针数组的指针
八 回调函数
把函数当作参数使用
九 野指针
(1)出现的原因
指针未初始化(int *p;且在之后访问指针p)
指针越界访问(数组)
指针指向空间被释放
(2)如何避免
指针初始化(int * p = NULL;)或(int * p = &a;)
小心越界访问
指向空间释放后立即置为NULL
避免返回局部变量地址(局部变量出作用域后会被释放)
使用前检测指针的有效性( if(p != NULL){ } )
第十二节 结构体
用来描述复杂类型
一 结构体简介
(1)结构体演示实例:
(2)结构体中的结构体指针类型
(3)打印(两种方式)
①printf(“%s,%d,%s,%lf”,LiMing.name,LiMing.age,LiMing.sex,LiMing.score);
②struct Stu * ps = &LiMing;
printf(“%s,%d,%s,%lf”,ps->name,ps->age,ps->sex,ps->score)
(4)输入
scanf(“%s, %d, %s, %lf”, LiMing.name, &(LiMing.age), LiMing.sex,&(LiMing.score);
scanf(“%s, %s, %lf”, LiMing.name, LiMing.sex, &(LiMing.score)); //跳过年龄
(5)修改
s1.age = 20;//可以直接修改
字符串的内容不可直接修改,需要通过strcpy或者scanf修改
二 匿名结构体
只能使用一次,创建的时候有几个变量,就能用几个,在之后不可再进行创建
三 结构体的自引用
主要应用于链表
四 内存对齐
1 概念
(1)首个成员放在结构体偏移量为0的地址处
(2)其他成员从对齐数的整数倍地址处存放(对齐数:默认对齐数和成员大小的较小者)
(3)结构体大小必须为所有成员对齐数最大者的整数倍
(4)嵌套结构体的对齐数 = (内部所有成员的最大对齐数)
(5)拥有嵌套结构体的结构体的最终大小:所有对齐数的整数倍(要考虑到嵌套结构体对 齐数)
2 内存对齐的原因
(1)平台原因:并非所有的平台都可以在任意地址读取任意类型的数据
(2)性能原因:未对齐处理的需要两次内存访问,对齐的只需一次(用空间换时间)
3 节省空间的办法
尽量使占用空间小的放在一起
4 修改默认对齐数
用于默认对齐数不适合时使用,#pragma pack()
5 计算结构体成员相对于结构体起始地址的偏移量
offset(struct S1, num);
五 结构体传参问题
(1)传参时要进行传址传参,节省空间
(2)传值传参:一大片空间,空间浪费大;安全性相同
(3)传址传参:4/8字节空间,节省空间;+const后安全性相同
原因:①函数传参时,参数进行压栈(进栈),若结构体过大,压栈致使系统开销过大,性能低下
②传参结束后调用函数,每次函数调用要在栈区开辟一块内存空间
六 柔性数组
结构体中最后一个元素允许是为止大小的数组,该数组称之为柔性数组(任意类型都可以,结构体类型的数组也可以)
(1)特点
(2)柔性数组与指针方式的对比
柔性数组 | 指针 |
开辟一个动态内存空间 (方便内存释放) | 开辟两个动态内存空间 |
开辟空间时连续的 (读取数据的时候,操作系统会预读取周围数据,以方便快速访问) (连续空间更有利于节约空间,多次申请开辟空间,会产生更多的内存碎片) |
扩展阅读:C语言结构体里的数组和指针(酷客)
第十三节 位段
一 使用方式
(1)类似于结构体
(2)采用不对齐的方式存储
(3)成员只能为:signed int / unsigned int / char类型
(4)成员后面:必须有冒号+数字
只能在可以满足需求的时候适当使用:此时的a只占用2bit的空间,只能存放00/01/10/11四种数据
二 内存分配
(1)signed int / unsigned int会以4字节空间来开辟
(2)char会以1字节空间来开辟
(3)不可跨平台,在可移植的应用程序上避免使用位段
三 跨平台需要考虑的问题
(1)int默认有无符号的问题
(2)int在不同平台占用空间的大小2/4/8
(3)成员在使用空间的时候,从左到右/从右向左的
(4)是否舍弃不足以存放的空间
四 位段的应用
每行都是一个int型(32bit),且空间变小后,网络速度也快
第十四节 枚举
将可能的取值一一列举出来
一 使用方式
二 枚举的优点
便于调试,不同于#define,在调试之前不会将变量名直接替换为等价的式子/数值,不会导致看到的和应用的不同的现象发生
第十五节 联合体/共用体
一 使用方式
根据联合体的特点,可以用来判断大小端
二 特点
(1)成员共用同一块内存空间
(2)因为共用同一块内存空间,所以一般同一时间只会使用一个,因为修改其中一个其他的也会被修改
(3)联合体的大小最小是最大成员的大小
(4)存在内存对齐
(5)联合体的存在的目的,并非是为了节省空间
三 内存对齐
第十六节 动态内存管理
一 动态开辟内存的原因
非动态申请内存空间char arr[40]
二 动态申请内存空间malloc
malloc | calloc | realloc |
申请的内存连续可用,返回值为指向该空间的指针,若申请失败,返回值为NULL | 同malloc | ①后面空间足够时,在原来空间后面直接追加,并返回原来位置 ②后面空间不足时,重新在内存中寻找一块区域存放,并复制原来空间内容,释放掉原来空间,返回新申请空间的起始地址 |
不会进行初始化,为随机值 | 会进行初始化,默认初始化为0 | 不会进行初始化,为随机值 |
需要判断是否申请成功 | 同malloc | 扩容失败的时候返回NULL,所以不要使用起初地址,防止原空间被搞丢 |
malloc(0):c语言中并未定义,具体结果取决于编译器 | realloc(NULL,20)等价于malloc(20); |
三 动态内存的释放和回收free(p)
(1)在进行释放的时候,自会知晓malloc申请了多大的一块内存空间
(2)p指向的空间,必须为动态开辟出来的,否则会出错
(3)p若为NULL指针,则无意义,啥都不干
(4)若在malloc开辟之后,不进行free操作进行释放,
①若程序结束,自动回收malloc开辟的空间
②若程序不结束,则不会自动回收,会造成内存泄漏(开辟空间无法回收被浪费)
(5)free释放掉内存后,p仍指向该空间,形成野指针(所以需要令p=NULL)
(6)当malloc申请空间后,未被free,情况频繁发生,直至内存被耗干时,电脑会死机
(7)现在电脑都会有保护机制,允许应用最多占用?GB的空间,来避免( 6 )问题的发生
四 易错点
(1)对NULL指针的解引用操作
申请空间后,判断空间是否申请成功
(2)对动态开辟空间的越界访问
对内存边界主动进行检查,类似于数组越界访问
(3)对非动态开辟内存进行free释放(直接出错)
(4)使用free释放动态开辟空间的一部分空间(直接出错)(造成部分空间内存泄漏,释放不了)
(5)对同一块空间多次释放(直接出错)
(6)动态开辟内存忘记释放(内存泄漏)
未使用free释放
开辟后,指向空间的指针被销毁,找不到申请的空间
第十七节 文件操作
一 文件简介
1 为什么使用文件
(1)当数据放在内存中的时候,程序退出,数据将自动销毁,再次使用时,需要重新录用
(不具备数据持久化)
(2)使用文件操作,我们将数据放入到硬盘之中(做到了数据持久化)
2 文件的分类
![](https://i-blog.csdnimg.cn/blog_migrate/f5e2e61e1207a3eea9e9d222202724db.png)
二 文件指针
(1)在操作文件的时候,会自动创建并填充一个文件信息区(FILE类型的变量)
(文件信息区一般包括:文件名称、状态、位置等)
(2)此时我们会创建一个FILE类型的指针,指向该文件信息区(FILE* pf)
(3)此时便可以通过文件指针很方便的找到与之相关联的文件
①(文件指针 --> 文件信息区 --> 与文件本身相关联)
②(文件指针 -----> 与文件本身相关联)
![](https://i-blog.csdnimg.cn/blog_migrate/ede841ea5c8d2bc57d2f571156f82b7d.png)
三 文件的打开和关闭
(1)文件打开和关闭配套使用,类似于malloc和free
①文件打开:自动创建并填充好文件信息区,并在最后返回起始地址
②文件打开失败:返回NULL指针(需要判空操作)
③fopen(“(文件的名称:位置+主干+后缀)”, ”(文件的打开方式)”)
④fclose(NULL);//会直接出错
⑤使用”a+”,并不会销毁掉原本文件中的内容
⑥打开方式(具体包含)
“w”:会在打开文件的时候,自动销毁之前的内容,致使打开的是一个空白文件
(2)具体使用
四 文件的顺序读写
1 相关函数
![](https://i-blog.csdnimg.cn/blog_migrate/7e54431ded1b511b6998b38d70a35fb7.png)
2 字符串输出函数fputc
![](https://i-blog.csdnimg.cn/blog_migrate/360c1226179107610a2203369ff97013.png)
3 字符串输入函数fgetc
![](https://i-blog.csdnimg.cn/blog_migrate/6958abbf1c387387a1a4a93ba4cfbc9e.png)
4 文本行输出函数fputs
![](https://i-blog.csdnimg.cn/blog_migrate/254f901c3e17ec5f5d44ad98f23cf6b7.png)
5 文本行输入函数fgets
6 格式化输出函数fprintf
要对比printf进行记忆
![](https://i-blog.csdnimg.cn/blog_migrate/e5f729204c46d39e7cf14684481959ec.png)
7 格式化输入函数fscanf
对比scanf进行记忆
![](https://i-blog.csdnimg.cn/blog_migrate/02f42c5e9fbc71ed190a7e864fedaa38.png)
8 二进制输出fwrite
以二进制形式写入,所以出现乱码
![](https://i-blog.csdnimg.cn/blog_migrate/163ef2656b8bc9b2bebcf363c158fe5d.png)
9 二进制输入fread
返回值:实际读取到的元素的个数
fread进行输入的要类型匹配
![](https://i-blog.csdnimg.cn/blog_migrate/49a0292db9df4e922f5936c47b4d9c32.png)
10 区分scanf / fscanf / sscanf 和printf / fprintf / sprintf
![](https://i-blog.csdnimg.cn/blog_migrate/7b751a0bba548d0143bbdccd3ad07450.png)
注意事项
一 main函数(主函数)
1.有且仅有一个,且在一个工程中也只能有一个
2.老式写法为void main,现在大都改为int main
3.程序执行从main函数开始执行
二 注释
两种注释方式: /* */ 以及 //
/**/为C语言的注释风格,但是例如/*a /* b */ c */,并不会注释掉c中内容,因为存在此缺陷,所以引用//(C++注释风格)
三 快捷键总结
ctrl + F5 (编译+链接+运行)
F5 (有的编译器会出现一闪而过的情况)
四 大小端字节序存储
(1)大端:高位放低地址,低位放低地址
(2)小端:高位放高地址,低位放低地址
正常存放:12 34 56 78(我们的理解)
内存中存放:78 56 34 12(小端存放)