C++ 基础
指针和引用的区别
指针是一个变量,存储的另一个变量的内存地址,
可以重复赋值执行不同的对象,运行为nullptr
适合动态分配,例如使用new,delete时;
用在实现链表、树等数据结构时;
以及明确没有对象的情况,使用nullptr。
int a = 99;
int ptr = &a;//使用进行定义,用取地址符获得变量地址
int b = p;//用p 获得该地址上存储的内容
引用是对象的别名,定义时必须初始化,
并且不能改变引用绑定的对象,没有空引用的概念
常用于函数参数传递、操作符重载、以及对象拷贝
int a = 90;
int &ref = a;//使用&定义引用,
int b = ref;//获取值是直接使用引用变量
const 关键字
const用于声明常量,保证变量的值不会被修改,
被修饰的变量存储字自读内存区,由于编译器会进行常量折叠优化,
所以建议使用cinstexptr替代。
在函数传参时使用const修饰表示在该函数内部不会修改变量。
在类里面使用const修饰的函数称为常量成员函数,只能读取成员变量。
常量指针:
不能通过常量指针修改所实现的对象,强调其所指向的对象的不可改变性。
const int * ptr;
int const * ptr; 都是常量指针nt a = 10;
const int b = 20;
const int* p = &a; // 合法:指向非常量对象
指针常量:
指针本身不可变(不能指向其他对象),但可以修改所指向的对象
int a = 10, b = 20;
int* const p = &a; // 必须初始化,之后不能再指向其他对象
*p = 30; // 合法:可以修改a的值
// p = &b; // 错误:不能修改指针p本身
const int* const p = &x; // 双重const
指向常量的指针常量:
指针本身和所指向的对象都不可变。
int a = 10;
const int* const p = &a;
// *p = 30; // 错误:不能修改对象
// p = &b; // 错误:不能修改指针
static关键字
static主要用于控制变量和函数的生命周期和作用域以及访问权限,
在函数内使用static修改变量称为静态变量,静态变量默认初始化为0,
静态变量存在于程序的整个生命周期,不会因为离开而被销毁,但是只在函数内部可见
在类里面使用static修饰成员变量称为静态成员变量
静态成员变量必须在类的外部单独定义,以便为其分配空间,
所有类的对象共享同一个静态成员变量的副本
在类里面使用static修饰的函数称为静态函数,
静态函数属于类而不属于类的实例,可以不用创建对象直接通过类名调用
静态函数不能直接访问非静态成员变量或非静态成员函数
普通函数被static修饰,表示该函数只能在所声明的文件内使用
define
定义预编译时的宏,字符串原样替换,无类型检查,不安全
inline
所修饰的函数称为内联函数,在编译时直接编译生成函数体,插入被调用的地方
这样做可以减少压栈,跳转和返回等操作,没有普通函数调用时的额外开销。
但是她也存在一些限限制:
1、不存在如何形式的循环语句
2、不能存在过多的条件判断语句
3、函数体不能过于庞大
4、内联函数声明必须在调用语句之前
const和define的区别
const用于定义常量,define用于对应宏,而宏也可以用于定义常量,
都用于在常量定义时,他们的区别有
1、const生效于编译的阶段,define生效于预处理阶段
2、const定义的常量,在C语言中是存储在内存中,需要额外的内存空间,
define定义的常量,运行时是直接的操作数,并不会存放在内存中
3、const定义的常量是带类型的,而define定义的常量不带类型,不利于类型检查
constepr
将变量声明为constexpr类型,由编译器来验证变量的值是否是⼀个常量表达式。
必须使⽤常量初始化,
constexpr 函数
constexpr 函数可以在编译时或运行时调用,取决于其参数是否为编译时常量。编译器会在编译时自动计算结果,否则在运行时执行。
constexpr 构造函数
constexpr 构造函数允许在编译时创建对象,使对象的成员变量在编译时初始化。常用于创建编译时数据结构(如数学库、配置类)。
voaltile
volatile 关键字修饰的变量表示:该变量的值可能在程序控制流之外被改变,因此编译器每次访问该变量时,都必须从内存中读取实际值,而不能依赖之前缓存的副本。
volatile 的主要作用是防止编译器优化,确保对变量的每次访问都直接与内存交互。
extern
extern的主要用途是声明变量或者函数,实现跨文件访问全局变量
extern int a;,仅声明 a 是其他地方定义的变量,告诉编译器 “变量存在”,但不分配内存
int a = 10;,会在内存中创建 a 的存储单元,为变量分配内存空间
延迟变量定义指:
先通过 extern 声明变量(告知编译器变量存在),暂时不定义变量(不分配内存),将变量的实际定义推迟到其他源文件或当前文件的后续位置。
使用extern还可以实现 C 和 C++ 代码的混合编程
#ifdef __cplusplus // 如果是 C++ 编译器
extern "C" { // 按 C 语言规则处理函数名
#endif
// 中间部分内容 按 C 语言规则处理函数名
#ifdef __cplusplus
}
#endif
std::atomic
std::atomic定义在头文件中,
它提供了对基本数据类型(如int, bool, pointer等)的原子操作封装,可以解决多线程数据竞争问题,
实现轻量级同步、实现内存序控制,就是精确控制原子操作之间的内存可见性和顺序
字符串操作函数
strcpy()
功能:把一个字符串复制到另一个字符串,包含\0结束符,要留意目标字符串的空间得足够大
strlen()
计算字符串的长度(不包含结束符\0)
strcat()
将一个字符串连接到另一个字符串的末尾,包含\0
strcmp()
逐个比较字符的 ASCII 值,直到发现不同或者遇到\0
std::string类中的length()和size()
这两个方法在功能上完全一样,都是返回字符串的字符个数,返回的长度不包含字符串结束符\0
C++内存管理
C++内存分区
C++ 内存分为 5 个主要区域,
栈 (Stack):存储局部变量、函数参数和返回地址。由编译器自动分配和释放,效率高但空间有限。
堆 (Heap):动态分配的内存区域,需手动管理(new/delete 或 malloc/free)。空间较大但容易产生碎片。
全局 / 静态存储区:存储全局变量和静态变量,程序启动时分配,结束时释放。
常量存储区:存储常量值(如字符串字面量),通常不可修改。
代码区:存储程序的机器码,只读区域。
内存泄漏?如何避免?
定义:动态分配的内存未被正确释放,导致无法被再次使用。
产生原因:
忘记调用 delete/free释放指针
代码异常导致流程跳过释放代码
指针丢失(如被覆盖或作用域结束)
避免方法:
使用智能指针(如std::unique_ptr, std::shared_ptr)
使用内存检测工具(如 Valgrind)
遵循 “谁分配,谁释放” 原则
什么是智能指针?有哪些种类?
定义:自动管理对象生命周期的类模板,避免手动内存管理。
种类:
std::unique_ptr:独占所有权,不可复制但可移动。
std::shared_ptr:共享所有权,使用引用计数管理生命周期。
std::weak_ptr:弱引用,可以从 std::shared_ptr 创建,但不会增加引⽤计数,不会影响资源的释放。⽤于解决 std::shared_ptr 可能导致的循环引⽤问题。通过 std::weak_ptr::lock() 可以获取⼀个 std::shared_ptr 来访问资源。
new 和 malloc 有什么区别?
new 是C++的运算符,可以为对象分配内存并调⽤相应的构造函数。new 返回的是具体类型的指针,⽽且不需要进⾏类型转换。new 在内存分配失败时会抛出 std::bad_alloc 异常。new 可以⽤于动态分配数组,并知道数组⼤⼩。delete 会调⽤对象的析构函数,然后释放内存。
malloc 是C语⾔库函数,只分配指定⼤⼩的内存块,不会调⽤构造函数。malloc 返回的是 void* ,需要进⾏类型转换,因为它不知道所分配内存的⽤途。malloc 在内存分配失败时返回 NULL 。malloc 只是分配指定⼤⼩的内存块,不了解所分配内存块的具体⽤途。free 只是简单地释放内存块,不会调⽤对象的析构函数。
delete 和 free 有什么区别?
delete 会调⽤对象的析构函数,确保资源被正确释放。delete 释放的内存块的指针值会被设置为 nullptr ,以避免野指针。delete 可以正确释放通过 new[] 分配的数组。
free 不了解对象的构造和析构,只是简单地释放内存块。free 不会修改指针的值,可能导致野指针问题。free 不了解数组的⼤⼩,不适⽤于释放通过 malloc 分配的数组。
什么是野指针,怎么产⽣的,如何避免
野指针是指指向已被释放的或⽆效的内存地址的指针。使⽤野指针可能导致程序崩溃、数据损坏或其他不可预测的
⾏为。
产生原因:
释放后没有置空指针
#include <iostream>void danglingPointerAfterDeletion() {int* ptr = new int(42);delete ptr;// 错误:ptr 现在是悬空指针// 正确做法:释放后将指针置为 nullptrptr = nullptr;// 安全检查if (ptr != nullptr) {std::cout << *ptr << std::endl;}
}
返回局部变量的指针
#include <iostream>// 错误示例:返回局部变量的指针
int* returnLocalVariablePointer() {int value = 42;return &value; // 错误:value 是局部变量,函数结束后内存被释放
}// 正确示例:返回动态分配的内存
int* returnDynamicPointer() {int* ptr = new int(42);return ptr; // 正确:但调用者需要负责释放内存
}// 更好的做法:使用智能指针
#include <memory>
std::unique_ptr<int> returnSmartPointer() {auto ptr = std::make_unique<int>(42);return ptr; // 所有权转移,安全
}
释放内存后没有调整指针
#include <iostream>
#include <vector>void pointerNotUpdatedAfterDeletion() {std::vector<int*> vec;for (int i = 0; i < 5; ++i) {vec.push_back(new int(i));}delete vec[2];// 错误:vec[2] 现在是悬空指针,但向量中仍保留该指针// 正确做法:删除后调整指针vec[2] = nullptr;// 安全访问for (auto ptr : vec) {if (ptr != nullptr) {std::cout << *ptr << std::endl;}}// 清理剩余内存for (auto ptr : vec) {delete ptr;}
}
函数参数指针被释放
#include <iostream>// 错误示例:释放调用方传递的指针
void incorrectFree(int* ptr) {delete ptr; // 错误:如果调用方没有动态分配内存,会导致未定义行为
}// 正确示例:通过引用传递指针,不释放内存
void processPointer(int*& ptr) {// 使用 ptr 但不释放它if (ptr != nullptr) {*ptr = 100;}
}// 更好的做法:使用智能指针明确所有权
#include <memory>
void processSmartPointer(std::shared_ptr<int> ptr) {// ptr 是 shared_ptr 的副本,引用计数增加if (ptr) {*ptr = 200;}// ptr 离开作用域时引用计数减1
}
避免方法:
在释放内存后将指针置为 nullptr
避免返回局部变量的指针
使⽤智能指针(如 std::unique_ptr 和 std::shared_ptr )
注意函数参数的⽣命周期, 避免在函数内释放调⽤⽅传递的指针,或者通过引⽤传递指针。
#include <iostream>
#include <memory>// 使用 unique_ptr 管理动态内存
void useUniquePtr() {std::unique_ptr<int> ptr = std::make_unique<int>(42);// 无需手动释放内存,ptr 离开作用域时自动释放// 转移所有权std::unique_ptr<int> ptr2 = std::move(ptr);if (!ptr) {std::cout << "ptr is null after move" << std::endl;}
}// 使用 shared_ptr 共享所有权
void useSharedPtr() {std::shared_ptr<int> ptr = std::make_shared<int>(42);{std::shared_ptr<int> ptr2 = ptr; // 引用计数增加std::cout << "Shared count: " << ptr.use_count() << std::endl; // 输出2} // ptr2 离开作用域,引用计数减1std::cout << "Shared count: " << ptr.use_count() << std::endl; // 输出1// 最后一个 shared_ptr 离开作用域时释放内存
}
野指针和悬浮指针的区别
野指针是指向已经被释放或者⽆效的内存地址的指针。通常由于指针指向的内存被释放,但指针本身没有被置为
nullptr 或者重新分配有效的内存,导致指针仍然包含之前的内存地址。使⽤野指针进⾏访问会导致未定义⾏
为,可能引发程序崩溃、数据损坏等问题。
悬浮指针是指向已经被销毁的对象的引⽤。当函数返回⼀个局部变量的引⽤,⽽调⽤者使⽤该引⽤时,就可能产⽣
悬浮引⽤。访问悬浮引⽤会导致未定义⾏为,因为引⽤指向的对象已经被销毁,数据不再有效。
关联对象类型:
野指针涉及指针类型。
悬浮指针涉及引⽤类型
问题表现:
野指针可能导致访问已释放或⽆效内存,引发崩溃或数据损坏。
悬浮指针可能导致访问已销毁的对象,引发未定义⾏为
产⽣原因:
野指针通常由于不正确管理指针⽣命周期引起。
悬浮指针通常由于在函数中返回局部变量的引⽤引起。
如何避免悬浮指针
避免在函数中返回局部变量的引⽤。
使⽤返回指针或智能指针⽽不是引⽤,如果需要在函数之外使⽤函数内部创建的对象。
内存对⻬是什么?为什么需要考虑内存对⻬?
内存对⻬是指数据在内存中的存储起始地址是某个值的倍数。
是硬件和软件之间的 “约定”,让 CPU 能快速、稳定地读写数据
硬件限制,比如 ARM、PowerPC 等架构,强制要求数据必须对齐,否则直接报错(程序崩溃)
x86 架构相对 “宽容”:虽然能处理不对齐的数据,但会降低性能
常见的是在构建结构体时注意数据对齐,牺牲空间换区快速读写
⾯向对象的三⼤特性
封装:将数据(成员变量)和操作数据的函数(成员函数)捆绑在一起,通过访问控制隐藏内部实现细节,仅对外提供必要接口。
继承:允许一个类(子类 / 派生类)继承另一个类(父类 / 基类)的属性和方法,实现代码复用和层次化设计。
多态:允许不同类的对象通过相同的接口进行调用,根据对象实际类型执行不同实现,增强代码灵活性和可扩展性。
有哪些访问修饰符
public:成员可被任意类访问。
private:成员只能被本类的成员函数访问(默认修饰符)。
protected:成员可被本类和子类的成员函数访问。
什么是多重继承?
一个子类同时继承多个父类的特性
简述⼀下 C++ 的重载和重写,以及它们的区别
重载:同一作用域内,函数名相同但参数列表不同(参数类型、个数或顺序),与返回值类型无关。编译时根据调用参数决定执行哪个版本。
重写:存在于继承关系中,子类重新定义父类的虚函数(函数签名完全相同)。运行时根据对象实际类型决定执行哪个版本(动态绑定)。
c++的多态如何实现
通过虚函数(Virtual Function)和指针 / 引用实现。父类声明虚函数,子类重写该函数,当通过父类指针 / 引用调用虚函数时,实际执行的是对象所属子类的版本。
成员函数/成员变量/静态成员函数/静态成员变量的区别
成员变量:每个对象独享一份,存储对象的状态。
成员函数:绑定到对象,可访问对象的成员变量和其他成员函数。
静态成员变量:所有对象共享一份,存储类级别的数据,需在类外初始化。
静态成员函数:不绑定到对象,只能访问静态成员变量和其他静态成员函数,通过类名直接调用
什么是构造函数和析构函数?
构造函数:与类同名,无返回值,用于对象初始化,创建对象时自动调用。
析构函数:与类同名前加~,无返回值,用于资源释放(如内存、文件句柄等),对象销毁时自动调用。
C++构造函数有⼏种,分别什么作⽤
默认构造函数:无参数或全默认参数,用于创建默认状态的对象。
参数化构造函数:带参数,用于自定义对象初始值。
拷贝构造函数:参数为同类对象的引用,用于创建新对象并复制已有对象的值(浅拷贝 / 深拷贝)。
移动构造函数:C++11 引入,参数为右值引用,用于高效转移资源所有权
什么是虚函数和虚函数表?
虚函数:在基类中用virtual声明的函数,允许子类重写,实现动态绑定。
虚函数表:每个包含虚函数的类都有一个虚函数表,存储该类的虚函数地址。每个对象包含一个指向虚函数表的指针(vptr),运行时通过 vptr 找到实际要调用的函数。
虚函数和纯虚函数的区别
虚函数:基类中声明并实现,子类可选择性重写,基类可实例化。
纯虚函数:基类中声明但不实现(virtual void func() = 0;),子类必须重写,包含纯虚函数的类是抽象类,不可实例化。
什么是抽象类和纯虚函数?
抽象类:包含至少一个纯虚函数的类,仅作为接口存在,不能实例化,必须通过子类实现纯虚函数后才能使用。
纯虚函数:定义接口规范,强制子类实现特定功能
简述⼀下虚析构函数,什么作⽤
虚析构函数:基类的析构函数声明为virtual,确保通过基类指针删除派生类对象时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。
说说为什么要虚析构,为什么不能虚构造
虚析构:防止通过基类指针删除派生类对象时,只调用基类析构函数导致子类资源泄漏。
不能虚构造:构造函数用于创建对象并初始化 vptr,此时对象尚未完全构造,无法实现动态绑定。
哪些函数不能被声明为虚函数?
构造函数
静态成员函数(不依赖对象)
内联函数(编译时展开,不支持动态绑定)
友元函数(非类成员)
深拷⻉和浅拷⻉的区别
浅拷贝:复制对象时仅复制成员变量的值,若包含指针,则只复制指针地址,导致多个对象共享同一块内存,可能引发内存泄漏或悬空指针。
深拷贝:复制对象时不仅复制值,还为指针成员分配新内存并复制内容,每个对象拥有独立资源,避免内存问题。
运算符重载
允许自定义类对内置运算符(如+, -, =, []等)的行为。
形式:返回类型 operator运算符(参数列表)
C++ STL
什么是STL,包含哪些组件
STL(Standard Template Library)是 C++ 标准库的核心组成部分,旨在提供高效、通用的算法和数据结构。它通过模板技术实现了代码复用,显著提升了开发效率。
STL 的四大核心组件分别是容器(Containers)、算法(Algorithms)、迭代器(Iterators)、函数对象(Function Objects)、
容器(Containers),用于存储数据的模板类,分为序列式容器、关联式容器和无序容器。
- 序列式容器:保持元素的插入顺序,如 vector、list、deque、array、forward_list。
- 关联式容器:基于键存储元素,支持高效查找,如 set、map、multiset、multimap。
- 无序容器(C++11+):使用哈希表实现,如 unordered_set、unordered_map。
算法(Algorithms)
操作容器元素的通用函数,如 sort、find、transform、merge、accumulate 等。算法通过迭代器与容器解耦,实现了通用性。
迭代器(Iterators)
遍历容器元素的接口,行为类似指针。STL 定义了 5 种迭代器类别:
- 输入迭代器(Input)
- 输出迭代器(Output)
- 前向迭代器(Forward)
- 双向迭代器(Bidirectional)
- 随机访问迭代器(Random Access)
函数对象(Function Objects)重载了 operator() 的类或结构体,可作为算法的参数(如排序规则),例如 less、greater。C++11 后,lambda 表达式逐渐替代了部分函数对象的使用。
STL 的特点
- 通用性:通过模板实现与数据类型无关的设计。
- 效率:算法经过高度优化,性能接近手写代码。
- 扩展性:支持自定义容器、算法和迭代器。
map && set的区别和实现原理
区别总结
1.元素存储方式
- set:仅存储键(key),每个元素都是唯一的,即不允许重复。
- map:存储键值对(key-value),每个键唯一,但值可以重复或不同。
2.迭代器访问 - set:迭代器直接指向键,例如:for (const auto& key : mySet) { … }
- map:迭代器指向pair<const Key, T>,需通过it->first(键)和it->second(值)访问,例如:for (const auto& pair : myMap) { cout << pair.first << ": " << pair.second; }
3.典型用途 - set:去重、快速查找某个元素是否存在(时间复杂度 O (log n))。
- map:需要键值映射关系的场景,如字典、缓存等。
实现原理
两者通常基于红黑树(一种自平衡二叉搜索树)实现,具有以下特点:
- 有序性:元素默认按键的升序排列(可通过自定义比较函数修改)。
- 插入 / 删除 / 查找效率:均为 O (log n),因为红黑树的平衡性保证了树的高度始终为 O (log n)。
- 内存开销:每个节点需要额外存储父节点、左右子节点的指针,以及颜色标记(红 / 黑),因此空间复杂度为 O (n)。
map && unordered_map的区别
1.数据结构:map基于红黑树,保证元素有序;unordered_map基于哈希表,元素无序。
2.时间复杂度:map操作均为 O (log n),unordered_map平均 O (1),但需处理哈希冲突。
3.选择依据:若业务需要有序遍历或范围查询,选map;若追求单元素操作效率,选unordered_map。”
set && unordered_set的区别
1.数据结构:set基于红黑树,保证元素有序;unordered_set基于哈希表,元素无序。
2.时间复杂度:set操作均为 O (log n),unordered_set平均 O (1),但需处理哈希冲突。
3.选择依据:若业务需要有序遍历或范围查询,选set;若追求单元素操作效率,选unordered_set。”
push_back 和 emplace_back 的区别
1.参数类型:
- push_back 接受已构造的对象,可能触发拷贝 / 移动操作。
- emplace_back 接受构造参数,直接就地构造对象。
2.效率:
- emplace_back 通常更高效,尤其对于构造代价高的对象(如包含动态资源的类)。
3.适用场景:
- 当需要隐式转换或直接构造对象时,emplace_back 更简洁;
- 当添加已有对象时,两者效果相同。”
vector和list的区别
“迭代器的核心作用是提供统一的方式遍历和操作容器元素,实现了算法与容器的解耦。迭代器失效主要发生在容器结构被修改时:
1.动态数组类容器(如vector):
- 插入可能因扩容导致所有迭代器失效;
- 删除会使被删位置及之后的迭代器失效。
2.链表类容器(如list):
- 插入不影响其他迭代器;
- 删除仅使被删节点的迭代器失效。
3.关联容器(如set):
- 插入和删除通常只影响被操作节点的迭代器。
C++模板全特化和偏特化
模板特化是 C++ 模板元编程的核心机制,允许为特定类型定制模板实现:
1.全特化:
- 语法:template<> + 类名 / 函数名 + 全限定类型(如template<> struct Foo)。
- 用途:为某个具体类型提供完全不同的实现逻辑。
2.偏特化:
- 语法:template<参数列表> + 类名 / 函数名 + 部分限定类型(如template struct Foo<T*>)。
- 用途:为一组类型(如指针、容器)提供统一的特殊处理。
3.关键区别:
- 全特化完全消除模板参数,偏特化仍保留部分参数。
- 函数模板不支持偏特化,需通过重载或类模板间接实现。
C++11的新特性有哪些
“C++11 引入了一系列现代编程特性,主要包括:
1.语法糖:auto、范围 for 循环、lambda 表达式简化代码。
2.内存管理:智能指针(unique_ptr/shared_ptr)避免内存泄漏。
3.移动语义:右值引用和std::move提升资源转移效率。
4.并发编程:标准线程库(std::thread)和原子操作支持多线程。
5.模板增强:可变参数模板和模板别名扩展泛型编程能力。
6.性能优化:移动语义减少深拷贝,constexpr支持编译期计算。
7.设计模式简化:lambda 表达式替代函数对象,智能指针简化资源管理。
8.兼容性:部分特性(如std::make_unique)在 C++14 中进一步完善。
9.其他特性:统一初始化、强类型枚举、nullptr等提升安全性和表达力。
前面这些加起来也就是一万零几百个字,一遍讲话每分钟120-15-字,就算最慢每分钟120字,10000/120 = 83.3 ,不到一个半小时,完成比完美重要,先记住基本的东西,其他的在实践当中去熟悉!