回顾:
1. 大型程序开发代码分类
-
头文件卫士
vim A.h #ifndef __A_H #define __A_H 头文件内容 #endif
-
大型程序代码分为3部分
头文件:
各种声明
全局变量声明
函数声明
结构体声明
联合体声明
枚举声明
源文件
各种定义
全局变量(普通变量,数组,结构体,联合体,枚举)定义
函数定义
主文件
调用管理,包含main函数
调用变量和函数之前包含对应的头文件进行声明
2. 结构体
2.1 目前C语言分配内存的方式
定义变量,数组,结构体,联合体
-
为何用结构体分配内存
既可以做到大量分配内存,也可以做到数据类型不一致
-
结构体声明和定义
先声明后定义
struct A { 成员; };
先用typedef对声明的结构体取别名然后定义
typedef struct A { 成员; } A_t; A_t a;
-
结构体初始化
传统初始化
全部按照顺序初始化
A_t a = {, , , ,};
标准初始化
不要按照顺序也不要全部初始化
A_t a = {各种.};
-
结构体成员访问
结构体变量名.成员名;
结构体变量的指针->成员名
-
结构体嵌套(包含)
结构体嵌套结构体
成员的访问:A.B.成员名;
A->B.成员名;
结构体包含结构体变量的指针
成员的访问:A.B->成员名
A->B->成员名
注意:B必须指向一块内存区域
-
结构体和数组的配合
数组中每个元素是一个结构体
玩法:数组结合结构体
-
结构体和函数的配合
只要将来编写一个函数访问操作结构体,此函数的形参一定是一个结构体指针变量
两点原因:指针变量将来永远只分配4字节,节省内存
将来函数通过指针修改结构体成员,否则只能查看不能修改(会=回想之前的swap函数)
void A(A_t *p) { .... } A_t;
函数的返回值当然也可以用结构体,可以返回一个结构体变量,也可以返回一个结构体指针(不能是局部非静态)
一般建议返回一个结构体指针(只需4字节)
3. 联合体
3.1 玩法和结构体一模一样,关键字union
3.2 所有成员共用一块内存
4. 枚举
4.1 本质
枚举的本质就是一堆整数的集合,更确切的来说,也就是给一堆得整数取了个别名而已,提高代码的可读性
例如:0表示红色,1表示绿色,2表示蓝色,过段时间可能就混淆了数字和颜色的对应关系
干脆给0,1,2三个数字取别名叫:RED, GREEN, BLUE,不管什么时候,只要看到别名,就能匹配到对应的颜色上
跟#define宏类似
#defin RED 0
#define GREEN 1
#define BLUE 2
结论:以后给整型数字取别名两种方法:采用枚举或者宏
4.2 特点
枚举值默认从0开始,后面的成员依次加1
枚举一般建议用大写
4,3 声明枚举数据类型的语法:
enum 枚举数据类型名(也可以不用) {枚举值,如果有多个用逗号分开};
例如:
enum COLOR {RED, GREEN, BLUE};
结果:
RED = 0, GREEN = 1, BLUE = 2 也就是0,1,2三个数字取别名
printf("%d %d %d\n", RED, GREEN, BLUE);
enum COLIR {RED, GREEN=250, BLUE};
结果:
RED = 0, GREEN = 250, BLUE = 251 也就是0,250,251三个数字取别名
参考代码:enum.c
/*枚举演示*/
#include <stdio.h>
int main(void)
{
enum {RED, GREEN=250, BLUE};
printf("%d %d %d\n", RED, GREEN, BLUE); // 可读性很好
printf("RED+GREEN+BLUE=%d\n", RED+GREEN+BLUE);
enum RESULT {RETURN_FILED, RETURN_OK};
printf("%d, %d\n", RETURN_FILED, RETURN_OK);
return 0;
}
4.4 枚举经典使用模板(操作系统核心代码片段)
vim enum2.c 添加
#include <stdio.h>
/*定义检测变量值的函数*/
int check(int a)
{
if(a != 0) {
printf("表示成功了\n");
return 0; //表示成功了
} else {
printf("表示失败了\n")
return 1; // 表示失败了
}
}
int main(void)
{
printf("%d\n", chenk(0));
printf("%d\n", check(1))
return 0;
}
结论:程序员很难判断0和1谁是成功,谁是失败,
除非认真仔细研究透代码,或者有详细的代码注释,
否则代码的可读性很差
解决办法:利用枚举或者#define宏提高代码的可读性,这里采用枚举
改进之后的经典代码(公式):
vim enum3.c
#include <stdio.h>
//声明枚举数据类型
enum RETURN {RETURN_OK, RETURN_FAILED};
// 对枚举类型取别名
typrdef enum RETURN return_t;
/*定义检测函数*/
return_t check(int a)
{ //函数的返回值是一个枚举值
if(a != 0) {
printf("表示成功了\n");
return RETURN_OK; //表示成功了
} else {
printf("表示失败了\n")
return RETURN_FAILED; // 表示失败了
}
}
int main(void)
{
printf("%d\n", chenk(0));
printf("%d\n", check(1))
return 0;
}
结论:任何程序员只要看到RETURN_OK,RETURN_FAILED就知道具体含义是成功还是失败了,提高了代码可读性
参考代码:enum2.c
/*枚举经典案例*/
#include <stdio.h>
/*声明枚举数据类型*/
enum RETURN {RETURN_OK, RETURN_FAILED};
/*对枚举类型取别名*/
typedef enum RETURN return_t;
/*定义检测变量值的函数*/
return_t check(int a)
{
if (a != 0) {
printf("表示成功了\n");
return RETURN_OK;
} else {
printf("表示失败了\n");
return RETURN_FAILED;
}
}
int main(void)
{
printf("%d\n", check(0));
printf("%d\n", check(1));
return 0;
}
参考代码:enum3.c
/*经典案例#define宏实现*/
#include <stdio.h>
/*定义宏*/
#define RETURN_OK 0;
#define RETURN_FAILED 1;
/*定义检测变量值的函数*/
int check(int a)
{
if (a != 0) {
printf("表示成功了\n");
return RETURN_OK;
} else {
printf("表示失败了\n");
return RETURN_FAILED;
}
}
int main(void)
{
printf("%d\n", check(0));
printf("%d\n", check(1));
return 0;
}
第十课:函数指针(核心中的核心)
1. 回顾指针函数概念
本质是一个韩式,只不过它的返回值是一个指针而已
例如:
int *add(int a, int b)...
2. 切记切记
函数名就是整个函数的首地址
int add(int a, int b) {
return a + b;
}
此函数在内存中的首地址就是add函数名
3. 函数指针概念
本质是程序员自己定义的新的数据类型(跟结构体,联合体,枚举一样,都是自己声明,自己定义)
用它定义的变量保存只能保存函数的首地址
4. 函数指针数据类型声明的语法:
返回值数据类型(*函数指针数据类型名) (形参表);
注意:声明的代码对于大型程序要写在头文件中
建议用typedef对函数指针数据类型取别名
例如
int (*pfunction) (int a, int b); // 这一堆代码就是一种函数指针数据类型,把这一堆当成int使用
// 书写起来太麻烦了,建议取别名
建议取别名:
typedef int (*pfunc_t) (int a, int b); // 对函数指针取别名叫pfunc_t,以后直接拿着别名定义变量
注意:声明时,形参的变量名可以不用写
5. 函数指针变量定义的语法格式
函数指针数据类型/别名 函数指针变量
例如:
pfunc_t pfunc; // 定义函数指针变量,此变量将来保存一个函数的地址
// 所以此变量永远也只分配4字节内存
6. 函数指针变量的初始化
pfunc_t pfunc = add; // 定义初始化函数指针变量pfunc,并且保存add函数的首地址,简称指向add函数
7. 通过函数指针变量来间接访问指向的函数,调用函数,语法格式
函数指针变量名(实参表)
例如:
pfunc(100, 200); //本质就是调用add函数,并且给add函数传递100和200
8. 切记
函数指针数据类型指定的函数返回值类型,形参表务必要和指向的函数的返回值和形参表类型保持一致
例如:
typedef int (*pfunc_t) (int a, int b); // 它要求返回值和形参表都是int类型
所以将来指向的函数:例如:
int add(int a, int b)....它的返回值和形参表也必须是int类型
参考代码:pfunction.c
/*函数指针演示*/
#include <stdio.h>
/*声明函数指针数据类型,并且取别名*/
typedef int (*pfunc_t)(int, int);
/*定义加法和减法函数*/
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int main(void)
{
int ret = 0;
/*定义函数指针并且初始化为空指针*/
pfunc_t pfunc = NULL; // pfunc_t=数据类型 pfunc=指针名
// 指向add函数
pfunc = add;
// 利用函数指针调用add函数
ret = pfunc(100, 200);
// ret = add(100, 200); // 也能通过编译,并执行出结果,但是不知道有没有危害
printf("%d\n", ret);
// 指向sub函数
pfunc = sub;
// 利用函数指针调用add函数
ret = pfunc(100, 200);
printf("%d\n", ret);
return 0;
}
9. 回调函数:
一个函数可以被当成参数传递给别的函数,这个函数的参数必须是函数指针
其中被传递的函数,称为回调函数,例如:add,sub
作用:将来写一个调用函数cal,然后完成一个功能,但是这个功能将来可能不能满足
别的程序员的要求,此时让cal函数具备调用别的程序员实现的函数
此时只需在cal函数的形参中添加一个函数指针,将来指向别的程序员
的回调函数完成他想要的结果即可
参考代码:pfunction1.c
/*回调函数演示*/
#include <stdio.h>
/*声明函数指针数据类型并且取别名*/
typedef int (*pfunc_t)(int, int);
/*定义add函数和sub函数:又称回调函数:类似袋鼠宝宝*/
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
/*定义调用函数:类似袋鼠妈妈*/
int cal(int a, int b, pfunc_t pfunc) // 类似把袋鼠宝宝pfunc放在袋鼠妈妈cal的口袋里,放点食物a和b
{
// 此函数的默认功能:做两个数相乘
if (NULL == pfunc) {
return a * b;
} else { // 说明默认功能不能满足需求,那么就调用别的程序员自己指定的回调函数完成它想要的功能
return pfunc(a, b); // 调用回调函数并且给回调函数传递参数
}
}
int main(void)
{
int ret = 0;
ret = cal(100, 200, add); // 调用cal函数,传递三个参数,最后一个参数回调函数add的首地址
printf("%d\n", ret);
ret = cal(100, 200, sub); // 调用cal函数,传递三个参数,最后一个参数回调函数sub的首地址
printf("%d\n", ret);
ret = cal(100, 200, NULL); // 调用cal函数,用此函数默认功能
printf("%d\n", ret);
return 0;
}
10. 函数指针经典代码演示
目的利用函数指针,循环,数组实现顺序调用一组函数
参考代码:pfunction2.c
/*函数指针,循环,数组结合使用,完成一组函数挨个调用*/
#include <stdio.h>
/*声明函数指针数据类型*/
typedef int (*pfunc_t)(int, int); // 类比当成int类型
/*定义加法,减法,乘法,除法函数*/
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main(void)
{
/*1.定义初始化一个函数指针数组,每个元素是一个函数指针,也就是一个函数的地址*/
pfunc_t array[] = {add, sub, mul, div, NULL}; //数组里的是地址 pfunc_t类比为int = int array[] = {},NULL如果不加虽然也可以运行出结果,但是有点问题,保险起见,还是加上
printf("%d\n", sizeof(array) / sizeof(array[0])); // 5
/*挨个调用*/
for(pfunc_t *pfunc = array; *pfunc; pfunc++) // 看做int i = 0;pfunc指向array, *pfunc取出里面元素也就是函数,pfunc++取出下一个函数
{
int ret = (*pfunc)(200, 100); // 先取出函数用(*pfunc),然后再传递实参
printf("ret = %d\n", ret);
}
return 0;
}
第十一课:多级指针(掌握到二级指针)
1. 回顾一级指针:
指向一个普通变量内存区域
例如:
int a = 250;
int *p = &a; // p保存变量a的首地址,p指向a
printf("a的首地址%p\n, a的值是%d\n", p, *p);
// 修改值
*p = 520;
2. 二级指针概念:
指向一级指针变量的指针,也就是二级指针变量保存一级指针变量的首地址
3. 二级指针变量定义的语法格式
数据类型 **二级指针变量名 = 一级指针变量首地址;
例如:
int a = 250;
int *p = &a; // p指向a
// 定义二级指针保存一级指针变量p的首地址
int **pp = &p; // pp 指向p
printf("p的首地址是%p,a的首地址是%p,a的变量的值是%d\n", pp, *pp, **pp);
// 修改变量a的值
**pp = 520;
立马浮现内存分布图:二级指针.png
参考代码:pppointer.c
/*二级指针演示*/
#include <stdio.h>
int main(void)
{
int a = 250;
int *p = &a; // p指向a,p保存a的首地址
int **pp = &p; // pp指向p,pp保存p的首地址
// 通过二级指针查看
printf("p的首地址是%p, a的首地址是%p, a的值是%d\n", pp, *pp, **pp);
// 通过二级指针修改
**pp = 520;
printf("%d %d %d\n", a, *p, **pp);
return 0;
}
目前看来,二级指针处理普通的变量多余,用一级指针即可拿下
4. 二级指针和字符串
经典笔试题:编写一个函数,实现两个字符串的交换
思路:
cahr *pa = "hello";
char *pb = "world";
目标:
pa指向->"world"; pb指向->"hello",指针互换
参考代码:swap.c
/*实现字符串交换:交换字符串首地址*/
#include <stdio.h>
/*无法交换*/
void swap(char *p1, char *p2)
{
char *p3 = p1;
p1 = p2;
p2 = p3;
printf("p1 = %s p2 = %s\n", p1, p2);
}
/*正确交换*/
void swap_ok(char **p1, char **p2)
{
char *p3 = *p1; //p3 = hello
*p1 = *p2; // 修改一级指针pa的值 // pa = p1 = world
*p2 = p3; // 修改一级指针pb的值 // pb = p2 = hello
}
int main(void)
{
char *pa = "hello";
char *pb = "world";
swap(pa, pb);
printf("pa = %s, pb = %s\n", pa, pb);
swap_ok(&pa, &pb);
printf("pa = %s, pb = %s\n", pa, pb);
return 0;
}
5. 二级指针和字符指针数组
5.1 回顾:字符指针数组
char *p[] = {"hello","world"}; // p[0]第0个元素的值是"hello"的首地址
// p[1]第一个元素的值是"world"的首地址
或者
char *p1 = "hello";
char *p2 = "world";
char *p[] = {p1, p2}; // p[0] = p1, p[1] = p2
注意:给你一个字符串,实际给你的是字符串的首地址
显然这里p具有二级指针的意味,通过p获取的是每个字符串的首地址
所以可以定义一个二级字符指针变量来保存字符指针数组的首地址,例如:
char **pstr = p;
printf("%s %s %s %s %s %s\n", p[0], p[1], pstr[0], pstr[1], **(pstr[0]), *(pstr+1));
结论:
char **pstr等价于 char *p[元素个数];
参考代码:ppstring.c
/*二级指针和字符指针数组等价关系演示*/
#include <stdio.h>
int main(void)
{
char *a[] = {"hello", "world"};
char **p = a; // p指向a
// 打印字符串
printf("%s %s\n", a[0], a[1]);
printf("%s %s\n", p[0], p[1]);
printf("%s %s\n", *(a + 0), *(a + 1));
printf("%s %s\n", *(p + 0), *(p + 1));
// 通过p来打印"hello"字符串中每个字符
printf("%c\n%c\n%c\n%c\n%c\n", *(*(p + 0) + 0), *(*(p + 0) + 1), *(*(p + 0) + 2),
*(*(p + 0) + 3), *(*(p + 0) + 4));
// 修改hello为hi
*(p + 0) = "hi";
printf("%s\n", *(p + 0));
return 0;
}
6. 实际开发,主函数main必须这样写
-
main函数的公式 (argc放的参数个数,argv放的参数内容(字符形式的))
int main(int argc, char *argv[]) 等价于 int main(int argc, char **argv) 不要写成:int main(void)
-
切记:只要在命令行终端(不管是什么命令行)输入的任何内容,操作系统都把它们当成字符串来处理
例如:
./helloworld 10 200
结论:操作系统把这些输入的内容都当成了字符串
"./helloworld"和"10"和"200"
-
问:main函数的形参agrc和argv到底是什么?
答:argc:当运行程序时(例如:./helloworld),操作系统用argc这个参数来保存你命令行输入的参数个数
例如:./helloworld,最后操作系统会自动给argc赋值为1,表示参数个数为1个
./helloworld 100,最后操作系统会自动给argc赋值为2,表示参数个数位2个
argv:当运形程序时,操作系统用argv字符指针数组来保存输入的字符串首地址
例如:./helloworld,最后操作系统自动给argv[0]赋值为"./helloworld"字符串的首地址
./helloworld 100 200 ,最后操作系统自动给argv[0]赋值为"./helloworld"字符串的首地址
argv[1]赋值为"100"字符串的首地址
argv[2]赋值为"200"字符串的首地址
结论:比如用户运行程序命令
./helloworld 100 200 300
结果: argc = 4 参数为4个 argv[0] = "./hellowprld" argv[1] = "100" argv[2] = "200" argv[3] = "300"
参考代码:main.c
/*main函数完整的编写框架*/ /*运行 ./main 100 200 argc = 3 argv[0] = "./main"首地址 argc[1] = "100"的首地址 argv[2] = "200"的首地址 */ #include <stdio.h> // int main(int argc, char **argv) int main(int argc, char *argv[]) { for(int i = 0; i < argc; i++) { printf("argc = %d, argv[%d] = %s\n", argc, i, argv[i]); } return 0; }
-
程序运行时可以通过命令行传递参数,例如:“100”,“200”
问:命令行输入的数字,最终该操作系统都处理成了字符串
实际程序需要整型数字,如何将字符串转成整型函数呢?
答:用strtoul标准库函数
strtoul函数功能:就是实现字符串转整型数字 ,例如:“100” > “200”
strtoul函数原型
unsigned long int strtoul(const char *str, char **endptr, int base)
str:传递要转换的字符串的首地址,例如:“100”
endptr:一律给NULL
base:指定那个进制转换
1.如果base = 0,根据实际数字的形式进行转换
例如:"100"根据10进制进行转换
"0x100"根据16进制进行转换0x100
2.如果给base = 16,强制按照16进制进行转换
例如:"100"根据16进制进行转换0x64
3.如果base = 8,强制按照8进制进行转换
例如:"100"根据16进制进行转换成0144
-
返回值:返回转换完成的整数
-
参考代码:main.c
/*main函数完整的编写框架*/
/*运行
./main 100 200
argc = 3
argv[0] = "./main"首地址
argc[1] = "100"的首地址
argv[2] = "200"的首地址
*/
#include <stdio.h>
#include <stdlib.h>
// int main(int argc, char **argv)
int main(int argc, char *argv[])
{
for(int i = 0; i < argc; i++)
{
printf("argc = %d, argv[%d] = %s\n", argc, i, argv[i]);
}
printf("\n");
if (argc != 3)
{
printf("请重新输入 %s <100><200>\n", argv[0]);
return -1;
}
//将用户输入的字符串转整数
int a = 0,b = 0; // 将来保存用户输入的数字
a = strtoul(argv[1], NULL, 0); // 第一个参数时要转换的字符,第二个参数写NULL就行,第三个参数时几进制,0为根据实际转换输出
b = strtoul(argv[2], NULL, 0);
printf("a = %d, b = %d\n", a, b);
return 0;
}
第十一课:结构体和函数指针的配合专项训练(实际产品开发比用)
参考代码:struct_pfunction.c
/*结构体和函数指针的配合*/
#include <stdio.h>
/*告诉gcc编译器,stu_t别名在下面的代码中实现*/
typedef struct student stu_t;
/*声明函数指针数据类型*/
typedef void (*pfunc_t)(stu_t *);
/*声明结构体*/
typedef struct student
{
char name[30];
int age;
pfunc_t pfunc1;
pfunc_t pfunc2;
} stu_t;
// 定义打印学生信息的函数
void show(stu_t *p)
{
printf("%s %d\n", p->name, p->age);
}
//定义岁数加1函数
void grow(stu_t *p)
{
p->age++;
}
int main(void)
{
//定义初始化结构体变量描述学生信息
stu_t student1 =
{
.name = "小明",
.age = 18,
.pfunc1 = show, // 指向show函数
.pfunc2 = grow // 指向grow函数
};
//调用student1学生的操作函数:show和grow
student1.pfunc1(&student1); // 调用show函数
student1.pfunc2(&student1); // 调用grow函数
student1.pfunc1(&student1); //调用show函数
return 0;
}