✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第1张图片


目录

一,写在前面

二,数据的存储

1,数据类型介绍

2,类型的基本归类

3,整形在内存中的存储

4,浮点型在内存中的存储

三,指针的进阶

1,字符指针

2,指针数组

3,数组指针的使用

 4,函数指针

5,函数指针数组

6,回调函数

7,指针和数组笔试题

四,字符函数和字符串函数

1,strlen

2,strcpy

3,strcat

4,strcmp

5,strstr

6,memcpy

7,memmove

8,模拟实现上述内存函数与字符串函数

 五,自定义类型:结构体,枚举,联合

1,结构体

2,枚举

3,联合(共用体)

六,动态内存管理

1,为什么存在动态内存分配

2,动态内存函数的介绍

七,C语言文件操作


一,写在前面

说实话,我上一篇写的基础篇能上全站热搜榜一,真的是诚惶诚恐,觉得自己配不上这个榜一,分享内容其实没那么好,得到C站的朋友的认可,有点小开心,这会更加督促我提升自己的水平,提升自己博客的质量,对的起大家的认可。学习本篇之前可以学习我上一篇的博客,有利于本篇更好的理解和学习。点击标题即可跳转到相应博文哟。

❤️万字总结,C语言的这些万年坑你还在踩吗(基础篇)❤️

上一篇讲的是基础,这篇讲的是高阶版,需要多练习多揣摩,本篇文章是之前学习C语言的总结,制作主要是我复习用的,既然是知识,当然分享是很重要的,还是那句老话,如果你认为这篇博客写的不错的话,求评论,求收藏,求点赞,您的三连是我最大的制作动力,本文大约三万字,没有时间看完可以收藏抽时间看,部分内容我以链接形式展示,废话不多说,让我们学起来吧!!!


二,数据的存储

1,数据类型介绍

char        //字符数据类型
short       //短整型
int         //整形
long        //长整型
long long   //更长的整形
float       //单精度浮点数
double      //双精度浮点数

类型的意义:

使用这个类型开辟内存空间的大小(大小决定了使用范围)。

如何看待内存空间的视角。

2,类型的基本归类

整形家族

char
   unsigned char
   signed char
short
   unsigned short[int]
   signed short[int]
int
   unsigned int
   signed int
long
   unsigned long[int]
   signed long[int]

浮点数家族

float
double

构造类型

  数组类型
  结构体类型 struct
  枚举类型 enum
  联合类型 union
int main()
{
	unsigned char a = 200;
	unsigned char b = 100;
	unsigned char c = 0;
	c = a + b;
	printf("%d %d", a + b, c);
	return 0;
}

程序的执行结果为( )

A.300 300

B.44 44

C.300 44

D.44 300

说明:printf在传入参数的时候如果是整形会默认传入四字节,所以a+b的结果是用一个四字节的整数接收的,不会越界。而c已经在c = a + b这一步中丢弃了最高位的1,所以只能是300-256得到的44了。

※由于printf是可变参数的函数,所以后面参数的类型是未知的,所以甭管你传入的是什么类型,printf只会根据类型的不同将用两种不同的长度存储。其中8字节的只有long long、float和double(注意float会处理成double再传入),其他类型都是4字节。所以虽然a + b的类型是char,实际接收时还是用一个四字节整数接收的。另外,读取时,%lld、%llx等整型方式和%f、%lf等浮点型方式读8字节,其他读4字节。

3,整形在内存中的存储

原码、反码、补码

原码

直接将二进制按照正负数的形式翻译成二进制就可以。

反码

将原码的符号位不变,其他位依次按位取反就可以得到了。

补码

反码+1就得到补码。

想了解原反补码的计算和进制的可以看看我之前的博客,二进制的讲解

关于C语言二进制相关的内容+笔试习题,建议收藏

原码、反码、补码说法错误的是( )

A.一个数的原码是这个数直接转换成二进制

B.反码是原码的二进制符号位不变,其他位按位取反

C.补码是反码的二进制加1

D.原码、反码、补码的最高位是0表示负数,最高位是1表示正数

 ABC正确,D关于符号位的描述说反了

数据在内存的储存

#include
int main()
{
	int a = 1;
	int b = -2;
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第2张图片

 数据在内存中存储中有大小端之分

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第3张图片

正数的原、反、补码都相同。

对于整形来说:数据存放内存中其实存放的是补码。

大小端介绍

大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;

小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。

为什么有大端和小端

为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一 个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具 体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字 节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。 例如一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小 端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小 端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

unsigned int a = 0x1234; 
unsigned char b = *(unsigned char*)&a;

在32位大端模式处理器上变量b等于( )

大端序中,低地址到高地址的四字节十六进制排列分别为00 00 12 34,其中第一个字节的内容为00,故选A

关于大小端字节序的描述正确的是( )

A.大小端字节序指的是数据在电脑上存储的二进制位顺序

B.大小端字节序指的是数据在电脑上存储的字节顺序

C.大端字节序是把数据的高字节内容存放到高地址,低字节内容存放在低地址处

D.小端字节序是把数据的高字节内容存放到低地址,低字节内容存放在高地址处

小端字节序: 低位放在低地址

大端字节序:高位放在低地址

下面代码的结果是( )

int main()
{
	char a[1000] = { 0 };
	int i = 0;
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	printf("%d", strlen(a));
	return 0;
}

a是字符型数组,strlen找的是第一次出现尾零(即值为0)的位置。考虑到a[i]其实是字符型,如果要为0,则需要-1-i的低八位要是全0,也就是问题简化成了“寻找当-1-i的结果第一次出现低八位全部为0的情况时,i的值”(因为字符数组下标为i时第一次出现了尾零,则字符串长度就是i)。只看低八位的话,此时-1相当于255,所以i==255的时候,-1-i(255-255)的低八位全部都是0,也就是当i为255的时候,a[i]第一次为0,所以a[i]的长度就是255了

4,浮点型在内存中的存储

常见的浮点数:

3.14159 1E10 浮点数家族包括: float、double、long double 类型。 浮点数表示的范围:float.h中定义

举个例子

int main()
{
	int n = 9;
	float* pFloat = (float*)&n;
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	*pFloat = 9.0;
	printf("num的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第4张图片

根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:

(-1)^S * M * 2^E

(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。

M表示有效数字,大于等于1,小于2。

2^E表示指数位。

举例来说: 十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。 那么,按照上面V的格式,可以得出s=0, M=1.01,E=2。

十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,s=1,M=1.01,E=2。

IEEE 754对有效数字M和指数E,还有一些特别规定。

E不全为0或不全为1

这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前 加上第一位的1。 比如: 0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位, 则为1.0*2^(-1),其阶码为-1+127=126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位 00000000000000000000000,则其二进制表示形式为:

0 01111110 00000000000000000000000

E全为0

这时,浮点数的指数E等于1-127(或者1-1023)即为真实值, 有效数字M不再加上第一位的1,而是还原为 0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。

E全为1

这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);

解释前面的题目:

下面,让我们回到一开始的问题:为什么 0x00000009 还原成浮点数,就成了 0.000000 ? 首先,将 0x00000009 拆 分,得到第一位符号位s=0,后面8位的指数 E=00000000 ,最后23位的有效数字M=000 0000 0000 0000 0000 1001。

9 -> 0000 0000 0000 0000 0000 0000 0000 1001

由于指数E全为0,所以符合上一节的第二种情况。因此,浮点数V就写成: V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146) 显然,V是一个很小的接近于0的正数,所以用十进制小 数表示就是0.000000。

再看例题的第二部分。 请问浮点数9.0,如何用二进制表示?还原成十进制又是多少? 首先,浮点数9.0等于二进制 的1001.0,即1.001×2^3。

9.0 -> 1001.0 ->(-1) ^ 01.0012 ^ 3->s = 0, M = 1.001, E = 3 + 127 = 130

那么,第一位的符号位s=0,有效数字M等于001后面再加20个0,凑满23位,指数E等于3+127=130,即 10000010。 所以,写成二进制形式,应该是s+E+M,即

0 10000010 001 0000 0000 0000 0000 0000

这个32位的二进制数,还原成十进制,正是 1091567616 。


三,指针的进阶

1,字符指针

一般使用

int main()
{
	char ch = 'w';
	char* pc = &ch;
	*pc = 'w';
	return 0;
}

高阶使用

int main()
{
	char* pstr = "hello bit.";
	printf("%s\n", pstr);
	return 0;
}

下面练习一道题

#include 
int main()
{
	char str1[] = "hello word.";
	char str2[] = "hello word.";
	char* str3 = "hello word.";
	char* str4 = "hello word.";
	if (str1 == str2)
		printf("str1 and str2 are same\n");
	else
		printf("str1 and str2 are not same\n");

	if (str3 == str4)
		printf("str3 and str4 are same\n");
	else
		printf("str3 and str4 are not same\n");

	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第5张图片

这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域, 当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始 化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。

下面关于"指针"的描述不正确的是:( )

A.当使用free释放掉一个指针内容后,指针变量的值被置为NULL

B.32位系统下任何类型指针的长度都是4个字节

C.指针的数据类型声明的是指针实际指向内容的数据类型

D.野指针是指向未分配或者已经释放的内存地址

Afree不会更改指针的指向。

B选项强调了32位系统,所以没问题。

CD选项是定义本身。

所以排除法也可以确定是A

关于下面代码描述正确的是:( )

char* p = "hello word";

A.把字符串hello bit存放在p变量中

B.把字符串hello bit的第一个字符存放在p变量中

C.把字符串hello bit的第一个字符的地址存放在p变量中

D.*p等价于hello bit

双引号引起来的这一段是一个常量字符串,本质是一个常量字符数组类型,赋给一个指针,相当于把一个数组的首地址赋给指针,即第一个元素h的地址。

只有选项C提到了第一个字符的地址,故选C

2,指针数组

定义

指针数组:能够指向数组的指针。

int* p1[10];
int(*p2)[10];
//p1, p2分别是什么?
int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个
指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

&数组名VS数组名

#include 
int main()
{
    int arr[10] = { 0 };
    printf("%p\n", arr);
    printf("%p\n", &arr);
    return 0;

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第6张图片

可见数组名和&数组名打印的地址是一样的。

#include 
int main()
{
	int arr[10] = { 0 };
	printf("arr = %p\n", arr);
	printf("&arr= %p\n", &arr);
	printf("arr+1 = %p\n", arr + 1);
	printf("&arr+1= %p\n", &arr + 1);
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第7张图片

根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。 实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下) 数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40.

下面哪个是数组指针( )

A.int** arr[10]

B.int (*arr[10])

C.char *(*arr)[10]

D.char(*)arr[10]

A是二级指针数组,B是指针数组,C是char *数组的指针,D是char *的数组。只有C是数组指针。

tip:根据优先级看只有C选项优先跟*结合,其他都不是指针,所以直接选C。

下面哪个代码是错误的?( )

#include 
int main()
{
	int* p = NULL;
	int arr[10] = { 0 };
	return 0;
}

A.p = arr;

B.int (*ptr)[10] = &arr;

C.p = &arr[0];

D.p = &arr;

就数据类型来看,A左右两边都是int *,B左右两边都是 int (*)[10],C左右两边都是int *,D左边是 int *,右边是 int (*)[10],故选D。

下面代码关于数组名描述不正确的是( )

int main()
{
  int arr[10] = {0};
  return 0;
}

A.数组名arr和&arr是一样的

B.sizeof(arr),arr表示整个数组

C.&arr,arr表示整个数组

D.除了sizeof(arr)和&arr中的数组名,其他地方出现的数组名arr,都是数组首元素的地址

A选项错误明显。arr的类型是int [10],而&arr的类型是int (*)[10],根本不是一个类型,不可能是一样的。而在 sizeof(arr)和&arr中,arr都是看成整体的,而一般它代表一个数组的首地址。

3,数组指针的使用

#include 
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	int(*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
	//但是我们一般很少这样写代码
	return 0;
}

一个数组指针的使用

#include 
void print_arr1(int arr[3][5], int row, int col)
{
    int i, j;
    for (i = 0; i < row; i++)
    {
        for (j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
void print_arr2(int(*arr)[5], int row, int col)
{
    int i, j;
    for (i = 0; i < row; i++)
    {
        for (j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
    print_arr1(arr, 3, 5);
    //数组名arr,表示首元素的地址
    //但是二维数组的首元素是二维数组的第一行
    //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
    //可以数组指针来接收
    print_arr2(arr, 3, 5);
    return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第8张图片

 4,函数指针

#include 
void test()
{
	printf("hehe\n");
}
int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第9张图片

pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无 参数,返回值类型为void。

5,函数指针数组

要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[10]])();
int* parr2[10]();
int (*)() parr3[10];

parr1 parr1 先和 [] 结合,说明parr1是数组,数组的内容是什么呢? 是 int (*)() 类型的 函数指针

下面哪个是函数指针?( )

A.int* fun(int a, int b);

B.int(*)fun(int a, int b);

C.int (*fun)(int a, int b);

D.(int *)fun(int a, int n);

 ABD没有区别,加的括号没有影响任何优先级,都是返回值为int *的函数,故选C。

定义一个函数指针,指向的函数有两个int形参并且返回一个函数指针,返回的指针指向一个有一个int形参且返回int的函数?下面哪个是正确的?( )

A.int (*(*F)(int, int))(int)

B.int (*F)(int, int)

C.int (*(*F)(int, int))

D.*(*F)(int, int)(int)

 D类型不完整先排除,然后看返回值,B的返回值是int,C的返回值是int *,故选A。判断返回值类型只需要删掉函数名/函数指针和参数列表再看就行了。int (*(*F)(int, int))(int)删掉(*F)(int, int)后剩下int (*)(int),符合题意

在游戏设计中,经常会根据不同的游戏状态调用不同的函数,我们可以通过函数指针来实现这一功能,下面哪个是:一个参数为int *,返回值为int的函数指针( )

A.int (*fun)(int)

B.int (*fun)(int *)

C.int* fun(int *)

D.int* (*fun)(int *)

 首先C压根就不是函数指针,先排除,然后D返回值不是int,排除,A的参数不是int *,排除,剩下B了。

声明一个指向含有10个元素的数组的指针,其中每个元素是一个函数指针,该函数的返回值是int,参数是int*,正确的是( )

A.(int *p[10])(int*)

B.int [10]*p(int *)

C.int (*(*p)[10])(int *)

D.int ((int *)[10])*p

 A选项,第一个括号里是一个完整定义,第二个括号里是个类型,四不像。BD选项,[]只能在标识符右边,双双排除。只有C是能编过的。

6,回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一 个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该 函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或 条件进行响应。

#include 
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void* p1, const void* p2)
{
    return (*(int*)p1 - *(int*)p2);
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;

    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
    for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第10张图片

7,指针和数组笔试题

一维数组

int main()
{
	int a[] = { 1,2,3,4 };
	
    printf("%d\n", sizeof(a));
	//数组名a单独放在sizeof内部,数组名表示整个数组,计算的是整个数组的大小
	
    printf("%d\n", sizeof(a + 0));
	//a表示首元素的地址,a+0还是首元素的地址,地址的大小是4/8字节
	
    printf("%d\n", sizeof(*a));   
 	//a表示首元素的地址,*a 就是首元素 ==> a[0] ,大小就是4
	//*a == *(a+0) == a[0]
	
    printf("%d\n", sizeof(a + 1));
	//a表示首元素的地址,a+1是第二个元素的地址,大小就是4/8
	
    printf("%d\n", sizeof(a[1]));
	//a[1] 就是第二个元素 - 4
	
    printf("%d\n", sizeof(&a));  
	//&a - 数组的地址 - 4/8 - int(*)[4]
	
    printf("%d\n", sizeof(*&a));  
	//*&a - &a是数组的地址,对数组的地址解引用拿到的是数组,所以大小时候16
	//相当于printf("%d\n", sizeof(a));//16
	
    printf("%d\n", sizeof(&a + 1));
	//4/8 &a是数组的地址,&a+1 是数组的地址+1,跳过整个数组,虽然跳过了数组,
	//还是地址  4/8
 	
    printf("%d\n", sizeof(&a[0]));
	//4/8
	
    printf("%d\n", sizeof(&a[0] + 1));
	//第二个元素的地址 4/8
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第11张图片

字符数组

char arr[] = { 'a','b','c','d','e','f' };
//arr中是没有放\0的,而strlen()求长度是找到\0才停止

printf("%d\n", strlen(arr));
//从arr位置(首元素地址)向后求长度,随机值

printf("%d\n", strlen(arr + 0));
//从arr位置(首元素地址)向后求长度,随机值

printf("%d\n", strlen(*arr));
//arr是首元素地址,*arr是字符‘a’-ascii-97,strlen把字符a对应的ascii码值97作为地址向后计数,非法访问!err


printf("%d\n", strlen(arr[1]));
//strlen把字符b对应的ascii码值98作为地址向后计数,非法访问!err

printf("%d\n", strlen(&arr));//&arr和arr地址值相同,都是首元素地址,但是意义不一样
//&arr传给strlen 

printf("%d\n", strlen(&arr + 1));//跳过整个数组后,向后计数,随机值-6
//内存空间连续,同时找到\0停止,但是strlen(arr)和strlen(&arr)得到的随机值比第二个多6个字符abcdef

printf("%d\n", strlen(&arr[0] + 1));//从字符b位置向后计数,随机数-1
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	//&arr的类型:数组指针: char(*)[6]
	
    printf("%d\n", sizeof(arr));
	//数组名单独放在sizeof内部,计算的是整个数组的大小,元素个数为6个(不含\0),类型为char 所以大小为6
	
    printf("%d\n", sizeof(arr + 0));
	//此处的arr代表的是首元素地址,arr+0仍是首元素地址char*,地址(指针)大小是4/8
	
    printf("%d\n", sizeof(*arr));
	//此处的arr代表的是首元素地址,*arr即为数组首元素,即为字符‘a’  大小为1
	
    printf("%d\n", sizeof(arr[1]));
	//arr[1]->字符‘b’,大小为1
	
    printf("%d\n", sizeof(&arr));
	//取出整个数组的地址,还是地址,地址的大小就是4/8  
	
    printf("%d\n", sizeof(&arr + 1));
	//取出数组arr的地址+1,跳过一个数组,还是地址,地址的大小为:4/8
	
    printf("%d\n", sizeof(&arr[0] + 1));//数组第二个元素的地址,4/8
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第12张图片

int main()
{
	char arr[] = "abcdef";
	//此时数组arr中存放了\0  strlen求长度,遇到\0即停止计数
	
    printf("%d\n", strlen(arr));
	//从arr位置开始向后计数,遇到\0即停,长度为6
	
    printf("%d\n", strlen(arr + 0));
	///从arr位置开始向后计数,遇到\0即停,长度为6
	
    printf("%d\n", strlen(*arr));//arr是首元素地址,*arr是字符‘a’
	// 对应ascii值为97,strlen把字符a对应的ascii码值97作为地址向后计数,err
	
    printf("%d\n", strlen(arr[1]));///arr[1]:‘b’对应ascii值为98,
	//strlen把字符b对应的ascii码值98作为地址向后计数,非法访问!err
	
    printf("%d\n", strlen(&arr)); 
	//&arr和arr地址值相同,都是首元素地址,但是意义不一样
	//&arr传给strlen  &arr类型:数组指针 char(*p)[6] 而strlen接收的类型为char*,不兼容,但是问题不大
	//从数组首元素位置向后计数,值为 6
	
    printf("%d\n", strlen(&arr + 1));
	//跳过整个数组后,向后计数,未知值
	
    printf("%d\n", strlen(&arr[0] + 1));
	//从b未知向后计数,长度为5
	return 0;
}

int main()
{
	char arr[] = "abcdef";
	//此时的arr数组里面是放了\0的
	
    printf("%d\n", sizeof(arr));
	//数组名单独放在sizeof内部,计算的是整个数组的大小,\0也算进去,大小为7
	
    printf("%d\n", sizeof(arr + 0));
	//此时的数组名是首元素地址,地址(指针)大小:4/8
	
    printf("%d\n", sizeof(*arr));
	//此时的数组名是首元素地址,*arr即为首元素,字符a->char类型,大小为1
	
    printf("%d\n", sizeof(arr[1]));
	//arr[1]:字符'b',大小为1
	
    printf("%d\n", sizeof(&arr));
	//取出数组的地址,还是地址,大小为4/8
	
    printf("%d\n", sizeof(&arr + 1));
	//取出数组的地址+1,跳过整个数组,还是地址:4/8
	
    printf("%d\n", sizeof(&arr[0] + 1));
	//取出第一个元素的地址+1,跳过一个元素,即为第二个元素的地址,4/8
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第13张图片

#include
int main()
{
	//&p[0]==>相当于&*(p+0)-->相当于sizeof(p),p存中存放的是字符a的地址,+1,即为字符b的地址,从字符b位置向后访问,  长度为5
	// p[0] :字符a  
	//&p[0]:字符a的地址 
	//&p[0] +1:字符b的地址
	const char* p = "abcdef";
	//p存放的是字符a的地址,
	//p+1:字符b的地址
	
     printf("%d\n", strlen(p));
	//p存放的是字符a的地址,即从字符a的地址向后计数,长度为6
	
    printf("%d\n", strlen(p + 1));
	//从字符b的地址向后计数,长度为5
	
    //printf("%d\n", strlen(*p));
 	//*p ->字符‘a’  即以字符a的ascii码值97为地址向后计数,非法访问,err
	
    //printf("%d\n", strlen(p[0]));
	//p[0] ->字符‘a’  即以字符a的ascii码值97为地址向后计数,非法访问,err
 	
    printf("%d\n", strlen(&p));
	//&p取出的是p变量的地址,即以p变量的地址(16进制)向后计数,  随机值
	
    printf("%d\n", strlen(&p + 1));//&p取出的是p变量的地址,&p+1,跳过p变量,即从p变量之后的位置向后访问  随机值
	
    printf("%d\n", strlen(&p[0] + 1));
	//&p[0]==>相当于&*(p+0)-->相当于sizeof(p),p存中存放的是字符a的地址,+1,即为字符b的地址,从字符b位置向后访问,  长度为5
	return 0;
}
int main()
{
	//因为指针指向的是常量字符串,不可以被修改
//所以可以用const修饰
//char* p = "abcdef";
	const char* p = "abcdef";
	//p存放的是字符a的地址
	
    printf("%d\n", sizeof(p));
	//p是指针,指向字符a,大小为4/8
	
    printf("%d\n", sizeof(p + 1));
	//p+1,指向的是字符b,指针,大小为4/8
	
    printf("%d\n", sizeof(*p));
	//p存放的是字符a的地址,*p:即为字符a,大小为1
	
    printf("%d\n", sizeof(p[0]));
	//p[0]->字符a ,大小为1
	
    printf("%d\n", sizeof(&p));
	//取出p变量的地址,仍是地址,大小为4/8
	
    printf("%d\n", sizeof(&p + 1));
	//取出p变量的地址+1,跳过p变量,但仍是地址,大小为4/8
	
    printf("%d\n", sizeof(&p[0] + 1));
	//&p[0]相当于&*(p+0),&和*抵消,&p[0]:字符a的地址,+1:字符b的地址,大小为4/8
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第14张图片

int main()
{
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));
	//数组名单独放在sizeof内部,计算的是整个数组的大小, 
	//数组元素为12个,每一个元素大小为4个字节,12*4=48
	
	printf("%d\n", sizeof(a[0][0]));
	//计算的是数组第有一行第一个元素的大小,int类型,大小为4
	
	printf("%d\n", sizeof(a[0]));
	//a[0]==>*(a+0)==>数组名是首元素地址,即为第一行的地址,解引用第一行的地址,
	// 就是第一行,所以计算的是第一行元素的大小 4*4=16
	//a[0] : 二维数组的第一行
	
	printf("%d\n", sizeof(a[0] + 1));
	//a[0]:第一行的数组名,代表第一行第一个元素的地址,a[0]+1::跳过一个元素,
	// 即为第一行第二个元素地址,大小为4/8  
	//注意:a[0] + 1 :不是第二行,a[0]是第一行的数组名,首元素地址,
	//即为第一行第一个元素地址,a[0]+1:跳过一个元素   a+1:a为数组名,首元素地址,第一行的地址,a+1,跳过一行,二维数组第二行

	printf("%d\n", sizeof(*(a[0] + 1)));
	//由上可得:a[0]+1:第一行第二个元素地址, *(a[0]+1):即为第一行第二个元素 int类型 大小为4

	printf("%d\n", sizeof(a + 1));
	//a为二维数组的数组名->首元素地址,e二维数组第一行的地址,+1,跳过一行,即为第二行的地址->地址,大小为4/8

	printf("%d\n", sizeof(*(a + 1)));
	//由上,a+1是第二行的地址,*(a+1)即为第二行,大小为4*4 = 16

	printf("%d\n", sizeof(&a[0] + 1));
	//a[0]是第一行的数组名,&a[0]就是第一行的地址,
	//(相当于是,数组名和取地址数组名的关系,二二者地址值相同,但是含义不同),&a[0]+1:跳过第一行,即为第二行地址,地址:4/8

	printf("%d\n", sizeof(*(&a[0] + 1)));
	//由上:(&a[0]+1):第二行地址,解引用就是第二行,大小为4*4 = 16

	printf("%d\n", sizeof(*a));
	//二维数组数组名是首元素地址,即为第一行的地址,解引用就是第一行, 大小为4* 4  = 16

	printf("%d\n", sizeof(a[3]));
	//a[3]假设存在,就是第四行的数组名,sizeof(a[3])相当于数组名单独放在sizeof内部,计算的是第四行的大小, 4*4 = 16
	//sizeof内部的表达式不参与运算,即不会真的去访问a[3]的空间,所以不出错,它只看一下第四行的类型,并没有真正去访问第四行的内容,

	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第15张图片

下面程序的结果是:( )

int main()
{
  int aa[2][5] = {10,9,8,7,6,5,4,3,2,1};
  int *ptr1 = (int *)(&aa + 1);
  int *ptr2 = (int *)(*(aa + 1));
  printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
  return 0;
}

&aa的类型是int (*)[2][5],加一操作会导致跳转一个int [2][5]的长度,直接跑到刚好越界的位置。减一以后回到最后一个位置1处。*(aa + 1)相当于aa[1],也就是第二行的首地址,自然是5的位置。减一以后由于多维数组空间的连续性,会回到上一行末尾的6处

下面程序的结果是:( )

int main()
{
  int a[5] = {5, 4, 3, 2, 1};
  int *ptr = (int *)(&a + 1);
  printf( "%d,%d", *(a + 1), *(ptr - 1));
  return 0;
}

 *(a + 1)等同于a[1],第一个是4,a的类型是int [5],&a的类型就是int(*)[5],是个数组指针。所以给int(*)[5]类型加一,相当于加了一个int [5]的长度。也就是这个指针直接跳过了a全部的元素,直接指在了刚好越界的位置上,然后转换成了int *后再减一,相当于从那个位置向前走了一个int,从刚好越觉得位置回到了1的地址处,所以第二个是1


四,字符函数和字符串函数

本节我之前的博客有详解,感兴趣的可以去考古

❤爆肝万字C进阶字符串+内存函数的介绍+库函数模拟实现+笔试题分析❤,建议收藏!!!

1,strlen

size_t strlen ( const char * str );

字符串已经 '\0' 作为结束标志,strlen函数返回的是在字符串中 '\0' 前面出现的字符个数(不包含 '\0' )。

参数指向的字符串必须要以 '\0' 结束。

注意函数的返回值为size_t,是无符号的( 易错 )

2,strcpy

char* strcpy(char * destination, const char * source );

源字符串必须以 '\0' 结束。

会将源字符串中的 '\0' 拷贝到目标空间。

目标空间必须足够大,以确保能存放源字符串。

目标空间必须可变。

3,strcat

char * strcat ( char * destination, const char * source );

源字符串必须以 '\0' 结束。

目标空间必须有足够的大,能容纳下源字符串的内容。

目标空间必须可修改。

4,strcmp

char * strcmp ( const char * str1, const char * str2)

第一个字符串大于第二个字符串,则返回大于0的数字

第一个字符串等于第二个字符串,则返回0

第一个字符串小于第二个字符串,则返回小于0的数字

5,strstr

char * strstr ( const char *, const char * );

6,memcpy

void * memcpy ( void * destination, const void * source, size_t num );

函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。

这个函数在遇到 '\0' 的时候并不会停下来。

如果source和destination有任何的重叠,复制的结果都是未定义的。

7,memmove

void * memmove ( void * destination, const void * source, size_t num );

和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。

如果源空间和目标空间出现重叠,就得使用memmove处理。

8,模拟实现上述内存函数与字符串函数

memmove实现重叠拷贝和不重叠拷贝
void* my_memmove(void* dest, const void* src, size_t count)//无符号整型
{
	//前到后
	assert(dest && src);
	void* ret = dest;
	if (dest < src)
	{
		while (count--)
		{
			*(char*)dest = *(char*)src;
			dest = (char*)dest + 1;
			src = (char*)src + 1;
		}
	}
	//后到前
	else
	{
		while (count--)
		{
			*((char*)dest + count) = *((char*)src + count);
		}
	}
	return ret;
}

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9 };
	my_memmove(arr + 2, arr, 16);
	//my_memmove(arr, arr + 2, 16);
	return 0;
}
my_memcpy(void* dest, const void* src, size_t count)
{
	void* set = dest;
	assert(dest && src);
	while (count--)
	{
		*(char*)dest = *(char*)src;
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}
	return  set;
}

int main()
{
	int arr1[10] = { 1,2,3,4,5,6,7 };
	int arr2[20] = { 0 };
	my_memcpy(arr2, arr1, 40);//拷贝的是整型数据
	int i = 0;
	for (i = 0; i < 20; i++)
	{
		printf("%d", arr2[i]);
	}
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第16张图片

char* my_strstr(const char*str1, const char* str2)
{
	assert(str1 && str2);
	char* s1;
	char* s2;
	char* cp = str1;
	if (*str2 == '\0')
		return str1;
	while (*cp)
	{
		s1 = cp;
		s2 = str2;
		//while (*s1!='\0'  && *s2 != '\0' && *s1 == *s2)
		while (*s1 && *s2 && *s1 == *s2)
		{
			s1++;
			s2++;
		}
		if (*s2 == '\0')
		{
			return cp;
		}
		cp++;
	}
	//找不到
	return NULL;
}


int main()
{
	char arr1[] = "i am good student, hehe student";
	char arr2[] = "student";
	//查找arr1中arr2第一次出现的位置
	char *ret = my_strstr(arr1, arr2);
	if (ret == NULL)
	{
		printf("找不到\n");
	}
	else
	{
		printf("%s\n", ret);
	}
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第17张图片

#include
#include
#include
my_strcat(char* dest, const char* src)
{
	assert(dest && src);


	// a b c \0
	// d e f \0
	//1,找到目标字符串的末尾\0
	//2,追加字符串直到\0
	//返回类型是char*,stract返回目标空间的起始地址
	char* ret = dest;
	while (*dest)
	{
		dest++;
	}
	//与strcpy追加相等
	while (*dest++ = *src++)
	{
		;
	}
	return ret;
	//返回类型是char*,stract返回目标空间的起始地址
}


int main()
{
	//strcpy字符串拷贝\0是停止的标志
	//stract字符串连接
	char arr1[20] = "abc";//保证数据可以放进去
	char arr2[20] = { 'd','e','f' };//无\0程序将会出现问题
	char arr3[20] = { 'd','e','f' ,'\0' };//正确书写
	my_strcat(arr1, arr3);
	printf("%s\n", arr1);
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第18张图片

int my_strcmp(const char* s1,const char* s2)
{
	assert(s1 && s2);
	// a b c d e \0
	// a d n \0
	//c与n不相等,比较assic值
	// a b c \0
	// a b c \0
	//相等
	while (*s1 == *s2)
	{
		if (*s1 == '\0')
		{
			return 0;
		}
		s1++;
		s2++;
	}
	return *s1 - *s2;//第一个字符串小于第二字符串,返回负数


}


int main()
{
	char arr1[] = "asihvw";
	char arr2[] = "asns";
	//字符串一一比较
	//返回值有三种可能性
	//发现相等\0,停下来,结果为0;
	//不同比较的是字符串对应的assic码值
	int ret = my_strcmp(arr1, arr2);
	if (ret == 0)
	{
		printf("=\n");
	}
	else if (ret < 0)
	{
		printf("<\n");
	}
	else
	{
		printf(">\n");
	}
	printf("%d\n", ret);
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第19张图片

char* my_strcpy(char* dest, const char* src)
{
	assert(dest && src);
	char* ret = dest;
	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}
int main()
{
	char arr1[] = "xxxxxxxx";
	char arr2[] = "abc";
	printf("%s\n", my_strcpy(arr1, arr2));
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第20张图片

int my_strlen(const char* str)
{
	int count = 0;
	while (*str !='\0')
	{
		count++;
		str++;
	}
	return count;
}


int main()
{
	char arr1[] = "sfsgssg";
	int ret = my_strlen(arr1);
	printf("%d",ret);
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第21张图片


 五,自定义类型:结构体,枚举,联合

1,结构体

结构的声明

struct tag
{
 member-list;
}variable-list;

结构的自引用

struct Node
{
 int data;
 struct Node* next;
};

结构体变量的定义和初始化

int x, y;
struct Point
{
	int x;
	int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = { x, y };
struct Stu        //类型声明
{
	char name[15];//名字
	int age;      //年龄
};
struct Stu s = { "zhangsan", 20 };//初始化
struct Node
{
	int data;
	struct Point p;
	struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化
struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始h,

结构体内存对齐

本节在我前面的博客有详解,建议欢迎各位考古,不了解结构体对齐对下面的题上手困难

❤️图解C语言结构体对齐,保姆级教学,建议收藏❤️

结构体的对齐规则

1,结构体的第一个成员永远放在结构体起始位置偏移为0的地址

2,结构体从第二个成员,总是放在一个对齐数的整数倍数
   对齐数 = 编译器默认的对齐数和变量自身大小的较小值

3,. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

4,如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
 

为什么存在内存对齐

1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能 在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的 内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。


int main()
{
	struct S1
	{
		char c1;
		int i;
		char c2;
	};
	printf("%d\n", sizeof(struct S1));
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第22张图片

 c1在结构体起始位置偏移为0的地址。

i是第二个成员,总是放在一个对齐数的整数倍数,对齐数 = 编译器默认的对齐数和变量自身大小的较小值,i本身为4,Vs默认为8,我们选择从4开始,中间的全部丢弃。

c2本身为一个字节,相比于Vs8来说还是1。

由第三条规则可得:结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

最大对齐数位4,此时我们的内存在8,总大小为9,继续往后延申到达11,总大小为12,是4的整数倍,符合第三条规则。

int main()
{
    struct S1
    {
        char c1;  //字节大小1 Vs为8 所以为1
        int i;    //字节大小4 Vs为8 所以为4
        char c2;  //字节大小1 Vs为8 所以为1
    };
    printf("%d\n", sizeof(struct S1));
    return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第23张图片

c1在结构体起始位置偏移为0的地址。

c2是第二个成员,总是放在一个对齐数的整数倍数,对齐数 = 编译器默认的对齐数和变量自身大小的较小值,c2本身为1,Vs默认为8,我们选择从1开始。

i本身为4个字节,相比于Vs8来说还是4。

由第三条规则可得:结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

最大对齐数位4,此时我们的内存在7,总大小为8,是4 的整数倍数,符合第三条规则。


int main()
{
	struct S3
	{
		double d;
		char c;
		int i;
	};
	printf("%d\n", sizeof(struct S3));
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第24张图片

d在结构体起始位置偏移为0的地址。

c是第二个成员,总是放在一个对齐数的整数倍数,对齐数 = 编译器默认的对齐数和变量自身大小的较小值,c2本身为1,Vs默认为8,我们选择从8开始。

i本身为4个字节,相比于Vs8来说还是4。但它必须从12开始,因为此时内存为9,要符合第二条规则。

由第三条规则可得:结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

最大对齐数位4,此时我们的内存在15,总大小为16,是8的整数倍,符合第三条规则。

int main()
{
    struct S3
    {  
        double d;     
        char c;        
        int i;         
    };
    printf("%d\n", sizeof(struct S3));
    struct S4
    {
        char c1;       //
        struct S3 s3;
        double d;
    };
    printf("%d\n", sizeof(struct S4));
    return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第25张图片

 c1在结构体起始位置偏移为0的地址。

由第四条规则可得嵌套的结构体对齐到自己的最大对齐数的整数倍处,S3最大整数倍数为8,所以我们必须从8开始,中间的丢弃,结构体S3本身为16,向后延申16到23.

d为8个字节,从24开始符合规则

由第三条规则可得:结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

最大对齐数位16,此时我们的内存在23,总大小为24,继续延申到31,总大小为32,是16的整数倍,符合第三条规则。

 在32位系统环境,编译选项为4字节对齐,那么sizeof(A)和sizeof(B)是( )

struct A
{
 int a;
 short b;
 int c;
 char d;
};
struct B
{
 int a;
 short b;
 char c;
 int d;
};

两个结构体都是向int看齐。结构体A中,a独自对齐一个4字节,b+c超过了4字节,所以b独自对齐一个4字节,c独自对齐一个4字节,剩下一个d独自对齐一个4字节,共16字节。结构体B中,a独自对齐一个四字节,b+c+d才超过了4字节,所以b和c一起对齐一个4字节,d单独对齐一个4字节,共12字节

下面代码的结果是:( )

#pragma pack(4)/*编译选项,表示4字节对齐 平台:VS2013。语言:C语言*/
int main(int argc, char* argv[])
{
  struct tagTest1
  {
    short a;
    char d; 
    long b;   
    long c;   
  };
  struct tagTest2
  {
    long b;   
    short c;
    char d;
    long a;   
  };
  struct tagTest3
  {
    short c;
    long b;
    char d;   
    long a;   
  };
  struct tagTest1 stT1;
  struct tagTest2 stT2;
  struct tagTest3 stT3;

  printf("%d %d %d", sizeof(stT1), sizeof(stT2), sizeof(stT3));
  return 0;
}
#pragma pack()

三个结构体都向最长的4字节long看齐。第一个a+d+b才超过4字节,所以a和d一起对齐一个4字节,剩下两人独自占用,共12字节,第二个同理c,d合起来对齐一个四字节,也是12字节。第三个因为c+b,d+a都超过4字节了,所以各自对齐一个4字节,共16字节。

在VS2013下,这个结构体所占的空间大小是( )字节

typedef struct{
  int a;
  char b;
  short c;
  short d;
}AA_t;

16,参考上面的计算方法

位段

位段的声明和结构是类似的,有两个不同:

1.位段的成员必须是 int、unsigned int 或signed int 。

2.位段的成员名后边有一个冒号和一个数字。

struct A
{
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};

位段的内存分配

1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型

2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
	struct S s = { 0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	return 0;
}

位段的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。

2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。

3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。

跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

2,枚举

枚举类型的定义

enum Day//星期
{
 Mon,
 Tues,
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};

枚举的优点

1. 增加代码的可读性和可维护性

2. 和#define定义的标识符比较枚举有类型检查,更加严谨。

3. 防止了命名污染(封装)

4. 便于调试

5. 使用方便,一次可以定义多个常量

3,联合(共用体)

定义

//联合类型的声明
union Un
{
 char c;
 int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));

联合的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为 联合至少得有能力保存最大的那个成员)

联合大小的计算

联合的大小至少是最大成员的大小。 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

union Un1
{
	char c[5];
	int i;
};
union Un2
{
	short c[7];
	int i;
};
//下面输出的结果是什么?
int main()
{
	printf("%d\n", sizeof(union Un1));
	printf("%d\n", sizeof(union Un2));
	return 0;
}

✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨_第26张图片

 有如下宏定义和结构定义

#define MAX_SIZE A+B
struct _Record_Struct
{
  unsigned char Env_Alarm_ID : 4;
  unsigned char Para1 : 2;
  unsigned char state;
  unsigned char avail : 1;
}*Env_Alarm_Record;
struct _Record_Struct *pointer = (struct _Record_Struct*)malloc
(sizeof(struct _Record_Struct) * MAX_SIZE);

说明:结构体向最长的char对齐,前两个位段元素一共4+2位,不足8位,合起来占1字节,最后一个单独1字节,一共3字节。另外,#define执行的是查找替换, sizeof(struct _Record_Struct) * MAX_SIZE这个语句其实是3*2+3,结果为9

下面代码的结果是( )

int main()
{
  unsigned char puc[4];
  struct tagPIM
  {
    unsigned char ucPim1;
    unsigned char ucData0 : 1;
    unsigned char ucData1 : 2;
    unsigned char ucData2 : 3;
  }*pstPimData;
  pstPimData = (struct tagPIM*)puc;
  memset(puc,0,4);
  pstPimData->ucPim1 = 2; 
  pstPimData->ucData0 = 3;
  pstPimData->ucData1 = 4;
  pstPimData->ucData2 = 5;
  printf("%02x %02x %02x %02x\n",puc[0], puc[1], puc[2], puc[3]);
  return 0;
}

A.02 03 04 05

B.02 29 00 00

C.02 25 00 00

D.02 29 04 00

puc是一个char数组,每次跳转一个字节,结构体不是,它只有第一个元素单独享用一字节,其他三个元素一起共用一字节,所以puc被结构体填充后,本身只有两个字节会被写入,后两个字节肯定是0,至此AD排除,然后第一个字节是2就是2了,第二个字节比较麻烦,首先ucData0给了3其实是越界了,1位的数字只能是0或1,所以11截断后只有1,同理ucData1给的4也是越界的,100截断后是00,只有5的101是正常的。填充序列是类似小端的低地址在低位,所以排列顺序是00 101 00 1。也就是0010 1001,即0x29,故选B。

下面代码的结果是:( )

#include 
union Un
{
	short s[7];
	int n;
};
int main()
{
  printf("%d\n", sizeof(union Un));
  return 0;
}

结构体向int对齐,7个short一共是14字节,对齐后是16字节。n是单独的4字节,由于是union,所以n与s共用空间,只取最长的元素,故占用16字节。

在X86下,有下列程序

#include
int main()
{
  union
  {
    short k;
    char i[2];
  }*s, a;
  s = &a;
  s->i[0] = 0x39;
  s->i[1] = 0x38;
  printf(“%x\n”,a.k);
  return 0;
}

union只有2字节,2字节的十六进制只有4位,所以答案CD排除。而位顺序类似小端,低地址在低处,所以39是低地址,在低位,38在高位,所以是3839,

下面代码的结果是:( )

enum ENUM_A
{
		X1,
		Y1,
		Z1 = 255,
		A1,
		B1,
};
enum ENUM_A enumA = Y1;
enum ENUM_A enumB = B1;
printf("%d %d\n", enumA, enumB);

枚举默认从0开始,所以X1是0,故Y1是1,给了数字后会根据数字向后推,那么Z1是255,A1是256,所以B1是257,


六,动态内存管理

本节在我之前的博客也有详解,欢迎考古

玩转C语言之动态内存管理

1,为什么存在动态内存分配

空间开辟大小是固定的。

数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

2,动态内存函数的介绍

C语言提供了一个动态内存开辟的函数

void* malloc (size_t size)

如果开辟成功,则返回一个指向开辟好空间的指针。

如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。

如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的

void free (void* ptr);

如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。

如果参数 ptr 是NULL指针,则函数什么事都不做。

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下

void* calloc (size_t num, size_t size);

函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。

与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

realloc函数的出现让动态内存管理更加灵活。 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存。

void* realloc (void* ptr, size_t size);

ptr 是要调整的内存地址

size 调整之后新大小

返回值为调整之后的内存起始位置。

这个函数调整原内存空间大小的基础上,

还会将原来内存中的数据移动到新的空间。

关于动态内存函数的说法错误的是:( )

A.malloc函数向内存申请一块连续的空间,并返回起始地址

B.malloc申请空间失败,返回NULL指针

C.malloc可以向内存申请0字节的空间

D.malloc申请的内存空间,不进行释放也不会对程序有影响

不释放会产生内存碎片,小型程序可以不关注,但是在中大型程序上影响极其深刻。故选D。AB是函数的基本功能,C选项比较特殊,malloc(0)是允许的,也会返回一个指针,只是没有空间所以不可使用而已。

动态申请的内存在内存的那个区域?( )

A.栈区

B.堆区

C.静态区

D.文字常量区

态内存分配都是在堆上分配的

以下哪个不是动态内存的错误( )

A.free参数为NULL

B.对非动态内存的free释放

C.对动态内存的多次释放

D.对动态内存的越界访问

A选项,是对的,free函数传递NULL指针,什么事情都不发生

B,C,D都是错误的

关于动态内存相关函数说法错误的是:( )

A.malloc函数和calloc函数的功能是相似的,都是申请一块连续的空间。

B.malloc函数申请的空间不初始化,calloc函数申请的空间会被初始化为0

C.realloc函数可以调整动态申请内存的大小,可大可小

D.free函数不可以释放realloc调整后的空间

 realloc在操作过程中是释放旧空间分配并返回新空间,所以返回的新空间也是需要释放的,故选D。AB是malloc和calloc的区别。C是realloc的基础功能。


七,C语言文件操作

之前我写的一篇博客对这章有非常详细的讲解,保姆级教学,建议读者阅读学习完此篇再做下面的习题,事半功倍!!!

❤️学懂C语言文件操作读这篇就够了(万字总结,附习题)❤️

C语言以二进制方式打开一个文件的方法是?( ) 

A.FILE *f = fwrite( "test.bin", "b" );

B.FILE *f = fopenb( "test.bin", "w" );

C.FILE *f = fopen( "test.bin", "wb" );

D.FILE *f = fwriteb( "test.bin" );

首先,因为要打开文件,AD直接拖出去,由于不存在一个“fopenb”函数,所以直接选C。二进制描述中的b要放在权限后,也就是“wb”才是合法的。

关于fopen函数说法不正确的是:( )

A.fopen打开文件的方式是"r",如果文件不存在,则打开文件失败

B.fopen打开文件的方式是"w",如果文件不存在,则创建该文件,打开成功

C.fopen函数的返回值无需判断

D.fopen打开的文件需要fclose来关闭

C选项中fopen的返回值可以检验文件是否打开成功,打开方式为"r"时尤其重要。ABD为文件操作的基本概念和原则。

下列关于文件名及路径的说法中错误的是:( )

A.文件名中有一些禁止使用的字符

B.文件名中一定包含后缀名

C.文件的后缀名决定了一个文件的默认打开方式

D.文件路径指的是从盘符到该文件所经历的路径中各符号名的集合

B选项中,文件名可以不包含后缀名。A的话,文件中不能包含这些字符:\/:*?"<>|,C表述了后缀名的作用,D是路径的基本概念。故选B。

C语言中关于文件读写函数说法不正确的是:( )

A.fgetc是适用于所有输入流字符输入函数

B.getchar也是适用于所有流的字符输入函数

C.fputs是适用于所有输出流的文本行输出函数

D.fread是适用于文件输入流的二进制输入函数

 B选项中,getchar只针对标准输入流stdin。即使对stdin重定向,getchar针对的也只是stdin。f系列的输入输出函数都是作用于所有流的的,所以AC没问题,D的表述也没问题,fread做的就是二进制的活。

下面程序的功能是什么?  ( )

int main()
{ 
  long num=0;
  FILE *fp = NULL;
  if((fp=fopen("fname.dat","r"))==NULL)
  {
    printf("Can’t open the file! ");
    exit(0):
  }
  while(fgetc(fp) != EOF)
  { 
    num++;
  }
  printf("num=%d\n",num);
  fclose(fp);
  return 0;
}

程序只通过只读方式打开了一个文件,文中使用的fgetc,且没有' '和'\n'相关的统计,统计文件的字符数

下面说法不正确的是:( )

A.scanf和printf是针对标准输入、输出流的格式化输入、输出语句

B.fscanf和fprintf是针对所有输入、输出流的格式化输入、输出语句

C.sscanf是从字符串中读取格式化的数据

D.sprintf是把格式化的数据写到输出流中

D选项中,sprintf是把格式化的数据写到字符串中,与输出流无关。其他三句都准确描述了函数功能。

你可能感兴趣的