【学习笔记】C语言
C语言
C语言一个很大的特点是能够访问物理地址,对硬件进行操作,所以既具有高级语言的功能,又具有低级语言的很多功能;
1. C语言基础
1.1 C语言基础
- C语言运行机制简述
- 编辑:编写.c文件,也就是源代码
- 编译:将.c程序翻译成目标文件(.obj) //计算机底层执行
- 链接:将目标文件.obj生成可执行文件(.exe) //计算机底层执行
- 运行:执行.exe文件,得到运行结果
- C语言严格区分大小写; 是由一条条语句构成,每个语句的结尾是;
- python中的print运行完后自动换行,但是在C里printf是不会自动换行的;
- define和const的常量全大写,变量名和函数名用小驼峰
- 原码、反码和补码
二进制的最高位是符号位,0表示正数、1表示负数;
计算机在运算的时候都是以补码来运算
正数的原码、反码补码三码合一
负数的补码是反码(原码除符号位外都取反)+1
- C程序的内存分配
栈区:主要存放局部变量
堆区:malloc函数动态分配的数据,放在堆
静态存储区/全局区:全局变量和静态数据
代码区:存放代码/指令
- 命名
函数、结构体、枚举、typedef ---- 大驼峰或者带模块的大驼峰: XXX_AaaBbb
局部变量 ---- 小驼峰:aaaAbb
全局变量 ---- 带g的小驼峰:g_aaaAbb
宏 ---- 全大写下划线分割:AAA_BBB
常量 ---- 全大写下划线分割:AAA_BBB
- 指针类型*
// * 应该跟随变量名或者函数名
int *p;
//if右侧没有变量或者函数名,跟随类型;
sz = sizeof(int*)
// 当*与变量或者函数之间有其他修饰符时,不要紧跟修饰符;
char * const NAME = "A"; // 有了const修饰符,有空格;
1.2 变量的数据类型
- 基本类型
- 数值类型
- 整型
- 短整型 short
- 整型 int
- 长整型 long
- 浮点型(浮点型默认是double类型,if想要float,则加f,例如float a = 1.1f)
- 单精度型 float(4字节,6位小数)
- 双精度型 double(8字节,15位小数)
- 整型
- 字符类型 char(1字节,本质上是一个整数,输出时是ascii码对应的字符,所以可以参与运算,
- 数值类型
- 结构类型
- 数组
- 结构体 struct
- 共用体 union
- 枚举类型 enum
- 指针类型
- 空类型 void
1.在c语言中,没有字符串类型,使用字符数组表示字符串
2.查看每个类型的大小,可以直接用sizeof(int)
3.直接int就是有符号的,等价于signed int,有正有负;unsigned是无符号数,只有正数;
4.输出是%f,默认是六位也就是%.6f
5.在c语言中有自动类型转换:从范围小的能够自动转换为类型大的(short,int,unsigned int,long,unsigned long,float,double,long double)
6.将精度高的转换为精度低的时候,要加上强制类型转换。强制类型转换不会对数据本身造成影响
- C语言传递参数可以是值传递,也可以是地址传递
- 值传递(在传递或赋值时,是拷贝了一份):
- 地址传递 (在传递时,是将地址传递给了接收变量):指针;数组
- 关于空void和void*
从本质上理解,无论是void还是int、char、double,变量类型就是固定大小的内存块;int是4个字节,double是8个字节,编译器没有固定void的大小,所以if定义void a;编译器就不允许,自然就会报错;
void*就不一样了,它是一个指针变量,因为对于任何一个指针变量,它的大小都是4个字节(4byte=32bit=2^32=4G,足以指向任何一个内存,地址本身就是一个操作受限的非负整数);所以编译器是能够给void星去分配地址的;void星类型指向的是一个没有特定类型的内存区域;也就是说void星就像是一张白纸,任何其他类型的指针都可以赋给它;
无论何种类型的指针变量其大小都是一定的,sizeof(int*) = sizeof(doubel *);他们的区别可以理解为在对+1时的跳跃是不同的,例如int类型的指针变量在+1的时候跳跃4个字节,而double类型的指针变量在跳跃的时候跳8个字节;所以void *就是跳跃力还没有确定,等真正用的时候再确定(进行强制转换),这样其实可以理解为一种泛化,参数传递的时候谁都行,包罗万象;
但是不允许对void*指针操作它指向的对象,不允许解引用;
1.3 常量
- 0表示八进制,0x表示十六进制
- 335u(u表示无符号)
- 常量经常用预处理器:#define PI 3.14
- 也可以使用const关键字
//const 数据类型 常量名=常量值
const double PI=3.14; //注意const是要加分号的,而define不需要
const是在编译、运行的时候起作用,而define是在编译的预处理阶段起作用;
define只是简单的替换,没有类型检查,尤其在涉及到计算的时候往往会出错,也就是有边界效应;
const不能重复定义,而define可以通过undef来取消某个符号的定义,再重新定义;
- const的几种用法详解
1.定义常量
const int a = 1;
int const a = 1;
//两种写法作用一样,但是需要注意一旦定义就不能修改,否则会报错;
2.修饰常量字符串
const char *p = "hello";
if 没有const
p[2] = 'a'; //这肯定是错误的(因为这种定义是存在静态内存区的),但是,编译是不会报错的!这很危险;
当有了const修饰后,编译都过不了;
3.修饰指针
常量指针,就是指向的是常量,所以不能通过指针去修改指向的值;
const int* a = &b;
//常量指针指的是不能通过这个指针改变变量的值,例如*a = 3;但是并不意味着其指向的值不能变,b = 3,这是对的,只是说不能够通过这个指针来改变了;
//常量指针只有上面一个要求,所以指针本身也是可以改变的,也就是可以指向其他的地址,例如 a = &c;
指针常量,就是说指针是常量,指针自身的值不能被修改
int* const a = &b;
//指针常量指的是指向的地址不能改变,但是地址中保存的值可以改变;
1.4 变量
- 局部变量系统不会初始化,全局变量系统会自动初始化
- 变量都是里面能看到外面的,外面看不到里面的;在同一个作用域的变量不能重名;
- {}是一个独立的作用域
1.5 运算符
- 算数运算符
整数运算符:+ - * / % ++ --
// c语言的除:整数除只保留整数
// c语言的取模: a % b = a - a/b * b
---
关系运算符:== != < > <= >=
---
逻辑运算符:&& || !
//在C语言中逻辑运算都是短路逻辑
---
赋值运算符:= += -= *= /= %=
---
位运算符:& | ^ ~ << >>
>> : 右移:符号位不变,用符号位补溢出的高位;
<< : 左移:符号位不变,低位补0;
---
三元运算符:条件表达式 ? 表达式1: 表达式2
- 优先级:算数运算符>关系运算符>逻辑运算符>赋值运算符
1.6 语句
- Switch语句中的表达式是一个常量表达式,必须是一个整型或枚举类型;default语句是可选的,当没有匹配的case时,执行default;
- if 没有写break,会执行下一个case语句块,直到遇到break或者执行到Switch结尾;
2. 枚举
- 枚举是一种构造数据类型,对于只要几个有限的特定数据,可以用枚举;
enum DAY
{
MON=1, TUE=2, WED=3 //if没有给对应的值赋值,那就会按照顺序赋值,例如TUE没有赋值,那会自动赋值为2;
};
enum DAY day; //enum DAY是枚举类型,day是枚举变量
day = WED; //给枚举变量day赋值,值就是某个枚举元素
----
enum DAY
{
MON=1, TUE=2, WED=3
}day; //这里就相当于上面的两句,定义了枚举类型,同时定义了一个枚举变量day
//1、第一个枚举成员的默认值为整型的0,后续是在前一个成员上加1;
//2、不能直接将一个整数,赋值给枚举变量,但是可以将整数,强转成枚举类型,再赋值给枚举变量
day = (enum DAY)n;
3. 函数
3.1 头文件
- 头文件就是.h的文件,里面包含了C函数声明和宏定义;
建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件;
- 引用头文件相当于复制头文件的内容;
-
include <>引用的是系统头文件,直接在系统下找; #include ”“是先在当前目录下去找,if没找到再去系统里找;
- 一个#include只能引用一个
- 在头文件中只能有函数的声明,而不能有函数的定义
#ifndef 头文件名 //其格式:__HEADER_FILE_H__
#define 头文件名
//当再次引用头文件条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。
头文件内容
#endif
3.2 函数
-
基本数据类型是值传递,所以在函数内修改,不会影响原来的值;if想要修改,可以传入变量的地址&
-
static修饰的是静态变量,存放在静态存储区,只会被初始化一次
普通的全局变量对整个工程可见,其他文件可以使用extern外部声明后直接使用,也就是其他文件不能再定义一个与其相同名字的变量;静态全局变量仅对当前文件可见,其他文件随便定义相同的不会影响;对于函数同样;
- 字符串常用的系统函数:string.h
strlen :字符串长度
strcpy :拷贝字符串,原来的会覆盖
strcat :连接字符串
- 日期时间常用的系统函数:time.h
ctime :当前时间
difftime :时间差
- Sprintf是一个系统函数,可以将结果存放到字符串中(将基本数据类型转换为字符串)
- 将字符串类型转化为基本数据类型:atoi
int num = atoi(str)
double d = atof(str)
char c = str[0]
//if字符串并不能转为数值类型的,则默认转换为0或0.0
4. 预处理命令
以#开头的是预处理命令,在编译之前对源文件进行加工的过程,称为预处理
- 宏定义的替换是在预处理阶段完成的,注意宏定义是简单的直接替换;宏定义不是语句,不需要加分号,if加上分号,那就带着分号一起替换了;
- if要终止宏定义可以用undef,否则那就是在整个源程序中都有效;
- 宏定义可以表示数据类型例如 #define UINT unsigned int
宏定义表示的数据类型只是简单的字符替换,由预处理器来完成;
typedef定义的数据类型是在编译阶段由编译器处理的,并不是简单的字符串替换,而是给原有的数据类型起一个新的名字,将它作为一种新的数据类型;在有指针的情况下,typedef要比define要好;
-
关于typedef
typedef可以为已经声明的类型构造一个更有意义的名称,它 不会引入新类型,只是引入现有类型的新名称;
下面是typedef的几种用法
1.定义一个类型的新名称,这不是像define那样简单的替换,而是封装
typedef char* PCHAR;
PCHAR p1, p2; //定义了两个指向字符类型的指针;
char* p1, p2; //注意这并不是定义了两个指针,p1是指向字符类型的指针,p2是字符变量;由此也看出不是简单的替换;
typedef unsigned int UINT; //使类型变得更简单通俗易懂;
typedef int ARRAY_100 [100];
ARRAY_100 arr1; //相当于int arr1[100];
2.使用typedef来声明结构体;
typedef struct Student{
int id;
char *name;
}STU; //if没有这个STU,那在定义的时候就是struct Student stu1;
STU stu1; //这样就很简单了;
3.使用typedef来定义函数指针
typedef void (*PFunCallBack)(char* pMsg, unsigned int nMsgLen); //简化类型声明;
使用的时候就可以
RedisSubCommand(const string& strKey, PFunCallBack pFunCallback, bool bOnlyOne); //简化操作
除了使用函数指针作为形参外,函数指针也是可以作为返回值的;
typedef只是定义函数类型,不会自动转换为指针;
typedef int F(int*, int);
typedef int (*PF)(int*, int);
//假设f1(int)的返回值是一个指向函数(函数的形参是int*, int,返回值是int)的指针;
PF f1(int); //正确;
F f1(int); //错误,F是函数类型,f1不能返回函数类型;
*F f1(int); //正确,显式的指定返回类型是一个指向函数的指针;
-
宏定义可以定义为带参数的宏,除了会进行字符串的替换外,还会用实参替换形参;
例如#define MAX(a,b) (a>b) ? a : b
带参宏定义时,不会为形参分配内存,所以不必指明参数类型;
带参宏定义仅仅时字符串的替换,不会对表达式进行计算,宏在编译之前就被处理掉了,没有机会参与编译,也不会占用内存;但是函数时一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存的代码;
# include :包含一个源文件代码
# define :定义宏
# undef :取消宏
# if :if给定条件为真,则编译下面的代码
# ifdef :if宏已经定义,则编译下面代码
# ifndef :if宏没有定义,则编译下面代码
5. 数组
5.1 数组
数组可以存放多个同一类型的数据,数组也是一种数据类型,是结构类型
- if只将一些元素初始化为零,则编译器自动将所有元素初始化为0:
int a[5] = {0}; //数组清零初始化;
- 数组名就是数组的首地址;一旦数组长度定义了,其长度是固定的,不能动态扩充;
//数组长度
int size = sizeof(arr) / sizeof(double) //if arr数组是double类型
5.2 字符串
- 在C语言中没有其他语言的字符串,是用字符数组表示的;字符串数组是以null(\0)结尾的,表示字符串结束
- \0有两个作用
- 在用strlen计算字符串长度时,遇到\0就会停止,进而就能够求得字符串的长度(这个长度不包括结束符)
- 在用printf打印的时候,遇到\0就会停止输出
字符串定义的两种方法
1. 字符数组,使用字符数字又有两种方法,如下:
char arr1[] = "abc"; //无需指定大小是因为编译器可以自己推断出来,比较推荐的一种方法;会自动补\0
char arr2[5] = "abcd"; //因为开辟的数组内存没有用完,会在最后自动补\0
char arr3[5] = "abcde"; //开辟的5个大小的内存空间都占满了,所以编译器将这个输出完后不会停止,继续输出,直至遇见\0,所以会出现乱码;
char arr1[] = {'a', 'b', 'c', '\0'}; //手动加结束符
char arr2[] = {'a', 'b', 'c', 'd',}; //这种定义方式不会自动加结束符;所以会出现乱码;
char arr3[4] = {'a', 'b', 'c'}; //指定大小后,会在末尾加结束符
2. 字符串常量
char *arr1 = "hello"; //arr1指向了字符串的第0个字符;
两种定义的区别在于在内存中的存储区域不同:方法1是存放在栈区,其可以读也可以写;方法2是存放在常量区,只有读取权限,而不能写;这意味着通过方法2的字符串一旦定义,那对这个字符串就不能够进行修改
char *str = "hello";
str[2] = 'a'; //这是错误的;if通过方法1字符数组就是可以的;
对于这两种定义方法,其输出都有三种方法;注意这两种定义方法都可以通过这三种方法输出!!
printf("%s", arr1); //直接输出字符串;
for(i = 0; i < strlen(arr1), i++){
printf("%s", *(arr1+i)); //使用*(arr1+i)输出;
}
for(i = 0; i < strlen(arr1), i++){
printf("%s", arr1[i]); //使用arr1[i]输出;
}
---
关于赋值
第1种方法字符串的赋值只能在定义的时候进行
char arr4[5];
arr4 = "abc"; //这是错误的;
可以采用strcpy进行赋值
strcpy(arr4, "abc");
第二种方法是可以修改的,其意味着修改了指针的指向;
char* a = "hello"
a = "world" //可以直接修改
printf("a = %s", a) //world
---
求字符串长度有两种方法
1. sizeof(arr1); //4,是包括最后的占位符;
2. strlen(arr1); //3,是字符串的实际长度;
- null是ascii表的第0个字符,为空字符,该字符既不显示,也不是控制字符,不会有任何效果,在C语言中仅作为字符串的结束标志;
if在给某个字符数组赋值时,赋给的元素个数小于该数组的长度,那会自动在后面加\0,表示字符串结束;
if个数正好等于数组的长度,那会出现乱码,所以在定义数组时要保证数组长度大于实际字符串长度,保证输出时不会出现未知字符;
5.3 二维数组
- 可以只给部分元素赋值,未赋值的元素自动取“0”
5.4 指针数组
- 指针和数组的关系
int a[5] = {1,2,3,4,5};
a是地址常量,存放第一个元素的地址,也就是a指向了a[0];
arr是指向数组第一个元素的地址,那么&a就应该是指向这个指针的指针,但是c语言编译器对其进行了优化,所以&a会直接返回a的值,所以本来应该是指向指针的指针变成了指向第一个元素的指针;也就是
&a = &a[0] = a;
a[3]等价于*(a+3);
---
int a[3] = {1,2,3};
int *pst = a;
print("%p", a);
print("%p", pst);
print("%p", a[0]);
print("%p", &a);
print("%p", &p); //除了最后一个外,剩余的输出都一样,都是数组第一个元素的地址,最后一个是指针p的地址;
pst指针指向数组首元素
a[i]、*(pst+i)、*(a+i)、pst[i]完全等价,都是访问数组第i+1个元素!!!!
- 指针数组
指针数组就是数组中的元素是指针变量
int *p[4]; //p是一个指针数组,每一个元素都是int*类型,也就是每一个元素都是一个指针变量,一个地址,这个指针指向int类型;
int a = 1;
int *p[0] = &a; //指针数组p的第一个值是变量a的地址
6. 指针
6.1 指针基础
& :取地址
*有两种含义:
- 在定义时:int* ptr (代表ptr是一个指针,指向int类型)
int* ptr = &a; //ptr是一个指针,指向int类型,ptr的值是a的地址;简单理解就是ptr指向了a
- 在使用时:*ptr(代表取出ptr指向的变量的值,也就是a的值)
*变量就是其指向的变量!
指针就是地址,地址就是指针!!!
指针变量是存放内存单元地址的变量,指针的本质上就是一个操作受限的非负整数(不能加乘除,只能减)
int* p; //p是变量名,int*表示p是指针变量,该变量只能存储int类型变量的地址,也就是该指针指向int数据
int i = 10;
int j;
p = &i; //取i的地址赋给p,p就指向了i
j = *p; //j=1,经过上一步后,*p就是i,两者在任何情况下可以互换;
- 指针变量既然是一个变量,所以其可以运算,涉及到指针的运算有四个:++,--,+,-,例如++,其意味着获得下一个元素的地址(指的是下一个元素,针对不同类型的元素,加的字节数也不一样,例如int就是加4);也就是说在指针的加减里,加减的是一个单位,并不是一个字节
- 指针也是可以比较的,>; <; >=; <=
- 空指针:在变量声明时,if指针没有明确的地址指向时,往往会为指针赋一个NULL,也就是空指针;
空指针NULL是一个定义在<stdio.h>中的值为零的常量 #define NULL 0;
- 指向指针的指针变量用两个**来完成
int **pptr; //指向另一个指针的指针变量
6.2 指针数组
int *ptr[3]; //数组的每一个元素都是int*类型,也就是每个元素都是一个指针,存放地址,指向int类型;
- 字符串指针???
6.3 函数指针
- 函数的传入值和返回值都可以是指针,但是需要注意,if在一个函数返回指针,在函数使用完毕以后,会销毁这块内存区,所以这个返回的指针就不一定还是指向原来的值了,所以往往if要返回指针,那要把这个指针指向的变量声明为static,这样这个变量会存在静态存储区,不会随着函数使用完毕后销毁;
- 函数指针意思是指向函数的指针,也就是把函数的首地址赋给一个指针变量;
int max(int a, int b){
return a > b ? a : b;
}
int (*pmax)(int, int); //未初始化,也可以直接在后面=max初始化;
//函数指针的名字是pmax
//int表示该函数指针指向的函数是返回int类型
//后面的两个int表示该函数指针指向的函数形参是两个int类型,也可以定义函数指针时写上形参的名字
pmax = &max;
pmax = max; //这两个语句等价,取地址符可有可无;
int maxVal = (*pmax)(x, y);
int maxVal = pmax(x, y);
int maxVal = max(x, y); //上面三条语句等价
---
int *p(); //p是返回指针的函数,该指针指向int数据
int (*p)();//p是函数指针,也就是指向函数的指针,指向的函数返回int类型
函数名本质就是一个地址
虽然不能定义形参是函数类型,但是形参可以是指向函数类型的指针,这时候形参看起来好像是函数类型,其实是指针(函数类型的形参会自动转为指针)
void getMax(int a, int b, int pmax(int x, int y)); //看似函数类型,实则是指针
void getMax(int a, int b, int (*pmax)(int x, int y)); //和上面等价,这是显式的
getMax(x, y, max); //直接调用;
int (*pmax)(int x, int y) = max; //pmax指向max函数;
getMax(x, y, pmax); //pmax是指向max的函数指针,和上面那个调用等价;
6.4 回调函数
先说结论:回调函数最大的优势在于解耦,在主体不变的情况下对不同的需求传递不同的行为!
个人理解,这和java里的抽象类有些类似,我们的目的都是为了解耦。
这个在C语言里实现的前提是函数可以作为参数传递,假设函数就是定制的一套规则,基本数据类型就是定义一些原料,我们传递不同的规则(也就是传递不同的函数),就能够根据这些规则用这些原料(就是我们传入的参数)作出不同的菜来。
所以其是依赖于函数指针,有这个机制后,才可以将函数像参数那样传递
如上图,在程序中,上层是我们的应用编程,也就是要实现的业务逻辑,下层是库函数,是系统为程序员提供的;在我们写程序时,会通过调用库函数里留下的API接口来实现我们自己的业务逻辑,但是有些库函数只是写了一种泛化的功能,具体的要留给程序员自己决定,所以库函数要求我们向它传递一个规则(一个函数),然后在合适的时间调用它完成业务功能,这个传入的函数就是回调函数(call back)
打个比方,有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“叫醒”这个行为是旅馆提供的,相当于库函数,但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数(to register a callback function)
参考自回调函数
- 我们仔细看一下这张图,然后想一下这个过程,我们在应用层决定了传入什么样的回调函数(主程序),然后主程序调用底层的库函数,库函数根据传进来的函数指针又去调用回调函数,这其实是一个高层调用底层,底层又反过来调用高层的过程,可能这就是回调的来源;
- 库函数其实就是作为了一种中间函数,其是不完整的,程序在运行时,通过登记不同的回调函数,就可以决定和改变中间函数的行为,这就提供了很大的灵活性,实现了程序解耦;
语言都是想通的,可以看这个python的例子
#回调函数1
def double(x): #生成一个2k形式的偶数
return x * 2
#回调函数2
def quadruple(x): #生成一个4k形式的偶数
return x * 4
from even import *
#中间函数
#接受一个生成偶数的函数作为参数,返回一个奇数
def getOddNumber(k, getEvenNumber):
return 1 + getEvenNumber(k)
#起始函数,这里是程序的主函数
def main():
k = 1
#当需要生成一个2k+1形式的奇数时
i = getOddNumber(k, double)
print(i)
#当需要一个4k+1形式的奇数时
i = getOddNumber(k, quadruple)
print(i)
#当需要一个8k+1形式的奇数时
i = getOddNumber(k, lambda x: x * 8)
print(i)
if __name__ == "__main__":
main()
从上面的例子中,当我们给中间函数传入了不同的规则(不同的回调函数)时,其表现出来的行为,实现的功能也不一样,这就是回调的优势所在;
在C语言中,回调函数的实现来自于能够将函数指针作为函数参数,就可以在别人的函数执行时调用你传入的函数(是通过函数指针来完成的)
例如将一个函数名作为参数传入另一个函数,那在被传入函数中要用函数指针来接收,例如:int (*f)(void),这个表示可以接受一个返回值是int类型,没有形参的函数
#include <stdlib.h>
#include <stdio.h>
//中间函数
void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))//由函数指针接收
{
for (size_t i=0; i<arraySize; i++)
array[i] = getNextValue();
//调用十次回调函数,这时候getNextValue指向的就是getNextRandomValue函数
}
// 回调函数:获取随机值
int getNextRandomValue(void)
{
return rand();
}
//主函数
int main(void)
{
int myarray[10];
populate_array(myarray, 10, getNextRandomValue); //将函数名作为参数传给函数;
for(int i = 0; i < 10; i++) {
printf("%d ", myarray[i]);
}
printf("\n");
return 0;
}
以下是来自知乎作者常溪玲的解说:
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。
6.5 动态内存分配
在C语言中,不同的数据分配在不同的内存区域,全局变量在静态存储区,非静态的局部变量,在内存中的动态存储区-栈中,临时使用的数据,建立动态内存分配区域,需要时随时开启,不需要时及时释放,也就是在堆区;
动态内存分配就是根据需要向系统申请所需大小的空间,由于未在声明部分定义其为变量或者数组,不能通过变量名或者数组名来引用这些数据,所以只能通过指针来引用;
- 动态内存分配相关函数
头文件<stdlib.h>声明了四个关于内存分配的函数
void *malloc(unsigned int size); //在堆区分配一个长度为size的连续空间,函数返回值是所分配区域的第一个字节的地址,也就是返回的指针指向该分配域的开头位置;
void *calloc(unsigned int n, unsigned int size); //和上面基本一样,在堆中分配n个长度为size的空间,常用来分配数组,这个空间一般比较大;
void free(void *p); //释放变量p指向的动态空间,无返回值
void *realloc(void *p, unsigned int size); //重新分配malloc或者calloc函数获得的动态空间大小,将p指向的动态空间大小变为size
其实java中的
A a = new A() //在堆中new了一个对象,栈中的引用a指向了堆中的对象;!!
就等价于
A *p = (A*)malloc(sizeof(A)); //在动态内存分配了一块大小是A类型大小的内存,然后将void*类型转为了A*类型,指针P指向了这块内存;
-
动态内存分配原则
- 避免分配大量的小内存块
- 仅在需要时分配内存,只要使用完要及时释放,否则会发生内存泄漏(谁分配,谁释放)
-
无类型指针
void * :这种指针称为无类型指针,也就是不指向哪一种具体的类型数据,是一个纯地址,不指向任何具体的对象;
假设某个变量p是void*类型,那就不能通过*p来取值,因为其仅仅是一个纯地址;
int a;
void *p;
p = &a; //其实这种是不行的,因为&a是int*类型的,但是p是void*类型的,但是很多版本例如c99会在编译时自动进行一个转换,不需要用户来强制转换了;也就是相当于 p = (void*)&a,这时候p得到a的纯地址,但是不能够通过*p来输出a的值;
7. 结构体
7.1 结构体
- 结构体简单理解就是类没有了方法,只有属性;
- 定义结构体
struct 结构体名称{
成员列表;
};
--
//也可以在定义结构体的时候就创建结构体变量
struct Student{
成员列表;
}stu1, stu2;
--
//if只定义几个变量,可以采用匿名的方法,也就是不写结构体名称
struct{
成员列表;
}stu1, stu2;
在创建了一个结构体变量后,需要给成员赋值
stu1.name = "liming";
struct Student stu3 = {"lihua", 12}
//但是不能在定义后再这样赋值;struct Student stu4; stu4 = {...};这是错误的
//或者在定义的时候就直接赋值
struct Student{
成员列表;
}stu1 = {"liming", 11};
struct Student stu = {"liming", 13};
struct Student *pstu = &stu;
if要访问成员,也就是属性;
stu.name;
pstu->name; //等价于(*pstu).name = stu.name
这句话的意思是说:pstu所指向的结构体变量的name成员;
注意只有指针变量才可以用->,普通变量只能用点
(*pstu)->name; //这是错的,因为*pstu=stu是普通变量;
7.2 共用体
union 共用体名称{
成员列表;
};
- 共用体和结构体最大的区别就在于所有的成员变量共用一块内存,也就是说这个共用体占的内存大小就是成员列表里最大数据类型所占用的内存大小,例如成员有int、short、char,那对于任何一个共用体它的大小是4个字节,这里面的三个成员共享四个字节;
8. 文件
输入输出流是以内存来定义的;
fopen() 创建或者打开一个文件
fclose() 关闭文件