头文件的两种包含方式的区别
- 使用<>包含头文件名时,编译器会在系统默认的路径下寻找头文件。这些路径由编译器的环境变量所指定,通常包括标准库文件、系统头文件和其他系统支持的库。
- 使用""包含头文件名时,编译器会先在当前源代码文件所在的目录下查找头文件,如果找不到,再去系统默认路径下查找。通常,使用""包含头文件时,头文件是自己定义的或者是程序所在的项目中的头文件。
简述gcc编译过程
1.预处理(Preprocessing):预处理阶段主要处理源代码中的宏定义、条件编译和包含文件。预处理器会替换宏定义、处理条件编译指令(如、等)并将包含的头文件插入到源代码中。预处理后的输出是一个扩展后的源代码文件,通常具有(C语言)或(C++)扩展名。
2.编译(Compilation):编译阶段将预处理后的源代码文件转换为汇编代码。编译器会检查源代码的语法、语义是否正确,进行各种优化,并将源代码转换为目标平台的汇编指令。编译后的输出是一个汇编代码文件,通常具有扩展名。
3.汇编(Assembly):汇编阶段将汇编代码文件转换为目标代码(机器码)。汇编器会将汇编指令翻译为目标平台的机器指令,并生成一个目标文件。目标文件包含了程序的机器码、符号信息和其他元数据。汇编后的输出是一个目标文件,通常具有(Linux)或(Windows)扩展名。
4.链接(Linking):链接阶段将一个或多个目标文件和库文件组合成一个可执行文件或库文件。链接器会解析符号引用,将它们与正确的地址和实现关联,并处理程序中使用的库函数。链接器还负责组织程序的内存布局,如代码段、数据段等。链接后的输出是一个可执行文件,通常具有无扩展名(Linux)或(Windows)扩展名。
程序在执行int main(int argc, char *argv[])时的内存结构,你了解吗?
当程序执行int main(int argc, char *argv[])时,操作系统会为其分配一块内存区域作为进程的虚拟地址空间,该虚拟地址空间由下面几个部分组成:
• 代码段(text segment):存放程序的机器代码。
• 数据区:
初始化的全局变量和静态变量:存放程序中已经初始化的全局变量和静态变量。
未初始化的全局变量和静态变量:存放程序中未初始化的全局变量和静态变量。这部分区域在程序启动时通常会被清零。
• 堆(heap):动态申请的内存。
• 栈(stack):程序运行时的临时数据区。
其中,main函数的参数argc和argv存放在栈中,全局变量和静态变量存放在数据段中,而动态申请的内存存放在堆中。
main函数的返回值有什么值得考究之处吗?
main函数的返回值是整型,通常用来表示程序的执行状态,返回值为0表示程序正常退出,非0值表示程序异常退出或出现错误。在一些特殊的应用场合,main函数的返回值也可以被用来传递额外的信息,例如传递程序执行的时间、错误类型等信息。
形参和实参的区别
形参和实参都是函数中的变量,但是它们的含义和作用不同。形参是在函数定义时声明的变量,用来接收调用函数时传递的实参。而实参是在函数调用时传递给函数的值或变量。
指针和引用的区别,传引用和传指针区别
指针和引用的区别:
1.内存地址:指针是一个变量,存储的是指向内存地址的值。而引用是一个别名,本质上是已经存在的内存地址的另一个名称。
2.可空性:指针可以为空,即指向空地址,表示没有指向任何对象。而引用必须总是指向一个有效的对象,不能为NULL。
3.操作方式:指针需要用“*”运算符进行解引用操作,才能访问指向的内存地址中的值。而引用可以直接操作它所指向的对象。
传递参数时可以通过传值、传指针和传引用三种方式。传值方式是将实际参数的值拷贝到形式参数中进行处理,传指针和传引用方式则是将实际参数的内存地址传递给形式参数进行处理。它们之间的区别如下:
1.内存开销:传值方式会在内存中创建临时变量,需要占用额外的内存空间,而传指针和传引用方式则不需要创建临时变量,节省了内存开销。
2.对实参的影响:传值方式不会影响实际参数的值,而传指针和传引用方式会直接修改实际参数的值。
3.代码可读性:传值方式代码可读性较好,传指针和传引用方式代码可读性较差,需要使用“*”和“&”等符号进行操作。
野指针出现情况、怎么解决
野指针是指向无效内存地址或已释放内存区域的指针。野指针的出现可能导致程序崩溃、数据损坏或不可预测的行为。以下是一些常见的野指针产生情况:
1. 未初始化的指针:声明指针变量时未对其进行初始化,导致指针指向一个随机的内存地址。
int *ptr = NULL;
int *ptr = (int *) malloc(sizeof(int));
2. 已释放的内存:指针指向的内存区域已经被释放,但没有将指针置为NULL,仍然尝试访问该内存。
free(ptr);
ptr = NULL;
3. 指针越界:指针在数组或内存块中越过边界,访问了不属于它的内存区域。
if (ptr != NULL) {
// 使用指针
}
4. 内存泄漏:连续分配内存而未释放,导致原来的指针丢失,并且无法释放已分配的内存。
要解决野指针问题,可以采取以下措施:
1. 初始化指针:声明指针变量时,确保对其进行初始化。可以将指针初始化为NULL,或者分配内存并将地址赋给指针。
2. 释放内存后置空:当释放指针指向的内存后,应将指针设置为NULL,以避免悬空指针。
3. 使用指针前进行检查:在使用指针前,检查其是否为NULL,避免访问无效内存地址。
4. 避免指针越界:在使用指针访问数组或内存块时,确保不会越过边界。可以使用下标或指针算术进行访问,但要确保不超过分配的内存范围。
5. 及时释放内存:确保不再需要的内存被正确释放,以避免内存泄漏。
malloc与free的实现原理?
malloc和free是C语言中动态内存分配和释放函数,它们的实现原理可以概括为以下几个步骤:
1. 空闲链表:维护一个链表,存储已经被释放的内存块,以及它们的大小信息。
2. 内存分配:当需要申请一块内存时,malloc会遍历空闲链表,找到第一个大于等于所需内存的空闲块。如果该块大小超出所需内存,则将该块分裂成两部分,一部分返回给用户,另一部分仍然放回空闲链表中。
3. 内存释放:当用户释放一块内存时,free会将该块内存添加到空闲链表中。如果释放的内存与其他空闲块相邻,则会将相邻的空闲块合并成一个更大的空闲块。
4. 内存对齐:由于硬件对内存访问的要求,malloc和free函数对内存块的大小和地址都需要进行特殊处理,以保证内存访问的正确性和高效性。
5. new和delete的实现原理, delete是如何知道释放内存的大小的?
new和delete的实现原理, delete是如何知道释放内存的大小的?
new和delete是C++语言中动态内存分配和释放操作符,它们的实现原理可以概括为以下几个步骤:
1. new的实现原理:当使用new操作符申请内存时,C++运行时会调用operator new函数进行内存分配。在分配内存时,operator new会调用malloc函数来完成内存分配,并返回分配的内存地址。
2. delete的实现原理:当使用delete操作符释放内存时,C++运行时会调用operator delete函数进行内存释放。在释放内存时,operator delete会调用free函数来完成内存释放,并将该内存块标记为已释放状态。
3. delete如何知道释放内存的大小:在C++中,每个动态分配的对象都会存储一个额外的信息,用来标记该对象所占用的内存大小。在delete操作符释放内存时,C++运行时会读取这个信息,并传递给operator delete函数,以便准确地释放内存块。如果在使用new操作符时没有指定所分配的内存大小,C++运行时会根据对象的类型来自动计算所需要的内存大小。
malloc申请的存储空间能用delete释放吗?
malloc申请的存储空间不能用delete释放,应该使用free进行释放。同样,new申请的存储空间也不能使用free进行释放,应该使用delete进行释放。
既然有了malloc/free,C++中为什么还需要new/delete呢?
new/delete是C++中的内存分配和释放运算符,可以动态地分配和释放内存。与malloc/free相比,new/delete在使用上更方便、更安全、更易于维护,也更符合面向对象编程的理念。另外,C++中的new运算符可以自动调用对象的构造函数进行初始化,而delete运算符可以自动调用对象的析构函数进行清理。
C++命名空间namespace的作用
namespace是C++中的一种机制,用于防止命名冲突和代码模块化。通过将变量、函数、类等定义在命名空间中,可以有效地避免不同代码模块之间的命名冲突,同时也便于代码的维护和管理。例如,可以将所有与文件操作相关的函数和变量定义在一个命名空间中,以避免与其他模块中的同名函数和变量产生冲突。可以使用namespace关键字定义命名空间,使用using namespace或using关键字引用命名空间中的符号。
大小端基础
大小端是指在多字节的数据类型(例如整型、浮点型等)在内存中的存储方式。大端指高位字节存储在低地址,低位字节存储在高地址;小端则相反,低位字节存储在低地址,高位字节存储在高地址。
大小端各自的优点是什么?
大端和小端的优点主要在于不同的架构和应用场景。大端在网络通信中比较常用,因为网络传输时一般是按照大端字节序进行传输的。而小端在嵌入式领域比较常用,因为小端字节序与大部分处理器的寄存器和存储器排列方式一致,访问时效率更高。
如何用代码判断大小端存储?
#include <stdio.h> int main() { unsigned int x = 0x12345678; char *c = (char*) &x; if (*c == 0x78) printf("小端存储\n"); else printf("大端存储\n"); return 0; }
这段代码首先声明了一个32位的整数变量x,并将其赋值为0x12345678。然后,将指向x的指针转换为一个char类型的指针,以便按字节访问该变量的内存。接下来,将指针指向的第一个字节与0x78进行比较,如果相等,则说明该机器使用小端存储;如果不相等,则说明该机器使用大端存储。
大小端转化:对一个输入的整型数进行大小端存储模式转化
void swap_endian(int *p) { char *p1 = (char*)p; char *p2 = p1 + sizeof(int) - 1; char tmp; while (p2 > p1) { tmp = *p1; *p1++ = *p2; *p2-- = tmp; } }
简述字节对齐?
字节对齐(Byte Alignment)是计算机内存分配中的一种优化策略,用于确保数据结构的成员或变量在内存中以特定的边界对齐。对齐的目的主要是为了提高访问速度和满足硬件平台的要求。不同的处理器和操作系统可能有不同的对齐要求和策略。
字节对齐通常涉及以下几个方面:结构体对齐、动态内存分配对齐、对齐修饰符
结构体struct和联合体union的区别
结构体和联合体都是用来组织数据的复合数据类型,但是它们的定义和使用方式不同。结构体可以包含多个不同类型的成员变量,而联合体只能包含一个成员变量。在使用结构体时,所有成员变量都可以被访问和修改,而在使用联合体时,只能访问当前活动的成员变量。此外,结构体的大小是各个成员变量大小之和,而联合体的大小是最大的成员变量大小。
struct内存对齐问题
#include <stdio.h> struct A { char a;//1+3 int b;//4 };//8 struct B { char a;//1+3 int b;//4 short c;//2+2 };//12 struct C { short a;//2 char b;//1+1 int c;//4 };//8 struct D { char a;//1+1 short b;//2 char c;//1+3 int d;//4 };//12 struct E { char a;//1 char x;//保留1 short b;//2 int c;//4 };//8 struct I { char b;//1+3 int c;//4 }d;//8 struct II { int a; struct I { char b;//1+3 int c;//4 }d;//8 };//12 int main() { printf("sizeof(struct A): %d\n",sizeof(struct A)); printf("sizeof(struct B): %d\n",sizeof(struct B)); printf("sizeof(struct C): %d\n",sizeof(struct C)); printf("sizeof(struct D): %d\n",sizeof(struct D)); printf("sizeof(struct E): %d\n",sizeof(struct E)); printf("sizeof(struct I): %d\n",sizeof(struct I)); printf("sizeof(struct II): %d\n",sizeof(struct II)); return 0; }
union内存对齐问题
union U
{
char s[9];
int n;
double d;
};
s占9字节,n占4字节,d占8字节,因此其至少需9字节的空间。然而其实际大小并不是9,用运算符sizeof测试其大小为16.这是因为这里存在字节对齐的问题,9既不能被4整除,也不能被8整除。因此补充字节到16,这样就符合所有成员的自身对齐了。
C++的构造函数
在面向对象编程中,创建对象时系统会自动调用构造函数来初始化对象,构造函数是一种特殊的类成员函数,它有如下特点:
1、构造函数的名字必须和类名相同,不能任意命名;
2、构造函数没有返回值;
3、构造函数可以被重载,但是每次对象创建只会调用其中的一个;
C++中的构造函数可以分为4类:
1默认构造函数、2初始化构造函数、3复制构造函数、4转换构造函数
默认构造函数和初始化构造函数在定义类的对象的时候,完成对象的初始化工作。
Student s2(1002,1008);
3复制函数
Student s3(s2);//将对象s2复制给s3
Student s4;
s4=s2; //赋值
拷贝构造函数和赋值运算符的行为比较相似,却产生不同的结果;拷贝构造函数使用已有的对象创建一个新的对象,赋值运算符是将一个对象的值复制给另一个已存在的对象。区分是调用拷贝构造函数还是赋值运算符,主要是否有新的对象产生。
4转换构造函数
将一个其他类型的数据转换为一个类的对象。构造函数中的类型数据可以是普通类型,也可以是类类型。
将int类型的r转换为Student类型的对象。
Student(int r)
{
int num=1004;
int age=r;
}
初始化列表、构造函数执行的两个阶段
初始化类的成员有两种方式:一是使用初始化列表,二是在构造函数体内进行赋值操作。使用初始化列表主要是基于性能问题,对于内置类型,如int, float等,使用初始化类表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表,为什么呢?由上面的测试可知,使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效的。
析构函数
1) 析构函数与构造函数对应,当对象结束生命周期时,系统会自动执行析构函数
2) 析构函数名也与类名相同,只是在函数名前面加“~”,不带参数、无返回值、不能重载
3) 如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数,它也不进行任何操作
4) 如果一个类中有指针,且在使用过程中动态申请了内存,那么最好显示构造函数在销毁类之前,释放申请的内存空间,避免内存泄漏
深拷贝和浅拷贝
浅拷贝:简单的复制拷贝操作
深拷贝:在堆区重新申请内存空间,进行拷贝操作
在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的,但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,从而导致指针悬挂现象,所以,此时,必须采用深拷贝。
C++如何实现多态
1) 静态多态:主要是重载,在编译时就已经确定
2) 动态多态:利用虚函数机制实现,在运行期间动态绑定
动态多态三个条件:
1、要有继承
2、要有虚函数重写
3、要有父类指针(父类引用)指向之类对象
class Father { public: void Face() { cout<<"Father's face"<<endl; } virtual void Say() { cout<<"Father say hello"<<endl; } } ; class Son: public Father { public: void Say() { cout<<"Son say hello"<<endl; } }; int main(){ Son son; Father *pFather=&son; pFather->Say(); return 0; }
输出Son say hello;如果没有virtual则输出Father say hello
虚函数的实现
1) 父类中声明加了virtual关键字的函数
2) 在子类重写时,不需要加virtual也是虚函数
在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表(位于常量区),表中存放了虚函数的地址。实际的虚函数在代码段中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中的虚函数时,会将其集成到的虚函数表中的地址替换为重新写的虚函数地址。
为什么构造函数不能是虚函数?
虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数。
为何析构函数必须是虚函数?
将可能被继承的父类的析构函数设置为虚函数,可以保证我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
为何默认的析构函数不是虚函数?
因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。只有当需要作为父类时,才设置为虚函数。使用了虚函数,会增加访问内存的开销,降低效率。
重载和重写(覆盖)
重载:在同一作用域中,两个函数名相同,参数列表不同(类型、个数)
重写:子类继承父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数
范围的区别:被重写的和重写得函数在两个类中,而重载和被重载得函数在同一个类中。
参数的区别:被重写的和重写函数的参数列表一定相同,而被重写函数和重载函数的参数列表一定不同。
C++11的新特性
1) auto关键字:编译器根据初始值自动推导出类型
2) nullptr关键字:特殊类型的字面值,可被转化成任意其他的指针类型
3) 智能指针:用于解决内存管理问题
4) 初始化列表:使用初始化列表对类进行初始化
5) 右值引用:消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
6) Atomic原子操作用于多线程资源互斥操作
7) 新增STL容器array和tuple
C++中的四种强制类型转换
1) static_cast:静态转换,在编译处理期间。
强制将一种数据类型转换成另一种数据类型
a) 基本数据类型之间的转换
b) 用于类层次结构中基类和派生类的指针或引用的转换(向上转换:把派生类指针或引用转成基类表示;向下转换:可以成功,但不安全,因为没有动态类型检查)
c) 把空指针转换成目标类型的空指针
2) const_cast:去常转换,编译时执行,不是运行时执行
去除对象的指针或引用的常性
3) reinterpret_cast:重新解释类型转换
a) 改变指针或引用的类型
b) 将指针或引用类型转换成一个足够长的整型
c) 将整型转换成指针或引用类型
4) dynamic_cast:
提供运行时地类型检查,用于将基类的指针或引用安全地转换成派生类地指针或引用,下行转换时只能用于含有虚函数的类,不然编译错误;对于指针,如果转换失败将返回 NULL;对于引用,如果转换失败将抛出std::bad_cast异常。
智能指针
主要用于管理在堆上分配的内存,将普通的指针封装成一个栈对象。当栈对象生命周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。
shared_ptr:采用引用计数的方法,记录当前内存资源被多少个智能指针所引用。
1) 该引用计数的内存在堆上进行分配。当新增一个时,引用计数+1,当过期时,引用计数-1。只有引用计数为0时,智能指针才会自动释放引用的内存资源。
2) 不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类
3) 可以通过make_shared函数或者通过构造函数传入普通指针,并可以通过get函数获得普通指针
weak_ptr:为了解决循环引用而导致的内存泄露问题。
1) weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似于一个普通指针,但是不指向计数的公共内存,但是可以检测到对象是否已经被释放,从而避免非法访问。
auto_ptr:自动释放内存,不能大规模使用,不适用于数组
函数指针
1) 指向函数的指针变量,即指向函数的入口地址
2) 声明:
int func(int x); //声明一个函数
int (*f) (int x); //声明一个函数指针
f=func; //将func函数的首地址赋给指针f
空指针
指针指向的地址为空的指针叫空指针(NULL指针)
野指针
指向“垃圾”内存(不可用内存)的指针
产生原因:指针创建时未初始化。指针变量刚被创建时不会自动成为NULL指针,它会随机指向一个内存地址。
悬垂指针
指针所指向的对象已经被释放或者回收了,但是指向该对象的指针没有作任何的修改,仍旧指向已经回收的内存地址。 此类指针称为垂悬指针。
fork、wait、exec函数
fork:父进程产生子进程,使用fork函数拷贝出来的一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候,使用写时拷贝机制分配内存。fork从父进程返回子进程的pid,从子进程返回0。
exec:可以加载一个elf文件去替换父进程,从而父进程和子进程就可以运行不同的程序。exec执行成功,则子进程从新的进程开始运行。
wait:调用了wait的父进程将会发生阻塞,直到有子进程的状态改变,成功返回0。
左值与右值
1) 右值引用&&的两个目的: C++11引入右值引用主要是为了实现移动语义和完美转发,移动语义为了避免临时对象的拷贝,为类增加移动构造函数,完美转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间同时将要拷贝的对象复制过来,而是"拿"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr。
2) 左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在地持久对象
3) 右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在地临时对象
int num = 10;
int &b = num; //正确
int &c = 10; //错误
int num = 10;
//int && a = num; //右值引用不能初始化为左值
int && a = 10;
标签:必背,八股文,函数,int,struct,C++,内存,构造函数,指针 From: https://blog.csdn.net/m0_64349464/article/details/141507137