C语言基础——从入门到入墙

C语言

C语言作为一切学习的起点,要多回来复习复习哟

计算机基础

计算机存储单位(从小到大)

1bit(比特位)= 1位二进制(0或1)

1B(Byte字节)=8bit

1KB (Kilobyte 千字节)=1024B,

1MB (Mega byte 兆字节 简称“兆”)=1024KB,

1GB(Giga byte 吉字节 又称“千兆”)=1024MB,

1TB(Tera byte 万亿字节 太字节)=1024GB,其中1024=2^10 ( 2 的10次方),

字节序

计算机中存储数据的方式

  • 大端序

数据的高字节存放到低地址,低字节存放到高地址

  • 小端序

数据的高字节存放到高地址,低字节存放到低地址

内存区域划分

  • 堆空间

只有使用malloc,calloc,realloc这三个函数申请的空间才是堆空间,容量很大,使用完后需要使用free主动释放

  • 栈空间

局部变量,函数调用都是要使用栈空间,先进后出,栈空间容量大概只有8M

  • 数据段

    • .bss 未初始化的静态变量(全局变量和static修饰的变量)
    • .data 初始化的静态变量(全局变量和static修饰的变量)
    • .rodata 常量 const修饰的常量
  • 代码段

    • .text 程序员写的代码段
    • .init 汇编启动代码

进制之间的转换,进制加减法

  • 短除法

注释

作用

辅助别人看懂你的代码

两种注释类型

单行注释

  • 一次只可以注释掉一行代码,注释的代码是不参与编译的
  • 用//表示单行注释
1
// 这是一个行注释

块注释

  • 一次注释掉多行
  • /*CONTENT*/表示块注释
  • 块注释不可以嵌套块注释使用
  • 块注释里面可以嵌套使用行注释
1
2
3
4
5
6
7
8
9
/*
这是一个块注释
// 这是一个块注释中嵌套的行注释
*/

/*
/*
*/ //这样使得后面的那一个*/失效
*/

变量

  • 用来存储数据
  • 变量实际上就是一块可以用来存储可变(类型可变,内容可变)的值的一块内存区域

定义(definition)和声明(declaration)变量

定义声明

[变量类型] {变量名}

会分配内存的声明,有几种写法

1
2
3
4
5
6
7
8
9
10
11
// 定义声明演示
short a; //定义一个short类型的变量,变量名为a,但是该变量没有初始化赋值

short a=89; //定义变量a,立马初始化为89

short a; //定义变量a
a=78; //在使用之前初始化为78

short a,b,c,d; //同时定义了四个变量,都是short类型,都没有初始化赋值

short a,b=89,c,d=56; // 同时定义了四个short类型变量,部分变量初始化

普通声明

extern [变量类型] {变量名}

仅声明,不会分配内存 如extern int a[];

命名规范

  • 变量名只能由字母,数字和下划线组成,不能包含其它特殊符号
  • 变量名不可以跟C语言的关键字同名
  • 首字符不可以是数字
  • 变量取名字通俗易懂,不要用拼音,用英文单词(行业潜规则)
  • 大驼峰法:类名,函数名(通用),属性名,命名空间
  • 小驼峰:变量,函数名(Java系)

数据类型

整数

关键字 类型名 占用字节(Byte)
short 短整型 2
int 整型 4
long 长整型 8(x64) / 4(x32)
long long 长长整型 8
unsigned short 无符号的短整型 2
unsigned int 无符号的整型 4
unsigned long 无符号的长整型 8(x64) / 4(x32)
unsigned long long 无符号的长长整型 8

整数在计算机中的存储方式

最高位是符号位,用0表示正数,用1表示负数

  • 正数:在计算机中采用原码存储的
  • 负数:在计算机中采用补码存储的
  • 原码:指的是一个数原本的二进制
  • 反码:原码全部取反
  • 补码:反码加1得到补码

字符

char类型表示字符,单引号表示单个字符

char可以用ASCII码以整数的形式进行输出,这是因为键盘上的每一个字符都在ascii表中

常用的ASCII码:

字符 ASCII码值
A 65
a 97
‘0’ 48

查看ASCII码表:man ascii

ascii1

ascii2

char字符越界

char类型数据本身占8位二进制,数据超出范围以后,计算机会把高位的去掉,保留低位

1
2
3
4
5
6
// 字符数据越界演示
char a=300; // 字符数据越界
printf("%d",a); // 结果:44

char a=417; // 字符数据越界
printf("%d",a); // 结果:-95

浮点数

当计算机接收到一个浮点数常量时,计算机默认其数据类型为double

关键字 类型名 占用字节(Bte)
float 单精度浮点型 4
double 双精度浮点型 8

浮点数在计算机中的存储方式

  1. 先求出整数部分的二进制(除2取余)
  2. 再来求出小数部分的二进制(乘2取整)
  3. 规格化存储

有些小数是无法精准表示:如12.56
有些小数是可以精准表示:如12.5

指针类型

在不同的处理器中指针长度不一,但在相同处理器中所有类型的指针长度都是一致的

指针就是一个固定长度的地址变量,存放的数据类型是十六进制地址

BOOL类型

在C99 中引入了_Bool类型 如果要使用bool的truefalse, 则需要#include <stdbool.h>

  • 只要不是0,全部都是真(TRUE)的
  • 只要是0,就是假(FALSE)的

枚举类型

枚举的本质就是整型常量

定义

enum 枚举名{枚举值};

  • 没有赋值符
  • 枚举值不指定时,默认从0开始
  • 枚举值不可以是整型

复合数据类型

结构体

结构体中包含多种数据类型,可以处理复杂数据结构情况

共用体

共用体中所有元素公用一块内存区域,共用体大小取决于其中最大的元素的大小,一般应用于互斥变量

数据表示方式

格式控制符

格式控制符 释义
%d 打印有符号整数的十进制
%i 输出:打印整数十进制 输入:接收八、十六进制
%ld 打印长整型
%lld 打印长整型
%hd 打印短整型
%c 打印字符的字面值
%u 把整数当成无符号数来计算然后打印
%lu 打印无符号长整型
%x 打印整数的十六进制
%#X 打印带0x的大写十六进制
%o 打印八进制
%#o 打印带0的八进制
%s 打印字符串
%p 打印地址值(首地址),打印指针
%f 打印float (%.3f 保留小数点后3位)
%lf 打印double

位宽

打印输出变量值的时候,可以设置打印的位宽(打印出来的变量占用的位置宽度)

  • 正数位宽:在数据的左边补充空格,凑够你需要的位宽
  • 负数位宽:在数据的右边补充空格,凑够你需要的位宽
1
2
3
4
5
6
// 位宽演示
int num = 404;
printf("%-10d%d",num,num); //404 404
// 中间间隔7个空格
printf("%2d",num); //404
// 设置2个位宽,位宽设置小了不起任何作用

小数位数

  • %f,%lf打印小数,默认是保留小数点后面6个有效数字
1
2
// 小数位数演示
%-15.9f 15表示15个位宽,9表示小数点后面保留9个数字

转义字符

\把一些特殊的字符转变含义

  • \181 \7A 把8进制或16进制数转换成10进制
  • \x 把十六进制数转为十进制

进制表示[!UnDone]

1
2
3
4
5
// 进制表示
int b = 4'b0111; //二进制
int n = 123; //十进制
int m = 0123; //八进制
int k = 0x123; //十六进制

格式化输入输出

输入SCANF

scanf("格式控制符",变量的地址);

scanf只会严格按照格式控制符进行读取,从键盘上读取到的输入会进入到缓冲区中,然后根据格式化控制符从IO缓冲区中读取合适的字符串后转换成对应的数据类型

int getchar(void)

  • 从缓冲区中一次读取一个字符
  • 返回从缓冲区中读取到的字符的ASCII码值

输出PRINTF

printf("格式控制符",变量的名字);

printf会把存放在IO缓冲区中的内容在屏幕上显示出来

IO缓冲区

有三种情况会自动刷新缓冲区(即输出缓冲区内容)

  1. return/exit/程序结束
  2. 缓冲区满
  3. \n

清空标准IO缓冲区

1
while(getchar() !='\n');

运算符

算术运算符

+ 算术加

- 算术减

* 算术乘

/ 算术除

% 算术余(只能对整数求余)

逻辑运算符

|| 逻辑或

可以理解为算术加 只要有1个1即为1

  • 运算规则:从左到右依次判断每个表达式的值是否为真,如果有一个表达式为真,后面的表达式不参与计算,整个表达式全部为真

&& 逻辑与

可以理解位算术乘 只要有1个0即为0

  • 运算规则:从左到右依次判断每个表达式的值是否为真,如果有一个表达式为假,后面的表达式不参与计算,整个表达式全部为假

! 逻辑反

取反 有1变0 有0变1

逻辑运算符混合运算

由于&&优先级高,所以先用括号把&&括起来作为一个整体,其它部分(其它部门都是逻辑或)按照从左到右

  • 条件1||条件2&&条件3||条件4

例如:if((a>b)||(a=a+10)&&(b=b-a)||(a-b<3))
整体看成逻辑或,从左到右计算,中间的条件2和条件3看成整体

  • 条件1&&条件2||条件3||条件4

例如:if((a>b)&&(a=a+10)||(b=b-a)||(a-b<3))
整体看成逻辑或,从左到右计算,前面的条件1和条件2看成整体

  • 条件1||条件2||条件3&&条件4

if((a>b)||(a=a+10)||(b=b-a)&&(a-b<3))

关系运算符

> 关系大于

< 关系小于

== 关系等于

!= 关系不等

>= 关系大于等于

<= 关系小于等于

赋值运算符

= 赋值运算符

左值

赋值运算左边的那个式子,就叫做左值

  • 左值只能是变量名,不可以是表达式,不可以是常量

右值

赋值运算右边的那个式子,叫做右值

  • 右值可以是任何合法的C语言表达式

位运算符

~ 按位逻辑取反

有1则0 有0则1

& 按位逻辑与

全1则1 有0则0

  • 任何二进制跟1按位与,结果保持不变
  • 任何二进制跟0按位与,结果一定是0

| 按位逻辑或

有1则1 全0则0

  • 任何二进制跟0按位或,结果保持不变
  • 任何二进制跟1按位或,结果一定是1

^ 按位逻辑异或

异则1 同则0

  • 任何一个数跟自己异或,结果是0
  • 任何一个数跟1异或,结果是把这个数最低的二进制反转
  • 任何一个数跟0异或,结果保持不变

求异或后取反可以表示逻辑同或

三目运算符

? :

表达式1?表达式2:表达式3

  • 表达式1为真返回表达式2
  • 表达式1为假返回表达式3

其它运算符

++ 自增

前置自增

++a;先把a的值加1,然后使用加1之后的结果

后置自增

a++;,先使用a原本的值,然后再把a加1

-- 自减

前置自减

--a;先把a的值减1,然后使用减1之后的结果

后置自减

a--;,先使用a原本的值,然后再把a减1

, 逗号表达式

变量=(表达式1,表达式2,表达式3...);

从左到右计算,最后的结果由最右边的表达式来决定

+= -= *= /= %= <<= >>= &= |= ^= 组合运算符

a += b; 等价于 a = a + b;

sizeof 取变量类型大小

sizeof括号内的表达式不会执行,sizeof只会去取表达式结果的类型

  • sizeof(i++);执行后i值不变

<< 左移

把一个整数的二进制向左边移动若干位(舍弃高位),低位补0

>> 右移

不同的操作系统采用不同的右移方式

  • 逻辑右移:把一个整数的二进制向右边移动若干位(舍弃低位),高位补0
  • 算术右移:把一个整数的二进制向右边移动若干位(舍弃低位),高位补符号位的拷贝

. -> 成员运算符

& 取地址

获取变量在内存中的地址

* 解引用

获取地址中保存的内容

运算符的优先级[!UnDone]


程序控制关键字

循环

循环内的变量都属于静态局部变量,循环外不可使用,循环内的定义不会导致重复定义

FOR循环

1
2
3
4
for(表达式1; 表达式2; 表达式3) // 循环控制 表达式分别都可以省略
{
// 循环体代码块
}
  1. 先执行表达式1
  2. 再判断表达式2是否成立
  • 表达式2成立:执行花括号里面的代码,然后执行表达式3,继续判断表达式2
  • 表达式2不成立:结束循环

WHILE循环

1
2
3
4
while(条件表达式)  //循环控制
{
// 循环体代码块
}
  • 条件为真,则执行循环,循环完成一轮后重新判断条件是否成立,若为假退出循环

DOWHILE循环

1
2
3
4
do
{
// 循环体代码块
}while(条件表达式);
  • 循环一定会至少执行一次
  • 条件为真,则继续执行循环,循环完成一轮后重新判断条件是否成立,若为假退出循环

循环控制关键字

  • break 提前彻底结束循环
  • continue 结束本次循环,继续后面的循环

分支

if-else if-else多分支

1
2
3
4
5
6
7
if(条件判断式){
//分支1
}else if(条件判断式2){
//分支2
}else{
//分支3
}
  • 分支只有的一句话的时候可以省略花括号
  • else永远和距离最近的if配对
  • 关键字与花括号之间不能有除空格或回车以外的其他字符

选择分支

1
2
3
4
5
6
7
8
9
10
11
switch(变量)
{
case 1:
// 分支1
break; // 结束case语句
case 2:
// 分支2 继续往下执行
default: // 不符合case列举除的情况则执行
// 分支3
break;
}
  • switch-case中的变量,只能是整数,字符,枚举这些类型
  • default与break都是可选项,default可以随意调整位置
  • case中的分支可以用花括号包裹,如果在case中定义新变量则必须使用花括号包裹

?:三目分支

1
条件判断式 ? 分支1 : 分支2;

跳转分支

1
2
3
LabelName:
// 语句
goto LabelName;
  • 标签下不可以定义新的变量(重复定义)
  • goto只能在同一个函数内部跳转

数组

当需要保存一定数量的相同类型的数据时,就需要用到数组

任何合法的C语言类型,都可以定义成数组

数组定义

[数据类型] {数组名}[数组元素个数]

数组名出现在表达式中,本身就是一个指针, 一个指向本数组首元素地址的指针
&数组名 表示的是指向整个数组的一个指针(数组指针)

数组初始化以及赋值

  • 完全初始化
1
2
int a[5]={45,85,74,96,63};  
char b[10]="hello";
  • 部分初始化
1
2
3
int a[5]={45,25};              
char b[10]={'h','e','l','l','o'}; // 'h' 表示单个字符
char s[20]={'h','h',0} // \0的ASCII码就是0 {0}即等价于{’\0’}

其它没有初始化的都是0

  • 自适应长度初始化
1
2
int a[]={85,96};      
char b[]="world" // "world" 表示字符串 多个字符

只能在定义数组的时候立马初始化才可行

  • 延迟赋值
1
2
3
4
int a[3];    //定义了数组,但是没有初始化
a[0]=45; //正确
a[1]=85; //正确
a[3]=88; //数组越界
  • 分块初始化
1
2
int b[100] = {[0 ... 49]=100, [50 ... 99]=200}; 
//把数组前面50个元素初始化为100,后面50个初始化为200

... 左右两边必须要有空格

数组重复写入

1
2
3
4
5
6
7
8
9
char buf[10];
while(1)
{
scanf("%s",buf);
}
// buf的值 (0表示\0)
// abcdefg0
// hijk0fg0
// lm0k0fg0

数组访问

通过下标来对数组进行读或写

数组指针访问

*(p+n) 等价于 p[n]

假如第一次输入的字符串长一点,下一次输入的字符串短一点,scanf都会自动帮你在字符串后面添加\0保存到buf中,短一点的字符串覆盖长字符串的前面部分,并且有个\0在后面(易忽略)

数组清空

若是在使用前不对数组进行清空,则有可能会有残留的不确定的值还存留在数组涵盖的范围内(如上所示),导致程序运行时产生不确定的结果

  • void bzero(void *s, size_t n);

s:数组名 n:数组大小

  • 初始化时利用 int buf[10]={0};

利用部分初始化把数组清零,-其它没有初始化的默认都是0

数组越界

数组的下标范围是从0到数组的长度-1,若是访问的时候下标不在该范围内,就会出现数组越界,有可能会导致段错误(Segmentation fault),越界也分为往前越界和往后越界

段错误

由于程序员在代码中访问了非法地址导致的(经常见于指针没有初始化,数组越界)

数组怪异的写法

1
2
int buf[10];
0[buf]=11; //等价于buf[0]=11;

数组取地址的写法(易混淆)

1
2
3
4
5
6
7
8
9
// 数组 int a[10]
a // int *类型的指针
&a // 数组指针
a[0] // 非指针,数组首元素值
&a[0] // 数组首元素的地址
a+1 // 加类型的大小 4个字节(int)
&a+1 // 加的是整个数组的大小(10*4)
a[0]+1 // 把a[0]的值加1
&a[0]+1 // 加类型的大小 4个字节(int)

数组指针

指向某个数组的指针

**类型 (*指针名)[数组元素个数]**

数组指针的定义

1
2
3
4
5
6
int a[10];
char b[15];
int c[10];
int (*p)[10]=&a; //定义了 int[10]类型的数组指针,指向a
p=&c; //p又转而指向同类型,不同地址的c
char (*q)[15]=&b; //定义了 char[15]类型的数组指针,指向b

同类型的数组指针仍然可以直接赋值,反之不行

同数组的指针相减

表示的是数组中两个指针之间间隔了多少个数据

数组存储

数组在计算机中是连续存储(数组元素的地址是紧挨着的)

字符串存储

字符串默认有一个结束标记\0(ASCII码值就是0)

1
2
3
4
char buf1[10]="hello"; //会自动在字符串的末尾添加\0
char buf2[5]="hello"; //没有空位自动添加\0
char buf[]={'h','e','l','l','o'}; // 属于字符数组存储 sizeof结果为5,没有加\0
char buf[]="hello" // 属于字符串存储 sizeof结果为6,加上了\0

一维数组大小

sizeof();

求任何类型数据的大小的运算符,若是计算字符串的长度,则会算上\0

size_t strlen(const char *s);

专门(只能)用来求字符串的实际长度的函数,使用时需要导入#include <string.h>

返回值:字符串的实际长度(不包括\0) s:要计算长度的字符串
原理:计算字符串实际长度,遇到\0认为字符串结束了

%s打印字符串的时候,也是遇到\0就认为字符串结束了


二维数组

用二维的方式存放大量相同类型的数据

二维数组定义

[数据类型] {数组名}[行数][列数]

任何合法的C语言类型,都可以定义成二维数组

二维数组名在表达式中代表该数组首元素的地址 相当于一个指向本数组首元素地址的指针
&二维数组名表示的是指向整个数组的一个指针(数组指针)

二维数组初始化和赋值

  • 部分初始化
1
int a[5][6]={45,78,96};

其它没有初始化的默认全部都是0

  • 自适应行数初始化
1
2
3
4
int a[][6]={89,9};                   //正确
char b[][10]={"hello","world","gec"};//正确
int a[][]={89,9}; //错误
int a[5][]={89,9}; //错误

不能自适应行列数是因为编译器无法正确确定行列排布方式,具有二义性

  • 分组初始化
1
2
char b[3][10]={{'h','e','l','l','o'},{'g','e','c'}}   //正确
int a[5][6]={{5},8,9,{89,63}}; //错误

要求从左到右连续分组,中间不要间断

  • 单独初始化
1
2
3
4
5
6
//分别赋值
int a[5][6];
a[0][0]=99;
a[1][2]=67;
char b[3][10];
b[0][1]='h';

二维数组的访问

行和列的下标:都是从0开始,注意越界问题

二维数组的指针访问

*(*(a+i)+j)等价于 *(a[i]+j)等价于a[i][j]

二维数组的存储

二维数组在计算机中是连续存储(数组元素的地址是紧挨着的)

可以把它看作是特殊的”一维数组”
可以把二维数组拆分成多个一维数组

二维数组占用空间大小

依据初始化列表中值的个数 (行下标为空的情况下),确定有几行,再通过 sizeof(类型) 确定大小

字符串

字符串的本质就是**char 字符串名[字符串长度]**

各类字符串的操作函数都需要先引入#include <string.h>

复制、拷贝字符串

char *strcpy(char *dest, const char *src);

把src中的字符串拷贝一份到dest里

  • strcpy拷贝字符串会自动在后面加上\0(特别是src比dest要短的情况下)

char *strncpy(char *dest, const char *src, size_t n);

把src中的字符串拷贝一份到dest里面,拷贝n个字节

  • strncpy拷贝字符串的时候不会自动添加\0

字符串的比较

比较标准:按照两个字符串中字符的ASCII码排列顺序来比较

int strcmp(const char *s1, const char *s2);

  • 参数:s1 s2就是你要比较的两个字符串

返回值:

  • s1大于s2 返回值>0
  • s1小于s2 返回值<0
  • s1等于s2 返回0

int strncmp(const char *s1, const char *s2, size_t n);

  • 参数:比较s1和s2的最前面的n个字符是否相同

字符串合并,拼接

char *strcat(char *dest, const char *src);

  • 参数:把src合并到dest中,合并的结果存放到dest中

char *strncat(char *dest, const char *src, size_t n);

注意:dest总的大小必须要超过dest字符串+src字符串之和

int sprintf(char *str, const char *format, ...);

  • 参数:str:保存拼接之后的字符串 format:格式控制字符串

字符串的切割

char *strtok(char *str, const char *delim);

底层原理:strtok会改变原始字符串,strtok会把原始字符串中遇到的切割符号替换成\0

第一次使用的时候str填原始字符串 之后使用时该参数为NULL

  • 参数:str:你要切割的字符串 delim:切割的标准

返回值:

  • 切割成功返回切割得到的子串
  • 字符串切割完毕或者切割过程中发了错误,返回NULL

判断子串

char *strchr(const char *s, int c);

  • 参数:s :你要查找的字符串 c:你要找的字符

返回值:

  • 返回字符c首次在s中出现的位置
  • 如果没有出现,返回NULL

char *strstr(const char *haystack, const char *needle);

实际用途:判断文件类型

二维数组名的写法(易混淆)

1
2
3
4
5
6
7
8
9
10
11
12
a          // 二维数组首元素的地址->  a[0]的地址,数组指针  int (*p)[10]
&a // 数组指针 int (*p)[7][10]
a[0] // 第一个一维数组int[10]的名字,表示该数组首元素a[0][0]的地址 int *
&a[0] // 数组指针 int (*p)[10]
a[0][0] // 非指针
&a[0][0] // int *
a+1 // 加类型的大小 int[10]大小
&a+1 // 加的是整个数组的大小,int[7][10]大小
a[0]+1 // 4个字节
&a[0]+1 // 加类型的大小 int[10]大小
a[0][0]+1 // 把数据加1
&a[0][0]+1 // 4个字节

指针

用来存放变量在内存中的地址首地址,使用指针可以间接访问地址中的变量值,指针的大小跟指针的类型没有任何关系,只跟操作系统的位数有关。在32位系统上,所有的指针大小都是4个字节,在64位系统上,所有的指针大小都是8个字节,指针是来存放变量在内存中的首地址的,而64位系统上所有的地址长度都是8个字节

首地址:在一定地址范围中地址值最小的那个,就是该地址范围的首地址

指针定义

[数据类型] *指针名

指针本身也是一个变量,只是这个变量存储的是地址值

指针类型转换

指针1=(指针类型)指针2

当两个指针类型不一致的时候又需要传值的时候,就需要强制转换

指针初始化和赋值

  • 立即初始化
1
2
int a=99;
int *p=&a; //定义了指针p,p里面存放变量a在内存中地址
  • 延迟初始化
1
2
3
int b=77;
int *q; // 定义了指针q
q=&b; // 指针q中存放变量b在内存的地址

通用指针

void *类型的指针,可以兼容任何其它类型的指针转换

野指针

定义了指针,但是指针没有明确的指向,这种指针就叫做野指针,解引用时会导致段错误

空指针

在C语言中 NULL 和 0 指的都是空指针,C++11后专门加入了nullptr来表示空指针

NULL表示的内存中的0x0000 0000

指针的读写

解引用和取地址互为逆运算

  • 解引用:获取地址中保存的内容

*指针

用于指针访问或者修改变量的值

  • 取地址:获取变量在内存中的地址

地址分配

地址的分配和释放都需要导入#include <stdlib.h>

void *malloc(size_t size);

分配堆内存空间

  • 参数:size:要分配的堆空间的大小(单位:字节)

返回值:

  • 分配给你的堆空间的首地址

void *calloc(size_t nmemb, size_t size);

申请分配nmemb块内存,每个的大小是size

  • 参数:nmemb:申请的内存区域块数 size:申请的内存区域每一块的大小

返回值:

  • 分配给你的堆空间的首地址

void *realloc(void *ptr, size_t size);

重新分配堆内存

  • 参数:ptr:要重新分配空间的堆空间的首地址 size:新的堆空间的大小

返回值:

  • 分配给你的新的堆空间的首地址

地址释放

void free(void *ptr);

释放堆空间,申请的堆空间必须主动释放,操作系统不会帮你释放

  • 参数:ptr:之前申请过的堆空间首地址

内存泄漏

一直申请堆内存,但是都没有释放,导致堆内存越来越少,最后没有可以使用的堆内存

堆内碎块

频繁地分配和释放不同大小的堆空间会导致堆内碎块

1
2
3
4
5
char *p = "hello"; // 指针指向字符串常量 常量无法修改
*p = 'k'; // 断错误
char str[10] = ["hello"];
char *p;
p = str; // 指针指向变量 ,可以修改

( 一 ) char p=”String” 此”String” 为常量 ( 二 ) strcpy(*str,”String”),此字符串是常量

指针的运算

+ 算术加

用指针去加一个整数,指针做加法,加的是类型的大小

一个指针加上另外一个指针没有任何实际意义

- 算术减

用指针去减一个整数,指针做减法,减的是类型的大小
结果:(指针1地址值-指针2地址值)/类型大小

一个指针减去另外一个指针有实际意义(在数组中是有意义的)
一个指针减另外一个指针,结果不是地址值直接相减,而是两个指针之间相差的对应类型之间的位数
++ 自增

1
2
3
4
*p++   //先解引用p,然后p加1
(*p)++ //先解引用p,然后把解引用的值加1
*++p //先将++用于p,然后解引用
++*p //先解引用,然后把结果加1

= 关系判断

比较两个指针是否相等

指针数组

数组中存放的全部都是指针,这种数组就叫做指针数组

指针类型 数组名[数组元素个数];

指针数组的定义

1
2
int *a[3];  //3个int *
char *b[4]; //4个char *

二级指针

二级指针也是指针,该指针用来存放一个一级指针在内存中的地址

二级指针解引用将变成对应的一级指针

二级指针的运算

二级指针做加减法,加减的是指针(一级指针)的大小(64位系统加减8的倍数 32系统加减4的倍数)

函数指针

指向函数的入口地址的指针

函数名就是个指针,函数名表示函数的入口地址

函数指针定义

1
2
3
4
5
int add(int a,int b);
//定义一个函数指针指向add
int (*p)(int,int)=add;
int *(*array[10])(int) // 定义一个数组,这个数组可以存放10个p类型的指针
//int *(*函数指针名字)(int) -> int *(*数组名[10])(int)

函数指针调用

1
2
3
int (*p)(int,int)=&add;
(*p)(15,16);
p(15,16);

函数指针的写法很特殊,add和&add没有区别


函数

将代码模块化,用来提升代码的复用率

函数封装原则上要”高内聚,低耦合”,高内聚就是把功能相关的模块集合在一起,低耦合就是每个模块之间的关联性降到可控范围的最低。

函数定义

1
2
3
4
返回值类型  函数名字(形参列表) // 函数头
{
//函数体
}
  • 函数头

函数的属性描述,如int add(int a,int b)就是一个函数头

  • 函数体:{}中包裹的代码块
  • 返回值类型:调用函数后将会返回的结果
  • 函数的声明(函数原型声明)

函数声明可以放在主函数的前面,也可以单独放去头文件中

  • 函数的定义:函数头和函数体完整地放在一起就是函数的定义

函数可以定义在主函数前面,也可以先在主函数前声明后,定义在主函数后

  • 形参:形式参数,函数声明中或者函数定义中圆括号里面的就是形参
  • 实参:实际参数,函数调用时程序员传递的参数就是实参

函数调用

函数名字(实参);

函数调用的过程

当你调用一个函数的时候,操作系统会帮你把这个函数入栈(压栈)
当函数调用结束的时候,函数会被自动出栈(弹栈)

入栈(压栈)

把局部变量/函数的入口地址存放到栈空间的栈顶

出栈(弹栈)

把局部变量/函数的入口地址从栈空间的栈顶自动释放掉

函数的入口地址

函数名就是个指针,就代表该函数在计算机中的地址(入口地址)

实参和形参

  • 无参传参

其实就是传入void类型的参数

  • 传值传参

实参把自己的值拷贝一份给形参,只要函数的形参写成普通变量(非指针),那就是传值

  • 传址传参

实参把自己的地址赋值给形参,只要函数的形参写成指针,那就是传地址

  • 实参和形参拥有各自独立的地址空间
  • 实参和形参可以同名
  • 任何数组作为函数的形参,sizeof()求大小都是当成指针来求大小

函数返回值

  • 没有返回值 void

要退出函数时直接写return

  • 返回普通变量类型

要退出函数时return对应的值,编译器会把局部变量的值备份一份到寄存器中,返回的就是寄存器里面的备份值

一般来说,如果函数的返回值类型是int,return 0表示函数正常退出,-1表示函数异常退出

  • 返回指针

不可以返回局部变量的地址

局部变量的作用域只是在定义它的函数中生效,当函数退出的时候,该局部变量的地址(栈空间)空间会被自动释放

递归函数

一个函数在执行的过程中,又再一次调用了该函数本身

一般栈空间只有数M,因此递归非常容易产生栈溢出,一定要保证初始化和退出条件的正确性。

递归三要素

  1. 确定递归形式参数
  2. 确定终止条件
  3. 确定求值逻辑

内联函数

当函数代码量很小,并且需要反复使用的时候,就可以选择加上inline关键字定义成内联

当gcc无法识别内联函数的关键字时,可以尝试在编译时使用-fgnu89-inline 参数

1
2
3
4
inline 函数的返回值  函数名(形参)
{
// 函数体
}

解决函数调用时入栈和出栈的时间耗费,对于经常要使用的,短小的代码适合定义成内联函数

原理

编译器会在调用的位置直接把内联函数的源码展开

优点

节约了函数调用入栈出栈的时间耗费,内联函数用内联代码替换函数调用,内联函数体的代码不能过长,因为内联函数省去调用函数的时间是以代码膨胀为代价的,内联函数不能包含循环语句,因为执行循环语句要比调用函数的开销大

缺点

增加主程序的长度,增加了主函数占用的内存,耗费了比较多的内存

回调函数

将函数作为参数交给适合触发该函数的函数,这个参数函数就称为回调函数。

变参函数

一个函数的形参类型,个数不确定,这种函数就是变参函数

... 用来来省略参数的类型和个数

原理

第一个参数必须是确定的

参数从右到左依次进栈,由前面确定的参数回溯出后面的变参(例如:printf的第一个参数格式输出符号回溯出后面的参数有几个,分别是什么类型)

char和short会被自动提升为int,float会被提升为double

主函数

int main(int argc,char **argv)

主函数也是一个参数,在运行程序的时候,空格后跟着的字符串作为主函数的参数传入

  • 参数 argc 参数个数 argv 参数字符串组

如果传入的是数字,这时候就需要将数字字符串转换成整型数字,
int atoi(const char *nptr);

将字符串转换成数字,需要导入#include <stdlib.h>

  • 参数:nptr –》你要转换的字符串
  • 返回值:转换后的整数

指针函数

只要一个函数的返回值是指针,那么这个函数就叫做指针函数

1
2
3
int *p(int,int);   // 指针函数
//返回函数指针: 返回值类型 (*函数名字(形参))(形参)
//返回数组指针: 数组类型 (*函数名字(形参))[数组元素个数]

结构体

结构体是一种复合数据类型

结构体定义

1
2
3
4
struct (结构体名)
{
// 数据类型...
}(别名);
  • 不加结构体名则为匿名结构体

结构体使用

1
2
3
4
5
6
7
//在定义前加上typedef 
typedef struct student
{
char name[10]; //姓名
}stu;
struct student stu1;
stu stu2; //等价于struct student stu2;
1
2
3
4
5
6
7
//给匿名结构体类型取别名stu,结构体指针类型取别名stup
typedef struct
{
char name[10]; //姓名
}stu,*stup;
stu stu1; //定义了一个学生结构体变量叫做stu1
stup p; //定义了一个学生结构体指针叫做p
1
2
3
4
5
6
7
//定义结构体时,定义一个结构体变量stu并初始化stu里的元素值
struct student
{
char name[10]; //姓名
int age; //年龄
float score; //分数
}stu={"zhangsan",18,65.5};
  • 结构体中不能直接定义函数,但是可以声明函数指针,给函数指针赋值一个函数即可通过结构体调用结构体中定义的函数函数赋值的函数

结构体赋值与初始化

  • 直接赋值
1
structstudent stu1={"zhangsan",18,56.9};     
  • 延迟赋值
1
2
3
4
struct student stu1;
strcpy(stu1.name,"张三"); //char数组不能直接赋值字符串 stu1.name="张三";是错误的
stu1.age=19;
stu1.score=85.5;
  • 命名赋值
1
2
3
4
5
struct student stu1={
.score=56,
.age=15,
.name="李四"
};

还可以部分初始化,十分灵活

  • 结构体赋值
1
2
struct student stu2;
stu2=stu1;

结构体运算

. 直接成员运算符

调用普通结构体里面的成员

-> 间接成员运算符

调用结构体指针里面的成员

sizeof() 结构体大小

结构体的存储需要满足字节对齐,如果你的操作系统是64位系统:

找到结构体中类型最大的成员,如果类型最大的成员>=8字节,整个结构体依照8字节对齐,如果类型最大的成员<8字节,整个结构体依照类型最大的成员对齐

位域

有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。

共用体

联合体中所有的成员共用同一块内存区域,联合体的大小由最大的成员大小来决定,也要满足字节对齐

共用体的定义

1
2
3
4
union 共用体名
{
//成员;
}(别名);

作用域

表示变量的作用范围

  • 如果全局变量跟局部变量同名,此时局部变量会隐藏全局变量,导致全局变量不可见

全局作用域(文件作用域)

在所有函数的外面定义的变量就是全局变量

  • 全局变量可以被所有的函数共享
  • 实际开发中,全局变量往往用来在不同的函数之间传递信息的(共享信息)
  • 全局变量没有初始化,默认都是0

局部作用域(代码块作用域)

在函数里面定义(包括形参)的变量,或者在for循环里面定义的就是局部变量

  • 局部变量只可以在定义它的那个函数范围内使用
  • 局部变量没有初始化,默认是垃圾数

链接性

变量能否在不同的文件中共享

外部链接性

全局变量具备外部链接性

  • 全局变量可以在定义它的.c文件中使用,也可以在其它的.c文件中使用

内部链接性

用static修饰的静态全局变量具备内部链接性(extern staic const define typedef)

  • 变量只能在定义它的.c文件中使用,其它文件中不能使用

无链接性

局部变量是无链接性,局部变量只能在定义的函数里面使用

存储持续性

描述变量的生命周期

  • 自动存储

变量从定义的位置开始,到函数结束自动释放(局部变量就是自动存储)

  • 静态存储

只要是static修饰的变量都是静态存储,全局变量也是静态存储,变量从定义的位置开始,到整个程序结束才释放

  • 动态存储

使用堆空间,只要使用malloc calloc realloc分配的地址空间,全部都是动态存储, 使用的时候要申请分配,使用完毕需要主动free释放

关键字

static

  • static修饰全局变量

改变全局变量的链接性,全局变量的链接性会从外部链接变成内部链接,此时这个全局变量只能在定义它的.c文件中使用,其它.c文件不可以使用

  • static修饰局部变量

表示该局部变量只能被初始化一次,static修饰的局部变量存放在数据段中
普通的局部变量存放在栈空间

  • static修饰函数

普通函数具备外部链接性,static修饰的函数具备内部链接性,只能在定义它的.c文件中使用

const

  • 修饰普通变量

标记为常量,常量是只读的(可以访问,不能修改)

  • 修饰指针
1
2
3
4
5
int a=99;
const int *p=&a; //表示指针p不可以修改a的内容
int const *p=&a; //第一种和第二种是等价的
int *const p=&a; //p指向a以后,后面就不可以再去指向其他变量的地址
int const * const p=&a; //p既不能修改指向的变量,也不能指向其他变量的地址

extern

  • 声明外部全局变量

如果当前文件需要使用在别的.c文件中定义的全局变量(非静态),用extern声明即可

  • 声明外部函数

如果当前文件需要使用在别的.c文件中定义的函数(非静态),用extern声明即可

typedef

给变量类型取别名

typedef 基本数据类型 新的别名;

1
2
3
4
5
typedef unsigned int u32;  // 给基本数据类型取别名
typedef char * charp; // 给指针取别名(用的比较多)
typedef int(*funp)(int); //给int(*)(int)函数指针,取了别名,叫做funp
typedef int a[10]; //给int[10]这种类型的数组,取了别名,叫做a
// 给结构体取别名

定义可移植数据类型

同样的代码,编译运行的时候,不同的系统可能会出现数据越界问题

解决方法:用别名来定义数据,需要的时候直接修改别名对应的数据类型即可

volatile

修饰易变的变量,告诉编译器这个变量的值短时间内会改变很多次,不要去优化该变量

一个定义为volatile的变量说明此变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份

编译器优化的含义

例如编译器会聪明地认为如果一个变量i赋值给了a,然后执行几行代码之后再赋值给b,那么编译器会认为a和b是相等的(过程中没有代码改变i的值),其实这是不完全对的,有些情况下虽然代码不能改变i的值,但是其他行为可以改变(例如i是一个寄存器变量或者端口数据)

预处理编译

头文件

用来包含其他头文件,函数的声明,全局变量,结构体,共用体,枚举的定义,内联函数等

#include <头文件>

自定义头文件

“” 用于自定义的头文件,编译器默认会先从当前路径下寻找这个头文件,如果找不到,就去系统的环境变量中查找头文件,两个地方都找不到就报错(没有这个头文件)

<> 通常用于系统提供的头文件,编译器默认去系统的环境变量中查找头文件

解决头文件重复包含的问题

1
2
3
4
#ifndef  _头文件的名字_H
#define _头文件的名字_H
//头文件里面的具体内容
#endif

宏定义define

用作字符的替换,一般写成大写

  • 普通宏定义 不带参数
1
#define  PATH  "/dev/fb0"
  • 宏函数 带参
1
#define max(a,b) a>b?a:b;

宏函数不是真正意义上的函数,宏函数没有入栈出栈的操作,普通函数需要入栈,出栈

特点

  1. 每个单独的宏定义,都各自占一行
  2. 宏定义一行写不下,必须加上续行符
  3. 宏定义中的参数都是没有数据类型的
  4. 带参数的宏定义,参数用圆括号括起来

条件编译

满足条件,编译器就会编译对应的代码,不满足,代码会被编译器忽略(相当于注释掉了代码)

  • 条件满足编译
1
2
3
4
5
6
7
#ifdef DEFINE
// CODE
#elif OTHERDEFINE
// CODE
#else OTHERDEFINE2
// CODE
#endif
  • 条件不满足编译
1
2
3
4

#ifndef DEFINE
// CODE
#endif

程序编译

一键生成可执行文件

gcc program.c -o program

预处理

展开预处理命令

gcc program.c -o program.i -E

编译

生成汇编文件

gcc program.i -o program.s -S

汇编

生成可重定位文件

gcc program.s -o program.o -c

链接

链接生成可执行文件

gcc program.o -o program

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022 Elimos
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信