18fde01fee5e4278981004762ce48cc4.png

✨✨ 欢迎大家来到小伞的大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C语言详解
小伞的主页:xiaosan_blog

gitee:许星让 (xu-xingrang) - Gitee.com

制作不易!点个赞吧!!谢谢喵!!

1、数据的存储

1.1 整形的存储:

计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位。

负数的原、反、补码:

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

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

补码:反码+1就得到补码。

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

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

1.2 大小端存储

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

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

记忆点:小端,小小小,大端则相反

写一个程序判断当前机器是大端机还是小端机

void check_endian() {
	//将int转char,取出低位首位数据如果为1,则说明数据的低位保存在内存的低地址中
	int i = 1;
	if ((char*)&i) {
		printf("小端");
	}
	else {
		//数据的低位保存在内存的高地址中
		printf("大端");
	}
}

void check_endian() {
	union
	{
		int i;
		char c;
	}un;
	//使用联合体共用一份资源
	//0000,设i的值为1
	//低    高
	//1000
	//char取低位的值,为1为低端,为0则高端
	un.i = 1;
	if (un.c == 1) {
		printf("小端");
	}
	else {
		printf("大端");
	}
}

1.3 整形不同类型间互相赋值时截断和提升问题

1.3.1 整形截断(Integer Truncation)

整形截断是指在将一个较大范围的数值类型(如浮点数或大范围的整数类型)转换为较小范围的整数类型时,数据的高位部分被丢弃,只保留低位的部分。这通常发生在强制类型转换时,导致数据的精度丧失。

整形截断的例子(类似于浮点数向下取整)

考虑将一个浮点数转换为整数类型,或将一个大范围整数类型转换为较小的整数类型。小数部分或高位部分会被截断,导致数据丢失。

#include <iostream>

int main() {
    double pi = 3.14159;
    int truncatedPi = (int)pi;  // 强制将 double 转换为 int
    std::cout << "Truncated Pi: " << truncatedPi << std::endl;  // 输出 3
    return 0;
}
1.3.2 整形提升(Integer Promotion)

整形提升是指在运算过程中,较小的整型数值会被自动提升为较大的整型数值。这种行为通常发生在参与算术运算时,C/C++编译器会根据整型提升规则自动将较小的整数类型提升为 int 类型或更大的数据类型。

整形提升规则确保算术运算中的精度不会丢失,并且避免了低位类型之间的溢出。

整形提升的规则

C/C++ 中的整形提升规则如下:

char 和 short 提升为 int:如果一个 char 或 short 类型的数值参与算术运算,它会被提升为 int 类型。这是因为 int 足够大,能够容纳 char 和 short 类型的数值范围。
如果 int 不足以容纳数值,提升为 long 或更高的数据类型如果计算涉及到的数据超出了 int 类型的范围(例如使用 64 位数值),则会提升为 long 或 long long 类型。
不同类型的算术运算会将结果提升为较大的类型:在表达式中,如果参与运算的数值类型不同,C/C++ 会将较小的类型提升为较大的类型,通常会提升到 int 或 long。

#include <iostream>

int main() {
    char a = 5;
    short b = 10;
    int result = a + b;  // a 和 b 都会被提升为 int 类型
    std::cout << "Result of a + b: " << result << std::endl;  // 输出 15
    return 0;
}

整形提升的实际应用

整形提升常用于以下几种情况:

算术运算:整形提升使得算术运算(如加法、减法、乘法、除法等)过程中,较小的数据类型被自动提升为 int 类型,保证了计算的精度和正确性。例如,char 和 short 被提升为 int 后进行加法运算,可以避免加法时发生溢出。

混合类型运算:当一个表达式中涉及到不同大小的整型时,C/C++ 会自动进行类型提升以确保计算结果的准确性。例如,char 和 int 相加时,char 会被提升为 int,结果也会是 int 类型。

1.3.3 整型提升与标准操作符

例如,在 C++ 中,默认情况下,所有整数运算(包括加法、减法、乘法等)都将小整数类型(如 char 和 short)提升为 int

#include <iostream>

int main() {
    char c = 10;
    short s = 20;
    int result = c * s;  // c 和 s 被提升为 int,运算结果是 int 类型
    std::cout << "Multiplication result: " << result << std::endl;  // 输出 200
    return 0;
}

在上面的代码中,c 是 char 类型,s 是 short 类型,在执行乘法运算时,它们都被提升为 int 类型进行计算,确保结果不会丢失精度。

概念 整型截断 整型提升
定义 数据从较大类型转换到较小类型时,高位数据被丢弃。 较小类型的数据在算术运算中自动提升为较大的类型。
常见场景 浮点数转换为整数,较大整数类型转换为较小类型。 char 和 short 类型自动提升为 int
影响
 
可能导致数据丢失、精度丧失或溢出。 保证算术运算精度,不会发生数据溢出。
例子 将 double 转换为 int,小数部分被截断。 char 与 short 在加法运算中提升为 int 类型。
1.3.4 总结

整型截断:是当较大类型转换为较小类型时,数据的高位部分被丢弃。它可能会导致数据丢失或溢出,开发者需要小心使用。
整型提升:是在算术运算中,较小的整数类型会被提升为 int 类型或更大的类型,避免了精度丧失或溢出,保证了计算结果的正确性。
理解这些概念,尤其是在进行类型转换和数值计算时,可以帮助开发者更好地处理数据类型,避免错误和不必要的性能问题。

1.2 浮点数的存储:

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

#include <stdio.h>
int main()
{
	int n = 9;
	float* pFloat = (float*)&n;
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	*pFloat = 9.0;
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	return 0;
}


n的值为:9
*pFloat的值为:0.000000
n的值为:1091567616
*pFloat的值为:9.000000
1.2.1 浮点数的存储

上面的代码中, n 和 *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这 么大?要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。

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

在这里插入图片描述

举例来说:
十进制的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规定:

(1)对于32位的浮点数(float),最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M;
(2)对于64位的浮点数(double),最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M。

在这里插入图片描述

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

前⾯说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中 xxxxxx 表示小数部分。
IEEE 754 规定,在计算机内部保存M时,默认这个数的第⼀位总是1,因此可以被舍去,只保存后⾯的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去后,等于可以保存24位有效数字。

至于指数E,情况就比较复杂
⾸先,E为一个无符号整数(unsigned int)
这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

我们在回看这个题目

#include <stdio.h>
int main()
{
	int n = 9;
	float* pFloat = (float*)&n;
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	*pFloat = 9.0;
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	return 0;
}

9的二进制为1001.0
按照公式:
V = (-1)^S * M * 2^E
V = (-1)^0 * 1.001 * 2^3
按照浮点数的存储模式
S E        M
0 00000000 00000000000000000000000

因为1 <= M < 2,存储时,默认保留小数位
M = 1.001所以M为 001 + 00000000000000000000

E + 中间值
32位下的:E+127
64位下的:E+2047
所以E + 127 = 130 = 10000010
S E        M
0 10000010 00100000000000000000000

进行二进制转十进制为1091567616

2、数组与指针

2.1 什么是指针?什么是数组?数组和指针的联系和区别

本质类型不同

数组:是一组相同类型元素的集合;

  • 数组中存放的是1个或者多个数据,但是数组元素个数不能为0。
  • 数组中存放的多个数据,类型是相同的。

指针:是存储地址的变量,指向某个内存位置。

1.占用内存不同:

数组:N * sizeof( type )连续的内存

指针:固定大小,4或8字节,跟随系统

2.赋值与修改

//数组为常量指针
int arr[5];
arr = new int[5]; // 错误!arr不能指向其他地址
指针为变量
int *p;
p = arr;       // 合法
p = &arr[2];   // 合法

3. 取地址操作 & 的含义

  • 数组名&arr 返回整个数组的地址(类型为 int(*)[5]),值与 arr 相同但类型不同。

printf("%p == %p\n", (void*)arr, (void*)&arr); // 输出相同地址
  • 指针&p 返回指针变量本身的地址。

2.2 什么是数组指针?什么是指针数组

指针数组是一个数组其元素都是指针数组指针指向一个数组的指针是一个指针

#include <stdio.h>
int main() {
    // 定义三个整型数组
    int arr1[] = { 1, 2, 3 };
    int arr2[] = { 4, 5, 6 };
    int arr3[] = { 7, 8, 9 };
    // 定义一个指针数组,存储整型数组的地址
    int* pArr[3] = { arr1, arr2, arr3 };
    // 计算每个整型数组的元素之和
    for (int i = 0; i < 3; i++) {
        int sum = 0;
        for (int j = 0; j < 3; j++) {
            sum += pArr[i][j];
        }
        printf("arr%d 的和: %d\n", i + 1, sum);
    }
    return 0;
}

arr1 的和: 6
arr2 的和: 15
arr3 的和: 24
#include <stdio.h>
int main() {
    int num1 = 1, num2 = 2, num3 = 3, num4 = 4, num5 = 5;
    // 指针数组//存放指针的数组
    int* ptr_array[] = { &num1, &num2, &num3, &num4, &num5 };
    // 二维整数数组
    int int_array[][3] = {
        {10, 11, 12},
        {20, 21, 22},
        {30, 31, 32},
    };
    // 数组指针//指向数组的指针
    int (*array_ptr)[3] = int_array;
    return 0;
}
int (*pB)[10];//就是一个数组指针

在这个示例中,我们创建了一个整数指针数组 ptr_array 和一个数组指针 array_ptr:
ptr_array 是一个整数指针数组,该数组包含 5 个整数指针。每个指针指向一个整数变量。
array_ptr 是一个指向二维整数数组的指针。这个二维数组存储了多个整数,每一行包含 3 个整数。

2.3 &数组名和数组名分别代表的意义?

数组名a

数组名可以作为数组第一个元素的指针。我们由数组和指针的关系知道,a代表这个地址数值,它相当于一个指针,指向第一个元素(&a[0]),即指向数组的首地址。组中的其他元素可以通过a的位移得到,此时的进阶是以数组中单个的元素类型为单位的,即a+i= & a[i]。

数组名取地址&a

对于一个普通的变量b&b是指用取地址符号取得变量b的存放地址;a在内存中没有分配空间,只对数组a的各个元素分配了存储空间,此处数组名字a显然不是普通的变量,&a也不代表所取a的存储地址。

int main() {
	int a[] = { 1,2,3 };
	printf(" a = %p\n&a = %p\n", a, &a);
	printf(" sizeof(a) = %d\nsizeof(&a) = %d\n", sizeof(a), sizeof(&a));
	/*
	a  = 000000DE1C6FF6D8
	&a = 000000DE1C6FF6D8
	sizeof(a) = 12
	sizeof(&a) = 8
	*/
	//sizeof(a) 为整个数组的大小,sizeof(&a)为指针大小
	//a指向a[0]的指针,&a指向整个数组的指针,虽然值相等,但是类型上是不同的
	return 0;
}

2.4 什么是二维数组?二级指针?

1.二维数组(可看作多个一维数组)

  • C 语言中的二维数组在内存中实际上是按行连续存储的。例如,定义一个二维数组int arr[3][4],它在内存中的存储方式就像是一个包含了 3 个长度为 4 的一维数组。
  • 这意味着可以将二维数组看作是一个一维数组,其中每个元素又是一个一维数组。这种连续存储的特性为通过指针访问二维数组元素提供了基础。

数组名含义:

二维数组名arr可以看作是一个指向数组首元素的指针。对于二维数组,首元素是一个包含 4 个int类型元素的一维数组。所以arr的类型是int (*)[4],即指向包含 4 个int类型元素的一维数组的指针。
可以通过arr[i]来访问二维数组的第i行,这里的arr[i]等价于*(arr + i),它表示第i行的首地址,而arr[i][j]则是第i行第j列的元素。

2.二级指针(指针的地址)

  • 二级指针是指向指针的指针。例如,int **p定义了一个二级指针p,它可以存储一个指向int类型指针的地址。
  • 二级指针在处理复杂的数据结构、动态内存分配以及函数参数传递等方面非常有用。它提供了一种间接访问和操作数据的方式,可以通过多次解引用来访问最终的数据。

与一维数组相比较

  • 一级指针存储的是一个变量的地址,通过解引用可以访问该变量的值。而二级指针存储的是一个指针的地址,需要两次解引用才能访问到最终的数据。
  • 例如,对于int *q,*q可以访问到一个int类型的值。而对于int **p,*p得到一个指向int类型的指针,**p才是最终的int类型的值。

2.5 什么是函数指针?

函数指针其本质是一个指针变量,该指针变量指向一个函数。C程序在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。函数指针示例:

#include <stdio.h>

int max(int x, int y) {
	return x > y ? x : y;
}

int main(void) {
	// 定义一个函数指针 p,指向函数 max
	//返回值   (参数)
	int (*p)(int, int) = max;
	int a, b, c, d;
	printf("请输入三个数字:");
	scanf("%d %d %d", &a, &b, &c);
	// 使用函数指针调用函数
	d = p(p(a, b), c);
	printf("最大的数字是: %d\n", d);
	return 0;
}

2.6 常见的笔试题

##一维数组

int main() {
	//一维数组
	int a[] = { 1,2,3,4 };

	printf("%d\n", sizeof(a));			//16:整个数组的大小
	printf("%d\n", sizeof(a + 0));		//4/8:a为首元素的地址,0:偏移量,a+0为指针//char*
	printf("%d\n", sizeof(*a));			//4:解引用为首元素大小//char
	printf("%d\n", sizeof(a + 1));		//4/8:同第二个//char*
	printf("%d\n", sizeof(a[1]));			//4:元素值//char
	printf("%d\n", sizeof(&a));			//4/8:指针//char*
	printf("%d\n", sizeof(*&a));			//16:=a:取地址在解引用//char
	printf("%d\n", sizeof(&a + 1));		//4/8:地址偏移还是指针,跳过整个数组//地址
	printf("%d\n", sizeof(&a[0]));		//4/8:指针//char*
	printf("%d\n", sizeof(&a[0] + 1));	//4/8:指针偏移//char*
	/* 16 8 4 8 4 8 16 8 8 8 */

    char arr[] = "abcdef";
	printf("%d\n", sizeof(arr));		//7:整体大小包括\0
	printf("%d\n", sizeof(arr + 0));	//4/8:指针//char*
	printf("%d\n", sizeof(*arr));		//1:char a大小//char
	printf("%d\n", sizeof(arr[1]));		//1:cahr a大小//char
	printf("%d\n", sizeof(&arr));		//4/8:地址//char*
	printf("%d\n", sizeof(&arr + 1));	//4/8:跳过整个数组,地址大小//char*
	printf("%d\n", sizeof(&arr[0] + 1));//4/8:cahr * + 1地址//char*
	
	printf("%d\n", strlen(arr));		//6
	printf("%d\n", strlen(arr + 0));	//6

	//整数提升//arr char *//arr* char //strlen('a');
	//printf("%d\n", strlen(*arr));		//报错
	//printf("%d\n", strlen(arr[1]));	//报错
	
	printf("%d\n", strlen(&arr));		//6:虽然大小相同,但是与arr类型不同,存在强转

	printf("%p\n", (&arr + 1));			//000000597AAFFABB
	printf("%d\n", strlen(&arr + 1));	//21 //跳过整个数组//碰巧到21位遇到\0

	printf("%d\n", strlen(&arr[0] + 1));//5 //下标1往后

	return 0;
}

##二维数组

int main(){	
	//二维数组
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));			//48
	printf("%d\n", sizeof(a[0][0]));	//4
	printf("%d\n", sizeof(a[0]));		//16:下标为0的数组大小a[4]
	printf("%p\n", (a[0] + 1));			//-1517291332,地址值
	printf("%d\n", sizeof(a[0] + 1));	//4/8:a[1][0]的地址
	printf("%d\n", sizeof(*(a[0] + 1)));//4
	printf("%d\n", **(a + 1));			//为0
	printf("%d\n", sizeof(a + 1));		//4/8:跳过整个二维数组
	printf("%d\n", sizeof(*(a + 1)));	//16:下标为1的数组a[4]
	printf("%d\n", **(&a[0] + 1));		//为0
	printf("%d\n", sizeof(&a[0] + 1));	//4/8:指向a[1][0]指针
	printf("%d\n", sizeof(*(&a[0] + 1)));//16
	printf("%d\n", sizeof(*a));//16
	printf("%d\n", sizeof(a[3]));//16
    return 0;
}

3. C语言常见的库函数

3.1 字符串函数:

3.1.1 strlen函数

:求字符串的长度,遇到\0返回长度

头文件:#include <string.h>

特殊考点:当strlen遇上转义字符,求其长度

转义字符 含义 ASCII 值(十进制)
\n 换行(Newline) 10
\t 水平制表符(Tab) 9
\r 回车(Carriage Return) 13
\b 退格(Backspace) 8
\f 换页(Form Feed) 12
\v 垂直制表符(Vertical Tab) 11
\' 单引号 39
\" 双引号 34
\\ 反斜杠 92
\0 空字符(Null Terminator) 0
\a 响铃(Alert/Bell) 7

strlen与sizeof的区别

strlen遇见\0返回长度,sizeof求字符串大小包括\0

3.2.1 strcpy()
  • 函数模型:char * strcpy( char * str1, const char * str2 );
  • 参数:目标字符串和被拷贝字符串
  • 字符串拷贝,将字符串str2的内容拷贝到字符串str1中
#include <stdio.h>
#include <string.h>

int main()
{
	char str1[20];
	char str2[] = "abcdef";
	strcpy(str1, str2);//将字符串str2的内容拷贝到字符串str1中
	printf("%s", str1);
	return 0;
}
  • strcpy()函数在使用时,目标字符串的空间必须足够大比被拷贝的字符串空间大,因为strcpy是当遇到’\0’是才会停止拷贝,这样才能正常使用,如果空间太小,就无法使用,因此在有些编译器(比如VS)中会认为该函数不安全
  • 但是在C语言的库中,还有另一个字符串拷贝函数----strncpy(),相比于strcpy()函数更安全
3.3.1 strcat()
  • 函数模型:char * strcat( char * str1, const char * str2 );
  • 将字符串str2追加到字符串str1后面

strcat()函数注意事项

  • 与strcpy()函数一样,strcat()同样存在不安全的情况,当strcat()追加自己时,就无法完成
3.4.1 strstr()
  • 函数模型:char * strstr( const char * string, const char * strCharSet );
  • 字符串查找函数,如果字符串strCharSet在字符串string中,返回第一次出现的位置的指针,若不在string中,则返回空指针

3.2内存函数

3.2.1 memcpy()

void *memcpy(void *dst, void const *src, size_t length);

  • 函数memcpy从source的位置开始向后复制num个字节的数据到destination指向的内存位置。
  • 这个函数在遇到 '\0' 的时候并不会停下来。
  • 如果source和destination有任何的重叠,复制的结果都是未定义的。

对于重叠的内存,交给memmove来处理。 memcpy函数的模拟实现:
 

void * memcpy ( void * dst, const void * src, size_t count)
{
    void * ret = dst;
    assert(dst);
    assert(src);
    /*
    * copy from lower addresses to higher addresses
    */
    while (count--) {
        *(char *)dst = *(char *)src;
        dst = (char *)dst + 1;
        src = (char *)src + 1;
    }
    return(ret);
}
3.3.1 memmove()

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

  • 和memcpy的差别就是memmove函数处理的源内存块和⽬标内存块是可以重叠的。
  • 如果源空间和⽬标空间出现重叠,就得使⽤memmove函数处理。
3.4.1 memset

void * memset ( void * ptr, int value, size_t num );

##memset是⽤来设置内存的,将内存中的值以字节为单位设置成想要的内容。 
#include <stdio.h>
#include <string.h>
int main ()
{
    char str[] = "hello world";
    memset (str,'x',6);
    printf(str);
    return 0;
}
3.5.1 memcpy

void *memcpy(void *str1, const void *str2, size_t n)

  • str1 -- 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
  • str2 -- 指向要复制的数据源,类型强制转换为 void* 指针。
  • n -- 要被复制的字节数。

3.6 整形和字符串转换函数

3.6.1 atoi函数
  • 1.使用atoi函数需要包含<stdlib.h>头文件;
  • 2.当参数位置传入的为NULL指针时会报错,当参数位置传入的为空字符串时,返回0;
  • 3.atoi函数会自动过滤开始时的空白字符;
  • 4.atoi函数会根据字符串内容自动判断整数的正负;
  • 5.当字符串的数字大小超过了整型数字的取值范围时,返回时会变为整型数据的最大或最小值;
  • 6.当字符串中的数字有非10进制字符隔断时,atoi函数会直接返回当前位置前面的数字;
3.6.2 itoa()函数

char *itoa(int value, char *string, int radix);

4.结构体+枚举+联合

4.1 结构体

整体的大小在满足为最大数据类型所占字节的倍数下要达到所占内存最小。

最大对起数为4;

从上到下,c1占一字节,剩下字节不足4字节,浪费3字节,c2继续占有1字节,但要满足4字节

4.2 enum

enum 枚举名 {枚举常量1, 枚举常量2, ..., 枚举常量n};

enum Month {

Jan = 1, Feb, Mar, Apr, May, Jun,

Jul, Aug, Sep, Oct, Nov, Dec

};

##当指定Jan为1,后面默认一次向后+1

4.3 union

联合体中最大成员所占内存的大小且必须为最大类型所占字节的最小倍数。

5.内存管理

malloc和calloc和realloc的区别

malloc:开辟未初始化的连续内存块,num_bytes的大小,其值为不确定

void *malloc(unsigned int num_bytes);

calloc:开辟n个size的大小,其值为0

void *calloc(size_t n, size_t size);

realloc:开辟size大小的空间,成功返回指向新内存块的指针

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

特性     malloc     calloc     realloc
作用 分配指定大小的内存块 分配多个元素并初始化为0 调整已分配内存块的大小
是否初始化 否(内存内容是未定义的随机值)  是(全部初始化为0)    否(保留原有内容)
参数 单个参数:总字节数 两个参数:元素个数 × 单个元素大小  两个参数:原指针 + 新大小
返回值 成功返回指向内存块的指针,失败返回 NULL  同上  同上(可能返回新地址)
是否可能移动内存  是(如果原内存块无法扩展)
适用场景 快速分配一块未初始化内存 分配数组并初始化  扩展或缩小已分配内存
内存对齐   是(适合任意类型)  是

注意:必须用free释放内存

6、程序的编译

6.1 C/C++程序编译连接的过程

会经过预处理、编译、汇编、链接

预处理:

宏替换,条件编译,去掉注释,展开头文件,处理像 #include#define 这样的指令,展开宏,并包含头文件,从而为编译工作准备源代码。

编译:

检查语法生成汇编语言

汇编:

将汇编代码翻译成机器码,生成目标文件,生成以 .obj.o 为扩展名的目标文件

链接:

将目标文件链接在一起,生成可执行程序

6.2 常见的笔试面试题

1. 头文件中的 ifndef/define/endif是干什么用的?

:用于解决头文件重复的问题,如果存在头文件则不包含,不存在则包含头文件

2. #include <filename.h> 和 #include "filename.h"有什么区别?

:<>为系统所提供的我文件,""为用户所写的头文件

3. C/C++程序编译链接过程?分别完成了什么事情

:首先,发现函数,查询当前文件是否包含对函数的使用方法,不存在,则在包含的头文件中查找,查找到函数地址,进行重定义,将函数地址赋值

4. 宏的优缺点?C++中会使用什么技术去替代宏

:优点:由于宏函数的使用(区别与调用)不需要创建函数栈帧,且高频调用 的函数的函数结构比较简单,常被写成宏函数,可以节省空间,提高效率

缺点:宏的操作本质上是一个"代替"的操作,在预编译阶段,会对宏进行简单的文本替换,这个特性使得使用宏时要考虑宏的各种副作用(常量的前后置++--等)和各种操作符的优先级 (写宏函数要加各种括号),导致宏函数很复杂还容易出错,因为是文本替换的原因,宏在预处理被展开,宏函数不会建立函数栈帧,这也使得宏函数没法调试

C++中会使用什么技术去替代宏

inline函数

优点
  1. 减少函数调用开销:内联函数消除了函数调用的开销,尤其是在小型函数中,这种优化效果尤为明显。
  2. 提高代码可读性:将小函数定义为内联函数,可以使代码更加简洁,易于理解。
  3. 有助于常量表达式:内联函数可以用于常量表达式的计算,编译器可以在编译时求值。
缺点
  1. 代码膨胀:如果内联函数被多次调用,编译器会在每个调用点插入函数体,这可能导致代码膨胀,增加可执行文件的大小。
  2. 编译时间增加:内联函数的代码在每个调用点都被插入,可能导致编译时间增加,尤其是在大型项目中。
  3. 不保证内联:即使使用了inline关键字,编译器也不一定会将函数内联。编译器会根据函数的复杂性、调用频率等因素决定是否内联。

7. volatile、extern、static、const、typedef、sizeof的作用

一、const 关键字总结:

作为一个程序员,我们看到关键字const时,首先想到的应该是:只读。因为,它要求其所修饰的对象为常量,不可对其修改和二次赋值操作(不能作为左值出现)。看几个例子的中const作用:

1. 修饰常量

用const修饰的变量是不可变的,对const变量赋值:一是在声明时对变量初始化,二是在函数中声明const形参,函数调用时会得到实参的值。以下两种定义形式在本质上是一样的:

const int a = 10;
int const a = 10;
2. 修饰指针

如果const位于*的左侧,eg:const int* a,则const就是用来修饰指针所指向的变量,此时为常量指针,即指针指向*a为常量;
如果const位于*的右侧,eg:int* const a,const就是修饰指针本身,此时为指针常量,即指针本身a是常量。

规则:

  • const离谁近,谁就不能被修改;
  • 因为常量在定义以后就不能被修改,所以使用const定义变量时必须初始化。
3.修饰函数参数

二、static 关键字总结: 

用const修饰函数参数,传递过来的参数在函数内不可以改变。

不加static修饰,为自动变量,存储于堆栈中。函数或代码块内部中的变量在函数或者代码块执行完毕后就自行销毁了,每次执行都会重新分配内存,每次执行完都会销毁。
加 static 修饰,为静态变量,存储于静态内存中,在整个程序执行过程中一直存在。static的局部变量,并不会随着函数的执行结束而被销毁,当它所在的函数被再次执行时,该静态局部变量会保留上次执行结束时的值,直到程序结束 static 变量才会被回收。可延长生命周期。
 

三、extern关键字总结:

1、对于变量:

extern关键字可以用来修饰变量,表示该变量在别的文件中已有声明。

佷显然使用extern关键字修饰的变量都是全局变量,因为在其它文件中引用局部变量是不会有意义的,也超出了局部变量的作用域。

注:只能用于扩展没有被static关键字修饰的全局变量。默认情况下全局变量只能在定义它的文件中使用(从定义该全局变量开始到所在文件的文件尾),但如果在另一个文件中将这个变量声明为外部变量,那么这个变量的作用域将被扩展到另外一个文件中。也可以在定义全局变量之前声明该变量,从而在文件中可以在定义该全局变量前使用该全局变量。

​​​简单的理解就是:若一个变量需要在同一个工程中不同文件里直接使用或修改,则需要将变量做extern声明。只需将该变量在其中一个文件中定义,然后在另外一个文件中使用extern声明便可使用,但两个变量类型需一致。
 

四、volatile关键字总结:

        volatile关键字的基本作用:一个使用volatile关键字定义变量,其实就是告诉编译系统这变量可能会被意想不到地改变。那么编译时,编译器就不会自作主张的去假设这个变量的值,而进行代码的优化了。确切的说就是,编译器在编译代码时,优化器每次遇到这个变量,都会到内存中重新读取,而不会使用保存在寄存器里的备份来对代码进行优化。

  我们已经了解了volatile关键字的基本作用。那么,在什么情况下使用volatile关键字呢?一般说来,在如下的几种情况通常会使用volatile关键字:

  • 在中断服务程序中修改的,供其它程序检测的变量,通常需要定义为volatile;
  • 在多任务环境下,各任务间共享的标志,通常也需要定义为volatile;
  • 存储器映射的硬件寄存器通常也需要定义为volatile,因为每次对它的读写都可能有不同意义;

编译优化时,为提高存取速度,有时会把变量读取到寄存器,方便读取;但有时别的线程改变了变量的值,但寄存器值不变,造成程序读取值不一致,所以使用volatile从变量内存中读取。

修饰某个变量,表明某个变量的值可能随时被外部改变,因此对这些变量的存取不能缓存到寄存器,每次使用时需要重新读取,从变量的地址中(内存中)读取数据。

volatile的作用是告知编译器,它修饰的变量随时都可能被改变,因此,编译后的程序每次在使用该变量的值时,都会从变量的地址中读取数据,而不是从寄存器中获取。

#include <stdio.h>
 //n被const修饰,cpu认为n不会改变,则就回到寄存器中读取
int main()
{
	const int n = 10;
	int *p = (int*)&n;
	*p = 20;//我们修改了内存中的值
	printf("%d\n", n);//打印10
	return 0;
}

#include <stdio.h>
 
int main()
{
	volatile const int n = 10;//告诉寄存器可能会发生改变,所以每次读取到内存中读取,而非寄存器
	int *p = (int*)&n;
	*p = 20;
	printf("%d\n", n);
	return 0;
}

五.sizeof 与 typedef

sizeof作为C语言中求取类型所占字节数的宏,经常配合malloc等使用。
typedef则是用来表明,类型别名。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐