数据类型
数据类型的本质: 固定内存块大小的方式.
内存上: 数组名+1 得到下一个元素, &数组名+1 得到下一个数组的大小(无意义).
- void 无类型, 就无法确定分配的内存大小, 因此 void 类型的变量会报错.
- 万能指针: ‘void *变量’ 指针可以确定大小. 很多没有返回值的内置函数完成后, 返回的就是这个 void 指针记录调用函数前的程序内存地址, 之后程序从那个地址继续运行.
- 编程最好明确所有东西的类型, 所以 ‘没返回值的函数’ 返回值上必须写 void.
func(void) 表示函数拒绝接收参数, 不需要参数的函数应该写成这样
func(void *Point), 表示函数的参数是任意类型的指针, 适用于 memcpy 可用于所有类型拷贝的函数
变量本质是一段连续内存的别名
字符格式的 \0 == 数字格式的 0, ‘\0’ == 0. “\0”是字符串
内存四区 (重要)
- 堆: 手动使用的内存空间
类型 新定义的指针名 = (类型 )malloc( sizeof(下一级类型 ***)) - 栈: 程序局部变量, 分为 ‘main() 的栈’ 和 ‘调用函数的临时栈’
- 全局区 global: 包含文字常量和全局变量还有静态变量, 多指针指向相同内容, 提高利用率.
- 代码区
画图 (重要)
凡事要画出四区图来分析程序
- 画图分清楚谁在哪里
char p = ‘Sx’; 在栈区的指针指向一个放在全局区的字符串
char q = ‘Sx’; 在栈区的另一个指针指向同一个全局区的字符串
由于全局区的效率策略, 第二个相同字符串不会重复创建, 只是让 p 和 q 都指向 ‘Sx’ - 变量的生命周期
char * get_str(){ 返回 ‘指向 char 类型的指针’ 的地址下图分析1常量strchar strPoint_Str = “定义的是变量的字符串, 这个会被返回全局区地址”
下图分析2变量 strchar str[] = “定义的是常量的数组名, 会返回函数临时栈区地址”;
全局区有’ ‘Sx\0’, 定义不是字符串, 定义的不是字符串, 定义的不是字符串, 因此, 函数临时栈区也会拷贝一份 ‘Sx\0’
用不着图分析 char tmp = (char )malloc()
tmp 函数结束就释放, 堆区不会释放, 堆区的地址返回有效return str;} __因为是数组不是字符串, 所以返回的是函数临时栈区的地址__
char *p; p = get_str() 乱码, 临时栈区已释放
char buf[]; strcpy(buf, get_str()); 不确定, 可能乱码, 也可能大函数拷贝时内部小函数的内存还在
栈是往下生长, 堆是往上生长. 栈中的数组是往上生长
字符串
char buf3[] = “abcd”; C 语言没有字符串类型, 只能用 char[] 代替. buf3 作为字符数组 有5个字节, 作为字符串有 4个字节
char str[100] = {0}; 要有结束符 0
sizeof(字符串类型) 的大小,包括’\0’;strlen(字符串类型) 长度不包括‘\0’(数字 0 和字符‘\0’等价)。
声明和定义区别
- 一般的情况下,把建立存储空间的声明称之为“定义”,而把不需要建立存储空间的声明称之为“声明”。
- 没{}的函数叫做定义, 有{}的函数叫声明
- 正数的原码, 反码, 补码都相同. 负数的反码是原码的取反, 补码 = 反码 +1
- 当一个小的数据类型赋值给一个大的数据类型,不会出错,因为编译器会自动转化。但当一个大的类型赋值给一个小的数据类型,那么就可能丢失高位。
scanf函数与getchar函数
getchar是从标准输入设备读取一个char。
scanf通过%转义的方式可以得到用户通过标准输入设备输入的数据。
gets(str)与scanf(“%s”,str)的区别:
gets(str)允许输入的字符串含有空格
scanf(“%s”,str)不允许含有空格指针
指针指向谁,就把谁的地址赋值给指针
放在=左边, 给内存赋值, 写内存 放在=右边, 取内存的值, 读内存定义
是一个结构体, 包含三个元素 - 指针的名字, 一般叫 p q
- 指针自己的地址, 对二级指针这个属性才有意义
- 指针内存放的内容, 是一个 0x?????? 的数值.
- 二维数组是矩阵, 二级指针只是指向指针的指针
- 常量指针和变量指针. a[], a 是常量指针, 只能 a+1, 不能 a++, 区别于手动定义的变量指针 p可以 p++
指针的使用
- 目的: 运行一个函数, 在函数内一次性改变多个 ‘传入实参的具体值’
类型 p= &XXXXX 初始化定义, 要把 ‘类型 ‘ 看成一个整体, 实际上是赋值操作 ‘新建指针自己的内存=’要指向的人’ 的地址
- 先声明, 再赋值地址指向, 才能用 * 修改指向的内容. 不能操作没有指向的野指针
- 取出: *p 相当于, 操作 ‘p 内存放的地址所在的内存’
- 取出格式, 一定要告诉编译器那个地址的内存是什么类型, 反向的理解是, 内存按照什么规则来解析,
例如以%s的 char 类型解读的话, 就要遵守 char 读到\n 为止的规则, 传入 &str[x] 或 p, 输出 str 第 x 个字母之后的所有char, 直到最后的结束符.printf(“%s”, 一个(char )类型的 p1); 会打印直到\0
printf(“%c”, 一个(char )类型的 p1); 会打印第一个 char
- strcpy(p, ‘some string’) 方法是给 p 指向的 ‘char str[]内存’ 拷贝,
省略 的原因是这个函数构造就是传入地址参数, 内部自行 传入的参数 - 静态只读字符串本身是 ‘指向在静态区第一个字母的地址’ 的栈指针,const char “hello” , 所以可以 > char *p = “hello”, 但是修改静态字符串会报错, 要修改只能用 str[].
- *p = p[], 某些情况 和 [] 等价, &的对立面是 /[]
1. 作为函数参数时, 因为传入的是单个地址, 随意替换 2. 参数以外的正确用法: 1. > char *str[] = {"hello", "hi"} 2. > char * *p = str = &str[0]; 二级指针指向的地址 = 一级指针地址 3. 参数以外的错误用法: > char * *str = {"hello", "hi"}
- a+1 = &a[1], a = &a[]
- char *a[3]= {“hello”, “hi”, “hei”} = 保存了 ‘三个字符串的首元素’ 的指针
通过函数改变实参的方法
要改 num, 执行修改的函数(参数 p), 调用函数实参(&num)*
- 众所周知, 函数内部只能复制参数生成临时变量, 无法改变传入的实参的具体值
- 指针传递参数的用法:
1. 函数定义: > void func ( char *p) __注意这个是新建的指针, 其值等于一个地址值__ 2. 函数传参实际调用: > func ( p 或者 &p[0]) __总之传入地址值)
- 因此要改变传入的实参, 只能把 &p 的地址传入函数, 函数内 ‘p = malloc 的堆地址’, 被调用函数是在heap上分配内存而非stack上
//不要轻易改变形参的值, 要引入一个辅助的指针变量. 把形参给接过来…..
int copy_str6(char from )
{
//(0) = ‘a’;
char tmp = from;
tmp = malloc; from = tmp - 正确格式:
- 定义函数格式: func( int *p ){},
- 使用函数格式: func(&num) 这个叫做传地址, 传数组名也可以因为是个常量=首元素地址
- 错误使用格式: func( p ), 这个叫做传变量, 即便传的是记录地址的变量也不可以
- 错误使用格式: func( a[]/a[100]/*a), 传了个形参, 编译器看来还是指针变量
- 面试题问法: 上述传入的 sizeof, a[] = a[100] = * a = 一个指针的大小
传地址: 函数定义时的形参比调用时的形参多一个 *, 就是在’地址作为参数传入’的时刻, 新建 ‘函数临时变量指向实参’ 的关系.
高一级传入: int b; fun(int *a); fun(&b);
传变量: 平级传入. 要搞事只能 malloc在堆, 然后返回地址给 p, 释放临时变量也没事.
平级传入: func(char *p); func(p);
typedef int A[9];
A b; //相当于, 去掉 typedef, b 替换到 A 的位置
指针数组
每个元素都是指针
指针指向谁, 就把谁的地址给指针
char a[] = {}; []的优先级比 高
数组指针
指向数组的指针, 指针一跳是一整个数组的长度
数组名a是数组首元素的起始地址,但并不是数组的起始地址。
int a[10] = {0} //最终指向的数组实例
typedef int A[10]; // 为了设定指针类型, 先定义数组类型. int 和实例类型 int 要匹配.
A *p = NULL; //数组指针类型
p = &a //&a 是整个数组首地址, a 是数组首元素地址. 值相同, 每一跳大小不同
使用时 ( p)[i], 优先级 常用的类型定义方式*
typedef int (*Point)[10]; //定义类型MyPointer 为指向int[10]类型的指针
Point q;
q = &a;
常用的变量定义方式
int (*q)[10]; //q 是一个指针, 指向一个数组
q = &a
不论怎么变, 只要注意, 声明的数组名*q 要指向 ‘第 0 个元素地址’, 这样一跳才正常
一维数组: ‘第0个元素地址’ = (数组名 + 0个),
q = 数组名, q = &数组名
二维数组: ‘第0个元素地址’ = ‘第0行第0个元素地址’ = (数组名 + 0行) + 0列
也就是, q = 数组名, q = 数组名
一维数组的 a 和 &a 值一样, 但是每一跳的内存大小不同
二维数组的 a =首行地址,和首行首元素地址 a 的值是一样, 然而他们的 ++, 一跳跳的内存大小不同
第 i 行首地址 = a + i = a[i]
第 i 行第 j 个元素地址 = (a+i) +j = &a[i][j]
第 i 行第 j 个元素值 = ( (a+i) +j) = a[i][j]
sizeof()
sizeof(首元素地址) = 这个元素所在序列的大小
sizeof(数组名) = 一根指针的大小
结构体
- struct xxx 两个单词合起来才是结构体类型
- 结构体内定义的变量不能赋值, 因为没有分配空间
- 新建一个结构体的实例
* > struct xxx *p, 刚刚定义的时候 p 是一个野指针 * > p = &tmp, 指针要有指向, 栈, 堆, string 都可以 * > p->age = 18, 指针结构体的用法 * (&tmp)->age == ( *p).age, tmp 是正常的结构体实例名, p 是指针结构体实例名
- 初始化赋值一个新的结构体实例: > struct Student sx = {22, “Sx”, }, 赋值的属性会按照 ‘结构体属性定义时的顺序’ 赋值.
指针传参, 结构体版
- 定义函数 > void func(const struct Student *p){}
__接收的是一个地址, 并把地址赋值给新建的结构体指针. 不修改地址本身就加上 const, 还要注意 const 的位置__
- 要传的参数: > struct Student sx = {22, “sx”}
- 调用函数: > func( &sx ) 传入一个地址参数
结构体内, 包含指针成员
sx.name = (char )malloc( sizeof(char) (strlen(“stardustx”) + 1) )
分配的空间大小 = 每个char字的大小 * (存储内容的长度 + 结束符)
释放: 先释放 name, 再释放指针
结构体提高
定义结构体时不能赋值, 因为定义不分配内存.
Student p = (Student )malloc()
或者 > struct Student p = NULL; p = &Sx;指向一个实例
一般用 Student.age, 闲得蛋疼等价于 (&Student)->age
__指针用: p->age 用法相当于 ( p).age__
Teacher tmp[老师个数]; 等价于 > Teacher tmp = NULL; tmp = (Teacher )malloc(老师个数 * sizeof(Teacher));
p = tmp; 间接赋值是指针存在最大意义, tmp 中存放 malloc 函数返回的堆地址, 赋值给一级指针 p
9.4 typedef
typedef为C语言的关键字,作用是为一种数据类型(基本类型或自定义数据类型)定义一个新名字,不能创建新类型。
与#define不同,typedef仅限于数据类型,而不是能是表达式或具体的值
#define发生在预处理,typedef发生在编译阶段
链表
错误 typedef struct player{
int age;
struct player next;}; 结构体本身没定义完, 内存大小不确定. 递归定义多捞啊
正确 typedef struct player{
int age;
struct player *next;}; 指针大小是固定的, 可以指向下一个自己的类型
开始连接 ALLMight.next = &Sx;
1 | //用指针指向, 替代新建节点=左边的名字, 有指针就不用名字了 |
函数指针
- 先定义类型
typedef 返回值类型 (*pFunc) (参数);
PFunc p2 = fun; 函数指针 p2 指向, PFunc 类型的 fun函数
p2() 等价于 fun() - 直接定义
int (*pFunc)(参数) = 函数实例名;
- 实际使用
创建条件[], 函数指针[], 创建一个字典, dict{ 条件: 函数指针}
for 匹配条件, 调用对应的函数指针 - 回调函数实现多态
void BigFunc ( int x, int y, int(*SmallFunc)(int a, int b)){
int x = SmallFunc(x, y);} //里面只能用 BigFunc 的参数, SmallFunc 的参数只是声明
把小函数名当做地址传入大函数, 大函数可以调用任意传入的小函数, 实现多功能大函数
作用域
void 外域:: 内部函数名(){}, 如果要在’外域’的外部声明内部函数, 类外定义内部函数就要这样格式
data 区域存放静态量, 在函数没调用之前就存在了, 所以不能 > static int j = var
static 的变量放到 const 的 data 区域, 第二次新建一个 static 的同名变量会失效, static 变量直到程序结束才释放.
全局变量要先赋值定义才能用, 和函数的模式一样
只能定义一次 > int a=1; 可以多次声明 > extern int a;
推荐定义的时候直接 a=0 , 赋值初始化
extern 普通全局变量是所有头文件和 main 文件都能连通使用
static 全局变量是只能在声明的本文件使用
- memset, 用来清空数据
- memcpy, 无视结束符的拷贝, strncpy 会被结束符结束拷贝. 可能会出现内存重叠错误
- memmove, 不会内存重叠的移动
- memcmp, 查看内存是否相等
代码操作内存
int p; p = (int )malloc(sizeof(int));
- malloc 分配 sizeof(int) 这么大的堆区内存空间, 成功的话, 返回堆区首元素地址
- 返回的地址是 (void )类型, 所以要转换成和指针类型匹配的 (int ) 类型
- 返回首元素内存地址给 *p
if(p != NULL){
free(p); p 指向的那块内存, 程序放弃使用权交还给系统
p=NULL} 把上述内存的入口删除
- 第一步, 不是释放 p 变量, 而是释放 ‘p指针内存放的地址’ 指向的内存. 释放的意思是用户放弃使用权, 交给系统回收
- 第二步, 把指针内存放的内容赋值为空
- 同一块内存被多个指针, 连续释放多次也会报错, 用 if(p != NULL) 判断
文件
用 fp 指针访问 File 文件, fp 不直接指向文件, 而是会自己调用了 fileOpen()
fp 是一个叫做 ‘句柄’ 结构体, 内部包含 {缓冲区, 文件描述符, 等} 成员属性
因此并不能直接操作 fp 指针, 而是调用文件库函数来操作 fp
- 打开和关闭和判断结尾
windows 平台的打开关闭是 wb, rbfclose(stdout); 关闭printf 输出
perror(“ 自定义 “); 打印库函数调用失败的原因
fp = fopen( 路径, 读写权限 ). fopen 自己去安排 File 结构体, 完成操作, 返回地址给 fp
feof(fp): 因为这个函数不会移动光标, 所以要__先fget(fp)读 fp, 再调用这个函数, 如果到文件结尾, 返回真. 读文件读到末尾是 EOF==-1. 类似字符串的结尾是 \n - 读文件
- fgetc(), fputc()读写单个字的函数, 都会自动移动光标
- fgets(), fputs() 按行读写
memset(buf, 0, sizeof(buf)) 先清空, 因为 fgets 失败的话, 缓冲区还是保留上次
fgets( 缓存到哪buf, sizeof(缓存到哪buf), 从’fp 或者 stdout’ 读) - 按块读写
fwrite( str, sizeof(str)单个块大小, 块个数, 写入文件)
读文件的块数目= fread( 读入缓存在 buf, 1每次读一块, sizeof(buf)单个块大小, 源文件)
避免要写的超过储存, 多用于每次写一个块, mdzz
- 写文件
fputc(‘ 单个字符’, 打开的文件 fp 或者屏幕sdtout) 写文件的函数, 每次写一个字符, 自己写循环写入
- 格式提取内容
sscanf(“1stardustx6”, “%1d%9s%1d”, &num1, sx, &num2);, 把 参数一 中的内容, 按照第二个参数的格式分拆, 赋值给后面的参数. 因为要改参数本身, 所以后面的都是地址: 数字需要加&, 字符串本身就是首地址的指针 - 提取后重组内容
sprintf(buf, “%d\n”, str), 把 str 格式化为字符串且加上换行, 再赋值给 buf - 写入文件
fprintf( 写入目标, “格式”, a, b, c)
- 格式提取内容
- 文件光标移动函数
fseek(fp, 0, SEEK_SET); 移动光标到相对于开头 0 个字节
fseek(fp, 0, SEEK_END); 移动光标到结尾
long size = ftell(fp); 获取光标所在位置到开头的大小缓冲区
要写文件的指令实际上存在缓冲区, 有以下 4 种方法, 从缓冲区真正写到文件- 缓冲区满, 自动写入, 不同 OS 缓冲区不一样
- 正常 fclose()
- fflush(), 迫不得已手动刷新
- 程序正常结束
程序编译步骤
C代码编译成可执行程序经过4步:
1)预处理:宏定义展开、头文件展开、条件编译等,同时将代码中的注释删除,这里并不会检查语法
2)编译:检查语法,将预处理后文件编译生成汇编文件
3)汇编:将汇编文件生成目标文件(二进制文件)
4)链接:C语言写的程序是需要依赖各种库的,所以编译之后还需要把库链接到最终的可执行程序中去
递归
1 | int reverse(char *theStr){ |