C指针学习

指针

    • 第九课:指针(C语言的灵魂)
      • 指针的定义
      • 指针变量定义的语法格式
      • 指针变量初始化通过取地址&来进行
      • 解引用运算符(又称取目标运算符)
      • 特殊指针(空指针NULL和野指针)
      • 指针运算(核心并且坑爹)
      • 指针和数组的那点事儿
      • 指针和函数的那点事儿
      • 指针作为函数的返回值
      • 指针变量
      • 常量,常量指针,指针常量,常量指针常量:围绕关键const
      • 无数据类型指针:void
      • C语言字符串
      • 字符串操作函数
      • 指针数组(实际开发很常用)
      • 字符指针数组
    • 第十课:预处理(核心内容)
      • 回顾C程序编译三步骤
      • 预处理(涉及的指令代码以#开头,后面不跟分号;)分类:
    • 第十一课:大型程序软件基本框架
      • 掌握头文件卫士
      • 实际开发产品代码组成部分:三部分
      • 大型程序的编译靠:Makefile
    • 第十二课:结构体(核心中的核心,实际开发必用)
      • 结构体特点
      • 结构体数据类型和定义的方法
      • 联合体
      • 复合类型:枚举
    • 第十三课:函数指针(核心中的核心)
      • 函数指针概念
      • 函数指针数据类型声明的语法
      • 函数指针变量定义语法格式
      • 函数指针变量的初始化
      • 通过函数指针变量来访问指向的函数,调用函数
      • 函数指针总结
      • 回调函数
      • 函数指针经典代码演示
    • 第十四课:多级指针(掌握到二级指针)
      • 二级指针概念
      • 定义二级指针变量的语法格式
      • 二级指针和字符串的那点事
      • 二级指针和字符指针数组的那点事
      • 实际开发,产品代码的主函数main书写
    • 第十五课:结构体和函数指针配合专项训练(实际产品开发必用)
    • 第十六课:malloc和free标准库函数
      • malloc和free函数特点
      • 详解malloc和free函数
    • 第十七课:文件操作相关库函数
      • 文件操作标准C库函数如下:
      • fopen函数原型
      • fclose 函数原型
      • fwrite函数原型
      • rewind函数原型
      • fread 函数原型
      • fseek函数原型
    • C语言综合演练
      • 案例

第九课:指针(C语言的灵魂)

指针的定义

指针本质就是一个变量,而这个变量永远只能存储一个内存地址(编号)
所以此变量对应的专业术语叫指针变量
通过指针变量保存的地址就可以对这块内存区域任意访问(读查看,写修改)
而指针指向的内存区域可以保存一个数字,而这个数字有数据类型

指针变量定义的语法格式

a)书写形式1
int * 变量名;
例如:int * pa; //定义一个指针变量pa

b)书写形式2
int* 变量名;
例如:int* pa; //定义一个指针变量

c)书写形式3
例如:int *pa; //定义一个指针变量

语义:都是定义一个指针变量pa,将来这个变量pa能够保存一块内存区域的首地址
也就是指针变量pa本身也会分配内存,只是它对应的内存用来存储其他内存区域的首地址
此过程简称pa指向某块内存区域
并且指针变量保存的首地址对应的内存区域保存着一个int类型的数据
也就是数据类型不是给指针变量使用,而是给指针变量指向的内存使用的

切记
指针变量分配的内存空间跟计算机硬件相关:
32位系统,一个地址值为32位,4字节,所以对应的的指针变量内存大小永远4字节
64位系统,一个地址值为64位,8字节,所以对应的的指针变量内存大小永远8字节
所以指针变量没有数据类型一说

例如
char *pa; //定义字符类型的指针变量pa,pa对应的内存为4字节,而pa指向的内存的数据类型
//为char类型,将来它指向的内存只能保存一个char类型的数字,对应的内存也就是1字节

short *pb; //定义字符类型的指针变量pb,pb对应的内存为4字节,而pb指向的内存的数据类型
//为short类型,将来它指向的内存只能保存一个short类型的数字,对应的内存也就是2字节

问:指针变量占用多大的内存空间呢
答:得看保存的地址有多大,这个跟计算机硬件相关:
32位系统,一个地址值32位,4字节
64位系统,一个地址值64位,8字节

结论
指针变量分配的内存空间为4字节或者8字节,所以指针变量本身没有数据类型
只是它指向的内存区域保存的数字有数据类型,所以int不是给指针变量用
而是给指针变量指向的内存区域保存的数字用的

d)连续定义指针变量形式
int *pa, *pb; //定义两个指针变量
int *pa, pb; //pa是指针变量,而pb就是一个普通的int类型变量

e)切记
定义指针变量后如果不初始化,此指针变量保存的一个地址值是随机的也就是此指针变量指向任意内存区域,相当危险,因为此块内存区域 不是操作系统合法给你分配的内存,此指针变量为野指针!

指针变量初始化通过取地址&来进行

int a = 250; //分配4字节内存空间,存储250数字,而这个数字类型为int类型
int *pa = &a; //定义一个指针变量,也就是分配一个4字节内存空间(前提是32位)
保存变量a对应的内存空间的首地址
俗称pa指向a
务必脑子中浮现内存的指向图!
char b = ‘A’; //分配1字节内存空间,存储字符A的ASCII码
int *pb = &b; //定义指针变量分配4字节内存空间,然后保存变量b的首地址 简称pb指向b
注意:指针变量pa和pb 指定的数据类型 int ,char 不是给他们用,而是给a 和 b用的!

问:
一旦通过指针变量来获取到指向的内存区域的首地址,如何通过指针变量
对指向的内存区域为所谓欲呢,也就是对指向的内存区域进行读查看或者写修改呢?

答:
通过解引用运算符:*

解引用运算符(又称取目标运算符)

功能:就是通过指针变量对指向的内存区域进行读查看或者写修改
语法格式:*指针变量 = 取目标
例如
char a = 100;
char *pa = &a;
或者:
char a = 100;
char *pa = NULL;
pa = &a;
//打印pa指向a的内存数据
printf(“%d\n”, *pa); //100
//修改pa指向a的内存数据
*pa = 10; //结果是变量a的内存由原来的100变成10
结论:sizeof(指针变量名) = 4(永远等于4字节)
参考代码: pointer.c

/*指针演示*/
#include

int main(void)
{
//定义初始化字符变量,分配1字节内存
char a ='A';
//定义初始化指针变量pa,pa指向a
char *pa = &a;

printf("&a = %p,pa = %p,&pa= %p\n",&a,pa,&pa);  //打印变量a,指针变量pa和指针变量本身的地址
printf("sizeof(a) = %d,sizeof(pa)=%d\n",sizeof(a),sizeof(pa)); //打印变量a 和指针变量占用内存大小

//通过pa获取a的值
printf("%hhd,%c\n",*pa,*pa);

//通过pa修改a的值
*pa = 'B';
printf("%hhd,%c\n",*pa,*pa);
printf("%hhd,%c\n",a,a);

return 0;

}

特殊指针(空指针NULL和野指针)

空指针保存的地址是0
野指针保存的地址是随机地址,不要有野指针,如果有一定要初始化为空指针
空指针和野指针的编程技巧
int *p; //野指针
int *p = NULL; // 让野指针变空指针安全

一定注意:每次使用指针时,要判断指针是否为空指针
if(NULL == p){ 不合法的地址 }else{合法的地址}

或者
if§{ 合法的地址 }else{不合法的地址}

或者
if(!p){ 不合法的地址} else{合法的地址 }

a)空指针
空指针变量保存一个空地址,用NULL表示,其实就是编号为0地址
空指针不可以随意访问,否则造成程序的崩溃!

例如
int *pa = NULL;
printf(“pa指向的0地址保存的数据为%#x\n”, *pa);
*pa = 250; //向0地址写入数据250

注意
全局指针变量没有初始化同样gcc也赋值一个null空地址

b)野指针
没有初始化的指针变量(局部指针变量),它保存着一个随机地址,指向着一块无效的内存
因为这块内存区操作系统并没有给你分配,如果对野指针进行非法访问
也会造成程序的崩溃!
int *pa; //pa就是野指针
printf(“pa指向的0地址保存的数据为%#x\n”, *pa);
*pa = 250; //向0地址写入数据250

c)切记
实际开发代码的编程规范(公式)
如果定义一个指针变量,一开始不清楚它到底指向谁,千万不能不初始化,否则变成了野指针
所以此时要求初始化为空指针NULL,一旦初始化为NULL,将来程序后面使用时
时时刻刻要记得对指针变量进行安全的判断,判断它是否为NULL,如果为NULL,让程序结束
或者函数返回,如果为有效地址,程序才能继续通过指针变量进行操作

例如

int *pa; //不建议这么写,非常危险
//安全做法:
int *pa = NULL; //赋值为空指针
// 假如这里忘记了给pa赋值,pa还是null,但是好的编码规矩是访问指针之前一定做安全判断;
if(NULL == pa) {
printf("pa指向空指针,不能继续访问.\n");
return -1 或者exit(0); //函数返回或者程序退出
} else {
printf("pa指向一块有效内存,可以继续访问\n");
printf("%d\n", *pa);
*pa = 250;
}

//安全做法:
int *pa = NULL; //初始化为空指针
int a = 250;
pa = &a; //让pa指向a,pa保存变量a的首地址(有效地址)
if(NULL == pa) {
printf("pa指向空指针,不能继续访问.\n");
return -1 或者exit(0); //函数返回或者程序退出
} else {
printf("pa指向一块有效内存,可以继续访问\n");
printf("%d\n", *pa);
*pa = 251;
}

参考代码:pointer.c

/*指针编码规范*/
#include
#include
int main(void)
{
int *pa = NULL;  //安全做法
int a =250;
pa = &a;   // pa指向a,保存a变量的首地址(有效地址)

//一定要进行安全判断
if(NULL == pa){
printf("pa为空指针,不能访问\n");
exit(0);   //立刻结束程序
}else{
printf("pa为有效指针.\n");
*pa = 520;
printf("a = %d\n",*pa);
}

//2. 无指向
int *pb = NULL ;   //安全做法
//一定要进行安全判断
if(NULL == pb){
printf("pb为空指针,不能访问\n");
exit(0);  //立刻结束程序
}else{
printf("pb为有效指针.\n");
*pb = 520;
printf("a = %d\n",*pb);
}
return 0;
}

指针运算(核心并且坑爹)

a)指针可以和一个整数做加减法运算,简称地址运算
切记:计算结果和指针指向的变量数据类型有关系

b)指针计算公式

  1. char/unsigned char型指针+1,表示实际地址+1
    例如:
    char *pa = 0x1000; //假设pa指向0x1000
    pa++; //pa=0x1001

  2. short/unsgined short型指针+1,表示实际地址+2
    short *pa = 0x1000; //假设pa指向0x1000
    pa++; //pa=0x1002

  3. int/unsigned int /long/unsigned long型指针+1,表示实际地址+4
    long *pa = 0x1000; //假设pa指向0x1000
    pa++; //pa=0x1004

参考代码:pointer2.c

/*指针运算*/
#include
int main(void)
{
char a = 'A';
char *pa = &a;
printf("pa = %p\n",pa);
pa++;
printf("pa = %p\n",pa);

unsigned short b = 250;
unsigned short *pb = &b;
printf("pb = %p\n",pb);
pb++;
printf("pb = %p\n",pb);

long c = 520;
long *pc = &c;
printf("pc = %p\n",pc);
pc++;
printf("pc = %p\n",pc);
return 0;
}

指针和数组的那点事儿

a)回顾数组相关内容
定义数组:int a[4] = {‘A’, ‘B’, ‘C’, ‘D’};
内存分布图:参见指针和数组.png

结论

  1. 数组名就是数组的首地址
    也等于第一个元素的首地址,即:a = &a[a] ,数组名本质也是一个指针,只是这个指针保存的地址是
    固定不变,不像指针变量保存的地址可以修改

    例如
    int a[4] = {‘A’, ‘B’, ‘C’, ‘D’};
    int c = 250; //数组名不可修改
    a = &c //万万不行的,gcc报错,这个数组名a不可修改,固定的一个地址// 指针变量可以修改
    int pa = NULL;
    int a =250;
    int b = 520;
    pa = &a ; //pa 指向a
    pa = &b; //改成pa指向b
    &a[4] - &a[1] = 3; // 也就是任意两个元素地址相减得到相隔元素的个数,拿元素个数
    sizeof(数据类型)得到时间地址相差的值

  2. &a[2]就是第2个元素的首地址

  3. a+2也是第2个元素的首地址

  4. *&a[2] 获取第2个元素的值

  5. *(a +2)同样表示获取第2个元素的值

  6. &a[2] - a = a + 2 - a = 2个元素,表示第2个元素和第0个元素之间差2个元素 ,实际的地址差8个字节=2个元素*int

指针和数组关系公式
int a[5] = {1,2,3,4,5};
int *pa = a; //定义指针变量保存整个数组的首地址
此时此刻脑子立马浮现内存图!
第i个元素地址 = &a[i] = &pa[i] = a+i = pa+i
第i个元素值 = a[i] = pa[i] = *(a+i) = *(pa+i)
注意
a++ ; //非法
pa++; //合法
*a++; //非法
pa++; //合法,先算pa,后算pa加1
切记
求数组元素个数公式
sizeof(a) /sizeof(a[0]) = 元素个数; //合法
sizeof(pa) /sizeof(pa[0]); //不合法,因为sizeof(pa) 永远等于4


参考代码pointer_array.c

/*指针和数组公式演示*/
#include
int main(void)
{
int a[] = {1,2,3,4,5};
int *pa = a;   //pa指向a,保存数组的首地址
int len = sizeof(a)/sizeof(a[0]);   //获取元素个数
//int len = sizeof(pa)/sizeof(pa[0]); //错误:sizeof(pa)/sizeof(pa[0]) = 4/4 = 1

//写法1:
//打印元素值
for(int i =0;i<len;i++){
printf("第%d个元素的值是:%d %d %d %d\n",i,a[i],pa[i],*(a+1),*(pa+1));
}

//修改其值,扩大100倍
for(int i =0;i<len;i++) {
pa[i] *=10;
*(pa+i) *=10;
}

//打印元素值
for(int i = 0;i<len;i++){
printf("第%d个元素的值是:%d %d %d %d\n",i,a[i],pa[i],*(a+i),*(pa+i));
}

//写法2:

for(pa = a;pa<a+len;pa++){
printf("元素值是:%d\n",*pa);  //打印每个元素
}
for(pa =a;pa<a+len;){
*pa++ *=10;    //修改元素值
}
for(pa=a;pa<a+len;pa++){
printf("元素值是:%d\n",*pa);   //打印每个元素值
}
return 0;
}

指针和函数的那点事儿

指针作为函数的形参(形参是指针变量)
结果
函数通过形参指针变量可以对指向的内存为所欲为(任意读取其值或者修改其值)
参考代码:swap.c

/*指针作为函数的形参*/
#include
/*定义swap函数*/
void swap(int *pa,int *pb)  //pa = &a,pb =&b;
{
int c = *pa;
*pa = *pb;
*pb = c;
}
/*定义将某个数据的某个bit位清0函数*/
void clear_bit(int *pc,int bit){
*pc &= ~(1<<bit);   //直接对实参变量c的内存进行修改
}
/*定义将某个数据的某个bit位置1函数*/
void set_bit(int *pc,int bit){
*pc |= (1<<bit);    //直接对实参变量c的内存进行修改
}
int main(void)
{
int a =100;
int b =200;
swap(&a, &b);    //调用swap实现数据交换,传递实参变量a和b的首地址给swap函数
printf("a = %d,b = %d\n",a,b);
int c  =0x55;    //01010101
clear_bit(&c,0);  //将变量c的第0位清0
printf("c = %#x\n",c);
set_bit(&c,0);    //将变量c的第0位置1
printf("c = %#x\n");
return 0;
}

指针作为函数的返回值

结果
就是函数返回一个指针,此类函数又称指针函数
指针函数返回的地址对应的变量注意它的类型,

切记
不能返回局部非静态变量地址否则将会返回一个野指针,其他三类变量的地址可以返回
指针函数声明和定义的格式:指针变量 = 返回值数据类型 *函数名(形参表){函数体语句}
参考代码:pointer_function.c

/*指针函数演示*/
#include

int g_a =520;   //全局非静态变量
static int g_b =520;  //全局静态变量

/*定义指针函数A,将来返回一个int类型的指针*/
int *A(void){
int a = 250;          //局部非静态变量
static int b  = 250;   //局部静态变量
//return &a;    //返回局部非静态变量地址,不可以,将来会返回一个野指针
//return &b;      //返回局部静态变量地址,可以
//return &g_a;      //返回全局非静态变量的地址,可以
return &g_b;        //返回全局静态变量的地址,可以
}
int main(void)
{
int *p = NULL;
p = A();    //调用A函数,然后用p来接收报存A函数返回的变量地址
printf("%d\n",*p);
printf("%p\n",p);
*p = 30000;   //修改指向的内存值,如果对野指针进行非法访问,直接崩溃
printf("%d\n",*p);

return 0;
}


指针变量

本质就是一个变量,只能存地址(32位,4字节/64位,8字节)
俗称指向一块内存区域
利用指针变量可以对指向的内存进行读查看,写修改

  1. 指针变量定义语法格式
    int * p或者int* p或者int *p;
    int *p1, *p2, …, *pn;

  2. 指针变量的初始化
    int a = 250;
    int *p = &a;
    立刻浮现一个内存的分布图!
    或者
    p = &a; //p指向a
    int b = 520;
    p = &b; //p由指向a变成执行b

  3. 通过指针变量来访问指向的内存区域:&和*
    *p; //读查看
    *p = 新值; //写修改


常量,常量指针,指针常量,常量指针常量:围绕关键const

(笔试题必考)
a)常量定义
不可修改的值,例如:250,'A’等
b)const关键字功能:常量化,四种形式:

  1. const可以修饰普通变量,一旦修饰该变量就会被当成常量处理
    一句话: 即其值一经初始化再也不能改
    例如:
    const int a = 250;
    a = 200; //gcc编译时会报错
    参考代码:const.c
   /*const关键字演示*/
   #include
   int main(void)
   {
      //形式1:普通变量常量化
      const int a =250;
      printf("a = %d\n",a);   //打印查看
      //a = 200;  //gcc报错
   }     
  
  1. 常量指针 (最常用)
    不能通过指针变量来修改指向的内存区域的值(保护内存区域不可乱改),但是指针变量保存的地址是可以修改
    例如:
    int a = 250;
    const int *p = &a; //定义初始化一个常量指针

    或者
    int const *p = &a; //定义初始化一个常量指针
    *p = 200; //gcc编译时会报错
    printf(“%d\n”, *p); //可以,仅仅是读查看
    int b = 300;
    p = &b; //可以,让p由原来指向a,保存a变量的首地址现在指向b变量保存b变量的首地址
    *p = 400; //gcc编译时会报错
    printf(“%d\n”, *p); //可以,仅仅是读查看
    用途
    主要保护指向的内存区域的值不被非法的,无意的篡改

   /*const关键字演示*/
   #include
   int main(void){
       //形式2 :常量指针
       int b = 520;
       const int *p = &b ; //定义初始化常量指针
   printf("%d\n",*p);  //可以
   
   //*p = 250;     //gcc 报错
   int c = 255;
   p = &c;    //可以,p重新指向c
   printf("c = %d\n",*p);  //可以
   //*p = 266;   //gcc报错
   
   return 0;
   }
  
  1. 指针常量
    指针永远指向一块内存区域,不能再指向别的内存,但是指向的内存去域的值可以修改
    例如
    int a = 100;
    int* const p = &a; //定义初始化指针常量,p永远只能保存变量a的地址
    *p = 300; //可以,可以修改指向的内存区域
    int b = 200;
    p = &b; //不可以,gcc报错
 参考代码:
   /*const关键字演示*/
   #include
   int main(void){
     //形式3:指针常量
     int d = 100;
     int* const p1 = &d;   //p只能保存变量d的地址
     *p1  =200;  //可以
     printf("d = %d\n",d);
     int e =300;
     int e =300;
     //p1 = &e;  //不可以,gcc报错
     return 0;
   }
  1. 常量指针常量 :
    指针变量本身不可修改并且指向的内容也不可修改,只能通过p来查看内存区域的值
    const int * const p;
    例如
    int a = 100;
    const int* const p = &a;
    *p = 300; //不可以,可以修改指向的内存区域
    int b = 200;
    p = &b; //不可以,gcc报错
    printf("%d\n", &p); //可以
    /*const关键字演示*/
    #include
    int main(void)
    {
        //形式1:普通变量常量化
        const int a =250;
        printf("a = %d\n",a);   //打印查看
        //a = 200;  //gcc报错
        
        //形式2 :常量指针(最常用)
        int b = 520;
        const int *p = &b ; //定义初始化常量指针
        printf("%d\n",*p);  //可以
        //*p = 250;     //gcc 报错
        int c = 255;
        p = &c;    //可以,p重新指向c
        printf("c = %d\n",*p);  //可以
        //*p = 266;   //gcc报错
    
        //形式3:指针常量
        int e =300;
        //p1 = &e;  //不可以,gcc报错
        
        //形式4:常量指针常量
        int f = 400;
        const int* const p2 =&f;
        //*p2 = 40000;   //不可以
        //p2 =&d ;   //不可以
        printf("f = %d\n",*p2);  //可以
        return 0;  
    }
    


无数据类型指针:void

a) 无数据类型指针(void *)
概念
它也是一个指针变量,也保存一个地址,同样占4字节内存空间
只是它指向的内存区域的数据类型是不可知的, 不确定的
称这种指针为无数据类型指针,写法:void *
例如:
void *p = &a;

b) 无类型指针特点

  1. 通过此种指针变量是无法获知指向的内存区域保存的数据的数据类型

  2. 不能直接对无类型指针进行解引用操作"*",因为你不知道指向的内存区域的数据类型
    也就不知道将来要取几个字节的数据,如果要想通过无类型指针获取内存的数据
    必须做数据类型的转换(为了代码的可读性建议采用强制转换)

    int a = 100;
    void *p = &a; //p仅仅指向a
    *p = 200; //通过p修改内存,gcc报错
    //应该这么做
    int *p1 = NULL;
    p1 = (int *)p; //将p 强制类型转为int *指针
    *p1 = 250; //修改4字节数据
    // 或者直接转换使用:
    *(int *)p =250; //直接将p转换为int类型指针,然后对内存进行操作

    参考代码 void_pointer.c

    /*无类型指针void *演示*/
    #include
    int main(void)
    {      
    int a =100;
    void *p = &a;  //p指向a
    // printf("a = %d\n",*p) //通过p无法获知要读取几个字节数据,gcc迷茫了
    //解决办法1 :间接
    //int *p1 =p; //隐式转换,不建议
    int *p1 = (int *)p;   //强制转换,将无类型指针转换成int类型指针,p1也指向变量a
    *p1 = 200;
    printf("a = %d\n",*p1);
    
    //解决办法2:直接
    * (int *)p = 300;  //直接将无类型指针进行强制转换然后解引用
    printf("a = %d\n",*(int *)p);
    return 0;
    }
    
    1. 无类型指针void *加减几,实际的地址就是加减几
      void *p = 0x1000; //假设p保存0x1000地址
      p++; //p=0x1001
      p++; //p=0x1002

    2. 指针综合演练(高级进阶)
      例如:
      int a = 0x12345678; //连续分配4字节内存,并且4字节内存放置的数据是 0x12345678
      void *p = &a; //虽然p指向a,但是通过p无法获取a变量的数据类型
      *p = 300; //gcc编译报错,不清楚到底取几个自己的数据,没办法只能给你报错

      问:如何通过一个指针来获取a的4字节数据或者其中任意1字节数据或者其中任意2字节数据呢?
      答:通过两种方法
      方式1:通过有类型的指针变量来获取1字节,2字节,4字节
      int a = 0x12345678;
      //char *p = &a; //隐式转换,代码可读性不高,gcc还要给个警告
      char *p = (char )&a; //强制转换,提高代码的可读性,将a变量的指针类型由int类型转换成char

      //获取其中1字节数据
      printf(“%#x\n”, *p++); //0x78
      printf(“%#x\n”, *p++); //0x56
      printf(“%#x\n”, *p++); //0x34
      printf(“%#x\n”, *p++); //0x12

      //获取2字节:
      int a = 0x12345678;
      //short *p = &a; //隐式转换,代码可读性不高 gcc还要给个警告
      short *p = ( short )&a; //强制转换,将&a的int类型转换成short
      printf(“%#x\n”, *p++); //0x5678
      printf(“%#x\n”, *p++); //0x1234

      //获取4字节数据
      int *p = &a;
      printf(“%#x\n”,*p); //0x12345678

      参考代码 pointer3.c

      /*利用有指针对内存为所欲为*/
      #include
      int main(void)
      {
      int a = 0x12345678;
      
      //1. 通过指针获取其中1字节数据
      char *p1 =(char *)&a;  //将int类型指针强转为char类型指针,否则采用隐士转换,p1保存变量a的首地址
      //打印
      printf("%#x\n",*p1++);
      printf("%#x\n",*p1++);
      printf("%#x\n",*p1++);
      printf("%#x\n",*p1++);
      
      //2.通过指针获取其中2字节数据
      short *p2 = (short *)&a;  //将int类型指针强转为short类型指针,否则采用隐士转换,p2保存变量a的首地址
      //打印
      printf("%#x\n",*p2++);
      printf("%#x\n",*p2++);
      
      //3.通过指针获取其中4字节数据
      int *p3 =&a;
      //打印
      printf("%#x\n",*p3);
      return 0;
      }
      

      方式2:通过无类型指针变量来访问操作
      例如:

      int a = 0x12345678;
      void *p = &a; //无需转换,p指向a
      //获取1字节数据
      char *p1 = (char *)p; //将p强制类型转换成char 类型指针
      printf(“%#x\n”, *p1++); //0x78
      printf(“%#x\n”, *p1++); //0x56
      printf(“%#x\n”, *p1++); //0x34
      printf(“%#x\n”, *p1++); //0x12

      // 获取2字节数据
      short *p2 = (short *)p; //将p强制类型转换成short类型指针
      printf(“%#x\n”, *p2++); //0x5678
      printf(“%#x\n”, *p2++); //0x1234

      或者直接转换使用
      printf(“%#x\n”,*(short *)(p+0)); //先算p+0 ,然后对结果进行强转为short *指针,最后解引用
      printf(“%#x\n”,
      (short *)(p+2));

      //获取4字节数据
      int *p3 = (int *)p; //将p强制类型转换成int类型指针
      printf(“%#x\n”,n1);
      或者直接操作无类型指针
      printf(“%#x\n”,
      (int *)(p+0)); //先算p+0 ,然后对结果进行强转为int *指针,最后解引用 *

      结论
      此方法虽然满足要求,但是如果做隐式转换,gcc老给一个警告
      问:能否将警告去掉呢?还有能够去掉指针变量++时跟类型相关问题呢?

      通过无类型指针void *实现


C语言字符串

  1. 回顾字符常量
    用单引号包含,例如:‘A’,‘B’,'1’等
    实际内存存储的是对应的整型ASCII码
    占位符:%c

  2. 字符串定义
    由一组连续的字符组成,并且用""包含起来,并且最后一个字符必须是’\0’ 此’\0’表示字符串的结束,此’\0’的ASCII码是0
    ‘0’的ASCII为48
    注意:研究字符串最终研究的就是里面的每个字符
    例如:“abcefg\0”(由字符’a’,‘b’,‘c’,‘d’m’\0’一个挨着一个组成)
    一般简写成:“abcd”(简化版,心里清楚,后面还有一个’\0’)

  3. 字符串特点
    a) 字符串的占位符:%s
    printf(“%s\n”, “abc\0”); //直接跟字符串
    或者
    printf(“%s\n”, 字符串的首地址);

    b) 字符串占用的内存空间是连续的,并且每个字节存储一个字符
    参见字符串内存图.png
    注意:'\0’字符串的结束符如果后面还有内容,那么这些内容为无效的
    例如:“abc\0efg\0”
    printf(“%s\n”, “abc\0efg”); //abc

    c) 多个并列的字符串将来会由gcc帮你合并成一个字符串
    “abc”“efg"合并成"abcefg”
    printf(“abc”“efg\n”); //abcefg
    参考代码str.c

/*字符串演示?*/
#include
int main(void)
{
//1. 打印字符串
printf("%s\n","abcd\0");   //打印abcd
printf("%s\n","abcd");     //打印abcd
printf("%s\n","hjkl\0abcd"); //hjkl

//字符串合并
printf("abc""efg\n");  //等价于printf("abcdef\n");
return 0;
}
  1. 字符串和指针的那点事儿
    a) 定义一个字符指针变量并且指向一个字符串,本质是指向这个字符串的首地址
    也就是指向字符串中第0个字符的首地址(也就是字符a的首地址)
    定义并且初始化字符串指针变量形式:
    char *p = “abcefg”; //p指向字符串"abcd"的首地址
    通过字符串指针变了打印字符串:printf(“%s\n”,p); //abcd

    b) 如果让一个字符指针变量指向一个字符串,此字符串无需跟’\0’,gcc将来帮你添加’\0’
    完整版:char *p = “abcd\0”;
    简化版:char *p = “abcd”; //将来gcc编译自动会给字符串添加’\0’,心里要清楚内存要用5字节

    d)切记(必考)
    不能通过字符指针变量来修改字符串的每个字符,只能查看(笔试题必考)
    因为将来gcc编译器自动将字符串单独放到一个所谓的常量区中(一块特殊的内存,只能看)
    例如
    char *p =“abcd”;
    printf(“%s\n”,p); //查看
    *(p+2) = ‘C’ ; //目标将其中的’c’变 ‘C’,不行报错
    参考代码

    /*字符串演示?*/
    #include
    int main(void)
    {
    //1. 打印字符串
    printf("%s\n","abcd\0");   //打印abcd
    printf("%s\n","abcd");     //打印abcd
    printf("%s\n","hjkl\0abcd"); //hjkl
    
    //字符串合并
    printf("abc""efg\n");  //等价于printf("abcdef\n");
    
    //3.指针形式字符串
    char *p ="abcdefg111";
    printf("%s\n",p);   //打印字符串
    *(p+2) = 'C';      //修改其中的字符,程序崩溃
    return 0;
    
    }
    

  2. 字符串和数组的那点事儿,两种写法:
    a)写法1
    char a[] = {‘a’, ‘b’, ‘c’, ‘\0’};
    注意:如果想把a当成字符串,需要手动最后添加’\0’,如果不添加a仅仅就是一个包含三个元素
    的数组,而不是有效字符串

    b)写法2
    char a[] = “abc”;
    注意:无需添加’\0’,将来编译器自动追加’\0’,所以对于次数组将来实际分配4字节内存

    c)切记
    不管是哪种写法,字符数组中的元素都是可以修改的(笔试题必考)
    例如
    将第2元素’c’修改为’C’:
    a[2] = ‘C’;
    或者
    *(a + 2) = ‘C’;

  3. 笔试题必考题目
    编写一个字符串比较函数my_strcmp
    参考代码:strcmp.c

    /*字符串演示·*/
    #include
    int main(void)
    {
    //4.数组形式的字符串
    char a[] = {'x','y','z','\0'};
    printf("%s\n",a);   //查看
    a[2] = 'Z';  //修改
    printf("%s\n",a);
    char b[] = "mnab";
    printf("%s\n",b);
    b[2] ='A';
    printf("%s\n",b);
    printf("sizeof(b)=%d\n",sizeof(b));   //5字节,gcc自动追加一个'\0'
    return 0;
    }
    

字符串操作函数

坚定信念
自己实现一个字符串操作函数完全没问题!
如果要用以下大神写好的字符串操作函数,需要添加头文件:#include

  1. strlen函数
    功能获取字符串有效长度(不包括’\0’)
    例如:printf(“长度:%d\n”, strlen(“abc”)); //3
    或者
    char *p = “abc”;
    printf(“长度:%d\n”, strlen§); //3

  2. strcat函数:功能是字符串拼接
    例如:
    char a[10] = “abc”;
    char *p = NULL;
    p = strcat(a, “xyz”); //把xyz拼接在abc的后面保存到数组中
    printf(“%s %s\n”, p, a); //abcxyz abcxyz

  3. strcmp函数:功能是字符比较函数
    例如:
    char *p1 = “abc”;
    char *p2 = “xyz”;
    int ret = strcmp(p1, p2);
    int ret = strcmp(“abc”, “xyz”); //本质最终传递的是字符串的首地址

  4. strcpy:字符串拷贝函数,会覆盖原先的字符串
    例如:
    char a[10] = “abc”;
    char *p = NULL;
    p = strcpy(a, “xyzmn”);
    printf(“%s %s\n”, p, a);

  5. sprintf 格式化输出函数:按照指定的个数获取字符串
    例:功能把数字(250)转成字符串"250"保存到数组中
    例如:
    char a[50] = {0};
    sprintf(a, “%d %g, %c”, 250, 250.2, ‘A’);
    printf(“%s”, a);

    参考代码 string1.c

 /*字符串操作函数演示*/
 #include
 #include  //为了声明大神的字符串操作函数
 
 int main(void)
 {
 char *p1 = "abc";
 printf("%d %d\n",strlen("abcd"),strlen(p1));  //获取有效字符个数
 
 //2. strcat 演示
 char a[10] = "abc";   //一次性分配10字节,目前用了3个字节,其余都是0
 char *p2 = NULL;
 p2 = strcat(a,"xyz");  //将"xyz"放到数组a中并且放到"abc"后面,并且返回数组的首地址,其实p2 = a
 printf("%s %s\n",a,p2);    //打印字符串
 printf("%s %s\n",a,p2);    //打印字符串
 printf("%d %d\n",sizeof(a),strlen(a));  //10 6
 
 //3. strcmp演示
 int ret =0;
 ret =strcmp("abc","abd");     //"abc"小于 "abd" :ret =-1
 printf("%d\n",ret);
 ret = strcmp("abd","abc");   //"abd" 大于 "abc": ret =1
 printf("%d\n",ret);
 ret = strcmp("abc","abc");   //"abc" 等于 "abc" : ret =0
 printf("%d\n",ret);
 char *p3 ="abc";
 char *p4 = "abd";
 ret = strcmp(p3,p4);   //p3
 printf("%d\n",ret);
 
 //strcpy演示
 char b[10] = "abc";
 char *p5 =NULL;
 p5 = strcpy(b,"xyzmn");  //将字符串xyzmn拷贝到数组b中(覆盖了abc),返回数组的首地址给p5,p5 = b
 printf("%s %s\n",b,p5); //打印字符串
 char *p6 = "hello";
 p5 = strcpy(b,p6);    //将字符串hello 拷贝到数组中
 printf("%s %s\n",b,p5);   //打印串
 
 //sprintf:格式化输入函数,按照指定的个数获取字符串
 char c[50] = {0};
 sprintf(c,"%d %g %c %#x",250,250.2,'A',250);  //将这些数字按照占位符的格式转换成字符串保存到C中
 printf("%s\n",c);
 return 0;
 }



指针数组(实际开发很常用)

a) 指针数组概念:数组中每个元素都是一个指针(地址)
元素只能是地址,不能是普通的数据
指定数组定义语法格式:
数据类型 *数组名[元素个数] = {地址列表};
例如

int a =10 ,b =20,c=30;
//以前做法:定义三个指针变量分别指向a,b,c
int *pa = &a;
int *pb = &b;
int *pb = &c;

如果定义大量的变量和对应的指针变量,代码极其啰嗦!
可以采用指针数组优化代码:

例如
int *p[3] = {&a,&b,&c} ; //无需定义大量指针变量
具体后续玩法跟数组一模一样:

结果
p[0] = &a = *(p+0)
p[1] = &b = *(p+1)
p[2] = &c = *(p+1)

通过地址获取变量的值:
*p[0] = *&a = **(p+0) = 10
*p[0] = *&a = **(p+0) = 20
*p[0] = *&a = **(p+0) = 30

元素个数 = sizeof§ /sizeof(p[0])


c) 应用场景
如果将来需要定义大量的一堆堆的指针变量,反而让代码看起来极其繁琐
采用指针数组来进行统一,类似数组来替换大量的变量
现在用指针数组替换大量的指针变量
例如
int a = 10, b = 20, c = 30, d = 40, e = 50; …
int *pa = &a, *pb = &b, *pc = &c, *pd = &d, *pe = &e; …
相当累啊,我好难啊,烦死了,想起指针数组来优化:
int *p = {&a, &b, &c, &d, &e …};
参考代码:array.c

/*指针数组演示*/
#include
int main(void)
{
int a =10,b = 20,c = 30;
int *p[3] = {&a,&b,&c};
int len = sizeof(p)/sizeof(p[0]);

//打印值
for(int i = 0;i<len;i++){
printf("%d %d\n",*p[i],**(p+i));    //p:数组首地址,p+1:第1个元素首地址,*(p+1):取出第一个元素值&b
//**(9+i) = *&b = 20
}

//修改值
for (int i = 0;i<len;i++){
*p[i] *=10;
**(p+i) *=10;
}

//打印
for(int i = 0;i<len;i++){
printf("%d %d\n",*p[i],**(p+i));
}
return 0;
}

字符指针数组

特殊的指针数组:字符指针数组

a)概念
字符指针数组是特殊的指针数组,每个元素是一个字符串的首地址
两种形式:
例如:
char *p[] = {“abc”, “efg”}; //第0个元素是字符串"abc"的首地址,第1个元素是字符串’efg’的首地址
//指针数组中存在的不是字符,一定是字符串的首地址
等价于
char *p1 = “abc”;
char *p2 = “efg”;
char *p[] = {p1, p2};

结果:
p[0] = p1 = “abc”
p[1] = p2 = “efg”;

b) 应用场景
需要用到大量字符串指针变量此种情形!
参考代码 array1.c

/*字符指针数组演示*/
#include
int main(void)
{
//形式1:
char *p[] = {"abc","efg"};
int len = sizeof(p)/sizeof(p[0]);

//打印
for(int i = 0;i<len;i++){
       printf("%s %s\n",p[i],*(p+i));
}

//不可修改
//*(p[0]+1) = 'B';   //将字符串abc中b变B
//*(p[1]+2) = 'G';   //将字符串efg中g变G

//形式2:
char *p1 = "abcd";
char *p2 = "efgh";
char *p3[] = {p1,p2};

//打印
for(int i= 0;i<len;i++)
     printf("%s %s\n",p3[i],*(p3+i));

return 0;
}

第十课:预处理(核心内容)

回顾C程序编译三步骤

  • 预处理:gcc -E -o xxx.i xxx.c //替换,拷贝过程
  • 只编译不链接:gcc -c -o xxx.o xxx.i/xxx.c //将预处理之后的源文件单独翻译成CPU能够识别的可执行文件
  • 链接:gcc -o xxx xxx.o //将单独编译完成的可执行文件添加函数相关代码(printf,strcmp,strcpy),生成最终的可执行文件

预处理(涉及的指令代码以#开头,后面不跟分号;)分类:

  1. 头文件包含预处理指令:#include,又分两类:
    #include <头文件>
    语义:在预处理是,gcc自动到操作系统/usr/include目录下找要包含的头文件,如果找到了,将头文件里面的所有内容拷贝到源文件中
    #include “头文件”
    语义:在预处理时,gcc先到当前目录下找包含的头文件,如果找到了那就拷贝,如果没有找到再去/usr/include
    目录下找要包含的头文件
    特殊
    如果包含的头文件不在当前目录下,也不在/usr/include目录下,头文件可能在别的目录下
    答:可以通过-I 选项指定头文件所在的路径(核心,开发常用)
    编译命令格式:gcc … xxx.i xxx.c -I 头文件所在的路径
    例:
    gcc -o A A.c -I /home/devops
    gcc -E -o xxx.i xxx.c -I /home/tarena/stdc/day10/


  2. 宏定义预处理指令: #define
    - 建议:宏的名称用大写
    - 宏定义指令又分两类:常量宏 和 参数宏(又称宏函数)

    1. 常量宏
      a)语法格式:#define 宏名 (值)
      例如:#define PI (3.14)
      语义:定义宏PI,其值为3.14
      效果:将来gcc会将程序中所有的宏PI替换成3.14
      建议:宏的名称用大写
      优点:提高代码的可移植性,让代码将来改起来方便

      案例:利用宏实现计算圆的周长和面积
      参考代码:circle.c
      编译命令:gcc -E -o circle.i circle.c //此时gcc将PI替换成3.14
      vim circle.i //跳转到文件的最后看你的代码是否发生了替换
      gcc -o circle circle.i //直接编译
      ./circle
      参考代码circle.c

    /*常量宏演示*/
    /*操作命令
    gcc -E -o circle.i circle.c
    vim circle.i //打开预处理之后跳转到最后观察是否替换
    gcc -o circle circle.c
    ./circle
    */
    #include
    /*定义宏*/
    #define PI  (3.14)
    
    int main(void)
    {
        double r =10;  //半径
    
        //优秀代码
        printf("周长:%lf\n",2*PI*r);
        printf("面积:%lf\n",PI*r*r);
        
        //垃圾代码:如果将来需要精确改动圆周率,改动量很大
        printf("周长:%lf\n",2*3.14*r);
        printf("周长:%lf\n",2*3.14*r);
        return 0;
    }
    

    1. 参数宏(又称宏函数)

      1. 语法格式:#define 宏名(宏参数) (宏值)
        注意:宏值里面的宏参数不要少圆括号
        如果宏参数有多个,用逗号来分区
        例如:#define SQUARE(x) ((x)*(x))
        #define SUB(x, y) ((x) - (y))
        语义:在预处理时,gcc先将宏参数替换成实际值,然后将代码中的宏名最终全部替换成宏值
        注意:宏值里面的宏参数不要忘记圆括号()
        优点:宏函数比普通函数的代码执行效率要高
        宏函数将来在预处理是做了替换,程序运行时直接运行
        宏函数涉及调用,传参(赋值)的过程,这个过程是需要消耗CPU资源

      案例
      宏函数演练,参考代码define.c
      编译好习惯:gcc -E -o define.i define.c
      vim define.i //好习惯看看替换的结果
      gcc -o define define.i

      利用宏函数实现数的平方

    /*宏函数演示*/
    #include
    /*定义宏函数SQUARE:求平方*/
    #define SQUARE(x)  ((x)*(x))
    
    /*定义宏函数SUB: 求两个数相减*/
    #define SUB(x,y)  ((x)-(y))
    
    /*清0 置1*/
    #define CLEAR_BIT(data,n)  (data &=~(1<<n))
    #define SET_BIT(data,n)   (data |=(1<<n))
    
    int main(void)
    {
    printf("%d\n",SQUARE(10));
    printf("%d\n",SQUARE(3+7));
    printf("%d\n",SUB(200,100));
    
    int a = 0x55,n=0;
    CLEAR_BIT(a,n);
    printf("a = %#x\n",a);
    SET_BIT(a,n);
    printf("a = %#x\n",a);
    
    return 0;
    }
    

    1. #(转字符串指令)和##(粘贴指令)
      1. #作用:将后面跟的宏参数转换为字符串
        例如:#N替换成了"N"
        参考代码:define.c

      2. ##作用:将其后面的宏参数进行替换然后与前面的部分粘连在一起,最终作为宏值替换
        例如:id##N替换成 id1
        参考代码:define.c

    /*宏函数演示*/
    #include
    /*定义宏函数PRINT*/
    #define PRINT(N)    (printf(#N"=%d\n",N))
    int main(void)
    {
    int b = 10,c =20;
    PRINT(b);    //printf("b""=%d\n",b) 等价于printf("b= %d\n",b);
    PRINT(c);
    return 0;
    }
    

    1. 编译器已经定义好的宏(直接使用,无需#define),实际开发非常常用!主要用于调试,log日志记录
      注意:都是两个下划线
    含义 占位符
    FILE 表示当前文件名 %s
    LINE 表示当前行号 %d
    FUNCTION 或者__func__ 表示当前所在的函数名 %s
    DATE 文件创建的日期 %s
    TIME 文件创建的时间 %s

    实际开发日志使用公式
    printf(“代码的这里错误:%s, %s, %s, %s, %d,出现了一个野指针的访问错误.\n”,
    DATE, TIME, FILE, FUNCTION, LINE);
    参考代码 define.c

    /*宏函数演示*/
    #include
    printf("代码的这里错误:%s, %s, %s, %s, %d,出现了一个野指针的访问错误.\n",
    __DATE__, __TIME__, __FILE__, __FUNCTION__, __LINE__);
    return 0;
    }
    

    1. 用户可以动态预定义宏 : 通过gcc的-D选项来指定宏
      作用:程序在编译的时候将-D选项指定宏给程序传递一个值
      注意:如果宏是一个字符串,那么-D后面的宏值需要用"说明
      例如
      gcc -DSIZE=250 -DWELCOME=“大神来了”
      结果代码中可以直接使用SIZE和WELCOME宏,并且他们的值分别是250和"达内"
      代码使用: printf(“%d %s\n”, SIZE, WELCOME);
      参考代码 define1.c
    /*预定义宏演示*/
    //编译命令:gcc -DSIZE=5 -DEND=\"很开心\" -o define1 define1.c
    #include
    int main(void)
    {
    int a[SIZE] = {0};
    
    //赋值
    for(int i= 0;i<SIZE;i++)
    a[i] = i+1;
    
    //打印
    for (int i = 0; i<SIZE;i++)
    printf("a[%d] = %d\n",i,a[i]);
    
    printf("%s\n",END);
    }
    

    1. 条件编译命令 (大型软件代码用的非常多)
      条件编译
      符合条件的,代码就编译,不符合条件的,代码不编译,让代码删除消失

      #if //如果,例如:#if A==1
      #ifdef //如果定义了…
      #ifndef //如果没有定义 …
      #elif //否则如果
      #else //否则
      #endif //和#if,#ifdef,#ifndef配对使用
      #undef //取消定义,和#define死对头,例如:#define PI (3.14) #undef PI
      参考代码:if.c

    /*条件编译预处理指令演示*/
    #include
    int main(void)
    {
    //编译命令:gcc -E -o if.i if.c ; vim if.i 查看结果; gcc -o if if.i ./if
    //编译命令:gcc -E -DA=1 -o if.i if.c ; vim if.i 查看结果; gcc -o if if.i ./if
    #if A==1  //如果A ==1,条件成立,printf代码将被编译,否则不编译(类型删除代码)
                    printf("1.\n");
    #endif  //跟#if配对
    
    //#if ... #else 演示
    //编译命令:gcc -E -o if.i if.c ; vim if.i 查看结果; gcc -o if if.i ./if
    //编译命令:gcc -E -DB=1 -o if.i if.c ; vim if.i 查看结果; gcc -o if if.i ./if
    #if B==1  //如果B等于1条件成立编译printf2 ,否则编译printf3
                   printf("2.\n");
    #else
                  printf("3.\n");
    #endif //跟#if配对
    
    //#ifdef /#dendef ...#else演示
    //编译命令:gcc -E -o if.i if.c ; vim if.i 查看结果; gcc -o if if.i ./if
    //编译命令:gcc -E -DC -o if.i if.c ; vim if.i 查看结果; gcc -o if if.i ./if
    #ifndef C
    //#ifdef C //如果定义C宏,编译print4,否则编译printf5
                 printf("4.\n");
    #else //可以不加
                 printf("5.\n");
    #endif //跟#ifdef / #ifndef配对
    
    //if defined ...#else 演示
    //编译命令:gcc -E -o if.i if.c ; vim if.i 查看结果; gcc -o if if.i ./if
    //编译命令:gcc -E -DD -E -o if.i if.c ; vim if.i 查看结果; gcc -o if if.i ./if
    //编译命令:gcc -E -DE -E -o if.i if.c ; vim if.i 查看结果; gcc -o if if.i ./if
    //编译命令:gcc -E -DD -DE -o if.i if.c ; vim if.i 查看结果; gcc -o if if.i ./if
    
    #if defined(D)
                  printf("6.\n");
    #elif !defined(DA) && !defined(E)
                 printf("7.\n");
    #else
                printf("8.\n");
    #endif
    return 0;
    }
    

    实际开发的演示代码
    利用条件编译让一套代码支持不同的CPU(X86,ARM,POWERPC,DSP,FPGA等),也不会增大代码的体积
    否则要分别给每类cpu都写一套代码,非常繁琐!

    vim A(void)

    {
    #if  ARCH==X86
    只编译X86相关代码
    #elif ARCH==ARM
    只编译ARM相关代码
    #elif ARCH ==POWERPC
    只编译POWERPC代码
    #elif ARCH ==DSP
    只编译DSP代码
    #else
    只编译FPGA代码
    #endif
    ...
    }
    

    编译生成 ARM的CPU代码:gcc -DARCH=ARM xxx xxx.c



第十一课:大型程序软件基本框架

掌握头文件卫士

切记
编写任何头文件(以.h结尾) 代码框架必须如下:

vim A.h 添加
#ifndef __A_H //此乃头文件卫士也,宏名一般跟头文件同名
#define __A_H
这里添加头文件要包含的代码内容
#endif //跟 #ifndef 配对
头文件卫士作用:防止头文件内容重复定义

b) 问:头文件卫士如何保护呢?
:分析原理(了解即可)
例如
//没有头文件卫士的情形:
vim a.h 添加
int a = 250; //定义变量
保存退出
vim b.h 添加
#include “a.h”
保存退出

vim main.c 添加

#include 
#include "a.h"
#include "b.h"
int main(void)
{
printf("%d\n", a);
return 0;
}

保存退出
gcc -E -o main.i main.c
vim main.i
结果:
int a = 250; //重复定义了
int a = 250; //重复定义了
int main(void)
{
printf(“%d\n”, a);
return 0;
}

问:如何解决?
答:添加头文件卫士
//添加头文件卫士情形:
vim a.h 添加
#ifndef __A_H
#define __A_H
int a = 250; //定义变量
#endif
保存退出

vim b.h 添加

#ifndef __B_H
#define __B_H
#include "a.h"
#endif

保存退出
vim main.c 添加

#include 
#include "a.h"
#include "b.h"
int main(void)
{
printf("%d\n", a);
return 0;
}

保存退出
gcc -E -o main.i main.c

vim main.i
结果:
int a = 250;
int main(void)
{
printf(“%d\n”, a);
return 0;
}

预处理时展开:main.c
//#include “a.h” //替换
#ifndef __A_H //一开始没有定义__A_H,条件成立
#define __A_H //没有定义__A_H,那么就定义__A_H
int a = 250; //条件成立,定义变量
#endif
//#include “b.h” //替换
#ifndef __B_H //一开始没有定义__B_H
#define __B_H //没有定义__B_H,那么就定义__B_H
//#include “a.h” //继续替换
#ifndef __A_H //前面已经定义了__A_H.所以后面两条语句消失,后续diam不编译,这样就不会跟前面的a重 复定义
#define __A_H
int a = 250; //条件不成立就不编译
#endif
#endif



实际开发产品代码组成部分:三部分

a) 代码开发原则
明确:实际的产品代码不可能一个源文件搞定,代码维护极其繁琐郁闷
实际产品代码根据按照功能将一个源文件分拆成多个源文件来实现,提高开发的效率和可维护性

b) 实际产品代码分拆三部分
头文件(.h)、源文件(.c)、主文件(.c)

  1. 头文件(.h):负责各种声明,例如:负责变量的声明和函数的声明
    变量的声明语法:extern 数据类型 全局变量名
    函数的声明语法:extern 返回数据类型/void 函数名(形参表/void)
    作用:将来别的源文件只需包含头文件即可访问变量和函数
    例如:根据动物的类型描述:鸡 、狗 、鸭,对应头文件分别是:dog.h,duck.h,chick.h

  2. 源文件(.c):负责各种定义,例如:变量的定义和函数的定义
    例如:根据动物的类型,dog.c描述狗,duck.c描述鸭子,chick.c描述鸡

  3. 主文件(.c):负责实现main函数,程序运行的第一个文件就是主文件,起到了统领的作用,统领各种源文件,也就是在main函数中
    各种调用访问源文件的变量和函数
    例如:main.c

建议:一个源文件对应一个头文件,并且源文件包含自己对应的头文件 例如:dog.c 包含dog.h 等
建议:如果头文件中有相同的内容,建议把相同的内容摘出来之后放到一个公共的头文件中将来其他头文件只需包含功能的头文件即可
vim common.h
两只眼
vim dog.h
#include “common.h” //包含公共头文件
vim duck.h
#include “common.h” //包含公共头文件

注意:static
案例:实现加,减,清0,置1
分析:
分析运算:加,减,
头文件:add.h sub.h set.h clear.h
源文件:add.c sub.c set.c clear.c
主文件:main.c

分步编译
gcc -E -o add.i add.c
gcc -E -o sub.i sub.c
gcc -E -o set.i set.c
gcc -E -o clear.i clear.c
gcc -E -o main.i main.c
gcc -c -o add.o add.i
gcc -c -o sub.o sub.i
gcc -c -o set.o set.i
gcc -c -o clear.o clear.i
gcc -E -o main.o main.o
gcc -o main main.o set.o clear.o sub.o add.o
./main

一步到位法:
gcc -o main main.c sub.c add.c set.c clear.c

参考代码1 :bit.h

      /*bit.h: 负责清0,置1运算声明*/
      #ifndef __BIT_H  //头文件卫士
      #define __BIT_H
      
      /*声明清0和置1函数*/
      extern void clear_bit(int *,int);
      extern void set_bit(int*,int);
      #endif

代码2 :bit.c
      /*bit.c:负责清0和置1定义*/
      #include "bit.h"   //包含自己的头文件
      /*清0和置1函数定义*/
      void clear_bit(int *p,int n)
      {
      *p &= ~(1<<n);
      }
      void set_bit(int *p,int n)
      {
      *p |=(1<<n);
      }

代码3 cal.h
      /*负责加,减运算*/
      #ifndef __CAL_H   //头文件卫士
      #define __CAL_H
      
      /*声明加法和减法函数*/
      extern int add(int,int);
      extern int sub(int,int);
      
      #endif

代码4:cal.c
      /*负责加,减运算定义*/
      #include "cal.h"  //包含自己的 cal.h
      
      /*加法和减法函数定义*/
      int add(int x,int y){
      return x+y;
      }
      
      int sub(int x,int y){
      return x -y;
      }

代码5: array.h
   #ifndef __ARRAY_H   //头文件卫士
   #define __ARRAY_H
   extern int array_sub(int a[],int len);
   #endif

代码6:array.c
   /*声明数组累加函数*/
   #include "array.h"
   
   int array_sub(int a[],int len)
   {
   int sub = 0;
   for(int i=0;i<len;i++){
   sub+=a[i];
   }
   return sub;
   }

代码7:**主函数main.c**
   /*负责调用cal.c和bit.c函数*/
   #include 
   /*添加各种函数的声音*/
   #include "bit.h"
   #include "cal.h"
   #include "array.h"
   int main(void)
   {
       printf("100+200 = %d\n",add(100,200));
       printf("100-200 = %d\n",sub(100,200));
   
        int a =0x55;
        clear_bit(&a,0);   //将a的第0位清 0
        printf("a = %#x\n",a); //0x54
        set_bit(&a,0);     //将a的第0位置1
        printf("a = %#x\n",a); //0x55
        //自行添加:求数组所有元素累加和:array.h,array.c
        int b[5] = {1,2,3,4,6};
        printf("%d %d\n",sizeof(b),sizeof(b[0]));
        printf("%p %p\n",&b,&b[0]);
        printf("b[5] = %d\n",array_sub(b,sizeof(b)/sizeof(b[0])));
        return 0;
   }

运行命令
gcc -o main *.c -std=c99



大型程序的编译靠:Makefile

  1. 问:如果项目产品代码有1万源文件.c,编译极其的繁琐,郁闷
    gcc -o main main.c a.c b.c … 一个万.c
    这么简化程序的编译呢?
    答:必须只能利用Makefile来实现

  2. Makefile功能
    能够制定编译规则,将来让gcc编译器根据这个规则来编译程序
    Makefile本质就是一个文本文件,此文件给make命令使用
    将来make命令会根据Makefile里面的编译规则让gcc编译程序

  3. Makefile语法格式
    目标:依赖1 依赖2 依赖3 …依赖N
    (TAB键)编译命令1
    (TAB键)编译命令2

    (TAB键)编译命令N
    (TAB键) 还可以是其他命令:ls/cp/cd等

    注意
    Makefile注释用#

    例如
    目标是把helloworld.c编译生成helloworld
    vim Makfile 添加
    #指定规则:一步到位
    helloworld:helloworld.c
    gcc -o helloworld helloworld.c
    #或者
    #指定规则1:分步
    helloworld:helloworld.o
    gcc -o helloworld helloworld.o
    #指定规则2:
    helloworld.o:helloworld.c
    gcc -c -o helloworld.o helloword.c

    案例
    利用Makefile编译helloworld.c文件
    mkdir -p /home/tarena/stdc/day11/Makefile1/
    cd /home/tarena/stdc/day11/Makefile1/
    vim helloworld.c
    vim Makefile
    make //编译程序
    ./helloworld
    make //编译提示helloworld是最新的

    vim helloworld.c //修改源文件
    ls -lh //查看helloworld.c和helloworld的时间戳
    make //又重新编译

    案例
    将昨天的多文件代码拷贝并且利用Makefile编译
    mkdir -p /home/tarena/stdc/day11/Makefile2/
    cd /home/tarena/stdc/day11/Makefile2/
    vim Makfile
    make
    ./main

  4. Makefile工作原理
    当执行make命令时,make命令首先在当前目录下找Makefile,一旦找到Makfile
    文件,打开此文件并且找到所有的编译规则,通过这些编译规则确定了最终的目标是
    helloworld和源文件helloworld.c,然后make命令首先在当前目录下找是否存在
    目标文件helloworld,如果helloworld存在,然后检查helloworld和helloworld.c
    的时间戳哪个更新,如果helloworld的时间戳比helloworld.c新,说明源文件没有
    改过,无需编译,提示文件最新,如果helloworld的时间戳比helloworld.c要旧
    说明helloworld.c修改过,根据编译规则的命令重新编译
    如果一开始没有找到helloworld,程序整个重新编译

  5. Makefile小技巧

    %.o:%.c
    (TAB键)gcc -c -o $@ $<
    说明:
    %.o:目标文件.o
    %.c:源文件.c
    $@:目标文件
    $<:源文件
    作用是将当前目录下所有的.c文件单独编译生成对应的.o目标文件
    



第十二课:结构体(核心中的核心,实际开发必用)

  1. 明确
    目前c语言分配内存的方法:定义变量和定义数组
    定义变量分配内存缺陷:不能大量分配内存(目前最多8字节,double和long long类型),可以采用数组来一次性分配大量内存
    定义数组分配内存缺陷:虽然可以一次性大量分配内存,但是数据类型都一样
    有些场合可能需要不同种类的数据类型
    例如:
    描述学生的信息:年龄,学号,分数,姓名

结构体特点

a)既可以大量一次性分配内存(借助数组)还能保证数据类型可以不一致
b)结构体对应的关键字:struct
c)结构体也是一种数据类型,它是程序员自行定义的一种数据类型,类比成int 类型
d)结构体分配的内存也是连续的,一个成员挨着一个成员

结构体数据类型和定义的方法

a)方法1: 直接声明和定义结构体数据类型变量(很少用)

  1. 语法:
    struct{
    结构体成员(就是一堆变量); //又称结构体字段
    }结构体变量名;
    例如:

  2. 描述学生信息
    //定义一个学生信息的结构体变量student1
    struct{
    int age; //年龄
    int id; //学号
    float score; //分数
    char name[30]; //姓名
    }student1;

    //再定义一个学生信息的结构体和变量student2
    struct{
    int age; //年龄
    int id; //学号
    float score; //分数
    char name[30]; //姓名
    }student2;

  3. 缺陷: 每次定义结构体变量,结构体成员都要重新写一遍,很烦躁!

b) 方法2:

  1. 先声明结构体数据类型, 然后用这种结构体数据类型定义结构体变量(常用,掌握)
    struct 结构体名 {
    结构体成员;
    };
    注意:此时不会分配内存,对于大型程序,结构体应该写在头文件中
  2. 用结构体数据类型定义结构体变量的语法:
    struct 结构体名 结构体变量名;
    注意:会分配内存
    类比:把struct结构体名,类比成int类型
    大型程序,结构体定义放到源文件中来写
  3. 例如:
    //1.声明描述学生信息的结构体数据类型
    struct student {
    int age; //描述学生的年龄
    int id; //描述学生的学号
    float score; //描述学生的学分
    char name[30]; //描述学生的姓名
    };
    //2.定义两个学生信息的结构体变量
    struct student student1;
    struct student studnet2;
    或者
    struct student student1, student2;
    
  4. 缺陷:每次定义结构体变量,struct 结构体名 每次都要书写,很烦躁!

c)方法3:

  1. 务必掌握typedef关键字(否则不是一个合格的程序员)
    功能:给数据类型取别名(外号)
    语法:typedef 原数据类型 别名;
    例如:
    对于基本数据类型取别名(实际开发代码)
    typedef char s8; //s=signed:有符号,8:8位
    typedef unsigned char u8; //u=unsiged
    typedef short s16;
    typedef unsigned short u16;

    typedef int s32; //给给int类型取别名叫s32
    s32 a 250; //等价于 int a =250;
    typedef unsigned int u32;
    typedef long long s64;
    typedef unsigned long long u64;
    typedef float f32;
    typedef double f64;
    使用:
    int a 写成 s32 a;
    unsigned char b 写成 u8 b

  2. 用typedef对声明的结构体取别名
    注意:规定:别名后面加_t,对于大型程序写头文件
    写法1:
    语法:typedef struct {
    结构体成员;
    }别名_t;

    例如
    typedef struct student { //注意:这里的结构体名可以省略
    int age; //描述学生的年龄
    int id; //描述学生的学号
    float score; //描述学生的学分
    char name[30]; //描述学生的姓名
    }stu_t;

    写法2
    struct studnet{
    int age; //描述学生的年龄
    int id; //描述学生的学号
    float score; //描述学生的学分
    char name[30]; //描述学生的姓名
    int weight; //学生的体重
    };

    然后去别名
    typedef struct student stu_t;

  3. 不管使用哪种typedef对结构体数据类型取别名,定义结构体变量都一样
    定义结构体变量语法:别名 结构体变量名;
    例如:定义两个学生信息的结构体变量
    stu_t student1;
    stu_t student2;
    或者:
    stu_t student1, student2;

  4. 结构体变量的初始化方式,两种方式:
    a) 传统初始化方式

    1. 语法:struct 结构体名/别名 结构体变量名 = {初始化的值};
    2. 例如:
      struct student student1 = {18, 666, 100, “游哥”, 128};
      或者
      stu_t student1 = {18, 666, 100, “游哥”};
      结果:
      age成员=18,id成员=666,score成员=100,name成员=“游哥”
    3. 缺陷:定义初始化的时候需要按照顺序全部初始化
      因为有些场合可以不用按照顺序,也不需要初始化全部成员,可能就是初始化其中某些成员

    b) 标记初始化方式
    两种方式

    1. 语法:
      struct 结构体名/别名 结构体变量名 = {
      .成员1 = 初始化值,
      .成员2 = 初始化值,

      };
      2. 例如:

      struct student student1 = {
      .name   = "游哥",
      .weight = 128,
      .age = 18,
      };
      或者
      stu_t  student1 = {
      .name   = "游哥",
      .weight = 128,
      .age = 18,
      };
      
      1. 优点:不用按照顺序,不用全部成员初始化

  5. 结构体变量成员的访问:两种形式
    a) 通过".“运算符来访问结构体变量的成员
    语法:结构体变量名.成员名; //将来就可以对结构体的这个成员进行访问
    例如:
    stu_t student1 = {.name = “游哥”, .weight = 128, .age = 18,};
    //读查看
    printf(”%s %d %d\n", student1.name, student1.weight, student1.age);
    //写修改
    strcpy(student1.name, “葛鹏”); //修改成员的值
    student1.weight = 821; //修改成员的值
    student1.age = 17;

    b) 通过"->"运算符来访问结构体指针变量的成员
    语法:结构体变量的指针->成员名; //将来可以访问这个成员的内存区域
    注意:结构体变量的首地址等于结构体变量第一个成员的首地址
    例如:
    stu_t student1 = {.name = “游哥”, .weight = 128, .age = 18,};
    stu_t *p = &student1; //定义一个结构体指针变量p指向student1结构体变量

    //读查看
    printf(“%s %d %d\n”, p->name, p->weight, p->age);
    strcpy(p->name, “葛鹏”);
    p->weight = 821;
    p->age = 17;
    方法1:参考代码 struct.c

    /*结构体演示: 不取别名,先声明后定义*/
    #include
    #include
    /*声明结构体数据类型:描述学生信息*/
    struct student{
    char name[30];    //姓名
    int age;        //年龄
    float score;    //分数
    };
    int main(void)
    {
    //通过结构体访问
    //然后定义初始化结构体变量,描述关羽通信的信息
    //struct student student1 = {"关羽",18,65.5};   //传统初始化方式
    struct student student1 = {.score=67.6,.name="关羽",.age=24};  //标记初始化
    //打印学生信息
    printf("%s,%d,%g\n",student1.name,student1.age,student1.score);
    //修改关系学生信息
    student1.age++;
    student1.score = 56;
    strcpy(student1.name,"关关");
    //打印学生信息
    printf("%s,%d,%g\n",student1.name,student1.age,student1.score);
    
    //2. 通过结构体变量指针访问
    struct student *p = &student1;  //p指针student1
    p->age++;
    p->score = 66;
    strcpy(p->name,"关公");
    //打印学生信息
    printf("%s,%d,%g\n",p->name,p->age,p->score);
    
    return 0;
    }
    

    方法2:参考代码struct2.c

    /*结构体的演示*/
    #include
    /*声明结构体数据类型然后去别名*/
    typedef struct student{
    char name[30];    //姓名
    int age;         //年龄
    }stu_t;
    //或者typedef struct student stu_t;
    
    int main(void)
    {
    //定义结构体变量描述学生信息
    stu_t student1 ={.age = 18,.name = "刘备"};
    stu_t *p =&student1;     //p指向student1
    printf("%s,%d,%s,%d\n",student1.name,student1.age,p->name,p->age);
    student1.age++;
    p->age++;
    printf("%s,%d,%s,%d\n",student1.name,student1.age,p->name,p->age);
    return 0;
    }
    

  6. 结构体变量之间可以直接赋值
    例如:
    stu_t student1 = {18, 666, 100, “游哥”, 128};
    stu_t student2 = student1;
    或者
    stu_t student1 = {18, 666, 100, “游哥”, 128};
    stu_t *p = &student1; //p指向student1
    stu_t student2 = *p;

  7. 结构体嵌套:结构体成员还是一个结构体
    例如:
    typedef struct B{
    int a;
    int b;
    }B_t;

    typedef struct C{
    int c;
    int d;
    B_t; //说明结构C包含结构体B
    }C_t;

    嵌套变量参考代码struct4.c

    /*结构体嵌套*/
    #include
    /*声明描述学生出生日期的结构体数据类型*/
    typedef struct birthday{
    int year;   //年
    int month;   //月
    int day;     //日
    }birth_t;
    
    /*声明描述学生信息的结构体数据类型*/
    typedef struct student{
    char name[30];    //姓名
    int age;         //年龄
    birth_t birth;   //出生日期,结构体的嵌套
    }stu_t;
    
    int main(void)
    {
    //定义初始化结构体变量描述关羽学生的信息
    //stu_t student1 ={"关羽",18,{2002,2,1}};  //传统初始化方式
    stu_t student1 = {.name = "关羽",.age =18, .birth = {.year = 2002,.month= 2,.day=1}};
    stu_t *p = &student1;  //p指向student1;
    printf("%s,%d,%d:%d:%d\n",student1.name,student1.age,student1.birth.year,student1.birth.month,student1.birth.day);   //打印学生信息
    
    p->age++;
    p->birth.year = 2003;
    printf("%s,%d,%d:%d:%d\n",p->name,p->age,p->birth.year,p->birth.month,p->birth.day);   //打印学生信息
    return 0;
    }
    

    嵌套指针参考代码struct5.c

    /*结构体嵌套结构体指针演示*/
    #include
    /*声明描述学生出生日期的结构体数据类型*/
    typedef struct birthday{
    int year;   //年
    int month;   //月
    int day;     //日
    }birth_t;
    
    /*声明描述学生信息的结构体数据类型*/
    typedef struct student{
    char name[30];    //姓名
    int age;         //年龄
    birth_t *pbirth;   //出生日期,结构体指针的嵌套
    }stu_t;
    
    int main(void)
    {
    //定义初始化结构体变量描述关羽学生的信息
    //stu_t student1 ={"关羽",18,NULL};  //传统初始化方式
    stu_t student1 = {.name = "关羽",.age =18, .pbirth = NULL};
    stu_t *p = &student1;  //p指向student1;
    //定义关羽学生出生日期的结构体变量
    birth_t birth = {.year = 2002,.month=2,.day=1};
    //将关羽学生信息和关羽出生日期关联起来
    p->pbirth = &birth;   //pbirth指向birth结构体变量
    //或者 :student1.pbirth = &birth;
    
    printf("%s,%d,%d:%d:%d\n",student1.name,student1.age,student1.pbirth->year,student1.pbirth->month,student1.pbirth->day);   //打印学生信息
    p->age++;
    p->pbirth->year = 2003;
    p->pbirth->month = 3;
    printf("%s,%d,%d:%d:%d\n",p->name,p->age,p->pbirth->year,p->pbirth->month,p->pbirth->day);   //打印学生信息
    return 0;
    }
    

  8. 函数的形参是结构体,两种形式
    a) 直接传递结构体变量本身,形参是实参的一份拷贝,结构体有多大就需要拷贝多大
    函数通过形参是不能修改结构体实参,只是对形参做了改变

    b) 直接传递结构体变量的地址
    函数通过形参可以直接修改结构体实参,代码执行效率高,如果是指针只需拷贝4字节

    c) 公式,规矩:如果函数要访问结构体,将来要传递结构体指针,不要传递结构体变量
    如果函数对结构体成员不进行修改,形参用const修饰

    void show(const stu_t *pst) { printf(“%s\n”, pst->name); //不让修改:strcpy(pst->name, “王八蛋”); } void grow(stu_t *pst) { pst->age++; }

  9. 结构体内存对齐问题(笔试题必考)
    a) gcc对结构体成员编译时,默认按4字节对齐
    例如:
    struct A {
    char buf[2];
    int val;
    };

    结果:sizeof(struct A) = 8
    内存分布图:结构体对齐.png

    b) 终极演示代码:

    /*结构体内存对齐*/
    #include 
    //声明结构体数据类型A
    struct A {
    char buf[2]; //4
    int val; //4
    };
    //声明结构体数据类型B
    struct B {
    char c; //4
    short s[2]; //4
    int i; //4
    };
    #pragma pack(1) //让gcc强制从这个地方开始后面代码按照1字节对齐方式编译
    //声明结构体类型C
    struct C {
    char c; //1
    short s[2]; //4
    int i; //4
    };
    #pragma pack() //让gcc到这里在恢复成默认4字节对齐
    //声明结构体类型D
    struct D {
    int i; //4
    char c; //4
    };
    //声明结构体类型E
    struct E {
    double d;  //8
    char c; //4
    };
    int main(void)
    {
    printf("sizeof(struct A) = %d\n", sizeof(struct A)); //8
    printf("sizeof(struct B) = %d\n", sizeof(struct B)); //12
    printf("sizeof(struct C) = %d\n", sizeof(struct C)); //9
    printf("sizeof(struct D) = %d\n", sizeof(struct D)); //8
    printf("sizeof(struct E) = %d\n", sizeof(struct E)); //12
    return 0;
    }
    

联合体

  1. 特点:
    a)它和结构体使用语法一模一样,只是将关键字struct换成union
    b)联合体中所有成员是共用一块内存,优点节省内存
    c)联合体占用的内存按成员中占内存最大的来算
    例如:union A {
    char a;
    short b;
    int c;
    };
    sizeof(union A) = 4;
    d)初始化问题
    union A a = {8}; //默认给第一个成员a,a = 8
    union A a = {.c = 8} //强制给c赋值

  2. 经典笔试题(作业)
    现象:

    1. X86架构的CPU为小端模式:数据的低位在内存的低地址,数据的高位在内存的高地址处
      例如:
      int a = 0x12345678;
      内存条
      低地址 高地址
      0-------1-----2-------3------4--------------------------------->
      0x78 0x56 0x34 0x12

    2. POWERPC架构的CPU为大端模式:
      数据的低位在内存的高地址,数据的高位在内存的低地址处
      例如:
      int a = 0x12345678;
      内存条
      低地址 高地址
      0-------1-----2-------3------4--------------------------------->
      0x12 0x34 0x56 0x78
      要求:编写一个程序求当前处理器是X86架构还是POWERPC架构
      思路:采用union或者指针
      提示:
      union A {
      char a;
      int b;
      };

      参考代码:

      #include 
      //声明一个联合体
      typedef union w
      {
      int a;  //4 字节
      char b; //1 字节
      } c_t;
      int main(void)
      {
      
      //定义联合体变量
      c_t c.a=1;
      if (c.b==1)
      printf("小端\nn");
      else
      printf("大端\n");
      return 1;
      }
      


复合类型:枚举

  1. 枚举的本质就是一堆整数的集合,列表,就是给一堆整数取了个别名,提高代码的可读性
    例如:0表示红色,1表示绿色,2表示蓝色,过段时间,可能就混淆了数字和颜色的对应关系
    干脆给0,1,2 三个树脂字取别名分别叫:RED,GREEN,BLUE,不管什么时候,只要看到别名就能匹配到对应的颜色上
    跟#define宏类似:
    #define RED 0
    #define GREEN 1
    #define BLUE 2
    结论:以后给整数数字去别名两种方式:采用枚举或者宏
    特点:枚举值默认是从0开始,后面的成员依次加1,枚举一般建议用大写

  2. 声明枚举数据类型的语法
    enum 枚举数据类型名(也可以不用) {枚举值,如果有多个用逗号分开};
    例如:enum COLOR {RED, GREEN, BLUE};
    结果:RED=0,GREEN=1,BLUE=2 等价于给0,1,2取别名
    printf(“%d %d %d\n”, RED, GREEN, BLUE); //0,1,2
    例如:
    enum COLOR {RED, GREEN=250, BLUE};
    结果:RED=0,GREEN=250,BLUE=251 等价于给0,250,251取别名
    printf(“%d %d %d\n”, RED, GREEN, BLUE); //0,250,251
    结论:将来程序中再描述红,绿,蓝三种颜色,就不需要用0,1,2(代码可读性非常差)
    用RED, GREEN,BLUE替换0,1,2
    参考代码:enum.c

    /*枚举演示*/
    #include
    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);  //可读性很好
    printf("%d\n",0+20+251);    //可读性很差,不清楚这些数字的含义
    
    enum RESULT {RETURN_OK,RETURN_FAILED};
    printf("%d %d\n",RETURN_OK,RETURN_FAILED);
    return 0;
    }
    
  3. 枚举的经典使用代码模板(操作系统核心代码片段)
    vim enum2.c 添加

    #include 
    /*定义检测函数*/
    int check(int a)
    {
    if(a != 0) {
    printf("表示成功了.\n");
    return 0; //表示成功
    } else {
    printf("表示失败了.\n");
    return 1; //表示失败
    }
    }
    int main(void)
    {
    printf("%d\n", check(1));
    return 0;
    }
    

    保存退出
    结论
    程序员很难判断 0和1谁是成功谁是失败!代码可读性非常差,除非认真仔细研究透代码,或者有详细的代码注释,否则代码的可读性很差
    解决办法:利用枚举方法或者#define宏来提高代码可读性,也就是给0和1取别名:RETURN_OK,RETURN_FAILED 这些采用枚举

    改进之后的经典代码(公式):
    vim enum3.c 添加

    #include 
    //定义枚举数据类型
    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(1));
    printf("%d\n", check(0));
    return 0;
    }
    

    结论
    任何程序员只要看到RETURN_OK,RETURN_FAILED就知道具体的含义时成功和失败了,大大提高了代码的可读性




第十三课:函数指针(核心中的核心)

  1. 回顾指针相关概念
    回顾指针函数概念:本质是一个函数,只是它的返回值是一个指针而已
    例如:int *add(int a, int b) …
    切记切记切记:函数名就是整个函数的首地址
    int add(int a, int b)
    {
    return a+b;
    }
    此函数在内存中的首地址就是add函数名

    a) 指针函数
    就是一个函数,只是它的返回值是一个指针
    例如:int *add(int a, int b)

    b) 函数名就是整个函数里面代码的首地址,简称函数的首地址
    int add(int a, int b)
    {
    printf(“%d\n”, a);
    printf(“%d\n”, b);
    return a + b;
    }
    add这个函数名就是整个函数add的首地址,就是三条语句的首地址,等于第一条语句
    printf函数所在内存的首地址

函数指针概念

本质就是一种程序员自己声明的数据类型(跟int,结构体,联合体,枚举一样的,都是自己声明)
用它定义的变量只能保存函数的首地址

函数指针数据类型声明的语法

(不会分配内存并且大型程序中写头文件)
返回值数据类型 (*函数指针名)(形参表);
注意:声明的代码对于大型程序要在头文件中,建议用typedef 对函数指针数据类型取别名
例如:
int (*pfunction)(int a, int b); //pfunction就是一种函数指针数据类型,把这一堆当成int型来使用
建议取别名:
typedef int (*pfunc_t)(int a, int b); //对函数指针取别名叫pfunc_t,以后直接拿着别名定义变量
注意:声明时形参变量名可以不用写

函数指针变量定义语法格式

函数指针数据类型/别名 函数指针变量;
例如:pfunc_t pfunc; //定义函数指针变量,此变量将来保存一个函数的地址,所以此变量永远也只分配4字节内存

函数指针变量的初始化

pfunc_t pfunc = add; //定义并且初始化函数指针变量,并且保存add函数的首地址,简称指向add函数
等价于
int (*pfunc)(int a, int b) = add; //pfunc指向add函数,此种写法极其繁琐

通过函数指针变量来访问指向的函数,调用函数

语法格式:函数指针变量名(实参表);
例如:pfunc(100,200); //本质就是调用add函数并且给add函数传递100和200

函数指针总结

a) 函数指针建议用typedef取别名
例如:pfunc_t

b) 函数指针的返回值和形参表务必要和指向的函数的返回值和形参表一致
例如:
typedef int (*pfunc_t)(int a ,int b); //它要求返回值类型和形参表都是int类型
所以将赖指向的函数,例如:
int add (int a,int b) … 它的返回值和形参表也必须是int类型,否则编译器gcc报错

c) 回调函数
一个函数可以被当成参数传递给别的函数,这个函数的参数必须是函数指针
来保存回调函数的地址(例如:add)

参考代码:pfunction.c

/*函数指针演示*/
#include

/*声明函数指针数据类型*/
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()
{
int ret =0;
//定义函数指针变量并且初始化为空指针
pfunc_t pfunc = NULL;
//指向add函数
pfunc = add;    //pfunc保存add函数的首地址
//利用函数指针调用add函数
ret = pfunc(100,200);
printf("%d\n",ret);

//指向sub函数
pfunc = sub;    //pfunc保存sub函数的首地址
//利用函数指针调用sub函数
ret = pfunc(100,200);
printf("%d\n",ret);
return 0;
}

回调函数

一个函数可以当成参数传递给别的函数,这个函数的参数必须是函数指针,其中被传递的函数称之为回调函数,例如:add,sub
作用:将来你写一个调用函数cal,然后完成一个功能,但是这个功能将来可能不能满足别的程序员的要求,此时让cal 函数具备
抵用别的程序员实现的函数,此时只需在cal函数的形参中添加一个函数指针,将来指向别的程序员的回调数完成他想要的结即可
参考代码pfunction1.c

/*回调函数演示*/
#include

/*声明函数指针数据类型并且取别名*/
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放到袋鼠妈妈的口袋里,并且给宝宝带点粮食: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的首地址
ret = cal(100,200,sub);  //调用cal函数,传递是三个参数,最后一个参数回调函数sub的首地址
printf("ret = %d\n",ret);
ret = cal(100,200,NULL);  //调用cal函数,用此函数默认的功能
printf("ret = %d\n",ret);
return 0;
}

函数指针经典代码演示

目的利用函数指针,循环,数组实现顺序调用一组函数
参考代码:pfunction2.c

/*函数指针,循环,数组结合使用:完成一组函数按个调用*/
#include 
/*声明函数指针数据类型*/
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 pfunc_array[] = {add, sub, mul, div, NULL};

//2.挨个调用
for(pfunc_t *pfunc = pfunc_array; *pfunc; pfunc++) {
int ret = (*pfunc)(100, 200);
printf("ret = %d\n", ret);
}
return 0;
}

结论
将来如果想让main函数调用一堆的函数,只需将函数放到数组中即可,将来

for循环自动帮你挨个调用!


第十四课:多级指针(掌握到二级指针)

  1. 回顾一级指针:指向一个普通变量的内存区域
    例如:int a = 250;
    int *p = &a; //p保存着a变量的首地址
    printf(“a的首地址%p, a的值是%d\n”, p, *p);
    *p =520; //修改变量a的值

二级指针概念

指向一级指针变量的指针,也就是二级指针变量存放着一级指针变量的首地址

定义二级指针变量的语法格式

数据类型 **二级指针变量名 = 一级指针的地址;
例如:
int a = 250;
int *p = &a;
int **pp = &p; //定义二级指针保存一级指针变量p的首地址
//读查看
printf(“p的地址是%p, a的地址是%p, a的变量是%d”, pp, *pp, **pp);
//写修改
**pp = 350;
目前来看,二级指针处理普通的变量多余, 用一级指针即可拿下!
参考代码ppointer.c

/*二级指针演示*/
#include

int main(void)
{
int a = 250;
int *p = &a; //p指向a
int **pp =&p; //pp指向p
printf("p的首地址%p,a的首地址是%p,a的值是%d\n",pp,*pp,**pp);
//通过二级指针修改a
**pp =520;
printf("%d %d %d\n",a,*p,**pp);
return 0;
}

目前看来,二级指针处理普通的变量多余,用一级指针即可拿下!

二级指针和字符串的那点事

经典的笔试题:编写一个函数实现两个字符串的交互
例如:
char *pa = “hello”;
char *pb = “world”;
目标:pa->“world”, pb->“hello”,指针互换
注意:通过pa和pb指针是无法修改字符串的值
参考代码:swap.c

/*实现字符串交换:交换字符串首地址*/
#include

void swap(char *p1,char *p2) //p1=pa,p2=pb
{
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;
*p1 = *p2;  //修改一级指针pa的值
*p2 = p3;  //修改一级指针的pb的值
}

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;
}

二级指针和字符指针数组的那点事

a) 回顾:字符指针数组形式
char *p[] = {“hello”, “world”}; //p[0]保存字符串"hello"的首地址, p[1]保存字符串"world"的首地址
或者
char *p1 = “hello”;
char *p2 = “world”;
char *p[] = {p1, p2};

注意:给你一个字符串,实际给你的是字符串的首地址
显然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

int main(void)
{
char *a[] = {"hello","world"};
char **p =a;  //p指向a

//打印字符串
printf("%s %s\n",a[0],a[1]);
printf("%s %s\n",*(a+0),*(a+1));
printf("%s %s\n",p[0],a[1]);
printf("%s %s\n",*(p+0),*(p+1));

//通过p来打印“hello字符串中的每个字符”
printf("%c %c %c %c %c\n",**(p+0),*(*(p+0)+1),*(*(p+0)+2),*(*(p+0)+3),*(*(p+0)+4));

//自行将"world"字符串中的每个字符打印出来
printf("%c %c %c %c %c\n",*p[1],*(p[1]+1),*(p[1]+2),*(p[1]+3),*(p[1]+4));

return 0;
}

实际开发,产品代码的主函数main书写

a) main函数的公式
int main(int argc, char *argv[])
或者:
int main(int argc, char **argv)
不要写:int main(void)

b) 切记
只要在命令行终端(不管是什么命令行)输入的任何内容,计算机都当成字符串处理
例如:./helloworld 100 200 ->实际计算机存储他们都是按照字符串存储
内存中最终存储字符串"./helloworld",字符串"100",字符串"200"

c) 问:main函数的argc,argv到底是什么鬼呢?
答:argc, argv功能是当运行程序时,可以在命令行终端上给程序传递数值
实际上输入的数值都是按照字符串处理了
argc:操作系统会帮你记录命令行输入的命令参数个数
argv:操作系统用argv字符指针数组来保存输入的字符串首地址

例如
启动helloworld程序,并且后面跟两个数字100和200
运行命令:./helloworld 100 200
结果是:
argc = 3
argv[0] = “./helloworld” //argv[0]指针指向字符串"./helloworld"
argv[1] = “100” //argv[1]指向字符串"100"
argv[2] = “200” //argv[1]指向字符串"200"

结论
比如用户运行程序命令:
./helloworld 100 200 300
结果:
argc = 4 参数个数为4个
argv[0] = “./helloworld”
argv[1] = “100”
argv[2] = “200”
argv[3] = “300”

参考代码main.c

/*main函数完整的编写框架*/
/*运行:
  ./main 100 200
  argc = 3
  argv[0]= "./main"
  argv[1]= "100"首地址
  argv[2]= "200"首地址
*/
#include

//int main(int argc,char **argv)
int main(int argc,char *argv[])
{
    //打印argc 和argv 的信息
    for(int i = 0;i<argc;i++){
       printf("argc = %d,argv[%d] = %s\n",argc,i,argv[i]);
    }
    return 0;
}

d) 结论
程序运行时,可以通过命令行给程序传递参数,例如"100",“200”
问:运行程序时,可以跟数字,但是数字最终都是以argv字符串形式存在,程序如何将一个字符串转对应的数字呢
例如:“100” 转成 100, "200"转200

答:利用大名鼎鼎的函数:strtoul标准C库函数 (大神编写好的函数,咱直接用即可)
函数功能:就是实现字符串转整型数字

例如
“100” 转成 100, "200"转200
函数声明如下:
unsigned long int strtoul(const char *str, char **endptr, int base)

形参
str:传递要转换的字符串的首地址,例如:“100”
endptr:一律给NULL
base:指定按那个进制进行转换

  1. 如果给0,根据实际的数字书写形式进行转换
    例如:“100”-> 100 //10进制
    “0100”->0100 //8进制
    “0x100”->0x100 //16进制
  2. 如果给16,强制转换成16进制
    例如:“100”->0x64
  3. 如果给8,强制转换成8进制
    例如:“100” ->0144

返回值
返回转换以后的数字

例如
argv[1] = “100”
int a = strtoul(argv[1], NULL, 0);
printf(“a = %d\n”, a); //a = 100

例2:参考代码main.c

/*main函数完整的编写框架*/
/*运行:
./main 100 200
argc = 3
argv[0]= "./main"
argv[1]= "100"首地址
argv[2]= "200"首地址
*/
#include
#include

//int main(int argc,char **argv)
int main(int argc,char *argv[])
{
//打印argc 和argv 的信息
for(int i = 0;i<argc;i++){
printf("argc = %d,argv[%d] = %s\n",argc,i,argv[i]);
}
printf("\n");

//对参数的个数进行安全判断,要求参数必须是3个
if(argc !=3){
printf("请重新输入:%s <100> <200>\n",argv[0]);
return -1;
}
//将用户输入的字符串转整型
int a =0,b = 0;  //将来保存用户输入的数字
a = strtoul(argv[1],NULL,0);   //将argv[1]指向的字符串“100”转整数并保存到变量a中
b = strtoul(argv[2],NULL,0);  //将argv[2]指向的字符串"200"转整数并保存到变量b中
printf("a = %d,b=%d\n",a,b);
printf("a+b = %d\n",a +b);
return 0;
}



第十五课:结构体和函数指针配合专项训练(实际产品开发必用)

  1. 参考代码struct_pfunction.c

    /*结构体和函数指针配合*/
    #include
    
    //此声明告诉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(int argc,char *argv[])
    {
      //定义初始化结构体变量描述学生信息
      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;
    }
    


第十六课:malloc和free标准库函数

回顾:C语言分配内存方法三种:

  1. 定义变量:缺陷:不能分配大量内存

  2. 定义数组 : 缺陷:可以做到大量分配内存,但是数据类型要一致,有些场合要求不一致
    局部数组:访问的范围有限
    全局数组:存在乱序访问的问题,只能添加互斥保护,而保护会降低运行效率

  3. 结构体(联合体):可以利用数组分配大量内存,也可以让其他成员数据类型不一致,还是有点小缺陷,总之还是利用了数组
    总结:以上三种方法如果采用数组实现大量分配内存,是可以的,但是致命的缺陷在与必须提前一次性分配好,这样有可能造成内存的浪费
    例如:
    int a[10000000]; //一次性分配了4 *1000000 字节内存,因为数组定义时需要指定下标
    程序实际运行时可能就用了100个字节,其他内存无形白白浪费了
    关键数组,变量,结构体都涉及局部性和全局性,局部非静态变量内存生命周期又很短

  4. 问:如何做到大量动态分配内存(想用多少内存就分配多少内存),并且分配的内存根据数据类型无关,不用考虑到什么局部和全局问题
    答:利用大名鼎鼎的malloc和free函数

malloc和free函数特点

  1. 随时随地动态的连续分配想要的内存,并且内存大小随时指定
  2. 分配的内存彻底跟数据类型无关
  3. 不用考虑局部非静态变量的缺陷(出了花括号内存消亡问题)
  4. 缺点:分配内存效率低下,只能在堆区分配内存(堆区概念第二阶段课程讲解)

详解malloc和free函数

  1. malloc函数的原型
    void *malloc(unsigned long size);
    - 功能
    从堆区分配的内存,只要不调用free函数就不会释放
    并且分配的内存数据都是随机数
 - **形参**:
   size:指定要分配的内存大小,单位是字节
   返回值:返回分配的内存的首地址,注意要做强制类型转换
   如果分配内存失败,操作系统会返回NULL
   返回值是无类型指针void * ,所以使用是记得做强制转换
   
 - **注意**:
 如果用此函数需要添加 #include 声明
 头文件:为了使用此函数必须添加头文件:#include 

例1:

//连续分配3字节并且将内存的首地址返回保存给指针变量p,p指向分配的8字节内存
int *p = (int *)malloc(8);
//安全判断
if(NULL ==p)
printf("分配内存失败。\n");
else{
printf("分配内存成功。\n");
*(p+0) =2;    //向8字节内存的前4字节写入数据2
*(p+1) =3;   //向8字节内存的后4字节写入数据3
printf("%d %d\n",*(p+0),*(p+1));
}

或者

void *p = malloc(8)
//安全判断
if(NULL ==p)
printf("分配内存失败。\n");
else{
printf("分配内存成功。\n");
*(int *)(p+0) =2;    //向8字节内存的前4字节写入数据2
*(int 4)(p+4) =3;   //向8字节内存的后4字节写入数据3
printf("%d %d\n",*(int *)(p+0),*(int *)(p+4));
}

例2

int *p = (int *)malloc(8);  //p指向连续分配的8个字节内存的首地址
if(NULL == p) {
printf("分配内存失败");
return -1;
} else {
printf("分配内存成功.\n");
*(p+0) = 2;
*(p+1) = 3;
printf("%d %d\n", *p, *(p+1));
}

或者

void *p = malloc(8); //p指向连续分配的8个字节内存的首地址
if(NULL == p) {
printf("分配内存失败");
return -1;
} else {
printf("分配内存成功.\n");
*(int *)(p+0) = 2;
*(int *)(p+4) = 3;
printf("%d %d\n", *(int *)(p+0), *(int *)(p+4));
}
  1. free函数原型
 void free(void *p);
 **函数功能**:
 调用此函数即可随时释放malloc分配的内存,将内存资源归还给操作系统
 
 **形参**:
 p: 传递malloc分配的内存的首地址

 **例如**:
 free(p);   //释放上面malloc分配的内存
 p = NULL;     //好习惯;释放完毕将指针变量赋值给NULL,否则p是一个野指针,因为他不再执行一块有效内存

参考代码malloc.c 和 malloc1.c
代码1:malloc.c

/*malloc和free函数演示*/
#include
#include

int main(int argc,char *argv[])
{
int *p =NULL;

//分配8字节内存并且返回8字节内存首地址给p,p指向分配的8字节内存
p = (int *)malloc(8);
//安全判断
if(NULL == p){
printf("分配内存失败.\n");
return -1;
}
//向8字节内存写入数据
*(p+0) =250;  //前4字节
*(p+1) =251;  //后4字节

//打印内存值
printf("%d %d\n",*(p+0),*(p+1));

//采用无类型指针接收
//分配8字节内存并且返回8字节内存首地址给p,p指向分配的8字节内存
void *p1 = (int *)malloc(8);
//安全判断
if(NULL == p1){
printf("分配内存失败.\n");
return -1;
}
//向8字节内存写入数据
*(int *)(p1+0) = 252;   //前4字节
*(int *)(p1+4) = 253;   //后4字节,先p+4:地址加4,指向后4字节内存,然后(int *): 将来要操作4字节内存

//打印内存值
printf("%d %d\n",*(int *)(p1+0),*(int *)(p1+4));

return 0;
}

代码2:malloc1.c

/*malloc和结构体*/
#include
#include  //malloc函数的声明
#include  //strcpy函数声明
/*声明描述学生信息的结构体数据类型*/
typedef struct student{
char name[30];   //姓名
int age;         //年龄
}stu_t;   //把stu_t当成int来用

//分配学生信息的内存并且初始化学生信息,并且返回学生信息对应的内存首地址
stu_t *get_student_info(void)
{
//调用malloc分配一块内存来描述存储学习信息,只要不调用free,这块内存永远存在,不用考虑局部和全局问题
stu_t *pstu = (stu_t *)malloc(sizeof(stu_t));  //分配内存大小就是一个结构体占用的内存大小

//安全判断
if(NULL == pstu){
printf("分配内存失败.\n");
return NULL;
}
//初始化学生信息
strcpy(pstu->name,"关羽");
pstu->age = 18;
return pstu;   //返回malloc分配的内存首地址,这块内存现在有了关羽学生的信息
}
int main(void)
{
//调用此函数获取学生信息,p指向malloc分配的存储学生信息的内存首地址
stu_t *p = get_student_info();
if(NULL == p){
return -1;
}
printf("%s %d\n",p->name,p->age);   //打印学生信息
free(p);      //释放malloc分配的内存
p = NULL;     //好习惯

return 0;
}

**练习1:malloc2.c**
/*获取不同学生的信息*/

#include
#include
#include

/*声明描述学生信息的结构体*/
typedef struct student{
char name[32];
int age;
}stu_t;

/*定义分配获取学生信息函数*/
stu_t *get_student_info(char *name,int age){
stu_t *p = (stu_t *)malloc(sizeof(stu_t));  //分配一个学生信息的内存
if(NULL == p){
return NULL;
}
//初始化学生信息,初始化内存
strcpy(p->name,name);
p->age = age;
return p;   //返回学生信息内存的首地址
}

#define LEN    (4) //定义学生个数常量宏
int main(void)
{
stu_t *array[LEN] = {NULL};   //保存每个学生信息内存的首地址
char name[32] = {0};  //保存输入的学生姓名
for(int i =0;i<LEN;i++){
printf("请输入学生姓名:");
scanf("%s",name);   //把输入的字符串姓名保存到数组name中
stu_t *p = get_student_info(name,i+18);  //调用此函数给1个学生信息分配内存并且初始化学生信息
array[i] = p ;     //把每个学生信息内存的首地址保存到结构体指针数组array中(每个元素是学生信息结构体的首地址)
}
for(int i =0;i<LEN;i++){
printf("%s %d\n",array[i]->name,array[i]->age);
}
//释放内存
for(int i = 0;i<LEN;i++){
free(array[i]);
}
}

**练习2 .一次性分配4个学生的内存malloc3.c**
/*获取不同学生的信息*/
#include
#include
#include
#define LEN  (4)   //定义学生个数常量宏
/*声明描述学生信息的结构*/
typedef struct student{
char name[32];
int age;
}stu_t;
/*定义分配获取学生信息函数*/
stu_t *get_student_info(void){
stu_t *p = (stu_t *)malloc(sizeof(stu_t)*4);  //分配4个学生信息的内存
if(NULL == p)
return NULL;
//初始化每个学生信息,初始化内存
stu_t *ptmp =p;    //临时保存4个学生信息内存的首地址,不能返回p,因为p后面做++运算
for(int i =0;i[LEN;i++){
printf("请输入学生的姓名:");
scanf("%s",p-](LEN;i++)%7B%0Aprintf(%22%E8%AF%B7%E8%BE%93%E5%85%A5%E5%AD%A6%E7%94%9F%E7%9A%84%E5%A7%93%E5%90%8D:%22);%0Ascanf(%22%25s%22,p-)name);
p->age = i+18;
p++;    //指向下一个学生的内存首地址
}
return ptmp;  //返回4个学生信息内存的首地址
}

int main(void)
{
stu_t *p = get_student_info();  //调用此函数给4个学生信息分配内存并且初始化学生信息
for(int i = 0;i<LEN;i++){
printf("%s %d\n",p[i].name,p[i].age);  //等价于(p+i)->name,(p+i)->age
}
free(p);
return 0;
}

  1. memset函数原型
    void memset(void *p, int data, int len);
    函数功能: 设置内存数据
    参数:
    p:传递要设置的内存的首地址
    data:传递要指定的数据
    len:传递要设置的内存大小
    例如: memset(p, 0, 8); //将以p为其实的内存,连续8字节都设置为0

第十七课:文件操作相关库函数

(f= file: 文件 ,open:打开,close: 关闭,read: 读,write: 写,seek: 定位)

文件操作标准C库函数如下:

fopen/fclose /fread/fwrite /fseek/rewind

fopen函数原型

FILE *fopen(const char *filename, const char *mode)
功能:打开文件
参数:
filename: 指定要打开的文件名,建议用绝对路径,例如:“/home/tarena/a.txt”
mode:指定打开文件时的方式

  • “r”:以只读(只能查看不能修改)方式打开,前提是改文件必须存在
  • “r+”:以读/写方式打开,前提是文件必须存在
  • “w”:以只写(只能修改不能查看)方式打开,如果文件存在会将文件清空,如果文件不存在,创建新文件
  • “w+”:以读/写方式打开,如果文件存在会将文件清空,如果文件不存在,创建新文件
  • “a”: 以追加附加方式打开只写文件,如果文件不存在,创建文件,如果文件存在,把新内容添加到文件尾部
  • “a+”: 以追加附加方式打开读/写文件,如果文件不存在,创建文件,如果文件存在,把新内容添加到文件尾部
    返回值:返回一个描述文件信息的结构体指针:FILE *指针,当int *
    打开失败返回NULL

fclose 函数原型

void fclose(FILE *fp)
功能:关闭文件,fp: 传递fopen的返回值就是文件指针


fwrite函数原型

unsigned long fwrite(void *ptr,unsigned long size,unsigned long nmemb,FILE fp)
功能:向文件写入数据,就是将内存中的数据写入到文件所在的硬盘上
参数:
- ptr :传递保存数据的内存首地址
- size:指定要写入的单个数据块的大小(类似文章中的段落),例如:一个int类型数据块 = 4字节
- nmemb:指定要写入的单个数据的个数(类似文章中段落的个数),例如:共10个数据块,所以总共写入大小 = 10

- fp:指定要写入的文件指针(就是文件),也就是硬盘

返回值:写入失败返回-1,写入成功返回实际写入的数据块个数(类似文章段落的个数)

每次写完,fp文件指针就会跑到文件的尾部!

rewind函数原型

void rewind(FILE *fp)
功能:将文件指针fp重新指定到文件开头

fread 函数原型

unsigned long fread(void *buffer, unsigned long size,unsigned long count,FILE *fp)
功能:从文件所在的硬盘上读取数据保存到内存中
buffer:保存数据的内存首地址
size: 指定要读取的单个数据块大小
count: 指定要读取的数据块的个数
fp:文件指针,就是文件
返回值:读取失败返回-1,读取成功返回实际读取的数据块个数
每次读完,fp文件指针就会跑到文件的尾部!

fseek函数原型

int fseek(FILE *fp, long offset , int fromwhere)
功能:定位文件指针,从哪里开始访问文件
fp:文件指针
offset: 偏移量

参考代码:file.c

/*文件操作标准库函数*/
#include
int main(void)
{
//打开文件
FILE *fp = NULL;  //创建初始化描述文件信息的结构体指针
fp = fopen("/root/study/day13/a.txt","w+");  //可读可写的方式打开
if(NULL == fp){
printf("文件打开失败.\n");
return -1;
}

//把数组中的内存数据向文件写入数据
int a[] = {1,2,3,4,5,6,7}; //内存中有8个数据
int len = sizeof(a)/sizeof(a[0]);
int size = 0;
size = fwrite(a,sizeof(int),len,fp);
printf("实际写入的数据块个数%d\n",size);

//将fp文件指针重新指定到文件的开头

//从文件所在的硬盘上读取数据到内存中
int b[8] = {0};   //保存读取的数据
size = fread(b,sizeof(int),10,fp);   //fp跑到文件的结尾
printf("实际读取了%d个数据块.\n",size);
for(int i =0;i<size;i++){
}

//将文件指针定位到从文件开头开始往后8个字节的位置
//1 2 3 4 5 6 7 8
int c[2] = {0};  //保存读取的数据
fseek(fp,8,SEEK_SET);
fread(c,sizeof(int),2,fp);   //此时fp跑到5这个数的位置
printf("%d %d\n",c[0],c[1]);

//将文件指针从当前位置往后移动8个字节
//1 2 3 4 5 6 7 8
//           fp
fseek(fp,8,SEEK_CUR);
fread(c,sizeof(int),2,fp);   //此时fp跑到文件尾部
printf("%d %d\n",c[0],c[1]);

//将文件尾部开始往前移动12个字节
//1 2 3 4 5 6 7 8
//         fp
fseek(fp,-12,SEEK_END);
fread(c,sizeof(int),2,fp);   //此时fp跑到8这个数的位置
printf("%d %d\n",c[0],c[1]);

//关闭文件
fclose(fp);
return 0;
}

C语言综合演练

实现自己的命令行终端,实现算术运算功能
例如
输入命令名称:add
输入计算的数字:100 200
看到计算结果

案例

  1. 文件1:cmd.h
    参考代码

    /*cmd.h头文件卫士*/
    #ifndef __CMD_H
    #define __CMD_H
    
    //声明函数指针,将来指向每个命令对应的处理函数
    typedef int (*cb_t)(int ,int );
    
    //声明描述命令信息的结构体数据类型
    typedef struct cmd{
    const char *name;  //描述命令的名称:"add" , "sub" 等
    cb_t callback;  //描述命令对应的成立函数:cmd_add ,cmd_sub等
    }cmd_t;
    
    //声明查找命令函数
    extern const cmd_t *find_cmd(const char *);
    #endif
    

  2. 文件2:cmd.c
    参考代码:

    /*cmd.c :各种定义*/
    #include "cmd.h"
    #include   //为了用strcmp函数
    
    /*定义加法和减法处理函数*/
    int cmd_add (int a,int b){
    return a + b;
    }
    int cmd_sub (int a,int b){
    return a - b;
    }
    /*定义加,减命令的结构体变量:采用结构体数组*/
    const cmd_t cmd_tbl[] = {
    {"add",cmd_add},  //加法命令的结构体信息
    {"sub",cmd_sub}   //减法命令的结构体信息
    };
    
    /*定义求数组元素个数宏*/
    #define ARRAY_SIZE(x)  (sizeof(x)/sizeof(x[0]))
    
    /*定义根据命令名称在数组中查找命令函数*/
    const cmd_t *find_cmd(const char *name)
    {
    for(int i = 0;i<ARRAY_SIZE(cmd_tbl);i++){
    if(!strcmp(name,cmd_tbl[i].name))   //根据传递的名称到数组的每个元素中的名称进行比较
    return &cmd_tbl[i]; //返回匹配的命令的结构体的首地址
    }
    return NULL;  //没有找到
    };
    

  3. 文件3 main.c

    /*main.c各种调用*/
    #include
    #include
    #include "cmd.h"
    
    #define LEN (32)
    static char buf[LEN];
    
    int main(void)
    {
    //用户可以不断深入命令
    while(1){
    int a =0,b =0,ret = 0;
    printf("请输入命令名:");
    scanf("%s",buf); //获取命令
    //到cmd.c的数组中根据
    //命令的名称找到命令的结构体
    //一旦找到就可以调用命令处理函数
    //找到之后返回命令对应
    //结构体的首地址保存到p
    const cmd_t *p;
    p = find_cmd(buf);
    if(p != NULL){
    printf("请输入两个数字:");
    scanf("%d%d",&a,&b);
    
    //找到处理函数
    ret = p->callback(a,b);
    printf("%d\n",ret);
    }else{
    //没有找到
    if(!strcmp(buf,"quit"))
    return 0;  //结束程序
    printf("命令名错了.\n");
    }
    } return 0; }
    


你可能感兴趣的:(Linux,C,C++,c语言,c++,指针,运维开发,devops)