C/C++知识总结
第一章 C语言基础知识
第二章 C语言高级编程
文章目录
前言
C语言标准变化:K&R C标准—>ANSI C标准(C89标准)—>C90标准—>C94标准—>C95标准—>C99标准。C99标准支持了不定长的数组,数组的长度可以用变量。
一、内存分区
1.1 数据类型
- 数据类型别名typedef,常与结构体结合使用。
- void定义变量没有任何意义,常用于对函数返回的限定,对函数参数的限定。而void* 无类型指针可以指向任何类型的数据。
- sizeof是c语言中的一个操作符,类似于++、–等等。返回的占用空间大小是为这个变量开辟的大小,而不只是它用到的空间。返回的数据结果类型是unsigned int。当数组名作为函数参数时,在函数内部,数组名退化成指针,所以不会返回数组的大小。
1.2 程序的分区模型
- 运行前:程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss) 3 个部分。总体来讲说,程序源代码被编译之后主要分成两种段:程序指令(代码区)和程序数据(数据区)。代码段属于程序指令,而数据域段和.bss段属于程序数据。分开的原因是指令区域对程序来讲说是只读且运行多个同样的程序的时候,程序执行的指令都是一样,保存一份程序的指令可以节省大量的内存。
- 运行可执行程序,操作系统把物理硬盘程序load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
- 栈区:由系统进行内存的管理。主要存放函数的参数以及局部变量。在函数完成执行,系统自行释放栈区内存,不需要用户管理。
- 堆区:由编程人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间。使用malloc或者new进行堆的申请。
- 堆分配内存API:
calloc,realloc
- 全局/静态区:全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量、静态变量和常量。静态存储区内的变量若不显示初始化,则编译器会自动以默认的方式进行初始化。全局静态存储区内的常量分为常变量和字符串常量,一经初始化,不可修改。局部常变量存放于栈,实际可间接通过指针或者引用进行修改,而全局常变量存放于静态常量区则不可以间接修改。
- ANSI C并没有规定编译器的实现者对字符串的处理,所以尽量不要去修改字符串常量。字符串常量地址是否相同,不同编译器不同规定,VS中字符串常量地址同文件和不同文件都相同。
1.3 函数的调用流程
- 一个函数调用过程所需要的信息一般包括以下几个方面:函数的返回地址(不是函数返回值的地址),函数的参数,临时变量,保存的上下文:包括在函数调用前后需要保持不变的寄存器。
- 函数的调用方和被调用方对于函数是如何调用的必须有一个明确的约定,只有双方都遵循同样的约定,函数才能够被正确的调用,这样的约定被称为”调用惯例“。调用惯例一般包含函数参数的传递顺序和方式,栈的维护方式,对函数本身的名字进行修饰。c语言里,存在着多个调用惯例,而默认的是cdecl。任何一个没有显示指定调用惯例的函数都是默认是cdecl惯例。
- 函数变量传递分析:main函数在栈区和堆区开辟的内存,子函数均可以使用。子函数在栈区和堆区开辟的内存,其余子函数可以使用。子函数在全局区开辟的内存,其余子函数和main函数可以使用。
- 栈的生长方向:在经典的操作系统中,栈总是向下增长的。压栈的操作使得栈顶的地址减小,弹出操作使得栈顶地址增大。所以栈底对应高地址,栈顶对应低地址。
- 内存的生长方向:高位字节数据存放到高地址,低位字节数据存放到低地址,称为小端对齐方式。
二、指针强化
- 空指针:不允许向NULL和非法地址拷贝内存。野指针:指针变量未初始化,指针释放后未置空,指针操作超越变量作用域。
*
放在等号的左边赋值(给内存赋值,写内存),*
放在等号的右边取值(从内存中取值,读内存)。*p = 20
在左边当左值,必须确保内存可写。int b = *p
放右面,从内存中读值。- 指针所指向的内存空间决定了指针的步长。指针的步长指的是,当指针+1时候,移动多少字节单位。
- 指针的间接赋值:用1级指针形参,去间接修改了0级指针(实参)的值。用2级指针形参,去间接修改了1级指针(实参)的值。用n级指针形参,去间接修改了n-1级指针(实参)的值。
- 指针做函数参数,具备输入和输出特性:输入:主调函数分配内存,输出:被调用函数分配内存。
- 字符串指针做函数参数:
- sizeof计算数组大小,数组包含’\0’字符,strlen计算字符串的长度,到’\0’结束。
- 字符串拷贝实现,字符串反转实现。
- 字符串格式化输出和输入:
sprintf,sscanf
。sscanf
格式匹配,分割字符串。
- 一级指针易错点:越界,指针叠加会不断改变指针指向,返回局部变量地址,同一块内存释放多次(不可以释放野指针)。
- const修饰指针:放在
*
号左侧,修饰指针指向的内存空间不能修改,但可修改指针的指向。放在*
号的右侧, 修饰指针的指向不能修改,但是可修改指针指向的内存空间。向右修饰。 - 二级指针:二级指针做参数的输出特性是指由被调函数分配内存,主调函数使用。二级指针做形参输入特性是指由主调函数分配内存,被调用函数使用。二级指针堆区开辟空间,释放要对应开辟顺序。
三、位运算
- 按位取反
~
, 按位与&
,按位或|
,按位异或^
。 - 按位与对于每个位,只有两个操作数的对应位都是1时结果才为1。按位或对于每个位,如果其中任意操作数中对应的位为1,那么结果位就为1。按位异或对于每个位,如果操作数中的对应位有一个是1(但不是都是1),那么结果是1。如果都是0或者都是1,则结果位0。
- 异或可以实现交换两个数而不需要临时变量。
a ^ b = c; a ^ c = b; b ^ c = a
(num & 1) == 1
可以判断num是奇数还是偶数,按位或能让数字中指定位置变为1,按位异或可以转置位。- 移位运算符:
<< >>
。左移运算符<<将其左侧操作数的值的每位向左移动,移动的位数由其右侧操作数指定。空出来的位用0填充,并且丢弃移出左侧操作数末端的位。左移一位相当于原值*2。右移运算符>>将其左侧的操作数的值每位向右移动,移动的位数由其右侧的操作数指定。丢弃移出左侧操作数有段的位。对于unsigned类型,使用0填充左端空出的位。对于有符号类型,结果依赖于机器。空出的位可能用0填充,或者使用符号(最左端)位的副本填充。 - 移位运算符能够提供快捷、高效(依赖于硬件)对2的幂的乘法和除法。
四、多维数组
- 一维数组名的值是一个指针常量,也就是数组第一个元素的地址。它的类型取决于数组元素的类型。
- 一维数组名在什么情况下不能作为指针常量呢?当数组名作为sizeof 操作符的操作数的时候,此时sizeof返回的是整个数组的长度,而不是指针数组指针的长度。当数组名作为&操作符的操作数的时候,此时返回的是一个指向数组的指针,而不是指向某个数组元素的指针常量。
- 使用指针操作一维数组时,索引下标可以为负数。
- 一维数组和指针:声明一个数组时,编译器根据声明所指定的元素数量为数组分配内存空间,然后再创建数组名,指向这段空间的起始位置。声明一个指针变量的时候,编译器只为指针本身分配内存空间,并不为任何整型值分配内存空间,指针并未初始化指向任何现有的内存空间。
- 作为函数参数的一维数组名:数组名其实就是一个指向数组第1个元素的指针,所以很明白此时传递给函数的是一份指针的拷贝。所以函数的形参实际上是一个指针。
- 一维数组名的值是一个指针常量,它的类型是“指向元素类型的指针”,它指向数组的第1个元素。多维数组也是同理,多维数组的数组名也是指向第一个元素,只不过第一个元素是一个数组。
int arr[3][10]
理解为一个一维数组,包含了3个元素,只是每个元素恰好是包含了10个元素的数组。arr就表示指向它的第1个元素的指针,所以arr是一个指向了包含了10个整型元素的数组的指针。- 指向数组的指针(数组指针):是指针,指向数组的指针,而数组的类型由元素类型和数组大小共同决例如 int arr[5] 的类型为 int[5],其数组指针定义为
int (*parr)[5]=&arr
。 - 指针数组(元素为指针):栈区指针数组做函数参数,退化为指针。堆区指针数组,注意分配和释放的内存顺序。
- 二维数组的3种形式参数
int arr[3][3]; int arr[][3]; int(*arr)[3]
五、结构体
- 结构体类型的定义,结构体变量的定义、初始化和成员的使用。
- 结构体赋值:系统提供的赋值操作是浅拷贝简单值拷贝,逐字节拷贝。如果结构体中有属性创建在堆区,就会出现问题,在释放期间,一段内存重复释放,一段内存泄露。解决方案:自己手动去做赋值操作,提供深拷贝。
- 结构体数组:栈区分配和堆区分配。
- 结构体嵌套一级指针:用2级指针形参,去间接修改了1级指针(实参)的值。
- 结构体嵌套二级指针:用3级指针形参,去间接修改了2级指针(实参)的值。
- 结构体成员偏移量:
offsetof
,或者用指针减法实现。 - **结构体字节对齐:**访问特定类型的变量只能在特定的地址访问,这就需要各个变量在空间上按一定的规则排列, 而不是简单地顺序排列,这就是内存对齐。内存对齐是操作系统为了提高访问内存的策略。操作系统在访问内存的时候,每次读取一定长度(这个长度是操作系统默认的对齐数,或者默认对齐数的整数倍)。如果没有对齐,为了访问一个变量可能产生二次访问。
- 结构体内存对齐原则:
- 结构体成员对齐规则。第一个结构体成员应该放在offset为0的地方,以后每个结构体成员应该放在offset为min(当前成员的大小,对齐模数比)整数倍的地方开始。
- 结构体总的大小,也就是sizeof 的结果,必须是min(结构体内部最大成员,对齐模数比)的整数倍,不足要补齐。
- 结构体嵌套结构体时候,子结构体放在该结构体中最大类型和对齐模数比的整数倍上即可。
#pragma pack(show)
:查看当前对齐模数;#pragma pack(1)
:修改对齐模数,可改成2的N次方。- 结构体可以嵌套另外一个结构体的任何类型变量。结构体嵌套本结构体普通变量(不可以),本结构体的类型大小无法确定。结构体嵌套本结构体指针变量(可以), 指针变量的空间能确定。
六、文件操作
- 程序中,经常看到的文本方式打开文件和二进制方式打开文件仅仅体现在换行符的处理上。在widows下,文件的换行符是\r\n,而在Linux下换行符则是\n。
- 文件I/O的一般概况:通过fopen函数打开,根据需要对文件进行读写,最后调用fclose函数关闭。标准I/O更为简单,不需要打开或者关闭。
- I/O函数以三种基本的形式处理数据:单个字符、文本行和二进制数据。对于每种形式都有一组特定的函数对它们进行处理。
- 文件指针可以理解为代指打开的文件。这个指针的类型为FILE类型。该类型定义在stdio.h头文件中。通过文件指针,我们就可以对文件进行各种操作。
- 文件缓冲区:系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量) 。
- 文件读写函数回顾。
- 文件读取结尾判断:文本文件读取是否结束,判断返回值是否是EOF(fgetc),或者NULL(fgets)。二进制文件的读取结束判断,判断返回值是否小于实际要读的数(fread)。注意feof函数作用:当文件读取结束时,判断文件读取结束的原因是否是遇到了文件结束结束标志。feof()的工作原理是,站在光标所在位置,向后看还有没有字符。如果有,返回0;如果没有,返回非0。它并不会读取相关信息,只是查看光标后是否还有内容。
- 文件读写案例。
七、链表
- 链表概念、链表的结点、链表的分类。
- 静态链表和动态链表:所有结点都是在程序中定义的,不是临时开辟的,也不能用完后释放,这种链表称为“静态链表”。动态链表,是指在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟结点和输入各结点数据,并建立起前后相链的关系。
- 带头和不带头链表:带头链表:固定一个节点作为头节点(数据域不保存有效数据),起一个标志位的作用,以后不管链表节点如果改变,此头节点固定不变。
- 单向链表、双向链表、循环链表。
- 链表的基本操作:结构体定义节点类型,创建链表,遍历链表,插入节点,删除节点,返回节点个数,销毁链表,反转链表。
八、函数指针、回调函数和递归函数
- 通过什么来区分两个不同的函数?一个函数在编译时被分配一个入口地址,这个地址就称为函数的指针,函数名代表函数的入口地址。c语言中通过typedef为函数类型重命名:
typedef int f(int, int); // f 为函数类型
。我们可以用一个指针变量来存放这个入口地址,然后通过该指针变量调用函数。通过函数类型定义一个函数指针指向某一个具体函数,才能调用。 - 函数指针(指向函数的指针):先定义函数类型,根据类型定义指针变量;先定义函数指针类型,根据类型定义指针变量;直接定义函数指针变量。
int my_func(int a,int b)
{
printf("ret:%d\n", a + b);
return 0;
}
//1 先定义函数类型,根据类型定义指针变量
typedef int f(int, int);
f* p = my_func;
//2 先定义函数指针类型,根据类型定义指针变量
typedef int(*p)(int, int);
p p1 = my_func;
//3 直接定义函数指针变量
int(*f)(int, int) = my_func;
- 函数指针数组,每个元素都是函数指针。
- 函数指针做函数参数(回调函数):函数指针变量常见的用途之一是把指针作为参数传递到其他函数,指向函数的指针也可以作为参数,以实现函数地址的传递。
- 函数指针是指向函数的指针,指针函数是返回类型为指针的函数。
- 利用回调函数实现打印任意类型数据,提供能够打印任意类型数组函数,利用回调函数提供查找功能。
- 利用回调函数实现对任意类型的数组进行排序,排序规则利用选择排序,排序的顺序用户可以自己指定。
- 递归函数就是直接或间接调用自身的函数,通过运行时堆栈来支持递归函数的实现。
- 递归实现字符串反转,递归打印数字千百十个位数字。
九、预处理
- 预处理是在程序源代码被编译之前,由预处理器 对程序源代码进行的处理。这个过程并不对程序的源代码语法进行解析,但它会把源代码分割或处理成为特定的符号为下一步的编译做准备工作。
- 文件包含指令(#include):
#include <>
常用于包含库函数的头文件,#include ""
常用于包含自定义的头文件。 - 宏定义:使用户能以一个简单的名字代替一个长的字符串,在预编译时将宏名替换成字符串的过程称为“宏展开”。宏定义可以是常数、表达式等,也可以引用已定义的宏名,不是C语言,不在行末加分号,有效范围为从定义到本源文件结束。
- 宏函数:把一些短小而又频繁使用的函数写成宏函数,这是由于宏函数没有普通函数参数压栈、跳转、返回等的开销,可以提高程序的效率。定义宏函数时用括号括住每一个参数,并括住宏的整体定义。
- 条件编译:防止头文件被重复包含引用。特殊的宏:
__FILE__ ; __LINE__; __DATE__; __TIME__
。
十、库的封装和使用
- 库是已经写好的、成熟的、可复用的代码。每个程序都需要依赖很多底层库,不可能每个人的代码从零开始编写代码,因此库的存在具有非常重要的意义。
- windows下静态库创建和使用:右键项目->属性 ->常规->配置类型 ->静态库,编译生成新的解决方案,在Debug文件夹下会得到
xxx.lib
(对象文件库),将该.lib文件和相应头文件给用户,用户就可以使用该库里的函数了。或者使用编译语句#pragma comment(lib,"./xxx.lib")
。 - 静态库优缺点:静态库对函数库的链接是放在编译时期完成的,静态库在程序的链接阶段被复制到了程序中,和程序运行的时候没有关系。浪费空间和资源,所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。静态链接的方式对于计算机内存和磁盘空间浪费非常严重。静态链接对程序的更新、部署和发布也会带来很多麻烦。
- 要解决空间浪费和更新困难这两个问题,最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不是将他们静态的链接在一起。就是不对哪些组成程序的目标程序进行链接,等程序运行的时候才进行链接。也就是说,把整个链接过程推迟到了运行时再进行,这就是动态链接的基本思想。
- windows下动态库创建和使用:右键项目->属性 ->常规->配置类型 ->动态库 ,编译生成新的解决方案,在Debug文件夹下会得到
xxx.dll
(对象文件库),将该.dll文件、.lib文件和相应头文件给用户,用户就可以使用该库里的函数了。在程序中指定链接引用链接库 :#pragma comment(lib,"./xxx.lib")
。 - 动态库的lib文件和静态库的lib文件区别:对一个DLL文件来说,其引入库文件(.lib)包含该DLL导出的函数和变量的符号名,而.dll文件包含该DLL实际的函数和数据。在使用动态库的情况下,在编译链接可执行文件时,只需要链接该DLL的引入库文件,该DLL中的函数代码和数据并不复制到可执行文件,直到可执行程序运行时,才去加载所需的DLL,将该DLL映射到进程的地址空间中,然后访问DLL中导出的函数。
- 编译生成新的解决方案,注意生成的库文件在当前文件夹的Debug文件夹下,将静态库的.lib文件(或动态库的.dll和.lib文件)和相应头文件给用户,用户就可以使用该库里的函数了。
十一、面向接口编程案例
- 要求在企业信息系统框架中集成第三方厂商的游戏功能产品。软件设计要求:能够满足用户需求,完成的产品可以与用户完美对接。
- 要求:能支持多个厂商的游戏功能产品入围;能够实现第三方产品和用户产品的对接;系统整体框架不轻易发生改变
- 提示:抽象游戏中玩家结构体设计;框架接口设计包含初始化游戏,核心功能战斗,查看玩家信息,结束游戏;游戏厂商1入围,游戏厂商2入围;框架接口分文件编写。
总结
本文章涉及代码:https://github.com/wangchengyongnevergiveup/C2.git
笔记来源:黑马程序员C语言课程