扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
gcc编译过程分为4个阶段:预处理、编译、汇编、链接。
预处理:头⽂件包含、宏替换、条件编译、删除注释
编译:主要进⾏词法、语法、语义分析等,检查⽆误后将预处理好的⽂件编译成汇编⽂件。
汇编:将汇编⽂件转换成 ⼆进制⽬标⽂件
链接:将项⽬中的各个⼆进制⽂件+所需的库+启动代码链接成可执⾏⽂件
全局变量存储在静态存储区中,局部变量在堆栈中
变量/函数的声明仅声明变量/函数存在于程序中的某个位置也就是后面程序会知道这个函数或者变量的类型,但不分配内存。
关于定义,当我们定义变量/函数时,除了声明的作用外,它还为该变量/函数分配内存。
NULL指针
NULL用于指示指针未指向有效位置。理想情况下,如果在声明时不知道指针的值,则应将指针初始化为NULL。
当由它指向的内存在程序中间被释放时,我们应该使指针为NULL。
悬空指针
悬空指针是没有指向正确内存位置的指针。当删除或释放对象时,如果不修改指针的值或者不置为NULL,就会出现悬空指针。
这时这个指针指向的内存可能被分配给了其他变量就会造成错误。所以是比较危险的。
野指针
就是只声明没有被初始化过的指针,他可能指向任何内存。
int * const p //指针常量:指针在前,常量在后
这个p只能指向一个位置,而不能指向其他位置。指向的变量值可以改变。
const int *p = &a; //常量指针 : 常量在前,指针在后
指向的变量值不能改变,但是可以改变这个指针指向的位置。
引用是C++的概念在C中叫取地址符号
值传递(swap1函数)
地址/指针传递(swap2函数)
引用传递(swap3函数)
void swap1(int,int); //值传递
void swap2(int *p1,int *p2); //地址传递
void swap3(int &a,int &b); //引用传递
这也就证明了地址传递和引用传递都是直接传递的变量所在的地址,函数的主要的作用就是对存储在地址中的变量进行直接的操作
而按值传递则不会修改原值
实际上指针传递也是值传递,他传递的是指针变量的值。
而引用则相当于间接寻址,他直接传递的是地址
c语言没有引用传递,只有c++有
结构体的浅拷⻉与深拷⻉当结构体中有指针成员的时候容易出现浅拷⻉与深拷⻉的问题。
浅拷贝存在的问题:当出现类的等号赋值时,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次free函数,此时teacher2已经是野指针,指向的内存空间已经被释放掉,再次free会报错;这时,必须采用深拷贝
深拷⻉就是,让两个结构体变量的指针成员分别指向不同的堆区空间,只是空间内容拷⻉⼀份,这样在各个结构体变量释放的时候就不会出现多次释放同⼀段堆区空间的问题
#include<>与#include ""的区别?include<>到系统指定⽬录寻找头⽂件,#include ""先到项⽬所在⽬录寻找头⽂件,如果没有找再到系统指定的⽬录下寻找
宏定义define宏定义又称为宏代换、宏替换,简称“宏”。
ifndef/define/endif 的作用?防止头文件被重复包含和编译。 头文件重复包含会增大程序大小,重复编译增加编译时间。
与内联区别#define 用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:
1.数据类型:const修饰的变量有明确的类型,而宏没有明确的数据类型
2.安全方面:const修饰的变量会被编译器检查,而宏没有安全检查
3.内存分配:const修饰的变量只会在第一次赋值时分配内存,而宏是直接替换,每次替换后的变量都会分配内存
4.作用场所:const修饰的变量作用在编译、运行的过程中,而宏作用在预编译中。从编译器的角度讲,大的优势是简单,方便。因为预处理就可以解决掉#define,不必让编译器来处理这个。
5.代码调试:const方便调试,而宏在预编译中进行所以没有办法进行调试。
在程序语句中使用的常量的地方, 最好是使用const定义,在这方面来说, const只有优势,没有劣势.如果要说const劣势的地方,那就是它不能做上面3中的define的需要在预处理的时候做的事情.其实,这并非它的劣势,而只是不是它所要担负的工作罢了.
c语⾔中有符号和⽆符号的区别?有符号:数据的最⾼位为符号位,0表示正数,1表示负数
⽆符号:数据的最⾼位不是符号位,⽽是数据的⼀部分,只有正数,所以范围更大
计算机只有加法处理方式
将符号位与其他位统⼀处理将减法运算转换成加法运算
指针:内存中每⼀个字节都会分配编号,这个编号就是地址, ⽽指针就是内存单元的编号。一个变量的地址就称为该变量的指针他保存的是一个地址。
指针变量::c语言有很多种变量,每种变量都会储存一种数据,而指针变量就是专门来储存指针的变量,本质是变量 只是该变量存放的是空间的地址编号
二级指针:指针本身也是一个变量,也要占用内存空间,而二级指针就是指向这块变量的指针。一般二级指针用在二维数组中。
int *p;
p=&a;
int *p就是指针变量
对a取地址,p就是一个指针用来保存地址。
C语言开发对内存使用有区域划分,分别是栈区(stack)、堆区(heap)、BSS、数据段(data)、代码段(text)。
BSS段 未初始化全局变量,未初始化全局静态变量
数据段 已初始化全局变量、已初始化全局静态变量、局部静态变量、常量数据
代码段 可执行代码、字符串常量
申请方式
stack:由系统自动分配。例如,声明在函数中一个局部变量int b;系统自动在栈中为b开辟空间
heap:需要程序员自己申请,并指明大小,在c中malloc函数
申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
申请效率的比较:
栈:由系统自动分配,速度较快。但程序员是无法控制的。
堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
堆和栈中的存储内容
栈:局部变量和形参
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
https://blog.csdn.net/blunt1991/article/details/14005995?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-1.pc_relevant_default&spm=1001.2101.3001.4242.2&utm_relevant_index=4
结构体中的成员拥有独⽴的空间,共⽤体的成员共享同⼀块空间,但是每个共⽤体成员能访问共⽤区的空间⼤⼩是由成员⾃身的类型决定。
共用体使用覆盖技术,成员变量相互覆盖。
意味着只读。防止被修饰的成员的内容被改变。明确的告诉使用者不要修改这个变量。
在函数声明时修饰参数,表示在函数访问时参数(包括指针和实参)的值不会发生变化。
对于指针而言,可以指定指针本身为const,也可以指定指针所指的数据为const,const修饰的都是后面的值
const int *b = &a;b不能修改
或者intconst b = &a;,b不能修改
同⼀个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的
不初始化:如果是局部数组 数组元素的内容随机 ,如果是全局数组,数组的元素内容自动赋为0
完全初始化:如果⼀个数组全部初始化 可以省略元素的个数 数组的⼤⼩由初始化的个数确定
数组名作为类型:代表的是整个数组的⼤⼩
数组名作为地址:代表的是数组⾸元素的地址
对数组名取地址:代表的是数组的⾸地址
arr //获得数组首元素地址等同于&arr[0]
arr、arr[0]、(arr+0) //获得数组首元素值
&arr //获得数组地址
arr+1 //获得数组第二个元素地址
arr[1]、*(arr+1) //获得数组第二个元素值
公式1:前面的地址必须是后面的地址正数倍,不是就补齐
公式2:整个Struct的地址必须是大字节的整数倍
int 4
double 8
char 1
占用字节为24
int 4
char 1
double 8
这样占用就是16节省了内存空间
https://blog.csdn.net/ijn842/article/details/81273232
volatile是一个类型修饰符(type specifier), 防止编译器对代码进行优化。
在编译期间,编译器可能对代码进行优化
当编译器看到此处的n被const修饰,从语义上来讲,n是不期望被修改的
所以优化的时候把n的值存放到寄存器中,以提高访问的效率
只要以后使用n的地方都去寄存器中取,即使n在内存中的值发生变化,寄存器也不受影响,所以输出的n的值为10。也就是多线程其他线程可能读到的值是修改之前的。
使用volatile修饰之后,就不会将此值拷贝到寄存器中,那么其他线程可以读到修改后的值,也就是可见性。
volatile的作用就是被设计用来修饰被不同线程访问和修改的变量
https://www.jianshu.com/p/29eb7b5c8b2d
attribute:属性,主要是用来在函数或数据声明中设置其属性,与编译器相关
可以设置多种属性:比如
1 int Add(int x,int y)
2 {3 int sum = 0;
4 sum = x + y;
5 return sum;
6 }
7
8 int main ()
9 {10 int a = 10;
11 int b = 12;
12 int ret = 0;
13 ret = Add(a,b);
14 return 0;
15 }
1、参数拷贝(参数实例化)分配内存。
2、保存当前指令的下一条指令,并跳转到被调函数。
这些操作均在main函数中进行。
接下来是调用Add函数并执行的一些操作,包括:
1、移动ebp、esp形成新的栈帧结构。
2、压栈(push)形成临时变量并执行相关操作。
在一个栈中,依据函数调用关系,发起调用的函数(caller)的栈帧在下面(高地址方向),被调用的函数的栈帧在上面。
每发生一次函数调用,便产生一个新的栈帧,当一个函数返回时,这个函数所对应的栈帧被清除(eliminated)
3、return一个值。
这些操作在Add函数中进行。
被调函数完成相关操作后需返回到原函数中执行刚才保存的下一条指令,操作如下:
1、出栈(pop)。
2、恢复main函数的栈帧结构。(pop )
3、返回main函数
这些操作也在Add函数中进行。 至此,在main函数中调用Add函数的整个过程已经完成。
总结起来整个过程就三步:
1)根据调用的函数名找到函数入口;
2)在栈中审请调用函数中的参数及函数体内定义的变量的内存空间
3)函数执行完后,释放函数在栈中的申请的参数和变量的空间,最后返回值(如果有的话)
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流