提示:本文章会持续更新......
在C语言中,内存主要分为5个区,分别为栈区、堆区、全局/静态存储区、常量存储区、代码区
栈区(stack)
栈区介绍:
1.栈区由编译器自动分配释放,由操作系统自动管理,无须手动管理。
2.栈区上的内容只在函数范围内存在,当函数运行结束,这些内容也会自动被销毁。
3.栈区按内存地址由高地址到低地址方向增长,其最大大小由编译时确定,速度快,但自由性差,最大空间不大。
4.栈区是先进后出原则,即先进去的被堵在屋里的最里面,后进去的在门口,释放的时候门口的先出去。
存放内容:
临时创建的局部变量和const定义的局部变量存放在栈区。
函数调用和返回时,其入口参数和返回值存放在栈区。
堆区(heap)
堆区介绍:
1.堆区由程序员分配内存和释放。
2.堆区按内存地址由低地址到高地址增长,用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上。
注意:堆内存牢记不忘释放,避免内存泄漏的情况
全局(静态)区
全局(静态)区介绍:
1.编译器编译时即分配内存,全局变量和静态变量的存储是放在一块的。
C语言中,已初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
全局区有 .bss段 和 .data段组成,可读可写。
常量区
1.字符串、数字等常量存放在常量区。
2.const修饰的全局变量存放在常量区。
3.程序运行期间,常量区的内容不可以被修改。
代码区
1.程序执行代码存放在代码区,其值不能修改(若修改则会出现错误)。
2.字符串常量和define定义的常量也有可能存放在代码区。
1.分配方式:
栈区: 栈区是由编译器自动分配和释放的,用于存储局部变量和函数调用信息。栈的分配和释放是由编译器隐式完成的,遵循后进先出(LIFO)的原则。
堆区: 堆区是由程序员手动管理的,用于存储动态分配的内存。在堆上分配内存需要使用诸如 malloc、calloc 或 new 等函数,而释放内存则需要使用相应的释放函数,例如 free 或 delete。
2.生命周期:
栈区: 变量在栈上的生命周期与其所在的函数调用相关,当函数返回时,栈上的局部变量会被自动销毁。
堆区: 堆上的内存的生命周期由程序员显式管理,可以在需要时分配,直到显式释放。
3.大小限制:
栈区: 栈的大小通常受到限制,因为它由系统分配,并在函数调用时自动管理。栈上的变量通常较小,并受限于栈的大小。
堆区: 堆的大小通常受到系统或编译器的限制,但相对于栈来说,它可以分配更大的内存块。
4.分配效率:
栈区: 栈的分配和释放是非常高效的,因为只需要移动栈指针即可完成。
堆区: 堆的分配和释放可能相对较慢,因为需要搜索合适的内存块并进行管理。
总体来说,栈区用于存储局部变量和函数调用信息,其管理由编译器自动完成;
而堆区用于存储动态分配的内存,其管理由程序员手动进行。
正确的使用和管理堆内存对于避免内存泄漏和提高程序的灵活性至关重要。
1.可以由数字、字母、下划线_组成
2.不能以数字开头
3.不能是关键字
4.区分大小写
声明:
声明只是告诉了编译器存在这么一个变量或者函数,这个函数或者变量在其他的位置定义过了,所以在这个过程中没有为其再次分配内存,因此声明可以声明多次;
声明并不会给变量分配内存空间!!
定义:
程序在运行时为其变量或函数开辟内存空间,定义只能定义一次。
至于初始化,则是完成内存空间开辟,为其开辟的内存填指定的值。
定义会给变量分配内存空间!!
在计算机中,内存的最小存储单位是比特(bit),而最小计量单位是字节(byte)。
1.最小存储单位 - 比特(bit):
定义: 比特是计算机中最基本的存储单位,它表示二进制的 0 或 1。
用途:比特通常用于表示数据的最小单元,例如一个开关的状态、一个电信号的状态等。
2.最小计量单位 - 字节(byte):
定义: 字节是计算机中最小的可寻址的存储单位,由8个比特组成。
用途: 字节是计算机内存和存储介质中最基本的单元,所有其他的存储单位(如千字节、兆字节等)都是基于字节的倍数。
在二进制计量中,1 字节等于 8 比特。因此,字节是计算机系统中最小的、可寻址的存储单元。存储容量通常用字节作为基本单位,并以字节的倍数(如千字节、兆字节、吉字节等)来表示内存和存储容量的大小。
c语言的编译过程可以大致分为4个阶段:预处理、编译、汇编、连接
1.预处理: 头文件包含、宏替换、条件编译、删除注释
2.编译: 主要进行词法、语法、语义分析等,检查无误后将预处理好的文件编译成汇编文件。
3.汇编: 将汇编文件转换成二进制目标文件
4.链接: 将项目中的各个二进制文件和所需要的库和启动代码链接成可执行文件。
1.#include<>到系统指定目录寻找头文件
2.#include" "先在项目所在目录寻找头文件,如果没有再到系统指定目录寻找头文件
从严格意义上来说,指针和指针变量是不同的,指针就是地址值,是一个数据,而指针变量是C语言中的变量,要在待定区域开辟空间,要用来保存地址数据,还可以被取地址。
区别:
指针的本质就是地址!
指针变量的本质是变量,是用来存放地址的!也就是说指针变量是用来存放数据的。
野指针概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针产生的原因:
1.指针未初始化就使用
2.指针越界访问
3.指针指向了一段已经释放的空间(指向了垃圾内存空间)
避免野指针的方法:
- 指针初始化
- 小心指针越界
- 指针指向空间释放即使置NULL
- 避免返回局部变量的地址
- 指针使用之前检查有效性
一个指针在32位的计算机上,占4个字节;
一个指针在64位的计算机上,占8个字节。
32位的操作系统有一个32位的地址总线,这意味着它可以寻址的内存空间为2^32字节(4 GB)。指针用于存储内存地址,因此在32位系统中,一个指针的大小为32位,即4字节。同理64位的操作系统也是这样。
1.最主要的问题是栈溢出(Stack Overflow)。这是由于每次函数调用都会在程序的调用栈上分配一些空间,而递归调用的深度过大会导致栈空间耗尽。
2.执行效率降低。 递归调用相对于迭代(循环)调用通常会涉及更多的函数调用开销和额外的栈空间分配,可能会导致性能下降。
(1)常用来跳出死循坏;
(2)打印错误;
因为goto会破坏程序的栈逻辑,同时使用 goto 可能导致代码的结构变得复杂,使得程序更难以理解和维护。
在计算机中,内存空间是按照字节(1B = 8 bit)划分的,每一个字节都有一个编号,这就是字节的地址。理论上可以从任意起始地址访问任意数据类型的变量,但在实际使用中,访问特定数据类型变量时需要在特定的内存起始地址进行访问,这就需要各种数据类型按照一定的规则在空间上进行排列,而不是顺序地一个接一个地存放,这就是字节对齐。
字节对齐简单点来说就是将不同的数据类型在内存中按照一定的规则对齐,(要求数据在内存中的存储起始地址是其大小的整数倍。)使得计算机在访问内存时更加高效。
在C语言中局部变量可以与全局变量同名,局部变量会屏蔽全局变量,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。
在 C 语言中,const 是一个关键字,用于声明常量和指定变量为只读。const 关键字的主要用途有以下几个:
1.声明常量
2.指定只读变量
3.指定只读指针
4.指定只读函数参数
volatile 是一个在 C 语言中用于声明变量的关键字,它告诉编译器该变量的值可能在程序执行期间被外部因素更改,因此编译器不应该对该变量的访问进行优化。volatile 主要用于以下几种情况:
1.中断服务程序中的共享变量: 当一个变量同时被中断服务程序和主程序使用时,为了确保对该变量的访问是可见的,可以使用 volatile 声明。
volatile int flag;
// 中断服务程序
void interruptHandler() {
flag = 1;
}
// 主程序
int main() {
while (!flag) {
// 等待中断标志的变化
}
// 处理中断
return 0;
}
2.多线程程序中的共享变量: 当一个变量同时被多个线程访问时,为了避免编译器对变量访问进行优化,可以使用 volatile。
volatile int sharedVariable;
// 线程1
void thread1() {
sharedVariable = 1;
}
// 线程2
void thread2() {
while (sharedVariable == 0) {
// 等待共享变量的变化
}
// 处理共享变量
}
3.对硬件寄存器的访问: 在嵌入式系统等情境下,volatile 常用于声明对硬件寄存器的访问,以确保编译器不会对这些访问进行优化。
volatile uint32_t *hardwareRegister = (uint32_t *)0x1000;
// 通过硬件寄存器进行操作
*hardwareRegister = 0xABCDEFF0;
#define GPD0CON (*(volatile unsigned int *)0x110002E0)//lED灯
(1)sizeof是运算符,用于获取数据类型或对象在内存中占用的字节数,sizeof 接受类型或表达式作为参数,返回指定类型或对象的字节大小。
(2)strlen() 是一个函数,用于计算字符串的长度,即字符串中的字符个数,不包括结尾的 null 字符 (‘\0’),strlen 接受一个以 null 结尾的字符串作为参数,返回字符串的长度。
#include
#include
int main(int argc, char *argv[])
{
char arr[]="hello";
printf("sizeof=%ld\n",sizeof(arr));
printf("strlen=%ld\n",strlen(arr));
return 0;
}
1.内存泄漏:
内存泄漏指的是程序在动态分配内存后,没有正确释放或者无法释放这部分内存,导致程序无法再次使用这块内存,从而造成了内存资源的浪费。
2.内存溢出:
内存溢出是指程序试图向一个已经分配的内存区域写入超过该区域大小的数据,导致数据越过了内存边界,覆盖了其他内存区域的数据
1.指针赋值字符串:
char *str = “Hello, World!”;
使用指针时,实际上是将指针指向字符串常量的首地址,字符串常量存储在只读的数据段,不能通过指针修改字符串的内容。
2.数组赋值字符串:
char str[] = “Hello, World!”;
使用字符数组时,会在栈上分配足够的空间来存储字符串的内容,并在数组的末尾添加 null 字符 (‘\0’),可以通过数组进行字符串的读写操作,因为它在栈上分配了一块可读写的内存空间。
总体而言,使用指针赋值字符串时,指针指向字符串常量,不能修改字符串内容。而使用字符数组赋值字符串时,数组在栈上分配一块内存,可以进行读写操作。
for、while、dowhile、goto四种方式。
(1)malloc申请后空间的值是随机的,并没有进行初始化;而calloc却在申请后,对空间逐一进行初始化,并设置值为0;
(2)malloc要申请的空间大小,需要我们手动的去计算;calloc并不需要人为的计算空间的大小。
(1)全局变量保存在内存的全局存储区中,占用静态的存储单元;(全局(静态)区)
(2)局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。(栈区)。
1.使用#define宏定义:#define LENGTH = 10
2.使用const关键字:const int LENGTH = 10 或 int const LENGTH = 10
3.使用枚举:enum { PI = 3.14159 };
区别:
这些方法之间的主要区别在于安全性和作用域。使用#define的常量没有类型,而使用const关键字的常量是有类型的。 使用const是更安全和推荐的做法,因为它提供了类型检查,而#define则可能导致潜在的错误。此外,#define是在预处理阶段进行文本替换的,可能会导致一些意外的行为,而const关键字是在编译阶段处理的。
#define 是一个预处理器指令, 用于在编译之前进行文本替换。
它主要用于创建宏定义,包括常量、函数或代码片段的文本替换。
没有类型检查,仅仅是简单的文本替换,因此可能导致错误难以调试。
typedef 用于为数据类型创建新的名称。
它提供了一种更加抽象和安全的方式来引入新类型,并且在一些情况下提高了代码的可读性。
typedef 可以为数组、结构体、枚举等创建别名。
C语言中static关键字的常用于三种情况:修饰局部变量,修饰全局变量,修饰函数。
1.static修饰局部变量:static修饰的局部变量只能在函数体内被调用。 并且静态局部变量的值不会因为函数调用的结束而清除。当函数再次调用时,该变量的值是上次调用结束后的值。静态局部变量被存储在静态存储区(局部变量存放在栈中)。 并且静态局部变量会被自动初始化为0。
2.static修饰全局变量: static修饰的全局变量成为静态全局变量。该变量只能在被当前文件的所有函数访问,不可以被其他文件内的函数访问。
3.static修饰函数: static修饰的函数成为静态函数。同样静态函数只能在当前文件中被调用,不能被其他文件调用。将函数限制在当前源文件中,防止在其他源文件中被访问。
在C语言中,extern 关键字主要用于在一个文件中声明一个全局变量或函数,而实际的定义(分配存储空间)可能在另一个文件中。
1.声明全局变量:
使用 extern 可以在一个文件中声明一个在其他文件中定义的全局变量,使得当前文件能够访问该变量。
2.声明函数:
使用 extern 可以在一个文件中声明一个在其他文件中定义的函数,使得当前文件能够调用该函数。
3.避免重复定义:
当在多个文件中包含同一个头文件时,头文件中通常包含变量或函数的声明(而非定义)。使用 extern 可以在头文件中声明这些变量或函数,防止在多个文件中出现重复定义。
#define SECONDS_IN_YEAR (60*60*24*365)UL
1). #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)
2). 懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
3). 意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。
4). 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。
1.用while(1){ }
2.用for( ; ;) { }
3.用goto
Loop:
goto Loop;
防止头文件被多次引用
1.一个有10个指针的数组,该指针指向一个整型数。
int*a[10];
2.一个指向有10个整型数数组的指针
int (*a)[10];
3.一个指向函数的指针,该函数有一个整型参数并返
回一个整型数;
int (*a)(int);
4.一个有10个指针的数组,该指针指向一个函数,该
函数有一个整型参数并返回一个整型数;
int(*a[10])(int)
1.内存分配方式:
数组在内存中是一块连续的存储空间,元素之间的地址是相邻的。
链表的节点在内存中可以是分散的,每个节点包含一个数据元素和指向下一个节点的指针。
2.大小和扩展性:
数组的大小是固定的,一旦创建就不能轻易改变。如果需要更大的空间,可能需要重新分配内存,并将数据复制到新的内存位置。
链表的大小可以动态增长或缩小,因为它不需要一块连续的内存空间。
总的来说,数组的数据是连续存储的,大小固定,链表的数据可以随机存储,大小可以动态改变。
1.递归深度过大:
当一个函数递归调用的层数过多时,每次函数调用都会在栈上分配一些空间,如果递归深度太大,栈的空间可能会用完,导致堆栈溢出。
2.局部变量过多:
如果一个函数中声明了大量的局部变量,这些变量会在栈上分配空间,如果空间不够,就会导致栈溢出。
3.死循环:
如果程序中有死循环(循环条件永远为真),栈空间可能会被不断地占用,最终导致栈溢出。
4.递归调用缺少终止条件:
在递归函数中,如果没有正确设置递归的终止条件,递归调用可能会无限进行,最终导致栈溢出。
5.函数调用层次过多:
如果程序中存在过多的函数调用,每次函数调用都会占用一些栈空间,如果调用层次太多,栈的空间可能会不够。
6.大型数据结构:
在栈上分配大型的数据结构,尤其是递归调用中,可能导致栈空间迅速耗尽。
7.缓冲区溢出:
在使用数组或缓冲区时,如果写入的数据超过了数组或缓冲区的边界,可能导致栈溢出,覆盖了相邻的栈帧。
8.函数调用时参数传递错误:
如果函数的参数传递错误,可能导致在函数内部访问了不属于当前栈帧的数据,从而破坏了栈的结构。
假设a=10,b=20;
1.通过算数运算
#include
int main(int argc, char *argv[])
{
int a=10;
int b=20;
a=a+b;
b=a-b;
a=a-b;
printf("a=%d\n",a);
printf("b=%d\n",b);
return 0;
}
2.通过位运算
#include
int main(int argc, char *argv[])
{
int a=10;
int b=20;
a=a^b;
b=a^b;
a=a^b;
printf("a=%d\n",a);
printf("b=%d\n",b);
return 0;
}
1.语法和操作符:
指针: 使用 * 操作符来声明指针和访问指针指向的值,同时使用 & 操作符获取变量的地址。
int x = 10;
int *ptr = &x; // 指针声明和赋值
int value = *ptr; // 通过指针访问值
引用: 使用 & 操作符来声明引用,而在使用引用时不需要 * 操作符。
int x = 10;
int &ref = x; // 引用声明
int value = ref; // 直接通过引用访问值
2.空指针和空引用:
指针: 指针可以是空指针,即指向空地址(nullptr 或 NULL)。
引用: 引用必须在声明时被初始化,并且不能引用空值。引用在声明后始终引用同一个对象。
3.重新赋值:
指针: 指针可以在运行时重新赋值,指向不同的内存地址。
int y = 20;
ptr = &y; // 重新赋值给指针
引用: 引用在声明后不能被重新赋值为引用其他对象,一旦引用被初始化,它将一直引用同一个对象。
int z = 30;
ref = z; // 不是重新赋值引用,而是修改引用所引用对象的值
4.空间占用:
指针: 指针通常占用一定的空间,存储目标对象的地址。
引用: 引用在内部不占用额外的空间,它被视为原对象的别名。
5.null 检查:
指针: 需要进行 null 检查,以确保指针不为空,以免导致悬空指针引用。
引用: 不存在 null 引用的概念,因为引用必须在声明时初始化,且不会在其生命周期内指向其他对象。
1.内存分配方式:
结构体: 结构体的每个成员都占用独立的内存空间,成员之间不共享内存。
共用体: 共用体的所有成员共享同一块内存空间,共用体的大小等于最大成员的大小。
2.成员访问:
结构体: 结构体的成员可以同时被访问,每个成员都有独立的内存地址。
共用体: 共用体的不同成员共享相同的内存空间,只能同时访问一个成员。
3.内存占用:
结构体: 结构体的大小等于所有成员大小的总和,每个成员都有独立的内存。
共用体: 共用体的大小等于最大成员的大小,因为共用体在任何时候只能存储一个
大端模式和小端模式的定义如下
1.小端模式就是;低位字节排放在内存的低地址端,高位字节排放在内存的高地址端
。
2.大端模式就是:高位字节排放在内存的低地址端,低位字节排放在内存的高地址端
。
举一个例子,比如数字0x12 34 56 78在内存中的表示形式为:
大端模式:
低地址 -----------------> 高地址
0x12 | 0x34 | 0x56 | 0x78
小端模式:
低地址 ------------------> 高地址
0x78 | 0x56 | 0x34 | 0x12
可见,大端模式和字符串的存储模式类似。
大端小端没有谁优谁劣,各自优势便是对方劣势:
小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。
大端模式 :符号位的判定固定为第一个字节,容易判断正负
代码验证:
答:是有可能申请1.2G的内存的。
解析:
回答这个问题前需要知道malloc的作用和原理,应用程序通过malloc函数可以向程序的虚拟空间,申请一块虚拟地址空间,与物理内存没有直接关系,得到的是在虚拟地址空间中的地址,之后程序运行所提供的物理内存是由操作系统完成的。
题目描述:
森林中有N棵树,它们都有灵性会相互攀比谁比较高,你是一个魔法
师,当你每次用魔法棒指定一棵树,这棵树的高度就不会变化,但其它树
的高度会加1,只有当所有树都一样高的时候,它们就不会再生长了。
你不希望他们无休止得长下去。问你至少操作多少次可以让所有的树都
相等呢?(比如你第一次指定第一棵树,下次指定了第二棵树,此时第一棵树还是会高度加一,每次指定只影响本轮的树是否生长)
例如:
3棵树高度是
1 2 3
第一次指定 第三棵不变,之后高度变成
2 3 3
第二次指定第二棵不变,之后高度变成
3 3 4
第三次指定第三棵不变,之后高度变成
4 4 4
然后高度就永远是4 4 4了。
答案就是3。即你至少需要使用3次魔法棒才能让所有树都相等
输入描述:
输入第一行给出一个整数n,
第二行给出n个整数,空格隔开
1 <= n <= 1000000
1<=每棵树高<=10000000
输出描述:
输出为一个整数表示最少使用魔法棒的次数
#include
#include
void arr_sort(int *arr, int n)
{
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = 0;
temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
}
int main(int argc, char *argv[])
{
int n = 0;
int count = 0;
while (1)
{
printf("请输入树的数量:\n");
scanf("%d", &n);
if (n > 1)
{
break;
}
printf("无效输入,请重新输入\n");
}
int *arr = malloc(n * sizeof(int));
if (arr == NULL)
{
perror("malloc");
return 0;
}
printf("请分别输入这些树的初始高度:\n");
for (int i = 0; i < n; i++)
{
scanf("%d", &arr[i]);
}
arr_sort(arr, n); //排序函数
printf("施魔法过程:\n");
do
{
for (int i = 0; i < n - 1; i++)
{
arr[i] += 1;
}
count++;
arr_sort(arr, n); //排序函数
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
} while (arr[n - 1] != arr[0]);
printf("施法次数:%d\n", count);
free(arr);
return 0;
}
strcpy:
strcpy函数可能会导致内存溢出。
strcpy拷贝函数不安全,他不做任何的检查措施,也不判断拷贝大小,不判断目的地址内存是否够用。
char *strcpy(char *strDest,const char *strSrc)
strncpy拷贝函数, 虽然计算了复制的大小,但是也不安全,没有检查目标的边界。
strncpy(dest, src, sizeof(dest));
strncpy_s是安全的。
strcmp:
用于比较两个字符串。不会导致内存溢出,但需要注意的是,如果字符串不以 null 结尾,可能会引发未定义的行为。
strcmp(str1,str2),是比较函数,若str1=str2,则返回零;若str1str2,则
返回正数。(比较字符串)
strcat:
将一个字符串连接到另一个字符串的末尾。可能导致内存溢出,因为它假设目标字符串有足够的空间来容纳连接后的结果。
strcat()函数主要用来将两个char类型连接。例如:
char d[20]=“Golden”;
char s[20]=“View”;
strcat(d,s);
//打印d
printf(“%s”,d);
输出 d 为 GoldenView (中间无空格)
strncat:
strncat()主要功能是在字符串的结尾追加n个字符
char * strncat(char *dest, const char *src, size_t n);
a) 一个整型数;
b)一个指向整型数的指针;
c)一个指向指针的指针,它指向的指针是指向一个整型数;
d)一个有10个整型的数组;
e)一个有10个指针的数组,该指针是指向一个整型数;
f)一个指向有10个整型数数组的指针;
g)一个指向函数的指针,该函数有一个整型参数并返回一个整型数;
h)一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数
答案:
a)int a
b)int *a;
c)int **a;
d)int a[10];
e)int *a [10];
f) int a[10], *p=a;
g)int (*a)(int)
h) int( *a[10])(int)