C语言进阶提升——指针


C语言高级编程
 

1. GCC 编译器 
    gcc 编译c文件的流程 
    (1) 预处理   处理 头文件展开  宏替换等 动作    不会进行任何语法检查
        gcc hello.c -E -o hello.i  
        -E 只进行预处理操作 
        -o 重命名 输出文件名字 
        hello.i  .i后缀 表示只进行预处理后的文件     
        
    (2) 编译   使用C编译工具  将预处理后的 C代码 编译为 汇编代码 
        gcc hello.i -S -o hello.s 
        -S 只进行 C编译为 汇编  
        .s 后缀 用于表示 汇编文件 
        
        该动作需要进行语法检查 
        
    (3) 汇编    使用汇编器  将汇编代码 编译为 二进制目标文件  不能单独运行 
        gcc  hello.s  -c -o hello.o 
        -c  仅进行 汇编, 生成 二进制目标文件
        .o  表示 二进制目标文件
    
    (4) 链接  将多个二进制目标文件 与库链接 生成可以执行的 二进制文件  
        gcc hello.o -o hello.out
        
        gcc 中的其他的选项  
        -I   用于指定头文件的搜索路径 
            gcc hello.c -c -o hello.o -I./inc

        -l   用于指定 链接第三方库的名字 
        -L   用于指定 链接第三方库的路径 
        -O   用于指定 编译优化等级  -O0 不优化  -O1 -O2 -O3    volatile 防止编译器优化
        -g   用于编译时 附加 调试数据  主要用于gdb调试 
    
GCC组件: 有4个组成部件 
    编译器   : C ---> 汇编 
    汇编器   : 汇编---> 二进制机器码
    链接器   : 链接多个二进制机器码 和 库 构成 可以运行的二进制程序 
    C库      : 一些有gcc提供的 常用的一些函数 
    
编译报错:
    1. 头文件错误    预处理阶段出现   通常为头文件路径错误或文件名写错了 
    
hello.c:5:19: fatal error: inc/a.h: 没有那个文件或目录
compilation terminated.
    
    2. 语法错误   在 编译阶段出现  通常为 标点符号或中文问题 
hello.c: In function ‘main’:
hello.c:10:2: error: expected ‘;’ before ‘func’
  func();
  ^
    中文符号问题 
hello.c: In function ‘main’:
hello.c:9:2: error: stray ‘\357’ in program
  printf("PI=%.2f\n", PI);
  ^
hello.c:9:2: error: stray ‘\274’ in program
hello.c:9:2: error: stray ‘\233’ in program
hello.c:10:2: error: expected ‘;’ before ‘func’
  func();
  ^
    3. 未定义标识符    变量/函数没有定义或声明就使用  
hello.c: In function ‘main’:
hello.c:12:19: error: ‘pelpe’ undeclared (first use in this function)
  printf("%.2d\n", pelpe);
                   ^
hello.c:12:19: note: each undeclared identifier is reported only once for each function it appears in

    4. 归档错误/ 链接错误   引用了其他文件或库中的函数 而没有声明 或 函数名字错误 
/tmp/cclYp5VM.o:在函数‘main’中:
hello.c:(.text+0x42):对‘func’未定义的引用
collect2: error: ld returned 1 exit status

作业:  输入一个数  分解质因数 
    20=2*2*5
    11=11

思路:  短除法  

逻辑错误:  bug  程序可以运行 但 没有达到预期效果甚至出现错误

调试工具:  软件工具 与 硬件工具 

    printf打印法调试 
    
    GDB 调试器: 软件调试工具 
    字符界面调试功能 

    1. 编译时添加-g 选择  附加调试信息 
    gcc zhishufenjie.c -g -o gdb.out

    2.     使用gdb运行调试程序 
    gdb gdb.out

GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from gdb.out...done.
(gdb) 

    gdb 命令   
    l  查看程序源码  
    设置断点    断点: 程序 运行到这里就会暂停 
    b 行号  
    info b  查看所有的断点 
    
    运行代码
    r 
    查看程序中变量的值 
    
    单步运行:   一次执行一行C代码  
    (gdb) n     运行一行
    (gdb) s      若这一行是函数调用,s则进入函数
    恢复程序运行 
    (gdb) c
    
    删除断点 
    del  1   删除断点1 
    
    quit  退出gdb  
    
C语言中  变量的存储类型 与 特性 

指针:  C语言的灵魂   手术刀      
    本质就是 一个直接访问内存 的工具   只有C语言有   

程序的本质:      就是操作内存  
    
指针是啥? 
    1. 指针变量    :   一个变量  存放一个内存的地址   
    2. 一个内存地址   指针常量 

指针变量的定义: 
    存储类型  指针指向的空间的数据类型 *  变量名;
示例: 
    int *p = &a;

指针变量的 赋值:  类型匹配才可以赋值 
    物理意义: 与指定的内存  建立联系 

&地址运算:   变量可取地址  有内存的量
    得到变量的内存地址   

野指针:  一个指针  指向的内存 不可知 或  不确定 或  非法(不可访问) 
空指针:  一个指针  指向内存 0地址(不允许访问)  即这个指针中的值是0   int *p = NULL;
宏 NULL 空指针   (void *)0  通常在程序中 表示一个指针 没有建立任何指向关系 

void * 类型 : 表示 万能指针, 一个可以指向 任意类型的指针 

指针的访问:  * 指针取值运算符     
   *指针变量  即等同于 指针指向的目标(对象)
    
    1  * 只能对指针操作 只能对有具体类型的指针 
    2 指针必须有建立指向关系   不能对空指针 野指针进行*操作 
            *空指针  段错误/空指针异常
            *野指针  结果未知   大概率段错误 
            void * 类型的指针  可以赋值  void *p = &a;    *p 不行   *(int*)p 可以的
                  不能*操作   通常是进行强制类型转换 为指定类型的  *
            
int b;
int *p = &b;
*&b; 等同于 b 
      * 是 & 的逆运算 
&*p; 等同于 &b
      & 是 * 的逆运算  

指针的算数运算: 
    指针 +或- 一个整数    其结果仍然是一个指针 
        其运算本质就是  地址的移动 + 向大地址方向移动  - 小地址方向移动 
        其移动的字节大小  由其指向的类型长度决定  +1 / -1 移动一个指向的类型 
    指针的 ++ -- 运算    p++; *p++; (*p)++; 
    
    指针 与 指针的 运算 
    1. 大地址 - 小地址  结果是一个整数  其大小为 这两个地址之间相隔的 指向的类型个数 
        (1)  这两个地址 必须在同一段连续的内存空间上  
        (2)  这两个地址 类型要一致 
    
    2. 关系运算   本质是 判断 这个两个地址 在内存中的位置  
    p  >  q   p地址 在 q地址的 大方向  
    p  <  q   p地址 在 q地址的 小方向
    p ==  q   p和q指针指向同一个内存 
    p !=  q   p和q指针指向不同的内存 
    p  >= q   p地址 在 q地址的 大方向  或 相同
    p  <= q   p地址 在 q地址的 小方向  或 相同
    
一级指针和一维数组: 
    数组名可以当指针用 
    指针变量 可以做数组用 
    区别在于  指针变量 可以重新赋值   而数组名不行 
    int arr[5] = {1,2,3,4,5};
    int *p = arr; // p = &arr[0]  等同 
    p[0]  == arr[0]  == *arr  == *p    //等同 
    p[1]  == arr[1]  == *(arr+1)  == *(p+1)
    
练习:  用指针方式遍历数组  写函数实现
void show_arr(int *arr, int len)   //要求使用arr和len完成遍历 不再定义其他变量
{
    while(len--) printf("%d ",*(arr++));
}

//*arr++   ==  *(arr++)  ==  先*arr用  然后 arr=arr+1; 

int main()
{
    int arr[] = {1,2,3,4,5};
    show_arr(arr,5);
    return 0;
}

有如下定义:
int a[10]={0,1,2,3,4,5};
int *p = a;
假设每个表达式中p=a;求表达式代表的值?
*p=?    a[0]   == 0 
*a=?    a[0]   == 0
p[0]=?  a[0]   == 0 
a[0]=?  
*p++=?  ==  *(p++) == 先*p用 作为表达式的值, 然后 p=p+1
*(p++)=?
*p+1=?     a[0] + 1 == 1
*(p+1)=?   a[1]  == 1
*a+1=?     a[0] + 1 == 1
*(a+1)=?   a[1]  == 1
a++;   不行  a数组名  常量指针 不能赋值


练习: 数组倒叙 
void arr2rra(int *start, int *end)   //倒叙 从start到end之间的 数组元素, 不能定义其他变量
{

}
int main()
{
    int arr[] = {1,2,3,4,5};
    show_arr(arr,5);
    arr2rra(arr, arr+4);
    show_arr(arr,5);
    return 0;
}

指针与字符串:

char s[] = "hello";  //s是一个数组 将"hello"复制一份到s数组中

cosnt char *p = "hello";  // p是一个指针变量 指向常量区字符串 
    *p 

求字符串的长度:
int len(const char *s)
{
    char *p = s;
    while(*p) p++;
    return p-s;
}

const 修饰指针: 
    cosnt char  *p;              *p 只读   p 可读可写 
    char * const p;              *p 可读可写  p 只读 
    const char * const p ;      *p 只读   p 只读 

作业: 使用指针完成
    1. 输入一个字符串  倒叙输出 字符串     
    2. 大数加法 
a = "128346197325817632587412435878032743712098317867293728651279"  '9' - '0' = 9
b = "908020203392378777239878950493849938744869530984603429835934"  '4' - '0' = 4
c = a+b                                                   ...7213   '3'  =(9+4+进位标记)%10+'0'
c 字符串 char c[200];  
    
预习:  指针变量 占用内存的大小  sizeof(p);
    1. 指针数组     数组指针  

总结指针的 5个运算:
    =  赋值运算  建立指向关系  
    *  取指向对象     *p 
    +/- n  地址移动
    p-q  指针相减 
    p>q  关系运算 

指针占用的内存大小: 
    指针变量指向的类型 不影响其 占用的内存大小  
    若 是32位机器   地址大小为 4字节 
    若 是64位机器   地址大小为 8字节 

指针数组 与 二级指针 :

指针数组: 就是一个数组其元素是  指针 
    如图所示 : 
    ps 是一个  常量字符串指针数组 
    定义:
    存储类型  元素类型   数组名[元素个数] = {};
    auto     cosnt char * ps[6] = {"Beijing", "Moscow", "New York", "","", NULL};

    遍历指针数组:
    for(int i= 0; i<5; i++)
        printf("%s ",ps[i]);

    指针方式遍历:
//二级指针
const char ** pps = ps;

    //遍历指针数组:
    for( int i = 0; i<5; i++)
        printf("%s ",*pps++);
    printf("\n");
    
练习: 
char *a[3] = {"hello","world","ok"};
char **p = a;
 
 假设每个表达式中p=a;求表达式代表含义或值?
a[0] == "hello" == char*  == %s 
*p   == a[0]  
**p  == *a[0] == 'h'
*a   == a[0]
**a  == *a[0] == 'h'
*p++ == a[0] 然后 p=p+1
*(p++) == *p++
**(p+1)) ==  *  "world"的地址 == 'w'
 
*(*(p+1)+1)  == *(a[1]+1) == 'o'
 
(*p)++  考虑p能否进行该操作? 可以操作 
    先*p == a[0]  然后  a[0] = a[0]+1; a[0] ---> "ello"

数组指针 与  二维数组:
二维数组:
int arr[2][3] = {1,2,3,4,5,6};
数组指针:  含义: 一个指针 指向一个一维数组 
int    (*p)[3] = arr;  // p = &arr[0]; 

&arr[0]=0x7ffc4307c1a0   // 数组指针类型  
 arr   =0x7ffc4307c1a0   // 数组指针类型 
 arr[0]=0x7ffc4307c1a0   // 常量int类型指针 
&arr[0][0] =0x7ffc4307c1a0 // 常量int类型指针  

问p+1是什么?  p+1  == arr[1]
练习:
int a[2][3];
int (*p)[3] = a;
 
假设每个表达式中p=a;求表达式代表含义?
a[0]    0号元素是一个一维数组 
*p      == a[0]
**p     == a[0][0] 
*a      == a[0]
**a     == a[0][0] 
*p++    == 先*p == a[0] 然后  p = p+1;  p----> a[1]
*(p++)  == *p++  
**(p+1) == *a[1] == a[1][0]
 
*(*(p+i)+j)
        ==  *(a[i]+j)  == a[i][j]
        
(*p)++  考虑p能否进行该操作?  不能  
    先*p == a[0] 然后  然后  a[0] = a[0]+1;  数组名不能赋值 

指针与函数: 
    指针函数:
        本质是一个函数, 返回值是一个指针 
    
    函数指针:
        本质是一个指针, 指向一个函数 

代码段: 
    存储代码 

    函数名:
        1. 代指这个函数本身
        2. 代指这个函数的首地址 
    
函数类型: 
    除去函数名 以及形参名等 名字  剩下的就是函数的类型 

函数指针定义:
    指向的函数返回值类型    (*函数指针名)(函数形参类型列表) = 对应类型的函数;
    
函数指针应用:  回调函数 多用于写接口  
接口: API  写接口时,该接口的有些内容还没有确定, 因此通过函数指针的方式 来传递参数

回调函数: 一个函数 其参数 有函数指针类型  

    
案例:  要求写一个函数,  参数传入一个数组, 要求输出 满足调用者条件的数?  参考funcp.c文件
void    func1(int arr,int len, 函数指针 )
    
创建线程的函数:  了解 
           int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
    
void *(*start_routine) (void *):
    
    start_routine 是一个指针  指向一个函数  该函数类型 为 void * (void *)
    void * (void *) 是 一个函数类型  这个类型的函数 有一个 void*类型的返回值 
                        有一个 void * 的形参 
    
函数指针数组:
    本质是一个数组  数组中元素是 函数指针  
    
递归函数: 
    函数中 存在 自己调用自己 (递推关系) 
    每次自己调用自己 问题规模在逐渐减小 , 向临界条件趋近
    临界条件: 退出自己调用自己 的条件 
    
示例: 递归求解 1+2+3+..+ n = ??
    求第n项的前n项和  S(n) = n + S(n-1);  递推式 
    临界条件          n == 1  S(n) == 1;
    
    递归的特性: 
        本质就是 对栈区空间的 利用
        递推过于 深入  可能导致栈区溢出 段错误 
        效率不高
        在一些特定情况下, 使用递归可以 简化代码量 
斐波那契数列
    1 1 2 3 5 8 13 21 34 55....
    A(n) = A(n-1) + A(n-2);
    n == 1 || n == 2  A(n) ==1;
        
作业: 
(1)杨辉三角 

1 1
1 2 1
1 3 3  1
1 4 6  4  1
1 5 10 10 5 1
.....

递推式: A(i,j) = A(i-1,j) + A(i-1,j-1)
临界条件: ( j == 1 || i = j ) A(i,j) == 1

求 第10行 第7列的值 

(2) 递归方式 实现选择排序 


结构体: 
    是一个构造数据类型  

引入: 描述一个人  
    姓名name   性别sex  年龄age   身高 high
    char[20]  char[10]   int      float 

有编程者  先定义一个结构体类型  然后利用定义的类型 定义变量 

成员: 结构体中的 数据对象 称 成员 

1. 定义一个 人 结构体类型 

struct 类型名
{//成员表
    成员类型 成员名; 
    char sex[10];
    int  age;
    float high;
};
示例:
struct people
{//成员表
    char name[20]; 
    char sex[10];
    int  age;
    float high;
};


2. 使用类型定义变量  
    //用结构体类型 定义变量 才会分配内存空间
    struct people zhang3;
    //定义变量并初始化
    struct people li4 = { "李4" , "男" , 18 , 1.80 };

3. 结构体变量 成员引用  
    . 成员引用运算符 

    结构体变量名.成员名 ;
    
    结构体变量 类型相同可以直接赋值;
    
4. 结构体指针 
    一个指针指向一个结构体 
    通过结构体指针 访问结构体  
    ->  结构体指针成员引用运算符 

5. 类型别名  给类型取别名 
    typedef  原类型名  别名;

//定义类型别名
typedef struct people  ren_t;
typedef struct people * ren_p;    

    定义类型同时就取别名
typedef struct people
{//成员表
    char name[20]; 
    char sex[10];
    int  age;
    float high;
} ren_t ;

    定义类型同时就定义变量
struct people
{//成员表
    char name[20]; 
    char sex[10];
    int  age;
    float high;
} zhang3;      
    
    定义类型同时就定义变量同时初始化 
struct people
{//成员表
    char name[20]; 
    char sex[10];
    int  age;
    float high;
} zhang3 = {"张三" , "男" , 18 , 1.80};     

关于结构体的类型:
    结构体的类型  由定义结构体类型时给定的名字决定, 名字不同则 类型不同 

无名结构体: 
struct
{//成员表
    char name[20]; 
    char sex[10];
    int  age;
    float high;
} zhang3;

结构体存储模型: 
    1.结构体类型 成员顺序 在内存中 按同样顺序排列 
    2.遵循紧凑排列 
    3.默认遵循 自动字节对齐 

字节对齐: 
    计算机为了 方便快速找到一个变量的位置  因此而引入字节对齐
    多字节类型(int,short) 要求其首地址 应该能够被 其长度 整除 

#pragma pack (2) /*指定按2字节对齐,后面定义的结构体均遵循这个规则*/
 
#pragma pack () /*取消指定对齐,恢复缺省对齐 自动字节对齐*/

结构体数组 :  
    本质一个数组  其元素是 结构体 
struct people    arr[5]; 
    0号元素的name  arr[0].name
    
结构体指针数组 
    本质一个数组  其元素 结构体指针 
struct people *arr[5];
    0号元素的name  arr[0]->name 

结构体数组指针 
    本质一个指针  指向 一个数组  数组元素是结构体 
struct people    (*p)[5];
    访问p指向的数组中的 0号 结构体中的 name 
    (*p)[0].name ;
    
结构体中的成员是一个结构体:
struct stu
{
    struct people ren;
    int id;
};

struct stu  zhang3; 
    zhang3.ren.name; 

struct people
{//成员表
    char name[20]; 
    char sex[10];
    int  age;
    float high;
    struct people *next;
};

共用体: 
    跟结构体类似, 区别在于 共用体成员  共用 同一块内存空间 
    其占用内存大小由 最大空间的那个成员决定 
    
定义 共用体类型 :
union 共用体类型名
{
    成员类型  成员名;
    ....
};

定义共用体变量:
union 共用体类型名  变量名; 
案例:
写一个函数  实现 两个数求和 
    这两个数 有可能是int型 也有可能是 float 

//定义一个共用体类型
union num_t
{
    int   i_t;
    float f_t;
    char  c_t;
};
// 共用体应用案例  
struct msg
{
    int type; //存放消息类型 
    union num_t // 共用体 存储消息本身 
    {
        int   i_t;
        float f_t;
        char  c_t;
    };
};

枚举类型:   参考示例 union.c 
    对常量进行 取别名 
enum  类型名 
{
    常量表;
};

动态内存分配: 
    就是向操作系统  申请堆区内存 
    可以在程序运行中 动态的 申请你想要的大小 
    
内存的申请 与 释放:
#include <stdlib.h>

       void *malloc(size_t size);  //申请内存 
       void free(void *ptr);       //释放内存

size: 要申请的内存大小 单位字节 
void *: 返回值  是一个无类型指针  
        申请成功时  该指针指向 你申请的堆区内存 
        申请失败    返回 NULL指针
        
    使用堆区内存时, 必须先将堆区内存地址 强制转换为 你要的内存 
    然后才能访问 

    ptr: 你申请时 系统给的堆区地址, 释放是需要作为free的参数 
    
关于申请与释放 注意事项:
    1. 申请时 给的堆区地址, 在没有释放前,不能丢失  
        丢失堆区指针,将导致 内存泄漏  
        生命周期 从申请开始 到释放结束
    2. 已经释放的 堆区内存 不能再访问了  继续访问 则段错误 
    3. 不能重复释放 
    4. 不能部分释放     

通常  malloc 申请的空间  
struct people* p = (struct people*)malloc(sizeof(struct people)); 
p->name 

练习: 
    申请一个结构体大小的堆区空间  输入 一个struct people 类型的结构体 
    
makefile工程管理器:   用于辅助编译的工具 
    对多文件编译时, 可以自动 对改变的.c文件进行 重新编译 
    通过对 文件的 时间戳  来判断.c文件与.o的新旧 来确定是否需要重新编译 
    
使用: 
    make 
    make 目标 
    
编写makefile,来配置工程管理器的编译过程 

编写规则:
目标:依赖 
<table>shell命令 用于将依赖编译为 目标文件的 shell命令  
    
变量定义: 
shell定义变量类似
变量名 = 值 

自动变量:
$*   不包含扩展名的目标文件名称
$+   所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件
$<    第一个依赖文件的名称
$?    所有时间戳比目标文件晚的的依赖文件,并以空格分开
$@  目标文件的完整名称
$^     所有不重复的目标依赖文件,以空格分开
$%    如果目标是归档成员,则该变量表示目标的归档成员名称

参考 示例 makefile 


    
    


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值