1.程序的编译和链接
预处理: 头文件展开, 文本替换, 去注释, 条件编译
汇编: 进行语法分析 + 符号汇总
编译: 将代码转换为二进制指令
链接: 合并文件: 符号解析, 重定位(修正地址), 生成符号表(上帝视角者, 需要找到对应函数的定义, 我们直接包含头文件而使用的库函数是需要找到其定义的)
2.#define定义宏
- 宏: 文本替换
- 缺点: a.没有类型检查, 无法调试. b.容易受优先级的影响 c.无法复用可能导致代码膨胀
- 优点: a.没有函数栈帧的开销(速度快)
1.定义常量
#define PI 3.14159
#define MAX_VALUE 100
2.定义函数
#define SQUARE(x) ((x) * (x))
3.代码片段替换
#define DEBUG_PRINT(x) printf("DEBUG: " #x " = %d\n", x)
3.条件编译
-
一种预处理器的功能,允许在编译时根据条件选择性地包含或排除代码的一部分
-
种类:
#if
、#ifdef、
#ifndef
、#elif
和#else
1.#if:根
据给定的条件表达式决定是否编译其中的代码块。如果条件表达式为真(非零),则编译代码块;否则,忽略代码块。
#if 条件表达式
// 代码块
#endif
//可以用来测试不同的代码
//代码有效
#if 1
// 代码块
#endif
//代码无效
#if 0
// 代码块
#endif
2.#ifdef:检查一个宏是否已经定义。如果宏已经定义,则编译代码块;否则,忽略代码块。
#ifdef 宏名称
// 代码块
#endif
3.ifnedf:检查一个宏是否未定义。如果宏未定义,则编译代码块;否则,忽略代码块。
#ifndef 宏名称
// 代码块
#endif
4.#elif:用于添加额外的条件分支。如果前面的条件(#if
或#elif
)不满足,而当前条件满足,则编译代码块。
#if 条件表达式1
// 代码块1
#elif 条件表达式2
// 代码块2
#else
// 代码块3
#endif
5.#else:
#if 条件表达式
// 代码块1
#else
// 代码块2
#endif
避免一个头文件被重复包含多次
1.使用预处理指令#pragma once
#ifndef HEADER_FILE_NAME_H
#define HEADER_FILE_NAME_H
// 头文件的内容
#endif
2.使用 #pragma once
4.头文件包含的 “ ” 和<>的区别
- “ ”:先在源文件所在目录下查找, 如果该头文件未找到,在标准路径下查找头文件。
- <>:查找头文件直接去标准路径下去查找.
5.原码, 反码, 补码
- 正数的原码, 反码, 补码相同
- 负数: 反码通过原码取反得到, 补码通过反码+1得到, 补码+1可以得到原码
C语言中的整数在内存中存储的是补码
示列:
数值 | 原码 | 反码 | 补码
------|----------|----------|----------
+0 | 00000000 | 00000000 | 00000000
-0 | 10000000 | 11111111 | 00000000
+3 | 00000011 | 00000011 | 00000011
-3 | 10000011 | 11111100 | 11111101
从上表可以看出,原码和反码表示中存在两个零,而补码表示中只有一个零,避免了数值的重复。
1.简化运算:使用补码可以简化加法、减法和乘法等运算
2.唯一表示:使用补码可以避免出现两个零的问题,只有一个零值。
3.范围表示:补码能够更好地表示有符号整数的范围,因为最高位被用作符号位,其余位用于表示数值。
6.大小端字节序
- 字节序: 数据在内存中的存储方式
- 大端: 低位字节存高地址
- 小端: 低位字节存低地址
- 例如: 0x12345678 --大端:0x12 0x34 0x56 0x78 -- 小端: 0x78 0x56 0x34 0x12
- 内存中: 从左往右为低地址->高地址, 在数学中为高位->低位
判断大小端的方法: 给一个整数1取第一个字节验证-->大端: 0, 小端1
方法1: 指针
int judge_sys()
{
int value = 1;
return *(char*)(&value);// 1 : 小端 0 : 大端
}
方法2: 联合体
//小端: 1 大端: 0
int judge_sys()
{
union judge
{
int value;
char arr[4];
};
union judge data;
data.value = 1;
return data.arr[0];
}
7.浮点数在内存中的存储
符号位 指数位 尾数位
----------------------------------------
| s | e | f |
----------------------------------------
8.0在内存中的存储: s = 0. e = 3(在内存中加上127), f = 0 然后转换为二进制即可
- 符号位(s): 占用1个bit位, 0表示正, 1 表示负
- 指数位(e): 占用8个bit位, 用于表示浮点数的指数部分
- IEEE 754标志规定E的实值要加一个中间数127(32位下), 1023(64位下)
- 尾数位(f): 占用23个bit位, 用于表示浮点数的尾数部分.
- IEEE 754标准规定 高位默认为1, 不显式存储
8.结构体
//自定义类型
struct Person {
char gender;
char name[15];
int age;
};
内存对齐
- 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:为了提高效率: 读取一个数据的时候尽可能一次就读取完, 而不是多次, 也确保了读取数据的完整性. 如上面若不内存对齐,获取age需要读2次
结构体内存对齐的规则
- 第一个成员在与结构体变量偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值. VS的默认值是8.
可以使用pragma pack()指令来设置默认对齐数
9.联合体
//在同一块内存空间中存储不同类型的数据。联合体中的各个成员共享同一块内存
union union_name {
member_type1 member_name1;
member_type2 member_name2;
// 可以有更多的成员
};
10.枚举
//是一种用户定义的数据类型,用于定义一组命名的常量
enum Weekday {
Monday, //0
Tuesday, //1
Wednesday, //2
Thursday, //3
Friday, //4
Saturday, //5
Sunday //6
};
11.指针
指针:一种变量类型, 用来存储地址, 通过指针我们可以直接访问该位置的数据
指针的类型决定了1.其解引用能访问多少个字节2.++,--能跨越多少字节
指针的种类
//1.字符指针
char *ptr = "hello world";
//2.数组指针
int (*p)[10]; //指向一个大小内容为10的数组
//3.指针数组
int* arr[10];
//4.函数指针
void (*pfunc)();
//5.函数指针数组
int (*parr1[10]])();
//6.指向函数指针数组的指针
//(*parr1) 表示是指针
//int (*[10])(); 表示数组, 类型是函数指针
int (*(*parr1)[10])();
数组与指针
传参
一维数组
#include <stdio.h>
void test(int arr[]){}
void test(int arr[10]){}
void test(int *arr){}
void test2(int *arr[20]){}
void test2(int **arr){}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}
arr:一维数组(int), arr表示首元素的地址, 类型是int, 使用一级指针接收
arr2:一位数组(int*), arr表示首元素的地址,类型是int*, 一级指针的地址用二级指针接收
或者是什么就用什么接收
二维数组
void test(int arr[3][5]){}
void test(int arr[][5]){}
void test(int (*arr)[5]){}//数组指针
int main()
{
int arr[3][5] = {0};
test(arr);
}
arr是二维数组, 首元素可以抽象为一个一维数组, 首元素的地址用数组指针接收
或者传什么就用什么接收
&数组名与数组名
- 一维数组
- 二维数组
- 数组名: 首元素的地址
- &数组名: 整个数组的地址
- 指针的大小: 4/8个字节
- sizeof(数组名), 计算的是整个数组的大小 (这里数组名表示整个数组)
- &数组名, 取出的是整个数组的地址。(这里数组名表示整个数组)
指针的使用
- 修改元素
- 回调函数
- 保存某元素的地址
12.动态内存的开辟
- malloc: 从堆中分配一块指定大小的内存空间, 并返回指向该内存块的指针
- calloc: 同malloc, 不过会将内存初始化为0
- realloc: 在指定的空间上进行扩容(原地扩, 异地扩)
- free: 释放申请的空间
操作
malloc
函数:void* malloc (size_t size);
功能:申请空间
calloc
函数:void* calloc (size_t num, size_t size);
功能:申请空间并初始化为0
realloc
函数:void* realloc (void* ptr, size_t size);
功能:扩容
若指针为NULL, 功能同malloc
若指针非NULL,size为0. 则释放内存并返回空指针
free
函数:void free (void* ptr);
功能:释放一块空间
指针可以为NULL(什么都不做)
不要多次释放同一块空间
不要释放一块空间的一部分