C语言: 语法知识

文章详细阐述了程序的编译和链接过程,包括预处理(头文件展开,文本替换等)、汇编、编译和链接阶段。提到了预定义符号如__FILE__和__LINE__,以及预处理指令如#include和#define用于宏定义。宏定义介绍了其优点和缺点,如速度优势但缺乏类型检查。文章还讨论了条件编译的使用,如#if、#ifdef和ifndef等,以及头文件包含时“”和“<></>”的区别和防止重复包含的策略。
摘要由CSDN通过智能技术生成

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(什么都不做)
不要多次释放同一块空间
不要释放一块空间的一部分

​​​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值