数组
在基础篇说过,数组实际上是构造类型之一,是连续存放的。
一维数组
定义
定义格式:[存储类型] 数据类型 数组名标识符[下标];
下面分模块来介绍一下数组的定义部分的内容。
1、初始化和元素引用:
可以看到数组是连续存储的,然后因为每个int都是四个字节,所以存储结构一目了然。
2、 数组名的本质与数组越界异常
从上面的程序例子中我们可以看到,我们在打印数组名的时候使用的是%p的格式来打印,这表示arr实际上是数组的起始地址。
怎么理解呢?意思就是arr这个标识符名字表示的就是一个地址的常量,这也就是说arr这个标识符不可以无条件的出现在等号的左边(因为这意味着会被修改,定义的时候那不叫修改嗷,那叫初始化,是合法的)。
再来探究一下数组到底是怎么往元素里面写值的,其实这个过程涉及到了指针偏移的概念。
来看这么个例子:
我们居然打印出来了arr[3]的值,但是很明显这不是越界了吗?这数组是怎么找到arr[3]的呢?
其实这都是指针偏移造成的,这也是为什么说数组存取速度快的原因,当取a[i]值的时候,其对应的是依赖 a[i] =*(a + i) 即使用数组名加上 i 个偏移地址来取得的,所以当我们取a[3]的时候,尽管已经越界,但是我们依然能够通过偏移量 3和数组名 a来依靠指针取得正确的值(注意这个在某些优化后的编译器是肯定会报越界异常的,只不过这里gcc更加开放而已嗷)。
二维数组
我们将从以下几点来说明二维数组。
1、定义以及初始化
格式为:[存储类型] 数据类型 标识符 [行下标][列下标];
初始化时行标是可以没有的,因为靠列标也能知道会分成几行呀。
cpp
#define M 2
#define N 3
int arr[M][N];
上面的程序就定义了一个二行三列的行列式,其从人类的逻辑上理解是下图这样的:
也就是一个方格的形式,但实际上其在计算机中存储依然是线性的一个数组:
也就是说第一行和第二行是连在一起的,第一行的最后一个元素地址再偏移一个就到了第二行行首的位置。
BTW,二维数组不过是一维数组的一个拓展,所以一维数组的特性二维数组都有,比如如果不初始化的话都是数组元素都是随机值,除了可以动态赋值之外,二维数组也可以进行静态初始化和部分初始化:
cpp
//动态赋值
for(int i = 0; i< M ;i++)
for(int j =0; j < N;j++)
scanf("%d",&a[i][j]);
//静态初始化
int a[M][N] = {{1,2,3},{4,5,6}};
/* 打印结果
1 2 3
4 5 6
*/
//局部初始化
int a[M][N] = {1,2,3};
/*打印结果
1 2 3
0 0 0
*/
//局部初始化
int a[M][N] = {{1,3},{6}}
/*打印结果
1 3 0
6 0 0
即已经初始化的位置就初始化为该值,其余未被初始化的位置就全为0
*/
//不初始化
int a[M][N];
/*
434223423 324234 324324
213123 21312321213 3432432
如果不初始化一样是一堆乱码
*/
2、元素引用
之前已经学习过:
数组名 [行标][列标];
3、深入理解二维数组
我们在上面用过这样的初始化语句:
cpp
int a[2][3] = {{1,2,3},{4,5,6}};
实际上其在内存中的存储是这样的,a[0]表示第一行元素(可以看作是一个小数组)a[0][0]的首地址,a[1]表示第二行元素(同样可以看作是一个小数组)a[1][0]的首地址,只不过这两个小数组是连续存储的而已,这也就意味着对数组名a+1,其跳转的应该是第二行元素的行首地址:
这就是行指针的含义,在指针专题我们还将深入理解指针这个东西,还会再提这块内容。
字符数组
对于字符数组我们从下面几个方面进行学习:
1、定义以及初始化还有存储池特点
[存储类型] 数据类型 标识符[下标] ...(可以多个下标,变成多维字符数组嘛)
对于字符数组的初始化,我们可以用单个字符初始化,也可以用字符串常量来初始化。
单个字符初始化:
字符串常量初始化,注意对于这一点是其区别于前面各种数组的关键,因为对字符串操作时其有一个存储上的特点,它会多出一个尾标记,即尾0,这用来标记当前字符串的一个结束。
比如我们有一个字符串"hello"共五个字符要存储,实际上它在数组当中存的是:h e l l o \0
共六个字符。
上面的代码相当于做了一个部分初始化,即数组长度为3,第一个元素是a,第二个元素和第三个元素都被默认置成尾0了.
2、输入输出
另外我们也可以使用gets函数来输入获取一个str,只不过这个函数之前说过不太安全,所以要慎用嗷。
之前的scanf和printf也可以用在字符数组的输入输出中:
但是scanf依然存在的问题是其不能输入一个带有分隔符的串,这一点使用的时候要注意。
连续写入的方式:
输出效果:
3、常用函数
使用字符串相关的函数的时候 ,要包含头文件string.h。
使用这些函数可以给我们使用字符数组的时候带来方便。
如下:
strlen 和 sizeof
strcpy 和 strncpy
strcat 和 strncat
strcmp 和 strncmp
1、 strlen 和 sizeof
strlen返回当前字符数组的长度:
而sizeof返回的是当前数据所占内存的字节数大小,我们来测试一下:
字符串长度为5在strlen函数的说明中就已经说过该函数计算字符串长度时不会加上尾0,所以自然为5,而字节数为6则证明了尾0也是一个字符,也要在内存中占据一个字节的大小嗷。
为了加深印象,再来一个例子:
可以看见strlen计算长度是依靠尾0的,上图程序中计算长度只到尾0就结束了,所以长度为5,而字节数可以看到为10,因为有两个尾0的存在。
2、 strcpy 和 strncpy
之前说过数组名在被初始化之后是不可以作为左值在赋值号左边出现的,那我们想给一个已经初始化过的串进行赋值怎么办?
就可以使用strcpy:
来测试一下:
而对于strncpy也是差不多的,它只是多了一个拷贝数量大小的参数,该参数表示要从源串拷贝多少字符到目标串中,一种最佳实践是将该参数设置为与目标串一样的大小,这样可以防止数组越界异常:
3、strcat 和 strncat
strcat的作用是连接:
很明显就是把两个串接起来嘛,来测试一下:
而strncat也是类似的,只是多了一个数量参数,其表示最多从源串里面取该参数数量个字符连接到目标串中。
如果源串字符数量不足该参数数量个字符,则取到尾0为止。
4、strcmp 和 strncmp
strcmp用来进行比较:
如果没有该函数的话,我们进行字符串的比较将是非常困难的(或者说是麻烦的)。
我们来测试一下:
可以看到如果相等的话返回的就是0,如果不相等的话,将返回两个串中首个不相匹配的asc码的差值,如上图程序中h的asc码为104,而w的asc码为119,str1减去str2正好为-15,所以不相等,相等的话返回值为0。
strncmp也是类似,多一个数量参数n,其表示只比较这两个串的前n位的字符是否相等。
指针
变量与地址的关系
什么是变量,什么又是地址?
先来看一行代码:
cpp
int i = 1;
这是一句变量的声明与定义(或者说定义变量与初始化,前面那是C++里的叫法)。
我们测试过,如果打印其地址和值的话,有如下:
cpp
&i = 0x21312321
i = 1
这表示了在当前0x21312321这块地址空间存放了 1这样一个值:
这相当于有一块内存地址空间,我们使用0x21312321这个标记来标识了这块内存地址空间,然后在里面存放了 1这个值(补码形式),这块空间的内容改成10、100 、1000都是没问题的。所以变量与地址的概念自然就出来了:
变量其实就是给用户用的,它是当前对某一块内存空间的抽象命名,我想把这块内存空间存储的内容修改成100就让 i = 100,而这块内存空间有一个绝对的名字(比如0x123213)就叫地址,这是给编译器用的,编译器通过它来查找这块内存空间从而进行对其内容的读写操作。
而我们所谓的指针其实就是地址,也就是说指针等价于地址。
指针与指针变量
那么啥又是指针和指针变量呢?
比方说有一个整形数3,我们现在想把这个3保存起来是不是就得用到一个整形变量,那么同理,假如有一个地址值,我们想把这个地址值给保存起来,此时就需要一个地址变量,又因为之前说过其实地址就是指针,所以就有了指针变量,我们存放进该变量中的地址值就是指针。
所以我们日常说的"让某指针指向哪里指向哪里"严格地说是不对的,因为指针本身就是地址,是一个常量是不能做左值的,我们所谓的改变指向其实指的是指针变量所存放的指针被改变了,比如某指针变量存放的是0x2000,那么该指针变量就指向0x2000,若改变了存放的变成了0x3000那么该指针变量就指向0x3000,是这个意思。
继续来深入,看一下嵌套的指针之间是什么关系。
cpp
int i=1;
还是先定义并初始化一个变量 i,其值为1,这句代码表示在内存当中OS将会分配一块内存空间给这个变量 i,其地址假设为0x1000:
现在我们定义一个p指针,该指针用来指向变量 i 的地址:
cpp
p = &i;
但很明显缺少一些东西是吗,i的地址是一个指针,所以我们需要一个变量,已经设置为p了,为了能够保存指针数据,说明该变量必须为指针变量:
cpp
int* p = &i;
这样是不是完整了?指针变量p保存了整形变量 i的地址值,但是只要是变量就有地址嗷,假设指针变量 p地址为0x2000:
现在有趣的事情来了,既然指针变量也有地址,我们是不是依然可以继续存放?
答案是肯定的:
cpp
q = &p;
还是分解着来看,对于指针变量p的地址值,我们应该使用什么类型去保存该地址呢?多加颗星就好了:
cpp
int** q = &p;
int*表示指针变量,然后int**就表示指针变量的地址值啦:
从图上明显可以看出来,我们依然可以推出这个二级指针肯定也是有其地址值的,上图假设为0x3000.
总结:
i 的值为 1;
&i 的值为0x1000;
p 的值为0x1000;
&p 的值为0x2000;
*p 的值为1;
q 的值为 0x2000;
&q 的值为 0x3000;
*q 的值为0x1000,也就是&i
**q 的值为1,也就是*(&i),为1
有了上面这些关系之后,我们想要访问变量 i就可以有两种方式来访问:
1、通过 i
2、通过 *p
这就引出了直接访问和间接访问的概念。
直接访问与间接访问
我们通过 i 来访问改变内存空间就称为直接访问;
而通过指针变量 p 的关联作用来访问改变变量 i 的值则称为间接访问(或者说一级间接访问,因为我们通过q也能够间接访问到i,这就成了二级间接访问)。
空指针与野指针
空指针:
cpp
int* p = NULL;
指针定义出来,但是还不清楚它具体要指向谁怎么用,此时就可以先将它指向NULL表示空,这个NULL 是define出来的一个宏,值为0,也就是在让指针指向起始地址为0的一块的空间,这样做的好处是如果指针进行了非法操作我们能够得到一个段错误的提示。
因为我们在当前系统上规定了0号这块空间不分配给任意一个进程,所以如果我们企图去写这样一块空间的话系统当然会给我们报错。
我们把指针置为空是为了防止野指针的产生,什么是野指针?
野指针的意思某一个指针的指向不确定或者说压根就没有指向,然而我们直接使用了这个指针,这就很可能会造成严重的问题。
空类型的指针
所谓空类型的指针就是指 void*;
对于void* ,任何类型的指针值都能够把自己的值赋值给void*,void也能够把当前自己的值赋值给任何类型的指针。
有一种特殊情况,void和一个函数指针之间来互相赋值就这一种情况是在C99标准当中没有定义的,也就是未定义行为。
定义与初始化的书写规则
这些在上面的讲解中都是写过了的,比较简单这里就不再赘述。
指针运算
指针所能执行的运算有:* 、 & 、关系运算(如比大小,指针比的是地址的大小,即两个指针的地址值的高低;还有比如++、--运算等)。
指针与数组的关系
指针与一维数组
这个就没啥好说的,因为之前已经说过很多了。
其实不难推出还有如下关系:&a[i] = a+1 = p+1 = &p[i],编译运行如下:
可以看到使用指针和数组名的效果是等价的。
实际上从上面的代码中我们也可以看出,事实上数组和指针都是可以互换着使用的,那么数组与指针唯一的区别是什么呢?
唯一的区别就是:数组名是常量,而上面的指针 p 是个变量。
像上图这么写,是不是看起来和数组别无二致,只不过是个匿名数组罢了,因为其没有数组名。
指针与二维数组
上面p = a为什么是错的问题在数组指针与指针数组那一节会进行讲解。
指针与字符数组
这个和我们之前说的其实没什么不同,有不同的地方主要在下面一些地方:
可以看到我们打印其sizeof所占字节数的时候为8,这是指针所占字节数(64位环境),然后长度为5这个和字符数组是一样的。
我们继续看:
现在我们试图对指针进行和对字符数组一样的拷贝操作,会发现存在段错误:
这是因为对于字符数组而言(上面代码没写,但是之前是有写过的,自己脑部一下叭),是把"world"的内容拷贝到str的地址以及其后面的地址中,对于字符数组的sizeof我们可以得到其所占字节数为6(hello+一个尾0字符)个字节,所以拷贝过程其实是用w覆盖h,o覆盖e,以此类推,正因为字符数组本身就是有内容的我们只不过是用strcpy函数来进行覆盖写所以对于字符数组使用strcpy没有问题。
而我们使用指针形式的strcpy报段错误,是因为使用"world"覆盖到的str,其所指向的是一个字符串常量,企图使用"world"去覆盖一个串常量这肯定是错误的,串常量在当前的存储位置是特殊的,在使用上不允许被改变和覆盖。
其实我思考了一下可能和内存分配有关系,对于字符数组形式的hello,是一串连续的地址空间来依次存放char类型的字符:
而对于指针形式的hello,则是单独使用一块内存空间来存放整个字符串:
正是因为这种存储方式不同的原因,所以对于字符数组进行copy写覆盖的时候一一对应是很容易做到的,对于字符串常量这种则不然,因为各个字符都并到一起了还怎么进行单个单个的写覆盖,所以必然报错。
那我们还是想改变指针str的指向内容咋办,直接改指向嘛:
cpp
str = "world";
相当于此时开辟了另一块空间来存放world这个字符串,然后指针str指向该空间即可。
const与指针
指针常量和常量指针已经说过太多了,不再赘述。
指针数组与数组指针
数组指针:本质是一个指针,指向一个数组,定义方式如下;
cpp
【存储类型】 数据类型 (*指针名)[下标];
[auto] int (*p)[3];
这样的写法其实不好理解,我们可以将定义方式抽象成 type name 这样的风格来想(但别这样写,编译器不认嗷),首先我们需要一个指针p,那么这个指针的数据类型是什么,那不是就是int[3]*吗,所以数组指针就是:int[3]* p;
这样的含义是数组指针p指向了一个 元素为三个的数组 的这种数据类型的起始位置,原来为int的时候指针p做+1操作指的是移动一个int类型的地址大小,而现在变成int[3]之后,指针p+1则表示的是一下移动三个int类型的地址大小(联系我们之前说的二维数组中的行指针就好懂了)。
这和我们上面在二维数组那一节提到的数组名a(a是二维数组的行指针)是一样的,数组名a其实就是一个数组指针,而p就是一个普通的指针,所以我们直接让p = a是错误的,指针类型无法匹配。
指针数组:本质是一个数组,这个数组存的是指针,定义方式如下:
cpp
【存储类型】 数据类型* 数组名[长度];
int* arr[3];
把数组指针的括号给去掉就成了指针数组了。
来简单测试一下:
多级指针
多级指针其实之前也提过了(还记得之前的二级指针吗),掌握到二级指针就已经够用了,这里不再赘述。
函数
函数的定义
定义格式如下:
cpp
数据类型 函数名(【数据类型 形参名,数据类型 形参名, ...】);
最经典的例子就是我们的main函数:
除了main函数之外,其它的函数如下图:
main函数上面的叫函数声明,函数下面的叫函数定义(实现)。
函数的传参
值传递
这个之前也说过太多了,不再赘述。
地址传递
不再赘述。
函数的调用
函数的嵌套调用
递归
不再赘述。
函数与数组
函数与一维数组
先来看一维数组传参与不传参时的一个小差异:
可以看到main函数中的数组长度为20,这是很自然的,因为int类型在32位机器上占四个字节,那五个int类型的数肯定占20个字节。
而对于传递给函数的形参数组名来说则意义不一样了,函数的形参只不过是用指针类型来接收了一个地址值(数组名就是个地址),所以在32位机器上一个指针类型的变量占8个字节,这是二者打印出来的sizeof大小不同的原因。
所以这就存在一个问题,在数组传参时,拥有形参的函数由于只拥有该数组的起始地址,所以它并不知道这个数组有多大,甚至它根本不知道这是一个数组,所以传递数组的时候,还应该传递其数组长度:
另外接收数组形参的时候,除了上述写法,也还有另外一种方式:
cpp
void printf_arr(int p[],int n);
但注意这种形式的写法和int* p是一样的,也就是说这种写法在定义时和形参时所代表的含义是不一样的,在形参时这代表就是一个指针,在定义的时候则表示其为一个数组。
函数与二维数组
这种写法不难理解,就是将这个3行4列的数组拿来当一个大数组来使用了,这种写法中的实参还可以写成*a,a[0],*(a+0)等。
如果还是希望以行列形式来打印的话,可以使用之前说过的数组指针的形参形式来接收实参:
可以看到正常打印输出,另外我们还能注意到在main函数中a的大小为48字节,这是因为12个整形元素在32位系统环境下就是12*4=48个字节,而对于p来说,还是和之前一样的,p是一个数组指针,本质是一个指针指向了一个拥有三个整形元素的数组,因为一个指针在32位系统环境下占8个字节,所以其打印出来结果为8。
和一维数组一样,我们依然可以写成下面这种形参形式:
cpp
void printf_arr1(int p[][N],int m,int n);
但这其实本质上就是一个数组指针,其所占字节数依然为8。
函数与字符数组
函数与指针
指针函数
指针函数本质是个函数,只不过返回值是个指针,定义形式为:
cpp
返回值* 函数名(形参列表);
如:
int* fun(int i);
函数指针
函数指针的本质是一个指针,其指向一个函数,定义形式为:
cpp
类型 (*指针名)(形参列表);
如:
int (*p)(int i);
函数指针数组
在上面程序的基础上,我们还能再写一个减法的函数,然后用函数指针来引用:
我们可以看到,函数指针p和q长得一模一样,那我们是不是可以搞一个函数指针数组来存储这两个指针呢?
当然可以,函数指针数组的定义形式如下:
cpp
类型 (*数组名[下标])(形参列表);
如:
int (*arr[N])(int i);
这个表达式意思是:arr是一个数组,数组当中有N个元素,这N个元素都是指向一个只具有一个int形参的函数的指针。
那么上面的程序就可以改写成:
在这些基础上,还有更离谱的套娃概念:
指向指针函数的函数指针数组:
cpp
int *(*funcp(N))(int);
构造类型
结构体
类型描述
形式如下:
cpp
struct 结构体名{
数据类型 成员1;
...
};
定义结构体时要注意,大括号后的分号不能丢,另外结构体作为一种类型描述是不占用任何存储空间的,所以无法在说明完成员数据之后就直接接等号给其赋值进行初始化(没有任何存储空间那初始化肯定就没有意义呀)。
嵌套定义
定义变量(变量、数组、指针),初始化以及成员引用
对于成员变量向上面这样赋值即可,引用则使用变量名.成员名的方式。
嵌套类型的定义初始化以及成员引用如上图。
我们还可以只初始化部分结构体中的元素内容:
如果是指针的话,那么需要用指针->成员名的方式来进行成员引用(也可以(*指针).成员名),简单示例:
数组形式的结构体:
结构体在内存当中所占用的内存大小
先来看这样一个例子:
分析一下,在64位系统下我们知道指针变量是占8个字节的,但是为什么结构体变量会是12个字节呢?
int 占4个字节float占4个字节,char占一个字节,加起来也就9个呀?这会和变量声明的顺序有关吗,我们将float与char互换顺序会发现一样是12个字节。
再加一个char呢:
结果依然不变,但是我们再变化一下,让一个char型放到float下面去:
会发现我们的结构体所占字节数竟然飙升到了16个,这是为什么呢?
这是因为结构体具有一个对齐情况。
这是由硬件决定的,因为硬件存储有不同的形式,如半字存储、字存储、双字存储等,为了方便硬件进行指令操作所以结构体这种变量天然的就存在一个地址对齐的机制。当然我们不学硬件的话很难从硬件角度来思考这个问题,所以我们从软件角度笼统的分析一下。
对于上面的示例程序,第一个int变量就相当于一个标尺这样的概念(这实际上取决你当前所使用的机器字长),如果是int类型的整倍数那么没有问题,该咋存就咋存,比如double。但是如果当前数据类型的存储大小比int值要小,那么此时要进行地址偏移,也就是说存完了当前变量之后要跳过多少地址去存储下一个变量。
所以int占四个字节,然后char型虽然只占一个字节但是不满四个字节也要占4个字节,float也要占四个字节所以共12个字节。
我们刚刚还试过:
cpp
int i;
char ch;
char ch1;
float f;
这种形式,会发现也只占12个字节,这是因为ch变量占的四个字节还可以容纳下一个char型一字节变量,所以总共还是十二字节。
但是当我们将一个char类型放到最下面的时候:
cpp
int i;
char ch;
float f;
char ch1;
此时占16个字节,知道为什么了吧?因为最下面的ch1变量也要占四个字节,所以共16个字节。
这是存在地址对齐的情况,如果我们不想让编译器对其进行地址对齐,那么我们可以在结构体上使用下面的语法:
此时可以发现该结构体所占内存数大小就正常了:
这里还要再聊一个函数传递结构体类型参数的问题,不难推敲出,如果使用值传递的话,那么对应函数的形参将也要开辟出和实参对应的字节数空间大小(上面的例子就是十二个字节),这很浪费空间,但是传递指针的话在64位系统环境只占8个字节,所以使用地址传递能够减少不必要的内存消耗。
共用体
cpp
union 共用体名{
数据类型 成员名1;
数据类型 成员名2;
...
};
简单的示例:
对于共用体来说,共用体实际上就是一块空间,空间大小按照成员变量中最大的来决定,这块空间谁爱用谁,不用就滚蛋。
因为此时成员变量中最大的是double类型为8个字节,所以其共用体大小为8,我们将double类型删去,则其共用体大小应该为4(即 int类型的字节数):
所以这也就意味着,哪个成员要使用这块空间就指使用哪个成员,千万别混着用,不然会出现很多问题(因为各个类型的存储方式不同):
可以看到,只有 i 是正常输出的,对于另外两个值我们并没有进行定义,但依然输出了值是因为此时这块共用体空间的四个字节已经被写入了整形类型的100的补码,所以当使用其它类型来输出的时候就出现了该补码所对应的该数据类型的值,这再次印证了共用体的所有成员变量所使用的空间是同一块,并且这块空间的大小为这个共同体中数据类型所占字节数最大的类型的字节数。
如果是指针的话也和结构体一样,可以使用箭头来引用成员变量:
其它的内容共用体和结构体是一样的,只需要注意一下上面所说的和结构体有区别的地方,还有一个部分也不太一样,是共用体中的位域概念。
值得注意的是,结构体和共用体是可以相互嵌套定义的。
位域
这个机制不是搞硬件的话并不重要,知道有这回事就好了。
它是共用体的另一种形式,与共用体的区别就是它存放变量时并不以字节为单位,而是以位为单位:
上图中表示char型的a只占一个位,b占两位,c占一位,也就是说上图中的union所占字节数为一个字节(因为char类型的变量y占一个字节,比结构体中的各个变量abc都大)。
不懂也没关系,这个没啥用。
枚举
cpp
enum 标识符{
成员1;
成员2;
...
};
示例:
可以看到,对于枚举结构来说,如果不赋初始值的话,那么默认从上往下是从0开始往后依次赋值的。
如果对其中一个初始化,那么顺下的都会依次自动加1:
如果是初始化了中间的某个值,则又从该值开始向后依次+1进行排列:
动态内存管理
动态内存管理一般原则:谁申请谁释放。
malloc
对于malloc函数,输入参数是需要多少个字节空间,然后该函数从堆空间上返回这块空间的起始地址,其类型为void*,表示可以是任意类型的指针来接收它。
calloc
对于calloc函数,其参数有两个,表示若某一个数据成员是size个字节大小,则该函数从堆空间上申请一块能存放nmemb个这种类型的数据成员的空间地址返回给函数的调用者。
realloc
对于realloc函数,其表示需要重新分配一块动态内存空间,参数有两个,从man手册上可以知道参数 ptr 必须是一个已经由malloc或者calloc分配后返回的指针,所以这个函数的含义就是其原来已经由malloc或者calloc函数分配过一块空间,起始地址为ptr,但是现在空间大小不太合适了,此时就需要使用realloc进行重新分配:从ptr这个地址开始,我要size个字节的空间。
比如一开始我们申请了100个字节的空间,但是用着用着发现不够用了,此时使用realloc函数申请原来的空间变为300个字节大小,那么realloc函数将会从原来的地址开始继续往下再连续分配200个字节以补足300个字节,然后返回这300字节大小空间的起始地址,为什么还需要返回起始地址?因为有时候如果原始的地址已经没有(或者说不满足需要)足够的连续空间的话,realloc函数将从其它位置寻找一块能够满足所需要的连续空间地址,此时新分配的地址与原地址不同,所以需要返回新空间的起始地址。
free
对于free函数,我们都知道是对一块已经分配了的空间进行释放,但是我们要正确理解这个释放:
1、free了之后的空间不能再使用
2、free了之后的空间并非不存在了,依然存在
我们的free并没有把原来使用的这块内存给扣掉,也没有把内存清掉,甚至指针的指向还可能指向该位置,但它一定是个野指针。
free后只是代表着我们的指针对于某一块内存空间不再具有指向或者说引用权限了,此时该空间被系统回收,而指针就没有了指向成为了一个野指针。
来验证一下这个事情:
可以看到我们在free掉p之后,依然可以对该指针进行操作,然后打印出来的 p地址还是原来free之前的地址,我们依然可以对其进行读写操作,这是很危险的事情,说不定该指针就指到了哪些空间程序就会出问题,所以一种最佳实践是,在free掉之后又暂时不知道该指针要指向哪里,那么就先将其置空:
可以看到此时就会出现段错误,有效终止了问题的扩大化。
typedef关键字的使用
typedef是为已有的数据类型改名。
cpp
typedef 已有数据类型 新名字;
简单使用一下:
这样做的好处是,如果以后我想改变 i的数据类型,那么我只要去typedef的位置改变int类型为我想要的类型即可,非常的方便。
那么这与我们的#define宏定义有什么区别?看起来似乎是一样的。
区别存在于复合类型上的一些声明上:
比如int这种复合类型,从上图我们可以看出,在连续定义两个指针变量p和q时,因为宏定义只是简单的文本替换,所以对于q来说它只会定义一个整形变量,而对于typedef来说则会严格的定义一个int的指针变量q。
typedef还可以给如下形式改名:
还可以给函数进行typedef: