萱仔求职复习系列:C++超基础期末复习版本
由于最近笔试面试太多了,实在是很久没有更新我的博客,真是非常惭愧,近期涉及到很多C++的相关知识,我还是准备系统的复习一下C++,这边有两个电子版的教材,不知道大家伙是否需要,需要的可以私我我可以共享一些资源(不是广告,不要money),这两本书是:C++Prime中文版第五五版和C++Prime Plus中文版第六版。
还有就是我把我复习工程中涉及到一些实验的代码都总结到了仓库里,这边到时候上传了之后,贴出网址,一些非常基础简单的代码,涵盖了C++基础知识,按照每个模块分类,一目了然。
接下来是我近期C++期末复习版本(简单的一些基础知识,怕面试的时候被问到基础反而唯唯诺诺嘿嘿)
---------------------------------------------------------------------------------------------------------------
1. C++ 基础知识
基本语法:
1、数据类型(int, float, double, char, bool, string)
原始类型:int, float, double, char, bool
string 是 C++ 中处理文本的类,头文件 <string>
auto 关键字用于自动推断变量类型
数据类型 | 描述 | 占用内存 | 常见错误与注意事项 |
int | 整数类型 | 4 字节 | 1. 可能超出范围(-2,147,483,648 到 2,147,483,647) |
2. 未初始化会导致未定义行为 | |||
float | 单精度浮点数 | 4 字节 | 1. 浮点数比较需小心(使用阈值) |
2. 精度有限,可能导致舍入错误 | |||
double | 双精度浮点数 | 8 字节 | 1. 同样需要注意浮点数比较 |
2. 转换为 int 时小数部分会丢失 | |||
char | 单个字符 | 1 字节 | 1. 可与整数运算,需小心 ASCII 值的使用 |
2. 未初始化会导致未定义行为 | |||
bool | 布尔类型(true 或 false) | 1 字节 | 1. 只能是 true 或 false,避免用整数表示 |
string | 字符串类型 | 动态分配 | 1. 使用时需确保包含 <string> 头文件 |
2. 初始化与赋值时需注意字符数组与 string 的区别 |
#include <iostream>
#include <string>
using namespace std;
int main() {
int age;
float height;
string name;
string adress;
cout << "输入名字: ";
cin >> name;
cout << "输入年龄: ";
cin >> age;
cout << "输入身高: ";
cin >> height;
cout << "输入家乡: ";
cin >> adress;
cout << "Hello, " << name << "! You are " << age << " years old and " << height << " meters tall." << endl;
return 0;
}
2、条件语句(if, switch)
if-else: 条件判断
switch: 多条件判断,适用于枚举或整数类型
if-else vs switch:
if-else 适合多个条件和复杂逻辑。可以嵌套多个条件。
switch 适合对单个变量进行多个精确值的判断(如选择菜单项),通常简洁、效率更高。
3、循环语句(for, while, do-while)
for 和while的对比:
for 适合已知次数的循环,并在条件、更新部分写明循环逻辑,代码紧凑清晰。
while 适合不确定循环次数的情况,尤其是需等待特定条件满足时。适合处理事件或监听某条件的代码。
while 和 do-while的对比:
while 先判断条件,若不满足,循环一次也不会执行。
do-while 则至少执行一次,不管初始条件如何。
以下表格是使用方法和用途
控制结构 | 用途 | 语法形式 | 特点和注意事项 |
if-else | 条件判断 | if (条件) { ... } else { ... } | 用于根据条件判断执行不同的代码块。可以嵌套多个 if-else 实现多层判断。 |
switch | 多分支选择 | switch (变量) { case 1: ...; break; case 2: ...; break; default: ...; } | 对单个变量的多个值做出选择。适合枚举或有限的具体值情况。每个 case 末尾要用 break 避免执行后续分支。 |
for 循环 | 固定次数循环 | for (初始化; 条件; 递增/递减) { ... } | 适用于已知循环次数的情况。for 的三个参数分别表示初始值、循环条件、每次循环的增减。 |
while 循环 | 条件控制循环 | while (条件) { ... } | 适用于循环次数不确定,但满足某条件时才执行的情况。条件在循环开始前检查。 |
do-while 循环 | 条件控制循环 | do { ... } while (条件); | 和 while 类似,但至少执行一次后再检查条件。适合先执行一次操作的情况。 |
代码略(在仓库里,这边放出来太长了)
4、函数定义、返回类型、参数传递方式(按值、按引用、指针)
按值传递:传递值的副本
按引用传递:传递引用,使用 & 符号
1、函数定义
返回类型:定义函数的返回值类型(例如 int、double、void 等)。如果函数不返回任何值,使用 void 作为返回类型。
函数名:标识函数的名字,用来在调用时进行引用。
参数列表:定义传递给函数的变量,写在括号内,可以为空。
函数体:包含了执行的代码块,位于大括号内。
2、函数参数传递方式
1. 按值传递 (Pass by Value)
在按值传递中,调用函数时传递的是变量的副本。这个副本在函数内部是独立存在的,不会影响到原始变量。
优点:安全性高,函数内的修改不影响原变量,因此不会有意外的副作用。
缺点:对于大数据类型(如大对象或数组),按值传递效率较低,因为需要复制数据。
适用场景:当只需要读取参数值,且不希望原数据被修改时使用。
2. 按引用传递 (Pass by Reference)
按引用传递时,函数接收的是变量的引用,也就是变量的别名。引用实际指向调用者的变量地址,因此函数内的修改会直接影响原变量。
优点:省略了数据的复制,效率较高。可以直接修改传入的变量。
缺点:如果函数中意外修改了数据,可能会引发不可预期的副作用。
适用场景:需要在函数中修改参数的值,或者需要高效地传递大对象时使用。
3. 指针传递 (Pass by Pointer)
指针传递的原理与引用类似,都是对原始数据的直接操作。不同之处在于,指针传递时传递的是变量的地址,函数通过地址访问变量内容。函数内部可以通过指针对原始变量进行修改。
优点:灵活性高,可以通过地址操作数据,同时也能实现“按引用传递”的效果。
缺点:指针需要解引用,容易因未初始化指针等问题产生错误,稍不注意可能造成内存泄漏或非法访问。
4. 默认参数 (Default Arguments)
默认参数允许在调用函数时不传入某些参数,此时会使用预设的默认值。如果在调用时提供参数,则会覆盖默认值。默认参数主要提升了函数的灵活性和可读性。
优点:减少函数重载的需求,使代码更简洁。
缺点:如果使用不当,可能使代码逻辑复杂化。
适用场景:函数参数不确定,且希望为缺省参数设定默认值时使用。
适用场景:需要传递对象的地址,或者操作复杂结构(如链表、树结构)时更适合使用指针。
参数传递方式 | 定义方式 | 适用场景 | 特性 |
值传递 | void func(int x) | 需要保护原始数据,不影响外部变量 | 函数接收参数的副本 |
引用传递 | void func(int &x) | 函数需要修改外部变量 | 函数接收变量的引用 |
指针传递 | void func(int *x) | 需要处理内存资源或指向对象时 | 通过解引用访问和修改实际数据 |
默认参数 | int func(int x = 10) | 参数可选,使用常见的默认值 | 未提供实参时使用默认值 |
3、函数重载 (Function Overloading)
函数重载允许多个同名函数存在,通过参数类型和个数的不同来区分不同的函数版本。编译器会根据传入的参数选择适当的函数调用。函数重载提升了代码的灵活性,使同一操作可以适用于不同的数据类型。
优点:增强代码的可读性和扩展性,可以复用函数名。
缺点:如果过度使用重载可能会增加代码复杂度。
同样代码放入仓库,需要自取。
什么时候用地址传递?
希望修改传入的数据:如交换变量、修改数组等。
传递大型数据:如大型数组、结构体等,避免内存浪费和性能开销。
需要输出多个值:可以通过传递指针来获得多个输出结果。
浅拷贝和深拷贝(面试常问的问题)
浅拷贝:仅复制对象的引用或指针,而不复制实际的数据内容。这样拷贝的结果是新对象和原对象共享同一块内存数据。对于简单数据(如数值或基本类型的变量),浅拷贝通常不会引发问题;但对于动态分配的内存、数组、或指针成员,则会造成多方共享同一数据区域的问题,可能导致意外的数据修改。
深拷贝:不仅复制对象的引用或指针,还会复制其所指向的数据内容。深拷贝可以保证新对象拥有独立的数据副本,不会和原对象共享内存数据,因此它们之间的操作互不干扰。
底层操作或优化:如操作硬件或高性能代码优化。
传指针:传递地址,使用 *
函数重载、内联函数(inline)
5、指针和引用(超级重难点,老搞乱)
指针定义:指针是一个变量,用于存储另一个变量的内存地址。通过指针,可以直接访问和操作内存。
指针类型 | 描述 | 示例代码 |
普通指针 | 存储单个变量的地址 | int* p; p = &a; |
空指针 | 指向 NULL,表示不指向任何有效地址 | int* p = nullptr; |
野指针 | 指向一个已经释放或未初始化的内存地址 | int* p; *p = 10; // 错误 |
指向数组的指针 | 指向数组的首地址,可以通过指针访问数组元素 | int arr[] = {1, 2, 3}; int* p = arr; |
指向指针的指针 | 指向另一个指针的地址 | int** pp; |
常量指针 | 指向的内容不可修改(指向的值是常量) | const int* p; |
指针常量 | 指针本身的地址不可修改 | int* const p; |
动态内存分配指针 | 使用 new 关键字动态分配内存 | int* p = new int; **p = 20; |
指针的声明、解引用、指针运算
引用的使用,区别于指针
动态内存分配 (new 和 delete)
指针的作用:我一直以为指针会多占用一些内存空间,因为不仅存储了实际的数据,还额外存储了数据的地址。但我后来觉得指针的存在并不仅仅是为了存储数据,指针主要是为了带来更强大的灵活性和更方便去控制一些内容。
1. 动态内存分配
通过指针可以实现动态内存分配,让程序在运行时根据需要申请内存,而不是一开始就指定好大小。比如说,输入一个数组大小未知的字符串时,指针允许在运行时动态地分配一个适合大小的数组。如果不使用指针,那只能定义一个固定大小的数组,浪费内存或者受限于固定大小。
比如,在图像处理、数据流处理等应用中,数据大小经常是变化的。动态内存分配能让程序更高效地使用内存。
2. 高效的数据传递(特别是大数据量)
当需要传递很大的数据(如一个数组或对象)时,通过指针传递地址,可以避免创建数据的副本,大幅提高效率。如果不使用指针,传递一个大的结构体会需要复制每一个字段。而使用指针,只需要传递数据所在的地址即可,效率更高。
假设有个大文件数据,需要进行多次处理。通过指针来传递文件数据,不仅节省内存,还避免了多次不必要的复制操作。
3. 直接访问和操作内存
指针可以直接访问和操作内存,通过改变指针指向的内容实现不同的数据操作,这在低级编程(比如操作硬件、嵌入式编程)中非常重要。就像一些嵌入式开发中,往往需要直接控制设备寄存器地址(这里我还没有操作过,只是一个自己的了解,但是了解不多,如果有误请指正嘿嘿)。通过指针可以直接对设备的寄存器进行读写操作,这在没有指针的情况下很难实现。
比如操作系统编写中,可能需要对设备寄存器进行访问,通过指针可以很方便地控制硬件。
4. 指针支持灵活的数据结构
指针可以灵活的实现链表、树、图等动态数据结构,这些数据结构在数据存储和处理时非常灵活。链表、树、图等结构通常需要在内存中动态管理节点,如果没有指针,无法高效实现这些结构。数据库内部常常会用树来实现数据检索,指针让树结构的节点随意增减变得很灵活。
5. 函数参数传递中的灵活性
指针在函数传参时可以带来更多灵活性,通过指针可以实现传入参数在函数内部被修改。使用指针可以传递值的引用,从而允许函数直接修改传入的变量。否则,C++的默认传参是值传递,函数无法直接修改原始数据。
比如在一些需要排序的算法中,可能会传入一个需要改变的数组,通过指针可以直接修改这个数组,而无需返回新的数组。
我本人在硕士期间用到了一些C++就是在图像方面,opencv库,指针能够直接访问和操作图像的像素数据,带来了极高的灵活性和效率。这里不是基础里的重点,我是为了自己方便自己记录才写进去的,大家不搞图像的可以无视这里
1. 基本图像数据的指针操作
OpenCV的
cv::Mat
类封装了图像数据,并提供了对数据的直接访问方法。图像的数据存储在一个连续的内存块中,称为data
成员,它是一个指向图像像素数据的uchar*
指针。利用这个指针,可以直接访问图像的像素值。2. 多通道图像的指针操作
在彩色图像中(如RGB图像),每个像素包含多个通道的数据。可以通过指针按顺序访问每个通道的数据,例如BGR格式的图像,第一个字节为蓝色通道,第二个字节为绿色,第三个字节为红色。
3. 使用
cv::Mat::ptr<T>()
方法进行类型安全的指针访问OpenCV提供了
cv::Mat::ptr<T>()
方法,可以直接获取每行图像数据的指针。这种方式不仅方便,还能提高代码的可读性和安全性。它允许我们按照图像的具体类型来访问指针,而不是通过uchar*
来访问。这里不是基础里的重点,我是为了自己方便自己记录才写进去的,大家不搞图像的可以无视这里
6。常见错误及注意事项
内存泄漏:如果动态分配的内存没有被释放,会导致内存泄漏。要确保每次使用 new 后都有对应的 delete。
双重释放:对同一个指针调用 delete 多次,会导致程序崩溃。释放后应将指针设为 nullptr。
指针算术:可以通过指针算术来遍历数组,但需要小心边界,避免访问非法内存。
int arr[] = {10, 20, 30};
int* p = arr;
for (int i = 0; i < 3; i++) {
std::cout << *(p + i) << std::endl; // 遍历数组
}
6、C++ 面向对象编程 (OOP)
OOP 是一种编程范式,核心理念是 封装 (Encapsulation)、继承 (Inheritance) 和 多态 (Polymorphism),其目标是通过类和对象的概念,模拟现实世界的实体和行为。
一些常见类与对象关键字的应用:(一章里面太杂乱,放不进去了,拆分一下)
1、类与对象
类 (Class):
类是创建对象的模板,定义了一组属性(成员变量)和行为(成员函数)。类中的成员可以有不同的访问权限(如 public, private 等)。
对象 (Object):
对象是类的实例,通过对象可以访问类的成员变量和成员函数。
类和对象的类比
类:水果的概念(例如“苹果”这一类的定义)。
属性:颜色、重量、味道等。
行为:可以被切开、被食用。
对象:某个具体的苹果(“一个红色的苹果”)。
属性:红色,重量 200 克,味道甜。
行为:具体的吃法、切割的方法。
定义类:
Car 类描述了汽车的共同特性(品牌和速度)和行为(设置速度、获取速度、显示信息)。
属性:brand 和 speed。
方法:setSpeed、getSpeed 和 display。
创建对象:
car1 和 car2 是从类 Car 创建的对象,每个对象都有自己的属性值:
car1 的品牌是 Toyota,初始速度是 120。
car2 的品牌是 BMW,初始速度是 150。
修改和使用对象:
car1 调用了 setSpeed 方法,修改了速度。
display 方法展示了每辆车的品牌和速度。
#include <iostream> #include <string> using namespace std; // 定义类 class Car { private: string brand; // 属性:品牌 int speed; // 属性:速度 public: // 构造函数 Car(string b, int s) { brand = b; speed = s; } // 方法:设置速度 void setSpeed(int s) { speed = s; } // 方法:获取速度 int getSpeed() { return speed; } // 方法:展示信息 void display() { cout << "Brand: " << brand << ", Speed: " << speed << " km/h" << endl; } }; int main() { // 创建对象(类的实例化) Car car1("Toyota", 120); // 对象 car1 Car car2("BMW", 150); // 对象 car2 // 使用对象调用方法 car1.display(); // 输出: Brand: Toyota, Speed: 120 km/h car2.display(); // 输出: Brand: BMW, Speed: 150 km/h // 修改 car1 的速度 car1.setSpeed(180); cout << "Updated speed of car1: " << car1.getSpeed() << " km/h" << endl; return 0; }
构造函数与析构函数
构造函数
构造函数是一个特殊的成员函数,用于初始化类的对象。它与类同名,并且没有返回值(甚至没有 void)。自动调用:当对象被创建时,构造函数会被自动调用。可以重载:一个类可以有多个构造函数,称为构造函数重载,取决于传递的参数。默认构造函数:不带参数的构造函数。如果没有定义,编译器会生成一个默认的构造函数。拷贝构造函数:用来初始化一个对象,使其成为另一个同类对象的副本。
需要构造函数的情况:当需要在对象创建时初始化成员变量时。如果类有复杂的初始化逻辑(例如,打开文件、连接数据库等)。
析构函数
析构函数是另一个特殊的成员函数,用于清理对象。它的名字以 ~ 开头并与类名相同。
自动调用:当对象生命周期结束时(例如离开作用域、显式销毁),析构函数被自动调用。
无参数、无返回值:析构函数不能重载。常用于释放动态分配的资源(如内存、文件句柄)。
如果不显式定义析构函数,编译器会自动提供一个默认析构函数。默认析构函数什么都不做,但它会释放对象的内存。如果类中有动态分配的资源(例如通过 new 分配的内存),需要手动定义析构函数以释放这些资源;否则会导致内存泄漏。
需要析构函数的情况:当类有动态分配的资源需要释放时。如果类需要在对象销毁时执行特定清理操作(例如,关闭文件、断开连接等)
2、继承与多态(虚函数、override 关键字)
继承允许一个类(子类/派生类)从另一个类(父类/基类)继承属性和方法,从而实现代码复用和扩展。继承分为三种类型:public 继承、protected 继承、private 继承。它们决定了基类成员在派生类中的访问权限。
访问控制(public, private, protected)
基类成员修饰符 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
public | 仍是 public | 成为 protected | 成为 private |
protected | 仍是 protected | 仍是 protected | 成为 private |
private | 不可访问 | 不可访问 | 不可访问 |
class Base {
public:
int x;
protected:
int y;
private:
int z;
};
class Derived : public Base {
void show() {
x = 10; // OK: public 成员在派生类中仍为 public
y = 20; // OK: protected 成员在派生类中仍为 protected
// z = 30; // 错误!private 成员在派生类中不可访问
}
};
封装性:protected 和 private 继承可以更好地隐藏实现细节,限制对基类成员的访问。
设计模式:继承类型反映了类之间的关系,增强代码的可读性。
多层继承
一个类可以继承另一个派生类,形成多层继承。
class A {
public:
void showA() { cout << "Class A" << endl; }
};
class B : public A {
public:
void showB() { cout << "Class B" << endl; }
};
class C : public B {
public:
void showC() { cout << "Class C" << endl; }
};
int main() {
C obj;
obj.showA(); // 来自 A
obj.showB(); // 来自 B
obj.showC(); // 来自 C
return 0;
}
3、重载:
1. 函数重载
函数重载是指在同一个作用域内定义多个同名函数,通过参数列表的不同来区分函数。参数列表的不同可以是:参数的个数不同。参数的类型不同。参数的顺序不同(类型不同的情况下)。函数重载可以让同一个函数名能够适应不同的参数场景。
参数类型或数量必须有区别,仅返回值不同无法构成重载,默认参数可能会导致歧义
运算符重载(例如 +, [], ())
运算符重载是 C++ 的一种特殊功能,允许程序员为自定义类型(类)定义新的运算符行为。比如可以重载 +、[]、() 等运算符,使它们适用于自定义类。比如运算符重载可以使自定义类型能够更直观地使用操作符。例如,我如果为 Matrix 类重载 + 运算符,就可以直接实现矩阵加法。
返回类型 operator运算符(参数列表) {
// 实现重载的运算逻辑
}
几乎所有运算符都可以被重载,除了以下几个:
:: (作用域解析运算符)
. (成员访问运算符)
.* (成员指针访问运算符)
sizeof (长度运算符)
typeid (类型识别)
4、标准模板库(STL):
C++ 标准模板库(STL,Standard Template Library)是 C++ 标准库的重要组成部分,提供了一组强大的模板类和算法,简化了编程工作。STL 的核心思想是利用模板技术,实现通用的、高效的、类型独立的数据结构和算法。
STL 包括四个主要部分:容器(Containers)算法(Algorithms)迭代器(Iterators)函数对象(Function Objects)
常用容器:vector, list, deque, set, map, unordered_map,容器是 STL 中存储数据的结构。它们提供了不同的方式来组织和存储数据,并支持灵活的数据存取。STL 提供了多种不同类型的容器,每种容器适用于不同的场景。
顺序容器:用于按顺序存储数据。
vector:动态数组,支持高效随机访问,适用于频繁访问和尾部插入的场景。
deque:双端队列,支持在两端高效插入和删除,适用于双端操作。
list:双向链表,支持在任意位置高效插入和删除,但不支持随机访问。
array:固定大小的数组,类似 C 风格的数组。
关联容器:用于按键值对存储数据,支持通过键快速查找。
set:集合,存储唯一的元素,自动排序。
map:映射,存储键值对,按键自动排序。
multiset:多重集合,允许重复元素,自动排序。
multimap:多重映射,允许键重复,按键排序。
无序容器:基于哈希表实现,存储数据时不排序。
unordered_set:无序集合,存储唯一元素。
unordered_map:无序映射,存储键值对。
unordered_multiset:无序多重集合,允许重复元素。
unordered_multimap:无序多重映射,允许键重复。
容器的使用:
STL 容器提供了很多常用的操作,如:
插入元素:push_back(), insert(), emplace()
删除元素:pop_back(), erase(), clear()
查找元素:find(), count(), at()
容器大小:size(), empty()
排序与反转:sort(), reverse()
关于所有容器中可以使用的那些操作列表如下:
容器类型 | vector | deque | list | array | set, map | unordered_set, unordered_map |
push_back() | 支持 | 支持 | 不支持尾部 | 不支持 | 不支持 | 不支持 |
insert() | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 |
emplace() | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 |
pop_back() | 支持 | 支持 | 不支持 | 不支持 | 不支持 | 不支持 |
erase() | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
clear() | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
find() | 不支持 | 不支持 | 不支持 | 不支持 | 支持 | 支持 |
count() | 不支持 | 不支持 | 不支持 | 不支持 | 支持 | 支持 |
at() | 支持 | 支持 | 不支持 | 支持 | 不支持 | 不支持 |
size() | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
empty() | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
sort() | 支持 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
reverse() | 支持 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
迭代器的使用
常用算法:sort(), find(), count(), lower_bound(), upper_bound()
由于我本人是更熟悉python,所以我决定这边将最常用的四个容器和python进行一个对比,方便我更加好的复习C++
容器类型 | C++ | Python | 主要区别 |
列表(List) | vector | list | 内存布局:C++ vector 和 Python list 都是动态数组,支持随机访问和动态扩展。 |
性能:C++ 的 vector 通常更高效。 | |||
切片:Python 的 list 支持切片操作,C++ 的 vector 不支持。 | |||
插入操作 | push_back(x) 插入元素到末尾,insert(position, x) 插入到指定位置 | append(x) 插入元素到末尾,insert(i, x) 插入到指定位置 | 操作方法类似。 |
但 C++ 更强调内存的管理,Python 插入操作时可以更灵活。 | |||
删除操作 | pop_back() 删除末尾元素,erase(position) 删除指定位置元素 | pop() 删除末尾元素,remove(x) 删除指定元素 | Python 的 remove(x) 删除指定值,第一个匹配项,C++ 使用 erase() 删除指定位置。 |
访问操作 | [] 访问元素,at() 安全访问元素 | [] 访问元素,get() 安全访问元素(如果指定默认值) | C++ 提供了 at(),可以在越界时抛出异常,Python 提供了 get() 方法来返回默认值。 |
排序操作 | std::sort() 排序 | sort() 排序 | 排序方法相同,但 C++ 的排序通常较为高效,且可以指定比较函数。 |
反转操作 | std::reverse() | reverse() | 操作相同 |
空检查 | empty() | len() 结果为 0 检查空 | C++ 使用 empty() 来检查是否为空,Python 使用 len() 来获取长度并判断是否为空。 |
容器类型 | C++ | Python | 主要区别 |
集合(Set) | set | set | 内存布局:C++ 使用红黑树实现,元素有序;Python 使用哈希表实现,元素无序。 |
性能:C++ 的 set 支持对元素进行排序,查找速度较快。Python 的 set 支持更快速的集合运算(如并集、交集等)。 | |||
插入操作 | insert(x) 插入元素 | add(x) 插入元素 | 操作方法类似,但 Python 的 add() 方法可以简写为 x in set,C++ 使用 insert()。 |
删除操作 | erase(x) 删除指定元素 | remove(x) 删除指定元素,discard(x) 删除元素忽略不存在 | Python 提供了 discard(),如果元素不存在则不抛出异常,C++ 使用 erase() 删除指定元素。 |
查找操作 | find(x) 查找元素 | in 操作符检查元素是否存在 | C++ 使用 find() 返回元素位置(若未找到返回 end()),Python 使用 in 来检查。 |
集合运算 | 不支持直接的集合运算 | 支持 union(), intersection(), difference() 等集合运算 | Python 提供了丰富的集合运算方法,而 C++ 的 set 不直接支持这些运算(需要手动实现)。 |
排序 | 自动按元素大小排序 | 无序,排序需要额外操作 sorted() | C++ 中的 set 元素有序,Python 中的 set 无序。 |
容器类型 | C++ | Python | 主要区别 |
字典(Map/Dict) | map | dict | 内存布局:C++ 使用红黑树实现,元素有序;Python 使用哈希表实现,元素无序。 |
性能:Python 的 dict 对查找、插入、删除的性能非常高,但无序;C++ 的 map 支持有序键。 | |||
插入操作 | insert({key, value}) 插入键值对 | [] 通过键插入值,update() 合并字典 | C++ 使用 insert() 插入键值对,Python 使用 [] 插入键值。 |
删除操作 | erase(key) 删除键值对 | del 删除键值对,pop(key) 返回并删除键值对 | C++ 使用 erase() 删除指定键,Python 使用 del 或 pop() 删除键值。 |
查找操作 | find(key) 查找键 | in 操作符检查键是否存在,get(key) 获取键值 | C++ 使用 find() 查找键,返回一个迭代器,Python 使用 in 或 get()。 |
排序 | 自动按键排序 | 无序,排序需要额外操作 sorted() | C++ 的 map 按键排序,Python 的 dict 无序。 |
更新 | 使用 [] 更新键值 | 使用 [] 更新键值 | C++ 和 Python 都支持通过 [] 直接更新键值对。 |
容器类型 | C++ | Python | 主要区别 |
元组(Tuple) | tuple | tuple | 不可变性:两者都是不可变容器,适合存储不同类型的元素。 |
大小:两者都必须在定义时确定大小。 | |||
访问操作 | std::get<i>(t) 访问第 i 个元素 | t[i] 访问第 i 个元素 | 操作类似,C++ 使用 std::get<i>(t),Python 使用索引访问。 |
切片操作 | 不支持切片操作 | 支持切片操作 t[start:end] | Python 支持 |
2. C++ 进阶知识
1、模板编程:
函数模板和类模板的定义与使用
模板特化与偏特化
2、异常处理:
C++ 提供了一种机制来处理程序运行时的错误,称为异常处理。异常是程序在执行过程中遇到的异常情况,如数组越界、文件无法打开等。常见的异常类型和处理方式涵盖了很多不同的错误场景,异常不仅仅是简单的 "程序崩溃" 问题,还可以包括资源管理、输入输出错误、越界访问等。内存泄漏本身通常不是异常(异常用于处理错误情况),但内存泄漏可能是错误发生的一部分,因此异常处理间接地帮助避免或诊断内存泄漏问题。
异常的抛出与捕获(try, catch, throw)
例如:std::out_of_range: 这种异常通常在访问数组或容器时,当索引超出有效范围时抛出。
try {
std::vector<int> v = {1, 2, 3};
std::cout << v.at(10) << std::endl; // 访问超出范围的元素
} catch (const std::out_of_range& e) {
std::cout << "Out of range: " << e.what() << std::endl;
}
v.at(index) 是 std::vector 的成员函数,用于访问向量中索引为 index 的元素,同时它会检查索引是否超出范围。如果索引超出范围,会抛出 std::out_of_range 异常。10 是越界索引:这里 v 的大小为 3,只有索引 0, 1, 2 是合法的。索引 10 超出了向量的范围,因此会抛出异常。
注意:v[10] 不会抛出异常,而是表现为未定义行为(可能导致崩溃或访问垃圾数据),因此推荐使用 at 进行安全访问。
catch (const std::out_of_range& e)
catch 是异常捕获块,用于处理 try 块中抛出的异常。std::out_of_range 是标准异常类,用于表示索引超出范围的异常。const 表示捕获的异常对象不能被修改,通常在异常处理中使用。
& e:捕获异常的引用,避免对象的拷贝操作。e 是捕获到的异常对象,e.what() 是标准异常类的成员函数,用于返回异常的详细描述。
自定义异常类
3、智能指针:
unique_ptr, shared_ptr, weak_ptr 的区别与使用场景
智能指针是 C++ 提供的一种管理动态内存的工具,让我可以不用手动管理内存,从而减少内存泄漏和非法访问的风险。智能指针通过 RAII(资源获取即初始化)原理,在对象生命周期结束时自动释放内存。
C++ 标准库中常用的三种智能指针:
std::unique_ptr:独占所有权:管理独占资源,如文件句柄或动态分配的内存,避免多方访问导致的不安全操作。
独占所有权,即一个对象只能由一个 unique_ptr 管理。
禁止拷贝操作,但可以通过 std::move 转移所有权。
内存由 unique_ptr 自动释放,生命周期与智能指针一致。
std::shared_ptr:共享所有权。需要多个对象共享资源时,例如图结构中的节点共享连接。
共享所有权,可以有多个 shared_ptr 指向同一个对象。
采用引用计数机制,记录当前有多少个 shared_ptr 管理同一个对象。当计数归零时,释放内存。
std::weak_ptr:弱引用,不影响共享所有权。防止循环引用(如图或双向链表)。用于缓存或观察者模式。
不影响引用计数,解决循环引用问题。
通常用作辅助引用 shared_ptr 所管理的对象。
必须通过 lock() 将 weak_ptr 转换为 shared_ptr 才能访问对象。
特性 | unique_ptr | shared_ptr | weak_ptr |
所有权管理 | 独占所有权 | 共享所有权 | 辅助引用(不共享所有权) |
内存释放 | 自动释放 | 引用计数归零时释放 | 依赖 shared_ptr |
拷贝操作 | 不支持 | 支持 | 支持 |
循环引用解决 | 无需考虑 | 会产生循环引用 | 防止循环引用 |
适用场景 | 独占资源 | 共享资源 | 缓存 |
4、多线程与并发:
这个多线程这里面试真的非常非常容易遇到,C++提供了多线程编程支持,可以利用现代多核处理器的并行能力。多线程并不是高并发的唯一标志,但它是实现高并发的一个重要方式。在实际应用中,高并发不仅涉及多线程,还可能涉及异步编程、I/O复用等技术。
特性 | 进程 | 线程 |
定义 | 程序的独立执行实例,拥有独立的内存空间。 | 进程内的执行流,线程共享进程的资源。 |
通信 | 进程间通信(IPC)复杂,如管道、共享内存等。 | 线程间通信简单,直接访问共享变量。 |
切换开销 | 高,涉及到内存切换和操作系统资源分配。 | 低,只需保存和恢复寄存器和栈指针。 |
失败影响范围 | 一个进程的失败不会影响其他进程。 | 一个线程的崩溃可能导致整个进程崩溃。 |
多线程与高并发的关系
高并发是指系统能够同时处理大量请求或任务。多线程是一种手段,通过并行执行任务提升程序的处理能力。多线程更关注如何并行处理任务。高并发更关注如何应对大量请求,包括任务分配、资源调度、负载均衡。
thread 类的使用,创建线程、线程同步
std::thread 是 C++ 标准库中的线程类,用于创建和管理线程。支持传递函数、lambda 表达式或类成员函数作为线程任务。多线程同时访问共享资源可能会引发数据竞争,以下工具可实现线程同步。
mutex 和 lock_guard 实现互斥访问
std::mutex 是互斥锁,用于保护共享资源。std::lock_guard 是一种 RAII 风格的锁,能自动管理锁的加解锁操作,避免忘记释放锁。
condition_variable 实现线程间的通信
condition_variable 是线程间的通信工具,允许一个线程等待另一个线程的通知。通常与 std::unique_lock<std::mutex> 配合使用。
3. 常用数据结构(后续补充力扣最常见的用法习题)
数组与链表:
数组:访问、插入、删除的时间复杂度
单链表、双向链表:增删改查操作及实现
栈与队列:
栈:后进先出(LIFO),常用操作:push(), pop(), top()
队列:先进先出(FIFO),常用操作:enqueue(), dequeue()
优先队列(priority_queue):堆的实现
树与图:
二叉树、二叉搜索树(BST):插入、删除、查找
平衡树(AVL 树,红黑树):自动平衡机制
图:邻接矩阵与邻接表的表示
树的遍历:前序、中序、后序、层序遍历
最小生成树(Prim、Kruskal算法),最短路径算法(Dijkstra、Floyd)
4. 常用算法(后续补充力扣最常见的用法习题)
排序算法:
冒泡排序、选择排序、插入排序
归并排序、快速排序(递归实现与优化技巧)
堆排序(基于最大/最小堆实现)
查找算法:
顺序查找
二分查找(适用于有序数组)
动态规划:
常见问题:斐波那契数列、背包问题、最长公共子序列(LCS)
思想:拆分子问题,记忆化搜索
贪心算法:
适用场景:区间覆盖问题、最优装载问题
回溯法:
经典问题:N皇后、迷宫问题、全排列问题
5. 面经(一些自己遇到的问题)
1、C++ 11的新特性
自动类型推导(auto):不写类型,编译器会判断。写 auto x = 5;编译器会知道 x 是个整数。
范围 for 循环:让遍历容器(像数组、列表)变得简单。例如,for (auto item : myList) 会自动取出 myList 中的每个元素。
智能指针:自动管理内存,不需要手动释放内存,编译器会在合适的时候帮助释放。
4. C++的运算符重载
运算符重载允许用户定义运算符的行为。通过重载运算符,可以使用户自定义的类型支持常用运算符,如 +, -, *, /, 等。重载需要定义一个成员函数或友元函数,并实现相应的逻辑。
5. 堆栈溢出的可能情况
堆栈溢出通常发生在递归调用没有适当终止条件时,或者分配了过大的局部变量(如大数组)。系统无法为局部变量提供足够的栈空间,从而导致溢出。
6. 如何对进程进行内存分配和管理
进程的内存分配通常使用系统调用,如 malloc、free (在 C/C++ 中) 或 VirtualAlloc、VirtualFree (在 Windows 中)。操作系统负责管理虚拟内存,通过分页和段式管理策略来优化内存使用。
7. 如何查到进程的所有子进程
在 Linux 中,可以使用 /proc/[pid]/task/[pid]/children 文件查看进程的所有子进程。在 Windows 中,可以使用 CreateToolhelp32Snapshot 函数结合 Process32First 和 Process32Next 来列出子进程。
8. 父进程怎么解决僵尸进程,如何避免?
父进程可以通过调用 wait() 或 waitpid() 函数来清理僵尸进程,以获取其退出状态。避免僵尸进程的方法是确保父进程及时回收子进程的退出状态,或设置信号处理程序以捕捉 SIGCHLD 信号。
9. 进程和线程有什么区别
进程是资源分配的基本单位,具有独立的地址空间、代码、数据和系统资源。
线程是执行的基本单位,属于进程,多个线程共享同一进程的资源,能更高效地进行并发处理。
特性 | 进程 | 线程 |
定义 | 程序的独立执行实例,拥有独立的内存空间。 | 进程内的执行流,线程共享进程的资源。 |
通信 | 进程间通信(IPC)复杂,如管道、共享内存等。 | 线程间通信简单,直接访问共享变量。 |
切换开销 | 高,涉及到内存切换和操作系统资源分配。 | 低,只需保存和恢复寄存器和栈指针。 |
失败影响范围 | 一个进程的失败不会影响其他进程。 | 一个线程的崩溃可能导致整个进程崩溃。 |
10. 深拷贝和浅拷贝的区别
浅拷贝: 复制对象的基本数据类型成员和指针,指针指向同一内存地址。
深拷贝: 复制对象的所有成员,指针成员指向新的内存地址,确保独立性。
11. 内存泄漏的原因以及处理的方法,测试怎么查询出来内存泄露
内存泄漏通常由于程序在动态分配内存后未能适时释放(如使用 new 而未 delete),或者失去了对分配内存的引用(如指针被覆盖或超出作用域)导致无法访问和释放。
12. 虚函数实现原理
虚函数通过虚函数表(vtable)和虚函数指针(vptr)实现动态绑定。每个包含虚函数的类都有一个 vtable,包含其虚函数的地址。对象包含一个指向其类的 vtable 的 vptr,调用虚函数时,通过 vptr 查找 vtable 实现相应函数。
13. 为什么需要资源管理与锁机制?
共享资源冲突:多个线程访问和修改同一数据时,可能出现数据竞争。比如银行账户余额在多个线程间更新时,可能导致错误余额。锁是用于保护共享资源的机制,确保同一时刻只有一个线程可以访问资源。
原文地址:https://blog.csdn.net/qq_44117805/article/details/143490222
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!