C基础

简介

优点:执行速度快,功能强大,编程自由。代码量小:dll封装等

缺点:编程周期长,可移植性较差,过于自由,容易出错,对于平台库依赖较多。

可用部分:网站后台,程序库,游戏引擎,写语言,操作系统,微处理器

构成:32个关键字,9种控制语句,34种运算符(算术运算符,关系运算符,逻辑运算符,位运算符,复制运算傅符,条件运算符,逗号运算符,指针运算符,求字节数,强制类型转换,分量运算符,下标运算符)

1
2
3
#include <xxx>  //表示导入系统文件

#include "xxx" //表示导入自定义文件

c编译步骤

  • 预处理:宏文件展开、头文件展开、条件编译等,同时将代码中的注释删除,并不检查语法
    • gcc -E hello.c -o hello.i
  • 编译:检查语法,将预处理后文件编译生成汇编文件
    • gcc -S hello.i -o hello.s
  • 汇编:将汇编文件生成目标文件(二进制文件)
    • gcc -c hello.s -o hello.o
  • 链接:程序依赖各种库,编译之后需要将库链接到最终的可执行程序中
    • gcc hello.o -o hello
    • -o 表示生成一个文件

程序执行过程

硬盘(外部存储设备)->内存(MEM,代码区,数据区,栈区,堆区)->CPU

64位与32位操作系统区别

  • 寄存器是CPU内部最基本的存储单元
  • CPU对外通过总线(地址、控制、数据)来和外部设备交互,总线的带宽是8位,同时CPU的寄存器也是8位,那个CPU就叫做8位CPU
  • 如果总线是32位,寄存器也是32位的,这个CPU为32位CPU
  • 所有的64位CPU兼容32位的指令,32位要兼容16位的指令,所以在64位的CPU上可以识别32位的指令
  • 在64位的架构上运行64位的操作系统,那个这个系统为64位
  • 64位的CPU运行32位的操作系统,这个系统为32位
  • 64位的软件不能运行在32位的CPU上

总线越宽,速度越快

寄存器,缓存,内存的关系

所有的运算都要放到CPU中计算,CPU直接打交道的其实是寄存器

内存和寄存器进行数据读写

数据类型

数据类型关键字:char, short, int ,long, float, double

unsigned, signed, struct, union, enum, void

控制语句关键字:if ,else, switch, case,default, for ,while , break, continue,goto, return

存储类关键字auto , extern, register, static, const

其他关键字:sizeof, typedef( 定义函数指针,定义别名), volatile(防止编译器做优化)

数据类型的作用:编译器预算对象(变量)分配的内存空间大小

基址:在编译过程中决定

常量:在程序运行过程中,其值不能发生变化的量

1
2
3
const int price = 3; //(不安全写法,限定在c语言中)

#define PI 3.14159 //宏定义常量

变量:在程序运行过程中,其值可以发生变化的量

标识符命名规则

  • 不能使用系统关键字

  • 允许字母,下划线,数字,数字不能开头

  • 标识符区分大小写

  • 见名知意

整型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
%d

%o 8进制int类型

%x 16进制int类型,字母以小写输出

%X 16进制int ,字母以大写输出

%u 输出10进制无符号数

定义八进制数据:以0开头

定义十六进制数据:以0x开头

在计算机中不可以直接定义二进制

&取地址符

sizeof()不是函数,不需要包含任何头文件,计算一个数据类型的大小,单位为字节。

操作系统栈和堆

地址空间布局:
操作系统地址空间布局

执行期间编译器自动分配,编译器用它实现函数调用,调用函数时,栈增长,函数返回时,栈收缩。局部变量、函数参数、返回数据、返回地址等放在栈中

栈的特点

  1. 内存分配取决于编译器,用户栈在程序运行期间可以动态的扩展和收缩
  2. 和数据结构中的“栈”本质上是不一样的,但是操作方式类似于栈。
  3. 数据从栈中的进出满足“后进后出”的规律。
  4. 栈向低地址方向增长,esp(栈指针)指向栈顶元素。

动态储存器分配器维护着的一个进程的虚拟存储器区域。一般由程序员分配释放(堆在操作系统对进程初始化的时候分配),若程序员不释放,程序结束时可能由OS回收,每个进程,内核都维护着一个变量brk指向堆顶。

堆的特点

  1. 内存分配取决于程序员,C/C++可以手动释放该片内存。
  2. 和数据结构的”堆“完全两回事,没有半点关系,在这里堆的结构更像链表
  3. 所有的对象,包括数组的对象都存在堆上
  4. 堆内存被所有的线程共享
  5. 引用类型总是放在堆中。
  6. 堆向高地址方向增长,内核维护的变量brk指向堆顶

注意:值类型和指针总是放在他们被声明的地方
当值类型的数据在方法体内被声明时,它们都应该放在栈上。
如果一个值类型被声明在方法体外且存在于一个引用类型中,那么它将会被堆里的引用类型所取代。

全局区/静态区:

全局变量、静态变量、常量的存储区域,程序终止时系统释放。

文字常量区:

存放常量字符串,程序结束后由系统释放。

程序代码区:

存放函数体(类成员函数和全局函数)的二进制代码。

实例

1
2
3
4
5
6
7
8
9
10
11
12
int a = 0;        //全局初始化区
char *p1; //全局未初始化区
void main()
{
int b; //栈
char s[] = "123"; //栈
char *p2; //栈
char *p3 = "sdfghhj"; //其中,“sdfghhj\0”常量区,p3在栈区
static int c = 0; //全局区
p1 = (char*)malloc(10); //10个字节区域在堆区
strcpy(p1,"sdfghhj"); //"sdfghhj\0"在常量区,编译器可能会优化p1和p3指向同一块区域
}

栈和堆的区别:

  1. 栈内存存储的的是局部变量,堆内存存储的是实体。
  2. 栈内存的更新的速度会更快些(局部变量),堆内存的更新速度相对更慢
  3. 栈内存的访问直接从地址读取数据到寄存器,然后放到目标地址,而堆内存的访问更麻烦,先将分配的地址放到寄存器,在读取地址的值,最后再放到目标文件中,开销更大。
  4. 栈内存是连续的空间,堆内存一般情况不是连续的,频繁地开辟空间,释放空间容易产生内存碎片(外碎片)。

栈和堆的联系:

堆中对象是直接由栈中的句柄(引用)管理者,所以堆负责产生真实对象,栈负责管理对象。

类型系统

字符变量实际上并不是将该字符本身放到变量的内存单元,而是将该字符对应的ASCII编码放到变量的存储单元。char的本质就是一个字节大小的整型。

不以f结尾的常量都是double类型,以f结尾的为float类型。

1
2
3
4
5
float a = 3.14 //实际为double类型转换为float类型

%p //打印地址,一个变量对应的内存地址编号(无符号十六禁止整型数)

a = 3.2e3f //科学计数 3.2*1000 = 3200

整型和字符型数据存储

数据在计算机中主要以补码的形式存储。

数据传输以bit表示。

原码:最高位为符号位,0表示正,1表示负。当两个整数相减或不同符号数相加时,必须比较两个数哪个绝对值大才能决定谁减谁,才能确定结果为正还是负,所以原码不便于加减运算

反码:正数与原码一样。负数:符号位不变,其它取反。反码运算也不方便,通常用来作为补码的中间过渡。

补码:计算机系统中,数值一律用补码来存储。对于正数:原码,反码,补码相同。负数:补码为它的反码加1补码符号位不动,其他位求反,加1得到原码

补码原因:

  • 统一零的编码(0在计算机中的存储方式:按照原码和反码都需要区分0和-0)
  • 将符号位和其他位统一处理
  • 将减法运算转变为加法运算
  • 两个补码表示的数相加时,如果最高位(符号位)有进位,则进位被舍弃。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
char ch = 10; 
原码 0000 1010
反码 0000 1010
补码 0000 1010
char ch1 = -10;
原码 1000 1010
反码 1111 0101
补码 1111 0110

ch + ch1 = 0
0000 1010
1111 0110
1 0000 0000
0000 0000

76 - 32 转化为 76 + (-32)
76原码 0100 1100
反码 0100 1100
补码 0100 1100
-32原码 1010 0000
反码 1101 1111
补码 1110 0000
1 0010 1100
0010 1100 得到原码为: 0010 110044
76 - 82
-82原码 1101 0010
反码 1010 1101
补码 1010 1110
相加: 1111 1010 得到原码: 1000 0110-6

8bit数据最大存储区间为:[-128, 127]

数据存储时,将-0对应的区间值设为-2^7 也就是-128

无符号:数据在计算机中不存在符号位

usigned char : 0 - 2^8 -1 : 0 - 255

数值溢出

当超过一个数据类型能够存放最大的范围时,数值会溢出

有符号最高位溢出的区别:符号位溢出会导致数的正负发生变化,但最高位的溢出会导致最高位丢失。

类型限定

限定符 含义
extern 声明一个变量,extern声明的变量没有建立存储空间。在定义的时候再创建存储空间。
const 定义一个常量,常量的值不能修改
volatile 防止编译器优化代码
register 定义寄存器变量,提高效率,建议型的变量而不是命令型的指令。如果CPU有空闲寄存器,则register生效,没有空闲则无效

字符串常量

  • 字符串常量是内存中一段连续的char空间,以’\0’结尾
  • 字符串常量是由双引号括起来的字符序列,如”china”
  • 字符串常量与字符常量的不同:
    • ‘a’为字符常量, 实际存储’a’
    • “a”为字符串常量 , 实际存储’a’’\0’
    • 每个字符串的结尾,编译器会自动的添加一个结束标志位’\0’,即”a”包含两个字符’a’和’\0’.
    • 占位符%s,表示输出一个字符串,遇到\0停止。
1
2
3
putchar() //输出字符,可以是变量,字符,数字
getchar() //从标准输入设备读取一个char
scanf() //内部参数中不能包含\n,可以用空格,逗号等。。。

运算符号

算术运算符

  • 两个整数相除一定得到一个整数,默认向下取整,如果要向上取整,原数+1后再做除法

  • 取余只能对整数

  • 自增,自减

  • 后自增: a++ , 先进行表达式计算,再进行++

  • 前自增: ++a ,在表达式之前进行++,再进行表达式计算

1
2
3
int a = 10;
int b = ++a * 10;// a = 11, b = 110
int b = a++ * 10;// a = 11, b = 100 先完成计算,再增加1

比较运算符

c语言的比较运算中,”真“用数字”1”来表示,”假“用数字”0“表示

运算符优先级

单目高于双目运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. [] () . 若同时出现,从左到右
2. \- ~(按位取反) ++ -- *(取值运算符) &(取地址运算符) ! (类型)(强制类型转换) sizeof, 从右到左
3. / * %
4. \+ -
5. << >>
6. \>= > < <=
7. == !=
8. & (按位与)
9. ^(按位异或)
10. |
11. &&
12. ||
13. ?:
14. = /= %= -= += >>= <<= &= ^= |=
15. , 逗号运算符号

数组和字符串

数组:把具有相同类型的若干变量按有序形式组织起来。

数组名是一个常量,是一个地址,指向数组首地址。

数组在内存中占用的大小:sizeof(数组名) = 数组类型*数组个数

数组的定义和初始化:{}

数组的个数必须是常量或常量表达式

数组必须预先知道大小,动态数组->开辟堆空间

二维数组

有行有列

数组的名为一个地址常量,二维数组的arr[0]也为地址常量

1
2
3
printf("%p\n",arr);
printf("%p\n",arr[0]);
printf("%p\n",&arr[0][0]); //所有输出都相等

多维数组: 嵌套定义

字符数组和字符串
1
2
3
4
5
char arr[5] = {'h','e','l','l','o'}; //字符数组
char arr[6] = {'h','e','l','l','o'}; //字符串,最后一个arr[5]自动赋值为0,相当于'\0'
char* arr = "hello";
char arr[] = "hello";
char arr[] = {"hello"};

字符串是字符数组的一个特例。

字符串结束标志为\0, 数字0等同于\0, 但不等同于’0’

字符数组与字符串的区别
  • C中没有字符串这种数据类型,可以通过char的数组来替代
  • 字符串一定是一个char的数组,但char的数组未必是字符串
  • 数字0(和字符’\0’等级)结尾的char数组就是一个字符串,但如果char数组没有以数字0结尾,那么就不是一个字符串,只是普通字符数组,所以字符串是一种特殊的char的数组。
1
char arr[100] = {110,111,112,32, 32,43};//数字对应ASCII码,可以打印出字符串

gets() 允许输入的字符串含有空格,scanf不允许含有空格

scanf("%[\^\n]",ch);//接收非\n的所有内容(通过正则表达式来做约束)

由于scanf和gets无法知道字符串s大小,必须遇到换行符或读到文件结尾为止才接收输入,因此容易导致字符数组越界(缓冲区溢出)的情况。

char* fgets(char *s, int size, FILE * stream)从stram指定的文件内读入字符,保存到s所指定的内存空间,直到出现换行字符,读到文件结尾或是已读了size-1个字符为止,最后会自动加上字符\0作为结束标志。

如果是从键盘输入,stream为stdin

1
2
3
puts():标准输出字符串,在输出完成后自动输出一个\n
fputf()
strlen() 计算指定字符串的长度,不包含字符串结束符\0

类型转换

不同类型数据之间进行混合运算时必然涉及到类型的转换问题

转换的两种方法:

  • 自动转换(隐式转换):遵循一定的规则,由编译系统自动完成
  • 强制类型转换:把表达式的运算结果强制转换成所需的数据类型

类型转换的原则:占用内存字节数少(值域小)的类型向占用内存字节数多(值域大)的类型转换,以保证精度不降低。

强制类型转换运算符,不会四舍五入

程序流程结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (适合多区间,带嵌套)

if else 条件嵌套

if eles if else

switch case break default(不适合多区间,执行效率高)

三目运算符: ?:

表达式1?表达式2:表达式3 如果表达式1为真,结果为表达式2,为假则表达式结果为3

while

do while

跳转语句

break 在switch条件语句和循环语句中都可以使用break语句

  • 出现在switch条件语句中时,作用是终止某个case并跳出switch结构
  • 当出现在循环语句中,作用是跳出当前内循环语句,执行后面的程序
  • 当出现在嵌套循环语句中,跳出最近的内循环语句,执行后面的程序

continue:在循环语句中,如果希望立即终止本次循环,并执行下一次循环,此时需要使用continue语句。

goto语句(无条件跳转,尽量少用)

1
2
3
goto FLAG;
xxxx
FLAG;

函数

函数分类

系统函数和用户定义的函数

  • 系统函数,即库函数:由编译系统提供,用户中不必自己定义,可以直接使用
  • 用户定义函数,用于解决用户的专门需要

调用函数的要素:

  • 头文件
  • 函数名字,必须和声明的名字一样
  • 功能
  • 参数
  • 返回值

函数定义和使用

返回值类型 函数名 (参数列表)

1
2
3
4
{
代码体;// 函数功能实现的过程
return;
}
  • 在不同函数中函数中的变量名可以重名,因为作用域不同。

  • 在函数调用过程中传递的参数为实参(有具体的值)。

  • 函数定义中参数称为形式参数。

  • 在函数调用过程中,将实参传递给形参。

  • 在函数调用结束,函数会在内存中销毁。

在定义函数时指定的形参,在未出现函数调用时,它们并不占用内存中的存储单元,因此称为形式参数或者形参,表示它们并不是实际存在的数据,所以,形参中的变量不能赋值

如果函数返回的类型和return语句中表达式的值不一致,则以函数返回类型为准,即函数返回类型决定返回值的类型。对数值型数据,可以自动进行类型转换。

注意:如果函数返回的类型和return语句中表达式的值不一致,而它又无法自动进行类型转换,程序则会报错。

实参可以常量、变量或表达式,无论实参数是何类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。所以,这里的变量是在圆括号外面定义好的、赋好值的变量。

void类型不可以直接定义数据,void类型可以作为函数的返回值类型,表示没有返回值。

函数声明

  • 函数声明:如果用户自己定义的函数,而该函数与调用它的函数(即主调函数)不在同一文件中,或者函数定义的位置在主调函数之后,则必须在调用此函数之前对被调用的函数做声明。

    • 所谓函数声明,就是在函数尚在未定义的情况下,事先将该函数的有关信息通知编译系统,相当于告诉编译器,函数在后面定义,以便编译器能够正常运行。
    • 注意:一个函数只能被定义一次,但可以声明多次。
  • 函数定义

声明和定义的区别

  • 声明变量不需要建立存储空间,如:extern int a;
  • 定义变量需要建立存储空间,如: int b;

从广义的角度来说声明中包含着定义,即定义是声明的一个特例,所以并非所有的声明都是定义。

  • int b; //即是声明,同时又是定义
  • 对于extern b 来说只是声明不是定义

一般情况下,把建立存储空间的声明称为“定义”, 而把不需要建立存储空间的声明称为“声明”

主函数和exit函数

在main函数中调用了exit和return结果是一样的,但在子函数中调用return只是代表子函数终止了,在子函数中使用exit,那么程序终止。

多文件编程

  • 函数功能实现放在其他.c文件中

  • 函数声明放到.h文件中

  • .h文件头部 # program once //防止头文件重复包含

头文件一般用于

  • 全局变量的定义
  • 函数的声明

  • 导入自己定义的头文件,用“xxx.h”

  • 一个相同名字的头文件对应一个相同名字的源文件

为了避免同一个文件被include多次,c/c++中有两种方式:

1
2
3
4
5
#ifndef //一般定义的方式为
#ifndef \__SOMEFILE_H__
#define \__SOMEFILE_H__
#endif
#pragma once //只能用于windows中

函数的三种参数传递方式

  • 传入参数:

    • 指针作为函数参数
    • 同时有const关键字修饰
    • 指针指向有效区域, 在函数内部做读操作
  • 传出参数:

    • 指针作为函数参数
    • 在函数调用前, 指针指向的空间可以无意义, 但必须有效
    • 在函数内部做写操作
    • 函数调用结束后充当函数返回值
  • 传入传出参数:

    • 指针作为函数参数
    • 在函数调用前, 指针指向的空间有实际意义
    • 在函数内部, 先做读操作, 再做写操作
    • 函数调用结束后, 充当函数返回值

指针

内存是沟通CPU和硬盘的桥梁

  • 暂存放CPU中的运算数据
  • 暂存与硬盘等外部存储器交换的数据

物理存储器和存储地址空间

物理存储器为实际存在的具体存储器芯片。

存储地址空间:对存储器编码的范围。软件中常说的内存含义。

  • 编码:对每个物理存储单元(一个字节)分配一个号码
  • 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写

内存地址

  • 将内存抽象为一个很大的一维字符数组
  • 编码就是对内存的每一个字节分配一个32位或64位的编号(与处理器的位数有关)
  • 内存编号称为内存地址。内存中的每一个数据都会分配相应的地址。
  • char:占一个字节分配一个地址
  • int:占四个字节分配四个地址
  • float, struct,函数,数组等

小端对齐,大端对齐

int* 为一个指针

地址也是一种特殊的数据类型,故存放地址的指针定义需要指明这一点,也就是二级指针的应用。

所有的指针类型存储的都是内存地址,内存地址都是一个无符号十六进制整型数**

&是取地址符号,是升维度的

*是取值符号,是降维度的

1
2
3
4
5
int main()
{
char ch = 97;
int* p = &ch; //指针类型不匹配,后面通过指针访问和修改数据都将报错
}

在定义指针类型的时候,一定要和变量的类型对应上。

野指针和空指针

野指针:指针变量指向一个未知的空间

1
int *p = 100;//野指针,程序中允许存在野指针

操作野指针对应的内存空间可能报错.

指针变量也是变量,是变量就可以任意赋值,不要越界即可(32位为4字节,64位为8字节),但是,任意数值赋值给指针变量没有意义,因为这样的指针就变成了野指针,此指针指向的区域是未知的(操作系统不允许操作此指针指向的内存区域).所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。

但是,野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用),c语言中可以把NULL赋值给此指针,这样就标志此指针为空指针,没有在任何指向。

操作系统将0-255的地址作为系统占用,不允许访问操作。

1
2
int *p = NULL;
#define NULL ((void*)0) //NULL为一个值为0的宏常量,内存地址为0的空间

操作空指针对应的空间一定会报错

空指针可以用作条件判断: if (p==NULL)

万能指针void *

void 指针可以*指向任意变量的内存空间

万能指针可以接收任意类型变量的内存地址

在通过万能指针修改变量的值时,需要找到变量对应的指针类型

const修饰的指针变量

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main()
{
const int a = 10;

int *p = &a;
*p = 100; //指针间接修改常量的值
printf("%d\n",a); //a = 100
}

const修饰指针:

  • const 修饰指针类型:const int* p (p为一个指向int的指针,)可以修改指针变量的值,不可以修改指针指向内存空间的值
  • const修饰指针变量:int * const p (p为const类型的指针,是一个常量),可以修改指针指向内存空间的值,不可以修改指针变量的值。

const int const p //const修饰指针类型,修饰指针变量,*只读指针

指针和数组

数组名是数组的首元素地址,但它是一个常量。

指针类型变量+1,等同于内存地址 + sizeof(type)

两个指针相减,得到的结果是两个指针的偏移量(步长)

所有的指针类型相减结果都是int类型。

数组作为函数参数会退化为指针,丢失了数组的精度

1
2
3
4
void my_strcpy(char *dest, char*ch)
{
while(*dest++ = *ch++);
}
1
2
int *p = &array[5];
p[-2]; //*(p-2),想当于 p[3]

指针操作数组时下标允许是负数

指针可以比较大小,逻辑运算

指针数组

  • 指针数组,是一个数组,数组的每个元素都是指针类型

  • 指针数组里面元素存储的是指针

  • 指针数组是一个特殊的二维数组模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main()
{
int a[] = {1,2,3};
int b[] = {4,5,6};
int c[] = {7,8,9};

//指针数组是一个特殊的二维数组模型
int* arr[] = {a,b,c};

//arr和&arr[0]是指针数组的首地址
//指针数组对应二级指针
printf("%p\n",arr);
printf("%p\n",&arr[0]);printf("%p\n",a);
printf("%p\n",a);

int **p = arr;
}

多级指针

二级指针就是指向一个一级指针变量地址的指针。

指针数组和二级指针建立关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>

int main()
{
int a[] = {1,2,3};
int b[] = {4,5,6};
int c[] = {7,8,9};

//指针是一个特殊的二维数组模型
int *arr[] = {a,b,c};

//指针数组和二维指针建立关系
int **p = arr;

printf("%d\n", **p);
//二级指针加偏移量,相当于跳过了一个一维数组大小
//一级指针加偏移量,相当于跳过一个元素
printf("%d\n",**(p+1));
printf("%d\n", *(*(p+1)+1)); //arr[1][1]

for(int i=0; i<3; i++)
{
for(int j = 0; j<3; j++)
{
printf("%d ", p[i][j]);
printf("%d ",*(p[i]+j));
printf("%d ",*(*(p+i)+j));
printf("\n");
}
}
}

值传递和地址传递

  • 值传递:形参不影响实参的值

  • 地址传递:形参可以改变实参的值

  • 数组名做函数参数,函数的形参会退化为指针。通过函数传递数组,一般都要给定数组的长度。

注意字符串和字符数组的区别

字符串去空格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>

void remove_space01(char* ch)
{
char str[100]={0};
char * p = str;

int i = 0;
int j = 0;
while(ch[i]!= '\0')
{
if(ch[i] != ' ')
{
str[j] = ch[i];
j++;
}
i++;
}
printf("%s\n",str);

while(*ch++ = *p++);
}

void remove_space(char* ch)
{
char* ftemp = ch;//遍历数组
char* rtemp = ch; //记录空格

while(*ftemp)
{
if(*ftemp != ' ')
{
*rtemp = *ftemp;
rtemp++;
}
ftemp++;
}
*rtemp = 0;

}

int main()
{
char test[] = " he l l o";
remove_space(test);
printf("%s\n",test);
}

内存管理

作用域

  • 代码块作用域({}之间的一段代码)
  • 函数作用域
  • 文件作用域

局部变量:在函数内部定义的变量,使用auto修饰,生命周期:从创建到函数结束

全局变量:在函数体外定义存放在数据区,可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,须用extern声明。

  • 全局变量的声明周期和程序运行周期一样
  • 不同文件的全局变量不可重名
  • 全局变量可以和局部变量重名,使用最近的一个

内存布局

在没有运行程序前,也就是程序没有加载到内存之前,可执行程序内部已经分好了3段信息,分别为代码区(text),数据区(data)和未初始化数据区(bss)3个部分。

静态(static)局部变量

  • static局部变量的作用域也是在定义的函数内有效,在数据区存储
  • static局部变量的生命周期和程序运行周期一样,同时static局部变量的值只初始化一次,但可以赋值多次
  • static局部变量若未赋以初值,则由系统自动赋值:数值型变量自动赋初值0,字符型变量赋空字符

静态全局变量

可以在本文件中使用,不可以在其他文件中使用

生命周期:数据区保存,从程序开始到程序结束

变量类型 作用域 生命周期 存储位置
局部变量 函数内部 从局部变量创建到函数结束 栈区
全局变量 项目中所有文件 从程序创建到程序销毁 数据区
静态局部变量 函数内部 从程序创建到程序销毁 数据区
静态全局变量 定义所在的文件 从程序创建到程序销毁 数据区

未初始化数据(根据编译器可能不同):

局部变量未初始化,值为乱码

未初始化的全局变量,值为0

  • 全局初始化数据区/静态数据区(data段)
    • 该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如:字符串常量)
  • 未初始化数据区(bss区)
    • 存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为0或者空(NULL)
  • 程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。

全局函数和静态函数

在c中函数默认为全局的,使用关键字static可以将函数声明为静态,函数定义为static就意味着这个函数只能在定义这个函数的文件中使用,在其他文件中不能使用,即使在其他文件中声明也没用。

对于不同文件中的static函数名可以相同

全局函数的名称是作用域中唯一的(c++中可以多态)

函数可以调用自己,称为递归调用,但一定要有出口

静态函数可以和全局函数重名,但作用域需要根据具体情况定

函数类型 作用域 生命周期 存储位置
全局函数 项目中的所有文件 从程序创建到程序销毁 平时在代码区(唤醒后存在栈区)
静态函数 定义所在文件中 从程序创建到程序销毁 代码区

注意:

  • 允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,互不干扰
  • 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>

//安全的常量,存储区域为数据区常量区
const int abc = 123;

//未初始化全局变量
int a1;

//初始化全局变量
int b1 = 10;

//未初始化静态全局变量
static int c1;

//初始化静态全局变量
static int d1 = 10;


int main()
{
int e1 = 10;

//未初始化局部静态变量
static int f1;

//初始化局部静态变量
static int h1 = 10;

//字符串常量
char* p = "hello world";
//数组
int arr[] = {1,2,3,4};
//指针
int* pp = arr;

printf("未初始化全局变量:%p\n",&a1);
printf("初始化全局变量:%p\n",&b1);
printf("未初始化静态全局变量:%p\n",&c1);
printf("初始化全局静态变量:%p\n",&d1);

printf("局部变量:%p\n",&e1);
printf("未初始化局部静态变量%p\n",&f1);
printf("初始化局部静态变量%p\n",&h1);
printf("字符串常量%p\n",&p);
printf("数组%p\n",arr);
printf("指针变量%p\n",pp);
printf("指针地址%p\n",&pp);
}

未初始化全局变量:0x601058
初始化全局变量:0x601040
未初始化静态全局变量:0x601050
初始化全局静态变量:0x601044
局部变量:0x7fffee32d54c
未初始化局部静态变量0x601054
初始化局部静态变量0x601048
字符串常量0x7fffee32d550
数组0x7fffee32d560
指针变量0x7fffee32d560
指针地址0x7fffee32d558

const修饰的局部常量是不安全的,const修饰的全局常量是安全的

内存模型

  • 代码区:程序执行二进制码(程序指令),特点:(共享:另外的执行程序可以调用它,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。只读:防止程序意外修改了它的指令。代码区还规划了局部变量的相关信息)
  • 数据区:
    • 初始化数据区(data段):包含了在程序中明确被初始化的全局变量,已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如:字符串常量)
    • 未初始化数据区(bss段):存入全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为0或者空NULL
    • 常量区
  • 栈区:系统为每一个应用程序分配一个临时的空间(局部变量,局部信息,函数参数,数组),栈区大小为:1M,在windowns中可以扩展到10M,在linux中可以扩展到16M
  • 堆区:存储大数据,图片,音频文件。
    • 手动开辟 malloc
    • 手动释放 free

栈区内存占用从高地址到低地址,数组的[0]从低地址开始。

两个连续的变量保存会存在一定的地址空缺是为了放置直接根据一个变量的地址推到下一个变量的地址。

栈区:先进后出,后进先出

  • 栈区(stack):栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值,返回值,局部变量等。在程序运行过程中实时加载和释放。因此:局部变量的生存周期为申请到释放该段栈空间。
  • 堆区(heap):堆是一个大容器,其容量要远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

堆空间开辟和释放

1
void* malloc(size_t size);

内存的动态存储区(堆区)分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型,分配的内存空间内容不确定,一般使用memset初始化。

1
2
int* p = (int*) malloc (sizeof(int) *1024);
void free(void* ptr);

释放ptr所指向的一块内存空间,ptr是一个任意类型的指针变量,指向被释放区域的首地址,对同一内存空间释放多次会出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>

int main()
{
//栈区大小
// int arr[820000*3] ={0};//段错误,核心转移

//开辟空间存储
int* p = (int*)malloc(sizeof(int)*1024);
printf("%p\n",p);
//使用堆空间
*p = 123;
printf("%d\n",*p);
//释放空间
free(p);

//p为野指针
printf("%p\n",p);
*p = 456;
printf("%d\n",*p);
return 0;
}

0x195b010
123
0x195b010
456

为了避免野指针的出现,一般将指针赋值为NULL.

开辟的空间使用指针或者数组的方式来进行操作

内存处理函数

1
2
3
#include <string.h>

void* memset(void* s,int c,size_t n);

将内存区域的前n个字节以参数c填入,返回值,s的首地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
int* p = (int*)malloc(sizeof(int)*10);

//重置内存空间的值
memset(p,'c',40);
for(int i=0; i<10;i++)
{
printf("%c\n",p[i]);
}

free(p);
return 0;
}
1
2
#include <string.h>
void* memcpy(void* dest, void* src, size_t n);

拷贝src所指的内存内容的前n个字节到dest所指的内存地址上。

注意:dest和src所指的内存空间不可重叠,可能会导致程序报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int *p = (int*)malloc(sizeof(int)*10);

memcpy(p, arr, sizeof(int)*10);
for(int i=0; i<10; i++)
{
printf("%d ",p[i]);
}
free(p);
return 0;
}

memcpy()与strcpy()的区别:

字符串拷贝遇到\0则自动结束,内存拷贝不会出现类似情况

内存拷贝:拷贝的内容和字节有关,和拷贝内容无关

如果拷贝的目标和源发生重叠,可能报错

1
2
3
4
memmove()//用法和memcpy一样,区别在于:dest和src所指的内存空间重叠时,memmove()仍然能处理,不过执行效率比memcpy低

int memcmp(const void* st, const void* s2, size_t n);//比较s1和s2所指向内存区域的前n个字节
//返回值:0,1,-1 等于,大于,小于

内存常见的问题

空指针允许多次释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void fun(int* p)
{
//传值
p = (int*)malloc(sizeof(int)*10);
}

void fun1(int** p)
{
//传地址
*p = (int*)malloc(sizeof(int)*10);
printf("形参%p\n",*p);
}

int* fun2()
{
//返回地址
int* p = malloc(sizeof(int) *10);
return p;
}

int main()
{
//数组下标越界
// char* p = (char*)malloc(sizeof(char)*10);
// strcpy(p,"hello worldS");
// printf("%s\n",p);
// free(p);

//野指针
// int* p = (int*)malloc(0);
// printf("%p\n",p);
// *p = 100;
// printf("%d\n",*p);
// free(p); //windows下程序挂,linux似乎做了优化?
// return 0;

//多次释放空间
// int* p = malloc(sizeof(int)*10);
// free(p);
// //解决办法
// p = NULL; //空指针允许多次释放
// free(p);//放弃,核心已转储

int* p = NULL;
// fun(p); //形参和实参一致,都是值传递

fun1(&p); //地址传递
for(int i=0; i<10;i++)
{
p[i] = i;
printf("%d ",p[i]);
}
printf("\n");
free(p);

int* p1 = fun2();
for(int i=0; i<10;i++)
{
p1[i] = i;
printf("%d ",p1[i]);
}
free(p1);
return 0;
}

二级指针对应的堆空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <stdlib.h>

int main()
{
//开辟二级指针对应的堆空间
int** p = (int**)malloc(sizeof(int*) *5);
for(int i =0; i<5; i++)
{
//开辟一级指针对应的堆空间
p[i] = (int*)malloc(sizeof(int) * 3);
}
for(int i=0; i<5; i++)
{
for(int j=0; j<3; j++)
{
p[i][j] = i+j;
printf("%d ",p[i][j]);
}
}
//free
for(int i=0;i<5;i++)
{
free(p[i]);
}
free(p);

return 0;
}

结构体

数组:描述一组具有相同类型数据的有序集合,用于处理大量相同类型的数据运算。

有时需要将不同类型的数据组合成一个有机的整体。显然单独定义变量会比较繁琐,数据不方便管理

定义结构体变量的方式:

  • 先声明结构体类型再定义变量名struct stu{成员列表}; struct stu Mike;
  • 在声明类型的同时定义变量。struct stu{成员列表}Mike,Bod;
  • 直接定义结构体类型变量(无类型名).struct {成员变量} Mike,Bob;

结构体类型和结构体变量关系:

  • 结构体类型:指定了一个结构体类型,相当于一个模型,但其中并无具体数据,系统对之也不分配实际内存单元
  • 结构体变量:系统根据结构体类型(内部成员状况)为止分配空间。

结构体数组:

结构体成员需要偏移对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>

struct Student{
char name[21];
int age;
char sex;
int score[3];
};

int main()
{
struct Student stu[3]=
{
{"黄x航",22,'M',89,90,89},
{"码x东",18,'F',89,54,65},
{"司正x",32,'M',89,98,98}

};

for(int i=0; i<3-1;i++)
{
for(int j=0; j<3-1-i;j++)
{
if(stu[j].age >stu[j+1].age)
{
struct Student temp = stu[j];
stu[j] = stu[j+1];
stu[j+1] = temp;
}
}
}
}

结构体赋值

用=可以进行复制

深拷贝和浅拷贝

如果结构体内部有指针指向堆内存,那么就不能使用编译器默认的赋值行为,应该手动控制赋值过程。

结构体指针

若结构体中包含有指针类型的成员数据,则在给结构体变量赋值的时候需要考虑指针赋值(是开辟新空间或是赋常量的值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

struct stu{
char name[21];
int age;
int scores[3];
char addr[51];
};

int main()
{
//结构体指针
struct stu ss = {"test",30, 100,100,100,"边境"};
struct stu* p = &ss;
printf("%s\n",(*p).name);
printf("%s\n",p->addr);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <stdlib.h>

typedef struct student ss;
struct student
{
char* name;
int age;
int* scores;
char* addr;
};


int main()
{
ss* p = (ss*)malloc(sizeof(ss)*3);
for(int i=0; i<3; i++)
{
p[i].name = (char*)malloc(sizeof(char)*21);
p[i].scores = (int*)malloc(sizeof(int)*3);
p[i].addr = (char*)malloc(sizeof(char)*21);
}

for(int i=0; i<3; i++)
{
scanf("%s%d%d%d%d%s",p[i].name,&p[i].age,
&p[i].scores[0],&p[i].scores[1],&p[i].scores[2],p[i].addr);
}

for(int i=0; i<3; i++)
{
printf("%s ",p[i].name);
printf("%d ",p[i].age);
printf("%d ",p[i].scores[0]);
printf("%d ",(p+i)->scores[1]);
printf("%d ",(p+i)->scores[2]);
printf("%s\n",(p+i)->addr);
}

//释放存储空间
for(int i=0;i<3;i++)
{
free(p[i].name);
free(p[i].scores);
free(p[i].addr);
}
free(p);
return 0;
}

结构体做函数参数

  • 结构体普通变量做函数参数

  • 结构体指针变量做函数参数

  • 结构体数组名做函数参数

  • const修饰结构体指针形参变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//普通变量
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct Student ss;
struct Student{
char name[21];
int age;
int score;
char addr[51];
};

void fun01(ss stu)
{
strcpy(stu.name,"lujunyi");
printf("%s\n",stu.name);
}

int main()
{
ss stu = {"宋江",50,101,"水船"};
fun01(stu);
printf("%s\n",stu.name); //值传递,不改变
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//结构体指针
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct student ss;

struct student{
char name[21];
int age;
int score;
char addr[21];
};

void fun02(ss* p)
{
strcpy(p->name, "公孙胜");
printf("%s\n",p->name);
}

int main()
{
//结构体指针作为函数参数
ss stu = {"吴用",50, 101,"水泊梁山"};

fun02(&stu);
printf("%s\n",stu.name);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//结构体数组做函数参数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct student ss;

struct student{
char name[21];
int age;
int score;
char addr[21];
};

//数组作为函数参数退化为指针,丢失元素精度,需要传递个数
void BubbleSort(ss stu[],int len)
{
for(int i = 0; i<len-1; i++)
{
for(int j = 0; j<len-i-1; j++)
{
if(stu[j].age >stu[j+1].age)
{
ss temp = stu[j];
stu[j] = stu[j+1];
stu[j+1] = temp;
}
}
}
}

int main()
{
ss stu[3] =
{
{"鲁智深",30,78,"五台山"},
{"呼吁",29,78,"三台山"},
{"呈共",31,87,"滇池"}
};

BubbleSort(stu, 3);

for(int i=0;i<3;i++)
{
printf("%s\t%d\t%d\t%s\n",stu[i].name,stu[i].age,stu[i].score,stu[i].addr);
}
return 0;
}