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
调试工具
软件工具 与 硬件工具
printf打印法调试
GDB 调试器: 软件调试工具
字符界面调试功能
1. 编译时添加-g 选择 附加调试信息
gcc zhishufenjie.c -g -o gdb.out
2. 使用gdb运行调试程序
gdb gdb.out
(gdb)界面
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 移动一个指向的类型
指针的 ++ -- 运算
指针 与 指针的 运算
1. 大地址 - 小地址 结果是一个整数 其大小为 这两个地址之间相隔的 指向的类型个数
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)
指针与字符串:
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 只读
总结指针的 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");
数组指针 与 二维数组:
二维数组:
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类型指针
指针与函数:
指针函数:
本质是一个函数, 返回值是一个指针
函数指针:
本质是一个指针, 指向一个函数
代码段:
存储代码
函数名:
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;
递归的特性:
本质就是 对栈区空间的 利用
递推过于 深入 可能导致栈区溢出 段错误
效率不高
在一些特定情况下, 使用递归可以 简化代码量
递归方式 实现选择排序
结构体:
是一个构造数据类型
引入: 描述一个人
姓名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
makefile工程管理器: 用于辅助编译的工具
对多文件编译时, 可以自动 对改变的.c文件进行 重新编译
通过对 文件的 时间戳 来判断.c文件与.o的新旧 来确定是否需要重新编译
使用:
make
make 目标
编写makefile,来配置工程管理器的编译过程
编写规则:
目标:依赖
<table>shell命令 用于将依赖编译为 目标文件的 shell命令
变量定义:
shell定义变量类似
变量名 = 值
自动变量:
$* 不包含扩展名的目标文件名称
$+ 所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件
$< 第一个依赖文件的名称
$? 所有时间戳比目标文件晚的的依赖文件,并以空格分开
$@ 目标文件的完整名称
$^ 所有不重复的目标依赖文件,以空格分开
$% 如果目标是归档成员,则该变量表示目标的归档成员名称