c++笔记(2)
C++特点
1. 面向对象编程(OOP)
C++ 支持面向对象编程,提供了以下核心特性:
类(Class):用于定义对象的属性和行为。
封装(Encapsulation):通过访问控制(如
public
、private
、protected
)隐藏实现细节。继承(Inheritance):支持代码重用和层次化设计。
多态(Polymorphism):允许通过基类指针调用派生类的函数。多态是指同一个接口可以表现出不同的行为。在 C++ 中,多态主要通过 虚函数(Virtual Function) 和 函数重载(Function Overloading) 来实现。
- 编译时多态:通过函数重载和运算符重载实现。
- 运行时多态:通过虚函数和继承实现。
- 允许子类重写父类的虚函数,实现不同的行为
2. 高效性
- C++ 继承了 C 语言的底层操作能力,可以直接操作内存和硬件。
- 支持手动内存管理(如
new
和delete
),但也提供了智能指针(如std::unique_ptr
和std::shared_ptr
)来简化内存管理。 - 性能接近 C 语言,适合开发高性能应用(如游戏引擎、操作系统等)。
C语言和C++的区别
C 语言:
主要支持过程式编程。
通过函数和结构体组织代码。
标准库较小
完全手动管理内存,使用
malloc
和free
不支持函数重载。
不支持异常处理。
C++语言:
支持多种编程范式,包括过程式、面向对象和泛型编程。
通过类、对象、模板等实现代码组织。
标准库更丰富,包括标准模板库(STL)、容器、算法、迭代器等。
支持手动内存管理(
new
和delete
),但也提供了智能指针(如std::unique_ptr
和std::shared_ptr
)来自动管理内存。支持函数重载,允许定义多个同名函数,只要它们的参数列表不同。
支持异常处理机制(
try
、catch
和throw
)。
说说 C++中struct 和class 的区别
**struct
**:
- 默认的成员访问权限是
public
。 - 通常用于表示简单的数据结构,类似于 C 语言中的结构体。
- 适合存储一组相关的数据,而不涉及复杂的操作。
- 默认的继承方式是
public
。
**class
**:
默认的成员访问权限是
private
。通常用于表示具有行为和属性的对象,强调封装和抽象。
适合实现面向对象编程中的类,包含数据成员和成员函数。
默认的继承方式是
private
。
include头文件的顺序以及双引号””和尖括号<>的区
头文件顺序
先包含标准库的头文件(如
<iostream>
、<vector>
等)。然后包含第三方库的头文件(如
<boost/any.hpp>
)。最后包含用户自定义的头文件(如
"myheader.h"
)。
原因:标准库和第三方库的头文件通常不会依赖用户自定义的头文件,因此先包含它们可以避免潜在的依赖问题。用户自定义的头文件可能依赖标准库或第三方库,因此放在最后。
**尖括号 <>
**:
- 用于包含标准库或第三方库的头文件。
- 编译器会在标准库路径和系统默认路径中查找这些头文件。
**双引号 ""
**:
- 用于包含用户自定义的头文件。
- 编译器会先在当前文件所在目录中查找头文件,如果找不到,再按照尖括号的方式查找。
说说C++结构体和C结构体的区别
C 结构体:
- 只能包含数据成员,不能包含成员函数。
- 所有成员默认是
public
,没有访问控制的概念。 - 不支持默认成员初始化。
- 定义结构体变量时需要显式使用
struct
关键字。
C++ 结构体:
可以包含数据成员和成员函数。
可以使用
public
、private
和protected
关键字来控制成员的访问权限。默认访问权限是
public
。支持继承,可以继承其他结构体或类。
可以定义构造函数和析构函数。
支持默认成员初始化。
定义结构体变量时不需要
struct
关键字。
导入C函数的关键字是什么,C++编译时和C有什么不同?
在 C++ 中导入 C 函数的关键字是 extern "C"
。它的作用是告诉 C++ 编译器按照 C 语言的命名规则(name mangling)来编译函数,以便与 C 代码兼容。
C++从代码到可执行二进制文件的过程
1. 预处理(Preprocessing)
预处理阶段由 预处理器 完成,主要任务是对源代码进行文本级别的处理,生成一个 预处理后的源代码文件
- 宏展开:将
#define
定义的宏替换为实际值。 - 头文件包含:将
#include
指定的头文件内容插入到源代码中。 - 条件编译:根据
#if
、#ifdef
、#ifndef
等条件编译指令,决定是否包含某些代码块。 - 删除注释:删除源代码中的注释。
2. 编译(Compilation)
编译阶段由 编译器 完成,将预处理后的源代码文件转换为 汇编代码(通常以 .s
为扩展名)。
- 词法分析:将源代码分解为有意义的词法单元(如关键字、标识符、运算符等)。
- 语法分析:根据语法规则检查代码的结构是否正确,生成语法树。
- 语义分析:检查代码的语义是否正确(如类型检查)。
- 代码优化:对代码进行优化,以提高执行效率。
- 生成汇编代码:将高级语言代码转换为目标机器的汇编代码。
3. 汇编(Assembly)
汇编阶段由 汇编器 完成,将汇编代码转换为 目标文件(通常以 .o
或 .obj
为扩展名)。目标文件是机器代码的二进制表示,但尚未链接为最终的可执行文件。
4. 链接(Linking)
链接阶段由 链接器 完成,将多个目标文件和库文件合并为一个 可执行文件(如 .exe
或 .out
)。
static关键字的作用
1. 在函数内部(局部变量)
当 static
用于函数内部的局部变量时,它会改变变量的存储周期和作用域:
- 存储周期:变量在程序的生命周期内一直存在,而不是在函数调用结束后销毁。
- 作用域:变量仍然只能在函数内部访问。
- 初始化:只初始化一次,后续调用函数时,变量会保留上一次的值。
2. 在类中(静态成员变量)
当 static
用于类的成员变量时,它会使其成为 类级别的变量,而不是实例级别的变量:
- 共享性:所有类的实例共享同一个静态成员变量。
- 存储周期:静态成员变量在程序的生命周期内存在。
- 访问方式:可以通过类名直接访问,而不需要创建类的实例。
3. 在类中(静态成员函数)
当 static
用于类的成员函数时,它会使其成为 类级别的函数:
- 访问方式:可以通过类名直接调用,而不需要创建类的实例。
- 限制:静态成员函数只能访问静态成员变量和静态成员函数,不能访问非静态成员(因为它们没有
this
指针)。
什么是函数指针,如何定义函数指针,有什么使用场景
函数指针是指向函数的指针变量。它存储了函数的地址,可以通过函数指针调用该函数。
如何定义函数指针?
函数指针的定义包括以下部分:
- 函数返回类型:指定函数返回值的类型。
- 指针名称:为函数指针命名。
- 参数列表:指定函数的参数类型和数量。
语法:
1 | 返回类型 (*指针名称)(参数类型1, 参数类型2, ...); |
示例:
1 | int add(int a, int b) { |
1. 回调函数(Callback Functions)
回调函数是通过函数指针传递给其他函数的函数。当某个事件发生时,回调函数会被调用。
示例:
1 |
|
2. 动态选择函数
通过函数指针,可以在运行时根据条件选择要调用的函数。
静态变量什么时候初始化?
1. 局部静态变量(函数内部的静态变量)
- 初始化时机:在第一次执行到该变量的定义语句时初始化。
- 特点:
- 只初始化一次,即使函数被多次调用,静态变量也不会重新初始化。
- 如果初始化过程中抛出异常,后续尝试访问该变量会导致未定义行为。
2. 全局静态变量(文件作用域的静态变量)
- 初始化时机:在程序启动时(
main
函数执行之前)进行初始化。 - 特点:
- 初始化的顺序与变量在文件中的定义顺序一致。
- 如果初始化过程中抛出异常,程序会终止。
3. 类的静态成员变量
- 初始化时机:在程序启动时(
main
函数执行之前)进行初始化。 - 特点:
- 必须在类外显式定义和初始化。
- 初始化的顺序与变量在文件中的定义顺序一致。
- 如果初始化过程中抛出异常,程序会终止。
4. 命名空间中的静态变量
- 初始化时机:在程序启动时(
main
函数执行之前)进行初始化。 - 特点:
- 初始化的顺序与变量在文件中的定义顺序一致。
- 如果初始化过程中抛出异常,程序会终止。
5. 静态变量的销毁时机
静态变量的销毁时机与初始化时机相对应:
- 局部静态变量:在程序结束时销毁。
- 全局静态变量、类的静态成员变量、命名空间中的静态变量:在程序结束时销毁。
- 销毁顺序:与初始化顺序相反。
nullptr调用成员函数可以吗?为什么?
在 C++ 中,使用 nullptr
调用成员函数是未定义行为(Undefined Behavior),但具体表现取决于是否访问了对象的成员数据。
在 C++ 中,成员函数的调用需要通过对象(或指针)来进行。当使用 nullptr
调用成员函数时,实际上是通过一个空指针访问对象,这会导致未定义行为。
未定义行为意味着:
- 程序可能会崩溃(如段错误)。
- 程序可能会继续运行,但产生错误的结果。
- 程序的行为可能因编译器、平台或运行环境的不同而不同。
如果成员函数没有访问对象的成员数据,且编译器没有对空指针进行检查,程序可能不会崩溃。这是因为:
- 成员函数在编译时会被转换为普通函数,第一个参数是对象的地址(
this
指针)。 - 如果函数中没有使用
this
指针(即没有访问成员数据),程序可能不会立即崩溃。
如果成员函数访问了对象的成员数据,程序会尝试通过 this
指针访问无效的内存地址,从而导致崩溃。
说说什么是野指针,怎么产生的,如何避免
野指针(Dangling Pointer)是指指向已经释放或无效内存的指针。访问野指针会导致未定义行为(Undefined Behavior),可能导致程序崩溃、数据损坏或安全漏洞。
野指针的产生原因
野指针通常由以下几种情况引起:
释放内存后未置空指针
- 当动态分配的内存被释放(如使用
delete
或free
)后,指针仍然指向原来的内存地址,但该地址已经无效。
函数返回局部变量的地址
- 函数返回局部变量的地址后,局部变量在函数结束时被销毁,指针指向的内存无效。
对象生命周期结束
- 当对象的生命周期结束时,指向该对象的指针变为野指针。
如何避免野指针的产生
释放内存后置空指针
- 在释放内存后,将指针置为
nullptr
,避免重复释放或访问无效内存。
避免返回局部变量的地址
- 不要返回局部变量的地址,而是返回动态分配的内存或静态/全局变量的地址。
使用智能指针
- 使用
std::unique_ptr
或std::shared_ptr
等智能指针,自动管理内存的生命周期,避免野指针。
说说静态局部变量,全局变量,局部变量的特点,以及使用场景
在 C++ 中,变量根据其作用域和生命周期可以分为局部变量、静态局部变量和全局变量。它们各自有不同的特点和适用场景。以下是详细的对比分析:
1. 局部变量
特点:
- 作用域:仅限于定义它的代码块(如函数、循环、条件语句等)。
- 生命周期:从定义处开始,到代码块结束时销毁。
- 存储位置:通常存储在栈(Stack)中。
- 初始化:如果未显式初始化,其值是未定义的(垃圾值)。
- 访问性:只能在定义它的代码块内访问。
使用场景:
- 用于函数内部的临时计算或存储。
- 适用于生命周期短、作用域小的场景。
2. 静态局部变量
特点:
- 作用域:仅限于定义它的代码块(如函数)。
- 生命周期:从第一次定义开始,直到程序结束。
- 存储位置:存储在全局/静态存储区。
- 初始化:如果未显式初始化,其值会被默认初始化为
0
。 - 访问性:只能在定义它的代码块内访问,但其值在多次函数调用之间保持不变。
使用场景:
- 用于在函数调用之间保持状态。
- 适用于需要持久化局部数据的场景。
3. 全局变量
特点:
- 作用域:整个程序(所有文件或通过
extern
声明访问)。 - 生命周期:从程序开始到程序结束。
- 存储位置:存储在全局/静态存储区。
- 初始化:如果未显式初始化,其值会被默认初始化为
0
。 - 访问性:可以在程序的任何地方访问。
使用场景:
- 用于在整个程序中共享数据。
- 适用于需要跨多个函数或文件访问的数据。
说说内联函数和宏函数的区别
堆和栈的区别:
栈(Stack) 在计算机科学中也称为 堆栈,是一种遵循 先进后出(FILO,First In Last Out) 或 后进先出(LIFO,Last In First Out) 原则的数据结构。
堆是一种动态内存分配区域,主要用于存储程序运行时动态分配的数据。堆的管理方式与栈完全不同:
- 堆的内存分配是随机的,不遵循任何特定的顺序。
- 堆的大小不固定,由程序员手动管理。
例如:
1 | Cint* ptr1 = (int*)malloc(sizeof(int)); // 在堆上分配内存 |
malloc和局部变量分配在堆还是栈?
1. malloc
分配的内存
malloc
函数用于在 堆(Heap) 上动态分配内存。
特点:
- 堆是一块由程序员手动管理的内存区域。
- 分配的内存需要显式释放(使用
free
函数),否则会导致内存泄漏。 - 堆的大小受系统内存限制,通常比栈大得多。
- 堆内存的生命周期由程序员控制,分配的内存在
free
之前一直有效。
2. 局部变量分配的内存
局部变量分配在 栈(Stack) 上。
特点:
- 栈是由编译器自动管理的内存区域。
- 栈内存的生命周期与函数调用相关,函数返回时自动释放。
- 栈的大小有限(通常几 MB),分配过大的局部变量可能导致栈溢出。
- 栈的分配和释放速度比堆快。
程序有哪些section,分别的作用?程序启动的过程?
程序的 Section(段)及其作用
在编译后的可执行文件中,程序通常被划分为多个段(Section),每个段有不同的作用。以下是常见的段及其功能:
1. .text 段
- 作用:存放程序的 代码(指令),即编译后的机器指令。
- 特点:
- 通常是只读的,防止程序运行时意外修改代码。
- 包含函数、主程序等的指令。
2. .data 段
- 作用:存放 已初始化 的全局变量和静态变量,注意存储已初始化且初始值非 0 的全局变量和静态变量。。
- 特点:
- 这些变量在程序启动时就已经被赋值。
- 可读可写。
3. .bss 段
- 作用:存放 未初始化 的全局变量和静态变量。
- 特点:
- 在程序加载时,这些变量会被初始化为 0。
- 不占用磁盘空间,仅记录变量的大小。
4. .rodata 段
- 作用:存放 只读数据,如字符串常量、常量数组等。
- 特点:
- 通常是只读的,防止程序运行时修改。
5. .heap 段
- 作用:用于 动态内存分配(如
malloc
、new
)。 - 特点:
- 由程序员手动管理。
- 大小不固定,随着程序运行动态增长。
6. .stack 段
- 作用:用于 函数调用,存放局部变量、函数参数、返回地址等。
- 特点:
- 由编译器自动管理。
- 大小有限,通常在几 MB 左右。
7. 其他段
- .plt/.got:用于动态链接,存放过程链接表(PLT)和全局偏移表(GOT)。
- .init/.fini:存放程序的初始化和终止代码。
程序启动的过程
程序从加载到运行的整个过程可以分为以下几个阶段:
1. 加载(Loading)
- 作用:将可执行文件从磁盘加载到内存。
- 过程:
- 操作系统读取可执行文件的头部信息,确定需要加载哪些段。
- 将
.text
、.data
、.rodata
等段加载到内存的指定位置。 - 为
.bss
段分配内存并初始化为 0。
2. 初始化(Initialization)
- 作用:初始化程序的运行环境。
- 过程:
- 设置堆(heap)和栈(stack)的起始地址。
- 调用
.init
段中的初始化代码(如果有)。 - 初始化全局变量和静态变量。
3. 执行(Execution)
- 作用:开始执行程序的主逻辑。
- 过程:
- 从
main
函数开始执行。 - 程序运行时,栈用于函数调用,堆用于动态内存分配。
- 从
4. 终止(Termination)
- 作用:程序正常结束或异常终止。
- 过程:
- 调用
.fini
段中的终止代码(如果有)。 - 释放堆内存。
- 返回退出状态给操作系统。
- 调用
内存对齐的主要使用场景包括:
- 提高内存访问效率。
- 满足硬件要求。
- 避免缓存行冲突。
- 与硬件设备交互。
- 优化数据结构。
- 确保跨平台兼容性。
内存泄漏
内存泄漏(Memory Leak) 是指程序在运行过程中动态分配的内存未能正确释放,导致内存占用不断增加,最终可能耗尽系统内存资源。以下是内存泄漏的原因、检测方法和解决方案:
1. 内存泄漏的原因
忘记释放内存:
- 使用
new
或malloc
分配内存后,未调用delete
或free
释放。
- 使用
指针丢失:
- 指针被重新赋值或超出作用域,导致无法访问已分配的内存。
异常导致未释放:
- 在释放内存之前发生异常,导致内存未释放。
循环引用:
- 在智能指针或对象之间存在循环引用,导致引用计数无法归零。
2. 检测内存泄漏的方法
手动检查:
- 仔细检查代码,确保每次
new
或malloc
都有对应的delete
或free
。
- 仔细检查代码,确保每次
使用工具:
- Valgrind(Linux):检测内存泄漏和非法内存访问。
日志记录:
- 在分配和释放内存时记录日志,方便追踪。
3. 解决内存泄漏的方案
使用智能指针:
- 使用
std::unique_ptr
或std::shared_ptr
自动管理内存。
- 使用
RAII(资源获取即初始化):
- 将资源(如内存)的生命周期与对象的生命周期绑定。
避免裸指针:
- 尽量避免直接使用裸指针,改用容器(如
std::vector
、std::string
)。
- 尽量避免直接使用裸指针,改用容器(如
检查循环引用:
- 使用
std::weak_ptr
打破循环引用。
- 使用
异常安全:
- 使用 RAII 或智能指针确保异常发生时资源仍能释放。