C++常见概念问题(3)
1. 构造函数的初始化顺序
基类构造函数:在派生类的构造函数中,基类的构造函数在派生类构造函数体执行之前调用。
成员变量初始化:类中的成员变量会按照其在类中声明的顺序进行初始化,而不是按照构造函数初始化列表中的顺序。
2. C++内存分区模型
C++分区模型 程序运行前,编译的时候:
代码区:存放函数体的二进制代码,由操作系统进行管理,特点(共享,可读)
全局区:存放全局变量和静态变量以及常量,该区域的数据在程序结束后由操作系统释放程序运行后:
栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
3. 宏定义和全局变量的区别
- 数据类型
宏定义:没有特定的数据类型,因为它仅仅是文本替换,编译器在预处理阶段进行替换,不占用内存。
全局变量:有具体的数据类型(如整型、浮点型、字符串等),并占用相应的内存空间。 - 定义方式
宏定义:使用预处理指令 #define 来定义,通常用来替代常量或表达式。例子:#define PI 3.14
全局变量:定义在函数外部 - 作用域
都可以在本文件使用。
如果跨文件,可以加入extern 关键字在其他文件中使用。
4. 全局变量,静态变量,常量
4.1 全局变量
全局变量是定义在函数外面的变量,本文件内都可以访问。如果想跨文件访问,也可以使用extern关键字实现。
int globalVar = 42;
另一个文件
extern int globalVar;
全局变量在程序运行前的编译阶段就分配了内存,直到程序完全结束,由操作系统释放。
4.2 静态变量
-
局部静态变量:给函数内部的局部变量加上关键字static,变成函数内部的静态变量,原本局部变量的生命周期随着函数结束。变成静态变量后,生命周期改变了,作用域没有改变。
生命周期变得和全局变量一样,持续到程序结束。由操作系统释放。
作用域没有改变,意味着别的函数还是无法访问这个局部静态变量,只有自己函数能访问。上次执行完这个函数,局部静态变量的值会被保存下来,下次再调用这个函数时,接着使用。 -
全局静态变量:那么给全局变量加上关键字static,相比于全局变量,全局静态变量有什么改变:首先生命周期没变,作用域变了,全局静态变量只能在本文件访问到,不能通过extern关键字跨文件访问。
为什么需要静态变量:
-
保持状态:
静态变量能够保存函数调用之间的状态。这意味着即使函数执行结束,该变量的值仍然可以被保留并在后续调用中继续使用。 -
共享数据:
在类或模块内部,静态变量可以被所有实例或调用共享。这样可以避免使用全局变量,同时又能跨多个实例共享数据,减少了命名冲突的风险。 -
内存管理:
静态变量通常在程序的整个生命周期内存在,不需要频繁地分配和释放内存。这对于性能敏感的应用尤为重要,因为它可以减少内存分配的开销。 -
初始化控制:
静态变量只会在第一次使用时进行初始化,这样可以确保其初始状态一致,并且在多次调用时不会重复初始化。 -
封装性:
使用静态变量可以隐藏实现细节,提供更好的封装性。从外部看,静态变量可以限制访问权限,使得数据的管理更加严谨。
4.3 常量
使用 const 关键字定义的不可修改的变量。
生命周期同样是编译时分配空间,程序结束时操作系统释放资源。
常量的作用域依赖于定义的位置,可以是全局的、局部的,或在类中定义。
函数重载的底层本质
核心:C++之所以有重载,其原因是函数名修饰规则不同
对于C语言,相同的函数名,其修饰名必然相同,与参数无关,因此不同参数的同名函数的修饰名是重复的,因此无法同时存在,更无法实现重载;
C++之所以能够实现重载,是因为编译器能够区分同名不同参的函数,而其区分的底层原理就是对原函数名进行修饰,或者叫重命名;
编译前的重载函数(同名不同参的函数),编译后就是不同名的函数,这个新的函数名中不仅包含了原名,还包含了参数信息;
C++和C怎么协同开发(c++项目怎么引用c的文件)
假如在c++文件中使用C的源文件example.c
// example.h
#ifndef EXAMPLE_H
#define EXAMPLE_H
#ifdef __cplusplus
extern "C" {
#endif
void c_function();
#ifdef __cplusplus
}
#endif
#endif // EXAMPLE_H
#ifndef EXAMPLE_H
头文件保护(Include Guard): #ifndef 是 "if not defined" 的缩写。这条指令检查宏 EXAMPLE_H 是否未被定义。如果没有被定义,则继续执行后面的代码;如果已经定义,编译器将跳过这个头文件的内容,以避免重复包含同一头文件造成的潜在错误。
#define EXAMPLE_H
定义宏: 如果上面的条件成立,这里会定义宏 EXAMPLE_H。这意味着在后续的编译过程中,如果再出现 #ifndef EXAMPLE_H,条件将不再成立,从而避免了重复包含该头文件。
#ifdef __cplusplus
检查 C++ 编译器: #ifdef 指令用于检查是否在 C++ 编译环境下编译。如果 __cplusplus 宏已定义,这表示当前正在使用 C++ 编译器。
extern "C" {
防止名称修饰: extern "C" 告诉 C++ 编译器以 C 的方式链接函数。在 C++ 中,为了支持函数重载,编译器会对函数名进行“名称修饰”。使用 extern "C" 可以避免这种情况,使得 C 函数可以在 C++ 中被正确调用。
void c_function();
函数声明: 这里声明了一个名为 c_function 的函数,其返回类型为 void,表示此函数不返回任何值。同时,没有参数列表,表示该函数接受任何数量和类型的参数。
#ifdef __cplusplus
结束 C++ 检查: 这一行与之前的 #ifdef __cplusplus 相对应,用于结束对 C++ 编译环境的检查。
}
结束 extern "C" 块: 这个括号标志着 extern "C" 块的结束,表示在这个块内的所有函数声明都应按照 C 的方式处理,而不是 C++ 的标准方式。
#endif // EXAMPLE_H
结束头文件保护: 这一行对应于前面开始的 #ifndef EXAMPLE_H,它结束了头文件保护的定义。任何在这个保护区块内的代码都会被保证只会被编译一次。
c++迭代器,和迭代器失效的原因
定义: 迭代器类似于指针,它们指向容器中的某个元素,并且能够在容器中前后移动。通过迭代器,可以读取和修改容器中的数据。
迭代器类似于指针,它们指向容器中的某个元素,并且能够在容器中前后移动。通过迭代器,可以读取和修改容器中的数据。
迭代器失效的原因
容器大小变化:
插入(Insert): 如果在迭代器当前位置插入元素,可能会导致所有指向该位置及之后元素的迭代器失效。
删除(Erase): 删除迭代器指向的元素会使该迭代器失效,通常还会影响其他迭代器(例如,删除某个位置之前的元素会使之后的迭代器失效)。
调整容器大小: 对于动态大小的容器(如 std::vector),如果超过了其容量,可能会重新分配内存,这会使所有指向旧内存的迭代器失效。
容器清空:
调用 clear() 函数清空容器时,所有与该容器相关的迭代器都会失效。
直接赋值或重新初始化:
例如,将一个容器赋值给另一个新容器后,之前的迭代器指向的内容可能会失效。
resize和reserve方法
resize()
resize 会 改变容器的大小。它的作用是调整容器的当前大小,即容器内元素的个数。
如果新大小大于当前大小:会增加容器的元素数量,并且这些新增的元素会根据容器的类型进行默认初始化。例如,对std::vector,新增加的元素将会是 0。
如果新大小小于当前大小:会删除容器中的多余元素,容器的大小会减少。
reserve()
reserve 不会改变容器的大小,它只是调整容器的 容量(capacity),即指定一个容器最大容量。
如果目前内存能够放下这个最大容量的数据结构,没变化。
如果目前内存放不下最大容量的数据结构,扩容为指定容量的内存大小。
使用reserve 主要是为了优化性能,避免在插入新元素时频繁重新分配内存。
假设 std::vector 中的每个元素占据 4 字节(在大多数平台和编译器中,int 通常是 4 字节),那么在调用 reserve(100) 后,std::vector 会为 100 个 int 元素分配内存空间,大小大约是 100 * sizeof(int)
重新设置容器大小:
- reserve() 用于请求容器在内部分配至少 n个元素的存储空间,以减少动态分配的次数。它不会改变容器的大小,只是调整内部容量(capacity)。
- resize() 用于改变容器的大小到 n。如果新大小大于当前大小,容器会增加元素,新增的元素会被初始化为零或默认值;如果新大小小于当前大小,超出部分的元素会被删除。
内存方面:
- reserve(): 只影响容量(capacity),不会导致元素的复制或移动,不会改变当前存储的元素。
- resize(): 会改变大小(size),可能会导致元素的移动和复制,特别是在增加元素时。
lambda表达式
捕获列表:决定 lambda 可以访问哪些外部变量,以及以何种方式访问。
参数列表:与普通函数一致,定义 lambda 接受的输入参数。
返回类型:可选,如果不写,编译器会自动推导。
函数体:包含具体的逻辑实现。
- capture(捕获列表): 用于指定lambda表达式可以访问的外部变量。捕获列表的常见方式包括:
值捕获:[x]:将变量 x 的值复制到 lambda 内部。
引用捕获:[&x]:将变量 x 的引用传入 lambda,允许在 lambda 内部修改原始变量。
捕获所有(值):[=]:将所有外部变量按值捕获。
捕获所有(引用):[&]:将所有外部变量按引用捕获。
混合捕获:[=, &x]:将所有外部变量按值捕获,但 x 按引用捕获。 - parameters(参数列表): 与普通函数的参数列表相同,定义lambda接受的输入参数,可以为空。
- return_type(返回类型,可选): 用于指定返回值的类型。如果不写,编译器会根据函数体自动推导返回类型。
- function body(函数体):包含实际的代码逻辑。
在C++中,lambda表达式(或称为匿名函数)是一种用于定义可调用对象(如函数对象)的语法糖。它的本质是提供一种方便的方式来创建和使用小的、临时的函数对象。lambda表达式可以被用作参数传递给函数,或者作为返回值。
当你在代码中使用 lambda 表达式时,编译器会将其转换为一个具体的类型,通常是一个匿名的、唯一的类。这使得 lambda 表达式不仅可以被调用,还可以携带状态(即捕获的变量)。
即使它们的结构相同,但由于捕获的变量不同,编译器会生成不同的类。
成员函数 operator():编译器会为这个类定义一个 operator() 方法,使得你可以像调用普通函数一样调用这个 lambda。
指针和引用的区别
指针(Pointer):指针是一个变量,它存储另一个变量的内存地址。通过指针可以间接访问或修改指针指向的变量。
引用(Reference):引用是某个已有变量的别名,引用在创建时必须初始化,并且不能再改变指向其他变量。一旦绑定到某个变量,引用就无法指向其他变量。
解释性语言和编译性语言区别
解释性语言:
Python,JavaScript,Ruby
编译性语言:
C,C++,Rust
解释性语言:
即时执行代码是逐行解释执行的,通常没有生成独立的可执行文件。解释器在运行时读取源代码,逐行解析并执行。
平台无关性:只要有合适的解释器,可以在任何平台上运行同样的源代码。
编译性语言:
先编译后执行代码首先通过编译器转换为机器语言(或中间代码),生成一个独立的可执行文件,之后运行时直接执行该可执行文件。
性能较高:编译后的代码是机器可直接执行的,通常比解释型语言更高效。
内存使用:
解释性语言:由于代码是在执行时逐行解释,通常需要更多的内存来加载解释器和源代码。
编译性语言:编译后的程序是机器代码,执行时不需要加载解释器,所以内存使用通常更高效。
修改后的行为:
解释性语言:修改代码后可以直接运行,修改和测试的周期较短。
编译性语言:修改后需要重新编译才能运行,因此调试周期较长。
调试与错误处理:
解释性语言:错误通常在运行时发生,调试过程比较灵活,但可能遇到更难以追踪的错误(如运行时错误)。
编译性语言:错误通常在编译阶段被发现,错误信息相对较为清晰,有助于尽早发现问题。
Java
Java 既不是单纯的解释性语言,也不是单纯的编译性语言,它是两者的结合,采用了 编译+解释 的执行方式。具体来说,Java 采用了一种编译和解释相结合的机制,这种机制被称为 “编译-解释混合模式”。以下是更详细的说明:
Java 的执行流程
源代码编译:
当你编写 Java 程序时,首先会使用 Java 编译器(如 javac)将 .java 源代码编译成 字节码(即 .class 文件)。这个过程就是编译。
字节码是一种中间语言,它与平台无关,不是机器码,而是Java虚拟机(JVM)可以理解的指令。
字节码的执行:
生成的字节码不会直接运行在硬件上,而是由 Java 虚拟机(JVM) 来解释执行。
JVM 会将字节码 解释 成具体平台的机器码,然后在该平台上执行。这个过程类似于解释性语言的运行机制。
在现代 JVM 中,还引入了 即时编译(JIT,Just-In-Time compilation) 技术。JIT 会在运行时将热点字节码(即被频繁执行的部分)动态编译成机器码,以提高程序的执行效率。
C++内存泄漏可能的原因
内存泄漏是指程序在申请内存后,未能正确释放,导致系统可用内存逐渐减少,最终可能引发程序崩溃或系统性能下降。
-
程序员在动态分配内存后,忘记使用 delete 或 delete[](对于数组)来释放内存。
-
构造函数中使用裸指针(int*, char* 等)来分配内存,但没有适当的内存管理机制(如智能指针),那么如果在析构函数没有释放内存的话,会造成内存泄漏。
-
异常导致的提前返回
当程序出现异常时,如果在栈展开过程中有动态分配的内存没有被释放,就会发生内存泄漏。特别是在没有使用 try-catch 块的情况下,异常会导致提前退出函数,而此时未释放的内存会被遗弃。 -
智能指针循环引用
-
内存管理不当的多线程问题
在多线程程序中,如果一个线程分配了内存,而另一个线程错误地释放了它,或者某个线程没有适当地释放内存,就会出现内存泄漏。