目录
3.在1G内存的计算机中能否malloc(1.2G)?为什么?
1.int main(int argc, char ** argv)函数中,参数argc和argv分别代表什么意思?
2.函数参数压栈顺序,即关于__stdcall和__cdecl调用方式的理解
2.头文件#ifndef/#define/#endif的作用
1.int main(int argc, char ** argv)函数中,参数argc和argv分别代表什么意思?
2.头文件#ifndef/#define/#endif的作用
本文要点
一、关键字/常量/变量
1.全局变量和局部变量的区别
全局变量:
- 在函数外部声明的变量,整个程序都可以访问。
- 声明时会被默认初始化,可以在任何函数中使用。
- 生命周期长,整个程序执行期间都存在。
- 全局变量存储在全局数据区(data)中
局部变量:
- 在函数内部或代码块内部声明的变量,只能在所属的函数或代码块中访问。
- 声明时没有默认初始化,需要手动赋值才能使用。
- 生命周期短,只在所属的函数或代码块的执行期间存在。
- 局部变量存储在栈区(stack)
2.static关键字
- 声明静态变量,使其生命周期延长或作用域限定在当前文件内。
- 声明静态函数,使其作用域限定在当前文件内。
- 声明静态成员变量,使其属于类本身而不是对象,多个对象共享同一份内存。
- 使用静态限定符,控制变量的初始化和生命周期。
3.const关键字
- 值不可修改:一旦常量被赋值后,其值将保持不变,不能再对其进行修改。
- 作用域限制:常量的作用域通常被限制在声明时所在的作用域内部
- 编译时确定:常量的值在编译时就已确定,并在运行时保持不变
4.const 和 #define的区别
不同之处:处理阶段+类型+作用域+符号表
- const是一种编译器关键字,而#define是预处理器指令。const在编译阶段进行处理,而#define在预处理阶段进行处理。
- const定义的常量具有类型,而#define没有。const在声明时需要指定常量的类型,编译器会进行类型检查。而#define只是简单的文本替换,没有类型检查。
- const定义的常量有作用域限制,可以根据声明位置的不同而有不同的作用域。而#define定义的常量没有作用域限制,整个程序中都有效。
- const生成符号表中的一个符号,有明确的名字和类型,可以进行调试和符号查找。而#define没有生成符号表,不会产生对应的符号。
5.extern关键字
- 声明一个在其他文件中定义的外部变量或函数。
- 告诉编译器在链接过程中需要找到对应的定义。
- 允许在当前文件中使用这些外部变量或函数而不需要重新定义。
6.C语言的基本类型有哪些(32位系统),占用字节空间
7.volatile声明的作用
volatile声明的变量是指可能会被意想不到地改变的变量,这样编译器就不会轻易优化该变量。它主要用于多线程编程中,用来保证共享变量的内存可见性。(注:指针也可用volatile)
三个常见场景
- 多线程中的共享变量
- 中断程序中访问到的非自动变量
- 并行设备的硬件寄存器
8.strcpy与memcpy的区别
strcpy:
- 用于字符串拷贝。
- 源字符串中的内容会被复制到目标字符串中,直到遇到字符串结束符 ‘\0’。
- 目标字符串必须有足够的空间来存储被复制的内容,否则可能导致缓冲区溢出。
memcpy:
- 用于字节级别的内存拷贝。
- 可以拷贝任意类型的内存块,不仅限于字符串。
- 不会检查字符串结束符,通过指定要拷贝的字节数进行拷贝。
- 可以用于拷贝部分或完整的数组、结构体等。
安全性:
- strcpy函数不进行源字符串长度的检查,如果源字符串太长,可能会导致目标字符串缓冲区溢出。
- memcpy函数本身没有长度限制,应确保源和目标内存区域不会发生重叠,否则可能会导致数据损坏。
- 为了提高安全性,可以使用像strcpy_s、strncpy_s这样提供了长度限制的函数。
总结:速记的话看这个
- strcpy适用于字符串拷贝,可以自动识别字符串结束符。
- memcpy适用于字节级别的内存拷贝,适用于任意类型的数据。
9.sizeof与strlen的区别
sizeof:
- 用于获取数据类型或变量的字节大小。
- 可以接受多种参数,包括数据类型、变量名、数组名等。
- 返回的是整个数据类型或变量占用的内存空间大小。
strlen:
- 用于获取以’\0’结尾的字符串的实际长度。
- 在运行时计算,需要遍历字符串的内容来确定长度。
- 返回的是字符串中的字符个数,不包括字符串结束符’\0’。
举例:
char str[] = "Hello";
size_t size_str = sizeof(str);
size_t length_str = strlen(str);
// size_str 的值为 6,因为包括字符串 "Hello" 的 5 个字符和结尾的 '\0',共 6 个字节
// length_str 的值为 5,因为字符串 "Hello" 有 5 个字符,不包括结尾的 '\0'
注意事项:
- sizeof返回的是静态的大小,而strlen返回的是实际的字符串长度。
- 在使用strlen时要确保操作的对象是以’\0’结尾的字符串,否则可能出现不确定的结果。
- sizeof可以用于任何数据类型或变量,而strlen只适用于字符串。
二、结构体/共同体/枚举/位运算
1.结构体和共用体的区别
结构体:
- 成员在内存中独立存储,每个成员占用独立的内存空间。
- 内存占用是成员之和,每个成员都占用独立的空间。
- 成员可以同时被访问,通过成员名字来访问。
- 适合存储和处理多个不同类型的数据,如员工信息、图形对象等。
共用体:
- 成员共享同一块内存空间,只能存储一个成员的值。
- 内存占用是最大成员的大小,所有成员共享该空间。
- 成员只能同时访问其中的一个,存取时要明确指定。
- 适合存储和处理只使用其中一种类型的数据,可以节省内存空间或进行数据类型转换。
#include <stdio.h>
struct MyStruct {
int x;
float y;
};
union MyUnion {
int a;
float b;
};
int main() {
struct MyStruct myStruct;
union MyUnion myUnion;
printf("结构体的大小: %zu 字节\n", sizeof(myStruct));
printf("共用体的大小: %zu 字节\n", sizeof(myUnion));
return 0;
}
结构体的大小: 8 字节
共用体的大小: 4 字节
三、指针
1.常见的变量定义
2.数组名与指针的区别
数组名:
- 是一个常量指针,指向数组的首元素。
- 大小固定为整个数组的大小。
- 无法被改变或重新赋值。
- 无法进行指针运算。
指针:
- 是一个变量,存储一个内存地址。
- 大小固定为指针类型的大小。
- 可以指向任意类型的对象。
- 可以被改变或重新赋值。
- 可以进行指针运算,如加法、减法等。
3.数组指针与指针数组的区别
数组指针(pointer to an array):数组指针是指向数组的指针变量。数组指针的类型声明中,数组指针可以指向整个数组,而不仅仅是数组的第一个元素。
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
// 遍历数组并打印元素
for (int i = 0; i < 5; i++) {
printf("%d ", *(ptr+i));
}
// 输出:1 2 3 4 5
指针数组(array of pointers):指针数组是一个数组,数组的元素都是指针。指针数组的每个元素指向一个独立的内存块,可以指向不同的变量或数据。
#include <stdio.h>
int main() {
int num1 = 1, num2 = 2, num3 = 3, num4 = 4, num5 = 5;
int* arr[5]; // 声明一个包含5个整型指针的指针数组
arr[0] = &num1;
arr[1] = &num2;
arr[2] = &num3;
arr[3] = &num4;
arr[4] = &num5;
printf("指针数组元素的值: ");
for (int i = 0; i < 5; i++) {
printf("%d ", *arr[i]); // 解引用指针数组的元素以获取其值
}
return 0;
}
指针数组元素的值: 1 2 3 4 5
4.指针函数与函数指针的区别
指针函数(function returning a pointer):指针函数是一种返回指针类型的函数。调用指针函数时,会返回一个指向函数计算结果的指针。
#include <stdio.h>
int* addIntegers(int a, int b) {
int* result = (int*)malloc(sizeof(int));
*result = a + b;
return result;
}
int main() {
int* sum = addIntegers(5, 3);
printf("和: %d\n", *sum);
free(sum);
return 0;
}
输出:和: 8
函数指针(pointer to a function):函数指针是指向函数的指针变量。函数指针的类型声明中,指针符号 * 出现在函数名之前,用于表示函数指针的类型。函数指针可以用于直接调用指向的函数,或者作为参数传递给其他函数。
#include <stdio.h>
int addIntegers(int a, int b) {
return a + b;
}
int main() {
int (*ptr)(int, int); // 声明一个指向以两个整数为参数并返回整数的函数的指针变量
ptr = addIntegers; // 将函数地址赋值给函数指针
int sum = ptr(5, 3); // 通过函数指针调用函数
printf("和: %d\n", sum);
return 0;
}
输出:和: 8
四、内存管理
1.内存分布模型
上图是比较经典的内存分布的模型图,下面将对上图中的不同的组成部分进行详细解释(从低地址到高地址)注:必须知道组成结构但是具体的含义只需要理解。
- 代码段:存放程序的机器指令(即二进制代码)。通常是只读的,因为程序的指令在执行过程中不应该被修改。
- 数据段:存放已初始化的全局变量和静态变量。这些变量在程序开始运行时已经赋予了初始值。
- BSS 段:存放未初始化的全局变量和静态变量。它们在程序开始运行时会自动初始化为0或者空指针。
- 堆区:动态分配的内存空间,用于存放程序运行时动态申请的内存。(程序员可以通过函数(如malloc、calloc等)或者操作系统提供的接口来申请和释放堆内存,堆从低地址向高地址增长。)
- 栈区:存放函数的局部变量、函数参数值以及函数调用和返回时的相关信息。栈区是按照“先进后出”的原则进行管理,内存的分配和释放是自动进行的,栈从高地址向低地址增长。是一块连续的空间。
- 共享区:也称为文件映射或共享内存,用于实现不同进程之间的内存共享。
2.malloc和calloc的区别
malloc
- malloc 分配的内存是未初始化的,其中的字节内容是不确定的(可能是随机值)。
- 如果内存分配失败,malloc 返回一个空指针 NULL,可以通过检查返回值来判断是否分配成功。
calloc
- calloc 分配的内存会被初始化为全0。
- calloc 在分配失败时会自动抛出错误(异常),可以使用异常处理机制来捕获和处理错误。
3.malloc的底层原理
1.结论:
- 当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
- 当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。
2.具体实现
- 当调用 malloc(size) 时,它首先计算需要分配的内存块大小,包括用户请求的大小以及内存管理所需的额外空间(例如内存块的管理信息)。
- malloc 会遍历一个数据结构(例如空闲链表或空闲块列表),查找合适大小的空闲内存块。
- 如果找到了合适的内存块,malloc 会将其标记为已分配,并返回一个指向该内存块的指针给用户。
- 如果没有足够大的空闲内存块可用,malloc 可能需要扩展程序的虚拟内存空间。它通过系统调用(例如 brk 或 mmap)向操作系统请求更多的连续内存空间。
- 当操作系统提供了更多的内存空间后,malloc 可以从新的空间中分配出合适大小的内存块,并将其标记为已分配。
- 在内存块被释放时,通过调用 free 函数,malloc 将其标记为未分配,并将该内存块添加到空闲内存块的列表中,以便后续的内存分配可以重复使用它们。
3.在1G内存的计算机中能否malloc(1.2G)?为什么?
是有可能申请1.2G的内存的。
malloc能够申请的空间大小与物理内存的大小没有直接关系,仅与程序的虚拟地址空间相关。根据 malloc
函数的作用和原理,应用程序通过 malloc
函数在虚拟地址空间中申请内存,并且与物理内存没有直接的关系。 malloc
返回的是在虚拟地址空间中的地址,而物理内存的分配是由操作系统完成的。
假设我们需要申请的内存大小为 1.2GB,转换为字节为 2^30 × 1.2 Byte,这个数值仍然在 unsigned int
的表示范围内。因为 malloc
函数需要一个 unsigned int
类型的参数来指定内存大小。
在当前使用的 Windows 环境中,可申请的最大内存空间通常超过 1.9GB。然而,具体可申请的内存大小受到操作系统版本、程序本身的大小、动态/共享库的使用情况、程序栈的大小等因素的影响。每次运行的结果可能存在差异,因为有些操作系统使用随机地址分布技术,导致进程的堆空间变小。
综上所述,根据 malloc(size_t)
函数的调用,是有可能申请超过机器物理内存大小的内存块的。
4.简述大端和小端
这两种字节序的选择取决于计算机系统架构的约定。大部分个人电脑和服务器采用小端字节序,例如 x86 架构的计算机。而一些嵌入式系统和网络协议则常使用大端字节序。
- 大端字节序:将多字节数据的高位字节存储在低地址位置,低位字节存储在高地址位置。类比:高位字节在前,低位字节在后。举例:整数值 0x12345678 在大端字节序中存储为 12 34 56 78,即高位字节 12 存储在低地址位置,低位字节 78 存储在高地址位置。
- 小端字节序:将多字节数据的低位字节存储在低地址位置,高位字节存储在高地址位置。类比:低位字节在前,高位字节在后。举例:整数值 0x12345678 在小端字节序中存储为 78 56 34 12,即低位字节 78 存储在低地址位置,高位字节 12 存储在高地址位置。
5.数组到底存储在哪里
数组在内存中存放的位置取决于数组的类型以及其定义的位置。在大多数情况下,数组被存储在栈内存或堆内存中。
- 栈内存:局部变量和函数参数通常存储在栈内存中。当您在一个函数内部定义数组时,它会分配一块连续的内存区域,用于存储数组元素。这种分配的内存空间在函数执行完毕后会自动回收。
- 堆内存:当使用 malloc()、calloc() 或 new 等动态内存分配函数分配数组时,它会存储在堆内存中。堆内存需要手动分配和释放,可以在函数执行期间或跨函数访问。
6.简述内存泄漏,如何检测和避免?
内存泄漏是指在程序运行过程中,分配的内存没有被及时释放,导致这部分内存无法再被程序使用。长时间运行或发生频繁的内存泄漏可能导致系统资源不足,性能下降或程序崩溃。
常见内存泄露情况:
- new和malloc申请资源使用后,没有用delete和free释放;
- 子类继承父类时,父类析构函数不是虚函数。
- 比如文件句柄、socket、自定义资源类没有使用对应的资源释放函数。
- shared_ptr共享指针成环,造成循环引用计数,资源得不到释放。
检测内存泄漏:
- 使用内存分析工具:使用工具如Valgrind(Linux),Dr. Memory(Windows),Instruments(macOS)等来检测内存泄漏。这些工具可以检查程序运行时的内存分配和释放情况,并报告潜在的泄漏位置。
避免内存泄漏:
- 使用智能指针:C++中的智能指针(如std::shared_ptr、std::unique_ptr)可以自动管理内存释放,避免手动释放内存的疏忽。
- 务必正确配对分配和释放:确保每次内存分配都有相应的释放,释放内存的操作必须与分配内存的操作对应(malloc和free 一定要malloc和free的次数一致否则就泄露)。
- 减少全局变量和长时间存活的对象:全局变量和长时间存活的对象可能导致无法释放的内存,尽量避免过多使用。
- 使用容器类的自动销毁机制:使用容器类如std::vector、std::map等,它们在销毁时会自动释放内部元素的内存。
- 注意循环引用:避免出现对象之间的循环引用,可以使用弱引用或断开引用关系的方式解决。
- 规范化资源管理:对于文件、网络连接等资源,确保在使用后及时释放,避免产生不必要的资源泄漏。
五、函数
1.int main(int argc, char ** argv)函数中,参数argc和argv分别代表什么意思?
在C语言中,主函数int main(int argc, char **argv)
用来作为程序的入口,argc
和argv
是其参数。
argc
是整型参数,表示命令行参数的个数。它记录了程序在运行时附带的命令行参数的数量,至少为1,因为程序自身的名称也算一个参数。argv
是一个指向字符串指针的数组,用来存储命令行参数的字符串。每个元素指向一个以null结尾的字符串,表示一个命令行参数。argv[0]
指向程序的名称,argv[1]
指向第一个参数,以此类推,argv[argc-1]
指向最后一个参数。
举个例子,假设我们在命令行中执行以下命令:
./program arg1 arg2 arg3
那么argc
的值为4,argv
的值如下所示:
argv[0] -> "program"
argv[1] -> "arg1"
argv[2] -> "arg2"
argv[3] -> "arg3"
argv[4] -> NULL
2.函数参数压栈顺序,即关于__stdcall和__cdecl调用方式的理解
函数参数压栈顺序是指在函数调用过程中,函数的参数是如何在栈上被压入内存的顺序。
在C/C++中,有两种常见的调用方式:__stdcall和__cdecl。下面是对它们的简要说明:
- __stdcall调用方式:在__stdcall调用方式下,函数的参数是从右往左依次压入栈中。被调函数负责清理栈上的参数。__stdcall调用方式通常被用于Win32 API,因为它具有固定的参数顺序,方便调用方和被调用方之间的交互。
- __cdecl调用方式:在__cdecl调用方式下,函数的参数是从右往左依次压入栈中。调用方负责清理栈上的参数。__cdecl调用方式是C/C++默认的函数调用方式。
使用__stdcall调用方式的主要原因是为了确保与其他代码库或操作系统的交互的正确性和一致性,两种不同的调用方式的区别就是为什么应对不同库的一致性和兼容性。
六、文件操作
1.#include<> 和 #include""的区别
从包含内容+搜索路径+文件目录对比
使用 #include<>:
- 用于包含系统提供的标准库头文件。
- 在编译器的搜索路径中寻找头文件。
- 编译器会先在系统的标准头文件目录中查找,如果找不到则报错。
使用 #include"":
- 用于包含用户自定义的头文件或项目中使用的其他非系统头文件。
- 在当前源文件的相对路径或指定的绝对路径中寻找头文件。
- 编译器会首先在当前源文件所在目录中查找,如果找不到再根据指定的路径查找。
2.头文件#ifndef/#define/#endif的作用
- #ifndef:用于判断当前头文件是否已经被包含。
- 如果该宏之前没有被定义过,则继续编译下面的代码。
- 如果该宏之前已被定义过,则跳过下面的代码,直接到 #endif。
- #define:用于定义一个宏。
- 通过定义一个特定的宏名称,例如
MY_HEADER_H
表示头文件已被包含。
- #endif:用于结束 #ifndef / #define / #endif 块。
- 标记了头文件的结束位置。
通过使用这种组合,可以防止同一个头文件被多次包含,以避免重复定义和编译错误。
七、其他知识
例如数据类型、输入输出格式化、运算符等
1.原码、反码、补码的定义
正数三码合一,负数原码最高位取1&反码除最高位都取反&补码对反码加一
原码:最高位表示符号,其余位表示数值的绝对值。正数的原码就是二进制表示,符号位为0。负数的原码符号位为1,数值位根据绝对值的二进制表示。
反码:正数的反码与原码相同,负数的反码是对原码除符号位外的每一位取反(0 变为 1,1 变为 0)。
补码:正数的补码与原码和反码相同,负数的补码是对反码加 1。