C语言进阶

数组

在基础篇说过,数组实际上是构造类型之一,是连续存放的。

一维数组

定义

定义格式:[存储类型] 数据类型 数组名标识符[下标];
下面分模块来介绍一下数组的定义部分的内容。

1、初始化和元素引用:

在这里插入图片描述
在这里插入图片描述

可以看到数组是连续存储的,然后因为每个int都是四个字节,所以存储结构一目了然。

2、 数组名的本质与数组越界异常

从上面的程序例子中我们可以看到,我们在打印数组名的时候使用的是%p的格式来打印,这表示arr实际上是数组的起始地址。
怎么理解呢?意思就是arr这个标识符名字表示的就是一个地址的常量,这也就是说arr这个标识符不可以无条件的出现在等号的左边(因为这意味着会被修改,定义的时候那不叫修改嗷,那叫初始化,是合法的)。

再来探究一下数组到底是怎么往元素里面写值的,其实这个过程涉及到了指针偏移的概念。
来看这么个例子:
在这里插入图片描述
在这里插入图片描述
我们居然打印出来了arr[3]的值,但是很明显这不是越界了吗?这数组是怎么找到arr[3]的呢?

其实这都是指针偏移造成的,这也是为什么说数组存取速度快的原因,当取a[i]值的时候,其对应的是依赖 a[i] =*(a + i) 即使用数组名加上 i 个偏移地址来取得的,所以当我们取a[3]的时候,尽管已经越界,但是我们依然能够通过偏移量 3和数组名 a来依靠指针取得正确的值(注意这个在某些优化后的编译器是肯定会报越界异常的,只不过这里gcc更加开放而已嗷)。

二维数组

我们将从以下几点来说明二维数组。
1、定义以及初始化
格式为:[存储类型] 数据类型 标识符 [行下标][列下标];

初始化时行标是可以没有的,因为靠列标也能知道会分成几行呀。

#define M 2
#define N 3

int arr[M][N];

上面的程序就定义了一个二行三列的行列式,其从人类的逻辑上理解是下图这样的:
在这里插入图片描述
也就是一个方格的形式,但实际上其在计算机中存储依然是线性的一个数组:
在这里插入图片描述
也就是说第一行和第二行是连在一起的,第一行的最后一个元素地址再偏移一个就到了第二行行首的位置。

BTW,二维数组不过是一维数组的一个拓展,所以一维数组的特性二维数组都有,比如如果不初始化的话都是数组元素都是随机值,除了可以动态赋值之外,二维数组也可以进行静态初始化和部分初始化:

//动态赋值
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、深入理解二维数组

我们在上面用过这样的初始化语句:

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位的字符是否相等。

在这里插入图片描述

在这里插入图片描述

指针

变量与地址的关系

什么是变量,什么又是地址?
先来看一行代码:

int i = 1;

这是一句变量的声明与定义(或者说定义变量与初始化,前面那是C++里的叫法)。

我们测试过,如果打印其地址和值的话,有如下:

&i = 0x21312321
i = 1

这表示了在当前0x21312321这块地址空间存放了 1这样一个值:
在这里插入图片描述

这相当于有一块内存地址空间,我们使用0x21312321这个标记来标识了这块内存地址空间,然后在里面存放了 1这个值(补码形式),这块空间的内容改成10、100 、1000都是没问题的。所以变量与地址的概念自然就出来了:

变量其实就是给用户用的,它是当前对某一块内存空间的抽象命名,我想把这块内存空间存储的内容修改成100就让 i = 100,而这块内存空间有一个绝对的名字(比如0x123213)就叫地址,这是给编译器用的,编译器通过它来查找这块内存空间从而进行对其内容的读写操作。

而我们所谓的指针其实就是地址,也就是说指针等价于地址。

指针与指针变量

那么啥又是指针和指针变量呢?
比方说有一个整形数3,我们现在想把这个3保存起来是不是就得用到一个整形变量,那么同理,假如有一个地址值,我们想把这个地址值给保存起来,此时就需要一个地址变量,又因为之前说过其实地址就是指针,所以就有了指针变量,我们存放进该变量中的地址值就是指针。

所以我们日常说的”让某指针指向哪里指向哪里“严格地说是不对的,因为指针本身就是地址,是一个常量是不能做左值的,我们所谓的改变指向其实指的是指针变量所存放的指针被改变了,比如某指针变量存放的是0x2000,那么该指针变量就指向0x2000,若改变了存放的变成了0x3000那么该指针变量就指向0x3000,是这个意思。

继续来深入,看一下嵌套的指针之间是什么关系。

int i=1;

还是先定义并初始化一个变量 i,其值为1,这句代码表示在内存当中OS将会分配一块内存空间给这个变量 i,其地址假设为0x1000:
在这里插入图片描述

现在我们定义一个p指针,该指针用来指向变量 i 的地址:

p = &i;

但很明显缺少一些东西是吗,i的地址是一个指针,所以我们需要一个变量,已经设置为p了,为了能够保存指针数据,说明该变量必须为指针变量:

int* p = &i;

这样是不是完整了?指针变量p保存了整形变量 i的地址值,但是只要是变量就有地址嗷,假设指针变量 p地址为0x2000:
在这里插入图片描述
现在有趣的事情来了,既然指针变量也有地址,我们是不是依然可以继续存放?
答案是肯定的:

q = &p;

还是分解着来看,对于指针变量p的地址值,我们应该使用什么类型去保存该地址呢?多加颗星就好了:

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,这就成了二级间接访问)。

空指针与野指针

空指针:

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的指向内容咋办,直接改指向嘛:

str = "world";

相当于此时开辟了另一块空间来存放world这个字符串,然后指针str指向该空间即可。

const与指针

指针常量和常量指针已经说过太多了,不再赘述。

指针数组与数组指针

数组指针:本质是一个指针,指向一个数组,定义方式如下;

【存储类型】 数据类型 (*指针名)[下标];
[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是错误的,指针类型无法匹配。

指针数组:本质是一个数组,这个数组存的是指针,定义方式如下:

【存储类型】 数据类型* 数组名[长度];
int* arr[3];

把数组指针的括号给去掉就成了指针数组了。
来简单测试一下:
在这里插入图片描述
在这里插入图片描述

多级指针

多级指针其实之前也提过了(还记得之前的二级指针吗),掌握到二级指针就已经够用了,这里不再赘述。

函数

函数的定义

定义格式如下:

数据类型 函数名(【数据类型 形参名,数据类型 形参名, ...);

最经典的例子就是我们的main函数:
在这里插入图片描述
除了main函数之外,其它的函数如下图:
在这里插入图片描述

main函数上面的叫函数声明,函数下面的叫函数定义(实现)。

函数的传参

值传递

在这里插入图片描述
这个之前也说过太多了,不再赘述。

地址传递

在这里插入图片描述
不再赘述。

函数的调用

函数的嵌套调用

在这里插入图片描述

递归

不再赘述。

函数与数组

函数与一维数组

先来看一维数组传参与不传参时的一个小差异:
在这里插入图片描述
在这里插入图片描述
可以看到main函数中的数组长度为20,这是很自然的,因为int类型在32位机器上占四个字节,那五个int类型的数肯定占20个字节。
而对于传递给函数的形参数组名来说则意义不一样了,函数的形参只不过是用指针类型来接收了一个地址值(数组名就是个地址),所以在32位机器上一个指针类型的变量占8个字节,这是二者打印出来的sizeof大小不同的原因。

所以这就存在一个问题,在数组传参时,拥有形参的函数由于只拥有该数组的起始地址,所以它并不知道这个数组有多大,甚至它根本不知道这是一个数组,所以传递数组的时候,还应该传递其数组长度:
在这里插入图片描述
在这里插入图片描述
另外接收数组形参的时候,除了上述写法,也还有另外一种方式:

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。

和一维数组一样,我们依然可以写成下面这种形参形式:

void printf_arr1(int p[][N],int m,int n);

但这其实本质上就是一个数组指针,其所占字节数依然为8。

函数与字符数组

在这里插入图片描述
在这里插入图片描述

函数与指针

指针函数

指针函数本质是个函数,只不过返回值是个指针,定义形式为:

返回值* 函数名(形参列表);
如:
int* fun(int i);

函数指针

函数指针的本质是一个指针,其指向一个函数,定义形式为:

类型 (*指针名)(形参列表);
如:
int (*p)(int i);

在这里插入图片描述
在这里插入图片描述

函数指针数组

在上面程序的基础上,我们还能再写一个减法的函数,然后用函数指针来引用:
在这里插入图片描述

在这里插入图片描述
我们可以看到,函数指针p和q长得一模一样,那我们是不是可以搞一个函数指针数组来存储这两个指针呢?
当然可以,函数指针数组的定义形式如下:

类型 (*数组名[下标])(形参列表);
如:
int (*arr[N])(int i);

这个表达式意思是:arr是一个数组,数组当中有N个元素,这N个元素都是指向一个只具有一个int形参的函数的指针。

那么上面的程序就可以改写成:
在这里插入图片描述
在这里插入图片描述
在这些基础上,还有更离谱的套娃概念:
指向指针函数的函数指针数组:

int *(*funcp(N))(int);

构造类型

结构体

类型描述

形式如下:

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个字节。

我们刚刚还试过:

int i;
char ch;
char ch1;
float f;

这种形式,会发现也只占12个字节,这是因为ch变量占的四个字节还可以容纳下一个char型一字节变量,所以总共还是十二字节。

但是当我们将一个char类型放到最下面的时候:

int i;
char ch;
float f;
char ch1;

此时占16个字节,知道为什么了吧?因为最下面的ch1变量也要占四个字节,所以共16个字节。

这是存在地址对齐的情况,如果我们不想让编译器对其进行地址对齐,那么我们可以在结构体上使用下面的语法:
在这里插入图片描述
在这里插入图片描述
此时可以发现该结构体所占内存数大小就正常了:
在这里插入图片描述

这里还要再聊一个函数传递结构体类型参数的问题,不难推敲出,如果使用值传递的话,那么对应函数的形参将也要开辟出和实参对应的字节数空间大小(上面的例子就是十二个字节),这很浪费空间,但是传递指针的话在64位系统环境只占8个字节,所以使用地址传递能够减少不必要的内存消耗。

共用体

union 共用体名{
	数据类型 成员名1;
	数据类型 成员名2;
	...
};

简单的示例:
在这里插入图片描述
对于共用体来说,共用体实际上就是一块空间,空间大小按照成员变量中最大的来决定,这块空间谁爱用谁,不用就滚蛋。

在这里插入图片描述
在这里插入图片描述
因为此时成员变量中最大的是double类型为8个字节,所以其共用体大小为8,我们将double类型删去,则其共用体大小应该为4(即 int类型的字节数):
在这里插入图片描述
所以这也就意味着,哪个成员要使用这块空间就指使用哪个成员,千万别混着用,不然会出现很多问题(因为各个类型的存储方式不同):
在这里插入图片描述
在这里插入图片描述
可以看到,只有 i 是正常输出的,对于另外两个值我们并没有进行定义,但依然输出了值是因为此时这块共用体空间的四个字节已经被写入了整形类型的100的补码,所以当使用其它类型来输出的时候就出现了该补码所对应的该数据类型的值,这再次印证了共用体的所有成员变量所使用的空间是同一块,并且这块空间的大小为这个共同体中数据类型所占字节数最大的类型的字节数。

如果是指针的话也和结构体一样,可以使用箭头来引用成员变量:
在这里插入图片描述

其它的内容共用体和结构体是一样的,只需要注意一下上面所说的和结构体有区别的地方,还有一个部分也不太一样,是共用体中的位域概念。

值得注意的是,结构体和共用体是可以相互嵌套定义的。

位域

这个机制不是搞硬件的话并不重要,知道有这回事就好了。
它是共用体的另一种形式,与共用体的区别就是它存放变量时并不以字节为单位,而是以位为单位:
在这里插入图片描述

上图中表示char型的a只占一个位,b占两位,c占一位,也就是说上图中的union所占字节数为一个字节(因为char类型的变量y占一个字节,比结构体中的各个变量abc都大)。

不懂也没关系,这个没啥用。

枚举

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是为已有的数据类型改名。

typedef 已有数据类型 新名字;

简单使用一下:
在这里插入图片描述

这样做的好处是,如果以后我想改变 i的数据类型,那么我只要去typedef的位置改变int类型为我想要的类型即可,非常的方便。

那么这与我们的#define宏定义有什么区别?看起来似乎是一样的。

区别存在于复合类型上的一些声明上:
在这里插入图片描述
比如int这种复合类型,从上图我们可以看出,在连续定义两个指针变量p和q时,因为宏定义只是简单的文本替换,所以对于q来说它只会定义一个整形变量,而对于typedef来说则会严格的定义一个int的指针变量q。

typedef还可以给如下形式改名:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
还可以给函数进行typedef:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/136440.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第六章 DNS域名解析服务器

1、DNS简介 DNS&#xff08;Domain Name System&#xff09;是互联网上的一项服务&#xff0c;它作为将域名和IP地址相互映射的一个分布式数据库&#xff0c;能够使人更方便的访问互联网。 DNS系统使用的是网络的查询&#xff0c;那么自然需要有监听的port。DNS使用的是53端口…

【Python】python读取,显示,保存图像的几种方法

一、PIL&#xff1a;Python Imaging Library&#xff08;pillow&#xff09; PIL读取图片不直接返回numpy对象&#xff0c;可以用numpy提供的函数np.array()进行转换&#xff0c;亦可用Image.fromarray()再从numpy对象转换为原来的Image对象&#xff0c;读取&#xff0c;显示&…

【OpenCV实现图像:用OpenCV图像处理技巧之白平衡算法2】

文章目录 概要Gray-world AlgotithmGround Truth Algorithm结论&#xff1a; 概要 随着数字图像处理技术的不断发展&#xff0c;白平衡算法成为了图像处理中一个关键的环节。白平衡的目标是校正图像中的颜色偏差&#xff0c;使得白色在图像中呈现真实的白色&#xff0c;从而提…

transfomer模型——简介,代码实现,重要模块解读,源码,官方

一、什么是transfomer Transformer是一种基于注意力机制&#xff08;attention mechanism&#xff09;的神经网络架构&#xff0c;最初由Vaswani等人在论文《Attention Is All You Need》中提出。它在自然语言处理&#xff08;NLP&#xff09;领域取得了巨大成功&#xff0c;特…

虚拟机CentOS 8 重启后不能上网

情况说明&#xff1a;原本虚拟机是可以上网的&#xff0c;然后嘚一下&#xff0c;重启后&#xff0c;连接不上网络&#xff0c;完了&#xff0c;上网查找一堆质料&#xff0c;我的连接方式是桥接模式&#xff08;复制物理网络连接状态&#xff09;。 好&#xff0c;有人说是vmn…

Git基本概念和使用方式

Git 是一种版本控制系统&#xff0c;用于管理文件版本的变化。以下是其基本概念和使用方式&#xff1a; 仓库&#xff08;repository&#xff09;&#xff1a;Git 存储代码的地方&#xff0c;可以理解为一个项目的文件夹。提交&#xff08;commit&#xff09;&#xff1a;Git …

【Go入门】struct类型

【Go入门】struct类型 struct Go语言中&#xff0c;也和C或者其他语言一样&#xff0c;我们可以声明新的类型&#xff0c;作为其它类型的属性或字段的容器。例如&#xff0c;我们可以创建一个自定义类型person代表一个人的实体。这个实体拥有属性&#xff1a;姓名和年龄。这样…

前端开发引入element plus与windi css

背景 前端开发有很多流行框架&#xff0c;像React 、angular、vue等等&#xff0c;本文主要讲vue 给新手用的教程&#xff0c;其实官网已经写的很清楚&#xff0c;这里再啰嗦只是为了给新手提供一个更加简单明了的参考手册。 一、打开element plus官网选则如图所示模块安装命令…

c语言练习第11周(1~5)

数列 1 1 2 3 5 8 13 21 ... 被称为斐波纳数列。 输入若干个正整数N&#xff0c;输出这个序列的前 N 项的和。 题干数列 1 1 2 3 5 8 13 21 ... 被称为斐波纳数列。 输入若干个正整数N&#xff0c;输出这个序列的前 N 项的和。输入样例3 5 4 1输出样例…

【第七章】软件设计师 之 程序设计语言与语言程序处理程序基础

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 1、前言 正规式 2、编译过程 编译型&…

【操作系统】4.2 文件系统

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

如何有效的保护Windows登录 安当加密

为了有效保护Windows安全登录&#xff0c;以下是一些建议&#xff1a; 使用强密码&#xff1a;强密码是保护Windows登录安全的重要措施之一。确保密码包含大写字母、小写字母、数字和特殊字符&#xff0c;长度至少为8位&#xff0c;并且不要使用容易猜到的单词或短语。启用多因…

pointnetgpd复现

参考&#xff1a; Installation Instructions — Dex-Net 0.2.0 documentation Install git clone https://github.com/lianghongzhuo/PointNetGPD.git 添加环境变量 gedit ~/.bashrc #添加下面这一行 export PointNetGPD_FOLDER$HOME/code/PointNetGPD #然后source source…

SLAM从入门到精通(SLAM落地的难点)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 在所有的slam算法中&#xff0c;基于反光柱的激光slam和基于二维码的视觉slam是落地最彻底的两种slam方法。和磁条、色带等传统导航方式相比较&…

U-Mail邮件系统三大安全措施,防止信息泄露!

在当信息化高速发展的今天&#xff0c;国内很多企业业务流程对OA系统、CRM系统、ERP系统、邮件系统等办公应用依赖度越来越高。这些办公应用给企业带来便利的同时也伴随着越来越多的信息安全问题&#xff0c;而在日常的办公场景中&#xff0c;由于内部员工非法泄漏或黑客入侵导…

Flowable 外部表单

内置表单需要在每个节点中去配置&#xff0c;当如果多个节点使用同一套表单属性就要配置多次比较麻烦&#xff0c;修改的时候也要修改多次&#xff0c;外部表单可以定义一次&#xff0c;然后其它节点都去引用同一个表单属性。 外部表单需要定义一个.form后缀的文件。 外部表单…

运行pytest时,给出警告 PytestConfigWarning: Unknown config option: result_log

问题&#xff1a;在ini中配置了一些选项后运行pytest&#xff0c;会出现下面的警告信息 解决&#xff1a;在ini中增加配置&#xff1a;addopts -p no:warnings

【Git】的分支与版本

前言 Git 的分支是指将代码库从某一个特定的提交记录开始的一个独立的开发线&#xff0c;也可以理解为是一种代码开发的并行方式。分支在 Git 中的使用非常广泛&#xff0c;它可以让多人在同一个代码库中并行开发&#xff0c;同时也能够很方便地进行代码版本控制和管理。 Git …

Python 多进程多线程

多任务 并发&#xff1a;在一段时间内交替执行多个任务 并行&#xff1a;在一段时间内同事一起执行多个任务 进程 Process 进程&#xff1a;一个程序运行在系统之上&#xff0c; 便称这个程序喂一个运行进程&#xff0c;并分配进程ID方便系统管理。操作系统进行资源分配和调…

【多线程】

文章目录 概念一、线程的生命周期图二、线程的创建方式一方式二线程API线程优先级sleep阻塞守护线程多线程并发安全问题 总结 概念 线程:一个顺序的单一的程序执行流程就是一个线程。代码一句一句的有先后顺序的执行。多线程:多个单一顺序执行的流程并发运行。造成"感官上…