C语言笔记

王道计算机【C语言督学营】完整笔记 【王道C语言初级阶段(C语言入门代码)】【王道C语言中级阶段(C语言入门代码)】 阅读详情

我的Github:HIROaaaa

一、C语言常见概念

1.编译器

C语⾔是⼀⻔编译型计算机语⾔, ⽂本⽂件本⾝⽆法执⾏,必须通过 编译 器翻译 和 链接器的链接 ,生成 二进制 的可执行文件( 程序运行必须经过编译和链接两个过程)

2.main函数

C语言程序由main函数开始,main 函数是程序的⼊⼝,main 函数也被叫做主函数。int 表⽰ main 函数执⾏结束的时候返回⼀个整型类型的值。

main函数有且仅有一个

3.库函数

C语言标准规定了一组函数,这些函数组成了一个函数库,被称为标准库。

4.关键字

C语言中有一批保留的名字的符号,这些符号被称为保留字或者关键字。

关键字都有特殊的意义,是保留给C语⾔使⽤的

程序员⾃⼰在创建标识符的时候是不能和关键字重复的

5.字符和ASCII编码

键盘上可以敲出各种字符,如:a,q,@,#等,这些符号都被称为字符,C语⾔中字符是⽤单引号括起来的。
每个字符都有一个二进制序列,叫做编码(ASCII编码)

6.ASCII码表

1.特殊的几个编码:
字符A~Z的ASCII码值从65~90   A是65
字符a~z的ASCII码值从97~122   a是97
对应的⼤⼩写字符(a和A)的ASCII码值的差值是32
数字字符0~9的ASCII码值从48~57   0是48
换⾏ \n 的ASCII值是:10
在这些字符中ASCII码值从0~31 这32个字符是不可打印字符,⽆法打印在屏幕上观察

7.字符串和\0

使⽤双引号括起来的⼀串字符就被称为字符串,如:"abcdef",就是⼀个字符串
字符串的末尾隐藏放着⼀个 \0 字符,这个 \0 字符是字符串 的结束标志。

8.转义字符

\n 是⼀个转义字符表⽰换⾏的意思,我们可以简单的理解为 \ 让 n 的意思
发⽣了转变, n 本来是⼀个普通的字符,被 \ 转义为换⾏的意思。
下⾯2种转义字符可以理解为:字符的 8进制或者16进制表⽰形式
\ddd :d d d表⽰1~3个⼋进制的数字。 如: \130 表⽰字符X
\xdd :d d表⽰ 2个⼗六进制数字 。 如: \x30 表⽰字符0

语句和语句分类

C语言的语句可以分为以下五类

1.空语句

空语句是最简单的, ⼀个分号就是⼀条语句 ,是空语句。
空语句,⼀般出现的地⽅是:这⾥需要⼀条语句,但是这个语句不需要做任何事,就可以写⼀个空语句。

2.表达式语句

表达式语句就是在表达式的后边加上分号
int main()
{
 int a = 20;
 int b = 0;
 b = a + 5; //表达式语句
 return 0;
}

3.函数调用语句

函数调用时会加上分号

int Add(int x, int y)
{
 return x+y;
}
int main()
{
 printf("hehe\n");//函数调⽤语句
 int ret = Add(2, 3);//函数调⽤语句 
 return 0;
}

4.复合语句

成对括号中的代码构成⼀个代码块,也被称为复合语句

5.控制语句

控制语句⽤于控制程序的执⾏流程,以实现程序的各种结构⽅式(C语⾔⽀持三种结构:顺序结构、选择结构、循环结构),它们由特定的语句定义符组成,C语⾔有九种控制句。
条件判断语句也叫分⽀语句:if语句、switch语句;
循环执⾏语句:do while语句、while语句、for语句;
转向语句:break语句、goto语句、continue语句、return语句。

注释

注释有2种形式:
1./**/ 的形式
2.// 的形式
不管是哪⼀种注释,都不能放在双引号⾥⾯。双引号⾥⾯的注释符号,会成为字符串的⼀部分,解释为普通符号,失去注释作⽤。
printf("hello/*这里是注释*/world");
编译时,注释会被替换成⼀个空格,所以 min/* 这⾥是注释 */Value 会变成 min Value ,⽽不 是 minValue

二、数据类型和变量

1.数据类型

1.字符型

char //character
[signed] char //有符号的
unsigned char //⽆符号的

2.整型

//短整型
short [int]
[signed] short [int]
unsigned short [int]
//整型
int
[signed] int
unsigned int
//⻓整型
long [int]
[signed] long [int]
unsigned long [int]
//更⻓的整型
//C99中引⼊
long long [int]
[signed] long long [int]
unsigned long long [int]

3.浮点型

float
double
long double

4.布尔类型

C 语⾔原来并没有为布尔值单独设置⼀个类型,⽽是使⽤整数 0 表⽰假,⾮零值表⽰真。
在 C99 中也引⼊了 布尔类型 ,是专⻔表⽰真假的。
_Bool
布尔类型的使⽤得包含头⽂件 <stdbool.h>
布尔类型变量的取值是: true 或者 false 
#define bool _Bool
#define false 0
#define true 1

运用:

_Bool flag = true;
if (flag)
 printf("i like C\n");

5.signed 和 unsigned

signed 和 unsigned 关键字修饰字符型和整型
signed 关键字,表⽰⼀个类型带有正负号,包含负值;(有符号整型)
unsigned 关键字,表⽰该类型不带有正负号,只能表⽰零和正整数。(无符号整型)
int类型默认带有正负号
整数变量声明为 unsigned 的好处是,同样⻓度的内存能够表⽰的最⼤整数值,增⼤了⼀倍。
unsigned int ⾥⾯的 int 可以省略
unsigned a;
字符类型 char 也可以设置 signed 和 unsigned 。
signed char c; // 范围为 -128 到 127
unsigned char c; // 范围为 0 到 255

6.数据类型的表示

为了代码的可移植性,需要知道某种整数类型的极限值时,应该尽量使⽤这些常量。
SCHAR_MIN , SCHAR_MAX :signed char 的最⼩值和最⼤值。
SHRT_MIN , SHRT_MAX :short 的最⼩值和最⼤值。
INT_MIN , INT_MAX :int 的最⼩值和最⼤值。
LONG_MIN , LONG_MAX :long 的最⼩值和最⼤值。
LLONG_MIN , LLONG_MAX :long long 的最⼩值和最⼤值。
UCHAR_MAX :unsigned char 的最⼤值。
USHRT_MAX :unsigned short 的最⼤值。
UINT_MAX :unsigned int 的最⼤值。
ULONG_MAX :unsigned long 的最⼤值。
ULLONG_MAX :unsigned long long 的最⼤值。

7.各种数据类型的长度

2.变量

类型是⽤来创建变量的

全局变量:在⼤括号外部定义的变量就是全局变量
全局变量的使⽤范围更⼴,整个⼯程中想使⽤,都是有办法使⽤的
局部变量:在⼤括号内部定义的变量就是局部变量
局部变量的使⽤范围是⽐较局限,只能在⾃⼰所在的局部范围内使⽤的
当局部变量和全局变量同名的时候,局部变量优先使⽤

1.变量的存储

内存有三个区域:栈区、堆区、静态区

局部变量放在栈区
全局变量放在静态区
堆区用来动态内存管理

2.科学计数法

3.占位符

1.占位符列举

%c不忽略空白字符(空格、换行符、制表符),可以通过scanf(" %c", &ch)来跳过字符前的空白字符

%s是从第一个非空白字符开始到第一个空白字符结束,所以单个%s无法读取有空格的字符串,即scanf不适合读取有空格的字符串

scanf读取字符串后会在字符串末尾加一个"\0"

2.printf限定输出宽度
printf() 允许限定占位符的最⼩宽度
int main()
{
 printf("%5d\n", 123); // 输出为 " 123"
 return 0;
}

%5代表限制输出的整型的宽度最少为5位,不满5位会自动在右侧补空格

对于⼩数,这个限定符会限制所有数字的最⼩显⽰宽度

// 输出 " 123.450000"
#include <stdio.h>
int main()
{
 printf("%12f\n", 123.45);
 return 0;
}

输出的数字中的'.'也被算作是1位!

%.5f表示限制浮点数的小数位数为5位
int main()
{
 printf("Number is %.5f\n", 0.5123451);
 return 0;
}
//输出“0.51234”

限制小数位数可以和限制输出字符串宽度一起使用

#include <stdio.h>
int main()
{
 printf("%6.2f\n", 0.5);
 return 0;
}
//输出“  0.50”

类似的,还可以限制字符串的输出("%.[m]s")

// 输出 hello
#include <stdio.h>
int main()
{
 printf("%.5s\n", "hello world");
 return 0;
}

但是此时不能与限制字符串宽度一起使用

限制字符串宽度和限制小数位数可以用'*'代替从而做到输入想要限制的宽度和位数

printf("%*.*f\n", 6, 2, 0.5);
//相当于
printf("%6.2f\n", 0.5);

3.printf输出总是显示正负

默认printf输出时只有负数会加'-'而正数不会加'+',可以通过在'%'加一个 '+'来解决

printf("%+d\n", 12); // 输出 +12
printf("%+d\n", -12); // 输出 -12

4.scanf用法

scanf() 函数⽤于读取⽤⼾的键盘输⼊
scanf() 必须提前知道⽤⼾输⼊的数据类型,才能处理数据
scanf() 处理数值占位符时,会⾃动过滤空⽩字符,包括空格、制表符、换⾏符等
scanf运行时,会先将用户的输入信息放入缓存,按下回车键后,会按照占位符对缓存进行解读,直到   所有占位符处理完毕  /   遇到了不符合条件的字符
所以除非遇到错误的输入(不符合条件的字符)或者读取完要读取的数据,不然scanf不会结束
scanf读取数据时,遇到不符合条件的字符会停止,在 下一次读取开始时会从上次一不合条件的字符开始
int main()
{
 int x;
 float y;
 
 // ⽤⼾输⼊ " -13.45e12# 0"
 scanf("%d", &x);
 printf("%d\n", x);
 scanf("%f", &y);
 printf("%f\n", y);
 return 0;
}

上面示例中,第一个scanf要读取整型数据,所以只读到-13,遇到'.'后因为不合条件停止,第二个scanf读取浮点型,会从之前没有读完的数据开始读取,读取到.45e12,后面的#不合条件,读取会停止

由于 scanf() 可以连续处理多个占位符,所以上⾯的例⼦也可以写成下⾯这样
int main()
{
 int x;
 float y;
 
 // ⽤⼾输⼊ " -13.45e12# 0"
 scanf("%d%f", &x, &y);
 return 0;
}

1.返回值

scanf返回值是一个整数,表示成功读取到的变量的数量

没有读取到变量或者匹配失败则返回0

没有读取到变量之前,读取错误或者读取到文件结尾,返回EOF(-1)(EOF - end of file ⽂件结束标志)

程序运行时按“ctrl+z”会提前结束输入

scanf也可以限制字符串读取的最大长度(%[m]s)

#include <stdio.h>
int main()
{
 char name[11];
 scanf("%10s", name);
 
 return 0;
}
上⾯⽰例中, name 是⼀个⻓度为11的字符数组, scanf() 的占位符 %10s 表⽰最多读取⽤⼾输⼊ 的10个字符,后⾯的字符将被丢弃,这样就不会有数组溢出的风险了(最后一位留给"\0")

2.赋值忽略符

输入不符合预定的字符时,赋值忽略符可‘*’以使该位置的输入被丢弃

#include <stdio.h>
int main()
{
 int year = 0;
 int month = 0;
 int day = 0;
 scanf("%d%*c%d%*c%d", &year, &month, &day);
 return 0;
}

比如上面的例子中输入年份可能是2025.11.8也可能是2025-11-8,加入赋值忽略符就可以避免数据解析失败

三、分支和循环 

C语⾔是结构化的程序设计语⾔,这⾥的结构指的是顺序结构、选择结构、循环结构,C语⾔是能够实现这三种结构的,其实我们如果仔细分析,我们⽇常所⻅的事情都可以拆分为这三种结构或者这三种
结构的组合。

1.if语句

目前跟人感觉无特别需要注意的点

2.switch 语句

switch 后的 expression 必须是整型表达式,也可以是字符,字符会转换成整型
case 后的值,必须是整形常量表达式
case 和后边的数字之间必须有空格
每⼀个 case 语句中的代码执⾏完成后,需要加上 break ,才能跳出这个switch语句
case和default没有顺序要求

3.while循环

无

4.for循环

无

5.do while循环

无

6.break和continue语句

break只终止其所在的那一层循环

continue 的作⽤是跳过本次循环中 continue 后边的代码,在 for 循环和 while 循环中有所差异
continue在for循环里会使循环直接到下一次的调整部分

7.goto语句

goto 语句可以实现在同⼀个函数 内跳转到设置好的标号处。
一般不适用goto语句,会导致代码执行流程很乱
goto语句主要是用来跳出深层嵌套的循环

8.随机数生成

rand

需要包含头文件<stdlib.h>

C语⾔提供了⼀个函数叫 rand ,这函数是可以⽣成随机数的
rand函数会返回⼀个伪随机数,这个随机数的范围是在0~ RAND_MAX 之间,这个RAND_MAX的⼤⼩是 依赖编译器上实现的,但是⼤部分编译器上是32767
rand函数直接生成是随机数是 伪随机的,每次运行生成的随机数会相同
所以要运用到srand函数来初始化rand函数

srand

C语⾔中⼜提供了⼀个函数叫 srand ,⽤来初始化随机数的⽣成器的
格式:
 srand (( unsigned int )  seed);
一般可以用time函数来作为srand的种子(seed)

time

time返回的是 1970年1⽉1⽇0时0分0秒到现在程序运⾏时间之间的
差值,单位是秒,返回类型是time_t
格式:time(timer)        头文件: <time.h>
time函数的参数 timer 如果是⾮NULL的指针的话,函数也会将这个返回的差值放在timer指向的内存 中带回去
当time(NULL)时,只返回差值,这个差值叫做时间戳

四、数组

1.基本要点

数组是⼀组相同类型元素的集合数组是⼀组相同类型元素的集合
数组中存放的是1个或者多个数据,但是数组元素个数不能为0
数组中存放的多个数据,类型是相同的
数组分为⼀维数组和多维数组
数组下标从0开始

2.数组在内存中的存储

数组在内存中是连续存放的,一维二维都是

3.sizeof 计算数组元素个数
 

#include <stido.h>

 int main()
 {
 int arr[10] = {0};
 printf("%d\n", sizeof(arr));
 return 0;
 }
这⾥输出的结果是40,计算的是数组所占内存空间的总⼤⼩,单位是字节

4.二维数组

二维数组就是把一位数组作为数组的元素

定义二位数组的方法:

int arr[3][5];
3表⽰数组有3⾏
5表⽰每⼀⾏有5个元素
二位数组初始化时可以省略行不能省略列
int arr[][5];

5.变长数组

C99中给⼀个变⻓数组(variable-length array,简称 VLA)的新特性,允许我们可以使⽤变量指定数组⼤⼩。
int n = a+b;
int arr[n];

VS不行!!!(但是有办法修改成可以的,方法可以自己搜,很简单)

五、函数

1.概念

⼊函数(function)的概念,有些翻译为:⼦程序,⼦程序这种翻译更加准确⼀些。
C语⾔中的函数就是⼀个完成某项特定的任务的⼀⼩段代码
函数一般有两类:
1.库函数
2.自定义函数

2.库函数

C语⾔并不提供库函数;C语⾔的国际标准ANSI C规定了⼀ 些常⽤的函数的标准,被称为标准库,那不同的编译器⼚商根据ANSI提供的C语⾔标准就给出了⼀系列函数的实现。这些函数就被称为库函数

库函数使用方法可以在以下网站查询:

C/C++官⽅的链接: https://zh.cppreference.com/w/c/header
cplusplus.com: https://legacy.cplusplus.com/reference/clibrary/

3.形参和实参

在函数使⽤的过程中,把函数的参数分为,实参和形参。

1.实参

实际参数就是真实传递给函数的参数。

2.形参

在函数调用时形参才会申请内存空间,形参的作用是存放函数调用过程中传过来的实参的值,( 形参的实例化)
形参是实参的⼀份临时拷⻉

4.return 语句

return后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执⾏表达式

return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况
return执行之后其后面的代码不再执行

5.函数的嵌套和链式访问

1.嵌套

嵌套调⽤就是函数之间的互相调⽤

2.链式访问

所谓链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数,像链条⼀样将函数串起来就是函数的链式访问

6.函数的声明和定义

函数的定义也是⼀种特殊的声明

多个文件

⼀般在企业中我们写代码时候,代码可能⽐较多,不会将所有的代码都放在⼀个⽂件中;我们往往会根据程序的功能,将代码拆分放在多个⽂件中
⼀般情况下,函数的声明、类型的声明放在头⽂件(.h)中,函数的实现是放在源⽂件(.c)⽂件中

7.static 和 extern

1.static

static修饰局部变量改变了变量的⽣命周期,⽣命周期改变的本质是改变了变量的存储类型,本 来⼀个局部变量是存储在内存的栈区的,但是被 static 修饰后存储到了静态区。存储在静态区的变 量和全局变量是⼀样的,⽣命周期就和程序的⽣命周期⼀样了,只有程序结束,变量才销毁,内存才回收。但是作⽤域不变的。
⼀个全局变量被static修饰,使得这个全局变量只能在本源⽂件内使⽤,不能在其他源⽂件内使⽤
本质原因是全局变量默认是具有外部链接属性的,在外部的⽂件中想使⽤,只要适当的声明就可以使 ⽤;但是全局变量被 static 修饰之后,外部链接属性就变成了内部链接属性,只能在⾃⼰所在的源 ⽂件内部使⽤了,其他源⽂件,即使声明了,也是⽆法正常使⽤的。
static修饰函数时同样,会让函数的外部链接属性变为内部链接属性

2.extern

extern 是⽤来声明外部符号的,如果⼀个全局的符号在A⽂件中定义的,在B⽂件中想使⽤,就可以使⽤ extern 进⾏声明,然后使⽤

8.函数的注意事项

函数的形式参数要和函数的实参个数匹配
函数的实参是数组,形参也是可以写成数组形式的
形参如果是⼀维数组,数组⼤⼩可以省略不写
形参如果是⼆维数组,⾏可以省略,但是列不能省略
数组传参,形参是不会创建新的数组的(传递的是数组首元素地址)
形参操作的数组和实参的数组是同⼀个数组

六、函数递归

1.递归

递归就是函数自己调用自己
递就是递推,归是回归
如果递归一直进行,不终止,机会造成 栈溢出 (Stack overflow),因为每次递归函数都会像内存申请空间,而如果递归不终止,那么函数就会一直像内存申请空间。
所以,递归有两个条件:
1.递归存在限制条件,满足条件时就递归结束
2.递归每次调用后会越来越接近这个限制条件
比如计算n的阶乘,可以写出以下递推公式 :
根据这个公式可以写出代码:
int Fact(int n)
{
    if(n==0)
     return 1;
    else
     return n*Fct(n-1);
}

2.递归与迭代

递归是⼀种很好的编程技巧,但是和很多技巧⼀样,也是可能被误⽤的
比如看到推导的公式就很容易被写成递归的形式, 但是有的时候递归会很浪费栈空间,可能引起栈溢出的问题
所以这种时候就要用迭代,也就是循环的方式
int Fact(int n)
{
 int i = 0;
 int ret = 1;
 for(i=1; i<=n; i++)
 {
 ret *= i;
 }
 return ret;
}

这种方法计算!n的效率比递归的方法要更好

七、操作符(待补充)

1.单目操作符

单⽬操作符有这些:
!、 ++ 、 -- 、 & 、 * 、 + 、 - 、 ~ 、 sizeof 、 ( 类型 )

sizeof

sizeof 是⼀个关键字,也是操作符,专⻔是⽤来计算sizeof的操作符数的类型⻓度的,单位是字节。
sizeof中表达式不计算
int main()
{
 short s = 2;
 int b = 10;
 printf("%d\n", sizeof(s = b+1));
 printf("s = %d\n", s);
 return 0;
}

这里输出

sizeof 在代码进⾏编译的时候,就根据表达式的类型确定了,表达式的执⾏在程序运⾏期间才能执⾏,在编译期间已经将sizeof处理掉了,所以在运⾏期间就不会执⾏表达式了。

2.成员访问操作符("."和"->")(待补充)

1.访问结构体的成员给其赋值

struct Student
{
    char name[20];
    int age;
};  //这里的分号不能漏掉!!
//结构体的声明
//struct tag
//{
//    member-list;  成员列表
//}variable-list;   创建全局的结构体变量    
//相当于

struct Student s7;


int main()
{
    struct Student s1={"hiro",18};   //创建一个结构体变量(局部的)
    struct Student s2={.name="hiro",.age=18}  // 可以通过 '.'来先找到结构体的成员然后给他赋值    (顺序可以打乱)
}

2.结构体可以嵌套

嵌套结构中用"{a,b,c}"给被嵌套的结构体中的结构体赋值

struct A
{
    char a;
    int b;.
};
struct Student
{
    struct A a;
    char name[20];
    int age;
};
int main()
{
    
    struct Student s1={{'a',100},"hiro",18};//字符串要用""单个字符用''
}

3.打印结构体中的成员

同样使用成员访问操作符打印

格式:

结构体变量.成员名

printf("%s %d", s1.name, s1.age);

4.寻找/打印嵌套的结构体中的成员

两次寻找

printf("%c %d", s1.a.m,s1.a.n);

5.结构体成员的间接访问 (指针)

3.强制类型转换

int a = 3.14;
//a的是int类型, 3.14是double类型,两边的类型不⼀致,编译器会报警告

int a = (int)3.14;
//意思是将3.14强制类型转换为int类型,这种强制类型转换只取整数部分

4.关系操作符

>       ⼤于运算符
<        ⼩于运算符
>=     ⼤于等于运算符
<=     ⼩于等于运算符
==     相等运算符
!=     不相等运算符
关系表达式通常返回 0 或1   ,表⽰真假。
要注意判断条件时的“==”不要写成“=”
关系操作符最好不要连用
i < j < k

1 (i < j) < k

1 i < j && j < k

5.条件操作符(三目操作符)

exp1 ? exp2 : exp3

如果exp1为真,则结果exp2整个表达式的结果,否则为exp3的结果

6.逻辑操作符(&& , || , !)

! :逻辑取反运算符(改变单个表达式的真假)。
&& :逻辑与运算符,就是并且的意思(两侧的表达式都为真,则为真,否则为假)。
|| :逻辑或运算符,就是或者的意思(两侧⾄少有⼀个表达式为真,则为真,否则为假)。

1.逻辑取反

#include <stdio.h>
int main()
{
 int flag = 0;
 if(!flag)
 {
 printf("do something\n");
 }
 return 0;
}
如果 flag 为真, !flag 就是假,如果 flag 为假, !flag 就是真

2.逻辑与

两边都为真才为真
从左往右判断,如果左边已经是假,则直接为假

3.逻辑或

只要有一边为真就为真

从左往右判断,如果左边已经是真,则直接为真

7.原码、反码、补码

整数的2进制表⽰⽅法有三种,即原码、反码和补码
有符号整数的三种表⽰⽅法均有符号位和数值位两部分
用 0 表示 “正”,用 1 表示“负”
正整数的原、反、补码都相同
负数都不同
负数的反码是原码的 符号位不变,其它位次按位取反
负数的补码是 反码+1
负数的补码 取反+1 获得原码

8.移位操作符

<< 左移操作符
>> 右移操作符
操作数都是以其补码的形式操作
不能移动负数位
num>>-2

1.左移操作符

移位规则:左边抛弃、右边补0
#include <stdio.h>
int main()
{
 int num = 10;
 int n = num<<1;
 printf("n= %d\n", n);
 printf("num= %d\n", num);
 return 0;
}

2.右移操作符

右移运算分两种:
逻辑右移:左边⽤0填充,右边丢弃
算术右移:左边⽤原该值的符号位填充,右边丢弃
#include <stdio.h>
int main()
{
 int num = 10;
 int n = num>>1;
 printf("n= %d\n", n);
 printf("num= %d\n", num);
 return 0;
}

9.位操作符

   &              按位与
   |               按位或
   ^              按位异或
   ~             按位取反
操作数必须是整数
可以利用这个代码来实现两个整数的交换
虽然可以这么写,但是这种方法不如普通的方法好!

10.逗号表达式

exp1, exp2, exp3, …expN
整个表达式的结果是最后⼀个表达式的结果

11.下标访问[]、函数调⽤()

arr[1] 相当于  *(arr+1)   []就是对arr解引用

12.操作符的属性:优先级、结合性

1.优先级

包含多个运算符的表达式中,哪个运算符应该先执行

比如'*'的优先级高于'+'

2.结合性

优先级相同的操作符之间执行的顺序

比如

a&&b&&c||d

从左至右依次执行:  a&&b   (a&&b)&&c   [(a&&b)&&c]||d

3.优先级与结合性汇总

13.表达式求值

1.整型提升

在进行整型运算时,表达式中的字符和短整型(类型长度小于int的)操作数在使用之前会先被转换为普通整型

例如

char a=3;
char b=127;
char c=a+b;

printf("%d",c);

计算 a+b过程中

要先将char类型的a和b转化为int 类型再进行计算   这时计算精度会提高

1.提升方法

2.有符号

常见编译器中char一般都是有符号的char

按照变量的数据类型的符号位来提升

例如上面的例子

a为  00000011

b为   01111111

a和b会被提升为整型并且按照符号位即最前面的0来提升

00000000000000000000000000000011  -a

00000000000000000000000001111111    -b

相加得

00000000000000000000000010000010

而c为char类型,只有8个比特位,所以c最后为

10000010 -c

打印时也会发生整型提升

c按照符号位'1'来提升

11111111111111111111111110000010  -c(补码)

打印原码

10000000000000000000000001111110  --原码

所以打印出'-126'

总结:打印时先将c提升,提升后的是c的补码,但是打印的是原码   (计算用补码,只有打印时用原码)

3.无符号

高位补0

2.算术转换

计算时,如果操作符的操作数类型不同,会先由小的类型转化为大的类型,再进行计算

注意 :这里已经不包括前面的整型提升

问题表达式的解析

八、指针

1.指针变量

1.解引用操作符

“*”   解引用操作符通过地址来访问变量,通过地址找到指向的空间

2.指针变量的大小

32位平台4字节   64位8字节

3.指针变量类型的意义

指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)

4.指针+-整数

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。

比如:
char类型的指针+1,地址+1
int类型的指针+1,地址+4

5.野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
成因:

1.指针未初始化

2.指针越界访问

3.指针指向的空间释放

可以通过
p=NULL;

来避免使用野指针

一般在这个指针用不到了之后这么做

6.二级指针

存放指针变量的地址的指针是二级指针

int b = 20;
int*pa;
int*ppa = &b;//等价于pa=&b;
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

7.字符指针变量

char*是字符指针

int main()
{
 char ch = 'w';
 char *pc = &ch;
 *pc = 'w';
 return 0;
}

还可以这么用:

int main()
{
 const char* pstr = "hello bit.";
 printf("%s\n", pstr);
 return 0;
}

这里并不是把整个字符串放在了字符指针里,而是把字符串首元素的地址给了字符指针

8.数组指针变量

1.定义

指向数组的指针,存放的是数组的地址

int (*p)[10];
p先和*结合,说明p是⼀个指针变量,然后指针指向的是⼀个⼤⼩为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫 数组指针

[]的优先级⾼于*,所以必须加上()来保证p先和*结合

2.数组指针的初始化
int arr[10] = {0};
int(*p)[10] = &arr;
int (*p) [10] = &arr;
2 |   |    |
3 |   |    |
4 |   |    p指向数组的元素个数
5 |   p是数组指针变量名
6 p指向的数组的元素类型
3.⼆维数组传参的本质
⼆维数组其实可以看做是每个元素是⼀维数组的数组
⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组
⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址
⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式,即:

int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};
test(arr, 3, 5);

void test(int (*p)[5], int r, int c)
                |
                |
                指向一个有五个元素的一维数组的指针

9.函数指针变量

指向函数的指针变量,存放函数的地址
int (*pf3) (int x, int y)
 |     |    ------------ 
 |     |         |
 |     |         pf3指向函数的参数类型和个数的交代
 |     函数指针变量名
 pf3指向函数的返回类型

int (*) (int x, int y) //pf3函数指针变量的类型
#include <stdio.h>
int Add(int x, int y)
{
 return x+y;
}
int main()
{
 int(*pf3)(int, int) = Add;
 
 printf("%d\n", (*pf3)(2, 3));
 printf("%d\n", pf3(3, 5));
 return 0;
}


//输出
//5
//8

其实就是把函数或者数组的名字换成了指针变量的名字

10.typedef在指针定义时的运用

typedef 是⽤来类型重命名的,可以将复杂的类型,简单化。
typedef unsigned int uint;
同样的,可以以用来重命名指针类型的变量
typedef int * ptr_t;

然后将其运用于数组指针和函数指针:

typedef int(*parr_t)[5];  //新的类型名必须在*的右边
typedef void(*pun_t)(int, int);
#include <stdio.h>
typedef int(*arr_t)[5];
typedef int(*pfun_t)(int, int);
int sum(int x, int y)
{
	return x + y;
}
int main()
{
//函数
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	pfun_t pfun = sum;
	int result = pfun(a, b);
	printf("%d", result);


//数组
	int arr[5] = {1,2,3,4,5};
	arr_t parr = &arr;
	for (int i = 0; i < 5; i++)
	{
		printf("%d", (*parr)[i]);  //要注意[]优先级大于*,如果写成parr[i]的话parr指向的是整个数组
	}
	return 0;
}

下面这段代码就可以用简化的方式来写:

void (*signal(int , void(*)(int)))(int);


简化:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

2.void*指针

指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进⾏指针的+-整数和解引⽤的运算。
比如:实现比大小的函数时,用void*类型接受,然后再强制类型转换成char*或者int*

3.const修饰指针

1.const放在*左边
const int* p=0;

*p = 20;  //no
 p = &m;  //ok
修饰的是指针指向的内容
这时 指针指向的内容不能被改变,但是 指针本身可以被改变(改地址)
2.指针放在*右边
int * const p=&n;

*p = 20;  //ok
 p = &m;  //no
修饰的是指针变量本⾝
这时指针指向的内容可以被改变,但是指针本身不能被改变

4.assert断言

assert.h 头⽂件定义了宏 assert() ⽤于在运⾏时确保程序符合指定条件,如果不符合,就报 错终⽌运⾏    这个宏常常被称为“断⾔”
assert(p != NULL);

表达式为真返回非0,为假返回0并报错,在标准错误流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号

有⽆需更改代码就能开启或关闭 assert() 的机制
确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include <assert.h>

在VS这样的集成开发环境中,assert在relese模式中会自动优化掉

5.数组中指针的运用

1.指针访问数组

#include <stdio.h>
int main()
{
 int arr[10] = {0};
1
2
3
4
比特就业课主页:https://m.cctalk.com/inst/s9yewhfr 比特就业课
 //输⼊
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //输⼊
 int* p = arr;
 for(i=0; i<sz; i++)
 {
 scanf("%d", p+i);
 //scanf("%d", arr+i);//也可以这样写
 }
 //输出
 for(i=0; i<sz; i++)
 {
 printf("%d ", *(p+i));
 }
 return 0;
}

数组名就是首元素地址,所以可以把数组名直接赋值给指针变量p,让指针变量p指向数组,这时,数组名arr和p是等价的

arr[i]相当于*(arr+i)    同样p[i]相当于*(p+i)

一维数组传参可以写成数组的形式也可以写成指针的形式

2.指针数组

指针数组是数组,是存放指针的数组


3.指针数组模拟⼆维数组

#include <stdio.h>
int main()
{
 int arr1[] = {1,2,3,4,5};
 int arr2[] = {2,3,4,5,6};
 int arr3[] = {3,4,5,6,7};
 //数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中
 int* parr[3] = {arr1, arr2, arr3};
 int i = 0;
 int j = 0;
 for(i=0; i<3; i++)
 {
 for(j=0; j<5; j++)
 {
 printf("%d ", parr[i][j]);
 }
 printf("\n");
 }
 return 0;
}

parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素
可以模拟出二维数组的效果,但是有区别,这里面的 地址不是连续的

6.函数指针数组

存放函数的地址的数组

int (*parr1[3])(int,int);

上面的定义指的是创建一个存放整型类型的数组,这个数组的元素是有两个int类型参数的函数的地址

7.转移表

转移表就是利用函数指针数组,将多个函数放在一个数组中来实现一定的功能

例如用转移表实现计算器,可以参考:转移表实现简易计算器-优快云博客

8.回调函数

回调函数就是⼀个通过函数指针调⽤的函数
把函数的指针(地址)作为参数传递给另⼀个函数,被调用的函数就是回调函数,回调函数不由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用的
可以用此来模拟实现qsort(快速排序): 模拟实现qsort-优快云博客
qsort的使用:
#include <stdio.h>
//qosrt函数的使⽤者得实现⼀个⽐较函数
int int_cmp(const void * p1, const void * p2)
{
 return (*( int *)p1 - *(int *) p2);
}
int main()
{
 int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
 int i = 0;
 
 qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
 for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
 printf( "%d ", arr[i]);
 }
 printf("\n");
 return 0;
}

qsort使用时需要一个比较函数,这个函数决定了排序的方式是逆序还是正序,是比较整型还是比较字符

struct Stu //学⽣
{
 char name[20];//名字
 int age;//年龄
};
//假设按照年龄来⽐较
int cmp_stu_by_age(const void* e1, const void* e2)
{
 return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
//strcmp - 是库函数,是专⻔⽤来⽐较两个字符串的⼤⼩的
//假设按照名字来⽐较
int cmp_stu_by_name(const void* e1, const void* e2)
{
 return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
//按照年龄来排序
void test2()
{
 struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
 int sz = sizeof(s) / sizeof(s[0]);
 qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}
//按照名字来排序
void test3()
{
 struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
 int sz = sizeof(s) / sizeof(s[0]);
 qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{
 test2();
 test3();
 return 0;
}

九、字符函数和字符串函数

1.字符分类函数

用来给每个字符分类的类型分类的函数,使用需要头文件<ctype.h>

int a = islower ( int c );

是小写字母返回非0,不是返回0

2.字符转换函数

int tolower ( int c ); //将参数传进去的⼤写字⺟转⼩写 
int toupper ( int c ); //将参数传进去的⼩写字⺟转⼤写

3.字符串函数及其模拟实现

头文件<string.h>

1.strlen

size_t strlen ( const char * str );

遇到字符串结尾‘\0’结束,参数指向的字符串必须以'\0'结尾

返回值类型size_t是无符号的

模拟实现:

#include <stdio.h>
//1.用循环找\0
int my_strlen(const char* str)
{
	int len = 0;
	for (int i = 0; str[i] != '\0'; i++)
		len++;
	return len;
}
//2.用地址
int my_strlen(const char* str)
{
	int len = 0;
	char *p = str;
	while (*p != '\0')
		p++;
	len = p - str;
	return len;
}
//3.不创建计数变量,用递归

int my_strlen(const char* str)
{
	if (*str == '\0')
		return 0;
	else
		return 1+my_strlen(str + 1);
}

2.strcpy

复制一个字符串的内容到另一个字符串

char* strcpy(char * destination, const char * source );

源字符串必须以'\0'结尾        会将字符串中的'\0'拷贝

目标空间必须足够大

模拟实现:

#include <stdio.h>
char* my_strcpy( char* str2,const char* str1)
{
	char* ret = str2;
	while ((*str2++ = *str1++))
	{
		;
	}
	return ret;
}

3.strcat

在一个字符串后面追加字符串

源字符串必须以 '\0' 结束                      ⽬标空间必须有⾜够的⼤

⽬标字符串中也得有 \0 ,否则没办法知道追加从哪⾥开始

char *my_strcat(char *dest, const char*src)
{
 char *ret = dest;
 assert(dest != NULL);
 assert(src != NULL);
 while(*dest)
 {
 dest++;
 }
 while((*dest++ = *src++))
 {
 ;
 }
 return ret;
}

4.strcmp

比较两个字符串的大小

第⼀个字符串⼤于第⼆个字符串,则返回⼤于0的数字
第⼀个字符串等于第⼆个字符串,则返回0
第⼀个字符串⼩于第⼆个字符串,则返回⼩于0的数字
模拟实现:
int my_strcmp (const char * str1, const char * str2)
{
 int ret = 0 ;
 assert(str1 != NULL);
 assert(str2 != NULL);
 while(*str1 == *str2)
 {
 if(*str1 == '\0')
 return 0;
 str1++;
 str2++;
 }
 return *str1-*str2;
}

5.strncpy

限定长度的复制

char * strncpy ( char * destination, const char * source, size_t num );
如果源字符串的⻓度⼩于num,则拷⻉完源字符串之后,在⽬标的后边追加0,直到num个。
6.strncat
限制长度的追加
char * strncat ( char * destination, const char * source, size_t num );
⻓度⼩于num的时候,只会将字符串中到\0 的内容追加
7.strncmp
限制长度的比较
int strncmp ( const char * str1, const char * str2, size_t num );
提前发现不⼀样就提前结束
8.strstr
找到字符串1在字符串2中第一次出现的位置
char * strstr ( const char * str1, const char * str2);
字符串的⽐较匹配不包含 \0 字符,以 \0 作为结束标志
#include <stdio.h>
#include <string.h>
int main ()
{
 char str[] ="This is a simple string";
 char * pch;
 pch = strstr (str,"simple");
 strncpy (pch,"sample",6);
 printf("%s\n", str);
 return 0;
}

模拟实现:strstr的模拟实现-优快云博客

6.strtok

找到str中的下一个标记,并将其用‘\0’结尾,返回一个指向这个标记的指针

char * strtok ( char * str, const char * sep);
sep参数指向⼀个字符串,定义了⽤作分隔符的字符集合
可以有多个标记字符!
strtok函数会改变被操作的字符串,所以被strtok函数切分的字符串⼀般都是临时拷⻉的内容并且可修改
strtok函数的第⼀个参数不为 NULL ,函数将找到str中第⼀个标记,strtok函数将保存它在字符串中的位置。
strtok函数的第⼀个参数为 NULL ,函数将在同⼀个字符串中被保存的位置开始,查找下⼀个标记。
不存在更多的标记,则返回 NULL 指针
#include <stdio.h>
#include <string.h>
int main()
{
 char arr[] = "192.168.6.111";
 char* sep = ".";
 char* str = NULL;
 for (str = strtok(arr, sep); str != NULL; str = strtok(NULL, sep))
 {
 printf("%s\n", str);
 }
 return 0;
}

7.strerror

strerror 函数可以把参数部分错误码对应的错误信息的字符串地址返回来
char* strerror ( int errnum );
在不同的系统和C语⾔标准库的实现中都规定了⼀些错误码,⼀般是放在 errno.h 这个头⽂件中说明的

C语⾔程序启动的时候就会⽤⼀个全局的变量errno来记录程序的当前错误码,启动的时候是0
使用标准库中的函数时发生错误时,就会将对应错误码放到errno中
strerror就用来显示错误码对应的错误信息的字符串
我们还可以用perror来直接将错误信息打印出来,perror就相当于strerror(errno)
并且perror会在打印完参数部分的字符串之后在结尾自动加上 一个冒号和一个空格
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main ()
{
FILE * pFile;
pFile = fopen ("unexist.ent","r");
if (pFile == NULL)
perror("Error opening file unexist.ent");
return 0;
}

//输出:Error opening file unexist.ent: No such file or directory

十、内存操作函数

1.memcpy

void * memcpy ( void * destination, const void * source, size_t num );
从source的位置开始向后复制num个字节的数据到destination指向的内存位置
不是字符串操作函数,所以遇到\0不会停
如果source和destination有任何的重叠,复制的结果都是未定义的
模拟实现:
void* my_memcpy(void* des, void* sou, size_t num)
{
	assert(des);
	assert(sou);
	void* ret = des;
	while (num--)
	{
		*(char*)des = *(char*)sou;
		des=(char*)des+1;
		sou=(char*)sou+1;
	}
	return ret;
}

2.memmove

void * memmove ( void * destination, const void * source, size_t num );
与memcpy功能相同,但是 复制的内存块可以重叠

3.memset

void * memset ( void * ptr, int value, size_t num );
作用是设置内存,可以把内存以字节为单位设置成想要的内容
#include <stdio.h>
#include <string.h>
int main ()
{
 char str[] = "hello world";
 memset (str,'x',6);
 printf(str);
 return 0;
}

//输出:
//xxxxxxworld

4.memcmp

int memcmp ( const void * ptr1, const void * ptr2, size_t num );
⽐较从ptr1和ptr2指针指向的位置开始,向后的num个字节
与strcmp的返回相同

十一、数据在内存中的存储

1.大小端

超过⼀个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为⼤端字节序存储和⼩端字节序存储
大端存储: 数据的低位字节内容保存在内存的⾼地址处,⾼位字节内容,保存在内存的低地址处
小端存储: 指数据的低位字节内容保存在内存的低地址处,⾼位字节内容,保存在内存的⾼地址处
因为一个数据类型的大小可能不止有一个字节,而计算机的存储是以字节为单位的,这样,就导致存储一个数据类型时 数据的每个字节需要有一个安排的方式,于是就有了大小端存储方式

2.浮点数在内存中的存储

1.基本概念

根据国际标准IEEE(电⽓和电⼦⼯程协会) 754,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式
V   =  (−1) ∗ S M ∗ 2 E
(−1) S 表⽰符号位,当S=0,V为正数;当S=1,V为负数
M 表⽰有效数字,M是⼤于等于1,⼩于2的
2 E 表⽰指数位
比如,十进制5.0写成二进制是101.0    就相当于   1.01*2^2
其中,1.01就是M        指数2就是E    而因为这是正数,所以S=1
IEEE 754规定:
对于32位的浮点数,最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数,最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M

2.存储过程

IEEE 754 对有效数字M和指数E,还有⼀些特别规定
在计算机内部保存M时,默认这个数的第⼀位总是1,因此可以被舍去,只保存后⾯的xxxxxx部分。⽐如保存1.01的时候,只保存01,等到读取的时候,再把第⼀位的1加上去
这样可以节省一位的有效数字
⾄于指数E,情况就⽐较复杂
⾸先,E为⼀个⽆符号整数(unsigned int)
如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存⼊内存时E的真实值必须再加上⼀个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。⽐如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001

3.提取过程

指数E从内存中取出还可以再分成三种情况
E不全为0或不全为1
指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第⼀位的1
E全为0
浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第⼀位的1,⽽是还原为0.xxxxxx的⼩数。这样做是为了表⽰±0,以及接近于0的很⼩的数字
E全为1
如果有效数字M全为0,表⽰±⽆穷⼤(正负取决于符号位s)

十二、自定义类型

1.结构体

在七、操作符的成员访问操作符里也有相关内容

1.结构体的声明

struct tag
{
 member-list;
}variable-list;

2.结构体变量的创建和初始化

#include <stdio.h>
struct Stu
{
 char name[20];//名字
 int age;//年龄
 char sex[5];//性别
 char id[20];//学号
};
int main()
{
 //按照结构体成员的顺序初始化
 struct Stu s = { "张三", 20, "男", "20230818001" };
 printf("name: %s\n", s.name);
 printf("age : %d\n", s.age);
 printf("sex : %s\n", s.sex);
 printf("id : %s\n", s.id);
 
 //按照指定的顺序初始化
 struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = 
"⼥" };
 printf("name: %s\n", s2.name);
 printf("age : %d\n", s2.age);
 printf("sex : %s\n", s2.sex);
 printf("id : %s\n", s2.id);
 return 0;
}

3.结构的特殊声明结构的特殊声明

声明结构体的时候可以不完全声明
struct
{
 int a;
 char b;
 float c;
}x;
struct
{
 int a;
 char b;
 float c;
}a[20], *p;
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次

4.结构的⾃引⽤

struct Node
{
 int data;
 struct Node* next;
};

作用:通过一个结构体去访问另一个结构体内的数据

例如:

#include <stdio.h>

// 定义自引用结构体(链表节点)
struct Node {
    int data;               // 数据域:存储整数
    struct Node* next;      // 自引用指针:指向另一个Node节点
};

int main()
{
    // 1. 创建第二个节点(被指向的节点)
    struct Node node2 = {20, NULL}; // next=NULL表示没有下一个节点
    
    // 2. 创建第一个节点,next指向node2(自引用的核心用法)
    struct Node node1 = {10, &node2};
    
    // 3. 输出第一个节点的data,以及它指向的第二个节点的data
    printf("node1.data = %d\n", node1.data);
    // 通过node1.next访问第二个节点的data
    printf("node1.next->data = %d\n", node1.next->data);
    
    return 0;
}

5.结构体内存对齐

对其规则:
1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处
对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值
VS 中默认的对其数为 8
Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍
如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍
结构体的内存对⻬是拿空间来换取时间的做法
#pragma 这个预处理指令,可以改变编译器的默认对⻬数
#pragma pack(1)//设置默认对⻬数为1

#pragma pack()//取消设置的对⻬数,还原为默认

6.结构体传参

{
 int data[1000];
 int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
 printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
 printf("%d\n", ps->num);
}
int main()
{
 print1(s); //传结构体
 print2(&s); //传地址
 return 0;
}

传地址更好!传参时参数需要压栈,会有时间和空间上的系统开销

7.结构体实现位段

位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型
2. 位段的成员名后边有⼀个冒号和⼀个数字
struct S
{
 char a:3;
 char b:4;
 char c:5;
 char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
位段的内存分配:
冒号后面的数字代表着这一个数据占多少个字节
位段的跨平台问题:
1.int 位段被当成有符号数还是⽆符号数是不确定的。
2. 位段中最⼤位的数⽬不能确定(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义

4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。
总结:
跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
*位段的应⽤
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通有帮助。
注意事项:
位段是没用地址的,不能通过地址来给其成员赋值,不能使用&操作
所以只能是先输入放在一个变量中,然后赋值给位段的成员
struct A
{
 int _a : 2;
 int _b : 5;
 int _c : 10;
 int _d : 30;
};


int main()
{
 struct A sa = {0};
 scanf("%d", &sa._b);//这是错误的
 
 //正确的⽰范
 int b = 0;
 scanf("%d", &b);
 sa._b = b;
 return 0;
}

2.联合体

1.联合体的声明

像结构体⼀样,联合体也是由⼀个或者多个成员构成,这些成员可以不同的类型
但是编译器只为最⼤的成员分配⾜够的内存空间。联合体的特点是所有成员共⽤同⼀块内存空间,样⼀个联合变量的⼤⼩,⾄少是最⼤成员的⼤⼩( 联合体⾄少得有能⼒保存最⼤的那个成员 )
所以联合体也叫:共⽤体
#include <stdio.h>
//联合类型的声明
union Un
{
 char c;
 int i;
};
int main()
{
 //联合变量的定义
 union Un un = {0};
 // 下⾯输出的结果是⼀样的吗?
 printf("%p\n", &(un.i));
 printf("%p\n", &(un.c));
 printf("%p\n", &un);
 return 0;
}

输出:
001AF85C
001AF85C
001AF85C

#include <stdio.h>
//联合类型的声明
union Un
{
 char c;
 int i;
};
int main()
{
 //联合变量的定义
 union Un un = {0};
 un.i = 0x11223344;
 un.c = 0x55;
 printf("%x\n", un.i);
 return 0;
}

输出:
11223355

可以看到,第四个字节的内容被更改了,可以画出图:

通过这个特性,我们也可以看出来当前编译器用的是 小端存储 ,并且可以利用这个特性来判断当前编译器的存储类型:
int check_sys()
{
 union
 {
 int i;
 char c;
 }un;
 un.i = 1;
 return un.c;//返回1是⼩端,返回0是⼤端
}

2.联合体的大小

联合的⼤⼩⾄少是最⼤成员的⼤⼩
当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍
可以利用联合体来节省内存空间:
⽐如,我们要搞⼀个活动,要上线⼀个礼品兑换单,礼品兑换单中有三种商品:图书、杯⼦、衬衫。每⼀种商品都有:库存量、价格、商品类型和商品类型相关的其他信息
我们可以把共有的内容写到结构体里,把各自独有的写到联合体里
struct gift_list
{
 int stock_number;//库存量
 double price; //定价
 int item_type;//商品类型
 
 union {
 struct
 {
 char title[20];//书名
 char author[20];//作者
 int num_pages;//⻚数
 }book;
 struct
 {
 char design[30];//设计
 }mug;
 struct
 {
 char design[30];//设计
 int colors;//颜⾊
 int sizes;//尺⼨
 }shirt;
 }item;
};

这样,就减少了两个物品独有的内容的大小

3.枚举类型

1.枚举类型的声明

枚举顾名思义就是⼀⼀列举。
把可能的取值⼀⼀列举

enum Day//星期
{
 Mon,
 Tues,
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};
这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值

enum Color
{
 RED=2,
 GREEN=4,
 BLUE=8
};

2.枚举类型的优点

1. 增加代码的可读性和可维护性
2. 和#define定义的标识符⽐较枚举有类型检查,更加严谨。
3. 便于调试,预处理阶段会删除 #define 定义的符号
4. 使⽤⽅便,⼀次可以定义多个常量
5. 枚举常量是遵循作⽤域规则的,枚举声明在函数内,只能在函数内使⽤

3.枚举类型的使用

enum Color//颜⾊
2 {
3 RED=1,
4 GREEN=2,
5 BLUE=4
6 };
7
8 enum Color clr = GREEN;//使⽤枚举常量给枚举变量赋值

C语言中可以拿整数给枚举变量赋值,但是C++中不行,C++的类型检查比较严格

十三、动态内存管理

1.malloc和free

1.malloc

void * malloc ( size_t size);
这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针
如果开辟成功,则返回⼀个指向开辟好空间的指针
如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定
如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器

2.free

void free ( void * ptr);
free用来回收和释放开辟的内存
如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做
malloc和free都声明在 stdlib.h 头⽂件中
#include <stdio.h>
#include <stdlib.h>
int main()
{
 int num = 0;
 scanf("%d", &num);
 int arr[num] = {0};
 int* ptr = NULL;
 ptr = (int*)malloc(num*sizeof(int));
 if(NULL != ptr)//判断ptr指针是否为空
 {
 int i = 0;
 for(i=0; i<num; i++)
 {
 *(ptr+i) = 0;
 }
 }
 free(ptr);//释放ptr所指向的动态内存
 ptr = NULL;//是否有必要?
 return 0;
}

ptr要指向起始位置,ptr最好不要改变

空间释放完成后要把原指针设置为NULL,free不会设置NULL

3.calloc

void * calloc ( size_t num, size_t size);
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0

4.realloc

void * realloc ( void * ptr, size_t size);
这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到 新 的空间
case1:在原空间后面扩大
case2:找到新的空间来扩大,并且把原空间的数据复制过去
使用这些函数后一定要检验指针!

2.常⻅的动态内存的错误

1. 对NULL指针的解引⽤操作
2. 对动态开辟空间的越界访问
3. 对⾮动态开辟内存使⽤free释放
4. 使⽤free释放⼀块动态开辟内存的⼀部分
5. 对同⼀块动态内存多次释放
6. 动态开辟内存忘记释放(内存泄漏)
忘记释放不再使⽤的动态开辟的空间会造成内存泄漏

3.柔性数组

结构中的最后⼀个元素允许是未知⼤⼩的数组,这就叫做『柔性数组』成员
struct st_type
{
 int i;
 int a[0];//柔性数组成员
};
有些编译器会报错⽆法编译可以改成:
struct st_type
{
 int i;
 int a[];//柔性数组成员
};
柔性数组的特点:
结构中的柔性数组成员前⾯必须⾄少⼀个其他成员
sizeof 返回的这种结构⼤⼩不包括柔性数组的内存
包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的⼤⼩,以适应柔性数组的预期⼤⼩
使用:
#include <stdio.h>
#include <stdlib.h>
int main()
{
 int i = 0;
 type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
 //业务处理
 p->i = 100;
 for(i=0; i<100; i++)
 {
 p->a[i] = i;
 }
 free(p);
 return 0;
}

优势:

上述的 type_a 结构也可以设计为下⾯的结构,也能完成同样的效果
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
 int i;
 int *p_a;
}type_a;
int main()
{
 type_a *p = (type_a *)malloc(sizeof(type_a));
 p->i = 100;
 p->p_a = (int *)malloc(p->i*sizeof(int));
 
 //业务处理
 for(i=0; i<100; i++)
 {
 p->p_a[i] = i;
 }
 
 //释放空间
 free(p->p_a);
 p->p_a = NULL;
 free(p);
 p = NULL;
 return 0;
}

1.⽅便内存释放

第二个做了⼆次内存分配,但是用户不一定知道,会有内存泄漏的风险
2. 有利于访问速度
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚

4.C/C++中程序内存区域划分

1. 栈区(stack):函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配⽅式类似于链表。
3. 数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。

十四、文件操作

1.文件的分类

1.程序文件:
  程序⽂件包括源程序⽂件(后缀为.c),⽬标⽂件(windows环境后缀为.obj),可执⾏程序(windows 环境后缀为.exe)
2.数据文件:
  ⽂件的内容不⼀定是程序,⽽是程序运⾏时读写的数据,⽐如程序运⾏需要从中读取数据的⽂件,或者输出内容的⽂件
根据数据的组织形式,数据⽂件被称为⽂本⽂件或者⼆进制⽂件
字符⼀律以ASCII形式存储,数值型数据既可以⽤ASCII形式存储,也可以使⽤⼆进制形式存储

2.流和标准流

流就是用来运输数据的工具
C程序针对⽂件、画⾯、键盘等的数据输⼊输出操作都是通过流操作的
⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作
为C语⾔程序在启动的时候,默认打开了3个流:
stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据
stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出流中
stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯

3.⽂件指针

缓冲⽂件系统中,关键的概念是“⽂件类型指针”,简称“⽂件指针”
每个被使⽤的⽂件都在内存中开辟了⼀个相应的⽂件信息区,⽤来存放⽂件的相关信息(如⽂件的名字,⽂件状态及⽂件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名 FILE
不同的C编译器的FILE类型包含的内容不完全相同,但是⼤同⼩异
每当打开⼀个⽂件的时候,系统会根据⽂件的情况⾃动创建⼀个FILE结构的变量,并填充其中的信息,使⽤者不必关⼼细节
⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使⽤起来更加⽅便
  FILE* pf;
定义pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个⽂件的⽂件信息区(是⼀个结构体变量)。通过该⽂件信息区中的信息就能够访问该⽂件。也就是说,通过⽂件指针变量能够间接找到与它关联的⽂件

4.文件的打开和关闭

⽂件在读写之前应该先打开⽂件,在使⽤结束之后应该关闭⽂件
编写程序的时候,在打开⽂件的同时,都会返回⼀个FILE*的指针变量指向该⽂件,也相当于建⽴了指针和⽂件的关系
//打开⽂件
FILE * fopen ( const char * filename, const char * mode );
//关闭⽂件
int fclose ( FILE * stream );

int main ()
{
 FILE * pFile;
 //打开⽂件
 pFile = fopen ("myfile.txt","w");
 //⽂件操作
 if (pFile!=NULL)
 {
 fputs ("fopen example",pFile);
 //关闭⽂件
 fclose (pFile);
 }
 return 0;
}

5.文件的读写

1.顺序读写

上⾯说的适⽤于所有输⼊流⼀般指适⽤于标准输⼊流和其他输⼊流(如⽂件输⼊流);所有输出流⼀般指适⽤于标准输出流和其他输出流(如⽂件输出流)

scanf —— 从标准输入(键盘)读取

fscanf —— 从文件流读取

int fscanf(FILE *stream, const char *format, ...);

sscanf —— 从字符串读取

int sscanf(const char *str, const char *format, ...);
#include <stdio.h>

int main() {
    char src[] = "name=Tom age=18 score=95.5";
    char name[10];
    int age;
    float score;
    // 按格式解析字符串(忽略固定字符 "name=" " age=" " score=")
    int ret = sscanf(src, "name=%s age=%d score=%f", name, &age, &score);
    printf("读取个数:%d\n", ret);
    printf("name=%s, age=%d, score=%.1f\n", name, age, score);
    return 0;
}

上面的函数返回的都是读取的数据的个数,printf也是

2.随机读写

1.fseek

根据⽂件指针的位置和偏移量来定位⽂件指针(⽂件内容的光标)。

int fseek ( FILE * stream, long int offset, int origin );
                                                偏移量     起始位置
int main ()
{
 FILE * pFile;
 pFile = fopen ( "example.txt" , "wb" );
 fputs ( "This is an apple." , pFile );
 fseek ( pFile , 9 , SEEK_SET );
 fputs ( " sam" , pFile );
 fclose ( pFile );
 return 0;
}

SEEK_SET      文件起始位置

SEEK_CUR     文件指针当前位置

SEEK_END     文件末尾

2.ftell

返回⽂件指针相对于起始位置的偏移量

long int ftell ( FILE * stream );
#include <stdio.h>
int main ()
{
 FILE * pFile;
 long size;
 pFile = fopen ("myfile.txt","rb");
 if (pFile==NULL) 
 perror ("Error opening file");
 else
 {
 fseek (pFile, 0, SEEK_END); // non-portable
 size=ftell (pFile);
 fclose (pFile);
 printf ("Size of myfile.txt: %ld bytes.\n",size);
 }
 return 0;
}
3.rewind
让⽂件指针的位置回到⽂件的起始
void rewind ( FILE * stream );
#include <stdio.h>
int main ()
{
 int n;
 FILE * pFile;
 char buffer [27];
 
 pFile = fopen ("myfile.txt","w+");
 for ( n='A' ; n<='Z' ; n++)
 fputc ( n, pFile);
 rewind (pFile);
 
 fread (buffer,1,26,pFile);
 fclose (pFile);
 
 buffer[26]='\0';
 printf(buffer);
 return 0;
}

3.⽂件读取结束的判定

1.被错误使⽤的 feof

在⽂件读取过程中,不能⽤feof函数的返回值直接来判断⽂件的是否结束

feof 的作⽤是:当⽂件读取结束的时候,判断是读取结束的原因是否是:遇到⽂件尾结束
⽂本⽂件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )

⼆进制⽂件的读取结束判断,判断返回值是否⼩于实际要读的个数

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 int c; // 注意:int,⾮char,要求处理EOF
 FILE* fp = fopen("test.txt", "r");
 if(!fp) {
 perror("File opening failed");
 return EXIT_FAILURE;
 }
 //fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOF
 while ((c = fgetc(fp)) != EOF) // 标准C I/O读取⽂件循环
 { 
 putchar(c);
 }
 //判断是什么原因结束的
 if (ferror(fp))
 puts("I/O error when reading");
 else if (feof(fp))
 puts("End of file reached successfully");
 fclose(fp);
}

读取或者写入出错时,会创建一个标记,这个标记可以被ferror识别到

识别到是读取或者写入错误ferror才返回真

4.文件缓冲区

ANSIC 标准采⽤“缓冲⽂件系统” 处理的数据⽂件的,所谓缓冲⽂件系统是指系统⾃动地在内存中为程序中每⼀个正在使⽤的⽂件开辟⼀块“⽂件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的⼤⼩根据C编译系统决定的

因为有缓冲区的存在,C语⾔在操作⽂件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候关闭⽂件
fflush(pf);
fflush 在⾼版本的VS上不能使⽤了

十五、编译和链接

1.翻译环境和运⾏环境

在 ANSI C 的任何⼀种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执⾏的机器指令(⼆进制指令)。
第2种是执⾏环境,它⽤于实际执⾏代码

2.翻译环境

1.大体过程

翻译环境是由编译和链接两个⼤的过程组成的,⽽编译⼜可以分解成:预处理(有些书也叫预编译)、编译、汇编三个过程

从.c文件到.obj文件的过程是编译,.obj文件和链接库到link.exe文件的过程是链接

多个.c文件生成可执行程序:

多个.c⽂件单独经过编译器,编译处理⽣成对应的⽬标⽂件。
注:在Windows环境下的⽬标⽂件的后缀是 .obj ,Linux环境下⽬标⽂件的后缀是 .o
多个⽬标⽂件和链接库⼀起经过链接器处理⽣成最终的可执⾏程序。
链接库是指运⾏时库(它是⽀持程序运⾏的基本函数集合)或者第三⽅库

2.预处理(预编译)

1.简述
在预处理阶段,源⽂件和头⽂件会被处理成为 .i 为后缀的⽂件

在 gcc 环境下想观察⼀下,对 test.c ⽂件预处理后的.i⽂件,命令如下:

gcc -E test.c -o test.i
预处理阶段主要处理那些源⽂件中#开始的预编译指令,规则如下:
将所有的 #define 删除,并展开所有的宏定义。
处理所有的条件编译指令,如: #if 、 #ifdef 、 #elif 、 #else 、 #endif 。
处理#include 预编译指令,将包含的头⽂件的内容插⼊到该预编译指令的位置。这个过程是递归进⾏的,也就是说被包含的头⽂件也可能包含其他⽂件。
删除所有的注释
添加⾏号和⽂件名标识,⽅便后续编译器⽣成调试信息等。
或保留所有的#pragma的编译器指令,编译器后续会使⽤。
  .i ⽂件中不再包含宏定义,因为宏已经被展开。并且包含的头⽂件都被插⼊到 .i⽂件中。所以当我们⽆法知道宏定义或者头⽂件是否包含正确的时候,可以查看预处理后的 .i ⽂件来确认

C语⾔设置了⼀些预定义符号,可以直接使⽤,预定义符号也是在预处理期间处理的

__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
printf("file:%s line:%d\n", __FILE__, __LINE__);
2.#define 定义常量
# define name stuff
#define MAX 1000
#define reg register //为 register这个关键字,创建⼀个简短的名字
#define do_forever for(;;) //⽤更形象的符号来替换⼀种实现
#define CASE break;case //在写case语句的时候⾃动把 break写上。
// 如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏
符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
 date:%s\ttime:%s\n" ,\
 __FILE__,__LINE__ , \
 __DATE__,__TIME__ )

可以使用‘\’(行续符)来让预处理器把其链接的代码视作一个完整的宏定义(只对宏生效)

#define定义时不要加上';',会引起两个分号的的问题

3.#define定义宏:
# define name( parament-list ) stuff
1 #define SQUARE( x ) x * x
int a = 5;
printf("%d\n" ,SQUARE( a + 1) );

要注意这里的替换是单纯的替换,替换执行的优先级高于运算符的优先级,所以最好在每一项上加上()来确保其优先级正确

#define DOUBLE( x) ( ( x ) + ( x ) )
带有副作⽤的宏参数:
x+1;//不带副作⽤
x++;//带有副作⽤
宏替换的规则:
在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
#include <stdio.h>

// 定义一个宏:把 NUM 替换成 100
#define NUM 100
// 定义一个宏:把 STR 替换成 "hello"
#define STR "hello"

int main() {
    // 场景1:代码中的 NUM/STR(不在字符串里)→ 正常替换
    int a = NUM;          // 预编译后:int a = 100;
    char *s1 = STR;       // 预编译后:char *s1 = "hello";

    // 场景2:字符串里的 NUM/STR(在""内)→ 完全不替换
    char *s2 = "NUM = 100";  // 预编译后还是:char *s2 = "NUM = 100";
    char *s3 = "STR is test";// 预编译后还是:char *s3 = "STR is test";
    
    printf("%d\n", a);    // 输出 100
    printf("%s\n", s1);   // 输出 hello
    printf("%s\n", s2);   // 输出 NUM = 100(不是 100 = 100)
    printf("%s\n", s3);   // 输出 STR is test(不是 hello is test)
    return 0;
}
4.宏函数的对⽐
# define MAX(a, b) ((a)>(b)?(a):(b))
这样用宏比用函数更好
宏⽐
⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多,函数在程序的规模和速度⽅⾯更胜⼀筹
函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏的参数是类型⽆关的
和函数相⽐宏的劣势:
每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度
宏是没法调试的
宏由于类型⽆关,也就不够严谨
宏可能会带来运算符优先级的问题,导致程容易出现错
宏有时候可以做函数做不到的事情。⽐如:宏的参数可以出现类型,但是函数做不到
#define MALLOC(num, type)\
 (type )malloc(num sizeof(type))
 ...
//使⽤
 MALLOC(10, int);//类型作为参数
//预处理器替换之后:
 (int *)malloc(10 sizeof(int));
5.#运算符
#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。
#运算符所执⾏的操作可以理解为”字符串化“
当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 .
就可以写:
# define PRINT(n) printf( "the value of " #n " is %d" , n);
当我们按照下⾯的⽅式调⽤的时候:
PRINT(a);//当我们把a替换到宏的体内时,就出现了#a,⽽#a就是转换为"a",时⼀个字符串代码就会被预处理为:
printf("the value of ""a" " is %d", a);
6.## 运算符:
## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称为记号粘合
这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的
写一个求两个数较大值的函数时, 如果输入变量的类型不同,那么每种数据类型都要写一个函数,但是如果我们用宏来写,可以这样:
//宏定义
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
 return (x>y?x:y); \
}
使⽤宏,定义不同函数
GENERIC_MAX(int) //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名
int main()
{
 //调⽤函数
 int m = int_max(2, 3);
 printf("%d\n", m);
 float fm = float_max(3.5f, 4.5f);
 printf("%f\n", fm);
 return 0;
}

##就是替换函数外面的函数名(变量名 / 函数名 / 类型名等),把其和其他的字符变成一个标识符,#是替换函数内的变量名

7.命名约定
把宏名全部⼤写
函数名不要全部⼤写
8.#undef

这条指令⽤于移除⼀个宏定义

# undef NAME
// 如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。
9.命令⾏定义

许多C 的编译器提供了⼀种能⼒,允许在命令⾏中定义符号。⽤于启动编译过程

当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处。(假定某个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个机器内存⼤些,我们需要⼀个数组能够⼤些。)
#include <stdio.h>
int main()
{
 int array [ARRAY_SIZE];
 int i = 0;
 for(i = 0; i< ARRAY_SIZE; i ++)
 {
 array[i] = i;
 }
 for(i = 0; i< ARRAY_SIZE; i ++)
 {
 printf("%d " ,array[i]);
 }
 printf("\n" );
 return 0;
}
编译指令:
//linux 环境演⽰
gcc -D ARRAY_SIZE= 10 programe.c
核心是在编译时通过编译器参数(而非代码里的 #define)定义宏
10.条件编译
在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的。因为我们有条件编译指令。
⽐如说:
调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译。
#include <stdio.h>
#define __DEBUG__
int main()
{
 int i = 0;
 int arr[10] = {0};
 for(i=0; i<10; i++)
 {
 arr[i] = i;
 #ifdef __DEBUG__
 printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 
 #endif //__DEBUG__
 }
 return 0;
}
常⻅的条件编译指令:
1.
#if 常量表达式
 //...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
 //..
#endif
2.多个分⽀的条件编译
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
11.头⽂件的包含

#include“xxxx.h”是先从源文件目录中找头文件,找不到再从库文件里找

#include<xxxx.h>是直接从库文件里找头文件,不会从源文件目录里找,所以不能用这个来包含自定义的头文件

12.嵌套⽂件包含
⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤

如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。
如果test.h ⽂件⽐较⼤,这样预处理后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家都能使⽤,⼜不做任何的处理,那么后果真的不堪设想。

可以通过条件编译的方法来避免

在开头写

#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__

或者是c语言自带的方法

在开头写

#pragma once

3.编译

编译过程就是将预处理后的⽂件进⾏⼀系列的:词法分析、语法分析、语义分析及优化,⽣成相应的汇编代码⽂件
编译过程的命令如下:
gcc -S test.i -o test.s
接下来以这条代码举例
array [index] = (index+ 4 )*( 2 + 6 );
1.词法分析
将源代码程序被输⼊扫描器,扫描器的任务就是简单的进⾏词法分析,把代码中的字符分割成⼀系列的记号(关键字、标识符、字⾯量、特殊字符等)
2.语法分析
将对扫描产⽣的记号进⾏语法分析,从⽽产⽣语法树。这些语法树是以表达式为节点的树
3.语义分析
由语义分析器来完成语义分析,即对表达式的语法层⾯分析。编译器所能做的分析是语义的静态分析。静态语义分析通常包括声明和类型的匹配,类型的转换等。这个阶段会报告错误的语法信息

4.汇编

汇编器是将汇编代码转转变成机器可执⾏的指令,每⼀个汇编语句⼏乎都对应⼀条机器指令。就是根据汇编指令和机器指令的对照表⼀⼀的进⾏翻译,也不做指令优化
汇编的命令如下:
gcc -c test.s -o test.o

5.链接

链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤
链接解决的是⼀个项⽬中多⽂件、多模块之间互相调⽤的问题
链接的作用就是让两个文件之间能够互相联系起来,比如:
一个源文件中引用了另一个源文件中的一个函数,链接过程就是让第一个源文件能够找到这个函数

6.运⾏环境

1. 程序必须载⼊内存中。在有操作系统的环境中:⼀般这个由操作系统完成。在独⽴的环境中,程序的载⼊必须由⼿⼯安排,也可能是通过可执⾏代码置⼊只读内存来完成。
2. 程序的执⾏便开始。接着便调⽤main函数
3. 开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使⽤静态(static)内存,存储于静态内存中的变量在程序的整个执⾏过程⼀直保留他们的值
4. 终⽌程序。正常终⽌main函数;也有可能是意外终⽌

十六、一个贪吃蛇项目

c语言程序设计电子版_超级详细的C语言笔记 第一章 第二节至第五节很感谢大家对小江的喜欢,我只是一个普通的大学生,虽然笔记写的可能比较差,但一笔一划都是我自己对知识的理解整合。手写笔记的原因有三:一是老师要收笔记,这个假期比较特别,老师怕我们在家只顾玩,让我们谢谢笔记,这样就算一部分平时分,而且期末复习也有资料可看;二是自己比较手写的东西,自己写一可以锻炼自己的心性,防止以后提笔忘字和字体变丑。三是笔记写在纸上可能有时不好携带,但是可以用软... 阅读详情

相关推荐

从C++到C——我的C语言学习笔记

刚上大学就学习了C++,有人跟我说C++包括了C语言,学过C++就没有必要在弄C语言了。也有人说,C语言是程序员的通用语言,我们都应该花时间好好研究一下。之前也有看过一些书,这些书的作者也都建议读者学好C,而且强调不是C++。一直都有学习C语言的打算,直到我尝试写C程序发现编译都

...... 3137

c语言笔记照片_超级详细的C语言笔记

第一章 第二节至第五节很感谢大家对小江的喜欢,我只是一个普通的大学生,虽然笔记写的可能比较差,但一笔一划都是我自己对知识的理解整合。手写笔记的原因有三:一是老师要收笔记,这个假期比较特别,老师怕我们在家只顾玩,让我们谢谢笔记,这样就算一部分平时分,而且期末复习也有资料可看;二是自己比较手写的东西,自己写一可以锻炼自己的心性,防止以后提笔忘字和字体变丑。三是笔记写在纸上可能有时不好携带,但是可以用软...

weixin_39799561的博客 585

工作:三菱PLC与伺服之CC-Link IE Field Network通讯应用

CC-LINK-IE

好记性不如烂笔头 2万+

零基础小白的C语言自学笔记——初识C语言

开始零基础自学编程,请多多指教!

Liner02_的博客 1326

使用 git一个仓库管理多个项目

我们需要在 git 里面创建一个空分支,该分支不继承任何提交,没有父节点,完全是一个干净的分支,这时候就需要使用 orphan。该命令会创建一个新的分支,并且该分支有前一个分支下的所有文件,我们不想提交任何内容,所以我们需要把当前内容全部删除。此时 master 分支已经有一个项目了,下面我们创建新的分支管理另一个项目。此时可以在该分支目录上初始化你的项目,在执行 git 命令提交代码。此时我们就完成了在同一个仓库管理不同项目的需求了。下载仓库到本地之后,进入项目目录,查看分支信息。

xrs151的博客 2025

Python爬虫实战:5分钟搞定东方财富网股票数据抓取(附完整代码)

本文详细介绍了如何使用Python爬虫快速抓取东方财富网的股票数据,包括环境配置、API分析、核心代码实现及优化技巧。通过简单的requests和pandas库,5分钟内即可获取高质量股票数据,适合量化交易和数据分析初学者。

weixin_29040367的博客 1182

翁恺C语言笔记

算找零:需要:1.有地方放数字2.有办法输入数字3.输入数字可以参与运算int main()printf("请输入金额(元):");printf("找您%d元。return 0;就是这个啦(今后仅仅展示大括号里面的内容)编译运行后在终端输入数值,按下回车如何输入:输入的东西去哪里了?

Epiphany_yy的博客 2036

地理空间坐标系统-不同椭球基准间的坐标转换

基于同一个椭球基准,可以构建三类地理空间坐标系:大地球面坐标系、大地空间直角坐标系、大地投影坐标系。由于坐标原点和椭球长短半径等都相同,这三类坐标系之间可以通过无损的数学方法相互之间进行转换。那么现在问题来了,不同椭球基准(在全球范围来看存在成百上千的椭球基准)之间的坐标怎么转换?比如,北京54、西安80、国家2000,特别是在我国,现在主推国家2000坐标系,那么如何将历史的测绘成果(北京54和...

技术改变世界 8336

C语言笔记(郝斌)

二次复习做的笔记,很基础,适合新手入门,复习巩固

m0_65575611的博客 3852

机器人动力学仿真软件:Webots_(14).机器人声音仿真

Webots 中的声音仿真主要通过Sound和两种节点来实现。Sound节点用于播放预定义的音频文件,而节点则允许在仿真过程中动态生成和播放声音。这些节点可以用于模拟机器人的语音输出、报警声、环境噪音等。以下是一个示例,展示如何在控制器代码中播放预定义的声音。// 控制器代码(.c) # include <webots/robot.h> # include <webots/sound.h> # define TIME_STEP 64 const char * sound_name = "sound";

kkchenjj的博客 1311

SAP Group Reporting入门指南(1)--开篇及系统环境搭建

文章介绍了SAP GR的发展历程、与BPC的对比优势,数据收集的多种方式(实时同步、外部导入等)以及合并整体流程,并推出《SAP Group Reporting入门指南》专栏。

bflyfish的博客 281

C语言基础学习笔记

此博文是整理本人初入编程世界学习C语言的经历,零基础想学习C语言的可参考本文的学习方式,内容包含视频+笔记+思维导图总结,根据视频资源编写自己见解的学习笔记,再从笔记中提炼总结思维导图。笔记看得云里雾里是正常的,一是需要配合视频,二是因为写笔记时是想到哪里记到哪里,每个人侧重点不同,笔记仅供参考。

weixin_45068267的博客 2127

LangChain4j + MCP:从工具协议到生产级 Agent

这两年,很多团队都已经把「大模型问答」接入到系统里了:前端一个输入框,后端调一下 OpenAI / DeepSeek / 本地模型,返回一段回答——这类 「简单问答型」AI 功能它只能说,不会做:不会主动查数据库,不会调用你的接口,不会帮你真正执行脚本;面对复杂一点的需求,比如「帮我生成这个报表的 SQL 并执行一下」,普通 ChatGPT 式的问答就开始力不从心了。而Agent 智能体想解决的,就是这件事:不只是回答你「怎么做」,而是能自己思考、自己决定要不要用工具、再用工具把事做完。

OFFTime_we的博客 1150

C语言笔记(全)

C语言笔记

Leon的博客 1020

32位快速加法器设计

32位快速加法器设计

好好学习,天天向上 3289

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

c语言0基础笔记

根据b站视频课程编写的笔记001.C语言课程介绍_哔哩哔哩_bilibili

wusandaofwy的博客 1606

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

C语言笔记~初始C语言

/定义一个整形数组,最多放10个元素。

2301_81121782的博客 1040

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

C语言笔记图片

Rringo_的博客 576

C语言学习笔记

大一时候的C语言学习笔记,自己以后也大概率是不会看的,留个纪念吧。好可以笔记可以放心的扔了。

tyfwin的博客 1168

探索C语言的奥秘:郝斌笔记带你飞

探索C语言的奥秘:郝斌笔记带你飞 【下载地址】C语言总结郝斌笔记 C语言总结(郝斌笔记)欢迎来到C语言学习的重要资源页面,这里汇总了由郝斌老师精心编制的C语言学习笔记,旨在帮助无论是初学者还是正在复习的同学深化对C语言的理解 项目...

gitblog_06597的博客 732

C 语言笔记目录

文章目录 这里将陆陆续续记录C语言学习笔记,顺序和发布时间可能没有关系。所以这里单独做一个目录索引。 数据结构 函数 指针

小田的笔记簿 163

C语言笔记资料整理

学习过程中的C语言笔记资料整理,分享给大家了,在github仓库里大家自取。

c_studer_的博客 392

【笔记】C语言转C++

网课链接:【C语言 转 C++ 简单教程】 https://www.bilibili.com/video/BV1UE411j7Ti/?以下笔记是我通过此网课整理 建议先看完网课 再用这个笔记进行复习。网课老师同时整理了思维导图 可以在视频简介获取。网课老师B站id:别喷我id。视频总时长:01:55:27。

2401_82552318的博客 1642

c语言笔记指针

2401_88045452的博客 269
下一篇: c语言的二分查找
hi_ro_a
博客等级 码龄1年 5粉丝 25原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值