目录
目录
前言
本篇主要是对C语言学完后对知识点进行的相对系统且详细的梳理
C语言是跨世纪的编程语言之一,经久不衰。相信本篇中一定还有你未曾了解过的知识
1.C语言中的数据类型
整型(整数)
一个字节即八个二进制位,取值范围便是由其能存放多大范围内的数据
- char // 字符串数据类型,存储时占一个字节大小,取值范围(-2^7至2^7-1即-128~127)
- short // 短整型,存储时占两个字节大小,取值范围(-2^15至2^15-1即-32768~32,767)
- int // 基本整型,存储时占四个字节大小,取值范围(-2^31至2^31-1)
- long // 长整型,存储时占四个字节大小,取值范围(-2^31至2^31-1)
- long long // 双长整型,存储时占八个字节大小,取值范围(-2^63至2^63-1)
(以上类型都默认为有符号数(除了char类型),可用unsigned可将以上类型定义为无符号数,则可存放的正整数的范围扩大一倍)
如unsigned int //无符号整型,存储时占四个字节,取值范围(0至2^32-1)
浮点型(小数)
- float // 单精度浮点型,存储时占四个字节大小,取值范围(0 以及 1.2*10^-38至3.4*10^38)
- double // 双精度浮点型,存储时占八个字节,取值范围(0 以及 2.3*10^-308至1.7*10^308)
- long double // 长双精度浮点型,存储时占八个字节,取值范围(0以及3.4*10^-4932至1.1*10^4932)
由上可见,不同的数据类型向内存申请的空间大小不同,根据所处理的数据大小不同,使用不同的数据类型可以更合理的利用存储空间。
代码如下(示例):
//sizeof()操作符可用于计算类型所占空间的大小
printf("char = %d\n", sizeof(char));
printf("short = %d\n", sizeof(short));
printf("int = %d\n", sizeof(int));
printf("long = %d\n", sizeof(long));
printf("long long = %d\n", sizeof(long long));
printf("float = %d\n", sizeof(float));
printf("double = %d\n", sizeof(double));
printf("long double = %d\n", sizeof(long double));
指针类型
指针类型是C语言中特有的数据类型。正如整型用于存放整数,浮点型用于存放小数一样,指针即是用于存放地址的一种数据类型
地址是什么?
为了能够更合理的利用计算机的内存空间,故此内存在设计时被划分成了一个个的内存单元,每个内存单元都有其对应的编号,这个编号便被称为地址。
(就好比类似于酒店为了客人能够更好的找到对应的房间,所以给每个房间都定了一个编号,这个编号便类似于地址)- 指针的大小是多少?
由上面数据类型可知,不同的数据类型在内存空间里都有对应的大小,指针也一样。在32位的机器上指针的大小即是4个字节,在64位机器上指针的大小即是8个字节。
(简单的来说呢:我们都知道cpu是无法直接在硬盘上读取数据的,而是通过内存读取。cpu通过地址总线、数据总线、控制总线三条线对内存中的数据进行传输和操作。
在32位的机器下,计算机有32根地址线,故通过通电(正负电)来区别0和1(电信号->数字信号),则32根地址线产生的所有电信号的可能性为2^32种,故地址都为32个二进制位,保证能将所有可能性的电信号都能够保存下来。又因为指针类型是用于存放地址的,故指针的大小在32位机器下为4个字节,64位机器下为8个字节)- 指针的类型
由于C语言中不同的数据类型看待内存的方式以及在内存中的存储方式都不一样(具体可看数据在内存中的存储),所以针对不同的数据类型的地址,则要用不同类型的指针进行存储
如int类型则需要使用int*整型指针来进行存储,float则需要float*的浮点型指针进行存储
(*即说明是一个指针类型)
聚合类型(自定义类型)
自定义类型即用户自己定义的数据类型,如:数组,结构体类型,枚举类型等
数组类型
数组类型也是自定义类型?没错,如下图
由此可见数组的类型即去掉数组名的部分
(int即数组元素的类型,(*)因为arr本质是一个指针,[10]即是这个数组类型有十个元素)
因为数组其实本质就是用户自定义的指向一块内存空间的指针,如上图即用户声明了一个数组类型名称为arr,指向的是一块可以存放十个整型数据的空间。
结构体类型
结构体类型即可以是上述基本类型组合而成的一个类型,它是一个可以存放更多细节的一个类型,有其内在的结构性。
简单举例如:一个人,姓名是字符类型,年龄则是整型,那么我们单是表示一个人这个类型,就需要一个该结构体类型,包括了字符类型和整型。如下:
struct People { char name[10]; int age; };
其中struct即为结构体关键字,用于声明一个结构体,People即是该结构体的名字,此时
struct People即为你自定义的一个数据类型,可用其来声明变量,如struct People p;
枚举类型
枚举顾名思义就是列举,列举出一个常量类型所有可能的数值即为枚举类型,如下图:
enum Color { RED,//0 YELLOW,//1 BLUE//2 };
其实enum即为枚举关键字,用于声明一个枚举类型,Color即是该枚举常量的名字,此时
enum Color即为你自定义的一个枚举类型,其说明enum Color这个常量只可能有以下三个值,分别为红黄蓝,这三种颜色在内存中对应着三个数值即0,1,2。这个数值可在声明时进行改变,默认依次递增。
2.变量和常量
变量
用数据类型定义的一个可改变的量(有如此多的数据类型,即有如此多的对应类型的变量)
变量即在C语言中可以改变的量
//定义变量的方式
short a = 20;//向内存申请2个字节用来存放20
float a = 95.6f;//向内存申请4个字节,用于存放小数95.6
变量分为全局变量和局部变量
全局变量——即定义在代码块({})之外的变量被称为全局变量
局部变量——即定义在代码块之内的变量被称为局部变量
两种变量的作用域不同,全局变量的作用域在整个工程中,局部变量的作用域在其所在的局部范围;相对应的变量出了作用域即被销毁(生命周期结束)
tips:
1、作用域即通常来说,一段程序代码中所用到的名字(变量名)并不总是有效/可用的,而限定这个变量的可用性的代码范围就是这个变量的作用域,如下图
i是在for循环内部声明的,那么出了for循环,这个i则不能被使用。for循环内即是这个变量i的作用域。
2、生命周期即在作用域内则生命周期未结束,出了作用域即生命周期结束
3、定义局部变量和全局变量时命名建议要不相同,相同的时候在作用域内局部变量优先使用。如下:
常量
粗糙的理解可以认为是程序运行过程中不变的量
- 字面常量
- const修饰的常量
- #define定义的标识符常量
- 枚举常量
(1)字面常量
字面常量就是一个数字/字母等不可改变的量,如3,a
若改变则违反了正常逻辑,如:3 = 4(4是不能赋值给3这个常量数字的)
(2)const修饰的常量
const——常数、常属性; 即变量被const修饰之后,该变量即变为了只读变量,不可以被修改
const int num = 4;//const修饰后即变成常变量(不能改变的变量)
//本质是变量,但具有常量的属性
printf("%d",num);
num = 0;
printf("%d",num);
//此时运行会报错,因为num被const修饰后不能被改变
(3)#define 定义的标识符常量
#define定义只是给某个符号赋予一个意义,如下代码:即是给MAX这个符号赋予了10这个值
在代码进行预处理时,这个MAX在代码里出现的地方都会被替换成10
#define MAX 10
int main()
{
int arr[MAX] = {0};//此处的MAX即为常量
printf("%d\n",MAX);
return 0;
}
(4)枚举常量
//枚举常量
//枚举 即 一一列举
//枚举关键字 - enum
#include<stdio.h>
enum Sex{MALE,FMALE,SECRET}//此三者为枚举常量,都有其各自的值
int main()
{
printf("%d\n",MALE);//默认为0
printf("%d\n",FMALE);//默认为1
printf("%d\n",SECRET);//默认为2
return 0;
}
3.转义字符、ASCII码、字符串
常见的转义字符
转义字符即\(反斜杠)+某个字符则改变了字符原有的含义,用于表示其它的含义。
注:\dd、\xdd输出为其十六进制/八进制数字转换成十进制数字后在ASCII码表中对应的字符。
ASCII码表
即对各种字符进行了相应编码后,这些编码统合在了一张表里
表里的每个字符都对应着一个数值,这个数值也反映着这个符号,C语言中通常使用char类型来存储这些字符(编码)
字符串
字符串即由双引号引起的一串字符
int main() { char arr1[] = "abc";//创建数组 //"abc" -- 'a','b', 'c','\0' -- '\0'字符串结束的标志,一般默认隐藏 char arr2[] = {'a','b','c','\0'};//'\0'的值即是0 printf("%s\n",arr1);//打印不会显示'\0' return 0; }
4.基本结构
(1)分支语句
- if语句
- 悬空else
悬空else遵循着就近原则,它会与最近的一个未匹配else的if进行配对- if的嵌套
if嵌套时只有一条语句时不需要加上{},但是要注意悬空else的问题,悬空else可能会与原来设计代码的逻辑思路不相符int main() { int a = 1; int b = 2; if (a == 0) if (b == 0)//if内包含着if即为if语句的嵌套调用 a = a + b; else//此时这个else即为悬空else a = a - b; cout << "a" << a << endl; return 0; }
上述代码的实际逻辑如下:
if (a == 0) { if (b == 0) a = a + b; else a = a - b; }
switch语句
case标签
满足case标签则从该条标签进入往下顺次执行直到遇到break关键字或者执行完毕default子句
当所有case标签都不符合时则执行default子句int main() { int i = 0; cin >> i; switch (i)//i必须是一个可以确定的值 { //case标签的标签必须是一个常量 case 0: case 1: case 2: break; //case标签下没有break的话,则会顺次执行 //如若i为0,则会顺次执行到case 2 default: break; } return 0; }
(2)循环语句
while语句
int i = 0;//循环变量的初始化 while (i < 10)//循环条件 { cout << "hello" << endl; i++;//对循环变量进行调整 }
for语句
语法:for( 对循环变量初始化;循环条件;对循环变量的调整)
//对循环变量的初始化,循环条件以及对循环变量的调整都放到了for循环中 for (int i = 0; i < 10; i++) cout << "hello" << endl;
do...while语句
int i = 0;//循环变量的初始化 do { cout << "hello" << endl; i++;//对循环变量进行调整 } while (i < 10);//循环条件
三种循环结构本质上都没有很大的区别,大部分情况下相互间可以进行替换。
while与for循环都是先进行判断循环条件再进入循环体,而do...while循环是肯定先执行一次循环体再判断是否继续进行循环
(for循环是较为常用的循环)
写一个循环时要注意:
一般都会需要有对循环变量进行初始化、进入循环条件、对循环变量进行调整三个步骤
goto语句:
goto,顾名思义就是go to...(去哪里)。这个语句很灵活,但是也可以完成循环的工作。但是由于太过灵活了,逻辑关系不好把控,所以不建议使用。简单使用方式如下:
5.各类运算符
(1)算术运算符
(2)关系运算符
(3)逻辑运算符 ![watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5Yid5qmZXzc=,size_20,color_FFFFFF,t_70,g_se,x_16](https://i-blog.csdnimg.cn/blog_migrate/e03dd184d49c3f05693d9260564a8ba9.png)
(4)赋值运算符
(5)其它一些运算符
sizeof()运算符:返回变量的大小;&取地址操作符:返回(取出)变量的地址;
?:条件表达式:举例 如 条件 ? 表达式1 : 表达式2
即条判断条件是否成立?如果成立则执行表达式1,不成立则执行表达式2.
(6)一些平常使用较少的运算符
移位操作符:<<(左移)和 >>(右移)
位操作符:& (按位与) | (按位或) ^(按位异或)
(tips:这里的位都是指二进制位,即一个整数化为二进制后的形态)
运算符按操作数来分类即有:
- 单目操作符:只对一个变量进行操作;
- 双目操作符:只对两个变量进行操作;
- 三目操作符:只对三个变量进行操作;
C语言中只有条件表达式是唯一一个三目操作符
6.关键字
即C语言本身已具有意义,不能用作其它用途的词,如关键字不能被用于变量名、函数名等
简单介绍一部分常用的关键字:
break:跳出当前循环
const :声明只读变量
continue:结束当前循环,开始下一轮循环
enum :声明枚举类型
return :子程序返回语句(可以带参数,也可不带参数)
sizeof:计算数据类型或变量长度(即所占字节数)
static :声明静态变量
struct:声明结构体类型
typedef:用以给数据类型取别名
unsigned:声明无符号类型变量或函数
void :声明函数无返回值或无参数,声明无类型指针
7.函数
函数即一组为了完成某个功能的语句组合。
一个函数执行一个功能,需要时在主函数中直接调用即可。(编写函数时要尽量做到高内聚低耦合)
(当函数体实现在main函数调用它之前,则不需要进行函数的声明)
//定义方法 int Add(int x, int y)//函数需定义返回的数据类型 { int z = x + y;//函数体的实现 return z;//函数的返回值 } int main() { int num1 = 10; int num2 = 20; int sum = Add(num1, num2);//调用即可 printf("%d", sum); return 0; }
8.数组
数组即是一组相同类型元素的组合,在前面已经简单介绍过了,在这做些扩展
(数组与指针有着密切的联系)
1、数组名
数组名的本质是一个指针常量,其指向着数组的首元素,指向不可被改变
(即相当于被const修饰的指针:int* const pArr指向不可改变)
数组名基本在所有情况下都代表着首元素地址,除了以下两种情况:
(1)、sizeof(数组名);(2)、&数组名
这两种情况数组名都表示整个数组,sizeof(数组名)即计算整个数组的大小;&数组名即表示取出整个数组的地址
我们来看些题目吧:(下面的printf语句输出的结果是什么?)
解答如下:
2、下标引用
数组是通过下标来对数组元素进行访问,除了优先级之外([ ]优先级高于 * ),其本质与指针的解引用(间接访问)完全相同。如下图:让我们来看几道题目:
(下面的结果输出是多少?)
解答如下:
3、多维数组
一般都是使用一维、二维数组(更高维以此类推)不完全初始化:数组元素大于初始化值的个数,剩下的元素默认为'0'
int arr1[10] = { 0 };//一维数组 int arr2[10][10] = { 0 };//二维
关于二维数组,以下的题目你会做吗?
解答如下:
4、数组名作函数参数
由第一点可知,数组名本质是一个指针常量,所以数组名作为函数参数时,传递过去的也是一个指针,指针类型为数组首元素的数据类型。
9.指针
指针即是一种存放地址的变量类型,前面已经大概介绍过,这里进行一些细节补充
1.指针的一些概念
1、指针相关操作符:
&(取地址操作符),*(间接访问操作符)2、多级指针
即用来存放相较于本身前一级的指针的地址
如二级指针用于存放一级指针的地址,以此类推
2.指针的使用
int* p = &a;
//这即是一个指针变量的声明及初始化,此时p中则存放了变量a的地址,*p则取到a变量中的值 tips:*(解引用操作符)说明p是一个指针,int则说明p指针中存放的是整型变量的地址ps:注意指针的使用都要进行初始化,养成良好习惯,避免野指针的使用以及对空指针进行解引用
野指针:声明了指针,该指针却无所指向(或者指针指向的空间被释放后,指针仍指向这片空间)
空指针:指向NULL的指针,NULL这个位置是不可访问的地址,对空指针进行解引用一般会造成程序的崩溃
3.指针类型
存储地址时各类型的指针其实都具备相同作用,但进行解引用时有本质的区别。
1、指针类型不同,则能够访问的内存空间大小不同。
如int* p能够访问四个字节,而char* p能够访问一个字节;
我们来通过一道题目来理解:(下面题目输出结果是什么呢)
首先需要了解大小端字节序:数据存储详解
解答如下:
arr数组在内存中的存储格式为:0x00ECFBF4: 01 00 00 00
0x00ECFBF8: 02 00 00 00
0x00ECFBFC: 03 00 00 00
0x00ECFC00: 04 00 00 00
0x00ECFC04: 05 00 00 00指针p的类型为short*类型的,因此p每次只能访问两个字节,for循环对数组中内容进行修改时,依次次访问的是:arr[0]的低两个字节,arr[0]的高两个字节,arr[1]的低两个字节,arr[1]的高两个字节(一共就只访问了数组前两个元素,一共八个字节)
故改变之后,数组中内容如下:
0x00ECFBF4: 00 00 00 00
0x00ECFBF8: 00 00 00 00
0x00ECFBFC: 03 00 00 00
0x00ECFC00: 04 00 00 00
0x00ECFC04: 05 00 00 00故最后打印:0 0 3 4 5
2、当指针+-整数时,指针类型决定了指针的步长。
如int* p(p+1则向后偏移了4个字节) char* p(p+1则向后偏移了1个字节)
我们也来看道题目来理解一下:(下面会输出什么呢?)
解答如下:
3、指针间运算
当两个指针需要指向同一块地址时,其二者可以进行相减运算(相加无意义)
(1)指针加减整数即意味着指向的位置(地址)往前或往后移动;
(2)指针减去指针得到的是两个指针中间的元素个数;(指针相减可求字符串长度)
指针相关的各式用法
- 字符指针
类似于字符数组,但又有不同
字符指针一般用于存放字符类型的地址,但也可以存放字符数组的地址(即指向一个字符串);
也可以用来指向字符串常量,如const char* p = "abcde",此时p存放的是字符串首元素a的地址,
由于字符串常量不可被修改,严格来说指针需要用const进行修饰- 指针数组
顾名思义即是存放指针的数组,数组中的每一个元素都为指针类型的变量
声明方式:int* p[10];即p是一个可以存放十个指针类型变量的数组- 数组指针
数组指针即为存放数组地址的指针
声明方式:int (*p)[10];即p是一个指向存放着十个元素的数组的指针- 指针传参
要对指针指向内容进行修改则传递指针的地址,无需修改则可以只传指针;
一级指针进行值的传参,则用一级指针进行接收即可,若要进行地址的传参,则需要用二级指针才可接收一级指针的地址- 函数指针
即指向函数的指针,函数名及表示着函数的地址(函数名在大部分情况下都是标识着一块地址,所以通过函数名可以直接找到函数体的实现位置,所以无需对函数名进行取地址以及解引用操作)
声明方式:int (*p)(int,int);即意为p为指向一个形参为两个int类型,返回值为int类型的函数的指针
- 回调函数
通过函数指针可以实现回调函数通过函数指针(地址)调用的函数。
如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们称为回调函数。回调函数不是由该函数的实现方值直接调用,而是在特定的事件或条件发生时由另外的一方调用,对于该事件或条件的响应
一个函数通过另一个函数间接调用,被调用的那个函数则被称为回调函数,这种机制则为回调函数机制。
- 函数指针数组
即是一个存放函数指针的数组,数组的每个元素都为函数指针,每个函数指针都指向着一个函数
声明方式:int (*p[10])(int,int);
//即p是一个数组,存放的数据为10个指向返回值为int类型,形参为两个int类型的函数的指针- 指向函数指针数组的指针
本质即是一个数组指针,其指向一个数组,数组的每个元素都为函数指针
声明方式:int (*(*p)[10]))(int,int);
//p首先是一个指针,然后指向的是一个数组,数组的每个元素的类型是函数指针,一共有十个这样类型的元素- qsort函数
qsort()是一个库函数,其可以对任意类型的数据进行排序
函数原型:
void qsort(void*base,size_t num,size_t width,int(*cmp)(const void*e1,const void*e2));
参数1:待排序数组的首元素地址;
参数2:元素的个数;
参数3:每个元素的大小;
参数4:一个函数指针。该函数指针指向的是一个返回值类型为int,形参是两个未知类型的变量的函数
该函数的形参意思是要传入需比较的两个元素的地址,然后该两个元素相减(e1-e2)
若e1>e2则返回一个大于0的数;e1<e2则返回小于0的数;e1=e2则返回0;
根据该函数的实现思想,可以将大部分常用排序修改成可以比较任意类型数据的排序
10.结构体
(1)基本概念
结构体即是一些数据类型的集合,用以表示更加复杂的数据类型
前面已经进行过简单介绍,这里也是对结构体的细节进行一些补充:
struct 结构体关键字 .和->为访问结构体成员的操作符例子:
如链表的一个结点,该结点即需要有能够存放数据(val),还需存放指针以保存下一个结点的地址的能力故其需要用一个结构体进行表示。如下:struct Node { int val;//存放数值 struct Node* node;//存放下一个结点的指针 };//该类型则为一个自定义的结点类型
需要注意的地方:
1、结构体传参时,由于可能结构体的成员比较多,整个体量比较大,所以进行传参时尽量避免传值,而是传递结构体的地址
2、结构体定义时,成员类型不能包含自己本身类型的变量,否则会造成类似死递归的情况
如下述代码就是错误的:struct Node { int val;//存放数值 struct Node node;//成员中不能包含自身类型的成员 };//该类型则为一个自定义的结点类型
3、结构体进行自引用时应通过指针来进行,指针存放自身的结构体类型的地址
(2) 结构体所占空间的大小的计算
内存对齐:
计算结构体类型所占的空间大小所要使用到的规则
GCC的编译器存放时无默认对齐数的概念,成员的大小直接往内放即可对齐规则:
1.第一个成员在结构体变量偏移量为0的地方(也就是最开始存储的位置)
2.其它成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值(VS编译器默认对齐数为8)
3.结构体总大小总是为最大对齐数(每个成员变量都有各自的对齐数)的整数倍
4.如果结构体内嵌套了结构体,则嵌套的结构体对齐到自己的最大对齐数的整数倍处,此时结构体的整体大小就是整个结构体最大对齐数(也要考虑到嵌套结构体的对齐数)的整数倍对齐数存在的原因:
1.平台原因(移植性问题):某些硬件平台只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常
2.性能原因:访问未对齐的内存,处理器需要进行两次内存访问,而对齐了的内存只需要进行一次内存访问
总结来说,结构体的内存对齐是以空间来换取时间的做法一些细节:
1.设计结构体时,如果要满足对齐的要求,又要节省空间,设计结构体时应让占用空间小的成员尽量集中在一起
2.在结构体对齐方式不合适的时候,可用预处理指令#pragma pack()修改默认对齐数
如#pragma pack(4)设置默认对齐数为4;下方再添加#pragma pack()则为取消设置的默认对齐数ps:上述结构体规则都是在vs平台下进行的叙述
拿之前的一道题目里的结构体来进行举例吧
下面结构体所占大小为什么是20个字节呢?
解答如下:
位段
位段即是对应着二进制位,使用其可以节省空间位段一般应用于网络传输上,一般用不到,且其存在着一个跨平台的问题,故大家知道有这么个东西就行(仅做了解)
位段与结构体的不同:
1.位段的成员一般是int,unsigned int,或signed int。
(都为整型,不能是浮点型)有时也可是char;
2.位段的成员名后边有一个冒号和一个数字
如:下图中A就是一个位段类型(也属于结构体的一种类型,不过和结构体有所区别)struct A { int _a:2;//2-->两个比特位,即二进制位 int _b:5;//5-->五个比特位 int _c:10;//… int _d:20;//该数字不可大于32 }; struct A s;//声明一个位段类型 sizeof(s) = 8;//计算该类型的大小(单位为字节),按照对齐规则计算
位段的内存分配:
位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式开辟的。
即一次开辟则开辟四个字节空间大小或一个字节的空间大小(所以上面计算大小为8字节)
装不下一个变量时则会放弃剩余空间,重新开辟一块新的内存空间去存储
存放的数据的二进制位如果大于其开辟的内存空间,则会截取后段恰好可以放进内存空间的二进制位。如:char a:3; struct A a = 10;-->1010;则只会存010三个二进制位;
11.动态内存管理
动态内存管理即使用动态内存函数可在堆区自己进行内存的开辟和管理
动态内存分配函数
malloc
函数原型:void* malloc(size_t size);返回一个指针,指向所申请到的那个空间;
如:int* p = (int*)malloc(10*sizeof(int));
//即向内存申请十个整型变量的空间交由整型指针p进行管理(使用)
realloc
函数原型:void* realloc(void* memblock,size_t size);
参数一、void* memblock:已开辟的空间的地址
参数二、申请多大的空间
当申请的空间过大或过小,则可以使用realloc进行调整,其可对已申请到的空间进行扩容,实现动态增长(可能申请失败,申请失败会返回空指针,故标准来说需要进行判断,其它的动态申请函数也同理)int* p = (int*)malloc(10*sizeof(int)); int* tmp = (int*)malloc(20*sizeof(int)); if(!tmp) { cout << "申请空间失败" << endl; return; } int* p = tmp;
上述代码即向内存申请十个整型变量的空间交由整型指针p进行管理(使用),但是后面感觉空间不够使用,则用realloc重新申请一块儿更大的内存空间,交由指针p继续管理
calloc
函数原型:void* calloc(size_t num,size_t size);
如:int* p1 = (int*)calloc(10, sizeof(int));//申请十个元素,每一个大小为int的空间(并把空间的每个字节初始化为0,这是该函数与malloc的唯一区别);free
函数原型:void free(void* memblock);
专门对动态申请的空间进行回收和释放
动态开辟内存时需注意的点
一些容易导致bug的地方,以及一些良好的编程习惯
内存泄漏:
使用完动态开辟的内存一定要用free进行回收释放,尽管现在编程时,编译器在程序结束之后都会对内存空间自动进行回收释放,但就职业化来说,要养成随手申请,随手释放的良好编程习惯空指针:
进行动态内存申请时,有可能申请失败,失败时会返回一个空指针。
为了防止对空指针进行解引用,故每次在动态申请了内存空间之后则需要对返回的指针进行判断,若为空指针,则及时退出程序,并返回错误信息free:
1.防止对非动态开辟的内存进行free操作
2.防止使用free释放动态内存的一部分(free只能从动态内存申请的开始的位置释放)
3.防止对同一块动态开辟内存多次释放(释放后将指针置为空指针即可避免)越界访问:
动态开辟的内存空间与数组相似,要注意申请空间的大小与实际操作的空间大小是否逾越边界,造成对内存的非法访问
关于内存空间
栈区:
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数返回数据、返回地址等。堆区:
即动态内存函数开辟空间的位置
一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。数据段:
(static)存放全局变量、静态数据。程序结束后由系统释放。代码段:
存放函数体(类成员函数和全局函数)的二进制代码。
12.文件操作
c语言中可以将数据信息等输出到磁盘上保存,需要时可以读取到内存中使用
文件名包含三部分:文件路径+文件名主干+文件后缀
文件操作主要就是一堆对文件进行操作的函数,看看函数原型,大概记忆一下就能运用了
文件相关的一些概念
IO流:
标准输入/输出流(stdin/stdout),即键盘/显示屏
缓冲文件系统:
即在磁盘和程序数据区之间存在输入缓冲区和输出缓冲区,存放或者输出数据时都要经过缓冲区。一般当缓冲区存放满了才进行输入或者输出
缓冲区其存在以下刷新策略:
- 无缓冲(立即刷新)
- 行缓冲(碰到\n即刷新)
- 全缓冲(缓冲区满了才刷新,一般往磁盘文件写文件时才用到)
- 程序运行结束一般都会刷新缓冲区
- C语言中,fflush()函数可以直接刷新缓冲区
文件指针:
C语言的标准库中(stdio.h)声明了一个FILE类型的结构体变量,每当打开一个文件时,则会返回一个FILE类型的指针,用于与文件进行交互(向文件内进行输入和输出)。
FILE类型的指针即将C语言程序与文件连接在一起
文件的打开方式
文件操作相关函数
fopen
函数原型:FILE* fopen ( const char * filename, const char* mode ); 用于打开文件,并返回一个操作该文件的指针
形参一为文件的地址(可为绝对路径也可为相对路径),形参二为文件的打开方式(后续会介绍)fclose
int fclose ( FILE * stream ); //用于关闭文件,原理同free()
stream = NULL;//将文件指针置为空指针fseek
函数原型:int fseek ( FILE * stream, long int offset, int origin );//根据文件指针的位置与偏移量进文件指针的定位
offset:偏移量;
origin:文件指针所在的位置
(SEEK_SET,SEEK_CUR,SEEK_END)分别对应着文件开头的位置,文件指针当前的位置以及最后的位置ftell
函数原型:long int ftell ( FILE * stream );//返回文件指针相较于起始位置的偏移量
可用于对文件内容大小的计算rewind
函数原型:void rewind ( FILE * stream );//让文件指针回到起始位置feof
函数原型:int feof ( FILE * stream );//如果读到了与流关联的文件结束指示符(EOF),则返回非零值;否则,返回零。
应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾(EOF)结束
牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束
1、文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
2、二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
文件读写函数
总结
本篇对C语言前中期学习大概涉及到的点进行了较为系统的梳理,看到这相信你多少都有些收获吧
还有些更细节的东西(涉及到一些底层的知识)会再后续的博客里进行整理,希望能帮到大家~
第一篇万字博客
C语言到这也暂时告一段落,也算是给自己画上暂时的句号吧