C语言入门到入土
C语言本质就是一门计算机语言,是一门计算机能够看懂的语言,把你用C语言编写的程序交付与计算机读写,然后再按照你的命令去执行,达到相应的效果,这就是C语言存在的目的。
开始学C的第一个程序一般都是以下这个。
这就是c语言的初阶运用,里面包含了函数,数据,语法,字符,以及一些运算符。
学习任意一门语言都是从认识单词开始,学C也不例外 首先要认识它的数据类型。
在此之前,首先了解字节
字节即是平常说的“B”,其实是叫做byte,1byte包含8个比特(bit),1bit可存放一个二进制数字,也就是 0或1,然后把存放的0或者1转换成10进制或者字符。
以下是不同单位空间大小以及换算。
一般分为两大类: 数字和符号
数字 -int为整形,表示整数,如果有符号位的话,需要去除一个符号位,正数最大为:2的31次方-1 ,负数最小为:-2的31次方(这俩绝对值加起来就是2^32-1,int占有8个字节,总共32bit,最多就是32位数,,所能存储的数的范围是有负号的一半,无负号的一半,由于0占了一位,所以正数比负数少一个, 就是这么大:int -2147483648~2147483647
补码就是一种十转二的方式
以-7为例
-unsigned int(无符号整型),就是忽略掉了负数的情况(可以理解为带符号就是可存放负数。不带的话就可以存放负数和0),最大就是2^32-1。
其他的 long int,short int,long long int只是int’的变式,改变空间大小用的
另外 数字中还有浮点型的变量,也就是带有小数点的,这种即是float, double 等,double支持的小数点位更多一点。
符号 -也就是char类型,符号的原理是通过读取其中的数字再通过相应的标准将其转化为一个符号,这些标准都存在于ASCII码值对照表中。
--最多是2^8-1,可以发现这里面的数字可以表示表中的任意一个符号。至于什么时候这些数字要转变为符号,就是看你写什么样的程序了.
**需要特别注意的是,这两种数据本质上没有什么区别,可以给int数据一个符号 存入的就是这个符号对应的ASCII码,反之也可以给char赋值一个整型数据,代表对应的字符,当然 给char型数据一个超过了255的数是输出不了字符的 因为超过了范围,而且没有对应的字符
在上面那个程序中,printf本质就是一个函数,用来输出相应的值,
比如 %d就是对应着一个int数据
%c对应字符,这些都叫做格式字符,在输入和输出过程中都要用到。
举个例子
在此之后,进入变量与常量。
变量在c语言中规定,我们使用一个变量之前需要对他进行定义
格式: 变量类型 变量名;
也就是告诉编译器,我接下来要使用的叫这个名字变量是个什么类型,给这个变量提供一个存放数据的空间。 而且变量名的命名是有规定的
而关键字就是一些定义过了的特殊的“单词”,比如
也就是具有特殊含义的。
-常量就是在程序中不能改变其值的量,
常量也有很多类型
同时常量也可以通过宏定义来确定
宏定义就是在程序开始执行之前所做的事情
比如
这里面要注意的是 给%s提供的是一个地址。
#define就是一个宏定义标识,将后面所出现的宏名也就是这里面URL NAME
之类的替换为引号里的数据,并且只是单纯的替换。如果尝试改变这些宏名指代的数据,系统就会报错。
比如这个程序中 count就是一个变量名,
注释
就是//后面的话语不参与程序的读写,只是方便程序员后续理解
/*
。。。。。。。
*/ 是另一种注释方法,可以注释掉很多行。
算数符
运算符也分类 比如算术运算符,关系运算符,逻辑运算符,
算术运算符也就是加减乘除赋值之类的,输出具体的值,
在算数表达式中,可能会存在不同类型的运算,这时候可以对数据进行显式转换
就是由程序员来强制转换其格式
如果放着不管的话 ,计算机会自动转换成相同的数据,也叫隐式转换
关系运算符是判断两个数据之间的关系,从而输出0或者1,否定输出0,正确输出1
具体如下
特别的
条件运算符
逗号运算符
这里面提几个点 ==与=不一样,一个是判断 一个是操作
++a与a++不一样,前者先给自身赋值,再访问,后者相反。
数组
是用来存储同一类型的数据的组,类似于数学中的集合
注意 数组被定义(分配空间)之后就不能被修改其空间大小了
如果想要访问数组里面的元素的话,要按照如下方法
也就是说数组当中序数是从0开始的,访问元素的时候就可以改变这个元素的值或者说是把这个值赋给其他的变量或者是带入到表达式里面计算
二维数组
一般数组里面存放一行数据,而二维数组里面的数据分行和列
以下是二维数组的定义和初始化方法
而访问这个数组中的元素就要联想直角坐标系了,前一个中括号里面相当于是横坐标也就是行数,后一个相当于纵坐标也就是列数,这样就确定好了这个数据的位置
矩阵转置就是相当于再打印的时候将行变成列 列变成行,但是并没有改变数组的储存形式
比如我原本的二维数组是 1 ,2,3
4,5,6
把它打印成 1,4
2,5
3,6,就是矩阵转置了
具体操作如下例
这个代码打印的时候将行列位置替换掉,利用的是for语句嵌套循环的规律 ,这个在后面会说到。
虽说是二维数组,但是计算机中数据的存放始终都是线性的,就只是把一行数据分开成很多行了而已,包括三维数组 四维数组 都是如此
字符与字符串
字符串顾名思义就是一堆字符组成的,字符串字面量有时也称为字符串常量,是有双引号括起来的一个字符序列如“hello”“321,无论双引号内是否包含字符,包含多少个字符,都代表一个字符串字面量。字符串字面量不同于字符常量,比如”a”,是字符串字面量,‘a’是字符常量。
字符串的声明以及初始化方式
生命的话就是 type name【inch】;
而字符串的初始化方式比较多样,如下
尤其要注意,字符串相当于是一个字符数组,但最后一个元素一定是\0,但是这个元素不会被打印出来.
C语言处理字符串的时候是跟警察字符数组名找到第一个字符,然后开始往下读取,直到读取到\0。所以省略对数组长度声明的时候,必须自己添加一个\0
语句
If语句
是一个判断型语句,表达式(一般是关系或者逻辑表达式)里面输出0或者1,正确则为返回1,执行大括号里面的程序,如果在表达式里面写个1或者加入一个赋值为1的变量,那么就会执行,0则相反。
然后if只是单个选择,如果有多两路不同的选择的话 就要用到else,与if共生且并列
但是if可以单独存在 else不能单独存在。也就是说当if语句表达式是错误的话,就会执行else语句里面的内容
还有一种就是elseif,它与if并列,判断完if后就一个一个地判断else if里的表达式,符合哪个就执行哪个,都不符合的时候执行else里的语句
举个例子
悬挂else
在C语言中,编译器通常将else与上一个(最近的)if进行匹配,除非通过花括号来制定匹配关系。看下面一个例子
这两列代码作用是一样的,所以在使用语句块的时候最好养成用大括号的习惯,减少不必要的失误
了解了这个特性之后,我们就可以多条件选择了,也就是条件的嵌套,满足多个条件才能执行某程序。
Putchar
就是放入一个字符
putchar就是用来输出(显示到屏幕的)的。
putchar 的适用对象是字符数据。(从putchar名字末尾的 char 也可以看出。)
一个putchar只能输出一个字
putchar函数的基本格式为:putchar(c)
Getchar
getchar返回的是字符的ASCII码值(整数)。
getchar在读取结束或者失败的时候,会返回EOF。(EOF意思是end of file,本质上是-1)
读取方式:只能输入字符型,输入时遇到回车键才从缓冲区依次提取字符。
结束输入:以Enter结束输入(空格不结束),接受空格符。
舍弃回车符的方法:以Enter结束输入时,接受空格,会舍弃最后的回车符
程序执行到getchar()函数时,自动从输入缓冲区中去找字符,如果输入缓冲区中没有字符的话,那么就等待用户输入字符,此时用户输入的字符,被输入到输入缓冲区中,键盘输入字符的时候首先进入输入缓冲区,然后getchar()函数获得的字符是从输入缓冲区中提取的且每次只能提取一个字符,以下是本函数的基本用途。
#include<stdio.h>
int main()
{
char ch = getchar();//输入字符
putchar(ch);
return 0;
}
意思就是从键盘读入一个字符,然后输出到屏幕。我们输入A,输出就是A,输入B,输出就是B。
那么我们如果输出的是ABC的话输出的是A。
因为当我们从键盘输入字符‘A’,‘B’, 'C',回车后,字符被放入了输入缓冲区,这个时候getchar()会从缓冲区中读取我们刚才输入的字符,一次只读一个,所以字符A就被拿出来了,赋值给了ch,然后putchar()又将ch放在了标准输出,,所以我们看见了最终的显示结果A。同时字符‘A’也被缓冲区释放了,而字符‘B’,'C'被留在了缓冲区
Switch语句
括号里面的表达式(算术,关系,逻辑表达式 都有可能)会自行计算输出一个值,然后如果与case后面某一个常量表达式相匹配,程序就会自动跳转到其后跟着的语句块
如果都没有匹配的则执行default后面的语句
但是这样会导致一个问题,因为这个语句会执行符合条件之后的所有语句
Case或者default都相当于是一个标签,switch负责跳到第一个与之匹配的case后面,并且执行后面的所有程序
就会出现这种情况,想避免这种情况的话就要使用到break语句
这样的话在执行完某一个case后面的语句之后 读取到break 就会跳出switch语句。
但是这样做的话就没办法同时执行两个case后面的语句了。
While和do…while语句
这两个语句是构成循环的一种方式
While是入口条件循环,do while是出口条件循环,也就是不管三七二十一,先把程序给执行一遍,执行完程序后再判断条件,真的话则退出语句。
在while语句当中,先判断表达式中的条件,否的话执行大括号里面的程序块,执行完之后再判断条件,如果为否,继续执行,直到实现表达式中的内容
比如我要计算出公差为1,首项为1的数列 从1加到100的值
而dowhile与其类似,只是判断顺序不一样。
循环结构
再就是我们要了解一个for语句
它的执行顺序就是,先1 再判断2,执行循环体,再执行3,再判断2,以此类推。
上述如此做的话必须要在程序的其他地方初始化或者调整循环,比如
把表达式1移到了for语句前,把count++移到了循环体,效果是一样的,不过麻烦很多, 没必要。
循环嵌套
简而言之就是在一个循环里面包含着另一个循环,外部循环执行一次,内部循环执行一整套,形象一点就是外内内内内……外内内内……外…………直到外层循环完毕。举一个简单的例子就是九九乘法表
最后综合一下,判断素数,
Break语句,深入了解
说白了就是·break语句只会跳出它所在的语句的循环,它在内层循环则仅跳出内层循环,在外层就会直接跳出一整个循环。
Continue语句
continue语句的作用是跳过本次循环体中余下尚未执行的语句,立即进行下一次的循环条件判定,可以理解为仅结束本次循环
比如在这个程序里面,continue是属于if语句的语句块,while语句的循环体,与putchar’并列属于这个循环体,读取到这个语句的时候就跳过了putchar,从而不会显示出C,也就是跳过了c。
Goto语句
这个语句基本用法就是 goto(标识),然后在后面某一个地方加入这个标识,就会跳过中间所有的程序,从那个标识之后开始执行
但是这样做容易使代码混乱,用的很少,了解一下就行,一般只用于跳出多层循环,节省时间
以下是基本用法例子。
指针
学习指针之前首先要了解数据的存放地址,
也就是说每一个变量都有一个对应的地址,而这个地址里面存放着的是这个变量代表的值
指针要做的事情就是通过获取这个变量的地址来访问这个变量代表的值。
而指针变量,就是说这个变量里存放的是一个指针,这个指针又指向了某个地址。
定义指针时的那个*与取值的*含义不同,后者是通过地址访问数值,前者只是用于定义
以下是指针以及指针变量的实际运用
在这里面定义一个符号变量和一个整型变量,并对它们取址,把获取到的地址赋值给指针变量,再对指针取值也就是“解引用”,获取对应的数据。
知道指针是个什么东西之后就了解一下数组与指针之间的关系
数组名其实是数组第一个元素的地址
显示这里取数组名的地址和第一个元素的地址是一样的 。(%p就是代表着访问后面一个变量的地址。)
所以当我们想用一个指针指向数组的时候,对指针赋值成数组名就可以了。
然后就是指针指向的是第一个元素的地址,直接对这个元素进行解引用的话,得出来的只能是第一个元素的值,那么想要通过这个指针访问数组里其他的元素的话,就要对指针进行运算
以下面的程序为例
得出以下结果。
另外数组名是地址常量,不可改变 所以不能对数组名赋值,但是指针变量是可变的。
指针数组
就是元素全都是指针的数组,
其中,与数组定义的形式类似,不过类型为指针(int*就是代表着指向整型数据的指针变量),
结果如下
初始化数组指针的时候运用取址运算符,将获取的地址赋值给各个元素。
而将指针数组与字符串结合起来又会发生一些奇妙地反应。
运行结果如下
数组指针
就是指向数组的指针,
至于为什么要这么定义,因为int*代表的是指针,而int代表的是数组,数组指针本质是数组,所以类型是int*,应将这个看为整体。
而这种指针数组就是各个元素指向的是int型数据,所以要用小括号将内俩括起来,p2先被定义为指针,
对指针数组赋值的时候,必须保证每个变量都是指针,也就是地址
这样初始化指针数组就是错误的。
指针与二维数组
二维数组可以看成是一个元素为数组的一维数组。
这有助于理解二维数组里面指针的运算
理解二维数组,一下面这段代码为例。
Void 和NULL指针
Void指针就也叫做通用指针
但是使用void指针的时候有风险
Void指针可以指向任意类型的数据,定义了这个指针之后没有立即初始化的时候,会随机指向一个值,如果这个值是系统的重要组成部分并且后续给它赋值从而更改了这个值的话,可能会有很严重的后果。
所以我们就引入了了NULL指针
来避免这种情况。
指向指针的指针。
总结套娃,用一个指针去指向一个指针,
注意它的类型就是type**,这就代表指向指针的指针
比如int** 就是一个指向指向一个整型变量的指针的指针。
比如二维数组名,可以看作是每行首元素的数组名,也就是指针的指针。
访问这个值的话就得两层解引用 比如指针的指针p,解引用*p成指针,再解引用**p就是一个值了
如此就完成了对二维数组各个元素的访问。
指向常量的指针和常量指针
函数,作用域 生存期存贮类型
平常的int main()其实就是一个函数,为主函数,其他的printf,scanf是一类库函数,是c语言为我们提供的,我们也可以自己定义函数。
这个函数就表明,在子函数中运算的值不会改变主函数中定义的变量的值,除非把返回值赋值给一个变量。函数只负责运算出结果。
函数与指针,指针函数,函数指针。
虽然函数没办法直接改变实参,但是可以通过改变其地址来改变实参的值,地址比较实在嘛
Void函数
局部变量全局变量 常变量
Extern存在的就是因为运用函数之后定义全局变量的时候位置不能确定,有的时候编译器读取到的一个变量目前还未被定义,但是在你程序的后面被定义了,这时候就要用到extern,告诉编译器别急着报错。
另外要注意的就是:
递归
算法范畴,在函数中嵌入函数,比如求阶乘,快速排序。
注意使用递归的时候一定要有结束条件,不然会陷入死循环而使程序崩溃。
内存布局和动态内存管理
代码段(text):这部分区域通常属于只读取,也有可能包含一些只读的常数变量,比如字符串常量
数据段(date):通常用来存放已经初始化的全局变量和静态变量
BSS:存放未初始化的全局变量和静态变量。程序开始运行的时候自动把各变量初始化为零。
堆 动态内存管理时分配的内存被存放在这里 大小不固定,比如malloc calloc函数,使用free的时候就会把被释放的内存从堆中剔除。
栈:函数执行的内存区域,比如局部变量,函数参数,函数返回值。,通常和堆共享同一片区域 ,每调用一次函数就会申请一次栈空间。函数参数入栈的顺序是从程序语句的右边开始往左去
区别:- 堆是手动申请或释放的
-栈是自动分配或者释放的
-堆的生存周期认为规定 从申请直到释放,不同的函数可以自由访问。
-栈的生存周期有函数被调用开始到函数返回,函数之间的局部变量不能个相互访问 。
发展方向:-堆是从低到高发展(内存申请方向),栈反之。
-栈的发展幅度较小,也就是间距,堆的间距跨了16个字节。
(如果用relloc函数重新分配指针指向的动态空间,那么这个空间的地址和新分配的尺寸有关)
、
宏定义拓展
宏定义
预处理(文件包含,宏定义,条件编译)之一;
只起到替换作用
不做计算 不做表达式求解
- 不带参数的宏定义
格式: 例子 #define NAME 3.14
- 为了便于区分,宏的名字一般都是大写
-宏定义是是简单的替换,预处理是编译之前进行的,而编译器的任务之一·就是语法检查,所以编译器不会检查宏定义的语法
-Define后面不要分号 因为他不是说明或者语句
-允许嵌套
-作用范围是整个程序,但是可以用#undef 宏名 来取消宏定义.
2 带参数的宏定义。
#deifne MAX(x,y) (((x)>(y))?(x):(y))
注意这宏名和参数之间是没有空格的
定义的时候记得给后面算式以及里面各个参数加上括号,避免出现问题。
内联函数
1.直观上定义:
联函数的定义与普通函数基本相同,只是在函数定义前加上关键字 inline。
inline void print(char *s)
{
printf("%s", s);
}
2.更深入的思考:
函数前面加上inline一定会有效果吗?
如果不加inline就不是内联函数了吗?
后面让我们慢慢来解答这两个问题 内联函数和编译过程的相爱相杀
二.为什么使用内联函数
内联函数最初的目的:代替部分 #define 宏定义;
使用内联函数替代普通函数的目的:提高程序的运行效率;
针对上述两个方面我们展开讨论:
1.为什么要代替部分宏定义
宏是预处理指令,在预处理的时候把所有的宏名用宏体来替换;内联函数是函数,在编译阶段把所有调用内联函数的地方把内联函数插入;
宏没有类型检查,无论对还是错都是直接替换;而内联函数在编译时进行安全检查;
宏的编写有很多限制,例如只能写一行,不能使用return控制流程等;
对于C++ 而言,使用宏代码还有另一种缺点:无法操作类的私有数据成员。
2.普通函数频繁调用的过程消耗栈空间
函数是一个可以重复使用的代码块,CPU 会一条一条地挨着执行其中的代码。CPU 在执行主调函数代码时如果遇到了被调函数,主调函数就会暂停,CPU 转而执行被调函数的代码;被调函数执行完毕后再返回到主调函数,主调函数根据刚才的状态继续往下执行。
一个 C/C++程序的执行过程可以认为是多个函数之间的相互调用过程,它们形成了一个或简单或复杂的调用链条,这个链条的起点是main(),终点也是main()。当main()调用完了所有的函数,它会返回一个值(例如return 0;)来结束自己的生命,从而结束整个程序。
函数调用是有时间和空间开销的。程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。
栈空间就是指放置程式的局部数据也就是函数内数据的内存空间,在系统下,栈空间是有限的,假如频繁大量的使用就会造成因栈空间不足所造成的程式出错的问题,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭。
如果函数体代码比较多,需要较长的执行时间,那么函数调用机制占用的时间可以忽略;如果函数只有一两条语句,那么大部分的时间都会花费在函数调用机制上,这种时间开销就就不容忽视。
具体的一个调用效率讨论在第六章节 内联函数的类方法实现
为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数(Inline Function)。但也存在缺点,就是每一调用处均会展开,增加了重复的代码量。
可以理解为内联函数的关键词是:替换
3.更深入的思考
通过上述内容我们知道内联函数是在调用的地方展开函数定义,那么问题又来了,展开也好,替换也好,都存在下面两个问题:
内联函数一定就会展开吗?
在什么情况下内联函数会展开?
三.内联函数和编译过程的相爱相杀
在这一节,我们先一口气回答前两节的所有问题,然后慢慢引出后面的话题。
函数前面加上inline一定会有效果吗?
答:不会,使用内联inline关键字修饰函数只是一种提示,编译器不一定认。
如果不加inline就不是内联函数了吗?
答:存在隐式内联,不用inline关键字,C++中在类内定义的所有函数都自动称为内联函数。
内联函数一定就会展开吗?
答:其实和第一个问题类似,还是看编译器认不认。
在什么情况下内联函数会展开?
答:首先需要满足有inline修饰或者是类中的定义的函数,然后再由编译器决定。
其实说白了,内联函数管不管用是由编译器说了算的!
那如何要求编译器展开内联函数呢?
编译器开优化:gcc -O2 test.c -o test,只有在编译器开启优化选项的时候,才会有inline行为的存在,比如对g++在-O0时就不会作任何的inline处理,对于-O2的优化方式,编译器会通过启发式算法决定是否值得对一个函数进行内联,同时要保证不会对生成文件的大小产生较大影响。 而-O3模式则不在考虑生成文件的大小;
使用attribute属性:static inline __attribute__((always_inline)) int add_i(int a,int b);
使用auto_inline:#pragma auto_inline(on/off),当使用#pragma auto_inline(off)指令时,会关闭对函数的inline处理,这时即使在函数前面加了inline指令,也不会对函数进行内联处理。
上述操作都仅仅是对编译器提出内联的建议,最终是否进行内联由编译器自己决定,大多数编译器拒绝它们认为太复杂的内联函数(例如,那些包含循环或者递归的),而且类的构造函数、析构函数和虚函数往往不是内联函数的最佳选择。
有关visual studio中编译优化选择的位置如图,gcc编译见上面的例子也可以直接man gcc查看。
四.内联函数怎么用,在哪儿用?
基本介绍完内联的概念,接下来说说内联怎么用,在哪儿用?
是定义在头文件还是源文件?
内联展开是在编译时进行的,只有链接的时候源文件之间才有关系。所以内联要想跨源文件必须把实现写在头文件里。如果一个内联函数会在多个源文件中被用到,那么必须把它定义在头文件中
内联函数的定义不一定要跟声明放在一个头文件里面:定义可以放在一个单独的头文件中,里面需要给函数定义前加上inline 关键字,原因看下面第 2.点;然后声明 放在另一个头文件中,此文件include上一个头文件。这种用法 boost里很常见:优点1. 实现跟API分离封装。优点2. 可以解决有关inline函数的循环调用问题。
1.隐式内联:如第三节说的C++中在类内定义的所有函数都自动称为内联函数,类的成员函数的定义直接写在类的声明中时,不需要inline关键字
#include <stdio.h>
class Trace{
public:
Trace()
{
noisy = 0;
}
void print(char *s)
{
if (noisy)
{
printf("%s", s);
}
}
void on(){ noisy = 1; }
void off(){ noisy = 0; }
private:
int noisy;
};
2.显示内联:需要使用inline关键字
#include <stdio.h>
class Trace{
public:
Trace()
{
noisy = 0;
}
void print(char *s); //类内没有显示声明
void on(){ noisy = 1; }
void off(){ noisy = 0; }
private:
int noisy;
};
//类外显示定义
inline void Trace::print(char *s)
{
if (noisy)
{
printf("%s", s);
}
}
五.内联函数和重定义
这一部分我们带着问题一步步进行分析思考
六.内联的局限性
内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?
1.内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。(一般情况,在函数频繁调用且函数内部代码很少的情况下使用内联)
2.每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
3.类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。
4.一个好的编译器将会根据函数的定义体,自动地取消不值得的内联。对函数作inline声明只是程序员对编译器提出的一个建议,而不是强制性的,并非一经指定为inline编译器就必须这样做。编译器有自己的判断能力,它会根据具体情况决定是否这样做。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了inline 不应该出现在函数的声明中)。具体是否会被编译器优化为内联也要看优化级别。有些函数即使声明为内联的也不一定会被编译器内联,这点很重要。比如虚函数和递归函数就不会被正常内联。通常,递归函数不应该声明成内联函数。(递归调用堆栈的展开并不像循环那么简单,比如递归层数在编译时可能是未知的,大多数编译器都不支持内联递归函数)。虚函数内联的主要原因则是想把它的函数体放在类定义内,为了图个方便,抑或是当作文档描述其行为, 比如精短的存取函数。将内联函数放在头文件里实现是合适的,省却你为每个文件实现一次的麻烦。而所以声明跟定义要一致,其实是指,如果在每个文件里都实现一次该内联函数的话,那么,最好保证每个定义都是一样的,否则,将会引起未定义的行为,即是说,如果不是每个文件里的定义都一样,那么,编译器展开的是哪一个,那要看具体的编译器而定。所以,最好将内联函数定义放在头文件中。
七.内联的使用建议
使用:
当对程序执行性能有要求时,那么就适当使用内联函数
当你想宏定义一个函数时,使用内联函数
写一些功能专一且性能关键的函数,这些函数的函数体不大,包含了很少的执行语句。通过inline声明,编译器不需要跳转到内存其他地址去执行函数调用,也不需要保留函数调用时的现场数据。
在类内部定义的函数会默认声明为inline函数,这有利于 类实现细节的隐藏。(但也需要斟酌如果不需要隐藏的时候,其实大部分是不推荐默认inline的)
不使用:
不使用:如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
不使用:如果函数体内出现循环或者开关语句;那么执行函数体内代码的时间要比函数调用的开销大。
八.内联和static
多数情况下,inline 前面会加static关键字。why?
分开理解:
static 意味着本地化,每个包含头文件的C文件均在本地产生一个独立的内联函数。当有多个C文件包含头文件时,不会因为函数名相同而报重定义错误。(代价就是 代码所占的空间会变大)
谨慎使用 static:如果只是想把函数定义写在头文件中,用 inline,不要用static。static 和 inline 不一样:
static 的函数是 internal linkage。不同编译单元可以有同名的static 函数,但该函数只对 对应的编译单元 可见。如果同一定义的 static 函数,被不同编译单元调用,每个编译单元有自己单独的一份拷贝,且此拷贝只对 对应的编译单元 可见。
inline 的函数是 external linkage,如果被不同编译单元调用,每个编译单元引用/链接的是同一函数,同一定义。
上面的不同直接导致:如果函数内有 static 变量,对inline 函数,此变量对不同编译单元是共享的(Meyer's Singleton);对于static 函数,此变量不是共享的。看后面的代码就明白区别了。
static inline 函数,跟 static 函数单独没有差别,所以没有意义,只会混淆视听。
版权声明:本文为CSDN博主「赵大宝字」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
:https://blog.csdn.net/qq_35902025/article/details/127912415
结构体
内存碎片
频繁调用malloc等函数分配释放空间会导致内存碎片的产生。
可能会导致堆内存支离破碎
调用malloc函数申请内存的时候
要从应用层到内核层再返回到应用层,比较耗费时间。
解决办法就是建造一个内存池(实际上就是让程序额外维护的一个缓存区域)。
就是调用完一块空间后,不用free将其释放,而是放入内存池当中。
要取用的话再从内存池中抽出,也就不需要再用malloc函数调用一个内存空间,从而避免的了内存碎片
当我们要申请内存块的时候就从内存池里查找有没有合适的废弃内存块,有的话就调出来没有的话就调用malloc函数。当释放内存块的时候也是查找内存池里面有没有足够的空间,有的话放进去,要用就再从里边取。
单链表
用头插法在链表中插入元素
先让要插入的节点的指针域指向头节点,再取消头指针先前的连接,再让head指向目标指针的空间
。
共用体
共用体所有成员共享同一个内存地址
结构体想象成一个小团体,里面有很多成员
共用体是人格分裂,成员为一体,但是不会同时出现,只会不停的切换(成员是灵魂,地址是肉体)
不能同时访问多个成员,比如我不能同时打印出所有的成员,也不能同时初始化所有的成员
共用体采用与开始地址对齐的方式分配内存,而且使用覆盖技术来实现内存的共用
比如这里面,当对pi赋值的时候,成员i的内容将会被改变,从而失去自身的意义。
同理,对str赋值的时候,会使pi失去意义。
共用体存储程序中逻辑相关但是情形互斥的变量。,使其共享内存空间的好处是除了可以节省内存空间以外,还可以避免因为操作失误引起逻辑上的冲突。
共用体所占内存空间大小如下
一般情况下是这样,但是实际大小还是会收到内存对齐方式的影响
枚举
顾名思义一一列举,
枚举常量与宏定义类似,但是枚举常量的是整型的,而宏定义没有限制。
Swich语句满足多个条件的用法举例。
满足任意一个case后的常量就可以执行语句了